事故原由

由于公司发号器通过雪花算法来生成,而雪花算法由:时间缀+机器码mid+自增序列组成,为了保证每个实例mid唯一性,机器码池midPool是保存在redis中的,每次服务后生成一个唯一mid丢到midPool里,服务下线时的清理掉此mid。项目代码通过@PreDestroy注解来处理服务下线后的清理工作,但是实际生产服务重启时部分实例并未执行清理,midPool满了,导致新启动的实例无法正确的初始化mid,使生产出现事故。
midPool结构:
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210416103014767.png
池结构很简单,key永久有效,大小1000。

java清理

@PreDestroy
public void exit(){
	// 清理midPool池中当前实例的的mid
	redisTemplate.del("midPool"+ip):
	log.info("我退出了");
}

容器启动

CMD ["/home/start.sh"]

/home/start.sh

#!/bin/bash
java -jar app.jar

容器停止

  1. 执行 kill -15 pid
  2. 然后等待 30s (默认时间,也可以修改成其他时间)
  3. 如果30s之内服务还没停止,将执行 kill -9

原因定位

以上代码一切看起来都没有问题,并且生产环境只有部分实例未执行清理的代码块。通过网络文章与运维团队测试定位到问题,容器的启动方式存在问题,start脚本是主进程,java为子进程。kill -15命令时,start主进程收到SIGTERM信号,但是并未将信号量传递到java子进程,所以java程序未能触发清理代码。

解决方案

既然知道了原因,那么就好解决了,有几种方案:

  1. 将SIGTERM信号传送给子进程
#!/bin/bash
java -jar app.jar &
pid="$!"

_kill() {
  kill $pid
  wait $pid
  exit 143
}
trap _kill SIGTERM
wait

步骤是把java程序后台启动以获得它的PID,最后一行加入wait命令防止shell退出,trap命令捕捉SIGTERM信号并执行一个命令

  1. 以java作为容器的主进程方式启动
CMD ["java", "-jar", "app.jar"]
  1. 对mid池进行改造,使可以redis中的mid可以过期时间,同时存活的实例定期的续约
  2. zookeeper临时节点:临时节点的生命周期和客户端会话绑定在一起,客户端会话失效,则这个节点就会被自动清除。

最终我们采用方案2+方案3的方式处理的,不能单靠退出清理的方式,避免可能在极端(oom、机器断电等)的情况下还是无法执行到清理代码。

参考文章

感谢大佬的文章帮忙定位到了问题
https://www.orchome.com/9962

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐