減少使用Java應用服務器,迎接Docker容器(還沒怎麼看懂)

隨着Docker的發展,越來越多的應用開發者開始使用Docker。James Strachan寫了一篇有關Java開發者如何使用Docker進行輕量級快速開發的文章。他告訴我們,使用Docker和服務發現的機制,可以有效減輕Java運維人員的負擔,進行項目的快速啓動和持續迭代。

多年來,Java生態系統一直在使用應用服務器。Java應用服務器(如Servlet Engine、JEE或OSGi)是一個可以作爲最小部署單元(如jar/war/ear/bundle等)進行部署和卸載Java代碼的JVM(Java虛擬機)進程。所以一個JVM進程可以在運行的過程中更換運行在其上的代碼。通常Java應用服務器提供存放文件的目錄或者REST/JMX 接口來修改正在運行的部署單元(Java代碼)。

由於內存資源在過去是相當寶貴的,所以把所有的Java代碼放到同一個JVM中去運行來減少多個進程帶來的內存碎片具有重要的意義。

多年來,在Java生產環境中,通常沒有人真正在運行着的JVM中卸載Java代碼,因爲這樣做很容易造成內存泄漏(線程、內存、數據庫鏈接、socket、正在運行的代碼等導致)。所以在生產環境中升級應用的較好做法是並行地在一個新的應用服務器中啓動應用程序;把流量從舊的應用實例遷移到新的應用實例上,當舊的應用實例結束正在處理的請求時,就可以被停止。

從概念上說是卸載了舊的程序,部署了新的程序;但是實際上是啓動了一個新的進程,並把流量遷移到新的進程上,然後結束那個舊進程。

目前,有向微服務發展的趨勢,每個進程做好一件事。多年來,使用應用服務器的最佳實踐方式,一直都是在每一個JVM中部署儘量少的部署單元。假如你把所有的服務(部署單元)部署到同一個JVM中;如果要升級這些服務中的一個,你就要關閉這個JVM進程,這就會影響到其它的服務。所以把每個應用單獨部署在不同的JVM進程中更安全和敏捷,這樣在任何時候升級一個服務都不會影響到其他的服務。

多個獨立的進程比一個龐大的進程更容易監控,也更容易瞭解哪個服務使用了多少內存、網絡、硬盤和CPU等。
Docker如何帶來改變

Docker容器提供了一種理想的方式來打包應用,使得應用在Linux機器上部署更加方便;對不同的操作環境和不同的程序都可以使用同一個Docker鏡像而不需要改變;容器之間彼此隔離,並且通過cgroups對IO、內存、CPU等的用量進行限制。所有在Linux上可以使用的技術(Java、python、ruby、nodejs、golang等)都可以在Docker容器中很好的運行。

Docker容器最大的優點之一就是你可以以重複的方式在任何機器上同時啓動多個實例,因爲這些實例都是基於同一個不變的、可重複使用的鏡像。每個容器實例都可以把自己的持久狀態掛在在捲上,但是它們的代碼(甚至配置)都來自同一個不變的鏡像。

所以在Docker上使用Java應用服務器的方式是爲應用服務器和你想在生產環境中運行的部署單元創建一個鏡像。

在升級服務的時候不再需要在webapps/deploy目錄下刪除掉一個WAR包或者調用 REST/JMX接口,或者任何其它方式,你只需要創建一個包含新的部署單元的鏡像,並且運行這個鏡像。

此外,Java應用服務器不再需要在運行時部署和卸載新的代碼;不再需要監控部署目錄的變化或者監聽來自REST/JMX接口的更改部署的請求;只需要在啓動的時候啓動鏡像中的代碼。

所以在Docker的世界中,Java應用服務器的理念(可以部署和卸載程序的動態JVM)正在逐漸消亡。

在Docker中使用應用服務的最好方式是把它們當作不可變的鏡像;運行在進程中的Java代碼就不再需要經常變動。新版本容器的滾動升級就可以在應用服務器之外完成(例如,通過kubernetes滾動升級,然後在容器前使用負載均衡)。
配置管理

自採用應用服務器以後,在Java生態環境中,應用被創建成一個不可變的二進制部署單元(jars、wars、ears、bundles等),發佈一次就可以在不同的環境中使用。爲了做到在不同的環境中運行,我們通常通過應用服務來查找資源(例如,在JEE環境下使用JNDI查找)比如查找數據庫的位置或者消息代理。所以就會有單獨的配置好的應用服務器集羣來部署你的程序(假設應用服務器都配置正確)。

儘管在不同的操作系統,Java版本,應用服務器版本或者不匹配的配置等不同環境下容易混亂,在初步階段程序可能還正常運行,但是如果不夠仔細的話,生產環境下可能會運行出錯。

而採用Docker的方法,就是把鏡像不變的理念延伸到操作系統和應用服務器上;所以根據操作系統、java環境,應用服務器和部署單元制定的同一個二進制鏡像可以在每一個特定環境下運行。所以在一個特定環境下不存在應用服務器配置錯誤的問題,因爲同一個二進制鏡像可以在所有環境下運行。

爲了做到這一點,在每一個環境下都有服務發現就顯得極其有用,這使得同一個鏡像在每個環境下都使用正確的配置並且準確無誤地運行變得簡單。例如,像kubernetes服務發現讓在所有環境使用同一個二進制鏡像並且使用服務發現連接數據庫、消息中間件變得可行。
總結

所以,這就意味着Java應用服務器沒用了嗎?在Docker的世界裏,確實再也沒有必要在生產環境中運行着的Java進程中熱部署Java代碼了。但是在開發過程中,有能力在運行的實例中熱部署一份代碼依舊非常有用。(儘管公平的說,你可以使用像JRebel這樣的工具在Java應用做到同樣的事情,大多數使用IDE調試的用戶就用這種方法)

所以我想說,Java應用服務器漸漸變得更像燒錄到固定鏡像中的一個框架,然後在外部雲中進行管理(比如通過Kubernetes)。雲(如Kubernetes和Docker)在許多方面接管了很多Java應用服務器原先做的功能,並且新鏡像的滾動升級對所有技術來說都是需要的(包括java/golang/nodejs/python/ruby等等)。

儘管Java用戶仍然想要Java應用服務器提供的一些服務,如servlet引擎、依賴代碼注入、事務處理、消息處理等等。但是你再也無需動態的在一個運行着的Java虛擬機中清理原先部署上去的代碼了,這樣你就可以輕易的在Java應用中植入一個servlet引擎。像Spring Boot這樣的方法向你展示瞭如何只通過依賴代碼注入和一個扁平化的類載入器,就足以勝任大多數應用服務器的功能。

作爲一個開發者,在用Java應用服務器工作時遇到最大問題之一就在於載入Java類時的複雜性,我相信在這一點上我們都討厭Java的類載入器問題。

儘管你可以通過使用BOM文件在項目中導入一個maven構建的依賴關係來修復這些問題,但是爲JEE服務開發者們屏蔽jar包的具體實現依然有一定的價值,你無需再搞清複雜的類載入器的樹關係或者圖關係。就算類路徑關係簡單,你還有可能面臨版本衝突問題。所以如果有辦法隔離類載入器會非常有用。不過有時候使用一個jar包的不同版本也意味着編碼上可能有些問題,是不是意外着是時候把代碼重構一下,變成兩個獨立的服務,這樣就可以有一個簡潔漂亮扁平的類載入器?

如果一個Java應用服務器進程現在只啓動了一個靜態已知的Java代碼集合,應用服務器的想法會變成一個幫助你進行代碼注入以及包含你所需模塊服務的方法,這就聽起來更像是一個框架而非我們原本意外的一個Java 應用服務器。

許多Java開發者學會了如何使用應用服務器,並且在Docker的世界中仍會繼續使用,這一點很好。但是與此同時我也看到,他們對此的使用真在消減,因爲許多應用服務器本來在過去幫我們完成的事情,現在Docker、Kubernetes及相關框架可以用一種更簡單、更高效的方式幫我們完成。

Docker和雲給我們帶來的一個巨大的好處就是,開發者可以選擇他們想要使用的技術,他們可以爲合適的工作選擇適當的工具,並且可以把他們的技術用同樣的方法進行管理和提高給用戶,無論使用的是何種語言何種框架。你可以在最初使用你知道的技術,隨着時代的變化遷移到更輕量級的替代中。

在fabric8項目中,我們確實不知道你想要使用何種應用服務器或者框架,所以Camel Boot、CDI 、Spring Boot 、 Karaf 、Tomcat 、 Vertx、Wildfly這些我們在quickstarts中都支持。感謝Kubernetes,我們可以提高同樣的供應、管理以及工具化經驗,無論你選擇的應用服務器或框架到底是什麼。舉個例子,如果你使用fabric8 V2開始一個新的Camel項目,我們強烈建議你使用Camel Boot工具或者嘗試使用Spring Boot Quickstarts。

我越來越多的看見Java用戶選擇像Camel Boot、CDI、Dropwizard、Vertx或者Spring Boot 這些更輕量級的框架,並且隨着時間越來越少使用Java應用服務器。儘管我們依然需要使用依賴注入和框架。

原文鏈接:the decline of Java application servers when using docker containers(翻譯:房偉利)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章