2 पॉइंट द्वारा GN⁺ 2025-08-23 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • उच्च-प्रदर्शन वेब सर्वर बनाने के लिए पहले select(), poll(), epoll जैसे विभिन्न event-based मॉडल इस्तेमाल किए जाते थे
  • लेकिन इन system call की performance सीमाओं के कारण io_uring आया, जिसने request को queue में डालकर kernel द्वारा asynchronous processing की विधि अपनाई
  • kTLS में kernel TLS encryption processing संभालता है, जिससे sendfile() के उपयोग की संभावना और hardware offloading जैसी अतिरिक्त optimization संभव हो जाती है
  • Descriptorless files के परिचय से file descriptor को सीधे पास किए बिना io_uring के लिए optimized access तरीका मिलता है
  • Rust, io_uring, kTLS को जोड़ने वाले tarweb open source project के जरिए प्रति-request अतिरिक्त system call के बिना HTTPS उपलब्ध कराया जाता है, और safety व memory management से जुड़े मुद्दों पर भी चर्चा होती है

उच्च-प्रदर्शन वेब सर्वर आर्किटेक्चर का विकास

  • 2000 के शुरुआती दशक से high-volume web server की मांग बढ़ी
  • शुरुआत में हर request के लिए नया process बनाने का तरीका आम था, लेकिन इसकी ऊंची लागत के कारण preforking तकनीक सामने आई
  • इसके बाद thread का उपयोग और select(), poll() के सक्रिय इस्तेमाल के जरिए context switching लागत कम करने की दिशा में विकास हुआ
  • लेकिन select() और poll() में भी connection बढ़ने पर kernel को बार-बार बड़े array देने पड़ते हैं, इसलिए scalability की सीमा मौजूद रहती है

epoll का आगमन

  • Linux environment में epoll आने से पुराने तरीकों की तुलना में multi-connection handling अधिक efficient हो गया
  • epoll केवल बदलावों (delta) को process करता है, जिससे अनावश्यक resource consumption कम होता है
  • सभी system call पूरी तरह खत्म नहीं होते, लेकिन लागत काफी कम हो जाती है

io_uring का अवलोकन

  • io_uring में हर request पर system call करने के बजाय request को memory queue में जोड़ा जाता है ताकि kernel उसे asynchronous तरीके से process कर सके
  • उदाहरण के लिए, accept() को queue में डाल देने पर kernel उसे process करके completion queue में result लौटाता है
  • web server queue में request जोड़ता है और result को अलग memory area में जांचता है
  • busy loop से बचने के लिए, queue में बदलाव न होने पर web server और kernel दोनों केवल जरूरत पड़ने पर ही system call करते हैं, जिससे power saving का लाभ मिलता है
  • उपयुक्त library का उपयोग करने पर, active server request processing के दौरान अलग system call के बिना काम कर सकता है

multi-core और NUMA environment

  • आधुनिक CPU के multi-core environment को देखते हुए प्रति-core single thread चलाना और data structure sharing को न्यूनतम करना प्रभावी रणनीति है
  • NUMA environment में प्रत्येक thread केवल अपने local node memory तक पहुंचकर optimization करता है
  • request distribution का पूर्ण संतुलन अभी अतिरिक्त शोध का विषय है

memory allocation

  • kernel और web server दोनों में memory allocation बना रहता है, और user space में allocation भी अंततः system call से जुड़ता है
  • web server की ओर से प्रति-connection fixed-size memory block पहले से allocate करके fragmentation और shortage की समस्या रोकी जाती है
  • kernel की ओर भी प्रति-connection I/O buffer चाहिए, जिसे socket option आदि से कुछ हद तक समायोजित किया जा सकता है
  • memory shortage होने पर गंभीर failure हो सकता है

kTLS (kernel TLS) परिचय

  • kTLS Linux kernel में encryption और decryption operations संभालने वाली सुविधा है
  • handshake application में process होता है, लेकिन उसके बाद kernel data transfer को plain text की तरह संभालता है
  • sendfile() का उपयोग संभव हो जाता है, जिससे user-kernel space के बीच memory copy कम होती है
  • अगर network card समर्थन दे, तो encryption operation को hardware पर offload करने का लाभ भी मिलता है

Descriptorless Files

  • user space से kernel space तक file descriptor सीधे पहुंचाने पर होने वाले overhead को कम करने के लिए यह तरीका आया
  • register_files का उपयोग कर io_uring के लिए ही मान्य अलग 'integer' file number इस्तेमाल किया जाता है, जो /proc/pid/fd में दिखाई नहीं देता
  • system का ulimit limit फिर भी लागू रहता है

tarweb project परिचय

  • tarweb ऊपर की सभी तकनीकों को लागू करने वाला एक example web server open source project है
  • यह single tar file की सामग्री serve करने वाली संरचना है, जिसमें Rust, io_uring, kTLS जैसी आधुनिक high-performance तकनीकें जुड़ी हैं
  • वास्तविक उपयोग के दौरान io_uring और kTLS की compatibility समस्याएं (जैसे setsockopt support न होना) थीं, जिनमें से कुछ issues को Pull Request के जरिए हल किया गया
  • project अभी अधूरा है, और Rust की rustls library handshake प्रक्रिया में memory allocation कर सकती है
  • मुख्य बात यह है कि प्रति request अतिरिक्त system call के बिना HTTPS service संभव है

benchmark और performance measurement

  • लेखक ने अभी पर्याप्त benchmark नहीं किए हैं, और code को व्यवस्थित करने के बाद performance test करने की योजना है

io_uring और Rust की safety समस्याएं

  • synchronous system call से अलग, io_uring में completion event आने तक memory buffer को release नहीं किया जाना चाहिए
  • io-uring crate Rust की compile-time safety की guarantee नहीं देता, और runtime check भी पर्याप्त नहीं हैं
  • गलत उपयोग होने पर C++ जैसी गंभीर समस्याएं हो सकती हैं, जिससे Rust की मूल safety कमजोर पड़ती है
  • pinning और borrow checker का सक्रिय उपयोग करने वाला अलग safer-ring crate आवश्यक है
  • इस मुद्दे पर community में पहले से चर्चा चल रही है

संदर्भ और अतिरिक्त लिंक

  • यह सामग्री 2025-08-22 के अनुसार HackerNews में चर्चा किए गए पोस्ट पर आधारित है

1 टिप्पणियां

 
GN⁺ 2025-08-23
Hacker News टिप्पणियाँ
  • io_uring का उपयोग करके write ऑपरेशन submit करते समय यह सुनिश्चित करना पड़ता है कि memory location free न हो जाए या overwrite न हो, लेकिन io-uring crate API में लगता है कि Rust का borrow checker इस हिस्से में मदद नहीं करता और न ही कोई runtime check है
    इस स्थिति पर लिखे गए लेख और टिप्पणियाँ देखी हैं, और कुल मिलाकर यही लगता है कि io_uring को wrap करने वाली safe Rust async library बनाना वास्तव में बहुत कठिन है
    याद है कि tokio टीम की Alice ने भी हाल में कहा था कि इस समस्या को पार करने में अब उतनी रुचि नहीं है
    क्योंकि अभी performance "काफ़ी अच्छी" है
    संदर्भ: https://boats.gitlab.io/blog/post/io-uring/

    • Rust async के बारे में अफ़सोस वाली कई बातें हैं, और यह उनमें से एक है
      Rust async उस समय डिज़ाइन हुआ था जब epoll standard था, और IOCP पर लगभग ध्यान नहीं दिया गया
      synchronous syscall में यह समस्या नहीं होती, क्योंकि read call के समय buffer का mutable reference kernel को दिया जाता है, और यह native Rust के ownership/borrow model से अच्छी तरह मेल खाता है
      लेकिन completion-based IO को ownership model में ठीक से फिट करने के लिए यह guarantee करनी पड़ती है कि operation पूरा होने तक user code चलता न रहे, और state machine polling structure में यह संभव नहीं है
      threading model या green thread structure यहाँ बिल्कुल फिट बैठता है
      अगर Rust ने "async-only target" जोड़ा होता तो शायद बेहतर रहता
      Rust developers ने asynchronous stackless polling model से बहुत उम्मीदें लगाई थीं, और अब हम उसका परिणाम देख रहे हैं

    • मेरा मानना है कि ownership model का एक रूप ऐसा है जिसे Rust का borrow checker ठीक से support नहीं कर पाता
      अस्थायी रूप से इसे “hot potato ownership” कह सकता हूँ, जहाँ buffer थोड़ी देर के लिए दे दिया जाता है और फिर वापस लिया जाता है
      Rust में इस pattern को safe code के रूप में लिखना बहुत कठिन है और code काफ़ी उलझा हुआ हो जाता है

    • tokio टीम की Alice की बात से अलग, file IO के मामले में रुचि है
      file IO पहले से ही spawn_blocking तरीके से implement किया गया है, इसलिए io_uring वाले वही buffer issue पहले से मौजूद हैं, और io_uring पर migrate करना इतना मुश्किल नहीं है
      लेकिन tokio::net का मौजूदा API io_uring-based buffer API के साथ compatible नहीं है, इसलिए readiness check तो किया जा सकता है, पर पूरा support देना कठिन है

    • safe io_uring interface बनाने के लिए शायद सबसे उपयुक्त तरीका यह है कि ring के ownership वाले buffer लेकर उनका उपयोग किया जाए, और write शुरू करते समय उन्हें वापस कर दिया जाए

    • ज़रूरी नहीं कि हर चीज़ को borrows से ही व्यक्त किया जाए
      Slab जैसी data structure का उपयोग करने पर इसे cancel safe बनाया जा सकता है
      संदर्भ: https://github.com/steelcake/io2

  • यह लेख पढ़कर सच में बहुत मज़ा आया
    performance test का इंतज़ार है, लेकिन यह बात प्रभावशाली लगी कि लेखक benchmark से पहले code को साफ़-सुथरा करना चाहता है
    benchmark को ही सब कुछ मानने वाले इस दौर में इस तरह सोचने वाला कोई है, यह ताज़गी भरा लगा
    लगभग 11 साल की उम्र में database बनाने की कोशिश करते समय cgi-bin देखा था, और अब समझ आया कि वह हर request पर नया process शुरू करने वाला तरीका था
    sendfile बड़े game forum में demo downloads को एक साथ serve करने के समय game changer था, और Netflix के 40ms उदाहरण या GTA 5 के loading time में कमी जैसे नतीजे देखकर लगता है कि इससे भी अधिक impactful engineering इसके भीतर छिपी है
    संबंधित लिंक: Common Gateway Interface, Netflix 40ms उदाहरण, GTA Online लोडिंग समय में कमी

    • सिर्फ CGI ही नहीं, पुराने CERN और Apache श्रृंखला के HTTP session भी पूरे server को fork करके चलते थे
      समय के साथ इसमें सुधार हुआ, लेकिन Apache के configuration model की वजह से nginx जैसे हल्के server, जो शुरू से event-based I/O के लिए डिज़ाइन किए गए थे, बहुत लोकप्रिय हो गए

    • sendfile की efficiency को लेकर मुझे संदेह है
      90 के दशक के अंत में यह लोकप्रिय ज़रूर था, लेकिन वास्तव में performance gain बहुत मामूली है, ऐसा मुझे लगता है

  • ज़्यादातर cloud workload orchestrator (CloudRun, GKE, EKS, local Docker आदि) default रूप से io_uring को disable रखते हैं
    अगर यह हिस्सा नहीं सुधरा, तो कुछ समय तक io_uring बहुत सीमित तकनीक बनकर रह जाएगा

    • यह सवाल उठता है कि वे io_uring को disable क्यों करते हैं

    • ऐसी स्थिति में फिर self-hosting पर लौटना पड़ेगा

  • यह पढ़कर सच में बहुत अच्छा लगा
    benchmark का इंतज़ार करूँगा, इसलिए धीरे करने में भी कोई दिक्कत नहीं, और benchmark से पहले code cleanup को प्राथमिकता देने वाले लेखक का नज़रिया बहुत प्रभावशाली लगा
    आजकल बहुत से project benchmark score पर ही पूरी तरह केंद्रित रहते हैं, इसलिए यह सोच वास्तव में ताज़गी भरी और सम्मान देने योग्य है
    मुझे नहीं पता था कि ktls या io_uring का इतना विविध उपयोग हो सकता है

  • इस समय asynchronous processing की स्थिति कुछ ऐसी है
    Rust: Futures, Pin, Waker, async runtime, Send/Sync bounds, async trait object आदि जैसी कई अवधारणाएँ समझनी पड़ती हैं
    C++20: coroutines
    Go: goroutines
    Java21+: virtual threads

    • C++ coroutines, Pin जिन समस्याओं को हल करता है, उनसे बचने के लिए heap allocation का उपयोग करती हैं
      यह C++ के "zero overhead" सिद्धांत से काफ़ी दूर जाने वाली बात है
      Rust में future में भी async trait को लाने में इतना समय लगने का कारण यही है कि Rust futures को heap allocate नहीं करता
      performance/portability बनाम complexity का trade-off हर project में अलग मूल्य रख सकता है

    • Send/Sync से जुड़े constraints दूसरी भाषाओं में भी अब भी अर्थपूर्ण हैं, और इनके बिना सूक्ष्म रूप से गलत code लिखना ज़्यादा आसान हो जाता है

    • अगर आप Rust में "काफ़ी ठीक-ठाक" स्तर का code लिख रहे हैं, और किसी और द्वारा बनाए गए mid-level primitive का उपयोग कर रहे हैं, तो ज़रूरी नहीं कि इन सभी अवधारणाओं को पूरी तरह समझें

    • Rust आपको मजबूर करता है कि इन अवधारणाओं को समझे बिना code compile ही न हो
      Go में goroutine asynchronous processing के बराबर नहीं है, और channels को समझे बिना goroutine को समझा नहीं जा सकता
      Go का channel implementation काफ़ी अनोखा है, इसलिए edge case behavior को सामान्य समझ से अनुमान लगाना आसान नहीं है
      Go में गहराई से समझे बिना भी coding हो जाती है, इसलिए इसके अपने फ़ायदे और नुकसान हैं
      "cheap threads" asynchronous processing के समान नहीं हैं
      tarweb (ब्लॉग में दिखाया गया server) io_uring-based event loop की single-thread structure है, जिसमें idea यह है कि हर CPU core पर एक thread हो
      "massive concurrency की वर्तमान स्थिति" की बजाय "cheap threads की वर्तमान स्थिति" कहना शायद ज़्यादा सही होगा
      cheap thread और async loop के बीच सबसे बड़ा अंतर यह है कि reasoning आसान होती है
      कमी यह है कि हर thread हल्का होने पर भी stack size की ज़रूरत रखता है

  • kTLS निश्चित रूप से प्रगति है
    मैंने भी कुछ साल पहले सचमुच ऐसा server बनाया था जिसमें प्रति request syscall की संख्या 0 थी, और उस पर ब्लॉग पोस्ट भी लिखी थी (https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html)
    लेकिन इसकी कमी यह है कि हमेशा busy-looping करनी पड़ती है
    io_uring ने पिछले कुछ वर्षों में वाकई प्रभावशाली गति से विकास किया है

  • यह project वाकई शानदार है, और मैं लंबे समय से इसी तरह की किसी चीज़ की कल्पना कर रहा था, इसलिए अच्छा लगा कि किसी ने इसे implement किया
    BPF को भी अगर Rust में लिखना हो तो Aya की सिफ़ारिश करूँगा
    Aya project Github

  • kTLS की मौजूदा स्थिति को लेकर जिज्ञासा है
    कुछ समय पहले मैंने Cilium के एक developer से पूछा था; Thomas Graf ने कहा कि उम्मीदें हैं, लेकिन व्यवहार में कई Linux distributions में kernel support कमज़ोर है, इसलिए इसे default रूप से enable करना अभी दूर की बात है

    • यह अफ़सोस की बात है, लेकिन यह भी जानना चाहता हूँ कि इसे enable करना कितना मुश्किल है
      क्या kernel को customize करना पड़ता है, या runtime में सीधे on किया जा सकता है
      FreeBSD में version 13 से kernel/OpenSSL में kTLS शामिल है, और sysctl (kern.ipc.tls.enable=1) से runtime toggle किया जा सकता है
      FreeBSD-15 में default value enabled हो जाएगी, और Netflix लगभग 10 साल से traffic encryption के लिए kTLS का उपयोग कर रहा है

    • कुल मिलाकर kTLS मुझे एक बुरा विचार लगता है

  • मुझे संदेह है कि प्रति core एक thread वाली संरचना time-slice आधारित system में सही बैठती है
    मेरे अनुभव में "oversubscribing" तरीका (यानी cores से ज़्यादा threads रखना) वास्तविक wall-clock time में फ़ायदा देता है
    जब preemptive scheduling न हो, या वैसी व्यवस्था हो, तब शायद प्रति core एक thread ज़्यादा उचित है
    बेशक, तब Unix की बात नहीं हो रही होगी

    • अगर कम latency और high throughput चाहिए, तो cores को isolate करके threads pin करना प्रभावी होता है
      यह तरीका Linux पर अच्छी तरह काम करता है, और trading systems आदि में inefficiency सहकर भी खूब इस्तेमाल होता है
      cores ज़्यादातर समय idle रहकर spin करते हैं और वास्तव में कोई काम नहीं होता, लेकिन latency और throughput के लिए यह optimal है

    • thread-per-core structure का जाल यह है कि लोग सोचते हैं "सिर्फ सुविधाजनक हिस्से ले लेते हैं"
      जबकि वास्तव में या तो पूरी तरह अपनाओ या बिल्कुल मत अपनाओ, यही स्थिति है
      आधे-अधूरे implementation में बिल्कुल efficiency नहीं मिलती
      लेकिन सही तरह से डिज़ाइन किया जाए तो लगभग हर स्थिति में यह बहुत प्रभावी हो सकता है
      TPC design knowledge, जैसे core-to-core load balancing, अच्छी तरह जानने वाले developers बहुत कम हैं

    • thread-per-core तभी efficient है जब workload "CPU-bound" हो
      इस server project की तरह जब ज़्यादातर काम asynchronous और event-based हो, तो server लगभग बिना I/O या syscall का इंतज़ार किए अगले request पर जा सकता है, इसलिए सैद्धांतिक रूप से प्रति core एक thread बिल्कुल सही structure है
      लेकिन वास्तविक दुनिया में ऐसी आदर्श स्थिति लगभग कभी नहीं मिलती, इसलिए threads को हमेशा nproc तक सीमित कर देना जोखिम भरा हो सकता है, यह याद रखना चाहिए

    • io_uring में प्रति core सिर्फ एक user thread रखना भी बुरा विकल्प नहीं लगता
      क्योंकि kernel भीतर से thread pool की तरह काम करता है

  • मैं DPDK जैसी पूरी तरह kernel bypass करने वाली शैली भी देखना चाहूँगा