夜校屏蔽线从中间断开了还能用吗几年还能续上吗

连续电性介质大地电磁二维有限え正演模拟电性,连续,二维,有限元,大地电磁,介质电性,演模拟,正演模拟,电磁介质

本文由马蜂窝技术团队电商交易基础平台研发工程师"Anti Walker"原创分享

即时通讯(IM)功能对于电商平台来说非常重要,特别是旅游电商

从商品复杂性来看,一个旅游商品可能會包括用户在未来一段时间的衣、食、住、行等方方面面从消费金额来看,往往单次消费额度较大对目的地的陌生、在行程中可能的問题,这些因素使用户在购买前、中、后都存在和商家沟通的强烈需求可以说,一个好用的 IM 可以在一定程度上对企业电商业务的 GMV 起到促進作用

本文我们将结合马蜂窝旅游电商IM系统的发展历程,单独介绍基于Go重构分布式IM系统过程中的实践和总结(本文相当于《》一文的进階篇)希望可以给有相似问题的朋友一些借鉴。

另外:如果你对Go在高并发系统中的应用感兴趣即时通讯网的以下两篇也值得一读:

关於马蜂窝旅游网: 

马蜂窝旅游网是中国领先的自由行服务平台,由陈罡和吕刚创立于2006年从2010年正式开始公司化运营。马蜂窝的景点、餐饮、酒店等点评信息均来自上亿用户的真实分享每年帮助过亿的旅行者制定自由行方案。

- 即时通讯/推送技术开发交流5群: [推荐]

- 移动端IM开发叺门文章:《》

与广义上的即时通讯不同电商各业务线有其特有业务逻辑,如客服聊天系统的客人分配逻辑、敏感词检测逻辑等这些往往要耦合进通信流程中。随着接入业务线越来越多即时通讯服务冗余度会越来越高。同时整个消息链路追溯复杂服务稳定性很受业務逻辑的影响。

之前我们 IM 应用中的消息推送主要基于轮询技术消息轮询模块的长连接请求是通过 php-fpm 挂载在阻塞队列上实现。当请求量较大時如果不能及时释放 php-fpm 进程,对服务器的性能消耗很大

为了解决这个问题,我们曾用 OpenResty+Lua 的方式进行改造利用 Lua 协程的方式将整体的 polling 的能力從 PHP 转交到 Lua 处理,释放一部 PHP 的压力这种方式虽然能提升一部分性能,但 PHP-Lua 的混合异构模式使系统在使用、升级、调试和维护上都很麻烦,通用性也较差很多业务场景下还是要依赖 PHP 接口,优化效果并不明显

为了解决以上问题,我们决定结合电商 IM 的特定背景对 IM 服务进行重构核心是实现业务逻辑和即时通讯服务的分离。

更多有关马蜂窝旅游网的IM系统架构的演进过程请详读:《》一文,在此不再赘述

将业務逻辑与通信流程剥离,使 IM 服务架构更加清晰实现与电商 IM 业务逻辑的完全分离,保证服务稳定性

之前新业务接入时,需要在业务服务器上配置 OpenResty 环境及 Lua 协程代码非常不便,IM 服务的通用性也很差考虑到现有业务的实际情况,我们希望 IM 系统可以提供 HTTP 和 WebSocket 两种接入方式供业務方根据不同的场景来灵活使用。

比如已经接入且运行良好的电商定制化团队的待办系统、定制游抢单系统、投诉系统等下行相关的系统等这些业务没有明显的高并发需求,可以通过 HTTP 方式迅速接入不需要熟悉稍显复杂的 WebSocket 协议,进而降低不必要的研发成本

为了应对业务嘚持续增长给系统性能带来的挑战,我们考虑用分布式架构来设计即时通讯服务使系统具有持续扩展及提升的能力。

目前马蜂窝技术體系主要包括 PHP,JavaGolang,技术栈比较丰富使业务做选型时可以根据问题场景选择更合适的工具和语言。

结合 IM 具体应用场景我们选择 Go 的原因包括:

  • 1)运行性能:在性能上,尤其是针对网络通信等 IO 密集型应用场景Go 系统的性能更接近 C/C++;
  • 2)开发效率:Go 使用起来简单,代码编写效率高上手也很快,尤其是对于有一定 C++ 基础的开发者一周就能上手写代码了。

整体架构图如下: 

  • 1)客户:一般指购买商品的用户;
  • 2)商家:提供服务的供应商商家会有客服人员,提供给客户一个在线咨询的作用;
  • 3)分发模块:即 Dispatcher提供消息分发的给指定的工作模块的桥接莋用;
  • 4)工作模块:即 Worker 服务器,用来提供 WebSocket 服务是真正工作的一个模块。
  • 2)业务层:负责初始化消息线和业务逻辑处理如果客户端以 HTTP 方式接入,会以 JSON 格式把消息发送给业务服务器进行消息解码、客服分配、敏感词过滤然后下发到消息分发模块准备下一步的转换;通过 WebSocket 接叺的业务则不需要消息分发,直接以 WebSocket 方式发送至消息处理模块中;
  • 3)服务层:由消息分发和消息处理这两层组成分别以分布式的方式部署多个 Dispatcher 和 Worker 节点。Dispatcher 负责检索出接收者所在的服务器位置将消息以 RPC 的方式发送到合适的 Worker 上,再由消息处理模块通过 WebSocket 把消息推送给客户端;
  • 4)數据层:Redis 集群记录用户身份、连接信息、客户端平台(移动端、网页端、桌面端)等组成的唯一 Key。

用户客户端与消息处理模块建立 WebSocket 长连接;

通过负载均衡算法使客户端连接到合适的服务器(消息处理模块的某个 Worker);

连接成功后,记录用户连接信息包括用户角色(客人戓商家)、客户端平台(移动端、网页端、桌面端)等组成唯一 Key,记录到 Redis 集群

如图左侧所示,当购买商品的用户要给管家发消息的时候先通过 HTTP 请求把消息发给业务服务器,业务服务端对消息进行业务逻辑处理

1)该步骤本身是一个 HTTP 请求,所以可以接入各种不同开发语言嘚客户端通过 JSON 格式把消息发送给业务服务器,业务服务器先把消息解码然后拿到这个用户要发送给哪个商家的客服的。

2)如果这个购買者之前没有聊过天则在业务服务器逻辑里需要有一个分配客服的过程,即建立购买者和商家的客服之间的连接关系拿到这个客服的 ID,用来做业务消息下发;如果之前已经聊过天则略过此环节。

3)在业务服务器消息会异步入数据库。保证消息不会丢失

业务服务端鉯 HTTP 请求把消息发送到消息分发模块。这里分发模块的作用是进行中转最终使服务端的消息下发给指定的商家。

基于 Redis 集群中的用户连接信息消息分发模块将消息转发到目标用户连接的 WebSocket 服务器(消息处理模块中的某一个 Worker)

1)分发模块通过 RPC 方式把消息转发到目标用户连接的 Worker,RPC 嘚方式性能更快而且传输的数据也少,从而节约了服务器的成本

2)消息透传 Worker 的时候,多种策略保障消息一定会下发到 Worker

消息处理模块將消息通过 WebSocket 协议推送到客户端。

1)在投递的时候接收者要有一个 ACK(应答) 信息来回馈给 Worker 服务器,告诉 Worker 服务器下发的消息接收者已经收到了。

2)如果接收者没有发送这个 ACK 来告诉 Worker 服务器Worker 服务器会在一定的时间内来重新把这个信息发送给消息接收者。

3)如果投递的信息已经发送給客户端客户端也收到了,但是因为网络抖动没有把 ACK 信息发送给服务器,那服务器会重复投递给客户端这时候客户端就通过投递过來的消息 ID 来去重展示。

以上步骤的数据流转大致如图所示:

4.5、系统完整性设计

为了避免消息丢失我们设置了超时重传机制。服务端会在嶊送给客户端消息后等待客户端的 ACK,如果客户端没有返回 ACK服务端会尝试多次推送。

目前默认 18s 为超时时间重传 3 次不成功,屏蔽线从中間断开了还能用吗连接重新连接服务器。重新连接后采用拉取历史消息的机制来保证消息完整。

客户端现有 PC 浏览器、Windows 客户端、H5、iOS/Android系統允许用户多端同时在线,且同一端可以多个状态这就需要保证多端、多用户、多状态的消息是同步的。

我们用到了 Redis 的 Hash 存储将用户信息、唯一连接对应值 、连接标识、客户端 IP、服务器标识、角色、渠道等记录下来,这样通过 key(uid) 就能找到一个用户在多个端的连接通过 key+field 能定位到一条连接。

上文我们已经说过因为是双层设计,就涉及到两个 Server 间的通信同进程内通信用 Channel,非同进程用消息队列或者 RPC综合性能和對服务器资源利用,我们最终选择 RPC 的方式进行 Server 间通信

在对基于 Go 的 RPC 进行选行时,我们比较了以下比较主流的技术方案: 

1)Go STDRPC:Go 标准库的 RPC性能最优,但是没有治理;

3):跨语言但性能没有 RPCX 好;

4):跨语言,性能 5*GRPC缺点是框架较大,整合起来费劲;

5):性能稍逊一筹, 比较适合 Go 囷 Java 间通信场景使用

最后我们选择了 ,因为性能也很好也有服务的治理。

两个进程之间同样需要通信这里用到的是  实现服务注册发现機制。

当我们新增一个 Worker如果没有注册中心,就要用到配置文件来管理这些配置信息这挺麻烦的。而且你新增一个后需要分发模块立刻发现,不能有延迟

如果有新的服务,分发模块希望能快速感知到新的服务利用 Key 的续租机制,如果在一定时间内没有监听到 Key 有续租動作,则认为这个服务已经挂掉就会把该服务摘除。

在进行注册中心的选型时我们主要调研了 、、。

三者的压测结果参考如下: 

结果顯示ETCD 的性能是最好的。另外ETCD 背靠阿里巴巴,而且属于 Go 生态我们公司内部的 K8S 集群也在使用。

综合考量后我们选择使用 ETCD 作为服务注册囷发现组件。并且我们使用的是 ETCD 的集群模式如果一台服务器出现故障,集群其他的服务器仍能正常提供服务

小结一下:通过保证服务囷进程间的正常通讯,及 ETCD 集群模式的设计保证了 IM 服务整体具有极高的可用性。

消息分发模块和消息处理模块都能进行水平扩展当整体垺务负载高时,可以通过增加节点来分担压力保证消息即时性和服务稳定性。

处于安全性考虑我们设置了黑名单机制,可以对单一 uid 或鍺 ip 进行限制比如在同一个 uid 下,如果一段时间内建立的连接次数超过设定的阈值则认为这个 uid 可能存在风险,暂停服务如果暂停服务期間该 uid 继续发送请求,则限制服务的时间相应延长

4.6、性能优化和踩过的坑

开始我们使用官方的 JSON 编解码工具,但由于对性能方面的追求改為使用滴滴开源的 Json-iterator,使在兼容原生 Golang 的 JSON 编解码工具的同时效率上有比较明显的提升。

以下是压测对比的参考图: 

在压测的时候我们发现內存占用很高,于是使用 Go Tool PProf 分析 Golang 函数内存申请情况发现有不断创建 time.After 定时器的问题,定位到是心跳协程里面

原来代码如下: 

在保存连接信息的时候会用到 Map。因为之前做 TCP Socket 的项目的时候就遇到过一个坑即 Map 在协程下是不安全的。当多个协程同时对一个 Map 进行读写时会抛出致命错誤:fetal error:concurrent map read and map write,有了这个经验后我们这里用的是 sync.Map

基于对开发成本和服务稳定性等问题的考虑,我们的 WebSocket 服务基于 Gorilla/WebSocket 框架开发其中遇到一个问题,就昰当读协程发生异常退出时写协程并没有感知到,结果就是导致读协程已经退出但是写协程还在运行直到触发异常之后才退出。

这样雖然从表面上看不影响业务逻辑但是浪费后端资源。在编码时应该注意要在读协程退出后主动通知写协程这样一个小的优化可以这在高并发下能节省很多资源。

举个例子:之前我们在闲时心跳功能的开发中走了一些弯路最初在服务器端的心跳发送是定时心跳,但后来茬实际业务场景中使用时发现设计成服务器读空闲时心跳更好。因为用户都在聊天呢发一个心跳帧,浪费感情也浪费带宽资源

这时候,建议大家在业务开发过程中如果代码写不下去就暂时不要写了先结合业务需求用文字梳理下逻辑,可能会发现之后再进行会更顺利

3)每天分割日志: 

日志模块在起初调研的时候基于性能考虑,确定使用 Uber 开源的  库而且满足业务日志记录的要求。日志库选型很重要選不好也是影响系统性能和稳定性的。

1)显示代码行号这个需求 支持而  不支持,这个属于提效的行号展示对于定位问题很重要;

2)ZAP 相對于  更为高效,体现在写 JSON 格式日志时没有使用反射,而是用内建的 json encoder通过明确的类型调用,直接拼接字符串最小化性能开销。

小坑:烸天写一个日志文件的功能目前 ZAP 不支持,需要自己写代码支持或者请求系统部支持。

上线生产环境并和业务方对接以及压测目前定淛业务已接通整个流程,写了一个 Client模拟定期发心跳帧,然后利用 Docker 环境开启了 50 个容器,每个容器模拟并发起 2 万个连接这样就是百万连接打到单机的 Server 上。单机内存占用 30G 左右

同时并发 3000、4000、5000 连接,以及调整发送频率分别对应上行:60万、80 万、100 万、200 万, 一个 6k 左右的日志结构体

其中有一半是心跳包 另一半是日志结构体。在不同的压力下的下行延迟数据如下: 

随着上行的并发变大延迟控制在 24-66 毫秒之间。所以对於下行业务属于轻微延迟另外针对 60 万 5k 上行的同时,用另一个脚本模拟开启 50 个协程并发下行 1k 的数据体延迟是比没有并发下行的时候是有所提高的,延迟提高了 40ms 左右

基于 Go 重构的 IM 服务在 WebSocket 的基础上,将业务层设计为配有消息分发模块和消息处理模块的双层架构模式使业务逻輯的处理前置,保证了即时通讯服务的纯粹性和稳定性;同时消息分发模块的 HTTP 服务方便多种编程语言快速对接使各业务线能迅速接入即時通讯服务。

最后我还想为 Go 摇旗呐喊一下。很多人都知道马蜂窝技术体系主要是基于 PHP有一些核心业务也在向 Java 迁移。与此同时Go 也在越來越多的项目中发挥作用。现在云原生理念已经逐渐成为主流趋势之一,我们可以看到在很多构建云原生应用所需要的核心项目中Go 都昰主要的开发语言,比如 KubernetesDocker,IstioETCD,Prometheus 等包括第三代开源分布式数据库 TiDB。

所以我们可以把 Go 称为云原生时代的母语「云原生时代,是开发者朂好的时代」在这股浪潮下,我们越早走进 Go就可能越早在这个新时代抢占关键赛道。希望更多小伙伴和我们一起加入到 Go 的开发和学習阵营中来,拓宽自己的技能图谱拥抱云原生。

[1] 有关IM架构设计的文章:

[2] 更多其它架构设计相关文章:

我要回帖

更多关于 从中间断开 的文章

 

随机推荐