2 điểm bởi GN⁺ 2024-12-17 | Chưa có bình luận nào. | Chia sẻ qua WhatsApp
  • Khi chạy nhiều instance SQLite cùng nhau trong môi trường serverless·edge, độ chờ I/O đồng bộ làm tăng tail latency; các nhà nghiên cứu từ Helsinki và Cambridge đã thử nghiệm cách giảm điều này bằng I/O bất đồng bộ và tách biệt lưu trữ
  • Linux io_uring thông qua hàng đợi gửi và hàng đợi hoàn tất cho phép ứng dụng tiếp tục làm việc khác trong khi yêu cầu I/O đang được xử lý, tạo nền tảng để giảm chặn luồng
  • Trong lúc chạy sqlite3_step(), nếu trang B-Tree cần thiết không có trong cache thì SQLite sẽ đọc đĩa bằng I/O đồng bộ như POSIX read(), khiến luồng dừng lại cho đến khi I/O hoàn tất
  • Thay vì chỉ thay các lời gọi POSIX, nhóm nghiên cứu đã điều chỉnh VM và BTree trong Limbo, dự án viết lại bằng Rust, để phù hợp với mô hình thực thi bất đồng bộ
  • Trong benchmark, tail latency p999 giảm tối đa 100 lần, nhưng p90·p99 gần như tương đương SQLite, và việc đánh giá nhiều reader/writer vẫn là bài toán để lại cho tương lai

Nghiên cứu nhằm làm SQLite nhanh hơn

  • Các nhà nghiên cứu từ University of Helsinki và Cambridge trong “Serverless Runtime / Database Co-Design With Asynchronous I/O” đã bàn về cách áp dụng I/O bất đồng bộ và tách biệt lưu trữ cho SQLite
  • Bài báo này là nền tảng cho Limbo, dự án viết lại SQLite bằng Rust
  • Vì là một workshop paper nên nội dung khá ngắn, và trọng tâm đặt vào serverless cùng edge computing
  • Điểm cốt lõi là dù bản thân SQLite đã rất nhanh, tail latency trong môi trường multi-tenant vẫn có thể giảm thêm nếu thay đổi mô hình thực thi

io_uring giảm thời gian chờ I/O như thế nào

  • io_uring của nhân Linux cung cấp giao diện I/O bất đồng bộ
  • Tên gọi này đến từ ring buffer được chia sẻ giữa user space và kernel space, giúp giảm overhead sao chép bộ đệm giữa hai không gian
  • Ứng dụng có thể gửi yêu cầu I/O rồi tiếp tục làm việc khác cho đến khi OS báo hoàn tất
  • Luồng hoạt động như sau
    • Dùng system call io_uring_setup() để thiết lập hai vùng nhớ: hàng đợi gửi và hàng đợi hoàn tất
    • Ứng dụng đưa yêu cầu I/O vào hàng đợi gửi rồi dùng io_uring_enter() để báo OS bắt đầu xử lý
    • Không giống read()write(), nó không chặn luồng mà trả quyền điều khiển về user space
    • Ứng dụng làm việc khác rồi định kỳ polling hàng đợi hoàn tất để kiểm tra I/O đã xong chưa

Nút thắt I/O đồng bộ trong quá trình thực thi truy vấn SQLite

  • Ứng dụng SQLite mở file cơ sở dữ liệu bằng sqlite3_open(), và trong quá trình này các I/O mức thấp của OS như POSIX open sẽ được gọi
  • sqlite3_prepare() chuyển câu lệnh SQL như SELECT, INSERT thành chuỗi lệnh bytecode
  • sqlite3_step() thực thi các lệnh bytecode cho đến khi tạo ra hàng mà truy vấn cần đọc hoặc cho đến khi thực thi xong
    • Nếu có hàng để đọc thì trả về SQLITE_ROW
    • Nếu câu lệnh hoàn tất thì trả về SQLITE_DONE
  • Trong quá trình thực thi, backend pager được gọi và duyệt qua B-Tree biểu diễn bảng và hàng
  • Nếu trang B-Tree cần thiết không có trong page cache của SQLite thì sẽ phát sinh truy cập đĩa
    • SQLite đọc nội dung trang từ đĩa vào bộ nhớ bằng I/O đồng bộ như POSIX read
    • Trong thời gian đó, sqlite3_step() chặn kernel thread
    • Muốn vẫn làm việc đồng thời trong lúc chờ I/O thì ứng dụng phải dùng nhiều luồng hơn

Vì sao muốn nhúng SQL vào serverless·edge

  • Nếu serverless computing chạy ở edge còn cơ sở dữ liệu nằm trong môi trường cloud, sẽ phát sinh chi phí round-trip mạng giữa hàm serverless và cloud
  • Có thể đặt dữ liệu cùng ở edge, nhưng một cách tiếp cận tốt hơn được đề xuất là nhúng cơ sở dữ liệu ngay trong edge runtime
  • Cloudflare Workers đã đạt được dạng này nhưng lại phơi bày giao diện KV
  • KV không phù hợp với mọi bài toán
    • Ánh xạ dữ liệu dạng bảng sang mô hình KV làm trải nghiệm phát triển kém đi
    • Đồng thời còn phát sinh chi phí tuần tự hóa và giải tuần tự hóa
  • SQL có thể phù hợp hơn, và SQLite là cơ sở dữ liệu nhúng nên có thể được đưa trực tiếp vào serverless runtime

Vì sao không dễ chỉ thay SQLite bằng io_uring

  • SQLite dùng I/O đồng bộ dựa trên các read()write() POSIX truyền thống
  • Với ứng dụng nhỏ đây có thể không phải vấn đề lớn, nhưng khi chạy hàng trăm cơ sở dữ liệu SQLite trên một máy chủ, nó có thể trở thành nút thắt
  • Trong môi trường cần tối đa hóa mức sử dụng tài nguyên máy chủ, I/O đồng bộ trở thành một ràng buộc
  • SQLite cũng có vấn đề về đồng thời và multi-tenancy
    • Vì I/O là đồng bộ và chặn, các ứng dụng trên cùng một máy phải tranh chấp tài nguyên
    • Kết quả là độ trễ tăng lên
  • Không dễ chỉ thay các lời gọi POSIX I/O bằng io_uring
    • Ứng dụng dùng blocking I/O phải được thiết kế lại để phù hợp với mô hình I/O bất đồng bộ của io_uring
    • Thư viện SQLite phải có khả năng trả quyền điều khiển về ứng dụng trong lúc I/O đang diễn ra
  • Thay vì chỉ đổi một phần lời gọi trong SQLite, nhóm nghiên cứu đã chọn cách viết lại SQLite bằng Rust và dùng io_uring

Mô hình thực thi bất đồng bộ của Limbo

  • Limbo là dự án viết lại SQLite bằng Rust, trong đó các thành phần VM và BTree được thay đổi để hỗ trợ I/O bất đồng bộ
  • Các lệnh bytecode đồng bộ được thay bằng phiên bản bất đồng bộ tương ứng
  • Ví dụ, lệnh Next sẽ tiến con trỏ và nếu cần thì nạp trang tiếp theo
    • Ở phiên bản đồng bộ cũ, nếu phát sinh I/O đĩa thì nó sẽ chặn cho đến khi đọc xong trang rồi mới trả về cho bên gọi
    • Trong phiên bản bất đồng bộ, NextAsync được gửi đi rồi trả về ngay lập tức
    • Bên gọi sau đó có thể chặn hoặc làm việc khác
  • I/O bất đồng bộ loại bỏ blocking và cải thiện khả năng đồng thời
  • Để tăng thêm mức sử dụng tài nguyên, bài báo cũng đề xuất tách biệt lưu trữ bằng cách tách query engine và storage engine
  • Một bài giải thích liên quan là Disaggregated Storage - a brief introduction

Kết quả benchmark và những câu hỏi còn lại

  • Benchmark mô phỏng một serverless runtime multi-tenant
  • Mỗi tenant có cơ sở dữ liệu nhúng riêng của mình
  • Số tenant thay đổi từ 1 đến 100, tăng theo bước 10
  • Với SQLite, mỗi tenant dùng một luồng riêng và truy vấn được thực thi trong từng luồng để đo đạc
  • Truy vấn chạy là SELECT * FROM users LIMIT 100, lặp lại 1000 lần
  • Limbo cũng thực hiện cùng thí nghiệm nhưng dùng coroutine của Rust
  • Kết quả là tail latency ở p999 giảm tối đa 100 lần
  • Độ trễ truy vấn của SQLite không suy giảm dần một cách đều đặn theo số lượng luồng tăng lên
  • Công việc vẫn đang tiếp tục, và bài báo còn để lại một số câu hỏi mở
    • Phần Future Work đề cập các benchmark bổ sung có nhiều reader và writer
    • Lợi ích chỉ thật sự nổi bật từ p999 trở đi
    • Hiệu năng p90 và p99 gần như giống SQLite
  • Mã nguồn Limbo đã được công bố dưới dạng mã nguồn mở
  • Limbo hiện là dự án chính thức của Turso, và đã có bài giới thiệu

Chưa có bình luận nào.

Chưa có bình luận nào.