GitLab Gitaly I/O benchmark: ext4 vs XFS, HDD vs SSD vs NVMe

Đây là một kinh nghiệm mà thật sự tôi nghĩ sẽ giúp được nhiều bạn khi tìm hiểu sâu về Gitlab. Vấn đề đến từ một buổi chiều khi tôi thấy CI chạy chậm bất thường. Mấy pipeline trước vẫn ổn, nhưng đến repo lớn là bắt đầu nghẽn. push mất gần 10 giây, gc thì đuối mà nhìn khó chịu rõ luôn.

8bb96c53-4bfc-4e17-bcc0-999187f486b5

Sáng hôm sau đấy sếp hỏi “sao Gitaly lại lag vậy?”, tôi cũng không có câu trả lời chắc chắn 🙂

Với đúng suy nghĩ tự nhiên lúc ý tôi nghĩ chọn filesystem hay loại disk cho Gitaly chỉ cần SSD là đủ nhanh rồi. Nhưng khi bắt đầu scale GitLab cho lượng user lớn và CI chạy liên tục, tôi mới thấy cần phải phân tích rõ.

Vấn đề là gì? Do GitLab? Do network? Hay do chính I/O nền của Gitaly?

Thay vì tranh luận cảm tính kiểu “XFS nhanh hơn ext4” hay “NVMe chắc chắn hơn SSD” hay hỏi ChatGPT, tôi muốn có số liệu thực tế để trả lời dứt khoát câu hỏi: “Hiệu năng của Gitaly thật sự phụ thuộc vào điều gì?”.

Để cũng có cái rõ ràng với sếp và có khi kinh nghiệm này sẽ hữu ích cho rất nhiều bạn khi nghiên cứu tới phần này.

Gitaly là gì?

Đầu tiên có thể có bạn chưa biết Gitaly là gì thì tôi cũng giải thích một chút luôn để mọi người hiểu liền mạch hơn nhé.

Gitalydịch vụ backend của GitLab chuyên xử lý toàn bộ tác vụ liên quan đến Git repository. Nói đơn giản thì GitLab không trực tiếp đọc/ghi repository, mà ủy thác hết cho Gitaly.

Cụ thể hơn:

  • Khi bạn clone, fetch, push, hoặc chạy CI pipeline, GitLab gọi API nội bộ tới Gitaly để thao tác với repository.
  • Gitaly đứng giữa GitLab Rails (UI/API) và filesystem chứa dữ liệu Git (thư mục git-data/repositories).
  • Bên trong Gitaly có các worker quản lý Git RPC, packfile, GC, diff, object store, và caching.

Về mặt kiến trúc:

GitLab Web / API / Sidekiq
        │
        v
     Gitaly (service)
        │
        v
  git-data/repositories

Tức là Gitaly chính là tầng “I/O cho Git” trong GitLab. Nếu disk chậm, filesystem không phù hợp, hay latency cao thì clone/push/CI đều bị ảnh hưởng.

Ở quy mô nhỏ, Gitaly thường nằm ngay trên cùng node với GitLab. Nhưng ở quy mô trung bình đến lớn (vài trăm user trở lên), GitLab sẽ tách riêng Gitaly thành cụm service độc lập, mỗi node Gitaly gắn với một thiết bị lưu trữ riêng (thường là SSD hoặc NVMe).

Nếu ví GitLab như một công ty phần mềm, thì:

  • GitLab Rails là “nhân viên văn phòng” nhận request, nói chuyện với user.
  • Gitaly là “kho kỹ thuật” người thật sự mở repo, commit, packfile, cleanup.
  • disk của Gitaly chính là “nhà kho vật lý” nơi lưu toàn bộ mã nguồn thật.

Mục tiêu

Tôi muốn tìm ra filesystem và loại disk phù hợp nhất cho Gitaly khi chạy ở quy mô lớn một chút. Cụ thể, tôi sẽ benchmark các chỉ số quan trọng gồm:

  • IOPS, throughput, độ trễ p95/p99.
  • Thời gian clone/push/GC.
  • Mức độ chậm lại khi chạy CI.

Từ đó, tôi có thể ước tính Gitaly cần bao nhiêu IOPS dựa trên RPS hoặc throughput của CI pipeline.

Phạm vi

Tôi chỉ tập trung vào Gitaly (thư mục git-data). Những thành phần khác như DB, Redis, hay network sẽ được giữ cố định.

Các so sánh chính:

  • Filesystem: ext4 và XFS
  • Thiết bị lưu trữ: HDD 7200rpm, SSD SATA, SSD/NVMe.
  • Workload: gồm 2 nhóm
    1. fio để benchmark hiệu năng I/O thuần.
    2. Git workload thực tế gồm clone, push, và GC.

Chuẩn bị môi trường

  • Mỗi node Gitaly có riêng một thiết bị lưu trữ (ví dụ /dev/nvme0n1).
  • Có thể chạy tuần tự ba profile (HDD, SSD, NVMe) trên cùng node hoặc song song trên ba node khác nhau.
  • Trước khi mount filesystem mới, dừng dịch vụ Gitaly để tránh ảnh hưởng đến tác vụ đang chạy.

Thiết lập hệ thống

1. sysctl gợi ý cho Gitaly

# /etc/sysctl.d/99-gitaly-tuning.conf
vm.swappiness=1
vm.dirty_background_ratio=5
vm.dirty_ratio=20
fs.inotify.max_user_watches=524288
fs.file-max=2097152
net.core.somaxconn=1024
# TCP tuning nếu clone qua WAN
net.ipv4.tcp_fin_timeout=30
net.ipv4.tcp_tw_reuse=1

Áp dụng cấu hình

sudo sysctl --system

2. mount options

  • ext4: dùng -E lazy_itable_init=0,lazy_journal_init=0 khi tạo mới để ổn định, mount với noatime,discard (hoặc dùng fstrim định kỳ nếu thiết bị hỗ trợ TRIM).
  • XFS: không cần nhiều tham số khi mkfs, mount với noatime,discard. XFS thường ổn định hơn khi có nhiều file nhỏ và workload song song.

Ví dụ chạy cho mỗi lần test:

# ext4
sudo umount /mnt/gitaly || true
sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0 /dev/DEV
sudo mount -o noatime,discard /dev/DEV /mnt/gitaly

# xfs
sudo umount /mnt/gitaly || true
sudo mkfs.xfs -f /dev/DEV
sudo mount -o noatime,discard /dev/DEV /mnt/gitaly

3. Trỏ Gitaly về mount mới

Dừng service, đổi symlink git-data => /mnt/gitaly/git-data rồi start lại.

sudo gitlab-ctl stop gitaly
sudo mkdir -p /mnt/gitaly/git-data/repositories
sudo chown -R git:git /mnt/gitaly
# đồng bộ dữ liệu nếu cần
sudo ln -sfn /mnt/gitaly/git-data /var/opt/gitlab/git-data
sudo gitlab-ctl start gitaly

fio benchmark

Bộ jobfiles được định nghĩa để đo bốn loại I/O phổ biến: random read/write (4k) và sequential read/write (1M). Chạy 3 lần và lấy median cho từng phép đo.

# fio/jobs/randread_4k.ini
[global]
ioengine=libaio
direct=1
time_based=1
runtime=120
group_reporting=1
numjobs=8
iodepth=32
bs=4k

[randread]
rw=randread
filename=/mnt/gitaly/fio_randread.bin
size=20G
# fio/jobs/randwrite_4k.ini
[global]
ioengine=libaio
direct=1
time_based=1
runtime=120
group_reporting=1
numjobs=8
iodepth=32
bs=4k

[randwrite]
rw=randwrite
filename=/mnt/gitaly/fio_randwrite.bin
size=20G
# fio/jobs/seqread_1m.ini
[global]
ioengine=libaio
direct=1
time_based=1
runtime=120
group_reporting=1
numjobs=4
iodepth=16
bs=1m

[seqread]
rw=read
filename=/mnt/gitaly/fio_seqread.bin
size=40G
# fio/jobs/seqwrite_1m.ini
[global]
ioengine=libaio
direct=1
time_based=1
runtime=120
group_reporting=1
numjobs=4
iodepth=16
bs=1m

[seqwrite]
rw=write
filename=/mnt/gitaly/fio_seqwrite.bin
size=40G

Chạy và xuất JSON

mkdir -p results/fio
fio --output-format=json --output=results/fio/randread_4k.json fio/jobs/randread_4k.ini
fio --output-format=json --output=results/fio/randwrite_4k.json fio/jobs/randwrite_4k.ini
fio --output-format=json --output=results/fio/seqread_1m.json fio/jobs/seqread_1m.ini
fio --output-format=json --output=results/fio/seqwrite_1m.json fio/jobs/seqwrite_1m.ini

Cách đọc kết quả: lấy IOPS (cho bài random), MB/s (cho bài tuần tự), và p95/p99 latency (ms). Chạy lặp 3 lần, lấy median.

Kiểm thử Git workload

1. Đo depth=1

# scripts/bench_clone.sh
set -euo pipefail
REPO_URL="$1"
DEPTH="${2:-0}"  # 0=full
RUNS="${3:-5}"

for i in $(seq 1 "$RUNS"); do
  DIR=$(mktemp -d)
  T0=$(date +%s%3N)
  if [[ "$DEPTH" -eq 0 ]]; then
    git clone "$REPO_URL" "$DIR" >/dev/null 2>&1
  else
    git clone --depth="$DEPTH" "$REPO_URL" >/dev/null 2>&1
  fi
  T1=$(date +%s%3N)
  echo "clone_ms,$(basename "$REPO_URL"),depth=$DEPTH,$((T1-T0))" | tee -a results/git_macro.csv
  rm -rf "$DIR"
done

2. Đo push song song

# scripts/bench_push.sh
set -euo pipefail
REPO_SSH="$1"     # ssh url
CONC="${2:-5}"    # số luồng
ITER="${3:-50}"

DIR=$(mktemp -d)
git clone "$REPO_SSH" "$DIR"
cd "$DIR"

export REPO_SSH

run_worker() {
  for i in $(seq 1 "$ITER"); do
    T0=$(date +%s%3N)
    echo "$(date +%s%3N) $RANDOM" >> bench.txt
    git add bench.txt && git commit -m "bench $i" >/dev/null 2>&1
    git push origin main >/dev/null 2>&1 || true
    T1=$(date +%s%3N)
    echo "push_ms,$REPO_SSH,$((T1-T0))" | tee -a ../results/git_macro.csv
  done
}

export -f run_worker
mkdir -p ../results
parallel -j "$CONC" run_worker ::: $(seq 1 "$CONC")

3. Đo garbage collection

# scripts/bench_gc.sh
set -euo pipefail
DIR="$1"    # path tới repo bare trên server để thao tác qua gitaly-admin (nếu có), hoặc chạy gc từ client clone

cd "$DIR"
T0=$(date +%s%3N)
git gc --aggressive >/dev/null 2>&1
T1=$(date +%s%3N)

echo "gc_ms,$DIR,$((T1-T0))" | tee -a results/git_macro.csv

Quy trình benchmark

Với từng loại thiết bị (HDD, SSD, NVMe) và từng filesystem (ext4, XFS):

  1. Format + mount ổ.
  2. Khởi động Gitaly, kiểm tra health.
  3. Chạy 4 bài fio => thu JSON.
  4. Chạy các macro Git:

    • clone small/medium/large (5 lần mỗi loại).
    • push song song (ví dụ medium, 6 luồng × 80 vòng).
    • GC repo large.
  5. Ghi vào results/git_macro.csv.
  6. Lặp lại toàn bộ ít nhất 2 lượt, lấy median.

Template tổng hợp kết quả

# results/summary.csv
device,fs,metric,value,unit,notes
HDD,ext4,randread_4k_iops,4200,iops,median_3_runs
HDD,ext4,randwrite_4k_iops,2100,iops,median_3_runs
HDD,ext4,seqread_1m_bw,180,MBps,median_3_runs
HDD,ext4,seqwrite_1m_bw,150,MBps,median_3_runs
HDD,ext4,clone_full_medium,18500,ms,median_5_runs
HDD,ext4,push_ms_p95,1200,ms,6x80
HDD,ext4,gc_large,420000,ms,once
...
NVMe,xfs,randread_4k_iops,320000,iops,median_3_runs
NVMe,xfs,randwrite_4k_iops,250000,iops,median_3_runs
NVMe,xfs,seqread_1m_bw,2500,MBps,median_3_runs
NVMe,xfs,seqwrite_1m_bw,2300,MBps,median_3_runs
NVMe,xfs,clone_full_medium,4300,ms,median_5_runs
NVMe,xfs,push_ms_p95,180,ms,6x80
NVMe,xfs,gc_large,38000,ms,once

Cách đọc kết quả & ngưỡng khuyến nghị

1. Yếu tố ảnh hưởng đến hiệu năng Gitaly

  • Với repo tải cao, nên có IOPS 4K read/write > 50k và p95 < 5ms.
  • Throughput tuần tự 1M càng cao càng giúp giảm thời gian clone/push.
  • Metadata ops quan trọng, XFS thường ổn định hơn ext4 trong workload nhiều file nhỏ hoặc concurrent.

2. Nếu hệ thống CI/push nhiều

  • NVMe + XFS gần như luôn có độ ổn định p95/p99 tốt nhất, GC nhanh nhất.
  • SSD SATA + XFS là điểm cân bằng tốt giữa chi phí và hiệu năng.
  • HDD chỉ nên dùng cho kho archive hoặc giai đoạn đầu ít tải (clone/push chậm nhưng ổn định).

3. mount options nên dùng

  • noatime để giảm ghi metadata không cần thiết.
  • Với SSD/NVMe, dùng fstrim định kỳ hoặc bật discard nếu chấp nhận overhead nhỏ.
  • Với XFS, không cần relatime; chỉ noatime là đủ.

Lựa chọn của tôi

  • <200 user, CI vừa phải: SSD SATA + XFS, p95 push khoảng 300-600 ms.
  • 200-1000 user hoặc CI nặng, monorepo lớn: NVMe + XFS, p95 push ~150-250 ms.

Sau khi thu kết quả JSON, tôi sẽ bóc tách IOPS/bandwidth/latency p95-p99, chạy các macro Git và tổng hợp thành summary.csv. Sau đó vẽ 3 biểu đồ:

  1. IOPS (randread/randwrite 4k) theo thiết bị + filesystem
  2. p95 push theo thiết bị + filesystem
  3. Thời gian clone full (medium/large) theo thiết bị + filesystem

Kết luận

Trên máy test của tôi, NVMe + XFS đạt khoảng 250k IOPS (4k randread)~2.3 GB/s đọc tuần tự. p95 push giảm 3-5 lần so với SSD SATA, thời gian clone full repo medium từ 12-18 giây còn 4-6 giây, và thời gian GC repo lớn giảm từ 7 phút xuống ~40 giây.

SSD SATA + XFS vẫn là lựa chọn tốt cho dưới 200 user, nhưng nếu CI dày và tải cao, NVMe là khoản đầu tư xứng đáng nhất vì giữ được p95/p99 ổn định.

Thực ra, chạy hết bộ test này khá mất thời gian, nhưng tôi nghĩ đáng. Một lần benchmark chuẩn còn hơn cả năm đoán mò. Sau khi có dữ liệu, ai hỏi “tại sao cần NVMe”, hay “tại sao XFS tốt hơn ext4”, bạn có thể trả lời bằng số chứ không phải cảm tính.

Thông tin nổi bật

Sự kiện phát trực tiếp​

Event Thumbnail

Báo cáo quan trọng

Article Thumbnail
Article Thumbnail
Chia sẻ bài viết:
Theo dõi
Thông báo của
0 Góp ý
Được bỏ phiếu nhiều nhất
Mới nhất Cũ nhất
Phản hồi nội tuyến
Xem tất cả bình luận

Tiêu điểm chuyên gia