Files
LokiStack/kubernetes/README.md
T
opsx c759e111ec feat(k8s): install.sh 三级部署预检——kubectl 缺失/连不上集群时给出可执行的具体修法
用户反馈:部署时报错只说「未找到 kubectl,请先安装」「kubectl 无法连接集群,
请检查 kubeconfig」,对新人等于没说 —— 不知道是没装工具、网络不通、凭据过期
还是 RBAC 没权限,也不知道下一步查什么。

原实现只有两行:
    command -v kubectl || die "未找到 kubectl,请先安装"
    kubectl version --request-timeout=5s || die "kubectl 无法连接集群,请检查 kubeconfig"

改为三级预检 + 一级软检查:

【0.1 kubectl 是否存在】
  按 CentOS/TencentOS、Ubuntu/Debian、macOS 分别给安装命令;
  额外给「机器没有外网」的替代路径(控制台下载 / 内网跳板机 / 只 scp 二进制)。

【0.2 kubeconfig 文件是否存在】← 新增,堵一个误判
  KUBECONFIG 指向不存在的文件时,kubectl 会忽略它并退回默认规则,
  最终报出 "Authentication required" / "connection refused" 这类误导性错误,
  让人白查凭据和网络。现在单独判一次,直接给出查找路径 + 各云控制台下载入口。
  支持 KUBECONFIG 用 : 分隔多路径,任一存在即通过。

【0.3 能否连上 API Server】按错误类型分类,各给专属修法
  · context 不存在      → get-contexts / use-context / 重新下载
  · 401 认证失败        → 云厂商 token 有效期几小时,重新下载;自建查客户端证书
  · 403 RBAC 拒绝       → auth whoami / auth can-i --list,列出部署所需的具体资源
  · x509 证书问题       → 确认证书过期;可信内网可临时 insecure-skip-tls-verify
  · 网络不通            → curl -k <API>/version、VPN/专线、安全组放行 6443、换跳板机
  · 其他                → 原样打印 kubectl 错误 + 通用排查清单

【0.4 部署权限软检查】auth can-i 非 yes 时只告警不阻断
  (首次部署时目标命名空间可能还不存在,硬拦会误伤正常流程)

失败时统一打印环境信息(kubeconfig 路径 / context / API Server / 当前用户),
一眼看出连的是哪个集群。连接超时可用 KUBECTL_TIMEOUT=15s 放宽(默认 8s)。

README 同步新增「前置条件」章节:预检项对照表 + 错误分类表 + 失败输出示例。

=== 验证 ===
从真实 install.sh 抽出预检段,用假 kubectl 精确模拟 8 种场景(两个脚本各跑一遍):
  A 没有 kubectl            → 退出 1,打印各系统安装命令 ✅
  B API Server 超时         → 退出 1,提示 VPN/专线/安全组 ✅
  C 没有 context            → 退出 1,提示 get-contexts/use-context ✅
  D 401 凭据过期            → 退出 1,提示重新下载 kubeconfig ✅
  E 403 权限不足            → 退出 1,列出所需资源权限 ✅
  F x509 证书过期           → 退出 1,提示证书/insecure 排障 ✅
  G 正常环境(真实集群)     → 通过预检,退出 0 ✅
  H kubeconfig 文件不存在   → 退出 1,提示下载位置(不误报成 401)✅
  结果:阻断 7/7、正常通过 1/1;bash -n 语法检查通过

⚠️ 踩坑记录:kubeconfig 多路径判断最初写成
     while IFS= read -r _p; do ...; done < <(printf "%s" "$PATHS" | tr ":" "\n")
   printf 输出没有结尾换行 → read 带着数据返回非 0(EOF)→ 循环体一次都不执行,
   导致所有场景都误报「找不到 kubeconfig」。改用 IFS=":" + for 拆分。
2026-10-28 07:25:15 +00:00

438 lines
18 KiB
Markdown
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.
# lokistack 部署到 Kubernetes
命名空间固定:**`log`**(日志相关资源统一放这里)
---
## 一条命令部署(推荐)
```bash
bash kubernetes/install.sh
```
脚本会依次完成:
1. 创建 `log` 命名空间
2. **生成随机密钥** `JWT_SECRET` / `ENCRYPTION_KEY` 写入 Secret `lokistack-secrets`
(已存在则跳过,**不会覆盖**)
3. 创建 PVC `lokistack-data`(30 GiB,使用集群默认 StorageClass)
4. 部署 `lokistack`(单副本 + Recreate)
5. 创建 Service `lokistack:8080`(ClusterIP)
6. 等待 Pod 就绪并输出访问方式
**需要 NodePort(配 ingress 时)**:
```bash
WITH_NODEPORT=1 bash kubernetes/install.sh
```
**幂等性**:重复执行只做增量更新,不会覆盖密钥、不会重建数据卷。
---
## 全部用法速查
| 场景 | 命令 |
|------|------|
| **标准部署**(自动探测云平台 + StorageClass)| `bash kubernetes/install.sh` |
| 同时开 NodePort(配 ingress 用)| `WITH_NODEPORT=1 bash kubernetes/install.sh` |
| 指定 StorageClass | `STORAGE_CLASS=cbs bash kubernetes/install.sh` |
| 指定 SC(自建用 local-path)| `STORAGE_CLASS=local-path bash kubernetes/install.sh` |
| **自建集群强制用节点本地盘**(hostPath)| `LOCAL_STORAGE=force bash kubernetes/install.sh` |
| 只诊断不动手(无变更)| `LOCAL_STORAGE=off bash kubernetes/install.sh` |
| **指定本地存储的节点/路径/容量** | `NODE_NAME=node1 PV_PATH=/data/lokistack PV_SIZE=50Gi bash kubernetes/install.sh` |
| 组合:自建 + 指定节点 + NodePort | `LOCAL_STORAGE=force NODE_NAME=node1 WITH_NODEPORT=1 bash kubernetes/install.sh` |
**环境变量一览**:
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `WITH_NODEPORT` | `0` | `1` = 额外创建 NodePort Service(30080)|
| `STORAGE_CLASS` | 空(自动探测)| 显式指定 StorageClass 名称 |
| `LOCAL_STORAGE` | `auto` | `auto` 无 SC 时自动建本地 PV / `off` 只诊断 / `force` 强制本地盘 |
| `NODE_NAME` | 空(第一个 Ready 节点)| hostPath PV 绑定的节点 |
| `PV_PATH` | `/var/lib/lokistack-data` | hostPath 在宿主机上的路径 |
| `PV_SIZE` | `30Gi` | PV 容量声明 |
---
## 环境要求
| 项 | 要求 |
|----|------|
| Kubernetes | 1.20+ |
| StorageClass | **脚本自动探测**(default 注解 → 唯一 SC → 手动 `STORAGE_CLASS=`);自建集群无 SC 时会提前报错并给出解决方案,不会白等 |
| 镜像 | 节点可拉 `lghost/lokistack:latest`(或改成自己的 Harbor/ACR/TCR/SWR) |
| 权限 | 有 `log` 命名空间的 create/apply 权限 |
## 前置条件(`install.sh` 会自动检查,不通过直接给出具体修法)
脚本启动时做**三级预检 + 一级软检查**,任何一级不通过都会立即退出并打印「是哪一类问题、
下一步做什么」,不会跑到一半才报错:
| 预检项 | 不通过时的提示内容 |
|--------|-------------------|
| **kubectl 是否安装** | 按 CentOS/TencentOS、Ubuntu/Debian、macOS 分别给安装命令;并给出「机器没有外网」时的替代方案(控制台下载 / 内网跳板机执行 / 只 scp 二进制) |
| **kubeconfig 是否存在** | 打印实际查找路径(`$KUBECONFIG` 或 `~/.kube/config`),给出腾讯云 TKE / 阿里云 ACK / AWS EKS 各自下载 kubeconfig 的入口,以及 `chmod 600` 建议 |
| **能否连上 API Server** | **按错误类型分类**(见下表),不是笼统一句「请检查 kubeconfig」 |
| **部署权限(软检查)** | `kubectl auth can-i create deployments.apps -n <ns>` 非 `yes` 时告警,提示绑定 Role 或改用管理员 kubeconfig;**不阻断**,避免首次部署时命名空间尚未创建导致误拦 |
### 连不上集群时的分类提示
| 实际错误 | 脚本的判断 | 给出的处理方向 |
|---------|-----------|---------------|
| `current-context is not set` / `context does not exist` | kubeconfig 没有可用 context | `kubectl config get-contexts` → `use-context` → 重新下载 kubeconfig |
| `Unauthorized` / `Authentication required` | 认证失败(云厂商 token 几小时就过期) | 回控制台重新下载 kubeconfig;自建集群查客户端证书 |
| `Forbidden` / `403` | 已连上集群但 RBAC 拒绝 | `kubectl auth whoami`、`auth can-i --list -n <ns>`,列出部署所需的具体资源权限 |
| `x509` / `certificate` / `tls:` | 证书过期或域名不匹配 | 确认 API Server 证书;可信内网可临时 `--insecure-skip-tls-verify` 排障 |
| `connection refused` / `no such host` / `context deadline exceeded` / `i/o timeout` | 网络不通 | ①`curl -k <API>/version` ②**内网地址需 VPN/专线** ③安全组放行 6443 ④查集群状态 ⑤换成跳板机试 |
失败时的完整输出形如:
```text
❌ 连不上集群 API Server(网络不通)
kubeconfig : /root/.kube/config
当前 context : tke-pre
API Server : https://172.19.16.13:443
当前用户 : admin
实际报错: Unable to connect to the server: context deadline exceeded (...)
── 按顺序排查 ───────────────────────────────────────────────────────
1) 地址对不对?用上面打印的 API Server 测一下: curl -k --max-time 5 <API Server>/version
2) 要不要 VPN / 专线?云上 API Server 常是内网地址(10.x / 172.16~31.x / 192.168.x)...
3) 安全组 / 防火墙:云厂商默认只放行运维网段访问 6443 端口
4) 集群本身是否正常:控制台看集群状态是否「运行中」
5) 换台机器试:在能访问集群的跳板机上执行本脚本,可排除本机网络问题
```
> 预检只读不写、耗时 < 1 秒。API Server 连接超时可用 `KUBECTL_TIMEOUT=15s` 放宽(默认 8s)。
## 脚本自动处理什么
`install.sh` 不是无脑 apply,它会先探测环境再动手:
| 场景 | 脚本行为 |
|------|---------|
| **云平台识别** | 读节点 `providerID`,识别出 腾讯云 TKE / AWS EKS / 阿里云 ACK / 华为云 CCE / GCP GKE / Azure AKS / K3s / 自建 |
| **StorageClass** | ① 显式 `STORAGE_CLASS=xxx` → ② `is-default-class` 注解 → ③ 集群唯一 SC → ④ **都没有则自动创建节点本地存储**(静态 local PV,零外部依赖) |
| **Secret** | 不存在则用 `openssl rand -hex 32` 生成;**已存在则跳过,绝不覆盖** |
| **PVC** | 把探测到的 SC 显式写进渲染后的 pvc.yaml(避免依赖隐式默认行为) |
| **就绪等待** | `rollout status` 最多 180s,超时后按**失败原因分类诊断** |
| **镜像拉不到** | 诊断为 `ImagePullBackOff` 时,按云给出对应仓库格式(TCR / ACR / SWR / Harbor) |
| **PVC 未绑定** | 诊断为 StorageClass 问题时,按云给出推荐 SC 名(cbs / gp3 / alicloud-disk-essd / csi-disk) |
**脚本不做什么**(需要人工决策的部分):
- ❌ 不自动改镜像仓库(不知道你的仓库地址,只给格式提示)
- ❌ 不覆盖已存在的 Secret / 已有数据卷
---
## 自建 K8s(kubeadm 裸装)的存储怎么办
自建集群最常见的坑就是**没有 StorageClass** → PVC 永久 Pending。脚本内置三种处理方式:
### 默认行为(`LOCAL_STORAGE=auto`):自动创建节点本地存储
检测到集群完全没有 SC 时,脚本自动创建一个**静态 local PV**:
```yaml
StorageClass: lokistack-local
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer # 先定 Pod 再绑 PV
reclaimPolicy: Retain # 误删 PVC 不丢数据
PersistentVolume: lokistack-data-pv
hostPath: { path: /var/lib/lokistack-data, type: DirectoryOrCreate }
nodeAffinity: 锁定一台节点
```
**特点**:
| 优点 | 代价 |
|------|------|
| **零外部依赖**(不拉任何镜像,国内网络友好)| 数据固定在单节点,Pod 不会漂移到别处 |
| kubelet 自动建目录(`DirectoryOrCreate`)| 只能供给一个 PVC(单实例场景够用)|
| Pod 一定调度到数据所在节点(`nodeAffinity`)| 节点损毁需从备份恢复(见「持久化」的 CSI 快照)|
**实测数据**:PVC 从 Pending 到 Bound **5 秒**,Pod 正常读写宿主机目录。
### 想要动态供给(支持多 PVC / 多实例)
装 Rancher **local-path-provisioner**(自建集群事实标准):
```bash
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
STORAGE_CLASS=local-path bash kubernetes/install.sh
```
### 想先看着不动手(`LOCAL_STORAGE=off`)
```bash
LOCAL_STORAGE=off bash kubernetes/install.sh
```
只打印诊断和 4 种解法,不做任何变更。
### 自建 K8s 存储方案选型
| 方案 | 外部依赖 | 适用 | 备注 |
|------|---------|------|------|
| **静态 local PV**(本脚本默认)| **零** | 单副本、数据固定 | 最省事,国内网络友好 |
| **local-path-provisioner** | 2 个镜像 | 需要动态供给/多 PVC | 自建集群首选,社区标准 |
| **OpenEBS LocalPV** | Helm + 较多镜像 | 要 LVM/ZFS 精细控制 | 较重 |
| **Longhorn** | Helm,每节点装 | 需要副本/高可用 | 每节点约 1C2G 开销 |
| **NFS + nfs-subdir-provisioner** | 需现成 NFS 服务器 | 多节点共享 | 依赖外部 NFS |
> 对 lokistack 这类**单副本 + JSON DB 单写者**的应用,**local PV 完全够用**,
> 不需要为了"高可用"上 Longhorn —— 副本存储反而会和"单写者"约束冲突。
---
## hostPath 数据的运维(自建集群必读)
自建集群用 hostPath 时,**数据在宿主机目录里**,不在云盘上。下面这些操作要会。
### 数据在哪
```bash
# 1) 找到 PV 绑定的节点和路径
kubectl get pv lokistack-data-pv -o custom-columns=\
'NAME:.metadata.name,PATH:.spec.hostPath.path,NODE:.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]'
# 2) 去那台节点上看
NODE=<上面的 NODE 值>
ssh root@${NODE} 'ls -la /var/lib/lokistack-data/'
# 应看到:lokistack.json kubeconfigs/ known_hosts
```
### 备份(不用停机)
```bash
NODE=<节点>
TS=$(date +%Y%m%d-%H%M%S)
ssh root@${NODE} "tar czf /tmp/lokistack-backup-${TS}.tar.gz -C /var/lib lokistack-data"
scp root@${NODE}:/tmp/lokistack-backup-${TS}.tar.gz ./
```
> `lokistack.json` 写入是原子的(临时文件 + fsync + rename),**热备份安全**,不用停 Pod。
### 迁移到另一台节点
```bash
OLD=<旧节点> NEW=<新节点>
# 1) 停止写入
kubectl -n log scale deploy lokistack --replicas=0
# 2) 从旧节点拉取数据
ssh root@${OLD} 'tar czf - -C /var/lib lokistack-data' > /tmp/lokistack-data.tar.gz
# 3) 推到新节点并解包
scp /tmp/lokistack-data.tar.gz root@${NEW}:/tmp/
ssh root@${NEW} 'mkdir -p /var/lib && tar xzf /tmp/lokistack-data.tar.gz -C /var/lib && ls -la /var/lib/lokistack-data'
# 4) 改 PV 的 nodeAffinity 指向新节点
kubectl patch pv lokistack-data-pv --type=json -p \
"[{\"op\":\"replace\",\"path\":\"/spec/nodeAffinity/required/nodeSelectorTerms/0/matchExpressions/0/values\",\"value\":[\"${NEW}\"]}]"
# 5) 恢复
kubectl -n log scale deploy lokistack --replicas=1
kubectl -n log rollout status deploy/lokistack
```
### 卸载但保留数据
```bash
# 删工作负载和服务(数据卷保留)
kubectl -n log delete deploy lokistack svc lokistack
# PVC 保留(Retain 策略,删了也不会丢 PV 里的数据)
# 想彻底清掉的话:
# kubectl delete pvc lokistack-data
# kubectl delete pv lokistack-data-pv
# 然后去节点上 rm -rf /var/lib/lokistack-data
# 如果要连脏数据一起重置重装:
kubectl -n log delete pvc lokistack-data
kubectl delete pv lokistack-data-pv
kubectl delete sc lokistack-local
ssh root@${NODE} 'rm -rf /var/lib/lokistack-data'
# 然后重新 bash install.sh
```
### 常见坑
| 现象 | 原因 | 处理 |
|------|------|------|
| Pod 一直 Pending | PV 的 nodeAffinity 指向的节点不可用 | `kubectl get nodes` 确认该节点 Ready,或按上面「迁移」流程改节点 |
| Pod Pending 且报 `node(s) didn't match node affinity` | Pod 被调度到其他节点 | 正常,scheduler 会重试;若持续,检查 PV 的 nodeAffinity 是否写错节点名 |
| 换节点后数据"消失" | 数据在新节点上不存在 | 走「迁移到另一台节点」流程,别只改 nodeAffinity |
| 磁盘写满 | hostPath 用节点本地盘 | `df -h /var/lib`,迁移到大盘或改 `PV_PATH` |
---
## 目录结构
```
kubernetes/
├── install.sh # 一键部署脚本(推荐入口)
├── namespace.yaml # log 命名空间
├── pvc.yaml # 数据卷 30Gi(storageClassName 留空 → 用默认 SC)
├── deployment.yaml # 平台本体(单副本 + Recreate,必须)
├── service.yaml # ClusterIP lokistack:8080
├── service-nodeport.yaml # NodePort 30080(配 ingress 时用)
└── examples/ # 云相关配置,不参与默认部署
├── ingress.yaml # 阿里云 ACK(alb class)
├── ingress-tke.yaml # 腾讯云 TKE(qcloud class)
└── secret.example.yaml # Secret 模板(仅供手工创建参考)
```
> ⚠️ `examples/` 里的文件**故意不放在 `kubernetes/` 根目录**:
> 它们是云厂商相关的(ingress class 各云不同),混在默认目录里会让
> `kubectl apply -f kubernetes/` 在不同云上互相覆盖或报错。
---
## 手动部署(等价于 install.sh,供需要精细控制时用)
```bash
# 1) 命名空间
kubectl apply -f kubernetes/namespace.yaml
# 2) 密钥(⚠️ 必须先做,否则 Pod 起不来)
# 密钥只生成一次,丢失会导致已保存的凭证无法解密
kubectl -n log create secret generic lokistack-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=ENCRYPTION_KEY="$(openssl rand -hex 32)"
# 3) 数据卷 + 平台 + 服务
kubectl apply -f kubernetes/pvc.yaml
kubectl apply -f kubernetes/deployment.yaml
kubectl apply -f kubernetes/service.yaml
# 4) 等待就绪
kubectl -n log rollout status deployment/lokistack --timeout=180s
```
> ❗**不要直接 `kubectl apply -f kubernetes/`**:Secret 不在目录里(密钥不能进 Git),
> 缺 Secret 时 Pod 会一直 `CreateContainerConfigError`。
---
## 访问
**临时(本地端口转发)**:
```bash
kubectl -n log port-forward svc/lokistack 8080:8080
# 浏览器打开 http://localhost:8080
```
**初始账号**:`admin` / `admin123` ← **首次登录请立即改密**
---
## 配 Ingress(可选)
以腾讯云 TKE 为例(其他云见 `examples/`):
```bash
# 1) 先开 NodePort(TKE 的 qcloud ingress 后端要求 NodePort 类型 Service)
WITH_NODEPORT=1 bash kubernetes/install.sh
# 2) 改域名后应用
vi kubernetes/examples/ingress-tke.yaml # 改 host
kubectl apply -f kubernetes/examples/ingress-tke.yaml
# 3) 查 CLB 分配的公网 IP,把域名解析过去
kubectl -n log get ingress lokistack
```
| 云 | ingressClassName | 备注 |
|----|------------------|------|
| 腾讯云 TKE | `qcloud` | 后端 Service 必须 NodePort |
| 阿里云 ACK | `alb` | 用 `alb.ingress.kubernetes.io/*` 注解 |
| AWS EKS | `alb` 或 `nginx` | 需装 AWS Load Balancer Controller |
| 华为云 CCE | `nginx` 或 `cce` | — |
| 自建 | `nginx` | 需装 ingress-nginx |
---
## 持久化(重要)
- `/app/data` 挂 PVC,保存 `lokistack.json`、`kubeconfigs/`、`known_hosts`。
- **必须 `replicas: 1` + `strategy: Recreate`**:JSON DB 是单写者;滚动更新时两个 Pod
同时挂同一个 RWO PVC 会互相覆盖。
- DB 写入本身是原子的(临时文件 + fsync + rename),容器被杀不会写坏文件。
### 备份:CSI VolumeSnapshot
各云都支持同一套 `snapshot.storage.k8s.io` API(AWS EBS / 腾讯 CBS / 华为 EVS / 阿里云 ESSD):
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: lokistack-data-20261025
namespace: log
spec:
volumeSnapshotClassName: <集群的 snapshotClass>
source:
persistentVolumeClaimName: lokistack-data
```
---
## 多云兼容
| 项 | 做法 |
|---|---|
| 存储类 | `pvc.yaml` **不写** `storageClassName`,用集群默认 SC;没有默认 SC 时填对应云的名字(AWS `gp3` / 腾讯 `cbs` / 华为 `csi-disk` / 阿里云 `alicloud-disk-essd`)|
| 镜像仓库 | `deployment.yaml` 的 `image` 填目标云可拉的仓库(Harbor / ACR / TCR / SWR)|
| Loki 存储 | 平台已支持 S3 / OSS / COS / OBS,均 S3 兼容,无需云厂商 SDK |
| 查询链路 | 平台与 Loki 同集群时,数据源选「集群服务」(namespace `log`、service `loki`、port `3100`),无需 Ingress/WAF |
| Namespace | 固定 `log`,跨云一致 |
---
## 与外部实例的关系
平台的 JSON DB 单写者,**不要两个实例共用同一份数据卷**。要么:
- 集群内新实例 + 迁移旧数据文件(停旧实例后拷贝);或
- 保留外部实例做带外管理,集群内实例使用独立数据卷(两套数据)。
---
## 日志
- 平台**只写 stdout**(Go/GIN 默认),不写日志文件;`kubectl logs` 可直接看。
- Kubernetes 由 kubelet 统一轮转,节点级配置建议:
```yaml
# /var/lib/kubelet/config.yaml
containerLogMaxSize: 50Mi
containerLogMaxFiles: 5
```
- Loki / Promtail 同样输出到 stdout,由容器运行时轮转;不要在容器内写业务日志文件。
- 应用自身的部署日志写在 DB(`deployment_logs`),可在部署详情里查看,不占容器日志空间。
---
## 常见问题
| 现象 | 原因 | 解决 |
|------|------|------|
| Pod `CreateContainerConfigError` | 缺 Secret `lokistack-secrets` | 跑 `install.sh`,或手动 `kubectl create secret` |
| Pod `Pending`,PVC `Pending` | 集群没有默认 StorageClass | `pvc.yaml` 里填 `storageClassName` |
| Pod `ImagePullBackOff` | 节点拉不到 `lghost/lokistack:latest` | 换镜像地址(ACR/TCR/SWR/Harbor) |
| `kubectl apply -f kubernetes/` 报 ingress 错误 | 误把 examples 里的 ingress 一起 apply 了 | 只 apply 根目录的 yaml,ingress 单独处理 |
| 重启后数据丢失 | `/app/data` 没挂 PVC | 确认 `deployment.yaml` 的 volumeMounts |
| 两个 Pod 同时写 DB | 用了 RollingUpdate 或多副本 | 必须 `replicas: 1` + `strategy: Recreate` |