FlowingDev

Protobuf, explained: वह गज़ब का एफ़िशिएंट बाइनरी फ़ॉर्मैट जो थोड़ा भाव खाता है

Protocol Buffers को समझें, Google का वह बिजली सा तेज़ बाइनरी सीरियलाइज़ेशन फ़ॉर्मैट जो आपके डेटा को छोटे, type-safe पैकेट्स में सिकोड़ देता है।

टूल आज़माएँ: प्रोटोबफ डिकोडर

एक लाइन में

Protocol Buffers (Protobuf) स्ट्रक्चर्ड डेटा को सीरियलाइज़ (serialize) करने के लिए एक language-neutral, बेहद तेज़ बाइनरी फ़ॉर्मैट है—इसे आप JSON का वह तगड़ा, जिम जाने वाला कज़िन मान सकते हैं जो पूरी तरह से टाइट, कंप्रेस्ड बाइनरी स्ट्रीम्स (binary streams) में ही बात करता है।

यह कौन सी प्रॉब्लम सॉल्व करता है

2000 के दशक की शुरुआत में, Google के पास एक बहुत बड़ी प्रॉब्लम थी: उनके ढेरों internal services एक-दूसरे से बात कर रहे थे, और उनका नेटवर्क ट्रैफ़िक बहुत भारी हो रहा था। उस समय, कंप्यूटर-टू-कंप्यूटर कम्युनिकेशन के लिए इंडस्ट्री का स्टैण्डर्ड XML हुआ करता था। अगर आपको एंगल ब्रैकेट्स (angle brackets) पढ़ने का शौक है, तो XML बढ़िया है, लेकिन यह अपने भारी-भरकम साइज़ और स्लो पार्सिंग (slow parsing) के लिए बदनाम है। हर बार जब कोई सर्वर एक छोटा सा डेटा भेजना चाहता था, जैसे कोई यूज़र ID, तो उसे आस-पास के टेक्स्ट टैग्स <userId>12345</userId> भेजने पड़ते थे, फिर रिसीव करने वाले सर्वर को उस स्ट्रिंग को पार्स (parse) करना पड़ता था, टैग्स को मैच करना पड़ता था, और "12345" को वापस integer में कंवर्ट करना पड़ता था।

जब JSON सीन में आया, तो वह हल्का ज़रूर था, लेकिन उसमें भी वही बुनियादी कमी थी: यह एक टेक्स्ट फ़ॉर्मैट था जो raw strings ट्रांसमिट करता था। अगर आप "emailAddress" फ़ील्ड वाले दस लाख रिकॉर्ड्स भेजते हैं, तो आप "emailAddress" स्ट्रिंग को नेटवर्क पर दस लाख बार भेज रहे होते हैं।

Google के इंजीनियर्स को एहसास हुआ कि यह bandwidth और CPU cycles की भारी बर्बादी है। उन्हें एक ऐसा फ़ॉर्मैट चाहिए था जो:

  1. नेटवर्क (wire) पर एकदम छोटा हो: फ़ील्ड के नाम मत भेजो, सिर्फ़ डेटा भेजो।
  2. पार्स करने में तेज़ हो: कंप्यूटर्स को bits और bytes पढ़ना पसंद है, टेक्स्ट कैरेक्टर्स (text characters) पार्स करना नहीं।
  3. Strictly typed हो: अगर मैं एक integer भेजता हूँ, तो आपको एक integer ही मिलना चाहिए, बिना इस उम्मीद के कि स्ट्रिंग ठीक से पार्स हो जाए।
  4. Backwards compatible हो: मैं किसी सर्विस को नए डेटा फ़ॉर्मैट में अपडेट कर सकूँ और उन पुराने क्लाइंट्स (clients) का कोड ब्रेक न हो जो अभी तक अपडेट नहीं हुए हैं।

उनका सॉलूशन था Protocol Buffers. Self-describing टेक्स्ट भेजने के बजाय, Protobuf एक पहले से शेयर किए गए "कॉन्ट्रैक्ट" (एक स्कीमा/schema) पर निर्भर करता है। क्योंकि सेंडर और रिसीवर दोनों को पहले से ही स्कीमा पता होता है, इसलिए नेटवर्क पर भेजे जाने वाले असली डेटा से सारा मेटाडेटा (metadata) हटा दिया जाता है, और सिर्फ़ raw, highly-compressed बाइनरी वैल्यूज़ ही बचती हैं।

Under the hood यह कैसे काम करता है

Protobuf को समझने के लिए, आपको इसे तीन हिस्सों में बाँटना होगा: कॉन्ट्रैक्ट, कंपाइलर, और वायर फ़ॉर्मैट (wire format)।

स्कीमा (The Schema - .proto)

सब कुछ एक .proto फ़ाइल से शुरू होता है। यह एक language-agnostic कॉन्ट्रैक्ट है जहाँ आप अपने डेटा का एग्ज़ैक्ट शेप (shape) डिफ़ाइन करते हैं।

syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  bool is_active = 3;
}

उन नंबर्स (= 1, = 2) पर ध्यान दिया? वो Field Tags हैं। यही Protobuf का असली जादू (secret sauce) हैं। JSON में, की (key) "name" फ़ील्ड को आइडेंटिफ़ाई (identify) करती है। Protobuf में, नंबर 1 फ़ील्ड को आइडेंटिफ़ाई करता है।

Compilation स्टेप

आप Protobuf पार्सिंग कोड (parsing code) को हाथ से नहीं लिखते। आप अपनी .proto फ़ाइल पर एक कंपाइलर टूल (protoc) रन करते हैं, और उसे बताते हैं कि आपको आउटपुट किस लैंग्वेज (language) में चाहिए। यह C#, TypeScript, Python, Go, या जो भी आप यूज़ कर रहे हैं, उसके लिए नेटिव क्लासेस (native classes) जनरेट कर देता है।

जब आपका ऐप किसी Person को भेजना चाहता है, तो आप बस उस जनरेटेड क्लास (generated class) का इंस्टेंस (instance) बनाते हैं, उसमें डेटा डालते हैं, और एक Serialize() मेथड कॉल करते हैं। इसके बाद सारा मुश्किल बाइनरी पैकिंग (binary packing) का काम वो जनरेटेड कोड ही सँभाल लेता है।

वायर फ़ॉर्मैट (Binary Magic)

जब आप उस Person ऑब्जेक्ट को सीरियलाइज़ (serialize) करते हैं, तो नेटवर्क पर असल में जाता क्या है?

यह एक [Key] [Value] अप्रोच का इस्तेमाल करता है, लेकिन बाइट लेवल (byte level) पर। इसमें Key कोई स्ट्रिंग नहीं होती; यह एक सिंगल बाइट (या कुछ बाइट्स) होती है जो आपके Field Tag और डेटा के Wire Type को कंबाइन करती है।

Wire Type रिसीवर को बताता है कि आने वाली वैल्यू कितनी लंबी है। यहाँ मुख्य वायर टाइप्स (wire types) दिए गए हैं:

Wire Type Meaning Used For
0 Varint int32, int64, uint32, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages
5 32-bit fixed32, sfixed32, float

उदाहरण के लिए, अगर आप is_active = true (Field 3, Wire Type 0) सेट करते हैं, तो Protobuf की (key) (3 और 0 को कंबाइन करके) और वैल्यू (true के लिए 1) को महज़ दो बाइट्स में एनकोड (encode) कर देता है। JSON में, "is_active": true 17 बाइट्स लेता है।

Varint ट्रिक

Protobuf नंबर्स को कंप्रेस करने के लिए "Base-128 Varints" का इस्तेमाल करता है। एक स्टैण्डर्ड 32-bit integer मेमोरी में हमेशा 4 बाइट्स लेता है, भले ही वह नंबर सिर्फ़ 5 ही क्यों न हो। Protobuf कहता है, "ज़ीरो (zeroes) के लिए 3 बाइट्स क्यों बर्बाद करें?"

Varints के साथ, हर बाइट 1 बिट (Most Significant Bit या MSB) रिज़र्व करती है जो एक फ़्लैग (flag) का काम करता है, जिसका मतलब होता है, "क्या और भी बाइट्स आ रही हैं?" अगर नंबर छोटा है, तो MSB 0 होता है, और नंबर बिल्कुल 1 बाइट में फ़िट हो जाता है। इसका मतलब है कि छोटे नंबर्स नेटवर्क पर (on the wire) काफ़ी कम स्पेस लेते हैं।

बिना स्कीमा के डिकोडिंग

क्योंकि वायर फ़ॉर्मैट में Wire Type शामिल होता है, एक पार्सर (parser) raw Protobuf बाइट स्ट्रीम (byte stream) को रीड करके उसका शेप (shape) पता लगा सकता है, वह भी बिना .proto फ़ाइल के। उसे यह नहीं पता होगा कि Field 1 का नाम "name" है, लेकिन वह यह देख पाएगा: "Field 1 एक 4 बाइट्स की length-delimited स्ट्रिंग है जिसमें 'John' है।" यही वजह है कि आप raw hex या Base64 Protobuf डेटा को किसी डिकोडर (decoder) में डाल सकते हैं और फिर भी आपको टैग्स (tags) और वैल्यूज़ (values) का एक यूज़ेबल, JSON-जैसा ट्री (tree) मिल जाएगा।

असल दुनिया की कहानियाँ

वह IoT फ़्लीट जिसकी साँसें फूलने लगीं

एक स्टार्टअप ने स्मार्ट एग्रीकल्चरल सेंसर्स (agricultural sensors) का एक फ़्लीट (fleet) बनाया जो सैकड़ों मील के खेतों में मिट्टी की नमी नापता था। शुरुआत में, उन्होंने सेंसर्स को ऐसा सेट किया कि वे सेलुलर नेटवर्क (cellular networks) के ज़रिए हर मिनट JSON पेलोड्स (payloads) का इस्तेमाल करके एक सेंट्रल सर्वर को पिंग (ping) करें। एक महीने के भीतर ही दो चीज़ें हुईं: उनके 3G डेटा का बिल आसमान छूने लगा, और सेंसर्स की बैटरी महीनों के बजाय हफ़्तों में ही खत्म हो गई।

उन्होंने डेटा को Protocol Buffers में सीरियलाइज़ (serialize) करने के लिए सेंसर के फ़र्मवेयर (firmware) को फिर से लिखा। चूँकि वे छोटे नंबर्स और बूलीयन्स (booleans) भेज रहे थे, इसलिए {"sensorId": 14, "moistureLevel": 42} जैसी सारी JSON स्ट्रिंग्स को हटाने से उनके पेलोड (payload) का साइज़ 80% से भी ज़्यादा कम हो गया। सबक (The lesson): जब बैंडविड्थ (bandwidth) महँगी हो और बैटरी लाइफ़ लिमिटेड हो, तो raw strings ट्रांसमिट करना एक ऐसा शौक है जिसे आप अफ़ोर्ड नहीं कर सकते। बाइनरी सीरियलाइज़ेशन (Binary serialization) फ़िज़िकल रिसोर्सेज़ बचाता है।

2018 का भयंकर माइक्रोसर्विस जाम

एक ई-कॉमर्स (e-commerce) कंपनी ने अपने मोनोलिथिक बैकएंड (monolithic backend) को 50 अलग-अलग माइक्रोसर्विसेज़ (microservices) में तोड़ने का फ़ैसला किया। उन्होंने कम्युनिकेशन के लिए JSON के साथ REST APIs को चुना। अचानक, ओवरऑल लेटेंसी (latency) बहुत बढ़ गई। सर्वर्स की प्रोफ़ाइलिंग (profiling) करने पर एक चौंकाने वाली बात सामने आई: बैकएंड सर्विसेज़ अपनी CPU cycles का 30% सिर्फ़ JSON को स्ट्रिंगीफ़ाई (stringify) और पार्स (parse) करने में बिता रही थीं, क्योंकि डेटा Auth सर्विस से Cart सर्विस और फिर Inventory सर्विस तक उछल-कूद कर रहा था।

उन्होंने अपने इंटरनल कम्युनिकेशन (internal communications) को gRPC पर माइग्रेट कर दिया, जो पूरी तरह से Protobuf पर बना एक फ़्रेमवर्क (framework) है। क्योंकि Protobuf पार्सिंग असल में सिर्फ़ मेमोरी ऑफ़सेट्स (memory offsets) पढ़ने और बिट्स को शिफ़्ट (shifting bits) करने का काम है, इसलिए CPU यूसेज तेज़ी से गिर गया। सर्विसेज़ के बीच डेटा पलक झपकते ही पहुँचने लगा। सबक: हाई-थ्रूपुट (high-throughput), इंटरनल बैकएंड सिस्टम्स में, टेक्स्ट पार्सिंग (text parsing) एक अदृश्य टैक्स की तरह है। बाइनरी सीरियलाइज़ेशन इस टैक्स से बचने का लीगल तरीक़ा है।

नेटवर्क डंप में वह मिस्ट्री पेलोड

एक सिक्योरिटी रिसर्चर (security researcher) एक मोबाइल ऐप की अनडॉक्यूमेंटेड (undocumented) API को रिवर्स-इंजीनियर (reverse-engineer) करने की कोशिश कर रही थी ताकि यह देख सके कि ऐप क्या डेटा कलेक्ट कर रहा है। उसने ऐप के ट्रैफ़िक को एक प्रॉक्सी (proxy) के ज़रिए रूट किया, इस उम्मीद में कि उसे बढ़िया, रीडेबल JSON देखने को मिलेगा। इसके बजाय, उसे अजीबोगरीब कचरा मिला। HTTP हेडर्स (headers) ने उसके शक को सही साबित कर दिया: Content-Type: application/x-protobuf.

उसके पास .proto स्कीमा (schema) नहीं था (ऐप के डेवलपर्स ने उसे अपने सर्वर्स पर रखा था)। लेकिन उसने उस Base64 पेलोड को उठाया और उसे एक raw Protobuf डिकोडर में डाल दिया। बाहर एक स्ट्रक्चर्ड ट्री (structured tree) निकल कर आया: Field 1: "johndoe", Field 2: "user@example.com", Field 3: 40.7128, Field 4: -74.0060। उसने तुरंत फ़ील्ड 3 और 4 को GPS कोऑर्डिनेट्स (coordinates) के तौर पर पहचान लिया। सबक: Protobuf एन्क्रिप्टेड (encrypted) नहीं है, यह सिर्फ़ एनकोडेड (encoded) है। बिना नक्शे के भी, Wire Types द्वारा छोड़े गए स्ट्रक्चरल सुरागों (structural breadcrumbs) की मदद से आप डेटा के ढाँचे (skeleton) को रिवर्स-इंजीनियर कर सकते हैं।

आम ग़लतियाँ और ट्रैप्स

  • फ़ील्ड टैग्स (field tags) को रीयूज़ करना: वह नंबर ही सब कुछ है (absolute source of truth)। अगर आप कोई फ़ील्ड डिलीट करते हैं (मान लीजिए, string name = 1;), तो आपको कभी भी उस नंबर 1 को किसी नए फ़ील्ड जैसे int32 age = 1; के लिए फिर से यूज़ नहीं करना चाहिए। अगर कोई पुराना क्लाइंट एक स्ट्रिंग भेजता है, और एक नया सर्वर उसे integer समझकर पढ़ने की कोशिश करता है, तो सब कुछ क्रैश (explode) हो जाएगा।
  • डिलीट किए गए फ़ील्ड्स को रिज़र्व (reserve) करना भूल जाना: ऊपर वाली ग़लती से बचने के लिए, Protobuf reserved कीवर्ड (keyword) देता है। अगर आप फ़ील्ड 2 को डिलीट करते हैं, तो आपको अपने स्कीमा में साफ़ तौर पर reserved 2; लिखना चाहिए। इससे कंपाइलर उन सभी डेवलपर्स पर चीखने (error देने) के लिए मजबूर हो जाएगा जो भविष्य में इसे रीयूज़ (reuse) करने की कोशिश करेंगे।
  • बिना सोचे-समझे डेटा टाइप्स (data types) बदलना: आप कुछ टाइप्स को सेफ़ली बदल सकते हैं (जैसे int32 को int64 में क्योंकि वे एक ही Varint वायर टाइप का यूज़ करते हैं), लेकिन किसी string को int32 में बदलना एक ब्रेकिंग चेंज (breaking change) है। इन दोनों के वायर टाइप्स बुनियादी तौर पर incompatible होते हैं।
  • यह मान लेना कि Protobuf सिक्योर (secure) है: यह एक सीरियलाइज़ेशन फ़ॉर्मैट है, कोई एन्क्रिप्शन (encryption) नहीं। अगर आप बिना TLS/SSL के HTTP पर Protobuf का यूज़ करके यूज़र्स का सेंसिटिव डेटा भेजते हैं, तो ट्रैफ़िक को इंटरसेप्ट (intercept) करने वाला कोई भी इंसान आसानी से raw values को डिकोड कर सकता है।
  • ब्राउज़र में इसे आँख मूँद कर यूज़ करना: हालाँकि gRPC-web जैसी टेक्नोलॉजीज़ मौजूद हैं, लेकिन वेब ब्राउज़र्स (web browsers) नेटिव रूप से JSON ही समझते हैं। Protobuf को किसी फ़्रंटएंड वेब ऐप (frontend web app) में लाने के लिए आपको अपने JavaScript बंडल में एक भारी-भरकम पार्सिंग लाइब्रेरी (parsing library) भेजनी पड़ती है, जो कभी-कभी बाइनरी फ़ॉर्मैट से होने वाली बैंडविड्थ की बचत पर पानी फेर देती है।

यह आपके रडार पर क्यों होना चाहिए

आपको अपनी public-facing APIs के लिए Protobuf का यूज़ नहीं करना चाहिए—वहाँ अभी भी JSON ही बेताज बादशाह है क्योंकि यह human-readable है, फ़्लेक्सिबल (flexible) है, और दुनिया के हर वेब ब्राउज़र द्वारा नेटिव रूप से समझा जाता है।

हालाँकि, जब पाइप के दोनों सिरे (both ends) आपके कंट्रोल में हों, तो आपको तुरंत Protobuf की ओर जाना चाहिए। अगर आप एक-दूसरे से बात करने वाले माइक्रोसर्विसेज़ (microservices), बैकएंड से बात करने वाले मोबाइल ऐप्स, रियल-टाइम मल्टीप्लेयर गेम्स (real-time multiplayer games), या IoT डिवाइसेज़ बना रहे हैं, तो Protobuf एक सुपरपावर (superpower) है। यह टीम्स के बीच सख़्त डेटा कॉन्ट्रैक्ट्स (strict data contracts) को एन्फ़ोर्स (enforce) करता है (क्योंकि हर कोई एक ही .proto फ़ाइल से कंपाइल करता है), कंपाइल टाइम (compile time) पर ही डेटा-टाइप एरर्स को पकड़ लेता है, और आपके नेटवर्क इंफ्रास्ट्रक्चर की परफ़ॉर्मेंस का एक-एक कतरा निचोड़ लेता है।

गहराई में जाएँ

  • Official Protocol Buffers Documentation - स्कीमा लिखने और कंपाइलर को यूज़ करने पर Google का ऑथॉरिटेटिव सोर्स।
  • Protobuf Encoding Specifications - इस फ़ॉर्मैट में इस्तेमाल होने वाले सटीक बिटवाइज़ ऑपरेशंस (bitwise operations) और वायर टाइप्स (wire types) की गहराई में एक शानदार डुबकी।
  • gRPC Official Site - हाई-परफ़ॉर्मेंस RPC फ़्रेमवर्क जो अपने डिफ़ॉल्ट राउटिंग (routing) और पेलोड लैंग्वेज के रूप में Protobuf का यूज़ करता है।
  • Variable-length quantity (Varints) on Wikipedia - Protobuf मोस्ट सिग्निफ़िकेंट बिट (Most Significant Bit) को चुराकर किस तरह integers को कंप्रेस करता है, उसके पीछे का मैथ (math) और इतिहास।

थ्योरी हो गई। अब हाथ आज़माइए — 100% आपके ब्राउज़र में।

टूल आज़माएँ: प्रोटोबफ डिकोडर