Sao lưu - Backup

Catalog: trái tim của Bareos

Catalog: trái tim của Bareos
Chia sẻ

Mất catalog là có dữ liệu mà không biết là gì. Cách bảo vệ catalog PostgreSQL của Bareos, các bẫy khi truy vấn và bốn câu SQL hữu ích.

Dữ liệu backup nằm trên volume, nhưng thứ cho biết volume nào chứa gì lại nằm trong catalog. Mất catalog thì bạn vẫn có hàng chục TB dữ liệu, chỉ là không còn biết đó là gì, của máy chủ nào, mốc nào. Bài này nói về cách bảo vệ catalog và cách khai thác nó.

Bài 7 đã đọc tỷ lệ nén từ bảng log của catalog. Thực ra gần như mọi câu hỏi vận hành đều trả lời được từ đó: job nào lỗi, pool nào sắp đầy, volume nào sắp được tái dùng. Bài này trả lời ba câu hỏi: catalog chứa gì, làm sao để không mất nó, và truy vấn nó thế nào cho đúng.

Catalog chứa gì

Catalog là một cơ sở dữ liệu, với Bareos hiện nay thường là PostgreSQL. Director ghi vào đó mọi thứ nó biết: job đã chạy, file đã lấy, volume nào nằm trong pool nào. Vài bảng chính cần biết:

BảngMỗi dòng làCột hay dùng
jobMột lần chạy jobjobid, name, type, level, jobstatus, jobbytes, jobfiles, schedtime, starttime, endtime, poolid
poolMột poolpoolid, name, maxvols, volretention
mediaMột volumemediaid, volumename, poolid, volstatus, volbytes, lastwritten, volretention
jobmediaMột đoạn dữ liệu của job nằm trên volumejobid, mediaid
file, pathMột file trong một jobTên file, đường dẫn, thuộc tính
logMột dòng báo cáo của jobjobid, time, logtext
Sơ đồ minh hoạ: Catalog: trái tim của Bareos

Một vài mã trạng thái sẽ gặp liên tục. Cột type: B là backup, R là restore. Cột level: F, I, D cho Full, Incremental, Differential. Cột jobstatus:

MãNghĩa
TThành công
WThành công nhưng có cảnh báo
E, e, fLỗi, lỗi không nghiêm trọng, lỗi nặng
ABị huỷ
IDừng giữa chừng, chưa hoàn tất
R, CĐang chạy, đang chờ chạy

Mất catalog thì mất gì

Volume trên đĩa vẫn còn nguyên. Nhưng để restore, Director cần biết file cần tìm nằm trong job nào, job đó nằm trên volume nào, ở vị trí nào. Tất cả thông tin đó nằm trong catalog.

Không có catalog, bạn còn công cụ bscan: đọc lại từng volume và dựng lại bản ghi vào catalog mới. Cách này dùng được, nhưng phải đọc toàn bộ dữ liệu trên kho. Với kho vài chục TB, riêng việc đọc có thể mất nhiều ngày, đúng lúc bạn đang cần restore gấp nhất.

Vì vậy, catalog đáng được bảo vệ ngang với chính dữ liệu backup. Nó nhỏ hơn dữ liệu rất nhiều lần, nên bảo vệ nó cũng rẻ hơn nhiều.

Bảo vệ catalog: job BackupCatalog là chưa đủ

Bareos cài sẵn một job tên BackupCatalog. Trước khi chạy, nó gọi script dump catalog ra file; sau đó backup file dump đó như một job thường; chạy xong thì xoá file dump tạm. Job này thường mang số Priority lớn hơn các job khác, tức ưu tiên thấp hơn, để chạy sau cùng trong đêm.

Điểm yếu của nó: bản backup catalog nằm trong chính kho của máy chủ backup đó. Nếu máy chủ backup hỏng đĩa hệ thống, hỏng kho, hoặc bị xoá nhầm, bạn mất cả catalog lẫn bản sao của nó cùng lúc. Nên giữ BackupCatalog chạy, nhưng coi nó là lớp thứ nhất, không phải lớp duy nhất.

Lớp thứ hai là dump định kỳ bằng pg_dump và đẩy ra khỏi máy chủ backup, kèm theo cả thư mục cấu hình:

#!/bin/bash
# /usr/local/sbin/sao-luu-catalog.sh
set -euo pipefail
DICH=/var/backups/bareos-catalog
NGAY=$(date +%F)
install -d -m 0700 "$DICH"

sudo -u postgres pg_dump -Fc bareos > "$DICH/catalog-$NGAY.dump"
tar -czf "$DICH/cauhinh-$NGAY.tar.gz" /etc/bareos

# Giu 14 ngay
find "$DICH" -name 'catalog-*.dump' -mtime +14 -delete
find "$DICH" -name 'cauhinh-*.tar.gz' -mtime +14 -delete

# Day ra may chu khac
rsync -a "$DICH/" saoluu@backup02.example.com:/srv/bareos-catalog/

Chạy nó bằng systemd timer, ví dụ mỗi sáng sau khi các job đêm đã xong:

# /etc/systemd/system/sao-luu-catalog.timer
[Timer]
OnCalendar=*-*-* 05:00
Persistent=true

[Install]
WantedBy=timers.target

Bản dump chứa danh sách máy chủ, đường dẫn và tên file của cả hệ thống. Hãy bảo vệ nó như dữ liệu nhạy cảm: quyền 0700, tài khoản đích chỉ được ghi vào đúng một thư mục.

Cuối cùng, có file dump chưa có nghĩa là khôi phục được. Thỉnh thoảng hãy thử nạp nó vào một cơ sở dữ liệu tạm:

sudo -u postgres createdb thu_khoi_phuc
sudo -u postgres pg_restore -d thu_khoi_phuc /var/backups/bareos-catalog/catalog-2026-01-15.dump
sudo -u postgres psql -d thu_khoi_phuc -c "SELECT count(*), max(jobid) FROM job"
sudo -u postgres dropdb thu_khoi_phuc

Trong quá trình vận hành, đội kỹ thuật Cloudzone từng gặp một máy chủ backup chạy đều đặn một thời gian mà bản dump catalog duy nhất lại nằm ngay trên chính máy đó. Không có sự cố nào xảy ra, nhưng đó là rủi ro chỉ lộ ra khi đi rà lại từng lớp.

Kích thước catalog tỷ lệ với số file, không phải số byte

Bảng file có một dòng cho mỗi file trong mỗi job. Một máy chủ chứa một file cơ sở dữ liệu 500 GB chỉ tốn một dòng. Một máy chủ web chứa hai triệu file ảnh nhỏ tốn hai triệu dòng cho mỗi bản Full, cộng thêm số file thay đổi của mỗi bản Incremental.

Nên khi ước lượng catalog, hãy đếm file, đừng đếm TB. Xem bảng nào đang lớn:

SELECT relname,
       pg_size_pretty(pg_total_relation_size(relid)) AS kich_thuoc,
       n_live_tup AS so_dong
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 5;

Nếu bảng file phình quá nhanh, hãy xem lại retention của bản ghi file. Khi bản ghi file đã bị dọn, job vẫn restore được nguyên khối, nhưng không còn chọn được từng file lẻ qua giao diện. Đây là đánh đổi, không phải lỗi.

Truy vấn catalog: người dùng chỉ-đọc và ba cái bẫy

Tạo người dùng chỉ-đọc cho báo cáo

Script báo cáo, dashboard hay công cụ giám sát không nên dùng tài khoản bareos của Director. Một câu UPDATE viết nhầm là đủ làm hỏng catalog. Tạo tài khoản riêng chỉ có quyền đọc:

CREATE ROLE bao_cao_ro LOGIN;
\password bao_cao_ro
GRANT CONNECT ON DATABASE bareos TO bao_cao_ro;
GRANT USAGE ON SCHEMA public TO bao_cao_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO bao_cao_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE bareos IN SCHEMA public
  GRANT SELECT ON TABLES TO bao_cao_ro;

Dòng cuối để bảng Bareos tạo thêm khi nâng cấp cũng tự có quyền đọc. Trong pg_hba.conf, chỉ mở cho đúng nguồn cần đọc, ví dụ chỉ trên máy chủ backup:

host  bareos  bao_cao_ro  127.0.0.1/32  scram-sha-256

Bẫy một: thời gian không múi giờ và extract(epoch)

Các cột starttime, endtime, schedtime, lastwritten có kiểu timestamp không múi giờ. Director ghi vào đó giờ địa phương, ví dụ giờ Việt Nam.

Hàm extract(epoch FROM ...) trên kiểu này luôn coi giá trị là giờ UTC, bất kể cơ sở dữ liệu đặt múi giờ nào. Đổi epoch đó ngược ra giờ thật sẽ lệch đúng bằng độ lệch múi giờ, ở Việt Nam là 7 tiếng:

-- sai: lech 7 tieng o Viet Nam
SELECT to_timestamp(extract(epoch FROM starttime)) FROM job WHERE jobid = 1234;

-- dung: in thang gia tri da luu
SELECT to_char(starttime, 'YYYY-MM-DD HH24:MI:SS') FROM job WHERE jobid = 1234;

Hiệu của hai epoch vẫn đúng, vì hai bên lệch cùng một lượng. Nên extract(epoch FROM endtime - starttime) để tính thời lượng job là an toàn. Chỉ khi cần giờ thật mới phải tránh.

Bẫy hai: ký tự ngoài ASCII trong câu SQL

Catalog Bareos thường được tạo với encoding SQL_ASCII. Một số thư viện, ví dụ psycopg của Python, khi đó mã hoá câu lệnh bằng bảng mã ASCII. Chỉ cần một chữ tiếng Việt có dấu trong câu SQL, kể cả trong dòng chú thích, là cả câu lệnh lỗi trước khi tới được máy chủ.

Cách tránh: giữ câu SQL gửi tới catalog hoàn toàn ASCII, đặt chú thích tiếng Việt ở phía code. Các ví dụ trong bài này cố ý viết chú thích không dấu vì lý do đó.

Bẫy ba: đếm và nối bảng

  • Nối jobmedia sinh dòng lặp. Một job ghi nhiều đoạn vào cùng một volume thì có nhiều dòng jobmedia. Muốn danh sách volume của một job phải dùng DISTINCT.
  • Đếm volume dùng count(m.mediaid), không dùng count(*). Khi LEFT JOIN pool với media, pool rỗng vẫn sinh một dòng toàn NULL; count(*) sẽ đếm nó thành một volume.
  • Cột kiểu ký tự đơn trả về dạng bytes. jobstatus và level có kiểu "char" của PostgreSQL. Một số thư viện trả chúng về dạng bytes thay vì chuỗi, nên so sánh với 'T' trong code sẽ luôn sai nếu chưa giải mã.

Bốn câu SQL hữu ích

Máy chủ nào đang lỗi, tính theo lần chạy gần nhất

Một job lỗi lúc 21:00 rồi tự chạy lại thành công lúc 23:00 thì không cần ai thức dậy. Vì vậy nên xét lần chạy gần nhất của mỗi job, không phải mọi dòng lỗi trong 24 giờ:

SELECT name, jobid, level, jobstatus,
       to_char(starttime, 'YYYY-MM-DD HH24:MI') AS bat_dau
FROM (
    SELECT DISTINCT ON (name) name, jobid, level, jobstatus, starttime
    FROM job
    WHERE type = 'B'
      AND schedtime > now() - interval '24 hours'
    ORDER BY name, jobid DESC
) gan_nhat
WHERE jobstatus IN ('E', 'e', 'f', 'A', 'I')
ORDER BY name;

Sắp theo jobid thay vì starttime: job bị huỷ trước khi kịp chạy có starttime rỗng, và PostgreSQL xếp giá trị rỗng lên đầu khi sắp giảm dần.

Dung lượng và số volume theo pool

SELECT p.name AS pool,
       count(m.mediaid) AS so_volume,
       p.maxvols AS tran,
       pg_size_pretty(COALESCE(sum(m.volbytes), 0)) AS dung_luong,
       count(m.mediaid) FILTER (WHERE m.volstatus = 'Append') AS dang_ghi,
       count(m.mediaid) FILTER (WHERE m.volstatus IN ('Full', 'Used')) AS da_day,
       count(m.mediaid) FILTER (WHERE m.volstatus IN ('Purged', 'Recycle')) AS tai_dung_duoc
FROM pool p
LEFT JOIN media m ON m.poolid = p.poolid
GROUP BY p.poolid, p.name, p.maxvols
ORDER BY COALESCE(sum(m.volbytes), 0) DESC;

Khi so_volume tiến gần tran mà tai_dung_duoc bằng 0, pool sắp hết chỗ ghi. Job sẽ đứng chờ với thông báo không tìm được volume để ghi.

Volume sắp hết hạn giữ

SELECT m.volumename, p.name AS pool, m.volstatus,
       to_char(m.lastwritten, 'YYYY-MM-DD HH24:MI') AS ghi_lan_cuoi,
       to_char(m.lastwritten + m.volretention * interval '1 second',
               'YYYY-MM-DD HH24:MI') AS het_han,
       pg_size_pretty(m.volbytes) AS dung_luong
FROM media m
JOIN pool p ON p.poolid = m.poolid
WHERE m.volstatus IN ('Full', 'Used')
  AND m.lastwritten + m.volretention * interval '1 second'
      < now() + interval '3 days'
ORDER BY m.lastwritten;

Dùng m.volretention của từng volume, đừng dùng p.volretention của pool. Mỗi volume mang theo hạn giữ từ lúc được tạo. Nếu bạn đổi retention của pool sau đó, volume cũ vẫn giữ giá trị cũ, và hai con số có thể khác nhau.

Volume đã quá hạn vẫn có thể còn trạng thái Full hay Used. Bareos dọn theo kiểu lười: chỉ prune khi cần một volume mới để ghi. Đừng cộng thẳng dung lượng các volume này vào phần "đã dùng" khi ước lượng chỗ trống.

Máy chủ quá 26 giờ chưa có bản thành công

SELECT name,
       to_char(max(endtime), 'YYYY-MM-DD HH24:MI') AS thanh_cong_gan_nhat
FROM job
WHERE type = 'B'
  AND jobstatus IN ('T', 'W')
GROUP BY name
HAVING max(endtime) < now() - interval '26 hours'
ORDER BY max(endtime);

Câu này bắt được job im lặng: không lỗi, chỉ đơn giản là không chạy. Nó có hai điểm mù. Job chưa từng thành công lần nào sẽ không xuất hiện. Job đã gỡ khỏi cấu hình vẫn xuất hiện mãi. Hãy đối chiếu với danh sách job đang cấu hình trước khi báo động.

Các câu so với now() chỉ đúng khi múi giờ phiên PostgreSQL khớp với múi giờ Director ghi vào catalog. Đây chính là cái bẫy timezone đã nói ở bài 2.

Tóm tắt

  • Catalog cho biết cái gì nằm ở đâu; mất nó thì chỉ còn bscan, chậm đúng lúc cần nhanh nhất.
  • Giữ BackupCatalog, nhưng thêm pg_dump định kỳ đẩy ra máy chủ khác, kèm /etc/bareos, và thử khôi phục bản dump.
  • Kích thước catalog tỷ lệ với số file, không phải số byte.
  • Báo cáo dùng tài khoản chỉ-đọc; tránh extract(epoch) khi cần giờ thật, giữ câu SQL toàn ASCII.
  • Xét lần chạy gần nhất của mỗi job, và dùng hạn giữ của từng volume thay vì của pool.

Bài sau: Diễn tập khôi phục: backup chưa restore thử là chưa có backup — một kịch bản diễn tập trên máy chủ Linux, từ file lẻ tới cơ sở dữ liệu, kèm checklist chạy định kỳ.

Bài viết thuộc chuỗi “Bareos từ cài đặt tới vận hành” của đội kỹ thuật Cloudzone.

Chia sẻ

Bài viết liên quan

Nén, chạy song song và tìm nút cổ chai trong Bareos

Nén, chạy song song và tìm nút cổ chai trong Bareos

LZ4 hay GZIP, cho bao nhiêu job chạy cùng lúc, và vì sao backup chậm thường do phía nguồn chứ không phải đĩa máy chủ backup.

Tính dung lượng backup Bareos theo đỉnh, đừng theo trung bình

Tính dung lượng backup Bareos theo đỉnh, đừng theo trung bình

Công thức '1,5 bản Full + churn trung vị' thường báo thiếu dung lượng backup. Mô phỏng từng ngày bằng Python để tìm đỉnh và đặt Maximum Volumes cho đúng.

Retention trong Bareos: vì sao đĩa backup cứ đầy

Retention trong Bareos: vì sao đĩa backup cứ đầy

Đặt retention 30 ngày mà đĩa vẫn đầy: Bareos xoá theo volume, prune lười, không truncate thì đĩa phình liên tục nguongx quota. Bài viết phân tích và hướng dẫn cách tìm, dọn Incremental mồ côi an toàn.

Full, Incremental hay Differential? Áp dụng trong BareOS

Full, Incremental hay Differential? Áp dụng trong BareOS

Differential không bao giờ tiết kiệm đĩa hơn Incremental, Incremental không bao giờ khôi phục nhanh hơn. Bài viết phân tích chi tiết kèm cách viết Schedule để hai job không chạy chồng.

Thực nghiệm cấu hình Bareos: Job, FileSet, Schedule, Pool nối với nhau thế nào

Thực nghiệm cấu hình Bareos: Job, FileSet, Schedule, Pool nối với nhau thế nào

Một job Bareos chỉ chạy khi năm, sáu resource trỏ đúng vào nhau. Bài này mổ xẻ cây cấu hình, viết bộ file backup máy chủ web01 và vì sao tên job phải ổn định.

Dựng Bareos + PostgreSQL + WebUI trên Ubuntu 24.04, và 3 bẫy cài đặt

Dựng Bareos + PostgreSQL + WebUI trên Ubuntu 24.04, và 3 bẫy cài đặt

Hướng dẫn cài Bareos, PostgreSQL và WebUI trên Ubuntu 24.04, kèm ba lỗi hay gặp khi cài mà tài liệu chính thức không nhắc tới.