Spiga

分布式架构之管理设计

2025-08-30 20:38:31

一、分布式锁

我们知道,在多线程情况下访问一些共享资源需要加锁,不然就会出现数据被写乱的问题。在分布式系统下,这样的问题也是一样的。只不过,我们需要一个分布式的锁服务。

分布式的锁服务需要有以下几个特点。

  • 安全性(Safety):在任意时刻,只有一个客户端可以获得锁(排他性)。
  • 避免死锁:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达。
  • 容错性:只要锁服务集群中的大部分节点存活,Client 就可以进行加锁解锁操作。

1. Redis 的分布式锁服务

我们通过以下命令对资源加锁。

SET resource_name my_random_value NX PX 30000

解释一下:

  • SET NX 命令只会在 key 不存在的时候给 key 赋值,PX 命令通知 Redis 保存这个 key 30000ms。
  • my_random_value 必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性。
  • PX 操作后面的参数代表的是这个 key 的存活时间,称作锁过期时间。
  • 当资源被锁定超过这个时间时,锁将自动释放。
  • 获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题。

通过下面的脚本为申请成功的锁解锁:

if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else 
    return 0 
end

如果 key 对应的 value 一致,则删除这个 key。通过这个方式释放锁是为了避免 Client 释放了其他 Client 申请的锁。

2. 分布式锁服务的一个问题

虽然 Redis 文档里说他们的分布式锁是没有问题的,但其实还是很有问题的。尤其是上面那个为了避免 Client 端把锁占住不释放,然后,Redis 在超时后把其释放掉,这事儿听起来就有点不靠谱。

我们来脑补一下,不难发现下面这个案例。

1. 如果 Client A 先取得了锁。
2. Client B 在等待 Client A 的工作完成。
3. 这个时候,如果 Client A 被挂在了某些事上,比如一个外部的阻塞调用,或是 CPU 被别的进程吃满,或是不巧碰上了 Full GC,导致 Client A 花了超过平时几倍的时间。
4. 然后,我们的锁服务因为怕死锁,就在一定时间后,把锁给释放掉了。
5. 此时,Client B 获得了锁并更新了资源。
6. 这个时候,Client A 服务缓过来了,然后也去更新了资源。于是乎,把 Client B 的更新给冲掉了。
7. 这就造成了数据出错。

要解决这个问题,需要引入 fence(栅栏)技术。一般来说,这就是乐观锁机制,需要一个版本号排它。

  • 锁服务需要有一个单调递增的版本号。
  • 写数据的时候,也需要带上自己的版本号。
  • 数据库服务需要保存数据的版本号,然后对请求做检查。

3. 从乐观锁到 CAS

如果数据库中也保留着版本号,那么完全可以用数据库来做这个锁服务,不就更方便了吗?

更新语句写成 SQL 大概是下面这个样子

UPDATE table_name SET xxx = @{xxx}, version=version+1 where version =@{version};

这不就是乐观锁吗?是的,这是乐观锁最常用的一种实现方式。如果我们使用版本号,或是 fence token 这种方式,就不需要使用分布式锁服务了。

这种 fence token 的玩法,在数据库那边一般会用 timestamp 时间截来玩。也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则 OK,否则就是版本冲突。

我们有时候都不需要增加额外的版本字段或是 fence token。比如,如果想更新库存,我们可以这样操作:

SELECT stock FROM tb_product where product_id=@{product_id};
UPDATE tb_product SET stock=stock-@{num} WHERE product_id=@{product_id} AND stock=@{stock};

先把库存数量(stock)查出来,然后在更新的时候,检查一下是否是上次读出来的库存。如果不是,说明有别人更新过了,我的 UPDATE 操作就会失败,得重新再来。

这其实就是计算机汇编指令中的原子操作 CAS(Compare And Swap),大量无锁的数据结构都需要用到这个。

我们一步一步地从分布式锁服务到乐观锁,再到 CAS,你看到了什么?你是否得思考一个有趣的问题——我们还需要分布式锁服务吗?

4. 分布式锁设计的重点

首先,我们需要明确一下分布式锁服务的初衷和几个概念性的问题。

  • 如果获得锁的进程挂掉了怎么办?锁还不回来了,会导致死锁。一般的处理方法是在锁服务那边加上一个过期时间,如果在这个时间内锁没有被还回来,那么锁服务要自动解锁,以避免全部锁住。
  • 如果锁服务自动解锁了,新的进程就拿到锁了,但之前的进程以为自己还有锁,那么就出现了两个进程拿到了同一个锁的问题,它们在更新数据的时候就会产生问题。对于这个问题,像 Redis 那样也可以使用 Check and Set 的方式来保证数据的一致性。
  • 在改变一个值的时候先检查一下是不是我之前读出来的值,这样来保证其间没有人改过。如果通过像 CAS 这样的操作的话,我们还需要分布式锁服务吗?的确是不需要了,不是吗?
  • 但现实生活中也有不需要更新某个数据的场景,只是为了同步或是互斥一下不同机器上的线程,这时候像 Redis 这样的分布式锁服务就有意义了。

所以,需要分清楚:我是用来修改某个共享源的,还是用来不同进程间的同步或是互斥的。如果使用 CAS 这样的方式(无锁方式)来更新数据,那么我们是不需要使用分布式锁服务的,而后者可能是需要的。所以,这是我们在决定使用分布式锁服务前需要考虑的第一个问题——我们是否需要?

如果确定要分布式锁服务,需要考虑下面几个设计。

  • 需要给一个锁被释放的方式,以避免请求者不把锁还回来,导致死锁的问题。
  • 分布式锁服务应该是高可用的,而且是需要持久化的。
  • 要提供非阻塞方式的锁服务。
  • 还要考虑锁的可重入性。

二、配置中心

除了代码之外,软件还有一些配置信息,比如数据库的用户名和密码,还有一些我们不想写死在代码里的东西,像线程池大小、队列长度等运行参数,以及日志级别、算法策略等,还有一些是软件运行环境的参数,所有这些东西,我们都叫做软件配置。

以前,我们把软件配置写在一个配置文件中,然而,在分布式系统下,这样的方式就变得非常不好管理,并容易出错。于是,为了便于管理,我们引入了一个集中式的配置管理系统,这就是配置中心的由来。

1. 区分软件的配置

首先,我们要区分软件的配置,软件配置的区分有多种方式。

有一种方式是把软件的配置分成静态配置和动态配置。所谓静态配置其实就是在软件启动时的一些配置,运行时基本不会进行修改,也可以理解为是环境或软件初始化时需要用到的配置。

我们的内容主要针对动态配置的管理。对于动态配置的管理,我们还要做好区分。一般来说,会有三个区分的维度。

  • 按运行环境分。一般来说,会有开发环境、测试环境、预发环境、生产环境。这些环境上的运行配置都不完全一样,但是理论来说,应该是大同小异的。
  • 按依赖区分。一种是依赖配置,一种是不依赖的内部配置。比如,外部依赖的 MySQL 或 Redis 的连接配置。还有一种完全是自己内部的配置。
  • 按层次分。就像云计算一样,配置也可以分成 IaaS、PaaS、SaaS 三层。基础层的配置是操作系统的配置,中间平台层的配置是中间件的配置,上层软件层的配置是应用自己的配置。

2. 配置中心的模型

  • 软件配置基本上来说,每个配置项就是 key/value 的模型。

  • 可以把软件的配置分成三层:

    • 操作系统层和平台层的配置项得由专门的运维人员或架构师来配置。
    • 而应用层的配置项,需要有相应的命名规范,最好有名字空间的管理,确保不同应用的配置项不会冲突。
  • 配置参数中,如果有外部服务依赖的配置,强烈建议不要放在配置中心里,而要放在服务发现系统中。

    因为一方面这在语义上更清楚一些,另外,这样会减少因为运行不同环境而导致配置不同的差异性(如测试环境的日志级别是 Debug 级)。

  • 配置需要有一个整体的版本管理,每次变动都能将版本差异记录下来。当然,如果可能,最好能和软件的版本号做关联。

3. 配置中心的架构

在这个图中可以看到,配置录入后,配置中心发出变更通知,配置变更控制器会来读取最新的配置,然后应用配置。我们来说说细节问题:

  • 为什么需要一个变更通知的组件,而不是让配置中心直接推送?

    原因是,分布式环境下,服务器太多,推送不太现实,而采用一个 Pub/Sub 的通知服务可以让数据交换经济一些。

  • 为什么不直接 Pub 数据过去,还要订阅方反向拉数据?

    直接推数据当然可以,但让程序反过来用 API 读配置的好处是,一方面,API 可以校验请求者的权限,另一方面,有时候还是需要调用配置中心的基本 API,比如下载最新的证书之类的。还有就是,服务启动时需要从服务中心拉一份配置下来。

  • 配置变更控制器部署在哪里?是在每个服务器上呢,还是在一个中心的地方?

    因为这个事是要变更配置,变更配置又是有很多步骤的,所以这些步骤算是一个事务。为了执行效率更好,事务成功率更大,建议把这个配置变更的控制放在每一台主机上。

  • 平台层的配置变更,有的参数是在服务启动的命令行上,这个怎么变更呢?

    一般来说,命令行上的参数需要通过 Shell 环境变量做成配置项,然后通过更改系统环境变量,并重启服务达到配置变更。

  • 操作系统的配置变更和平台层的配置变更最好模块化掉,就像云服务中的不同尺寸的主机型号一样。

    这样有利于维护和减少配置的复杂性。

  • 应用服务配置更新的标准化。

    因为一个公司的应用由不同的团队完成,所以,可能其配置会因为应用的属性不同而不一样。为了便于管理,最好有统一的配置更新。一般来说,有的应用服务的配置是在配置文件中,有的应用服务的配置是通过调用 Admin API 的方式变更,不同的应用系统完全不一样。

    要完成配置的标准化,这里给几个方案:

    • 可以通过一个开发框架或 SDK 的方式来解决,也就是应用代码找你这个 SDK 来要配置,并通过 observer 模式订阅配置修改的事件,或是直接提供配置变更的 Admin 的 API。

      这种方式的好处在于在开发期标准化,并可以规范开发;不好的是,耦合语言。

    • 通过一个标准应用运维脚本,让应用方自己来提供应用变更时的脚本动作。

      这种方式虽然通过运维的方式标准化掉配置变更的接口,就可以通过一个配置控制器来统一操作各个应用变更,但是在这个脚本中各个应用方依然使用着各种不同的方式来变更配置。这种方式的好处是不耦合语言,灵活,但对于标准化的建设可能不利,而且使用或者调用脚本是 Bug 很多的东西,容易出问题。

    • 或是结合上述两种方案,不使用开发阶段的 SDK 方式嵌入到应用服务中,而是为每个应用服务单独做一个 Agent。这个 Agent 对外以 Admin API 的方式服务,后面则适配应用的配置变更手段,如更新配置文件,或者调用应用的 API 等。这种方式在落地方面是很不错的。

4. 配置中心的设计重点

  • 配置中心主要的用处是统一和规范化管理所有的服务配置,也算是一种配置上的治理活动。

  • 配置更新的时候是一个事务处理,需要考虑事务的问题,如果变更不能继续,需要回滚到上个版本的配置。配置版本最好和软件版本对应上。

  • 配置更新控制器,需要应用服务的配合,比如,配置的 reload,服务的优雅重启,服务的 Admin API,或是通过环境变量……这些最好是由一个统一的开发框架搞定。

  • 配置更新控制器还担任服务启动的责任,由配置更新控制器来启动服务。

    这样,配置控制器会从配置中心拉取所有的配置,更新操作系统,设置好启动时用的环境变量,并更新好服务需要的配置文件 ,然后启动服务。

三、边车模式

所谓的边车模式,对应于我们生活中熟知的边三轮摩托车。也就是说,我们可以通过给一个摩托车加上一个边车的方式来扩展现有的服务和功能。这样可以很容易地做到 " 控制 " 和 " 逻辑 " 的分离。

1. 边车模式设计

具体来说,可以理解为,边车就有点像一个服务的 Agent,这个服务所有对外的进出通讯都通过这个 Agent 来完成。这样,我们就可以在这个 Agent 上做很多文章了。但是,我们需要保证的是,这个 Agent 要和应用程序一起创建,一起停用。

边车模式有时候也叫搭档模式,或是伴侣模式,或是跟班模式。编程的本质就是将控制和逻辑分离和解耦,而边车模式也是异曲同工,同样是让我们在分布式架构中做到逻辑和控制分离。

对于监视、日志、限流、熔断、服务注册、协议转换等等这些功能,其实都是大同小异,甚至是完全可以做成标准化的组件和模块的。一般来说,我们有两种方式。

  • 一种是通过 SDK、Lib 或 Framework 软件包方式,在开发时与真实的应用服务集成起来。
  • 另一种是通过像 Sidecar 这样的方式,在运维时与真实的应用服务集成起来。

这两种方式各有优缺点。

  • 以软件包的方式可以和应用密切集成,有利于资源的利用和应用的性能,但是对应用有侵入,而且受应用的编程语言和技术限制。同时,当软件包升级的时候,需要重新编译并重新发布应用。

  • 以 Sidecar 的方式,对应用服务没有侵入性,并且不用受到应用服务的语言和技术的限制,而且可以做到控制和逻辑的分开升级和部署。但是,这样一来,增加了每个应用服务的依赖性,也增加了应用的延迟,并且也会大大增加管理、托管、部署的复杂度。

    注意,对于一些“老的系统”,因为代码太老,改造不过来,我们又没有能力重写。比如一些银行里很老的用 C 语言写的子系统,我们想把它们变成分布式系统,需要对其进行协议的改造以及进行相应的监控和管理。这个时候,Sidecar 的方式就很有价值了。因为没有侵入性,所以可以很快地低风险地改造。

Sidecar 服务在逻辑上和应用服务部署在一个结点中,其和应用服务有相同的生命周期。对比于应用程序的每个实例,都会有一个 Sidecar 的实例。Sidecar 可以很快也很方便地为应用服务进行扩展,而不需要应用服务的改造。比如:

  • Sidecar 可以帮助服务注册到相应的服务发现系统,并对服务做相关的健康检查。如果服务不健康,我们可以从服务发现系统中把服务实例移除掉。
  • 当应用服务要调用外部服务时, Sidecar 可以帮助从服务发现中找到相应外部服务的地址,然后做服务路由。
  • Sidecar 接管了进出的流量,我们就可以做相应的日志监视、调用链跟踪、流控熔断……这些都可以放在 Sidecar 里实现。
  • 然后,服务控制系统可以通过控制 Sidecar 来控制应用服务,如流控、下线等。

于是,我们的应用服务则可以完全做到专注于业务逻辑。

  • 注意,如果把 Sidecar 这个实例和应用服务部署在同一台机器中,那么,其实 Sidecar 的进程在理论上来说是可以访问应用服务的进程能访问的资源的。比如,Sidecar 是可以监控到应用服务的进程信息的。
  • 另外,因为两个进程部署在同一台机器上,所以两者之间的通信不存在明显的延迟。也就是说,服务的响应延迟虽然会因为跨进程调用而增加,但这个增加完全是可以接受的。
  • 另外,我们可以看到这样的部署方式,最好是与 Docker 容器的方式一起使用的。

2. 边车设计的重点

首先,我们要知道边车模式重点解决什么样的问题。

  • 控制和逻辑的分离。
  • 服务调用中上下文的问题。

边车模式从概念上理解起来比较简单,但是在工程实现上来说,需要注意以下几点:

  • 进程间通讯机制是这个设计模式的重点,千万不要使用任何对应用服务有侵入的方式,比如,通过信号的方式,或是通过共享内存的方式。最好的方式就是网络远程调用的方式(因为都在 127.0.0.1 上通讯,所以开销并不明显)。

  • 服务协议方面,也请使用标准统一的方式。这里有两层协议,一个是 Sidecar 到 service 的内部协议,另一个是 Sidecar 到远端 Sidecar 或 service 的外部协议。

    • 对于内部协议,需要尽量靠近和兼容本地 service 的协议;
    • 对于外部协议,需要尽量使用更为开放更为标准的协议。

    无论是哪种,都不应该使用与语言相关的协议。

  • 使用这样的模式,需要在服务的整体打包、构建、部署、管控、运维上设计好。使用 Docker 容器方面的技术可以帮助你全面降低复杂度。

  • Sidecar 中所实现的功能应该是控制面上的东西,而不是业务逻辑上的东西,所以请尽量不要把业务逻辑设计到 Sidecar 中。

  • 小心在 Sidecar 中包含通用功能可能带来的影响。例如,重试操作,这可能不安全,除非所有操作都是幂等的。

  • 另外,还要考虑允许应用服务和 Sidecar 的上下文传递的机制。

    例如,包含 HTTP 请求标头以选择退出重试,或指定最大重试次数等等这样的信息交互。或是 Sidecar 告诉应用服务限流发生,或是远程服务不可用等信息,这样可以让应用服务和 Sidecar 配合得更好。

Sidecar 适用于什么样的场景,下面罗列几个:

  • 一个比较明显的场景是对老应用系统的改造和扩展。
  • 另一个是对由多种语言混合出来的分布式服务系统进行管理和扩展。
  • 其中的应用服务由不同的供应商提供。
  • 把控制和逻辑分离,标准化控制面上的动作和技术,从而提高系统整体的稳定性和可用性。也有利于分工——并不是所有的程序员都可以做好控制面上的开发的。

Sidecar 不适用于什么样的场景,下面罗列几个:

  • 架构并不复杂的时候,不需要使用这个模式,直接使用 API Gateway 或者 Nginx 和 HAProxy 等即可。
  • 服务间的协议不标准且无法转换。
  • 不需要分布式的架构。

四、服务网络

前面,我讨论了 Sidecar 边车模式,这是一个非常不错的分布式架构的设计模式。因为这个模式可以有效地分离系统控制和业务逻辑,并且可以让整个系统架构在控制面上可以集中管理,可以显著地提高分布式系统的整体控制和管理效率,并且可以让业务开发更快速。

试想一下,如果某云服务提供商,提供了一个各式各样的分布式设计模式的 Sidecar 集群,那么我们就只用写业务逻辑相关的 service 了。写好一个就往这个集群中部署,开发和运维工作量都会得到巨大的降低和减少。

1. 什么是 Service Mesh

这就是 CNCF(Cloud Native Computing Foundation,云原生计算基金会)目前主力推动的新一代的微服务架构——Service Mesh 服务网格。

Service Mesh 这个服务网络专注于处理服务和服务间的通讯。其主要负责构造一个稳定可靠的服务通讯的基础设施,并让整个架构更为的先进和 Cloud Native。在工程中,Service Mesh 基本来说是一组轻量级的服务代理和应用逻辑的服务在一起,并且对于应用服务是透明的。

说白了,就是下面几个特点。

  • Service Mesh 是一个基础设施。
  • Service Mesh 是一个轻量的服务通讯的网络代理。
  • Service Mesh 对于应用服务来说是透明无侵入的。
  • Service Mesh 用于解耦和分离分布式系统架构中控制层面上的东西。

Service Mesh 就像是网络七层模型中的第四层 TCP 协议。其把底层的那些非常难控制的网络通讯方面的控制面的东西都管了(比如:丢包重传、拥塞控制、流量控制),而更为上面的应用层的协议,只需要关心自己业务应用层上的事了。如 HTTP 的 HTML 协议。

2. Service Mesh 的演化路径

Service Mesh 的出现并不是一个偶然,而是一个必然,其中的演化路径如下:

  • 一开始是最原始的两台主机间的进程直接通信。
  • 然后分离出网络层来,服务间的远程通信,通过底层的网络模型完成。
  • 再后来,因为两边的服务在接收的速度上不一致,所以需要应用层中实现流控。
  • 后来发现,流控模块基本可以交给网络层实现,于是 TCP/IP 就成了世界上最成功的网络协议。
  • 再往后面,分布式系统中有 " 弹力设计 "。于是,我们在更上层中加入了像限流、熔断、服务发现、监控等功能。
  • 然后,我们发现这些弹力设计的模式都是可以标准化的。将这些模式写成 SDK/Lib/Framework,这样就可以在开发层面上很容易地集成到我们的应用服务中。
  • 接下来,我们发现,SDK、Lib、Framework 不能跨编程语言。有什么改动后,要重新编译重新发布服务,太不方便了。应该有一个专门的层来干这事,于是出现了 Sidecar。
  • 然后Sidecar 集群就成了 Service Mesh。
  • 再后来,Sidecar 组成了一个平台,一个 Cloud Native 的服务流量调度的平台
  • 加上对整个集群的管理控制面板,就成了我们整个的 Service Mesh 架构。

3. Service Mesh 的设计重点

Service Mesh 因为其调度了流量,如果 Service Mesh 有 bug,或是 Sidecar 的组件不可用,会导致整个架构出现致命的问题。所以,在设计 Service Mesh 的时候,需要小心考虑,如果 Service Mesh 所管理的 Sidecar 出了问题,那应该怎么办?

所以,Service Mesh 这个网格一定要是高可靠的。

一种比较好的方式是,除了在本机有 Sidecar,我们还可以部署一下稍微集中一点的 Sidecar——比如为某个服务集群部署一个集中式的 Sidecar。一旦本机的有问题,可以走集中的。

这样一来,Sidecar 本来就是用来调度流量的,而且其粒度可以细到每个服务的实例,可以粗到一组服务,还可以粗到整体接入。这看来看去都像是一个 Gateway 的事。

Service Mesh 不像 Sidecar 需要和 Service 一起打包一起部署,Service Mesh 完全独立部署。这样一来,Service Mesh 就成了一个基础设施,就像一个 PaaS 平台。

五、网关模式

前面我们提到了 Gateway。其实,大部分的时候并不需要为每个服务的实例都配置上一个 Sidecar。其实,一个服务集群配上一个 Gateway 就可以了,或是一组类似的服务配置上一个 Gateway。

这样一来,Gateway 方式下的架构,可以细到为每一个服务的实例配置一个自己的 Gateway,也可以粗到为一组服务配置一个,甚至可以粗到为整个架构配置一个接入的 Gateway。于是,整个系统架构的复杂度就会变得简单可控起来。

1.网关模式设计

一个网关需要有以下的功能:

  • 请求路由。因为不再是 Sidecar 了,所以网关一定要有请求路由的功能。这样一来,对于调用端来说,也是一件非常方便的事情。因为调用端不需要知道自己需要用到的其它服务的地址,全部统一地交给 Gateway 来处理。
  • 服务注册。为了能够代理后面的服务,并把请求路由到正确的位置上,网关应该有服务注册功能,也就是后端的服务实例可以把其提供服务的地址注册、取消注册。
  • 负载均衡。因为一个网关可以接收多个服务实例,所以网关还需要在各个对等的服务实例上做负载均衡策略。
  • 弹力设计。网关还可以把弹力设计中的那些异步、重试、幂等、流控、熔断、监视等都可以实现进去。这样,同样可以像 Service Mesh 那样,让应用服务只关心自己的业务逻辑而不是控制逻辑。
  • 安全方面。SSL 加密及证书管理、Session 验证、授权、数据校验,以及对请求源进行恶意攻击的防范。错误处理越靠前的位置就是越好,所以,网关可以做到一个全站的接入组件来对后端的服务进行保护。

当然,网关还可以做更多更有趣的事情,比如:

  • 灰度发布。网关完全可以做到对相同服务不同版本的实例进行导流,还可以收集相关的数据。这样对于软件质量的提升,甚至产品试错都有非常积极的意义。
  • API 聚合。使用网关可以将多个单独请求聚合成一个请求。在微服务体系的架构中,因为服务变小了,所以一个明显的问题是,客户端可能需要多次请求才能得到所有的数据。这样一来,客户端与后端之间的频繁通信会对应用程序的性能和规模产生非常不利的影响。于是,我们可以让网关来帮客户端请求多个后端的服务,然后把后端服务的响应结果拼装起来,回传给客户端。
  • API 编排。同样在微服务的架构下,要走完一个完整的业务流程,我们需要调用一系列 API,就像一种工作流一样,这个事完全可以通过网页来编排这个业务流程。我们可能通过一个 DSL 来定义和编排不同的 API,也可以通过像 AWS Lambda 服务那样的方式来串联不同的 API。

2. Gateway、Sidecar 和 Service Mesh

  • 首先,Sidecar 的方式主要是用来改造已有服务。

  • 当 Sidecar 在架构中越来越多时,需要我们对 Sidecar 进行统一的管理。于是就出现了我们的 Service Mesh。

  • 然而,Service Mesh 的架构和部署太过于复杂,会让运维的复杂度变大。为了简化这个架构的复杂度,Gateway 更为适合。

    Gateway 只负责进入的请求,不像 Sidecar 还需要负责对外的请求。因为 Gateway 可以把一组服务给聚合起来,所以服务对外的请求可以交给对方服务的 Gateway。于是,我们只需要用一个负责进入请求的 Gateway 来简化需要同时负责进出请求的 Sidecar 的复杂度。

总而言之,Gateway 的方式比 Sidecar 和 Service Mesh 更好。当然,具体问题还要具体分析。

3. 网关的设计重点

  • 第一点是高性能。在技术设计上,网关不应该也不能成为性能的瓶颈。对于高性能,最好使用高性能的编程语言来实现。网关对后端的请求,以及对前端的请求的服务一定要使用异步非阻塞的 I/O 来确保后端延迟不会导致应用程序中出现性能问题。
  • 第二点是高可用。因为所有的流量或调用经过网关,所以网关必须成为一个高可用的技术组件,它的稳定直接关系到了所有服务的稳定。网关如果没有设计,就会变成一个单点故障。因此,一个好的网关至少要做到以下几点。
    • 集群化。网关要成为一个集群,其最好可以自己组成一个集群,并可以自己同步集群数据,而不需要依赖于一个第三方系统来同步数据。
    • 服务化。网关还需要做到在不间断的情况下修改配置,一种是像 Nginx reload 配置那样,可以做到不停服务,另一种是最好做到服务化。也就是说,得要有自己的 Admin API 来在运行时修改自己的配置。
    • 持续化。比如重启,就是像 Nginx 那样优雅地重启。有一个主管请求分发的主进程。当我们需要重启时,新的请求被分配到新的进程中,而老的进程处理完正在处理的请求后就退出。
  • 第三点是高扩展。因为网关需要承接所有的业务流量和请求,所以一定会有或多或少的业务逻辑。而业务逻辑是多变和不确定的。比如,需要在网关上加入一些和业务相关的东西。因此,一个好的 Gateway 还需要是可以扩展的,并能进行二次开发的。

另外,在运维方面,网关应该有以下几个设计原则。

  • 业务松耦合,协议紧耦合。在业务设计上,网关不应与后面的服务之间形成服务耦合,也不应该有业务逻辑。网关应该是在网络应用层上的组件,不应该处理通讯协议体,只应该解析和处理通讯协议头。另外,除了服务发现外,网关不应该有第三方服务的依赖。
  • 应用监视,提供分析数据。网关上需要考虑应用性能的监控,除了有相应后端服务的高可用的统计之外,还需要使用 Tracing ID 实施分布式链路跟踪,并统计好一定时间内每个 API 的吞吐量、响应时间和返回码,以便启动弹力设计中的相应策略。
  • 用弹力设计保护后端服务。网关上一定要实现熔断、限流、重试和超时等弹力设计。
  • DevOps。因为网关这个组件太关键了,所以需要 DevOps 这样的东西,将其发生故障的概率降到最低。这个软件需要经过精良的测试,包括功能和性能的测试,还有浸泡测试。还需要有一系列自动化运维的管控工具。

在整体的架构方面,有如下一些注意事项。

  • 不要在网关中的代码里内置聚合后端服务的功能,而应考虑将聚合服务放在网关核心代码之外。可以使用 Plugin 的方式,也可以放在网关后面形成一个 Serverless 服务。

  • 网关应该靠近后端服务,并和后端服务使用同一个内网,这样可以保证网关和后端服务调用的低延迟,并可以减少很多网络上的问题。

    这里多说一句,网关处理的静态内容应该靠近用户(应该放到 CDN 上),而网关和此时的动态服务应该靠近后端服务。

  • 网关也需要做容量扩展,所以需要成为一个集群来分担前端带来的流量。这一点,要么通过 DNS 轮询的方式实现,要么通过 CDN 来做流量调度,或者通过更为底层的性能更高的负载均衡设备。

  • 对于服务发现,可以做一个时间不长的缓存,这样不需要每次请求都去查一下相关的服务所在的地方。当然,如果你的系统不复杂,可以考虑把服务发现的功能直接集成进网关中。

  • 为网关考虑隔离设计方式。用不同的网关服务不同的后端服务,或是用不同的网关服务前端不同的客户。

另外,因为网关是为用户请求和后端服务的桥接装置,所以需要考虑一些安全方面的事宜。具体如下:

  • 加密数据。可以把 SSL 相关的证书放到网关上,由网关做统一的 SSL 传输管理。
  • 校验用户的请求。一些基本的用户验证可以放在网关上来做,比如用户是否已登录,用户请求中的 token 是否合法等。但是,我们需要权衡一下,网关是否需要校验用户的输入。因为这样一来,网关就需要从只关心协议头,到需要关心协议体。而协议体中的东西一方面不像协议头是标准的,另一方面解析协议体还要耗费大量的运行时间,从而降低网关的性能。对此,可以看具体需求,一方面如果协议体是标准的,那么可以干;另一方面,对于解析协议所带来的性能问题,需要做相应的隔离。
  • 检测异常访问。网关需要检测一些异常访问,比如,在一段比较短的时间内请求次数超过一定数值;还比如,同一客户端的 4xx 请求出错率太高……对于这样的一些请求访问,网关一方面要把这样的请求屏蔽掉,另一方面需要发出警告,有可能会是一些比较重大的安全问题,如被黑客攻击。

六、部署升级策略

服务部署的模式。一般来说,有如下几种:

  • 停机部署: 把现有版本的服务停机,然后部署新的版本。
  • 蓝绿部署:部署好新版本后,把流量从老服务那边切过来。
  • 滚动部署:一点一点地升级现有的服务。
  • 灰度部署:把一部分用户切到新版本上来,然后看一下有没有问题。如果没有问题就继续扩大升级,直到全部升级完成。
  • AB 测试:同时上线两个版本,然后做相关的比较。

1. 停机部署

停机部署其实是最简单粗暴的方式,就是简单地把现有版本的服务停机,然后部署新的版本。

这种方式的优势是,在部署过程中不会出现新老版本同时在线的情况,所有状态完全一致。停机部署主要是为了新版本的一致性问题。

这种方式的问题是会停机,对用户的影响很大。所以,一般来说,这种部署方式需要事前挂公告,选择一个用户访问少的时间段来做。

2. 蓝绿部署

蓝绿部署与停机部署最大的不同是,其在生产线上部署相同数量的新服务,然后当新的服务测试确认 OK 后,把流量切到新的服务这边来。蓝绿部署比停机部署好的地方是,它无需停机。

生产线上有两套相同的集群,一套是 Prod 是真实服务的,另一套是 Stage 是预发环境,发布发 Stage,然后把流量切到 Stage 这边,于是 Stage 就成了 Prod,而之前的 Prod 则成了 Stage。有点像换页似的。

这种部署的问题就是有点浪费,因为需要使用双倍的资源。

另外,如果服务中有状态,比如一些缓存什么的,停机部署和蓝绿部署都会有问题。

3. 滚动部署

滚动部署策略是指通过逐个替换应用的所有实例,来缓慢发布应用的一个新版本。

这种部署方式直接对现有的服务进行升级,虽然便于操作,而且在缓慢地更新的过程中,对于有状态的服务也是比较友好的,状态可以在更新中慢慢重建起来。但是,这种部署的问题也是比较多的。

  • 在发布过程中,会出现新老两个版本同时在线的情况,同一用户的请求可能在新老版中切换而导致问题。
  • 我们的新版程序没有在生产线上经过验证就上线了。
  • 在整个过程中,生产环境处于一个新老更替的中间状态,如果有问题要回滚就有点麻烦了。
  • 如果在升级过程中,需要做别的一些运维工作,我们还要判断哪些结点是老版本的,哪些结点是新版本的。这太痛苦了。
  • 因为新老版本的代码同时在线,所以其依赖的服务需要同时处理两个版本的请求,这可能会带来兼容性问题。
  • 我们无法让流量在新老版本中切换。

4. 灰度部署(金丝雀)

灰度部署是指逐渐将生产环境流量从老版本切换到新版本。通常流量是按比例分配的。例如 90% 的请求流向老版本,10% 的请求流向新版本。然后没有发现问题,就逐步扩大新版本上的流量,减少老版本上的流量。

除了切流量外,对于多租户的平台,例如云计算平台,灰度部署也可以将一些新的版本先部署到一些用户上,如果没有问题,扩大部署,直到全部用户。一般的策略是,从内部用户开始,然后是一般用户,最后是大客户。

5. AB 测试

AB 测试和蓝绿部署或是灰度部署完全是不一样的。

AB 测试是同时上线两个版本,然后做相关的比较。它是用来测试应用功能表现的方法,例如可用性、受欢迎程度、可见性等。

蓝绿部署是为了不停机,灰度部署是对新版本的质量没信心。而 AB 测试是对新版的功能没信心。注意,一个是质量,一个是功能。

比如,网站 UI 大改版,推荐算法的更新,流程的改变,我们不知道新的版本否会得到用户青睐或是能得到更好的用户体验,我们需要收集一定的用户数据才能知道。

AB 测试,其包含了灰度发布的功能。也就是说,如果只是观测有没有 bug,那就是灰度发布了。如果我们复杂一点,要观测用户的一些数据指标,这完全也可能做成自动化的,如果新版本数据好,就自动化地切一点流量过来,如果不行,就换一批用户(样本)再试试。

对于灰度发布或是 AB 测试可以使用下面的技术来选择用户。

  • 浏览器 cookie。
  • 查询参数。
  • 地理位置。
  • 技术支持,如浏览器版本、屏幕尺寸、操作系统等。
  • 客户端语言。