istio源碼分析——pilot-agent如何管理envoy生命週期

來源:互聯網
上載者:User

原文:istio源碼分析——pilot-agent如何管理envoy生命週期

聲明

  1. 分析的源碼為0.7.1版本
  2. 環境為k8s
  3. 由於沒有C++ 基礎,所以源碼分析止步於 C++,但也學到很多東西

pilot-agent 是什嗎?

 當我們執行 kubectl apply -f <(~istioctl kube-inject -f sleep.yaml) 的時候,k8s就會幫我們建立3個容器。
[root@izwz9cffi0prthtem44cp9z ~]# docker ps |grep sleep8e0de7294922        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 -efUID        PID  PPID  C STIME TTY          TIME CMD1337         1     0  0 May09 ?        00:00:49 /usr/local/bin/pilot-agent proxy1337       567     1  1 09:18 ?        00:04:42 /usr/local/bin/envoy -c /etc/ist

為什麼要用pilot-agent?

envoy不直接和k8s,Consul,Eureka等這些平台互動,所以需要其他服務與它們對接,管理配置,pilot-agent就是其中一個 【控制台】

啟動envoy

載入配置

在啟動前 pilot-agent 會產生一個設定檔:/etc/istio/proxy/envoy-rev0.json:
istio.io/istio/pilot/pkg/proxy/envoy/v1/config.go #88func 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 #274func (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

啟動envoy

pilot-agent 使用 exec.Command啟動envoy,並且會監聽envoy的運行狀態(如果envoy非正常退出,status 返回非nil,pilot-agent會有策略把envoy重新啟動)。

proxy.config.BinaryPath 為envoy二進位檔案路徑:/usr/local/bin/envoy。

args 為上面介紹的envoy啟動參數。

istio.io/istio/pilot/pkg/proxy/envoy/v1/watcher.go #353func (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    }}

熱更新envoy

在這裡我們只討論pilot-agent如何讓envoy熱更新,至於如何去觸發這步會在後面的文章介紹。

envoy熱更新策略

想詳細瞭解envoy的熱更新策略可以看官網部落格Envoy hot restart。

簡單介紹下envoy熱更新步驟:

  1. 啟動另外一個envoy2進程(Secondary process)
  2. envoy2通知envoy1(Primary process)關閉其管理的連接埠,由envoy2接管
  3. 通過UDS把envoy1可用的listen sockets拿過來
  4. envoy2初始化成功,通知envoy1在一段時間內(drain-time-s)優雅關閉正在工作的請求
  5. 到了時間(parent-shutdown-time-s),envoy2通知envoy1自行關閉
  6. envoy2升級為envoy1
從上面的執行步驟來看,poilt-agent只負責啟動另一個envoy進程,其他由envoy自行處理。

什麼時候進行熱更新?

在poilt-agent啟動的時候,會監聽 /etc/certs/目錄下的檔案,如果這個目錄下的檔案被修改或刪除,poilt-agent就會通知envoy進行熱更新。至於如何觸發對這些檔案進行修改和刪除會在接下來的文章介紹。
istio.io/istio/pilot/pkg/proxy/envoy/v1/watcher.go #177func 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-nodesidecar~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 #258func (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": CREATE2018-04-24T13:59:35.513228Z    info    watchFileEvents: "/etc/certs//..2018_04_24_13_59_35.824521609": MODIFY|ATTRIB2018-04-24T13:59:35.513283Z    info    watchFileEvents: "/etc/certs//..data_tmp": RENAME2018-04-24T13:59:35.513347Z    info    watchFileEvents: "/etc/certs//..data": CREATE2018-04-24T13:59:35.513372Z    info    watchFileEvents: "/etc/certs//..2018_04_24_04_30_11.964751916": DELETE

搶救envoy

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()    }()    ......}

搶救envoy

使用 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 #164func (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 #72var (  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 9Epoch 6: set retry delay to 400ms, budget to 8Epoch 6: set retry delay to 800ms, budget to 7

優雅關閉envoy

服務下線或升級我們都希望它們能很平緩的進行,讓使用者無感知 ,避免打擾使用者。這就要服務收到退出通知後,處理完正在執行的任務才關閉,而不是直接關閉。envoy是否支援優雅關閉?這需要k8s,pilot-agent也支援這種玩法。因為這存在一種關聯關係k8s管理pilot-agent,pilot-agent管理envoy。

k8s讓服務優雅退出

網上有篇部落格總結了k8s優雅關閉pods,我這邊簡單介紹下優雅關閉流程:
  1. k8s 發送 SIGTERM 訊號到pods下所有服務的1號進程
  2. 服務接收到訊號後,優雅關閉任務,並退出
  3. 過了一段時間(default 30s),如果服務沒有退出,k8s會發送 SIGKILL 訊號,讓容器強制退出。

pilot-agent 讓envoy優雅退出

  • pilot-agent接收k8s訊號
pilot-agent會接收syscall.SIGINT, syscall.SIGTERM,這2個訊號都可以達到優雅關閉envoy的效果。
istio.io/istio/pkg/cmd/cmd.go #29func WaitSignal(stop chan struct{}) {    sigs := make(chan os.Signal, 1)    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)    <-sigs    close(stop)    _ = log.Sync()}
  • 通知子服務關閉envoy
在golang有一個上下文管理組件 context,這個包通過廣播的方式通知各子服務執行關閉操作。
istio.io/istio/pilot/cmd/pilot-agent/main.go #242ctx, 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.gofunc (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 #297func (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自己不能進行優雅關閉,envoy會接收SIGTERM,SIGHUP,SIGCHLD,SIGUSR1這4個訊號,但是這4個都與優雅無關,這4個訊號的作用可看官方文檔。當然官方也注意到這個問題,可以到github瞭解一下2920 3307。
  • 替代方案
其實使用優雅關閉想達到的目的是:讓服務平滑升級,減少對使用者的影響。所以我們可以用金絲雀部署來實現,並非一定要envoy實現。大致的流程:
  1. 定義服務的舊版本(v1),新版本(v2)
  2. 發布新版本
  3. 將流量按照梯度的方式,慢慢遷移到v2
  4. 遷移完成,運行一段時間,沒問題就關閉v1
  • golang 優雅退出HTTP服務
藉此機會瞭解下golang的優雅關閉,golang在1.8版本的時候就支援這個特性
net/http/server.go #2487func (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上討論優雅關閉機制很相似:
golang機制
  1. 關閉監聽者(ln, err := net.Listen("tcp", addr),向ln賦nil)
  2. 定時查詢是否有未關閉的連結
  3. 所有連結都是退出,服務退出
envoy機制:
  1. ingress listeners stop accepting new connections (clients see TCP connection refused) but continues to service existing connections. egress listeners are completely unaffected
  2. configurable delay to allow workload to finish servicing existing connections
  3. envoy (and workload) both terminate
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.