สถาปัตยกรรม

การออกแบบเครื่องคิดค่าบริการแบบ idempotent

Writing
สถาปัตยกรรมApr 12, 202511 min readไทย
KafkaJavaCassandra

ระบบคิดค่าบริการมักล้มเหลวในแบบที่แพงที่สุด คือเงียบ ๆ ด้วยการเรียกเก็บซ้ำหรือทำเรกคอร์ดหาย ทางแก้ไม่ใช่การ retry ให้มากขึ้น แต่คือทำให้ทุกการทำงานปลอดภัยต่อการทำซ้ำ

รูปร่างของปัญหา

เครื่องคิดค่าบริการเปลี่ยนเหตุการณ์ให้กลายเป็นเงิน ในธุรกิจโทรคมนาคมนั่นหมายถึงเรกคอร์ดรายละเอียดการโทร (CDR): วันละหลายพันล้านรายการ มาถึงไม่เรียงลำดับ บางครั้งมาซ้ำ บางครั้งก็หายไป โหมดความล้มเหลวที่แพงที่สุด ไม่ใช่การล่ม แต่เป็นความเงียบ ที่ batch ถูกลองใหม่แล้วเรียกเก็บเงินลูกค้าซ้ำ หรือ partition ที่หล่นหาย ทำให้รายได้สูญไปโดยไม่มีใครสังเกตเป็นเดือน

การลองใหม่ไม่ได้แก้ปัญหานี้ ยิ่งลองใหม่มากยิ่งแย่ลง คำตอบที่ยั่งยืนเพียงทางเดียวคือทำให้ทุกการเขียน ปลอดภัยต่อการทำซ้ำ เพื่อให้การเล่นเรกคอร์ดเดิมซ้ำไม่เปลี่ยนแปลงอะไรเลย

“ระบบที่ idempotent ไม่กลัวการ retry เพราะมันถูกสร้างมาให้เล่นซ้ำได้”

graph LR
  A[CDR sources] --> B(["Kafka ingest"])
  B --> C[Rating engine]
  C --> D{{"Idempotency store"}}
  C --> E[("Ledger")]
  E --> F[Reconciliation]
  F -. verifies .-> E

ภาพที่ 1 / การไหลของข้อมูลการคิดค่าบริการ - ทุกเรกคอร์ดมีคีย์กำกับ ledger และการกระทบยอดต้องตรงกัน ไม่เช่นนั้น pipeline จะหยุด

ทำให้การเขียนเป็น idempotent

ทั้งหมดนี้ตั้งอยู่บนคีย์กันซ้ำที่ได้จากตัวเรกคอร์ดเอง ไม่ใช่จากเวลาที่ประมวลผล การ upsert ลง ledger ใช้คีย์นี้ ดังนั้นความพยายามครั้งที่สองจะพบผลลัพธ์เดิมและคืนค่ากลับโดยไม่เปลี่ยนแปลง

public RatingResult rate(CallDetailRecord cdr) {
    // The dedup key turns a replay into a no-op, not a double charge.
    String key = cdr.accountId() + ":" + cdr.eventId();

    return ledger.upsert(key, () -> {
        Money amount = tariff.price(cdr);
        return new RatingResult(key, amount, cdr.timestamp());
    });
}

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

rating:
  idempotency:
    store: cassandra
    ttl: 90d            # keep keys long enough to cover replays
  reconcile:
    interval: 60s
    on_mismatch: halt   # never settle a record we cannot explain

สิ่งที่เราวัดได้

เครื่องนี้ทำงานในโปรดักชันข้ามหลายตลาดมากว่าหนึ่งปี:

  • 4B เรกคอร์ดที่คิดค่าบริการต่อวัน กระทบยอดครบวงจร
  • 0 การรั่วไหลของรายได้ เมื่อเทียบกับระบบต้นทาง
  • 60s รอบการกระทบยอด หยุดทันทีเมื่อไม่ตรงกัน
  • 1 จอ คู่มือที่ทีม on-call ใช้งานจริง

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