• Feeds

  • Archive for the ‘service’ Category


    架构面试题 – 为什么我的朋友圈不见了?

    经常有朋友问到,“感觉你们的系统最近没什么太大变化,你们那么多工程师在忙什么?”,下面的这个场景,可能是工程师花费了不少时间的情况之一。

    有如下一个场景,某个服务需要构建一个列表数据返回给调用方(调用方通常是客户端),服务本身是一个数据聚合器,它由内部多个远程服务的数据聚合而生成。在正常情况下,需要将所有内部服务的结果全获取成功后再返回。但是在一个大系统中,多个服务中某个服务出现不稳定的概率会比较大,当出现如图远程服务3不可用的时候,有三种不同的解决思路。

    list1

    • 方案1:忽略出错的数据(图中数据3),直接返回数据1、2、4。
    • 方案2:遇到任意失败,整个请求返回错误503 service unavailable。
    • 方案3:忽略出错的数据(图中数据3),并告知调用方出错的范围,需要自定义的返回格式。如 {“load_data3_success”: false}

    如果你作为一个架构师,会选择哪种方案?

    方案一类似架构设计里面常说的优雅降级,在出现问题情况下,除了数据3不能返回之外,其它数据可以正常返回,原理上可以将损失降低到最低。但这种方案会给用户体验带来一定伤害,用户在使用系统时候会存在不确定性的心理感受。

    方案二比较依赖调用方的容错逻辑,如果调用方保存了上一次缓存,且容错逻辑处理得当,用户表面会感受不到这个异常。如果没有容错逻辑,最坏情况则将会返回白页。但是即使有容错逻辑,由于正常的数据也不能及时返回,从工程师到用户可能不太容易接受这个结果。

    方案三是一个看起来相对合理的方案,但是需要添加自定义的字段,本来这个调用是一个标准的LIST数据返回,但如要判断每个数据项是否返回失败,需要额外添加一些标识字段如 {“load_data3_success”: false},用于标识哪些数据返回失败了。因此,接口设计及实现变得更加繁琐,调用方也需要实现缓存及容错逻辑,从服务方到调用方的熵都增加了很多。

    因此,这个选择题已经不好做了。但雪上加霜的是,在大部分应用中,对于数据列表访问同时还存在未读数的功能,如下图中的小红点数字。如果这个未读数由另外一个API提供(本讨论假设未读数API功能正常),情况就更复杂。

    补充讨论一下,如果不提供单独的未读数API,客户端需要每次需要加载新的全量数据才能本地算出未读数,会带来访问速度的下降及客户端更多流量的消耗。因此大多数情况提供一个未读数API整体开销会更低。通过未读数API判断当服务端有新数据时候才去访问列表接口。

    list2

    这时候如果未读数都出来了,远程数据又取不到的情况下,你作为架构师,会选择何种方案?至少,碰到这种情况时如果还未找到理想方案,建议不要盲目优化,因为它除了增加系统的熵,不会将事情变得更好。

    在线服务的黑天鹅

    软件随想录(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……