原文:istio源码分析——pilot-agent如何管理envoy生命周期html
当咱们执行
kubectl apply -f <(~istioctl kube-inject -f sleep.yaml)
的时候,k8s就会帮咱们创建3个容器。
[root@izwz9cffi0prthtem44cp9z ~]# docker ps |grep sleep 8e0de7294922 istio/proxy ccddc800b2a2 registry.cn-shenzhen.aliyuncs.com/jukylin/sleep 990868aa4a42 registry-vpc.cn-shenzhen.aliyuncs.com/acs/pause-amd64:3.0
在这3个容器中,咱们关注istio/proxy
。这个容器运行着2个服务。pilot-agent
就是接下来介绍的:如何管理envoy的生命周期。
[root@izwz9cffi0prthtem44cp9z ~]# docker exec -it 8e0de7294922 ps -ef UID PID PPID C STIME TTY TIME CMD 1337 1 0 0 May09 ? 00:00:49 /usr/local/bin/pilot-agent proxy 1337 567 1 1 09:18 ? 00:04:42 /usr/local/bin/envoy -c /etc/ist
envoy不直接和k8s,Consul,Eureka等这些平台交互,因此须要其余服务与它们对接,管理配置,pilot-agent就是其中一个 【控制面板】。
在启动前 pilot-agent 会生成一个配置文件:/etc/istio/proxy/envoy-rev0.json:
istio.io/istio/pilot/pkg/proxy/envoy/v1/config.go #88 func BuildConfig(config meshconfig.ProxyConfig, pilotSAN []string) *Config { ...... return out }
文件的具体内容能够直接查看容器里面的文件
docker exec -it 8e0de7294922 cat /etc/istio/proxy/envoy-rev0.json
关于配置内容的含义能够看 官方的文档
一个二进制文件启动总会须要一些参数,envoy也不例外。
istio.io/istio/pilot/pkg/proxy/envoy/v1/watcher.go #274 func (proxy envoy) args(fname string, epoch int) []string { ...... return startupArgs }
envoy启动参数能够经过
docker logs 8e0de7294922
查看,下面是从终端截取envoy的参数。了解具体的参数含义
官网文档。
-c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster sleep --service-node sidecar~172.00.00.000~sleep-55b5877479-rwcct.default~default.svc.cluster.local --max-obj-name-len 189 -l info --v2-config-only
pilot-agent 使用exec.Command
启动envoy,而且会监听envoy的运行状态(若是envoy非正常退出,status 返回非nil,pilot-agent会有策略把envoy从新启动)。
proxy.config.BinaryPath
为envoy二进制文件路径:/usr/local/bin/envoy。node
args
为上面介绍的envoy启动参数。git
istio.io/istio/pilot/pkg/proxy/envoy/v1/watcher.go #353 func (proxy envoy) Run(config interface{}, epoch int, abort <-chan error) error { ...... /* #nosec */ cmd := exec.Command(proxy.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 { case err := <-abort: ...... case err := <-done: return err } }
在这里咱们只讨论pilot-agent如何让envoy热更新,至于如何去触发这步会在后面的文章介绍。
想详细了解envoy的热更新策略能够看官网博客 Envoy hot restart。简单介绍下envoy热更新步骤:github
drain-time-s
)优雅关闭正在工做的请求parent-shutdown-time-s
),envoy2通知envoy1自行关闭从上面的执行步骤来看,poilt-agent只负责启动另外一个envoy进程,其余由envoy自行处理。
在poilt-agent启动的时候,会监听
/etc/certs/
目录下的文件,若是这个目录下的文件被修改或删除,poilt-agent就会通知envoy进行热更新。至于如何触发对这些文件进行修改和删除会在接下来的文章介绍。
istio.io/istio/pilot/pkg/proxy/envoy/v1/watcher.go #177 func watchCerts(ctx context.Context, certsDirs []string, watchFileEventsFn watchFileEventsFn, minDelay time.Duration, updateFunc func()) { fw, err := fsnotify.NewWatcher() if err != nil { log.Warnf("failed to create a watcher for certificate files: %v", err) return } defer func() { if err := fw.Close(); err != nil { log.Warnf("closing watcher encounters an error %v", err) } }() // watch all directories for _, d := range certsDirs { if err := fw.Watch(d); err != nil { log.Warnf("watching %s encounters an error %v", d, err) return } } watchFileEventsFn(ctx, fw.Event, minDelay, updateFunc) }
-c /etc/istio/proxy/envoy-rev1.json --restart-epoch 1 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster sleep --service-node sidecar~172.00.00.000~sleep-898b65f84-pnsxr.default~default.svc.cluster.local --max-obj-name-len 189 -l info --v2-config-only
热更新启动参数和第一次启动参数的不一样的地方是 -c 和 --restart-epoch,其实-c 只是配置文件名不一样,它们的内容是同样的。--restart-epoch 每次进行热更新的时候都会自增1,用于判断是进行热更新仍是打开一个存在的envoy(这里的意思应该是第一次打开envoy)
具体看官方描述
istio.io/istio/pilot/pkg/proxy/agent.go #258 func (a *agent) reconcile() { ...... // discover and increment the latest running epoch epoch := a.latestEpoch() + 1 // buffer aborts to prevent blocking on failing proxy abortCh := make(chan error, MaxAborts) a.epochs[epoch] = a.desiredConfig a.abortCh[epoch] = abortCh a.currentConfig = a.desiredConfig go a.waitForExit(a.desiredConfig, epoch, abortCh) }
2018-04-24T13:59:35.513160Z info watchFileEvents: "/etc/certs//..2018_04_24_13_59_35.824521609": CREATE 2018-04-24T13:59:35.513228Z info watchFileEvents: "/etc/certs//..2018_04_24_13_59_35.824521609": MODIFY|ATTRIB 2018-04-24T13:59:35.513283Z info watchFileEvents: "/etc/certs//..data_tmp": RENAME 2018-04-24T13:59:35.513347Z info watchFileEvents: "/etc/certs//..data": CREATE 2018-04-24T13:59:35.513372Z info watchFileEvents: "/etc/certs//..2018_04_24_04_30_11.964751916": DELETE
envoy是一个服务,既然是服务都不可能保证100%的可用,若是envoy不幸运宕掉了,那么pilot-agent如何进行抢救,保证envoy高可用?
在上面提到pilot-agent启动envoy后,会监听envoy的退出状态,发现非正常退出状态,就会抢救envoy。
func (proxy envoy) Run(config interface{}, epoch int, abort <-chan error) error { ...... // Set if the caller is monitoring envoy, for example in tests or if envoy runs in same // container with the app. if proxy.errChan != nil { // Caller passed a channel, will wait itself for termination go func() { proxy.errChan <- cmd.Wait() }() return nil } done := make(chan error, 1) go func() { done <- cmd.Wait() }() ...... }
使用 kill -9 能够模拟envoy非正常退出状态。当出现非正常退出,pilot-agent的抢救机制会被触发。若是第一次抢救成功,那固然是好,若是失败了,pilot-agent会继续抢救,最多抢救10次,每次间隔时间为 2 n 100 time.Millisecond。超过10次都没有救活,pilit-agent就会放弃抢救,宣布死亡,而且退出istio/proxy,让k8s从新启动一个新容器。
istio.io/istio/pilot/pkg/proxy/agent.go #164 func (a *agent) Run(ctx context.Context) { ...... for { ...... select { ...... case status := <-a.statusCh: ...... if status.err == errAbort { //pilot-agent通知退出 或 envoy非正常退出 log.Infof("Epoch %d aborted", status.epoch) } else if status.err != nil { //envoy非正常退出 log.Warnf("Epoch %d terminated with an error: %v", status.epoch, status.err) ...... a.abortAll() } else { //正常退出 log.Infof("Epoch %d exited normally", 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 = a.retry.budget - 1 log.Infof("Epoch %d: set retry delay to %v, budget to %d", status.epoch, delayDuration, a.retry.budget) } else { //宣布死亡,退出istio/proxy log.Error("Permanent error: budget exhausted trying to fulfill the desired configuration") a.proxy.Panic(a.desiredConfig) return } } else { log.Debugf("Epoch %d: restart already scheduled", status.epoch) } } case <-time.After(delay): ...... case _, more := <-ctx.Done(): ...... } } }
istio.io/istio/pilot/pkg/proxy/agent.go #72 var ( errAbort = errors.New("epoch aborted") // DefaultRetry configuration for proxies DefaultRetry = Retry{ MaxRetries: 10, InitialInterval: 200 * time.Millisecond, } )
Epoch 6: set retry delay to 200ms, budget to 9 Epoch 6: set retry delay to 400ms, budget to 8 Epoch 6: set retry delay to 800ms, budget to 7
服务下线或升级咱们都但愿它们能很平缓的进行,让用户无感知 ,避免打扰用户。这就要服务收到退出通知后,处理完正在执行的任务才关闭,而不是直接关闭。envoy是否支持优雅关闭?这须要k8s,pilot-agent也支持这种玩法。由于这存在一种关联关系k8s管理pilot-agent,pilot-agent管理envoy。
网上有篇博客总结了 k8s优雅关闭pods,我这边简单介绍下优雅关闭流程:
pilot-agent会接收syscall.SIGINT, syscall.SIGTERM,这2个信号均可以达到优雅关闭envoy的效果。
istio.io/istio/pkg/cmd/cmd.go #29 func WaitSignal(stop chan struct{}) { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs close(stop) _ = log.Sync() }
在golang有一个上下文管理包
context
,这个包经过广播的方式通知各子服务执行关闭操做。
istio.io/istio/pilot/cmd/pilot-agent/main.go #242 ctx, cancel := context.WithCancel(context.Background()) go watcher.Run(ctx) stop := make(chan struct{}) cmd.WaitSignal(stop) <-stop //通知子服务 cancel() istio.io/istio/pilot/pkg/proxy/agent.go func (a *agent) Run(ctx context.Context) { ...... for { ...... select { ...... //接收到主服务信息通知envoy退出 case _, more := <-ctx.Done(): if !more { a.terminate() return } } } } istio.io/istio/pilot/pkg/proxy/envoy/v1/watcher.go #297 func (proxy envoy) Run(config interface{}, epoch int, abort <-chan error) error { ...... select { case err := <-abort: log.Warnf("Aborting epoch %d", epoch) //发送 KILL信号给envoy if errKill := cmd.Process.Kill(); errKill != nil { log.Warnf("killing epoch %d caused an error %v", epoch, errKill) } return err ...... } }
上面展现了pilot-agent从k8s接收信号到通知envoy关闭的过程,这个过程说明了poilt-agent也是支持优雅关闭。但最终envoy并不能进行优雅关闭,这和pilot-agent发送KILL信号不要紧,这是由于envoy自己就不支持。
来到这里很遗憾通知你envoy本身不能进行优雅关闭,envoy会接收SIGTERM,SIGHUP,SIGCHLD,SIGUSR1这4个信号,可是这4个都与优雅无关,这4个信号的做用可看 官方文档。固然官方也注意到这个问题,能够到github了解一下 2920 3307。
其实使用优雅关闭想达到的目的是:让服务平滑升级,减小对用户的影响。因此咱们能够用 金丝雀部署来实现,并不是必定要envoy实现。大体的流程:
借此机会了解下golang的优雅关闭,golang在1.8版本的时候就支持这个特性
net/http/server.go #2487 func (srv *Server) Shutdown(ctx context.Context) error { atomic.AddInt32(&srv.inShutdown, 1) defer atomic.AddInt32(&srv.inShutdown, -1) srv.mu.Lock() // 把监听者关掉 lnerr := srv.closeListenersLocked() srv.closeDoneChanLocked() //执行开发定义的函数若是有 for _, f := range srv.onShutdown { go f() } srv.mu.Unlock() //定时查询是否有未关闭的连接 ticker := time.NewTicker(shutdownPollInterval) defer ticker.Stop() for { if srv.closeIdleConns() { return lnerr } select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } } }
其实golang的关闭机制和envoy在github上讨论优雅关闭机制很类似:
ln, err := net.Listen("tcp", addr)
,向ln赋nil)