Linux में epoll और io_uring की तुलना
(sibexi.co)- TinyGate reverse proxy ने worker-आधारित संरचना से epoll पर जाकर performance बेहतर की, लेकिन बाद में सीमाओं से टकराने पर उसे io_uring पर दोबारा लिखा गया
- epoll, I/O संभव होने का समय बताने वाला readiness model है, इसलिए
epoll_waitके बादread()/write()को अलग से कॉल करना पड़ता है - io_uring, I/O completion के आधार पर चलने वाला completion model है, और application व kernel shared ring buffer के ज़रिए submission queue और completion queue का आदान-प्रदान करते हैं
io_uring_enter()मूल रूप से ज़रूरी होता है, लेकिन इससे कई operations को एक साथ submit और reap किया जा सकता है, औरIORING_SETUP_SQPOLLsyscall कम करता है लेकिन इसकी कीमत CPU usage के रूप में चुकानी पड़ती है- अगर आप kernel v5.1+ चलाने वाले आधुनिक Linux server पर नया project शुरू कर रहे हैं, तो io_uring को epoll से अधिक उपयुक्त विकल्प माना जाता है
TinyGate ने epoll की सीमाएँ उजागर कीं
- TinyGate, छात्रों के साथ बनाया गया एक reverse proxy server था, और उसका पहला version एक साधारण worker-आधारित संरचना पर आधारित था
- शैक्षणिक project के रूप में यह काम करता था, लेकिन nginx या haproxy जैसे tools की तुलना में इसकी architectural सीमाएँ बड़ी थीं
- दूसरा version epoll-आधारित बना, जिससे पहले version की तुलना में performance काफ़ी बेहतर हुई
- फिर भी benchmark में यह nginx/haproxy को पार नहीं कर सका
- बाद में epoll की सीमाओं के कारण इसे io_uring पर शिफ्ट किया गया, और project को शुरुआत से फिर लिखना पड़ा
epoll: readiness notification और बार-बार होने वाला syscall
- epoll, Linux में लंबे समय से इस्तेमाल होने वाला asynchronous I/O management तरीका है, और 2002 में Linux kernel में शामिल किया गया था
- इसका मूल विचार I/O संभव होने का समय बताने वाली readiness notification है
- epoll यह बताता है कि “अब पढ़ा या लिखा जा सकता है”
- असली data read और write उसके बाद application द्वारा
read()याwrite()syscall से किया जाता है
- सामान्य flow में हर event पर syscall की लागत बार-बार आती है
epoll_ctl, file descriptor register करने वाला एक one-time syscall है- लेकिन हर वास्तविक I/O event पर
epoll_waitऔरread()/write()की ज़रूरत होती है - नतीजतन event handling में लगातार अतिरिक्त syscall जुड़ते रहते हैं
- syscall, user mode और kernel mode के बीच context switch कराता है, और connections बढ़ने के साथ यह overhead भी बढ़ता जाता है
io_uring: completion model और shared ring buffer
- io_uring, epoll के Linux kernel में आने के लगभग 17 साल बाद 2019 में आया, और kernel v5.1+ में supported है
- epoll के विपरीत, यह I/O संभव है या नहीं पर नहीं बल्कि I/O पूरा हुआ या नहीं इस आधार पर काम करता है
- application और kernel shared memory के ring buffer को साथ में इस्तेमाल करते हैं
- submission queue में application, kernel को दिए जाने वाले काम डालता है
- completion queue में kernel, पूरे हुए operations के नतीजे वापस डालता है
- default setup में kernel से submission queue चेक करवाने के लिए
io_uring_enter()कॉल करना पड़ता है- एक ही कॉल में कई operations submit किए जा सकते हैं और कई completions हासिल की जा सकती हैं
- यह epoll और
read()के combination की तरह हर operation पर syscall की जोड़ी दोहराने वाली संरचना नहीं है
IORING_SETUP_SQPOLLका उपयोग करने पर kernel thread, submission queue को poll करता है- सामान्य running state में syscall लगभग ख़त्म किए जा सकते हैं
- queue खाली होने पर भी kernel thread चलता रहता है, इसलिए CPU का उपयोग होता है
sq_thread_idleके बाद यह sleep में जा सकता है, लेकिन लागत पूरी तरह ग़ायब नहीं होती
code example से दिखता अंतर
-
epoll example
stdinfile descriptor को register किया जाता है, और event आने पर अलग सेread()कॉल किया जाता हैepoll_create1से epoll instance बनाया जाता हैepoll_ctlसेSTDIN_FILENOregister किया जाता हैepoll_waitसे तब तक block किया जाता है जब तक read संभव न हो- event आने पर
read()syscall से data पढ़ा जाता है - इस flow में हर वास्तविक I/O event पर
epoll_waitऔरreadदोनों की ज़रूरत पड़ती है
-
io_uring example
- इसमें
liburingका उपयोग किया गया है io_uring_queue_initसे ring initialize की जाती हैio_uring_get_sqeसे submission queue entry ली जाती हैio_uring_prep_readसेstdinread operation तैयार किया जाता हैio_uring_submitसे submit किया जाता है औरio_uring_wait_cqeसे completion का इंतज़ार किया जाता है- io_uring example में अलग से readiness check नहीं है, और completion के समय
read()को अलग से कॉल नहीं किया जाता - सरलता के लिए दोनों examples में महत्वपूर्ण exception handling छोड़ी गई है
- अगर
stdinमें data नहीं है, तो यह हमेशा के लिए block हो सकता है - io_uring example यह जाँचता नहीं है कि submission queue भर जाने पर
io_uring_get_sqe()NULLलौटाएगा
- इसमें
io_uring इस्तेमाल करते समय अतिरिक्त शर्तें
- zero-copy I/O इस्तेमाल करने के लिए
io_uring_register_buffers()से buffers को पहले register करना पड़ता है- इससे kernel को हर operation पर memory दोबारा map करने से बचाया जा सकता है
- network transfer में kernel 6.0+ का
IORING_OP_SEND_ZC, buffer को kernel में copy किए बिना send प्रदान करता है
IORING_SETUP_SQPOLLsyscall घटा सकता है, लेकिन इसकी कीमत CPU usage है- queue खाली होने पर भी kernel thread लगातार poll करता रहता है
- idle timeout के बाद sleep में जा सकता है, लेकिन लागत ख़त्म नहीं होती
- io_uring में errors, synchronous syscall की direct return value के रूप में नहीं बल्कि completion queue entry के
resfield में asynchronous तरीके से लौटते हैं- error handling,
cqe->resके ज़रिए करनी होती है
- error handling,
आधुनिक Linux server पर क्या चुनें
- epoll, I/O उपलब्ध होने के समय की सूचना और अलग syscall calls पर आधारित Linux का पुराना asynchronous I/O तरीका है
- io_uring, आधुनिक Linux में completion-आधारित model और batched submission/completion handling प्रदान करता है
- अगर आधुनिक Linux server पर शुरुआत से नया project बनाया जा रहा है, तो io_uring चुनना अधिक स्वाभाविक है
- अगर पुराने system support को उचित समय पर छोड़ा जा सकता है, तो kernel v5.1+ environment में epoll चुनने के बहुत कारण नहीं बचते
1 टिप्पणियां
Hacker News की राय
मैंने GitHub रिपॉज़िटरी https://github.com/sibexico/TinyGate को बस बहुत थोड़ी देर देखा, और लगता है कि CPU pinning अभी इस्तेमाल नहीं हो रही है
थ्रेड्स और listen socket को CPU पर pin करके, और
sockopt SO_INCOMING_CPUका इस्तेमाल करके, performance को थोड़ा और बढ़ाया जा सकता हैअगर outgoing sockets को भी CPU के हिसाब से align किया जाए तो काफ़ी बड़ा सुधार मिल सकता है, लेकिन मेरी जानकारी में इसके लिए कोई अच्छा API नहीं है। Linux में compatible NICs के लिए traffic steering/flow steering API है, और अगर आपको पता हो कि NIC कौन-सा hash इस्तेमाल करता है—शायद Toeplitz—तो backend की ओर जाने वाले source ports को समझदारी से चुनकर hash match कराया जा सकता है
लक्ष्य यह है कि proxy CPU के बीच communication के बिना packets को process करे
https://github.com/concurrencykit/ck और https://github.com/microsoft/mimalloc भी देखना अच्छा रहेगा। ये zero-copy और memory-aligned reverse proxy के लिए काफ़ी उपयुक्त होंगे
अगर DDoS defense और ज़्यादा advanced L4 features जोड़ने हैं, तो https://docs.ebpf.io/ebpf-library/libxdp/libxdp/ भी देखने लायक है
यह सच में बहुत अच्छा लेख है
इस लेख की वजह से मैं
uring, kernel development, और C की rabbit hole में उतर गया। मैं काफ़ी समय से Rust और C++ development कर रहा हूँ, लेकिन छोटे और ठीक-ठाक आकार के C programs में एक तरह की सादगी और कलात्मकता भी होती हैio_uring-आधारित web server में अभी shared buffers को test नहीं किया गया है। वजह यह है कि file से read करके write करने के बजाय,mmapकिए गए region से सीधे send किया जाता हैअसल में मैं
io_uringके साथsendfileइस्तेमाल करना चाहता हूँ, लेकिन अभी इसका support नहीं हैRust और kTLS जैसे buzzwords के साथ एक लेख: https://blog.habets.se/2025/04/io-uring-ktls-and-rust-for-ze...
यह HN पर भी आया था: https://news.ycombinator.com/item?id=44980865
splice(2)implement किया गया है, इसलिएuringके साथ sendfile-जैसा तरीका इस्तेमाल किया जा सकता है। यहsendfileजितना सुविधाजनक नहीं है, लेकिन लगभग वैसा ही काम करेगाअगर इसे DPDK के साथ बनाया जाए तो चीज़ें बहुत ज़्यादा complex हो जाएँगी, लेकिन performance में nginx को पछाड़ने का मौका मिलेगा
अगर इसे FPGA पर चलाने लायक बनाया जाए तो यह और भी complex हो जाएगा
सीख यह है कि performance के लिए abstraction को गरम चाकू से मक्खन काटने की तरह चीरकर निकल जाने वाला रवैया चाहिए, लेकिन उसके साथ हर चीज़ और कठिन हो जाती है। sockets और per-connection thread वाला तरीका उस दौर में अच्छा approach था जब network, CPU की तुलना में बहुत धीमा था, और आज भी कई बार यही सबसे सरल तरीका होता है
मैं भी हमेशा इसे लेकर जिज्ञासु रहा हूँ, इसलिए मूल फ़र्क सीखने के लिए हाल में मैंने HTTP file server की कुछ implementations लिखकर देखीं
https://theconsensus.dev/p/2026/05/18/serving-files-three-wa...
proxy के संदर्भ में
epoll_waitbusy polling का ज़िक्र भी होना चाहिए। मैंने हाल में low-latency options देखते समय इसे देखा था, और DPDK/VMA/io_uring के बिना भी सिर्फ़ साधारण sockets से user-space busy polling के काफ़ी क़रीब पहुँचना संभव लगा, और Fastly ने इसमें योगदान दिया है और इसका इस्तेमाल कर रहा हैयह इतना low-level है कि मैं यह दावा नहीं कर सकता कि मैंने सब कुछ समझ लिया है; बस concept स्तर तक समझा हूँ, इसलिए लिंक छोड़ रहा हूँ। यह सिर्फ़ NAPI
epollcontext के हिसाब से काम करता है और NAPI ID को आसानी से control नहीं किया जा सकता, लेकिन अगर पूरी machine को सिर्फ़ proxy के लिए इस्तेमाल किया जाए, तो NAPI ID के हिसाब से sockets को dedicated pollers को assign करने जैसा एक आसान जुगाड़ संभव हैमेरा use case proxy नहीं था, बल्कि एक machine पर N sockets को poll करके मिले हुए data को process करना था। उस स्थिति में यह व्यावहारिक नहीं लगा, हालाँकि हो सकता है कि एक single thread में NAPI contexts को round-robin में poll करके यह संभव हो। अच्छा होगा अगर कभी kernel को आसानी से यह बता सकें कि “मुझ पर भरोसा करो, इस single socket को मैं आख़िरकार poll करूँगा, इसलिए IRQ path कभी इस्तेमाल मत करो”
इस kernel feature पर पुरानी HN चर्चा: https://news.ycombinator.com/item?id=43749271
Fastly contributor की बढ़िया presentation slides, जिनमें big picture समझने में मदद करने वाले diagrams हैं: https://netdevconf.info/0x18/docs/netdev-0x18-paper10-talk-s...
LWN लेख: https://lwn.net/Articles/1008399/, https://lwn.net/Articles/997491/, https://lwn.net/Articles/959462/
kernel docs: https://docs.kernel.org/networking/napi.html#irq-mitigation
अगर आपको C++ और asynchronous networking पसंद है, तो Boost.Asio मौजूद है
epollevent loop से बदला, तो RPS लगभग 16% बेहतर हो गया। यह नतीजा एक मध्यम आकार के SQL server पर मिला, इसलिए बहुत अच्छी तरह पैकेज की गई libraries इस्तेमाल करते समय सावधानी रखनी चाहिएepollbackend कोio_uringसे बदला, तो CPU उपयोग काफी बढ़ गया। यह इस बात पर बहुत निर्भर कर सकता है कि इसे कैसे इस्तेमाल किया गया और event code में कैसे integrate किया गयालगता है 2050 तक Linux में socket को poll करने के 20 तरीके हो जाएंगे
io_uringके अंदर भी यही हाल है। और तेज़ जाने के लिएio_uringone-shot mode आया, फिर उसके बाद multi-shot mode भी आ गयाहाँ,
io_uringनिश्चित रूप सेepollसे तेज़ है। मेरे मामले मेंio_uringrequests per second के हिसाब से लगभग 20% तेज़ लगासमस्या यह है कि इसे kernel में explicitly enable करना पड़ता है, और security कारणों से लगभग हर जगह यह disabled रहता है। लगता है kernel और user space के बीच direct memory sharing जैसी कोई चीज़ है, जो थोड़ा असहज लगती है। हाल में
io_uringको निशाना बनाने वाले कई exploits भी हुए हैंइसलिए Go जैसी engineering projects, जो जहाँ संभव हो highest performance चाहती हैं, भी
io_uringको कोई गहरा reasonable default नहीं बनातीं। अगर आप यह risk लेना चाहते हैं, तो अपनी पसंद की language में इसे सीधे चला सकते हैं। यह तेज़ है, लेकिन इसकी कीमत potential exploit की संभावना हैepollनहीं बल्किpollसे बनाई गई मेरी POSIX-styleio_uringemulation कभी-कभीio_uringसे भी तेज़ रही है। हालांकि बड़े zero-copy buffers के मामले मेंio_uringसबसे बेहतर हैio_uringasynchronous I/O न होने पर भी उपयोगी है। उदाहरण के लिएmkdirके बाद उसी directory को खोलने जैसी operation chain को एक single atomic task की तरह implement किया जा सकता हैअगर networking में आप packets per second को अधिकतम करना चाहें, तो kernel limits[1] बहुत जल्दी सामने आ जाती हैं, और फिर अंततः GSO/GRO जैसी सुविधाओं का उपयोग करना पड़ता है या network stack को पूरी तरह bypass करना पड़ता है
1: https://github.com/axboe/liburing/discussions/1346
io_uringको पूरी तरह support करते हैं। यह बहुत हाल की बात है, लेकिन इससे बहुत-से enterprise Linux installations शामिल हो जाते हैं। Gemini ने यह भी “कहा” कि Ubuntu और SuSE भी support करते हैं, लेकिन इसे साबित करने के लिए कोई लिंक नहीं दियाhttps://access.redhat.com/solutions/4723221
Go को भी support पर फिर से विचार करना चाहिए। एक बार कोशिश करना बनता है
io_uringfeature detection करने का विकल्प नहीं हो सकता? exploit क्या सिर्फ उन programs की समस्या है जोio_uringइस्तेमाल करते हैं, या पूरे OS की समस्या है?io_uring—आखिरकार ऐसी प्रकृति की होती है जहाँ memory isolation की जिम्मेदारी काफी हद तक user पर आ जाती हैलेकिन
io_uringके मामले में ring kernel के अंदर होती है, इसलिए user बहुत कुछ कर भी नहीं सकताउम्मीद है LLM की वजह से आगे चलकर यह बेहतर होगा, लेकिन यह हल करना मुश्किल समस्या है। kernel के भीतर से इसे संभालना भी बहुत कठिन है, और कई लोग इसे tune करना ठीक से समझते भी नहीं हैं