हम एक पूरा auth system ऐसे stores पर चलाते हैं जो सिर्फ get और put जानते हैं
एक auth system देखने में ऐसा लगता है जैसे उसे एक relational database चाहिए। Users, roles, sessions, OAuth grants, refresh tokens, सब आपस में cross-referenced। हमारा एक भी इस्तेमाल नहीं करता। यह Azure Table Storage पर चलता है, एक ऐसा store जो आपको एक partition key, एक row key, और लगभग इसके अलावा कुछ नहीं देता। कोई joins नहीं, कोई मतलब का secondary index नहीं, कोई increment operator नहीं, और उस शक्ल में कोई multi-row transaction नहीं जिसकी आपको आदत है। यह एक सोचा-समझा फैसला था। यहाँ बताते हैं कि इस फैसले से क्या मिलता है, वे patterns जो इसे काम करने लायक बनाते हैं, और वह हिस्सा जहाँ SQL सचमुच आसान रहा होता।
ऐसा करने की वजह है लागत और operational सादगी। खत्म हो जाने वाला कोई connection pool नहीं, size करने या patch करने या fail over करने के लिए कोई database instance नहीं, per-partition scaling मुफ्त में मिलती है। एक table को रखने में लगभग कोई लागत नहीं आती और कुछ भी खत्म होने वाला नहीं है। इसकी कीमत है एक ऐसा store जिसमें कोई query planner नहीं, और यह तभी फायदेमंद होता है जब आपके access patterns उतने ही अनुमानित हों जितना यह store बुद्धू है। Auth के patterns ऐसे ही हैं, इसलिए पूरा design इसी पर टिका है।
जो काम एक table layout करता, वह यहाँ key करती है। joins न होने पर, आप key को इस तरह design करते हैं कि query ही key बन जाए। आप जान-बूझकर denormalize करते हैं, partitions ऐसे चुनते हैं कि आपके hot reads point-get बनें, और composite identities को सीधे row key में encode करते हैं, जैसे उन entries के लिए "{pk}|{rk}" जिन्हें एक ऐसे store में compound identity चाहिए जो सिर्फ एक ही row-key slot देता है। "इसे कैसे ढूंढूँ" वाले सवाल का जवाब आपको key design करते वक्त देना होता है, query के वक्त नहीं। auth के लिए यह ठीक है, क्योंकि access patterns पता हैं और वे बदलते नहीं: id से एक user लाना, एक user के grants लाना, एक code consume करना। आप data को टटोल नहीं रहे। हर सवाल आपको पहले से पता है।
अपना change log आप खुद रखते हैं। हर row में एक server-managed Timestamp होता है, और उस पर incremental backup बनाने का मन करता है: वह सब खींच लो जहाँ Timestamp gt watermark। यह demo में चलता है और production में दम तोड़ देता है, क्योंकि store सिर्फ partition key और row key को index करता है, इसके अलावा कुछ नहीं। Timestamp किसी भी index में नहीं है, इसलिए उस पर filter करना table की हर row को जाँचता है। यह query का भेस पहने एक full scan है, और table जितने दिन बढ़ती है यह उतना ही धीमा होता जाता है। तो इसके बजाय store अपना change log खुद लिखता है। हर mutation, हर upsert और हर delete, logical table name से keyed एक change-log table में एक row जोड़ देता है, जिसमें composite "{pk}|{rk}" row key होता है और एक flag बताता है कि row लिखी गई या delete हुई। अब "watermark के बाद क्या बदला" पूरी table के scan के बजाय एक छोटे, indexed partition का एक read है, और backup सिर्फ उन्हीं rows को point-read करता है जो असल में बदलीं। आप change-data-capture को आम writes से दोबारा बना लेते हैं, और यह table के size के बजाय churn के हिसाब से scale करता है।
बिना transactions के concurrency। यहाँ कोई SELECT ... FOR UPDATE नहीं है। इसके बदले आपको एक atomic primitive मिलता है: conditional write, जो आपको एक ETag की शक्ल में दिया जाता है। आप एक row तभी लिखते हैं जब आपने जो copy पढ़ी थी वह आपके नीचे बदली न हो। concurrent access में जिस भी चीज़ को सुरक्षा चाहिए, उसे optimistic concurrency और conflict पर retry के रूप में लिखा जाता है। सबसे साफ मिसाल है account-lockout counter। AccessFailedCount को atomically increment होना है, लेकिन store में कोई increment है ही नहीं। तो आप row पढ़ते हैं, count बढ़ाते हैं, और जो ETag आपने पढ़ा उस पर conditional रखकर वापस लिखते हैं, और अगर कोई आपसे पहले कर गया तो दोबारा कोशिश करते हैं। यही loop एक ऐसे store पर counter को atomic बनाने का तरीका है जिसमें कोई counter है ही नहीं। एक साथ पचास गलत passwords दाग दीजिए और उनमें से हर एक गिना जाएगा, क्योंकि conflict खोया नहीं जाता बल्कि पकड़ा जाता है और दोबारा किया जाता है।
एक delete भी एक lock हो सकता है। एक-बार-इस्तेमाल की खपत, यानी एक authorization code या एक one-time token जो ठीक एक ही बार redeem होना चाहिए, एक conditional delete है। यह एक ETag conditional-delete है: अगर delete कामयाब हुआ तो आप race जीत गए और आगे बढ़ सकते हैं; अगर यह इसलिए नाकाम हुआ कि row पहले ही गायब थी, तो किसी और ने उसे पहले consume कर लिया और आप रुक जाते हैं। delete ही lock है। यही तरकीब leader election भी करती है, जो एक blob lease पर बनी है, यानी किसी अलग coordination service के बजाय एक atomic write से बना एक lock। जब write खुद atomic हो, तो आपको किसी lock service की जरूरत लगभग कभी नहीं पड़ती।
Deletes को first-class होना ही पड़ता है, और यही आधी वजह है कि change log मौजूद है। एक key-value store में कोई delete marker नहीं होता: एक delete हुई row बस गायब हो जाती है, इसलिए बदलावों के लिए scan करने वाली कोई भी चीज़ यह देख तक नहीं सकती कि वह चली गई। row timestamps पर आधारित एक backup चुपचाप उस delete हुए user को हमेशा के लिए आगे ढोता रहेगा। change log delete को एक delete की तरह दर्ज करता है, इसलिए restore उसे दोबारा जिंदा करने के बजाय दोहरा देता है। वह failure mode, यानी एक ऐसा backup जो आपके हटाए हुए हर शख्स को ईमानदारी से वापस ले आता है, अपने आप में एक पूरी कहानी है और अपनी अलग post का हकदार है, लेकिन यह pattern इस सूची में आता है: जिस store में कोई delete log न हो, वहाँ delete log आप खुद रखते हैं।
अब बही का दूसरा पहलू, यानी आप क्या खोते हैं। आप ad-hoc queries खोते हैं: एक सचमुच का नया access pattern एक नए key design या एक full scan का मतलब हो सकता है, क्योंकि आपको बचाने के लिए कोई query planner नहीं है। आप joins खोते हैं, इसलिए denormalized copies को हाथ से बनाए रखते हैं और उनकी consistency के मालिक खुद होते हैं। आप multi-row transactions खोते हैं, इसलिए हर invariant को इस तरह design करते हैं कि वह एक ही item के अंदर रहे, क्योंकि आपको सिर्फ single-item atomicity ही मिलती है। अगर आपका data गहराई से relational है, या आपके access patterns अनजान हैं और अभी भी बदल रहे हैं, तो यह गलत औजार है और यह तकलीफ देगा।
और ठीक इसीलिए यह auth के लिए फिट बैठता है। एक relational database अपनी कीमत उन सवालों के जवाब देकर वसूल करता है जो आपने अभी सोचे तक नहीं। एक auth system के पास वे सवाल होते ही नहीं। आप जो चीज़ें पूछते हैं, यह user लाओ, यह code consume करो, यह leader चुनो, जो बदला उसका backup लो, उनका सेट छोटा, जाना-पहचाना, और स्थिर है। उस query planner को छोड़ दीजिए जिसे आप कभी इस्तेमाल करने ही वाले नहीं थे, और बदले में आपको एक storage layer मिलता है जिसे चलाने में लगभग कोई लागत नहीं और जिसमें fail over करने को कुछ है ही नहीं। ज्यादातर software के लिए यह एक बुरा सौदा है। इसके लिए, यह सही सौदा है।
ये patterns Authagonal के नीचे का storage engine हैं: एक key-value design जिसे चलाने में लगभग कोई लागत नहीं, और यही एक बड़ी वजह है कि हम हर feature को हर plan में रखते हैं।