2 คะแนน โดย GN⁺ 2023-11-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • PR #1705 สำหรับรีแฟกเตอร์ PDS ของ Bluesky atproto เปลี่ยนให้ PDS ใช้ ดาต้าสโตร์ SQLite แบบ single-tenant และเปลี่ยนให้เก็บ repo ของผู้ใช้แต่ละคนกับสถานะบัญชีส่วนตัวไว้ในไฟล์ SQLite แยกของแต่ละคน
  • DB ของผู้ใช้จะถูกเก็บในโครงสร้างพาธ /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did} และ คีย์สำหรับลงนาม ของแต่ละ repo จะถูกเก็บไว้ข้างไฟล์ SQLite นั้น
  • abstraction สำหรับเข้าถึงข้อมูลผู้ใช้เดิมถูกแทนที่ด้วย ActorStore และเนื่องจาก SQLite ไม่รองรับ transaction พร้อมกันหลายรายการ งานเขียนจึงต้องทำ transaction กับ store อย่างชัดเจน
  • แฮนเดิลไฟล์ DB ที่เปิดอยู่และคีย์สำหรับลงนามถูกจัดการด้วย LRUCache โดยคงแฮนเดิลไฟล์ที่เปิดอยู่ได้สูงสุด 30k รายการและคีย์ 30k รายการไว้ในหน่วยความจำ และเมื่อ DB ถูกดันออกจากแคชก็จะปิดแฮนเดิลไฟล์
  • มีการนำ SQLite DB แยก 3 ชุดมาใช้สำหรับจัดการสถานะบริการ และรันในโหมด WAL เพื่อให้สามารถอ่านพร้อมกันและทำ streaming replication ได้ โดยมีแผนจะรวม Litestream หรือเครื่องมือคล้ายกันไว้ในดิสทริบิวชัน PDS

การเปลี่ยนแปลงหลักของ PR

  • PR #1705 รีแฟกเตอร์ PDS ให้ใช้ ดาต้าสโตร์ SQLite แบบ single-tenant
  • ผู้ใช้แต่ละคนมีไฟล์ SQLite เฉพาะของตนเอง และไฟล์นี้เก็บ repo ของผู้ใช้นั้นกับสถานะบัญชีส่วนตัว
  • DB ของผู้ใช้ถูกเก็บในพาธแบบลำดับชั้นโดยใช้แฮช DID
    • รูปแบบพาธ: /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
  • repo signing key ของแต่ละ repo ถูกเก็บไว้ในตำแหน่งเดียวกับไฟล์ SQLite

ActorStore และโมเดล transaction

  • abstraction สำหรับเข้าถึงข้อมูลผู้ใช้เปลี่ยนจาก “services” เดิมมาเป็น ActorStore
  • ความแตกต่างหลักของ ActorStore คือมีการแยกคลาสสำหรับการอ่านและการเขียน
  • เนื่องจาก SQLite ไม่รองรับ transaction พร้อมกันหลายรายการ หากต้องการทำงานเขียนจะต้องทำ transaction กับ store อย่างชัดเจน
  • บันทึก commit มีการทำ reader และ transactor ใหม่, จัดการ race ของ transaction ใน actor store, และจัดระเบียบ interface ของ store เป็นต้น

การจัดการแคชและแฮนเดิลไฟล์

  • มีการคง LRUCache สำหรับคีย์ลงนามและฐานข้อมูล
  • ขีดจำกัดที่ตั้งไว้มีดังนี้
    • แฮนเดิลไฟล์ที่เปิดอยู่สูงสุด 30k รายการ
    • คีย์ที่เก็บไว้ในหน่วยความจำสูงสุด 30k รายการ
  • เมื่อฐานข้อมูลถูกดันออกจากแคช จะปิดแฮนเดิลไฟล์
  • commit ที่เกี่ยวข้องมี actor store in lru cache, fix open handles

SQLite DB 3 ชุดสำหรับสถานะบริการ

  • นอกจาก DB แยกตามผู้ใช้แล้ว ยังมีการนำฐานข้อมูล SQLite แยก 3 ชุดมาใช้สำหรับจัดการสถานะบริการ
    • service DB: จัดการข้อมูลบัญชี, invite code, refresh token เป็นต้น
    • did cache DB: มีเพียงตารางเดียวสำหรับแคช DID resolution
    • sequencer DB: มีเพียงตารางเดียวสำหรับจัดการลำดับการอัปเดต repo ทั้งหมดของบริการหนึ่ง
  • ไฟล์ SQLite แต่ละไฟล์รันใน WAL mode
  • เป้าหมายของ WAL mode คือทำให้สามารถอ่านพร้อมกันและทำ streaming replication ได้
  • มีแผนจะรวม Litestream หรือเครื่องมือคล้ายกันไว้ในดิสทริบิวชัน PDS

สถานะการรีวิวและการ merge

  • PR นี้ประกอบด้วย 143 commits และถูก merge จากบรานช์ pds-sqlite-refactor เข้าสู่บรานช์ pds-v2
  • วันที่ merge คือ 1 พฤศจิกายน 2023 และ commit สำหรับ merge คือ 8449ceb
  • ผู้รีวิว devinivy ทิ้งโน้ตและคอมเมนต์หลายรายการก่อนอนุมัติการเปลี่ยนแปลง
  • devinivy ประเมินการรีแฟกเตอร์นี้ว่า มี “การทำให้ง่ายขึ้นที่ยอดเยี่ยมจำนวนมาก” และโดยรวมให้ความรู้สึกเป็นระเบียบ
  • หลัง merge แล้ว บรานช์ pds-sqlite-refactor ถูกลบ

คำถามภายหลัง

  • เมื่อวันที่ 28 กุมภาพันธ์ 2025 npetrangelo ตรวจดูขนาดของการเปลี่ยนแปลงใน PR นี้ และขอให้สรุป trade-off ระหว่าง สถาปัตยกรรม Postgres เดิมกับสถาปัตยกรรม SQLite ที่ PR นี้นำเข้ามา
  • เนื้อหาที่ให้มาไม่มีคำตอบจากฝั่ง Bluesky ต่อคำถามดังกล่าว

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

 
GN⁺ 2023-11-08
ความคิดเห็นจาก Hacker News
  • ผมชอบ SQLite แต่แนวทางที่ แยกสคีมาหรือฐานข้อมูลสำหรับแต่ละ tenant โดยทั่วไปมีความยุ่งยากมาก
    ถ้าใช้ row-level security (RLS) บนอินสแตนซ์ที่ใช้ร่วมกัน ต่อให้ migration ล้มเหลวก็สามารถ rollback ทั้งหมดได้ แต่ในกรณีสคีมาแยกตาม tenant ถ้า data migration ล้มเหลวเพราะข้อมูลที่คาดไม่ถึง ผู้ใช้จะค้างอยู่ในสคีมาคนละเวอร์ชันจนกว่าจะหาสาเหตุเจอ
    พอไปถึงระดับที่ต้อง sharding ก็อาจเกิดเรื่องคล้ายกันอยู่ดี แต่ก่อนถึงจุดนั้น ฐานข้อมูลเดียว จะง่ายที่สุด และภายหลังก็อาจต้องรวมข้อมูลหรือย้าย ownership ของทรัพยากรแบบ atomic
    ไม่ได้คัดค้านการจัดวางแบบนี้นะ มันมีกรณีที่เหมาะกับการใช้ แต่ที่บริษัทเรากำลังเร่งหนีออกจากสคีมาแยกตาม tenant แบบเต็มกำลัง ถ้าไม่ลงทุนทำให้ดี ปัญหาจะเยอะมาก และผมคิดว่าตอนเริ่มคิดไอเดีย คนส่วนใหญ่มักยังไม่พร้อมสำหรับเรื่องนี้
    ที่น่าสนใจคือราว 10 ปีก่อน แอปเริ่มจาก SQLite แยกตาม tenant แล้วย้ายไปเป็นสคีมาแยกตาม tenant บน PostgreSQL และตอนนี้กำลังไปสู่สคีมาเดียวที่มี RLS เรียกได้ว่าเดินไปในทิศทางตรงกันข้ามอย่างสิ้นเชิง

    • ในฐานะคนที่เคยดูแล ฐานข้อมูลขนาดมหึมา ใน production ผมไม่อยากทำอีกแล้ว
      เมื่อโหลดใหญ่พอ การเปลี่ยนแปลงทุกอย่างจะกลายเป็นความเสี่ยง เพราะเราไม่สามารถทดสอบกรณีสุดขั้วด้านประสิทธิภาพได้ครบถ้วนทั้งหมด
      รูปแบบที่เจอบ่อยคือผู้ใช้ free tier ไปเจอ code path ที่ไม่มีดัชนี แล้วทำให้ production พัง

    • การที่ data migration ล้มเหลวแล้วผู้ใช้บางส่วนค้างอยู่คนละเวอร์ชันของสคีมา อาจไม่ใช่ปัญหาใหญ่ก็ได้
      ถ้าเป็นบริการที่ใหญ่และซับซ้อนขนาดนั้น โดยปกติจะอัปเกรดสคีมาเป็นขั้น ๆ: 1. ทำให้โค้ดเข้ากันได้กับสคีมาในอนาคต, 2. migrate ข้อมูล, 3. เอาการรองรับสคีมาเก่าออก
      ดังนั้นโดยทั่วไปควรปลอดภัยแม้จะรันอยู่ในสถานะระหว่างขั้นที่ 1 กับ 2 เป็นเวลานาน แน่นอนว่าบั๊กใหม่เป็นข้อยกเว้น แต่จากมุมมองการปฏิบัติการ ตราบใดที่ใช้กระบวนการแบบนี้ ผมมองว่าระบบที่ย้อนกลับไปอยู่ใน สถานะกลางระหว่าง migration ก็ยังโอเค

    • ถ้าลูกค้าของผลิตภัณฑ์มีน้อยกว่า 100 ราย การให้ผู้ใช้อยู่คนละเวอร์ชันของสคีมาอาจเป็นเรื่องดีด้วยซ้ำ
      ลูกค้าแต่ละรายอาจมีตารางเวลาและความต้องการในการอัปเกรดต่างกัน และผมก็รู้จักธุรกิจที่ทำงานปรับแต่งให้ลูกค้าบางรายจนในทางปฏิบัติไม่ได้รันโค้ดเดียวกันด้วยซ้ำ
      สุดท้ายแล้วขึ้นอยู่กับ โครงสร้างธุรกิจ

    • พูดอย่างเป็นธรรม เมื่อ 10 ปีก่อนยังไม่มี RLS มันเปิดตัวใน PostgreSQL 9.5 เมื่อปี 2016

    • https://blog.turso.tech/introducing-embedded-replicas-deploy...

      https://electric-sql.com/

  • ผมไม่แน่ใจว่าคำว่า “SQLite ไม่รองรับธุรกรรมพร้อมกัน” หมายถึงอะไร
    เท่าที่รู้ มันรองรับตราบใดที่ไม่ได้เข้าถึงไฟล์ .db ผ่าน file share อย่าง UNC หรือ NFS: https://www.sqlite.org/wal.html
    ผมเคยใช้มันอ่านและอัปเดตฐานข้อมูลจากหลายเธรด/โปรเซสบนเครื่องเดียวกัน และถ้าต้องการมุมมองที่สอดคล้องกันหรือไม่อยากถือ transaction ไว้นาน ก็สามารถใช้ sqlite backup API ทำ snapshot ได้
    อาจมีอะไรที่ผมพลาดไปก็ได้ และผมไม่ได้แตะ SQLite มาหลายปีแล้ว เลยไม่มั่นใจ

    • ไม่ใช่แล้ว ผมเข้าใจผิด จริง ๆ แล้วมันใกล้เคียงกับ อ่านหลายราย เขียนรายเดียว
      ดูเหมือนว่าผมจะสมมติแบบนั้นมาตลอดและไม่ได้ตรวจสอบให้ละเอียดพอ แต่ฐานข้อมูลส่วนใหญ่ที่ผมทำด้วย SQLite ก็เน้นอ่านมากกว่าเขียน
      ขอแก้ไข

    • ถ้ารอไปอีกหน่อย hctree [1] จะเสถียรขึ้น และจะเลือกได้ระหว่างกลไก backend แบบดั้งเดิมกับ backend ที่รองรับ concurrency ซึ่งเพิ่งนำมาใช้งานใหม่

      [1] https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html

    • ตามเอกสาร writer แค่ต่อท้ายเนื้อหาใหม่ที่ปลายไฟล์ WAL ทำให้อ่านและเขียนพร้อมกันได้ แต่เพราะมีไฟล์ WAL เพียงไฟล์เดียว จึงมี writer ที่เขียนพร้อมกันได้เพียงรายเดียว
      สิ่งที่บทความต้นฉบับพูดน่าจะหมายถึงงานอัปเดตต้องรันแบบลำดับ

    • ถ้า traffic ต่ำก็ทำงานได้ แต่เมื่อ transaction ใหญ่ขึ้นหรือจำนวนการเขียนพร้อมกันเพิ่มขึ้น ต่อให้เปิด WAL ก็จะถึงจุดที่เกิดปัญหา database locked
      สามารถเลี่ยงได้ในระดับแอปพลิเคชันระดับหนึ่ง แต่โดยทั่วไปถ้าถึงจุดนั้นแล้วควรพิจารณา database backend อื่นอย่างจริงจัง

    • อย่างน้อยตอนที่ผมตรวจสอบครั้งล่าสุด น่าจะหมายถึงไม่มี row-level locking และ table-level locking ก็จำกัดมาก
      ตามเอกสาร writer ยังล็อกทั้งฐานข้อมูลอยู่

  • น่าสนใจ และผมชอบกลยุทธ์แบบ 1 ผู้ใช้ต่อ 1 ฐานข้อมูล
    แต่สงสัยว่าจัดการข้อมูลที่ต้อง aggregate ข้ามผู้ใช้อย่างไร เช่น ถ้าผม subscribe ผู้ใช้อื่นอยู่ แล้วผู้ใช้นั้นโพสต์ ฐานข้อมูลของผมจะอัปเดตด้วยโพสต์ใหม่อย่างไร หรือโครงสร้างนี้ใช้เฉพาะกับ ข้อมูลถาวร อย่างข้อมูลโปรไฟล์หรือความสัมพันธ์การ follow ส่วนข้อมูลเชิงโต้ตอบอย่าง feed แยกจัดการต่างหาก
    ผมชอบด้วยที่ “connection pooling” เป็นเพียงการจำกัดจำนวน handle ที่เปิดอยู่ด้วย LRU cache และก็น่าสนใจที่แต่ละ DB connection เป็น single-threaded จึงจัดการ concurrency ในระดับ tenancy ไม่ใช่ระดับ connection
    ดูเหมือนว่าจะวาง rate limit ต่อฐานข้อมูลไว้ด้านบนได้ง่าย เพื่อป้องกันการใช้งานผิดปกติของผู้ใช้บางราย
    ผมก็สงสัยเหมือนกันว่ามีวิธีง่าย ๆ ในการตั้งค่า Litestream สำหรับฐานข้อมูลจำนวนเท่าใดก็ได้หรือไม่

  • เห็นการนำ SQLite/Litestream มาใช้บนเซิร์ฟเวอร์มากขึ้นทีไรก็ดีใจเสมอ เราเองก็ใช้ตอนสร้างแอปใหม่อยู่
    SQLite + Litestream เป็นตัวเลือกที่ดีกว่าสำหรับฐานข้อมูลแบบ tenant และต้นทุนการ replicate/backup ไปยัง S3/R2 ถูกกว่าฐานข้อมูล managed บนคลาวด์ราคาแพงมาก [1]
    ถูกกว่า SQLServer บน Azure ได้สูงสุด 3900%

[1] https://docs.servicestack.net/ormlite/litestream

  • ไม่เข้าใจว่า ถูกลง 3900% หมายความว่าอย่างไร

  • ที่ทำงานฟินเทคเดิม บริษัทเก็บบัญชีลูกค้าเป็น ไฟล์ sqlite3 ที่เข้ารหัส ไว้ใน blob storage ซึ่งค่อนข้างเข้ากับรูปแบบการเข้าถึงข้อมูลได้ดี

    • สงสัยว่าตอนแก้ไขแล้วอัปโหลดกลับไป เขาจัดการ file lock กันอย่างไร
  • มองเผิน ๆ เหมือนเป็นการผสมกันของสิ่งที่แย่ที่สุดกับสิ่งที่น่ากลัวมาก
    อยากให้มีใครเขียนบทความดี ๆ ที่อธิบายข้อดีด้วยตัวเลขจริง และวิเคราะห์ข้อบกพร่องที่คาดได้ ถ้าได้เรียนรู้อย่างถูกต้อง นี่อาจเป็นหัวข้อที่น่าสนใจมากจริง ๆ

    • อธิบายได้ไหมว่าทำไมมันถึงดูเหมือน “การผสมกันของสิ่งที่แย่ที่สุดกับสิ่งที่น่ากลัวมาก”?
      มองเผิน ๆ แล้ว โดยเฉพาะถ้าสมมติว่ากำลังสร้าง distributed system ที่ผู้ใช้จำนวนมากซึ่งไม่ใช่ผู้ดูแลระบบมืออาชีพจะต้องรันและ deploy เอง ก็ดูเป็นตัวเลือกที่ค่อนข้างสมเหตุสมผล
      เป้าหมายตรงนี้ก็น่าจะเป็นแบบนั้น และคาดว่าการหลีกเลี่ยงความจำเป็นในการตั้งค่า กำหนดค่า และดูแลฐานข้อมูลเพิ่มเติมหรือเซิร์ฟเวอร์อื่น ๆ น่าจะเป็นเป้าหมายของการออกแบบ
  • อยากให้คนที่รู้จัก Bluesky ดีกว่านี้ช่วยอธิบายว่าข้อมูลอะไรถูกเก็บใน SQLite และข้อมูลอะไรไม่ถูกเก็บ
    เดาว่าคงไม่ใช่อย่างข้อความระหว่างผู้ใช้

    • ผมคิดว่าข้อความระหว่างผู้ใช้ก็ถูกเก็บไว้ใน ฐานข้อมูล SQLite เหล่านั้นด้วย
      ให้นึกถึงอีเมล ถ้าคุณส่งอีเมลและใส่ cc ไปห้าคน คนทั้งเจ็ดคนก็จะเก็บสำเนาอีเมลเดียวกันไว้ในเซิร์ฟเวอร์อีเมลของตัวเอง
      กล่าวคือไม่ใช่โครงสร้างที่มีฐานข้อมูลกลางหนึ่งตัวเก็บอีเมลฉบับหนึ่ง แล้วให้คนอื่น ๆ อ้างอิงถึงมัน
      การทำ sharding ฐานข้อมูลเชิงสัมพันธ์โดยพื้นฐานก็ทำงานทำนองนี้
      การ denormalize ข้อมูลแบบนี้แทบจะจำเป็นเมื่อแอปพลิเคชันขยายตัว โดยเฉพาะในแอปพลิเคชัน many-to-many ที่มีสัดส่วนการอ่านสูงกว่าการเขียนมาก
      ถ้าการอ่านเมื่อเทียบกับการเขียนมีไม่มาก โครงสร้างฐานข้อมูลเชิงสัมพันธ์แบบ single master กับหลาย slave ก็สามารถรองรับคำขอและข้อมูลได้มากอย่างน่าประหลาดใจ
    • จะมี โพสต์และคำตอบกลับ ทั้งหมดที่คุณในฐานะผู้ใช้เคยลงไว้
      ตอนนี้ Bluesky โฮสต์ PDS เองแทบจะเป็นรายเดียว แต่เป้าหมายสุดท้ายคือให้ผู้ใช้ปลายทางทุกคนมี PDS ของตัวเอง
      Inrupt/SOLID เรียกแนวคิดนี้ว่า “pod”
      ในทางปฏิบัติ เมื่อวานเพิ่ง onboard PDS สำหรับโปรดักชันตัวที่สอง ดังนั้นก็มีความคืบหน้าอยู่
    • ถ้าคำว่า message หมายถึง direct message หรือข้อความส่วนตัวระหว่างสองฝ่าย ปัจจุบัน Bluesky ยังไม่มีฟีเจอร์นั้น
      มีแต่ข้อความสาธารณะที่ broadcast ไปทั่วโลก
      ผมไม่ได้ไปค้นเพิ่มเติมว่ามีแผนทำ direct message หรือไม่
  • ทำไมถึงเอาผู้ใช้ไป hash ด้วย sha256 แล้วแบ่งเป็น ไดเรกทอรีปลายทางสองตัวอักษร?
    md5 เร็วกว่ามากและแก้ปัญหาเดียวกันไม่ใช่หรือ?

    • ถ้าให้เดา hash นั้นน่าจะถูกคำนวณไม่บ่อยนัก ดังนั้นความต่างด้านประสิทธิภาพจึงถูกกลบจนเป็นแค่ noise
      คุณค่าในการไม่ต้องตอบคำถามว่า “ทำไมถึงใช้ hash ที่ไม่ปลอดภัย” และการตัดหรือลดความเป็นไปได้ของปัญหาด้านความปลอดภัยบางประเภทนั้นมากกว่า
    • ในสเกลนั้นอาจต้องกังวลเรื่อง collision ก็ได้
      หรือไม่ก็เหมือนผมที่โดนเครื่องมือความปลอดภัยของบริษัทถล่มจนไม่อยากสร้างข้อยกเว้นแยกสำหรับการใช้ md5 ทุกครั้ง
    • การปล่อยให้ cryptographic hash ที่ถูกทำลายแล้ววิ่งไปมาไม่ใช่เรื่องดีต่อสุขภาพระบบ
      ถ้าไม่ต้องการ security hash ก็มี hash แบบ non-cryptographic ที่เร็วอยู่มากมาย
    • นี่น่าจะเป็นเพราะ ข้อจำกัดของ filesystem มากกว่าเรื่อง collision คือจำนวนไฟล์สูงสุดในหนึ่งไดเรกทอรี
  • Bluesky ยังเป็นระบบเชิญเท่านั้นอยู่ไหม?

    • ใช่ แต่ไม่ใช่เพราะเหตุผลอย่าง “growth hacking”
      เป็นวิธีจำกัดการเติบโตระหว่างที่ขยายระบบในด้าน backend และการป้องกันการใช้งานในทางที่ผิด
      มีคิวเฉพาะสำหรับนักพัฒนา และน่าจะได้รับสิทธิ์เข้าถึงค่อนข้างเร็ว: https://atproto.com/blog/call-for-developers