ओपनटेलीमेट्री के साथ एंड-टू-एंड ट्रेसिंग
August 31, 2022
चाहे आप माइक्रोसर्विसेज़ को लागू करें या नहीं (और शायद आपको नहीं करना चाहिए), आपका सिस्टम संभवतः कई घटकों से बना होता है। सबसे सरल सिस्टम शायद एक रिवर्स प्रॉक्सी, एक ऐप, और एक डेटाबेस से बना होता है। इस मामले में, मॉनिटरिंग न केवल एक अच्छा विचार है; यह एक आवश्यकता है। जितने अधिक घटकों के माध्यम से एक अनुरोध प्रवाहित हो सकता है, उतनी ही मजबूत यह आवश्यकता होती है।
हालांकि, मॉनिटरिंग केवल यात्रा की शुरुआत है। जब अनुरोध बड़े पैमाने पर विफल होने लगते हैं, तो आपको सभी घटकों में एक समग्र दृश्य की आवश्यकता होती है। इसे ट्रेसिंग कहा जाता है, और यह ऑब्जर्वेबिलिटी के मुख्य स्तंभों में से एक है; अन्य दो मेट्रिक्स और लॉग्स हैं।
इस पोस्ट में, मैं केवल ट्रेस पर ध्यान केंद्रित करूंगा और बताऊंगा कि आप ऑब्जर्वेबिलिटी की यात्रा कैसे शुरू कर सकते हैं।
W3C ट्रेस कॉन्टेक्स्ट स्पेसिफिकेशन
एक ट्रेसिंग समाधान को विभिन्न प्रौद्योगिकी स्टैक्स में काम करने के लिए एक मानक प्रारूप प्रदान करना चाहिए। ऐसे प्रारूप को एक स्पेसिफिकेशन का पालन करना चाहिए, चाहे वह औपचारिक हो या डी फैक्टो।
यह समझना आवश्यक है कि एक स्पेसिफिकेशन शायद ही कभी कहीं से भी प्रकट होती है। आमतौर पर, बाजार में पहले से ही कुछ अलग-अलग कार्यान्वयन होते हैं। अधिकांश समय, एक नई स्पेसिफिकेशन एक अतिरिक्त कार्यान्वयन की ओर ले जाती है, जैसा कि प्रसिद्ध XKCD कॉमिक बताती है:

हालांकि, कभी-कभी एक चमत्कार होता है: बाजार नई स्पेसिफिकेशन का पालन करता है। यहां, ट्रेस कॉन्टेक्स्ट एक W3C स्पेसिफिकेशन है, और ऐसा लगता है कि यह काम कर गया है:
यह स्पेसिफिकेशन मानक HTTP हेडर्स और एक मान प्रारूप को परिभाषित करता है जो डिस्ट्रिब्यूटेड ट्रेसिंग परिदृश्यों को सक्षम करने के लिए कॉन्टेक्स्ट जानकारी को प्रसारित करता है। स्पेसिफिकेशन मानकीकृत करता है कि कैसे कॉन्टेक्स्ट जानकारी को सेवाओं के बीच भेजा और संशोधित किया जाता है। कॉन्टेक्स्ट जानकारी डिस्ट्रिब्यूटेड सिस्टम में व्यक्तिगत अनुरोधों को विशिष्ट रूप से पहचानती है और प्रदाता-विशिष्ट कॉन्टेक्स्ट जानकारी को जोड़ने और प्रसारित करने का एक साधन भी परिभाषित करती है। -- https://www.w3.org/TR/trace-context/
दस्तावेज़ से दो महत्वपूर्ण अवधारणाएं उभरती हैं:
- एक ट्रेस एक अनुरोध के पथ का अनुसरण करता है जो कई घटकों में फैला होता है
- एक स्पैन एक एकल घटक से बंधा होता है और एक बाल-पैरेंट संबंध द्वारा दूसरे स्पैन से जुड़ा होता है

इस लेखन के समय, स्पेसिफिकेशन एक W3C सिफारिश है, जो अंतिम चरण है।
ट्रेस कॉन्टेक्स्ट के पहले से ही कई कार्यान्वयन हैं। उनमें से एक OpenTelemetry है।
OpenTelemetry को स्वर्ण मानक के रूप में
आप IT के परिचालन भाग के जितने करीब होंगे, OpenTelemetry के बारे में सुनने की संभावना उतनी ही अधिक होगी:
OpenTelemetry टूल्स, APIs, और SDKs का एक संग्रह है। इसका उपयोग टेलीमेट्री डेटा (मेट्रिक्स, लॉग्स, और ट्रेस) को इंस्ट्रूमेंट, जनरेट, कलेक्ट, और एक्सपोर्ट करने के लिए किया जाता है ताकि आप अपने सॉफ्टवेयर के प्रदर्शन और व्यवहार का विश्लेषण कर सकें। OpenTelemetry कई भाषाओं में सामान्य रूप से उपलब्ध है और उपयोग के लिए उपयुक्त है। -- https://opentelemetry.io/
OpenTelemetry CNCF द्वारा प्रबंधित एक प्रोजेक्ट है। OpenTelemetry से पहले दो प्रोजेक्ट्स थे:
- OpenTracing, जैसा कि नाम से पता चलता है, ट्रेस पर केंद्रित था
- OpenCensus, जिसका लक्ष्य मेट्रिक्स और ट्रेस को प्रबंधित करना था
दोनों प्रोजेक्ट्स विलय हो गए और लॉग्स को जोड़ दिया गया। OpenTelemetry अब ऑब्जर्वेबिलिटी पर केंद्रित "लेयर्स" का एक सेट प्रदान करता है:
- विभिन्न भाषाओं में इंस्ट्रूमेंटेशन APIs
- विभिन्न भाषाओं में कैनोनिकल कार्यान्वयन
- इंफ्रास्ट्रक्चर घटक जैसे कलेक्टर्स
- इंटरऑपरेबिलिटी प्रारूप, जैसे W3C का ट्रेस कॉन्टेक्स्ट
ध्यान दें कि जबकि OpenTelemetry एक ट्रेस कॉन्टेक्स्ट कार्यान्वयन है, यह और भी कुछ करता है। ट्रेस कॉन्टेक्स्ट स्वयं को HTTP तक सीमित रखता है, जबकि OpenTelemetry स्पैन को गैर-वेब घटकों, जैसे Kafka, को पार करने की अनुमति देता है। यह इस ब्लॉग पोस्ट के दायरे से बाहर है।
उपयोग-मामला
मेरा पसंदीदा उपयोग-मामला एक ई-कॉमर्स दुकान है, तो इसे बदलने नहीं देंगे। इस मामले में, दुकान माइक्रोसर्विसेज़ के आसपास डिज़ाइन की गई है, जिनमें से प्रत्येक REST API के माध्यम से पहुंच योग्य है और एक API गेटवे के पीछे सुरक्षित है। ब्लॉग पोस्ट के लिए आर्किटेक्चर को सरल बनाने के लिए, मैं केवल दो माइक्रोसर्विसेज़ का उपयोग करूंगा: catalog उत्पादों को प्रबंधित करता है, और pricing उत्पादों की कीमत को संभालता है।
जब कोई उपयोगकर्ता ऐप पर आता है, तो होम पेज सभी उत्पादों को प्राप्त करता है, उनकी संबंधित कीमत प्राप्त करता है, और उन्हें प्रदर्शित करता है।

चीजों को और दिलचस्प बनाने के लिए, catalog एक स्प्रिंग बूट एप्लिकेशन है जो कोटलिन में कोडेड है, जबकि pricing एक पायथन फ्लास्क एप्लिकेशन है।
ट्रेसिंग हमें गेटवे, दोनों माइक्रोसर्विसेज़ और, यदि संभव हो तो, डेटाबेस में अनुरोध के पथ का अनुसरण करने की अनुमति देनी चाहिए।
गेटवे में ट्रेस
प्रवेश बिंदु ट्रेसिंग का सबसे रोमांचक हिस्सा है, क्योंकि इसे ट्रेस ID उत्पन्न करना चाहिए: इस मामले में, प्रवेश बिंदु गेटवे है। मैं डेमो को लागू करने के लिए Apache APISIX का उपयोग करूंगा:
Apache APISIX लोड बैलेंसिंग, डायनामिक अपस्ट्रीम, कैनरी रिलीज़, सर्किट ब्रेकिंग, प्रमाणीकरण, ऑब्जर्वेबिलिटी, आदि जैसी समृद्ध ट्रैफ़िक प्रबंधन सुविधाएँ प्रदान करता है।-- https://apisix.apache.org/
Apache APISIX एक प्लगइन आर्किटेक्चर पर आधारित है और एक OpenTelemetry प्लगइन प्रदान करता है:
opentelemetryप्लगइन का उपयोग OpenTelemetry स्पेसिफिकेशन के अनुसार ट्रेसिंग डेटा की रिपोर्ट करने के लिए किया जा सकता है। प्लगइन केवल HTTP पर बाइनरी-एन्कोडेड OLTP का समर्थन करता है। -- https://apisix.apache.org/docs/apisix/plugins/opentelemetry/
आइए opentelemetry प्लगइन को कॉन्फ़िगर करें:
apisix: enable_admin: false #1 config_center: yaml #1 plugins: - opentelemetry #2 plugin_attr: opentelemetry: resource: service.name: APISIX #3 collector: address: jaeger:4318 #4
- डेमो को आसानी से अनुसरण करने के लिए Apache APISIX को स्टैंडअलोन मोड में चलाएं। यह प्रोडक्शन में भी एक अच्छा अभ्यास है
opentelemetryको एक ग्लोबल प्लगइन के रूप में कॉन्फ़िगर करें- सेवा का नाम सेट करें। यह वह नाम है जो ट्रेस डिस्प्ले घटक में दिखाई देगा
- ट्रेस को
jaegerसेवा पर भेजें। निम्नलिखित अनुभाग इसे वर्णित करेगा।
हम हर रूट को ट्रेस करना चाहते हैं, इसलिए प्रत्येक रूट में प्लगइन जोड़ने के बजाय, हमें प्लगइन को एक ग्लोबल के रूप में सेट करना चाहिए:
global_rules: - id: 1 plugins: opentelemetry: sampler: name: always_on #1
- ट्रेसिंग का प्रदर्शन पर प्रभाव पड़ता है। जितना अधिक हम ट्रेस करते हैं, उतना ही अधिक प्रभाव पड़ता है। इसलिए, हमें प्रदर्शन प्रभाव और ऑब्जर्वेबिलिटी के लाभों के बीच सावधानी से संतुलन बनाना चाहिए। हालांकि, डेमो के लिए, हम हर अनुरोध को ट्रेस करना चाहते हैं।
ट्रेस को कलेक्ट, स्टोर और डिस्प्ले करना
जबकि ट्रेस कॉन्टेक्स्ट एक W3C स्पेसिफिकेशन है और OpenTelemetry एक डी फैक्टो मानक है, बाजार में ट्रेस को कलेक्ट, स्टोर और डिस्प्ले करने के लिए कई समाधान मौजूद हैं। प्रत्येक समाधान सभी तीन क्षमताएं प्रदान कर सकता है या केवल उनका हिस्सा। उदाहरण के लिए, Elastic स्टैक स्टोरेज और डिस्प्ले को संभालता है, लेकिन कलेक्शन के लिए आपको किसी और पर निर्भर रहना होगा। दूसरी ओर, Jaeger और Zipkin सभी तीन क्षमताओं को पूरा करने के लिए एक पूर्ण सूट प्रदान करते हैं।
Jaeger और Zipkin OpenTelemetry से पहले के हैं, इसलिए प्रत्येक का अपना ट्रेस ट्रांसपोर्ट प्रारूप है। हालांकि, वे OpenTelemetry प्रारूप के साथ एकीकरण प्रदान करते हैं।
इस ब्लॉग पोस्ट के दायरे में, सटीक समाधान प्रासंगिक नहीं है, क्योंकि हमें केवल क्षमताओं की आवश्यकता है। मैंने Jaeger को चुना क्योंकि यह एक ऑल-इन-वन Docker इमेज प्रदान करता है: प्रत्येक क्षमता का अपना घटक होता है, लेकिन वे सभी एक ही इमेज में एम्बेडेड होते हैं, जो कॉन्फ़िगरेशन को बहुत आसान बनाता है।
इमेज के प्रासंगिक पोर्ट्स निम्नलिखित हैं:
| पोर्ट | प्रोटोकॉल | घटक | कार्य |
|---|---|---|---|
16686 | HTTP | क्वेरी | फ्रंटएंड सर्व करें |
4317 | HTTP | कलेक्टर | OpenTelemetry प्रोटोकॉल (OTLP) को gRPC पर स्वीकार करें, यदि सक्षम हो |
4318 | HTTP | कलेक्टर | OpenTelemetry प्रोटोकॉल (OTLP) को HTTP पर स्वीकार करें, यदि सक्षम हो |
Docker Compose बिट इस तरह दिखता है:
services: jaeger: image: jaegertracing/all-in-one:1.37 #1 environment: - COLLECTOR_OTLP_ENABLED=true #2 ports: - "16686:16686" #3
all-in-oneइमेज का उपयोग करें- बहुत महत्वपूर्ण: OpenTelemetry प्रारूप में कलेक्टर को सक्षम करें
- UI पोर्ट को एक्सपोज़ करें
अब जब हमने इंफ्रास्ट्रक्चर सेट कर लिया है, तो हम अपने एप्लिकेशन में ट्रेस को सक्षम करने पर ध्यान केंद्रित कर सकते हैं।
Flask ऐप्स में ट्रेस
pricing सेवा एक सरल Flask एप्लिकेशन है। यह डेटाबेस से एकल उत्पाद की कीमत प्राप्त करने के लिए एक एकल एंडपॉइंट प्रदान करता है।
@app.route('/price/<product_str>') #1-2 def price(product_str: str) -> Dict[str, object]: product_id = int(product_str) price: Price = Price.query.get(product_id) #3 if price is None: return jsonify({'error': 'Product not found'}), 404 else: low: float = price.value - price.jitter #4 high: float = price.value + price.jitter #4 return { 'product_id': product_id, 'price': round(uniform(low, high), 2) #4 }
- एंडपॉइंट
- रूट को उत्पाद के id की आवश्यकता होती है
- SQLAlchemy का उपयोग करके डेटाबेस से डेटा प्राप्त करें
- वास्तविक प्राइसिंग इंजन समय के साथ एक ही कीमत कभी नहीं लौटाते; मजे के लिए कीमत को थोड़ा यादृच्छिक कर दें
चेतावनी: प्रति कॉल एकल कीमत प्राप्त करना अत्यधिक अक्षम है। इसमें उत्पादों के रूप में कई कॉल्स की आवश्यकता होती है, लेकिन यह एक और दिलचस्प ट्रेस बनाता है। वास्तविक जीवन में, रूट को कई उत्पाद आईडी स्वीकार करने और एक अनुरोध-प्रतिक्रिया में सभी संबंधित कीमतों को प्राप्त करने में सक्षम होना चाहिए।
अब एप्लिकेशन को इंस्ट्रूमेंट करने का समय है। दो विकल्प उपलब्ध हैं: स्वचालित इंस्ट्रूमेंटेशन और मैन्युअल इंस्ट्रूमेंटेशन। स्वचालित कम प्रयास और त्वरित जीत है; मैन्युअल को फोकस्ड डेवलपमेंट समय की आवश्यकता होती है। मैं स्वचालित से शुरू करने और केवल आवश्यक होने पर मैन्युअल जोड़ने की सलाह दूंगा।
हमें कुछ Python पैकेज जोड़ने की आवश्यकता है:
opentelemetry-distro[otlp]==0.33b0 opentelemetry-instrumentation opentelemetry-instrumentation-flask
हमें कुछ पैरामीटर्स को कॉन्फ़िगर करने की आवश्यकता है:
pricing: build: ./pricing environment: OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1 OTEL_RESOURCE_ATTRIBUTES: service.name=pricing #2 OTEL_METRICS_EXPORTER: none #3 OTEL_LOGS_EXPORTER: none #3
- ट्रेस को Jaeger पर भेजें
- सेवा का नाम सेट करें। यह वह नाम है जो ट्रेस डिस्प्ले घटक में दिखाई देगा
- हम न तो लॉग्स में और न ही मेट्रिक्स में रुचि रखते हैं
अब, मानक flask run कमांड का उपयोग करने के बजाय, हम इसे रैप करते हैं:
opentelemetry-instrument flask run
इसके साथ ही, हम पहले से ही मेथड कॉल्स और Flask रूट्स से स्पैन कलेक्ट करते हैं।
यदि आवश्यक हो, तो हम मैन्युअल रूप से अतिरिक्त स्पैन जोड़ सकते हैं, जैसे:
from opentelemetry import trace @app.route('/price/<product_str>') def price(product_str: str) -> Dict[str, object]: product_id = int(product_str) with tracer.start_as_current_span("SELECT * FROM PRICE WHERE ID=:id", attributes={":id": product_id}) as span: #1 price: Price = Price.query.get(product_id) # ...
- कॉन्फ़िगर किए गए लेबल और एट्रिब्यूट के साथ एक अतिरिक्त स्पैन जोड़ें
स्प्रिंग बूट ऐप्स में ट्रेस
catalog सेवा एक रिएक्टिव स्प्रिंग बूट एप्लिकेशन है जो कोटलिन में विकसित की गई है। यह दो एंडपॉइंट प्रदान करती है:
- एक एकल उत्पाद प्राप्त करने के लिए
- दूसरा सभी उत्पाद प्राप्त करने के लिए
दोनों पहले उत्पाद डेटाबेस में देखते हैं, फिर ऊपर दिए गए pricing सेवा से कीमत प्राप्त करते हैं।
पायथन की तरह, हम स्वचालित और मैन्युअल इंस्ट्रूमेंटेशन का लाभ उठा सकते हैं। आइए कम लटकते फल से शुरू करें, स्वचालित इंस्ट्रूमेंटेशन। JVM पर, हम इसे एक एजेंट के माध्यम से प्राप्त करते हैं:
java -javaagent:opentelemetry-javaagent.jar -jar catalog.jar
पायथन की तरह, यह हर मेथड कॉल और HTTP एंट्री पॉइंट के लिए स्पैन बनाता है। यह JDBC कॉल्स को भी इंस्ट्रूमेंट करता है, लेकिन हमारे पास एक रिएक्टिव स्टैक है और इसलिए R2DBC का उपयोग करते हैं। रिकॉर्ड के लिए, समर्थन जोड़ने के लिए एक GitHub इश्यू खुला है।
हमें डिफ़ॉल्ट व्यवहार को कॉन्फ़िगर करने की आवश्यकता है:
catalog: build: ./catalog environment: APP_PRICING_ENDPOINT: http://pricing:5000/price OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1 OTEL_RESOURCE_ATTRIBUTES: service.name=orders #2 OTEL_METRICS_EXPORTER: none #3 OTEL_LOGS_EXPORTER: none #3
- ट्रेस को Jaeger पर भेजें
- सेवा का नाम सेट करें। यह वह नाम है जो ट्रेस डिस्प्ले घटक में दिखाई देगा
- हम न तो लॉग्स में और न ही मेट्रिक्स में रुचि रखते हैं
पायथन की तरह, हम मैन्युअल इंस्ट्रूमेंटेशन जोड़कर खेल को बढ़ा सकते हैं। दो विकल्प उपलब्ध हैं, प्रोग्रामेटिक और एनोटेशन-आधारित। पूर्व थोड़ा शामिल है जब तक कि हम स्प्रिंग क्लाउड स्लूथ को पेश नहीं करते। आइए एनोटेशन जोड़ें।
हमें एक अतिरिक्त डिपेंडेंसी की आवश्यकता है:
<dependency> <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-instrumentation-annotations</artifactId> <version>1.17.0-alpha</version> </dependency>
सावधान रहें, आर्टिफैक्ट को हाल ही में io.opentelemetry:opentelemetry-extension-annotations से स्थानांतरित किया गया था।
अब हम अपने कोड को एनोटेट कर सकते हैं:
@WithSpan("ProductHandler.fetch") //1 suspend fun fetch(@SpanAttribute("id") id: Long): Result<Product> { //2 val product = repository.findById(id) return if (product == null) Result.failure(IllegalArgumentException("Product $id not found")) else Result.success(product) }
- कॉन्फ़िगर किए गए लेबल के साथ एक अतिरिक्त स्पैन जोड़ें
- पैरामीटर को एक एट्रिब्यूट के रूप में उपयोग करें, जिसमें कुंजी
idपर सेट हो और मान पैरामीटर का रनटाइम मान हो
परिणाम
अब हम अपने सरल डेमो के साथ खेल सकते हैं और परिणाम देख सकते हैं:
curl localhost:9080/products curl localhost:9080/products/1
प्रतिक्रियाएं दिलचस्प नहीं हैं, लेकिन आइए Jaeger UI को देखें। हम दोनों ट्रेस पाते हैं, प्रति कॉल एक:

हम एकल ट्रेस के स्पैन में गहराई से जा सकते हैं:

ध्यान दें कि हम उपरोक्त UML डायग्राम के बिना अनुक्रम प्रवाह का अनुमान लगा सकते हैं। और भी बेहतर, अनुक्रम एक घटक के अंदरूनी कॉल्स को प्रदर्शित करता है।
प्रत्येक स्पैन में एट्रिब्यूट्स होते हैं जो स्वचालित इंस्ट्रूमेंटेशन द्वारा जोड़े गए हैं और जो हमने मैन्युअल रूप से जोड़े हैं:

निष्कर्ष
इस पोस्ट में, मैंने एक अनुरोध को एक API गेटवे, दो अलग-अलग टेक स्टैक्स पर आधारित ऐप्स, और उनके संबंधित डेटाबेस में ट्रेस करके ट्रेसिंग को प्रदर्शित किया है। मैंने केवल ट्रेसिंग की सतह को छुआ है: वास्तविक दुनिया में, ट्रेसिंग में HTTP से असंबंधित घटक, जैसे Kafka और मैसेज क्यूज़, शामिल हो सकते हैं।
फिर भी, अधिकांश सिस्टम किसी न किसी तरह से HTTP पर निर्भर करते हैं। इसे सेट अप करना तुच्छ नहीं है, लेकिन यह बहुत कठिन भी नहीं है। घटकों में HTTP अनुरोधों को ट्रेस करना आपके सिस्टम की ऑब्जर्वेबिलिटी की ओर आपकी यात्रा की एक अच्छी शुरुआत है।
इस पोस्ट के लिए पूरा स्रोत कोड GitHub पर पाया जा सकता है।
आगे जाने के लिए: