本文由小米技术团队分享,原题“小爱接入层单机百万长连接演进”,有修订。
小爱接入层是小爱云端负责设备接入的第一个服务,也是最重要的服务之一,本篇文章介绍了小米技术团队2020至2021年在这个服务上所做的一些优化和尝试,最终将单机可承载长连接数从30w提升至120w+,节省了机器30+台。
提示:什么是“小爱”?
小爱(全名“小爱同学”)是小米旗下的人工智能语音交互引擎,搭载在小米手机、小米AI音箱、小米电视等设备中,在个人移动、智能家庭、智能穿戴、智能办公、儿童娱乐、智能出行、智慧酒店、智慧学习共八大类场景中使用。
(本文同步发布于:http://www.52im.net/thread-3860-1-1.html)
本文是专题系列文章的第7篇,总目录如下:
整个小爱的架构分层如下:
接入层主要的工作在鉴权授权层和传输层,它是所有小爱设备和小爱大脑交互的第一个服务。
由上图我们知道小爱接入层的重要功能有如下几个:
小爱接入层最早的实现是基于Akka和Play,我们使用它们搭建了第一个版本,该版本特点如下:
随着小爱长连接的数量突破千万大关,针对早期的接入层方案,我们发现了一些问题。
主要的问题如下:
1)长连接数量上来后,需要维护的内存数据越来越多,JVM的GC成为不可忽略的性能瓶颈,且一旦代码写的不好有GC风险。经过之前事故分析,Akka+Play版的接入层其单实例长连接数量的上限在28w左右。
2)老版本的接入层实现比较随意,其Akka Actor之间存在非常多的状态依赖而不是基于不可变的消息传递这样使得Actor之间的通信变成了函数调用,导致代码可读性差且维护很困难,没有发挥出Akka Actor在构建并发程序的优势。
3)作为接入层服务,老版本对协议的解析是有很强的依赖的,这导致它要随着版本变动而频繁上线,其上线会引起长连接重连,随时有雪崩的风险。
4)由于依赖Play框架,我们发现其长连接打点有不准确的问题(因为拿不到底层TCP连接的数据),这个会影响我们每日巡检对服务容量的评估,且依赖其他框架在长连接数量上来后我们没有办法做更细致的优化。
基于早期接入层技术方案的种种问题,我们打算重构接入层。
对于新版接入层我们制定的目标是:
于是,我们开始了单机百万长连接的漫漫实践之路。。。
接入层与外部服务的关系理清如下:
接入层的主要功能划分如下:
把之前的单一模块按照是否有状态,拆分为两个子模块。
具体如下:
所以,按照上面的原则,理论上我们会做出这样的功能划分,即前端很小、后端很大。示意图如下图所示。
模块拆分为前后端:
补充:前端负责建立与维护设备长连接的状态,为有状态服务;后端负责具体业务请求,为无状态服务。后端服务上线不会导致设备连接断开重连及鉴权调用,避免了长连接状态因版本升级或逻辑调整而引起的不必要抖动;
前端使用CPP实现:
后端暂时使用Scala实现:
通讯使用ZeroMQ:
进程间通讯最高效的方式是共享内存,ZeroMQ基于共享内存实现,速度没问题。
整体架构:
如上图所示,由四个子模块组成:
8.2.1)传输层:
WebSocket 部分使用 C++ 和 ASIO 实现 websocket-lib。小爱长连接基于WebSocket协议,因此我们自己实现了一个WebSocket长连接库。
这个长连接库的特点是:
压测显示该库的性能十分优异的:
这一层同时也承担了除原始WebSocket外,其他两种通道的的收发任务。
目前传输层一共支持以下3种不同的客户端接口:
8.2.2)分发层:
把不同的传输层事件转化成统一事件投递到状态机,这一层起到适配器的作用,确保无论前面的传输层使用哪种类型,到达分发层变都变成一致的事件向状态机投递。
8.2.3)状态机处理层:
主要的处理逻辑都位于这一层中,这里非常重要的一个部分是对于发送通道的封装。
对于小爱应用层协议,不同的通道处理逻辑是完全一致的,但是在处理和安全相关逻辑上每个通道又有细节差异。
比如:
针对这种情况:我们使用C++的多态特性来处理,专门抽象了一个Channel接口,这个接口中提供的方法包含了一个请求处理的一些关键差异步骤,比如如何发送消息到客户端,如何stop连接,如何处理发送失败等等。对于3种(ws/wss/xmd)不同的发送通道,每个通道有自己的Channel实现。
客户端连接对象一创建,对应类型的具体Channel对象就立刻被实例化。这样状态机主逻辑中只实现业务层的公共逻辑即可,当在有差异逻辑调用时,直接调用Channel接口完成,这样一个简单的多态特性帮助我们分割了差异,确保代码整洁。
8.2.4)ZeroMQ 通讯层:
通过两个线程将ZeroMQ的读写操作异步化,同时负责若干私有指令的封装和解析。
8.3.1)无状态化改造:
后端做的最重要改造之一就是将所有与连接状态相关的信息进行剔除。
整个服务以 Request(一次连接上可以传输N个Request)为核心进行各种转发和处理,每次请求与上一次请求没有任何关联。一个连接上的多次请求在后端模块被当做独立请求处理。
8.3.2)架构:
Scala 服务采用 Akka-Actor 架构实现了业务逻辑。
服务从 ZeroMQ 收到消息后,直接投递到 Dispatcher 中进行数据解析与请求处理,在 Dispatcher 中不同的请求会发送给对应的 RequestActor进行 Event 协议解析并分发给该 event 对应的业务 Actor 进行处理。最后将处理后的请求数据通过XmqActor 发送给后端 AIMS&XMQ 服务。
一个请求在后端多个 Actor 中的处理流程:
8.3.3)Dispatcher 请求分发:
前端与后端之间通过 Protobuf 进行交互,避免了Json 解析的性能消耗,同时使得协议更加规范化。
后端服务从 ZeroMQ 收到消息后,会在 DispatcherActor 中进行PB协议解析并根据不同的分类(简称CMD)进行数据处理,分类包括如下几种。
* BIND 命令:
鉴权功能,由于鉴权功能逻辑复杂,使用C++语言实现起来较为困难,目前依然放在 scala 业务层进行鉴权。该部分对设备端请求的 HTTP Headers 进行解析,提取其中的 token 进行鉴权,并将结果返回前端。
* LOGIN 命令:
设备登入,设备鉴权通过后当前连接已成功建立,此时会进行 Login 命令的执行,用于将该长连接信息发送至AIMS并记录于Varys服务中,方便后续的主动下推等功能。在 Login 过程中,服务首先将请求 Account 服务获取长连接的 uuid(用于连接过程中的路由寻址),然后将设备信息+uuid 发送至AIMS进行设备登入操作。
* LOGOUT 命令:
设备登出,设备在与服务端断开连接时需要进行 Logout 操作,用于从 Varys 服务中删除该长连接记录。
* UPDATE 与 PING 命令:
a. Update 命令,设备状态信息更新,用于更新该设备在数据库中保存的相关信息;
b. Ping 命令,连接保活,用于确认该设备处于在线连接状态。
* TEXT_MESSAGE 与 BINARY_MESSAGE:
文本消息与二进制消息,在收到文本消息或二进制消息时将根据 requestid 发送给该请求对应的RequestActor进行处理。
8.3.4)Request 请求解析:
针对收到的文本和二进制消息,DispatcherActor 会根据 requestId 将其发送给对应的RequestActor进行处理。
其中:文本消息将会被解析为Event请求,并根据其中的 namespace 和 name 将其分发给指定的业务Actor。二进制消息则会根据当前请求的业务场景被分发给对应的业务Actor。
在完成新架构 1.0 调整过程中,我们也在不断压测长连接容量,总结几点对容量影响较大的点。
8.4.1)协议优化:
a. JSON替换为Protobuf: 早期的前后端通信使用的是 json 文本协议,后来发现 json 序列化、反序列化这部分对CPU的占用较大,改为了 protobuf 协议后,CPU占用率明显下降。
b. JSON支持部分解析:业务层的协议是基于json的,没有办法直接替换,我们通过"部分解析json"的方式,只解析很小的 header 部分拿到 namespace 和 name,然后将大部分直接转发的消息转发出去,只将少量 json 消息进行完整反序列化成对象。此种优化后CPU占用下降10%。
8.4.2)延长心跳时间:
在第一次测试20w连接时,我们发现在前后端收发的消息中,一种用来保持用户在线状态的心跳PING消息占了总消息量的75%,收发这个消息耗费了大量CPU。因此我们延长心跳时间也起到了降低CPU消耗的目的。
8.4.3)自研内网通讯库:
为了提高与后端服务通信的性能,我们使用自研的TCP通讯库,该库是基于Boost ASIO开发的一个纯异步的多线程TCP网络库,其卓越的性能帮助我们将连接数提升到120w+。
经过新版架构1.0版的优化,验证了我们的拆分方向是正确的,因为预设的目标已经达到:
再重新审视下我们的理想目标,以这个为方向,我们就有了2.0版的雏形:
具体就是:
2.0版目标是:经过以上改造后,期望单前端模块可以达到200w+的连接处理能力。
[1] 上一个10年,著名的C10K并发连接问题
[2] 下一个10年,是时候考虑C10M并发问题了
[3] 一文读懂高性能网络编程中的线程模型
[4] 深入操作系统,一文读懂进程、线程、协程
[5] Protobuf通信协议详解:代码演示、详细原理介绍等
[6] WebSocket从入门到精通,半小时就够!
[7] 如何让你的WebSocket断网重连更快速?
[8] 从100到1000万高并发的架构演进之路
学习交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK
(本文同步发布于:http://www.52im.net/thread-3860-1-1.html)