Redis实战:如何构建类微博的亿级社交平台
微博及 Twitter 这两大社交平台都重度依赖 Redis 来承载海量用户访问。本文介绍如何使用 Redis 来设计一个社交系统,以及如何扩展 Redis 让其能够承载上亿用户的访问规模。
虽然单台 Redis 具备极佳的性能,但随着系统规模增大,单台服务器不能存储所有数据、以及没办法处理所有读写请求的问题迟早都会出现,这时我们就需要对 Redis 进行扩展,让它能够满足需求。
在介绍如何扩展之前,我们先看下如何用 Redis 来搭建一个社交平台。
使用 Redis 搭建社交平台
用 Redis 来搭建一个社交平台,需要首先考虑以下几个核心功能。
1. 已发表微博
可以使用 Redis 的 hash 来保存已发表微博。
一条微博通常包括多个字段,比如发表时间、发表用户、正文内容等,通常使用微博 id 作为 key 将多个键值对作为 hash 保存在 Redis 中。
2. 信息流
当一个用户访问它的首页信息流时候,他可以看到他所有关注用户最新的信息。key 是当前用户的 uid, 信息流的内容以 id / timestamp 的形式保存在 zset 中,timestamp 用于排序,以便返回的列表是按照时间顺序排列。微博的 id 用于业务下一步获取微博的相关信息。
3. 关注与粉丝
我们可以把关注及粉丝库也存在 zset 中,依旧使用 timestamp 来排序。key 是当前用户 uid。
了解上述结构之后,我们继续来看如何使用 Redis 来扩展整个系统,具备处理亿级用户的能力。
我们首先要做的,就是在 Redis 能够存储所有数据并且能够正常地处理写查询的情况下,让 Redis 的读查询处理能力超过单台 Redis 服务器所能提供的读查询处理能力。
扩展读性能
假定我们用 Redis 构建一个与微博或 Twitter 具有相同特性和功能的社交网站,网站的其中一个特性就是允许用户查看他们自己的 profile 页和个人首页信息流,每当用户访问时,程序就会从信息流里面获取大约 30 条内容。
因为一台专门负责获取信息流的 Redis 服务器每秒至少可以同时为 3,000 ~ 10,000 个用户获取信息流消息,所以这一操作对于规模较小的社交网站来说并不会造成什么问题。
但是对于规模更大的社交网站来说,程序每秒需要获取的信息流消息数量将远远超过单台 Redis 服务器所能处理的上限,因此我们必须想办法提升 Redis 每秒能够获取的信息流消息数量。
下面我们将会讨论如何使用只读的从服务器提升系统处理读查询的性能,使得系统的整体读性能能够超过单台 Redis 服务器所能提供的读查询性能上限。
在对读查询的性能进行扩展,并将额外的服务器用作从服务器以提高系统处理读查询的性能之前,让我们先来回顾一下 Redis 提高性能的几个途径。
在使用短结构时,请确保压缩列表的最大长度不会太大以至于影响性能。
根据程序需要执行的查询的类型,选择能够为这种查询提供最好性能的结构。比如说,不要把 LIST 当作 SET 使用;也不要获取整个 HASH 然后在客户端里面对其进行排序,而是应该直接使用 ZSET;诸如此类。
在将大体积的对象缓存到 Redis 之前,考虑对它进行压缩以减少读取和写入对象时所需的网络带宽。对比压缩算法 lz4、gzip 和 bzip2,看看哪个算法能够对被存储的数据提供最好的压缩效果和最好的性能。
使用 pipeline(pipeline 是否启用事务性质由具体的程序决定)以及连接池。
在做好了能确保读查询和写查询能够快速执行的一切准备之后,接下来要考虑的就是如何实际解决“怎样才能处理更多读请求”这个正题。
提升 Redis 读取能力的最简单方法,就是添加提供读能力的从服务器。
用户可以运行一些额外的服务器,让它们与主服务器进行连接,然后接受主服务器发送的数据副本并通过网络进行准实时的更新(具体的更新速度取决于网络带宽)。通过将读请求分散到不同的从服务器上面进行处理,用户可以从新添加的从服务器上获得额外的读查询处理能力。
记住:只对主服务器进行写入
在使用只读从服务器的时候,请务必记得只对 Redis 主服务器进行写入。在默认情况下,尝试对一个被配置为从服务器的 Redis 服务器进行写入将引发一个错误(就算这个从服务器是其他从服务器的主服务器,也是如此)。
简单来说,要将一个 Redis 服务器变为从服务器,我们只需要在 Redis 的配置文件里面,加上一条slaveof host port语句,并将 host 和 port 两个参数的值分别替换为主服务器的 IP 地址和端口号就可以了。除此之外,我们还可以通过对一个正在运行的 Redis 服务器发送SLAVEOF host port命令来把它配置为从服务器。需要注意的一点是,当一个从服务器连接至主服务器的时候,从服务器原本存储的所有数据将被清空。最后,通过向从服务器发送SLAVEOF no one命令,我们可以让这个从服务器断开与主服务器的连接。
使用多个 Redis 从服务器处理读查询时可能会遇到的最棘手的问题,就是主服务器临时下线或者永久下线。每当有从服务器尝试与主服务器建立连接的时候,主服务器就会为从服务器创建一个快照,如果在快照创建完毕之前,有多个从服务器都尝试与主服务器进行连接,那么这些从服务器将接收到同一个快照。从效率的角度来看,这种做法非常好,因为它可以避免创建多个快照。
但是,同时向多个从服务器发送快照的多个副本,可能会将主服务器可用的大部分带宽消耗殆尽。使主服务器的延迟变高,甚至导致主服务器已经建立了连接的从服务器断开。
解决从服务器重同步(resync)问题的其中一个方法,就是减少主服务器需要传送给从服务器的数据数量,这可以通过构建树状复制中间层来完成。
(图:一个 Redis 主从复制树示例,树的最底层由 9 个从服务器组成,而中间层则由 3 个复制辅助服务器组成)
从服务器树非常有用,在对不同数据中心(data center)进行复制的时候,这种从服务器树甚至是必需的:通过缓慢的广域网(WAN)连接进行重同步是一件相当耗费资源的工作,这种工作应该交给位于中间层的从服务器去做,而不必劳烦最顶层的主服务器。但是另一方面,构建从服务器树也会带来复杂的网络拓扑结构(topology),这增加了手动和自动处理故障转移的难度。
除了构建树状的从服务器群组之外,解决从服务器重同步问题的另一个方法就是对网络连接进行压缩,从而减少需要传送的数据量。一些 Redis 用户就发现使用带压缩的 SSH 隧道(tunnel)进行连接可以明显地降低带宽占用,比如某个公司就曾经使用这种方法,将复制单个从服务器所需的带宽从原来的 21Mbit 降低为 1.8Mbit(http://mng.bz/2ivv)。如果读者也打算使用这个方法的话,那么请记得使用 SSH 提供的选项来让 SSH 连接在断线后自动重连。
加密和压缩开销
一般来说,使用 SSH 隧道带来的加密开销并不会给服务器造成大的负担,因为2.6 GHz 主频的英特尔酷睿 2 单核处理器在只使用单个处理核心的情况下,每秒能够使用 AES-128 算法加密 180MB 数据,而在使用 RC4 算法的情况下,每秒则可以加密大约 350MB 数据。在处理器足够强劲并且拥有千兆网络连接的情况下,程序即使在加密的情况下也能够充分地使用整个网络连接。
唯一可能会出问题的地方是压缩—因为 SSH 默认使用的是 gzip 压缩算法。SSH 提供了配置选项,可以让用户选择指定的压缩级别(具体信息可以参考SSH的文档),它的 1 级压缩在使用之前提到的 2.6GHz 处理器的情况下,可以在复制的初始时候,以每秒 24~52MB 的速度对 Redis 的 RDB 文件进行压缩;并在复制进入持续更新阶段之后,以每秒 60~80MB 的速度对 Redis 的 AOF 文件进行压缩。
使用 Redis Sentinel
Redis Sentinel 可以配合 Redis 的复制功能使用,并对下线的主服务器进行故障转移。Redis Sentinel 是运行在特殊模式下的 Redis 服务器,但它的行为和一般的 Redis 服务器并不相同。
Sentinel 会监视一系列主服务器以及这些主服务器的从服务器,通过向主服务器发送PUBLISH命令和SUBSCRIBE命令,并向主服务器和从服务器发送PING命令,各个 Sentinel 进程可以自主识别可用的从服务器和其他 Sentinel。
当主服务器失效的时候,监视这个主服务器的所有 Sentinel 就会基于彼此共有的信息选出一个 Sentinel,并从现有的从服务器当中选出一个新的主服务器。当被选中的从服务器转换成主服务器之后,那个被选中的 Sentinel 就会让剩余的其他从服务器去复制这个新的主服务器(在默认设置下,Sentinel 会一个接一个地迁移从服务器,但这个数量可以通过配置选项进行修改)。
一般来说,使用 Redis Sentinel 的目的就是为了向主服务器属下的从服务器提供自动故障转移服务。此外,Redis Sentinel 还提供了可选的故障转移通知功能,这个功能可以通过调用用户提供的脚本来执行配置更新等操作。
更深入了解 Redis Sentinel 可以阅读http://redis.io/topics/sentinel
在了解如何扩展读性能的方法之后,接下来我们该考虑如何扩展写性能了。
扩展写性能和内存容量
随着被缓存的数据越来越多,当数据没办法被存储到单台机器上面的时候,我们就需要想办法把数据分割存储到由多台机器组成的集群里面。
扩展写容量
尽管这一节中讨论的是如何使用分片来增加可用内存的总数量,但是这些方法同样可以在一台 Redis 服务器的写性能到达极限的时候,提升 Redis 的写吞吐量。
在对写性能进行扩展之前,首先需要确认我们是否已经用尽了一切办法去降低内存占用,并且是否已经尽可能地减少了需要写入的数据量。
对自己编写的所有方法进行了检查,尽可能地减少程序需要读取的数据量。
将无关的功能迁移至其他服务器。
在对 Redis 进行写入之前,尝试在本地内存中对将要写入的数据进行聚合计算,这一做法可以应用于所有分析方法和统计计算方法。
使用锁去替换可能会给速度带来限制的 WATCH/MULTI/EXEC 事务,或者使用 Lua 脚本。
在使用 AOF 持久化的情况下,机器的硬盘必须将程序写入的所有数据都存储起来,这需要花费一定的时间。对于 400,000 个短命令来说,硬盘每秒可能只需要写入几 MB 的数据;但是对于 100,000 个长度为 1KB 的命令来说,硬盘每秒将需要写入100MB 的数据。
如果用尽了一切方法降低内存占用并且尽可能地提高性能之后,问题仍然未解决,那么说明我们已经遇到了只使用单台机器带来的瓶颈,是时候将数据分片到多台机器上面了。
本文介绍的数据分片方法要求用户使用固定数量的 Redis 服务器。举个例子,如果写入量预计每 6 个月就会增加 4 倍,那么我们可以将数据预先分片(preshard)到 256 个分片里面,从而拥有一个在接下来的 2 年时间里面都能够满足预期写入量增长的分片方案(具体要规划多长远的方案要由你自己决定)。
为了应对增长而进行预先分片
在为了应对未来可能出现的流量增长而对系统进行预先分片的时候,我们可能会陷入这样一种处境:目前拥有的数据实在太少,按照预先分片方法计算出的机器数量去存储这些数据只会得不偿失。为了能够如常地对数据进行分割,我们可以在单台机器上面运行多个 Redis 服务器,并将每个服务器用作一个分片。
注意,在同一台机器上面运行多个 Redis 服务器的时候,请记得让每个服务器都监听不同的端口,并确保所有服务器写入的都是不同的快照文件或 AOF 文件。
在单台机器上面运行多个 Redis 服务器
上面介绍了如何将写入命令分片到多台服务器上面执行,从而增加系统的可用内存总量并提高系统处理写入操作的能力。但是,如果你在执行诸如搜索和排序这样的复杂查询时,感觉系统的性能受到了 Redis 单线程设计的限制,而你的机器又有更多的计算核心、更多的通信网络资源,以及更多用于存储快照文件和 AOF 文件的硬盘 I/O,那么你可以考虑在单台机器上面运行多个 Redis 服务器。你需要做的就是对位于同一台机器上面的所有服务器进行配置,让它们分别监听不同的端口,并确保它们拥有不同的快照配置或 AOF 配置。
扩展复杂的业务场景
在对各式各样的 Redis 服务进行扩展的时候,常常会遇到这样一种情况:因为服务执行的查询并不只是读写那么简单,所以只对数据进行简单分片并不足以满足复杂业务场景的需求。
对社交网站进行扩展
下面介绍如何对类似微博或者 Twitter 这样的社交网站进行扩展,介绍的目的是为了让我们更好的理解使用什么样的数据结构及方法来构建一个大型社交网络,这些方法几乎可以无限制地进行——只要资金允许,我们可以将一个社交网站扩展至任意规模。
对社交网站进行扩展的第一步,就是找出经常被读取的数据以及经常被写入的数据,并思考是否有可能将常用数据和不常用数据分开。
首先,假设我们已经把用户已发表的微博放在一个独立的 Redis 服务器,并使用只读的从服务器处理针对这些数据进行大量读取操作。那么一个社交网站上需要进行扩展的主要是两个类型的数据:信息流、关注及粉丝列表。
扩展已发表微博的数据库
当你的社交网站获得一定的访问量之后,我们需要对存储已发表微博的数据库做进一步的扩展,而不仅仅只添加从服务器。
因为每条微博都完整地存储在一个单独的 HASH 里面,所以程序可以很容易地基于散列所在的键,把各条微博 hash 分片到由多个 Redis 服务器组成的集群里面。
因为对每条微博 hash 进行分片并不困难,所以分片的工作应该并不难完成。扩展微博数据库的另一种方法,就是将 Redis 用作缓存,并把最新发布的消息存储到 Redis 里,而较旧(也就是较少读取)的消息则存储到以硬盘存储为主的服务器里面,像 PostgreSQL、MySQL、Riak、MongoDB 等。
在一个社交网站上,主要的信息流有 3 种:用户首页的信息流、profile 信息流以及分组信息流。各个信息流本身都是相似的,所以我们将使用相同的处理方式。
下面我们来看社交系统中最核心的两种系统如何通过不同的分片策略对其进行扩展。
1.对信息流列表进行分片
标题所说的“对信息流进行分片”实际上有些词不达意,因为首页信息流和分组列表信息流通常都比较短(最大通常只有 1,000 条,实际的数量由zset-max-ziplist-size选项的值决定),因此实际上并不需要对信息流的内容进行分片;我们真正要做的是根据键名,把不同的信息流分别存储到不同的分片上面。
另一方面,社交网站每个用户 profile 信息流通常无限增长的。尽管绝大多数用户每天最多只会发布几条微博,但也有话痨用户以明显高于这一频率的速度发布大量信息。以 Twitter 为例,该网站上发布信息最多的 1,000 个用户,每人都发布了超过 150,000 条推文,而其中发布最多的 15 个用户,每人都发布了上百万条推文。
从实用性的角度来看,一个合乎情理的做法是限制每个用户的已发表微博最多只能存储大约 20,000 条信息,并将最旧的信息删除或者隐藏——这种做法足以处理 99.999% 的 Twitter 用户,而我们也会使用这一方案来对社交网站的个人信息流进行扩展。扩展个人信息流的另一种方法,就是使用本节稍后介绍的关注库进行扩展的技术。
2.通过分片对关注及粉丝列表扩展
虽然对信息流进行扩展的方法相当直观易懂,但是对关注和粉丝列表这些由有序集合构成的“列表”进行扩展却并不容易。这些有序集合绝大多数都很短(如 Twitter 上 99.99% 的用户的关注者都少于 1,000 人),但是也存在少量用户的列表非常大,他们关注了非常多的人或者拥有数量庞大的粉丝。
从实用性的角度来考虑,一个合理的做法是给用户以及分组可以关注的人数设置一个上限(比如新浪微博普通用户最大允许关注 2,000 用户)。不过这个方法虽然可以控制用户的关注人数,但是仍然解决不了单个用户的粉丝数人数过多的问题。
为了处理关注和粉丝列表变得非常巨大的情况,我们需要将实现这些列表的有序集合划分到多个分片上面,说得更具体一样,也就是根据分片的数量把用户的粉丝划分为多个部分,存在多个 zset 中。为此,我们需要为ZADD命令、ZREM命令和ZRANGEBYSCORE命令实现特定的分片版本。
和信息流分片的区别是,这次分片的对象是数据而不是键。此外,为了减少程序创建和调用连接的数量,把关注和粉丝的数据放置在同一个分片里面将是一种非常有意义的做法。因此这次我们将使用新的方法对数据进行分片。
为了能够在关注及粉丝数据进行分片的时候,把两者数据都存储到同一个分片里面,程序将会把关注者和被关注者双方的 ID 用作查找分片键的其中一个参数。
总结
本章对各式各样的程序进行了回顾,介绍了一些对它们进行扩展以处理更多读写流量并获得更多可用内存的方法,其中包括使用只读从服务器、使用可以执行写查询的从服务器、使用分片以及使用支持分片功能的类和函数。尽管这些方法可能没有完全覆盖读者在扩展特定程序时可能会遇到的所有问题,但是这些例子中展示的每项技术都可以广泛地应用到其他情景里面。
本文希望向读者传达这样一个概念:对任何系统进行扩展都是一项颇具挑战性的任务。但是通过 Redis,我们可以使用多种不同的方法来对平台进行扩展,从而把平台扩展成我们想要的规模。
本文节选自人民邮电出版社《Redis 实战》第 8、10 章,由聚焦 Redis 领域的黄健宏翻译,感兴趣的读者可以在各大书店购买。以下是翻译过程中的一张照片,看出译者对质量非常用心。
图片来源:http://blog.huangz.me/diary/2015/memories-of-redis-in-action-translation.html
人邮也新开了公众号「人邮 IT 书坊」长期提供最新 IT 图书资讯,欢迎关注。
更多精彩,关注人邮IT书坊
一个为程序员提供干货、书讯、赠书活动的地方
参考阅读
本文由人民邮电出版社信息技术分社授权「高可用架构」发表。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。
高可用架构
改变互联网的构建方式
Redis实战:如何构建类微博的亿级社交平台的更多相关文章
- 支撑微博亿级社交平台,小白也能玩转Redis集群(实战篇)
上篇文章<支撑微博亿级社交平台,小白也能玩转Redis集群(原理篇)>介绍了Redis集群相关原理,这篇文章将介绍Redis Cluster集群的搭建.配置,运维.扩容等具体操作 集群搭建 ...
- 支撑微博亿级社交平台,小白也能玩转Redis集群(原理篇)
Redis作为一款性能优异的内存数据库,支撑着微博亿级社交平台,也成为很多互联网公司的标配.这里将以Redis Cluster集群为核心,基于最新的Redis5版本,从原理再到实战,玩转Redis集群 ...
- Redis系列9:Geo 类型赋能亿级地图位置计算
Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...
- Redis实战--使用Jedis实现百万数据秒级插入
echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!!! 当我们 ...
- 子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践
本文原文内容来自InfoQ的技术分享,本次有修订.勘误和加工,感谢原作者的分享. 1.前言 自从2018年8月20日子弹短信在锤子发布会露面之后(详见<老罗最新发布了“子弹短信”这款IM,主打熟 ...
- Netty Redis 亿级流量 高并发 实战 (长文 修正版)
目录 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -30[ 博客园 总入口 ] 写在前面 1.1. 快速的能力提升,巨大的应用价值 1.1.1. 飞速提升能力,并且满足实际开发要求 1 ...
- 亿级 ELK 日志平台构建部署实践
本篇主要讲工作中的真实经历,我们怎么打造亿级日志平台,同时手把手教大家建立起这样一套亿级 ELK 系统.日志平台具体发展历程可以参考上篇 「从 ELK 到 EFK 演进」 废话不多说,老司机们座好了, ...
- 千亿级平台技术架构:为了支撑高并发,我把身份证存到了JS里
@ 目录 一.用户信息安全规范 1.1 用户信息.敏感信息定义及判断依据 1.1.1 个人信息 1.1.2 个人敏感信息 1.2 用户信息存储的注意事项 二.框架技术实现 2.1 用户敏感信息自 ...
- Redis 实战篇:巧用Bitmap 实现亿级海量数据统计
在移动应用的业务场景中,我们需要保存这样的信息:一个 key 关联了一个数据集合. 常见的场景如下: 给一个 userId ,判断用户登陆状态: 显示用户某个月的签到次数和首次签到时间: 两亿用户最近 ...
随机推荐
- 云服务器搭建 Nginx 静态网站
第一步:安装 Nginx 在 CentOS 上,可直接使用 yum 来安装 Nginx(当然也可以通过下载压缩包.解压.编译的方式安装,不过太麻烦了) yum install nginx -y 第二步 ...
- Qt浅谈之一:内存泄露(总结),对于QWidget可以setAttribute(Qt::WA_DeleteOnClose),而且绝对不能手动删除栈上的对象
一.简介 Qt内存管理机制:Qt 在内部能够维护对象的层次结构.对于可视元素,这种层次结构就是子组件与父组件的关系:对于非可视元素,则是一个对象与另一个对象的从属关系.在 Qt 中,在 Qt 中,删除 ...
- Emacs常用快捷键
基本命令 C-x C-f 打开/新建文件 C-x C-s 保存当前缓冲区 C-x C-w 当前缓冲区另存为 C-x C-v 关闭当前Buffer并打开新文件 C-x i 光标处插入文件 C-x b 切 ...
- width:100%和width:inherit
前几天遇到过这么一个问题.我想让子盒子的宽度等于父盒子的宽度.父盒子宽度为一个具体值比如说200px.我将子盒子宽度设为了100%.按道理说应该是可以等于父盒子的宽度的,但结果并没有,而是通栏了.然后 ...
- iOS进阶路线以及进阶书籍
第一,熟悉ARC机制:首先要了解ARC的前世今生.假设了解不清楚会导致两种可能,1,一个对象的引用莫名奇异为空.或失效了.这个一般都能在开发阶段及时发现,由于会导致应用异常.2.导致内存溢出:不了解A ...
- Java随机验证吗
<span style="font-size:18px;">package com.java.process.jsp; import java.awt.Color; i ...
- swift学习第十二天:类的属性定义
类的属性介绍 Swift中类的属性有多种 存储属性:存储实例的常量和变量 计算属性:通过某种方式计算出来的属性 类属性:与整个类自身相关的属性 存储属性 存储属性是最简单的属性,它作为类实例的一部分, ...
- Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others)
Crazy Bobo Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others) Tota ...
- 2015第30周四Java日志组件
Java 日志 API 从功能上来说,日志 API 本身所需求的功能非常简单,只需要能够记录一段文本即可.API 的使用者在需要进行记录时,根据当前的上下文信息构造出相应的文本信息,调用 API 完成 ...
- js获取浏览器和元素对象的尺寸
1.屏幕尺寸 window.screen.height //屏幕分辨率的高 window.screen.width //屏幕分辨率的宽 window.screen.availHeight //屏幕可用 ...