• Feeds

  • 分布式缓存的一起问题

    背景说明

    分布式缓存中为了可用性及高性能的考虑,可以使用如下一种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分钟过期延缓了过期的访问数据库的压力,但相关压力仍然会传递到数据库。

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

    基于分发机制的公众订阅平台设计


    如下图所示,一个基于分发机制的公众订阅平台,是指一个移动信息发布系统,可提供订阅功能的系统企业帐号可以给所有的订阅用户按照一定的条件(如地域或用户特征)群发消息,系统的主要关注点包括

    • 分发的throughput。由于群发的放大效应,如何让拥有大量订阅用户的企业帐号能及时投递消息。以及到客户端的移动push通道也存在throughput的问题。
    • 存储的性能,根据客户端的访问型态,如果考虑数据保存在云端,大多需要更新最近联系人、用户与企业订阅帐号之间的会话索引,以及每个会话的未读提醒数。由于客户端本地也可以保存部分数据,因此不同的实现型态可能云端存储及更新的数据有一定差异。
    • 分发及存储模型。分发指数据投递推拉方式的选取,由于筛选条件及通过移动网络主动push的可能性,推送方式可以更好满足业务型态。存储模型指如何选择合适的模型让上万订阅用户的推送能更及时快捷。

    (图中紫色为可能的瓶颈点)

    从图来看,实现并不复杂,但是当系统规模增大之后,如要达到投递即时性需要考验一些工程能力。
    紫色的三个框都可以用key/list来表达,redis的性能和功能可以匹配这样一个场景,这也是为什么近几年redis会如此流行,但离完美匹配类似场景还有一定距离。

    单元化与分布式架构的切分问题

    单元化是将一个系统的架构按某种数据特征维度进行垂直的划分,比如网站有100万用户,如按照用户维度进行划分,则可以分成10个单元,每个单元存储10万用户资料。单元化的一些收益如下

    • 由于每个单元数据规模可控,相关维度内的所有资料可放在一个数据库中(如上例中的用户资料),不需要复杂的sharding分库分表逻辑,存储及缓存访问得到极大的简化。同时开发也变得简单,工程师不需要有丰富的“大规模大并发系统”开发经验。
    • 同时由于计算离存储更近,也可以让数据离用户更近,比如用户数据存储在地理上靠近用户的位置,数据有了更好的局部性(locality),因此也会获得更好的访问性能。部署上相关单元的前端、缓存、数据库、数据挖掘等节点可在同一个机柜,架构上让大数据的访问变得低廉,也在部分程度上让大数据更为快速及敏捷。
    • 可以自然支持不同用户分片支持不同的功能特性,天然的A/B testing试验场。

    分布式是将一个系统的数据分布到多个单元,以便使系统能够scale out,具有更好的可扩展性。当今大型网站基本上是分布式设计的。分布式系统除了机房内的,考虑到系统扩展性、用户访问的便捷性、机房规模的物理限制、异地容灾(比如2013年4月及8月的微信机房故障)等需要,大型系统也会考虑地理分布在多个机房。

    但是在社交网络中,由于数据的网状访问,单元化会碰到较难选择合适的单元化切分维度的问题,比如按用户或按内容进行单元化不能很好的适应数据访问局部性的问题,同时地理分布式也面临相似问题,由于社交网络中用户的页面需要访问的,所有地理分布的机房都同步需要全量数据,导致部署和维护成本较高。

    假定系统中存在一个跨单元的数据访问总线,并且总线的访问满足

    1. 局部性,单元内的访问,大部分的数据可以在单元内命中。
    2. 封装性,单元内的应用程序使用统一的方法访问数据,不需要关注数据的具体位置。

    那么这个数据总线应该如何设计?放在哪个层级比较合适?比如service layer, cache or storage(e.g. Google Spanner)?