ประสบการณ์รับมือคำขอเว็บ 1 พันล้านครั้งด้วยโค้ดที่แสนน่าเบื่อ
(notes.billmill.org)- แชร์ประสบการณ์การสร้างระบบเปรียบเทียบแผนประกันสุขภาพ Medicare ของสหรัฐฯ ขึ้นใหม่ โดยใช้ เทคโนโลยีที่ผ่านการพิสูจน์แล้วเท่านั้น (เช่น Postgres, golang, React) ในโครงสร้างที่เรียบง่าย เพื่อรองรับ คำขอเว็บมากกว่า 1 พันล้านครั้ง ได้อย่างเสถียร
- ออกแบบสถาปัตยกรรมโดยมี ความเรียบง่ายและเสถียรภาพ เป็นเป้าหมาย จนทำได้ทั้ง ความเร็วตอบสนองเฉลี่ยต่ำกว่า 10ms และ อัตราเหตุขัดข้องที่ต่ำมาก
- ใช้ innovation token อย่างประหยัดเฉพาะกับการแยกโครงสร้างหลักที่จำเป็น (โมดูลขนาดใหญ่ 3 ตัว, การสื่อสารผ่าน gRPC) และเลือกใช้แนวทางที่น่าเบื่อแต่เชื่อถือได้สำหรับทุกอย่างที่เหลือ
- สร้างองค์ประกอบด้านปฏิบัติการทั้งหมดตั้งแต่ การจัดการ DB schema, ETL pipeline, testing, logging, documentation, CLI tool ให้ทำซ้ำได้และเรียบง่าย จนกลายเป็น ระบบที่ทั้งทีมเข้าใจและดูแลรักษาได้ง่าย
- แสดงให้เห็นอย่างชัดเจนว่า การควบคุมคุณภาพอย่างต่อเนื่องและการทำงานเป็นทีมที่แข็งแกร่ง ใช้ได้ผลแม้ในโปรเจกต์ภาครัฐขนาดใหญ่
Serving a billion web requests with boring code
ภาพรวมระดับสูง
- เป็นหัวหน้านักพัฒนาของเว็บไซต์ภาครัฐสหรัฐฯ สำหรับเปรียบเทียบและซื้อแผน Medicare ตลอดระยะเวลา สองปีครึ่ง
- รองรับ API request เฉลี่ยวันละ 5 ล้านครั้ง โดยมีความเร็วตอบสนองเฉลี่ยต่ำกว่า 10ms และ 95% ของคำขอใช้เวลาไม่เกิน 100ms
- อัตราเหตุขัดข้องต่ำมาก จนกรณีที่วิศวกรจริง ๆ ต้องถูกปลุกตอนเช้ามืดมีนับได้ไม่กี่ครั้ง
- ใช้เฉพาะเทคโนโลยีที่ผ่านการพิสูจน์และใคร ๆ ก็เข้าใจได้อย่าง Postgres, golang, React เพื่อสร้างระบบที่เสถียรได้อย่างต่อเนื่อง
Boring über alles
- หลักการสำคัญสูงสุดคือให้ความสำคัญกับ "เทคโนโลยีที่น่าเบื่อแต่ผ่านการพิสูจน์แล้ว" ก่อนเสมอ (Choose Boring Technology)
- ความพยายามเชิงนวัตกรรมจะใช้ innovation token อย่างประหยัดเฉพาะในจุดที่จำเป็นจริง ๆ
- ชอบ เทคโนโลยีและกระบวนการที่เสถียรและชัดเจน มากกว่าวิธีแก้ปัญหาที่ซับซ้อนและหวือหวา
ส่วนที่น่าเบื่อ
- Postgres: แกนหลักของการจัดเก็บข้อมูล ตอบโจทย์ทั้งความน่าเชื่อถือและการขยายระบบ แม้แต่การค้นหาที่ซับซ้อน (เช่น faceted search) ก็แก้ด้วย Postgres
- golang: build และ deploy ได้รวดเร็ว ได้ binary ที่ชัดเจน การจัดการ error ตรงไปตรงมา และสมาชิกใหม่ในทีมก็ปรับตัวได้ง่าย
- React: เป็นเฟรมเวิร์ก SPA ที่ผ่านการพิสูจน์มากที่สุด และสมาชิกทีมก็คุ้นเคยอยู่แล้ว เรื่อง accessibility และการรองรับอุปกรณ์หลากหลายก็เป็นปัจจัยสำคัญเช่นกัน
- ในระยะยาวมีปัญหาเรื่อง bundle size และความเร็วที่ลดลง แต่ในบริบทตอนนั้นถือเป็นตัวเลือกที่ดีที่สุดสำหรับการส่งมอบผลลัพธ์ให้ทันเวลา
innovation tokens
- Modular backend: แบ็กเอนด์ทั้งหมดไม่ได้เป็นทั้ง microservices หรือ monolith แต่จัดเป็น 3 โมดูลขนาดใหญ่ (druginfo, planinfo, beneinfo)
- แต่ละโมดูลใช้ Postgres DB แยกกัน และแชร์ข้อมูลกันผ่าน gRPC เท่านั้น
druginfo: ทำดัชนีข้อมูลราคายาอย่างละเอียดมาก ซึ่งมีจำนวนชุดผสมของร้านยา ประกัน และบรรจุภัณฑ์เพิ่มขึ้นแบบทวีคูณ จึงต้องมีการ preprocess ที่ซับซ้อนและการปรับแต่งประสิทธิภาพplaninfo: รับข้อมูล CMS ชุดใหม่ทุกวัน แล้วสร้าง DB ใหม่ทั้งก้อนเพื่อใช้งาน (รักษาความเป็น immutable)beneinfo: เป็นส่วนเดียวที่เก็บข้อมูลผู้สมัครจริง โดยเก็บ PII ที่อ่อนไหวให้น้อยที่สุด และใส่ใจทั้งด้านการออกแบบและการปฏิบัติการเพื่อลดความเสี่ยงข้อมูลรั่วไหล
- gRPC: มีข้อดีที่สามารถนิยามอินเทอร์เฟซการสื่อสารระหว่างโมดูลอย่างชัดเจนในรูปแบบโค้ด และเชื่อมกับเครื่องมืออัตโนมัติได้ดีมาก
- แต่ก็พบข้อเสียเช่น build, tooling และ debugging ที่ซับซ้อน และความตรงไปตรงมาที่น้อยกว่าเมื่อเทียบกับ JSON API
- ใช้ grpc-gateway เพื่อรองรับเว็บไคลเอนต์และรับทราฟฟิกปริมาณมากได้อย่างไม่มีปัญหา
การคงความเข้ากันได้ย้อนหลังอย่างเข้มงวด
- รักษา backwards compatibility ของทั้ง API และฐานข้อมูล อย่างเคร่งครัด
- ฟิลด์ของ public API จะไม่ถูกลบเด็ดขาด และจะคงไว้ตลอดไปตราบใดที่ไม่มีปัญหาด้านความปลอดภัย
- คอลัมน์ใน DB สามารถเพิ่มได้อิสระ แต่การลบต้องผ่านหลายขั้นตอนในการตรวจสอบ (ลบการอ้างอิง → รอหลายสัปดาห์ → ค่อยลบจริง)
- วินัยนี้กลายเป็นฐานสำคัญที่ทำให้เปลี่ยนแปลงได้เร็ว พร้อมกับ deploy และปฏิบัติการได้อย่างเสถียร
Faceted search
- สร้าง faceted search ด้วย Postgres เพียงอย่างเดียวแทน ElasticSearch
- ใช้ฟังก์ชันยาว 250 บรรทัดเพียงตัวเดียวที่ประกอบเงื่อนไขบน plan table ที่ทำดัชนีไว้อย่างดี เพื่อจัดการ logic การค้นหาทั้งหมด
- โฟกัสที่ความต้องการทางธุรกิจ และแก้ปัญหาอย่างเรียบง่ายโดยไม่เพิ่มความซับซ้อนที่ไม่จำเป็น
Database
-
creation
- จัดการ DB schema ด้วยไฟล์
.sqlที่มีหมายเลขกำกับ และโหลดตามลำดับเพื่อรับประกันความน่าเชื่อถือ - DB ของ
planinfo/beneinfoถูกสร้างใหม่ทุกวัน จึงไม่ต้องทำ migration และหากมีความผิดพลาดในการตั้งค่า เช่น version ไม่ตรงกัน ก็ออกแบบให้แอปไม่เริ่มทำงานเลย
- จัดการ DB schema ด้วยไฟล์
-
ETL
- ใช้ shell script แยกตามแต่ละ data source เพื่อโหลดขึ้น S3 → จากนั้น EC2 instance ที่รันด้วย cron จะดึง ETL code/data ล่าสุดมาสร้าง RDS DB ใหม่
- ใช้ คำสั่ง COPY ของ Postgres อย่างจริงจัง เพื่อจัดการโหลดข้อมูลจำนวนมากได้มีประสิทธิภาพกว่าการใช้ INSERT
- ใช้เวลาเพียง 2~4 ชั่วโมงต่อวันในการสลับข้อมูลระดับหลายร้อยล้านแถวไปยัง DB ใหม่
-
models
- ใช้ไลบรารี xo เพื่อสร้าง DB model อัตโนมัติ และใช้ custom template เพื่อให้ได้โค้ดที่เหมาะกับทีม
-
testing
- ความผิดพลาดที่ใหญ่ที่สุด คือการทำเทสต์ด้วย sqlmock มากเกินไป จนเมื่อข้อมูลเปลี่ยนบ่อย การดูแลรักษากลายเป็นเรื่องยุ่งยากมาก
- ถ้าเป็น DB จริงที่ immutable การทดสอบกับฐานข้อมูลจริงน่าจะมีประสิทธิภาพมากกว่า
-
Local database for development
- มีสคริปต์ที่สร้างข้อมูลบางส่วนของแต่ละตารางแบบอัตโนมัติ ทำให้นักพัฒนาแต่ละคนสามารถทดสอบและพัฒนาบน local DB ขนาดเล็กที่อิงจากข้อมูลจริงได้
- หากเตรียมเครื่องมือนี้ไว้ก่อนที่ DB จะใหญ่เกินไป จะช่วยเพิ่มประสิทธิภาพการพัฒนาของทั้งทีมได้อย่างมาก
Miscellaneous tooling
- พัฒนา CLI tool เป็น shell script เพื่อทำงานอัตโนมัติด้านปฏิบัติการและการสังเกตการณ์หลายรูปแบบ โดยรวม utility ทั้งหมดไว้จัดการในที่เดียว
- พัฒนาและใช้งานเครื่องมือที่ยึดหน้างานจริงอย่างจริงจัง เช่น การแปลง splunk log เป็นกราฟได้ทันทีผ่านคำสั่งใน Slack
Logging
- สร้าง request id ตั้งแต่จุดที่รับคำขอเข้ามา และแนบ id นี้ไว้กับ log context ทั้งหมดเพื่อให้ตามรอยได้จากทุกที่
- ออกแบบระบบ logging ที่ปลอดภัยและเป็นระเบียบด้วย zerolog
- บันทึก log ที่จำเป็นไว้ในทุกช่วงเวลาสำคัญ เช่น จุดเข้า-ออกของระบบ และสถานการณ์ผิดปกติ
Documentation
- แปลงเอกสาร GitHub markdown ด้วย sphinx-book-theme แล้วนำไปใช้งานเป็น wikibook
- สมาชิกทีมทุกคนมีส่วนร่วมกับการทำเอกสารอย่างจริงจัง เพื่อให้สามารถค้นหาเอกสารของระบบทั้งหมดได้จากที่เดียว
- วัฒนธรรมการทำเอกสารที่ยอดเยี่ยมช่วยเพิ่มการเติบโตของทีม การดูแลรักษา และประสิทธิภาพในการ onboarding สมาชิกใหม่อย่างมาก
Runtime integrations
- พยายามอธิบายให้ลูกค้าลดคำขอที่ทำให้ประสิทธิภาพฝั่งไคลเอนต์ลดลง (เช่น การฝัง analytics script) ให้เหลือน้อยที่สุด
- เปลี่ยนการประมวลผล query จาก browser runtime ไปเป็น build-time เพื่อรักษาประสิทธิภาพของบริการ
- ในความเป็นจริงไม่สามารถกันคำขอของลูกค้าได้ทั้งหมด และบางส่วนก็ส่งผลให้ประสิทธิภาพลดลง
And more
- นอกจากเรื่องเทคนิคแล้ว ยังย้ำว่า บรรยากาศทีมที่เป็นบวกและร่วมมือกัน รวมถึงแรงจูงใจที่แข็งแกร่ง คือแรงขับเคลื่อนที่แท้จริงของความสำเร็จของระบบขนาดใหญ่
- นี่เป็นกรณีศึกษาที่ทำให้เห็นพลังของการตัดสินใจเชิงปฏิบัติเล็ก ๆ แต่สำคัญ และการควบคุมคุณภาพอย่างสม่ำเสมอ
18 ความคิดเห็น
เรื่องน่าเบื่อแบบนี้ตั้ง 2.5 ปีเลยเหรอ?!
สงสัยว่า Faceted Search คืออะไรเลยตามไปอ่าน แล้วก็มีเรื่องให้อ่านต่ออีกพอสมควรเลยครับ
https://www.cybertec-postgresql.com/en/faceting-large-result-sets/
https://roaringbitmap.org/about/
https://github.com/cybertec-postgresql/pgfaceting
ความเห็นเกี่ยวกับคำว่า "น่าเบื่อ" น่าสนใจดีนะครับ 555 ถ้าจะเปลี่ยนเป็นคำอื่น คำไหนจะดีกว่ากัน? แบบเดิม ๆ, ธรรมดา ๆ?
การแปล
boringว่า "น่าเบื่อ" ทำให้ความหมายดั้งเดิมหายไปมากเกินไปหน่อยboringnessเป็นหนึ่งในปรัชญาการออกแบบของ Go ครับทำเป็นน่าเบื่อสุด ๆ…
เกาหลีนี่เป็นประเทศแห่ง Java แบบเปิดเรื่อง-กลางเรื่อง-จบเรื่องด้วย Java เลย เลยรู้สึกแปลกกันนิดหน่อย 555
ผมคิดว่า golang กับ React ต่างก็เป็นภาษาเขียนโค้ดเชิงองค์กรแบบ "boring" ของยุคใหม่ทั้งหมด
คำว่า boring -> แปลเป็น "น่าเบื่อ" ก็ไม่ได้ตรงความหมาย 100% เลย ทำให้ดูเหมือนว่านัยของคำนี้จะสื่อไปถึงผู้อ่านชาวเกาหลีได้ไม่ครบถ้วน
อยากอยู่ในโลกที่น่าเบื่อของ Postgres, golang และ React
จริงครับ พอเห็นชื่อเรื่องแล้วก็คิดว่าเป็นมุกอะไรสักอย่าง
ดูเหมือนว่าในต่างประเทศจะมองว่านั่นเป็นสแตกที่น่าเบื่อ
จริง ๆ แล้ว Go ก็เป็นตัวเลือกที่ง่ายที่สุดสำหรับการทำเว็บเซิร์ฟเวอร์อยู่แล้ว..
เหมือนว่าต้องพัฒนาด้วยภาษาอย่าง Rust หรือสาย FP ถึงจะไม่ถูกมองว่าน่าเบื่อ
เรื่องที่ธรรมดามาก ๆ .. ธรรมดาเสียจนมองข้ามประเด็นสำคัญที่กำลังพลาดไป..
สแต็กระดับนี้ดูเหมือนจะไม่ได้ชวนเบื่อขนาดนั้นนะครับ ถ้าจะให้น่าเบื่อจริง ๆ อย่างน้อยก็น่าจะต้องเป็น Java เวอร์ชันต่ำกว่า 1.8 หรือไม่ก็ VB ประมาณนั้น... เป็นความคิดที่ค่อนข้างลบหลู่แวบเข้ามาในหัวครับ
ในต้นฉบับมีลิงก์ที่เกี่ยวกับ boring อยู่ แต่เมื่อดูเนื้อหาแล้ว ดูเหมือนว่า boring ในที่นี้จะหมายถึง คุ้นเคยมากเกินไป
มีคำที่เหมาะสมกว่านี้อย่าง
experienced,verified,skillfulอยู่แล้ว แต่กลับจงใจใช้คำว่าboringดูเหมือนมีเจตนาเพื่อเรียกกระแสมันอาจไม่ได้หมายถึงว่าน่าเบื่อตอนเขียน แต่อาจจะหมายถึงว่าเป็นสแต็กแบบเดิมๆ ที่ใช้บ่อยมากจนเริ่มเบื่อหรือเปล่าครับ
เคอร์เนลลินุกซ์ประมาณ 2.6.29...
แค่บอกว่าใช้ gRPC ก็แบบ.. 555
ผมก็เริ่มจากคิดเหมือนกันว่า golang น่าเบื่อเหรอ?
ถ้าเป็นระดับ classic asp ก็คงพอจะเรียกว่าน่าเบื่อได้อยู่หรอก