引子
現(xiàn)在在公司的一項(xiàng)工作是負(fù)責(zé)IM系統(tǒng)的長(zhǎng)連接,我們的長(zhǎng)連接系統(tǒng)是用C實(shí)現(xiàn)的,事件驅(qū)動(dòng)使用的是libevent,有一次和另一個(gè)朋友交流,他們說(shuō)他們的長(zhǎng)連接是基于netty實(shí)現(xiàn)的,實(shí)現(xiàn)起來(lái)比我們的要簡(jiǎn)單方便很多,當(dāng)時(shí)就想后面有時(shí)間就比較一下這兩種實(shí)現(xiàn)方案。
其實(shí)這倆本來(lái)是沒(méi)有可比性的,所以本文對(duì)比的是netty+java實(shí)現(xiàn)的長(zhǎng)連接和libevent+c實(shí)現(xiàn)的長(zhǎng)連接系統(tǒng)的對(duì)比。
長(zhǎng)連接系統(tǒng)簡(jiǎn)介
IM的長(zhǎng)連接是指用戶端和后臺(tái)建立的一條較長(zhǎng)時(shí)間存在的信息通路,起到的最大作用是消息的實(shí)時(shí)觸達(dá)。當(dāng)后臺(tái)有消息的時(shí)候,會(huì)通過(guò)這個(gè)通路實(shí)時(shí)的發(fā)送給用戶。
我們的用戶端有app端,js端,pc端,為了能兼容所有的設(shè)備和瀏覽器,我們初始的協(xié)議選擇了http協(xié)議,使用的是long polling的方式,后來(lái)協(xié)議增加了websocket協(xié)議,在高級(jí)別瀏覽器和微信小程序能提供更好的體驗(yàn)。
內(nèi)存對(duì)比
基于libevent+C實(shí)現(xiàn)
基本數(shù)據(jù)結(jié)構(gòu)
- session數(shù)組
- 每個(gè)鏈接的動(dòng)態(tài)buffer
session數(shù)組的索引是fd,存儲(chǔ)的是session對(duì)象,session對(duì)象主要包括一個(gè)鏈接的客戶端ip,登錄時(shí)間,讀和寫(xiě)的buffer指針,讀和寫(xiě)event以及創(chuàng)建時(shí)間等。每個(gè)session對(duì)象416B,由于使用的是數(shù)組,我們根據(jù)系統(tǒng)的最大承受量,預(yù)留了100w個(gè)session對(duì)象,所以占用空間416M。
每個(gè)鏈接的讀buffer是為了解決tcp的粘包問(wèn)題,即讀到一個(gè)完整的業(yè)務(wù)包才能交到下一個(gè)環(huán)節(jié)處理。
每個(gè)鏈接的寫(xiě)buffer是為了解決tcp的鏈接不可寫(xiě)的問(wèn)題,因?yàn)槭褂玫氖钱惒讲僮?,有?shù)據(jù)的時(shí)候,有可能tcp鏈接并不可寫(xiě),這時(shí)候就緩存在寫(xiě)buffer里面,可寫(xiě)的時(shí)候再寫(xiě)。
基于netty+java實(shí)現(xiàn)
netty中一個(gè)鏈接是一個(gè)channel,每一個(gè)channel會(huì)有一個(gè)DefaultPipeline,然后會(huì)有一個(gè)HeadContext和TailContext以及Unsafe對(duì)象。java對(duì)象比較多,所以java占用的內(nèi)存會(huì)多一些。
netty實(shí)現(xiàn)的長(zhǎng)連接的內(nèi)存會(huì)有一個(gè)gc的問(wèn)題,因?yàn)殚L(zhǎng)連接的鏈接相關(guān)對(duì)象的存活時(shí)間是和鏈接相同聲明周期的,那么會(huì)有很多內(nèi)存存儲(chǔ)在老年代,會(huì)導(dǎo)致fgc的時(shí)間比較長(zhǎng)。
線程管理(CPU)對(duì)比
基于libevent+C實(shí)現(xiàn)
因?yàn)槲覀兪褂玫氖羌儺惒降脑O(shè)計(jì),所以我們首先申請(qǐng)了和cpu核數(shù)一樣多的線程數(shù),然后通過(guò)fd取模得到對(duì)應(yīng)的線程索引,那么這個(gè)鏈接的所有讀寫(xiě)都是這個(gè)線程負(fù)責(zé)的。(accept的線程后面會(huì)單說(shuō))
基于netty+java實(shí)現(xiàn)
netty的EventLoopGroup的一個(gè)實(shí)現(xiàn)是MultithreadEventLoopGroup,這個(gè)里面使用多個(gè)線程,每一個(gè)線程都是一個(gè)NioEventLoop,channel選擇EventLoop的方式是輪詢,所以每一個(gè)channel里有一個(gè)引用指向這個(gè)NioEventLoop,這個(gè)channel的所有讀寫(xiě)都是這個(gè)NioEventLoop完成的(包括accept的線程)。
線程交互
線程交互指的是一個(gè)鏈接的要給另一個(gè)鏈接發(fā)送數(shù)據(jù),而這兩個(gè)鏈接又有可能屬于不同的線程,所以存在線程交互的問(wèn)題。
基于libevent+C實(shí)現(xiàn)
libevent是基于事件驅(qū)動(dòng)的,所以我們引入了pipe,線程之間交互是通過(guò)pipe的讀事件通知的,當(dāng)一個(gè)線程管理的鏈接需要給另一個(gè)線程管理的鏈接發(fā)送數(shù)據(jù)的時(shí)候,就通過(guò)pipe通知另一個(gè)線程。pipe有一個(gè)比較好的特性是write接口原子性,即發(fā)送4096的數(shù)據(jù)是原子的,就是多個(gè)線程同時(shí)寫(xiě)的時(shí)候不會(huì)出現(xiàn)一個(gè)線程寫(xiě)一半數(shù)據(jù),然后另一個(gè)線程寫(xiě)一半數(shù)據(jù)的情況。
基于netty+java的實(shí)現(xiàn)
netty的實(shí)現(xiàn)沒(méi)有使用事件的機(jī)制,而是加入了一個(gè)隊(duì)列,同一個(gè)線程在處理完nio的事件后會(huì)檢查一下隊(duì)列是否有數(shù)據(jù)要發(fā),因?yàn)閚io在沒(méi)有數(shù)據(jù)的時(shí)候是會(huì)block的,所以需要wakeup一下。
accept線程優(yōu)化
長(zhǎng)連接是一個(gè)外網(wǎng)的系統(tǒng),那么不能避免的會(huì)有攻擊或者垃圾流量過(guò)來(lái),所以有可能鏈接的請(qǐng)求特別多,在我們的系統(tǒng)里面,accept的線程的cpu占用是其他線程的2倍。
基于libevent+C的優(yōu)化
給同一個(gè)fd(accept socket的fd)分配多個(gè)event,然后綁定到多個(gè)線程,這樣cpu就比較均勻了,缺點(diǎn)是:同一個(gè)event會(huì)有多個(gè)線程同時(shí)相應(yīng),但只有一個(gè)socket能accept到結(jié)果。
nginx的優(yōu)化
nginx的accept的fd不是固定到一個(gè)線程管理的,哪個(gè)線程空閑就那個(gè)線程管理,這樣也能使cpu比較均勻。
實(shí)現(xiàn)復(fù)雜度
上面提到了內(nèi)存管理(分為動(dòng)態(tài)和靜態(tài)兩部分)、線程分布、線程交互和accept優(yōu)化四個(gè)方面,netty幾乎都已經(jīng)比較好的解決了,所以如果基于netty和java實(shí)現(xiàn)的話,實(shí)現(xiàn)復(fù)雜程度就會(huì)簡(jiǎn)單很多,如果用libevent和c來(lái)實(shí)現(xiàn),都需要自己來(lái)實(shí)現(xiàn),實(shí)現(xiàn)復(fù)雜程度陡增。
總結(jié)
上面從內(nèi)存、CPU、線程交互、accpet線程優(yōu)化以及實(shí)現(xiàn)復(fù)雜度五個(gè)方面比較了基于netty和基于libevent+c的實(shí)現(xiàn),總結(jié)起來(lái):
- 內(nèi)存上c有優(yōu)勢(shì),特別是長(zhǎng)連接這樣的系統(tǒng),大部分內(nèi)存都會(huì)進(jìn)入java的老年代
- accept線程優(yōu)化方面,netty并沒(méi)有很好的考慮
- 實(shí)現(xiàn)復(fù)雜程度,netty+java的方案簡(jiǎn)單很多,對(duì)并發(fā)連接數(shù)不是很多的系統(tǒng),可以優(yōu)先考慮