1. 现状分析

1.1 当前问题

  1. 引擎直接关闭:目前的引擎关闭操作是直接关闭,没有进行额外的收尾操作。
  2. 数据丢失风险
    • 应用更新和添加:在更新或添加应用时会导致容器重启,这对于一些应用(如 IoT)来说,由于无法及时消费队列中的数据,会导致数据丢失。
    • Hazelcast 组网:多个节点直接关闭时,由于某一写分片来不及备份,可能导致数据丢失。
  3. 缺乏优雅关闭机制:当前的关闭操作是直接和突兀的,没有给业务一定的缓冲时间。

2. 改进方案和实现

2.1 配置 Spring 优雅关闭机制

通过配置 Spring 的优雅关闭机制,可以在 JVM 关闭时优雅地关闭 Spring 应用上下文,避免数据丢失。

  1. 检查容器配置中,是否配置了terminationGracePeriodSeconds字段,不配置(默认为30)或配置为30即可
  2. 需要在application-dev.properties文件里添加如下配置

配置文件(application.properties)

server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

SpringApplication 类

private void registerShutdownHook(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        Thread shutdownHook = new Thread(() -> {
            try {
                context.close();
            } catch (Exception ex) {
                // Handle exception...
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
}
通过 Runtime.getRuntime().addShutdownHook(shutdownHook); 监听 k8s 发送的 SIGTERM 信号。

2.2 实现 DisposableBean 接口

通过实现 DisposableBean 接口,可以在 Spring 容器关闭时执行一些自定义的销毁逻辑。

示例代码

@RestController
@CrossOrigin
public class RpcController implements DisposableBean {
    ...
    @Override
    public void destroy() throws Exception {
        long start = System.currentTimeMillis();
        logger.warn("\n\n\n ######################  iidp server destroy start at {}\n\n\n", start);
        
        // 自定义销毁逻辑
        ...

        long end = System.currentTimeMillis();
        logger.warn("\n\n\n ######################  iidp server destroy finish at {}, cost: {} ms", end, end - start);
    }
    
    ...
}

2.3 优雅关闭 Hazelcast

通过优雅关闭 Hazelcast 当前成员,避免数据丢失。

示例代码

// 优雅关闭 Hazelcast 当前成员,避免数据丢失
// 参考: https://docs.hazelcast.com/hazelcast/5.5/maintain-cluster/shutdown
Hazelcast.getInstance().shutdown();

2.4 触发销毁事件

在关闭容器时,给当前容器中的所有应用发送销毁事件。

示例代码

public void destroyEvent() {
    // 给当前容器所有的 app 发送 destroy event

    Meta meta = new Meta(Meta.SUPERUSER, new HashMap<>());
    Map<String, AppDataInfo> appDataInfoMap = EngineContainer.getBussinessAppGroupContainer().getAppDataInfoMap();
    Map<String, AppDataInfo> appDataInfoMap2 = EngineContainer.getBaseAppGroupContainer().getAppDataInfoMap();
    List<AppDataInfo> appDataInfoList = new ArrayList<>(appDataInfoMap.values());
    appDataInfoList.addAll(appDataInfoMap2.values());

    for (AppDataInfo appInfo : appDataInfoList) {
        Map<String, List<String>> events = appInfo.getEvents();
        if (events == null || events.isEmpty()) {
            continue;
        }
        List<String> methodList = events.get(EventType.DESTROY.getType());
        if (methodList == null || methodList.isEmpty()) {
            continue;
        }

        meta.addArgument("app", appInfo.getName());
        meta.addArgument(MetaConstant.TAG, appInfo.getTag());

        logger.info("============== App [{}] 销毁事件 {} 开始 ==============", appInfo.getName(), methodList);

        for (String method : methodList) {
            try {
                String[] modelMethod = method.split(MetaConstant.METHOD_SEPARATOR);
                if (modelMethod.length == 3 && "ASYNC".equals(modelMethod[2])) {
                    meta.get(modelMethod[0]).callAsync(modelMethod[1]);
                } else {
                    meta.get(modelMethod[0]).call(modelMethod[1]);
                }
            } catch (Exception ex) {
                logger.error(String.format("执行应用: %s 事件报错", appInfo.getNameTag()), ex);
            }
        }

        logger.info("============== App [{}] 销毁事件 {} 结束 ==============", appInfo.getName(), methodList);
    }
}

2.5 app定义destroy销毁事件

在app中的app.json文件中定义销毁事件

"events":{
  "destroy": [
    "trace_whitelist::destroy"  # 新增destroy事件
  ],
  "startUp": [
    "trace_whitelist::register"
  ],
  "register" : [
  ],
  "login": [
  ]
}

3. 总结

通过以上改进措施,可以实现系统的优雅关闭,避免数据丢失,提高系统的可靠性和稳定性。这些措施包括配置 Spring 的优雅关闭机制、实现 DisposableBean 接口、优雅关闭 Hazelcast 以及触发销毁事件。通过这些改进,可以为业务提供一定的缓冲时间,确保数据的完整性和一致性。