多集群 & 多租户

2021-08-22 系统架构 微服务

# 一、多集群

注意:下面讨论的前提是单个机房内的多集群。

# 1.1 多集群的必要性

对于类似账号服务的 L0 级别的服务,几乎所有的服务都有依赖,需要尽可能的提高服务的可用性。

  • 从单一集群考虑:多个节点保证可用性,我们通常使用 N+2 的方式来冗余节点(N 一般通过压测得出);
  • 从单一集群故障带来的影响面角度考虑:冗余多套集群,例如依赖的 redis 出现问题,整个集群挂掉了;
  • 从单个机房内的机房故障考虑:多机房部署,如果在云上可能是多个可用区。

# 1.2 如何实现多集群

  • 多套冗余的集群 对应多套独占的缓存,带来更好的性能和冗余能力;
  • 要尽量 避免按照业务划分 集群资源,业务隔离集群带来的问题是缓存命中率下降,不同业务形态数据正交,可以退而求其次整个集群全部连接。

# 1.3 如何降低健康检查流量

对于账号这种大量服务依赖的服务,仅仅是健康检查流量就可能会导致 30% 以上的资源占用(B 站之前的真实情况)。可以使用 子集算法 从全集群中选取一批节点(子集),利用划分子集限制连接池大小。

  • 通常 20~100 个后端,部分场景需要大子集,比如大批量读写操作;
  • 算法核心:后端节点平均分给客户端;
  • 客户端重启,保持重新均衡,同时对后端重启保持透明,同时连接的变动最小。即:消费者节点数变化的时候需要重新平衡,且变动尽可能小。

子集算法的实现如下:

// from google sre
func Subset(backends []string, clientID, subsetSize int) []string {
	subsetCount := len(backends) / subsetSize

	// Group clients into rounds; each round uses the same shuffled list:
	round := clientID / subsetCount

	r := rand.New(rand.NewSource(int64(round)))
	r.Shuffle(len(backends), func(i, j int) { backends[i], backends[j] = backends[j], backends[i] })

	// The subset id corresponding to the current client:
	subsetID := clientID % subsetCount

	start := subsetID * subsetSize
	return backends[start : start+subsetSize]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

为什么上面这个算法可以保证可以均匀分布?

首先,shuffle 算法保证在 round 一致的情况下,backend 的排列一定是一致的。

因为每个实例拥有从 0 开始的连续唯一的自增 id,且计算过程能够保证每个 round 内所有实例拿到的服务列表的排列一致,因此在同一个 round 内的 client 会分别选择 backend 排列的不同部分的切片作为选中的后端服务来建立连接。

下面通过示例运行一遍该算法:

backends:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

clientID:0~9

subsetSize:4

划分结果:

Round 0: [0, 6, 3, 5, 1, 7, 11, 9, 2, 4, 8, 10]
          |---C0---|  |---C1----|  |---C2----|

Round 1: [8, 11, 4, 0, 5, 6, 10, 3, 2, 7, 9, 1]
          |---C4---|  |---C5----|  |---C6----|

Round 2: [8, 3, 7, 2, 1, 4, 9, 10, 6, 5, 0, 11]
          |---C7---|  |---C8----|  |---C9----|
1
2
3
4
5
6
7
8

所以只要 client id 是连续的,那么 client 发向后端的连接就一定是连续的。

# 二、多租户

在一个微服务架构中允许 多系统共存 是利用微服务稳定性以及模块化最有效的方式之一,这种方式一般被称为多租户。租户可以是测试、金丝雀发布、影子系统(shadow systems),甚至服务层或者产品线,使用租户能够 保证代码的隔离性 并且能够 基于流量租户做路由决策

# 2.1 多测试环境

实际场景

假设现在有一个服务调用链 A -> B -> C,如果 C 同时有多个同学开发,如果甲同学的代码正在测试中,但是乙同学不小心发了一个版本,就会导致甲同学的代码被冲掉,导致测试同学测着测着就出现 bug。测试同学无法得知这个问题是由于环境导致的还是代码缺陷。

解决方案 1:多套物理环境

搭建多套测试环境,可以做到物理隔离,但是也会存在一些问题:

  • 混用环境导致的不可靠测试;
  • 多套环境带来的硬件成本;
  • 难以做负载测试,仿真线上真实流量情况;
  • 治标不治本,无法知道当前环境谁在使用,并且几套环境可以满足需求?万一又多几个人开发是不是又需要再来几套?

解决方案 2:多租户,染色发布

  1. 待测试的服务 B 在一个隔离的沙盒环境中启动,并且在沙盒环境下可以访问集成环境(UAT)C 和 D;
  2. 测试流量路由到服务 B,同时保持生产流量正常流入到集成服务,服务 B 仅仅处理测试流量而不处理生产流量;
  3. 另外要确保集成流量不要被测试流量影响。

生产中的测试提出了两个基本要求,它们也构成了多租户体系结构的基础:

  • 流量路由:能够基于流入栈中的流量类型做路由;
  • 隔离性:能够可靠的隔离测试和生产中的资源,这样可以保证对于关键业务微服务没有副作用。

具体实现方法如下:

  • 注册时:测试服务需要带上染色标签;
  • 测试时:需要在 Http 请求头中打上该标签,并在服务间透传;
  • 负载均衡策略:将原始的数组结构修改为 Map 结构,优先染色标签路由。

# 2.2 全链路压测

和上面的测试环境的解决方案类似,但是我们需要搭建一套和线上一致的影子系统。

  • 基础设施需要做改造,采用同样的基础设施节点;
    • 缓存:影子应用存储的数据放到影子库中,使用不同的 db;
    • 数据库:自动将线上的数据结构复制一份到影子数据库中,里面的表结构保持一致,数据库名做一些变化,例如 db_shadow;
    • 消息队列:推送消息的时候使用不同的 topic 或者是携带一些 metadata 信息;
  • 需要提前做一些数据初始化的操作,提前进行准备;
  • 压测时携带压测标签,将流量自动路由到影子服务进行压测。

这种方案同样可以用于灰度发版当中。

Last Updated: 2023-01-28 4:31:25