มาสอดแนม🔭 Microservice ด้วย Open Telemetry และ Jaeger กัน

Thanatach Assavarattanakorn
SCB Engineer
Published in
4 min readApr 25, 2024

📌คำเตือน ! บทความนี้คือภาคต่อ

เนื่องจากบทความนี้เป็นบทความที่ใช้เนื้อหา Scenario และ Code ต่อเนื่องจาก มารู้จักและเล่นกับ Saga : Microservices Design Patterns กันเถอะ ทำให้หากไม่ได้อ่านบทความภาคแรกมา อาจจะทำให้เกิดความมึนงงเล็กน้อยนะครับผม 😵‍💫 ถ้าเป็นไปได้ แวะเลี้ยวรถกลับไปอ่านบทความก่อนหน้านี้มาได้จะแจ่มแมวมากๆเลยครับผม 🚗

ถ้าเลี้ยวรถกลับมาเรียบร้อยแล้วก็ Let’s Go สู่เนื้อหากันเถอะทุกคน 😄

อะไรคือ OpenTelemetry 🔭

คือเจ้า OpenTelemetry เนี่ย เป็น Observability framework ที่ใช้เก็บและจัดการกับพวกค่า Metrics และ Traces หรือ Logs หรืออะไรก็ตามแต่ที่เกิดขึ้นในโปรแกรมของเรา (ไม่จำเป็นต้องเป็น MicroService เสมอไป) และสามารถส่งออก (หรือไม่ส่งก็ได้) ไปยัง Platform หรือ Tool ที่เราต้องการ ยกตัวอย่างเช่น Jaeger ที่เรากำลังจะใช้ในวันนี้เลยครับบ

สำหรับเจ้า OpenTelemetry เนี่ย ผมจะพาไปรู้จักอย่างคร่าวๆเพื่อให้เราคุ้นชินกับมันง่ายขึ้นในบทความนี้กันนะครับ โดยมันจะมีสิ่งที่ควรต้องรู้ก่อนประมาณนี้ครับ 😎

  • Traces
  • Spans
  • Metrics
  • OTLP

แต่ว่าจริงๆแล้ว สำหรับ OpenTelemetry เนี่ย มีรายละเอียดปลีกย่อยที่ลงลึกลงไปค่อนข้างเยอะเลย 😅 แต่ถ้าเอาพอเข้าใจบทความนี้ ตามเก็บเท่านี้ก็น่าจะเพียงพอครับ

🔸Traces

แปลตรงตัวเลยก็คือร่องรอย 👣 ของสิ่งที่เกิดขึ้น ณ ตอนนั้น เป็นเหมือนภาพกว้างใน 1 Request ของ Microservice เรา ว่าเกิดอะไรขึ้นบ้าง ซึ่งจะประกอบไปด้วย Spans ย่อย เปรียบเทียบเป็นเหมือนภาพกว้างของ Waterfall 1 ก้อนใหญ่ ๆ ของ Flow เราครับ

🔸Spans

เป็นการ Represent ช่วงย่อย ๆ ของการดำเนินการ หรือ Process ใด ๆ ที่เกิดขึ้นใน Traces อีกที เปรียบเทียบเป็นเหมือนโขดหิน หรือในแต่ละขั้นของชั้นใน Waterfall

🔸Metrics

เป็นค่าตัวชี้วัด หรือค่าสถานะของการทำงานนั้น หรือ Host ที่กำลังทำงานในเวลานั้นๆ

🔸OTLP

เป็น OpenTelemetry Protocol หรือเรียกง่ายๆว่าเป็น Protocal ที่ทาง OpenTelemetry พัฒนาขึ้นเองเพื่อใช้ Export ค่าต่างๆออกไป ซึ่งในบทความนี้เราใช้ส่งออกไปหา Jaeger นั่นเอง อ้อ ! เจ้า OTLP นี่เห็นว่ามีพื้นฐานมาจาก gRPC ด้วยนะครับผม

อะไรคือ Jaeger

จริงๆเข้าใจว่าน้อง Jaeger เนี่ย ค่อนข้างจะเป็นที่รู้จักในวงกว้างอยู่แล้ว 🤓 โดยเฉพาะนักเลงสาย Go ที่ถือเป็น Icon ของ Tool ตัวนี้ เลยจะขออธิบายแบบรวบสั้นๆนะครับผม 🤤

เจ้า Jaeger เนี่ย เปรียบเสมือนเป็นตัว Visualization ตัว Traces ที่เกิดขึ้นให้เราเห็นและเข้าใจได้ง่ายๆอย่างสวยงามครับ และแน่นอนว่ารองรับ Protocal อย่าง OTLP ด้วย ทำให้เราสามารถ Export ข้อมูลจาก OpenTelemetry ที่อยู่ในแต่ละ Service เราเข้า Jaeger ตรงๆแบบไม่ต้องใช้ Database เลยครับผม 🤩

แต่ถ้าเป็นเคสขึ้น Prod ที่ไม่ใช่กรณีศึกษาแบบนี้ควรใช้ Database นะครับ

มาลุยสนามจริงกันเถอะ

ภารกิจของเราในวันนี้ 👨🏽‍🔧 คือ Integrate ตัว OpenTelemetry เราเข้า Project และ Export ข้อมูล Traces ต่างๆไปยัง Jaeger ครับ

ในตอนนี้เช่นเคยเลยครับ หากใครขี้เกียจตามอ่าน Code ในแต่ละ Step สามารถข้ามขั้นไป Clone Project นี้มาเล่นได้เลยนะครับ⤵️ https://github.com/thanatath/HelloKafkaBySaga

เริ่มจากสร้าง File Instrumentation สำหรับ OpenTelemetry กันก่อนเลยนะครับ

โดยจะขอ Note ไว้ว่าเนื่องจาก Project เรามีการใช้ KafkaJS จึงต้องมีการใช้ KafkaJsInstrumentation เพิ่มเติมด้วยครับ

require('dotenv').config()

const opentelemetry = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-proto');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { Resource } = require('@opentelemetry/resources');
const { KafkaJsInstrumentation } = require('opentelemetry-instrumentation-kafkajs');

function setupTelemetry(serviceName){
const sdk = new opentelemetry.NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTLP_TRACE_ENDPOINT,
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTLP_METRIC_ENDPOINT,
}),
}),
instrumentations: [
getNodeAutoInstrumentations(),
new KafkaJsInstrumentation({}),
],
});
return sdk.start();
}
module.exports = setupTelemetry

หลังจากนั้นในแต่ละ Service ของเรา เราจะเพิ่มสองบรรทัดนี้เข้าไปเพื่อให้ก่อนที่จะ Run แต่ละ Service ขึ้นมา จะต้อง Run Instrumentation ก่อน

const thisService = "order";
require('../config/instrumentation.js')(thisService)

หมายเหตุ ถ้าเป็นตาม Document ของ OpenTelemetry เลยเนี่ย เขาจะใช้ท่าประมาณนี้ครับ

node - require ./instrumentation.js app.js

เอาหละ จบแล้วกับการ Integrate ตัว OpenTelemetry เข้าไปยัง Project ของเรา 😍 ซึ่งหลายๆคนคงจะเอ๊ะกันใช่ไหมหละครับ ว่ามันแค่นี้จริง ๆ ใช่ไหม 🧐

คือเพราะว่าท่าที่เราใช้อยู่ตอนนี้เนี่ย เป็นท่า Import ตัว SDK ของ OpenTelemetry ที่เขามีให้มาจัดการ Instrumentation กับโปรแกรมเราแบบ Automatic เลย 🪄 และยิ่งหากเราใช้ท่าที่อยู่ใน Document ของ OpenTelemetry ที่เป็นตาม CodeBlock ด้านบนแทนแล้วเนี่ย ทำให้เราไม่จำเป็นต้องแตะ Code เดิมเราแม้แต่น้อยเลยครับ 😏

และในบทความนี้ จะยังไม่ถึงขั้นแตะ Code ให้เก็บ Context ในแต่ละ Unit Process เพื่อส่งเข้าไปในแต่ละ Spans นะครับ เพื่อความกระชับ และง่ายที่สุดครับ

โอเคค ถัดมาเราจะใช้ Jaeger เพื่อรับ OTLP จาก OpenTelemetry ซึ่งสามารถเข้าไปโหลดตัว Jaeger ได้จาก Official Site เลยนะครับ โดยส่วนตัวผมจะใช้ Binaries ที่ Compile มาสำหรับ Windows เลย และ Execute ด้วย Command นี้ครับ

jaeger-all-in-one --collector.zipkin.host-port=:9411 --collector.otlp.enabled --query.max-clock-skew-adjustment=0
pause

หลังจากนั้นทดสอบเข้า http://localhost:16686/ จะเห็น UI ของ Jaeger แบบนี้เลยครับ ซึ่งผมได้ลองส่งคำสั่งซื้อใน Project ของเราไปบ้างแล้ว เลยจะเห็น Traces ขึ้นมานะครับ

❗️หลังจากนี้จะมีการพูดถึง Scenario จาก Project ที่เป็นเนื้อหาจากบทความก่อนหน้านี้ ถ้ายังไม่ได้เลี้ยวรถไปอ่านกัน เลี้ยวไปตอนนี้ยังทันนะครับ

เอาหละ เดี๋ยวผมจะลอง Execute คำสั่งซื้อรัวๆเลยนะครับ

จะเห็นว่าขึ้น Traces มาเต็มเลย 😵

ทีนี้มาลองดู Happy Case ✅ ตาม Scenario เดิมของ Project เรากันครับ ในกรณีที่ทุก Service ไม่เกิดปัญหาอะไร ทีนี้เนี่ย เราจะสามารถเห็นสิ่งที่เกิดขึ้นระหว่าง Producer และ Consumer ได้อย่างชัดเจนเลย ว่าเกิดอะไรขึ้นบ้าง ใคร Produce จังหวะไหน ใคร Consume ไปช่วงเวลาไหน และเป็นลำดับอย่างไร โดยจาก Traces ที่เกิดขึ้นเราจะเห็น Flow ของ Service ประมาณนี้ครับ

Order (Produce) ➡️ CheckStock (Produce) ➡️ Purchase (Produce) ➡️ Notification (Produce) ➡️ Order (Consume)

ทีนี้เราลองมาดูในส่วนของ System Architecture ➡️ DAG กัน

จะเห็นเลยใช่ไหมครับ ว่าแต่ละ Service ถูกเรียกจากไหนไปไหน และยังแสดงถึงจำนวนครั้งที่เรียกด้วย

เอาหละ ถัดมา เรามาลอง Scenario ที่สินค้าในคลังหมดกันเถอะ ว่า Flow ที่เราเห็นจะเปลี่ยนเป็นยังไงบ้าง

จะเห็นว่า Flow ที่เกิดขึ้น กลายเป็น

Order (Produce) ➡️ CheckStock (Produce) ➡️ Order (Consume)

และในมุมมองของ DAG เราจะเห็น Flow ที่

CheckStock (Produce) ➡️ Order (Consume)

เพิ่มขึ้นมาด้วยครับ

ถัดมา ถ้าเป็นเคส Third Party ที่เป็น Payment ของเรามีปัญหาหละ ❌

เราจะเห็น Flow เป็นประมาณนี้ครับ

Order (Produce) ➡️ CheckStock (Produce) ➡️ Purchase (Produce) ➡️ CheckStock (Consume) ➡️ Order (Consume)

และแน่นอนว่าเราก็ยังเห็นอีกด้วย ว่าแต่ละ Service คุยกันด้วย Topic อะไรกันบ้าง

และเมื่อดูใน Console เราก็จะเห็น Flow ตามที่เราวางไว้เลยครับ คือหลังจาก Purchase มีปัญหาแล้วเนี่ย CheckStock เราก็ Rollback การตัด Stock ครับ

❗️หมายเหตุ Console ที่เห็นมาจากการใช้ Node Concurrently ทำให้สามารถดู Log ของแต่ละ Service ที่กำลังทำงานอยู่พร้อม ๆ กันได้ครับ

Reference

--

--

Thanatach Assavarattanakorn
SCB Engineer

Software Engineer, QA spin my head right round right round