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

Postgres บอกว่าไม่มี replica ทั้งที่มันอยู่ตรงนั้นมาตลอด

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

มีอยู่ช่วงสั้นๆ ที่ database production ของเราเป็น single point of failure เรามีหลักฐานสองชิ้นที่เป็นอิสระต่อกัน เราเขียนเรื่องนี้ลงเอกสารสามฉบับ และก้าวต่อไปที่เห็นชัดๆ คือจ่ายเงินซื้อ high availability

ความจริงคือ database ตัวนั้นเป็นคู่สอง node ที่มี automatic failover มาตลอด ไม่มีอะไรพังเลย นอกจากวิธีที่เราอ่านมัน นี่คือเรื่องของ status หนึ่งบรรทัดกับ system view หนึ่งตัว ที่จับมือกันโกหกอย่างมั่นใจ และ query บรรทัดเดียวที่จับได้

ช้างหน้าตาเป็นมิตรยืนอยู่หน้ากระจก ชูการ์ดเปล่าหนึ่งใบ ขณะที่ฝาแฝดหน้าเหมือนกันโบกมืออยู่หลังแผ่นกระจกฝ้า

หลักฐานชิ้นที่ 1: "Replicas: 1"

ThaiPo เก็บ account state ไว้ใน managed Postgres บน Fly.io บน plan แบบ shared-CPU วันหนึ่งเรารันคำสั่งดู status ของ cluster ในบรรดาสิ่งที่มันพิมพ์ออกมา มีบรรทัดนี้:

Replicas: 1

replica หนึ่งตัว node หนึ่งตัว ของที่พังได้หนึ่งชิ้น ใครที่เคยตั้ง replicas: 1 ใน deployment manifest ก็อ่านแบบนี้ทั้งนั้น เพราะที่นั่นมันแปลว่ามีทั้งหมดหนึ่งชุดถ้วน

แต่ที่นี่มันแปลว่า replica หนึ่งตัว เพิ่มเติมจาก primary รวมเป็นสอง node ทุก plan ของโปรดักต์นี้รัน primary คู่กับ streaming standby แต่ตอนนั้นเรายังไม่รู้ และ engineer ที่รอบคอบไม่เชื่อ output ของ CLI แค่บรรทัดเดียว engineer ที่รอบคอบต้อง verify

หลักฐานชิ้นที่ 2: การ verify

Postgres บอกเรื่อง standby ของมันได้ตรงๆ เราเลย connect เข้าไปถาม:

SELECT * FROM pg_stat_replication;
-- one row, every column that matters NULL

ได้มาหนึ่งแถวที่แทบไม่มีอะไร ไม่มี address ไม่มี state ไม่มี position บนจอมันแยกไม่ออกเลยจาก "ไม่มี standby ต่ออยู่" และเข้ากันพอดีกับหลักฐานชิ้นที่ 1 สองแหล่ง คำตอบเดียวกัน เราจดลงไป: node เดียว ไม่มี failover ต้องแก้

แต่ลองอ่าน comment นั้นอีกรอบ หนึ่งแถว ระบบที่ว่างเปล่าคืนศูนย์แถว มีบางอย่างอยู่ตรงนั้น และ Postgres กำลังปฏิเสธที่จะบรรยายมัน

NULL ไม่ใช่ศูนย์

statistics view ของ Postgres มีพฤติกรรมอย่างหนึ่งที่มีเขียนไว้ใน document สมเหตุสมผล และเป็นกับดักชั้นเยี่ยม ถ้า role ของคุณไม่มีสิทธิ์เห็นรายละเอียด session ของคนอื่น view จะไม่โยน permission error ออกมา มันคืนแถวนั้นให้ แล้วลบ column ที่บรรยายแถวนั้นให้ว่าง

database role ของ application ถือ pg_read_all_data จึงอ่านได้ทุก table แต่ไม่ได้ถือ pg_read_all_stats ซึ่งเป็น built-in role อีกตัวแยกต่างหาก อ่านข้อมูลได้ทั้งหมด ฟังดูน่าจะครอบคลุมการอ่าน view เล็กๆ เรื่อง replication ด้วย แต่มันไม่ครอบคลุม และไม่มีอะไรบอกคุณเลย

ถ้าเป็น error เราคงไปเปิด document ภายในสามสิบวินาที แต่แถวที่ถูกปิดบังพาเราไปเขียนคำว่า "single point of failure" ไว้สามที่ output ที่ถูก permission ปิดบังคือความผิดแบบที่น่าเชื่อที่สุด เพราะรูปร่างถูกต้อง มาถึงโดยไม่บ่นสักคำ และบังเอิญตรงกับสิ่งที่เราเชื่ออยู่แล้ว

ถามสิ่งที่ปิดบังไม่ได้

วิธีแก้เล็กจนแทบจะเป็นการดูถูกกัน:

SELECT count(*) FROM pg_stat_replication;
-- 1

Postgres ซ่อนได้ว่าแถวนั้นบอกอะไร แต่ซ่อนไม่ได้ว่าแถวนั้นมีอยู่ standby หนึ่งตัว ต่ออยู่ ตรงตามที่ plan สัญญาไว้ทุกอย่าง

ยังมีวิธีสังเกตแบบทั่วไปสำหรับ database ตัวนี้ ที่ใช้ได้อีกนานหลังเหตุการณ์นี้ ลองรัน SELECT backend_type FROM pg_stat_activity ถ้าทุก session ที่ไม่ใช่ของ role คุณเองกลับมาเป็น NULL คุณไม่ได้กำลังมองระบบที่ idle อยู่ คุณกำลังมองระบบผ่านกระจกฝ้า ข้อสรุปใดๆ เรื่อง replication, activity หรือ lock จากมุมนั้นไม่มีค่าอะไรเลย และทางเลือกมีแค่ count(*) หรือ role ที่มีสิทธิ์มากกว่า

การปิดบังนี้เลือกปิดเป็นบางส่วน ซึ่งเป็นเหตุผลที่มันดูน่าเชื่อขนาดนั้น counter ระดับ database และ statistics ราย table อ่านได้ปกติภายใต้ role เดียวกัน คุณได้ telemetry ที่ใช้งานได้มากพอดีที่จะไว้ใจส่วนที่ว่างเปล่า

สิ่งที่เราเกือบทำลงไป

คำตอบที่ผิดอยู่รอดนานพอจะไปถึงเอกสารสามฉบับ ยังไม่ได้ซื้ออะไรไป cluster แบบนี้ไม่มีคำสั่ง resize "ซื้อ high availability" จึงจะหมายถึงการสร้าง cluster ใหม่แล้ว restore เข้าไป: คือ migrate database production ที่มีความเสี่ยงจริงของมันเอง เพื่อให้ได้ standby ที่ต่ออยู่และ streaming อยู่แล้ว

database ตัวนี้มีประวัติหลอกเรามาก่อน บน plan แบบ shared-CPU มันจะ throttle เมื่อโควตา burst หมด และ session benchmark ยาวๆ ก็คือสิ่งที่ทำให้โควตาหมดได้พอดี เราเคยวัด SELECT 1 ที่ execute บน server ใน 0.014 มิลลิวินาที แต่ใช้เวลา 150 ถึง 220 บนนาฬิกาจริง แล้วกลับมาเป็นสองมิลลิวินาทีในอีกสิบนาทีต่อมา และ load จากการวินิจฉัยของเราเองก็น่าจะมีส่วนช่วยให้มันช้าอยู่อย่างนั้น เครื่องมือวัดกับคนไข้คือเครื่องเดียวกัน

สิ่งที่ standby ไม่ได้ปกป้อง

พอเกลี้ยกล่อมตัวเองให้เลิกตื่นตูมกับสัญญาณเตือนปลอมได้หนึ่งเรื่อง ก็ง่ายมากที่จะเหวี่ยงไปผิดอีกฝั่ง แล้วเก็บ database เข้าแฟ้ม "highly available เรียบร้อย" นี่จึงเป็นรายการตรงไปตรงมาว่า node ตัวที่สองนั้นไม่ครอบคลุมอะไรบ้าง

  • Transaction ไม่กี่ตัวสุดท้าย replication เป็นแบบ asynchronous (synchronous_standby_names ว่างอยู่) commit ได้รับการ acknowledge โดยไม่รอ standby failover จึงอาจทำให้สิ่งที่กำลังเดินทางอยู่หายไปได้ recovery point เล็ก แต่ไม่ใช่ศูนย์
  • Region ทั้งสอง node อยู่ใน region เดียวกัน คู่นี้รอดได้ถ้า node หนึ่งพัง แต่ไม่รอดถ้าไปทั้งตึก
  • Account ทั้งสอง node และ backup ของมันที่ provider ทำไว้เอง อยู่ใต้ account ของ provider เจ้าเดียว เสีย account ไป ทั้งหมดก็ไปด้วยกัน

ข้อสุดท้ายนี่แหละคือเหตุผลที่ backup ตัวที่นับได้จริงอยู่ที่อื่น scheduled job บน infrastructure ของอีกบริษัทหนึ่ง dump Postgres ทุกหกชั่วโมงไปเก็บใน object storage ของ provider เจ้าที่สาม และ mirror message log ไปที่นั่นทุกวัน ด้วยสำเนาที่มีแต่เพิ่มอย่างเดียว รอบไหนล้มเหลวจะมีอีเมลส่งมา กฎของเราพูดตรงๆ: backup ที่อยู่ใน account ที่คุณกลัวจะเสียไป ไม่ใช่ backup สำหรับความกลัวข้อนั้น

ฉบับสั้น

  • NULL ไม่ใช่ศูนย์ และ NULL หนึ่งแถวก็ไม่ใช่ศูนย์แถว นับก่อนแล้วค่อยอ่าน
  • เมื่อระบบตอบมาโดยไม่มี error ให้เช็กว่ามันได้รับอนุญาตให้ตอบหรือเปล่า ความเงียบจาก role ที่ไม่มีสิทธิ์ไม่ใช่หลักฐาน
  • สองแหล่งที่ตรงกันมีความหมายน้อยมาก ถ้าเราอ่านผิดทั้งคู่ไปในทิศทางเดียวกัน ของเราทั้งสองแหล่งมีผู้เขียนคนเดียวกัน คือเราเอง
  • เขียนลงไปว่า redundancy ของคุณไม่ครอบคลุมอะไรบ้าง ไว้ข้างๆ คำอ้างว่าคุณมีมัน รายการนั้นสั้น ชวนอึดอัดนิดหน่อย และเป็นย่อหน้าที่มีประโยชน์ที่สุดใน runbook

standby อยู่ตรงนั้นมาตั้งแต่วันแรก replay ทุก write โดยไม่เคยมีใครเอ่ยถึงสักครั้ง เราติดค้างคำขอโทษมันหนึ่งคำ กับ privileged role อีกหนึ่งตัว

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

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

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

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