由于项目目前业务场景涉及服务的水平扩缩容,这就要求每台服务器上的后台服务具备平滑退出的能力。
    所谓平滑退出,是指服务器被收回时,项目内部的业务操作不会因为服务器被收回导致业务上的错误,尤其是项目使用到了一些三方中间件,比如说redis,nacos或kafka等等。
    当下的实现方式涉及两个方面。其一是kafka根据关闭信号停止消费消息,其二是当jvm获取到退出信号后,项目内部维护的线程池,能够保证活跃的线程处理完其负责的任务在销毁。
    针对两个问题,实现步骤如下:
    第一,kafka主动停止消费

    1. @Slf4j
    2. @RestController
    3. public class ExitController {
    4. @Autowired
    5. private KafkaListenerEndpointRegistry registry;
    6. @GetMapping("/exit/{password}")
    7. public ResultHelper<Integer> materialToADS(@PathVariable("password") String password) {
    8. //最大五千
    9. return BizTemplate.execute(new BizCallBack<Integer>() {
    10. @Override
    11. public void paramCheck() {
    12. if (!StringUtils.equals(password, "password")) {
    13. throw new BizException("无效请求!");
    14. }
    15. }
    16. @Override
    17. public Integer preCheck() {
    18. return null;
    19. }
    20. @Override
    21. public Integer execute() throws Exception {
    22. try {
    23. log.info("========收到关闭指令========");
    24. log.info("========kafka停止接收消息========");
    25. log.info("========阻塞60s,等待现有任务消费完毕========");
    26. Set<String> containerIds = registry.getListenerContainerIds();
    27. for (String containerId : containerIds) {
    28. registry.getListenerContainer(containerId).stop();
    29. }
    30. try {
    31. Thread.sleep(1000 * 60);
    32. } catch (InterruptedException e) {
    33. e.printStackTrace();
    34. }
    35. log.info("========服务即将关闭========");
    36. System.exit(0);
    37. } catch (Exception e) {
    38. log.error("异常信息,error:", e);
    39. log.info("========服务退出异常========");
    40. return 1;
    41. }
    42. log.info("========服务正常退出中。。。========");
    43. return 0;
    44. }
    45. });
    46. }
    47. }

    第二,线程池的退出

    1. public class ThreadPoolShutDownHook {
    2. private static final ThreadPoolShutDownHook INSTANCE = new ThreadPoolShutDownHook();
    3. private List<ExecutorService> executorServices = Lists.newArrayList();
    4. private AtomicBoolean closed = new AtomicBoolean(false);
    5. public static ThreadPoolShutDownHook getInstance() {
    6. return INSTANCE;
    7. }
    8. private ThreadPoolShutDownHook() {
    9. Runtime.getRuntime().addShutdownHook(new Thread() {
    10. @Override
    11. public void run() {
    12. shutdown();
    13. }
    14. });
    15. }
    16. @PreDestroy
    17. public void shutdown() {
    18. if (!closed.compareAndSet(false, true)) {
    19. return;
    20. }
    21. for (ExecutorService executorService : executorServices) {
    22. tryShutdownNow(executorService);
    23. }
    24. }
    25. private void tryShutdownNow(ExecutorService executorService) {
    26. try {
    27. executorService.shutdownNow();
    28. } catch (Throwable e) {
    29. //ignore logger maybe has been destroyed
    30. }
    31. try {
    32. executorService.awaitTermination(1, TimeUnit.SECONDS);
    33. } catch (InterruptedException e) {
    34. //ignore logger maybe has been destroyed
    35. }
    36. }
    37. public ExecutorService register(ExecutorService executorService) {
    38. this.executorServices.add(executorService);
    39. return executorService;
    40. }
    41. }

    需要注意的是项目内部的线程池在配置完毕后需要注册近钩子维护的列表中。
    第三步是对项目关停脚本进行编写

    1. #!/bin/bash
    2. APP_NAME=project_name.jar
    3. SPRING_PROFILES_ACTIVE=pro
    4. #使用说明,用来提示输入参数
    5. usage() {
    6. echo "Usage: sh 执行脚本.sh [start|stop|restart[r]|status]"
    7. exit 1
    8. }
    9. #检查程序是否在运行
    10. is_exist() {
    11. pid=$(ps -ef | grep $APP_NAME | grep -v grep | awk '{print $2}')
    12. #如果不存在返回1,存在返回0
    13. if [ -z "${pid}" ]; then
    14. return 1
    15. else
    16. return 0
    17. fi
    18. }
    19. #启动方法
    20. start() {
    21. is_exist
    22. if [ $? -eq "0" ]; then
    23. echo " ## ${APP_NAME} 该服务已经运行. pid=${pid} ."
    24. else
    25. if [ "$2" != "" ]; then
    26. SPRING_PROFILES_ACTIVE=$2
    27. fi
    28. nohup java -server -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Xms512m -Xmx4g -Xmn256m -Xss512k -XX:+StartAttachListener -verbose:gc -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=oom.hprof -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=18887 -jar $APP_NAME --spring.profiles.active=${SPRING_PROFILES_ACTIVE} >/dev/null 2>&1 &
    29. echo " ## $APP_NAME 启动成功 ! 已激活profiles: ${SPRING_PROFILES_ACTIVE}"
    30. fi
    31. }
    32. #停止方法
    33. stop() {
    34. is_exist
    35. if [ $? -eq "0" ]; then
    36. echo " ## ${APP_NAME} 关闭signal已发出,请耐心等待"
    37. curl http://localhost:port/exit/password
    38. status=0
    39. until [ $status -eq "1" ]; do
    40. is_exist
    41. status=$?
    42. done
    43. echo " ## status is $status "
    44. echo " ## ${APP_NAME} 服务已关闭"
    45. else
    46. echo " ## ${APP_NAME} 服务没法有运行,无法停止"
    47. fi
    48. }
    49. #输出运行状态
    50. status() {
    51. is_exist
    52. if [ $? -eq "0" ]; then
    53. echo " ## ${APP_NAME} 服务正在运行. 进程号(pid): ${pid}"
    54. else
    55. echo " ## ${APP_NAME} 服务已停止运行."
    56. fi
    57. }
    58. #重启
    59. restart() {
    60. stop
    61. deployJar
    62. start
    63. }
    64. #根据输入参数,选择执行对应方法,不输入则执行使用说明
    65. case "$1" in
    66. "start")
    67. start
    68. ;;
    69. "stop")
    70. stop
    71. ;;
    72. "status")
    73. status
    74. ;;
    75. "restart")
    76. restart
    77. ;;
    78. "r")
    79. restart
    80. ;;
    81. *)
    82. usage
    83. ;;
    84. esac

    参考文章地址:https://www.jianshu.com/p/62574e09acbf