更新时间:2025-11-26 15:02:37

重要提示

注意:本操作需要停机执行。停机时间主要取决于以下因素:

  • 各组件的数据量大小
  • 磁盘性能
  • 网络性能

1. 适用范围

proton 2.16.0 及以上版本

2. 迁移方案概述

  1. 在待添加节点安装同版本依赖包;
  2. 添加节点到集群;
  3. 卸载基础组件;
  4. 数据目录迁移;
  5. 删除待删除节点并重新安装组件。

3. 技术原理

3.1 基础架构说明

  • Proton 基础组件采用 StatefulSet 类型部署
  • 使用 PVC 挂载主机上的数据目录
  • 每个组件在单个节点上可启动 0 或 1 个副本
  • 每个组件最多支持 3 个副本
  • 默认部署在原来的 master 节点

3.2 数据目录切换机制

  • 在服务停止时将数据目录迁移至新位置
  • 服务启动时自动读取新的数据目录
  • 通过更新 data_path 配置完成切换

3.3 StatefulSet 特性说明

  1. 有序性

    • 组件按固定顺序启动和停止
    • 组件集群的组成依赖于启动顺序
    • 数据目录变更时必须确保节点映射关系明确
  2. 配置限制

    • StatefulSet 不支持直接修改某些字段
    • 数据目录变更需通过删除后重新安装实现

3.4 数据迁移工具

  • 主要工具rsync

    • 保留文件权限和属主信息
    • 支持全量及增量传输
    • 支持跨节点拷贝
    • 支持传输速度限制
    • 支持数据校验
  • 备用方案tar + scp

    • rsync 不可用时使用
    • 流程:打包 -> 传输 -> 解包

3.5 组件管理方式

  1. Operator 管理组件

    • nebula
    • proton_mongodb
    • proton_mariadb

    这些组件以 CR (Custom Resource) 形式存在,停止服务时需删除对应的 CR。
    同时,由于删除 CR 时,pv/pvc 不会自动删除,因此需要手动删除,以避免老 pv 绑定待删除的节点,导致最终对应的组件无法正常启动。

  2. Helm 管理组件

    • 默认位于 resource 命名空间
    • 通过删除 Helm release 停止服务
  3. 应用服务

    • 部署在特定命名空间中
    • 默认命名空间为空间为 anyshare

基于此,在停止组件服务之前,需先停止 anyshare namespace 下的 deployment、daemonset 和 statefulset,以避免数据一致性问题。


4. 准备工作

停机时间外操作,不计入总停机时间。

  • 将当前集群使用的对应版本的依赖包上传至任意一个 Master 节点,并解压备用。
  • 上传所有使用到的命令和脚本(包括如下app_manager.sh和其他使用到的命令)。
  • 在待添加的节点,均安装好依赖包。

依赖包远程分发安装和后续的步骤执行均依赖待添加节点和当前既有节点所有密码一致,如不一致则需要手动配置 root 用户的密钥互信

#!/bin/bash
# filename: app_manager.sh
# description: app_manager.sh is used to stop and start all applications in the cluster.
# author: proton
# date: 2025-09-25
set -e

# record file
record_file="/opt/app.replicas"
# timeout seconds
timeout_seconds=600
# check interval
check_interval=5

# 根据 deploy-service 获取应用服务所在的namespace
#ns=$(kubectl get deploy -A -o json | yq e '.items[] | select(.metadata.name == "deploy-service") | .metadata.namespace' -)
ns=$(kubectl get deploy -A -ojsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.namespace}{"\n"}{end}'|awk -F "\t" '$1=="deploy-service"{print $2; exit}');
[ -z "${ns}" ] && ns='anyshare'

# 检查应用服务是否处于期望的状态
check_workloads_ready() {
    local component_ns="$1";
    local desired="$2";
    if [ "${desired}" == "stop" ]; then
        running_pod_count=$(kubectl get pods -n ${component_ns} -ojsonpath='{range .items[*]}{.status.phase}{"\n"}{end}'|awk 'BEGIN{count=0}{if ($1!="Succeeded") count++}END{print count}');
        if [[ "${running_pod_count}" -eq 0 ]]; then
            echo "all pods in namespace ${component_ns} are stopped!"
            return 0
        fi
        return 1
    fi
    if [ "${desired}" == "start" ]; then
        # get unready deploy&sts&ds
        unready_deploys=$(kubectl get deployments -n "${component_ns}" -o custom-columns=REPLICAS:.spec.replicas,READY_REPLICAS:.status.readyReplicas \
            | awk 'BEGIN{count=0}NR>1 {if ($1 != $2) count++} END{print count}');
            
        unready_sts=$(kubectl get statefulsets -n "${component_ns}" -o custom-columns=REPLICAS:.spec.replicas,READY_REPLICAS:.status.readyReplicas \
            | awk 'BEGIN{count=0}NR>1 {if ($1 != $2) count++} END{print count}');

        unready_ds=$(kubectl get daemonsets -n "${component_ns}" -o custom-columns=DESIRED:.status.desiredNumberScheduled,READY:.status.numberReady \
            | awk 'BEGIN{count=0}NR>1 {if ($1 != $2) count++} END{print count}');

        # check if all workloads are ready
        if [[ "${unready_deploys}" -eq 0 && "${unready_sts}" -eq 0 && "${unready_ds}" -eq 0 ]]; then
            echo "all deploy&ds&sts in namespace ${component_ns} are ready!"
            return 0
        fi
        return 1
    fi
    return 233
}

# 等待应用服务达到期望状态
wait_for_workloads() {
    local component_ns="$1";
    local desired="$2";
    local elapsed=0

    echo -n "Waiting for all deploy/sts/ds in namespace ${component_ns} to be ${desired}..."
    while [ $elapsed -lt $timeout_seconds ]; do
        if check_workloads_ready "$component_ns" "$desired"; then
            echo " done"
            return 0
        fi
        sleep $check_interval
        elapsed=$((elapsed + check_interval))
        echo -n "."
    done                    
    echo " timeout reached!"
    return 1
}

# 停止应用服务
stop_apps() {
    # 仅当记录文件不存在时,才记录当前应用服务的副本信息
    if [ ! -f "${record_file}" ]; then
        touch "${record_file}"
        kubectl get deploy -n${ns} -o jsonpath='{range .items[*]}{"deployment"}{"\t"}{.metadata.name}{"\t"}{.spec.replicas}{"\n"}{end}' > "${record_file}"
        kubectl get sts -n${ns} -o jsonpath='{range .items[*]}{"statefulset"}{"\t"}{.metadata.name}{"\t"}{.spec.replicas}{"\n"}{end}' >> "${record_file}"
    fi

    # 通过打一个不存在的label实现停止指定namespace下所有的daemonset
    for ds in $(kubectl get ds -n${ns} -ojsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
        echo "Stopping daemonset ${ds}..."
        kubectl -n${ns} patch ds ${ds} -p '{"spec":{"template":{"spec":{"nodeSelector":{"stop-by-admin":"true"}}}}}'
    done

    # 通过scale=0停止所有deploy和sts
    while read -r app_type app_name app_replicas _; do
        if [ "$app_replicas" -gt 0 ]; then
            echo "Stopping ${app_type} ${app_name}..."
            kubectl -n${ns} scale ${app_type} ${app_name} --replicas=0
        fi
    done < "${record_file}"

    # 等待所有应用服务的pod停止
    wait_for_workloads "${ns}" "stop" || true
}

# 启动应用服务
start_apps() {
    # 删除label来启动daemonset
    for ds in $(kubectl get ds -n${ns} -ojsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do
        echo "Starting daemonset ${ds}..."
        kubectl -n${ns} patch ds ${ds} --type=merge -p '{"spec":{"template":{"spec":{"nodeSelector":{"stop-by-admin":null}}}}}'
    done

    # 按照记录文件内容启动所有deploy和sts
    while read -r app_type app_name app_replicas _; do
        if [ "$app_replicas" -gt 0 ]; then
            echo "Starting ${app_type} ${app_name} with ${app_replicas} replicas..."
            kubectl -n${ns} scale ${app_type} ${app_name} --replicas=${app_replicas}
        fi
    done < "${record_file}"

    # 等待所有应用服务的pod启动
    if wait_for_workloads "${ns}" "start"; then
        echo "All applications started successfully"
        rm -f "${record_file}"
    else
        echo "Warning: Some applications failed to start properly in time, manual check is needed"
        exit 1
    fi
}

[ -z "$1" ] && { echo "Usage: $0 {stop|start}" >&2; exit 233; }

case $1 in
    stop)
        stop_apps
        ;;
    start)
        start_apps
        ;;
    *)
        echo "Usage: $0 {stop|start}" >&2
        exit 233
        ;;
esac

5. 镜像预热(可选)

停机时间外操作,不计入总停机时间。

在每个节点上,提前导入所有基础组件和业务服务的镜像到本地,以确保在后续启动时不再需要拉取镜像,从而加速组件服务的启动过程。


6. 获取 Secret

停机时间外操作,不计入总停机时间。

此步骤用于备份 persist-component-manage-componentsproton-cli-config 两个 Secret,并将其内容导出至同名目录的子项文件中(已解码)。

执行以下命令导出当前的 Secret:

# 创建更新 Secret 的子目录
rm -rf persist-component-manage-components proton-cli-config
mkdir -p persist-component-manage-components proton-cli-config

# 获取命名空间
ns_1=$(kubectl get secret -A -ojsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.namespace}{"\n"}{end}'|awk -F "\t" '$1=="persist-component-manage-components"{print $2; exit}')
ns_2=$(kubectl get secret -A -ojsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.namespace}{"\n"}{end}'|awk -F "\t" '$1=="proton-cli-config"{print $2; exit}')

# 备份原有 Secret
kubectl -n${ns_1} get secret persist-component-manage-components -o yaml > persist-component-manage-components.yaml
kubectl -n${ns_2} get secret proton-cli-config -o yaml > proton-cli-config.yaml

# 获取当前配置并解码为明文
for item in $(yq '.data | keys | .[]' persist-component-manage-components.yaml); do
    yq ".data[\"$item\"]" persist-component-manage-components.yaml | base64 -d > "persist-component-manage-components/${item}"
done

for item in $(yq '.data | keys | .[]' proton-cli-config.yaml); do
    yq ".data[\"$item\"]" proton-cli-config.yaml | base64 -d > "proton-cli-config/${item}"
done

7. 在线全量拷贝数据目录

停机时间外操作,不计入总停机时间。

7.1 规划

  • 规划变更节点的对应关系,这个决定了每个组件的数据目录从哪个节点拷贝到哪个节点,需要规划并记录清楚
  • 每个组件的数据目录位置和定义,参考《proton组件切换数据目录》内相关说明

7.2 拷贝

  • 容器组件数据目录拷贝
    如果数据量较大,数据目录可以提前进行拷贝,使用如下 rsync 命令在待迁移组件的数据源节点进行数据目录内容拷贝。注意:
  1. 复制时需确保包含最后一级目录本体,保留文件权限和属主属组信息;
  2. 要依据步骤4里面记录的组件副本和节点、数据目录的映射关系,一一进行数据目录的复制,不要错乱;
  3. 为减少对于业务的影响,建议在业务低峰期进行数据目录的拷贝,同时应该限制拷贝速度,减少对业务造成影响;
  4. 拷贝的目标目录不应为非空状态,否则会覆盖原有数据!
  5. 注意空间是否充足,避免拷贝失败。

--bwlimit单位固定为KB/s,应按照实际情况指定合适的数值,减少或者避免对业务的影响

rsync -avz --bwlimit=5000 --progress <old_data_path> root@1.2.3.4:<new_data_path>
  • 主机服务数据目录拷贝
    数据目录应当提前进行拷贝,使用如下 rsync 命令进行数据目录内容拷贝。注意:
  1. 复制时需确保包含最后一级目录本体,保留文件权限和属主属组信息;
  2. 拷贝的目标目录不应为非空状态,否则会覆盖原有数据!
  3. 注意空间是否充足,避免拷贝失败。
  4. 不可变化主机服务前后节点,仅支持在同节点上进行数据目录的切换。

--bwlimit单位固定为KB/s,应按照实际情况指定合适的数值,减少或者避免对业务的影响

rsync -avz --bwlimit=5000 --progress <old_data_path> <new_data_path>

8. 停机

预计停机时间:<3分钟

大部分应用未配置 graceful shutdown,单个 Pod 会等待默认的 terminationGracePeriodSeconds=30s,并最终被 SIGKILL

执行以下命令,将所有应用服务停止:

bash app_manage.sh stop

然后,使用以下命令检查所有 Pod 状态,确保没有非 Completed 状态的 Pod,规避数据一致性风险:

如果进行了应用服务namespace的自定义,应使用自定义的namespace

kubectl -n anyshare get po

9. 添加节点

预计停机时间:<5分钟

  • 切换到依赖包解压目录下,执行如下命令,获取当前的集群完整配置文件
proton-cli get conf > middle.yaml
  • 手动编辑配置文件,进行节点添加动作

仅需要在middle.yaml文件中的.nodes下和.cs.master进行参考原有信息进行节点信息添加,其他内容无需修改,注意节点名称的唯一性

vi middle.yaml
  • 执行如下命令,更新集群配置

依赖所有节点的ssh密码一致,如果无法保持一致,须提前自行配置密钥互信,然后不传入-p <YOUR_SSH_PASSWORD>执行

proton-cli apply -f middle.yaml -p <YOUR_SSH_PASSWORD>

10. 更新 Secret

预计停机时间:<1分钟

内容:

使用修改后的 Secret 信息替换集群中原有的 Secret。

10.1 修改导出的 Secret

在之前的步骤中,系统会创建两个目录:persist-component-manage-componentsproton-cli-config。使用 vi 或其他编辑工具修改这两个目录下的文件,一一对应更新每一个组件的使用的节点为新的节点。需要特别注意的是:

  • 应先备份源文件再进行操作。
  • 对于每个组件副本变更前后的节点和数据目录信息,都应进行详细记录,确保组件副本编号和节点一致。

10.2 更新 Secret

  • 执行以下命令更新 Secret:
# 获取命名空间
ns_1=$(kubectl get secret -A -ojsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.namespace}{"\n"}{end}'|awk -F "\t" '$1=="persist-component-manage-components"{print $2; exit}')
ns_2=$(kubectl get secret -A -ojsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.namespace}{"\n"}{end}'|awk -F "\t" '$1=="proton-cli-config"{print $2; exit}')

# 更新 Secret
kubectl -n${ns_1} create secret generic persist-component-manage-components --from-file=persist-component-manage-components/ -o yaml --dry-run=client | kubectl -n${ns_1} apply -f -
kubectl -n${ns_2} create secret generic proton-cli-config --from-file=proton-cli-config/ -o yaml --dry-run=client | kubectl -n${ns_2} apply -f -

11. 卸载组件

预计停机时间:<3分钟

内容:

  • 卸载所有的基础组件。可以一次指定多个 Helm Release 名称进行卸载:

如果进行了资源组件的namespace的自定义,应使用自定义的namespace

helm3 -n resource delete kafka opensearch-master prometheus proton-etcd proton-mq-nsq proton-policy-engine proton-redis zookeeper grafana;
  • nebulamariadbmongodb 需要通过删除相应的 CR 实现组件卸载,并同步删除 pv/pvc:

如果进行了资源组件的namespace的自定义,应使用自定义的namespace

Nebula

kubectl delete nebulaclusters.app.nebula-graph.io nebula -n resource;
# sleep for a while to wait for the deletion complete
sleep 33;
kubectl get pvc -A|grep resource|awk '{print $2}'|xargs -I {} kubectl delete pvc -lapp=nebula -n {};
kubectl get pv -A|grep resource|awk '{print $2}'|xargs -I {} kubectl delete pv -lapp=nebula;

MariaDB

kubectl delete rdsmariadbclusters.rds.proton.aishu.cn mariadb -n resource;
# sleep for a while to wait for the deletion complete
sleep 33;
kubectl delete pvc -lapp=app=mariadb-mariadb -n resource;
kubectl delete pv -lapp=app=mariadb-mariadb;

MongoDB

kubectl delete mongodboperators.mongodb.proton.aishu.cn mongodb -n resource;
# sleep for a while to wait for the deletion complete
sleep 33;
kubectl delete pvc -lapp=mongodb-mongodb -n resource;
kubectl delete pv -lapp=mongodb-mongodb;

12. 停止主机服务

预计停机时间:<3分钟

使用如下命令停止相关的主机服务

systemctl stop docker containerd kubelet proton-cr proton-cr-registry proton-cr-chartmuseum docker.socket;

13. 复制数据目录

预计停机时间:根据数据量、磁盘性能和网络性能而有所不同。

  1. 容器组件数据目录的复制
    使用如下 rsync 命令在待变更节点组件的数据源节点进行数据目录内容拷贝。注意:
  2. 复制时需确保包含最后一级目录本体,保留文件权限和属主属组信息;
  3. 要依据步骤4里面记录的组件副本和节点、数据目录的映射关系,一一进行数据目录的复制,不要错乱;
  4. 为减少对于业务的影响,建议在业务低峰期进行数据目录的拷贝,同时应该限制拷贝速度,减少对业务造成影响;
  5. 拷贝的目标目录不应为非空状态,否则会覆盖原有数据!
rsync -avz --progress <old_data_path> root@1.2.3.4:<new_data_path>
  1. 主机服务数据目录的复制

使用如下 rsync 命令进行数据目录内容拷贝。注意:

  1. 复制时需确保包含最后一级目录本体,保留文件权限和属主属组信息;
  2. 拷贝的目标目录不应为非空状态,否则会覆盖原有数据!
  3. 注意空间是否充足,避免拷贝失败。
  4. 不应变化数据目录,因为本次仅进行每个组件的节点切换。
rsync -avz --progress <old_data_path> <new_data_path>

14. 启动主机服务

预计停机时间:<20分钟
如果不重启物理机的话,预计停机时间:<10分钟

  1. 启动主机服务
    如果是docker作为容器运行使用如下命令启动相关的主机服务
systemctl start docker containerd kubelet proton-cr proton-cr-registry proton-cr-chartmuseum docker.socket;

如果是containerd作为容器运行时,为确保显示效果,应直接重启节点的方式启动服务

reboot
  1. 等待 proton-cs 集群恢复正常
    执行如下命令检查 proton-cs 集群状态,确认所有pod都处于completed状态或者running+ready状态

如果进行了资源组件的namespace的自定义,应使用自定义的namespace

kubectl get no;
kubectl get po -n kube-system;
kubectl get po -n resource

15. 安装组件&踢出节点

预计停机时间:<5分钟

部分组件的启动时间和数据量相关,极端情况下,可能超过5min,达到10min左右

  • 进入依赖包解压目录,使用以下命令获取当前的配置文件:
proton-cli get conf > current.yaml
  • 修改配置文件,进行节点踢出动作和组件归属节点变更动作
  1. .nodes下删除需要踢出的节点
  2. .cs.master下删除需要踢出的节点
  3. 其他组件下进行节点变更,注意和之前拷贝数据目录时一一对应,不要出错
vi current.yaml
  • 复用数据目录的情况下安装组件:
proton-cli apply -f current.yaml
# sleep for a while to wait for the installation complete
sleep 33;
# rollout restart operators to make sure mariadb master svc is successfully created
kubectl -n proton-rds-mariadb-operator-system rollout restart deploy proton-rds-mariadb-operator-controller-manager

如果删除的节点数量超过一个,那么可能在proton-cli apply -f current.yaml的时候会出现kubesuite命令执行失败的情况,重复执行proton-cli apply -f current.yaml即可。

  • 检查组件的 Pod 是否有异常(如果进行了资源组件的namespace的自定义,应使用自定义的namespace):

如果进行了资源组件的namespace的自定义,应使用自定义的namespace

kubectl -n resource get po

16. 启动应用服务

预计时间:<10分钟

  • 根据记录的副本数信息,恢复应用服务的 Deployment、StatefulSet 和 DaemonSet 的副本数。执行以下命令启动服务:
bash app_manage.sh start
  • 然后使用以下命令检查 Pod 是否已成功启动,确保没有异常 Pod:

如果进行了应用服务namespace的自定义,应使用自定义的namespace

kubectl -n anyshare get po

17. 检查

预计时间:<5分钟

  • 多方检查服务状态,是否全部正常运行。
  • 多方检查数据完整性,是否全部正常。