最近基于 Aeron 實(shí)現(xiàn)了一下 Service Mesh Sidecar 的本地通信榔组,但是在 IdleStrategy 上犯了難刮刑,無論怎么選都感覺不合適干旁,這幾天跟大數(shù)據(jù)部門寫 C++ 的同學(xué)聊了一下,一句話幫我解開了難題桩撮,不得不感慨一下:雖然一直不認(rèn)為自己是一個 CURD boy敦第,但是看起來也沒有高明多少(尷尬......)。
這篇文章簡單總結(jié)了一下我遇到的問題店量,簡而言之芜果,就是在 Aeron 共享內(nèi)存通信的場景下,如何告訴對端進(jìn)程有新消息可讀了融师,而不是讓對端進(jìn)程一直在輪詢檢查右钾。
0.
首先解釋一下,何為共享內(nèi)存通信中的通知機(jī)制旱爆?
以 Aeron 的 Conductor 通信為例:
(如果你不了解 Aeron 也沒關(guān)系舀射,重點(diǎn)關(guān)注讀取數(shù)據(jù)那部分,想要詳細(xì)了解可以參考我之前的文章疼鸟。)
Aeron 實(shí)現(xiàn)了一個很優(yōu)秀的無鎖輪詢算法:
- 「寫入者」先在 length 字段寫入一個負(fù)值后控,然后寫入消息體,最后再更新 length 字段空镜;
- 「讀取者」輪詢 length 字段浩淘,如果是正值,那就表示有待讀取的消息吴攒。
Aeron 是通過控制 length 字段张抄,結(jié)合輪詢機(jī)制,實(shí)現(xiàn)了通知機(jī)制洼怔。
簡而言之署惯,本文要探討的通知機(jī)制就是:「寫入者」如何告訴「讀取者」有新的數(shù)據(jù)可讀。
這是個比較細(xì)節(jié)的問題镣隶,對于一個 Java 開發(fā)來說极谊,你可能根本就不會認(rèn)為這是個問題,比如說網(wǎng)絡(luò)通信安岂,我們用 Netty 就好轻猖,追問一句 Netty 的 reactor 模型是如何實(shí)現(xiàn)的?答:基于 epoll 的事件通知域那。至于通知事件是如何產(chǎn)生的咙边,這可能就沒人關(guān)心了,因?yàn)檫@是內(nèi)核里的邏輯了。
當(dāng)然败许,我們用共享內(nèi)存機(jī)制就是想跳出內(nèi)核王带,實(shí)現(xiàn)一個輕量的高性能的通信方式,所以也不會亦步亦趨的照搬內(nèi)核的邏輯市殷,但是掌握 epoll 的通知機(jī)制非常有助于理解通知機(jī)制中的 tradeoff愕撰。
1. epoll 通知機(jī)制
epoll 是一個非常優(yōu)秀的事件通知機(jī)制。
總體來看被丧,調(diào)用 epoll_wait
時盟戏,如果有就緒的 fds绪妹,那么直接返回甥桂;如果沒有,那么主動讓出 CPU邮旷,阻塞等待黄选。等到有 fd 就緒的時候,會回調(diào)等待隊(duì)列中注冊的 ep_poll_callback
函數(shù)婶肩,更新就緒列表办陷,然后喚醒線程,返回結(jié)果律歼。
優(yōu)秀民镜!如果持續(xù)有就緒事件,以輪詢的模式運(yùn)行险毁,不需要切換線程制圈;如果沒有就緒事件,會讓出 CPU畔况,等待回調(diào)鲸鹦,不至于浪費(fèi)資源。
epoll 的核心就是等待隊(duì)列的回調(diào)機(jī)制跷跪,因?yàn)榫途w列表的設(shè)置都是在這個回調(diào)中完成馋嗜,另外還做了阻塞時的喚醒操作。
上圖以 socket 為例吵瞻,展示了 socket 文件的等待隊(duì)列葛菇,以及與 epitem 的關(guān)聯(lián)關(guān)系。
對于 TCP 網(wǎng)絡(luò)的場景橡羞,再深入一下眯停,我們看一下 ep_poll_callback
回調(diào)是如何執(zhí)行的。換句話說尉姨,這里想再了解一下 Linux 內(nèi)核對于網(wǎng)絡(luò)包接收的通知機(jī)制庵朝。
在之前的文章中,我用 debug 的方式查看過 ep_poll_callback
的調(diào)用棧,再結(jié)合理論知識九府,其實(shí)不難理解整體的過程椎瘟。(當(dāng)然細(xì)節(jié)很多,短時間不太可能完整理解)
首先網(wǎng)卡收包是一個硬中斷:
然后是軟中斷(debug 的調(diào)用棧主要展示的就是這個過程):
主干流程很清晰侄旬,軟中斷執(zhí)行線程一路調(diào)用肺蔚,最終在 sock_def_readable
方法中回調(diào)了 socket 等待隊(duì)列中注冊的 func,也就是 epoll_ctl 時注冊的 ep_poll_callback
儡羔。
其中有個細(xì)節(jié)很有意思宣羊,軟中斷處理函數(shù) net_rx_action
第一步操作是通過 napi_poll 繼續(xù)收包,而不是立即交付數(shù)據(jù)汰蜘,繼續(xù)響應(yīng)中斷仇冯。
收到中斷,改為輪詢族操,達(dá)到條件再退回到等待中斷苛坚,這個設(shè)計(jì)很巧妙,既減少中斷的頻次色难,又避免了單純輪詢對 CPU 資源的浪費(fèi)泼舱。
2. 共享內(nèi)存通信的通知機(jī)制
2.1 semaphore
由于剛接觸共享內(nèi)存通信就站在了 Martin Thompson 這位巨人的肩膀上,最近從頭學(xué)了一下 man7/tlpi 才意識到一個事情枷莉,通常來說兩個進(jìn)程同時操作一塊內(nèi)存區(qū)域是需要加鎖的娇昙,Aeron 這種無鎖的算法實(shí)際上是一種很高級的方式。
如果不用這種無鎖的方式笤妙,那么可以使用 POSIX semaphores冒掌,既處理了并發(fā)操作,又提供了通知機(jī)制危喉。
2.2 epoll 通知 + 輪詢
既然 Aeron 已經(jīng)提供了無鎖的實(shí)現(xiàn)宋渔,再退回到 semaphore 同步就不太合適了。
參考內(nèi)核處理網(wǎng)絡(luò)收包的方式辜限,同時又利用好內(nèi)核提供的基礎(chǔ)設(shè)施皇拣,那么可以這樣來實(shí)現(xiàn)通知機(jī)制。
使用 named pipe 傳遞通知信號薄嫡,「讀取者」使用 epoll 監(jiān)聽 named pipe氧急,如果有新的通知,那么轉(zhuǎn)為輪詢策略毫深,讀取共享內(nèi)存中的數(shù)據(jù)吩坝,最后再退回到 epoll 等待新的通知。
既能盡可能快的處理新消息哑蔫,又不至于將 CPU 資源都浪費(fèi)在輪詢上钉寝。
當(dāng)然弧呐,可以用于傳遞的信號的基礎(chǔ)設(shè)施有很多,這里選擇 named pipe 原因是使用方式與 shm_open 這套 API 比較統(tǒng)一嵌纲。關(guān)于各種共享內(nèi)存的方式可以參考我上篇文章俘枫,如果已經(jīng)使用了 memfd_create 的方式構(gòu)建共享內(nèi)存的話,那么用 eventfd 傳遞通知信號更合適逮走。