乐趣区

关于直播:6000-字干货详解直播聊天室的无限用户优化

融云近期推出直播 SDK,两步即可实现视频直播能力。在第二步“开始直播”阶段,调用一个接口就能公布视频流,其余用户便可退出房间观看直播并在公屏发送弹幕与主播互动。移步【融云寰球互联网通信云】收费体验

在直播中,弹幕交互是用户和主播互动的次要形式,应用的就是 IM 中的聊天室性能。融云直播 SDK 残缺封装了直播业务所需的全副性能,包含久经考验的无下限聊天室组件,让直播聊天室轻松应答亿级音讯并发。

本文次要分享融云的直播聊天室高可用架构及平滑扩缩容实现计划,并具体介绍聊天室服务的两大瓶颈:人员无下限 & 海量音讯并发的破解之法。

直播聊天室的次要性能

除了用户最可感可知的多类型音讯发送和治理外,直播聊天室还要承当用户治理等工作。在万物皆可直播的当下,超大型直播场景不足为奇,更是对直播聊天室提出了人数无下限和海量音讯并发的挑战。这两个进阶能力的撑持水平决定了直播聊天室服务的下限。具体如下:

丰盛的聊天室音讯类型和音讯进阶性能:

发送文字、语音、图片等内置音讯类型和可实现点赞、礼物等性能的自定义音讯类型;

内容平安治理,包含敏感词设置,聊天内容反垃圾处理等;

聊天室音讯治理性能,包含音讯优先级、音讯散发管制等等。

便捷粗疏的聊天室治理性能:

聊天室用户治理,包含创立、退出、销毁、禁言、查问、封禁(踢人)等;

聊天室用户白名单性能,白名单用户处于被爱护状态不会被主动踢出,且发送音讯优先级别最高;

聊天室实时统计及音讯路由等性能。

聊天室人数无下限:

一些大型直播场景,如春晚、国庆大阅兵等,直播间累计观看动辄上千万人次,同时观看人数也可达数百万。

另外,直播间的一大特点是,用户进出聊天室十分频繁,高热度直播间的人员进出秒并发可能上万,这对服务撑持用户高低线以及用户治理的能力提出了十分大的挑战。

海量音讯并发:

直播聊天室人数无下限,天然带来了海量并发音讯的问题。一个百万人数的聊天室,音讯的上行已是巨量,音讯散发量更是几何级回升。

如果服务器只做音讯的消峰解决,峰值音讯的沉积会造成整体音讯延时增大。延时的累积效应会导致音讯与直播视频流在工夫线上产生偏差,进而影响用户观看直播时互动的实时性。所以,服务器的海量音讯散发能力非常重要。

直播聊天室的架构

高可用架构

高可用是分布式系统架构设计中必须思考的因素之一,简略来说就是缩小零碎不能提供服务的工夫。

高可用零碎须要反对服务故障主动转移、服务精准熔断降级、服务治理、服务限流、服务可回滚、服务主动扩容 / 缩容等能力。

以服务的高可用为指标,融云直播聊天室的零碎架构如下图 1:


(图 1 融云直播聊天室的零碎架构)

这套零碎架构次要分三层:

连贯层: 次要治理服务跟客户端的长链接;

存储层 :以后应用的是 Redis,作为二级缓存,次要存储聊天室的信息,比方人员列表、黑白名单、封禁列表等。服务更新或重启时,能够从 Redis 中加载出聊天室的备份信息;

业务层 :这是整个聊天室的外围,为了实现跨机房容灾,融云将服务部署在多个可用区,并依据能力和职责,将其分为聊天室服务和音讯服务。

聊天室服务次要负责解决治理类申请,比方聊天室人员的进出、封禁 / 禁言、上行音讯解决审核等;音讯服务次要缓存本节点须要解决的用户信息以及音讯队列信息,并负责聊天室音讯的散发。

在海量用户高并发场景下,音讯散发能力将决定着零碎的性能。以一个百万级用户量的聊天室为例,一条上行音讯对应的是百万倍的散发。这种状况下,海量音讯的散发,依附单台服务器是无奈实现的。

融云的优化思路是:将一个聊天室的人员分拆到不同的音讯服务上,在聊天室服务收到音讯后向音讯服务扩散,再由音讯服务分发给用户。以百万在线的聊天室为例,假如聊天室音讯服务共 200 台,那均匀每台音讯服务治理 5000 人左右,每台音讯服务在散发音讯时只须要给落在本台服务器上的用户散发即可。

在聊天室服务中,聊天室的上行信令是根据聊天室 ID 应用一致性哈希算法来抉择节点的;在音讯服务中,根据用户 ID 应用一致性哈希算法来决定用户具体落在哪个音讯服务。

一致性哈希抉择的落点绝对固定,能够将聊天室的行为汇聚到一个节点上,极大晋升服务的缓存命中率。聊天室人员进出,黑 / 白名单设置以及音讯发送时的判断等解决间接拜访内存即可,毋庸每次都拜访第三方缓存,从而进步了聊天室的响应速度和散发速度。

最初,Zookeeper 在架构中次要用来做服务发现,各服务实例均注册到 Zookeeper。

平滑扩缩容

作为平安、牢靠的寰球互联网通信云服务商,保障任何状况下服务的延续性和可用性是融云的使命。

随着直播这种模式被越来越多人承受,直播聊天室面对人数激增以致服务器压力逐渐增大的状况越来越多。所以,在服务压力逐渐增大 / 缩小的过程中是否进行平滑的扩 / 缩容十分重要。

在服务的主动扩缩容方面,业内提供的计划大体一致。通过压力测试理解单台服务器的瓶颈点 → 通过对业务数据的监控来判断是否须要进行扩缩 → 触发设定的条件后报警并主动进行扩缩容。

鉴于直播聊天室的强业务性,具体执行中要保障在扩缩容中整体聊天室业务不受影响。

聊天室服务扩缩容

聊天室服务在进行扩缩容时,能够通过 Redis 来加载成员列表、封禁 / 黑白名单等信息。须要留神的是:在聊天室进行主动销毁时,需先判断以后聊天室是否应该是本节点的。如果不是,跳过销毁逻辑,防止 Redis 中的数据因为销毁逻辑而失落。细节如下图 2:


(图 2 聊天室服务扩缩容计划)

音讯服务扩缩容

音讯服务在进行扩缩容时,大部分成员须要依照一致性哈希的准则路由到新的音讯服务节点上。这个过程会突破以后的人员均衡,并做一次整体的人员转移。

在扩容时,融云依据聊天室的沉闷水平逐渐转移人员。

在聊天室有音讯时,音讯服务会遍历缓存在本节点上的所有用户进行音讯的告诉拉取,在此过程中判断此用户是否属于这台节点,如果不是,将此用户同步退出到属于他的节点。

在用户拉取音讯时,如果本机缓存列表中没有该用户,音讯服务会向聊天室服务发送申请确认此用户是否在聊天室中,如果在则同步退出到音讯服务,不在则间接丢掉。

在音讯服务缩容时,音讯服务会从公共 Redis 取得全副成员,并依据落点计算将本节点用户筛选进去并放入用户治理列表中。

有限用户的治理和音讯散发

用户的高低线和治理

聊天室服务治理了所有人员的进出,人员的列表变动也会异步存入 Redis 中。

音讯服务则保护属于本人的聊天室人员,用户在被动退出和退出房间时,须要依据一致性哈希算出落点后同步给对应的音讯服务。

聊天室取得音讯后,聊天室服务播送给所有聊天室音讯服务,由音讯服务进行音讯的告诉拉取。音讯服务会检测用户的音讯拉取状况,在聊天室沉闷的状况下,30s 内人员没有进行拉取或者累计 30 条音讯没有拉取,音讯服务会判断以后用户曾经离线,而后踢出此人,并且同步给聊天室服务对此成员做下线解决。

亿级音讯的散发策略

聊天室服务的音讯散发及拉取计划如下图 3:


(图 3 聊天室服务的音讯散发及拉取计划)

音讯告诉拉取

在图 3 中,用户 A 在聊天室中发送一条音讯,首先由聊天室服务解决,聊天室服务将音讯同步到各音讯服务节点,音讯服务向本节点缓存的所有成员下发告诉拉取,图中服务器向用户 B 和用户 Z 下发了告诉。

在音讯散发过程中,server 做了告诉合并。

告诉拉取的具体流程为

① 客户端胜利退出聊天,将所有成员退出到待告诉队列中(如已存在则更新告诉音讯工夫);

② 下发线程,轮训获取待告诉队列;

③ 向队列中用户下发告诉拉取。

通过这个流程可保障下发线程一轮只会向同一用户发送一个告诉拉取,即多个音讯会合并为一个告诉拉取,无效晋升了服务端性能且升高了客户端与服务端的网络耗费。

音讯拉取

用户的音讯拉取流程如下图 4:


(图 4 用户的音讯拉取流程)

用户 B 收到告诉后向服务端发送拉取音讯申请,该申请最终将由音讯节点 1 进行解决,音讯节点 1 将依据客户端传递的最初一条音讯工夫戳,从音讯队列中返回音讯列表,参考下图 5:


(图 5 客户端拉取音讯示例)

用户端本地最大工夫为 1585224100000,从 server 端能够拉取到比这个数大的两条音讯。

音讯控速:

服务器应答海量音讯时,须要做音讯的控速解决。这是因为,在直播聊天室中,大量用户在同一时段发送的海量音讯,个别状况下内容基本相同。如果将所有音讯全局部发给客户端,客户端很可能呈现卡顿、音讯提早等问题,重大影响用户体验。

所以服务器对音讯的上下行都做了限速解决。


(图 6 音讯控速)

服务器的限速控制策略如下:

服务器上行限速管制(抛弃)策略:针对单个聊天室的音讯上行的限速管制,默认为 200 条 / 秒,可依据业务须要调整。达到限速后发送的音讯将在聊天室服务抛弃,不再向各音讯服务节点同步。

服务器上行限速(抛弃)策略:服务端的上行限速管制,次要是依据音讯环形队列的长度进行管制,达到最大值后最“老”的音讯将被淘汰抛弃。

每次下发告诉拉取后服务端将该用户标记为拉取中,用户理论拉取音讯后移除该标记。

如果产生新音讯时用户有拉取中标记,且距设置标记工夫在 2 秒内,则不会下发告诉(升高客户端压力,抛弃告诉未抛弃音讯);超过 2 秒则持续下发告诉(间断屡次告诉未拉取则触发用户踢出策略,不在此赘述)。

因而,音讯是否被抛弃取决于客户端拉取速度(受客户端性能、网络影响), 客户端及时拉取音讯则没有被抛弃的音讯。

聊天室的音讯优先级

音讯控速的外围是对音讯的取舍,这就须要对音讯做优先级划分:

白名单音讯,这类音讯最为重要,级别最高,个别零碎类告诉或者治理类信息会设置为白名单音讯;

高优先级,仅次于白名单音讯,没有非凡设置过的音讯都为高优先级;

低优先级,最低优先级的音讯,这类音讯大多是一些文字类音讯。

具体如何划分,开发者能够在后盾或者通过接口进行设置。

服务器对三种音讯执行不同的限速策略,在高并发时,低优先级音讯被抛弃的概率最大。

服务器将三种音讯别离存储在三个音讯桶中,客户端在拉取音讯时依照白名单音讯 > 高优先级音讯 > 低优先级音讯的程序拉取。

客户端的音讯接管和渲染优化

在音讯同步机制方面,如果聊天室每收到一条音讯都间接下发到客户端,无疑会给客户端带来极大性能挑战。特地是在每秒几千或上万条音讯的并发场景下,继续的音讯解决会占用客户端无限的资源,影响用户其它方面的互动。

思考到以上问题,融云为聊天室独自设计了告诉拉取机制,由服务端进行一系列分频限速聚合等管制后,再告诉客户端拉取。具体分为以下几步:

① 客户端胜利退出聊天室;

② 服务端下发告诉拉取信令;

③ 客户端依据本地存储的音讯最大工夫戳,去服务端拉取音讯。

这里须要留神的是,首次退出聊天室时,本地并没有无效工夫戳,此时会传 0 给服务拉取最近 50 条音讯并存库。后续再次拉取时才会传递数据库里存储的音讯的最大工夫戳,进行差量拉取。

客户端拉取到音讯后,会进行排重解决,而后将排重后的数据上抛业务层,以防止下层反复显示。

另外直播聊天室中的音讯即时性较强,直播完结或用户退出聊天室后,之前拉取的音讯大部分不须要再次查看,因而融云在用户退出聊天室时,会革除数据库中该聊天室的所有音讯,以节约存储空间。

在音讯渲染方面,客户端也通过一系列优化保障在直播聊天室大量刷屏的场景下仍有不俗的体现。

① 采纳 MVVM 机制,将业务解决和 UI 刷新严格辨别。每收到一条音讯,都在 ViewModel 的子线程将所有业务解决好,并将页面刷新须要的数据筹备结束后,才告诉页面刷新。

② 准确应用 LiveData 的 setValue() 和 postValue() 办法。曾经在主线程的事件通过 setValue() 形式告诉 View 刷新,以防止过多的 postValue() 造成主线程负担过重。

③ 缩小非必要刷新。比方在音讯列表滑动时,并不需要将接管到的新音讯刷新进去,仅进行提醒即可。

④ 通过谷歌的数据比照工具 DiffUtil 辨认数据是否有更新,仅更新有变更的局部数据。

⑤ 管制全局刷新的次数,尽量通过部分刷新进行 UI 更新。

通过以上机制,从压测后果看,在中端手机、聊天室每秒 400 条音讯时,音讯列表体现晦涩,没有卡顿。

面向海量并发的自定义属性

存储和散发优化

在直播聊天室业务中,除了失常的收发音讯外,业务层常常须要设置本人的一些业务属性,如在语音直播聊天室场景中的主播麦位信息、角色治理等,还有狼人杀等卡牌类游戏场景中记录用户的角色和牌局状态等。

绝对于聊天室音讯,自定义属性有必达和时效的要求,比方麦位、角色等信息须要实时同步给聊天室的所有成员,而后客户端再依据自定义属性刷新本地的业务。

自定义属性的存储:

自定义属性是以 key 和 value 的模式进行传递和存储的,自定义属性的操作行为次要有两种:设置、删除。服务器存储自定义属性也分两局部,别离是全量的自定义属性汇合,以及自定义属性汇合变更记录。如下图 7 所示:


(图 7 自定义属性存储构造)

服务器存储的两份数据,提供了两种查问聊天自定义属性的接口,别离是查问全量数据和查问增量数据。这两种接口的组合利用极大地晋升了聊天室服务的属性查问响应和自定义散发能力。

自定义属性的拉取:

内存中的全量数据,次要给从未拉取过自定义属性的成员应用。刚进入聊天室的成员,间接拉取全量自定义属性数据而后展现即可。

对于曾经拉取过全量数据的成员来说,若每次都拉取全量数据,客户端想取得本次的批改内容,就须要比对客户端的全量自定义属性与服务器端的全量自定义属性,无论比对行为放在哪一端,都会减少肯定的计算压力。

所以,为了实现增量数据的同步,构建一份属性变更记录汇合十分必要。这样,大部分成员在收到自定义属性有变更来拉取时,都能够取得增量数据。

属性变更记录采纳的是一个有序的 map 汇合。key 为变更工夫戳,value 里存着变更的类型以及自定义属性内容,这个有序的 map 提供了这段时间内所有的自定义属性的动作。

自定义属性的散发逻辑与音讯统一,均为告诉拉取。客户端在收到自定义属性变更拉取的告诉后,带着本人本地最大自定义属性的工夫戳来拉取,比方,如果客户端传的工夫戳为 4,则会拉取到工夫戳为 5 和工夫戳为 6 的两条记录。客户端拉取到增量内容后在本地进行回放,而后对本人本地的自定义属性进行批改和渲染。

应用的最佳实际

基于聊天室自定义属性,能够十分不便地实现聊天室内一些业务的管制及刷新。

接下来,展现一下客户端在聊天室属性应用的次要步骤以及代码示例:

设置全局属性监听器

private void setKVListener() {RongChatRoomClient.KVStatusListener kvStatusListener = new RongChatRoomClient.KVStatusListener() {
            @Override
            public void onChatRoomKVSync(String roomId) {resetTimer();// 清空计时器
            }

            @Override
            public void onChatRoomKVUpdate(String roomId, Map<String, String> chatRoomKvMap) {updateSeatInfo(roomId,chatRoomKvMap, ActionType.UPDATE);// 依据回调的 KV 信息刷新页面。}

            @Override
            public void onChatRoomKVRemove(String roomId, Map<String, String> chatRoomKvMap) {updateSeatInfo(roomId,chatRoomKvMap, ActionType.DELETE);// 依据回调的 KV 信息刷新页面。}
        };
        RongChatRoomClient.getInstance().addKVStatusListener(kvStatusListener);
    }

onChatRoomKVSync() 是胜利退出聊天室后,客户端和服务实现属性同步时的回调。此处在回调内将计时器清零,否则在计时器到时后弹出 Toast 给用户揭示。

onChatRoomKVUpdate() 是聊天室属性发生变化时的回调,首次退出聊天室时,会回调聊天室内的全量属性。依据此回调里的信息间接更新 UI 即可。

onChatRoomKVRemove() 是聊天室内某个属性被删除时的回调,只需在回调办法里更新 UI 即可。

设置聊天室属性

private void setKV(String rooId, boolean isAutoDel, boolean overWrite) {Map<String, String> kvMap = new ArrayMap<>();
        kvMap.put("s1", "user1"),
                kvMap.put("s2", "user2"),
                kvMap.put("s3", "user3"),
                kvMap.put("s4", "user4"),
                RongChatRoomClient.getInstance().setChatRoomEntries(rooId, kvMap, isAutoDel, overWrite, new IRongCoreCallback.SetChatRoomKVCallback() {
                    @Override
                    public void onSuccess() {Log.e("ChatRoomStatusDeatil", "setChatRoomEntries===onSuccess");
                        updateUI(); // 设置胜利,更新 UI}

                    @Override
                    public void onError(IRongCoreEnum.CoreErrorCode coreErrorCode, Map<String, IRongCoreEnum.CoreErrorCode> map) {Log.e("ChatRoomStatusDeatil", "setChatroomEntry===onError" + coreErrorCode);
                    }
                });
    }
退出移动版