在地铁上,你可能经常使用微博浏览、点赞热门话题,甚至参与抽奖活动并转发相关内容。这些行为涉及到微博数据统计中的各种指标,主要包括:
- 微博的互动数据:评论数、点赞数、转发数、浏览数、表态数等;
- 用户的社交数据:粉丝数、关注数、发布微博数、私信数等。
微博维度的计数代表了一条微博在平台上的受欢迎程度,而用户维度的数据,特别是粉丝数,则反映了用户在微博社交网络中的影响力和受关注程度。这些计数信息对于用户和平台都具有重要意义
但在设计计数系统时,不少人会出现性能不高、存储成本很大的问题,比如,把计数与微博数据存储在一起,这样每次更新计数的时候都需要锁住这一行记录,降低了写入的并发。在我看来,之所以出现这些问题,还是因为你对计数系统的设计和优化不甚了解,所以要想解决痛点,你有必要形成完备的设计方案。
计数在业务上的特点
微博系统中微博条目的数量已经超过了千亿级别。仅仅计算微博的转发、评论、点赞、浏览等核心计数,其数据量级已经达到了几千亿的级别。而微博条目的数量还在不断高速增长,随着微博业务的不断发展,微博维度的计数种类也可能会持续扩展(比如增加了表态数)。因此,仅仅是微博维度上的计数量级就已经过了万亿级别。
此外,微博的用户量级已经超过了 10 亿,用户维度的计数量级相比微博维度来说虽然相差很大,但也达到了百亿级别。面对如此庞大的数据量,如何存储这些过万亿级别的数字,对我们来说确实是一大挑战。
考虑到访问量大和性能要求高的情况,对于微博这样拥有数亿活跃用户的社交平台来说,计数系统需要能够应对每秒数百万次的访问量,同时要求在毫秒级别内返回结果。为了达到这样的性能要求,我们可以采取一些简单而有效的方法,比如选择高性能的存储和缓存技术,优化数据库设计和查询,采用分布式架构,以及设置负载均衡和故障恢复机制。这样可以保证系统在高并发情况下仍然能够快速、稳定地处理大量请求,满足用户的需求
支撑高并发的计数系统要如何设计
在最初设计计数系统时,微博的流量还没有现在这么庞大。我们遵循了KISS(Keep It Simple and Stupid)原则,选择了使用MySQL来存储计数数据。这是因为MySQL是我们团队最熟悉的数据库,我们在运维方面也有丰富的经验。举个具体的例子来说,我们将微博的计数数据存储在MySQL数据库中的单个表中,每个微博对应一行记录,包括评论数、点赞数、转发数等计数数据列。这样的设计简单易于实现和维护,符合我们当时的需求和团队的技术水平。
以微博 ID 为主键,然后将转发数、评论数、点赞数和浏览数等微博维度的计数数据分别存储在单独的列中,这样可以方便地通过一条SQL语句来获取特定微博的计数数据。例如:
select repost_count, comment_count, praise_count, view_count from t_weibo_count where weibo_id = ?
在数据量级和访问量级都不大的情况下,采用以微博ID为主键,将转发数、评论数、点赞数和浏览数等计数数据存储在单个MySQL表中的方式是最简单的。但随着微博的不断壮大,之前的计数系统面临了诸多问题和挑战。
随着微博用户数量和发布的微博数量迅速增加,计数数据量级也随之飞速增长。当MySQL数据库单表的存储量级达到几千万时,性能会受到损耗。因此,为了解决这些问题,我们考虑采用分库分表的方式,将数据量分散存储,以提升读取计数数据的性能。
我们用“weibo_id”作为分区键,在选择分库分表的方式时,考虑了下面两种:
对于分库分表的方式,有两种常见的策略可以考虑。一种是根据微博ID进行哈希分库分表,另一种是根据微博ID生成的时间来进行分库分表。
首先,根据哈希算法对weibo_id计算哈希值,然后根据这个哈希值确定需要存储到哪一个数据库的哪一张表中。这种方法可以将数据均匀地分散到多个数据库和表中,以实现负载均衡和提升读取性能。
另一种方式是按照weibo_id生成的时间来进行分库分表。可以利用发号器生成的ID中的时间戳信息,将微博数据按照时间戳进行分库分表,比如每天一张表或者每月一张表等。这样可以根据微博的发布时间快速定位到对应的数据库和表,便于数据的管理和查询。
因为越是最近发布的微博,计数数据的访问量就越大,所以虽然我考虑了两种方案,但是按照时间来分库分表会造成数据访问的不均匀,最后用了哈希的方式来做分库分表。
图片
在微博最初的版本中,首页信息流并不展示计数数据,因此使用MySQL可以承受当时的计数数据读取访问量。但随着微博的发展,首页信息流也开始展示转发、评论和点赞等计数数据,导致信息流的访问量急剧增加。仅仅依靠数据库已无法满足如此高的并发读取需求。
为了应对这一挑战,我们考虑使用Redis来加速读请求。通过部署多个Redis从节点来提升可用性和性能,并通过Hash的方式对数据进行分片,以保证计数的读取性能。然而,采用数据库+缓存的方式存在一个严重的弊端:无法保证数据的一致性。例如,如果数据库写入成功而缓存更新失败,就会导致数据不一致,从而影响计数的准确性。
因此,为了解决数据一致性的问题,我们最终决定完全抛弃MySQL,全面采用Redis作为计数的存储组件。Redis的高性能和内存存储特性使其能够轻松应对高并发的读取请求,并且通过持久化机制和主从复制,可以保证数据的持久性和可用性,同时也降低了数据不一致的风险。
图片
针对热门微博高频写入的情况,可以考虑以下简单的方法来降低写入压力:
- 异步处理: 将计数写入操作异步化,先将操作记录在消息队列中,再由后台任务异步处理写入计数数据,减轻数据库的写入压力。
- 计数缓存: 使用缓存暂时存储计数数据,减少对数据库的直接写入请求,提高写入性能。
- 合并写入: 将相同微博的计数操作合并,减少数据库的写入次数,如多个用户同时转发同一条微博时,将转发操作合并为一次写入计数数据的操作。
- 分片存储: 根据微博ID进行分片存储,将数据分散到不同存储节点上,分散写入压力。
- 写入限流: 实行写入限流策略,限制每个用户或微博的写入频率,防止写入请求过载数据库。
图片
如何降低计数系统的存储成本
在微博这样的场景下,我们面临着处理万亿级别计数数据的挑战。对于这种规模的数据存储,我们需要在有限的成本下实现全量计数数据的存取。Redis作为内存存储系统,相较于使用磁盘存储的MySQL,存储成本差异巨大。举例来说,一台服务器可以挂载2TB的磁盘,但内存可能只有128GB,这意味着磁盘存储空间是内存的16倍。
Redis因其通用性而对内存的使用较为粗放,存在大量指针和额外数据结构开销。比如,若要存储一个KV类型的计数信息,键(Key)是8字节的长整型weibo_id,值(Value)是4字节整型的转发数,在Redis中将会占用超过70个字节的空间,这造成了空间的巨大浪费。
在面对这一问题时,如何优化存储空间呢?
我建议对原生Redis进行改造,采用新的数据结构和数据类型来存储计数数据。我的改造主要涉及两点:
首先,原生Redis在存储Key时是按照字符串类型来存储的。比如,一个8字节的Long类型的数据,需要28个字节的存储空间(8字节的字符串头部信息 + 19字节的数字长度 + 1字节的字符串结尾标志)。如果我们直接使用Long类型来存储,只需要8个字节,节省了20个字节的空间。
其次,我去除了原生Redis中多余的指针。现在,如果要存储一个键值对(KV)信息,只需要12个字节(8字节的weibo_id + 4字节的转发数),相比之前有很大的改进。
同时,我们也会使用一个大的数组来存储计数信息,存储的位置是基于 weibo_id 的哈希值来计算出来的,具体的算法像下面展示的这样:
同时,我们也会使用一个大的数组来存储计数信息,存储的位置是基于 weibo_id 的哈希值来计算出来的,具体的算法像下面展示的这样:
在对原生Redis进行改造后,我们还需要进一步考虑如何节省内存的使用。举例来说,微博的计数数据包括转发数、评论数、浏览数、点赞数等等。如果每个计数都需要存储weibo_id,那么总共需要的存储空间是48字节(8字节的weibo_id * 4个微博ID + 每个计数4字节)。
然而,我们可以将相同微博ID的计数数据存储在一起,这样就只需要记录一个微博ID,省去了多余的三个微博ID的存储开销。这样一来,存储空间就进一步减少了。
不过,即使经过上面的优化,由于计数的量级实在是太过巨大,并且还在以极快的速度增长,所以如果我们以全内存的方式来存储计数信息,就需要使用非常多的机器来支撑。
针对微博计数数据具有明显的热点属性的情况,我们考虑优化计数服务,增加SSD磁盘,将时间上较久远的数据存储在磁盘上,内存中只保留最近的数据,以尽量减少服务器的使用。
具体做法是,将较久远的计数数据dump到SSD磁盘上,而内存中仅保留最近的数据。当需要读取冷数据时,使用单独的I/O线程异步地从SSD磁盘加载冷数据到一个单独的Cold Cache中。
经过以上优化措施,我们的计数服务现在已经能够支撑高并发大数据量的考验,无论是在性能、成本还是可用性方面都能够满足业务需求。通过微博设计计数系统的例子,我想强调的是,在系统设计过程中需要了解当前系统面临的痛点,并针对这些痛点进行细致的优化。
举例来说,微博计数系统的痛点是存储成本。因此,我们在后期的优化中主要围绕如何使用有限的服务器存储全量的计数数据展开。即使对开源组件(如Redis)进行深度定制可能会增加运维成本,但这些优化都被视为实现计数系统的必要权衡。通过深入了解系统痛点并针对性地进行优化,我们能够更好地提高系统的性能、降低成本,并确保系统的可用性。
总结
数据库 + 缓存的方案是计数系统的初级阶段,完全可以支撑中小访问量和存储量的存储服务。如果你的项目还处在初级阶段,量级还不是很大,那么你一开始可以考虑使用这种方案。
通过对原生 Redis 组件的改造,我们可以极大地减小存储数据的内存开销。
使用 SSD+ 内存的方案可以最终解决存储计数数据的成本问题。这个方式适用于冷热数据明显的场景,你在使用时需要考虑如何将内存中的数据做换入换出。
随着互联网技术的发展,越来越多的业务场景需要大量的内存资源来存储业务数据,但对性能或延迟要求不高。全内存存储会带来极大的成本浪费,因此一些开源组件开始支持使用SSD替代内存存储冷数据,比如Pika和SSDB。我建议您了解它们的实现原理,以便在需要时在项目中使用。
在微博的计数服务中也采用了类似的思路,将热点数据存储在内存中,而将冷数据存储在SSD上,这样既保证了性能,又降低了成本。如果您的业务需要大量内存存储热点数据,不妨考虑采用类似的思路来优化您的系统。