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的支持。目前的最佳实践如下图所示: (控制面使用Service Center作为服务注册与发现组件,Apollo作为配置管理组件) 因此,在Mesher的构架下,对接Istio Pilot服务发现,只需开发Pilot插件并实现Mesher的服务发现接口即可。
为什么要和Istio集成
目前Istio的数据面只有Envoy一种选择,即Service Mesh技术,与Mesher等同。它解放了开发者,让开发者无需学习开发框架,只需开发自己的业务代码,在部署运行期即可变为云原生服务,这一切都很棒。但是go chassis作为一个分布式开发框架,为追求性能的开发者提供了另一个选择,让开发者能够在使用统一控制面的情况下,提升go语言项目的性能,而其余语言则使用service mesh技术接入。
xDS API
xDS API 是 Envoy 定义的一系列发现服务,即 x Discovery Service 。对于服务发现而言,服务往往代表一个提供某项功能的 API ,由一系列具体的实例组成。在 xDS API 中,服务被定义为 Cluster ,每个 Cluster 对应的实例定义为 Endpoint 。
xDS API 分为 v1 和 v2 两个版本, v1 为基于 http 协议的 RESTful API ,而 v2 则使用 gRPC 协议。目前 v1 已经为 deprecated 状态, Istio1.0 版本也不再提供 v1 API ,因此本文主要讨论 v2 API 。
xDS API 协议定义中,除了各种资源的 DS 接口,还定义了 ADS , Aggregated 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 信息, VersionInfo 和 Nonce 。下面我们一一进行分析。
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
另外,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资源client
conn, _ := 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.Cluster
clusters := []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相关的Url
req.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.Endpoints
for _, endpoint := range endpionts {
for _, lbendpoint := range endpoint.LbEndpoints {
socketAddress := lbendpoint.Endpoint.Address.GetSocketAddress()
// 获取服务实例对应的地址和端口
addr := socketAddress.Address
port := socketAddress.GetPortValue()
}
}
使用服务名称和Subset获取Endpoin t
当用户在Kubernetes中部署DestinationRule后,同一个服务会获取到多个Cluster,其中subset为空字符串的Cluster,对应所有服务实例。而带有subset的Cluster,对应subset中labels指定的一组特定服务实例。例如,为booking服务设置如下DestinationRule:
apiVersion: networking.istio.io/v1alpha3kind: DestinationRule
metadata:
name: booking-destinationrule
spec:
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.local
inbound|8090|v2|booking.default.svc.cluster.local
inbound|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的服务注册&发现接口设计
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-v2 和 mesher集成Pilot示例: https://github.com/go-mesh/mesher-examples/tree/master/pilotv2-example 。因为go-chassis的插件化设计,使得SDK和mesher sidecar都可以很方便的接入Pilot服务发现。
至此,我们已经完成Pilot服务发现的集成。xDS API提供了非常丰富的内容,除了服务发现,还包括路由规则、服务治理配置等等。在后续的文章中,我们会进一步介绍xDS API以及Pilot的相关集成。敬请大家关注!
▼
往期精彩回顾
▼
ServiceComb Alpha 集群动态主节点实现
基于CSE的微服务架构实践-Spring Boot技术栈选型
单体应用微服务改造实践
扫码加群
更多精彩
好看你就 点点 我
戳 “阅读原文” 给ServiceComb点个“Star”吧 ~