<?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; centos</title>
	<atom:link href="http://tonybai.com/tag/centos/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 12 Apr 2026 22:30:28 +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在十亿次循环和百万任务中表现不如Java，究竟为何？</title>
		<link>https://tonybai.com/2024/12/02/why-go-sucks/</link>
		<comments>https://tonybai.com/2024/12/02/why-go-sucks/#comments</comments>
		<pubDate>Sun, 01 Dec 2024 22:08:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Atoi]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[gnet]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Intn]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[lensm]]></category>
		<category><![CDATA[LoopUnrolling]]></category>
		<category><![CDATA[optimize]]></category>
		<category><![CDATA[OS]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[random]]></category>
		<category><![CDATA[real]]></category>
		<category><![CDATA[RES]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[strconv]]></category>
		<category><![CDATA[sys]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[top]]></category>
		<category><![CDATA[Uint32]]></category>
		<category><![CDATA[User]]></category>
		<category><![CDATA[waitgroup]]></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=4418</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/12/02/why-go-sucks 编程语言比较的话题总是能吸引程序员的眼球！ 近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由Ben Dicken (@BenjDicken) 做的语言性能测试，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的内存开销测试，对比了多种语言在处理百万任务时的内存开销。 下面是这两项测试的结果示意图： 10亿循环测试结果 百万任务内存开销测试结果 我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。 那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。 我们先从十亿次循环测试开始。 1. 循环测试跑的慢，都因编译器优化还不够 下面是作者给出的Go测试程序： // why-go-sucks/billion-loops/go/code.go package main import ( "fmt" "math/rand" "os" "strconv" ) func main() { input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line if e != nil { panic(e) } u [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/12/02/why-go-sucks">本文永久链接</a> &#8211; https://tonybai.com/2024/12/02/why-go-sucks</p>
<p>编程语言比较的话题总是能吸引程序员的眼球！</p>
<p>近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由<a href="https://benjdd.com">Ben Dicken (@BenjDicken)</a> 做的<a href="https://benjdd.com/languages/">语言性能测试</a>，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的<a href="https://hez2010.github.io/async-runtimes-benchmarks-2024/">内存开销测试</a>，对比了多种语言在处理百万任务时的内存开销。</p>
<p>下面是这两项测试的结果示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-2.png" alt="" /><br />
<center>10亿循环测试结果</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-3.png" alt="" /><br />
<center>百万任务内存开销测试结果</center></p>
<p>我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。</p>
<p>那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。</p>
<p>我们先从<strong>十亿次循环测试</strong>开始。</p>
<h2>1. 循环测试跑的慢，都因编译器优化还不够</h2>
<p>下面是作者给出的<a href="https://github.com/bddicken/languages/blob/main/loops/go/code.go">Go测试程序</a>：</p>
<pre><code>// why-go-sucks/billion-loops/go/code.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            a[i] = a[i] + j%u // Simple sum
        }
        a[i] += r // Add a random value to each element in array
    }
    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>这段代码通过命令行参数获取一个整数，然后生成一个随机数，接着通过两层循环对一个数组的每个元素进行累加，最终输出该数组中以随机数为下标对应的数组元素的值。</p>
<p>我们再来看一下”竞争对手”的测试代码。C测试代码如下：</p>
<pre><code>// why-go-sucks/billion-loops/c/code.c

#include "stdio.h"
#include "stdlib.h"
#include "stdint.h"

int main (int argc, char** argv) {
  int u = atoi(argv[1]);               // Get an input number from the command line
  int r = rand() % 10000;              // Get a random integer 0 &lt;= r &lt; 10k
  int32_t a[10000] = {0};              // Array of 10k elements initialized to 0
  for (int i = 0; i &lt; 10000; i++) {    // 10k outer loop iterations
    for (int j = 0; j &lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration
      a[i] = a[i] + j%u;               // Simple sum
    }
    a[i] += r;                         // Add a random value to each element in array
  }
  printf("%d\n", a[r]);                // Print out a single element from the array
}
</code></pre>
<p>下面是Java的测试代码：</p>
<pre><code>// why-go-sucks/billion-loops/java/code.java

package jvm;

import java.util.Random;

public class code {

    public static void main(String[] args) {
        var u = Integer.parseInt(args[0]); // Get an input number from the command line
        var r = new Random().nextInt(10000); // Get a random number 0 &lt;= r &lt; 10k
        var a = new int[10000]; // Array of 10k elements initialized to 0
        for (var i = 0; i &lt; 10000; i++) { // 10k outer loop iterations
            for (var j = 0; j &lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration
                a[i] = a[i] + j % u; // Simple sum
            }
            a[i] += r; // Add a random value to each element in array
        }
        System.out.println(a[r]); // Print out a single element from the array
    }
}
</code></pre>
<p>你可能不熟悉C或Java，但从代码的形式上来看，C、Java与Go的代码确实处于“同等条件”。这不仅意味着它们在相同的硬件和软件环境中运行，更包括它们采用了相同的计算逻辑和算法，以及一致的输入参数处理等方面的相似性。</p>
<p>为了确认一下原作者的测试结果，我在一台阿里云ECS上(amd64，8c32g，CentOS 7.9)对上面三个程序进行了测试(使用time命令测量计算耗时)，得到一个基线结果。我的环境下，C、Java和Go的编译器版本如下：</p>
<pre><code>$go version
go version go1.23.0 linux/amd64

$java -version
openjdk version "17.0.9" 2023-10-17 LTS
OpenJDK Runtime Environment Zulu17.46+19-CA (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Zulu17.46+19-CA (build 17.0.9+8-LTS, mixed mode, sharing)

$gcc -v
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
目标：x86_64-redhat-linux
配置为：../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
线程模型：posix
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
</code></pre>
<p>测试步骤与结果如下：</p>
<pre><code>Go代码测试：

$cd why-go-sucks/billion-loops/go
$go build -o code code.go
$time ./code 10
456953

real    0m3.766s
user    0m3.767s
sys 0m0.007s

C代码测试：

$cd why-go-sucks/billion-loops/c
$gcc -O3 -std=c99 -o code code.c
$time ./code 10
459383

real    0m3.005s
user    0m3.005s
sys 0m0.000s

Java代码测试：

$javac -d . code.java
$time java -cp . jvm.code 10
456181

real    0m3.105s
user    0m3.092s
sys 0m0.027s
</code></pre>
<p>从测试结果看到(基于real时间)：采用-O3优化的C代码最快，Java落后一个身位，而<strong>Go则比C慢了25%，比Java慢了21%</strong>。</p>
<blockquote>
<p>注：time命令的输出结果通常包含三个主要部分：real、user和sys。real是从命令开始执行到结束所经过的实际时间（墙钟时间），我们依次指标为准。user是程序在<strong>用户模式下执行所消耗的CPU时间</strong>。sys则是程序<strong>在内核模式下执行所消耗的CPU时间（系统调用）</strong>。如果总时间（real）略低于用户时间（user），这表明程序可能在某些时刻被调度或等待，而不是持续占用CPU。这种情况可能是由于输入输出操作、等待资源等原因。如果real时间显著小于user时间，这种情况通常发生在并发程序中，其中多个线程或进程在不同的时间段执行，导致总的用户CPU时间远大于实际的墙钟时间。sys时间保持较低，说明系统调用的频率较低，程序主要是执行计算而非进行大量的系统交互。</p>
</blockquote>
<p>这时作为Gopher的你可能会说：<strong>原作者编写的Go测试代码不够优化，我们能优化到比C还快</strong>！</p>
<p>大家都知道原代码是不够优化的，随意改改计算逻辑就能带来大幅提升。但我们不能忘了“同等条件”这个前提。你采用的优化方法，其他语言（C、Java）也可以采用。</p>
<p>那么，在不改变“同等条件”的前提下，我们还能优化点啥呢？本着能提升一点是一点的思路，我们尝试从下面几个点优化一下，看看效果：</p>
<ul>
<li>去除不必要的if判断</li>
<li>使用更快的rand实现</li>
<li>关闭边界检查</li>
<li>避免逃逸</li>
</ul>
<p>下面是修改之后的代码：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_optimize.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, _ := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    u := int32(input)
    r := int32(rand.Uint32() % 10000)   // Use Uint32 for faster random number generation
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            a[i] = a[i] + j%u // Simple sum
        }
        a[i] += r // Add a random value to each element in array
    }
    z := a[r]
    fmt.Println(z) // Print out a single element from the array
}
</code></pre>
<p>我们编译并运行一下测试：</p>
<pre><code>$cd why-go-sucks/billion-loops/go
$go build -o code_optimize -gcflags '-B' code_optimize.go
$time ./code_optimize 10
459443

real    0m3.761s
user    0m3.759s
sys 0m0.011s
</code></pre>
<p>对比一下最初的测试结果，这些“所谓的优化”没有什么卵用，优化前你估计也能猜测到这个结果，因为除了关闭边界检查，其他优化都<strong>没有处于循环执行的热路径之上</strong>。</p>
<blockquote>
<p>注：rand.Uint32() % 10000的确要比rand.Intn(10000)快，我自己的benchmark结果是快约1倍。</p>
</blockquote>
<p>那Go程序究竟慢在哪里呢？在“同等条件”下，我能想到的只能是<strong>Go编译器后端在代码优化方面优化做的还不够</strong>，相较于GCC、Java等老牌编译器还有明显差距。</p>
<p>比如说，原先的代码中在内层循环中频繁访问a&#91;i&#93;，导致数组访问的读写操作较多（从内存加载a&#91;i&#93;，更新值后写回）。GCC和Java编译器在后端很可能做了这样的优化：将数组元素累积到一个临时变量中，并在外层循环结束后写回数组，这样做可以<strong>减少内层循环中的内存读写操作，充分利用CPU缓存和寄存器，加速数据处理</strong>。</p>
<blockquote>
<p>注：数组从内存或缓存读，而一个临时变量很大可能是从寄存器读，那读取速度相差还是很大的。</p>
</blockquote>
<p>如果我们手工在Go中实施这一优化，看看能达到什么效果呢？我们改一下最初版本的Go代码(code.go)，新代码如下：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_local_var.go 

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        temp := a[i]
        for j := int32(0); j &lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration
            temp += j % u // Simple sum
        }
        temp += r // Add a random value to each element in array
        a[i] = temp
    }
    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>编译并运行测试：</p>
<pre><code>$go build -o code_local_var code_local_var.go
$time ./code_local_var 10
459169

real    0m3.017s
user    0m3.017s
sys 0m0.007s
</code></pre>
<p>我们看到，测试结果直接就比Java略好一些了。显然Go编译器没有做这种优化，从code.go的汇编也大致可以看出来：</p>
<p><img src="https://tonybai.com/wp-content/uploads/why-go-sucks-4.png" alt="" /><br />
<center>使用<a href="https://github.com/loov/lensm">lensm</a>生成的汇编与go源码对应关系</center></p>
<p>而Java显然做了这类优化，我们在原Java代码版本上按上述优化逻辑修改了一下：</p>
<pre><code>// why-go-sucks/billion-loops/java/code_local_var.java

package jvm;

import java.util.Random;

public class code {

    public static void main(String[] args) {
        var u = Integer.parseInt(args[0]); // 获取命令行输入的整数
        var r = new Random().nextInt(10000); // 生成随机数 0 &lt;= r &lt; 10000
        var a = new int[10000]; // 定义长度为10000的数组a

        for (var i = 0; i &lt; 10000; i++) { // 10k外层循环迭代
            var temp = a[i]; // 使用临时变量存储 a[i] 的值
            for (var j = 0; j &lt; 100000; j++) { // 100k内层循环迭代，每次外层循环迭代
                temp += j % u; // 更新临时变量的值
            }
            a[i] = temp + r; // 将临时变量的值加上 r 并写回数组
        }
        System.out.println(a[r]); // 输出 a[r] 的值
    }
}
</code></pre>
<p>但从运行这个“优化”后的程序的结果来看，其对java代码的提升幅度几乎可以忽略不计：</p>
<pre><code>$time java -cp . jvm.code 10
450375

real    0m3.043s
user    0m3.028s
sys 0m0.027s
</code></pre>
<p>这也直接证明了即便采用的是原版java代码，java编译器也会生成带有抽取局部变量这种优化的可执行代码，java程序员无需手工进行此类优化。</p>
<p>像这种编译器优化，还有不少，比如大家比较熟悉的循环展开(Loop Unrolling)也可以提升Go程序的性能：</p>
<pre><code>// why-go-sucks/billion-loops/go/code_loop_unrolling.go

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
)

func main() {
    input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line
    if e != nil {
        panic(e)
    }
    u := int32(input)
    r := int32(rand.Intn(10000))        // Get a random number 0 &lt;= r &lt; 10k
    var a [10000]int32                  // Array of 10k elements initialized to 0
    for i := int32(0); i &lt; 10000; i++ { // 10k outer loop iterations
        var sum int32
        // Unroll inner loop in chunks of 4 for optimization
        for j := int32(0); j &lt; 100000; j += 4 {
            sum += j % u
            sum += (j + 1) % u
            sum += (j + 2) % u
            sum += (j + 3) % u
        }
        a[i] = sum + r // Add the accumulated sum and random value
    }

    fmt.Println(a[r]) // Print out a single element from the array
}
</code></pre>
<p>运行这个Go测试程序，性能如下：</p>
<pre><code>$go build -o code_loop_unrolling code_loop_unrolling.go
$time ./code_loop_unrolling 10
458908

real    0m2.937s
user    0m2.940s
sys 0m0.002s
</code></pre>
<p>循环展开可以增加指令级并行性，因为展开后的代码块中可以有更多的独立指令，比如示例中的计算j % u、(j+1) % u、(j+2) % u和(j+3) % u，这些计算操作是独立的，可以并行执行，打破了依赖链，从而更好地利用处理器的并行流水线。而原版Go代码中，每次迭代都会根据前一次迭代的结果更新a&#91;i&#93;，形成一个依赖链，这种顺序依赖性迫使处理器只能按顺序执行这些指令，导致流水线停顿。</p>
<p>不过其他语言也可以做同样的手工优化，比如我们对C代码做同样的优化(why-go-sucks/billion-loops/c/code_loop_unrolling.c)，c测试程序的性能可以提升至2.7s水平，这也证明了初版C程序即便在-O3的情况下编译器也没有自动为其做这个优化：</p>
<pre><code>$time ./code_loop_unrolling 10
459383

real    0m2.723s
user    0m2.722s
sys 0m0.001s
</code></pre>
<p>到这里我们就不再针对这个10亿次循环的性能问题做进一步展开了，从上面的探索得到的初步结论就是<strong>Go编译器优化做的还不到位所致</strong>，期待后续Go团队能在编译器优化方面投入更多精力，争取早日追上GCC/Clang、Java这些成熟的编译器优化水平。</p>
<p>下面我们再来看Go在百万任务场景下内存开销大的“问题”。</p>
<h2>2. 内存占用高，问题出在Goroutine实现原理</h2>
<p>我们先来看第二个问题的测试代码：</p>
<pre><code>package main

import (
    "fmt"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    numRoutines := 100000
    if len(os.Args) &gt; 1 {
        n, err := strconv.Atoi(os.Args[1])
        if err == nil {
            numRoutines = n
        }
    }

    var wg sync.WaitGroup
    for i := 0; i &lt; numRoutines; i++ {
        wg.Add(1)
        go func() {
            time.Sleep(10 * time.Second)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>这个代码其实就是根据传入的task数量启动等同数量的goroutine，然后每个goroutine模拟工作负载sleep 10s，这等效于百万长连接的场景，只有连接，但没有收发消息。</p>
<p>相对于上一个问题，这个问题更好解释一些。</p>
<p>Go使用的groutine是一种有栈协程，文章中使用的是每个task一个goroutine的模型，且维护百万任务一段时间，这会真实创建百万个goroutine（G数据结构），并为其分配栈空间(2k起步)，这样你可以算一算，不考虑其他结构的占用，仅每个goroutine的栈空间所需的内存都是极其可观的：</p>
<pre><code>mem = 1000000 * 2000 Bytes = 2000000000 Bytes = 2G Bytes
</code></pre>
<p>所以启动100w goroutine，保底就2GB内存出去了，这与原作者测试的结果十分契合(原文是2.5GB多)。并且，内存还会随着goroutine数量增长而线性增加。</p>
<p>那么如何能减少内存使用呢？如果采用每个task一个goroutine的模型，这个内存占用很难省去，除非将来Go团队对goroutine实现做大修。</p>
<p>如果task是网络通信相关的，可以使用类似gnet这样的直接基于epoll建构的框架，其主要的节省在于不会启动那么多goroutine，而是通过一个goroutine池来处理数据，每个池中的goroutine负责一批网络连接或网络请求。</p>
<p>在一些Gopher的印象中，Goroutine一旦分配就不回收，这会使他们会误认为一旦分配了100w goroutine，这2.5G内存空间将始终被占用，真实情况是这样么？我们用一个示例程序验证一下就好了：</p>
<pre><code>// why-go-sucks/million-tasks/million-tasks.go

package main

import (
    "fmt"
    "log"
    "os"
    "os/signal"
    "runtime"
    "sync"
    "syscall"
    "time"
)

// 打印当前内存使用情况和相关信息
func printMemoryUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&amp;m)

    // 获取当前 goroutine 数量
    numGoroutines := runtime.NumGoroutine()

    // 获取当前线程数量
    numThreads := runtime.NumCPU() // Go runtime 不直接提供线程数量，但可以通过 NumCPU 获取逻辑处理器数量

    fmt.Printf("======&gt;\n")
    fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
    fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
    fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
    fmt.Printf("\tNumGC = %v", m.NumGC)
    fmt.Printf("\tNumGoroutines = %v", numGoroutines)
    fmt.Printf("\tNumThreads = %v\n", numThreads)
    fmt.Printf("&lt;======\n\n")
}

// 将字节转换为 MB
func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

func main() {
    const signal1Goroutines = 900000
    const signal2Goroutines = 90000
    const signal3Goroutines = 10000

    // 用于接收退出信号
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    // 控制 goroutine 的退出
    signal1Chan := make(chan struct{})
    signal2Chan := make(chan struct{})
    signal3Chan := make(chan struct{})

    var wg sync.WaitGroup
    ticker := time.NewTicker(5 * time.Second)
    go func() {
        for range ticker.C {
            printMemoryUsage()
        }
    }()

    // 等待退出信号
    go func() {
        count := 0
        for {
            &lt;-sigChan
            count++
            if count == 1 {
                log.Println("收到第一类goroutine退出信号")
                close(signal1Chan) // 关闭 signal1Chan，通知第一类 goroutine 退出
                continue
            }
            if count == 2 {
                log.Println("收到第二类goroutine退出信号")
                close(signal2Chan) // 关闭 signal2Chan，通知第二类 goroutine 退出
                continue
            }
            log.Println("收到第三类goroutine退出信号")
            close(signal3Chan) // 关闭 signal3Chan，通知第三类 goroutine 退出
            return
        }
    }()

    // 启动第一类 goroutine（在收到 signal1 时退出）
    log.Println("开始启动第一类goroutine...")
    for i := 0; i &lt; signal1Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal1Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第一类goroutine(900000) ok")

    time.Sleep(time.Second * 5)

    // 启动第二类 goroutine（在收到 signal2 时退出）
    log.Println("开始启动第二类goroutine...")
    for i := 0; i &lt; signal2Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal2Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第二类goroutine(90000) ok")

    time.Sleep(time.Second * 5)

    // 启动第三类goroutine（随程序退出而退出）
    log.Println("开始启动第三类goroutine...")
    for i := 0; i &lt; signal3Goroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 模拟工作
            for {
                select {
                case &lt;-signal3Chan:
                    return
                default:
                    time.Sleep(10 * time.Second) // 模拟一些工作
                }
            }
        }(i)
    }
    log.Println("启动第三类goroutine(90000) ok")

    // 等待所有 goroutine 完成
    wg.Wait()
    fmt.Println("所有 goroutine 已退出，程序结束")
}
</code></pre>
<p>这个程序我就不详细解释了。大致分三类goroutine，第一类90w个，在我发送第一个ctrl+c信号后退出，第二类9w个，在我发送第二个ctrl+c信号后退出，最后一类1w个，随着程序退出而退出。</p>
<p>在我的执行环境下编译和执行一下这个程序，并结合runtime输出以及使用top -p pid的方式查看其内存占用：</p>
<pre><code>$go build million-tasks.go
$./million-tasks 

2024/12/01 22:07:03 开始启动第一类goroutine...
2024/12/01 22:07:05 启动第一类goroutine(900000) ok
======&gt;
Alloc = 511 MiB TotalAlloc = 602 MiB    Sys = 2311 MiB  NumGC = 9   NumGoroutines = 900004  NumThreads = 8
&lt;======

2024/12/01 22:07:10 开始启动第二类goroutine...
2024/12/01 22:07:11 启动第二类goroutine(90000) ok
======&gt;
Alloc = 577 MiB TotalAlloc = 668 MiB    Sys = 2553 MiB  NumGC = 9   NumGoroutines = 990004  NumThreads = 8
&lt;======

2024/12/01 22:07:16 开始启动第三类goroutine...
2024/12/01 22:07:16 启动第三类goroutine(90000) ok
======&gt;
Alloc = 597 MiB TotalAlloc = 688 MiB    Sys = 2593 MiB  NumGC = 9   NumGoroutines = 1000004 NumThreads = 8
&lt;======

======&gt;
Alloc = 600 MiB TotalAlloc = 690 MiB    Sys = 2597 MiB  NumGC = 9   NumGoroutines = 1000004 NumThreads = 8
&lt;======
... ...

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 1000004 NumThreads = 8
&lt;======
</code></pre>
<p>100w goroutine全部创建ok后，我们查看一下top输出：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875556   2.5g    988 S  54.0  8.2   0:30.92 million-tasks
</code></pre>
<p>我们看到RES为2.5g，和我们预期的一致！</p>
<p>接下来，我们停掉第一批90w个goroutine，看RES是否会下降，何时会下降！</p>
<p>输入ctrl+c，停掉第一批90w goroutine：</p>
<pre><code>^C2024/12/01 22:10:15 收到第一类goroutine退出信号
======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 723198  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 723198  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 536 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 10  NumGoroutines = 100004  NumThreads = 8
&lt;======
... ...
</code></pre>
<p>但同时刻的top显示RES并没有变化：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812   2.5g    988 S   0.0  8.2   0:56.38 million-tasks
</code></pre>
<p>等待两个GC间隔的时间后(大约4分)，Goroutine的栈空间被释放：</p>
<pre><code>======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 12  NumGoroutines = 100004  NumThreads = 8
&lt;======
</code></pre>
<p>top显示RES从2.5g下降为大概700多MB（RES的单位是KB）：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 764136    992 S   0.0  2.4   1:01.87 million-tasks
</code></pre>
<p>接下来，我们再停掉第二批9w goroutine：</p>
<pre><code>^C2024/12/01 22:16:21 收到第二类goroutine退出信号
======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 100004  NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 465 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 13  NumGoroutines = 10004   NumThreads = 8
&lt;======

</code></pre>
<p>此时，top值也没立即改变：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 764136    992 S   0.0  2.4   1:05.99 million-tasks
</code></pre>
<p>大约等待一个GC间隔(2分钟)后，top中RES下降：</p>
<pre><code>======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10004   NumThreads = 8
&lt;======
</code></pre>
<p>RES变为不到700M：</p>
<pre><code>  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5800 root      20   0 3875812 699156    992 S   0.0  2.2   1:06.75 million-tasks
</code></pre>
<p>第三次按下ctrl+c，程序退出：</p>
<pre><code>^C2024/12/01 22:18:46 收到第三类goroutine退出信号
======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10003   NumThreads = 8
&lt;======

======&gt;
Alloc = 458 MiB TotalAlloc = 695 MiB    Sys = 2606 MiB  NumGC = 14  NumGoroutines = 10003   NumThreads = 8
&lt;======

所有 goroutine 已退出，程序结束
</code></pre>
<p>我们看到Go是会回收goroutine占用的内存空间的，并且归还给OS，只是这种归还比较lazy。尤其是，第二次停止goroutine前，go程序剩下10w goroutine，按理论来讲需占用大约200MB的空间，实际上却是700多MB；第二次停止goroutine后，goroutine数量降为1w，理论占用应该在20MB，但实际却是600多MB，我们看到go运行时这种lazy归还OS内存的行为可能也是“故意为之”，是为了避免反复从OS申请和归还内存。</p>
<h2>3. 小结</h2>
<p>本文主要探讨了Go语言在十亿次循环和百万任务的测试中的表现令人意外地逊色于Java和C语言的原因。我认为Go在循环执行中的慢速表现，主要是其编译器优化不足，影响了执行效率。 而在内存开销方面，Go的Goroutine实现是使得内存使用量大幅增加的“罪魁祸首”，这是由于每个Goroutine初始都会分配固定大小的栈空间。</p>
<p>通过本文的探讨，我的主要目的是希望大家不要以讹传讹，而是要搞清楚背后的真正原因，并正视Go在某些方面的不足，以及其当前在某些应用上下文中的局限性。 同时，也希望Go开发团队在编译器优化方面进行更多投入，以提升Go在高性能计算领域的竞争力。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/why-go-sucks">这里</a>下载。</p>
<h2>4. 参考资料</h2>
<ul>
<li><a href="https://benjdd.com/languages/">Billion nested loop iterations</a> &#8211; https://benjdd.com/languages/</li>
<li><a href="https://hez2010.github.io/async-runtimes-benchmarks-2024/">How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?</a> &#8211; https://hez2010.github.io/async-runtimes-benchmarks-2024/</li>
</ul>
<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/12/02/why-go-sucks/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<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>TB一周萃选[第7期]</title>
		<link>https://tonybai.com/2018/01/28/7th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/</link>
		<comments>https://tonybai.com/2018/01/28/7th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/#comments</comments>
		<pubDate>Sun, 28 Jan 2018 01:31:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Angular]]></category>
		<category><![CDATA[aws-lambda]]></category>
		<category><![CDATA[carbon]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[escape-analysis]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.9]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[Iot]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[kidsprogramming]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[LOGO]]></category>
		<category><![CDATA[MIT]]></category>
		<category><![CDATA[nonblocking]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[ROS]]></category>
		<category><![CDATA[ROS2]]></category>
		<category><![CDATA[ROSCon]]></category>
		<category><![CDATA[ROSCon2017]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[scratch]]></category>
		<category><![CDATA[SeymourPapert]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Vus.js]]></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>
		<category><![CDATA[阻塞IO]]></category>
		<category><![CDATA[阿根廷]]></category>
		<category><![CDATA[非阻塞IO]]></category>
		<category><![CDATA[马斯切拉诺]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2541</guid>
		<description><![CDATA[本文是首发于个人微信公众号的文章“TB一周萃选[第7期]”的归档。 我看过小马哥(哈维尔·马斯切拉诺)踢球， 你看过小马哥踢球， 他看过小马哥踢球。 我们看过小马哥踢球， 你们看过小马哥踢球， 他们看过小马哥踢球！ &#8212; 改编自网络资料 都说三九天是一年中最冷的一段时间，但我们这里稍有偏差，就个人赶脚：四九、五九才是我们这里温度的最低点。这一周的感受用一句东北话来说就是嘎嘎冷！体感温度近零下30摄氏度：一开车门，好不容易凝聚在身体周遭的“热量”瞬间散失，似乎已经有10多年没有感觉到如此持续的寒冷了。 但巴萨新闻中的一则消息却让作为阿根廷和巴萨双重球迷的我感到了一丝温暖。北京时间本周五凌晨，在巴萨主场与西班牙人队的国王杯四分之一决赛前，梦三主力、巴萨后防中坚小马哥携着自己的家人在巴萨队友的列队欢迎下、在诺坎普主场球迷山呼海啸般的欢呼声中走入诺坎普，和大家做着最后的告别。对于一名职业球员来说，这已经算是在俱乐部层面能得到的最高荣誉了。 虽说梅球王是我的最爱，但小马哥也是我十分喜欢和尊敬的一名足球运动员，在他的身上你几乎能够看到一名职业运动员所有的“正能量”标签：高超的专业能力、职业、自律、低调、坚毅、领导力、热爱足球、热爱家庭、没有绯闻等。对于小马哥这样的功勋球员，以“不只是一家俱乐部(Mes que un club)”为使命的巴萨俱乐部也做出了最大的让步，为小马哥设定了较低的转会费，让他可以按照自己的意愿成功转会到中超的华夏幸福。 小马哥将自己职业生涯中最好的七年奉献给了巴萨，对巴萨的贡献可谓是居功至伟！看看小马哥为巴萨赢得的荣誉吧。 感谢小马哥，祝福小马哥在后续的职业生涯中一切顺利！在中国生活的快乐！ 一、一周文章精粹 1. Hello, 中国! 由于“众所周知”的原因，大陆地区的Gopher们在访问Go官方站点时十分困难。这一定程度上影响了Go在大陆地区的推广。但Go语言在大陆地区的发展势头让Go team看到了建立大陆地区mirror站的必要性。就在这一周，中国的Gopher们迎来了一个Go官方的好消息，那就是Go语言大陆地区官方网站上线了。网站的地址是https://golang.google.cn，这个网站目前就是Go官方站的mirror，很多深层的链接可能依然指向源站，不过迈出第一步总是好的。 文章链接：“Hello，中国!” 2. 尚未修复的逃逸分析缺陷(Escape-Analysis Flaws) William Kennedy是著名的Go语言培训师，也是《Go in action》这本书的作者之一，他在Ardan Labs网站上撰写了许多篇关于Go语言的学习资料。其中最新的一篇“Escape Analysic Flaws”探讨了当前Go compiler(截至到Go 1.9)中依然存在的逃逸分析的缺陷，包括： Indirect Assignment Indirect Call Slice and Map Assignments Interfaces Unknown Go实际编码过程中减少在heap上的内存分配是提升性能，减少cost的好方法，通过William的分析，我们也期望能做到尽量避免逃逸的情况，但有些时候做起来很难。因此，让Go compiler自身变得更聪明才是终极解决方法。 文章链接：“Escape-Analysis Flaws” 3. Github用户使用的编程语言排名 国外友人Ben Fredericksont通过对2011以来github的public event数据的分析，得出了关于github上编程语言的使用变化趋势，包括：top [...]]]></description>
			<content:encoded><![CDATA[<p>本文是首发于<a href="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&amp;size=102&amp;__biz=MzIyNzM0MDk0Mg==&amp;mid=2247483848&amp;idx=1&amp;sn=a3cd9182a2b2d3716623cc2c43d59f37&amp;send_time=">个人微信公众号</a>的文章<strong>“TB一周萃选[第7期]”</strong>的归档。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/farewell-mascherano.jpg" alt="img{512x368}" /></p>
<blockquote>
<p>我看过小马哥(哈维尔·马斯切拉诺)踢球，<br />
  你看过小马哥踢球，<br />
  他看过小马哥踢球。<br />
  我们看过小马哥踢球，<br />
  你们看过小马哥踢球，<br />
  他们看过小马哥踢球！</p>
<p>&#8212; 改编自网络资料</p>
</blockquote>
<p>都说三九天是一年中最冷的一段时间，但我们这里稍有偏差，就个人赶脚：四九、五九才是我们这里温度的最低点。这一周的感受用一句东北话来说就是<strong>嘎嘎冷</strong>！体感温度近零下30摄氏度：一开车门，好不容易凝聚在身体周遭的“热量”瞬间散失，似乎已经有10多年没有感觉到如此持续的寒冷了。</p>
<p>但巴萨新闻中的一则消息却让作为阿根廷和巴萨双重球迷的我感到了一丝温暖。北京时间本周五凌晨，在<a href="https://www.fcbarcelona.com/">巴萨</a>主场与西班牙人队的国王杯四分之一决赛前，梦三主力、巴萨后防中坚小马哥携着自己的家人在巴萨队友的列队欢迎下、在诺坎普主场球迷山呼海啸般的欢呼声中走入诺坎普，和大家做着最后的告别。对于一名职业球员来说，这已经算是在俱乐部层面能得到的最高荣誉了。</p>
<p>虽说<a href="http://tonybai.com/tag/Messi">梅球王</a>是我的最爱，但小马哥也是我十分喜欢和尊敬的一名足球运动员，在他的身上你几乎能够看到一名职业运动员所有的“正能量”标签：高超的专业能力、职业、自律、低调、坚毅、领导力、热爱足球、热爱家庭、没有绯闻等。对于小马哥这样的功勋球员，以“不只是一家俱乐部(Mes que un club)”为使命的巴萨俱乐部也做出了最大的让步，为小马哥设定了较低的转会费，让他可以按照自己的意愿成功转会到中超的华夏幸福。</p>
<p>小马哥将自己职业生涯中最好的七年奉献给了巴萨，对巴萨的贡献可谓是居功至伟！看看小马哥为巴萨赢得的荣誉吧。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/macherano-awards-in-barca.jpg" alt="img{512x368}" /></p>
<p>感谢小马哥，祝福小马哥在后续的职业生涯中一切顺利！在中国生活的快乐！</p>
<h2>一、一周文章精粹</h2>
<h3>1. Hello, 中国!</h3>
<p>由于“众所周知”的原因，大陆地区的Gopher们在访问Go官方站点时十分困难。这一定程度上影响了Go在大陆地区的推广。但Go语言在大陆地区的发展势头让Go team看到了建立大陆地区mirror站的必要性。就在这一周，中国的Gopher们迎来了一个Go官方的好消息，那就是Go语言大陆地区官方网站上线了。网站的地址是https://golang.google.cn，这个网站目前就是Go官方站的mirror，很多深层的链接可能依然指向源站，不过迈出第一步总是好的。</p>
<p>文章链接：<a href="https://blog.golang.org/hello-china">“Hello，中国!”</a></p>
<h3>2. 尚未修复的逃逸分析缺陷(Escape-Analysis Flaws)</h3>
<p><a href="https://github.com/goinggo">William Kennedy</a>是著名的Go语言培训师，也是<a href="https://book.douban.com/subject/25858023/">《Go in action》</a>这本书的作者之一，他在<a href="https://www.ardanlabs.com/">Ardan Labs网站</a>上撰写了许多篇关于Go语言的学习资料。其中最新的一篇“Escape Analysic Flaws”探讨了当前Go compiler(截至到<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9</a>)中依然存在的<a href="https://en.wikipedia.org/wiki/Escape_analysis">逃逸分析</a>的缺陷，包括：</p>
<ul>
<li>Indirect Assignment</li>
<li>Indirect Call</li>
<li>Slice and Map Assignments</li>
<li>Interfaces</li>
<li>Unknown</li>
</ul>
<p>Go实际编码过程中减少在heap上的内存分配是提升性能，减少cost的好方法，通过William的分析，我们也期望能做到尽量避免逃逸的情况，但有些时候做起来很难。因此，让Go compiler自身变得更聪明才是终极解决方法。</p>
<p>文章链接：<a href="https://www.ardanlabs.com/blog/2018/01/escape-analysis-flaws.html">“Escape-Analysis Flaws”</a></p>
<h3>3. Github用户使用的编程语言排名</h3>
<p>国外友人Ben Fredericksont通过对2011以来github的public event数据的分析，得出了关于github上编程语言的使用变化趋势，包括：top ten活跃语言、主流语言的活跃程度变化趋势、2018值得学习的几个热门新语言、几门趋势下降很快的语言、科学计算语言的变化趋势、函数式语言的变化趋势等。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/languages-to-learn-in-2018.png" alt="img{512x368}" /><br />
图：2018值得学习的几个热门新语言</p>
<p>文章链接：<a href="http://www.benfrederickson.com/ranking-programming-languages-by-github-users/">“Ranking Programming Languages by GitHub Users”</a></p>
<h3>4. Nonblocking I/O指南</h3>
<p>Go语言的默认的网络I/O编程模型是阻塞I/O，这可以大幅降低应用开发者在处理网络I/O时的心智负担。但这也仅限于“用户层面”，研究过<a href="http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">Go runtime调度</a>的gopher都知道，在runtime内部，关于网络I/O的调度实际上是Nonblocking的。imgix的工程师<a href="http://twitter.com/copyconstruct">Cindy Sridharan</a>曾全面细致总结了对Nonblocking I/O的技术要点的理解，这里推荐给大家。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/non-blocking-io-guide.jpg" alt="img{512x368}" /></p>
<p>文章链接：<a href="https://medium.com/@copyconstruct/nonblocking-i-o-99948ad7c957">“Nonblocking I/O”</a></p>
<h3>5. 预测：2018年的最佳Linux发行版</h3>
<p><a href="http://tonybai.com/tag/Kernel">Linux内核</a>已经成为这个星球上使用最为广泛的操作系统内核了，无论是云服务器，还是桌面机，从移动终端到Iot设备，现代人身边10米范围内，一般总能找出一台运行着Linux内核的设备。而对于用户而言，看到的更多是基于Linux内核的各种发行版，比如：Ubuntu、CentOS等。年初JACK WALLEN在linux.com博客上撰文预测了2018年各个领域的最佳Linux发行版，包括从sysadmin、桌面版、server版、便携版、iot版等多个方面。这些预测基于distrowatch.com上各个发行版的人气排名。</p>
<p>文章链接：<a href="https://www.linux.com/blog/learn/intro-to-linux/2018/1/best-linux-distributions-2018">“best linux distributions for 2018”</a></p>
<h3>6. 如何使用Go语言创建基于AWS Lambda的serverless应用</h3>
<p><a href="https://aws.amazon.com/cn/blogs/compute/announcing-go-support-for-aws-lambda/">AWS Lambda宣布支持Go</a>不久，各路关于如何使用Go在AWS Lambda创建serverless应用的资料便接踵踏来。这里推荐的就是其中的一篇。对于想使用Go在AWS Lambda上“尝鲜”的Gopher们，这是个不错的入门文章。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/aws-lambda-and-go.png" alt="img{512x368}" /></p>
<p>文章链接：<a href="https://read.acloud.guru/serverless-golang-api-with-aws-lambda-34e442385a6a">“Serverless Golang API with AWS Lambda”</a></p>
<h3>7. JavaScript框架终极指南</h3>
<p>JavaScript这门语言虽然“颜值”不那么高，但这并不妨碍它抱上浏览器这一“大腿”，并还进军了服务端市场。在这一过程中，JavaScript领域诞生了诸多Framework，最出名的莫过于三巨头：<a href="https://angular.io/">Angular</a>、<a href="https://reactjs.org/">React</a>和<a href="https://github.com/vuejs">Vue.js</a>这三个框架了。除此之外，还有太多我甚至没有听过名字的框架。这里推荐的“JavaScript框架终极指南”一文就是对JavaScript目前的主流框架的状态、优劣势进行详细总结说明的一篇文章，希望能帮助你挑选出最适合你的Js框架。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/react-angular-vue-jan-2018.png" alt="img{512x368}" /></p>
<p>文章链接：<a href="https://javascriptreport.com/the-ultimate-guide-to-javascript-frameworks">“The Ultimate Guide to JavaScript Frameworks”</a></p>
<h2>二、一周资料分享</h2>
<h3>1. ROSCon 2017资料</h3>
<p><a href="http://tonybai.com/2017/08/01/hello-ros/">ROS</a>作为世界上应用最为广泛、最具影响力的开源机器人操作系统，它从2012年开始举办的ROSCon大会就备受关注，2017年ROSCon大会在加拿大温哥华举行。在人工智能、智能驾驶如此“热”的今天，ROS作为很多智能驾驶平台（比如百度的<a href="http://tonybai.com/2017/08/15/hello-apollo/">Apollo</a>、<a href="https://www.tier4.jp/">tierIV</a>的<a href="https://www.autoware.ai/">autoware</a>等）的底层支撑组件自然吸引了自全世界范围内的学者和工程师的眼球和参与。这次大会的topic是干货满满，由于是<a href="https://github.com/ros2/ros2">ROS2</a>发布正式版前的最后一次大会，因此涉及ROS2的topics十分多，算是为ROS2正式登场预热(注：ROS2在2017.12.10正式发布，代号：Ardent Apalone)。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/roscon-vancouver-2017.png" alt="img{512x368}" /></p>
<p>资料分享链接：<a href="https://roscon.ros.org/2017/">“ROSCon 2017资料”</a></p>
<h2>三、一周工具推荐</h2>
<h3>1. carbon：一款源码图片创建和分享的工具</h3>
<p>在技术文章写作中，我们会有大量的代码截图的需求，但限于客观原因，截图的质量和风格难于把控。Carbon这个工具就是来帮助解决这个问题的。Carbon是一个在线服务，支持通过将源码文件拖拽到生成框中自动生成代码图片。Carbon支持几乎所有主流语言，并可以自动识别，并且Carbon支持多种风格的代码高亮样式，比如：Monokai、Solarized等。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/carbon.png" alt="img{512x368}" /><br />
图：Carbon主页</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/carbon-demo.png" alt="img{512x368}" /><br />
图：Carbon生成的Go源码图片</p>
<p>推荐工具链接：<a href="https://carbon.now.sh">Carbon</a></p>
<h2>四、一周图书推荐</h2>
<h3>1.《Hello World! Second Edition &#8211; Computer Programming for Kids and Other Beginners》</h3>
<p>都说00后是互联网时代的原住民，那么伴着这轮AI热，我们是否可以大胆地说2020后或2025后是AI时代的原住民呢。这让我仿佛看到了“<a href="https://movie.douban.com/subject/11026735/">超能陆战队</a>”中男主小宏所使用的IT装备和掌握的编程技能。也许在未来10年后，编程就会像数学、语文一样成为在AI时代的基本技能。而这一切都要从娃娃抓起，从编程基础抓起。Sande父子合作编写的这本《Hello World》图文并茂地将孩子带入二进制的程序世界，孩子将在轻松惬意的氛围中学习基础的编程概念：如内存、循环、输入和输出、数据结构和图形用户界面等。对于如今智力水平普遍较高的孩子们来说，这些内容就像小游戏般容易掌握。书中使用的教学语言是<a href="http://tonybai.com/tag/python">Python</a>，别忘了目前的Python可是AI时代的top3语言，并是AI第一语言的强有力的竞争者。</p>
<p>很多人说：当前儿童编程的第一语言是MIT的<a href="https://scratch.mit.edu/">Scratch</a>，我不能否认这一点，Scratch就是为Kids们所创造的，它是MIT继<a href="https://en.wikipedia.org/wiki/Seymour_Papert">Seymour Papert教授</a>在创建<a href="https://turtleacademy.com/">LOGO语言</a>、探索儿童编程教育后的又一杰作。全图形化的编程教学让孩子们很是喜欢。但我个人觉得如果能结合一些真实代码，尤其是对于中高年级的学生来说，将是大有裨益的。</p>
<p>作为Gopher，我一直在想足够简洁的Go语言也是可以作为儿童编程教学语言的，希望能早日出现一门以Go语言为第一教学语言的儿童编程图书。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/7th-issue/book-hello-world-2nd-en.png" alt="img{512x368}" /></p>
<p>图书链接：<br />
   <a href="https://book.douban.com/subject/26005639/">《父与子的编程之旅 &#8211; 与小卡特一起学Python》</a><br />
   <a href="https://www.manning.com/books/hello-world-second-edition">《Hello World! Second Edition &#8211; Computer Programming for Kids and Other Beginners》</a></p>
<hr />
<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>微博：http://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/01/28/7th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>理解Docker的多阶段镜像构建</title>
		<link>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/</link>
		<comments>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/#comments</comments>
		<pubDate>Sat, 11 Nov 2017 11:26:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[baseimage]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[CGO_ENABLED]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[DockerHub]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httpserver]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libc.a]]></category>
		<category><![CDATA[libc.so.6]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[multi-stage-build]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[musl-libc]]></category>
		<category><![CDATA[strace]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[可移植性]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2459</guid>
		<description><![CDATA[Docker技术从2013年诞生到目前已经4年有余了。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的Docker 17.05版本起，Docker开始支持容器镜像的多阶段构建(multi-stage build)了。 什么是镜像多阶段构建呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。 一、同构的镜像构建 我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在Ubuntu 14.04上编译应用，并将应用打入基于ubuntu系列base image的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如centos中就可能会运行失败。 1、同构镜像构建举例 这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为Ubuntu 16.04 x86_64虚拟机、Go 1.8.3和docker 17.09.0-ce。 我们用一个Go语言中最常见的http server作为例子： // github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go package main import ( "net/http" "log" "fmt" ) func home(w http.ResponseWriter, req *http.Request) { w.Write([]byte("Welcome to this website!\n")) } func main() { http.HandleFunc("/", home) fmt.Println("Webserver start") fmt.Println(" -&#62; listen on port:1111") err := [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/tag/docker">Docker</a>技术从<a href="https://www.infoq.com/news/2013/03/Docker">2013年诞生</a>到目前已经4年有余了。对于已经接纳和使用<a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker技术</a>在日常开发工作中的开发者而言，构建<a href="https://docs.docker.com/get-started">Docker镜像</a>已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的<a href="https://github.com/moby/moby/releases/tag/v17.05.0-ce">Docker 17.05版本</a>起，Docker开始支持容器镜像的<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">多阶段构建(multi-stage build)</a>了。</p>
<p>什么是<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">镜像多阶段构建</a>呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。</p>
<h2>一、同构的镜像构建</h2>
<p>我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在<a href="https://hub.docker.com/_/ubuntu/">Ubuntu 14.04</a>上编译应用，并将应用打入基于<a href="https://hub.docker.com/_/ubuntu/">ubuntu系列base image</a>的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如<a href="https://hub.docker.com/_/centos/">centos</a>中就可能会运行失败。</p>
<h3>1、同构镜像构建举例</h3>
<p>这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为<strong>Ubuntu 16.04 x86_64虚拟机、<a href="http://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8.3</a>和docker 17.09.0-ce</strong>。</p>
<p>我们用一个Go语言中最常见的http server作为例子：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go
package main

import (
        "net/http"
        "log"
        "fmt"
)

func home(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Welcome to this website!\n"))
}

func main() {
        http.HandleFunc("/", home)
        fmt.Println("Webserver start")
        fmt.Println("  -&gt; listen on port:1111")
        err := http.ListenAndServe(":1111", nil)
        if err != nil {
                log.Fatal("ListenAndServe:", err)
        }
}

</code></pre>
<p>编译这个程序：</p>
<pre><code># go build -o myhttpserver httpserver.go
# ./myhttpserver
Webserver start
  -&gt; listen on port:1111
</code></pre>
<p>这个例子看起来很简单，也没几行代码，但背后Go net/http包在底层做了大量的事情，包括很多系统调用，能够反映出应用与操作系统的“耦合”，这在后续的讲解中会体现出来。接下来我们就来为这个程序构建一个docker image，并基于这个image来启动一个myhttpserver容器。我们选择ubuntu:14.04作为base image：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile
From ubuntu:14.04

COPY ./myhttpserver /root/myhttpserver
RUN chmod +x /root/myhttpserver

WORKDIR /root
ENTRYPOINT ["/root/myhttpserver"]

执行构建：

# docker build -t myrepo/myhttpserver:latest .
Sending build context to Docker daemon  5.894MB
Step 1/5 : FROM ubuntu:14.04
 ---&gt; dea1945146b9
Step 2/5 : COPY ./myhttpserver /root/myhttpserver
 ---&gt; 993e5129c081
Step 3/5 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in 104d84838ab2
 ---&gt; ebaeca006490
Removing intermediate container 104d84838ab2
Step 4/5 : WORKDIR /root
 ---&gt; 7afdc2356149
Removing intermediate container 450ccfb09ffd
Step 5/5 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in 3182766e2a68
 ---&gt; 77f315e15f14
Removing intermediate container 3182766e2a68
Successfully built 77f315e15f14
Successfully tagged myrepo/myhttpserver:latest

# docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttpserver   latest              77f315e15f14        18 seconds ago      200MB

# docker run myrepo/myhttpserver
Webserver start
  -&gt; listen on port:1111

</code></pre>
<p>以上是最基本的image build方法。</p>
<p>接下来，我们可能会遇到如下需求：<br />
* 搭建一个Go程序的构建环境有时候是很耗时的，尤其是对那些依赖很多第三方开源包的Go应用来说，下载包就需要很长时间。我们最好将这些易变的东西统统打包到一个用于Go程序构建的builder image中；<br />
* 我们看到上面我们构建出的myrepo/myhttpserver image的SIZE是200MB，这似乎有些过于“庞大”了。虽然每个主机node上的docker有cache image layer的能力，但我们还是希望能build出更加精简短小的image。</p>
<h3>2、借助golang builder image</h3>
<p>Docker Hub上提供了一个带有go dev环境的官方<a href="https://hub.docker.com/_/golang/">golang image repository</a>，我们可以直接使用这个golang builder image来辅助构建我们的应用image；对于一些对第三方包依赖较多的Go应用，我们也可以以这个golang image为base image定制我们自己的专用builder image。</p>
<p>我们基于golang:latest这个base image构建我们的golang-builder image，我们编写一个Dockerfile.build用于build golang-builder image:</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.build
FROM golang:latest

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go
</code></pre>
<p>在同目录下构建golang-builder image:</p>
<pre><code># docker build -t myrepo/golang-builder:latest -f Dockerfile.build .
Sending build context to Docker daemon  5.895MB
Step 1/4 : FROM golang:latest
 ---&gt; 1a34fad76b34
Step 2/4 : WORKDIR /go/src
 ---&gt; 2361824677d3
Removing intermediate container 01d8f4e9f0c4
Step 3/4 : COPY httpserver.go .
 ---&gt; 1ff14bb0bc56
Step 4/4 : RUN go build -o myhttpserver ./httpserver.go
 ---&gt; Running in 37a1b76b7b9e
 ---&gt; 2ac5347bb923
Removing intermediate container 37a1b76b7b9e
Successfully built 2ac5347bb923
Successfully tagged myrepo/golang-builder:latest

REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
myrepo/golang-builder   latest              2ac5347bb923        3 minutes ago       739MB
</code></pre>
<p>接下来，我们就基于golang-builder中已经build完毕的myhttpserver来构建我们最终的应用image：</p>
<pre><code># docker create --name appsource myrepo/golang-builder:latest
# docker cp appsource:/go/src/myhttpserver ./
# docker rm -f appsource
# docker rmi myrepo/golang-builder:latest
# docker build -t myrepo/myhttpserver:latest .
</code></pre>
<p>这段命令的逻辑就是从基于golang-builder image启动的容器appsource中将已经构建完毕的myhttpserver拷贝到主机当前目录中，然后删除临时的container appsource以及上面构建的那个golang-builder image；最后的步骤和第一个例子一样，基于本地目录中的已经构建完的myhttpserver构建出最终的image。为了方便，你也可以将这一系列命令放到一个Makefile中去。</p>
<h3>3、使用size更小的alpine image</h3>
<p>builder image并不能帮助我们为最终的应用image“减重”，myhttpserver image的Size依旧停留在200MB。要想“减重”，我们需要更小的base image，我们选择了<a href="https://hub.docker.com/_/alpine/">alpine</a>。<a href="https://news.ycombinator.com/item?id=10782897">Alpine image</a>的size不到4M，再加上应用的size，最终应用Image的Size估计可以缩减到20M以下。</p>
<p>结合builder image，我们只需将Dockerfile的base image改为alpine:latest：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.alpine

From alpine:latest

COPY ./myhttpserver /root/myhttpserver
RUN chmod +x /root/myhttpserver

WORKDIR /root
ENTRYPOINT ["/root/myhttpserver"]
</code></pre>
<p>构建alpine版应用image:</p>
<pre><code># docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine .
Sending build context to Docker daemon  6.151MB
Step 1/5 : FROM alpine:latest
 ---&gt; 053cde6e8953
Step 2/5 : COPY ./myhttpserver /root/myhttpserver
 ---&gt; ca0527a62d39
Step 3/5 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in 28d0a8a577b2
 ---&gt; a3833af97b5e
Removing intermediate container 28d0a8a577b2
Step 4/5 : WORKDIR /root
 ---&gt; 667345b78570
Removing intermediate container fa59883e9fdb
Step 5/5 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in adcb5b976ca3
 ---&gt; 582fa2aedc64
Removing intermediate container adcb5b976ca3
Successfully built 582fa2aedc64
Successfully tagged myrepo/myhttpserver-alpine:latest

# docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttpserver-alpine   latest              582fa2aedc64        4 minutes ago       16.3MB
</code></pre>
<p>16.3MB，Size的确降下来了！我们基于该image启动一个容器，看应用运行是否有什么问题：</p>
<pre><code># docker run myrepo/myhttpserver-alpine:latest
standard_init_linux.go:185: exec user process caused "no such file or directory"
</code></pre>
<p>容器启动失败了！为什么呢？因为alpine image并非ubuntu环境的同构image。我们在下面详细说明。</p>
<h2>二、异构的镜像构建</h2>
<p>我们的image builder: myrepo/golang-builder:latest是基于golang:latest这个image。<a href="https://github.com/docker-library/golang/">golang base image</a>有两个模板：Dockerfile-debain.template和Dockerfile-alpine.template。而golang:latest是基于debian模板的，与ubuntu兼容。构建出来的myhttpserver对<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/">动态共享链接库</a>的情况如下：</p>
<pre><code> # ldd myhttpserver
    linux-vdso.so.1 =&gt;  (0x00007ffd0c355000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffa8b36f000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffa8afa5000)
    /lib64/ld-linux-x86-64.so.2 (0x000055605ea5d000)
</code></pre>
<p><a href="https://www.debian.org/">debian</a>系的linux distribution使用了<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">glibc</a>。但alpine则不同，<a href="https://alpinelinux.org/">alpine</a>使用的是<a href="http://www.musl-libc.org/">musl libc</a>的实现，因此当我们运行上面的那个容器时，<a href="http://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/">加载器</a>因找不到myhttpserver依赖的libc.so.6而失败退出。</p>
<p>这种构建环境与运行环境不兼容的情况我这里称之为“异构的镜像构建”。那么如何解决这个问题呢？我们继续看：</p>
<h3>1、静态构建</h3>
<p>在主流编程语言中，<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">Go的移植性</a>已经是数一数二的了，尤其是<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>之后，Go将runtime中的C代码都用Go重写了，对libc的依赖已经降到最低了，但仍有一些feature提供了两个版本的实现：<a href="http://tonybai.com/tag/c">C实现</a>和Go实现。并且默认情况下，即在CGO_ENABLED=1的情况下，程序和预编译的标准库都采用了C的实现。关于这方面的详细论述请参见我之前写的<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">《也谈Go的可移植性》</a>一文，这里就不赘述了。于是采用了不同libc实现的debian系和alpine系自然存在不兼容的情况。要解决这个问题，我们首先考虑对Go程序进行静态构建，然后将静态构建后的Go应用放入alpine image中。</p>
<p>我们修改一下Dockerfile.build，在编译Go源文件时加上CGO_ENABLED=0：</p>
<pre><code>// github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.build

FROM golang:latest

WORKDIR /go/src
COPY httpserver.go .

RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go
</code></pre>
<p>构建这个builder image：</p>
<pre><code># docker build -t myrepo/golang-static-builder:latest -f Dockerfile.build .
Sending build context to Docker daemon  4.096kB
Step 1/4 : FROM golang:latest
 ---&gt; 1a34fad76b34
Step 2/4 : WORKDIR /go/src
 ---&gt; 593cd9692019
Removing intermediate container ee005d487ad5
Step 3/4 : COPY httpserver.go .
 ---&gt; a095eb69e716
Step 4/4 : RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go
 ---&gt; Running in d9f3b3a6c36c
 ---&gt; c06fe8dccbad
Removing intermediate container d9f3b3a6c36c
Successfully built c06fe8dccbad
Successfully tagged myrepo/golang-static-builder:latest

# docker images
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
myrepo/golang-static-builder   latest              c06fe8dccbad        31 seconds ago      739MB

</code></pre>
<p>接下来，我们再基于golang-static-builder中已经build完毕的静态连接的myhttpserver来构建我们最终的应用image：</p>
<pre><code># docker create --name appsource myrepo/golang-static-builder:latest
# docker cp appsource:/go/src/myhttpserver ./
# ldd myhttpserver
    not a dynamic executable
# docker rm -f appsource
# docker rmi myrepo/golang-static-builder:latest
# docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine .
</code></pre>
<p>运行新image:</p>
<pre><code># docker run myrepo/myhttpserver-alpine:latest
Webserver start
  -&gt; listen on port:1111
</code></pre>
<p>Note: 我们可以用strace来证明静态连接时Go只使用的是Go自己的runtime实现，而并未使用到libc.a中的代码：</p>
<pre><code># CGO_ENABLED=0 strace -f go build httpserver.go 2&gt;&amp;1 | grep open | grep -o '/.*\.a'  &gt; go-static-build-strace-file-open.txt
</code></pre>
<p>打开<a href="http://tonybai.com/wp-content/uploads/go-static-build-strace-file-open.txt">go-static-build-strace-file-open.txt</a>文件查看文件内容，你不会找到libc.a这个文件（在Ubuntu下，一般libc.a躺在/usr/lib/x86_64-linux-gnu/下面），这说明go build根本没有尝试去open libc.a文件并获取其中的符号定义。</p>
<h3>2、使用alpine golang builder</h3>
<p>我们的Go应用运行在alpine based的container中，我们可以使用alpine golang builder来构建我们的应用(无需静态链接)。前面提到过golang有alpine模板：</p>
<pre><code>REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
golang                       alpine              9e3f14138abd        7 days ago          269MB
</code></pre>
<p>alpine版golang builder的Dockerfile内容如下：</p>
<pre><code>//github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.alpine.build

FROM golang:alpine

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go

</code></pre>
<p>后续的操作与前面golang builder的操作并不二致：利用alpine golang builder构建我们的应用，并将其打入alpine image，这里就不赘述了。</p>
<h2>三、多阶段镜像构建：提升开发者体验</h2>
<p>在Docker 17.05以前，我们都是像上面那样构建镜像的。你会发现即便采用异构image builder模式，我们也要维护两个Dockerfile，并且还要在docker build命令之外执行一些诸如从容器内copy应用程序、清理build container和build image等的操作。Docker社区看到了这个问题，于是实现了<a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">多阶段镜像构建机制</a>（multi-stage）。</p>
<p>我们先来看一下针对上面例子，multi-stage build所使用Dockerfile：</p>
<pre><code>//github.com/bigwhite/experiments/multi_stage_image_build/multi_stages/Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o myhttpserver ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/myhttpserver .
RUN chmod +x /root/myhttpserver

ENTRYPOINT ["/root/myhttpserver"]
</code></pre>
<p>看完这个Dockerfile的内容，你的第一赶脚是不是把之前的两个Dockerfile合并在一块儿了，每个Dockerfile单独作为一个“阶段”！事实也是这样，但这个Docker也多了一些新的语法形式，用于建立各个“阶段”之间的联系。针对这样一个Dockerfile，我们应该知道以下几点：</p>
<ul>
<li>支持Multi-stage build的Dockerfile在以往的多个build阶段之间建立内在连接，让后一个阶段构建可以使用前一个阶段构建的产物，形成一条构建阶段的chain；</li>
<li>Multi-stages build的最终结果仅产生一个image，避免产生冗余的多个临时images或临时容器对象，这正是我们所需要的：我们只要结果。</li>
</ul>
<p>我们来使用multi-stage来build一下上述例子：</p>
<pre><code># docker build -t myrepo/myhttserver-multi-stage:latest .
Sending build context to Docker daemon  3.072kB
Step 1/9 : FROM golang:alpine as builder
 ---&gt; 9e3f14138abd
Step 2/9 : WORKDIR /go/src
 ---&gt; Using cache
 ---&gt; 7a99431d1be6
Step 3/9 : COPY httpserver.go .
 ---&gt; 43a196658e09
Step 4/9 : RUN go build -o myhttpserver ./httpserver.go
 ---&gt; Running in 9e7b46f68e88
 ---&gt; 90dc73912803
Removing intermediate container 9e7b46f68e88
Step 5/9 : FROM alpine:latest
 ---&gt; 053cde6e8953
Step 6/9 : WORKDIR /root/
 ---&gt; Using cache
 ---&gt; 30d95027ee6a
Step 7/9 : COPY --from=builder /go/src/myhttpserver .
 ---&gt; f1620b64c1ba
Step 8/9 : RUN chmod +x /root/myhttpserver
 ---&gt; Running in e62809993a22
 ---&gt; 6be6c28f5fd6
Removing intermediate container e62809993a22
Step 9/9 : ENTRYPOINT /root/myhttpserver
 ---&gt; Running in e4000d1dde3d
 ---&gt; 639cec396c96
Removing intermediate container e4000d1dde3d
Successfully built 639cec396c96
Successfully tagged myrepo/myhttserver-multi-stage:latest

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
myrepo/myhttserver-multi-stage   latest              639cec396c96        About an hour ago   16.3MB
</code></pre>
<p>我们来Run一下这个image：</p>
<pre><code># docker run myrepo/myhttserver-multi-stage:latest
Webserver start
  -&gt; listen on port:1111
</code></pre>
<h2>四、小结</h2>
<p>多阶段镜像构建可以让开发者通过一个Dockerfile，一次性地、更容易地构建出size较小的image，体验良好并且更容易接入CI/CD等自动化系统。不过当前多阶段构建仅是在Docker 17.05及之后的版本中才能得到支持。如果想学习和实践这方面功能，但又没有环境，可以使用<a href="https://labs.play-with-docker.com/">play-with-docker</a>提供的实验环境。</p>
<p><img src="http://tonybai.com/wp-content/uploads/labs-play-with-docker.png" alt="img{512x368}" /><br />
Play with Docker labs</p>
<blockquote>
<p>以上所有示例代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/multi_stage_image_build">这里</a>下载到。</p>
</blockquote>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>使用Kubeadm安装Kubernetes-Part2</title>
		<link>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/</link>
		<comments>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/#comments</comments>
		<pubDate>Fri, 30 Dec 2016 08:48:26 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[Apt-get]]></category>
		<category><![CDATA[BGP]]></category>
		<category><![CDATA[brctl]]></category>
		<category><![CDATA[calico]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[cephrbd]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[ipset]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kube-apiserver]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[minion]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[nodeport]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[route]]></category>
		<category><![CDATA[swarm]]></category>
		<category><![CDATA[swarmkit]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[troubleshooting]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[vip]]></category>
		<category><![CDATA[VXLAN]]></category>
		<category><![CDATA[weave]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[服务编排]]></category>
		<category><![CDATA[集群]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2169</guid>
		<description><![CDATA[此文为《使用Kubeadm安装Kubernetes》的第二部分。文章第一部分在这里可以看到。 五、weave network for pod 经过上面那么多次尝试，结果是令人扫兴的。Weave network似乎是最后一颗救命稻草了。有了前面的铺垫，这里就不详细列出各种命令的输出细节了。Weave network也有专门的官方文档用于指导如何与kubernetes集群集成，我们主要也是参考它。 1、安装weave network add-on 在kubeadm reset后，我们重新初始化了集群。接下来我们安装weave network add-on： # kubectl apply -f https://git.io/weave-kube daemonset "weave-net" created 前面无论是Flannel还是calico，在安装pod network add-on时至少都还是顺利的。不过在Weave network这次，我们遭遇“当头棒喝”:(: # kubectl get pod --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system dummy-2088944543-4kxtk 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system etcd-iz25beglnhtz 1/1 Running 0 [...]]]></description>
			<content:encoded><![CDATA[<p>此文为《使用Kubeadm安装Kubernetes》的第二部分。文章第一部分在<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">这里</a>可以看到。</p>
<h3>五、weave network for pod</h3>
<p>经过上面那么多次尝试，结果是令人扫兴的。Weave network似乎是最后一颗救命稻草了。有了前面的铺垫，这里就不详细列出各种命令的输出细节了。Weave network也有<a href="https://www.weave.works/docs/net/latest/kube-addon/">专门的官方文档</a>用于指导如何与kubernetes集群集成，我们主要也是参考它。</p>
<h4>1、安装weave network add-on</h4>
<p>在kubeadm reset后，我们重新初始化了集群。接下来我们安装weave network add-on：</p>
<pre><code># kubectl apply -f https://git.io/weave-kube
daemonset "weave-net" created
</code></pre>
<p>前面无论是Flannel还是calico，在安装pod network add-on时至少都还是顺利的。不过在Weave network这次，我们遭遇“当头棒喝”:(:</p>
<pre><code># kubectl get pod --all-namespaces -o wide
NAMESPACE     NAME                                   READY     STATUS              RESTARTS   AGE       IP             NODE
kube-system   dummy-2088944543-4kxtk                 1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                      1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-pzv8p        1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-09dcb              0/4       ContainerCreating   0          42m       &lt;none&gt;         iz25beglnhtz
kube-system   kube-proxy-z465f                       1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running             0          42m       10.47.217.91   iz25beglnhtz
kube-system   weave-net-3wk9h                        0/2       CrashLoopBackOff    16         17m       10.47.217.91   iz25beglnhtz
</code></pre>
<p>安装后，weave-net pod提示:CrashLoopBackOff。追踪其Container log，得到如下错误信息：</p>
<pre><code># docker logs cde899efa0af
time="2016-12-28T08:25:29Z" level=info msg="Starting Weaveworks NPC 1.8.2"
time="2016-12-28T08:25:29Z" level=info msg="Serving /metrics on :6781"
Wed Dec 28 08:25:29 2016 &lt;5&gt; ulogd.c:843 building new pluginstance stack: 'log1:NFLOG,base1:BASE,pcap1:PCAP'
time="2016-12-28T08:25:29Z" level=fatal msg="ipset [destroy] failed: ipset v6.29: Set cannot be destroyed: it is in use by a kernel component\n: exit status 1"
</code></pre>
<h4>2、解决ipset destroy错误</h4>
<p>从上述的错误日志来看，似乎某些内核组件占用了一些IP资源，没有释放。ipset(administration tool for IP sets)这个工具以前从来没有接触过。在node上利用apt-get install 一个ipset工具，手工执行以下命令：</p>
<pre><code># ipset destroy
ipset v6.29: Set cannot be destroyed: it is in use by a kernel component

</code></pre>
<p>这个错误输出与container中的error log一模一样。试着用ipset看看哪些ip资源没有释放，这一招让我们看到了蛛丝马迹：</p>
<p>在minion node上执行：</p>
<pre><code># ipset list
Name: felix-calico-hosts-4
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 1048576
Size in memory: 224
References: 1
Members:
123.56.200.187
59.110.67.15

Name: felix-all-ipam-pools
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 1048576
Size in memory: 448
References: 1
Members:
192.168.0.0/16

Name: felix-masq-ipam-pools
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 1048576
Size in memory: 448
References: 1
Members:
192.168.0.0/16
</code></pre>
<p>我们看到了calico字样。原来是calico的“残留势力”在作祟啊。进一步我们发现calico创建的一个network device依旧存在于两个Node上：</p>
<pre><code>47: tunl0@NONE: &lt;NOARP,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 192.168.91.0/32 scope global tunl0
       valid_lft forever preferred_lft forever
</code></pre>
<p>我们试图删除它，但最终都以失败告终：</p>
<pre><code># ip tunnel show
tunl0: ip/ip  remote any  local any  ttl inherit  nopmtudisc

 #ip tunnel del tunl0
delete tunnel "tunl0" failed: Operation not permitted

</code></pre>
<p>无奈只能把它down掉：</p>
<pre><code>#ip -f inet addr delete 192.168.91.0/32  dev tunl0

47: tunl0@NONE: &lt;NOARP,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0

# ifconfig tunl0 down

47: tunl0@NONE: &lt;NOARP&gt; mtu 1440 qdisc noqueue state DOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
</code></pre>
<p>但依旧无法删除它。我们通过ipset del命令将上面ipset占用的ip entry逐个删除掉（比如ipset del felix-calico-hosts-4  123.56.200.187）。但即便全部清空，ipset destroy依然失败。</p>
<p>无奈之下，决定重启一下两个Node试试。重启后，calico创建的这个tunnel居然消失了。</p>
<h4>3、再遇路由冲突错误</h4>
<p>重启ECS实例后，我们重新从头来创建cluster。不过在执行“kubectl apply -f https://git.io/weave-kube” 后我们发现weave-net pod依旧没有起来，这次的错误是“路有冲突”：</p>
<pre><code>#docker logs 80383071f721
Network 10.32.0.0/12 overlaps with existing route 10.0.0.0/8 on host.
</code></pre>
<p>查看当前路由表：</p>
<pre><code>netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         123.56.203.247  0.0.0.0         UG        0 0          0 eth1
10.0.0.0        10.47.223.247   255.0.0.0       UG        0 0          0 eth0
10.47.216.0     0.0.0.0         255.255.248.0   U         0 0          0 eth0
100.64.0.0      10.47.223.247   255.192.0.0     UG        0 0          0 eth0
123.56.200.0    0.0.0.0         255.255.252.0   U         0 0          0 eth1
172.16.0.0      10.47.223.247   255.240.0.0     UG        0 0          0 eth0
192.168.0.0     0.0.0.0         255.255.240.0   U         0 0          0 docker0
</code></pre>
<p>的确weave-net默认要使用的 10.32.0.0/12与 10.0.0.0/8 存在交集。对此，weave net官方是给出<a href=":https://www.weave.works/documentation/net-latest-using-weave/net-latest-configuring-weave/">解决方案</a>了的。</p>
<p>我们先将https://git.io/weave-kube对应的yaml文件下载到本地：weave-daemonset.yaml。修改该文件，为container增加IPALLOC_RANGE环境变量：</p>
<pre><code>containers:
        - name: weave
          env:
            - name: IPALLOC_RANGE
              value: 172.30.0.0/16

</code></pre>
<p>更新weave net pod：</p>
<pre><code># kubectl delete -f weave-daemonset.yaml
daemonset "weave-net" deleted

# kubectl apply -f weave-daemonset.yaml
daemonset "weave-net" created
</code></pre>
<p>不过依然存在路有冲突。原来路由表里已经存在了一条这样的路由：</p>
<pre><code>172.16.0.0      10.28.63.247    255.240.0.0     UG    0      0        0 eth0
</code></pre>
<p>这条路由应该没有什么用，也许是之前折腾时被某个network addon加进去的。于是用route命令将其删除：</p>
<pre><code># route del -net 172.16.0.0 netmask 255.240.0.0 gw 10.28.63.247
</code></pre>
<p>再次更新weave net pod并查看cluster status：</p>
<pre><code># kubectl delete -f weave-daemonset.yaml
daemonset "weave-net" deleted

# kubectl apply -f weave-daemonset.yaml
daemonset "weave-net" created

# kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                                   READY     STATUS    RESTARTS   AGE       IP             NODE
kube-system   dummy-2088944543-93f4c                 1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                      1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          20m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-wbc7h        1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-206tg              4/4       Running   0          21m       172.30.0.2     iz25beglnhtz
kube-system   kube-proxy-n2xmf                       1/1       Running   0          21m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running   0          20m       10.47.217.91   iz25beglnhtz
kube-system   weave-net-h38k5                        2/2       Running   0          18s       10.47.217.91   iz25beglnhtz

</code></pre>
<p>这回weave-net pod running了。taint master node并且minion node join后cluster依旧是ok的：</p>
<pre><code># kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                                   READY     STATUS    RESTARTS   AGE       IP             NODE
kube-system   dummy-2088944543-93f4c                 1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                      1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          22m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-wbc7h        1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-206tg              4/4       Running   0          23m       172.30.0.2     iz25beglnhtz
kube-system   kube-proxy-377zh                       1/1       Running   0          8s        10.28.61.30    iz2ze39jeyizepdxhwqci6z
kube-system   kube-proxy-n2xmf                       1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running   0          22m       10.47.217.91   iz25beglnhtz
kube-system   weave-net-9tf1d                        2/2       Running   0          8s        10.28.61.30    iz2ze39jeyizepdxhwqci6z
kube-system   weave-net-h38k5                        2/2       Running   0          2m        10.47.217.91   iz25beglnhtz
</code></pre>
<h4>4、测试weave net跨节点pod连通性</h4>
<p>这回我们依旧启动my-nginx service，在任意一个节点curl localhost:30062，我们发现被调度到minion node上的my-nginx container也收到了request并成功回复response：</p>
<pre><code>172.30.0.1 - - [30/Dec/2016:03:14:47 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.47.0" "-"
</code></pre>
<p>Weave net初步测试ok！</p>
<h3>六、小结</h3>
<p>虽然过程坎坷，但最终在Weave net的帮助下，我们还是初步调通了一个使用kubeadm安装的kubernetes cluster。后来我发现，在K8s官方博客中有一篇名为《<a href="http://blog.kubernetes.io/2016/09/how-we-made-kubernetes-easy-to-install.html">Kubernetes: How we made Kubernetes insanely easy to install</a>》的文章，其使用的pod network add-on也是weave network。</p>
<p>这是一个试验环境。后续我们还是要进一步探究如何用上Flannel的。同时，Kubernetes 1.5带来的<a href="http://blog.kubernetes.io/2016/12/five-days-of-kubernetes-1.5.html">诸多新特性</a>，比如：Master HA等还需要进一步试验证明。</p>
<p>为了满足我们的production环境要求，之前实践的<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd">Ceph RBD为K8s提供存储卷</a>、<a href="http://tonybai.com/2016/11/16/how-to-pull-images-from-private-registry-on-kubernetes-cluster">k8s从private registry拉取image</a>、<a href="http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster">k8s集群的安全配置</a>等还要在新集群上进一步试验，直到满足我们的要求。</p>
<p style='text-align:left'>&copy; 2016 &#8211; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>使用Kubeadm安装Kubernetes</title>
		<link>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/</link>
		<comments>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/#comments</comments>
		<pubDate>Fri, 30 Dec 2016 04:23:25 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[Apt-get]]></category>
		<category><![CDATA[BGP]]></category>
		<category><![CDATA[brctl]]></category>
		<category><![CDATA[calico]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[cephrbd]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[ipset]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kube-apiserver]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[kubelet]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[minion]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[nodeport]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[route]]></category>
		<category><![CDATA[swarm]]></category>
		<category><![CDATA[swarmkit]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[troubleshooting]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[vip]]></category>
		<category><![CDATA[VXLAN]]></category>
		<category><![CDATA[weave]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[服务编排]]></category>
		<category><![CDATA[集群]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2105</guid>
		<description><![CDATA[在《当Docker遇到systemd》一文中，我提到过这两天儿一直在做的一个task：使用kubeadm在Ubuntu 16.04上安装部署Kubernetes的最新发布版本-k8s 1.5.1。 年中，Docker宣布在Docker engine中集成swarmkit工具包，这一announcement在轻量级容器界引发轩然大波。毕竟开发者是懒惰的^0^，有了docker swarmkit，驱动developer去安装其他容器编排工具的动力在哪里呢？即便docker engine还不是当年那个被人们高频使用的IE浏览器。作为针对Docker公司这一市场行为的回应，容器集群管理和服务编排领先者Kubernetes在三个月后发布了Kubernetes1.4.0版本。在这个版本中K8s新增了kubeadm工具。kubeadm的使用方式有点像集成在docker engine中的swarm kit工具，旨在改善开发者在安装、调试和使用k8s时的体验，降低安装和使用门槛。理论上通过两个命令：init和join即可搭建出一套完整的Kubernetes cluster。 不过，和初入docker引擎的swarmkit一样，kubeadm目前也在active development中，也不是那么stable，因此即便在当前最新的k8s 1.5.1版本中，它仍然处于Alpha状态，官方不建议在Production环境下使用。每次执行kubeadm init时，它都会打印如下提醒日志： [kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. 不过由于之前部署的k8s 1.3.7集群运行良好，这给了我们在k8s这条路上继续走下去并走好的信心。但k8s在部署和管理方面的体验的确是太繁琐了，于是我们准备试验一下kubeadm是否能带给我们超出预期的体验。之前在aliyun ubuntu 14.04上安装kubernetes 1.3.7的经验和教训，让我略微有那么一丢丢底气，但实际安装过程依旧是一波三折。这既与kubeadm的unstable有关，同样也与cni、第三方网络add-ons的质量有关。无论哪一方出现问题都会让你的install过程异常坎坷曲折。 一、环境与约束 在kubeadm支持的Ubuntu 16.04+, CentOS 7 or HypriotOS v1.0.1+三种操作系统中，我们选择了Ubuntu 16.04。由于阿里云尚无官方16.04 Image可用，我们新开了两个Ubuntu 14.04ECS实例，并通过apt-get命令手工将其升级到Ubuntu 16.04.1，详细版本是：Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64)。 Ubuntu 16.04使用了systemd作为init system，在安装和配置Docker时，可以参考我的这篇《当Docker遇到system》。Docker版本我选择了目前可以得到的lastest stable release: [...]]]></description>
			<content:encoded><![CDATA[<p>在《<a href="http://tonybai.com/2016/12/27/when-docker-meets-systemd/">当Docker遇到systemd</a>》一文中，我提到过这两天儿一直在做的一个task：使用<a href="http://kubernetes.io/docs/admin/kubeadm/">kubeadm</a>在<a href="http://tonybai.com/tag/ubuntu">Ubuntu 16.04</a>上安装部署<a href="http://kubernetes.io/">Kubernetes</a>的最新发布版本-<a href="http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html">k8s 1.5.1</a>。</p>
<p>年中，Docker宣布在Docker engine中集成swarmkit工具包，这一announcement在轻量级容器界引发轩然大波。毕竟开发者是懒惰的^0^，有了docker swarmkit，驱动developer去安装其他容器编排工具的动力在哪里呢？即便docker engine还不是当年那个被人们高频使用的IE浏览器。作为针对Docker公司这一市场行为的回应，容器集群管理和服务编排领先者Kubernetes在三个月后发布了<a href="http://blog.kubernetes.io/2016/09/kubernetes-1.4-making-it-easy-to-run-on-kuberentes-anywhere.html">Kubernetes1.4.0版本</a>。在这个版本中K8s新增了kubeadm工具。kubeadm的使用方式有点像集成在<a href="http://tonybai.com/tag/docker">docker engine</a>中的<a href="http://tonybai.com/2016/10/11/some-problems-under-swarm-mode-in-docker-1-12">swarm kit工具</a>，旨在改善开发者在安装、调试和使用k8s时的体验，降低安装和使用门槛。理论上通过两个命令：init和join即可搭建出一套完整的Kubernetes cluster。</p>
<p>不过，和初入docker引擎的swarmkit一样，kubeadm目前也在active development中，也不是那么stable，因此即便在当前最新的k8s 1.5.1版本中，它仍然处于Alpha状态，官方不建议在Production环境下使用。每次执行kubeadm init时，它都会打印如下提醒日志：</p>
<pre><code>[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters.

</code></pre>
<p>不过由于<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu">之前部署的k8s 1.3.7集群</a>运行良好，这给了我们在k8s这条路上继续走下去并走好的信心。但k8s在部署和管理方面的体验的确是太繁琐了，于是我们准备试验一下kubeadm是否能带给我们超出预期的体验。之前在aliyun ubuntu 14.04上安装kubernetes 1.3.7的经验和教训，让我略微有那么一丢丢底气，但实际安装过程依旧是一波三折。这既与kubeadm的unstable有关，同样也与<a href="https://github.com/containernetworking/cni">cni</a>、第三方网络add-ons的质量有关。无论哪一方出现问题都会让你的install过程异常坎坷曲折。</p>
<h3>一、环境与约束</h3>
<p>在kubeadm支持的Ubuntu 16.04+, CentOS 7 or HypriotOS v1.0.1+三种操作系统中，我们选择了Ubuntu 16.04。由于阿里云尚无官方16.04 Image可用，我们新开了两个Ubuntu 14.04ECS实例，并通过apt-get命令手工将其升级到Ubuntu 16.04.1，详细版本是：Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64)。</p>
<p>Ubuntu 16.04使用了<a href="https://github.com/systemd/systemd">systemd</a>作为init system，在安装和配置Docker时，可以参考我的这篇《<a href="http://tonybai.com/2016/12/27/when-docker-meets-systemd/">当Docker遇到system</a>》。Docker版本我选择了目前可以得到的lastest stable release: 1.12.5。</p>
<pre><code># docker version
Client:
 Version:      1.12.5
 API version:  1.24
 Go version:   go1.6.4
 Git commit:   7392c3b
 Built:        Fri Dec 16 02:42:17 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.5
 API version:  1.24
 Go version:   go1.6.4
 Git commit:   7392c3b
 Built:        Fri Dec 16 02:42:17 2016
 OS/Arch:      linux/amd64

</code></pre>
<p>至于Kubernetes版本，前面已经提到过了，我们就使用最新发布的Kubernetes 1.5.1版本。1.5.1是<a href="http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html">1.5.0</a>的一个<a href="https://groups.google.com/forum/#!topic/kubernetes-announce/iclRj-6Nfsg">紧急fix版本</a>，主要”to address default flag values which in isolation were not problematic, but in concert could result in an insecure cluster”。官方建议skip 1.5.0，直接用1.5.1。</p>
<p>这里再重申一下：Kubernetes的安装、配置和调通是很难的，在阿里云上调通就更难了，有时还需要些运气。Kubernetes、Docker、cni以及各种网络Add-ons都在active development中，也许今天还好用的step、tip和trick，明天就out-dated，因此在借鉴本文的操作步骤时，请谨记这些^0^。</p>
<h3>二、安装包准备</h3>
<p>我们这次新开了两个ECS实例，一个作为master node，一个作为minion node。Kubeadm默认安装时，master node将不会参与Pod调度，不会承载work load，即不会有非核心组件的Pod在Master node上被创建出来。当然通过kubectl taint命令可以解除这一限制，不过这是后话了。</p>
<p>集群拓扑：</p>
<pre><code>master node：10.47.217.91，主机名：iZ25beglnhtZ
minion node：10.28.61.30，主机名：iZ2ze39jeyizepdxhwqci6Z
</code></pre>
<p>本次安装的主参考文档就是Kubernetes官方的那篇《<a href="http://kubernetes.io/docs/getting-started-guides/kubeadm">Installing Kubernetes on Linux with kubeadm</a>》。</p>
<p>本小节，我们将进行安装包准备，即将kubeadm以及此次安装所需要的k8s核心组件统统下载到上述两个Node上。注意：如果你有加速器，那么本节下面的安装过程将尤为顺利，反之，&#8230; <img src='https://tonybai.com/wp-includes/images/smilies/icon_sad.gif' alt=':(' class='wp-smiley' /> 。以下命令，在两个Node上均要执行。</p>
<h4>1、添加apt-key</h4>
<pre><code># curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
OK
</code></pre>
<h4>2、添加Kubernetes源并更新包信息</h4>
<p>添加Kubernetes源到sources.list.d目录下：</p>
<pre><code># cat &lt;&lt;EOF &gt; /etc/apt/sources.list.d/kubernetes.list
  deb http://apt.kubernetes.io/ kubernetes-xenial main
  EOF

# cat /etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
</code></pre>
<p>更新包信息：</p>
<pre><code># apt-get update
... ...
Hit:2 http://mirrors.aliyun.com/ubuntu xenial InRelease
Hit:3 https://apt.dockerproject.org/repo ubuntu-xenial InRelease
Get:4 http://mirrors.aliyun.com/ubuntu xenial-security InRelease [102 kB]
Get:1 https://packages.cloud.google.com/apt kubernetes-xenial InRelease [6,299 B]
Get:5 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 Packages [1,739 B]
Get:6 http://mirrors.aliyun.com/ubuntu xenial-updates InRelease [102 kB]
Get:7 http://mirrors.aliyun.com/ubuntu xenial-proposed InRelease [253 kB]
Get:8 http://mirrors.aliyun.com/ubuntu xenial-backports InRelease [102 kB]
Fetched 568 kB in 19s (28.4 kB/s)
Reading package lists... Done
</code></pre>
<h4>3、下载Kubernetes核心组件</h4>
<p>在此次安装中，我们通过apt-get就可以下载Kubernetes的核心组件，包括kubelet、kubeadm、kubectl和kubernetes-cni等。</p>
<pre><code># apt-get install -y kubelet kubeadm kubectl kubernetes-cni
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following package was automatically installed and is no longer required:
  libtimedate-perl
Use 'apt autoremove' to remove it.
The following additional packages will be installed:
  ebtables ethtool socat
The following NEW packages will be installed:
  ebtables ethtool kubeadm kubectl kubelet kubernetes-cni socat
0 upgraded, 7 newly installed, 0 to remove and 0 not upgraded.
Need to get 37.6 MB of archives.
After this operation, 261 MB of additional disk space will be used.
Get:2 http://mirrors.aliyun.com/ubuntu xenial/main amd64 ebtables amd64 2.0.10.4-3.4ubuntu1 [79.6 kB]
Get:6 http://mirrors.aliyun.com/ubuntu xenial/main amd64 ethtool amd64 1:4.5-1 [97.5 kB]
Get:7 http://mirrors.aliyun.com/ubuntu xenial/universe amd64 socat amd64 1.7.3.1-1 [321 kB]
Get:1 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubernetes-cni amd64 0.3.0.1-07a8a2-00 [6,877 kB]
Get:3 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubelet amd64 1.5.1-00 [15.1 MB]
Get:4 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubectl amd64 1.5.1-00 [7,954 kB]
Get:5 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubeadm amd64 1.6.0-alpha.0-2074-a092d8e0f95f52-00 [7,120 kB]
Fetched 37.6 MB in 36s (1,026 kB/s)
... ...
Unpacking kubeadm (1.6.0-alpha.0-2074-a092d8e0f95f52-00) ...
Processing triggers for systemd (229-4ubuntu13) ...
Processing triggers for ureadahead (0.100.0-19) ...
Processing triggers for man-db (2.7.5-1) ...
Setting up ebtables (2.0.10.4-3.4ubuntu1) ...
update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults
Setting up ethtool (1:4.5-1) ...
Setting up kubernetes-cni (0.3.0.1-07a8a2-00) ...
Setting up socat (1.7.3.1-1) ...
Setting up kubelet (1.5.1-00) ...
Setting up kubectl (1.5.1-00) ...
Setting up kubeadm (1.6.0-alpha.0-2074-a092d8e0f95f52-00) ...
Processing triggers for systemd (229-4ubuntu13) ...
Processing triggers for ureadahead (0.100.0-19) ...
... ...
</code></pre>
<p>下载后的kube组件并未自动运行起来。在 /lib/systemd/system下面我们能看到kubelet.service：</p>
<pre><code># ls /lib/systemd/system|grep kube
kubelet.service

//kubelet.service
[Unit]
Description=kubelet: The Kubernetes Node Agent
Documentation=http://kubernetes.io/docs/

[Service]
ExecStart=/usr/bin/kubelet
Restart=always
StartLimitInterval=0
RestartSec=10

[Install]
WantedBy=multi-user.target
</code></pre>
<p>kubelet的版本：</p>
<pre><code># kubelet --version
Kubernetes v1.5.1
</code></pre>
<p>k8s的核心组件都有了，接下来我们就要boostrap kubernetes cluster了。同时，问题也就随之而来了，而这些问题以及问题的解决才是本篇要说明的重点。</p>
<h3>三、初始化集群</h3>
<p>前面说过，理论上通过kubeadm使用init和join命令即可建立一个集群，这init就是在master节点对集群进行初始化。和k8s 1.4之前的部署方式不同的是，kubeadm安装的k8s核心组件都是以容器的形式运行于master node上的。因此在kubeadm init之前，最好给master node上的docker engine挂上加速器代理，因为kubeadm要从gcr.io/google_containers repository中pull许多核心组件的images，大约有如下一些：</p>
<pre><code>gcr.io/google_containers/kube-controller-manager-amd64   v1.5.1                     cd5684031720        2 weeks ago         102.4 MB
gcr.io/google_containers/kube-apiserver-amd64            v1.5.1                     8c12509df629        2 weeks ago         124.1 MB
gcr.io/google_containers/kube-proxy-amd64                v1.5.1                     71d2b27b03f6        2 weeks ago         175.6 MB
gcr.io/google_containers/kube-scheduler-amd64            v1.5.1                     6506e7b74dac        2 weeks ago         53.97 MB
gcr.io/google_containers/etcd-amd64                      3.0.14-kubeadm             856e39ac7be3        5 weeks ago         174.9 MB
gcr.io/google_containers/kubedns-amd64                   1.9                        26cf1ed9b144        5 weeks ago         47 MB
gcr.io/google_containers/dnsmasq-metrics-amd64           1.0                        5271aabced07        7 weeks ago         14 MB
gcr.io/google_containers/kube-dnsmasq-amd64              1.4                        3ec65756a89b        3 months ago        5.13 MB
gcr.io/google_containers/kube-discovery-amd64            1.0                        c5e0c9a457fc        3 months ago        134.2 MB
gcr.io/google_containers/exechealthz-amd64               1.2                        93a43bfb39bf        3 months ago        8.375 MB
gcr.io/google_containers/pause-amd64                     3.0                        99e59f495ffa        7 months ago        746.9 kB
</code></pre>
<p>在Kubeadm的文档中，Pod Network的安装是作为一个单独的步骤的。kubeadm init并没有为你选择一个默认的Pod network进行安装。我们将首选<a href="github.com/coreos/flannel">Flannel</a> 作为我们的Pod network，这不仅是因为我们的上一个集群用的就是flannel，而且表现稳定。更是由于Flannel就是coreos为k8s打造的专属overlay network add-ons。甚至于flannel repository的readme.md都这样写着：“flannel is a network fabric for containers, designed for Kubernetes”。如果我们要使用Flannel，那么在执行init时，按照kubeadm文档要求，我们必须给init命令带上option：&#8211;pod-network-cidr=10.244.0.0/16。</p>
<h4>1、执行kubeadm init</h4>
<p>执行kubeadm init命令：</p>
<pre><code># kubeadm init --pod-network-cidr=10.244.0.0/16
[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters.
[preflight] Running pre-flight checks
[preflight] Starting the kubelet service
[init] Using Kubernetes version: v1.5.1
[tokens] Generated token: "2e7da9.7fc5668ff26430c7"
[certificates] Generated Certificate Authority key and certificate.
[certificates] Generated API Server key and certificate
[certificates] Generated Service Account signing keys
[certificates] Created keys and certificates in "/etc/kubernetes/pki"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/admin.conf"
[apiclient] Created API client, waiting for the control plane to become ready //如果没有挂加速器，可能会在这里hang住。
[apiclient] All control plane components are healthy after 54.789750 seconds
[apiclient] Waiting for at least one node to register and become ready
[apiclient] First node is ready after 1.003053 seconds
[apiclient] Creating a test deployment
[apiclient] Test deployment succeeded
[token-discovery] Created the kube-discovery deployment, waiting for it to become ready
[token-discovery] kube-discovery is ready after 62.503441 seconds
[addons] Created essential addon: kube-proxy
[addons] Created essential addon: kube-dns

Your Kubernetes master has initialized successfully!

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:

http://kubernetes.io/docs/admin/addons/

You can now join any number of machines by running the following on each node:

kubeadm join --token=2e7da9.7fc5668ff26430c7 123.56.200.187

</code></pre>
<p>init成功后的master node有啥变化？k8s的核心组件均正常启动：</p>
<pre><code># ps -ef|grep kube
root      2477  2461  1 16:36 ?        00:00:04 kube-proxy --kubeconfig=/run/kubeconfig
root     30860     1 12 16:33 ?        00:01:09 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local
root     30952 30933  0 16:33 ?        00:00:01 kube-scheduler --address=127.0.0.1 --leader-elect --master=127.0.0.1:8080
root     31128 31103  2 16:33 ?        00:00:11 kube-controller-manager --address=127.0.0.1 --leader-elect --master=127.0.0.1:8080 --cluster-name=kubernetes --root-ca-file=/etc/kubernetes/pki/ca.pem --service-account-private-key-file=/etc/kubernetes/pki/apiserver-key.pem --cluster-signing-cert-file=/etc/kubernetes/pki/ca.pem --cluster-signing-key-file=/etc/kubernetes/pki/ca-key.pem --insecure-experimental-approve-all-kubelet-csrs-for-group=system:kubelet-bootstrap --allocate-node-cidrs=true --cluster-cidr=10.244.0.0/16
root     31223 31207  2 16:34 ?        00:00:10 kube-apiserver --insecure-bind-address=127.0.0.1 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota --service-cluster-ip-range=10.96.0.0/12 --service-account-key-file=/etc/kubernetes/pki/apiserver-key.pem --client-ca-file=/etc/kubernetes/pki/ca.pem --tls-cert-file=/etc/kubernetes/pki/apiserver.pem --tls-private-key-file=/etc/kubernetes/pki/apiserver-key.pem --token-auth-file=/etc/kubernetes/pki/tokens.csv --secure-port=6443 --allow-privileged --advertise-address=123.56.200.187 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --anonymous-auth=false --etcd-servers=http://127.0.0.1:2379
root     31491 31475  0 16:35 ?        00:00:00 /usr/local/bin/kube-discovery
</code></pre>
<p>而且是多以container的形式启动：</p>
<pre><code># docker ps
CONTAINER ID        IMAGE                                                           COMMAND                  CREATED                  STATUS                  PORTS               NAMES
c16c442b7eca        gcr.io/google_containers/kube-proxy-amd64:v1.5.1                "kube-proxy --kubecon"   6 minutes ago            Up 6 minutes                                k8s_kube-proxy.36dab4e8_kube-proxy-sb4sm_kube-system_43fb1a2c-cb46-11e6-ad8f-00163e1001d7_2ba1648e
9f73998e01d7        gcr.io/google_containers/kube-discovery-amd64:1.0               "/usr/local/bin/kube-"   8 minutes ago            Up 8 minutes                                k8s_kube-discovery.7130cb0a_kube-discovery-1769846148-6z5pw_kube-system_1eb97044-cb46-11e6-ad8f-00163e1001d7_fd49c2e3
dd5412e5e15c        gcr.io/google_containers/kube-apiserver-amd64:v1.5.1            "kube-apiserver --ins"   9 minutes ago            Up 9 minutes                                k8s_kube-apiserver.1c5a91d9_kube-apiserver-iz25beglnhtz_kube-system_eea8df1717e9fea18d266103f9edfac3_8cae8485
60017f8819b2        gcr.io/google_containers/etcd-amd64:3.0.14-kubeadm              "etcd --listen-client"   9 minutes ago            Up 9 minutes                                k8s_etcd.c323986f_etcd-iz25beglnhtz_kube-system_3a26566bb004c61cd05382212e3f978f_06d517eb
03c2463aba9c        gcr.io/google_containers/kube-controller-manager-amd64:v1.5.1   "kube-controller-mana"   9 minutes ago            Up 9 minutes                                k8s_kube-controller-manager.d30350e1_kube-controller-manager-iz25beglnhtz_kube-system_9a40791dd1642ea35c8d95c9e610e6c1_3b05cb8a
fb9a724540a7        gcr.io/google_containers/kube-scheduler-amd64:v1.5.1            "kube-scheduler --add"   9 minutes ago            Up 9 minutes                                k8s_kube-scheduler.ef325714_kube-scheduler-iz25beglnhtz_kube-system_dc58861a0991f940b0834f8a110815cb_9b3ccda2
.... ...
</code></pre>
<p>不过这些核心组件并不是跑在pod network中的（没错，此时的pod network还没有创建），而是采用了host network。以kube-apiserver的pod信息为例：</p>
<pre><code>kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          1h        10.47.217.91   iz25beglnhtz
</code></pre>
<p>kube-apiserver的IP是host ip，从而推断容器使用的是host网络，这从其对应的pause容器的network属性就可以看出：</p>
<pre><code># docker ps |grep apiserver
a5a76bc59e38        gcr.io/google_containers/kube-apiserver-amd64:v1.5.1            "kube-apiserver --ins"   About an hour ago   Up About an hour                        k8s_kube-apiserver.2529402_kube-apiserver-iz25beglnhtz_kube-system_25d646be9a0092138dc6088fae6f1656_ec0079fc
ef4d3bf057a6        gcr.io/google_containers/pause-amd64:3.0                        "/pause"                 About an hour ago   Up About an hour                        k8s_POD.d8dbe16c_kube-apiserver-iz25beglnhtz_kube-system_25d646be9a0092138dc6088fae6f1656_bbfd8a31

</code></pre>
<p>inspect pause容器，可以看到pause container的NetworkMode的值：</p>
<pre><code>"NetworkMode": "host",
</code></pre>
<p>如果kubeadm init执行过程中途出现了什么问题，比如前期忘记挂加速器导致init hang住，你可能会ctrl+c退出init执行。重新配置后，再执行kubeadm init，这时你可能会遇到下面kubeadm的输出：</p>
<pre><code># kubeadm init --pod-network-cidr=10.244.0.0/16
[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters.
[preflight] Running pre-flight checks
[preflight] Some fatal errors occurred:
    Port 10250 is in use
    /etc/kubernetes/manifests is not empty
    /etc/kubernetes/pki is not empty
    /var/lib/kubelet is not empty
    /etc/kubernetes/admin.conf already exists
    /etc/kubernetes/kubelet.conf already exists
[preflight] If you know what you are doing, you can skip pre-flight checks with `--skip-preflight-checks`
</code></pre>
<p>kubeadm会自动检查当前环境是否有上次命令执行的“残留”。如果有，必须清理后再行执行init。我们可以通过”kubeadm reset”来清理环境，以备重来。</p>
<pre><code># kubeadm reset
[preflight] Running pre-flight checks
[reset] Draining node: "iz25beglnhtz"
[reset] Removing node: "iz25beglnhtz"
[reset] Stopping the kubelet service
[reset] Unmounting mounted directories in "/var/lib/kubelet"
[reset] Removing kubernetes-managed containers
[reset] Deleting contents of stateful directories: [/var/lib/kubelet /etc/cni/net.d /var/lib/etcd]
[reset] Deleting contents of config directories: [/etc/kubernetes/manifests /etc/kubernetes/pki]
[reset] Deleting files: [/etc/kubernetes/admin.conf /etc/kubernetes/kubelet.conf]

</code></pre>
<h4>2、安装flannel pod网络</h4>
<p>kubeadm init之后，如果你探索一下当前cluster的状态或者核心组件的日志，你会发现某些“异常”，比如：从kubelet的日志中我们可以看到一直刷屏的错误信息：</p>
<pre><code>Dec 26 16:36:48 iZ25beglnhtZ kubelet[30860]: E1226 16:36:48.365885   30860 docker_manager.go:2201] Failed to setup network for pod "kube-dns-2924299975-pddz5_kube-system(43fd7264-cb46-11e6-ad8f-00163e1001d7)" using network plugins "cni": cni config unintialized; Skipping pod
</code></pre>
<p>通过命令kubectl get pod &#8211;all-namespaces -o wide，你也会发现kube-dns pod处于ContainerCreating状态。</p>
<p>这些都不打紧，因为我们还没有为cluster安装Pod network呢。前面说过，我们要使用Flannel网络，因此我们需要执行如下安装命令：</p>
<pre><code>#kubectl apply -f  https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
configmap "kube-flannel-cfg" created
daemonset "kube-flannel-ds" created

</code></pre>
<p>稍等片刻，我们再来看master node上的cluster信息：</p>
<pre><code># ps -ef|grep kube|grep flannel
root      6517  6501  0 17:20 ?        00:00:00 /opt/bin/flanneld --ip-masq --kube-subnet-mgr
root      6573  6546  0 17:20 ?        00:00:00 /bin/sh -c set -e -x; cp -f /etc/kube-flannel/cni-conf.json /etc/cni/net.d/10-flannel.conf; while true; do sleep 3600; done

# kubectl get pods --all-namespaces
NAMESPACE     NAME                                   READY     STATUS    RESTARTS   AGE
kube-system   dummy-2088944543-s0c5g                 1/1       Running   0          50m
kube-system   etcd-iz25beglnhtz                      1/1       Running   0          50m
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          50m
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running   0          50m
kube-system   kube-discovery-1769846148-6z5pw        1/1       Running   0          50m
kube-system   kube-dns-2924299975-pddz5              4/4       Running   0          49m
kube-system   kube-flannel-ds-5ww9k                  2/2       Running   0          4m
kube-system   kube-proxy-sb4sm                       1/1       Running   0          49m
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running   0          49m
</code></pre>
<p>至少集群的核心组件已经全部run起来了。看起来似乎是成功了。</p>
<h4>3、minion node：join the cluster</h4>
<p>接下来，就该minion node加入cluster了。这里我们用到了kubeadm的第二个命令：kubeadm join。</p>
<p>在minion node上执行（注意：这里要保证master node的9898端口在防火墙是打开的）：</p>
<pre><code># kubeadm join --token=2e7da9.7fc5668ff26430c7 123.56.200.187
[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters.
[preflight] Running pre-flight checks
[tokens] Validating provided token
[discovery] Created cluster info discovery client, requesting info from "http://123.56.200.187:9898/cluster-info/v1/?token-id=2e7da9"
[discovery] Cluster info object received, verifying signature using given token
[discovery] Cluster info signature and contents are valid, will use API endpoints [https://123.56.200.187:6443]
[bootstrap] Trying to connect to endpoint https://123.56.200.187:6443
[bootstrap] Detected server version: v1.5.1
[bootstrap] Successfully established connection with endpoint "https://123.56.200.187:6443"
[csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request
[csr] Received signed certificate from the API server:
Issuer: CN=kubernetes | Subject: CN=system:node:iZ2ze39jeyizepdxhwqci6Z | CA: false
Not before: 2016-12-26 09:31:00 +0000 UTC Not After: 2017-12-26 09:31:00 +0000 UTC
[csr] Generating kubelet configuration
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf"

Node join complete:
* Certificate signing request sent to master and response
  received.
* Kubelet informed of new secure connection details.

Run 'kubectl get nodes' on the master to see this machine join.
</code></pre>
<p>也很顺利。我们在minion node上看到的k8s组件情况如下：</p>
<pre><code>d85cf36c18ed        gcr.io/google_containers/kube-proxy-amd64:v1.5.1      "kube-proxy --kubecon"   About an hour ago   Up About an hour                        k8s_kube-proxy.36dab4e8_kube-proxy-lsn0t_kube-system_b8eddf1c-cb4e-11e6-ad8f-00163e1001d7_5826f32b
a60e373b48b8        gcr.io/google_containers/pause-amd64:3.0              "/pause"                 About an hour ago   Up About an hour                        k8s_POD.d8dbe16c_kube-proxy-lsn0t_kube-system_b8eddf1c-cb4e-11e6-ad8f-00163e1001d7_46bfcf67
a665145eb2b5        quay.io/coreos/flannel-git:v0.6.1-28-g5dde68d-amd64   "/bin/sh -c 'set -e -"   About an hour ago   Up About an hour                        k8s_install-cni.17d8cf2_kube-flannel-ds-tr8zr_kube-system_06eca729-cb72-11e6-ad8f-00163e1001d7_01e12f61
5b46f2cb0ccf        gcr.io/google_containers/pause-amd64:3.0              "/pause"                 About an hour ago   Up About an hour                        k8s_POD.d8dbe16c_kube-flannel-ds-tr8zr_kube-system_06eca729-cb72-11e6-ad8f-00163e1001d7_ac880d20
</code></pre>
<p>我们在master node上查看当前cluster状态：</p>
<pre><code># kubectl get nodes
NAME                      STATUS         AGE
iz25beglnhtz              Ready,master   1h
iz2ze39jeyizepdxhwqci6z   Ready          21s
</code></pre>
<p>k8s cluster创建”成功”！真的成功了吗？“折腾”才刚刚开始:(！</p>
<h3>三、Flannel Pod Network问题</h3>
<p>Join成功所带来的“余温”还未散去，我就发现了Flannel pod network的问题，troubleshooting正式开始:(。</p>
<h4>1、minion node上的flannel时不时地报错</h4>
<p>刚join时还好好的，可过了没一会儿，我们就发现在kubectl get pod &#8211;all-namespaces中有错误出现：</p>
<pre><code>kube-system   kube-flannel-ds-tr8zr                  1/2       CrashLoopBackOff   189        16h
</code></pre>
<p>我们发现这是minion node上的flannel pod中的一个container出错导致的，跟踪到的具体错误如下：</p>
<pre><code># docker logs bc0058a15969
E1227 06:17:50.605110       1 main.go:127] Failed to create SubnetManager: error retrieving pod spec for 'kube-system/kube-flannel-ds-tr8zr': Get https://10.96.0.1:443/api/v1/namespaces/kube-system/pods/kube-flannel-ds-tr8zr: dial tcp 10.96.0.1:443: i/o timeout
</code></pre>
<p>10.96.0.1是pod network中apiserver service的cluster ip，而minion node上的flannel组件居然无法访问到这个cluster ip！这个问题的奇怪之处还在于，有些时候这个Pod在被调度restart N多次后或者被删除重启后，又突然变为running状态了，行为十分怪异。</p>
<p>在flannel github.com issues中，至少有两个open issue与此问题有密切关系：</p>
<p>https://github.com/coreos/flannel/issues/545</p>
<p>https://github.com/coreos/flannel/issues/535</p>
<p>这个问题暂无明确解。当minion node上的flannel pod自恢复为running状态时，我们又可以继续了。</p>
<h4>2、minion node上flannel pod启动失败的一个应对方法</h4>
<p>在下面issue中，很多developer讨论了minion node上flannel pod启动失败的一种可能原因以及临时应对方法：</p>
<p>https://github.com/kubernetes/kubernetes/issues/34101</p>
<p>这种说法大致就是minion node上的kube-proxy使用了错误的interface，通过下面方法可以fix这个问题。在minion node上执行：</p>
<pre><code>#  kubectl -n kube-system get ds -l 'component=kube-proxy' -o json | jq '.items[0].spec.template.spec.containers[0].command |= .+ ["--cluster-cidr=10.244.0.0/16"]' | kubectl apply -f - &amp;&amp; kubectl -n kube-system delete pods -l 'component=kube-proxy'
daemonset "kube-proxy" configured
pod "kube-proxy-lsn0t" deleted
pod "kube-proxy-sb4sm" deleted
</code></pre>
<p>执行后，flannel pod的状态：</p>
<pre><code>kube-system   kube-flannel-ds-qw291                  2/2       Running   8          17h
kube-system   kube-flannel-ds-x818z                  2/2       Running   17         1h

</code></pre>
<p>经过17次restart，minion node上的flannel pod 启动ok了。其对应的flannel container启动日志如下：</p>
<pre><code># docker logs 1f64bd9c0386
I1227 07:43:26.670620       1 main.go:132] Installing signal handlers
I1227 07:43:26.671006       1 manager.go:133] Determining IP address of default interface
I1227 07:43:26.670825       1 kube.go:233] starting kube subnet manager
I1227 07:43:26.671514       1 manager.go:163] Using 59.110.67.15 as external interface
I1227 07:43:26.671575       1 manager.go:164] Using 59.110.67.15 as external endpoint
I1227 07:43:26.746811       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
I1227 07:43:26.749785       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE
I1227 07:43:26.752343       1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE
I1227 07:43:26.755126       1 manager.go:246] Lease acquired: 10.244.1.0/24
I1227 07:43:26.755444       1 network.go:58] Watching for L3 misses
I1227 07:43:26.755475       1 network.go:66] Watching for new subnet leases
I1227 07:43:27.755830       1 network.go:153] Handling initial subnet events
I1227 07:43:27.755905       1 device.go:163] calling GetL2List() dev.link.Index: 10
I1227 07:43:27.756099       1 device.go:168] calling NeighAdd: 123.56.200.187, ca:68:7c:9b:cc:67
</code></pre>
<p>issue中说到，在kubeadm init时，显式地指定&#8211;advertise-address将会避免这个问题。不过目前不要在&#8211;advertise-address后面写上多个IP，虽然文档上说是支持的，但实际情况是，当你显式指定&#8211;advertise-address的值为两个或两个以上IP时，比如下面这样：</p>
<pre><code>#kubeadm init --api-advertise-addresses=10.47.217.91,123.56.200.187 --pod-network-cidr=10.244.0.0/16
</code></pre>
<p>master初始化成功后，当minion node执行join cluster命令时，会panic掉：</p>
<pre><code># kubeadm join --token=92e977.f1d4d090906fc06a 10.47.217.91
[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters.
... ...
[bootstrap] Successfully established connection with endpoint "https://10.47.217.91:6443"
[bootstrap] Successfully established connection with endpoint "https://123.56.200.187:6443"
E1228 10:14:05.405294   28378 runtime.go:64] Observed a panic: "close of closed channel" (close of closed channel)
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:70
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:63
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:49
/usr/local/go/src/runtime/asm_amd64.s:479
/usr/local/go/src/runtime/panic.go:458
/usr/local/go/src/runtime/chan.go:311
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:85
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:96
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:97
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:52
/go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:93
/usr/local/go/src/runtime/asm_amd64.s:2086
[csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request
panic: close of closed channel [recovered]
    panic: close of closed channel

goroutine 29 [running]:
panic(0x1342de0, 0xc4203eebf0)
    /usr/local/go/src/runtime/panic.go:500 +0x1a1
k8s.io/kubernetes/pkg/util/runtime.HandleCrash(0x0, 0x0, 0x0)
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:56 +0x126
panic(0x1342de0, 0xc4203eebf0)
    /usr/local/go/src/runtime/panic.go:458 +0x243
k8s.io/kubernetes/cmd/kubeadm/app/node.EstablishMasterConnection.func1.1()
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:85 +0x29d
k8s.io/kubernetes/pkg/util/wait.JitterUntil.func1(0xc420563ee0)
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:96 +0x5e
k8s.io/kubernetes/pkg/util/wait.JitterUntil(0xc420563ee0, 0x12a05f200, 0x0, 0xc420022e01, 0xc4202c2060)
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:97 +0xad
k8s.io/kubernetes/pkg/util/wait.Until(0xc420563ee0, 0x12a05f200, 0xc4202c2060)
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:52 +0x4d
k8s.io/kubernetes/cmd/kubeadm/app/node.EstablishMasterConnection.func1(0xc4203a82f0, 0xc420269b90, 0xc4202c2060, 0xc4202c20c0, 0xc4203d8d80, 0x401, 0x480, 0xc4201e75e0, 0x17, 0xc4201e7560, ...)
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:93 +0x100
created by k8s.io/kubernetes/cmd/kubeadm/app/node.EstablishMasterConnection
    /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:94 +0x3ed

</code></pre>
<p>关于join panic这个问题，在这个issue中有详细讨论：https://github.com/kubernetes/kubernetes/issues/36988</p>
<h4>3、open /run/flannel/subnet.env: no such file or directory</h4>
<p>前面说过，默认情况下，考虑安全原因，master node是不承担work load的，不参与pod调度。我们这里机器少，只能让master node也辛苦一下。通过下面这个命令可以让master node也参与pod调度：</p>
<pre><code># kubectl taint nodes --all dedicated-
node "iz25beglnhtz" tainted
</code></pre>
<p>接下来，我们create一个deployment，manifest描述文件如下：</p>
<pre><code>//run-my-nginx.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx:1.10.1
        ports:
        - containerPort: 80
</code></pre>
<p>create后，我们发现调度到master上的my-nginx pod启动是ok的，但minion node上的pod则一直失败，查看到的失败原因如下：</p>
<pre><code>Events:
  FirstSeen    LastSeen    Count    From                    SubObjectPath    Type        Reason        Message
  ---------    --------    -----    ----                    -------------    --------    ------        -------
  28s        28s        1    {default-scheduler }                    Normal        Scheduled    Successfully assigned my-nginx-2560993602-0440x to iz2ze39jeyizepdxhwqci6z
  27s        1s        26    {kubelet iz2ze39jeyizepdxhwqci6z}            Warning        FailedSync    Error syncing pod, skipping: failed to "SetupNetwork" for "my-nginx-2560993602-0440x_default" with SetupNetworkError: "Failed to setup network for pod \"my-nginx-2560993602-0440x_default(ba5ce554-cbf1-11e6-8c42-00163e1001d7)\" using network plugins \"cni\": open /run/flannel/subnet.env: no such file or directory; Skipping pod"
</code></pre>
<p>在minion node上的确没有找到/run/flannel/subnet.env该文件。但master node上有这个文件：</p>
<pre><code>// /run/flannel/subnet.env

FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
</code></pre>
<p>于是手动在minion node上创建一份/run/flannel/subnet.env，并复制master node同名文件的内容，保存。稍许片刻，minion node上的my-nginx pod从error变成running了。</p>
<h4>4、no IP addresses available in network: cbr0</h4>
<p>将之前的一个my-nginx deployment的replicas改为3，并创建基于该deployment中pods的my-nginx service：</p>
<pre><code>//my-nginx-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 80
    nodePort: 30062
    protocol: TCP
  selector:
    run: my-nginx
</code></pre>
<p>修改后，通过curl localhost:30062测试服务连通性。发现通过VIP负载均衡到master node上的my-nginx pod的request都成功得到了Response，但是负载均衡到minion node上pod的request，则阻塞在那里，直到timeout。查看pod信息才发现，原来新调度到minion node上的my-nginx pod并没有启动ok，错误原因如下：</p>
<pre><code>Events:
  FirstSeen    LastSeen    Count    From                    SubObjectPath    Type        Reason        Message
  ---------    --------    -----    ----                    -------------    --------    ------        -------
  2m        2m        1    {default-scheduler }                    Normal        Scheduled    Successfully assigned my-nginx-1948696469-ph11m to iz2ze39jeyizepdxhwqci6z
  2m        0s        177    {kubelet iz2ze39jeyizepdxhwqci6z}            Warning        FailedSync    Error syncing pod, skipping: failed to "SetupNetwork" for "my-nginx-1948696469-ph11m_default" with SetupNetworkError: "Failed to setup network for pod \"my-nginx-1948696469-ph11m_default(3700d74a-cc12-11e6-8c42-00163e1001d7)\" using network plugins \"cni\": no IP addresses available in network: cbr0; Skipping pod"

</code></pre>
<p>查看minion node上/var/lib/cni/networks/cbr0目录，发现该目录下有如下文件：</p>
<pre><code>10.244.1.10   10.244.1.12   10.244.1.14   10.244.1.16   10.244.1.18   10.244.1.2    10.244.1.219  10.244.1.239  10.244.1.3   10.244.1.5   10.244.1.7   10.244.1.9
10.244.1.100  10.244.1.120  10.244.1.140  10.244.1.160  10.244.1.180  10.244.1.20   10.244.1.22   10.244.1.24   10.244.1.30  10.244.1.50  10.244.1.70  10.244.1.90
10.244.1.101  10.244.1.121  10.244.1.141  10.244.1.161  10.244.1.187  10.244.1.200  10.244.1.220  10.244.1.240  10.244.1.31  10.244.1.51  10.244.1.71  10.244.1.91
10.244.1.102  10.244.1.122  10.244.1.142  10.244.1.162  10.244.1.182  10.244.1.201  10.244.1.221  10.244.1.241  10.244.1.32  10.244.1.52  10.244.1.72  10.244.1.92
10.244.1.103  10.244.1.123  10.244.1.143  10.244.1.163  10.244.1.183  10.244.1.202  10.244.1.222  10.244.1.242  10.244.1.33  10.244.1.53  10.244.1.73  10.244.1.93
10.244.1.104  10.244.1.124  10.244.1.144  10.244.1.164  10.244.1.184  10.244.1.203  10.244.1.223  10.244.1.243  10.244.1.34  10.244.1.54  10.244.1.74  10.244.1.94
10.244.1.105  10.244.1.125  10.244.1.145  10.244.1.165  10.244.1.185  10.244.1.204  10.244.1.224  10.244.1.244  10.244.1.35  10.244.1.55  10.244.1.75  10.244.1.95
10.244.1.106  10.244.1.126  10.244.1.146  10.244.1.166  10.244.1.186  10.244.1.205  10.244.1.225  10.244.1.245  10.244.1.36  10.244.1.56  10.244.1.76  10.244.1.96
10.244.1.107  10.244.1.127  10.244.1.147  10.244.1.167  10.244.1.187  10.244.1.206  10.244.1.226  10.244.1.246  10.244.1.37  10.244.1.57  10.244.1.77  10.244.1.97
10.244.1.108  10.244.1.128  10.244.1.148  10.244.1.168  10.244.1.188  10.244.1.207  10.244.1.227  10.244.1.247  10.244.1.38  10.244.1.58  10.244.1.78  10.244.1.98
10.244.1.109  10.244.1.129  10.244.1.149  10.244.1.169  10.244.1.189  10.244.1.208  10.244.1.228  10.244.1.248  10.244.1.39  10.244.1.59  10.244.1.79  10.244.1.99
10.244.1.11   10.244.1.13   10.244.1.15   10.244.1.17   10.244.1.19   10.244.1.209  10.244.1.229  10.244.1.249  10.244.1.4   10.244.1.6   10.244.1.8   last_reserved_ip
10.244.1.110  10.244.1.130  10.244.1.150  10.244.1.170  10.244.1.190  10.244.1.21   10.244.1.23   10.244.1.25   10.244.1.40  10.244.1.60  10.244.1.80
10.244.1.111  10.244.1.131  10.244.1.151  10.244.1.171  10.244.1.191  10.244.1.210  10.244.1.230  10.244.1.250  10.244.1.41  10.244.1.61  10.244.1.81
10.244.1.112  10.244.1.132  10.244.1.152  10.244.1.172  10.244.1.192  10.244.1.211  10.244.1.231  10.244.1.251  10.244.1.42  10.244.1.62  10.244.1.82
10.244.1.113  10.244.1.133  10.244.1.153  10.244.1.173  10.244.1.193  10.244.1.212  10.244.1.232  10.244.1.252  10.244.1.43  10.244.1.63  10.244.1.83
10.244.1.114  10.244.1.134  10.244.1.154  10.244.1.174  10.244.1.194  10.244.1.213  10.244.1.233  10.244.1.253  10.244.1.44  10.244.1.64  10.244.1.84
10.244.1.115  10.244.1.135  10.244.1.155  10.244.1.175  10.244.1.195  10.244.1.214  10.244.1.234  10.244.1.254  10.244.1.45  10.244.1.65  10.244.1.85
10.244.1.116  10.244.1.136  10.244.1.156  10.244.1.176  10.244.1.196  10.244.1.215  10.244.1.235  10.244.1.26   10.244.1.46  10.244.1.66  10.244.1.86
10.244.1.117  10.244.1.137  10.244.1.157  10.244.1.177  10.244.1.197  10.244.1.216  10.244.1.236  10.244.1.27   10.244.1.47  10.244.1.67  10.244.1.87
10.244.1.118  10.244.1.138  10.244.1.158  10.244.1.178  10.244.1.198  10.244.1.217  10.244.1.237  10.244.1.28   10.244.1.48  10.244.1.68  10.244.1.88
10.244.1.119  10.244.1.139  10.244.1.159  10.244.1.179  10.244.1.199  10.244.1.218  10.244.1.238  10.244.1.29   10.244.1.49  10.244.1.69  10.244.1.89

</code></pre>
<p>这已经将10.244.1.x段的所有ip占满，自然没有available的IP可供新pod使用了。至于为何占满，这个原因尚不明朗。下面两个open issue与这个问题相关：</p>
<p>https://github.com/containernetworking/cni/issues/306</p>
<p>https://github.com/kubernetes/kubernetes/issues/21656</p>
<p>进入到/var/lib/cni/networks/cbr0目录下，执行下面命令可以释放那些可能是kubelet leak的IP资源：</p>
<pre><code>for hash in $(tail -n +1 * | grep '^[A-Za-z0-9]*$' | cut -c 1-8); do if [ -z $(docker ps -a | grep $hash | awk '{print $1}') ]; then grep -irl $hash ./; fi; done | xargs rm
</code></pre>
<p>执行后，目录下的文件列表变成了：</p>
<pre><code>ls -l
total 32
drw-r--r-- 2 root root 12288 Dec 27 17:11 ./
drw-r--r-- 3 root root  4096 Dec 27 13:52 ../
-rw-r--r-- 1 root root    64 Dec 27 17:11 10.244.1.2
-rw-r--r-- 1 root root    64 Dec 27 17:11 10.244.1.3
-rw-r--r-- 1 root root    64 Dec 27 17:11 10.244.1.4
-rw-r--r-- 1 root root    10 Dec 27 17:11 last_reserved_ip

</code></pre>
<p>不过pod仍然处于失败状态，但这次失败的原因又发生了变化：</p>
<pre><code>Events:
  FirstSeen    LastSeen    Count    From                    SubObjectPath    Type        Reason        Message
  ---------    --------    -----    ----                    -------------    --------    ------        -------
  23s        23s        1    {default-scheduler }                    Normal        Scheduled    Successfully assigned my-nginx-1948696469-7p4nn to iz2ze39jeyizepdxhwqci6z
  22s        1s        22    {kubelet iz2ze39jeyizepdxhwqci6z}            Warning        FailedSync    Error syncing pod, skipping: failed to "SetupNetwork" for "my-nginx-1948696469-7p4nn_default" with SetupNetworkError: "Failed to setup network for pod \"my-nginx-1948696469-7p4nn_default(a40fe652-cc14-11e6-8c42-00163e1001d7)\" using network plugins \"cni\": \"cni0\" already has an IP address different from 10.244.1.1/24; Skipping pod"

</code></pre>
<p>而/var/lib/cni/networks/cbr0目录下的文件又开始迅速增加！问题陷入僵局。</p>
<h4>5、flannel vxlan不通，后端换udp，仍然不通</h4>
<p>折腾到这里，基本筋疲力尽了。于是在两个node上执行kubeadm reset，准备重新来过。</p>
<p>kubeadm reset后，之前flannel创建的bridge device cni0和网口设备flannel.1依然健在。为了保证环境彻底恢复到初始状态，我们可以通过下面命令删除这两个设备：</p>
<pre><code># ifconfig  cni0 down
# brctl delbr cni0
# ip link delete flannel.1
</code></pre>
<p>有了前面几个问题的“磨炼”后，重新init和join的k8s cluster显得格外顺利。这次minion node没有再出现什么异常。</p>
<pre><code>#  kubectl get nodes -o wide
NAME                      STATUS         AGE       EXTERNAL-IP
iz25beglnhtz              Ready,master   5m        &lt;none&gt;
iz2ze39jeyizepdxhwqci6z   Ready          51s       &lt;none&gt;

# kubectl get pod --all-namespaces
NAMESPACE     NAME                                   READY     STATUS    RESTARTS   AGE
default       my-nginx-1948696469-71h1l              1/1       Running   0          3m
default       my-nginx-1948696469-zwt5g              1/1       Running   0          3m
default       my-ubuntu-2560993602-ftdm6             1/1       Running   0          3m
kube-system   dummy-2088944543-lmlbh                 1/1       Running   0          5m
kube-system   etcd-iz25beglnhtz                      1/1       Running   0          6m
kube-system   kube-apiserver-iz25beglnhtz            1/1       Running   0          6m
kube-system   kube-controller-manager-iz25beglnhtz   1/1       Running   0          6m
kube-system   kube-discovery-1769846148-l5lfw        1/1       Running   0          5m
kube-system   kube-dns-2924299975-mdq5r              4/4       Running   0          5m
kube-system   kube-flannel-ds-9zwr1                  2/2       Running   0          5m
kube-system   kube-flannel-ds-p7xh2                  2/2       Running   0          1m
kube-system   kube-proxy-dwt5f                       1/1       Running   0          5m
kube-system   kube-proxy-vm6v2                       1/1       Running   0          1m
kube-system   kube-scheduler-iz25beglnhtz            1/1       Running   0          6m

</code></pre>
<p>接下来我们创建my-nginx deployment和service来测试flannel网络的连通性。通过curl my-nginx service的nodeport，发现可以reach master上的两个nginx pod，但是minion node上的pod依旧不通。</p>
<p>在master上看flannel docker的日志：</p>
<pre><code>I1228 02:52:22.097083       1 network.go:225] L3 miss: 10.244.1.2
I1228 02:52:22.097169       1 device.go:191] calling NeighSet: 10.244.1.2, 46:6c:7a:a6:06:60
I1228 02:52:22.097335       1 network.go:236] AddL3 succeeded
I1228 02:52:55.169952       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:00.801901       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:03.801923       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:04.801764       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:05.801848       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:06.888269       1 network.go:225] L3 miss: 10.244.1.2
I1228 02:53:06.888340       1 device.go:191] calling NeighSet: 10.244.1.2, 46:6c:7a:a6:06:60
I1228 02:53:06.888507       1 network.go:236] AddL3 succeeded
I1228 02:53:39.969791       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:45.153770       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:48.154822       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:49.153774       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:50.153734       1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2
I1228 02:53:52.154056       1 network.go:225] L3 miss: 10.244.1.2
I1228 02:53:52.154110       1 device.go:191] calling NeighSet: 10.244.1.2, 46:6c:7a:a6:06:60
I1228 02:53:52.154256       1 network.go:236] AddL3 succeeded
</code></pre>
<p>日志中有大量：“Ignoring not a miss”字样的日志，似乎vxlan网络有问题。这个问题与下面issue中描述颇为接近：</p>
<p>https://github.com/coreos/flannel/issues/427</p>
<p>Flannel默认采用vxlan作为backend，使用kernel vxlan默认的udp 8742端口。Flannel还支持udp的backend，使用udp 8285端口。于是试着更换一下flannel后端。更换flannel后端的步骤如下：</p>
<ul>
<li>将https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml文件下载到本地;</li>
<li>修改kube-flannel.yml文件内容：主要是针对net-conf.json属性，增加”Backend”字段属性：</li>
</ul>
<pre><code>---
kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "type": "flannel",
      "delegate": {
        "isDefaultGateway": true
      }
    }
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "udp",
        "Port": 8285
      }
    }
---
... ...
</code></pre>
<ul>
<li>卸载并重新安装pod网络</li>
</ul>
<pre><code># kubectl delete -f kube-flannel.yml
configmap "kube-flannel-cfg" deleted
daemonset "kube-flannel-ds" deleted

# kubectl apply -f kube-flannel.yml
configmap "kube-flannel-cfg" created
daemonset "kube-flannel-ds" created

# netstat -an|grep 8285
udp        0      0 123.56.200.187:8285     0.0.0.0:*
</code></pre>
<p>经过测试发现：udp端口是通的。在两个node上tcpdump -i flannel0 可以看到udp数据包的发送和接收。但是两个node间的pod network依旧不通。</p>
<h4>6、failed to register network: failed to acquire lease: node “iz25beglnhtz” not found</h4>
<p>正常情况下master node和minion node上的flannel pod的启动日志如下：</p>
<p>master node flannel的运行:</p>
<pre><code>I1227 04:56:16.577828       1 main.go:132] Installing signal handlers
I1227 04:56:16.578060       1 kube.go:233] starting kube subnet manager
I1227 04:56:16.578064       1 manager.go:133] Determining IP address of default interface
I1227 04:56:16.578576       1 manager.go:163] Using 123.56.200.187 as external interface
I1227 04:56:16.578616       1 manager.go:164] Using 123.56.200.187 as external endpoint
E1227 04:56:16.579079       1 network.go:106] failed to register network: failed to acquire lease: node "iz25beglnhtz" not found
I1227 04:56:17.583744       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
I1227 04:56:17.585367       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE
I1227 04:56:17.587765       1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE
I1227 04:56:17.589943       1 manager.go:246] Lease acquired: 10.244.0.0/24
I1227 04:56:17.590203       1 network.go:58] Watching for L3 misses
I1227 04:56:17.590255       1 network.go:66] Watching for new subnet leases
I1227 07:43:27.164103       1 network.go:153] Handling initial subnet events
I1227 07:43:27.164211       1 device.go:163] calling GetL2List() dev.link.Index: 5
I1227 07:43:27.164350       1 device.go:168] calling NeighAdd: 59.110.67.15, ca:50:97:1f:c2:ea

</code></pre>
<p>minion node上flannel的运行：</p>
<pre><code># docker logs 1f64bd9c0386
I1227 07:43:26.670620       1 main.go:132] Installing signal handlers
I1227 07:43:26.671006       1 manager.go:133] Determining IP address of default interface
I1227 07:43:26.670825       1 kube.go:233] starting kube subnet manager
I1227 07:43:26.671514       1 manager.go:163] Using 59.110.67.15 as external interface
I1227 07:43:26.671575       1 manager.go:164] Using 59.110.67.15 as external endpoint
I1227 07:43:26.746811       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
I1227 07:43:26.749785       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE
I1227 07:43:26.752343       1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE
I1227 07:43:26.755126       1 manager.go:246] Lease acquired: 10.244.1.0/24
I1227 07:43:26.755444       1 network.go:58] Watching for L3 misses
I1227 07:43:26.755475       1 network.go:66] Watching for new subnet leases
I1227 07:43:27.755830       1 network.go:153] Handling initial subnet events
I1227 07:43:27.755905       1 device.go:163] calling GetL2List() dev.link.Index: 10
I1227 07:43:27.756099       1 device.go:168] calling NeighAdd: 123.56.200.187, ca:68:7c:9b:cc:67
</code></pre>
<p>但在进行上面问题5的测试过程中，我们发现flannel container的启动日志中有如下错误：</p>
<p>master node:</p>
<pre><code># docker logs c2d1cee3df3d
I1228 06:53:52.502571       1 main.go:132] Installing signal handlers
I1228 06:53:52.502735       1 manager.go:133] Determining IP address of default interface
I1228 06:53:52.503031       1 manager.go:163] Using 123.56.200.187 as external interface
I1228 06:53:52.503054       1 manager.go:164] Using 123.56.200.187 as external endpoint
E1228 06:53:52.503869       1 network.go:106] failed to register network: failed to acquire lease: node "iz25beglnhtz" not found
I1228 06:53:52.503899       1 kube.go:233] starting kube subnet manager
I1228 06:53:53.522892       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
I1228 06:53:53.524325       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE
I1228 06:53:53.526622       1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE
I1228 06:53:53.528438       1 manager.go:246] Lease acquired: 10.244.0.0/24
I1228 06:53:53.528744       1 network.go:58] Watching for L3 misses
I1228 06:53:53.528777       1 network.go:66] Watching for new subnet leases
</code></pre>
<p>minion node:</p>
<pre><code># docker logs dcbfef45308b
I1228 05:28:05.012530       1 main.go:132] Installing signal handlers
I1228 05:28:05.012747       1 manager.go:133] Determining IP address of default interface
I1228 05:28:05.013011       1 manager.go:163] Using 59.110.67.15 as external interface
I1228 05:28:05.013031       1 manager.go:164] Using 59.110.67.15 as external endpoint
E1228 05:28:05.013204       1 network.go:106] failed to register network: failed to acquire lease: node "iz2ze39jeyizepdxhwqci6z" not found
I1228 05:28:05.013237       1 kube.go:233] starting kube subnet manager
I1228 05:28:06.041602       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
I1228 05:28:06.042863       1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE
I1228 05:28:06.044896       1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE
I1228 05:28:06.046497       1 manager.go:246] Lease acquired: 10.244.1.0/24
I1228 05:28:06.046780       1 network.go:98] Watching for new subnet leases
I1228 05:28:07.047052       1 network.go:191] Subnet added: 10.244.0.0/24

</code></pre>
<p>两个Node都有“注册网络”失败的错误：failed to register network: failed to acquire lease: node “xxxx” not found。很难断定是否是因为这两个错误导致的两个node间的网络不通。从整个测试过程来看，这个问题时有时无。在下面flannel issue中也有类似的问题讨论：</p>
<p>https://github.com/coreos/flannel/issues/435</p>
<p>Flannel pod network的诸多问题让我决定暂时放弃在kubeadm创建的kubernetes cluster中继续使用Flannel。</p>
<h3>四、Calico pod network</h3>
<p>Kubernetes支持的pod network add-ons中，除了Flannel，还有<a href="https://projectcalico.org/">calico</a>、<a href="https://www.weave.works/">Weave net</a>等。这里我们试试基于边界网关BGP协议实现的Calico pod network。Calico Project针对在kubeadm建立的K8s集群的Pod网络安装也有专门的<a href="http://docs.projectcalico.org/v2.0/getting-started/kubernetes/installation/hosted/kubeadm/">文档</a>。文档中描述的需求和约束我们均满足，比如：</p>
<p>master node带有kubeadm.alpha.kubernetes.io/role: master标签：</p>
<pre><code># kubectl get nodes -o wide --show-labels
NAME           STATUS         AGE       EXTERNAL-IP   LABELS
iz25beglnhtz   Ready,master   3m        &lt;none&gt;        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubeadm.alpha.kubernetes.io/role=master,kubernetes.io/hostname=iz25beglnhtz

</code></pre>
<p>在安装calico之前，我们还是要执行kubeadm reset重置环境，并将flannel创建的各种网络设备删除，可参考上面几个小节中的命令。</p>
<h4>1、初始化集群</h4>
<p>使用calico的kubeadm init无需再指定&#8211;pod-network-cidr=10.244.0.0/16 option：</p>
<pre><code># kubeadm init --api-advertise-addresses=10.47.217.91
[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters.
[preflight] Running pre-flight checks
[preflight] Starting the kubelet service
[init] Using Kubernetes version: v1.5.1
[tokens] Generated token: "531b3f.3bd900d61b78d6c9"
[certificates] Generated Certificate Authority key and certificate.
[certificates] Generated API Server key and certificate
[certificates] Generated Service Account signing keys
[certificates] Created keys and certificates in "/etc/kubernetes/pki"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/admin.conf"
[apiclient] Created API client, waiting for the control plane to become ready
[apiclient] All control plane components are healthy after 13.527323 seconds
[apiclient] Waiting for at least one node to register and become ready
[apiclient] First node is ready after 0.503814 seconds
[apiclient] Creating a test deployment
[apiclient] Test deployment succeeded
[token-discovery] Created the kube-discovery deployment, waiting for it to become ready
[token-discovery] kube-discovery is ready after 1.503644 seconds
[addons] Created essential addon: kube-proxy
[addons] Created essential addon: kube-dns

Your Kubernetes master has initialized successfully!

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:

http://kubernetes.io/docs/admin/addons/

You can now join any number of machines by running the following on each node:

kubeadm join --token=531b3f.3bd900d61b78d6c9 10.47.217.91

</code></pre>
<h4>2、创建calico network</h4>
<pre><code># kubectl apply -f http://docs.projectcalico.org/v2.0/getting-started/kubernetes/installation/hosted/kubeadm/calico.yaml
configmap "calico-config" created
daemonset "calico-etcd" created
service "calico-etcd" created
daemonset "calico-node" created
deployment "calico-policy-controller" created
job "configure-calico" created
</code></pre>
<p>实际创建过程需要一段时间，因为calico需要pull 一些images：</p>
<pre><code># docker images
REPOSITORY                                               TAG                        IMAGE ID            CREATED             SIZE
quay.io/calico/node                                      v1.0.0                     74bff066bc6a        7 days ago          256.4 MB
calico/ctl                                               v1.0.0                     069830246cf3        8 days ago          43.35 MB
calico/cni                                               v1.5.5                     ada87b3276f3        12 days ago         67.13 MB
gcr.io/google_containers/etcd                            2.2.1                      a6cd91debed1        14 months ago       28.19 MB
</code></pre>
<p>calico在master node本地创建了两个network device：</p>
<pre><code># ip a
... ...
47: tunl0@NONE: &lt;NOARP,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 192.168.91.0/32 scope global tunl0
       valid_lft forever preferred_lft forever
48: califa32a09679f@if4: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default
    link/ether 62:39:10:55:44:c8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
</code></pre>
<h4>3、minion node join</h4>
<p>执行下面命令，将minion node加入cluster：</p>
<pre><code># kubeadm join --token=531b3f.3bd900d61b78d6c9 10.47.217.91
</code></pre>
<p>calico在minion node上也创建了一个network device:</p>
<pre><code>57988: tunl0@NONE: &lt;NOARP,UP,LOWER_UP&gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 192.168.136.192/32 scope global tunl0
       valid_lft forever preferred_lft forever
</code></pre>
<p>join成功后，我们查看一下cluster status：</p>
<pre><code># kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                                       READY     STATUS    RESTARTS   AGE       IP             NODE
kube-system   calico-etcd-488qd                          1/1       Running   0          18m       10.47.217.91   iz25beglnhtz
kube-system   calico-node-jcb3c                          2/2       Running   0          18m       10.47.217.91   iz25beglnhtz
kube-system   calico-node-zthzp                          2/2       Running   0          4m        10.28.61.30    iz2ze39jeyizepdxhwqci6z
kube-system   calico-policy-controller-807063459-f21q4   1/1       Running   0          18m       10.47.217.91   iz25beglnhtz
kube-system   dummy-2088944543-rtsfk                     1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   etcd-iz25beglnhtz                          1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-apiserver-iz25beglnhtz                1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-controller-manager-iz25beglnhtz       1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-discovery-1769846148-51wdk            1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-dns-2924299975-fhf5f                  4/4       Running   0          23m       192.168.91.1   iz25beglnhtz
kube-system   kube-proxy-2s7qc                           1/1       Running   0          4m        10.28.61.30    iz2ze39jeyizepdxhwqci6z
kube-system   kube-proxy-h2qds                           1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
kube-system   kube-scheduler-iz25beglnhtz                1/1       Running   0          23m       10.47.217.91   iz25beglnhtz
</code></pre>
<p>所有组件都是ok的。似乎是好兆头！但跨node的pod network是否联通，还需进一步探究。</p>
<h4>4、探究跨node的pod network联通性</h4>
<p>我们依旧利用上面测试flannel网络的my-nginx-svc.yaml和run-my-nginx.yaml，创建my-nginx service和my-nginx deployment。注意：这之前要先在master node上执行一下”kubectl taint nodes &#8211;all dedicated-”，以让master node承载work load。</p>
<p>遗憾的是，结果和flannel很相似，分配到master node上http request得到了nginx的响应；minion node上的pod依旧无法联通。</p>
<p>这次我不想在calico这块过多耽搁，我要快速看看下一个候选者：weave net是否满足要求。</p>
<h3>由于wordpress莫名其妙的问题，导致这篇文章无法发布完整，因此将其拆分为两个部分，本文为第一部分，第二部分请移步<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/">这里</a>阅读。</h3>
<p style='text-align:left'>&copy; 2016 &#8211; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>使用Ceph RBD为Kubernetes集群提供存储卷</title>
		<link>https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/</link>
		<comments>https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/#comments</comments>
		<pubDate>Mon, 07 Nov 2016 09:43:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[ceph-deploy]]></category>
		<category><![CDATA[cephrbd]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[ext4]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MDS]]></category>
		<category><![CDATA[NFS]]></category>
		<category><![CDATA[NTP]]></category>
		<category><![CDATA[openshift]]></category>
		<category><![CDATA[OSD]]></category>
		<category><![CDATA[persistent-volume]]></category>
		<category><![CDATA[persistent-volume-claim]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[PV]]></category>
		<category><![CDATA[PVC]]></category>
		<category><![CDATA[rbd]]></category>
		<category><![CDATA[secret]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[volume]]></category>
		<category><![CDATA[块存储]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[对象存储]]></category>
		<category><![CDATA[数据卷]]></category>
		<category><![CDATA[文件系统]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2047</guid>
		<description><![CDATA[一旦走上使用Kubernetes的道路，你就会发现这条路并不好走，充满荆棘。即便你使用Kubernetes建立起的集群规模不大，也是需要“五脏俱全”的，否则你根本无法真正将kubernetes用起来，或者说一个半拉子Kubernetes集群很可能无法满足你要支撑的业务需求。在目前我正在从事的一个产品就是这样，光有K8s还不够，考虑到”有状态服务”的需求，我们还需要给Kubernetes配一个后端存储以支持Persistent Volume机制，使得Pod在k8s的不同节点间调度迁移时，具有持久化需求的数据不会被清除，且Pod中Container无论被调度到哪个节点，始终都能挂载到同一个Volume。 Kubernetes支持多种Volume类型，这里选择Ceph RBD（Rados Block Device）。选择Ceph大致有三个原因： Ceph经过多年开发，已经逐渐步入成熟； Ceph在Ubuntu 14.04.x上安装方便（仅通过apt-get即可），并且在未经任何调优（调优需要你对Ceph背后的原理十分熟悉）的情况下，性能可以基本满足我们需求； Ceph同时支持对象存储、块存储和文件系统接口，虽然这里我们可能仅需要块存储。 即便这样，Ceph与K8s的集成过程依旧少不了“趟坑”，接下来我们就详细道来。 一、环境和准备条件 我们依然使用两个阿里云ECS Node，操作系统以及内核版本为：Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64)。 Ceph采用当前Ubuntu 14.04源中最新的Ceph LTS版本：JEWEL10.2.3。 Kubernetes版本为上次安装时的1.3.7版本。 二、Ceph安装原理 Ceph分布式存储集群由若干组件组成，包括：Ceph Monitor、Ceph OSD和Ceph MDS，其中如果你仅使用对象存储和块存储时，MDS不是必须的（本次我们也不需要安装MDS），仅当你要用到Cephfs时，MDS才是需要安装的。 Ceph的安装模型与k8s有些类似，也是通过一个deploy node远程操作其他Node以create、prepare和activate各个Node上的Ceph组件，官方手册中给出的示意图如下： 映射到我们实际的环境中，我的安装设计是这样的： admin-node, deploy-node(ceph-deploy)：10.47.136.60 iZ25cn4xxnvZ mon.node1，(mds.node1): 10.47.136.60 iZ25cn4xxnvZ osd.0: 10.47.136.60 iZ25cn4xxnvZ osd.1: 10.46.181.146 iZ25mjza4msZ 实际上就是两个Aliyun ECS节点承担以上多种角色。不过像iZ25cn4xxnvZ这样的host name太反人类，长远考虑还是换成node1、node2这样的简单名字更好。通过编辑各个ECS上的/etc/hostname, /etc/hosts，我们将iZ25cn4xxnvZ换成node1，将iZ25mjza4msZ换成node2： 10.47.136.60 （node1)： # cat /etc/hostname node1 # cat [...]]]></description>
			<content:encoded><![CDATA[<p>一旦走上使用<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>的道路，你就会发现这条路并不好走，充满荆棘。即便你<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">使用Kubernetes建立起的集群</a>规模不大，也是需要“五脏俱全”的，否则你根本无法真正将kubernetes用起来，或者说一个半拉子Kubernetes集群很可能无法满足你要支撑的业务需求。在目前我正在从事的一个产品就是这样，光有K8s还不够，考虑到”有状态服务”的需求，我们还需要给Kubernetes配一个后端存储以支持<a href="http://kubernetes.io/docs/user-guide/persistent-volumes/">Persistent Volume</a>机制，使得Pod在k8s的不同节点间调度迁移时，具有持久化需求的数据不会被清除，且Pod中Container无论被调度到哪个节点，始终都能挂载到同一个Volume。</p>
<p>Kubernetes<a href="http://kubernetes.io/docs/user-guide/persistent-volumes/#types-of-persistent-volumes">支持多种Volume类型</a>，这里选择<a href="http://docs.ceph.com/docs/master/rbd/rbd/">Ceph RBD（Rados Block Device）</a>。选择<a href="http://ceph.com/">Ceph</a>大致有三个原因：</p>
<ul>
<li>Ceph经过多年开发，已经逐渐步入成熟；</li>
<li>Ceph在<a href="http://tonybai.com/tag/ubuntu">Ubuntu 14.04.x</a>上安装方便（仅通过apt-get即可），并且在未经任何调优（调优需要你对Ceph背后的原理十分熟悉）的情况下，性能可以基本满足我们需求；</li>
<li>Ceph同时支持对象存储、块存储和文件系统接口，虽然这里我们可能仅需要块存储。</li>
</ul>
<p>即便这样，Ceph与K8s的集成过程依旧少不了“趟坑”，接下来我们就详细道来。</p>
<h4>一、环境和准备条件</h4>
<p>我们依然使用两个阿里云ECS Node，操作系统以及内核版本为：Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64)。</p>
<p>Ceph采用当前Ubuntu 14.04源中最新的Ceph LTS版本：<a href="http://docs.ceph.com/docs/master/release-notes#v10.2.3-jewel">JEWEL10.2.3</a>。</p>
<p><a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">Kubernetes版本</a>为上次安装时的1.3.7版本。</p>
<h4>二、Ceph安装原理</h4>
<p>Ceph分布式存储集群由若干组件组成，包括：Ceph Monitor、Ceph OSD和Ceph MDS，其中如果你仅使用对象存储和块存储时，MDS不是必须的（本次我们也不需要安装MDS），仅当你要用到Cephfs时，MDS才是需要安装的。</p>
<p>Ceph的安装模型与k8s有些类似，也是通过一个deploy node远程操作其他Node以create、prepare和activate各个Node上的Ceph组件，官方手册中给出的示意图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/ceph-install.png" alt="img{}" /></p>
<p>映射到我们实际的环境中，我的安装设计是这样的：</p>
<pre><code>admin-node, deploy-node(ceph-deploy)：10.47.136.60  iZ25cn4xxnvZ
mon.node1，(mds.node1): 10.47.136.60  iZ25cn4xxnvZ
osd.0: 10.47.136.60   iZ25cn4xxnvZ
osd.1: 10.46.181.146 iZ25mjza4msZ
</code></pre>
<p>实际上就是两个Aliyun ECS节点承担以上多种角色。不过像iZ25cn4xxnvZ这样的host name太反人类，长远考虑还是换成node1、node2这样的简单名字更好。通过编辑各个ECS上的/etc/hostname, /etc/hosts，我们将iZ25cn4xxnvZ换成node1，将iZ25mjza4msZ换成node2：</p>
<pre><code>10.47.136.60 （node1)：

# cat /etc/hostname
node1

# cat /etc/hosts
127.0.0.1 localhost
127.0.1.1    localhost.localdomain    localhost

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.47.136.60 admin
10.47.136.60 node1
10.47.136.60 iZ25cn4xxnvZ
10.46.181.146 node2

----------------------------------
10.46.181.146 （node2)：

# cat /etc/hostname
node2

# cat /etc/hosts
127.0.0.1 localhost
127.0.1.1    localhost.localdomain    localhost

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.46.181.146  node2
10.46.181.146  iZ25mjza4msZ
10.47.136.60  node1
</code></pre>
<p>于是上面的环境设计就变成了：</p>
<pre><code>admin-node, deploy-node(ceph-deploy)：node1 10.47.136.60
mon.node1, (mds.node1) : node1  10.47.136.60
osd.0:  node1 10.47.136.60
osd.1:  node2 10.46.181.146
</code></pre>
<h4>三、Ceph安装步骤</h4>
<h5>1、安装ceph-deploy</h5>
<p>Ceph提供了一键式安装工具ceph-deploy来协助Ceph集群的安装，在deploy node上，我们首先要来安装的就是ceph-deploy，Ubuntu 14.04官方源中的ceph-deploy是1.4.0版本，比较old，我们需要添加Ceph源，安装最新的ceph-deploy：</p>
<pre><code># wget -q -O- 'https://download.ceph.com/keys/release.asc' | sudo apt-key add -
OK

# echo deb https://download.ceph.com/debian-jewel/ $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/ceph.list
deb https://download.ceph.com/debian-jewel/ trusty main

#apt-get update
... ...

# apt-get install ceph-deploy
Reading package lists... Done
Building dependency tree
Reading state information... Done
.... ...
The following NEW packages will be installed:
  ceph-deploy
0 upgraded, 1 newly installed, 0 to remove and 105 not upgraded.
Need to get 96.4 kB of archives.
After this operation, 622 kB of additional disk space will be used.
Get:1 https://download.ceph.com/debian-jewel/ trusty/main ceph-deploy all 1.5.35 [96.4 kB]
Fetched 96.4 kB in 1s (53.2 kB/s)
Selecting previously unselected package ceph-deploy.
(Reading database ... 153022 files and directories currently installed.)
Preparing to unpack .../ceph-deploy_1.5.35_all.deb ...
Unpacking ceph-deploy (1.5.35) ...
Setting up ceph-deploy (1.5.35) ...
</code></pre>
<p>注意：ceph-deploy只需要在admin/deploy node上安装即可。</p>
<h5>2、前置设置</h5>
<p>和安装k8s一样，在ceph-deploy真正执行安装之前，需要确保所有Ceph node都要开启NTP，同时建议在每个node节点上为安装过程创建一个安装账号，即ceph-deploy在ssh登录到每个Node时所用的账号。这个账号有两个约束：</p>
<ul>
<li>具有sudo权限；</li>
<li>执行sudo命令时，无需输入密码。</li>
</ul>
<p>我们将这一账号命名为cephd，我们需要在每个ceph node上(包括admin node/deploy node)都建立一个cephd用户，并加入到sudo组中。</p>
<pre><code>以下命令在每个Node上都要执行：

useradd -d /home/cephd -m cephd
passwd cephd

添加sudo权限：
echo "cephd ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/cephd
sudo chmod 0440 /etc/sudoers.d/cephd
</code></pre>
<p>在admin node(deploy node)上，登入cephd账号，创建该账号下deploy node到其他各个Node的ssh免密登录设置，密码留空：</p>
<pre><code>在deploy node上执行：

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/cephd/.ssh/id_rsa):
Created directory '/home/cephd/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/cephd/.ssh/id_rsa.
Your public key has been saved in /home/cephd/.ssh/id_rsa.pub.
The key fingerprint is:
....
</code></pre>
<p>将deploy node的公钥copy到其他节点上去：</p>
<pre><code>$ ssh-copy-id cephd@node1
The authenticity of host 'node1 (10.47.136.60)' can't be established.
ECDSA key fingerprint is d2:69:e2:3a:3e:4c:6b:80:15:30:17:8e:df:3b:62:1f.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
cephd@node1's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'cephd@node1'"
and check to make sure that only the key(s) you wanted were added.
</code></pre>
<p>同样，执行 ssh-copy-id cephd@node2，完成后，测试一下免密登录。</p>
<pre><code>$ ssh node1
Welcome to Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
New release '16.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

Welcome to aliyun Elastic Compute Service!
</code></pre>
<p>最后，在Deploy node上创建并编辑~/.ssh/config，这是Ceph官方doc推荐的步骤，这样做的目的是可以避免每次执行ceph-deploy时都要去指定 &#8211;username {username} 参数。</p>
<pre><code>//~/.ssh/config
Host node1
   Hostname node1
   User cephd
Host node2
   Hostname node2
   User cephd
</code></pre>
<h5>3、安装ceph</h5>
<p>这个环节参考的是Ceph官方doc<a href="http://docs.ceph.com/docs/master/install/manual-deployment/">手工部署一节</a>。</p>
<p>如果之前安装过ceph，可以先执行如下命令以获得一个干净的环境：</p>
<pre><code>ceph-deploy purgedata node1 node2
ceph-deploy forgetkeys
ceph-deploy purge node1 node2
</code></pre>
<p>接下来我们就可以来全新安装Ceph了。在deploy node上，建立cephinstall目录，然后进入cephinstall目录执行相关步骤。</p>
<p>我们首先来创建一个ceph cluster，这个环节需要通过执行ceph-deploy new {initial-monitor-node(s)}命令。按照上面的安装设计，我们的ceph monitor node就是node1，因此我们执行下面命令来创建一个名为ceph的ceph cluster：</p>
<pre><code>$ ceph-deploy new node1
[ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf
[ceph_deploy.cli][INFO  ] Invoked (1.5.35): /usr/bin/ceph-deploy new node1
[ceph_deploy.cli][INFO  ] ceph-deploy options:
[ceph_deploy.cli][INFO  ]  username                      : None
[ceph_deploy.cli][INFO  ]  func                          : &lt;function new at 0x7f71d2051938&gt;
[ceph_deploy.cli][INFO  ]  verbose                       : False
[ceph_deploy.cli][INFO  ]  overwrite_conf                : False
[ceph_deploy.cli][INFO  ]  quiet                         : False
[ceph_deploy.cli][INFO  ]  cd_conf                       : &lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f71d19f5710&gt;
[ceph_deploy.cli][INFO  ]  cluster                       : ceph
[ceph_deploy.cli][INFO  ]  ssh_copykey                   : True
[ceph_deploy.cli][INFO  ]  mon                           : ['node1']
[ceph_deploy.cli][INFO  ]  public_network                : None
[ceph_deploy.cli][INFO  ]  ceph_conf                     : None
[ceph_deploy.cli][INFO  ]  cluster_network               : None
[ceph_deploy.cli][INFO  ]  default_release               : False
[ceph_deploy.cli][INFO  ]  fsid                          : None
[ceph_deploy.new][DEBUG ] Creating new cluster named ceph
[ceph_deploy.new][INFO  ] making sure passwordless SSH succeeds
[node1][DEBUG ] connection detected need for sudo
[node1][DEBUG ] connected to host: node1
[node1][DEBUG ] detect platform information from remote host
[node1][DEBUG ] detect machine type
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /sbin/initctl version
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /bin/ip link show
[node1][INFO  ] Running command: sudo /bin/ip addr show
[node1][DEBUG ] IP addresses found: [u'101.201.78.51', u'192.168.16.1', u'10.47.136.60', u'172.16.99.0', u'172.16.99.1']
[ceph_deploy.new][DEBUG ] Resolving host node1
[ceph_deploy.new][DEBUG ] Monitor node1 at 10.47.136.60
[ceph_deploy.new][DEBUG ] Monitor initial members are ['node1']
[ceph_deploy.new][DEBUG ] Monitor addrs are ['10.47.136.60']
[ceph_deploy.new][DEBUG ] Creating a random mon key...
[ceph_deploy.new][DEBUG ] Writing monitor keyring to ceph.mon.keyring...
[ceph_deploy.new][DEBUG ] Writing initial config to ceph.conf...
</code></pre>
<p>new命令执行完后，ceph-deploy会在当前目录下创建一些辅助文件：</p>
<pre><code># ls
ceph.conf  ceph-deploy-ceph.log  ceph.mon.keyring

$ cat ceph.conf
[global]
fsid = f5166c78-e3b6-4fef-b9e7-1ecf7382fd93
mon_initial_members = node1
mon_host = 10.47.136.60
auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx
</code></pre>
<p>由于我们仅有两个OSD节点，因此我们在进一步安装之前，需要先对ceph.conf文件做一些配置调整：<br />
修改配置以进行后续安装：</p>
<pre><code>在[global]标签下，添加下面一行：
osd pool default size = 2
</code></pre>
<p>ceph.conf保存退出。接下来，我们执行下面命令在node1和node2上安装ceph运行所需的各个binary包：</p>
<pre><code># ceph-deploy install nod1 node2
.... ...
[node2][INFO  ] Running command: sudo ceph --version
[node2][DEBUG ] ceph version 10.2.3 (ecc23778eb545d8dd55e2e4735b53cc93f92e65b)
</code></pre>
<p>这一过程ceph-deploy会SSH登录到各个node上去，执行apt-get update, 并install ceph的各种组件包，这个环节耗时可能会长一些（依网络情况不同而不同），请耐心等待。</p>
<h5>4、初始化ceph monitor node</h5>
<p>有了ceph启动的各个程序后，我们首先来初始化ceph cluster的monitor node。在deploy node的工作目录cephinstall下，执行：</p>
<pre><code># ceph-deploy mon create-initial

[ceph_deploy.conf][DEBUG ] found configuration file at: /root/.cephdeploy.conf
[ceph_deploy.cli][INFO  ] Invoked (1.5.35): /usr/bin/ceph-deploy mon create-initial
[ceph_deploy.cli][INFO  ] ceph-deploy options:
[ceph_deploy.cli][INFO  ]  username                      : None
[ceph_deploy.cli][INFO  ]  verbose                       : False
[ceph_deploy.cli][INFO  ]  overwrite_conf                : False
[ceph_deploy.cli][INFO  ]  subcommand                    : create-initial
[ceph_deploy.cli][INFO  ]  quiet                         : False
[ceph_deploy.cli][INFO  ]  cd_conf                       : &lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f0f7ea2fe60&gt;
[ceph_deploy.cli][INFO  ]  cluster                       : ceph
[ceph_deploy.cli][INFO  ]  func                          : &lt;function mon at 0x7f0f7ee93de8&gt;
[ceph_deploy.cli][INFO  ]  ceph_conf                     : None
[ceph_deploy.cli][INFO  ]  default_release               : False
[ceph_deploy.cli][INFO  ]  keyrings                      : None
[ceph_deploy.mon][DEBUG ] Deploying mon, cluster ceph hosts node1
[ceph_deploy.mon][DEBUG ] detecting platform for host node1...
[node1][DEBUG ] connected to host: node1
[node1][DEBUG ] detect platform information from remote host
[node1][DEBUG ] detect machine type
....

[iZ25cn4xxnvZ][INFO  ] Running command: ceph --cluster=ceph --admin-daemon /var/run/ceph/ceph-mon.iZ25cn4xxnvZ.asok mon_status
[ceph_deploy.mon][INFO  ] mon.iZ25cn4xxnvZ monitor has reached quorum!
[ceph_deploy.mon][INFO  ] all initial monitors are running and have formed quorum
[ceph_deploy.mon][INFO  ] Running gatherkeys...
[ceph_deploy.gatherkeys][INFO  ] Storing keys in temp directory /tmp/tmpP_SmXX
[iZ25cn4xxnvZ][DEBUG ] connected to host: iZ25cn4xxnvZ
[iZ25cn4xxnvZ][DEBUG ] detect platform information from remote host
[iZ25cn4xxnvZ][DEBUG ] detect machine type
[iZ25cn4xxnvZ][DEBUG ] find the location of an executable
[iZ25cn4xxnvZ][INFO  ] Running command: /sbin/initctl version
[iZ25cn4xxnvZ][DEBUG ] get remote short hostname
[iZ25cn4xxnvZ][DEBUG ] fetch remote file
[iZ25cn4xxnvZ][INFO  ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --admin-daemon=/var/run/ceph/ceph-mon.iZ25cn4xxnvZ.asok mon_status
[iZ25cn4xxnvZ][INFO  ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.admin osd allow * mds allow * mon allow *
[iZ25cn4xxnvZ][INFO  ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.bootstrap-mds mon allow profile bootstrap-mds
[iZ25cn4xxnvZ][INFO  ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.bootstrap-osd mon allow profile bootstrap-osd
[iZ25cn4xxnvZ][INFO  ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.bootstrap-rgw mon allow profile bootstrap-rgw
... ...
[ceph_deploy.gatherkeys][INFO  ] Storing ceph.client.admin.keyring
[ceph_deploy.gatherkeys][INFO  ] Storing ceph.bootstrap-mds.keyring
[ceph_deploy.gatherkeys][INFO  ] keyring 'ceph.mon.keyring' already exists
[ceph_deploy.gatherkeys][INFO  ] Storing ceph.bootstrap-osd.keyring
[ceph_deploy.gatherkeys][INFO  ] Storing ceph.bootstrap-rgw.keyring
[ceph_deploy.gatherkeys][INFO  ] Destroy temp directory /tmp/tmpP_SmXX
</code></pre>
<p>这一过程很顺利。命令执行完成后我们能看到一些变化：</p>
<p>在当前目录下，出现了若干*.keyring，这是Ceph组件间进行安全访问时所需要的：</p>
<pre><code># ls -l
total 216
-rw------- 1 root root     71 Nov  3 17:24 ceph.bootstrap-mds.keyring
-rw------- 1 root root     71 Nov  3 17:25 ceph.bootstrap-osd.keyring
-rw------- 1 root root     71 Nov  3 17:25 ceph.bootstrap-rgw.keyring
-rw------- 1 root root     63 Nov  3 17:24 ceph.client.admin.keyring
-rw-r--r-- 1 root root    242 Nov  3 16:40 ceph.conf
-rw-r--r-- 1 root root 192336 Nov  3 17:25 ceph-deploy-ceph.log
-rw------- 1 root root     73 Nov  3 16:28 ceph.mon.keyring
-rw-r--r-- 1 root root   1645 Oct 16  2015 release.asc
</code></pre>
<p>在node1(monitor node)上，我们看到ceph-mon已经运行起来了：</p>
<pre><code>cephd@node1:~/cephinstall$ ps -ef|grep ceph
ceph     32326     1  0 14:19 ?        00:00:00 /usr/bin/ceph-mon --cluster=ceph -i node1 -f --setuser ceph --setgroup ceph
</code></pre>
<p>如果要手工停止ceph-mon，可以使用stop ceph-mon-all 命令。</p>
<h5>5、prepare ceph OSD node</h5>
<p>至此，ceph-mon组件程序已经成功启动了，剩下的只有OSD这一关了。启动OSD node分为两步：prepare 和 activate。OSD node是真正存储数据的节点，我们需要为ceph-osd提供独立存储空间，一般是一个独立的disk。但我们环境不具备这个条件，于是在本地盘上创建了个目录，提供给OSD。</p>
<pre><code>在deploy node上执行：

ssh node1
sudo mkdir /var/local/osd0
exit

ssh node2
sudo mkdir /var/local/osd1
exit
</code></pre>
<p>接下来，我们就可以执行prepare操作了，prepare操作会在上述的两个osd0和osd1目录下创建一些后续activate激活以及osd运行时所需要的文件：</p>
<pre><code>cephd@node1:~/cephinstall$ ceph-deploy osd prepare node1:/var/local/osd0 node2:/var/local/osd1
[ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf
[ceph_deploy.cli][INFO  ] Invoked (1.5.35): /usr/bin/ceph-deploy osd prepare node1:/var/local/osd0 node2:/var/local/osd1
[ceph_deploy.cli][INFO  ] ceph-deploy options:
[ceph_deploy.cli][INFO  ]  username                      : None
[ceph_deploy.cli][INFO  ]  disk                          : [('node1', '/var/local/osd0', None), ('node2', '/var/local/osd1', None)]
[ceph_deploy.cli][INFO  ]  dmcrypt                       : False
[ceph_deploy.cli][INFO  ]  verbose                       : False
[ceph_deploy.cli][INFO  ]  bluestore                     : None
[ceph_deploy.cli][INFO  ]  overwrite_conf                : False
[ceph_deploy.cli][INFO  ]  subcommand                    : prepare
[ceph_deploy.cli][INFO  ]  dmcrypt_key_dir               : /etc/ceph/dmcrypt-keys
[ceph_deploy.cli][INFO  ]  quiet                         : False
[ceph_deploy.cli][INFO  ]  cd_conf                       : &lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f072603e8c0&gt;
[ceph_deploy.cli][INFO  ]  cluster                       : ceph
[ceph_deploy.cli][INFO  ]  fs_type                       : xfs
[ceph_deploy.cli][INFO  ]  func                          : &lt;function osd at 0x7f0726492d70&gt;
[ceph_deploy.cli][INFO  ]  ceph_conf                     : None
[ceph_deploy.cli][INFO  ]  default_release               : False
[ceph_deploy.cli][INFO  ]  zap_disk                      : False
[ceph_deploy.osd][DEBUG ] Preparing cluster ceph disks node1:/var/local/osd0: node2:/var/local/osd1:
[node1][DEBUG ] connection detected need for sudo
[node1][DEBUG ] connected to host: node1
[node1][DEBUG ] detect platform information from remote host
[node1][DEBUG ] detect machine type
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /sbin/initctl version
[node1][DEBUG ] find the location of an executable
[ceph_deploy.osd][INFO  ] Distro info: Ubuntu 14.04 trusty
[ceph_deploy.osd][DEBUG ] Deploying osd to node1
[node1][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf
[ceph_deploy.osd][DEBUG ] Preparing host node1 disk /var/local/osd0 journal None activate False
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /usr/sbin/ceph-disk -v prepare --cluster ceph --fs-type xfs -- /var/local/osd0
[node1][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=fsid
[node1][WARNIN] command: Running command: /usr/bin/ceph-osd --check-allows-journal -i 0 --cluster ceph
[node1][WARNIN] command: Running command: /usr/bin/ceph-osd --check-wants-journal -i 0 --cluster ceph
[node1][WARNIN] command: Running command: /usr/bin/ceph-osd --check-needs-journal -i 0 --cluster ceph
[node1][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=osd_journal_size
[node1][WARNIN] populate_data_path: Preparing osd data dir /var/local/osd0
[node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/ceph_fsid.782.tmp
[node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/fsid.782.tmp
[node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/magic.782.tmp
[node1][INFO  ] checking OSD status...
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json
[
ceph_deploy.osd][DEBUG ] Host node1 is now ready for osd use.
[node2][DEBUG ] connection detected need for sudo
[node2][DEBUG ] connected to host: node2
... ...
[node2][INFO  ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json
[ceph_deploy.osd][DEBUG ] Host node2 is now ready for osd use.
</code></pre>
<p>prepare并不会启动ceph osd，那是activate的职责。</p>
<h5>6、激活ceph OSD node</h5>
<p>接下来，我们来激活各个OSD node：</p>
<pre><code>$ ceph-deploy osd activate node1:/var/local/osd0 node2:/var/local/osd1

... ...
[node1][WARNIN] got monmap epoch 1
[node1][WARNIN] command: Running command: /usr/bin/timeout 300 ceph-osd --cluster ceph --mkfs --mkkey -i 0 --monmap /var/local/osd0/activate.monmap --osd-data /var/local/osd0 --osd-journal /var/local/osd0/journal --osd-uuid 6def4f7f-4f37-43a5-8699-5c6ab608c89c --keyring /var/local/osd0/keyring --setuser ceph --setgroup ceph
[node1][WARNIN] Traceback (most recent call last):
[node1][WARNIN]   File "/usr/sbin/ceph-disk", line 9, in &lt;module&gt;
[node1][WARNIN]     load_entry_point('ceph-disk==1.0.0', 'console_scripts', 'ceph-disk')()
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 5011, in run
[node1][WARNIN]     main(sys.argv[1:])
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 4962, in main
[node1][WARNIN]     args.func(args)
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 3324, in main_activate
[node1][WARNIN]     init=args.mark_init,
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 3144, in activate_dir
[node1][WARNIN]     (osd_id, cluster) = activate(path, activate_key_template, init)
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 3249, in activate
[node1][WARNIN]     keyring=keyring,
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 2742, in mkfs
[node1][WARNIN]     '--setgroup', get_ceph_group(),
[node1][WARNIN]   File "/usr/lib/python2.7/dist-packages/ceph_disk/main.py", line 2689, in ceph_osd_mkfs
[node1][WARNIN]     raise Error('%s failed : %s' % (str(arguments), error))
[node1][WARNIN] ceph_disk.main.Error: Error: ['ceph-osd', '--cluster', 'ceph', '--mkfs', '--mkkey', '-i', '0', '--monmap', '/var/local/osd0/activate.monmap', '--osd-data', '/var/local/osd0', '--osd-journal', '/var/local/osd0/journal', '--osd-uuid', '6def4f7f-4f37-43a5-8699-5c6ab608c89c', '--keyring', '/var/local/osd0/keyring', '--setuser', 'ceph', '--setgroup', 'ceph'] failed : 2016-11-04 14:25:40.325009 7fd1aa73f800 -1 filestore(/var/local/osd0) mkfs: write_version_stamp() failed: (13) Permission denied
[node1][WARNIN] 2016-11-04 14:25:40.325032 7fd1aa73f800 -1 OSD::mkfs: ObjectStore::mkfs failed with error -13
[node1][WARNIN] 2016-11-04 14:25:40.325075 7fd1aa73f800 -1  ** ERROR: error creating empty object store in /var/local/osd0: (13) Permission denied
[node1][WARNIN]
[node1][ERROR ] RuntimeError: command returned non-zero exit status: 1
[ceph_deploy][ERROR ] RuntimeError: Failed to execute command: /usr/sbin/ceph-disk -v activate --mark-init upstart --mount /var/local/osd0
</code></pre>
<p>激活没能成功，在激活第一个节点时，就输出了如上错误日志。日志的error含义很明显：权限问题。</p>
<p>ceph-deploy尝试在osd node1上以ceph:ceph启动ceph-osd，但/var/local/osd0目录的权限情况如下：</p>
<pre><code>$ ls -l /var/local
drwxr-sr-x 2 root staff 4096 Nov  4 14:25 osd0
</code></pre>
<p>osd0被root拥有，以ceph用户启动的ceph-osd程序自然没有权限在/var/local/osd0目录下创建文件并写入数据了。这个问题在ceph官方issue中有很多人提出来，也给出了临时修正方法：</p>
<pre><code>将osd0和osd1的权限赋予ceph:ceph：

node1：
sudo chown -R ceph:ceph /var/local/osd0

node2：
sudo chown -R ceph:ceph /var/local/osd1

</code></pre>
<p>修改完权限后，我们再来执行activate：</p>
<pre><code>$ ceph-deploy osd activate node1:/var/local/osd0 node2:/var/local/osd1
[ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf
[ceph_deploy.cli][INFO  ] Invoked (1.5.35): /usr/bin/ceph-deploy osd activate node1:/var/local/osd0 node2:/var/local/osd1
[ceph_deploy.cli][INFO  ] ceph-deploy options:
[ceph_deploy.cli][INFO  ]  username                      : None
[ceph_deploy.cli][INFO  ]  verbose                       : False
[ceph_deploy.cli][INFO  ]  overwrite_conf                : False
[ceph_deploy.cli][INFO  ]  subcommand                    : activate
[ceph_deploy.cli][INFO  ]  quiet                         : False
[ceph_deploy.cli][INFO  ]  cd_conf                       : &lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f3c90c678c0&gt;
[ceph_deploy.cli][INFO  ]  cluster                       : ceph
[ceph_deploy.cli][INFO  ]  func                          : &lt;function osd at 0x7f3c910bbd70&gt;
[ceph_deploy.cli][INFO  ]  ceph_conf                     : None
[ceph_deploy.cli][INFO  ]  default_release               : False
[ceph_deploy.cli][INFO  ]  disk                          : [('node1', '/var/local/osd0', None), ('node2', '/var/local/osd1', None)]
[ceph_deploy.osd][DEBUG ] Activating cluster ceph disks node1:/var/local/osd0: node2:/var/local/osd1:
[node1][DEBUG ] connection detected need for sudo
[node1][DEBUG ] connected to host: node1
[node1][DEBUG ] detect platform information from remote host
[node1][DEBUG ] detect machine type
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /sbin/initctl version
[node1][DEBUG ] find the location of an executable
[ceph_deploy.osd][INFO  ] Distro info: Ubuntu 14.04 trusty
[ceph_deploy.osd][DEBUG ] activating host node1 disk /var/local/osd0
[ceph_deploy.osd][DEBUG ] will use init type: upstart
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /usr/sbin/ceph-disk -v activate --mark-init upstart --mount /var/local/osd0
[node1][WARNIN] main_activate: path = /var/local/osd0
[node1][WARNIN] activate: Cluster uuid is f5166c78-e3b6-4fef-b9e7-1ecf7382fd93
[node1][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=fsid
[node1][WARNIN] activate: Cluster name is ceph
[node1][WARNIN] activate: OSD uuid is 6def4f7f-4f37-43a5-8699-5c6ab608c89c
[node1][WARNIN] activate: OSD id is 0
[node1][WARNIN] activate: Initializing OSD...
[node1][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring mon getmap -o /var/local/osd0/activate.monmap
[node1][WARNIN] got monmap epoch 1
[node1][WARNIN] command: Running command: /usr/bin/timeout 300 ceph-osd --cluster ceph --mkfs --mkkey -i 0 --monmap /var/local/osd0/activate.monmap --osd-data /var/local/osd0 --osd-journal /var/local/osd0/journal --osd-uuid 6def4f7f-4f37-43a5-8699-5c6ab608c89c --keyring /var/local/osd0/keyring --setuser ceph --setgroup ceph
[node1][WARNIN] activate: Marking with init system upstart
[node1][WARNIN] activate: Authorizing OSD key...
[node1][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring auth add osd.0 -i /var/local/osd0/keyring osd allow * mon allow profile osd
[node1][WARNIN] added key for osd.0
[node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/active.4616.tmp
[node1][WARNIN] activate: ceph osd.0 data dir is ready at /var/local/osd0
[node1][WARNIN] activate_dir: Creating symlink /var/lib/ceph/osd/ceph-0 -&gt; /var/local/osd0
[node1][WARNIN] start_daemon: Starting ceph osd.0...
[node1][WARNIN] command_check_call: Running command: /sbin/initctl emit --no-wait -- ceph-osd cluster=ceph id=0
[node1][INFO  ] checking OSD status...
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json
[node1][WARNIN] there is 1 OSD down
[node1][WARNIN] there is 1 OSD out

[node2][DEBUG ] connection detected need for sudo
[node2][DEBUG ] connected to host: node2
[node2][DEBUG ] detect platform information from remote host
[node2][DEBUG ] detect machine type
[node2][DEBUG ] find the location of an executable
[node2][INFO  ] Running command: sudo /sbin/initctl version
[node2][DEBUG ] find the location of an executable
[ceph_deploy.osd][INFO  ] Distro info: Ubuntu 14.04 trusty
[ceph_deploy.osd][DEBUG ] activating host node2 disk /var/local/osd1
[ceph_deploy.osd][DEBUG ] will use init type: upstart
[node2][DEBUG ] find the location of an executable
[node2][INFO  ] Running command: sudo /usr/sbin/ceph-disk -v activate --mark-init upstart --mount /var/local/osd1
[node2][WARNIN] main_activate: path = /var/local/osd1
[node2][WARNIN] activate: Cluster uuid is f5166c78-e3b6-4fef-b9e7-1ecf7382fd93
[node2][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=fsid
[node2][WARNIN] activate: Cluster name is ceph
[node2][WARNIN] activate: OSD uuid is 4733f683-0376-4708-86a6-818af987ade2
[node2][WARNIN] allocate_osd_id: Allocating OSD id...
[node2][WARNIN] command: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring osd create --concise 4733f683-0376-4708-86a6-818af987ade2
[node2][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd1/whoami.27470.tmp
[node2][WARNIN] activate: OSD id is 1
[node2][WARNIN] activate: Initializing OSD...
[node2][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring mon getmap -o /var/local/osd1/activate.monmap
[node2][WARNIN] got monmap epoch 1
[node2][WARNIN] command: Running command: /usr/bin/timeout 300 ceph-osd --cluster ceph --mkfs --mkkey -i 1 --monmap /var/local/osd1/activate.monmap --osd-data /var/local/osd1 --osd-journal /var/local/osd1/journal --osd-uuid 4733f683-0376-4708-86a6-818af987ade2 --keyring /var/local/osd1/keyring --setuser ceph --setgroup ceph
[node2][WARNIN] activate: Marking with init system upstart
[node2][WARNIN] activate: Authorizing OSD key...
[node2][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring auth add osd.1 -i /var/local/osd1/keyring osd allow * mon allow profile osd
[node2][WARNIN] added key for osd.1
[node2][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd1/active.27470.tmp
[node2][WARNIN] activate: ceph osd.1 data dir is ready at /var/local/osd1
[node2][WARNIN] activate_dir: Creating symlink /var/lib/ceph/osd/ceph-1 -&gt; /var/local/osd1
[node2][WARNIN] start_daemon: Starting ceph osd.1...
[node2][WARNIN] command_check_call: Running command: /sbin/initctl emit --no-wait -- ceph-osd cluster=ceph id=1
[node2][INFO  ] checking OSD status...
[node2][DEBUG ] find the location of an executable
[node2][INFO  ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json
</code></pre>
<p>没有错误报出！但OSD真的运行起来了吗？我们还需要再确认一下。</p>
<p>我们先通过ceph admin命令将各个.keyring同步到各个Node上，以便可以在各个Node上使用ceph命令连接到monitor：</p>
<p>注意：执行ceph admin前，需要在deploy-node的/etc/hosts中添加：</p>
<pre><code>10.47.136.60 admin
</code></pre>
<p>执行ceph admin:</p>
<pre><code>$ ceph-deploy admin admin node1 node2
[ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf
[ceph_deploy.cli][INFO  ] Invoked (1.5.35): /usr/bin/ceph-deploy admin admin node1 node2
[ceph_deploy.cli][INFO  ] ceph-deploy options:
[ceph_deploy.cli][INFO  ]  username                      : None
[ceph_deploy.cli][INFO  ]  verbose                       : False
[ceph_deploy.cli][INFO  ]  overwrite_conf                : False
[ceph_deploy.cli][INFO  ]  quiet                         : False
[ceph_deploy.cli][INFO  ]  cd_conf                       : &lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f072ee3b758&gt;
[ceph_deploy.cli][INFO  ]  cluster                       : ceph
[ceph_deploy.cli][INFO  ]  client                        : ['admin', 'node1', 'node2']
[ceph_deploy.cli][INFO  ]  func                          : &lt;function admin at 0x7f072f6cf5f0&gt;
[ceph_deploy.cli][INFO  ]  ceph_conf                     : None
[ceph_deploy.cli][INFO  ]  default_release               : False
[ceph_deploy.admin][DEBUG ] Pushing admin keys and conf to admin
[admin][DEBUG ] connection detected need for sudo
[admin][DEBUG ] connected to host: admin
[admin][DEBUG ] detect platform information from remote host
[admin][DEBUG ] detect machine type
[admin][DEBUG ] find the location of an executable
[admin][INFO  ] Running command: sudo /sbin/initctl version
[admin][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf
[ceph_deploy.admin][DEBUG ] Pushing admin keys and conf to node1
[node1][DEBUG ] connection detected need for sudo
[node1][DEBUG ] connected to host: node1
[node1][DEBUG ] detect platform information from remote host
[node1][DEBUG ] detect machine type
[node1][DEBUG ] find the location of an executable
[node1][INFO  ] Running command: sudo /sbin/initctl version
[node1][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf
[ceph_deploy.admin][DEBUG ] Pushing admin keys and conf to node2
[node2][DEBUG ] connection detected need for sudo
[node2][DEBUG ] connected to host: node2
[node2][DEBUG ] detect platform information from remote host
[node2][DEBUG ] detect machine type
[node2][DEBUG ] find the location of an executable
[node2][INFO  ] Running command: sudo /sbin/initctl version
[node2][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf

$sudo chmod +r /etc/ceph/ceph.client.admin.keyring
</code></pre>
<p>接下来，查看一下ceph集群中的OSD节点状态：</p>
<pre><code>$ ceph osd tree
ID WEIGHT  TYPE NAME             UP/DOWN REWEIGHT PRIMARY-AFFINITY
-1 0.07660 root default
-2 0.03830     host node1
 0 0.03830         osd.0            down        0          1.00000
-3 0.03830     host iZ25mjza4msZ
 1 0.03830         osd.1            down        0          1.00000
</code></pre>
<p>果不其然，两个osd节点均处于down状态，一个也没有启动起来。问题在哪？</p>
<p>我们来查看一下node1上的日志：/var/log/ceph/ceph-osd.0.log：</p>
<pre><code>016-11-04 15:33:17.088971 7f568d6db800  0 pidfile_write: ignore empty --pid-file
2016-11-04 15:33:17.102052 7f568d6db800  0 filestore(/var/lib/ceph/osd/ceph-0) backend generic (magic 0xef53)
2016-11-04 15:33:17.102071 7f568d6db800 -1 filestore(/var/lib/ceph/osd/ceph-0) WARNING: max attr value size (1024) is smaller than osd_max_object_name_len (2048).  Your backend filesystem appears to not support attrs large enough to handle the configured max rados name size.  You may get unexpected ENAMETOOLONG errors on rados operations or buggy behavior
2016-11-04 15:33:17.102410 7f568d6db800  0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: FIEMAP ioctl is disabled via 'filestore fiemap' config option
2016-11-04 15:33:17.102425 7f568d6db800  0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: SEEK_DATA/SEEK_HOLE is disabled via 'filestore seek data hole' config option
2016-11-04 15:33:17.102445 7f568d6db800  0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: splice is supported
2016-11-04 15:33:17.119261 7f568d6db800  0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: syncfs(2) syscall fully supported (by glibc and kernel)
2016-11-04 15:33:17.127630 7f568d6db800  0 filestore(/var/lib/ceph/osd/ceph-0) limited size xattrs
2016-11-04 15:33:17.128125 7f568d6db800  1 leveldb: Recovering log #38
2016-11-04 15:33:17.136595 7f568d6db800  1 leveldb: Delete type=3 #37
2016-11-04 15:33:17.136656 7f568d6db800  1 leveldb: Delete type=0 #38
2016-11-04 15:33:17.136845 7f568d6db800  0 filestore(/var/lib/ceph/osd/ceph-0) mount: enabling WRITEAHEAD journal mode: checkpoint is not enabled
2016-11-04 15:33:17.137064 7f568d6db800 -1 journal FileJournal::_open: disabling aio for non-block journal.  Use journal_force_aio to force use of aio anyway
2016-11-04 15:33:17.137068 7f568d6db800  1 journal _open /var/lib/ceph/osd/ceph-0/journal fd 18: 5368709120 bytes, block size 4096 bytes, directio = 1, aio = 0
2016-11-04 15:33:17.137897 7f568d6db800  1 journal _open /var/lib/ceph/osd/ceph-0/journal fd 18: 5368709120 bytes, block size 4096 bytes, directio = 1, aio = 0
2016-11-04 15:33:17.138243 7f568d6db800  1 filestore(/var/lib/ceph/osd/ceph-0) upgrade
2016-11-04 15:33:17.138453 7f568d6db800 -1 osd.0 0 backend (filestore) is unable to support max object name[space] len
2016-11-04 15:33:17.138481 7f568d6db800 -1 osd.0 0    osd max object name len = 2048
2016-11-04 15:33:17.138485 7f568d6db800 -1 osd.0 0    osd max object namespace len = 256
2016-11-04 15:33:17.138488 7f568d6db800 -1 osd.0 0 (36) File name too long
2016-11-04 15:33:17.138895 7f568d6db800  1 journal close /var/lib/ceph/osd/ceph-0/journal
2016-11-04 15:33:17.140041 7f568d6db800 -1  ** ERROR: osd init failed: (36) File name too long
</code></pre>
<p>的确发现了错误日志：</p>
<pre><code>2016-11-04 15:33:17.138481 7f568d6db800 -1 osd.0 0    osd max object name len = 2048
2016-11-04 15:33:17.138485 7f568d6db800 -1 osd.0 0    osd max object namespace len = 256
2016-11-04 15:33:17.138488 7f568d6db800 -1 osd.0 0 (36) File name too long
2016-11-04 15:33:17.138895 7f568d6db800  1 journal close /var/lib/ceph/osd/ceph-0/journal
2016-11-04 15:33:17.140041 7f568d6db800 -1  ** ERROR: osd init failed: (36) File name too long
</code></pre>
<p>进一步搜索ceph官方文档，发现在<a href="http://docs.ceph.com/docs/jewel/rados/configuration/filesystem-recommendations/">文件系统推荐</a>这个doc中有提到，官方不建议采用ext4文件系统作为ceph的后端文件系统，如果采用，那么对于ext4的filesystem，应该在ceph.conf中添加如下配置：</p>
<pre><code>osd max object name len = 256
osd max object namespace len = 64
</code></pre>
<p>由于配置已经分发到个个node上，我们需要到各个Node上同步修改：/etc/ceph/ceph.conf，添加上面两行。然后重新activate osd node，这里不赘述。重新激活后，我们来查看ceph osd状态：</p>
<pre><code>$ ceph osd tree
ID WEIGHT  TYPE NAME             UP/DOWN REWEIGHT PRIMARY-AFFINITY
-1 0.07660 root default
-2 0.03830     host node1
 0 0.03830         osd.0              up  1.00000          1.00000
-3 0.03830     host iZ25mjza4msZ
 1 0.03830         osd.1              up  1.00000          1.00000

 $ceph -s
    cluster f5166c78-e3b6-4fef-b9e7-1ecf7382fd93
     health HEALTH_OK
     monmap e1: 1 mons at {node1=10.47.136.60:6789/0}
            election epoch 3, quorum 0 node1
     osdmap e11: 2 osds: 2 up, 2 in
            flags sortbitwise
      pgmap v29: 64 pgs, 1 pools, 0 bytes data, 0 objects
            37834 MB used, 38412 MB / 80374 MB avail
                  64 active+clean

$ !ps
ps -ef|grep ceph
ceph       17139       1  0 16:20 ?        00:00:00 /usr/bin/ceph-osd --cluster=ceph -i 0 -f --setuser ceph --setgroup ceph
</code></pre>
<p>可以看到ceph osd节点上的ceph-osd启动正常，cluster 状态为active+clean，至此，Ceph Cluster集群安装ok（我们暂不需要Ceph MDS组件）。</p>
<h4>四、创建一个使用Ceph RBD作为后端Volume的Pod</h4>
<p>在这一节中，我们就要将Ceph RBD与Kubenetes做集成了。<a href="https://github.com/kubernetes/kubernetes">Kubernetes的官方源码</a>的examples/volumes/rbd目录下，就有一个使用cephrbd作为kubernetes pod volume的例子，我们试着将其跑起来。</p>
<p>例子提供了两个pod描述文件：rbd.json和rbd-with-secret.json。由于我们在ceph install时在ceph.conf中使用默认的安全验证协议<a href="http://docs.ceph.com/docs/master/rados/configuration/auth-config-ref/">cephx &#8211; The Ceph authentication protocol</a>了：</p>
<pre><code>auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx
</code></pre>
<p>因此我们将采用rbd-with-secret.json这个pod描述文件来创建例子中的Pod，限于篇幅，这里仅节选json文件中的volumes部分：</p>
<pre><code>//例子中的rbd-with-secret.json

{
    ... ...
        "volumes": [
            {
                "name": "rbdpd",
                "rbd": {
                    "monitors": [
                           "10.16.154.78:6789",
                           "10.16.154.82:6789",
                           "10.16.154.83:6789"
                                 ],
                    "pool": "kube",
                    "image": "foo",
                    "user": "admin",
                    "secretRef": {
                           "name": "ceph-secret"
                                         },
                    "fsType": "ext4",
                    "readOnly": true
                }
            }
        ]
    }
}
</code></pre>
<p>volumes部分是和ceph rbd紧密相关的一些信息，各个字段的大致含义如下：</p>
<p>name：volume名字，这个没什么可说的，顾名思义即可。<br />
rbd.monitors：前面提到过ceph集群的monitor组件，这里填写monitor组件的通信信息，集群里有几个monitor就填几个；<br />
rbd.pool：Ceph中的<a href="http://docs.ceph.com/docs/master/rados/operations/pools/">pool记号</a>，它用来给ceph中存储的对象进行逻辑分区用的。默认的pool是”rbd”；<br />
rbd.image：Ceph磁盘块设备<a href="https://en.wikipedia.org/wiki/Disk_image">映像文件</a>；<br />
rbd.user：ceph client访问ceph storage cluster所使用的用户名。ceph有自己的一套<a href="http://docs.ceph.com/docs/master/rados/operations/user-management/">user管理系统</a>，user的写法通常是TYPE.ID，比如client.admin（是不是想到对应的文件：ceph.client.admin.keyring）。client是一种type，而admin则是user。一般来说，Type基本都是client。<br />
secret.Ref：引用的k8s secret对象名称。</p>
<p>上面的字段中，有两个字段值我们无法提供：rbd.image和secret.Ref，现在我们就来“填空”。我们在root用户下建立k8s-cephrbd工作目录，我们首先需要使用ceph提供的rbd工具创建Pod要用到image：</p>
<pre><code># rbd create foo -s 1024

# rbd list
foo
</code></pre>
<p>我们在rbd pool中(在上述命令中未指定pool name，默认image建立在rbd pool中)创建一个大小为1024Mi的ceph image foo，rbd list命令的输出告诉我们foo image创建成功。接下来，我们尝试将foo image映射到内核，并格式化该image：</p>
<pre><code>root@node1:~# rbd map foo
rbd: sysfs write failed
RBD image feature set mismatch. You can disable features unsupported by the kernel with "rbd feature disable".
In some cases useful info is found in syslog - try "dmesg | tail" or so.
rbd: map failed: (6) No such device or address
</code></pre>
<p>map操作报错。不过从错误提示信息，我们能找到一些蛛丝马迹：“RBD image feature set mismatch”。ceph新版中在map image时，给image默认加上了许多feature，通过rbd info可以查看到：</p>
<pre><code># rbd info foo
rbd image 'foo':
    size 1024 MB in 256 objects
    order 22 (4096 kB objects)
    block_name_prefix: rbd_data.10612ae8944a
    format: 2
    features: layering, exclusive-lock, object-map, fast-diff, deep-flatten
    flags:
</code></pre>
<p>可以看到foo image拥有： layering, exclusive-lock, object-map, fast-diff, deep-flatten。不过遗憾的是我的Ubuntu 14.04的3.19内核仅支持其中的layering feature，其他feature概不支持。我们需要手动disable这些features：</p>
<pre><code># rbd feature disable foo exclusive-lock, object-map, fast-diff, deep-flatten
root@node1:/var/log/ceph# rbd info foo
rbd image 'foo':
    size 1024 MB in 256 objects
    order 22 (4096 kB objects)
    block_name_prefix: rbd_data.10612ae8944a
    format: 2
    features: layering
    flags:
</code></pre>
<p>不过每次这么来disable可是十分麻烦的，一劳永逸的方法是在各个cluster node的/etc/ceph/ceph.conf中加上这样一行配置：</p>
<pre><code>rbd_default_features = 1 #仅是layering对应的bit码所对应的整数值
</code></pre>
<p>设置完后，通过下面命令查看配置变化：</p>
<pre><code># ceph --show-config|grep rbd|grep features
rbd_default_features = 1
</code></pre>
<p>关于image features的这个问题，zphj1987的<a href="http://www.zphj1987.com/2016/06/07/rbd%E6%97%A0%E6%B3%95map(rbd-feature-disable)/">这篇文章</a>中有较为详细的讲解。</p>
<p>我们再来map一下foo这个image：</p>
<pre><code># rbd map foo
/dev/rbd0

# ls -l /dev/rbd0
brw-rw---- 1 root disk 251, 0 Nov  5 10:33 /dev/rbd0
</code></pre>
<p>map后，我们就可以像格式化一个空image那样对其进行格式化了，这里格成ext4文件系统（格式化这一步大可不必，在后续小节中你会看到）：</p>
<pre><code># mkfs.ext4 /dev/rbd0
mke2fs 1.42.9 (4-Feb-2014)
Discarding device blocks: done
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=1024 blocks, Stripe width=1024 blocks
65536 inodes, 262144 blocks
13107 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=268435456
8 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks:
    32768, 98304, 163840, 229376

Allocating group tables: done
Writing inode tables: done
Creating journal (8192 blocks): done
Writing superblocks and filesystem accounting information: done
</code></pre>
<p>接下来我们来创建ceph-secret这个k8s secret对象，这个secret对象用于k8s volume插件访问ceph集群：</p>
<p>获取client.admin的keyring值，并用base64编码：</p>
<pre><code># ceph auth get-key client.admin
AQBiKBxYuPXiJRAAsupnTBsURoWzb0k00oM3iQ==

# echo "AQBiKBxYuPXiJRAAsupnTBsURoWzb0k00oM3iQ=="|base64
QVFCaUtCeFl1UFhpSlJBQXN1cG5UQnNVUm9XemIwazAwb00zaVE9PQo=
</code></pre>
<p>在k8s-cephrbd下建立ceph-secret.yaml文件，data下的key字段值即为上面得到的编码值：</p>
<pre><code>//ceph-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: ceph-secret
data:
  key: QVFCaUtCeFl1UFhpSlJBQXN1cG5UQnNVUm9XemIwazAwb00zaVE9PQo=
</code></pre>
<p>创建ceph-secret：</p>
<pre><code># kubectl create -f ceph-secret.yaml
secret "ceph-secret" created

# kubectl get secret
NAME                  TYPE                                  DATA      AGE
ceph-secret           Opaque                                1         16s
</code></pre>
<p>至此，我们的rbd-with-secret.json全貌如下：</p>
<pre><code>{
    "apiVersion": "v1",
    "kind": "Pod",
    "metadata": {
        "name": "rbd2"
    },
    "spec": {
        "containers": [
            {
                "name": "rbd-rw",
                "image": "kubernetes/pause",
                "volumeMounts": [
                    {
                        "mountPath": "/mnt/rbd",
                        "name": "rbdpd"
                    }
                ]
            }
        ],
        "volumes": [
            {
                "name": "rbdpd",
                "rbd": {
                    "monitors": [
                        "10.47.136.60:6789"
                                 ],
                    "pool": "rbd",
                    "image": "foo",
                    "user": "admin",
                    "secretRef": {
                        "name": "ceph-secret"
                        },
                    "fsType": "ext4",
                    "readOnly": true
                }
            }
        ]
    }
}
</code></pre>
<p>基于该Pod描述文件，创建使用cephrbd作为后端存储的pod：</p>
<pre><code># kubectl create -f rbd-with-secret.json
pod "rbd2" created

# kubectl get pod
NAME                        READY     STATUS    RESTARTS   AGE
rbd2                        1/1       Running   0          16s

# rbd showmapped
id pool image snap device
0  rbd  foo   -    /dev/rbd0

# mount
... ...
/dev/rbd0 on /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-foo type ext4 (rw)
</code></pre>
<p>在我的环境中，pod实际被调度到了另外一个k8s node上运行了：</p>
<p>pod被调度到另外一个 node2   上：</p>
<pre><code># docker ps
CONTAINER ID        IMAGE                                          COMMAND                  CREATED             STATUS              PORTS                    NAMES
32f92243f911        kubernetes/pause                               "/pause"                 2 minutes ago       Up 2 minutes                                 k8s_rbd-rw.c1dc309e_rbd2_default_6b6541b9-a306-11e6-ba01-00163e1625a9_a6bb1b20

#docker inspect 32f92243f911
... ...
"Mounts": [
            {
                "Source": "/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/volumes/kubernetes.io~secret/default-token-40z0x",
                "Destination": "/var/run/secrets/kubernetes.io/serviceaccount",
                "Mode": "ro",
                "RW": false,
                "Propagation": "rprivate"
            },
            {
                "Source": "/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/etc-hosts",
                "Destination": "/etc/hosts",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Source": "/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/containers/rbd-rw/a6bb1b20",
                "Destination": "/dev/termination-log",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Source": "/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/volumes/kubernetes.io~rbd/rbdpd",
                "Destination": "/mnt/rbd",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],
... ...
</code></pre>
<h4>五、Kubernetes Persistent Volume和Persistent Volume Claim</h4>
<p>上面一小节讲解了Kubernetes volume与Ceph RBD的结合，但是k8s volume还不能完全满足实际生产过程对持久化存储的需求，因为k8s volume的lifetime和pod的生命周期相同，一旦pod被delete，那么volume中的数据就不复存在了。于是k8s又推出了Persistent Volume(PV)和Persistent Volume Claim(PVC)组合，故名思意：即便挂载其的pod被delete了，PV依旧存在，PV上的数据依旧存在。</p>
<p>由于有了之前的“铺垫”，这里仅仅给出使用PV和PVC的步骤：</p>
<h5>1、创建disk image</h5>
<p>$ rbd create ceph-image -s 128 #考虑后续format快捷，这里只用了128M，仅适用于Demo哦。</p>
<pre><code># rbd create ceph-image -s 128
# rbd info rbd/ceph-image
rbd image 'ceph-image':
    size 128 MB in 32 objects
    order 22 (4096 kB objects)
    block_name_prefix: rbd_data.37202ae8944a
    format: 2
    features: layering
    flags:
</code></pre>
<p>如果这里不先创建一个ceph-image，后续Pod启动时，会出现如下的一些错误，比如pod始终处于ContainerCreating状态：</p>
<pre><code># kubectl get pod
NAME                        READY     STATUS              RESTARTS   AGE
ceph-pod1                   0/1       ContainerCreating   0          13s

</code></pre>
<p>如果出现这种错误情况，可以查看/var/log/upstart/kubelet.log，你也许能看到如下错误信息：</p>
<pre><code>I1107 06:02:27.500247   22037 operation_executor.go:768] MountVolume.SetUp succeeded for volume "kubernetes.io/secret/01d049c6-9430-11e6-ba01-00163e1625a9-default-token-40z0x" (spec.Name: "default-token-40z0x") pod "01d049c6-9430-11e6-ba01-00163e1625a9" (UID: "01d049c6-9430-11e6-ba01-00163e1625a9").
I1107 06:03:08.499628   22037 reconciler.go:294] MountVolume operation started for volume "kubernetes.io/rbd/ea848a49-a46b-11e6-ba01-00163e1625a9-ceph-pv" (spec.Name: "ceph-pv") to pod "ea848a49-a46b-11e6-ba01-00163e1625a9" (UID: "ea848a49-a46b-11e6-ba01-00163e1625a9").
E1107 06:03:09.532348   22037 disk_manager.go:56] failed to attach disk
E1107 06:03:09.532402   22037 rbd.go:228] rbd: failed to setup mount /var/lib/kubelet/pods/ea848a49-a46b-11e6-ba01-00163e1625a9/volumes/kubernetes.io~rbd/ceph-pv rbd: map failed exit status 2 rbd: sysfs write failed
In some cases useful info is found in syslog - try "dmesg | tail" or so.
rbd: map failed: (2) No such file or directory
</code></pre>
<h5>2、创建PV</h5>
<p>我们直接复用之前创建的ceph-secret对象，PV的描述文件ceph-pv.yaml如下：</p>
<pre><code>apiVersion: v1
kind: PersistentVolume
metadata:
  name: ceph-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  rbd:
    monitors:
      - 10.47.136.60:6789
    pool: rbd
    image: ceph-image
    user: admin
    secretRef:
      name: ceph-secret
    fsType: ext4
    readOnly: false
  persistentVolumeReclaimPolicy: Recycle
</code></pre>
<p>执行创建操作：</p>
<pre><code># kubectl create -f ceph-pv.yaml
persistentvolume "ceph-pv" created

# kubectl get pv
NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM     REASON    AGE
ceph-pv   1Gi        RWO           Recycle         Available                       7s
</code></pre>
<h5>3、创建PVC</h5>
<p>pvc是Pod对Pv的请求，将请求做成一种资源，便于管理以及pod复用。我们用到的pvc描述文件ceph-pvc.yaml如下：</p>
<pre><code>kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ceph-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

</code></pre>
<p>执行创建操作：</p>
<pre><code># kubectl create -f ceph-pvc.yaml
persistentvolumeclaim "ceph-claim" created

# kubectl get pvc
NAME         STATUS    VOLUME    CAPACITY   ACCESSMODES   AGE
ceph-claim   Bound     ceph-pv   1Gi        RWO           12s

</code></pre>
<h5>4、创建挂载ceph RBD的pod</h5>
<p>pod描述文件ceph-pod1.yaml如下：</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  name: ceph-pod1
spec:
  containers:
  - name: ceph-busybox1
    image: busybox
    command: ["sleep", "600000"]
    volumeMounts:
    - name: ceph-vol1
      mountPath: /usr/share/busybox
      readOnly: false
  volumes:
  - name: ceph-vol1
    persistentVolumeClaim:
      claimName: ceph-claim
</code></pre>
<p>创建pod操作：</p>
<pre><code># kubectl create -f ceph-pod1.yaml
pod "ceph-pod1" created

# kubectl get pod
NAME                        READY     STATUS              RESTARTS   AGE
ceph-pod1                   0/1       ContainerCreating   0          13s
</code></pre>
<p>Pod还处于ContainerCreating状态。pod的创建，尤其是挂载pv的Pod的创建需要一小段时间，耐心等待一下，我们可以查看一下/var/log/upstart/kubelet.log：</p>
<pre><code>I1107 11:44:38.768541   22037 mount_linux.go:272] `fsck` error fsck from util-linux 2.20.1

fsck.ext2: Bad magic number in super-block while trying to open /dev/rbd1
/dev/rbd1:
The superblock could not be read or does not describe a valid ext2/ext3/ext4
filesystem.  If the device is valid and it really contains an ext2/ext3/ext4
filesystem (and not swap or ufs or something else), then the superblock
is corrupt, and you might try running e2fsck with an alternate superblock:
    e2fsck -b 8193 &lt;device&gt;
 or
    e2fsck -b 32768 &lt;device&gt;

E1107 11:44:38.774080   22037 mount_linux.go:110] Mount failed: exit status 32
Mounting arguments: /dev/rbd1 /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-ceph-image ext4 [defaults]
Output: mount: wrong fs type, bad option, bad superblock on /dev/rbd1,
       missing codepage or helper program, or other error
       In some cases useful info is found in syslog - try
       dmesg | tail  or so

I1107 11:44:38.839148   22037 mount_linux.go:292] Disk "/dev/rbd1" appears to be unformatted, attempting to format as type: "ext4" with options: [-E lazy_itable_init=0,lazy_journal_init=0 -F /dev/rbd1]
I1107 11:44:39.152689   22037 mount_linux.go:297] Disk successfully formatted (mkfs): ext4 - /dev/rbd1 /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-ceph-image
I1107 11:44:39.220223   22037 operation_executor.go:768] MountVolume.SetUp succeeded for volume "kubernetes.io/rbd/811a57ee-a49c-11e6-ba01-00163e1625a9-ceph-pv" (spec.Name: "ceph-pv") pod "811a57ee-a49c-11e6-ba01-00163e1625a9" (UID: "811a57ee-a49c-11e6-ba01-00163e1625a9").
</code></pre>
<p>可以看到，k8s通过fsck发现这个image是一个空image，没有fs在里面，于是默认采用ext4为其格式化，成功后，再行挂载。等待一会后，我们看到ceph-pod1成功run起来了：</p>
<pre><code># kubectl get pod
NAME                        READY     STATUS    RESTARTS   AGE
ceph-pod1                   1/1       Running   0          4m

# docker ps
CONTAINER ID        IMAGE                                                 COMMAND                  CREATED             STATUS              PORTS               NAMES
f50bb8c31b0f        busybox                                               "sleep 600000"           4 hours ago         Up 4 hours                              k8s_ceph-busybox1.c0c0379f_ceph-pod1_default_811a57ee-a49c-11e6-ba01-00163e1625a9_9d910a29

# docker exec 574b8069e548 df -h
Filesystem                Size      Used Available Use% Mounted on
none                     39.2G     20.9G     16.3G  56% /
tmpfs                     1.9G         0      1.9G   0% /dev
tmpfs                     1.9G         0      1.9G   0% /sys/fs/cgroup
/dev/vda1                39.2G     20.9G     16.3G  56% /dev/termination-log
/dev/vda1                39.2G     20.9G     16.3G  56% /etc/resolv.conf
/dev/vda1                39.2G     20.9G     16.3G  56% /etc/hostname
/dev/vda1                39.2G     20.9G     16.3G  56% /etc/hosts
shm                      64.0M         0     64.0M   0% /dev/shm
/dev/rbd1               120.0M      1.5M    109.5M   1% /usr/share/busybox
tmpfs                     1.9G     12.0K      1.9G   0% /var/run/secrets/kubernetes.io/serviceaccount
tmpfs                     1.9G         0      1.9G   0% /proc/kcore
tmpfs                     1.9G         0      1.9G   0% /proc/timer_list
tmpfs                     1.9G         0      1.9G   0% /proc/timer_stats
tmpfs                     1.9G         0      1.9G   0% /proc/sched_debug
</code></pre>
<h4>六、简单测试</h4>
<p>这一节我们要对cephrbd作为k8s PV的效用做一个简单测试。测试步骤：</p>
<p>1) 在container中，向挂载的cephrbd写入数据；<br />
2) 删除ceph-pod1<br />
3) 重新创建ceph-pod1，查看数据是否还存在。</p>
<p>我们首先通过touch 、vi等命令向ceph-pod1挂载的cephrbd volume写入数据：我们通过容器f50bb8c31b0f 创建/usr/share/busybox/hello-ceph.txt，并向文件写入”hello ceph”一行字符串并保存。</p>
<pre><code># docker exec -it f50bb8c31b0f touch /usr/share/busybox/hello-ceph.txt
# docker exec -it f50bb8c31b0f vi /usr/share/busybox/hello-ceph.txt
# docker exec -it f50bb8c31b0f cat /usr/share/busybox/hello-ceph.txt
hello ceph
</code></pre>
<p>接下来删除ceph-pod1：</p>
<pre><code># kubectl get pod
NAME                        READY     STATUS    RESTARTS   AGE
ceph-pod1                   1/1       Running   0          4h

# kubectl delete pod/ceph-pod1
pod "ceph-pod1" deleted

# kubectl get pod
NAME                        READY     STATUS        RESTARTS   AGE
ceph-pod1                   1/1       Terminating   0          4h

# kubectl get pv,pvc
NAME         CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                REASON    AGE
pv/ceph-pv   1Gi        RWO           Recycle         Bound     default/ceph-claim             4h
NAME             STATUS    VOLUME    CAPACITY   ACCESSMODES   AGE
pvc/ceph-claim   Bound     ceph-pv   1Gi        RWO           4h

</code></pre>
<p>可以看到ceph-pod1的删除需要一段时间，这段时间pod一直处于“ Terminating”状态。同时，我们看到pod的删除并没有影响到pv和pvc object，它们依旧存在。</p>
<p>最后，我们再次来创建一下一个使用同一个pvc的pod，为了避免“不必要”的麻烦，我们建立一个名为ceph-pod2.yaml的描述文件：</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  name: ceph-pod2
spec:
  containers:
  - name: ceph-busybox2
    image: busybox
    command: ["sleep", "600000"]
    volumeMounts:
    - name: ceph-vol2
      mountPath: /usr/share/busybox
      readOnly: false
  volumes:
  - name: ceph-vol2
    persistentVolumeClaim:
      claimName: ceph-claim
</code></pre>
<p>创建ceph-pod2：</p>
<pre><code># kubectl create -f ceph-pod2.yaml
pod "ceph-pod2" created

root@node1:~/k8stest/k8s-cephrbd# kubectl get pod
NAME                        READY     STATUS    RESTARTS   AGE
ceph-pod2                   1/1       Running   0          14s

root@node1:~/k8stest/k8s-cephrbd# docker ps
CONTAINER ID        IMAGE                                                 COMMAND                  CREATED             STATUS              PORTS               NAMES
574b8069e548        busybox                                               "sleep 600000"           11 seconds ago      Up 10 seconds                           k8s_ceph-busybox2.c5e637a1_ceph-pod2_default_f4aeebd6-a4c3-11e6-ba01-00163e1625a9_fc94c0fe
</code></pre>
<p>查看数据是否依旧存在：</p>
<pre><code># docker exec -it 574b8069e548 cat /usr/share/busybox/hello-ceph.txt
hello ceph
</code></pre>
<p>数据完好无损的被ceph-pod2读取到了！</p>
<h4>七、小结</h4>
<p>至此，对k8s与ceph的集成仅仅才是一个开端，更多的feature和坑等待挖掘。近期发现文章越写越长，原因么？自己赶脚是因为目标系统越来越大，越来越复杂。深入K8s的过程，就是继续给自己挖坑的过程^_^。</p>
<p>我，不是在填坑的路上，就是在坑里:)。</p>
<p>BTW，列一下参考资料：<br />
1、<a href="http://docs.ceph.com">Ceph官方文档</a>；<br />
2、<a href="https://docs.openshift.com/enterprise/3.1/install_config/storage_examples/ceph_example.html">OpenShift中的K8s与Ceph RBD集成的文档</a>;<br />
3、<a href="http://kubernetes.io/docs/user-guide/persistent-volumes/">Kubernetes官方文档Persistent volumes部分</a>；<br />
4、zphj1987博主的<a href="http://www.zphj1987.com/2016/06/07/rbd%E6%97%A0%E6%B3%95map(rbd-feature-disable)/">这篇文章</a>。</p>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/feed/</wfw:commentRss>
		<slash:comments>29</slash:comments>
		</item>
		<item>
		<title>VirtualBox虚拟机下Windows登录密码破解方法</title>
		<link>https://tonybai.com/2014/10/29/crack-windows-logon-password-under-virtualbox/</link>
		<comments>https://tonybai.com/2014/10/29/crack-windows-logon-password-under-virtualbox/#comments</comments>
		<pubDate>Wed, 29 Oct 2014 09:28:52 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CDLinux]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[Crack]]></category>
		<category><![CDATA[livecd]]></category>
		<category><![CDATA[MacAir]]></category>
		<category><![CDATA[TinyCoreLinux]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[virtualbox]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[Win7]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[破解]]></category>
		<category><![CDATA[虚拟机]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1589</guid>
		<description><![CDATA[近两年虚拟机的发展给开发人员带来了极大便利，安装一个新环境，只需从别人那里copy一份虚拟机文件即可，分分钟搞定。我之前一直在Ubuntu下工 作，Windows偶尔使用，于是在Ubuntu VirtualBox下安装了一个Windows 7。今年将工作环境迁移到Mac Air下了，但偶尔也有Windows的使用需求，于是直接从我原来的Ubuntu下将Win7的Vdi文件Copy到Air上，便直接可以使用Win7 了，省去了重新安装Win7以及庞大的Office组件的工作。 前两天，打开Mac Air下的VirtualBox，启动Win7虚拟机，在Win7登录界面输入密码后，系统提示我密码错误。反复输入多次，将我常用的密码都试了一遍依旧 无法进入。我只能在原Ubuntu下临时用用Win7。但毕竟在Air上没有Win7十分不便，一些Word, PPT文档需要在两天机器上传来传去。无奈下，我都由了重新在Air下安装一个Win7甚至是Win8的打算了。 今天又有一个PPT编写的task，这件事再次被提上日程。我换了下思维：能不能破解一下Win7登录密码呢？于是求助度娘（谷哥离去好久了）。还别说， 还真是有破解方法，多数是通过PE工具盘快速修改登录密码。但PE工具盘挺大（几百兆，公司下载不便），我的又是虚拟机环境，这种方法不是我的菜啊。于是 又看到另外一种思路：通过某个Linux livecd或安装盘引导，mount windows分区，将C:\Windows\System32\cmd.exe改名为osk.exe。osk.exe是虚拟键盘程序。在Win7登录页 面的左下角可以启动这个虚拟键盘程序。一旦我替换成功，启动虚拟键盘程序就变成了启动Win7命令行程序。有了命令行，我们就可以通过net user命令查看当前账户列表、重置某个用户的password了。思路很清晰，是我的菜。 在我的Ubuntu机器上倒是有几个Linux发行版的live cd iso文件，比如ubuntu 14.04.1 desktop, centos7 desktop，不过个头都太大了，传到我的Air上还是很费劲的。我想最好有一个tiny的linux发行版。度娘告诉我有很多选择。我首先选了Tiny Core Linux， TinyCore-5.4.iso才不到14M。于是打开Win7虚拟机的&#8220;设置&#8221;页面，将 TinyCore-5.4.iso作为虚拟iso&#8220;插入&#8221;IDE光驱。TinyCore的启动就是秒秒的事情。TinyCore的桌面风格模仿Mac OS，桌面下方放置了一个dock条。TinyCore自带mount tools，打开后，用鼠标点击sda2，sda2盘符由红变绿，说明mount成功。 打开TinyCore的Terminal程序，进入/mnt/sda2，本想安装方案修改cmd.exe的名字，但Tiny Core提示：这是个Read-Only Filesystem。显然这是个只读mount。于是各种尝试读写挂在（包括修改/etc/fstab、mount -a, mount -o remount等），都无法改变Read-Only Filesystem的事实，于是放弃。 换国人的发行版：CDLinux，这个发行版似乎已经不再更新，最新版本 CDlinux_mini-0.9.7.1.iso，发布时间是2012年3月18日。CDLinux的Size比TinyLinux稍大些，36M。 CDLinux启动略慢，并且只是Console Only（标准版带有桌面环境），没有图形桌面。进入命令行后，执行一下mount命令，发现/dev/sda2居然是rw方式挂载载/media /xxx下的，于是进入该目录，尝试touch test.txt，完全没有问题。 于是按照方案说明，将C:\Windows\System32下的osk.exe备份一下，将cmd.exe改名为osk.exe。 将光盘盘片删除，启动Win7，进入登陆页面时，点击左下角&#8220;轻松访问&#8221;按钮，选择&#8220;不使用键盘键入&#8221;，确定。命令行窗口弹出。 在命令行窗口执行net user 查看用户名列表。我的用户名是tonybai，再通过net user tonybai newpassword重置tonybai的密码。执行成功后，用新密码登陆，顺利进入Win7 Desktop。Crack成功！ &#169; 2014, [...]]]></description>
			<content:encoded><![CDATA[<p>近两年虚拟机的发展给开发人员带来了极大便利，安装一个新环境，只需从别人那里copy一份虚拟机文件即可，分分钟搞定。我之前一直在<a href="http://tonybai.com/tag/Ubuntu/">Ubuntu</a>下工 作，Windows偶尔使用，于是在Ubuntu<a href="http://virtualbox.org/"> VirtualBox</a>下安装了一个Windows 7。今年将工作环境迁移到Mac Air下了，但偶尔也有Windows的使用需求，于是直接从我原来的<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Ubuntu</a>下将Win7的Vdi文件Copy到Air上，便直接可以使用Win7 了，省去了重新安装Win7以及庞大的Office组件的工作。</p>
<p>前两天，打开Mac Air下的VirtualBox，启动Win7虚拟机，在Win7登录界面输入密码后，系统提示我密码错误。反复输入多次，将我常用的密码都试了一遍依旧 无法进入。我只能在原Ubuntu下临时用用Win7。但毕竟在Air上没有Win7十分不便，一些Word, PPT文档需要在两天机器上传来传去。无奈下，我都由了重新在Air下安装一个Win7甚至是Win8的打算了。</p>
<p>今天又有一个PPT编写的task，这件事再次被提上日程。我换了下思维：能不能破解一下Win7登录密码呢？于是求助度娘（谷哥离去好久了）。还别说， 还真是有破解方法，多数是通过PE工具盘快速修改登录密码。但PE工具盘挺大（几百兆，公司下载不便），我的又是虚拟机环境，这种方法不是我的菜啊。于是 又看到另外一种思路：通过某个Linux livecd或安装盘引导，mount windows分区，将C:\Windows\System32\cmd.exe改名为osk.exe。osk.exe是虚拟键盘程序。在Win7登录页 面的左下角可以启动这个虚拟键盘程序。一旦我替换成功，启动虚拟键盘程序就变成了启动Win7命令行程序。有了命令行，我们就可以通过net user命令查看当前账户列表、重置某个用户的password了。思路很清晰，是我的菜。</p>
<p>在我的Ubuntu机器上倒是有几个Linux发行版的live cd iso文件，比如ubuntu 14.04.1 desktop, centos7 desktop，不过个头都太大了，传到我的Air上还是很费劲的。我想最好有一个tiny的linux发行版。度娘告诉我有很多选择。我首先选了<a href="http://distro.ibiblio.org/tinycorelinux/">Tiny Core Linux</a>， TinyCore-5.4.iso才不到14M。于是打开Win7虚拟机的&ldquo;设置&rdquo;页面，将 TinyCore-5.4.iso作为虚拟iso&ldquo;插入&rdquo;IDE光驱。TinyCore的启动就是秒秒的事情。TinyCore的桌面风格模仿Mac OS，桌面下方放置了一个dock条。TinyCore自带mount tools，打开后，用鼠标点击sda2，sda2盘符由红变绿，说明mount成功。</p>
<p>打开TinyCore的Terminal程序，进入/mnt/sda2，本想安装方案修改cmd.exe的名字，但Tiny Core提示：这是个Read-Only Filesystem。显然这是个只读mount。于是各种尝试读写挂在（包括修改/etc/fstab、mount -a, mount -o remount等），都无法改变Read-Only Filesystem的事实，于是放弃。</p>
<p>换国人的发行版：<a href="http://cdlinux.info/wiki/">CDLinux</a>，这个发行版似乎已经不再更新，最新版本 CDlinux_mini-0.9.7.1.iso，发布时间是2012年3月18日。CDLinux的Size比TinyLinux稍大些，36M。 CDLinux启动略慢，并且只是Console Only（标准版带有桌面环境），没有图形桌面。进入命令行后，执行一下mount命令，发现/dev/sda2居然是rw方式挂载载/media /xxx下的，于是进入该目录，尝试touch test.txt，完全没有问题。</p>
<p>于是按照方案说明，将C:\Windows\System32下的osk.exe备份一下，将cmd.exe改名为osk.exe。</p>
<p>将光盘盘片删除，启动Win7，进入登陆页面时，点击左下角&ldquo;轻松访问&rdquo;按钮，选择&ldquo;不使用键盘键入&rdquo;，确定。命令行窗口弹出。</p>
<p>在命令行窗口执行net user 查看用户名列表。我的用户名是tonybai，再通过net user tonybai newpassword重置tonybai的密码。执行成功后，用新密码登陆，顺利进入Win7 Desktop。Crack成功！</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/29/crack-windows-logon-password-under-virtualbox/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>探讨Docker容器中修改系统变量的方法</title>
		<link>https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/</link>
		<comments>https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/#comments</comments>
		<pubDate>Tue, 14 Oct 2014 13:56:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[phusion]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[SharedMemory]]></category>
		<category><![CDATA[shmget]]></category>
		<category><![CDATA[Ubuntu]]></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=1563</guid>
		<description><![CDATA[探讨完Docker对共享内存状态持久化的支持状况后，我将遗留产品build到一个pre-production image中，测试启动是否OK。很显然，我过于乐观了，Docker之路并不平坦。我收到了shmget报出的EINVAL错误码，提示参数非法。 shmget的manual对EINVAL错误码的说明如下： EINVAL： A&#160; new&#160; segment&#160; was&#160; to&#160; be&#160; created&#160; and size &#60; SHMMIN or size &#62; SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment. 显然我们要创建的shared memory的size很可能大于SHMMAX这个系统变量了。那么一个从base image创建出的容器中的系统变量到底是什么值呢？我们来查看一下，我们基于&#34;centos:centos6&#34;启动一个Docker容器，并检查其中的 系统变量值设置： $ sudo docker run -it &#34;centos:centos6&#34; /bin/bash bash-4.1# [...]]]></description>
			<content:encoded><![CDATA[<p>探讨完<a href="http://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/">Docker对共享内存状态持久化的支持状况</a>后，我将遗留产品build到一个pre-production image中，测试启动是否OK。很显然，我过于乐观了，<a href="http://www.docker.com">Docker</a>之路并不平坦。我收到了shmget报出的EINVAL错误码，提示参数非法。 shmget的manual对EINVAL错误码的说明如下：</p>
<p><font face="Courier New">EINVAL：<br />
	A&nbsp; new&nbsp; segment&nbsp; was&nbsp; to&nbsp; be&nbsp; created&nbsp; and size &lt; SHMMIN or size &gt; SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment.</font></p>
<p>显然我们要创建的shared memory的size很可能大于SHMMAX这个系统变量了。那么一个从base image创建出的容器中的系统变量到底是什么值呢？我们来查看一下，我们基于&quot;centos:centos6&quot;启动一个Docker容器，并检查其中的 系统变量值设置：</p>
<p><font face="Courier New">$ sudo docker run -it &quot;centos:centos6&quot; /bin/bash<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	33554432<br />
	bash-4.1# sysctl -a|grep shmmax<br />
	kernel.shmmax = 33554432</font></p>
<p>可以看出默认情况下，当前容器中root账号看到的shmmax值我<font face="Courier New">33554432</font>， 我的程序要创建的shm size的确要大于这个值，报出EINVAL错误也就无可厚非了。我尝试按照物理机上的方法临时修改一下该值：</p>
<p><font face="Courier New">bash-4.1# echo 68719476736 &gt; /proc/sys/kernel/shmmax<br />
	bash: /proc/sys/kernel/shmmax: Read-only file system</font></p>
<p><font face="Courier New">/proc/sys/kernel/shmmax居然是只读的，无法修改。</font></p>
<p><font face="Courier New">我又尝试修改/etc/sysctl.conf这个持久化系统变量的地方，但打开/etc/sysctl.conf文件，我发现我又错了，这 个文件中shmmax的值如下：</font></p>
<p><font face="Courier New"># Controls the maximum shared segment size, in bytes<br />
	kernel.shmmax = 68719476736</font></p>
<p><font face="Courier New"><font face="Courier New">/etc/sysctl.conf文件 中的系统变量shmmax的值是68719476736，而系统当前的实际值则是33554432，难道是/etc /sysctl.conf中的值没有生效，于是我手工重新加载一次该文件：</font></font></p>
<p><font face="Courier New"><font face="Courier New">-bash-4.1# sysctl -p<br />
	error: &quot;Read-only file system&quot; setting key &quot;net.ipv4.ip_forward&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;net.ipv4.conf.default.rp_filter&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;net.ipv4.conf.default.accept_source_route&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.sysrq&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.core_uses_pid&quot;<br />
	error: &quot;net.ipv4.tcp_syncookies&quot; is an unknown key<br />
	error: &quot;net.bridge.bridge-nf-call-ip6tables&quot; is an unknown key<br />
	error: &quot;net.bridge.bridge-nf-call-iptables&quot; is an unknown key<br />
	error: &quot;net.bridge.bridge-nf-call-arptables&quot; is an unknown key<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.msgmnb&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.msgmax&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.shmmax&quot;<br />
	error: &quot;Read-only file system&quot; setting key &quot;kernel.shmall&quot;</font></font></p>
<p><font face="Courier New"><font face="Courier New">我得到了和之前类似的错误结果：只读文件系统，无法修改。于是乎两个问题萦绕在我的面前：<br />
	1、为什么容器内当前系统变量值与sysctl.conf中的不一致？<br />
	2、为什么无法修改当前系统变量值?</font></font></p>
<p><font face="Courier New"><font face="Courier New">在翻阅了Stackoverflow, github docker issues后，我得到了的答案如下：</font></font></p>
<p><font face="Courier New"><font face="Courier New">1、Docker的base image做的很精简，甚至都没有init进程，原本在OS启动时执行生效系统变量的过程(sysctl -p)也给省略了，导致这些系统变量依旧保留着kernel默认值。以CentOs为例，在linux kernel boot后，init都会执行/etc/rc.d/rc.sysinit，后者会加载/etc/sysctl.conf中的系统变量值。下面是 CentOs5.6中的rc.sysinit代码摘录：</font></font></p>
<p><font face="Courier New"><font face="Courier New">&#8230; &#8230;<br />
	# Configure kernel parameters<br />
	update_boot_stage RCkernelparam<br />
	sysctl -e -p /etc/sysctl.conf &gt;/dev/null 2&gt;&amp;1<br />
	&#8230; &#8230;</font></font></p>
<p><font face="Courier New"><font face="Courier New">2、Docker容器中的系统变量在non-priviledged模式下目前(我使用的时docker 1.2.0版本)就无法修改，</font></font>这 和resolv.conf、hosts等文件映射到宿主机对应的文件有不同。</p>
<p><font face="Courier New">$ mount -l<br />
	&#8230;. &#8230;.<br />
	/dev/mapper/ubuntu&#8211;Server&#8211;14&#8211;vg-root on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)<br />
	/dev/mapper/ubuntu&#8211;Server&#8211;14&#8211;vg-root on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)<br />
	/dev/mapper/ubuntu&#8211;Server&#8211;14&#8211;vg-root on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)<br />
	&#8230; &#8230;</font></p>
<p>那么我们该如何修改系统变量值来满足遗留产品的需求呢？</p>
<p><b>一、使用&#8211;privileged选项</b></p>
<p>我们使用&#8211;privileged这个特权选项来启动一个基于centos:centos6的新容器，看看是否能对shmmax这样的系统变量值 进行修改：</p>
<p><font face="Courier New">$ sudo docker run -it &#8211;privileged&nbsp; &quot;centos:centos6&quot; /bin/bash<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	33554432<br />
	bash-4.1# echo 68719476736 &gt; /proc/sys/kernel/shmmax<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	68719476736</font><br />
	<font face="Courier New">bash-4.1# sysctl -p<br />
	net.ipv4.ip_forward = 0<br />
	net.ipv4.conf.default.rp_filter = 1<br />
	net.ipv4.conf.default.accept_source_route = 0<br />
	kernel.sysrq = 0<br />
	kernel.core_uses_pid = 1<br />
	&#8230; &#8230;<br />
	kernel.msgmnb = 65536<br />
	kernel.msgmax = 65536<br />
	kernel.shmmax = 68719476736<br />
	kernel.shmall = 4294967296</font></p>
<p>可以看出，通过&#8211;privileged选项，容器获得了额外的特权，并且可以对系统变量的值进行修改了。不过这样的修改是不能保存在容器里的， 我们stop 容器，再重启该容器就能看出来：</p>
<p><font face="Courier New">$ sudo docker start 3e22d65a7845<br />
	$ sudo docker attach 3e22d65a7845<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	33554432</font></p>
<p>shmmax的值在容器重启后又变回了原先的那个默认值。不过重启后的容器依旧具有privileged的特权，我们还可以重新手工执行命令对系 统变量进行修改：</p>
<p><font face="Courier New">bash-4.1# echo 68719476736 &gt; /proc/sys/kernel/shmmax<br />
	bash-4.1# cat /proc/sys/kernel/shmmax<br />
	68719476736</font></p>
<p>但即便这样，也无法满足我们的需求，我们总不能每次都在容器中手工执行系统变量值修改的操作吧。privileged选项的能力能否带到 image中呢？答案是目前还不能，我们无法在build image时通过privileged选项修改系统变量值。</p>
<p>这样一来，我们能做的只有把产品启动与系统变量值修改放在一个脚本中了，并将该脚本作为docker 容器的cmd命令来执行，比如我们构建一个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 />
	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 ./start.sh /bin/start.sh<br />
	RUN chmod +x /bin/start.sh<br />
	CMD ["/bin/start.sh]</font></p>
<p><font face="Courier New">//start.sh<br />
	sysctl -p<br />
	/usr/bin/supervisord</font></p>
<p>这样，start.sh在supervisord启动前将系统变量值重新加载，而supervisord后续启动的程序就可以看到这些新系统变量 的值了。不过别忘了利用这个image启动容器时要加上&#8211;priviledged选项，否则容器启动就会失败。</p>
<p><b>二、使用phusion/baseimage</b></p>
<p>前面说过/etc/sysctl.conf中的值没有生效是因为docker官方提供的centos:centos6把init进程的初始化过程给精 简掉了。<a href="https://registry.hub.docker.com/u/phusion/baseimage/">phusion/baseimage</a>是目前docker registery上仅次于ubuntu和centos两个之后的base image，其提供了/sbin/my_init这个init进程，用于在container充当init进程的角色。那么my_init是否可以用于执行sysctl -p呢？我们试验一下：</p>
<p>我们先pull这个base image下来：<font face="Courier New">sudo docker pull phusion/baseimage。pull成功后，我们先基于&ldquo;phusion/baseimage&rdquo;启动一个容器做一些explore工作：</font></p>
<p><font face="Courier New">$ sudo docker run -i -t &quot;phusion/baseimage&quot;<br />
	*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh&#8230;<br />
	No SSH host key available. Generating one&#8230;<br />
	Creating SSH2 RSA key; this may take some time &#8230;<br />
	Creating SSH2 DSA key; this may take some time &#8230;<br />
	Creating SSH2 ECDSA key; this may take some time &#8230;<br />
	Creating SSH2 ED25519 key; this may take some time &#8230;<br />
	invoke-rc.d: policy-rc.d denied execution of restart.<br />
	*** Running /etc/rc.local&#8230;<br />
	*** Booting runit daemon&#8230;<br />
	*** Runit started as PID 100</font></p>
<p><font face="Courier New">通过nsenter进去，查看一下/sbin/my_init的源码，我们发现这是一个python脚本，不过从头到尾浏览一遍，没有发现sysctl加载/etc/sysctl.conf系统变量的操作。</font></p>
<p><font face="Courier New">不过，phusion文档中说my_init可以在初始化过程中执行/etc/my_init.d下的脚本。那是不是我们将一个执行sysctl -p的脚本放入/etc/my_init.d下就可以实现我们的目的了呢？试试。</font></p>
<p><font face="Courier New">我们编写一个脚本：load_sys_varibles.sh</font></p>
<p><font face="Courier New">#!/bin/sh<br />
	sysctl -p &gt; init.txt</font></p>
<p><font face="Courier New">下面是制作image的Dockerfile:</font></p>
<p><font face="Courier New">FROM phusion/baseimage:latest<br />
	MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	RUN echo &quot;kernel.shmmax = 68719476736&quot; &gt;&gt; /etc/sysctl.conf<br />
	RUN mkdir -p /etc/my_init.d<br />
	ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh<br />
	RUN chmod +x /etc/my_init.d/load_sys_varibles.sh<br />
	CMD ["/sbin/my_init"]</font></p>
<p><font face="Courier New">phusion/baseimage是基于ubuntu的OS，其sysctl.conf默认情况下没啥内容，所以我们在Dockerfile中向这个文件写入我们需要的系统变量值。构建image并启动容器：</font></p>
<p><font face="Courier New">$ sudo docker build -t &quot;myphusion:v1&quot; ./<br />
	Sending build context to Docker daemon 13.12 MB<br />
	Sending build context to Docker daemon<br />
	Step 0 : FROM phusion/baseimage:latest<br />
	&nbsp;&#8212;&gt; cf39b476aeec<br />
	Step 1 : MAINTAINER Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; d0e9b51a3e4f<br />
	Step 2 : RUN echo &quot;kernel.shmmax = 68719476736&quot; &gt;&gt; /etc/sysctl.conf<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; 2c800687cc83<br />
	Step 3 : RUN mkdir -p /etc/my_init.d<br />
	&nbsp;&#8212;&gt; Using cache<br />
	&nbsp;&#8212;&gt; fe366eea5eb4<br />
	Step 4 : ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh<br />
	&nbsp;&#8212;&gt; a641bb595fb9<br />
	Removing intermediate container c381b9f001c2<br />
	Step 5 : RUN chmod +x /etc/my_init.d/load_sys_varibles.sh<br />
	&nbsp;&#8212;&gt; Running in 764866552f25<br />
	&nbsp;&#8212;&gt; eae3d7f1eac5<br />
	Removing intermediate container 764866552f25<br />
	Step 6 : CMD ["/sbin/my_init"]<br />
	&nbsp;&#8212;&gt; Running in 9ab8d0b717a7<br />
	&nbsp;&#8212;&gt; 8be4e7b6b174<br />
	Removing intermediate container 9ab8d0b717a7<br />
	Successfully built 8be4e7b6b174</font></p>
<p><font face="Courier New">$ sudo docker run -it &quot;myphusion:v1&quot;<br />
	*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh&#8230;<br />
	No SSH host key available. Generating one&#8230;<br />
	Creating SSH2 RSA key; this may take some time &#8230;<br />
	Creating SSH2 DSA key; this may take some time &#8230;<br />
	Creating SSH2 ECDSA key; this may take some time &#8230;<br />
	Creating SSH2 ED25519 key; this may take some time &#8230;<br />
	invoke-rc.d: policy-rc.d denied execution of restart.<br />
	*** Running /etc/my_init.d/load_sys_varibles.sh&#8230;<br />
	sysctl: setting key &quot;kernel.shmmax&quot;: Read-only file system<br />
	*** /etc/my_init.d/load_sys_varibles.sh failed with status 255</font></p>
<p><font face="Courier New">*** Killing all processes&#8230;</font></p>
<p><font face="Courier New">唉，还是老问题！即便是在my_init中执行，依旧无法逾越Read-only file system，查看Phusion/baseimage的Dockerfile才知道，它也是From ubuntu:14.04的，根不变，上层再怎么折腾也没用。</font></p>
<p><font face="Courier New">换一种容器run方法吧，加上&#8211;privileged：</font></p>
<p><font face="Courier New">$ sudo docker run -it &#8211;privileged&nbsp; &quot;myphusion:v1&quot;<br />
	*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh&#8230;<br />
	No SSH host key available. Generating one&#8230;<br />
	Creating SSH2 RSA key; this may take some time &#8230;<br />
	Creating SSH2 DSA key; this may take some time &#8230;<br />
	Creating SSH2 ECDSA key; this may take some time &#8230;<br />
	Creating SSH2 ED25519 key; this may take some time &#8230;<br />
	invoke-rc.d: policy-rc.d denied execution of restart.<br />
	*** Running /etc/my_init.d/load_sys_varibles.sh&#8230;<br />
	*** Running /etc/rc.local&#8230;<br />
	*** Booting runit daemon&#8230;<br />
	*** Runit started as PID 102</font></p>
<p><font face="Courier New">这回灵光了。enter到容器里看看设置的值是否生效了：</font></p>
<p><font face="Courier New">root@9e399f46372a:~#cat /proc/sys/kernel/shmmax<br />
	68719476736</font></p>
<p><font face="Courier New">结果如预期。这样来看phusion/baseimage算是为sysctl -p加载系统变量值提供了一个便利，但依旧无法脱离&#8211;privileged，且依旧无法在image中持久化这个设置。</font></p>
<p><font face="Courier New">在Docker github的issue中有人提出建议在Dockerfile中加入类似RUNP这样的带有特权的指令语法，但不知何时才能在Docker中加入这一功能。</font></p>
<p><font face="Courier New">总而言之，基于目前docker官网提供的base image，我们很难找到特别理想的修改系统变量值的方法，除非自己制作base image，这个还没尝试过，待后续继续研究。</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/14/discussion-on-the-approach-to-modify-system-variables-in-docker/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
	</channel>
</rss>
