音乐播放器
Great Wei
 
文章 标签
16

Powered by Gridea | Theme: Fog

美图长连接平台 简介

美图长连接服务

随着科技的飞速发展,技术的日新月异,长连接的运用场景日益增多。不仅在后端服务中被广泛运用,比较常见的有数据库的访问、服务内部状态的协调等,而且在 App 端的消息推送、聊天信息、直播弹字幕等场景长连接服务也是优选方案。长连接服务的重要性也在各个场合被业界专家不断提及,与此同时也引起了更为广泛地关注和讨论,各大公司也开始构建自己的长连接服务。

美图公司于2016 年初开始构建长连接服务,与此同时, Go 在编程语言领域异军突起,考虑到其丰富的编程库,完善的工具链,简单高效的并发模型等优势,使我们最终选择 Go 去作为实现长连接服务的语言。在通信协议的选择上,考虑到 MQTT 协议的轻量、简单、易于实现的优点,选择了 MQTT 协议作为数据交互的载体。其整体的架构会在下文中做相应地介绍。

美图长连接服务(项目内部代号为bifrost )已经历时三年,在这三年的时间里,长连接服务经过了业务的检验,同时也经历了服务的重构,存储的升级等,长连接服务从之前支持单机二十几万连接到目前可以支撑单机百万连接。在大多数长连接服务中存在一个共性问题,那就是内存占用过高,我们经常发现单个节点几十万的长连接,内存却占用十几G 甚至更多,有哪些手段能降低内存呢?

本文将从多个角度介绍长连接服务在内存优化路上的探索,首先会先通过介绍当前服务的架构模型,Go 语言的内存管理,让大家清晰地了解我们内存优化的方向和关注的重要数据。后面会重点介绍我们在内存优化上做的一些尝试以及具体的优化手段,希望对大家有一定的借鉴意义。

架构模型


从架构图中我们可以清晰地看到由7 个模块组成,分别是:conf 、grpcsrv 、mqttsrv、session、pubsub、packet、util ,每个模块的作用如下:

conf :配置管理中心,负责服务配置的初始化,基本字段校验。

grpcsrv :grpc 服务,集群内部信息交互协调。

mqttsrv :mqtt 服务,接收客户端连接,同时支持单进程多端口 MQTT 服务。

session :会话模块,管理客户端状态变化,MQTT 信息的收发。

pubsub :发布订阅模块,按照 Topic 维度保存 session 并发布 Topic 通知给 session。

packet:协议解析模块,负责 MQTT 协议包解析。

util :工具包,目前集成监控、日志、grpc 客户端、调度上报四个子模块。

Go的内存管理

...

业务优化

session模块主要用户处理消息的收发,在实现时考虑到在通常场景中业务的消息生产大于客户端的消费速度的情况,为了缓解这种状况,设计时引入消息的缓冲队列,这种做法同样也有助于做客户端消息的流控。

缓冲消息队列借助chan 实现 ,chan 大小根据经验将初始化默认配置为 128 。但在目前线上推送的场景中,我们发现,消息的生产一般小于消费的速度,128 缓冲大小明显偏大,因此我们把长度调整为 16 ,减少内存的分配。

在设计中按照topic 对客户端进行分组管理的算法中,采用空间换时间的方式,组合 map 和 list 两种数据结构对于客户端集合操作提供O(1)的删除、O(1)的添加、O(n)的遍历。数据的删除采用标记删除方式,使用辅助 slice 结构进行记录,只有到达预设阈值才会进行真正的删除。虽然标记删除提高了遍历和添加的性能,但也同样带来了内存损耗问题。

大家一定好奇什么样的场景需要提供这样的复杂度,在实际中其场景有以下两种情况:

在实际的网络场景中,客户端随时都可能由于网络的不稳定断开或者重新建联,因此集合的增加和删除需要在常数范围内。

在消息发布的流程中,采用遍历集合逐一发布通知方式,但随着单个topic 上的用户量的增加,经常会出现单个 topic 用户集合消息过热的问题,耗时太久导致消息挤压,因此针对集合的遍历当然也要求尽量快。

通过benchamrk 数据分析,在标记回收 slice 长度在 1000 时,可以提供最佳的性能,因此默认配置阈值为 1000。在线上服务中,无特殊情况都是采用默认配置。但在当前推送服务的使用中,发现标记删除和延迟回收机制好处甚微,主要是因为 topic 和客户端为 1 : 1 方式,也就是不存在客户端集合,因此调整回收阈值大小为 2,减少无效内存占用。

上述所有优化,只要简单调整配置后服务灰度上线即可,在设计实现时通过conf 模块动态配置,降低了服务的开发和维护成本。通过监控对比优化效果如下表,在优化后在线连接数比优化的在线连接更多的情况下, heap 使用内存使用数量由原来的 4.16G 下降到了 3.5G ,降低了约 0.66 G。

golang 代码优化

在实现上面展示的架构的时候发现在session 模块 和 mqttsrv 模块之间存在很多共享变量,目前实现方式都是采用指针或者值拷贝的,由于 session的数量和客户端数据量成正比也就导致消耗大量内存用于共享数据,这不仅仅增加 GC 压力,同样对于内存的消耗也是巨大的。就此问题思考再三,参考系统的库 context 的设计在架构中也抽象 context 包负责模块之间交互信息传递,统一分配内存。此外还参考他人减少临时变量的分配的优化方式,提高系统运行效率。主要优化角度参考如下:

在频繁申请内存的地方,使用pool 方式进行内存管理

小对象合并成结构体一次分配,减少内存分配次数

缓存区内容一次分配足够大小空间,并适当复用

slice 和 map 采 make 创建时,预估大小指定容量

调用栈避免申请较多的临时对象

减少[]byte 与 string 之间转换,尽量采用 []byte 来字符串处理

目前系统具被完备的单元测试、集成测试,因此经过一周的快速的开发重构后灰度上线监控数据对比如下表:在基本相同的连接数上,heap 使用内存约占用降低 0.27G,stack 申请内存占用降低 3.81G。为什么 stack 会大幅度降低呢?

通过设置stackDebug 重新编译程序追查程序运行过程,优化前 goroutine 栈的大多数在内存为 16K,通过减少临时变量的分配,拆分大函数处理逻辑,有效的减少触发栈的内存扩容(详细分析见参考文章),优化后 goroutine 栈内存降低到 8 K。一个连接需要启动两个 goroutine 负责数据的读和写,粗略计算一个连接减少约 16 K 的内存,23 w 连接约降低 3.68 G 内存。

网络模型优化

在Go 语言的网络编程中经典的实现都是采用同步处理方式,启动两个 goroutine 分别处理读和写请求,goroutine 也不像 thread ,它是轻量级的。但对于一百万连接的情况,这种设计模式至少要启动两百万的 goroutine,其中一个 goroutine 使用栈的大小在 2 KB 到 8KB, 对于资源的消耗也是极大的。在大多数场景中,只有少数连接是有数据处理,大部分 goroutine 阻塞 IO 处理中。在因此可以借鉴 C 语言的设计,在程序中使用 epoll 模型做事件分发,只有活跃连接才会启动 goroutine 处理业务,基于这种思想修改网络处理流程。

网络模型修改测试完成后开始灰度上线,通过监控数据对比如下表:在优化后比优化前的连接数多10 K的情况下,heap 使用内存降低 0.33 G,stack 申请内存降低 2.34 G,优化效果显著。


请到客户端“主题--自定义配置--配置”中填入leancloud_appID和key