1 คะแนน โดย GN⁺ 2024-09-26 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Wafris เปลี่ยนที่เก็บข้อมูล Redis ที่ผู้ใช้เป็นเจ้าของของไคลเอนต์ Rails middleware v1 ไปเป็น ที่เก็บข้อมูลบน SQLite ใน v2 เพื่อช่วยลดทั้งความยากในการดีพลอยและเวลาแฝงในการประเมินคำขอ
  • เดิมทีการเลือก Redis ได้รับอิทธิพลจากแรงเฉื่อยของระบบนิเวศ Rails เช่น Heroku และ Sidekiq แต่ในการใช้งานจริง ปัญหาคือผู้ใช้ต้องรับบทเป็น ผู้ดูแล Redis ไปด้วย
  • เส้นทางการอ่าน ที่ต้องเปรียบเทียบทุกคำขอ HTTP ขาเข้ากับกฎต่าง ๆ คือคอขวดหลัก ส่วนการเขียนเพื่อรายงานสามารถทำให้ช้าลง หรือแยกไปทำแบบแบตช์และอะซิงก์ได้
  • จากการทดสอบการค้นหา IP range ที่แย่ที่สุดด้วยชุดข้อมูล 1.2 ล้านรายการบน MacBook Air M2 ในเครื่อง พบว่า SQLite ให้ประสิทธิภาพดีกว่า Redis แบบโลคัลประมาณ 3 เท่า และยังไม่รวมเวลาแฝงของเครือข่าย
  • v2 ใช้โครงสร้างการซิงก์ที่ตรวจสอบกฎใหม่ตามช่วงเวลาหรือตามจำนวนคำขอ แล้วดาวน์โหลด SQLite DB ใหม่ทั้งก้อน ส่งผลให้การติดตั้งที่สำเร็จเพิ่มขึ้นประมาณ 3 เท่า

แรงเสียดทานในการดีพลอยที่เห็นได้จาก v1 แบบใช้ Redis

  • Wafris เป็นบริษัทไฟร์วอลล์เว็บแอปพลิเคชันโอเพนซอร์สที่ให้บริการไคลเอนต์ middleware สำหรับ Rails applications
  • ไคลเอนต์ v1 ต้องใช้ Redis storage ที่ผู้ใช้เป็นเจ้าของ ซึ่งดีพลอยไปพร้อมกับแอปพลิเคชัน
  • การเลือก Redis ในช่วงแรกได้รับอิทธิพลจากสภาพแวดล้อมที่เชื่อม Redis บน Heroku ได้ง่าย ความสะดวกในการเข้าถึงจากระยะไกล และตัวอย่างความสำเร็จอย่าง Sidekiq
  • แต่สภาพแวดล้อมจริงของผู้ใช้มีความหลากหลายกว่า และผู้ใช้จำนวนมากประสบปัญหาในการดีบัก ปัญหาการดีพลอยและตั้งค่า Redis
  • ในงาน RailsWorld 2023 ก็มีบรรยากาศเชิงลบต่อสมมติฐานที่ว่า Rails application จะต้องมี Redis server อยู่ข้าง ๆ โดยปริยาย

หัวใจของปัญหาความเร็วคือเวลาแฝงของเครือข่าย

  • Redis แม้จะเร็วเมื่อเทียบกับ RDBMS แบบดั้งเดิม แต่เพราะเป็นฐานข้อมูลแยกต่างหาก จึงต้องมีการจัดการการเชื่อมต่อ หน่วยความจำ และโปรเซส
  • ในสภาพแวดล้อมคลาวด์ เวลาแฝงของเครือข่าย กระทบต่อประสิทธิภาพการประมวลผลคำขอโดยตรง
    • Wafris ต้องเปรียบเทียบทุกคำขอ HTTP ที่เข้ามายังแอปพลิเคชันกับกฎที่บันทึกไว้
    • ต่อให้ทำไคลเอนต์ v1 ให้เร็วที่สุดแล้ว หากเครือข่ายที่แอปพลิเคชันรันอยู่ช้า การตอบสนองโดยรวมก็ยังช้าลงได้
  • Rails app เองก็ไม่ได้ถูกดีพลอยเป็น “majestic monolith” เดียวเสมอไป
    • แอปที่ดีพลอยข้ามหลายโซน
    • แอปที่แยกเป็นหลายเซิร์ฟเวอร์ที่มีความรับผิดชอบทับซ้อนกัน
    • แอปที่มีเพียงบางส่วนเป็น Rails และดีพลอยร่วมกับภาษาอื่นหรือเฟรมเวิร์กอื่น
  • ในสภาพแวดล้อมการปฏิบัติงานแบบนี้ แรงเสียดทานจากการใช้ Redis ยิ่งเพิ่มขึ้น

การแยกการอ่านและการเขียนในกระบวนการจัดการคำขอของ Wafris

  • Wafris ติดตั้งเป็น Rails middleware โดยตั้งกฎอย่าง “บล็อก IP 1.2.3.4” แล้วนำคำขอที่เข้ามาไปเทียบกับกฎเหล่านั้น
  • ขั้นตอนการทำงานแบบย่อมีสองช่วง
    1. เปรียบเทียบคำขอ HTTP กับกฎ ถ้าตรงกันให้ตอบ 403 ไม่เช่นนั้น 200
    2. รายงานผลลัพธ์ เช่น บล็อก อนุญาต หรือปล่อยผ่าน
  • ขั้นตอนแรกคือ การอ่านกฎ ซึ่งสำคัญกว่าการเขียนรายงานมาก
    • คำขอต้องถูกประมวลผลตามลำดับ
    • ถ้าการกรองทำงานไม่ถูกต้อง คำขอที่ไม่ดีอาจหลุดผ่านได้
    • การประเมินคำขอมีผลต่อประสิทธิภาพของเว็บไซต์ที่ผู้ใช้รับรู้ได้
    • ส่วนการเขียนรายงานสามารถทำให้ช้าลง ทำเป็นแบตช์ หรือทำแบบอะซิงก์ได้

เหตุผลที่เลือก SQLite

  • คอขวดหลักของ Wafris คือ network I/O และข้อความในเอกสาร SQLite ที่ว่า “SQLite does not compete with client/server databases. SQLite competes with fopen().” มีอิทธิพลต่อการตัดสินใจ
  • ภายใต้สมมติฐานว่าเพียงตัด network round-trip ออก ก็อาจเร็วกว่าโครงสร้างแบบ Redis ได้ จึงทำ benchmark ระหว่าง SQLite กับ Redis
  • แหล่งอ้างอิงที่ใช้มีดังนี้

ขอบเขตของเบนช์มาร์กและข้อจำกัดที่ตั้งใจไว้

  • เบนช์มาร์กนี้ไม่ใช่การเปรียบเทียบประสิทธิภาพฐานข้อมูลแบบทั่วไป แต่เป็นการทดสอบแบบมีอคติที่มุ่งไปยัง hot path และคิวรีที่แย่ที่สุดของ Wafris
  • คิวรีที่แย่ที่สุดคือการค้นหาในโครงสร้างข้อมูล “lexical decimal” ที่แมป IP ranges กับหมวดหมู่
    • ตัวอย่างแบบง่ายคือการแมป IP → ประเทศ โดยตรวจว่า IP address อยู่ในช่วงระหว่างสองที่อยู่หรือไม่ แล้วคืนค่าประเทศ
    • โครงสร้างนี้มีขนาดระดับหลายล้านแถว และในกรณี IPv6 แต่ละรายการมีขนาดใหญ่
  • การค้นหาแบบช่วงถูกคำนวณล่วงหน้าแล้วเขียนลงในทั้งสองสตอเรจ
    • ตารางของ SQLite
    • sorted set ของ Redis
  • ในกรณีที่หนักที่สุด แต่ละคำขอ HTTP ขาเข้าต้องนำ IP ของคำขอไปเทียบกับช่วงต่อไปนี้
    1. ช่วง allow ที่ผู้ใช้กำหนดเอง
    2. ช่วง block ที่ผู้ใช้กำหนดเอง
    3. ช่วง GeoIP
    4. ช่วงชื่อเสียงของ IP
  • คิวรีประเภทนี้สำคัญมากพอ จึงไม่ได้พอร์ตคิวรีหรือฟีเจอร์อื่นมาทดสอบ และทดสอบเฉพาะชนิดนี้อย่างเดียว

วิธีทดสอบและผลลัพธ์

  • การทดสอบใช้ Redis ที่ติดตั้งด้วย Homebrew บนเครื่องโลคัล MacBook Air M2 และ SQLite DB แบบโลคัล
  • โปรโตคอลมีดังนี้
    1. ใช้ชุดข้อมูลช่วงเดิมจำนวน 1.2 ล้านรายการ
    2. รันชุด IP หลายชุดกับ SQLite และ Redis ตามลำดับเดียวกัน
    3. ในแต่ละตัวคูณ รันทดสอบ 5 ครั้งและใช้ค่าเฉลี่ย
  • สำหรับ use case เฉพาะของ Wafris, SQLite ให้ประสิทธิภาพดีกว่า Redis แบบโลคัลประมาณ 3 เท่า
  • ผลลัพธ์นี้เป็นตัวเลข ก่อนคำนึงถึงเวลาแฝงของเครือข่าย
  • การทดสอบสะท้อนทั้งการตั้งค่าที่เรียบง่ายโดยตั้งใจและข้อบกพร่องของการใช้งานจริง จึงยากจะสรุปเหมารวมเป็นการเปรียบเทียบฐานข้อมูลแบบทั่วไป

ความต่างด้านปฏิบัติการที่ไม่ปรากฏในกราฟ

  • แม้ประสิทธิภาพของ SQLite จะแย่กว่า Redis มากในเบนช์มาร์ก ก็ยังประเมินว่าในสภาพแวดล้อมจริงอาจเร็วกว่าได้ เพราะมีเวลาแฝงของเครือข่ายไปยัง Redis ที่อยู่ในดาต้าเซ็นเตอร์หรือรีเจียนเดียวกัน
  • ต่อให้ Redis server ถูกจัดวางอย่างแข็งแรงด้วยคลัสเตอร์หรือการชาร์ด ก็ยังมีข้อจำกัดอย่างแบนด์วิดท์เครือข่าย จำนวนการเชื่อมต่อ และเวลาแฝงข้ามรีเจียนอยู่ดี
  • SQLite อยู่แบบโลคัลบนแต่ละ compute instance จึงแทบจะทำให้ ต้นทุนของการขยายแนวนอน หายไปสำหรับ use case นี้ของ Wafris
  • การ onboard ก็ง่ายกว่าฝั่ง SQLite เช่นกัน
    • ผู้ใช้สามารถเพิ่ม gem เข้าเว็บแอปและรันได้ โดยไม่จำเป็นต้องรู้ด้วยซ้ำว่ามีการใช้ SQLite
  • Redis ยังมีพื้นที่ให้ปรับแต่งเพิ่มอีกมาก แต่ Wafris พบว่ายากที่จะทำให้ผู้ใช้ยอมเปลี่ยนค่าตั้งต้นพื้นฐานอย่างนโยบายการขับแคชออกอย่างสม่ำเสมอ

การเปลี่ยนโครงสร้างที่จำเป็นหลังย้ายมา SQLite

  • โฟลว์การอัปเดตของ v1 ที่ใช้ Redis นั้นเรียบง่าย
    1. ผู้ใช้อัปเดตกฎใน Wafris Hub
    2. Wafris Hub อัปเดตกฎใน Redis storage ของผู้ใช้
  • ใน SQLite, Wafris Hub ไม่สามารถ “push” ฐานข้อมูล SQLite ไปยังเว็บเซิร์ฟเวอร์ได้โดยตรง
  • ผู้ให้บริการ SQLite as a service บางรายทำแนวทางคล้ายกันได้ แต่ไม่เหมาะกับ Wafris เพราะข้อพิจารณาด้านต้นทุน ประสิทธิภาพ และความปลอดภัย
    • ผู้ใช้แต่ละรายต้องเป็นคนดีพลอยเอง
    • ต้องเปิดพอร์ต
    • ต้องยอมรับการเชื่อมต่อขาเข้า
  • โฟลว์การอัปเดตของ v2 ที่ใช้ SQLite มีดังนี้
    1. ผู้ใช้อัปเดตกฎใน Wafris Hub
    2. ไคลเอนต์ตรวจสอบกฎที่อัปเดตแล้วเป็นช่วง ๆ ตามเวลา หรือตามจำนวนคำขอ
    3. ถ้ากฎเปลี่ยน ไคลเอนต์จะดาวน์โหลดฐานข้อมูล SQLite ใหม่ทั้งหมด
  • โครงสร้างนี้ลดภาระด้านการติดตั้งและตั้งค่าของผู้ใช้ลงอย่างมาก และทำให้การติดตั้งไคลเอนต์ v2 ที่สำเร็จเพิ่มขึ้นประมาณ 3 เท่า

โครงสร้าง SQLite ในการดีพลอยแบบกระจาย

  • ในสภาพแวดล้อมคลาวด์อย่าง AWS, Heroku และ Fly เมื่อ Rails app ทำ autoscaling จำนวน compute instances อาจเพิ่มขึ้น แต่ฐานข้อมูลไม่ได้เพิ่มขึ้นตามเสมอไป
  • เมื่อคำขอเพิ่มจาก 100 req/s เป็น 10,000 req/s dyno, machine หรือ EC2 instance อาจถูกเพิ่มเข้ามา แต่ คอขวดของฐานข้อมูล อาจยังคงอยู่เหมือนเดิม
  • จากที่ Wafris สังเกต สาเหตุหลักของการล่มของ Rails app มักไม่ใช่ DDoS จริง แต่เป็น credential stuffing หรือทราฟฟิกบอตที่ไม่ดี
    • ทราฟฟิกประเภทนี้กระตุ้น autoscaling
    • หลังจากนั้นการเชื่อมต่อฐานข้อมูลอาจถูกใช้จนหมดและทำให้แอปล่มได้
  • หากซิงก์ SQLite DB ไปยังแต่ละ compute instance ก็จะทำให้ทุกการเรียกบนอินสแตนซ์ใหม่ยังคงเป็นแบบโลคัลได้

เส้นทางการเขียนถูกถอดออกจากไคลเอนต์

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

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

 
GN⁺ 2024-09-26
ความคิดเห็นจาก Hacker News
  • โมเดลที่ให้แต่ละเซิร์ฟเวอร์แอปพลิเคชันมีสำเนาของ ไฟล์ฐานข้อมูล SQLite แล้วสลับเปลี่ยนทั้งไฟล์เป็นระยะ ๆ นั้นน่าสนใจมาก
    ในกรณีนี้ใช้กับกฎของไฟร์วอลล์เว็บแอปพลิเคชัน แต่ก็น่าจะเหมาะกับ การตั้งค่า feature flag ด้วยเช่นกัน เพราะ feature flag อาจถูกตรวจสอบหลายสิบครั้งต่อหนึ่งคำขอ และมักต้องใช้คิวรีอย่าง “ผู้ใช้อยู่ในกลุ่ม A และ IP อยู่ในประเทศ B” ซึ่ง SQLite ในเครื่องจัดการได้ดี โดยทั่วไปแล้ว feature flag ยังยอมรับได้หากการอัปเดตเผยแพร่ล่าช้าไปหลายวินาที หรือมากกว่านั้น

    • วิธีนี้เป็นโมเดลที่ใช้ใน CDN ด้วย คืออัปเดตไฟล์ตั้งค่าระดับโลกที่รวมใบรับรองของลูกค้าทั้งหมด กฎการ routing ของ HTTP ฯลฯ ไว้ในโครงสร้าง B-tree แบบไฟล์เดียว แล้วแจกจ่าย “บันเดิล” นั้นไปยัง edge location ทั้งหมดบ่อย ๆ
      แต่ยังไม่เคยเห็นกรณีที่ใช้ SQLite สำหรับจุดประสงค์นี้ โดยปกติจะใช้ฐานข้อมูลสไตล์ DBM อย่าง LMDB หรือ Kyoto Cabinet
    • Airbnb ก็ใช้โมเดลนี้เพื่อแจกจ่าย คำแปล, feature flag, การตั้งค่า, ดัชนีค้นหา เป็นต้น แทนที่จะใช้ SQLite เขาใช้ Sparkey ซึ่งเป็นรูปแบบไฟล์คีย์-แวลูที่ Spotify สร้างขึ้น
      ช่วงแรก งาน Cron ของแต่ละเซิร์ฟเวอร์จะดึงข้อมูลของบริการนั้น ๆ มา และหลังจากย้ายไป Kubernetes ก็ใช้ DaemonSet ร่วมกับวิธีคล้ายการติดแท็กโฮสต์/taints เพื่อดาวน์โหลดข้อมูลหลายชุดลงแต่ละโฮสต์ จากนั้นรับประกันว่าบริการที่ใช้ข้อมูลนั้นจะรันเฉพาะบนโฮสต์นั้นเท่านั้น ใน Ruby เรียกสิ่งนี้ว่า “hammerspace”: https://github.com/airbnb/hammerspace
    • ปัญหาการอ่านค่า feature flag หลายครั้งต่อหนึ่งคำขอ แก้ได้ด้วยการดึงค่า flag เพียงครั้งเดียวแล้ว เก็บไว้ในอ็อบเจ็กต์คำขอ
      วิธีนี้ทำให้คำขอหนึ่ง ๆ ไม่ต้องจ่ายต้นทุนการ lookup ที่แพงซ้ำมากกว่าหนึ่งครั้ง และไม่มีความเสี่ยงที่ค่าจะไม่สอดคล้องกันเพราะ flag ถูกอัปเดตระหว่างประมวลผลคำขอ
    • หลังจากจัดการ feature flag มานานระหว่างการ migration ที่ยืดเยื้อ ก็ยิ่งมั่นใจว่าองค์ประกอบพื้นฐานที่มีประโยชน์จริง ๆ คือ “สิ่งที่เหมือน SQLite แต่มี ตัวนับเวอร์ชันระดับโลก เพิ่มขึ้นทุกครั้งที่มีการเปลี่ยนแปลง และสามารถดึงหรือใช้เฉพาะส่วนต่างระหว่างหมายเลขเวอร์ชันได้”
      บนสิ่งนั้นสามารถวางกลไกการแจกจ่ายได้หลากหลาย เช่น เครือข่าย gossip หรือเครือข่ายแคช/การกระจายแบบต้นไม้ที่ระบุชัดเจน ใครก็ตามที่มีเวอร์ชันใหม่กว่าก็สามารถส่งอัปเดตให้ได้
      สามารถฝังเวอร์ชัน DB ปัจจุบันไว้ใน artifact ของแอปพลิเคชัน และหลังจากแอปรันแล้วก็ยังอัปเดตได้อย่างมีประสิทธิภาพ สำหรับ feature flag หรือ live config สามารถ fallback ไปยังสถานะล่าสุด แทนค่าเริ่มต้นระดับโค้ดได้
      ไคลเอนต์ใด ๆ ก็สามารถส่งตัวนับระดับโลกไปเหมือน ETAG แล้วรับเฉพาะส่วนต่างได้ และการเชื่อมต่อใหม่หลังเครือข่ายหลุดชั่วคราวก็จะง่ายและมีต้นทุนต่ำ
      หากเก็บประวัติย้อนหลังได้สักไม่กี่นาที ก็สามารถแนบตัวนับไปกับการเรียกไมโครเซอร์วิสในลักษณะเดียวกับ W3C Baggage header เพื่อให้หลายบริการประเมิน flag/การตั้งค่า/ข้อมูลบนฐานหมายเลขเวอร์ชันเดียวกันได้ แม้ไม่มีฟีเจอร์ประเมินตามเวลา การบันทึกหมายเลข generation ลงในล็อกก็ช่วยการดีบักภายหลังได้มาก
    • SQLite สำหรับการแจกจ่ายนั้นเจ๋งมาก แนวคิดนี้ดูเหมือนได้แรงบันดาลใจมาบางส่วนจากโปรเจกต์ Datasette และภายหลังในด้าน reporting กับการสำรวจข้อมูลก็น่าจะยังทำอะไรบางอย่างได้
  • นอกจากความหน่วงของเครือข่ายแล้ว ใน Redis ผมเห็นว่า latency ของการอ่าน/เขียนมักแปรผันเกือบเชิงเส้นตามจำนวนคีย์ที่ lookup และกราฟในบทความก็ดูคล้ายกัน
    ตอนใช้ Postgres กับ Redis ร่วมกันตามวัตถุประสงค์ในแอปแบบ monolithic ก็ทำงานได้ค่อนข้างดี แต่การยัดฟีเจอร์ใหม่เข้าไปในคลัสเตอร์ Redis ที่ใช้ร่วมกันนั้นง่ายเกินไป Redis เป็น single-threaded ดังนั้นฟีเจอร์ที่ไม่คำนึงถึงผลกระทบเพียงอย่างเดียวที่ bulk lookup คีย์มากกว่า 100,000 คีย์ ก็ทำให้ฟีเจอร์อื่นช้าลงได้
    ดังนั้นจึงเสนอแนวทางว่า Redis เหมาะเป็นพิเศษเมื่ออ่านเขียนทีละคีย์ หรือจำนวนคีย์น้อย ๆ คงที่ เช่น lock หรือ rate limit ของ endpoint ยอดนิยม ซึ่งมีฟีเจอร์สุ่มหลายอย่างใช้ Redis
    ในกรณีนี้ Redis น่าจะโดดเด่นกับการ lookup คีย์เดียวแบบง่าย ๆ (ที่อยู่ IP) แต่ดูไม่เหมาะกับการอ่านที่ซับซ้อนกว่านั้น เช่น การแสดง range query ผมเองก็ไม่เข้าใจลึกนักว่าทำไม SQLite ถึงทำงานได้ดีขนาดนี้เมื่อเทียบกับ Redis ในเครื่อง จึงรู้สึกคาดไม่ถึงและน่าสนใจ

    • Redis มักกลายเป็น กับดัก เพราะผู้คนเข้าใจข้อดีข้อเสียผิดได้ง่าย
      มอง Redis เป็นแคชที่มี primitive operation หลากหลายกว่าจะดีที่สุด ถ้าใช้ให้เหมาะสม มันเร็วและเสถียร
      แต่ถ้าเริ่มเอาสิ่งที่ไม่เข้ากับ RDBMS หลักไปใส่ใน Redis ไม่นานก็จะมี work queue, lock หลายประเภท ฯลฯ และสุดท้ายก็เป็นเพียงเรื่องของเวลาก่อนที่ประสิทธิภาพจะตกหน้าผา หรือพังด้วยเหตุผลอื่น กระบวนการกู้คืนมักยุ่งเหยิง และมักจบด้วยการยอมรับการสูญเสียข้อมูลบางส่วน
      แนวโน้มนี้เกิดขึ้นทีละนิดได้ง่าย จึงต้องมีวินัยเพื่อหลีกเลี่ยง ประสิทธิภาพของ SQLite ไม่ได้มาจากการหลีกเลี่ยง overhead ของเครือข่ายเท่านั้น แต่ยังเพราะหลายคนประเมิน ต้นทุน serialization/deserialization ต่ำเกินไปด้วย แม้โปรโตคอลของ Redis จะค่อนข้างมินิมัล แต่ต้นทุนก็สะสมได้ และใน SQLite งานจำนวนมากลงเอยเป็นการคัดลอกหน่วยความจำภายในโปรเซส
  • เมื่อไม่กี่สัปดาห์ก่อน ในแฮกกาธอนภายในของ Neon ผมทำเซิร์ฟเวอร์ Node.js เล็ก ๆ ที่แปลง RESP ซึ่งเป็น wire protocol ของ Redis ให้เป็นคิวรี Postgres
    เป็นโปรเจกต์แฮ็กที่สนุกมาก: https://github.com/btholt/redis-to-postgres

  • ฟังดูเหมือน use case เฉพาะที่ SQLite ฝั่งเซิร์ฟเวอร์ซึ่งฐานข้อมูลเป็นแบบอ่านอย่างเดียวและไม่ต้อง replication ค่อนข้างเหมาะมาก
    อีกทางเลือกหนึ่งอาจเป็นการโหลดไฟล์สแตติกขึ้นหน่วยความจำ แต่ในกรณีนี้ข้อมูลดูจะเยอะเกินกว่าจะเก็บไว้ในหน่วยความจำตลอดเวลา SQLite จึงเป็นทางเลือกที่ดี

    • หวังว่าบทความจะสื่อออกไปได้ว่า นี่เป็นโซลูชันที่ยอดเยี่ยมและเหมาะกับ use case เฉพาะ ไม่ใช่ ตัวทดแทนแบบ 1:1 ของ Redis หรือ Postgres
    • ถ้าเริ่มจากวิธีโหลดไฟล์สแตติกขึ้นหน่วยความจำ แล้วค่อย ๆ เติมเฉพาะฟีเจอร์ที่จำเป็นให้มากพอเพื่อให้ใช้ได้กับ use case ทั่วไป สุดท้ายก็จะกลายเป็น ฐานข้อมูลเชิงสัมพันธ์ แบบ SQLite
      ความแตกต่างคือ SQLite มีความแข็งแกร่งมาก ประสิทธิภาพดี และผ่านการทดสอบมาอย่างยอดเยี่ยมเป็นพิเศษ
  • ผมสงสัยตรงที่บอกว่าใน RailsWorld 2023 มีบรรยากาศเหมือน “ได้กลิ่นเลือด” เกี่ยวกับ Redis
    ในอาชีพเคยทำแอป Rails บนโปรดักชันแค่ตัวเดียว และแอปนั้นใช้ Redis เลยไม่ค่อยรู้กระแสช่วงนี้ อยากรู้ว่า ecosystem กำลังหันหลังให้ Redis ในมุมธุรกิจอยู่หรือเปล่า เป็นเพราะการเปลี่ยน license หรือจริง ๆ แล้วเป็นสถานการณ์ที่ใกล้เคียงกับ YAGNI กันแน่
    ตอนนั้นใช้กับ Rescue เป็นหลักเพื่อจัดคิวงาน async อย่างการสร้างดัชนีและ transcoding และในตอนนั้นก็ดูเป็นเครื่องมือที่ค่อนข้างดี

    • ค่อนข้างใกล้กับ YAGNI อยู่บ้าง เหตุผลใหญ่ที่สุดที่ชุมชน Rails หันมาใช้ Redis กันมากคือ async jobs และเครื่องมือที่คนส่วนใหญ่หยิบมาใช้คือ Sidekiq
      ในแบบสำรวจชุมชน Rails ปี 2024 Redis ก็ยังเป็น data store ที่ใช้ในแอปมากที่สุด
      แต่ถึงหลายแอปจะใช้ Redis จริง ๆ แล้วก็มักใช้แค่กับกรณีอย่าง Sidekiq ไม่ได้เอาไปใช้กับ real-time leaderboard หรือฟีเจอร์ vector DB อะไรทำนองนั้น ภาพการใช้งานจริงเลยค่อนข้างไม่ชัดนัก
    • มองว่าเป็นเรื่องของ ความเรียบง่าย ล้วน ๆ
      ตอนนี้ setup ของ Rails ที่มีทราฟฟิกระดับหนึ่งมักมี frontend server, SQL DB, key-value store (Redis หรือ Memcached) และ cache store ที่ชี้ไปยัง key-value store นั้น แต่รูปแบบการใช้ key-value ทั่วไปอย่างการเก็บ API quota หรือ rate limit กับรูปแบบการใช้ cache นั้นต่างกันพอสมควร เลยยุ่งยาก
      ประสิทธิภาพดิสก์เร็วพอแล้วและ SQL ก็เร็วขึ้น จึงมีความเคลื่อนไหวที่จะตัด key-value store ออก แล้วแบ่งงานแบบ key-value ดั้งเดิมไปให้ SQL DB ส่วน backend ของ cache ไปไว้บนดิสก์ ดิสก์ NVMe รุ่นใหม่เร็วเกือบใกล้ RAM แต่ถูกกว่ามาก จึง cache ได้มากกว่า
    • ในอดีต Redis มีประโยชน์เหมือน shared memory ที่ฉลาดมาก มีฟีเจอร์อย่าง expiration ในตัว ถ้าทำ clustering ก็แชร์ข้ามหลายเครื่องได้
      แต่ในยุค Kubernetes และ Redis-as-a-service “shared memory” นั้นกลับไปอยู่บน VM อีกเครื่องใน rack อื่น พอถึงจุดนั้นก็เท่ากับยอมแพ้เรื่องประสิทธิภาพการใช้ทรัพยากรแล้ว ผมว่าอ่านเขียนไฟล์จาก S3 ไปเลยยังดีกว่า
    • SSD สมัยนี้เร็วแล้ว คนควรใช้ RocksDB กันมากขึ้น สามารถรัน key-value store ที่ใหญ่กว่ามากบน SSD ได้
  • สำหรับข้อความที่ว่า “Redis เร็วกว่า RDBMS แบบดั้งเดิม แต่ก็ยังเป็น database ที่ต้องจัดการ connection, memory, process ฯลฯ จึงทำให้ stack เปราะบางขึ้น” ผมคิดว่าเมื่อเริ่มจัดการ transaction ที่เกินระดับของเล่น ไม่ว่าจะเป็น relational หรือ non-relational database ทั้งหมดก็ต้องการการจัดการและบำรุงรักษาในระดับใกล้เคียงกัน
    ส่วนที่ว่า “เร็ว” ก็น่าสนใจนิดหน่อย ถ้าไม่ต้องสนใจ join การ insert และ query แถวก็เร็วพอสมควร

    • เหตุผลที่ SQLite เร็วเป็นพิเศษในสภาพแวดล้อม server คือไม่ต้องมี network call เวลาทำ query หรือดึงข้อมูล
      เมื่อเข้าใจว่ามันอ่านจาก local disk โดยตรง เรื่อง join หรือ transaction ก็แทบไม่มีความหมายแล้ว เพราะเส้นทางนี้เร็วกว่าอยู่แล้วหลายหลัก
    • SQLite มีงาน VACUUM ที่ดูคล้าย garbage collection อยู่บ้าง
      ทุกครั้งที่อ่านเอกสารว่าเมื่อไหร่ควรรัน VACUUM ก็ยังสับสน ตอน deploy แอปที่ใช้ SQLite ครั้งล่าสุด ผมแค่ตั้ง counter ไว้ แล้วรัน VACUUM หลังมี write จำนวนมาก
    • ไม่เห็นด้วยกับคำว่า “database ทั้งหมดต้องการการจัดการและบำรุงรักษาในระดับใกล้เคียงกัน”
      ถ้าเป็น database ที่ต้องรันเป็น server process แยกต่างหาก ความพยายามด้านการจัดการและบำรุงรักษาก็ย่อมต่างกันอย่างเลี่ยงไม่ได้ มองแบบสุดโต่งแล้ว ยากที่จะเชื่อว่าการใช้ SQLite กับ Oracle จะไม่ต่างกันจริง ๆ
  • อาจสนใจ Redka ซึ่งเป็นการ implement Redis ใหม่ด้วย SQLite ใน Go: https://github.com/nalgeon/redka

    • น่าสนใจพอจะพิจารณาย้าย แต่พอเห็นส่วนที่ว่า “ตาม benchmark แล้ว Redka ช้ากว่า Redis หลายเท่า” ก็ทำให้ลังเล
      เป็นโปรเจกต์ที่ยอดเยี่ยมจริง แต่ถ้าระดับนี้ก็แทบไม่มีแรงจูงใจให้ย้าย
  • อยากตรวจสอบว่ารูปแบบการใช้ Redis ถูกต้องไหม
    ใน v1 WAF กับ Redis อยู่บน server เดียวกัน และเมื่อ client ตั้งกฎใหม่ใน admin panel กฎนั้นจะเข้า Redis บน server เดียวกับ admin panel จากนั้นด้วยกลไก sync ภายในของ Redis กฎจะถูกอัปเดตไปยัง Redis local ที่อยู่ข้าง WAF ทั่วโลก ใช่โครงสร้างแบบนี้หรือไม่ อยากยืนยันว่าเมื่อมี request ใหม่เข้ามาที่ WAF ตัวใด WAF จะตรวจสอบ request/IP ด้วยกฎใน Redis ที่อัปเดตแล้วหรือเปล่า
    ใน v2 มีการเอา Redis cluster ออก แล้วให้ WAF server ทุกตัวมี SQLite DB สร้างกลไก sync แยกต่างหากเพื่อกระจายกฎใหม่จาก admin panel ไปยังแต่ละ WAF+SQLite server และเมื่อมี request ใหม่เข้ามา WAF จะตรวจสอบอย่างรวดเร็วด้วยกฎใน SQLite ที่อัปเดตแล้ว ใช่โครงสร้างแบบนี้ไหม

  • ประโยคที่ดีที่สุดคือ “SQLite ไม่ได้แข่งกับฐานข้อมูลแบบ client/server แต่ SQLite แข่งกับ fopen()