Istio 技术与实践01: 源码解析之 Pilot 多云平台服务发现机制

1,474 阅读8分钟

本文结合 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通用的抽象模型。

从pilot的代码目录也可以清楚看到,至少支持consul、k8s、eureka、cloudfoundry等平台。

服务发现的主要行为定义

服务发现的几重要方法方法和前面看到的 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 的通信方式,包括接口定义我们在后面详细展开。

这样几个功能组件的交互会是这个样子。

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实例列表,进而根据相应的负载均衡策略将请求转发到对应的目标实例上。

文中着重描述以上的通用模板流程和一般机制,很多细节忽略掉了。后续根据需要对于以上点上的重要功能会展开。如以上2和3步骤在 Kubernetes 中如何支持将在后面一篇文章《Istio 技术与实践02:Istio 源码分析之 Istio+Kubernetes 的服务发现》中重点描述,将了解到在 Kubernetes 环境下,Istio 如何使用 Pilot 服务发现的 Adapter 方式集成 Kubernetes 的 Service 资源,从而解决长久以来在 Kubernetes 上运行微服务使用两套名字服务的尴尬局面。

注:文中代码基于commit: 505af9a54033c52137becca1149744b15aebd4ba