Files
LokiStack/scripts/release.sh
T
opsx 4d582e62eb
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
fix(release): digest 校验改为轮询,避免 Docker Hub 传播延迟导致误判
现象:刚 push 完立刻查 tags API,latest 已是新 digest 而 v0.1 还是旧值,
脚本按"三个 tag 不一致 = 并发覆盖"直接 exit 1,把正常发布拦下来了。

根因:Docker Hub 的 /v2/repositories/<repo>/tags/<tag> 有秒级~十几秒的
传播延迟,单次查询不能作为判据。

修复:轮询最多 12 次、每次间隔 5 秒(共 60 秒),三者一致才继续;
超时仍不一致则 exit 1,并在报错里区分两种可能:
  - latest 与 v0.1 不同 → 疑似并发构建覆盖(提示检查 flock 是否被绕过)
  - 仍为 "?"            → 推送根本没成功或仓库不可达
本次实测:第 1 次轮询发现未同步,等待 5 秒后三者均为 1ce3d4166506 ✅

顺带:不可变 tag 之前出现 -dirty 后缀,是因为改完脚本没提交就发版;
本次先提交再发版,tag 恢复干净(2.0.0-20261031-<sha>)。
2026-10-31 18:12:46 +00:00

225 lines
10 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# ============================================================
# release.sh — 构建 + 推送(latest / v0.1 / 不可变 tag 三合一)
# ============================================================
# 本项目的发布策略:**清单与容器启动统一使用 latest**。
# kubernetes/deployment.yaml → lghost/lokistack:latest + imagePullPolicy: Always
# docker-compose.yml → lghost/lokistack:latest
# docker-compose.prod.yml → lghost/lokistack:${IMAGE_TAG:-latest}
#
# 所以每次发版必须让 latest 与 v0.1 **同时**指向新镜像,否则会出现
# "k8s 拉到新版、docker 起还是旧版"这种版本漂移。
#
# 本脚本另外额外打一个不可变 tag(<版本>-<日期>-<短commit>):
# - 它不写进任何清单,只用于事后追溯/回滚(仓库里能查到那一刻的确切镜像)
# - 需要"固定版本部署"的生产环境可以手动改成这个 tag
#
# 为什么不在脚本里改 yml 的 tag:
# 上一版脚本会把 deployment.yaml 的 image 改成不可变 tag 并把 pullPolicy
# 改成 IfNotPresent,结果与"统一用 latest"的约定相反,还容易忘记提交
# 导致下次 apply 退回旧 tag。改成默认不动清单。
#
# 用法:
# bash scripts/release.sh # 构建 + 推三个 tag + 滚动重启(拉 latest)
# bash scripts/release.sh --no-deploy # 只构建 + 推送,不碰集群
# bash scripts/release.sh --pin # 额外把 deployment.yaml 钉到不可变 tag(可选)
# bash scripts/release.sh --dry-run # 只检查锁与前置条件,不构建不推送
#
# 部署后自愈与版本戳:
# 滚动完成后核对 Pod digest 与远端 latest;不一致时自动删 Pod 强制重拉
# (节点镜像缓存/拉取时机问题),仍不一致才 exit 1。
# 核对通过后给 Deployment 打三个 annotation,直接看集群就知道线上跑哪版:
# lokistack.io/release-image / release-tag / release-time
#
# 并发保护(重要):
# 本项目所有环境都拉 latest,两个构建同时跑会互相覆盖 latest:
# 后完成的那次生效,先完成的那次白推。曾出现"中断的构建后来完成,
# 把 latest 指回旧代码",表现为"改了但没生效",极难排查。
# 因此脚本用 flock 加独占锁,同一时刻只允许一个发布任务。
# flock 在进程退出时自动释放,不会留下需要手工清理的僵尸锁。
# ============================================================
set -euo pipefail
cd "$(dirname "$0")/.."
IMAGE_REPO="${IMAGE_REPO:-lghost/lokistack}"
VERSION="${VERSION:-2.0.0}"
MODE="${1:-}"
# ── 独占锁:同一时刻只允许一个发布任务 ────────────────────────────────
# 为什么必须加:所有清单都指向 latest,两个构建并发时谁后完成谁生效,
# 另一个的推送等于白做;更糟的是完成顺序不可控,可能把 latest 指向更旧的
# 代码。这是"改了但没生效"类问题的根源之一。
# flock 跟随文件描述符,进程退出(含 Ctrl-C / set -e 中断)自动释放。
LOCK_FILE="${RELEASE_LOCK_FILE:-/var/lock/lokistack-release.lock}"
mkdir -p "$(dirname "${LOCK_FILE}")" 2>/dev/null || true
if ! exec 9>"${LOCK_FILE}"; then
echo "❌ 无法创建锁文件 ${LOCK_FILE}(权限不足?)"
exit 1
fi
if ! flock -n 9; then
echo "❌ 已有发布任务正在进行,本次不启动。"
echo " 锁文件 : ${LOCK_FILE}"
echo " 查看占用 : fuser -v ${LOCK_FILE} 2>/dev/null || lsof ${LOCK_FILE}"
echo " 无需手工删锁文件:持有进程退出时 flock 自动释放。"
exit 1
fi
echo "🔒 已获取发布锁:${LOCK_FILE}"
echo "=========================================="
echo " 前置检查"
echo "=========================================="
echo " 仓库 : ${IMAGE_REPO}"
if ! command -v docker >/dev/null 2>&1; then
echo "❌ 未找到 docker 命令"
exit 1
fi
echo " docker : $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo '未知')"
if [ "${MODE}" = "--dry-run" ]; then
echo ""
echo "(--dry-run) 只做前置检查:不构建、不推送、不动集群。"
echo " → 锁获取正常。"
exit 0
fi
SHORT_SHA=$(git rev-parse --short=8 HEAD 2>/dev/null || echo "nogit")
DIRTY=""
if ! git diff --quiet 2>/dev/null; then
DIRTY="-dirty"
echo "⚠️ 工作区有未提交改动,不可变 tag 会带 -dirty(发布前建议先提交)"
fi
IMMUTABLE_TAG="${VERSION}-$(date +%Y%m%d)-${SHORT_SHA}${DIRTY}"
echo "=========================================="
echo " 构建: ${IMAGE_REPO} → latest / v0.1 / ${IMMUTABLE_TAG}"
echo "=========================================="
docker build \
-t "${IMAGE_REPO}:latest" \
-t "${IMAGE_REPO}:v0.1" \
-t "${IMAGE_REPO}:${IMMUTABLE_TAG}" .
IMAGE_ID=$(docker images --format '{{.ID}}' "${IMAGE_REPO}:latest" | head -1)
echo "✅ 构建完成 ${IMAGE_ID}"
echo ""
echo "推送三个 tag(latest 与 v0.1 必须同 digest,否则环境间会版本漂移)..."
# Docker Hub 偶发 502(实测踩过:v0.1 推成功、latest 失败 →
# latest 指着旧镜像,Pod 重启后跑的是旧代码)。所以每次 push 都重试。
push_with_retry() {
local tag="$1" attempt
for attempt in 1 2 3; do
if docker push "${IMAGE_REPO}:${tag}" 2>&1 | tail -1; then
return 0
fi
echo " ⚠️ push ${tag} 第 ${attempt} 次失败,10 秒后重试..."
sleep 10
done
echo "❌ push ${tag} 连续 3 次失败(镜像仓库异常?)"
return 1
}
push_with_retry latest
push_with_retry v0.1
push_with_retry "${IMMUTABLE_TAG}"
# ── 校验三个 tag 指向同一个镜像 ──
echo ""
echo "校验远端 digest 一致性..."
digest_of() {
curl -s "https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/$1" 2>/dev/null \
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("digest","?"))' 2>/dev/null \
|| echo "?"
}
# Docker Hub 的 tags API 有传播延迟:刚 push 完立刻查,常常还是旧 digest。
# 实测遇到过 latest 已是新值、v0.1 还停在旧值,如果只查一次就会误判成
# "并发覆盖"并拒绝部署。所以轮询到三者一致为止(最多 60 秒)。
DIGEST_ATTEMPTS=12
DIGEST_SLEEP=5
for attempt in $(seq 1 ${DIGEST_ATTEMPTS}); do
D_LATEST=$(digest_of latest)
D_V01=$(digest_of v0.1)
D_IMM=$(digest_of "${IMMUTABLE_TAG}")
if [ "${D_LATEST:0:19}" = "${D_V01:0:19}" ] && [ "${D_LATEST:0:19}" = "${D_IMM:0:19}" ] \
&& [ "${D_LATEST:0:19}" != "?" ]; then
break
fi
if [ "${attempt}" -lt "${DIGEST_ATTEMPTS}" ]; then
echo " ⏳ 远端 digest 尚未同步(第 ${attempt}/${DIGEST_ATTEMPTS} 次),${DIGEST_SLEEP}s 后重试…"
sleep ${DIGEST_SLEEP}
fi
done
echo " latest : ${D_LATEST:0:19}"
echo " v0.1 : ${D_V01:0:19}"
echo " ${IMMUTABLE_TAG}: ${D_IMM:0:19}"
if [ "${D_LATEST:0:19}" != "${D_V01:0:19}" ] || [ "${D_LATEST:0:19}" != "${D_IMM:0:19}" ]; then
echo "❌ 三个 tag digest 不一致(已等待 $((DIGEST_ATTEMPTS * DIGEST_SLEEP)) 秒)"
echo " 若 latest 与 v0.1 不同:可能是并发构建覆盖了 latest(检查 flock 锁是否被绕过)"
exit 1
fi
echo " → 一致 ✅"
if [ "${MODE}" = "--pin" ]; then
echo ""
echo "(--pin)把 kubernetes/deployment.yaml 钉到 ${IMMUTABLE_TAG}..."
sed -i "s|image: ${IMAGE_REPO}:.*|image: ${IMAGE_REPO}:${IMMUTABLE_TAG}|" kubernetes/deployment.yaml
sed -i "s|imagePullPolicy: Always|imagePullPolicy: IfNotPresent|" kubernetes/deployment.yaml
echo "⚠️ 记得提交 deployment.yaml,否则下次 apply 会退回 latest"
fi
if [ "${MODE}" != "--no-deploy" ]; then
export KUBECONFIG="${KUBECONFIG:-/root/.kube/config}"
echo ""
echo "应用清单(image: latest + Always → 强制重拉)..."
kubectl apply -f kubernetes/deployment.yaml
# 清单内容没变时 apply 不会触发滚动,这里显式重启一次
kubectl -n log rollout restart deploy/lokistack
kubectl -n log rollout status deploy/lokistack --timeout=300s
pod_digest() {
kubectl -n log get po -l app=lokistack \
-o jsonpath='{.items[0].status.containerStatuses[0].imageID}' 2>/dev/null | sed 's/.*@//'
}
POD_DIGEST=$(pod_digest)
echo ""
echo "=========================================="
echo " 部署完成,核对 Pod 与远端 latest:"
echo " 远端 latest : ${D_LATEST:0:19}"
echo " Pod 运行中 : ${POD_DIGEST:0:19}"
# ── 自愈:digest 不一致时强制重拉一次 ──
# 常见原因:节点缓存了旧的 latest 映射、或 rollout 与 push 完成时间太接近。
# 直接删 Pod 让 kubelet 重新解析 latest 通常就恢复了,不需要人工介入。
if [ "${POD_DIGEST:0:19}" != "${D_LATEST:0:19}" ]; then
echo " ⚠️ 不一致,删除 Pod 强制重新拉取 latest..."
kubectl -n log delete po -l app=lokistack --wait=false >/dev/null 2>&1 || true
for _ in $(seq 1 30); do
sleep 8
POD_DIGEST=$(pod_digest)
[ "${POD_DIGEST:0:19}" = "${D_LATEST:0:19}" ] && break
done
echo " 重拉后 Pod : ${POD_DIGEST:0:19}"
fi
if [ "${POD_DIGEST:0:19}" = "${D_LATEST:0:19}" ]; then
echo " → 一致 ✅"
# ── 版本戳:下次排查"线上到底跑哪版"直接看 annotation ──
kubectl -n log annotate deploy/lokistack --overwrite \
"lokistack.io/release-image=${D_LATEST#sha256:}" \
"lokistack.io/release-tag=${IMMUTABLE_TAG}" \
"lokistack.io/release-time=$(date -Is)" >/dev/null 2>&1 \
&& echo " 已标注 run 版本:${D_LATEST:0:19} (${IMMUTABLE_TAG})"
else
echo " → 不一致 ❌(重拉后仍不符,检查节点镜像缓存 / 网络)"
echo "=========================================="
exit 1
fi
echo "=========================================="
fi
echo ""
echo "回滚方法(latest 策略下靠不可变 tag 追溯):"
echo " docker pull ${IMAGE_REPO}:<上次的 ${VERSION}-日期-commit>"
echo " docker tag ${IMAGE_REPO}:<该 tag> ${IMAGE_REPO}:latest && docker push ${IMAGE_REPO}:latest"
echo " kubectl -n log rollout restart deploy/lokistack"