CaiWuXiTong/deploy/activate-incremental-release.sh
WuYongJie 374df29d2b 修复生产报表负载根因 (#257)
消除报表缓存穿透与后台千万级全表计数,降低同步频率并调整共享数据库资源。
2026-09-23 20:15:59 +08:00

237 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
set -eu
REGISTRY_IMAGE="${1:?REGISTRY_IMAGE is required}"
IMAGE_TAG="${2:?IMAGE_TAG is required}"
REMOTE_DIR="${3:-/opt/caiwuxitong}"
DEPLOY_SHA="${4:?DEPLOY_SHA is required}"
RUN_MIGRATIONS="${5:-0}"
KEEP_IMAGES="${6:-2}"
IMAGE_NAME="caiwu_akaunting"
LOCK_FILE="$REMOTE_DIR/.deploy.lock"
REGISTRY_API="http://127.0.0.1:5000/v2/caiwuxitong/app"
TAG_HISTORY="$REMOTE_DIR/.registry-tags"
if [ "$(id -u)" -ne 0 ]; then
echo "activate-incremental-release.sh must run as root" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/.env" ]; then
echo "Missing $REMOTE_DIR/runtime/.env; run bootstrap-server.sh first" >&2
exit 1
fi
clickhouse_env="$REMOTE_DIR/runtime/clickhouse.env"
if [ ! -f "$clickhouse_env" ]; then
clickhouse_password="$(openssl rand -hex 24)"
umask 077
{
printf '%s\n' \
'CLICKHOUSE_DB=caiwu_business_analytics' \
'CLICKHOUSE_USER=caiwu_app' \
"CLICKHOUSE_PASSWORD=$clickhouse_password" \
'CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1' \
'BUSINESS_CLICKHOUSE_ENABLED=true' \
'BUSINESS_CLICKHOUSE_URL=http://clickhouse:8123' \
'BUSINESS_CLICKHOUSE_DATABASE=caiwu_business_analytics' \
'BUSINESS_CLICKHOUSE_USERNAME=caiwu_app' \
"BUSINESS_CLICKHOUSE_PASSWORD=$clickhouse_password"
} > "$clickhouse_env"
chmod 600 "$clickhouse_env"
fi
if ! grep -q '^CLICKHOUSE_PASSWORD=.' "$clickhouse_env" \
|| ! grep -q '^BUSINESS_CLICKHOUSE_PASSWORD=.' "$clickhouse_env"; then
echo "Incomplete $clickhouse_env; refusing to overwrite private ClickHouse credentials" >&2
exit 1
fi
if [ ! -f "$REMOTE_DIR/runtime/config-db.env" ]; then
echo "Missing $REMOTE_DIR/runtime/config-db.env; run bootstrap-server.sh first" >&2
exit 1
fi
if ! grep -q '^BUSINESS_STORAGE_DRIVER=qiniu$' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_ACCESS_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null \
|| ! grep -q '^QINIU_SECRET_KEY=.' "$REMOTE_DIR/runtime/qiniu-storage.env" 2>/dev/null; then
echo "Private business storage runtime is incomplete" >&2
exit 1
fi
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "Another CaiWuXiTong deployment is already running" >&2
exit 1
fi
PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
PREVIOUS_SHA="$(cat "$REMOTE_DIR/.deployed-sha" 2>/dev/null || true)"
restore_previous_image() {
case "$PREVIOUS_IMAGE" in
"$IMAGE_NAME":*)
IMAGE_TAG="${PREVIOUS_IMAGE#*:}"
RUN_MIGRATIONS=0
export IMAGE_TAG RUN_MIGRATIONS
docker compose -f compose.jiangling.yml up -d app || true
;;
esac
}
docker pull "$REGISTRY_IMAGE"
docker tag "$REGISTRY_IMAGE" "$IMAGE_NAME:$IMAGE_TAG"
LOADED_SHA="$(docker image inspect "$IMAGE_NAME:$IMAGE_TAG" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')"
if [ "$LOADED_SHA" != "$DEPLOY_SHA" ]; then
echo "Image revision mismatch: expected $DEPLOY_SHA, got $LOADED_SHA" >&2
exit 1
fi
cd "$REMOTE_DIR"
export IMAGE_TAG RUN_MIGRATIONS
# Pull the independent columnar runtime before migrations or app switching.
# A registry/network failure therefore leaves both the database and current app untouched.
docker compose -f compose.jiangling.yml pull clickhouse
if [ "$RUN_MIGRATIONS" = "1" ]; then
# 先创建独立业务镜像库并授权应用账号,再启动会执行迁移的新应用。
# 该脚本幂等;已有数据库只会补齐授权,不会覆盖业务数据。
docker compose -f compose.jiangling.yml up -d config_db
config_db_attempt=1
while [ "$config_db_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
break
fi
sleep 5
config_db_attempt=$((config_db_attempt + 1))
done
if [ "$(docker inspect caiwu_config_db --format '{{.State.Health.Status}}' 2>/dev/null || true)" != "healthy" ]; then
docker compose -f compose.jiangling.yml logs --tail 80 config_db >&2 || true
echo "Shared MariaDB did not become healthy before mirror database initialization" >&2
exit 1
fi
docker exec caiwu_config_db sh /docker-entrypoint-initdb.d/10-config-db-security.sh
fi
docker compose -f compose.jiangling.yml up -d --remove-orphans
clickhouse_healthy=0
clickhouse_attempt=1
while [ "$clickhouse_attempt" -le 24 ]; do
if [ "$(docker inspect caiwu_clickhouse --format '{{.State.Health.Status}}' 2>/dev/null || true)" = "healthy" ]; then
clickhouse_healthy=1
break
fi
sleep 5
clickhouse_attempt=$((clickhouse_attempt + 1))
done
if [ "$clickhouse_healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 clickhouse >&2 || true
restore_previous_image
echo "ClickHouse did not become healthy; previous application image was restored when available" >&2
exit 1
fi
healthy=0
attempt=1
while [ "$attempt" -le 30 ]; do
if curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
healthy=1
break
fi
sleep 5
attempt=$((attempt + 1))
done
if [ "$healthy" -ne 1 ]; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed health check; previous image was restored when available" >&2
exit 1
fi
if ! docker exec -u www-data caiwu_app php artisan storage:business-files:sync --require-qiniu; then
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
restore_previous_image
echo "Release failed private business storage verification; previous image was restored when available" >&2
exit 1
fi
if ! curl -fsS -o /dev/null http://127.0.0.1:18088/serviceworker.js; then
restore_previous_image
echo "Release failed after business storage activation; previous image was restored when available" >&2
exit 1
fi
printf '%s\n' "$DEPLOY_SHA" > "$REMOTE_DIR/.deployed-sha.tmp"
mv "$REMOTE_DIR/.deployed-sha.tmp" "$REMOTE_DIR/.deployed-sha"
# 只运行已登记的上游后台读取和 E 签宝状态轮询,不启用整个 Laravel
# 调度器,避免意外启动账单提醒等既有任务。上游读取共用一把锁,E 签宝单独加锁。
# 普通商品优先高频增量,并在配置的每周低峰窗口自动继续全量校准。
cat > /etc/cron.d/caiwu-jiansun-snapshots <<'EOF'
CRON_TZ=Asia/Shanghai
* * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-catalog --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan upstream:snapshots:refresh-all --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables --quiet' >/dev/null 2>&1
# 店铺经营报表按 15 分钟刷新;单轮覆盖 50000 行回看窗口,避免空闲时持续重读千万级账单。
7,22,37,52 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-store-report --max-batches=12 --quiet >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-third-party-warehouse-storage --max-batches=0 --quiet && /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:warm-report-caches --quiet' >/dev/null 2>&1
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --quiet >/dev/null 2>&1
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
*/2 * * * * root /usr/bin/flock -n /run/caiwu-esign-polling.lock /usr/bin/docker exec -u www-data caiwu_app php artisan esign:poll-signatures --quiet >/dev/null 2>&1
28 1 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan jushuitan:sync-shipping --days=3 >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/caiwu-jiansun-snapshots
docker image ls "$IMAGE_NAME" --format '{{.Repository}}:{{.Tag}}' \
| awk '!seen[$0]++' \
| tail -n "+$((KEEP_IMAGES + 1))" \
| while IFS= read -r old_image; do
[ -n "$old_image" ] && docker image rm "$old_image" >/dev/null 2>&1 || true
done
docker image prune -f --filter 'label=com.caiwu.project=CaiWuXiTong' >/dev/null || true
# Track tags by successful deployment order, retain only current + previous,
# delete older manifests, and reclaim their unreferenced blobs.
touch "$TAG_HISTORY"
awk -v tag="$IMAGE_TAG" '$0 != tag && NF { print }' "$TAG_HISTORY" > "$TAG_HISTORY.tmp"
printf '%s\n' "$IMAGE_TAG" >> "$TAG_HISTORY.tmp"
mv "$TAG_HISTORY.tmp" "$TAG_HISTORY"
deleted_registry_tag=0
while [ "$(wc -l < "$TAG_HISTORY")" -gt "$KEEP_IMAGES" ]; do
OLD_TAG="$(sed -n '1p' "$TAG_HISTORY")"
DIGEST="$(curl -fsSI \
-H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"$REGISTRY_API/manifests/$OLD_TAG" \
| awk -F ': ' 'tolower($1) == "docker-content-digest" { gsub("\r", "", $2); print $2 }')"
if [ -n "$DIGEST" ]; then
curl -fsS -X DELETE "$REGISTRY_API/manifests/$DIGEST" >/dev/null
deleted_registry_tag=1
fi
docker image rm "127.0.0.1:5000/caiwuxitong/app:$OLD_TAG" >/dev/null 2>&1 || true
sed '1d' "$TAG_HISTORY" > "$TAG_HISTORY.tmp"
mv "$TAG_HISTORY.tmp" "$TAG_HISTORY"
done
if [ "$deleted_registry_tag" -eq 1 ]; then
systemctl stop docker-registry
if ! docker-registry garbage-collect --delete-untagged /etc/docker/registry/config.yml; then
echo "Registry garbage collection failed; the service will still be restarted" >&2
fi
systemctl start docker-registry
fi
docker compose -f compose.jiangling.yml ps
echo "DEPLOYED_SHA=$DEPLOY_SHA"
: "$PREVIOUS_SHA"