1. 程式人生 > >Istio技術與實踐02:原始碼解析之Istio on Kubernetes 統一服務發現

Istio技術與實踐02:原始碼解析之Istio on Kubernetes 統一服務發現

前言

文章Istio技術與實踐01: 原始碼解析之Pilot多雲平臺服務發現機制結合Pilot的程式碼實現介紹了Istio的抽象服務模型和基於該模型的資料結構定義,瞭解到Istio上只是定義的服務發現的介面,並未實現服務發現的功能,而是通過Adapter機制以一種可擴充套件的方式來整合各種不同的服務發現。本文重點講解Adapter機制在Kubernetes平臺上的使用。即Istio on Kubernetes如何實現服務發現。

Istio的官方設計上特別強調其架構上的可擴充套件性,即通過框架定義與實現解耦的方式來整合各種不同的實現。如Pilot上的adapter機制整合不同的服務登錄檔,Mixer通過提供一個統一的面板給資料面Sidecar,後端可以通過模板定義的方式對接不同的Backend來進行各種訪問管理。但就現階段實現,從程式碼或者文件的細節去細看其功能,還是和Kubernetes結合最緊密。

KubernetesIstio的結合

從場景和架構上看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的服務發現

時,根據服務註冊配置的方式,如果是Kubernetes,則會走到這個分支來構造K8sServiceController。

case serviceregistry.KubernetesRegistry:



    s.createK8sServiceControllers(serviceControllers, args); err != nil {

    
return err  }

建立controller其實就是建立了一個Kubenernetes的controller,可以看到List/Watch了Service、Endpoints、Node、Pod幾個資源物件。

// NewController creates a new Kubernetes controller

func NewController(client kubernetes.Interface, options ControllerOptions) *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) {

          return client.CoreV1().Services(options.WatchedNamespace).List(opts)

       },

       func(opts meta_v1.ListOptions) (watch.Interface, error) {

          return client.CoreV1().Services(options.WatchedNamespace).Watch(opts)

       })

    out.endpoints = out.createInformer(&v1.Endpoints{}, "Endpoints", options.ResyncPeriod,

       func(opts meta_v1.ListOptions) (runtime.Object, error) {

          return client.CoreV1().Endpoints(options.WatchedNamespace).List(opts)

       },

       func(opts meta_v1.ListOptions) (watch.Interface, error) {

          return client.CoreV1().Endpoints(options.WatchedNamespace).Watch(opts)

       })

    out.nodes = out.createInformer(&v1.Node{}, "Node", options.ResyncPeriod,

       func(opts meta_v1.ListOptions) (runtime.Object, error) {

          return client.CoreV1().Nodes().List(opts)

       },

       func(opts meta_v1.ListOptions) (watch.Interface, error) {

          return client.CoreV1().Nodes().Watch(opts)

       })

    out.pods = newPodCache(out.createInformer(&v1.Pod{}, "Pod", options.ResyncPeriod,

       func(opts meta_v1.ListOptions) (runtime.Object, error) {

          return client.CoreV1().Pods(options.WatchedNamespace).List(opts)

       },

       func(opts meta_v1.ListOptions) (watch.Interface, error) {

          return client.CoreV1().Pods(options.WatchedNamespace).Watch(opts)

       }))



    return out

 }

createInformer 中其實就是建立了SharedIndexInformer。這種方式在Kubernetes的各種Controller中廣泛使用。Informer呼叫 APIserver List Watch 兩種型別的 API。在初始化的時,先呼叫 List API 獲得全部資源物件,快取在記憶體中; 然後,呼叫 Watch API Watch這種這種資源物件,維護快取。

Service informer := cache.NewSharedIndexInformer(

    &cache.ListWatch{ListFunc: lf, WatchFunc: wf}, 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)

    return svc, nil

 }

最終是從infromer的快取中獲取Service資源物件。

func (c *Controller) serviceByKey(name, namespace string) (*v1.Service, bool) {

    item, exists, err := c.services.informer.GetStore().GetByKey(KeyFunc(name, namespace))

    return item.(*v1.Service), true
}

 獲取服務例項列表也是類似,也是從Informer的快取中獲取對應資源,只是涉及的物件和處理過程比Service要複雜一些。

func (c *Controller) InstancesByPort(hostname model.Hostname, reqSvcPort int,

    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 := range c.endpoints.informer.GetStore().List() {

       ep := *item.(*v1.Endpoints)

       …

    }

 ...

 }

 }

return nil, nil

 }

可以看到就是做了如下的轉換,將Kubernetes的對一個服務發現的資料結構轉換成Istio的抽象模型對應的資料結構。

其實在conversion.go中提供了多個convert的方法將Kubernetes的資料物件轉換成Istio的標準格式。除了上面的對Service、Instance的convert外,還包含對port,label、protocol的convert。如下面protocol的convert就值得一看。

func ConvertProtocol(name string, proto v1.Protocol) model.Protocol {

    out := model.ProtocolTCP
switch proto {

    case v1.ProtocolUDP:

       out = model.ProtocolUDP
case v1.ProtocolTCP:

       prefix := name

       i := strings.Index(name, "-")

       if i >= 0 {

          prefix = name[:i]

       }

       protocol := model.ParseProtocol(prefix)

       if protocol != model.ProtocolUDP && protocol != model.ProtocolUnsupported {

          out = protocol

       }

    }

    return out

 }

看過Istio文件的都知道在使用Istio和Kuberntes結合的場景下建立Pod時要求滿足4個約束。其中重要的一個是Port必須要有名,且Port的名字名字的格式有嚴格要求:Service 的埠必須命名,且埠的名字必須滿足格式 <protocol>[-<suffix>],例如name: http2-foo 。在K8s場景下這部分我們一般可以不對Pod命名的,看這段解析的程式碼可以看服務的Protocol是從name中解析出來的。如果Service的protocol是UDP的,則協議UDP;如果是TCP的,則會從名字中繼續解析協議。如果名稱是不可識別的字首或者埠上的流量就會作為普通的 TCP 流量來處理。

另外同時在Informer 中新增對add、delete、和update事件的回撥,分別對應 informer 監聽到建立、更新和刪除這三種事件型別。可以看到這裡是將待執行的回撥操作包裝成一個task,再壓到Queue中,然後在Queue的run()方法中拿出去挨個執行,這部分不細看了。

到這裡Kuberntes特有的服務發現能力就介紹完了。中規定的服務發現的介面中定義的全部發方法。除了初始化了一個kube controller來從Kubeapiserver中獲取和維護服務發現數據外,在pilot server初始化的時候,還有一個重要的initDiscoveryService初始化,這個discoveryserver使用contrller,其實是上的服務發現供。釋出成通用協議的介面,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構建服務治理能力,也將成為微服務真正落地的一個最可能的途徑。有幸參與其中讓我們一起去見證和經歷這個趨勢吧。