原文: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)