马蜂窝 IM 系统架构的演化和升级

今天,愈来愈多的用户被马蜂窝持续积累的笔记、攻略、嗡嗡等优质的分享内容所吸引,在这里激发了去旅行的热情,同时也拉动了马蜂窝交易的增加。在帮助用户作出旅行决策、完成交易的过程当中,IM 系统起到了重要的做用。php

IM 系统为用户与商家创建了直接沟通的渠道,帮助用户解答购买旅行产品中的问题,既促成了订单交易,也帮用户打消了疑虑,促成用户旅行愿望的实现。伴随着业务的快速发展,几年间,马蜂窝 IM 系统也经历了几回比较重要的架构演化和转型。浏览器

IM 1.0 —— 初期阶段

初期为了支持业务快速上线,且当时版本流量较低,对并发要求不高,IM 系统的技术架构主要以简单和可用为目的,实现的功能也很基础。服务器

IM 1.0 使用 PHP 开发,实现了 IM 基本的用户/客服接入、消息收发、咨询列表管理功能。用户咨询时,会经过平均分配的策略分配给客服,记录用户和客服的关联关系。用户/客服发送消息时,经过调用消息转发模块,将消息投递到对方的 Redis 阻塞队列里。收消息则经过 HTTP 长链接调用消息轮询模块,有消息时即刻返回,没有消息则阻塞一段时间返回,这里阻塞的目的是下降轮询的间隔。消息收发模型以下图所示:网络

消息轮询模块优化

上图模型中消息轮询模块的长链接请求是经过 php-fpm 挂载在阻塞队列上,当该请求变多时,若是不能及时释放 php-fpm 进程,会对服务器性能消耗较大,负载很高。架构

为了解决这个问题,咱们对消息轮询模块进行了优化,选用基于 OpenResty 框架,利用 Lua 协程的方式来优化 php-fmp 长时间挂载的问题。Lua 协程会经过对 Nginx 转发的请求标记判断是否拦截网络请求,若是拦截,则会将阻塞操做交给 Lua 协程来处理,及时释放 php-fmp,缓解对服务器性能的消耗。优化的处理流程见下图:并发

IM 2.0 —— 需求定制阶段

伴随着业务的快速增加,IM 系统在短时间内面临着大量定制需求的增长,开发了许多新的业务模块。面对大量的用户咨询,客服的服务能力已经招架不住。所以,IM 2.0 将重心放在提高业务功能体验上,好比在处理用户的咨询时,将从前单一的分配方式演变为采用平均、权重、排队等多种方式;为了提高客服的效率,客服的咨询回复也增长了可选配置,例如自动回复、FAQ 等。框架

以一个典型的用户咨询场景为例,当用户打开 App 或者网页时,会经过链接层创建长链接,以后在咨询入口发起咨询时,会携带着消息线索初始化消息链路,创建一条可复用、可检索的消息线;发送消息时,经过消息服务将消息存储到 DB 中,同时会根据消息线检索当前咨询是否被分配到客服,调用分配服务的目的是为当前咨询完善客服信息;最后将客服信息更新到链路关系中。php-fpm

这样,一条完整的消息链路就创建完毕,以后用户/客服发出的消息经过转发服务传输给对方,处理流程以下图所示:性能

IM 3.0 —— 服务拆分阶段

业务量在不断积累,随着模块增长,IM 系统的代码膨胀得很快。因为代码规范没有统1、接口职责不够单1、模块间耦合较多等种缘由,改动一个需求极可能会影响到其它模块,使新需求的开发和维护成本都很高。优化

为了解决这种局面,IM 系统必需要进行架构升级,首要任务就是服务的拆分。目前,通过拆分后的 IM 系统总体分为 4 块大的服务,包括客服服务、用户服务、IM 服务、数据服务,以下图所示:

  • 客服服务:围绕提高客服效率和用户体验提供多种方式,如提供群组管理、成员管理、质检服务等来提高客服团队的运营和管理水平;经过分配服务、转接服务来使用户的接待效率更灵活高效;支持自动回复、FAQ、知识库服务等来提高客服咨询的回复效率等。

  • 用户服务:分析用户行为,为用户作兴趣推荐及用户画像,以及统计用户对马蜂窝商家客服的满意度。

  • IM 服务:支持单聊和群聊模式,提供实时消息通知、离线消息推送、历史消息漫游、联系人列表、文件上传与存储、消息内容风控检测等。

  • 数据服务:经过采集用户咨询的来源入口、是否咨询下单、是否有客服接待、用户咨询以及客服回复的时间信息等,定义数据指标,经过数据分析进行离线数据运算,最终对外提供数据统计信息。主要的指标信息有 30 秒、1 分钟回复率、咨询人数、无应答次数、平均应答时间、咨询销售额、咨询转化率、推荐转化率、分时接待压力、值班状况、服务评分等。

用户状态流转

现有的 IM 系统 中,用户咨询时一个完整的用户状态流转以下图所示:

用户点击咨询按钮触发事件,此时用户状态进入初始态。发送消息时,系统更改用户状态为待分配,经过调用分配服务分配了对应的客服后,用户状态更改成已分配、未解决。当客服解决了用户或者客服回复后用户长时间未说话,触发系统自动解决的操做,此时用户状态更改成已解决,一个咨询流程结束。

IM 服务的重构

在服务拆分的过程当中,咱们须要考虑特定服务的通用性、可用性和降级策略,同时须要尽量地下降服务间的依赖,避免因为单一服务不可用致使总体服务瘫痪的风险。在这期间,公司其它业务线对 IM 服务的使用需求也愈来愈多,使用频次和量级也开始加大。初期阶段的 IM 服务当链接量大时,只能经过修改代码实现水平扩容;新业务接入时,还须要在业务服务器上配置 Openresty 环境及 Lua 协程代码,业务接入很是不便,IM 服务的通用性也不好。

考虑到以上问题,咱们对 IM 服务进行了全面重构,目标是将 IM 服务抽取成独立的模块,不依赖其它业务,对外提供统一的集成和调用方式。考虑到 IM 服务对并发处理高和损耗低的要求,选择了 Go 语言来开发此模块,新的 IM 服务设计以下图:

其中,比较重要的 Proxy 层和 Exchange 层提供了如下服务:

1. 路由规则,例如 ip-hash、轮询、最小链接数等,经过规则将客户端散列到不一样的 ChannelManager 实例上。

2. 对客户端接入的管理,接入后的链接信息会同步到 DispatchTable 模块,方便 Dispatcher 进行检索。

3.ChannelManager 与客户端间的通讯协议,包括客户端请求创建链接、断线重连、主动断开、心跳、通知、收发消息、消息的 QoS 等。

4. 对外提供单发、群发消息的 REST 接口。这里须要根据场景来决定是否使用,例如用户咨询客服的场景就须要经过这个接口下发消息,主要缘由在如下 3 点:

  • 发消息时会有建立消息线、分配管家等逻辑,这些逻辑目前是 PHP 实现,IM 服务须要知道 PHP 的执行结果,一种方式是使用 Go 从新实现,另一种方式是经过 REST 接口调用 PHP 返回,这样会带来 IM 服务和 PHP 业务过多的网络交互,影响性能。

  • 转发消息时,ChannelManager 多个实例间须要互相通讯,例如 ChannelManager1 上的用户 A 给 ChannelManager2 上的客服 B 发消息,若是实例间无通讯机制,消息没法转发。当要再扩展 ChannelManager 实例时,新增实例须要和其它已存在实例分别创建通讯,增长了系统扩展的复杂度。

  • 若是客户端不支持 WebSocket 协议,做为降级方案的 HTTP 长链接轮循只能用来收消息,发消息须要经过短链接来处理。其它场景不须要消息转发,只用来给 ChannelManager 传输消息的场景,可经过 WebSocket 直接发送。

改造后的 IM 服务调用流程

初始化消息线及分配客服过程由 PHP 业务完成。须要消息转发时,PHP 业务调用 Dispatcher 服务的发消息接口,Dispatcher 服务经过共享的 Dispatcher Table 数据,检索出接收者所在的 ChannelManager 实例,将消息经过 RPC 的方式发送到实例上,ChannelManager 经过 WebSocket 将消息推送给客户端。IM 服务调用流程以下图所示:

当链接数超过当前 ChannelManager 集群承载的上限时,只需扩展 ChannelManager 实例,由 ETCD 动态的通知到监听侧,从而作到平滑扩容。目前浏览器版本的 JS-SDK 已经开发完毕,其它业务线经过接入文档,就能方便地集成 IM 服务。

在 Exchange 层的设计中,有 3 个问题须要考虑:

1. 多端消息同步

如今客户端有 PC 浏览器、Windows 客户端、H五、iOS/Android,若是一个用户登陆了多端,当有消息过来时,须要查找出这个用户的全部链接,当用户的某个端断线后,须要定位到这一个链接。

上面提到过,链接信息都是存储在 DispatcherTable 模块中,所以 DispatcherTable 模块要能根据用户信息快速检索出链接信息。DispatcherTable 模块的设计用到了 Redis 的 Hash 存储,当客户端与 ChannelManager 创建链接后,须要同步的元数据有 uid(用户信息)、uniquefield(惟一值,一个链接对应的惟一值)、wsid(链接标示符)、clientip(客户端 ip)、serverip(服务端 ip)、channel(渠道),对应的结构大体以下:

这样经过 key(uid) 能找到一个用户多个端的链接,经过 key+field 能定位到一条链接。链接信息的默认过时时间为 2 小时,目的是避免因客户端链接异常中断致使服务端没有捕获到,从而在 DispatcherTable 中存储了一些过时数据。

2. 用户在线状态同步

好比一个用户前后和 4 个客服咨询过,那么这个用户会出如今 4 个客服的咨询列表里。当用户上线时,要保证 4 个客服看到用户都是在线状态。

要作到这一点有两种方案,一种是客服经过轮询获取用户的状态,但这样当用户在线状态没有变化时,会发起不少无效的请求;另一种是用户上线时,给客服推送上线通知,这样会形成消息扩散,每个咨询过的客服都须要扩散通知。咱们最终采起的是第二种方式,在推送的过程当中,只给在线的客服推送用户状态。

3. 消息的不丢失,不重复

为了不消息丢失,对于采用长链接轮询方式的咱们会在发起请求时,带上客户端已读消息的 ID,由服务端计算出差值消息而后返回;使用 WebSocket 方式的,服务端会在推送给客户端消息后,等待客户端的 ACK,若是客户端没有 ACK,服务端会尝试屡次推送。

这时就须要客户端根据消息 ID 作消息重复的处理,避免客户端可能已收到消息,可是因为其它缘由致使 ACK 确认失败,触发重试,致使消息重复。

IM 服务的消息流

上文提到过 IM 服务须要支持多终端,同时在角色上又分为用户端和商家端,为了能让通知、消息在输出时根据域名、终端、角色动态输出差别化的内容,引入了 DDD (领域驱动设计)的建模方法来对消息进行处理,处理过程以下图所示:

总结和展望

伴随着马蜂窝「内容+交易」模式的不断深化,IM 系统架构也经历着演化和升级的不一样阶段,从初期粗旷无序的模式走向统一管理,逐渐规范、造成规模。

咱们取得了一些进步,固然,还有更长的路要走。将来,结合公司业务的发展脚步和团队的技术能力,咱们将不断进行 IM 系统的优化。目前咱们正在计划将消息轮询模块中的服务端代码用 Go 替换,使其再也不依赖 PHP 及 OpenResty 环境,实现更好地解耦;另外,咱们将基于 TensorFlow 实现向智慧客服的探索,经过训练数据模型、分析数据,进一步提高人工客服的解决效率,提高用户体验,更好地为业务赋能。

本文做者:马蜂窝电商平台 IM 研发团队。

(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)

相关文章
相关标签/搜索