我是3y,一年CRUD
经验用十年的markdown
程序员常年被誉为职业八股文选手
好几天没更新austin
的系列文章啦,主要是一直在写austin
的代码。而这篇文章我想了很久标题,最后定为《优雅,不过时》。文章的内容主要由以下部分组成:
- 应用发布重启了怎么办?内存数据不是丢失了吗?
- 什么是优雅停机?如何实现优雅停机?
- 如何优雅地调整线程池的参数?
如果你的项目遇到了类似的问题,也可以借鉴下我今天所讲解的内容,读完我相信你肯定会有些收获。
01、应用发布重启了怎么办
众所周知,如果我们系统在运行的过程中,内存数据没存储起来那就会导致丢失。对于austin
项目而言,就会使消息丢失,并且无法下发到用户上。
这个在我讲述完我是如何设计「发送消息消费端」以及「读取文件」时,尤其问得比较多。为了部分没有追更的读者,我再简单讲述下我这边的设计:
在austin-handler
模块,每个渠道的每种消息类型我都用到了线程池进行隔离而消费:
在austin-cron
模块,我读取文件是把每一条记录放至了单线程池做LazyPending
,目的为了延迟消费做批量下发。
敏感的技术人看到内存队列或线程池(线程池也需要指定对应的内存队列)就很正常地想:内存队列可能的size
为1024
,而服务器在重启的时候可能内存队列的数据还没消费完,此时你怎么办?数据就丢了吗?
我们使用线程池/内存队列在很多场景下都是为了提高吞吐量,有得就必有失。至于重启服务器导致内存数据的丢失,就看你评估对自己的业务带来多少的影响了。
针对这种问题,austin
本身就开发好了相关的功能作为「补充」,通过实时计算引擎flink
的能力可以实时在后台查看消息下发的情况:
可以在离线
hive找到消息下发失败的userId
(离线这块暂未实现),输入具体的receiverId
可以查看实时下发时失败的原因
查明原因之后再通过csv
文件上传的做补发。
不过,这是平台提供做补发的能力,从技术上的角度,还有别的思路尽量避免线程池或者内存队列的数据因重启而丢失的数据吗?有的,优雅关闭线程池
02、优雅停机
所谓「优雅停机」就是关闭的时候先将自己需要处理的内容处理完了,之后才关闭。如果你直接kill -9
,是没有「优雅」这一说法的,神仙都救不了。
1、在网络层:TCP有四次挥手、TCP KeepAlive
、HTTP KeepAlive
让连接 优雅地关闭,避免很多报错。
2、在Java里边通过Runtime.getRuntime().addShutdownHook()
注册事件,当虚拟机关闭的前调用该方法的具体逻辑进行善后。
3、在Spring里边执行了ApplicationContext
的close
之后,只要我们Bean配置了destroy
策略,那Spring在关闭之前也会先执行我们的已实现好的destroy
方法
4、在Tomcat容器提供了几种关闭的姿势,先暂停请求,选择等待N秒才完全关闭容器。
5、在Java线程池提供了shutdown
和shutdownNow
供我们关闭线程,显然shutdown
是优雅关闭线程池的方法。
我们的austin
项目是基于SpringBoot
环境构造的,所以我们可以重度依赖SpringBoot进行优雅停机。
1、我们设置应用服务器的停机模式为graceful
server.shutdown=graceful
2、在austin
已经引入动态线程池而非使用Spring管理下的ThreadPoolTaskExecutor
,所以我们可以把自己创建出来的线程池在Spring关闭的时候,进行优雅shutdown
(想要关闭其他的资源时,也可以类似干这种操作)
注:如果是使用Spring封装过的线程池ThreadPoolTaskExecutor
,默认就会优雅关闭,因为它是实现了DisposableBean
接口的
03、如何优雅地调整线程池的参数?
austin
在整个项目里边,还是有挺多地方是用到了线程池,特别重要的是从MQ
里消费所创建的线程池。
有小伙伴当时给过建议:有没有打算引入动态线程池,不用发布就调整线程池的参数从而临时提高消费能力。顺便在这给大家推荐美团的线程池文章:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html,如果没读过这篇文章的,建议都去读下,挺不错的。
美团这篇文章讲述了动态线程池的思路,但应该是未官方开源,所以有很多小伙伴基于文章的思路造了好用的轮子。比如 Hippo4J 和dynamic-tp 都是比较优秀的轮子了。
这两个仓库我都看了下源码, Hippo4J 有无依赖中间件实现动态线程池,也有默认实现Nacos
和Apollo
的版本,并有着管理后台,而dynamic-tp 默认实现依赖Nacos
或Apollo
。大佬们的代码都写得很不错,我推荐大家都可以去学学。
我在最初的时候接的是dynamic-tp的代码,因为我本身austin
就接入了Apollo
,也感觉暂时不太需要管理后台。后来 Hippo4J 作者找我聊了下,希望我能接入Hippo4J。
我按照我目前的使用场景对着代码看了一把,我是需要通过在创建线程池后再动态调参的场景。于是跟 Hippo4J 作者反馈了下,他果断说晚上或明天就给我实现(:恐怖如斯,太肝了
不过,周三我反馈完,周四晚上我差不多就将 dynamic-tp 快接入完了。我目前现在打算先跑着(毕竟切换API其实也是需要时间成本的),后续看有没有遇到痛点或者空的时候再迁移到Hippo4J再体验体验
也不为别的,就看中龙台大佬比我还肝(自己提出的场景,开源作者能很快地反馈并实现,太强了,丝毫不担心有大坑要我自己搞)
04、总结
对于austin
而言,正常的重启发布我们通过优雅停机来尽可能减少系统的处理数据时的丢失。如果消息是真的非常重要而且需要做补发,在austin
中也可以通过上传文件的方式再做补发,且能看到实时推送的数据链路统计和某个用户下发消息失败的原因。
我相信,这已经能覆盖线上绝大多数的场景了。
或许后续也可以针对某些场景在消费端做exactly once
+ 幂等 来解决kill -9
的窘境,但要知道的是:想要保证数据不丢失、不重复发送给用户,一定会带来性能的损耗,这是需要做平衡的。
在项目很少使用线程池之前,一直可能认为线程池的相关面试题就是八股文。但当你项目系统真的遇到线程池优雅关闭的问题、线程池参数动态调整的问题,你就会发现之前看的内容其实是很有意义的。
阿,原来可以设置参数让核心线程数也会回收的(之前一直都没有注意过呢)
阿,原来都大多数框架都有提供对应的扩展接口给我们监听关闭,默认的实现都有优雅停机的机制咯,之前一直都不知道呢。
....
austin
还在持续优化和更新中,欢迎大佬们给点意见和想法一起讨论,对该项目感兴趣的同学也可以到我的GitHub上逛逛,或许有可能这个季度的KPI
就有了咯。
动态线程池的仓库地址:
都看到这里了,点个赞一点都不过分吧?我是3y,下期见。
关注我的微信公众号【Java3y】除了技术我还会聊点日常,有些话只能悄悄说~ 【对线面试官+从零编写Java项目】 持续高强度更新中!求star!!原创不易!!求三连!!
austin项目源码Gitee链接:gitee.com/austin
austin项目源码GitHub链接:github.com/austin