1035 lines
40 KiB
Markdown
1035 lines
40 KiB
Markdown
# 节点驱逐 - 完整解决方案
|
||
|
||
## 一、环境概览
|
||
|
||
### 集群节点信息
|
||
|
||
| 节点 IP | 角色 | 操作系统 | 状态 | 处理方式 |
|
||
|---------------|-------------------------------|-----------------------------------------|--------|------------------------|
|
||
| 10.20.1.130 | controlplane, etcd, worker | openEuler 20.03 (LTS-SP3) | 保留 | master 节点不动 |
|
||
| 10.20.1.133 | worker | openEuler 20.03 (LTS-SP3) | 保留 | 中间件调度目标 |
|
||
| 10.20.1.134 | worker | openEuler 20.03 (LTS-SP3) | 保留 | 中间件调度目标 (mysql) |
|
||
| 10.20.1.141 | worker | BigCloud Enterprise Linux For Euler | 保留 | 中间件调度目标 |
|
||
| **10.20.1.142** | worker | BigCloud Enterprise Linux For Euler | **清退** | 一个月后清退 |
|
||
| **10.20.1.144** | worker | BigCloud Enterprise Linux For Euler | **清退** | 一个月后清退 |
|
||
| **10.20.1.145** | worker | BigCloud Enterprise Linux For Euler | **清退** | 一个月后清退 |
|
||
|
||
### 需求拆解
|
||
|
||
1. **清退节点**:10.20.1.142、10.20.1.144、10.20.1.145(一个月后执行)
|
||
2. **jxyd 命名空间 Deployment**:生命周期与被清退节点一致 → 即**直接删除**
|
||
3. **jxyd 命名空间中间件**:需保留,迁移调度到 10.20.1.133、10.20.1.134、10.20.1.141
|
||
4. **资源调整**:超标 Deployment 统一降配 + 同步调整 JVM 参数
|
||
|
||
---
|
||
|
||
## 二、操作流程总览
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Phase 1: 信息采集与备份(立即执行) │
|
||
│ - 导出所有 deployment/statefulset 信息 │
|
||
│ - 记录当前 Pod 分布 │
|
||
│ - 备份所有 YAML │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ Phase 2: 资源调整(立即执行) │
|
||
│ - 扫描 jxyd 业务 deployment 的资源配置(不扫描中间件) │
|
||
│ - 将超标资源统一降配 │
|
||
│ - 同步修改 CUST_JAVA_OPTS 环境变量 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ Phase 3: 中间件及业务迁移(立即执行) │
|
||
│ - 给保留节点及待清退节点打 label │
|
||
│ - 调度中间件至保留节点,调度业务 Deployment 至待清退节点 │
|
||
│ - 验证迁移及调度在对应节点上正常运行 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ Phase 4: 节点清退(一个月后执行) │
|
||
│ - 删除 jxyd 命名空间中的业务 deployment │
|
||
│ - cordon + drain 节点 │
|
||
│ - 从集群中移除节点 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 三、Phase 1:信息采集与备份
|
||
|
||
### 1.1 备份脚本 `phase1_backup.sh`
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# ============================================================
|
||
# Phase 1: 信息采集与备份
|
||
# 在 master 节点 (10.20.1.130) 上执行
|
||
# ============================================================
|
||
|
||
BACKUP_DIR="/root/wdd/backup_260617/$(date +%Y%m%d_%H%M%S)"
|
||
NAMESPACE="jxyd"
|
||
mkdir -p "${BACKUP_DIR}"
|
||
|
||
echo "=========================================="
|
||
echo " Phase 1: 信息采集与备份"
|
||
echo " 备份目录: ${BACKUP_DIR}"
|
||
echo "=========================================="
|
||
|
||
# 1. 导出节点信息
|
||
echo "[1/6] 导出节点信息..."
|
||
kubectl get nodes -o wide --show-labels > "${BACKUP_DIR}/nodes_info.txt"
|
||
|
||
# 2. 导出 jxyd 命名空间所有资源
|
||
echo "[2/6] 导出 jxyd 命名空间所有资源 YAML..."
|
||
kubectl get all -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/all_resources.yaml"
|
||
|
||
# 3. 单独导出每个 deployment
|
||
echo "[3/6] 导出每个 Deployment YAML..."
|
||
mkdir -p "${BACKUP_DIR}/deployments"
|
||
for dep in $(kubectl get deployments -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}'); do
|
||
kubectl get deployment "${dep}" -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/deployments/${dep}.yaml"
|
||
echo " 已备份 deployment: ${dep}"
|
||
done
|
||
|
||
# 4. 单独导出每个 statefulset(中间件通常用 statefulset)
|
||
echo "[4/6] 导出每个 StatefulSet YAML..."
|
||
mkdir -p "${BACKUP_DIR}/statefulsets"
|
||
for sts in $(kubectl get statefulsets -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
|
||
kubectl get statefulset "${sts}" -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/statefulsets/${sts}.yaml"
|
||
echo " 已备份 statefulset: ${sts}"
|
||
done
|
||
|
||
# 5. 导出 ConfigMap 和 Secret
|
||
echo "[5/6] 导出 ConfigMap 和 Secret..."
|
||
mkdir -p "${BACKUP_DIR}/configmaps" "${BACKUP_DIR}/secrets"
|
||
for cm in $(kubectl get configmaps -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}'); do
|
||
kubectl get configmap "${cm}" -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/configmaps/${cm}.yaml"
|
||
done
|
||
for sec in $(kubectl get secrets -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}'); do
|
||
kubectl get secret "${sec}" -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/secrets/${sec}.yaml"
|
||
done
|
||
|
||
# 6. 导出 Pod 分布信息(关键:记录哪些 Pod 在待清退节点上)
|
||
echo "[6/6] 导出 Pod 分布信息..."
|
||
echo "=== 所有 Pod 的节点分布 ===" > "${BACKUP_DIR}/pod_distribution.txt"
|
||
kubectl get pods -n ${NAMESPACE} -o wide >> "${BACKUP_DIR}/pod_distribution.txt"
|
||
|
||
echo ""
|
||
echo "--- 待清退节点上的 Pod ---" >> "${BACKUP_DIR}/pod_distribution.txt"
|
||
for NODE in 10.20.1.142 10.20.1.144 10.20.1.145; do
|
||
echo "" >> "${BACKUP_DIR}/pod_distribution.txt"
|
||
echo "=== 节点 ${NODE} 上的 Pod ===" >> "${BACKUP_DIR}/pod_distribution.txt"
|
||
kubectl get pods -n ${NAMESPACE} -o wide --field-selector spec.nodeName=${NODE} >> "${BACKUP_DIR}/pod_distribution.txt"
|
||
done
|
||
|
||
# 7. 导出 Service 和 Ingress
|
||
mkdir -p "${BACKUP_DIR}/services" "${BACKUP_DIR}/ingresses"
|
||
for svc in $(kubectl get svc -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}'); do
|
||
kubectl get svc "${svc}" -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/services/${svc}.yaml"
|
||
done
|
||
for ing in $(kubectl get ingress -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
|
||
kubectl get ingress "${ing}" -n ${NAMESPACE} -o yaml > "${BACKUP_DIR}/ingresses/${ing}.yaml"
|
||
done
|
||
|
||
# 8. 生成资源使用汇总
|
||
echo ""
|
||
echo "=== 资源使用汇总 ===" | tee "${BACKUP_DIR}/resource_summary.txt"
|
||
kubectl top nodes 2>/dev/null | tee -a "${BACKUP_DIR}/resource_summary.txt"
|
||
echo "" | tee -a "${BACKUP_DIR}/resource_summary.txt"
|
||
kubectl top pods -n ${NAMESPACE} 2>/dev/null | tee -a "${BACKUP_DIR}/resource_summary.txt"
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
echo " 备份完成!目录: ${BACKUP_DIR}"
|
||
echo "=========================================="
|
||
```
|
||
|
||
---
|
||
|
||
## 四、Phase 2:资源检查与调整
|
||
|
||
### 2.1 Python 脚本 `phase2_resource_adjust.py`
|
||
|
||
此脚本会:
|
||
- 扫描 jxyd 命名空间下所有 Deployment(中间件不存在资源限制,且只在 StatefulSet,因此阶段二不考虑中间件)
|
||
- 检测哪些容器的资源超标(超过 limits cpu:2 / memory:2Gi)
|
||
- 对超标的 Deployment 自动执行 `kubectl patch` 降配
|
||
- 同步修改 `CUST_JAVA_OPTS` 环境变量
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Phase 2: 资源检查与调整
|
||
功能:
|
||
1. 扫描 jxyd 命名空间下所有 Deployment 的资源配置
|
||
2. 识别超标资源(超过 limits cpu:2 / memory:2Gi)
|
||
3. 自动 patch 降配 + 修改 CUST_JAVA_OPTS
|
||
4. 生成变更报告
|
||
|
||
使用方式:
|
||
python3 phase2_resource_adjust.py --dry-run # 仅预览,不执行变更
|
||
python3 phase2_resource_adjust.py --apply # 执行变更
|
||
"""
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
import re
|
||
import argparse
|
||
from datetime import datetime
|
||
|
||
|
||
# ============================================================
|
||
# 配置区
|
||
# ============================================================
|
||
NAMESPACE = "jxyd"
|
||
|
||
# 目标资源规格
|
||
TARGET_RESOURCES = {
|
||
"limits": {"cpu": "2", "memory": "2Gi"},
|
||
"requests": {"cpu": "1", "memory": "500Mi"}
|
||
}
|
||
|
||
# 目标 JVM 参数
|
||
TARGET_JAVA_OPTS = "-Xms500m -Xmx2000m -Dlog4j2.formatMsgNoLookups=true"
|
||
|
||
# ============================================================
|
||
# 工具函数
|
||
# ============================================================
|
||
|
||
def run_kubectl(args, capture=True):
|
||
"""执行 kubectl 命令"""
|
||
cmd = ["kubectl"] + args
|
||
result = subprocess.run(cmd, capture_output=capture, text=True)
|
||
if result.returncode != 0:
|
||
print(f" [错误] 命令失败: {' '.join(cmd)}")
|
||
print(f" stderr: {result.stderr}")
|
||
return None
|
||
return result.stdout
|
||
|
||
|
||
def parse_memory(mem_str):
|
||
"""将内存字符串转换为字节数"""
|
||
if not mem_str:
|
||
return 0
|
||
mem_str = str(mem_str)
|
||
units = {
|
||
'Ki': 1024, 'Mi': 1024**2, 'Gi': 1024**3, 'Ti': 1024**4,
|
||
'K': 1000, 'M': 1000**2, 'G': 1000**3, 'T': 1000**4,
|
||
'k': 1000, 'm': 0.001 # millibytes (unusual but valid)
|
||
}
|
||
for suffix, multiplier in sorted(units.items(), key=lambda x: -len(x[0])):
|
||
if mem_str.endswith(suffix):
|
||
try:
|
||
return float(mem_str[:-len(suffix)]) * multiplier
|
||
except ValueError:
|
||
return 0
|
||
try:
|
||
return float(mem_str) # 纯数字,单位为字节
|
||
except ValueError:
|
||
return 0
|
||
|
||
|
||
def parse_cpu(cpu_str):
|
||
"""将 CPU 字符串转换为核心数(float)"""
|
||
if not cpu_str:
|
||
return 0.0
|
||
cpu_str = str(cpu_str)
|
||
if cpu_str.endswith('m'):
|
||
return float(cpu_str[:-1]) / 1000.0
|
||
return float(cpu_str)
|
||
|
||
|
||
def is_resource_over_limit(resources):
|
||
"""检查资源是否超标"""
|
||
limits = resources.get("limits", {})
|
||
cpu_limit = limits.get("cpu", "0")
|
||
mem_limit = limits.get("memory", "0")
|
||
|
||
cpu_over = parse_cpu(cpu_limit) > parse_cpu(TARGET_RESOURCES["limits"]["cpu"])
|
||
mem_over = parse_memory(mem_limit) > parse_memory(TARGET_RESOURCES["limits"]["memory"])
|
||
|
||
return cpu_over or mem_over
|
||
|
||
|
||
# ============================================================
|
||
# 主逻辑
|
||
# ============================================================
|
||
|
||
def get_deployments():
|
||
"""获取所有 Deployment 信息"""
|
||
output = run_kubectl([
|
||
"get", "deployments", "-n", NAMESPACE,
|
||
"-o", "json"
|
||
])
|
||
if not output:
|
||
return []
|
||
data = json.loads(output)
|
||
return data.get("items", [])
|
||
|
||
|
||
def analyze_and_patch(workloads, kind, dry_run=True):
|
||
"""分析并 patch 超标资源,若副本数为 0 则直接删除"""
|
||
report = {
|
||
"business": [],
|
||
"patched": [],
|
||
"skipped": [],
|
||
"errors": [],
|
||
"deleted": []
|
||
}
|
||
|
||
for wl in workloads:
|
||
name = wl["metadata"]["name"]
|
||
replicas = wl["spec"].get("replicas", 1)
|
||
|
||
if replicas == 0:
|
||
print(f"\n [删除] {kind}/{name} (副本数为 0)")
|
||
if dry_run:
|
||
print(" [DRY-RUN] 跳过删除")
|
||
report["skipped"].append({"name": name, "reason": "replicas=0"})
|
||
else:
|
||
result = run_kubectl([
|
||
"delete", kind.lower(), name,
|
||
"-n", NAMESPACE
|
||
])
|
||
if result is not None:
|
||
print(" [成功] 已删除")
|
||
report["deleted"].append(name)
|
||
else:
|
||
print(" [失败] 删除失败")
|
||
report["errors"].append({"name": name, "reason": "delete failed"})
|
||
continue
|
||
|
||
spec = wl["spec"]["template"]["spec"]
|
||
containers = spec.get("containers", [])
|
||
|
||
category = "business"
|
||
|
||
for idx, container in enumerate(containers):
|
||
container_name = container.get("name", f"container-{idx}")
|
||
resources = container.get("resources", {})
|
||
limits = resources.get("limits", {})
|
||
requests = resources.get("requests", {})
|
||
|
||
# 提取当前 JAVA_OPTS
|
||
current_java_opts = ""
|
||
env_list = container.get("env", [])
|
||
for env in env_list:
|
||
if env.get("name") == "CUST_JAVA_OPTS":
|
||
current_java_opts = env.get("value", "")
|
||
break
|
||
|
||
item = {
|
||
"name": name,
|
||
"kind": kind,
|
||
"container": container_name,
|
||
"container_index": idx,
|
||
"category": category,
|
||
"current_limits_cpu": limits.get("cpu", "未设置"),
|
||
"current_limits_memory": limits.get("memory", "未设置"),
|
||
"current_requests_cpu": requests.get("cpu", "未设置"),
|
||
"current_requests_memory": requests.get("memory", "未设置"),
|
||
"current_java_opts": current_java_opts,
|
||
"over_limit": is_resource_over_limit(resources),
|
||
}
|
||
|
||
report[category].append(item)
|
||
|
||
# 只调整超标的
|
||
if item["over_limit"]:
|
||
print(f"\n [超标] {kind}/{name} (容器: {container_name})")
|
||
print(f" 当前: limits={{cpu:{limits.get('cpu', 'N/A')}, memory:{limits.get('memory', 'N/A')}}}")
|
||
print(f" 目标: limits={{cpu:{TARGET_RESOURCES['limits']['cpu']}, memory:{TARGET_RESOURCES['limits']['memory']}}}")
|
||
|
||
if dry_run:
|
||
print(f" [DRY-RUN] 跳过 patch")
|
||
report["skipped"].append(item)
|
||
else:
|
||
# 构造 patch JSON
|
||
patch = {
|
||
"spec": {
|
||
"template": {
|
||
"spec": {
|
||
"containers": []
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# 构建容器 patch(需要按索引)
|
||
container_patch = {
|
||
"name": container_name,
|
||
"resources": TARGET_RESOURCES
|
||
}
|
||
|
||
# 同步修改 CUST_JAVA_OPTS(如果存在该 env)
|
||
if current_java_opts:
|
||
# 使用 kubectl set env 更简洁,这里用 strategic merge patch
|
||
new_env = []
|
||
for env in env_list:
|
||
if env.get("name") == "CUST_JAVA_OPTS":
|
||
new_env.append({
|
||
"name": "CUST_JAVA_OPTS",
|
||
"value": TARGET_JAVA_OPTS
|
||
})
|
||
else:
|
||
new_env.append(env)
|
||
container_patch["env"] = new_env
|
||
|
||
patch["spec"]["template"]["spec"]["containers"] = [container_patch]
|
||
|
||
patch_json = json.dumps(patch)
|
||
|
||
result = run_kubectl([
|
||
"patch", kind.lower(), name,
|
||
"-n", NAMESPACE,
|
||
"--type", "strategic",
|
||
"-p", patch_json
|
||
])
|
||
|
||
if result is not None:
|
||
print(f" [成功] 已 patch {kind}/{name}")
|
||
report["patched"].append(item)
|
||
else:
|
||
print(f" [失败] patch {kind}/{name} 失败")
|
||
report["errors"].append(item)
|
||
else:
|
||
# 未超标但可能也需要同步 JAVA_OPTS
|
||
if current_java_opts and current_java_opts != TARGET_JAVA_OPTS:
|
||
print(f"\n [JAVA_OPTS 不一致] {kind}/{name} (容器: {container_name})")
|
||
print(f" 当前: {current_java_opts}")
|
||
print(f" 目标: {TARGET_JAVA_OPTS}")
|
||
|
||
if not dry_run:
|
||
result = run_kubectl([
|
||
"set", "env",
|
||
f"{kind.lower()}/{name}",
|
||
"-n", NAMESPACE,
|
||
f"CUST_JAVA_OPTS={TARGET_JAVA_OPTS}",
|
||
"-c", container_name
|
||
])
|
||
if result is not None:
|
||
print(f" [成功] 已更新 JAVA_OPTS")
|
||
else:
|
||
print(f" [失败] 更新 JAVA_OPTS 失败")
|
||
|
||
return report
|
||
|
||
|
||
def print_summary(report, kind):
|
||
"""打印汇总报告"""
|
||
print(f"\n{'='*60}")
|
||
print(f" {kind} 资源扫描报告")
|
||
print(f"{'='*60}")
|
||
|
||
print(f"\n 🚀 业务 Deployment ({len(report['business'])} 个容器):")
|
||
for item in report["business"]:
|
||
over = "⚠️ 超标" if item["over_limit"] else "✅ 正常"
|
||
print(f" - {item['name']}/{item['container']}: "
|
||
f"limits(cpu={item['current_limits_cpu']}, mem={item['current_limits_memory']}) "
|
||
f"requests(cpu={item['current_requests_cpu']}, mem={item['current_requests_memory']}) "
|
||
f"{over}")
|
||
|
||
if report.get("deleted"):
|
||
print(f"\n 🗑️ 已删除 (副本数为0): {len(report['deleted'])} 个")
|
||
if report["patched"]:
|
||
print(f"\n 🔧 已 Patch: {len(report['patched'])} 个")
|
||
if report["skipped"]:
|
||
print(f" ⏭️ 跳过 (DRY-RUN): {len(report['skipped'])} 个")
|
||
if report["errors"]:
|
||
print(f" ❌ 失败: {len(report['errors'])} 个")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Phase 2: jxyd 命名空间资源检查与调整")
|
||
group = parser.add_mutually_exclusive_group(required=True)
|
||
group.add_argument("--dry-run", action="store_true", help="仅预览,不执行变更")
|
||
group.add_argument("--apply", action="store_true", help="执行变更")
|
||
args = parser.parse_args()
|
||
|
||
dry_run = args.dry_run
|
||
|
||
print(f"\n{'#'*60}")
|
||
print(f" Phase 2: 资源检查与调整")
|
||
print(f" 命名空间: {NAMESPACE}")
|
||
print(f" 模式: {'DRY-RUN(仅预览)' if dry_run else '⚡ APPLY(执行变更)'}")
|
||
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print(f"{'#'*60}")
|
||
|
||
if not dry_run:
|
||
confirm = input("\n ⚠️ 确认要执行资源调整吗?(yes/no): ")
|
||
if confirm.lower() != "yes":
|
||
print(" 已取消。")
|
||
return
|
||
|
||
# 扫描 Deployment (业务)
|
||
print(f"\n{'='*60}")
|
||
print(" 扫描 Deployment...")
|
||
print(f"{'='*60}")
|
||
deployments = get_deployments()
|
||
dep_report = analyze_and_patch(deployments, "Deployment", dry_run)
|
||
print_summary(dep_report, "Deployment")
|
||
|
||
# 生成变更日志
|
||
log_file = f"/root/wdd/backup_260617/phase2_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||
log_data = {
|
||
"timestamp": datetime.now().isoformat(),
|
||
"mode": "dry-run" if dry_run else "apply",
|
||
"deployment_report": {
|
||
"business_count": len(dep_report["business"]),
|
||
"patched_count": len(dep_report["patched"]),
|
||
"error_count": len(dep_report["errors"]),
|
||
}
|
||
}
|
||
|
||
try:
|
||
with open(log_file, 'w') as f:
|
||
json.dump(log_data, f, indent=2, ensure_ascii=False)
|
||
print(f"\n 📄 报告已保存到: {log_file}")
|
||
except Exception as e:
|
||
print(f"\n [警告] 无法保存报告文件: {e}")
|
||
|
||
print(f"\n{'#'*60}")
|
||
print(f" Phase 2 完成!")
|
||
print(f"{'#'*60}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
---
|
||
|
||
## 五、Phase 3:中间件迁移
|
||
|
||
### 3.1 给保留节点打 Label
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# ============================================================
|
||
# Phase 3.1: 给保留节点打 Label
|
||
# ============================================================
|
||
|
||
echo "=== 给保留节点打 label: jxyd-middleware=true ==="
|
||
|
||
# 保留节点
|
||
kubectl label node 10.20.1.133 jxyd-middleware=true --overwrite
|
||
kubectl label node 10.20.1.134 jxyd-middleware=true --overwrite
|
||
kubectl label node 10.20.1.141 jxyd-middleware=true --overwrite
|
||
|
||
echo ""
|
||
echo ""
|
||
echo "=== 给待清退节点打 label: jxyd-business=true ==="
|
||
# 待清退节点(将业务部署到这些节点)
|
||
kubectl label node 10.20.1.142 jxyd-business=true --overwrite
|
||
kubectl label node 10.20.1.144 jxyd-business=true --overwrite
|
||
kubectl label node 10.20.1.145 jxyd-business=true --overwrite
|
||
|
||
echo ""
|
||
echo "=== 验证 label ==="
|
||
kubectl get nodes -l jxyd-middleware=true -o wide
|
||
kubectl get nodes -l jxyd-business=true -o wide
|
||
```
|
||
|
||
### 3.2 调度与迁移脚本 `phase3_migrate_workloads.py`
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Phase 3: 工作负载迁移与调度
|
||
功能:
|
||
1. 识别 jxyd 命名空间下的中间件 StatefulSet 与 业务 Deployment
|
||
2. 为中间件添加 nodeSelector,调度到保留节点(过滤包含 hostPath 的中间件)
|
||
3. 为业务 Deployment 添加 nodeSelector,调度到待清退节点
|
||
4. 验证调度和迁移结果
|
||
|
||
使用方式:
|
||
python3 phase3_migrate_workloads.py --list # 列出迁移计划
|
||
python3 phase3_migrate_workloads.py --dry-run # 预览变更
|
||
python3 phase3_migrate_workloads.py --apply # 执行迁移
|
||
python3 phase3_migrate_workloads.py --verify # 验证迁移结果
|
||
"""
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
import argparse
|
||
from datetime import datetime
|
||
|
||
|
||
NAMESPACE = "jxyd"
|
||
|
||
# 目标节点 label
|
||
TARGET_NODE_SELECTOR = {"jxyd-middleware": "true"}
|
||
BUSINESS_NODE_SELECTOR = {"jxyd-business": "true"}
|
||
|
||
# 中间件关键词
|
||
MIDDLEWARE_KEYWORDS = [
|
||
"mysql", "redis", "rabbitmq", "kafka", "zookeeper", "elasticsearch",
|
||
"nacos", "minio", "mongo", "postgres", "nginx",
|
||
"sentinel", "rocketmq", "emqx", "mqtt", "influxdb", "grafana",
|
||
"prometheus", "xxl-job", "seata"
|
||
]
|
||
|
||
# 待清退节点
|
||
EVICT_NODES = ["10.20.1.142", "10.20.1.144", "10.20.1.145"]
|
||
|
||
|
||
def run_kubectl(args):
|
||
cmd = ["kubectl"] + args
|
||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||
if result.returncode != 0:
|
||
print(f" [错误] {' '.join(cmd)}: {result.stderr.strip()}")
|
||
return None
|
||
return result.stdout
|
||
|
||
|
||
def is_middleware(name):
|
||
name_lower = name.lower()
|
||
return any(kw in name_lower for kw in MIDDLEWARE_KEYWORDS)
|
||
|
||
|
||
def get_workloads(kind):
|
||
output = run_kubectl(["get", kind, "-n", NAMESPACE, "-o", "json"])
|
||
if not output:
|
||
return []
|
||
return json.loads(output).get("items", [])
|
||
|
||
|
||
def list_workloads():
|
||
"""列出工作负载迁移与调度计划"""
|
||
print(f"\n{'='*60}")
|
||
print(f" jxyd 命名空间 - 迁移与调度计划")
|
||
print(f"{'='*60}")
|
||
|
||
# StatefulSet (中间件)
|
||
sts_workloads = get_workloads("statefulsets")
|
||
mw_list = [w for w in sts_workloads if is_middleware(w["metadata"]["name"])]
|
||
print(f"\n 📦 中间件 StatefulSet (调度到保留节点,{len(mw_list)} 个):")
|
||
for w in mw_list:
|
||
name = w["metadata"]["name"]
|
||
replicas = w["spec"].get("replicas", 1)
|
||
node_selector = w["spec"]["template"]["spec"].get("nodeSelector", {})
|
||
volumes = w["spec"]["template"]["spec"].get("volumes", [])
|
||
has_host_path = any("hostPath" in v for v in volumes)
|
||
host_path_flag = "[含 hostPath,不可迁移]" if has_host_path else ""
|
||
print(f" ✅ {name} (replicas={replicas}, nodeSelector={node_selector}) {host_path_flag}")
|
||
|
||
# Deployment (业务)
|
||
dep_workloads = get_workloads("deployments")
|
||
print(f"\n 🚀 业务 Deployment (调度到待清退节点,{len(dep_workloads)} 个):")
|
||
for w in dep_workloads:
|
||
name = w["metadata"]["name"]
|
||
replicas = w["spec"].get("replicas", 1)
|
||
node_selector = w["spec"]["template"]["spec"].get("nodeSelector", {})
|
||
if replicas == 0:
|
||
print(f" - {name} (replicas=0, 忽略调度)")
|
||
else:
|
||
print(f" ✅ {name} (replicas={replicas}, nodeSelector={node_selector})")
|
||
|
||
print(f"\n 💡 提示: 若状态不准确可人工审核配置。")
|
||
|
||
|
||
def migrate_workloads(dry_run=True):
|
||
"""执行工作负载的迁移与调度"""
|
||
print(f"\n{'='*60}")
|
||
print(f" 工作负载调度 - {'DRY-RUN' if dry_run else 'APPLY'}")
|
||
print(f"{'='*60}")
|
||
|
||
patched = 0
|
||
errors = 0
|
||
|
||
def apply_patch(kind, w, target_ns, is_middleware_check=False):
|
||
nonlocal patched, errors
|
||
name = w["metadata"]["name"]
|
||
replicas = w["spec"].get("replicas", 1)
|
||
current_ns = w["spec"]["template"]["spec"].get("nodeSelector", {})
|
||
|
||
if replicas == 0:
|
||
return # 忽略副本数为 0 的
|
||
|
||
if is_middleware_check:
|
||
volumes = w["spec"]["template"]["spec"].get("volumes", [])
|
||
has_host_path = any("hostPath" in v for v in volumes)
|
||
if has_host_path:
|
||
print(f" [跳过] {kind}/{name} 包含 hostPath 挂载,不能进行迁移")
|
||
return
|
||
|
||
# 检查是否已包含所有的目标 selector
|
||
if all(current_ns.get(k) == v for k, v in target_ns.items()):
|
||
print(f" [跳过] {kind}/{name} 已满足 nodeSelector 目标")
|
||
return
|
||
|
||
print(f" [调度] {kind}/{name}")
|
||
print(f" 当前 nodeSelector: {current_ns}")
|
||
print(f" 目标 nodeSelector: {target_ns}")
|
||
|
||
if dry_run:
|
||
print(f" [DRY-RUN] 跳过")
|
||
return
|
||
|
||
merged_selector = {**current_ns, **target_ns}
|
||
patch = {"spec": {"template": {"spec": {"nodeSelector": merged_selector}}}}
|
||
result = run_kubectl([
|
||
"patch", kind, name, "-n", NAMESPACE, "--type", "strategic", "-p", json.dumps(patch)
|
||
])
|
||
|
||
if result is not None:
|
||
print(f" [成功] 已 patch")
|
||
patched += 1
|
||
else:
|
||
print(f" [失败]")
|
||
errors += 1
|
||
|
||
# 处理中间件
|
||
sts_workloads = get_workloads("statefulsets")
|
||
for w in [w for w in sts_workloads if is_middleware(w["metadata"]["name"])]:
|
||
apply_patch("statefulset", w, TARGET_NODE_SELECTOR, True)
|
||
|
||
# 处理业务
|
||
dep_workloads = get_workloads("deployments")
|
||
for w in dep_workloads:
|
||
apply_patch("deployment", w, BUSINESS_NODE_SELECTOR, False)
|
||
|
||
print(f"\n 汇总: 已调度 {patched} 个, 失败 {errors} 个")
|
||
|
||
|
||
def verify_migration():
|
||
"""验证迁移及调度结果"""
|
||
print(f"\n{'='*60}")
|
||
print(f" 调度结果验证")
|
||
print(f"{'='*60}")
|
||
|
||
issues = []
|
||
|
||
def check_workloads(kind, workloads, target_label_key, target_nodes_list, is_middleware_check=False):
|
||
for w in workloads:
|
||
name = w["metadata"]["name"]
|
||
replicas = w["spec"].get("replicas", 1)
|
||
if replicas == 0:
|
||
continue
|
||
|
||
ns = w["spec"]["template"]["spec"].get("nodeSelector", {})
|
||
if is_middleware_check:
|
||
volumes = w["spec"]["template"]["spec"].get("volumes", [])
|
||
if any("hostPath" in v for v in volumes):
|
||
continue
|
||
|
||
if ns.get(target_label_key) != "true":
|
||
issues.append(f" ⚠️ {kind}/{name} 未设置 {target_label_key} nodeSelector")
|
||
|
||
pods_output = run_kubectl(["get", "pods", "-n", NAMESPACE, "-l", f"app={name}", "-o", "json"])
|
||
if not pods_output or json.loads(pods_output).get("items", []) == []:
|
||
pods_output = run_kubectl(["get", "pods", "-n", NAMESPACE, "-o", "json"])
|
||
if pods_output:
|
||
all_pods = [p for p in json.loads(pods_output).get("items", []) if p["metadata"]["name"].startswith(name)]
|
||
else:
|
||
all_pods = []
|
||
else:
|
||
all_pods = json.loads(pods_output).get("items", [])
|
||
|
||
for pod in all_pods:
|
||
pod_name = pod["metadata"]["name"]
|
||
node = pod["spec"].get("nodeName", "unknown")
|
||
phase = pod["status"].get("phase", "Unknown")
|
||
|
||
# Check target nodes
|
||
if target_nodes_list is not None and node not in target_nodes_list:
|
||
issues.append(f" ⚠️ Pod {pod_name} 调度异常,当前节点 {node},应在目标节点集合 {target_nodes_list} 中")
|
||
# Exclude nodes
|
||
elif target_nodes_list is None and node in EVICT_NODES:
|
||
issues.append(f" ⚠️ Pod {pod_name} 仍在待清退节点 {node} 上")
|
||
else:
|
||
print(f" ✅ {pod_name} -> 节点 {node} (状态: {phase})")
|
||
|
||
# 1. 中间件需在保留节点上(非待清退节点)
|
||
sts_workloads = get_workloads("statefulsets")
|
||
mw_list = [w for w in sts_workloads if is_middleware(w["metadata"]["name"])]
|
||
check_workloads("statefulset", mw_list, "jxyd-middleware", None, True)
|
||
|
||
# 2. 业务需在待清退节点上
|
||
dep_workloads = get_workloads("deployments")
|
||
check_workloads("deployment", dep_workloads, "jxyd-business", EVICT_NODES, False)
|
||
|
||
if issues:
|
||
print(f"\n ⚠️ 发现 {len(issues)} 个问题:")
|
||
for issue in issues:
|
||
print(issue)
|
||
else:
|
||
print(f"\n ✅ 所有的工作负载已成功调度到对应的目标节点!")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Phase 3: 工作负载迁移与调度")
|
||
group = parser.add_mutually_exclusive_group(required=True)
|
||
group.add_argument("--list", action="store_true", help="列出迁移计划")
|
||
group.add_argument("--dry-run", action="store_true", help="预览变更")
|
||
group.add_argument("--apply", action="store_true", help="执行迁移")
|
||
group.add_argument("--verify", action="store_true", help="验证迁移结果")
|
||
args = parser.parse_args()
|
||
|
||
if args.list:
|
||
list_workloads()
|
||
elif args.dry_run:
|
||
migrate_workloads(dry_run=True)
|
||
elif args.apply:
|
||
confirm = input(" ⚠️ 确认要执行迁移和调度吗?(yes/no): ")
|
||
if confirm.lower() == "yes":
|
||
migrate_workloads(dry_run=False)
|
||
else:
|
||
print(" 已取消。")
|
||
elif args.verify:
|
||
verify_migration()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
---
|
||
|
||
## 六、Phase 4:节点清退(一个月后执行)
|
||
|
||
### 4.1 清退脚本 `phase4_evict_nodes.sh`
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# ============================================================
|
||
# Phase 4: 节点清退(一个月后执行)
|
||
#
|
||
# ⚠️ 此脚本将执行不可逆操作,请确认:
|
||
# 1. Phase 2 资源调整已完成
|
||
# 2. Phase 3 中间件迁移已完成并验证
|
||
# 3. 已获得相关审批
|
||
# ============================================================
|
||
|
||
set -e
|
||
|
||
NAMESPACE="jxyd"
|
||
EVICT_NODES=("10.20.1.142" "10.20.1.144" "10.20.1.145")
|
||
LOG_DIR="/root/wdd/backup_260617/phase4_$(date +%Y%m%d_%H%M%S)"
|
||
mkdir -p "${LOG_DIR}"
|
||
|
||
echo "=========================================="
|
||
echo " Phase 4: 节点清退"
|
||
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo " 日志: ${LOG_DIR}"
|
||
echo "=========================================="
|
||
|
||
# ============================================================
|
||
# Step 1: 最终确认
|
||
# ============================================================
|
||
echo ""
|
||
echo "⚠️ 即将执行以下操作:"
|
||
echo " 1. 删除 ${NAMESPACE} 命名空间中的业务 Deployment"
|
||
echo " 2. Cordon 节点: ${EVICT_NODES[*]}"
|
||
echo " 3. Drain 节点: ${EVICT_NODES[*]}"
|
||
echo " 4. 从集群中删除节点: ${EVICT_NODES[*]}"
|
||
echo ""
|
||
read -p "确认执行?(输入 YES 继续): " CONFIRM
|
||
if [ "${CONFIRM}" != "YES" ]; then
|
||
echo "已取消。"
|
||
exit 0
|
||
fi
|
||
|
||
# ============================================================
|
||
# Step 2: 最终备份
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 2: 最终备份 ==="
|
||
kubectl get all -n ${NAMESPACE} -o yaml > "${LOG_DIR}/final_backup_all.yaml"
|
||
kubectl get pods -n ${NAMESPACE} -o wide > "${LOG_DIR}/final_pod_distribution.txt"
|
||
echo " 备份完成"
|
||
|
||
# ============================================================
|
||
# Step 3: 删除 jxyd 业务 Deployment
|
||
# (说明:中间件仅存在于 StatefulSet,因此所有的 Deployment 均为业务)
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 3: 删除 jxyd 业务 Deployment ==="
|
||
|
||
# 获取所有 deployment
|
||
ALL_DEPS=$(kubectl get deployments -n ${NAMESPACE} -o jsonpath='{.items[*].metadata.name}')
|
||
|
||
for dep in ${ALL_DEPS}; do
|
||
echo " [删除-业务] ${dep}"
|
||
kubectl delete deployment "${dep}" -n ${NAMESPACE} --grace-period=30 2>&1 | tee -a "${LOG_DIR}/delete_deployments.log"
|
||
done
|
||
|
||
# ============================================================
|
||
# Step 4: Cordon 节点(标记不可调度)
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 4: Cordon 节点 ==="
|
||
for NODE in "${EVICT_NODES[@]}"; do
|
||
echo " Cordon ${NODE}..."
|
||
kubectl cordon "${NODE}" 2>&1 | tee -a "${LOG_DIR}/cordon.log"
|
||
done
|
||
|
||
echo ""
|
||
kubectl get nodes -o wide | tee "${LOG_DIR}/nodes_after_cordon.txt"
|
||
|
||
# ============================================================
|
||
# Step 5: Drain 节点(驱逐所有 Pod)
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 5: Drain 节点 ==="
|
||
for NODE in "${EVICT_NODES[@]}"; do
|
||
echo " Drain ${NODE}..."
|
||
kubectl drain "${NODE}" \
|
||
--ignore-daemonsets \
|
||
--delete-emptydir-data \
|
||
--force \
|
||
--grace-period=60 \
|
||
--timeout=300s \
|
||
2>&1 | tee -a "${LOG_DIR}/drain_${NODE}.log"
|
||
|
||
echo " 等待 30 秒让 Pod 完成迁移..."
|
||
sleep 30
|
||
done
|
||
|
||
# ============================================================
|
||
# Step 6: 验证 Pod 已全部迁走
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 6: 验证 Pod 迁移情况 ==="
|
||
for NODE in "${EVICT_NODES[@]}"; do
|
||
REMAINING=$(kubectl get pods --all-namespaces --field-selector spec.nodeName=${NODE} --no-headers 2>/dev/null | grep -v "kube-system" | wc -l)
|
||
if [ "${REMAINING}" -gt 0 ]; then
|
||
echo " ⚠️ 节点 ${NODE} 上仍有 ${REMAINING} 个非系统 Pod:"
|
||
kubectl get pods --all-namespaces --field-selector spec.nodeName=${NODE} -o wide | grep -v "kube-system"
|
||
else
|
||
echo " ✅ 节点 ${NODE} 上的用户 Pod 已全部迁走"
|
||
fi
|
||
done
|
||
|
||
# ============================================================
|
||
# Step 7: 从集群中删除节点
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 7: 从集群中删除节点 ==="
|
||
read -p "确认从集群中删除节点?(输入 YES 继续): " CONFIRM2
|
||
if [ "${CONFIRM2}" == "YES" ]; then
|
||
for NODE in "${EVICT_NODES[@]}"; do
|
||
echo " 删除节点 ${NODE}..."
|
||
kubectl delete node "${NODE}" 2>&1 | tee -a "${LOG_DIR}/delete_nodes.log"
|
||
done
|
||
else
|
||
echo " 跳过节点删除。可稍后手动执行: kubectl delete node <node>"
|
||
fi
|
||
|
||
# ============================================================
|
||
# Step 8: 最终验证
|
||
# ============================================================
|
||
echo ""
|
||
echo "=== Step 8: 最终验证 ==="
|
||
echo "--- 集群节点状态 ---"
|
||
kubectl get nodes -o wide | tee "${LOG_DIR}/nodes_final.txt"
|
||
echo ""
|
||
echo "--- jxyd 命名空间 Pod 状态 ---"
|
||
kubectl get pods -n ${NAMESPACE} -o wide | tee "${LOG_DIR}/pods_final.txt"
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
echo " Phase 4 完成!"
|
||
echo " 日志目录: ${LOG_DIR}"
|
||
echo "=========================================="
|
||
```
|
||
|
||
---
|
||
|
||
## 七、快速命令参考
|
||
|
||
### 如果需要手动单独操作
|
||
|
||
```bash
|
||
# ============================================================
|
||
# 常用单条命令(按需执行)
|
||
# ============================================================
|
||
|
||
# 1. 查看 jxyd 下所有 deployment 的资源配置
|
||
kubectl get deploy -n jxyd -o custom-columns=\
|
||
NAME:.metadata.name,\
|
||
CPU_LIMIT:.spec.template.spec.containers[0].resources.limits.cpu,\
|
||
MEM_LIMIT:.spec.template.spec.containers[0].resources.limits.memory,\
|
||
CPU_REQ:.spec.template.spec.containers[0].resources.requests.cpu,\
|
||
MEM_REQ:.spec.template.spec.containers[0].resources.requests.memory
|
||
|
||
# 2. 查看某个 deployment 的完整资源配置
|
||
kubectl get deploy <NAME> -n jxyd -o jsonpath='{.spec.template.spec.containers[0].resources}' | python3 -m json.tool
|
||
|
||
# 3. 手动 patch 单个 deployment 的资源
|
||
kubectl patch deploy <NAME> -n jxyd --type='strategic' -p '{
|
||
"spec": {
|
||
"template": {
|
||
"spec": {
|
||
"containers": [{
|
||
"name": "<CONTAINER_NAME>",
|
||
"resources": {
|
||
"limits": {"cpu": "2", "memory": "2Gi"},
|
||
"requests": {"cpu": "1", "memory": "500Mi"}
|
||
},
|
||
"env": [{
|
||
"name": "CUST_JAVA_OPTS",
|
||
"value": "-Xms500m -Xmx2000m -Dlog4j2.formatMsgNoLookups=true"
|
||
}]
|
||
}]
|
||
}
|
||
}
|
||
}
|
||
}'
|
||
|
||
# 4. 查看哪些 Pod 在待清退节点上
|
||
for node in 10.20.1.142 10.20.1.144 10.20.1.145; do
|
||
echo "=== $node ==="
|
||
kubectl get pods -n jxyd --field-selector spec.nodeName=$node -o wide
|
||
done
|
||
|
||
# 5. 单独给 StatefulSet 设置 nodeSelector
|
||
kubectl patch statefulset <NAME> -n jxyd --type='strategic' -p '{"spec":{"template":{"spec":{"nodeSelector":{"jxyd-middleware":"true"}}}}}'
|
||
|
||
# 5.1 单独给业务 Deployment 设置 nodeSelector
|
||
kubectl patch deploy <NAME> -n jxyd --type='strategic' -p '{"spec":{"template":{"spec":{"nodeSelector":{"jxyd-business":"true"}}}}}'
|
||
|
||
# 6. 快速 cordon(标记不可调度)
|
||
kubectl cordon 10.20.1.142
|
||
kubectl cordon 10.20.1.144
|
||
kubectl cordon 10.20.1.145
|
||
|
||
# 7. 快速 drain(驱逐 + 清空)
|
||
kubectl drain 10.20.1.142 --ignore-daemonsets --delete-emptydir-data --force --grace-period=60
|
||
kubectl drain 10.20.1.144 --ignore-daemonsets --delete-emptydir-data --force --grace-period=60
|
||
kubectl drain 10.20.1.145 --ignore-daemonsets --delete-emptydir-data --force --grace-period=60
|
||
```
|
||
|
||
---
|
||
|
||
## 八、注意事项与风险提示
|
||
|
||
### ⚠️ 关键风险点
|
||
|
||
| 风险 | 说明 | 应对措施 |
|
||
|------|------|----------|
|
||
| 中间件识别遗漏 | 自动识别依赖关键词,可能漏判 | **先用 `--list` 模式人工审核** |
|
||
| PVC/PV 数据丢失 | StatefulSet 的持久化数据可能绑定在特定节点的 local PV | 提前检查 PV 类型,如为 `local-storage` 需手动迁移数据 |
|
||
| 资源降配影响服务 | CPU/内存缩减可能导致 OOM 或性能下降 | 降配后监控1-2天,关注 Pod 重启情况 |
|
||
| Strategic Merge Patch 覆盖 env | patch containers 时注意 env 的合并策略 | 脚本已处理,但建议 patch 后验证 |
|
||
| Drain 超时 | 存在强制终止 Pod 的风险 | 已设置 `grace-period=60` 和 `timeout=300s` |
|
||
|
||
### ✅ 执行顺序清单
|
||
|
||
```
|
||
□ Phase 1: 执行备份脚本,确认备份完整
|
||
□ Phase 2: 先 --dry-run 预览资源调整
|
||
□ Phase 2: --apply 执行资源调整(仅针对 Deployment 业务)
|
||
□ Phase 2: 监控1-2天确认服务稳定
|
||
□ Phase 3: 给保留节点打 label
|
||
□ Phase 3: 先 --list 查看迁移和调度计划(并确认 hostPath)
|
||
□ Phase 3: 人工确认中间件识别结果,如有遗漏则修改 MIDDLEWARE_KEYWORDS
|
||
□ Phase 3: --dry-run 预览迁移与调度
|
||
□ Phase 3: --apply 执行迁移与调度
|
||
□ Phase 3: --verify 验证迁移与调度结果
|
||
□ Phase 3: 监控中间件服务是否正常
|
||
□ ---- 等待一个月 ----
|
||
□ Phase 4: 执行最终清退脚本
|
||
□ Phase 4: 验证集群状态正常
|
||
```
|