此文是依据赵磊在【QCON高可用架构群】中的分享内容整理而成。转载请事先联系赵磊及相关编辑。

赵磊,Uber高级project师,08年上海交通大学毕业。曾就职于微软。后添加Facebook主要负责Messenger的后端消息服务。这个系统在当时支持Facebook全球5亿人同一时候在线。眼下在Uber负责消息系统的构建并推进核心服务在高可用性方向的发展。

前言

赵磊在7月21号的全球架构师峰会深圳站上,做了主题演讲:Uber高可用消息系统构建,对于这个热门主题,高可用架构群展开了热议,大家对分布式系统中的各种错误处理非常感兴趣。Tim Yang特邀赵磊通过微信群,在大洋彼岸的硅谷给大家进一步分享。

分布式系统单点故障怎么办



【亲述】Uber容错设计与多机房容灾方案 - 高可用架构系列_数据中心

non-sharded, stateless 类型服务非常easy解决单点故障。 通常load balancer能够依照固定的时间间隔,去health check每一个node, 当某一个node出现问题时,load balancer能够把故障的node从pool中排除。

非常多服务的health check设计成简单的TCP connect, 或者用HTTP GET的方式,去ping一个特定的endpoint。当业务逻辑比較复杂时,可能业务endpoint故障,可是health endpoint还能正常返回,导致load balancer无法发现单点故障,这样的情况能够考虑在health check endpoint中添加简单的业务逻辑推断。

对于短时间的network故障。可能会导致这段时间非常多RPC call failures。 在RPC client端一般会实现backoff retry。 failure可能有几种原因:

  1. TCP connect fail,这样的情况下retry不会影响业务逻辑,由于Handler还没有运行。
  2. receive timeout, client无法确定handler是不是已经收到了request 并且处理了request,假设handler反复运行会产生side effect,比方database write或者訪问其它的service。 client retry可能会影响业务逻辑。

对于sharded service。关键是怎样找到故障点。并且将更新的membership同步到全部的nodes。

以下讨论几种sharding的方案:

  1. 将key space hash到非常多个小的shard space, 比方4K个shards。 通过zookeeper (distributed mutex) 选出一个master。来将shard分配到node上,并且health check每一个node。
    当遇到单点故障时,将已经assigned的shards转移到其它的nodes上。 由于全局仅仅有一个single master, 从而保证了shard map的全局一致。当master故障时,其它的backup node会获得lock成为Master。
  2. Consistent hashing方式。consistent hashing 通经常使用来实现cache cluster,不保证一致性。 由于每一个client会独立health check每一个node, 同一时候更新局部的membership。
    在network partition的情况或者某一个node不停的重新启动, 非常可能不同的client上的membership不一致,从而将相同的key写在了不同的node上。 当一致性的需求提高时。须要collaborative health check, 即每一个node要monitor全部其它node的health。
    Uber在这里使用的是gossip protocol。node之间交换health check的信息。

大面积故障怎么办



【亲述】Uber容错设计与多机房容灾方案 - 高可用架构系列_mysql_02

大面积故障时。比方交换机故障(rack switch failure)。可用的机器不足以处理全部的请求。 我们尽可能做的就是用50%的capacity 处理50%的请求或者50%用户的全部请求。而尽量避免整个服务故障。 当设计一个服务的时候,它的throughput应该是可linear scale的。

  1. 在相同的CPU占用情况下,1个机器应该处理100个请求,那么5个机器应该能够处理500个请求。
  2. 并且在相同的机器数量下,20%的CPU能够处理200个请求,那么60%的CPU应该能够处理3倍即600个请求。

后者是非常难实现的。并且当CPU越高的时候,服务的throughput并非线性的。

通常在80%CPU以上的情况,throughput会下降非常快。

随着CPU使用添加,request的latency也会提高。

这对上下游的服务可能都是一个挑战,可能会导致cascade failure。

对于nodejs或者java nio一类的async IO框架来说,另外一个问题就是event loop lag。 这两者可能导致connection数量添加。以下举两个样例

  1. 有些RPC transport支持pipelining但不支持multiplexing (out of order responses), pipelining是指在同一个TCP连接上能够连续发出Req1, Req2, Req3, Response1, Response2, Response3。即Response的顺序必须和Request的顺序是一致。Req1假设须要非常长时间。Req2和3就都不能返回。一个Request假设占用太长时间,会导致后面的非常多个Request timeout。RPC client通常也会限制在一个TCP connection上面的max pending requests。但timeout发生。或者max pending requests情况下。client会主动创建新的connection。
  2. event loop lag 是指程序占用太长时间运行连续的CPU intensive任务。 仅仅有当任务结束时,event loop才会handle IO events,比方从socket上面读数据。否则收到的数据仅仅能保存在kernel 的TCP buffer里。通常这个buffer size小于64KB。当buffer满时(并且service又非常长时间没有读buffer)。socket的远端就不能发送很多其它的数据。这时也会导致远端的transport error。
    相同的。client会主动创建新的connection,当connection添加到预设的fd limit时。service就不能继续accept新的TCP connection了,事实上是不能open新的文件了。并且。绝大部分的程序没有測试过达到fd limit的场景。非常多API须要open file, 比方logging和core dump. 所以,一旦达到fd limit, 就像out of memory一样,将非常难recover,仅仅能crash process. 而这时正是过载的时候,重新启动实际上降低了capacity。 不论什么crash在过载的情况下仅仅会更糟。facebook在这防止过载上做的非常好。在C++实现的thrift server上,有一个或者多个threads仅仅负责accept TCP connections. 你能够指定最多的connections for thrift calls。
    这个connection limit是远小于fd limit, 当connection太多时,thrift server能够fail fast。所以。这样的情况下能够让service能一直保持在max qps。

整个数据中心挂掉怎么办



【亲述】Uber容错设计与多机房容灾方案 - 高可用架构系列_数据中心_03

在Uber的场景中,假设rider已经在一个trip上了,我们通产会等trip结束后才把rider迁移到其它的数据中心。我们叫做soft failover。否则须要hard failover,我们会把DNS指向其它的数据中心。

并且用户的DNS服务器非常可能在一段时间内还是cache曾经的ip,并且这个cache的时间是基本没办法控制的,所以我们会在load balancer上返回HTTP redirect。这样手机的客户端收到后会马上转向新的备份数据中心。

惊群问题(thundering herd), 非常多服务在provision的时候依据寻常的QPS预留了非常少的容量空间。当数据中心或者load balancer重新启动的时候,假设全部的客户端同一时候发起请求,这时的QPS能够是平时的非常多倍。 非常可能导致大部分请求都失败。一方面须要在客户端实现exponential backoff, 即请求失败后retry的间隔时间是增长的。比方1秒,5秒。20秒等等。

另外在load balancer上实现rate limiting或者global blackhole switch, 后者能够有效的丢掉一部分请求而避免过载,同一时候尽早触发客户端的backoff逻辑。

假设大家用AWS或者其它云服务的话,AWS的一个region通常包含几个数据中心。各个数据中心甚至在相邻的介个城市,有独立的空调系统和供电。

数据中心之间有独立的网络 high throughput low latency。 可是在region之间的网络一般是共同拥有的 high throughput high lantecy

整个region挂掉非常少发生。

能够把服务部署在多个可用区(Availability Zone)来保证高可用性。

Q & A



Q1:health check endpoint中实现简单的业务逻辑。这个意思是load balancer中有业务逻辑检查的插件么?这样load balancer会不会非常重啊。能够具体说一下么?

load balancer仍然是HTTP GET, health check 没有额外的开销。可是服务本身处理health的方式不同。可添加业务逻辑相关的检查 比方是不是能够訪问数据库。

Q2:region切换时,用户的数据是怎么迁移的?

这个是个非常好的问题,Uber採取的是个非常特别的方法。 realtime系统会在每次用户state change。

state change的时候把新的state下载到手机上,并且是加密的。当用户须要迁移到新的数据中心的时候,手机须要上传之前下载的state,服务就能够从之前的state開始,可是non-realtime系统 比方用户数据是通过sql replication来同步的。是Master-master。并且Uber在上层有个数据抽象,数据是基本上immutable的 append-only 所以基本不存在冲突。

Q3:假设是req timeout,但另外一边已经运行成功了,这时候重试。那不就是产生了两次数据?特别是insert这样的类型的。

是的,假设是GET类型的请求能够retry。 可是POST类型的请求 那么仅仅能在conn timeout时能够安全的retry。 可是receive timeout不能重试。(Tim补充看法:对于POST请求。假设service实现了幂等操作也是能够retry)。 有些类型的数据能够自己主动merge比方set和map。

Q4:那receive timeout。这样的情况下。仅仅能通过merge或者冲突对照解决?

恩 是的。 须要在逻辑层推断是不是能够retry。 这个我建议在更上层实现, 比方在消息系统中,全程不retry 就能够保证at most once delivery, 假设须要保证at least once delivery 须要添加数据库和client dedupe。

Q5:大面积故障时Uber用什么手段来控制仅仅处理部分用户请求?

我们实现了一些rate limiting 和 circuit breaking的库。可是这时针对全部请求的。

我们如今还没有做到仅仅处理某些用户的请求。

Q6:“将key space hash到相对小的shard space, 由于全局仅仅有一个single master, 从而保证了shard map的全局一致” 这个方法每次计算shard node的时候,必须先询问下master么?

是的。 在client端有一个shard map的cache, 每隔几秒钟能够refresh, 假设是复杂的实现。则能够是master 推送shardmap change。

Q7:多个机房的数据是sharding存储(就是每一个机房仅仅存储一部分用户数据),还是全部机房都有全部用户全量数据?

Uber如今的做法是每一个机房有全部用户的数据。 facebook的做法是一个机房有一部分用户的数据。

Q8:那多个机房的数据同步採用什么方案?

facebook用的就是mysql replication,有些细节我不清楚。 Uber还没有跨数据中心的replication,可是我们考虑买riak的enterprise服务,能够支持跨数据中心的 replication。

对于sql数据 我们就2个方案:大部分用户数据还是在postgresql里的(没有sharding, 是个single node),由于Uber起家的时候就在postgres上,这个数据是用postgres原生支持的replication, 另外有个mysql的, mysql存的是trip的数据, 所以是append only并且不须要merge的。

这个我还须要确认是不是每一个数据中心里面有全量的数据还是仅仅有本地产生的trip数据
Uber数据抽象做的比較好,数据分为3类:
最小的 realtime的,跟ongoing trip的个数成正比。

正在迁移到riak
比較大 非realtime的。跟user个数成正比。

在postgresql里面 用postgresql的relication,正在迁移到mysql,用mysql的replication
最大 非realtime的,跟trip个数成正比。 在MySQL里面有非常多partition,一个用户在一个partitionl里面,一个partition一个全局的master。写都去master。 并且Partition非常少迁移,所以当seconary变成Master时。可能没实用户之前的trip的信息,replication是offline的 好像是通过backup-restore实现的。

Q9: 那怎样实现“每一个机房都有全量数据”的?

不是实时的,是在应用层实现的。并且如今还没開始大规模使用。 另外问下riak 有同学在用么? Uber 的非常多系统去年就開始迁移到riak上了,由于riak是保证availability的 。将来在Uber会是重点。

Q10:Uber的消息系统是基于nodejs的吗?客户端长链接的性能和效率方面怎样优化?

是基于nodejs的。

我们没有特别优化性能。只是stress test看起来2个物理机能够保持800K连接。

Q11:Uber消息系统协议自己DIY吗? 是否基于TLS? PUSH消息QPS能达到多少?

是的。基于HTTPS。

具体QPS我不太记得了。

Q12:riak的性能怎样?主要存储哪些类型的数据呢?存储引擎用什么?raik的二级索引有没实用到呢?

riak性能我没測试过,跟数据类型和consistency level都有关系。 可能区别比較大。 我们如今用的好像是leveldb。

Q13:应用层实现多机房数据一致的话,是同一时候多写吗? 这个latency会不会太长?

sql如今都是用在non-realtime系统里面,所以latency可能会比較长。

Q14:Uber rpc用的什么框架。上面提到了Thrift有好的fail fast策略,Uber有没有在rpc框架层面进行fail fast设计?

Uber在RPC方面还刚開始。 我们一直是用http+json的,近期在朝tchannel+thrift发展, tchannel是一个相似http2.0的transport,tchannel 在github上能找到。我们的nodejs thrift 是自己实现的。由于apache thrift在node上做的不是非常好。thrift的实现叫做thriftify ​​https://github.com/Uber/thriftify​​正好推荐下我的开源项目哈。 在thrift server上我们没有做fail fast, 怎样保护是在routing service中实现的。

Q15:Uber走https协议。有没有考虑spdy/http2.0之类的呢?在中国网速状况不是非常好的,Uber有没有一些https连接方面的优化措施?

正在考虑迁移到HTTP2.0,这个主要是手机端有没有对应的client实现。 server端我们用的是nginx,nginx上有个experiemnt quality的extension能够支持spdy。

我们还考虑过用facebook的proxygen ​​https://github.com/facebook/proxygen​​。proxygen支持spdy。 我在facebook的chat service是用proxygen实现的,并且facebook 几十万台PHP server都在proxygen上,所以能够说是工业级强度的基础设施,只是build起来要花点时间。

Q16:为了避免服务过载和cascade failure,除了在服务链的前端採用一些fail fast 的设计,还有没有其它的实践作法,比方还是想支持一部分用户或特定类型的请求。採用优先级队列等。

就这个问题,Uber,facebook在服务化系统中还有没有其它技术实践?另外出现大规模服务过载后的恢复流程方面。有没有碰到什么坑或建议?

“比方还是想支持一部分用户或特定类型的请求” 这个事实上比較难实现 由于当服务过载的时候 在acceptor thread就停止接受新的connection了,那就不知道是哪个用户的请求 。这个须要在应】用层实现,比方feature flag能够针对一些用户关掉一些feature。 我发现有个非常实用的东西就是facebook有个global kill switch,能够同意x%的流量,这个当全部service一起crash 重新启动的时候比較实用。