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

前言

本文结合Pilot中的关键代码来说明下Istio的服务发现的机制、原理和流程。并以Eureka为例看下Adapter的机制如何支持多云环境下的服务发现。可以了解到: 1. Istio的服务模型; 2. Istio发现的机制和原理; 3. Istio服务发现的adpater机制。 基于以上了解可以根据需开发集成自有的服务注册表,完成服务发现的功能。

服务模型

首先,Istio作为一个(微)服务治理的平台,和其他的微服务模型一样也提供了Service,ServiceInstance这样抽象服务模型。如Service的定义中所表达的,一个服务有一个全域名,可以有一个或多个侦听端口。

 1type Service struct {
 2    // Hostname of the service, e.g. "catalog.mystore.com"
 3    Hostname Hostname `json:"hostname"`
 4    Address string `json:"address,omitempty"`
 5    Addresses map[string]string `json:"addresses,omitempty"`
 6    // Ports is the set of network ports where the service is listening for connections
 7    Ports PortList `json:"ports,omitempty"`
 8    ExternalName Hostname `json:"external"`
 9    ...
10 }

当然这里的Service不只是mesh里定义的service,还可以是通过serviceEntry接入的外部服务。每个port的定义在这里:

1type Port struct {
2    Name string `json:"name,omitempty"`
3    Port int `json:"port"`
4    Protocol Protocol `json:"protocol,omitempty"`
5 }

除了port外,还有 一个name和protocol。可以看到支持如下几个Protocol :

 1const (
 2   ProtocolGRPC Protocol = "GRPC"
 3    ProtocolHTTPS Protocol = "HTTPS"
 4    ProtocolHTTP2 Protocol = "HTTP2"
 5    ProtocolHTTP Protocol = "HTTP"
 6    ProtocolTCP Protocol = "TCP"
 7    ProtocolUDP Protocol = "UDP"
 8    ProtocolMongo Protocol = "Mongo"
 9    ProtocolRedis Protocol = "Redis"
10    ProtocolUnsupported Protocol = "UnsupportedProtocol"
11 )

每个服务实例ServiceInstance的定义如下

1type ServiceInstance struct {
2    Endpoint         NetworkEndpoint `json:"endpoint,omitempty"`
3    Service          *Service        `json:"service,omitempty"`
4    Labels           Labels          `json:"labels,omitempty"`
5    AvailabilityZone string          `json:"az,omitempty"`
6    ServiceAccount   string          `json:"serviceaccount,omitempty"`
7 }

熟悉SpringCloud的朋友对比下SpringCloud中对应interface,可以看到主要字段基本完全一样。

1public interface ServiceInstance {
2    String getServiceId();
3    String getHost();
4    int getPort();
5    boolean isSecure();
6    URI getUri();
7    Map<String, String> getMetadata();
8 }

以上的服务定义的代码分析,结合官方spec可以非常清楚的定义了服务发现的数据模型。但是,Istio本身没有提供服务发现注册和服务发现的能力,翻遍代码目录也找不到一个存储服务注册表的服务。Discovery部分的文档是这样来描述的:

对于服务注册,Istio认为已经存在一个服务注册表来维护应用程序的服务实例(Pod、VM),包括服务实例会自动注册这个服务注册表上;不健康的实例从目录中删除。而服务发现的功能是Pilot提供了通用的服务发现接口,供数据面调用动态更新实例。

即Istio本身不提供服务发现能力,而是提供了一种adapter的机制来适配各种不同的平台。

多平台支持的Adpater机制

具体讲,Istio的服务发现在Pilot中完成,通过以下框图可以看到,Pilot提供了一种平台Adapter,可以对接多种不同的平台获取服务注册信息,并转换成Istio通用的抽象模型。



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

服务发现的主要行为定义

服务发现的几重要方法方法和前面看到的Service的抽象模型一起定义在service中。,可以认为是Istio服务发现的几个主要行为。

 1// ServiceDiscovery enumerates Istio service instances.
 2 type ServiceDiscovery interface {
 3    // 服务列表
 4    Services() ([]*Service, error)
 5    // 根据域名的得到服务
 6    GetService(hostname Hostname) (*Service, error)
 7    // 被InstancesByPort代替
 8    Instances(hostname Hostname, ports []string, labels LabelsCollection) ([]*ServiceInstance, error)
 9    //根据端口和标签检索服务实例,最重要的以方法。
10    InstancesByPort(hostname Hostname, servicePort int, labels LabelsCollection) ([]*ServiceInstance, error)
11    //根据proxy查询服务实例,如果是sidecar和pod装在一起,则返回该服务实例,如果只是装了sidecar,类似gateway,则返回空
12    GetProxyServiceInstances(*Proxy) ([]*ServiceInstance, error)
13    ManagementPorts(addr string) PortList
14 }

下面选择其中最简单也可能是大家最熟悉的Eureka的实现来看下这个adapter机制的工作过程

主要流程分析

1.    服务发现服务入口

Pilot有三个独立的服务分别是agent,discovery和sidecar-injector。分别提供sidecar的管理,服务发现和策略管理,sidecar自动注入的功能。Discovery的入口都是pilot的pilot-discovery。 在service初始化时候,初始化ServiceController 和 DiscoveryService。

1if err := s.initServiceControllers(&args); err != nil {
2    return nil, err
3 }
4 if err := s.initDiscoveryService(&args); err != nil {
5    return nil, err
6 }

前者是构造一个controller来构造服务发现数据,后者是提供一个DiscoveryService,发布服务发现数据,后面的分析可以看到这个DiscoveryService向Envoy提供的服务发现数据正是来自Controller构造的数据。我们分开来看。

2.    Controller对接不同平台维护服务发现数据

首先看Controller。在initServiceControllers根据不同的registry类型构造不同的conteroller实现。如对于Eureka的注册类型,构造了一个Eurkea的controller。

 1case serviceregistry.EurekaRegistry:
 2    eurekaClient := eureka.NewClient(args.Service.Eureka.ServerURL)
 3    serviceControllers.AddRegistry(
 4       aggregate.Registry{
 5          Name:             serviceregistry.ServiceRegistry(r),
 6          ClusterID:        string(serviceregistry.EurekaRegistry),
 7          Controller:       eureka.NewController(eurekaClient, args.Service.Eureka.Interval),
 8          ServiceDiscovery: eureka.NewServiceDiscovery(eurekaClient),
 9          ServiceAccounts:  eureka.NewServiceAccounts(),
10       })

可以看到controller里包装了Eureka的client作为句柄,不难猜到服务发现的逻辑正式这个client连Eureka的名字服务的server获取到。

1func NewController(client Client, interval time.Duration) model.Controller {
2    return &controller{
3       interval:         interval,
4       serviceHandlers:  make([]serviceHandler, 0),
5       instanceHandlers: make([]instanceHandler, 0),
6       client:           client,
7    }
8 }

可以看到就是使用EurekaClient去连EurekaServer去获取服务发现数据,然后转换成Istio通用的Service和ServiceInstance的数据结构。分别要转换convertServices convertServiceInstances, convertPorts, convertProtocol等。

 1// InstancesByPort implements a service catalog operation
 2 func (sd *serviceDiscovery) InstancesByPort(hostname model.Hostname, port int,
 3    tagsList model.LabelsCollection) ([]*model.ServiceInstance, error) {
 4    apps, err := sd.client.Applications()
 5services := convertServices(apps, map[model.Hostname]bool{hostname: true})
 6
 7out := make([]*model.ServiceInstance, 0)
 8for _, instance := range convertServiceInstances(services, apps) {
 9   out = append(out, instance)
10}
11return out, nil
12}

Eureka client或服务发现数据看一眼,其实就是通过Rest方式访问/eureka/v2/apps连Eureka集群来获取服务实例的列表。

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

Application是本地对Instinstance对象的包装。

1type application struct {
2    Name      string      `json:"name"`
3    Instances []*instance `json:"instance"`
4 }

又看到了eureka熟悉的ServiceInstance的定义。当年有个同志提到一个方案是往metadata这个map里塞租户信息,在eureka上做多租。

1type instance struct { // nolint: maligned
2    Hostname   string `json:"hostName"`
3    IPAddress  string `json:"ipAddr"`
4    Status     string `json:"status"`
5    Port       port   `json:"port"`
6    SecurePort port   `json:"securePort"`
7    Metadata metadata `json:"metadata,omitempty"`
8 }

以上我们就看完了服务发现数据生成的过程。对接名字服务的服务发现接口,获取数据,转换成Istio抽象模型中定义的标准格式。下面看下这些服务发现数据怎么提供出去被Envoy使用的。

3.    DiscoveryService 发布服务发现数据

在pilot server初始化的时候,除了前面初始化了一个controller外,还有一个重要的initDiscoveryService初始化Discoveryservice

 1environment := model.Environment{
 2    Mesh:             s.mesh,
 3    IstioConfigStore: model.MakeIstioStore(s.configController),
 4    ServiceDiscovery: s.ServiceController,
 5    ..
 6 }
 7 
 8 s.EnvoyXdsServer = envoyv2.NewDiscoveryServer(environment, v1alpha3.NewConfigGenerator(registry.NewPlugins()))
 9 s.EnvoyXdsServer.Register(s.GRPCServer)
10 ..

即构造gRPC server提供了对外的服务发现接口。DiscoveryServer定义如下

 1//Pilot支持Evnoy V2的xds的API
 2 type DiscoveryServer struct {
 3    // env is the model environment.
 4    env model.Environment
 5    ConfigGenerator *v1alpha3.ConfigGeneratorImpl
 6    modelMutex      sync.RWMutex
 7    services        []*model.Service
 8    virtualServices []*networking.VirtualService
 9    virtualServiceConfigs []model.Config
10 }

即提供了这个grpc的服务发现Server,sidecar通过这个server获取服务发现的数据,而server使用到的各个服务发现的功能通过Environment中的ServiceDiscovery句柄来完成.从前面environment的构造可以看到这个ServiceDiscovery正是上一个init构造的controller。

1// Environment provides an aggregate environmental API for Pilot
2 type Environment struct {
3    // Discovery interface for listing services and instances.
4    ServiceDiscovery

DiscoveryServer在如下文件中开发了对应的接口,即所谓的XDS API,可以看到这些API都定义在envoyproxy/go-control-plane/envoy/service/discovery/v2 下面,即对应数据面服务发现的标准API。Pilot和很Envoy这套API的通信方式,包括接口定义我们在后面详细展开。

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

  1. Controller使用EurekaClient来获取服务列表,提供转换后的标准的服务发现接口和数据结构;
  2. Discoveryserver基于Controller上维护的服务发现数据,发布成gRPC协议的服务供Envoy使用。

非常不幸的是,码完这篇文字码完的时候,收到社区里merge了这个PR :因为Eureka v2.0 has been discontinued,Istio服务发现里removed eureka adapter 。即1.0版本后再也看不到Istio对Eureka的支持了。这里描述的例子真的就成为一个例子了。

总结

我们以官方文档上这张经典的图来端到端的串下整个服务发现的逻辑:

  1. Pilot中定义了Istio通用的服务发现模型,即开始分析到的几个数据结构;

  2. Pilot使用adapter方式对接不同的(云平台的)的服务目录,提取服务注册信息;

  3. Pilot使用将2中服务注册信息转换成1中定义的自定义的数据结构。

  4. Pilot提供标准的服务发现接口供数据面调用。

  5. 数据面获取服务服务发现数据,并基于这些数据更新sidecar后端的LB实例列表,进而根据相应的负载均衡策略将请求转发到对应的目标实例上。

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

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