基于Spring Boot 2.7与旧版中间件环境的分布式故障演练与健康检查平台架构设计报告
@Entity public class MiddlewareDependency { @Id @GeneratedValue private Long id; private String type; // KAFKA, REDIS, CONSUL private String targetIp; private Integer targetPort; private String version; // e.g., "0.10.2" }
3.2 根权限故障注入引擎(Root Execution Engine) 这是本平台的核心差异化能力。通过在Agent端利用Root权限操作Linux内核网络栈,我们可以模拟应用层无法实现的底层故障。 3.2.1 网络分区模拟:深入iptables机制 为了验证业务组件在网络断开时的表现,我们不能简单地停止中间件服务(因为这会影响其他使用者),而是要实施“点对点”的阻断。 技术选型:OUTPUT链 vs INPUT链
方案A:阻断INPUT链:模拟“服务器收不到中间件的回包”。
方案B:阻断OUTPUT链:模拟“服务器无法向中间件发送请求”。 决策:推荐使用OUTPUT链。对于TCP连接,阻断OUTPUT会导致本地Socket缓冲区填满或SYN包发不出去,这能更准确地测试客户端的连接超时(Connection Timeout)和写入超时(Write Timeout)配置。 命令构建策略: Agent需封装如下逻辑来执行断网:
注入故障(Drop模式): iptables -A OUTPUT -p tcp -d <Middleware_IP> --dport <Middleware_Port> -j DROP -m comment --comment "CHAOS_TEST_ID_101"
注入故障(Reject模式 - 可选): iptables -A OUTPUT -p tcp -d <Middleware_IP> --dport <Middleware_Port> -j REJECT --reject-with tcp-reset -m comment --comment "CHAOS_TEST_ID_101"
故障恢复: 通过--comment标记精确删除规则,避免误删系统其他防火墙规则: iptables -D OUTPUT -p tcp -d <Middleware_IP> --dport <Middleware_Port> -j DROP -m comment --comment "CHAOS_TEST_ID_101"
Java实现细节(ProcessBuilder): 在Java中执行Root命令需要极度谨慎,必须处理标准输出(STDOUT)和标准错误(STDERR)流,否则会导致进程死锁 。 public void executeIptablesRule(String targetIp, int targetPort, boolean enable) { List cmd = new ArrayList<>(); cmd.add("iptables"); cmd.add(enable? "-A" : "-D"); cmd.add("OUTPUT"); cmd.add("-p"); cmd.add("tcp"); cmd.add("-d"); cmd.add(targetIp); cmd.add("--dport"); cmd.add(String.valueOf(targetPort)); cmd.add("-j"); cmd.add("DROP"); cmd.add("-m"); cmd.add("comment"); cmd.add("--comment"); cmd.add("CHAOS_AGENT_Managed");
ProcessBuilder pb = new ProcessBuilder(cmd); try { Process p = pb.start(); // 必须异步消费流,防止缓冲区满导致挂起 consumeStream(p.getInputStream()); consumeStream(p.getErrorStream()); int exitCode = p.waitFor(); if (exitCode!= 0) { throw new RuntimeException("iptables execution failed with code " + exitCode); } } catch (Exception e) { throw new RuntimeException("System command failed", e); }
}
3.2.2 进程级故障:服务重启 针对“业务组件所在的服务器”这一描述,Agent还需具备重启业务进程的能力,以验证其启动时的依赖检查逻辑。
问题:如果在Spring Boot 2.7的Agent中直接引入kafka-clients:0.10.0.0,会因为类路径(Classpath)中存在Spring Boot自带的新版依赖而导致NoSuchMethodError或ClassNotFoundException。
需求:Agent必须能同时运行“现代化的Spring Boot代码”和“旧版的中间件连接代码”。 4.1 Maven Shade Relocation 解决方案 我们不能简单地依赖Spring Boot的依赖管理。必须将用于探测的旧版客户端代码隔离。最成熟的方案是使用Maven Shade Plugin进行包重定位(Relocation)。 4.1.1 实施步骤
创建独立模块:创建一个名为legacy-probe-driver的Maven子模块。
引入旧版依赖:在该模块中引入kafka-clients:0.10.x,jedis:2.9.x等。
配置Shade插件: 在pom.xml中配置Shade插件,将旧版库的包名进行重写。例如,将org.apache.kafka重命名为shaded.legacy.kafka。 org.apache.maven.plugins maven-shade-plugin package shade org.apache.kafka com.myplatform.shaded.kafka010 redis.clients.jedis com.myplatform.shaded.jedis2
Agent集成:主Agent项目依赖这个shaded jar。在代码中,当我们需要创建一个针对旧版Kafka的探测器时,我们使用重定位后的类(或者通过反射调用封装好的探测服务)。
探测方法:使用AdminClient(或旧版的SimpleConsumer)尝试列出Topic列表 。
旧版兼容:对于0.9/0.10等老版本,AdminClient可能不存在或API不同。需封装一个统一接口HealthProbe,针对不同版本提供不同实现(如Kafka010Probe, Kafka2Probe)。
超时设置:必须显式设置极短的request.timeout.ms(如5秒),否则在iptables DROP场景下,探针线程会长时间阻塞 。 4.2.2 Redis (Jedis vs Lettuce) Spring Boot 2.7默认使用Lettuce。但为了探测旧版Redis(或者为了简单起见),Jedis往往更适合做这种“一锤子买卖”的健康检查,因为它是同步阻塞IO,逻辑简单直接 。
探测方法:建立连接 -> 发送PING -> 期待PONG。
配置:socketTimeout必须设置,否则在断网时会挂起。 4.2.3 Consul Consul提供HTTP API,这是最容易探测的。
探测方法:使用Spring的RestTemplate或WebClient调用Consul Agent的本地接口 http://<Consul_IP>:8500/v1/status/leader 或查询Catalog服务 。
验证:当网络阻断时,该HTTP请求应抛出ConnectTimeoutException。
系统实现细节与代码结构 6.1 技术栈清单 | 组件 | 技术选型 | 说明 | |---|---|---| | JDK | Java 11/17 | Spring Boot 2.7的推荐运行环境 | | Web框架 | Spring Boot 2.7 | 核心容器,提供REST API与WebSocket支持 | | 前端模板 | Thymeleaf + Bootstrap 5 | 服务端渲染,简单易用,无需前后端分离部署 | | 日志监听 | Apache Commons IO (Tailer) | 高效的文件尾部监听组件 | | 依赖隔离 | Maven Shade Plugin | 解决旧版中间件驱动冲突 | | 进程交互 | Java ProcessBuilder | 调用iptables, tc, systemctl | | 数据库 | H2 (Dev) / PostgreSQL (Prod) | 存储组件拓扑关系 | 6.2 Agent端核心代码骨架 6.2.1 故障注入服务 (FaultInjectionService.java) @Service public class FaultInjectionService {
// 使用ScheduledExecutor作为看门狗 private final ScheduledExecutorService watchdog = Executors.newScheduledThreadPool(1);
public void blockNetwork(String targetIp, int port, int durationSeconds) { // 1. 安全检查:禁止阻断SSH或本地环回 if (port == 22 |
| targetIp.equals("127.0.0.1")) { throw new SecurityException("Target not allowed"); }
// 2. 构建iptables命令 // 使用comment标记以便后续精确删除 String comment = "CHAOS_BLOCK_" + targetIp + "_" + port; String cmd = { "iptables", "-A", "OUTPUT", "-p", "tcp", "-d", targetIp, "--dport", String.valueOf(port), "-j", "DROP", "-m", "comment", "--comment", comment }; // 3. 执行Root命令 CommandExecutor.executeRoot(cmd); // 4. 注册看门狗自动恢复(Deadman Switch) watchdog.schedule(() -> recoverNetwork(targetIp, port), durationSeconds + 5, TimeUnit.SECONDS); } public void recoverNetwork(String targetIp, int port) { String comment = "CHAOS_BLOCK_" + targetIp + "_" + port; // 使用-D删除规则 String cmd = { "iptables", "-D", "OUTPUT", "-p", "tcp", "-d", targetIp, "--dport", String.valueOf(port), "-j", "DROP", "-m", "comment", "--comment", comment }; CommandExecutor.executeRoot(cmd); }
}
6.2.2 日志监听适配器 (LogMonitor.java) 利用TailerListenerAdapter将日志事件转换为WebSocket消息推送。 public class LogMonitor implements TailerListener { private final SimpMessagingTemplate messagingTemplate; private final String componentId;
@Override public void handle(String line) { // 简单的关键词匹配,实际生产可使用正则 if (line.contains("Exception") |
| line.contains("Connection refused") | | line.contains("Timeout")) { LogEvent event = new LogEvent(componentId, "ERROR", line, System.currentTimeMillis()); messagingTemplate.convertAndSend("/topic/logs/" + componentId, event); } else if (line.contains("Connected") |
| line.contains("Recovery")) { LogEvent event = new LogEvent(componentId, "RECOVERY", line, System.currentTimeMillis()); messagingTemplate.convertAndSend("/topic/logs/" + componentId, event); } } //... 其他接口方法实现 }
6.3 管控端UI设计(Thymeleaf) 6.3.1 故障演练控制台 (dashboard.html) 页面应包含三个主要区域:
User=root ExecStart=/usr/bin/java -jar /opt/chaos-agent/chaos-agent.jar Restart=always
[Install] WantedBy=multi-user.target
7.2 安全加固