Istio 服務註冊外掛機制程式碼解析
Istio服務註冊外掛機制
在Istio架構中,Pilot元件負責維護網格中的標準服務模型,該標準服務模型獨立於各種底層平臺,Pilot通過介面卡和各底層平臺對接,以使用底層平臺中的服務資料填充此標準模型。
例如Pilot中的Kubernetes介面卡通過Kubernetes API Server到kubernetes中的Service以及對應的POD例項,將該資料被翻譯為標準模型提供給Pilot使用。通過介面卡模式,Pilot還可以從Cloud Foundry, Consul中獲取服務資訊,也可以開發介面卡將其他提供服務發現的元件整合到Pilot中。
本文將從程式碼出發,對Pilot的服務註冊機制進行分析。
備註: 本文分析的程式碼對應Istio commit 58186e1dc3392de842bc2b2c788f993878e0f123
服務註冊相關的物件
首先我們來了解一下Pilot中關於服務註冊的一些基本概念和相關資料結構。
Istio原始碼中,和服務註冊相關的物件如下面的UML類圖所示。
Service
原始碼檔案:pilot/pkg/model/service.go
Service用於表示Istio服務網格中的一個服務(例如 catalog.mystore.com:8080)。每一個服務有一個全限定域名(FQDN)和一個或者多個接收客戶端請求的監聽埠。
一個服務可以有一個可選的 負載均衡器/虛擬IP,DNS解析會對應到該虛擬IP(負載均衡器的IP)上。 一般來說,不管後端的服務例項如何變化,VIP是不會變化的,Istio會維護VIP和後端例項真實IP的對應關係。
例如在Kubernetes中,服務 foo 的FQDN為foo.default.svc.cluster.local, 擁有一個虛擬IP 10.0.1.1,在埠80和8080上監聽客戶端請求。
type Service struct { // Hostn/伺服器名 Hostname Hostname `json:"hostname"` // 虛擬IP / 負載均衡器 IP Address string `json:"address,omitempty"` // 如果服務部署在多個叢集中,ClusterVIPs會儲存不同叢集中該服務對應的VIP ClusterVIPs map[string]string `json:"cluster-vips,omitempty"` // 服務埠列表 Ports PortList `json:"ports,omitempty"` // 執行該服務的服務賬號 ServiceAccounts []string `json:"serviceaccounts,omitempty"` // 該服務是否為一個 “外部服務”, 採用 ServiceEntry 定義的服務該標誌為true MeshExternal bool // 服務解析規則: 包括 // ClientSideLB: 由Envoy代理根據其本地的LB pool進行請求路由 // DNSLB: 查詢DNS伺服器得到IP地址,並將請求發到該IP // Passthrough: 將請求發轉發到其原始目的地 Resolution Resolution // 服務建立時間 CreationTime time.Time `json:"creationTime,omitempty"` // 服務的一些附加屬性 Attributes ServiceAttributes }
ServiceInstance
原始碼檔案:pilot/pkg/model/service.go
SercieInstance中存放了服務例項相關的資訊,一個Service可以對應到一到多個Service Instance,Istio在收到客戶端請求時,會根據該Service配置的LB策略和路由規則從可用的Service Instance中選擇一個來提供服務。
type ServiceInstance struct { // Endpoint中包括服務例項的IP:Port,UID等 EndpointNetworkEndpoint `json:"endpoint,omitempty"` // 對應的服務 Service*Service`json:"service,omitempty"` // 該例項上的標籤,例如版本號 LabelsLabels`json:"labels,omitempty"` // 執行該服務的服務賬號 ServiceAccount string`json:"serviceaccount,omitempty"` }
Registry
原始碼檔案: pilot/pkg/serviceregistry/aggregate/controller.go
Registry代表一個通過介面卡插入到Pilot中的服務登錄檔,即Kubernetes,Cloud Foundry 或者 Consul 等具體後端的服務部署/服務註冊發現平臺。
Registry結構體中包含了Service Registry相關的一些介面和屬性。
type Registry struct { // 登錄檔的型別,例如Kubernetes, Consul, 等等。 Name serviceregistry.ServiceRegistry // 某些型別的服務登錄檔支援多叢集,例如Kubernetes,在這種情況下需要用CluterID來區分同一型別下不同叢集的服務登錄檔 ClusterID string // 控制器,負責向外傳送該Registry相關的Service變化訊息 model.Controller // 服務發現介面,用於獲取登錄檔中的服務資訊 model.ServiceDiscovery }
Istio支援以下幾種服務登錄檔型別:
原始碼檔案: pilot/pkg/serviceregistry/platform.go
// ServiceRegistry defines underlying platform supporting service registry type ServiceRegistry string const ( // MockRegistry,用於測試的服務登錄檔,包含兩個硬編碼的test services MockRegistry ServiceRegistry = "Mock" // ConfigRegistry,可以從Configstore中獲取定義的service registry,加入到Istio的服務列表中 KubernetesRegistry ServiceRegistry = "Kubernetes" // 從Consul獲取服務資料的服務登錄檔 ConsulRegistry ServiceRegistry = "Consul" // 採用“Mesh Configuration Protocol”的服務登錄檔 MCPRegistry ServiceRegistry = "MCP" )
其中支援最完善的就是Kubernetes了,我在專案中使用了Consul,填坑的經驗證明對Consul的支援只是原型驗證級別的,要在產品中使用的話還需要對其進行較多的改進和優化。
登錄檔中最後一個型別是 MCP,MCP 是 “Mesh Configuration Protocol” 的縮寫。 Istio 使用了 MCP 實現了一個服務註冊和路由配置的標準介面,MCP Server可以從Kubernetes,Cloud Foundry, Consul等獲取服務資訊和配置資料,並將這些資訊通過MCP提供給 MCP Client,即Pilot,通過這種方式,將目前特定平臺的相關的程式碼從Pilot中剝離到獨立的MCP伺服器中,使Pilot的架構和程式碼更為清晰。MCP將逐漸替換目前的各種Adapter。更多關於MCP的內容參見:
- https://docs.google.com/document/d/1o2-V4TLJ8fJACXdlsnxKxDv2Luryo48bAhR8ShxE5-k/edit
- https://docs.google.com/document/d/1S5ygkxR1alNI8cWGG4O4iV8zp8dA6Oc23zQCvFxr83U/edit
Controller
原始碼檔案: pilot/pkg/model/controller.go
Controller抽象了一個Service Registry變化通知的介面,該介面會將Service及Service Instance的增加,刪除,變化等訊息通知給ServiceHandler。
呼叫Controller的Run方法後,Controller會一直執行,將監控Service Registry的變化,並將通知到註冊到Controller中的ServiceHandler中。
type Controller interface { // 新增一個Service Handler,服務的變化會通知到該Handler AppendServiceHandler(f func(*Service, Event)) error // 新增一個Service Instance Handler, 服務例項的變化會通知到該Handler AppendInstanceHandler(f func(*ServiceInstance, Event)) error // 啟動Controller的主迴圈,對Service Catalog的變化進行分發 Run(stop <-chan struct{}) }
ServiceDiscovery
原始碼檔案: pilot/pkg/model/service.go
ServiceDiscovery抽象了一個服務發現的介面,可以通過該介面獲取到Service Registry中的Service和Service Instance。
type ServiceDiscovery interface { // 列出該Service Registry中的所有服務 Services() ([]*Service, error) // 根據主機名查詢服務 // 該介面已廢棄 GetService(hostname Hostname) (*Service, error) // 根據主機名,服務端點和標籤查詢服務例項 InstancesByPort(hostname Hostname, servicePort int, labels LabelsCollection) ([]*ServiceInstance, error) // 查詢邊車代理所在節點上的服務例項 GetProxyServiceInstances(*Proxy) ([]*ServiceInstance, error) // 獲取邊車代理所在的Region,Zone和SubZone GetProxyLocality(*Proxy) string // 管理埠,Istio生成的配置會將管理埠的流量排除,不進行路由處理 ManagementPorts(addr string) PortList // 列出用於監控檢查的探針 WorkloadHealthCheckInfo(addr string) ProbeList }
Service Registry初始化流程
Service Registry初始化的主要邏輯在Pilot-discovery程式的主函式中,對應的原始碼為:pilot/cmd/pilot-discovery/main.go和pilot/pkg/bootstrap/server.go。
在pilot/pkg/bootstrap/server.go中,初始化了各種Service Registry,其流程如下圖所示:
(備註: MCP Registry尚在開發過程中)
Pilot將各個Service Registry(Memory, Kube, Consul)儲存在serviceregistry.aggreagete.Controller中進行統一管理,Pilot會從所有型別的Registry中查詢服務和服務例項,並監控所有Registry的資料變化,當Registry資料變化後,Pilot會清空其內部的快取並通過ADS介面向Envoy推送更新。
備註:上圖中的controller實際上是Service Registry,aggregate controller和具體的各個型別的controller同時實現了Registry要求的controller和discovery interface。
Registry的業務邏輯在Kube Controller和Consul controller中,我們主要使用了Consul Controller, 其主要方法如下:
原始碼檔案: pilot/pkg/serviceregistry/consul/controller.go
▼+Controller : struct [fields] -client : *api.Client -monitor : Monitor [methods] +AppendInstanceHandler(f func(*model.ServiceInstance, model.Event)) : error +AppendServiceHandler(f func(*model.Service, model.Event)) : error +GetIstioServiceAccounts(hostname model.Hostname, ports []int) : []string +GetProxyServiceInstances(node *model.Proxy) : []*model.ServiceInstance, error +GetService(hostname model.Hostname) : *model.Service, error +InstancesByPort(hostname model.Hostname, port int, labels model.LabelsCollection) : []*model.ServiceInstance, error +ManagementPorts(addr string) : model.PortList +Run(stop chan ) +Services() : []*model.Service, error +WorkloadHealthCheckInfo(addr string) : model.ProbeList -getCatalogService(name string, q *api.QueryOptions) : []*api.CatalogService, error -getServices() : map[string][]string, error [functions] +NewController(addr string, interval time.Duration) : *Controller, error
可以看到Consul Controller物件同時實現了Registry要求的Controller和ServiceDiscovery介面,可以提供Registry的變化通知和服務查詢相關功能。
目前Consul Controller的實現比較簡單粗暴,定時通過Consul的Rest API獲取服務資料並和上一次的查詢結果進行對比,如果資料發生了變化則通知Pilot discovery進行更新。該方式發起了大量對Consul Server的HTTP請求,會導致Consul Server CPU佔用率高和大量TCP Socket處於TIME_WAIT狀態,不能直接在產品環境下使用。
原始碼檔案: pilot/pkg/serviceregistry/consul/monitor.go
//定時輪詢Consul Server Rest介面,以獲取服務資料變化 func (m *consulMonitor) run(stop <-chan struct{}) { ticker := time.NewTicker(m.period) for { select { case <-stop: ticker.Stop() return case <-ticker.C: m.updateServiceRecord() m.updateInstanceRecord() } } } //比較這一次和上一次的服務資料,如有變化則回撥ServiceHandler進行通知 func (m *consulMonitor) updateServiceRecord() { svcs, _, err := m.discovery.Catalog().Services(nil) if err != nil { log.Warnf("Could not fetch services: %v", err) return } newRecord := consulServices(svcs) if !reflect.DeepEqual(newRecord, m.serviceCachedRecord) { // This is only a work-around solution currently // Since Handler functions generally act as a refresher // regardless of the input, thus passing in meaningless // input should make functionalities work //TODO obj := []*api.CatalogService{} var event model.Event for _, f := range m.serviceHandlers { go func(handler ServiceHandler) { if err := handler(obj, event); err != nil { log.Warnf("Error executing service handler function: %v", err) } }(f) } m.serviceCachedRecord = newRecord } }
我們在Consul Registry中增加了快取,並降低了Pilot輪詢Consul server的頻率,以減少Pilot頻繁呼叫給Consul server帶來的大量壓力,下一步打算採用Consul watch來代替輪詢,優化Consul Registry的服務變化通知機制。