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

image

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

聲明

  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 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

爲何要用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 #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

啓動envoy

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
    }
}

熱更新envoy

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

envoy熱更新策略

image

想詳細瞭解envoy的熱更新策略能夠看官網博客 Envoy hot restart

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

  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 #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

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 #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

image

服務下線或升級咱們都但願它們能很平緩的進行,讓用戶無感知 ,避免打擾用戶。這就要服務收到退出通知後,處理完正在執行的任務才關閉,而不是直接關閉。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 #29
func 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 #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本身不能進行優雅關閉,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 #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上討論優雅關閉機制很類似:
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
相關文章
相關標籤/搜索