কয়েক সপ্তাহ ধরে, Elevare Digital-এর একটি cron job নির্ধারিত সময়ে জেগে উঠত, তার কিউ (queue) পরীক্ষা করত এবং সফলভাবে সম্পন্ন হওয়ার লগ (log) রাখত। এটি ঠিক শূন্যটি ড্রাফট অনুমোদন করেছিল। উনিশটি কন্টেন্ট অপেক্ষায় পড়ে ছিল। দলটি পরে জানতে পারে, যখন সেই নীরব ব্যবধানটি একটি অদ্ভুত ব্যাপার থেকে একটি ছোট ব্যাকলগ (backlog)-এ পরিণত হয়েছিল। কিছুই ক্র্যাশ করেনি। কোনো পেজিং অ্যালার্ট (paging alert) আসেনি। সিস্টেমটি প্রযুক্তিগতভাবে সুস্থ ছিল কিন্তু কার্যকারিতার দিক থেকে মৃত ছিল।
এটি হলো স্বয়ংক্রিয় পাইপলাইনের (autonomous pipelines) এক নীরব আতঙ্ক। যখন আপনি লুপ থেকে মানুষকে সরিয়ে দেন, তখন আপনি সেই ব্যক্তিকে সরিয়ে দেন যে লক্ষ্য করে যে আসলে কিছুই ঘটছে না।
যে পাইপলাইনটি নিজেই চলত
Elevare Digital একটি সম্পূর্ণ স্বয়ংক্রিয় কন্টেন্ট ওয়ার্কফ্লো (workflow) পরিচালনা করে। সফটওয়্যার এজেন্টরা ড্রাফট তৈরি করে। একটি নির্ধারিত অ্যাপ্রুভার ক্রন (approver cron) গেটকিপার হিসেবে কাজ করে, সেই ড্রাফটগুলো পর্যালোচনা করে এবং অনুমোদিত আইটেমগুলোকে সরাসরি পাবলিশিং-এ পাঠিয়ে দেয়। প্রতিটি ব্যাচ অনুমোদনের জন্য কোনো মানুষ ড্যাশবোর্ড খোলে না। মূল উদ্দেশ্য হলো মেশিনটি একঘেয়ে কাজগুলো সামলাবে, আর দলটি অন্যান্য সমস্যার দিকে মনোযোগ দেবে।
এই মডেলে, বিশ্বাসই হয়ে ওঠে আপনার প্রাথমিক ইন্টারফেস। আপনি শিডিউলারের (scheduler) ওপর ভরসা করেন যে এটি কাজ শুরু করবে। আপনি কাজের ওপর ভরসা করেন। আপনি এক্সিট কোড (exit code)-এর ওপর ভরসা করেন। যখন লগগুলোতে নিয়মিত 200 OK রেসপন্স দেখা যায়, তখন আপনি ধরে নেন যে কাজ চলছে। কয়েক সপ্তাহ ধরে সেই হার্টবিট ছিল নিখুঁত। ক্রনটি প্রতিবার সঠিক সময়ে চলত। কিন্তু এটি আসলে কখনোই আসল কাজটি করেনি।
উনিশটি ড্রাফট এবং কোনো অ্যালার্ম নেই
এই বিষয়টি আকস্মিকভাবে ধরা পড়ে। কেউ একজন অবশেষে লক্ষ্য করলেন যে পাবলিশিং কিউটি শান্ত হয়ে গেছে, অথবা হয়তো তারা কোনো ডাউনস্ট্রিম মেট্রিক (downstream metric) পরীক্ষা করে একটি ফ্ল্যাটলাইন (flatline) দেখতে পেলেন। তারা যা খুঁজে পেলেন তা হলো উনিশটি ড্রাফট সম্পূর্ণ অবহেলিত অবস্থায় পড়ে ছিল। অ্যাপ্রুভারটি নিষ্ঠার সাথে কাজ করছিল, প্রতিদিন সফলতার লগ দিচ্ছিল, কিন্তু সেগুলোর একটিও প্রসেস করেনি।
একটি ম্যানুয়াল ওয়ার্কফ্লোতে, একজন মানব পর্যালোচক প্রথম দিনেই একটি খালি ইনবক্স বা পেন্ডিং আইটেমের স্তূপ লক্ষ্য করতেন। স্বয়ংক্রিয় সংস্করণে, কার্যক্রমের অনুপস্থিতি দেখতে ঠিক কাজের অনুপস্থিতির মতোই মনে হচ্ছিল। ক্রনটির হতাশ করার মতো কোনো ম্যানেজার ছিল না। এটি কেবল যথাসময়ে কাজ শুরু করত এবং দ্রুত কাজ শেষ করে বিদায় নিত।
দুটি বাগ, একটি শূন্য ফলাফল
এই ব্যর্থতার দুটি কারণ ছিল। কোনটিই সিনট্যাক্স এরর (syntax error), টাইমআউট (timeout) বা ডিপেন্ডেন্সি আউটটেজ (dependency outage) ছিল না। উভয়ই ছিল সিম্যান্টিক ভুল (semantic mistakes), যা কুয়েরি ইঞ্জিনের (query engine) চোখে উনিশটি বৈধ রো (row)-কে শূন্যে পরিণত করেছিল।
প্রথমত, একটি টাইপ মিসম্যাচ (type mismatch)। ড্রাফট তৈরি করা এজেন্টটি রেকর্ডগুলোকে article হিসেবে ট্যাগ করে লিখেছিল। কিন্তু অ্যাপ্রুভার ক্রনটি বিশেষভাবে thread টাইপের জন্য কুয়েরি করেছিল। এটি হলো সেই ধরনের বিচ্যুতি (drift) যা ঘটে যখন প্রোডিউসার (producer) এবং কনজিউমার (consumer) সমান্তরাল পথে বিবর্তিত হয়। একটি দল—বা একটি এজেন্ট—নির্ধারণ করেছিল যে আউটপুটটি একটি আর্টিকেল। অন্যটি কনজিউমারটি এমনভাবে লিখেছিল যেন এটি থ্রেড গ্রহণ করবে। কোনো টাইপ সিস্টেমই কম্পাইল-টাইম এরর (compile-time error) দেয়নি কারণ এগুলো সম্ভবত লুজ স্ট্রিং ট্যাগ (loose string tags), সম্ভবত JSON ফিল্ড বা আনএনফোর্সড (unenforced) varchar ভ্যালু ছিল। ডাটাবেসটি কেবল কোনো মিল খুঁজে পায়নি এবং একটি খালি সেট (empty set) রিটার্ন করেছে। ইঞ্জিনের কাছে এটি কোনো এরর কন্ডিশন নয়। এটি একটি ভুল প্রশ্নের সঠিক উত্তর।
দ্বিতীয়ত, অ্যাপ্রুভারের কুয়েরিতে একটি ইনার জয়েন (inner join) নীরবে সমস্ত রো-কে গিলে ফেলেছিল। যদি কুয়েরিটি ড্রাফট টেবিলকে অন্য একটি টেবিলের সাথে যুক্ত করে—হয়তো মেটাডেটা, স্ট্যাটাস ফ্ল্যাগ বা রাউটিং রুলস দেখার জন্য—এবং জয়েন কন্ডিশনটি ব্যর্থ হয়, তবে ইনার জয়েনটি ঠিক ডিজাইন অনুযায়ী কাজ করেছিল। এটি অমিল থাকা রো-গুলোকে বাদ দিয়েছিল। রেজাল্ট সেটে কোনো অনাথ (orphan) রো দেখা দেয়নি। কোনো নাল (null) ভ্যালু সমস্যা নির্দেশ করেনি। উনিশটি ড্রাফট ছাঁকনি দিয়ে পানি যাওয়ার মতো কুয়েরিটি পার হয়ে গেল, এবং অ্যাপ্লিকেশন লেয়ার একটি একদম পরিষ্কার, খালি তালিকা পেল।
যেহেতু কুয়েরি কোনো রো রিটার্ন করেনি, ফাংশনটি কোনো সমস্যা ছাড়াই শেষ হয়ে গেল। কোনো এক্সেপশন (exception) দেখা দেয়নি। HTTP রেসপন্স ছিল 200 OK। ক্রনটি সফলতার লগ দিল এবং আবার ঘুমাতে চলে গেল।
'প্রসেসড জিরো' বা শূন্য প্রসেস করার ফাঁদ
এখানেই সমস্যার মূল। একটি কিউ-ভিত্তিক সিস্টেমে (queue-based system), একজন কনজিউমার প্রায়শই প্রসেস করার জন্য শূন্যটি রো খুঁজে পায়। কিউ খালি হয়ে যায়। ওয়ার্কার দ্রুত কাজ শেষ করে। লগ দেখে লেখা থাকে processed: 0 এবং দলটি এটিকে সুসংবাদ হিসেবে দেখে: আমরা চাহিদার সাথে তাল মিলিয়ে চলতে পারছি। এটি একটি সুস্থ অবস্থা।
কিন্তু processed: 0 দুটি সম্পূর্ণ ভিন্ন বাস্তবতাকে প্রকাশ করে:
- সুস্থ অবস্থা (Healthy state): শূন্য প্রসেস হয়েছে কারণ পেন্ডিং কিছু নেই। কিউ খালি। সিস্টেমটি ডিজাইন অনুযায়ী আইডল (idle)।
- ত্রুটিপূর্ণ অবস্থা (Broken state): শূন্য প্রসেস হয়েছে কারণ কনজিউমার কাজগুলো দেখতে পাচ্ছে না। কিউতে উনিশটি রো আছে। সিস্টেমটি অন্ধ, আইডল নয়।
কিউ ডেপথের (queue depth) ওপর কোনো স্বাধীন পরীক্ষা ছাড়া, এই দুটি অবস্থা একই রকম টেলিমেট্রি (telemetry) প্রদান করে। ড্যাশবোর্ডে এগুলো দেখতে একই রকম লাগে, লগ অ্যাগ্রিগেটরগুলোতে (log aggregators) একই রকম মনে হয় এবং PagerDuty-তে একই নীরবতা তৈরি করে। আপনি এমন একটি মনিটরিং কৌশল তৈরি করেছেন যা কেবল তখনই শনাক্ত করে যখন ওয়ার্কার চিৎকার করে, কিন্তু যখন সে কাজের স্তূপের পাশ দিয়ে নীরবে চলে যায়, তখন তা ধরতে পারে না।
ব্যবধান কমিয়ে আনা
Elevare Digital তাদের মনিটরিংয়ের বিষয়বস্তু পরিবর্তন করে সমস্যার সমাধান করেছে। তারা শুধুমাত্র এরর রেট (error rates) এবং সাকসেস স্ট্যাটাসের (success statuses) ওপর নির্ভর করা বন্ধ করে দিয়েছে। পরিবর্তে, তারা উপলব্ধ কাজ এবং সম্পন্ন কাজের মধ্যে ব্যবধানের ওপর ভিত্তি করে অ্যালার্ট দেওয়া শুরু করেছে।
প্রতিটি ব্যাচের পর, তারা এখন একটি সাধারণ ইনভ্যারিয়েন্ট চেক (invariant check) চালায়:
- যদি processed ০ হয় এবং pending rows ০-এর বেশি হয়, তবে একটি high severity alert ট্রিগার করুন।
এই নিয়মটি উদ্দেশ্যমূলকভাবে কারণ সম্পর্কে নিরপেক্ষ (agnostic)। এটি কোনো বিষয় নিয়ে মাথা ঘামায় না যে ত্রুটিটি একটি ভুল ফিল্টার, একটি ভাঙা join, নাকি একটি ভুলভাবে টাইপ করা enum string-এর কারণে হয়েছে। এটি কেবল এই বিষয়টি দেখে যে কাজ আছে কিন্তু কোনো কাজ সম্পন্ন হয়নি। এটি মনিটরিংয়ের দৃষ্টিভঙ্গিকে “প্রসেস কি কোনো অভিযোগ করেছে?” থেকে বদলে “কাজ কি এগিয়েছে?”-এ নিয়ে আসে।
এটি সমর্থন করার জন্য, তারা queue depth-কে একটি প্রথম শ্রেণির মেট্রিক (first-class metric) হিসেবে বিবেচনা করে, যা সময়ের সাথে ট্র্যাক করা হয়, কেবল একটি স্পট-চেক হিসেবে নয়। যদি producer ক্রমাগত rows যোগ করতে থাকে অথচ consumer ক্রমাগত success রিপোর্ট করে, তবে এই depth trendটি একটি অকাট্য প্রমাণ (smoking gun) হয়ে দাঁড়ায়। একটি স্ট্যাটিক স্ন্যাপশট মিথ্যা বলতে পারে, কিন্তু ক্রমবর্ধমান ব্যাকলগ (backlog) কখনোই তা করে না।
স্বায়ত্তশাসিত সিস্টেমের (Autonomous Systems) জন্য শিক্ষা
Elevare-এর এই ঘটনাটি যারা হ্যান্ডস-অফ (hands-off) পাইপলাইন পরিচালনা করেন, তাদের জন্য কিছু ব্যবহারিক নিয়ম প্রদান করে।
processed rows থেকে scanned rows-কে আলাদাভাবে লগ করুন। Consumer হয়তো এমন একটি কুয়েরি (query) চালাতে পারে যা চল্লিশটি row-কে স্পর্শ করে, কিন্তু ভুল ক্রাইটেরিয়ার কারণে সবগুলোকে ফিল্টার করে ফেলে এবং processed: 0 রিপোর্ট করে। আপনি যদি কেবল চূড়ান্ত সংখ্যাটি লগ করেন, তবে আপনি সেই অদৃশ্য মিথস্ক্রিয়াটি (ghost interaction) মিস করবেন। একটি scanned-rows মেট্রিক প্রকাশ করে যে কর্মী সেখানে উপস্থিত হয়েছিল, কাজগুলো দেখেছিল এবং বিভ্রান্ত হয়ে চলে গেছে। scanned এবং processed-এর মধ্যকার এই ব্যবধানটিই প্রায়শই আপনার প্রাথমিক সংকেত হয়ে দাঁড়ায়।
queue depth-কে একটি time-series হিসেবে ট্র্যাক করুন। একটি কিউ (queue) সাময়িকভাবে খালি থাকা ঠিক আছে। কিন্তু ওয়ার্কাররা 'green' (সচল) থাকা সত্ত্বেও যদি কিউ ক্রমাগত বাড়তে থাকে, তবে তা ঠিক নয়। Consumer throughput-এর বিপরীতে depth প্লট করুন। যখন এই দুটি ভিন্ন দিকে যায়, তখন অবিলম্বে তদন্ত করুন, এমনকি যদি প্রতিটি হেলথ চেক (health check) পাসও হয়।
শুধুমাত্র mocks নয়, আসল producer output-এর বিপরীতে consumers-কে টেস্ট করুন। মক করা (mocked) ডেটা দিয়ে করা ইউনিট টেস্টগুলো পরীক্ষকের ধারণার ওপর ভিত্তি করে তৈরি হয়। যদি mock factory thread টাইপ তৈরি করে এবং consumer thread টাইপ প্রত্যাশা করে, তবে আপনার টেস্টগুলো পাস করবে কিন্তু প্রোডাকশনে ব্যর্থ হবে। এমন ইন্টিগ্রেশন টেস্ট চালান যা producer-এর আউটপুট থেকে প্রকৃত রেকর্ড সংগ্রহ করে। নিশ্চিত করুন যে producer যা লিখছে, consumer তা সত্যিই দেখতে পাচ্ছে।
ডেটা টাইপ এবং enum ভ্যালুগুলোকে কন্ট্রাক্ট (contract) হিসেবে বিবেচনা করুন। JSON ব্লবের মধ্যে ঢিলেঢালা স্ট্রিং ট্যাগগুলো ততক্ষণ সুবিধাজনক যতক্ষণ না সেগুলো অদৃশ্য ব্যর্থতার কারণ হয়ে দাঁড়ায়। স্পষ্টভাবে স্কিমা (schema) সংজ্ঞায়িত করুন। কনস্ট্যান্ট (constants) শেয়ার করুন। Producer এবং consumer-এর সংযোগস্থলে পেলোড (payload) যাচাই করুন। যদি কন্ট্রাক্ট ভেঙে যায়, তবে সিস্টেমটিকে সীমানার (boundary) কাছে স্পষ্টভাবে ব্যর্থ হতে হবে, কোনো WHERE ক্লজের ভেতরে নীরবে নয়।
আসল শিক্ষা
স্বায়ত্তশাসিত সিস্টেমগুলো মানুষের মতো ব্যর্থ হয় না। তারা অসুস্থতার জন্য ছুটি চায় না, প্রতিবার exception দেয় না বা স্পষ্ট crash dump রেখে যায় না। তারা কেবল 200 OK রিটার্ন করে এবং ইনভেন্টরি পচে যেতে দেয়। আপনার অ্যালার্টগুলো যদি কেবল আর্তনাদ শোনার জন্য তৈরি হয়, তবে আপনি সবচেয়ে ব্যয়বহুল ব্যর্থতাগুলো মিস করবেন—যেখানে সবকিছু ঠিকঠাক মনে হয় কিন্তু আসলে কোনো কাজই সম্পন্ন হয় না।
আপনার observability-কে এমনভাবে ডিজাইন করুন যেন তা ব্যবধানটি লক্ষ্য করতে পারে। কতটুকু কাজ প্রবেশ করছে তার বিপরীতে কতটুকু কাজ সম্পন্ন হচ্ছে তা পরিমাপ করুন। যখন এই দুটি আর মিলে না, তখন ধরে নিন মেশিনটি আপনার কাছে মিথ্যা বলছে। কারণ মাঝে মাঝে, একটি নিখুঁত সাকসেস লগ হলো এমন একটি সিস্টেমের একমাত্র লক্ষণ যা সম্পূর্ণ অন্ধ হয়ে গেছে।
