THAIPO.ai
กลับไปหน้าบล็อก

เครดิต prepaid คือเบรกเกอร์: คืนที่เงินหมดแล้วไฟดับทั้งระบบ

10 พฤศจิกายน 2569 · ThaiPo

คืนหนึ่ง background job ตัวหนึ่งใช้เงินไปราว $3.40 กับ model ตัวเล็กราคาถูก แล้วการแปลสดก็หยุดลงในทุกแชทบนแพลตฟอร์ม ตัวแปลไม่มี bug ไม่มี deploy ที่พัง health check เขียวทุกตัว เราแค่เงินหมด โดยตั้งใจ และเป็นไปตามที่ออกแบบไว้

ThaiPo จ่ายค่า language model ด้วยเครดิตแบบ prepaid และปิด auto-reload ไว้ เราเคยพูดถึงเรื่องนี้ไว้ย่อหน้าเดียวใน โพสต์เรื่องบิล LLM ของเรา ซึ่งตอนนั้นฟังดูเรียบร้อยดี โพสต์นี้คือเวอร์ชันที่ไม่เรียบร้อย: เบรกเกอร์ตัวนี้มีราคาเท่าไหร่ตอนที่มันตัด และทุกอย่างที่เราต้องสร้างเพิ่ม เพื่อให้มันตัดน้อยลงและไม่มีวันตัดแบบเงียบๆ

ตู้ไฟในบ้านที่มีเบรกเกอร์ตัวหนึ่งสับลง ข้างๆ มีมิเตอร์หยอดเหรียญเล็กๆ ขึ้นว่าหมด และ chat bubble เรียงแถวรออย่างอดทนอยู่ในความมืด

ข้อแลกเปลี่ยน

ถ้าเปิด auto-reload ไว้ loop ที่วิ่งไม่หยุดจะให้ผลลัพธ์เป็นใบแจ้งหนี้ ถ้าปิด loop เดียวกันจะให้ผลลัพธ์เป็นระบบล่ม: ยอดเงินแตะศูนย์ แล้ว provider ตอบทุก request ด้วย 400 อย่างสุภาพว่ายอดเครดิตต่ำเกินไป สำหรับโปรดักต์ที่ดูแลโดย developer คนเดียว และใช้ค่า model วันละไม่กี่ดอลลาร์ ระบบล่มเป็นเรื่องที่รอดได้ แต่บิลสี่หลักที่โผล่มาในคืนเดียวไม่ใช่ ยอดเงินคงเหลือจึงเป็นเพดานค่าใช้จ่ายไปในตัว

นี่เป็นการตัดสินใจเชิงออกแบบที่มีราคา และราคาก็คือเพดานนี้แยกไม่ออกว่าอันไหนคือ retry storm อันไหนคือวันอังคารธรรมดาๆ มันตัดไฟเหมือนกันทั้งสองแบบ

คืนที่มันตัดจริง

เราต้อง classify ตารางคำศัพท์ที่ระบบเรียนรู้มาทั้งตาราง และรัน job นั้นแบบ synchronous ด้วย key ของ production มันดัน input ราว 900,000 token และ output 500,000 token ผ่านไปในชั่วโมงเดียว ประมาณ 65 เท่าของอัตรา output ปกติของเรา ยอดเงินก็ต่ำอยู่ก่อนแล้ว ตอนนั้นเป็นเวลาตีหนึ่งที่กรุงเทพฯ ซึ่งสำหรับ traffic ของเราคือกลางช่วง peak พอดี

เบรกเกอร์ทำหน้าที่ตามที่ถูกติดตั้งมาให้ทำทุกประการ และมันก็เป็น production outage ที่เราก่อเองด้วยสคริปต์ maintenance ด้วย ทั้งสองประโยคเป็นความจริง และไม่มีประโยคไหนหักล้างอีกประโยคได้

มีสองรายละเอียดที่ทำให้แย่ลงไปอีก ตัวแปลเป็น queue worker มันจึง "healthy" ตลอดเวลา: process ยังอยู่ check ผ่านหมด และทุก model call โดนปฏิเสธทุกครั้ง อีกเรื่องคือ bulk job รอบหนึ่งจบด้วย exit 0 ทั้งที่ปล่อยผ่าน 85,018 จาก 111,798 แถวไปโดยไม่ได้ตรวจเลย เพราะเครดิตหมดกลางทาง และ failure mode ของมันคือ fail-open exit code เป็นแค่คำกล่าวอ้าง ไม่ใช่ผลลัพธ์ ให้นับแถว

รอบที่รันใหม่ไปทาง batch API: 88,118 แถว ไม่ถึงสามนาที $6.78 ไม่มี failure เลย ราคาครึ่งเดียว งาน bulk และ backfill ต้องไปทาง batch API นอกช่วง peak หลังจากเช็ก runway แล้ว และไม่มีวันไปยืมงบของ key ที่ใช้งานสด

ถามไม่ได้ว่าเงินเหลือเท่าไหร่

การป้องกันที่นึกออกเป็นอย่างแรกคือ alarm เตือนยอดเงินต่ำ และอุปสรรคที่เจอเป็นอย่างแรกคือไม่มี endpoint ให้ถามยอดคงเหลือ provider เปิดให้ดูว่าเราจ่ายไปเท่าไหร่และใช้ไปเท่าไหร่ แต่ไม่บอกว่าเหลือเท่าไหร่ ตัวเลขนั้นมีอยู่แค่ใน web console

หน้า credits ของเราจึงใช้วิธีแบบ dead reckoning คือรู้จุดตั้งต้นแล้วคำนวณต่อเอาเอง operator พิมพ์ยอดจาก console ใส่เข้าไปครั้งเดียว จากนั้นยอดคงเหลือเท่ากับยอดที่ sync ไว้ ลบด้วยการใช้งานที่ตีราคาแล้วนับตั้งแต่ sync และ runway คือยอดคงเหลือหารด้วย burn

ใช้ตัวเลขการใช้งานจากไหน สำคัญกว่าที่เห็น endpoint รายงานมีสองตัว ตัว cost คืนค่าเป็นดอลลาร์ที่ถูกเรียกเก็บจริง แต่ช้าไปหลายชั่วโมง "ยอดคงเหลือ" ที่สร้างบนตัวนี้จึงนิ่งสนิทในขณะที่เงินกำลังไหลออก ตัว usage คืนจำนวน token ภายในไม่กี่นาที เราจึงตีราคาเองด้วย rate card ของเราเอง โดยไม่ลืมว่ารายการ batch คิดเงินครึ่งเดียว หกชั่วโมงหลัง sync ตัวเลขห่างจาก console ราวสี่เซนต์

Alarm ที่หลับตลอดช่วงระบบล่ม

หน้า credits ป้อนข้อมูลให้ alert อีกที: cron ที่อยู่นอกแพลตฟอร์มเช็ก runway ทุกสิบนาที และส่งข้อความ LINE เข้ามือถือของ operator เมื่อ runway เริ่มสั้น

แล้วยอดเงินก็หมดกลางดึก integration ตัวหนึ่งส่งล้มเหลว 99 จาก 101 ครั้ง อยู่ราวสี่ชั่วโมงครึ่ง และเรารู้เรื่องนี้จากลูกค้า alarm พูดไปครั้งเดียวพอดี ตอนที่ยังมีเครดิตเหลืออยู่ จากนั้นก็เงียบไปตลอดทั้ง outage

มันมีรูอยู่สองรู และทั้งสองรูสอนอะไรได้เยอะ

  • ไม่มีระดับที่ต่ำกว่า "critical" บันไดมีแค่ low แล้วก็ critical และแต่ละระดับพูดวันละครั้ง critical ใช้สิทธิ์ของมันไปแล้ว พอยอดเงินแตะศูนย์จริงๆ ก็ไม่เหลืออะไรที่แย่กว่านั้นให้พูด มันเลยไม่พูดอะไร
  • "หมด" ถูกคำนวณมาจาก runway runway คือยอดคงเหลือหารด้วย burn ช่วงล่าสุด outage ที่ยาวจะลาก burn ลงไปหาศูนย์ การหารจึงไม่มีคำตอบ และ "ไม่มีคำตอบ" ถูกอ่านว่า "ยังไม่ได้ sync ยอดเงิน" ซึ่งไปล้างสถานะ alert ทิ้ง alarm เงียบลง เพราะ outage ลากยาว

ทางแก้คือระดับ empty ที่อ่านจากยอดเงิน ไม่ใช่จาก runway และเตือนซ้ำทุกสามชั่วโมง เพราะตอนที่โปรดักต์ล่มอยู่ ความเงียบจะถูกอ่านว่ากลับมาปกติแล้ว ตอนนี้มี probe แบบคิดเงินขนาดหนึ่ง token คอยถาม provider ตรงๆ ว่ายังรับ request แบบเสียเงินอยู่หรือเปล่า ซึ่งช่วยแก้เคสที่ชวนงงไปด้วย: ถ้าหน้า credits ขึ้น $0 แต่ probe ผ่าน นั่นไม่ใช่ outage แต่เป็น sync ที่เก่าไปแล้วหลังเติมเงิน เคสนี้ยังเตือนแค่วันละครั้งเหมือนเดิม เพราะ alarm ที่ตะโกนเรื่องงานบัญชีวันละแปดรอบ คือ alarm ที่เราจะหัดเมินมันเอง

bug ตระกูลเดียวกันโผล่มาอีกสองครั้ง usage report ที่อ่านไม่ได้ถูกมองว่าใช้ไป $0 ทำให้ "ยอดคงเหลือ" ดีดกลับไปเต็มยอด ตรงจังหวะที่มันต่ำที่สุดพอดี และเมื่อ sync มีอายุครบหนึ่งสัปดาห์ query ก็ขอ bucket รายชั่วโมงมากกว่าที่หนึ่งหน้าอนุญาต ได้ 400 กลับมา แล้วก็อ่านว่าเป็น $0 เหมือนกัน ทุกตัวพังไปในทิศทาง "ทุกอย่างเรียบร้อยดี" ทั้งหมด

ค่าเฉลี่ยรายสัปดาห์ไม่ใช่ตัวเลือกที่รอบคอบ

ตัวเลข burn ตัวหลักเคยเป็นค่าเฉลี่ยย้อนหลังเจ็ดวัน เพราะมันดูนิ่งดี แล้วเราก็ย้าย traffic ส่วนใหญ่ไปใช้ model ที่ถูกกว่า และหน้า credits ก็รายงาน burn มากกว่าของจริงเกินสี่เท่าอยู่หลายวัน: บอกว่า runway เหลือสองวัน ในขณะที่คำตอบจริงคือประมาณแปด

ภาพกลับด้านของเรื่องนี้ต่างหากที่อันตราย หลังเกิด spike ค่าเฉลี่ยรายสัปดาห์จะรายงานแค่เศษเสี้ยวของอัตราจริง runway จะดูยาวเกินจริงไปมาก และเครดิต prepaid จะหมดก่อนที่ค่าเฉลี่ยจะตามทัน ตัวเลขหลักตอนนี้จึงเป็น 24 ชั่วโมงล่าสุด ส่วนตัวเลขรายสัปดาห์ยังอยู่ในฐานะเส้น trend ซึ่งก็เป็นอย่างเดียวที่มันเคยทำได้ดี

ภาษี deploy

ยังมีรูรั่วอีกรูที่เจอตอนนั่งจ้องหน้า credits นั้น รอบการขุดคำศัพท์ของเราถูก throttle ให้รันเป็นช่วงๆ แต่ timestamp "รันล่าสุดเมื่อ" อยู่ใน memory ของ process ทุก deploy จะ restart process ลืม timestamp แล้วรันทั้งรอบใหม่หลัง boot สามสิบวินาที ที่เด็ดกว่านั้นคือ shutdown signal จะ cancel รอบที่กำลังรันอยู่ boot ถัดไปจึงเริ่มใหม่ตั้งแต่ต้น

แต่ละรอบราคาถูก แต่ความถี่ในการ release กลายเป็นรายการค่าใช้จ่ายไปเงียบๆ ตอนนี้ timestamp อยู่ใน Redis และเขียนก็ต่อเมื่อรอบนั้นรันจนจบ รอบที่ถูกขัดจังหวะจึงยังได้รันใหม่ ส่วนรอบที่จบแล้วไม่รันซ้ำ

สิ่งที่เราจะบอกคนที่คิดจะทำตาม

  • เพดานค่าใช้จ่ายแบบตัดขาดเป็น feature จริงที่มีต้นทุนจริง เลือกมันอย่างรู้ตัว และเลือกเฉพาะเมื่อ architecture ของคุณทนการหยุดชะงักได้ ของเราทนได้: ทุกข้อความอยู่ใน durable log อยู่แล้วและไม่มีอะไรหาย แม้ว่าคำแปลที่ช้าเกินหนึ่งชั่วโมงจะตั้งใจไม่ส่งก็ตาม
  • ทดสอบ alarm กับความล้มเหลวที่มันมีไว้เพื่อจับ ของเราถูกทดสอบกับ "กำลังจะหมด" แต่ไม่เคยทดสอบกับ "หมดไปแล้ว"
  • ไม่รู้ไม่ได้แปลว่าศูนย์ report ที่อ่านไม่ได้ การหารที่ไม่มีคำตอบ และยอดเงินที่ไม่เคย sync คือสามสถานะที่ต่างกัน และไม่มีสถานะไหนแปลว่าปลอดภัย
  • วัด burn จากวันล่าสุด ค่าเฉลี่ยเอาไว้ใส่กราฟ
  • งาน bulk ต้องไม่ใช้งบร่วมกับงานที่มีคนนั่งรออยู่ ไม่ว่ากรณีใด

เบรกเกอร์ยังอยู่ต่อไป เรายอมขอโทษที่บริการขาดช่วง ดีกว่าต้องมานั่งอธิบายใบแจ้งหนี้ เราแค่เลิกหวังให้มันเป็นคนบอกเราเองว่ามันตัดไปแล้ว

ThaiPo ออกแบบ สร้าง และดูแลโดยนักพัฒนาคนเดียว Steven Acreman

ลองใช้ในแชทของคุณเอง

ThaiPo อยู่ใน LINE และแปลทุกข้อความให้สองทาง ฟรีตลอดไป เริ่มใช้ฟรี หรือ เพิ่มเพื่อน @thaipo.ai ใน LINE แล้วบอทจะส่งลิงก์สมัครให้ในแชทเลย

ThaiPo is operated by Marine Entertainment Co., Ltd. · Docs · Blog · Privacy · Terms · Contact