《Release It! Second Edition》阅读笔记(一)

前言

本文是 《Release It! Second Edition》 这本书的流水账式阅读笔记。

Living in Production

软件只有在线上(上线)才能体现它的价值。上线之前的开发、测试、计划等等都是上线前的前奏。这本书讲的是围绕上线以及线上稳定性的各方面的知识。

Create Stability

Case Study: The Exception That Grounded an Airline

举了一个航空公司的系统故障的例子,在这个例子中因为一个未捕获的 SQLException 异常导致整个航空系统瘫痪了数小时。

讲了一下用到的方法:

  • 通过 jvm 的 thread dumps 找到各个服务 block 的地方
  • 通过反编译的方式拿到了关键问题的 java 代码,从而最终确定了 bug 是哪段代码导致的

bug 总是不可避免的,谁也不能保证再也不会出现 bug。这个例子中最糟糕的问题不是这个 bug 本身,而是这个 bug 瘫痪了整个系统: 一个系统中的 bug 所产生的影响传递到了所有相关的系统。我们要解决的问题是:如何阻止一个系统中的 bug 影响其他所有的系统。 后面的章节会讲如何组织这类问题的一些设计模式。

Stabilize Your System

Extending Your Life Span

测试可以发现很多问题,一切没有通过测试发现的问题最终都将会发生:如果你没有测试一下在七天后会发生的内存泄露问题,那么七天后就会发生内存泄露。

问题是,大部分测试运行的时间都太短了,测试环境、开发环境的服务都会频繁的重启,并没有像生产环境一样一个服务持续运行很长一段时间, 这样就会导致很多问题无法在测试和开发的过程中发现,直到到了生产环境才在某天触发。

如何应对这类需要长时间运行才能发现的问题,并在生产环境之前提前发现这类问题?你需要准备一个类似的长时间运行的环境,如果条件不允许的话, 简单的一台机器上运行长时间的服务,然后负载测试的时候不要只跑一会儿,试试持续跑几个小时甚至几天。

Failure Modes

为你的系统设计故障方法,你可以设计一些安全的故障方法,在出现故障的时候保护系统的其他部分不受故障的影响。

Stopping Crack Propagation

不要让故障持续传递

Chain of Failure

故障链上的每一个步故障可能都会导致后面系统的故障,导致出现雪崩效应,出现一个小 bug 导致链路上的所有系统都故障的问题。

一种为每个可能故障的做准备的方法是,检查所有外部调用、所有 I/O 调用、所有资源使用以及所有预期的结果,自问一下:『所有的这些操作可能会出现哪些异常情况?』:

  • 无法完成初始化连接会咋样?
  • 如果建立一个连接需要 10 分钟会咋样?
  • 连接建立了然后又被断开了呢?
  • 可以建立连接但是无法获取的响应信息?
  • 如果我的查询需要 2 分钟才能获得响应呢?
  • 当尝试写一个关键的错误日志(比如前面的 SQLException)的时候磁盘满了呢?

每一次线上故障都是唯一的,但是我们可以从这些故障中找到一些故障模型,通过分析这些故障模型就可以找到一些通用的解决办法。

Stability Antipatterns

本章讲一些可能会摧毁你的系统的反模式。

Integration Points

现在大部分系统的结构已经从之前的单体架构变成了集成多个服务的类似网状的结构,网状结构中的各个依赖链路是摧毁你系统的一个关键点, 下面看看这些依赖链路是怎么在各种场景下成为毁灭者的,以及你可以如何应对。

Socket-Based Protocols

大多数高级协议都是基于 socket 实现的。虽然这些高级协议都有自己的故障模型,但是它们都会受 socket 层故障的影响。

  • connection refused 是最容易处理、一般也不会被忽略的一种异常情况

  • TCP 三次握手(SYN、SYN/ACK、ACK)期间的异常情况: * 如果远程服务没有监听指定的端口,客户端会收到 RST 报,这个过程的响应特别快,调用方会得到一个异常/错误返回码 * 远程服务监听了指定端口,监控端口的同时会有一个 listen queue(在队列内的连接处于发送了 SYN,但是还没收到 SYN/ACK 回复的 pending 状态):

    • 队列满了:新进来的连接会被拒绝(不放到 listen queue 中,不回复 SYN/ACK),客户端会反复重传 SYN 包
    • 队列没满,但是没有 accept 回复 SYN/ACK 的话,客户端会反复重传 SYN 包
    • open() 调用在三次握手没完成的时候都会阻塞在那里,直到连接成功或超时(connection timeout)。 连接超时的阻塞时间一般都是分钟级别的,甚至十几分钟。
  • 客户端连接成功了并且发送了一个请求,但是服务端可能会花很长时间才会完成读取请求并返回一个响应的操作。 在服务端返回响应前, read() 调用会一直阻塞在那里,通常这个阻塞是没有时间限制的,如果你想唤醒这个阻塞调用的话,必须得设置 socket 超时时间。

网络故障通常有两种方式:快速或者缓慢。类似 connection refused 的快速故障调用者可以在几毫秒内就获知这个结果, 类似 ACK 包被丢弃的缓慢故障会导致分钟级别的线程阻塞问题,阻塞的线程无法处理其他事物,如果所有线程都阻塞的话,可能会导致整个系统宕机。

所以,缓慢的响应比没有响应(快速得到结果)更糟。

The 5 A.M. Problem

可以通过抓包工具(tcpdump、Wireshark)来调查 socket 相关的问题。

作者讲了一个因为链路中的一个防火墙频繁丢弃了 ACK 包导致服务 hang 住最终导致整条链路都挂了的案例 (有个连接池,连接池中每个空闲连接的超时时间是一个小时,因为丢包问题导致无法及时剔除有问题的连接,然后使用连接的时候用了有问题的连接出现服务 block)。

一般需要二十分钟 socket 库才能感知到重传导致的超时问题(tcp_retries2),上面案例中他们用的系统需要三十分钟才能感知到超时, 也就是说他们服务中的 write socket 操作会出现长达三十分钟的 block 问题,read socket 操作会 block forever!

这个例子想说的是,有时候故障不一定是应用层面导致的问题,有时可能是上层或下层导致的问题,你需要知道如何深挖至少两层来找到真正的问题所在。

HTTP Protocols

HTTP 协议也是基于 socket 的,所以它有上面提到 socket 相关的所有问题,同时它也有自己特殊的问题:

  • 服务端可能接受了 TCP 连接,但是不响应 HTTP 请求
  • 服务端可能接受连接,但是不读取请求。如果请求的 body 特别大的话,可能会超出服务的的 TCP 窗口大小,这会导致 调用方的 TCP buffer 被填满,这将导致 write socket 操作 block 住。这种情况下正在发送的请求永远都不会完成
  • 服务端可能会返回一个调用方无法处理的响应,比如 "418 I’m a teapot." 或者 "451 Resource censored."
  • 服务端可能会返回一个调用方非预期的或者无法处理的内容类型,比如,返回一个 404 的 Html 页面而不是预期的 JSON 响应。(比如,有些 ISP 在 dns 查询失败的时候会返回一个 html 的错误页面)
  • 服务端声明会返回一个 JSON 数据,但是实际上是个普通的纯文本数据或者二进制数据

推荐使用那种可以详细控制超时(包括连接超时和读取超时)以及如何处理响应的客户端库,不要用那种直接把响应映射为对象的库,应该在确认获取到了预期的数据后再把响应作为合法数据进行处理。

Vendor API Libraries

供应商提供的 API 库通常意味着:代码质量差、bug 多、bug 修复周期长、有些甚至看不到源代码无法自助修复 bug。

供应商 API 库主要的稳定性杀手是其中的 block 操作:内部连接池/资源池、socket read 调用、HTTP 连接或者是不安全的序列化操作。 供应商 API 库布满了大量不安全的代码实践。

Countering Integration Point Problems

如何防范集成链路中各个依赖的问题,下面有一些建议:

  • 警惕必然会出现的问题:链路上的每个依赖必然会出现某种故障,你需要为这些故障做预案和准备。
  • 为多种形式的故障准备预案。
  • 故障会快速传递:远程系统中的故障会快速成为你的系统中的问题,如果你的代码防御性不够的话,这个问题通常会演变成瀑布式故障(故障风暴)。
  • 应用一些模式来抵御链路中的问题:通过熔断(Circuit Breaker)、超时( Timeouts)、解耦中间件 (Decoupling Middleware)、握手(Handshaking)这些防御式编程方法来帮助你抵御链路上的危险问题。

Chain Reactions

目前的架构风格一般分为水平扩展和垂直扩展,然后大部分服务都是水平扩展架构的。

水平扩展的模式一下,一般都有类似负载均衡或集群的模式。 水平扩展虽然不容易受单点故障问题的影响,但是仍旧有负载相关的失败模式。 比如,有一个节点因为内存泄露导致服务异常了,此时这个节点上的流量通过负载均衡器会分摊到剩下的健康节点上, 但是剩下的节点可能也会因为这些新的流量也导致出现异常(因为这些节点也有内存泄露问题,只是暂时还未达到临界点)。 解决这种连锁反(chain reaction)应的唯一办法是修复导致问题的 bug。

把一层分隔为多个池的舱壁模式(Bulkhead)可以减轻连锁反应说带来的影响。

连锁反应有时是因为一些阻塞的线程而导致的问题,比如一个节点中所有处理请求的线程都阻塞了,这会导致服务无法响应请求, 新进来的连接会分散到同一层的其他节点上,加大其他节点出现问题的几率。

Remember This:

  • 意识到,有时一个服务宕机会危及剩下的服务,导致它们也出现宕机的问题。连锁反应可能会导致整个层的服务都宕机, 其他依赖这层的服务必须保护自己,否则它们可能会因为失败风暴导致宕机。
  • 追杀资源泄露问题。大部分连锁反应都是内存泄露导致的问题。
  • 追杀令人费解的时间相关的 bug。如果一个节点出现死锁问题,很有可能会导致其他节点也出现死锁问题。
  • 使用自动扩容功能。随着时间的推移,自动扩容的速度将超过连锁反应的传递速度,最终终止连锁反应。
  • 使用舱壁(Bulkheads)模式进行防御:在服务端使用舱壁模式阻止连锁反应危及整个服务, 在客户端使用熔断器(Circuit Breaker)模式来处理一部分服务宕机的情况。

Cascading Failures

瀑布式故障,故障雪崩,指的是某一层的服务的故障导致调用方那一层也出现故障。

瀑布式故障通常是低层次的故障导致资源池异常的结果,比如请求依赖的外部服务时没有设置超时。

下游服务的故障会导致上游服务触发重试逻辑,然后随着下游服务故障的加重最后触发了重试风暴,最终打挂下游服务。 针对这种重试风暴的情况,调用方需要应用熔断器(Circuit Breaker)模式。

最高效的对付瀑布式故障的模式是熔断器(Circuit Breaker)和超时。

Remember This:

  • 确保你的服务在外部依赖异常时不会受牵连导致宕机。
  • 仔细检查各种资源池。资源池异常通常会导致瀑布式故障。比如,长时间无法获取到需要的资源的连接池,获取连接的线程会一直阻塞,其他等待连接的线程也会阻塞。 安全的资源池应该限制一个线程获取资源的等待时间。
  • 通过超时和熔断器(Circuit Breaker)模式来防御瀑布式故障问题。

Users

用户太少是个问题,用户太多其实也会成为问题。比如未准备好的促销活动或者网红转发导致的用户量暴增,这些个的用户增长可能都会打垮服务。

Traffic

流量同样也有类似的问题,一般服务都有自己的预估容量,如果超出了可支撑的容量就会导致问题。 如果你的服务是跑在云上的话,云服务的自动扩容功能是好手段,不过有可能会出现因为自动扩容导致巨额账单的问题。

Heap Memory

基于内存的 session 中应该保存尽可能少的数据。可以通过各个语言提供的弱引用(weak references)相关的技术来保证及时释放内存。

Off-Heap Memory, Off-Host Memory

善用把数据存放到外部进程的方式来代替进程内的用户数据内存,比如 Memcached、Redis

Sockets

一个服务器所能处理的接入连接数是有限制的,主要是端口数限制,解决的办法是虚拟 ip 地址,服务通过监听网卡上的多个虚拟 ip 的方式来突破端口数量限制。

Closed Sockets

关闭 socket 有时也可能会触发问题。比如记得留意一下 TIME_WAIT 状态的连接。

Expensive to Serve

有些用户可能比其他用户更难服务,比如大部分用户可能只是浏览一下商品页面,这些页面的内容通常可以使用缓存, 还有一部分用户会下单、付款,这些功能对服务的要求更高,可能会触发一些问题。

对于这类问题的发现方法是压力测试,比如你预期的转化率是 2% 那你在做压力测试的时候就可以考虑测试 4%、6%、10% 的转化率。

Unwanted Users

不是所有的用户/请求都是服务想要的,比如不恰当的客户端导致的 DDoS 攻击、不遵守规范的网络爬虫等等。 你的服务需要考虑如何应对这些不想要的用户,让他们处于你们的控制下否则害虫就会大量滋生。

Malicious Users

同样也不乏怀有恶意的用户,比如各种脚本小子、攻击者等等,比如最常见的 DDoS 攻击、各种安全漏洞攻击之类的,需要对这些恶意用户保持警惕。

Remember This

  • 用户会消耗内存:每个用户 session 都需要一点内存,最小化内存使用可以提高服务的容量。
  • 用户会做古怪的、随机的事情:真实世界的用户会做你无法预测的事情。测试脚本对这种情况可能不是特别有用,可以考虑看看fuzzing toolkits, property- based testing, or simulation testing。
  • 恶意用户无处不在:确保你的系统可以方便的进行各类漏洞的修补工作,及时更新使用的框架、持续学习。
  • 用户有时会像暴徒一样涌进来,比如有网红推荐了你的网站,大量涌入的用户可能会触发 hang 住、死锁等问题,需要对这些情况有所预案,比如对热链进行压力测试等。

Blocked Threads

很多时候服务故障的时候并不会崩溃而是所有线程都阻塞在那等待着一些不能完成的事情。

所以建议不只是要有内部监控(日志抓取、进程监控、端口监控等)也可以有个外部监控,比如一个模拟真实用户操作的客户端,如果这个客户端无法得到预期的结果的话,肯定是哪里有问题了,即便服务还在运行。

metric 监控同样也可以帮助快速发现问题,比如登录成功数、支付失败数等计数类 metric 监控。

线程阻塞可能发生在任何时候:从连接池中获取一个连接、处理缓存或对象登记、或者是进行一次外部调用。

不安全的多线程代码通常有四种情况:

  • 错误情况和异常的组合数太多了,导致测试无法完全覆盖
  • 非预期的交互会把问题引入到前面安全的代码中
  • 时间非常重要,应用可能会同时 hang 住多个并发请求
  • 开发者从来没测试过应用处理 1 万个并发请求的情况

在开发环境很难发现 hang 住的情况。不要总想着自己实现连接池,实现一个可靠、安全、高性能的连接池比你想象的要更困难。

尽量不要在同步方法里修改共享对象,建议是把这些共享对象实现为不可变的或者在同步方法里使用一个原对象的副本。

Spot the Blocking

Use Caching, Carefully:

  • 所有应用级别的缓存都需要配置最大内存使用
  • 需要监控缓存命中率
  • 不要缓存没意义或者可以很快生成不需要放入缓存的数据
  • 缓存数据自身应该使用弱引用(weak reference)来帮助 gc 释放内存
  • 缓存要适时更新或过期,不要出现旧数据导致出问题的情况

Libraries

第三方库是万恶之源,大部分阻塞的线程都来自第三方库。很多作为一个服务的客户端的库都有一个内置的资源池,它们通常会在出问题时永久阻塞发送请求的线程。 以及它们通常不能配置失败模型,比如如何处理所有连接都在等待那些永远都不会返回的响应的情况。

如果是个开源库,你还可以通过各种方法快速的修复相关问题,如果是供应商提供的库的话,你可能需要自己包装一下这个库,使用自己可控的方式来处理请求。

Remember This:

  • 消除阻塞线程相关的反模式可以解决大部分的故障问题
  • 仔细检查资源池
  • 使用久经考验的原语。任何并发相关的库都比你现造的生产者/消费者队列的轮子要经过更多的测试。
  • 使用超时机制进行防御
  • 小心那些没法看到源码的库

Self-Denial Attacks

市场部门的营销活动可能会导致自我拒绝的攻击,一些全局共享的资源也会导致类似的问题,比如分布式全局锁服务。

Avoiding Self-Denial

可以通过构建一个 shared-nothing 的架构(服务间不共享任何资源,不依赖其他服务的正常运行)来阻止机器相关的自我拒绝问题。 如果没法做到的话,可以考虑实现 fall back 机制,当一个依赖的服务异常时使用另一个服务或机制来实现类似的功能。

也可以搭配硬件负载均衡器来进行流量管理、基础设施分区、使用新的云服务资源来处理营销活动或流量陡增的场景。

自动扩容也是一个方法,不过要考虑到扩容的速度问题,如果都是虚拟机的话扩容可能会比较慢,可以考虑使用预扩容的方式来应对即将到来的营销活动。

对于人为导致的攻击问题,解决方法是:培训、教育、交流。

Scaling Effects

任何时候你有一个多对一或多对少关系,当一端增长时候,你都可能会遇到尺度效应(scaling effects)的问题。

比如,一个数据库服务在只有 10 台机器作为调用方的时候可以正常工作,但是当你又新加了 50 台机器的时候,这个数据服务就崩溃了。

开发环境和测试环境中因为使用的机器数量太少,基本上很难发现尺度效应相关的问题。

Point-to-Point Communications

尺度效应问题的一个多发地是点对点通信的场景:服务内部实例之间需要点对点的互相通信。 当需要点对点通信的节点数量增加到数以千计的时候基本上都会成为很大问题, 然而除非你们是 google 或微软否则没法在测试环境搭建与生产等量的机器信息,所以测试一般都覆盖不了这种 case。

如果机器数量比较少点对点通信可能没啥大问题,随着机器数量的增加,可以考虑使用下面的方式替换点对点通信:

  • UDP 广播
  • TCP 或 UDP 多播
  • 发布/订阅消息
  • 消息队列

Shared Resources

另一个危及稳定性的尺度效应问题是共享资源(Shared Resources)问题。 当这种共享资源过载的时候,它就会成为其他依赖这个共享资源的服务的容量瓶颈。

Remember This

  • 检查生产环境和 QA 环境的区别,找到上了生产环境后可能会因为尺度效应导致问题的风险点。
  • 小心点对点通信,点对点通信的方式会导致非常的难以扩容。
  • 小心共享资源。如果一定要依赖一些共享资源的话,记得做压力测试,以及要确保它们的客户端在共享资源变慢或者锁住的时候仍旧能够工作。

Unbalanced Capacities

链路中各个系统的容量不一样,尤其是前端部分一般容量都比后端部分大,在突然出现的流量高峰的时候容量不够的部分就会成为瓶颈, 但是让所有系统的容量都预留的特别足只为了应对某一天的突发流量也不是特别现实。

但是也还是要应对偶然的突发流量的,调用方可以在下游异常时应用熔断器模式来减轻下游服务的压力, 服务提供方可以使用 Handshaking 和 Backpressure 来告诉调用方请求限流了,同时也可以考虑使用 Bulkheads 模式来为高优先级的调用方保留容量。

Drive Out Through Testing

不均衡的容量是另一个无法在 QA 环境发现的稳定性问题。

Remember This

  • 检查服务器和线程数量,防止在生产环境出现容量不均衡导致的问题,这个情况一般在测试环境中发现不了,因为测试环境中一把就一两台服务器。
  • 留意尺度效应和突增用户。
  • 虚拟化 QA 环境,扩容 QA 环境。尝试测试调用方和被调方非等比扩容下的 case 。
  • 压力测试接口的两端。既要测试后端服务(突然涌入10倍的流量会咋样?)也要测试前端流量(如果后端异常了会咋样?)。

Dogpile

当一群服务同时施加这种瞬时负载的时候,这种情况就叫做叠罗汉(Dogpile)。 dogpile 可能发生的场景:

  • 多个服务启动的时候,比如代码升级或服务重启
  • 在半夜触发的定时任务(或者每小时触发)
  • 配置管理系统推送一个变更

有些配置管理工具允许配置一个随机因子(这样推送事件就不会集中在某一点)来打散短时间内的 dogpile 问题。

dogpile 也可能会发生在因为外部因素导致出现了同步触发的流量。 需要小心那些多个线程等待一个线程完成的情况,当那个线程完成的时候,那些等待的线程就会对下游服务产生 dogpile 现象。

Remember This

  • Dogpile 需要你花费更多的容量来处理它产生的波峰问题。
  • 使用随机时钟来打散波峰,不要把所有定时任务都放在半夜执行或者每小时执行,混合使用来打散负载。
  • 通过增加 backoff 次数来规避脉冲现象。固定周期的重试间隔会产生周期性的脉冲,应该使用 backoff 算法这样不同的调用者会因为不同的周期而产生不同的流量点。

Force Multiplier

自动化赋予了管理员以少量工作完成大规模迁移的能力,这个就是 Force Multiplier

Outage Amplification

自动化系统或策略所产生的结果可能不一定是符合预期的结果,比如 reddit 有次故障就是跟自动化系统有关:

http://www.reddit.com/r/announcements/comments/4y0m56/why_reddit_was_down_on_aug_11

Controls and Safeguards

我们可以在我们的控制面板软件中实现一些保护措施(safeguards)来预防自动化可能产生的非预期结果:

  • 如果观察者报告有 80% 以上的系统不可用了,这个情况很有可能是观察者有问题而不是系统出问题了。
  • 应用迟滞现象(Hysteresis),快速启动机器,但是关闭的时候要慢一点。启动一台新的机器要比下掉一台旧机器更安全。
  • 当预期的状态跟观察到的状态有很大差异的时候,需要增加确认信号。
  • 消费资源的系统需要有足够的状态以便在他们尝试启动无限的实例的时候检测到这一异常情况。
  • 建立减速带来控制动力。假设你的控制面板每秒检测一次过量的负载,但启动一个虚拟机来处理负载需要五分钟。 确保它不会因为持续存在的负载而启动 300 台虚拟机。

Remember This

  • 在做大破坏的前寻求帮助。基础设施管理工具可以在很短的时间内产生大量的破坏力,为它们构建限制器和保护措施这样它们就不会一次性把整个系统都摧毁。
  • 小心滞后的时间和动量。自动化启动的操作需要时间。 这个时间通常比监控时间间隔更长,因此要确保系统对动作的响应有一定的延迟(不要重复触发大量多余的自动化动作,比如前面提到的扩容问题)。
  • 提防错觉和迷信。控制系统可以感知环境,但是它们也可能被愚弄。 它们会计算出一个预期的状态和一个当前状态的『信念』,这两个任何一个都可能出错。

Slow Responses

通常一个慢响应要比一个拒绝连接或返回错误要更糟糕。

Remember This

  • 慢响应会触发级联故障。
  • 对于网站来说,慢响应会导致更多的流量。
  • 考虑快速失败。
  • 查找内存泄露和资源竞争。

Unbounded Result Sets

设计的时候要对外部系统持怀疑态度,时常考虑:系统 X 的啥操作会影响我系统的稳定性。

无限的结果集在某天可能会对你的系统找出特别大的伤害, 比如一个查询操作,因为没有限制返回的结果,平时数据量少看不出影响,突然有一天返回了上百万行的数据,此时可能就好影响你的系统的稳定性。

Black Monday

Remember This

  • 使用合理的数据容量
  • 分页
  • 不要依赖数据提供方。不要期望数据提供方会按照预期的提供有限的结果集,万一有一天他们把整个表的数据都返回给你了呢?
  • 在应用级别的协议中增加限制。服务调用,RMI, DCOM, XML-RPC 以及其他类型的请求/响应调用都非常的脆弱,非常容易然后大量的数据导致占用太多的内存。

Wrapping Up


Comments