Mesher 整合 Istio 實踐
背景
Pilot是Isito的控制面元件,提供服務發現和配置管理功能。因為Istio使用Envoy作為資料面,因此Pilot實現了Envoy所定義的xDS API,作為xDS Server向Envoy提供服務資訊和配置資訊。本文講解Pilot xDS API的一些細節,並介紹Mesher對接Pilot的一些實踐。 Mesher脫胎於go-chassis,一個go語言的微服務SDK,提供了路由、負載均衡、容錯熔斷、限流等微服務治理核心能力,Mesher直接在程式碼層面引用go-chassis的核心能力,並在此基礎上構建了作為網路代理的功能。Mesher的架構中,一些關鍵功能都做了介面定義和外掛化的實現,包括控制面的服務發現、配置管理。當前Mesher的控制面可以接入Apache ServiceComb的服務發現元件Service Center,而配置管理則支援多種配置形式,包括檔案、環境變數和命令列等。此外,由於外掛化的設計,Mesher也實現了對開源配置管理中心Apollo的支援。目前的最佳實踐如下圖所示: (控制面使用Service Center作為服務註冊與發現元件,Apollo作為配置管理元件) 因此,在Mesher的構架下,對接Istio Pilot服務發現,只需開發Pilot外掛並實現Mesher的服務發現介面即可。
為什麼要和Istio整合
目前Istio的資料面只有Envoy一種選擇,即Service Mesh技術,與Mesher等同。它解放了開發者,讓開發者無需學習開發框架,只需開發自己的業務程式碼,在部署執行期即可變為雲原生服務,這一切都很棒。但是go chassis作為一個分散式開發框架,為追求效能的開發者提供了另一個選擇,讓開發者能夠在使用統一控制面的情況下,提升go語言專案的效能,而其餘語言則使用service mesh技術接入。
xDS API
xDS API 是 Envoy 定義的一系列發現服務,即 x Discovery Service 。對於服務發現而言,服務往往代表一個提供某項功能的 API ,由一系列具體的例項組成。在 xDS API 中,服務被定義為 Cluster ,每個 Cluster 對應的例項定義為 Endpoint 。
xDS API 分為 v1 和 v2 兩個版本, v1 為基於 http 協議的 RESTful API ,而 v2 則使用 gRPC 協議。目前 v1 已經為 deprecated 狀態, Istio1.0 版本也不再提供 v1 API ,因此本文主要討論 v2 API 。
xDS API 協議定義中,除了各種資源的 DS 介面,還定義了 ADS , Aggregated Discovery Service ,即聚合發現服務。通過 ADS ,可以獲取服務的多種資訊,而 Pilot 也是通過 ADS 介面為資料面提供資訊的。
在 ADS 介面中,通過 TypeUrl 來指定需要獲取的資源型別,每種資源型別對應的 TypeUrl 如下:
資源型別 | TypeUrl |
Cluster | type.googleapis.com/envoy.api.v2.Cluster |
Endpoint | type.googleapis.com/envoy.api.v2.ClusterLoadAssignment |
Router | type.googleapis.com/envoy.api.v2.RouteConfiguration |
Listener | type.googleapis.com/envoy.api.v2.Listener |
向 ADS Server 傳送請求時,除了指定資源型別,還要包括 Node 資訊, VersionInfo 和 Nonce 。下面我們一一進行分析。
Node
其中NodeInfo為sidecar所在節點的資訊,可以根據具體的環境獲取。NodeInfo包含Id和Cluster兩個欄位,Pilot中約定Id的格式為:
{type}~{ipAddress}~{id}~{domain}
NodeId包含四部分,以~劃分。其中,type的型別為Sidecar, Ingress或Router,因為Mesher是作為資料面代理執行,因此type為Sidecar。當type為Sidecar時,ipAddr必須為一個有效的IP地址,一般我們在sidecar中獲取當前Pod的IP地址作為ipAddr。id為Pod名稱和namespace名稱,以橫線相連。最後一部分domain則為完整的namespace,例如istio-system.svc.cluster.local。
VersionInfo和Nonce
另外,Cluster和Listener都是全域性的,在獲取Cluster和Listener的時候,不需要指定資源名稱。而Ednpoint和Router都對應具體的Cluster,因此獲取Endpoint和Router時,需要指定相關Cluster的名稱。
實戰Pilot xDS API
現在,申請服務資訊的請求資料都已經分析完畢。接下來,我們就呼叫Pilot xDS API來獲取Cluster資訊,為了方便閱讀,所有錯誤處理都略過並顯式的進行佔位宣告:
import apiv2core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
// 構建ADS資源client
conn, _ := grpc.Dial(client.PilotAddr, grpc.WithInsecure())
adsClient := v2.NewAggregatedDiscoveryServiceClient(conn)
adsResClient, _ := adsClient.StreamAggregatedResources(context.Background())
// 構建xDS資源請求物件
req := &apiv2.DiscoveryRequest{
TypeUrl:"type.googleapis.com/envoy.api.v2.Cluster",
VersionInfo:time.Now().String(),
ResponseNonce: time.Now().String(),
Node: &apiv2core.Node{
Id:"sidecar~192.168.1.20~myservice~default.svc.cluster.local",
Cluster: "myservice",
}
}
// 傳送請求並接收ADS資源
_:= adsResClient.Send(req)
resp, _ := adsResClient.Recv()
resources := resp.GetResources()
獲取到ADS資源後,resources變數的型別為protobuf Any型別的陣列,需要使用protobuf將Any型別的變數解析為具體的ADS資源型別:
// 將ADS資源解析為Clustervar cluster apiv2.Cluster
clusters := []apiv2.Cluster{}
for _, res := range resources {
_ := proto.Unmarshal(res.GetValue(), &cluster)
clusters = append(clusters, cluster)
}
至此,我們已經從Pilot中獲取到Cluster資訊。關於Cluster的詳細定義和說明,可以參考github.com/envoyproxy/data-plane-api。在Pilot中,Cluster.Name由4部分組成:
direction|port|subset|host
其中direction為inbound或outbound,表示網路流量的方向。port為該Cluster監聽的埠,在Kubernetes環境下,就是Service所暴露的埠,subset為Kubernetes中Subset的名稱,一般在DestinationRule中定義,每個Subset對應一組標籤,用於路由、負載均衡等。而host為Kubernetes Service的完整名稱,如booking.default.svc.cluster.local。
因此,從Cluster.Name中,我們可以獲取非常重要的資訊:
-
服務的名稱
-
服務對應的標籤Subset
接下來,我們利用這些資訊獲取服務例項。
使用服務名稱獲取Endpoint
對於服務發現,獲取服務名稱後,需要根據名稱獲取對應的服務示例,在 xDS 中,就是獲取 Endpoints 。我們繼續使用 ADS API ,獲取 Endpoint 資訊:
// 構建ADS資源Client和之前是一致的// 構建req時,略有不同:
req.TypeUrl ="type.googleapis.com/envoy.api.v2.ClusterLoadAssignment" // TypeUrl為Endpoint相關的Url
req.ResourceNames = []string{clusterName} // Endpoints屬於某個Cluster,要指定Cluster的名稱
獲取 Endpiont 時,我們指定 request.ResourcesNames ,將 cluster Name 傳入,獲取該 Cluster 所有的 Endpoint 。返回的 Response 實際型別為 ClusterLoadAssignment 。該結構巢狀層次比較多,獲取實際的 IP 和埠的程式碼如下:
var loadAssignment apiv2.ClusterLoadAssignmentfor _, res := range resources {
if err := proto.Unmarshal(res.GetValue(), &loadAssignment); err != nil {
break
}
}
endpionts := loadAssignment.Endpoints
for _, endpoint := range endpionts {
for _, lbendpoint := range endpoint.LbEndpoints {
socketAddress := lbendpoint.Endpoint.Address.GetSocketAddress()
// 獲取服務例項對應的地址和埠
addr := socketAddress.Address
port := socketAddress.GetPortValue()
}
}
使用服務名稱和Subset獲取Endpoin t
當用戶在Kubernetes中部署DestinationRule後,同一個服務會獲取到多個Cluster,其中subset為空字串的Cluster,對應所有服務例項。而帶有subset的Cluster,對應subset中labels指定的一組特定服務例項。例如,為booking服務設定如下DestinationRule:
apiVersion: networking.istio.io/v1alpha3kind: DestinationRule
metadata:
name: booking-destinationrule
spec:
host: booking
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v3
labels:
version: v3
那麼,我們獲取Cluster時,會得到4個Cluster,其Host都以booking開頭:
inbound|8090||booking.default.svc.cluster.localinbound|8090|v1|booking.default.svc.cluster.local
inbound|8090|v2|booking.default.svc.cluster.local
inbound|8090|v3|booking.default.svc.cluster.local
其中第一個Cluster對應3個例項,這裡每個例項都是“概念”上的,只要Kubernetes中的Pod滿足label標籤條件,都會出現在例項列表中,因此一個例項可能最終對應多個執行的Pod。subset為v1的Cluster,對應label為version=v1的例項,subset v2 v3亦是如此。這樣,當Mesher進行服務發現時,可以根據Consumer提供的tags與subset對應的label進行對比,返回tags指定的特定例項。
在xDS API中,僅能通過Cluster.Name獲取subset的名稱,並不能獲取subset對應的標籤,因此我們需要呼叫kuber-apiserver相關的API,通過subset名稱獲取對應的標籤。
import "k8s.io/client-go/rest"
config, _ = rest.InClusterConfig()
config.APIPath = "apis"
config.GroupVersion = &schema.GroupVersion{
Group:"networking.istio.io",
Version: "v1alpha3",
}
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: serializer.NewCodecFactory(runtime.NewScheme())}
k8sRestClient, _ := rest.RESTClientFor(config)
k8sClient.Get()
req.Resource("destinationrules")
req.Namespace(namespace)
result := req.Do()
rawBody, _ := result.Raw()
獲取rawBody之後,只需按照DestinationRule的格式進行解析即可,不再贅述。
實現Pilot服務發現的整合
至此,我們已經從Pilot中獲取到服務發現所需的全部資訊。包括Cluster,Endpoint以及相關標籤的處理。接下來,只需要實現Mesher服務發現介面並提供外掛即可。在Mesher服務發現的介面中,將註冊與發現進行了分離,Registrator介面用於服務的註冊,ServiceDiscovery用於服務的發現。由於Pilot已經從其他元件中獲取了服務資訊,因此我們僅需要實現ServiceDiscovery介面即可。
Mesher的服務註冊&發現介面設計
type ServiceDiscovery interface {GetMicroServiceID(appID, microServiceName, version, env string) (string, error)
GetAllMicroServices() ([]*MicroService, error)
GetMicroService(microServiceID string) (*MicroService, error)
GetMicroServiceInstances(consumerID, microServiceName string) ([]*MicroServiceInstance, error)
FindMicroServiceInstances(consumerID, microServiceName string, tags utiltags.Tags) ([]*MicroServiceInstance, error)
AutoSync()
Close() error
}
具體的實現細節我們不再贅述,ServiceDiscovery中的MicroService對應xDS API中的Cluster,MicroServiceInstance對應Endpoint。其中的3個關鍵函式,我們做一個簡要的說明:
關鍵函式 | 實現 |
GetMicroService | 呼叫ADS API獲取Clusters,根據MicroServiceID查詢匹配的Cluster,轉換成MicroService並返回 |
GetMicroServiceInstances | 引數microServiceName作為Cluster名稱,查詢對應的Endpiont,根據地址和埠組成MicroServiceInstance列表並返回 |
FindMicroServiceInstances | 同GetMicroServiceInstances,但是需要根據tags引數與subset中的labels進行匹配,並返回複合匹配條件的MicroServiceInstance列表 |
具體的實現邏輯,可以參考 Mesher Pilot plugin的程式碼: https://github.com/go-mesh/mesher/blob/master/plugins/registry/istiov2/registry.go
至此,資料面代理Mesher整合Pilot的服務發現就已經實現了。基於Mesher的外掛化設計,整合Istio Pilot時只需引入外掛的包路徑即可:
import _ "github.com/go-mesh/mesher/plugins/registry/istiov2"
並且在conf/chassis.yaml中指定服務發現的型別為pilotv2:
cse:service:
registry:
registrator:
disabled: true # 關閉自注冊
serviceDiscovery:
type: pilotv2
address: grpc://istio-pilot.istio-system:15010 # 指定pilot的地址
具體可以參考 go-chassis整合Pilot示例: https://github.com/go-chassis/go-chassis-examples/tree/master/pilot-v2 和 mesher整合Pilot示例: https://github.com/go-mesh/mesher-examples/tree/master/pilotv2-example 。因為go-chassis的外掛化設計,使得SDK和mesher sidecar都可以很方便的接入Pilot服務發現。
至此,我們已經完成Pilot服務發現的整合。xDS API提供了非常豐富的內容,除了服務發現,還包括路由規則、服務治理配置等等。在後續的文章中,我們會進一步介紹xDS API以及Pilot的相關整合。敬請大家關注!
掃碼加群
更多精彩

好看你就 點點 我
戳 “閱讀原文” 給ServiceComb點個“Star”吧 ~