SpringBoot应用实现零停机更新(代码更新不停机)

1.前言

        在个人或者企业服务器上,总归有要更新代码的时候,普通的做法必须先终止原来进程,因为新进程和老进程端口是一个,新进程在启动时候,必定会出现端口占用的情况。

2. 痛点

        如果此时有大量的用户在访问,但是你的代码又必须要更新,这时如果采用上面的做法(先终止原来进程,重新启动新进程),那么必定会导致一段时间内的用户无法访问,这段时间取决于项目启动速度,在单体应用下,如何解决这一痛点?

3.解决方案

         3.1  通过更改nginx的转发地址来实现

        一种简单办法是,新代码先用其他端口启动,启动完毕后,更改nginx的转发地址,nginx重启非常快,这样就避免了大量的用户访问失败,最后终止老进程就可以。

        但是还是比较麻烦,端口换来换去,即使你写个脚本,也是比较麻烦。

        3.2  再springboot项目中通过代码实现

        在spring项目中可以使用获取ServletWebServerFactory(webServer的工厂类),可以使用该工厂类的getWebServer()获取一个Web服务,它有start、stop方法启动和关闭Web服务。

       

@Slf4j
@SpringBootApplication
public class TargetinfoApplication implements CommandLineRunner {


    @Value("${server.port}")
    private String serverPort;

    public static void main(String[] args) {
        //SpringApplication.run(TargetinfoApplication.class, args);
        String[] newArgs = args.clone();
        int defaultPort = 9088;
        boolean needChangePort = false;
        if (isPortInUse(defaultPort)) {
            newArgs = new String[args.length + 1];
            System.arraycopy(args, 0, newArgs, 0, args.length);
            newArgs[newArgs.length - 1] = "--server.port=9090";
            needChangePort = true;
        }
        ConfigurableApplicationContext run = SpringApplication.run(TargetinfoApplication.class, newArgs);
        if (needChangePort) {
            String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort);
            try {
                String os = System.getProperty("os.name").toLowerCase();
                if (os.contains("win")) {
                    processWinCommand(defaultPort);
                }
                if (os.contains("nix") || os.contains("nux") || os.contains("mac")) {
                    Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor();
                }
                // 等待端口释放
                while (isPortInUse(defaultPort)) {
                }
                // 更改 Web 服务器监听的端口 (先获取webServer工厂,然后设置端口号,最后创建webServer运行程序)
                ServletWebServerFactory webServerFactory = getWebServerFactory(run);
                //设置Web服务器监听的端口:还是监听原来的端口
                ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort);
                //创建并启动新的Web服务器
                WebServer webServer = webServerFactory.getWebServer(invokeSelfInitialize(((ServletWebServerApplicationContext) run)));
                webServer.start();

                // 停止原先的 Web 服务器
                ((ServletWebServerApplicationContext) run).getWebServer().stop();
            } catch (IOException | InterruptedException ignored) {
            }
        }
    }

    @Override
    public void run(String... args) throws Exception {
        InetAddress address = InetAddress.getLocalHost();
        String ip = address.getHostAddress();
        String hostName = address.getHostName();
        log.info("road server Started! [{}<{}:{}>].", hostName, ip, serverPort);
        log.info("接口文档访问地址:http://{}:{}/doc.html", ip, serverPort);

    }

    private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext context) {
        try {
            Method method = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer");
            method.setAccessible(true);
            return (ServletContextInitializer) method.invoke(context);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

    }

    private static boolean isPortInUse(int port) {
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            return false;
        } catch (IOException e) {
            return true;
        }
    }


    private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
        String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);

        return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }

    /**
     * 执行windows命令
     * @param port
     */
    public static void processWinCommand(int port) {
        List<Integer> pids = getListeningPids(port);
        terminateProcesses(pids);
    }

    /**
     * 获取占用指定端口的进程的 PID 列表
     * @param port
     * @return
     */
    private static List<Integer> getListeningPids(int port) {
        List<Integer> pids = new ArrayList<>();
        try {
            Process process = new ProcessBuilder("cmd", "/c", "netstat -ano").start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                if (!line.trim().isEmpty() && line.contains(String.format(":%d", port)) && line.contains("LISTENING")) {
                    String[] parts = line.trim().split("\\s+");
                    System.out.println(line);
                    String pid = parts[4];
                    System.out.println(pid);
                    pids.add(Integer.parseInt(pid));
                }

            }
            reader.close();
            process.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return pids;
    }

    /**
     * 终止指定 PID 的进程
     * @param pids
     */
    private static void terminateProcesses(List<Integer> pids) {
        for (Integer pid : pids) {
            try {
                System.out.println("Terminating process with PID: " + pid);
                ProcessBuilder builder = new ProcessBuilder("cmd", "/c", "taskkill", "/F", "/PID", pid.toString());
                Process process = builder.start();
                process.waitFor();

                // 检查进程是否已经终止
                System.out.println(checkIfProcessIsRunning(pid));
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 检查指定的进程是否运行  true是 false否
     * @param pid
     * @return
     */
    public static boolean checkIfProcessIsRunning(int pid)  {
        try {
            ProcessBuilder builder = new ProcessBuilder("cmd", "/c", "tasklist /FI \"PID eq " + pid + "\"");
            Process process = builder.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            boolean found = false;
            while ((line = reader.readLine()) != null) {
                if (line.contains(Integer.toString(pid))) {
                    found = true;
                    break;
                }
            }
            reader.close();
            process.waitFor();
            return found;
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
            return true; // 返回 true 表示进程可能存在,但检查失败
        }
    }
}
 

  

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值