在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到一线互联网企业如阿里、网易、有赞、希音、百度、滴滴的面试资格。
最近,尼恩指导一个小伙伴简历,写了一个《长连接网关项目架构与实操》,此项目帮这个小伙拿到 字节/阿里/微博/汽车之家 面邀, 所以说,这是一个牛逼的项目。
为了帮助大家拿到更多面试机会,拿到更多大厂offer,
尼恩决定:9月份給大家出一章视频介绍这个项目的架构和实操,《33章: 10Wqps 高并发 Netty网关架构与实操》,预计月底发布。然后,提供一对一的简历指导,这里简历金光闪闪、脱胎换骨。
《33章: 10Wqps 高并发 Netty网关架构与实操》 海报如下:
配合《33章:10Wqps 高并发 Netty网关架构与实操》, 尼恩会梳理几个工业级、生产级网关案例,作为架构素材、设计的素材。
前面梳理了
除了以上的5个案例,在梳理学习案例的过程中,尼恩又找到一个漂亮的生产级案例:《单体120万连接,小爱网关如何架构?》,
注意,这又一个非常 牛逼、非常顶级的工业级、生产级网关案例。
这些案例,并不是尼恩的原创。
这些案例,仅仅是尼恩在《33章:10Wqps 高并发 Netty网关架构与实操》视频备课的过程中,在互联网查找资料的时候,收集起来的,供大家学习和交流使用。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取
作者:小爱技术团队
小爱(又名“小爱同学”)是小米公司旗下的一款人工智能语音交互引擎,
“小爱同学”是小米集团统一的智能语音服务基础设施,
“小爱同学”客户端被集成在小米手机、小米 AI 音箱、小米电视等设备中,广泛应用于个人移动、智能家居、智能穿戴、智能办公、儿童娱乐、智能出行、智慧酒店和智慧学习等八大场景。
小爱接入层是小爱云端设备接入的关键服务,也是核心服务之一。
小米技术团队在 2020 年至 2021 年期间对该服务进行的一系列优化和尝试,最终,成功地将单机可承载的长连接数量从 30 万提高到 120 w+,节省了 30 +台机器。
小爱整体架构的分层如下所示:
接入服务主要负责鉴权授权层和传输层的工作,它是所有小爱设备与小爱大脑互动的第一个服务。
从上图我们可以看出,小爱接入服务的重要功能包括以下几点:
小爱接入层最早的实现是基于Akka和playframework,我们使用它们搭建了第一个版本,其特点如下:
注意, playframework 简称为 play , 是一款 和springmvc 类似的web 开发框架。
随着小爱长连接数量达到千万级别,我们发现早期接入层方案存在一些问题。
主要的问题如下:
鉴于早期接入层技术方案存在诸多问题,我们决定重构接入层。
新版接入层的设计目标如下:
因此,我们开始了实现单机百万长连接的探索之旅。
接入层与外部服务的关系理清如下:
接入层的主要职责可以概括为以下几点:
把之前的单一模块按照是否有状态,拆分为两个子模块。
具体如下:
因此,根据上述原则,理论上我们将实现这样的功能划分,即前端较小,后端较大。示意图如下所示。
模块拆分为前端和后端:
补充:前端负责维护设备长连接的状态,因此它是有状态的服务;而后端负责处理具体的业务请求,所以它是无状态的服务。后端服务的上线不会导致设备连接中断并重新连接,也不会引发鉴权调用,这样就避免了因为版本升级或逻辑调整而导致的长连接状态的不必要波动。
前端使用C++实现:
后端暂时使用Scala实现:
通讯使用ZeroMQ:
整体架构:
如上图所示,由四个子模块组成:
WebSocket 部分采用 C++和 ASIO 实现 websocket-lib。小爱长连接基于 WebSocket 协议,因此我们自主实现了一个 WebSocket 长连接库。
这个长连接库的特点是:
压测显示该库的性能十分优异的:
长链接数 | qps | P99延时 |
---|---|---|
100w | 5w | 5ms |
这一层同时也负责除原始 WebSocket 外,其他两种通道的收发任务。
目前传输层一共支持以下 3 种不同的客户端接口:
将不同的传输层事件转化为统一事件并投递给状态机,这一层起到适配器的作用,确保无论前面的传输层使用哪种类型,到达分发层都会变成一致的事件投递给状态机。
主要的处理逻辑都在这一层,这里非常重要的一个部分是对发送通道的封装。
对于小爱应用层协议,不同的通道处理逻辑是完全一致的,但在处理和安全相关逻辑上每个通道又有细节差异。
比如:
针对这种情况:我们使用 C++的多态特性来处理,专门抽象了一个 Channel 接口,这个接口中提供的方法包含了一个请求处理的一些关键差异步骤,如何发送消息到客户端,如何停止连接,如何处理发送失败等等。对于 3 种 (ws/wss/xmd) 不同的发送通道,每个通道有自己的 Channel 实现。
客户端连接对象一创建,对应类型的具体 Channel 对象就立刻被实例化。这样状态机主逻辑中只需实现业务层的公共逻辑即可,当有差异逻辑调用时,直接调用 Channel 接口完成,这样一个简单的多态特性帮助我们分割了差异,确保代码整洁。
通过两个线程将 ZeroMQ 的读写操作异步化,同时负责若干私有指令的封装和解析。
后端做的最重要改造之一就是将所有与连接状态相关的信息进行剔除。
整个服务以 Request(一次连接上可以传输 N 个 Request)为核心进行各种转发和处理,每次请求与上一次请求没有任何关联。一个连接上的多次请求在后端模块被当作独立请求处理。
Scala 服务采用 Akka-Actor 架构实现了业务逻辑。
服务从 ZeroMQ 收到消息后,直接投递到 Dispatcher 中进行数据解析与请求处理,在 Dispatcher 中不同的请求会发送给对应的 RequestActor 进行 Event 协议解析并分发给该 event 对应的业务 Actor 进行处理。最后将处理后的请求数据通过 XmqActor 发送给后端 AIMS&XMQ 服务。
一个请求在后端多个 Actor 中的处理流程:
通过使用 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 命令:
TEXT_MESSAGE 与 BINARY_MESSAGE:
文本消息与二进制消息,在收到文本消息或二进制消息时将根据 requestid 发送给该请求对应的RequestActor进行处理。
收到的文本和二进制消息会根据 requestId 被 DispatcherActor 发送给相应的 RequestActor 进行处理。
其中:文本消息会被解析为 Event 请求,然后根据其中的 namespace 和 name 分发给指定的业务 Actor。而二进制消息则会根据当前请求的业务场景被分发给对应的业务 Actor。
在完成新架构 1.0 调整过程中,我们也在不断压测长连接容量,总结几点对容量影响较大的点。
在首次测试 20w 连接时,我们发现在前后端收发的消息中,用于保持用户在线状态的心跳 PING 消息占总消息量的 75%,收发这个消息消耗了大量 CPU。因此,我们延长心跳时间,也达到了降低 CPU 消耗的目的。
为提高与后端服务通信性能,我们使用自研的 TCP 通讯库,该库基于 Boost ASIO 开发,是一个纯异步的多线程 TCP 网络库,其优异性能帮助我们将连接数提升到 120w+。
经过新版架构1.0版的优化,验证了我们的拆分方向是正确的,因为预设的目标已经达到:
再重新审视下我们的理想目标,以这个为方向,我们就有了2.0版的雏形:
具体就是:
2.0版目标是:经过以上改造后,期望单前端模块可以达到200w+的连接处理能力。
架构之路,充满了坎坷
架构和高级开发不一样 , 架构问题是open/开放式的,架构问题是没有标准答案的
正由于这样,很多小伙伴,尽管耗费很多精力,耗费很多金钱,但是,遗憾的是,一生都没有完成架构升级。
所以,在架构升级/转型过程中,确实找不到有效的方案,可以来找40岁老架构尼恩求助.
前段时间一个小伙伴,他是跨专业来做Java,现在面临转架构的难题,但是经过尼恩几轮指导,顺利拿到了Java架构师+大数据架构师offer 。所以,如果遇到职业不顺,找老架构师帮忙一下,就顺利多了。
《阿里2面:你们部署多少节点?1000W并发,当如何部署?》
《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓