得物客服一站式工作台卡顿优化之路

693 阅读9分钟

一、背景

客服一站式工作台包含了在线、电话、工单和工具类四大功能模块。其中很多通用的模块,比如工单详情、订单详情都是通过iframe的形式嵌套的,在系统加载过程中会比较耗时,再加上在线消息通信模块强依赖tinode第三方SDK,很多方法都是直接调用tinode提供的api,同时也继承了tinode很多不合理的方式,从使用tinode到目前为止,因迭代资源的投入,一直没有对tinode源码做一些优化和改进,当消息通信的模式改成广播之后,会话卡顿问题就暴露出来了。通过对tinode源码消息链路模块的阅读,发现了有不少的优化空间,本文则是针对消息链路这块阐述的具体优化实现。

二、发现问题

1、消息数据处理流程存在缺陷

经过对tinode第三方sdk源码的阅读,发现其中客服在“接收”和“发送”消息的链路上有很大的优化空间,在原有的逻辑中,从发送消息到快速渲染页面再到tinode响应返回结果再去刷新渲染页面,以及客服接受到消息的时候,会对整个消息进行刷新,反序列化、排序、去重、状态处理等等都需要多次的循环,再加上通信模式改为广播模式,大数据量循环任务,对于性能来说是个严峻的挑战。

客服“接收”和“发送”消息链路概图(一)

客服“接收”和“发送”消息链路概图(二)

图中红色区域有较多的for循环是耗时最多的场景,原因是要获取用户与客服沟通记录(原有tinode中提供的方式,topic.message() 会执行n次),反序列化、会话状态处理、排序、去重都会将所有聊天消息进行遍历,其中 反序列化为耗时最高的场景,如果客服跟用户之前的聊天消息越多,遍历次数就越多,耗时就越久,再加上JavaScript是单线程,遍历次数多了就会形成阻塞,导致客服在快速切换会话的时候,循环还未结束,页面未渲染完成,就出现卡顿现象。

三、优化思路

每一位用户从客户端进线到坐席客服工作台的时候,会生成的一个会话id(sessionId),每一个会话id下面的每一条人工消息中都会有一个消息id(msgid)

客服在跟用户之间来回沟通的消息回合比较多,为了减少“老代码”中多次循环降低性能的操作,想到最核心的任务就是尽量避免去遍历聊天的消息数据(因为消息太多了),遵循能不遍历聊天消息就不遍历的原则,对于原逻辑中的“去重”和“排序”逻辑做了重写,这个时候,上面提到的会话id和消息id就起到了非常重要的作用。

1、去重

本次优化方案中采用全局维护一个msgidCacheMaps Map数据结构,这个数据结构有两个维度,sessionId 和 msgid ,用来保存当前会话(sessionId)中每条消息的msgid,消息对话中,人工客服发送的消息会经历从虚拟消息到真实消息两个阶段(这里的的虚拟消息指的是在人工会话中,客服向网关发送消息后,为了快速让消息展示在聊天区域,通过前一个消息seq + 0.002生成虚拟的seq即:virtualSeq,等到网关返回真实的seq后,再将virtualSeq替换成真实的seq),虚拟消息阶段会保存msgid到Map中,对于系统推送的消息,没有msgid,不需要经历这个过程,直接放进会话池,真实消息(tinode返回seq)阶段,根据msgid到msgidCacheMaps Map数据结构中进行查询,存在此msgid,说明是重复数据,配合seq进行替换即可。

2、排序

本次的优化方案是采用 二分查找插入排序 ****的方法,全局维护一个seqCacheMaps Map数据结构,这个数据结构跟上面去重有些类似,也有两个维度,sessionId和seq,二分查找插入排序的方法,用seq(真实seq)和virtualSeq(虚拟seq)作为查找的依据,每次消息进来,根据二分法快速找到当前seq可插入位置,虚拟消息阶段,直接插入,真实消息阶段(msgidCacheMaps存在此msgid),直接替换,但是这个时候遇到一个问题,因为在人工会话过程中客服向用户发的每一条消息都会在网关进行敏感词校验,没有触发到敏感词就会将消息发送到客服端展示给用户,如果触发到敏感词,含有敏感词的消息就会被网关拦截,消息也不会到达用户侧,此时网关也不会返回seq,那么没有返回seq,又该如何处理呢?那就是在tinode返回阶段,会把前面virtualSeq替换为前一个消息seq + 0.002,确保其位置有序不会错乱的展示在在聊天区域

“去重” 和 “排序” 概图

3、缓存回收(结束会话销毁)

在上面 去重 和 排序 中提到,为了减少遍历次数,全局维护两个数据仓库(msgidCacheMaps Map数据结构、seqCacheMaps Map数据结构), 但是每位客服每天的会话量在100+,再加上每条会话中客服和用户的来回消息数约40+,如果客服再去查看历史消息,一页20条,如果只存不删,存储的数据量还比较庞大的,容易导致内存溢出,那么什么时候删除比较合适呢?根据业务情况,最后选择在结束会话、会话转接、推送离线的情况下会对挂载在全局的hash map进行销毁,释放内存。

数据“存储”和“删除”概图

4、消息状态

这里的消息状态指:已读、未读、已接收、发送中、发送失败……等

在客服和用户沟通过程中,客服侧和用户侧所展示的消息状态都是实时更新的,客服发送消息给用户,当用户读了这条消息后会返回info协议(推送已经消息通知)告诉h5侧该条消息已读,然后h5侧对该条消息进行状态更新

  • 原处理方式: 当客服给用户发送消息后,对当前会话中这个用户的所有历史消息进行遍历,进行全部重置操作,这个时候如果遇到用户与客服沟通的消息很多的情况,就会导致遍历次数多,产生严重消耗性能等问题。
  • 优化方案: 先过滤掉历史消息和非客服发送的消息,通过二分法的方式去找到该消息,然后直接改变状态。在收到用户发送的消息后,对messagePools(当前用户所有会话)中的客服发送的消息倒序进行状态更新为已读,因为既然用户都发消息过来了,说明客服发送的消息已经被阅读过了,就不需要按照之前老的逻辑再去给每个消息都遍历去设置状态了,浪费性能,除发送中和发送失败的消息外,全部渲染为已读
  • 具体实现:客户端推送长链note事件告诉H5,H5侧记录已读的这条消息的seq,对于小于等于seq的客服发送的消息数据进行状态更新,即: recv(已接收) => read(已读)
  • 发送消息:目前发送消息只会执行2次,第一次会快速将消息展示到沟通页面,然后再进行消息的发送(wss),当收到ack后,会进行二次消息状态更新,只通过msgid会找到需要更新的消息进行更新,不再需要利用 tinode 提供的topic.message方法进行全量遍历了
  • 接收消息:客服接收用户消息只会触发一次消息更新,不需要再对当前用户的全量数据进行遍历更新新状态了,同时也会回ack

5、敏感词拦截处理

IM聊天页面在用户进线后,对于用户和坐席客服之间发送的消息会进线敏感词监控(仅监控 和 禁止发送)。

  • 原方案: 坐席客服在编辑消息后,点击发送,调用后端敏感词接口,没有触发到敏感词校验通过后才能发出去,如果出现网络波动接口返回慢的时候,就会让客服感觉发消息卡一下才能出去的情况。
  • 优化方案: 通过网关拦截,客服发送消息的时候,直接渲染到聊天区域,网关去检验发送的消息是否触发到敏感词,如果有触发到敏感词,那么网关会返回一个状态告诉h5,h5再根据返回的结果更改状态去提示客服。

敏感词逻辑概图

四、优化前后数据对比

优化链路技术方案实现整体在2月28号发布上线,所以以2月28号为时间截点,拉取了优化前后的数据对比,具体如下所示。

1、优化前

如上图所示,统计了2022年2月1日 ~ 2022年2月28日总进线的两个数据指标:

  • 平均首次响应时长: 8.40秒
  • 平均响应时长: 19.9秒

2、优化后

如上图所示,统计了2022年3月1日 ~ 2022年3月9日总进线的两个数据指标:

  • 平均首次响应时长: 6.82秒,比优化前减少了1.58秒
  • 平均响应时长: 18.22秒,比优化前减少了1.68秒

五、总结

一般来说 IM 产品的用户量和活跃度通常都很大,在一些特殊的时间点经常容易造成流量的波峰,因此技术上需要能够应对突发的量级,同时IM一般主要包含这4个特点:实时性、可靠性、一致性、安全性,对于IM的优化还有很长的路要走,在保证业务稳定情况下,后续我们也会围绕着四个特点继续努力打磨,让符合得物自己的IM SDK越来越完善,形成行业消息通信的标杆。

文/YU BO

关注得物技术,做最潮技术人!