各位,多集群这个场景在服务网格这一块也算是越来越热了。早在1.4版本,istio社区就已经提出了多集群环境下istio的部署模型,提供统一的控制面管理多集群中服务的能力。而最近随着服务网格的配套可观测项目kiali推出v1.69版本,我们更是可以在一个kiali实例中就纵览多集群中的流量规则、流量拓扑与服务详情,多集群的使用体验逐渐完善,利用这个场景玩法的用户也是越来越多了。
不过,任何新事物的引入都需要有其存在意义,否则便只能说是画蛇添足。要使用服务网格的多集群管理能力,首先要明白:我们为什么需要服务网格来统一管理多集群,多集群能给我们带来什么?
我们可以分步骤地思考这个问题:
(1)多集群业务之间是否存在关联?
我们使用Kubernetes集群+服务网格,最普遍的使用场景当然是在集群中部署我们的业务应用,并对外提供服务访问。那么当两个集群中的业务之间并无关联的情况下,是否有必要去将两个集群都加入到一个服务网格之中呢?
答案应该是“否”,也就是说,如果要将多个集群中的服务用一个服务网格的控制面统一管理,则集群中的服务互相之间应该存在业务上的关联,要么是服务之间存在调用关系,要么是同一个服务在两个集群中都有对应的工作负载。
为什么呢?因为服务网格的控制面是统一的、中心化的。如果将两个集群A和B都加入同一个服务网格实例进行管理,服务网格中部署的流量规则就会在所有集群中生效。
举个极端的例子:集群A和集群B中存在一个相同命名空间、相同名称的Service,但实际上两个同名Service提供的并不是一个服务,二者并无业务上的关联。在这种情况下,服务网格的控制面会将两个Service识别成同一个在多集群环境中部署的Service,针对此Service的调用将会被负载均衡,流量均匀地发往两个集群中的工作负载。这就很可能造成一半的业务流量出现错误。因此如果集群中的服务之间并无业务联系,还是推荐将不同集群分别用不同服务网格的控制面进行管理,以免出现上述的尴尬情况。
(2)服务的工作负载应该如何部署?
在确认了多集群中部署的服务之间应该存在业务关联后,我们需要确认部署方式,即一个业务应用相关的多个服务应该如何在多集群中部署。
一个直观的想法是将一部分服务部署在集群A,一部分服务部署在集群B,并配置服务之间的互相访问连通性,在统一控制面的管理下,多集群也可以实现单集群部署的效果,多集群的服务互相之间可以进行服务发现,并互相进行调用。
但此用法的问题在于:相比单集群部署的服务,这种方式并没有带来更多显而易见的好处,反而徒增了业务架构的复杂性,属实画蛇添足。实际上,由于跨集群调用的延迟肯定是赶不上集群内部调用,用这种方式部署反而会给业务的性能造成拖累。
另一个想法听上去要有意义的多:提供服务的冗余部署,在两个集群中各存在一套相同的微服务部署,每个微服务都在两个集群中部署了自己的工作负载。通过这种方式,两个集群可以互为主备,当一个集群中的某个服务挂掉、甚至整个集群都挂掉的时候,依赖这个服务的工作负载还可以转而去调用另一个集群中的工作负载。
社区中也有着类似的思考,在istio社区的《部署模型》一文中,就提出了多集群部署能够带来哪些新的特性:
您可以将单个网格配置为包括多集群。 在单一网格中使用多集群部署, 与单一集群部署相比其具备以下更多能力:
- 故障隔离和故障转移:当 cluster-1 下线,业务将转移至 cluster-2。
- 位置感知路由和故障转移:将请求发送到最近的服务。
- 团队或项目隔离:每个团队仅运行自己的集群。
总结一下可以得出:环境的隔离和故障转移是多集群部署带来的两个重要特性。
在默认情况下,虽然多集群部署带来了环境隔离和故障转移的好处,但集群间调用仍会影响部分请求在调用链上的性能。比如,服务1调用服务2,而服务2在集群A和集群B中都部署了工作负载。由于现在集群A和B都被统一的服务网格控制面管理,实际调用时服务1就会在服务2的多集群工作负载之间做负载均衡,也就是说,将有约一般的请求会是跨集群的调用,对应请求的延时自然也就不可避免地上升了。
集群内流量保持
如上所述,很多时候,跨集群调用其实并不是一个我们在多集群部署下的预期行为,甚至可以说是多集群带来的一种副作用。从1.14版本开始,istio社区提供了集群内流量保持的功能,只需要简单的配置就可以让发往服务的流量保持在集群之中,避免出现非预期的跨集群调用。
服务网格ASM同样支持对集群内流量保持的配置,有关具体配置方法及效果,可以参考使用集群内流量保持来防止流量在集群之间串门-阿里云开发者社区。
鉴于之前已经写过了集群内流量保持的配置方法,这回我们简单看看它是怎么实现的。
集群内流量保持实际对应着MeshConfig中一个名为serviceSettings的选项,其中保存了需要将调用流量保持在集群之内的服务的域名:
serviceSettings:
- settings:
clusterLocal: true
hosts:
- "mysvc.myns.svc.cluster.local"
在代码实现上,Istio实现一个ClusterLocalProvider interface,提供GetClusterLocalHosts方法来随时获取配置了集群内流量保持的服务域名,并通过监听MeshConfig实现域名的实时更新。
// ClusterLocalHosts is a map of host names or wildcard patterns which should only
// be made accessible from within the same cluster.
type ClusterLocalHosts struct {
specific map[host.Name]struct{
}
wildcard map[host.Name]struct{
}
}
// IsClusterLocal indicates whether the given host should be treated as a
// cluster-local destination.
func (c ClusterLocalHosts) IsClusterLocal(h host.Name) bool {
_, _, ok := MostSpecificHostMatch(h, c.specific, c.wildcard)
return ok
}
...
// ClusterLocalProvider provides the cluster-local hosts.
type ClusterLocalProvider interface {
// GetClusterLocalHosts returns the list of cluster-local hosts, sorted in
// ascending order. The caller must not modify the returned list.
GetClusterLocalHosts() ClusterLocalHosts
}
这个interface被用于控制面推送服务的cluster和endpoint配置的过程中。以服务endpoint的推送简单举例,Istio会为集群中的每个服务新建一个EndpointBuilder,负责构建该服务的端点信息配置,在新建时会调用推送上下文中的IsClusterLocal方法,这个方法实际就是利用ClusterLocalProvider来判断当前的服务是否被配置了集群内流量保持,返回布尔值并保存在EndpointBuilder中。
func NewEndpointBuilder(clusterName string, proxy *model.Proxy, push *model.PushContext) EndpointBuilder {
...
b := EndpointBuilder{
clusterName: clusterName,
network: proxy.Metadata.Network,
proxyView: proxy.GetView(),
clusterID: proxy.Metadata.ClusterID,
locality: proxy.Locality,
service: svc,
clusterLocal: push.IsClusterLocal(svc),
destinationRule: dr,
nodeType: proxy.Type,
push: push,
proxy: proxy,
subsetName: subsetName,
hostname: hostname,
port: port,
dir: dir,
}
在实际构建服务的端点配置时,Istio根据EndpointBuilder中的这个字段来判断是否要选择性地跳过该服务在其它集群中的端点的配置下发。
// build LocalityLbEndpoints for a cluster from existing EndpointShards.
func (b *EndpointBuilder) buildLocalityLbEndpointsFromShards(
shards *model.EndpointShards,
svcPort *model.Port,
) []*LocalityEndpoints {
...
// Determine whether or not the target service is considered local to the cluster
// and should, therefore, not be accessed from outside the cluster.
isClusterLocal := b.clusterLocal
shards.Lock()
// Extract shard keys so we can iterate in order. This ensures a stable EDS output.
keys := shards.Keys()
// The shards are updated independently, now need to filter and merge for this cluster
for _, shardKey := range keys {
if shardKey.Cluster != b.clusterID {
// If the downstream service is configured as cluster-local, only include endpoints that
// reside in the same cluster.
if isClusterLocal || b.service.Attributes.NodeLocal {
continue
}
}
endpoints := shards.Shards[shardKey]
...
在这篇文章中曾提到,Istio本身并不进行服务发现,而是通过对接现成的服务注册中心来获取服务的端点信息,目前最常见的就是对接Kubernetes集群的服务注册中心,获取其中的Service和Endpoint资源。在多集群部署模式下,一个服务网格实例自然是对接了多个集群的服务注册中心,Istio在实现上会将来自不同注册中心的服务信息进行分组,也就是不同的shard,shard的key就是集群的id,一个shard自然就代表了一个集群。
可以看到上面的集群内流量保持实现就是利用了shard的机制。正常情况下,两个集群中的同命名空间、同名服务会被服务网格认为是同一服务,其在多集群中的所有端点会被汇总在一起,下发给多集群中的每个网格代理。而集群内流量保持实际就是在此基础上进行了一个小小改造——只有端点的shard与网格代理的shard是同一个时,才为网格代理添加此端点信息,即:
当为集群1中的网格代理下发服务A的端点时
如果此端点位于集群1:向端点信息列表中追加此端点信息
否则:跳过处理此端点
—————————————— ☕️休息一下的分割线 ——————————————
我们简单回顾一下集群内流量保持机制。其优点显而易见:配置简单、代价小,只需要MeshConfig的一个字段即可实现服务的集群内流量保持效果,利用服务网格对不同服务注册中心的天然区分来大量简化配置。
不过这种做法也可以称之为一把双刃剑。前面提到,故障隔离和故障转移是服务网格统一管理多集群部署的一个重要优势,然而这个优势却无法与当前的集群内流量保持配置进行兼容。原因就在于集群内流量保持实际上是为网格代理抛弃了其它集群中服务的端点信息,从而使得网格代理只能访问本集群内的服务端点;当本集群内的服务发生故障时,网格代理并无能力迅速将服务调用故障转移到该服务在其它集群中的端点,因为这些端点早在下发的时候就被抛弃了!
这也就引出我们的下一个议题——
以泳道模式实现集群内流量保持,并增加故障转移能力
实际上,Istio提供的集群内流量保持配置并不是实现这一能力的唯一手段。从另一个角度来想,集群内流量保持实际上就是以不同集群这个天然的区隔为依据,将应用服务的多套环境进行了隔离,并让流量的调用链只能在相同的环境中传递。实际上,这和我们在《以服务网格实现无侵入的全链路灰度:最佳实践与方法》中讨论的微服务全链路灰度场景是高度相似的:都是在做服务环境的隔离,只不过一个是隔离了服务的不同版本、一个则是隔离了服务在不同集群中的部署。
在全链路灰度场景中,我们曾提到使用最基础的VirtualService和DestinationRule资源,就可以实现泳道模式:通过pod 的 label对两套应用服务的环境进行逻辑上的隔离。在多集群部署下,我们当然也可以做到同样的事情。
首先,我们为多集群中的每个工作负载打上标签,让同一服务在多个集群中的不同工作负载可以互相区分,我们以mocka和mockb两个服务为例(两个服务都在两个集群中各部署一个工作负载),配置这样的DestinationRule,为每个服务手动创建分区:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mocka
spec:
host: mocka
subsets:
- labels:
cluster: cluster-a
name: cluster-a
- labels:
cluster: cluster-b
name: cluster-b
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mockb
spec:
host: mockb
subsets:
- labels:
cluster: cluster-a
name: cluster-a
- labels:
cluster: cluster-b
name: cluster-b
此例中,我们为服务的工作负载都打上cluster的标签,标签值代表其所属的集群(cluster-a或cluster-b)。
然后我们配置VirtualService,以流量规则的方式限制流量只能在本集群之内流动:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- match:
- sourceLabels:
cluster: cluster-a
route:
- destination:
host: mockb
subset: cluster-a
- match:
- sourceLabels:
cluster: cluster-b
route:
- destination:
host: mockb
subset: cluster-b
可以看到,上述流量规则和我们之前讨论过使用泳道模式进行全链路灰度时配置的规则是几乎一致的,区别只是讲代表版本的version标签换成了代表所属集群的cluster标签。
—————————————— ☕️休息,休息一下 ——————————————
上面这个实践同样实现了集群内流量保持的效果,与这个方案相比,网格自带的集群内流量配置的优势就又体现出来了:只需要配置MeshConfig即可,无需给工作负载添加标签,也不用写这么多流量规则。那么这种泳道模式的方案就一无是处了吗?也不尽然。
与集群内流量保持相比,泳道模式有一个优点在于:泳道模式只是做了服务在逻辑上的隔离,实际上、每个网格代理中仍然存储着一个服务在所有集群中的端点信息,可以很方便迅速地实现故障转移。这对于集群内流量保持来说是办不到的,因为服务在其它集群的端点信息从一开始就没有被下发到网格代理中。
在全链路灰度一文中我们已经提到:阿里云服务网格ASM已经在VirtualService中支持了开箱即用的流量降级配置能力,我们只需要修改上面的VirtualService,增加一段回退配置即可轻松实现故障转移。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- match:
- sourceLabels:
cluster: cluster-a
route:
- destination:
host: mockb
subset: cluster-a
fallback:
target:
host: mockb
subset: cluster-b
- match:
- sourceLabels:
cluster: cluster-b
route:
- destination:
host: mockb
subset: cluster-b
fallback:
target:
host: mockb
subset: cluster-a
在正常调用情况下,对服务的调用仍然遵循集群内流量保持原则。而当集群内服务发生不可用情况时、后续针对该服务的调用则会迅速转移到服务在另一个集群的端点上。例如:这里配置了针对mockb服务的调用,如果调用方来自cluster-a,则调用mockb在cluster-a中的端点,而如果cluster-a中的mockb服务不可用,则会转而调用cluster-b中的mockb服务端点。
泳道模式+流量降级的方案,在实现了集群内流量保持效果的前提下,也仍然保持了服务网格统一管理多集群部署的各种优势,可说是一种功能范围更为全面的最佳实践。不过,该方案也显而易见地存在配置复杂的缺点,最典型的场景例如:当我们需要为服务配置另一个维度的子集时(比如配置服务的不同版本),这个方案将会让需要配置的服务子集数量膨胀一倍。
比如:
- 服务A在cluster-a和cluster-b中各部署一个子集
- 服务A又存在v1和v2两个版本,cluster-a和cluster-b中都部署了服务A的两个版本
- 此时我们需要为服务A定义4个子集:cluster-a-v1、cluster-a-v2、cluster-b-v1、cluster-b-v2
小结
本文中,我们简要探讨了在使用服务网格统一管理多集群部署时,能够充分利用多集群和服务网格特性的服务部署方式和流量管理最佳实践。并针对实践中的重要部分:集群内流量保持的实现进行了简要梳理。简单来说,集群内流量保持可以通过服务网格的集群内流量保持配置或是通过泳道模式的流量规则来实现。二者的对比如下
集群内流量保持配置 | 泳道模式 | |
---|---|---|
版本支持 | 有版本支持限制,从社区1.14版本开始支持 | 无版本限制,是个服务网格基本都能配 |
配置复杂度 | 低,只需要简单修改MeshConfig | 高,需要针对服务配置流量规则 |
功能全面性 | 低,只能保证流量在集群内保持,无法兼容多集群故障转移等自定义场景 | 高,通过流量规则配置、自定义程度高,可以轻松配置集群之间的故障转移或是其他流量管理场景。 |
在实际实践过程中,还需要根据两种方案的优缺点以及自身需求对方案进行选择。一般来说,如果我们需求对多集群流量进行诸如故障转移这样的精细化控制,则推荐选择以泳道模式区隔多集群流量;否则,可以选择使用集群内流量保持配置这一简单方法。
可能是东半球最好用的服务网格——阿里云服务网格ASM!👉👉👉https://help.aliyun.com/product/147365.html👈👈👈