聊天室项目遇到的坑

最近在做一个视频聊天室的项目,工程中,碰到了很多坑,感觉可以记录一下。

下行推送消息

现在这个聊天室下行推送的消息还不多,也就4、5种,但最开始做的时候,是用IM的思维做的,认为每一条消息都有必要推送下去,以致于遇到了第一个大坑:下行的能力。

因为客户端已经维护了一个长连接,不太好为了这个项目再重新开启一个长连接,共用导致了很多操作上的不便,包括下行消息频率上的控制、针对不同版本的客户端推送不同的消息,等等。

聊天室和qq的群聊不同,它理论上不应该有人数限制,那么产生的消息量是n*n,这个n是没有大小限制的。如果稍微计算一下就会发现,这个下行流量是服务器承受不了的,必须主动丢消息。
采取的措施主要有这么几个:

  • 可以考虑限制消息的大小
  • 限制用户发送消息的频率
  • 限制下推的频率,主动丢弃
  • 当用户较多的,可以认为的将一个房间划分成多个,例如将落到一台服务器上的用户划分成一个小房间
  • 力所能及的下推。最开始实现时,为了尽快的下推消息,采用的是异步处理。当有新的消息到来时,会派生一个进程下推(Erlang),但是压测时发现,系统的内存和CPU都出现飙升。之后就改为了将前端的消息放在队列中,一条一条的处理,处理完一条再下推另一条,尽量保证系统存活

redis

我们是用redis来维护房间成员的,每一次用户加入、退出的时候都要写redis,每一次下推的时候都要读取用户列表。

为了快速处理用户的加入退出,我们用的是hash数据结构。这样下推的,就需要调用hgetall来拿所有用户的列表。了解的人就能看出来,hgetall是一个深坑。

因为hgetall处理太慢了,返回的应答又比较大,redis读写较频繁的时候,就容易cpu急剧上升,后面的redis请求超时失败。

网上针对hgetall给了一种解决方案,就是用一个all字段存储整个hash table,这样不用调用hgetall就可以拿到整个列表。但是聊天室是一个用户频繁加入退出的场景,这种情况下维护all字段,代价太大。
最终,我们定下的解决办法是减少redis读写次数、并针对hash table做一个缓存,可以维护一段时间,比如5s,尽管会导致不精确,但聊天室本来对消息要求就不精确。

还有一个问题是最近才出现的。就是随着用户越来越多,房间成员列表也就越来越大,悲剧的是,我们最开始把这个列表(hash)放在了一个节点上,导致压测时出现了很多读写超时。所以,在做评估时,一定要预料到数据规模,该分片要及时分片。

另外,使用redis给出几个建议:

  • 读写分离
  • 尽量不要使用lrem、hgetall这些慢操作
  • 数据量大的话,要分片

ets分表

项目开发是用erlang写的。erlang自身带一个很好用的内存数据库ets。每一个下发服务都在ets中维护着一部分房间成员列表。最开始,为了简单,我把所有房间的成员关系都存在了一个ets表中,这样维护起来很简单,而且开始用户量不大,并没有什么问题。但是随着用户量的增多,到50w-100w的时候,就发现下发服务的内存飙升,消息几乎推不下去。

反复检查后发现瓶颈是这个ets表。一个ets表自身是带锁的,多个进程写时,要先获得锁,同时又会有进程在读取一个房间的所有成员,这样就造成ets命令的堆积,内存越来越大。试验了{write_currency, true}和{read_currency,true},但效果很不理想,这两个优化都是以牺牲内存为前提的,对改善内存的帮助并不大。

最后采取了对ets表分表,每一个房间分配一个ets表,这样读取时可以直接将整个表读出,不用做匹配,读操作足够轻。不过,要注意,一个服务节点中ets表的个数是受限的,需要定时清理一些,这样就又引入一个问题,那就是ets表被关闭后,重新读取时,要有恢复数据的逻辑。

采用分表后,内存表现非常好。

posted @ 2016-06-28 17:40  我的娃会叫爸爸啦  阅读(901)  评论(0编辑  收藏  举报