Istio 技術與實踐01: 原始碼解析之 Pilot 多雲平臺服務發現機制

華為雲發表於2019-03-01

本文結合 Pilot 中的關鍵程式碼來說明下 Istio 的服務發現,並以 Eureka 為例看下 Adapter 的實現機制。可以瞭解到:

  • Istio 的服務模型
  • Istio 發現的機制和原理
  • Istio 服務發現的 adpater 機制

基於以上了解可以根據需開發整合自有的服務登錄檔。

服務模型

首先,Istio 作為一個(微)服務治理的平臺,和其他的微服務模型一樣也提供了 Service,ServiceInstance 這樣抽象服務模型。如 Service 的定義中所表達的,一個服務有一個全域名,可以有一個或多個偵聽埠。

type Service struct {

   // Hostname of the service, e.g. "catalog.mystore.com"
   Hostname Hostname `json:"hostname"`
   Address string `json:"address,omitempty"`
   Addresses map[string]string `json:"addresses,omitempty"`
   // Ports is the set of network ports where the service is listening for connections
   Ports PortList `json:"ports,omitempty"`
   ExternalName Hostname `json:"external"`
   ...
}
複製程式碼

當然這裡的 Service 不只是 mesh 裡定義的 service,還可以是通過 serviceEntry 接入的外部服務。

每個port的定義在這裡:

type Port struct {

   Name string `json:"name,omitempty"`
   Port int `json:"port"`
   Protocol Protocol `json:"protocol,omitempty"`

}
複製程式碼

除了port號外,還有 一個 name 和 protocol。可以看到支援這麼幾個 Protocol

const (

   ProtocolGRPC Protocol = "GRPC"
   ProtocolHTTPS Protocol = "HTTPS"
   ProtocolHTTP2 Protocol = "HTTP2"
   ProtocolHTTP Protocol = "HTTP"
   ProtocolTCP Protocol = "TCP"
   ProtocolUDP Protocol = "UDP"
   ProtocolMongo Protocol = "Mongo"
   ProtocolRedis Protocol = "Redis"
   ProtocolUnsupported Protocol = "UnsupportedProtocol"
)
複製程式碼

而每個服務例項 ServiceInstance 的定義如下:

type ServiceInstance struct {



  Endpoint         NetworkEndpoint `json:"endpoint,omitempty"`
  Service          *Service        `json:"service,omitempty"`
  Labels           Labels          `json:"labels,omitempty"`
  AvailabilityZone string          `json:"az,omitempty"`
  ServiceAccount   string          `json:"serviceaccount,omitempty"`

}
複製程式碼

熟悉 SpringCloud 的朋友對比下 SpringCloud 中對應 interface,可以看到主要欄位基本完全一樣。

public interface ServiceInstance {
    String getServiceId();
    String getHost();
    int getPort();
    boolean isSecure();
    URI getUri();
    Map<string< span="" style="word-wrap: break-word;box-sizing: border-box;outline: none;-webkit-appearance: none;word-break: break-word;-webkit-tap-highlight-color: transparent;">, String> getMetadata();
 }</string<><string< span="" style="word-wrap: break-word;box-sizing: border-box;outline: none;-webkit-appearance: none;word-break: break-word;-webkit-tap-highlight-color: transparent;"></string<>
複製程式碼

以上的服務定義的程式碼分析,結合官方 spec 可以非常清楚的定義了服務發現的資料模型。但是,Istio 本身沒有提供服務發現註冊和服務發現的能力,翻遍程式碼目錄也找不到一個儲存服務登錄檔的服務。Discovery 部分的文件是這樣來描述的:

對於服務註冊,Istio 認為已經存在一個服務登錄檔來維護應用程式的服務例項(Pod、VM),包括服務例項會自動註冊這個服務登錄檔上;不健康的例項從目錄中刪除。而服務發現的功能是 Pilot 提供了通用的服務發現介面,供資料面呼叫動態更新例項。

即:Istio 本身不提供服務發現能力,而是提供了一種 adapter 的機制來適配各種不同的平臺。

多平臺支援的Adpater機制

具體講,Istio 的服務發現在 Pilot 中完成,通過以下框圖可以看到,Pilot提供了一種平臺 Adapter,可以對接多種不同的平臺獲取服務註冊資訊,並轉換成Istio通用的抽象模型。

Istio 技術與實踐01: 原始碼解析之 Pilot 多雲平臺服務發現機制

從pilot的程式碼目錄也可以清楚看到,至少支援consul、k8s、eureka、cloudfoundry等平臺。

Istio 技術與實踐01: 原始碼解析之 Pilot 多雲平臺服務發現機制

服務發現的主要行為定義

服務發現的幾重要方法方法和前面看到的 Service 的抽象模型一起定義在 service 中。可以認為是 Istio 服務發現的幾個主要行為。

// ServiceDiscovery enumerates Istio service instances.
type ServiceDiscovery interface {
    // 服務列表
    Services() ([]*Service, error)
    // 根據域名的得到服務
    GetService(hostname Hostname) (*Service, error)
    // 被InstancesByPort代替
    Instances(hostname Hostname, ports []string, labels LabelsCollection) ([]*ServiceInstance, error)
    //根據埠和標籤檢索服務例項,最重要的以方法。
    InstancesByPort(hostname Hostname, servicePort int, labels LabelsCollection) ([]*ServiceInstance, error)
    //根據proxy查詢服務例項,如果是sidecar和pod裝在一起,則返回該服務例項,如果只是裝了sidecar,類似gateway,則返回空
    GetProxyServiceInstances(*Proxy) ([]*ServiceInstance, error)
    ManagementPorts(addr string) PortList
 }
複製程式碼

下面選擇其中最簡單也可能是大家最熟悉的 Eureka 的實現來看下這個 adapter 機制的工作過程.

主要流程分析

1.服務發現服務入口

Pilot 有三個獨立的服務分別是 agent,discovery和sidecar-injector。分別提供 sidecar 的管理,服務發現和策略管理,sidecar自動注入的功能。Discovery的入口都是 pilot 的 pilot-discovery。

在 service 初始化時候,初始化 ServiceController 和 DiscoveryService。

if err := s.initServiceControllers(&args); err != nil {
   returnnil, err
 }
 if err := s.initDiscoveryService(&args); err != nil {
   returnnil, err
 }
複製程式碼

前者是構造一個 controller 來構造服務發現資料,後者是提供一個 DiscoveryService,釋出服務發現資料,後面的分析可以看到這個 DiscoveryService 向 Envoy 提供的服務發現資料正是來自 Controller構造的資料。我們分開來看。

2.Controller 對接不同平臺維護服務發現資料

首先看 Controller。在 initServiceControllers 根據不同的registry型別構造不同 的conteroller 實現。如對於 Eureka 的註冊型別,構造了一個 Eurkea 的 controller。

case serviceregistry.EurekaRegistry:
   eurekaClient := eureka.NewClient(args.Service.Eureka.ServerURL)
   serviceControllers.AddRegistry(
      aggregate.Registry{
         Name:             serviceregistry.ServiceRegistry(r),
         ClusterID:        string(serviceregistry.EurekaRegistry),
         Controller:       eureka.NewController(eurekaClient, args.Service.Eureka.Interval),
         ServiceDiscovery: eureka.NewServiceDiscovery(eurekaClient),
         ServiceAccounts:  eureka.NewServiceAccounts(),
      })
複製程式碼

可以看到 controller 裡包裝了 Eureka 的 client 作為控制程式碼,不難猜到服務發現的邏輯正式這個 client 連 Eureka 的名字服務的 server 獲取到。

func NewController(client Client, interval time.Duration) model.Controller {
   return &controller{
       interval:         interval,
       serviceHandlers:  make([]serviceHandler, 0),
       instanceHandlers: make([]instanceHandler, 0),
       client:           client,
    }
 }
複製程式碼

ServiceDiscovery 中定義的幾個重要方法,我們拿最重要的 InstancesByPort 來看下在 Eureka 下是怎麼支援,其他的幾個都類似。可以看到就是使用 Eureka client 去連 Eureka server 去獲取服務發現資料,然後轉換成istio通用的 Service 和 ServiceInstance 的資料結構。分別要轉換 convertServices convertServiceInstances convertPorts convertProtocol 等。


// InstancesByPort implements a service catalog operation
func (sd *serviceDiscovery) InstancesByPort(hostname model.Hostname, port int,
  tagsList model.LabelsCollection) ([]*model.ServiceInstance, error) {

  apps, err := sd.client.Applications()
  services := convertServices(apps, map[model.Hostname]bool{hostname: true})

  out := make([]*model.ServiceInstance, 0)
  for _, instance := range convertServiceInstances(services, apps) {
     out = append(out, instance)
  }
  return out, nil
}
複製程式碼

Eureka client 或服務發現資料看一眼,其實就是通過 Rest 方式訪問/eureka/v2/apps 連 Eureka叢集來獲取服務例項的列表。

func (c *client) Applications() ([]*application, error) {
  req, err := http.NewRequest("GET", c.url+appsPath, nil)
  req.Header.Set("Accept", "application/json")
  resp, err := c.client.Do(req)
  data, err := ioutil.ReadAll(resp.Body)
  var apps getApplications
  if err = json.Unmarshal(data, &apps); err != nil {
     return nil, err
  }

  return apps.Applications.Applications, nil
}
複製程式碼

Application 是本地對 Instinstance物件的包裝。

type application struct {
   Name      string      `json:"name"`
   Instances []*instance `json:"instance"`
}
複製程式碼

又看到了 eureka 熟悉的 ServiceInstance 的定義。當年有個同志提到一個方案是往 metadata 這個 map 裡塞租戶資訊,在 eureka 上做多租。

type instance struct { // nolint: maligned
   Hostname   string `json:"hostName"`
   IPAddress  string `json:"ipAddr"`
   Status     string `json:"status"`
   Port       port   `json:"port"`
   SecurePort port   `json:"securePort"`
   Metadata metadata `json:"metadata,omitempty"`
}
複製程式碼

以上我們就看完了服務發現資料生成的過程。對接名字服務的服務發現介面,獲取資料,轉換成Istio抽象模型中定義的標準格式。下面看下這些服務發現資料怎麼提供出去被Envoy使用的。

3.DiscoveryService 釋出服務發現資料

在 pilot server 初始化的時候,除了前面初始化了一個 controller 外,還有一個重要的 initDiscoveryService 初始化 Discoveryservice

environment := model.Environment{
   Mesh:             s.mesh,
   IstioConfigStore: model.MakeIstioStore(s.configController),
   ServiceDiscovery: s.ServiceController,
   ..
}
…
s.EnvoyXdsServer = envoyv2.NewDiscoveryServer(environment, v1alpha3.NewConfigGenerator(registry.NewPlugins()))
s.EnvoyXdsServer.Register(s.GRPCServer)
..
複製程式碼

即構造 gRPC server 提供了對外的服務發現介面。DiscoveryServer 定義如下

//Pilot支援Evnoy V2的xds的API
type DiscoveryServer struct {
   // env is the model environment.
   env model.Environment
   ConfigGenerator *v1alpha3.ConfigGeneratorImpl
   modelMutex      sync.RWMutex
   services        []*model.Service
   virtualServices []*networking.VirtualService
   virtualServiceConfigs []model.Config
}
複製程式碼

即提供了這個 grpc 的服務發現 Server,sidecar 通過這個 server 獲取服務發現的資料,而 server 使用到的各個服務發現的功能通過 Environment中的 ServiceDiscovery 控制程式碼來完成。從前面 environment 的構造可以看到這個 ServiceDiscovery 正是上一個 init 構造的 controller。

// Environment provides an aggregate environmental API for Pilot
type Environment struct {
   // Discovery inte**ce for listing services and instances.
   ServiceDiscovery
複製程式碼

DiscoveryServer 在如下檔案中開發了對應的介面,即所謂的 XDS API,可以看到這些API都定義在 envoyproxy/go-control-plane/envoy/service/discovery/v2 下面,即對應資料面服務發現的標準API。Pilot 和很 Envoy 這套 API 的通訊方式,包括介面定義我們在後面詳細展開。

Istio 技術與實踐01: 原始碼解析之 Pilot 多雲平臺服務發現機制

這樣幾個功能元件的互動會是這個樣子。

Istio 技術與實踐01: 原始碼解析之 Pilot 多雲平臺服務發現機制

Controller 使用 EurekaClient 來獲取服務列表,提供轉換後的標準的服務發現介面和資料結構。

Discoveryserver 基於 Controller 上維護的服務發現資料,釋出成 gRPC 協議的服務供 Envoy使用。

非常不幸的是,碼完這篇文字碼完的時候,收到社群裡 merge 了這個 PR :因為 Eureka v2.0 has been discontinued,Istio 服務發現裡 removed eureka adapter 。即1.0版本後再也看不到 Istio 對 Eureka 的支援了。這裡描述的例子真的就成為一個例子了。

總結

我們以官方文件上這張經典的圖來端到端的串下整個服務發現的邏輯:

  • Pilot 中定義了 Istio 通用的服務發現模型,即開始分析到的幾個資料結構;

  • Pilot 使用 adapter 方式對接不同的(雲平臺的)的服務目錄,提取服務註冊資訊;

  • Pilot 使用將2中服務註冊資訊轉換成1中定義的自定義的資料結構。

  • Pilot 提供標準的服務發現介面供資料面呼叫。

資料面獲取服務服務發現資料,並基於這些資料更新sidecar後端的LB例項列表,進而根據相應的負載均衡策略將請求轉發到對應的目標例項上。

Istio 技術與實踐01: 原始碼解析之 Pilot 多雲平臺服務發現機制

文中著重描述以上的通用模板流程和一般機制,很多細節忽略掉了。後續根據需要對於以上點上的重要功能會展開。如以上2和3步驟在 Kubernetes 中如何支援將在後面一篇文章《Istio 技術與實踐02:Istio 原始碼分析之 Istio+Kubernetes 的服務發現》中重點描述,將瞭解到在 Kubernetes 環境下,Istio 如何使用 Pilot 服務發現的 Adapter 方式整合 Kubernetes 的 Service 資源,從而解決長久以來在 Kubernetes 上執行微服務使用兩套名字服務的尷尬局面。

注:文中程式碼基於commit:
505af9a54033c52137becca1149744b15aebd4ba

相關文章