Ghi chú của tôi về thiết kế schema Postgres của GitLab (2022)
(shekhargulati.com)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
bigserialcho 380 bảng, dùngserial4cho 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)vàtext, nhưng sử dụng kiểutextthường xuyên hơn. - Kiểu
textkhô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ụngCHECK.
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 timezonevàtimestamp 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ùngtimestamp 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
jsonbtrong nhiều bảng.
Các mẹo khác
- Sử dụng trường kiểm toán như
updated_attrong 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
smallintthay 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
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 + iidcủ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ộ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
Nếu để lộ khóa chính
idcủ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 issueTấ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
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
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
https://play.clickhouse.com/play?user=play#U0VMRUNUIHVuaXEoc...
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 componentCá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
intsangbigint. Cần một chút chuẩn bị và code tùy chỉnh, nhưng có thể làm không downtimeNhì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
bigserialrồi mã hóa khóa: https://github.com/abevoelker/gfc64Tuy 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
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 ứngHã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
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
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à lookupKhi 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
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ó 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
textso vớivarcharTá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
texttrừ 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 varyinghoạt động như một domain trêntext”, 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ùngtextcó 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...
Đổi
varchar(300)thànhvarchar(200)đòi hỏi ghi lại mọi hàng, nhưng cập nhật ràng buộc trên cộttextvề 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ểutextvà ràng buộcCHECKgiúp schema tiến hóa dễ hơn so vớicharacter varyinghoặcvarchar(n)khi có kiểm tra độ dàiCâ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.
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.Đâ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_IIDvàCI_MERGE_REQUEST_IIDnghĩ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
int32vàint64. Có lẽ nên có một kiểu số nguyên 5 byte hỗ trợ cardinality khoảng 1 nghìn tỷ.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?
Nghe nói dùng kiểu UUID v4 native của PostgreSQL thay cho
bigseriallàm kích thước bảng tăng 25% và tốc độ chèn giảm xuống còn 25% so vớibigserial; 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ì.
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.
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.
UUID tạo ra nhiều lần ghi không theo thứ tự, dẫn đến phình page.
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 đó.