ตอนทำงานบนโปรเจกต์ใหม่ เราตัดสินใจตลอดเวลาที่จะทำให้ scale ภายหลังง่ายหรือยาก บางครั้งเลือก short-term gain เพื่อส่งเร็ว แต่บางครั้งก้าว technical debt เพราะไม่รู้ว่ามีทางเลือก
ผมกล่าวได้เพราะที่ Honeybadger เราทำบางอย่างทำให้ชีวิตยากกว่าที่ควร หากเข้าใจจุดสำคัญไม่กี่จุด scaling จะน้อยเจ็บปวดมาก
ใช้ UUIDs
พูด "primary key" คนส่วนใหญ่คิด auto-increment number ใช้ได้ดีระบบเล็ก แต่ introduce ปัญหาใหญ่ตอน scale
ในเวลาใดเวลาหนึ่ง database server เพียงตัวเดียว generate primary key หมายถึง ทั้งหมด writes ต้องผ่าน server เดียว แย่มากหากอยากทำ thousands of writes ต่อวินาที
ใช้ UUIDs เป็น primary keys หลีกเลี่ยงปัญหานี้ UUIDs เป็น unique identifiers 长这样: 123e4567-e89b-12d3-a456-426655440000
Wikipedia อธิบาย:
เมื่อสร้างตาม standard methods UUIDs เป็น unique สำหรับการใช้งานจริง ไม่ต้องการ central registration authority หรือ coordination ระหว่างฝ่ายที่สร้าง ความน่าจะเป็นที่ UUID จะซ้ำไม่ได้เป็นศูนย์ แต่ใกล้ศูนย์จนสามารถมองข้ามได้
ดังนั้น ใครก็สร้าง UUID ใช้ระบุสิ่งใดสิ่งหนึ่งได้ด้วยความแน่ใจเกือบเต็มว่า identifier ไม่ซ้ำกับที่สร้างไว้ก่อน และไม่ถูกสร้างซ้ำในอนาคต ข้อมูลที่ติดป้าย UUID โดยฝ่ายอิสระจึงรวมเป็น database เดียว หรือส่งผ่าน channel เดียวกันได้โดยไม่ต้อง resolve conflicts ระหว่าง identifiers
ใช้ UUIDs เป็น primary keys writes ไม่ต้องผ่าน database เดียว แทนกระจายไปหลาย servers
นอกจากนี้ยืดหยุ่นทำได้เช่น generate id ของ record ก่อน save ลง database อาจมีประโยชน์หากอยากส่ง record ไป cache หรือ search server แต่ไม่อยากรอ database transaction
เปิด UUIDs โดย default ใน Rails apps ง่าย แก้ไข config file:
# config/application.rb
config.active_record.primary_key = :uuid
ใช้ UUIDs สำหรับตารางเดี่ยวกับ Rails migrations:
create_table :users, id: :uuid do |t|
t.string :name
end
เป็นแค่ config option หากเปิดตอนเริ่มพัฒนา จะประหยัดอัปมวยตอนพยายาม scale โบนัส: ทำให้ bots และ bad actors ยากเดา private URLs
Counts และ counters
เริ่มมองจะเห็น counts และ counters ทุกที่ email client แสดง unread emails blogs มี pagination footer ใช้ total posts คำนวณจำนวนหน้า
มีสองปัญหา scaling กับ counters:
- Database queries อย่าง
select count(*) from usersช้าตัวตน มัน loop ทุก record ใน recordset ผลลัพธ์ หากมีล้าน records จะใช้เวลานาน - พยายามเร่ง counters ด้วย "counter caches" ใช้ได้ แต่จำกัดความสามารถกระจาย writes หลาย database servers
วิธีแก้ง่ายที่สุด: หลีกเลี่ยง counters ที่ทำได้ มากขึ้นตอน initial design
เช่น อาจเลือก paging by date-range แทน count อาจเลือกไม่แสดงสถิติประโยชน์นิดหน่อยที่จะเป็น地獄ตอน generate ภายหลัง คุณเข้าใจแล้ว
หมดอายุ & เก็บข้อมูลเก่า (Expiring & warehousing data)
มี upper limit ข้อมูลที่เก็บใน single postgres table ได้ ขึ้นกับ RAM, CPU และ disk IO หมายถึงจะมีเวลาต้องย้าย stale data ออกจาก main tables
ดู case 簡單: อยากลบ records มากกว่า 1 ปี ใน database หลาย gigabytes
หากไม่เคย deal ปัญหานี้ อาจลured ทำแบบนี้:
MyRecords.where("created_at < ?", 1.year.ago).destroy
ปัญหา: query นี้รันนานวันหรือสัปดาห์ Database ใหญ่เกินไป
ปัญหาเจ็บปวดมาก เพราะบ่อยครั้งไม่รู้ว่ามีจนช้เกินไป ไม่คิด data purging strategies ตอนบริษัทเล็ก database มี 1,000 records
หากวางแผนล่วงหน้า มีวิธีแก้ง่าย: แค่ partition tables แทนเขียนทั้งหมดไป my_records เขียนข้อมูลสัปดาห์นี้ไป my_records_1 สัปดาห์หน้า my_records_2 ตอนลบสัปดาห์ที่แล้ว แค่ drop table my_records_1 query นี้เสร็จเร็วมาก ไม่เหมือน delete
Partition ด้วย fields อื่นได้ ไหนที่เหมาะกับ use-case
มี postgres extension ชื่อ pg_partman จัดการรายละเอียดทั้งหมด และ partition database ได้โดยไม่เปลี่ยนโค้ดบรรทัดไหน หรือถ้าชอบจัดการ partition ใน Ruby มี gem partitionable
คำส่งท้าย
ครั้งหน้าที่สร้างโปรเจกต์จากศูนย์ ขอแนะนำให้ใช้เวลาคิด scaling หน่อย อย่า obsessed อย่าใช้วันกังวล HAML หรือ ERB แต่ถามตัวเองว่า ไม่มี easy wins ที่หยิบได้โดยแค่วางแผนล่วงหน้า