自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Spring Boot 應(yīng)用零停機(jī)更新策略

開(kāi)發(fā) 前端
在個(gè)人或者企業(yè)服務(wù)器上,總歸有要更新代碼的時(shí)候,普通的做法必須先終止原來(lái)進(jìn)程,因?yàn)樾逻M(jìn)程和老進(jìn)程端口是一個(gè),新進(jìn)程在啟動(dòng)時(shí)候,必定會(huì)出現(xiàn)端口占用的情況,但是,還有黑科技可以讓兩個(gè)SpringBoot進(jìn)程真正的共用同一個(gè)端口,這是另一種解決辦法,我們下回分解。

前言

在個(gè)人或者企業(yè)服務(wù)器上,總歸有要更新代碼的時(shí)候,普通的做法必須先終止原來(lái)進(jìn)程,因?yàn)樾逻M(jìn)程和老進(jìn)程端口是一個(gè),新進(jìn)程在啟動(dòng)時(shí)候,必定會(huì)出現(xiàn)端口占用的情況,但是,還有黑科技可以讓兩個(gè)SpringBoot進(jìn)程真正的共用同一個(gè)端口,這是另一種解決辦法,我們下回分解。

那么就會(huì)出現(xiàn)一個(gè)問(wèn)題,如果此時(shí)有大量的用戶在訪問(wèn),但是你的代碼又必須要更新,這時(shí)候如果采用上面的做法,那么必定會(huì)導(dǎo)致一段時(shí)間內(nèi)的用戶無(wú)法訪問(wèn),這段時(shí)間還取決于你的項(xiàng)目啟動(dòng)速度,那么在單體應(yīng)用下,如何解決這種事情?

一種簡(jiǎn)單辦法是,新代碼先用其他端口啟動(dòng),啟動(dòng)完畢后,更改 nginx 的轉(zhuǎn)發(fā)地址,nginx 重啟非???,這樣就避免了大量的用戶訪問(wèn)失敗,最后終止老進(jìn)程就可以。

但是還是比較麻煩,端口換來(lái)?yè)Q去,即使你寫(xiě)個(gè)腳本,也是比較麻煩,有沒(méi)有一種可能,新進(jìn)程直接啟動(dòng),自動(dòng)處理好這些事情?

答案是有的。

設(shè)計(jì)思路

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

  • SpringBoot內(nèi)嵌Servlet容器的原理是什么
  • DispatcherServlet是如何傳遞給Servlet容器的

先看第一個(gè)問(wèn)題,用Tomcat來(lái)說(shuō),這個(gè)首先得Tomcat本身支持,如果Tomcat不支持內(nèi)嵌,SpringBoot估計(jì)也沒(méi)辦法,或者可能會(huì)另找出路。

Tomcat 本身有一個(gè) Tomcat 類,沒(méi)錯(cuò)就叫 Tomcat,全路徑是org.apache.catalina.startup.Tomcat,我們想啟動(dòng)一個(gè) Tomcat,直接 new Tomcat(),之后調(diào)用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 源碼中,根據(jù)你引入的 Servlet 容器依賴,通過(guò)下面代碼可以獲取創(chuàng)建對(duì)應(yīng)容器的工廠,拿 Tomcat 來(lái)說(shuō),創(chuàng)建 Tomcat 容器的工廠類是TomcatServletWebServerFactory。

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

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

調(diào)用ServletWebServerFactory.getWebServer就可以獲取一個(gè) Web 服務(wù),他有 start、stop 方法啟動(dòng)、關(guān)閉 Web 服務(wù)。

而 getWebServer 方法的參數(shù)很關(guān)鍵,也是第二個(gè)問(wèn)題,DispatcherServlet 是如何傳遞給 Servlet 容器的。

SpringBoot 并不像上面 Tomcat 的例子一樣簡(jiǎn)單的通過(guò)tomcat.addServlet把 DispatcherServlet 傳遞給 Tomcat,而是通過(guò)個(gè) Tomcat 主動(dòng)回調(diào)來(lái)完成的,具體的回調(diào)通過(guò)ServletContainerInitializer接口協(xié)議,它允許我們動(dòng)態(tài)地配置 Servlet、過(guò)濾器。

SpringBoot 在創(chuàng)建 Tomcat 后,會(huì)向 Tomcat 添加一個(gè)此接口的實(shí)現(xiàn),類名是TomcatStarter,但是TomcatStarter也只是一堆 SpringBoot 內(nèi)部ServletContextInitializer的集合,簡(jiǎn)單的封裝了一下,這些集合中有一個(gè)類會(huì)向 Tomcat 添加 DispatcherServlet。

在 Tomcat 內(nèi)部啟動(dòng)后,會(huì)通過(guò)此接口回調(diào)到 SpringBoot 內(nèi)部,SpringBoot 在內(nèi)部會(huì)調(diào)用所有ServletContextInitializer集合來(lái)初始化,

而 getWebServer 的參數(shù)正好就是一堆ServletContextInitializer集合。

那么這時(shí)候還有一個(gè)問(wèn)題,怎么獲取ServletContextInitializer集合?

非常簡(jiǎn)單,注意,ServletContextInitializerBeans是實(shí)現(xiàn)Collection的。

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

到這里所有用到的都準(zhǔn)備完畢了,思路也很簡(jiǎn)單。

  1. 判斷端口是否占用
  2. 占用則先通過(guò)其他端口啟動(dòng)
  3. 等待啟動(dòng)完畢后終止老進(jìn)程
  4. 重新創(chuàng)建容器實(shí)例并且關(guān)聯(lián)DispatcherServlet

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

實(shí)現(xiàn)代碼

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

測(cè)試

我們先寫(xiě)一個(gè)小 demo。

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

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

圖片圖片

我們先啟動(dòng) v1 版本,并且使用 IDEA 中最好用的接口調(diào)試插件Cool Request測(cè)試,可以發(fā)現(xiàn)此時(shí)都正常。

圖片圖片

好的我們不用關(guān)閉 v1 的進(jìn)程,直接啟動(dòng) v2 的 jar 包,并且啟動(dòng)后,可以一直在 Cool Request 測(cè)試接口時(shí)間內(nèi)的可用程度。

稍等后,就會(huì)看到 v2 代碼已經(jīng)生效,而在這個(gè)過(guò)程中,服務(wù)只有極短的時(shí)間不可用,不會(huì)超過(guò)1秒。

圖片圖片

責(zé)任編輯:武曉燕 來(lái)源: 一安未來(lái)
相關(guān)推薦

2017-04-12 11:15:52

ReactsetState策略

2011-11-04 14:07:20

微軟Hotmail策略

2020-02-10 09:35:18

數(shù)據(jù)中心服務(wù)器技術(shù)

2018-10-24 14:30:30

緩存服務(wù)更新

2025-02-19 10:17:39

2025-03-11 00:55:00

Spring停機(jī)安全

2018-10-19 11:07:02

主流緩存更新

2012-11-21 09:34:58

SaaS應(yīng)用SaaS應(yīng)用集成軟件集成

2022-12-23 08:28:42

策略模式算法

2023-04-13 08:15:47

Redis緩存一致性

2012-02-01 10:29:13

2009-10-30 09:19:43

2024-07-26 07:59:25

2024-09-27 08:25:47

2009-03-09 18:46:11

Windows phoWindows Mob

2017-09-20 09:46:38

Spring BootSpring Clou內(nèi)存

2018-06-21 11:27:06

Windows 7更新停止

2021-07-13 18:42:38

Spring Boot腳手架開(kāi)發(fā)

2010-11-11 14:36:17

MySQL

2015-10-30 09:33:48

ChromeAndroid合一
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)