融云直播聊天室高可用架构及平滑扩缩容实现方案

xzdxmynet 发布于 2024-02-08 阅读(99)

融云最近推出了直播SDK,可以分两步实现视频直播能力。 第二步“开始直播”,可以通过调用接口发布视频流,其他用户可以加入房间观看直播并在公屏上发表评论与主播互动。

在直播中,弹幕互动是用户与主播互动的主要方式,利用IM中的聊天室功能。 融云直播SDK完整封装了直播业务所需的所有功能,包括久经考验的无限聊天室组件,让直播聊天室轻松应对数十亿消息并发。

本文主要分享了融云直播聊天室高可用架构和平滑扩缩容的实现方案,并详细介绍了聊天室服务的两大瓶颈:无限人数和如何破解海量消息并发。

实时聊天室主要功能

除了发送和管理用户最可见的多种类型消息外,实时聊天室还承担用户管理等任务。 如今,一切都可以直播。 超大直播场景屡见不鲜,这对无限人数、海量并发消息的直播聊天室提出了挑战。 这两项高级能力的支持程度决定了直播间服务的上限。 详情如下:

丰富的聊天室消息类型和高级消息功能:

方便详细的聊天室管理功能:

聊天室人数没有上限:

一些大型直播场景,如春晚、国庆阅兵等,直播间可以吸引数千万观众,同时观看人数可达数百万。

另外,直播间的一大特点是用户进出聊天室非常频繁,热点直播间的并发进出人数可能达到每秒数万人。 这对服务支持用户线上线下以及用户管理的能力提出了很大的挑战。

海量消息并发:

实时聊天室的人数没有上限,这自然带来了海量并发消息的问题。 在数百万人的聊天室中,消息的向上流量已经是巨大的,消息分发量呈指数级增长。

如果服务器只进行消息消峰处理,峰值消息的积累会导致整体消息延迟增加。 延迟的累积效应会导致消息和直播视频流在时间线上发生偏差,从而影响用户观看直播时交互的实时性。 因此,服务器的海量消息分发能力非常重要。

实时聊天室的结构

高可用架构

高可用性是分布式系统架构设计必须考虑的因素之一。 简单来说就是减少系统无法提供服务的时间。

高可用系统需要支持服务自动故障切换、精准的服务熔断和降级、服务治理、服务限流、服务回滚、服务自动扩缩容等能力。

以服务高可用性为目标,融云直播间系统架构如图1所示:

(图1融云直播间系统架构)

该系统架构主要分为三层:

连接层:主要管理服务与客户端之间的长链接;

存储层:目前使用Redis作为二级缓存,主要存储聊天室信息,如人员名单、黑白名单、禁止名单等,当服务更新或重启时,聊天室的备份信息房间可以从Redis加载;

业务层:这是整个聊天室的核心。 为了实现跨机房容灾,融云将服务部署在多个可用区,并根据能力和职责划分为聊天室服务和消息服务。

聊天室服务主要负责处理管理请求,如聊天室人员的进出、屏蔽/禁止、上游消息处理和审核等; 消息服务主要负责缓存本节点需要处理的用户信息和消息队列信息,并负责聊天室消息的处理。 分配。

在大量用户的高并发场景下,消息分发能力将决定系统的性能。 以百万用户的聊天室为例,一条上行消息对应一百万次分发。 在这种情况下,依靠单台服务器无法实现海量消息的分发。

荣云的优化思路是,将一个聊天室里的人拆分到不同的消息服务中。 聊天室服务收到消息后,扩散到消息服务,然后消息服务分发给用户。 以百万人在线的聊天室为例,假设总共有200个聊天室消息服务,则每个消息服务平均管理5000人左右。 各个消息服务在分发消息时,只需将消息分发给自己服务器上的用户即可。 就是这样。

聊天室业务中,聊天室上行信令采用一致性哈希算法,根据聊天室ID选择节点; 消息服务中,根据用户ID采用一致性哈希算法来判断用户落在哪条消息上。 服务。

一致性哈希选择的位置相对固定,可以将聊天室行为聚合到一个节点上,大大提高服务的缓存命中率。 聊天室人员进出、黑白名单设置、发送消息时的判断等都可以直接通过访问内存来处理。 无需每次都访问第三方缓存,从而提高了聊天室的响应速度和分发速度。

最后,在架构中主要用于服务发现,对每个服务实例进行注册。

平滑膨胀和收缩

作为安全可靠的全球互联网通信云服务提供商,保证任何情况下服务的连续性和可用性是融云的使命。

随着直播的形式被越来越多的人接受,直播聊天室越来越面临着人数激增的情况,导致服务器压力逐渐增大。 因此,能够随着业务压力逐渐增大/减小而平滑地扩缩容是非常重要的。

在服务的自动扩缩容方面,业界提供的解决方案大体一致。 通过压力测试了解单台服务器的瓶颈点 → 通过监控业务数据判断是否需要扩缩容 → 触发设定条件后进行报警并自动扩缩容。

鉴于直播间的业务性较强,具体实施时必须保证扩缩容时聊天室整体业务不受影响。

聊天室服务扩容和缩容:

当聊天室服务扩容或缩容时,可以使用Redis加载会员列表、封禁/黑名单等信息。 需要注意的是,当聊天室被自动销毁时,需要首先判断当前聊天室是否属于该节点。 如果没有,则跳过销毁逻辑,避免由于销毁逻辑导致Redis中的数据丢失。 详细信息如图2所示:

(图2聊天室服务扩缩容方案)

消息服务扩缩容:

当消息服务扩容或缩容时,需要根据一致性哈希原理将大部分成员路由到新的消息服务节点。 这一过程将打破目前的人员平衡,进行整体人员调整。

在扩张的时候,荣云根据聊天室的活跃程度,逐步调人。

当聊天室有消息时,消息服务会遍历该节点缓存的所有用户,拉取消息通知。 在此过程中,会判断用户是否属于该节点。 如果没有,则该用户将被同步添加到属于他的列表中。 节点。

当用户拉取消息时,如果该用户不在本地缓存列表中,则消息服务会向聊天室服务发送请求,确认该用户是否在聊天室中。 如果用户在聊天室,则会同步添加到消息服务中。 如果没有,则直接丢弃。

当消息服务缩容时,消息服务会从公共Redis中获取所有成员,根据落点计算过滤掉该节点的用户,并将其放入用户管理列表中。

无限的用户管理和消息分发

用户登录及离线管理

聊天室服务管理所有人员的进入和退出,人员列表的变化也异步存储在Redis中。

消息服务拥有自己的聊天室人员。 当用户主动加入和退出房间时,需要基于一致性哈希计算放置点并同步到相应的消息服务。

聊天室获取消息后,聊天室服务将其广播到所有聊天室消息服务,消息服务拉取该消息的通知。 消息服务会检测用户的消息拉取状态。 当聊天室处于活跃状态,并且该人在30秒内未拉取消息或总共30条消息未拉取时,消息服务将确定当前用户离线,然后将该人踢出。 并同步到聊天室服务,使会员离线登录。

数十亿条消息的分发策略

聊天室服务的消息分发和拉取方案如图3所示:

(图3聊天室服务的消息分发和拉取解决方案)

消息通知拉取:

在图3中,用户A在聊天室中发送消息,该消息首先由聊天室服务处理。 聊天室服务将消息同步到各个消息服务节点。 消息服务向该节点中缓存的所有成员发送通知拉取。 图 服务器向用户B和用户Z发送通知。

在消息分发过程中,完成了通知合并。

通知拉取的具体流程为:

① 客户端加入聊天成功,并将所有成员添加到待通知队列中(如果已存在,则更新通知消息时间);

② 发送线程并进行轮转训练,获取需要通知的队列;

③ 向队列中的用户发送通知。

通过这个过程,可以保证发布线程在一轮中只向同一个用户发送一个通知拉取,即多条消息会合并为一个通知拉取,有效提高了服务器的性能,减少了客户端和服务器的网络消耗。

消息拉取:

用户消息拉取流程如图4所示:

(图4 用户消息拉取流程)

用户B收到通知后,向服务器发送拉取消息请求。 该请求最终会被消息节点1处理。消息节点1会根据客户端传递的最后一条消息的时间戳,从消息队列中返回一个消息列表。 参考下图5:

(图5客户端拉取消息示例)

客户端最大本地时间为00,从机可以拉取两条大于该数字的消息。

消息速度控制:

当服务器处理海量消息时,需要控制消息速度。 这是因为在直播聊天室中,大量用户同时发送海量消息,而且一般内容基本相同。 如果所有消息都分发到客户端,客户端很可能会出现卡顿、消息延迟等问题,严重影响用户体验。

因此,服务器对上下行消息都进行了限速处理。

(图6消息速度控制)

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

服务器上行限速控制(丢弃)策略:对单个聊天室的上行消息进行限速控制。 默认200条消息/秒,可以根据业务需求调整。 达到速率限制后发送的消息将被聊天室服务丢弃,并且不再同步到各个消息服务节点。

服务器下行限速(丢弃)策略:服务器下行限速控制主要根据消息环队列的长度进行控制。 达到最大值后,最旧的消息将被淘汰并丢弃。

每次发送拉取通知后,服务器都会将用户标记为已拉取,待用户真正拉取消息后,该标记将被移除。

如果用户在新消息产生时有拉取标记,且标记时间在2秒以内,则不会发送通知(减少客户端压力,丢弃通知但不丢弃消息); 如果超过2秒,则继续发送通知(如果连续多次没有拉取通知,则会触发用户踢出策略,此处不再赘述)

因此,消息是否被丢弃取决于客户端的拉取速度(受客户端性能和网络影响)。 如果客户端及时拉取消息,就不会出现消息被丢弃的情况。

聊天室消息优先级

消息速度控制的核心是消息的选择,这需要对消息进行优先级排序:

白名单消息,此类消息最重要,级别最高。 一般情况下,系统通知或管理信息会被设置为白名单消息;

优先级高,仅次于白名单消息。 没有特殊设置的消息都是高优先级;

低优先级,最低优先级的消息,这些消息大部分是文本消息。

开发者可以在后台或通过界面设置具体的划分。

服务器针对三种类型的消息实施不同的限速策略。 当并发度较高时,低优先级的消息被丢弃的概率最高。

服务器将三种类型的消息分别存储在三个消息桶中。 客户端拉取消息时,按照白名单消息>高优先级消息>低优先级消息的顺序拉取消息。

客户端消息接收和渲染优化

从消息同步机制来看,如果聊天室收到的每一条消息都直接发送给客户端,无疑会给客户端带来很大的性能挑战。 尤其是在每秒几千、几万条消息的并发场景下,连续的消息处理会占用客户端有限的资源,影响用户交互的其他方面。

考虑到上述问题,融云为聊天室设计了单独的通知拉取机制。 服务器进行一系列分频、限速、聚合等控制后,通知客户端拉取。 具体分为以下步骤:

① 客户端成功加入聊天室;

② 服务器发送通知拉取信令;

③ 客户端根据本地存储的消息的最大时间戳去服务端拉取消息。

这里需要注意的是,第一次加入聊天室时,本地没有有效的时间戳。 此时会传0给服务拉取最新的50条消息存入数据库。 稍后再次拉取时,会传递数据库中存储的消息的最大时间戳,进行差异拉取。

客户端拉取消息后,会进行去重处理,然后将去重后的数据向上抛给业务层,避免在上层重复显示。

此外,实时聊天室中的消息具有很高的即时性。 直播结束或者用户退出聊天室后,之前拉取的消息大部分不需要再次查看。 因此,当用户退出聊天室时,融云会清除数据库中的聊天室。 所有消息以节省存储空间。

在消息渲染方面,客户端也经过了一系列的优化,保证在直播聊天室大量屏幕消耗的场景下依然有良好的表现。

① 使用MVVM机制严格区分业务处理和UI刷新。 每次收到消息,子线程都会处理完所有的业务,准备好页面刷新所需的数据,然后再通知页面刷新。

②准确运用()、()方法。 主线程已经发生的事件通过()通知View刷新,避免过多的()导致主线程负担过重。

③ 减少不必要的刷新。 比如消息列表滑动时,不需要刷新收到的新消息,只是提示。

④ 利用Google的数据对比工具识别数据是否更新,仅更新发生变化的数据。

⑤ 控制全局刷新次数,尽量通过局部刷新来更新UI。

通过上述机制,根据压测结果,中端手机和聊天室每秒接收400条消息时,消息列表表现流畅,无卡顿。

海量并发的自定义属性

存储和分配优化

在直播间业务中,业务层除了正常的收发消息外,往往还需要设置一些自己的业务属性,比如直播间语音聊天室场景中主持人的麦位信息、角色管理等,还有狼人等卡牌。 在类似游戏的场景中记录用户的角色和卡牌游戏状态。

与聊天室消息相比,自定义属性有一定的要求和时效性。 例如,麦位、角色等信息需要实时同步到聊天室所有成员,然后客户端根据自定义属性刷新本地业务。

自定义属性的存储:

自定义属性以键和值的形式传输和存储。 自定义属性主要有两种操作:设置和删除。 服务器对自定义属性的存储也分为两部分,即全套自定义属性和自定义属性集的变更记录。 如下图7所示:

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

服务器上存储的两条数据提供了两种查询聊天自定义属性的接口,即查询全量数据和查询增量数据。 这两个接口的结合应用,大大提高了聊天室服务的属性查询响应和定制化分发能力。

拉取自定义属性:

内存中的全量数据主要由从未拉取过自定义属性的成员使用。 刚进入聊天室的会员可以直接拉取所有自定义属性数据并展示。

对于已经拉取全量数据的会员,如果每次拉取全量数据,客户端如果想要获取这次修改,需要将客户端自定义属性的全量与全量进行比较服务器端的自定义属性。 无论比较行为放在哪一端,都会增加一定的计算压力。

因此,为了实现增量数据同步,需要构建一组属性变化记录。 通过这种方式,大多数成员在收到要拉取的自定义属性的更改时可以获得增量数据。

属性变化记录使用有序映射集合。 键为变更时间戳,值存储变更类型和自定义属性内容。 此有序映射提供了此期间的所有自定义属性操作。

自定义属性的分发逻辑和消息一致,都是通知拉取。 客户端收到自定义属性变更拉取通知后,以本地最大自定义属性的时间戳进行拉取。 比如客户端传过来的时间戳是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 chatRoomKvMap) {
              updateSeatInfo(roomId,chatRoomKvMap, ActionType.UPDATE);//根据回调的 KV 信息刷新页面。
            }
            @Override
            public void onChatRoomKVRemove(String roomId, Map chatRoomKvMap) {
                  updateSeatInfo(roomId,chatRoomKvMap, ActionType.DELETE);//根据回调的 KV 信息刷新页面。
            }
        };
        RongChatRoomClient.getInstance().addKVStatusListener(kvStatusListener);
    }

() 是客户端和服务成功加入聊天室后完成属性同步时的回调。 这里,定时器是在回调内清除的,否则定时器到期后会弹出一个Toast提醒用户。

()是聊天室属性改变时的回调。 第一次加入聊天室时,聊天室中的所有属性都会被回调。 直接根据这个回调中的信息更新UI即可。

() 是删除聊天室属性时的回调。 您只需要在回调方法中更新UI即可。

设置聊天室属性

private void setKV(String rooId, boolean isAutoDel, boolean overWrite) {
        Map 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 map) {
                        Log.e("ChatRoomStatusDeatil", "setChatroomEntry===onError" + coreErrorCode);
                    }
                });
    }
 

标签:  聊天室 高可用 自定义属性 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。