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

Recap

ShopMicro คือห้าเซอร์วิส Go ที่ต่อสายไว้ตามสองเส้นทาง เส้นทาง synchronous คือ gRPC: API Gateway grpc-gateway → แปลง REST/JSON เป็น gRPC call แบบ typed ไปยัง Catalog → และ Order → ซึ่งแต่ละตัวเป็นเจ้าของ PostgreSQL ของตัวเอง เส้นทาง asynchronous คือ event: Order publish order.created ไป Kafka ผ่าน outbox → และ relay → ของตัวเอง Payment → ตอบสนองแล้ว publish payment.* และ consumer อิสระสองตัวอ่านผลนั้น — saga → ย้ายแต่ละคำสั่งซื้อไปยัง status สุดท้าย และ Notification → enqueue RabbitMQ send-job ที่ worker → ส่ง รอบ ๆ ทั้งหมดนั้น layer resilience → ทำให้ทุก synchronous hop มีขอบเขต กู้ตัวเองได้ และรู้ทัน outage

ทุกส่วนของประโยคนั้นคือสิ่งที่คุณมีโค้ดทำงานได้แล้ว และที่สำคัญกว่าคือ อธิบายได้ ว่าทำไม ถึงออกมาเป็นรูปนี้

POST /v1/orders เดียวทำให้ทั้งระบบเคลื่อนไหว โดย client ไม่เคยรออะไรเลยหลังจาก response PENDING แรก:

sequenceDiagram
actor Client
participant Gateway as API Gateway
participant Order as Order Service (gRPC)
participant DB as orders + outbox (Postgres)
participant Relay as Outbox Relay
participant KOrders as Kafka: orders
participant Payment as Payment Service
participant KPay as Kafka: payments
participant Saga as Order Saga
participant Notif as Notification Service
participant RMQ as RabbitMQ: notification.send
participant Worker as Notification Worker
Client->>Gateway: POST /v1/orders
Gateway->>Order: CreateOrder (gRPC)
Order->>DB: insert order + items + outbox (order.created)
Order-->>Gateway: Order{PENDING}
Gateway-->>Client: 200 OK (PENDING)
Relay->>DB: poll unpublished outbox rows
Relay->>KOrders: publish order.created
KOrders->>Payment: consume order.created
Payment->>KPay: publish payment.succeeded / payment.failed
KPay->>Saga: consume (group "order")
Saga->>DB: ApplyPaymentResult → CONFIRMED / CANCELLED
KPay->>Notif: consume (group "notification")
Notif->>RMQ: enqueue send-job
RMQ->>Worker: deliver job (ack / retry / DLQ)

consumer สองตัวทางขวา — saga กับ Notification — อ่าน topic payments เดียวกัน ภายใต้ group ต่างกัน ดังนั้นแต่ละตัวเห็นผลการชำระเงินทุกอันแบบเป็นอิสระ: ตัวหนึ่งอัปเดต state ของคำสั่งซื้อ อีกตัวแจ้งลูกค้า และไม่มีตัวไหนรู้ว่าอีกตัวมีอยู่ นั่นคือสถาปัตยกรรมทั้งหมดในภาพเดียว

ทุกทางเลือกในคอร์สนี้เป็น trade-off ไม่ใช่ default คุณเถียงได้ทั้งสองทางในแต่ละอัน:

Decisionทำไมทางนี้ต้นทุนที่คุณยอมรับสร้างใน
Microservices แทน monolithdeploy/scale เป็นอิสระ; ขอบเขตความเป็นเจ้าของชัดต่อ domainnetwork hop, partial failure, และภาระ operation ที่ monolith ไม่มีArchitecture →
gRPC สำหรับ call แบบ synchronousrequest/response แบบ typed, เร็ว, schema-first ระหว่าง gateway กับเซอร์วิสdependency ตอน runtime แบบแข็ง: callee ที่ down เป็นปัญหาของ callerThe gRPC Server →
REST façade ผ่าน grpc-gatewayHTTP/JSON ประตูเดียว generate จาก .proto เดียวกัน — ไม่ driftรูป REST ถูกจำกัดด้วยสิ่งที่ google.api.http แสดงได้; มี hop เพิ่มgrpc-gateway →
Kafka เป็น event log ที่ replay ได้consumer อิสระหลายตัว, ประวัติเต็ม, replay ให้เซอร์วิสที่เพิ่มทีหลังconsumer ทุกตัวต้อง idempotent ภายใต้ at-least-once redeliveryTopics & Groups →
RabbitMQ เป็น work queueหนึ่ง job หนึ่ง worker, ack/retry/dead-letter ต่อ message; scale ด้วยการเพิ่ม workerไม่มี replay — job ที่พลาดตอน worker down หายไปนอกจากยังอยู่ใน queueExchanges & Queues →
Outbox pattern สำหรับ publishevent กับการเปลี่ยน state commit ใน transaction เดียว — ไม่มีวันมีอันหนึ่งโดยไม่มีอีกอันrelay poll เพิ่ม latency; event ถูก publish แบบ at-least-once ไม่ใช่ exactly-onceOutbox & Relay →
Choreography sagaไม่มี coordinator ให้สร้างหรือพึ่ง; การเพิ่ม consumer ไม่ทำให้เซอร์วิสเดิมเสียอะไรลำดับ end-to-end ไม่ได้เขียนไว้ที่ใดที่หนึ่งThe Saga Handler →
Idempotent consumersprocessed_events + terminal-state guard ทำให้ at-least-once ปลอดภัยtable เพิ่มและ bookkeeping ต่อ event บน consumer ทุกตัวThe Saga Handler →
Resilience interceptorstimeout, retry, และ circuit breaker บนทุก gRPC hop มองไม่เห็นจากโค้ด businesspolicy global blunt; behavior ต้องรู้ ไม่ใช่อ่านได้ที่ call siteTimeouts & retries →
Kubernetes + Helmdeployment ของทั้ง stack ที่ทำซ้ำได้และมี version เดียวความซับซ้อนของ cluster จริงที่ไม่ต้องมีสำหรับ local single-nodeKubernetes →

ถ้าคุณบอกคอลัมน์ที่สอง และ ที่สามของแต่ละแถวได้ คุณเข้าใจระบบนี้แล้ว ไม่ใช่แค่ว่าทำงานยังไง แต่รู้ด้วยว่าทำไมเราถึงยอมให้ทำงานแบบนี้

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

  • ไม่มี payment gateway จริง Payment ตัดสินจากกฎ TotalCents ไม่ใช่ card network Process & Publish → พูดชัดว่าการต่อจริงต้องมี payments table ที่ persist และ idempotency key เพราะการ charge card เป็น side effect ที่การออกแบบ stateless ไม่เคยตั้งใจทำให้ปลอดภัย
  • ไม่มี authentication หรือ authorization gateway รับทุก request deployment จริงวาง identity ไว้ข้างหน้า — เรื่องที่ Where to go next → หยิบต่อ
  • ไม่มี notification provider จริง worker “ส่ง” ด้วยการ log และผู้รับเป็น stub — event ชำระเงินพก order_id ไม่ใช่อีเมลของลูกค้า การ resolve ข้อมูลติดต่อถูกละไว้อย่างตั้งใจใน Consuming events →
  • At-least-once ไม่ใช่ exactly-once ทั้ง Kafka และ RabbitMQ hop redeliver ได้ นั่นคือเหตุผลที่ consumer ทุกตัว idempotent แทนที่จะสมมติว่า message มาถึงครั้งเดียว
  • ไม่มี distributed compensation saga ย้ายคำสั่งซื้อไป CONFIRMED หรือ CANCELLED แต่ไม่ unwind กระบวนการหลายขั้นที่ขั้น ทีหลัง ล้มเหลว — saga ที่รวยกว่าพร้อม compensation จริงคือจุดที่ orchestrator เริ่มคุ้มค่า

คุณสร้างระบบ microservice แบบ event-driven จริง: สอง gRPC service บน Postgres หลัง REST gateway ที่ generate มา, Kafka event log และ RabbitMQ work queue ที่ใช้เพื่อสิ่งที่แต่ละอันเก่ง, เซอร์วิส Payment แบบ event-driven, choreography saga ที่ทำให้ปลอดภัยด้วย outbox และ idempotent consumer, เซอร์วิส Notification และ worker ที่เชื่อมสอง broker, และ resilience layer ที่ทำให้ฝั่ง synchronous มีขอบเขตและกู้ตัวเองได้ — ทั้งหมด package สำหรับ Docker Compose และ Kubernetes มากกว่าโค้ด คุณ defend ทุกการตัดสินใจเชิงสถาปัตยกรรมเป็น trade-off ได้แล้ว และบอกได้เป๊ะว่าคอร์สทำอะไรให้ง่ายลงและทำไม บทถัดไป Where to go next → เปลี่ยนการทำให้ง่ายเหล่านั้นเป็น roadmap ที่เป็นรูปธรรมสำหรับพา ShopMicro ไปต่อ