समस्या और दायरा
एक यूज़र सर्विस PostgreSQL पर चलती है, और इसकी users टेबल में 200 मिलियन पंक्तियाँ हैं। username TEXT NOT NULL में एक यूनीक इंडेक्स है। हर ऑनलाइन रिक्वेस्ट, बैकग्राउंड जॉब और इवेंट कंज्यूमर वर्तमान में इसे पढ़ता और लिखता है। टीम इस फ़ील्ड का नाम बदलकर handle करना चाहती है; माइग्रेशन के दौरान, दोनों कॉलम बिल्कुल समान व्यावसायिक वैल्यू का प्रतिनिधित्व करते हैं।
सर्विस में 80 एप्लिकेशन इंस्टेंस हैं, और एक रोलिंग डिप्लॉयमेंट में 30 मिनट लगते हैं। बैकग्राउंड वर्कर्स वेब इंस्टेंस की तुलना में बाद में रीस्टार्ट हो सकते हैं। सामान्य ट्रैफ़िक को रोका नहीं जा सकता है, और कोई मेंटेनेंस विंडो नहीं है। मौजूदा SLO प्रभावी रहते हैं: यदि राइट p99, लॉक वेट, रेप्लिकेशन लैग, या डेटाबेस लोड प्रोडक्शन थ्रेशोल्ड को पार करता है, तो माइग्रेशन को धीमा (throttle) या बंद कर दिया जाना चाहिए। लक्ष्य प्रत्येक फेज़ को एक स्पष्ट इनवेरिएंट (invariant), एंट्री गेट, एग्जिट गेट और रोलबैक पाथ देते हुए नाम बदलने के काम को पूरा करना है।
200 मिलियन पंक्तियाँ, 80 इंस्टेंस और 30 मिनट का रोलआउट इंटरव्यू के अनुमान हैं। "ज़ीरो डाउनटाइम" का अर्थ मौजूदा SLO की रक्षा करते हुए कोई योजनाबद्ध आउटेज न होना है; इसका अर्थ शॉर्ट लॉक्स, विफल प्रयासों या थ्रॉटलिंग को अनदेखा करना नहीं है। मुख्य कौशल एक डेटाबेस परिवर्तन को कई फ़ॉरवर्ड- और बैकवर्ड-कम्पैटिबल रिलीज़ में विभाजित करना है, इसलिए यह एक बैकएंड प्रश्न है। शार् Harding, क्रॉस-डेटाबेस माइग्रेशन और मल्टी-प्राइमरी कन्फ़्लिक्ट्स पहले दौर के दायरे से बाहर हैं।
इंटरव्यूअर क्या मूल्यांकन कर रहा है
पहला संकेत यह है कि क्या उम्मीदवार कम्पैटिबिलिटी समस्या को पहचानता है। यदि टीम सीधे RENAME COLUMN username TO handle चलाती है, तो पुराने इंस्टेंस अभी भी username को एड्रेस करते हैं जबकि नए इंस्टेंस केवल handle को जानते हैं। 30 मिनट की मिक्स्ड-वर्जन विंडो के दौरान कम से कम एक संस्करण विफल हो जाता है। एक तेज़ डेटाबेस स्टेटमेंट एप्लिकेशन रोलआउट को ज़ीरो-डाउनटाइम नहीं बनाता है।
दूसरा संकेत स्कीमा माइग्रेशन, डेटा माइग्रेशन और कोड कटओवर को अलग करना है। एक सुरक्षित डिज़ाइन आमतौर पर expand-and-contract का पालन करता है: एक ऐसा स्ट्रक्चर जोड़ें जिसे पुराना कोड अनदेखा कर सके, कम्पैटिबल कोड डिप्लॉय करें, बैचों में बैकफ़िल और वैलिडेट करें, रीड और राइट को स्विच करें, और उसके बाद ही पुराने स्ट्रक्चर को हटाएं। प्रत्येक चरण स्वतंत्र रूप से डिप्लॉय करने योग्य, ऑब्ज़र्वेबल और पुनः प्रयास करने योग्य (retryable) होना चाहिए। 200 मिलियन पंक्तियों का अपडेट और विनाशकारी (destructive) DDL एक डिप्लॉयमेंट ट्रांज़ैक्शन में शामिल नहीं होते हैं।
तीसरा संकेत PostgreSQL लॉक और स्कैन सीमाओं को समझना है। विभिन्न ALTER TABLE सब-कमांड के लिए अलग-अलग लॉक्स की आवश्यकता होती है, और PostgreSQL तब तक ACCESS EXCLUSIVE लेता है जब तक कि दस्तावेज़ीकरण अन्यथा न कहे। बिना डिफ़ॉल्ट के नलेबल कॉलम जोड़ने से टेबल दोबारा नहीं लिखी जाती है, लेकिन फिर भी इसे लॉक की आवश्यकता होती है। यदि वह लॉक अनुरोध किसी लंबे ट्रांज़ैक्शन के पीछे कतार में लग जाता है, तो बाद के अनुरोध उसके पीछे कतारबद्ध हो सकते हैं। NOT VALID, VALIDATE CONSTRAINT, और CREATE INDEX CONCURRENTLY समवर्ती राइट्स पर प्रभाव को कम करते हैं, लेकिन प्रत्येक में अलग-अलग लॉक्स, लोड और विफलता-पुनर्प्राप्ति (failure-recovery) व्यवहार होता है।
अंत में, इंटरव्यूअर इनवेरिएंट-संचालित गेट्स की तलाश कर रहा है। एक मजबूत उत्तर केवल "कॉलम जोड़ें, डुअल-राइट करें, बैकफ़िल करें, कॉलम हटाएं" सूचीबद्ध करने से अधिक करता है। यह बताता है कि प्रत्येक राइटर कब कम्पैटिबल होता है, छूटे हुए राइट्स का पता कैसे लगाया जाता है, बैकफ़िल इडेम्पोटेंट (idempotent) क्यों है, केवल नए कॉलम को पढ़ना कब सुरक्षित हो जाता है, प्रत्येक फेज़ किस बिंदु पर रोलबैक कर सकता है, और पुराना कॉलम कब एक व्यावहारिक रिकवरी पाथ नहीं रह जाता है।
स्पष्टीकरण वाले प्रश्न
- सभी राइटर्स कहाँ हैं? वेब सर्विसेज़, वर्कर्स, शेड्यूल्ड जॉब्स, एडमिनिस्ट्रेटिव स्क्रिप्ट्स, डेटाबेस फ़ंक्शन्स, ट्रिगर्स, CDC, ETL, BI क्वेरीज़, व्यूज़ और बाहरी इंटीग्रेशन्स की इन्वेंट्री बनाएं। एक अपग्रेड न किया गया राइटर लगातार असंगति (inconsistency) को फिर से पैदा कर सकता है।
- क्या माइग्रेशन के दौरान कॉलम बिल्कुल समान रहने चाहिए? इस समस्या में, हाँ: यह पूरी तरह से नाम बदलना (rename) है। यदि
handleनॉर्मलाइज़ेशन, नए केस नियम, या यूज़र द्वारा चुने गए मान भी पेश करता है, तो कन्फ़्लिक्ट हैंडलिंग एक अलग डेटा-माइग्रेशन समस्या बन जाती है। usernameविशिष्टता (uniqueness) कैसे लागू की जाती है? नए कॉलम को एक समकक्ष यूनीक इंडेक्स या कंस्ट्रेंट की आवश्यकता होती है। पुष्टि करें कि केस संवेदनशीलता, कोलेशन, नल व्यवहार और कोई भी पार्शियल प्रेडिकेट नहीं बदलता है।- क्या एप्लिकेशन और डेटाबेस परिवर्तन अलग-अलग शिप हो सकते हैं? वे होने ही चाहिए। यदि स्कीमा और एप्लिकेशन केवल एक अविभाज्य ऑपरेशन के रूप में डिप्लॉय हो सकते हैं, तो टीम कई कम्पैटिबिलिटी फेज़ के माध्यम से आगे नहीं बढ़ सकती है।
- माइग्रेशन कितने समय तक चल सकता है? 200 मिलियन पंक्तियों को बैकफ़िल करने में घंटों या दिन लग सकते हैं। डिज़ाइन को पॉज़, रिज़्यूमे, पूर्णता और पुराने कॉलम के रिटेंशन की नीतियों की आवश्यकता होती है जो कई रिलीज़ तक बनी रहें।
- रोलबैक का क्या अर्थ है? एप्लिकेशन कोड को रोल बैक करना, बैकफ़िल को रोकना, पुराने कॉलम के राइट्स को रीस्टोर करना और ड्रॉप किए गए कॉलम को रिकवर करना चार अलग-अलग ऑपरेशन्स हैं। पुराने कॉलम को हटा दिए जाने के बाद एक सामान्य एप्लिकेशन रोलबैक अब सुरक्षित नहीं है।
30-सेकंड का उत्तर
"मैं उसी स्थान पर (in place) नाम नहीं बदलूँगा। मैं expand-and-contract का उपयोग करूँगा। पहले एक शॉर्ट lock_timeout के साथ एक नलेबल handle जोड़ें; यदि लॉक अनुपलब्ध है, तो विफल हो जाएं और पुनः प्रयास करें। कम्पैटिबिलिटी रिलीज़ A अभी भी username को पढ़ती है लेकिन दोनों कॉलमों पर एटॉमिक रूप से लिखती है। सभी 80 इंस्टेंस और प्रत्येक वर्कर के अपग्रेड होने के बाद, छोटे, इडेम्पोटेंट प्राइमरी-की बैचों में handle IS NULL वाली पंक्तियों को बैकफ़िल करें। पूरे समय नल्स, मिसमैच, राइट एरर्स, लॉक्स, p99, रेप्लिकेशन लैग, WAL और ऑटोवैक्यूम की निगरानी करें। बैकफ़िल के बाद, नए यूनीक इंडेक्स को समवर्ती रूप से (concurrently) बनाएं, एक NOT VALID चेक जोड़ें, इसे अलग से वैलिडेट करें, और फिर NOT NULL सेट करें। रिलीज़ B, username फ़ॉलबैक के साथ handle को प्राथमिकता देती है और डुअल राइट्स बनाए रखती है। ऑब्ज़र्वेशन के बाद, केवल handle को पढ़ें, फिर पुराने-कॉलम के राइट्स को रोक दें। पुराने कॉलम को पूरी रोलबैक विंडो के दौरान बनाए रखें और कोड, जॉब्स, व्यूज़ और कंज्यूमर्स द्वारा इसे संदर्भित न किए जाने के बाद ही इसे ड्रॉप करें। एक विफल गेट सिस्टम को उसके वर्तमान कम्पैटिबल फेज़ में छोड़ देता है; कॉलम को ड्रॉप करने को एक सामान्य रोलबैक पॉइंट के रूप में नहीं माना जाता है।"
चरण-दर-चरण समाधान
पहले माइग्रेशन इनवेरिएंट्स और गेट्स को परिभाषित करें
DDL निष्पादित करने से पहले, चार इनवेरिएंट्स लिखें:
- मिक्स्ड वर्जन्स के दौरान,
usernameरोलबैक के लिए सिंगल सोर्स ऑफ ट्रुथ बना रहता है।handleनल हो सकता है, लेकिन एक नॉन-नल मानusernameके बराबर होना चाहिए। - कम्पैटिबिलिटी रिलीज़ के प्रत्येक राइटर तक पहुँचने के बाद, प्रत्येक नए राइट को एक ट्रांज़ैक्शन में दोनों कॉलमों को अपडेट करना चाहिए। एक कॉलम दूसरे के बिना सफल नहीं हो सकता।
- रीड्स को पुराने कॉलम से तब तक अलग नहीं किया जा सकता जब तक कि नल और नॉन-नल मिसमैच की संख्या शून्य न हो जाए, सभी राइटर्स कम्पैटिबल न हों, और नया इंडेक्स और कंस्ट्रेंट्स मान्य न हों।
- स्कीमा कॉन्ट्रैक्शन तब तक शुरू नहीं हो सकता जब तक कि प्रोडक्शन कोड, बैकग्राउंड जॉब्स, व्यूज़, रिपोर्ट्स, CDC और रोलबैक बिल्ड्स को अब
usernameकी आवश्यकता न हो।
प्रत्येक फेज़ के लिए डिप्लॉयमेंट वर्शन, प्रारंभ समय, ओनर, प्रोग्रेस कर्सर, डैशबोर्ड और एग्जिट कंडीशन्स रिकॉर्ड करें। माइग्रेशन रनर को एक लीज़ या PostgreSQL एडवाइजरी लॉक रखना चाहिए ताकि दो एक्ज़ीक्यूटर्स एक साथ एक ही स्टेप को आगे न बढ़ा सकें। प्रत्येक स्टेप को एक यूनीक वर्शन और ड्यूरेबल सक्सेस रिकॉर्ड की भी आवश्यकता होती है, जिससे रीस्टार्ट होने पर पूरे माइग्रेशन को फिर से चलाने के बजाय उसे वहीं से रिज़्यूमे किया जा सके।
फेज़ एक: व्यवहार बदले बिना स्कीमा का विस्तार (Expand) करें
प्रोडक्शन-साइज़ कॉपी पर DDL का रिहर्सल करें और लंबे ट्रांज़ैक्शन, लॉक कतारों और डिस्क हेडरूम का निरीक्षण करें। बिना डिफ़ॉल्ट के एक नलेबल कॉलम जोड़ें:
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE users ADD COLUMN handle text;
COMMIT;बिना डिफ़ॉल्ट वाला एक नलेबल कॉलम 200 मिलियन पंक्तियों को दोबारा नहीं लिखता है, लेकिन ALTER TABLE को अभी भी एक मजबूत लॉक की आवश्यकता होती है। lock_timeout ऐसे प्रयास को विफल कर देता है जो जल्दी से लॉक प्राप्त नहीं कर सकता है, जिससे डिप्लॉयमेंट सिस्टम जिटर (jitter) के साथ बाद में पुनः प्रयास कर सकता है। निष्पादन से पहले असामान्य लंबे ट्रांज़ैक्शन की पहचान करें और निरीक्षण करें कि निष्पादन के दौरान कौन किसे ब्लॉक कर रहा है। अनिश्चित काल तक प्रतीक्षा करना असुरक्षित है क्योंकि एक कतारबद्ध मजबूत लॉक के कारण बाद के टेबल अनुरोध उसके पीछे जमा हो सकते हैं।
इस चरण के बाद पुराने एप्लिकेशन handle को अनदेखा करते हैं, इसलिए एप्लिकेशन रोलबैक स्वतंत्र रहता है। इस जोड़ को NOT NULL, एक वोलेटाइल डिफ़ॉल्ट, एक यूनीक कंस्ट्रेंट और एक पूर्ण अपडेट के साथ न मिलाएं। यह मेटाडेटा परिवर्तन, टेबल स्कैन, इंडेक्स निर्माण और डेटा राइट प्रवर्धन को एक उच्च जोखिम वाले ऑपरेशन में जोड़ देगा।
फेज़ दो: कम्पैटिबल राइटर्स डिप्लॉय करें जबकि पुराना कॉलम रीड सोर्स बना रहे
रिलीज़ A निम्नानुसार व्यवहार करती है:
- रीड्स केवल
usernameका उपयोग करना जारी रखते हैं, इसलिए यूज़र-विज़िबल व्यवहार अपरिवर्तित रहता है। - यूज़र क्रिएशन और अपडेट एक SQL ट्रांज़ैक्शन में
usernameऔरhandleमें समान मान लिखते हैं। - किसी भी कॉलम पर विफलता पूरे ट्रांज़ैक्शन को रोल बैक कर देती है; डुअल राइट्स दो एसिंक्रोनस रिक्वेस्ट नहीं हैं।
- मेट्रिक्स वास्तविक यूज़रनेम लॉग किए बिना डुअल-राइट प्रयासों, विफलताओं और मिसमैच को रिकॉर्ड करते हैं।
रिलीज़ A रोलआउट के दौरान, जो इंस्टेंस अपग्रेड नहीं हुए हैं वे अभी भी केवल username लिखते हैं, इसलिए नई handle IS NULL पंक्तियाँ अस्थायी रूप से मान्य हैं। यह अपवाद केवल तभी समाप्त होता है जब सभी 80 वेब इंस्टेंस, वर्कर्स, शेड्यूल्ड जॉब्स और स्वतंत्र कंज्यूमर्स एक कम्पैटिबल वर्शन की रिपोर्ट करते हैं। यदि टीम प्रत्येक राइटर का हिसाब नहीं रख सकती है—उदाहरण के लिए, कोई थर्ड-पार्टी प्रोग्राम सीधे डेटाबेस में लिखता है—तो एक अस्थायी डेटाबेस ट्रिगर कॉलमों को सिंक्रनाइज़ कर सकता है। इसका नकारात्मक पहलू छिपा हुआ राइट व्यवहार, अतिरिक्त लागत और अधिक जटिल रेप्लिकेशन, CDC और इंसिडेंट डायग्नोसिस है। ऐसे ट्रिगर को हटाने की तारीख के साथ माइग्रेशन इंफ्रास्ट्रक्चर के रूप में समझें।
फेज़ तीन: प्राइमरी-की रेंज द्वारा इडेम्पोटेंट रूप से बैकफ़िल करें
बैकग्राउंड माइग्रेशन केवल तभी शुरू करें जब प्रत्येक राइटर कम्पैटिबल हो। OFFSET के बजाय प्राइमरी-की कीसेट रेंज का उपयोग करें, और प्रत्येक बैच को एक छोटे ट्रांज़ैक्शन में रखें:
UPDATE users
SET handle = username
WHERE id > $1
AND id <= $2
AND handle IS NULL;handle IS NULL प्रेडिकेट बैच को सुरक्षित रूप से पुनः प्रयास करने योग्य बनाता है और किसी ऑनलाइन अनुरोध द्वारा पहले से लिखे गए मान को अधिलेखित (overwrite) करने से बचाता है। अंतिम पूर्ण प्राइमरी-की रेंज को सुरक्षित रखें (persist करें)। रनर किसी भी बैच के बाद रुक सकता है और रीस्टार्ट होने के बाद अंतिम पुष्ट रेंज से जारी रह सकता है। एक वर्कर के साथ शुरुआत करें, फिर पहले से निश्चित थ्रूपुट का वादा किए बिना मापों के आधार पर बैच साइज़ और विलंब को ट्यून करें।
प्रत्येक बैच के बाद, राइट p99, डेटाबेस CPU, लॉक वेट, रेप्लिकेशन लैग, WAL, डिस्क, डेड टुपल्स और ऑटोवैक्यूम देखें। जैसे ही कोई भी माप अपने प्रोडक्शन थ्रेशोल्ड के करीब पहुंचता है, बैच का आकार कम करें या रोक दें। उद्देश्य SLO के भीतर स्थिर पूर्णता है। केवल इसलिए कि स्क्रिप्ट छोटी है, एकल 200-मिलियन-पंक्ति ट्रांज़ैक्शन उचित नहीं है।
एक ही समय में दो स्वतंत्र वैलिडेशन चलाएं:
SELECT count(*) FROM users WHERE handle IS NULL;
SELECT count(*)
FROM users
WHERE handle IS DISTINCT FROM username;पहला काउंट लगातार शून्य पर गिरना चाहिए। दूसरा नल अंतर और असमान नॉन-नल मान दोनों को कैप्चर करने के लिए IS DISTINCT FROM का उपयोग करता है। कोई भी गैर-शून्य परिणाम कटओवर को रोकता है और प्राइमरी-की रेंज द्वारा जांचा जाता है। बैकफ़िल को किसी गैर-शून्य कन्फ़्लिक्ट को चुपचाप ओवरराइट नहीं करना चाहिए क्योंकि यह किसी पुराने राइटर या गलत नए लॉजिक को छिपा सकता है जो अभी भी सक्रिय है।
फेज़ चार: इंडेक्स और कंस्ट्रेंट्स जोड़ें
चूँकि username यूनीक है, नए कॉलम को एक समकक्ष यूनीक इंडेक्स की आवश्यकता है। बैकफ़िल और डुप्लिकेट वैलिडेशन के बाद, इसे ट्रांज़ैक्शन ब्लॉक के बाहर निष्पादित करें:
CREATE UNIQUE INDEX CONCURRENTLY users_handle_key
ON users (handle);एक समवर्ती निर्माण सामान्य राइट्स को जारी रखने की अनुमति देता है, लेकिन यह अधिक काम करता है, टेबल को दो बार स्कैन करता है, और प्रासंगिक ट्रांज़ैक्शन की प्रतीक्षा करता है। यह अभी भी CPU और I/O की खपत करता है। एक विफल निर्माण पीछे एक INVALID इंडेक्स छोड़ सकता है। रिकवरी को कैटलॉग का निरीक्षण करना चाहिए, विफल आर्टिफ़ैक्ट को हटाना चाहिए, और कारण को ठीक करने के बाद पुनः प्रयास करना चाहिए; केवल एक कमांड विफलता यह साबित नहीं करती है कि डेटाबेस अपनी मूल स्थिति में वापस आ गया है।
नॉन-नलेबिलिटी को भी फेज़ेज़ में स्थापित करें:
ALTER TABLE users
ADD CONSTRAINT users_handle_not_null
CHECK (handle IS NOT NULL) NOT VALID;
ALTER TABLE users
VALIDATE CONSTRAINT users_handle_not_null;
ALTER TABLE users
ALTER COLUMN handle SET NOT NULL;
ALTER TABLE users
DROP CONSTRAINT users_handle_not_null;NOT VALID पुरानी पंक्तियों को तुरंत स्कैन किए बिना बाद के राइट्स के लिए चेक को लागू करना शुरू कर देता है। VALIDATE CONSTRAINT फिर निचले लॉक स्तर के साथ मौजूदा पंक्तियों की जांच करता है। एक मान्य CHECK यह साबित करता है कि कोई नल नहीं है, जिससे बाद के SET NOT NULL को अपने सामान्य फ़ुल-टेबल स्कैन को छोड़ने की अनुमति मिलती है। प्रत्येक DDL स्टेटमेंट को अभी भी एक छोटा लॉक वेट, एक अलग निष्पादन चरण और प्रोडक्शन मॉनिटरिंग मिलती है। "समवर्ती" और "नो रीराइट" का अर्थ "मुफ़्त" नहीं है।
फेज़ पाँच: रीड्स को कट ओवर करें, फिर पुराने-कॉलम राइट्स को रोकें
रिलीज़ B, handle को प्राथमिकता देती है, नल होने पर username पर वापस जाती है, और एटॉमिक डुअल राइट्स जारी रखती है। गेट कहता है कि नल्स पहले से ही शून्य होने चाहिए, लेकिन फ़ॉलबैक रोलबैक के लिए कम्पैटिबिलिटी को सुरक्षित रखता है और अप्रत्याशित डेटा को अलग करता है। कैनेरी के साथ शुरुआत करें, धीरे-धीरे विस्तार करें, और नए-कॉलम और पुराने-कॉलम के परिणामों, त्रुटियों और व्यावसायिक मेट्रिक्स की तुलना करें।
ऑब्ज़र्वेशन विंडो के बाद, रिलीज़ C दोनों कॉलम लिखना जारी रखते हुए केवल handle पढ़ती है। एक रीड-पाथ समस्या अभी भी B या A पर रोल बैक कर सकती है क्योंकि username अप-टू-डेट रहता है। बैकग्राउंड जॉब्स, कम-आवृत्ति वाले एंडपॉइंट्स और एक पूर्ण डिप्लॉयमेंट चक्र को कवर करने वाली अवलोकन अवधि के बाद ही रिलीज़ D को username लिखना बंद करना चाहिए।
पुराने-कॉलम राइट्स को रोकने से रोलबैक सिमेंटिक्स बदल जाते हैं। बाद में ऐसे बिल्ड पर रोलबैक के लिए जो केवल username जानता है, पहले डुअल राइट्स को रीस्टोर करने और अंतराल के दौरान बनाए गए मानों को रिवर्स-बैकफ़िल करने की आवश्यकता होती है। एप्लिकेशन को सीधे रोल बैक करने से पुराना (stale) डेटा सामने आ जाएगा। इस आवश्यकता को रनबुक और डिप्लॉयमेंट गेट में रखें ताकि इंसिडेंट हैंडलिंग किसी इंजीनियर की याददाश्त पर निर्भर न रहे।
फेज़ छह: कॉन्ट्रैक्शन में देरी करें
हटाने से पहले, यह साबित करने के लिए कोड सर्च, क्वेरी लॉग, डिपेंडेंसी कैटलॉग और कंज्यूमर इन्वेंट्री का उपयोग करें कि username का कोई रीडर नहीं है। पुराने इंडेक्स, कंस्ट्रेंट्स, ट्रिगर्स और व्यू डिपेंडेंसीज़ को पहले हटाएं, फिर एक अलग रिलीज़ में कॉलम को ड्रॉप करें:
ALTER TABLE users DROP COLUMN username;कॉलम को ड्रॉप करना एक विनाशकारी सीमा को पार करता है। भले ही PostgreSQL टेबल को तुरंत दोबारा न लिखे, पुराने एप्लिकेशन, स्कीमा कैश, व्यूज़ और बाहरी क्वेरीज़ तुरंत विफल हो सकती हैं। हटाया गया डेटा एक साधारण एप्लिकेशन रोलबैक के लिए भी अनुपलब्ध होता है। ड्रॉप को रीड और राइट कटओवर के बाद कम से कम एक पूर्ण रोलबैक-रिटेंशन विंडो के बाद होना चाहिए, उस कोड रिलीज़ से अलग जो कॉलम का उपयोग करना बंद कर देता है। बैकअप डिजास्टर रिकवरी है, लो-लेटेंसी डिप्लॉयमेंट रोलबैक नहीं।
एक मजबूत उत्तर का उदाहरण
"मुख्य जोखिम 30 मिनट के रोलिंग डिप्लॉयमेंट के दौरान कम्पैटिबिलिटी का है। मैं इस बदलाव को विस्तार (expansion), कम्पैटिबल राइट्स, बैकफ़िल और वैलिडेशन, रीड कटओवर और विलंबित कॉन्ट्रैक्शन में विभाजित करूँगा।
पहले मैं एक छोटे lock_timeout के साथ नलेबल handle जोड़ता हूँ; यदि लॉक उपलब्ध नहीं है, तो प्रयास विफल हो जाता है और पुनः प्रयास करता है। बिना डिफ़ॉल्ट वाला एक नलेबल कॉलम टेबल को दोबारा नहीं लिखता है, लेकिन DDL अभी भी एक मजबूत लॉक लेता है, इसलिए मैं लंबे ट्रांज़ैक्शन और लॉक कतार का निरीक्षण करता हूँ। रिलीज़ A अभी भी username पढ़ती है, और प्रत्येक राइटर एक डेटाबेस ट्रांज़ैक्शन में दोनों कॉलम अपडेट करता है। बैकफ़िल सभी 80 इंस्टेंस, वर्कर्स और कंज्यूमर्स के अपग्रेड होने के बाद ही शुरू होता है।
बैकफ़िल UPDATE ... WHERE handle IS NULL के साथ छोटे प्राइमरी-की रेंज ट्रांज़ैक्शन का उपयोग करता है, प्रगति को बनाए रखता है, और पुनः प्रयास करने के लिए सुरक्षित है। इसकी दर प्रोडक्शन p99, रेप्लिकेशन लैग, WAL, डेड टुपल्स और ऑटोवैक्यूम का अनुसरण करती है। नल काउंट शून्य तक पहुंचना चाहिए, और handle IS DISTINCT FROM username शून्य रहना चाहिए। एक गैर-शून्य कन्फ़्लिक्ट को ओवरराइट करने के बजाय उसकी जांच की जाती है।
डेटा गेट के बाद, मैं CREATE UNIQUE INDEX CONCURRENTLY के साथ समकक्ष यूनीक इंडेक्स बनाता हूँ और विफलता के बाद किसी भी बचे हुए अमान्य इंडेक्स को हैंडल करता हूँ। मैं CHECK ... NOT VALID जोड़ता हूँ, इसे अलग से वैलिडेट करता हूँ, और फिर NOT NULL सेट करता हूँ। रिलीज़ B पुराने-कॉलम फ़ॉलबैक के साथ नए कॉलम को प्राथमिकता देती है और डुअल राइट्स बनाए रखती है। रिलीज़ C केवल नए कॉलम को पढ़ती है लेकिन फिर भी डुअल-राइट करती है। रिलीज़ D एक पूर्ण अवलोकन चक्र के बाद ही पुराने-कॉलम राइट्स को रोकती है।
प्रत्येक फेज़ का एक रोलबैक पाथ होता है। रीड कटओवर से पहले मैं एप्लिकेशन को सीधे रोल बैक कर सकता हूँ। नए कॉलम को पढ़ते समय लेकिन फिर भी डुअल-राइटिंग करते समय, मैं पुराने रीड्स पर वापस लौट सकता हूँ। पुराने राइट्स बंद होने के बाद, पुराने एप्लिकेशन के सुरक्षित होने से पहले मुझे डुअल राइट्स को रीस्टोर करना होगा और रिवर्स-बैकफ़िल करना होगा। अंत में, कोड, जॉब्स, व्यूज़, CDC और रिपोर्ट्स द्वारा username को संदर्भित न किए जाने और रोलबैक विंडो बीत जाने के बाद, मैं इसे एक अलग रिलीज़ में ड्रॉप कर देता हूँ। एक विफल गेट सिस्टम को विनाशकारी कॉन्ट्रैक्शन की ओर ले जाने के बजाय एक कम्पैटिबल स्थिति में छोड़ देता है।"
सामान्य गलतियाँ
- कॉलम का नाम उसी स्थान पर बदलना → रोलआउट के दौरान पुराने और नए इंस्टेंस को अलग-अलग नामों की आवश्यकता होती है → एक नए कॉलम और कई कम्पैटिबल रिलीज़ का उपयोग करें।
- एक ट्रांज़ैक्शन में 200 मिलियन पंक्तियों को अपडेट करना → ट्रांज़ैक्शन WAL, लॉक्स, रेप्लिकेशन लैग, ब्लोट और रिकवरी समय को बढ़ाता है → छोटे, इडेम्पोटेंट प्राइमरी-की रेंज बैचों का उपयोग करें।
- डुअल राइट्स शुरू होते ही बैकफ़िल शुरू करना → जो इंस्टेंस अपग्रेड नहीं हुए हैं वे अभी भी नए नल्स बना सकते हैं → शून्य नल्स को एक गेट बनाने से पहले प्रत्येक राइटर के कम्पैटिबल होने तक प्रतीक्षा करें।
- डुअल राइट्स को दो अनुरोधों के रूप में लागू करना → एक टाइमआउट केवल एक कॉलम को अपडेट कर सकता है → एक डेटाबेस ट्रांज़ैक्शन में दोनों को एटॉमिक रूप से अपडेट करें।
- केवल
handle IS NULLकी जाँच करना → असमान गैर-शून्य मान पकड़ में नहीं आते हैं →IS DISTINCT FROMकी भी जाँच करें और कन्फ़्लिक्ट्स की जाँच करें। ADD COLUMNको लॉक-मुक्त मानना → मेटाडेटा DDL को अभी भी एक टेबल लॉक की आवश्यकता होती है, और एक कतारबद्ध DDL अनुरोध ब्लॉकिंग को बढ़ा सकता है → शॉर्ट लॉक वेट, रिट्रीज़, और लॉक/ट्रांज़ैक्शन मॉनिटरिंग का उपयोग करें।- यूनीक इंडेक्स को सामान्य रूप से बनाना → एक बड़े-टेबल का इंडेक्स निर्माण अस्वीकार्य अवधि के लिए राइटर्स को ब्लॉक कर सकता है →
CONCURRENTLYका उपयोग करें और अतिरिक्त लोड और अमान्य-इंडेक्स रिकवरी को संभालें। - बैकफ़िल के तुरंत बाद पुराने कॉलम को ड्रॉप करना → कम-आवृत्ति वाले वर्कर्स, व्यूज़, स्कीमा कैश, या रोलबैक बिल्ड्स को अभी भी इसकी आवश्यकता हो सकती है → एक पूर्ण अवलोकन और रोलबैक विंडो तक कॉन्ट्रैक्शन में देरी करें।
- बैकअप को रोलबैक बटन के रूप में मानना → 200-मिलियन-पंक्ति डेटाबेस को रीस्टोर करना एप्लिकेशन रोलबैक की तुलना में बहुत धीमा और जोखिम भरा है → विनाशकारी सीमा तक एक ऑनलाइन-कम्पैटिबल स्ट्रक्चर बनाए रखें।
- "ज़ीरो-इम्पैक्ट माइग्रेशन" का वादा करना → DDL, इंडेक्स और बैकफ़िल सभी लॉक्स या संसाधनों का उपभोग करते हैं → बिना किसी योजनाबद्ध आउटेज का वादा करें, SLO की रक्षा करें, और थ्रेशोल्ड पार होने से पहले रोकें।
अनुवर्ती प्रश्न
डिफ़ॉल्ट मान के साथ handle क्यों न जोड़ें?
PostgreSQL एक नॉन-वोलेटाइल कॉन्स्टेंट डिफ़ॉल्ट के लिए टेबल रीराइट से बच सकता है, लेकिन कोई भी कॉन्स्टेंट "इस पंक्ति के मौजूदा username को कॉपी करें" व्यक्त नहीं करता है। भौतिक रूप से तेज़ डिफ़ॉल्ट को भी DDL लॉक की आवश्यकता होती है और यह पुराने इंस्टेंस द्वारा केवल पुराने कॉलम में लिखने की समस्या को हल नहीं करता है। इस माइग्रेशन को अभी भी कम्पैटिबल राइटर्स और डेटा बैकफ़िल की आवश्यकता है। भविष्य के इन्सर्ट्स को डिफ़ॉल्ट की आवश्यकता है या नहीं, यह एक व्यावसायिक-अर्थ संबंधी निर्णय है, रोलआउट योजना का विकल्प नहीं।
क्या होगा यदि handle को लोअरकेस किया जाना चाहिए और फिर से यूनीक बनाया जाना चाहिए?
वह अब केवल नाम बदलना नहीं रह जाता है। एक नॉर्मलाइज़ेशन फ़ंक्शन और कन्फ़्लिक्ट पॉलिसी को परिभाषित करें, फिर रेप्लिकेट या ऑफ़लाइन जॉब पर lower(username) में टकरावों की गणना करें। तय करें कि किसी मान को संरक्षित करना है, प्रत्यय जोड़ना है, या यूज़र कार्रवाई की आवश्यकता है। बैकफ़िल के दौरान मूल मान और रूपांतरण स्थिति संग्रहीत करें, और कन्फ़्लिक्ट्स शून्य तक पहुंचने के बाद ही यूनीक इंडेक्स बनाएं। ऑनलाइन राइट्स और बैकफ़िल को समान नॉर्मलाइज़ेशन कार्यान्वयन का उपयोग करना चाहिए।
क्या होगा यदि सभी राइटर्स एक साथ अपग्रेड नहीं हो सकते?
एक बाहरी सिस्टम के लिए जो जल्दी से माइग्रेट नहीं हो सकता है, एक अस्थायी डेटाबेस ट्रिगर जोड़ें जो पुराने-कॉलम के राइट को नए कॉलम में कॉपी करता है और उपयोग रिकॉर्ड करता है ताकि शेष कॉलर को ढूंढा जा सके। ट्रिगर को ऐसे अनुरोधों को अस्वीकार करना चाहिए जो परस्पर विरोधी मान प्रदान करते हैं। रिकर्शन, रेप्लिकेशन और CDC व्यवहार का मूल्यांकन करें। एक बार प्रत्येक कॉलर के माइग्रेट हो जाने के बाद, ट्रिगर को हटाने से पहले अक्षम करें और निरीक्षण करें ताकि छिपा हुआ व्यावसायिक तर्क स्थायी न बने।
क्या होगा यदि बैकफ़िल के दौरान रेप्लिकेशन लैग बढ़ता रहता है?
नए बैचों को रोकें और प्रगति का पीछा करने के लिए वर्कर्स को जोड़ने के बजाय रेप्लिकास को बराबरी करने दें। बैच आकार, कमिट कैडेंस, WAL दर, लंबी क्वेरीज़ और ऑटोवैक्यूम का निरीक्षण करें। छोटे बैचों और कम समवर्तीता (concurrency) के साथ फिर से शुरू करें। यदि रीड्स रेप्लिकास पर निर्भर करते हैं, तो रेप्लिकेशन लैग पहले से ही एक यूज़र-विज़िबल जोखिम है; बैकफ़िल पूर्णता तिथि प्रोडक्शन SLO के अधीन है।
क्या CREATE UNIQUE INDEX CONCURRENTLY को विफलता के बाद बस फिर से चलाया जा सकता है?
इसे आँख बंद करके दोबारा न चलाएँ। समान नाम वाला एक अमान्य इंडेक्स कैटलॉग में रह सकता है, और एक समवर्ती यूनीक निर्माण किसी विफल फेज़ के दौरान अन्य ट्रांज़ैक्शन के विरुद्ध पहले से ही विशिष्टता लागू कर चुका हो सकता है। इंडेक्स की वैधता का निरीक्षण करें, डेटा या संसाधन विफलता की पहचान करें, रनबुक के अनुसार विफल इंडेक्स को हटाएं, और फिर दोबारा बनाएं। कमांड एक नियमित ट्रांज़ैक्शन ब्लॉक के अंदर भी नहीं चल सकता है, इसलिए माइग्रेशन टूल को उस निष्पादन मोड का समर्थन करना चाहिए।
किस फेज़ को रोल बैक करना सबसे कठिन है?
पुराने-कॉलम के राइट्स बंद होने के बाद, पुराना मान पिछड़ने लगता है। पुराने कॉलम को हटा दिए जाने के बाद, डेटा और स्कीमा दोनों एक विनाशकारी सीमा को पार कर जाते हैं। पुराने वाले के लिए पुराने बिल्ड के सुरक्षित होने से पहले डुअल राइट्स को रीस्टोर करने और रिवर्स-बैकफ़िल करने की आवश्यकता होती है। बाद वाले के लिए आमतौर पर फ़ॉरवर्ड रिपेयर या बैकअप रीस्टोरेशन की आवश्यकता होती है। इन कार्रवाइयों को अलग-अलग रिलीज़ में रखें और पुराने कॉलम को पर्याप्त लंबी अवलोकन विंडो के लिए अद्यतित रखें।