37 điểm bởi GN⁺ 6 ngày trước | 20 bình luận | Chia sẻ qua WhatsApp
  • Mọi cơ sở dữ liệu rốt cuộc đều là một tập hợp tệp có cấu trúc nằm trên hệ thống tệp, nên với ứng dụng ở giai đoạn đầu, việc tự quản lý tệp cũng có thể đạt hiệu năng đủ tốt
  • Khi triển khai cùng một máy chủ bằng Go, Bun và Rust để so sánh ba cách tiếp cận quét tệp · bản đồ trong bộ nhớ · tìm kiếm nhị phân trên đĩa, kết quả cho thấy chỉ với truy cập tệp đơn giản cũng có thể đạt thông lượng cao
  • Cách dùng bản đồ trong bộ nhớ cho hiệu năng tốt nhất (tối đa 169k req/s), còn SQLite đạt 25k req/s, ổn định nhưng có thêm overhead
  • Phần lớn dịch vụ có thể xử lý tới khoảng 90 triệu DAU chỉ với một tệp SQLite duy nhất, nên ở giai đoạn sản phẩm ban đầu không cần một cơ sở dữ liệu riêng biệt
  • Chỉ từ thời điểm tập dữ liệu vượt quá RAM hoặc cần join · tìm kiếm đa điều kiện · ghi đồng thời · transaction thì mới cần đưa cơ sở dữ liệu vào

Cơ sở dữ liệu có thực sự cần thiết không

  • Cơ sở dữ liệu rốt cuộc cũng chỉ là tập hợp tệp; SQLite là một tệp đơn, còn PostgreSQL được cấu thành từ thư mục và tiến trình
    • Mọi cơ sở dữ liệu đều đọc và ghi vào hệ thống tệp, hoạt động theo cách tương tự như khi code gọi open()
    • Vì vậy, vấn đề cốt lõi không phải là “có dùng tệp hay không”, mà là “dùng các tệp của cơ sở dữ liệu hay tự quản lý trực tiếp
    • Nhiều ứng dụng ở giai đoạn đầu vẫn có thể đạt hiệu năng đủ tốt ngay cả khi tự quản lý

Cấu hình thí nghiệm

  • Cùng một HTTP server được triển khai bằng Go, Bun(TypeScript), Rust và so sánh hai chiến lược lưu trữ
    • Sử dụng ba tệp JSONL: users.jsonl, products.jsonl, orders.jsonl
    • Tạo bằng POST /users, truy vấn bằng GET /users/:id
    • Chỉ benchmark đường dẫn đọc (GET)
  • Cách tiếp cận 1: đọc tệp ở mỗi request

    • Khi có request, mở tệp và quét toàn bộ các dòng, parse JSON rồi kiểm tra ID có khớp hay không
    • Trung bình phải đọc một nửa tệp nên có độ phức tạp O(n)
    • Dữ liệu càng lớn thì tốc độ xử lý request càng giảm mạnh
  • Cách tiếp cận 2: nạp toàn bộ vào bộ nhớ

    • Khi khởi động, đọc toàn bộ tệp và lưu vào hash map theo ID
    • Ghi được phản ánh đồng thời vào map và tệp, còn đọc chỉ cần tra cứu một map duy nhất nên là O(1)
    • Tệp đóng vai trò lưu trữ bền vững, còn map đóng vai trò chỉ mục
    • Go dùng sync.RWMutex, Rust dùng RwLock để hỗ trợ đọc song song
  • Cách tiếp cận 3: tìm kiếm nhị phân trên đĩa

    • Một giải pháp trung gian để truy vấn nhanh mà không phải đưa toàn bộ dữ liệu lên RAM
    • Tạo tệp dữ liệu được sắp xếp theo ID và tệp chỉ mục độ rộng cố định (58 byte/bản ghi)
    • Dùng ReadAt để tìm kiếm O(log n) trong chỉ mục, rồi đọc đúng một bản ghi tại offset tương ứng
    • Khi thêm bản ghi mới, thứ tự sắp xếp bị phá vỡ nên cần tái tạo chỉ mục hoặc merge định kỳ
    • Mẫu merge này hoạt động tương tự LSM-tree

Môi trường benchmark

  • Quy mô tập dữ liệu: 10k, 100k, 1M bản ghi
  • Công cụ tạo tải: wrk, thực hiện request GET ngẫu nhiên trong 10 giây với 4 thread và 50 kết nối đồng thời
  • Test trên cùng một máy (Apple M1 Mac mini, macOS 15) với Go 1.26, Bun 1.3, Rust 1.94
  • Trong Go, có thêm so sánh với tìm kiếm nhị phân (trên đĩa)SQLite(modernc.org/sqlite)

Kết quả chính

  • Hiệu năng quét tuyến tính suy giảm mạnh: ở 1M bản ghi, Go chỉ còn 23 req/s, Bun 19 req/s
  • Tìm kiếm nhị phân (trên đĩa): trong dải 10k~1M bản ghi chỉ giảm từ 45k xuống 38k req/s, tức chỉ giảm 15%
    • Nhờ hiệu ứng page cache của OS, vùng chỉ mục phía trên luôn được giữ trong bộ nhớ
  • SQLite: duy trì hiệu năng ổn định với 25k req/s và độ trễ trung bình 2ms
  • Tìm kiếm nhị phân nhanh hơn SQLite khoảng 1,7 lần, cho thấy SQLite có overhead trong truy vấn PK đơn giản
  • Cách dùng map trong bộ nhớ cho hiệu năng tốt nhất: 97k~169k req/s, độ trễ dưới 0,5ms
  • Bun nhanh hơn Go: Bun 106k req/s, Go 97k req/s
    • Bun dựa trên JavaScriptCore + Zig(uWebSockets), bỏ qua libuv
  • Rust áp đảo ở quét tuyến tính: nhanh hơn Go 3~6 lần, được cho là nhờ hiệu quả parse JSON và I/O
  • Lựa chọn tối ưu theo từng trường hợp sử dụng

    • Thông lượng tuyệt đối cao nhất: Rust map trong bộ nhớ (169k req/s)
    • Tốt nhất khi không thể nạp vào RAM: Go tìm kiếm nhị phân (~40k req/s)
    • Khi cần SQL: SQLite (25k req/s)
    • Triển khai đơn giản nhất: Go quét tuyến tính (~20 dòng code)

Ý nghĩa của 25.000 req/s

  • Lưu lượng web thông thường giả định tỷ lệ đỉnh:trung bình = 2:1
    • Trung bình 12.500 req/s → mức đỉnh 25.000 req/s
  • Giả sử mỗi người dùng hoạt động thực hiện 10 lượt truy vấn mỗi giờ và tỷ lệ truy cập đồng thời ở thời điểm đỉnh là 10%
    • Công thức số request ở đỉnh: DAU × 0.000278
  • Kết quả tính toán DAU bão hòa cho từng cách tiếp cận
    • Go quét tuyến tính: 2.8M
    • Go tìm kiếm nhị phân: 144M
    • SQLite: 90M
    • Go map trong bộ nhớ: 349M
    • Bun map trong bộ nhớ: 381M
    • Rust map trong bộ nhớ: 608M
  • Phần lớn sản phẩm không chạm tới các con số này
    • Ví dụ: SaaS có 10.000 khách hàng → 3 req/s, ứng dụng 100.000 DAU → 30 req/s
  • Kết luận là phần lớn sản phẩm giai đoạn đầu không cần cơ sở dữ liệu
    • Ngay cả khi cần, một tệp SQLite duy nhất cũng có thể xử lý tới 90 triệu DAU

Khi nào cần cơ sở dữ liệu

  • Khi tập dữ liệu không còn vừa trong RAM

    • Với hàng chục triệu bản ghi trở lên, chỉ riêng chỉ mục cũng đã cần vài GB
    • Cần phân trang dữ liệu, và cơ sở dữ liệu tự động xử lý việc này
  • Khi cần truy vấn theo trường khác ngoài ID

    • Tìm kiếm đa điều kiện đòi hỏi quét tệp hoặc thêm map mới
    • Nếu duy trì nhiều map thì về bản chất là đang tự xây một query engine
  • Khi cần join

    • Phải đọc và kết hợp nhiều tệp, khi đó SQL hiệu quả hơn
  • Khi có ghi đồng thời từ nhiều tiến trình

    • Map trong bộ nhớ của từng instance bị tách rời, làm mất tính nhất quán
    • Cần một nguồn sự thật duy nhất bên ngoài → vai trò của cơ sở dữ liệu
  • Khi cần ghi nguyên tử giữa các entity

    • Cần đảm bảo việc tạo đơn hàng và trừ tồn kho cùng thành công hoặc cùng thất bại
    • Khi đó phải tự triển khai transaction log riêng, còn DB giải quyết bằng ACID
    • Với những công cụ nội bộ, side project, sản phẩm giai đoạn đầu không có các ràng buộc này
    • Vẫn có thể vận hành tốt trong RAM của một máy chủ đơn
    • Các tệp JSONL sau này cũng có thể migrate sang cơ sở dữ liệu một cách dễ dàng

Phụ lục và mã nguồn

  • Có kèm mã server Go, Bun, Rust
  • Cung cấp riêng dữ liệu seed và script chạy benchmark (run_bench.sh)
  • Tệp ZIP chứa go-server/, bun-server/, rust-server/, seed.ts
  • Script sẽ seed dữ liệu ở ba quy mô, chạy tải bằng wrk rồi kết thúc

Hướng dẫn liên quan đến DB Pro

  • DB Pro** là client cơ sở dữ liệu cho Mac, Windows, Linux**

    • Tích hợp chức năng query, duyệt và quản trị
    • Hỗ trợ nền tảng web cộng tác và AI tích hợp
    • Ở phiên bản mới nhất, hỗ trợ kết nối cơ sở dữ liệu SQLite của Val Town
    • Trong v1.3.0, bổ sung tính năng tạo cơ sở dữ liệu, trình soạn thảo nhiều truy vấn và kết nối PlanetScale Vitess

20 bình luận

 
happing94 5 ngày trước

Đây là cái quái gì vậy
Cứ tưởng người ta dùng DB là vì hiệu năng à

 
botplaysdice 5 ngày trước

Tôi nghĩ đây là một bài viết rất hay. Đặc biệt, những tài liệu có chứa các 'con số' như vậy thì rất quý. Đây là thời đại mà không dễ gì thấy được những lập trình viên có dù chỉ là 'cảm nhận đại khái' về việc code chúng ta tạo ra và tech stack chúng ta mang về dùng có những overhead nào, nên tôi đã đọc rất thích thú.

 
foriequal0 4 ngày trước

Tôi cũng đồng ý. Tôi nghĩ đây là tài liệu mang lại trực giác quan trọng về Mechanical sympathy hay việc điều tiết nhịp độ phát triển. Giống như "Latency Numbers Every Programmer Should Know" vậy.
Và tôi không đọc bài này theo hướng cho rằng một hướng cụ thể nào đó vô điều kiện là tốt hơn. Ngược lại, những con số mà tất cả các cách tiếp cận được nhắc đến trong bài cho thấy lại giống như "mức hiệu năng dư sức đáp ứng cho phần lớn doanh nghiệp", nên tôi hiểu là hãy chọn cách phù hợp với tình huống vấn đề.

 
botplaysdice 5 ngày trước

Những viên ngọc trong phần trả lời cũng chỉ là phần thêm thôi.

 

Nếu có lý do phải làm như vậy thì hẳn cũng đáng để cân nhắc chứ nhỉ? Chẳng hạn như bị ràng buộc hiệu năng cực kỳ nghiêm ngặt.
Nhưng trong đa số trường hợp, có thật sự cần chọn cách này không? Đâu phải là DB không có những ưu điểm của nó..

 
m00nlygreat 5 ngày trước

Chỉ nên đọc nó như một sự thay đổi trong cách nghĩ thôi, vậy mà mọi người nhạy cảm quá.

 

Đúng vậy. Có thể xem đây như một đề xuất rằng ở giai đoạn đầu của việc kinh doanh, khi chưa có nhiều người dùng, thay vì mua DB hay làm mọi thứ phức tạp, chỉ với file I/O cơ bản cũng có thể đi đến lúc mô hình kinh doanh ổn định.

 
smash8106 5 ngày trước

Tôi cũng đồng ý. Đôi khi trong dịch vụ, DB được xem trọng quá mức cần thiết, và cũng có lúc người ta đầu tư quá nhiều vào thiết kế như thể chỉ cần phá vỡ chuẩn hóa là sẽ xảy ra chuyện lớn vậy.
Không phải là đừng dùng DB, mà chỉ cần xem đây như một dịp để làm mới lại suy nghĩ về việc vì sao ta dùng nó, và rốt cuộc nền tảng cốt lõi của dịch vụ là gì, như vậy thôi cũng đã đủ hay rồi.
Cuối cùng thì sự cân bằng lúc nào cũng quan trọng.

 
cafedead 5 ngày trước

Ngay từ thời điểm chọn SQLite cho máy chủ production, bạn sẽ phải liên tục suy nghĩ xem khi nào nên chuyển sang hệ khác.
Ngày xưa, bản thân chi phí của DB (chi phí mua máy chủ, IDC, chi phí giấy phép, v.v.) khá đắt nên còn đáng để cân nhắc,
nhưng ngày nay, khi thứ gọi là triển khai chỉ bằng một cú nhấp đã khả thi, liệu có thực sự cần phải băn khoăn nữa không?

 

Ngay cả bây giờ DB vẫn đắt mà.

 

Dĩ nhiên, nếu là "dự án giai đoạn đầu hoặc ứng dụng quy mô nhỏ" thì có thể không cần database. Không chỉ database, mà các thành phần khác cũng có thể làm qua loa bằng bất cứ thứ gì. Vấn đề là khi quy mô tăng lên. Chỉ là một bài viết xem số liệu cho vui thôi.

 
carnoxen 5 ngày trước

https://hackers.pub/@gnh1201/2025/…

Đôi khi cũng không cần cài đặt một cơ sở dữ liệu riêng. Tuy chỉ giới hạn trên Windows...

 

Nhìn tiêu đề là tôi bật cười luôn.

 

Thỉnh thoảng tôi cũng nghĩ liệu các thực thể chính có nhất thiết phải đảm bảo tính bền vững thông qua RDBMS hay không. Vì hiện cũng có khá nhiều công nghệ thay thế để cung cấp SSOT mà.

 

Nếu Sqlite hỏng thì coi như hết cách..

 

Có trường hợp nào sqlite bị hỏng không? Tôi khá tò mò. Ngoại trừ việc di chuyển hoặc xóa tệp bất thường.

 
Ý kiến trên Hacker News
  • Tôi thực sự thích bài này. Nó cho thấy máy tính nhanh đến mức nào
    Tuy vậy, tôi không đồng ý với kết luận ở phần cuối. Tác giả nói điều này không áp dụng cho các ứng dụng có ràng buộc “nhiều tiến trình cần ghi đồng thời”, nhưng trên thực tế, ngay cả ở giai đoạn đầu của sản phẩm cũng thường có những trường hợp như cron hay message queue với worker riêng cần ghi cùng lúc
    Có thể ép chỉ cho server chính ghi, nhưng như vậy sẽ làm tăng độ phức tạp kiến trúc
    Vì thế, xét thuần về góc độ scale thì tôi đồng ý với tác giả, nhưng nhìn rộng hơn thì tôi nghĩ dùng cơ sở dữ liệu vẫn tốt hơn. Đặc biệt, SQLite là một lựa chọn hợp lý
    Nếu cần scale, cứ cache dữ liệu truy cập thường xuyên trong bộ nhớ. Tổ hợp tôi dùng là SQLite + cache in-memory

    • Tôi cũng rất hay gặp tình huống tương tự. Dù một server là đủ, nhưng ngay khi cần dự phòng máy chủ, bạn sẽ cần network storage và cuối cùng sẽ nghiêng về một DB có thể truy cập qua mạng
      S3 đôi khi dùng được, nhưng vẫn có khá nhiều hạn chế để coi là giải pháp thay thế hoàn chỉnh
    • Dạo này khi bắt đầu dự án mới tôi mặc định dùng SQLite. Hiệu năng rất nhanh, và nếu sau này quy mô tăng lên thì cũng dễ chuyển sang Postgres
      Không cần quản lý hay backup một DB server riêng nên đơn giản và rẻ hơn nhiều
    • Sau khi xem benchmark Rust 1M, tôi lại một lần nữa nhận ra máy tính nhanh đến mức nào
  • Tôi rất thích SQLite, nhưng cũng nhận ra nó không phải câu trả lời cho mọi vấn đề
    Khi làm một ứng dụng từ điển phía client, tôi đã thử bản port SQLite wasm, nhưng file DB lớn hơn dự kiến, nén cũng không hiệu quả, và tải lên cũng chậm
    Cuối cùng tôi chuyển sang tự tạo index trực tiếp từ file TSV gốc rồi nén bằng zstd, sau đó giải nén mỗi lần trong wasm. Cách này nhanh hơn SQLite rất nhiều
    Kích thước module cũng giảm từ 800KB xuống 52KB, và ngay cả khi chạy nhiều instance cùng lúc cũng không thành gánh nặng
    Tôi dùng stringzilla cho tìm kiếm chuỗi, và nó nhanh đến mức khó tin
    SQLite rất tuyệt, nhưng không phải đáp án cho mọi hoàn cảnh

  • Benchmark SQLite vẫn chưa được tối ưu tốt
    Chỉ cần thêm

    db.SetMaxOpenConns(runtime.NumCPU())
    db.SetMaxIdleConns(runtime.NumCPU())
    

    thế này thôi thì trên máy của tôi hiệu năng đã nhảy từ 27,700 r/s lên 89,687 r/s
    Tôi cũng thử prepared statement và đổi timestamp sang int, nhưng khác biệt không lớn

  • Bài viết ổn, nhưng đoạn nói “mọi DB đều truy cập filesystem bằng open()” thì không chính xác
    Các ứng dụng như SQLite dùng mmap để ánh xạ trực tiếp file vào không gian bộ nhớ. Cách này bỏ qua syscall và cho phép truy cập nhanh hơn nhiều
    Ở phần sau bài viết có giải thích quá trình đọc toàn bộ file vào bộ nhớ, nhưng nếu dùng mmap thì có lẽ sẽ tốt hơn

    • Đúng là bài viết đã đơn giản hóa phần IO của DB
      Tuy nhiên, cũng khó nói mmap lúc nào cũng tốt hơn. Có người thích tự xử lý ở logic ứng dụng hơn là phụ thuộc vào API của OS
      Xem thêm bài nghiên cứu liên quan: nghiên cứu về mmap của CMU
    • Backend store mà mmap dùng rốt cuộc cũng vẫn là file trên filesystem
      Cách diễn đạt “hoạt động như open()” có hơi đơn giản hóa, nhưng về mặt kỹ thuật thì vẫn đúng
  • Từ lâu rồi tôi từng làm một web app bán hàng nhỏ bằng Perl, và vì không thể cài gì trên server của ISP nên tôi dùng hash dựa trên file
    Khách hàng đã dùng nguyên như vậy hơn 20 năm cho tới khi qua đời, rồi gia đình tiếp quản và chuyển sang Wordpress
    Lần cuối tôi kiểm tra, số đơn hàng đã lên đến hàng trăm nghìn mà hiệu năng vẫn ổn
    Nhờ phần cứng phát triển, kiểu hacky này đã sống lâu hơn tôi tưởng. Nếu là bây giờ thì có lẽ SQLite cũng đã quá đủ

    • Tôi tò mò không biết trang đó bán sản phẩm gì
  • Nếu tự tay triển khai storage, bạn sẽ hiểu DB hoạt động như thế nào
    Bạn phải xử lý index và cấu trúc dữ liệu sao cho hiệu quả, rồi cuối cùng sẽ đi đến kết luận rằng “nếu không phải đồ chơi thì lẽ ra ngay từ đầu nên dùng DB”

  • Relational Databases Aren’t Dinosaurs, They’re Sharks
    So với lợi ích ít ỏi có được ở ứng dụng nhỏ, thì sự lãng phí thời gian vì phát minh lại bánh xe còn lớn hơn nhiều

    • So sánh cá mập với khủng long thật sự rất chuẩn
      Từ thời Phấn trắng, cá mập đã gần như có hình dạng giống bây giờ, và sau đó vẫn sống sót mà không thay đổi nhiều
      Trong khi đó khủng long, thằn lằn bay và mosasaurus đã biến mất, còn cá mập, cá sấu và rắn lớn thì gần như vẫn tồn tại nguyên vẹn cho đến nay nhờ thiết kế được tối ưu hóa
      Tôi nghĩ DB quan hệ cũng là kiểu tồn tại như vậy
  • Tôi thích đọc những bài như thế này
    Dù vậy, trong 99% trường hợp tôi vẫn dùng DB có SQL và transaction
    Tuy nhiên gần đây, trong một dự án cá nhân, tôi đã thử quản lý dữ liệu bằng một filesystem đơn giản dựa trên file YAML, và ở quy mô của tôi thì hoàn toàn không có vấn đề hiệu năng
    Điều quan trọng hơn hiệu năng là con người có thể đọc được và có thể diff
    Nhưng trong đa số trường hợp, tôi vẫn sẽ chọn DB có ngôn ngữ truy vấn và tính nhất quán được đảm bảo

  • Cuối cùng rồi lúc nào bạn cũng sẽ cần các tính năng của DB và đảm bảo ACID
    Mỗi khi buộc phải dùng legacy flat-file store, tôi lại phải chật vật vá thêm tính nhất quán, transaction và ngôn ngữ truy vấn. Rốt cuộc vẫn là tự phát minh lại bánh xe

  • Khi cần tính nguyên tử, DB là thứ bắt buộc
    Việc triển khai ghi nguyên tử trên filesystem là cực kỳ mong manh
    Vì lý do này mà nhiều DB gặp vấn đề hỏng dữ liệu khi crash. Trước đây RocksDB trên Windows từng như vậy

    • Nếu cần thay đổi nguyên tử trên file thì tôi sẽ cứ dùng SQLite
      Tự triển khai cảm giác như một việc điên rồ. Học cách ghi an toàn bằng API của OS thì cũng tốt, nhưng giờ đó là một kỹ năng quá ngách
      Hơn nữa khả năng người kế nhiệm không bảo trì nổi là rất cao. Cuối cùng rồi cũng sẽ quay về DB thôi
    • Đoạn code trong bài, chỉ cần một lần mất điện thôi là sớm muộn gì cũng thành file rỗng
      Tối thiểu phải ghi ra file tạm trên cùng filesystem, fsync, rồi rename để thay thế
    • Với trường hợp đơn giản thì không mong manh đến vậy
      Nếu ghi toàn bộ DB ra file tạm, flush xong rồi move để thay thế thì trên Unix là nguyên tử
      Nhưng cách này hoàn toàn không scale được. Chỉ một cập nhật nhỏ cũng phải ghi lại toàn bộ file, lại còn cần quản lý lock. Nó chỉ giải quyết được một phần của ACID
    • Nhìn theo cách đó thì thực ra bạn đã đang xử lý chữ A trong ACID rồi
      Nhân tiện, DuckDB là một OLAP DB hoạt động rất tốt ngay cả với workload out-of-core
    • Tính đến năm 2025, Linux + ext4 hỗ trợ ghi nguyên tử một khối và nhiều khối
      Liên kết tài liệu chính thức
 

Vẫn có thể sống mà không cần tủ lạnh, nhưng sẽ có những bất tiện.
Nếu đã có thể dùng tủ lạnh thì không có lý do gì để không dùng.

 
foobarman 4 ngày trước

Có phải Nora là kẻ cực hữu của Ilbe không

 

Có vẻ như bình luận này cho thấy lối suy nghĩ bị gò bó của các lập trình viên Hàn Quốc và cả mặt bằng của GeekNews.