tpn(taobao push notification)在使用redis计算消息未读数的过程中,遇到了一系列的问题,下面把这个过程整理了一下,也让大家了解这个纠结的过程,供大家以后使用redis或者做类似的功能时进行参考
redis在tpn里面主要是用于计算移动千牛(Android、IOS)上的消息未读数。tpn的未读消息数是基于bizId维度的,即同一个bizId(每条消息的业务id,如果商品id、订单id等),即使有多条消息,未读数也只能算1。因此在接收消息,计算移动千牛未读数的过程中,就需要对bizId去重,这个去重的功能就是通过redis来实现的。随着消息量的不断上涨,这个基于redis的去重方案也不断变化。
一、基于redis Set结构的未读数计算
前面说到的tpn未读数计算的最大特点就是基于bizId去重,在java里面,我们很容易想到利用HashMap或者HashSet来判重,因此最初tpn就是利用redis的Set结构来进行判重。主要利用了redis set结构的这两个命令:SADD和SCARD
SADD key member [member....]:将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 如果member元素不在集合里面,则返回1;如果member元素已经存在于集合当中,则返回0。
SCARD key:返回集合 key 中元素的数量。
有了这两个命令,计算未读数的步骤就是这样的:
tpn会为用户保留7天内的消息,也就是说保存到redis set结构中的bizId失效时间是7天,同时用户在查看消息后,就会把其对应的redis set清空(即如果一个用户连续几天都不查看千牛的消息,那么其对应的redis set集合里面就会保存大量的bizid)。tpn总共有6台redis机器,每台机器上部署5个redis实例,每个实例的maxmemory设为1G,总共30G的内存用于存放消息bizId。在tpn的早期,由于用户量不多,消息量也不大,redis的内存完全可以存放7天内的所有消息bizId,因此这个方案work的很好。但随着全网大多数活跃卖家开始使用千牛,tpn的消息量也随之暴涨,越来越多的消息bizId给redis带来了极大的压力,在消息高峰期,tpn的日志里会有大量的redis timeout异常(tpn使用jedis,配置的timeout是300ms),经过分析,主要是由下面原因造成的:
- 缓存失效造成的超时:前面我们提到了,tpn的每个redis实例的maxmemory设置的是1G,因为bizId越来越多,因此很快每个redis 实例的内存就超过了maxmemory。而redis在处理客户端请求时,如果发现当前内存的使用量已经大于等于maxmemory,就会去失效部分过期的缓存,直到内存使用量小于maxmemory。很明显这个失效缓存释放内存的操作会影响redis的rt。在消息高峰期,redis实例的内存使用量一直再maxmemory附加徘徊,造成redis在应对大量请求的同时,还要不停地失效缓存释放内存,造成频繁超时。
因为bizId太多,而redis内存不够,所以造成redis请求大量超时,最简单地办法就是加机器,部署更多的redis实例来存储越来越多的消息bizId。初步估计了一下,要完全把7天内的所有消息bizId都保存到内存中,需要高达上百G的内存:交易消息和商品消息是tpn最主要的两类消息,因为目前全网大多数活跃卖家都使用了千牛,为了去重,tpn需要把全网7天内所有新增的交易id和商品id都保存到redis内存中,换句话来说,也就是要用内存来保存7天内tc和ic新增的所有id。tpn基本不可能申请到这么多的redis机器,就算有这么多的redis机器,部署维护成本也是巨大的。就算不用redis,使用tair的rdb,这个陈本仍然是不能接受的。
在移动千牛客户端,推送没有正常到达的情况下(比如长连接断开的时候),是依赖客户端在发现长连接断开以后调用messagecount.get接口来获取到消息未读数,然后促使用户手动获取最新的消息。当redis的内存使用量接近极限时,调用redis的sadd、scard命令很容易就timeout了,因此不能正确地计算出消息未读数,就会造成用户不能及时获取到最新的消息。
总的来说,redis的内存容量不足以容纳越来越多的业务消息bizId,造成大量redis请求超时,不能正确地计算消息未读数。因此需要对上述方案进行优化。
二、redis用于消息去重判断,tair存放未读数消息数的方案
根据上面的分析,当redis内存使用量达到了上限时,很容易发送timeout,同时redis内存使用量会之所以会很快地达到上限,主要是因为不活跃用户的set结构里面保存了大量的bizId。在不能快速增加redis机器的前提下,最简单地方法就是在夜间重启redis。重启redis会带来一下影响:
所有用户保存在set里面的消息bizId全部被清空了,就会造成误判:即对同一个bizId的消息重复提醒用户有新消息。但这个并不会对用户造成太大的影响:因为活跃用户会及时地来查看消息,所以活跃的set结构基本都是空的;而非活跃用户的redis set结构虽然有很多消息bizId,但是因为其是不活跃的,就算被清空,很快又会有新的bizId存放进去,但认为是不活跃用户,对这种情况基本无感知。
因为set结构被清空,所以所有用户的消息未读数也被清空(通过scard命令来计算未读数)。根据前面的分析,在消息推送不能正常达到的情况下,正确的未读数会促使用户主动地来获取最新消息,所以基本不能接受重启redis的时候,清空用户的消息未读数
因为不能接受随意清空用户的消息未读数,所以我们不能定期重启redis来释放内存。但是如果我们把消息去重和计算未读数分开,即redis的set结构只用于判断一条消息是否是新消息,是否需要增加未读数,而把未读数保存在其他的地方,如果tair之类的,那我们是不是就可以定期重启redis了呢?因此我们得到了下面的方案:
继续是用redis的set结构来判断一条消息是不是新消息,是不是需要增加消息未读数
不再使用redis的scard命令计算消息未读数,而是采用基于tair的计数器来计算消息未读数,即如果通过redis的set结构判断出是新消息,则对保存在tair里面的未读数计数器执行incr unReadCountKey 1。
这样一来,redis就只用于对消息bizId去重,而不再用于计算消息未读数,消息未读数单独保存在基于tair的计数器当中。因此我们就大胆地定期在夜间重启redis了。这个方案成功work了一段时间,但过了一段时间后,应用在请求redis的时候又开始是不是抛出大量的timeout exception。分析了一下,问题还是处在redis内存上:
虽然可以通过定期重启redis来释放内存,但是redis内存的增加的速度是不可预期的,我们并不能每次都能在内存使用达到极限前重启redis
有时候虽然redis的整体内存使用量还没有达到极限,但是如果一个用户的set结构里面的bizId太多了,scard命令仍然会timeout
所以这个方案还不是一个最佳的方案,仍然需要通过更好的办法来降低redis的内存使用量
三、基于redis的bloomfilter的消息去重方案
从方案一到方案二,我们一直想解决的就是如何用最小的内存来判断一个消息bizId是不是新的bizId,即一个消息bizId是不是已经存在了。以最小的内存来实现判断操作,很容易就联想到bloomfilter。但是在这个场景,我们不能简单地使用bloomfilter,先来计算一下“最直接”地使用bloomfilter需要多大的内存:bloomfilter的所占用的内存由bitSize决定,而根据公式:
bitSize = (int) Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)));
我们为每个用户的每个消息类型创建一个bloomfilter,以500万用户,每个用户订阅了10个消息类型,那么这个用于去重的bloomfilter所占用的内存总量是:
totalMemory(G) = 5000000*10*Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)))
这个totalMemory的大小就取决于maxKey和errorRate,保证errorRate不变的前提下,bloomfilter 的maxKey越大,bloomfilter所需要的内存也就越大。那我们估算一下使用bloomfilter,需要多少内存。
以商品消息和交易小为例,不同的卖家,7天内的消息数从几个到几万个不等。最小的是7天只有几条消息,最多的7天内有7万多条。就算取个1000的评价值,这5000w个bloomfilter的内存消耗也在上百G,这明显行不通。
但是,tpn的消息未读数还有一个业务特点就是,当一个用户的某个消息类型的未读数已经超99了,就不再显示具体的数字,而是显示成99+,同时一个用户的消息未读数超过了99,那么其实他自己对消息未读数的敏感性也不高了,即就算有一条消息不是新消息,但是仍然给未读数+1了,用户也察觉不出来。
因此,在上面的公式里,我们可以把每个bloomfilter的maxKey设为100,那这样一来,所占用的内存就是一个十分能够接受的数字了:设errorRate=0.0001,maxKey=100,那么上面的5000w个bloomfilter只需要11G的内存,很明显,这不是一个完全可以接受的内存消耗。
这样一来,我们就得出下面这个基于redis bloomfilter去重方案:
- 通过redis的setbit命令来实现一个远端的bloomfilter,具体可以参见这个例子:https://github.com/olylakers/RedisBloomFilter/blob/master/src/main/java/org/olylakers/bloomfilter/BloomFilter.java
- 每次来一条新消息,通过redis的bloomfilter来判断这是不是一条新消息
- 如果是,则对tair中的未读数计数器+1
- 用户每次读取消息后,则清空对应的bloomfilter
这样一来,终于我们可以通过能接受的内存来实现未读数的计算,不再要每天担心redis是不是内存不够用了,应用又频繁抛timeout exception了
四、诡异的connection broken pipe
在方案三上线以后,我认为这些redis应该会消停了,redis运行一段时间后,的确再也没用timeout exception了,但是在运行一段时间后,tpn在向redis执行请求时,往redis写入命令时会报这个异常:
java.net.SocketException: Broken pipe。我们知道,如果一个socket连接已经被远端给close掉了,但是客户端没有察觉,仍然通过这个连接读写数据,那么就会产生Broken pipe异常。因为tpn使用jedis,通过common pool来实现jedis的connection pool,我第一反应就是tpn没用正确使用jedis的connection pool,没有销毁掉broken的redis connection,而是已经重新把归还给了connection pool,或者是jedis的connection pool有bug,造成了connection泄露,导致ton在往一条已经往一条已经被close的连接写入数据。但是仔细检查了一遍tpn的代码和jedis connection pool的代码,发现没用什么问题,那就说明有些redis是真的被redis服务端给关闭了,但是jedis 的connection pool没有发现。
因为客户端的jedis pool没有问题,那么基本上可以确定的确是redis server端关闭了一些连接。首先怀疑的就是tpn的redis 配置出错了,错误地配置了redis.conf里的timeout 配置项:
首先怀疑的是不是tpn的redis配置不多,造成因此就去查看redis的相关代码。redis的配置文件redis.config里面有timeou这个配置项:
# Close the connection after a client is idle for N seconds (0 to disable) timeout 0
检查了下tpn 6台redis上的所有配置文件,发现都没有配置这个选择,但是tpn部署了两个版本的redis,redis-2.6.14和redis-2.4,结果在redis-2.4里面,如果没有配置这个值,redis就会使用默认的值,5*60(s),而redis-2.6.14的默认值是0,即disable timeout,同时又去查看了下jedis common pool的设置,发现minEvictableIdleTimeMillis=1000L * 60L * 60L * 5L(ms),即一个redis连接的空闲时间超过5个小时才会被connection pool给回收。很明显,就是因为客户端和服务端的connection idle time设置不一样,造成了connection被一端关闭了,但是另一端没有感知,所有造成了broken pipe。解决办法就是把redid-2.4升级到redid-2.6.14。
五、总结
从方案一到方案三,我最大的感触就是,在解决问题,优化方案的时候,不能仅仅执拗于技术本身,而是要联系业务思考。这个redis的bloomfilter的想法我很早就有了,但是我之前一直没有想到tpn未读数消息数只显示99+这个业务逻辑,而是一直想如何通过降低消息bizId的长度来尽可能地去节省内存,结果越想越复杂,然后就没有然后了。。。。
相关推荐
如果在编译过程中遇到时间错误,可能是因为源码包含时间戳信息,可以通过`date -s 'yyyy-mm-dd hh:mm:ss'`设置系统日期,然后使用`clock -w`将时间写入CMOS。如果在执行`make test`时出现需要TCL 8.4以上版本的提示...
如果在编译过程中遇到错误,可能需要进入src目录并查看相关文件的说明。编译成功后,需要创建Redis的运行目录,并将编译好的redis-cli和redis-server二进制文件以及配置文件redis.conf拷贝到运行目录中。之后可以...
### Redis实践与总结 ...然而,在实际部署和使用过程中,需要注意合理配置和管理,避免因不当使用而导致性能瓶颈或安全问题。通过对上述案例的学习,我们可以更好地理解如何在一线开发中有效地运用Redis。
在尝试使用Jedis客户端通过网络连接Linux服务器上的Redis时,可能会遇到一个常见的错误提示:“DENIED Redis is running in protected mode”。这个问题通常出现在Linux环境下,当Redis服务启动时默认开启了一个安全...
- **防火墙限制**:如果安装过程中遇到防火墙限制问题,需要关闭防火墙或开放Redis使用的端口(默认为6379)。 - **路径错误**:若因路径设置错误而导致服务无法启动,需要先停止服务,再进行相应的路径修正。 ```...
由于各大互联网公司的应用场景复杂多样,因此他们在使用Redis时会遇到各种棘手的问题。通过与这些公司合作解决生产环境中的问题,研究人员可以积累实战经验,并据此改进Redis的性能和稳定性。 ### 知识框架与系统观...
在"項目總結"文档中,开发团队会详述项目实施过程中遇到的问题、解决方案以及性能优化的策略,为后续的维护和升级提供参考。 通过这样的架构设计,我们可以实现一个高效、稳定的商城秒杀系统,满足高并发场景的...
### Redis面试知识点详解 #### 1、什么是Redis? Redis是一个完全开源且免费的高性能key-value数据库,遵循BSD许可协议。...以上是关于Redis的一些关键知识点和面试中可能会遇到的问题及解答。希望对您有所帮助!
这本书详细地介绍了如何使用Redis以及如何解决在实际应用中可能遇到的问题。该书适合那些想要了解和实践Redis应用的开发者,特别是那些希望利用Redis解决特定问题的用户。书中的内容覆盖了Redis的基础知识、数据类型...
在开始安装前,务必先阅读此文件,了解安装过程和可能遇到的问题。 安装Redis集群时,除了单个实例的配置,还需要设置多个实例作为主从,并配置Sentinel。在`go_redis`脚本中,可能已经内置了集群配置的逻辑,你只...
总结,使用Zabbix监控Redis集群是一个涉及多步骤的过程,包括Zabbix服务器和Redis节点的安装、依赖包的管理、监控脚本的编写以及Zabbix模板的配置。通过这个系统,我们可以有效地监控Redis集群的健康状况,确保数据...
如果你在过程中遇到任何问题或有疑问,可以通过提供的博主链接进行留言,获取进一步的帮助和支持。 【总结】 “Tomcat-Redis-Session管理”是一种有效提升Web应用性能和可扩展性的方法,通过合理配置,可以解决...
标题中提到的问题在于,当在Redis中使用`get`或`hget`命令尝试获取一个不存在的键或字段时,Redis会返回`(nil)`。在终端显示时,这可能会误导开发者认为返回的是Lua中的`nil`类型。然而,事实并非如此。在Lua中,`...
在编译过程中可能会遇到依赖问题。如果出现“cc 未找到命令”的错误,意味着系统缺少GCC编译器,可以通过`yum`命令安装: ```bash yum -y install gcc automake autoconf libtool make ``` 如果遇到“jemalloc/...
如果在编译过程中遇到 `jemalloc/jemalloc.h` 文件不存在的错误,可以通过指定分配器为 libc 来解决: ```bash make MALLOC=libc ``` #### 五、安装 Tcl 在编译测试阶段可能会出现需要 Tcl 8.5 或更高版本的...
5. **错误处理**:在与Redis交互时,可能会遇到网络错误或其他问题。hiredis提供了错误检查机制,如`redisGetReply`返回的错误指针,可以帮助调试和处理异常情况。 6. **同步与异步操作**:hiredis提供了异步API,...
如果在保存时遇到磁盘写满或坏盘等问题,可以使用config set命令更改保存路径,以确保RDB文件能被正确保存。 以上是根据给定文件内容,对Redis性能调优方面的知识点进行的总结。这些内容涉及了Redis的数据库管理、...
Redis是一款高性能的键值数据库,常用于数据缓存、消息队列等场景。在Windows环境下安装Redis并配置可视化工具,可以方便地管理和监控Redis...在使用过程中遇到任何问题,可以查阅Redis的官方文档或社区资源寻求帮助。
在实际操作过程中,遇到各种各样的问题是不可避免的,关键是要学会查找相关文档和资料,以及利用社区资源解决问题。对于开发者而言,养成良好的记录习惯非常重要,这不仅有助于个人技术积累,还能为同行提供参考和...