ข้ามไปยังเนื้อหา

การสื่อสารใน Microservices

ใน monolith สองโมดูลทำงานร่วมกันด้วยการเรียกฟังก์ชันของกันและกัน ซึ่งก็คือการเรียก method บน stack เดียวกัน ใน process เดียวกัน และใช้หน่วยความจำร่วมกัน การเรียกแบบนี้ไม่มีวันล้มเหลวเพราะเครือข่าย ไม่ timeout ไม่มาถึงซ้ำสองครั้ง และไม่ทำข้อความหาย latency วัดกันเป็นนาโนวินาที ส่วนรูปแบบความล้มเหลวก็เป็นแค่บั๊กธรรมดาใน code เท่านั้น

พอคุณแยกโมดูลเหล่านั้นออกเป็น service คนละตัว ทุกการทำงานร่วมกันที่เคยเป็นการเรียกฟังก์ชันจะกลายเป็นข้อความที่ต้องข้ามเครือข่าย ซึ่งช้ากว่าหน่วยความจำมาก ทำ packet หล่นและสลับลำดับได้ แถม service อีกฝั่งอาจกำลัง deploy อยู่ รับโหลดเกิน หรือหายไปเฉย ๆ ก็ได้ การสื่อสารจึงเลิกเป็นเพียงรายละเอียดเล็กน้อย และกลายเป็นหนึ่งในประเด็นสำคัญที่นิยามทั้งระบบ

การเรียกฟังก์ชันซ่อนการตัดสินใจหลายอย่างที่การเรียกบริการบังคับให้คุณต้องเลือกอย่างชัดเจน ผู้เรียกจะรอคำตอบ หรือยิงทิ้งแล้วไปต่อ? ผู้เรียกกำลังพูดกับบริการเพียงหนึ่งเดียว หรือกระจายไปยังหลายบริการ? แล้วผู้เรียกจะหาที่อยู่ของบริการที่ต้องการได้อย่างไร ในเมื่อ instance เริ่มทำงาน หยุดทำงาน และย้ายที่อยู่ตลอดเวลา? และคุณจะป้องกันไม่ให้เครือข่ายสะดุดเพียงครั้งเดียวลุกลามกลายเป็นความขัดข้องที่ล้มทั้งระบบได้อย่างไร?

ไม่มีคำตอบเดียวที่ถูกต้อง คำตอบที่ถูกขึ้นอยู่กับการโต้ตอบ และระบบจริงก็ใช้หลายรูปแบบไปพร้อมกัน โมดูลนี้จะมอบคลังคำศัพท์สำหรับรูปแบบเหล่านั้น และ pattern ที่นำมาใช้งานจริง

การโต้ตอบระหว่างบริการแตกต่างกันไปตามแกนอิสระสองแกน

แกนแรกคือ synchronous กับ asynchronous ในการโต้ตอบแบบ synchronous ผู้เรียกส่งคำขอแล้วบล็อกรอจนกว่าจะได้รับการตอบกลับ ทั้งสองบริการถูกผูกมัดกันในเชิงเวลา เพราะทั้งคู่ต้องพร้อมใช้งานในขณะเดียวกัน ในการโต้ตอบแบบ asynchronous ผู้เรียกส่งข้อความให้ตัวกลางแล้วไปทำอย่างอื่นต่อ ผู้รับจะประมวลผลเมื่อใดก็ตามที่พร้อม ดังนั้นทั้งสองบริการไม่จำเป็นต้องทำงานพร้อมกันเลย

แกนที่สองคือ one-to-one กับ one-to-many การโต้ตอบแบบ one-to-one คือคำขอที่ถูกจัดการโดย instance หนึ่งของบริการหนึ่งเท่านั้น การโต้ตอบแบบ one-to-many คือข้อความที่กระจายออกไปยังผู้บริโภคทุกรายที่สนใจ ดังนั้น event หนึ่งสามารถขับเคลื่อนปฏิกิริยาอิสระได้หลายอย่าง

flowchart LR
  subgraph Sync[Synchronous: caller waits]
    A[Service A] -->|request| B[Service B]
    B -->|response| A
  end
  subgraph Async[Asynchronous: caller moves on]
    P[Producer] -->|message| Broker[(Broker)]
    Broker --> C1[Consumer 1]
    Broker --> C2[Consumer 2]
  end
สองรูปแบบการสื่อสาร — แบบ synchronous request/response และแบบ asynchronous messaging ผ่าน broker

การไขว้แกนทั้งสองทำให้ได้แผนผังง่าย ๆ ของรูปแบบการโต้ตอบ:

  • Synchronous, one-to-one — การเรียกแบบ request/response ไปยังบริการเดียว นี่คือ Remote Procedure Invocation (RPI) ผ่าน REST หรือ gRPC
  • Asynchronous, one-to-one — command ที่ส่งไปยังผู้บริโภคหนึ่งรายผ่าน queue และประมวลผลเมื่อพร้อม
  • Asynchronous, one-to-many — event ที่เผยแพร่ไปยัง topic และส่งถึงผู้สมัครรับทุกราย ทั้งสองรูปแบบ async คือ Messaging

เหนือรูปแบบดิบเหล่านี้มี pattern ที่จัดระเบียบการสื่อสารสำหรับทั้งระบบ:

  • Remote Procedure Invocation — รูปแบบที่ง่ายที่สุด: การเรียก request/response แบบมี type เข้าใจได้ง่าย แต่ผูกมัดกันในเชิงเวลา
  • Messaging — การสื่อสารผ่าน broker ลดการผูกมัดของบริการในเชิงเวลาและรองรับการกระจาย (broadcast)
  • API Gateway — จุดเข้าจุดเดียวที่จัดเส้นทาง รวมผลลัพธ์ และรับภาระ cross-cutting concerns ให้กับ client ภายนอก
  • Backends for Frontends — gateway แยกต่างหากสำหรับ client แต่ละชนิด แต่ละตัวปรับแต่งเฉพาะและเป็นเจ้าของโดยทีม client
  • Service Discovery — วิธีที่ผู้เรียกค้นหาตำแหน่งบนเครือข่ายของ instance บริการเมื่อ instance เกิดและดับ

เราเริ่มจากรูปแบบการโต้ตอบดิบสองแบบ — Remote Procedure Invocation และ Messaging — เพราะ pattern ที่สูงกว่าทุกตัวสร้างขึ้นจากรูปแบบเหล่านี้ จากนั้นเราดูว่า client ภายนอกเข้าถึงระบบผ่าน API Gateway อย่างไร แนวคิดนั้นพิเศษขึ้นกลายเป็น Backends for Frontends อย่างไร และสุดท้ายว่าผู้เรียกใด ๆ ค้นหา instance ที่ยังมีชีวิตอยู่ผ่าน Service Discovery อย่างไร แต่ละบทเรียนมีรูปแบบเดียวกัน: สถานการณ์ที่นำไปสู่ pattern, แรงที่ขัดแย้งกัน, สิ่งที่ pattern ทำ, แผนภาพ, ตัวอย่าง และการมองอย่างตรงไปตรงมาว่าต้องจ่ายอะไรบ้าง

  • Remote Procedure Invocation — เริ่มที่นี่ รูปแบบที่ง่ายที่สุด
  • Messaging — ทางเลือกแบบ asynchronous
แกน synchronous กับ asynchronous อธิบายเรื่องอะไร?
รูปแบบการโต้ตอบแบบใดเป็น one-to-many?
เหตุใดการเรียกบริการจึงแตกต่างจากการเรียกฟังก์ชันโดยพื้นฐาน?