Mesher集成Istio实践

Mesher集成Istio实践_第1张图片

背景

Pilot是Isito的控制面组件,提供服务发现和配置管理功能。因为Istio使用Envoy作为数据面,因此Pilot实现了Envoy所定义的xDS API,作为xDS Server向Envoy提供服务信息和配置信息。本文讲解Pilot xDS API的一些细节,并介绍Mesher对接Pilot的一些实践。

Mesher脱胎于go-chassis,一个go语言的微服务SDK,提供了路由、负载均衡、容错熔断、限流等微服务治理核心能力,Mesher直接在代码层面引用go-chassis的核心能力,并在此基础上构建了作为网络代理的功能。Mesher的架构中,一些关键功能都做了接口定义和插件化的实现,包括控制面的服务发现、配置管理。当前Mesher的控制面可以接入Apache ServiceComb的服务发现组件Service Center,而配置管理则支持多种配置形式,包括文件、环境变量和命令行等。此外,由于插件化的设计,Mesher也实现了对开源配置管理中心Apollo的支持。目前的最佳实践如下图所示:

Mesher集成Istio实践_第2张图片

控制面使用Service Center作为服务注册与发现组件,Apollo作为配置管理组件

因此,在Mesher的构架下,对接Istio Pilot服务发现,只需开发Pilot插件并实现Mesher的服务发现接口即可。

为什么要和Istio集成

目前Istio的数据面只有Envoy一种选择,即Service Mesh技术,与Mesher等同。它解放了开发者,让开发者无需学习开发框架,只需开发自己的业务代码,在部署运行期即可变为云原生服务,这一切都很棒。但是go chassis作为一个分布式开发框架,为追求性能的开发者提供了另一个选择,让开发者能够在使用统一控制面的情况下,提升go语言项目的性能,而其余语言则使用service mesh技术接入。

xDS API

xDS APIEnvoy定义的一系列发现服务,即x Discovery Service。对于服务发现而言,服务往往代表一个提供某项功能的API,由一系列具体的实例组成。在xDS API中,服务被定义为Cluster,每个Cluster对应的实例定义为Endpoint

xDS API分为v1v2两个版本,v1为基于http协议的RESTful API,而v2则使用gRPC协议。目前v1已经为deprecated状态,Istio1.0版本也不再提供v1 API,因此本文主要讨论v2 API

xDS API 协议定义中,除了各种资源的DS接口,还定义了ADSAggregated Discovery Service,即聚合发现服务。通过ADS,可以获取服务的多种信息,而Pilot也是通过ADS接口为数据面提供信息的。

ADS接口中,通过TypeUrl来指定需要获取的资源类型,每种资源类型对应的TypeUrl如下:

资源类型 TypeUrl
Cluster type.googleapis.com/envoy.api.v2.Cluster
Endpoint type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
Router type.googleapis.com/envoy.api.v2.RouteConfiguration
Listener type.googleapis.com/envoy.api.v2.Listener

ADS Server发送请求时,除了指定资源类型,还要包括Node信息,VersionInfoNonce。下面我们一一进行分析。

Node

其中NodeInfo为sidecar所在节点的信息,可以根据具体的环境获取。NodeInfo包含Id和Cluster两个字段,Pilot中约定Id的格式为:

{type}~{ipAddress}~{id}~{domain}

NodeId包含四部分,以~划分。其中,type的类型为Sidecar, Ingress或Router,因为Mesher是作为数据面代理运行,因此type为Sidecar。当type为Sidecar时,ipAddr必须为一个有效的IP地址,一般我们在sidecar中获取当前Pod的IP地址作为ipAddr。id为Pod名称和namespace名称,以横线相连。最后一部分domain则为完整的namespace,例如istio-system.svc.cluster.local。

VersionInfo和Nonce

VersionInfo和Nonce分别代表xDS客户端最近一次申请的资源的版本信息和Nonce随机值。客户端首次申请ADS资源时,VersionInfo和Nonce为空,Pilot返回资源时,会返回资源对应的VersionInfo和Nonce,客户端需要保存VersionInfo和Nonce,当再次请求资源时,带上这两个值,以便xDS API Server对服务的状态进行管理。在Pilot的实现中,VersionInfo和Nonce都是以当前时间戳作为VersionInfo和Nonce的。

另外,Cluster和Listener都是全局的,在获取Cluster和Listener的时候,不需要指定资源名称。而Ednpoint和Router都对应具体的Cluster,因此获取Endpoint和Router时,需要指定相关Cluster的名称。


实战Pilot xDS API

现在,申请服务信息的请求数据都已经分析完毕。接下来,我们就调用Pilot xDS API来获取Cluster信息,为了方便阅读,所有错误处理都略过并显式的进行占位声明:

import apiv2core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
// 构建ADS资源clientconn, _ := grpc.Dial(client.PilotAddr, grpc.WithInsecure())adsClient := v2.NewAggregatedDiscoveryServiceClient(conn)adsResClient, _ := adsClient.StreamAggregatedResources(context.Background())// 构建xDS资源请求对象req := &apiv2.DiscoveryRequest{ TypeUrl: "type.googleapis.com/envoy.api.v2.Cluster", VersionInfo: time.Now().String(), ResponseNonce: time.Now().String(), Node: &apiv2core.Node{ Id: "sidecar~192.168.1.20~myservice~default.svc.cluster.local", Cluster: "myservice", }}// 发送请求并接收ADS资源_:= adsResClient.Send(req)resp, _ := adsResClient.Recv()resources := resp.GetResources()

获取到ADS资源后,resources变量的类型为protobuf Any类型的数组,需要使用protobuf将Any类型的变量解析为具体的ADS资源类型:

// 将ADS资源解析为Clustervar cluster apiv2.Clusterclusters := []apiv2.Cluster{}for _, res := range resources {    _ := proto.Unmarshal(res.GetValue(), &cluster)      clusters = append(clusters, cluster)}

至此,我们已经从Pilot中获取到Cluster信息。关于Cluster的详细定义和说明,可以参考github.com/envoyproxy/data-plane-api。在Pilot中,Cluster.Name由4部分组成:

direction|port|subset|host

其中direction为inbound或outbound,表示网络流量的方向。port为该Cluster监听的端口,在Kubernetes环境下,就是Service所暴露的端口,subset为Kubernetes中Subset的名称,一般在DestinationRule中定义,每个Subset对应一组标签,用于路由、负载均衡等。而host为Kubernetes Service的完整名称,如booking.default.svc.cluster.local。

因此,从Cluster.Name中,我们可以获取非常重要的信息:

  • 服务的名称

  • 服务对应的标签Subset

接下来,我们利用这些信息获取服务实例。

使用服务名称获取Endpoint

对于服务发现,获取服务名称后,需要根据名称获取对应的服务示例,在xDS中,就是获取Endpoints。我们继续使用ADS API,获取Endpoint信息:

// 构建ADS资源Client和之前是一致的// 构建req时,略有不同:req.TypeUrl =  "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment" // TypeUrl为Endpoint相关的Urlreq.ResourceNames = []string{clusterName} // Endpoints属于某个Cluster,要指定Cluster的名称

获取Endpiont时,我们指定request.ResourcesNames,将cluster Name传入,获取该Cluster所有的Endpoint。返回的Response实际类型为ClusterLoadAssignment。该结构嵌套层次比较多,获取实际的IP和端口的代码如下:

var loadAssignment apiv2.ClusterLoadAssignmentfor _, res := range resources {      if err := proto.Unmarshal(res.GetValue(), &loadAssignment); err != nil {            break      }}
endpionts := loadAssignment.Endpointsfor _, endpoint := range endpionts { for _, lbendpoint := range endpoint.LbEndpoints { socketAddress := lbendpoint.Endpoint.Address.GetSocketAddress() // 获取服务实例对应的地址和端口 addr := socketAddress.Address port := socketAddress.GetPortValue() }}


使用服务名称和Subset获取Endpoint

当用户在Kubernetes中部署DestinationRule后,同一个服务会获取到多个Cluster,其中subset为空字符串的Cluster,对应所有服务实例。而带有subset的Cluster,对应subset中labels指定的一组特定服务实例。例如,为booking服务设置如下DestinationRule:

apiVersion: networking.istio.io/v1alpha3kind: DestinationRulemetadata:  name: booking-destinationrulespec:  host: booking  subsets:  - name: v1    labels:      version: v1  - name: v2    labels:      version: v2  - name: v3    labels:      version: v3

那么,我们获取Cluster时,会得到4个Cluster,其Host都以booking开头:

inbound|8090||booking.default.svc.cluster.localinbound|8090|v1|booking.default.svc.cluster.localinbound|8090|v2|booking.default.svc.cluster.localinbound|8090|v3|booking.default.svc.cluster.local

其中第一个Cluster对应3个实例,这里每个实例都是“概念”上的,只要Kubernetes中的Pod满足label标签条件,都会出现在实例列表中,因此一个实例可能最终对应多个运行的Pod。subset为v1的Cluster,对应label为version=v1的实例,subset v2 v3亦是如此。这样,当Mesher进行服务发现时,可以根据Consumer提供的tags与subset对应的label进行对比,返回tags指定的特定实例。

在xDS API中,仅能通过Cluster.Name获取subset的名称,并不能获取subset对应的标签,因此我们需要调用kuber-apiserver相关的API,通过subset名称获取对应的标签。

import "k8s.io/client-go/rest"
config, _ = rest.InClusterConfig()config.APIPath = "apis"config.GroupVersion = &schema.GroupVersion{ Group: "networking.istio.io", Version: "v1alpha3",}config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: serializer.NewCodecFactory(runtime.NewScheme())}k8sRestClient, _ := rest.RESTClientFor(config)
k8sClient.Get()req.Resource("destinationrules")req.Namespace(namespace)
result := req.Do()rawBody, _ := result.Raw()

获取rawBody之后,只需按照DestinationRule的格式进行解析即可,不再赘述。

实现Pilot服务发现的集成

至此,我们已经从Pilot中获取到服务发现所需的全部信息。包括Cluster,Endpoint以及相关标签的处理。接下来,只需要实现Mesher服务发现接口并提供插件即可。在Mesher服务发现的接口中,将注册与发现进行了分离,Registrator接口用于服务的注册,ServiceDiscovery用于服务的发现。由于Pilot已经从其他组件中获取了服务信息,因此我们仅需要实现ServiceDiscovery接口即可。

Mesher集成Istio实践_第3张图片

Mesher的服务注册&发现接口设计

type ServiceDiscovery interface {      GetMicroServiceID(appID, microServiceName, version, env string) (string, error)      GetAllMicroServices() ([]*MicroService, error)      GetMicroService(microServiceID string) (*MicroService, error)      GetMicroServiceInstances(consumerID, microServiceName string) ([]*MicroServiceInstance, error)      FindMicroServiceInstances(consumerID, microServiceName string, tags utiltags.Tags) ([]*MicroServiceInstance, error)      AutoSync()      Close() error}

具体的实现细节我们不再赘述,ServiceDiscovery中的MicroService对应xDS API中的Cluster,MicroServiceInstance对应Endpoint。其中的3个关键函数,我们做一个简要的说明:

关键函数 实现
GetMicroService 调用ADS API获取Clusters,根据MicroServiceID查找匹配的Cluster,转换成MicroService并返回
GetMicroServiceInstances 参数microServiceName作为Cluster名称,查找对应的Endpiont,根据地址和端口组成MicroServiceInstance列表并返回
FindMicroServiceInstances 同GetMicroServiceInstances,但是需要根据tags参数与subset中的labels进行匹配,并返回复合匹配条件的MicroServiceInstance列表

具体的实现逻辑,可以参考Mesher Pilot plugin的代码:https://github.com/go-mesh/mesher/blob/master/plugins/registry/istiov2/registry.go

至此,数据面代理Mesher集成Pilot的服务发现就已经实现了。基于Mesher的插件化设计,集成Istio Pilot时只需引入插件的包路径即可:

import _ "github.com/go-mesh/mesher/plugins/registry/istiov2"

并且在conf/chassis.yaml中指定服务发现的类型为pilotv2:

cse:  service:    registry:      registrator:        disabled: true # 关闭自注册      serviceDiscovery:        type: pilotv2        address: grpc://istio-pilot.istio-system:15010 # 指定pilot的地址

具体可以参考go-chassis集成Pilot示例:https://github.com/go-chassis/go-chassis-examples/tree/master/pilot-v2mesher集成Pilot示例:https://github.com/go-mesh/mesher-examples/tree/master/pilotv2-example。因为go-chassis的插件化设计,使得SDK和mesher sidecar都可以很方便的接入Pilot服务发现。

至此,我们已经完成Pilot服务发现的集成。xDS API提供了非常丰富的内容,除了服务发现,还包括路由规则、服务治理配置等等。在后续的文章中,我们会进一步介绍xDS API以及Pilot的相关集成。敬请大家关注!

Mesher集成Istio实践_第4张图片

往期精彩回顾

ServiceComb Alpha 集群动态主节点实现

基于CSE的微服务架构实践-Spring Boot技术栈选型

单体应用微服务改造实践

扫码加群

Mesher集成Istio实践_第5张图片

更多精彩

好看你就点点

“阅读原文”给ServiceComb点个“Star”吧~

你可能感兴趣的:(Mesher集成Istio实践)