[istio源码分析][pilot] pilot之pilot_agent

1. 前言

转载请说明原文出处, 尊重他人劳动成果!

源码位置: https://github.com/nicktming/istio
分支: tming-v1.3.6 (基于1.3.6版本)

本文将继续分析pilot中的内容, 将分析pilot-agent内容, 该pilot-agent将负责envoy的生命周期.

2. 初识pilot-agent

[root@master ~]# kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
productpage-v1-8554d58bff-d7j8d   2/2     Running   2          13d
...
[root@master ~]# kubectl exec -it productpage-v1-8554d58bff-d7j8d -c istio-proxy bash
istio-proxy@productpage-v1-8554d58bff-d7j8d:/$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
istio-p+     1     0  0 Feb02 ?        00:00:11 /usr/local/bin/pilot-agent proxy sidecar --domain default.svc.cluster.local --configPath /etc/istio/proxy --binaryPath /u
istio-p+    11     1  0 Feb02 ?        00:00:36 /usr/local/bin/envoy -c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 
istio-p+    24     0  0 05:53 pts/0    00:00:00 bash
istio-p+    33    24  0 05:53 pts/0    00:00:00 ps -ef
istio-proxy@productpage-v1-8554d58bff-d7j8d:/$ 

1. 可以看到注入的容器istio-proxy1号进程是命令/usr/local/bin/pilot-agent proxy sidecar ...执行的.
2. 另外可以看到envoy程序的父进程是1号进程, 也就是说pilot-agent进程负责启动envoy进程, 并且envoy启动的配置文件在/etc/istio/proxy/envoy-rev0.json.

istio-proxy@productpage-v1-8554d58bff-d7j8d:/$ cat /etc/istio/proxy/envoy-rev0.json
{
  "node": {
    "id": "sidecar~10.0.12.9~productpage-v1-8554d58bff-d7j8d.default~default.svc.cluster.local",
    "cluster": "productpage.default",
    ...
  },
  ...
  "dynamic_resources": {
    "lds_config": {
      "ads": {}
    },
    "cds_config": {
      "ads": {}
    },
    "ads_config": {
      "api_type": "GRPC",
      "grpc_services": [
        {
          "envoy_grpc": {
            "cluster_name": "xds-grpc"
          }
        }
      ]
    }
  },
  "static_resources": {
    "clusters": [
      ...
      {
        "name": "xds-grpc",
        "type": "STRICT_DNS",
        ...
        "hosts": [
          {
            "socket_address": {"address": "172.31.71.181", "port_value": 15010}
          }
        ],
      ...
}
istio-proxy@productpage-v1-8554d58bff-d7j8d:/$ 

可以看到xds的配置为172.31.71.181:15010, 是pilot的地址.

3. pilot-agent

// pilot/cmd/pilot-agent/main.go
var (
    ...
    rootCmd = &cobra.Command{
        Use:          "pilot-agent",
        ...
    }
    proxyCmd = &cobra.Command{
        Use:   "proxy",
        ...
        RunE: func(c *cobra.Command, args []string) error {
            ...
            tlsCertsToWatch = []string{
                tlsServerCertChain, tlsServerKey, tlsServerRootCert,
                tlsClientCertChain, tlsClientKey, tlsClientRootCert,
            }
            // envoy的配置信息
            proxyConfig := mesh.DefaultProxyConfig()
            ...
            // 生成envoy
            envoyProxy := envoy.NewProxy(proxyConfig, role.ServiceNode(), proxyLogLevel, proxyComponentLogLevel, pilotSAN, role.IPAddresses, dnsRefreshRate, opts)
            // 生成agent
            agent := proxy.NewAgent(envoyProxy, proxy.DefaultRetry, features.TerminationDrainDuration())
            // watch
            watcher := envoy.NewWatcher(tlsCertsToWatch, agent.ConfigCh())
            go waitForCompletion(ctx, agent.Run)
            go waitForCompletion(ctx, watcher.Run)
            cmd.WaitSignal(make(chan struct{}))
            return nil
        },
    }
)

1. envoy的配置信息proxyConfig.
2. envoyProxy是运行envoy的一个代理.
3. agent 负责envoy的生命周期.
4. watcher监控文件certs的变化.
5. 运行agentwatcher.
6. 等待``agent```结束.

3.1 agent

理解了agent, 对整个结构就理解了.

// pilot/pkg/proxy/agent.go
type Agent interface {
    // 返回一个config channel用于发送文件更新
    // agent会与当前config比较来决定是否需要重启envoy, 如果启动失败会以exponential back-off形式重试
    ConfigCh() chan<- interface{}
    Run(ctx context.Context)
}
type Proxy interface {
    // 运行 传入config, epoch, abort channel
    Run(interface{}, int, <-chan error) error
    // 清理某个epoch的信息
    Cleanup(int)
    // 重试了所有的次数后还无法成功 panic该epoch
    Panic(interface{})
}

1. Proxy有三个方法, 分别需要实现Run, CleanupPanic方法.

1.1 Run中传入三个参数, 第一个是proxy运行的时候的config文件. 第二个是epoch, 相当于版本号. 第三个是传入的一个channel, 外部可以通过该channel杀死proxy.
1.2 Cleanup传入的是epoch, 对该版本做一些清理工作. 比如envoy删除对应的配置文件, 比如传入0, 就把/etc/istio/proxy/envoy-rev0.json文件删除.

2. Agent有两个方法.

2.1 ConfigCh返回一个config channel用于发送文件更新. agent会与当前config比较来决定是否需要重启envoy, 如果启动失败会以exponential back-off形式重试.
2.2 Run启动agent.

3.2 agent

agentAgent的一个实现体.

// pilot/pkg/proxy/agent.go
func NewAgent(proxy Proxy, retry Retry, terminationDrainDuration time.Duration) Agent {
    return &agent{
        // 要运行的proxy
        proxy:                    proxy,
        retry:                    retry,
        // 每个版本对应的config
        epochs:                   make(map[int]interface{}),
        configCh:                 make(chan interface{}),
        statusCh:                 make(chan exitStatus),
        // 每个版本对应的退出channel
        abortCh:                  make(map[int]chan error),
        terminationDrainDuration: terminationDrainDuration,
    }
}
func (a *agent) ConfigCh() chan<- interface{} {
    return a.configCh
}
reconcile

先了解一下reconcile再看Run方法.

    // cancel any scheduled restart
    a.retry.restart = nil
    log.Infof("Reconciling retry (budget %d)", a.retry.budget)
    // check that the config is current
    // 与当前config比较
    if reflect.DeepEqual(a.desiredConfig, a.currentConfig) {
        log.Infof("Desired configuration is already applied")
        return
    }
    // discover and increment the latest running epoch
    // 增加一个版本号
    epoch := a.latestEpoch() + 1
    // buffer aborts to prevent blocking on failing proxy
    abortCh := make(chan error, maxAborts)
    // 当前版本对应的config和abort channel
    a.epochs[epoch] = a.desiredConfig
    a.abortCh[epoch] = abortCh
    // 更新当前agent的config
    a.currentConfig = a.desiredConfig
    go a.runWait(a.desiredConfig, epoch, abortCh)
}
func (a *agent) runWait(config interface{}, epoch int, abortCh <-chan error) {
    log.Infof("Epoch %d starting", epoch)
    // 同步运行proxy 返回结果为err
    err := a.proxy.Run(config, epoch, abortCh)
    // envoy proxy 运行完将结果组装成一个exitStatus 发送到 a.statusCh
    a.statusCh <- exitStatus{epoch: epoch, err: err}
}

1. 增加一个版本号epoch.
2. 添加新版本对应的config和abort channel.
3. 更新当前agentcurrentConfig, 新版本的config是该agent要运行的内容.
4. runWait是个同步操作, 会等到envoy proxy后返回结果了, 才会把结果写入到a.statusCh这个channel中.

Run

Run方法

func (a *agent) Run(ctx context.Context) {
    ...
    for {
        ...
        select {
        // 有文件更新
        case config := <-a.configCh:
            if !reflect.DeepEqual(a.desiredConfig, config) {
                // 如果有新文件
                // 更新a.desiredConfig
                a.desiredConfig = config
                ...
                a.reconcile()
            }
        // envoy proxy运行结束
        case status := <-a.statusCh:
            // 删除该版本内存中内容
            delete(a.epochs, status.epoch)
            delete(a.abortCh, status.epoch)
            a.currentConfig = a.epochs[a.latestEpoch()]
            if status.err == errAbort {
                log.Infof("Epoch %d aborted", status.epoch)
            } else if status.err != nil {
                log.Warnf("Epoch %d terminated with an error: %v", status.epoch, status.err)
                a.abortAll()
            } else {
                log.Infof("Epoch %d exited normally", status.epoch)
            }
            // cleanup for the epoch
            // 为当前版本做清理工作 因为该版本的envoy proxy已经运行结束
            a.proxy.Cleanup(status.epoch)
            if status.err != nil {
                // skip retrying twice by checking retry restart delay
                if a.retry.restart == nil {
                    if a.retry.budget > 0 {
                        delayDuration := a.retry.InitialInterval * (1 << uint(a.retry.MaxRetries-a.retry.budget))
                        restart := time.Now().Add(delayDuration)
                        a.retry.restart = &restart
                        a.retry.budget--
                        log.Infof("Epoch %d: set retry delay to %v, budget to %d", status.epoch, delayDuration, a.retry.budget)
                    } else {
                        log.Error("Permanent error: budget exhausted trying to fulfill the desired configuration")
                        // 已经试过所有的次数 panic该版本并且agent整体退出
                        a.proxy.Panic(status.epoch)
                        return
                    }
                } else {
                    log.Debugf("Epoch %d: restart already scheduled", status.epoch)
                }
            }
        // 定时操作
        case <-reconcileTimer.C:
            a.reconcile()
        // 结束agent
        case <-ctx.Done():
            a.terminate()
            log.Info("Agent has successfully terminated")
            return
        }
    }
}

Run是从这里控制整个envoy proxy的生命周期.
1.a.configCh中获得config信息, 如果与当前的config不同, 则表明需要重新调整envoy proxy. 调用reconcile进行调整. 这个是供外部调用控制的, 比如watcher.
2.envoy proxy运行结束后, 会向a.statusCh发送信息, 所以第二个分支是处理这类信息的, 首先删除agent内存中关于该版本epoch的信息包括a.epochs, a.abortCh等, 并且调用proxy的清理方法(envoy proxy会删除对应的在磁盘上的配置文件). 如果proxy是带有错误信息退出的话, 则可能需要重试(定时重试的机制第三个case).
3. 定时重试的机制.

3.2 proxy

proxyistio指的就是envoy, 所以接下来看一下envoy是如何实现Proxy的三个方法的.

Run
func (e *envoy) Run(config interface{}, epoch int, abort <-chan error) error {
    var fname string
    if len(e.config.CustomConfigFile) > 0 {
        // there is a custom configuration. Don't write our own config - but keep watching the certs.
        fname = e.config.CustomConfigFile
    } else if _, ok := config.(proxy.DrainConfig); ok {
        fname = drainFile
    } else {
        // 生成envoy的配置文件
        out, err := bootstrap.WriteBootstrap(
            &e.config, e.node, epoch, e.pilotSAN, e.opts, os.Environ(), e.nodeIPs, e.dnsRefreshRate)
        ...
        fname = out
    }
    // 构造envoy运行的配置文件
    args := e.args(fname, epoch, istioBootstrapOverrideVar.Get())
    log.Infof("Envoy command: %v", args)
    // 运行
    cmd := exec.Command(e.config.BinaryPath, args...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Start(); err != nil {
        return err
    }
    done := make(chan error, 1)
    go func() {
        // 等待运行结果
        done <- cmd.Wait()
    }()
    select {
    // 外部可以中断该envoy的运行
    case err := <-abort:
        log.Warnf("Aborting epoch %d", epoch)
        if errKill := cmd.Process.Kill(); errKill != nil {
            log.Warnf("killing epoch %d caused an error %v", epoch, errKill)
        }
        return err
    // envoy运行结束后返回的内容
    case err := <-done:
        return err
    }
}

Run方法的操作可以总结成以下几步:
1. bootstrap.WriteBootstrap生成配置文件(envoy-rev%d.json)写在磁盘上, 主要是根据proxyConfig.
2. 生成envoy运行的args参数.
3. 运行envoy, 并异步等待运行结果将其写入done这个channel中.
4. 有两种情况Run方法会退出:

4.1 外部(agent)向abort channel中写入信息使其主动杀死envoy进程.
4.2 envoy运行结束. (错误或者无错误的运行结束)

5. 返回最终的错误信息err, 也有可能是nil(没有错误).

Cleanup 和 Panic
func (e *envoy) Cleanup(epoch int) {
    filePath := configFile(e.config.ConfigPath, epoch)
    // 删除此版本的配置文件
    if err := os.Remove(filePath); err != nil {
        log.Warnf("Failed to delete config file %s for %d, %v", filePath, epoch, err)
    }
}
func (e *envoy) Panic(epoch interface{}) {
    log.Error("cannot start the e with the desired configuration")
    if epochInt, ok := epoch.(int); ok {
        // print the failed config file
        // 打印所有失败的配置文件
        filePath := configFile(e.config.ConfigPath, epochInt)
        b, _ := ioutil.ReadFile(filePath)
        log.Errorf(string(b))
    }
    os.Exit(-1)
}

1. Cleanup就是删除此版本在磁盘上的配置文件.
2. Panic打印完所有失败的配置文件后退出程序.

3.3 Watcher

// pilot/cmd/pilot-agent/main.go
...
tlsCertsToWatch = []string{
   tlsServerCertChain, tlsServerKey, tlsServerRootCert,
   tlsClientCertChain, tlsClientKey, tlsClientRootCert,
}
...
watcher := envoy.NewWatcher(tlsCertsToWatch, agent.ConfigCh())
...
// pilot/pkg/proxy/envoy/watcher.go
func NewWatcher(
    certs []string,
    updates chan<- interface{}) Watcher {
    return &watcher{
        certs:   certs,
        updates: updates,
    }
}
func (w *watcher) Run(ctx context.Context) {
    w.SendConfig()
    go watchCerts(ctx, w.certs, watchFileEvents, defaultMinDelay, w.SendConfig)
    <-ctx.Done()
    log.Info("Watcher has successfully terminated")
}

func (w *watcher) SendConfig() {
    h := sha256.New()
    // 向agent.configCh发送信息
    generateCertHash(h, w.certs)
    w.updates <- h.Sum(nil)
}

这里不展开了, 主要是通过监控tlsCertsToWatch几个文件, 如果发生变化, 则向agent.configCh发送信息.

4. 总结

[istio源码分析][pilot] pilot之pilot_agent_第1张图片
conclusion.png

1. 通过Watcher结构体监控tlsCertsToWatch文件的变化, 然后向a.configCh发信息进而调用reconcile来启动envoy.
2. agent会定时调用reconcile并判断是否需要重启envoy.
3. envoy运行结束(有错误或无错误运行结束)后会向a.statusCh发送信息, 然后agent会做一些此envoy版本的清理工作.

5. 参考

1. istio 1.3.6源码
2. https://segmentfault.com/a/1190000015171622

你可能感兴趣的:([istio源码分析][pilot] pilot之pilot_agent)