• Feeds

  • Posts Tagged ‘rpc’


    在线服务的黑天鹅

    软件随想录(More Joel on Software)有这样一段话

    提高服务稳定性的最大困难,就是”黑天鹅难题”(problem of black swans)。这个名词是由Nassim Taleb提出来的(www.edge.org/3rd_culture/taleb04/taleb_indexx.html),他这样定义:”黑天鹅代表外来因素,是一个超出正常预料的事件。”几乎所有的互联网服务中断,都来自于意料之外的突发事件,属于极其小概率的非主流意外。这类事件是如此罕见,以至于常规的统计方法—-比如”故障间隔平均时间”—-都失效了。”请问新奥尔良市发生特大洪水的平均间隔时间是多少?”

    Tim这两天也是刚忙完InfoQ的ArchSummit的演讲,正在利用周日休息一下,但是一醒来就收到消息说某服务有些问题。于是赶紧连线了一堆正在处理事件的工程师,等拿到初步的原因分析之后,已经4小时之后了。

    “墨菲定律”,一位工程师说,“前几天觉得这个地方有些隐患,已经存在一段时间了,打算下周来处理,未料它今天就出了问题……”。墨菲定律是指“凡是可能出错的事必定会出错”,指的是任何一个事件,只要具有大于零的机率,就不能够假设它不会发生。因此在线服务发生问题之后,总有工程师随即证明墨菲定律的有效性。

    不过我觉得用黑天鹅难题更能体现在线服务可用性的不可控,可用性是指一个系统中提供服务与设计时间的比例,通常用百分比来表示。在线服务通常看到最多的是以下3种

    • 99.9%,服务中断时间:525.6分钟/年
    • 99.99%,服务中断时间:52.56分钟/年
    • 99.999%,服务中断时间:5.256分钟/年

    当一个系统有大量用户使用之后,对系统可用性有较高要求,互联网服务通常会把可用性目标定在99.99%及以上,但极少能达到99.999%的。Tim有一个服务由于功能简单且稳定,较少需要变更代码,且有容错的设计,服务池没有单点问题,所以跟同事们说2014年可以达成99.999%了。未料这个服务池最近被一个极偶然的扫描脚本全部干掉了,尽管运维工程师马上进行了处理,但是最后也较难满足1年低于5分钟中断的期望。虽然这个是个偶然的案例,但是它却是典型的黑天鹅事件。

    对于系统服务可用性的问题,在专业领域其实有3个词汇去描述的。描述的顺序通常是 fault -> error -> failure。这方面大多定义引用来自《Patterns for Fault Tolerant Software》一书,在书中描述如下。
    fault-error-failure
    用一个通俗的例子来描述三者的定义是,如果把fault比作是数据库网线断了,则error是网络不通连不上数据库,导致的failure是不能注册用户。由于error及failure是不可避免的,所以在代码设计上更多的是做容错(fault-tolerance)。

    我们可以通过服务之间进行良好的容错设计,进一步用代码review,关键路径的梳理来确保容错策略的落实。上线的checklist来确保变更出现异常时候的应对。即便如此,容错只能帮助我们规避大部分已知的问题,随着系统长时间运行,总是有意外情况出现,曾经有同事碰到关键的服务中出现内存出错这种小概率事件,查出来之后,当事的工程师的肯定为了怎么写好问题总结那一段话在绞尽脑汁。

    当团队规模比较小的时候,服务本身可控性还是比较强,关键路径中的每一段代码团队成员非常熟悉。当出现异常问题,团队成员也可以快速拿出处理方法来解决。但是当系统变大,每个团队只参与大系统一个环节时,问题会变得更复杂。从概率的角度,大的系统中小的模块的failure不可避免,容错流程总是存在疏忽的地方。当系统中存在复杂的网状调用,无法完全做到松耦合(理想的松耦合是指一个服务的失败不会引起另外一个依赖服务的失败),因此任何一个模块中,可能由于一个缺乏经验工程师的一行不经意的代码造成整个大系统不可控的后果。

    大型系统不可预知问题的排查通常需要更多时间,需要多个团队共同参与来定位及解决问题。但在线服务由于可用性的要求,出了问题之后,解决问题的紧急程度会比分析问题更高,因此并不会第一时间来讨论及分析问题,现场的工程师需要凭有限的现象迅速做出判断,将问题消灭在萌芽阶段。但正是由于缺少完整分析问题的时间,故障也往往难以第一时间有效解决,问题延续的时间比预期的要久也成为常见的现象。

    对于公司来说,肯定希望所有的服务都有99.999%的可用性;但由于黑天鹅的现象,完美的可用性较难达到,这也很容易成为工程师的心理负担。当稳定性出现问题后,负责的技术团队心理沮丧程度不会比一个战败的队伍更好,甚至一些工程师还会造成长期心理上的压力及阴影。夜深人静时候电话突然响起,第一反应会心头一紧,“莫非是服务出问题了?”。

    工程师应该用什么样的心态去看待出现的问题?

    一方面,各种故障failure它确实是一种客观存在,给用户访问及体验带来了不便。我们不会通过回避问题来避免它的出现。当出现问题后,需要通过问题复盘的方式,帮助我们来重审事件的经过,检查流程、机制、代码等共性层面的问题,避免在同一个地方再次踩坑。同时也可以反思团队在项目中的表现是否达到了平均以上的水平,是否存在一些低级错误?

    在另外一方面,如果问题超出了之前的认知及应对策略的范围,属于黑天鹅式的问题,则没必要太多的自责。具有完美心态的工程师需要理性的看待各种批评及质疑,毕竟在一定程度,这是业界在共同应对的一个类型的难题,这些不稳定问题的出现也是在线服务的一部分。

    应用层的容错与分层设计

    针对在项目中碰到的一些容错设计问题,团队最近进行了一次技术沙龙,讨论了以下话题。

    为什么需要应用层的容错设计?

    一个完整的系统在内部是由很多小服务构成,服务之间以及服务与资源之间会存在远程调用。

    • 每个系统的可用性不可能达到100%
    • 各种网络及硬件问题,如网络拥堵、网络中断、硬件故障……
    • 远程服务平均响应速度变慢

    服务器平均响应速度如果慢下来,慢慢消耗掉系统所有资源,进而导致整个系统不可用。因此在分布式系统中,除了远程服务本身需要有容错设计之外,在应用层的远程调用的环节,需要有良好的容错设计。

    应用层的容错设计有哪些方法?以下是微博团队使用过的一些实践。


    访问MySQL的容错设计

    • 写操作:如果master异常,直接抛异常。
    • 读操作:如果slave有多个,先选择其中一个slave,如果获取连接失败,再选择其他的slave,如果全部不可用,最后选择master。


    访问Memcached/Redis的容错设计

    首先设置so_timeout,避免无限制等待;服务器连接如果IO异常,设置错误标志,一段时间停止访问;出错后定期主动(比如ping Redis)或被动(当被再次访问时)探测服务是否恢复。

    Failover机制:
    如果连接某个node失败, 当前pool启用一致性hash切换到backup node;如果backup node没有数据,则通过另外一个服务池(数据副本)获取数据。


    访问远程HTTP API的容错设计

    设置so_timeout;部分场景:短超时,重试一次;另外由于HTTP service情况的多样性,业务层面还有通用的降级机制。


    访问不同资源使用不同方法存在的问题

    从上面列举的部分场景来看,在访问不同资源时候,每种client访问都有一些相通的原理,但却要使用不同的重复实现。由于各个client独立实现,实现时候由于各个远程服务协议及行为的差异,导致这些容错原理无法直接复用。另外在代码层面,不同的client也使用了不同年代的一些底层库,一些早期client的实现,数据层,连接层,协议层全部耦合在一起,也造成维护成本进一步加大。

    比如之前一些服务开发中碰到的类似如下的问题:

    • hbase-client由于没有实现容错设计,导致访问出现了抖动,影响了同一服务池的其他调用,需要增加类似MySQL client的容错及快速失败策略;
    • MySQL slave流量出现不均衡了,由于多个slave IP之间没有使用公用的负载均衡策略,因此需要重新添加、上线及验证。

    另外目前分布式系统中大部分远程资源都是IO bound而不是CPU bound,而client大部分又是同步调用,造成大部分调用都在等待远程返回,同时也消耗了工作线程资源,以及大量线程context switch。


    有没有可能统一的client?

    这些策略原理上是可以公用的,能否出一个统一的client层来一劳永逸?不过这个需求不是twitter干过吗?

    Finagle,不仅是平时理解的RPC框架,还有目标是想成为一个commons client,从另外一个层面,广义上访问远程资源也都可以理解成RPC,所以Finagle也常称为RPC框架。

    Finagle implements uniform client and server APIs for several protocols, and is designed for high performance and concurrency.

    在Twitter体系,分布式服务可以从future, service, filter三个层次理解,容错、超时、授权、tracing、重试等机制都是体现在filter中;而future则将client从多线程、队列、连接池、资源管理释放出来,从关注控制流到关注数据流。并且默认变成异步方式。

    Finagle的FailFast模块会避免分发请求到出现问题的服务,它通过来记录到每个host的错误来进行标记,当出错以后,Finagle会通过一个后台线程定期重连以检查是否恢复。当host宕机时,相关的service会标记成不可用。


    如果来redisign一个通用的网络client,它应该包括哪些元素?

    • 具有服务的分层设计,借鉴Future/Service/Filter概念
    • 具有网络的分层设计,区分协议层、数据层、传输层、连接层
    • 独立的可适配的codec层,可以灵活增加HTTP,Memcache,Redis,MySQL/JDBC,Thrift等协议的支持。
    • 将多年各种远程调用High availability的经验融入在实现中,如负载均衡,failover,多副本策略,开关降级等。
    • 通用的远程调用实现,采用async方式来减少业务服务的开销,并通过future分离远程调用与数据流程的关注。
    • 具有状态查看及统计功能
    • 当然,最终要的是,具备以下通用的远程容错处理能力,超时、重试、负载均衡、failover……

    服务管理框架的尝试

    大型软件系统开发需要模块化,在分布式系统中,模块化通常是将功能分成不同的远程服务(RPC)来实现。比如可以用Java RMI、Web Service、Facebook开源的Thrift等一些技术。同样,在一个大型系统中,服务化之后服务的可维护、可管理、可监控以及高可用、负载均衡等因素同服务本身同样重要。

    服务管理目前并无直接解决方案,Thrift作者Mark Slee提到

    It’s also possible to use Thrift to actually build a services management tool. i.e. have a central Thrift service that can be queried to find out information about which hosts are running which services. We have done this internally, and would share more details or open source it, but it’s a bit too particular to the way our network is set up and how we cache data. The gist of it, though, is that you have a highly available meta-service that you use to configure your actual application server/clients.

    Source: [Thrift] Handling failover and high availability with thriftservices

    如果开发一个自己的服务管理框架,需要具备以下功能

    • 快速失败,这个在本厂意义重大,很多远程服务调用是在关键路径中,它可以容忍失败,但是不能容忍堵塞
    • failover,客户端failover支持,并支持自动失效探测及恢复调用
    • 中心化配置及推送功能,所有client在同一时刻配置的一致性,并且client会跟配置中心保持长连
    • 负载均衡策略:支持round robin,least active, consistent hash,或者基于脚本的动态路由策略。这个都是由配置中心来控制
    • 动态启用及停用服务及节点:可以动态启动及停用服务(热发布),由于有推送功能,相对容易实现
    • 跨语言:支持client能使用常见主流语言来访问
    • 版本管理:同一服务可以有不同的版本并存
    • 访问统计及动态运行参数查看:可以对方法级别进行访问统计及实时观察

    访问策略

    服务框架倾向于直连的方案,即client是直接连接server,而不会增加中间物理上的代理层,服务框架只做中心配置、访问策略、服务发现、配置通知等职责。

    路由的特殊需求

    通常的服务访问,使用上述round robin等3种策略即可,但是在实际工程实践中,我们发现有些不同的需求。比如计数这样的远程服务,读操作可随机访问一台远程节点,但写操作需要访问所有的服务节点才能实现。因此我们需要有广播式的访问需求。由于计数服务对实时性和一致性要求较高,不适合采用异步如Pub/Sub这样方式去实现,因此在client还需要支持同步的广播调用。

    耦合及侵入的矛盾

    在设计服务管理系统之前,我们希望不跟一种具体的技术(如Thrift)绑定,比如client和server服务实现方不需要太多关心底层技术。但是在实际实现过程中碰到不少矛盾。

    IDL侵入

    在使用Thrift之后服务实现很难绕过Thrift IDL,使用方需要自己维护IDL以及Thrift生成的代码,服务框架支持将Thrift服务注册到配置系统中。虽然也可以绕过IDL来实现服务,但是框架相关功能的实现和维护成本比较高。

    RPC框架的侵入

    Thrift Transport可以使用TCP(Socket)或者是HTTP
    这个也是非常好的特性,在某些情况Transport使用HTTP会带来很多便利,使用HTTP虽然有一些额外开销,但是HTTP的周边配套设施的完善足够抵消这种开销。使用TCP很多状态实时监控都需要服务系统从头做起。

    Thrift的Version与服务的version存在一定的重复
    服务牵涉到版本管理,我们希望通过发现服务来管理,但是Thrift本身也有版本的设计。

    这些矛盾的本质就是服务框架需要的一些功能是自己实现还是依赖Thrift来实现,很多Thrift使用方如Twitter rpc-client干脆就直接在Thrift框架基础上增强。

    虽然存在上述一些待解决问题,厂内第一个使用服务框架管理的服务即将上线,很快每天会有数十亿的调用将会在此之上产生,同时也会有新的挑战出现。


    Figure 1: Facebook Service Management Console
    (来源:http://www.slideshare.net/adityaagarwal/qcon Slide 27)