Istio技術與實踐02:源碼解析之Istio on Kubernetes 統一服務發現
*
*
【摘要】 本文基於Pilot服務發現Kubernetes部分源碼重點介紹在Istio on Kubernetes環境下,如何基於Pilot的Adapter機制實現Istio管理的服務直接使用Kubernetes service來做統一服務發現,避免了其他微服務框架運行在Kubernetes環境時上下兩套服務目錄的局面。並以此為入口從架構、場景等方面總結下Istio和Kubernetes的結合關係。
前言
前面文章《Istio源碼解析 Pilot服務發現的Adapter機制》結合Pilot的代碼實現介紹了Istio的抽象服務模型和基於該模型的數據結構定義,了解到Istio上只是定義的服務發現的介面,並未實現服務發現的功能,而是通過Adapter機制以一種可擴展的方式來集成各種不同的服務發現。本文重點講解Adapter機制在Kubernetes平台上的使用。即Istio+Kubernetes如何實現服務發現。
Istio的官方設計上特彆強調其架構上的可擴展性,即通過框架定義與實現解耦的方式來集成各種不同的實現。如Pilot上的adapter機制集成不同的服務註冊表,Mixer通過提供一個統一的面板給數據面Sidecar,後端可以通過模板定義的方式對接不同的Backend來進行各種訪問管理。但就現階段實現,從代碼或者文檔的細節去細看其功能,還是和Kubernetes結合最緊密。
Kubernetes和Istio的結合
從場景和架構上看Istio和Kubernetes都是非常契合的一種搭配。
首先從場景上看Kuberntes為應用負載的部署、運維、擴縮容等提供了強大的支持。通過Service機制提供了負載間訪問機制,通過域名結合Kubeproxy提供的轉發機制可以方便的訪問到對端的服務實例。因此如上圖可以認為Kubernetes提供了一定的服務發現和負載均衡能力,但是較深入細緻的流量治理能力,因為Kubnernetes所處的基礎位置並未提供,而Istio正是補齊了這部分能力,兩者的結合提供了一個端到端的容器服務運行和治理的解決方案。
從架構看Istio和Kubernetes更是深度的結合。得益於Kuberntes Pod的設計,數據面的Sidecar作為一種高性能輕量的代理自動注入到Pod中和業務容器部署在一起,接管業務容器的inbound和outbound的流量,從而實現對業務容器中服務訪問的治理。在控制面上Istio基於其Adapter機制集成Kubernetes的域名,從而避免了兩套名字服務的尷尬場景。
在本文中將結合Pilot的代碼實現來重點描述圖中上半部分的實現,下半部分的內容Pilot提供的通用的API給Envoy使用可參照上一篇文章的DiscoverServer部分的描述。
基於Kubernetes的服務發現
理解了Pilot的ServiceDiscovery的Adapter的主流程後,了解這部分內容比較容易。Pilot-discovery在initServiceControllers時,根據服務註冊配置的方式,如果是Kubernetes,則會走到這個分支來構造K8sServiceController。
caseserviceregistry.KubernetesRegistry:
s.createK8sServiceControllers(serviceControllers,args);err != nil {
returnerr
}
創建controller其實就是創建了一個Kubenernetes的controller,可以看到List/Watch了Service、Endpoints、Node、Pod幾個資源對象。
// NewControllercreates a new Kubernetes controller
funcNewController(clientkubernetes.Interface,optionsControllerOptions) *Controller{
out := &Controller{
domainSuffix: options.DomainSuffix,
client: client,
queue: NewQueue(1* time.Second),
}
out.services =out.createInformer(&v1.Service{},"Service",options.ResyncPeriod,
func(opts meta_v1.ListOptions) (runtime.Object,error) {
returnclient.CoreV1().Services(options.WatchedNamespace).List(opts)
},
func(opts meta_v1.ListOptions) (watch.Interface,error) {
returnclient.CoreV1().Services(options.WatchedNamespace).Watch(opts)
})
out.endpoints =out.createInformer(&v1.Endpoints{},"Endpoints",options.ResyncPeriod,
func(opts meta_v1.ListOptions) (runtime.Object,error) {
returnclient.CoreV1().Endpoints(options.WatchedNamespace).List(opts)
},
func(opts meta_v1.ListOptions) (watch.Interface,error) {
returnclient.CoreV1().Endpoints(options.WatchedNamespace).Watch(opts)
})
out.nodes =out.createInformer(&v1.Node{},"Node",options.ResyncPeriod,
func(opts meta_v1.ListOptions) (runtime.Object,error) {
returnclient.CoreV1().Nodes().List(opts)
},
func(opts meta_v1.ListOptions) (watch.Interface,error) {
returnclient.CoreV1().Nodes().Watch(opts)
})
out.pods =newPodCache(out.createInformer(&v1.Pod{},"Pod",options.ResyncPeriod,
func(opts meta_v1.ListOptions) (runtime.Object,error) {
returnclient.CoreV1().Pods(options.WatchedNamespace).List(opts)
},
func(opts meta_v1.ListOptions) (watch.Interface,error) {
returnclient.CoreV1().Pods(options.WatchedNamespace).Watch(opts)
}))
returnout
}
在createInformer中其實就是創建了SharedIndexInformer。這種方式在Kubernetes的各種Controller中廣泛使用。Informer調用APIserver的 List 和 Watch 兩種類型的 API。在初始化的時,先調用 List API 獲得全部資源對象,緩存在內存中; 然後,調用 Watch API 去 Watch這種這種資源對象,維護緩存。
Serviceinformer := cache.NewSharedIndexInformer(
&cache.ListWatch, o,
resyncPeriod, cache.Indexers{})
下面看下Kubernetes場景下對ServiceDiscovery介面的實現。我們看下Kubernetes下提供的服務發現的介面,包括獲取服務列表和服務實例列表。
func(c *Controller)GetService(hostname model.Hostname) (*model.Service,error) {
name,namespace,err := parseHostname(hostname)
item,exists := c.serviceByKey(name,namespace)
svc := convertService(*item,c.domainSuffix)
returnsvc,nil
}
最終是從infromer的緩存中獲取Service資源對象。
func(c *Controller)serviceByKey(name,namespacestring) (*v1.Service,bool) {
item,exists,err := c.services.informer.GetStore().GetByKey(KeyFunc(name,namespace))
returnitem.(*v1.Service),true
}
獲取服務實例列表也是類似,也是從Informer的緩存中獲取對應資源,只是涉及的對象和處理過程比Service要複雜一些。
func(c *Controller) InstancesByPort(hostname model.Hostname,reqSvcPortint,
labelsList model.LabelsCollection) ([]*model.ServiceInstance,error) {
// Get actual service by name
name,namespace,err := parseHostname(hostname)
item,exists := c.serviceByKey(name,namespace)
svc := convertService(*item,c.domainSuffix)
svcPortEntry,exists := svc.Ports.GetByPort(reqSvcPort)
for_,item :=rangec.endpoints.informer.GetStore().List() {
ep := *item.(*v1.Endpoints)
…
}
...
}
}
returnnil,nil
}
可以看到就是做了如下的轉換,將Kubernetes的對一個服務發現的數據結構轉換成Istio的抽象模型對應的數據結構。
其實在conversion.go中提供了多個convert的方法將Kubernetes的數據對象轉換成Istio的標準格式。除了上面的對Service、Instance的convert外,還包含對port,label、protocol的convert。如下面protocol的convert就值得一看。
funcConvertProtocol(namestring,proto v1.Protocol) model.Protocol{
out := model.ProtocolTCP
switchproto {
casev1.ProtocolUDP:
out = model.ProtocolUDP
casev1.ProtocolTCP:
prefix := name
i := strings.Index(name,"-")
ifi >={
prefix = name[:i]
}
protocol := model.ParseProtocol(prefix)
ifprotocol != model.ProtocolUDP&& protocol != model.ProtocolUnsupported{
out = protocol
}
}
returnout
}
看過Istio文檔的都知道在使用Istio和Kuberntes結合的場景下創建Pod時要求滿足4個約束。其中重要的一個是Port必須要有名,且Port的名字名字的格式有嚴格要求:Service 的埠必須命名,且埠的名字必須滿足格式
[-],例如name: http2-foo 。在K8s場景下這部分我們一般可以不對Pod命名的,看這段解析的代碼可以看服務的Protocol是從name中解析出來的。如果Service的protocol是UDP的,則協議UDP;如果是TCP的,則會從名字中繼續解析協議。如果名稱是不可識別的前綴或者埠上的流量就會作為普通的 TCP 流量來處理。
另外同時在Informer中添加對add、delete和update事件的回調,分別對應informer監聽到創建、更新和刪除這三種事件類型。可以看到這裡是將待執行的回調操作包裝成一個task,再壓到Queue中,然後在Queue的run()方法中拿出去挨個執行,這部分不細看了。
到這裡Kuberntes特有的服務發現能力就介紹完了。即kubecontroller也實現了ServiceDiscovery中規定的服務發現的介面中定義的全部發方法。除了初始化了一個kube controller來從Kubeapiserver中獲取和維護服務發現數據外,在pilot server初始化的時候,還有一個重要的initDiscoveryService初始化DiscoveryServer,這個discoveryserver使用contrller,其實是ServiceDiscovery上的服務發現供。發布成通用協議的介面,V1是rest,V2是gRPC,進而提供服務發現的能力給Envoy調用,這部分是Pilot服務發現的通用機制,在上篇文章的adapter機制中有詳細描述,這裡不再贅述。
總結
以上介紹了istio基於Kubernetes的名字服務實現服務發現的機制和流程。整個調用關係如下,可以看到和其他的Adapter實現其實類似。
1.KubeController使用List/Watch獲取和維護服務列表和其他需求的資源對象,提供轉換後的標準的服務發現介面和數據結構;
2.Discoveryserver基於Controller上維護的服務發現數據,發布成gRPC協議的服務供Envoy使用。
前面只是提到了服務發現的數據維護,可以看到在Kubernetes場景下,Istio只是是使用了kubeAPIServer中service這種資源對象。在執行層面,說到Service就不得不說Kuberproxy,因為Service只是一個邏輯的描述,真正執行轉發動作的是Kubeproxy,他運行在集群的每個節點上,把對Service的訪問轉發到後端pod上。在引入Istio後,將不再使用Kubeproxy做轉發了,而是將這些映射關係轉換成為pilot的轉發模型,下發到envoy進行轉發,執行更複雜的控制。這些在後面分析Discoveryserver和Sidecar的交互時再詳細分析。
在和Kubnernetes結合的場景下
強調下幾個概念:
1.Istio的治理Service就是Kubernetes的Service。不管是服務描述的manifest還是存在於服務註冊表中服務的定義都一樣。
2.Istio治理的是服務間的通信。這裡的服務並不一定是所謂的微服務,並不在乎是否做了微服務化。只要有服務間的訪問,需要治理就可以用。一個大的單體服務打成一個鏡像在Kuberntes里部署起來被其他負載訪問和分拆成微服務後被訪問,在治理看來沒有任何差別。
本文只是描述了在服務發現上兩者的結合,隨著分析的深入,會發現Istio和Kubernetes的更多契合。K8s編排容器服務已經成為一種事實上的標準;微服務與容器在輕量、快速部署運維等特徵的匹配,微服務運行在容器中也正成為一種標準實踐;隨著istio的成熟和ServiceMesh技術的流行,使用Istio進行微服務治理的實踐也正越來越多;而istio和k8s的這種天然融合使得上面形成了一個完美的閉環。對於雲原生應用,採用kubernetes構建微服務部署和集群管理能力,採用Istio構建服務治理能力,也將成為微服務真正落地的一個最可能的途徑。有幸參與其中讓我們一起去見證和經歷這個趨勢吧。
註:文中代碼基於commit:
505af9a54033c52137becca1149744b15aebd4ba
TAG:容器魔方 |