<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; yum</title>
	<atom:link href="http://tonybai.com/tag/yum/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Fri, 17 Apr 2026 00:21:29 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go编译的几个细节，连专家也要停下来想想</title>
		<link>https://tonybai.com/2024/11/11/some-details-about-go-compilation/</link>
		<comments>https://tonybai.com/2024/11/11/some-details-about-go-compilation/#comments</comments>
		<pubDate>Sun, 10 Nov 2024 22:13:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[archive]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CFLAGS]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[DWARF]]></category>
		<category><![CDATA[expvar]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[getaddrinfo]]></category>
		<category><![CDATA[getnameinfo]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[glibc-static]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-sqlite3]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golist]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[helloworld]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[inittask]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[ldflags]]></category>
		<category><![CDATA[LD_LIBRARY_PATH]]></category>
		<category><![CDATA[libresolv.so]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[NameResolution]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[sqlite-devel]]></category>
		<category><![CDATA[sqlite3]]></category>
		<category><![CDATA[TinyGo]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[内联]]></category>
		<category><![CDATA[动态链接]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[死码消除]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[静态链接]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4383</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation 在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。 注：本文示例使用的环境为Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。 1. Go编译默认采用静态链接还是动态链接？ 我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？ 很多人脱口而出：动态链接，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？ 我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点： $go env&#124;grep CGO_ENABLED CGO_ENABLED='1' 验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可： // go-compilation/main.go package main import "fmt" func main() { fmt.Println("hello, world") } 构建该程序： $go build -o helloworld-default main.go 之后，我们查看一下生成的可执行文件helloworld-default的文件属性： $file helloworld-default helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ldd [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/some-details-about-go-compilation-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/11/11/some-details-about-go-compilation">本文永久链接</a> &#8211; https://tonybai.com/2024/11/11/some-details-about-go-compilation</p>
<p>在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。</p>
<blockquote>
<p>注：本文示例使用的环境为<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23/">Go 1.23.0</a>、Linux Kernel 3.10.0和CentOS 7.9。</p>
</blockquote>
<h2>1. Go编译默认采用静态链接还是动态链接？</h2>
<p>我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？</p>
<p>很多人脱口而出：<a href="https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/">动态链接</a>，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？</p>
<p>我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点：</p>
<pre><code>$go env|grep CGO_ENABLED
CGO_ENABLED='1'
</code></pre>
<p>验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可：</p>
<pre><code>// go-compilation/main.go

package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>构建该程序：</p>
<pre><code>$go build -o helloworld-default main.go
</code></pre>
<p>之后，我们查看一下生成的可执行文件helloworld-default的文件属性：</p>
<pre><code>$file helloworld-default
helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-default
   不是动态可执行文件
</code></pre>
<p>我们看到，虽然CGO_ENABLED=1，但默认情况下，Go构建出的helloworld程序是静态链接的(statically linked)。</p>
<p>那么默认情况下，Go编译器是否都会采用静态链接的方式来构建Go程序呢？我们给上面的main.go添加一行代码：</p>
<pre><code>// go-compilation/main-with-os-user.go

package main

import (
    "fmt"
    _ "os/user"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>和之前的hello, world不同的是，这段代码多了一行<strong>包的空导入</strong>，导入的是os/user这个包。</p>
<p>编译这段代码，我们得到helloworld-with-os-user可执行文件。</p>
<pre><code>$go build -o helloworld-with-os-user main-with-os-user.go
</code></pre>
<p>使用file和ldd检视文件helloworld-with-os-user：</p>
<pre><code>$file helloworld-with-os-user
helloworld-with-os-user: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-os-user
    linux-vdso.so.1 =&gt;  (0x00007ffcb8fd4000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fb5d6fce000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fb5d6c00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb5d71ea000)
</code></pre>
<p>我们看到：<strong>一行新代码居然让helloworld从静态链接变为了动态链接</strong>，同时这也是如何编译出一个hello world版的动态链接Go程序的答案。</p>
<p>通过nm命令我们还可以查看Go程序依赖了哪些C库的符号：</p>
<pre><code>$nm -a helloworld-with-os-user |grep " U "
                 U abort
                 U __errno_location
                 U fprintf
                 U fputc
                 U free
                 U fwrite
                 U malloc
                 U mmap
                 U munmap
                 U nanosleep
                 U pthread_attr_destroy
                 U pthread_attr_getstack
                 U pthread_attr_getstacksize
                 U pthread_attr_init
                 U pthread_cond_broadcast
                 U pthread_cond_wait
                 U pthread_create
                 U pthread_detach
                 U pthread_getattr_np
                 U pthread_key_create
                 U pthread_mutex_lock
                 U pthread_mutex_unlock
                 U pthread_self
                 U pthread_setspecific
                 U pthread_sigmask
                 U setenv
                 U sigaction
                 U sigaddset
                 U sigemptyset
                 U sigfillset
                 U sigismember
                 U stderr
                 U strerror
                 U unsetenv
                 U vfprintf
</code></pre>
<p>由此，我们可以得到一个结论，在默认情况下(CGO_ENABLED=1)，Go会尽力使用静态链接的方式，但在某些情况下，会采用动态链接。那么究竟在哪些情况下会默认生成动态链接的程序呢？我们继续往下看。</p>
<h2>2. 在何种情况下默认会生成动态链接的Go程序？</h2>
<p>在以下几种情况下，Go编译器会默认(CGO_ENABLED=1)生成动态链接的可执行文件，我们逐一来看一下。</p>
<h3>2.1 一些使用C实现的标准库包</h3>
<p>根据上述示例，我们可以看到，在某些情况下，即使只依赖标准库，Go 仍会在CGO_ENABLED=1的情况下采用动态链接。这是因为代码依赖的标准库包使用了C版本的实现。虽然这种情况并不常见，但<a href="https://pkg.go.dev/os/user">os/user包</a>和<a href="https://pkg.go.dev/net">net包</a>是两个典型的例子。</p>
<p>os/user包的示例在前面我们已经见识过了。user包允许开发者通过名称或ID查找用户账户。对于大多数Unix系统(包括linux)，该包内部有两种版本的实现，用于解析用户和组ID到名称，并列出附加组ID。一种是用纯Go编写，解析/etc/passwd和/etc/group文件。另一种是基于cgo的，依赖于标准C库（libc）中的例程，如getpwuid_r、getgrnam_r和getgrouplist。当cgo可用(CGO_ENABLED=1)，并且特定平台的libc实现了所需的例程时，将使用基于cgo的（libc支持的）代码，即采用动态链接方式。</p>
<p>同样，net包在名称解析(Name Resolution，即域名或主机名对应IP查找)上针对大多数Unix系统也有两个版本的实现：一个是纯Go版本，另一个是基于C的版本。C版本会在cgo可用且特定平台实现了相关C函数(比如getaddrinfo和getnameinfo等)时使用。</p>
<p>下面是一个简单的使用net包并采用动态链接的示例：</p>
<pre><code>// go-compilation/main-with-net.go

package main

import (
    "fmt"
    _ "net"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>编译后，我们查看一下文件属性：</p>
<pre><code>$go build -o helloworld-with-net main-with-net.go 

$file helloworld-with-net
helloworld-with-net: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-net
    linux-vdso.so.1 =&gt;  (0x00007ffd75dfd000)
    libresolv.so.2 =&gt; /lib64/libresolv.so.2 (0x00007fdda2cf9000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007fdda2add000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007fdda270f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdda2f13000)
</code></pre>
<p>我们看到C版本实现依赖了libresolv.so这个用于名称解析的C库。</p>
<p>由此可得，当Go在默认cgo开启时，一旦依赖了标准库中拥有C版本实现的包，比如os/user、net等，Go编译器会采用动态链接的方式编译Go可执行程序。</p>
<h3>2.2 显式使用cgo调用外部C程序</h3>
<p>如果使用cgo与外部C代码交互，那么生成的可执行文件必然会包含动态链接。下面我们来看一个调用cgo的简单示例。</p>
<p>首先，建立一个简单的C lib：</p>
<pre><code>// go-compilation/my-c-lib

$tree my-c-lib
my-c-lib
├── Makefile
├── mylib.c
└── mylib.h

// go-compilation/my-c-lib/Makefile

.PHONY:  all static

all:
        gcc -c -fPIC -o mylib.o mylib.c
        gcc -shared -o libmylib.so mylib.o
static:
        gcc -c -fPIC -o mylib.o mylib.c
        ar rcs libmylib.a mylib.o

// go-compilation/my-c-lib/mylib.h

#ifndef MYLIB_H
#define MYLIB_H

void hello();
int add(int a, int b);

#endif // MYLIB_H

// go-compilation/my-c-lib/mylib.c

#include &lt;stdio.h&gt;

void hello() {
    printf("Hello from C!\n");
}

int add(int a, int b) {
    return a + b;
}
</code></pre>
<p>执行make all构建出动态链接库libmylib.so！接下来，我们编写一个Go程序通过cgo调用libmylib.so中：</p>
<pre><code>// go-compilation/main-with-call-myclib.go 

package main

/*
#cgo CFLAGS: -I ./my-c-lib
#cgo LDFLAGS: -L ./my-c-lib -lmylib
#include "mylib.h"
*/
import "C"
import "fmt"

func main() {
    // 调用 C 函数
    C.hello()

    // 调用 C 中的加法函数
    result := C.add(3, 4)
    fmt.Printf("Result of addition: %d\n", result)
}
</code></pre>
<p>编译该源码：</p>
<pre><code>$go build -o helloworld-with-call-myclib main-with-call-myclib.go
</code></pre>
<p>通过ldd可以看到，可执行文件helloworld-with-call-myclib是动态链接的，并依赖libmylib.so：</p>
<pre><code>$ldd helloworld-with-call-myclib
    linux-vdso.so.1 =&gt;  (0x00007ffcc39d8000)
    libmylib.so =&gt; not found
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007f7166df5000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007f7166a27000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f7167011000)
</code></pre>
<p>设置LD_LIBRARY_PATH(为了让程序找到libmylib.so)并运行可执行文件helloworld-with-call-myclib：</p>
<pre><code>$ LD_LIBRARY_PATH=./my-c-lib:$LD_LIBRARY_PATH ./helloworld-with-call-myclib
Hello from C!
Result of addition: 7
</code></pre>
<h3>2.3 使用了依赖cgo的第三方包</h3>
<p>在日常开发中，我们经常依赖一些第三方包，有些时候这些第三方包依赖cgo，比如<a href="https://github.com/mattn/go-sqlite3">mattn/go-sqlite3</a>。下面就是一个依赖go-sqlite3包的示例：</p>
<pre><code>// go-compilation/go-sqlite3/main.go
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // 打开数据库（如果不存在，则创建）
    db, err := sql.Open("sqlite3", "./test.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 创建表
    sqlStmt := `CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);`
    _, err = db.Exec(sqlStmt)
    if err != nil {
        log.Fatalf("%q: %s\n", err, sqlStmt)
    }

    // 插入数据
    _, err = db.Exec(`INSERT INTO user (name) VALUES (?)`, "Alice")
    if err != nil {
        log.Fatal(err)
    }

    // 查询数据
    rows, err := db.Query(`SELECT id, name FROM user;`)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        err = rows.Scan(&amp;id, &amp;name)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%d: %s\n", id, name)
    }

    // 检查查询中的错误
    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>编译和运行该源码：</p>
<pre><code>$go build demo
$ldd demo
    linux-vdso.so.1 =&gt;  (0x00007ffe23d8e000)
    libdl.so.2 =&gt; /lib64/libdl.so.2 (0x00007faf0ddef000)
    libpthread.so.0 =&gt; /lib64/libpthread.so.0 (0x00007faf0dbd3000)
    libc.so.6 =&gt; /lib64/libc.so.6 (0x00007faf0d805000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faf0dff3000)
$./demo
1: Alice
</code></pre>
<p>到这里，有些读者可能会问一个问题：如果需要在上述依赖场景中生成静态链接的Go程序，该怎么做呢？接下来，我们就来看看这个问题的解决细节。</p>
<h2>3. 如何在上述情况下实现静态链接？</h2>
<p>到这里是不是有些烧脑了啊！我们针对上一节的三种情况，分别对应来看一下静态编译的方案。</p>
<h3>3.1 仅依赖标准包</h3>
<p>在前面我们说过，之所以在使用os/user、net包时会在默认情况下采用动态链接，是因为Go使用了这两个包对应功能的C版实现，如果要做静态编译，让Go编译器选择它们的纯Go版实现即可。那我们仅需要关闭CGO即可，以依赖标准库os/user为例：</p>
<pre><code>$CGO_ENABLED=0 go build -o helloworld-with-os-user-static main-with-os-user.go
$file helloworld-with-os-user-static
helloworld-with-os-user-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-with-os-user-static
    不是动态可执行文件
</code></pre>
<h3>3.2 使用cgo调用外部c程序（静态链接）</h3>
<p>对于依赖cgo调用外部c的程序，我们要使用静态链接就必须要求外部c库提供静态库，因此，我们需要my-c-lib提供一份libmylib.a，这通过下面命令可以实现(或执行make static)：</p>
<pre><code>$gcc -c -fPIC -o mylib.o mylib.c
$ar rcs libmylib.a mylib.o
</code></pre>
<p>有了libmylib.a后，我们还要让Go程序静态链接该.a文件，于是我们需要修改一下Go源码中cgo链接的flag，加上静态链接的选项：</p>
<pre><code>// go-compilation/main-with-call-myclib-static.go
... ...
#cgo LDFLAGS: -static -L my-c-lib -lmylib
... ...
</code></pre>
<p>编译链接并查看一下文件属性：</p>
<pre><code>$go build -o helloworld-with-call-myclib-static main-with-call-myclib-static.go

$file helloworld-with-call-myclib-static
helloworld-with-call-myclib-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=b3da3ed817d0d04230460069b048cab5f5bfc3b9, not stripped
</code></pre>
<p>我们得到了预期的结果！</p>
<h3>3.3 依赖使用cgo的外部go包（静态链接）</h3>
<p>最麻烦的是这类情况，要想实现静态链接，我们需要找出外部go依赖的所有c库的.a文件(静态共享库)。以我们的go-sqlite3示例为例，go-sqlite3是sqlite库的go binding，它依赖sqlite库，同时所有第三方c库都依赖libc，我们还要准备一份libc的.a文件，下面我们就先安装这些：</p>
<pre><code>$yum install -y gcc glibc-static sqlite-devel
... ...

已安装:
  sqlite-devel.x86_64 0:3.7.17-8.el7_7.1                                                                                          

更新完毕:
  glibc-static.x86_64 0:2.17-326.el7_9.3
</code></pre>
<p>接下来，我们就来以静态链接的方式在go-compilation/go-sqlite3-static下编译一下：</p>
<pre><code>$go build -tags 'sqlite_omit_load_extension' -ldflags '-linkmode external -extldflags "-static"' demo

$file ./demo
./demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c779f5c3eaa945d916de059b56d94c23974ce61c, not stripped
</code></pre>
<p>这里命令行中的-tags &#8216;sqlite_omit_load_extension&#8217;用于禁用SQLite3的动态加载功能，确保更好的静态链接兼容性。而-ldflags &#8216;-linkmode external -extldflags “-static”&#8216;的含义是使用外部链接器(比如gcc linker)，并强制静态链接所有库。</p>
<p>我们再看完略烧脑的几个细节后，再来看一个略轻松的话题。</p>
<h2>4. Go编译出的可执行文件过大，能优化吗？</h2>
<p>Go编译出的二进制文件一般较大，一个简单的“Hello World”程序通常在2MB左右：</p>
<pre><code>$ls -lh helloworld-default
-rwxr-xr-x 1 root root 2.1M 11月  3 10:39 helloworld-default
</code></pre>
<p>这一方面是因为Go将整个runtime都编译到可执行文件中了，另一方面也是因为Go静态编译所致。那么在默认情况下，Go二进制文件的大小还有优化空间么？方法不多，有两种可以尝试：</p>
<ul>
<li>去除符号表和调试信息</li>
</ul>
<p>在编译时使用-ldflags=”-s -w”标志可以去除符号表和调试符号，其中-s用于去掉符号表和调试信息，-w用于去掉DWARF调试信息，这样能显著减小文件体积。以helloworld为例，可执行文件的size减少了近四成：</p>
<pre><code>$go build -ldflags="-s -w" -o helloworld-default-nosym main.go
$ls -l
-rwxr-xr-x 1 root root 2124504 11月  3 10:39 helloworld-default
-rwxr-xr-x 1 root root 1384600 11月  3 13:34 helloworld-default-nosym
</code></pre>
<ul>
<li>使用tinygo</li>
</ul>
<p><a href="https://github.com/tinygo-org/tinygo/">TinyGo</a>是一个Go语言的编译器，它专为资源受限的环境而设计，例如微控制器、WebAssembly和其他嵌入式设备。TinyGo的目标是提供一个轻量级的、能在小型设备上运行的Go运行时，同时尽可能支持Go语言的特性。tinygo的一大优点就是生成的二进制文件通常比标准Go编译器生成的文件小得多：</p>
<pre><code>$tinygo build -o helloworld-tinygo main.go
$ls -l
总用量 2728
-rwxr-xr-x  1 root root 2128909 11月  5 05:43 helloworld-default*
-rwxr-xr-x  1 root root  647600 11月  5 05:45 helloworld-tinygo*
</code></pre>
<p>我们看到：tinygo生成的可执行文件的size仅是原来的30%。</p>
<blockquote>
<p>注：虽然TinyGo在特定场景（如IoT和嵌入式开发）中非常有用，但在常规服务器环境中，由于生态系统兼容性、性能、调试支持等方面的限制，可能并不是最佳选择。对于需要高并发、复杂功能和良好调试支持的应用，标准Go仍然是更合适的选择。</p>
<p>注：这里使用的tinygo为0.34.0版本。</p>
</blockquote>
<h2>5. 未使用的符号是否会被编译到Go二进制文件中？</h2>
<p>到这里，相信读者心中也都会萦绕一些问题：到底哪些符号被编译到最终的Go二进制文件中了呢？未使用的符号是否会被编译到Go二进制文件中吗？在这一小节中，我们就来探索一下。</p>
<p>出于对Go的了解，我们已经知道无论是GOPATH时代，还是Go module时代，Go的编译单元始终是包(package)，一个包（无论包中包含多少个Go源文件）都会作为一个编译单元被编译为一个目标文件(.a)，然后Go链接器会将多个目标文件链接在一起生成可执行文件，因此如果一个包被依赖，那么它就会进入到Go二进制文件中，它内部的符号也会进入到Go二进制文件中。</p>
<p>那么问题来了！是否被依赖包中的所有符号都会被放到最终的可执行文件中呢？我们以最简单的helloworld-default为例，它依赖fmt包，并调用了fmt包的Println函数，我们看看Println这个符号是否会出现在最终的可执行文件中：</p>
<pre><code>$nm -a helloworld-default | grep "Println"
000000000048eba0 T fmt.(*pp).doPrintln
</code></pre>
<p>居然没有！我们初步怀疑是inline优化在作祟。接下来，关闭优化再来试试：</p>
<pre><code>$go build -o helloworld-default-noinline -gcflags='-l -N' main.go

$nm -a helloworld-default-noinline | grep "Println"
000000000048ec00 T fmt.(*pp).doPrintln
0000000000489ee0 T fmt.Println
</code></pre>
<p>看来的确如此！不过当使用”fmt.”去过滤helloworld-default-noinline的所有符号时，我们发现fmt包的一些常见的符号并未包含在其中，比如Printf、Fprintf、Scanf等。</p>
<p>这是因为Go编译器的一个重要特性：死码消除(dead code elimination)，即编译器会将未使用的代码和数据从最终的二进制文件中剔除。</p>
<p>我们再来继续探讨一个衍生问题：如果Go源码使用空导入方式导入了一个包，那么这个包是否会被编译到Go二进制文件中呢？其实道理是一样的，如果用到了里面的符号，就会存在，否则不会。</p>
<p>以空导入os/user为例，即便在CGO_ENABLED=0的情况下，因为没有使用os/user中的任何符号，在最终的二进制文件中也不会包含user包：</p>
<pre><code>$CGO_ENABLED=0 go build -o helloworld-with-os-user-noinline -gcflags='-l -N' main-with-os-user.go
[root@iZ2ze18rmx2avqb5xgb4omZ helloworld]# nm -a helloworld-with-os-user-noinline |grep user
0000000000551ac0 B runtime.userArenaState
</code></pre>
<p>但是如果是带有init函数的包，且init函数中调用了同包其他符号的情况呢？我们以expvar包为例看一下：</p>
<pre><code>// go-compilation/main-with-expvar.go

package main

import (
    _ "expvar"
    "fmt"
)

func main() {
    fmt.Println("hello, world")
}
</code></pre>
<p>编译并查看一下其中的符号：</p>
<pre><code>$go build -o helloworld-with-expvar-noinline -gcflags='-l -N' main-with-expvar.go
$nm -a helloworld-with-expvar-noinline|grep expvar
0000000000556480 T expvar.appendJSONQuote
00000000005562e0 T expvar.cmdline
00000000005561c0 T expvar.expvarHandler
00000000005568e0 T expvar.(*Func).String
0000000000555ee0 T expvar.Func.String
00000000005563a0 T expvar.init.0
00000000006e0560 D expvar..inittask
0000000000704550 d expvar..interfaceSwitch.0
... ...
</code></pre>
<p>除此之外，如果一个包即便没有init函数，但有需要初始化的全局变量，比如crypto包的hashes：</p>
<pre><code>// $GOROOT/src/crypto/crypto.go
var hashes = make([]func() hash.Hash, maxHash)
</code></pre>
<p>crypto包的相关如何也会进入最终的可执行文件中，大家自己动手不妨试试。下面是我得到的一些输出：</p>
<pre><code>$go build -o helloworld-with-crypto-noinline -gcflags='-l -N' main-with-crypto.go
$nm -a helloworld-with-crypto-noinline|grep crypto
00000000005517b0 B crypto.hashes
000000000048ee60 T crypto.init
0000000000547280 D crypto..inittask
</code></pre>
<p>有人会问：os/user包也有一些全局变量啊，为什么这些符号没有被包含在可执行文件中呢？比如：</p>
<pre><code>// $GOROOT/src/os/user/user.go
var (
    userImplemented      = true
    groupImplemented     = true
    groupListImplemented = true
)
</code></pre>
<p>这就要涉及Go包初始化的逻辑了。我们看到crypto包包含在可执行文件中的符号中有crypto.init和crypto..inittask这两个符号，显然这不是crypto包代码中的符号，而是Go编译器为crypto包自动生成的init函数和inittask结构。</p>
<p>Go编译器会为每个包生成一个init函数，即使包中没有显式定义init函数，同时<a href="https://go.dev/src/cmd/compile/internal/pkginit/init.go">每个包都会有一个inittask结构</a>，用于运行时的包初始化系统。当然这么说也不足够精确，如果一个包没有init函数、需要初始化的全局变量或其他需要运行时初始化的内容，则编译器不会为其生成init函数和inittask。比如上面的os/user包。</p>
<p>os/user包确实有上述全局变量的定义，但是这些变量是在编译期就可以确定值的常量布尔值，而且未被包外引用或在包内用于影响控制流。Go编译器足够智能，能够判断出这些初始化是”无副作用的”，不需要在运行时进行初始化。只有真正需要运行时初始化的包才会生成init和inittask。这也解释了为什么空导入os/user包时没有相关的init和inittask符号，而crypto、expvar包有的init.0和inittask符号。</p>
<h2>6. 如何快速判断Go项目是否依赖cgo？</h2>
<p>在使用开源Go项目时，我们经常会遇到项目文档中没有明确说明是否依赖Cgo的情况。这种情况下，如果我们需要在特定环境（比如CGO_ENABLED=0）下使用该项目，就需要事先判断项目是否依赖Cgo，有些时候还要快速地给出判断。</p>
<p>那究竟是否可以做到这种快速判断呢？我们先来看看一些常见的作法。</p>
<p>第一类作法是源码层面的静态分析。最直接的方式是检查源码中是否存在import “C”语句，这种引入方式是CGO使用的显著标志。</p>
<pre><code>// 在项目根目录中执行
$grep -rn 'import "C"' .
</code></pre>
<p>这个命令会递归搜索当前目录下所有文件，显示包含import “C”的行号和文件路径，帮助快速定位CGO的使用位置。</p>
<p>此外，CGO项目通常包含特殊的编译指令，这些指令以注释形式出现在源码中，比如前面见识过的#cgo CFLAGS、#cgo LDFLAGS等，通过对这些编译指令的检测，同样可以来判断项目是否依赖CGO。</p>
<p>不过第一类作法并不能查找出Go项目的依赖包是否依赖cgo。而找出直接依赖或间接依赖是否依赖cgo，我们需要工具帮忙，比如使用Go工具链提供的命令分析项目依赖：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
</code></pre>
<p>其中ImportPath是依赖包的导入路径，而CgoFiles则是依赖中包含import “C”的Go源文件。我们以go-sqlite3那个依赖cgo的示例来验证一下：</p>
<pre><code>// cd go-compilation/go-sqlite3

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
runtime/cgo: [cgo.go]
github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go]
</code></pre>
<p>用空导入os/user的示例再来看一下：</p>
<pre><code>$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}'  main-with-os-user.go | grep -v '\[\]'
runtime/cgo: [cgo.go]
os/user: [cgo_lookup_cgo.go getgrouplist_unix.go]
</code></pre>
<p>我们知道os/user有纯go和C版本两个实现，因此上述判断只能说“对了一半”，当我关闭CGO_ENABLED时，Go编译器不会使用基于cgo的C版实现。</p>
<p>那是否在禁用cgo的前提下对源码进行一次编译便能验证项目是否对cgo有依赖呢？这样做显然谈不上是一种“快速”的方法，那是否有效呢？我们来对上面的go-sqlite3项目做一个测试，我们在关闭CGO_ENABLED时，编译一下该示例：</p>
<pre><code>// cd go-compilation/go-sqlite3
$ CGO_ENABLED=0 go build demo
</code></pre>
<p>我们看到，Go编译器并未报错！似乎该项目不需要cgo!  但真的是这样吗？我们运行一下编译后的demo可执行文件：</p>
<pre><code>$ ./demo
2024/11/03 22:10:36 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);
</code></pre>
<p>我们看到成功编译出来的程序居然出现运行时错误，提示需要cgo！</p>
<p>到这里，没有一种方法可以快速、精确的给出项目是否依赖cgo的判断。也许判断Go项目是否依赖CGO并没有捷径，需要从源码分析、依赖检查和构建测试等多个维度进行。</p>
<h2>7. 小结</h2>
<p>在本文中，我们深入探讨了Go语言编译过程中的几个重要细节，尤其是在静态链接和动态链接的选择上。通过具体示例，我们了解到：</p>
<ul>
<li>
<p>默认链接方式：尽管CGO_ENABLED默认值为1，Go编译器在大多数情况下会采用静态链接，只有在依赖特定的C库或标准库包时，才会切换到动态链接。</p>
</li>
<li>
<p>动态链接的条件：我们讨论了几种情况下Go会默认生成动态链接的可执行文件，包括依赖使用C实现的标准库包、显式使用cgo调用外部C程序，以及使用依赖cgo的第三方包。</p>
</li>
<li>
<p>实现静态链接：对于需要动态链接的场景，我们也提供了将其转为静态链接的解决方案，包括关闭CGO、使用静态库，以及处理依赖cgo的外部包的静态链接问题。</p>
</li>
<li>
<p>二进制文件优化：我们还介绍了如何通过去除符号表和使用TinyGo等方法来优化生成的Go二进制文件的大小，以满足不同场景下的需求。</p>
</li>
<li>
<p>符号编译与死码消除：最后，我们探讨了未使用的符号是否会被编译到最终的二进制文件中，并解释了Go编译器的死码消除机制。</p>
</li>
</ul>
<p>通过这些细节探讨，我希望能够帮助大家更好地理解Go编译的复杂性，并在实际开发中做出更明智的选择，亦能在面对Go编译相关问题时，提供有效的解决方案。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go-compilation">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/11/11/some-details-about-go-compilation/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解API网关的主要功能特性</title>
		<link>https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example/</link>
		<comments>https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example/#comments</comments>
		<pubDate>Sun, 03 Dec 2023 09:35:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[37signals]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[Analytics]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[API-Gateway]]></category>
		<category><![CDATA[API-Managemnet]]></category>
		<category><![CDATA[APIDocumentation]]></category>
		<category><![CDATA[APIGateway]]></category>
		<category><![CDATA[APIGatewayPerformanceOptimization]]></category>
		<category><![CDATA[APIGatewayPlugins]]></category>
		<category><![CDATA[APIGatewayPolicies]]></category>
		<category><![CDATA[APIGovernance]]></category>
		<category><![CDATA[APIHealthChecks]]></category>
		<category><![CDATA[APILifecycleManagement]]></category>
		<category><![CDATA[APIMocking]]></category>
		<category><![CDATA[APIMonetization]]></category>
		<category><![CDATA[APIProxy]]></category>
		<category><![CDATA[APIRequestLogging]]></category>
		<category><![CDATA[APIRequestThrottling]]></category>
		<category><![CDATA[APIRequestTransformation]]></category>
		<category><![CDATA[APIRequestValidation]]></category>
		<category><![CDATA[APIResponseCaching]]></category>
		<category><![CDATA[APIResponseLogging]]></category>
		<category><![CDATA[APIResponseTransformation]]></category>
		<category><![CDATA[APIResponseValidation]]></category>
		<category><![CDATA[APISecurityScanning]]></category>
		<category><![CDATA[APITesting]]></category>
		<category><![CDATA[APITransformation]]></category>
		<category><![CDATA[APIVersioning]]></category>
		<category><![CDATA[API管理]]></category>
		<category><![CDATA[API网关]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[Caching]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[circuitbreaker]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[CORS(Cross-OriginResourceSharing)]]></category>
		<category><![CDATA[DeveloperPortal]]></category>
		<category><![CDATA[DHH]]></category>
		<category><![CDATA[ErrorHandling]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[JWT(JSONWebTokens)]]></category>
		<category><![CDATA[KONG]]></category>
		<category><![CDATA[lb]]></category>
		<category><![CDATA[LoadBalancing]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[monitoring]]></category>
		<category><![CDATA[OAuth]]></category>
		<category><![CDATA[OpenIDConnect]]></category>
		<category><![CDATA[RateLimiting]]></category>
		<category><![CDATA[redis]]></category>
		<category><![CDATA[RequestRouting]]></category>
		<category><![CDATA[RESTfulAPIs]]></category>
		<category><![CDATA[ROR]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[RubyOnRail]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[serverless]]></category>
		<category><![CDATA[ServiceComposition]]></category>
		<category><![CDATA[ServiceDiscovery]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[SOA]]></category>
		<category><![CDATA[SSL/TLS]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[TrafficManagement]]></category>
		<category><![CDATA[Transformation]]></category>
		<category><![CDATA[tyk]]></category>
		<category><![CDATA[WAF(WebApplicationFirewall)]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[云原生]]></category>
		<category><![CDATA[单体]]></category>
		<category><![CDATA[可观测]]></category>
		<category><![CDATA[微服务]]></category>
		<category><![CDATA[架构]]></category>
		<category><![CDATA[流量控制]]></category>
		<category><![CDATA[熔断]]></category>
		<category><![CDATA[监控]]></category>
		<category><![CDATA[负载均衡]]></category>
		<category><![CDATA[超时]]></category>
		<category><![CDATA[路由转发]]></category>
		<category><![CDATA[身份认证]]></category>
		<category><![CDATA[重试]]></category>
		<category><![CDATA[高可用]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4060</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example 在当今的技术领域中，“下云”的概念正逐渐抬头，像David Heinemeier Hansson(37signals公司的联合创始人, Ruby on Rails的Creator)就直接将公司所有的业务都从公有云搬迁到了自建的数据中心中。虽说大多数企业不会这么“极端”，但随着企业对云原生架构采用的广泛与深入，不可避免地面临着对云服务的依赖。云服务在过去的几年中被广泛应用于构建灵活、可扩展的应用程序和基础设施，为企业提供了许多便利和创新机会。然而，随着业务规模的增长和数据量的增加，云服务的成本也随之上升。企业开始意识到，对云服务的依赖已经成为一个值得重新评估的议题。云服务的开销可能占据了企业可用的预算的相当大部分。为了保持竞争力并更好地控制成本，企业需要寻找方法来减少对云服务的依赖，寻找更经济的解决方案，同时确保仍能获得所需的性能、安全性和可扩展性。 在这样的背景下，我们的关注点是选择一款适宜的API网关，从主流功能特性的角度来评估候选者的支持。API网关作为现代云原生应用架构中的关键组件，扮演着连接前端应用和后端服务的中间层，负责管理、控制和保护API的访问。它的功能特性对于确保API的安全性、可靠性和可扩展性至关重要。 尽管API网关并不是一个新鲜事物了，但对于那些长期依赖于云供应商的服务的人来说，它似乎变得有些“陌生”。因此，本文旨在帮助我们重新理解API网关的主要特性，并获得对API网关选型的能力，以便在停止使用云供应商服务之前，找到一个合适的替代品^_^。 1. API网关回顾 API网关是现代应用架构中的关键组件之一，它的存在简化了应用程序的架构，并为客户端提供一个单一的访问入口，并进行相关的控制、优化和管理。API网关可以帮助企业实现微服务架构、提高系统的可扩展性和安全性，并提供更好的开发者体验和用户体验。 1.1 API网关的演化 随着互联网的快速发展和企业对API的需求不断增长，API网关作为一种关键的中间层技术逐渐崭露头角并经历了一系列的演进和发展。这里将API网关的演进历史粗略分为以下几个阶段: API网关之前的早期阶段 在互联网发展的早期阶段，大多数应用程序都是以单体应用的形式存在。后来随着应用规模的扩大和业务复杂性的增加，单体应用的架构变得不够灵活和可扩展，面向服务架构（Service-Oriented Architecture，SOA）逐渐兴起，企业开始将应用程序拆分成一组独立的服务。这个时期，每个服务都是独立对外暴露API，客户端也是通过这些API直接访问服务，但这会导致一些安全性、运维和扩展性的问题。之后，企业也开始意识到需要一种中间层来管理和控制这种客户端到服务的通信行为，并确保服务的可靠性和安全性，于是开始有了API网关的概念。 API网关的兴起 早期的API网关，其主要功能就是单纯的路由和转发。API网关将请求从客户端转发到后端服务，并将后端服务的响应返回给客户端。在这个阶段，API网关的功能非常简单，主要用于解决客户端和后端服务之间的通信问题。 API网关的成熟 随着微服务架构的兴起和API应用的不断发展，企业开始将应用程序进一步拆分成更小的、独立部署的微服务。每个对外暴露的微服务都有自己的API，并通过API网关进行统一管理和访问。API网关在微服务架构中的作用变得更加重要，它的功能也逐渐丰富起来了。 在这一阶段，它不仅负责路由和转发请求，API网关还增加了安全和治理的功能，可以满足几个不同领域的微服务需求。比如：API网关可以通过身份认证、授权、访问控制等功能来保护API的安全；通过基于重试、超时、熔断的容错机制等来对API的访问进行治理；通过日志记录、基于指标收集以及Tracing等对API的访问进行观测与监控；支持实时的服务发现等。 API网关(图来自网络) API网关的云原生化 随着云原生技术的发展，如容器化和服务网格（Service Mesh）等，API网关也在不断演进和适应新的环境。在云原生环境中，API网关实现了与容器编排系统（如Kubernetes）和服务网格集成，其自身也可以作为一个云原生服务来部署，以实现更高的可伸缩性、弹性和自动化。同时，新的技术和标准也不断涌现，如GraphQL和gRPC等，API网关也增加了对这些新技术的集成和支持。 1.2 API网关的主要功能特性 从上面的演化历史我们看到：API网关的演进使其从最初简单的请求转发角色，逐渐成为整个API管理和微服务架构中的关键组件。它不仅扮演着API管理层与后端服务层之间的适配器，也是云原生架构中不可或缺的基础设施，使微服务管理更加智能化和自动化。下面是现代API网关承担的主要功能特性，我们后续也会基于这些特性进行示例说明： 请求转发和路由 身份认证和授权 流量控制和限速 高可用与容错处理 监控和可观测性 2. 那些主流的API网关 下面是来自CNCF Landscape中的主流API网关集合(截至2023.11月)，图中展示了关于各个网关的一些细节，包括star数量和背后开发的公司或组织： 主流的API网关还有各大公有云提供商的实现，比如：Amazon的API Gateway、Google Cloud的API Gateway以及上图中的Azure API Management等，但它们不在我们选择范围之内；虽然被CNCF收录，但多数API网关受到的关注并不高，超过1k star的不到30%，这些不是很受关注或dev不是那么active的项目也无法在生产环境担当关键角色；而像APISIX、Kong这两个受关注很高的网关，它们是建构在Nginx之上实现的，技术栈与我们不契合；而像EMISSARY INGRESS、Gloo等则是完全云原生化或者说是Kubernetes Native的，无法在无Kubernetes的基于VM或裸金属的环境下部署和运行。 好吧，剩下的只有几个Go实现的API Gateway了，在它们之中，我们选择用Tyk API网关来作为后续API功能演示的示例。 注：这并不代表Tyk API网关就要比其他Go实现的API [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example">本文永久链接</a> &#8211; https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example</p>
<p>在当今的技术领域中，“下云”的概念正逐渐抬头，像<a href="https://dhh.dk/">David Heinemeier Hansson</a>(37signals公司的联合创始人, Ruby on Rails的Creator)就直接<a href="https://37signals.com/podcast/leaving-the-cloud/">将公司所有的业务都从公有云搬迁到了自建的数据中心</a>中。虽说大多数企业不会这么“极端”，但随着企业对云原生架构采用的广泛与深入，不可避免地面临着对云服务的依赖。云服务在过去的几年中被广泛应用于构建灵活、可扩展的应用程序和基础设施，为企业提供了许多便利和创新机会。然而，随着业务规模的增长和数据量的增加，云服务的成本也随之上升。企业开始意识到，对云服务的依赖已经成为一个值得重新评估的议题。云服务的开销可能占据了企业可用的预算的相当大部分。为了保持竞争力并更好地控制成本，企业需要寻找方法来减少对云服务的依赖，寻找更经济的解决方案，同时确保仍能获得所需的性能、安全性和可扩展性。</p>
<p>在这样的背景下，我们的关注点是选择一款适宜的API网关，从主流功能特性的角度来评估候选者的支持。API网关作为现代云原生应用架构中的关键组件，扮演着连接前端应用和后端服务的中间层，负责管理、控制和保护API的访问。它的功能特性对于确保API的安全性、可靠性和可扩展性至关重要。</p>
<p>尽管API网关并不是一个新鲜事物了，但对于那些长期依赖于云供应商的服务的人来说，它似乎变得有些“陌生”。因此，本文旨在帮助我们重新理解API网关的主要特性，并获得对API网关选型的能力，以便在停止使用云供应商服务之前，找到一个合适的替代品^_^。</p>
<h2>1. API网关回顾</h2>
<p>API网关是现代应用架构中的关键组件之一，它的存在简化了应用程序的架构，并为客户端提供一个单一的访问入口，并进行相关的控制、优化和管理。API网关可以帮助企业实现微服务架构、提高系统的可扩展性和安全性，并提供更好的开发者体验和用户体验。</p>
<h2>1.1 API网关的演化</h2>
<p>随着互联网的快速发展和企业对API的需求不断增长，API网关作为一种关键的中间层技术逐渐崭露头角并经历了一系列的演进和发展。这里将API网关的演进历史粗略分为以下几个阶段:</p>
<ul>
<li>API网关之前的早期阶段</li>
</ul>
<p>在互联网发展的早期阶段，大多数应用程序都是<a href="https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices/">以单体应用的形式存在</a>。后来随着应用规模的扩大和业务复杂性的增加，单体应用的架构变得不够灵活和可扩展，面向服务架构（Service-Oriented Architecture，SOA）逐渐兴起，企业开始将应用程序拆分成一组独立的服务。这个时期，每个服务都是独立对外暴露API，客户端也是通过这些API直接访问服务，但这会导致一些安全性、运维和扩展性的问题。之后，企业也开始意识到需要一种中间层来管理和控制这种客户端到服务的通信行为，并确保服务的可靠性和安全性，于是开始有了API网关的概念。</p>
<ul>
<li>API网关的兴起</li>
</ul>
<p>早期的API网关，其主要功能就是单纯的路由和转发。API网关将请求从客户端转发到后端服务，并将后端服务的响应返回给客户端。在这个阶段，API网关的功能非常简单，主要用于解决客户端和后端服务之间的通信问题。</p>
<ul>
<li>API网关的成熟</li>
</ul>
<p>随着微服务架构的兴起和API应用的不断发展，企业开始将应用程序进一步拆分成更小的、独立部署的微服务。每个对外暴露的微服务都有自己的API，并通过API网关进行统一管理和访问。API网关在微服务架构中的作用变得更加重要，它的功能也逐渐丰富起来了。</p>
<p>在这一阶段，它不仅负责路由和转发请求，API网关还增加了安全和治理的功能，可以满足几个不同领域的微服务需求。比如：API网关可以通过身份认证、授权、访问控制等功能来保护API的安全；通过基于重试、超时、熔断的容错机制等来对API的访问进行治理；通过日志记录、基于指标收集以及Tracing等对API的访问进行观测与监控；支持实时的服务发现等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-3.png" alt="" /><br />
<center>API网关(图来自网络)</center></p>
<ul>
<li>API网关的云原生化</li>
</ul>
<p>随着云原生技术的发展，如容器化和服务网格（Service Mesh）等，API网关也在不断演进和适应新的环境。在云原生环境中，API网关实现了与容器编排系统（如Kubernetes）和服务网格集成，其自身也可以作为一个云原生服务来部署，以实现更高的可伸缩性、弹性和自动化。同时，新的技术和标准也不断涌现，如GraphQL和gRPC等，API网关也增加了对这些新技术的集成和支持。</p>
<h2>1.2 API网关的主要功能特性</h2>
<p>从上面的演化历史我们看到：API网关的演进使其从最初简单的请求转发角色，逐渐成为整个API管理和微服务架构中的关键组件。它不仅扮演着API管理层与后端服务层之间的适配器，也是云原生架构中不可或缺的基础设施，使微服务管理更加智能化和自动化。下面是现代API网关承担的主要功能特性，我们后续也会基于这些特性进行示例说明：</p>
<ul>
<li>请求转发和路由</li>
<li>身份认证和授权</li>
<li>流量控制和限速</li>
<li>高可用与容错处理</li>
<li>监控和可观测性</li>
</ul>
<h2>2. 那些主流的API网关</h2>
<p>下面是来自<a href="https://https://landscape.cncf.io">CNCF Landscape</a>中的主流API网关集合(截至2023.11月)，图中展示了关于各个网关的一些细节，包括star数量和背后开发的公司或组织：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-2.png" alt="" /></p>
<p>主流的API网关还有各大公有云提供商的实现，比如：<a href="https://aws.amazon.com/cn/api-gateway/">Amazon的API Gateway</a>、<a href="https://cloud.google.com/api-gateway">Google Cloud的API Gateway</a>以及上图中的Azure API Management等，但它们不在我们选择范围之内；虽然被CNCF收录，但多数API网关受到的关注并不高，超过1k star的不到30%，这些不是很受关注或dev不是那么active的项目也无法在生产环境担当关键角色；而像<a href="https://apisix.apache.org/">APISIX</a>、<a href="https://konghq.com/">Kong</a>这两个受关注很高的网关，它们是建构在Nginx之上实现的，技术栈与我们不契合；而像<a href="https://github.com/emissary-ingress/emissary">EMISSARY INGRESS</a>、Gloo等则是完全云原生化或者说是Kubernetes Native的，无法在无Kubernetes的基于VM或裸金属的环境下部署和运行。</p>
<p>好吧，剩下的只有几个Go实现的API Gateway了，在它们之中，我们选择用<a href="https://tyk.io/blog/res-api-management-vendor-comparisons/">Tyk API网关</a>来作为后续API功能演示的示例。</p>
<blockquote>
<p>注：这并<a href="https://tyk.io/blog/enter-the-leader-tyk-recognised-as-a-leader-in-gartners-2023-magic-quadrant-for-api-management/">不代表Tyk API网关就要比其他Go实现的API Gateway优秀</a>，只是它的资料比较齐全，适合在本文中作演示罢了。</p>
</blockquote>
<h2>3. API网关主要功能特性示例(Tyk API网关版本)</h2>
<h3>3.1 Tyk API网关简介</h3>
<p>记得在至少5年前就知道<a href="https://github.com/TykTechnologies/tyk">Tyk API网关</a>的存在，印象中它是使用Go语言开发的早期的那批API网关之一。Tyk从最初的纯开源项目，到如今由背后商业公司支持，以<a href="https://opensource.com/article/21/11/open-core-vs-open-source">Open Core模式开源</a>的网关，一直保持了active dev的状态。经过多年的演进，它已经一款功能强大的<a href="https://tyk.io/docs/tyk-oss-gateway/">开源兼商业API管理和网关解决方案</a>，提供了全面的功能和工具，帮助开发者有效地管理、保护和监控API。同时，Tyk API网关支持多种安装部署方式，即可以单一程序的方式放在物理机或VM上运行，也可以支持容器部署，通过<a href="https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose">docker-compose</a>拉起，亦可以通过<a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">Kubernetes Operator</a>将其部署在Kubernetes中，这也让Tyk API网关具备了在各大公有云上平滑迁移的能力。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-4.png" alt="" /></p>
<p>关于<a href="https://tyk.io/docs/tyk-oss-gateway/">Tyk API网关开源版本的功能详情</a>，可以点击左边超链接到其官网查阅，这里不赘述。</p>
<h3>3.2 安装Tyk API网关</h3>
<p>下面我们就来安装一下Tyk API网关，我们直接在VM上安装，VM上的环境是CentOS 7.9。Tyk API提供了很多中安装方法，这里<a href="https://tyk.io/docs/tyk-oss/ce-redhat-rhel-centos/">使用CentOS的yum包管理工具安装Tyk API网关</a>，大体步骤如下(演示均以root权限操作)。</p>
<h4>3.2.1 创建tyk gateway软件源</h4>
<p>默认的yum repo中是不包含tyk gateway的，我们需要在/etc/yum.repos.d下面创建一个新的源，即新建一个tyk_tyk-gateway.repo文件，其内容如下：</p>
<pre><code>[tyk_tyk-gateway]
name=tyk_tyk-gateway
baseurl=https://packagecloud.io/tyk/tyk-gateway/el/7/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/tyk/tyk-gateway/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300

[tyk_tyk-gateway-source]
name=tyk_tyk-gateway-source
baseurl=https://packagecloud.io/tyk/tyk-gateway/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/tyk/tyk-gateway/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
</code></pre>
<p>接下来我们执行下面命令来创建tyk_tyk-gateway这个repo的YUM缓存：</p>
<pre><code>$yum -q makecache -y --disablerepo='*' --enablerepo='tyk_tyk-gateway'
导入 GPG key 0x5FB83118:
 用户ID     : "https://packagecloud.io/tyk/tyk-gateway (https://packagecloud.io/docs#gpg_signing) &lt;support@packagecloud.io&gt;"
 指纹       : 9179 6215 a875 8c40 ab57 5f03 87be 71bd 5fb8 3118
 来自       : https://packagecloud.io/tyk/tyk-gateway/gpgkey
</code></pre>
<p>repo配置和缓存完毕后，我们就可以安装Tyk API Gateway了：</p>
<pre><code>$yum install -y tyk-gateway
</code></pre>
<p>安装后的tky-gateway将以一个<a href="https://tonybai.com/2016/12/27/when-docker-meets-systemd/">systemd daemon服务</a>的形式存在于主机上，程序意外退出或虚机重启后，该服务也会被systemd自动拉起。通过systemctl status命令可以查看服务的运行状态：</p>
<pre><code># systemctl status tyk-gateway
● tyk-gateway.service - Tyk API Gateway
   Loaded: loaded (/usr/lib/systemd/system/tyk-gateway.service; enabled; vendor preset: disabled)
   Active: active (running) since 日 2023-11-19 20:22:44 CST; 12min ago
 Main PID: 29306 (tyk)
    Tasks: 13
   Memory: 19.6M
   CGroup: /system.slice/tyk-gateway.service
           └─29306 /opt/tyk-gateway/tyk --conf /opt/tyk-gateway/tyk.conf

11月 19 20:34:54 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time="Nov 19 20:34:54" level=error msg="Connection to Redis faile...b-sub
11月 19 20:35:04 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time="Nov 19 20:35:04" level=error msg="cannot set key in pollerC...ured"
11月 19 20:35:04 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time="Nov 19 20:35:04" level=error msg="Redis health check failed...=main
Hint: Some lines were ellipsized, use -l to show in full.
</code></pre>
<h4>3.2.2 安装redis</h4>
<p>我们看到tyk-gateway已经成功启动，但从其服务日志来看，它在连接redis时报错了！tyk gateway默认将数据存储在redis中，为了让tyk gateway正常运行，我们还需要安装redis！这里我们使用容器的方式安装和运行一个redis服务：</p>
<pre><code>$docker pull redis:6.2.14-alpine3.18
$docker run -d --name my-redis -p 6379:6379 redis:6.2.14-alpine3.18
e5d1ec8d5f5c09023d1a4dd7d31d293b2d7147f1d9a01cff8eff077c93a9dab7
</code></pre>
<p>拉取并运行redis后，我们通过redis-cli验证一下与redis server的连接：</p>
<pre><code># docker run -it --rm redis:6.2.14-alpine3.18  redis-cli -h 192.168.0.24
192.168.0.24:6379&gt;
</code></pre>
<p>我们看到可以正常连接！但此时Tyk Gateway仍然无法与redis正常连接，我们还需要对Tyk Gateway做一些配置调整！</p>
<h4>3.2.3 配置Tyk Gateway</h4>
<p>yum默认将Tyk Gateway安装到/opt/tyk-gateway下面，这个路径下的文件布局如下：</p>
<pre><code>$tree -F -L 2 .
.
├── apps/
│   └── app_sample.json
├── coprocess/
│   ├── api.h
│   ├── bindings/
│   ├── coprocess_common.pb.go
│   ├── coprocess_mini_request_object.pb.go
│   ├── coprocess_object_grpc.pb.go
│   ├── coprocess_object.pb.go
│   ├── coprocess_response_object.pb.go
│   ├── coprocess_return_overrides.pb.go
│   ├── coprocess_session_state.pb.go
│   ├── coprocess_test.go
│   ├── dispatcher.go
│   ├── grpc/
│   ├── lua/
│   ├── proto/
│   ├── python/
│   └── README.md
├── event_handlers/
│   └── sample/
├── install/
│   ├── before_install.sh*
│   ├── data/
│   ├── init_local.sh
│   ├── inits/
│   ├── post_install.sh*
│   ├── post_remove.sh*
│   ├── post_trans.sh
│   └── setup.sh*
├── middleware/
│   ├── ottoAuthExample.js
│   ├── sampleMiddleware.js
│   ├── samplePostProcessMiddleware.js
│   ├── samplePreProcessMiddleware.js
│   ├── testPostVirtual.js
│   ├── testVirtual.js
│   └── waf.js
├── policies/
│   └── policies.json
├── templates/
│   ├── breaker_webhook.json
│   ├── default_webhook.json
│   ├── error.json
│   ├── monitor_template.json
│   └── playground/
├── tyk*
└── tyk.conf
</code></pre>
<p>其中tyk.conf就是tyk gateway的配置文件，我们先看看其默认的内容：</p>
<pre><code>$cat /opt/tyk-gateway/tyk.conf
{
  "listen_address": "",
  "listen_port": 8080,
  "secret": "xxxxxx",
  "template_path": "/opt/tyk-gateway/templates",
  "use_db_app_configs": false,
  "app_path": "/opt/tyk-gateway/apps",
  "middleware_path": "/opt/tyk-gateway/middleware",
  "storage": {
    "type": "redis",
    "host": "redis",
    "port": 6379,
    "username": "",
    "password": "",
    "database": 0,
    "optimisation_max_idle": 2000,
    "optimisation_max_active": 4000
  },
  "enable_analytics": false,
  "analytics_config": {
    "type": "",
    "ignored_ips": []
  },
  "dns_cache": {
    "enabled": false,
    "ttl": 3600,
    "check_interval": 60
  },
  "allow_master_keys": false,
  "policies": {
    "policy_source": "file"
  },
  "hash_keys": true,
  "hash_key_function": "murmur64",
  "suppress_redis_signal_reload": false,
  "force_global_session_lifetime": false,
  "max_idle_connections_per_host": 500
}
</code></pre>
<p>我们看到：storage下面存储了redis的配置信息，我们需要将redis的host配置修改为我们的VM地址：</p>
<pre><code>    "host": "192.168.0.24",
</code></pre>
<p>然后重启Tyk Gateway服务：</p>
<pre><code>$systemctl daemon-reload
$systemctl restart tyk-gateway
</code></pre>
<p>之后，我们再查看tyk gateway的运行状态：</p>
<pre><code>systemctl status tyk-gateway
● tyk-gateway.service - Tyk API Gateway
   Loaded: loaded (/usr/lib/systemd/system/tyk-gateway.service; enabled; vendor preset: disabled)
   Active: active (running) since 一 2023-11-20 06:54:07 CST; 41s ago
 Main PID: 20827 (tyk)
    Tasks: 15
   Memory: 24.8M
   CGroup: /system.slice/tyk-gateway.service
           └─20827 /opt/tyk-gateway/tyk --conf /opt/tyk-gateway/tyk.conf

11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Loading API configurations...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Tracking hostname" api_nam...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Initialising Tyk REST API ...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="API bind on custom port:0"...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Checking security policy: ...fault
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="API Loaded" api_id=1 api_n...ip=--
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Loading uptime tests..." p...k-mgr
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="Initialised API Definition...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=warning msg="All APIs are protected ...=main
11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time="Nov 20 06:54:07" level=info msg="API reload complete" prefix=main
Hint: Some lines were ellipsized, use -l to show in full.
</code></pre>
<p>从服务日志来看，现在Tyk Gateway可以正常连接redis并提供服务了！我们也可以通过下面的命令验证网关的运行状态：</p>
<pre><code>$curl localhost:8080/hello
{"status":"pass","version":"5.2.1","description":"Tyk GW","details":{"redis":{"status":"pass","componentType":"datastore","time":"2023-11-20T06:58:57+08:00"}}}
</code></pre>
<p>“/hello”是Tyk Gateway的内置路由，由Tyk网关自己提供服务。</p>
<p>到这里Tyk Gateway的安装和简单配置就结束了，接下来，我们就来看看API Gateway的主要功能特性，并借助Tyk Gateway来展示一下这些功能特性。</p>
<blockquote>
<p>注：查看Tyk Gateway的运行日志，可以使用journalctl -u tyk-gateway -f命令实时follow最新日志输出。</p>
</blockquote>
<h3>3.3 功能特性：请求转发与路由</h3>
<p>请求转发和路由是API Gateway的主要功能特性之一，API Gateway可以根据请求的路径、方法、查询参数等信息将请求转发到相应的后端服务，其内核与反向代理类似，不同之处在于API Gateway增加了“API”这层抽象，更加专注于构建、管理和增强API。</p>
<p>下面我们来看看Tyk如何配置API路由，我们首先创建一个新API。</p>
<h4>3.3.1 创建一个新API</h4>
<p>Tyk开源版支持两种创建API的方式，一种是通过<a href="https://tyk.io/docs/getting-started/create-api/#tutorial-create-an-api-with-the-tyk-gateway-api">调用Tyk的控制类API</a>，一种则是<a href="https://tyk.io/docs/getting-started/create-api/#tutorial-create-an-api-in-file-based-mode">通过传统的配置文件，放入特定目录下</a>。无论哪种方式添加完API，最终都要通过Tyk Gateway热加载(hot reload)或重启才能生效。</p>
<blockquote>
<p>注：Tyk Gateway的商业版本提供Dashboard，可以以图形化的方式管理API，并且商业版本的API定义会放在Postgres或MongoDB中，我们这里用开源版本，只能手工管理了，并且API定义只能放在文件中。</p>
</blockquote>
<p>下面，我们就来在Tyk上创建一个新的API路由，该路由示例的示意图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-5.png" alt="" /></p>
<p>在未添加新API之前，我们使用curl访问一下该API路径：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Not Found
</code></pre>
<p>Tyk Gateway由于找不到API路由，返回Not Found。接下来，我们采用调用tyk gateway API的方式来添加路由：</p>
<pre><code>$curl -v -H "x-tyk-authorization: {tyk gateway secret}" \
  -s \
  -H "Content-Type: application/json" \
  -X POST \
  -d '{
    "name": "no-authn-v1",
    "slug": "no-authn-v1",
    "api_id": "no-authn-v1",
    "org_id": "1",
    "use_keyless": true,
    "auth": {
      "auth_header_name": "Authorization"
    },
    "definition": {
      "location": "header",
      "key": "x-api-version"
    },
    "version_data": {
      "not_versioned": true,
      "versions": {
        "Default": {
          "name": "Default",
          "use_extended_paths": true
        }
      }
    },
    "proxy": {
      "listen_path": "/api/v1/no-authn",
      "target_url": "http://localhost:18081/",
      "strip_listen_path": true
    },
    "active": true
}' http://localhost:8080/tyk/apis | python -mjson.tool 

* About to connect() to localhost port 8080 (#0)
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
&gt; POST /tyk/apis HTTP/1.1
&gt; User-Agent: curl/7.29.0
&gt; Host: localhost:8080
&gt; Accept: */*
&gt; x-tyk-authorization: {tyk gateway secret}
&gt; Content-Type: application/json
&gt; Content-Length: 797
&gt;
} [data not shown]
* upload completely sent off: 797 out of 797 bytes
&lt; HTTP/1.1 200 OK
&lt; Content-Type: application/json
&lt; Date: Wed, 22 Nov 2023 05:38:40 GMT
&lt; Content-Length: 53
&lt;
{ [data not shown]
* Connection #0 to host localhost left intact
{
    "action": "added",
    "key": "no-authn-v1",
    "status": "ok"
}
</code></pre>
<p>从curl返回结果我们看到：API已经被成功添加。这时tyk gateway的安装目录/opt/tyk-gateway的子目录apps下会新增一个名为no-authn-v1.json的配置文件，这个文件内容较多，有300行，这里就不贴出来了，这个文件就是新增的no-authn <a href="https://tyk.io/docs/tyk-gateway-api/api-definition-objects/">API的定义文件</a>。</p>
<p>不过此刻，Tyk Gateway还需热加载后才能为新的API提供服务，调用下面API可以触发Tyk Gateway的热加载：</p>
<pre><code>$curl -H "x-tyk-authorization: {tyk gateway secret}" -s http://localhost:8080/tyk/reload/group | python -mjson.tool
{
    "message": "",
    "status": "ok"
}
</code></pre>
<blockquote>
<p>注：即便触发热加载成功，但如果body中的json格式错，比如多了一个结尾逗号，Tyk Gateway是不会报错的！</p>
</blockquote>
<p>API路由创建完毕并生效后，我们再来访问一下API：</p>
<pre><code>$ curl localhost:8080/api/v1/no-authn
{
    "error": "There was a problem proxying the request"
}
</code></pre>
<p>我们看到：Tyk Gateway返回的已经不是“Not Found”了！现在我们创建一下no-authn这个API服务，考虑到适配更多后续示例，这里建立这样一个http server：</p>
<pre><code>// api-gateway-examples/httpserver

func main() {
    // 解析命令行参数
    port := flag.Int("p", 8080, "Port number")
    apiVersion := flag.String("v", "v1", "API version")
    apiName := flag.String("n", "example", "API name")
    flag.Parse()                                         

    // 注册处理程序
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(*r)
        fmt.Fprintf(w, "Welcome api: localhost:%d/%s/%s\n", *port, *apiVersion, *apiName)
    })                                                                                     

    // 启动HTTP服务器
    addr := fmt.Sprintf(":%d", *port)
    log.Printf("Server listening on port %d\n", *port)
    log.Fatal(http.ListenAndServe(addr, nil))
}
</code></pre>
<p>我们启动一个该http server的实例：</p>
<pre><code>$go run main.go -p 18081 -v v1 -n no-authn
2023/11/22 22:02:42 Server listening on port 18081
</code></pre>
<p>现在我们再通过tyk gateway调用一下no-authn这个API：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
</code></pre>
<p>我们看到这次路由通了！no-authn API返回了期望的结果！</p>
<h4>3.3.2 负载均衡</h4>
<p>如果no-authn API存在多个服务实例，Tyk Gateway也可以将请求流量负载均衡到多个no-authn服务实例上去，下图是Tyk Gateway进行<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/load-balancing/">请求流量负载均衡</a>的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-6.png" alt="" /></p>
<p>要实现负责均衡，我们需要调整no-authn API的定义，这次我们直接修改/opt/tyk-gateway/apps/no-authn-v1.json，变更的配置主要有三项：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

  "proxy": {
    "preserve_host_header": false,
    "listen_path": "/api/v1/no-authn",
    "target_url": "",                  // (1) 改为""
    "disable_strip_slash": false,
    "strip_listen_path": true,
    "enable_load_balancing": true,     // (2) 改为true
    "target_list": [                   // (3) 填写no-authn服务实例列表
      "http://localhost:18081/",
      "http://localhost:18082/",
      "http://localhost:18083/"
    ],
</code></pre>
<p>修改完配置后，调用Tyk的控制类API使之生效，然后我们启动三个no-authn的API实例：</p>
<pre><code>$go run main.go -p 18081 -v v1 -n no-authn
$go run main.go -p 18082 -v v1 -n no-authn
$go run main.go -p 18083 -v v1 -n no-authn
</code></pre>
<p>接下来，我们多次调用curl访问no-authn API：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18083/v1/no-authn

$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18083/v1/no-authn
</code></pre>
<p>我们看到：Tyk Gateway在no-authn API的各个实例之间做了等权重的轮询。如果我们停掉实例3，再来访问该API，我们将得到下面结果：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Bad Request

$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Welcome api: localhost:18082/v1/no-authn
$curl localhost:8080/api/v1/no-authn
Bad Request
</code></pre>
<blockquote>
<p>注：Tyk Gateway商业版通过Dashboard<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/load-balancing/">支持配置带权重的RR负载均衡算法</a>。</p>
</blockquote>
<p>我们看到：实例3已经下线，但Tyk Gateway并不会跳过该已经下线的实例，这在生产环境会给客户端带来不一致的响应。</p>
<h4>3.3.3 服务实例存活检测(uptime test)</h4>
<p>Tyk Gateway在开启负载均衡的时候，也提供了对后端服务实例的存活检测机制，当某个服务实例down了后，负载均衡机制会绕过该实例将请求发到下一个处于存活状态的实例；而当down机实例恢复后，Tyk Gateway也能及时检测到服务实例上线，并将其加入流量负载调度。</p>
<p>支持存活检测(uptime test)的API定义配置如下：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

"uptime_tests": {
    "disable": false,
    "poller_group":"",
    "check_list": [
      {
        "url": "http://localhost:18081/"
      },
      {
        "url": "http://localhost:18082/"
      },
      {
        "url": "http://localhost:18083/"
      }
    ],
    "config": {
      "enable_uptime_analytics": true,
      "failure_trigger_sample_size": 3,
      "time_wait": 300,
      "checker_pool_size": 50,
      "expire_utime_after": 0,
      "service_discovery": {
        "use_discovery_service": false,
        "query_endpoint": "",
        "use_nested_query": false,
        "parent_data_path": "",
        "data_path": "",
        "port_data_path": "",
        "target_path": "",
        "use_target_list": false,
        "cache_disabled": false,
        "cache_timeout": 0,
        "endpoint_returns_list": false
      },
      "recheck_wait": 0
    }
}

"proxy": {
    ... ...
    "enable_load_balancing": true,
    "target_list": [
      "http://localhost:18081/",
      "http://localhost:18082/",
      "http://localhost:18083/"
    ],
    "check_host_against_uptime_tests": true,
    ... ...
}
</code></pre>
<p>我们新增了uptime_tests的配置，uptime_tests的check_list中的url的值要与proxy中target_list中的值完全一样，这样Tyk Gateway才能将二者对应上。另外proxy的check_host_against_uptime_tests要设置为true。</p>
<p>这样配置并生效后，等我们将服务实例3停掉后，后续到no-authn的请求就只会转发到实例1和实例2了。而当恢复实例3运行后，Tyk Gateway又会将流量分担到实例3上。</p>
<h4>3.3.4 动态负载均衡</h4>
<p>上面负载均衡示例中target_list中的目标实例的IP和端口的手工配置的，而在云原生时代，我们经常会基于容器承载API服务实例，当容器因故退出，并重新启动一个新容器时，IP可能会发生变化，这样上述的手工配置就无法满足要求，这就对API Gateway提出了与服务发现组件集成的要求：通过服务发现组件动态获取服务实例的访问列表，进而实现<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/service-discovery/">动态负载均衡</a>。</p>
<p>Tyk Gateway内置了主流服务发现组件(比如Etcd、Consul、ZooKeeper等)的对接能力，鉴于环境所限，这里就不举例了，大家可以在Tyk Gateway的<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/service-discovery/examples/">服务发现示例文档页面</a>找到与不同服务发现组件对接时的配置示例。</p>
<h4>3.3.5 IP访问限制</h4>
<p>针对每个API，API网关还提供IP访问限制的特性，比如Tyk Gateway就提供了<a href="https://tyk.io/docs/tyk-apis/tyk-gateway-api/api-definition-objects/ip-whitelisting/">IP白名单</a>和<a href="https://tyk.io/docs/tyk-apis/tyk-gateway-api/api-definition-objects/ip-blacklisting/">IP黑名单</a>功能，通常二选一开启一种限制即可。</p>
<p>以白名单为例，即凡是在白名单中的IP才被允许访问该API。下面是白名单配置样例：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

  "enable_ip_whitelisting": true,
  "allowed_ips": ["12.12.12.12", "12.12.12.13", "12.12.12.14"],
</code></pre>
<p>生效后，当我们访问no-authn API时，会得到下面错误：</p>
<pre><code>$curl localhost:8080/api/v1/no-authn
{
    "error": "access from this IP has been disallowed"
}
</code></pre>
<p>如果开启的是黑名单，那么凡是在黑名单中的IP都被禁止访问该API，下面是黑名单配置样例：</p>
<pre><code>// /opt/tyk-gateway/apps/no-authn-v1.json

  "enable_ip_blacklisting": true,
  "blacklisted_ips": ["12.12.12.12", "12.12.12.13", "12.12.12.14", "127.0.0.1"],
</code></pre>
<p>生效后，当我们访问no-authn API时，会得到如下结果：</p>
<pre><code>$curl 127.0.0.1:8080/api/v1/no-authn
{
    "error": "access from this IP has been disallowed"
}
</code></pre>
<p>到目前为止，我们的API网关和定义的API都处于“裸奔”状态，因为没有对客户端进行身份认证，任何客户端都可以访问到我们的API，显然这不是我们期望的，接下来，我们就来看看API网关的一个重要功能特性：身份认证与授权。</p>
<h3>3.4 功能特性：身份认证和授权</h3>
<p>在《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example">通过实例理解Go Web身份认证的几种方式</a>》一文中，我们提到过：<strong>建立全局的安全通道是任何身份认证方式的前提</strong>。</p>
<h4>3.4.1 建立安全通道，卸载TLS证书</h4>
<p>Tyk Gateway支持在Gateway层面<a href="https://tyk.io/docs/basic-config-and-security/security/tls-and-ssl/">统一配置TLS证书</a>，同时也起到在Gateway卸载TLS证书的作用：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-7.png" alt="" /></p>
<p>这次我们要在tyk.conf中进行配置，才能在Gateway层面生效。这里我们借用《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/">通过实例理解Go Web身份认证的几种方式</a>》一文中生成的几个证书(大家可以在https://github.com/bigwhite/experiments/tree/master/authn-examples/tls-authn/make_certs下载)，并将它们放到/opt/tyk-gateway/certs/下面：</p>
<pre><code>$ls /opt/tyk-gateway/certs/
server-cert.pem  server-key.pem
</code></pre>
<p>然后，我们在/opt/tyk-gateway/tyk.conf文件中增加下面配置：</p>
<pre><code>// /opt/tyk-gateway/tyk.conf 

  "http_server_options": {
    "use_ssl": true,
    "certificates": [
      {
        "domain_name": "server.com",
        "cert_file": "./certs/server-cert.pem",
        "key_file": "./certs/server-key.pem"
      }
    ]
  }
</code></pre>
<p>之后，重启tyk gateway服务，使得tyk.conf的配置修改生效。</p>
<blockquote>
<p>注：在/etc/hosts中设置server.com为127.0.0.1。</p>
</blockquote>
<p>现在我们用之前的http方式访问一下no-authn的API：</p>
<pre><code>$curl server.com:8080/api/v1/no-authn
Client sent an HTTP request to an HTTPS server.
</code></pre>
<p>由于全局启用了HTTPS，采用http方式的请求将被拒绝。我们换成https方式访问：</p>
<pre><code>// 不验证服务端证书
$curl -k https://server.com:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn

// 验证服务端的自签证书
$curl --cacert ./inter-cert.pem https://server.com:8080/api/v1/no-authn
Welcome api: localhost:18081/v1/no-authn
</code></pre>
<h4>3.4.2 Mutual TLS双向认证</h4>
<p>在《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example">通过实例理解Go Web身份认证的几种方式</a>》一文中，我们介绍的第一种身份认证方式就是TLS双向认证，那么Tyk Gateway对MTLS的支持如何呢？<a href="https://tyk.io/docs/basic-config-and-security/security/mutual-tls/">Tyk官方文档</a>提到它既支持<a href="https://tyk.io/docs/basic-config-and-security/security/mutual-tls/client-mtls">client mTLS</a>，也支持<a href="https://tyk.io/docs/basic-config-and-security/security/mutual-tls/upstream-mtls">upstream mTLS</a>。</p>
<p>我们更关心的是client mTLS，即客户端在与Gateway建连后，Gateway会使用Client CA验证客户端的证书！我最初认为这个Client CA的配置是在tyk.conf中，但找了许久，也没有发现配置Client CA的地方。</p>
<p>在no-authn API的定义文件(no-authn-v1.json)中，我们做如下配置改动：</p>
<pre><code>  "use_mutual_tls_auth": true,
  "client_certificates": [
      "/opt/tyk-gateway/certs/inter-cert.pem"
  ],
</code></pre>
<p>但使用下面命令访问API时报错：</p>
<pre><code>$curl --key ./client-key.pem --cert ./client-cert.pem --cacert ./inter-cert.pem https://server.com:8080/api/v1/no-authn
{
    "error": "Certificate with SHA256 bc8717c0f2ea5a0b81813abb3ec42ef8f9bf60da251b87243627d65fb0e3887b not allowed"
}
</code></pre>
<p>如果将”client_certificates”的配置中的inter-cert.pem改为client-cert.pem，则是可以的，但个人感觉这很奇怪，不符合逻辑，将tyk gateway的文档、issue甚至代码翻了又翻，也没找到合理的配置client CA的位置。</p>
<p><a href="https://tyk.io/docs/apim-best-practice/api-security-best-practice/authentication/">Tyk Gateway支持多种身份认证方式</a>，下面我们来看一种使用较为广泛的方式：JWT Auth。</p>
<blockquote>
<p>主要JWT身份认证方式的原理和详情，可以参考我之前的文章《<a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/">通过实例理解Go Web身份认证的几种方式</a>》。</p>
</blockquote>
<h4>3.4.3 JWT Token Auth</h4>
<p>下面是我为这个示例做的一个示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-8.png" alt="" /></p>
<p>这是我们日常开发中经常遇到的场景，即通过portal用用户名和密码登录后便可以拿到一个jwt token，然后后续的访问功能API的请求仅携带该jwt token即可。API Gateway对于portal/login API不做任何身份认证；而对后续的功能API请求，通过共享的secret(也称为static secret)对请求中携带的jwt token进行签名验证。</p>
<p>portal/login API由于不进行authn，这样其配置与前面的no-authn API几乎一致，只是API名称、路径和target_list有不同：</p>
<pre><code>// apps/portal-login-v1.json

{
  "name": "portal-login-v1",
  "slug": "portal-login-v1",
  "listen_port": 0,
  "protocol": "",
  "enable_proxy_protocol": false,
  "api_id": "portal-login-v1",
  "org_id": "1",
  "use_keyless": true,
  ... ...
  "proxy": {
    "preserve_host_header": false,
    "listen_path": "/api/v1/portal/login",
    "target_url": "",
    "disable_strip_slash": false,
    "strip_listen_path": true,
    "enable_load_balancing": true,
    "target_list": [
      "http://localhost:28084"
    ],
    "check_host_against_uptime_tests": true,
  ... ...
}
</code></pre>
<p>对应的portal login API也不复杂：</p>
<pre><code>// api-gateway-examples/portal-login/main.go

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

func main() {
    // 创建一个基本的HTTP服务器
    mux := http.NewServeMux()

    username := "admin"
    password := "123456"
    key := "iamtonybai"

    // for uptime test
    mux.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    // login handler
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        // 从请求头中获取Basic Auth认证信息
        user, pass, ok := req.BasicAuth()
        if !ok {
            // 认证失败
            w.WriteHeader(http.StatusUnauthorized)
            return
        }

        // 验证用户名密码
        if user == username &amp;&amp; pass == password {
            // 认证成功，生成token
            token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
                "username": username,
                "iat":      jwt.NewNumericDate(time.Now()),
            })
            signedToken, _ := token.SignedString([]byte(key))
            w.Write([]byte(signedToken))
        } else {
            // 认证失败
            http.Error(w, "Invalid username or password", http.StatusUnauthorized)
        }
    })

    // 监听28084端口
    err := http.ListenAndServe(":28084", mux)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>运行该login API服务后，我们用curl命令获取一下jwt token：</p>
<pre><code>$curl -u 'admin:123456' -k https://server.com:8080/api/v1/portal/login
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA
</code></pre>
<p>现在我们再来建立protected API：</p>
<pre><code>// apps/protected-v1.json

{
  "name": "protected-v1",
  "slug": "protected-v1",
  "listen_port": 0,
  "protocol": "",
  "enable_proxy_protocol": false,
  "api_id": "protected-v1",
  "org_id": "1",
  "use_keyless": false,    // 设置为false, gateway才会进行jwt的验证
  ... ...
  "enable_jwt": true,      // 开启jwt
  "use_standard_auth": false,
  "use_go_plugin_auth": false,
  "enable_coprocess_auth": false,
  "custom_plugin_auth_enabled": false,
  "jwt_signing_method": "hmac",        // 设置alg为hs256
  "jwt_source": "aWFtdG9ueWJhaQ==",    // 设置共享secret: base64("iamtonybai")
  "jwt_identity_base_field": "username", // 设置代表请求中的用户身份的字段，这里我们用username
  "jwt_client_base_field": "",
  "jwt_policy_field_name": "",
  "jwt_default_policies": [
     "5e189590801287e42a6cf5ce"        // 设置security policy，这个似乎是jwt auth必须的
  ],
  "jwt_issued_at_validation_skew": 0,
  "jwt_expires_at_validation_skew": 0,
  "jwt_not_before_validation_skew": 0,
  "jwt_skip_kid": false,
  ... ...
  "version_data": {
    "not_versioned": true,
    "default_version": "",
    "versions": {
      "Default": {
        "name": "Default",
        "expires": "",
        "paths": {
          "ignored": null,
          "white_list": null,
          "black_list": null
        },
        "use_extended_paths": true,
        "extended_paths": {
          "persist_graphql": null
        },
        "global_headers": {
          "username": "$tyk_context.jwt_claims_username" // 设置转发到upstream的请求中的header字段username
        },
        "global_headers_remove": null,
        "global_response_headers": null,
        "global_response_headers_remove": null,
        "ignore_endpoint_case": false,
        "global_size_limit": 0,
        "override_target": ""
      }
    }
  },
  ... ...
  "enable_context_vars": true, // 开启上下文变量
  "config_data": null,
  "config_data_disabled": false,
  "tag_headers": ["username"], // 设置header
  ... ...
}
</code></pre>
<p>这个配置就相对复杂许多，也是翻阅了很长时间资料才验证通过的配置。JWT Auth必须有关联的policy设置，我们在tyk gateway开源版中要想设置policy，需要现在tyk.conf中做如下设置：</p>
<pre><code>// /opt/tyk-gateway/tyk.conf

  "policies": {
    "policy_source": "file",
    "policy_record_name": "./policies/policies.json"
  },
</code></pre>
<p>而policies/policies.json的内容如下：</p>
<pre><code>// /opt/tyk-gateway/policies/policies.json
{
    "5e189590801287e42a6cf5ce": {
        "rate": 1000,
        "per": 1,
        "quota_max": 100,
        "quota_renewal_rate": 60,
        "access_rights": {
            "protected-v1": {
                "api_name": "protected-v1",
                "api_id": "protected-v1",
                "versions": [
                    "Default"
                ]
            }
        },
        "org_id": "1",
        "hmac_enabled": false
    }
}

</code></pre>
<p>上述设置完毕并重启tyk gateway生效后，且protected api服务也已经启动时，我们访问一下该API服务：</p>
<pre><code>$curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA" -k https://server.com:8080/api/v1/protected
invoke protected api ok
</code></pre>
<p>我们看到curl发出的请求成功通过了Gateway的验证！并且通过protected API输出的请求信息来看，Gateway成功解析出username，并将其作为Header中的字段传递给了protected API服务实例：</p>
<pre><code>http.Request{Method:"GET", URL:(*url.URL)(0xc0002f6240), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept":[]string{"*/*"}, "Accept-Encoding":[]string{"gzip"}, "Authorization":[]string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA"}, "User-Agent":[]string{"curl/7.29.0"}, "Username":[]string{"admin"}, "X-Forwarded-For":[]string{"127.0.0.1"}}, Body:http.noBody{}, GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:"localhost:28085", Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:"[::1]:55583", RequestURI:"/", TLS:(*tls.ConnectionState)(nil), Cancel:(&lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0002e34f0)}
</code></pre>
<p>如果不携带Authorization头字段或jwt的token是错误的，那么结果将如下所示：</p>
<pre><code>$ curl -k https://server.com:8080/api/v1/protected
{
    "error": "Authorization field missing"
}

$ curl -k -H "Authorization: Bearer xxx" https://server.com:8080/api/v1/protected
{
    "error": "Key not authorized"
}
</code></pre>
<p>一旦通过API Gateway的身份认证，上游的API服务就会拿到客户端身份，有了唯一身份后，就可以进行<a href="https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/">授权操作</a>了，其实policy设置本身也是一种授权访问控制。Tyk Gateway自身也<a href="https://tyk.io/docs/tyk-dashboard/rbac/#understanding-the-concept-of-users-and-permissions">支持RBAC等模型</a>，也支持与OPA(open policy agent)等的集成，但更多是在商业版的tyk dashboard下完成的，这里也就不重点说明了。</p>
<p>下面的Gateway的几个主要功能特性由于试验环境受限以及文章篇幅考量，我不会像上述例子这么细致的说明了，只会简单说明一下。</p>
<h3>3.5 功能特性：流量控制与限速</h3>
<p>Tyk Gateway内置提供了强大的流量控制功能，可以通过<a href="https://tyk.io/docs/basic-config-and-security/control-limit-traffic/rate-limiting/">全局级别和API级别的限速</a>来管理请求流量。此外，Tyk Gateway 还<a href="https://tyk.io/docs/basic-config-and-security/control-limit-traffic/request-quotas/">支持请求配额（request quota）</a>来限制每个用户或应用程序在一个时间周期内的请求次数。</p>
<p>流量不仅和请求速度和数量有关系，与请求的大小也有关系，Tyk Gateway还支持在全局层面和API层面<a href="https://tyk.io/docs/basic-config-and-security/control-limit-traffic/request-size-limits/">设置Request的size limit</a>，以避免超大包对网关运行造成不良影响。</p>
<h3>3.6 功能特性：高可用与容错处理</h3>
<p>在许多情况下，我们要为客户确保服务水平(service level)，比如：最大往返时间、最大响应时延等。Tyk Gateway提供了一系列功能，可帮助我们确保网关的高可用运行和SLA服务水平。</p>
<p><a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/health-check/">Tyk支持健康检查</a>，这对于确定Tyk Gateway的状态极为重要，没有健康检查，就很难知道网关的实际运行状态如何。</p>
<p>Tyk Gateway还<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/circuit-breakers/">内置了断路器(circuit breaker)</a>，这个断路器是基于比例的，因此如果y个请求中的x请求都失败了，断路器就会跳闸，例如，如果x = 10，y = 100，则阈值百分比为10%。当失败比例到达10%时，断路器就会切断流量，同时跳闸还会触发一个事件，我们可以记录和处理该事件。</p>
<p>当upstream的服务响应迟迟不归时，Tyk Gateway还可以<a href="https://tyk.io/docs/planning-for-production/ensure-high-availability/enforced-timeouts/">设置强制超时</a>，可以确保服务始终在给定时间内响应。这在高可用性系统中非常重要，因为在这种系统中，响应性能至关重要，这样才能干净利落地处理错误。</p>
<h3>3.7 功能特性：监控与可观测性</h3>
<p>微服务时代，可观测性对运维以及系统高可用的重要性不言而喻。Tyk Gateway在多年的演化过程中，也逐渐增加了对可观测的支持，</p>
<p>可观测主要分三大块：</p>
<ul>
<li>log</li>
</ul>
<p>Tyk Gateway支持设置输出日志的级别(log level)，默认是info级别。Tyk输出的是结构化日志，这使得它可以很好的与其他日志收集查询系统集成，<a href="https://tyk.io/docs/log-data/#logging">Tyk支持与主流的日志收集工具对接</a>，包括：logstash、sentry、Graylog、Syslog等。</p>
<ul>
<li>metrics</li>
</ul>
<p>度量数据是反映网关系统健康状况、错误计数和类型、IT基础设施（服务器、虚拟机、容器、数据库和其他后端组件）及其他流程的硬件资源数据的重要参考。运维团队可以通过<a href="https://tyk.io/docs/planning-for-production/monitoring/">使用监控工具来利用实时度量的数据</a>，识别运行趋势、在系统故障时设置警报、确定问题的根本原因并缓解问题。</p>
<p>Tyk Gateway内置了<a href="https://tyk.io/blog/service-level-objectives-for-your-apis-with-tyk-prometheus-and-grafana/">对主流metrics采集方案Prometheus+Grafana的支持</a>，可以在网关层面以及对API进行实时度量数据采集和展示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-9.png" alt="" /></p>
<ul>
<li>tracing</li>
</ul>
<p>Tyk Gateway从5.2版本开始<a href="https://tyk.io/docs/product-stack/tyk-gateway/advanced-configurations/distributed-tracing/open-telemetry/open-telemetry-overview/">支持了与服务Tracing界的标准：OpenTelemetry的集成</a>，这样你可以使用多种支持OpenTelemetry的Tracing后端，比如Jaeger、Datadog等。Tracing可在Gateway层面开启，也可以延展到API层面。</p>
<h2>4. 小结</h2>
<p>本文对已经相对成熟的API网关技术做了回顾，对API网关的演进阶段、主流特性以及当前市面上的主流API网关进行了简要说明，并以Go实现的Tyk Gateway社区开源版为例，以示例方式对API网关的主要功能做了介绍。</p>
<p>总体而言，Tyk Gateway是一款功能强大，社区相对活跃并有商业公司支持的产品，文档很丰富，但从实际使用层面，这些文档对Tyk社区版本的使用者来说并不友好，指导性不足(更多用商业版的Dashboard说明，与配置文件难于对应)，就像本文例子中那样，为了搞定JWT认证，笔者着实花了不少时间查阅资料，甚至阅读源码。</p>
<p>Tyk Gateway的配置设计平坦，没有层次和逻辑，感觉是随着时间随意“堆砌”上去的。并且配置文件更新时，如果出现格式问题，Tyk Gateway并不报错，让人难于确定配置是否真正生效了，只能用<a href="https://tyk.io/docs/tyk-gateway-api/">Tyk Gateway的控制API</a>去查询结果来验证，非常繁琐低效。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/api-gateway-examples">这里</a>下载，文中涉及的一些tyk gateway api和security policy的配置也可以在其中查看。</p>
<h2>5. 参考资料</h2>
<ul>
<li><a href="https://37signals.com/podcast/leaving-the-cloud/">Leaving the Cloud</a> &#8211; https://37signals.com/podcast/leaving-the-cloud/</li>
<li><a href="https://www.infoq.com/articles/past-present-future-api-gateways/">The Past, Present, and Future of API Gateways</a> &#8211; https://www.infoq.com/articles/past-present-future-api-gateways/</li>
<li><a href="https://blog.oneuptime.com/moving-from-aws-to-bare-metal/">How moving from AWS to Bare-Metal saved us 230,000/yr</a> &#8211; https://blog.oneuptime.com/moving-from-aws-to-bare-metal/</li>
<li><a href="https://navendu.me/posts/gateway-and-mesh/">A Comprehensive Guide to API Gateways, Kubernetes Gateways, and Service Meshes</a> &#8211; https://navendu.me/posts/gateway-and-mesh/</li>
<li><a href="https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway">Use API gateways in microservices</a> &#8211; https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway</li>
<li><a href="https://blog.postman.com/the-tyk-api-gateway-and-postman/">The Tyk API Gateway and Postman</a> &#8211; https://blog.postman.com/the-tyk-api-gateway-and-postman/</li>
<li><a href="https://javascript.plainenglish.io/getting-started-to-tyk-api-gateway-with-keycloak-16307435584a">Getting Started with Tyk API Gateway with Keycloak</a> &#8211; https://javascript.plainenglish.io/getting-started-to-tyk-api-gateway-with-keycloak-16307435584a</li>
<li><a href="https://medium.com/@asoorm/observing-your-api-metrics-with-tyk-elasticsearch-kibana-74e8fd946c39">Observing your API traffic with Tyk, Elasticsearch &amp; Kibana</a> &#8211; https://medium.com/@asoorm/observing-your-api-metrics-with-tyk-elasticsearch-kibana-74e8fd946c39</li>
<li><a href="https://community.tyk.io/t/set-up-jwt-token-in-tyk-gateway/6572/9">Set up JWT token in tyk gateway</a> &#8211; https://community.tyk.io/t/set-up-jwt-token-in-tyk-gateway/6572/9</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>提高您的kubectl生产力（第三部分）：集群上下文切换、使用别名减少输入和插件扩展</title>
		<link>https://tonybai.com/2019/08/31/kubectl-productivity-part3/</link>
		<comments>https://tonybai.com/2019/08/31/kubectl-productivity-part3/#comments</comments>
		<pubDate>Sat, 31 Aug 2019 05:01:43 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alias]]></category>
		<category><![CDATA[apiserver]]></category>
		<category><![CDATA[apt]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[bashrc]]></category>
		<category><![CDATA[bash_profile]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[complete-alias]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[fzf]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[jsonpath]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[krew]]></category>
		<category><![CDATA[kube-controller-manager]]></category>
		<category><![CDATA[kube-scheduler]]></category>
		<category><![CDATA[kubeconfig]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubectl-aliases]]></category>
		<category><![CDATA[kubectl-ctx]]></category>
		<category><![CDATA[kubectl-ns]]></category>
		<category><![CDATA[kubectx]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[kubens]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[RESTAPI]]></category>
		<category><![CDATA[RESTFUL]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[xpath]]></category>
		<category><![CDATA[yaml]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[zsh]]></category>
		<category><![CDATA[上下文]]></category>
		<category><![CDATA[命名空间]]></category>
		<category><![CDATA[插件]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2757</guid>
		<description><![CDATA[本文翻译自《Boosting your kubectl productivity》。 第一部分：什么是kubectl？ 第二部分：命令完成、资源规范快速查看和自定义列输出格式什么是kubectl？ 4. 轻松切换集群和名称空间 当kubectl必须向Kubernetes API发出请求时，它会读取系统上所谓的kubeconfig文件，以获取它需要访问的所有连接参数并向API服务器发出请求。 默认的kubeconfig文件是~/.kube/config。此文件通常由某个命令自动创建或更新（例如，aws eks update-kubeconfig或者gcloud container clusters get-credentials，如果您使用托管Kubernetes服务）。 使用多个集群时，您的kubeconfig文件中配置了多个集群的连接参数。这意味着，您需要一种方法来告诉kubectl 您希望它连接到哪个集群。 在集群中，您可以设置多个名称空间（名称空间是物理集群中的一种“虚拟”集群）。Kubectl也会从kubeconfig文件确定用于请求的命名空间。因此，您需要一种方法来告诉kubectl 您希望它使用哪个命名空间。 本节将介绍kubectl切换集群上下文的原理以及它是如何轻松完成的。 请注意，您还可以在KUBECONFIG环境变量中列出多个kubeconfig文件。在这种情况下，所有这些文件将在执行时合并为单个有效配置。您还可以使用&#8211;kubeconfig指定kubectl命令的选项以覆盖默认的kubeconfig文件。请参阅官方文档。 Kubeconfig文件 让我们看看kubeconfig文件实际包含的内容： 如您所见，kubeconfig文件由一组上下文组成。上下文包含以下三个元素： 集群(cluster)：集群的API服务器的URL 用户(user)：集群的特定用户的身份验证凭据 命名空间(namespace)：连接到集群时使用的命名空间 实际上，人们经常在他们的kubeconfig文件中为每个集群的配置一个上下文。但是，你也可以为每个集群配置多个上下文，其用户或命名空间不同。但这似乎不太常见，因此通常在集群和上下文之间存在一对一的映射。 在任何给定时间，其中一个上下文被设置为当前上下文（通过kubeconfig文件中的专用字段）： 当kubectl读取kubeconfig文件时，它总是使用当前上下文中的信息。因此，在上面的例子中，kubectl将连接到Hare集群。 因此，要切换到另一个集群，您只需更改kubeconfig文件中的当前上下文： 在上面的示例中，kubectl现在将连接到Fox集群。 要切换到同一集群中的另一个命名空间，您可以更改当前上下文的命名空间元素的值： 在上面的示例中，kubectl现在将使用Fox群集中的Prod命名空间（而不是之前设置的Test命名空间）。 请注意，kubectl还提供了&#8211;cluster，&#8211;user和&#8211;namespace，以及&#8211;context允许您覆盖单个元素和当前上下文本身的选项，无论kubeconfig文件中设置了什么。见kubectl options。 理论上，您可以通过手动编辑kubeconfig文件来执行这些更改。但当然这很乏味。以下部分介绍了允许您自动执行这些更改的各种工具。 使用kubectx kubectx是一种非常流行的用于在集群和命名空间之间切换的工具。 此工具提供允许您分别更改当前上下文和命名空间的命令kubectx和kubens命令。 如上所述，如果每个集群只有一个上下文，则更改当前上下文意味着更改集群。 在这里，您可以看到这两个命令： 在表象之下，这些命令只是编辑kubeconfig文件，如上一节中所述。 要安装kubectx，只需按照GitHub页面上的说明操作即可。 kubectx和kubens都通过完成交办提供命令完成(command completion)。这允许您自动完成上下文名称和名称空间，这样您就不必完全键入它们。您也可以在GitHub页面上找到设置完成的说明。 kubectx的另一个有用功能是交互模式。这与fzf工具结合使用，您必须单独安装（事实上，安装fzf，将自动启用kubectx交互模式）。交互模式允许您通过交互式模糊搜索界面（由fzf提供）选择目标上下文或命名空间。 使用shell别名 实际上，您并不需要单独的工具来更改当前上下文和命名空间，因为kubectl也提供了执行此操作的命令。特别是，该kubectl config命令提供了用于编辑kubeconfig文件的子命令。这里是其中的一些： kubectl config get-contexts：列出所有上下文 kubectl [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自<a href="https://learnk8s.io/blog/kubectl-productivity/">《Boosting your kubectl productivity》</a>。</p>
<p>第一部分：<a href="https://tonybai.com/2019/08/29/kubectl-productivity-part1/">什么是kubectl？</a><br />
第二部分：<a href="https://tonybai.com/2019/08/30/kubectl-productivity-part2/">命令完成、资源规范快速查看和自定义列输出格式什么是kubectl？</a></p>
<h2>4. 轻松切换集群和名称空间</h2>
<p>当kubectl必须向<a href="https://tonybai.com/tag/k8s">Kubernetes</a> API发出请求时，它会读取系统上所谓的kubeconfig文件，以获取它需要访问的所有连接参数并向API服务器发出请求。</p>
<blockquote>
<p>默认的kubeconfig文件是~/.kube/config。此文件通常由某个命令自动创建或更新（例如，aws eks update-kubeconfig或者gcloud container clusters get-credentials，如果您使用托管Kubernetes服务）。</p>
</blockquote>
<p>使用多个集群时，您的kubeconfig文件中配置了多个集群的连接参数。这意味着，您需要一种方法来告诉kubectl 您希望它连接到哪个集群。</p>
<p>在集群中，您可以设置多个<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">名称空间</a>（名称空间是物理集群中的一种“虚拟”集群）。Kubectl也会从kubeconfig文件确定用于请求的命名空间。因此，您需要一种方法来告诉kubectl 您希望它使用哪个命名空间。</p>
<p>本节将介绍kubectl切换集群上下文的原理以及它是如何轻松完成的。</p>
<blockquote>
<p>请注意，您还可以在<a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable">KUBECONFIG环境变量</a>中列出多个kubeconfig文件。在这种情况下，所有这些文件将在执行时合并为单个有效配置。您还可以使用&#8211;kubeconfig指定kubectl命令的选项以覆盖默认的kubeconfig文件。请参阅<a href="https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/">官方文档</a>。</p>
</blockquote>
<h3>Kubeconfig文件</h3>
<p>让我们看看kubeconfig文件实际包含的内容：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-1.png" alt="img{512x368}" /></p>
<p>如您所见，kubeconfig文件由一组上下文组成。上下文包含以下三个元素：</p>
<ul>
<li>集群(cluster)：集群的API服务器的URL</li>
<li>用户(user)：集群的特定用户的身份验证凭据</li>
<li>命名空间(namespace)：连接到集群时使用的命名空间</li>
</ul>
<blockquote>
<p>实际上，人们经常在他们的kubeconfig文件中为每个集群的配置一个上下文。但是，你也可以为每个集群配置多个上下文，其用户或命名空间不同。但这似乎不太常见，因此通常在集群和上下文之间存在一对一的映射。</p>
</blockquote>
<p>在任何给定时间，其中一个上下文被设置为当前上下文（通过kubeconfig文件中的专用字段）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-2.png" alt="img{512x368}" /></p>
<p>当kubectl读取kubeconfig文件时，它总是使用当前上下文中的信息。因此，在上面的例子中，kubectl将连接到Hare集群。</p>
<p>因此，要切换到另一个集群，您只需更改kubeconfig文件中的当前上下文：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-3.png" alt="img{512x368}" /></p>
<p>在上面的示例中，kubectl现在将连接到Fox集群。</p>
<p>要切换到同一集群中的另一个命名空间，您可以更改当前上下文的命名空间元素的值：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-4.png" alt="img{512x368}" /></p>
<p>在上面的示例中，kubectl现在将使用Fox群集中的Prod命名空间（而不是之前设置的Test命名空间）。</p>
<blockquote>
<p>请注意，kubectl还提供了&#8211;cluster，&#8211;user和&#8211;namespace，以及&#8211;context允许您覆盖单个元素和当前上下文本身的选项，无论kubeconfig文件中设置了什么。见kubectl options。</p>
</blockquote>
<p>理论上，您可以通过手动编辑kubeconfig文件来执行这些更改。但当然这很乏味。以下部分介绍了允许您自动执行这些更改的各种工具。</p>
<h3>使用kubectx</h3>
<p><a href="https://github.com/ahmetb/kubectx/">kubectx</a>是一种非常流行的用于在集群和命名空间之间切换的工具。</p>
<p>此工具提供允许您分别更改当前上下文和命名空间的命令kubectx和kubens命令。</p>
<blockquote>
<p>如上所述，如果每个集群只有一个上下文，则更改当前上下文意味着更改集群。</p>
</blockquote>
<p>在这里，您可以看到这两个命令：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-5.gif" alt="img{512x368}" /></p>
<blockquote>
<p>在表象之下，这些命令只是编辑kubeconfig文件，如上一节中所述。</p>
</blockquote>
<p>要安装kubectx，只需按照<a href="https://github.com/ahmetb/kubectx/#installation">GitHub页面上的说明操作即可</a>。</p>
<p>kubectx和kubens都通过完成交办提供命令完成(command completion)。这允许您自动完成上下文名称和名称空间，这样您就不必完全键入它们。您也可以在<a href="https://github.com/ahmetb/kubectx/#installation">GitHub页面</a>上找到设置完成的说明。</p>
<p>kubectx的另一个有用功能是<a href="https://github.com/ahmetb/kubectx/#interactive-mode">交互模式</a>。这与<a href="https://github.com/junegunn/fzf">fzf</a>工具结合使用，您必须单独安装（事实上，安装fzf，将自动启用kubectx交互模式）。交互模式允许您通过交互式模糊搜索界面（由fzf提供）选择目标上下文或命名空间。</p>
<h3>使用shell别名</h3>
<p>实际上，您并不需要单独的工具来更改当前上下文和命名空间，因为kubectl也提供了执行此操作的命令。特别是，该kubectl config命令提供了用于编辑kubeconfig文件的子命令。这里是其中的一些：</p>
<ul>
<li>kubectl config get-contexts：列出所有上下文</li>
<li>kubectl config current-context：获取当前上下文</li>
<li>kubectl config use-context：更改当前上下文</li>
<li>kubectl config set-context：更改上下文的元素</li>
</ul>
<p>但是，直接使用这些命令并不是很方便，因为它们很难输入。但是你可以做的是将它们包装成可以更容易执行的shell别名。</p>
<p>我基于这些命令创建了一组别名，这些命令提供了与kubectx类似的功能。在这里你可以看到他们的行动：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-6.gif" alt="img{512x368}" /></p>
<blockquote>
<p>请注意，别名使用fzf来提供交互式模糊搜索界面（如kubectx的交互模式）。这意味着，您需要安装fzf才能使用这些别名。</p>
</blockquote>
<p>以下是别名的定义：</p>
<pre><code># Get current context
alias krc='kubectl config current-context'
# List all contexts
alias klc='kubectl config get-contexts -o name | sed "s/^/  /;\|^  $(krc)$|s/ /*/"'
# Change current context
alias kcc='kubectl config use-context "$(klc | fzf -e | sed "s/^..//")"'

# Get current namespace
alias krn='kubectl config get-contexts --no-headers "$(krc)" | awk "{print \$5}" | sed "s/^$/default/"'
# List all namespaces
alias kln='kubectl get -o name ns | sed "s|^.*/|  |;\|^  $(krn)$|s/ /*/"'
# Change current namespace
alias kcn='kubectl config set-context --current --namespace "$(kln | fzf -e | sed "s/^..//")"'
</code></pre>
<p>要安装这些别名，你只需要在上面定义添加到您的~/.bashrc或~/.zshrc文件，并重新加载你的shell(source ~/.bashrc or source ~/.zshrc)！</p>
<h3>使用插件</h3>
<p>Kubectl允许安装可以像本机命令一样调用的插件。例如，您可以安装名为kubectl-foo的插件，然后将其调用为kubectl foo。</p>
<blockquote>
<p>Kubectl插件将在本文的后续部分中详细介绍。</p>
</blockquote>
<p>能够像这样更改当前上下文和命名空间不是很好吗？例如，运行kubectl ctx以更改上下文，kubectl ns更改名称空间？</p>
<p>我创建了两个允许这样做的插件：</p>
<ul>
<li><a href="https://github.com/weibeld/kubectl-ctx">kubectl-CTX</a></li>
<li><a href="https://github.com/weibeld/kubectl-ns">kubectl-NS</a></li>
</ul>
<p>在内部，插件构建在上一节的别名之上。</p>
<p>在这里你可以看到插件的实际效果：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-7.gif" alt="img{512x368}" /></p>
<blockquote>
<p>请注意，插件使用fzf来提供交互式模糊搜索界面。这意味着，您需要安装fzf才能使用这些插件。</p>
</blockquote>
<p>要安装插件，你只需要将名为的shell脚本<a href="https://raw.githubusercontent.com/weibeld/kubectl-ctx/master/kubectl-ctx">kubectl-ctx</a>和<a href="https://raw.githubusercontent.com/weibeld/kubectl-ns/master/kubectl-ns">kubectl-ns</a>的脚本下载以到PATH下的任何目录中，并使他们具备可执行权限（例如，使用chmod +x）。紧接着，你就应该能够使用kubectl ctx和kubectl ns！</p>
<h2>5. 使用自动生成的别名减少输入</h2>
<p>Shell别名通常是减少手工输入的好方法。该<a href="https://github.com/ahmetb/kubectl-aliases">kubectl-aliases</a>项目就是以这个想法为核心，并提供800多个kubectl命令别名。</p>
<p>您可能想知道如何记住800个别名？实际上，您不需要记住它们，因为它们都是根据一个简单的方案生成的，下面将显示一些示例别名：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-8.png" alt="img{512x368}" /></p>
<p>如您所见，别名由<strong>组件(component)</strong>组成，每个组件代表kubectl命令的特定元素。每个别名可以有一个用于基本命令，操作和资源的组件，以及用于选项的多个组件，您只需根据上述方案从左到右“填充”这些组件。</p>
<blockquote>
<p>请注意，目前完全详细的方案在<a href="https://github.com/ahmetb/kubectl-aliases#syntax-explanation">GitHub页面</a>上。在那里，您还可以找到别名的<a href="https://github.com/ahmetb/kubectl-aliases/blob/master/.kubectl_aliases">完整列表</a>。</p>
</blockquote>
<p>例如，别名kgpooyamlall代表命令kubectl get pods -o yaml &#8211;all-namespaces：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-9.png" alt="img{512x368}" /></p>
<p>请注意，大多数选项组件的相对顺序无关紧要。所以，kgpooyamlall相当于kgpoalloyaml。</p>
<p>您不需要将所有组件用于别名。例如k，kg，klo，ksys，或者kgpo是有效的别名也。此外，您可以在命令行中将别名与其他单词组合使用。</p>
<p>例如，您可以k proxy用于运行kubectl proxy：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-10.png" alt="img{512x368}" /></p>
<p>或者您可以kg roles用于运行kubectl get roles（目前不存在Roles资源的别名组件）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-11.png" alt="img{512x368}" /></p>
<p>要获取特定Pod，您可以使用kgpo my-pod以运行kubectl get pod my-pod：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-12.png" alt="img{512x368}" /></p>
<p>请注意，某些别名甚至需要在命令行上的进一步参数。例如，kgpol别名代表kubectl get pods -l。该-l选项需要一个参数（标签规范）。所以，你必须使用这个别名，例如，像这样:</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part3-13.png" alt="img{512x368}" /></p>
<blockquote>
<p>出于这个原因，你可以使用a，f以及l只在一个别名的结尾部分。</p>
</blockquote>
<p>一般来说，一旦你掌握了这个方案，就可以直观地从你想要执行的命令中推断出别名，并节省大量的输入！</p>
<h3>安装</h3>
<p>要安装kubectl-别名，你只需要下载<a href="https://raw.githubusercontent.com/ahmetb/kubectl-aliases/master/.kubectl_aliases">.kubectl-aliases</a>GitHub文件，并在你的~/.bashrc或~/.zshrc文件生效它：</p>
<pre><code>source ~/.kubectl_aliases
</code></pre>
<p>重新加载shell后，您应该能够使用所有800个kubectl别名！</p>
<h3>命令完成</h3>
<p>如您所见，您经常在命令行上向别名添加更多单词。例如：</p>
<pre><code>$kgpooyaml test-pod-d4b77b989
</code></pre>
<p>如果你使用kubectl命令完成，那么你可能习惯于自动完成资源名称之类的事情。但是当你使用别名时，你还可以这样做吗？</p>
<p>这是一个重要的问题，因为如果它不起作用，那将消除这些别名的一些好处！</p>
<p>答案取决于您使用的shell。</p>
<p>对于Zsh，完成对于别名是开箱即用的。</p>
<p>不幸的是，对于Bash，默认情况下，对于别名，完成功能不起作用。好消息是它可以通过一些额外的步骤来完成。下一节将介绍如何执行此操作。</p>
<h3>在Bash中启用别名的完成</h3>
<p>Bash的问题在于它尝试在别名上尝试完成（每当你按Tab键），而不是在别名命令（如Zsh）上。由于您没有所有800个别名的完成脚本，因此不起作用。</p>
<p><a href="https://github.com/cykerway/complete-alias">complete-alias</a>项目提供了解决这个问题的通用解决方案。它使用别名的完成机制，在内部将别名扩展到别名命令，并返回扩展命令的完成建议。这意味着，它使别名的完成行为与别名命令完全相同。</p>
<p>在下文中，我将首先解释如何安装complete-alias，然后如何配置它以启用所有kubectl别名的完成。</p>
<h4>安装complete-alias</h4>
<p>首先，complete-alias依赖于<a href="https://github.com/scop/bash-completion">bash-completion</a>。因此，您需要确保在安装complete-alias之前安装了bash-completion。早先已经为Linux和macOS提供了相关说明。</p>
<blockquote>
<p>对于macOS用户的重要注意事项：与kubectl完成脚本一样，complete-alias不适用于Bash 3.2，这是macOS上Bash的默认版本。特别是，complete-alias依赖于bash-completion v2（brew install bash-completion@2），它至少需要Bash 4.1。这意味着，要在macOS上使用complete-alias，您需要安装较新版本的Bash。</p>
</blockquote>
<p>要安装complete-alias，您只需bash_completion.sh从GitHub存储库下载脚本，并将其在您的~/.bashrc文件中source：</p>
<pre><code>source ~/bash_completion.sh
</code></pre>
<p>重新加载shell后，应正确安装complete-alias。</p>
<h4>启用kubectl别名的完成</h4>
<p>从技术上讲，complete-alias提供了_complete_aliasshell函数。此函数检查别名并返回别名命令的完成建议。</p>
<p>要将其与特定别名挂钩，您必须使用completeBash内置来设置别名_complete_alias的完成功能。</p>
<p>举个例子，我们k来看一下代表kubectl命令的别名。要设置_complete_alias此别名的完成功能，您必须执行以下命令：</p>
<pre><code>$complete -F _complete_alias k
</code></pre>
<p>这样做的结果是，无论何时在k别名上自动完成，_complete_alias都会调用该函数，该函数检查别名并返回kubectl命令的完成建议。</p>
<p>作为另一个例子，让我们采用kg代表的别名kubectl get：</p>
<pre><code>$complete -F _complete_alias kg
</code></pre>
<p>同样，这样做的结果是，当您自动完成时kg，您将获得与之相同的完成建议kubectl get。</p>
<blockquote>
<p>请注意，可以以这种方式对系统上的任何别名使用complete-alias。</p>
</blockquote>
<p>因此，要启用所有 kubectl别名的完成，您只需为每个别名运行上述命令。以下代码片段完全相同（假设您安装了kubectl-aliases ~/.kubectl-aliases）：</p>
<pre><code>for _a in $(sed '/^alias /!d;s/^alias //;s/=.*$//' ~/.kubectl_aliases); do
  complete -F _complete_alias "$_a"
done
</code></pre>
<p>只需将此片段添加到您的~/.bashrc文件中，重新加载您的shell，现在您应该可以使用所有800 kubectl别名的完成！</p>
<h2>6. 使用插件扩展kubectl</h2>
<p>从<a href="https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.12.md#sig-cli-1">版本1.12</a>开始，kubectl包含一个<a href="https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/">插件机制</a>，允许您使用自定义命令扩展kubectl。</p>
<p>以下是kubectl插件的示例，可以调用为kubectl hello：</p>
<pre><code>$ kubectl hello
Hello, I'm a kubectl plugin!
</code></pre>
<blockquote>
<p>kubectl插件机制将严格遵循Git插件机制的设计。</p>
</blockquote>
<p>本节将向您展示如何安装插件，您可以在哪里找到现有的插件，以及如何创建自己的插件。</p>
<h4>安装插件</h4>
<p>Kubectl插件作为简单的可执行文件分发，其名称的形式为kubectl-x。前缀kubectl-是必需的，接下来是允许调用插件的新kubectl子命令。</p>
<p>例如，上面显示的hello插件将作为名为的文件分发kubectl-hello。</p>
<p>要<a href="https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/#installing-kubectl-plugins">安装插件</a>，您只需将kubectl-x文件复制到您的任何目录中PATH并使其可执行（例如，使用chmod +x）。之后，您可以立即调用该插件kubectl x。</p>
<p>您可以使用以下命令列出系统上当前安装的所有插件：</p>
<pre><code>$kubectl plugin list
</code></pre>
<p>如果您有多个具有相同名称的插件，或者存在不可执行的插件文件，则此命令还会显示警告。</p>
<h3>使用krew查找和安装插件</h3>
<p>Kubectl插件可以像软件包一样共享和重用。但是在哪里可以找到其他人共享的插件？</p>
<p>该<a href="https://github.com/GoogleContainerTools/krew">krew项目</a>旨在提供一个统一的解决方案，共享，查找，安装和管理kubectl插件。该项目将自己称为“kubectl插件的包管理器”（名称krew是brew的提示）。</p>
<p>Krew 以kubectl<a href="https://github.com/GoogleContainerTools/krew-index">插件索引</a>为中心，您可以从中选择和安装。</p>
<pre><code>$ kubectl krew search | less
$ kubectl krew search view
$ kubectl krew info view-utilization
$ kubectl krew install view-utilization
$ kubectl krew list
</code></pre>
<p>如您所见，krew本身是一个kubectl插件。这意味着，安装krew本质上就像安装任何其他kubectl插件一样。您可以在<a href="https://github.com/GoogleContainerTools/krew/#installation">GitHub页面</a>上找到krew的详细安装说明。</p>
<p>最重要的krew命令如下：</p>
<pre><code># Search the krew index (with an optional search query)
$ kubectl krew search [&lt;query&gt;]
# Display information about a plugin
$ kubectl krew info &lt;plugin&gt;
# Install a plugin
$ kubectl krew install &lt;plugin&gt;
# Upgrade all plugins to the newest versions
$ kubectl krew upgrade
# List all plugins that have been installed with krew
$ kubectl krew list
# Uninstall a plugin
$ kubectl krew remove &lt;plugin&gt;
</code></pre>
<p>请注意，使用krew安装插件并不妨碍以传统方式安装插件。即使你使用krew，你仍然可以通过其他方式安装你在其他地方找到的插件（或自己创建）。</p>
<blockquote>
<p>请注意，该kubectl krew list命令仅列出已使用krew安装的插件，而该kubectl plugin list命令列出了所有插件，即使用krew安装的插件和以其他方式安装的插件。</p>
</blockquote>
<h3>在其他地方寻找插件</h3>
<p>Krew仍然是一个年轻的项目，目前<a href="https://github.com/GoogleContainerTools/krew-index/">krew索引</a>中只有大约30个插件。如果你在那里找不到你需要的东西，你可以在其他地方寻找插件，例如，在GitHub上。</p>
<p>我建议查看<a href="https://github.com/topics/kubectl-plugins">kubectl-plugins GitHub主题</a>。你会发现有几十个可用的插件值得一看。</p>
<h3>创建自己的插件</h3>
<p>当然，您可以<a href="https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/#writing-kubectl-plugins">创建自己的kubectl插件</a>，这很容易实现。</p>
<p>您只需创建一个可执行文件，执行您想要的操作，为其命名kubectl-x，然后按上述方法安装它。</p>
<p>可执行文件可以是任何类型，Bash脚本，编译的Go程序，Python脚本，它确实无关紧要。唯一的要求是它可以由操作系统直接执行。</p>
<p>我们现在创建一个示例插件。在上部分中，您使用kubectl命令列出每个pod的容器镜像。您可以轻松地将此命令转换为可以调用的插件，比如说kubectl img。</p>
<p>为此，只需创建一个名为kubectl-img以下内容的文件：</p>
<pre><code>#!/bin/bash
kubectl get pods -o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image'

</code></pre>
<p>现在使文件可执行，chmod +x kubectl-img并将其移动到您的任何PATH中的目录。之后，您可以立即开始使用该插件kubectl img！</p>
<blockquote>
<p>如上所述，kubectl插件可以用任何编程语言或脚本语言编写。如果使用shell脚本，则可以从插件轻松调用kubectl。但是，您可以使用实际编程语言编写更复杂的插件，例如，使用<a href="https://kubernetes.io/docs/reference/using-api/client-libraries/">Kubernetes客户端库</a>。如果使用<a href="https://tonybai.com/tag/go">Go</a>，您还可以使用<a href="https://github.com/kubernetes/cli-runtime">cli-runtime库</a>，它专门用于编写kubectl插件。</p>
</blockquote>
<h3>分享你的插件</h3>
<p>如果您认为其中一个插件可能对其他人有用，请随时在GitHub上分享。确保将其添加到<a href="https://github.com/topics/kubectl-plugins">kubectl-plugins主题</a>中，以便其他人可以找到它。</p>
<p>您还可以请求将您的插件添加到<a href="https://github.com/GoogleContainerTools/krew-index/">krew索引</a>中。您可以在<a href="https://github.com/GoogleContainerTools/krew/blob/master/docs/DEVELOPER_GUIDE.md">krew GitHub存储库</a>中找到有关如何执行此操作的说明。</p>
<h3>命令完成</h3>
<p>目前，插件机制遗憾的是还不支持命令完成。这意味着您需要完全键入插件名称以及插件的任何参数。</p>
<p>但是，在kubectl GitHub存储库中有一个处于open状态的<a href="https://github.com/kubernetes/kubectl/issues/585">功能请求issue</a>。因此，此功能有可能在将来的某个时间得到实现。</p>
<p>以上就是有关kubectl高效使用的所有内容了！</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/08/31/kubectl-productivity-part3/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>提高您的kubectl生产力（第二部分）：命令完成、资源规范快速查看和自定义列输出格式</title>
		<link>https://tonybai.com/2019/08/30/kubectl-productivity-part2/</link>
		<comments>https://tonybai.com/2019/08/30/kubectl-productivity-part2/#comments</comments>
		<pubDate>Fri, 30 Aug 2019 06:39:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[apiserver]]></category>
		<category><![CDATA[apt]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[bashrc]]></category>
		<category><![CDATA[bash_profile]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[jsonpath]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kube-controller-manager]]></category>
		<category><![CDATA[kube-scheduler]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[RESTAPI]]></category>
		<category><![CDATA[RESTFUL]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[xpath]]></category>
		<category><![CDATA[yaml]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[zsh]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2753</guid>
		<description><![CDATA[本文翻译自《Boosting your kubectl productivity》。 第一部分：什么是kubectl？ 1. 通过命令完成(command completion)减少输入 命令完成是提高你的kubectl生产力的最有用但经常被忽视的技巧之一。 命令完成允许您使用Tab键自动完成kubectl命令的各个部分。这适用于子命令，选项和参数，包括资源名称等难以输入的内容。 在这里你可以看到kubectl命令完成的动作： 命令完成在Bash和Zsh shell下均可用。 在官方文档中包含有关设置命令完成的详细说明，下面的章节我们再带着大家回顾一下。 命令完成的工作原理 通常，命令完成是一个shell功能，它通过completion script(完成脚本)的方式工作。完成脚本是一个shell脚本，用于定义特定命令的完成行为。获取完成脚本可以完成相应的命令。 Kubectl可以使用以下命令自动生成并打印出Bash和Zsh的完成脚本： $kubectl completion bash # or $kubectl completion zsh 理论上，在适当的shell中获取此命令的输出可以完成kubectl命令。 但是，在实践中，Bash（包括Linux和macOS之间的差异）和Zsh的细节不同。以下部分解释了所有这些情况： 在Linux上为Bash设置命令完成 在macOS上设置Bash的命令完成 设置Zsh的命令完成 在Linux上的Bash Bash的完成脚本取决于bash-completion项目，因此您必须先安装它。 您可以使用各种包管理器安装bash-completion 。例如： $sudo apt-get install bash-completion # or $yum install bash-completion 您可以使用以下命令测试是否正确安装了bash-completion： $type _init_completion 如果这输出shell函数的代码，则已正确安装bash-completion。如果该命令输出not found错误，则必须将以下行添加到您的~/.bashrc文件中： $source /usr/share/bash-completion/bash_completion 是否必须将此行添加到您的~/.bashrc文件中，取决于您用于安装bash-completion的包管理器。对于APT来说，这是必要的，对于yum，则无需。 安装bash-completion后，您必须进行设置，以便在所有shell会话中获取kubectl 完成脚本。 一种方法是将以下行添加到您的~/.bashrc文件中： [...]]]></description>
			<content:encoded><![CDATA[<p>本文翻译自<a href="https://learnk8s.io/blog/kubectl-productivity/">《Boosting your kubectl productivity》</a>。</p>
<p>第一部分：<a href="https://tonybai.com/2019/08/29/kubectl-productivity-part1/">什么是kubectl？</a></p>
<h2>1. 通过命令完成(command completion)减少输入</h2>
<p>命令完成是提高你的kubectl生产力的最有用但经常被忽视的技巧之一。</p>
<p>命令完成允许您使用Tab键自动完成kubectl命令的各个部分。这适用于子命令，选项和参数，包括资源名称等难以输入的内容。</p>
<p>在这里你可以看到kubectl命令完成的动作：</p>
<p><img src="https://tonybai.com/wp-content/uploads/kubectl/kubectl-productivity-part2-1.gif" alt="img{512x368}" /></p>
<p>命令完成在<a href="https://www.gnu.org/software/bash/">Bash</a>和<a href="https://www.zsh.org/">Zsh shell</a>下均可用。</p>
<p>在<a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/#enabling-shell-autocompletion">官方文档</a>中包含有关设置命令完成的详细说明，下面的章节我们再带着大家回顾一下。</p>
<h3>命令完成的工作原理</h3>
<p>通常，命令完成是一个shell功能，它通过completion script(完成脚本)的方式工作。完成脚本是一个shell脚本，用于定义特定命令的完成行为。获取完成脚本可以完成相应的命令。</p>
<p>Kubectl可以使用以下命令自动生成并打印出Bash和Zsh的完成脚本：</p>
<pre><code>$kubectl completion bash
# or
$kubectl completion zsh
</code></pre>
<p>理论上，在适当的shell中获取此命令的输出可以完成kubectl命令。</p>
<p>但是，在实践中，Bash（包括Linux和macOS之间的差异）和Zsh的细节不同。以下部分解释了所有这些情况：</p>
<ul>
<li>在Linux上为Bash设置命令完成</li>
<li>在macOS上设置Bash的命令完成</li>
<li>设置Zsh的命令完成</li>
</ul>
<h3>在Linux上的Bash</h3>
<p>Bash的完成脚本取决于<a href="https://github.com/scop/bash-completion">bash-completion项目</a>，因此您必须先安装它。</p>
<p>您可以使用<a href="https://github.com/scop/bash-completion#installation">各种包管理器</a>安装bash-completion 。例如：</p>
<pre><code>$sudo apt-get install bash-completion
# or
$yum install bash-completion
</code></pre>
<p>您可以使用以下命令测试是否正确安装了bash-completion：</p>
<pre><code>$type _init_completion

</code></pre>
<p>如果这输出shell函数的代码，则已正确安装bash-completion。如果该命令输出not found错误，则必须将以下行添加到您的~/.bashrc文件中：</p>
<pre><code>$source /usr/share/bash-completion/bash_completion
</code></pre>
<blockquote>
<p>是否必须将此行添加到您的~/.bashrc文件中，取决于您用于安装bash-completion的包管理器。对于APT来说，这是必要的，对于yum，则无需。</p>
</blockquote>
<p>安装bash-completion后，您必须进行设置，以便在所有shell会话中获取kubectl 完成脚本。</p>
<p>一种方法是将以下行添加到您的~/.bashrc文件中：</p>
<pre><code>source &lt;(kubectl completion bash)
</code></pre>
<p>另一种可能性是将kubectl完成脚本添加到/etc/bash_completion.d目录中（如果它不存在则创建它）：</p>
<pre><code>$kubectl completion bash &gt;/etc/bash_completion.d/kubectl
</code></pre>
<blockquote>
<p>/etc/bash_completion.d目录中的所有完成脚本都是由bash-completion自动获取的。</p>
</blockquote>
<p>两种方法都是等价的。</p>
<p>重新加载shell后，kubectl命令完成应该正常工作！</p>
<h3>在MacOS上的Bash</h3>
<p>有了macOS，就会出现轻微的复杂情况。原因是macOS上的Bash默认版本是3.2，这已经过时了。遗憾的是，kubectl完成脚本至少需要Bash 4.1，因此不适用于Bash 3.2。</p>
<blockquote>
<p>Apple在macOS中包含过时版本的Bash的原因是较新版本使用Apple不支持的GPLv3许可证。</p>
</blockquote>
<p>这意味着，要在macOS上使用kubectl命令完成，<strong>您必须安装较新版本的Bash</strong>。您甚至可以将它设为新的默认shell，这将为您节省很多此类麻烦。这实际上并不困难，您可以在我之前编写的macOS文章中的升级Bash中找到说明。</p>
<p><strong>在继续之前，请确保您现在确实使用的是Bash 4.1或更新版本（请查看bash &#8211;version）</strong>。</p>
<p>Bash的完成脚本取决于<a href="https://github.com/scop/bash-completion">bash-completion项目</a>，因此您必须先安装它。</p>
<p>您可以使用<a href="https://brew.sh/">Homebrew</a>安装bash-completion ：</p>
<pre><code>$brew install bash-completion@2
</code></pre>
<blockquote>
<p>bash-completion v2的@2代表。kubectl完成脚本需要bash-completion v2，而bash-completion v2至少需要Bash 4.1。这就是您不能在低于4.1的Bash版本上使用kubectl完成脚本的原因。</p>
</blockquote>
<p>该brew install命令的输出包含一个“警告”部分，其中包含将以下行添加到您的~/.bash_profile文件的说明：</p>
<pre><code>export BASH_COMPLETION_COMPAT_DIR=/usr/local/etc/bash_completion.d
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] &amp;&amp; . "/usr/local/etc/profile.d/bash_completion.sh"
</code></pre>
<p>您必须这样做才能完成bash-completion的安装。但是，我建议将这些行添加到您~/.bashrc文件中而不是~/.bash_profile文件中。这能确保子shell中也可以使用bash-completion。</p>
<p>重新加载shell后，可以使用以下命令测试是否正确安装了bash-completion：</p>
<pre><code>$type _init_completion
</code></pre>
<p>如果这输出shell函数的代码，那么你就完成了。</p>
<p>现在，您必须进行设置以便kubectl 完成脚本在所有shell会话中获取。</p>
<p>一种方法是将以下行添加到您的~/.bashrc文件中：</p>
<pre><code>source &lt;(kubectl completion bash)
</code></pre>
<p>另一种可能性是将kubectl完成脚本添加到/usr/local/etc/bash_completion.d目录：</p>
<pre><code>$kubectl completion bash &gt;/usr/local/etc/bash_completion.d/kubectl
</code></pre>
<blockquote>
<p>这仅在您使用Homebrew安装bash-completion时才有效。在这种情况下，bash-completion会在此目录中提供所有完成脚本。</p>
</blockquote>
<p>如果您还使用Homebrew安装了kubectl，您甚至不必执行上述步骤，因为完成脚本应该已经通过kubectl howbrew formula放在/usr/local/etc/bash_completion.d目录中了。在这种情况下，kubectl完成应该在安装bash-completion后自动开始工作。</p>
<p>最后，所有这些方法都是等效的。</p>
<p>重新加载shell后，kubectl完成应该正常工作！</p>
<h3>Zsh</h3>
<p>Zsh的完成脚本没有任何依赖项。因此，您所要做的就是设置所有内容，以便在所有shell会话中获取源代码。</p>
<p>您可以通过在~/.zshrc文件中添加以下行来完成此操作：</p>
<pre><code>source &lt;(kubectl completion zsh)
</code></pre>
<p>如果在重新加载shell后出现错误:command not found: compdef，则必须启用compdef内置功能，您可以通过将以下内容添加到~/.zshrc文件的开头来执行此操作：</p>
<pre><code>autoload -Uz compinit
compinit
</code></pre>
<h2>2. 快速查找资源规范</h2>
<p>创建YAML资源定义时，您需要知道这些资源的字段及其含义。一个可以查找到此类信息的位置是在<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/">API参考文档</a>中，那里包含了所有资源的完整规范。</p>
<p>但是，每次需要查找某些内容时都要切换到Web浏览器很乏味。因此，kubectl提供了kubectl explain命令，可以打印出终端中所有资源的资源规范。</p>
<p>kubectl explain用法如下：</p>
<pre><code>$kubectl explain resource[.field]...
</code></pre>
<p>该命令输出所请求资源或字段的规范。kubectl explain显示的信息与API参考中的信息相同。</p>
<p>默认情况下，kubectl explain仅显示单个级别的字段。您可以使用显示整个字段树的标志:&#8211;recursive：</p>
<pre><code>$kubectl explain deployment.spec --recursive
</code></pre>
<p>如果您不确定可以使用哪些资源名称，可以使用kubectl explain以下命令显示所有这些名称：</p>
<pre><code>$kubectl api-resources
</code></pre>
<p>此命令以复数形式显示资源名称（例如，deployments而不是deployment）。对于拥有短名称的资源，它还显示该资源的短名称（例如：deploy）。不要担心这些差异，对于kubectl来说，所有这些名称变体都是等同的。也就是说，你可以在kubectl explain中使用它们中的任何一个。</p>
<p>例如，以下所有命令都是等效的：</p>
<pre><code>$kubectl explain deployments.spec
# or
$kubectl explain deployment.spec
# or
$kubectl explain deploy.spec
</code></pre>
<h2>3. 使用自定义列输出格式</h2>
<p>kubectl get命令的默认输出格式（用于读取资源）如下：</p>
<pre><code>$kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
engine-544b6b6467-22qr6   1/1     Running   0          78d
engine-544b6b6467-lw5t8   1/1     Running   0          78d
engine-544b6b6467-tvgmg   1/1     Running   0          78d
web-ui-6db964458-8pdw4    1/1     Running   0          78d

</code></pre>
<p>这对于人类而言，是一种很好的可读格式，但它只包含有限的信息。如您所见，每个资源只显示一些字段（与完整资源定义相比）。</p>
<p>这就是自定义列输出格式的用武之地。它允许您自由定义要显示在其中的列和数据。您可以选择要在输出中显示为单独列的资源的任何字段</p>
<p>自定义列输出选项的用法如下：</p>
<pre><code>-o custom-columns=&lt;header&gt;:&lt;jsonpath&gt;[,&lt;header&gt;:&lt;jsonpath&gt;]...
</code></pre>
<p>您必须将每个输出列定义为一</p>
<p>&lt;</p>
<p>header>:<jsonpath>对：</p>
<ul>
<li>
<p>&lt;</p>
</li>
</ul>
<p>header> 是列的名称，您可以选择任何您想要的。<br />
* <jsonpath> 是一个选择资源字段的表达式（在下面更详细地说明）。</p>
<p>我们来看一个简单的例子：</p>
<pre><code>$ kubectl get pods -o custom-columns='NAME:metadata.name'
NAME
engine-544b6b6467-22qr6
engine-544b6b6467-lw5t8
engine-544b6b6467-tvgmg
web-ui-6db964458-8pdw4
</code></pre>
<p>这里，输出包含一个显示所有Pod名称的列。</p>
<p>选择Pod名称的表达式是metadata.name。这样做的原因是Pod的名称在Pod资源字段的metadata的name字段中定义（您可以在API参考中查找或使用kubectl explain pod.metadata.name）。</p>
<p>现在，假设您要在输出中添加一个附加列，例如，显示每个Pod正在运行的节点。为此，您只需向自定义列选项添加适当的列规范：</p>
<pre><code>$kubectl get pods \
  -o custom-columns='NAME:metadata.name,NODE:spec.nodeName'
NAME                      NODE
engine-544b6b6467-22qr6   ip-10-0-80-67.ec2.internal
engine-544b6b6467-lw5t8   ip-10-0-36-80.ec2.internal
engine-544b6b6467-tvgmg   ip-10-0-118-34.ec2.internal
web-ui-6db964458-8pdw4    ip-10-0-118-34.ec2.internal
</code></pre>
<p>选择节点名称的表达式是spec.nodeName。这是因为已调度Pod的节点保存在Pod的spec.nodeName字段中（请参阅参考资料kubectl explain pod.spec.nodeName）。</p>
<blockquote>
<p>请注意，Kubernetes资源字段区分大小写。</p>
</blockquote>
<p>您可以通过这种方式将资源的任何字段设置为输出列。只需浏览资源规范并尝试使用您喜欢的任何字段！</p>
<p>但首先，让我们仔细看看这些字段选择表达式。</p>
<h3>JSONPath表达式</h3>
<p>选择资源字段的表达式基于<a href="https://goessner.net/articles/JsonPath/index.html">JSONPath</a>。</p>
<p>JSONPath是一种从JSON文档中提取数据的语言（类似于XPath for XML）。选择单个字段只是JSONPath的最基本用法。它有很多功能，如列表选择器，过滤器等。</p>
<p>但是，kubectl explain仅支持JSONPath功能的一部分。以下通过示例用法总结了这些支持的功能：</p>
<pre><code># Select all elements of a list
$kubectl get pods -o custom-columns='DATA:spec.containers[*].image'

# Select a specific element of a list
$kubectl get pods -o custom-columns='DATA:spec.containers[0].image'

# Select those elements of a list that match a filter expression
$kubectl get pods -o custom-columns='DATA:spec.containers[?(@.image!="nginx")].image'

# Select all fields under a specific location, regardless of their name
$kubectl get pods -o custom-columns='DATA:metadata.*'

# Select all fields with a specific name, regardless of their location
$kubectl get pods -o custom-columns='DATA:..image'
</code></pre>
<p>特别重要的是[]操作符。Kubernetes资源的许多字段都是列表，此运算符允许您选择这些列表中的项目。它通常与通配符一起使用，[*]以选择列表中的所有项目。</p>
<p>您将在下面找到一些使用此表示法的示例。</p>
<h3>示例应用程序</h3>
<p>使用自定义列输出格式的可能性是无穷无尽的，因为您可以在输出中显示资源的任何字段或字段组合。以下是一些示例应用程序，但您可以自己探索并找到对您有用的应用程序！</p>
<blockquote>
<p>提示：如果您经常使用其中一个命令，则可以为其创建shell别名。</p>
</blockquote>
<h4>显示Pods的容器镜像</h4>
<pre><code>$kubectl get pods \
  -o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image'
NAME                       IMAGES
engine-544b6b6467-22qr6    rabbitmq:3.7.8-management,nginx
engine-544b6b6467-lw5t8    rabbitmq:3.7.8-management,nginx
engine-544b6b6467-tvgmg    rabbitmq:3.7.8-management,nginx
web-ui-6db964458-8pdw4     wordpress

</code></pre>
<p>此命令显示每个Pod的所有容器镜像的名称。</p>
<blockquote>
<p>请记住，Pod可能包含多个容器。在这种情况下，单个Pod的容器镜像在同一列中显示为逗号分隔列表。</p>
</blockquote>
<h4>显示节点的可用区域</h4>
<pre><code>$kubectl get nodes \
  -o custom-columns='NAME:metadata.name,ZONE:metadata.labels.failure-domain\.beta\.kubernetes\.io/zone'
NAME                          ZONE
ip-10-0-118-34.ec2.internal   us-east-1b
ip-10-0-36-80.ec2.internal    us-east-1a
ip-10-0-80-67.ec2.internal    us-east-1b
</code></pre>
<p>如果您的Kubernetes群集部署在公共云基础架构（例如AWS，Azure或GCP）上，则此命令非常有用。它显示每个节点所在的可用区域。</p>
<blockquote>
<p>可用区域是云的概念，表示地理区域内的一个可复制点。</p>
</blockquote>
<p>每个节点的可用区域通过特殊标签failure-domain.beta.kubernetes.io/zone获得。如果集群在公共云基础结构上运行，则会自动创建此标签，并将其值设置为节点的可用区域的名称。</p>
<p>标签不是Kubernetes资源规范的一部分，因此您无法在API参考中找到上述标签。但是，如果将节点输出为YAML或JSON，则可以看到它（以及所有其他标签）：</p>
<pre><code>$kubectl get nodes -o yaml
# or
$kubectl get nodes -o json
</code></pre>
<p>除了探索资源规范之外，这通常是发现有关资源的更多信息的好方法。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/08/30/kubectl-productivity-part2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>docker容器内服务程序的优雅退出</title>
		<link>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/</link>
		<comments>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/#comments</comments>
		<pubDate>Thu, 09 Oct 2014 13:58:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BestPractice]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Dockerfile]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[nsenter]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[supervisor]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[yum]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[命令行]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[映像]]></category>
		<category><![CDATA[最佳实践]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[虚拟化]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1555</guid>
		<description><![CDATA[近期在试验如何将我们的产品部署到docker容器中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。 一、优雅退出的原理 对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的&#8220;优雅退出&#8221;操作。 与&#8220;优雅退出&#8221;对立的是&#8220;暴力退出&#8221;，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致&#8220;不一致&#8221;状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。 二、测试用&#8220;服务程序&#8221; 为了测试docker容器对优雅退出的支持，我们编写如下&#8220;服务程序&#8221;用于放在docker容器中运行： //dockerapp1.go package main import &#34;fmt&#34; import &#34;time&#34; import &#34;os&#34; import &#34;os/signal&#34; import &#34;syscall&#34; type signalHandler func(s os.Signal, arg interface{}) type signalSet struct { &#160;&#160;&#160;&#160;&#160;&#160;&#160; m map[os.Signal]signalHandler } func signalSetNew() *signalSet { &#160;&#160;&#160;&#160;&#160;&#160;&#160; ss := new(signalSet) [...]]]></description>
			<content:encoded><![CDATA[<p><span style="line-height: 1.6em;">近期在试验如何将我们的产品部署到</span><a href="http://docker.com" style="line-height: 1.6em;">docker容器</a><span style="line-height: 1.6em;">中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。</span></p>
<p><b>一、优雅退出的原理</b></p>
<p>对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的&ldquo;优雅退出&rdquo;操作。</p>
<p>与&ldquo;优雅退出&rdquo;对立的是&ldquo;暴力退出&rdquo;，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致&ldquo;不一致&rdquo;状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。</p>
<p><b>二、</b><b>测试用&ldquo;服务程序&rdquo;</b></p>
<p>为了测试docker容器对优雅退出的支持，我们编写如下&ldquo;服务程序&rdquo;用于放在docker容器中运行：</p>
<p><font face="Courier New">//dockerapp1.go</font></p>
<p><font face="Courier New">package main</font></p>
<p><font face="Courier New">import &quot;fmt&quot;<br />
	import &quot;time&quot;<br />
	import &quot;os&quot;<br />
	import &quot;os/signal&quot;<br />
	import &quot;syscall&quot;</font></p>
<p><font face="Courier New">type signalHandler func(s os.Signal, arg interface{})</font></p>
<p><font face="Courier New">type signalSet struct {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m map[os.Signal]signalHandler<br />
	}</font></p>
<p><font face="Courier New">func signalSetNew() *signalSet {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := new(signalSet)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.m = make(map[os.Signal]signalHandler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return ss<br />
	}</font></p>
<p><font face="Courier New">func (set *signalSet) register(s os.Signal, handler signalHandler) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, found := set.m[s]; !found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[s] = handler<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p><font face="Courier New">func (set *signalSet) handle(sig os.Signal, arg interface{}) (err error) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if _, found := set.m[sig]; found {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; set.m[sig](sig, arg)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return nil<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } else {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fmt.Errorf(&quot;No handler available for signal %v&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; panic(&quot;won&#39;t reach here&quot;)<br />
	}</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</font></p>
<p><font face="Courier New">func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;signal termiate received, app exit normally\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGTERM, handler)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;unknown signal received: %v, app exit unexpectedly\n&quot;, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>关于<a href="http://tonybai.com/tag/golang">Go语言</a>对系统Signal的处理，可以参考《<a href="http://tonybai.com/2012/09/21/signal-handling-in-go/">Go中的系统Signal处理</a>》一文。</p>
<p><b>三、制作测试用docker image</b></p>
<p>在《 <a href="http://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/">Ubuntu Server 14.04安装docker</a>》一文中，我们完成了在ubuntu 14.04上安装docker的步骤。要制作测试用docker image，我们首先需要pull一个base image。我们以CentOS6.5为例：</p>
<p>在Ubuntu 14.04上执行：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">sudo&nbsp; docker pull centos:centos6</font></p>
<p>docker会自动从<a href="https://registry.hub.docker.com">官方仓库</a>下载一个制作好的docker image。下载成功后，我们可以run一下试试，像这样：</p>
<p><font face="Courier New">$&gt; sudo docker run -t -i centos:centos6 /bin/bash</font></p>
<p>我们查看一下CentOS6的小版本：<br />
	<font face="Courier New">$&gt; cat /etc/centos-release<br />
	CentOS release 6.5 (Final)</font></p>
<p>这是一个极其精简的CentOS，各种工具均未安装：<br />
	<font face="Courier New">bash-4.1# telnet<br />
	bash: telnet: command not found<br />
	bash-4.1# ssh<br />
	bash: ssh: command not found<br />
	bash-4.1# ftp<br />
	bash: ftp: command not found<br />
	bash-4.1# echo $PATH<br />
	/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</font></p>
<p>如果你要安装一些必要的工具，可以直接使用yum install，默认的base image已经将yum配置好了，可以直接使用。如果通过公司代理访问外部网络，别忘了先export http_proxy。另外docker直接使用宿主机的/etc/resolv.conf作为容器的DNS，我们也无需额外设置DNS。</p>
<p>接下来，我们就制作我们的第一个测试用image。安装官方推荐的Best Practice，我们使用Dockerfile来bulid一个测试用image。步骤如下：</p>
<p>- 建立~/ImagesFactory目录<br />
	- 将构建好的dockerapp1拷贝到~/ImagesFactory目录下<br />
	- 进入~/ImagesFactory目录，创建Dockerfile文件，Dockerfile内容如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	COPY ./dockerapp1 /bin<br />
	CMD /bin/dockerapp1</font></p>
<p>- 执行docker build，结果如下：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v1&quot; ./<br />
	Sending build context to Docker daemon 7.496 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp1 /bin<br />
	2014/10/09 16:05:25 lchown /var/lib/docker/aufs/mnt/fb0e864d3f07ca17ef8b6b69f034728e1f1158fd3f9c83fa48243054b2f26958/bin/dockerapp1: not a directory</font></p>
<p>居然build失败，提示什么not a directory。于是各种Search，终于发现问题所在，原来是&ldquo;<font face="Courier New">COPY ./dockerapp1 /bin</font>&rdquo;这条命令错了，少了个&ldquo;/&rdquo;，将&quot; /bin&quot;改为&ldquo;/bin/&rdquo;就OK了，Docker真是奇怪啊，这块明显应该做得更兼容些。新的Dockerfile如下：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	COPY ./dockerapp1 /bin/<br />
	CMD /bin/dockerapp1</font></p>
<p>构建结果如下：</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v1&quot; ./<br />
	Sending build context to Docker daemon 7.496 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp1 /bin/<br />
	&nbsp;&#8212;&gt; 20c3783c42ab<br />
	Removing intermediate container cab639ab4321<br />
	Step 3 : CMD /bin/dockerapp1<br />
	&nbsp;&#8212;&gt; Running in 31875d3c37f9<br />
	&nbsp;&#8212;&gt; 21a720a808a7<br />
	Removing intermediate container 31875d3c37f9<br />
	Successfully built 21a720a808a7</font></p>
<p><font face="Courier New">$ sudo docker images<br />
	REPOSITORY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TAG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; VIRTUAL SIZE<br />
	test&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 21a720a808a7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 59 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 214.6 MB</font></p>
<p><b>四、第一个测试容器</b></p>
<p>我们基于image &quot;test:v1&quot;启动一个测试容器：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v1&quot;<br />
	daf3ae88fec23a31cde9f6b9a3f40057953c87b56cca982143616f738a84dcba</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	daf3ae88fec2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 17 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 16 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; condescending_sammet&nbsp;&nbsp; </font></p>
<p>通过docker run命令，我们基于image&quot;test:v1&quot;启动了一个容器。通过docker ps命令可以看到容器成功启动，容器id：<font face="Courier New">daf3ae88fec2，别名为：</font><font face="Courier New">condescending_sammet。</font></p>
<p><font face="Courier New">根据Dockerfile我们知道，容器启动后将执行&quot;/bin/dockerapp1&quot;这个程序，dockerapp1退出，容器即退出。 run命令的&quot;-d&quot;选项表示容器将以daemon的形式运行，我们在前台无法看到容器的输出。那么我们怎么查看容器的输出呢？我们可以通过 docker logs + 容器id的方式查看容器内应用的标准输出或标准错误。我们也可以进入容器来查看。</font></p>
<p><font face="Courier New">进入容器有多种方法，比如用sudo docker attach </font><font face="Courier New"><font face="Courier New">daf3ae88fec2</font>。attach后，就好比将daemon方式运行的容器 拿到了前台，你可以Ctrl + C一下，可以看到如下dockerapp1的输出:</font></p>
<p><font face="Courier New">^Chandle signal: interrupt</font></p>
<p><font face="Courier New">另外一种方式是利用nsenter工具进入我们容器的namespace空间。ubuntu 14.04下可以通过如下方式安装该工具：</font></p>
<p><font face="Courier New">$ wget <a class="moz-txt-link-freetext" href="https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz">https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz</a>; tar xzvf util-linux-2.24.tar.gz<br />
	$ cd util-linux-2.24<br />
	$ ./configure &#8211;without-ncurses &amp;&amp; make nsenter<br />
	$ sudo cp nsenter /usr/local/bin</font></p>
<p>安装后，我们通过如下方式即可进入上面的容器：</p>
<p><font face="Courier New">$ echo $(sudo docker inspect &#8211;format &quot;{{ .State.Pid }}&quot; daf3ae88fec2)<br />
	5494<br />
	$ sudo nsenter &#8211;target 5494 &#8211;mount &#8211;uts &#8211;ipc &#8211;net &#8211;pid<br />
	-bash-4.1# ps -ef<br />
	UID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PID&nbsp; PPID&nbsp; C STIME TTY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; TIME CMD<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp; 0 09:20 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 /bin/dockerapp1<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp;&nbsp; 0&nbsp; 0 09:32 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 -bash<br />
	root&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 27&nbsp;&nbsp;&nbsp; 16&nbsp; 0 09:32 ?&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 00:00:00 ps -ef<br />
	-bash-4.1# </font></p>
<p>进入容器后通过ps命令可以看到正在运行的dockerapp1程序。在容器内，我们可以通过kill来测试dockerapp1的运行情况：</p>
<p><font face="Courier New">-bash-4.1# kill -s SIGINT 1</font></p>
<p>通过前面的attach窗口，我们可以看到dockerapp1输出:</p>
<p><font face="Courier New">handle signal: interrupt</font></p>
<p>如果你发送SIGTERM信号，那么dockerapp1将终止运行，容器也就停止了。</p>
<p><font face="Courier New">-bash-4.1# kill 1</font></p>
<p>attach窗口显示：</p>
<p><font face="Courier New">signal termiate received, app exit normally</font></p>
<p>我们可以看到容器启动后默认执行的时Dockerfile中的CMD命令，如果Dockerfile中有多行CMD命令，Docker在启动容器 时只会执行最后一条CMD命令。如果在docker run中指定了命令，docker则会执行命令行中的命令而不会执行dockerapp1，比如：</p>
<p><font face="Courier New">$ sudo docker run -t -i &quot;test:v1&quot; /bin/bash<br />
	bash-4.1# </font></p>
<p>这里我们看到直接执行的时bash，dockerapp1并未执行。</p>
<p><b>五、docker stop的行为</b></p>
<p>我们先来看看docker stop的manual：</p>
<p><font face="Courier New">$ sudo docker stop &#8211;help<br />
	Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]<br />
	Stop a running container by sending SIGTERM and then SIGKILL after a grace period<br />
	&nbsp; -t, &#8211;time=10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.</font></p>
<p>可以看出当我们执行docker stop时，docker会首先向容器内的当前主程序发送一个SIGTERM信号，用于容器内程序的退出。如果容器在收到SIGTERM后没有马上退出， 那么stop命令会在等待一段时间（默认是10s）后，再向容器发送SIGKILL信号，将容器杀死，变为退出状态。</p>
<p>我们来验证一下docker stop的行为。启动刚才那个容器：</p>
<p><font face="Courier New">$ sudo docker start daf3ae88fec2<br />
	daf3ae88fec2</font></p>
<p><font face="Courier New">attach到容器daf3ae88fec2<br />
	$ sudo docker attach daf3ae88fec2</font></p>
<p>新打开一个窗口，执行docker stop命令：<br />
	<font face="Courier New">$ sudo docker stop daf3ae88fec2<br />
	daf3ae88fec2</font></p>
<p>可以看到attach窗口输出：<br />
	<font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p>通过docker ps查看，发现容器已经退出。</p>
<p>也许通过上面的例子还不能直观的展示stop命令的<b>两阶段行为</b>，因为dockerapp1收到SIGTERM后直接就退出 了，stop命令无需等待容器慢慢退出，也无需发送SIGKILL。我们改造一下dockerapp1这个程序。</p>
<p>我们复制一下dockerapp1.go为dockerapp2.go，编辑dockerapp2.go，将handler中对SIGTERM的 处理注释掉，其他不变：</p>
<p><font face="Courier New">handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;handle signal: %v\n&quot;, s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /*<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;signal termiate received, app exit normally\n&quot;)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p>我们使用dockerapp2来构建一个新image：test:v2，将Dockerfile中得dockerapp1换成 dockerapp2即可。</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v2&quot; ./<br />
	Sending build context to Docker daemon 9.369 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp2 /bin/<br />
	&nbsp;&#8212;&gt; 27cd613a9bd7<br />
	Removing intermediate container 07c760b6223b<br />
	Step 3 : CMD /bin/dockerapp2<br />
	&nbsp;&#8212;&gt; Running in 1aac086452a7<br />
	&nbsp;&#8212;&gt; 82eb876fefd2<br />
	Removing intermediate container 1aac086452a7<br />
	Successfully built 82eb876fefd2</font></p>
<p>利用image &quot;test:v2&quot;创建一个容器来测试stop。</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v2&quot;<br />
	29f3ec1af3c355458cbbd802a5e8a53da28e9f51a56ce822c7bba2a772edceac</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	29f3ec1af3c3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 7 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 6 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; romantic_feynman&nbsp;</font>&nbsp;&nbsp;</p>
<p>Attach到这个容器并观察，在另外一个窗口stop该container。我们在attach窗口只看到如下输出：</p>
<p><font face="Courier New">handle signal: terminated</font></p>
<p>stop命令的执行没有立即返回，而是等待容器退出。等待10s后，容器退出，stop命令执行结束。从这个例子我们可以明显看出stop的两阶 段行为。</p>
<p>如果我们以<font face="Courier New">sudo docker run -i -t &quot;test:v1&quot; /bin/bash</font>形式启动容器，那stop命令会将SIGTERM发送给bash这个程序，即使你通过nsenter进入容 器，启动了dockerapp1，dockerapp1也不会收到SIGTERM，dockerapp1会随着容器的退出而被强行终止，就像被 kill -9了一样。</p>
<p><b>六、多进程容器服务</b>程序</p>
<p>上面无论是dockerapp1还是dockerapp2，都是一个单进程服务程序。如果我们在容器内执行一个多进程程序，我们该如何优雅退出 呢？我们先来编写一个多进程的服务程序dockerapp3：</p>
<p>在dockerapp1.go的基础上对main和sysSignalHandleDemo进行修改形成dockerapp3.go，修改后这两 个函数的代码如下：</p>
<p><font face="Courier New">//dockerapp3.go<br />
	&#8230; &#8230;</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; go sysSignalHandleDemo()</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pid, _, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;err fork process, err: %v\n&quot;, err)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if pid == 0 {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;i am in child process, pid = %v\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the child process wait<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;i am parent process, pid = %v\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;fork ok, childpid = %v\n&quot;, pid)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; time.Sleep(time.Hour) // make the main goroutine wait!<br />
	}</font></p>
<p><font face="Courier New">func sysSignalHandleDemo() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss := signalSetNew()<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; handler := func(s os.Signal, arg interface{}) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: handle signal: %v\n&quot;, syscall.Getpid(), s)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if s == syscall.SIGTERM {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: signal termiate received, app exit normally\n&quot;, syscall.Getpid())<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(0)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGINT, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR1, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGUSR2, handler)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ss.register(syscall.SIGTERM, handler)</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c := make(chan os.Signal)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; var sigs []os.Signal<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for sig := range ss.m {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sigs = append(sigs, sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; signal.Notify(c)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sig := &lt;-c</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err := ss.handle(sig, nil)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if err != nil {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Printf(&quot;%v: unknown signal received: %v, app exit unexpectedly\n&quot;, syscall.Getpid(), sig)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; os.Exit(1)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	}</font></p>
<p>dockerapp3利用fork创建了一个子进程，这样dockerapp3实际上是两个进程在运行，各自有自己的signal监听 goroutine，goroutine的处理逻辑是相同的。注意：由于Windows和Mac OS X不具备fork语义，因此在这两个平台上运行dockerapp3不会得到预期结果。</p>
<p>利用dockerapp3，我们创建image &quot;test:v3&quot;:</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:v3&quot; ./<br />
	[sudo] password for tonybai:<br />
	Sending build context to Docker daemon 11.24 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai <a class="moz-txt-link-rfc2396E" href="mailto:bigwhite.cn@gmail.com">&lt;bigwhite.cn@gmail.com&gt;</a><br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : COPY ./dockerapp3 /bin/<br />
	&nbsp;&#8212;&gt; 6ccf97065853<br />
	Removing intermediate container 6d85fe241939<br />
	Step 3 : CMD /bin/dockerapp3<br />
	&nbsp;&#8212;&gt; Running in 75d76380992a<br />
	&nbsp;&#8212;&gt; c9e7bf361ed7<br />
	Removing intermediate container 75d76380992a<br />
	Successfully built c9e7bf361ed7</font></p>
<p>启动基于test:v3 image的容器：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:v3&quot;<br />
	781cecb4b3628cb33e1b104ea57e506ad5cb4a44243256ebd1192af86834bae6<br />
	$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	781cecb4b362&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:v3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/sh -c /bin/doc&nbsp;&nbsp; 5 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 4 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; insane_bohr&nbsp;&nbsp;&nbsp;</font>&nbsp;&nbsp;&nbsp;</p>
<p>通过docker logs查看dockerapp3的输出：</p>
<p><font face="Courier New">$ sudo docker logs 781cecb4b362<br />
	i am parent process, pid = 1<br />
	fork ok, childpid = 13<br />
	i am in child process, pid = 13</font></p>
<p>可以看出主进程pid为1，子进程pid为13。我们通过stop停止该容器：</p>
<p><font face="Courier New">$ sudo docker stop 781cecb4b362<br />
	781cecb4b362</font></p>
<p>再次通过docker logs查看：</p>
<p><font face="Courier New">$ sudo docker logs 781cecb4b362<br />
	i am parent process, pid = 1<br />
	fork ok, childpid = 13<br />
	i am in child process, pid = 13<br />
	1: handle signal: terminated<br />
	1: signal termiate received, app exit normally</font></p>
<p>我们可以看到主进程收到了stop发来的SIGTERM并退出，主进程的退出导致容器退出，导致子进程13也无法生存，并且没有优雅退出。而在非 容器状态下，子进程是可以被init进程接管的。</p>
<p>因此对于docker容器内运行的多进程程序，stop命令只会将SIGTERM发送给容器主进程，要想让其他进程也能优雅退出，需要在主进程与 其他进程间建立一种通信机制。在主进程退出前，等待其他子进程退出。待所有其他进程退出后，主进程再退出，容器停止。这样才能保证服务程序的优雅 退出。</p>
<p><b>七、容器内启动多个服务程序</b></p>
<p>虽说docker <a href="https://docs.docker.com/articles/dockerfile_best-practices/">best practice</a>建议一个container内只放置一个服务程序，但对已有的一些遗留系统，在架构没有做出重构之前，很可能会有在一个 container中部署两个以上服务程序的情况和需求。而docker Dockerfile只允许执行一个CMD，这种情况下，我们就需要借助类似supervisor这样的进程监控管理程序来启动和管理container 内的多个程序了。</p>
<p>下面我们来自制作一个基于centos:centos6的安装了supervisord以及两个服务程序的image。我们将dockerapp1拷贝一份，并将拷贝命名为dockerapp1-brother。下面是我们的Dockerfile：</p>
<p><font face="Courier New">FROM centos:centos6<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	RUN yum install python-setuptools -y<br />
	RUN easy_install supervisor<br />
	RUN mkdir -p /var/log/supervisor<br />
	COPY ./supervisord.conf /etc/supervisord.conf<br />
	COPY ./dockerapp1 /bin/<br />
	COPY ./dockerapp1-brother /bin/<br />
	CMD ["/usr/bin/supervisord"]</font></p>
<p>supervisord的配置文件supervisord.conf内容如下：</p>
<p><font face="Courier New">; supervisor config file</font></p>
<p><font face="Courier New">[unix_http_server]<br />
	file=/var/run/supervisor.sock&nbsp;&nbsp; ; (the path to the socket file)<br />
	chmod=0700&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ; sockef file mode (default 0700)</font></p>
<p><font face="Courier New">[supervisord]<br />
	logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)<br />
	pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)<br />
	childlogdir=/var/log/supervisor&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ; (&#39;AUTO&#39; child log dir, default $TEMP)</font></p>
<p><font face="Courier New">[rpcinterface:supervisor]<br />
	supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface</font></p>
<p><font face="Courier New">[supervisorctl]<br />
	serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL&nbsp; for a unix socket</font></p>
<p><font face="Courier New">[supervisord]<br />
	nodaemon=false</font></p>
<p><font face="Courier New">[program:dockerapp1]<br />
	command=/bin/dockerapp1<br />
	stdout_logfile=/tmp/dockerapp1.log<br />
	stopsignal=TERM<br />
	stopwaitsecs=10</font></p>
<p><font face="Courier New">[program:dockerapp1-brother]<br />
	command=/bin/dockerapp1-brother<br />
	stdout_logfile=/tmp/dockerapp1-brother.log<br />
	stopsignal=QUIT<br />
	stopwaitsecs=10</font></p>
<p>开始build镜像：<br />
	&nbsp;&nbsp;&nbsp; <font face="Courier New">$&gt; sudo docker build -t=&quot;test:supervisor-v1&quot; ./<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; Successfully built d006b9ad10eb</font></p>
<p>基于该镜像，启动一个容器：<br />
	<font face="Courier New">$&gt; sudo docker run -d &quot;test:supervisor-v1&quot;<br />
	05ded2b898c90059d4c9b5c6ccc8603b6848ae767360c42bd9b36ff87fb4b9df</font></p>
<p>执行ps命令查看镜像id：<br />
	<font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES</font></p>
<p>怎么回事？Container没有启动起来？</p>
<p><font face="Courier New">$ sudo docker ps -a<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	05ded2b898c9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:supervisor-v1&nbsp;&nbsp;&nbsp; &quot;/usr/bin/supervisor&nbsp;&nbsp; 22 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Exited (0) 21 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; hungry_engelbart</font></p>
<p>通过ps -a查看，container启动是成功了，但是成功退出了。于是尝试查看一下log：</p>
<p><font face="Courier New">sudo docker logs 05ded2b898c9<br />
	/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a &quot;-c&quot; argument specifying an absolute path to a configuration file for improved security.<br />
	&nbsp; &#39;Supervisord is running as root and it is searching &#39;</font></p>
<p>似乎是supervisord转为daemon程序，容器主进程退出了，容器随之终止了。</p>
<p>看来容器内的supervisord不能以daemon形式运行，应该以前台形式run。修改一下supervisord.conf中得配置：</p>
<p>将<br />
	<font face="Courier New">[supervisord]<br />
	nodaemon=false</font></p>
<p>改为</p>
<p><font face="Courier New">[supervisord]<br />
	nodaemon=true</font></p>
<p>重新制作镜像:</p>
<p><font face="Courier New">$ sudo docker build -t=&quot;test:supervisor-v2&quot; ./<br />
	Sending build context to Docker daemon 13.12 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM centos:centos6<br />
	&nbsp;&#8212;&gt; 68edf809afe7<br />
	Step 1 : MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; c617b456934a<br />
	Step 2 : RUN yum install python-setuptools -y<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; e09c66a1ea8c<br />
	Step 3 : RUN easy_install supervisor<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 9c8797e8c27e<br />
	Step 4 : RUN mkdir -p /var/log/supervisor<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 9bfc67f8517d<br />
	Step 5 : COPY ./supervisord.conf /etc/supervisord.conf<br />
	&nbsp;&#8212;&gt; 8c514f998363<br />
	Removing intermediate container 4a185856e6ed<br />
	Step 6 : COPY ./dockerapp1 /bin/<br />
	&nbsp;&#8212;&gt; 0317bd4914d3<br />
	Removing intermediate container ac5738380854<br />
	Step 7 : COPY ./dockerapp1-brother /bin/<br />
	&nbsp;&#8212;&gt; d89711888bdf<br />
	Removing intermediate container eadc9444e716<br />
	Step 8 : CMD ["/usr/bin/supervisord"]<br />
	&nbsp;&#8212;&gt; Running in aaa042ac3914<br />
	&nbsp;&#8212;&gt; 9655256bbfed<br />
	Removing intermediate container aaa042ac3914<br />
	Successfully built 9655256bbfed</font></p>
<p>有了前面的铺垫，这次build image瞬间完成。启动容器，查看容器启动状态，查看容器内supervisord的运行日志如下：</p>
<p><font face="Courier New">$ sudo docker run -d &quot;test:supervisor-v2&quot;<br />
	61916f1c82338b28ced101b6bde119e4afb7c7fa349b4332ed51a43a4586b1b9</font></p>
<p><font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	61916f1c8233&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; test:supervisor-v2&nbsp;&nbsp; &quot;/usr/bin/supervisor&nbsp;&nbsp; 16 seconds ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 16 seconds&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; prickly_einstein</font></p>
<p><font face="Courier New">$ sudo docker logs 8eb3e9892e66</font></p>
<p><font face="Courier New">/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a &quot;-c&quot; argument specifying an absolute path to a configuration file for improved security.<br />
	&nbsp; &#39;Supervisord is running as root and it is searching &#39;<br />
	2014-10-09 14:36:02,334 CRIT Supervisor running as root (no user in config file)<br />
	2014-10-09 14:36:02,349 INFO RPC interface &#39;supervisor&#39; initialized<br />
	2014-10-09 14:36:02,349 CRIT Server &#39;unix_http_server&#39; running without any HTTP authentication checking<br />
	2014-10-09 14:36:02,349 INFO supervisord started with pid 1<br />
	2014-10-09 14:36:03,354 INFO spawned: &#39;dockerapp1&#39; with pid 14<br />
	2014-10-09 14:36:03,363 INFO spawned: &#39;dockerapp1-brother&#39; with pid 15<br />
	2014-10-09 14:36:04,368 INFO success: dockerapp1 entered RUNNING state, process has stayed up for &gt; than 1 seconds (startsecs)<br />
	2014-10-09 14:36:04,369 INFO success: dockerapp1-brother entered RUNNING state, process has stayed up for &gt; than 1 seconds (startsecs)</font></p>
<p>可以看到supervisord已经将dockerapp1和dockerapp1-brother启动起来了。</p>
<p>现在我们尝试停止容器，我们预期是supervisord在退出前通知dockerapp1和dockerapp1-brother先退出，我们可以通过 查看容器内的/tmp/dockerapp1.log和/tmp/dockerapp1-brother.log来确认supervisord是否做了通 知。</p>
<p><font face="Courier New">$ sudo docker stop 61916f1c8233<br />
	61916f1c8233</font></p>
<p><font face="Courier New">$ sudo docker logs 61916f1c8233<br />
	&#8230; &#8230;<br />
	2014-10-09 14:37:52,253 WARN received SIGTERM indicating exit request<br />
	2014-10-09 14:37:52,254 INFO waiting for dockerapp1, dockerapp1-brother to die<br />
	2014-10-09 14:37:52,254 INFO stopped: dockerapp1-brother (exit status 0)<br />
	2014-10-09 14:37:52,256 INFO stopped: dockerapp1 (exit status 0)</font></p>
<p>通过容器的log，我们看出supervisord是等待两个程序退出后才退出的，不过我们还是要看看两个程序的输出日志以最终确认。重新启动容器，通过nsenter进入到容器中。</p>
<p><font face="Courier New">-bash-4.1# vi /tmp/dockerapp1.log</font></p>
<p><font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p><font face="Courier New">-bash-4.1# vi /tmp/dockerapp1-brother.log</font></p>
<p><font face="Courier New">handle signal: terminated<br />
	signal termiate received, app exit normally</font></p>
<p>两个程序的标准输出日志证实了我们的预期。</p>
<p>BTW，在物理机上测试supervisord以daemon形式运行，当kill掉supervisord时，supervisord是不会通知其监控 和管理的程序退出的。只有在以non-daemon形式运行时，supervisord才会在退出前先通知下面的程序退出。如果在一段时间内下面程序没有 退出，supervisord在退出前会kill -9强制杀死这些程序的进程。</p>
<p>最后要说的时，在验证一些想法时，没有必要build image，我们可以直接将本地文件copy到容器中，下面是一个例子，我们将dockerapp1和dockerapp1-brother拷贝到镜像中：<br />
	<font face="Courier New">$ sudo docker ps<br />
	CONTAINER ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; IMAGE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CREATED&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; STATUS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PORTS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; NAMES<br />
	4d8982bfccc7&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; centos:centos6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;/bin/bash&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 26 minutes ago&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Up 26 minutes&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sharp_thompson&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	$ sudo docker inspect -f &#39;{{.Id}}&#39; 4d8982bfccc7<br />
	4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4<br />
	$ sudo cp dockerapp1&nbsp; /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1<br />
	$ sudo cp dockerapp1-brother&nbsp; /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1-brother</font></p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
