当前位置: 石墨 >> 石墨发展 >> 石墨文档是如何通过WebSocket
大家好,我是ConardLi。
Web服务端推送技术经过了长轮询、短轮询的发展,最终到HTML5标准带来的WebSocket规范逐步成为了目前业内主流技术方案。它使得消息推送、消息通知等功能的实现变得异常简单,那么百万级别连接下的Websocket网关该如何实践呢?本文整理自石墨文档资深工程师杜旻翔根据石墨Websocket重构的实践经验。
「作者:杜旻翔」infoq.cn/article/GymHAbqVROqo44jHD1引言在石墨文档的业务中,如文档分享、评论、幻灯片演示和文档表格跟随等场景,涉及多客户端数据同步和服务端批量数据推送的需求,采用短轮询或长轮询的方式无法很好的满足服务端消息推送、消息通知的业务场景,因此选择业内的主流方案:基于HTML5标准定义的WebSocket规范。
随着石墨文档的发展,现在日连接峰值达到了百万量级,日益增长的用户连接数和停止更新的架构设计导致了内存和CPU使用量急剧增长,因此我们考虑对网关进行重构,以适应发展需求。
2网关1.0网关1.0是使用Node.js基于Socket.IO进行修改开发的版本,很好的满足了当时用户量级下的业务场景需求。
2.1架构网关1.0版本架构设计图:
网关1.0客户端连接流程:
用户通过NGINX连接网关,该操作被业务服务感知;业务服务感知到用户连接后,会进行相关用户数据查询,再将消息Pub到Redis;网关服务通过RedisSub收到消息;查询网关集群中的用户会话数据,向客户端进行消息推送。2.2痛点虽然1.0版本的网关在线上运行良好,但是不能很好的支持后续业务的扩展,并且有以下几个问题需要解决:
资源消耗:Nginx仅使用证书,大部分请求被透传,产生了一定的资源浪费,同时之前的Node网关性能不好,消耗大量的CPU、内存。维护与观测:未接入石墨的监控体系,无法和现有监控告警联通,维护上存在一定的困难;业务耦合问题:业务服务与网关功能被集成到了同一个服务中,无法针对业务部分性能损耗进行针对性水平扩容,为了解决性能问题,以及后续的模块扩展能力,都需要进行服务解耦。3网关2.0网关2.0需要解决很多问题:石墨文档内部有很多组件:文档、表格、幻灯片和表单等等。在1.0版本中组件对网关的业务调用可以通过:Redis、Kafka和HTTP接口,来源不可查,管控困难。此外,从性能优化的角度考虑也需要对原有服务进行解耦合,将1.0版本网关拆分为网关功能部分和业务处理部分,网关功能部分为WS-Gateway:集成用户鉴权、TLS证书验证和WebSocket连接管理等;业务处理部分为WS-API:组件服务直接与该服务进行gRPC通信。可针对具体的模块进行针对性扩容;服务重构加上Nginx移除,整体硬件消耗显著降低;服务整合到石墨监控体系。
3.1整体架构网关2.0版本架构设计图:
网关2.0客户端连接流程:
客户端与WS-Gateway服务通过握手流程建立WebSocket连接;连接建立成功后,WS-Gateway服务将会话进行节点存储,将连接信息映射关系缓存到Redis中,并通过Kafka向WS-API推送客户端上线消息;WS-API通过Kafka接收客户端上线消息及客户端上行消息;WS-API服务预处理及组装消息,包括从Redis获取消息推送的必要数据,并进行完成消息推送的过滤逻辑,然后Pub消息到Kafka;WS-Gateway通过SubKafka来获取服务端需要返回的消息,逐个推送消息至客户端。3.2握手流程网络状态良好的情况下,完成如下图所示步骤1到步骤6之后,直接进入WebSocket流程;网络环境较差的情况下,WebSocket的通信模式会退化成HTTP方式,客户端通过POST方式推送消息到服务端,再通过GET长轮询的方式从读取服务端返回数据。客户端初次请求服务端连接建立的握手流程:
Client发送GET请求尝试建立连接;Server返回相关连接数据,sid为本次连接产生的唯一SocketID,后续交互作为凭证;{"sid":"xxx","upgrades":["websocket"],"pingInterval":xxx,"pingTimeout":xxx}
Client携带步骤2中的sid参数再次请求;Server返回40,表示请求接收成功;Client发送POST请求确认后期降级通路情况;Server返回ok,此时第一阶段握手流程完成;尝试发起WebSocket连接,首先进行2probe和3probe的请求响应,确认通信通道畅通后,即可进行正常的WebSocket通信。3.3TLS内存消耗优化客户端与服务端连接建立采用的wss协议,在1.0版本中TLS证书挂载在Nginx上,HTTPS握手过程由Nginx完成,为了降低Nginx的机器成本,在2.0版本中我们将证书挂载到服务上,通过分析服务内存,如下图所示,TLS握手过程中消耗的内存占了总内存消耗的大概30%左右。
这个部分的内存消耗无法避免,我们有两个选择:
采用七层负载均衡,在七层负载上进行TLS证书挂载,将TLS握手过程移交给性能更好的工具完成;优化Go对TLS握手过程性能,在与业内大佬曹春晖(曹大)的交流中了解到,他最近在Go官方库提交的PR