本文作者趙化冰,將在明天下午 1 點半在成都螞蟻 C 空間為大家分享《服務網格技術在5G網絡管理平臺中的落地實踐》歡迎大家,查看活動詳情。
Istio Pilot 組件介紹
在Istio架構中,Pilot組件屬于最核心的組件,負責了服務網格中的流量管理以及控制面和數據面之間的配置下發。Pilot內部的代碼結構比較復雜,本文中我們將通過對Pilot的代碼的深入分析來了解Pilot實現原理。
首先我們來看一下Pilot在Istio中的功能定位,Pilot將服務信息和配置數據轉換為xDS接口的標準數據結構,通過gRPC下發到數據面的Envoy。如果把Pilot看成一個處理數據的黑盒,則其有兩個輸入,一個輸出:
目前Pilot的輸入包括兩部分數據來源:
- 服務數據: 來源于各個服務注冊表(Service Registry),例如Kubernetes中注冊的Service,Consul Catalog中的服務等。
- 配置規則: 各種配置規則,包括路由規則及流量管理規則等,通過Kubernetes CRD(Custom Resources Definition)形式定義并存儲在Kubernetes中。
Pilot的輸出為符合xDS接口的數據面配置數據,并通過gRPC Streaming接口將配置數據推送到數據面的Envoy中。
備注:Istio代碼庫在不停變化更新中,本文分析所基于的代碼commit為: d539abe00c2599d80c6d64296f78d3bb8ab4b033
Pilot-Discovery 代碼結構
Istio Pilot的代碼分為Pilot-Discovery和Pilot-Agent,其中Pilot-Agent用于在數據面負責Envoy的生命周期管理,Pilot-Discovery才是控制面進行流量管理的組件,本文將重點分析控制面部分,即Pilot-Discovery的代碼。
下圖是Pilot-Discovery組件代碼的主要結構:Pilot-Discovery的入口函數為:pilot/cmd/pilot-discovery/main.go中的main方法。main方法中創建了Discovery Server,Discovery Server中主要包含三部分邏輯:
Config Controller
Config Controller用于管理各種配置數據,包括用戶創建的流量管理規則和策略。Istio目前支持三種類型的Config Controller:
- Kubernetes:使用Kubernetes來作為配置數據的存儲,該方式直接依附于Kubernetes強大的CRD機制來存儲配置數據,簡單方便,是Istio最開始使用的配置存儲方案。
- MCP (Mesh Configuration Protocol):使用Kubernetes來存儲配置數據導致了Istio和Kubernetes的耦合,限制了Istio在非Kubernetes環境下的運用。為了解決該耦合,Istio社區提出了MCP,MCP定義了一個向Istio控制面下發配置數據的標準協議,Istio Pilot作為MCP Client,任何實現了MCP協議的Server都可以通過MCP協議向Pilot下發配置,從而解除了Istio和Kubernetes的耦合。如果想要了解更多關于MCP的內容,請參考文后的鏈接。
- Memory:一個在內存中的Config Controller實現,主要用于測試。
目前Istio的配置包括:
- Virtual Service: 定義流量路由規則。
- Destination Rule: 定義和一個服務或者subset相關的流量處理規則,包括負載均衡策略,連接池大小,斷路器設置,subset定義等等。
- Gateway: 定義入口網關上對外暴露的服務。
- Service Entry: 通過定義一個Service Entry可以將一個外部服務手動添加到服務網格中。
- Envoy Filter: 通過Pilot在Envoy的配置中添加一個自定義的Filter。
Service Controller
Service Controller用于管理各種Service Registry,提出服務發現數據,目前Istio支持的Service Registry包括:
- Kubernetes:對接Kubernetes Registry,可以將Kubernetes中定義的Service和Instance采集到Istio中。
- Consul: 對接Consul Catalog,將Consul中定義的Service采集到Istio中。
- MCP: 和MCP config controller類似,從MCP Server中獲取Service和Service Instance。
- Memory: 一個內存中的Service Controller實現,主要用于測試。
Discovery Service
Discovery Service中主要包含下述邏輯:
- 啟動gRPC Server并接收來自Envoy端的連接請求。
- 接收Envoy端的xDS請求,從Config Controller和Service Controller中獲取配置和服務信息,生成響應消息發送給Envoy。
- 監聽來自Config Controller的配置變化消息和來自Service Controller的服務變化消息,并將配置和服務變化內容通過xDS接口推送到Envoy。(備注:目前Pilot未實現增量變化推送,每次變化推送的是全量配置,在網格中服務較多的情況下可能會有性能問題)。
Pilot-Discovery 業務流程
Pilot-Disocvery包括以下主要的幾個業務流程:
初始化Pilot-Discovery的各個主要組件
Pilot-Discovery命令的入口為pilot/cmd/pilot-discovery/main.go中的main方法,在該方法中創建Pilot Server,Server代碼位于文件pilot/pkg/bootstrap/server.go中。Server主要做了下面一些初始化工作:
- 創建并初始化Config Controller。
- 創建并初始化Service Controller。
- 創建并初始化Discovery Server,Pilot中創建了基于Envoy V1 API的HTTP Discovery Server和基于Envoy V2 API的GPRC Discovery Server。由于V1已經被廢棄,本文將主要分析V2 API的gRPC Discovery Server。
- 將Discovery Server注冊為Config Controller和Service Controller的Event Handler,監聽配置和服務變化消息。
創建gRPC Server并接收Envoy的連接請求
Pilot Server創建了一個gRPC Server,用于監聽和接收來自Envoy的xDS請求。pilot/pkg/proxy/envoy/v2/ads.go 中的 DiscoveryServer.StreamAggregatedResources方法被注冊為gRPC Server的服務處理方法。
當gRPC Server收到來自Envoy的連接時,會調用DiscoveryServer.StreamAggregatedResources方法,在該方法中創建一個XdsConnection對象,并開啟一個goroutine從該connection中接收客戶端的xDS請求并進行處理;如果控制面的配置發生變化,Pilot也會通過該connection把配置變化主動推送到Envoy端。
配置變化后向Envoy推送更新
這是Pilot中最復雜的一個業務流程,主要是因為代碼中采用了多個channel和queue對變化消息進行合并和轉發。該業務流程如下:
- Config Controller或者Service Controller在配置或服務發生變化時通過回調方法通知Discovery Server,Discovery Server將變化消息放入到Push Channel中。
- Discovery Server通過一個goroutine從Push Channel中接收變化消息,將一段時間內連續發生的變化消息進行合并。如果超過指定時間沒有新的變化消息,則將合并后的消息加入到一個隊列Push Queue中。
- 另一個goroutine從Push Queue中取出變化消息,生成XdsEvent,發送到每個客戶端連接的Push Channel中。
- 在DiscoveryServer.StreamAggregatedResources方法中從Push Channel中取出XdsEvent,然后根據上下文生成符合xDS接口規范的DiscoveryResponse,通過gRPC推送給Envoy端。(gRPC會為每個client連接單獨分配一個goroutine來進行處理,因此不同客戶端連接的StreamAggregatedResources處理方法是在不同goroutine中處理的)
響應Envoy主動發起的xDS請求
Pilot和Envoy之間建立的是一個雙向的Streaming gRPC服務調用,因此Pilot可以在配置變化時向Envoy推送,Envoy也可以主動發起xDS調用請求獲取配置。Envoy主動發起xDS請求的流程如下:
- Envoy通過創建好的gRPC連接發送一個DiscoveryRequest
- Discovery Server通過一個goroutine從XdsConnection中接收來自Envoy的DiscoveryRequest,并將請求發送到ReqChannel中
- Discovery Server的另一個goroutine從ReqChannel中接收DiscoveryRequest,根據上下文生成符合xDS接口規范的DiscoveryResponse,然后返回給Envoy。
Discovery Server業務處理關鍵代碼片段
下面是Discovery Server的關鍵代碼片段和對應的業務邏輯注解,為方便閱讀,代碼中只保留了邏輯主干,去掉了一些不重要的細節。
處理xDS請求和推送的關鍵代碼
該部分關鍵代碼位于 istio.io/istio/pilot/pkg/proxy/envoy/v2/ads.go
文件的StreamAggregatedResources 方法中。StreamAggregatedResources方法被注冊為gRPC Server的handler,對于每一個客戶端連接,gRPC Server會啟動一個goroutine來進行處理。
代碼中主要包含以下業務邏輯:
- 從gRPC連接中接收來自Envoy的xDS 請求,并放到一個channel reqChannel中。
- 從reqChannel中接收xDS請求,根據xDS請求的類型構造響應并發送給Envoy。
- 從connection的pushChannel中接收Service或者Config變化后的通知,構造xDS響應消息,將變化內容推送到Envoy端。
// StreamAggregatedResources implements the ADS interface.
func (s *DiscoveryServer) StreamAggregatedResources(stream ads.AggregatedDiscoveryService_StreamAggregatedResourcesServer) error {
......
//創建一個goroutine來接收來自Envoy的xDS請求,并將請求放到reqChannel中
con := newXdsConnection(peerAddr, stream)
reqChannel := make(chan *xdsapi.DiscoveryRequest, 1)
go receiveThread(con, reqChannel, &receiveError)
......
for {
select{
//從reqChannel接收Envoy端主動發起的xDS請求
case discReq, ok := <-reqChannel:
//根據請求的類型構造相應的xDS Response并發送到Envoy端
switch discReq.TypeUrl {
case ClusterType:
err := s.pushCds(con, s.globalPushContext(), versionInfo())
case ListenerType:
err := s.pushLds(con, s.globalPushContext(), versionInfo())
case RouteType:
err := s.pushRoute(con, s.globalPushContext(), versionInfo())
case EndpointType:
err := s.pushEds(s.globalPushContext(), con, versionInfo(), nil)
}
//從PushChannel接收Service或者Config變化后的通知
case pushEv := <-con.pushChannel:
//將變化內容推送到Envoy端
err := s.pushConnection(con, pushEv)
}
}
}
處理服務和配置變化的關鍵代碼
該部分關鍵代碼位于 istio.io/istio/pilot/pkg/proxy/envoy/v2/discovery.go
文件中,用于監聽服務和配置變化消息,并將變化消息合并后通過Channel發送給前面提到的 StreamAggregatedResources 方法進行處理。
ConfigUpdate是處理服務和配置變化的回調函數,service controller和config controller在發生變化時會調用該方法通知Discovery Server。
func (s *DiscoveryServer) ConfigUpdate(req *model.PushRequest) {
inboundConfigUpdates.Increment()
//服務或配置變化后,將一個PushRequest發送到pushChannel中
s.pushChannel <- req
}
在debounce方法中將連續發生的PushRequest進行合并,如果一段時間內沒有收到新的PushRequest,再發起推送;以避免由于服務和配置頻繁變化給系統帶來較大壓力。
// The debounce helper function is implemented to enable mocking
func debounce(ch chan *model.PushRequest, stopCh <-chan struct{}, pushFn func(req *model.PushRequest)) {
......
pushWorker := func() {
eventDelay := time.Since(startDebounce)
quietTime := time.Since(lastConfigUpdateTime)
// it has been too long or quiet enough
//一段時間內沒有收到新的PushRequest,再發起推送
if eventDelay >= DebounceMax || quietTime >= DebounceAfter {
if req != nil {
pushCounter++
adsLog.Infof("Push debounce stable[%d] %d: %v since last change, %v since last push, full=%v",
pushCounter, debouncedEvents,
quietTime, eventDelay, req.Full)
free = false
go push(req)
req = nil
debouncedEvents = 0
}
} else {
timeChan = time.After(DebounceAfter - quietTime)
}
}
for {
select {
......
case r := <-ch:
lastConfigUpdateTime = time.Now()
if debouncedEvents == 0 {
timeChan = time.After(DebounceAfter)
startDebounce = lastConfigUpdateTime
}
debouncedEvents++
//合并連續發生的多個PushRequest
req = req.Merge(r)
case <-timeChan:
if free {
pushWorker()
}
case <-stopCh:
return
}
}
}
完整的業務流程
參考閱讀
關于 ServiceMeshe 社區
ServiceMesher 社區是由一群擁有相同價值觀和理念的志愿者們共同發起,于 2018 年 4 月正式成立。
社區關注領域有:容器、微服務、Service Mesh、Serverless,擁抱開源和云原生,致力于推動 Service Mesh 在中國的蓬勃發展。