訊息中心優化
為什麼要優化
訊息中心是給數百萬騎手和幾十萬商家推送訊息,然而其程式碼是將以前分散在各個組的推送訊息的程式碼給拷貝了一份,程式碼沒有註釋,邏輯複雜混亂,難於理解,程式碼質量差,效能低下。主要有以下問題:
1.推送訊息緩慢,傳送全國訊息,要好幾個小時 2.發一次全國訊息redis暴增,cpu飈高,人肉告訴運營不要再推全國了,維護人員和運營都比較煩 3.推送資料在記憶體中停留時間過久,線上頻繁fullgc 4.有幾個介面耗時太大,實現不合理,效能存在嚴重問題 5.程式每次上線需要通知運營不要推送訊息,以免訊息丟失 6.一個DTO裡面有60過個欄位,同一個意思,好幾個欄位都在用,邏輯重複混亂,各種"神邏輯",維護人和對接人都比較頭疼 7.方法和變數命名費解 8.各種歷史bug,以前沒發現,現在突然發現了,例如equals判斷可能丟擲NEP 9.線上經常慢查
在出了幾次問題之後,下定決心要進行大的改造了,畢竟出了問題自己和整個團隊都要背鍋。
改造過程
從今年年初到現在,我和張佳對訊息中心進行了效能,邏輯,方案,監控等方面的持續的優化,當然這之中也接入了許多業務的需求。目前訊息中心比較穩定,效能也上去了,推送一百萬騎手15分鐘左右(還能更快)。
一。效能優化
改造一,去Redis快取佇列
改造之前,訊息中心將查詢出來的騎手和商家放入redis做快取佇列,然後一個個pop,存入redis用的java原生的序列化方式,佔用記憶體比較多。經常是運營傳送兩次全國訊息,redis記憶體就不足了。
Redis處理資料是單執行緒,並且消費redis的時候,還在各個定時任務之間加了一把分散式鎖,導致只有一臺機器在消費,在傳送城市比較多,有幾百萬騎手的時候,傳送完需要三四個小時。
解決方案
採用RocketMQ做快取佇列,RocketMQ是阿里開源的一款高效能高吞吐量的訊息中介軟體,支援海量堆積,消費失敗有重試機制(當然需要在業務端做冪等)。
RocketMQ叢集模式,每臺機器都能做消費者,提高吞吐量。整個訊息中心使用一個topic,不同型別的訊息使用子tag區分不同的行為。
改造二,訊息非同步傳送
改造之前,訊息中心查詢騎手,同步查出所有的騎手,然後將這個幾百萬的大List在記憶體裡按照城市分組,組裝推送DTO,看過一次OOM之後dump出來的檔案,有三個大的物件,每個物件佔用記憶體25%左右,不頻繁fullgc才怪。
雖然查詢騎手用了多執行緒,但是查詢耗時還是很大,拖累了整個介面的耗時。有釋出任務,剛好這臺機器上有傳送大量訊息,這臺機器上的訊息就丟失了。
每次定時任務推送將所有的騎手資訊按照城市分組後,作為一個ZSet Member序列化後寫入到Redis,如果資料結構發生變化不相容,並且在記憶體對推送物件作分組等操作,資料量特別大時,效能低
撤銷訊息,需要先取出ZSet的內容,然後遍歷Redis 相應的ZSet刪除內容。資料量特別大是造成jvm無響應,對其他介面也有很大影響。
解決方案
採用非同步傳送的方式,記錄一個定時任務,然後掃描定時任務,每次傳送訊息按照城市推送,分頁查詢觸達物件,城市列表儲存到Redis Set,關聯到對應的NotifyId。每次傳送一頁訊息,每頁500條訊息。改完之後再沒有出現過OOM,介面耗時也從十幾秒到二十幾毫秒。
改造三,sql慢查
主要的慢查集中app重新整理訊息,鷹眼查詢頁面,強制閱讀更新,查詢訊息已讀數和傳送總數。
解決方案
app重新整理訊息主要是通過接受者反查訊息,先查詢未讀的置頂訊息,再查詢普通訊息,需要查詢好幾次資料庫,一條訊息對應幾萬個接受者,非常適合快取。傳送訊息的時候同步訊息id到Redis key中,然後非同步同步訊息內容到Redis中。
訊息中心訊息每個月訊息700萬左右,加上覆雜的查詢條件,城市採用json儲存,mysql做不到按城市搜尋,接入全文搜尋引擎ElasticSearch,非同步同步訊息到ElasticSearch,鷹眼查詢從ElasticSearch中查詢。
強制閱讀需要在幾千萬的接受者列表中更新一條記錄的isforce read欄位,這對資料庫的壓力還是很大的,採用一張擴充套件表,每個騎手對應一條記錄強制閱讀的記錄,將更新幾千萬資料的大表換成更新三百萬的小表。
已讀數和傳送總數是在幾千萬的接受者列表中查詢這條的訊息的傳送總數和已讀數,同樣對資料庫壓力比較大。傳送總數在發完訊息的時候就確定了,放入Redis hash中,已讀數每次增加hash field,已讀數和未讀數的查詢放在Redis中。
改造之前有效訊息查詢分為永久有效訊息和有實效的訊息,istime limit = 0 or (istime limit = 1 and validtime begin < now() and validtime end > now(),改為永久有效訊息validtime end為一個很大的時間,簡化查詢有效訊息sql為validtime begin < now() and validtime end > now()
二。程式碼邏輯方案優化
各種傳送訊息場景的欄位都在一個DTO中60多個欄位,同一個意思,不同場景用不同的欄位,還有很多廢棄的欄位,基本沒有註釋,沒有文件。方法不區分業務場景的情況下強行復用,定時傳送訊息,立即傳送訊息,儲存草稿訊息都在一個介面中。
還有一些設計上的不合理,傳送多城市訊息,每個城市一條訊息,運營新建一條訊息,鷹眼後臺顯示不只一條訊息,不利於運營統計訊息觸達率。傳送訊息類別維度不統一,商家,騎手,城市團隊中並列了指定傳送者。
解決方案
看程式碼邏輯,刪除無用廢棄程式碼,下掉廢棄介面,寫ofollow,noindex" target="_blank">對接文件 ,方案改造文件 ,程式碼加上註釋,良好的程式碼註釋真的很重要,欄位是否必填,欄位什麼意思,特殊處理的地方。傳送多城市訊息改為一條訊息,傳送總數和已讀數放redis儲存和查詢,避免遍歷notify-receiver的8個庫。設計上的不合理改造,詳細看技術文件 。
三。監控優化
對主要的介面耗時,呼叫異常,dubbo執行緒進行打點監控,在granfa上配置報警資訊,出現異常在釘釘群報警。訊息傳送延遲釘釘報警,rocketMQ堆積報警,騎手大批量延遲報警。記錄每條訊息的狀態流轉日誌。
其它改造
- 查詢IO,採用批量查詢,減少網路開銷,多執行緒查詢,減少耗時
- mysql只選出必要欄位,當查詢量比較大,並且有大的欄位時,對網路的頻寬佔用還是比較大的。
- 不同的查詢物件對應不同的子類,避免一個DTO中對應欄位太多
- 工廠模式,代理模式,橋接模式等重構原來程式碼
- @Cache註解整個物件做快取的key,其中只有幾個欄位有效,改為使用其中有效的欄位生成key
後續優化
- 大量傳送訊息和點對點的訊息走不同的RocketMQ topic,避免大量傳送的訊息阻塞點對點的傳送
- 下掉沒有流量的介面
- 點對點發送訊息和鷹眼傳送訊息的DTO分開,鷹眼傳送訊息的很多欄位app傳送訊息用不到
- 傳送訊息去重