- SQLite की file-based संरचना सरल है, लेकिन एक ही समय में कई write operations होने पर locking conflicts हो सकते हैं
- Jellyfin लंबे समय से SQLite का उपयोग करता रहा है, लेकिन कुछ सिस्टमों में transaction के दौरान database locked errors के कारण application रुक जाने की समस्या आई
- EF Core की interceptor सुविधा का उपयोग करके तीन locking strategies (No-Lock, Optimistic, Pessimistic) लागू की गईं, ताकि इस समस्या को कम किया जा सके
- Optimistic तरीका retry-आधारित है और performance loss को न्यूनतम रखता है, जबकि Pessimistic तरीका स्थिरता बढ़ाता है लेकिन speed कम कर देता है
- यह approach दूसरे EF Core applications पर भी आसानी से लागू की जा सकने वाली संरचना देती है, और SQLite concurrency समस्याओं के लिए एक व्यावहारिक विकल्प प्रदान करती है
SQLite की मूल संरचना और सीमाएँ
- SQLite एक file-based relational database engine है जो application के अंदर चलता है
- यह सारा data एक ही file में store करता है और किसी अलग server application की ज़रूरत नहीं होती
- क्योंकि application इस single file को पूरी तरह manage करता है, इसलिए एक साथ कई processes access करें तो conflict का जोखिम रहता है
- इसलिए SQLite का उपयोग करने वाले applications को एक समय में केवल एक write operation करना चाहिए
Write-Ahead-Log(WAL) mode
- SQLite, WAL(Write-Ahead-Log) फीचर के ज़रिए concurrency limitations को कम करता है
- WAL file, database changes को रिकॉर्ड करने वाली journal file की तरह काम करती है
- यह कई write operations को parallel queue कर सकती है और read के समय WAL के changes लागू करती है
- लेकिन WAL भी पूरी तरह परफेक्ट नहीं है, और कुछ परिस्थितियों में locking conflicts अब भी होते हैं
SQLite transaction problems
- Transactions, change operations की atomicity सुनिश्चित करने और read blocking को नियंत्रित करने का काम करते हैं
- Jellyfin के कुछ सिस्टमों में transaction के दौरान SQLite “database is locked” error लौटाकर तुरंत रुक जाता था
- यह समस्या operating system, disk speed, या virtualization की मौजूदगी से अलग रिपोर्ट की गई
- इसे reproduce करना कठिन था और यह अनियमित रूप से होती थी, इसलिए root cause पता लगाना मुश्किल था
Jellyfin में SQLite का उपयोग और समस्याएँ
- Recommended environment (non-network storage, SSD) में समस्या कम दिखती थी, लेकिन 10.11 से पहले के versions में parallel jobs limit bug के कारण
- library scan jobs बहुत ज़्यादा parallel में चलने लगे, जिससे हज़ारों concurrent write requests पैदा हुए
- SQLite engine की retry और timeout limits पार हो गईं, और database overload व errors होने लगे
- लंबे transactions और inefficient queries ने भी समस्या को और बढ़ाया
EF Core आधारित समाधान
- Jellyfin ने codebase को EF Core पर migrate करने के बाद बेहतर structural control हासिल किया
- EF Core के Interceptors का उपयोग करके सभी commands और transactions की execution intercept कर transparent locking control लागू किया गया
- तीन locking strategies लागू की गईं
- No-Lock: default mode, कोई अलग locking नहीं। ज़्यादातर मामलों में performance impact से बचने के लिए उपयोग
- Optimistic Locking: failure होने पर Polly library की मदद से retry
- Pessimistic Locking: हर write operation से पहले ReaderWriterLockSlim के ज़रिए पूरे database को lock
Optimistic Locking कैसे काम करता है
- इसमें माना जाता है कि operation सफल होगा, और failure पर retry किया जाता है
- अगर दो write operations टकराते हैं, तो एक fail होकर कुछ समय बाद फिर retry करता है
- Polly library का उपयोग करके केवल locking के कारण हुई failures को ही retry target बनाया गया
- Pessimistic तरीके की तुलना में overhead कम है और performance loss भी कम है
Pessimistic Locking कैसे काम करता है
- हर write operation के समय पूरा database lock कर दिया जाता है
- write के दौरान सभी read और write operations block हो जाते हैं
- यह तरीका सबसे स्थिर है, लेकिन सबसे धीमा भी
- उदाहरण के लिए, “Alice” table पढ़ते समय “Bob” table में write technically संभव हो, तब भी इसे अनुमति नहीं दी जाती
- ReaderWriterLockSlim का उपयोग करके multiple reads की अनुमति और केवल single write की अनुमति दी जाती है
आगे की योजना: Smart Locking
- Optimistic और Pessimistic तरीकों को मिलाकर Smart Locking लाने पर विचार हो रहा है
- उद्देश्य दोनों के फ़ायदे मिलाकर performance और stability के बीच संतुलन बनाना है
नतीजे और लागू करने की संभावना
- शुरुआती test results के अनुसार, दोनों locking modes समस्या हल करने में प्रभावी रहे
- समस्या की मूल वजह अब भी स्पष्ट नहीं है, लेकिन अब users के पास Jellyfin को स्थिर रूप से उपयोग करने का विकल्प है
- इंटरनेट पर भी ऐसी कई error reports थीं, लेकिन कोई पूरी तरह स्थापित समाधान मौजूद नहीं था
- Jellyfin का implementation EF Core interceptor-आधारित, आसानी से copy और apply किया जा सकने वाला ढाँचा देता है
- caller को अंदरूनी locking behavior के बारे में जानने की ज़रूरत नहीं होती
- वही SQLite concurrency समस्या झेल रहे दूसरे EF Core applications भी इसे तुरंत उपयोग में ला सकते हैं
2 टिप्पणियां
Hacker News राय
पहले मैंने SQLite की blocking समस्या झेली थी, और उसकी वजह डिस्क fragmentation थी
पुराने Android टैबलेट पर वे उपयोगकर्ता, जो ऐप को कई वर्षों तक रोज़ 8 घंटे इस्तेमाल करते थे, धीमापन और lock errors की शिकायत कर रहे थे
डेटा कॉपी करके लेने पर समस्या दोबारा सामने नहीं आती थी, लेकिन आखिर में डिवाइस को सीधे लेकर जांच करने पर पता चला कि DB फ़ाइल को नई जगह कॉपी करके फिर मूल नाम से वापस रखकर उसे 'defrag' करने पर समस्या पूरी तरह गायब हो गई
Jellyfin DB में भी इसी तरीके से performance improvement मिला
SQLite के transactions डिफ़ॉल्ट रूप से "deferred" mode में शुरू होते हैं
यानी असली write operation की कोशिश करने तक यह write lock नहीं लेता
SQLITE_BUSYerror तब आता है जब read transaction write में बदलने की कोशिश करे और उसी समय कोई दूसरा transaction पहले से write lock पकड़े होसमाधान है
busy_timeoutसेट करना, और जिन transactions में write शामिल हो वे "immediate" mode में शुरू करनाइसका अच्छा विवरण इस blog post में है
SQLITE_BUSYसमस्या है। ऐसे मामलों को मैंने यहाँ इकट्ठा किया हैSQLITE_BUSYएक तरह की architecture smell लगती है। WAL mode में मैं read-only connection pool और single write connection pool को अलग डिज़ाइन करता हूँ। इससे lock holding state साफ़ रहती है और contention situation को पहले से डिज़ाइन में शामिल किया जा सकता हैbusy_timeoutलागू नहीं होता। WAL mode में pages एक ही log file में append होते हैं, इसलिए अगर read के दौरान write में बदलने की कोशिश हो तो serialization guarantee के लिए SQLite तुरंत fail कर देता है। "immediate" mode इससे बचाता हैSQLITE_BUSYका ज़िक्र नहीं था, शायद configuration छूट गई थीलगता है लेख का कुछ हिस्सा गलत है
SQLite खुद lock management करता है, इसलिए application को सीधे file access control करने की ज़रूरत नहीं होती
और WAL कई parallel writes की अनुमति नहीं देता। यह सिर्फ read और single write को एक साथ होने देता है
SQLite एक शानदार database है, लेकिन इसके default settings बहुत ज़्यादा conservative हैं, यह खटकता है
production में इस्तेमाल करने के लिए कई PRAGMA settings समायोजित करनी पड़ती हैं
जब SQLite का नया hctree फीचर stable हो जाएगा, तब से मैं शायद सिर्फ SQLite ही इस्तेमाल करूँगा
नाम में
hcशायद High Concurrency का संक्षेप होगाofficial docs link
ऐसे लेख पढ़कर लगता है कि समस्या के root cause analysis की बजाय बस अस्थायी उपाय बताए गए हैं
गहरी debugging और शोध के ज़रिए सटीक कारण निकालना ही सच में मूल्यवान साझा होगा
लगता है यह भी ठीक से नहीं समझा गया कि WAL mode भी आखिरकार single write, multiple read संरचना ही है
parallel writes संभव नहीं हैं, यह सिर्फ इतना करता है कि read transactions writes से block न हों
पूरा MVCC होता तो अच्छा होता, लेकिन मौजूदा संरचना भी सिद्धांत समझ लेने पर काफ़ी अच्छी तरह काम करती है
मैंने भी Jellyfin में ऐसी ही समस्या झेली है
आम तौर पर सब ठीक चलता है, लेकिन कुछ स्थितियों में DB lock होकर अटक जाता है
logs में सिर्फ “database is locked” दिखता है, और आखिर में Docker container restart करना पड़ता है
यह ज़्यादातर TV UI में कई बटन तेज़ी से दबाने पर होता है
थोड़ा अलग विषय है, लेकिन अगर SQLite in-memory DB को बहुत सारे insert/delete कामों में इस्तेमाल करें तो memory usage लगातार बढ़ता जाता है
उदाहरण के लिए, हर 5 मिनट में 1 लाख rows डालकर हटाने का काम कई दिनों तक दोहराएँ तो macOS पर memory 1GB तक पहुँच जाती है
ऐसे मामले में कौन-सी setting समायोजित की जा सकती है, यह जानना चाहूँगा
auto_vacuumon है या नहीं, यह देखना चाहिएVACUUM docs
SQLite शानदार है, लेकिन ऐसी समस्याएँ देखकर कभी-कभी लगता है कि सीधे Postgres इस्तेमाल करना बेहतर होगा
अगर single-file portability या embedded use case नहीं है, तो Postgres concurrency समस्याएँ ज़्यादा आसानी से संभालता है
"हैं?" वाला हिस्सा था, इसलिए मैंने तुरंत पहले comments देखे, और जैसा उम्मीद थी...