《分布式系统应用设计》读书笔记

写在前面

本书主要描述了一系列可重复的通用模式,这些模式可以使可靠的分布式系统的开发更加简单和高效。

模式的目的是提供一般建议或结构来指导设计。

第一部分 单节点模式

单节点模式下有三种模式:

  • 边车模式
  • 大使模式
  • 适配器模式

相同点都是在单节点(或单机)上增加容器的方式实现的。
区别:

  • 边车:容器作为一个功能扩展,且共享了相同的资源,如磁盘。
  • 大使:容器作为代理,可以做分发、分流等功能。
  • 适配器:容器作为一种“转接头”,目的是将信息转换成另一种统一的形式。

容器化的目标是围绕特定的资源建立边界,边界描绘了团队的所有权,旨在提供关注点的分离。

将单机上应用拆车一组容器的动机:

  • 首先是要考虑资源隔离,将不同的资源需求和优先级附加到两个不同的容器中,高优先级的容器可以提供足够的资源来确保高响应等服务性能,当有资源竞争的时候,两个容器的配置策略会确保低优先级的服务先终止,从而最大限度保证高优先级服务的资源提供。
  • 其次是团队扩展中的协作,让每个团队拥有小而专注的部分。如果正确实现,一些组建通常是可重复使用的模块,可供其他团队使用。
  • 最后,关注点的分离可以确保应用程序得到很好的理解,并且可以轻松进行测试、更新和部署。

边车模式

边车模式是由两个容器组成的单节点模式。其中一个是应用程序容器,包含应用程序的核心逻辑;另一个是边车容器,作用是增加和改进应用程序的功能(功能扩展)
边车的一个场景是适配那些不希望修改源代码的遗留程序。另一个场景是将模块化和可重用的组建封装为边车。
边车容器组通过原子容器组被调度到同一台机器上(如 k8s 的 Pod),两者之间共享许多资源,包括文件系统、主机名、网络。

使用场景

  1. 适配场景:为遗留系统增加 HTTPS 功能

如由于历史原因有一个服务未提供 HTTPS 功能,直接修改原服务需要很高的维护成本。考虑这种场景可以通过添加一个 Nginx 边车容器的方式提供 HTTPS 流量。Nginx 容器与原 Web 服务位于同一网络命名空间中,Web 服务运行在 localhost 上,Nginx 提供 Pod 外部 IP 地址上的 HTTPS 流量。

  1. 基于边车模式的动态配置

为了考虑中更好的易用性和自动化能力(如回滚等),同时保障配置更安全等功能,通过边车容器实现一个动态配置的功能。
如:两容器共享配置目录,通过API上传(推送)配置信息到边车服务,边车服务通知应用程序加载配置,边车容器还可以做配置版本记录以实现一键回滚。

  1. 监控场景:为应用程序添加 topz 接口

topz 是个实现了和命令行 top 命令一样功能的工具,可以在 github 上进行搜索,不同的语言都有实现库,通过使用边车容器的方式避免了修改原应用程序(避免侵入性)。可以部署 topz 容器使它运行在服务容器的同一 PID 命名空间中。

docker run --pid=container:${APP_ID} -p 8080:8080 brendanburns/topz:db0fa58 /server --address=0.0.0.0:8080
  1. 作为应用流程的一部分:实现源代码更新后应用的自动部署

Paas 平台记服务

边车服务通过简单的循环,不断将文件系统与 Git 仓库同步:

#!/bin/bash
while true; do
  git pull
  sleep 10
done

通过检测文件系统源代码版本的变化实现服务版本的自动更新。

边车模式模块化和可重用性设计的考虑

  1. 参数化容器。把容器视为程序代码中的一个方法,最好通过环境变量的方式实现(其次用命令行参数)。
  2. 创建容器对外 API。职责分离。
  3. 文档化容器。以便可重用并快速继承到新应用中。

大使模式

大使模式是一种代理模式,作为大使可以实现流量分发业务分片的功能。大使容器代理应用容器与外部进行交互。

使用场景

  1. 通过大使模式做服务分片

当不用大使模式时可以做分片逻辑中实现将流量转发到指定分片,但这使部署更加复杂。
通过引入大使容器,包含将请求路由到相应存储分片所需的全部逻辑,将分片服务与应用容器关注点分离。

空.jpg
  1. 使用大使模式实现服务代理

代理功能。如在跨多个环境运行可移植程序时,最主要的挑战之一是服务发现和配置。代理大使服务负责内省内部环境并代理适当的连接等。

  1. 使用大使模式做请求验证或拆分:灰度

使用大使模式可以将流量按一定规则分发到后方不同的服务版本实例中。如 10% 的流量用于实验。

适配器模式

适配器模式用于转换容器的接口,以使其符合第三方应用所需的某些预定义接口格式。
例如对于监控和运维应用程序,不同的应用容器可以提供不同的监控接口,而适配器容器适配了这种异构性并转换为一致的接口。

使用场景

  1. 使用 Prometheus 监控

Prometheus 是个监控聚合器,将收集指标聚合到一个时序 DB 中,并提供了可视化和查询语言来展示指标。Prometheus 希望每个容器都暴露一个特定的 API 从而获取指标。如使用 redis_exporter 提供 redis 的信息收集。

  1. 使用 Fluentd 格式化不同日志的格式

如果不同系统在如何将日志记录到输出流中的做法不一而又无法做侵入式修改时,我们可以考虑使用适配器模式。

Fluentd 是个开源日志代理,拥有丰富的社区支持插件。如 fluent-plugin-redis-slowlog 插件来监控 Redis 服务的慢查询日志,fluent-plugin-storm 插件手机 Apache Storm 系统的日志。

第二部分 服务模式

目录结构有些难以理解,到底哪些是模式,但也不一定按条文来定这些模式。有些问题就是在场景中需要拿出来讨论的。

单节点上调度一组容器所设计的模式是一种紧耦合的系统,并且依赖一些共享资源,例如磁盘、网络或进程间通信等。
与单节点模式相比,多节点分布式模式呈现更多的松耦合特征,同时,这也决定了组件之间的通信方式主要基于网络通信。

微服务简介

微服务主要描述了那些在不同进程中运行,由多个不同组件联合构建而成并通过预先定义 API 进行通信的系统。

微服务架构好处。
特别是可靠性敏捷性。更小的模块 => 更小的团队负责开发和维护 => 团队成员更专注按既定的目标快速迭代。
此外,微服务间用 API 使团队彼此负责。团队间提供可靠的约定 => 只需要保证约定的稳定 => 减少团队间同步依赖 => 不必关心接口内实现细节 => 提高每个团队自身的迭代和代码演进能力。
最后,微服务可帮助实现更高的扩展性。更小的模块 => 独立扩展(水平/分片/其他等) => 各个服务可实现各自的最佳扩展。

缺陷。
松散耦合。出问题后的调试成本加大。有些错误往往是在不同节点上运行大型系统所引发的。因而,微服务系统也更难于设计和构建。服务之间可能采用多种通信方式、通信模式(同步、异步、消息传递等),以及服务之间的不同协调与控制方式。

而上述挑战也正是引入分布式设计模式的重要动机。设计模式是由许多设计实践总结下来,有了设计模式的指导,会让设计变得更容易。

基于副本的负载均衡模式

目标是增加应用服务的副本来提供服务能力。手段上还需要提供负载均衡能力。性能上考虑加入缓存层。整个模式大概是这个结构:负载均衡 + 缓存层 + 多副本应用层。负载均衡需要支持探测,以控制伸缩、升级等;缓存层可扩展加入限流、SSL卸载等;

负载均衡器通常基于轮转(Round-Robin)或使用某种形式的会话黏性(Session Stickiness)分发请求。

无状态服务

无状态服务值不需要保存状态即可正常运行的服务。多个副本来提供冗余和扩展性。要想提供“高可用性”服务级别协议(SLA)的服务至少需要两个副本(单个实例在升级时一定会有服务中断的时间间隔)。典型的无状态服务包括静态的内容服务器和复杂的中间件系统(接收和聚合来自多个不同后端的响应)。

探针。
开发和部署就绪探测器用以协调负载均衡器同样重要。k8s探测器分有三种探针:启动(startupProbe,1.17 版本新增)、存活(livenessProbe)、就绪(readinessProbe)。分别表征:是否在启动过程中(避免误杀)、是否健康存活、是否准备好并可以接受流量。

有状态服务

场景:副本中有记录状态信息,最常见有内存中存有会话信息。

考虑的问题:

  • 负载分发的标识信息。通常通过对源和目标IP地址进行哈希计算并使用哈希值作为标识。但如果采用了网络地址转换(NAT )的话,则无法配合外部的 IP 进行使用。可以采用应用级别信息的跟踪,如 Cookie。
  • 一致性哈希。当副本按比例伸缩的时候,一致性哈希函数的好处很明显,一致性哈希可最大限度地减少需要映射变更的用户数,从而减少伸缩对应用的影响。

一致性哈希。
主要思想是用一个大数做模,通过取模得到许多槽位,再对槽位分组归类到节点上。参考
缺点是具有数据倾斜的问题。

缓存层及扩展

缓存可以提供极大的性能提升。所有不发生(或不频繁发生)变化的资源都可以进行缓存,尤其是少量的、频繁访问的资源。

Varnish 缓存是个高性能的开源 HTTP 加速器。(知识需要扩展:Varnish 能做什么)

扩展缓存层。

  • 速率限制。在缓存层添加拒绝服务防御很有意义。(Varnish 支持,可根据 IP 地址和请求路径以及用户是否登录来配置流量额度。)
  • SSL 卸载。SSL 卸载可提高应用服务的性能。(Varnish 不支持,但 Nginx 支持)

SSL 存在 SSL 延迟的弊端。
在 HTTPS 中,完成 TCP 握手还需要完成 SSL 握手,因此 HTTPS 比 HTTP 更耗时。
此外,SSL 中还需要额外的用于加解密处理,因此还会消耗服务器的 CPU 性能。从数据上说,性能会减少 80%,据说 TLS 1.3 有优化,但仍避免不了延迟。

完整的复制无状态服务示例

分片服务模式

在任何数据量大难以由单节点来满足服务的场景下,都应该想到分片。一般来说,复制是用来缓解请求压力的,一般用于构建无状态服务;分片除了可以缓解请求压力,更重要的是缓解存储压力(大数据量的拆分)。

分片需要考虑:

  • 与副本相结合使用,提升性能。
  • 哈希函数的选择:确定性和均匀性。输入对应的输出唯一,输出在空间分布中保持均匀。
  • 按什么进行分片(分片函数的键),考虑中分片粒度的维度与粗细:地域信息/路由信息/……
  • 一致性哈希,主要用于优化重新分片的效率
  • 分片代理,作为分片的负载分发(如 nginx 通过 hash $request_uri consistent 设置整个 uri 的一致性哈希函数)
  • 热分片系统。可根据业务情况实现分片的副本进行动态伸缩(如:A片访问流量是B片和C片的4倍,则自动让 A片用两个副本分布在两个机器上,B片、C片分布在一个机器上)

场景:

  • 缓存分片。用于扩充缓存数据量,增大服务并发量。
  • 大型多人游戏分区(业务上地域差别大时互动少而设置分片)。

分散/聚集(+副本)模式

分散/聚集模式(Scatter/Gather Pattern)类似于分片,但这里主要考量的是“计算”的分片,而不是数据的分片。这种模式主要解决的问题是并行处理问题。
一般模式是:有一个根节点和多个叶子节点(分片节点)组成,所有叶子节点计算自己所属的一部分数据,最后在根节点做整合。

该模式的使用原则:主要是为了打破在计算过程中,由于单机内存、CPU、磁盘带宽问题带来的瓶颈。

该模式引出了一个问题:整体性能由最慢的叶子节点决定(滞后问题)。另外,还会因为分片量越大导致此问题出现的概率也越大的问题。解决方案是,为每个叶子节点再增加副本同时处理。(所以整体来说是分散/聚集+副本的模式。可以解决单点故障及无中断升级。)

该模式下一些结论:

  • 由于每个节点上有开销,增加并行并不总能加快速度。
  • 由于存在滞后问题,增加并行不总能加快最终速度。
  • 用户的一个请求,在内部实际会产生大量请求(100个节点就是1:100的放大)
  • 滞后问题回影响到可用性,如果向100个节点发出请求,且每个节点的失败概率为 0.1,那么等价于几乎每个用户的请求都会发生失败。
  • 叶子分片上加入副本,可规避单点故障,提高可用性,同时可支持服务无中断升级。

场景:

  • 分布式文档搜索。每个分片上搜索最后在主节点上取并集。

函数与事件驱动处理

以上已经介绍了长时间运行的服务的设计。但还有一类应用用在只需要临时处理一个请求,或只需响应特定事件,如 FaaS。
FaaS 本质上是一种基于事件的应用程序模型。通常不适合有大量处理开销的场景,如视频编解码、压缩日志,或其他优先级低但却长时间运行的后台计算。而非常适合对时间敏感的事件处理。

FaaS(Function as a Service)被称为无服务器计算,这确实没错(因为你确实看不到服务器)。但值得区分事件驱动的FaaS 和更广泛的 无服务器计算 的概念。区别就在于有无事件驱动。
如多租户的容器协调也是无服务器的,但并不是时间驱动。
在物理机集群上运行的开源 FaaS 是事件驱动的,但不是无服务器。

无服务器计算让您可以在不考虑服务器的情况下构建并运行应用程序和服务。它消除了基础设施管理任务,例如服务器或集群配置、修补、操作系统维护和容量预置。您能够为几乎任何类型的应用程序或后端服务构建无服务器应用程序,并且运行和扩展具有高可用性的应用程序所需的所有操作都可由您负责。

FaaS 的好处:

  • 简化开发人员从代码开发到服务发布的流程。
  • 代码有良好的可管理型和自动伸缩能力。
  • 本身更加模块化和解耦,控制更精细。

挑战:

  • 通过网络的方式通信。
  • 所有状态存储在存储服务中。
  • 强制的解耦使部署服务操作更复杂,调查问题更复杂。

不适合的场景:

  • 每次服务需要向内存加载大量数据的场景

K8S 的 kebuless 是一个 FaaS 框架,可以让你在 k8s 集群中部署函数代码而不用关心基础环境。

场景:

  • 在请求处理前添加默认值(类似于服务内的中间件一样 kubeless function deploy ...
  • 基于事件的管道处理流程(如 源代码变更->编译->推送到生产环境)
  • 等等

所有权的选举

分布式所有权问题堪称可靠分布式系统设计中最复杂又最重要的部分。在分布式系统中,为了达到高可用需要应对容器失败或挂起、机器故障、平滑升级等各种原因,就需要运行多个副本服务,其中只有一个副本是系统的主副本。

许多分布式键值存储实现了这种一致性算法,这些系统提供了复制的、可靠的数据存储,提供了构建更复杂的锁服务和选举所必须的原语。如 etcd、ZooKeeper 和 consul。这些系统提供的基本原语是能够为特定键执行比较和交换操作(CAS)

从这句话中可以体会到,这些能提供锁的服务,和我个人常常在 Go 服务中实现锁的功能差不多,只是多线程变成了多进程。

锁的设计常常包括这几点:

  • a. 锁的抢占:通过 CAS 原子比较和交换来抢占锁
  • b. 锁的超时:在多进程中服务可能会挂掉而来不及主动释放锁(这点是在多线程中不同的,线程需要自己处理异常挂掉)
  • c. 锁的延时:由于 2 中超时的引入,所以获取者在存活状态下要保持锁的占有。
  • d. 锁的版本号:由于存在多进程获取锁、超时(被动)释放锁、主动释放锁三种操作的关系,多进程下需要知道那个进程获取到了锁,甚至是哪个进程在哪个 epoch 中获取的锁来避免异常场景。

详细说下 d 的问题。
比如,A 在得到锁 S 后,由于某原因失联了,S 超时然后被 B 抢占到,此时 A 再次获得了锁去释放,那么有可能会释放掉 B 的锁。所以要记录是 A 获得了锁。
但还有可能,如在这种背景: A 获得了锁后发送请求 A1 到后端服务,后端验证锁为 A1 才会处理 A1 请求。若 A 发送 A1 后还没被处理 A 失联超时丢失了锁,然后 B 获得了锁发送了 B2 请求并正确的到了处理,此时 A1 请求重新达到后端,此时由于要爆炸时间的有序性不可以被处理,所以就用到了 epoch 的概念。很简单,有个递增的数就行,看场景了。

以 etcd 为例。

如下方 help 命令解释,etcd 可通过 etcdctl set --swap-with-value unlocked my-lock alice (unlocked 为 previous value,my-lock 为 key,alice 为 value 等价于版本号)来实现 CAS 功能,然后又提供 --ttl 来实现超时。

# etcdctl set  --help
USAGE:
   etcdctl set [command options] <key> <value>
...
OPTIONS:
   --ttl value              key time-to-live in seconds (default: 0)
   --swap-with-value value  previous value
   --swap-with-index value  previous index (default: 0)

# etcdctl mk  --help
NAME:
   etcdctl mk - make a new key with a given value

USAGE:
   etcdctl mk [command options] <key> <value>

OPTIONS:
   --in-order   create in-order key under directory <key>
   --ttl value  key time-to-live in seconds (default: 0)

# etcdctl rm --help
NAME:
   etcdctl rm - remove a key or a directory

USAGE:
   etcdctl rm [command options] <key>

OPTIONS:
   --dir               removes the key if it is an empty directory or a key-value pair
   --recursive, -r     removes the key and all child keys(if it is a directory)
   --with-value value  previous value
   --with-index value  previous index (default: 0)

实践中的 etcd 分布式锁租约:

  • 上锁:etcdctl --ttl=10 mk my-lock alice 当不存在时才能创建成功,并设定超时 10s。
  • 维持锁: etcdctl --ttl=10 --swap-with-value alice my-lock alice 当对比 my-lock 为 alice 时,再此设置为 alice 并延长时间 10s
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容