Netty 系列之 Netty 百萬(wàn)級(jí)推送服務(wù)設(shè)計(jì)要點(diǎn)(轉(zhuǎn))

1. 背景

1.1. 話題來(lái)源

最近很多從事移動(dòng)互聯(lián)網(wǎng)和物聯(lián)網(wǎng)開(kāi)發(fā)的同學(xué)給我發(fā)郵件或者微博私信我,咨詢推送服務(wù)相關(guān)的問(wèn)題毁菱。問(wèn)題五花八門(mén)矾瑰,在幫助大家答疑解惑的過(guò)程中,我也對(duì)問(wèn)題進(jìn)行了總結(jié)侣诵,大概可以歸納為如下幾類(lèi):

  1. Netty 是否可以做推送服務(wù)器?
  2. 如果使用 Netty 開(kāi)發(fā)推送服務(wù)恬试,一個(gè)服務(wù)器最多可以支撐多少個(gè)客戶端窝趣?
  3. 使用 Netty 開(kāi)發(fā)推送服務(wù)遇到的各種技術(shù)問(wèn)題疯暑。

由于咨詢者眾多训柴,關(guān)注點(diǎn)也比較集中,我希望通過(guò)本文的案例分析和對(duì)推送服務(wù)設(shè)計(jì)要點(diǎn)的總結(jié)妇拯,幫助大家在實(shí)際工作中少走彎路幻馁。

1.2. 推送服務(wù)

移動(dòng)互聯(lián)網(wǎng)時(shí)代,推送 (Push) 服務(wù)成為 App 應(yīng)用不可或缺的重要組成部分越锈,推送服務(wù)可以提升用戶的活躍度和留存率仗嗦。我們的手機(jī)每天接收到各種各樣的廣告和提示消息等大多數(shù)都是通過(guò)推送服務(wù)實(shí)現(xiàn)的。

隨著物聯(lián)網(wǎng)的發(fā)展甘凭,大多數(shù)的智能家居都支持移動(dòng)推送服務(wù)稀拐,未來(lái)所有接入物聯(lián)網(wǎng)的智能設(shè)備都將是推送服務(wù)的客戶端,這就意味著推送服務(wù)未來(lái)會(huì)面臨海量的設(shè)備和終端接入丹弱。

1.3. 推送服務(wù)的特點(diǎn)

移動(dòng)推送服務(wù)的主要特點(diǎn)如下:

  1. 使用的網(wǎng)絡(luò)主要是運(yùn)營(yíng)商的無(wú)線移動(dòng)網(wǎng)絡(luò)德撬,網(wǎng)絡(luò)質(zhì)量不穩(wěn)定,例如在地鐵上信號(hào)就很差躲胳,容易發(fā)生網(wǎng)絡(luò)閃斷蜓洪;
  2. 海量的客戶端接入,而且通常使用長(zhǎng)連接坯苹,無(wú)論是客戶端還是服務(wù)端隆檀,資源消耗都非常大;
  3. 由于谷歌的推送框架無(wú)法在國(guó)內(nèi)使用,Android 的長(zhǎng)連接是由每個(gè)應(yīng)用各自維護(hù)的恐仑,這就意味著每臺(tái)安卓設(shè)備上會(huì)存在多個(gè)長(zhǎng)連接泉坐。即便沒(méi)有消息需要推送,長(zhǎng)連接本身的心跳消息量也是非常巨大的裳仆,這就會(huì)導(dǎo)致流量和耗電量的增加坚冀;
  4. 不穩(wěn)定:消息丟失、重復(fù)推送鉴逞、延遲送達(dá)记某、過(guò)期推送時(shí)有發(fā)生;
  5. 垃圾消息滿天飛构捡,缺乏統(tǒng)一的服務(wù)治理能力液南。

為了解決上述弊端,一些企業(yè)也給出了自己的解決方案勾徽,例如京東云推出的推送服務(wù)滑凉,可以實(shí)現(xiàn)多應(yīng)用單服務(wù)單連接模式,使用 AlarmManager 定時(shí)心跳節(jié)省電量和流量喘帚。

2. 智能家居領(lǐng)域的一個(gè)真實(shí)案例

2.1. 問(wèn)題描述

智能家居 MQTT 消息服務(wù)中間件畅姊,保持 10 萬(wàn)用戶在線長(zhǎng)連接,2 萬(wàn)用戶并發(fā)做消息請(qǐng)求吹由。程序運(yùn)行一段時(shí)間之后若未,發(fā)現(xiàn)內(nèi)存泄露,懷疑是 Netty 的 Bug倾鲫。其它相關(guān)信息如下:

  1. MQTT 消息服務(wù)中間件服務(wù)器內(nèi)存 16G粗合,8 個(gè)核心 CPU;
  2. Netty 中 boss 線程池大小為 1乌昔,worker 線程池大小為 6隙疚,其余線程分配給業(yè)務(wù)使用。該分配方式后來(lái)調(diào)整為 worker 線程池大小為 11磕道,問(wèn)題依舊供屉;
  3. Netty 版本為 4.0.8.Final。

2.2. 問(wèn)題定位

首先需要 dump 內(nèi)存堆棧溺蕉,對(duì)疑似內(nèi)存泄露的對(duì)象和引用關(guān)系進(jìn)行分析伶丐,如下所示:

image

我們發(fā)現(xiàn) Netty 的 ScheduledFutureTask 增加了 9076%,達(dá)到 110W 個(gè)左右的實(shí)例焙贷,通過(guò)對(duì)業(yè)務(wù)代碼的分析發(fā)現(xiàn)用戶使用 IdleStateHandler 用于在鏈路空閑時(shí)進(jìn)行業(yè)務(wù)邏輯處理撵割,但是空閑時(shí)間設(shè)置的比較大,為 15 分鐘辙芍。

Netty 的 IdleStateHandler 會(huì)根據(jù)用戶的使用場(chǎng)景啡彬,啟動(dòng)三類(lèi)定時(shí)任務(wù)羹与,分別是:ReaderIdleTimeoutTask、WriterIdleTimeoutTask 和 AllIdleTimeoutTask庶灿,它們都會(huì)被加入到 NioEventLoop 的 Task 隊(duì)列中被調(diào)度和執(zhí)行纵搁。

由于超時(shí)時(shí)間過(guò)長(zhǎng),10W 個(gè)長(zhǎng)鏈接鏈路會(huì)創(chuàng)建 10W 個(gè) ScheduledFutureTask 對(duì)象往踢,每個(gè)對(duì)象還保存有業(yè)務(wù)的成員變量腾誉,非常消耗內(nèi)存。用戶的持久代設(shè)置的比較大峻呕,一些定時(shí)任務(wù)被老化到持久代中利职,沒(méi)有被 JVM 垃圾回收掉,內(nèi)存一直在增長(zhǎng)瘦癌,用戶誤認(rèn)為存在內(nèi)存泄露猪贪。

事實(shí)上,我們進(jìn)一步分析發(fā)現(xiàn)讯私,用戶的超時(shí)時(shí)間設(shè)置的非常不合理热押,15 分鐘的超時(shí)達(dá)不到設(shè)計(jì)目標(biāo),重新設(shè)計(jì)之后將超時(shí)時(shí)間設(shè)置為 45 秒斤寇,內(nèi)存可以正惩把ⅲ回收,問(wèn)題解決娘锁。

2.3. 問(wèn)題總結(jié)

如果是 100 個(gè)長(zhǎng)連接牙寞,即便是長(zhǎng)周期的定時(shí)任務(wù),也不存在內(nèi)存泄露問(wèn)題致盟,在新生代通過(guò) minor GC 就可以實(shí)現(xiàn)內(nèi)存回收碎税。正是因?yàn)槭f(wàn)級(jí)的長(zhǎng)連接尤慰,導(dǎo)致小問(wèn)題被放大馏锡,引出了后續(xù)的各種問(wèn)題。

事實(shí)上伟端,如果用戶確實(shí)有長(zhǎng)周期運(yùn)行的定時(shí)任務(wù)杯道,該如何處理?對(duì)于海量長(zhǎng)連接的推送服務(wù)责蝠,代碼處理稍有不慎党巾,就滿盤(pán)皆輸,下面我們針對(duì) Netty 的架構(gòu)特點(diǎn)霜医,介紹下如何使用 Netty 實(shí)現(xiàn)百萬(wàn)級(jí)客戶端的推送服務(wù)齿拂。

3. Netty 海量推送服務(wù)設(shè)計(jì)要點(diǎn)

作為高性能的 NIO 框架,利用 Netty 開(kāi)發(fā)高效的推送服務(wù)技術(shù)上是可行的肴敛,但是由于推送服務(wù)自身的復(fù)雜性署海,想要開(kāi)發(fā)出穩(wěn)定吗购、高性能的推送服務(wù)并非易事,需要在設(shè)計(jì)階段針對(duì)推送服務(wù)的特點(diǎn)進(jìn)行合理設(shè)計(jì)砸狞。

3.1. 最大句柄數(shù)修改

百萬(wàn)長(zhǎng)連接接入捻勉,首先需要優(yōu)化的就是 Linux 內(nèi)核參數(shù),其中 Linux 最大文件句柄數(shù)是最重要的調(diào)優(yōu)參數(shù)之一刀森,默認(rèn)單進(jìn)程打開(kāi)的最大句柄數(shù)是 1024踱启,通過(guò) ulimit -a 可以查看相關(guān)參數(shù),示例如下:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">[root@lilinfeng ~]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 256324
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024

...... 后續(xù)輸出省略</pre>

當(dāng)單個(gè)推送服務(wù)接收到的鏈接超過(guò)上限后研底,就會(huì)報(bào)“too many open files”埠偿,所有新的客戶端接入將失敗。

通過(guò) vi /etc/security/limits.conf 添加如下配置參數(shù):修改之后保存榜晦,注銷(xiāo)當(dāng)前用戶胚想,重新登錄,通過(guò) ulimit -a 查看修改的狀態(tài)是否生效芽隆。

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">* soft  nofile  1000000

  • hard  nofile  1000000</pre>

需要指出的是浊服,盡管我們可以將單個(gè)進(jìn)程打開(kāi)的最大句柄數(shù)修改的非常大,但是當(dāng)句柄數(shù)達(dá)到一定數(shù)量級(jí)之后胚吁,處理效率將出現(xiàn)明顯下降牙躺,因此,需要根據(jù)服務(wù)器的硬件配置和處理能力進(jìn)行合理設(shè)置腕扶。如果單個(gè)服務(wù)器性能不行也可以通過(guò)集群的方式實(shí)現(xiàn)孽拷。

3.2. 當(dāng)心 CLOSE_WAIT

從事移動(dòng)推送服務(wù)開(kāi)發(fā)的同學(xué)可能都有體會(huì),移動(dòng)無(wú)線網(wǎng)絡(luò)可靠性非常差半抱,經(jīng)常存在客戶端重置連接脓恕,網(wǎng)絡(luò)閃斷等。

在百萬(wàn)長(zhǎng)連接的推送系統(tǒng)中窿侈,服務(wù)端需要能夠正確處理這些網(wǎng)絡(luò)異常炼幔,設(shè)計(jì)要點(diǎn)如下:

  1. 客戶端的重連間隔需要合理設(shè)置,防止連接過(guò)于頻繁導(dǎo)致的連接失斒芳颉(例如端口還沒(méi)有被釋放)乃秀;
  2. 客戶端重復(fù)登陸拒絕機(jī)制;
  3. 服務(wù)端正確處理 I/O 異常和解碼異常等圆兵,防止句柄泄露跺讯。

最后特別需要注意的一點(diǎn)就是 close_wait 過(guò)多問(wèn)題,由于網(wǎng)絡(luò)不穩(wěn)定經(jīng)常會(huì)導(dǎo)致客戶端斷連殉农,如果服務(wù)端沒(méi)有能夠及時(shí)關(guān)閉 socket刀脏,就會(huì)導(dǎo)致處于 close_wait 狀態(tài)的鏈路過(guò)多。close_wait 狀態(tài)的鏈路并不釋放句柄和內(nèi)存等資源超凳,如果積壓過(guò)多可能會(huì)導(dǎo)致系統(tǒng)句柄耗盡愈污,發(fā)生“Too many open files”異常危队,新的客戶端無(wú)法接入,涉及創(chuàng)建或者打開(kāi)句柄的操作都將失敗钙畔。

下面對(duì) close_wait 狀態(tài)進(jìn)行下簡(jiǎn)單介紹茫陆,被動(dòng)關(guān)閉 TCP 連接狀態(tài)遷移圖如下所示:

image

圖 3-1 被動(dòng)關(guān)閉 TCP 連接狀態(tài)遷移圖

close_wait 是被動(dòng)關(guān)閉連接是形成的,根據(jù) TCP 狀態(tài)機(jī)擎析,服務(wù)器端收到客戶端發(fā)送的 FIN簿盅,TCP 協(xié)議棧會(huì)自動(dòng)發(fā)送 ACK,鏈接進(jìn)入 close_wait 狀態(tài)揍魂。但如果服務(wù)器端不執(zhí)行 socket 的 close() 操作桨醋,狀態(tài)就不能由 close_wait 遷移到 last_ack,則系統(tǒng)中會(huì)存在很多 close_wait 狀態(tài)的連接现斋。通常來(lái)說(shuō)喜最,一個(gè) close_wait 會(huì)維持至少 2 個(gè)小時(shí)的時(shí)間(系統(tǒng)默認(rèn)超時(shí)時(shí)間的是 7200 秒,也就是 2 小時(shí))庄蹋。如果服務(wù)端程序因某個(gè)原因?qū)е孪到y(tǒng)造成一堆 close_wait 消耗資源瞬内,那么通常是等不到釋放那一刻,系統(tǒng)就已崩潰限书。

導(dǎo)致 close_wait 過(guò)多的可能原因如下:

  1. 程序處理 Bug虫蝶,導(dǎo)致接收到對(duì)方的 fin 之后沒(méi)有及時(shí)關(guān)閉 socket,這可能是 Netty 的 Bug倦西,也可能是業(yè)務(wù)層 Bug能真,需要具體問(wèn)題具體分析;
  2. 關(guān)閉 socket 不及時(shí):例如 I/O 線程被意外阻塞扰柠,或者 I/O 線程執(zhí)行的用戶自定義 Task 比例過(guò)高粉铐,導(dǎo)致 I/O 操作處理不及時(shí),鏈路不能被及時(shí)釋放卤档。

下面我們結(jié)合 Netty 的原理蝙泼,對(duì)潛在的故障點(diǎn)進(jìn)行分析。

設(shè)計(jì)要點(diǎn) 1:不要在 Netty 的 I/O 線程上處理業(yè)務(wù)(心跳發(fā)送和檢測(cè)除外)裆装。Why? 對(duì)于 Java 進(jìn)程踱承,線程不能無(wú)限增長(zhǎng),這就意味著 Netty 的 Reactor 線程數(shù)必須收斂哨免。Netty 的默認(rèn)值是 CPU 核數(shù) * 2,通常情況下昙沦,I/O 密集型應(yīng)用建議線程數(shù)盡量設(shè)置大些琢唾,但這主要是針對(duì)傳統(tǒng)同步 I/O 而言,對(duì)于非阻塞 I/O盾饮,線程數(shù)并不建議設(shè)置太大采桃,盡管沒(méi)有最優(yōu)值懒熙,但是 I/O 線程數(shù)經(jīng)驗(yàn)值是 [CPU 核數(shù) + 1,CPU 核數(shù) *2 ] 之間普办。

假如單個(gè)服務(wù)器支撐 100 萬(wàn)個(gè)長(zhǎng)連接工扎,服務(wù)器內(nèi)核數(shù)為 32,則單個(gè) I/O 線程處理的鏈接數(shù) L = 100/(32 * 2) = 15625衔蹲。 假如每 5S 有一次消息交互(新消息推送肢娘、心跳消息和其它管理消息),則平均 CAPS = 15625 / 5 = 3125 條 / 秒舆驶。這個(gè)數(shù)值相比于 Netty 的處理性能而言壓力并不大橱健,但是在實(shí)際業(yè)務(wù)處理中,經(jīng)常會(huì)有一些額外的復(fù)雜邏輯處理沙廉,例如性能統(tǒng)計(jì)拘荡、記錄接口日志等,這些業(yè)務(wù)操作性能開(kāi)銷(xiāo)也比較大撬陵,如果在 I/O 線程上直接做業(yè)務(wù)邏輯處理珊皿,可能會(huì)阻塞 I/O 線程,影響對(duì)其它鏈路的讀寫(xiě)操作巨税,這就會(huì)導(dǎo)致被動(dòng)關(guān)閉的鏈路不能及時(shí)關(guān)閉亮隙,造成 close_wait 堆積。

設(shè)計(jì)要點(diǎn) 2:在 I/O 線程上執(zhí)行自定義 Task 要當(dāng)心垢夹。Netty 的 I/O 處理線程 NioEventLoop 支持兩種自定義 Task 的執(zhí)行:

  1. 普通的 Runnable: 通過(guò)調(diào)用 NioEventLoop 的 execute(Runnable task) 方法執(zhí)行溢吻;
  2. 定時(shí)任務(wù) ScheduledFutureTask: 通過(guò)調(diào)用 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit) 系列接口執(zhí)行。

為什么 NioEventLoop 要支持用戶自定義 Runnable 和 ScheduledFutureTask 的執(zhí)行果元,并不是本文要討論的重點(diǎn)促王,后續(xù)會(huì)有專(zhuān)題文章進(jìn)行介紹。本文重點(diǎn)對(duì)它們的影響進(jìn)行分析而晒。

在 NioEventLoop 中執(zhí)行 Runnable 和 ScheduledFutureTask蝇狼,意味著允許用戶在 NioEventLoop 中執(zhí)行非 I/O 操作類(lèi)的業(yè)務(wù)邏輯,這些業(yè)務(wù)邏輯通常用消息報(bào)文的處理和協(xié)議管理相關(guān)倡怎。它們的執(zhí)行會(huì)搶占 NioEventLoop I/O 讀寫(xiě)的 CPU 時(shí)間迅耘,如果用戶自定義 Task 過(guò)多,或者單個(gè) Task 執(zhí)行周期過(guò)長(zhǎng)监署,會(huì)導(dǎo)致 I/O 讀寫(xiě)操作被阻塞颤专,這樣也間接導(dǎo)致 close_wait 堆積。

所以钠乏,如果用戶在代碼中使用到了 Runnable 和 ScheduledFutureTask栖秕,請(qǐng)合理設(shè)置 ioRatio 的比例,通過(guò) NioEventLoop 的 setIoRatio(int ioRatio) 方法可以設(shè)置該值晓避,默認(rèn)值為 50簇捍,即 I/O 操作和用戶自定義任務(wù)的執(zhí)行時(shí)間比為 1:1只壳。

我的建議是當(dāng)服務(wù)端處理海量客戶端長(zhǎng)連接的時(shí)候,不要在 NioEventLoop 中執(zhí)行自定義 Task暑塑,或者非心跳類(lèi)的定時(shí)任務(wù)吼句。

設(shè)計(jì)要點(diǎn) 3:IdleStateHandler 使用要當(dāng)心。很多用戶會(huì)使用 IdleStateHandler 做心跳發(fā)送和檢測(cè)事格,這種用法值得提倡惕艳。相比于自己?jiǎn)⒍〞r(shí)任務(wù)發(fā)送心跳,這種方式更高效分蓖。但是在實(shí)際開(kāi)發(fā)中需要注意的是尔艇,在心跳的業(yè)務(wù)邏輯處理中,無(wú)論是正常還是異常場(chǎng)景么鹤,處理時(shí)延要可控终娃,防止時(shí)延不可控導(dǎo)致的 NioEventLoop 被意外阻塞。例如蒸甜,心跳超時(shí)或者發(fā)生 I/O 異常時(shí)棠耕,業(yè)務(wù)調(diào)用 Email 發(fā)送接口告警,由于 Email 服務(wù)端處理超時(shí)柠新,導(dǎo)致郵件發(fā)送客戶端被阻塞窍荧,級(jí)聯(lián)引起 IdleStateHandler 的 AllIdleTimeoutTask 任務(wù)被阻塞,最終 NioEventLoop 多路復(fù)用器上其它的鏈路讀寫(xiě)被阻塞恨憎。

對(duì)于 ReadTimeoutHandler 和 WriteTimeoutHandler蕊退,約束同樣存在。

3.3. 合理的心跳周期

百萬(wàn)級(jí)的推送服務(wù)憔恳,意味著會(huì)存在百萬(wàn)個(gè)長(zhǎng)連接瓤荔,每個(gè)長(zhǎng)連接都需要靠和 App 之間的心跳來(lái)維持鏈路。合理設(shè)置心跳周期是非常重要的工作钥组,推送服務(wù)的心跳周期設(shè)置需要考慮移動(dòng)無(wú)線網(wǎng)絡(luò)的特點(diǎn)输硝。

當(dāng)一臺(tái)智能手機(jī)連上移動(dòng)網(wǎng)絡(luò)時(shí),其實(shí)并沒(méi)有真正連接上 Internet程梦,運(yùn)營(yíng)商分配給手機(jī)的 IP 其實(shí)是運(yùn)營(yíng)商的內(nèi)網(wǎng) IP点把,手機(jī)終端要連接上 Internet 還必須通過(guò)運(yùn)營(yíng)商的網(wǎng)關(guān)進(jìn)行 IP 地址的轉(zhuǎn)換,這個(gè)網(wǎng)關(guān)簡(jiǎn)稱(chēng)為 NAT(NetWork Address Translation)屿附,簡(jiǎn)單來(lái)說(shuō)就是手機(jī)終端連接 Internet 其實(shí)就是移動(dòng)內(nèi)網(wǎng) IP郎逃,端口,外網(wǎng) IP 之間相互映射拿撩。

GGSN(GateWay GPRS Support Note) 模塊就實(shí)現(xiàn)了 NAT 功能衣厘,由于大部分的移動(dòng)無(wú)線網(wǎng)絡(luò)運(yùn)營(yíng)商為了減少網(wǎng)關(guān) NAT 映射表的負(fù)荷,如果一個(gè)鏈路有一段時(shí)間沒(méi)有通信時(shí)就會(huì)刪除其對(duì)應(yīng)表压恒,造成鏈路中斷影暴,正是這種刻意縮短空閑連接的釋放超時(shí),原本是想節(jié)省信道資源的作用探赫,沒(méi)想到讓互聯(lián)網(wǎng)的應(yīng)用不得以遠(yuǎn)高于正常頻率發(fā)送心跳來(lái)維護(hù)推送的長(zhǎng)連接型宙。以中移動(dòng)的 2.5G 網(wǎng)絡(luò)為例,大約 5 分鐘左右的基帶空閑伦吠,連接就會(huì)被釋放妆兑。

由于移動(dòng)無(wú)線網(wǎng)絡(luò)的特點(diǎn),推送服務(wù)的心跳周期并不能設(shè)置的太長(zhǎng)毛仪,否則長(zhǎng)連接會(huì)被釋放搁嗓,造成頻繁的客戶端重連,但是也不能設(shè)置太短箱靴,否則在當(dāng)前缺乏統(tǒng)一心跳框架的機(jī)制下很容易導(dǎo)致信令風(fēng)暴(例如微信心跳信令風(fēng)暴問(wèn)題)腺逛。具體的心跳周期并沒(méi)有統(tǒng)一的標(biāo)準(zhǔn),180S 也許是個(gè)不錯(cuò)的選擇衡怀,微信為 300S棍矛。

在 Netty 中,可以通過(guò)在 ChannelPipeline 中增加 IdleStateHandler 的方式實(shí)現(xiàn)心跳檢測(cè)抛杨,在構(gòu)造函數(shù)中指定鏈路空閑時(shí)間够委,然后實(shí)現(xiàn)空閑回調(diào)接口,實(shí)現(xiàn)心跳的發(fā)送和檢測(cè)怖现,代碼如下:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public void initChannel({@link Channel} channel) {
channel.pipeline().addLast("idleStateHandler", new {@link IdleStateHandler}(0, 0, 180));
channel.pipeline().addLast("myHandler", new MyHandler());
}
攔截鏈路空閑事件并處理心跳:
public class MyHandler extends {@link ChannelHandlerAdapter} {
{@code @Override}
public void userEventTriggered({@link ChannelHandlerContext} ctx, {@link Object} evt) throws {@link Exception} {
if (evt instanceof {@link IdleStateEvent}} {
// 心跳處理
}
}
}</pre>

3.4. 合理設(shè)置接收和發(fā)送緩沖區(qū)容量

對(duì)于長(zhǎng)鏈接茁帽,每個(gè)鏈路都需要維護(hù)自己的消息接收和發(fā)送緩沖區(qū),JDK 原生的 NIO 類(lèi)庫(kù)使用的是 java.nio.ByteBuffer, 它實(shí)際是一個(gè)長(zhǎng)度固定的 Byte 數(shù)組屈嗤,我們都知道數(shù)組無(wú)法動(dòng)態(tài)擴(kuò)容潘拨,ByteBuffer 也有這個(gè)限制,相關(guān)代碼如下:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public abstract class ByteBuffer
extends Buffer
implements Comparable <bytebuffer>{
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly;</bytebuffer></pre>

容量無(wú)法動(dòng)態(tài)擴(kuò)展會(huì)給用戶帶來(lái)一些麻煩恢共,例如由于無(wú)法預(yù)測(cè)每條消息報(bào)文的長(zhǎng)度战秋,可能需要預(yù)分配一個(gè)比較大的 ByteBuffer,這通常也沒(méi)有問(wèn)題讨韭。但是在海量推送服務(wù)系統(tǒng)中脂信,這會(huì)給服務(wù)端帶來(lái)沉重的內(nèi)存負(fù)擔(dān)。假設(shè)單條推送消息最大上限為 10K透硝,消息平均大小為 5K狰闪,為了滿足 10K 消息的處理,ByteBuffer 的容量被設(shè)置為 10K濒生,這樣每條鏈路實(shí)際上多消耗了 5K 內(nèi)存埋泵,如果長(zhǎng)鏈接鏈路數(shù)為 100 萬(wàn),每個(gè)鏈路都獨(dú)立持有 ByteBuffer 接收緩沖區(qū),則額外損耗的總內(nèi)存 Total(M) = 1000000 * 5K = 4882M丽声。內(nèi)存消耗過(guò)大礁蔗,不僅僅增加了硬件成本,而且大內(nèi)存容易導(dǎo)致長(zhǎng)時(shí)間的 Full GC雁社,對(duì)系統(tǒng)穩(wěn)定性會(huì)造成比較大的沖擊浴井。

實(shí)際上,最靈活的處理方式就是能夠動(dòng)態(tài)調(diào)整內(nèi)存霉撵,即接收緩沖區(qū)可以根據(jù)以往接收的消息進(jìn)行計(jì)算磺浙,動(dòng)態(tài)調(diào)整內(nèi)存,利用 CPU 資源來(lái)?yè)Q內(nèi)存資源徒坡,具體的策略如下:

  1. ByteBuffer 支持容量的擴(kuò)展和收縮撕氧,可以按需靈活調(diào)整,以節(jié)約內(nèi)存喇完;
  2. 接收消息的時(shí)候伦泥,可以按照指定的算法對(duì)之前接收的消息大小進(jìn)行分析,并預(yù)測(cè)未來(lái)的消息大小何暮,按照預(yù)測(cè)值靈活調(diào)整緩沖區(qū)容量奄喂,以做到最小的資源損耗滿足程序正常功能。

幸運(yùn)的是海洼,Netty 提供的 ByteBuf 支持容量動(dòng)態(tài)調(diào)整跨新,對(duì)于接收緩沖區(qū)的內(nèi)存分配器,Netty 提供了兩種:

  1. FixedRecvByteBufAllocator:固定長(zhǎng)度的接收緩沖區(qū)分配器坏逢,由它分配的 ByteBuf 長(zhǎng)度都是固定大小的域帐,并不會(huì)根據(jù)實(shí)際數(shù)據(jù)報(bào)的大小動(dòng)態(tài)收縮。但是是整,如果容量不足肖揣,支持動(dòng)態(tài)擴(kuò)展。動(dòng)態(tài)擴(kuò)展是 Netty ByteBuf 的一項(xiàng)基本功能浮入,與 ByteBuf 分配器的實(shí)現(xiàn)沒(méi)有關(guān)系龙优;
  2. AdaptiveRecvByteBufAllocator:容量動(dòng)態(tài)調(diào)整的接收緩沖區(qū)分配器,它會(huì)根據(jù)之前 Channel 接收到的數(shù)據(jù)報(bào)大小進(jìn)行計(jì)算事秀,如果連續(xù)填充滿接收緩沖區(qū)的可寫(xiě)空間彤断,則動(dòng)態(tài)擴(kuò)展容量。如果連續(xù) 2 次接收到的數(shù)據(jù)報(bào)都小于指定值易迹,則收縮當(dāng)前的容量宰衙,以節(jié)約內(nèi)存。

相對(duì)于 FixedRecvByteBufAllocator睹欲,使用 AdaptiveRecvByteBufAllocator 更為合理供炼,可以在創(chuàng)建客戶端或者服務(wù)端的時(shí)候指定 RecvByteBufAllocator一屋,代碼如下:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT)</pre>

如果默認(rèn)沒(méi)有設(shè)置,則使用 AdaptiveRecvByteBufAllocator袋哼。

另外值得注意的是冀墨,無(wú)論是接收緩沖區(qū)還是發(fā)送緩沖區(qū),緩沖區(qū)的大小建議設(shè)置為消息的平均大小先嬉,不要設(shè)置成最大消息的上限轧苫,這會(huì)導(dǎo)致額外的內(nèi)存浪費(fèi)楚堤。通過(guò)如下方式可以設(shè)置接收緩沖區(qū)的初始大幸呗:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">/**
* Creates a new predictor with the specified parameters.
*
* @param minimum
* the inclusive lower bound of the expected buffer size
* @param initial
* the initial buffer size when no feed back was received
* @param maximum
* the inclusive upper bound of the expected buffer size
*/
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) </pre>

對(duì)于消息發(fā)送,通常需要用戶自己構(gòu)造 ByteBuf 并編碼身冬,例如通過(guò)如下工具類(lèi)創(chuàng)建消息發(fā)送緩沖區(qū):

image

圖 3-2 構(gòu)造指定容量的緩沖區(qū)

3.5. 內(nèi)存池

推送服務(wù)器承載了海量的長(zhǎng)鏈接衅胀,每個(gè)長(zhǎng)鏈接實(shí)際就是一個(gè)會(huì)話。如果每個(gè)會(huì)話都持有心跳數(shù)據(jù)酥筝、接收緩沖區(qū)滚躯、指令集等數(shù)據(jù)結(jié)構(gòu),而且這些實(shí)例隨著消息的處理朝生夕滅嘿歌,這就會(huì)給服務(wù)器帶來(lái)沉重的 GC 壓力掸掏,同時(shí)消耗大量的內(nèi)存。

最有效的解決策略就是使用內(nèi)存池宙帝,每個(gè) NioEventLoop 線程處理 N 個(gè)鏈路丧凤,在線程內(nèi)部,鏈路的處理時(shí)串行的步脓。假如 A 鏈路首先被處理愿待,它會(huì)創(chuàng)建接收緩沖區(qū)等對(duì)象,待解碼完成之后靴患,構(gòu)造的 POJO 對(duì)象被封裝成 Task 后投遞到后臺(tái)的線程池中執(zhí)行仍侥,然后接收緩沖區(qū)會(huì)被釋放,每條消息的接收和處理都會(huì)重復(fù)接收緩沖區(qū)的創(chuàng)建和釋放鸳君。如果使用內(nèi)存池农渊,則當(dāng) A 鏈路接收到新的數(shù)據(jù)報(bào)之后,從 NioEventLoop 的內(nèi)存池中申請(qǐng)空閑的 ByteBuf或颊,解碼完成之后砸紊,調(diào)用 release 將 ByteBuf 釋放到內(nèi)存池中,供后續(xù) B 鏈路繼續(xù)使用饭宾。

使用內(nèi)存池優(yōu)化之后批糟,單個(gè) NioEventLoop 的 ByteBuf 申請(qǐng)和 GC 次數(shù)從原來(lái)的 N = 1000000/64 = 15625 次減少為最少 0 次(假設(shè)每次申請(qǐng)都有可用的內(nèi)存)。

下面我們以推特使用 Netty4 的 PooledByteBufAllocator 進(jìn)行 GC 優(yōu)化作為案例看铆,對(duì)內(nèi)存池的效果進(jìn)行評(píng)估徽鼎,結(jié)果如下:

垃圾生成速度是原來(lái)的 1/5,而垃圾清理速度快了 5 倍。使用新的內(nèi)存池機(jī)制否淤,幾乎可以把網(wǎng)絡(luò)帶寬壓滿悄但。

Netty4 之前的版本問(wèn)題如下:每當(dāng)收到新信息或者用戶發(fā)送信息到遠(yuǎn)程端,Netty 3 均會(huì)創(chuàng)建一個(gè)新的堆緩沖區(qū)石抡。這意味著檐嚣,對(duì)應(yīng)每一個(gè)新的緩沖區(qū),都會(huì)有一個(gè) new byte[capacity]啰扛。這些緩沖區(qū)會(huì)導(dǎo)致 GC 壓力嚎京,并消耗內(nèi)存帶寬。為了安全起見(jiàn)隐解,新的字節(jié)數(shù)組分配時(shí)會(huì)用零填充鞍帝,這會(huì)消耗內(nèi)存帶寬。然而煞茫,用零填充的數(shù)組很可能會(huì)再次用實(shí)際的數(shù)據(jù)填充帕涌,這又會(huì)消耗同樣的內(nèi)存帶寬。如果 Java 虛擬機(jī)(JVM)提供了創(chuàng)建新字節(jié)數(shù)組而又無(wú)需用零填充的方式续徽,那么我們本來(lái)就可以將內(nèi)存帶寬消耗減少 50%蚓曼,但是目前沒(méi)有那樣一種方式。

在 Netty 4 中實(shí)現(xiàn)了一個(gè)新的 ByteBuf 內(nèi)存池钦扭,它是一個(gè)純 Java 版本的 jemalloc (Facebook 也在用)∪野妫現(xiàn)在,Netty 不會(huì)再因?yàn)橛昧闾畛渚彌_區(qū)而浪費(fèi)內(nèi)存帶寬了土全。不過(guò)捎琐,由于它不依賴于 GC,開(kāi)發(fā)人員需要小心內(nèi)存泄漏裹匙。如果忘記在處理程序中釋放緩沖區(qū)瑞凑,那么內(nèi)存使用率會(huì)無(wú)限地增長(zhǎng)。

Netty 默認(rèn)不使用內(nèi)存池概页,需要在創(chuàng)建客戶端或者服務(wù)端的時(shí)候進(jìn)行指定籽御,代碼如下:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)</pre>

使用內(nèi)存池之后,內(nèi)存的申請(qǐng)和釋放必須成對(duì)出現(xiàn)惰匙,即 retain() 和 release() 要成對(duì)出現(xiàn)技掏,否則會(huì)導(dǎo)致內(nèi)存泄露。

值得注意的是项鬼,如果使用內(nèi)存池哑梳,完成 ByteBuf 的解碼工作之后必須顯式的調(diào)用 ReferenceCountUtil.release(msg) 對(duì)接收緩沖區(qū) ByteBuf 進(jìn)行內(nèi)存釋放,否則它會(huì)被認(rèn)為仍然在使用中绘盟,這樣會(huì)導(dǎo)致內(nèi)存泄露鸠真。

3.6. 當(dāng)心“日志隱形殺手”

通常情況下悯仙,大家都知道不能在 Netty 的 I/O 線程上做執(zhí)行時(shí)間不可控的操作,例如訪問(wèn)數(shù)據(jù)庫(kù)吠卷、發(fā)送 Email 等锡垄。但是有個(gè)常用但是非常危險(xiǎn)的操作卻容易被忽略,那便是記錄日志祭隔。

通常货岭,在生產(chǎn)環(huán)境中,需要實(shí)時(shí)打印接口日志疾渴,其它日志處于 ERROR 級(jí)別千贯,當(dāng)推送服務(wù)發(fā)生 I/O 異常之后,會(huì)記錄異常日志程奠。如果當(dāng)前磁盤(pán)的 WIO 比較高丈牢,可能會(huì)發(fā)生寫(xiě)日志文件操作被同步阻塞,阻塞時(shí)間無(wú)法預(yù)測(cè)瞄沙。這就會(huì)導(dǎo)致 Netty 的 NioEventLoop 線程被阻塞,Socket 鏈路無(wú)法被及時(shí)關(guān)閉慌核、其它的鏈路也無(wú)法進(jìn)行讀寫(xiě)操作等距境。

以最常用的 log4j 為例,盡管它支持異步寫(xiě)日志(AsyncAppender)垮卓,但是當(dāng)日志隊(duì)列滿之后垫桂,它會(huì)同步阻塞業(yè)務(wù)線程,直到日志隊(duì)列有空閑位置可用粟按,相關(guān)代碼如下:

<pre style="margin: 0px 0px 1.5rem; padding: 0px; font-family: Courier, "Courier New", monospace; display: block; font-weight: 400; background: rgb(249, 250, 252); border-radius: 5px; overflow: hidden; color: rgb(74, 74, 74); font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> synchronized (this.buffer) {
while (true) {
int previousSize = this.buffer.size();
if (previousSize < this.bufferSize) {
this.buffer.add(event);
if (previousSize != 0) break;
this.buffer.notifyAll(); break;
}
boolean discard = true;
if ((this.blocking) && (!Thread.interrupted()) && (Thread.currentThread() != this.dispatcher)) // 判斷是業(yè)務(wù)線程
{
try
{
this.buffer.wait();// 阻塞業(yè)務(wù)線程
discard = false;
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}

    }</pre>

類(lèi)似這類(lèi) BUG 具有極強(qiáng)的隱蔽性诬滩,往往 WIO 高的時(shí)間持續(xù)非常短,或者是偶現(xiàn)的灭将,在測(cè)試環(huán)境中很難模擬此類(lèi)故障疼鸟,問(wèn)題定位難度非常大。這就要求讀者在平時(shí)寫(xiě)代碼的時(shí)候一定要當(dāng)心庙曙,注意那些隱性地雷空镜。

3.7. TCP 參數(shù)優(yōu)化

常用的 TCP 參數(shù),例如 TCP 層面的接收和發(fā)送緩沖區(qū)大小設(shè)置捌朴,在 Netty 中分別對(duì)應(yīng) ChannelOption 的 SO_SNDBUF 和 SO_RCVBUF吴攒,需要根據(jù)推送消息的大小,合理設(shè)置砂蔽,對(duì)于海量長(zhǎng)連接洼怔,通常 32K 是個(gè)不錯(cuò)的選擇。

另外一個(gè)比較常用的優(yōu)化手段就是軟中斷左驾,如圖所示:如果所有的軟中斷都運(yùn)行在 CPU0 相應(yīng)網(wǎng)卡的硬件中斷上镣隶,那么始終都是 cpu0 在處理軟中斷泽台,而此時(shí)其它 CPU 資源就被浪費(fèi)了,因?yàn)闊o(wú)法并行的執(zhí)行多個(gè)軟中斷矾缓。

image

圖 3-3 中斷信息

大于等于 2.6.35 版本的 Linux kernel 內(nèi)核怀酷,開(kāi)啟 RPS,網(wǎng)絡(luò)通信性能提升 20% 之上嗜闻。RPS 的基本原理:根據(jù)數(shù)據(jù)包的源地址蜕依,目的地址以及目的和源端口,計(jì)算出一個(gè) hash 值琉雳,然后根據(jù)這個(gè) hash 值來(lái)選擇軟中斷運(yùn)行的 cpu样眠。從上層來(lái)看,也就是說(shuō)將每個(gè)連接和 cpu 綁定翠肘,并通過(guò)這個(gè) hash 值檐束,來(lái)均衡軟中斷運(yùn)行在多個(gè) cpu 上,從而提升通信性能。

3.8. JVM 參數(shù)

最重要的參數(shù)調(diào)整有兩個(gè):

  • -Xmx:JVM 最大內(nèi)存需要根據(jù)內(nèi)存模型進(jìn)行計(jì)算并得出相對(duì)合理的值不翩;
  • GC 相關(guān)的參數(shù): 例如新生代和老生代乍钻、永久代的比例,GC 的策略甥桂,新生代各區(qū)的比例等,需要根據(jù)具體的場(chǎng)景進(jìn)行設(shè)置和測(cè)試邮旷,并不斷的優(yōu)化黄选,盡量將 Full GC 的頻率降到最低。

轉(zhuǎn)載地址:https://www.infoq.cn/article/netty-million-level-push-service-design-points

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末婶肩,一起剝皮案震驚了整個(gè)濱河市办陷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌律歼,老刑警劉巖民镜,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異苗膝,居然都是意外死亡殃恒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)辱揭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)离唐,“玉大人,你說(shuō)我怎么就攤上這事问窃『蓿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵域庇,是天一觀的道長(zhǎng)嵌戈。 經(jīng)常有香客問(wèn)我覆积,道長(zhǎng),這世上最難降的妖魔是什么熟呛? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任宽档,我火速辦了婚禮,結(jié)果婚禮上庵朝,老公的妹妹穿的比我還像新娘吗冤。我一直安慰自己,他們只是感情好九府,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布椎瘟。 她就那樣靜靜地躺著,像睡著了一般侄旬。 火紅的嫁衣襯著肌膚如雪肺蔚。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天儡羔,我揣著相機(jī)與錄音宣羊,去河邊找鬼。 笑死笔链,一個(gè)胖子當(dāng)著我的面吹牛段只,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鉴扫,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼澈缺!你這毒婦竟也來(lái)了坪创?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤姐赡,失蹤者是張志新(化名)和其女友劉穎莱预,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體项滑,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡依沮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了枪狂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片危喉。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖州疾,靈堂內(nèi)的尸體忽然破棺而出辜限,到底是詐尸還是另有隱情,我是刑警寧澤严蓖,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布薄嫡,位于F島的核電站氧急,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏毫深。R本人自食惡果不足惜吩坝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哑蔫。 院中可真熱鬧钉寝,春花似錦、人聲如沸鸳址。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)稿黍。三九已至疹瘦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間巡球,已是汗流浹背言沐。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酣栈,地道東北人险胰。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像矿筝,于是被迫代替她去往敵國(guó)和親起便。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 該文章為轉(zhuǎn)載窖维,原文章請(qǐng)點(diǎn)擊 1. 背景 1.1. Netty 3.X系列版本現(xiàn)狀 根據(jù)對(duì)Netty社區(qū)部分用戶的調(diào)...
    Pramyness閱讀 1,965評(píng)論 1 14
  • Netty的簡(jiǎn)單介紹 Netty 是一個(gè) NIO client-server(客戶端服務(wù)器)框架榆综,使用 Netty...
    AI喬治閱讀 8,394評(píng)論 1 101
  • 前奏 https://tech.meituan.com/2016/11/04/nio.html 綜述 netty通...
    jiangmo閱讀 5,842評(píng)論 0 13
  • 2018 2 11 星期日 晴 今天是周末,臨近過(guò)年了铸史,事情總是那么多鼻疮。今天還要出去大采購(gòu),因?yàn)樘鞖饫淞战危?..
    99d29bce557c閱讀 102評(píng)論 0 0
  • 八極五詩(shī)七文章判沟,一讀一課一書(shū)房。 談笑往來(lái)衣食客崭篡,成朋成友成儒商挪哄。 注:1/八天打一遍太極,編號(hào)A 媚送,五天寫(xiě)一首詩(shī)...
    城中詩(shī)客閱讀 143評(píng)論 0 1