- 1 SpringBoot零停機滾動更新
- 1.1 引言
- 1.2 單體應用設計思路
- 1.3 單體應用實現程式碼
1 SpringBoot零停機滾動更新
1.1 引言
在個人或者企業伺服器上,總歸有要更新程式碼的時候,普通的做法必須先終止原來程序,因為新程序和老程序埠是一個,新程序在啟動時候,必定會出現埠占用的情況,但是,還有黑科技可以讓兩個SpringBoot程序真正的共用同一個埠,這是另一種解決辦法
那麼就會出現一個問題,如果此時有大量的使用者在訪問,但是程式碼又必須要更新,這時候如果採用上面的做法,那麼必定會導致一段時間內的使用者無法訪問,這段時間還取決於專案啟動速度,那麼在單體應用下,如何解決這種事情?
一種簡單辦法是,新程式碼先用其他埠啟動,啟動完畢後,更改nginx
的轉發地址,nginx
重啟非常快,這樣就避免了大量的使用者訪問失敗,最後終止老程序就可以。但是還是比較麻煩,埠換來換去,即使寫個指令碼,也是比較麻煩,有沒有一種可能,新程序直接啟動,自動處理好這些事情?答案是有的。
1.2 單體應用設計思路
這裡涉及到幾處原始碼類的知識,如下。
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());
}
到這裡所有用到的都準備完畢了,思路也很簡單。
- 判斷埠是否佔用
- 佔用則先透過其他埠啟動
- 等待啟動完畢後終止老程序
- 重新建立容器例項並且關聯
DispatcherServlet
在第三步和第四步之間,速度很快的,這樣就達到了無縫更新程式碼的目的。
1.3 單體應用實現程式碼
@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";
}
}