共计 9550 个字符,预计需要花费 24 分钟才能阅读完成。
本文由石墨文档技术杜旻翔分享,原题“石墨文档 Websocket 百万长连贯技术实际”,有订正。
1、引言
在石墨文档的局部业务中,例如文档分享、评论、幻灯片演示和文档表格追随等场景,波及到多客户端数据实时同步和服务端批量数据在线推送的需要,个别的 HTTP 协定无奈满足服务端被动 Push 数据的场景,因而抉择采纳 WebSocket 计划进行业务开发。
随着石墨文档业务倒退,目前日连贯峰值已达百万量级,日益增长的用户连接数和不合乎目前量级的架构设计导致了内存和 CPU 使用量急剧增长,因而咱们思考对长连贯网关进行重构。
本文分享了石墨文档长连贯网关从 1.0 架构演进到 2.0 的过程,并总结了整个性能优化的实际过程。
学习交换:
- 即时通讯 / 推送技术开发交换 5 群:215477170 [举荐]
- 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》
- 开源 IM 框架源码:https://github.com/JackJiang2…
(本文同步公布于:http://www.52im.net/thread-37…)
2、专题目录
本文是系列文章的第 6 篇,总目录如下:
《长连贯网关技术专题(一):京东京麦的生产级 TCP 网关技术实际总结》
《长连贯网关技术专题(二):知乎千万级并发的高性能长连贯网关技术实际》
《长连贯网关技术专题(三):手淘亿级挪动端接入层网关的技术演进之路》
《长连贯网关技术专题(四):爱奇艺 WebSocket 实时推送网关技术实际》
《长连贯网关技术专题(五):喜马拉雅自研亿级 API 网关技术实际》
《长连贯网关技术专题(六):石墨文档单机 50 万 WebSocket 长连贯架构实际》(* 本文)
3、v1.0 架构面临的问题
这套长连贯网关零碎的 v1.0 版是应用 Node.js 基于 Socket.IO 进行批改开发的版本,很好的满足了过后用户量级下的业务场景需要。
3.1 架构介绍
1.0 版架构设计图:
1.0 版客户端连贯流程:
1)用户通过 NGINX 连贯网关,该操作被业务服务感知;
2)业务服务感知到用户连贯后,会进行相干用户数据查问,再将音讯 Pub 到 Redis;
3)网关服务通过 Redis Sub 收到音讯;
4)查问网关集群中的用户会话数据,向客户端进行音讯推送。
3.2 面临的问题
尽管 1.0 版本的长连贯网关在线上运行良好,然而不能很好的反对后续业务的扩大。
并且有以下几个问题须要解决:
1)资源耗费:Nginx 仅应用 TLS 解密,申请透传,产生了大量的资源节约,同时之前的 Node 网关性能不好,耗费大量的 CPU、内存;
2)保护与观测:未接入石墨的监控体系,无奈和现有监控告警联通,保护上存在肯定的艰难;
3)业务耦合问题:业务服务与网关性能被集成到了同一个服务中,无奈针对业务局部性能损耗进行针对性程度扩容,为了解决性能问题,以及后续的模块扩大能力,都须要进行服务解耦。
4、v2.0 架构演进实际
4.1 概述
长连贯网关零碎的 v2.0 版须要解决很多问题。
比方,石墨文档外部有很多组件(文档、表格、幻灯片和表单等等),在 1.0 版本中组件对网关的业务调用能够通过 Redis、Kafka 和 HTTP 接口,起源不可查,管控艰难。
此外,从性能优化的角度思考也须要对原有服务进行解耦合,将 1.0 版本网关拆分为网关性能局部和业务解决局部。
具体是:
1)网关性能局部为 WS-Gateway:集成用户鉴权、TLS 证书验证和 WebSocket 连贯治理等;
2)业务解决局部为 WS-API:组件服务间接与该服务进行 gRPC 通信。
另外还有:
1)可针对具体的模块进行针对性扩容;
2)服务重构加上 Nginx 移除,整体硬件耗费显著升高;
3)服务整合到石墨监控体系。
4.2 整体架构
2.0 版本架构设计图:
2.0 版本客户端连贯流程:
1)客户端与 WS-Gateway 服务通过握手流程建设 WebSocket 连贯;
2)连贯建设胜利后,WS-Gateway 服务将会话进行节点存储,将连贯信息映射关系缓存到 Redis 中,并通过 Kafka 向 WS-API 推送客户端上线音讯;
3)WS-API 通过 Kafka 接管客户端上线音讯及客户端上行音讯;
4)WS-API 服务预处理及组装音讯,包含从 Redis 获取音讯推送的必要数据,并进行实现音讯推送的过滤逻辑,而后 Pub 音讯到 Kafka;
5)WS-Gateway 通过 Sub Kafka 来获取服务端须要返回的音讯,一一推送音讯至客户端。
4.3 握手流程
网络状态良好的状况下,实现如下图所示步骤 1 到步骤 6 之后,间接进入 WebSocket 流程;网络环境较差的状况下,WebSocket 的通信模式会进化成 HTTP 形式,客户端通过 POST 形式推送音讯到服务端,再通过 GET 长轮询的形式从读取服务端返回数据。
客户端首次申请服务端连贯建设的握手流程:
流程阐明如下:
1)Client 发送 GET 申请尝试建设连贯;
2)Server 返回相干连贯数据,sid 为本次连贯产生的惟一 Socket ID,后续交互作为凭证:
{“sid”:”xxx”,”upgrades”:[“websocket”],”pingInterval”:xxx,”pingTimeout”:xxx}
3)Client 携带步骤 2 中的 sid 参数再次申请;
4)Server 返回 40,示意申请接管胜利;
5)Client 发送 POST 申请确认前期降级通路状况;
6)Server 返回 ok,此时第一阶段握手流程实现;
7)尝试发动 WebSocket 连贯,首先进行 2probe 和 3probe 的申请响应,确认通信通道畅通后,即可进行失常的 WebSocket 通信。
4.4 TLS 内存耗费优化
客户端与服务端连贯建设采纳的 wss 协定,在 1.0 版本中 TLS 证书挂载在 Nginx 上,HTTPS 握手过程由 Nginx 实现。为了升高 Nginx 的机器老本,在 2.0 版本中咱们将证书挂载到服务上。
通过剖析服务内存,如下图所示,TLS 握手过程中耗费的内存占了总内存耗费的大略 30% 左右。
这个局部的内存耗费无奈防止,咱们有两个抉择:
1)采纳七层负载平衡,在七层负载上进行 TLS 证书挂载,将 TLS 握手过程移交给性能更好的工具实现;
2)优化 Go 对 TLS 握手过程性能,在与业内大佬曹春晖(曹大)的交换中理解到,他最近在 Go 官网库提交的 PR,以及相干的性能测试数据。
4.5 Socket ID 设计
对每次连贯必须产生一个惟一码,如果呈现反复会导致串号,音讯凌乱推送的问题。抉择 SnowFlake 算法作为惟一码生成算法。
物理机场景中,对正本所在物理机进行固定编号,即可保障每个正本上的服务产生的 Socket ID 是惟一值。
K8S 场景中,这种计划不可行,于是采纳注册下发的形式返回编号,WS-Gateway 所有正本启动后向数据库写入服务的启动信息,获取正本编号,以此作为参数作为 SnowFlake 算法的正本编号进行 Socket ID 生产,服务重启会继承之前已有的正本编号,有新版本下发时会依据自增 ID 下发新的正本编号。
于此同时,Ws-Gateway 正本会向数据库写入心跳信息,以此作为网关服务自身的健康检查根据。
4.6 集群会话治理计划:事件播送
客户端实现握手流程后,会话数据在以后网关节点内存存储,局部可序列化数据存储到 Redis,存储构造阐明如下图所示。
由客户端触发或组件服务触发的音讯推送,通过 Redis 存储的数据结构,在 WS-API 服务查问到返回音讯体的指标客户端的 Socket ID,再由 WS-Gateway 服务进行集群生产。如果 Socket ID 不在以后节点,则须要进行节点与会话关系的查问,找到客端户 Socket ID 理论对应的 WS-Gateway 节点,通常有以下两种计划(如下图所示)。
在确定应用事件播送形式进行网关节点间的消息传递后,进一步抉择应用哪种具体的消息中间件,列举了三种待选的计划(如下图所示)。
于是对 Redis 和其余 MQ 中间件进行 100w 次的入队和出队操作,在测试过程中发现在数据小于 10K 时 Redis 性能体现非常优良。
进一步结合实际状况:播送内容的数据量大小在 1K 左右,业务场景简略固定,并且要兼容历史业务逻辑,最初抉择了 Redis 进行音讯播送。
后续还能够将 WS-API 与 WS-Gateway 两两互联,应用 gRPC stream 双向流通信节俭内网流量。
4.7 心跳机制
会话在节点内存与 Redis 中存储后,客户端须要通过心跳上报继续更新会话工夫戳,客户端依照服务端下发的周期进行心跳上报,上报工夫戳首先在内存进行更新,而后再通过另外的周期进行 Redis 同步,防止大量客户端同时进行心跳上报对 Redis 产生压力。
具体流程:
1)客户端建设 WebSocket 连贯胜利后,服务端下发心跳上报参数;
2)客户端根据以上参数进行心跳包传输,服务端收到心跳后会更新会话工夫戳;
3)客户端其余上行数据都会触发对应会话工夫戳更新;
4)服务端定时清理超时会话,执行被动敞开流程;
5)通过 Redis 更新的工夫戳数据进行 WebSocket 连贯、用户和文件之间的关系进行清理。
会话数据内存以及 Redis 缓存清理逻辑:
for{
select{
case<-t.C:
var now = time.Now().Unix()
var clients = make([]*Connection, 0)
dispatcher.clients.Range(func(_, v interface{}) bool{client := v.(*Connection)
lastTs := atomic.LoadInt64(&client.LastMessageTS)
if now-lastTs > int64(expireTime) {clients = append(clients, client)
} else{dispatcher.clearRedisMapping(client.Id, client.Uid, lastTs, clearTimeout)
}
return true
})
for_, cli := rangeclients {cli.WsClose()
}
}
}
在已有的两级缓存刷新机制上,进一步通过动静心跳上报频率的形式升高心跳上报产生的服务端性能压力,默认场景中客户端对服务端进行距离 1s 的心跳上报,假如目前单机承载了 50w 的连接数,以后的 QPS 为:QPS1 = 500000/1。
从服务端性能优化的角度思考,实现心跳失常状况下的动静距离,每 x 次失常心跳上报,心跳距离减少 a,减少下限为 y,动静 QPS 最小值为:QPS2=500000/y。
极限状况下,心跳产生的 QPS 升高 y 倍。在单次心跳超时后服务端立即将 a 值变为 1s 进行重试。采纳以上策略,在保障连贯品质的同时,升高心跳对服务端产生的性能损耗。
4.8 自定义 Headers
应用 Kafka 自定义 Headers 的目标是防止网关层呈现对音讯体解码而带来的性能损耗。
客户端 WebSocket 连贯建设胜利后,会进行一系列的业务操作,咱们抉择将 WS-Gateway 和 WS-API 之间的操作指令和必要的参数放到 Kafka 的 Headers 中,例如通过 X-XX-Operator 为播送,再读取 X-XX-Guid 文件编号,对该文件内的所有用户进行音讯推送。
在 Kafka Headers 中写入了 trace id 和 工夫戳,能够追中某条音讯的残缺生产链路以及各阶段的工夫耗费。
4.9 音讯接管与发送
type Packet struct{
…
}
type Connect struct{
*websocket.Con
send chanPacket
}
func NewConnect(conn net.Conn) *Connect {
c := &Connect{
send: make(chanPacket, N),
}
goc.reader()
goc.writer()
return c
}
客户端与服务端的音讯交互第一版的写法相似以上写法。
对 Demo 进行压测,发现每个 WebSocket 连贯都会占用 3 个 goroutine,每个 goroutine 都须要内存栈,单机承载连非常无限。
次要受制于大量的内存占用,而且大部分工夫 c.writer() 是闲置状态,于是思考,是否只启用 2 个 goroutine 来实现交互。
type Packet struct{
…
}
type Connect struct{
*websocket.Conn
mux sync.RWMutex
}
func NewConnect(conn net.Conn) *Connect {
c := &Connect{
send: make(chanPacket, N),
}
goc.reader()
return c
}
func(c *Connect) Write(data []byte) (err error) {
c.mux.Lock()
deferc.mux.Unlock()
…
return nil
}
保留 c.reader() 的 goroutine,如果应用轮询形式从缓冲区读取数据,可能会产生读取提早或者锁的问题,c.writer() 操作调整为被动调用,不采纳启动 goroutine 继续监听,升高内存耗费。
调研了 gev 和 gnet 等基于事件驱动的轻量级高性能网络库,实测发现在大量连贯场景下可能产生的音讯提早的问题,所以没有在生产环境下应用。
4.10 外围对象缓存
确定数据接管与发送逻辑后,网关局部的外围对象为 Connection 对象,围绕 Connection 进行了 run、read、write、close 等函数的开发。
应用 sync.pool 来缓存该对象,加重 GC 压力,创立连贯时,通过对象资源池获取 Connection 对象。
生命周期完结之后,重置 Connection 对象后 Put 回资源池。
在理论编码中,倡议封装 GetConn()、PutConn() 函数,收敛数据初始化、对象重置等操作。
var ConnectionPool = sync.Pool{
New: func() interface{} {
return &Connection{}
},
}
func GetConn() *Connection {
cli := ConnectionPool.Get().(*Connection)
return cli
}
func PutConn(cli *Connection) {
cli.Reset()
ConnectionPool.Put(cli) // 放回连接池
}
4.11 数据传输过程优化
音讯流转过程中,须要思考音讯体的传输效率优化,采纳 MessagePack 对音讯体进行序列化,压缩音讯体大小。调整 MTU 值避免出现分包状况,定义 a 为探测包大小,通过如下指令,对指标服务 ip 进行 MTU 极限值探测。
ping-s {a} {ip}
a = 1400 时,理论传输包大小为:1428。
其中 28 由 8(ICMP 回显申请和回显应答报文格式)和 20(IP 首部)形成。
如果 a 设置过大会导致应答超时,在理论环境包大小超过该值时会呈现分包的状况。
在调试适合的 MTU 值的同时通过 MessagePack 对音讯体进行序列号,进一步压缩数据包的大小,并减小 CPU 的耗费。
4.12 基础设施反对
应用 EGO 框架进行服务开发:业务日志打印,异步日志输入,动静日志级别调整等性能,不便线上问题排查晋升日志打印效率;微服务监控体系,CPU、P99、内存、goroutine 等监控。
客户端 Redis 监控:
客户端 Kafka 监控:
自定义监控大盘:
5、查看成绩的时刻:性能压测
5.1 压测筹备
筹备的测试平台有:
1)抉择一台配置为 4 核 8G 的虚拟机,作为服务机,指标承载 48w 连贯;
2)抉择八台配置为 4 核 8G 的虚拟机,作为客户机,每台客户机凋谢 6w 个端口。
5.2 模仿场景一
用户上线,50w 在线用户。
单个 WS-Gateway 每秒建设连接数峰值为:1.6w 个 /s,每个用户占用内存:47K。
5.3 模仿场景二
测试工夫 15 分钟,在线用户 50w,每 5s 推送一条所有用户,用户有回执。
推送内容为:
42[“message”,{“type”:”xx”,”data”:{“type”:”xx”,”clients”:[{“id”:xx,”name”:”xx”,”email”:”xx@xx.xx”,”avatar”:”ZgG5kEjCkT6mZla6.png”,”created_at”:1623811084000,”name_pinyin”:””,”team_id”:13,”team_role”:”member”,”merged_into”:0,”team_time”:1623811084000,”mobile”:”+xxxx”,”mobile_account”:””,”status”:1,”has_password”:true,”team”:null,”membership”:null,”is_seat”:true,”team_role_enum”:3,”register_time”:1623811084000,”alias”:””,”type”:”anoymous”}],”userCount”:1,”from”:”ws”}}]
测试通过 5 分钟后,服务异样重启,重启起因是内存使用量到超过限度。
剖析内存超过限度的起因:
新增的播送代码用掉了 9.32% 的内存:
接管用户回执音讯的局部耗费了 10.38% 的内存:
进行测试规定调整,测试工夫 15 分钟,在线用户 48w,每 5s 推送一条所有用户,用户有回执。
推送内容为:
42[“message”,{“type”:”xx”,”data”:{“type”:”xx”,”clients”:[{“id”:xx,”name”:”xx”,”email”:”xx@xx.xx”,”avatar”:”ZgG5kEjCkT6mZla6.png”,”created_at”:1623811084000,”name_pinyin”:””,”team_id”:13,”team_role”:”member”,”merged_into”:0,”team_time”:1623811084000,”mobile”:”+xxxx”,”mobile_account”:””,”status”:1,”has_password”:true,”team”:null,”membership”:null,”is_seat”:true,”team_role_enum”:3,”register_time”:1623811084000,”alias”:””,”type”:”anoymous”}],”userCount”:1,”from”:”ws”}}]
连接数建设峰值:1w 个 /s,接收数据峰值:9.6w 条 /s,发送数据峰值 9.6w 条 /s。
5.4 模仿场景三
测试工夫 15 分钟,在线用户 50w,每 5s 推送一条所有用户,用户无需回执。
推送内容为:
42[“message”,{“type”:”xx”,”data”:{“type”:”xx”,”clients”:[{“id”:xx,”name”:”xx”,”email”:”xx@xx.xx”,”avatar”:”ZgG5kEjCkT6mZla6.png”,”created_at”:1623811084000,”name_pinyin”:””,”team_id”:13,”team_role”:”member”,”merged_into”:0,”team_time”:1623811084000,”mobile”:”+xxxx”,”mobile_account”:””,”status”:1,”has_password”:true,”team”:null,”membership”:null,”is_seat”:true,”team_role_enum”:3,”register_time”:1623811084000,”alias”:””,”type”:”anoymous”}],”userCount”:1,”from”:”ws”}}]
连接数建设峰值:1.1w 个 /s,发送数据峰值 10w 条 /s,出内存占用过高之外,其余没有异常情况。
内存耗费极高,剖析火焰图,大部分耗费在定时 5s 进行播送的操作上。
5.5 模仿场景四
测试工夫 15 分钟,在线用户 50w,每 5s 推送一条所有用户,用户有回执。每秒 4w 用户高低线。
推送内容为:
42[“message”,{“type”:”xx”,”data”:{“type”:”xx”,”clients”:[{“id”:xx,”name”:”xx”,”email”:”xx@xx.xx”,”avatar”:”ZgG5kEjCkT6mZla6.png”,”created_at”:1623811084000,”name_pinyin”:””,”team_id”:13,”team_role”:”member”,”merged_into”:0,”team_time”:1623811084000,”mobile”:”+xxxx”,”mobile_account”:””,”status”:1,”has_password”:true,”team”:null,”membership”:null,”is_seat”:true,”team_role_enum”:3,”register_time”:1623811084000,”alias”:””,”type”:”anoymous”}],”userCount”:1,”from”:”ws”}}]
连接数建设峰值:18570 个 /s,接收数据峰值:329949 条 /s,发送数据峰值:393542 条 /s,未出现异常状况。
5.6 压测总结
在 16 核 32G 内存的硬件条件下:单机 50w 连接数,进行以上包含用户高低线、音讯回执等四个场景的压测,内存和 CPU 耗费都合乎预期,并且在较长时间的压测下,服务也很稳固。
测试的后果基本上是能满足目前量级下的资源节约要求的,咱们认为齐全能够在此基础上持续欠缺性能开发。
6、本文小结
面临日益减少的用户量,网关服务的重构是势在必行。
本次重构次要是:
1)对网关服务与业务服务的解耦,移除对 Nginx 的依赖,让整体架构更加清晰;
2)从用户建设连贯到底层业务推送音讯的整体流程剖析,对其中这些流程进行了具体的优化。
2.0 版本的长连贯网关有了更少的资源耗费,更低的单位用户内存损耗、更加欠缺的监控报警体系,让网关服务自身更加牢靠。
以上优化内容次要是以下各个方面:
1)可降级的握手流程;
2)Socket ID 生产;
3)客户端心跳处理过程的优化;
4)自定义 Headers 防止了音讯解码,强化了链路追踪与监控;
5)音讯的接管与发送代码结构设计上的优化;
6)对象资源池的应用,应用缓存升高 GC 频率;
7)音讯体的序列化压缩;
8)接入服务观测基础设施,保障服务稳定性。
在保障网关服务性能过关的同时,更进一步的是收敛底层组件服务对网关业务调用的形式,从以前的 HTTP、Redis、Kafka 等形式,对立为 gRPC 调用,保障了起源可查可控,为后续业务接入打下了更好的根底。
7、相干文章
[1] WebSocket 从入门到精通,半小时就够!
[2] 搞懂古代 Web 端即时通讯技术一文就够:WebSocket、socket.io、SSE
[3] 从游击队到正规军(三):基于 Go 的马蜂窝旅游网分布式 IM 零碎技术实际
[4] 12306 抢票带来的启发:看我如何用 Go 实现百万 QPS 的秒杀零碎(含源码)
[5] Go 语言构建千万级在线的高并发音讯推送零碎实际(来自 360 公司)
[6] 跟着源码学 IM(六):手把手教你用 Go 疾速搭建高性能、可扩大的 IM 零碎
本文已同步公布于“即时通讯技术圈”公众号。
同步公布链接是:http://www.52im.net/thread-37…