基于Go的马蜂窝旅游网分布式IM系统技术实践
個人博客導(dǎo)航頁(點(diǎn)擊右側(cè)鏈接即可打開個人博客):互聯(lián)網(wǎng)老兵帶你入門技術(shù)棧?
一、引言
即時(shí)通訊(IM)功能對于電商平臺來說非常重要,特別是旅游電商。
從商品復(fù)雜性來看,一個旅游商品可能會包括用戶在未來一段時(shí)間的衣、食、住、行等方方面面。從消費(fèi)金額來看,往往單次消費(fèi)額度較大。對目的地的陌生、在行程中可能的問題,這些因素使用戶在購買前、中、后都存在和商家溝通的強(qiáng)烈需求。可以說,一個好用的 IM 可以在一定程度上對企業(yè)電商業(yè)務(wù)的 GMV 起到促進(jìn)作用。
本文我們將結(jié)合馬蜂窩旅游電商IM系統(tǒng)的發(fā)展歷程,單獨(dú)介紹基于Go重構(gòu)分布式IM系統(tǒng)過程中的實(shí)踐和總結(jié)(本文相當(dāng)于《從游擊隊(duì)到正規(guī)軍(一):馬蜂窩旅游網(wǎng)的IM系統(tǒng)架構(gòu)演進(jìn)之路》一文的進(jìn)階篇),希望可以給有相似問題的朋友一些借鑒。
系列文章:
《從游擊隊(duì)到正規(guī)軍(一):馬蜂窩旅游網(wǎng)的IM系統(tǒng)架構(gòu)演進(jìn)之路》
《從游擊隊(duì)到正規(guī)軍(二):馬蜂窩旅游網(wǎng)的IM客戶端架構(gòu)演進(jìn)和實(shí)踐總結(jié)》
《從游擊隊(duì)到正規(guī)軍(三):基于Go的馬蜂窩旅游網(wǎng)分布式IM系統(tǒng)技術(shù)實(shí)踐》(* 本文)
二、技術(shù)背景和問題
與廣義上的即時(shí)通訊不同,電商各業(yè)務(wù)線有其特有業(yè)務(wù)邏輯,如客服聊天系統(tǒng)的客人分配邏輯、敏感詞檢測邏輯等,這些往往要耦合進(jìn)通信流程中。隨著接入業(yè)務(wù)線越來越多,即時(shí)通訊服務(wù)冗余度會越來越高。同時(shí)整個消息鏈路追溯復(fù)雜,服務(wù)穩(wěn)定性很受業(yè)務(wù)邏輯的影響。
之前我們 IM 應(yīng)用中的消息推送主要基于輪詢技術(shù),消息輪詢模塊的長連接請求是通過 php-fpm 掛載在阻塞隊(duì)列上實(shí)現(xiàn)。當(dāng)請求量較大時(shí),如果不能及時(shí)釋放 php-fpm 進(jìn)程,對服務(wù)器的性能消耗很大。
為了解決這個問題,我們曾用 OpenResty+Lua 的方式進(jìn)行改造,利用 Lua 協(xié)程的方式將整體的 polling 的能力從 PHP 轉(zhuǎn)交到 Lua 處理,釋放一部 PHP 的壓力。這種方式雖然能提升一部分性能,但 PHP-Lua 的混合異構(gòu)模式,使系統(tǒng)在使用、升級、調(diào)試和維護(hù)上都很麻煩,通用性也較差,很多業(yè)務(wù)場景下還是要依賴 PHP 接口,優(yōu)化效果并不明顯。
為了解決以上問題,我們決定結(jié)合電商 IM 的特定背景對 IM 服務(wù)進(jìn)行重構(gòu),核心是實(shí)現(xiàn)業(yè)務(wù)邏輯和即時(shí)通訊服務(wù)的分離。
更多有關(guān)馬蜂窩旅游網(wǎng)的IM系統(tǒng)架構(gòu)的演進(jìn)過程,請?jiān)斪x:《從游擊隊(duì)到正規(guī)軍(一):馬蜂窩旅游網(wǎng)的IM系統(tǒng)架構(gòu)演進(jìn)之路》一文,在此不再贅述。
三、基于Go的雙層分布式IM架構(gòu)
3.1、實(shí)現(xiàn)目標(biāo)
1)業(yè)務(wù)解耦:
將業(yè)務(wù)邏輯與通信流程剝離,使 IM 服務(wù)架構(gòu)更加清晰,實(shí)現(xiàn)與電商 IM 業(yè)務(wù)邏輯的完全分離,保證服務(wù)穩(wěn)定性。
2)接入方式靈活:
之前新業(yè)務(wù)接入時(shí),需要在業(yè)務(wù)服務(wù)器上配置 OpenResty 環(huán)境及 Lua 協(xié)程代碼,非常不便,IM 服務(wù)的通用性也很差。考慮到現(xiàn)有業(yè)務(wù)的實(shí)際情況,我們希望 IM 系統(tǒng)可以提供 HTTP 和 WebSocket 兩種接入方式,供業(yè)務(wù)方根據(jù)不同的場景來靈活使用。
比如已經(jīng)接入且運(yùn)行良好的電商定制化團(tuán)隊(duì)的待辦系統(tǒng)、定制游搶單系統(tǒng)、投訴系統(tǒng)等下行相關(guān)的系統(tǒng)等,這些業(yè)務(wù)沒有明顯的高并發(fā)需求,可以通過 HTTP 方式迅速接入,不需要熟悉稍顯復(fù)雜的 WebSocket 協(xié)議,進(jìn)而降低不必要的研發(fā)成本。
3)架構(gòu)可擴(kuò)展:
為了應(yīng)對業(yè)務(wù)的持續(xù)增長給系統(tǒng)性能帶來的挑戰(zhàn),我們考慮用分布式架構(gòu)來設(shè)計(jì)即時(shí)通訊服務(wù),使系統(tǒng)具有持續(xù)擴(kuò)展及提升的能力。
3.2、語言選擇
目前,馬蜂窩技術(shù)體系主要包括 PHP,Java,Golang,技術(shù)棧比較豐富,使業(yè)務(wù)做選型時(shí)可以根據(jù)問題場景選擇更合適的工具和語言。
結(jié)合 IM 具體應(yīng)用場景,我們選擇 Go 的原因包括:
- 1)運(yùn)行性能:在性能上,尤其是針對網(wǎng)絡(luò)通信等 IO 密集型應(yīng)用場景。Go 系統(tǒng)的性能更接近 C/C++;
- 2)開發(fā)效率:Go 使用起來簡單,代碼編寫效率高,上手也很快,尤其是對于有一定 C++ 基礎(chǔ)的開發(fā)者,一周就能上手寫代碼了。
3.3、架構(gòu)設(shè)計(jì)
整體架構(gòu)圖如下:?
名詞解釋:
- 1)客戶:一般指購買商品的用戶;
- 2)商家:提供服務(wù)的供應(yīng)商,商家會有客服人員,提供給客戶一個在線咨詢的作用;
- 3)分發(fā)模塊:即 Dispatcher,提供消息分發(fā)的給指定的工作模塊的橋接作用;
- 4)工作模塊:即 Worker 服務(wù)器,用來提供 WebSocket 服務(wù),是真正工作的一個模塊。
架構(gòu)分層:
- 1)展示層:提供 HTTP 和 WebSocket 兩種接入方式;
- 2)業(yè)務(wù)層:負(fù)責(zé)初始化消息線和業(yè)務(wù)邏輯處理。如果客戶端以 HTTP 方式接入,會以 JSON 格式把消息發(fā)送給業(yè)務(wù)服務(wù)器進(jìn)行消息解碼、客服分配、敏感詞過濾,然后下發(fā)到消息分發(fā)模塊準(zhǔn)備下一步的轉(zhuǎn)換;通過 WebSocket 接入的業(yè)務(wù)則不需要消息分發(fā),直接以 WebSocket 方式發(fā)送至消息處理模塊中;
- 3)服務(wù)層:由消息分發(fā)和消息處理這兩層組成,分別以分布式的方式部署多個 Dispatcher 和 Worker 節(jié)點(diǎn)。Dispatcher 負(fù)責(zé)檢索出接收者所在的服務(wù)器位置,將消息以 RPC 的方式發(fā)送到合適的 Worker 上,再由消息處理模塊通過 WebSocket 把消息推送給客戶端;
- 4)數(shù)據(jù)層:Redis 集群,記錄用戶身份、連接信息、客戶端平臺(移動端、網(wǎng)頁端、桌面端)等組成的唯一 Key。
3.4、服務(wù)流程
步驟一:
如上圖右側(cè)所示:
用戶客戶端與消息處理模塊建立 WebSocket 長連接;
通過負(fù)載均衡算法,使客戶端連接到合適的服務(wù)器(消息處理模塊的某個 Worker);
連接成功后,記錄用戶連接信息,包括用戶角色(客人或商家)、客戶端平臺(移動端、網(wǎng)頁端、桌面端)等組成唯一 Key,記錄到 Redis 集群。
步驟二:
如圖左側(cè)所示,當(dāng)購買商品的用戶要給管家發(fā)消息的時(shí)候,先通過 HTTP 請求把消息發(fā)給業(yè)務(wù)服務(wù)器,業(yè)務(wù)服務(wù)端對消息進(jìn)行業(yè)務(wù)邏輯處理。
1)該步驟本身是一個 HTTP 請求,所以可以接入各種不同開發(fā)語言的客戶端。通過 JSON 格式把消息發(fā)送給業(yè)務(wù)服務(wù)器,業(yè)務(wù)服務(wù)器先把消息解碼,然后拿到這個用戶要發(fā)送給哪個商家的客服的。
2)如果這個購買者之前沒有聊過天,則在業(yè)務(wù)服務(wù)器邏輯里需要有一個分配客服的過程,即建立購買者和商家的客服之間的連接關(guān)系。拿到這個客服的 ID,用來做業(yè)務(wù)消息下發(fā);如果之前已經(jīng)聊過天,則略過此環(huán)節(jié)。
3)在業(yè)務(wù)服務(wù)器,消息會異步入數(shù)據(jù)庫。保證消息不會丟失。
步驟三:
業(yè)務(wù)服務(wù)端以 HTTP 請求把消息發(fā)送到消息分發(fā)模塊。這里分發(fā)模塊的作用是進(jìn)行中轉(zhuǎn),最終使服務(wù)端的消息下發(fā)給指定的商家。
步驟四:
基于 Redis 集群中的用戶連接信息,消息分發(fā)模塊將消息轉(zhuǎn)發(fā)到目標(biāo)用戶連接的 WebSocket 服務(wù)器(消息處理模塊中的某一個 Worker)
1)分發(fā)模塊通過 RPC 方式把消息轉(zhuǎn)發(fā)到目標(biāo)用戶連接的 Worker,RPC 的方式性能更快,而且傳輸?shù)臄?shù)據(jù)也少,從而節(jié)約了服務(wù)器的成本。
2)消息透傳 Worker 的時(shí)候,多種策略保障消息一定會下發(fā)到 Worker。
步驟五:
消息處理模塊將消息通過 WebSocket 協(xié)議推送到客戶端。
1)在投遞的時(shí)候,接收者要有一個 ACK(應(yīng)答) 信息來回饋給 Worker 服務(wù)器,告訴 Worker 服務(wù)器,下發(fā)的消息接收者已經(jīng)收到了。
2)如果接收者沒有發(fā)送這個 ACK 來告訴 Worker 服務(wù)器,Worker 服務(wù)器會在一定的時(shí)間內(nèi)來重新把這個信息發(fā)送給消息接收者。
3)如果投遞的信息已經(jīng)發(fā)送給客戶端,客戶端也收到了,但是因?yàn)榫W(wǎng)絡(luò)抖動,沒有把 ACK 信息發(fā)送給服務(wù)器,那服務(wù)器會重復(fù)投遞給客戶端,這時(shí)候客戶端就通過投遞過來的消息 ID 來去重展示。
以上步驟的數(shù)據(jù)流轉(zhuǎn)大致如圖所示:
3.5、系統(tǒng)完整性設(shè)計(jì)
3.5.1 可靠性
(1)消息不丟失:
為了避免消息丟失,我們設(shè)置了超時(shí)重傳機(jī)制。服務(wù)端會在推送給客戶端消息后,等待客戶端的 ACK,如果客戶端沒有返回 ACK,服務(wù)端會嘗試多次推送。
目前默認(rèn) 18s 為超時(shí)時(shí)間,重傳 3 次不成功,斷開連接,重新連接服務(wù)器。重新連接后,采用拉取歷史消息的機(jī)制來保證消息完整。
(2)多端消息同步:
客戶端現(xiàn)有 PC 瀏覽器、Windows 客戶端、H5、iOS/Android,系統(tǒng)允許用戶多端同時(shí)在線,且同一端可以多個狀態(tài),這就需要保證多端、多用戶、多狀態(tài)的消息是同步的。
我們用到了 Redis 的 Hash 存儲,將用戶信息、唯一連接對應(yīng)值 、連接標(biāo)識、客戶端 IP、服務(wù)器標(biāo)識、角色、渠道等記錄下來,這樣通過 key(uid) 就能找到一個用戶在多個端的連接,通過 key+field 能定位到一條連接。
3.5.2 可用性
上文我們已經(jīng)說過,因?yàn)槭请p層設(shè)計(jì),就涉及到兩個 Server 間的通信,同進(jìn)程內(nèi)通信用 Channel,非同進(jìn)程用消息隊(duì)列或者 RPC。綜合性能和對服務(wù)器資源利用,我們最終選擇 RPC 的方式進(jìn)行 Server 間通信。
在對基于 Go 的 RPC 進(jìn)行選行時(shí),我們比較了以下比較主流的技術(shù)方案:?
1)Go STDRPC:Go 標(biāo)準(zhǔn)庫的 RPC,性能最優(yōu),但是沒有治理;
2)RPCX:性能優(yōu)勢 2*GRPC + 服務(wù)治理;
3)GRPC:跨語言,但性能沒有 RPCX 好;
4)TarsGo:跨語言,性能 5*GRPC,缺點(diǎn)是框架較大,整合起來費(fèi)勁;
5)Dubbo-Go:性能稍遜一籌, 比較適合 Go 和 Java 間通信場景使用。
最后我們選擇了?RPCX,因?yàn)樾阅芤埠芎?#xff0c;也有服務(wù)的治理。
兩個進(jìn)程之間同樣需要通信,這里用到的是?ETCD?實(shí)現(xiàn)服務(wù)注冊發(fā)現(xiàn)機(jī)制。
當(dāng)我們新增一個 Worker,如果沒有注冊中心,就要用到配置文件來管理這些配置信息,這挺麻煩的。而且你新增一個后,需要分發(fā)模塊立刻發(fā)現(xiàn),不能有延遲。
如果有新的服務(wù),分發(fā)模塊希望能快速感知到新的服務(wù)。利用 Key 的續(xù)租機(jī)制,如果在一定時(shí)間內(nèi),沒有監(jiān)聽到 Key 有續(xù)租動作,則認(rèn)為這個服務(wù)已經(jīng)掛掉,就會把該服務(wù)摘除。
在進(jìn)行注冊中心的選型時(shí),我們主要調(diào)研了?ETCD、ZooKeeper、Consul。
三者的壓測結(jié)果參考如下:?
?
結(jié)果顯示,ETCD 的性能是最好的。另外,ETCD 背靠阿里巴巴,而且屬于 Go 生態(tài),我們公司內(nèi)部的 K8S 集群也在使用。
綜合考量后,我們選擇使用 ETCD 作為服務(wù)注冊和發(fā)現(xiàn)組件。并且我們使用的是 ETCD 的集群模式,如果一臺服務(wù)器出現(xiàn)故障,集群其他的服務(wù)器仍能正常提供服務(wù)。
小結(jié)一下:通過保證服務(wù)和進(jìn)程間的正常通訊,及 ETCD 集群模式的設(shè)計(jì),保證了 IM 服務(wù)整體具有極高的可用性。
3.5.3 擴(kuò)展性
消息分發(fā)模塊和消息處理模塊都能進(jìn)行水平擴(kuò)展。當(dāng)整體服務(wù)負(fù)載高時(shí),可以通過增加節(jié)點(diǎn)來分擔(dān)壓力,保證消息即時(shí)性和服務(wù)穩(wěn)定性。
3.5.4 安全性
處于安全性考慮,我們設(shè)置了黑名單機(jī)制,可以對單一 uid 或者 ip 進(jìn)行限制。比如在同一個 uid 下,如果一段時(shí)間內(nèi)建立的連接次數(shù)超過設(shè)定的閾值,則認(rèn)為這個 uid 可能存在風(fēng)險(xiǎn),暫停服務(wù)。如果暫停服務(wù)期間該 uid 繼續(xù)發(fā)送請求,則限制服務(wù)的時(shí)間相應(yīng)延長。
3.6、性能優(yōu)化和踩過的坑
3.6.1 性能優(yōu)化
1)JSON 編解碼:
開始我們使用官方的 JSON 編解碼工具,但由于對性能方面的追求,改為使用滴滴開源的 Json-iterator,使在兼容原生 Golang 的 JSON 編解碼工具的同時(shí),效率上有比較明顯的提升。
以下是壓測對比的參考圖:?
2)time.After:
在壓測的時(shí)候,我們發(fā)現(xiàn)內(nèi)存占用很高,于是使用 Go Tool PProf 分析 Golang 函數(shù)內(nèi)存申請情況,發(fā)現(xiàn)有不斷創(chuàng)建 time.After 定時(shí)器的問題,定位到是心跳協(xié)程里面。
原來代碼如下:?
優(yōu)化后的代碼為:
優(yōu)化點(diǎn)在于 for 循環(huán)里不要使用 select + time.After 的組合。
3)Map 的使用:
在保存連接信息的時(shí)候會用到 Map。因?yàn)橹白?TCP Socket 的項(xiàng)目的時(shí)候就遇到過一個坑,即 Map 在協(xié)程下是不安全的。當(dāng)多個協(xié)程同時(shí)對一個 Map 進(jìn)行讀寫時(shí),會拋出致命錯誤:fetal error:concurrent map read and map write,有了這個經(jīng)驗(yàn)后,我們這里用的是 sync.Map
3.6.2 踩坑經(jīng)驗(yàn)
1)協(xié)程異常:
基于對開發(fā)成本和服務(wù)穩(wěn)定性等問題的考慮,我們的 WebSocket 服務(wù)基于 Gorilla/WebSocket 框架開發(fā)。其中遇到一個問題,就是當(dāng)讀協(xié)程發(fā)生異常退出時(shí),寫協(xié)程并沒有感知到,結(jié)果就是導(dǎo)致讀協(xié)程已經(jīng)退出但是寫協(xié)程還在運(yùn)行,直到觸發(fā)異常之后才退出。
這樣雖然從表面上看不影響業(yè)務(wù)邏輯,但是浪費(fèi)后端資源。在編碼時(shí)應(yīng)該注意要在讀協(xié)程退出后主動通知寫協(xié)程,這樣一個小的優(yōu)化可以這在高并發(fā)下能節(jié)省很多資源。
2)心跳設(shè)計(jì):
舉個例子:之前我們在閑時(shí)心跳功能的開發(fā)中走了一些彎路。最初在服務(wù)器端的心跳發(fā)送是定時(shí)心跳,但后來在實(shí)際業(yè)務(wù)場景中使用時(shí)發(fā)現(xiàn),設(shè)計(jì)成服務(wù)器讀空閑時(shí)心跳更好。因?yàn)橛脩舳荚诹奶炷?#xff0c;發(fā)一個心跳幀,浪費(fèi)感情也浪費(fèi)帶寬資源。
這時(shí)候,建議大家在業(yè)務(wù)開發(fā)過程中如果代碼寫不下去就暫時(shí)不要寫了,先結(jié)合業(yè)務(wù)需求用文字梳理下邏輯,可能會發(fā)現(xiàn)之后再進(jìn)行會更順利。
3)每天分割日志:?
日志模塊在起初調(diào)研的時(shí)候基于性能考慮,確定使用 Uber 開源的?ZAP?庫,而且滿足業(yè)務(wù)日志記錄的要求。日志庫選型很重要,選不好也是影響系統(tǒng)性能和穩(wěn)定性的。
ZAP 的優(yōu)點(diǎn)包括:
1)顯示代碼行號這個需求,ZAP?支持而?Logrus?不支持,這個屬于提效的。行號展示對于定位問題很重要;
2)ZAP 相對于?Logrus?更為高效,體現(xiàn)在寫 JSON 格式日志時(shí),沒有使用反射,而是用內(nèi)建的 json encoder,通過明確的類型調(diào)用,直接拼接字符串,最小化性能開銷。
小坑:每天寫一個日志文件的功能,目前 ZAP 不支持,需要自己寫代碼支持,或者請求系統(tǒng)部支持。
四、性能表現(xiàn)
壓測 1:
上線生產(chǎn)環(huán)境并和業(yè)務(wù)方對接以及壓測,目前定制業(yè)務(wù)已接通整個流程,寫了一個 Client。模擬定期發(fā)心跳幀,然后利用 Docker 環(huán)境。開啟了 50 個容器,每個容器模擬并發(fā)起 2 萬個連接。這樣就是百萬連接打到單機(jī)的 Server 上。單機(jī)內(nèi)存占用 30G 左右。
壓測 2:
同時(shí)并發(fā) 3000、4000、5000 連接,以及調(diào)整發(fā)送頻率,分別對應(yīng)上行:60萬、80 萬、100 萬、200 萬, 一個 6k 左右的日志結(jié)構(gòu)體。
其中有一半是心跳包 另一半是日志結(jié)構(gòu)體。在不同的壓力下的下行延遲數(shù)據(jù)如下:?
結(jié)論:
隨著上行的并發(fā)變大,延遲控制在 24-66 毫秒之間。所以對于下行業(yè)務(wù)屬于輕微延遲。另外針對 60 萬 5k 上行的同時(shí),用另一個腳本模擬開啟 50 個協(xié)程并發(fā)下行 1k 的數(shù)據(jù)體,延遲是比沒有并發(fā)下行的時(shí)候是有所提高的,延遲提高了 40ms 左右。
五、本文小結(jié)
基于 Go 重構(gòu)的 IM 服務(wù)在 WebSocket 的基礎(chǔ)上,將業(yè)務(wù)層設(shè)計(jì)為配有消息分發(fā)模塊和消息處理模塊的雙層架構(gòu)模式,使業(yè)務(wù)邏輯的處理前置,保證了即時(shí)通訊服務(wù)的純粹性和穩(wěn)定性;同時(shí)消息分發(fā)模塊的 HTTP 服務(wù)方便多種編程語言快速對接,使各業(yè)務(wù)線能迅速接入即時(shí)通訊服務(wù)。
最后,我還想為 Go 搖旗吶喊一下。很多人都知道馬蜂窩技術(shù)體系主要是基于 PHP,有一些核心業(yè)務(wù)也在向 Java 遷移。與此同時(shí),Go 也在越來越多的項(xiàng)目中發(fā)揮作用。現(xiàn)在,云原生理念已經(jīng)逐漸成為主流趨勢之一,我們可以看到在很多構(gòu)建云原生應(yīng)用所需要的核心項(xiàng)目中,Go 都是主要的開發(fā)語言,比如 Kubernetes,Docker,Istio,ETCD,Prometheus 等,包括第三代開源分布式數(shù)據(jù)庫 TiDB。
所以我們可以把 Go 稱為云原生時(shí)代的母語。「云原生時(shí)代,是開發(fā)者最好的時(shí)代」,在這股浪潮下,我們越早走進(jìn) Go,就可能越早在這個新時(shí)代搶占關(guān)鍵賽道。希望更多小伙伴和我們一起,加入到 Go 的開發(fā)和學(xué)習(xí)陣營中來,拓寬自己的技能圖譜,擁抱云原生。(本文同步發(fā)布于:http://www.52im.net/thread-2909-1-1.html)
附Java/C/C++/機(jī)器學(xué)習(xí)/算法與數(shù)據(jù)結(jié)構(gòu)/前端/安卓/Python/程序員必讀/書籍書單大全:
(點(diǎn)擊右側(cè) 即可打開個人博客內(nèi)有干貨):技術(shù)干貨小棧
=====>>①【Java大牛帶你入門到進(jìn)階之路】<<====
=====>>②【算法數(shù)據(jù)結(jié)構(gòu)+acm大牛帶你入門到進(jìn)階之路】<<===
=====>>③【數(shù)據(jù)庫大牛帶你入門到進(jìn)階之路】<<=====
=====>>④【W(wǎng)eb前端大牛帶你入門到進(jìn)階之路】<<====
=====>>⑤【機(jī)器學(xué)習(xí)和python大牛帶你入門到進(jìn)階之路】<<====
=====>>⑥【架構(gòu)師大牛帶你入門到進(jìn)階之路】<<=====
=====>>⑦【C++大牛帶你入門到進(jìn)階之路】<<====
=====>>⑧【ios大牛帶你入門到進(jìn)階之路】<<====
=====>>⑨【W(wǎng)eb安全大牛帶你入門到進(jìn)階之路】<<=====
=====>>⑩【Linux和操作系統(tǒng)大牛帶你入門到進(jìn)階之路】<<=====
天下沒有不勞而獲的果實(shí),望各位年輕的朋友,想學(xué)技術(shù)的朋友,在決心扎入技術(shù)道路的路上披荊斬棘,把書弄懂了,再去敲代碼,把原理弄懂了,再去實(shí)踐,將會帶給你的人生,你的工作,你的未來一個美夢。
總結(jié)
以上是生活随笔為你收集整理的基于Go的马蜂窝旅游网分布式IM系统技术实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高等数学(第七版)同济大学 总习题六 个
- 下一篇: 【操作系统⑩】——进程死锁【银行家算法+