<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          代碼更新不停機(jī):SpringBoot應(yīng)用實(shí)現(xiàn)零停機(jī)更新的新質(zhì)生產(chǎn)力

          共 11660字,需瀏覽 24分鐘

           ·

          2024-08-08 08:00

             
          點(diǎn)擊關(guān)注公眾號(hào):互聯(lián)網(wǎng)架構(gòu)師,后臺(tái)回復(fù) 2T獲取2TB學(xué)習(xí)資源!

          上一篇:2T架構(gòu)師學(xué)習(xí)資料干貨分享

          大家好,我是互聯(lián)網(wǎng)架構(gòu)師!
          在個(gè)人或者企業(yè)服務(wù)器上,總歸有要更新代碼的時(shí)候,普通的做法必須先終止原來進(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è)問題,如果此時(shí)有大量的用戶在訪問,但是你的代碼又必須要更新,這時(shí)候如果采用上面的做法,那么必定會(huì)導(dǎo)致一段時(shí)間內(nèi)的用戶無法訪問,這段時(shí)間還取決于你的項(xiàng)目啟動(dòng)速度,那么在單體應(yīng)用下,如何解決這種事情?
          一種簡(jiǎn)單辦法是,新代碼先用其他端口啟動(dòng),啟動(dòng)完畢后,更改nginx的轉(zhuǎn)發(fā)地址,nginx重啟非常快,這樣就避免了大量的用戶訪問失敗,最后終止老進(jìn)程就可以。
          但是還是比較麻煩,端口換來換去,即使你寫個(gè)腳本,也是比較麻煩,有沒有一種可能,新進(jìn)程直接啟動(dòng),自動(dòng)處理好這些事情?

          答案是有的。

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

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

          1. SpringBoot內(nèi)嵌Servlet容器的原理是什么
          2. DispatcherServlet是如何傳遞給Servlet容器的
          先看第一個(gè)問題,用Tomcat來說,這個(gè)首先得Tomcat本身支持,如果Tomcat不支持內(nèi)嵌,SpringBoot估計(jì)也沒辦法,或者可能會(huì)另找出路。關(guān)注:互聯(lián)網(wǎng)架構(gòu)師,回復(fù)關(guān)鍵詞:2T 獲取阿里內(nèi)部Java性能調(diào)優(yōu)手冊(cè)!
          Tomcat本身有一個(gè)Tomcat類,沒錯(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容器依賴,通過下面代碼可以獲取創(chuàng)建對(duì)應(yīng)容器的工廠,拿Tomcat來說,創(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è)問題,DispatcherServlet是如何傳遞給Servlet容器的。
          SpringBoot并不像上面Tomcat的例子一樣簡(jiǎn)單的通過tomcat.addServlet把DispatcherServlet傳遞給Tomcat,而是通過個(gè)Tomcat主動(dòng)回調(diào)來完成的,具體的回調(diào)通過ServletContainerInitializer接口協(xié)議,它允許我們動(dòng)態(tài)地配置Servlet、過濾器。
          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ì)通過此接口回調(diào)到SpringBoot內(nèi)部,SpringBoot在內(nèi)部會(huì)調(diào)用所有ServletContextInitializer集合來初始化,
          而getWebServer的參數(shù)正好就是一堆ServletContextInitializer集合。
          那么這時(shí)候還有一個(gè)問題,怎么獲取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. 占用則先通過其他端口啟動(dòng)
          3. 等待啟動(dòng)完畢后終止老進(jìn)程
          4. 重新創(chuàng)建容器實(shí)例并且關(guān)聯(lián)DispatcherServlet
          在第三步和第四步之間,速度很快的,這樣就達(dá)到了無縫更新代碼的目的。

          實(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.classnewArgs);
                  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è)試

          我們先寫一個(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è)過程中,服務(wù)只有極短的時(shí)間不可用,不會(huì)超過1秒。

          妙不妙?


          最后,關(guān)注公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師,在后臺(tái)回復(fù):2T,可以獲取我整理的 Java 系列面試題和答案,非常齊全。


          正文結(jié)束


          推薦閱讀 ↓↓↓

          1.JetBrains 如何看待自己的軟件在中國(guó)被頻繁破解?

          2.無意中發(fā)現(xiàn)了一位清華妹子的資料庫(kù)!

          3.程序員一般可以從什么平臺(tái)接私活?

          4.40歲,剛被裁,想說點(diǎn)啥。

          5.為什么國(guó)內(nèi) 996 干不過國(guó)外的 955呢?

          6.中國(guó)的鐵路訂票系統(tǒng)在世界上屬于什么水平?                        

          7.15張圖看懂瞎忙和高效的區(qū)別!


          瀏覽 86
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲精品一二三四区 | 91超碰在线观看 | 看A片视频 | 操逼免费无码 | 淫香淫色综合 |