- Wafris เป็นบริษัทไฟร์วอลล์เว็บแอปพลิเคชันโอเพนซอร์สที่มีไคลเอนต์แบบ Rails middleware
- ไคลเอนต์ v1 เดิมต้องใช้ที่เก็บข้อมูล Redis แบบโลคัล แต่ใน v2 เปลี่ยนมาใช้ SQLite
- อธิบายเบื้องหลังการตัดสินใจย้ายจาก Redis ไป SQLite รวมถึงประเด็นด้านประสิทธิภาพและการเปลี่ยนแปลงสถาปัตยกรรม
TL;DR
- SQLite มีทั้งสิ่งที่ทำได้ดีและสิ่งที่ทำได้ไม่ดี
- Redis ก็มีทั้งสิ่งที่ทำได้ดีและสิ่งที่ทำได้ไม่ดี
- RDBMS แบบดั้งเดิม (Postgres/MySQL) ก็มีทั้งสิ่งที่ทำได้ดีและสิ่งที่ทำได้ไม่ดี
- ระบบเก็บข้อมูลเหล่านี้ไม่สามารถสลับแทนกันได้โดยตรง และถ้าพยายามทำเช่นนั้นมักจะเจอปัญหา
- บทความนี้อธิบายกระบวนการทดสอบและการตัดสินใจระหว่างการปรับสถาปัตยกรรมไคลเอนต์ v1 ที่ใช้ Redis ไปเป็นไคลเอนต์ v2 ที่ใช้ SQLite
ปัจจัยที่บีบให้ต้องเปลี่ยน
- เป้าหมายของ Wafris คือทำให้นักพัฒนาปกป้องเว็บไซต์ได้อย่างง่ายดาย
- v1 ยังทำเป้าหมายนี้ได้ไม่สมบูรณ์เพราะปัญหาเรื่องการดีพลอย Redis
- เดิมเลือก Redis เพราะทำงานในสภาพแวดล้อมอย่าง Heroku ที่ใช้งาน Redis ได้ง่าย แต่ผู้ใช้จำนวนมากกลับเจอปัญหาเรื่องการดีพลอย Redis
- การบังคับให้ผู้ใช้ต้องมี DB แยกต่างหากอย่าง Redis ไม่ได้เป็นประโยชน์ต่อผู้ใช้
"ความเร็ว" คืออะไร?
- Redis "เร็วกว่า" RDBMS แบบดั้งเดิม แต่ก็ยังต้องจัดการเรื่องการเชื่อมต่อ หน่วยความจำ โปรเซส และอื่น ๆ
- ในสภาพแวดล้อมคลาวด์ ความหน่วงเครือข่ายอาจเป็นปัญหาใหญ่
- ทุกคำขอ HTTP ที่เข้ามาต้องถูกประเมินตามกฎของ Wafris ดังนั้นความหน่วงเครือข่ายอาจทำให้แอปพลิเคชันช้าลง
สมมติฐานแบบโมโนลิทิก (Monolith-ish)
- แม้จะมีแอปแบบกระจายเต็มรูปแบบอยู่บ้าง แต่แอป Rails ส่วนใหญ่เป็น "Majestic Monoliths"
- แอปที่ดีพลอยข้ามหลายพื้นที่ แบ่งเซิร์ฟเวอร์ที่มีฟังก์ชันทับซ้อนกัน หรือเป็น Rails เพียงบางส่วน จะมีปัญหามากขึ้นเมื่อใช้ Redis
ทบทวนสถาปัตยกรรมใหม่
- Wafris เป็นเว็บแอปพลิเคชันไฟร์วอลล์ที่ติดตั้งในรูปแบบ Rails middleware
- หากแบ่งอย่างง่าย จะมี 2 ขั้นตอนคือ 1) เปรียบเทียบคำขอ HTTP กับกฎ และ 2) รายงานผลการประมวลผล
- การ "อ่าน" กฎ (ขั้นตอนที่ 1) สำคัญกว่าการ "เขียน" (ขั้นตอนที่ 2) มาก
- การอ่านต้องประมวลผลแบบลำดับ ต้องห้ามล้มเหลว และมีผลต่อประสิทธิภาพที่ผู้ใช้รับรู้
- ส่วนการเขียนสามารถช้ากว่าได้ ทำแบบแบตช์ได้ หรือทำแบบอะซิงโครนัสได้
เข้าสู่ SQLite
- มีคนอื่นอธิบายไว้ดีแล้วว่า SQLite เหมาะกับงานแบบใด
- SQLite ไม่ได้แข่งกับฐานข้อมูลแบบ client/server แต่แข่งกับ
fopen()
- หากตัด network round-trip ออกไป ก็คาดว่าจะเร็วกว่า Redis มาก
- จึงตัดสินใจทำการประเมิน benchmark ระหว่าง SQLite และ Redis
Benchmark ระหว่าง SQLite และ Redis
- การทำ benchmark เป็นศาสตร์มืดที่ทำให้เราหลอกตัวเองด้วยตัวเลขที่ดูแม่นยำ
- การ benchmark ระบบเก็บข้อมูลยิ่งยากเข้าไปอีก
- เป้าหมายไม่ใช่หาความเร็วสัมบูรณ์ แต่สร้าง benchmark ที่เฉพาะกับข้อมูลและ use case ของเรา
- มองข้ามการปรับจูนเพื่อเพิ่มประสิทธิภาพ เพราะต้องการให้ Wafris ใส่เข้าแอปแล้วทำงานได้ทันที
- เราทดสอบเส้นทางหลักและคิวรีที่แย่ที่สุดของแอปจริง ไม่ใช่ benchmark เชิงทฤษฎี
- คำขอไปยังโครงสร้างข้อมูล "lexical decimal" ที่ซับซ้อนซึ่งแมปช่วง IP (IPv4, IPv6) ไปยังหมวดหมู่ เป็นคิวรีที่แย่ที่สุด
- คำนวณการค้นหาแบบช่วงล่วงหน้าแล้วเขียนลงทั้งตาราง SQLite และ sorted set ของ Redis
- ทุกคำขอ HTTP ที่เข้ามาต้องนำ IP ของคำขอไปเทียบกับช่วงที่ผู้ใช้กำหนดสำหรับอนุญาต/บล็อก ช่วง GeoIP และช่วงคะแนนความน่าเชื่อถือของ IP
โปรโตคอลการทดสอบ
- ทดสอบบน M2 MacBook Air โดยใช้ Redis ที่ติดตั้งผ่าน Homebrew และ SQLite DB แบบโลคัล
- ทดสอบกับชุดข้อมูลช่วงเดิมที่มี 1.2 ล้านรายการ
- รันชุด IP หลายชุดกับ SQLite และ Redis ในลำดับเดียวกัน
- ในแต่ละตัวคูณ รันทดสอบ 5 ครั้งแล้วหาค่าเฉลี่ย
ผลการทดสอบ
- SQLite ชนะ Redis แบบขาดลอย (ใน use case เฉพาะของเรา)
- SQLite เร็วกว่า Redis instance แบบโลคัลราว 3 เท่า
- นี่ยังเป็นผลลัพธ์ก่อนพิจารณาความหน่วงเครือข่าย
- ต่อให้ SQLite แค่เทียบชั้น Redis ได้ ก็ยังได้เปรียบเพราะตัดเวลาเครือข่ายออกไปได้
สิ่งที่ไม่มีในกราฟ
- ต่อให้ประสิทธิภาพของ SQLite แย่ลง 2 เท่าใน benchmark ในโลกจริงก็อาจยังเร็วกว่าเพราะความหน่วงเครือข่าย
- ไม่ว่า Redis server จะทรงพลังแค่ไหน ก็ยังมีข้อจำกัดเรื่องแบนด์วิดท์เครือข่าย การเชื่อมต่อ และความหน่วงข้ามภูมิภาค
- SQLite สามารถสเกลแนวนอนได้แทบไม่จำกัดแบบ "ฟรี"
- การ onboard ด้วย SQLite ดีขึ้นมาก ผู้ใช้อาจไม่ทันรู้ตัวด้วยซ้ำว่ามันกำลังถูกใช้งาน
- แม้จะเค้นประสิทธิภาพจาก Redis ได้มากกว่านี้ แต่ก็ยากที่จะโน้มน้าวให้ผู้ใช้เปลี่ยนการตั้งค่า Redis
ผลลัพธ์เป็นเพียงจุดเริ่มต้น
- แม้พิสูจน์ได้ว่า SQLite เร็วกว่า Redis แต่ก็มี trade-off จริงอยู่
- การทดสอบข้างต้นไม่ได้คำนึงถึงการเขียนข้อมูล
- การจัดการการแย่งกันระหว่างการอ่านและการเขียนยังต้องใช้สิ่งต่าง ๆ อย่างการเชื่อมต่อ connection pool และ transaction
- เหมือนกับรถซูเปอร์คาร์ไฟฟ้าที่แบกบล็อกคอนกรีตได้ยาก จึงไม่ควรใช้ SQLite ในบทบาทที่ไม่เหมาะกับมัน
สร้างสถาปัตยกรรมการซิงก์
- ใน v1 (Redis) เมื่อผู้ใช้อัปเดตกฎจาก Wafris Hub กฎในที่เก็บข้อมูล Redis ก็จะถูกอัปเดตตาม
- แต่กับ SQLite วิธีนี้ใช้ไม่ได้ เพราะไม่สามารถ "push" ไปยังเว็บเซิร์ฟเวอร์ได้
- ใน v2 (SQLite) กระบวนการคือ 1) ผู้ใช้อัปเดตกฎใน Wafris Hub 2) ไคลเอนต์ตรวจสอบกฎที่อัปเดตเป็นช่วง ๆ 3) หากมีกฎอัปเดต ก็จะดาวน์โหลด SQLite DB ลูกใหม่ทั้งก้อน
- วิธีนี้ลดภาระการติดตั้งและคอนฟิกของผู้ใช้ลงอย่างมาก
- อัตราความสำเร็จในการติดตั้งไคลเอนต์ v2 เพิ่มขึ้น 3 เท่า
สถาปัตยกรรมแบบกระจายด้วย SQLite
- ลองนึกถึงแอป Rails ที่ดีพลอยบนผู้ให้บริการคลาวด์โดยเปิดใช้งาน auto-scaling
- หากคำขอเพิ่มจาก 100req/s เป็น 10,000req/s instance ฝั่งคอมพิวต์จะสเกล แต่ DB จะไม่สเกลตาม
- นี่คือสาเหตุหลักที่ทำให้แอป Rails ล่มจากภาวะโหลดเกินในโลกจริง
- การซิงก์ SQLite DB ไปยังแต่ละ compute instance ช่วยแก้ปัญหานี้ได้ เพราะทำให้ทุกการเรียกเป็นแบบโลคัล
แล้วการเขียนล่ะ?
- ทีมแยกแอปออกเป็นเส้นทางอ่าน (ประเมินกฎ) และเส้นทางเขียน (รายงานผล) แล้วเลือกมองข้ามเส้นทางเขียนไปก่อน
- เส้นทางเขียนถูกออกแบบใหม่เป็น 1) เชื่อมต่อไปยัง Wafris Hub แบบอะซิงโครนัสเพื่อรายงาน 2) ส่งรายงานแบบแบตช์ 3) ตัดการเขียน DB ออกจากไคลเอนต์ทั้งหมด
- วิธีนี้อาจไม่เหมาะกับคนอื่น แต่สิ่งที่เราใส่ใจคือผู้ใช้ที่ต้องการไคลเอนต์ Wafris ที่ติดตั้งง่ายและทำงานเร็ว
บทสรุป
- ทีมพึงพอใจกับสถาปัตยกรรม v2 ที่ใช้ SQLite อย่างมาก
- มันช่วยให้หลายเว็บไซต์ทนต่อการโจมตีและออนไลน์ต่อไปได้แล้ว
- การเริ่มต้นใช้งานง่ายขึ้นมาก ทำให้งานซัพพอร์ตของเราลดลงและผู้ใช้ก็ยุ่งยากน้อยลง
- นี่ถือเป็นชัยชนะเพื่ออินเทอร์เน็ตที่ปลอดภัยและมั่นคงยิ่งขึ้น
7 ความคิดเห็น
แม้ว่า SQLite จะดีเพียงพออยู่แล้ว แต่ในกรณีนี้ก็อดคิดไม่ได้ว่า มันอาจจะเป็น use case ที่ไม่ค่อยเหมาะกับ Redis ตั้งแต่แรกหรือเปล่า...
การทำเบนช์มาร์กบน M2 นี่ก็แบบว่า...
งั้นต้องวัดกันทุก AWS instance เลยเหรอครับ? คุณคาดหวังจากโอเพนซอร์สมากเกินไปนะ
ทำบนสภาพแวดล้อมเซิร์ฟเวอร์เดียวกัน แบบนี้มีปัญหาไหมครับ?
สำหรับเบนช์มาร์กจำเป็นต้องใช้ CPU รุ่นที่ระบุไว้โดยเฉพาะหรือเปล่าครับ...?
สิ่งที่ทำบน m2 อาจมีจุดไหนบ้างที่เป็นปัญหาได้? (นอกเหนือจากประเด็นที่ว่าสภาพแวดล้อมบริการจริงไม่ได้ใช้โปรเซสเซอร์ m2)
นั่นแหละคือปัญหา ไปทดลองกันในแล็บ แล้วก็อ้างว่านี่สมบูรณ์แบบสำหรับเชิงพาณิชย์!