成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Spring Boot 應用零停機更新策略

開發 前端
在個人或者企業服務器上,總歸有要更新代碼的時候,普通的做法必須先終止原來進程,因為新進程和老進程端口是一個,新進程在啟動時候,必定會出現端口占用的情況,但是,還有黑科技可以讓兩個SpringBoot進程真正的共用同一個端口,這是另一種解決辦法,我們下回分解。

前言

在個人或者企業服務器上,總歸有要更新代碼的時候,普通的做法必須先終止原來進程,因為新進程和老進程端口是一個,新進程在啟動時候,必定會出現端口占用的情況,但是,還有黑科技可以讓兩個SpringBoot進程真正的共用同一個端口,這是另一種解決辦法,我們下回分解。

那么就會出現一個問題,如果此時有大量的用戶在訪問,但是你的代碼又必須要更新,這時候如果采用上面的做法,那么必定會導致一段時間內的用戶無法訪問,這段時間還取決于你的項目啟動速度,那么在單體應用下,如何解決這種事情?

一種簡單辦法是,新代碼先用其他端口啟動,啟動完畢后,更改 nginx 的轉發地址,nginx 重啟非???,這樣就避免了大量的用戶訪問失敗,最后終止老進程就可以。

但是還是比較麻煩,端口換來換去,即使你寫個腳本,也是比較麻煩,有沒有一種可能,新進程直接啟動,自動處理好這些事情?

答案是有的。

設計思路

這里涉及到幾處源碼類的知識,如下。

  • SpringBoot內嵌Servlet容器的原理是什么
  • DispatcherServlet是如何傳遞給Servlet容器的

先看第一個問題,用Tomcat來說,這個首先得Tomcat本身支持,如果Tomcat不支持內嵌,SpringBoot估計也沒辦法,或者可能會另找出路。

Tomcat 本身有一個 Tomcat 類,沒錯就叫 Tomcat,全路徑是org.apache.catalina.startup.Tomcat,我們想啟動一個 Tomcat,直接 new Tomcat(),之后調用start()就可以了。

并且他提供了添加Servlet、配置連接器這些基本操作。

public class Main {
    public static void main(String[] args) {
        try {
            Tomcat tomcat = new Tomcat();
            tomcat.getConnector();
            tomcat.getHost();
            Context context = tomcat.addContext("/", null);
            tomcat.addServlet("/","index",new HttpServlet(){
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                    resp.getWriter().append("hello");
                }
            });
            context.addServletMappingDecoded("/","index");
            tomcat.init();
            tomcat.start();
        }catch (Exception e){}
    }
}

在 SpringBoot 源碼中,根據你引入的 Servlet 容器依賴,通過下面代碼可以獲取創建對應容器的工廠,拿 Tomcat 來說,創建 Tomcat 容器的工廠類是TomcatServletWebServerFactory。

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

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

調用ServletWebServerFactory.getWebServer就可以獲取一個 Web 服務,他有 start、stop 方法啟動、關閉 Web 服務。

而 getWebServer 方法的參數很關鍵,也是第二個問題,DispatcherServlet 是如何傳遞給 Servlet 容器的。

SpringBoot 并不像上面 Tomcat 的例子一樣簡單的通過tomcat.addServlet把 DispatcherServlet 傳遞給 Tomcat,而是通過個 Tomcat 主動回調來完成的,具體的回調通過ServletContainerInitializer接口協議,它允許我們動態地配置 Servlet、過濾器。

SpringBoot 在創建 Tomcat 后,會向 Tomcat 添加一個此接口的實現,類名是TomcatStarter,但是TomcatStarter也只是一堆 SpringBoot 內部ServletContextInitializer的集合,簡單的封裝了一下,這些集合中有一個類會向 Tomcat 添加 DispatcherServlet。

在 Tomcat 內部啟動后,會通過此接口回調到 SpringBoot 內部,SpringBoot 在內部會調用所有ServletContextInitializer集合來初始化,

而 getWebServer 的參數正好就是一堆ServletContextInitializer集合。

那么這時候還有一個問題,怎么獲取ServletContextInitializer集合?

非常簡單,注意,ServletContextInitializerBeans是實現Collection的。

protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) {
    return new ServletContextInitializerBeans(context.getBeanFactory());
}

到這里所有用到的都準備完畢了,思路也很簡單。

  1. 判斷端口是否占用
  2. 占用則先通過其他端口啟動
  3. 等待啟動完畢后終止老進程
  4. 重新創建容器實例并且關聯DispatcherServlet

在第三步和第四步之間,速度很快的,這樣就達到了無縫更新代碼的目的。

實現代碼

@SpringBootApplication()
@EnableScheduling
public class WebMainApplication {
    public static void main(String[] args) {
        String[] newArgs = args.clone();
        int defaultPort = 8088;
        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(WebMainApplication.class, newArgs);
        if (needChangePort) {
            String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort);
            try {
                Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor();
                while (isPortInUse(defaultPort)) {
                }
                ServletWebServerFactory webServerFactory = getWebServerFactory(run);
                ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort);
                WebServer webServer = webServerFactory.getWebServer(invokeSelfInitialize(((ServletWebServerApplicationContext) run)));
                webServer.start();

                ((ServletWebServerApplicationContext) run).getWebServer().stop();
            } catch (IOException | InterruptedException ignored) {
            }
        }
    }

    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;
        }
    }

    protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) {
        return new ServletContextInitializerBeans(context.getBeanFactory());
    }

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

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

測試

我們先寫一個小 demo。

@RestController()
@RequestMapping("port/test")
public class TestPortController {
    @GetMapping("test")
    public String test() {
        return "1";
    }
}

并且打包成 jar,然后更改返回值為 2,并打包成 v2 版本的 jar 包,此時有兩個代碼,一個新的一個舊的。

圖片圖片

我們先啟動 v1 版本,并且使用 IDEA 中最好用的接口調試插件Cool Request測試,可以發現此時都正常。

圖片圖片

好的我們不用關閉 v1 的進程,直接啟動 v2 的 jar 包,并且啟動后,可以一直在 Cool Request 測試接口時間內的可用程度。

稍等后,就會看到 v2 代碼已經生效,而在這個過程中,服務只有極短的時間不可用,不會超過1秒。

圖片圖片

責任編輯:武曉燕 來源: 一安未來
相關推薦

2017-04-12 11:15:52

ReactsetState策略

2011-11-04 14:07:20

微軟Hotmail策略

2020-02-10 09:35:18

數據中心服務器技術

2018-10-24 14:30:30

緩存服務更新

2025-02-19 10:17:39

2025-03-11 00:55:00

Spring停機安全

2018-10-19 11:07:02

主流緩存更新

2012-11-21 09:34:58

SaaS應用SaaS應用集成軟件集成

2025-06-12 09:16:54

2023-04-13 08:15:47

Redis緩存一致性

2012-02-01 10:29:13

2022-12-23 08:28:42

策略模式算法

2009-10-30 09:19:43

2009-03-09 18:46:11

Windows phoWindows Mob

2024-09-27 08:25:47

2024-07-26 07:59:25

2018-06-21 11:27:06

Windows 7更新停止

2017-09-20 09:46:38

Spring BootSpring Clou內存

2015-10-30 09:33:48

ChromeAndroid合一

2010-11-11 14:36:17

MySQL
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产偷录叫床高潮录音 | 国产精品色 | 国产婷婷| 日本一区二区三区四区 | 成人精品一区亚洲午夜久久久 | www.788.com色淫免费 | 久久天堂网 | 国产精品爱久久久久久久 | 国产小u女发育末成年 | 日韩在线免费 | 国产精品一区二区视频 | 三级欧美 | 日韩三级在线观看 | 成人免费观看网站 | 亚洲网一区 | 爱爱小视频 | 国产精品高潮呻吟久久久久 | 亚洲成人免费视频在线观看 | 日韩在线视频播放 | 91亚洲国产成人久久精品网站 | 天天欧美 | 97av在线| 精品中文字幕视频 | 成人免费网视频 | 精品视频在线一区 | 男女久久久 | 午夜精品网站 | 国产精品视频播放 | 久久久精品综合 | 天天影视亚洲综合网 | 免费黄色网址视频 | 亚洲欧美日韩在线一区二区 | 日韩中文字幕视频在线 | 亚洲成av人影片在线观看 | 中文字幕免费视频 | 91久久久久 | 精品在线一区 | 久久久精品一区 | 黄篇网址| 成人激情视频免费观看 | 日韩欧美国产一区二区三区 |