हफ्तों तक, Elevare Digital में एक cron job निर्धारित समय पर जागता, अपनी कतार (queue) की जाँच करता और सफलतापूर्वक पूरा होने का लॉग दर्ज करता। उसने ठीक शून्य ड्राफ्ट्स को मंजूरी दी। उन्नीस कंटेंट पीस इंतज़ार कर रहे थे। टीम को इसका पता बाद में चला, जब वह खामोश अंतराल एक अजीबोगरीब बात से बढ़कर एक छोटा बैकलॉग बन गया। कुछ भी क्रैश नहीं हुआ था। कोई पेजिंग अलर्ट नहीं आया। सिस्टम तकनीकी रूप से स्वस्थ था लेकिन कार्यात्मक रूप से मृत था।

यह स्वायत्त पाइपलाइनों (autonomous pipelines) का शांत आतंक है। जब आप लूप से इंसान को हटा देते हैं, तो आप उस व्यक्ति को भी हटा देते हैं जो यह नोटिस करता है कि कुछ भी नहीं हो रहा है।

वह पाइपलाइन जो खुद ही चलती रही

Elevare Digital एक पूरी तरह से स्वचालित कंटेंट वर्कफ़्लो चलाता है। सॉफ्टवेयर एजेंट ड्राफ्ट्स तैयार करते हैं। एक शेड्यूल्ड अप्रूवर cron गेटकीपर के रूप में कार्य करता है, उन ड्राफ्ट्स की समीक्षा करता है और स्वीकृत आइटम्स को सीधे पब्लिशिंग के लिए भेज देता है। कोई भी इंसान हर बैच को मंजूरी देने के लिए डैशबोर्ड नहीं खोलता। इसका पूरा उद्देश्य यह है कि मशीन उबाऊ कामों को संभाल ले ताकि टीम अन्य समस्याओं पर ध्यान दे सके।

इस मॉडल के तहत, भरोसा ही आपका प्राथमिक इंटरफ़ेस बन जाता है। आप शेड्यूलर के चलने पर भरोसा करते हैं। आप जॉब के चलने पर भरोसा करते हैं। आप एग्जिट कोड (exit code) पर भरोसा करते हैं। जब लॉग्स में 200 OK रिस्पॉन्स की निरंतर धड़कन (heartbeat) दिखाई देती है, तो आप मान लेते हैं कि काम चल रहा है। हफ्तों तक, वह धड़कन एकदम सही थी। cron हर बार समय पर चला। बस उसने कभी वास्तविक काम नहीं किया।

उन्नीस ड्राफ्ट्स और कोई अलार्म नहीं

यह खोज आकस्मिक थी। आखिरकार किसी ने ध्यान दिया कि पब्लिशिंग कतार शांत हो गई है, या शायद उन्होंने किसी डाउनस्ट्रीम मेट्रिक की जाँच की और उसे स्थिर (flatline) पाया। उन्हें जो मिला वह उन्नीस ड्राफ्ट्स का ढेर था जो पूरी तरह से बिना छुए पड़े थे। अप्रूवर कर्तव्यपरायणता से चल रहा था, हर दिन सफलता का लॉग दर्ज कर रहा था, लेकिन उसने उनमें से एक को भी प्रोसेस नहीं किया था।

एक मैनुअल वर्कफ़्लो में, एक मानव समीक्षक पहले ही दिन खाली इनबॉक्स या लंबित आइटम्स के ढेर को नोटिस कर लेता। स्वचालित संस्करण में, गतिविधि की अनुपस्थिति बिल्कुल काम की अनुपस्थिति जैसी लग रही थी। cron के पास निराश करने के लिए कोई मैनेजर नहीं था। वह बस समय पर आता रहा और जल्दी घर चला जाता रहा।

दो बग्स, एक खाली परिणाम

इस विफलता के दो कारण थे। दोनों में से कोई भी सिंटैक्स एरर, टाइमआउट या डिपेंडेंसी आउटेज नहीं था। दोनों सिमेंटिक गलतियाँ (semantic mistakes) थीं जिन्होंने क्वेरी इंजन की नज़र में उन्नीस वैध रोज़ (rows) को शून्य कर दिया।

पहला, एक टाइप मिसमैच (type mismatch)। ड्राफ्ट्स तैयार करने वाले एजेंट ने article टैग वाले रिकॉर्ड लिखे। अप्रूवर cron ने विशेष रूप से thread प्रकारों के लिए क्वेरी की। यह उस तरह का विचलन (drift) है जो तब होता है जब प्रोड्यूसर्स और कंज्यूमर्स समानांतर रास्तों पर विकसित होते हैं। एक टीम—या एक एजेंट—ने तय किया कि आउटपुट एक आर्टिकल है। दूसरे ने कंज्यूमर को इस धारणा के साथ लिखा कि वह थ्रेड्स को ग्रहण करेगा। किसी भी टाइप सिस्टम ने कंपाइल-टाइम एरर नहीं दिया क्योंकि ये संभवतः लूज़ स्ट्रिंग टैग्स थे, शायद JSON फ़ील्ड्स या बिना लागू किए गए varchar मान। डेटाबेस को बस कोई मैच नहीं मिला और उसने एक खाली सेट लौटा दिया। इंजन के लिए यह कोई एरर कंडीशन नहीं है। यह एक गलत सवाल का सही जवाब है।

दूसरा, अप्रूवर की क्वेरी में एक इनर जॉइन (inner join) ने चुपचाप पूरी रोज़ को निगल लिया। यदि क्वेरी ने ड्राफ्ट्स टेबल को किसी अन्य टेबल से जोड़ा—शायद मेटाडेटा, स्टेटस फ्लैग्स, या रूटिंग नियमों के लुकअप के लिए—और जॉइन कंडीशन विफल हो गई, तो इनर जॉइन ने बिल्कुल वैसा ही व्यवहार किया जैसा उसे डिज़ाइन किया गया था। इसने गैर-मिलान वाली रोज़ को बाहर कर दिया। परिणाम सेट में कोई अनाथ (orphan) रोज़ नहीं दिखीं। किसी भी नल (null) ने समस्या का संकेत नहीं दिया। उन्नीस ड्राफ्ट्स क्वेरी से छलनी से पानी की तरह निकल गए, और एप्लिकेशन लेयर को एक बिल्कुल साफ, खाली लिस्ट प्राप्त हुई।

क्योंकि क्वेरी ने कोई रोज़ नहीं लौटाई, इसलिए फ़ंक्शन बिना किसी समस्या के समाप्त हो गया। कोई एक्सेप्शन (exception) ऊपर नहीं आया। HTTP रिस्पॉन्स 200 OK था। cron ने सफलता दर्ज की और वापस सो गया।

'प्रोसेस्ड ज़ीरो' का जाल

समस्या का सार यहाँ है। कतार-आधारित (queue-based) सिस्टम में, एक कंज्यूमर को अक्सर प्रोसेस करने के लिए शून्य रोज़ मिलती हैं। कतार खाली हो जाती है। वर्कर जल्दी काम खत्म कर लेता है। लॉग में processed: 0 लिखा आता है और टीम इसे अच्छी खबर मानती है: हम मांग के अनुसार काम कर रहे हैं। यह एक स्वस्थ स्थिति है।

लेकिन processed: 0 दो पूरी तरह से अलग वास्तविकताओं को दर्शाता है:

  • स्वस्थ स्थिति: शून्य प्रोसेस हुए क्योंकि शून्य लंबित हैं। कतार खाली है। सिस्टम डिज़ाइन के अनुसार खाली (idle) है।
  • टूटी हुई स्थिति: शून्य प्रोसेस हुए क्योंकि कंज्यूमर काम को देख नहीं पा रहा है। कतार में उन्नीस रोज़ हैं। सिस्टम अंधा है, खाली नहीं।

कतार की गहराई (queue depth) पर स्वतंत्र जाँच के बिना, ये दोनों स्थितियाँ एक जैसी टेलीमेट्री (telemetry) देती हैं। वे डैशबोर्ड में एक जैसी दिखती हैं, लॉग एग्रीगेटर्स में एक जैसी लगती हैं, और PagerDuty में एक जैसी खामोशी पैदा करती हैं। आपने एक ऐसी मॉनिटरिंग रणनीति बनाई है जो यह पता लगाती है कि वर्कर कब चिल्लाता है, न कि तब जब वह वास्तविक काम के ढेर के पास से चुपचाप निकल जाता है।

अंतर को कम करना

Elevare Digital ने अपनी मॉनिटरिंग की जाने वाली चीज़ों को बदलकर समस्या का समाधान किया। उन्होंने केवल एरर रेट (error rates) और सक्सेस स्टेटस (success statuses) पर निर्भर रहना बंद कर दिया। इसके बजाय, उन्होंने उपलब्ध कार्य (available work) और पूर्ण कार्य (completed work) के बीच के अंतर (gap) पर अलर्ट भेजना शुरू कर दिया।

अब हर बैच के बाद, वे एक सरल इनवेरिएंट चेक (invariant check) चलाते हैं:

  • यदि processed 0 है और pending पंक्तियाँ (rows) 0 से अधिक हैं, तो हाई सेवेरिटी (high severity) अलर्ट ट्रिगर करें।

यह नियम जानबूझकर कारण के प्रति तटस्थ (agnostic) रखा गया है। इसे इस बात से फर्क नहीं पड़ता कि चूक किसी खराब फ़िल्टर, टूटे हुए जॉइन (join), या गलत टाइप किए गए enum स्ट्रिंग के कारण हुई थी। इसे केवल इस बात से मतलब है कि काम मौजूद है और कोई काम हुआ नहीं। यह मॉनिटरिंग को "क्या प्रोसेस ने शिकायत की?" से बदलकर "क्या काम आगे बढ़ा?" पर ले आता है।

इसका समर्थन करने के लिए, वे क्यू डेप्थ (queue depth) को केवल एक स्पॉट-चेक के रूप में नहीं, बल्कि समय के साथ ट्रैक किए जाने वाले एक 'फर्स्ट-क्लास मेट्रिक' (first-class metric) के रूप में देखते हैं। यदि कंज्यूमर (consumer) लगातार सफलता की रिपोर्ट करता रहता है और प्रोड्यूसर (producer) पंक्तियाँ जोड़ता रहता है, तो डेप्थ ट्रेंड एक पुख्ता सबूत (smoking gun) बन जाता है। एक स्टैटिक स्नैपशॉट झूठ बोल सकता है, लेकिन बढ़ता हुआ बैकलॉग (backlog) कभी झूठ नहीं बोलता।

ऑटोनॉमस सिस्टम्स (Autonomous Systems) के लिए सबक

Elevare की घटना में उन सभी के लिए कुछ व्यावहारिक नियम दिए गए हैं जो 'हैंड्स-ऑफ' (hands-off) पाइपलाइन्स चला रहे हैं।

स्कैन की गई पंक्तियों (scanned rows) को प्रोसेस की गई पंक्तियों (processed rows) से अलग लॉग करें। कंज्यूमर एक ऐसी क्वेरी चला सकता है जो चालीस पंक्तियों को छूती है, खराब मानदंडों (criteria) के कारण उन सभी को फ़िल्टर कर देती है, और processed: 0 रिपोर्ट करती है। यदि आप केवल अंतिम संख्या को लॉग करते हैं, तो आप उस 'घोस्ट इंटरैक्शन' (ghost interaction) को मिस कर देते हैं। 'स्कैन की गई पंक्तियों' का मेट्रिक यह बताता है कि वर्कर आया, काम को देखा, और भ्रमित होकर चला गया। स्कैन की गई और प्रोसेस की गई पंक्तियों के बीच का वह अंतर अक्सर आपका सबसे शुरुआती संकेत होता है।

क्यू डेप्थ (queue depth) को टाइम-सीरीज (time-series) के रूप में ट्रैक करें। एक क्यू जो अस्थायी रूप से खाली है, वह ठीक है। लेकिन एक क्यू जो वर्कर के 'ग्रीन' (सफल) रहने के बावजूद लगातार बढ़ती जा रही है, वह ठीक नहीं है। डेप्थ को कंज्यूमर थ्रूपुट (consumer throughput) के विरुद्ध प्लॉट करें। जब ये दोनों अलग दिशाओं में जाने लगें, तो तुरंत जांच करें, भले ही हर हेल्थ चेक पास हो रहा हो।

कंज्यूमर्स का परीक्षण केवल मोंक्स (mocks) के बजाय वास्तविक प्रोड्यूसर आउटपुट के विरुद्ध करें। मॉक डेटा के साथ यूनिट टेस्ट टेस्टर की धारणाओं को दर्शाते हैं। यदि मॉक फैक्ट्री thread प्रकार बनाती है और कंज्यूमर thread प्रकार की अपेक्षा करता है, तो आपके टेस्ट पास हो जाएंगे जबकि प्रोडक्शन फेल हो जाएगा। ऐसे इंटीग्रेशन टेस्ट चलाएं जो प्रोड्यूसर के आउटपुट से वास्तविक रिकॉर्ड प्राप्त करें। सुनिश्चित करें कि कंज्यूमर वास्तव में वह देख सके जो प्रोड्यूसर लिखता है।

डेटा प्रकारों (data types) और enum वैल्यूज़ को कॉन्ट्रैक्ट (contracts) के रूप में मानें। JSON ब्लॉब्स में ढीले स्ट्रिंग टैग तब तक सुविधाजनक होते हैं जब तक वे अदृश्य विफलता बिंदु (failure points) नहीं बन जाते। स्कीमा (schemas) को स्पष्ट रूप से परिभाषित करें। कॉन्स्टेंट्स (constants) साझा करें। प्रोड्यूसर और कंज्यूमर के बीच के जोड़ पर पेलोड (payloads) को वैलिडेट करें। यदि कॉन्ट्रैक्ट टूटता है, तो सिस्टम को सीमा (boundary) पर स्पष्ट रूप से विफल होना चाहिए, न कि WHERE क्लॉज के अंदर चुपचाप।

असली निष्कर्ष

ऑटोनॉमस सिस्टम इंसानों की तरह विफल नहीं होते हैं। वे बीमारी का बहाना बनाकर छुट्टी नहीं लेते, हर बार एक्सेप्शन (exceptions) नहीं फेंकते, या स्पष्ट क्रैश डंप (crash dumps) नहीं छोड़ते। वे 200 OK रिटर्न करते हैं और इन्वेंट्री को सड़ने देते हैं। यदि आपके अलर्ट केवल चीखें सुनने के लिए बने हैं, तो आप सबसे महंगे विफलताओं को मिस कर देंगे—वे जहाँ सब कुछ ठीक दिखता है और कुछ भी नहीं होता।

अपनी ऑब्जर्वेबिलिटी (observability) को अंतर (gap) पर नज़र रखने के लिए डिज़ाइन करें। जो काम अंदर आता है और जो काम बाहर निकलता है, उसकी तुलना करें। जब ये दोनों मेल न खाएं, तो मान लें कि मशीन आपसे झूठ बोल रही है। क्योंकि कभी-कभी, एक परफेक्ट सक्सेस लॉग उस सिस्टम का एकमात्र लक्षण होता है जो पूरी तरह से अंधा हो चुका है।