This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
<audio id="audio" title="52 | 管理设计篇之“分布式锁”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/da/a3199ca0ff2ace332e3d7418f8f7c8da.mp3"></audio>
我们知道在多线程情况下访问一些共享资源需要加锁不然就会出现数据被写乱的问题。在分布式系统下这样的问题也是一样的。只不过我们需要一个分布式的锁服务。对于分布式的锁服务一般可以用数据库DB、Redis和ZooKeeper等实现。不管怎么样分布式的锁服务需要有以下几个特点。
<li>
**安全性Safety**:在任意时刻,只有一个客户端可以获得锁(**排他性**)。
</li>
<li>
**避免死锁**:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达。
</li>
<li>
**容错性**只要锁服务集群中的大部分节点存活Client就可以进行加锁解锁操作。
</li>
# Redis的分布式锁服务
这里提一下避免死锁的问题。下面以Redis的锁服务为例参考 [Redis的官方文档](https://redis.io/topics/distlock) )。
我们通过以下命令对资源加锁。
```
SET resource_name my_random_value NX PX 30000
```
解释一下:
<li>
`SET NX` 命令只会在 `key` 不存在的时候给 `key` 赋值,`PX` 命令通知Redis保存这个key 30000ms。
</li>
<li>
`my_random_value` 必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性。
</li>
<li>
PX 操作后面的参数代表的是这个key的存活时间称作锁过期时间。
</li>
<li>
当资源被锁定超过这个时间时,锁将自动释放。
</li>
<li>
获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题。
</li>
这里的原理是只有在某个key不存在的情况下才能设置set成功该key。于是这就可以让多个进程并发去设置同一个key只有一个进程能设置成功。而其它的进程因为之前有人把key设置成功了而导致失败也就是获得锁失败
我们通过下面的脚本为申请成功的锁解锁:
```
if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1] then
return redis.call(&quot;del&quot;,KEYS[1])
else
return 0
end
```
如果key对应的value一致则删除这个key。
通过这个方式释放锁是为了避免Client释放了其他Client申请的锁。
例如下面的例子演示了不区分Client会出现的一种问题。
1. Client A 获得了一个锁。
1. 当尝试释放锁的请求发送给Redis时被阻塞没有及时到达Redis。
1. 锁定时间超时Redis认为锁的租约到期释放了这个锁。
1. Client B 重新申请到了这个锁。
1. Client A的解锁请求到达将Client B锁定的key解锁。
1. Client C 也获得了锁。
1. Client B 和Client C 同时持有锁。
通过执行上面脚本的方式释放锁Client的解锁操作只会解锁自己曾经加锁的资源所以是安全的。
关于value的生成官方推荐从 /dev/urandom中取20个byte作为随机数。或者采用更加简单的方式例如使用RC4加密算法在 /dev/urandom中得到一个种子Seed然后生成一个伪随机流。
也可以采用更简单的方法,使用时间戳+客户端编号的方式生成随机数。Redis的官方文档说“这种方式的安全性较差一些但对于绝大多数的场景来说已经足够安全了”。
# 分布式锁服务的一个问题
注意虽然Redis文档里说他们的分布式锁是没有问题的但其实还是很有问题的。尤其是上面那个为了避免Client端把锁占住不释放然后Redis在超时后把其释放掉。不知道你怎么想但我觉得这事儿听起来就有点不靠谱。
我们来脑补一下,不难发现下面这个案例。
<li>
如果Client A先取得了锁。
</li>
<li>
其它Client比如说Client B在等待Client A的工作完成。
</li>
<li>
这个时候如果Client A被挂在了某些事上比如一个外部的阻塞调用或是CPU被别的进程吃满或是不巧碰上了Full GC导致Client A 花了超过平时几倍的时间。
</li>
<li>
然后,我们的锁服务因为怕死锁,就在一定时间后,把锁给释放掉了。
</li>
<li>
此时Client B获得了锁并更新了资源。
</li>
<li>
这个时候Client A服务缓过来了然后也去更新了资源。于是乎把Client B的更新给冲掉了。
</li>
<li>
这就造成了数据出错。
</li>
这听起来挺严重的吧。我画了个图示例一下。
<img src="https://static001.geekbang.org/resource/image/93/89/937d9975899662d90a96f4cd70580d89.png" alt="" />
千万不要以为这是脑补出来的案例。其实这个是真实案例。HBase就曾经遇到过这样的问题你可以在他们的PPT[HBase and HDFS: Understanding FileSystem Usage in HBase](https://www.slideshare.net/enissoz/hbase-and-hdfs-understanding-filesystem-usage))中看到相关的描述。
要解决这个问题你需要引入fence栅栏技术。一般来说这就是乐观锁机制需要一个版本号排它。我们的流程就变成了下图中的这个样子。
<img src="https://static001.geekbang.org/resource/image/ce/c3/ce3454e9a8bbfe4628899391c003a5c3.png" alt="" />
我们从图中可以看到:
- 锁服务需要有一个单调递增的版本号。
- 写数据的时候,也需要带上自己的版本号。
- 数据库服务需要保存数据的版本号,然后对请求做检查。
如果使用ZooKeeper做锁服务的话那么可以使用 `zxid` 或 znode的版本号来做这个fence 版本号。
# 从乐观锁到CAS
但是,我们想想,如果数据库中也保留着版本号,那么完全可以用数据库来做这个锁服务,不就更方便了吗?下面的图展示了这个过程。
<img src="https://static001.geekbang.org/resource/image/95/41/9557fb5b7269eb5d7d53568298803141.png" alt="" />
使用数据版本Version记录机制即为数据增加一个版本标识一般是通过为数据库表增加一个数字类型的 “version” 字段来实现的。当读取数据时将version字段的值一同读出数据每更新一次对此version值加一。
当我们提交更新的时候数据库表对应记录的当前版本信息与第一次取出来的version值进行比对。如果数据库表当前版本号与第一次取出来的version值相等则予以更新否则认为是过期数据。更新语句写成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操作就会失败得重新再来。
细心的你一定发现了这不就是计算机汇编指令中的原子操作CASCompare And Swap大量无锁的数据结构都需要用到这个。关于CAS的话题你可以看一下我在CoolShell上写的[无锁队列的实现](https://coolshell.cn/articles/8239.html) )。
**我们一步一步地从分布式锁服务到乐观锁再到CAS你看到了什么你是否得思考一个有趣的问题——我们还需要分布式锁服务吗**
# 分布式锁设计的重点
最后,我们来谈谈分布式锁设计的重点。
一般情况下我们可以使用数据库、Redis或ZooKeeper来做分布式锁服务这几种方式都可以用于实现分布式锁。
分布式锁的特点是,保证在一个集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。这就是所谓的分布式互斥。所以,大家在做某个事的时候,要去一个服务上请求一个标识。如果请求到了,我们就可以操作,操作完后,把这个标识还回去,这样别的进程就可以请求到了。
首先,我们需要明确一下分布式锁服务的初衷和几个概念性的问题。
<li>
如果获得锁的进程挂掉了怎么办?锁还不回来了,会导致死锁。一般的处理方法是在锁服务那边加上一个过期时间,如果在这个时间内锁没有被还回来,那么锁服务要自动解锁,以避免全部锁住。
</li>
<li>
如果锁服务自动解锁了,新的进程就拿到锁了,但之前的进程以为自己还有锁,那么就出现了两个进程拿到了同一个锁的问题,它们在更新数据的时候就会产生问题。对于这个问题,我想说:
</li>
<li>
像Redis那样也可以使用Check and Set的方式来保证数据的一致性。这就有点像计算机原子指令CASCompare And Swap一样。就是说我在改变一个值的时候先检查一下是不是我之前读出来的值这样来保证其间没有人改过。
</li>
<li>
如果通过像CAS这样的操作的话我们还需要分布式锁服务吗的确是不需要了不是吗
</li>
<li>
但现实生活中也有不需要更新某个数据的场景只是为了同步或是互斥一下不同机器上的线程这时候像Redis这样的分布式锁服务就有意义了。
</li>
所以需要分清楚我是用来修改某个共享源的还是用来不同进程间的同步或是互斥的。如果使用CAS这样的方式无锁方式来更新数据那么我们是不需要使用分布式锁服务的而后者可能是需要的。**所以,这是我们在决定使用分布式锁服务前需要考虑的第一个问题——我们是否需要?**
如果确定要分布式锁服务,你需要考虑下面几个设计。
<li>
需要给一个锁被释放的方式以避免请求者不把锁还回来导致死锁的问题。Redis使用超时时间ZooKeeper可以依靠自身的sessionTimeout来删除节点。
</li>
<li>
分布式锁服务应该是高可用的,而且是需要持久化的。对此,你可以看一下 [Redis的文档RedLock](https://redis.io/topics/distlock) 看看它是怎么做到高可用的。
</li>
<li>
要提供非阻塞方式的锁服务。
</li>
<li>
还要考虑锁的可重入性。
</li>
我认为Redis也是不错的ZooKeeper在使用起来需要有一些变通的方式好在Apache有 [Curator](https://curator.apache.org/) 帮我们封装了各种分布式锁的玩法。
# 小结
好了,我们来总结一下今天分享的主要内容。首先,我介绍了为什么需要分布式锁。就像单机系统上的多线程程序需要用操作系统锁或数据库锁来互斥对共享资源的访问一样,分布式程序也需要通过分布式锁来互斥对共享资源的访问。
分布式锁服务一般可以通过Redis和ZooKeeper等实现。接着以Redis为例我介绍了怎样用它来加锁和解锁由此引出了锁超时后的潜在风险。我们看到类似于数据库的乐观并发控制这种风险可以通过版本号的方式来解决。
进一步数据库如果本身利用CAS等手段支持这种版本控制方式其实也就没必要用一个独立的分布式锁服务了。最后我们发现分布式锁服务还能用来做同步这是数据库锁做不了的事情。下篇文章中我们将聊聊配置中心相关的技术希望对你有帮助。
也欢迎你分享一下你在留言区给我分享下哪些场景下你会用到锁你都用哪种平台的锁服务有没有用到数据库锁是OCC还是悲观锁如果是悲观锁的话你又是怎样避免死锁的
我在这里给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,142 @@
<audio id="audio" title="53 | 管理设计篇之“配置中心”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/e4/79119949d10c1f06246935b0cf55d8e4.mp3"></audio>
我们知道除了代码之外软件还有一些配置信息比如数据库的用户名和密码还有一些我们不想写死在代码里的东西像线程池大小、队列长度等运行参数以及日志级别、算法策略等还有一些是软件运行环境的参数如Java的内存大小应用启动的参数包括操作系统的一些参数配置……
所有这些东西我们都叫做软件配置。以前我们把软件配置写在一个配置文件中就像Windows下的ini文件或是Linux下的conf文件。然而在分布式系统下这样的方式就变得非常不好管理并容易出错。于是为了便于管理我们引入了一个集中式的配置管理系统这就是配置中心的由来。
现在,软件的配置中心是分布式系统的一个必要组件。这个系统听起来很简单,但其实并不是。我见过好多公司的配置中心,但是我觉得做得都不好,所以,想写下这篇文章给你一些借鉴。
# 配置中心的设计
## 区分软件的配置
首先,我们要区分软件的配置,软件配置的区分有多种方式。
有一种方式是把软件的配置分成静态配置和动态配置。所谓静态配置其实就是在软件启动时的一些配置,运行时基本不会进行修改,也可以理解为是环境或软件初始化时需要用到的配置。
例如操作系统的网络配置软件运行时Docker进程的配置这些配置在软件环境初始化时就确定了未来基本不会修改了。而所谓动态配置其实就是软件运行时的一些配置在运行时会被修改。比如日志级别、降级开关、活动开关。
当然,我们这里的内容主要针对动态配置的管理。
对于动态配置的管理,我们还要做好区分。一般来说,会有三个区分的维度。
<li>
**按运行环境分**。一般来说,会有开发环境、测试环境、预发环境、生产环境。这些环境上的运行配置都不完全一样,但是理论来说,应该是大同小异的。
</li>
<li>
**按依赖区分**。一种是依赖配置一种是不依赖的内部配置。比如外部依赖的MySQL或Redis的连接配置。还有一种完全是自己内部的配置。
</li>
<li>
**按层次分**。就像云计算一样配置也可以分成IaaS、PaaS、SaaS三层。基础层的配置是操作系统的配置中间平台层的配置是中间件的配置如Tomcat的配置上层软件层的配置是应用自己的配置。
</li>
这些分类方式其实是为了更好地管理我们的配置项。小公司无所谓,而当一个公司变大了以后了,如果这些东西没有被很好地管理起来,那么会增加太多系统维护的复杂度。
## 配置中心的模型
有了上面为配置项的分类,我们就可以设计软件配置模型了。
首先软件配置基本上来说每个配置项就是key/value的模型。
然后我们把软件的配置分成三层。操作系统层和平台层的配置项得由专门的运维人员或架构师来配置。其中的value应该是选项而不是让用户可以自由输入的最好是有相关的模板来初始化全套的配置参数。而应用层的配置项需要有相应的命名规范最好有像C++那样的名字空间的管理,确保不同应用的配置项不会冲突。
另外,我们的配置参数中,如果有外部服务依赖的配置,强烈建议不要放在配置中心里,而要放在服务发现系统中。因为一方面这在语义上更清楚一些,另外,这样会减少因为运行不同环境而导致配置不同的差异性(如测试环境和生产环境的不同)。
对于不同运行环境中配置的差异来说比如在开发环境和测试环境下日志级别是Debug级对于生产环境则是Warning或Error级因为环境的不一样会导致我们需要不同的配置项的值。这点需要考虑到。
还有,我们的配置需要有一个整体的版本管理,每次变动都能将版本差异记录下来。当然,如果可能,最好能和软件的版本号做关联。
我们可以看到其中有些配置是通过模板来选择的有的配置需要在不同环境下配置不同值。所以还需要一个配置管理的工具可能是命令行的也可以是Web的。这个工具的界面在文本中下面这个UI的mockup只是想表明一个模型
<img src="https://static001.geekbang.org/resource/image/5a/b7/5aeb4055738bd15188a007ccbbbc38b7.png" alt="" />
用户可以根据不同的机器型号还有不同的环境直接调出后台配置好的相关标准配置的模板。对于一些用户需要自己调整的参数也可以在这个模板上进行调整和配置(当然,为了方便运维和管理最好不要进行调整)。然后,用户可以在下面的那个表格中填写好自己的应用要用的参数和各个环境中的值。
这样一来这个工具就可以非常方便地让开发人员来配置他们自己的软件配置。而我们的配置中心还需要提API来让应用获取配置。这个API上至少需要有如下参数服务名配置的版本号配置的环境。
## 配置中心的架构
接下来,要来解决配置落地的问题。我们可以看到,和一个软件运行有关系的各种配置隶属于不同的地方,所以,要让它们落地还需要些不一样的细节要处理。文本中,我们给了一个大概的架构图。
<img src="https://static001.geekbang.org/resource/image/74/b5/745c444c53457239de884a943adff1b5.png" alt="" />
在这个图中可以看到,我们把配置录入后,配置中心发出变更通知,配置变更控制器会来读取最新的配置,然后应用配置。这看上去很简单,但是有很多细节问题,下面我来一一说明。
<li>
**为什么需要一个变更通知的组件,而不是让配置中心直接推送?** 原因是分布式环境下服务器太多推送不太现实而采用一个Pub/Sub的通知服务可以让数据交换经济一些。
</li>
<li>
**为什么不直接Pub数据过去还要订阅方反向拉数据** 直接推数据当然可以但让程序反过来用API读配置的好处是一方面API可以校验请求者的权限另一方面有时候还是需要调用配置中心的基本API比如下载最新的证书之类的。还有就是服务启动时需要从服务中心拉一份配置下来。
</li>
<li>
**配置变更控制器部署在哪里?是在每个服务器上呢,还是在一个中心的地方?** 我觉得因为这个事是要变更配置,变更配置又是有很多步骤的,所以这些步骤算是一个事务。为了执行效率更好,事务成功率更大,建议把这个配置变更的控制放在每一台主机上。
</li>
<li>
**平台层的配置变更,有的参数是在服务启动的命令行上,这个怎么变更呢?** 一般来说命令行上的参数需要通过Shell环境变量做成配置项然后通过更改系统环境变量并重启服务达到配置变更。
</li>
<li>
**操作系统的配置变更和平台层的配置变更最好模块化掉,就像云服务中的不同尺寸的主机型号一样。** 这样有利于维护和减少配置的复杂性。
</li>
<li>
**应用服务配置更新的标准化。** 因为一个公司的应用由不同的团队完成所以可能其配置会因为应用的属性不同而不一样。为了便于管理最好有统一的配置更新。一般来说有的应用服务的配置是在配置文件中有的应用服务的配置是通过调用Admin API的方式变更不同的应用系统完全不一样你似乎完全没有方法做成统一的。这里给几个方案。
</li>
<li>
可以通过一个开发框架或SDK的方式来解决也就是应用代码找你这个SDK来要配置并通过observer模式订阅配置修改的事件或是直接提供配置变更的Admin 的 API。这种方式的好处在于在开发期标准化并可以规范开发不好的是耦合语言。
</li>
<li>
通过一个标准应用运维脚本让应用方自己来提供应用变更时的脚本动作。这种方式虽然通过运维的方式标准化掉配置变更的接口就可以通过一个配置控制器来统一操作各个应用变更但是在这个脚本中各个应用方依然使用着各种不同的方式来变更配置。这种方式的好处是不耦合语言灵活但对于标准化的建设可能不利而且使用或者调用脚本是Bug很多的东西容易出问题。
</li>
<li>
或是结合上述两种方案不使用开发阶段的SDK方式嵌入到应用服务中而是为每个应用服务单独做一个Agent。这个Agent对外以Admin API的方式服务后面则适配应用的配置变更手段如更新配置文件或者调用应用的API等。这种方式在落地方面是很不错的这其中是另一种设计模式后面会讲到
</li>
# 配置中心的设计重点
配置中心主要的用处是统一和规范化管理所有的服务配置,也算是一种配置上的治理活动。所以,配置中心的设计重点应该放在如何统一和标准化软件的配置项,其还会涉及到软件版本、运行环境、平台、中间件等一系列的配置参数。如果你觉得软件配置非常复杂,那么,你应该静下心来仔细梳理或治理一下现有的配置参数,并简化相应的配置,使用模块会是一种比较好的简化手段。
根据我们前面《编程范式游记》中所说的编程的本质是对logic和control的分离所以对于配置也一样其也有控制面上的配置和业务逻辑面上的配置控制面上的配置最好能标准统一。
配置更新的时候是一个事务处理,需要考虑事务的问题,如果变更不能继续,需要回滚到上个版本的配置。配置版本最好和软件版本对应上。
配置更新控制器需要应用服务的配合比如配置的reload服务的优雅重启服务的Admin API或是通过环境变量……这些最好是由一个统一的开发框架搞定。
配置更新控制器还担任服务启动的责任,由配置更新控制器来启动服务。这样,配置控制器会从配置中心拉取所有的配置,更新操作系统,设置好启动时用的环境变量,并更新好服务需要的配置文件 ,然后启动服务。(当然,你也可以在服务启动的脚本中真正启动服务前放上一段让配置更新控制器更新配置的脚本。无论怎么样,这些都可以在运维层面实现,不需要业务开发人员知道。)
# 小结
好了,我们来总结一下今天分享的主要内容。首先,传统单机软件的配置通常保存在文件中,但在分布式系统下,为了管理方便,必须有一个配置中心。然后我讲了配置的区分:按静态和动态、运行环境、依赖和层次来区分。进一步,从区分出的情况出发,层次方面,平台、中间件和应用三个层次由不同职责的运维人员来配置。
外部依赖的配置并不适合放在配置中心里,而最好是由服务发现系统来提供。开发环境和生产环境的日志级别配置也会不同。出于这些特点,可以用一个配置管理工具来管理这些配置。接着,我介绍了配置管理架构中几个关键问题的解决思路。最后,我介绍了配置中心的几个设计重点。下篇文章中,我们讲述边车模式。希望对你有帮助。
也欢迎你分享一下你的分布式系统用到了配置中心吗?它是怎样实现的呢?配置的动态更新是怎么处理的?有没有版本管理,和服务的版本又是怎样关联的呢?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="54 | 管理设计篇之“边车模式”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/a8/335d3228f6ed1869d24e9465502d37a8.mp3"></audio>
所谓的边车模式,对应于我们生活中熟知的边三轮摩托车。也就是说,我们可以通过给一个摩托车加上一个边车的方式来扩展现有的服务和功能。这样可以很容易地做到&quot;控制&quot;&quot;逻辑&quot;的分离。
也就是说,我们不需要在服务中实现控制面上的东西,如监视、日志记录、限流、熔断、服务注册、协议适配转换等这些属于控制面上的东西,而只需要专注地做好和业务逻辑相关的代码,然后,由“边车”来实现这些与业务逻辑没有关系的控制功能。
# 边车模式设计
具体来说你可以理解为边车就有点像一个服务的Agent这个服务所有对外的进出通讯都通过这个Agent来完成。这样我们就可以在这个Agent上做很多文章了。但是我们需要保证的是这个Agent要和应用程序一起创建一起停用。
边车模式有时候也叫搭档模式,或是伴侣模式,或是跟班模式。就像我们在《编程范式游记》中看到的那样,**编程的本质就是将控制和逻辑分离和解耦,而边车模式也是异曲同工**,同样是让我们在分布式架构中做到逻辑和控制分离。
对于监视、日志、限流、熔断、服务注册、协议转换等等这些功能,其实都是大同小异,甚至是完全可以做成标准化的组件和模块的。一般来说,我们有两种方式。
<li>
一种是通过SDK、Lib或Framework软件包方式在开发时与真实的应用服务集成起来。
</li>
<li>
另一种是通过像Sidecar这样的方式在运维时与真实的应用服务集成起来。
</li>
这两种方式各有优缺点。
<li>
以软件包的方式可以和应用密切集成,有利于资源的利用和应用的性能,但是对应用有侵入,而且受应用的编程语言和技术限制。同时,当软件包升级的时候,需要重新编译并重新发布应用。
</li>
<li>
以Sidecar的方式对应用服务没有侵入性并且不用受到应用服务的语言和技术的限制而且可以做到控制和逻辑的分开升级和部署。但是这样一来增加了每个应用服务的依赖性也增加了应用的延迟并且也会大大增加管理、托管、部署的复杂度。
注意对于一些“老的系统”因为代码太老改造不过来我们又没有能力重写。比如一些银行里很老的用C语言或是COBAL语言写的子系统我们想把它们变成分布式系统需要对其进行协议的改造以及进行相应的监控和管理。这个时候Sidecar的方式就很有价值了。因为没有侵入性所以可以很快地低风险地改造。
Sidecar服务在逻辑上和应用服务部署在一个结点中其和应用服务有相同的生命周期。对比于应用程序的每个实例都会有一个Sidecar的实例。Sidecar可以很快也很方便地为应用服务进行扩展而不需要应用服务的改造。比如
</li>
<li>
Sidecar可以帮助服务注册到相应的服务发现系统并对服务做相关的健康检查。如果服务不健康我们可以从服务发现系统中把服务实例移除掉。
</li>
<li>
当应用服务要调用外部服务时, Sidecar可以帮助从服务发现中找到相应外部服务的地址然后做服务路由。
</li>
<li>
Sidecar接管了进出的流量我们就可以做相应的日志监视、调用链跟踪、流控熔断……这些都可以放在Sidecar里实现。
</li>
<li>
然后服务控制系统可以通过控制Sidecar来控制应用服务如流控、下线等。
</li>
于是,我们的应用服务则可以完全做到专注于业务逻辑。
<img src="https://static001.geekbang.org/resource/image/e3/f7/e30300b16a8fe0870ebfbec5a093b4f7.png" alt="" />
注意如果把Sidecar这个实例和应用服务部署在同一台机器中那么其实Sidecar的进程在理论上来说是可以访问应用服务的进程能访问的资源的。比如Sidecar是可以监控到应用服务的进程信息的。
另外,因为两个进程部署在同一台机器上,所以两者之间的通信不存在明显的延迟。也就是说,服务的响应延迟虽然会因为跨进程调用而增加,但这个增加完全是可以接受的。
另外我们可以看到这样的部署方式最好是与Docker容器的方式一起使用的。为什么Docker一定会是分布式系统或是云计算的关键技术相信你从我的这一系列文章中已经看到其简化架构的部署和管理的重要作用。否则这么多的分布式架构模式实施起来会有很多麻烦。
# 边车设计的重点
首先,我们要知道边车模式重点解决什么样的问题。
1. 控制和逻辑的分离。
1. 服务调用中上下文的问题。
我们知道,熔断、路由、服务发现、计量、流控、监视、重试、幂等、鉴权等控制面上的功能,以及其相关的配置更新,本质来上来说,和服务的关系并不大。但是传统的工程做法是在开发层面完成这些功能,这就会导致各种维护上的问题,而且还会受到特定语言和编程框架的约束和限制。
而随着系统架构的复杂化和扩张我们需要更统一地管理和控制这些控制面上的功能所以传统的在开发层面上完成控制面的管理会变得非常难以管理和维护。这使得我们需要通过Sidecar模式来架构我们的系统。
边车模式从概念上理解起来比较简单,但是在工程实现上来说,需要注意以下几点。
<li>
进程间通讯机制是这个设计模式的重点千万不要使用任何对应用服务有侵入的方式比如通过信号的方式或是通过共享内存的方式。最好的方式就是网络远程调用的方式因为都在127.0.0.1上通讯,所以开销并不明显)。
</li>
<li>
服务协议方面也请使用标准统一的方式。这里有两层协议一个是Sidecar到service的内部协议另一个是Sidecar到远端Sidecar或service的外部协议。对于内部协议需要尽量靠近和兼容本地service的协议对于外部协议需要尽量使用更为开放更为标准的协议。但无论是哪种都不应该使用与语言相关的协议。
</li>
<li>
使用这样的模式需要在服务的整体打包、构建、部署、管控、运维上设计好。使用Docker容器方面的技术可以帮助你全面降低复杂度。
</li>
<li>
Sidecar中所实现的功能应该是控制面上的东西而不是业务逻辑上的东西所以请尽量不要把业务逻辑设计到Sidecar中。
</li>
<li>
小心在Sidecar中包含通用功能可能带来的影响。例如重试操作这可能不安全除非所有操作都是幂等的。
</li>
<li>
另外我们还要考虑允许应用服务和Sidecar的上下文传递的机制。 例如包含HTTP请求标头以选择退出重试或指定最大重试次数等等这样的信息交互。或是Sidecar告诉应用服务限流发生或是远程服务不可用等信息这样可以让应用服务和Sidecar配合得更好。
</li>
当然我们要清楚Sidecar适用于什么样的场景下面罗列几个。
- 一个比较明显的场景是对老应用系统的改造和扩展。
- 另一个是对由多种语言混合出来的分布式服务系统进行管理和扩展。
- 其中的应用服务由不同的供应商提供。
- 把控制和逻辑分离,标准化控制面上的动作和技术,从而提高系统整体的稳定性和可用性。也有利于分工——并不是所有的程序员都可以做好控制面上的开发的。
同时我们还要清楚Sidecar不适用于什么样的场景下面罗列几个。
- 架构并不复杂的时候不需要使用这个模式直接使用API Gateway或者Nginx和HAProxy等即可。
- 服务间的协议不标准且无法转换。
- 不需要分布式的架构。
# 小结
好了,我们来总结一下今天分享的主要内容。首先,我介绍了什么是边车模式。为了把诸如监视、日志、限流等控制逻辑与业务逻辑分离解耦,我们可以采用边车模式。与之对应的另一种实现控制逻辑的方式是库或框架。虽然相对来说边车模式资源消耗较大,但控制逻辑不会侵入业务逻辑,还能适应遗留老系统的低风险改造。
边车作为另一个进程和服务进程部署在同一个结点中通过一个标准的网络协议如HTTP来进行通信。这样可以做到低延迟和标准化。同时用Docker来打包边车和服务两者可以非常方便部署。最后我指出了边车模式适用和不适用的场景。下篇文章中我们讲述服务网格。希望对你有帮助。
也欢迎你分享一下你实现服务的同时有没有实现边车模式有没有用到Docker来打包边车和服务两者
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="55 | 管理设计篇之“服务网格”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/a2/0c1cd6ae58330b23dfbd6aa26ce860a2.mp3"></audio>
前面我讨论了Sidecar边车模式这是一个非常不错的分布式架构的设计模式。因为这个模式可以有效地分离系统控制和业务逻辑并且可以让整个系统架构在控制面上可以集中管理可以显著地提高分布式系统的整体控制和管理效率并且可以让业务开发更快速。
那么我们不妨在上面这个模式下think big一下。假如我们在一个分布式系统中已经把一些标准的Sidecar给部署好了。比如前面文章说过的熔断、限流、重试、幂等、路由、监视等这些东西。我们在每个计算结点上都部署好了这些东西那么真实的业务服务只需要往这个集群中放就可以和本地的Sidecar通信然后由Sidecar委托代理与其它系统的交互和控制。这样一来我们的业务开发和运维岂不是简单之极了
是啊试想一下如果某云服务提供商提供了一个带着前面我们说过的那些各式各样的分布式设计模式的Sidecar集群那么我们的用户真的就只用写业务逻辑相关的service了。写好一个就往这个集群中部署开发和运维工作量都会得到巨大的降低和减少。
# 什么是Service Mesh
这就是CNCFCloud Native Computing Foundation云原生计算基金会目前主力推动的新一代的微服务架构——Service Mesh服务网格。
在[Whats a service mesh? And why do I need one?](https://buoyant.io/2017/04/25/whats-a-service-mesh-and-why-do-i-need-one/) 中解释了什么是Service Mesh。
A service mesh is a dedicated infrastructure layer for handling service-to-service communication. Its responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.
Service Mesh这个服务网络专注于处理服务和服务间的通讯。其主要负责构造一个稳定可靠的服务通讯的基础设施并让整个架构更为的先进和Cloud Native。在工程中Service Mesh基本来说是一组轻量级的服务代理和应用逻辑的服务在一起并且对于应用服务是透明的。
说白了,就是下面几个特点。
- Service Mesh是一个基础设施。
- Service Mesh是一个轻量的服务通讯的网络代理。
- Service Mesh对于应用服务来说是透明无侵入的。
- Service Mesh用于解耦和分离分布式系统架构中控制层面上的东西。
说起来Service Mesh就像是网络七层模型中的第四层TCP协议。其把底层的那些非常难控制的网络通讯方面的控制面的东西都管了比如丢包重传、拥塞控制、流量控制而更为上面的应用层的协议只需要关心自己业务应用层上的事了。如HTTP的HTML协议。
[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html) 这篇文章里也详细解释了Service Mesh的出现并不是一个偶然而是一个必然其中的演化路径如下。
<li>
一开始是最原始的两台主机间的进程直接通信。
</li>
<li>
然后分离出网络层来,服务间的远程通信,通过底层的网络模型完成。
</li>
<li>
再后来,因为两边的服务在接收的速度上不一致,所以需要应用层中实现流控。
</li>
<li>
后来发现流控模块基本可以交给网络层实现于是TCP/IP就成了世界上最成功的网络协议。
</li>
<li>
再往后面我们知道了分布式系统中的8个谬论 [The 8 Fallacies of Distributed Computing](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing) ,意识到需要在分布式系统中有&quot;弹力设计&quot;。于是,我们在更上层中加入了像限流、熔断、服务发现、监控等功能。
</li>
<li>
然后我们发现这些弹力设计的模式都是可以标准化的。将这些模式写成SDK/Lib/Framework这样就可以在开发层面上很容易地集成到我们的应用服务中。
</li>
<li>
接下来我们发现SDK、Lib、Framework不能跨编程语言。有什么改动后要重新编译重新发布服务太不方便了。应该有一个专门的层来干这事于是出现了Sidecar。
</li>
<img src="https://static001.geekbang.org/resource/image/d8/c7/d8aaf6cfe490ffc3b89d08decf7c96c7.png" alt="" /><br />
图片来自[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)
然后呢Sidecar集群就成了Service Mesh。图中的绿色模块是真实的业务应用服务蓝色模块则是Sidecar其组成了一个网格。而我们的应用服务完全独立自包含只需要和本机的Sidecar依赖剩下的事全交给了Sidecar。
<img src="https://static001.geekbang.org/resource/image/e9/bd/e9235eeaf30df456748d391144bd2bbd.png" alt="" /><br />
图片来自[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)
于是 Sidecar组成了一个平台一个Cloud Native的服务流量调度的平台你是否还记得我在《分布式系统的本质》那一系列文章中所说的关键技术中的流量调度和应用监控其都可以通过Service Mesh这个平台来完成
<img src="https://static001.geekbang.org/resource/image/3d/d1/3d66848ecdc7e582015d8178e702d3d1.png" alt="" /><br />
图片来自[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)
加上对整个集群的管理控制面板就成了我们整个的Service Mesh架构。
<img src="https://static001.geekbang.org/resource/image/bf/78/bf90978e3488ff0c8eb5f8c759ab1078.png" alt="" /><br />
图片来自[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)
<img src="https://static001.geekbang.org/resource/image/bb/3f/bb846cf73db84f1551f3051fc1705b3f.png" alt="" /><br />
图片来自[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)
# Service Mesh相关的开源软件
目前比较流行的Service Mesh开源软件是 [Istio](https://istio.io) 和 [Linkerd](https://linkerd.io)它们都可以在Kubernetes中集成。当然还有一个新成员 [Conduit](https://conduit.io)它是由Linkerd的作者出来自己搞的由Rust和Go写成的。Rust负责数据层面Go负责控制面。号称吸取了很多Linkerd的Scala的教训比Linkerd更快还轻更简单。
我虽然不是语言的偏好者但是不可否认Rust/Go的性能方面比Scala要好得多得多尤其是要做成一个和网络通讯相关的基础设施性能是比较重要的。
对此我还是推荐大家使用Rust/Go语言实现的lstio和Conduit后者比前者要轻很多。你可以根据你的具体需求挑选或是自己实现。
lstio是目前最主流的解决方案其架构并不复杂其核心的Sidecar被叫做Envoy使者用来协调服务网格中所有服务的出入站流量并提供服务发现、负载均衡、限流熔断等能力还可以收集大量与流量相关的性能指标。
在Service Mesh控制面上有一个叫Mixer的收集器用来从Envoy收集相关的被监控到的流量特征和性能指标。然后通过Pilot的控制器将相关的规则发送到Envoy中让Envoy应用新的规则。
最后还有一个为安全设计的lstio-Auth身份认证组件用来做服务间的访问安全控制。
整个lstio的架构图如下。
<img src="https://static001.geekbang.org/resource/image/1a/f2/1a579db1c95608588052b167e68836f2.png" alt="" />
# Service Mesh的设计重点
Service Mesh作为Sidecar一个集群应用Sidecar需要的微观层面上的那些设计要点在这里就不再复述了欢迎大家看我之前的文章。这里更多地说一下Service Mesh在整体架构上的一些设计要点。
我们知道像Kubernetes和Docker也是分布式系统管理面上的技术解决方案它们一样对于应用程序是透明的。最重要的是Kubernetes和Docker对于应用服务的干扰是比较少的。也就是说Kubernetes和Docker的服务进程的失败不会导致应用服务的异常运行。然后Service Mesh则不是因为其调度了流量所以如果Service Mesh有bug或是Sidecar的组件不可用就会导致整个架构出现致命的问题。
所以在设计Service Mesh的时候我们需要小心考虑如果Service Mesh所管理的Sidecar出了问题那应该怎么办所以Service Mesh这个网格一定要是高可靠的或者是出现了故障有workaround的方式。一种比较好的方式是除了在本机有Sidecar我们还可以部署一下稍微集中一点的Sidecar——比如为某个服务集群部署一个集中式的Sidecar。一旦本机的有问题可以走集中的。
这样一来Sidecar本来就是用来调度流量的而且其粒度可以细到每个服务的实例可以粗到一组服务还可以粗到整体接入。这看来看去都像是一个Gateway的事。所以我相信使用Gateway来干这个事应该是最合适不过的了。这样我们的Service Mesh的想像空间一下子就大多了。
Service Mesh不像Sidecar需要和Service一起打包一起部署Service Mesh完全独立部署。这样一来Service Mesh就成了一个基础设施就像一个PaaS平台。所以Service Mesh能不能和Kubernetes密切结合就成为了非常关键的因素。
# 小结
好了,我们来总结一下今天分享的主要内容。首先,边车模式进化的下一阶段,就是把它的功能标准化成一个集群,其结果就是服务网格。它在分布式系统中的地位,类似于七层网络模型中的传输层协议,而服务本身则只需要关心业务逻辑,因此类似于应用层协议。然后,我介绍了几个实现了服务网格的开源软件。最后,我介绍了服务网格的几个设计重点。下篇文章中,我们讲述网关模式。希望对你有帮助。
也欢迎你分享一下你接触到的分布式系统有没有用到服务网格?具体用的是哪个开源或闭源的框架?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="56 | 管理设计篇之“网关模式”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/f5/759be953a4ec93fe27aecd0ec936bef5.mp3"></audio>
前面我们讲了Sidecar和Service Mesh这两种设计模式它们都是在不侵入业务逻辑的情况下把控制面control plane和数据面data plane的处理解耦分离。但是这两种模式都让我们的运维成本变得特别大因为每个服务都需要一个Sidecar这让本来就复杂的分布式系统的架构就更为复杂和难以管理了。
在谈Service Mesh的时候我们提到了Gateway。我个人觉得并不需要为每个服务的实例都配置上一个Sidecar。其实一个服务集群配上一个Gateway就可以了或是一组类似的服务配置上一个Gateway。
这样一来Gateway方式下的架构可以细到为每一个服务的实例配置一个自己的Gateway也可以粗到为一组服务配置一个甚至可以粗到为整个架构配置一个接入的Gateway。于是整个系统架构的复杂度就会变得简单可控起来。
<img src="https://static001.geekbang.org/resource/image/2c/f9/2c82836fe26b71ce6ad228bf285795f9.png" alt="" />
这张图展示了一个多层Gateway架构其中有一个总的Gateway接入所有的流量并分发给不同的子系统还有第二级Gateway用于做各个子系统的接入Gateway。可以看到网关所管理的服务粒度可粗可细。通过网关我们可以把分布式架构组织成一个星型架构由网络对服务的请求进行路由和分发也可以架构成像Servcie Mesh那样的网格架构或者只是为了适配某些服务的Sidecar……
但是我们也可以看到这样一来Sidecar就不再那么轻量了而且很有可能会变得比较重了。
总的来说Gateway是一个服务器也可以说是进入系统的唯一节点。这跟面向对象设计模式中的Facade模式很像。Gateway封装内部系统的架构并且提供API给各个客户端。它还可能有其他功能如授权、监控、负载均衡、缓存、熔断、降级、限流、请求分片和管理、静态响应处理等等。
下面,我们来谈谈一个好的网关应该有哪些设计功能。
# 网关模式设计
一个网关需要有以下的功能。
<li>
**请求路由**。因为不再是Sidecar了所以网关一定要有请求路由的功能。这样一来对于调用端来说也是一件非常方便的事情。因为调用端不需要知道自己需要用到的其它服务的地址全部统一地交给Gateway来处理。
</li>
<li>
**服务注册**。为了能够代理后面的服务并把请求路由到正确的位置上网关应该有服务注册功能也就是后端的服务实例可以把其提供服务的地址注册、取消注册。一般来说注册也就是注册一些API接口。比如HTTP的Restful请求可以注册相应API的URI、方法、HTTP头。 这样Gateway就可以根据接收到的请求中的信息来决定路由到哪一个后端的服务上。
</li>
<li>
**负载均衡**。因为一个网关可以接收多个服务实例所以网关还需要在各个对等的服务实例上做负载均衡策略。简单点就是直接Round-Robin轮询复杂点的可以设置上权重进行分发再复杂一点还可以做到session粘连。
</li>
<li>
**弹力设计**。网关还可以把弹力设计中的那些异步、重试、幂等、流控、熔断、监视等都可以实现进去。这样同样可以像Service Mesh那样让应用服务只关心自己的业务逻辑或是说数据面上的事而不是控制逻辑控制面
</li>
<li>
**安全方面**。SSL加密及证书管理、Session验证、授权、数据校验以及对请求源进行恶意攻击的防范。错误处理越靠前的位置就是越好所以网关可以做到一个全站的接入组件来对后端的服务进行保护。
</li>
当然,网关还可以做更多更有趣的事情,比如:
<li>
**灰度发布**。网关完全可以做到对相同服务不同版本的实例进行导流,还可以收集相关的数据。这样对于软件质量的提升,甚至产品试错都有非常积极的意义。
</li>
<li>
**API聚合**。使用网关可以将多个单独请求聚合成一个请求。在微服务体系的架构中,因为服务变小了,所以一个明显的问题是,客户端可能需要多次请求才能得到所有的数据。这样一来,客户端与后端之间的频繁通信会对应用程序的性能和规模产生非常不利的影响。于是,我们可以让网关来帮客户端请求多个后端的服务(有些场景下完全可以并发请求),然后把后端服务的响应结果拼装起来,回传给客户端(当然,这个过程也可以做成异步的,但这需要客户端的配合)。
</li>
<li>
**API编排**。同样在微服务的架构下要走完一个完整的业务流程我们需要调用一系列API就像一种工作流一样这个事完全可以通过网页来编排这个业务流程。我们可能通过一个DSL来定义和编排不同的API也可以通过像AWS Lambda服务那样的方式来串联不同的API。
</li>
# Gateway、Sidecar和Service Mesh
通过上面的描述我们可以看到网关、边车和Service Mesh是非常像的三种设计模式很容易混淆。因此我在这里想明确一下这三种设计模式的特点、场景和区别。
首先Sidecar的方式主要是用来改造已有服务。我们知道要在一个架构中实施一些架构变更时需要业务方一起过来进行一些改造。然而业务方的事情比较多像架构上的变更会低优先级处理这就导致架构变更的“政治复杂度”太高。而通过Sidecar的方式我们可以适配应用服务成为应用服务进出请求的代理。这样我们就可以干很多对于业务方完全透明的事情了。
当Sidecar在架构中越来越多时需要我们对Sidecar进行统一的管理。于是我们为Sidecar增加了一个全局的中心控制器就出现了我们的Service Mesh。在中心控制器出现以后我们发现可以把非业务功能的东西全部实现在Sidecar和Controller中于是就成了一个网格。业务方只需要把服务往这个网格中一放就好了与其它服务的通讯、服务的弹力等都不用管了像一个服务的PaaS平台。
然而Service Mesh的架构和部署太过于复杂会让我们运维层面上的复杂度变大。为了简化这个架构的复杂度我认为Sidecar的粒度应该是可粗可细的这样更为方便。但我认为Gateway更为适合而且Gateway只负责进入的请求不像Sidecar还需要负责对外的请求。因为Gateway可以把一组服务给聚合起来所以服务对外的请求可以交给对方服务的Gateway。于是我们只需要用一个负责进入请求的Gateway来简化需要同时负责进出请求的Sidecar的复杂度。
总而言之我觉得Gateway的方式比Sidecar和Service Mesh更好。当然具体问题还要具体分析。
# 网关的设计重点
第一点是**高性能**。在技术设计上网关不应该也不能成为性能的瓶颈。对于高性能最好使用高性能的编程语言来实现如C、C++、Go和Java。网关对后端的请求以及对前端的请求的服务一定要使用异步非阻塞的 I/O 来确保后端延迟不会导致应用程序中出现性能问题。C和C++可以参看Linux下的epoll和Windows的I/O Completion Port的异步IO模型Java下如Netty、Vert.x、Spring Reactor的NIO框架。当然我还是更喜欢Go语言的goroutine 加 channel玩法。
第二点是**高可用**。因为所有的流量或调用经过网关,所以网关必须成为一个高可用的技术组件,它的稳定直接关系到了所有服务的稳定。网关如果没有设计,就会成变一个单点故障。因此,一个好的网关至少要做到以下几点。
- **集群化**。网关要成为一个集群,其最好可以自己组成一个集群,并可以自己同步集群数据,而不需要依赖于一个第三方系统来同步数据。
- **服务化**。网关还需要做到在不间断的情况下修改配置一种是像Nginx reload配置那样可以做到不停服务另一种是最好做到服务化。也就是说得要有自己的Admin API来在运行时修改自己的配置。
- **持续化**。比如重启就是像Nginx那样优雅地重启。有一个主管请求分发的主进程。当我们需要重启时新的请求被分配到新的进程中而老的进程处理完正在处理的请求后就退出。
第三点是**高扩展**。因为网关需要承接所有的业务流量和请求所以一定会有或多或少的业务逻辑。而我们都知道业务逻辑是多变和不确定的。比如需要在网关上加入一些和业务相关的东西。因此一个好的Gateway还需要是可以扩展的并能进行二次开发的。当然像Nginx那样通过Module进行二次开发的固然可以。但我还是觉得应该做成像AWS Lambda那样的方式也就是所谓的Serverless或FaaSFunction as a Service那样的方式。
另外,在**运维方面**,网关应该有以下几个设计原则。
<li>
**业务松耦合,协议紧耦合**。在业务设计上,网关不应与后面的服务之间形成服务耦合,也不应该有业务逻辑。网关应该是在网络应用层上的组件,不应该处理通讯协议体,只应该解析和处理通讯协议头。另外,除了服务发现外,网关不应该有第三方服务的依赖。
</li>
<li>
**应用监视,提供分析数据**。网关上需要考虑应用性能的监控除了有相应后端服务的高可用的统计之外还需要使用Tracing ID实施分布式链路跟踪并统计好一定时间内每个API的吞吐量、响应时间和返回码以便启动弹力设计中的相应策略。
</li>
<li>
**用弹力设计保护后端服务**。网关上一定要实现熔断、限流、重试和超时等弹力设计。如果一个或多个服务调用花费的时间过长,那么可接受超时并返回一部分数据,或是返回一个网关里的缓存的上一次成功请求的数据。你可以考虑一下这样的设计。
</li>
<li>
**DevOps**。因为网关这个组件太关键了所以需要DevOps这样的东西将其发生故障的概率降到最低。这个软件需要经过精良的测试包括功能和性能的测试还有浸泡测试。还需要有一系列自动化运维的管控工具。
</li>
在整体的**架构方面**,有如下一些注意事项。
<li>
不要在网关中的代码里内置聚合后端服务的功能而应考虑将聚合服务放在网关核心代码之外。可以使用Plugin的方式也可以放在网关后面形成一个Serverless服务。
</li>
<li>
网关应该靠近后端服务并和后端服务使用同一个内网这样可以保证网关和后端服务调用的低延迟并可以减少很多网络上的问题。这里多说一句网关处理的静态内容应该靠近用户应该放到CDN上而网关和此时的动态服务应该靠近后端服务。
</li>
<li>
网关也需要做容量扩展所以需要成为一个集群来分担前端带来的流量。这一点要么通过DNS轮询的方式实现要么通过CDN来做流量调度或者通过更为底层的性能更高的负载均衡设备。
</li>
<li>
对于服务发现,可以做一个时间不长的缓存,这样不需要每次请求都去查一下相关的服务所在的地方。当然,如果你的系统不复杂,可以考虑把服务发现的功能直接集成进网关中。
</li>
<li>
为网关考虑bulkhead设计方式。用不同的网关服务不同的后端服务或是用不同的网关服务前端不同的客户。
</li>
另外,因为网关是为用户请求和后端服务的桥接装置,所以需要考虑一些安全方面的事宜。具体如下:
<li>
**加密数据**。可以把SSL相关的证书放到网关上由网关做统一的SSL传输管理。
</li>
<li>
**校验用户的请求**。一些基本的用户验证可以放在网关上来做比如用户是否已登录用户请求中的token是否合法等。但是我们需要权衡一下网关是否需要校验用户的输入。因为这样一来网关就需要从只关心协议头到需要关心协议体。而协议体中的东西一方面不像协议头是标准的另一方面解析协议体还要耗费大量的运行时间从而降低网关的性能。对此我想说的是看具体需求一方面如果协议体是标准的那么可以干另一方面对于解析协议所带来的性能问题需要做相应的隔离。
</li>
<li>
**检测异常访问**。网关需要检测一些异常访问比如在一段比较短的时间内请求次数超过一定数值还比如同一客户端的4xx请求出错率太高……对于这样的一些请求访问网关一方面要把这样的请求屏蔽掉另一方面需要发出警告有可能会是一些比较重大的安全问题如被黑客攻击。
</li>
# 小结
好了,我们来总结一下今天分享的主要内容。首先,网关模式能代替边车模式,区别是它将分布在各个服务边上的边车换成了集中式的网关。网关不必管理所有服务节点,而是可以根据需要,为指定的服务集群配上网关,也可以在网关前面加上更高层的网关,从而构造出一个星型的结构。
接着,我列举了网关模式的功能特性。然后,我介绍了网关模式的设计重点。由于网关的功能比较多,因此在设计上要考虑的点也比较多,需要我们仔细思考和斟酌。下篇文章中,我们讲述部署升级策略。希望对你有帮助。
也欢迎你分享一下你接触到的分布式系统有没有用到网关?网关的功能如何?有没有把服务的弹力设计做在里面?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)

View File

@@ -0,0 +1,141 @@
<audio id="audio" title="57 | 管理设计篇之“部署升级策略”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/26/99c63b23a8b8e6914607a0008d35b726.mp3"></audio>
在分布式系统的世界里,一个服务有多个实例,所以部署或是升级一个服务也会变得比较麻烦。今天我们讨论服务部署的模式。一般来说,有如下几种:
<li>
停机部署Big Bang / Recreate 把现有版本的服务停机,然后部署新的版本。
</li>
<li>
蓝绿部署Blue/Green /Stage部署好新版本后把流量从老服务那边切过来。
</li>
<li>
滚动部署Rolling Update / Ramped 一点一点地升级现有的服务。
</li>
<li>
灰度部署Canary把一部分用户切到新版本上来然后看一下有没有问题。如果没有问题就继续扩大升级直到全部升级完成。
</li>
<li>
AB测试A/B Testing同时上线两个版本然后做相关的比较。
</li>
下面,我们来看一下每种方式的使用场景和优缺点。
# 停机部署
停机部署其实是最简单粗暴的方式,就是简单地把现有版本的服务停机,然后部署新的版本。有时候,我们不得不使用这样的方式来部署或升级多个服务。比如,新版本中的服务使用到了和老版本完全不兼容的数据表设计。这个时候,我们对生产有两个变更,一个是数据库,另一个是服务,而且新老版本互不兼容,所以只能使用停机部署的方式。
这种方式的优势是,在部署过程中不会出现新老版本同时在线的情况,所有状态完全一致。停机部署主要是为了新版本的一致性问题。
这种方式的问题是会停机,对用户的影响很大。所以,一般来说,这种部署方式需要事前挂公告,选择一个用户访问少的时间段来做。
# 蓝绿部署
蓝绿部署与停机部署最大的不同是其在生产线上部署相同数量的新服务然后当新的服务测试确认OK后把流量切到新的服务这边来。蓝绿部署比停机部署好的地方是它无需停机。
我们可以看到这种部署方式就是我们说的预发环境。在我以前的金融公司里也经常用这种方式生产线上有两套相同的集群一套是Prod是真实服务的另一套是Stage是预发环境发布发Stage然后把流量切到Stage这边于是Stage就成了Prod而之前的Prod则成了Stage。有点像换页似的。
这种方式的优点是没有停机,实时发布和升级,也避免有新旧版本同时在线的问题。但这种部署的问题就是有点浪费,因为需要使用双倍的资源(不过,这只是在物理机时代,在云计算时代没事,因为虚拟机部署完就可以释放了)。
另外,如果我们的服务中有状态,比如一些缓存什么的,停机部署和蓝绿部署都会有问题。
# 滚动部署
滚动部署策略是指通过逐个替换应用的所有实例来缓慢发布应用的一个新版本。通常过程如下在负载调度后有个版本A的应用实例池一个版本B的实例部署成功可以响应请求时该实例被加入到池中。然后版本A的一个实例从池中删除并下线。
这种部署方式直接对现有的服务进行升级,虽然便于操作,而且在缓慢地更新的过程中,对于有状态的服务也是比较友好的,状态可以在更新中慢慢重建起来。但是,这种部署的问题也是比较多的。
<li>
在发布过程中,会出现新老两个版本同时在线的情况,同一用户的请求可能在新老版中切换而导致问题。
</li>
<li>
我们的新版程序没有在生产线上经过验证就上线了。
</li>
<li>
在整个过程中,生产环境处于一个新老更替的中间状态,如果有问题要回滚就有点麻烦了。
</li>
<li>
如果在升级过程中,需要做别的一些运维工作,我们还要判断哪些结点是老版本的,哪些结点是新版本的。这太痛苦了。
</li>
<li>
因为新老版本的代码同时在线,所以其依赖的服务需要同时处理两个版本的请求,这可能会带来兼容性问题。
</li>
<li>
而且,我们无法让流量在新老版本中切换。
</li>
# 灰度部署(金丝雀)
灰度部署又叫金丝雀部署。其得名来源于矿井中的金丝雀。17世纪英国矿井工人发现金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯金丝雀也会停止歌唱。而当瓦斯含量超过一定限度时虽然鲁钝的人类毫无察觉金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下工人们每次下井都会带上一只金丝雀作为&quot;瓦斯检测指标&quot;,以便在危险状况下紧急撤离。
灰度部署是指逐渐将生产环境流量从老版本切换到新版本。通常流量是按比例分配的。例如90%的请求流向老版本10%的请求流向新版本。然后没有发现问题,就逐步扩大新版本上的流量,减少老版本上的流量。
除了切流量外,对于多租户的平台,例如云计算平台,灰度部署也可以将一些新的版本先部署到一些用户上,如果没有问题,扩大部署,直到全部用户。一般的策略是,从内部用户开始,然后是一般用户,最后是大客户。
这个技术大多数用于缺少足够测试,或者缺少可靠测试,或者对新版本的稳定性缺乏信心的情况下。
把一部分用户切到新版上来,然后看一下有没有问题。如果没有问题就继续扩大升级,直到全部升级完成。
# AB测试
AB测试和蓝绿部署或是金丝雀灰度部署完全是不一样的。
AB测试是同时上线两个版本然后做相关的比较。它是用来测试应用功能表现的方法例如可用性、受欢迎程度、可见性等。
蓝绿部署是为了不停机灰度部署是对新版本的质量没信心。而AB测试是对新版的功能没信心。注意一个是质量一个是功能。
比如网站UI大改版推荐算法的更新流程的改变我们不知道新的版本否会得到用户青睐或是能得到更好的用户体验我们需要收集一定的用户数据才能知道。
于是我们需要在生产线上发布两个版本拉一部分用户过来当小白鼠然后通过科学的观测得出来相关的结论。AB测试旨在通过科学的实验设计、采样样本代表性、流量分割与小流量测试等方式来获得具有代表性的实验结论并确信该结论在推广到全部流量时可信。
我们可以看到AB测试其包含了灰度发布的功能。也就是说我们的观测如果只是观测有没有bug那就是灰度发布了。当然如果我们复杂一点要观测用户的一些数据指标这完全也可能做成自动化的如果新版本数据好就自动化地切一点流量过来如果不行就换一批用户样本再试试。
对于灰度发布或是AB测试可以使用下面的技术来选择用户。
- 浏览器cookie。
- 查询参数。
- 地理位置。
- 技术支持,如浏览器版本、屏幕尺寸、操作系统等。
- 客户端语言。
# 小结
部署应用有很多种方法,实际采用哪种方式取决于需求和预算。当发布到开发或者模拟环境时,停机或者滚动部署是一个好选择,因为干净和快速。当发布到生产环境时,滚动部署或者蓝绿部署通常是一个好选择,但新平台的主流程测试是必须的。
蓝绿部署也不错但需要额外的资源。如果应用缺乏测试或者对软件的功能和稳定性影响缺乏信心那么可以使用金丝雀部署或者AB测试发布。如果业务需要根据地理位置、语言、操作系统或者浏览器特征等参数来给一些特定的用户测试那么可以采用AB测试技术。
<img src="https://static001.geekbang.org/resource/image/1b/09/1be6c93b43915e97a23d3c681daee909.png" alt="" />
好了我们来总结一下今天分享的主要内容。首先常见的部署升级策略有停机、蓝绿、滚动、灰度和AB测试这几种。然后我讲述了每一种部署策略的含义和优缺点。最后我将它们放在一起做了一个比较。下篇文章是《分布式系统设计模式》第三部分——性能设计的第一篇&quot;缓存&quot;。希望对你有帮助。
也欢迎你分享一下你接触到的部署方式有哪些?在什么场景下使用哪一种部署方式?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
<li>弹力设计篇
<ul>
- [认识故障和弹力设计](https://time.geekbang.org/column/article/3912)
- [隔离设计Bulkheads](https://time.geekbang.org/column/article/3917)
- [异步通讯设计Asynchronous](https://time.geekbang.org/column/article/3926)
- [幂等性设计Idempotency](https://time.geekbang.org/column/article/4050)
- [服务的状态State](https://time.geekbang.org/column/article/4086)
- [补偿事务Compensating Transaction](https://time.geekbang.org/column/article/4087)
- [重试设计Retry](https://time.geekbang.org/column/article/4121)
- [熔断设计Circuit Breaker](https://time.geekbang.org/column/article/4241)
- [限流设计Throttle](https://time.geekbang.org/column/article/4245)
- [降级设计degradation](https://time.geekbang.org/column/article/4252)
- [弹力设计总结](https://time.geekbang.org/column/article/4253)
- [分布式锁Distributed Lock](https://time.geekbang.org/column/article/5175)
- [配置中心Configuration Management](https://time.geekbang.org/column/article/5819)
- [边车模式Sidecar](https://time.geekbang.org/column/article/5909)
- [服务网格Service Mesh](https://time.geekbang.org/column/article/5920)
- [网关模式Gateway](https://time.geekbang.org/column/article/6086)
- [部署升级策略](https://time.geekbang.org/column/article/6283)
- [缓存Cache](https://time.geekbang.org/column/article/6282)
- [异步处理Asynchronous](https://time.geekbang.org/column/article/7036)
- [数据库扩展](https://time.geekbang.org/column/article/7045)
- [秒杀Flash Sales](https://time.geekbang.org/column/article/7047)
- [边缘计算Edge Computing](https://time.geekbang.org/column/article/7086)