微博是从 2010 年开始引入 Redis ,现在 Redis 已经广泛应用于微博的多个业务场景,如关系、计数、通知提醒等,目前 Redis 集群存储超过百亿记录,每天上万亿的读取访问。随着业务的快速发展,我们在使用过程中碰到的问题及解决方法给大家做一个分享。主要包括以下方面: 实现机制高可用、业务极致定制以及服务化。
Redis 2.0 时代(2010 - 2011)
实现机制高可用优化
微博最早使用的是 Redis 2.0 版本,在初期业务规模不大的时候, Redis 服务运行比较稳定。但是随着业务数据量和访问量的增加,一些问题逐渐暴露出来:
持久化问题
在我们大多数业务场景中 Redis 是当做存储来使用,会开启持久化机制。线上采用单机多实例的部署结构,服务器的内存使用率也会比较高。由于官方版本触发 bgsave和 bgrewriteaof 操作的时间点是不可控的,依赖于相关的配置项和业务的写入模型,因此可能会出现单机部署的多个 Redis 实例同时触发 bgsave 或 bgrewriteaof 操作,这两个操作都是通过 fork 出一个子进程来完成的,由于 copy-on-write 机制,可能会导致服务器内存很快耗尽, Redis 服务崩溃。
此外在磁盘压力较大时(生成 rdb、aof 重写),对 aof 的写入及 fsync 操作可能会出现阻塞,虽然从 2.4 版本开始 fsync 操作调整到 bio 线程来做,主线程 aof 的写入阻塞仍会导致服务阻塞。
主从同步问题
为了提高服务可用性,避免单点问题,我们线上业务 Redis 大多采用主从结构部署。官方版本的主从同步机制,在网络出现问题时(如瞬断),会导致主从重新进行一次全量复制。对单个端口来说,如果数据量小,那么这个影响不大,而如果数据量比较大的话,就会导致网络流量暴增,同时 slave 在加载 rdb 时无法响应任何请求。当然官方 2.8 版本支持了 psync 增量复制的机制,一定程度上解决了主从连接断开会引发全量复制的问题,但是这种机制受限于复制积压缓冲区大小,同时在主库故障需要执行切主操作场景下,主从仍然需要进行全量复制。
版本升级及管理问题

早期 Redis 版本运行不够稳定,经常需要修复 bug、支持新的运维需求及版本优化,导致版本迭代很频繁。官方版本在执行升级操作时,需要服务重启,我们大多数线上业务都开启了持久化机制,重启操作耗时较长,加上使用 Redis 业务线比较多,版本升级操作的复杂度很高。由于统一版本带来的运维工作量实在太高,线上 Redis 版本曾经一度增加到十几个,给版本管理也带来很大的困难。
为了解决以上问题我们对 Redis 原生实现机制做了以下优化:
1. 对于持久化机制,采用 rdb + aof 的持久化方式。
aof 文件按固定大小滚动,生成 rdb 文件时记录当前 aof 的 position,全量的数据包含在 rdb 和所记录位置点之后的 aof 文件,废弃 aof 重写机制,生成 rdb 后删除无效的 aof 文件;增加了定时持久化操作的配置项 cronsave,将单机部署的多个 Redis 实例的持久化操作分散在不同的时间点进行,并且错开业务高峰;将对 aof 的写入操作也放到 bio 线程来做,解决磁盘压力较大时 Redis 阻塞的问题。
2. 对于主从同步机制,借鉴 MySQL 的复制机制并做了简化。
使用 rdb + aof 的方式,支持基于 aofpositon 的增量复制。从库只需与主库进行一次全量同步同步,后续主从连接断开或切主操作,从库都是与主库进行增量复制。
对于版本升和管理级的问题, Redis 的核心处理逻辑封装到动态库,内存中的数据保存在全局变量里,通过外部程序来调用动态库里的相应函数来读写数据。版本升级时只需要替换成新的动态库文件即可,无须重新载入数据。通过这样的方式,版本升级只需执行一条指令,即可在毫秒级别完成代码的升级,同时对客户端请求无任何影响。
除了以上几点,也做了很多其它的优化,如主从延迟时间检测,危险命令认证等。通过逐步的优化,内部的 Redis 版本也开始进入稳定期,应用规模也在持续的增加。
业务极致定制化时代(2012 - 2013)
RedisCounter / LongSet
在某些特定的业务场景下,随着业务规模的持续增加, Redis 的使用又暴露出来一些问题,尤其是服务成本问题(小编:是省服务器的意思?)。为此结合特定的业务场景我们对 Redis 做了一些定制的优化。这里主要介绍一下在关系和计数两个业务场景下做的定制优化。
- 关系
微博关系业务包含添加、取消关注,判断关注关系等相关的业务逻辑,引入 Redis 后使用的是 hash 数据结构,并且当作存储使用。但是随着用户规模的快速增长,关系服务 Redis 容量达到十几 TB,并且还在快速的增长,如何应对成本压力?
为了解决服务成本问题,我们把 Redis 的角色由 storage 调整为 cache。
这是因为随着用户数量的增长,业务模型由初期的热点数据不集中已经转变为有明显的冷热之分。对于关注关系变更、判断关注关系,hash 数据结构是最佳的数据结构,但是存在以下问题:
- cache miss 后回写关注列表性能差,对于关注数较多的微博会员,回写操作耗时可达到 10ms,这对于单线程的 Redis 来说是致命的;
- Redis hash 结构的内存使用率不高,要保证 cahce 的命中率所需的 cache 容量仍然是很大的。
于是,我们定制了 longset 数据结构,它是一个“固定长度开放寻址的 hash 数组”,通过选择合适的 hash 算法及数组填充率,可实现关注关系变更及判断的性能与原生 Redis hash 相当,同时 cache miss 后通过 client 重建 longset 结构,实现 O(1) 复杂度回写。
通过定制 longset 数据结构,将关系 Redis 内存占用降低了一个数量级(小编:这该节约了多少服务器……发奖金了吗?),同时保证了服务性能。
计数
微博有很多计数场景,如用户纬度的关注数、粉丝数,微博纬度的转发数、评论数等。计数作为微博中一项很重要的数据,在微博业务中承担了很重要的角色。为更好的满足计数业务需求,我们基于 Redis 定制了内部的计数服务。
原生 Redis 为了支持多数据类型,需要维护很多指针信息,存储一份业务计数要占到约 80 个字节,内存利用率很低。为此我们定制了第一版计数器 Redis counter,通过预先分配内存数组存储计数,并且采用 doublehash 解决冲突,减少了原生 Redis 大量的指针开销。通过以上优化将内存成本降低到原来的 1/4 以下。(小编:又节约了 3 / 4 服务器……)
随着微博的发展,微博纬度的计数不断增加,在原来的转发数、评论数基础上,又增加了表态数,2013 年还上线了阅读数。 Redis counter 已不能很好的解决这类扩展问题:
- 存储单条微博相关的计数,需要重复存储微博 mid 信息,并且数据全部存储在内存,服务成本较高;
- 获取单条微博全部的计数,需要调用多次计数接口,对服务端压力很大。
为此我们又设计了改进版的计数器 CounterService,增加如下特性:
- Schema 支持多列:支持动态加列,内存使用精简到 bit
- 冷热数据分离:频繁访问的热数据存储在 memory,访问较少的冷数据存储在磁盘,降低服务成本
- LRU 缓存冷数据:增加 LRU 模块,缓存访问到的冷数据,保证冷数据的访问性能。
- 异步 IO 线程访问冷数据:避免冷数据的访问影响服务的整体性能
通过以上的定制优化,我们从根本上解决了计数业务的成本及性能问题。
除了以上关系、计数业务场景的定制优化,为了满足判断类业务场景需求,定制了 BloomFilter 服务;为了满足 feed 聚合业务场景需求,定制了 VerctorService 服务;为了降低服务成本,定制了 SSDCache 服务等。(小编:老板感动得流泪了)
服务化时代(2014 -)
Cache Service、SSD Cache
随着微博业务的快速增长,Redis 集群规模也在持续增加,目前微博 Redis 集群内存占用数十 TB,服务于数百个业务线,Redis 集群的管理依然面临很多的问题。
数据迁移问题
随着时间推移,越来越多的业务由于数据量的增加,单端口到内存占用已经达到上限,微博内部建议单端口内存不超过 20GB,因此需要重新拆分端口,这就涉及到数据迁移,目前迁移操作是通过内部开发的一个迁移工具来完成的,迁移操作的成本相对较高。
数据路由问题
目前的使用方式,需要在业务代码中实现数据路由规则,路由规则的变更需要重新上线代码,业务变更复杂度较高。同时节点配置采用 DNS 的方式,存在实时性和负载不均的问题,虽然使用过程中有对应的解决策略,但是需要一定的运维干预,运维复杂度较高。
HA 系统不成熟
当前的 HA 系统更多的是采用自动发现问题,手动确认处理的策略,没有实现真正意义的自动化,运维成本依然很高。
为了解决以上问题,我们在 Redis 基础上实现服务化框架 CacheService。
CacheService 最早是为了解决内部使用 memcached 遇到的问题而开发的服务化框架,主要包含以下几个模块:
配置中心 ConfigServer
微博内部的配置服务中心,主要是管理静态配置和动态命名服务的一个远程服务,并能够在配置发生变更的时候实时通知监听的 ConfigClient。
资源层
实际的数据存储引擎,初期支持 memcached,后续又扩展了 Redis、SSDCache 组件,其中 SSDCache 是为了降低服务成本,内部开发的基于 SSD 的存储组件,用于缓存介于 memory 和 DB 之间的 warm 数据。
代理层
代理业务端的请求,并基于设定的路由规则转发到后端的 cache 资源,它本身是无状态的。proxy 启动后会去从 ConfigServer 加载后端 cache 资源的配置列表进行初始化,并接收 ConfigServer 的配置变更的实时通知。
客户端
提供给业务方使用的 SDK 包,通过它不需要在业务代码中实现数据路由规则,业务方也无需关心后端 cache 的资源。只需要简单配置所使用的服务池名 group 和业务标识 namespace 即可使用 cache 资源,client 从 ConfigServer 获取 proxy 的节点列表,选择合适的 proxy 节点发送请求,支持多种负载均衡策略,同时会自动探测 proxy 节点变更。
集群管理系统 ClusterManager
管理集群中各个组件的运行状态以保证业务的 SLA 指标,当出现异常时会自动执行运维处理。同时配置变更、数据迁移等集群操作也都是由它来负责。
为支持 Redis 服务化,在服务化框架扩展支持了 Redis proxy,同时为了实
现在线数据迁移,参照 Redis cluster 的设计思想,对内部 Redis 存储做了改造,支持 slot 数据分片,数据迁移操作由 ClusterManager 组件执行,完成 slot 的重新规划及数据迁移。此外还支持 Redis 的 failover 机制,在master 或 slave 节点故障时会自动执行容错处理。我们 Redis 服务化项目 tribe 是从 2015 年底开始上线,处于逐步完善过程中。
总结
从对 Redis 的优化历程可以看出,技术的进步是由业务的需求推动的,我们需要拥抱需求。同时对于一个服务我们需要持续优化并保证服务的运维友好性才能保证服务的生命力。后续的一些计划,完善服务化体系中冷热数据分级存储机制以降低服务成本;引入新的组件以更好的满足业务需求、进一步完善集群管理组件降低运维复杂度。
作者:刘东辉
刘东辉,新浪微博基础架构组研发工程师。2013 年加入微博,先后参与微博 Redis、CounterService、SSDCache、CacheService 等基础组件的设计与开发工作,目前专注于分布式缓存、存储方向。