Service Mesh深度學習系列|istio源碼分析之pilot-agent組件分析
- 2019 年 12 月 5 日
- 筆記
程式猿能量站:每篇一個小技巧,帶你玩轉IT新時代!
Istio是由Google/IBM/Lyft共同開發的新一代Service Mesh開源項目。本文將從pilot-agent的部署存在形式開始,深入淺出地剖析pilot-agent的各個功能。
註:本文分析的istio程式碼版本為0.8.0,commit為0cd8d67,commit時間為2018年6月18日。

上面是官方關於pilot的架構圖,因為是old_pilot_repo目錄下,可能與最新架構有出入,僅供參考。圖裡的agent對應pilot-agent二進位,proxy對應envoy二進位,它們兩個在同一個容器中,discovery service對應pilot-discovery二進位,在另外一個跟應用分開部署的單獨的deployment中。
- discovery service:從Kubernetes apiserver list/watch service/endpoint/pod/node等資源資訊,翻譯為envoy可以直接理解的配置格式。
- proxy:也就是envoy,直接連接discovery service,間接地從Kubernetes apiserver等服務註冊中心獲取集群中微服務的註冊情況。
- agent:本文分析對象pilot-agent,生成envoy配置文件,管理envoy生命周期。
- service A/B:使用了istio的應用,如Service A/B,的進出網路流量會被proxy接管。
對於模組的命名方法,本文採用模組對應源碼main.go所在包名稱命名法。其他istio分析類似文章有其他命名方法。比如pilot-agent也被稱為istio pilot,因為它在Kubernetes上的部署形式為一個叫istio-pilot的deployment。
pilot-agent的部署存在形式
pilot-agent在pilot/cmd包下面,是個單獨的二進位。 pilot-agent跟envoy打包在同一個docker鏡像里,鏡像由Dockerfile.proxy定義,Makefile(include了tools/istio-docker.mk)把這個dockerfile build成了${HUB}/proxy:${TAG}鏡像,也就是Kubernetes里跟應用放在同一個pod下的sidecar。非Kubernetes情況下需要把pilot-agent、envoy跟應用部署在一起,這就有點「污染」應用的意思了。
pilot-agent功能簡述
在proxy鏡像中,pilot-agent負責的工作包括:
- 生成envoy的配置。
- 啟動envoy。
- 監控並管理envoy的運行狀況,比如envoy出錯時pilot-agent負責重啟envoy,或者envoy配置變更後reload envoy。
而envoy負責接受所有發往該pod的網路流量,分發所有從pod中發出的網路流量。
根據程式碼中的sidecar-injector-configmap.yaml(用來配置如何自動化地inject istio sidecar),inject過程中,除了proxy鏡像作為sidecar之外,每個pod還會帶上initcontainer(Kubernetes中的概念),具體鏡像為proxy_init。proxy_init通過注入iptables規則改寫流入流出pod的網路流量規則,使得流入流出pod的網路流量重定向到proxy的監聽埠,而應用對此無感。
pilot-agent主要功能分析之一:生產envoy配置
envoy的配置主要在pilot-agent的init方法與proxy命令處理流程的前半部分生成。其中init方法為pilot-agent二進位的命令行配置大量的flag與flag默認值,而proxy命令處理流程的前半部分負責將這些flag組裝成為envoy的配置ProxyConfig對象。下面分析幾個相對重要的配置。
role
pilot-agent的role類型為model包下的Proxy,決定了pilot-agent的「角色」,role包括以下屬性:
- Type pilot-agent有三種運行模式。根據role.Type變數定義,類型為model.Proxy,定義在context.go文件中,允許的3個取值範圍為: i. "sidecar" 默認值,可以在啟動pilot-agent,調用proxy命令時覆蓋。Sidecar type is used for sidecar proxies in the application containers. ii. "ingress" Ingress type is used for cluster ingress proxies. iii. "router" Router type is used for standalone proxies acting as L7/L4 routers.
- IPAddress, ID, Domain 它們都可以通過pilot-agent的proxy命令的對應flag來提供用戶自定義值。如果用戶不提供,則會在proxy命令執行時,根據istio連接的服務註冊中心(service registry)類型的不同,會採用不同的配置方式。agent當前使用的具體service registry類型保存在pilot-agent的registry變數里,在init函數中初始化為默認值Kubernetes。當前只處理以下三種情況:
- Kubernetes
- Consul
- Other
registry值 |
role.IPAddress |
rule.ID |
role.Domain |
---|---|---|---|
Kubernetes |
環境變數INSTANCE_IP |
環境變數POD_NAME.環境變數POD_NAMESPACE |
環境變數POD_NAMESPACE.svc.cluster.local |
Consul |
private IP,默認127.0.0.1 |
IPAddress.service.consul |
service.consul |
Other |
private IP,默認127.0.0.1 |
IPAddress |
「」 |
其中的private ip通過WaitForPrivateNetwork函數獲得。
istio需要從服務註冊中心(service registry)獲取微服務註冊的情況。當前版本中istio可以對接的服務註冊中心類型包括:
- "Mock" MockRegistry is a service registry that contains 2 hard-coded test services.
- "Config" ConfigRegistry is a service registry that listens for service entries in a backing ConfigStore.
- "Kubernetes" KubernetesRegistry is a service registry backed by K8s API server.
- "Consul" ConsulRegistry is a service registry backed by Consul.
- "Eureka" EurekaRegistry is a service registry backed by Eureka.
- "CloudFoundry" CloudFoundryRegistry is a service registry backed by Cloud Foundry.
官方about文檔說當前支援Kubernetes, Nomad with Consul,未來準備支援 Cloud Foundry,Apache Mesos。另外根據官方的feature成熟度文檔,當前只有Kubernetes的集成達到stable程度,Consul,Eureka和Cloud Foundry都還是alpha水平。
envoy命令行參數及配置文件
agent.waitForExit會調用envoy.Run方法啟動envoy進程,為此需要獲取envoy二進位所在文件系統路徑和命令行參數兩部分資訊:
- envoy二進位所在文件系統路徑:evony.Run通過proxy.config.BinaryPath變數得知envoy二進位所在的文件系統位置,proxy就是envoy對象,config就是pilot-agent的main方法在一開始初始化的proxyConfig對象。裡面的BinaryPath在pilot-agent的init方法中被初始化,初始值來自pilot/pkg/model/context.go的DefaultProxyConfig函數,值是/usr/local/bin/envoy。
- envoy的啟動參數形式為下面的startupArgs,包含一個-c指定的配置文件,還有一些命令行參數。除了下面程式碼片段中展示的這些參數,還可以根據agent啟動參數,再加上–concurrency, –service-zone等參數。
startupArgs := []string{"-c", fname, "--restart-epoch", fmt.Sprint(epoch), "--drain-time-s", fmt.Sprint(int(convertDuration(proxy.config.DrainDuration) / time.Second)), "--parent-shutdown-time-s", fmt.Sprint(int(convertDuration(proxy.config.ParentShutdownDuration) / time.Second)), "--service-cluster", proxy.config.ServiceCluster, "--service-node", proxy.node, "--max-obj-name-len", fmt.Sprint(MaxClusterNameLength), }
以上envoy命令行參數及其來源:
- –restart-epoch:epoch決定了envoy hot restart的順序,在後面會有詳細描述,第一個envoy進程對應的epoch為0,後面新建的envoy進程對應epoch順序遞增1。
- –drain-time-s:在pilot-agent init函數中指定默認值為2秒,可通過pilot-agent proxy命令的drainDuration flag指定。
- –parent-shutdown-time-s:在pilot-agent init函數中指定默認值為3秒,可通過pilot-agent proxy命令的parentShutdownDuration flag指定。
- –service-cluster:在pilot-agent init函數中指定默認值為"istio-proxy",可通過pilot-agent proxy命令的serviceCluster flag指定。
- –service-node:將agent.role的Type,IPAddress,ID和Domain屬性用"~"連接起來
而上面的-c指定的envoy配置文件有幾種生成的方式:
- 運行pilot-agent時,用戶不指定customConfigFile參數(agent init時默認為空),但是制定了templateFile參數(agent init時默認為空),這時agent的main方法會根據templateFile幫用戶生成一個customConfigFile,後面就視作用戶制定了customConfigFile。這個流程在agent的main方法里。
- 如果用戶制定了customConfigFile,那麼就用customConfigFile。
- 如果用戶customConfigFile和templateFile都沒指定,則調用pilot/pkg包下的bootstrap_config.go中的WriteBootstrap自動生成一個配置文件,默認將生成的配置文件放在/etc/istio/proxy/envoy-rev%d.json,這裡的%d會用epoch序列號代替。WriteBootstrap在envoy.Run方法中被調用。
舉個例子的話,根據參考文獻中某人實驗,第一個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配置文件,如上面例子中的envoy-rev0.json,那麼pilot-agent的proxy命令處理流程中前半部分整理的大量envoy參數中的一部分會被寫入這個配置文件中,比如DiscoveryAddress,DiscoveryRefreshDelay,ZipkinAddress,StatsdUdpAddress。
證書文件
agent會監控chainfile,keyfile和rootcert三個證書文件的變化,如果是Ingress工作模式,則還會加入ingresscert,ingress key這2個證書文件。
pilot-agent主要功能分析之二:envoy監控與管理
為envoy生成好配置文件之後,pilot-agent還要負責envoy進程的監控與管理工作,包括:
- 創建envoy對象,結構體包含proxyConfig(前面步驟中為envoy生成的配置資訊),role.serviceNode(似乎是agent唯一標識符),loglevel和pilotsan(service account name)。
- 創建agent對象,包含前面創建的envoy結構體,一個epochs的map,3個channel:configCh, statusCh和abortCh。
- 創建watcher並啟動協程執行watcher.Runwatcher.Run首先啟動協程執行agent.Run(agent的主循環),然後調用watcher.Reload(kickstart the proxy with partial state (in case there are no notifications coming)),Reload會調用agent.ScheduleConfigUpdate,並最終導致第一個envoy進程啟動,見後面分析。然後監控各種證書,如果證書文件發生變化,則調用ScheduleConfigUpdate來reload envoy,然後watcher.retrieveAZ(TODO)。
- 調用cmd.WaitSignal,等待進程接收到SIGINT, SIGTERM訊號,接受到訊號之後會kill所有envoy進程,並退出agent進程。
上面第三步啟動協程執行的agent.Run是agent的主循環,會一直通過監聽以下幾個channel來監控envoy進程:
- agent的configCh:如果配置文件,主要是那些證書文件發生變化,則調用agent.reconcile來reload envoy。
- statusCh:這裡的status其實就是exitStatus,處理envoy進程退出狀態,處理流程如下: i. 把剛剛退出的epoch從agent維護的兩個map里刪了,後面會講到這兩個map。把agent.currentConfig置為agent.latestEpoch對應的config,因為agent在reconcile的過程中只有在desired config和current config不同的時候才會創建新的epoch,所以這裡把currentConfig設置為上一個config之後,必然會造成下一次reconcile的時候current與desired不等,從而創建新的envoy。 ii. 如果exitStatus.err是errAbort,表示是agent讓envoy退出的(這個error是調用agent.abortAll時發出的),這時只要log記錄epoch序列號為xxx的envoy進程退出了。 iii. 如果exitStatus.err並非errAbort,則log記錄epoch異常退出,並給所有當前正在運行的其他envoy進程對應的abortCh發出errAbort。所以後續其他envoy進程也都會被kill掉,並全都往agent.statusCh寫入exitStatus,當前的流程會全部再為每個epoch進程走一遍。 iv. 如果是其他exitStatus(什麼時候會進入這個情況?比如exitStatus.err是wait epoch進程得到的正常退出資訊,即nil),則log記錄epoch正常退出。 v. 調用envoy.Cleanup,刪除剛剛退出的envoy進程對應的配置文件,文件路徑由ConfigPath和epoch序列號串起來得到。 vi. 如果envoy進程為非正常退出,也就是除了「否則」描述的case之外的兩種情況,則試圖恢復剛剛退出的envoy進程(可見前面向所有其他進程發出errAbort消息的意思,並非永遠停止envoy,pilot-agent接下來馬上就會重啟被abort的envoy)。恢復方式並不是當場啟動新的envoy,而是schedule一次reconcile。如果啟動不成功,可以在得到exitStatus之後再次schedule(每次間隔時間為2ⁿ*200毫秒),最多重試10次(budget),如果10次都失敗,則退出整個golang的進程(os.Exit),由容器運行時環境決定如何恢復pilot-agent。所謂的schedule,就是往agent.retry.restart寫入一個預定的未來的某個時刻,並扣掉一次budget(budget在每次reconcile之前都會被重置為10),然後就結束當前循環。在下一個循環開始的時候,會檢測agent.retry.restart,如果非空,則計算距離reconcile的時間delay。
- time.After(delay):監聽是否到時間執行schedule的reconcile了,到了則執行agent.reconcile。
- ctx.Done:執行agent.terminate terminate方法比較簡單,向所有的envoy進程的abortCh發出errAbort消息,造成他們全體被kill(Cmd.Kill),然後agent自己return,退出當前的循環,這樣就不會有人再去重啟envoy。
pilot-agent主要功能分析之三:envoy啟動流程
- 前面pilot-agent proxy命令處理流程中,watcher.Run會調用agent.ScheduleConfigUpdate,這個方法只是簡單地往configCh里寫一個新的配置,所謂的配置是所有certificate算出的sha256哈希值。
- configCh的這個事件會被agent.Run監控到,然後調用agent.reconcile。
- reconcile方法會啟動協程執行agent.waitForExit從而啟動envoy看reconcile方法名就知道是用來保證desired config和current config保持一致的。reconcile首先會檢查desired config和current config是否一致,如果是的話,就不用啟動新的envoy進程。否則就啟動新的envoy。在啟動過程中,agent維護兩個map來管理一堆envoy進程,在調用waitForExit之前會將desiredConfig賦值給currentConfig,表示reconcile工作完成: i. 第一個map是agent.epochs,它將整數epoch序列號映射到agent.desiredConfig。這個序列號從0開始計數,也就是第一個envoy進程對應epoch 0,後面遞增1。但是如果有envoy進程異常退出,它對應的序列號並非是最大的情況下,這個空出來的序列號不會在計算下一個新的epoch序列號時(agent.latestEpoch方法負責計算當前最大的epoch序列號)被優先使用。所以從理論上來說序列號是會被用光的。 ii. 第二個map是agent.abortCh,它將epoch序列號映射到與envoy進程一一對應的abortCh。abortCh使得pilot-agent可以在必要時通知對應的envoy進程推出。這個channel初始化buffer大小為常量10,至於為什麼需要10個buffer,程式碼中的注釋說buffer aborts to prevent blocking on failing proxy,也就是萬一想要abort某個envoy進程,但是envoy卡住了abort不了,有buffer的話,就不會使得管理進程也卡住。
- waitForExit會調用agent.proxy.Run,也就是envoy的Run方法,這裡會啟動envoy。envoy的Run方法流程如下:
- 調用exec.Cmd.Start方法(啟動了一個新進程),並將envoy的標準輸出和標準錯誤置為os.Stdout和Stderr。
- 持續監聽前面說到由agent創建並管理的,並與envoy進程一一對應的abortCh,如果收到abort事件通知,則會調用Cmd.Process.Kill方法殺掉envoy,如果殺進程的過程中發生錯誤,也會把錯誤資訊log一下,然後把從abortCh讀到的事件返回給waitForExit。waitForExit會把該錯誤再封裝一下,加入epoch序列號,然後作為envoy的exitStatus,並寫入到agent.statusCh里。
- 啟動一個新的協程來wait剛剛啟動的envoy進程,並把得到的結果寫到done channel里,envoy結構體的Run方法也會監聽done channel,並把得到的結果返回給waitForExit這裡我們總結啟動envoy過程中的協程關係:agent是全局唯一一個agent協程,它在啟動每個envoy的時候,會再啟動一個waitForExit協程,waitForExit會調用Command.Start啟動另外一個進程運行envoy,然後waitForExit負責監聽abortCh和envoy進程執行結果。
Cmd.Wait只能用於等待由Cmd.Start啟動的進程,如果進程結束並範圍值為0,則返回nil,如果返回其他值則返回ExitError,也可能在其他情況下返回IO錯誤等,Wait會釋放Cmd所佔用的所有資源。
每次配置發生變化,都會調用agent.reconcile,也就會啟動新的envoy,這樣envoy越來越多,老的envoy進程怎麼辦?agent程式碼的注釋里已經解釋了這問題,原來agent不用關閉老的envoy,同一台機器上的多個envoy進程會通過unix domain socket互相通訊,即使不同envoy進程運行在不同容器里,也一樣能夠通訊。而藉助這種通訊機制,可以自動實現新envoy進程替換之前的老進程,也就是所謂的envoy hot restart。
程式碼注釋原文:Hot restarts are performed by launching a new proxy process with a strictly incremented restart epoch. It is up to the proxy to ensure that older epochs gracefully shutdown and carry over all the necessary state to the latest epoch. The agent does not terminate older epochs.
而為了觸發這種hot restart的機制,讓新envoy進程替換之前所有的envoy進程,新啟動的envoy進程的epoch序列號必須比之前所有envoy進程的最大epoch序列號大1。
程式碼注釋原文:The restart protocol matches Envoy semantics for restart epochs: to successfully launch a new Envoy process that will replace the running Envoy processes, the restart epoch of the new process must be exactly 1 greater than the highest restart epoch of the currently running Envoy processes.
參考文獻
下一代 Service Mesh — istio 架構分析
istio源碼分析——pilot-agent如何管理envoy生命周期
作者簡介:
丁軼群,諧雲科技CTO
2004年作為高級技術顧問加入美國道富銀行(浙江)技術中心,負責分散式大型金融系統的設計與研發。2011年開始領導浙江大學開源雲計算平台的研發工作,是浙江大學SEL實驗室負責人,2013年獲得浙江省第一批青年科學家稱號,CNCF會員,多次受邀在Cloud Foundry, Docker大會上發表演講,《Docker:容器與容器雲》主要作者之一。