完善SpacetimeDB逐文件增量备份

新增data-dir逐文件CAS基线、history归档与安全清理
发布并校验OSS latest pointer,支持异机自动恢复
接入dev和release可选的files-history定时备份profile
补齐备份测试、生产门禁、环境示例与运维文档
This commit is contained in:
2026-07-16 18:00:06 +08:00
parent b303605bf2
commit 9ae3f53989
10 changed files with 3052 additions and 73 deletions
File diff suppressed because it is too large Load Diff
@@ -514,6 +514,76 @@ const checks = [
reason:
'生产冷备份 service 必须用 node -- 分隔脚本参数,避免 Node 22 抢占业务 --env-file。',
},
{
file: 'deploy/systemd/genarrative-database-backup.service',
excludes: '--storage-format files',
reason: '生产数据库备份主 service 必须继续保持 archive full 默认行为。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
includes: 'ExecStart=\nExecStart=/usr/bin/node -- /opt/genarrative/current/scripts/database-backup-to-oss.mjs --env-file',
reason: 'files-history drop-in 必须先清空主 service 的 ExecStart,并使用 current release 脚本。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
includes: '--storage-format files --mode history --work-dir /var/lib/genarrative/database-backups/files-history',
reason: 'files-history drop-in 必须只执行逐文件历史归档,并复用已建立基线的独立 work-dir。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
excludes: '--stop-service',
reason: 'files-history 在线归档不可停止 SpacetimeDB。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
excludes: '--database',
reason: 'files-history drop-in 不得写死数据库名,必须从环境文件读取。',
},
{
file: 'deploy/systemd/genarrative-database-backup-files-history.conf',
excludes: '--bucket',
reason: 'files-history drop-in 不得写死 OSS bucket,必须从环境文件读取。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'DATABASE_BACKUP_PROFILE="${DATABASE_BACKUP_PROFILE:-archive-full}"',
reason: 'Server-Provision 的数据库备份 profile 必须默认保持 archive-full。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'archive-full|files-history)',
reason: 'Server-Provision 必须拒绝未知数据库备份 profile。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: '--storage-format files --mode history --work-dir',
reason: 'Server-Provision 启用 files-history 前必须用真实 current release 脚本执行只读 baseline 预检。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'run_cmd rm -f "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN}" "${DATABASE_BACKUP_LEGACY_DEV_DROP_IN}"',
reason: 'Server-Provision 切回 archive-full 时必须移除托管及现场遗留的 history drop-in。',
},
{
file: 'scripts/jenkins-server-provision.sh',
includes: 'install_file "${rendered_drop_in}" "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN}" 0644',
reason: 'Server-Provision 必须从仓库模板安装托管的 files-history drop-in。',
},
{
file: 'jenkins/Jenkinsfile.production-server-provision',
includes: "choice(name: 'DATABASE_BACKUP_PROFILE', choices: ['archive-full', 'files-history']",
reason: 'Server-Provision Job 必须显式暴露 archive-first 的数据库备份 profile。',
},
{
file: 'jenkins/Jenkinsfile.production-server-provision',
includes: "string(name: 'DATABASE_BACKUP_FILES_HISTORY_WORK_DIR', defaultValue: '/var/lib/genarrative/database-backups/files-history'",
reason: 'Server-Provision Job 必须允许 dev/release 为 files-history 选择各自的 baseline state 目录。',
},
{
file: 'jenkins/Jenkinsfile.production-server-provision',
excludes: "params.DEPLOY_TARGET == 'release' && databaseBackupProfile == 'files-history'",
reason: 'release 必须能在显式选择 profile 且 baseline 预检通过后启用 files-history。',
},
{
file: 'scripts/database-backup-to-oss.mjs',
includes: 'assertSufficientWorkDirSpace({dataDir, workDir, args, env})',
@@ -6837,6 +6907,7 @@ const checks = [
const nodeEnvFileCommandFiles = [
'package.json',
'deploy/systemd/genarrative-database-backup.service',
'deploy/systemd/genarrative-database-backup-files-history.conf',
'scripts/deploy/production-stdb-publish.sh',
'scripts/deploy/pingora-direct-enable.sh',
'scripts/deploy/pingora-direct-rollback.sh',
File diff suppressed because it is too large Load Diff
+70 -1
View File
@@ -10,6 +10,11 @@ GENARRATIVE_OPENSSL_VERSION="${GENARRATIVE_OPENSSL_VERSION:-3.2.0}"
GENARRATIVE_OPENSSL_PREFIX="${GENARRATIVE_OPENSSL_PREFIX:-/opt/genarrative/openssl-3.2.0}"
GENARRATIVE_OPENSSL_SOURCE_URL="${GENARRATIVE_OPENSSL_SOURCE_URL:-https://github.com/openssl/openssl/releases/download/openssl-${GENARRATIVE_OPENSSL_VERSION}/openssl-${GENARRATIVE_OPENSSL_VERSION}.tar.gz}"
GENARRATIVE_OPENSSL_SOURCE_SHA256="${GENARRATIVE_OPENSSL_SOURCE_SHA256:-14c826f07c7e433706fb5c69fa9e25dab95684844b4c962a2cf1bf183eb4690e}"
DATABASE_BACKUP_PROFILE="${DATABASE_BACKUP_PROFILE:-archive-full}"
DATABASE_BACKUP_FILES_HISTORY_WORK_DIR="${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR:-/var/lib/genarrative/database-backups/files-history}"
DATABASE_BACKUP_FILES_HISTORY_DROP_IN_DIR="/etc/systemd/system/genarrative-database-backup.service.d"
DATABASE_BACKUP_FILES_HISTORY_DROP_IN="${DATABASE_BACKUP_FILES_HISTORY_DROP_IN_DIR}/10-files-history.conf"
DATABASE_BACKUP_LEGACY_DEV_DROP_IN="${DATABASE_BACKUP_FILES_HISTORY_DROP_IN_DIR}/10-dev-files.conf"
require_non_root_relative_path() {
local label="$1"
@@ -63,6 +68,21 @@ validate_server_names() {
done
}
validate_database_backup_profile() {
case "${DATABASE_BACKUP_PROFILE}" in
archive-full|files-history)
;;
*)
echo "[server-provision] DATABASE_BACKUP_PROFILE 只能是 archive-full 或 files-history,当前值: ${DATABASE_BACKUP_PROFILE}" >&2
exit 1
;;
esac
if [[ ! "${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR}" =~ ^/var/lib/genarrative/database-backups/[A-Za-z0-9._/-]+$ || "${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR}" == *..* ]]; then
echo "[server-provision] DATABASE_BACKUP_FILES_HISTORY_WORK_DIR 必须是 /var/lib/genarrative/database-backups/ 下不含连续点号的绝对路径,当前值: ${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR}" >&2
exit 1
fi
}
run_cmd() {
echo "+ $*"
if [[ "${DRY_RUN}" != "true" ]]; then
@@ -917,6 +937,52 @@ render_database_backup_service() {
deploy/systemd/genarrative-database-backup.service
}
render_database_backup_files_history_drop_in() {
local current_escaped env_escaped work_dir_escaped
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
work_dir_escaped="$(escape_sed_replacement "${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR}")"
sed \
-e "s|/opt/genarrative/current|${current_escaped}|g" \
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
-e "s|/var/lib/genarrative/database-backups/files-history|${work_dir_escaped}|g" \
deploy/systemd/genarrative-database-backup-files-history.conf
}
configure_database_backup_profile() {
local rendered_drop_in
if [[ "${DATABASE_BACKUP_PROFILE}" == "archive-full" ]]; then
echo "[server-provision] 数据库备份 profile=archive-full,保留主 service 的全量冷备行为。"
run_cmd rm -f "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN}" "${DATABASE_BACKUP_LEGACY_DEV_DROP_IN}"
return
fi
echo "[server-provision] 数据库备份 profile=files-history,先只读验证 full baseline state。"
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ /usr/bin/node -- ${CURRENT_LINK}/scripts/database-backup-to-oss.mjs --env-file ${API_ENV_FILE} --storage-format files --mode history --work-dir ${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR} --dry-run"
else
if [[ ! -f "${CURRENT_LINK}/scripts/database-backup-to-oss.mjs" ]]; then
echo "[server-provision] current release 缺少数据库备份脚本: ${CURRENT_LINK}/scripts/database-backup-to-oss.mjs" >&2
exit 1
fi
/usr/bin/node -- "${CURRENT_LINK}/scripts/database-backup-to-oss.mjs" \
--env-file "${API_ENV_FILE}" \
--storage-format files \
--mode history \
--work-dir "${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR}" \
--dry-run
fi
run_cmd install -d -o genarrative -g genarrative -m 0750 "${DATABASE_BACKUP_FILES_HISTORY_WORK_DIR}"
run_cmd install -d -o root -g root -m 0755 "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN_DIR}"
run_cmd rm -f "${DATABASE_BACKUP_LEGACY_DEV_DROP_IN}"
rendered_drop_in="$(mktemp)"
render_database_backup_files_history_drop_in >"${rendered_drop_in}"
install_file "${rendered_drop_in}" "${DATABASE_BACKUP_FILES_HISTORY_DROP_IN}" 0644
rm -f "${rendered_drop_in}"
}
render_health_patrol_service() {
local current_escaped
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
@@ -930,6 +996,7 @@ require_path deploy/systemd/genarrative-api.service
require_path deploy/systemd/genarrative-external-generation-worker@.service
require_path deploy/systemd/genarrative-external-generation-controller.service
require_path deploy/systemd/genarrative-database-backup.service
require_path deploy/systemd/genarrative-database-backup-files-history.conf
require_path deploy/systemd/genarrative-database-backup.timer
require_path deploy/systemd/genarrative-health-patrol.service
require_path deploy/systemd/genarrative-health-patrol.timer
@@ -951,9 +1018,10 @@ require_path scripts/deploy/maintenance-off.sh
require_path scripts/deploy/maintenance-status.sh
validate_server_names
validate_database_backup_profile
require_non_root_relative_path "PROVISION_TOOLS_DIR" "${PROVISION_TOOLS_DIR}"
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, source_commit=$(cat .jenkins-source-commit)"
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, database_backup_profile=${DATABASE_BACKUP_PROFILE}, source_commit=$(cat .jenkins-source-commit)"
run_cmd id
require_root_for_real_provision
@@ -1033,6 +1101,7 @@ else
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
fi
ensure_api_runtime_env_defaults
configure_database_backup_profile
if [[ ! -f "${WORKER_ENV_FILE}" ]]; then
echo "+ create ${WORKER_ENV_FILE} from example"