一、背景
客服一站式工作臺包含了線上、電話、工單和工具類四大功能模組。其中很多通用的模組,比如工單詳情、訂單詳情都是通過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
關注得物技術,做最潮技術人!