Sao lưu - Backup

Tự động hoá bconsole và những cái bẫy im lặng

Tự động hoá bconsole và những cái bẫy im lặng
Chia sẻ

Script quanh bconsole thường chạy êm mà vẫn làm sai. Sáu cái bẫy hay gặp khi tự động hoá Bareos, mỗi bẫy kèm script sai và script đúng.

Script gọi bconsole thường chạy êm, không báo lỗi, và đôi khi làm sai hoàn toàn. Lỗi nguy hiểm nhất trong tự động hoá Bareos không phải lỗi làm script dừng, mà là lỗi làm script tin rằng mọi việc đã xong.

Bài 9 kết thúc bằng một checklist diễn tập định kỳ. Muốn chạy nó đều đặn, sớm muộn gì bạn cũng viết script quanh bconsole: kiểm Director còn sống, tạm tắt một job, chờ hết job đang chạy rồi reload. Bài này gom sáu cái bẫy hay gặp nhất khi làm việc đó. Mỗi bẫy có một đoạn script “sai” trông rất hợp lý, và một đoạn “đúng” đặt ngay sau.

Vì sao bconsole khó tự động hoá

bconsole được viết cho người gõ tay. Người đọc output bằng mắt, thấy lỗi thì dừng. Script thì chỉ có hai thứ để dựa vào: mã thoát và chuỗi ký tự trong output. Cả hai đều dễ đánh lừa:

  • Mã thoát ít nói lên điều gì. bconsole thường thoát mã 0 kể cả khi không nối được Director.
  • Output viết cho người đọc. Định dạng có thể đổi giữa các phiên bản, và câu báo lỗi dùng lại những từ của câu báo thành công.
  • Một số trạng thái chỉ sống trong bộ nhớ. Lệnh chạy thành công, nhưng hiệu lực mất ở lần reload kế tiếp.

Nguyên tắc chung cho cả bài: tìm dấu hiệu thành công cụ thể, và đọc lại trạng thái thật sau mỗi thao tác ghi. Đừng suy ra kết quả từ việc lệnh chạy không lỗi.

Bẫy 1: bconsole thoát mã 0 dù không nối được Director

Đây là đoạn kiểm tra sức khoẻ hay gặp nhất, và cả hai cách viết dưới đều sai:

# SAI: tin vào mã thoát của bconsole
echo "status dir" | bconsole > /dev/null
if [ $? -eq 0 ]; then
  echo "Director OK"
fi

# SAI: tìm chữ "Director" trong output
if echo "status dir" | bconsole | grep -q "Director"; then
  echo "Director OK"
fi

Cách thứ nhất sai vì mã thoát là 0 ngay cả khi Director đã chết. Cách thứ hai sai tinh vi hơn. Câu báo lỗi kiểu Failed to connect to Director cũng chứa chữ “Director”, nên grep vẫn khớp. Script báo “Director OK” đúng vào lúc Director không trả lời.

Dấu hiệu chắc chắn là dòng bắt tay mà Director gửi về khi kết nối thành công, bắt đầu bằng 1000 OK. Cách viết đúng tìm đúng dòng đó, và bọc bconsole trong timeout để script không treo vô hạn:

#!/bin/bash
set -uo pipefail

# Chạy bconsole có giới hạn thời gian, giữ lại toàn bộ output để soi
bc() {
  timeout 60 bconsole -c /etc/bareos/bconsole.conf <<< "$1"
}

out=$(bc "status dir")
rc=$?

if [ "$rc" -ne 0 ]; then
  echo "bconsole lỗi hoặc quá giờ (mã $rc)" >&2
  exit 2
fi
if ! grep -q '^1000 OK' <<< "$out"; then
  echo "Không nối được Director:" >&2
  tail -n 5 <<< "$out" >&2
  exit 2
fi
echo "Director OK"

Lưu ý set -uo pipefail ở đầu, không có -e. Ở đây chúng ta muốn tự bắt mã thoát để in thông báo rõ ràng, thay vì để shell dừng im lặng.

Bẫy 2: lệnh disable chỉ sống trong bộ nhớ

Cần tạm dừng backup một máy chủ trong đợt bảo trì, cách nhanh nhất là disable job=… trong bconsole. Lệnh chạy ngay, status dir không còn thấy job đó trong lịch. Vấn đề là trạng thái này chỉ nằm trong bộ nhớ của Director:

# SAI: tắt job bằng lệnh bconsole rồi coi như xong
echo "disable job=backup-web01" | bconsole
# ...hôm sau có người thêm job mới và reload:
echo "reload" | bconsole
# backup-web01 lại chạy theo lịch như chưa từng bị tắt

Tài liệu Bareos có ghi rằng trạng thái bật/tắt đặt bằng lệnh sẽ mất khi reload. Nhưng người reload thường không phải người đã tắt job. Họ thêm một máy chủ mới, reload, và vô tình bật lại mọi thứ đang được tắt. Không có cảnh báo nào cả.

Cách bền là ghi Enabled = No vào chính file cấu hình, kiểm cú pháp, rồi mới reload. Sau reload, đọc lại trạng thái từ Director để chắc nó đã nhận:

#!/bin/bash
set -euo pipefail

JOB=backup-web01
FILE=/etc/bareos/bareos-dir.d/job/${JOB}.conf
TMP=$(mktemp)
BAK=${FILE}.bak

cp -p "$FILE" "$BAK"

# Bỏ mọi dòng Enabled cũ, chèn đúng MỘT dòng sau dòng Name.
# Chạy lại bao nhiêu lần kết quả vẫn như nhau.
awk '
  /^[[:space:]]*Enabled[[:space:]]*=/ { next }
  { print }
  /^[[:space:]]*Name[[:space:]]*=/ && !done { print "  Enabled = No"; done = 1 }
' "$BAK" > "$TMP"

install -m 0640 -o root -g bareos "$TMP" "$FILE"
rm -f "$TMP"

# Kiểm cú pháp trước khi nạp; sai thì trả file cũ
if ! bareos-dir -t -c /etc/bareos > /dev/null 2>&1; then
  cp -p "$BAK" "$FILE"
  echo "Cấu hình lỗi, đã trả lại file cũ" >&2
  exit 1
fi

echo "reload" | timeout 60 bconsole > /dev/null || true

# Đọc lại trạng thái THẬT từ Director, không tin lệnh reload
if echo "show job=${JOB}" | timeout 60 bconsole | grep -q 'Enabled[[:space:]]*=[[:space:]]*No'; then
  echo "${JOB}: đã tắt bền"
else
  echo "${JOB}: Director chưa nhận Enabled = No" >&2
  exit 1
fi

Đoạn awk xoá mọi dòng Enabled cũ rồi chèn đúng một dòng mới. Chạy một lần hay mười lần, file cho ra vẫn giống hệt nhau. Tính chất đó sẽ quan trọng ở bẫy 5.

Nếu tạm dừng cả hệ thống, có thể áp cùng cách cho resource Schedule. Một Schedule mang Enabled = No trong file thì không job nào dùng nó khởi động được, dù ai đó reload bao nhiêu lần.

Bẫy 3: cắt khối Running Jobs bằng sed

Trước khi reload hay bảo trì, script thường hỏi “còn job nào đang chạy không”. Output của status dir có một khối Running Jobs:, nên người ta cắt khối đó ra:

Khối Running Jobs trên một máy chủ backup thật lúc không có job: câu No Jobs running. rồi tới dòng ==== (đã che phiên bản và số job)
Khối Running Jobs trên một máy chủ backup thật lúc không có job: câu No Jobs running. rồi tới dòng ==== (đã che phiên bản và số job)
# SAI: cắt từ tiêu đề tới dòng ==== đầu tiên
running=$(echo "status dir" | bconsole | sed -n '/^Running Jobs:/,/^====/p' | grep -c 'backup-')
if [ "$running" -eq 0 ]; then
  echo "Không có job chạy, reload được"
fi

Đoạn này trông đúng, và in ra đúng khối cần xem khi không có job nào. Nhưng khi có job, ngay dưới dòng tiêu đề cột là một dòng kẻ =====. sed dừng ở dòng kẻ đó, tức là dừng trước dòng job đầu tiên. Kết quả trông y hệt “không có job chạy”, đúng vào lúc đang có job.

Cách đúng là cắt theo khối kế tiếp, không theo dòng kẻ, rồi tìm câu khẳng định rõ ràng:

#!/bin/bash
set -uo pipefail

out=$(echo "status dir" | timeout 60 bconsole)
grep -q '^1000 OK' <<< "$out" || { echo "Không nối được Director" >&2; exit 2; }

# Lấy trọn khối từ "Running Jobs:" tới trước "Terminated Jobs:"
khoi=$(awk '/^Running Jobs:/{p=1} /^Terminated Jobs:/{p=0} p' <<< "$out")

if grep -q '^No Jobs running\.' <<< "$khoi"; then
  echo "Không có job chạy"
else
  echo "Đang có job chạy:"
  echo "$khoi"
  exit 1
fi

Script coi “không thấy câu No Jobs running.” là “có job chạy”. Nếu định dạng output đổi ở phiên bản sau, script sẽ nghiêng về phía an toàn: từ chối reload, thay vì reload giữa lúc có job. Khi cần độ chắc cao hơn, truy vấn catalog trực tiếp theo cột trạng thái của bảng Job thường ổn định hơn đọc output dành cho người.

Bẫy 4: reload báo lỗi vẫn có thể đã chạy

Lệnh reload có thể quá giờ vì Director đang bận, hoặc trả output có chữ lỗi. Phản xạ tự nhiên là coi như chưa reload. Nhưng “lệnh báo lỗi” và “Director chưa nạp cấu hình” là hai chuyện khác nhau. Director có thể đã nạp xong, chỉ là phản hồi không về kịp tới script.

Nếu script tin vào thông báo lỗi, nó có thể dừng lại trong khi cấu hình mới đã có hiệu lực. Ngược lại, nếu tin vào mã thoát 0, nó có thể đi tiếp khi Director chưa nạp gì. Cả hai hướng đều sai vì cùng một lý do: quyết định dựa vào kết quả lệnh, không dựa vào trạng thái.

#!/bin/bash
set -uo pipefail

JOB=backup-web01

# Lệnh reload có thể quá giờ hoặc báo lỗi mà Director vẫn đã nạp xong.
# Vì vậy không quyết định theo kết quả lệnh, mà theo trạng thái đọc lại.
for lan in 1 2 3; do
  echo "reload" | timeout 60 bconsole > /dev/null 2>&1 || true
  if echo "show job=${JOB}" | timeout 60 bconsole | grep -q 'Enabled[[:space:]]*=[[:space:]]*No'; then
    echo "Director đã nạp cấu hình mới (lần thử ${lan})"
    exit 0
  fi
  sleep $(( lan * 30 ))
done
echo "Sau 3 lần vẫn chưa thấy cấu hình mới, cần người kiểm tra" >&2
exit 1

Vòng lặp này thử lại được vì reload thừa một lần thường vô hại. Điều kiện dừng là trạng thái đọc lại từ Director, không phải output của lệnh reload. Quy ước đơn giản: không chắc thì coi như chưa xong, và thử lại khi rảnh.

Khi có nhiều script cùng muốn reload, nên gom về một chỗ duy nhất. Một hàng đợi nhỏ nhận yêu cầu, chờ tới lúc không có job chạy, rồi reload một lần cho tất cả. Cách này tránh được việc hai script reload chồng nhau, hoặc một script reload khi script khác đang ghi file dở.

Bẫy 5: chạy lại thao tác không idempotent sau một lần lỗi

Một thao tác bị báo lỗi giữa chừng không có nghĩa là nó chưa chạy gì. Có thể nó đã chạy một phần, hoặc chạy hết rồi mới lỗi ở bước sau. Chạy lại một thao tác kiểu “nối thêm” lúc đó sẽ tạo ra bản trùng:

# SAI: nối thêm dòng; chạy lại sau một lần lỗi sẽ thành hai dòng
sed -i '/Name = backup-web01/a\  Enabled = No' /etc/bareos/bareos-dir.d/job/backup-web01.conf

Lần đầu, lệnh sed chạy xong nhưng bước sau báo lỗi. Người vận hành chạy lại, và file giờ có hai dòng Enabled = No. Với một khối cấu hình lớn hơn, bạn có thể có hai bản cùng một resource trong một file. Tuỳ trường hợp, Bareos báo lỗi cú pháp ở lần reload sau, hoặc lặng lẽ chấp nhận, và cả hai đều khó truy ra nguyên nhân.

Có hai cách phòng. Cách thứ nhất là chỉ dùng thao tác idempotent: ghi đè nguyên khối, hoặc xoá rồi chèn như đoạn awk ở bẫy 2. Cách thứ hai là đọc trạng thái trước khi thử lại:

#!/bin/bash
set -euo pipefail

FILE=/etc/bareos/bareos-dir.d/job/backup-web01.conf

# Trước khi thử lại: đọc trạng thái hiện tại, đừng đoán
so_dong=$(grep -c '^[[:space:]]*Enabled[[:space:]]*=' "$FILE" || true)
echo "Số dòng Enabled hiện có: ${so_dong}"
sha256sum "$FILE"

case "$so_dong" in
  0) echo "Chưa có, an toàn để thêm" ;;
  1) echo "Đã có một dòng, không làm gì thêm"; exit 0 ;;
  *) echo "Có ${so_dong} dòng trùng, dừng lại để người kiểm tra" >&2; exit 1 ;;
esac

Ghi lại kích thước file trước và sau mỗi lần sửa là thói quen rẻ. File bị nhân đôi thường chỉ lộ ra nhờ kích thước tăng bất thường.

Bẫy 6: quên yes, script tưởng job đã chạy

Lệnh run trong bconsole hỏi xác nhận trước khi đưa job vào hàng đợi. Chạy qua pipe mà thiếu từ khoá yes, bconsole gặp hết stdin trước khi nhận được câu trả lời:

# SAI: thiếu "yes", bconsole chờ xác nhận rồi gặp hết stdin
echo "run job=backup-web01 level=Full" | bconsole
echo "Đã chạy backup Full"

Script in “Đã chạy backup Full”, nhưng thường không có job nào được tạo. Cách đúng là thêm yes, rồi lấy JobId từ output làm bằng chứng:

#!/bin/bash
set -uo pipefail

out=$(echo "run job=backup-web01 level=Full yes" | timeout 60 bconsole)
jobid=$(grep -oE 'Job queued\. JobId=[0-9]+' <<< "$out" | grep -oE '[0-9]+$' || true)

if [ -z "$jobid" ]; then
  echo "Job không được đưa vào hàng đợi:" >&2
  tail -n 5 <<< "$out" >&2
  exit 1
fi
echo "Đã xếp hàng JobId=${jobid}"

Có JobId, các bước sau theo dõi được đúng job đó bằng wait jobid=.

Bảng tổng hợp

BẫyTriệu chứngCách đúng
Tin mã thoát bconsoleBáo OK khi Director đã chếtTìm dòng 1000 OK, bọc timeout
Tìm chữ “Director”Câu lỗi cũng khớpTìm dấu hiệu thành công, không tìm từ khoá chung
disable bằng lệnhJob tự bật lại sau reloadGhi Enabled = No vào file, kiểm cú pháp, reload, đọc lại
Cắt Running Jobs theo dòng ====Không thấy job đầu tiênCắt tới Terminated Jobs:, tìm No Jobs running.
Tin kết quả lệnh reloadDừng sai hoặc đi tiếp saiĐọc lại trạng thái, không chắc thì thử lại
Chạy lại lệnh “nối thêm”Dòng hoặc khối cấu hình bị nhân đôiThao tác idempotent, đọc trạng thái trước khi thử lại
Thiếu yes khi runKhông có job nào được tạoThêm yes, lấy JobId làm bằng chứng

Vài thói quen chung khi viết script quanh bconsole

  • Mỗi thao tác ghi đi kèm một bước đọc lại. Tắt job thì show job=, chạy job thì lấy JobId, reload thì kiểm cấu hình đã có hiệu lực.
  • Luôn có timeout. Script chạy bằng cron mà treo thì không ai thấy, và lần chạy sau có thể chồng lên.
  • Kiểm cú pháp bằng bareos-dir -t trước mọi lần reload. Giữ bản sao file cũ để trả lại ngay khi kiểm không qua.
  • Dùng giá trị mặc định an toàn. Không chắc có job chạy hay không thì coi như có. Không chắc đã reload chưa thì coi như chưa.
  • Đo trên hệ thống thật trước khi tin một mẫu chuỗi. Chạy lệnh ở cả trạng thái bình thường lẫn lỗi, chép output thật ra, rồi mới viết grep.

Tóm tắt

  • bconsole thường thoát mã 0 kể cả khi không nối được Director; hãy tìm dòng 1000 OK.
  • disable bằng lệnh mất sau reload; muốn bền phải ghi Enabled = No vào file.
  • Output viết cho người đọc dễ đánh lừa script; cắt theo khối và tìm câu khẳng định rõ ràng.
  • Lệnh báo lỗi vẫn có thể đã chạy; quyết định theo trạng thái đọc lại, không theo kết quả lệnh.
  • Viết thao tác idempotent, và đọc trạng thái thật trước khi thử lại sau một lần lỗi.

Bài sau: Khi WebUI không đủ: thiết kế portal sinh cấu hình Bareos — gom các nguyên tắc trên thành một công cụ quản trị: cấu hình sinh từ cơ sở dữ liệu, kiểm trước khi nạp, và lan can để không đụng vào cấu hình viết tay.

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

Diễn tập khôi phục: backup chưa restore thử là chưa có backup

Diễn tập khôi phục: backup chưa restore thử là chưa có backup

Job backup báo OK chưa chứng minh dữ liệu dùng được. Hướng dẫn diễn tập restore file, thư mục, cơ sở dữ liệu với Bareos, đo thời gian và tách quyền.

Catalog: trái tim của Bareos

Catalog: trái tim của Bareos

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.

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.