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-proxy
的1
号进程是命令/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. 运行agent
和watcher
.
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
,Cleanup
和Panic
方法.1.1
Run
中传入三个参数, 第一个是proxy
运行的时候的config
文件. 第二个是epoch
, 相当于版本号. 第三个是传入的一个channel
, 外部可以通过该channel
杀死proxy
.
1.2Cleanup
传入的是epoch
, 对该版本做一些清理工作. 比如envoy
删除对应的配置文件, 比如传入0
, 就把/etc/istio/proxy/envoy-rev0.json
文件删除.
2.
Agent
有两个方法.2.1
ConfigCh
返回一个config channel用于发送文件更新.agent
会与当前config
比较来决定是否需要重启envoy
, 如果启动失败会以exponential back-off
形式重试.
2.2Run
启动agent
.
3.2 agent
agent
是Agent
的一个实现体.
// 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. 更新当前agent
的currentConfig
, 新版本的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
proxy
在istio
指的就是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.2envoy
运行结束. (错误或者无错误的运行结束)
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. 总结
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