1 điểm bởi GN⁺ 2024-02-18 | 1 bình luận | Chia sẻ qua WhatsApp

Ghi chú của tôi về thiết kế schema Postgres của GitLab

  • Bằng cách xem xét schema Postgres của GitLab, tôi muốn so sánh với schema mình đang thiết kế và học các thực hành tốt nhất từ định nghĩa schema của GitLab.
  • GitLab là nền tảng DevOps mã nguồn mở, là đối thủ thay thế cho GitHub và có thể tự host.

Sử dụng loại khóa chính phù hợp

  • Khi cơ sở dữ liệu còn nhỏ thì khó thấy, nhưng khi mở rộng, khóa chính ảnh hưởng đến dung lượng lưu trữ, tốc độ ghi và tốc độ đọc.
  • Trong 573 bảng, GitLab dùng kiểu khóa chính bigserial cho 380 bảng, dùng serial4 cho 170 bảng và phần còn lại 23 bảng dùng khóa chính tổng hợp.

Sử dụng ID nội bộ và ID bên ngoài

  • Việc không phơi bày khóa chính ra thế giới bên ngoài là một thực hành tốt.
  • GitLab sử dụng cả ID nội bộ (id) và ID bên ngoài (iid) trong các bảng như issues, ci_pipelines, deployments, epics.

Sử dụng kiểu dữ liệu text và ràng buộc kiểm tra

  • Schema của GitLab dùng cả character varying(n)text, nhưng sử dụng kiểu text thường xuyên hơn.
  • Kiểu text không có ràng buộc độ dài, và GitLab định nghĩa ràng buộc độ dài bằng cách sử dụng CHECK.

Quy tắc đặt tên

  • Tất cả các bảng đều dùng số nhiều, và tiền tố tên module được dùng để tạo không gian tên.
  • Tên bảng và cột tuân theo quy tắc snake_case.

Sử dụng múi giờ cho timestamp

  • GitLab sử dụng cả timestamp with timezonetimestamp without timezone.
  • Đối với tác vụ hệ thống, GitLab dùng timestamp without timezone; đối với tác vụ của người dùng thì dùng timestamp with timezone.

Ràng buộc khóa ngoại

  • GitLab sử dụng ràng buộc khóa ngoại cho hầu hết các bảng, nhưng không dùng trong một số bảng như audit_events, abuse_reports, web_hooks_logs, spam_logs.

Phân vùng các bảng lớn

  • GitLab phân vùng các bảng có thể có dung lượng lớn để tăng hiệu năng truy vấn.

Hỗ trợ trường hợp sử dụng tìm kiếm LIKE bằng Trigram và gin_trgm_ops

  • GitLab sử dụng chỉ mục GIN (Generalized Inverted Index) để thực hiện tìm kiếm hiệu quả.

Sử dụng jsonb

  • Schema của GitLab sử dụng kiểu dữ liệu jsonb trong nhiều bảng.

Các mẹo khác

  • Sử dụng trường kiểm toán như updated_at trong các bảng có thể chỉnh sửa, và không sử dụng trong các bảng log không thể chỉnh sửa.
  • Enums được lưu bằng smallint thay vì character varying, giúp tiết kiệm dung lượng.

Nhận xét của GN⁺:

  • Thiết kế schema của GitLab cung cấp cái nhìn sâu sắc về thiết kế cơ sở dữ liệu, đặc biệt là các bài học quan trọng về tối ưu hóa schema cho hệ thống quy mô lớn.
  • Vì GitLab là mã nguồn mở, các quyết định thiết kế schema như vậy mang lại ví dụ thực tế để các nhà phát triển khác áp dụng cho dự án của mình.
  • Điều có thể học được từ schema của GitLab là cần cân nhắc kỹ các yếu tố ảnh hưởng lớn đến hiệu năng và bảo trì cơ sở dữ liệu như chọn kiểu dữ liệu, chiến lược lập chỉ mục, phân vùng và việc sử dụng ràng buộc khóa ngoại.

1 bình luận

 
GN⁺ 2024-02-18
Các ý kiến trên Hacker News
  • Tôi thắc mắc vì sao lại cần thông lệ không để lộ khóa chính ra bên ngoài. Nếu yêu cầu dù sao cũng phải được xác thực, tôi cũng không rõ việc ngăn đoán ID có giá trị gì
    Nếu chỉ với một ID đoán được mà có thể làm điều gì đó hữu ích mà không cần xác thực/ủy quyền, thì tức là ở nơi khác đã hỏng nghiêm trọng rồi; nên tập trung vào phần đó thay vì thêm độ phức tạp không cần thiết vào schema. Việc che giấu thông tin cạnh tranh để đối thủ không ước tính được số lượng khách hàng thì có thể có giá trị, nhưng có vẻ GitLab không quá bận tâm đến chuyện đó. Quyết định id + iid của GitLab có khả năng chủ yếu xuất phát từ yêu cầu về hiệu năng truy vấn hơn là để ngăn đoán ID nội bộ

    • Đúng, nhưng việc ID có thể bị đoán được có thể khiến một lỗ hổng bảo mật trở nên khủng khiếp hoặc tệ hơn rất nhiều
      Nếu bạn để lộ UUID cho người dùng, ngay cả khi có cùng lỗ hổng, kẻ tấn công vẫn phải đoán trúng UUID nên khó hơn nhiều, và có thể cần một nguồn thứ cấp. Dù có rò rỉ dữ liệu, bạn vẫn có thời gian ứng phó và có thể ước tính lượng dữ liệu bị rò rỉ. Ngược lại, nếu dùng ID tuần tự, vấn đề sẽ ngay lập tức phình thành rò rỉ toàn diện và có thể trở thành sự cố quy mô lớn phải báo cáo bắt buộc cho cơ quan giám sát dữ liệu cá nhân. Đây là kiểu phòng thủ chiều sâu lẽ ra không cần phải hữu dụng, nhưng thực tế có những phần mềm tệ hại đã bị xuyên thủng theo cách này
    • Như bài viết cũng nói, chuyện này gần với thông tin cạnh tranh hơn là bảo mật. ID tự tăng đơn giản sẽ tiết lộ tổng số bản ghi hoặc tốc độ tăng trưởng của bảng
      Nếu để lộ khóa chính id của bảng issue, thì khi tạo issue trong một dự án, nó không bắt đầu từ 1, nên có thể dễ dàng đoán được trên toàn GitLab có bao nhiêu issue
    • Cụm từ diễn kịch bảo mật bị lạm dụng quá nhiều. Bảo mật có thể và nên được cấu thành từ nhiều lớp; việc một lớp như xác thực bị phá vỡ không nên đồng nghĩa với việc các lớp còn lại cũng trở nên dễ dàng truy cập
      Tất nhiên, nếu chỉ với ID đoán được mà có thể làm gì đó mà không cần xác thực/ủy quyền thì đó đúng là vấn đề lớn. Nhưng nếu tại thời điểm đó không còn lớp nào khác, thì coi như ván cờ đã kết thúc. Bug không báo trước cho ta, đặc biệt là những bug tinh vi. Khi một bug như vậy xuất hiện, nếu ít nhất ID đã được làm cho không thể đoán được, bạn sẽ biết ơn vì đã tránh được tình huống toàn bộ tài khoản người dùng trong hệ thống trở nên dễ dàng truy cập
    • Kiểm soát truy cập cũng có bug. ID không thể đoán được khiến việc khai thác một số bug như vậy khó hơn rất nhiều
      Dĩ nhiên trước hết phải tập trung bảo đảm kiểm soát truy cập đúng đắn, nhưng ID không thể đoán được có thể tạo ra khác biệt giữa một thảm họa khủng khiếp và một sự cố hú vía. Nếu UUID không phù hợp, cũng có thể dùng ID cơ sở dữ liệu tự tăng rồi mã hóa nó; với một lớp phần mềm phù hợp, ID được mã hóa cũng hoạt động gần như tự động
    • Chỉ là khác biệt nhỏ về thuật ngữ, nhưng có vẻ nên gọi đây là thông tin cạnh tranh hơn là “business intelligence”, vốn thường chỉ việc khai thác dữ liệu nội bộ của công ty. https://en.wikipedia.org/wiki/Competitive_intelligence
  • Trong 128 triệu kho lưu trữ công khai, đa số là fork của kho khác và chỉ tồn tại để tạo pull request vào kho chính, nên nếu không nhầm thì có lẽ chúng không có issue
    Những dự án đồ chơi nhỏ hoặc dự án bị bỏ rơi nhanh cũng nhiều khả năng không có hoặc có rất ít issue. Chắc chắn có các dự án có hàng trăm, hàng nghìn issue, nhưng trung bình trên toàn bộ 128 triệu repository có lẽ khá thấp, nên có khả năng vẫn nằm dưới giới hạn 2 tỷ. Dù vậy, tôi đồng ý rằng việc dùng kiểu 4 byte, chính xác là 31 bit, cho bảng đó là một quả bom hẹn giờ đối với một số tổ chức, bao gồm github.com

    • Hiện tại vẫn còn dưới giới hạn, với 362.107.148 repository818.516.506 issue/pull request duy nhất
      https://play.clickhouse.com/play?user=play#U0VMRUNUIHVuaXEoc...
    • Tôi cho rằng quyết định rời xa Rails của GitHub cũng phần nào bị ảnh hưởng bởi một thiếu sót lớn của ActiveRecord, tức là thiếu hỗ trợ khóa chính tổng hợp
      Một yêu cầu cơ bản như PRIMARY KEY(repo_id, issue_id) trở nên phức tạp một cách không cần thiết trong ActiveRecord, và phải dùng cách vòng vo là vừa có khóa duy nhất vừa có một khóa chính riêng để phù hợp với ActiveRecord, vốn yêu cầu một cột khóa chính đơn. Khóa chính UUID cũng trông như một cách né tránh, nhưng ràng buộc duy nhất cho cặp (repo_id, issue_id) vẫn cần thiết, làm tăng kích thước cơ sở dữ liệu và overhead. Nhìn rộng hơn, kiến trúc MVC nguyên khối dựa trên các tầng model/controller/view đơn nhất của Ruby on Rails cũng tạo ra vấn đề về khả năng mở rộng và bảo trì khi ứng dụng lớn dần; tôi cho rằng MVC phù hợp hơn trong kiến trúc mô-đun hoặc dựa trên component
    • Tôi tự hỏi liệu có ai biết chắc GitLab Cloud dùng cơ sở dữ liệu đa tenant hay dùng cơ sở dữ liệu riêng theo người dùng/khách hàng/tổ chức không
      Các sản phẩm cung cấp cả tự host lẫn cloud thường chuộng cơ sở dữ liệu theo từng khách hàng. Vì như vậy phần codebase dùng chung đơn giản hơn nhiều, do có thể dùng cùng truy vấn bất kể kiểu hosting. Nếu là cơ sở dữ liệu theo từng khách hàng thì gần như sẽ không chạm đến các giới hạn sử dụng kiểu đó; còn nếu chạm đến thì tự host có lẽ phù hợp hơn
    • Quả bom hẹn giờ đó là loại bom có thể tháo ngòi bằng một migration 11 giây
    • Có thể migration khóa chính từ int sang bigint. Cần một chút chuẩn bị và code tùy chỉnh, nhưng có thể làm không downtime
      Nhìn chung chúng tôi đang quản lý các migration lớn theo quy trình này, có chỉnh sửa đôi chút để dùng: http://zemanta.github.io/2021/08/25/column-migration-from-in...
      Khóa ngoại, chỉ mục và các ràng buộc nói chung khiến quá trình trở nên khó hơn, nhưng không phải là không thể. Trong trường hợp của tôi, migration dữ liệu mất vài giờ, nhưng không cần phải nhanh. Theo tôi biết, GitLab có công cụ chạy các tác vụ sau nâng cấp để chúng hoạt động ở bất kỳ điểm nào trong quá trình nâng cấp phiên bản
  • Câu chuyện về kích thước lưu trữ của cột UUID không mấy thuyết phục. Nếu bảng có 5 cột khác thì chênh lệch 128-bit so với 64-bit không lớn
    Mối lo quan trọng hơn là hiệu năng. UUIDv4 được hỗ trợ rộng rãi, nhưng vì hoàn toàn ngẫu nhiên nên không lý tưởng cho hiệu năng chỉ mục. UUIDv7[0] gần với Snowflake[1], có tính cục bộ theo thời gian, nhưng các triển khai chưa phổ biến bằng. Một cách tiếp cận riêng là dùng bigserial rồi mã hóa khóa: https://github.com/abevoelker/gfc64
    Tuy nhiên cách này 1) không thể xoay vòng giá trị bí mật và 2) một khi bị lộ, bất kỳ ai cũng có thể ước tính kiểu Fermi về kích thước bảng. Tách ID công khai và ID nội bộ thì phiền phức, còn nếu ID công khai là UUIDv4 thì cũng phải đánh đổi hiệu năng. Tôi cho rằng UUIDv7 là giải pháp đáp ứng được nhiều điều kiện nhất
    [0]: https://uuid7.com/
    [1]: https://en.wikipedia.org/wiki/Snowflake_ID

    • Vấn đề không chỉ là kích thước của riêng cột đó, mà còn là mọi nơi id đó được dùng làm khóa ngoại, cùng kích thước chỉ mục cần cho các cột khóa ngoại tương ứng
      Hãy nghĩ đến một giá trị như ID người dùng, có thể được hàng chục, hàng trăm khóa ngoại tham chiếu trong toàn bộ cơ sở dữ liệu
    • Vấn đề là 5 cột còn lại không được lập chỉ mục
      Hiệu năng cơ sở dữ liệu có ba cấp độ. 1) Cả chỉ mục và dữ liệu đều nằm trong bộ nhớ. 2) Chỉ mục nằm trong bộ nhớ nhưng dữ liệu thì không. 3) Cả chỉ mục và dữ liệu đều không nằm trong bộ nhớ. Nếu ở mức 1 thì tốt, còn nếu không thì phải giữ mức 2 bằng mọi giá. Kích thước chỉ mục tăng gấp đôi khiến việc đó khó hơn
    • Có thể xem khóa chính của cơ sở dữ liệu như con trỏ cơ bản kiểu typedef void*. Kích thước của nó ảnh hưởng đến hiệu năng tổng thể: mức dùng bộ nhớ/đĩa, nút thắt thông lượng, cho đến thời gian CPU dùng để so sánh khóa trong các vòng lặp sâu nhất của join và lookup
      Khi CPU x86-64 mới xuất hiện, tác động hiệu năng của việc chuyển sang con trỏ 64-bit lớn đến mức x32/ilp32 đã được tạo ra, và đó cũng là lý do .NET đến nay vẫn đặt “prefer 32-bit” làm mặc định. Dùng UUID 128-bit làm khóa chính cơ sở dữ liệu là một sai lầm khủng khiếp
    • UUIDv7 cũng không phải vạn năng. Trong nhiều trường hợp, bạn không muốn để lộ thời điểm tạo tài nguyên
      Ví dụ, có thể bạn muốn tải video lên một tháng trước khi công khai, nhưng không muốn khán giả biết điều đó
    • Cũng có những biến thể khác của cách tiếp cận này: https://pgxn.org/dist/permuteseq/
      Cũng có thể mã hóa giá trị khi hiển thị trong URL, email, v.v.: https://wiki.postgresql.org/wiki/Pseudo_encrypt
      Như vậy có thể giữ được nhiều lợi ích của chỉ mục tuần tự, đồng thời vẫn có thể thay đổi khóa. Tuy nhiên nếu đổi khóa thì bookmark sẽ hỏng, các liên kết đã gửi trong email cũ sẽ vô hiệu, và về cơ bản để lại hiệu ứng giống như đổi tên mọi thứ
  • Đây chỉ là bắt bẻ nhỏ, nhưng tôi muốn nói về phần text so với varchar
    Tác giả dùng rất nhiều lời để cố chứng minh một khác biệt hiệu năng không tồn tại, rồi kết luận rằng “không có khác biệt hiệu năng lớn giữa hai kiểu”. Đây là chủ đề đã được kết luận từ lâu, và không phải là “không lớn” mà là “không có”. Wiki PostgreSQL[1] nêu rõ nên dùng text trừ khi có lý do rất chính đáng, còn tài liệu[2] cũng nói “với nhiều mục đích, character varying hoạt động như một domain trên text”, và trong hộp Tip màu xanh nói rằng “không có khác biệt hiệu năng giữa ba kiểu này”. Vì vậy việc GitLab chủ yếu dùng text có vẻ cho thấy họ đã đọc tài liệu và thiết kế schema phù hợp với PostgreSQL, chứ không phải một schema “di động” nửa vời
    [1] https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use...
    [2] https://www.postgresql.org/docs/current/datatype-character.h...

    • Trên thực tế, khi phải migrate schema để phù hợp với thay đổi độ dài chuỗi lưu trữ, sẽ có khác biệt hiệu năng đáng kể
      Đổi varchar(300) thành varchar(200) đòi hỏi ghi lại mọi hàng, nhưng cập nhật ràng buộc trên cột text về cơ bản gần như miễn phí, chỉ cần quét toàn bảng để kiểm tra các giá trị hiện có có thỏa ràng buộc mới hay không. Bài viết cũng nói rằng dùng kiểu text và ràng buộc CHECK giúp schema tiến hóa dễ hơn so với character varying hoặc varchar(n) khi có kiểm tra độ dài
  • Câu khóa ngoại rất đắt được lặp lại thường xuyên, nhưng là một luận điểm hiếm khi có benchmark
    Có nhiều cách triển khai sai, nhưng dù sao ở đâu đó trong stack cũng đang cưỡng chế tính toàn vẹn. Muốn tận dụng cơ sở dữ liệu thay vì tự triển khai lại điều đó thì cần kiến thức và thử nghiệm, và thường sẽ giúp tránh các sự cố lớn

  • Tôi tò mò liệu đã có ai tổng hợp hoặc để ý đến khác biệt về hiệu năng giữa GitLab và GitHub chưa.
    Cả hai đều là ứng dụng dựa trên Rails, nhưng nhìn chung thời gian tải trang của GitLab cảm giác tệ hại so với GitHub.

    • Vài năm trước khi dùng GitLab, tôi gặp vấn đề hiệu năng phía client rất nặng với các pull request lớn. GitHub cũng không lý tưởng, nhưng vẫn xử lý ở mức chấp nhận được.
    • So sánh với GitHub cũng giống như so sánh Chrome với các trình duyệt khác, thậm chí cả các trình duyệt dựa trên Chromium.
      Chrome và GitHub sẽ dùng đủ mọi mánh khóe dù có làm hỏng trải nghiệm người dùng. Ví dụ, có lần tôi mở diff merge trên GitHub của công ty, dùng Ctrl F để tìm kiếm, không có kết quả thì chuyển tiếp và lần mò thủ công lịch sử Git; đến diff thứ 100 mới phát hiện file quan trọng nhất bị giấu sâu bên trong. Có lẽ vì ai đó thấy dễ đạt chỉ số tải trang và được thăng chức hơn.
    • GitHub nhìn chung ổn định và thường khá nhanh, ngoại trừ hai lần sự cố năm ngoái. Nếu không thì tôi đã không dùng phím tắt bàn phím.
      Đây là bài viết của một cựu nhân viên có thể giúp hiểu văn hóa của GitLab và việc xem nhẹ hiệu năng: https://news.ycombinator.com/item?id=39303323
      Tôi không dùng GitLab đủ nhiều để tự cảm nhận vấn đề hiệu năng, nhưng nghĩ bài này có thể hữu ích.
  • Tôi luôn thắc mắc chữ I thêm vào trong các biến CI CI_PIPELINE_IIDCI_MERGE_REQUEST_IID nghĩa là gì.
    Tôi đoán đó là một lựa chọn liên quan đến cơ sở dữ liệu, và bài viết này đã xác nhận điều đó.

  • Nhìn vào câu “1 quintillion bằng 1.000.000.000 billion” thì thấy khá kỳ lạ khi chúng ta hầu như chỉ chọn giữa int32int64. Có lẽ nên có một kiểu số nguyên 5 byte hỗ trợ cardinality khoảng 1 nghìn tỷ.

    • Nếu không đóng gói giá trị thật chặt thì chọn kích thước không phải lũy thừa của 2 là vô lý.
  • Không dùng ID tự tăng có thể hợp lý, nhưng tôi không rõ lợi ích của việc có 2 ID, một cho nội bộ và một cho bên ngoài.
    Số cột và chỉ mục tăng lên, lúc nào cũng phải tra cứu trước, và tôi cũng không nghĩ ra kịch bản bảo mật nào trong đó ta đổi khóa nội bộ mà không đổi khóa bên ngoài. Có điều gì tôi đang bỏ sót không?

    • Khi làm gì đó theo từng project, dù sao ta cũng đã có sẵn thông tin cần thiết. Ngoài ra, để issue của mỗi project bắt đầu từ 1 thân thiện với người dùng hơn là bắt đầu từ một con số như 2 nghìn tỷ 700 tỷ 305 triệu 717 nghìn 325.
  • Nghe nói dùng kiểu UUID v4 native của PostgreSQL thay cho bigserial làm kích thước bảng tăng 25% và tốc độ chèn giảm xuống còn 25% so với bigserial; tôi tò mò vì sao UUIDv4 lại tệ đến vậy.
    UUID chẳng phải chỉ là số 128-bit sao? Không biết chi phí sinh nó cực đắt hay đang xảy ra chuyện gì.

    • UUIDv4 hoàn toàn ngẫu nhiên, còn chỉ mục B-tree kỳ vọng các giá trị “lệch sang phải” có thứ tự hợp lý.
      Vì vậy việc lập chỉ mục trên cột UUIDv4 chậm lại, và đây là động lực phát triển UUIDv6 và UUIDv7.
    • Mức tăng kích thước 25% là đúng, nhưng đó là mức tăng tuyến tính nhỏ và dự đoán được, 8 byte mỗi hàng. So với phần dữ liệu còn lại của hàng thì không đáng lo lắm.
      Vấn đề lớn hơn là tốc độ chèn. Với UUID, tốc độ chèn bị giới hạn bởi lượng RAM khả dụng. Số nguyên tự tăng thì không như vậy. Số nguyên có tương quan với thời gian, còn UUID4 là ngẫu nhiên, nên khi quy mô tăng, đặc tính hiệu năng thay đổi về cơ bản. Với bảng nhỏ, penalty khi chèn gần như có thể bỏ qua, nhưng khi kích thước chỉ mục B-tree chạm giới hạn bộ nhớ, PostgreSQL không thể giữ toàn bộ B-tree UUID trong RAM và phải dựa vào việc thay trang đĩa. Số nguyên tự tăng dùng cùng các trang chỉ mục cho các hàng gần nhau theo thời gian, nên dưới cùng tải không cần chạm đĩa. Khi đạt đến quy mô này, khác biệt không phải là giảm đều 25% mà là vách đá hiệu năng 25 lần, và ngoài migration schema thì gần như chỉ còn cách mua thêm RAM.
    • Tôi nghĩ là do B-tree. B-tree và page hoạt động tốt hơn khi chủ yếu chỉ page cuối được dùng nhiều.
      UUID tạo ra nhiều lần ghi không theo thứ tự, dẫn đến phình page.
    • Khi thứ tự sắp xếp phân bố ngẫu nhiên, cache locality của B-tree trở nên kém. Các lần chèn không đi vào page cuối mà rải rác khắp nơi.
      Locality của chèn theo batch về sau cũng kém khi truy vấn, vì phải tìm các record liên quan một cách ngẫu nhiên. Kết cục là phải trả chi phí cả lúc chèn lẫn lúc select sau đó.