首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >边缘云K8S离线高可用设计

边缘云K8S离线高可用设计

原创
作者头像
老周聊架构
发布2025-12-05 21:43:17
发布2025-12-05 21:43:17
1620
举报
文章被收录于专栏:架构之路架构之路

一、背景

我们的应用部署在边缘云环境中,面临两个关键挑战:一是场地断网时,Apollo配置无法加载,导致K8S启动失败;二是K8S集群的etcd服务状态不一致时,同样会阻碍K8S正常启动。为保障应用高可用性,我们计划实施双保险策略:一方面,设计Apollo的离线配置方案,确保断网时仍能获取必要配置;另一方面,支持应用在物理机上的直接启动,作为备用方案。

二、业务场景

2024年9月,第11号台风“摩羯”以最强强度登陆海南,引发严重破坏,导致大面积停水停电。受此影响,当地三个中转场因断网断电而无法正常运转,造成批量快件中转延误。具体状况如下:

  • ‌断网断电导致中转延误‌:现场网络和电力中断,三个中转场被迫停止运作,快件中转流程严重受阻。
  • 网络恢复后应用启动失败‌:尽管网络逐步恢复,自动化分拣设备却无法启动,每日超过100万件的分拣量远超人工处理能力,导致分拣工作陷入停滞。
  • 研发现场紧急支援‌:为应对危机,研发团队迅速启动临时方案,通过拉起K8S应用提供支持。研发人员亲临现场,协助自动化设备重新运转,全力保障快件时效性,有效缓解了延误问题。

这里我大概说下我们整体的架构,上层是总部数据中心,你可以理解是公司的私有云或者公有云(类似腾讯云),下层是边缘云,那边缘云是啥呢?(边缘云将计算能力下沉到网络边缘(如工厂、仓库),就近处理数据,减少传输延迟并提升效率‌,避免完全依赖中心云‌。)

有些模块我就不重点介绍了,这里说下,边缘云应用K8S是需要依赖总部的Apollo配置中心的数据的,如果地区网络故障,中心云端的配置就拉取不到边缘云的应用K8S,应用K8S启动失败,进而边缘云的微服务都提供不了服务,更别说那条数据上传到总部中心云的链路了。

三、高可用方案

3.1 Apollo离线配置预加载到本地

3.1.1 方案描述

  • 研发 riemann-common-apollo starter 组件,其核心流程是:在应用启动时,组件会首先尝试从总部的 Apollo 配置中心拉取配置;在成功获取配置后,除了正常使用,该组件还会将这份配置文件的内容完整地保存到本地的 MongoDB 数据库(场地边缘云数据库)中,实现配置数据的持久化备份。
  • 为了实现高可用性,我们改进了配置加载机制。当应用重启时,组件会优先尝试从总部 Apollo 配置中心加载最新配置。如果因网络故障导致无法连接到 Apollo,组件将自动降级,转为从本地 MongoDB 中读取之前备份的配置文件进行加载,从而确保应用在极端情况下仍能正常启动并提供服务。

3.1.2 方案对比选型

3.1.3 方案实现

riemann-common-apollo starter 组件SPI插件化,核心代码片段:

代码语言:java
复制
public class CustomConfigFactory implements ConfigFactory {

    private static final Logger logger = LoggerFactory.getLogger(CustomConfigFactory.class);

    private final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);

    public Config create(String namespace) {

        ConfigFileFormat format = this.determineFileFormat(namespace);
        long start = System.currentTimeMillis();
        ConfigRepository configRepository = ConfigFileFormat.isPropertiesCompatible(format)
            ? this.createPropertiesCompatibleFileConfigRepository(namespace, format)
            : this.createLocalConfigRepository(namespace);
        logger.debug("Create config repository spent {} ms", System.currentTimeMillis() - start);
        return this.createRepositoryConfig(namespace, configRepository);
    }

    protected Config createRepositoryConfig(String namespace, ConfigRepository configRepository) {

        return new DefaultConfig(namespace, configRepository);
    }

    public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) {

        ConfigRepository configRepository = this.createLocalConfigRepository(namespace);
        switch (configFileFormat) {
            case Properties:
                return new PropertiesConfigFile(namespace, configRepository);
            case XML:
                return new XmlConfigFile(namespace, configRepository);
            case JSON:
                return new JsonConfigFile(namespace, configRepository);
            case YAML:
                return new YamlConfigFile(namespace, configRepository);
            case YML:
                return new YmlConfigFile(namespace, configRepository);
            case TXT:
                return new TxtConfigFile(namespace, configRepository);
            default:
                return null;
        }
    }

    ConfigRepository createLocalConfigRepository(String namespace) {

        if (this.configUtil.isInLocalMode()) {
            logger.warn("==== Apollo is in local mode! Won't pull configs from remote server for namespace {} ! ====", namespace);
            return new LocalFileConfigRepository(namespace);
        } else if (ConfigSourceType.MONGO.name().equals(Foundation.server().getEnvType())) {
            logger.warn("==== Apollo is in mongo mode! Won't pull configs from remote server for namespace {} ! ====", namespace);
            return new LocalFileConfigRepository(namespace, new MongoConfigRepository(namespace, this.createRemoteConfigRepository(namespace)));
        } else {
            return new LocalFileConfigRepository(namespace, this.createRemoteConfigRepository(namespace));
        }
    }

    RemoteConfigRepository createRemoteConfigRepository(String namespace) {

        return new RemoteConfigRepository(namespace);
    }

    PropertiesCompatibleFileConfigRepository createPropertiesCompatibleFileConfigRepository(String namespace, ConfigFileFormat format) {

        String actualNamespaceName = this.trimNamespaceFormat(namespace, format);
        PropertiesCompatibleConfigFile configFile = (PropertiesCompatibleConfigFile)ConfigService.getConfigFile(actualNamespaceName, format);
        return new PropertiesCompatibleFileConfigRepository(configFile);
    }

    ConfigFileFormat determineFileFormat(String namespaceName) {

        String lowerCase = namespaceName.toLowerCase();
        ConfigFileFormat[] formats = ConfigFileFormat.values();
        for (ConfigFileFormat format : formats) {
            if (lowerCase.endsWith("." + format.getValue())) {
                return format;
            }
        }
        return ConfigFileFormat.Properties;
    }

    String trimNamespaceFormat(String namespaceName, ConfigFileFormat format) {

        String extension = "." + format.getValue();
        return !namespaceName.toLowerCase().endsWith(extension) ? namespaceName
            : namespaceName.substring(0, namespaceName.length() - extension.length());
    }
}

代码说明:

  • 实现Apollo提供的ConfigFactory接口,提供配置创建功能
  • 支持多种配置文件格式(Properties/XML/JSON/YAML等)
  • 区分本地模式和远程模式,本地模式不从远程服务器拉取配置
  • 通过determineFileFormat方法自动识别文件格式
  • 支持MongoDB作为配置存储源的特殊模式

3.2 物理机启动应用

3.2.1 方案描述

  • 当K8S应用启动后,jar包和相关配置文件,在启动脚本中打离线zip包。
  • 当K8S无法启动时,找到物理机上最近一次启动的K8S目录 ,解压后执行start.sh脚本启动应用。

3.2.2 方案实现

start.sh 启动脚本生成离线包,脚本片段:

代码语言:bash
复制
LIB_DIR=$DEPLOY_DIR/lib
#LIB_JARS=$LIB_DIR/*
LIB_JARS=$LIB_DIR/`ls $LIB_DIR | grep riemann-commons-apollo`:$LIB_DIR/*

if [ -z $JAVA_HOME ]; then
  JAVA=${SERVER_JAVA_HOME}
else
  JAVA=$JAVA_HOME/bin/java
fi
if [ -z "$DEPLOYMENT_NAME" ]; then
  JAVA=/usr/bin/java
fi
nohup $JAVA $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS $JAVA_DUMP_OPTS $APP_ARGS -classpath $CONF_DIR:$LIB_JARS $MAINCLASS >$STDOUT_FILE 2>&1 &

PIDS=`ps -f | grep java | grep -v grep | grep "$DEPLOY_DIR" | awk '{print $2}'`
if [ -n "$PIDS" ]; then
    echo "start success!PID: $PIDS"
else
    echo "fail to start"
fi

if [ -n "$DEPLOYMENT_NAME" ]; then
EMERGENCY_NAME=riemann-emergency-app
mkdir -p /app/deploy/logs/${EMERGENCY_NAME}/
fs=`find /app/deploy -maxdepth 1 -mindepth 1 -print | grep -v logs | grep -v com | grep -v iast`;
for f in $fs;do
    echo "cp -Rf $f /app/deploy/logs/${EMERGENCY_NAME}/";
    cp -Rf $f /app/deploy/logs/${EMERGENCY_NAME}/;
done;
sed -i '4a\RIEMANN_BASE_ZNO='$RIEMANN_BASE_ZNO /app/deploy/logs/${EMERGENCY_NAME}/bin/start.sh
fi
  • ‌环境变量与路径设置‌

设置库文件目录和类路径,特别处理包含"riemann-commons-apollo"的JAR文件

  • ‌Java执行器选择‌

优先使用JAVA_HOME中的Java,其次使用SERVER_JAVA_HOME,最后使用系统默认Java。

  • ‌应用启动‌

使用nohup后台运行Java应用,支持多种Java参数选项(内存、调试、JMX等)。

  • 进程状态检查‌

通过ps命令检查应用是否成功启动,输出启动结果和进程ID

  • ‌应急备份机制‌

当存在DEPLOYMENT_NAME时,创建应急备份;复制部署目录下的相关文件到应急目录;在应急启动脚本中插入环境变量。

四、创新点

4.1 中心云网络与边缘云网络隔离时,边缘云K8S应用可离线启动。

配置中心(如 Apollo、ZK 或 Nacos)通常部署在总部集群,因此极易受到地区网络中断的影响,导致应用启动时出现不可预知的问题。为此,我们的策略是在网络正常时将所有必要的配置文件同步并存储在本地。一旦发生网络异常,系统将立即降级读取本地缓存文件,从而有效隔离网络波动对应用启动的影响,保障应用始终具备高可用性。

4.2 K8S 集群异常兜底,实现物理机应急启动。

在地区网络断网时,本地 K8S 集群本身也可能面临异常,例如 Etcd 节点短期内状态对不齐、无法加载 ConfigMap,以及镜像加载故障等。

考虑到场地资源通常有限,集群往往按最低要求部署在三台物理机上,K8S 故障会造成严重的业务中断。因此,我们设计了一套物理机离线启动的兜底方案,确保在 K8S 集群完全不可用时,应用仍能在任何一台物理机上找到应急机制,实现快速恢复。

4.3 离线启动包的轻量化实现

  • 全集成化: 离线包的生成逻辑已集成在 start.sh 脚本中,无需额外的文件挂载或外部依赖
  • 资源分布: 在每一台物理机上都会生成完整的离线包,确保 K8S 异常时,无论运维人员登录哪一台机器,都能找到。
  • 环境兼容: 应用的启动同样通过统一 start.sh 脚本执行,该脚本内置了环境兼容性处理,使得应用可以无缝地在 K8S 或物理机上启动,保证应急方案的快速和可靠性。

五、价值总结

5.1 实现系统高可用与应急能力提升

尽管极端天气(如台风)或地区专线电缆意外中断不是日常事件,但它们在业务中是不可避免的风险。本方案通过预先建立离线启动和配置的应急预案,将“意外”视为一种常态,确保系统能够有效应对这类“非偶然性意外”。这不仅提升了系统在网络完全隔离时的自愈能力,更保障了核心应用的高可用性,真正做到有备无患。

5.2 减少经济损失并保护客户信任度

在缺乏应急机制的情况下,一旦遇到极端情况,中转环节的大批量延误票件可能高达几十万万以上,随之而来的客诉赔偿将造成巨大的经济损失。同时,频繁的系统故障和延误还会严重损害客户信任度。

本方案通过保障应用在任何环境下的持续运行,直接减少了因系统故障导致的物流中转延误和客诉,从而有效降低了经济损失,并维护了品牌的市场信誉。

5.3 大幅简化操作流程与提升故障时效

方案落地前,断网后的运维操作极为繁琐和低效:

  • 配置导入困难: 配置文件需要人工从总部导出,再携带文件赶往场地,且必须在 K8S 启动并重新初始化环境的过程中导入,操作难度大。
  • 故障定位滞后: K8S 启动异常时,由于总部网络断开,总部同事无法远程分析定位问题,处理难度和时间成本大幅增加。

新方案实现了配置和启动的本地化和自动化,彻底消除了这些人工干预和跨地域依赖,从而显著提升了运维处理能力和故障恢复时效。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、业务场景
  • 三、高可用方案
  • 四、创新点
  • 五、价值总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档