聊天室总结

聊天室总结

1. 我刚来公司的时候,聊天室是单机的方案:即只有一个chatroom服务,以chatroomId作为targetId, 这意味着,一个聊天室的所有行为,都只能在一台server上。包括加入、退出聊天室;聊天室发消息、聊天室发Notify,以及最终的用户来聊天室拉取消息。

2. 随着聊天室业务的爆发性增长,单机方案已经无法撑住高并发的用户量。这时的架构改进为:把notify和拉取消息的行为,拆分到新增加的chatroommessage服务中,即按业务进行了架构拆分,chatroommessage以userId作为targetId, 这样,当一个聊天室人数很多时,可以通过扩容硬件来分担Notify和拉取的行为。且可将chatroom和chatroommessage部署在不同的server上,实现了读写分离(由chatroom服务做发消息(写),由chatroommessage做拉取消息(读)),由于发消息的时候,需要保证一个聊天室内的msgId是唯一的,所以每发一条消息,都要通过加写锁来生成一个msgId, 所以发消息和聊天室中所有的成员是耦合的。

3. 随着聊天室业务量的继续增涨,chatroom再次成为了系统的瓶颈,当发消息并发达到一定程度时,每秒几万条消息的时候,chatroom服务无法撑住。这时,再次按业务将chatroom服务做架构拆分,拆分出chatroomsendmessage,chatroomsendmessage(以userId作为targetId)用于消息丢弃策略,即每个聊天室每秒钟最多可通过200条消息(其中low level msg最多100条,其余为hight level msg)。
注:这200条消息是个单点的数,如果部署10台chatroomsendmessage, 则需要把每台节点每秒钟可通过大约20+条,以此类推。然后在chatroom在做一次消息丢弃,保证每个聊天室每秒钟最多通过200条消息。

该消息丢弃策略是以手机sdk每秒钟最多处理100条消息为依据的(没接收一条消息,睡眠10毫秒)

4. 今后架构的发展方向:
1)将发消息与聊天室成员做解耦
目前解耦的主要障碍在于,生产msgId时,依赖于该聊天室最新一条消息的时间,若要聊天室最新消息的时间,则需要没写入一条,都排队加锁。

那么为什么msgId需要这样做呢?李淼是有两点原因:1. 为了保证msgid唯一;2. 为了保证消息有序。
我的想法:1. 要保证msgId唯一,可以通过appId_chatroomId_userId_currentTime做hashcode来保证唯一;
2. 怎样保证消息有序呢?如果发消息放到了chatroomsendmessage服务(以userid作为targetId), 那么对于同一个聊天室,分布在不同的chatroomsendmessage节点上的用户发消息时,有可能造成,同一个聊天室有两条消息的发消息时间完全一样。-- 这个问题怎样解决有待于思考。

2)当一个聊天室人数过多,比如50万人同时加入,也可能把chatroom单点撑爆,采用的办法是,把JoinChrm和ExitChrm的行为放到chatroommessage节点上来做,然后再同步到chatroom节点。这样,就可以在chatroom节点上做查询聊天室人数,查询聊天室成员,聊天室是否存在等行为。
思考:如果这样的话,为什么不把聊天室成员,维护在公用的redis中呢?当然,公用的Redis的访问速度肯定比不上内存那么快。
最好的办法是,使用LRUHashMap来存储聊天室成员,如果聊天室成员太多,可以存放到本地的redis或者ssdb中。

3)需要解决的问题:
《1》当chatroommessage的节点上没有人时,仍然给该节点广播消息,这样造成了不必要的内存浪费。解决方案是,使用公用的redis为每个聊天室存储消息队列。仅用于第一次加入聊天室时拉取历史消息。
《2》当服务器扩容后,怎样保证聊天室中的人不丢?解决方案:也是通过公用的redis里面保存聊天室中的所有人。
《3》第一次加载DB时,改用异步而不是同步,并设定好初始值:目的是服务器万一撑不住时,重启server后第一次加载DB值时不会造成请求积压。
《4》chatroom中的行为,是否都是不得不放在那的,如果不是,则放到chatroomsendmessage中。
《5》chatroom中实际上只需要存储最新的500人即可(调用查询聊天室成员接口,最多返回500)。然后存储总人数。
《6》QueryChatroomActor还在直接查询DB,这样如果有用户恶意攻击,可能DB服务器挂掉·。
《7》chatroom和chatroomsendmessage中向chatroommessage和chatroom通信时出口的LinkedBlockingQueue的队列的数目,可以是CPU核数*2, 因为是IO内网操作,有IO的等待时间。理想的启动线程数=[任务执行时间/(任务执行时间 - IO等待时间)] * CPU核数
《8》按userId作为targetId,这样通过hashcode取模的方式决定落到哪台server, 是否是平均分布的?大部分userId落到同一台上的概率有多大?--> 等高并发来时,去生产环境bc.log上去检验下。--> 用了一致性Hash,误差率在十分之一左右,比用随机算法略差
《9》用户因为离线被踢出聊天室后,并没有通知客户端,这样有些用户很诧异,为什么可以发成功消息,但确收不到消息。-- 对于发消息不自动加入聊天室的app。-- 现在客户端添加了重连机制:当手机断线,再连上线之后,会自动调用加入聊天室方法。
《10》离线30秒,或者错过30条消息被踢出聊天室,要是普通聊天室成员就算了,但对于主播(很多主播不允许发消息),一旦被踢出聊天室(当他的手机信息短时间不好),就看不到用户发的消息了,如果他退出再加入,会造成聊天室关闭。所以我建议增加专门针对主播的逻辑代码。
《11》当消息并发高时,可能会造成对红包消息的丢失,我建议如果可以定义某种类型的消息是永远不可丢失的是最理想的。
《12》聊天室对于自定义的消息,在网络不好的情况下,会有拉取到重复的消息,怎样解决呢?limiao说root cause是因为server端发的Notify有问题,导致client来重复拉取。
《13》使用LinkedBlockingQueue实现异步的时候,如果Queue满了,则新来的会被直接丢弃,所以写代码的时候要考虑这种场景。
《14》目前chatroom单点,如果他挂了,聊天室中的数据就全部没了,即时转到其他chatroom服务器,那么当时聊天室中的数据也全丢了。但因为聊天室中的数据是有状态的,例如聊天室中的人,所以建议把聊天室中的成员持久化存储到redis,即使单点chatrom挂了,仍然可以从Redis中恢复聊天室中的成员数据。聊天室中的消息丢就丢了,没关系。

4)将发消息(chatroomsendmessage)和聊天室成员(chatroom)解耦的剩余未解决问题:
《1》是否分发与是否禁言:
先看内存,如果内存为空,则从DB中查询。--> 如果放到chatroomsendmessage中,问题在于,从DB中加载的时候,可能需要加载全量的禁言和NotForward到以userId为targetId的内存服务器,造成一定空间的浪费。
综上,暂时不把NotForward和Forbidden放到chatroomsendmessage中

《2》消息模板路由:
不可以放到chatroomsendmessage中。--> 因为消息模板路由,是需要计算MsgId来发消息,所以也需要该聊天室的最新消息。跟正常发消息同理。所以等正常发消息和chatroom解耦时,才能将消息模板路由跟chatroom解耦。

5. 聊天室技术精粹:

1) 通过LinkedBlockingQueue做生产者、消费者,实现异步缓冲,
好处:1)保证发送方快速响应 2)限流(处理不过来时自动丢弃)
缺点:1)不能实时处理;2)费内存

2) 通过构建CUP数目的LinkedBlockingQueue来充分使用CPU

3) 对map进行set值时,使用putIfAbsent方式,避免写锁的性能瓶颈,且不会造成多次对map进行set。
缺点:当map没有完全set完时,其他线程读该map,数据不全。

4) 所以DB, jedis的写操作,都通过LinkedBlockingQueue来实现异步操作

5) 可通过Jmx来实时控制用到的参数,且可通过jmx来刷新同步数据库。

6) 消息队列采用ConcurrentSkipList来替代TreeMap, 是因为读写时跳表的数据结构优于红黑树。

7) 采用LRUHashMap来保存量大的map值,如聊天室成员,以防止数据过多造成内存泄露

8) DB值加载到内存,不做实时,而是一段时间(例如2小时)同步一次,但第一次加载时要全部同步DB数据,如负载过高时重启,这个过程比较耗时,可能造成线程积压,解决方案是,第一次加载DB值时,先使用事先设定好的初始值,然后在异步地从DB中加载DB的值到内存。

9) 采用log4j2, 异步写,可线上通过jmx实时修改Log级别。

6. 总结
要保证聊天室能处理高并发,要做到下面几点:
1. 结构是可伸缩,可扩展的,即聊天室人无限增加、消息量无限密集的时候,要可以通过增加硬件可以撑住。

2. 对于代码级别的优化:根本思路是尽量避免出现O(n)操作,尽量避免加锁(只要在业务运行范围内的不准确,如putIfAbsent)

3. 不直接操作DB, 而是所有的操作都是内存操作。尽量避免对持久化存储做同步读写操作(写操作改成异步写,读操作可以一段时间同步到内存一次)

你可能感兴趣的:(实战经验)