<?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; image</title>
	<atom:link href="http://tonybai.com/tag/image/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>依赖Kafka的Go单元测试例解</title>
		<link>https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka/</link>
		<comments>https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka/#comments</comments>
		<pubDate>Mon, 08 Jan 2024 13:42:41 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bitnami]]></category>
		<category><![CDATA[confluent]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-compose]]></category>
		<category><![CDATA[event]]></category>
		<category><![CDATA[fake-object]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Kafka]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[Scala]]></category>
		<category><![CDATA[segmentio]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[SUT]]></category>
		<category><![CDATA[testcontainers]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[Topic]]></category>
		<category><![CDATA[wasi]]></category>
		<category><![CDATA[单元测试]]></category>
		<category><![CDATA[容器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4107</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka Kafka是Apache基金会开源的一个分布式事件流处理平台，是Java阵营(最初为Scala)中的一款杀手级应用，其提供的高可靠性、高吞吐量和低延迟的数据传输能力，让其到目前为止依旧是现代企业级应用系统以及云原生应用系统中使用的重要中间件。 在日常开发Go程序时，我们经常会遇到一些依赖Kafka的代码，如何对这些代码进行测试，尤其是单测是摆在Go开发者前面的一个现实问题！ 有人说用mock，是个路子。但看过我的《单测时尽量用fake object》一文的童鞋估计已经走在了寻找kafka fake object的路上了！Kafka虽好，但身形硕大，不那么灵巧。找到一个合适的fake object不容易。在这篇文章中，我们就来聊聊如何测试那些依赖kafka的代码，再往本质一点说，就是和大家以找找那些合适的kafka fake object。 1. 寻找fake object的策略 在《单测时尽量用fake object》一文中，我们提到过，如果测试的依赖提供了tiny版本或某些简化版，我们可以直接使用这些版本作为fake object的候选，就像etcd提供了用于测试的自身简化版的实现(embed)那样。 但Kafka并没有提供tiny版本，我们也只能选择《单测时尽量用fake object》一文提到的另外一个策略，那就是利用容器来充当fake object，这是目前能搞到任意依赖的fake object的最简单路径了。也许以后WASI(WebAssembly System Interface)成熟了，让wasm脱离浏览器并可以在本地系统上飞起，到时候换用wasm也不迟。 下面我们就按照使用容器的策略来找一找适合的kafka container。 2. testcontainers-go 我们第一站就来到了testcontainers-go。testcontainers-go是一个Go语言开源项目，专门用于简化创建和清理基于容器的依赖项，常用于Go项目的单元测试、自动化集成或冒烟测试中。通过testcontainers-go提供的易于使用的API，开发人员能够以编程方式定义作为测试的一部分而运行的容器，并在测试完成时清理这些资源。 注：testcontainers不仅提供Go API，它还覆盖了主流的编程语言，包括：Java、.NET、Python、Node.js、Rust等。 在几个月之前，testcontainers-go项目还没有提供对Kafka的直接支持，我们需要自己使用testcontainers.GenericContainer来自定义并启动kafka容器。2023年9月，以KRaft模式运行的Kafka容器才被首次引入testcontainers-go项目。 目前testcontainers-go使用的kafka镜像版本是confluentinc/confluent-local:7.5.0。Confluent是在kafka背后的那家公司，基于kafka提供商业化支持。今年初，Confluent还收购了Immerok，将apache的另外一个明星项目Flink招致麾下。 confluent-local并不是一个流行的kafka镜像，它只是一个使用KRaft模式的零配置的、包含Confluent Community RestProxy的Apache Kafka，并且镜像是实验性的，仅应用于本地开发工作流，不应该用在支持生产工作负载。 生产中最常用的开源kafka镜像是confluentinc/cp-kafka镜像，它是基于开源Kafka项目构建的，但在此基础上添加了一些额外的功能和工具，以提供更丰富的功能和更易于部署和管理的体验。cp-kafka镜像的版本号并非kafka的版本号，其对应关系需要cp-kafka镜像官网查询。 另外一个开发领域常用的kafka镜像是bitnami的kafka镜像。Bitnami是一个提供各种开源软件的预打包镜像和应用程序栈的公司。Bitnami Kafka镜像是基于开源Kafka项目构建的，是一个可用于快速部署和运行Kafka的Docker镜像。Bitnami Kafka镜像与其内部的Kakfa的版本号保持一致。 下面我们就来看看如何使用testcontainers-go的kafka来作为依赖kafka的Go单元测试用例的fake object。 这第一个测试示例改编自testcontainers-go/kafka module的example_test.go： // testcontainers/kafka_setup/kafka_test.go package main import ( "context" "fmt" "testing" "github.com/testcontainers/testcontainers-go/modules/kafka" [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-unit-testing-deps-on-kafka-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka">本文永久链接</a> &#8211; https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka</p>
<p><a href="https://kafka.apache.org">Kafka</a>是Apache基金会开源的一个分布式事件流处理平台，是Java阵营(最初为Scala)中的一款杀手级应用，其提供的高可靠性、高吞吐量和低延迟的数据传输能力，让其到目前为止依旧是现代企业级应用系统以及云原生应用系统中使用的重要中间件。</p>
<p>在日常开发Go程序时，我们经常会遇到一些<a href="https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration/">依赖Kafka的代码</a>，如何对这些代码进行测试，尤其是单测是摆在Go开发者前面的一个现实问题！</p>
<p>有人说用mock，是个路子。但看过我的《<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/">单测时尽量用fake object</a>》一文的童鞋估计已经走在了寻找kafka fake object的路上了！Kafka虽好，但身形硕大，不那么灵巧。找到一个合适的fake object不容易。在这篇文章中，我们就来聊聊如何测试那些依赖kafka的代码，再往本质一点说，就是和大家以找找那些合适的kafka fake object。</p>
<h2>1. 寻找fake object的策略</h2>
<p>在《<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/">单测时尽量用fake object</a>》一文中，我们提到过，如果测试的依赖提供了tiny版本或某些简化版，我们可以直接使用这些版本作为fake object的候选，就像etcd提供了<a href="https://github.com/etcd-io/etcd/blob/main/tests/integration/embed">用于测试的自身简化版的实现(embed)</a>那样。</p>
<p>但Kafka并没有提供tiny版本，我们也只能选择《<a href="https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/">单测时尽量用fake object</a>》一文提到的另外一个策略，那就是<strong>利用容器来充当fake object</strong>，这是目前能搞到任意依赖的fake object的最简单路径了。也许以后<a href="https://wasi.dev/">WASI(WebAssembly System Interface)</a>成熟了，让wasm脱离浏览器并可以在本地系统上飞起，到时候换用wasm也不迟。</p>
<p>下面我们就按照使用容器的策略来找一找适合的kafka container。</p>
<h2>2. testcontainers-go</h2>
<p>我们第一站就来到了<a href="https://golang.testcontainers.org/">testcontainers-go</a>。testcontainers-go是一个Go语言开源项目，专门用于简化创建和清理基于容器的依赖项，常用于Go项目的单元测试、自动化集成或冒烟测试中。通过testcontainers-go提供的易于使用的API，开发人员能够以编程方式定义作为测试的一部分而运行的容器，并在测试完成时清理这些资源。</p>
<blockquote>
<p>注：<a href="https://testcontainers.com">testcontainers</a>不仅提供Go API，它还覆盖了主流的编程语言，包括：Java、.NET、Python、Node.js、<a href="https://tonybai.com/2023/02/22/rust-vs-go-in-2023/">Rust</a>等。</p>
</blockquote>
<p>在几个月之前，<a href="https://github.com/testcontainers/testcontainers-go/">testcontainers-go</a>项目还没有提供对Kafka的直接支持，我们需要自己使用testcontainers.GenericContainer来自定义并启动kafka容器。2023年9月，<a href="https://github.com/testcontainers/testcontainers-go/pull/1610">以KRaft模式运行的Kafka容器才被首次引入testcontainers-go项目</a>。</p>
<p>目前testcontainers-go使用的kafka镜像版本是<a href="https://hub.docker.com/r/confluentinc/confluent-local">confluentinc/confluent-local:7.5.0</a>。<a href="https://www.confluent.io">Confluent</a>是在kafka背后的那家公司，基于kafka提供商业化支持。今年初，Confluent还收购了Immerok，将apache的另外一个明星项目Flink招致麾下。</p>
<p><a href="https://hub.docker.com/r/confluentinc/confluent-local">confluent-local</a>并不是一个流行的kafka镜像，它只是一个使用KRaft模式的零配置的、包含Confluent Community RestProxy的Apache Kafka，并且镜像是实验性的，仅应用于本地开发工作流，不应该用在支持生产工作负载。</p>
<p>生产中最常用的开源kafka镜像是<a href="https://hub.docker.com/r/confluentinc/cp-kafka">confluentinc/cp-kafka镜像</a>，它是基于开源Kafka项目构建的，但在此基础上添加了一些额外的功能和工具，以提供更丰富的功能和更易于部署和管理的体验。cp-kafka镜像的版本号并非kafka的版本号，其对应关系需要cp-kafka镜像官网查询。</p>
<p>另外一个开发领域常用的kafka镜像是bitnami的kafka镜像。Bitnami是一个提供各种开源软件的预打包镜像和应用程序栈的公司。Bitnami Kafka镜像是基于开源Kafka项目构建的，是一个可用于快速部署和运行Kafka的Docker镜像。Bitnami Kafka镜像与其内部的Kakfa的版本号保持一致。</p>
<p>下面我们就来看看如何使用testcontainers-go的kafka来作为依赖kafka的Go单元测试用例的fake object。</p>
<p>这第一个测试示例改编自testcontainers-go/kafka module的example_test.go：</p>
<pre><code>// testcontainers/kafka_setup/kafka_test.go

package main

import (
    "context"
    "fmt"
    "testing"

    "github.com/testcontainers/testcontainers-go/modules/kafka"
)

func TestKafkaSetup(t *testing.T) {
    ctx := context.Background()

    kafkaContainer, err := kafka.RunContainer(ctx, kafka.WithClusterID("test-cluster"))
    if err != nil {
        panic(err)
    }

    // Clean up the container
    defer func() {
        if err := kafkaContainer.Terminate(ctx); err != nil {
            panic(err)
        }
    }()

    state, err := kafkaContainer.State(ctx)
    if err != nil {
        panic(err)
    }

    if kafkaContainer.ClusterID != "test-cluster" {
        t.Errorf("want test-cluster, actual %s", kafkaContainer.ClusterID)
    }
    if state.Running != true {
        t.Errorf("want true, actual %t", state.Running)
    }
    brokers, _ := kafkaContainer.Brokers(ctx)
    fmt.Printf("%q\n", brokers)
}
</code></pre>
<p>在这个例子中，我们直接调用kafka.RunContainer创建了一个名为test-cluster的kafka实例，如果没有通过WithImage向RunContainer传入自定义镜像，那么默认我们将启动一个confluentinc/confluent-local:7.5.0的容器（注意：随着时间变化，该默认容器镜像的版本也会随之改变）。</p>
<p>通过RunContainer返回的kafka.KafkaContainer我们可以获取到关于kafka容器的各种信息，比如上述代码中的ClusterID、kafka Broker地址信息等。有了这些信息，我们后续便可以与以容器形式启动的kafka建立连接并做数据的写入和读取操作了。</p>
<p>我们先来看这个测试的运行结果，与预期一致：</p>
<pre><code>$ go test
2023/12/16 21:45:52 github.com/testcontainers/testcontainers-go - Connected to docker:
  ... ...
  Resolved Docker Host: unix:///var/run/docker.sock
  Resolved Docker Socket Path: /var/run/docker.sock
  Test SessionID: 19e47867b733f4da4f430d78961771ae3a1cc66c5deca083b4f6359c6d4b2468
  Test ProcessID: 41b9ef62-2617-4189-b23a-1bfa4c06dfec
2023/12/16 21:45:52 Creating container for image docker.io/testcontainers/ryuk:0.5.1
2023/12/16 21:45:53 Container created: 8f2240042c27
2023/12/16 21:45:53 Starting container: 8f2240042c27
2023/12/16 21:45:53 Container started: 8f2240042c27
2023/12/16 21:45:53 Waiting for container id 8f2240042c27 image: docker.io/testcontainers/ryuk:0.5.1. Waiting for: &amp;{Port:8080/tcp timeout:&lt;nil&gt; PollInterval:100ms}
2023/12/16 21:45:53 Creating container for image confluentinc/confluent-local:7.5.0
2023/12/16 21:45:53 Container created: a39a495aed0b
2023/12/16 21:45:53 Starting container: a39a495aed0b
2023/12/16 21:45:53 Container started: a39a495aed0b
["localhost:1037"]
2023/12/16 21:45:58 Terminating container: a39a495aed0b
2023/12/16 21:45:58 Container terminated: a39a495aed0b
PASS
ok      demo    6.236s
</code></pre>
<p>接下来，在上面用例的基础上，我们再来做一个Kafka连接以及数据读写测试：</p>
<pre><code>// testcontainers/kafka_consumer_and_producer/kafka_test.go

package main

import (
    "bytes"
    "context"
    "errors"
    "net"
    "strconv"
    "testing"
    "time"

    "github.com/testcontainers/testcontainers-go/modules/kafka"

    kc "github.com/segmentio/kafka-go" // kafka client
)

func createTopics(brokers []string, topics ...string) error {
    // to create topics when auto.create.topics.enable='false'
    conn, err := kc.Dial("tcp", brokers[0])
    if err != nil {
        return err
    }
    defer conn.Close()

    controller, err := conn.Controller()
    if err != nil {
        return err
    }
    var controllerConn *kc.Conn
    controllerConn, err = kc.Dial("tcp", net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port)))
    if err != nil {
        return err
    }
    defer controllerConn.Close()

    var topicConfigs []kc.TopicConfig
    for _, topic := range topics {
        topicConfig := kc.TopicConfig{
            Topic:             topic,
            NumPartitions:     1,
            ReplicationFactor: 1,
        }
        topicConfigs = append(topicConfigs, topicConfig)
    }

    err = controllerConn.CreateTopics(topicConfigs...)
    if err != nil {
        return err
    }

    return nil
}

func newWriter(brokers []string, topic string) *kc.Writer {
    return &amp;kc.Writer{
        Addr:                   kc.TCP(brokers...),
        Topic:                  topic,
        Balancer:               &amp;kc.LeastBytes{},
        AllowAutoTopicCreation: true,
        RequiredAcks:           0,
    }
}

func newReader(brokers []string, topic string) *kc.Reader {
    return kc.NewReader(kc.ReaderConfig{
        Brokers:  brokers,
        Topic:    topic,
        GroupID:  "test-group",
        MaxBytes: 10e6, // 10MB
    })
}

func TestProducerAndConsumer(t *testing.T) {
    ctx := context.Background()

    kafkaContainer, err := kafka.RunContainer(ctx, kafka.WithClusterID("test-cluster"))
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }

    // Clean up the container
    defer func() {
        if err := kafkaContainer.Terminate(ctx); err != nil {
            t.Fatalf("want nil, actual %v\n", err)
        }
    }()

    state, err := kafkaContainer.State(ctx)
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }

    if state.Running != true {
        t.Errorf("want true, actual %t", state.Running)
    }

    brokers, err := kafkaContainer.Brokers(ctx)
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }

    topic := "test-topic"
    w := newWriter(brokers, topic)
    defer w.Close()
    r := newReader(brokers, topic)
    defer r.Close()

    err = createTopics(brokers, topic)
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }
    time.Sleep(5 * time.Second)

    messages := []kc.Message{
        {
            Key:   []byte("Key-A"),
            Value: []byte("Value-A"),
        },
        {
            Key:   []byte("Key-B"),
            Value: []byte("Value-B"),
        },
        {
            Key:   []byte("Key-C"),
            Value: []byte("Value-C"),
        },
        {
            Key:   []byte("Key-D"),
            Value: []byte("Value-D!"),
        },
    }

    const retries = 3
    for i := 0; i &lt; retries; i++ {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        // attempt to create topic prior to publishing the message
        err = w.WriteMessages(ctx, messages...)
        if errors.Is(err, kc.LeaderNotAvailable) || errors.Is(err, context.DeadlineExceeded) {
            time.Sleep(time.Millisecond * 250)
            continue
        }

        if err != nil {
            t.Fatalf("want nil, actual %v\n", err)
        }
        break
    }

    var getMessages []kc.Message
    for i := 0; i &lt; len(messages); i++ {
        m, err := r.ReadMessage(context.Background())
        if err != nil {
            t.Fatalf("want nil, actual %v\n", err)
        }
        getMessages = append(getMessages, m)
    }

    for i := 0; i &lt; len(messages); i++ {
        if !bytes.Equal(getMessages[i].Key, messages[i].Key) {
            t.Errorf("want %s, actual %s\n", string(messages[i].Key), string(getMessages[i].Key))
        }
        if !bytes.Equal(getMessages[i].Value, messages[i].Value) {
            t.Errorf("want %s, actual %s\n", string(messages[i].Value), string(getMessages[i].Value))
        }
    }
}
</code></pre>
<p>我们<a href="https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients">使用segmentio/kafka-go这个客户端</a>来实现kafka的读写。关于如何使用segmentio/kafka-go这个客户端，可以参考我之前写的《<a href="https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients">Go社区主流Kafka客户端简要对比</a>》。</p>
<p>这里我们在TestProducerAndConsumer这个用例中，先通过testcontainers-go的kafka.RunContainer启动一个Kakfa实例，然后创建了一个topic: “test-topic”。我们在写入消息前也可以不单独创建这个“test-topic”，Kafka默认启用topic自动创建，并且segmentio/kafka-go的高级API：Writer也支持AllowAutoTopicCreation的设置。不过topic的创建需要一些时间，如果要在首次写入消息时创建topic，此次写入可能会失败，需要retry。</p>
<p>向topic写入一条消息(实际上是一个批量Message，包括四个key-value pair)后，我们调用ReadMessage从上述topic中读取消息，并将读取的消息与写入的消息做比较。</p>
<blockquote>
<p>注：近期<a href="https://github.com/segmentio/kafka-go/pull/1117">发现kafka-go的一个可能导致内存暴涨的问题</a>，在kafka ack返回延迟变大的时候，可能触发该问题。</p>
</blockquote>
<p>下面是执行该用例的输出结果：</p>
<pre><code>$ go test
2023/12/17 17:43:54 github.com/testcontainers/testcontainers-go - Connected to docker:
  Server Version: 24.0.7
  API Version: 1.43
  Operating System: CentOS Linux 7 (Core)
  Total Memory: 30984 MB
  Resolved Docker Host: unix:///var/run/docker.sock
  Resolved Docker Socket Path: /var/run/docker.sock
  Test SessionID: f76fe611c753aa4ef1456285503b0935a29795e7c0fab2ea2588029929215a08
  Test ProcessID: 27f531ee-9b5f-4e4f-b5f0-468143871004
2023/12/17 17:43:54 Creating container for image docker.io/testcontainers/ryuk:0.5.1
2023/12/17 17:43:54 Container created: 577309098f4c
2023/12/17 17:43:54 Starting container: 577309098f4c
2023/12/17 17:43:54 Container started: 577309098f4c
2023/12/17 17:43:54 Waiting for container id 577309098f4c image: docker.io/testcontainers/ryuk:0.5.1. Waiting for: &amp;{Port:8080/tcp timeout:&lt;nil&gt; PollInterval:100ms}
2023/12/17 17:43:54 Creating container for image confluentinc/confluent-local:7.5.0
2023/12/17 17:43:55 Container created: 1ee11e11742b
2023/12/17 17:43:55 Starting container: 1ee11e11742b
2023/12/17 17:43:55 Container started: 1ee11e11742b
2023/12/17 17:44:15 Terminating container: 1ee11e11742b
2023/12/17 17:44:15 Container terminated: 1ee11e11742b
PASS
ok      demo    21.505s
</code></pre>
<p>我们看到默认情况下，testcontainer能满足与kafka交互的基本需求，并且testcontainer提供了一系列Option(WithXXX)可以对container进行定制，以满足一些扩展性的要求，但是这需要你对testcontainer提供的API有更全面的了解。</p>
<p>除了开箱即用的testcontainer之外，我们还可以使用另外一种方便的基于容器的技术：<a href="https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose">docker-compose来定制和启停我们需要的kafka image</a>。接下来，我们就来看看如何使用docker-compose建立fake kafka object。</p>
<h2>3. 使用docker-compose建立fake kafka</h2>
<h3>3.1 一个基础的基于docker-compose的fake kafka实例模板</h3>
<p>这次我们使用bitnami提供的kafka镜像，我们先建立一个“等价”于上面“testcontainers-go”提供的kafka module的kafka实例，下面是docker-compose.yml：</p>
<pre><code>// docker-compose/bitnami/plaintext/docker-compose.yml

version: "2"

services:
  kafka:
    image: docker.io/bitnami/kafka:3.6
    network_mode: "host"
    volumes:
      - "kafka_data:/bitnami"
    environment:
      # KRaft settings
      - KAFKA_CFG_NODE_ID=0
      - KAFKA_CFG_PROCESS_ROLES=controller,broker
      - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@localhost:9093
      # Listeners
      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
      - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://:9092
      - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
      # borrow from testcontainer
      - KAFKA_CFG_BROKER_ID=0
      - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1
      - KAFKA_CFG_OFFSETS_TOPIC_NUM_PARTITIONS=1
      - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1
      - KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS=0
      - KAFKA_CFG_LOG_FLUSH_INTERVAL_MESSAGES=9223372036854775807
volumes:
  kafka_data:
    driver: local
</code></pre>
<p>我们看到其中一些配置“借鉴”了testcontainers-go的kafka module，我们启动一下该容器：</p>
<pre><code>$ docker-compose up -d
[+] Running 2/2
 ✔ Volume "plaintext_kafka_data"  Created                                                                                    0.0s
 ✔ Container plaintext-kafka-1    Started                                                                                    0.1s
</code></pre>
<p>依赖该容器的go测试代码与前面的TestProducerAndConsumer差不多，只是在开始处去掉了container的创建过程：</p>
<pre><code>// docker-compose/bitnami/plaintext/kafka_test.go

func TestProducerAndConsumer(t *testing.T) {
    brokers := []string{"localhost:9092"}
    topic := "test-topic"
    w := newWriter(brokers, topic)
    defer w.Close()
    r := newReader(brokers, topic)
    defer r.Close()

    err := createTopics(brokers, topic)
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }
    time.Sleep(5 * time.Second)
    ... ...
}
</code></pre>
<p>运行该测试用例，我们看到预期的结果：</p>
<pre><code>go test
write message ok  Value-A
write message ok  Value-B
write message ok  Value-C
write message ok  Value-D!
PASS
ok      demo    15.143s
</code></pre>
<p>不过对于单元测试来说，显然我们不能手动来启动和停止kafka container，我们需要为每个用例填上setup和teardown，这样也能保证用例间的相互隔离，于是我们增加了一个docker_compose_helper.go文件，在这个文件中我们提供了一些帮助testcase启停kafka的helper函数：</p>
<pre><code>// docker-compose/bitnami/plaintext/docker_compose_helper.go

package main

import (
    "fmt"
    "os/exec"
    "strings"
    "time"
)

// helpler function for operating docker container through docker-compose command

const (
    defaultCmd     = "docker-compose"
    defaultCfgFile = "docker-compose.yml"
)

func execCliCommand(cmd string, opts ...string) ([]byte, error) {
    cmds := cmd + " " + strings.Join(opts, " ")
    fmt.Println("exec command:", cmds)
    return exec.Command(cmd, opts...).CombinedOutput()
}

func execDockerComposeCommand(cmd string, cfgFile string, opts ...string) ([]byte, error) {
    var allOpts = []string{"-f", cfgFile}
    allOpts = append(allOpts, opts...)
    return execCliCommand(cmd, allOpts...)
}

func UpKakfa(composeCfgFile string) ([]byte, error) {
    b, err := execDockerComposeCommand(defaultCmd, composeCfgFile, "up", "-d")
    if err != nil {
        return nil, err
    }
    time.Sleep(10 * time.Second)
    return b, nil
}

func UpDefaultKakfa() ([]byte, error) {
    return UpKakfa(defaultCfgFile)
}

func DownKakfa(composeCfgFile string) ([]byte, error) {
    b, err := execDockerComposeCommand(defaultCmd, composeCfgFile, "down", "-v")
    if err != nil {
        return nil, err
    }
    time.Sleep(10 * time.Second)
    return b, nil
}

func DownDefaultKakfa() ([]byte, error) {
    return DownKakfa(defaultCfgFile)
}
</code></pre>
<p>眼尖的童鞋可能看到：在UpKakfa和DownKafka函数中我们使用了硬编码的“time.Sleep”来等待10s，通常在镜像已经pull到本地后这是有效的，但却不是最精确地等待方式，<a href="https://pkg.go.dev/github.com/testcontainers/testcontainers-go@v0.26.0/wait">testcontainers-go/wait</a>中提供了等待容器内程序启动完毕的多种策略，如果你想用更精确的等待方式，可以了解一下wait包。</p>
<p>基于helper函数，我们改造一下TestProducerAndConsumer用例：</p>
<pre><code>// docker-compose/bitnami/plaintext/kafka_test.go
func TestProducerAndConsumer(t *testing.T) {
    _, err := UpDefaultKakfa()
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }

    t.Cleanup(func() {
        DownDefaultKakfa()
    })
    ... ...
}
</code></pre>
<p>我们在用例开始处通过UpDefaultKakfa使用docker-compose将kafka实例启动起来，然后<a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14/">注册了Cleanup函数</a>，用于在test case执行结束后销毁kafka实例。</p>
<p>下面是新版用例的执行结果：</p>
<pre><code>$ go test
exec command: docker-compose -f docker-compose.yml up -d
write message ok  Value-A
write message ok  Value-B
write message ok  Value-C
write message ok  Value-D!
exec command: docker-compose -f docker-compose.yml down -v
PASS
ok      demo    36.402s
</code></pre>
<p>使用docker-compose的最大好处就是可以通过docker-compose.yml文件对要fake的object进行灵活的定制，这种定制与testcontainers-go的差别就是你无需去研究testcontiners-go的API。</p>
<p>下面是使用tls连接与kafka建立连接并实现读写的示例。</p>
<h3>3.2 建立一个基于TLS连接的fake kafka实例</h3>
<p>Kafka的配置复杂是有目共睹的，为了建立一个基于TLS连接，我也是花了不少时间做“试验”，尤其是listeners以及证书的配置，不下点苦功夫读文档还真是配不出来。</p>
<p>下面是一个基于bitnami/kafka镜像配置出来的基于TLS安全通道上的kafka实例：</p>
<pre><code>// docker-compose/bitnami/tls/docker-compose.yml

# config doc:  https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md

version: "2"

services:
  kafka:
    image: docker.io/bitnami/kafka:3.6
    network_mode: "host"
    #ports:
      #- "9092:9092"
    environment:
      # KRaft settings
      - KAFKA_CFG_NODE_ID=0
      - KAFKA_CFG_PROCESS_ROLES=controller,broker
      - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@localhost:9094
      # Listeners
      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,SECURED://:9093,CONTROLLER://:9094
      - KAFKA_CFG_ADVERTISED_LISTENERS=SECURED://:9093
      - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SECURED:SSL,PLAINTEXT:PLAINTEXT
      - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SECURED
      # SSL settings
      - KAFKA_TLS_TYPE=PEM
      - KAFKA_TLS_CLIENT_AUTH=none
      - KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM=
      # borrow from testcontainer
      - KAFKA_CFG_BROKER_ID=0
      - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1
      - KAFKA_CFG_OFFSETS_TOPIC_NUM_PARTITIONS=1
      - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1
      - KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS=0
      - KAFKA_CFG_LOG_FLUSH_INTERVAL_MESSAGES=9223372036854775807
    volumes:
      # server.cert, server.key and ca.crt
      - "kafka_data:/bitnami"
      - "./kafka.keystore.pem:/opt/bitnami/kafka/config/certs/kafka.keystore.pem:ro"
      - "./kafka.keystore.key:/opt/bitnami/kafka/config/certs/kafka.keystore.key:ro"
      - "./kafka.truststore.pem:/opt/bitnami/kafka/config/certs/kafka.truststore.pem:ro"
volumes:
  kafka_data:
    driver: local
</code></pre>
<p>这里我们使用pem格式的证书和key，在上面配置中，volumes下面挂载的kafka.keystore.pem、kafka.keystore.key和kafka.truststore.pem分别对应了以前在Go中常用的名字：server-cert.pem(服务端证书), server-key.pem(服务端私钥)和ca-cert.pem(CA证书)。</p>
<p>这里整理了一个一键生成的脚本docker-compose/bitnami/tls/kafka-generate-cert.sh，我们执行该脚本生成所有需要的证书并放到指定位置(遇到命令行提示，只需要一路回车即可)：</p>
<pre><code>$bash kafka-generate-cert.sh
.........++++++
.............................++++++
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:
State or Province Name (full name) []:
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
Signature ok
subject=/C=XX/L=Default City/O=Default Company Ltd
Getting Private key
.....................++++++
.........++++++
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:
State or Province Name (full name) []:
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
Signature ok
subject=/C=XX/L=Default City/O=Default Company Ltd
Getting CA Private Key
</code></pre>
<p>接下来，我们来改造用例，使之支持以tls方式建立到kakfa的连接：</p>
<pre><code>//docker-compose/bitnami/tls/kafka_test.go

func createTopics(brokers []string, tlsConfig *tls.Config, topics ...string) error {
    dialer := &amp;kc.Dialer{
        Timeout:   10 * time.Second,
        DualStack: true,
        TLS:       tlsConfig,
    }

    conn, err := dialer.DialContext(context.Background(), "tcp", brokers[0])
    if err != nil {
        fmt.Println("creating topic: dialer dial error:", err)
        return err
    }
    defer conn.Close()
    fmt.Println("creating topic: dialer dial ok")
    ... ...
}

func newWriter(brokers []string, tlsConfig *tls.Config, topic string) *kc.Writer {
    w := &amp;kc.Writer{
        Addr:                   kc.TCP(brokers...),
        Topic:                  topic,
        Balancer:               &amp;kc.LeastBytes{},
        AllowAutoTopicCreation: true,
        Async:                  true,
        //RequiredAcks:           0,
        Completion: func(messages []kc.Message, err error) {
            for _, message := range messages {
                if err != nil {
                    fmt.Println("write message fail", err)
                } else {
                    fmt.Println("write message ok", string(message.Topic), string(message.Value))
                }
            }
        },
    }

    if tlsConfig != nil {
        w.Transport = &amp;kc.Transport{
            TLS: tlsConfig,
        }
    }
    return w
}

func newReader(brokers []string, tlsConfig *tls.Config, topic string) *kc.Reader {
    dialer := &amp;kc.Dialer{
        Timeout:   10 * time.Second,
        DualStack: true,
        TLS:       tlsConfig,
    }

    return kc.NewReader(kc.ReaderConfig{
        Dialer:   dialer,
        Brokers:  brokers,
        Topic:    topic,
        GroupID:  "test-group",
        MaxBytes: 10e6, // 10MB
    })
}

func TestProducerAndConsumer(t *testing.T) {
    var err error
    _, err = UpDefaultKakfa()
    if err != nil {
        t.Fatalf("want nil, actual %v\n", err)
    }

    t.Cleanup(func() {
        DownDefaultKakfa()
    })

    brokers := []string{"localhost:9093"}
    topic := "test-topic"

    tlsConfig, _ := newTLSConfig()
    w := newWriter(brokers, tlsConfig, topic)
    defer w.Close()
    r := newReader(brokers, tlsConfig, topic)
    defer r.Close()
    err = createTopics(brokers, tlsConfig, topic)
    if err != nil {
        fmt.Printf("create topic error: %v, but it may not affect the later action, just ignore it\n", err)
    }
    time.Sleep(5 * time.Second)
    ... ...
}

func newTLSConfig() (*tls.Config, error) {
    /*
       // 加载 CA 证书
       caCert, err := ioutil.ReadFile("/path/to/ca.crt")
       if err != nil {
               return nil, err
       }

       // 加载客户端证书和私钥
       cert, err := tls.LoadX509KeyPair("/path/to/client.crt", "/path/to/client.key")
       if err != nil {
               return nil, err
       }

       // 创建 CertPool 并添加 CA 证书
       caCertPool := x509.NewCertPool()
       caCertPool.AppendCertsFromPEM(caCert)
    */
    // 创建并返回 TLS 配置
    return &amp;tls.Config{
        //RootCAs:      caCertPool,
        //Certificates: []tls.Certificate{cert},
        InsecureSkipVerify: true,
    }, nil
}
</code></pre>
<p>在上述代码中，我们按照segmentio/kafka-go为createTopics、newWriter和newReader都加上了tls.Config参数，此外在测试用例中，我们用newTLSConfig创建一个tls.Config的实例，在这里我们一切简化处理，采用InsecureSkipVerify=true的方式与kafka broker服务端进行握手，既不验证服务端证书，也不做双向认证(mutual TLS)。</p>
<p>下面是修改代码后的测试用例执行结果：</p>
<pre><code>$ go test
exec command: docker-compose -f docker-compose.yml up -d
creating topic: dialer dial ok
creating topic: get controller ok
creating topic: dial control listener ok
create topic error: EOF, but it may not affect the later action, just ignore it
write message error: [3] Unknown Topic Or Partition: the request is for a topic or partition that does not exist on this broker
write message ok  Value-A
write message ok  Value-B
write message ok  Value-C
write message ok  Value-D!
exec command: docker-compose -f docker-compose.yml down -v
PASS
ok      demo    38.473s
</code></pre>
<p>这里我们看到：createTopics虽然连接kafka的各个listener都ok，但调用topic创建时，返回EOF，但这的确不影响后续action的执行，不确定这是segmentio/kafka-go的问题，还是kafka实例的问题。另外首次写入消息时，也因为topic或partition未建立而失败，retry后消息正常写入。</p>
<p>通过这个例子我们看到，基于docker-compose建立fake object有着更广泛的灵活性，如果做好容器启动和停止的精准wait机制的话，我可能会更多选择这种方式。</p>
<h2>4. 小结</h2>
<p>本文介绍了如何在Go编程中进行依赖Kafka的单元测试，并探讨了寻找适合的Kafka fake object的策略。</p>
<p>对于Kafka这样的复杂系统来说，找到合适的fake object并不容易。因此，本文推荐使用容器作为fake object的策略，并分别介绍了使用testcontainers-go项目和使用docker-compose作为简化创建和清理基于容器的依赖项的工具。相对于刚刚加入testcontainers-go项目没多久的kafka module而言，使用docker-compose自定义fake object更加灵活一些。但无论哪种方法，开发人员都需要对kafka的配置有一个较为整体和深入的理解。</p>
<p>文中主要聚焦使用testcontainers-go和docker-compose建立fake kafka的过程，而用例并没有建立明确的sut(被测目标)，比如针对某个函数的白盒单元测试。</p>
<p>文本涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/unit-testing-deps-on-kafka">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2024年，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; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用Go开发Kubernetes Operator：基本结构</title>
		<link>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/</link>
		<comments>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/#comments</comments>
		<pubDate>Mon, 15 Aug 2022 14:47:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[controller]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[CR]]></category>
		<category><![CDATA[CRD]]></category>
		<category><![CDATA[CustomResourceDefinition]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[framework]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubebuilder]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[operator]]></category>
		<category><![CDATA[operator-framework]]></category>
		<category><![CDATA[operator-sdk]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[prometheus]]></category>
		<category><![CDATA[reconcile]]></category>
		<category><![CDATA[reconciliation]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[replicas]]></category>
		<category><![CDATA[ReplicaSet]]></category>
		<category><![CDATA[resource]]></category>
		<category><![CDATA[role]]></category>
		<category><![CDATA[role-binding]]></category>
		<category><![CDATA[scale]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[service-account]]></category>
		<category><![CDATA[spec]]></category>
		<category><![CDATA[TPR]]></category>
		<category><![CDATA[webserver]]></category>
		<category><![CDATA[伸缩]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3638</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1 注：文章首图基于《Kubernetes Operators Explained》修改 几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像Operator这样的应用模式和扩展方式日益受到开发者与运维者的欢迎。 我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的拿手好戏，是时候来研究一下operator了。 一. Operator的优点 kubernetes operator的概念最初来自CoreOS &#8211; 一家被红帽(redhat)收购的容器技术公司。 CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：etcd operator和prometheus operator。 注：etcd于2013年由CoreOS以开源形式发布；prometheus作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。 下面是CoreOS对Operator这一概念的诠释：Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序。 图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档) Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。 那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比： 通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。 我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。 在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。 我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。 不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面： 对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入； 从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：kubebuilder、operator framework sdk等； operator的能力也有高低之分，operator framework就提出了一个包含五个等级的operator能力模型(CAPABILITY MODEL)，见下图。使用Go开发高能力等级的operator需要对client-go这个kubernetes官方go client库中的API有深入的了解。 图：operator能力模型(截图来自operator framework官网) 当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。 二. Kubernetes resource、resource type、API和controller介绍 Kubernetes发展到今天，其本质已经显现： Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)； 其API就是“sql语句”； API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)； 每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)； 每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)； [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1">本文永久链接</a> &#8211; https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1</p>
<blockquote>
<p>注：文章首图基于《Kubernetes Operators Explained》修改</p>
</blockquote>
<p><a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准</a>，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">Operator这样的应用模式和扩展方式</a>日益受到开发者与运维者的欢迎。</p>
<p>我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的<strong>拿手好戏</strong>，是时候来研究一下operator了。</p>
<h3>一. Operator的优点</h3>
<p><a href="https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html">kubernetes operator的概念最初来自CoreOS</a> &#8211; 一家被红帽(redhat)收购的容器技术公司。</p>
<p>CoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：<a href="https://web.archive.org/web/20170224100544/https://coreos.com/blog/introducing-the-etcd-operator.html">etcd operator</a>和<a href="https://web.archive.org/web/20170224101137/https://coreos.com/blog/the-prometheus-operator.html">prometheus operator</a>。</p>
<blockquote>
<p>注：<a href="https://etcd.io">etcd</a>于2013年由CoreOS以开源形式发布；<a href="https://prometheus.io">prometheus</a>作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。</p>
</blockquote>
<p>下面是CoreOS对Operator这一概念的诠释：<strong>Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-4.png" alt="" /><br />
<center>图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档)</center></p>
<p>Operator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。</p>
<p>那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-2.png" alt="" /></p>
<p>通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。</p>
<p>我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。</p>
<p>在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。</p>
<p>我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。</p>
<p>不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面：</p>
<ul>
<li>对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入；</li>
<li>从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：<a href="https://github.com/kubernetes-sigs/kubebuilder">kubebuilder</a>、<a href="https://sdk.operatorframework.io">operator framework sdk</a>等；</li>
<li>operator的能力也有高低之分，operator framework就提出了一个包含<strong>五个等级的operator能力模型(CAPABILITY MODEL)</strong>，见下图。使用Go开发高能力等级的operator需要对<a href="https://github.com/kubernetes/client-go">client-go</a>这个kubernetes官方go client库中的API有深入的了解。</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-3.png" alt="" /><br />
<center>图：operator能力模型(截图来自operator framework官网)</center></p>
<p>当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。</p>
<h3>二. Kubernetes resource、resource type、API和controller介绍</h3>
<p>Kubernetes发展到今天，其本质已经显现：</p>
<ul>
<li>Kubernetes就是一个“数据库”(数据实际持久存储在etcd中)；</li>
<li>其API就是“sql语句”；</li>
<li>API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)；</li>
<li>每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)；</li>
<li>每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)；</li>
<li>Kubernetes这个“数据库”内置了很多“表”，比如Pod、Deployment、DaemonSet、ReplicaSet等；</li>
</ul>
<p>下面是一个Kubernetes API与resource关系的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-5.png" alt="" /></p>
<p>我们看到resource type有两类，一类的namespace相关的(namespace-scoped)，我们通过下面形式的API操作这类resource type的实例：</p>
<pre><code>VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE - 操作某特定namespace下面的resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME - 操作某特定namespace下面的resource type中的某个具体的resource实例
</code></pre>
<p>另外一类则是namespace无关，即cluster范围(cluster-scoped)的，我们通过下面形式的API对这类resource type的实例进行操作：</p>
<pre><code>VERB /apis/GROUP/VERSION/RESOURCETYPE - 操作resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/RESOURCETYPE/NAME - 操作resource type中的某个具体的resource实例
</code></pre>
<p>我们知道Kubernetes并非真的只是一个“数据库”，它是服务编排和容器调度的平台标准，它的基本调度单元是Pod(也是一个resource type)，即一组容器的集合。那么Pod又是如何被创建、更新和删除的呢？这就离不开控制器(controller)了。<strong>每一类resource type都有自己对应的控制器(controller)</strong>。以pod这个resource type为例，它的controller为ReplicasSet的实例。</p>
<p>控制器的运行逻辑如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-6.png" alt="" /><br />
<center>图：控制器运行逻辑(引自《Kubernetes Operators Explained》一文)</center></p>
<p>控制器一旦启动，将尝试获得resource的当前状态(current state)，并与存储在k8s中的resource的期望状态（desired state，即spec)做比对，如果不一致，controller就会调用相应API进行调整，尽力使得current state与期望状态达成一致。这个达成一致的过程被称为<strong>协调(reconciliation)</strong>，协调过程的伪代码逻辑如下：</p>
<pre><code>for {
    desired := getDesiredState()
    current := getCurrentState()
    makeChanges(desired, current)
}
</code></pre>
<blockquote>
<p>注：k8s中有一个object的概念？那么object是什么呢？它类似于Java Object基类或Ruby中的Object超类。不仅resource type的实例resource是一个(is-a)object，resource type本身也是一个object，它是kubernetes concept的实例。</p>
</blockquote>
<p>有了上面对k8s这些概念的初步理解，我们下面就来理解一下Operator究竟是什么！</p>
<h3>三. Operator模式 = 操作对象(CRD) + 控制逻辑(controller)</h3>
<p>如果让运维人员直面这些内置的resource type(如deployment、pod等)，也就是前面“使用operator vs. 不使用operator”对比图中的第二种情况, 运维人员面临的情况将会很复杂，且操作易错。</p>
<p>那么如果不直面内置的resource type，那么我们如何自定义resource type呢, Kubernetes提供了Custom Resource Definition，CRD(在coreos刚提出operator概念的时候，crd的前身是Third Party Resource, TPR)可以用于自定义resource type。</p>
<p>根据前面我们对resource type理解，定义CRD相当于建立新“表”(resource type)，一旦CRD建立，k8s会为我们自动生成对应CRD的API endpoint，我们就可以通过yaml或API来操作这个“表”。我们可以向“表”中“插入”数据，即基于CRD创建Custom Resource(CR)，这就好比我们创建Deployment实例，向Deployment“表”中插入数据一样。</p>
<p>和原生内置的resource type一样，光有存储对象状态的CR还不够，原生resource type有对应controller负责协调(reconciliation)实例的创建、伸缩与删除，CR也需要这样的“协调者”，即我们也需要定义一个controller来负责监听CR状态并管理CR创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个controller不再是面向原生Resource type的实例，而是<strong>面向CRD的实例CR的controller</strong>。</p>
<p>有了自定义的操作对象类型(CRD)，有了面向操作对象类型实例的controller，我们将其打包为一个概念：“Operator模式”，operator模式中的controller也被称为operator，它是在集群中对CR进行维护操作的主体。</p>
<h3>四. 使用kubebuilder开发webserver operator</h3>
<blockquote>
<p>假设：此时你的本地开发环境已经具备访问实验用k8s环境的一切配置，通过kubectl工具可以任意操作k8s。</p>
</blockquote>
<p><strong>再深入浅出的概念讲解都不如一次实战对理解概念更有帮助</strong>，下面我们就来开发一个简单的Operator。</p>
<p>前面提过operator开发非常verbose，因此社区提供了开发工具和框架来帮助开发人员简化开发过程，目前主流的包括operator framework sdk和kubebuilder，前者是redhat开源并维护的一套工具，支持使用go、ansible、helm进行operator开发(其中只有go可以开发到能力级别5的operator，其他两种则不行)；而kubebuilder则是kubernetes官方的一个sig(特别兴趣小组)维护的operator开发工具。目前基于operator framework sdk和go进行operator开发时，operator sdk底层使用的也是kubebuilder，所以这里我们就直接使用kubebuilder来开发operator。</p>
<p>按照operator能力模型，我们这个operator差不多处于2级这个层次，我们定义一个Webserver的resource type，它代表的是一个基于nginx的webserver集群，我们的operator支持创建webserver示例(一个nginx集群)，支持nginx集群伸缩，支持集群中nginx的版本升级。</p>
<p>下面我们就用kubebuilder来实现这个operator！</p>
<h4>1. 安装kubebuilder</h4>
<p>这里我们采用源码构建方式安装，步骤如下：</p>
<pre><code>$git clone git@github.com:kubernetes-sigs/kubebuilder.git
$cd kubebuilder
$make
$cd bin
$./kubebuilder version
Version: main.version{KubeBuilderVersion:"v3.5.0-101-g5c949c2e",
KubernetesVendor:"unknown",
GitCommit:"5c949c2e50ca8eec80d64878b88e1b2ee30bf0bc",
BuildDate:"2022-08-06T09:12:50Z", GoOs:"linux", GoArch:"amd64"}
</code></pre>
<p>然后将bin/kubebuilder拷贝到你的PATH环境变量中的某个路径下即可。</p>
<h4>2. 创建webserver-operator工程</h4>
<p>接下来，我们就可以使用kubebuilder创建webserver-operator工程了：</p>
<pre><code>$mkdir webserver-operator
$cd webserver-operator
$kubebuilder init  --repo github.com/bigwhite/webserver-operator --project-name webserver-operator

Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.12.2
go: downloading k8s.io/client-go v0.24.2
go: downloading k8s.io/component-base v0.24.2
Update dependencies:
$ go mod tidy
Next: define a resource with:
kubebuilder create api
</code></pre>
<blockquote>
<p>注：&#8211;repo指定go.mod中的module root path，你可以定义你自己的module root path。</p>
</blockquote>
<h4>3. 创建API，生成初始CRD</h4>
<p>Operator包括CRD和controller，这里我们就来建立自己的CRD，即自定义的resource type，也就是API的endpoint，我们使用下面kubebuilder create命令来完成这个步骤：</p>
<pre><code>$kubebuilder create api --version v1 --kind WebServer
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/webserver_types.go
controllers/webserver_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
</code></pre>
<p>之后，我们执行make manifests来生成最终CRD对应的yaml文件：</p>
<pre><code>$make manifests
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
</code></pre>
<p>此刻，整个工程的目录文件布局如下：</p>
<pre><code>$tree -F .
.
├── api/
│   └── v1/
│       ├── groupversion_info.go
│       ├── webserver_types.go
│       └── zz_generated.deepcopy.go
├── bin/
│   └── controller-gen*
├── config/
│   ├── crd/
│   │   ├── bases/
│   │   │   └── my.domain_webservers.yaml
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches/
│   │       ├── cainjection_in_webservers.yaml
│   │       └── webhook_in_webservers.yaml
│   ├── default/
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager/
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus/
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac/
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   ├── service_account.yaml
│   │   ├── webserver_editor_role.yaml
│   │   └── webserver_viewer_role.yaml
│   └── samples/
│       └── _v1_webserver.yaml
├── controllers/
│   ├── suite_test.go
│   └── webserver_controller.go
├── Dockerfile
├── go.mod
├── go.sum
├── hack/
│   └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md

14 directories, 40 files
</code></pre>
<h4>4. webserver-operator的基本结构</h4>
<p>忽略我们此次不关心的诸如leader election、auth_proxy等，我将这个operator例子的主要部分整理到下面这张图中：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-7.png" alt="" /></p>
<p>图中的各个部分就是使用kubebuilder生成的<strong>operator的基本结构</strong>。</p>
<p>webserver operator主要由CRD和controller组成：</p>
<ul>
<li>CRD</li>
</ul>
<p>图中的左下角的框框就是上面生成的CRD yaml文件：config/crd/bases/my.domain_webservers.yaml。CRD与api/v1/webserver_types.go密切相关。我们在api/v1/webserver_types.go中为CRD定义spec相关字段，之后make manifests命令可以解析webserver_types.go中的变化并更新CRD的yaml文件。</p>
<ul>
<li>controller</li>
</ul>
<p>从图的右侧部分可以看出，controller自身就是作为一个deployment部署在k8s集群中运行的，它监视CRD的实例CR的运行状态，并在Reconcile方法中检查预期状态与当前状态是否一致，如果不一致，则执行相关操作。</p>
<ul>
<li>其它</li>
</ul>
<p>图中左上角是有关controller的权限的设置，controller通过serviceaccount访问k8s API server，通过role.yaml和role_binding.yaml设置controller的角色和权限。</p>
<h4>5. 为CRD spec添加字段(field)</h4>
<p>为了实现Webserver operator的功能目标，我们需要为CRD spec添加一些状态字段。前面说过，CRD与api中的webserver_types.go文件是同步的，我们只需修改webserver_types.go文件即可。我们在WebServerSpec结构体中增加Replicas和Image两个字段，它们分别用于表示webserver实例的副本数量以及使用的容器镜像：</p>
<pre><code>// api/v1/webserver_types.go

// WebServerSpec defines the desired state of WebServer
type WebServerSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // The number of replicas that the webserver should have
    Replicas int `json:"replicas,omitempty"`

    // The container image of the webserver
    Image string `json:"image,omitempty"`

    // Foo is an example field of WebServer. Edit webserver_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}
</code></pre>
<p>保存修改后，<strong>执行make manifests</strong>重新生成config/crd/bases/my.domain_webservers.yaml</p>
<pre><code>$cat my.domain_webservers.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.9.2
  creationTimestamp: null
  name: webservers.my.domain
spec:
  group: my.domain
  names:
    kind: WebServer
    listKind: WebServerList
    plural: webservers
    singular: webserver
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: WebServer is the Schema for the webservers API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: WebServerSpec defines the desired state of WebServer
            properties:
              foo:
                description: Foo is an example field of WebServer. Edit webserver_types.go
                  to remove/update
                type: string
              image:
                description: The container image of the webserver
                type: string
              replicas:
                description: The number of replicas that the webserver should have
                type: integer
            type: object
          status:
            description: WebServerStatus defines the observed state of WebServer
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}
</code></pre>
<p>一旦定义完CRD，我们就可以将其安装到k8s中：</p>
<pre><code>$make install
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
{Version:kustomize/v3.8.7 GitCommit:ad092cc7a91c07fdf63a2e4b7f13fa588a39af4f BuildDate:2020-11-11T23:14:14Z GoOs:linux GoArch:amd64}
kustomize installed to /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain created
</code></pre>
<p>检查安装情况：</p>
<pre><code>$kubectl get crd|grep webservers
webservers.my.domain                                             2022-08-06T21:55:45Z
</code></pre>
<h4>6. 修改role.yaml</h4>
<p>在开始controller开发之前，我们先来为controller后续的运行“铺平道路”，即设置好相应权限。</p>
<p>我们在controller中会为CRD实例创建对应deployment和service，这样就要求controller有操作deployments和services的权限，这样就需要我们修改role.yaml，增加service account:  controller-manager 操作deployments和services的权限：</p>
<pre><code>// config/rbac/role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: manager-role
rules:
- apiGroups:
  - my.domain
  resources:
  - webservers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - my.domain
  resources:
  - webservers/finalizers
  verbs:
  - update
- apiGroups:
  - my.domain
  resources:
  - webservers/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  - ""
  resources:
  - services
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
</code></pre>
<p>修改后的role.yaml先放在这里，后续与controller一并部署到k8s上。</p>
<h4>7. 实现controller的Reconcile(协调)逻辑</h4>
<p>kubebuilder为我们搭好了controller的代码架子，我们只需要在controllers/webserver_controller.go中实现WebServerReconciler的Reconcile方法即可。下面是Reconcile的一个简易流程图，结合这幅图理解代码就容易的多了：</p>
<p><img src="https://tonybai.com/wp-content/uploads/developing-kubernetes-operators-in-go-part1-8.png" alt="" /></p>
<p>下面是对应的Reconcile方法的代码：</p>
<pre><code>// controllers/webserver_controller.go

func (r *WebServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("Webserver", req.NamespacedName)

    instance := &amp;mydomainv1.WebServer{}
    err := r.Get(ctx, req.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Return and don't requeue
            log.Info("Webserver resource not found. Ignoring since object must be deleted")
            return ctrl.Result{}, nil
        }

        // Error reading the object - requeue the request.
        log.Error(err, "Failed to get Webserver")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Check if the webserver deployment already exists, if not, create a new one
    found := &amp;appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new deployment
        dep := r.deploymentForWebserver(instance)
        log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Deployment created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Deployment")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Ensure the deployment replicas and image are the same as the spec
    var replicas int32 = int32(instance.Spec.Replicas)
    image := instance.Spec.Image

    var needUpd bool
    if *found.Spec.Replicas != replicas {
        log.Info("Deployment spec.replicas change", "from", *found.Spec.Replicas, "to", replicas)
        found.Spec.Replicas = &amp;replicas
        needUpd = true
    }

    if (*found).Spec.Template.Spec.Containers[0].Image != image {
        log.Info("Deployment spec.template.spec.container[0].image change", "from", (*found).Spec.Template.Spec.Containers[0].Image, "to", image)
        found.Spec.Template.Spec.Containers[0].Image = image
        needUpd = true
    }

    if needUpd {
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }

    // Check if the webserver service already exists, if not, create a new one
    foundService := &amp;corev1.Service{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name + "-service", Namespace: instance.Namespace}, foundService)
    if err != nil &amp;&amp; errors.IsNotFound(err) {
        // Define a new service
        srv := r.serviceForWebserver(instance)
        log.Info("Creating a new Service", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
        err = r.Create(ctx, srv)
        if err != nil {
            log.Error(err, "Failed to create new Servie", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Service created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Service")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Tbd: Ensure the service state is the same as the spec, your homework

    // reconcile webserver operator in again 10 seconds
    return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}
</code></pre>
<p>这里大家可能发现了：<strong>原来CRD的controller最终还是将CR翻译为k8s原生Resource，比如service、deployment等。CR的状态变化(比如这里的replicas、image等)最终都转换成了deployment等原生resource的update操作</strong>，这就是operator的精髓！理解到这一层，operator对大家来说就不再是什么密不可及的概念了。</p>
<p>有些朋友可能也会发现，上面流程图中似乎没有考虑CR实例被删除时对deployment、service的操作，的确如此。不过对于一个7&#215;24小时运行于后台的服务来说，我们更多关注的是其变更、伸缩、升级等操作，删除是优先级最低的需求。</p>
<h4>8. 构建controller image</h4>
<p>controller代码写完后，我们就来构建controller的image。通过前文我们知道，这个controller其实就是运行在k8s中的一个deployment下的pod。我们需要构建其image并通过deployment部署到k8s中。</p>
<p>kubebuilder创建的operator工程中包含了Makefile，通过make docker-build即可构建controller image。docker-build使用golang builder image来构建controller源码，不过如果不对Dockerfile稍作修改，你很难编译过去，因为默认GOPROXY在国内无法访问。这里最简单的改造方式是使用vendor构建，下面是改造后的Dockerfile：</p>
<pre><code># Build the manager binary
FROM golang:1.18 as builder

ENV GOPROXY https://goproxy.cn
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
COPY vendor/ vendor/
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
#RUN go mod download

# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM katanomi/distroless-static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
</code></pre>
<p>下面是构建的步骤：</p>
<pre><code>$go mod vendor
$make docker-build

test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
KUBEBUILDER_ASSETS="/home/tonybai/.local/share/kubebuilder-envtest/k8s/1.24.2-linux-amd64" go test ./... -coverprofile cover.out
?       github.com/bigwhite/webserver-operator    [no test files]
?       github.com/bigwhite/webserver-operator/api/v1    [no test files]
ok      github.com/bigwhite/webserver-operator/controllers    4.530s    coverage: 0.0% of statements
docker build -t bigwhite/webserver-controller:latest .
Sending build context to Docker daemon  47.51MB
Step 1/15 : FROM golang:1.18 as builder
 ---&gt; 2d952adaec1e
Step 2/15 : ENV GOPROXY https://goproxy.cn
 ---&gt; Using cache
 ---&gt; db2b06a078e3
Step 3/15 : WORKDIR /workspace
 ---&gt; Using cache
 ---&gt; cc3c613c19c6
Step 4/15 : COPY go.mod go.mod
 ---&gt; Using cache
 ---&gt; 5fa5c0d89350
Step 5/15 : COPY go.sum go.sum
 ---&gt; Using cache
 ---&gt; 71669cd0fe8e
Step 6/15 : COPY vendor/ vendor/
 ---&gt; Using cache
 ---&gt; 502b280a0e67
Step 7/15 : COPY main.go main.go
 ---&gt; Using cache
 ---&gt; 0c59a69091bb
Step 8/15 : COPY api/ api/
 ---&gt; Using cache
 ---&gt; 2b81131c681f
Step 9/15 : COPY controllers/ controllers/
 ---&gt; Using cache
 ---&gt; e3fd48c88ccb
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go
 ---&gt; Using cache
 ---&gt; 548ac10321a2
Step 11/15 : FROM katanomi/distroless-static:nonroot
 ---&gt; 421f180b71d8
Step 12/15 : WORKDIR /
 ---&gt; Running in ea7cb03027c0
Removing intermediate container ea7cb03027c0
 ---&gt; 9d3c0ea19c3b
Step 13/15 : COPY --from=builder /workspace/manager .
 ---&gt; a4387fe33ab7
Step 14/15 : USER 65532:65532
 ---&gt; Running in 739a32d251b6
Removing intermediate container 739a32d251b6
 ---&gt; 52ae8742f9c5
Step 15/15 : ENTRYPOINT ["/manager"]
 ---&gt; Running in 897893b0c9df
Removing intermediate container 897893b0c9df
 ---&gt; e375cc2adb08
Successfully built e375cc2adb08
Successfully tagged bigwhite/webserver-controller:latest
</code></pre>
<blockquote>
<p>注：执行make命令之前，先将Makefile中的IMG变量初值改为IMG ?= bigwhite/webserver-controller:latest</p>
</blockquote>
<p>构建成功后，执行make docker-push将image推送到镜像仓库中(这里使用了docker公司提供的公共仓库)。</p>
<h4>9. 部署controller</h4>
<p>之前我们已经通过make install将CRD安装到k8s中了，接下来再把controller部署到k8s上，我们的operator就算部署完毕了。执行make deploy即可实现部署：</p>
<pre><code>$make deploy
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
cd config/manager &amp;&amp; /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize edit set image controller=bigwhite/webserver-controller:latest
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/webserver-operator-system created
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain unchanged
serviceaccount/webserver-operator-controller-manager created
role.rbac.authorization.k8s.io/webserver-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/webserver-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/webserver-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-proxy-rolebinding created
configmap/webserver-operator-manager-config created
service/webserver-operator-controller-manager-metrics-service created
deployment.apps/webserver-operator-controller-manager created
</code></pre>
<p>我们看到deploy不仅会安装controller、serviceaccount、role、rolebinding，它还会创建namespace，也会将crd安装一遍。也就是说deploy是一个完整的operator安装命令。</p>
<blockquote>
<p>注：使用make undeploy可以完整卸载operator相关resource。</p>
</blockquote>
<p>我们用kubectl logs查看一下controller的运行日志：</p>
<pre><code>$kubectl logs -f deployment.apps/webserver-operator-controller-manager -n webserver-operator-system
1.6600280818476188e+09    INFO    controller-runtime.metrics    Metrics server is starting to listen    {"addr": "127.0.0.1:8080"}
1.6600280818478029e+09    INFO    setup    starting manager
1.6600280818480284e+09    INFO    Starting server    {"path": "/metrics", "kind": "metrics", "addr": "127.0.0.1:8080"}
1.660028081848097e+09    INFO    Starting server    {"kind": "health probe", "addr": "[::]:8081"}
I0809 06:54:41.848093       1 leaderelection.go:248] attempting to acquire leader lease webserver-operator-system/63e5a746.my.domain...
I0809 06:54:57.072336       1 leaderelection.go:258] successfully acquired lease webserver-operator-system/63e5a746.my.domain
1.6600280970724037e+09    DEBUG    events    Normal    {"object": {"kind":"Lease","namespace":"webserver-operator-system","name":"63e5a746.my.domain","uid":"e05aaeb5-4a3a-4272-b036-80d61f0b6788","apiVersion":"coordination.k8s.io/v1","resourceVersion":"5238800"}, "reason": "LeaderElection", "message": "webserver-operator-controller-manager-6f45bc88f7-ptxlc_0e960015-9fbe-466d-a6b1-ff31af63a797 became leader"}
1.6600280970724993e+09    INFO    Starting EventSource    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "source": "kind source: *v1.WebServer"}
1.6600280970725305e+09    INFO    Starting Controller    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer"}
1.660028097173026e+09    INFO    Starting workers    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "worker count": 1}
</code></pre>
<p>可以看到，controller已经成功启动，正在等待一个WebServer CR的相关事件(比如创建)！下面我们就来创建一个WebServer CR!</p>
<h4>10. 创建WebServer CR</h4>
<p>webserver-operator项目中有一个CR sample，位于config/samples下面，我们对其进行改造，添加我们在spec中加入的字段：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 3
</code></pre>
<p>我们通过kubectl创建该WebServer CR：</p>
<pre><code>$cd config/samples
$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample created
</code></pre>
<p>观察controller的日志：</p>
<pre><code>1.6602084232243123e+09  INFO    controllers.WebServer   Creating a new Deployment   {"Webserver": "default/webserver-sample", "Deployment.Namespace": "default", "Deployment.Name": "webserver-sample"}
1.6602084233446114e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到当CR被创建后，controller监听到相关事件，创建了对应的Deployment和service，我们查看一下为CR创建的Deployment、三个Pod以及service：</p>
<pre><code>$kubectl get service
NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes                 ClusterIP   172.26.0.1     &lt;none&gt;        443/TCP        22d
webserver-sample-service   NodePort    172.26.173.0   &lt;none&gt;        80:30010/TCP   2m58s

$kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
webserver-sample   3/3     3            3           4m44s

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          4m52s
</code></pre>
<p>我们访问一下该服务：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>服务如预期返回响应！</p>
<h4>11. 伸缩、变更版本和Service自愈</h4>
<p>接下来我们来对CR做一些常见的运维操作。</p>
<ul>
<li>副本数由3变为4</li>
</ul>
<p>我们将CR的replicas由3改为4，对容器实例做一次扩展操作：</p>
<pre><code>// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 4
</code></pre>
<p>然后通过kubectl apply使之生效：</p>
<pre><code>$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample configured
</code></pre>
<p>上述命令执行后，我们观察到operator的controller日志如下：</p>
<pre><code>1.660208962767797e+09   INFO    controllers.WebServer   Deployment spec.replicas change {"Webserver": "default/webserver-sample", "from": 3, "to": 4}
</code></pre>
<p>稍后，查看pod数量：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-v9gvg   1/1     Running   0          42s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          9m41s
</code></pre>
<p>webserver pod副本数量成功从3扩为4。</p>
<ul>
<li>变更webserver image版本</li>
</ul>
<p>我们将CR的image的版本从nginx:1.23.1改为nginx:1.23.0，然后执行kubectl apply使之生效。</p>
<p>我们查看controller的响应日志如下：</p>
<pre><code>1.6602090494113188e+09  INFO    controllers.WebServer   Deployment spec.template.spec.container[0].image change {"Webserver": "default/webserver-sample", "from": "nginx:1.23.1", "to": "nginx:1.23.0"}
</code></pre>
<p>controller会更新deployment，导致所辖pod进行滚动升级：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS              RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running             0          10m
webserver-sample-bc698b9fb-vk6gw   1/1     Running             0          10m
webserver-sample-bc698b9fb-xgrgb   1/1     Running             0          10m
webserver-sample-ffcf549ff-g6whk   0/1     ContainerCreating   0          12s
webserver-sample-ffcf549ff-ngjz6   0/1     ContainerCreating   0          12s
</code></pre>
<p>耐心等一小会儿，最终的pod列表为：</p>
<pre><code>$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-ffcf549ff-g6whk   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-m6z24   1/1     Running   0          3m12s
webserver-sample-ffcf549ff-ngjz6   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-t7gvc   1/1     Running   0          4m16s
</code></pre>
<ul>
<li>service自愈：恢复被无删除的Service</li>
</ul>
<p>我们来一次“误操作”，将webserver-sample-service删除，看看controller能否帮助service自愈：</p>
<pre><code>$kubectl delete service/webserver-sample-service
service "webserver-sample-service" deleted
</code></pre>
<p>查看controller日志：</p>
<pre><code>1.6602096994710526e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}
</code></pre>
<p>我们看到controller检测到了service被删除的状态，并重建了一个新service！</p>
<p>访问新建的service：</p>
<pre><code>$curl http://192.168.10.182:30010
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>可以看到service在controller的帮助下完成了自愈！</p>
<h3>五. 小结</h3>
<p>本文对Kubernetes Operator的概念以及优点做了初步的介绍，并基于kubebuilder这个工具开发了一个具有2级能力的operator。当然这个operator离完善还有很远的距离，其主要目的还是帮助大家理解operator的概念以及实现套路。</p>
<p>相信你阅读完本文后，对operator，尤其是其基本结构会有一个较为清晰的了解，并具备开发简单operator的能力！</p>
<p>文中涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/webserver-operator">这里</a>下载 &#8211; https://github.com/bigwhite/experiments/tree/master/webserver-operator。</p>
<h3>六. 参考资料</h3>
<ul>
<li>kubernetes operator 101, Part 1: Overview and key features &#8211; https://developers.redhat.com/articles/2021/06/11/kubernetes-operators-101-part-1-overview-and-key-features</li>
<li>Kubernetes Operators 101, Part 2: How operators work &#8211; https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work</li>
<li>Operator SDK: Build Kubernetes Operators &#8211; https://developers.redhat.com/blog/2020/04/28/operator-sdk-build-kubernetes-operators-and-deploy-them-on-openshift</li>
<li>kubernetes doc: Custom Resources &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/</li>
<li>kubernetes doc: Operator pattern &#8211; https://kubernetes.io/docs/concepts/extend-kubernetes/operator/</li>
<li>kubernetes doc: API concepts &#8211; https://kubernetes.io/docs/reference/using-api/api-concepts/</li>
<li>Introducing Operators: Putting Operational Knowledge into Software 第一篇有关operator的文章 by coreos &#8211; https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html</li>
<li>CNCF Operator白皮书v1.0 &#8211; https://github.com/cncf/tag-app-delivery/blob/main/operator-whitepaper/v1/Operator-WhitePaper_v1-0.md</li>
<li>Best practices for building Kubernetes Operators and stateful apps &#8211; https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps</li>
<li>A deep dive into Kubernetes controllers &#8211;  https://docs.bitnami.com/tutorials/a-deep-dive-into-kubernetes-controllers</li>
<li>Kubernetes Operators Explained &#8211; https://blog.container-solutions.com/kubernetes-operators-explained</li>
<li>书籍《Kubernetes Operator》 &#8211; https://book.douban.com/subject/34796009/</li>
<li>书籍《Programming Kubernetes》 &#8211; https://book.douban.com/subject/35498478/</li>
<li>Operator SDK Reaches v1.0 &#8211; https://cloud.redhat.com/blog/operator-sdk-reaches-v1.0</li>
<li>What is the difference between kubebuilder and operator-sdk &#8211; https://github.com/operator-framework/operator-sdk/issues/1758</li>
<li>Kubernetes Operators in Depth &#8211; https://www.infoq.com/articles/kubernetes-operators-in-depth/</li>
<li>Get started using Kubernetes Operators &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/ </li>
<li>Use Kubernetes operators to extend Kubernetes’ functionality &#8211; https://developer.ibm.com/learningpaths/kubernetes-operators/operators-extend-kubernetes/</li>
<li>memcached operator &#8211; https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，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><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</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://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</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; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/feed/</wfw:commentRss>
		<slash:comments>12</slash:comments>
		</item>
		<item>
		<title>一文告诉你如何抢先体验Go泛型</title>
		<link>https://tonybai.com/2020/11/28/httpstonybai-com20201128how-to-experience-go-generics-first/</link>
		<comments>https://tonybai.com/2020/11/28/httpstonybai-com20201128how-to-experience-go-generics-first/#comments</comments>
		<pubDate>Fri, 27 Nov 2020 22:50:23 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[gitee]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[go2go]]></category>
		<category><![CDATA[goland]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[Gopher部落]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[playground]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[RussCox]]></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=3006</guid>
		<description><![CDATA[本文首发于我主持的“Gopher部落”知识星球，欢迎大家加入星球，一起学习Go语言！年底前8.8折优惠，不要错过哦！ 2020年11月22日，Go核心开发团队技术负责人Russ Cox在golang-dev论坛上确认了Go泛型将在Go 1.18落地(2022.2)： 这对于那些迫切期盼go加入泛型的gopher来说无疑是一个重大利好消息！不过，泛型是把双刃剑！泛型的加入势必会让Go语言的复杂性大幅提升。我很是担心Go加入泛型后会像C++模板那样被“滥用”而形成很多奇技淫巧，这显然不是Go项目组想看到的。因此他们现在在宣传泛型时都是比较谨慎的。Robert Griesemer在GopherCon 2020大会上演讲“Typing [Generic] Go”中明确给出了Go泛型的使用时机： 可增强静态类型安全性的时候 可以更高效的使用内存的时候 可以(显著的)提升性能的时候 虽然这不能完全避免滥用，但至少表明了Go团队对泛型使用的态度。“能力越大，责任越大”，大家在使用泛型时务必三思而后行！ 现在，Go泛型已经处于“箭在弦上不得不发”的状态，作为Gopher，我们能做的就是拥抱它！ 离Go 1.18发布还有一年多的时间，对于极其渴望支持泛型的gopher来说，这个时间有点长！好在Go项目组已经提供了一些抢先体验Go泛型语法的方法，这里我们就来全面介绍一下，小伙伴们可以根据自己的情况任选一种抢先体验Go泛型！ 1. Go泛型在线playground 2020.6月末，Ian Lance Taylor和Robert Griesemer在Go官方博客发表了文章《The Next Step for Generics》，介绍了Go泛型工作的最新进展。同时，Go团队还推出了可以在线试验Go泛型语法的playground：https://go2goplay.golang.org： 通过该在线playground，我们可以体验最新的Go泛型语法并查看编译和运行结果。 在线playground的好处就在于可以随时随地访问和体验，体验设备也不局限于计算机，甚至可以使用手机/平板终端。不过该playground在国内访问不畅，并且体验仅局限于单文件的形式，对于复杂一些的项目无法支持。 2. 基于源码编译出go2go工具 Go项目在dev.go2go分支上加入了Go泛型语法的实现，我们可以在本地基于Go项目源码构建出可以用于体验Go泛型的go2go工具。 要想构建go2go工具，我们首先就需要下载Go项目源码。但截至目前，Go项目仓库github.com/golang/go有45000多次提交，在国内以20k/s的速度clone这个仓库那是相当耗时费力，还不一定有好结果（经常断连，一断连，就要重新来过）。当然如果你有高速vpn则另当别论了。这里介绍一个下载github上Go项目源码的过渡方法：利用gitee(码云)建立Go仓库镜像库，然后从码云以2M/s速度下载。步骤如下： 在gitee上建立一个公共仓库，比如：gitee.com/bigwhite/go，在建立仓库时选择“导入现有库”，填入现有库的地址：https:///github.com/golang.go.git，之后，强大的“码云”就会帮助我们快速同步了。 之后我们就可以从码云clone这个仓库：gitee.com/bigwhite/go，2M/s的速度，一分钟内就完成clone。并且码云支持强制从源仓库github.com/golang/go同步最新更新到镜像仓库，十分方便。 $git clone https://gitee.com/bigwhite/go.git 既然我已经在码云建立的go仓库的镜像，各位小伙伴儿们就可以直接clone我的公共库(https://gitee.com/bigwhite/go)来获取go仓库源码了。 接下来，我们来构建go2go工具，主要步骤如下(当前环境为ubuntu，并已安装的go的版本为go 1.15.4 linux/amd64)： 切换到dev.go2go分支 // 进入下载后的go仓库源码目录(我这里为~/.bin/go) $git checkout dev.go2go Branch 'dev.go2go' set up to track remote branch [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/how-to-experience-go-generics-first-2.png" alt="img{512x368}" /></p>
<p>本文首发于我主持的<a href="https://articles.zsxq.com/id_bjzje91weqn7.html">“Gopher部落”知识星球</a>，欢迎大家加入星球，一起学习Go语言！年底前8.8折优惠，不要错过哦！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>2020年11月22日，Go核心开发团队技术负责人<a href="http://swtch.com/%7Ersc/">Russ Cox</a>在<a href="https://groups.google.com/g/golang-dev/c/U7eW9i0cqmo/m/ffs0tyIYBAAJ?pli=1">golang-dev论坛</a>上确认了Go泛型将在Go 1.18落地(2022.2)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-generics-at-gophercon-2020-12.png" alt="img{512x368}" /></p>
<p>这对于那些迫切期盼go加入泛型的gopher来说无疑是一个重大利好消息！不过，泛型是把双刃剑！泛型的加入势必会让Go语言的复杂性大幅提升。我很是担心Go加入泛型后会像C++模板那样被“滥用”而形成很多<strong>奇技淫巧</strong>，这显然不是Go项目组想看到的。因此他们现在在宣传泛型时都是比较谨慎的。<a href="https://github.com/griesemer">Robert Griesemer</a>在<a href="https://www.gophercon.com/">GopherCon 2020</a>大会上演讲<a href="https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA">“Typing [Generic] Go”</a>中明确给出了Go泛型的使用时机：</p>
<ul>
<li>可增强静态类型安全性的时候</li>
<li>可以更高效的使用内存的时候</li>
<li>可以(显著的)提升性能的时候</li>
</ul>
<p>虽然这不能完全避免滥用，但至少表明了Go团队对泛型使用的态度。<a href="https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA">“能力越大，责任越大”</a>，大家在使用泛型时务必<strong>三思而后行</strong>！</p>
<p>现在，Go泛型已经处于“箭在弦上不得不发”的状态，作为Gopher，我们能做的就是拥抱它！</p>
<p>离Go 1.18发布还有一年多的时间，对于极其渴望支持泛型的gopher来说，这个时间有点长！好在Go项目组已经提供了一些抢先体验Go泛型语法的方法，这里我们就来全面介绍一下，小伙伴们可以根据自己的情况任选一种抢先体验Go泛型！</p>
<h3>1. Go泛型在线playground</h3>
<p>2020.6月末，Ian Lance Taylor和Robert Griesemer在Go官方博客发表了文章<a href="http://blog.golang.org/generics-next-step">《The Next Step for Generics》</a>，介绍了Go泛型工作的最新进展。同时，Go团队还推出了可以<a href="https://go2goplay.golang.org">在线试验Go泛型语法的playground</a>：https://go2goplay.golang.org：</p>
<p><img src="https://tonybai.com/wp-content/uploads/how-to-experience-go-generics-first-1.png" alt="img{512x368}" /></p>
<p>通过该在线playground，我们可以体验最新的Go泛型语法并查看编译和运行结果。</p>
<p>在线playground的好处就在于可以随时随地访问和体验，体验设备也不局限于计算机，甚至可以使用手机/平板终端。不过<strong>该playground在国内访问不畅</strong>，并且体验仅局限于单文件的形式，对于复杂一些的项目无法支持。</p>
<h3>2. 基于源码编译出go2go工具</h3>
<p>Go项目在<strong>dev.go2go</strong>分支上加入了Go泛型语法的实现，我们可以在本地基于Go项目源码构建出可以用于体验Go泛型的go2go工具。</p>
<p>要想构建go2go工具，我们<strong>首先就需要下载Go项目源码</strong>。但截至目前，<a href="https://github.com/golang/go">Go项目仓库</a><strong>github.com/golang/go</strong>有45000多次提交，在国内以20k/s的速度clone这个仓库那是相当耗时费力，还不一定有好结果（经常断连，一断连，就要重新来过）。当然如果你有高速vpn则另当别论了。这里介绍一个下载github上Go项目源码的过渡方法：<strong>利用gitee(码云)建立Go仓库镜像库，然后从码云以2M/s速度下载</strong>。步骤如下：</p>
<ul>
<li>
<p>在gitee上建立一个公共仓库，比如：<strong>gitee.com/bigwhite/go</strong>，在建立仓库时选择“导入现有库”，填入现有库的地址：<strong>https:///github.com/golang.go.git</strong>，之后，强大的“码云”就会帮助我们快速同步了。</p>
</li>
<li>
<p>之后我们就可以从码云clone这个仓库：<strong>gitee.com/bigwhite/go</strong>，2M/s的速度，一分钟内就完成clone。并且码云支持强制从源仓库github.com/golang/go同步最新更新到镜像仓库，十分方便。</p>
</li>
</ul>
<pre><code>$git clone https://gitee.com/bigwhite/go.git
</code></pre>
<blockquote>
<p>既然我已经在码云建立的go仓库的镜像，各位小伙伴儿们就可以直接clone我的公共库(https://gitee.com/bigwhite/go)来获取go仓库源码了。</p>
</blockquote>
<p>接下来，我们来构建go2go工具，主要步骤如下(当前环境为ubuntu，并已安装的go的版本为<a href="https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ">go 1.15.4</a> linux/amd64)：</p>
<ul>
<li>切换到dev.go2go分支</li>
</ul>
<pre><code>// 进入下载后的go仓库源码目录(我这里为~/.bin/go)
$git checkout dev.go2go
Branch 'dev.go2go' set up to track remote branch 'dev.go2go' from 'origin'.
Switched to a new branch 'dev.go2go'
</code></pre>
<blockquote>
<p>注：ubuntu需安装<strong>build-essential</strong>(apt-get install build-essential)，否则在go源码编译过程可能会出现“fatal error: stdlib.h: No such file or directory”的错误。</p>
</blockquote>
<ul>
<li>编译dev.go2go分支源码</li>
</ul>
<p>Go源码编译是“一键式”的，并且速度非常快！进入到Go项目源码下的src目录(cd ~/.bin/go/src)，执行下面命令：</p>
<pre><code>$./all.bash 

Building Go cmd/dist using /root/.bin/go1.15.4. (go1.15.4 linux/amd64)
Building Go toolchain1 using /root/.bin/go1.15.4.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.
... ...
ALL TESTS PASSED
---
Installed Go for linux/amd64 in /root/.bin/go
Installed commands in /root/.bin/go/bin
*** You need to add /root/.bin/go/bin to your PATH.
</code></pre>
<p>构建后的可执行文件go与gofmt被放在了bin目录下(~/go/bin)，为方便使用，我们最好将其所在路径配置到<strong>PATH</strong>环境变量中。。</p>
<ul>
<li>验证构建结果</li>
</ul>
<pre><code>$go version
go version devel +440f144a10 Tue Nov 24 01:29:01 2020 +0000 linux/amd64
</code></pre>
<p>如果看到上面结果，说明构建是ok的。</p>
<p>接下来，我们就来使用构建出的go工具体验一下编译运行一个使用泛型语法编写的源文件<strong>sort.go2</strong>：</p>
<pre><code>// sort.go2

package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

type sliceFn[T any] struct {
    s   []T
    cmp func(T, T) bool
}

func (s sliceFn[T]) Len() int           { return len(s.s) }
func (s sliceFn[T]) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) }
func (s sliceFn[T]) Swap(i, j int)      { s.s[i], s.s[j] = s.s[j], s.s[i] }

func SliceFn[T any](s []T, cmp func(T, T) bool) {
    sort.Sort(sliceFn[T]{s, cmp})
}

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }

    SliceFn(langs, func(p1, p2 Lang) bool { return p1.Rank &lt; p2.Rank })
    fmt.Println(langs) // [{go 1} {rust 2} {swift 3}]
}
</code></pre>
<p>go2go是以go tool的一个子命令形式存在的，它支持编译和运行以<strong>.go2</strong>为后缀的Go源文件，如果让它编译和运行<strong>.go</strong>文件，它会报如下错误：</p>
<pre><code>$go tool go2go run sort.go
Go file sort.go was not created by go2go
</code></pre>
<p>编译运行上面的sort.go2的命令和结果如下：</p>
<pre><code>$go tool go2go run sort.go2
[{go 1} {rust 2} {swift 3}]
</code></pre>
<p>有小伙伴可能会说，这个例子也是单一源文件，太简单！那我们接下来就整一个稍复杂些的。go2go这个子命令自带了一些复杂的Go泛型包，这些包的源码被放在了Go仓库源码的<strong>src/cmd/go2go/testdata</strong>下面：</p>
<pre><code>$tree -LF 2 go2path
go2path
└── src/
    ├── alg/
    ├── chans/
    ├── constraints/
    ├── graph/
    ├── gsort/
    ├── list/
    ├── maps/
    ├── metrics/
    ├── orderedmap/
    ├── sets/
    └── slices/
</code></pre>
<p>go2go目前仅支持gopath mode，还不支持<a href="https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/">module-ware mode</a>。go2go支持专用的GO2PATH环境变量用于指示GOPATH路径，也可以用传统的GOPATH环境变量。为了使用go2go自带的那些样例源码包，我们需要将GOPATH或GO2PATH设置为<strong>\$GOROOT/src/cmd/go2go/testdata/go2path</strong>。我们在go2path路径下建立我们的样例repo：</p>
<pre><code>$tree -LF 5 go2path
go2path
└── src/
    │   ... ...
    ├── github.com/
    │   └── bigwhite/
    │       └── gsort-demo/
    │           └── demo.go2
    │   ... ...
    └── slices/
        ├── slices.go2
        └── slices_test.go2

// demo.go2
package main

import (
    "fmt"
    "gsort"
)

type Lang struct {
    Name string
    Rank int
}

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }

    gsort.SliceFn(langs, func(p1, p2 Lang) bool { return p1.Rank &lt; p2.Rank })
    fmt.Println(langs)
}
</code></pre>
<p>我们可以用两种方法运行demo.go2：</p>
<pre><code>// 设置GO2PATH：

~/.bin/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo$ GO2PATH=$GOROOT/src/cmd/go2go/testdata/go2path go tool go2go run demo.go2
[{go 1} {rust 2} {swift 3}]

或

// 设置GOPATH和关闭GO111MODULE：

~/.bin/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo$ GOPATH=$GOROOT/src/cmd/go2go/testdata/go2path GO111MODULE=off go tool go2go run demo.go2
[{go 1} {rust 2} {swift 3}]
</code></pre>
<p>通过源码构建go2go工具的方法是体验Go泛型最基本的方法，我们还可以定期更新Go项目源码以体验泛型草案的最新变化。我们还可以通过<strong>go doc cmd/go2go</strong>来查看go2go命令的文档。</p>
<h3>3. 使用go2go的docker容器</h3>
<p>如果觉得使用源码构建本地可用的go2go工具依然“门槛高”或者繁琐，那么可以利用一些gopher已经上传的现成的docker容器来构建使用了泛型语法的&#42;.go2文件。这里使用的是<strong>levonet/golang:go2go</strong>：</p>
<pre><code>$docker pull levonet/golang:go2go
</code></pre>
<ul>
<li>使用go2go容器编译运行单个&#42;.go2文件</li>
</ul>
<p>我们还以上面那个sort.go2为例，该文件可以放在任意目录下，然后我们在该文件所在目录下执行下面命令即可编译运行它：</p>
<pre><code>$ docker run --rm -v "$PWD":/go/src/myapp -w /go/src/myapp levonet/golang:go2go go tool go2go run sort.go2
[{go 1} {rust 2} {swift 3}]
</code></pre>
<p>这句docker run命令的含义是：将宿主机当前工作目录(即sort.go2所在目录)挂载到容器中的<strong>/go/src/myapp</strong>下面，并将<strong>/go/src/myapp</strong>作为当前工作目录，执行<strong>go tool go2go run sort.go2</strong>。</p>
<p>对于复杂的如上面的github.com/bigwhite/gsort-demo的例子，通过docker容器一样可以编译，只不过命令复杂一些：</p>
<pre><code>~/temp/github.com $docker run --rm -v "$PWD":/usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com -w /usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo -e GO2PATH="/usr/local/lib/go/src/cmd/go2go/testdata/go2path" levonet/golang:go2go go tool go2go run demo.go2
[{go 1} {rust 2} {swift 3}]
</code></pre>
<p>我们将github.com目录放在任意目录下，比如：~/temp，然后将当前目录挂载到容器的<strong>/usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com</strong>目录下，设定工作目录为<strong>/usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo</strong>，然后为容器新增以环境变量<strong>GO2PATH</strong>，这样我们就可以编译运行demo.go2了。</p>
<blockquote>
<p>注1：容器中的GOROOT为/usr/local/lib/go</p>
</blockquote>
<h3>4. 使用Goland体验Go泛型</h3>
<p>著名Go语言IDE产品goland也<a href="https://blog.jetbrains.com/go/2020/11/24/experimenting-with-go-type-parameters-generics-in-goland/">宣布支持体验最新的Go泛型语法</a>，由于笔者很少使用图形化的IDE，因此各位小伙伴可自行通过这篇博客https://blog.jetbrains.com/go/2020/11/24/experimenting-with-go-type-parameters-generics-in-goland/来了解具体情况。</p>
<h3>5. 参考资料</h3>
<ul>
<li>levonet/golang &#8211; https://hub.docker.com/r/levonet/golang</li>
<li>dev.go2go branch &#8211; https://go.googlesource.com/go/+/refs/heads/dev.go2go/README.go2go.md</li>
</ul>
<hr />
<p><strong>“Gopher部落”知识星球开球了！</strong>高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-column-pgo-with-qr-and-text.png" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，感谢小伙伴们学习支持！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</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://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/11/28/httpstonybai-com20201128how-to-experience-go-generics-first/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>后端程序员一定要看的语言大比拼：Java vs. Go vs. Rust</title>
		<link>https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/</link>
		<comments>https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/#comments</comments>
		<pubDate>Thu, 30 Apr 2020 17:01:20 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JVM]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Maven]]></category>
		<category><![CDATA[musl]]></category>
		<category><![CDATA[OpenJDK]]></category>
		<category><![CDATA[REST]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[STW]]></category>
		<category><![CDATA[wrk]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[性能基准]]></category>
		<category><![CDATA[递归]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2901</guid>
		<description><![CDATA[这是Java，Go和Rust之间的比较。这不是基准测试，更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较，当然还有一个小的基准测试，可以看到每秒处理的请求数量，我将尝试对这些数字进行有意义的解读。 为了尝试尽可能公平比较，我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单，它提供了三个REST服务端点(endpoint)。 Web服务提供的服务端点 这三个Web服务的代码仓库托管在github上。 编译后的二进制文件尺寸 有关如何构建二进制文件的一些信息。对于Java，我使用maven-shade-plugin和mvn package命令将所有内容构建到一个大的jar中。对于Go，我使用go build。最后，我使用了cargo build &#8211;release构建Rust服务的二进制文件。 每个程序的大小（以兆字节为单位） 编译后的文件大小还取决于所选的库/依赖项，因此，如果依赖项的身躯臃肿，则编译后的程序也将难以幸免。在我的特定情况下，针对我选择的特定库，以上是程序编译后的大小。 在后续的一个单独小节中，我会把这三个程序都构建并打包为docker镜像，并列出它们的大小，以显示每种语言所需的运行时开销。下面有更多详细信息。 内存使用情况 空闲状态 每个应用程序在内存空闲时的内存使用情况 什么？Go和Rust版本显示空闲时内存占用量的条形图在哪里？好了，它们在那里，只有JVM启动的程序在空闲状态时消耗160 MB以上的内存，它什么也没做。Go应用程序仅使用0.86 MB，Rust应用也仅使用了0.36 MB。这是一个巨大的差异！在这里，Java使用的内存比Go和Rust应用使用的内存高出两个数量级，只是空占着内存却什么都不做。那是巨大的资源浪费。 服务REST请求 让我们使用wrk发起访问API的请求，并观察内存和CPU使用情况，以及在我的计算机上三个版本程序的每个端点每秒处理的请求数。 wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35 上面的wrk命令使用两个线程并在连接池中保持400个打开的连接，并重复调用GET端点，持续30秒。这里我仅使用两个线程，因为wrk和被测程序都在同一台计算机上运行，所以我不希望它们在可用资源（尤其是CPU）上相互竞争（太多）。 每个Web服务都经过单独测试，并且在每次运行之间都重新启动了Web服务。 以下是该程序的每个版本的三个运行中的最佳结果。 /hello 该端点返回Hello，World！信息。它分配字符串“ Hello，World！” 并将其序列化并以JSON格式返回。 /hello端点的CPU使用率 /hello端点的内存使用情况 /hello端点处理的每秒请求数 /greeting/{name} 该端点接受一个段路径参数{name}，然后格式化字符串“Hello,{name}!”，序列化并以JSON格式的问候消息返回。 /greeting端点的CPU使用率 /greeting端点的内存使用情况 /greeting端点处理的每秒请求数 /fibonacci/{number} 该端点接受一个段路径参数{number}，并返回序列化为JSON格式的斐波纳契数和输入数。 对于这个特定的端点，我选择以递归形式实现它。我毫不怀疑，迭代实现会产生更好的性能结果，并且出于生产目的，应该选择一种迭代形式，但是在生产代码中，有些情况下必须使用递归（并非专门用于计算第n个斐波那契数 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-1.png" alt="" /></p>
<p>这是<a href="https://tonybai.com/tag/java">Java</a>，<a href="https://tonybai.com/tag/go">Go</a>和Rust之间的比较。这不是<a href="https://tonybai.com/2015/08/25/go-debugging-profiling-optimization/">基准测试</a>，更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较，当然还有一个小的基准测试，可以看到每秒处理的请求数量，我将尝试对这些数字进行有意义的解读。</p>
<p>为了尝试尽可能公平比较，我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单，它提供了三个REST服务端点(endpoint)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-2.png" alt="" /><br />
<center>Web服务提供的服务端点</center></p>
<p>这三个Web服务的代码仓库托管在<a href="https://github.com/dexterdarwich/ws-compare">github上</a>。</p>
<h2>编译后的二进制文件尺寸</h2>
<p>有关如何构建二进制文件的一些信息。对于Java，我使用<a href="http://maven.apache.org/plugins/maven-shade-plugin/">maven-shade-plugin</a>和<code>mvn package</code>命令将所有内容构建到一个大的jar中。对于Go，我使用go build。最后，我使用了cargo build &#8211;release构建Rust服务的二进制文件。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-3.png" alt="" /><br />
<center> 每个程序的大小（以兆字节为单位）</center></p>
<p>编译后的文件大小还取决于所选的库/依赖项，因此，如果依赖项的身躯臃肿，则编译后的程序也将难以幸免。在我的特定情况下，针对我选择的特定库，以上是程序编译后的大小。</p>
<p>在后续的一个单独小节中，我会把这三个程序都构建并打包为<a href="https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/">docker镜像</a>，并列出它们的大小，以显示每种语言所需的<a href="https://tonybai.com/2020/03/21/illustrated-tales-of-go-runtime-scheduler/">运行时</a>开销。下面有更多详细信息。</p>
<h2>内存使用情况</h2>
<h3>空闲状态</h3>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-4.png" alt="" /><br />
<center>每个应用程序在内存空闲时的内存使用情况</center></p>
<p>什么？Go和Rust版本显示空闲时内存占用量的条形图在哪里？好了，它们在那里，只有JVM启动的程序在空闲状态时消耗160 MB以上的内存，它什么也没做。Go应用程序仅使用0.86 MB，Rust应用也仅使用了0.36 MB。这是一个巨大的差异！在这里，Java使用的内存比Go和Rust应用使用的内存高出两个数量级，只是空占着内存却什么都不做。那是巨大的资源浪费。</p>
<h3>服务REST请求</h3>
<p>让我们使用<a href="https://github.com/wg/wrk">wrk</a>发起访问API的请求，并观察内存和CPU使用情况，以及在我的计算机上三个版本程序的每个端点每秒处理的请求数。</p>
<pre><code>wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello
wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane
wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35
</code></pre>
<p>上面的wrk命令使用两个线程并在连接池中保持400个打开的连接，并重复调用GET端点，持续30秒。这里我仅使用两个线程，因为wrk和被测程序都在同一台计算机上运行，所以我不希望它们在可用资源（尤其是CPU）上相互竞争（太多）。</p>
<p>每个Web服务都经过单独测试，并且在每次运行之间都重新启动了Web服务。</p>
<p>以下是该程序的每个版本的三个运行中的最佳结果。</p>
<ul>
<li>/hello</li>
</ul>
<p>该端点返回Hello，World！信息。它分配字符串“ Hello，World！” 并将其序列化并以JSON格式返回。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-5.png" alt="" /><br />
<center>/hello端点的CPU使用率</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-6.png" alt="" /><br />
<center>/hello端点的内存使用情况</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-7.png" alt="" /><br />
<center>/hello端点处理的每秒请求数</center></p>
<ul>
<li>/greeting/{name}</li>
</ul>
<p>该端点接受一个段路径参数{name}，然后格式化字符串“Hello,{name}!”，序列化并以JSON格式的问候消息返回。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-8.png" alt="" /><br />
<center>/greeting端点的CPU使用率</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-9.png" alt="" /><br />
<center>/greeting端点的内存使用情况</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-10.png" alt="" /><br />
<center>/greeting端点处理的每秒请求数</center></p>
<ul>
<li>/fibonacci/{number}</li>
</ul>
<p>该端点接受一个段路径参数{number}，并返回序列化为JSON格式的斐波纳契数和输入数。</p>
<p>对于这个特定的端点，我选择以递归形式实现它。我毫不怀疑，迭代实现会产生更好的性能结果，并且出于生产目的，应该选择一种迭代形式，但是在生产代码中，有些情况下必须使用递归（并非专门用于计算第n个斐波那契数 ）。为此，我希望该实现涉及大量CPU栈分配。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-11.png" alt="" /><br />
<center>/fibonacci端点的CPU使用率</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-12.png" alt="" /><br />
<center>/fibonacci端点的内存使用情况</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-13.png" alt="" /><br />
<center>/fibonacci端点处理的每秒请求数</center></p>
<p>在Fibonacci端点测试期间，Java是唯一一个有150个请求超时的实现，如下面wrk的输出所示。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-14.png" alt="" /><br />
<center>超时时间</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-15.png" alt="" /><br />
<center>/fibonacci端点的延迟</center></p>
<h2>运行时大小</h2>
<p>为了模拟现实世界中的云原生应用程序，并避免“它仅可以在我的机器上运行！”，我分别为这三个应用程序创建了一个docker镜像。</p>
<p>Docker文件的源代码包含在代码库相应程序文件夹下。</p>
<p>作为我使用过的Java应用程序的基础镜像，openjdk:8-jre-alpine是已知大小最小的镜像之一，但是，这附带了一些警告，这些警告可能适用于您的应用程序，也可能不适用于您的应用程序，主要是alpine镜像在处理环境变量名称方面不是posix兼容的，因此您不能在Dockerfile中使用ENV中的（点）字符（不过这没什么大不了的），另一个是alpine Linux镜像是使用musl libc而不是<a href="https://tonybai.com/tag/glibc">glibc</a>编译的，这意味着如果您的应用程序依赖于需要glibc，它可能无法正常工作。不过，在这里，alpine镜像工作是正常的。</p>
<p>至于应用程序的Go版本和Rust版本，我已经对其进行了静态编译，这意味着它们不希望在运行时镜像中存在libc（glibc，musl…等），这也意味着它们不需要运行OS的基本镜像。因此，我使用了scratch docker镜像，这是一个no-op镜像，以零开销托管已编译的可执行文件。</p>
<p>我使用的Docker镜像的命名约定为{lang}/webservice。该应用程序的Java，Go和Rust版本的镜像大小分别为113、8.68和4.24 MB。</p>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-16.png" alt="" /><br />
<center>最终Docker镜像大小</center></p>
<h2>结论</h2>
<p><img src="https://tonybai.com/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-17.png" alt="" /><br />
<center>三种语言的比较</center></p>
<p>在得出任何结论之前，我想指出这三种语言之间的关系。Java和Go都是支持垃圾回收的语言，但是Java会提前编译为在JVM上运行的字节码。启动Java应用程序时，JIT编译器会被调用以通过将字节码编译为本地代码来优化字节码，以提高应用程序的性能。</p>
<p>Go和Rust都提前编译为本地代码，并且在运行时不会进行进一步的优化。</p>
<p>Java和Go都是支持垃圾收集的语言，具有<strong>STW(停止世界)</strong>的副作用。这意味着，每当垃圾收集器运行时，它将停止应用程序，进行垃圾收集，并在完成后从停止的地方恢复应用程序。大多数垃圾收集器需要停止运行，但是有些实现似乎不需要这样做。</p>
<p>当Java语言在90年代创建时，其最大的卖点之一是<strong>一次编写，可在任何地方运行</strong>。当时这非常好，因为市场上没有很多虚拟化解决方案。如今，大多数CPU支持虚拟化，这种虚拟化抵消了使用某种语言进行开发的诱惑(该语言承诺可以运行在任何平台上)。Docker和其他解决方案以更为低廉的代价提供虚拟化。</p>
<p>在整个测试中，应用程序的Java版本比Go或Rust对应版本消耗了更多的内存，在前两个测试中，Java使用的内存大约增加了8000％。这意味着对于实际应用程序，Java应用程序的运行成本会更高。</p>
<p>对于前两个测试，Go应用程序使用的CPU比Java少20％，同时处理比java版多出38％的请求。另一方面，Rust版本使用的CPU比Go减少了57％，而处理的请求却增加了13％。</p>
<p>第三次测试在设计上是占用大量CPU的资源，因此我想从中挤出CPU的每一分。Go和Rust都比Java多使用了1％的CPU。而且我认为，如果wrk不是在同一台计算机上运行，那么这三个版本都会使CPU达到100%的上限值。在内存方面，Java使用的内存比Go和Rust多2000％。Java可以处理的请求比Go多出20％，而Rust可以处理的请求比Java多出15％。</p>
<p>在撰写本文时，Java编程语言已经存在了将近30年，这使得在市场上寻找Java开发人员变得相对容易。另一方面，Go和Rust都是相对较新的语言，因此与Java相比，自然而然的开发人员的数量更少些。不过，Go和Rust都拥有很大的吸引力，许多开发人员正在将它们用于新项目，并且有许多使用Go和Rust的生产中正在运行的项目，因为简单地说，就资源而言，它们比Java更有效。</p>
<p>在编写本文的程序时，我同时学习了Go和Rust。就我而言，Go的学习曲线很短，因为它是一种相对容易掌握的语言，并且与其他语言相比语法很小。我只用了几天就用Go编写了程序。关于Go需要注意的一件事是编译速度，我不得不承认，与Java/C/C++/Rust等其他语言相比，它的速度非常快。该程序的Rust版本花了我大约一个星期的时间来完成，我不得不说，大部分时间都花在弄清borrow checker向我要什么上。Rust具有严格的所有权规则，但是一旦掌握了Rust的所有权和借用概念，编译器错误消息就会突然变得更加有意义。违反借阅检查规则时，Rust编译器对您大吼的原因是因为编译器希望在编译时证明已分配内存的寿命和所有权。这样做可以保证程序的安全性（例如：没有悬挂的指针，除非使用了不安全(unsafe)的代码逃离检查），并且在编译时确定了释放位置，从而消除了垃圾收集器的需求和运行时成本。当然，这是以学习Rust的所有权系统为代价的。</p>
<p>在竞争方面，我认为Go是Java（通常是JVM语言）的直接竞争对手，但不是Rust的竞争对手。另一方面，Rust是Java，Go，C和C ++的重要竞争对手。</p>
<p>由于他们的效率，我看到了自己将会在Go和Rust中编写更多的程序，但是很可能在Rust中编写更多的程序。两者都非常适合Web服务，CLI，系统程序（..etc）开发。但是，Rust比Go具有根本优势。它不是垃圾收集的语言，与C和C++相比，它可以安全地编写代码。例如，Go并不是特别适合用于编写OS内核，而这里又是Rust的亮点，并与C/C ++竞争，因为它们是使用OS编写的长期存在和事实上的语言。Rust与C/C++竞争的另一种方式在嵌入式世界中，我将继续进行讨论。</p>
<p>感谢您的阅读！</p>
<p>本文翻译自<a href="https://medium.com/@dexterdarwich/comparison-between-java-go-and-rust-fdb21bd5fb7c">《Comparison between Java, Go, and Rust》</a>。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Kubernetes Deployment故障排除图解指南</title>
		<link>https://tonybai.com/2019/12/08/k8s-deployment-troubleshooting/</link>
		<comments>https://tonybai.com/2019/12/08/k8s-deployment-troubleshooting/#comments</comments>
		<pubDate>Sat, 07 Dec 2019 16:04:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[CrashLoopBackOff]]></category>
		<category><![CDATA[cronjob]]></category>
		<category><![CDATA[DaemonSet]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[ImagePullBackOff]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[ingress-nginx]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[label]]></category>
		<category><![CDATA[loadbalancer]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[RunContainerError]]></category>
		<category><![CDATA[selector]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[StatefulSet]]></category>
		<category><![CDATA[troubleshooting]]></category>
		<category><![CDATA[yaml]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[标签]]></category>
		<category><![CDATA[负载均衡]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2834</guid>
		<description><![CDATA[下面是一个示意图，可帮助你调试Kubernetes Deployment（你可以在此处下载它的PDF版本）。 当你希望在Kubernetes中部署应用程序时，你通常会定义三个组件： 一个Deployment &#8211; 这是一份用于创建你的应用程序的Pod副本的”食谱”； 一个Service &#8211; 一个内部负载均衡器，用于将流量路由到内部的Pod上； 一个Ingress &#8211; 描述如何流量应该如何从集群外部流入到集群内部的你的服务上。 下面让我们用示意图快速总结一下要点。 在Kubernetes中，你的应用程序通过两层负载均衡器暴露服务：内部的和外部的 内部的负载均衡器称为Service，而外部的负载均衡器称为Ingress Pod不会直接部署。Deployment会负责创建Pod并管理它们 假设你要部署一个简单的”HelloWorld”应用，该应用的YAML文件的内容应该类似下面这样： // hello-world.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-deployment labels: track: canary spec: selector: matchLabels: any-name: my-app template: metadata: labels: any-name: my-app spec: containers: - name: cont1 image: learnk8s/app:1.0.0 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-1.png" alt="img{512x368}" /></p>
<hr />
<p>下面是一个示意图，可帮助你调试Kubernetes Deployment（你可以在<a href="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-kubernetes.pdf">此处</a>下载它的PDF版本）。</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-2.png" alt="img{512x368}" /></p>
<p>当你希望在<a href="https://tonybai.com/tag/kubernetes">Kubernetes</a>中部署应用程序时，你通常会定义三个组件：</p>
<ul>
<li>一个<strong>Deployment</strong> &#8211; 这是一份用于创建你的应用程序的Pod副本的”食谱”；</li>
<li>一个<strong>Service</strong> &#8211; 一个内部负载均衡器，用于将流量路由到内部的Pod上；</li>
<li>一个<strong>Ingress</strong> &#8211; 描述如何流量应该如何从集群外部流入到集群内部的你的服务上。</li>
</ul>
<p>下面让我们用示意图快速总结一下要点。</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-3.png" alt="img{512x368}" /><br />
<center>在Kubernetes中，你的应用程序通过两层负载均衡器暴露服务：内部的和外部的</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-4.png" alt="img{512x368}" /><br />
<center>内部的负载均衡器称为Service，而外部的负载均衡器称为Ingress</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-5.png" alt="img{512x368}" /><br />
<center>Pod不会直接部署。Deployment会负责创建Pod并管理它们</center></p>
<p>假设你要部署一个简单的”HelloWorld”应用，该应用的YAML文件的内容应该类似下面这样：</p>
<pre><code>// hello-world.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    track: canary
spec:
  selector:
    matchLabels:
      any-name: my-app
  template:
    metadata:
      labels:
        any-name: my-app
    spec:
      containers:
      - name: cont1
        image: learnk8s/app:1.0.0
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    name: app
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
    paths:
    - backend:
        serviceName: app
        servicePort: 80
      path: /

</code></pre>
<p>这个定义很长，组件之间的相互关系并不容易看出来。</p>
<p>例如：</p>
<ul>
<li>什么时候应使用端口80，又是何时应使用端口8080？</li>
<li>你是否应该为每个服务创建一个新端口以免它们相互冲突？</li>
<li>标签(label)名重要吗？它们是否在每一处都应该是一样的？</li>
</ul>
<p>在进行调试之前，让我们回顾一下这三个组件是如何相互关联的。</p>
<p>让我们从Deployment和Service开始。</p>
<h2>一. 连接Deployment和Service</h2>
<p>令人惊讶的消息是，Service和Deployment之间根本没有连接。</p>
<p>事实是：Service直接指向Pod，并完全跳过了Deployment。</p>
<p>因此，你应该注意的是Pod和Service之间的相互关系。</p>
<p>你应该记住三件事：</p>
<ul>
<li>Service selector应至少与Pod的一个标签匹配；</li>
<li>Service的<strong>targetPort</strong>应与Pod中容器的<strong>containerPort</strong>匹配；</li>
<li>Service的<strong>port</strong>可以是任何数字。多个Service可以使用同一端口号，因为它们被分配了不同的IP地址。</li>
</ul>
<p>下面的图总结了如何连接端口：</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-6.png" alt="img{512x368}" /><br />
<center>考虑上面被一个服务暴露的Pod</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-7.png" alt="img{512x368}" /><br />
<center>创建Pod时，应为Pod中的每个容器定义containerPort端口</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-8.png" alt="img{512x368}" /><br />
<center>当创建一个Service时，你可以定义port和targetPort，但是哪个用来连接容器呢？</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-9.png" alt="img{512x368}" /><br />
<center>targetPort和containerPort应该始终保持匹配</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-10.png" alt="img{512x368}" /><br />
<center>如果容器暴露3000端口(containerPort)，那么targetPort应该匹配这一个端口号</center></p>
<p>再来看看YAML，标签和ports/targetPort应该匹配：</p>
<pre><code>// hello-world.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    track: canary
spec:
  selector:
    matchLabels:
      any-name: my-app
  template:
    metadata:
      labels:
        any-name: my-app
    spec:
      containers:
      - name: cont1
        image: learnk8s/app:1.0.0
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    any-name: my-app
</code></pre>
<p>那deployment顶部的<strong>track: canary</strong>标签呢?</p>
<p>它也应该匹配吗？</p>
<p>该标签属于deployment，service的选择器未使用它来路由流量。</p>
<p>换句话说，你可以安全地删除它或为其分配其他值。</p>
<p>那<strong>matchLabels</strong>选择器呢？</p>
<p><strong>它必须始终与Pod的标签匹配</strong>，并且被Deployment用来跟踪Pod。</p>
<p>假设你已经进行了所有正确的设置，该如何测试它呢？</p>
<p>你可以使用以下命令检查Pod是否具有正确的标签：</p>
<pre><code>$ kubectl get pods --show-labels
</code></pre>
<p>或者，如果你拥有属于多个应用程序的Pod：</p>
<pre><code>$ kubectl get pods --selector any-name=my-app --show-labels
</code></pre>
<p><strong>any-name=my-app</strong>就是标签：<strong>any-name: my-app</strong>。</p>
<p>还有问题吗？</p>
<p>你也可以连接到Pod！</p>
<p>你可以使用kubectl中的port-forward命令连接到service并测试连接。</p>
<pre><code>$ kubectl port-forward service/&lt;service name&gt; 3000:80
</code></pre>
<ul>
<li>service/<service name> 是服务的名称- 在上面的YAML中是“my-service”</li>
<li>3000是你希望在计算机上打开的端口</li>
<li>80是service通过port字段暴露的端口</li>
</ul>
<p>如果可以连接，则说明设置正确。</p>
<p>如果不行，则很可能是你填写了错误的标签或端口不匹配。</p>
<h2>二. 连接Service和Ingress</h2>
<p>接下来是配置Ingress以将你的应用暴露到集群外部。</p>
<p>Ingress必须知道如何检索服务，然后检索Pod并将流量路由给它们。</p>
<p>Ingress按名字和暴露的端口检索正确的服务。</p>
<p>在Ingress和Service中应该匹配两件事：</p>
<ul>
<li>Ingress的<strong>servicePort</strong>应该匹配service的<strong>port</strong>；</li>
<li>Ingress的<strong>serviceName</strong>应该匹配服务的<strong>name</strong>。</li>
</ul>
<p>下面的图总结了如何连接端口：</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-11.png" alt="img{512x368}" /><br />
<center>你已经知道servive暴露一个port</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-12.png" alt="img{512x368}" /><br />
<center>Ingress有一个字段叫servicePort</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-13.png" alt="img{512x368}" /><br />
<center>service的port和Ingress的service应该始终保持匹配</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-14.png" alt="img{512x368}" /><br />
<center>如果你为service指定的port是80，那么你也应该将ingress的servicePort改为80</center></p>
<p>实践中，你应该查看以下几行(下面代码中的my-service和80)：</p>
<pre><code>// hello-world.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-service   --- 需关注
spec:
  ports:
  - port: 80       --- 需关注
    targetPort: 8080
  selector:
    any-name: my-app
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
    paths:
    - backend:
        serviceName: my-service --- 需关注
        servicePort: 80 --- 需关注
      path: /
</code></pre>
<p>你如何测试Ingress是否正常工作呢？</p>
<p>你可以使用与以前相同的策略kubectl port-forward，但是这次你应该连接到Ingress控制器，而不是连接到Service。</p>
<p>首先，使用以下命令检索Ingress控制器的Pod名称：</p>
<pre><code>$ kubectl get pods --all-namespaces
NAMESPACE   NAME                              READY STATUS
kube-system coredns-5644d7b6d9-jn7cq          1/1   Running
kube-system etcd-minikube                     1/1   Running
kube-system kube-apiserver-minikube           1/1   Running
kube-system kube-controller-manager-minikube  1/1   Running
kube-system kube-proxy-zvf2h                  1/1   Running
kube-system kube-scheduler-minikube           1/1   Running
kube-system nginx-ingress-controller-6fc5bcc  1/1   Running
</code></pre>
<p>标识Ingress Pod（可能在其他命名空间中）并描述它以检索端口：</p>
<pre><code>$ kubectl describe pod nginx-ingress-controller-6fc5bcc \
 --namespace kube-system \
 | grep Ports
Ports:         80/TCP, 443/TCP, 18080/TCP
</code></pre>
<p>最后，连接到Pod：</p>
<pre><code>$ kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system
</code></pre>
<p>此时，每次你访问计算机上的端口3000时，请求都会转发到Ingress控制器Pod上的端口80。</p>
<p>如果访问http://localhost:3000，则应找到提供网页服务的应用程序。</p>
<h3>回顾Port</h3>
<p>快速回顾一下哪些端口和标签应该匹配：</p>
<ul>
<li>service selector应与Pod的标签匹配</li>
<li>service的targetPort应与Pod中容器的containerPort匹配</li>
<li>service的端口可以是任何数字。多个服务可以使用同一端口，因为它们分配了不同的IP地址。</li>
<li>ingress的servicePort应该匹配service的port</li>
<li>serivce的名称应与ingress中的serviceName字段匹配</li>
</ul>
<p>知道如何构造YAML定义只是故事的一部分。</p>
<p>出了问题后该怎么办？</p>
<p>Pod可能无法启动，或者正在崩溃。</p>
<h2>三. kubernetes deployment故障排除的3个步骤</h2>
<p>在深入研究失败的deployment之前，我们必须对Kubernetes的工作原理有一个明确定义的思维模型。</p>
<p>由于每个deployment中都有三个组件，因此你应该自下而上依次调试所有组件。</p>
<ul>
<li>你应该先确保Pods正在运行</li>
<li>然后，专注于让service将流量路由到到正确的Pod</li>
<li>然后，检查是否正确配置了Ingress</li>
</ul>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-15.png" alt="img{512x368}" /><br />
<center>你应该从底部开始对deployment进行故障排除。首先，检查Pod是否已就绪并正在运行。</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-16.png" alt="img{512x368}" /><br />
<center> 如果Pod已就绪，则应调查service是否可以将流量分配给Pod。</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-17.png" alt="img{512x368}" /><br />
<center>最后，你应该检查service与ingress之间的连接。</center></p>
<h3>1. Pod故障排除</h3>
<p>在大多数情况下，问题出在Pod本身。</p>
<p>你应该确保Pod正在运行并准备就绪。</p>
<p>该如何检查呢？</p>
<pre><code>$ kubectl get pods
NAME                    READY STATUS            RESTARTS  AGE
app1                    0/1   ImagePullBackOff  0         47h
app2                    0/1   Error             0         47h
app3-76f9fcd46b-xbv4k   1/1   Running           1         47h
</code></pre>
<p>在上述会话中，最后一个Pod处于就绪并正常运行的状态；但是，前两个Pod既不处于Running也不是Ready。</p>
<p>你如何调查出了什么问题？</p>
<p>有四个有用的命令可以对Pod进行故障排除：</p>
<ul>
<li>kubectl logs
<pod name> 有助于检索Pod容器的日志</li>
<li>kubectl describe pod
<pod name> 检索与Pod相关的事件列表很有用</li>
<li>kubectl get pod
<pod name> 用于提取存储在Kubernetes中的Pod的YAML定义</li>
<li>kubectl exec -ti
<pod name> bash 在Pod的一个容器中运行交互式命令很有用</li>
</ul>
<p>应该使用哪一个呢？</p>
<p>没有一种万能的。</p>
<p>相反，我们应该结合着使用它们。</p>
<h4>常见Pod错误</h4>
<p>Pod可能会出现启动和运行时错误。</p>
<p>启动错误包括：</p>
<ul>
<li>ImagePullBackoff</li>
<li>ImageInspectError</li>
<li>ErrImagePull</li>
<li>ErrImageNeverPull</li>
<li>RegistryUnavailable</li>
<li>InvalidImageName</li>
</ul>
<p>运行时错误包括：</p>
<ul>
<li>CrashLoopBackOff</li>
<li>RunContainerError</li>
<li>KillContainerError</li>
<li>VerifyNonRootError</li>
<li>RunInitContainerError</li>
<li>CreatePodSandboxError</li>
<li>ConfigPodSandboxError</li>
<li>KillPodSandboxError</li>
<li>SetupNetworkError</li>
<li>TeardownNetworkError</li>
</ul>
<p>有些错误比其他错误更常见。</p>
<p>以下是最常见的错误列表以及如何修复它们的方法。</p>
<h4>ImagePullBackOff</h4>
<p>当Kubernetes无法获取到Pod中某个容器的镜像时，将出现此错误。</p>
<p>共有三个可能的原因：</p>
<ul>
<li>镜像名称无效-例如，你拼错了名称，或者image不存在</li>
<li>你为image指定了不存在的标签</li>
<li>你尝试检索的image属于一个私有registry，而Kubernetes没有凭据可以访问它</li>
</ul>
<p>前两种情况可以通过更正image名称和标记来解决。</p>
<p>针对第三种情况，你应该将私有registry的访问凭证通过Secret添加到k8s中并在Pod中引用它。</p>
<p><a href="https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/">官方文档中有一个有关如何实现此目标的示例</a>。</p>
<h4>CrashLoopBackOff</h4>
<p>如果容器无法启动，则Kubernetes将显示错误状态为：CrashLoopBackOff。</p>
<p>通常，在以下情况下容器无法启动：</p>
<ul>
<li>应用程序中存在错误，导致无法启动</li>
<li>你<a href="https://stackoverflow.com/questions/41604499/my-kubernetes-pods-keep-crashing-with-crashloopbackoff-but-i-cant-find-any-lo">未正确配置容器</a></li>
<li>Liveness探针失败太多次</li>
</ul>
<p>你应该尝试从该容器中检索日志以调查其失败的原因。</p>
<p>如果由于容器重新启动太快而看不到日志，则可以使用以下命令：</p>
<pre><code>$ kubectl logs &lt;pod-name&gt; --previous
</code></pre>
<p>这个命令打印前一个容器的错误消息。</p>
<h4>RunContainerError</h4>
<p>当容器无法启动时，出现此错误。</p>
<p>甚至在容器内的应用程序启动之前。</p>
<p>该问题通常是由于配置错误，例如：</p>
<ul>
<li>挂载不存在的卷，例如ConfigMap或Secrets</li>
<li>将只读卷安装为可读写</li>
</ul>
<p>你应该使用kubectl describe pod
<pod-name> 命令收集和分析错误。</p>
<h4>处于Pending状态的Pod</h4>
<p>当创建Pod时，该Pod保持Pending状态。</p>
<p>为什么？</p>
<p>假设你的调度程序组件运行良好，可能的原因如下：</p>
<ul>
<li>集群没有足够的资源（例如CPU和内存）来运行Pod</li>
<li>当前的命名空间具有ResourceQuota对象，创建Pod将使命名空间超过配额</li>
<li>该Pod绑定到一个处于pending状态的 PersistentVolumeClaim</li>
</ul>
<p>最好的选择是检查kubectl describe命令输出的“事件”部分内容：</p>
<pre><code>$ kubectl describe pod &lt;pod name&gt;
</code></pre>
<p>对于因ResourceQuotas而导致的错误，可以使用以下方法检查集群的日志：</p>
<pre><code>$ kubectl get events --sort-by=.metadata.creationTimestamp
</code></pre>
<h4>处于未就绪状态的Pod</h4>
<p>如果Pod正在运行但未就绪(not ready)，则表示readiness就绪探针失败。</p>
<p>当“就绪”探针失败时，Pod未连接到服务，并且没有流量转发到该实例。</p>
<p>就绪探针失败是应用程序的特定错误，因此你应检查kubectl describe中的“ 事件”部分以识别错误。</p>
<h3>2. 服务的故障排除</h3>
<p>如果你的Pod正在运行并处于就绪状态，但仍无法收到应用程序的响应，则应检查服务的配置是否正确。</p>
<p>service旨在根据流量的标签将流量路由到Pod。</p>
<p>因此，你应该检查的第一件事是服务关联了多少个Pod。</p>
<p>你可以通过检查服务中的端点(endpoint)来做到这一点：</p>
<pre><code>$ kubectl describe service &lt;service-name&gt; | grep Endpoints
</code></pre>
<p>端点是一对<ip address:port>，并且在服务（至少）以Pod为目标时，应该至少有一个端点。</p>
<p>如果“端点”部分为空，则有两种解释：</p>
<ul>
<li>你没有运行带有正确标签的Pod（提示：你应检查自己是否在正确的命名空间中）</li>
<li>service的selector标签上有错字</li>
</ul>
<p>如果你看到端点列表，但仍然无法访问你的应用程序，则targetPort可能是你服务中的罪魁祸首。</p>
<p>你如何测试服务？</p>
<p>无论服务类型如何，你都可以使用kubectl port-forward来连接它：</p>
<pre><code>$kubectl port-forward service/&lt;service-name&gt; 3000:80
</code></pre>
<p>这里：</p>
<ul>
<li><service-name> 是服务的名称</li>
<li>3000 是你希望在计算机上打开的端口</li>
<li>80 是服务公开的端口</li>
</ul>
<h3>3.Ingress的故障排除</h3>
<p>如果你已到达本节，则：</p>
<ul>
<li>Pod正在运行并准备就绪</li>
<li>服务会将流量分配到Pod</li>
</ul>
<p>但是你仍然看不到应用程序的响应。</p>
<p>这意味着最有可能是Ingress配置错误。</p>
<p>由于正在使用的Ingress控制器是集群中的第三方组件，因此有不同的调试技术，具体取决于Ingress控制器的类型。</p>
<p>但是在深入研究Ingress专用工具之前，你可以用一些简单的方法进行检查。</p>
<p>Ingress使用serviceName和servicePort连接到服务。</p>
<p>你应该检查这些配置是否正确。</p>
<p>你可以通过下面命令检查Ingress配置是否正确：</p>
<pre><code>$kubectl describe ingress &lt;ingress-name&gt;
</code></pre>
<p>如果backend一列为空，则配置中必然有一个错误。</p>
<p>如果你可以在“backend”列中看到端点，但是仍然无法访问该应用程序，则可能是以下问题：</p>
<ul>
<li>你如何将Ingress暴露于公共互联网</li>
<li>你如何将集群暴露于公共互联网</li>
</ul>
<p>你可以通过直接连接到Ingress Pod来将基础结构问题与Ingress隔离开。</p>
<p>首先，获取你的Ingress控制器Pod（可以位于其他名称空间中）：</p>
<pre><code>$ kubectl get pods --all-namespaces
NAMESPACE   NAME                              READY STATUS
kube-system coredns-5644d7b6d9-jn7cq          1/1   Running
kube-system etcd-minikube                     1/1   Running
kube-system kube-apiserver-minikube           1/1   Running
kube-system kube-controller-manager-minikube  1/1   Running
kube-system kube-proxy-zvf2h                  1/1   Running
kube-system kube-scheduler-minikube           1/1   Running
kube-system nginx-ingress-controller-6fc5bcc  1/1   Running
</code></pre>
<p>描述它以检索端口：</p>
<pre><code># kubectl describe pod nginx-ingress-controller-6fc5bcc
 --namespace kube-system \
 | grep Ports
</code></pre>
<p>最后，连接到Pod：</p>
<pre><code>$ kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system
</code></pre>
<p>此时，每次你访问计算机上的端口3000时，请求都会转发到Pod上的端口80。</p>
<p>现在可以用吗？</p>
<ul>
<li>如果可行，则问题出在基础架构中。你应该调查流量如何路由到你的集群。</li>
<li>如果不起作用，则问题出在Ingress控制器中。你应该调试Ingress。</li>
</ul>
<p>如果仍然无法使Ingress控制器正常工作，则应开始对其进行调试。</p>
<p>目前有许多不同版本的Ingress控制器。</p>
<p>热门选项包括Nginx，HAProxy，Traefik等。</p>
<p>你应该查阅Ingress控制器的文档以查找故障排除指南。</p>
<p>由于<a href="https://github.com/kubernetes/ingress-nginx">Ingress Nginx</a>是最受欢迎的Ingress控制器，因此在下一部分中我们将介绍一些有关调试ingress-nginx的技巧。</p>
<h4>调试Ingress Nginx</h4>
<p>Ingress-nginx项目有一个Kubectl的<a href="https://kubernetes.github.io/ingress-nginx/kubectl-plugin/">官方插件</a>。</p>
<p>你可以用kubectl ingress-nginx来：</p>
<ul>
<li>检查日志，后端，证书等。</li>
<li>连接到ingress</li>
<li>检查当前配置</li>
</ul>
<p>你应该尝试的三个命令是：</p>
<ul>
<li>kubectl ingress-nginx lint，它会检查 nginx.conf</li>
<li>kubectl ingress-nginx backend，以检查后端（类似于kubectl describe ingress <ingress-name>）</li>
<li>kubectl ingress-nginx logs，查看日志</li>
</ul>
<blockquote>
<p>请注意，你可能需要为Ingress控制器指定正确的名称空间&#8211;namespace <name>。</p>
</blockquote>
<h2>四. 总结</h2>
<p>如果你不知道从哪里开始，那么在Kubernetes中进行故障排除可能是一项艰巨的任务。</p>
<p>你应该始终牢记从下至上解决问题：从Pod开始，然后通过Service和Ingress向上移动堆栈。</p>
<p>你在本文中了解到的调试技术也可以应用于其他对象，例如：</p>
<ul>
<li>failing Job和CronJob</li>
<li>StatefulSets和DaemonSets</li>
</ul>
<p><strong>本文翻译自<a href="https://learnk8s.io/">learnk8s</a>上的文章<a href="https://learnk8s.io/troubleshooting-deployments">A visual guide on troubleshooting Kubernetes deployments</a>。</strong></p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/12/08/k8s-deployment-troubleshooting/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>使用nomad在weave网络中部署工作负载</title>
		<link>https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/</link>
		<comments>https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/#comments</comments>
		<pubDate>Sat, 20 Apr 2019 04:15:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[CIDR]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[driver]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[iproute]]></category>
		<category><![CDATA[job]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[openssh-server]]></category>
		<category><![CDATA[overlay]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[proxy]]></category>
		<category><![CDATA[route]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[subnet]]></category>
		<category><![CDATA[task]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[virtualbox]]></category>
		<category><![CDATA[vm]]></category>
		<category><![CDATA[weave]]></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=2707</guid>
		<description><![CDATA[当初Kubernetes网络的设计目标是使得开发者使用pod时在网络这一层面可以像使用传统物理主机或虚拟机一样。具体的基本要求如下： 所有pod间均应可以在无需NAT的情况下直接通信； 所有集群节点与所有集群的Pod之间均应可以在无需NAT的情况下直接通信； 容器自身的地址和其他pod看到的它的地址是同一个地址； 按照这样的要求，集群中的每个pod都在一个平坦的、共享网络命名空间中，并且每个Pod都拥有一个IP，通信时无需端口映射。 用户也需要额外考虑如何建立Pod之间的连接，也不需要考虑将容器端口映射到主机端口等问题。基于这些要求而实现的k8s pod网络模型，将具有向后兼容的特性，可以使得Pod从某些角度上可以被看成是一个传统的物理主机或vm来对待。 在《使用nomad实现集群管理和微服务部署调度》一文中，我们看到nomad部署调度的driver为docker的服务实例都是通过主机和容器间的端口映射来对外提供服务的。服务实例多的时候，大量服务端口出现在眼前，我们很难用端口判断这是什么服务。并且通过映射端口暴露服务有局限，对于那些需要映射到主机固定端口的服务来说，很可能存在与其他服务的端口冲突而导致部署失败。除此之外，这种端口映射的方式还缺少隔离的作用，所有实例暴露的端口在同一个全局网络空间。 nomad是否可以像k8s一样将服务实例部署到overlay网络中从而实现每个服务实例所在container可以被看成一个独立的vm；并且我们还可以通过划分overlay的网段来隔离，实现某种意义上的“多租户”呢？在本篇文章中，我们来试验一下上述想法是否可行。 一、搭建试验环境 我们这次在一个VirtualBox搭建的三节点环境中进行验证。如果小伙伴对这段很熟悉，或者有现成的环境可用，那么可以跳过这一小节。另外这节不是重点，我不会对这个过程用过多文字做解释。 1. 创建虚机，组建网络 我们在一台ubuntu 18.04 desktop版本主机上搭建环境，所使用的软件版本信息如下： VirtualBox: 5.2.18 Guest OS: Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-142-generic x86_64) 组件环境的虚拟机和网络拓扑示意图如下： 如上图所示：三个vm 通过连入host-only网络(vboxnet0)实现内网通；通过连入NAT网络（NatNetwork）实现外网通。（怪异：在windows上的virtualbox实际上通过natnetwork即可实现全通的，无需host-only network，但是在ubuntu下居然不行）。 每个vm中网络配置如下： # cat /etc/network/interfaces # This file describes the network interfaces available on your system # and how to activate them. For more information, see [...]]]></description>
			<content:encoded><![CDATA[<p>当初<a href="https://tonybai.com/tag/kubernetes">Kubernetes</a>网络的设计目标是<strong>使得开发者使用pod时在网络这一层面可以像使用传统物理主机或虚拟机一样</strong>。具体的基本要求如下：</p>
<ul>
<li>所有pod间均应可以在无需NAT的情况下直接通信；</li>
<li>所有集群节点与所有集群的Pod之间均应可以在无需NAT的情况下直接通信；</li>
<li>容器自身的地址和其他pod看到的它的地址是同一个地址；</li>
</ul>
<p>按照这样的要求，集群中的每个pod都在一个平坦的、共享网络命名空间中，并且每个Pod都拥有一个IP，通信时无需端口映射。 用户也需要额外考虑如何建立Pod之间的连接，也不需要考虑将容器端口映射到主机端口等问题。基于这些要求而实现的k8s pod网络模型，将具有向后兼容的特性，可以使得Pod从某些角度上可以被看成是一个传统的物理主机或vm来对待。</p>
<p>在<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">《使用nomad实现集群管理和微服务部署调度》</a>一文中，我们看到nomad部署调度的driver为docker的服务实例都是通过主机和容器间的端口映射来对外提供服务的。服务实例多的时候，大量服务端口出现在眼前，我们很难用端口判断这是什么服务。并且通过映射端口暴露服务有局限，对于那些需要映射到主机固定端口的服务来说，很可能存在与其他服务的端口冲突而导致部署失败。除此之外，这种端口映射的方式还缺少隔离的作用，所有实例暴露的端口在同一个全局网络空间。</p>
<p>nomad是否可以像k8s一样将服务实例部署到overlay网络中从而实现每个服务实例所在container可以被看成一个独立的vm；并且我们还可以通过划分overlay的网段来隔离，实现某种意义上的“多租户”呢？在本篇文章中，我们来试验一下上述想法是否可行。</p>
<h2>一、搭建试验环境</h2>
<p>我们这次在一个<a href="https://www.virtualbox.org/">VirtualBox</a>搭建的三节点环境中进行验证。<strong>如果小伙伴对这段很熟悉，或者有现成的环境可用，那么可以跳过这一小节</strong>。另外这节不是重点，我不会对这个过程用过多文字做解释。</p>
<h3>1. 创建虚机，组建网络</h3>
<p>我们在一台<a href="https://tonybai.com/tag/ubuntu">ubuntu</a> 18.04 desktop版本主机上搭建环境，所使用的软件版本信息如下：</p>
<ul>
<li>
<p>VirtualBox: 5.2.18</p>
</li>
<li>
<p>Guest OS: Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-142-generic x86_64)</p>
</li>
</ul>
<p>组件环境的虚拟机和网络拓扑示意图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/virtualbox-3-vm-environment-for-nomad.png" alt="img{512x368}" /></p>
<p>如上图所示：三个vm 通过连入host-only网络(vboxnet0)实现内网通；通过连入NAT网络（NatNetwork）实现外网通。（怪异：在windows上的virtualbox实际上通过natnetwork即可实现全通的，无需host-only network，但是在ubuntu下居然不行）。</p>
<p>每个vm中网络配置如下：</p>
<pre><code># cat /etc/network/interfaces

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
iface enp0s3 inet dhcp

auto enp0s8
iface enp0s8 inet dhcp

</code></pre>
<p>保存后，执行/etc/init.d/networking restart生效。</p>
<p>另外每个vm上安装了openssh-server(apt install openssh-server)并设置root可登陆。三个vm的主机名分为为u1、u2和u3（可通过hostnamectl &#8211;static set-hostname u1设置。并在/etc/hosts中添加主机名和内网IP的对应关系）。</p>
<p>每台主机上安装了docker引擎(通过apt install docker.io安装），docker版本信息如下：</p>
<pre><code># docker version
Client:
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        6247962
 Built:             Tue Feb 26 23:56:24 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       6247962
  Built:            Tue Feb 12 22:47:29 2019
  OS/Arch:          linux/amd64
  Experimental:     false

</code></pre>
<h2>二、使用weave创建跨节点的overlay network</h2>
<p>我们选择<a href="https://www.weave.works/">weave</a>作为overlay network的实现。</p>
<h3>1. 安装weave</h3>
<p>我们在每个vm节点上安装目前最新版本的weave，以一个节点为例：</p>
<pre><code># curl -L git.io/weave -o /usr/local/bin/weave
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
100   595    0   595    0     0     62      0 --:--:--  0:00:09 --:--:--   137
100 52227  100 52227    0     0   4106      0  0:00:12  0:00:12 --:--:-- 21187

# chmod a+x /usr/local/bin/weave

# weave version
weave script 2.5.1

... ...

</code></pre>
<p>通过weave setup预先将weave相关的容器Image下载到各个节点，为后面的weave launch所使用。</p>
<pre><code># weave setup

2.5.1: Pulling from weaveworks/weave
... ...
c458f7a37ca6: Pull complete
Digest: sha256:a170dd93fa7e678cc37919ffd65601d1015da6c3f10878534ac237381ea0db19
Status: Downloaded newer image for weaveworks/weave:2.5.1
2.5.1: Pulling from weaveworks/weaveexec
... ...
c11f30d06b58: Pull complete
Digest: sha256:ad53aaabf648548ec26cceac3ab49394778322e1623f0d184a2b74ad06338087
Status: Downloaded newer image for weaveworks/weaveexec:2.5.1
latest: Pulling from weaveworks/weavedb
9b0681f946a1: Pull complete
Digest: sha256:c280cf4e7208f4ca0d2514539e0f476dd12db70beacdc368793b7736de023d8d
Status: Downloaded newer image for weaveworks/weavedb:latest

</code></pre>
<h3>2. 启动跨多节点(peer) weave network</h3>
<p>weave的一个优点是建立跨节点overlay network时并不需要一个外部的存储(比如etcd），位于多个节点上的weave进程会自动同步相关信息。而且weave支持动态向weave overlay network中添加节点。</p>
<p>我们来初始化这个由三个vm节点构成的weave overlay network：</p>
<pre><code>root@u1:~# weave launch --no-dns 192.168.56.4 192.168.56.5
78f459a4a8acc07d46c1f86a15a519b91978c809876452b9d9c1294e760394a9

root@u2:~# weave launch --no-dns 192.168.56.3 192.168.56.5
1f379e50f3917e05bd133589f75594d7b2da20a680bb1e5e7172e37a18abe3ff

root@u3:~# weave launch --no-dns 192.168.56.3 192.168.56.4
aa600bfad8db8711e2cbc5f8e127022460ca3738226dd7aa33bb5b9b049f8cee

</code></pre>
<p>执行完上面命令后，在任意一个vm节点上执行下面命令，查看节点weave之间的连接状态：</p>
<pre><code>root@u1:~# weave status connections
&lt;- 192.168.56.4:54715    established fastdp 8e:d8:ad:a8:32:eb(u2) mtu=1376
&lt;- 192.168.56.5:51504    established fastdp f6:58:43:5c:68:d7(u3) mtu=1376

</code></pre>
<p>我们看到u1节点已经和u2、u3节点成功建立了连接，weave的工作模式是fastdp(fast data path)，mtu为默认的1376（<a href="https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/">适当调节weave mtu可以提升weave overlay network的网络性能</a>）。<br />
我们也可以通过weave status命令查看一下weave网络的整体状态：</p>
<pre><code># weave status

        Version: 2.5.1 (up to date; next check at 2019/04/18 12:35:41)

        Service: router
       Protocol: weave 1..2
           Name: f6:58:43:5c:68:d7(u3)
     Encryption: disabled
  PeerDiscovery: enabled
        Targets: 3
    Connections: 3 (2 established, 1 failed)
          Peers: 3 (with 6 established connections)
 TrustedSubnets: none

        Service: ipam
         Status: ready
          Range: 10.32.0.0/12
  DefaultSubnet: 10.32.0.0/12

        Service: dns
         Domain: weave.local.
       Upstream: 10.0.3.3
            TTL: 1
        Entries: 0

        Service: proxy
        Address: unix:///var/run/weave/weave.sock

        Service: plugin (legacy)
     DriverName: weave

</code></pre>
<h3>3. 在weave overlay network中创建container并测试overlay网内container的互通性</h3>
<p>我们通过为docker指定net driver为weave的方式让docker在weave overlay network中创建container：</p>
<pre><code>root@u1:~# docker run -ti --net=weave busybox /bin/sh

root@u2:~# docker run -ti --net=weave busybox /bin/sh

root@u3:~# docker run -ti --net=weave busybox /bin/sh

</code></pre>
<p>我们在u1上启动的容器内去ping位于其他两个vm上启动的新容器：</p>
<pre><code>/ # ping -c 3 10.32.0.1
PING 10.32.0.1 (10.32.0.1): 56 data bytes
64 bytes from 10.32.0.1: seq=0 ttl=64 time=1.540 ms
64 bytes from 10.32.0.1: seq=1 ttl=64 time=1.548 ms
64 bytes from 10.32.0.1: seq=2 ttl=64 time=1.434 ms

--- 10.32.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.434/1.507/1.548 ms

/ # ping -c 3 10.46.0.0
PING 10.46.0.0 (10.46.0.0): 56 data bytes
64 bytes from 10.46.0.0: seq=0 ttl=64 time=5.118 ms
64 bytes from 10.46.0.0: seq=1 ttl=64 time=1.608 ms
64 bytes from 10.46.0.0: seq=2 ttl=64 time=1.837 ms

--- 10.46.0.0 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.608/2.854/5.118 ms

</code></pre>
<p>我们看到位于weave overlay network中的三个容器是连通的。</p>
<h3>4. 测试host到weave overlay网络中容器的连通性</h3>
<p>考虑到后续host上的<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">consul</a>会对部署在weave overlay network中的container中的服务做health check，因此需要在host上能连通位于overlay network中的container。</p>
<p>我们来测试一下：</p>
<pre><code>root@u1:~# docker run -ti --net=weave busybox /bin/sh

/ # ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
29: ethwe0@if30: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1376 qdisc noqueue
    link/ether aa:8f:45:8f:5f:d6 brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.0/12 brd 10.47.255.255 scope global ethwe0
       valid_lft forever preferred_lft forever
31: eth0@if32: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1500 qdisc noqueue
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

root@u1:~# ping 10.40.0.0
PING 10.40.0.0 (10.40.0.0) 56(84) bytes of data.

^C
--- 10.40.0.0 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3024ms

</code></pre>
<p>从测试结果来看，在host无法ping通位于weave network上的container。这个问题实则也显而易见，因为当前host上的路由表中没有以weave网络range: 10.32.0.0/12为目的地址的路由，并且weave网络设备也并未启用ip地址：</p>
<pre><code>root@u1:~# ip route
default via 10.0.3.2 dev enp0s8
10.0.3.0/24 dev enp0s8  proto kernel  scope link  src 10.0.3.15
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1
172.18.0.0/16 dev docker_gwbridge  proto kernel  scope link  src 172.18.0.1
192.168.56.0/24 dev enp0s3  proto kernel  scope link  src 192.168.56.3

</code></pre>
<p>关于这个问题，weave官方给出了<a href="https://www.weave.works/docs/net/latest/tasks/manage/host-network-integration/">答案</a>：我们可以通过weave expose命令自动为主机上的weave设备分配ip地址，添加到10.32.0.0/12的路由。</p>
<pre><code>root@u1:~# weave expose
10.40.0.1

root@u1:~# ip a

.... ...

7: weave: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1376 qdisc noqueue state UP group default qlen 1000
    link/ether b2:97:b5:7b:0f:a9 brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.1/12 brd 10.47.255.255 scope global weave
       valid_lft forever preferred_lft forever
    inet6 fe80::b097:b5ff:fe7b:fa9/64 scope link
       valid_lft forever preferred_lft forever

.... ...

root@u1:~# ip route
default via 10.0.3.2 dev enp0s8
10.0.3.0/24 dev enp0s8  proto kernel  scope link  src 10.0.3.15
10.32.0.0/12 dev weave  proto kernel  scope link  src 10.40.0.1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1
172.18.0.0/16 dev docker_gwbridge  proto kernel  scope link  src 172.18.0.1
192.168.56.0/24 dev enp0s3  proto kernel  scope link  src 192.168.56.3

</code></pre>
<p>我们看到在u1节点上执行完expose之后，weave设备拥有了自己的ip地址，并且主机路由表中也增加了10.32.0.0/12网络的路由。我们再来测试一下u1上主机到container是否通了：</p>
<pre><code>root@u1:~# ping 10.40.0.0
PING 10.40.0.0 (10.40.0.0) 56(84) bytes of data.
64 bytes from 10.40.0.0: icmp_seq=1 ttl=64 time=4.42 ms

64 bytes from 10.40.0.0: icmp_seq=2 ttl=64 time=1.04 ms
64 bytes from 10.40.0.0: icmp_seq=3 ttl=64 time=1.21 ms
^C
--- 10.40.0.0 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.048/2.228/4.425/1.554 ms

</code></pre>
<p>网络已经打通。我们继续在u2、u3两个节点上执行weave expose，这样三台主机都可以通过网络reach到位于任何一台主机上的、weave network中的container。</p>
<p>而从container到host，原本就可以访问，以u1上的container为例：</p>
<pre><code>/ # ping 192.168.56.3
PING 192.168.56.3 (192.168.56.3): 56 data bytes
64 bytes from 192.168.56.3: seq=0 ttl=64 time=0.345 ms
^C
--- 192.168.56.3 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.345/0.345/0.345 ms

/ # ping 192.168.56.4
PING 192.168.56.4 (192.168.56.4): 56 data bytes
64 bytes from 192.168.56.4: seq=0 ttl=63 time=1.277 ms
^C
--- 192.168.56.4 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.277/1.277/1.277 ms

</code></pre>
<h2>三、安装consul和nomad集群</h2>
<p>在<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">《使用nomad实现集群管理和微服务部署调度》</a>一文中，我们已经详细说过consul和nomad的安装配置过程，这里仅列出步骤，不再详细说明。已经有环境的朋友可以略过该步骤！</p>
<h3>1. 安装consul</h3>
<p>在每个节点上执行下面步骤安装：</p>
<pre><code># wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip
# unzip consul_1.4.4_linux_amd64.zip
# mv consul /usr/local/bin

# mkdir -p ~/consul-install/consul-data

</code></pre>
<p>启动consul集群：</p>
<pre><code>u1:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=192.168.56.3 -datacenter=dc1 &gt; consul-1.log &amp; 2&gt;&amp;1

u2:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=192.168.56.4 -datacenter=dc1 -join 192.168.56.3 &gt; consul-2.log &amp; 2&gt;&amp;1

u3:

nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=192.168.56.5 -datacenter=dc1 -join 192.168.56.3 &gt; consul-3.log &amp; 2&gt;&amp;1
</code></pre>
<p>查看启动状态：</p>
<pre><code>#  consul operator raft list-peers
Node      ID                                    Address            State     Voter  RaftProtocol
consul-1  db838e7c-2b02-949b-763b-a6646ee51981  192.168.56.3:8300  leader    true   3
consul-2  33c81139-5054-7e76-f320-7d28d7528cc8  192.168.56.4:8300  follower  true   3
consul-3  4eda7d24-3fe2-45f5-f4ad-b95fa39f13c1  192.168.56.5:8300  follower  true   3

</code></pre>
<p>如果输出类似上面的日志，则说明consul集群启动成功！</p>
<p>接下来为了利用consul内嵌的DNS server，我们修改一下各个node的DNS配置 /etc/resolvconf/resolv.conf.d/base：</p>
<pre><code>//  /etc/resolvconf/resolv.conf.d/base

nameserver 192.168.56.3
nameserver 192.168.56.4

options timeout:2 attempts:3 rotate single-request-reopen

# /etc/init.d/resolvconf restart

[ ok ] Restarting resolvconf (via systemctl): resolvconf.service.
</code></pre>
<h3>2. 安装nomad并启动nomad集群</h3>
<p>下面是在每个node上安装nomad的步骤：</p>
<pre><code># wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip

# mkdir nomad-install

# unzip nomad_0.8.7_linux_amd64.zip

# mv nomad /usr/local/bin

# nomad version
Nomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES)

</code></pre>
<p>在每个node上创建agent.hcl文件，放到nomad-install下面：</p>
<pre><code>// agent.hcl

data_dir = "/root/nomad-install/nomad.d"

bind_addr = "192.168.56.3" //node 内网ip，这里以u1 host为例

server {
  enabled = true
  bootstrap_expect = 3
}

client {
  enabled = true
}

</code></pre>
<p>启动集群(基于consul)：</p>
<pre><code>u1:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-1.log &amp; 2&gt;&amp;1

u2:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-2.log &amp; 2&gt;&amp;1

u3:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>查看nomad集群状态：</p>
<pre><code># nomad server members -address="http://192.168.56.3:4646"
Name       Address       Port  Status  Leader  Protocol  Build  Datacenter  Region
u1.global  192.168.56.3  4648  alive   false   2         0.8.7  dc1         global
u2.global  192.168.56.4  4648  alive   true    2         0.8.7  dc1         global
u3.global  192.168.56.5  4648  alive   false   2         0.8.7  dc1         global

# nomad operator raft list-peers -address="http://192.168.56.3:4646"
Node       ID                 Address            State     Voter  RaftProtocol
u3.global  192.168.56.5:4647  192.168.56.5:4647  follower  true   2
u2.global  192.168.56.4:4647  192.168.56.4:4647  leader    true   2
u1.global  192.168.56.3:4647  192.168.56.3:4647  follower  true   2

</code></pre>
<p>nomad集群启动成功！</p>
<h2>四. nomad实现在weave overlay network中的job部署</h2>
<h3>1. 创建位于weave overlay network中的nomad task service实例</h3>
<p>我们定义如下nomad job的配置文件：</p>
<pre><code>//httpbackend.nomad

job "httpbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpbackend" {
    count = 3

    task "httpbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpbackendservice:v1.0.0"
        dns_servers =  ["192.168.56.3", "192.168.56.4", "192.168.56.5"]
        network_mode = "weave"
        logging {
          type = "json-file"
        }
      }

      resources {
        network {
          mbits = 10
        }
      }

      service {
        name = "httpbackend"
      }
    }
  }
}

</code></pre>
<p>与之前文章中job的配置文件不同的是，该job配置在task的config中增加了：</p>
<ul>
<li>
<p>dns_servers：由于docker 18.09在-net=weave下，container没有继承host的/etc/resolv.conf文件，我们为了能在container中通过服务的domain查询到其真实ip地址，我们在docker的执行参数中加入dns_servers，我们将u1,u2,u3都作为dns server提供了。</p>
</li>
<li>
<p>network_node：我们希望nomad调度负载、创建docker容器时将docker container创建在weave network中，因此我们在network_node中传入”weave”，这就相当于在执行docker时执行：docker run &#8230; &#8211;net=weave &#8230; &#8230;</p>
</li>
</ul>
<p>我们来创建一下该job：</p>
<pre><code># nomad job run -address=http://192.168.56.3:4646 httpbackend.nomad

==&gt; Monitoring evaluation "806eaecf"
    Evaluation triggered by job "httpbackend"
    Allocation "6e06be74" created: node "11212ed9", group "httpbackend"
    Allocation "e7ed8569" created: node "aa5a06fe", group "httpbackend"
    Allocation "fd6c6a05" created: node "fe7a7e9c", group "httpbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "806eaecf" finished with status "complete"

# nomad job status -address=http://192.168.56.3:4646  httpbackend
ID            = httpbackend
Name          = httpbackend
Submit Date   = 2019-04-19T13:18:21+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group   Queued  Starting  Running  Failed  Complete  Lost
httpbackend  0       0         3        0       0         0

Allocations
ID        Node ID   Task Group   Version  Desired  Status   Created  Modified
6e06be74  11212ed9  httpbackend  0        run      running  54s ago  7s ago
e7ed8569  aa5a06fe  httpbackend  0        run      running  54s ago  6s ago
fd6c6a05  fe7a7e9c  httpbackend  0        run      running  54s ago  12s ago

</code></pre>
<p>我们查看一下u1节点上的httpbackend负载的状态和ip：</p>
<pre><code>root@u1:~/nomad-install/jobs# docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS               NAMES
2e2229cf8f64        c196c122feea             "/root/httpbackendse…"   49 seconds ago      Up 48 seconds                           httpbackend-e7ed8569-fdde-537b-91b3-84583d1ea238
912ac43350f7        weaveworks/weave:2.5.1   "/home/weave/weaver …"   22 hours ago        Up 22 hours                             weave

root@u1:~/nomad-install/jobs# docker exec 2e2229cf8f64 ip a
... ...
49: ethwe0@if50: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1376 qdisc noqueue
    link/ether a2:f1:ef:d7:89:ee brd ff:ff:ff:ff:ff:ff
    inet 10.40.0.0/12 brd 10.47.255.255 scope global ethwe0
       valid_lft forever preferred_lft forever
.... ...

</code></pre>
<p>我们看到新创建的container的ip为10.40.0.0，是weave network subnet range中的一个地址。</p>
<p>我们访问一下该服务：</p>
<pre><code># curl http://10.40.0.0:8081
this is httpbackendservice, version: v1.0.0

</code></pre>
<p>我们看到了预期返回的结果。通过consul的域名访问也同样ok：</p>
<pre><code># curl httpbackend.service.dc1.consul:8081
this is httpbackendservice, version: v1.0.0

</code></pre>
<p>我们从一个位于weave network中的container中去访问httpbackend服务，依然会得到正确的应答结果：</p>
<pre><code># docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 ubuntu /bin/bash

root@3fe76a39b66f:/# curl httpbackend.service.dc1.consul:8081
this is httpbackendservice, version: v1.0.0

</code></pre>
<h2>五、 应用隔离</h2>
<p>有些时候我们需要将部署的应用之间做隔离，让彼此无法互相访问。weave overlay network是支持这样做的，我们一起来看一下。</p>
<h3>1.重建weave网络</h3>
<p>我们首先需要重新创建weave网络，使之能支持划分不同subnet。</p>
<p>先在每个node上执行下面命令，将原有的weave网络清理干净：</p>
<pre><code># weave reset

</code></pre>
<p>执行后，发现weave网络设备、weave相关容器、路由表中有关weave的路由都不见了。</p>
<p>我们重新建立三节点的weave网络，在这个10.32.0.0/16的大网中，我们划分若干subnet，默认的subnet为10.32.0.0/24。</p>
<pre><code>u1:

# weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.4 192.168.56.5

# weave expose

u2:

# weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.3 192.168.56.5

# weave expose

u3:

# weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.3 192.168.56.4

# weave expose

</code></pre>
<p>接下来我们在不同的subnet下分别建立两个container：</p>
<p>首先在u1上，在default subnet下建立两个container a1和a2：</p>
<pre><code>#docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 --name a1 busybox /bin/sh

#docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 --name a2 busybox /bin/sh

</code></pre>
<p>再在u2上在subnet 10.32.1.0/24下建立两个container：b1和b2</p>
<pre><code>u2上：

# docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b1 busybox /bin/sh

# docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b2 busybox /bin/sh

</code></pre>
<p>我们经过测试发现：a1与a2、a1与b1都是可以ping通的，这与我们的预期a1与b1、b2不通不符。我们发现b1(10.32.0.2)、b2(10.32.0.3)两个容器的ip地址居然依然在default subnet内，似乎通过环境变量WEAVE_CIDR传递的subnet信息没有生效。<br />
在weave的一个<a href="https://github.com/weaveworks/weave/issues/2420">issue</a>中，有开发者提到：WEAVE_CIDR仅用于weave proxy模式，在weave作为plugin模式工作时，docker不会将该环境变量信息传递给weave。也就是说即便上面在u2上创建b1、b2时设置了环境变量WEAVE_CIDR，weave插件也无法得到该信息，于是依旧在默认subnet范围为b1、b2分配了ip。</p>
<h3>2. 让docker使用weave proxy模式</h3>
<p>weave proxy是位于docker client与docker engine(docker daemon)之间的代理服务：</p>
<pre><code>docker client --&gt; weave proxy ---&gt; docker engine/daemon

</code></pre>
<p>默认情况下，/var/run/docker.sock是docker client和docker engine之间的通信“媒介”，Docker daemon默认监听的Unix域套接字(Unix domain socket)：/var/run/docker.sock，docker client以及容器中的进程可以通过它与Docker daemon进行通信。</p>
<p>我们可通过docker -H xxx.sock或通过设置 DOCKER_HOST环境变量的方式让docker client与传入的unix socket通信。这样我们就可以将weave proxy的套接字unix:///var/run/weave/weave.sock（通过weave env查看到）传给docker client了。我们来测试一下：</p>
<pre><code>u1:

# docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 --name a1 busybox /bin/sh

# docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 --name a2 busybox /bin/sh

u2:

# docker -H unix:///var/run/weave/weave.sock  run -ti --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b1 busybox /bin/sh

#docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b2 busybox /bin/sh

</code></pre>
<p>四个container启动后，我们发现b1、b2的ip地址都在WEAVE_CIDR指定的空间内，a1、a2间互通；b1、b2间互通，但a1、a2与b1、b2间是不通的。这样就与预期相符了。</p>
<h3>3. nomad与weave proxy模式集成实现应用工作负载的隔离</h3>
<p>接下来，我们来看看如何将nomad和weave的proxy模式集成在一起，实现工作负载分配在不同subnet。</p>
<p>这里我们就无法仅仅通过在job配置文件中传入参数的方式来实现了，我们需要修改一下agent.hcl并重启nomad集群。以u1节点上的agent.hcl为例，我们需要改为下面这样：</p>
<pre><code>data_dir = "/root/nomad-install/nomad.d"

bind_addr = "192.168.56.5"

server {
  enabled = true
  bootstrap_expect = 3
}

client {
  enabled = true
  "options":{
     "docker.endpoint":"unix://var/run/weave/weave.sock"
  }
}

</code></pre>
<p>我们在client配置block中增加一个options，设置了docker.endpoint为weave proxy监听的weave.sock。重启集群：</p>
<pre><code>u1:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-1.log &amp; 2&gt;&amp;1

u2:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-2.log &amp; 2&gt;&amp;1

u3:

# nohup nomad agent -config=/root/nomad-install/agent.hcl  &gt; nomad-3.log &amp; 2&gt;&amp;1

</code></pre>
<p>接下来，我们重建一个httpbackend-another-subnet.nomad，内容如下：</p>
<pre><code>//httpbackend-another-subnet.nomad

job "httpbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpbackend" {
    count = 3

    task "httpbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpbackendservice:v1.0.0"
        dns_servers =  ["192.168.56.3", "192.168.56.4", "192.168.56.5"]
        logging {
          type = "json-file"
        }
      }

      env {
        WEAVE_CIDR="net:10.32.1.0/24"
      }

      resources {
        network {
          mbits = 10
        }
      }

      service {
        name = "httpbackend"
      }
    }
  }
}

</code></pre>
<p>我们去掉了network_mode = “weave”，增加了一个env：WEAVE_CIDR=”net:10.32.1.0/24&#8243;。run这个job：</p>
<pre><code># nomad job run -address=http://192.168.56.3:4646 httpbackend-another-subnet.nomad
==&gt; Monitoring evaluation "e94bdd00"
    Evaluation triggered by job "httpbackend"
    Allocation "3f5032b5" created: node "11212ed9", group "httpbackend"
    Allocation "40d75ae8" created: node "aa5a06fe", group "httpbackend"
    Allocation "627fe1e7" created: node "fe7a7e9c", group "httpbackend"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "e94bdd00" finished with status "complete"

# docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS               NAMES
700bbea7c89e        c196c122feea             "/w/w /root/httpback…"   17 seconds ago      Up 16 seconds                           httpbackend-40d75ae8-fe75-c560-b87b-c1272db4850c
8b7e29522b8b        weaveworks/weave:2.5.1   "/home/weave/weaver …"   10 hours ago        Up 10 hours                             weave
root@u1:~/nomad-install/jobs# docker exec 700bbea7c89e ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
142: eth0@if143: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
144: ethwe@if145: &lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN&gt; mtu 1376 qdisc noqueue
    link/ether f2:55:9d:26:72:56 brd ff:ff:ff:ff:ff:ff
    inet 10.32.1.192/24 brd 10.32.1.255 scope global ethwe
       valid_lft forever preferred_lft forever

</code></pre>
<p>我们看到新创建的httpbackend container的ip已经分配到10.32.1.0/24 subnet下面了。这种方式使得我们可以任意安排我们的job放入哪个subnet。</p>
<h3>4. 遗留问题</h3>
<p>我们通过consul go api试图从consul中获取service: httpbackend的ip信息，我们得到了如下的输出：</p>
<pre><code>#  ./services
10.0.3.15 : 0
10.0.3.15 : 0
10.0.3.15 : 0
[]

</code></pre>
<p>如果在httpbackend的service配置中使用如下配置：</p>
<pre><code> service {
        name = "httpbackend"
        address_mode = "driver"
      }

</code></pre>
<p>那么，我们得到的是下面结果：</p>
<pre><code># ./services
172.17.0.3 : 0
172.17.0.2 : 0
172.17.0.2 : 0
[]

</code></pre>
<p>也就是说nomad在consul中记录的container的advertise ip不是我们想要的weave subnet网段的ip信息，这样就会导致我们通过consul的DNS服务或者通过consul api获取的服务ip信息有误，导致无法通过这两种方式访问到服务实例。在nomad的最新版v0.9.0中该问题依然存在。</p>
<p>综上，“隔离”的目的得到了部分满足，期待后续nomad的改进。</p>
<h2>六、参考资料</h2>
<ul>
<li>
<p>https://www.weave.works/docs/net/latest/install/installing-weave/</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/install/using-weave/#peer-connections</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/install/plugin/plugin/#launching</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/tasks/manage/host-network-integration/</p>
</li>
<li>
<p>https://docs.docker.com/v17.09/engine/userguide/networking/configure-dns/</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/drivers/docker.html#client-requirements</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/tasks/manage/application-isolation/</p>
</li>
<li>
<p>https://www.weave.works/docs/net/latest/tasks/weave-docker-api/weave-docker-api/</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/drivers/docker.html</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/configuration/client.html</p>
</li>
<li>
<p>https://www.nomadproject.io/docs/job-specification/service.html#using-driver-address-mode</p>
</li>
<li>
<p>https://success.docker.com/article/networking</p>
</li>
</ul>
<p>本文涉及到的配置文件和源码，参见<a href="https://github.com/bigwhite/experiments/tree/master/nomad-demo/part3">这里</a>。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用nomad实现工作负载版本升级</title>
		<link>https://tonybai.com/2019/04/09/upgrade-workload-using-nomad/</link>
		<comments>https://tonybai.com/2019/04/09/upgrade-workload-using-nomad/#comments</comments>
		<pubDate>Tue, 09 Apr 2019 06:30:31 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[blue-green-deploy]]></category>
		<category><![CDATA[canary-deploy]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[fabio]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[job]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[rolling-update]]></category>
		<category><![CDATA[route]]></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=2698</guid>
		<description><![CDATA[书接上文。 在《使用nomad实现集群管理和微服务部署调度》一文中，我们介绍了使用nomad进行集群管理和工作负载调度的轻量级方案（相较于Kubernetes方案）。在本文中，我们继续对方案进行延展，介绍一下在nomad集群中工作负载版本升级的一些常用模式和实现方法，包括滚动升级、蓝绿部署和金丝雀部署。 一. 初始状态 这里我们利用基于tcp+sni路由(listener端口为9996)的httpsbackend-sni-1的job作为演示job，该job的初始部署nomad job文件为：httpsbackend-tcp-sni-1.nomad (注：不同的是，这里将count初始值改为了3)。 当前httpsbackend-sni-1这个job的状态如下： # nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T10:57:29+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 [...]]]></description>
			<content:encoded><![CDATA[<p>书接<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">上文</a>。</p>
<p>在<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">《使用nomad实现集群管理和微服务部署调度》</a>一文中，我们介绍了使用nomad进行集群管理和工作负载调度的轻量级方案（相较于<a href="https://tonybai.com/tag/kubernetes">Kubernetes方案</a>）。在本文中，我们继续对方案进行延展，介绍一下在nomad集群中工作负载版本升级的一些常用模式和实现方法，包括<a href="https://en.wikipedia.org/wiki/Rolling_release">滚动升级</a>、<a href="https://www.martinfowler.com/bliki/BlueGreenDeployment.html">蓝绿部署</a>和<a href="https://martinfowler.com/bliki/CanaryRelease.html">金丝雀部署</a>。</p>
<h2>一. 初始状态</h2>
<p>这里我们利用基于tcp+sni路由(listener端口为9996)的httpsbackend-sni-1的job作为演示job，该job的初始部署nomad job文件为：<a href="https://github.com/bigwhite/experiments/blob/master/nomad-demo/part1/jobs/httpsbackend-tcp-sni-1.nomad">httpsbackend-tcp-sni-1.nomad</a> (注：不同的是，这里将count初始值改为了3)。</p>
<p>当前httpsbackend-sni-1这个job的状态如下：</p>
<pre><code># nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T10:57:29+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         3        0       3         0

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created    Modified
7ac186b8  7acdd7bc  httpsbackend-sni-1  22       run      running   1m18s ago  1m1s ago
8a79085f  c281658a  httpsbackend-sni-1  22       run      running   1m18s ago  46s ago
f9ffef32  9e3ef19f  httpsbackend-sni-1  22       run      running   1m18s ago  59s ago
0ed95591  9e3ef19f  httpsbackend-sni-1  20       stop     complete  5d19h ago  7m16s ago
604d2151  9e3ef19f  httpsbackend-sni-1  20       stop     complete  5d19h ago  7m16s ago
06404fff  7acdd7bc  httpsbackend-sni-1  20       stop     complete  5d20h ago  7m14s ago

</code></pre>
<p>fabio路由表如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-job-httpsbackend-sni-1-initial.png" alt="img{512x368}" /></p>
<pre><code># curl -k https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.0

</code></pre>
<p>接下来，我们就以这个job为基础，使用各种版本升级模式对其进行更新。</p>
<h2>二. 滚动更新(rolling update)</h2>
<p>下面是blog.itaysk.com上一篇文章中的有关滚动更新的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-rolling-update-diagram.png" alt="img{512x368}" /><br />
可以大致看出所谓<strong>滚动更新</strong>就是对目标环境下老版本的程序进行逐批的替换，每批的数量可以是1，也可以大于1，根据目标实例的个数自定义。替换过程中，新老版本是并存的，直到所有目标实例都被替换为新版本。</p>
<p>nomad支持通过在job描述文件中增加update配置来支持滚动更新。我们创建httpsbackend-tcp-sni-1-rolling-update.nomad，考虑篇幅，这里仅列出与httpsbackend-tcp-sni-1.nomad的差异：</p>
<pre><code># diff httpsbackend-tcp-sni-1-rolling-update.nomad ./httpsbackend-tcp-sni-1.nomad
14,19d13
&lt;     update {
&lt;       max_parallel = 1
&lt;       min_healthy_time = "30s"
&lt;       healthy_deadline = "5m"
&lt;     }
&lt;
23c17
&lt;         image = "bigwhite/httpsbackendservice:v1.0.1"
---
&gt;         image = "bigwhite/httpsbackendservice:v1.0.0"

</code></pre>
<p>新job nomad文件使用了v1.0.1版本的httpsbackendservice image，增加了update {&#8230;}配置环节，其中的max_parallel指示的是滚动更新每批更新的数量，这里是1，也就是说一批仅用新版本替换一个老版本实例。</p>
<p>执行滚动更新：</p>
<pre><code># nomad job run httpsbackend-tcp-sni-1-rolling-update.nomad
==&gt; Monitoring evaluation "8d39ab53"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "348ef16b"
    Allocation "88c1a29e" created: node "7acdd7bc", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "8d39ab53" finished with status "complete"

</code></pre>
<p>httpsbackendservice job的task group有三个task实例，因此更新需要一些时间，我们在更新过程中查看job status：</p>
<pre><code># nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T13:06:35+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         3        0       4         0

Latest Deployment
ID          = 348ef16b
Status      = running
Description = Deployment is running

Deployed
Task Group          Desired  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  3        1       0        0          2019-04-08T13:16:35+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created   Modified
88c1a29e  7acdd7bc  httpsbackend-sni-1  23       run      running   44s ago   41s ago
7ac186b8  7acdd7bc  httpsbackend-sni-1  22       run      running   2h9m ago  2h9m ago
8a79085f  c281658a  httpsbackend-sni-1  22       run      running   2h9m ago  2h9m ago
f9ffef32  9e3ef19f  httpsbackend-sni-1  22       stop     complete  2h9m ago  44s ago

</code></pre>
<p>我们看到nomad job status命令输出的信息中多出了“Latest Deployment”一个小节，在该小节中，我们看到了一个ID为348ef16b的deployment正在run。这个deployment对应的就是这次的滚动更新，我们看到下面的allocations列表中，一个version为22的allocation已经stop，一个version为23的allocation已经run，这说明nomad已经完成了一个task实例的版本升级。</p>
<p>我们再来查看一下job执行的最终状态：</p>
<pre><code># nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T13:06:35+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         3        0       6         0

Latest Deployment
ID          = 348ef16b
Status      = successful
Description = Deployment completed successfully

Deployed
Task Group          Desired  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  3        3       3        0          2019-04-08T13:18:43+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created    Modified
da1b545b  7acdd7bc  httpsbackend-sni-1  23       run      running   34s ago    2s ago
44da5693  9e3ef19f  httpsbackend-sni-1  23       run      running   1m25s ago  36s ago
88c1a29e  7acdd7bc  httpsbackend-sni-1  23       run      running   2m10s ago  1m26s ago
7ac186b8  7acdd7bc  httpsbackend-sni-1  22       stop     complete  2h11m ago  1m24s ago
8a79085f  c281658a  httpsbackend-sni-1  22       stop     complete  2h11m ago  34s ago
f9ffef32  9e3ef19f  httpsbackend-sni-1  22       stop     complete  2h11m ago  2m10s ago

</code></pre>
<p>我们看到job执行的最终结果：ID为348ef16b的deployment执行成功；所有version 为23的allocations都处于running状态。task group的三个task实例都处于healthy状态。这说明滚动更新成功了！</p>
<p>我们也可以通过nomad提供的deployment子命令查看deployment的状态，deployment id作为命令参数：</p>
<pre><code># nomad deployment list
ID        Job ID              Job Version  Status      Description
348ef16b  httpsbackend-sni-1  23           successful  Deployment completed successfully

# nomad deployment status 348ef16b
ID          = 348ef16b
Job ID      = httpsbackend-sni-1
Job Version = 23
Status      = successful
Description = Deployment completed successfully

Deployed
Task Group          Desired  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  3        3       3        0          2019-04-08T13:18:43+08:00

</code></pre>
<p>滚动更新后的路由：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-job-httpsbackend-sni-1-initial.png" alt="img{512x368}" /></p>
<p>测试一下部署成功的新版本服务：</p>
<pre><code># curl -k https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.1

</code></pre>
<h2>三. 金丝雀部署(canary deployment)</h2>
<p>金丝雀部署是另外一种十分有用的部署模式，下面示意图来自blog.itaysk.com：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-canary-deployment-diagram.png" alt="img{512x368}" /></p>
<p>金丝雀 (Canary)得名于矿工的一个工作习惯：下矿洞前，先会放一只金丝雀进去探测是否有有毒气体，看金丝雀能否活下来。如果金丝雀活下来，则继续下矿操作；否则停止下矿。金丝雀部署亦是先部署少量新版本的服务实例，发布后，开发者可简单地通过手工测试验证新版本实例，又或通过完善的自动化测试基础设施对新版本实例进行详尽验证；甚至是直接接收部分生产流量以充分验证新版本功能、稳定性、性能等，<strong>以给予开发者更多信心</strong>。如果金丝雀实例通过全部测试验证，则把所有老版本全部升级为新版本。如果金丝雀测试失败，则直接回退金丝雀实例，发布失败。</p>
<p>nomad支持两种模式的canary部署：既支持部署canary实例去直接接收生产流量（按比例权重），也可以将其与生产实例隔离开来（利用路由）单独测试验证，下面分别说说这两种模式。</p>
<h3>1. 部署canary实例去直接接收生产流量（按比例权重）</h3>
<p>我们创建一个新的nomad job文件：httpsbackend-tcp-sni-1-canary-1.nomad</p>
<pre><code># diff  httpsbackend-tcp-sni-1-canary-1.nomad  httpsbackend-tcp-sni-1-rolling-update.nomad
18d17
&lt;       canary = 1
24c23
&lt;         image = "bigwhite/httpsbackendservice:v1.0.2"
---
&gt;         image = "bigwhite/httpsbackendservice:v1.0.1"

</code></pre>
<p>我们看到除了新版本task使用v1.0.2版image之外，最大的不同就是在update {&#8230;}配置区域增加了一行：</p>
<pre><code>canary = 1

</code></pre>
<p>我们来plan一下该nomad文件：</p>
<pre><code># nomad job plan httpsbackend-tcp-sni-1-canary-1.nomad
+/- Job: "httpsbackend-sni-1"
+/- Task Group: "httpsbackend-sni-1" (1 canary, 3 ignore)
  +/- Update {
        AutoRevert:       "false"
    +/- Canary:           "0" =&gt; "1"
        HealthCheck:      "checks"
        HealthyDeadline:  "300000000000"
        MaxParallel:      "1"
        MinHealthyTime:   "30000000000"
        ProgressDeadline: "600000000000"
      }
  +/- Task: "httpsbackend-sni-1" (forces create/destroy update)
    +/- Config {
      +/- image:              "bigwhite/httpsbackendservice:v1.0.1" =&gt; "bigwhite/httpsbackendservice:v1.0.2"
          logging[0][type]:   "json-file"
          port_map[0][https]: "7777"
        }

Scheduler dry-run:
- All tasks successfully allocated.

... ...

</code></pre>
<p>我们看到nomad分析的结果是：需要创建一个canary实例，忽略三个已经存在的旧版本task实例。同时task group的canary属性从“0”变为了“1”。</p>
<p>我们来run该job：</p>
<pre><code># nomad job run httpsbackend-tcp-sni-1-canary-1.nomad
==&gt; Monitoring evaluation "0494a8a9"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "3e541fb3"
    Allocation "4d678e67" created: node "c281658a", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "0494a8a9" finished with status "complete"

</code></pre>
<p>查看job的run状态：</p>
<pre><code># nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T21:04:49+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         4        0       6         0

Latest Deployment
ID          = 3e541fb3
Status      = running
Description = Deployment is running but requires promotion

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  false     3        1         1       0        0          2019-04-08T21:14:49+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created    Modified
4d678e67  c281658a  httpsbackend-sni-1  24       run      running   31s ago    15s ago
da1b545b  7acdd7bc  httpsbackend-sni-1  23       run      running   7h57m ago  7h56m ago
44da5693  9e3ef19f  httpsbackend-sni-1  23       run      running   7h57m ago  7h57m ago
88c1a29e  7acdd7bc  httpsbackend-sni-1  23       run      running   7h58m ago  7h58m ago

# nomad deployment status 3e541fb3
ID          = 3e541fb3
Job ID      = httpsbackend-sni-1
Job Version = 24
Status      = running
Description = Deployment is running but requires promotion

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  false     3        1         1       1        0          2019-04-08T21:15:35+08:00

</code></pre>
<p>我们看到：</p>
<ul>
<li>
<p>处于running状态的allocations变成了4个，但是只有一个是version = 24的，其余都为version = 23。version = 24这个显然是我们新部署的canary实例，而另外三个则为原有的老版本实例。</p>
</li>
<li>
<p>在Deployment输出信息中，我们看到了一个描述信息：“Deployment is running but requires promotion”，意思是此次用于部署canary实例的Deployment已经running了，但是还未到最终状态，还需要promote命令。只有promote后，整个的更新工作才算是ok。</p>
</li>
</ul>
<p>下面是canary部署后的fabio的路由：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-job-httpsbackend-sni-1-canary-1.png" alt="img{512x368}" /></p>
<p>我们看到canary实例与其余老版本的路由规则是一致的，并平分的负载权重。也就是说新部署的canary实例与老版本实例一起承载生产流量(canary实例占25%的权重)，我们来验证一下：</p>
<pre><code># curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.1
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.1
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.1

</code></pre>
<p>我们看到第一个请求的流量就打到了我们部署的Canary实例身上了。</p>
<p>如果经过一段时间的验证后，证明canary实例满足要求，我们就要继续推动部署的进程使得该nomad deployment走向最终状态：即将老版本的实例都升级为新版本。</p>
<pre><code># nomad deployment promote 3e541fb3
==&gt; Monitoring evaluation "b5e29b1a"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "3e541fb3"
    Allocation "085a518e" created: node "7acdd7bc", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "b5e29b1a" finished with status "complete"

# nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T21:04:49+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         3        0       9         0

Latest Deployment
ID          = 3e541fb3
Status      = successful
Description = Deployment completed successfully

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  true      3        1         3       3        0          2019-04-08T21:30:54+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created     Modified
40276d89  9e3ef19f  httpsbackend-sni-1  24       run      running   56s ago     11s ago
085a518e  7acdd7bc  httpsbackend-sni-1  24       run      running   1m49s ago   58s ago
4d678e67  c281658a  httpsbackend-sni-1  24       run      running   16m17s ago  1m49s ago
da1b545b  7acdd7bc  httpsbackend-sni-1  23       stop     complete  8h12m ago   56s ago
44da5693  9e3ef19f  httpsbackend-sni-1  23       stop     complete  8h13m ago   1m48s ago
88c1a29e  7acdd7bc  httpsbackend-sni-1  23       stop     complete  8h14m ago   1m47s ago

</code></pre>
<p>通过deployment promote命令使得canary deployment进程继续推进，直到将所有老版本的实例都用canary实例替换掉。也就是我们最终看到的上面的version = 24的allocations都处于running状态，并且一共是三个实例。</p>
<p>我们再来测试一下升级后的服务：</p>
<pre><code># curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2

</code></pre>
<p>我们看到：所有实例都升级到了v1.0.2版本。</p>
<h3>2.将canary实例与生产实例隔离开来（利用路由）单独测试验证</h3>
<p>如果开发者对自己的代码很有信心，不需要将canary实例暴露在生产流量中去验证，nomad也支持将canary实例与生产实例隔离开来（利用路由）单独测试验证。</p>
<p>我们基于httpsbackend-tcp-sni-1-canary-1.nomad改写出一个httpsbackend-tcp-sni-1-canary-2.nomad：</p>
<pre><code># diff httpsbackend-tcp-sni-1-canary-2.nomad httpsbackend-tcp-sni-1-canary-1.nomad
24c24
&lt;         image = "bigwhite/httpsbackendservice:v1.0.3"
---
&gt;         image = "bigwhite/httpsbackendservice:v1.0.2"
43d42
&lt;     canary_tags = ["urlprefix-canary.mysite-sni-1.com/ proto=tcp+sni"]

</code></pre>
<p>我们看到，在新的job文件中，我们除了将image版本升级为v1.0.3，我们还在service{&#8230;}配置区域增加了下面这行：</p>
<pre><code>canary_tags = ["urlprefix-canary.mysite-sni-1.com/ proto=tcp+sni"]

</code></pre>
<p>该配置是canary实例专有的，这里我们通过在canary_tags为canary实例单独定义了路由，以免和老版本实例共享路由分担生产流量。</p>
<p>我们照例运行该job并查看job执行后的status：</p>
<pre><code># nomad job run httpsbackend-tcp-sni-1-canary-2.nomad
==&gt; Monitoring evaluation "44e36161"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "e43d2551"
    Allocation "73319890" created: node "7acdd7bc", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "44e36161" finished with status "complete"

# nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T21:35:03+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         4        0       9         0

Latest Deployment
ID          = e43d2551
Status      = running
Description = Deployment is running but requires promotion

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  false     3        1         1       1        0          2019-04-08T21:45:51+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created     Modified
73319890  7acdd7bc  httpsbackend-sni-1  25       run      running   2m24s ago   1m36s ago
40276d89  9e3ef19f  httpsbackend-sni-1  24       run      running   17m18s ago  16m33s ago
085a518e  7acdd7bc  httpsbackend-sni-1  24       run      running   18m11s ago  17m20s ago
4d678e67  c281658a  httpsbackend-sni-1  24       run      running   32m39s ago  18m11s ago

</code></pre>
<p>这个输出信息和之前的canary模式差别不大。但是从fabio路由表上我们看到如下信息：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-job-httpsbackend-sni-1-canary-2.png" alt="img{512x368}" /></p>
<p>fabio单独为canary实例生成了一个新路由，以区别于老版本的三个实例的路由。</p>
<p>开发人员单独测试canary实例时，可以通过下面方式注入流量:</p>
<pre><code># curl -k  https://canary.mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.3

</code></pre>
<p>而生产流量依旧流入老版本的实例中：</p>
<pre><code># curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.2

</code></pre>
<p>canary实例经过测试验证后，同样可以通过promote完成对老版本的升级部署：</p>
<pre><code># nomad deployment promote e43d2551
==&gt; Monitoring evaluation "34a67391"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "e43d2551"
    Allocation "193cbc2f" created: node "c281658a", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "34a67391" finished with status "complete"

# nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-08T21:35:03+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         3        0       12        0

Latest Deployment
ID          = e43d2551
Status      = successful
Description = Deployment completed successfully

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  true      3        1         3       3        0          2019-04-08T21:58:24+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created     Modified
528a75bd  7acdd7bc  httpsbackend-sni-1  25       run      running   51s ago     10s ago
193cbc2f  c281658a  httpsbackend-sni-1  25       run      running   1m39s ago   52s ago
73319890  7acdd7bc  httpsbackend-sni-1  25       run      running   13m31s ago  1m39s ago
40276d89  9e3ef19f  httpsbackend-sni-1  24       stop     complete  28m25s ago  50s ago
085a518e  7acdd7bc  httpsbackend-sni-1  24       stop     complete  29m18s ago  1m38s ago
4d678e67  c281658a  httpsbackend-sni-1  24       stop     complete  43m46s ago  1m39s ago

</code></pre>
<p>同时，canary实例在fabiolb上的路由也会自动删除掉。canary_tags在promote后将不再起作用，fabio使用的是tags。</p>
<pre><code># curl -k  https://canary.mysite-sni-1.com:9996/
curl: (35) gnutls_handshake() failed: The TLS connection was non-properly terminated.
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.3
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.3
# curl -k  https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.3

</code></pre>
<h2>四. 蓝绿部署(blue-green deployment)</h2>
<p>下面的蓝绿部署模式的示意图同样来自blog.itaysk.com：</p>
<p><img src="https://tonybai.com/wp-content/uploads/nomad-blue-green-deployment-diagram.png" alt="img{512x368}" /></p>
<p>与之前的滚动更新、金丝雀部署不同的是，蓝绿部署需要“两套”环境，通过路由指向来切换流量究竟经过哪套环境。</p>
<p>但是在<a href="https://github.com/hashicorp/nomad-guides/tree/master/application-deployment/go-blue-green">nomad官方关于blue-green部署的例子</a>中，nomad实际只维护了一套环境，并且例子中是利用nomad的canary机制来实现的蓝绿部署。这种实现方式并非严格遵循“蓝绿部署”的公认的定义。</p>
<p>但nomad官方对于blue-green部署的理解似乎仅限如此。我们也来看一下nomad的这种“全量金丝雀”的蓝绿方案：</p>
<p>我们创建httpsbackend-tcp-sni-1-blue-green.nomad文件，重点内容差异如下：</p>
<pre><code># diff httpsbackend-tcp-sni-1-blue-green.nomad httpsbackend-tcp-sni-1-canary-1.nomad
18c18
&lt;       canary = 3
---
&gt;       canary = 1
24c24
&lt;         image = "bigwhite/httpsbackendservice:v1.0.4"
---
&gt;         image = "bigwhite/httpsbackendservice:v1.0.2"

</code></pre>
<p>我们看到这里canary = 3，与count值相同，这也是将其称为“全量金丝雀”的原因。</p>
<p>使用该文件部署新版本实例：</p>
<pre><code># nomad job run httpsbackend-tcp-sni-1-blue-green.nomad
==&gt; Monitoring evaluation "7a5074f3"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "3c8740f2"
    Allocation "338ee344" created: node "c281658a", group "httpsbackend-sni-1"
    Allocation "3dec73d2" created: node "9e3ef19f", group "httpsbackend-sni-1"
    Allocation "e6975673" created: node "9e3ef19f", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "7a5074f3" finished with status "complete"

# nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-09T13:38:49+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         6        0       12        0

Latest Deployment
ID          = 3c8740f2
Status      = running
Description = Deployment is running but requires promotion

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  false     3        3         3       3        0          2019-04-09T13:49:41+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status   Created     Modified
338ee344  c281658a  httpsbackend-sni-1  26       run      running  57s ago     5s ago
3dec73d2  9e3ef19f  httpsbackend-sni-1  26       run      running  57s ago     11s ago
e6975673  9e3ef19f  httpsbackend-sni-1  26       run      running  57s ago     10s ago
528a75bd  7acdd7bc  httpsbackend-sni-1  25       run      running  15h52m ago  15h51m ago
193cbc2f  c281658a  httpsbackend-sni-1  25       run      running  15h52m ago  15h52m ago
73319890  7acdd7bc  httpsbackend-sni-1  25       run      running  16h4m ago   15h52m ago

</code></pre>
<p>部署ok后，6个实例共同接收生产流量。当然我们也可以通过canary_tags为新的部署设定不同路由，选择哪一种要看部署新实例后打算对新实例如何进行测试。</p>
<p>测试验证ok后，像canary deployment一样，通过promote命令用新版本替换老版本。</p>
<pre><code># nomad deployment promote 3c8740f2
==&gt; Monitoring evaluation "fad3a69b"
    Evaluation triggered by job "httpsbackend-sni-1"
    Evaluation within deployment: "3c8740f2"
    Evaluation status changed: "pending" -&gt; "complete"
==&gt; Evaluation "fad3a69b" finished with status "complete"

# nomad job status httpsbackend-sni-1
ID            = httpsbackend-sni-1
Name          = httpsbackend-sni-1
Submit Date   = 2019-04-09T13:38:49+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group          Queued  Starting  Running  Failed  Complete  Lost
httpsbackend-sni-1  0       0         3        0       15        0

Latest Deployment
ID          = 3c8740f2
Status      = successful
Description = Deployment completed successfully

Deployed
Task Group          Promoted  Desired  Canaries  Placed  Healthy  Unhealthy  Progress Deadline
httpsbackend-sni-1  true      3        3         3       3        0          2019-04-09T13:49:41+08:00

Allocations
ID        Node ID   Task Group          Version  Desired  Status    Created     Modified
338ee344  c281658a  httpsbackend-sni-1  26       run      running   4m43s ago   15s ago
3dec73d2  9e3ef19f  httpsbackend-sni-1  26       run      running   4m43s ago   15s ago
e6975673  9e3ef19f  httpsbackend-sni-1  26       run      running   4m43s ago   15s ago
528a75bd  7acdd7bc  httpsbackend-sni-1  25       stop     complete  15h55m ago  14s ago
193cbc2f  c281658a  httpsbackend-sni-1  25       stop     complete  15h56m ago  15s ago
73319890  7acdd7bc  httpsbackend-sni-1  25       stop     complete  16h8m ago   14s ago

</code></pre>
<p>测试结果：</p>
<pre><code># curl -k https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.4

</code></pre>
<p>如果要快速切换回原来的版本，可以使用：</p>
<pre><code>nomad job revert httpsbackend-sni-1 {old_allocation_version}

</code></pre>
<h2>五. 其他</h2>
<p>本文涉及到的nomad job文件源码可在<a href="https://github.com/bigwhite/experiments/tree/master/nomad-demo/part2">这里</a>下载。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/04/09/upgrade-workload-using-nomad/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>官宣：慕课网课程“Kubernetes实战：高可用集群搭建、配置、运维与应用”上线了</title>
		<link>https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/</link>
		<comments>https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/#comments</comments>
		<pubDate>Wed, 17 Oct 2018 10:28:04 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[cloud-native]]></category>
		<category><![CDATA[CNCF]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[controller]]></category>
		<category><![CDATA[dashboard]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[EFK]]></category>
		<category><![CDATA[elastic]]></category>
		<category><![CDATA[ElasticSearch]]></category>
		<category><![CDATA[ELK]]></category>
		<category><![CDATA[event]]></category>
		<category><![CDATA[harbor]]></category>
		<category><![CDATA[heapster]]></category>
		<category><![CDATA[High-Available]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[imooc]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[label]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[Master]]></category>
		<category><![CDATA[microservice]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[network]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[PV]]></category>
		<category><![CDATA[PVC]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[storage]]></category>
		<category><![CDATA[StorageClass]]></category>
		<category><![CDATA[troubleshooting]]></category>
		<category><![CDATA[volume]]></category>
		<category><![CDATA[weave]]></category>
		<category><![CDATA[worker]]></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=2644</guid>
		<description><![CDATA[距离我的第一门网课《Kubernetes基础：开启云原生之门》上线已经过去5个多月了，我的实战课《Kubernetes实战：高可用集群搭建、配置、运维与应用》终于在9月27日正式上线了。 一. 课程介绍 《Kubernetes实战：高可用集群搭建、配置、运维与应用》的课程内容与最初课程设计时规划的内容大纲没有太多出入，基本就是根据我最初的想法拟定的内容，这也基本是我这两年学习k8s、积累的k8s实践的路线。整个课程基于kubernetes 1.10.2版本(docker 17.03.2ce)。课程内容大致分为七个部分（与课程主页的课程目录结构稍有差异，但课程内容是一致的）： 第一章 搭建你的第一个Kubernetes集群 本章介绍了一个使用kubeadm引导的Kubernetes集群的搭建和基本配置方法。 1-1: 导学 1-2: 安装准备 1-3: 初始化集群master节点 1-4: 向集群加入worker节点 1-5: 安装dashboard和heapster 1-6: 验证集群安装结果 第二章 Kubernetes集群探索 本章对kubeadm初始化集群的原理进行了讲解，并对已经建立的k8s集群中的各个组件进行详细介绍，包括功用、原理和配置等 2-1: kubeadm init流程揭秘 2-2: kubeadm join流程揭秘 2-3: kubernetes核心组件详解 2-4: kubectl详解 第三章 Kubernetes网络、安全与存储 本章讲解k8s集群的三个难点：网络、安全与存储的概念和运行原理。 3-1：kubernetes集群网络 3-1-1: kubernetes集群的“三个网络” 3-1-2: kubernetes网络的设计要求 3-1-3: kubernetes网络实现 3-1-4: pod网络实现原理 3-1-5: pod网络方案对比 3-1-6: service网络实现原理 3-2: kubernetes集群安全 3-2-1: kube-apiserver安全模型 3-2-2: [...]]]></description>
			<content:encoded><![CDATA[<p>距离我的第一门网课<a href="https://www.imooc.com/learn/978">《Kubernetes基础：开启云原生之门》</a>上线已经过去5个多月了，我的实战课<a href="https://coding.imooc.com/class/chapter/284.html">《Kubernetes实战：高可用集群搭建、配置、运维与应用》</a>终于在9月27日正式上线了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-frontpage.png" alt="img{512x368}" /></p>
<h3>一. 课程介绍</h3>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-content-1.png" alt="img{512x368}" /></p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-content-2.png" alt="img{512x368}" /></p>
<p><a href="https://coding.imooc.com/class/chapter/284.html">《Kubernetes实战：高可用集群搭建、配置、运维与应用》</a>的课程内容与最初课程设计时规划的内容大纲没有太多出入，基本就是根据我最初的想法拟定的内容，<strong>这也基本是我这两年学习k8s、积累的k8s实践的路线</strong>。整个课程基于kubernetes 1.10.2版本(<a href="https://tonybai.com/tag/docker">docker</a> 17.03.2ce)。课程内容大致分为七个部分（与课程主页的课程目录结构稍有差异，但课程内容是一致的）：</p>
<p>第一章 搭建你的第一个<a href="https://tonybai.com/tag/kubernetes">Kubernetes集群</a></p>
<p>本章介绍了一个使用<a href="https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">kubeadm</a>引导的Kubernetes集群的搭建和基本配置方法。</p>
<ul>
<li>1-1: 导学</li>
<li>1-2: 安装准备</li>
<li>1-3: 初始化集群master节点</li>
<li>1-4: 向集群加入worker节点</li>
<li>1-5: <a href="https://tonybai.com/2017/09/26/some-notes-about-deploying-kubernetes-dashboard-1-7-0/">安装dashboard和heapster</a></li>
<li>1-6: 验证集群安装结果</li>
</ul>
<p>第二章 <a href="https://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/">Kubernetes集群探索</a></p>
<p>本章对kubeadm初始化集群的原理进行了讲解，并对已经建立的k8s集群中的各个组件进行详细介绍，包括功用、原理和配置等</p>
<ul>
<li>2-1: kubeadm init流程揭秘</li>
<li>2-2: kubeadm join流程揭秘</li>
<li>2-3: kubernetes核心组件详解</li>
<li>2-4: <a href="https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/">kubectl</a>详解</li>
</ul>
<p>第三章 Kubernetes网络、安全与存储<br />
本章讲解k8s集群的三个难点：网络、安全与存储的概念和运行原理。</p>
<p>3-1：kubernetes<a href="https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/">集群网络</a></p>
<ul>
<li>3-1-1: kubernetes集群的“三个网络”</li>
<li>3-1-2: kubernetes网络的设计要求</li>
<li>3-1-3: kubernetes网络实现</li>
<li>3-1-4: pod网络实现原理</li>
<li>3-1-5: pod网络方案对比</li>
<li>3-1-6: service网络实现原理</li>
</ul>
<p>3-2: <a href="https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/">kubernetes集群安全</a></p>
<ul>
<li>3-2-1: kube-apiserver安全模型</li>
<li>3-2-2: 传输安全</li>
<li>3-2-3: <a href="https://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/">身份验证</a></li>
<li>3-2-4: 授权</li>
<li>3-2-5: 准入控制</li>
</ul>
<p>3-3 kubernets集群存储</p>
<ul>
<li>3-3-1: Volume</li>
<li>3-3-2: <a href="https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">PV和PVC</a></li>
<li>3-3-3: StorageClass和动态PV供给</li>
<li>3-3-4: Kubernetes存储模型</li>
</ul>
<p>第四章 <a href="https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/">高可用Kubernetes集群</a>搭建方案<br />
本章介绍了什么是高可用k8s集群，并给出了一个可行的高可用Kubernetes集群的搭建方案。</p>
<ul>
<li>4-1: 什么是高可用Kubernetes集群</li>
<li>4-2: 高可用Kubernetes集群方案</li>
</ul>
<p>第五章 Kubernetes集群常见运维操作</p>
<p>本章讲解了Kubernetes集群的基本运维操作，包括node管理、service、pod管理、日志查看等。并讲解了面对k8s集群问题时如何做troubleshooting。</p>
<ul>
<li>5-1: 管理Node与Label</li>
<li>5-2: 管理Namespace、Service和Pod</li>
<li>5-3: <a href="https://tonybai.com/2017/10/16/out-of-node-resource-handling-in-kubernetes-cluster/">计算资源管理</a></li>
<li>5-4: 查看事件和容器日志</li>
<li>5-5: 常用TroubleShooting方法</li>
</ul>
<p>第六章 Kubernetes支撑<a href="https://www.cncf.io/">云原生应用</a>开发案例<br />
本章讲解了Kubernetes集群的应用：支撑云原生应用开发。并通过实际操作讲解了镜像仓库、集中日志以及云应用治理框架的搭建和使用。</p>
<ul>
<li>6-1: Kubernetes与云原生应用</li>
<li>6-2: <a href="https://tonybai.com/2017/12/08/deploy-high-availability-harbor-on-kubernetes-cluster/">高可用私有镜像仓库搭建</a></li>
<li>6-3: <a href="https://tonybai.com/2018/06/13/setup-efk-on-kubernetes-1-10-3-in-the-hard-way/">基于ElasticSearch Stack搭建集群Logging设施</a></li>
<li>6-4: <a href="https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/">基于istio service mesh实现服务治理</a></li>
</ul>
<p>第七章 课程回顾与总结</p>
<h3>二. 做网课目的与课程思路</h3>
<p>当初接下慕课商务的这门课主要是出于两个目的：</p>
<ul>
<li>通过这门课程对自己的k8s学习和实践做一个阶段性的系统总结</li>
<li>尝试一下网课这个“新鲜”事物</li>
</ul>
<p>现在看来，当初这两个“目的”都实现了。但是录制网课的确是件很“辛苦”的事情，不知道多少的夜晚和周末都留给了“网课资料编写和录制”。尤其是Kubernetes这个主题，讲起来“顾虑”很多：</p>
<ul>
<li>
<p>和编程语言课不同，Kubernetes平台是个复杂的平台，外延生态很庞大。k8s概念多，如果不把概念和原理交待清楚、讲透彻，直接就上手操作，那样学习后，对k8s的理解仍然不会很深刻，很多问题仍然无法自己去解决，尤其是中高级阶段。 这就导致很多小伙伴认为课程概念讲解“有些多”；</p>
</li>
<li>
<p>生产环境中k8s集群有大有小，使用目的也是大不相同，安装方式也是有很多种(官方就列了10多种)，所在的网络环境以及使用的pod网络插件也是区别很大，遇到的问题更是千差万别，这里在准备 课程时也是思来想去，无法覆盖所有生产环境的所有情况。最后决定使用kubeadm搭建一个4节点的集群(使用weave network plugin)，可能能更好的满足初学者的需求，学员们更容易获取搭建这样一个 k8s环境所需的资源。而关于课程中实际操作部分重点集中在前面的k8s搭建、集群探索以及后面的k8s对云应用支撑的环节。所以如果小伙伴们的环境与课程不同，可以在课程后提问，我会尽量第一时间、细致的回答各位的问题。</p>
</li>
<li>
<p>关于时长，我在课程里尽量做到没有”废话“。现在的网课多根据“时长”定价（虽然不赞同，但是目前也没有一个更好的量化课程质量的方法）：比如10个小时以上可能就会定到399元，但是不足10小时，可能就在199元这个价位。<strong>于是我努力地将课程做到了“199”这个价位上了</strong>。对于真正想学习k8s的小伙伴们，这也许是一个“好消息”:)。</p>
</li>
</ul>
<h3>三. 课程小结</h3>
<p>Kubernetes还在快速不断地演进！我个人觉得学完本门课程也仅仅是“Kubernetes实践之路”的一个开始而已！应用上云的趋势已经不可逆转，对于云应用开发人员来说，<strong>了解和学习Kubernetes就像当年单机时代开发人员要去了解PC操作系统一样重要</strong>！希望本门课程能给更多的开发者带去帮助！</p>
<p>下面是课程的自制海报，欢迎转发:)</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<hr />
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2018 &#8211; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>TB一周萃选[第2期]</title>
		<link>https://tonybai.com/2017/12/22/2nd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/</link>
		<comments>https://tonybai.com/2017/12/22/2nd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/#comments</comments>
		<pubDate>Fri, 22 Dec 2017 15:27:32 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[135editor]]></category>
		<category><![CDATA[bleve]]></category>
		<category><![CDATA[blogging]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[CloudNativeCon]]></category>
		<category><![CDATA[conduit]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[CSDN]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[envoy]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gonum]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[gorgonia]]></category>
		<category><![CDATA[growth-hacker]]></category>
		<category><![CDATA[IBM]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[istio]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[katacontainer]]></category>
		<category><![CDATA[KubeCon]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[linkerd]]></category>
		<category><![CDATA[mesos]]></category>
		<category><![CDATA[rkt]]></category>
		<category><![CDATA[ServiceMesh]]></category>
		<category><![CDATA[solr]]></category>
		<category><![CDATA[swarm]]></category>
		<category><![CDATA[TB一周萃选]]></category>
		<category><![CDATA[Wechat]]></category>
		<category><![CDATA[wukong]]></category>
		<category><![CDATA[全文检索]]></category>
		<category><![CDATA[分词]]></category>
		<category><![CDATA[圣诞节]]></category>
		<category><![CDATA[大数据]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[平安夜]]></category>
		<category><![CDATA[微信公众号]]></category>
		<category><![CDATA[数据科学]]></category>
		<category><![CDATA[构建]]></category>
		<category><![CDATA[程序员杂志]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2500</guid>
		<description><![CDATA[本文是首发于个人微信公众号的文章TB一周萃选[第2期]的归档。 封面 “我天性不宜交际。 在多数场合，我不是觉得对方乏味，就是害怕对方觉得我乏味。可是我既不愿忍受对方的乏味，也不愿费劲使自己显得有趣，那都太累了。 我独处时最轻松，因为我不觉得自己乏味，即使乏味，也自己承受，不累及他人，无需感到不安。” ——周国平 本周日晚上就是平安夜了！ 圣诞节，是西方最重要的节日之一，也是一个公历纪年的最后一个节日。对于中华大地的人们来说，圣诞节这个洋节日影响力倒不是那么大，不过它却是一个重要的日子，它提醒着大家：这一年要结束了！该总结的总结，该计划的也要开始计划了。 圣诞节是一个美丽的节日。在西方，绿色的挂满彩饰的圣诞树、创意十足的圣诞贺卡、白胡子红袍子的慈祥的圣诞老人、装满礼物的圣诞袜以及美味的圣诞大餐构成了圣诞节永恒不变的节日主题。不过中国人的过法与西方完全不同，尤其是年轻人。他们喜欢成双成对地在商业街以休闲购物的方式过圣诞节，这不仅是商业元素的引导，可能也是荷尔蒙的需要。对于渐渐步入中年的我而言，家庭的分量更重。守在孩子和老婆身边，更能带来心灵上的温暖。 一、一周文章精粹 1、七牛CEO许式伟：”我与Go语言的这十年” 许式伟是大中华地区Go首席布道者（至少，我还不知道谁使用Go和大力推广Go早过许总^_^），并且身体力行、率先垂范地在自己的项目中、在自己的公司产品全面使用Go技术栈。在这篇文章中，许总回顾了Go语言10年来的成长以及他个人使用和推广Go语言的历程。许总对Go有着深刻的理解和洞察力，在这篇文章的结尾处许总再次给出了自己对Go语言未来十年的预测，这里笔者表示不能同意再多了^0^。这里将一段文字摘录如下： 下一个十年会怎样？我知道有一些人很期望 Go 语言特性的迭代。但是如果你抱有这种想法可能会失望，因为下一个十年 Go 不会发生太大的变化。对远期需求变化的预测和把控能力，是 Go 的最大魅力之一。这一点上能够和 Go 相比的是 C 语言（C 语言不同版本的规范差异极少），但因为 Go 要解决的问题更多，做到这一点实际上也更难。下一个十年 Go 仍然会继续深耕服务端开发的生态，同时积极探索其他潜在的应用市场。 原文链接：“我与Go语言的这十年” 图：Go语言的十年 2、追求极简：Docker镜像构建演化史 这是笔者在CSDN《程序员杂志》2017.12上投稿的一篇文章。这两年容器技术飞速发展，除了Docker之外，又有Rkt、kata container等容器引擎或runtime的出现。但Docker依然是容器领域使用最为广泛的主流技术。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。这篇文章将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。 原文链接：“Docker镜像构建演化史” 3、Service Mesh时代的选边与站队 2017年KubeCon&#38;CloudNativeCon Austin大会上，作为代表下一代微服务解决方案设计理念的Service Mesh成为“热词”而被众人追捧。国内的ServiceMesh也是刚刚起步，方兴未艾。这篇“Service Mesh时代的选边与站队 ”就是发表在国内ServiceMesh社区上的一篇文章。文章脉络大致如下： Service Mesh的地位与生态格局 大公司间关于Service Mesh的布局与斗争策略 istio尚未发布1.0时，最早提出Service Mesh概念的小公司buoyant的努力喘息 Service Mesh的2018 原文链接：“Service Mesh 时代的选边与站队” 4、全文检索数据库Bleve简介 去年年末在做一个全文检索查询功能时曾用过陈辉的wukong引擎，不过wukong引擎由于作者的日理万机，无闲打理，已经不再维护。而在Go语言实现的全文检索工具领域，国外社区更流行的是Bleve。这篇文章介绍了作者所在公司为何用bleve替换solr，并对bleve中概念、使用方法进行了介绍，算是Bleve的入门文章。不过对于中文分词和全文检索的支持好坏，还需验证。 原文链接：“Go实现的全文检索数据库Bleve简介” [...]]]></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一周萃选[第2期]</strong>的归档。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/gopher-christmas.jpg" alt="img{512x368}" /><br />
封面</p>
<blockquote>
<p>“我天性不宜交际。</p>
<p>在多数场合，我不是觉得对方乏味，就是害怕对方觉得我乏味。可是我既不愿忍受对方的乏味，也不愿费劲使自己显得有趣，那都太累了。</p>
<p>我独处时最轻松，因为我不觉得自己乏味，即使乏味，也自己承受，不累及他人，无需感到不安。”        ——周国平</p>
</blockquote>
<p>本周日晚上就是平安夜了！</p>
<p><a href="https://en.wikipedia.org/wiki/Christmas">圣诞节</a>，是西方最重要的节日之一，也是一个公历纪年的最后一个节日。对于中华大地的人们来说，圣诞节这个洋节日影响力倒不是那么大，不过它却是一个重要的日子，它提醒着大家：<strong>这一年要结束了！该总结的总结，该计划的也要开始计划了</strong>。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/christmas-day.jpg" alt="img{512x368}" /></p>
<p>圣诞节是一个美丽的节日。在西方，绿色的挂满彩饰的圣诞树、创意十足的圣诞贺卡、白胡子红袍子的慈祥的圣诞老人、装满礼物的圣诞袜以及美味的圣诞大餐构成了圣诞节永恒不变的节日主题。不过中国人的过法与西方完全不同，尤其是年轻人。他们喜欢成双成对地在商业街以休闲购物的方式过圣诞节，这不仅是商业元素的引导，可能也是荷尔蒙的需要。对于渐渐步入中年的我而言，家庭的分量更重。守在孩子和老婆身边，更能带来心灵上的温暖。</p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/christmas-postcard-1907.jpg" alt="img{512x368}" /></p>
<h2>一、一周文章精粹</h2>
<h3>1、<a href="https://www.qiniu.com/">七牛</a>CEO<a href="https://weibo.com/xushiweizh">许式伟</a>：”我与<a href="http://tonybai.com/tag/go">Go语言</a>的这十年”</h3>
<p>许式伟是大中华地区Go首席布道者（至少，我还不知道谁使用Go和大力推广Go早过许总^_^），并且身体力行、率先垂范地在自己的项目中、在自己的公司产品全面使用Go技术栈。在这篇文章中，许总回顾了<a href="http://tonybai.com/2017/09/24/go-ten-years-and-climbing/">Go语言10年</a>来的成长以及他个人使用和推广Go语言的历程。许总对Go有着深刻的理解和洞察力，在这篇文章的结尾处许总再次给出了自己对Go语言未来十年的预测，这里笔者表示不能同意再多了^0^。这里将一段文字摘录如下：</p>
<blockquote>
<p>下一个十年会怎样？我知道有一些人很期望 Go 语言特性的迭代。但是如果你抱有这种想法可能会失望，因为下一个十年 Go 不会发生太大的变化。对远期需求变化的预测和把控能力，是 Go 的最大魅力之一。这一点上能够和 Go 相比的是 C 语言（C 语言不同版本的规范差异极少），但因为 Go 要解决的问题更多，做到这一点实际上也更难。下一个十年 Go 仍然会继续深耕服务端开发的生态，同时积极探索其他潜在的应用市场。</p>
</blockquote>
<p>原文链接：<a href="https://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==&amp;mid=2653370520&amp;idx=1&amp;sn=69827cc58f3bee76abb9778a8c286915&amp;key=aa4d734c2f5165c43f84c9affec15b08721124970a2831fb8f1fd0bd8e4130234c7a6e9cb300e3a5dccca45b88ba7be73a852e515e8a57c68450ff21b0d47141c160f7a1554c9b532ed449f0fcec8148&amp;ascene=0&amp;uin=MTYwMzM0NjYyMQ%3D%3D&amp;devicetype=iMac+MacBookAir6%2C2+OSX+OSX+10.9.2+build(13C64)&amp;version=11020201&amp;lang=zh_CN&amp;pass_ticket=J6dBgepwYkSeUbwD7vdoXH7qZWH3o0gvnsMESYbiL1opRfDiLSA8owEztxcczj4v">“我与Go语言的这十年”</a></p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/gophers10th.jpg" alt="img{512x368}" /><br />
图：Go语言的十年</p>
<h3>2、追求极简：Docker镜像构建演化史</h3>
<p>这是笔者在CSDN《程序员杂志》2017.12上投稿的一篇文章。这两年容器技术飞速发展，除了<a href="http://tonybai.com/tag/docker">Docker</a>之外，又有<a href="https://github.com/rkt/rkt">Rkt</a>、<a href="https://katacontainers.io/">kata container</a>等容器引擎或runtime的出现。但Docker依然是容器领域使用最为广泛的主流技术。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。这篇文章将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。</p>
<p>原文链接：<a href="http://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/">“Docker镜像构建演化史”</a></p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-2.png" alt="img{512x368}" /></p>
<h3>3、Service Mesh时代的选边与站队</h3>
<p>2017年<a href="https://kccncna17.sched.com/">KubeCon&amp;CloudNativeCon Austin大会</a>上，作为代表下一代微服务解决方案设计理念的<a href="https://buoyant.io/2017/04/25/whats-a-service-mesh-and-why-do-i-need-one/">Service Mesh</a>成为“热词”而被众人追捧。国内的ServiceMesh也是刚刚起步，方兴未艾。这篇“Service Mesh时代的选边与站队 ”就是发表在国内<a href="http://www.servicemesh.cn/">ServiceMesh社区</a>上的一篇文章。文章脉络大致如下：</p>
<ul>
<li>Service Mesh的地位与生态格局</li>
<li>大公司间关于Service Mesh的布局与斗争策略</li>
<li>istio尚未发布1.0时，最早提出Service Mesh概念的小公司<a href="https://buoyant.io">buoyant</a>的努力喘息</li>
<li>Service Mesh的2018</li>
</ul>
<p>原文链接：<a href="https://mp.weixin.qq.com/s/hHzDa1T_UKPB97ttFRaDCQ">“Service Mesh 时代的选边与站队”</a></p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/servicemesh-google-products.png" alt="img{512x368}" /></p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/istio-arch.png" alt="img{512x368}" /></p>
<h3>4、全文检索数据库Bleve简介</h3>
<p>去年年末在做一个全文检索查询功能时曾用过陈辉的<a href="http://tonybai.com/2016/12/06/an-intro-to-wukong-fulltext-search-engine/">wukong引擎</a>，不过wukong引擎由于作者的日理万机，无闲打理，已经不再维护。而在Go语言实现的全文检索工具领域，国外社区更流行的是<a href="https://github.com/blevesearch/bleve">Bleve</a>。这篇文章介绍了作者所在公司为何用bleve替换solr，并对bleve中概念、使用方法进行了介绍，算是Bleve的入门文章。不过对于中文分词和全文检索的支持好坏，还需验证。</p>
<p>原文链接：<a href="https://medium.com/wireless-registry-engineering/short-introduction-to-bleve-5de4bbf16657">“Go实现的全文检索数据库Bleve简介”</a></p>
<h3>5、十年专业写博经验谈</h3>
<p>Andrew Chen是硅谷的一位企业家，创业顾问，“<a href="http://andrewchen.co/how-to-be-a-growth-hacker-an-airbnbcraigslist-case-study/">Growth Hacker is the new VP of Marketing</a>”一文作者，目前就职于uber。他还是一位拥有10年写博经验的博主。在“十年专业写博经验谈”一文中，他总结了10年来写博的经验教训，并逐条给出详细的亲历讲解。</p>
<p>原文链接：<a href="http://andrewchen.co/professional-blogging">“10 years of professional blogging – what I’ve learned”</a></p>
<h3>6、Go数据科学Data Sheet</h3>
<p>Go语言在数据科学领域算得上是一个年轻，但却极具潜力的选手。近一年来，Go语言在大数据领域已经有了<a href="https://github.com/gonum/gonum">gonum</a>、<a href="https://github.com/gorgonia/gorgonia">gorgonia</a>等用于数值计算和数据分析的library。gorgonia项目的作者Chewxy这篇”Data Science In Go: A Cheat Sheet”就是使用gonum和gorgonia进行数据科学计算和统计计算的速查手册。</p>
<p>原文链接：<a href="https://www.cheatography.com/chewxy/cheat-sheets/data-science-in-go-a/">“Data Science In Go: A Cheat Sheet”</a></p>
<p><img src="http://tonybai.com/wp-content/uploads/weekly-issues/2nd-issue/go-data-science.jpg" alt="img{512x368}" /></p>
<h2>二、一周资料分享</h2>
<p><a href="https://blog.golang.org/8years">Go正式发布8年</a>后，市面上关于Go语言入门的书籍和课程资料已经出现很多了，无论免费的还是收费。和其他语言的技术资料一样，很多资料质量良莠不齐。hackr.io针对Go语言的教程发起了社区投票，在这里我们可以看到社区对这些资料的质量甄别，同时这也是一份很好的Go书籍资料集合。这个投票是open的，你也可以提交list上尚没有的gobook，并根据你的阅读体验贡献你的vote。</p>
<p>原文地址：<a href="https://hackr.io/tutorials/learn-golang">“Best Community up-voted Go programming resources” </a></p>
<h2>三、一周工具推荐</h2>
<h3>1、135editor</h3>
<p>之前将blog内容同步到微信公众号的时候，多为简单的复制粘贴，导致很多朋友抱怨公众号文章格式太粗糙，尤其是贴代码部分。自从有了做“TB一周萃选”这个weekly issue后，我就在市面上搜寻好用的微信公号文章编辑器。之前用的是微信编辑器(www.wxbj.cn)，简洁易用。但不知何故，该站点现在似乎变成了“易企秀”。于是我将编辑器换成了<a href="http://www.135editor.com/">135editor</a>，这个似乎更加强大，就是左栏下方的广告推广多了一些。</p>
<p><strong>135editor</strong>还支持在绑定公众号后的素材库同步，省了一步copy的动作。</p>
<h2>四、一周书籍推荐</h2>
<h3>1、Kubernetes Handbook</h3>
<p><a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>赢得了与mesos、docker swarm的关于容器管理和服务编排引擎的“战争”，<a href="https://techcrunch.com/2017/12/18/as-kubernetes-surged-in-popularity-in-2017-it-created-a-vibrant-ecosystem/">成为这个领域当之无愧的领头羊</a>。越来越多的公司开始试用Kubernetes，这里推荐一个有关于Kubernetes的开源书《Kubernetes Handbook》，是由talkingdata的jimmy song编写整理的。该书的最大特点就是全面，从K8s的基本概念、运维手段到k8s的领域应用，并且有详细的实践操作讲解。</p>
<p>书籍链接：<a href="https://jimmysong.io/kubernetes-handbook/">《Kubernetes Handbook》</a></p>
<hr />
<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; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/12/22/2nd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>追求极简：Docker镜像构建演化史</title>
		<link>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/</link>
		<comments>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/#comments</comments>
		<pubDate>Wed, 20 Dec 2017 23:31:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alpine]]></category>
		<category><![CDATA[baseimage]]></category>
		<category><![CDATA[builder]]></category>
		<category><![CDATA[busybox]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[CSDN]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Dockerfile]]></category>
		<category><![CDATA[dotCloud]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[LXC]]></category>
		<category><![CDATA[multi-stage-build]]></category>
		<category><![CDATA[musl-libc]]></category>
		<category><![CDATA[namespaces]]></category>
		<category><![CDATA[scratch]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[unionfs]]></category>
		<category><![CDATA[多阶段构建]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[程序员杂志]]></category>
		<category><![CDATA[镜像]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2495</guid>
		<description><![CDATA[本文首发于CSDN《程序员》杂志2017.12期，这里是原文地址。 本文为《程序员》杂志授权转载，谢绝其他转载。全文如下： 自从2013年dotCloud公司(现已改名为Docker Inc)发布Docker容器技术以来，到目前为止已经有四年多的时间了。这期间Docker技术飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。 对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。 一、镜像：继承中的创新 谈镜像构建之前，我们先来简要说下镜像。 Docker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Sun发布了Solaris Container技术，从此开启了内核容器之门。 2008年，以Google公司开发人员为主导实现的Linux Container(即LXC)功能在被merge到Linux内核中。LXC是一种内核级虚拟化技术，主要基于Namespaces和Cgroups技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的创新之处在于其基于Union File System技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为镜像（即image），原理见下图（引自Docker官网）： 图1：Docker镜像原理 镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。 与Solaris Container、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。 二、“镜像是个筐”：初学者的认知 “镜像是个筐，什么都往里面装” &#8211; 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码： //httpserver.go package main import ( "fmt" "net/http" ) func main() { fmt.Println("http daemon start") fmt.Println(" -&#62; listen on port:8080") http.ListenAndServe(":8080", nil) } 接下来，我们来编写一个用于构建目标image的Dockerfile： From ubuntu:14.04 RUN [...]]]></description>
			<content:encoded><![CDATA[<p>本文首发于<a href="https://www.csdn.net/">CSDN</a><a href="http://programmer.csdn.net/">《程序员》</a>杂志<a href="http://blog.csdn.net/qq_40027052/article/details/78720370">2017.12期</a>，这里是<a href="https://mp.weixin.qq.com/s/6--iyRTiAtpSpsLd0Tgf8w">原文地址</a>。</p>
<p>本文为《程序员》杂志授权转载，谢绝其他转载。全文如下：</p>
<p>自从2013年<a href="https://en.wikipedia.org/wiki/DotCloud">dotCloud公司</a>(现已改名为<a href="https://en.wikipedia.org/wiki/Docker,_Inc.">Docker Inc</a>)发布<a href="http://tonybai.com/tag/docker">Docker容器技术</a>以来，到目前为止已经有四年多的时间了。这期间<a href="https://en.wikipedia.org/wiki/Docker_(software)">Docker技术</a>飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。</p>
<p>对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。</p>
<h3>一、镜像：继承中的创新</h3>
<p>谈镜像构建之前，我们先来简要说下<strong>镜像</strong>。</p>
<p>Docker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在<a href="https://en.wikipedia.org/wiki/Sun_Microsystems">Sun公司</a>的<a href="https://en.wikipedia.org/wiki/Solaris_(operating_system)">Solaris操作系统</a>上，<a href="http://tonybai.com/tag/solaris">Solaris</a>是当时最先进的服务器操作系统。2005年Sun发布了<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>技术，从此开启了内核容器之门。</p>
<p>2008年，以Google公司开发人员为主导实现的Linux Container(即<a href="https://en.wikipedia.org/wiki/LXC">LXC</a>)功能在被merge到<a href="https://www.kernel.org/">Linux内核</a>中。LXC是一种内核级虚拟化技术，主要基于<a href="https://en.wikipedia.org/wiki/Cgroups#NAMESPACE-ISOLATION">Namespaces</a>和<a href="https://en.wikipedia.org/wiki/Cgroups">Cgroups</a>技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的<strong>创新之处</strong>在于其基于<a href="https://en.wikipedia.org/wiki/UnionFS">Union File System</a>技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为<strong>镜像</strong>（即image），原理见下图（引自Docker官网）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-layers-and-container.png" alt="img{512x368}" /><br />
图1：Docker镜像原理</p>
<p>镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。</p>
<p>与<a href="https://en.wikipedia.org/wiki/Solaris_Containers">Solaris Container</a>、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。</p>
<h3>二、“镜像是个筐”：初学者的认知</h3>
<p><strong>“镜像是个筐，什么都往里面装”</strong> &#8211; 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码：</p>
<pre><code>//httpserver.go

package main

import (
        "fmt"
        "net/http"
)

func main() {
        fmt.Println("http daemon start")
        fmt.Println("  -&gt; listen on port:8080")
        http.ListenAndServe(":8080", nil)
}

</code></pre>
<p>接下来，我们来编写一个用于构建目标image的Dockerfile：</p>
<pre><code>From ubuntu:14.04

RUN apt-get update \
      &amp;&amp; apt-get install -y software-properties-common \
      &amp;&amp; add-apt-repository ppa:gophers/archive \
      &amp;&amp; apt-get update \
      &amp;&amp; apt-get install -y golang-1.9-go \
                            git \
      &amp;&amp; rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"

COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
      &amp;&amp; chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>构建这个Image：</p>
<pre><code># docker build -t repodemo/httpd:latest .
//...构建输出这里省略...

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB
</code></pre>
<p>整个镜像的构建过程因环境而定。如果您的网络速度一般，这个构建过程可能会花费你10多分钟甚至更多。最终如我们所愿，基于repodemo/httpd:latest这个镜像的容器可以正常运行：</p>
<pre><code># docker run repodemo/httpd
http daemon start
  -&gt; listen on port:8080

</code></pre>
<p>一个Dockerfile最终生产出一个镜像。Dockerfile由若干Command组成，每个Command执行结果都会单独形成一个layer。我们来探索一下构建出来的镜像：</p>
<pre><code># docker history 183dbef8eba6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B
27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0B
a9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB
... ...
aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       &amp;&amp; apt-get...   356MB
.... ...
&lt;missing&gt;           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB

</code></pre>
<p>我们去除掉那些Size为0或很小的layer，我们看到三个size占比较大的layer，见下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-2.png" alt="img{512x368}" /><br />
图2：Docker镜像分层探索</p>
<p>虽然Docker引擎利用r缓存机制可以让同主机下非首次的镜像构建执行得很快，但是在Docker技术热情催化下的这种构建思路让docker镜像在存储和传输方面的优势荡然无存，要知道一个ubuntu-server 16.04的虚拟机ISO文件的大小也就不过600多MB而已。</p>
<h3>三、”理性的回归”：builder模式的崛起</h3>
<p>Docker使用者在新技术接触初期的热情“冷却”之后迎来了“理性的回归”。根据上面分层镜像的图示，我们发现最终镜像中包含构建环境是多余的，我们只需要在最终镜像中包含足够支撑httpd运行的运行环境即可，而base image自身就可以满足。于是我们应该去除不必要的中间层：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-3-1.png" alt="img{512x368}" /><br />
图3：去除不必要的分层</p>
<p>现在问题来了！如果不在同一镜像中完成应用构建，那么在哪里、由谁来构建应用呢？至少有两种方法：</p>
<ol>
<li>在本地构建并COPY到镜像中；</li>
<li>借助构建者镜像(builder image)构建。</li>
</ol>
<p>不过方法1本地构建有很多局限性，比如：本地环境无法复用、无法很好融入持续集成/持续交付流水线等。借助builder image进行构建已经成为Docker社区的一个最佳实践，Docker官方为此也推出了各种主流编程语言的官方base image，比如：<a href="http://tonybai.com/tag/go">go</a>、<a href="http://tonybai.com/tag/java">java</a>、node、<a href="http://tonybai.com/tag/python">python</a>以及<a href="http://tonybai.com/tag/ruby">ruby</a>等。借助builder image进行镜像构建的流程原理如下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-3-2.png" alt="img{512x368}" /><br />
图4：借助builder image进行镜像构建的流程图</p>
<p>通过原理图，我们可以看到整个目标镜像的构建被分为了两个阶段：</p>
<ol>
<li>第一阶段：构建负责编译源码的构建者镜像；</li>
<li>第二阶段：将第一阶段的输出作为输入，构建出最终的目标镜像。</li>
</ol>
<p>我们选择golang:1.9.2作为builder base image，构建者镜像的Dockerfile.build如下：</p>
<pre><code>// Dockerfile.build

FROM golang:1.9.2

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go
</code></pre>
<p>执行构建：</p>
<pre><code># docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .
</code></pre>
<p>构建好的应用程序httpd放在了镜像repodemo/httpd-builder中的/go/src目录下，我们需要一些“胶水”命令来连接两个构建阶段，这些命令将httpd从<strong>构建者镜像</strong>中取出并作为下一阶段构建的输入：</p>
<pre><code># docker create --name extract-httpserver repodemo/httpd-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-builder
</code></pre>
<p>通过上面的命令，我们将编译好的httpd程序拷贝到了本地。下面是目标镜像的Dockerfile：</p>
<pre><code>//Dockerfile.target
From ubuntu:14.04

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

WORKDIR /root
ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>接下来我们来构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd:latest -f Dockerfile.target .
</code></pre>
<p>我们来看看这个镜像的“体格”：</p>
<pre><code># docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB
</code></pre>
<p>200MB！目标镜像的Size降为原来的 1/2 还多。</p>
<h3>四、“像赛车那样减去所有不必要的东西”：追求最小镜像</h3>
<p>前面我们构建出的镜像的Size已经缩小到200MB，但这还不够。200MB的“体格”在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重，减到尽可能的小，就像赛车那样，为了能减轻重量将所有不必要的东西都拆除掉：我们仅保留能支撑我们的应用运行的必要库、命令，其余的一律不纳入目标镜像。当然不仅仅是Size上的原因，小镜像还有额外的好处，比如：内存占用小，启动速度快，更加高效；不会因其他不必要的工具、库的漏洞而被攻击，减少了“攻击面”，更加安全。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-1.png" alt="img{512x368}" /><br />
图5：目标镜像还能更小些吗？</p>
<p>一般应用开发者不会从scratch镜像从头构建自己的base image以及目标镜像的，开发者会挑选适合的base image。一些“蝇量级”甚至是“草量级”的官方base image的出现为这种情况提供了条件。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-size.png" alt="img{512x368}" /><br />
图6：一些base image的Size比较(来自imagelayers.io截图)</p>
<p>从图中看，我们有两个选择：<a href="https://www.busybox.net/">busybox</a>和<a href="https://alpinelinux.org/">alpine</a>。</p>
<p>单从image的size上来说，busybox更小。不过busybox默认的libc实现是uClibc，而我们通常运行环境使用的libc实现都是glibc，因此我们要么选择静态编译程序，要么使用busybox:glibc镜像作为base image。</p>
<p>而 alpine image 是另外一种蝇量级 base image，它使用了比 glibc 更小更安全的 <a href="http://www.musl-libc.org/">musl libc</a> 库。 不过和 busybox image 相比，alpine image 体积还是略大。除了因为 musl比uClibc 大一些之外，alpine还在镜像中添加了自己的包管理系统apk，开发者可以使用apk在基于alpine的镜像中添 加需要的包或工具。因此，对于普通开发者而言，alpine image显然是更佳的选择。不过alpine使用的libc实现为<a href="http://www.musl-libc.org/">musl</a>，与基于glibc上编译出来的应用程序不兼容。如果直接将前面构建出的httpd应用塞入alpine，在容器启动时会遇到下面错误，因为加载器找不到glibc这个动态共享库文件：</p>
<pre><code>standard_init_linux.go:185: exec user process caused "no such file or directory"
</code></pre>
<p>对于Go应用来说，我们可以采用静态编译的程序，但一旦采用静态编译，也就意味着我们将失去一些libc提供的原生能力，比如：在linux上，你无法使用系统提供的DNS解析能力，只能使用Go自实现的DNS解析器。</p>
<p>我们还可以采用基于alpine的builder image，golang base image就提供了alpine 版本。 我们就用这种方式构建出一个基于alpine base image的极小目标镜像。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-image-history-4-2.png" alt="img{512x368}" /><br />
图7：借助 alpine builder image 进行镜像构建的流程图</p>
<p>我们新建两个用于 alpine 版本目标镜像构建的 Dockerfile：Dockerfile.build.alpine 和Dockerfile.target.alpine：</p>
<pre><code>//Dockerfile.build.alpine
FROM golang:alpine

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

// Dockerfile.target.alpine
From alpine

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

WORKDIR /root
ENTRYPOINT ["/root/httpd"]

</code></pre>
<p>构建builder镜像：</p>
<pre><code>#  docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
repodemo/httpd-alpine-builder    latest              d5b5f8813d77        About a minute ago   275MB
</code></pre>
<p>执行“胶水”命令：</p>
<pre><code># docker create --name extract-httpserver repodemo/httpd-alpine-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-alpine-builder
</code></pre>
<p>构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-alpine            latest              895de7f785dd        13 seconds ago      16.2MB
</code></pre>
<p>16.2MB！目标镜像的Size降为不到原来的十分之一。我们得到了预期的结果。</p>
<h3>五、“要有光，于是便有了光”：对多阶段构建的支持</h3>
<p>至此，虽然我们实现了目标Image的最小化，但是整个构建过程却是十分繁琐，我们需要准备两个Dockerfile、需要准备“胶水”命令、需要清理中间产物等。作为Docker用户，我们希望用一个Dockerfile就能解决所有问题，于是就有了Docker引擎对多阶段构建(multi-stage build)的支持。注意：这个特性非常新，只有Docker 17.05.0-ce及以后的版本才能支持。</p>
<p>现在我们就按照“多阶段构建”的语法将上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一个Dockerfile中：</p>
<pre><code>//Dockerfile

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o httpd ./httpserver.go

From alpine:latest

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

ENTRYPOINT ["/root/httpd"]
</code></pre>
<p>Dockerfile的语法还是很简明和易理解的。即使是你第一次看到这个语法也能大致猜出六成含义。与之前Dockefile最大的不同在于在支持多阶段构建的Dockerfile中我们可以写多个“From baseimage”的语句了，每个From语句开启一个构建阶段，并且可以通过“as”语法为此阶段构建命名(比如这里的builder)。我们还可以通过COPY命令在两个阶段构建产物之间传递数据，比如这里传递的httpd应用，这个工作之前我们是使用“胶水”代码完成的。</p>
<p>构建目标镜像：</p>
<pre><code># docker build -t repodemo/httpd-multi-stage .

# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-multi-stage       latest              35e494aa5c6f        2 minutes ago       16.2MB
</code></pre>
<p>我们看到通过多阶段构建特性构建的Docker Image与我们之前通过builder模式构建的镜像在效果上是等价的。</p>
<h3>六、来到现实</h3>
<p>沿着时间的轨迹，Docker 镜像构建走到了今天。追求又快又小的镜像已成为了 Docker 社区 的共识。社区在自创 builder 镜像构建的最佳实践后终于迎来了多阶段构建这柄利器，从此构建 出极简的镜像将不再困难。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<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 style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
	</channel>
</rss>
