วิธีเพิ่มคอลัมน์ในฐานข้อมูล PostgreSQL ไม่ให้ระบบล่มด้วยคำสั่ง lock_timeout

10 นาที 19 views บันทึกเป็น PDF
วิธีเพิ่มคอลัมน์ในฐานข้อมูล PostgreSQL ไม่ให้ระบบล่มด้วยคำสั่ง lock_timeout

เคยไหม? รันคำสั่งเพิ่มคอลัมน์ในฐานข้อมูลแล้วเว็บล่มทั้งระบบ เรียนรู้วิธีใช้ lock_timeout ป้องกันปัญหาคิวล็อกที่โปรแกรมเมอร์มือใหม่ต้องรู้

ทำไมการเพิ่มคอลัมน์ในฐานข้อมูลถึงทำระบบล่มได้

เวลาเราเขียนโปรแกรมแล้วต้องการเพิ่มข้อมูลใหม่ลงในฐานข้อมูล (Database - ที่เก็บข้อมูลของแอปพลิเคชัน) เรามักจะใช้คำสั่งที่เรียกว่า Migration (ขั้นตอนการปรับเปลี่ยนโครงสร้างฐานข้อมูล) ซึ่งดูเหมือนจะเป็นเรื่องปกติและปลอดภัย แต่ในโลกของการทำงานจริง การพิมพ์คำสั่งเพียงบรรทัดเดียวอาจทำให้ทั้งระบบใช้งานไม่ได้โดยไม่รู้ตัว

Lock (การล็อกข้อมูลเพื่อป้องกันไม่ให้ผู้อื่นเข้าถึง) คือกลไกที่ฐานข้อมูลใช้เพื่อรักษาความถูกต้องของข้อมูล เมื่อเราสั่งเพิ่มคอลัมน์ใหม่ ฐานข้อมูลต้องการสิทธิ์พิเศษที่เรียกว่า Access Exclusive Lock (การล็อกแบบผูกขาดสิทธิ์การเข้าถึง) เพื่อป้องกันไม่ให้ใครเข้ามาอ่านหรือเขียนข้อมูลในระหว่างที่โครงสร้างกำลังเปลี่ยน

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

ALTER TABLE orders ADD COLUMN note text;
-- คำสั่งนี้จะขอสิทธิ์ Access Exclusive Lock เพื่อเพิ่มคอลัมน์ note เข้าไปในตาราง orders

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

ผลลัพธ์: หากมีคำสั่งอื่นค้างอยู่ คำสั่งนี้จะรอ และทำให้คำสั่งถัดไปทั้งหมดในระบบต้องรอตามไปด้วยจนกว่าจะครบเวลา Timeout (เวลาที่กำหนดให้รอสูงสุด)

รู้จักกับหายนะที่เกิดจากคำสั่งที่ดูเหมือนไม่มีอะไร

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

ลองนึกภาพว่าเรามีตาราง orders (ตารางเก็บข้อมูลคำสั่งซื้อ) ที่มีข้อมูลเป็นล้านแถว หากมีใครสักคนเปิดหน้าเว็บค้างไว้แล้วมีการเชื่อมต่อค้างอยู่ หรือมีโปรแกรมเมอร์เปิดหน้าจอ Terminal (หน้าจอพิมพ์คำสั่ง) ทิ้งไว้โดยมีคำสั่งค้างอยู่ ฐานข้อมูลจะถือว่าตารางนั้นถูกใช้งานอยู่ตลอดเวลา

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

-- ตัวอย่างสถานะการล็อกที่เกิดขึ้นเมื่อรอคิว
pid | mode | granted | query
10542 | AccessShareLock | t | SELECT count(*) FROM orders
10549 | AccessExclusiveLock | f | ALTER TABLE orders ADD COLUMN note text;
10556 | AccessShareLock | f | SELECT count(*) FROM orders WHERE id = 5;

คำอธิบาย: บรรทัดแรกคือการดึงข้อมูลที่ค้างอยู่ บรรทัดที่สองคือคำสั่งของเราที่กำลังรอคิว (granted = f) และบรรทัดสุดท้ายคือการดึงข้อมูลปกติที่ต้องมาต่อคิวรอเราอยู่

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

ทางออกที่ง่ายที่สุดแต่คนมักมองข้าม

วิธีป้องกันปัญหานี้ง่ายมากจนน่าตกใจ นั่นคือการเพิ่มคำสั่ง lock_timeout (เวลาจำกัดในการรอคิวล็อก) เข้าไปก่อนรันคำสั่งแก้ไขตาราง คำสั่งนี้จะบอกฐานข้อมูลว่า "ถ้าคุณขอสิทธิ์ล็อกตารางไม่ได้ภายในเวลาที่กำหนด ให้ยกเลิกคำสั่งนี้ทิ้งซะ อย่ารอให้ระบบพัง"

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

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

-- เพิ่มคำสั่งนี้ก่อนรันการแก้ไขตาราง
SET lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN note text;

คำอธิบาย: คำสั่ง SET lock_timeout = '2s' คือการบอกว่าให้รอคิวล็อกได้ไม่เกิน 2 วินาที หากเกินจากนี้ให้หยุดทำงานทันที

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

ความแตกต่างระหว่าง Lock Timeout และ Statement Timeout

มือใหม่หลายคนมักสับสนระหว่าง lock_timeout กับ statement_timeout (เวลาจำกัดในการทำงานของคำสั่ง) ซึ่งทั้งสองตัวนี้ทำหน้าที่ต่างกันอย่างสิ้นเชิง การใช้ผิดตัวอาจทำให้เราแก้ปัญหาไม่ตรงจุดและเกิดผลเสียตามมาได้

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

ในขณะที่ lock_timeout สนใจแค่ตอน "รอคิว" เท่านั้น มันไม่สนใจว่าถ้าได้สิทธิ์แล้วคำสั่งจะรันนานเท่าไหร่ มันแค่ต้องการให้เราไม่ไปขวางทางคนอื่นนานเกินไป นี่คือเครื่องมือที่เหมาะสมที่สุดสำหรับการจัดการกับโครงสร้างฐานข้อมูลในสภาพแวดล้อมจริง

-- ตัวอย่างการใช้ SET LOCAL เพื่อจำกัดขอบเขต
DO $$ BEGIN
  SET LOCAL lock_timeout = '2s';
  ALTER TABLE orders ADD COLUMN note text;
END $$;

คำอธิบาย: การใช้ SET LOCAL ช่วยให้การตั้งค่านี้มีผลเฉพาะภายในคำสั่งชุดนี้เท่านั้น ไม่ส่งผลกระทบต่อคำสั่งอื่นๆ ที่อาจกำลังทำงานอยู่ในเซสชันเดียวกัน

ผลลัพธ์: ความปลอดภัยของระบบจะเพิ่มขึ้น เพราะเราจำกัดความเสี่ยงเฉพาะขั้นตอนที่ต้องการเท่านั้น

สร้างระบบ Retry เพื่อความอุ่นใจ

เมื่อเราใส่ lock_timeout เข้าไปแล้ว สิ่งที่จะเกิดขึ้นคือถ้าตารางติดล็อก คำสั่งของเราจะล้มเหลวทันที ซึ่งแน่นอนว่าเรายังคงต้องการเพิ่มคอลัมน์นั้นอยู่ดี วิธีแก้คือการเขียน Retry Loop (ระบบวนซ้ำเพื่อลองใหม่) เพื่อให้ระบบพยายามรันคำสั่งใหม่เรื่อยๆ จนกว่าจะสำเร็จ

เราสามารถเขียนโปรแกรมให้ฐานข้อมูลลองรันคำสั่งซ้ำทุกๆ 1 วินาที หากครั้งแรกติดล็อก ก็ให้รอแล้วลองใหม่ในวินาทีถัดไป วิธีนี้จะทำให้ Migration ของเราทำงานได้สำเร็จในเวลาที่เหมาะสม โดยไม่ไปสร้างความเดือดร้อนให้ผู้ใช้งานคนอื่นในระบบ

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

-- ตัวอย่างการทำ Retry Loop ในฐานข้อมูล
DO $$ BEGIN
  FOR i IN 1..10 LOOP
    BEGIN
      SET LOCAL lock_timeout = '2s';
      ALTER TABLE orders ADD COLUMN note text;
      RETURN;
    EXCEPTION WHEN lock_not_available THEN
      PERFORM pg_sleep(1);
    END;
  END LOOP;
  RAISE 'ไม่สามารถล็อกตารางได้หลังจากลอง 10 ครั้ง';
END $$;

คำอธิบาย: โค้ดนี้จะพยายามรันคำสั่งสูงสุด 10 ครั้ง หากติดล็อกก็จะรอ 1 วินาทีแล้วลองใหม่ หากครบ 10 ครั้งแล้วยังไม่ได้ก็จะแจ้งเตือนข้อผิดพลาด

ผลลัพธ์: คำสั่งจะพยายามหาจังหวะที่ตารางว่างเพื่อทำงานให้สำเร็จโดยอัตโนมัติ ลดภาระของโปรแกรมเมอร์ในการต้องคอยมาตรวจสอบเอง

สรุป: นิสัยเล็กๆ ที่สร้างความต่างให้โปรแกรมเมอร์

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

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

จำไว้ว่าบั๊กที่เกิดจากความประมาทในระบบฐานข้อมูล มักจะเป็นบั๊กที่แก้ยากและสร้างความเสียหายมากที่สุด เริ่มต้นจากการเพิ่มบรรทัด lock_timeout ในโปรเจกต์ของคุณตั้งแต่วันนี้ แล้วคุณจะพบว่าการเป็นโปรแกรมเมอร์ที่เก่ง ไม่ใช่แค่คนที่เขียนโค้ดเร็ว แต่คือคนที่เขียนโค้ดได้อย่างปลอดภัยและมั่นคง


ที่มา: One line stops a migration from taking down production. Almost nobody adds it. — DEV Community

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เรียนรู้วิธีเขียนแอป Flutter เชื่อมต่อ Meta Smart Glasses ให้ทำงานเร็ว ไม่กระตุก ด้วยการวางสถาปัตยกรรมโค้ดและการจัดการข้อมูลแบบมือโปรที่มือใหม่ทำตามได้จริง

ที่มา: DEV Community

3 hours ago 10 นาที
5 views
เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

อยากทำระบบ Real-Time แต่ไม่รู้จะเลือกใช้ Polling, SSE หรือ WebSocket ดี? มาดูวิธีเลือกใช้ให้เหมาะกับงาน เพื่อให้แอปของคุณทำงานลื่นไหลและประหยัดทรัพยากรเซิร์ฟเวอร์

ที่มา: DEV Community

6 hours ago 10 นาที
4 views