• Linux 7.0 में सर्वर का पारंपरिक डिफ़ॉल्ट PREEMPT_NONE preemption mode हटा दिए जाने के बाद, उसी हार्डवेयर पर PostgreSQL throughput आधा रह जाने वाली गंभीर performance regression सामने आई
  • AWS इंजीनियर ने 96-vCPU Graviton4 मशीन पर pgbench चलाया, और Linux 6.x की तुलना में Linux 7.0 पर transactions per second 98,565 से गिरकर 50,751 हो गए, जबकि CPU का 55% एक ही spinlock function में खर्च हुआ
  • PostgreSQL के shared buffer pool तक पहुँच को सुरक्षित रखने वाला spinlock जब 4KB memory pages के minor page faults के साथ जुड़ता है, तब lock पकड़े हुए scheduler द्वारा preemption होने पर प्रतीक्षा कर रहे सभी backend CPU बर्बाद करते हुए spin करते रहते हैं
  • Huge Pages (2MB या 1GB) सक्षम करने पर संभावित page faults की संख्या 3.1 करोड़ से घटकर कुछ दसियों हज़ार या सैकड़ों रह जाती है, जिससे regression दूर हो जाती है
  • Kernel पक्ष से Restartable Sequences (rseq) अपनाने का सुझाव दिया गया, लेकिन PostgreSQL community का कहना है कि kernel upgrade के कारण performance गिरना ही "userspace को नहीं तोड़ते" सिद्धांत के खिलाफ है

समस्या क्या दिखी

  • AWS इंजीनियर Salvatore Dipietro ने 96-vCPU Graviton4 प्रोसेसर पर pgbench चलाकर scale factor 8,470 (लगभग 84.7 करोड़ rows वाली table), 1,024 clients और 96 threads के साथ high-parallel load test किया
  • Linux 6.x में 98,565 TPS, जबकि Linux 7.0 में 50,751 TPS तक throughput लगभग आधा गिर गया
  • perf profiling के अनुसार CPU समय का 55.60% s_lock function के अंदर खर्च हुआ
    • Call path: StartReadBufferGetVictimBufferStrategyGetBuffers_lock

Preemption क्या है

  • OS scheduler द्वारा चल रहे thread को रोककर CPU किसी दूसरे thread को देना preemption कहलाता है
  • Linux 7.0 से पहले तीन विकल्प थे
    • PREEMPT_NONE: thread जब तक खुद CPU न छोड़े (syscall, I/O block, sleep), तब तक उसे लगभग नहीं रोका जाता। यह पारंपरिक server default था, जिसमें context switch कम होते हैं और throughput अधिक मिलता है
    • PREEMPT_FULL: लगभग हर सुरक्षित बिंदु पर चल रहे thread को रोका जा सकता है। Response time घटता है, लेकिन context switch overhead बढ़ता है। यह पारंपरिक desktop default था
    • PREEMPT_LAZY: Linux 6.12 में जोड़ा गया समझौता मॉडल, जो natural boundaries का इंतज़ार करते हुए ज़रूरत पड़ने पर preemption की अनुमति देता है। इसे PREEMPT_NONE जैसी throughput characteristics के करीब लाने के लिए डिज़ाइन किया गया
  • Linux 7.0 में PREEMPT_NONE को आधुनिक CPU architectures पर हटा दिया गया, और केवल PREEMPT_FULL तथा PREEMPT_LAZY बचे
    • PREEMPT_LAZY ज़्यादातर server software के लिए विकल्प की तरह काम करता है, लेकिन PostgreSQL में घातक अंतर सामने आया

PostgreSQL memory management

  • PostgreSQL fixed-size data pages (डिफ़ॉल्ट 8KB) को मूल storage unit की तरह इस्तेमाल करता है, और table rows, B-tree index nodes, metadata आदि सब इन्हीं pages में रखे जाते हैं
  • Disk reads कम करने के लिए यह shared buffer pool नाम की बड़ी shared memory region में हाल में पढ़े गए data pages cache करता है
  • Client connect होने पर dedicated backend process बनता है, और अगर page buffer pool में न हो तो उसे disk से पढ़ने के बाद खाली buffer या evict किए जा सकने वाले buffer की तलाश करनी पड़ती है
    • इस buffer selection का काम करने वाला function है StrategyGetBuffer

PostgreSQL का spinlock

  • Spinlock ऐसा locking mechanism है जिसमें lock का इंतज़ार करते समय thread सोता नहीं, बल्कि loop में घूमकर बार-बार check करता रहता है
    • बहुत छोटे critical sections में thread को सुलाने और फिर जगाने की लागत की तुलना में spinning अधिक कुशल हो सकती है
  • इसकी मुख्य धारणा है: lock पकड़े हुए thread उसे बहुत जल्दी छोड़ देगा
  • StrategyGetBuffer buffer selection को सुरक्षित रखने के लिए एक single global spinlock का उपयोग करता है
    • 96-vCPU और 1,024 clients वाले environment में सभी backend उसी lock के लिए प्रतिस्पर्धा कर रहे थे

Virtual memory और TLB

  • हर process virtual memory addresses का उपयोग करता है, जिन्हें hardware page tables (multi-level tree structure) के माध्यम से physical addresses में बदलता है
  • हर बार page table traversal धीमा होता है, इसलिए CPU हाल की translations को cache करने के लिए TLB (Translation Lookaside Buffer) रखता है
    • TLB hit होने पर access तेज़ होता है, जबकि TLB miss पर page table walk करना पड़ता है और समय लगता है
  • Linux lazy allocation सिद्धांत अपनाता है, यानी virtual memory allocate करते समय actual physical page पहली access पर map किया जाता है
    • पहली access पर minor page fault होता है: kernel physical page allocate करता है, mapping दर्ज करता है, और यह सामान्य read/write से कुछ microseconds धीमा होता है

4KB pages की समस्या

  • Benchmark में shared_buffers को 120GB पर सेट किया गया था, जो 4KB memory pages के हिसाब से लगभग 3.1 करोड़ memory pages बनते हैं, यानी 3.1 करोड़ संभावित first-access page faults
  • 120GB shared buffer pool के साथ लंबे benchmark में नई memory regions लगातार working set में आती रहती हैं, इसलिए page faults सिर्फ शुरुआत में नहीं बल्कि लगातार होते रहते हैं
  • अगर StrategyGetBuffer के अंदर spinlock पकड़े हुए shared memory को access करते समय वह region अभी mapped न हो, तो minor page fault होता है
  • PREEMPT_NONE (Linux 7.0 से पहले): backend A page-fault handler में जाने पर भी voluntary rescheduling points से बचता है, इसलिए fault resolve होने से पहले उसके schedule-out होने की संभावना कम रहती है। Wait time अपेक्षा से लंबा हो सकता है, लेकिन नुकसान सीमित रहता है
  • PREEMPT_LAZY (Linux 7.0 के बाद): scheduler page-fault handler के अंदर backend A को preempt करके किसी दूसरे process को schedule कर सकता है। Fault resolve होने के बाद भी scheduler control वापस दे तब तक अतिरिक्त प्रतीक्षा समय t जुड़ जाता है
    • यह अतिरिक्त प्रतीक्षा समय सिर्फ t नहीं, बल्कि उस समय spin कर रहे सभी backends की संख्या × t जितनी CPU बर्बादी में बदल जाता है
    • 96-vCPU और सैकड़ों backends वाले environment में यह multiplier effect घातक साबित हुआ, और नतीजा यह कि CPU का 56% s_lock में खर्च होने लगा

Huge Pages से समाधान

  • shared_buffers 120GB होने पर memory page size बदलने से संभावित page faults की संख्या नाटकीय रूप से घटती है
    • 4KB pages: ~31,000,000 संभावित page faults
    • 2MB Huge Pages: ~61,440
    • 1GB Huge Pages: ~120
  • Page size बढ़ाने से सिर्फ page faults नहीं घटते, बल्कि TLB pressure भी कम होता है: बहुत कम TLB entries में वही memory cover हो जाती है, इसलिए TLB misses और page table walks घटते हैं
  • StrategyGetBuffer को lock पकड़े हुए fault नहीं करना पड़ता, इसलिए lock holder तेज़ी से काम पूरा कर लेता है और बाकी backends को milliseconds की बजाय केवल microseconds इंतज़ार करना पड़ता है। Regression समाप्त हो जाती है
  • PostgreSQL में huge pages की setting huge_pages parameter से नियंत्रित होती है
    • off, on, try (डिफ़ॉल्ट) तीन values समर्थित हैं
    • try में huge pages उपलब्ध होने पर उनका उपयोग होता है, और न होने पर 4KB पर चुपचाप fallback हो जाता है, इसलिए गलत configuration का पता न चलने का जोखिम रहता है
    • on पर सेट करने से अगर huge pages उपलब्ध न हों तो PostgreSQL start होने में fail हो जाएगा, जिससे समस्या तुरंत दिख जाएगी
  • Trade-off: huge pages pre-allocation और reservation मॉडल पर काम करते हैं, इसलिए PostgreSQL उन्हें पूरा उपयोग न करे तब भी वह memory system के बाकी हिस्सों के लिए उपलब्ध नहीं रहती। अगर page का कुछ ही हिस्सा उपयोग हो तो बाकी व्यर्थ जाता है। फिर भी बड़े shared_buffers वाले production environments में यह trade-off आमतौर पर स्वीकार करने लायक है

आगे क्या हो सकता है

  • Preemption changes डिज़ाइन करने वाले Intel kernel engineer Peter Zijlstra ने PostgreSQL को Restartable Sequences (rseq) अपनाने का सुझाव दिया
    • rseq Linux kernel का ऐसा feature है जो userspace code को critical section के दौरान preemption या migration हुआ या नहीं, यह detect करने और उस section को restart करने की सुविधा देता है
    • PostgreSQL के spinlock path में rseq लगाने से वह स्थिति टाली जा सकती है जिसमें preempt हुआ lock holder सभी waiting backends को delay कर दे
  • PostgreSQL community की प्रतिक्रिया नकारात्मक रही
    • Linux 7.0 से पहले जो performance मुफ़्त में मिल रही थी, उसे वापस पाने के लिए अलग kernel feature अपनाना स्वीकार करना कठिन है
    • उनका मानना है कि यह kernel के पुराने सिद्धांत "userspace को नहीं तोड़ते" के खिलाफ है, यानी upgrade से पहले जो software सही चल रहा था, वह upgrade के बाद भी सही चलना चाहिए

अभी कोई टिप्पणी नहीं है.

अभी कोई टिप्पणी नहीं है.