乐趣区

关于golang:GlideIM-Golang-实现的高性能的分布式-IM

GlideIM 是一款 齐全开源, Golang 实现的高性能分布式 IM 服务, 有残缺的安卓 APP 示例, JAVA SDK, Web 端示例, 继续更新迭代中.

GlideIM 反对单实例, 分布式部署. 反对 WebSocket, TCP 两种连贯协定, 内置 JSON, ProtoBuff 两种音讯替换协定, 并反对增加其余协定, 音讯加密等. 还实现了智能心跳保活机制, 死链接检测, 音讯送达机制等性能.

这个我的项目自 2020 年中旬开始, 三端均开发由我一个人单独不间断开发直到现在, 也是我第一个消耗最多精力去开发及学习的我的项目. 因为我工作较多闲暇工夫, 基本上都在开发本我的项目, 边学习边开发, 查阅了大量材料, 通过大量的思考, 指标就是一个高性能分布式 IM. 第一版微服务架构在开发三个月的时候根本曾经实现了, 前面则是做了微服务架构调整, 和进一步优化 IM 服务细节, 截至目前(2022 年 3 月 3 日), 间隔我预期的第二版, 只剩下一些收尾工作.

本我的项目将继续更新迭代.

  • 服务端源码: GlideIM – GitHub
  • Android App 下载: Android – GitHub
  • Web 端: GlideIM

一. 性能

1.1 用户侧性能

  • 无感掉线重连, 音讯同步
  • 登录注册及放弃登录
  • 一对一聊天, 群聊
  • 音讯漫游, 历史记录
  • 离线音讯
  • 多设施登录, 同设施互挤下线
  • 多种类型的音讯 (图片, 语音等, 由客户端定义)
  • 音讯撤回
  • 联系人治理, 群治理

1.2 开发侧性能

  • 反对 WebSocket, TCP, 自定义连贯协定
  • 反对 JSON 或 Protobuff 或自定义数据交换协定
  • 反对分布式部署, 程度扩大
  • 心跳保活, 超时断开, 清理死链接
  • 音讯缓冲, 异步解决, 弱网优化
  • 音讯送达机制, 音讯重发, ACK
  • 音讯去重, 程序保障, 读扩散

二. 我的项目架构

为了进步可用性和整体的稳定性, 单机性能的限度, 必须应用分布式架构, 微服务的模式不便了保护. 本我的项目对 IM 业务局部拆分了六大外围主模块(服务), 每个服务能够程度任意数量扩大, 整个零碎能够具备肯定的伸缩性, 每个模块依据其业务个性划分, 逻辑和接口拆散, 在保障接口简洁性的同时也有足够的扩展性.

2.1 聊天服务划分

Gateway

Gateway 模块为治理用户连贯的聊天服务网关, 所有用户音讯上下行都由这个模块解决. Gateway 治理用户连贯, 音讯的接管解析, 音讯下发, 判断连贯是否存活, 标识用户连贯, 断开用户连贯.

Gateway 依赖 Messaging 服务, 接管到客户端音讯将交由 Messaging 解决, Gateway 提供指定 uid 登录, 登出, 下发音讯三个接口.

Messaging

Messaging 负责不同类型音讯的路由, 例如群音讯, API 音讯, 也解决局部类型的音讯, 例如 ACK 音讯, 单聊音讯, 心跳音讯. 依据音讯类型别离转发给 Dispatch (用户音讯), API (API 音讯), Group(群音讯).

Messaging 依赖 API, GroupMessaging, Dispatch, 提供一个音讯路由接口.

Group

(GroupMessaging), 群聊服务, 次要负责多人聊天音讯的下发, 保留, 群音讯确认, 成员治理. 用户上线后同步联系人时初始化群聊列表, 依据群聊所在服务通进入群聊天.

Group 服务依赖 Dispatch 服务, 提供群更新, 成员更新, 发送音讯到指定群三个接口.

API

目前临时作为长连贯的登录鉴权, HTTP API 接口都能够通过长连贯音讯拜访, 这个能够依据具体情况灵便配置, 只需配置相干路由即可.

API 依赖 Dispatch, Group 服务. 提供一个解决 API 类型音讯的接口.

Dispatch

音讯路由服务, 用于音讯路由到用户所在网关, 在用户登录时告诉 Dispatch 更新缓存用户对应的网关, 缓存信息通过一致性哈希保留在固定的一个服务上, 音讯下发时查问缓存, 依据查问到的网关信息放入到 NSQ 队列, 每 Gateway 订阅本人的音讯. 这里的音讯不肯定是用户音讯, 也能够是告诉网关更新用户状态的管制音讯, 例如登录登出, 因为应用音讯队列进行通信, 所以叫做音讯, 其实是调用 Gateway 的接口.

Dispatch 不间接依赖任何服务, 音讯通过 NSQ 发送到 Gateway, 提供更新路由和下发音讯两个接口.

Broker

群聊与用户网关一样是属于带状态服务, 音讯须要精确疾速路由到群所在门路, Broker 和 Dispatch 性能大抵类似.

NSQ

NSQ 是 Golang 实现的音讯队列, 所有音讯都通过 NSQ 路由. 相比其余 MQ 抉择 NSQ 的理由: 去核心分布式(生产生产直连), 低提早, 不须要程序, 高性能, 简略二进制协定.

在每一台生产者上都部署一个 nsqd.

2.2 HTTP API 服务划分

  • 认证: 用户鉴权, 登录注册等
  • 用户: 用户信息管理
  • 音讯: 音讯同步, 拉取等接口, 音讯 ID
  • 群治理: 群增删改查
  • 其余

以上划分只是在我的项目中的模块划分, 并未独立成服务, 但拆分这些是轻而易举的事件, 下面这些接口既能够通过 HTTP 服务拜访, 也可应用长连贯拜访, 长连贯 与 HTTP 拜访的差别在于: 1. 长连贯拜访须要增加 公共申请体, 2. 公共响应体定义不同.

2.3 音讯路由

网关音讯路由

在分布式部署环境下, 网关可能部署任意个实例, 用户可能连贯到其中任意一个实例中, 当须要给某一个用户发送音讯, 或者断开某个用户的连贯时, 咱们须要找到这个用户所在的网关, 这就须要记录所有在线用户所在网关. 可能快想到的是应用 Redis, 或者 Redis 加二级缓存, 但 IM 零碎音讯吞吐量十分大, 而且存在扩散等其余起因, Redis 很容易成为性能瓶颈.

GlideIM 应用一致性哈希算法, 将每个用户连贯的网关信息依照 UID 散布在不同的 Dispatch 服务上, 从而达到扩散缓存, 负载平衡, 及进步可用性的目标.

如上图所示, Dispatch 在整个环节中承担了音讯散发的角色. 当某一个 Dispatch 服务宕机后, 该服务中所有缓存的网关信息都将失落, 依据一致性哈希算法, 原来的申请都转向了下一个 Dispatch, 显然这个服务上是没有已宕机的那个服务缓存的信息的, 咱们能够在用户登录后将登录信息缓存在 Redis 中 (查问登录设施列表等其余场景共用), Dispatch 内存中没查到再去 Redis 查找, 查找一次后缓存在内存中即可. 新上线的用户则不影响, 只影响在线期间网关信息保留的 Dispatch 服务宕机的用户.

群音讯的路由

群和用户一样 Group 服务也须要一样的路由, 不同群可能散布在不同的服务上, 但群并不会随便切换所在服务, 个别只有群所在的服务异样的时候从新加载群信息才可能重新分配, Broker 读多写少, 因而只须要在所有 Broker 中都缓存所有群所在服务即可, 群音讯能够通过任意一台 Broker 转发, 当 Group 服务掉线和加载群时告诉所有 Broker 更新即可.

2.4 设计准则

在本我的项目中, 我始终在摸索和谋求我的项目整洁性与简单零碎架构之间矛盾的解决方案, 但因为我没有任何相干教训, 在我的项目中可能呈现一些非主流的格调或谬误冗余的设计, 若你对此有任何纳闷或指教, 请在 issues 中自在地发表你的想法.

在面对一个宏大简单的问题, 将问题拆分并形象成对象, 划分为一个个小问题往往不便解决, OOP 在这方面始终是劣势, 本我的项目应用了较多的 OOP 思维.

应用接口

对一些可能须要扩大的关键点, GlideIM 应用接口的形式实现的, 例如 Dao (数据拜访对象)层实现均为接口, 出于工夫起因(还有我自己对数据库方面常识匮乏), 我的项目中对数据库方面均是简略实现, 并未思考例如性能等优化, 但接口不影响前期对这方面降级和替换.

按业务分包

包的划分形式个别有两种, 按层分包和按业务分包. 按层分包就是雷同类型的模块放在同一个包, 例如将所有的 API 申请解决模块放一层, 将申请和响应的实体对象放在同一个包, 这这办法给模块的治理减少了不便, 咱们更改一个接口须要在几个不同的包中批改代码, 这样分包一个包里的不同代码没有任何关联, 齐全是相似于图书馆管理员依据书的出版社进行分类一样.

而按性能分包则不会呈现更新一个性能, 批改不同包的代码的状况, 一个性能相干的代码都在雷同包内. GlideIM 每个包尽量都遵循繁多职责准则, 多个包之间尽量升高耦合度.

模块接口依赖

我的项目包的划分根本和微服务的模块划分保持一致, 刚开始启动的本我的项目时就依照了按性能划分模块的办法, 并且每个模块只裸露提供给其余模块的接口, 模块之间的调用只通过特定的接口, 这样设计即不便保护也不便其他人查看代码, 不会使得模块之间的调用凌乱, 而且也为前期微服务化的提供了极大便当.

例如 Gateway 模块的接口

// 提供给其余模块的接口
type Interface interface {ClientSignIn(tempId int64, uid int64, device int64)
    ClientLogout(uid int64, device int64)
    EnqueueMessage(uid int64, device int64, message *message.Message)
}
// Gateway 依赖的接口
type MessageHandleFunc func(from int64, device int64, message *message.Message) 

其余模块只须要晓得 Gateway 模块提供客户端的登录登出和将音讯退出到指定 ID 的客户端的队列即可, 其余模块不用晓得 Gateway 的具体实现, 咱们能够轻而易举的将这个模块替换为一个微服务服务.

2.5 微服务

GlideIM 应用 RPCX 作为微服务的根底, 开箱即用的微服务计划让我抉择了它, RPCX 功能丰富, 性能优越, 集成了服务发现, 多种路由计划, 以及失败模式, 服务发现应用 ETCD.

服务间通信均应用 Protobuff + RPC 形式, 在性能上这是最好的抉择组合.

2.6 我的项目介绍

我的项目目录

一下目录构造省略了局部不重要的目录

├─cmd                   // 入口(从这里开始运行)
│  ├─performance_test   // 性能测试代码入口
│  ├─run                // 我的项目程序入口
│  │  ├─api_http        // API 接口的 HTTP 服务, 提供给客户端拜访
│  │  ├─api_rpc         // API 接口的 RPC 服务, 提供给其余服务应用
│  │  ├─broker          // 群路由服务
│  │  ├─dispatch        // 网关路由服务
│  │  ├─getaway         // 网关服务
│  │  ├─group           // 群服务
│  │  ├─messaging       // IM 音讯路由服务
│  │  └─singleton       // 单实例运行 (这里同时启动 IM 和 HTTP API 接口)
│  └─script             // 部署脚本
│      ├─etcd           // 启动 etcd 脚本
│      ├─glide_im       // 我的项目部署脚本
│      └─nsq            // nsq 部署脚本
├─config                // 配置入口
├─doc                   // 我的项目文档
├─im                    // IM 外围逻辑入口
│  ├─api                ////////////// API 接口
│  │  ├─apidep          // API 内部依赖        
│  │  ├─auth            // 登录认证
│  │  ├─comm            // 公共
│  │  ├─groups          // 群治理
│  │  ├─http_srv        // api http 服务启动逻辑
│  │  ├─msg             // 音讯
│  │  ├─router          // 通过长连贯拜访接口的路由形象
│  │  └─user            // 用户相干
│  ├─client             ///////////// 用户连贯治理相干
│  ├─conn               // 长连贯根底形象
│  ├─dao                // 数据拜访层, 数据库相干
│  ├─group              // 群聊, 及群聊音讯
│  ├─message            // IM 音讯定义
│  ├─messaging          // IM 音讯路由
│  └─statistics         // 数据统计, 测试用
├─pkg                   ///////////// 包, 公共依赖治理
│  ├─db                 // 数据库
│  ├─hash               // hash 算法实现
│  ├─logger             // 日志打印
│  ├─lru                // lru 缓存实现
│  ├─mq_nsq             // nsq 封装
│  ├─rpc                // rpc 封装, 基于 rpcx
│  └─timingwheel        // 定时器, 工夫轮算法实现
├─protobuf              //////////// protobuf 音讯定义
│  ├─gen                // 编译好的文件
│  ├─im                 // im 音讯定义
│  └─rpc                // rpc 通信音讯定义
├─service               /////////// 微服务
│  ├─api_service        // api 微服务实现
│  ├─broker             // 群路由 broker 服务
│  ├─dispatch           // 网关音讯路由服务
│  ├─gateway            // 网关服务实现
│  ├─group_messaging    // 群服务
│  ├─messaging_service  // im 音讯路由服务
├─sql                   // 数据库表构造 SQL
  • 我的项目查看指南

IM 外围逻辑在根目录下 im 中, 除了微服务相干, IM 次要业务逻辑实现均在这个目录下, im 下包的划分大抵能够看做是前面微服务的划分, im/conn 包中是长连贯服务器启动和连贯对象接口的逻辑, 新的连贯将交由 im/client 包中治理, 这个包大抵是治理连贯, 读写解析音讯的, 收到链接中的音讯将交由 im/messaging 包解决, 这个包工具音讯类型交给不同的模块, 例如: 认证音讯给 api 模块解决, 群音讯分发给 group, 单聊音讯则 im/client 下发. im 下波及业务逻辑的包下根本都有一个 interface.go 文件, 这个文件定义了这个包的依赖以及向内部提供的接口.

微服务相干的代码都在根目录 service 包上面, IM 外围业务相干的服务逻辑都在 im 下, 服务只是对其接口应用 rpc 服务的形式进行实现, 并在 im 包中对应接口的默认实现进行替换, 例如 messaging_service 服务 (音讯路由) 则是将 im/messaging 包下的 Interface 接口的实现替换为 messaging_serviceServer, 而 messaging 依赖的其余包则是对应服务下的 Client, 查阅每个服务包下的 run.go 即可找到服务启动代码, 其中蕴含了其依赖的和实现设置.

对于将外围逻辑和微服务剥离的这种模式是因为一开始并将微服务的划分定型 (也通过了几次较大的改变), 或者为了不便将 IM 局部外围逻辑先实现和理清, 所以将两者放在两个不同的中央, 但在我实际的过程中, 对于服务的改变和 im 下外围逻辑(模块接口改变除外) 的改变并不会相互株连, 这点给我很大的便当, 我还有肯定的自由度划分增加服务, 例如在 group 服务两头加一个 broker, 或者能够针对特定 im 中某个模块的接口中的一个办法进行非凡解决.

然而下面所说的便当仅仅是因为我过后对微服务不太熟悉, 我的项目开发过程改变较大, 离开改变的时候略微不便一些. 这样做代价就是, 两者离开不利于代码查看, 这其实是一种分层, im 下定义接口及默认实现, 下层 service 定义接口的实现, 但这些接口之间的依赖关系, 还是要去 im 中寻找, 前面可能会思考将这两个包合并在一起.

我的项目次要依赖

  • BurntSushi/toml: 这是一款优良的配置文件格式, 集体比拟喜爱
  • gin: 优良的 HTTP Web 框架
  • protobuff: Google 出品二进制数据传输协定
  • gorilla/websocket: Golang 中最多 star 的 WebSocket 库
  • nsq: 简略, 高性能, 分布式 MQ
  • rpcx: 高性能, 功能丰富的微服务框架
  • gorm: ORM
  • go-redis/redis: Redis 客户端
  • ants: 协程池

构建和运行

根目录下 cmd/run 下的包为程序入口, 包名示意其服务 / 模式, 例如 api_http 包为以 HTTP 服务启动 API 的接口(提供给客户端 HTTP 形式调用), api_rpc 为启动 API RPC 服务(提供给其余服务调用), 为了疾速调试, 这里还有一个单实例模式 singleton, 这个入口会同时启动 IM 长连贯服务和 HTTP API 服务, 不便调试 im 外围逻辑, 或者调试客户端用.

环境依赖

  • 单实例

    • redis
    • mysql
  • 微服务

    • nsq
    • etcd
    • 蕴含单实例的所有依赖

配置文件

  • 单实例模式在 singleton 包中 config.toml 批改相干配置
  • 微服务模式须要将 service/example_config.toml 文件复制到对于服务入口下, 并依据环境批改相干配置

如果依赖或者其余起因无奈在 IDE 中运行代码, 能够在 下载曾经编译好的 singleton 模式的可执行文件.

2.7 现存问题

协定相干

  • 客户端协定抉择

目前为了不便客户端应用 json 进行通信, 也是因为浏览器对二进制协定的反对不敌对, 但后端对二者都有实现, 协定动静抉择并没有实现, 或者辨别 websocket 和 tcp 网关, 将二者辨别, 浏览器的与挪动端环境有差别, 但未做解决.

  • Protobuf 和 Json 的兼容

微服务应用 protobuf 协定, 而客户端音讯可能以 json 协定, 局部应用同一个由 protoc 编译生成的 struct 在兼容上存在一些问题, 暂未解决.

  • 音讯解析性能

应用 go 中的 json.Marshal 性能极差, 在整个音讯流转过程中占用了大量工夫, 暂未优化, 目前有多种第三方计划能够抉择.

  • 协定版本

音讯协定可能会降级, 新老客户端应用不同协定版本兼容上为做解决.

数据库相干

  • 音讯 ID 生成

我的项目中目前应用 Redis Incr 生成递增 ID, 存在性能问题, 前面能够思考应用 Leaf 等计划.

  • 数据库, 表, 查问优化

未对这方面做任何优化, dao 层均只是简略实现了 CRUD 性能.

微服务相干

  • 配置管理

目前启动服务是通过本地配置文件加载配置启动, 部署治理起来不太不便, 前面思考应用配置核心.

  • 包构造

目前微服务和 IM 逻辑是分层设计, 不不便保护和查看, 前面须要调整.

群聊的相干

  • 音讯风暴

群音讯存在扩散问题, 尤其是大群一个成员发消息扩散几千上万次, 群数量略微多一点或者音讯略微频繁一点, 音讯数量非常容易失控. 在微服务模式下须要对系统网关的用户音讯进行合并打包成一条音讯.

  • 群聊服务故障

群聊属于带状态服务, 当群所在服务宕机后如何不影响群音讯的散发, 这点暂未解决, 对于复原能够新增一个群聊服务监控服务, 监控到某一个群聊服务掉线立马复原, 但这仅是故障复原而不是对故障转移.

三. 设计细节

因为 GlideIM 的设计细节较多, 限于工夫和篇幅, 这里只列举了几个较为重要的环节来做简略的阐明, 详细情况或其余未说明的中央能够查看源码或退出探讨群一起探讨.

3.1 音讯类型

IM 音讯类型指的是聊天协定音讯类型, 留神区别聊天内容的音讯类型, IM 音讯类型关联前后端单方的业务逻辑, 聊天音讯类型只须要在前端解决.

  • IM 音讯类型

    • 聊天音讯: 重发, 重试, 撤回, 群聊, 单聊
    • ACK 音讯: 服务器确认收到, 接受者确认告诉, 接受者确认送达
    • 心跳音讯: 客户端心跳, 服务端心跳
    • API 音讯: 令牌认证, 退出等
    • 告诉音讯: 新联系人, kickout, 多端登录等
  • 聊天内容音讯类型

    • 图片
    • 文本

音讯类型由客户端定义及解决, 二进制音讯例如语音, 图片则上传到服务器再发送 URL. 例如红包这种互动型音讯, 一样能够应用这种办法.

客户端之间定义约定音讯类型即不便了前期增加音讯类型, 也不便了后端保护, 后端并不需要晓得音讯内容的类型.

3.2 送达机制

虽说 TCP 是牢靠传输, 但在消息传递过程并不是十拿九稳的, 例如, 接受者在线但网络不好, 客户端在重连, 则音讯可能不能及时送达接受者, 设计一个送达机制进步送达率是必要的. GlideIM 采纳了单条音讯两次确认 (服务端和接收者) 的送达机制.

A 发送一条音讯给 B, 如果 B 在线, 服务端则回复 A 一条服务端确认收到音讯通知 A 服务端已收到, 曾经入库保留了, 而后服务端发送给 B, B 收到则发送一条确认收到音讯给服务端, 服务端收到后再发送一条确认送达音讯给 A, 若 B 不在线则间接发送给 A 确认送达, A 此时就晓得这条音讯肯定被 B 收到了, 在此过程中 A 如果没有收到 B 确认收到则多次重试.

在客户端做重发防止了服务端逻辑复杂化, 而客户端做则大大简化了逻辑. 有了这个送达机制, 不论是音讯丢了还是网络环境不好的时候, 都能进步送达率和音讯及时率, 再者若咱们应用 UDP, 有了这个机制也能够确保音讯的送达率.

GlideIM 音讯 ACK 机制几种音讯失落状况下的时序图如下:

以上只是 GlideIM 保障送达的一部分, 实际上还有接收端依据 Seq 判断是否音讯缺失等其余措施.

3.3 音讯路由

在分布式环境下, 用户连贯到不同的网关上, 咱们必须晓得用户所在的网关能力精确的投递音讯, 因而, 必须在用户连贯到网关的时候缓存所在网关信息, 在投递音讯的时候能力精确地投递到用户所在网关.

GlideIM 的设计是在用户登录时在 Redis 中缓存网关信息, 咱们当然不能每次发消息都查问 Redis, 能够在内存中缓存一份, 内存中没有则查问 Redis. 用户登录聊天服务后依据联系人列表所在不同网关, 告诉网关更新本人的路由信息.

为了防止客户端频繁切换网关, 客户端接入网关由服务端在用户登录时返回, 这样做还能够为制订负载平衡策略, 多端登录连贯同网关等做思考.

3.4 保活机制

TCP 虽有 KeepAlive, 但其默认工夫太长, 且不能判断服务是否可用或客户端是否可用, KeepAlive 仅仅保障 TCP 处于连贯状态, 如果应用层产生谬误, 服务端连贯却仍旧通顺显著是不行的.

心跳分为服务端心跳和客户端心跳, 大部分状况是客户端侧网络不通顺, 例如进电梯, 息屏省电模式, 而服务端个别只会在宕机的状况才会网络不通顺, 因而 GlideIM 在客户端进行被动心跳, 若在指定工夫内服务端未收到心跳则服务端开始发送心跳包, 指定次数没有收到客户端心跳则断开连接.

客户端心跳为 30s, 但并不是每隔 30s 都发心跳, 而是 30s 内客户端没有被动发送任何音讯, 则进行一次心跳, 服务端也是依据这个规定判断, 这样缩小了心跳的次数的同时也能确保存活.

3.5 音讯协定

音讯协定须要思考到音讯编码后的大小, 可读性, 编码速度, 反对的语言等. 能够抉择二进制协定和文本协定两种, 二进制例如 Protobuff, 或者自定义, 文本协定例如 JSON, XML.

GlideIM 同时实现了 Protobuff 和 JSON 两种音讯协定. 客户端能够自由选择应用哪种协定, 在测试后果中显示 Protobuff 比 JSON 至多快 10 倍, 在应用 JSON 时整个流程中, 音讯解析占用了很大一部分工夫.

3.6 音讯去重和排序

GlideIM 的音讯去重依附全局音讯 ID, 音讯 ID 临时应用 Redis Incr, 前面会应用美团 Leaf, 在 GlideIM 中替换音讯 ID 生成策略非常简单, 音讯 ID 由客户端发送音讯时获取, 发送时附带该 ID 以实现去重, 和发送方音讯的排序.

在一对一单聊的状况, GlideIM 只保障发送方有序, 而一个会话内的所有音讯是不用保障程序的, 绝对其付出的代价和带来的收益. 而群聊则是保障所有音讯有序的, 群音讯下发会附带以后群的间断递增 Seq, 收到群音讯依据 Seq 排序, 若发现不间断则拉取不间断局部的音讯.

四. 性能测试

GlideIM 音讯面对高并发吞吐量压力测试:

4.1 测试后果

单实例部署模式下, 4H8G, 100Mbps 宽带实践上反对 20w 沉闷用户在线同时聊天, 此时带宽是性能的瓶颈.

4.2 测试过程

服务端配置

Windows 10
AMD R5 3600 6 核 12 线程 
16GB 内存
100Mbps 网卡

案例 1 测试过程

A 机器运行服务端, B 模仿客户端连贯登录发消息的过程, 并运行数据库

同时模仿 2000 客户端, 每距离 60ms-200ms 发送一条音讯, 每个客户端发送 600 条音讯, 共计 1200_000 条音讯.

网卡负载均匀 90% (100Mbps), 每秒约 30_000 条音讯吞吐量, 每秒上下行各 15k 条, 送达率 100%, 所有音讯延时 <=20ms, 参考后果 1 图.

测试状况:

网络 100%
2000 连贯
5-20 条音讯 / 秒 / 连贯
30k 音讯 /tps
送达率 100%

案例 2 测试过程

这个案例只为了测试程序在高并发状况下的运行状况, 参考价值也仅限于程序自身的短板和代码逻辑极限, 在理论状况中咱们无奈疏忽网络速率等因素的影响.

因为须要去除网卡速率限度, 服务和模仿客户端都在同一台设施上运行则不受网络因素影响, 模仿 10000 个链接, 每个连贯 50ms-100ms 发送一条音讯, 总共发送 800 条.

此案例音讯吞吐量极限为 28w, 此时已达到 cpu 性能极限, 占用率约 98%, 客户端模仿 cpu 占用率高于服务端, 但无关紧要.

测试案例 pprof CPU 应用状况剖析数据: cpu.out

测试状况:

CPU 98 %
内存约 3 GB
10000 连贯峰值, 每 10ms 开始一个连贯
10-20 条音讯 / 秒 / 连贯, 累计发送 800 条
280k 音讯 /tps 峰值
送达率 100%

测试环境限度

因为测试只有两台电脑, 存在端口数限度(部署到服务器, 用户连贯不存在这个限度), 为了进步 MySQL 连接池的大小进步并发, 必须尽量用更少的连接数和每个连贯更密集的发送音讯来进行测试

百兆网卡下限速率只有 12.5MB/s, 一条音讯蕴含 20 个汉字则至多须要 40B, 再加上报头其余占用, 假如 100B, 则理论百兆网卡最大的音讯并发是 12.5 x 1024 x 1024 / 100 = 13w 条, 而去除
ACK, 再上下行音讯数量对半, 理论可能就 6w/s 音讯就是 GlideIM 在 100Mbps 环境下的实践极限.

测试代码

单机性能测试

测试代码门路: /cmd/performance_test/

1. 启动服务器

go test -v -run=TestServerPerf

2. 开始用户模仿

go test -v -run=TestRunClient

4.3 性能参考指标阐明

连接数

很多 IM 我的项目喜爱用相似于 “ 百万连贯 ” 之类的字眼, 很多人可能是受惯例 HTTP 服务 “ 百万并发 ” 的影响, 认为百万连贯与百万并发具备统一的性能参考价值, 开发者为了吸引眼球应用百万连贯的字眼, 也的确反对百万链接.

事实上, TCP(或 WS) 连接数的限度并不在于程序自身, 零碎最大可关上 文件描述符 和 内存 限度了最大连接数, 一个 TCP 连贯大概须要占用 4-10KB 内存, 一台 16GB 的服务器实践上最大反对约 16 1024
1024 / 5 ≈ 335w 连贯.

沉闷用户数则比单纯的连接数更有参考价值, 例如反对 100w 沉闷连贯, 每个沉闷连贯 10s 发送一条音讯.

音讯吞吐量

相比连接数, 音讯吞吐量更具备参考价值, 音讯吞吐量须要联合网络速率做参考. 音讯吞吐量受限点次要有一下几个:

  1. 音讯送达保障机制的性能(即保障送达又不冗余)
  2. 音讯数据传输协定的性能(单条音讯尽可能减小体积)
  3. 非用户音讯的数据包数量管制(例如心跳包, 内容同步包等)
  4. 网络速率和品质(内部因素)
  5. 程序自身音讯下发设计缺点

其余指标

考量一款 IM 的性能的意义大部分可能只在于揭示它本身的性能短板, 横向比照其余我的项目在大部分状况都无奈得出正确的比照后果, 单项指标也不能齐全掂量整个我的项目的优劣, 咱们应该联合其业务逻辑, 设计思维, 学习其长处能力有所播种.

  1. 音讯送达率
  2. 音讯提早和音讯程序的准确性
  3. 链接保活和去死链(心跳)

4.4 性能指标估算

1. 连接数

假如设内存为 M GB

实践连接数  = M * 1024m * 1024k / (4k/tcp)

激进估算连接数 = M * 1024m * 1024k / (10k/tcp)

2. 音讯吞吐量估算

设网卡速率为 S Mbps, 均匀每条音讯为 K 字节(写入 TCP 连贯时的总大小).

音讯吞吐量 = S / 8 * 1024k * 1024b / K

3. 沉闷用户数估算

设吞吐量 T, 设均匀每个用户每 N 秒发送一条音讯.

  • 在确认送达机制下 (每条音讯的发送, 假如收发单方读在线, 服务端客户都需 ACK, 共须要 5 条音讯上下行)
沉闷用户数 = T / 5 * N
  • 仅确认送达服务端
沉闷用户数 = T / 3 * N
退出移动版