我们的应用部署在边缘云环境中,面临两个关键挑战:一是场地断网时,Apollo配置无法加载,导致K8S启动失败;二是K8S集群的etcd服务状态不一致时,同样会阻碍K8S正常启动。为保障应用高可用性,我们计划实施双保险策略:一方面,设计Apollo的离线配置方案,确保断网时仍能获取必要配置;另一方面,支持应用在物理机上的直接启动,作为备用方案。
2024年9月,第11号台风“摩羯”以最强强度登陆海南,引发严重破坏,导致大面积停水停电。受此影响,当地三个中转场因断网断电而无法正常运转,造成批量快件中转延误。具体状况如下:

这里我大概说下我们整体的架构,上层是总部数据中心,你可以理解是公司的私有云或者公有云(类似腾讯云),下层是边缘云,那边缘云是啥呢?(边缘云将计算能力下沉到网络边缘(如工厂、仓库),就近处理数据,减少传输延迟并提升效率,避免完全依赖中心云。)
有些模块我就不重点介绍了,这里说下,边缘云应用K8S是需要依赖总部的Apollo配置中心的数据的,如果地区网络故障,中心云端的配置就拉取不到边缘云的应用K8S,应用K8S启动失败,进而边缘云的微服务都提供不了服务,更别说那条数据上传到总部中心云的链路了。
3.1 Apollo离线配置预加载到本地
3.1.1 方案描述
3.1.2 方案对比选型

3.1.3 方案实现

riemann-common-apollo starter 组件SPI插件化,核心代码片段:
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());
}
}代码说明:
3.2 物理机启动应用
3.2.1 方案描述
3.2.2 方案实现
start.sh 启动脚本生成离线包,脚本片段:
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_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 离线启动包的轻量化实现
5.1 实现系统高可用与应急能力提升
尽管极端天气(如台风)或地区专线电缆意外中断不是日常事件,但它们在业务中是不可避免的风险。本方案通过预先建立离线启动和配置的应急预案,将“意外”视为一种常态,确保系统能够有效应对这类“非偶然性意外”。这不仅提升了系统在网络完全隔离时的自愈能力,更保障了核心应用的高可用性,真正做到有备无患。
5.2 减少经济损失并保护客户信任度
在缺乏应急机制的情况下,一旦遇到极端情况,中转环节的大批量延误票件可能高达几十万万以上,随之而来的客诉赔偿将造成巨大的经济损失。同时,频繁的系统故障和延误还会严重损害客户信任度。
本方案通过保障应用在任何环境下的持续运行,直接减少了因系统故障导致的物流中转延误和客诉,从而有效降低了经济损失,并维护了品牌的市场信誉。
5.3 大幅简化操作流程与提升故障时效
方案落地前,断网后的运维操作极为繁琐和低效:
新方案实现了配置和启动的本地化和自动化,彻底消除了这些人工干预和跨地域依赖,从而显著提升了运维处理能力和故障恢复时效。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。