增加负载的两种方式
欢迎
当一个服务在承受负载时,操作者面临一个选择。将现有的机器变得更大(增加CPU、内存、更快的磁盘)。或者添加更多的机器,每个机器都执行相同的工作。
第一个路径被称为垂直扩展(scale up)。第二个路径被称为水平扩展(scale out)。
这堂课将教你为什么几乎每个现代网络架构都选择水平扩展,以及工作负载的哪个属性使得这种选择成为可能。答案隐藏在一个词中: 状态。
到此你将理解:
- 垂直与水平扩展的成本曲线以及它们适用的场景
- '有状态' 和 '无状态' 在实际应用中的含义,以及其中一个为什么可以廉价地扩展
- 计算副本机量的数学公式,在预期负载和峰值负载下
- 保持一个层次不崩溃的头room规则,在队列弯曲点之前
- 状态必须存在的位置(它永远不会消失)以及如何将其从需要扩展的层次推出
为什么在一个阈值之后水平扩展会胜出
垂直扩展:一个更大的盒子
优点:简单。无需代码更改。无需协调。相同的过程现在有了更多的CPU。
缺点:天花板。最大的可获得的VM有限制的内存和核心。超过这个范围,钱再也买不到了。成本在供应商的产品中超线性的曲线上升。这个单独的机器发生故障,整个服务都将下线。
水平扩展:多个较小的盒子
优点:无天花板(到你愿意为之支付和协调机器为止)。容量随副本数量线性增加,可预测。一个副本的故障将移除1/N的容量,而不是100%。
缺点:需要工作负载支持。一些工作负载(一个大型数据库,一个保持活跃会话的有状态游戏服务器)抵抗水平扩展。协调和负载分配成为操作关注点。
交叉点: 任何需要在多台机器上运行以抵御单点故障的生产服务都必须在至少两台机器上运行。一旦接受了两台机器,你已经选择了水平扩展。从那时起,问题不再是‘我们应该怎么做?’而是‘我们如何以最低成本添加下一个副本?’
关键驱动力: 一个在机器本身上不持有 per-request 状态的工作负载。那么任何副本都可以回答任何请求,并且添加一个副本可以不需要协调来增加容量。
有状态 vs 无状态在实际应用中
状态从系统中消失了,它只不过移动了
有状态组件: 持有信息,信息丢失将改变行为。例如,持有用户帐户的数据库,持有会话令牌的缓存,绑定长时间流连接到特定用户的工作人员。
无状态组件: 不持有信息,信息丢失将不重要。例如,读取请求的Web层,查询数据库,写入响应。每个请求独立;该层记不住请求之间的任何内容。
关键见解: 系统中的状态从未消失,它会移动到设计用于保存它的层(数据库、Redis集群、对象存储)。面向流量的层可以成为无状态层,然后无状态层可以水平扩展,因为任何副本都可以回答任何请求。
实际测试: 如果您在这个层中随机杀死一个进程并重新启动它,用户是否会遇到错误的答案或丢失会话?如果是的,它持有状态;如果不是,它不持有状态。
示例
- 读取请求,查询Postgres,返回JSON的Python Web进程:无状态。状态存储在Postgres中。
- 将用户购物车存储在本地内存中的一台Python Web进程:有状态。杀死进程会丢失购物车。
- 持有打开连接到聊天用户的WebSocket服务器:在连接方面有状态。杀死进程会丢失连接;客户端必须重新连接。通常,这些仍然可以以适当的方式(粘性会话、一致性哈希)水平扩展。
- Redis 缓存前置 Postgres: 缓存内容为有状态,但如果容忍缓存失效是可以接受的。一个副本失败意味着缓存失效,而不是数据丢失。
为了实现水平扩展设计,就是将需要扩展的层的状态推到外部。
审计可疑层
一个团队在6个后端VM后面运行一个推荐API。该应用:从请求中读取用户ID,从Postgres中获取用户的最近活动,运行评分算法,返回一系列推荐项目。两个非标准行为:
- 应用程序在进程内存中保留一个'最近用户活动'缓存,在用户首次请求时填充,在后续请求时重复使用。
- 应用程序使用粘性会话:一旦用户访问到VM #3,随后的所有请求都将发送到VM #3(代理已配置为基于cookie进行粘性路由)。
副本公式
最简单的容量公式
一旦一个层变为无状态,就可以用算术来计算它所需的容量。你需要足够的副本,以便在稳定状态下请求以同样的速度到达和离开,并留有应对峰值的预留容量。
公式:
副本数 = ⌈ (峰值负载 × 暴增因子) / 每个副本的容量 ⌉ + 预留容量
其中:
- 峰值负载:你期望在正常运行中达到的最大持续请求/秒
- 暴增因子:覆盖短暂高峰超过峰值的乘数(通常为1.5x到2x的可预测流量,3x或更多的病毒/不可预测流量)
- 每个副本的容量:一个副本在可接受的延迟和利用率下处理的请求/秒(通常在70%CPU测量,而不是在饱和情况下)
- 预留容量:额外的副本,以便几 个副本的失败不会让整个层崩溃(对于较小的舰队通常为1-2副本,对于较大的舰队为10-20%)
工作示例:一个后端每秒处理100个请求,占用每个实例的CPU为70%。峰值负载为600个请求每秒。您期望偶尔会有2倍的洪峰。您希望在丢失2个实例的情况下-survive-不超过80%的利用率。
实例数量 = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14个实例
80%的规则
每个实例的容量不是饱和点。永远不要在100%的CPU利用率时测量容量,而要在70-80%的CPU利用率时测量。
在80%以上的利用率时,队列曲线会急剧上升:在60%利用率时运行的队列在90%利用率时运行时间为80毫秒。延迟,而不是吞吐量,会首先出现问题。(《无状态水平扩展几何》的配套课程推导了这一曲线。)
自动扩展与静态分配
静态:为峰值乘以增压头room并接受低利用率的成本,off-peak。
自动扩展:根据观察到的利用率、目标延迟或队列深度,控制器添加和删除实例。
自动扩展的注意事项:冷启动时间很重要。如果一个新的实例需要2分钟来启动,自动扩展无法响应30秒的洪峰。成熟的自动扩展在scale-up阈值以下保留一个预先分配的实例的温暖池。
为新服务-sizing一个舰队
您的团队计划推出一个视频元数据API。基准测试显示一个实例每秒处理250个请求,占用70%的CPU,99%的延迟为50毫秒。市场预测峰值负载为4000个请求每秒,在黄金时间段内。计划的促销活动可能会短暂地达到3倍的峰值。您希望在不超过80%利用率的生存实例的情况下,服务能够承受3个同时发生的实例故障。
冷启动、缓慢排放和其他现实边缘
真实的舰队有真实的边缘
公式假设实例瞬间出现,瞬间接受流量,瞬间排出流量。生产中没有这些假设成立。
冷启动: 新副本需要启动操作系统、启动进程、加载配置、预热缓存以及通过健康检查。冷启动时间从5秒(容器重启)到5分钟(全VM启动+镜像拉取)不等。自动扩展无法响应短于此延迟的突发流量。
慢排放:要从池子中移除的副本需要一定时间来完成在途请求,否则用户会看到截断的响应。反向代理支持排放(停止接受新请求,完成活动请求),但需要几秒钟到几分钟。
预热池:生产集群保持一个预先配置但处于空闲状态的副本池,随时准备接收流量。它在响应突发流量方面更快,但会在平时带来一定的稳定成本。
连接排放 vs 立即杀死:优雅关闭在这里很重要。发送排放信号的SIGTERM比发送SIGKILL更长,但不会中断用户请求。
健康检查时间窗:刚启动的副本在通过第一次健康检查之前,它的数据库连接池可能还没有预热;代理然后将实际流量发送给它,初始的几十个请求可能会很慢。要测试实际路径,而不是仅仅测试进程的存活状态,需要调整健康检查。
粘性增长:即使是 nominally 无状态的层也会随着时间的推移而产生粘性(CDN 缓存、DNS 解析器缓存、连接池)。在‘相同副本’中出现不同行为时要怀疑。
预热池还是积极自动扩展?
您的视频元数据API(与前一个问题相同,大小为51副本,用于平稳峰值和突发流量)在新上传的病毒视频上传时会出现30秒的突发流量,达到5倍正常负载。当前,自动扩展需要90秒的时间从冷启动(镜像拉取+预热)添加一个新副本。在90秒的间隙内,延迟急剧上升,一些请求超时。
在受限条件下设计无状态层
合成
您已经了解了为什么在较小的阈值之后水平扩展更胜一筹,实际中状态是什么含义,如何在预期和洪峰负载下-sizing一支船队,以及水平扩展在边缘处出现的问题。
应用所有四个。
为 feed.example.com 设计一个社交 feed API 的后端层。约束:每个副本的容量为 200 req/s,70% CPU 使用率;预期峰值负载为 1500 req/s;洪峰因素为 2.5x(偶尔会有趋势故事);能够承受两次同时副本故障;冷启动时间为 60 秒;突发负载可以持续 45 秒;预算允许一些空闲容量,但不能永久预留 2.5 倍容量。
课程下一步将去哪里
下一步课程将去哪里
您现在已经掌握了无状态层的工作思维模型:为什么它可以扩展、如何-sizing 它、什么在其边缘处会出现问题,以及状态必须在将其推出需要增长的层次时移动到哪里。
接下来的课程(cs_distsys_ingress_egress_separation)解决了一个更微妙的问题,即即使无状态层的大小和配置得当,入站和出站流量共享相同网络路径时也可能出现意想不到的问题。经典示例是代理试图连接到自己;解决方案涉及将一个层分为两个具有不同责任的层。
配对课程:geometry_of_stateless_horizontal_scaling derive 队列曲线、Little's Law 应用于副本船队、以及80%利用率弯曲的几何意义。
好样的。继续前进。