1 คะแนน โดย GN⁺ 2024-07-28 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • One Million Checkboxes ซึ่งเปิดตัวเมื่อวันที่ 26 มิถุนายน 2024 เป็นเว็บไซต์ที่ทุกคนควบคุมเช็กบ็อกซ์ 1 ล้านช่องชุดเดียวกันแบบเรียลไทม์ และก่อนปิดตัวลง 2 สัปดาห์ให้หลัง ได้ประมวลผลการติ๊กไปมากกว่า 650 ล้านครั้ง
  • สถานะจริง ๆ มีขนาดเพียง 1 ล้านบิต หรือ 125KB แต่สถาปัตยกรรมเริ่มต้นที่สร้างด้วย nginx, Flask/gunicorn และ Redis pubsub ไปถึงขีดจำกัดอย่างรวดเร็วเมื่อเจอทราฟฟิกที่เกินคาด
  • เมื่อผู้คนนับหมื่นหลั่งไหลเข้ามาจาก Hacker News, Reddit, Mastodon และ Twitter ปัญหาต่าง ๆ ก็เผยออกมาต่อเนื่อง ทั้ง Redis connection หมด แบนด์วิดท์พุ่งสูง การขาด input validation และการนำอัปเดตเก่ามาใช้
  • การรับมือเน้นมาตรการที่ทำได้เร็ว เช่น ขยายเซิร์ฟเวอร์และ Redis, ประมวลผลอัปเดตเป็น batch, ลดขนาดรูปแบบข้อมูลที่ส่ง, ตั้ง เพดานแบนด์วิดท์ 250Mbit/s ด้วย Linux tc และสคริปต์รีสตาร์ตโปรเซส
  • หลังจากนั้นย้ายแบ็กเอนด์ไปเป็น Go จนเสถียรขึ้น และช่วงท้ายใช้ Redis Lua script จัดการตรรกะการ freeze เช็กบ็อกซ์แบบ atomic ก่อนที่เว็บไซต์จะจบลงเมื่อวันที่ 11 กรกฎาคม 2024 เวลา 4:35 PM ตามเวลาฝั่งตะวันออกของสหรัฐฯ

เว็บไซต์และการออกแบบช่วงแรก

  • One Million Checkboxes(OMCB) เป็นเว็บไซต์ที่เปิดตัวเมื่อวันที่ 26 มิถุนายน 2024 โดยมีเช็กบ็อกซ์แบบ global จำนวน 1 ล้านช่อง
    • เมื่อผู้ใช้คนหนึ่งเปิดหรือปิดเช็กบ็อกซ์ การเปลี่ยนแปลงจะแสดงบนหน้าจอของผู้ใช้ทุกคนทันที
    • ใช้เวลาสร้าง 2 วัน และคาดว่าจะมีผู้ใช้มากสุดเพียงระดับไม่กี่ร้อยคน
  • กระแสตอบรับจริงใหญ่กว่าที่คาดไว้มาก
    • ภายในไม่กี่ชั่วโมงหลังเปิดตัว มีผู้คนนับหมื่นเข้ามาและปรับเช็กบ็อกซ์ไปหลายล้านช่อง
    • ทราฟฟิกมาจาก Hacker News, /r/InternetIsBeautiful, Mastodon, Twitter
    • ไม่กี่วันต่อมาก็ถูกนำเสนอใน Washington Post และ New York Times
  • log ช่วงต้นของวันแรกบางส่วนไม่เหลืออยู่
    • เพราะตอนแรกเก็บไว้เฉพาะ log ล่าสุด 1 ล้านรายการของแต่ละวัน
    • เริ่มเสถียรขึ้นตั้งแต่วันที่สอง และวันนั้นมีการติ๊กมากกว่า 50 ล้านครั้ง
    • ก่อนเว็บไซต์ปิด ยอดติ๊กสะสมเกิน 650 ล้านครั้ง

สถาปัตยกรรมเดิมที่มี Redis เป็นศูนย์กลาง

  • สถานะของเช็กบ็อกซ์ถูกแทนด้วย 1 ล้านบิต
    • กล่องที่ถูกติ๊กเป็น 1 กล่องที่ไม่ถูกติ๊กเป็น 0
    • ขนาดสถานะทั้งหมดคือ 125KB
    • client เก็บ bitset และอ้างอิงตอน render
  • client ถูกออกแบบมาเพื่อหลีกเลี่ยงภาระบน DOM
    • ไม่ใส่องค์ประกอบทั้ง 1 ล้านรายการลงใน DOM ทั้งหมด
    • ใช้ react-window เพื่อ render เฉพาะเช็กบ็อกซ์ที่เห็นบนหน้าจอปัจจุบันกับ buffer ขนาดเล็กเท่านั้น
  • โครงสร้างเซิร์ฟเวอร์เป็นแบบที่คำนึงถึงการ scale แนวนอนอย่างง่าย
    • nginx ให้บริการ static content และส่งต่อ API request กับ websocket connection ไปยัง Flask server
    • Flask server เป็นสอง instance ที่รันด้วย gunicorn
    • Redis ทำหน้าที่เก็บสถานะเช็กบ็อกซ์และเป็น message queue
  • วิธีใช้ Redis ก็ตรงไปตรงมา
    • เปลี่ยนสถานะเช็กบ็อกซ์แต่ละช่องด้วย primitive สำหรับจัดการ bit ของ Redis
    • เมื่อ client ส่งเหตุการณ์การติ๊ก Flask จะ flip bit ใน Redis และบันทึก event ลง pubsub
    • Flask server ทั้งสองตัวอ่าน pubsub แล้วแจ้งการเปลี่ยนแปลงให้ client ที่เชื่อมต่อกับตัวเอง
  • snapshot ของสถานะทั้งหมดเป็นกลไกสำหรับชดเชยอัปเดตที่ตกหล่น
    • ใช้เพื่อทำให้ client ที่พลาดอัปเดตเพราะแท็บอยู่เบื้องหลังกลับมาตรงกัน
    • implementation แรกส่งสถานะทั้งหมดทุก 30 วินาที

หลักการ scale

  • ต้องสามารถคำนวณเพดานค่าใช้จ่ายได้
    • หลีกเลี่ยงวิธี auto scale แบบไม่จำกัดที่ทำให้ค่าใช้จ่ายพุ่ง
    • เลือกยอมให้ระบบพังเมื่อมีโหลดเกินคาด
  • สมมติว่าความนิยมจะอยู่ไม่นาน
    • ให้ความสำคัญกับการรับมือที่ทำได้ในไม่กี่ชั่วโมง มากกว่าโซลูชันที่สมบูรณ์ซึ่งใช้เวลาหลายวันหรือหลายสัปดาห์
    • ยอมรับ technical debt ที่เกิดขึ้นระหว่างนั้น
  • การเลือกเทคโนโลยีเน้นความเรียบง่ายและดูแลเองได้โดยตรง
    • เลือกโครงสร้างที่สามารถ SSH เข้าเซิร์ฟเวอร์ไปรันคำสั่งและ debug ได้
    • ใช้ dependency ที่ดูแลและ debug เองได้เป็นหลัก
  • ประสบการณ์หลักของเว็บไซต์คือ การ sync แบบ global
    • ไม่ว่าจะเลื่อนไปที่ไหน ต้องเห็นการเปลี่ยนแปลงทันที
    • ไม่ scale ด้วยวิธีส่งเฉพาะเช็กบ็อกซ์ที่ผู้ใช้กำลังดูอยู่

วันแรก: เพิ่มเซิร์ฟเวอร์และคอขวดที่ Redis

  • ภายใน 30 นาทีหลังเปิดตัว โหลดพุ่งสูงขึ้น และแม้เว็บไซต์ยังทำงานอยู่ แต่ก็อยู่ในสภาพที่ยืนระยะได้ยาก
    • วิธีปรับปรุงที่ชัดเจนที่สุดคือเพิ่มเซิร์ฟเวอร์
    • nginx สามารถ reverse proxy ไปยัง Flask instance บน VM อื่นได้ง่าย และสถานะก็อยู่ใน Redis อยู่แล้ว
  • เซิร์ฟเวอร์ตัวที่สองถูกเพิ่มราว 12:30 PM และโหลดก็แตะ 100% ทันที
    • ตอนแรกคาดว่าเพิ่มเซิร์ฟเวอร์หนึ่งหรือสองเครื่องก็น่าจะพอ
    • แต่ในความเป็นจริง ทราฟฟิกเพิ่มขึ้นตามการขยายระบบ
    • ขึ้นอันดับ 1 บน Hacker News และกิจกรรมบน Twitter ก็พุ่งขึ้นมาก
  • connection ระหว่าง Flask server กับ Redis กลายเป็นคอขวด
    • ไม่มี Redis connection pool และ Redis เข้าใกล้ภาวะ connection ไม่พอ
    • จึงเปลี่ยนไปส่งอัปเดตแบบรวมเป็น batch
    • ไม่ได้คำนึงถึงความเข้ากันได้กับ client เดิม โดยมองว่าผู้ใช้จะ refresh หน้าเอง
  • มีการเพิ่ม Redis connection pool ด้วย แต่ในชุด gunicorn กับ Flask มันทำงานได้ไม่เรียบร้อยนัก
    • ถึงอย่างนั้นก็ดูเหมือนช่วยลดจำนวน connection ไปยัง Redis ได้
    • หลังจากนั้นไม่ได้ขุดปัญหานี้ต่อเชิงลึก และย้ายไปทำ Go แทน
  • rate limit สำหรับการสร้าง session ถูกถอดออก
    • สถานะของ rate limit เก็บอยู่ใน Redis และ Redis กำลังจะหมด connection
    • ปัญหาไม่ได้อยู่ที่ session ใหม่พุ่งจำนวนมาก แต่อยู่ที่การส่งข้อมูลจำนวนมากจาก session เดียว
    • มองว่าเป็นมาตรการที่เสี่ยงในระยะสั้นแต่ยอมรับได้
  • Redis instance ก็ถูกอัปเกรดเป็นสเปกที่ใหญ่ขึ้น
    • ใช้ Digital Ocean managed Redis อยู่
    • ขยายจาก instance ขนาดเล็ก 1 shared CPU, 2GB RAM เป็น 4 dedicated CPU, 32GB RAM
    • การ resize ใช้เวลาประมาณ 30 นาที

ปัญหาแบนด์วิดท์และการลดปริมาณข้อมูลที่ส่ง

  • ช่วงแรกไม่ได้คำนึงถึงค่าแบนด์วิดท์มากพอ
    • Digital Ocean คิดเงิน $0.01 ต่อ GB เมื่อเกินแบนด์วิดท์ฟรี
    • จากงานก่อนหน้ามีแบนด์วิดท์ฟรี 1TB และคิดว่า OMCB ไม่น่าจะมีผลมาก
  • snapshot ของสถานะทั้งหมดอาจกินแบนด์วิดท์อย่างรวดเร็ว
    • 1 ล้านบิตเท่ากับ 1Mbit
    • ถ้าส่งให้ 1,000 คนทุก 30 วินาที จะอยู่ที่ประมาณ 2GB ต่อนาที หรือ 120GB ต่อชั่วโมง
    • ตัวเลขนี้ยังไม่รวม incremental update
  • การตรวจสอบแบนด์วิดท์และตั้งเพดานค่าใช้จ่ายทำบนกล่อง nginx
    • ใช้ ip -s link show dev eth0 ตรวจสอบจำนวน byte ที่ส่ง
    • เพราะใช้ nginx reverse proxy ตัวเดียว จึงอนุมานแหล่งที่มาของแบนด์วิดท์ได้ง่าย
  • การลดปริมาณข้อมูลที่ส่งดำเนินไปสองทาง
    • ลดความถี่ของ snapshot สถานะทั้งหมด
    • ลดขนาดรูปแบบข้อมูลของ incremental update
  • รูปแบบ batch update ถูกบีบให้สั้นลงมาก
    • รูปแบบเดิมเป็นรายการ dict เช่น { "index": 123, "value": true }
    • รูปแบบสุดท้ายเป็นคู่ของ array index ที่เป็น true และ array index ที่เป็น false ในรูป [[123, 125], [124]]
    • วิธีนี้สั้นกว่า implementation เดิม 5 เท่า
  • ใช้ Linux tc ตั้ง hard cap เพื่อป้องกันค่าใช้จ่ายพุ่ง
    • จำกัดทราฟฟิกของ public interface eth0 ไว้ที่ 250Mbit/s
    • เทียบได้กับราว 2GB ต่อนาที หรือเกือบ 3TB ต่อวัน
    • ที่ราคา $0.01 ต่อ GB จึงหลีกเลี่ยงสถานการณ์ที่ค่าใช้จ่ายพุ่งจนควบคุมไม่ได้ข้ามคืน

วันที่สอง: input validation ที่ตกหล่นและ Redis replica

  • เช้าวันถัดมาเว็บไซต์ล่ม และสาเหตุคือ ขาด input validation
    • ไม่ได้กันเช็กบ็อกซ์ที่มี index เกิน 1 ล้าน
    • มีคนไปปรับเช็กบ็อกซ์ที่ index ระดับหลายร้อยล้าน
    • ทำให้ดูเหมือนจำนวนกล่องที่ถูกติ๊กถึง 1 ล้านและระบบตัดสินว่าเว็บไซต์จบแล้ว
  • ข้อมูล Redis ก็ใหญ่ขึ้นโดยไม่จำเป็น
    • มี 0 หลายล้านตัวถูกเพิ่มระหว่าง bit ที่ 1 ล้านกับ bit ที่ 100 ล้าน
    • ข้อมูลที่ส่งไป client ใหญ่ขึ้น 100 เท่า
  • การกู้คืนทำได้รวดเร็ว
    • หยุด nginx
    • คัดลอกเฉพาะ 1 ล้านบิตแรกของ bitset เดิมไปเป็น bitset ใหม่
    • เก็บ bitset เดิมไว้สำหรับ debug
    • เปลี่ยนโค้ดให้ชี้ไปที่ bitset ใหม่และเพิ่ม input validation
  • การโหลดหน้าแรกก็ช้าลงด้วย
    • Redis มีโหลดสูง และ bug ใน connection pool ก็ทำให้สร้าง connection มากเกินไป
    • แทนที่จะ debug ปัญหา connection pool จึงเพิ่ม Redis replica เพื่อกระจายโหลดและ connection ของ primary
  • ต้องหา private IP ของ replica ด้วยตนเอง
    • ตามคู่มือของ Digital Ocean prefix replica- ใช้ได้กับ public DNS แต่ใช้ไม่ได้กับ private DNS
    • มองว่าการใช้ public IP มีความเสี่ยงที่จะวิ่งผ่าน public internet และโดนคิดค่าแบนด์วิดท์
    • ลองเชื่อมต่อไปยัง address ที่อยู่ใกล้ private IP ของ primary และเซิร์ฟเวอร์อื่น ๆ จนเจอ private IP ของ replica ในครั้งที่สามหรือสี่
    • จากนั้น hardcode IP ดังกล่าวไว้

การรีสตาร์ตโปรเซสและการแก้ stale update

  • Flask process crash ต่อเนื่อง และสาเหตุดูเหมือนเป็น Redis connection ไม่พอ
    • แทนที่จะ debug แบบละเอียด จึงสร้าง bash script สำหรับตรวจจำนวน Flask process ที่กำลังรัน
    • หาก process ที่รันอยู่มีน้อยกว่า 3 ตัว ให้รีสตาร์ต systemd unit
    • ใส่สคริปต์ไว้ใน crontab
  • ปรับ nginx config ไปพร้อมกันด้วย
    • เปลี่ยนให้เซิร์ฟเวอร์ที่ down ถูกถอดออกจาก rotation ชั่วคราว
    • หลังเปลี่ยนนี้เว็บไซต์ก็เสถียรขึ้น
  • การ sync สถานะของ client มี bug แบบ stale update
    • client ได้รับทั้ง incremental update และ full-state snapshot
    • เพราะทั้งสองแบบไม่มี timestamp จึงอาจนำ incremental update เก่ามาใช้หลังได้รับ snapshot ใหม่แล้ว
    • ผลคือผู้ใช้อาจเห็นสถานะที่ผิดโดยสิ้นเชิงจนกว่าจะถึง full-state snapshot รอบถัดไป
  • มีการเพิ่มการบรรเทาด้วย timestamp
    • ใส่ timestamp ให้ full-state snapshot
    • ใส่ timestamp ให้แต่ละ update ที่บันทึกลง Redis pubsub
    • batch ที่ส่งให้ client ใส่ timestamp สูงสุดในบรรดา incremental update ที่รวมอยู่
    • client ถูกเปลี่ยนให้ทิ้ง batch ที่เก่ากว่า full-state snapshot ล่าสุด
  • วิธีแก้นี้ยังไม่สมบูรณ์
    • หากใน batch มี update ใหม่แม้เพียงหนึ่งรายการ ส่วนใหญ่ของ batch อาจเป็น update เก่าแต่ก็ยังถูกนำไปใช้ได้
    • ถึงอย่างนั้นก็ดีขึ้นมากจากเดิม

เขียนใหม่ด้วย Go และทำให้เสถียร

  • เช้าวันถัดมาเว็บไซต์ยังอยู่ และหลังจากนั้นก็โฟกัสที่การเขียนแบ็กเอนด์ใหม่
    • ตอนนั้นมีอีเมลจาก Washington Post มาถึงแล้วด้วย
    • พร้อมกันนั้นก็คิดแผนว่าจะปิดเว็บไซต์อย่างไร
  • แผนปิดคือให้กล่องที่ถูกติ๊ก freeze หากไม่ถูกยกเลิกการติ๊กอย่างรวดเร็ว
    • การเปลี่ยนแปลงนี้อาจทำให้กิจกรรมพุ่งและต้องทำงานกับเซิร์ฟเวอร์เพิ่ม
    • ไม่มั่นใจว่าโครงสร้างเดิมบน Flask จะรับไหวหรือไม่
  • เขียนแบ็กเอนด์ใหม่เป็น Go ร่วมกับเพื่อนชื่อ Eliot
    • ตั้งแต่วันอาทิตย์ 2 PM ถึง 2 AM ช่วยกันคุย implementation และ port แบ็กเอนด์ทั้งหมด
    • ย้ายโดยไม่ได้เปลี่ยนโครงสร้างมากนัก
    • จุดที่ติดขัดรวมถึงการหา Go socketio library ที่รองรับ protocol เวอร์ชันล่าสุด
  • ประสิทธิภาพดีขึ้นอย่างมาก
    • scale ได้ดีเกินไปจน bot สามารถอัดทราฟฟิกเข้ามามากเกินไปได้
    • จึงต้องมี rate limit ที่ดีกว่าเดิม
  • คืนวันอาทิตย์มี DDoS ด้วย
    • รับมือโดยวางเว็บไซต์ไว้หลัง Cloudflare และปรับ nginx config เล็กน้อย

ตรรกะการปิดเว็บไซต์

  • หลังเขียนใหม่ด้วย Go เว็บไซต์ทำงานได้เสถียร
    • จากนั้นใช้เวลาหนึ่งสัปดาห์รับมือกับการสัมภาษณ์และความสนใจที่เข้ามา
    • แล้วจึงเริ่มงานปิดเว็บไซต์
  • วิธีปิดคือการ freeze เช็กบ็อกซ์
    • หากกล่องที่ถูกติ๊กไม่ถูกยกเลิกการติ๊กอย่างรวดเร็ว จะเข้าสู่สถานะ frozen
    • เมื่อเวลาผ่านไป เว็บไซต์ทั้งหมดจะเข้าสู่สถานะ frozen โดยสมบูรณ์
  • เพิ่มสถานะเข้าไปใน Redis
    • เพิ่ม hashtable สำหรับเก็บเวลาล่าสุดที่เช็กบ็อกซ์แต่ละช่องถูกติ๊ก
    • เป็นสถานะที่ใหญ่เกินกว่าจะส่งให้ client แต่เก็บใน Redis ได้ไม่มีปัญหา
    • เก็บค่า time_to_freeze ด้วย
  • ตัดสินสถานะ freeze ตอน uncheck
    • ถ้า now - last_checked > time_to_freeze จะไม่ uncheck
    • แต่จะอัปเดต frozen_bitset เพื่อระบุว่าเช็กบ็อกซ์นั้นอยู่ในสถานะ frozen
    • frozen_bitset ถูกกระจายไปยัง client ด้วยวิธีเดียวกับสถานะ checked
    • client จะ disable เช็กบ็อกซ์ที่เปิด frozen bit อยู่
  • เพิ่มงานแยกเพื่อให้ freeze ได้แม้ไม่มีใคร uncheck
    • ค้นหา bit ที่ควรถูก freeze เป็นระยะ ๆ แต่ยังไม่ได้ถูกแสดงว่า frozen แล้วทำให้เป็นสถานะ frozen
    • ใส่ตรรกะที่เกี่ยวข้องไว้ใน Redis Lua script เพื่อให้รันแบบ atomic
    • ทำให้หลีกเลี่ยง race condition ได้ง่าย
  • การเปลี่ยนแปลงเพื่อปิดถูกนำไปใช้หลังเปิดตัว 2 สัปดาห์กับอีก 1 วัน
    • วันที่ 11 กรกฎาคม 2024 เวลา 4:35 PM ตามเวลาฝั่งตะวันออกของสหรัฐฯ box 491915 ถูกติ๊ก และเว็บไซต์ก็จบลง

ค่าใช้จ่ายและบทเรียนที่ได้

  • ค่าใช้จ่ายในการรันเว็บไซต์อยู่ที่ประมาณ $850
    • donation ใกล้เคียงกับค่าใช้จ่ายนี้พอสมควร
    • สรุปได้ว่าไม่ได้ขาดทุนมาก
  • พอใจกับการเลือก Redis และ nginx
    • มองว่า Redis และ nginx เป็นเทคโนโลยีที่มีประโยชน์มาก
    • การดูแลเองทำให้ debug และแก้ไขได้ง่าย
    • แต่การควบคุม managed Redis instance ได้ไม่เต็มที่ก็ไม่สะดวกอยู่บ้าง
  • การไม่ออกแบบการ scale ขนาดใหญ่ยาว ๆ ตั้งแต่แรกถือว่าเป็นการตัดสินใจที่ดี
    • มองว่ายากที่จะคาดเดาว่าอะไรจะไปได้ดีบนอินเทอร์เน็ต
    • ถ้าใช้เวลาหลายสัปดาห์คิดเรื่อง scale ตั้งแต่แรก อาจไม่ได้เปิดตัวเลย
    • การมีผู้ใช้จำนวนมากเข้ามาช่วยเป็นแรงจูงใจในการดูแลระบบและช่วยจัดลำดับความสำคัญ
  • ยังยืนยันว่ามีความต้องการต่อปฏิสัมพันธ์แบบไม่ระบุตัวตนที่มีขอบเขตจำกัด
    • ผู้คนสนใจเว็บไซต์ที่ให้โต้ตอบกับคนแปลกหน้าในรูปแบบที่จำกัด
    • ทำให้มั่นใจมากขึ้นว่าจะสร้างเว็บไซต์ประเภทนี้ต่อไป

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

 
GN⁺ 2024-07-28
ความคิดเห็นใน Hacker News
  • เป็นบทความที่มีอะไรให้เรียนรู้มากมาย พร้อมความรู้เชิงประวัติศาสตร์เกี่ยวกับระบบแบบกระจาย
    ถ้าไม่นับพื้นที่จัดเก็บ ดูเหมือนว่าจะเจอ จุดหยุดชะงักและจุดล้มเหลว แทบทุกประเภท และดีที่ได้เห็นกระบวนการแก้ปัญหา
    ไม่รู้มาก่อนว่า Redis รองรับ Lua พอเห็นแบบนี้ก็อยากลองใช้เป็นที่เก็บสถานะทางเลือกดู
    แบนด์วิดท์เป็นหนึ่งในเรื่องที่ไม่พอใจที่สุดของบริการคลาวด์ เพราะไม่มี ขีดจำกัดแบบตายตัว ที่ช่วยกันไม่ให้ค่าใช้จ่ายเกินได้

    • พื้นที่จัดเก็บก็เจอมาเหมือนกัน แต่เป็นแบบน่าเบื่อ ๆ การตั้งค่า logrotate ไม่ถูกต้องจนดิสก์เกือบเต็ม และตอนส่ง log ของ box-check ไปที่ Redis ก็ต้องทำกลไกย้าย log เก่าลงดิสก์เพื่อไม่ให้ Redis ระเบิด
      แต่ทั้งสองอย่างก็ไม่ใช่ปัญหาใหญ่ และโปรเจกต์ที่พื้นที่จัดเก็บไม่ใช่ปัญหาสำคัญนี่ค่อนข้างน่าทึ่ง สำหรับผมถือเป็นประสบการณ์ใหม่
      แบนด์วิดท์นี่น่าปวดหัวจริง ๆ ประมาณสองวันต้องคอยตึงเครียด ดูจำนวนไบต์ขาออกของ NIC แล้วคำนวณใหม่อยู่เรื่อย ๆ และการไม่มี hard cap ก็น่ากลัวมาก ถึงแม้ Digital Ocean จะมีราคาค่อนข้างสมเหตุสมผลแล้วก็ตาม
      ผมยังไม่เคยใช้บริการ serverless ยอดนิยม แต่เข้าใจว่าที่นั่นค่าบริการแบนด์วิดท์ค่อนข้างหนัก
      และ Lua ใน Redis นั้นทรงพลังจริง ๆ ถ้ายอมแลกกับประสิทธิภาพที่ลดลงเล็กน้อย ก็ช่วยข้ามปัญหายาก ๆ ที่มี race condition เยอะได้มาก และทำงานด้วยแล้วสนุก
  • เป็นบทความที่ยอดเยี่ยม และเว็บไซต์ก็น่ายินดีด้วย
    แต่โดยส่วนตัว ผมมองว่า บทความที่เขียนนี้ เป็นส่วนที่น่าภูมิใจที่สุด

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

    • จะถูกต้องก็ต่อเมื่อรับเอาแนวคิดว่า “ดังนั้นจงทำให้ระบบ เรียบง่ายและเป็นพื้นฐานไว้ก่อน” ไปด้วย
      ผมเคยเห็นหลายระบบที่ microservice กลายเป็น “ตัวเลือกที่ชัดเจน” ไม่ใช่เพราะต้องการสเกลหรือแยกทีม แต่เพราะนักพัฒนาแค่อยากทำแบบนั้น
      การสเกลระบบแบบนั้นเป็นงานทรมานจริง ๆ
  • บทความที่เกี่ยวข้องเมื่อไม่นานมานี้: One Million Checkboxes - https://news.ycombinator.com/item?id=40800869 - มิถุนายน 2024, 305 ความคิดเห็น

  • โปรเจกต์แบบนี้สนุกดี
    ประมาณ 6 ปีก่อนผมปล่อย Pixmap บน Android ซึ่งเป็น แอปแก้ไขพิกเซลแบบร่วมมือกัน ขนาดเล็กที่รองรับกริดใหญ่ขึ้นอย่าง 1024x1024
    ผมทำคิวสำหรับนำแต่ละ event ไปใช้กับภาพ PNG และเมื่อ client เชื่อมต่อก็โหลด PNG เริ่มต้น จากนั้น event การวาดแต่ละพิกเซลจะรับเพียง object เล็ก ๆ หนึ่งตัว
    แบบนี้ทำให้การโหลดครั้งแรกใช้ประโยชน์จากการบีบอัดภาพได้ และชุดการเปลี่ยนแปลงหลังจากนั้นก็มีขนาดเล็กมาก อีกทั้ง event ทั้งหมดถูกเก็บไว้ใน log จึงสามารถ “กรอภาพย้อนกลับ” ได้ด้วย [0]
    [0] 22mb: https://blog.winricklabs.com/images/pixmap-rewind-demo.gif

    • เจ๋งดี ผมเคยดูไอเดียคล้าย ๆ กันในการส่ง อัปเดตระดับพิกเซล ไปยังเว็บ client หลายตัว แต่ถ้าทำตามวิธีที่ผมคิดไว้ น่าจะใช้แบนด์วิดท์และพื้นที่จัดเก็บมากเกินไป
      ตอนนี้เลยกำลังลองเล่นกับ canvas ที่ระบุพิกัดผ่านการเรียก API ได้
      https://x.com/RussTheMagic/status/1816749136487588311
  • บทความดีมาก อยากรู้ว่าสุดท้ายแล้ว มีค่าใช้จ่ายเท่าไร

    • ควรใส่เรื่องนี้ไว้ด้วย
      ค่าใช้จ่ายรวมประมาณ 850 ดอลลาร์ และแทบจะพอดีกับเงินบริจาค
      หลังย้ายไป Go แล้ว ผมพลาดที่ไม่ได้ลด infra ลงให้เหมาะสม และจริง ๆ แล้ว replica Redis ตัวที่สองที่เพิ่มเข้ามาก็น่าจะเอาออกได้ ถ้าโฟกัสเรื่องค่าใช้จ่าย คิดว่าน่าจะลดลงครึ่งหนึ่งได้
      แต่เงินบริจาคแทบจะครอบคลุมค่าใช้จ่ายอยู่แล้ว และมีเรื่องอื่นให้ทำมากเกินไป เลยไม่ได้โฟกัสกับมันมากนัก
      หลังปิดเว็บไซต์แล้ว ผมยังคง infra ไว้อีกพักเพื่อเตรียมกราฟและอื่น ๆ เลยมีค่าใช้จ่ายเพิ่มอีกเล็กน้อย ตอนนี้ขาดทุนอยู่นิดหน่อยแต่ไม่มาก
  • ในฐานะคนที่กำลังเรียนแบ็กเอนด์ใหม่ ๆ ผมสงสัยว่ามีสถาปัตยกรรมทางเลือกที่ง่ายกว่าสำหรับโปรเจกต์นี้ไหม
    ถ้ามีวิธีที่ง่ายกว่านี้ในการโฮสต์ สถานะบิต 1 ล้านบิต และซิงก์กับ client ก็คงดี วิธีแก้บางส่วนในบทความเข้าใจยาก
    โปรเจกต์ของผู้เขียนยอดเยี่ยมมาก

    • ขอโทษถ้าบางส่วนของบทความยากไป
      ผมอยากอธิบายเทคโนโลยีที่ใช้ให้ยาวกว่านี้ แต่บทความก็ยาวมากอยู่แล้ว เลยรู้สึกว่าใส่เพิ่มได้ยาก
      ถ้ามีคำถาม ผมยินดีตอบ
      เอาจริง ๆ ผมไม่ค่อยแน่ใจว่าจะทำให้สถาปัตยกรรมง่ายกว่านี้มากได้อย่างไร น่าจะมีบริการที่ใช้กับงานแบบนี้ได้ แต่ผมมองว่านั่นใกล้เคียงกับการโยนความซับซ้อนไปให้คนอื่นมากกว่า
      สุดท้ายสิ่งที่ต้องมีก็คือฐานข้อมูลสำหรับติดตามกล่องที่ถูกเช็ก วิธีใส่ข้อมูลลงฐานข้อมูล วิธีแจ้งสถานะปัจจุบันให้ client รู้ วิธีให้ client แจ้ง server และอัปเดตสถานะเมื่อเช็กกล่อง วิธีแจ้ง client เมื่อกล่องถูกเช็กหรือยกเลิก และวิธีไม่ render องค์ประกอบ DOM 1 ล้านชิ้นตลอดเวลา
      ในที่นี้ผมใช้ Redis เก็บสถานะการเช็ก และเพื่อความเรียบง่ายก็เก็บ 1 ล้านบิตตรง ๆ แล้วส่งบิตทั้ง 1 ล้านบิตให้ client ข้อมูลไม่ได้ใหญ่มากจึงดี
      ใช้ Flask กับ WebSocket จัดการ event การเช็กและ update ส่งทั้งการ update กล่องเดี่ยวและการ update กล่องทั้ง 1 ล้านกล่อง และใช้ react-window เพื่อหลีกเลี่ยงปัญหา rendering
      ส่วน nginx static content กับ reverse proxy ที่เหลือ ส่วนใหญ่เป็นกลไกเพื่อให้สเกลง่ายขึ้น ดังนั้นแม้ไม่มีรายละเอียดเหล่านั้นก็ implement ได้และเว็บก็ทำงานได้ เพียงแต่จะรับโหลดเท่าเดิมไม่ได้
    • คุณอาจยัดทุกอย่างในบทความเข้าไปใน โปรเซสเดียว ก็ได้
      แทนที่จะใช้ฐานข้อมูล ก็เก็บชุดบิตไว้ในไฟล์แล้ว mmap เอา แทนที่จะใช้ reverse proxy ก็ให้แอปพลิเคชันจัดการ HTTP request และการเชื่อมต่อ WebSocket โดยตรงได้
    • เอาจริง ๆ ระดับนี้แทบจะเป็นแบบที่ง่ายที่สุดแล้ว
      มันคือเว็บเซิร์ฟเวอร์ไม่กี่ตัวที่มี cache กับคิว publish/subscribe อยู่ด้านหลัง
      อาจทำทั้งหมด ในหน่วยความจำ บนโฮสต์ใหญ่เครื่องเดียวได้ แต่ถ้ารองรับ demand ไม่ทันหรือล้มเหลวด้วยเหตุผลใดก็ตาม ก็จะตันสนิท
    • เอาจริง ๆ ผมคิดว่ายากที่จะทำให้เรียบง่ายกว่านี้มาก
      ยกเว้นวิธีที่สเกลไม่ได้อย่างการมี global list ของ boolean 1 ล้านตัว อยู่ในโปรเซสเดียวกับ backend API
    • ใช้ quadtree เพื่อสรุปทั้งบล็อกของ checkbox ที่มีสถานะเดียวกันเป็น tuple (checked, start_x, start_y, end_x, end_y) ก็พอแล้ว ไม่ใช่วิธีที่ชัดเจนมากเหรอ
  • เจ๋งดี
    สงสัยว่าบทความถัดไปจะเป็น การวิเคราะห์สถิติ ว่า checkbox ไหนถูกเช็กน้อยที่สุดหรือมากที่สุดหรือเปล่า
    ยังจำได้ว่ารู้สึกเศร้าเล็กน้อยตอนเลื่อนลงไปไกล ๆ แล้วเลือก checkbox อันหนึ่ง จากนั้นมันก็ถูกยกเลิกแทบจะทันที

    • เร็ว ๆ นี้จะปล่อย ข้อมูลดิบ ให้
      ก่อนหน้านั้นยังมีอีกเรื่องหนึ่งเกี่ยวกับเว็บไซต์ที่ต้องเล่า
  • สงสัยว่าเกมยังมีชีวิตอยู่ไหม
    เข้า https://onemillioncheckboxes.com/ แล้วไม่มีอะไรถูกเช็กเลย และใน JS console เห็นแค่นี้
    {"total":0,"totalGold":0,"totalRed":0,"totalGreen":0,"totalPurple":0,"totalOrange":0,"recentlyChecked":false}

    • ตามต้นฉบับระบุว่า “ทะลุ 650 ล้าน ก่อนปิดเว็บไซต์ในอีก 2 สัปดาห์ต่อมา”
  • ตัวอย่างที่ตรงข้ามสุด ๆ กับ implementation ที่สเกลได้ คือ implementation checkbox 1 ล้านอันในโค้ดน้อยกว่า 1000 ตัวอักษร มีเวอร์ชัน Deno
    https://gist.github.com/jeff-hykin/4cdebafd8698298d021f103e2...