1 điểm bởi GN⁺ 2024-09-13 | 1 bình luận | Chia sẻ qua WhatsApp
  • SQLite vẫn chưa đủ để dùng nguyên cấu hình mặc định của Rails trong môi trường production, nhưng nếu thêm một số thiết lập thì có thể xây dựng ứng dụng hiệu năng cao và có khả năng phục hồi tốt
  • Khi có tải ghi đồng thời, khóa ghi toàn cục của SQLite sẽ gây ra SQLITE_BUSY, và luồng transaction ghi thông thường của Rails không thật sự phù hợp với chế độ deferred mặc định
  • Từ sqlite3-ruby 1.6.9, có thể đổi chế độ transaction mặc định sang IMMEDIATE trong database.yml, giúp việc chờ và thử lại khi không lấy được khóa ghi an toàn hơn
  • busy_timeout vẫn giữ Ruby GVL trong lúc chờ nên làm giảm khả năng song song của Puma; nếu dùng sleep trong busy_handler để nhả GVL và thử lại theo khoảng 1ms thì có thể giảm độ trễ đuôi dài
  • Chế độ WAL, transaction IMMEDIATE, busy handler có nhả GVL, cấu hình SQLite mặc định của Rails 7.1, và tùy chọn tách connection pool đọc/ghi là các trụ cột chính để cải thiện hiệu năng SQLite on Rails

Những giới hạn bộc lộ trong cấu hình Rails + SQLite mặc định

  • Để vận hành ứng dụng Rails trên SQLite theo cách vừa hiệu quả vừa bền bỉ, cấu hình mặc định hiện tại vẫn chưa đủ
  • Mục tiêu là biến trải nghiệm mặc định của SQLite trong Rails 8 thành trạng thái sẵn sàng cho production
  • Ứng dụng demo là Lorem News
    • Đây là bản clone cơ bản kiểu Hacker News, gồm người dùng, bài viết và bình luận
    • Nội dung được tạo bằng Lorem Ipsum
  • Việc kiểm thử tải dùng oha load testing CLIbenchmarking routes được tích hợp trong ứng dụng
  • Khi gửi request tuần tự tới endpoint post#create trong 5 giây, RPS ổn định và mọi request đều thành công
  • Khi gửi 4 request đồng thời tới cùng endpoint trong 5 giây, một số request trả về 500 response

SQLITE_BUSY và transaction IMMEDIATE

  • Vấn đề đầu tiên thấy trong log là exception SQLITE_BUSY
  • SQLite dùng khóa ghi trên cơ sở dữ liệu để chỉ cho phép một thao tác ghi diễn ra tại một thời điểm
    • Mỗi lần chỉ một connection có thể giữ khóa ghi
    • Nếu connection mới cố lấy khóa ghi khi connection khác đang giữ khóa, SQLITE_BUSY sẽ xảy ra
  • Khi ứng dụng Rails chịu tải đồng thời lớn hơn, tỷ lệ request thất bại do SQLITE_BUSY cũng tăng lên
  • Chế độ transaction mặc định của SQLite là deferred, nghĩa là không lấy khóa cho đến khi thao tác ghi thực sự xảy ra
    • Điều này có lợi về hiệu năng trong môi trường chỉ có một connection hoặc có nhiều transaction chỉ đọc
    • Nhưng ứng dụng Rails production dùng nhiều connection trên nhiều thread, và Rails bọc các truy vấn ghi trong transaction, nên mặc định này xung đột với cách hoạt động đó
  • Nếu thất bại trong việc lấy khóa ghi ở giữa transaction, SQLite không thể thử lại truy vấn đó một cách an toàn mà không làm hỏng mức cô lập serializable, nên sẽ ném exception ngay lập tức
  • IMMEDIATE transaction cố lấy khóa ghi ngay từ lúc bắt đầu transaction
    • SQLite có thể đưa truy vấn ghi vào hàng đợi và thử lấy khóa lại sau
    • Cách này phù hợp hơn deferred với kiểu transaction ghi của Rails
  • Gem sqlite3-ruby từ bản 1.6.9 đã hỗ trợ cấu hình chế độ transaction mặc định
    • Rails truyền các khóa cấp cao nhất trong database.yml vào quá trình khởi tạo cơ sở dữ liệu của sqlite3-ruby
    • Có thể cấu hình trong database.yml để transaction SQLite của Rails chạy ở chế độ IMMEDIATE
  • Sau thay đổi này, trong bài test tải đơn giản, hệ thống xử lý gần như không còn lỗi 500 dưới tải đồng thời, nhưng với 16 request đồng thời thì vẫn xuất hiện lại một số lỗi

busy_timeout, GVL và busy_handler tùy biến

  • Điểm nghẽn tiếp theo là khi số request đồng thời tiến gần hoặc vượt quá số Puma worker, độ trễ p99 tăng vọt
  • Thời gian xử lý request thực tế vẫn ổn định ngay cả khi tải đồng thời gấp 3 lần số Puma worker, nhưng khi bắt đầu xuất hiện các request mất khoảng 5 giây thì cũng đồng thời xuất hiện các response 500 do SQLITE_BUSY
  • Mốc 5 giây này khớp với cấu hình timeout trong database.yml, được ánh xạ tới thiết lập busy_timeout của SQLite
  • busy_timeout không ném exception BUSY ngay, mà chờ trong số mili giây đã chỉ định và tiếp tục thử lấy lại khóa ghi
    • SQLite thử lấy lại khóa theo một dạng exponential backoff
    • Chỉ khi không lấy được khóa trong thời gian timeout thì mới ném exception BUSY
    • Ứng dụng web có thể mở nhiều connection nhưng không cần tự điều phối thứ tự ghi, mà giao việc đó cho SQLite
  • Điểm nghẽn xuất phát từ việc SQLite được nhúng trong tiến trình Ruby, và sqlite3-ruby không nhả Ruby GVL khi gọi mã C của SQLite
    • Một Puma worker vẫn giữ GVL trong lúc chờ truy vấn cơ sở dữ liệu trả về
    • Khi đó các Puma worker khác thậm chí cũng khó gửi truy vấn ghi tới SQLite cùng lúc
    • Tính chất ghi tuần tự của SQLite khiến việc xử lý request trong Rails cũng trở nên tuyến tính hơn, làm giảm mạnh throughput
  • SQLite cung cấp hook busy_handler ở mức thấp hơn busy_timeout
    • busy_timeout thực chất là một triển khai busy_handler cụ thể do SQLite cung cấp
    • sqlite3-ruby có binding cho hàm C sqlite3_busy_handler, nên có thể tạo Ruby callback được gọi khi truy vấn vào hàng đợi
  • Nếu dùng Kernel.sleep trong callback Ruby, có thể nhả GVL trong lúc truy vấn chờ thử lại khóa ghi
  • Cách này cải thiện đáng kể độ trễ p99 dưới tải đồng thời, nhưng ở độ trễ p99.99 thì vẫn còn hiện tượng request chậm nhất ngày càng chậm hơn khi tải đồng thời tăng lên

Đuôi dài của exponential backoff và thử lại mỗi 1ms

  • Logic busy_timeout của SQLite khi được viết lại bằng Ruby tạo ra cấu trúc khiến các truy vấn chờ lâu bị bất lợi hơn
  • Ở các lần thử lại đầu tiên, thời gian chờ còn nhỏ, nhưng số lần callback tăng thì thời gian chờ cũng tăng theo
    • Lần chờ đầu tiên là 1ms
    • Ở lần gọi thứ 10 sẽ chờ 50ms
    • Từ lần thứ 12 trở đi sẽ chờ 100ms mỗi lần
    • Nếu tổng thời gian chờ vượt quá timeout 5000ms thì exception sẽ xảy ra
  • Nếu liên tục có truy vấn ghi mới đi vào, các truy vấn mới có thể thử lấy khóa thường xuyên hơn nhờ thời gian chờ ngắn hơn
    • Trong lúc một truy vấn cũ đã chờ ba lần đang ngủ 10ms, một truy vấn mới có thể thử lại ba lần với các khoảng 1ms, 2ms, 5ms
    • Kiểu backoff tăng dần này làm tăng khả năng truy vấn cũ không lấy được khóa ghi và bị timeout
  • Giải pháp là để mọi truy vấn thử lại với cùng tần suất, bất kể “tuổi” của truy vấn
  • Trong nhánh main của sqlite3-ruby đã có thay đổi này
    • Tại thời điểm bài viết được viết, tính năng này vẫn chưa có trong bản tagged release
    • Ruby callback dùng sleep để nhả GVL trong lúc chờ
    • Luôn chỉ sleep 1ms
  • Sau thay đổi này, biểu đồ độ trễ p99.99 trong benchmark trở nên phẳng hơn
    • Vẫn còn một bước nhảy khi mức đồng thời vượt quá một nửa số Puma worker
    • Nhưng sau đó độ trễ đuôi dài ổn định quanh mức khoảng 0,5 giây

Chế độ WAL và tách connection pool đọc/ghi

  • Bốn điều kiện cần cho hiệu năng SQLite on Rails là transaction IMMEDIATE, busy handler có nhả GVL, cấu hình SQLite phù hợp, và chế độ WAL
  • write-ahead log cho phép SQLite xử lý nhiều thao tác đọc đồng thời
    • rollback journal mode mặc định chỉ cho phép một truy vấn tại một thời điểm, dù là đọc hay ghi
    • Chế độ WAL cho phép nhiều reader cùng lúc, nhưng writer vẫn chỉ có một tại một thời điểm
  • Từ Rails 7.1, Rails áp dụng cấu hình mặc định tốt hơn cho cơ sở dữ liệu SQLite
    • Những cấu hình này rất quan trọng để SQLite hoạt động tốt trong bối cảnh ứng dụng web
    • Chi tiết cấu hình và lý do được trình bày trong bài blog riêng
  • Đòn bẩy hiệu năng thứ năm, mang tính tùy chọn, là tách pool chỉ đọcpool chỉ ghi
    • Chế độ WAL của SQLite hỗ trợ nhiều connection đọc và một connection ghi
    • Nếu connection pool Active Record bị bão hòa bởi các connection ghi, tác vụ đọc có thể bị chặn không cần thiết
  • Có thể dùng hỗ trợ multiple databases của Rails để trỏ cấu hình reader và writer tới cùng một cơ sở dữ liệu SQLite
    • Trên thực tế đây không phải các cơ sở dữ liệu riêng biệt, mà cùng trỏ tới một cơ sở dữ liệu duy nhất
    • Kết quả là có thể tạo ra các connection pool và cấu hình connection tách biệt với nhau
  • Pool connection reader gồm các connection chỉ đọc, còn pool writer chỉ có một connection
  • Các model Active Record được cấu hình để kết nối tới pool phù hợp theo vai trò
  • Dùng automatic role switching của Rails để mặc định mọi web request dùng pool reader, và chỉ chuyển sang pool writer khi cần ghi vào cơ sở dữ liệu
    • Vì cùng dùng một cơ sở dữ liệu nên không cần delay để bảo đảm read your own writes
    • Vá phương thức transaction của adapter ActiveRecord để transaction gắn với cơ sở dữ liệu writer
  • Sự kết hợp giữa “deferred requests” và các connection pool được cô lập này cho thấy cải thiện hiệu năng theo chỉ số RPS đơn thuần trong bài test endpoint tạo comment

Các cải tiến được đóng gói thành gem

  • Không cần tự triển khai toàn bộ các cải tiến này trực tiếp trong ứng dụng Rails
  • Có thể cài activerecord-enhancedsqlite3-adapter để áp dụng các cải tiến liên quan
  • Tính năng connection pool tách biệt là chức năng thử nghiệm mới hơn, nên cần thêm cấu hình để opt-in
  • Cách tiếp cận này gói lại các công cụ, kỹ thuật và giá trị mặc định để dùng SQLite nhanh và linh hoạt trong môi trường production của Rails
  • Rails là một web application framework rất phù hợp để làm việc với SQLite, và hệ sinh thái công cụ cùng gem liên quan đang tiếp tục phát triển

1 bình luận

 
GN⁺ 2024-09-13
Các ý kiến trên Hacker News
  • Nếu bạn định dùng SQLite + Rails, dự án Litestack của Oldmoe (X/GitHub) rất đáng xem
    Litestack là một Ruby gem tận dụng tính nhúng của SQLite cho các ứng dụng Ruby và Ruby on Rails, cung cấp trong một gói duy nhất cơ sở dữ liệu SQL, cache nhanh, hàng đợi tác vụ, message broker, công cụ tìm kiếm toàn văn và nền tảng chỉ số
    Tôi đang dùng trong dự án hiện tại và rất hài lòng; link là https://github.com/oldmoe/litestack

  • Không thể tưởng tượng nổi để viết một bài chi tiết đến mức này thì đã mất bao nhiêu ngày
    Đây là bài hữu ích cho những ai đang suy nghĩ về việc mở rộng ứng dụng web SQLite, không chỉ trong Rails

  • Bất kỳ ai làm việc với SQLite, bất kể ngôn ngữ hay framework sử dụng, đều nên đọc bài này
    Vài năm trước tôi đã phải tự mình tìm ra phần lớn những điều này, nên cảm ơn vì đã tổng hợp lại

  • Tôi đang xây dựng một hệ thống phân tích FOSS và vì cần dễ cài đặt, tôi muốn gửi dữ liệu sự kiện vào một cơ sở dữ liệu SQLite riêng, tách khỏi dữ liệu ứng dụng chính
    Ngay cả một website bận rộn ở mức trung bình cũng có thể phát sinh hơn 1000 sự kiện mỗi giây, nên tôi lo về khả năng mở rộng
    Tôi thắc mắc liệu cách gom sự kiện trong bộ nhớ máy chủ rồi mỗi 1 giây thực hiện một lần ghi theo lô có phải là cách hợp lý để tránh giới hạn nhiều tác vụ ghi của SQLite không, hay có ý tưởng nào tốt hơn không

    • Tôi thấy đó là ý tưởng khá hợp lý và tốt. Tôi đã triển khai cách tương tự trong nhiều hệ thống, và nói chung xử lý theo lô giúp giảm overhead trên mỗi mục
      Điều này cũng có thể chứng minh dễ dàng bằng benchmark, và cũng có thể đặt toàn bộ các mục trong lô vào một transaction duy nhất
      Với xử lý theo lô, thực tế sẽ có một thread lấy batch và thực hiện ghi thật sự, nên cũng rất phù hợp với giới hạn chỉ 1 tác vụ ghi đồng thời của SQLite
      Tuy nhiên độ phức tạp sẽ tăng lên một chút. Cần quyết định sẽ làm gì nếu một lần ghi theo lô không kết thúc trong vòng 1 giây, có để kích thước queue lưu trong bộ nhớ là không giới hạn không, nếu không giới hạn thì có chắc khi server quá tải nó sẽ không chết vì OOM không, nếu đặt giới hạn thì có chấp nhận mất mục không, nếu bỏ thì sẽ bỏ mục nào, giới hạn queue là bao nhiêu
      Những câu hỏi như vậy chắc chắn sẽ xuất hiện trong hầu hết mọi hệ thống cần queue, và hữu ích không chỉ cho hiện tại mà cả cho các tình huống sau này
    • Không phải SQLite không thể ghi. Chỉ là nó chỉ hỗ trợ một transaction ghi tại một thời điểm
      Nếu khó tin tưởng hiệu năng đồng thời transaction của SQLite, bạn có thể tuần tự hóa toàn bộ thao tác ghi trong một thread hoặc process cụ thể
    • Ở đây cách đó đã hoạt động tốt và đã thay đổi cách tôi nghĩ về SQLite. Hipp cũng tham gia làm cùng, và SQLite có thể đạt hiệu năng cực lớn: https://use.expensify.com/blog/scaling-sqlite-to-4m-qps-on-a...
    • Dữ liệu phân tích thường thiên về ghi, nên tôi khuyên dùng ClickHouse
      Nếu dùng tính năng async-insert[0] của ClickHouse, bạn không cần lo việc xử lý sự kiện theo lô ở phía ứng dụng
      Nếu đang tìm một giải pháp nhúng, có thể dùng chDB dựa trên ClickHouse
      [0] https://clickhouse.com/blog/asynchronous-data-inserts-in-cli...
    • Ghi theo lô có lẽ là ý tưởng tốt, nhưng cách tốt nhất để làm việc kiểu này với SQLite là dùng WAL và có một writer duy nhất được chỉ định
      Có thể có bao nhiêu worker đọc tùy ý, còn writer nhận input từ thứ gì đó như queue
      Chỉ cần làm như vậy, hiệu năng thường thật sự đáng kinh ngạc
  • Tôi vẫn chưa thật sự hiểu xu hướng dùng SQLite làm cơ sở dữ liệu backend cho production
    SQLite rất tuyệt với vai trò cơ sở dữ liệu ứng dụng phía client nhỏ gọn, có thể nhúng, như danh bạ điện thoại, nhưng chính các nhà phát triển của nó cũng luôn từ chối mở rộng vượt ra ngoài phạm vi đó
    Ví dụ, họ không thêm các kiểu native hữu ích như ngày/giờ hay UUID, vì làm vậy sẽ khiến mã và kích thước đối tượng nhúng phình to, và kết quả là bị kẹt trong trạng thái “mọi thứ đều là chuỗi”
    Có thể bật tính toàn vẹn tham chiếu, nhưng các tùy chọn ràng buộc cũng rất hạn chế
    Tôi không hiểu vì sao cứ cố nhét nó vào một vai trò không phù hợp và cũng không được hỗ trợ đúng cách như vậy

    • Trước hết, tôi không chắc Richard Hipp có cùng suy nghĩ về vai trò “vốn được định sẵn” của SQLite hay không
      Lý do thì đơn giản. Với các mẫu truy cập thiên về đọc, SQLite cực kỳ nhanh, đủ nhanh để đơn giản hóa mã truy cập cơ sở dữ liệu, và chẳng hạn các truy vấn N+1 trong nhiều trường hợp thực tế không còn là vấn đề
      Ngoài ra, SQLite loại bỏ một tầng trong kiến trúc N-tier, qua đó cũng giảm bớt những thứ có thể hỏng. Nếu từng tự vận hành Postgres hay MySQL, bạn sẽ biết thực tế có rất nhiều thứ có thể trục trặc
      Nó không phải lựa chọn hoàn hảo cho mọi ứng dụng, thậm chí là cho phần lớn ứng dụng, nhưng xu hướng hiện nay giống một sự điều chỉnh cân bằng trước quan niệm rõ ràng là sai rằng SQLite chỉ phù hợp với “cơ sở dữ liệu ứng dụng client nhúng nhỏ”
    • Cuối cùng sẽ nhận ra rằng “API server” thực ra ngay từ đầu đã là một DBMS
      Khi nhìn như vậy, cấu trúc có thêm một DBMS khác bên cạnh DBMS xử lý cùng dữ liệu trở nên khá buồn cười
      Vì thế hướng đi tách ra thành để client kết nối trực tiếp tới Postgres, hoặc bỏ Postgres và tập trung hơn vào DBMS tự xây của mình
      Nếu chọn phương án thứ 2, SQLite là một engine dễ xây dựng bên trên. Không hoàn hảo, nhưng đó là công cụ ta đang có
      Việc nhận ra điều này lan rộng trên quy mô lớn mới xảy ra tương đối gần đây, nên đang có nhiều thử nghiệm để xem cái gì làm được và không làm được
      Đó là vòng tuần hoàn tự nhiên của điện toán, nơi những thứ cũ lại trở nên mới
      Có thể đọc Postgres thành MySQL, MSSQL, Oracle hoặc DBMS khác cũng được
    • UUID thì dùng chuỗi hoặc BLOB, ngày tháng thì dùng chuỗi hoặc timestamp dạng số nguyên/số thực là được, không phải sao?
      Sự đơn giản hóa này mang lại nhiều lợi ích không chỉ cho nhà phát triển SQLite hay phần cứng cấu hình thấp, mà cả cho nhà phát triển ứng dụng
      Tài liệu đơn giản hơn, đường cong học tập ngắn hơn, bề mặt lỗi và kích thước binary cũng giảm
      Phần mềm ngày nay có xu hướng thêm sự cồng kềnh và phức tạp vào mọi thứ, nên thật tuyệt khi có vài dự án như SQLite đang chống lại điều đó
  • Bài viết rất hay, và tôi tự hỏi liệu có tài liệu tương tự cho Django không
    ArchiveBox dùng SQLite thông qua Django, và chúng tôi khá thường gặp đúng những vấn đề giống như bài viết mô tả với Rails
    Sẽ thật tốt nếu có một giải pháp tầng SQLite không bắt buộc phải tuần tự hóa mọi thao tác ghi qua những đường khác của ứng dụng

  • Gem sqlite3-ruby theo thiết kế không nhả GVL trong khi gọi SQLite; nhìn vào bình luận issue được liên kết https://github.com/sparklemotion/sqlite3-ruby/issues/287#iss... thì có vẻ đã có nghi ngờ rằng chi phí giành lại lock là lớn, nhưng chưa được kiểm chứng
    Nghĩ tới tất cả các cách đi vòng này thì thấy hơi đáng ngờ
    Nếu là văn hóa extension của Python thì có lẽ thiết kế sẽ ngược lại, nên tôi tò mò thực tế bên đó làm thế nào
    Ngoài ra, trong issue được liên kết còn có bình luận rằng “gem extralite là một client SQLite thay thế có nhả GVL trong khi bị blocking, và nội dung liên quan đến concurrency nằm ở https://github.com/digital-fabric/extralite?tab=readme-ov-fi.... Nhìn chung nó nhanh hơn gem này nhiều và cũng không có vấn đề về concurrency”

    • Có thể xem thảo luận chi tiết hơn tại https://github.com/sparklemotion/sqlite3-ruby/pull/528https://github.com/digital-fabric/extralite/pull/46
      Đã xác minh rằng nếu chỉ đơn giản nhả GVL ở mọi step của máy ảo SQLite thì hiệu năng đơn luồng sẽ tệ đi đáng kể
      Tìm điểm cân bằng giữa hiệu năng đơn luồng và đa luồng là việc khó
      Trong Rails, do có connection pool nên biết chắc là đa luồng, nhưng gem cấp thấp cũng được dùng nhiều trong các thư viện và công cụ khác chạy trong môi trường đơn luồng
  • Một vài giá trị tinh chỉnh tôi duy trì cho web service cá nhân làm chơi của mình như sau
    PRAGMA journal_mode = WAL;
    PRAGMA busy_timeout = 5000;
    PRAGMA synchronous = NORMAL;
    PRAGMA cache_size = 1000000000;
    PRAGMA foreign_keys = true;
    PRAGMA temp_store = memory;
    Và tôi dùng transaction BEGIN IMMEDIATE
    https://kerkour.com/sqlite-for-servers

    • Bạn nghĩ sao về cache_sizemmap_size?
  • Tôi thích cả SQLite lẫn Rails, nhưng chuyện này trông khá giống việc dùng MS Access trong môi trường production

    • Không phải là quá khác, nhưng trong các kịch bản thiên về đọc, SQLite có hiệu năng tốt hơn Jet (MS Access) rất nhiều
      Hơn nữa, so với vài chục năm trước khi Access còn được dùng nhiều hơn, máy tính và tốc độ đĩa hiện nay đã nhanh hơn rất nhiều
      Nếu thiên về đọc, không chỉ SQLite mà cả Jet cũng có thể khá dễ dàng đạt tới hàng chục nghìn request mỗi giây
      Phần lớn ứng dụng không có tới hàng trăm nghìn người dùng đồng thời, nên SQLite có thể rất phù hợp
      Một điểm mạnh nữa của SQLite là gần như nền tảng và ngôn ngữ nào cũng có client
      Lưu trữ, sao lưu và tính di động cũng là các trường hợp sử dụng rất hợp với SQLite. Tôi đã từng thúc đẩy mạnh việc dùng SQLite theo từng hộp trong một dự án có dữ liệu nhập chỉ giới hạn trong một khoảng thời gian nhất định, và đến giờ vẫn cảm thấy như vậy lẽ ra tốt hơn
      Thay vì tạo schema phức tạp và các tính năng xuất/lưu trữ tùy chỉnh, có thể chỉ cần sao chép một file để làm bản lưu trữ hoặc bản sao lưu, và cũng không cần phải cân nhắc quá sâu các thay đổi schema phát sinh theo thời gian
      Tùy tình huống, nhưng đây là một lời giải khá ổn cho nhiều vấn đề. Nó giống như việc trong phần lớn ứng dụng, PostgreSQL hoặc một RDBMS khác thường là lựa chọn tốt hơn các tùy chọn NoSQL có khả năng mở rộng cao hơn
      Trước đây chúng ta có xu hướng thiết kế quá mức, còn giờ đây hiệu năng tính toán và I/O đang tiến đến mức ngày càng khó biện minh cho những nỗ lực đó
    • Giá mà có một thứ kiểu MS Access dành cho web app. Tôi đã thử nhiều sản phẩm dựng website, nhưng không cái nào trực quan như Access
    • Tôi tò mò vì sao lại như vậy. Đặc biệt nếu là ứng dụng thiên về đọc thì đang lo ngại giới hạn mở rộng nào?
    • Với Pieter Levels thì cách này hiệu quả. Tất nhiên nếu số người dùng vượt một ngưỡng nhất định thì sẽ phát sinh vấn đề
  • Bài viết rất hữu ích và được viết tốt
    Tôi thắc mắc vì sao phương thức busy_timeout mặc định lại có độ trễ theo cấp số nhân để phạt các truy vấn cũ
    Lý do gì khiến điều này hợp lý làm mặc định?