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

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


Istio技术与实践系列文章,持续更新中。。

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

Istio技术与实践02:源码解析之Istio on Kubernetes 统一服务发现



服务模型

首先,Istio作为一个(微)服务治理的平台,和其他的微服务模型一样也提供了ServiceServiceInstance这样抽象服务模型。如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号外,还有 一个nameprotocol。可以看到支持如下几个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, String> getMetadata();
 }

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

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

 

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


多平台支持的Adpater机制

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

image.png

pilot的代码目录也可以清楚看到,至少支持consulk8seurekacloudfoundry等平台。

 image.png


服务发现的主要行为定义

服务发现的几重要方法方法和前面看到的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有三个独立的服务分别是agentdiscoverysidecar-injector。分别提供sidecar的管理,服务发现和策略管理,sidecar自动注入的功能。Discovery的入口都是pilotpilot-discovery 

service初始化时候,初始化ServiceController DiscoveryService

if err := s.initServiceControllers(&args); err != nil {
    return nil, err
 }
 if err := s.initDiscoveryService(&args); err != nil {
    return nil, err
 }

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

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

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

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里包装了Eurekaclient作为句柄,不难猜到服务发现的逻辑正式这个clientEureka的名字服务的server获取到。

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

可以看到就是使用EurekaClient去连EurekaServer去获取服务发现数据,然后转换成Istio通用的ServiceServiceInstance的数据结构。分别要转换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/appsEureka集群来获取服务实例的列表。

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的服务发现Serversidecar通过这个server获取服务发现的数据,server使用到的各个服务发现的功能通过Environment中的ServiceDiscovery句柄来完成.从前面environment的构造可以看到这个ServiceDiscovery正是上一个init构造的controller

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

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

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

image.png

1.       Controller使用EurekaClient来获取服务列表,提供转换后的标准的服务发现接口和数据结构;

2.       Discoveryserver基于Controller上维护的服务发现数据,发布成gRPC协议的服务供Envoy使用。

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


总结

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

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

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

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

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

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

image.png

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


注:文中代码基于commit505af9a54033c52137becca1149744b15aebd4ba


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。