1 पॉइंट द्वारा GN⁺ 3 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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_SQPOLL syscall कम करता है लेकिन इसकी कीमत 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

    • stdin file descriptor को register किया जाता है, और event आने पर अलग से read() कॉल किया जाता है
    • epoll_create1 से epoll instance बनाया जाता है
    • epoll_ctl से STDIN_FILENO register किया जाता है
    • 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 से stdin read 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_SQPOLL syscall घटा सकता है, लेकिन इसकी कीमत CPU usage है
    • queue खाली होने पर भी kernel thread लगातार poll करता रहता है
    • idle timeout के बाद sleep में जा सकता है, लेकिन लागत ख़त्म नहीं होती
  • io_uring में errors, synchronous syscall की direct return value के रूप में नहीं बल्कि completion queue entry के res field में asynchronous तरीके से लौटते हैं
    • error handling, cqe->res के ज़रिए करनी होती है

आधुनिक 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 टिप्पणियां

 
GN⁺ 3 시간 전
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 करे

    • रिपॉज़िटरी के v0 और v1 लगभग शुरू से दोबारा लिखे गए पूरी तरह अलग implementations हैं, और अभी तीसरी implementation पर काम चल रहा है, जो शायद आख़िरी होगी। आर्किटेक्चर के चुनाव भी पूरी तरह बदल गए हैं
    • मैं उस patch का benchmark देखना चाहूँगा
  • 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/ भी देखने लायक है

    • योजना यह थी कि दूसरे layers पर optimization लागू करने के बाद allocator की तरफ़ जाएँ। अभी मैं छात्रों के साथ allocators पढ़ रहा हूँ, और ब्लॉग की पिछली पोस्ट Zig भाषा में बने custom allocator के बारे में थी
  • यह सच में बहुत अच्छा लेख है
    इस लेख की वजह से मैं 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_wait busy polling का ज़िक्र भी होना चाहिए। मैंने हाल में low-latency options देखते समय इसे देखा था, और DPDK/VMA/io_uring के बिना भी सिर्फ़ साधारण sockets से user-space busy polling के काफ़ी क़रीब पहुँचना संभव लगा, और Fastly ने इसमें योगदान दिया है और इसका इस्तेमाल कर रहा है
    यह इतना low-level है कि मैं यह दावा नहीं कर सकता कि मैंने सब कुछ समझ लिया है; बस concept स्तर तक समझा हूँ, इसलिए लिंक छोड़ रहा हूँ। यह सिर्फ़ NAPI epoll context के हिसाब से काम करता है और 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 मौजूद है

    • हाल ही में मैंने Asio को अपने हाथ से बनाए गए epoll event loop से बदला, तो RPS लगभग 16% बेहतर हो गया। यह नतीजा एक मध्यम आकार के SQL server पर मिला, इसलिए बहुत अच्छी तरह पैकेज की गई libraries इस्तेमाल करते समय सावधानी रखनी चाहिए
    • database server में Asio के epoll backend को io_uring से बदला, तो CPU उपयोग काफी बढ़ गया। यह इस बात पर बहुत निर्भर कर सकता है कि इसे कैसे इस्तेमाल किया गया और event code में कैसे integrate किया गया
    • Boost बहुत असुविधाजनक है। यह build करने और इस्तेमाल करने में कठिन विशाल dynamic libraries का सेट है। मैं पहले से CMake इस्तेमाल कर रहा था, फिर भी Boost को install करके discoverable बनाना बहुत झंझटभरा था। हालांकि यह अनुभव Mac पर था
  • लगता है 2050 तक Linux में socket को poll करने के 20 तरीके हो जाएंगे

    • सही है, io_uring के अंदर भी यही हाल है। और तेज़ जाने के लिए io_uring one-shot mode आया, फिर उसके बाद multi-shot mode भी आ गया
  • हाँ, io_uring निश्चित रूप से epoll से तेज़ है। मेरे मामले में io_uring requests 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 की संभावना है

    • default रूप से disable किए जाने की मुख्य वजह अब हल हो गई है। latest RC में cBPF support आ गया है, जिससे सब कुछ बंद करने के बजाय executable operations को restrict किया जा सकता है
    • यह case पर निर्भर करता है। epoll नहीं बल्कि poll से बनाई गई मेरी POSIX-style io_uring emulation कभी-कभी io_uring से भी तेज़ रही है। हालांकि बड़े zero-copy buffers के मामले में io_uring सबसे बेहतर है
      io_uring asynchronous 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
    • RHEL 9 और 10 अब default रूप से io_uring को पूरी तरह support करते हैं। यह बहुत हाल की बात है, लेकिन इससे बहुत-से enterprise Linux installations शामिल हो जाते हैं। Gemini ने यह भी “कहा” कि Ubuntu और SuSE भी support करते हैं, लेकिन इसे साबित करने के लिए कोई लिंक नहीं दिया
      https://access.redhat.com/solutions/4723221
      Go को भी support पर फिर से विचार करना चाहिए। एक बार कोशिश करना बनता है
    • Go जैसे project में runtime शुरू होते समय सिर्फ एक बार io_uring feature detection करने का विकल्प नहीं हो सकता? exploit क्या सिर्फ उन programs की समस्या है जो io_uring इस्तेमाल करते हैं, या पूरे OS की समस्या है?
    • हर तरह की polling-mode networking—RDMA, DPDK, io_uring—आखिरकार ऐसी प्रकृति की होती है जहाँ memory isolation की जिम्मेदारी काफी हद तक user पर आ जाती है
      लेकिन io_uring के मामले में ring kernel के अंदर होती है, इसलिए user बहुत कुछ कर भी नहीं सकता
      उम्मीद है LLM की वजह से आगे चलकर यह बेहतर होगा, लेकिन यह हल करना मुश्किल समस्या है। kernel के भीतर से इसे संभालना भी बहुत कठिन है, और कई लोग इसे tune करना ठीक से समझते भी नहीं हैं