• Feeds

  • 分布式服务框架的4项特性

    在移动及云时代,尽管大部分可扩展的问题可以通过云平台解决,但是服务本身的扩展性挑战仍然存在。比如一个新的项目,用PHP或JSP实现了基本功能,部署在Apache或Tomcat等容器上,在业界这种部署在一个容器内的功能模块通常可以称为一个service。服务容器很容易通过EC2或者docker等方式来扩展部署更多的实例。但service本身的管理的以下几个方面的问题仍然需要架构师去设计及解决。

    1、服务的远程调用(RPC)。

    随着系统的用户访问规模增大,以及系统功能的增多,众多的功能模块(service)很难用单个service来承载,这些不同功能的service可能由不同的开发团队开发,甚至使用不同的开发语言,最终部署在不同的服务器容器内。Service之间的调用需要一种协议及远程调用的实现,需要具备灵活的data type支持,对调用双方透明(理想情况它就像在执行本地调用),并且具有良好的性能。比较典型的RPC实现有
    Google: Protocol Buffers RPC
    Facebook: Thrift
    Twitter: Finagle

    2、服务的分布式调用链及服务状态跟踪统计

    随着系统内部的服务增多,一个功能的调用可能会通过系统内部多个服务相互调用来完成,并且这些服务可能由不同的团队开发,并且分布在不同的服务器容器,甚至可能在多个地域不同的机房内。因此分布式系统需要有一种方式来清晰的了解系统的调用及运行状况,测量系统的运行性能,方便准确的指导系统的优化及改进。

    由于trace的主要功能都是依赖日志输出来完成,因此通常也需要建设相关的分布式日志系统及数据实时分析展示系统等,分布式日志收集及数据实时分析也是一个非常大的话题,本文不展开详谈。比较典型的trace系统有
    Google: Dapper
    Twitter: zipkin

    3、服务的配置管理。包括服务发现、负载均衡及服务依赖管理。

    比较常用的服务发现是域名方式,调用方通过域名来定位目标服务。域名寻址方式可通过配置多IP(或VIP)来实现负载均衡。域名方式存在一些弊端,常用的DNS服务器通常是上个世纪的产物,管理繁琐,更新域名记录后由于协议设计上存在的TTL缓存机制,修改不能立即生效,也无法做变更的push操作。更高级的特性如控制流量、灰度发布等也无法实现。

    目前,成熟的分布式服务较多使用基于ZooKeepr的配置服务,ZooKeeper由于与client保持长连,因此具有push能力,可以迅速的调整配置及生效。但由于ZooKeeper本身只是一个通用工具,分布式服务具体场景各种高级特性还需要自行在此基础上实现。

    基于DNS的服务发现在负载均衡方面具有明显缺点,由于多IP或VIP使用类似round robin的策略,在实践中,同一服务的多个IP之间负载通常并不均衡。而服务提供方并没有有效手段对runtime期间不均衡进行调整。基于ZooKeeper的配置服务可以支持各种复杂的特性,但需要做一些定制化开发,但服务发现作为系统中最核心的一个环节,这些定制化开发需要较多的经验的积累及沉淀,之前在线上就碰到过配置服务将一个服务池中疑似不稳定的节点移除过多,从而导致出现雪崩的情况。

    除了ZooKeeper之外,业界还有一些类似工具,如serfdomconsui

    4、服务之间的调度及生命周期管理

    目前大部分服务的部署都是按照事先的规划安装在机房不同的服务器上,配置服务通常只是起服务节点的failover作用,业务中真正按弹性调度来运作的系统还不普遍。但原理上所有的service可以看做是MapReduce中的task,它的调度及生命周期可以很高效的由分布式容器来管理,并且根据service的属性来灵活的分配资源,比如控制CPU的核及内存大小。

    业界比较成熟的有Apache旗下的MesosYARN。它们的特性有点类似,但是由不同的组织开发。Mesos主要功能是由C++来实现,可以支持docker container来进行调度,因此它的实现更偏底层一些。Yarn是Hadoop 2的一部分,可以更灵活的来调度MapReduce jobs,它是一种JVM内部的实现,可以很好的支持JVM级别的任务分配及调度。

    上面介绍了这么多,主要是最近考虑团队在上述1-4之间做一些事情。一方面目前业界在这几点之间还有一些缺失或者欠优化之处,另外1-4点之间也可以适当做一些实现的整合。整合并不是要产出一个大而庞杂的软件,我个人是极力反对大而全,也不喜欢沉重的框架,业务的service实现方不应该import太多工具或者SDK,因此将要做的功能肯定是透明及可插拔的。

    由于这件事情并没有严格的可参考目标,因此对于最终实现的是哪个子集还存在一些不确定因素,这个项目如果具有通用性,也考虑以开源的方式来开发。对这方面有什么想法,欢迎留言。

    开源移动通讯架构与XMPP

    XMPP由于上下游良好的开源生态得到了广泛的采纳与应用,但是到了移动为主的时代,XMPP的不足也暴露出来。

    XMPP全称是Extensible Messaging and Presence Protocol(也称为Jabber),是一种支持消息及状态的协议,但在线状态在移动场景并是一个必需的feature。由于智能手机具有随时在线的特点,状态可以视为永远在线;即便在app没有打开的情况下,系统可以通过push等方式发送最新的信息,因此大部分面向移动的通讯软件直接去掉了状态的特性。因此设计成支持多终端状态的XMPP在移动领域并不是擅长之地。另外一方面XMPP是一种基于XML的协议,它的请求及应答机制也是主要为稳定长连网络环境所设计,对于带宽偏窄及长连不稳定的移动网络并不是特别优化,因此它的弊端就充分暴露出来了。

    移动领域也有不少非XMPP的开源实现,尽管赢得主流认可的不多。比较商业化的有telegram,其特性跟微信比较类似,在一些国家也取得了飞速发展,尽管其宣称的开源还不完全。国内有前几天蘑菇街发布的的TeamTalk, 其开源地址是 https://github.com/mogutt 从发布的短短几天来看, star/fork 数非常可观。其介绍支持企业内部通讯场景,且具有完整的移动端支持。

    最近也在考虑在这方面做一些尝试,一个理想的适合移动时代的IM开源软件,它应该具有哪些因素?

    • 分布式扩展能力。在XMPP领域,由于Openfire的简单易用,成为很多团队首选的方案,但使用Openfire的团队都需要接着思考扩展openfire的分布式扩展能力,以便承担更大的用户访问规模。
    • 移动友好的协议,协议具有良好的长连及短连自适应能力,具有数据的增量更新能力,较低的重连成本等。有网友推荐MQTT http://mqtt.org/,尚未深入评估。
    • 移动SDK,主要实现协议层及网络逻辑,以简化客户端接入及开发成本。
    • 开发语言,更多考虑一些能带来编程乐趣的新型语言,在一定程度上,scale的问题可以做到与语言无关。

    在微博上交流的时候,一些网友还提到了XMPP的federation功能,federation类似邮件协议,利用XMPP可以实现多个域的用户互联互通。但由于此功能对于企业的商业化利益较难看清,因此在过去很长一段时间都没有得到充分发展。Google最近也放弃了多年来坚持的XMPP federation的支持。

    需要补充的是,尽管前面提到XMPP种种问题,但是也别低估一种新协议的认知成本,很多时候选择XMPP并不是因为它协议强大或多么适合用户的场景,而是当大众群体已经了解一种协议之后,即使这种方式存在种种问题,但还是较难广泛认可及接受一种方式或协议。在没有特殊原因的情况下,普通的通讯场景仍然建议使用XMPP方式。

    分布式缓存的一起问题

    背景说明

    分布式缓存中为了可用性及高性能的考虑,可以使用如下一种master/slave设计模式。

    图中的proxy是逻辑的概念,可以是基于client的包装实现,也可以是独立的proxy服务,但本文大部分是指独立的服务。几个主要的问题说明如下。

    为什么cache要使用两个集群((master/slave)来存放?

    主要出于可用性及高性能的考虑。传统的架构使用基于一致性哈希的分布式缓存,数据只存在一份副本,在出现cache节点单点故障时,虽然可以由一致性哈希算法将请求均匀落到其他节点,但由于穿透的请求较多,仍然给数据库带来较大的访问压力。为了避免对数据穿透带来的冲击,数据使用两份副本可以避免穿透的问题。同时在数据访问较大时候,也可以更好的分担流量,避免峰值单份数据跑满对系统带来的冲击。

    为什么两份副本要使用master/slave结构?

    由于大型系统中通常存在多个client同时操作同一份数据,需要确保所有client对数据修改时数据的一致性。为了避免两cluster两份副本数据不一致带来的困扰,使用了一个简单的做法,在配置中人为指定一个cluster为master,所有的数据以master为准。

    为什么一些场景需要使用CAS?

    CAS在计算机并发领域通常指Compare-and-swap,在memcached中,也称为Check And Set. 在分布式系统中,一份数据可能同时被多个调用修改,比如微博中的@箱,一个用户同时收到多个@的情况还是比较常见,比如当原来@箱里面记录是{1,2,3}时,4和5由不同的调用来源同时到达,如果没有同步的保护,系统的数据有可能最终被写成{1,2,3,4}或{1,2,3,5},由于memcached没有原生的list结构,list都是一个自定义的value, 则很容易出现client A覆盖了同时在写的client B的数据。因此假如两个调用方同时读到{1,2,3}时,第一个写入{1,2,3,4}会成功,后续的{1,2,3,5}CAS写入就会失败,因为此时服务器已经不是{1,2,3}了,失败的调用向服务端取回{1,2,3,4},最终写入{1,2,3,4,5}

    在master/slave场景,比起普通的memcache CAS有什么区别?

    目前的做法是master cas成功之后,直接修改slave,并不同时在slave执行cas操作。由于数据存在两份副本,当数据不一致时,无法自动处理数据的不一致冲突。因此在实践上只以master操作为准。

    为什么使用proxy?

    使用proxy主要是出于可用性、命中率以及可运维方面的考虑
    可用性与可运维:当进行服务器增容或缩容时,如果client的数量较大,如果未使用proxy模式,client所在服务器通常需要修改配置并且逐个重启。重启(系统维护)一方面带来可用性方面的问题,运维方面也较为繁琐。
    命中率:如果业务场景需要较高的命中率(比如>90%),则增容或缩容就变得较为复杂,需要client配合做一些策略,比如扩容后仍然访问扩容前旧的节点的数据以保证命中率。如果用proxy模式则极大降低client的访问复杂性,将相关逻辑都封装在proxy之后。

    分布式缓存的一起问题

    最近某业务有一起master单点故障,导致在问题的时间段内,用户看不到最近发生变更的数据。由于在上述场景中,实现cas时候的流程如下
    1) master.cas(k,v)
    2) 如果1成功,slave.set(k,v)
    3) 如果1失败,不执行slave.set(),直接return;

    由于第三步在失败时,并不会set slave,导致数据出现一致性问题,即使slave依然可用,新的数据不会写入cache。

    首先看在master failure时,为什么不切换到slave cas?
    先说自动切换的问题
    上文也提过,两份数据副本在出现数据不一致后,并不能自动仲裁达到最终一致性,但是指定master角色可以达到最终一致性。如果master角色可以由调用方自动切换,则会带来数据的混乱。调用方存在多个节点,至少需要统一的config server来保证切换的一致性。另外,自动切换发生后,无法达到两份数据的最终一致性。
    再说由运维手工切换
    由于不牵涉到代码的逻辑判断,虽然切换也会带来一些数据一致性问题,在具体场景下(比如master长久宕机)切换可以接受。

    在出现上述问题后,其他一些解决方案如下。
    1. proxy在master cas失败时候delete slave data
    2. client在master cas失败时set slave, 并且将数据过期时间设成5分钟

    上述方案很难完美,一些明显存在的问题如下
    方案1:
    命中率的问题。由于delete导致修改的数据迅速失效,会导致读取量的增加,在读写均密集的业务场景,可能会导致数据访问出现波动。
    接口职责单一性的问题。proxy在cas调用中隐藏了删除数据的逻辑,这是一个未在正常期望范围内的额外操作,在特殊情况下,可能会导致不可预料的情况出现。(尽管在实际操作中proxy提供配置开关选项)

    方案2:
    依然是命中率的问题,5分钟过期延缓了过期的访问数据库的压力,但相关压力仍然会传递到数据库。

    希望通过上面说明读者能理解这个场景的问题。在这个场景下,完美的方案应当如何设计?