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