Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 499, comments - 13, trackbacks - 0, articles - 1

          本文由小米技術(shù)團隊分享,原題“小愛接入層單機百萬長連接演進(jìn)”,有修訂。

          1、引言

          小愛接入層是小愛云端負(fù)責(zé)設(shè)備接入的第一個服務(wù),也是最重要的服務(wù)之一,本篇文章介紹了小米技術(shù)團隊2020至2021年在這個服務(wù)上所做的一些優(yōu)化和嘗試,最終將單機可承載長連接數(shù)從30w提升至120w+,節(jié)省了機器30+臺

          提示:什么是“小愛”?

          小愛(全名“小愛同學(xué)”)是小米旗下的人工智能語音交互引擎,搭載在小米手機、小米AI音箱、小米電視等設(shè)備中,在個人移動、智能家庭、智能穿戴、智能辦公、兒童娛樂、智能出行、智慧酒店、智慧學(xué)習(xí)共八大類場景中使用。

          (本文同步發(fā)布于:http://www.52im.net/thread-3860-1-1.html

          2、專題目錄

          本文是專題系列文章的第7篇,總目錄如下:

          1. 長連接網(wǎng)關(guān)技術(shù)專題(一):京東京麥的生產(chǎn)級TCP網(wǎng)關(guān)技術(shù)實踐總結(jié)
          2. 長連接網(wǎng)關(guān)技術(shù)專題(二):知乎千萬級并發(fā)的高性能長連接網(wǎng)關(guān)技術(shù)實踐
          3. 長連接網(wǎng)關(guān)技術(shù)專題(三):手淘億級移動端接入層網(wǎng)關(guān)的技術(shù)演進(jìn)之路
          4. 長連接網(wǎng)關(guān)技術(shù)專題(四):愛奇藝WebSocket實時推送網(wǎng)關(guān)技術(shù)實踐
          5. 長連接網(wǎng)關(guān)技術(shù)專題(五):喜馬拉雅自研億級API網(wǎng)關(guān)技術(shù)實踐
          6. 長連接網(wǎng)關(guān)技術(shù)專題(六):石墨文檔單機50萬WebSocket長連接架構(gòu)實踐
          7. 長連接網(wǎng)關(guān)技術(shù)專題(七):小米小愛單機120萬長連接接入層的架構(gòu)演進(jìn)》(* 本文

          3、什么是小愛接入層

          整個小愛的架構(gòu)分層如下:

          接入層主要的工作在鑒權(quán)授權(quán)層和傳輸層,它是所有小愛設(shè)備和小愛大腦交互的第一個服務(wù)。

          由上圖我們知道小愛接入層的重要功能有如下幾個:

          • 1)安全傳輸和鑒權(quán):維護(hù)設(shè)備和大腦的安全通道,保障身份認(rèn)證有效和傳輸數(shù)據(jù)安全;
          • 2)維護(hù)長連接:維持設(shè)備和大腦的長連接(Websocket等),做好連接狀態(tài)存儲,心跳維護(hù)等工作;
          • 3)請求轉(zhuǎn)發(fā):針對每一次小愛設(shè)備的請求做好轉(zhuǎn)發(fā),保障每一次請求的穩(wěn)定。

          4、早期接入層的技術(shù)實現(xiàn)

          小愛接入層最早的實現(xiàn)是基于AkkaPlay,我們使用它們搭建了第一個版本,該版本特點如下:

          • 1)基于Akka我們基本做到了初步的異步化,保障核心線程不被阻塞,性能尚可。
          • 2)Play框架天然支持Websocket,因此我們在有限的人力下能夠快速搭建和實現(xiàn),且能夠保障協(xié)議實現(xiàn)的標(biāo)準(zhǔn)性。

          5、早期接入層的技術(shù)問題

          隨著小愛長連接的數(shù)量突破千萬大關(guān),針對早期的接入層方案,我們發(fā)現(xiàn)了一些問題。

          主要的問題如下:

          1)長連接數(shù)量上來后,需要維護(hù)的內(nèi)存數(shù)據(jù)越來越多,JVM的GC成為不可忽略的性能瓶頸,且一旦代碼寫的不好有GC風(fēng)險。經(jīng)過之前事故分析,Akka+Play版的接入層其單實例長連接數(shù)量的上限在28w左右。

          2)老版本的接入層實現(xiàn)比較隨意,其Akka Actor之間存在非常多的狀態(tài)依賴而不是基于不可變的消息傳遞這樣使得Actor之間的通信變成了函數(shù)調(diào)用,導(dǎo)致代碼可讀性差且維護(hù)很困難,沒有發(fā)揮出Akka Actor在構(gòu)建并發(fā)程序的優(yōu)勢。

          3)作為接入層服務(wù),老版本對協(xié)議的解析是有很強的依賴的,這導(dǎo)致它要隨著版本變動而頻繁上線,其上線會引起長連接重連,隨時有雪崩的風(fēng)險。

          4)由于依賴Play框架,我們發(fā)現(xiàn)其長連接打點有不準(zhǔn)確的問題(因為拿不到底層TCP連接的數(shù)據(jù)),這個會影響我們每日巡檢對服務(wù)容量的評估,且依賴其他框架在長連接數(shù)量上來后我們沒有辦法做更細(xì)致的優(yōu)化。

          6、新版接入層的設(shè)計目標(biāo)

          基于早期接入層技術(shù)方案的種種問題,我們打算重構(gòu)接入層。

          對于新版接入層我們制定的目標(biāo)是:

          • 1)足夠穩(wěn)定:上線盡可能不斷連接且服務(wù)穩(wěn)定;
          • 2)極致性能:目標(biāo)單機至少100w長連接,最好不要受GC影響;
          • 3)最大限度可控:除了底層網(wǎng)絡(luò)I/O的系統(tǒng)調(diào)用,其他所有代碼都要是自己實現(xiàn)/或者內(nèi)部實現(xiàn)的組件,這樣我們有足夠的自主權(quán)。

          于是,我們開始了單機百萬長連接的漫漫實踐之路。。。

          7、新版接入層的優(yōu)化思路

          7.1 接入層的依賴關(guān)系

          接入層與外部服務(wù)的關(guān)系理清如下:

          7.2 接入層的功能劃分

          接入層的主要功能劃分如下:

          • 1)WebSocket解析:收到的客戶端字節(jié)流,要按照WebSocket協(xié)議要求解析出數(shù)據(jù);
          • 2)Socket狀態(tài)保持:存儲連接的基本狀態(tài)信息;
          • 3)加密解密:與客戶端通訊的所有數(shù)據(jù)都是加密過的,而與后端模塊之間傳輸是json明文的;
          • 4)順序化:同一個物理連接上,先后兩個請求A、B到達(dá)服務(wù)器,后端服務(wù)中B可能先于A得到了應(yīng)答,但是我們收到B不能立刻發(fā)送給客戶端,必須等待A完成后,再按照A,B的順序發(fā)給客戶端;
          • 5)后端消息分發(fā):接入層后面不止對接單個服務(wù),可能根據(jù)不同的消息轉(zhuǎn)發(fā)給不同的服務(wù);
          • 6)鑒權(quán):安全相關(guān)驗證,身份驗證等。

          7.3 接入層的拆分思路

          把之前的單一模塊按照是否有狀態(tài),拆分為兩個子模塊。

          具體如下:

          • 1)前端:有狀態(tài),功能最小化,盡量少上線;
          • 2)后端:無狀態(tài),功能最大化,上線可做到用戶無感知。

          所以,按照上面的原則,理論上我們會做出這樣的功能劃分,即前端很小、后端很大。示意圖如下圖所示。

          8、新版接入層的技術(shù)實現(xiàn)

          8.1 總覽

          模塊拆分為前后端:

          • 1)前端有狀態(tài),后端無狀態(tài);
          • 2)前后端是獨立進(jìn)程,同機部署。

          補充:前端負(fù)責(zé)建立與維護(hù)設(shè)備長連接的狀態(tài),為有狀態(tài)服務(wù);后端負(fù)責(zé)具體業(yè)務(wù)請求,為無狀態(tài)服務(wù)。后端服務(wù)上線不會導(dǎo)致設(shè)備連接斷開重連及鑒權(quán)調(diào)用,避免了長連接狀態(tài)因版本升級或邏輯調(diào)整而引起的不必要抖動;

          前端使用CPP實現(xiàn):

          • 1)Websocket協(xié)議完全自己解析:可以從Socket層面獲取所有信息,任何Bug都可以處理;
          • 2)更高的CPU利用率:沒有任何額外JVM代價,無GC拖累性能;
          • 3)更高的內(nèi)存利用率:連接數(shù)量變大后與連接相關(guān)的內(nèi)存開銷變大,自己管理可以極端優(yōu)化。

          后端暫時使用Scala實現(xiàn):

          • 1)已實現(xiàn)的功能直接遷移,比重寫代價要低得多;
          • 2)依賴的部分外部服務(wù)(比如鑒權(quán))有可直接利用的Scala(Java)SDK庫,而沒有C++版本,若用C++重寫代價非常大;
          • 3)全部功能無狀態(tài)化改造,可以做到隨時重啟而用戶無感知。

          通訊使用ZeroMQ

          進(jìn)程間通訊最高效的方式是共享內(nèi)存,ZeroMQ基于共享內(nèi)存實現(xiàn),速度沒問題。

          8.2 前端實現(xiàn)

          整體架構(gòu):

           

          如上圖所示,由四個子模塊組成:

          • 1)傳輸層:Websocket協(xié)議解析,XMD協(xié)議解析;
          • 2)分發(fā)層:屏蔽傳輸層的差異,不管傳輸層使用的什么接口,在分發(fā)層轉(zhuǎn)化成統(tǒng)一的事件投遞到狀態(tài)機;
          • 3)狀態(tài)機層:為了實現(xiàn)純異步服務(wù),使用自研的基于Actor模型的類Akka狀態(tài)機框架XMFSM,這里面實現(xiàn)了單線程的Actor抽象;
          • 4)ZeroMQ通訊層:由于ZeroMQ接口是阻塞實現(xiàn),這一層通過兩個線程分別負(fù)責(zé)發(fā)送和接收。

          8.2.1)傳輸層:

          WebSocket 部分使用 C++ 和 ASIO 實現(xiàn) websocket-lib。小愛長連接基于WebSocket協(xié)議,因此我們自己實現(xiàn)了一個WebSocket長連接庫。

          這個長連接庫的特點是:

          • a. 無鎖化設(shè)計,保障性能優(yōu)異;
          • b. 基于BOOST ASIO 開發(fā),保障底層網(wǎng)絡(luò)性能。

          壓測顯示該庫的性能十分優(yōu)異的:

          這一層同時也承擔(dān)了除原始WebSocket外,其他兩種通道的的收發(fā)任務(wù)。

          目前傳輸層一共支持以下3種不同的客戶端接口:

          • a. websocket(tcp):簡稱ws;
          • b. 基于ssl的加密websocket(tcp):簡稱wss;
          • c. xmd(udp):簡稱xmd。

          8.2.2)分發(fā)層:

          把不同的傳輸層事件轉(zhuǎn)化成統(tǒng)一事件投遞到狀態(tài)機,這一層起到適配器的作用,確保無論前面的傳輸層使用哪種類型,到達(dá)分發(fā)層變都變成一致的事件向狀態(tài)機投遞。

          8.2.3)狀態(tài)機處理層:

          主要的處理邏輯都位于這一層中,這里非常重要的一個部分是對于發(fā)送通道的封裝。

          對于小愛應(yīng)用層協(xié)議,不同的通道處理邏輯是完全一致的,但是在處理和安全相關(guān)邏輯上每個通道又有細(xì)節(jié)差異。

          比如:

          • a. wss 收發(fā)不需要加解密,加解密由更前端的Nginx做了,而ws需要使用AES加密發(fā)送;
          • b. wss 在鑒權(quán)成功后不需要向客戶端下發(fā)challenge文本,因為wss不需要做加解密;
          • c. xmd 發(fā)送的內(nèi)容與其他兩個不同,是基于protobuf封裝的私有協(xié)議,且xmd需要處理發(fā)送失敗后的邏輯,而ws/wss不用考慮發(fā)送失敗的問題,由底層Tcp協(xié)議保證。

          針對這種情況:我們使用C++的多態(tài)特性來處理,專門抽象了一個Channel接口,這個接口中提供的方法包含了一個請求處理的一些關(guān)鍵差異步驟,比如如何發(fā)送消息到客戶端,如何stop連接,如何處理發(fā)送失敗等等。對于3種(ws/wss/xmd)不同的發(fā)送通道,每個通道有自己的Channel實現(xiàn)。

          客戶端連接對象一創(chuàng)建,對應(yīng)類型的具體Channel對象就立刻被實例化。這樣狀態(tài)機主邏輯中只實現(xiàn)業(yè)務(wù)層的公共邏輯即可,當(dāng)在有差異邏輯調(diào)用時,直接調(diào)用Channel接口完成,這樣一個簡單的多態(tài)特性幫助我們分割了差異,確保代碼整潔。

          8.2.4)ZeroMQ 通訊層:

          通過兩個線程將ZeroMQ的讀寫操作異步化,同時負(fù)責(zé)若干私有指令的封裝和解析。

          8.3 后端實現(xiàn)

          8.3.1)無狀態(tài)化改造:

          后端做的最重要改造之一就是將所有與連接狀態(tài)相關(guān)的信息進(jìn)行剔除。

          整個服務(wù)以 Request(一次連接上可以傳輸N個Request)為核心進(jìn)行各種轉(zhuǎn)發(fā)和處理,每次請求與上一次請求沒有任何關(guān)聯(lián)。一個連接上的多次請求在后端模塊被當(dāng)做獨立請求處理。

          8.3.2)架構(gòu):

          Scala 服務(wù)采用 Akka-Actor 架構(gòu)實現(xiàn)了業(yè)務(wù)邏輯。

          服務(wù)從 ZeroMQ 收到消息后,直接投遞到 Dispatcher 中進(jìn)行數(shù)據(jù)解析與請求處理,在 Dispatcher 中不同的請求會發(fā)送給對應(yīng)的 RequestActor進(jìn)行 Event 協(xié)議解析并分發(fā)給該 event 對應(yīng)的業(yè)務(wù) Actor 進(jìn)行處理。最后將處理后的請求數(shù)據(jù)通過XmqActor 發(fā)送給后端 AIMS&XMQ 服務(wù)。

          一個請求在后端多個 Actor 中的處理流程:

          8.3.3)Dispatcher 請求分發(fā):

          前端與后端之間通過 Protobuf 進(jìn)行交互,避免了Json 解析的性能消耗,同時使得協(xié)議更加規(guī)范化。

          后端服務(wù)從 ZeroMQ 收到消息后,會在 DispatcherActor 中進(jìn)行PB協(xié)議解析并根據(jù)不同的分類(簡稱CMD)進(jìn)行數(shù)據(jù)處理,分類包括如下幾種。

          * BIND 命令:

          鑒權(quán)功能,由于鑒權(quán)功能邏輯復(fù)雜,使用C++語言實現(xiàn)起來較為困難,目前依然放在 scala 業(yè)務(wù)層進(jìn)行鑒權(quán)。該部分對設(shè)備端請求的 HTTP Headers 進(jìn)行解析,提取其中的 token 進(jìn)行鑒權(quán),并將結(jié)果返回前端。

          * LOGIN 命令:

          設(shè)備登入,設(shè)備鑒權(quán)通過后當(dāng)前連接已成功建立,此時會進(jìn)行 Login 命令的執(zhí)行,用于將該長連接信息發(fā)送至AIMS并記錄于Varys服務(wù)中,方便后續(xù)的主動下推等功能。在 Login 過程中,服務(wù)首先將請求 Account 服務(wù)獲取長連接的 uuid(用于連接過程中的路由尋址),然后將設(shè)備信息+uuid 發(fā)送至AIMS進(jìn)行設(shè)備登入操作。

          * LOGOUT 命令:

          設(shè)備登出,設(shè)備在與服務(wù)端斷開連接時需要進(jìn)行 Logout 操作,用于從 Varys 服務(wù)中刪除該長連接記錄。

          * UPDATE 與 PING 命令:

          a. Update 命令,設(shè)備狀態(tài)信息更新,用于更新該設(shè)備在數(shù)據(jù)庫中保存的相關(guān)信息;

          b. Ping 命令,連接保活,用于確認(rèn)該設(shè)備處于在線連接狀態(tài)。

          * TEXT_MESSAGE 與 BINARY_MESSAGE:

          文本消息與二進(jìn)制消息,在收到文本消息或二進(jìn)制消息時將根據(jù) requestid 發(fā)送給該請求對應(yīng)的RequestActor進(jìn)行處理。

          8.3.4)Request 請求解析:

          針對收到的文本和二進(jìn)制消息,DispatcherActor 會根據(jù) requestId 將其發(fā)送給對應(yīng)的RequestActor進(jìn)行處理。

          其中:文本消息將會被解析為Event請求,并根據(jù)其中的 namespace 和 name 將其分發(fā)給指定的業(yè)務(wù)Actor。二進(jìn)制消息則會根據(jù)當(dāng)前請求的業(yè)務(wù)場景被分發(fā)給對應(yīng)的業(yè)務(wù)Actor。

          8.4 其他優(yōu)化

          在完成新架構(gòu) 1.0 調(diào)整過程中,我們也在不斷壓測長連接容量,總結(jié)幾點對容量影響較大的點。

          8.4.1)協(xié)議優(yōu)化:

          a. JSON替換為Protobuf: 早期的前后端通信使用的是 json 文本協(xié)議,后來發(fā)現(xiàn) json 序列化、反序列化這部分對CPU的占用較大,改為了 protobuf 協(xié)議后,CPU占用率明顯下降。

          b. JSON支持部分解析:業(yè)務(wù)層的協(xié)議是基于json的,沒有辦法直接替換,我們通過"部分解析json"的方式,只解析很小的 header 部分拿到 namespace 和 name,然后將大部分直接轉(zhuǎn)發(fā)的消息轉(zhuǎn)發(fā)出去,只將少量 json 消息進(jìn)行完整反序列化成對象。此種優(yōu)化后CPU占用下降10%。

          8.4.2)延長心跳時間:

          在第一次測試20w連接時,我們發(fā)現(xiàn)在前后端收發(fā)的消息中,一種用來保持用戶在線狀態(tài)的心跳PING消息占了總消息量的75%,收發(fā)這個消息耗費了大量CPU。因此我們延長心跳時間也起到了降低CPU消耗的目的。

          8.4.3)自研內(nèi)網(wǎng)通訊庫:

          為了提高與后端服務(wù)通信的性能,我們使用自研的TCP通訊庫,該庫是基于Boost ASIO開發(fā)的一個純異步的多線程TCP網(wǎng)絡(luò)庫,其卓越的性能幫助我們將連接數(shù)提升到120w+。

          9、未來規(guī)劃

          經(jīng)過新版架構(gòu)1.0版的優(yōu)化,驗證了我們的拆分方向是正確的,因為預(yù)設(shè)的目標(biāo)已經(jīng)達(dá)到:

          • 1)單機承載的連接數(shù) 28w => 120w+(普通服務(wù)端機器 16G內(nèi)存 40核 峰值請求QPS過萬),接入層下線節(jié)省了50%+的機器成本;
          • 2)后端可以做到無損上線。

          再重新審視下我們的理想目標(biāo),以這個為方向,我們就有了2.0版的雛形:

          具體就是:

          • 1)后端模塊使用C++重寫,進(jìn)一步提高性能和穩(wěn)定性。同時將后端模塊中無法使用C++重寫的部分,作為獨立服務(wù)模塊運維,后端模塊通過網(wǎng)絡(luò)庫調(diào)用;
          • 2)前端模塊中非必要功能嘗試遷移到后端,讓前端功能更少,更穩(wěn)定;
          • 3)如果改造后,前端與后端處理能力差異較大,考慮到ZeroMQ實際是性能過剩的,可以考慮使用網(wǎng)絡(luò)庫替換掉ZeroMQ,這樣前后端可以從1:1單機部署變?yōu)?:N多機部署,更好的利用機器資源。

          2.0版目標(biāo)是:經(jīng)過以上改造后,期望單前端模塊可以達(dá)到200w+的連接處理能力。

          10、參考資料

          [1] 上一個10年,著名的C10K并發(fā)連接問題

          [2] 下一個10年,是時候考慮C10M并發(fā)問題了

          [3] 一文讀懂高性能網(wǎng)絡(luò)編程中的線程模型

          [4] 深入操作系統(tǒng),一文讀懂進(jìn)程、線程、協(xié)程

          [5] Protobuf通信協(xié)議詳解:代碼演示、詳細(xì)原理介紹等

          [6] WebSocket從入門到精通,半小時就夠!

          [7] 如何讓你的WebSocket斷網(wǎng)重連更快速?

          [8] 從100到1000萬高并發(fā)的架構(gòu)演進(jìn)之路

          學(xué)習(xí)交流:

          - 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM

          - 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK 

          (本文同步發(fā)布于:http://www.52im.net/thread-3860-1-1.html



          作者:Jack Jiang (點擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 南华县| 从江县| 罗江县| 涪陵区| 洛浦县| 亳州市| 拜城县| 涞水县| 延津县| 莱州市| 电白县| 揭阳市| 鄢陵县| 桐庐县| 珲春市| 曲沃县| 乌兰浩特市| 亳州市| 黔南| 沐川县| 扶绥县| 南昌市| 右玉县| 天水市| 来宾市| 长沙县| 阿巴嘎旗| 南木林县| 蓝田县| 利辛县| 城口县| 阿拉尔市| 商河县| 乐业县| 基隆市| 南涧| 四平市| 桐城市| 青田县| 泰安市| 福州市|