2 คะแนน โดย GN⁺ 2023-08-12 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • คอขวดของ Postgres ในแอป SaaS แบบเดี่ยวรุนแรงขึ้น จนการใช้ CPU ค้างอยู่ที่ 60~80% และครั้งหนึ่งพุ่งถึง 100% จนเกิดเหตุขัดข้องสั้น ๆ
  • การสเกลแนวตั้ง ด้วยการเปลี่ยนไปใช้ DB instance ที่ใหญ่ขึ้นนั้นแตะขีดจำกัดแล้ว ทำให้รับโหลดที่เพิ่มขึ้นด้วยวิธีเดิมได้ยาก
  • write sharding และ microservices อาจเพิ่มความจุและความยืดหยุ่นในการปฏิบัติการได้ แต่ก็ยังคงมี ต้นทุนของความซับซ้อน ทั้งด้านแบ็กอัป การมอนิเตอร์ การย้ายข้อมูล ORM และ network topology
  • ในทางปฏิบัติ ทีมใช้เวลา 3 เดือนลบคิวรีหนัก ปรับแต่งโค้ด Rails และจูน Postgres พร้อมแยกคิวรีแบบอ่านอย่างเดียวที่มีต้นทุนสูงบางส่วนไปยัง DB สำเนา
  • ผลลัพธ์คือการใช้ CPU สูงสุดรายสัปดาห์ของ DB ลดจาก 90% เหลือ 30% ทำให้ยังมีพื้นที่ที่จะใช้ระบบปัจจุบันต่อไปได้อีกก่อนย้ายไปสู่สถาปัตยกรรมรุ่นถัดไป

คอขวดของ Postgres และจุดสิ้นสุดของการสเกลแนวตั้ง

  • เมื่อโหลดของแอป SaaS แบบเดี่ยวเพิ่มขึ้น ประสิทธิภาพของ Postgres ก็กลายเป็นคอขวดหลัก
    • การใช้ CPU อยู่ในช่วง 60~80%
    • อย่างน้อยหนึ่งครั้งพุ่งขึ้นถึง 100% และทำให้เกิดเหตุขัดข้องช่วงสั้น ๆ
  • ก่อนหน้านี้เมื่อ DB เริ่มยุ่ง ก็จะแก้ด้วยการเปลี่ยนไปใช้ instance ที่ใหญ่กว่าเพื่อซื้อเวลา
    • วิธีนี้ช่วยให้โฟกัสกับงานอื่นอย่างการพัฒนาฟีเจอร์ได้
    • แต่ในตอนนั้นก็ใช้งาน instance ที่ใหญ่ที่สุดอยู่แล้ว จึงไม่สามารถ สเกลแนวตั้ง ต่อได้อีก

สถาปัตยกรรมรุ่นถัดไปที่น่าสนใจแต่มีราคาแพง

  • ตัวเลือกหลักที่ถูกพูดถึงคือ write sharding และ microservices
  • write sharding คือการมี DB cluster ที่แยกจากกัน และเขียนข้อมูลลง DB ที่กำหนดตามกลยุทธ์การแบ่งพาร์ทิชัน
    • มีศักยภาพในการเพิ่มความจุได้อีก 2~3 หลัก
  • microservices คือการแยก monolith ออกเป็นหลายบริการ และให้แต่ละบริการมี data store ของตัวเอง
    • สามารถเลือก data store ให้เหมาะกับ workload ของแต่ละบริการได้
  • ทั้งสองแนวทางช่วยเพิ่มทางเลือกด้าน fault tolerance และความยืดหยุ่นในการปฏิบัติการ แต่เป้าหมายเร่งด่วนในตอนนั้นคือทำให้ ประสิทธิภาพของ DB กลับมาอยู่ในระดับที่ควบคุมได้

ความซับซ้อนเมื่อเพิ่มแล้วจะสร้างต้นทุนต่อเนื่อง

  • ความซับซ้อนที่เพิ่มขึ้นไม่ได้มีแค่ต้นทุนในการทำโครงสร้างใหม่ แต่ยังตามมาด้วย ต้นทุนด้านความใส่ใจ ในระยะยาว
  • หากเลือก DB sharding การตัดสินใจทางเทคนิคทุกครั้งหลังจากนั้นก็ต้องรับมือกับความซับซ้อนใหม่นี้ไปด้วย
    • แบ็กอัป
    • การมอนิเตอร์
    • การย้ายข้อมูล
    • ORM
    • network topology
  • microservices ก็สร้างภาระคล้ายกัน และการต้องดูแลสถาปัตยกรรมเพิ่มเติมอาจทำให้การส่งมอบฟีเจอร์ช้าลงหรือถูกยกเลิกไปเลย

มองหาพื้นที่เหลือในระบบปัจจุบันก่อน

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

การปรับแต่งที่ทำจริง

  • งานแรกมีวิศวกรสองคนใช้เวลาราว 3 เดือน จัดการปัญหาด้านประสิทธิภาพของ DB เป็นหลัก
    • ไม่มีไม้ตายเพียงอย่างเดียว
    • ใช้ telemetry เพื่อหาคิวรีที่หนัก
    • ระบุตำแหน่งที่ทำให้เกิดคิวรีใน codebase ของ Rails
    • ปรับแต่งหรือลบคิวรีเหล่านั้น
    • จูนการตั้งค่าหลายส่วนของ Postgres
  • งานที่สองมีวิศวกรอีกสองคนแก้ codebase เพื่อให้ คิวรีแบบอ่านอย่างเดียว ที่มีต้นทุนสูงบางส่วนไปรันบน DB สำเนา
    • แยกคิวรี SELECT ที่เกิดบ่อยที่สุดจากการ polling ของ web client ออกจาก DB หลัก

ผลลัพธ์และหลักการในการปฏิบัติการ

  • เมื่อรวมสองงานเข้าด้วยกัน การใช้ CPU สูงสุดรายสัปดาห์ของ DB ลดจาก 90% เหลือ 30%
    • การใช้ CPU สูงสุดรายสัปดาห์: {l:90,30}
  • มี headroom ของ CPU เพิ่มขึ้นมาก และความสามารถในการย้ายโหลดออกจาก DB หลักก็ดีขึ้นด้วย
  • การที่หลายคนร่วมกันแก้หลายส่วนของ codebase ยังช่วยสะสมความรู้แบบกระจายเกี่ยวกับระบบเดิม
  • ความซับซ้อนไม่ได้แย่เสมอไป และในสักวันหนึ่งก็อาจต้องย้ายไปสู่โครงสร้างที่ซับซ้อนกว่า ก่อนจะชนข้อจำกัดพื้นฐานของสถาปัตยกรรม DB
  • แต่ก่อนจะถึงวันนั้น หากรีดประสิทธิภาพจากระบบปัจจุบันให้เต็มที่ก่อน ก็จะสามารถทำงานกับ ระบบที่เรียบง่ายและน่าเบื่อ ได้นานขึ้น ซึ่งคุ้มค่ากว่าในแง่ต้นทุนและการใช้งานจริง

1 ความคิดเห็น

 
GN⁺ 2023-08-12
ความคิดเห็นจาก Hacker News
  • เมื่อเห็นปัญหาประสิทธิภาพของฐานข้อมูล ความคิดที่ร้อนแรงที่สุดของผมสำหรับโปรเจกต์ใหม่คือ ควรออกแบบให้ hot path ของแอปพลิเคชันไม่ใช้ join เลย
    พื้นที่จัดเก็บมีราคาถูก ก็ denormalize ให้หมด แล้วอัปเดตทั้งหมดใน transaction ก็พอ พอเอา join ออกแล้วเร็วขึ้นแค่ไหนนี่น่าทึ่งจริง ๆ ส่วน query วิเคราะห์แบบเฉพาะกิจ ก็ replicate ไปยังฐานข้อมูลอีกตัวสำหรับงานวิเคราะห์ได้
    ผมมีความรู้สึกซับซ้อนต่อ DynamoDB ของ Amazon แต่แนวคิดที่ว่า ถ้าจะใช้ให้ถูก ต้องวางแผนรูปแบบการใช้งานก่อน แล้วค่อยกำหนด schema ทีหลังนั้น เป็นสิ่งที่ฐานข้อมูลเชิงสัมพันธ์ก็น่าเอามาใช้ ทุกวันนี้ผมถึงขั้นมองว่า ถ้าไม่ใช่เพื่อการวิเคราะห์ join ก็ไม่จำเป็น ฐานข้อมูลหลัก ๆ มีคุณสมบัติ ACID และพื้นที่จัดเก็บก็ถูกอย่างเหลือเชื่อ ดังนั้นก็ denormalize ไปเลย
    เพื่อป้องกัน hot partition ควรใช้สิ่งที่ใกล้เคียง UUID แทน integer ไม่ใช่ยาวิเศษและมีข้อเสียอยู่ แต่เราจะคุ้นกับประสิทธิภาพแบบ “ดีพอเสมอ” ที่ scale แนวนอนได้ มากกว่าประสิทธิภาพยอดเยี่ยมของ integer ที่สักวันหนึ่งจะพังลง
    ความคิดที่ร้อนแรงกว่านั้นอีกคือ ควรใส่ index ให้ทุก column แต่เรื่องนั้นไว้คุยกันวันหลัง

    • พูดตรง ๆ คือผมไม่เห็นด้วยเลย ตอนสร้างสตาร์ทอัพ ช่วงปีที่ 1–5 แทบไม่ได้สนใจประสิทธิภาพ และในปีที่ 6 เกิดปัญหาประสิทธิภาพกับตารางบางตัว ต้องใช้แรงวิศวกรหลายเดือนเพื่อ optimize
      จากมุมมอง technical debt ถ้าพยายามทำให้ทุกอย่างเร็วตั้งแต่ต้น คงแพงกว่านี้มาก ความเร็วในการพัฒนาจะช้าลงอย่างมาก และมีโอกาสสูงที่จะล้มเหลวในวิกฤตหลายครั้ง
      แทนที่จะเป็นแบบนั้น เราจ่ายค่าเครื่องเพิ่มจากที่จำเป็นจริงเดือนละไม่กี่พันดอลลาร์ และประหยัดแรงคนไปได้หลายเดือนในช่วงที่ยังจ้างวิศวกรได้ไม่พอและ opportunity cost ของการพัฒนาฟีเจอร์สูงมาก เพราะเราไม่อาจรู้ล่วงหน้าได้ว่าคอขวดจะอยู่ตรงไหน การทำให้ทุกอย่างเร็วตั้งแต่แรกคงต้องใช้แรงงานมากขึ้น 10–20 เท่า คอขวดบางอย่างก็มาแบบไม่คาดคิด
      Join อาจเป็นโทษในระดับขนาดใหญ่มาก แต่สตาร์ทอัพส่วนใหญ่ โดยเฉพาะช่วงแรก ๆ ยังไม่มีปัญหาเรื่อง scale การ denormalize อาจเป็น optimization ที่ดี แต่ต้องจ่ายต้นทุนด้านความเร็วจากการ sync ทุกสำเนาทุกครั้งที่มีการเปลี่ยนแปลง สุดท้ายต้องมีใครสักคนทำบั๊กที่ไม่อัปเดตฟิลด์ที่ถูก denormalize และไม่ใช่แหล่งข้อมูลหลัก ทำให้ผู้ใช้เห็นข้อมูลเก่า โดยทั่วไปการใช้ join แล้วค่อย optimize ภายหลังด้วย read-aside cache ฯลฯ มักมีต้นทุนรวมถูกกว่าการบิด schema ให้ผิดรูป
    • ถ้า join มี index ที่ถูกต้องและเกิดขึ้นในเซิร์ฟเวอร์เดียวกัน ก็ไม่ควรส่งผลต่อประสิทธิภาพมากนัก คำว่า “เอา join ออกแล้วเร็วขึ้นมาก” ไม่จริงหาก ใช้ join อย่างถูกต้อง
      ในทางกลับกัน การเขียนข้อมูลเดียวไป 20 ที่แทนที่จะเป็น 1 ที่ ช้ากว่ามากในแง่ประสิทธิภาพ และ query ก็ซับซ้อนอย่างยิ่งและเกิดบั๊กได้ง่าย กลายเป็นว่าอัปเดตไปแล้ว 18 ที่แต่ลืมอีก 2 ที่
      ข้อดีของ denormalization มักอ้างว่าพื้นที่จัดเก็บถูก แต่พื้นที่จัดเก็บเป็นปัญหาเล็กที่สุดในเรื่องนี้ ปัญหาจริงคือ พื้นที่ผิวของบั๊ก ที่ใหญ่ขึ้นมากและประสิทธิภาพการเขียนที่แย่ ซึ่งประสิทธิภาพการเขียนนั้นก็สามารถกัดกินประสิทธิภาพการอ่านได้ง่าย ๆ
    • แอปพลิเคชันมีทั้งแบบ “สูง (tall)” และแบบ “กว้าง (wide)” คำแนะนำเรื่องการออกแบบและ optimize ฐานข้อมูลแทบทั้งหมดมีไว้สำหรับ แอปพลิเคชันแบบสูง
      แอปพลิเคชันแบบสูงโดยพื้นฐานแล้วทำงานหลักอย่างเดียว และอย่างอื่นคอยสนับสนุนงานนั้น บริษัท Big Tech ส่วนใหญ่ที่นึกออกก็อยู่กลุ่มนี้ ใน data model มีแนวคิดขับเคลื่อนสำคัญจริง ๆ อยู่เพียงไม่กี่อย่าง
      Facebook โดยเนื้อแท้มีแค่คน โพสต์ และโฆษณา Netflix ก็ประมาณ account กับ show ส่วนผลิตภัณฑ์ของ Amazon มี seller, buyer, item เป็นแกนหลัก และอาจมีบางอย่างด้าน logistics ต่อท้าย
      เหตุผลที่แอปพลิเคชันแบบสูงมีเยอะคือมันง่าย ง่ายกว่าแอปพลิเคชันแบบกว้างที่มักเรียกว่า “enterprise” มาก เหตุผลที่ซอฟต์แวร์ enterprise แย่ก็เพราะมันยาก และตรงนี้คือพื้นที่ที่ยังถูกสำรวจน้อยที่สุดและมีโอกาสมหาศาล ผู้เล่นเดิมอย่าง Oracle ทำได้แย่มากในพื้นที่นี้ และถ้าคุณเข้าไปด้วยวิธีคิดแบบแอปพลิเคชันสูง คุณก็จะเป็นแบบนั้นเหมือนกัน
      คำแนะนำอย่าง “อย่าใช้ join เด็ดขาด”, “ออกแบบโดยยึดตารางเดียวเป็นศูนย์กลาง” อาจสมเหตุสมผลสำหรับแอปพลิเคชันแบบสูง แต่เป็นคำแนะนำที่แย่มากสำหรับ แอปพลิเคชันแบบกว้าง เรามักเห็นบริษัทแอปพลิเคชันที่สูงมาก ๆ พยายามทำสิ่งที่อยู่นอก core competency แล้วล้มเหลวอย่างน่าอนาถ เพราะเต็มไปด้วยคนที่ยกคำแนะนำนี้ขึ้นหิ้ง
      คำแนะนำนี้มีไว้สำหรับบริษัทที่ประสบความสำเร็จแล้ว ทำงานง่าย ๆ และเก็บผลไม้ที่เอื้อมถึงได้ง่ายไปหมดแล้ว แม้แต่แอปพลิเคชันแบบสูงที่ยังไม่ตกเป็นเหยื่อของความสำเร็จ ก็ไม่จำเป็นต้องเฉือน data model เพื่อประสิทธิภาพ บริษัทที่ประสบความสำเร็จอย่างมหาศาลแล้วและกำลังบีบประสิทธิภาพหยดสุดท้ายต่างหากที่ต้องคิดเรื่องนี้ และบริษัทเหล่านั้นเป็นกลุ่มที่ต้องการคำแนะนำน้อยที่สุด คำแนะนำที่ยึดแอปพลิเคชันแบบสูงเป็นศูนย์กลาง เช่น “FAANG ทำแบบนี้ คุณก็ต้องทำ”, “ถ้ามีผู้ใช้พันล้านคนจะทำยังไง?” ทำให้หัวของคนที่อยากทำสิ่งที่น่าสนใจกว่าการแสดงโฆษณาให้คนนับพันล้านสกปรกไปหมด
    • ในช่วงไม่กี่ปีที่ผ่านมา ผมเห็นความเจ็บปวดที่เกิดจากนักพัฒนาพึ่งพา denormalization มากกว่าปัญหาประสิทธิภาพจากการ join ตารางใหญ่เสียอีก
      join ขนาดใหญ่ยังมีวิธีแก้ประสิทธิภาพภายหลังได้ เช่น ดันไปอยู่ใน materialized view หรือทำ ETL ไปยัง column store แต่ถ้าใครสักคนคัดลอก column subtotal_cents ไว้ใน model Order, Invoice, Payment, NotificationEmail, UserProfileRecentOrders แล้วทำให้มี 296 จุดที่อ้างอิงหรืออัปเดตมัน เส้นทางกลับสู่สภาพปกติจะยาวมาก
    • ผมแทบไม่เคยเห็นกรณีที่ join เองเป็นคอขวดด้านประสิทธิภาพเลย ถ้าจะมีหลักการเดียวที่สังเกตได้คือ “อย่าทำให้ตารางใหญ่เกินไป
      โดยมากปัญหาเกิดจากตารางที่มีลักษณะเป็น historical record ข้อมูลที่จำเป็นต่อการปฏิบัติงานประจำวันจริง ๆ เป็นเพียงส่วนน้อยมากของตาราง แต่ไม่ว่าจะมี index อะไร การทำงานกับตารางขนาดมหึมาก็ย่อมช้าลง การเพิ่ม index เข้าไปอีกเองก็กลายเป็นปัญหา
      อย่างน้อยในฐานข้อมูลเชิงสัมพันธ์แบบดั้งเดิม การใส่ index ให้ทุก column อย่างเดียวไม่พอ ต้องกำหนด index แบบผสมหลาย column ที่ถูกต้องและสามารถถูกใช้งานได้ด้วย ส่วน DynamoDB อาจต่างออกไป
  • รีดประสิทธิภาพจากสิ่งที่มีให้มากที่สุด และเมื่อรีดไปได้สักพักแล้ว ให้เปลี่ยนวิธีมองปัญหา แล้วลองรีดจากตรงนี้ ตรงนี้ และตรงนี้เพิ่มอีก จู่ ๆ ก็จะรู้ว่ายังเหลืออะไรให้ใช้ได้อีกมาก
    ผมเคยปรับแต่งโมโนลิธขนาดใหญ่ราวสองเดือน จากสภาพที่ PM และทีมมองว่าไม่มีอะไรให้รีดแล้วที่ต่ำกว่า 2,000 RPS พอเปลี่ยนฮาร์ดแวร์ก็ขึ้นไปได้ต่ำกว่า 3,200 RPS จากนั้นปรับอีกไม่กี่วันเป็น 4,000 RPS พยายามเพิ่มอีกหน่อยเป็น 10,000 RPS และราวหนึ่งสัปดาห์ถัดมาก็ขึ้นไปถึง 40,000 RPS
    มีคนพูดว่า “แค่นี้ก็พอแล้ว ไม่จำเป็นต้องไปต่อ” แต่เมื่อเปลี่ยนหลายอย่างพอสมควร มันกระโดดเป็น มากกว่า 2 ล้าน RPS บนเครื่องเดียว และหนึ่งเดือนถัดมาก็รองรับได้อย่างเสถียรที่ มากกว่า 40 ล้าน RPS พร้อม latency ต่ำ ยังมีช่องให้ดันต่อได้อีกเล็กน้อยด้วย
    ตอนนี้เราใช้ไม่ถึง 5% ของความจุที่ดึงออกมาได้ แค่เปลี่ยนวิธีคิดเกี่ยวกับปัญหาก็ทำให้เกิดการเปลี่ยนแปลงระดับนั้นแล้ว การย้ายจากเซิร์ฟเวอร์เก่าไปเซิร์ฟเวอร์ใหม่เพียงอย่างเดียวทำให้เพิ่มจาก 1,800 RPS ไปเป็นแค่เกิน 3,000 RPS นิดหน่อยเท่านั้น การเพิ่มฮาร์ดแวร์ไม่ได้แก้ปัญหารากฐาน และการเพิ่มความซับซ้อนก็เป็นเพียงการเลื่อนปัญหาออกไป เมื่อเปลี่ยนวิธีคิดเกี่ยวกับปัญหา ตัวปัญหาเองและคำตอบก็เปลี่ยนไป

    • อยากรู้จริง ๆ ว่าโมโนลิธนั้นทำอะไรได้แย่มากขนาดไหน ถึงรีดประสิทธิภาพออกมาได้ระดับนั้น เป็นเพราะ query แย่เกินไปหรือเปล่า มี query มากเกินไปไหม ไม่มี cache เลยหรือไม่ หรือกำลังทำงานที่ควรทำพร้อมกันได้แบบ synchronous อยู่หรือเปล่า
    • อยากรู้ว่าสามารถแชร์ได้ไหมว่าเนื้อหาหรือโปรโตคอลของ request เหล่านี้คืออะไร และ ขนาด payload เฉลี่ยของ request กับ response แต่ละฝั่งอยู่ประมาณเท่าไร
  • ถ้าจะเสริมประโยคที่ว่า “แยกโมโนลิธออกเป็นหลายบริการที่เชื่อมต่อกัน โดยแต่ละบริการมีที่เก็บข้อมูลของตัวเองซึ่งขยายได้ตามเงื่อนไขของมัน” ณ จุดนี้ไม่จำเป็นต้องแยก microservice ทุกอย่างที่เป็นไปได้ออกมาทั้งหมดก็ได้ แค่ถามว่า “การแยกส่วนไหนจะส่งผลมากที่สุด?” ก็พอ
    ในกรณีของผม ผมแยกข้อมูล time-series บางส่วนจาก Mongo ไปไว้ที่ Cassandra โครงสร้างตารางของ Cassandra เข้ากันได้ดีกว่ามาก ชุดข้อมูลนั้นมี schema ที่นิยามไว้ชัดเจน และ Cassandra สามารถ pack ข้อมูลได้มีประสิทธิภาพกว่ามาก ในส่วนนั้นเราไม่ต้องการความยืดหยุ่นของเอกสาร JSON
    เพราะข้อมูลส่วนนั้นเป็นสัดส่วนใหญ่ของข้อมูลทั้งหมด หลังจากนั้น Mongo ก็อยู่ในสภาพที่ค่อนข้างน่าพอใจ การแยกที่จำเป็นมีเพียงครั้งเดียว ในทางเทคนิค ก่อนและหลังจากนั้นก็ยังเป็นโมโนลิธเหมือนเดิม เพียงแต่บริการเดียวกันเขียนลงฐานข้อมูลสองตัวเท่านั้น
    ที่น่าขันคือภายหลังมีสถาปนิกแนวถกบนโต๊ะคนหนึ่งอยากรวมข้อมูลทั้งหมดกลับเข้าไปเป็น JSON document store และก็มีการถกกันหลายรอบในทำนองว่า “เราเคยไปทางนั้นมาแล้ว และรู้ว่าปลายทางเป็นอย่างไร”

    • น่าสนใจที่แนวคิด microservice ถูกโยนออกมาเหมือนเป็น “ทางแก้” ที่ชัดเจนเกินไป แต่มันไม่ใช่แบบนั้น
      การขยายแนวตั้ง ควรเป็นวิธีแรกที่ชัดเจนกว่า เป็นจุดที่หลายคนมองข้าม และบทความก็พูดถึงบางส่วน แต่การขยายแนวตั้งใกล้เคียงกับการขยายแนวนอนที่ไม่ทำลายความสอดคล้องของฐานข้อมูล
      ยังมีช่องให้รีดได้อีกมาก และเมื่อเกิดปัญหาประสิทธิภาพ ก็แทบไม่ค่อยมีเหตุให้ต้องโยน anti-pattern อย่างการละเลย join หรือการตรวจสอบความถูกต้องของข้อมูลออกมาอย่างง่าย ๆ
    • ในบทความเองก็มีการแยกในความหมายหนึ่งเหมือนกัน เพราะโยน SELECT query หนัก ๆ บางตัวไปให้ replica
    • ที่น่าสนุกคือผมมักเจอปัญหาตรงกันข้ามอยู่บ่อย ๆ ต้องอธิบายซ้ำ ๆ ว่าทำไม Cassandra ถึงไม่เหมาะกับข้อมูลที่อายุค่อนข้างสั้นและอัปเดตบ่อย เหตุผลคือ tombstone
    • จุดสำคัญอีกอย่างคือเซิร์ฟเวอร์หนึ่งตัวสามารถอ่านจากที่เก็บข้อมูลสองแห่งได้ เหมือน read replica ที่คอมเมนต์พี่น้องพูดถึง ไม่มีอะไรห้ามไม่ให้ process เดียวกันอ่าน Postgres และ Redis ร่วมกัน
  • ถ้านักพัฒนาอ่านผลลัพธ์ของ EXPLAIN/ANALYZE เป็น และทำ indexing กับการปรับ query ให้เหมาะสมได้ถูกต้อง ก็จะหลีกเลี่ยงการตัดสินใจออกแบบเกินจำเป็นได้มากมาย
    แค่บันทึก query คัดกรองตัวที่ถูกรันบ่อยมากหรือใช้เวลานานมาก cache ตัวที่รันบ่อย และปรับแต่งตัวที่หนัก ถ้าทำอย่างเป็นระบบ ระบบก็จะแข็งแรงขึ้น
    จากประสบการณ์ของผม สิ่งที่ช่วยได้มากคือ APM, slow query log, read/write replica ของฐานข้อมูล, partitioning และ sharding

    • แค่การอ่าน output ของ explain ก็อาจเป็นงานใหญ่ทีเดียว ฐานข้อมูลเป็นโลกอีกใบหนึ่งโดยสมบูรณ์ โดยเฉพาะถ้าแทบไม่ได้เขียน SQL เองโดยตรง
      เครื่องมืออย่าง https://explainmysql.com ช่วยให้เห็นชัดขึ้นว่าจริง ๆ แล้วควรปรับอะไร จึงเป็นระบบที่ง่ายกว่าสำหรับนักพัฒนาที่มีความรู้พอจะตั้งค่าฐานข้อมูลได้ แต่ยังไม่ถึงขั้นเข้าใจวิธีใช้งานภายใน
      เดาว่าคงมีใครสักคนกำลังทำ ระบบ AI ที่ใส่ schema กับ log เข้าไปแล้วรัน SQL ที่ปรับปรุงให้แบบวิเศษ ๆ อยู่แล้ว จะเชื่อได้หรือไม่ก็ไม่รู้ แต่หลายบริษัทคงอยากใช้ของแบบนั้นมากกว่าจ้าง DBA ประจำ
    • อยากรู้ว่ามีแหล่งข้อมูลดี ๆ สำหรับทำความเข้าใจ SQL execution plan ไหม โปรเจกต์ปัจจุบันเจอปัญหาประสิทธิภาพ query ของ MS SQL Server เยอะมาก
      ไม่แน่ใจว่าจำเป็นต้องระบุ index hint สำหรับทุก query เสมอหรือเปล่า บางครั้งดูเหมือนว่า query ไม่ได้ใช้ index นั้น แม้ index จะมีอยู่ก็ตาม คิดว่าถ้าใช้ SQL execution plan น่าจะช่วยให้เข้าใจปัญหานี้ได้ดีขึ้น
    • นับไม่ถ้วนแล้วว่าผมเห็นเซสชัน “สถาปัตยกรรมเจเนอเรชันถัดไป” มากี่ครั้ง ซึ่งส่วนใหญ่สามารถแทนที่ได้ด้วยการทำ due diligence ต่อ implementation ปัจจุบัน
      โค้ดแย่ ๆ แก้ด้วยสถาปัตยกรรมใหม่ไม่ได้ มันแค่เลื่อนปัญหาออกไปชั่วคราวเท่านั้น
  • นึกถึงคำพูดหนึ่งที่ชอบขึ้นมาได้ “คุณออกไปรบด้วยกองทัพที่คุณมี ไม่ใช่กองทัพที่คุณอยากได้ หรือกองทัพที่คุณหวังว่าจะมีในภายหลัง”
    อาจอยากมองข้ามไปว่าคำพูดนี้มาจาก Donald Rumsfeld แต่เขาก็มีคำพูดดี ๆ อย่าง “unknown unknowns” อยู่เหมือนกัน
    เวลาทำงานเป็นทีม ผมมักนึกถึงคำพูดนี้บ่อย ๆ ไม่ใช่ทุกคนจะเห็นด้วยอย่างสมบูรณ์ หรือมีความเข้าใจเดียวกันและเป้าหมายร่วมกันเสมอไป บางคนอาจทำงานแบบไม่มีประสิทธิภาพ หรือในวิธีที่ผมไม่ชอบ แต่การมีทีมย่อมดีกว่าไม่มีทีม ดังนั้นจึงต้องหาวิธีที่ดีที่สุดในการบรรลุเป้าหมายด้วยทีมที่มีอยู่
    ใช้กับระบบได้ดีเช่นกัน

    • Rumsfeld มีคำคมดี ๆ อยู่ไม่น้อย ส่วนใหญ่เกิดขึ้นในบริบทของการอธิบายว่าสงครามอิรักเละเทะขนาดนั้นได้อย่างไร และสถานการณ์นั้นเองก็ดูเหมือนต้องการภาวะผู้นำที่เขาน่าจะมอบให้ได้อย่างยิ่ง หากดูจากคำคมของเขาเพียงอย่างเดียว
    • ทุกครั้งที่ได้ยินชื่อ Rumsfeld ผมจะนึกถึงฉากที่เขาปฏิเสธอยู่นานกว่า 10 นาทีที่จะยืนยันว่าเขาไม่ใช่กิ้งก่า
      https://www.youtube.com/watch?v=XH_34tqxAjA
    • “ไม่มี แผนการรบ ใดรอดพ้นหลังปะทะกับศัตรู”
      https://www.google.com/search?q=no+battle+plan+survives
    • คำพูดของ Mattis ที่ว่า “ศัตรูก็มีหนึ่งเสียงโหวตเหมือนกัน” ก็เป็นคำเตือนความจริงที่ดี แม้จะทำให้ผู้คนโกรธมาก แต่มีประโยชน์ในบริบทอย่างความปลอดภัย ความเป็นส่วนตัว และ DRM
    • ผมก็ชอบประโยคคล้าย ๆ กันของ Steven Pressfield ด้วย “นักกีฬารู้ว่าไม่มีวันไหนที่ตื่นขึ้นมาโดยไม่เจ็บปวด เขาต้องลงแข่งทั้งที่บาดเจ็บ”
      ข้อนี้ใช้กับตัวเราเองได้ดีกว่ากับระบบ
  • จากประสบการณ์ของผม ในเว็บแอปที่สร้างบน ORM มี ผลไม้ห้อยต่ำ ให้เก็บจากการปรับแต่งคิวรีเยอะมากเมื่อโหลดฐานข้อมูลกลายเป็นปัญหา
    นอกเหนือจากพื้นฐานอย่าง “มีปัญหา N+1 หรือไม่” แล้ว บางครั้ง ORM ก็สร้างคิวรีที่ดีที่สุดไม่ได้ ผมไม่อยากสร้างเว็บแอปโปรดักชันที่ซับซ้อนโดยไม่มี ORM แต่บางครั้งก็ต้องสามารถหลุดออกจาก ORM ได้
    ต้องทำ profiling คิวรีที่รันจริงในโปรดักชันและใช้ทรัพยากรมากที่สุด ดูคิวรีนั้น แล้วทำความเข้าใจรูปแบบของตารางเป้าหมาย บางกรณี ORM ใช้ join แต่จริง ๆ แล้วต้องใช้ subquery และก็มีกรณีกลับกันด้วย บางครั้งต้อง pre-aggregate ผลลัพธ์บางส่วน หรือปรับเงื่อนไข WHERE ใน join ที่ซับซ้อน
    ผมเคยเห็นคิวรีที่ ORM สร้าง ซึ่งรันค่อนข้างบ่อยและกำลังฆ่าฐานข้อมูล จากเดิมใช้เวลารันเกิน 20 วินาที กลายเป็นต่ำกว่า 1 วินาทีด้วยการแก้ไขเล็ก ๆ ไม่กี่จุด

    • ตอนนี้กำลังทำงานกับ SQLAlchemy ซึ่งเป็น ORM ของ Python และการทำให้ INSERT ใช้ RETURNING ไม่ใช่เรื่องง่าย ต้องตั้งค่า option ที่ไม่ค่อย intuitive อย่าง expire_on_commit=False
      option นี้ก็ไม่ได้รับประกันว่าจะใช้ RETURNING เพียงแต่จะใช้ถ้า database driver และฐานข้อมูลรองรับ และ ORM รองรับชุด driver/ฐานข้อมูลนั้น SQL ที่สร้างขึ้นจะแสดงใน log แต่ไม่มี API ให้ตรวจสอบจริง ๆ ดังนั้นถ้าไม่ capture log เองแล้วขูดออกมา ก็ไม่มีทางบังคับใน test suite ว่าต้องใช้ RETURNING โชคดีที่ใน framework Pytest ทำได้ค่อนข้างง่าย
      ผมชอบ ORM แต่เรื่องแบบนี้ซับซ้อนจนน่าหงุดหงิดในหลายชั้น ผมเข้าใจว่า SQLAlchemy เป็นไลบรารีขนาดใหญ่ จึงทำให้ทุกอย่างง่ายไม่ได้ แต่กรณีนี้แสดง trade-off ของการใช้ ORM ได้ดี
      ผมรู้ว่าถ้าใช้ insert() เองใน Core ก็จะได้ตามต้องการ ตรงนี้กำลังพูดถึงกรณีที่ .add() อ็อบเจกต์ ORM เข้าไปใน AsyncSession
    • ปัญหาใหญ่ของ ORM คือ object-oriented ไม่ใช่วิธีที่ดีในการจัดระเบียบซอฟต์แวร์หรือข้อมูลในโดเมนส่วนใหญ่
      business logic ส่วนใหญ่เหมาะจะอธิบายด้วยภาษาของ relational algebra และส่วนขยายบางอย่าง มากกว่า OOP
  • ประโยคที่ว่า “ต้นทุนที่แท้จริงของความซับซ้อนที่เพิ่มขึ้น และมักเป็นต้นทุนที่ใหญ่กว่ามาก คือความสนใจ” ก็คือ ภาระทางปัญญา นั่นเอง
    ผมเหนื่อยกับการทำงานในระบบ microservices ที่ยังมี downtime อยู่ แต่ไม่มีใครรู้ว่าทั้งหมดทำงานอย่างไร ส่วนใหญ่จริง ๆ แล้วเป็น distributed monolith ที่การเปลี่ยนแปลงกินหลายบริการและต้อง deploy ตามลำดับ ข้อมูลต้อง replicate งานต้อง synchronize และสถานะต้องแชร์กัน
    https://www.youtube.com/watch?v=y8OnoxKotPQ

    • microservices ที่ “ไม่มีใครรู้ว่าทั้งหมดทำงานอย่างไร” เป็น กลิ่นเหม็นทางสถาปัตยกรรม ที่พบได้บ่อยมาก ประเด็นเดิมคือไม่ควรมีใครต้องรู้ทั้งหมด และไม่ควรจำเป็นต้องรู้ด้วย
      แต่ถ้าการซ่อมหรือแก้ระบบต้องมีใครสักคนรู้ทั้งหมด นั่นเป็นสัญญาณแรงมากว่าละเมิดกฎอย่าง single responsibility หรือ abstraction ที่ถูกต้องผ่าน API แต่จากประสบการณ์ของผมพบได้บ่อยมาก การ debug pipeline ที่ประกอบด้วย microservices N ตัว มักต้องรันและ build ทั้ง N service ในเครื่อง local
      พูดอย่างเคร่งครัด นี่คือ monolith ที่เพิ่ม network partition และความแปรปรวนของ build/deploy แบบไร้ขีดจำกัดเข้าไป สุดท้ายผมมองว่าเป็นสภาพแวดล้อมการทำงานที่ยากสุดขั้ว เกินความสามารถของโปรแกรมเมอร์มนุษย์คนใดก็ตาม
  • ผมชอบบทความนี้ หลายเดือนที่ผ่านมา ผมพยายามสื่อสารข้อความเดียวกันนี้กับผู้จัดการ แต่ก็ไม่ค่อยสำเร็จ
    คลัสเตอร์ Redshift ที่ overload ล่มไปหลายครั้ง และตอนนี้เรา scale ขึ้นถึงขีดสุดด้วย node RA3 แล้ว จึงกำลังจะเริ่ม migration ขนาดใหญ่ไปยัง “โครงสร้างพื้นฐานรุ่นถัดไป” ซึ่งในที่นี้หมายถึง คลัสเตอร์ Redshift สามชุด ที่จัดการด้วย CDK
    โครงสร้างพื้นฐานใหม่ซับซ้อนกว่าการตั้งค่าปัจจุบันมาก และผมไม่แน่ใจว่ามันจะเป็นกระสุนเงินที่ทุกคนคาดหวังหรือไม่

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

  • วิธีแก้ปัญหาที่หาโอกาสปรับแต่งประสิทธิภาพในระบบแล้ว รีดประสิทธิภาพที่เหลืออยู่ ออกมาได้ เป็นอะไรที่สนุกจริง ๆ
    นึกถึงหนังสือ Understanding_Software_Dynamics ของ Richard L. Sites หนังสือเล่มนี้สอนวิธีวัดและแก้ปัญหา latency รวมถึงการลด latency ในระดับใหญ่สามารถสร้างการประหยัดได้มากแค่ไหน
    ปัญหาแบบนี้วัดและให้เหตุผลได้ยาก แต่ทางแก้มักเรียบง่าย เช่น ในหน้า 9 เขาบอกว่า “[a] simple change paid for 10 years of my salary.”
    สักวันหนึ่งอยากลองทำ optimization ที่มีผลกระทบมากขนาดนั้นบ้าง

    • ที่ Google ผมเคยทำงานแบบนั้นอย่างน้อยหนึ่งครั้ง Google ใช้ทรัพยากรเครื่องจักรจำนวนมหาศาล และยังมี เครื่องมือด้านประสิทธิภาพ ที่ยอดเยี่ยม จึงเป็นสภาพแวดล้อมที่เอื้อมาก
      https://research.google/pubs/pub36575/
      แต่รอบตัวก็มีคนเก่ง ๆ มากมาย ดังนั้นถ้าคุณเจอโอกาสใหญ่ ก็มักมีเหตุผลว่าทำไมคนอื่นยังไม่กระโดดเข้ามาทำ อาจเป็นเหตุผลทางเทคนิค หรือเหตุผลเชิงองค์กรก็ได้
      ตัวอย่างของกรณีหลังคือ โดยปกติ Google จะไม่ให้รางวัลกับงานประเภทนี้มากนัก เว้นแต่จะอยู่ในช่วงที่มีแรงกดดันด้านทรัพยากร คุณอาจได้โบนัสเพื่อนร่วมงานราว 100 ดอลลาร์จาก optimization บางอย่าง แต่ไม่ใช่คอมมิชชัน 10% การเลื่อนตำแหน่ง หรือสิทธิ์รับเงินเดือนโดยไม่ต้องมาทำงานเป็นเวลา 10 ปีแต่อย่างใด โดยทั่วไปบริษัทมักอยากให้วิศวกรไปทำงานกับ การเติบโตของรายได้ มากกว่าการลดต้นทุน นโยบายนี้ถูกต้องหรือไม่นั้นเป็นเรื่องที่เกินระดับเงินเดือนของผม
    • สิ่งที่ติดใจในทางแก้สุดท้ายคือ เขาเพิ่งปรับแต่ง query หลังจากเพิ่ม instance เป็นค่าที่ใหญ่ที่สุดเท่าที่เป็นไปได้แล้ว
      ตอนนี้เหมือนจะไม่สามารถคลิกไม่กี่ครั้งในคอนโซลจัดการเพื่อเพิ่มค่าการตั้งค่าให้สูงขึ้นได้อีก จึงต้องใช้สมองจริง ๆ เพื่อแก้ปัญหาความจุ หากปรับแต่งบางส่วนของโค้ดให้เร็วกว่านี้ ก็อาจไม่จำเป็นต้องใช้การตั้งค่า instance ที่ใหญ่ขนาดนั้นตั้งแต่แรก