भविष्य को आकार देने वाली तकनीक पर गहन लेख।

eBPF स्पिनलॉक और कर्नल डीबगिंग की कला

eBPF स्पिनलॉक बग Linux कर्नल में कैसे दिखते हैं, इन्हें ढूँढना क्यों कठिन है, और डीबगिंग eBPF के safety model के बारे में क्या बताती है।

फ्रोज़न सर्किट-बोर्ड लैंडस्केप के अंदर घूमता और चिंगारियाँ छोड़ता एक चमकता हुआ पैडलॉक

eBPF में स्पिनलॉक का बग कोई स्टैक ट्रेस या एरर मैसेज देकर अपनी मौजूदगी नहीं बताता। वह मशीन के रुक जाने से पता चलता है। कोई लॉग नहीं, कोई वॉर्निंग नहीं, कोई ग्रेसफुल डिग्रेडेशन नहीं — बस एक CPU कोर ऐसे लॉक पर अनंत काल तक घूमता रहता है जो कभी रिलीज़ नहीं होगा। अगर आप भाग्यशाली हैं, तो वॉचडॉग टाइमर चलेगा और ट्रेस के साथ kernel panic मिलेगा। अगर किस्मत खराब है, तो मशीन बस जम जाएगी और आपको उसे पावर-साइकल करना पड़ेगा।

eBPF से उम्मीद की जाती है कि वह सुरक्षित रहे। वह कर्नल में चलता है, लेकिन उससे पहले एक verifier से गुजरता है, जो यह सिद्ध करने की कोशिश करता है कि प्रोग्राम खत्म होंगे, अमान्य मेमोरी एक्सेस नहीं करेंगे और कर्नल की स्थिति को खराब नहीं करेंगे। तो स्पिनलॉक के बग आखिर निकल कैसे जाते हैं? वजह यह है कि verifier अलग-अलग प्रोग्राम की जाँच करता है, जबकि स्पिनलॉक के बग प्रोग्राम, maps और कर्नल के शेड्यूलिंग फ़ैसलों के आपसी मेल-जोल से पैदा होते हैं — ऐसी चीज़ें जिन्हें कोई स्टैटिक verifier पूरी तरह पहले से नहीं भाँप सकता।

eBPF स्पिनलॉक किसलिए हैं

eBPF प्रोग्राम कर्नल context में चलते हैं, और अक्सर एक साथ कई CPU पर। जब दो eBPF प्रोग्राम एक ही map value को अपडेट करना चाहते हैं — काउंटर, कोई डेटा स्ट्रक्चर, या state machine — तो उन्हें सिंक्रोनाइज़ेशन चाहिए। इसके लिए eBPF bpf_spin_lock और bpf_spin_unlock देता है।

स्पिनलॉक सबसे सरल लॉक है: उसे पाने की कोशिश करो, और अगर वह पहले से पकड़ा हुआ है, तो उसके रिलीज़ होने तक एक तंग loop में घूमते रहो। न सोना, न रुकना, न कतार में लगना। eBPF के लिए यह ठीक है, क्योंकि eBPF प्रोग्राम ऐसे context में चलते हैं जहाँ सोना मना है — interrupt handlers, softirqs, pre-emption बंद वाले हिस्से। जो mutex caller को सुला दे, वह कर्नल को क्रैश कर देगा। स्पिनलॉक बस तब तक CPU cycles जलाता है जब तक लॉक खाली न हो जाए।

struct map_value {
struct bpf_spin_lock lock;
__u64 counter;
__u32 last_pid;
};
SEC("tp/sched/sched_switch")
int handle_sched_switch(struct trace_event_raw_sched_switch *ctx) {
__u32 key = 0;
struct map_value *val;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
bpf_spin_lock(&val->lock);
val->counter++;
val->last_pid = ctx->next_pid;
bpf_spin_unlock(&val->lock);
return 0;
}

यह सीधा-सादा लगता है। और इस जैसे सरल मामलों में — लॉक करो, वैल्यू अपडेट करो, अनलॉक करो — यह ठीक चलता है। दिक्कतें तब शुरू होती हैं जब लॉकिंग पैटर्न जटिल हो जाते हैं, या जब eBPF प्रोग्राम और कर्नल के अपने लॉकिंग के बीच ऐसी निर्भरताएँ बन जाती हैं जिनकी उम्मीद नहीं थी।

स्पिनलॉक के बग कैसे सामने आते हैं

स्पिनलॉक का क्लासिक बग डेडलॉक है: प्रोग्राम A लॉक 1 पकड़े हुए है और लॉक 2 पाने की कोशिश कर रहा है, जबकि प्रोग्राम B लॉक 2 पकड़े हुए है और लॉक 1 चाहता है। दोनों एक-दूसरे का इंतज़ार करते हुए हमेशा घूमते रहते हैं। कर्नल context में इसका मतलब है कि वे CPU कोर हमेशा के लिए गए — वे कभी कुछ और नहीं चलाएँगे।

eBPF verifier इनमें से कुछ मामलों को रोक देता है। वह ऐसे प्रोग्राम रिजेक्ट कर देता है जो एक साथ एक से ज़्यादा स्पिनलॉक पकड़ते हैं, जिससे क्लासिक AB-BA डेडलॉक खत्म हो जाता है। लेकिन सभी डेडलॉक को वह नहीं रोक सकता, क्योंकि कुछ eBPF स्पिनलॉक और कर्नल के अपने लॉक के रिश्ते से पैदा होते हैं।

एक परिदृश्य सोचिए: tracepoint से जुड़ा एक eBPF प्रोग्राम map में एक स्पिनलॉक लेता है। लॉक पकड़े रहते हुए, उसी CPU पर एक hardware interrupt आ जाता है। Interrupt handler एक और eBPF प्रोग्राम चलाता है जो वही स्पिनलॉक पाना चाहता है। डेडलॉक — interrupt तब तक वापस नहीं लौट सकता जब तक उसे लॉक न मिले, और लॉक तब तक रिलीज़ नहीं हो सकता जब तक बाधित प्रोग्राम फिर से न चले, और वह तब तक नहीं चल सकता जब तक interrupt वापस न लौटे।

Deadlock scenario (same CPU):
CPU 0 running eBPF program A
→ acquires spinlock on map_value
→ hardware interrupt fires
→ CPU 0 runs interrupt handler
→ interrupt handler runs eBPF program B
→ program B tries to acquire same spinlock
→ spinlock is held by program A
→ program B spins... forever
→ interrupt handler never returns
→ program A never resumes
→ program A never releases lock
→ CPU 0 is permanently stuck

कर्नल के अपने स्पिनलॉक इसे इस तरह संभालते हैं कि लॉक पकड़े रहने के दौरान interrupts बंद कर देते हैं (spin_lock_irqsave)। eBPF स्पिनलॉक भी यही करते हैं — bpf_spin_lock preemption बंद करता है और kernel 5.1+ पर softirqs भी बंद करता है। लेकिन सभी interrupt contexts कवर नहीं होते, और सभी kernel versions इसे एक जैसा नहीं संभालते, और बारीक बग यहीं छिपे रहते हैं।

डीबगिंग की समस्या

eBPF में स्पिनलॉक की समस्याओं को डीबग करना कुछ खास वजहों से मुश्किल है।

रिप्रोडक्शन गैर-निर्धारित होता है। स्पिनलॉक के बग टाइमिंग पर निर्भर करते हैं — कौन सा CPU कौन सा प्रोग्राम चलाता है, interrupts कब आते हैं, critical section कितना लंबा चलता है। एक टेस्ट जो 999 बार ठीक चलता है, 1000वीं बार डेडलॉक कर सकता है। डेवलपमेंट में इन्हें भरोसे से दोहराना मुश्किल है, क्योंकि आपकी डेवलपमेंट मशीन का CPU topology, interrupt की फ़्रीक्वेंसी और लोड पैटर्न प्रोडक्शन से अलग होता है।

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

पारंपरिक डीबगिंग टूल मदद नहीं करते। डेडलॉक हुई स्थिति को देखने के लिए आप कर्नल में debugger नहीं जोड़ सकते (KGDB से जोड़ सकते हैं, लेकिन डेडलॉक से पहले serial connection सेट करना ज़रूरी है)। dmesg बेकार है, क्योंकि उसमें कुछ लिखा ही नहीं जा रहा। भरोसेमंद जानकारी सिर्फ़ lockup detector के आउटपुट से मिलती है — अगर वह मशीन पूरी तरह मरने से पहले चल जाए।

वे रणनीतियाँ जो सचमुच काम करती हैं

इन चुनौतियों के बावजूद, लोग eBPF स्पिनलॉक के बग ढूँढते और ठीक करते हैं। यह रहा तरीका।

लॉक ordering विश्लेषण। कुछ भी चलाने से पहले, हर eBPF प्रोग्राम के लॉकिंग व्यवहार का विश्लेषण करें। हर प्रोग्राम कौन से maps एक्सेस करता है? कौन से लॉक पकड़ता है? क्या दो प्रोग्राम जो एक ही लॉक छूते हैं, एक ही CPU पर चल सकते हैं (same tracepoint, या tracepoint बनाम interrupt)? यह उबाऊ मैन्युअल विश्लेषण है, लेकिन सबसे आम डेडलॉक पैटर्न को होने से पहले ही पकड़ लेता है।

लॉक होल्ड टाइम की निगरानी। अपने eBPF प्रोग्राम में इंस्ट्रूमेंटेशन जोड़कर मापें कि स्पिनलॉक कितनी देर पकड़े जाते हैं। कुछ माइक्रोसेकंड से ज़्यादा देर तक पकड़ा गया स्पिनलॉक खतरे की घंटी है — इससे interrupt-आधारित डेडलॉक की खिड़की बड़ी होती है और दूसरे CPU बेकार में cycles जलाते हैं। छोटे critical sections तेज़ भी होते हैं और सुरक्षित भी।

// Monitoring lock hold time in eBPF
SEC("tp/sched/sched_switch")
int handle_switch(void *ctx) {
struct map_value *val;
__u64 start, elapsed;
__u32 key = 0;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
start = bpf_ktime_get_ns();
bpf_spin_lock(&val->lock);
// Critical section — keep this minimal
val->counter++;
bpf_spin_unlock(&val->lock);
elapsed = bpf_ktime_get_ns() - start;
// Report if lock was held too long
if (elapsed > 1000) {  // > 1 microsecond
bpf_printk("lock held for %llu ns", elapsed);
}
return 0;
}

Per-CPU डेटा स्ट्रक्चर। सबसे अच्छा स्पिनलॉक बग वह है जो हो ही न सके। अगर हर CPU अपने डेटा की अपनी कॉपी पर काम करे और बाद में उन्हें जोड़ दिया जाए, तो कोई कंटेंशन नहीं होता और कोई लॉक नहीं चाहिए। eBPF के BPF_MAP_TYPE_PERCPU_HASH और BPF_MAP_TYPE_PERCPU_ARRAY ठीक इसी काम के लिए बने हैं। काउंटर, histogram और accumulators को लगभग कभी साझा-स्टेट स्पिनलॉक की ज़रूरत नहीं पड़ती।

लॉक की जगह atomic ऑपरेशन। सरल कामों के लिए — काउंटर बढ़ाना, किसी वैल्यू की तुलना करके swap करना — __sync_fetch_and_add और इसी तरह के atomics तेज़ भी हैं और डेडलॉक-मुक्त भी। काउंटर बढ़ाने के लिए स्पिनलॉक की ज़रूरत नहीं। स्पिनलॉक उन ऑपरेशन के लिए रखें जहाँ सचमुच कई फ़ील्ड एक साथ atomically अपडेट करने हों।

इससे eBPF के safety model के बारे में क्या पता चलता है

eBPF की safety की कहानी प्रभावशाली है, लेकिन बारीक भी। Verifier गारंटी देता है कि अलग-अलग प्रोग्राम सुरक्षित हैं: वे खत्म होते हैं, अमान्य मेमोरी एक्सेस नहीं करते, कर्नल की स्थिति नहीं बिगाड़ते। लेकिन 'सुरक्षित' प्रोग्राम मिलकर असुरक्षित सिस्टम बन सकते हैं, जब उनके आपसी असर से ऐसी टाइमिंग निर्भरताएँ बनें जिन्हें verifier विश्लेषित नहीं कर सकता।

यह स्टैटिक विश्लेषण की एक बुनियादी सीमा है। Verifier हर प्रोग्राम को अलग-थलग देखता है। उसे नहीं पता कि और कौन से प्रोग्राम लोड हैं, वे कौन से maps साझा करते हैं, या वे किस कर्नल context में चलते हैं। स्पिनलॉक पकड़ने वाला प्रोग्राम 'सुरक्षित' है — वह सही तरीके से लॉक लेता और छोड़ता है। लेकिन क्या वह हर दूसरे लोड किए गए eBPF प्रोग्राम के साथ मिलकर भी सुरक्षित है, यह रनटाइम परिस्थितियों पर निर्भर करता है।

कर्नल समुदाय इस पर धीरे-धीरे काम कर रहा है। हाल के patches ने तय किया है कि कौन से eBPF प्रोग्राम टाइप स्पिनलॉक इस्तेमाल कर सकते हैं, critical sections कितने लंबे हो सकते हैं, और स्पिनलॉक पकड़े रहते हुए कौन से ऑपरेशन वर्जित हैं। हर पाबंदी बगों की खिड़की संकरी करती है, लेकिन साथ ही यह भी सीमित करती है कि eBPF प्रोग्राम क्या कर सकते हैं।

eBPF लॉकिंग के व्यावहारिक नियम

काफ़ी स्पिनलॉक समस्याएँ डीबग करने के बाद कुछ पैटर्न साफ़ दिखने लगते हैं, जिनसे बचा जा सकता है।

  1. Per-CPU maps को प्राथमिकता दें। अगर आपका डेटा CPU के हिसाब से बाँटा जा सकता है और user space में जोड़ा जा सकता है, तो वही करें। कोई लॉक नहीं, कोई कंटेंशन नहीं, कोई डेडलॉक नहीं। यह ज़्यादातर उपयोग (80%) संभालता है — काउंटर, इवेंट लॉग, histograms।
  2. स्पिनलॉक की जगह atomics चुनें। अगर आपको साझा काउंटर या compare-and-swap चाहिए, तो atomic ऑपरेशन इस्तेमाल करें। ये lock-free हैं और डेडलॉक नहीं कर सकते।
  3. स्पिनलॉक ज़रूरी हों तो critical sections छोटे रखें। काउंटर बढ़ाएँ, timestamp अपडेट करें, pointer swap करें। इससे ज़्यादा कुछ नहीं। लॉक पकड़े रहते कभी helper functions न बुलाएँ — आपको नहीं पता कि वे अंदर से कौन सा लॉक लेते हैं।
  4. एक ही लॉक दो अलग eBPF प्रोग्राम टाइप से कभी न पकड़ें। अगर kprobe प्रोग्राम और tracepoint प्रोग्राम दोनों एक ही map value को लॉक करते हैं, तो आप एक interrupt की दूरी पर डेडलॉक से हैं। अलग maps या per-CPU डेटा इस्तेमाल करें।
  5. लोड के तहत टेस्ट करें। स्पिनलॉक के बग टाइमिंग पर निर्भर होते हैं। ये कंटेंशन में सामने आते हैं — ऊँचा CPU उपयोग, बार-बार interrupts, कई eBPF प्रोग्राम एक साथ चलते हुए। खाली डेवलपमेंट मशीनों पर नहीं, असली प्रोडक्शन जैसे लोड के साथ टेस्ट करें।

eBPF ने हम Linux कर्नल से कैसे इंटरैक्ट करते हैं उसे मूल रूप से बदल दिया है, और user-space प्रोग्राम को कर्नल-स्तरीय observability और networking तक सुरक्षित पहुँच दी है। लेकिन 'सुरक्षित' की भी सीमाएँ हैं। Verifier eBPF को कच्चे kernel modules लिखने से कहीं ज़्यादा सुरक्षित बनाता है — नाटकीय रूप से। लेकिन यह उसे उस तरह सुरक्षित नहीं बनाता जिस तरह user-space प्रोग्रामिंग सुरक्षित होती है। उन सीमाओं को समझना, खासकर साझा स्टेट और लॉकिंग के आसपास, ही वह चीज़ है जो प्रोडक्शन में चलने वाले eBPF प्रोग्राम को टेस्ट में चलने वालों से अलग करती है।