论坛6 | 云时代的运维与安全 黄鑫 「从零到千万——极光IM的演进之路」



  • 时间: 2016年7月28日
    发言人: 黄鑫|极光推送CTO兼首席科学家
    主题: 从零到千万——极光IM的演进之路


    大家好,我是2014年底加入的极光,极光IM也算是我负责的第一个技术产品,我见证了从没有上线,到现在每天千万级的用户量。特别底层的东西我没有参与,但是我可以演示一下整个演进过程中我们做了什么大的方案,我们从Push到IM过程中有什么。Push和IM的核心点,它是否稳定高效就是两个技术,一个技术是长链接。可能一个APP有百万在线,如下我做一个SDK,做一个云服务,可以轻轻松松达到一两亿在线,所以怎么样用最少的服务器撑这么多长链接是相对复杂的事情,第二件事情是离线消息,很多客户自己会做Push,我们也合作了一些做客服或者做IM的SDK,他们SDK里面又包了一层我们的SDK,他们希望我们解决离线消息的问题。我这次重点讲的就是关于离线消息方面的演进过程。

    判断技术核心指标很简单,无论是长链接还是离线消息,就是两个指标,一个是稳定性,你的离线消息是不是足够稳定。比如说我每次应该有一百万条离线消息是不是都能发下去,会不会出现丢消息的情况,里面会考虑容灾这些。第二个是及时性,一百万消息我多久能发下去?无论是Push还是IM都是如此。

    什么是离线消息,就是发送消息的时候如果目标不在线,这条消息就会被视为离线消息。就像刚才所说的,如果一个应用退出到后台的时候,正常长链接已经不在了,这时候发消息是发不下去的,这时候我们把它存储为离线消息,当用户再次上线的时候会收到这个消息。比如说上电梯,在电梯里网络已经断掉了,你再上线的时候,你要把聊天信息给我发回来。离线消息指是目标没有收到的历史消息,当目标上线的时候需要马上收到。

    0_1470894853695_upload-517e720e-a9f2-442f-ac66-c41cbb4a4906

    我们看一下最初的时候,这是我刚来到极光时候离线消息一直在用的方案,当然后来有了很多演进方案,是一些小修小补。我们是用Couchbase做存储,如果大家看过Couchbase应该有所了解,它有三个节点,一般会撑到20万左右的QPS。QPS还是一个小事情,最大的好处是说可以实现很多扩容,本身是集群方案,有容灾。另外Couchbase还是有一些问题,第一,逐渐倾向于闭源,社区也不活跃。出现问题很头疼去解决,Couchbase如果出现小规模的问题,我们是很难求助于社区,如果求助于公司它的响应非常慢,这一点我们也在考虑是不是有方案能替代。

    第二是分Key存储的,另外消息和消息之间相互独立。什么意思?我说一下我们的key是以UID和MsgidID作为组件存储的,我发一千万条消息给一条客户,或者某一条消息发给客户的话,消息和消息之间不会受到影响。比如说我这个用户和UID出现了问题也没有问题,不会受到影响。它的好处是高并发的可以存储不同的目标消息,我举个这样的例子,比如说现在有一个用户,他收到某一个APP给他推送一百条消息。如果你是以用户为维度存储List的话,我不停在推,中间肯定会出现很多并发的问题,不停的写,其实效率非常低。但是如果我以每一条消息加上每一个UID为一个单独存储的话,其实你可以达到写的最好的效率,包括你读的时候也一样,并且消息和消息之间非常独立,也决定了你在后续处理过程中,即使失败也不会出现什么问题,当然这是在Push层面。当它衍生到IM的时候,其实出现了很多问题,我后面会说到。

    当我们离线消息下发的时候,我大概说一下流程:第一件事情就是当一个用户上线的时候就开始做一次检查,检查到底有没有离线消息。刚开始我们每一个用户上线的时候都查一下离线消息,把它拿出来。但是我们发现效率非常低,因为很的用户都是持续在线的,并没有离线消息,但是它一直在访问离线消息Couchbase的库,导致Couchbase的QPS非常高。于是我们就做了一个缓存,首先我存一个Redis 的表,做的事情就是查询UID 上是不是有离线消息,这个方案事实上解决了很多问题。因为UID相对来说可以这样划分,扩容的时候并不会出现太多问题,我完全可以用多个点来实现这样的事情。开始用户会存储很多离线消息,因为也有一些商业化因素,也有一些其他性能层面的因素,因此限制了每个UID最大的消息限制。第二步我们获取这个UID最多能下发多少离线消息,下发完之后,比如说能查到UID下发五条离线消息,我就把五继续向后传。刚开始我说我们用的是UID加Msgid,我们可以取到UID下划线1到 UID 下划线6,但是我们只取到五,取到这五条消息,我们逐渐一条一条的发下去。第一条发UID是1,Msgid消息是1,我就加个下划线,这样可以把消息一条一条发消息。这样可以快速查询现在有多少离线消息,可以很快的确定出下发的消息是1—5还是3—10,我可以循环顺序下发每条消息。我们当然在中间做了一些容灾和其他措施,但是整体技术方案就是这样。

    2014年底的时候我们启动了IM项目,我们认为IM和Push是一样的,也需要保持跟服务端的长链接,也需要当用户不在线的时候及时下发离线消息。所有人第一个想到的方案IM是不是可以跟Push共用这套方案?我们2014年马上上线了一个版本,就是跟Push共用同一条歇息,但是我们发现了很多问题。

    Push与IM离线消息

    我们认为它在离线消息层应该是一样的,但是我们仔细回想实际的业务场景他们是不是一样的?我们可以想一下,一条消息是多个目标的,如果大家做APP肯定知道,我发一条广播消息,这个消息是发给一千万用户的。一对一千万,一对一百万是非常常见的一个指标,这是Push。但是IM不一样,大家至少会限制群的最大聊天人数,一般聊天也是点对点的,跟Push是不一样的场景。

    Push一般来说是单个目标少消息的,不太可能我现在是一个用户,一个APP瞬间推一百条消息,这个用户肯定会把APP卸掉,一般是单目标少消息,并且频率很低。IM不一样,它是单消息少目标,比如说一个大群500人,消息最多发给500人就行了,它的频率非常的高,我们频繁的聊天绝对不是说我单独做这样的事情。另外会涉及到多通道的问题,我们拿微信举例,在手机和PC上分别登陆微信,当有一条消息发下来的时候,我同时需要发给PC也需要发给手机,这是多设备的问题,但是在Push基本不存在这样的问题。

    最后因为IM和Push不一样,因为IM聊天的人就这么多,所以性能压力在技术难度层面是比Push小很多的,但是它的麻烦是业务复杂度比较高,比如说会涉及到讨论组、群聊、黑名单,有很多业务场景,这是IM复杂的地方。

    所以我们回顾了一下,想了一下Push的问题到底在哪儿。Push的问题有几个:

    1. 刚开始我们说当查到我的离线消息是10条的时候,我把这10条辨别出来1—10,从1下发一条,2下发一条,10下发一条。但是IM的问题是我们肯定会遇到这样的情况,现在手机是静音状态的,再拿出来的时候我的微信一定已经积累了几百条消息。如果几百条消息是一条一条发下去的话,客服端会有很大的压力。在Push层面不会出现这样的问题,我拿出来手机也就几条Push消息,而且我的Push都是开着的。
    2. SDK一条条接收的性能问题。
    3. 多通道不同平台的设备消息同步。你逐渐获取的过程中可能出现各种时序、并发层面的问题,基于此我们就出了IM离线消息第一个版本。

    我们希望IM解决什么问题?第一个最需要解决的问题,就是一定要解决大量的离线消息下发不会对服务端产生各种性能问题的影响。大家可以这么想,我一次查一百条,和一百次查一条是不一样的。IM和Push不一样,Push的话有两条消息,一条是两点推的,一条是两点零五推的,看到后面这条然后再看到前面这一条没关系,但是IM顺序不能错。会话消息共享、离线消息同步的问题这些都需要我们解决。

    0_1470895073757_upload-2a796fec-9306-4ec1-ba81-5a9ddf837bbe

    如果我们刚开始就做IM的话,因为以前我也做过IM的事情,做IM我第一个可能都会想到这样的方案,但是我们从Push到IM的层面可能就要做一些改变。第一个就是用List的结构存储,以前我们都说我们的Key是某一个UID加上某一条消息,如果你用这样的方案必然面临着每次都要单独去存,消息顺序也无法得到保证,这个时候我以设备作维度,每个设备维护一条消息,不停往里塞。在这一点上大家肯定很容易想到,我们回想一下Push为什么不能做这样的事?就是因为如果你这样做的话会出现很多并发层面的问题,但是IM业务逻辑、性能压力不大,所以还好一些。

    每一个设备对一个List,只要来一个消息就往这儿塞,这个时候我们会发现它的消息写入非常慢。这是为什么呢?它会存在一个冲突的问题。比如说我在一个群聊的场景下,可能500个人都在同一个群,我收到的消息可能这个屋子里的人都在讲话,这就有顺序的问题。我在写你也在写,这时候就出现了冲突。这个时候如果冲突的话怎么办?要不我写的时候锁住,锁住的话所有人写的就会很慢,要不然就失败重新写,失败重新写,就会造成整个性能非常低。这时候我们做了一个改变,大规模的群聊毕竟是少数场景,那么如果他写入失败的话,我们就临时写入一个失败队列,这个失败队列并不会像主体群一样有那么大的并发量,失败队列是以时间排序的。每当我们认为失败队列一段时间没有写的时候,我们会把失败队列统一批量的刷进某一个设备对应的List里,这会解决很多问题。另外会话消息的共享,用户设备的独立,我们用这样一个方案的时候它整体就解决了很多问题,就是我们从单消息存储的方案变成了List的方案。

    下图具体的顺序图,大家感兴趣的话可以再仔细看一下。

    0_1470895130327_upload-26df43c3-b2a8-4b13-903a-4153cb580458

    我们刚开始做的一直是安卓端和IOS端,没有做Web端。IOS和安卓有一个好处,就是他们都能拿到手机的唯一设备,但是PC端是不一样的。我们可以经常进行更换,比如说换一个电脑。有一次我们发现为什么这一天UID发生了特别猛烈的增长,我们发现很多UID是我们在Web端生成的,于是我们要想一个方案。因为我们是以设备为单独建立某一个List的,如果设备数大量增长的话,我们整个Couchbase内存也会出现很多问题。比如说我现在有1千万PC,比如说某一个用户,今天我登了第一个电脑,过一分钟清了再登,我可能一天就会产生非常多的队列,而且这个队列是不会被消化的,一直都是猛增,这会对我们服务器产生很多损耗。这个时候我们想了一个办法,我们在整个长链接和Web端,就是JS端加了一个放号池的作用。

    比如说UID从1到1万,这是最大的UID,接下来我们认为Web端不可能存在50万人同时在线。我们就给放号池规定是1—50万,有一个人上来,我放一个号下去是1,如果用户下线我就把1回收掉,我们就和放号池出打交道。无论用户频繁更换多少设备,我服务端只对应50万队列,这是最高了,这就很好的解决了问题。第二个问题是说我们会出现很多会话消息获取的问题,这时候WebIM最重要的就是起到刚才说到的目的。

    我们回顾一下Push和IM的方案,我们中间并没有任何复杂的技术,刚开始我在介绍IM的情况下,我也会想为什么会有这么多不合理的地方,现在我再回顾一下,会发现我们认为很多不合理的方案在历史情况下一定有它的原因,并且如果我们认为IM是很常见的方案的话,如果换一个其他业务场景的话很可能是不合适的,所以我们接下来在做任何设计方案的时候,可能真的要实际根据某一个实际业务场景,去想一下APP到底业务流程是什么样的,读写比什么样,各种频率是什么样,这时候我们才能设计出一个更好的方案。

    整个演讲里我并没有讲我们大家做一个IM需要怎么样,因为我相信不同的公司可能也不一样,比如说直播类,微信的聊天类,有一个APP就是强调群聊,另外一个APP强调单聊,肯定都有适合自己的架构让整个下发频率更高。我最希望跟大家分享一下,我们在解决这些方案过程中用到的大家看起来并不复杂的技术,谢谢大家。


登录后回复
 

与 青云QingCloud 社区 的连接断开,我们正在尝试重连,请耐心等待