- Rails 8 ได้ ถอดการพึ่งพา Redis ออกจากสแตกพื้นฐาน และเปลี่ยนไปประมวลผลงานทั้งหมดบน ฐานข้อมูลเชิงสัมพันธ์ (RDB) ผ่าน SolidQueue·SolidCache·SolidCable
- แม้ Redis จะรวดเร็วและเสถียร แต่ก็ก่อให้เกิด ความซับซ้อนในการปฏิบัติการ เช่น การตั้งค่า ความปลอดภัย การจัดการคลัสเตอร์ และการสำรองข้อมูล
- SolidQueue ใช้ความสามารถ
FOR UPDATE SKIP LOCKED ของ PostgreSQL เพื่อทำ การประมวลผลงานแบบขนานโดยไม่เกิดการแข่งขันกัน
- มีฟีเจอร์แบบเสียเงินของ Redis+Sidekiq อย่าง งานตามรอบ การควบคุมการทำงานพร้อมกัน และแดชบอร์ดมอนิเตอร์ริง (Mission Control) ให้ใช้ ฟรี
- แอป Rails ส่วนใหญ่ใช้ SolidQueue ก็เพียงพอ และมีเพียงบางกรณีที่ต้องการ การประมวลผลความเร็วสูงมากหรือแบบเรียลไทม์ เท่านั้นที่ยังควรใช้ Redis
ต้นทุนแฝงของ Redis
- นอกเหนือจากค่าโฮสติ้งแบบพื้นฐานแล้ว Redis ยังมีภาระในการดูแลต่อเนื่อง เช่น การติดตั้ง การบำรุงรักษา การตั้งค่าความปลอดภัย และการจัดการ HA cluster
- จำเป็นต้องมี การเชื่อมต่อเครือข่ายและการตั้งค่าไฟร์วอลล์ระหว่าง Rails กับ Redis, การยืนยันตัวตนของไคลเอนต์, และ การออร์เคสเตรตโปรเซส Sidekiq
- เมื่อเกิดปัญหา ต้อง ดีบักทั้ง Redis และ RDBMS พร้อมกัน และยังต้องมี กลยุทธ์สำรองข้อมูลแบบสองชุด
- ในทางกลับกัน สแตก Rails ที่ไม่มี Redis จะต้องดูแลเพียง PostgreSQL ตัวเดียว ทำให้เรียบง่ายกว่า
หลักการทำงานของ SolidQueue
- ใช้ความสามารถ
FOR UPDATE SKIP LOCKED ของ PostgreSQL เพื่อให้เวิร์กเกอร์หลายตัวหยิบงานได้พร้อมกันโดยไม่เกิด lock contention
- โครงสร้างตารางหลัก
solid_queue_jobs: เก็บเมทาดาทาของงาน
solid_queue_scheduled_executions: รอคิวงานที่ถูกตั้งเวลาไว้
solid_queue_ready_executions: คิวงานที่พร้อมรัน
- โปรเซส worker·dispatcher·scheduler·supervisor จะโพลตารางคนละชุดเป็นระยะและทำงานร่วมกัน
- ด้วยการออกแบบ MVCC และ autovacuum ของ PostgreSQL จึงรองรับงานแทรกและลบจำนวนมากได้อย่างเสถียร
การตั้งเวลางานซ้ำ
- SolidQueue รองรับ งานซ้ำแบบ cron มาให้ในตัว และกำหนดค่าได้ผ่านไฟล์
config/recurring.yml
- ตัว scheduler จะนำงานที่ถึงเวลารันเข้าไปในคิว และ ตั้งเวลารอบถัดไปให้อัตโนมัติ
- ใช้ไลบรารี Fugit สำหรับพาร์สตารางเวลาแบบภาษาธรรมชาติ และใช้ Concurrent::ScheduledTask เพื่อสร้างเธรด
- ยืมแนวทางการตั้งเวลาแบบ deterministic ของ GoodJob มาใช้ ทำให้ ตารางเวลายังคงเดิมแม้โปรเซสจะรีสตาร์ต
ฟีเจอร์ควบคุมการทำงานพร้อมกัน
- SolidQueue ใช้ แพตเทิร์น POSIX semaphore เพื่อรองรับ การจำกัดจำนวนการทำงานพร้อมกันในระดับงาน
- ตัวอย่าง: เมื่อตั้งค่า
limits_concurrency to: 1, key: ->(user) { user.id } จะอนุญาตให้รันงานได้เพียง 1 งานต่อผู้ใช้
- สามารถกำหนดเวลาหมดอายุของ semaphore (
duration) เพื่อ ป้องกันงานชนกันและ deadlock
- ตารางที่เกี่ยวข้อง
solid_queue_semaphores: ติดตามข้อจำกัดการทำงานพร้อมกัน
solid_queue_blocked_executions: เก็บงานที่กำลังรอ
การมอนิเตอร์ด้วย Mission Control
- Mission Control Jobs คือแดชบอร์ดโอเพนซอร์สฟรีสำหรับ Rails 8 ที่สามารถ mount ได้ง่ายที่พาธ
/jobs
- ฟีเจอร์หลัก
- สถานะคิวแบบเรียลไทม์ การติดตามงานที่ล้มเหลว การสั่ง retry/ทิ้งงาน
- การแสดงไทม์ไลน์ของงานที่ตั้งเวลาและงานซ้ำ
- กราฟ throughput และเมตริกแยกตามคิว
- รองรับการสืบค้นด้วย SQL ทำให้ วิเคราะห์จากฐานข้อมูลได้โดยตรงโดยไม่ต้องใช้เครื่องมือเพิ่ม
การย้ายจาก Sidekiq ไปสู่ SolidQueue
- ขั้นที่ 1: ตั้งค่า
config.active_job.queue_adapter = :solid_queue
- ขั้นที่ 2: รัน
bundle add solid_queue จากนั้นรัน rails solid_queue:install และ db:migrate
- ขั้นที่ 3: แปลงตาราง cron ใน
sidekiq.yml เป็น recurring.yml
- ขั้นที่ 4: เพิ่ม
jobs: bundle exec rake solid_queue:start ลงใน Procfile
- ขั้นที่ 5: ลบ gem ที่เกี่ยวข้องกับ Redis·Sidekiq
- โค้ด ActiveJob เดิมสามารถทำงานต่อได้โดยไม่ต้องแก้ไข
กรณีที่ยังจำเป็นต้องใช้ Redis
- มีงานต่อเนื่องระดับหลายพันรายการต่อวินาที
- ระบบเรียลไทม์ที่ต้องการ latency ต่ำกว่า 1ms
- ต้องการ โครงสร้าง pub/sub ที่ซับซ้อน หรือ rate limiting และการนับค่าแบบละเอียด
- ตัวอย่างเช่น Shopify มีการประมวลผล 833 คำขอต่อวินาที และใช้เวิร์กเกอร์โปรเซส 1,172 ตัว พร้อมโครงสร้างพื้นฐาน Redis
คู่มือการติดตั้งใช้งานจริง
- เมื่อสร้างแอปใหม่บน Rails 8 จะมีการตั้งค่า SolidQueue·SolidCache·SolidCable ให้อัตโนมัติ
- แนะนำให้ตั้งค่า การเชื่อมต่อฐานข้อมูลสำหรับ queue แยกต่างหาก ใน
config/database.yml
- เพิ่มการยืนยันตัวตนให้ Mission Control และ mount route
/jobs
- เพิ่ม
jobs: bundle exec rake solid_queue:start ลงใน Procfile.dev แล้วรัน bin/dev เพื่อสตาร์ตทั้งระบบ
- หลังสร้างงานทดสอบแล้ว สามารถตรวจสอบสถานะได้ใน Mission Control
ปัญหาที่พบบ่อยและวิธีแก้
- สามารถใช้ ฐานข้อมูลเดียว ได้เช่นกัน แต่ความยืดหยุ่นในการปฏิบัติการจะลดลง
- ใน Mission Control บน production จำเป็นต้องเพิ่มการยืนยันตัวตนเสมอ
- ค่าเริ่มต้นของ polling interval คือ 1 วินาทีสำหรับงานที่ตั้งเวลาไว้ และ 0.2 วินาทีสำหรับงานทันที ซึ่งเหมาะกับแอปส่วนใหญ่
- หากใช้ ActionCable/Turbo Streams ต้องตั้งค่า
SolidCable ให้เชื่อมต่อฐานข้อมูลแยกต่างหาก
การขยายระบบและประสิทธิภาพ
- SolidQueue สามารถขยายระบบได้เพียงพอสำหรับแอป Rails ส่วนใหญ่
- บนพื้นฐาน PostgreSQL สามารถประมวลผลงานได้ 200~300 งานต่อวินาที และ 37signals ประมวลผล 20 ล้านงานต่อวัน ได้โดยไม่ใช้ Redis
- ตารางเปรียบเทียบ
| รายการ |
Redis + Sidekiq |
SolidQueue |
| ความซับซ้อนในการตั้งค่า |
ต้องใช้บริการแยก |
ใช้ DB ที่มีอยู่ |
| ภาษาสำหรับคิวรี |
คำสั่ง Redis |
SQL |
| การมอนิเตอร์ |
ต้องมีแดชบอร์ดแยก |
Mission Control |
| สถานการณ์เมื่อเกิดปัญหา |
มากกว่า 6 แบบ |
2 แบบ |
| Throughput |
หลายพันรายการ/วินาที |
200–300 รายการ/วินาที |
| เหมาะกับใคร |
99.9% ของแอป |
95% ของแอป |
บทสรุป
- Redis และ Sidekiq เป็นเทคโนโลยีที่ยอดเยี่ยม แต่สำหรับ แอป Rails ส่วนใหญ่แล้วก่อให้เกิดความซับซ้อนและต้นทุนที่มากเกินไป
- SolidQueue ทำให้เกิด ความเรียบง่ายในการปฏิบัติการ การลดต้นทุน และการดูแลรักษาที่มีประสิทธิภาพ ด้วยแนวทางฐานข้อมูลเดียว
- ในยุคของ Rails 8 จึงแนะนำให้ เปลี่ยนมาใช้ SolidQueue เป็นตัวเลือกพื้นฐาน
2 ความคิดเห็น
Redis ก็ดีนะ
ความเห็นจาก Hacker News
คิดว่า ผู้เขียนโอเพนซอร์สทุกคนมีสิทธิ์ควบคุมขอบเขตของโปรเจ็กต์ตัวเอง
แต่ทีมของเรากลับเสียใจกับการเปลี่ยนจาก good_job ไปเป็น SolidQueue
Basecamp เน้น MySQL เป็นหลัก จึงไม่รับ คิวรีที่เฉพาะกับเอนจิน RDBMS ถ้าไปดู GitHub issue จะเห็นว่าโฟกัสอยู่แค่ประสิทธิภาพของ MySQL
อีกทั้งยังไม่มี การรองรับงานแบบแบตช์ (PR ที่เกี่ยวข้อง)
ใน JOIN ที่ซับซ้อน MySQL มักวาง query plan ผิด ฉันเลยใช้ STRAIGHT_JOIN เพื่อบังคับลำดับ เป็นการเตรียมไว้สำหรับอนาคต
ฉันกำลังเทียบสองตัวนี้เป็นตัวเลือกสำหรับย้ายมาจาก resque อยู่ GoodJob ใช้ ฟีเจอร์เฉพาะของ pg เลยเข้ากับ pgbouncer transaction mode ไม่ได้
มันต้องคง session ไว้ต่อเนื่องเลยค่อนข้างน่ารำคาญ แต่ประโยชน์ด้านประสิทธิภาพก็ไม่ได้มีนัยสำคัญมากสำหรับสเกลส่วนใหญ่
ถึงอย่างนั้น โมเดลการพัฒนาและความอ่านง่ายของโค้ด ของ GoodJob ก็ทำให้เชื่อมั่นได้มากกว่ามาก
ถ้าทำให้สภาพแวดล้อมโปรดักชันเรียบง่ายขึ้นได้ ก็เป็นเรื่องดีเสมอ
สำหรับ Rails ฉันคิดว่าสถานการณ์ที่เหมาะที่สุดคือ โครงสร้างที่สลับไป Redis ได้ง่าย
อยากให้เริ่มต้นด้วย SolidQueue แล้วถ้าไปชนเพดานด้านการขยายระบบก็ย้ายไป Redis ได้
แอป Rails ส่วนใหญ่ไม่ได้มีทราฟฟิกสูงมาก การต้องดูแลสองระบบกลับซับซ้อนกว่า
แน่นอนว่าบางแอปก็พึ่งพาการทำงานเฉพาะของคิวบางตัว แต่โดยทั่วไปเปลี่ยนแค่การตั้งค่าก็พอ
มันใช้ snapshot ควบคู่กันเพื่อไม่ให้ log โตเกินไปไหม และอยากรู้ว่ามันใช้ได้ใน โหมดกระจาย ด้วยหรือไม่
โดยเฉพาะเวลาที่การสร้างงานเกิดขึ้นพร้อมกับการเปลี่ยนแปลง DB อื่น ๆ การเสียการรับประกันแบบนั้นเป็นปัญหา
Redis ได้เปรียบในจุดนี้เพราะเป็น state store ที่เบาและแยกอิสระ
ดูเหมือน SolidQueue จะไม่ได้ทำให้การแยกแบบนี้ชัดเจน (riverqueue.com)
ฉันลองทดลองใช้ SolidQueue ใน side project
สรุปคือ ถ้า Sidekiq ไม่ได้มีปัญหา ก็ไม่มีเหตุผลต้องเปลี่ยน
ค่อยพิจารณาเฉพาะตอนที่อยากเลิกใช้โครงสร้างพื้นฐาน Redis
ถ้าเป็นโปรเจ็กต์ใหม่ GoodJob ดูสุกงอมกว่าและชุมชนก็ดีกว่า
ฉันไม่ชอบตรงที่ UI ของ SolidQueue เรียบง่ายเกินไป และไม่มีการปรับดัชนีให้เหมาะสม พอข้อมูลเยอะหน้าก็ค้าง
อีกเรื่องที่ต้องคิดคือการใช้ RDBMS จะเพิ่ม ต้นทุนการจัดการ connection pool
สำหรับคนที่กังวลเรื่องการขยายระบบ ถ้าไปดู benchmark ของ Oban ใน Elixir
มันประมวลผลงานได้หนึ่งล้านงานต่อนาทีบนโหนดเดียว ซึ่งแอปส่วนใหญ่มีปริมาณงานน้อยกว่านั้นมาก
มันเป็นโครงสร้างที่ใส่งานทีเดียว 5000 งานแบบแบตช์ ดังนั้น TPS จริง ๆ แค่ราว 200
ถ้าใส่งานทีละงานโดยไม่มีแบตช์ ภาระของ SQL transaction จะสูงกว่านี้มาก
เราเก็บ งานไว้ใน DB มาตั้งแต่ก่อนมี SolidQueue
ข้อดีคือสามารถทำ snapshot สถานะโปรดักชันมาที่สภาพแวดล้อมพัฒนาได้ตรง ๆ
แต่ rate limiter ยังไว้บน Redis เพื่อไม่ให้ DB รับภาระหนักเกินไป
ข้อจำกัดของคิวที่อิง DB คือ payload ขนาดใหญ่
ถ้าใส่ JSON ก้อนใหญ่ลงคิวจะไม่มีประสิทธิภาพเพราะมี overhead ฝั่งเขียน DB
Redis (Sidekiq) เร็วกว่าเยอะในกรณีแบบนี้
SolidQueue+SQLite ก็โอเคถ้าใช้แค่ ส่งผ่าน primary key
แต่ถ้ามี worker หลายตัว polling DB เดียวกัน ก็จะกลายเป็นคอขวดอย่างรวดเร็ว
ฉันคิดว่าควรเก็บข้อมูลใหญ่ไว้ใน สตอเรจภายนอกอย่าง S3 แล้วส่งแค่ตัวอ้างอิงจะดีกว่า
ไม่ทราบว่ามีข้อมูลที่สรุปผล benchmark ไว้หรือเปล่า
SolidQueue พูดถึง SKIP LOCKED แต่การคง transaction ไว้สำหรับงานที่ใช้เวลา 15 นาทีเป็นเรื่องเสี่ยง
transaction ที่เปิดค้างนานจะทำลายประสิทธิภาพ DB และเปราะบางต่อการหลุดของเครือข่าย
โครงสร้างแบบนี้อาจนำไปสู่ anti-pattern ได้ ทีหลังถึงได้เห็นว่าเหมือนจะใช้วิธี lease
เห็นด้วยกับแนวคิด Postgres for everything
ฉันคิดว่าการ รวมทุกอย่างไว้ที่ PostgreSQL ตัวเดียว เป็นเรื่องดี
ไม่รู้จะโต้แย้งอุปมานี้ยังไงดี
เลยสงสัยว่ายังมีเหตุผลอะไรที่ต้องใช้ Redis ทั้งที่เพิ่มความซับซ้อน
“ธุรกิจที่ latency ต่ำกว่า 1ms สำคัญ” นี่หมายถึงทำ HFT ด้วย Rails เหรอ?
Postgres จะครองโลก