服务模型
首先,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();
MapgetMetadata();
}
以上的服务定义的代码分析,结合官方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 {
return nil, err
}
if err := s.initDiscoveryService(&args); err != nil {
return nil, 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,
}
}
可以看到就是使用EurekaClient去连EurekaServer去获取服务发现数据,然后转换成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 interface for listing services and instances.
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实例列表,进而根据相应的负载均衡策略将请求转发到对应的目标实例上。
注:文中代码基于commit:505af9a54033c52137becca1149744b15aebd4ba