Service Mesh深度學習系列|istio原始碼分析之pilot
本文分析的istio程式碼版本為0.8.0,commit為0cd8d67,commit時間為2018年6月18日。
上面是官方關於pilot的架構圖,因為是old_pilot_repo目錄下,可能與最新架構有出入,僅供參考。所謂的pilot包含兩個元件:pilot-agent和pilot-discovery。圖裡的agent對應pilot-agent二進位制,proxy對應envoy二進位制,它們兩個在同一個容器中,discovery service對應pilot-discovery二進位制,在另外一個跟應用分開部署的單獨的deployment中。
- discovery service:從Kubernetes apiserver list/watch service/endpoint/pod/node等資源資訊,監聽istio控制平面配置資訊(Kubernetes CRD),翻譯為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跟應用部署在一起,這個就有點“汙染”應用的意思了。
支援istio新api的sidecar映象為proxyv2,映象中包含的pilot-agent和envoy二進位制檔案和proxy映象中的完全相同,只是使用不同的配置。但是當前僅完成部分開發工作,makefile中build proxyv2映象的target預設也不會自動執行。
以上的HUB和TAG是編譯istio原始碼過程中makefile中的一些變數,HUB對應映象儲存的倉庫,TAG預設為istio版本號,如0.8.0。
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包括以下屬性:
- Typepilot-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。當前只處理以下三種情況:
i. Kubernetes
ii. Consul
iii. 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.terminateterminate方法比較簡單,向所有的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:容器與容器雲》主要作者之一。 更多資訊,歡迎關注“諧雲科技”