Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 422, comments - 13, trackbacks - 0, articles - 0

本文由作者“大白菜”分享,有较多修订和改动。注意:本系列是给IM初学者的文章,IM老油条们还望海涵,勿喷!

1、引言

前两篇《编码实践篇(单聊功能)》、《编码实践篇(群聊功能)》分别实现了控制台版本的IM单聊和群聊的功能。

通过前两篇这两个小案例来体验的只是Netty在IM系统这种真实的开发实践,但对比在真实的Netty应用开发当中,本系列的案例是非常的简单的,主要目的其实是让大家可以更好地了解其原理,从而写出更高质量的 Netty 代码。

不过,虽然 Netty 的性能很高,但是也不能保证随意写出来的项目就是性能很高的,所以本篇将主要讲解几个基于Netty的IM系统的优化实战技术点。

学习交流:

本文同步发布于:http://www.52im.net/thread-3988-1-1.html

2、写在前面

建议你在阅读本文之前,务必先读本系列的前三篇《IM系统设计篇》、《编码实践篇(单聊功能)》、《编码实践篇(群聊功能)》。

最后,在开始本文之前,请您务必提前了解Netty的相关基础知识,可从本系列首篇《IM系统设计篇》中的“知识准备”一章开始。

3、系列文章

本文是系列文章的第3篇,以下是系列目录:

4、基于Netty的IM系统常见优化方向

常见优化方向脑图:

我们逐条详细解释一下这些优化的目的:

  • 1)心跳检测:主要是避免连接假死现象;
  • 2)连接断开:则删除通道绑定属性、删除对应的映射关系,这些信息都是保存在内存当中的,如果不删除则造成资源浪费;
  • 3)性能问题:用户 ID 和 Channel 的关系绑定存在内存当中,比如:Map,key 是用户 ID,value 是 Channel,如果用户量多的情况(客户端数量过多),那么服务端的内存将被消耗殆尽;
  • 4)性能问题:每次服务端往客户端推送消息,都需从Map里查找到对应的Channel,如果数量较大和查询频繁的情况下如何保证查询性能;
  • 5)安全问题:HashMap 是线程不安全的,并发情况下,我们如何去保证线程安全;
  • 6)身份校验:如何 LoginHandler 是负责登录认证的业务 Handler,AuthHandler 是负责每次请求时校验该请求是否已经认证了,这些 Handler 在链接就绪时已经被添加到 Pipeline 管道当中,其实,我们可以采用热插拔的方式去把一些在做业务操作时用不到的 Handler 给剔除掉。

以上是基于Netty的IM系统开发当中,需要去注意的技术优化点,当然还有很多其他的细节,比如:线程池这块,需要大家慢慢去从实战中积累。

5、本篇优化方向

本篇主要的优化内容主要是在第二篇单聊功能第三篇群聊功能的基础上继续完善几点。

具体的优化方向如下:

  • 1)无论客户端还是服务端都分别只有一个 Handler,这样的话,业务越来越多,Handler 里面的代码就会越来越臃肿,我们应该想办法把 Handler 拆分成各个独立的 Handler;
  • 2)如果拆分的 Handler 很多,每次有连接进来,那么都会触发 initChannel () 方法,所有的 Handler 都得被 new 一遍,我们应该把这些 Handler 改成单例模式(不需要每次都 new,提高效率);
  • 3)发送消息时,无论是单聊还是群聊,对方不在线,则把消息缓存起来,等待其上线再推送给他;
  • 4)连接断开时,无论是主动和被动,需要删除 Channel 属性、删除用户和 Channel 映射关系。

6、业务拆分以及单例模式优化

6.1 概述

主要优化细节如下:

  • 1)自定义 Handler 继承 SimpleChannelInboundHandler,那么解码的时候,会自动根据数据格式类型转到相应的 Handler 去处理;
  • 2)@Shareable 修饰 Handler,保证 Handler 是可共享的,避免每次都创建一个实例。

6.2 登录Handler优化

@ChannelHandler.Sharable

public class ClientLogin2Handler extends SimpleChannelInboundHandler<LoginResBean> {

    //1.构造函数私有化,避免创建实体

    private ClientLogin2Handler(){}

    //2.定义一个静态全局变量

    public static ClientLogin2Handler instance=null;

    //3.获取实体方法

    public static ClientLogin2Handler getInstance(){

        if(instance==null){

            synchronized(ClientLogin2Handler.class){

                if(instance==null){

                    instance=new ClientLogin2Handler();

                }

            }

        }

        return instance;

    }

 

    protected void channelRead0(

        ChannelHandlerContext channelHandlerContext,

        LoginResBean loginResBean) throws Exception {

 

        //具体业务代码,参考之前

    }

}

6.3 消息发送Handler优化

@ChannelHandler.Sharable

public class ClientMsgHandler extends SimpleChannelInboundHandler<MsgResBean> {

    //1.构造函数私有化,避免创建实体

    private ClientMsgHandler(){}

    //2.定义一个静态全局变量

    public static ClientMsgHandler instance=null;

    //3.获取实体方法

    public static ClientMsgHandler getInstance(){

        if(instance==null){

            synchronized(ClientMsgHandler.class){

                if(instance==null){

                    instance=new ClientMsgHandler();

                }

            }

        }

        return instance;

    }

 

    protected void channelRead0(

        ChannelHandlerContext channelHandlerContext,

        MsgResBean msgResBean) throws Exception {

 

        //具体业务代码,参考之前

    }

}

6.4 initChannel方法优化

.handler(newChannelInitializer<SocketChannel>() {

    @Override

    public void initChannel(SocketChannel ch) {

        //1.拆包器

        ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,5,4));

        //2.解码器

        ch.pipeline().addLast(new MyDecoder());

        //3.登录Handler,使用单例获取

        ch.pipeline().addLast(ClientLogin2Handler.getInstance());

        //4.消息发送Handler,使用单例获取

        ch.pipeline().addLast(ClientMsgHandler.getInstance());

        //5.编码器

        ch.pipeline().addLast(new MyEncoder());

    }

});

6.5 小结

这种业务拆分以及单例模式优优化是Netty开发当中很常用的,可以更好的维护基于Netty的代码并提高应用性能。

7、数据缓存优化

为了提高用户体验,在发送消息(推送消息)时,如果接收方不在线,则应该把消息缓存起来,等对方上线时,再推送给他。

7.1 数据缓存到集合

//1.定义一个集合存放数据(真实项目可以存放数据库或者redis缓存),这样数据比较安全。

private List<Map<Integer,String>> datas=new ArrayList<Map<Integer,String>>();

 

//2.服务端推送消息

private void pushMsg(MsgReqBean bean,Channel channel){

    Integer touserid=bean.getTouserid();

    Channel c=map.get(touserid);

 

    if(c==null){//对方不在线

        //2.1存放到list集合

        Map<Integer,String> data=new HashMap<Integer, String>();

        data.put(touserid,bean.getMsg());

        datas.add(data);

 

        //2.2.给消息“发送人”响应

        MsgResBean res=new MsgResBean();

        res.setStatus(1);

        res.setMsg(touserid+">>>不在线");

        channel.writeAndFlush(res);

 

    }else{//对方在线

        //2.3.给消息“发送人”响应

        MsgResBean res=new MsgResBean();

        res.setStatus(0);

        res.setMsg("发送成功);

        channel.writeAndFlush(res);

 

        //2.4.给接收人推送消息

        MsgRecBean res=new MsgRecBean();

        res.setFromuserid(bean.getFromuserid());

        res.setMsg(bean.getMsg());

        c.writeAndFlush(res);

    }

}

7.2 上线推送

private void login(LoginReqBean bean, Channel channel){

    Channel c=map.get(bean.getUserid());

    LoginResBean res=new LoginResBean();

    if(c==null){

        //1.添加到map

        map.put(bean.getUserid(),channel);

        //2.给通道赋值

        channel.attr(AttributeKey.valueOf("userid")).set(bean.getUserid());

        //3.登录响应

        res.setStatus(0);

        res.setMsg("登录成功");

        res.setUserid(bean.getUserid());

        channel.writeAndFlush(res);

 

        //4.根据user查找是否有尚未推送消息

        //思路:根据userid去lists查找.......

 

    }else{

        res.setStatus(1);

        res.setMsg("该账户目前在线");

        channel.writeAndFlush(res);

    }

}

8、连接断开事件处理优化

如果客户端网络故障导致连接断开了(非主动下线),那么服务端就应该能监听到连接的断开,且此时应删除对应的 map 映射关系。但是映射关系如果没有删除掉,将导致服务器资源没有得到释放,进而影响客户端的下次同一个账号登录以及大量的客户端掉线时性能。

8.1 正确写法

实例:

public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {

    //映射关系

    private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();

    //连接断开,触发该事件

    @Override

    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        //1.获取Channel

        Channel channel=ctx.channel();

 

        //2.从map里面,根据Channel找到对应的userid

        Integer userid=null;

        for(Map.Entry<Integer, Channel> entry : map.entrySet()){

            Integer uid=entry.getKey();

            Channel c=entry.getValue();

            if(c==channel){

                userid=uid;

            }

        }

        //3.如果userid不为空,则需要做以下处理

        if(userid!=null){

            //3.1.删除映射

            map.remove(userid);

            //3.2.移除标识

            ctx.channel().attr(AttributeKey.valueOf("userid")).remove();

        }

    }

}

8.2 错误写法

Channel 断开,服务端监听到连接断开事件,但是此时 Channel 所绑定的属性已经被移除掉了,因此这里无法直接获取的到 userid。

实例:

public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter {

    //映射关系

    private static Map<Integer, Channel> map=new HashMap<Integer, Channel>();

 

    //连接断开,触发该事件

    @Override

    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        //1.获取Channel绑定的userid

        Object userid=channel.attr(AttributeKey.valueOf("userid")).get();

 

        //2.如果userid不为空

        if(userid!=null){

            //1.删除映射

            map.remove(userid);

            //2.移除标识

            ctx.channel().attr(AttributeKey.valueOf("userid")).remove();

        }

    }

}

9、本篇小结

本篇内容还是相对容易理解的,主要是优化前面两篇实现的IM聊天功能,优化内容是业务 Handler 的拆分以及使用单例模式、接受人不在线则缓存数据、等其上线再推送、监听连接断开删除对应的映射关系。

限于篇幅,本系列文章文章没办法真正讲解开发一个完整IM系统所涉及的方方面面,如果有兴趣,可以继续阅读更有针对性的IM开发文章,比如IM架构设计IM通信协议IM通信安全群聊优化弱网优化网络保活等。

10、参考资料

[1] 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

[2] 理论联系实际:一套典型的IM通信协议设计详解

[3] 浅谈IM系统的架构设计

[4] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端

[5] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

[6] 一套原创分布式即时通讯(IM)系统理论架构方案

[7]  一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[8] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

[9] 从新手到专家:如何设计一套亿级消息量的分布式IM系统

[10] 基于实践:一套百万消息量小规模IM系统技术要点总结

[11] 探探的IM长连接技术实践:技术选型、架构设计、性能优化

[12] 拿起键盘就是干,教你徒手开发一套分布式IM系统

[13] 万字长文,手把手教你用Netty打造IM聊天

[14] 基于Netty实现一套分布式IM系统

[15] SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM聊天功能

本文同步发布于:http://www.52im.net/thread-3988-1-1.html

posted @ 2022-07-25 12:02 Jack Jiang 阅读(126) | 评论 (0)编辑 收藏

一、更新内容简介

本次更新为次要版本更新,进行了若干优化(更新历史详见:码云 Release Nodes)。可能是市面上唯一同时支持 UDP+TCP+WebSocket 三种协议的同类开源IM框架。

二、MobileIMSDK简介

MobileIMSDK 是一套专为移动端开发的原创IM通信层框架:

  • 历经8年、久经考验;
  • 超轻量级、高度提炼,lib包50KB以内;
  • 精心封装,一套API同时支持UDP、TCP、WebSocket三种协议(可能是全网唯一开源的);
  • 客户端支持 iOSAndroid标准JavaH5小程序(开发中..)、Uniapp(开发中..);
  • 服务端基于Netty,性能卓越、易于扩展;👈
  • 可与姊妹工程 MobileIMSDK-Web 无缝互通实现网页端聊天或推送等;👈
  • 可应用于跨设备、跨网络的聊天APP、企业OA、消息推送等各种场景。

MobileIMSDK工程始于2013年10月,起初用作某产品的即时通讯底层实现,完全从零开发,技术自主可控!

您可能需要:查看关于MobileIMSDK的详细介绍

三、代码托管同步更新

OsChina.net

GitHub.com

四、MobileIMSDK设计目标

让开发者专注于应用逻辑的开发,底层复杂的即时通讯算法交由SDK开发人员,从而解偶即时通讯应用开发的复杂性。

五、MobileIMSDK框架组成

整套MobileIMSDK框架由以下5部分组成:

  1. Android客户端SDK:用于Android版即时通讯客户端,支持Android 2.3及以上,查看API文档
  2. iOS客户端SDK:用于开发iOS版即时通讯客户端,支持iOS 8.0及以上,查看API文档
  3. Java客户端SDK:用于开发跨平台的PC端即时通讯客户端,支持Java 1.6及以上,查看API文档
  4. H5客户端SDK:暂无开源版,查看精编注释版
  5. 服务端SDK:用于开发即时通讯服务端,支持Java 1.7及以上版本,查看API文档

整套MobileIMSDK框架的架构组成:

 另外:MobileIMSDK可与姊妹工程 MobileIMSDK-Web 无缝互通,从而实现Web网页端聊天或推送等。

六、MobileIMSDK v6.2更新内容 

【重要说明】:

MobileIMSDK v6.2 为次要版本,进行了若干优化! 查看详情

【新增的特性】:

  1. [服务端] 新增两个聊天消息前置处理回调,方便开发者进行内容鉴黄、过滤、修改等运营管理;
  2. [服务端] 新增新增了一个与 Web 互通情况下的 C2C 模式回调,用于开发者在互通模式下实现离线消息 Push 逻辑;

【其它优化和提升】:

  1. [Andriod] 支持最新的 Andriod 12,解决了 Demo 工程中的 Andriod12 兼容问题;
  2. [Andriod] 解决了 Demo 工程在最新 Android Studio 编译时报方法数超过 65535 的经典问题;
  3. [服务端] 升级 log4j2 至 2.17.0,解决 Log4j2 远程代码执行高危漏洞;
  4. [服务端] 为 ServerEventListener 类中的 onUserLogout 回调增加 beKickoutCode 参数;
  5. [服务端] [优化] 尝试解决与 Web 互通情况下,MQProvider 中的 work 方法会因异步消息导致的 AlreadCloseException 问题;

【版本地址】:

https://gitee.com/jackjiang/MobileIMSDK/releases/6.2

posted @ 2022-07-20 10:29 Jack Jiang 阅读(88) | 评论 (0)编辑 收藏

     摘要: 本文由作者“大白菜”分享,有较多修订和改动。注意:本系列是给IM初学者的文章,IM老油条们还望海涵,勿喷!1、引言接上两篇《IM系统设计篇》、《编码实践篇(单聊功能)》,本篇主要讲解的是通过实战编码实现IM的群聊功能,内容涉及群聊技术实现原理、编码实践等知识。学习交流:- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》- 开源IM框架源码:https://...  阅读全文

posted @ 2022-07-18 15:06 Jack Jiang 阅读(101) | 评论 (0)编辑 收藏

     摘要: 本文由作者“大白菜”分享,个人博客 cmsblogs.cn,有较多修订和改动。注意:本系列是给IM初学者的文章,IM老油条们还望海涵,勿喷!1、引言接上篇《IM系统设计篇》,本篇主要讲解的是通过实战编码实现IM的单聊功能,内容涉及技术原理、编码实践。补充说明:因为本系列文章主要目的是引导IM初学者在基于Netty的情况下,如何一步一步从零写出IM的逻辑和思维能力,因而为了简...  阅读全文

posted @ 2022-07-11 11:39 Jack Jiang 阅读(100) | 评论 (0)编辑 收藏

本文收作者“大白菜”分享,有改动。注意:本系列是给IM初学者的文章,IM老油条们还望海涵,勿喷!

1、引言

这又是一篇基于Netty的IM编码实践文章,因为合成一篇内容太长,读起来太累,所以也就顺着作者的思路分开成4篇,读起来心理压力也就没那么大了。

这个系列的几篇文章分享的是:假设在没有任何成型的第3方IM库或SDK的情况下,以网络编程的基础技术视野,思考和实践如何基于Netty网络库从零写一个可以聊天的IM系统的过程,没有眼花缭乱的架构设计、也没有高端大气的模式设计方法论,有的只是从IM入门者的角度的思路和实战,适合IM初学者阅读。

本篇主要是徒手撸IM系列的开篇,主要讲解的是的IM设计思路,不涉及实践编码,希望给你带来帮助。

学习交流:

本文已同步发布于:http://www.52im.net/thread-3963-1-1.html

2、知识准备

* 重要提示:本系列文章主要是代码实战分享,如果你对即时通讯(IM)技术理论了解的不多,建议先详细阅读:《零基础IM开发入门:什么是IM系统?》、《新手入门一篇就够:从零开发移动端IM》。

不知道 Netty 是什么?这里简单介绍下:

Netty 是一个 Java 开源框架。Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

也就是说,Netty 是一个基于 NIO 的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。

Netty 相当简化和流线化了网络应用的编程开发过程,例如,TCP 和 UDP 的 Socket 服务开发。

Netty的基础入门好文章:

如果你连Java的NIO都不知道是什么,下面的文章建议优先读:

Netty源码和API的在线查阅地址:

3、系列文章

本文是系列文章的第1篇,以下是系列目录:

  1. 基于Netty,徒手撸IM(一):IM系统设计篇》(* 本文
  2. 《基于Netty,徒手撸IM(二):编码实践篇(单聊功能)》
  3. 《基于Netty,徒手撸IM(三):编码实践篇(群聊功能)》
  4. 《基于Netty,徒手撸IM(一):编码实践篇(系统优化)》

4、需求分析

业务场景: 本次实战就是模拟微信的IM聊天,每个客户端和服务端建立连接,并且可以实现点对点通信(单聊),点对多点通信(群聊)。

设计思路: 我们要实现的是点(客户端)对点(客户端)的通讯,但是我们大部分情况下接触的业务都是客户端和服务端之间的通讯(所谓的C/S模式?),客户端只需要知道服务端的 IP 地址和端口号即可发起通讯了。那么客户端和客户端应该怎么去设计呢?

技术思考:难道是手机和手机之间建立通讯连接(所谓的P2P),互相发送消息吗?

这种方案显然不是很好的方案:

  • 1)首先: 客户端和客户端之间通讯,首先需要确定对方的 IP 地址和端口号,显然不是很现实;
  • 2)其次: 即使有办法拿到对方的 IP 地址和端口号,那么每个点(客户端)既作为服务端还得作为客户端,无形之中增加了客户端的压力。

其实:我们可以使用服务端作为IM聊天消息的中转站,由服务端主动往指定客户端推送消息。如果是这种模式的话,那么 Http 协议是无法支持的(因为Http 是无状态的,只能一请求一响应的模式),于是就只能使用 TCP 协议去实现了。

Jack Jiang注:此处作者表述不太准确,因为虽然HTTP是无状态的,但一样可以实现即时通讯能力,有兴趣的读者可以阅读以下几篇文章,了解一下这些曾经利用HTTP实现即时通讯聊天的技术方法:

  1. 新手入门贴:史上最全Web端即时通讯技术原理详解
  2. Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
  3. 网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket

5、IM单聊思路设计

5.1 通讯架构原理

以下是通讯架构原理图:

 

如上图所示,通讯流程解析如下:

  • 1)实现客户端和客户端之间通讯,那么需要使用服务端作为通讯的中转站,每个客户端都必须和服务端建立连接;
  • 2)每个客户端和服务端建立连接之后,服务端保存用户 ID 和通道的映射关系,其中用户 ID 作为客户端的唯一标识;
  • 3)客户端 A 往客户端 B 发送消息时,先把消息发送到服务端,再有服务端往客户端 B 进行推送。

针对上述第“3)”点,服务端如何找到客户端 B 呢?

客户端 A 往服务端发送消息时,消息携带的信息有:“客户端 A 用户 ID”、“客户端 B 用户 ID”、“消息内容”。这样服务端就能顺利找到服务端 B 的通道并且进行推送消息了。

5.2 消息推送流程

每个客户端和服务端建立连接的时候,必须把个人用户信息上传到服务端,由服务端统一保存映射关系。如果某个客户端下线了,则服务端监听到连接断开,删除对应的映射关系。

其次:发起群聊的时候,需要传递 touser 字段,服务端根据该字段在映射表里面查找到对应的连接通道并发起消息推送。

上述逻辑原理如下图所示:

5.3 更多的细节

其实在真正要做IM之前,要考虑的技术细节还是很多的,以下这几篇文章就步及到了典型的几个IM热门技术点,有兴趣的一定要读一读:

  1. 移动端IM开发需要面对的技术问题
  2. 谈谈移动端 IM 开发中登录请求的优化
  3. IM消息送达保证机制实现(一):保证在线实时消息的可靠投递
  4. IM消息送达保证机制实现(二):保证离线消息的可靠投递
  5. 如何保证IM实时消息的“时序性”与“一致性”?

6、IM群聊思路设计

群聊指的是一个组内多个用户之间的聊天,一个用户发到群组的消息会被组内任何一个成员接收 。

具体架构思路如下所示:

如上图所示,群聊通讯流程解析如下。

1)群聊其实和单聊整体上思路都是一致的,都是需要保存每个用户和通道的对应关系,方便后期通过用户 ID 去查找到对应的通道,再跟进通道推送消息。

2)如何把消息发送给多个组内的成员呢?

其实很简单,服务端再保存另外一份映射关系,那就是聊天室和成员的映射关系。发送消息时,首先根据聊天室 ID 找到对应的所有成员,然后再跟进各个成员的 ID 去查找到对应的通道,最后由每个通道进行消息的发送。

3)成员加入某个群聊组的时候,往映射表新增一条记录,如果成员退群的时候则删除对应的映射记录。

通过上面的架构图可以发现,群聊和单聊相比,其实就是多了一份映射关系而已。

其实群聊是IM里相对来说技术难度较高的功能,有兴趣的读者可以阅读下面这几篇:

  1. IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?
  2. IM群聊消息如此复杂,如何保证不丢不重?
  3. 移动端IM中大规模群消息的推送如何保证效率、实时性?
  4. 现代IM系统中聊天消息的同步和存储方案探讨
  5. 关于IM即时通讯群聊消息的乱序问题讨论
  6. IM群聊消息的已读回执功能该怎么实现?
  7. IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?
  8. 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

另外,对于超大规模群聊,技术难度更是指数上升:

  1. 网易云信技术分享:IM中的万人群聊技术方案实践总结
  2. 阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处
  3. IM群聊消息的已读未读功能在存储空间方面的实现思路探讨
  4. 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等
  5. 融云IM技术分享:万人群聊消息投递方案的思考和实践
  6. 微信直播聊天室单房间1500万在线的消息架构演进之路

7、本文小结

本篇主要是帮助读者掌握单聊和群聊的核心设计思路。

单聊: 主要是服务器保存了一份用户和通道之间的映射关系,发送消息的时候,根据接收人 ID 找到其对应的通道 Channel,Channel 的 write () 可以给客户端发送消息。

群聊: 保存两份关系,分别是用户 ID 和 Channel 之间的关系、群组 ID 和用户 ID 的关系。推送消息的时候,首先根据聊天组 ID 找到其对应的成员,遍历每个成员再进行找出其对应的通道即可。

整体来说,思路还是很简单的,掌握了该设计思路以后,你会发现设计一款 IM 聊天软件其实也不是很复杂。

8、相关文章

如果你觉得对本系列文章还不够详细,可以系统学习以下系列文章:

  1. 跟着源码学IM(一):手把手教你用Netty实现心跳机制、断线重连机制
  2. 跟着源码学IM(二):自已开发IM很难?手把手教你撸一个Andriod版IM
  3. 跟着源码学IM(三):基于Netty,从零开发一个IM服务端
  4. 跟着源码学IM(四):拿起键盘就是干,教你徒手开发一套分布式IM系统
  5. 跟着源码学IM(五):正确理解IM长连接、心跳及重连机制,并动手实现
  6. 跟着源码学IM(六):手把手教你用Go快速搭建高性能、可扩展的IM系统
  7. 跟着源码学IM(七):手把手教你用WebSocket打造Web端IM聊天
  8. 跟着源码学IM(八):万字长文,手把手教你用Netty打造IM聊天
  9. 跟着源码学IM(九):基于Netty实现一套分布式IM系统
  10. 跟着源码学IM(十):基于Netty,搭建高性能IM集群(含技术思路+源码)
  11. SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM聊天功能

9、参考资料

[1] 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

[2] 理论联系实际:一套典型的IM通信协议设计详解

[3] 浅谈IM系统的架构设计

[4] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端

[5] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

[6] 一套原创分布式即时通讯(IM)系统理论架构方案

[7]  一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[8] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

[9] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

[10] 从新手到专家:如何设计一套亿级消息量的分布式IM系统

[11] 基于实践:一套百万消息量小规模IM系统技术要点总结

[12] 探探的IM长连接技术实践:技术选型、架构设计、性能优化

本文已同步发布于:http://www.52im.net/thread-3963-1-1.html

posted @ 2022-07-04 18:38 Jack Jiang 阅读(150) | 评论 (0)编辑 收藏

本文由作者jhon_11分享,有大量修订和改动。

1、引言

如何设计一款高性能、高并发、高可用的im综合消息平台是很多公司发展过程中会碰到且必须要解决的问题。比如一家公司内部的通讯系统、各个互联网平台的客服咨询系统,都是离不开一款好用且维护的方便im综合消息系统。

那么,我们应该怎么样来设计一款三高特性的im系统,并能同时支持各个业务线的接入(比如:内部OA通讯、客服咨询、消息推送等等功能)有呢?

下面就由我来介绍一下我所负责的公司IM综合消息系统所经历的架构设计历程,以及架构设计过程中的一些思路和总结,希望能给你带来启发。

学习交流:

本文已同步发布于:http://www.52im.net/thread-3954-1-1.html

2、初版IM架构

2.1 概述

im第一版设计的初衷是公司需要一款im消息中间件用于支撑客服咨询业务。

但是,考虑到为了方便日后其他业务线也能接入消息沟通平台,所以一开始就将整个消息中心的能力需求给到中间件团队进行开发,以便除客服外的各业务线接入综合消息中心,从而实现多元的消息实时触达能力。

2.2 初版架构介绍

初版架构图如下图所示:

针对上面的架构图,我们逐个解释一下各模块的作用。

1)存储端:

在初版的架构下,存储端我们使用tidb、redis作为主要存储:

  • [1] redis用于存储消息已读未读,缓存连接信息等功能;
  • [2] tidb作为开源的分布式数据库,选择它是为了方便消息的存储。

2)mq消息总线:

我们使用rocketmq来实现消息总线(PS:即分布式情况下,不同im实例间通过MQ进行消息交互)。

消息总线是整个im的核心,使用rocketmq能支持十万级别的tps。基本所有服务都要从消息总线中消费消息进行业务处理。

3)zookeeper注册中心:各个服务会注册到zk中,方便服务之间内部进行调用,同样也可以暴露服务给外部进行调用。

4)link服务:

link服务主要用于接收客户端的ws(WebSocket协议)、tcp、udp等协议的连接。

同时调用用户服务进行认证,并投递连接成功的消息给位置服务进行消费,存储连接信息。

ws(WebSocket协议)过来的消息先到link再投递到消息总线。

5)消息分发服务:

消息分发服务主要用于接收消息总线推过来的消息进行处理,按照im内部消息协议构造好消息体后,又推送到消息总线中(比如会推给会话服务、消息盒子、link服务)。

6)位置服务:

存储link的(WebSocket协议)连接、tcp连接等信息,并使用redis进行缓存(key为userId),方便根据UserId查询到该用户所登录的客户端连接在哪个link上。

一个用户在相同设备只能登录一个,但可以支持多端登录。

7)用户服务:用于存储所有用户,提供认证查询接口。

8)消息盒子:存储所有消息,提供消息查询、消息已读未读、消息未读数、消息检索等功能。

9)会话服务:管理会话、群聊会话、单聊会话等功能。

2.3 整体时序图

整体架构的时序图如下:

3、初版IM架构存在的问题及思考

在上节的架构设计介绍中,我们详细分享了初版IM系统架构的设计思路以及具体流程。

那么在初版IM架构设计中还存在什么样的问题,又该如何优化呢?我们一条条来看看。

3.1 使用MQ消息总线的问题

正如上节所分享的那样,我们初版IM架构中,link服务到消息分发服务的消息使用的MQ消息总线。

初版架构设计中,link服务将消息下推给消息分发服务进行处理时,使用的是mq消息总线(通俗了说,IM集群内不同IM实例间的通信是依赖于MQ进行的消息传递),而mq消息总线必然做对有一定的时延(而且时延受制于MQ本身的系统实现和技术策略)。

举个例子:

当两个处于不同IM实例的客户端A和B聊天时,A用户发送消息到link --> 消息总线 --> 消息分发服务 --> 消息总线 --> link --> B用户。

正如上面这个例子,im消息投递流程太长了,并且这样也会大大降低系统的吞吐量。

3.2 消息落库为写扩散的问题

其实现阶段我们使用的是跟微信一样的写扩散策略(详见《企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等》)。

那么为啥微信使用写扩散不是缺陷,而对于我们的IM架构来说确是缺陷呢?

微信的技术特性:

  • 1)微信号称没有存储用户的聊天记录,全是实时推送;
  • 2)微信聊天记录全部会在我们手机端存储一份,两台手机终端上的聊天记录并不互通,并且互不可见。

我们的IM综合消息中心技术特性:

  • 1)综合消息中心是会有拉取历史聊天记录(服务端拉取)的功能,存储了全量消息;
  • 2)综合消息中心的客户端,需要支持网页版本。

综上所述:

  • 1)写扩散对微信这样有移动端的富客户端版本的即时通讯产品十分友好,每个消息在消息分发的时候给处于这个会话(单聊,群聊)下的所有用户所在客户端先推送消息,没找到连接就针对这个用户写一个离线缓存消息,那么下次该用户登录进来,可以从缓存中拉取到该消息,并且清掉缓存;
  • 2)写扩散对于我们这类通用综合消息平台并不友好,由于接入方大部分是网页版的客户端,所以没有缓存消息的能力,浏览器刷新就没有了任何消息,所以需要实时去服务端拉取历史消息。假设我是写扩散,在一个群聊中有五百个用户,针对这五百个用户在这个会话,我需要去写五百条消息,大大的增加了写io,并且还不能写缓存(得写数据库)。

3.3 tidb存在不稳定性和事务并发的问题

tidb是目前主流的开源分布式数据库,查询效率高、无需分库分表。

但同样的,tidb存在一些隐藏的问题:

  • 1)tidb在高并发情况下,并发事务会导致事务失败,具体原因不知;
  • 2)tidb排错成本高,公司很少有tidb专业运维,经常遇到不走索引的情况。

3.4 群聊、单聊冗余在同一个服务的问题

在我们初版的IM架构设计中,单聊和群聊是冗余在会话服务中的,并且冗余在同一张表的。

其实单聊、群聊从数据角度来说,还是会有些不同(比如业务属性)虽然都是会话,我们还是需要将这两个服务拆分开,细粒度的服务拆分能更好的把控整体的逻辑。

4、升级版IM架构

4.1 初始架构问题

正如前面两节分享的那样,渐渐的我们发现初版im架构有很大的不足之处。

在生产上暴露出了以下问题:

  • 1)tps没达到预期,吞吐量不能满足公司业务的发展;
  • 2)使用的存储中间件难以维护(主要是tidb),试错成本高,经常在生产暴露问题,并且速度越来越慢;
  • 3)消息写扩散没有太大必要,并大大增加了系统io次数(原因见上一节);
  • 4)一些特性无法支持,比如消息图文检索,消息已读未读。

4.2 升级版im架构介绍

本次升级后的im架构如下图所示:

如上图所示,改版后的各模块情况如下:

  • 1)存储端:存储端我们改用了mysql,针对消息服务单独使用了主从mysql集群(主节点用于写消息、从节点用于消息检索)——;
  • 2)mq消息总线:与第一版相比没有改动;
  • 3)link服务:与第一版相比,改动了link服务到消息分发服务的消息推送方式(由MQ总线方式变更为tcp实时推送);
  • 4)消息分发服务:集成了消息处理能力、路由能力,每台消息分发服务拥有所有link服务的tcp连接;
  • 5)单聊服务:负责单聊会话的管理能力;
  • 6)群聊服务:负责群聊会话的管理能力;
  • 7)用户服务:提供用户认证,登录\注册能力。

5、详细对比针对初版IM架构的改动

升级版的IM架构,对比初始初始,具体主要是下面这些改动。

5.1 改进了不同im实例间的消息分发方式

针对初版MQ消息总结的问题,升级版架构中,我们将link到消息分发服务改为tcp实时连接,百万客户端连接同一台link机器,消息实时触达能力tps达到16万。

link到消息分发服务的改版是本次设计的亮点之一,完全消除了mq推送的时延性,并且路由简单,几乎实时触达。

举个例子:当两个处于不同IM实例的客户端A和B聊天时

  • 1)初版架构中是:A用户发送消息到link --> 消息总线 --> 消息分发服务 --> 消息总线 --> link --> B用户;
  • 2)升级版架构是:用户A --> link --> 消息分发 --> link --> 用户B。

而且:link服务到消息分发服务集群的消息推送使用轮询负载均衡的方式,保证公平,不会导致个别机器负载过高。

5.2 取消了位置服务

取消了位置服务(这里的位置不是指的IM消息里的地理位置消息哦),消息分发服务集成位置服务的能力。

消息分发服务本身业务简单,不需要再单独划分位置服务,因为会增加网络io,并且消息分发服务直连link,而让它负责路由则更加方便。

5.3 存储由tidb改成了mysql

存储端由tidb改成了mysql,增强了可维护性,消息服务使用mysql主从读写分离方式,提高了消息落库速度与检索速度的同时,也减轻数据库压力。

前面有提到过使用tidb这样维护成本高,排查问题难的分布式数据库是一件很痛苦的事情。

而我们使用mysql更加稳定,大家对mysql的学习成本相对较低。针对消息服务使用读写分离的方式,能大大提高消息的吞吐量。

5.4 实现了初版无法实现的特性功能

升级版架构中,我们实现了初版无法实现的特性功能,比如消息已读未读、红包推送、商品链接推送等功能。

新版综合消息中心加入了消息已读未读、发送红包、链接推送等功能,但这些功能带有一定的业务特性,毕竟不是所有Im都需要,可通过配置取消这些功能。

5.5 消息由写扩散改为读扩散

升级版IM架构中,消息存储由写扩散改为了读扩散。

前面我们有提到写扩散和读扩散的利弊,对于网页端IM我们更适合使用读扩散,只需要落一条消息,大大提高消息服务的吞吐量.

5.6 增加了门面服务

升级版IM架构中,我们增加门面服务 im-logic,用于暴露给第三方业务线接口调用。

初版架构中,都是im的各个服务各自暴露接口给到外部进行调用, 而升级版架中我们统一使用logic服务暴露给外部调用。

在logic服务针对调用可以做一些处理,这样不会影响到整体im的通用,不会增加im底层代码的复杂度,从而将业务逻辑与底层进行解耦。

6、优化后的效果对比

针对升级版和初版IM架构,我们也做了一些对比测试,具体的测试过程就是详细展开了。

以下是测试结果:

7、业务线接入im综合消息系统的业务划分思考

7.1 到底该如何设计高性能通用im综合消息系统

关于业务线接入im综合消息系统的业务划分,我也做了一些总结和思考,为了更形象和易于理解,我这里以客服系统以及企业微信为例来进行分析。

假如我开发了一款通用的im综合消息系统,现在有很多业务方需要接入我们,我们该如何进行业务域的清晰划分就显得尤为重要,需要在妥协与不妥协中进行平衡。

就像当前市面上开源的im消息平台来说,存在的问题主要是:要么是集成了很多的业务逻辑,要么就只是一款单纯的客服系统,再或者就是一款IM好友聊天系统,中间的业务划分并不明确。当然,这也有好处,拿来就能用,并不需要进行二次业务封装。

那么,到底如何将im设计为一款真正的高性能通用im综合消息系统呢?

通用的综合消息消息平台只需要有通用的底层能力:

以下案例假设在我已经按照上述架构设计了一版im综合消息中心。

7.2 以客服系统为例

客服系统:

客服系统不光需要实现自身业务,还需要整合im的消息能力(消费im的消息),来进行场景分析,实现会话变更、信令消息推送等逻辑。

客服系统内部需要根据im的底层支持能力进行相应的业务封装以及客服系统的客服用户池,c端用户池如何初始化到im的用户中心这些问题都是需要考虑进去的。

7.3 内部OA通信为例

内部OA通信:

 

员工内部OA通信系统需要集成IM好友功能,需要根据im的用户中心封装组织架构,用户权限等功能。

同时,内部通信系统需要根据im实现消息已读未读,群聊列表,会话列表拉取等功能。

8、本文小结

im的综合消息平台是一款需要高度结合业务的中间件系统,它直接与业务打交道,跟普通的中间件有根本的区别。

一款好用的im综合消息平台,直接取决于你的通用性,可扩展性以及系统吞吐能力。

希望这篇文章所分享的内容,能对大家开发im时候的思路有所启迪。

9、参考资料

[1] 从零到卓越:京东客服即时通讯系统的技术架构演进历程

[2] 从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路

[3] 瓜子IM智能客服系统的数据架构设计(整理自现场演讲,有配套PPT)

[4] 阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

[5] 新手入门一篇就够:从零开发移动端IM

[6] 零基础IM开发入门(一):什么是IM系统?

[7] 基于实践:一套百万消息量小规模IM系统技术要点总结

[8] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

[9] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

[10] 从新手到专家:如何设计一套亿级消息量的分布式IM系统

[11] 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

[12] 阿里IM技术分享(三):闲鱼亿级IM消息系统的架构演进之路

[13] 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

本文已同步发布于:http://www.52im.net/thread-3954-1-1.html

posted @ 2022-06-28 10:40 Jack Jiang 阅读(127) | 评论 (0)编辑 收藏

关于MobileIMSDK

MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架,超轻量级、高度提炼,一套API优雅支持UDP 、TCP 、WebSocket 三种协议,支持iOS、Android、H5、标准Java平台,服务端基于Netty编写。

工程开源地址是:

关于RainbowChat

► 详细产品介绍:http://www.52im.net/thread-19-1-1.html
► 版本更新记录:http://www.52im.net/thread-1217-1-1.html
► 全部运行截图:Android端iOS端
► 在线体验下载:专业版(TCP协议)专业版(UDP协议)      (关于 iOS 端,请:点此查看

 

RainbowChat是一套基于开源IM聊天框架 MobileIMSDK 的产品级移动端IM系统。RainbowChat源于真实运营的产品,解决了大量的屏幕适配、细节优化、机器兼容问题(可自行下载体验:专业版下载安装)。

* RainbowChat可能是市面上提供im即时通讯聊天源码的,唯一一款同时支持TCP、UDP两种通信协议的IM产品(通信层基于开源IM聊天框架  MobileIMSDK 实现)。

v8.2 版更新内容

此版更新内容更多历史更新日志):

(1)Android端主要更新内容新增“扫一扫”等功能及优化!】:

  • 1)[bug]解决了客户端被踢掉后,再次登陆时提示socket错误的问题;
  • 2)[优化]优化了扫码加群界面中,群头像加载失败时的默认显示样式;
  • 3)[优化]优化了切换账号和被踢时跳转到登陆界面的切换性能;
  • 4)[优化]重构了主要类代码,更方便集成;
  • 5)[新增]搜索功能(支持好友、群聊、聊天记录搜索(与微信逻辑一样));
  • 6)[新增]“聊信信息”界面中新增“查找聊天记录”功能;
  • 7)[新增]“群聊信息”界面中新增“查找聊天记录”、“清空聊天记录”功能。

(2)服务端主要更新内容:

  • 1)[优化][服务端]升级了MobileIMSDK至v6.2beta(改动了onUserLoginout方法参数);
  • 2)[优化][服务端]解决了log4j2的两个jar包冲突导致在linux下不能正常输出log的问题.

此版主要新增功能运行截图更多截图点此查看):

posted @ 2022-06-25 22:37 Jack Jiang 阅读(97) | 评论 (0)编辑 收藏

     摘要: 本文由字节跳动技术团队开发工程师王浩分享,即时通讯网收录时有较多修订。1、引言对于移动互联网时代的用户来说,短视频应用再也不是看看视频就完事,尤其抖音这种头部应用,已经是除了传统IM即时通讯软件以外的新型社交产品了。对于中国人一年一度最重的节日——春节来说,红包是必不可少的节日特定社交元素,而抖音自然不会被错过。在2022年的春节活动期间,抖音将视频和春节红包相结合,用户可...  阅读全文

posted @ 2022-06-20 17:12 Jack Jiang 阅读(160) | 评论 (0)编辑 收藏

本文由B站微服务技术团队资深开发工程师周佳辉原创分享。

1、引言

如果你在 2015 年就使用 B 站,那么你一定不会忘记那一年 B 站工作日选择性崩溃,周末必然性崩溃的一段时间。

也是那一年 B 站投稿量激增,访问量随之成倍上升,而过去的 PHP 全家桶也开始逐渐展露出颓势,运维难、监控难、排查故障难、调用路径深不见底。

也就是在这一年,B 站开始正式用 Go 重构 B 站,从此B站的API网关技术子开始了从0到1的持续演进。。。

* 补充说明:本次 API 网关演进也以开源形式进行了开发,源码详见本文“12、本文源码”。

PS:本文分享的API网关涉及到的主要是HTTP短连接,虽然跟长连接技术有些差异,但从架构设计思路和实践上是一脉相承的,所以也就收录到了本《长连接网关技术专题》系列文章中。

学习交流:

本文已同步发布于:http://www.52im.net/thread-3941-1-1.html

2、关于作者

周佳辉:哔哩哔哩资深开发工程师。始终以简单为核心的技术设计理念,追求极致简单有效的后端架构。

2017 年加入 B 站,先后从事账号、网关、基础库等开发工作。编码 C/V 技能传授者,技术文档背诵者。开源社区爱好者,安全技术爱好者,云计算行业活跃用户,网络工程熟练工。史诗级 bug 生产者,熟练掌握 bug 产生的各类场景。

3、专题目录

本文是专题系列文章的第8篇,总目录如下:

4、正式用Go重构B站

鉴于引言中所列举的各种技术问题,也是在2015年,财队开始正式用 Go 重构 B 站。

B站第一个 Go 项目——bilizone,由冠冠老师(郝冠伟)花了一个周末时间编码完成。

commit 4ccb1497ca6d94cec0ea1b2555dd1859e6f4f223

Author: felixhao <g******[url=mailto:1@gmail.com]1@gmail.com[/url]>

Date:   Wed Jul 1 18:55:00 2015 +0800

    project init

commit 6e338bc0ee638621e01918adb183747cf2a9e567

Author: 郝冠伟 <h*******@bilibili.com>

Date:   Wed Jul 1 11:21:18 2015 +0800

    readme

▲ 郝冠伟:哔哩哔哩主站技术中心架构师

bilizone 其实还是一个大而全的应用,bilizone 在当时重构的主要意义是将谁也理不清的 PHP 逻辑梳理成了一个比较标准的 Go 应用。

bilizone 在当时最大的意义就是为用户终端提供了基本稳定的数据结构、相对可靠的接口和比较有效的监控。

但因 bilizone 依旧是一个单体应用,所以它依旧继承了单体应用所具有的缺点:

  • 1)代码复杂度高:方法被滥用、超时设置混乱、牵一发而动全身;
  • 2)一挂全挂:最常见的比如,超时设置不合理、goroutine 大量堆积、雪崩;
  • 3)测试及维护成本高:小改动都需要测试所有 case,运维发布胆战心惊。

所以此时B站的崩溃频率虽然已经有所降低,但一炸全炸的问题依旧是一个心腹大患。

5、基于微服务的B站架构初具雏形

鉴于bilizone所面临的单体应用技术缺点,接下来的一次重构,让B站基于微服务的全局架构面貌就将初具雏形。

为了实现微服务模式下的 bilibili,我们将一个 bilizone 应用拆分成多个独立业务应用,如账号、稿件、广告等等,这些业务通过 SLB 直接对外提供 API。

当时的调用模式如下图所示:

但是随着功能拆分后,我们对外暴露了一批微服务,但是因为缺乏统一的出口而面临了不少困难。

这些困难主要是:

  • 1)客户端与微服务直接通信,强耦合;
  • 2)需要多次请求,客户端聚合数据,工作量巨大,延迟高;
  • 3)协议不利于统一,各个部门间有差异,反而需要通过客户端来兼容;
  • 4)面向“端”的 API 适配,耦合到了内部服务;
  • 5)多终端兼容逻辑复杂,每个服务都需要处理;
  • 6)统一逻辑无法收敛,比如安全认证、限流。

6、基于BFF模式的微服务架构

基于上节的初阶微服务架构带来的技术问题,以及我们想要将对端的处理进行内聚的想法,我们自然的而然的就想到在客户端与后端服务之间加一个 app-interface 的组件,这就是接下来的 BFF(Backend for Frontend)模式。

app-interface 的工作模式如下图所示:

有了这个 BFF 之后,我们可以在该服务内进行大量的数据聚合,按照业务场景来设计粗粒度的 API。

这样,后续服务的演进也带来了很多优势:

  • 1)轻量交互:协议精简、聚合;
  • 2)差异服务:数据裁剪以及聚合、针对终端定制化 API;
  • 3)动态升级:原有系统兼容升级,更新服务而非协议;
  • 4)沟通效率提升:协作模式演进为移动业务和网关小组。

BFF 可以认为是一种适配服务,将后端的微服务为客户端的需要进行适配(主要包括聚合裁剪和格式适配等逻辑),向终端设备暴露友好和统一的 API,方便无线设备接入访问后端服务,在其中可能还伴随有埋点、日志、统计等需求。

然而,这个时期的 BFF 还有一个致命的一个问题是——整个 app-interface 属于 single point of failure,严重代码缺陷或者流量洪峰可能引发集群宕机所有接口不可用。

7、基于多套BFF模式的微服务架构

针对上节中BFF模式下架构的技术问题,于是我们在上述基础上进一步迭代,将 app-interface 进行业务拆分。

进而多套 BFF 的模式横空出世:

由此模式开始,基本确定了 B 站微服务接口的对接模式,这套模式也随之在全公司内推广开来。

8、垂直BFF模式时代(2016年至2019年)

接上节,当 B 站网关的架构发展为多套垂直 BFF 之后,开发团队围绕该模式平稳迭代了相当长的一段时间。

而后随着B站业务的发展,团队人员的扩充和几次组织架构调整,此时开始出现直播、电商等独立业务,这些业务的发展我们之后再细说。

而在这些调整之后,有一个团队的职责越来越清晰:主站网关组。

主站网关组的主要职责就是维护上述各类功能的 BFF 网关,此时 bilibili 的主要流量入口为粉板 App。这里可以简单细说一下粉板 App 上的所有业务组成。

主站业务:

  • 1)网关组维护的 BFF,如推荐、稿件播放页等;
  • 2)业务层自行维护的 BFF,如评论、弹幕、账号等。

独立业务:

  • 1)电商服务;
  • 2)直播服务;
  • 3)动态服务。

主站业务的 BFF 其实被分为两类:

  • 1)一类是由网关组负责的 BFF;
  • 2)另一类是业务自行维护的 BFF。

而这两类 BFF 的技术栈其实基本一致,基本功能职责也相差不多。如此划分的原因是让网关组可以更专注于迭代客户端特性功能,免去理解部分独立业务场景的接口,如登陆页应该让对安全更专业账号的同学自行维护。

在这里我们也可以简述一下,一个新需求应该如何决定参与的 BFF :

  • 1)如果这个功能能由业务层的业务 BFF 独立完成,则网关组不需介入;
  • 2)如果该功能是一个客户端特性需求,如推荐流等复合型业务,需要对接公司大量部门时,则由网关同学参与开发 BFF。

当时主站技术部的后端同学遵循以上两个规则,基本能够满足业务的快速开发和迭代。

我把这段时间称为垂直 BFF 时代,因为基本主站每个业务或多或少都有各种形式的网关存在,大家通过这个网关向外提供接口,该网关和 SLB 进行直接交互。

9、基于业务的统一API网关架构

接上节,我们再来谈一谈几项重要的业务:电商、直播和动态。

电商和直播其实并不是同一时期衍生的,直播在主站 PHP 时期就诞生了,而电商相对更晚一些。

当时直播的技术栈组成有 C++、PHP、Go,其中早期大部分业务逻辑由 PHP 和 C++ 实现,稍晚一些也开始逐步试用主站的 Go 实现部分业务逻辑。其中 PHP 负责对终端提供接口,C++ 主要实现核心业务功能。因此我们可以简单理解为直播使用由 PHP 编写的 BFF 网关。

动态团队其实派生自直播团队,因此技术栈和直播当时基本一致,这里可以简单省略。

而众所周知,大部分电商团队的技术栈都是 Java 和 Spring 或 Dubbo。

因这几个业务实现上几乎没有相似的地方,且大家对 gRPC 协议逐渐地认同,因此技术栈上大家基本没有大一统的想法,互相能调通即可。

而随着 B 站团队进一步的壮大、流量持续的增长,进而经历了诸多线上故障、事故分析之后,大家慢慢发现了这套架构下的各种问题。

这些问题主要是:

  • 1)单个复杂模块也会导致后续业务集成的高难度,根据康威法则,复杂聚合型 BFF 和多团队之间就出现不匹配问题,团队之间沟通协调成本高,交付效率低下;
  • 2)很多跨横切面逻辑,比如安全认证,日志监控,限流熔断等。随着时间的推移,功能的迭代,代码变得越来越复杂,技术债越堆越多。

此时:我们可能还需要一个能协调横跨切面的组件,将路由、认证、限流、安全等组件全部上提,能够统一更新发布,把业务集成度高的 BFF 层和通用功能服务层进行分层,进而大家开始引入基于业务的“统一API网关”架构(如下图所示)。

在新的架构中:统一网关承担了重要的角色,它是解耦拆分和后续升级迁移的利器。

在统一网关的配合下:单块 BFF 实现了解耦拆分,各业务线团队可以独立开发和交付各自的微服务,研发效率大大提升。

另外:把跨横切面逻辑从 BFF 剥离到网关上去以后,BFF 的开发人员可以更加专注业务逻辑交付,实现了架构上的关注分离(Separation of Concerns)。

10、从基于业务的多网关到全局统一网关(2022年至今)

在这两三年的时间里,各个业务团队或多或少都有自己业务网关组建独立的维护团队,也为网关的功能作出过相当多的投入。

但随着 B 站业务的发展,公司级中间件功能的不断更替演进,如果将对接各个中间件的工作在每个网关上都实现一次的话带来的人力投入和沟通成本会相当巨大,且实现标准不统一、运营方式不统一无法起到 API 网关所带来的最佳收益。

因此微服务团队开发了一款 B 站内部意义上的标准 API 网关(全局统一API网关),该 API 网关汇集以往各型网关中流量治理的优秀经验,对相关功能做出完善设计改进。

该 API 网关的目前的主要功能除了常规的限流、熔断、降级、染色外,还会基于这些基础功能和公司各类中间件的基础上,提供各种额外能力。

这些额外进阶型AP 质量治理的相关功能主要是:

  • 1)全链路灰度;
  • 2)流量采样分析、回放;
  • 3)流量安全控制;
  • ...

业务团队在接入 API 网关后都可以一并获得这些功能,为业务的迅速迭代做出力所能及的保障。

11、不仅仅是 API 网关

在开发 API 网关的同时,我们也会更进一步关注业务团队开发、对接 API 时的体验,我们将以网关作为统一标准 API 规范的起点,为业务团队提供更有效的 API 开发生态。

这些API 开发生态可能是:

  • 1)规划 API 业务域,简化 SRE 运维;
  • 2)标准 API 元信息平台;
  • 3)精确的 API 文档和调试工具;
  • 4)类型安全的 API 集成 SDK;
  • 5)API 兼容性保障服务。

API 网关是我们 API 治理生态中的一个标志性里程碑,我们希望在 API 网关的开发中能够多多倾听大家的意见,希望能有更多的声音来帮助我们理清思路。

本次 API 网关演进也以开源形式进行了开发,在这里欢迎大家指导(本次源码详见本文“12、本文源码)。

12、本文源码

主地址:https://github.com/go-kratos/gateway

备地址:https://github.com/52im/gateway

或从原文链接中下载附件:http://www.52im.net/thread-3941-1-1.html

13、参考资料

[1] 喜马拉雅自研亿级API网关技术实践

[2] 手淘亿级移动端接入层网关的技术演进之路

[3] 从100到1000万高并发的架构演进之路

[4] 一文读懂大型分布式系统设计的方方面面

[5] 零基础理解大型分布式架构的演进历史、技术原理、最佳实践

本文已同步发布于:http://www.52im.net/thread-3941-1-1.html

posted @ 2022-06-14 11:56 Jack Jiang 阅读(136) | 评论 (0)编辑 收藏

本文引用了文章“月活 12.8 亿的微信是如何防止崩溃的?”和论文“Overload Control for Scaling WeChat Microservices”的内容,有大量改动、优化和修订。

1、引言

微信是一款国民级的即时通讯IM应用,月活用户早就超过10亿,而且经常过年过节会遇到聊天消息量暴增的情况,服务是很容易出现过载的,但事实是微信的后台服务一直比较稳定,那么他们是怎么做到的呢?

本文以微信发表的论文《Overload Control for Scaling Wechat Microservices》 为基础(论文PDF原文下载见文末附件),分享了微信基于大规模微服务架构的后台过载管控和保护策略,以及微信根据IM业务特点的一些独特的架构设计做法,其中很多方法很有借鉴意义,值得一读。

 (本文已同步发布于:http://www.52im.net/thread-3930-1-1.html

2、微信所面临的并发压力

截止论文《Overload Control for Scaling Wechat Microservices》发表前,微信后端有超过3000多个服务(包括即时聊天、社交关系、移动支付和第三方授权等),占用20000多台机器(随着微信的广泛普及,这些数字仍在不断增加)。

面向前端请求的入口服务每天需要处理10亿到100亿级别的请求,而每个这样的请求还会触发更多内部的关联服务,从整体来看,微信后端需要每秒处理数亿个请求。

随着微信的不断发展,这些服务子系统一直在快速进行更新迭代。以2018 年的3月到5月为例,在短短的两个月时间里,微信的各服务子系统平均每天发生近千次的变更,运维压力可想而之。

另外:微信每天请求量的分布很不平均,高峰期请求量能达到平时的3倍。而在特殊日子里(比如过年的时候),高峰期的流量能飙升到平时的10倍。有时朋友圈里有什么刷屏的活动,流量肯定也会突增。由此可见,微信后端系统的并发压力相当之大。

而且:微信后端的这些服务所处的环境也是不断变化的,包括硬件故障、代码bug、系统变更等,都会导致服务可承受的容量动态变化。

3、微信的后端服务架构

微信后端采用的也是微服务架构。说是微服务,其实我理解就是采用统一的 RPC 框架搭建的一个个独立的服务,服务之间互相调用,实现各种各样的功能,这也是现代服务的基本架构。毕竟谁也不希望看到我朋友圈崩了,导致跟我聊天也不行了,这也是微信的典型好处。

微信后端的微服务架构一般分为3层:

如上图所示,这3层服务分别是:

  • 1)“入口跳板”服务(接收外部请求的前端服务);
  • 2)“共享跳板”服务(中间层协调服务);
  • 3)“基础服务”(不再向其他服务发出请求的服务,也就是充当请求的接收器)。

微信后端的大多数服务属于“共享跳板”服务,“入口跳板”服务比如登录、发送聊天消息、支付服务等。“基础服务”也就是日常最好理解的这些信息数据接口类,比如账户数据、个人信息、好友/联系人信息等。

按照微信后端服务的请求量(每日在十亿到百亿之间),入口协议触发对“共享跳板”服务和“基础服务”更多的请求,核心服务每秒要处理上亿次的请求,也就是显而易见的了。

4、什么是过载保护

1)什么是服务过载?

服务过载就是服务的请求量超过服务所能承受的最大值,从而导致服务器负载过高,响应延迟加大。

用户侧表现就是无法加载或者加载缓慢,这会引起用户进一步的重试,服务一直在处理过去的无效请求,导致有效请求跌 0,甚至导致整个系统产生雪崩。

2)为什么会发生服务过载?

互联网天生就会有突发流量、秒杀、抢购、突发大事件、节日甚至恶意攻击等,都会造成服务承受平时数倍的压力,比如微博经常出现某明星官宣结婚或者离婚导致服务器崩溃的场景,这就是服务过载。

3)过载保护的好处

过载保护主要是为了提升用户体验,保障服务质量,在发生突发流量时仍然能够提供一部分服务能力,而不是整个系统瘫痪。

系统瘫痪就意味着用户流失、口碑变差、夫妻吵架,甚至威胁生命安全(假如腾讯文档崩溃,这个文档正好用于救灾)。

而微信团队在面对这种量级的高并发请求挑战,做法是精细化的服务过载控制。我们继续往下学习。

5、微信面临的过载控制技术挑战

过载控制对于大规模在线应用程序来说至关重要,这些应用程序需要在不可预测的负载激增的情况下实现 24×7 服务可用性。

传统的过载控制机制是为具有少量服务组件、相对狭窄的“前门”和普通依赖关系的系统而设计的。

而微信这种现代即时通讯im应用的全时在线服务特性,在架构和依赖性方面正变得越来越复杂,远远超出了传统过载控制的设计目标。

这些技术痛点包括:

  • 1)由于发送到微信后端的服务请求没有单一的入口点,因此传统的全局入口点(网关)集中负载监控方法并不适用;
  • 2)特定请求的服务调用图可能依赖于特定于请求的数据和服务参数,即使对于相同类型的请求也是如此(因此,当特定服务出现过载时,很难确定应该限制哪些类型的请求以缓解这种情况);
  • 3)过多的请求中止浪费了计算资源,并由于高延迟而影响了用户体验;
  • 4)由于服务的调用链极其复杂,而且在不断演化,导致有效的跨服务协调的维护成本和系统开销过高。

由于一个服务可能会向它所依赖的服务发出多个请求,并且还可能向多个后端服务发出请求,因此我们必须特别注意过载控制。我们使用一个专门的术语,叫作“后续过载”,用于描述调用多个过载服务或多次调用单个过载服务的情况。

“后续过载”给有效的过载控制带来了挑战。当服务过载时随机执行减载可以让系统维持饱和的吞吐量,但后续过载可能会超预期大大降低系统吞吐量 …

即:在大规模微服务场景下,过载会变得比较复杂,如果是单体服务,一个事件只用一个请求,但微服务下,一个事件可能要请求很多的服务,任何一个服务过载失败,就会造成其他的请求都是无效的。如下图所示。

 比如:在一个转账服务下,需要查询分别两者的卡号, 再查询 A 时成功了,但查询 B 失败,对于查卡号这个事件就算失败了。比如查询成功率只有 50%, 那对于查询两者卡号这个成功率只有 50% * 50% = 25% 了, 一个事件调用的服务次数越多,那成功率就会越低。

6、微信的过载控制机制

微信的微服务过载控制机制叫“DAGOR”(因为微信把它的服务间关系模型叫“directed acyclic graph ”,简称DAG)。

显然这种微服务底层的机制必须是和具体的业务实现无关的。DAGOR还必须是去中心化的,否则的话在微信这么大且分布不均的流量下,过载控制很难做到实时和准确。同时也无法适应微服务快速的功能迭代发布(平均每天要发生近1000次的微服务上下线)。

此外,DAGOR还需要解决一个问题:服务调用链很长,如果底层服务因为过载保护丢弃了请求,上层服务耗费的资源全浪费了,而且很影响用户体验(想想进度条走到99%告诉你失败了)。所以过载控制机制在各服务之间必须有协同作用,有时候需要考虑整个调用链的情况。

首先我们来看怎么检测到服务过载。

7、微信如何判断过载

通常判断过载可以使用吞吐量、延迟、CPU 使用率、丢包率、待处理请求数、请求处理事件等等。

微信使用在请求在队列中的平均等待时间作为判断标准。平均等待时间就是从请求到达,到开始处理的时间。

为啥不使用响应时间?因为响应时间是跟服务相关的,很多微服务是链式调用,响应时间是不可控的,也是无法标准化的,很难作为一个统一的判断依据。

那为什么也不使用 CPU 负载作为判断标准呢? 因为 CPU 负载高不代表服务过载,因为一个服务请求处理及时,CPU 处于高位反而是比较良好的表现。实际上 CPU 负载高,监控服务是会告警出来,但是并不会直接进入过载处理流程。

腾讯微服务默认的超时时间是 500ms,通过计算每秒或每 2000 个请求的平均等待时间是否超过 20ms,判断是否过载,这个 20ms 是根据微信后台 5 年摸索出来的门槛值。

采用平均等待时间还有一个好处是:独立于服务,可以应用于任何场景,而不用关联于业务,可以直接在框架上进行改造。

当平均等待时间大于 20ms 时,以一定的降速因子过滤调部分请求,如果判断平均等待时间小于 20ms,则以一定的速率提升通过率,一般采用快降慢升的策略,防止大的服务波动,整个策略相当于一个负反馈电路。

 

8、微信的过载控制策略

微信后台一旦检测到服务过载,就需要按照一定的过载保户策略对请求进行过滤控制,来决定哪些请求能被过载服务处理,哪些是需要丢弃的。

前面我们分析过,对于链式调用的微服务场景,随机丢弃请求会导致整体服务的成功率很低。所以请求是按照优先级进行控制的,优先级低的请求会优先丢弃。

那么从哪些维度来进行优化级的分级呢?

8.1 基于业务的优先级控制

对于微信来说,不同的业务场景优先级是不同的, 比如:

  • 1)登录场景是最重要的业务(不能登录一切都白瞎);
  • 2)支付消息比普通im聊天消息优先级高(因为用户对金钱是更敏感的);
  • 3)普通消息又比朋友圈消息优先级高(必竟微信的本质还是im聊天)。

所以在微信内是天然存在业务优先级的。

微信的做法是,预先定义好所有业务的优先级并保存在一个Hash Table里:

没有定义的业务,默认是最低优先级。

业务优先级在各个业务的入口服务(Entry Services)中找到请求元信息里。由于一个请求成功与否依赖其下游服务所有的后续请求,所以下游服务的所有后续请求也会带上相同的业务优先级。当服务过载时,会处理优先级更高的请求,丢弃优先级低的请求。

然而,只用业务优先级决定是否丢弃请求,容易造成系统颠簸,比如:

  • 1)支付请求突然上涨导致过载,消息请求被丢弃;
  • 2)丢弃消息请求后,系统负载降低了,又开始处理消息请求;
  • 3)然而,处理消息请求又导致服务过载,又会在下一个窗口抛弃消息请求。

这样反复调整服务请求管制,整体体验非常不好。所以微信需要更精细化的服务请求管制。

PS:微信尝试过提供API让服务提供方自己修改业务优先级,后来在实践中发现这种做法在不同的团队中极难管理,且对于过载控制容易出错,最终放弃了。

8.2 基于用户的优先级控制

很明显,正如上节内容所述,只基于业务优先级的控制是不够的:

  • 1)首先不可能因为负载高,丢弃或允许通过一整个业务的请求,因为每个业务的请求量很大,那一定会造成负载的大幅波动;
  • 2)另外如果在业务中随机丢弃请求,在过载情况下还是会导致整体成功率很低。

为了解决这个问题,微信引入用户优先级。

微信在每个业务优先级内按用户ID计算出的128个优先级:

首先用户优先级也不应该相同,对于普通人来说通过 hash 用户唯一 ID计算用户优先级(这个hash函数每小时变一次,让所有用户都有机会在相对较长的时间内享受到高优先级,保证“公平”)。跟业务优先级一样,单个用户的访问链条上的优先级总是一致的。

这里有个疑问:为啥不采用会话 ID 计算优先级呢?

从理论上来说采用会话 ID 和用户 ID 效果是一样的,但是采用会话 ID 在用户重新登录时刷新,这个时候可能用户的优先级可能变了。在过载的情况下,他可能因为提高了优先级就恢复了。

这样用户会养成坏习惯,在服务有问题时就会重新登录,这样无疑进一步加剧了服务的过载情况。

于是,因为引入了用户优先级,那就和业务优先级组成了一个二维控制平面。根据负载情况,决定这台服务器的准入优先级(B,U),当过来的请求业务优先级大于 B,或者业务优先级等于 B,但用户优先级高于 U 时,则通过,否则决绝。

下图就是这个“优先级(B,U)”控制逻辑(我们会在后面再具体讨论):

8.3 自适应优先级调整

在大规模微服务场景下,服务器的负载变化是非常频繁的。所以服务器的准入优先级是需要动态变化的,微信分了几十个业务优先级,每个业务优先级下有 128 个用户优先级,所以总的优先级是几千个。

如何根据负载情况调整优先级呢?

最简单的方式是从右到左遍历:每调整一次判断下负载情况。这个时间复杂度是 O(n), 就算使用二分法,时间复杂度也为 O(logn),在数千个优先级下,可能需要数十次调整才能确定一个合适的优先级,每次调整好再统计优先级,可能几十秒都过去了,这个方法无疑是非常低效的。

微信提出了一种基于直方图统计的方法快速调整准入优先级:服务器上维护者目前准入优先级下,过去一个周期的(1s 或 2000 次请求)每个优先级的请求量。当过载时,通过消减下一个周期的请求量来减轻负载。假设上一个周期所有优先级的通过的请求总和是 N,下一个周期的请求量要减少 N*a,怎么去减少呢?每提升一个优先级就减少一定的请求量,一直提升到 减少的数目大于目标量,恢复负载使用相反的方法,只不是系数为 b ,比 a 小,也是为了快降慢升。根据经验值 a 为 5%,b 为 1%。

为了进一步减轻过载机器的压力,能不能在下游过载的情况下不把请求发到下游呢?否则下游还是要接受请求、解包、丢弃请求,白白的浪费带宽,也加重了下游的负载。

为了实现这个能力:在每次请求下游服务时,下游把当前服务的准入优先级返回给上游,上游维护下游服务的准入优先级,如果发现请求优先级达不到下游服务的准入门槛,直接丢弃,而不再请求下游,进一步减轻下游的压力。

9、实验数据

微信的这套服务过载控制策略(即DAGOR)在微信的生产环境已经运作多年,这是对它的设计可行性的最好证明。

但并没有为学术论文提供必要的图表,所以微信同时进行了一组模拟实验。

下面的图表突出显示了基于排队时间而非响应时间的过载控制的好处。在发生后续过载的情况下,这些好处最为明显(图右)。

10、小结一下

微信的整个过载控制逻辑流程如下图所示:

针对上面这张图,我们来解读一下:

  • 1)当用户从微信发起请求,请求被路由到接入层服务,分配统一的业务和用户优先级,所有到下游的字请求都继承相同的优先级;
  • 2)根据业务逻辑调用 1 个或多个下游服务,当服务收到请求,首先根据自身服务准入优先级判断请求是接受还是丢弃(服务本身根据负载情况周期性的调整准入优先级);
  • 3)当服务需要再向下游发起请求时,判断本地记录的下游服务准入优先级(如果小于则丢弃,如果没有记录或优先级大于记录则向下游发起请求);
  • 4)下游服务返回上游服务需要的信息,并且在信息中携带自身准入优先级;
  • 5)上游接受到返回后解析信息,并更新本地记录的下游服务准入优先级。

微信的整个过载控制策略有以下三个特点:

  • 1)业务无关的:使用请求等待时间而不是响应时间,制定用户和业务优先级,这些都与业务本身无关;
  • 2)高效且公平: 请求链条的优先级是一致的,并且会定时改变 hash 函数调整用户优先级,过载情况下,不会总是影响固定的用户;
  • 3)独立控制和联合控制结合:准入优先级取决于独立的服务,但又可以联合下游服务的情况,优化服务过载时的表现。

11、写在最后

微信团队的分享只提到过载控制,但我相信服务调用方应该还有一些其他机制,能够解决不是因为下游服务过载,而是因为网络抖动导致的请求超时问题。

微信的这套微服务过载控制机制(即DAGOR)提供的服务无关、去中心化、高效和公平等特性很好地在微信后端跑了很多年。

最后,微信团队还分享了他们设计和运维DAGOR宝贵经验:

  • 1)大规模微服务架构中的过载控制必须在每个服务中实现分散和自治;
  • 2)过载控制应该要考虑到各种反馈机制(例如 DAGOR 的协作准入控制),而不是仅仅依赖于开环启发式;
  • 3)应该通过分析实际工作负载来了解过载控制设计。

12、参考资料

[1] Overload Control for Scaling WeChat Microservices

[2] 罗神解读“Overload Control for Scaling WeChat Microservices”

[3] 2W台服务器、每秒数亿请求,微信如何不“失控”?

[4] DAGOR:微信微服务过载控制系统

[5] 月活 12.8 亿的微信是如何防止崩溃的?

[6] 微信朋友圈千亿访问量背后的技术挑战和实践总结

[7] QQ 18年:解密8亿月活的QQ后台服务接口隔离技术

[8] 微信后台基于时间序的海量数据冷热分级架构设计实践

[9] 架构之道:3个程序员成就微信朋友圈日均10亿发布量[有视频]

[10] 快速裂变:见证微信强大后台架构从0到1的演进历程(一)

[11] 一份微信后台技术架构的总结性笔记

13、论文原文

论文PDF请下载此附件:

因无法上传附件,请从此链接:http://www.52im.net/thread-3930-1-1.html文末的“参考资料”附件中下载

论文PDF全部内容概览:

本文已同步发布于:http://www.52im.net/thread-3930-1-1.html

posted @ 2022-06-06 16:31 Jack Jiang 阅读(581) | 评论 (0)编辑 收藏

仅列出标题
共43页: First 上一页 10 11 12 13 14 15 16 17 18 下一页 Last 
Jack Jiang的 Mail: jb2011@163.com, 联系QQ: 413980957, 微信: hellojackjiang