在40岁老架构师 尼恩的读者社区(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、网易、有赞、希音、百度、网易、滴滴的面试资格,遇到一几个很重要的面试题::
- 分布式缓存系统,如何架构?
- 百亿级访问,如何做缓存架构?
最近,有个小伙伴微博一面,又遇到了这个问题:百亿级访问,如何做缓存架构?
接下来, 尼恩借助微博Cache架构的设计实践案例,为大家揭晓这个问题的答案。
本文非常重要。 大家可以收藏起来,慢慢消化和掌握。
为啥呢?在现代互联网应用中,分布式缓存已经成为了应用性能优化的标配。一个好的缓存系统需要具有高可用性、高性能,并且能够保证数据的一致性和容错性。
本文将介绍如何设计高可用的分布式缓存架构,包括架构基础知识、整体架构设计、实现原理、一致性模型以及故障处理。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典 PDF》V101版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取
在分布式系统中,缓存系统的构建是关键一环,其基础架构主要包括以下几个重要部分:
为了防止单一节点压力过大,导致系统崩溃,我们需要对缓存数据进行分片。这样,不同的缓存节点就可以分别管理不同的数据片,并负责处理相关的读写请求。这种分片的方式,既可以有效地分散数据压力,也能提高系统的响应速度。
当客户端发起请求时,我们需要选取一个适当的缓存节点来处理。这就需要负载均衡算法的介入,该算法可以根据当前系统的负载情况、网络拓扑结构等因素,来选择最适合处理该请求的节点。这样,既可以保证客户端请求的及时响应,也能避免某个节点压力过大,影响系统的整体运行。
在分布式环境下,由于网络通信等问题,可能会导致缓存节点间的故障。为了提高系统的可用性,我们需要设计一些容错机制。例如,数据备份可以防止数据丢失,故障转移可以确保即使某个节点出现问题,系统也能继续运行。这些容错机制,可以有效地提高系统的稳定性和可靠性。
由于数据在多个节点上进行分布存储,因此需要确保数据的一致性。为了实现这一目标,我们需要采用一些协议和算法来处理并发读写操作。例如,我们可以使用分布式事务协议来确保数据的原子性和一致性,或者使用乐观锁算法来避免并发写操作的冲突。这些手段,都可以有效地保证数据的一致性,确保系统的正确运行。
在构建一个具有高可用性和高并发能力的分布式缓存系统时,以下几个方面是必须要考虑的:
一个缓存服务节点负责处理对应的数据分片,同时提供各类缓存操作的接口。通过使用多个缓存服务节点,我们可以构建一个缓存集群,从而提升系统的可扩展性和可用性。
共享存储设备用于存放缓存数据和元数据,这些数据能够被多个缓存服务节点所共享。常见的共享存储设备包括分布式文件系统、分布式块存储以及分布式对象存储等。
负载均衡设备的作用是将客户端的请求分发到适当的缓存服务节点,并且可以根据缓存服务节点的负载状况进行动态调整。
配置服务负责维护和管理缓存系统的元数据信息,包括节点信息、分片信息、负载均衡规则以及故障转移设置等。
一个分布式缓存系统的实现需要涉及到很多技术,如数据分片算法、负载均衡算法、容错处理算法、一致性算法等。
在分布式缓存系统中,数据分片算法是一项关键技术。常见的数据分片算法包括哈希算法、范围算法、顺序算法以及随机算法等。哈希算法作为最常用的分片算法之一,它能够通过哈希函数将数据的键值映射为一个数字,进而根据这个数字进行分片。
负载均衡策略在缓存系统中起着至关重要的作用。它需要综合考虑缓存节点的负载状况、网络拓扑以及客户端请求等多个因素。常见的负载均衡策略包括轮询策略、最少连接数策略、IP 哈希策略以及加权轮询策略等。
在分布式环境下,数据一致性是一个核心问题。当多个客户端同时对同一个键值进行读写操作时,需要确保数据的一致性。
常见的一致性算法包括 Paxos 算法、Raft 算法以及 Zab 算法等。
强一致性是指所有客户端在读取同一个键值时,都能得到相同的结果。而最终一致性是指,如果客户端在缓存节点中写入了数据,那么在未来的某个时间点,所有的缓存节点最终会拥有相同的数据。
强一致性可以通过复制数据来实现,但这可能会对系统的性能和可用性产生影响。最终一致性则可以通过异步复制和协议算法来实现,从而在一定程度上提高系统的性能和可用性。
在分布式缓存系统中,故障和异常是不可避免的。为了确保系统的可用性,我们需要采取一系列措施来处理故障和异常,如数据备份、故障转移、数据恢复以及监控告警等。同时,还需要定期对系统进行维护和优化,以确保系统的高可用性和高性能。
本文详细介绍了如何构建高可用的分布式缓存系统,包括架构基础知识、整体架构设计、实现原理、一致性模型以及故障处理等方面。一个优秀的缓存系统需要具备高可用性、高性能,并能保证数据的一致性和容错性。
微博作为拥有 1.6 亿 + 日活跃用户,每日访问量达百亿级的大型社交平台,其高效且不断优化的缓存体系在支撑庞大用户群的海量访问中起到了至关重要的作用。
首先是微博在运行过程中的数据挑战,然后是Feed系统架构,
接下来会着重分析Cache架构及演进,最后是总结、展望。
整个系统可以分为五个层次,最顶层是终端层,例如 Web 端、客户端(包括 iOS 和安卓设备)、开放平台以及第三方接入的接口。接下来是平台接入层,主要是为了将优质资源集中分配给关键核心接口,以便在突发流量时具备更好的弹性服务能力,提高服务稳定性。再往下是平台服务层,主要包括 Feed 算法、关系等。然后是中间层,通过各种中间介质提供服务。最底层是存储层,整个平台架构大致如此。
当我们在日常生活中使用微博时,例如在主页或客户端刷新一下,会看到最新的十到十五条微博。那么这个过程是如何实现的呢?
刷新操作会获取到用户的关注关系,例如1000个关注,就会获取这 1000 个 ID,根据这1000 个UID,获取每个用户发布的微博,同时会获取这个用户的Inbox,就是她收到的特殊的一些消息。
比如分组的一些微博,群的微博,下面她的关注关系,她关注人的微博列表,获取这一系列微博列表之后进行集合、排序,从而获取需要的微博 ID,再对这些ID去取每一条微博ID对应的微博内容。
如果这些微博是转发的,还会获取原微博,并根据原微博获取用户信息,通过原微博取用户信息,进一步根据用户的过滤词对这些微博进行过滤,过滤掉用户不想看到的微博,留下这些微博后,再进一步来看,用户对这些微博有没有收藏、赞,做一些flag设置,还会对这些微博各种计数,转发、评论、赞数进行组装,最后才把这十几条微博返回给用户的各种端。
这样看,用户一次请求会得到十几条记录,后端服务器需要对几百甚至几千条数据进行实时处理并返回给用户,整个过程对Cache体系强度依赖。因此,Cache架构设计优劣会直接影响微博系统的表现。
然后我们看一下Cache架构,它主要分为六层:
接下来,我们会重点讨论微博Cache架构演进过程,在微博刚上线时,我们把它作为一个简单的KV键值对数据类型来存储,我们主要采取哈希分片存储在MC池中,而,上线几个月后,我们发现了一些问题,比如由于某些节点机器宕机或其他原因,大量的请求会穿透Cache层达到DB上去,从而导致整个请求速度变慢,甚至DB僵死。
为了解决这个问题,我们迅速对系统进行了改造,增加了一个高可用(HA)层。这样,即使主层出现某些节点宕机或无法正常运行的情况,请求也会进一步穿透到 HA 层,而不会穿透到DB层,这样可以确保在任何情况下,系统的命中率都不会降低,从而显著提高系统服务的稳定性。
目前,这种做法在业界得到了广泛应用。然而,有些人直接使用哈希技术,这其实是有一些风险的。例如,如果一个节点(如节点 3)宕机了,主层会将其摘除,并将节点 3 的一些请求分配给其他节点。如果这个业务量不是很大,数据库可以承受住这种压力。但是,如果节点 3 恢复后重新加入,它的访问量会回来,如果因为网络或其他原因再次宕机,节点 3 的请求又会分配给其他节点。这时就可能会出现问题,之前分配给其他节点的请求已经没有人更新,如果没有被及时删除,就会出现数据混乱的情况。
微信和微博之间存在很大差异。实际上,微博更像是一个开放的广场型业务。例如,在突发事件或某明星恋情曝光等情况下,瞬间流量可能会激增至 30%。在这种情况下,大量的请求会集中出现在某些节点上,使得这些节点变得异常繁忙,即使使用 MC 也无法满足如此巨大的请求量。这时,整个 MC 层就会成为瓶颈,导致整个系统变慢。为了解决这个问题,我们引入了 L1 层,它实际上是一个主关系池。每个 L1 层的大小约为 Main 层的六分之一、八分之一或十分之一,具体取决于请求量。在请求量较大时,我们会增加 4 到 8 个 L1 层。这样,当请求到来时,首先会访问 L1 层。如果 L1 层命中,请求就会直接访问;如果没有命中,请求会继续访问 Main-HA 层。在突发流量情况下,L1 层可以承受大部分热请求,从而减轻微博的内存压力。对于微博来说,新数据会变得越来越热门,而只需要增加很少的内存,就可以应对更大的请求量。
总结一下,我们通过简单的 KV 数据类型存储,主要以 MC 为主,层内 HASH 节点不漂移,Miss 则穿透到下一层读取。通过多组 L1 读取性能提升,可以应对峰值和突发流量,同时降低成本。对于读写策略,我们采用多写,读的话采用逐层穿透,如果 Miss 就进行回写。对于存储的数据,我们最初采用 Json/xml,2012 年后直接采用 Protocol|Buffer 格式,对于较大的数据,我们使用 QuickL 进行压缩。
关于简单的 QA 数据,我们已经知道如何处理。但是对于复杂的集合类数据,例如关注了 2000 个人,新增一个人就涉及到部分修改。有一种方式是把 2000 个 ID 全部拿下来进行修改,这样会带来更大的带宽和机器压力。还有一些分页获取的需求,例如我只需要取其中的第几页,比如第二页,也就是第十到第二十个,能否不要全量把所有数据取回去。还有一些资源的联动计算,例如关注某些人里面的 ABC 也关注了用户 D,这种涉及到部分数据的修改、获取和计算,对于 MC 来说,它实际上并不擅长。所有的关注关系都存在 Redis 里面,通过 Hash 分布和储存,一组多存的方式来进行读写分离。现在 Redis 的内存大概有 30 个 T,每天都有 2-3 万亿的请求。
在使用 Redis 的过程中,我们还是遇到了一些其他问题。例如,从关注关系来看,我关注了 2000 个 UID,有一种方式是全量存储,但是微博有大量的用户,有些用户登陆比较少,有些用户特别活跃,这样全部放在内存里面成本开销是比较大的。所以我们就把 Redis 使用改成 Cache,只存活跃的用户,如果你最近一段时间没有活跃,会把你从 Redis 里面踢掉,再次有访问到你的时候再把你加进来。但是,Redis 的工作机制是单线程模式,如果它加某一个 UV,关注 2000 个用户,可能扩展到两万个 UID,两万个 UID 塞回去基本上 Redis 就卡住了,没办法提供其他服务。因此,我们扩展了一种新的数据结构,两万个 UID 直接开了端,写的时候直接依次把它写到 Redis 里面去,读写的整个效率就会非常高,它的实现是一个 long 型的开放数组,通过 Double Hash 进行寻址。
对于 Redis,我们还进行了一些其他的扩展。例如,之前的一些分享中,大家可以在网上看到,我们把数据放到公共变量里面,整个升级过程,我们测试 1G 的话加载要 10 分钟,10G 大概要十几分钟以上,现在是毫秒级升级。对于 AOF,我们采用滚动的 AOF,每个 AOF 是带一个 ID 的,达到一定的量再滚动到下一个 AOF 里面去。对于 RDB 落地的时候,我们会记录构建这个 RDB 时,AOF 文件以及它所在的位置,通过新的 RDB、AOF 扩展模式,实现全增量复制。
接下来,我们将讨论其他一些数据类型,例如计数。实际上,在互联网公司的每个领域,计数都是必不可少的。对于一些中小型业务,MC 和 Redis 已经足够满足需求。然而,在微博中,计数具有一些特殊性,例如一条微博可能有多个计数,包括转发数、评论数和点赞数。此外,一个用户可能有粉丝数、关注数等各种数字。由于计数的特性,其 Value size 通常较小,大约为 2-8 个字节,最常见的是 4 个字节。每天新增的微博大约有十亿条记录,总的记录数量则更为庞大。在一次请求中,可能需要返回数百条计数。
最初,我们选择使用 Memcached,但它存在一个问题,即当计数超过其容量时,会导致部分计数被剔除,或在宕机或重启后计数将丢失。此外,有许多计数为零,此时如何存储,是否需要存储,以及如何避免占用大量内存等问题需要考虑。微博每天有十亿计数,仅存储零值就会占用大量内存。如果不存储,可能会导致穿透到数据库层,从而影响服务性能。
从 2010 年开始,我们转向使用 Redis 进行访问。然而,随着数据量的不断增加,我们发现 Redis 的内存利用率相对较低。一条 KV 大约需要 65 个字节,但实际上我们只需要 8 个字节来存储一个计数,加上 Value 的 4 个字节,实际有效存储只有 12 个字节。其余的 40 多个字节被浪费。这还只是单个 KV 的情况,如果一个 Key 有多个计数,浪费的空间会更多。例如,四个计数,一个 Key 占 8 个字节,每个计数占 4 个字节,总共需要 16 个字节,但实际上只用了 26 个字节。
然而,使用 Redis 存储需要约 200 个字节。后来,我们通过自主研发 Counter Service,将内存使用量降低到 Redis 的五分之一至十五分之一以下。同时,我们实现了冷热数据分离,将热数据存储在内存中,将冷数据放入 LRU 中。当冷数据重新变热时,将其放到 RDB 和 AOF 中,实现全增量复制。通过这种方式,单机可以存储百亿级的热数据和千亿级的冷数据。
整个存储架构的概述如下:顶部是内存,底部是 SSD。内存中预先划分为 N 个 Table,每个 Table 根据 ID 的指针序列分配一定范围。当有新的 ID 过来时,首先找到它所在的 Table,然后进行增加或减少操作。当内存不足时,将一个小 Table 导出到 SSD 中,并保留新的位置以供新的 ID 使用。
有人可能会有疑问,如果在某个范围内,ID 的计数原本设定为 4 个字节,但由于微博的热度,计数超过了 4 个字节,变成了很大的一个计数,这种情况如何处理?对于超过限制的计数,我们将其存放在 Aux dict 中。对于存储在 SSD 中的 Table,我们有专门的 IndAux 进行访问,并通过 RDB 方式进行复制。
除了计数之外,微博还有一些业务,如存在性判断。例如,一条微博是否已获得点赞、阅读或推荐。如果用户已经阅读过该微博,就不再向其显示。这类数据的特点是,虽然每条记录非常小(例如,Value 只需 1 个 bit),但总数据量巨大。例如,微博每天发布约 1 亿条新微博,阅读量可能达到上百亿、上千亿。如何存储这些数据是一个大问题。而且其中许多存在性为 0。前面提到的问题再次出现:0 是否需要存储?如果存储,每天将存储上千亿条记录;如果不存储,大量请求将穿透 Cache 层到达 DB 层,任何 DB 都无法承受如此大的流量。
我们也进行了一些选型,首先直接考虑我们能不能用Redis,单条KV65个字节,一个KV可以8个字节的话,Value只有1个bit,这样算下来我每日新增内存有效率是非常低的。第二种我们新开发的Counter Service,单条KV Value1个bit,我就存1个byt,总共9个byt就可以了,这样每日新增内存900G,存的话可能就只能存最新若干天的,存个三天差不多快3个T了,压力也挺大,但比Redis已经好很多。
我们最终方案采用自己开发Phantom,先采用把共享内存分段分配,最终使用的内存只用120G就可以,算法很简单,对每个Key可以进行N次哈希,如果哈希的某一个位它是1,如果进行3次哈希,三个数字把它设为1,把X2也进行三次哈希,后面来判断X1是否存在的时候,进行三次哈希来看,如果都为1就认为它是存在的,如果某一个哈希X3,它的位算出来是0,那就百分百肯定不存在的。
它的实现架构比较简单,把共享内存预先拆分到不同Table里面,在里面进行开方式计算,然后读写,落地的话采用AOF+RDB的方式进行处理。整个过程因为放在共享内存里面,进程要升级重启数据也不会丢失。对外访问的时候,建Redis协议,它直接扩展新的协议就可以访问我们这个服务了。
小结一下,到目前为止,关注Cache集群内高可用、它的扩展性,包括它的性能,还有一个特别重要就是存储成本,还有一些我们没有关注到,比如21运维性如何,微博现在已经有几千差不多上万台服务器等等。
面向资源/组件管理
本地配置模式
常规峰值、突发流量
业务数据分类多
业务关联资源太多
采取的方案首先就是对整个Cache进行服务化管理,对配置进行服务化管理,避免频繁重启,另外如果配置发生变更,直接用一个脚本修改一下。
服务化还引入Cluster Manager,实现对外部的管理,通过一个界面来进行管理,可以进行服务校验。服务治理方面,可以做到扩容、缩容,SLA也可以得到很好保障。另外对于开发来说,现在就可以屏蔽Cache资源。
最后简单总结一下,对于微博Cache架构来说,从它数据架构、性能、储存成本、服务化不同方面进行优化增强。
https://blog.csdn.net/java_cpp_/article/details/130663371
https://blog.csdn.net/k6T9Q8XKs6iIkZPPIFq/article/details/108271182
《消息推送 架构设计》
《阿里2面:你们部署多少节点?1000W并发,当如何部署?》
《美团2面:5个9高可用99.999%,如何实现?》
《网易一面:单节点2000Wtps,Kafka怎么做的?》
《字节一面:事务补偿和事务重试,关系是什么?》
《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》
《亿级短视频,如何架构?》
《炸裂,靠“吹牛”过京东一面,月薪40K》
《太猛了,靠“吹牛”过顺丰一面,月薪30K》
《炸裂了…京东一面索命40问,过了就50W+》
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓