The Amazon Builders' Library

62 阅读16分钟

The Amazon Builders' Library

Caching Challenges & Strategies

When do we use cache?

Hot Key/Hot Partition: Consider cache when encountering uneven request patterns that leads to hot key / partition so that one result of request can be used across multiple requests.

业务方对于一致性的要求:使用Cache难免会对数据一致性造成影响,数据源变化的频率或者缓存过期的时间共同决定了业务方数据的一致性。比如:相对稳定的数据通常可以被Cache 更长的时间。

Local Caches

Local Cache 是实现Cache最简单的方式,相比与使用外部的分布式Cache如Redis,Local Cache实现简单且减少Network overhead。实现方式通常为HashMap。

但是它的缺点在于:

  • 数据不一致:Client 向Server 集群发起两个请求可能收到不同的结果,因为两台Server上缓存的数据不一样;

  • Overload Downstream & Cold Start 问题:服务启动时缓存中没有数据,此时所有的请求可能都会穿透到DownStream服务,下游服务的压力依然正比于Service 的流量,这样会导致我们使用缓存来缓解下游压力的目标落空。SingleFlight能够解决该方法,它可以将多个并发请求绑定为一个请求,当一个请求拿到结果后,其他的并发请求复用结果即可。

监控指标:缓存命中率 和 对下游服务请求Rate

External Cache

相比于Local Cache,External Cache的好处是:

  • 没有多实例数据不一致的问题,所有实例都从同一个缓存中取数据

  • 没有Cold Start问题,因为服务重启/部署时,Cache依然在独立工作

  • 更大的存储空间,分布式缓存能够使用的空间大于单实例内存

但是使用Redis的问题是:

  • 增加了系统的复杂度和运维成本。当缓存失效的时候,仍然会造成下游服务的spike,为了提升稳定性,Amazon推荐的做法是使用** Local Cache + External Cache**。

  • Redis的水平扩容:如果能支持Consistent Hashing 是比较好的,如果不支持一致性Hashing 则需要对所有缓存项进行Rebalance。上线前应该模拟Rebalance或者一致性Hash操作,观察流量变化。

  • 安全问题:Attacker 可以利用 Response Time 推测数据是否被缓存。

Inline Cache V.S Side Cache

Inline Cache:由Cache 服务提供读写,Application 与 Storage不进行交互,由Cache Service与持久层进行交互。例子:DAX (Amazon DynamoDB Accelerator),HTTP Cache (internal Client Cache / external Nginx Cache)。

优点:提供了一个统一的API给用户,用户不用关心Cache 更新和删除逻辑

缺点:

  1. 需要协议的支持,如HTTP需要1.1 协议支持Cache

  2. 如果缓存服务Crash,则Client会认为整个DownStream Crash,但Storage可能本身没有问题 ,这会降低Availablility。

Side Cache:用户自行控制Cache 的写入和读出 (Cache Aside, Cache Around..)。

Cache Expiration

缓存需要考虑的三个要素是:缓存项的大小、过期时间 TTL、逐出策略。研发需要根据实际情况来设置这些参数,Amazon的做法是对缓存的命中率,缓存使用的空间,下游服务的请求数量进行监控,根据监控结果,调整缓存的参数。Amazon 警告我们必须规避的做法是:设置了缓存参数之后就不再回顾缓存的性能,以及验证缓存的有效率。

Soft and Hard TTL Pattern: Amazon的IAM使用的策略是设置两个过期时间,当达到Soft TTL时,Client可以对缓存项进行更新;当下游服务不可用时,可以延长过期时间到Hard TTL。

这个Pattern 还用于处理下游Backpressure 的情况:下游服务可以返回一个BackPressure事件,提醒上游应该将Soft TTL切换到Hard TTL,直到下游系统发送解除 BackPressure 事件。

Other Consideration

缓存收到下游服务的Error

当缓存收到下游服务的Error时,可以采取两种方法填充请求的缓存项目:1. 让上游系统看到Stale Data,2. 让上游系统看到Error。不论采取哪种做法,其目的是为了防止上游系统持续对下游系统产生流量,导致下游系统的情况进一步恶化。

缓存安全问题 Security

缓存中的数据通常都是序列化的数据,所以数据安全性会有一定的问题,Amazon ElasticCache 支持 in-transit / at-rest 加密。

另一种场景是attacker 可以将错误的数据注入到缓存中,导致其他的User看到的数据都是错误的。

第三种场景是,缓存的响应时间会比DB 的响应时间更短,Attacker 可以利用这个信息来判断某项数据是否处于缓存中。

Thundering Herd

当缓存冷启动或者在缓存过期的时候来了大量的相同请求,此时会对下游系统造成超出预期的压力,Request Coalescing 是解决该问题的一种方法,将多个请求合并为一个请求给到后端,其他请求复用这个请求的结果。

Nginx & Varnish 都提供的SingleFlight的操作,同时,应用层也可以实现自定义的SingleFlight。

Amazon best practise and consideration

  • 确保系统有对缓存的需求,并且评估引入缓存前后系统的 cost, latency, availability 的提升。在使用缓存之前一定要考虑引入缓存带来的收益是否值得引入缓存带来的风险。

  • 将缓存系统当做业务系统一样谨慎:设置对缓存的监控,调整缓存的参数,设置对于缓存异常的报警。

  • 关注缓存的Size,淘汰策略和TTL,设置 Test 来验证这些参数设置的合理性

  • 设想缓存失效场景下Service 应该如何运行:Cold Start, Cache Outages, Traffic Pattern 的改变,保证服务在遇到缓存问题的时候不会崩溃,包括缓存Error,返回Stale Data等。

  • 考虑缓存的安全性和隐私性、考虑DB的结构能否适应缓存结构的变化、考虑缓存在收到下游Error的时候应该如何表现。

Challenge with distributed system

**Independent Failure + Non-Deterministic **造成了分布式系统中大多数问题。

分布式系统的类别

Offline

离线分布式系统:Batch Processing, Data Analysis,Machine Learning Pipeline,离线分布式系统拥有分布式系统大多数的优点 (scalability, fault-tolerance),但避免了分布式系统的缺点 (Complexy failure modes, non-determinism)

Soft real-time

近实时的系统包括:搜索引擎,EC2的权限系统,即使系统状态不能更新到最新的状态,系统仍然能够提供服务。

Realtime

Request & Reply Mode

实时系统要求在极短的时间内给出响应,比如 AWS API,Credit Card Transaction,以及大多数的Web Service。

在 一个 Round Trip中需要经历如下步骤:

ProcessErrors
Client 向 Server 发起 RequestNetwork 超时或者丢失,Server拒绝请求
Network deliver client requestSever Crash
Server validate requestServer 拒绝服务:bit corruption, 版本不兼容,bug
Server update local statesServer本地执行错误
Server sends response to Client网卡错误
Network deliver response网络错误/超时/丢包
Client validates replyClient认为请求不合法
Client update local state本地执行错误

Making retries safe with idempotent APIs

一个复杂的操作通常都是由不同模块的功能组成的,在执行一个操作的过程中,可能会涉及多个模块,一个模块的失败可能会导致整个流程的失败。在Amazon 的经验中,最简单的方法就是最有效的方法:原地重试直到调用成功;这种方法不是空穴来风,在 Marc的文章中Timeouts, retries and backoff with jitter,他指出大多数随机的RPC调用失败都可以通过重试解决。

Reducing Client Complexity with Idempotent API Design

一个幂等的API是指不管这个API被执行多少次,它给客户端返回的结果都是一致的,幂等在分布式系统中十分有用。

如何判断请求是否重复?

Amazon 的做法是使用Client-Side的 RequestID,Server侧持久化已经处理过的RequestID,以此来去重。这里不建议对重复请求抛出异常,因为这样违反了幂等并且按照PoSD的建议来说,这个异常应该被Server消化掉。

Late Arriving Request and the life span of unique client reqeust identifiers

有点类似于CAS的ABA问题,两次重试请求的间隔出现了删除instance的操作,此时 Server 需要给出幂等的响应。

image

Reference

10 Lessons from 10 Years of Amazon Web Services

Timeouts, retries and backoff with jitter

Timeouts, retries and backoffs with jitter

In Amazon, we design our systems to tolerate and reduce the probability of failure, and avoid magnifying a small percentage of failures into a complete outage

  • Timeout:超时

  • Retry:重试

  • Backoff: 降级

大部分的failure出现在一个请求长时间没有完成,此时处理该请求的线程会长时间占用系统资源比如内存,CPU,TCP链接,如果有大量的长请求,会导致系统资源被消耗殆尽。Timeout 是一种用于解决该问题的手段。

通常,重复一次请求就能使前一个被timeout阻止的请求成功,并且重试能够防止Partial Failure和 Transient Failure 异常。但不是所有异常都能通过重试解决的,如果系统已经到达了瓶颈,重试只会加重系统的负担;而且如果系统的API不是幂等的,会产生重复的效果。

Backoff:增加请求重试的间隔,防止系统存在大量的请求连接。

系统的流量不是constant,可能存在一个时刻系统流量激增;如果错误是由于overload产生的,则无法通过重试解决。Amazon使用了Jitter (重试前加一个随机等待时间) 来防止流量激增。

Retry 的间隔需要观察下游接口的 Latency Metrics 决定,例如:P50, P99

Backoff:

  • Exponential Backoff: 重试的间隔时间指数增长。

  • Capped Exponential Backoff: 设置一个最长的Retry时间,如果超过这个时间则返回请求失败。

Retry 的问题:

  • 在一个五级的分布式系统中,如果每一层都需要重试三次,并且最下层的DB服务Fail,此时DB的workload 会增加243 倍,因为每一层的重试效果是叠加的 (3 * 3 *3 * 3* 3)。如果在最上层重试,则其下游每一层都需要进行重试,是对资源的一种浪费,所以Amazon提出的最佳实践是在链路中一个点上进行重试。

  • Load & Circuit Break: 即使系统只在调用链的某个节点上进行重试,也会出现overload的情况,限流和熔断是解决这个问题的方法。熔断指的是系统load超过一定阈值的时候就关闭下游服务。限流通常使用的算法是Token bucket,以及 Retry Throttling

  • 重试条件:下游接口保证幂等

  • 什么请求能retry:以HTTP为例,如果是客户端错误,则请求不应重试,因为不管重试多少次都是失败的;如果是服务端错误则可以尝试重试。但最终一致的系统可能会导致系统行为的变化,客户端的错误可能会因为服务端状态的更新而被接受。

In our experience, a good place to start is to remember that retries are selfish. Retries are a way for clients to assert the importance of their request and demand that the service spend more of its resources to handle it.

Reference

Introducing Retry Throttling | Amazon Web Services

Reliability: Constant work and a good cup of coffee

一个关于效率的问题:

Anti-Fragility: 一个系统如果能在压力流量下仍然保持高效的运行,那么我们认为这个系统是 Anti-Fragile 的。

image

Computers: They do exactly as you tell them

告诉计算机对于特定的输入应该给出什么样的回复,然后计算机便可以无限的重复这项工作。但是不同 scale 的系统面对的系统挑战不同,QPS 10 的系统与 QPS 10K 的系统在量级上不同,即使最简单的 CRUD 放大 1000 倍,也是一个很困难的问题,为什么会这样?

When we're operating highly reliable systems, variability is our biggest challenge.

流量的突然增长会导致请求排队,每个Client 收到 Response 的时间变长,如果出现了 timeout,那么系统的 workload 会进一步增大,导致系统变得更慢。现实中遇到的 variability 的例子是每个节点的工作状态有可能出现偏差,wallet billing 的 ES 集群因为没有 Replica backup, 一个 node crash 导致整个集群因为消费阻塞而无法写入(没有很好的故障隔离机制)。

所以为了应对流量突然增长,大多数服务系统都是按照 Peak QPS进行 capacity planning的,字节内部的 TCE 服务也都是这样部署的,这样的系统有三个特点

This is why most reliable systems use very simple and dumb but reliable constant work patterns.

  • 不会因为流量的增加/减少出现 scale out / scale in 的情况

  • 不存在 mode,所有的 operation 都是一致的

  • 如果系统出现了 variation,当有stress traffic 到来的时候,系统会处理更少的请求,保证正常请求的质量

Cache 是有 mode 的:当 cache 是 empty 的时候,请求延迟更高,缓存雪崩的时候,可能会带崩下游的系统。

Cache appears to be anti-fragile at first but most amplify fragility when over-stressed.

Amazon Route 53 health checks and healthiness

Amazon Route 53 Health check 服务于服务发现、DNS、RDS (Proxy & Data Node) HA。Route 53 (应该是个 side car 否则这样会影响业务的进程) 有一组 Health checker 分布于不同的 AZ,每次检查周期,所有的 Route53 node 都会向 target 发送心跳检测请求,并且将结果发送到一个aggregator 上进行聚合.

Health checks can be subject to noise.

Route 53 的设计是符合 constant work 的,并且用户可以决定使用多少个 checker 的结果来判定节点的健康状态

  • 如果一个用户定义了 10 个 checker,而整个 fleet 中有 10000 个 checker,那么心跳检测仍然会发送 10000 次,但是只有其中 10 个结果会发送给 aggregator。因此不存在 traffic variation 的情况。

  • 另外如果 checker 本身因为机房事故宕机了,那么每个 target 收到的心跳检测更少,相当于更高效的处理了请求。并且受事故影响的机房也会被其他 AZ 的 checker 检测到。

Amazon Simple Storage Service (S3) as a configuration loop

当用户需要对 EC2 集群的配置更新,AWS 的工作流有两种方式:

  1. 将用户的更新封装为一个 Event,由 hyperplane 消费 event 然后更新到集群的 TLB 中。
  • 如果出现了流量高峰,整体的延迟会变高,如果出现timeout会引起Client retry
  1. 将用户的更新封装为 event,上传到 S3,hyperplane 异步的从 S3 中下载 config,并且更新到 TLB 中。
  • workload 是固定的,每次 hyperplane 只会从 S3 下载固定数量的配置,并且更新到 TLB 中,不存在 traffic variation

  • 可能会造成资源的浪费,但是这个资源的浪费几乎是可以忽略的,系统发送一个请求和发送 1k 个请求的资源消耗是差不多的。

  • 拿咖啡店的例子来说:相比每次为 Client 新做一杯咖啡,即使最后我卖不完咖啡机里所有的咖啡,浪费了咖啡的成本和维护咖啡热量的成本,这个沉默成本是可以接受的。

Constant work pattern doesn't need separate retries and state machines, it can even save energy in comparison to a design that uses a workflow.

The value of simple design: easy to understand, use and operate

A good design has to handle many stresses and faults, and over enough time "survival of the fittest" tends to eliminate designs that have too many or too few moving parts or are not practical.

Using load shedding to avoid overload

Load shedding mechanism

It's important to understand many predictable conditions that lead to service brownout.

  • Unexpected surges in traffic

  • Sudden loss of fleet capacity

  • Clients shift from cheap request to expensive requests (cache missing, or writes)

Understanding the cost of dropping requests

Load shedding的使用场景假设了drop request 的成本是很低的,在某些场景下 drop reqeust 会比 hold request 的成本更高,这时候需要让server继续处理请求。

  • Recharge:充值服务(或者其他涉及营收的场景)如果 drop request,那么就会对公司的收益造成损失;

  • Advertisement:广告服务,如果drop request,成本明显会比recharge服务低。

Prioritize Request

当server overload的时候,我们需要考虑处理什么请求以及将什么请求丢弃,对于server来说,来自TLB的ping请求是最重要的,如果server无法及时响应TLB的ping请求,那么则会被标记为 bad 节点。

在 amazon.com 这个复杂的 service 架构中,如果服务之间的prior heuristic出现了冲突,整体的可用性也会受影响。

Keeping an eye on the clock

如果server 处理了很长时间的请求,Client侧timeout了决定不再等待请求了,如果server不能及时的停止工作,则会造成资源的浪费。从server的视角看,它成功deliever了请求,但是从Client的视角看,server返回的是error。

☕️咖啡店的例子:

如果咖啡师用了很长的时间制作了一杯咖啡,导致用户不耐烦并且不想等待了,那么最终做出来的这杯咖啡也是浪费的,不如在Client取消请求的时候,及时停止工作,及时止损。

The late reply is like a tree falling in the forest

Client可以将timeout传给server,server根据这个timeout决定是否处理请求。在TikTok 服务中,即使 mesh proxy 给 Client返回了timeout,server端的请求仍会继续处理,导致资源的浪费。

Client 如何将timeout发送给server?

  • Absolute timeout:传递绝对时间

  • Duration timeout:传递duration

**Absolute timeout **

绝对时间方案的问题在于分布式系统中可能会出现时钟漂移,导致设置的Client timeout并不是用户期望的timeout,Amazon Time Sync Server 通过每个AWS region 维护一组 a fleet of redundant satellite-controlled + atomic clocks 来同步EC2 的时间。

Duration timeout

Servers are good measure elapsed durations locally because they do not need to gain consensus with other servers.

但是在单机上使用elapse time 也会有问题:

  • 它需要Server保证时钟的单调性,即不会回退。Server <> NTP server交互的过程中可能会更新本地的时钟

  • 另一个问题是在极端的情况下,请求可能会卡在TCP buffer,应用层的Server不知道何时开始计算 countdown。

即使Server能够感知timeout,但是如果handle client timeout 也是一个值得考虑的问题:

Things are rarely that simple

  • Cache 命中会比cache 失效快很多,server 预先无法感知cache是否命中或者失效。如果使用 Golang 中的 ctx.Done 很有可能导致 cache aside 缓存失效,由于timeout导致回源失败。

  • Hot Partition Issue: 分片数据库可能存在hotspot问题,Server预先也无法知道 client请求会被route到哪个partition上

Watch out for queues

Many modern service architectures use in-memory queues to connect thread pools to process requests during various stages of work.

TCP based service, the operating system maintains a buffer for each socket.

Load balance 通常会有一个surge queue 当链接数超过了服务capacity,过量的请求会先放入 surge queue中。这个queue可能会导致service brownout因为当service拿到请求的时候不知道它在队列里排了多久。一种更安全的方式是使用 spillover: fast failed rather than queueing excess request.

Protecting against overload in lower layers

Think about overload differently