【編者的話】
隨著Docker的發(fā)展,越來越多的應(yīng)用開發(fā)者開始使用Docker。James Strachan寫了一篇有關(guān)Java開發(fā)者如何使用Docker進行輕量級快速開發(fā)的文章。他告訴我們,使用Docker和服務(wù)發(fā)現(xiàn)的機制,可以有效減輕Java運維人員的負擔,進行項目的快速啟動和持續(xù)迭代。
多年來,Java生態(tài)系統(tǒng)一直在使用應(yīng)用服務(wù)器。Java應(yīng)用服務(wù)器(如Servlet Engine、JEE或OSGi)是一個可以作為最小部署單元(如jar/war/ear/bundle等)進行部署和卸載Java代碼的 JVM(Java虛擬機)進程。所以一個JVM進程可以在運行的過程中更換運行在其上的代碼。通常Java應(yīng)用服務(wù)器提供存放文件的目錄或者 REST/JMX 接口來修改正在運行的部署單元(Java代碼)。
由于內(nèi)存資源在過去是相當寶貴的,所以把所有的Java代碼放到同一個JVM中去運行來減少多個進程帶來的內(nèi)存碎片具有重要的意義。
多年來,在Java生產(chǎn)環(huán)境中,通常沒有人真正在運行著的JVM中卸載Java代碼,因為這樣做很容易造成內(nèi)存泄漏(線程、內(nèi)存、數(shù)據(jù)庫鏈接、 socket、正在運行的代碼等導(dǎo)致)。所以在生產(chǎn)環(huán)境中升級應(yīng)用的較好做法是并行地在一個新的應(yīng)用服務(wù)器中啟動應(yīng)用程序;把流量從舊的應(yīng)用實例遷移到新的應(yīng)用實例上,當舊的應(yīng)用實例結(jié)束正在處理的請求時,就可以被停止。
從概念上說是卸載了舊的程序,部署了新的程序;但是實際上是啟動了一個新的進程,并把流量遷移到新的進程上,然后結(jié)束那個舊進程。
目前,有向微服務(wù)發(fā)展的趨勢,每個進程做好一件事。多年來,使用應(yīng)用服務(wù)器的最佳實踐方式,一直都是在每一個JVM中部署盡量少的部署單元。假如你把所有的服務(wù)(部署單元)部署到同一個JVM中;如果要升級這些服務(wù)中的一個,你就要關(guān)閉這個JVM進程,這就會影響到其它的服務(wù)。所以把每個應(yīng)用單獨部署在不同的JVM進程中更安全和敏捷,這樣在任何時候升級一個服務(wù)都不會影響到其他的服務(wù)。
多個獨立的進程比一個龐大的進程更容易監(jiān)控,也更容易了解哪個服務(wù)使用了多少內(nèi)存、網(wǎng)絡(luò)、硬盤和CPU等。
Docker如何帶來改變
Docker容器提供了一種理想的方式來打包應(yīng)用,使得應(yīng)用在Linux機器上部署更加方便;對不同的操作環(huán)境和不同的程序都可以使用同一個Docker鏡像而不需要改變;容器之間彼此隔離,并且通過cgroups對IO、內(nèi)存、CPU等的用量進行限制。所有在 Linux上可以使用的技術(shù)(Java、python、ruby、nodejs、golang等)都可以在Docker容器中很好的運行。
Docker容器最大的優(yōu)點之一就是你可以以重復(fù)的方式在任何機器上同時啟動多個實例,因為這些實例都是基于同一個不變的、可重復(fù)使用的鏡像。每個容器實例都可以把自己的持久狀態(tài)掛在在卷上,但是它們的代碼(甚至配置)都來自同一個不變的鏡像。
所以在Docker上使用Java應(yīng)用服務(wù)器的方式是為應(yīng)用服務(wù)器和你想在生產(chǎn)環(huán)境中運行的部署單元創(chuàng)建一個鏡像。
在升級服務(wù)的時候不再需要在webapps/deploy目錄下刪除掉一個WAR包或者調(diào)用 REST/JMX接口,或者任何其它方式,你只需要創(chuàng)建一個包含新的部署單元的鏡像,并且運行這個鏡像。
此外,Java應(yīng)用服務(wù)器不再需要在運行時部署和卸載新的代碼;不再需要監(jiān)控部署目錄的變化或者監(jiān)聽來自REST/JMX接口的更改部署的請求;只需要在啟動的時候啟動鏡像中的代碼。
所以在Docker的世界中,Java應(yīng)用服務(wù)器的理念(可以部署和卸載程序的動態(tài)JVM)正在逐漸消亡。
在Docker中使用應(yīng)用服務(wù)的最好方式是把它們當作不可變的鏡像;運行在進程中的Java代碼就不再需要經(jīng)常變動。新版本容器的滾動升級就可以在應(yīng)用服務(wù)器之外完成(例如,通過kubernetes滾動升級,然后在容器前使用負載均衡)。
[page]配置管理
自采用應(yīng)用服務(wù)器以后,在Java生態(tài)環(huán)境中,應(yīng)用被創(chuàng)建成一個不可變的二進制部署單元(jars、wars、ears、 bundles等),發(fā)布一次就可以在不同的環(huán)境中使用。為了做到在不同的環(huán)境中運行,我們通常通過應(yīng)用服務(wù)來查找資源(例如,在JEE環(huán)境下使用 JNDI查找)比如查找數(shù)據(jù)庫的位置或者消息代理。所以就會有單獨的配置好的應(yīng)用服務(wù)器集群來部署你的程序(假設(shè)應(yīng)用服務(wù)器都配置正確)。
盡管在不同的操作系統(tǒng),Java版本,應(yīng)用服務(wù)器版本或者不匹配的配置等不同環(huán)境下容易混亂,在初步階段程序可能還正常運行,但是如果不夠仔細的話,生產(chǎn)環(huán)境下可能會運行出錯。
而采用Docker的方法,就是把鏡像不變的理念延伸到操作系統(tǒng)和應(yīng)用服務(wù)器上;所以根據(jù)操作系統(tǒng)、java環(huán)境,應(yīng)用服務(wù)器和部署單元制定的同一個二進制鏡像可以在每一個特定環(huán)境下運行。所以在一個特定環(huán)境下不存在應(yīng)用服務(wù)器配置錯誤的問題,因為同一個二進制鏡像可以在所有環(huán)境下運行。
為了做到這一點,在每一個環(huán)境下都有服務(wù)發(fā)現(xiàn)就顯得極其有用,這使得同一個鏡像在每個環(huán)境下都使用正確的配置并且準確無誤地運行變得簡單。例如,像kubernetes服務(wù)發(fā)現(xiàn)讓在所有環(huán)境使用同一個二進制鏡像并且使用服務(wù)發(fā)現(xiàn)連接數(shù)據(jù)庫、消息中間件變得可行。
總結(jié)
所以,這就意味著Java應(yīng)用服務(wù)器沒用了嗎?在Docker的世界里,確實再也沒有必要在生產(chǎn)環(huán)境中運行著的Java進程中熱部署Java代碼了。但是在開發(fā)過程中,有能力在運行的實例中熱部署一份代碼依舊非常有用。(盡管公平的說,你可以使用像JRebel這樣的工具在Java 應(yīng)用做到同樣的事情,大多數(shù)使用IDE調(diào)試的用戶就用這種方法)
所以我想說,Java應(yīng)用服務(wù)器漸漸變得更像燒錄到固定鏡像中的一個框架,然后在外部云中進行管理(比如通過Kubernetes)。云(如 Kubernetes和Docker)在許多方面接管了很多Java應(yīng)用服務(wù)器原先做的功能,并且新鏡像的滾動升級對所有技術(shù)來說都是需要的(包括 java/golang/nodejs/python/ruby等等)。
盡管Java用戶仍然想要Java應(yīng)用服務(wù)器提供的一些服務(wù),如servlet引擎、依賴代碼注入、事務(wù)處理、消息處理等等。但是你再也無需動態(tài)的在一個運行著的Java虛擬機中清理原先部署上去的代碼了,這樣你就可以輕易的在Java應(yīng)用中植入一個servlet引擎。像Spring Boot這樣的方法向你展示了如何只通過依賴代碼注入和一個扁平化的類載入器,就足以勝任大多數(shù)應(yīng)用服務(wù)器的功能。
作為一個開發(fā)者,在用Java應(yīng)用服務(wù)器工作時遇到最大問題之一就在于載入Java類時的復(fù)雜性,我相信在這一點上我們都討厭Java的類載入器問題。
盡管你可以通過使用BOM文件在項目中導(dǎo)入一個maven構(gòu)建的依賴關(guān)系來修復(fù)這些問題,但是為JEE服務(wù)開發(fā)者們屏蔽jar包的具體實現(xiàn)依然有一定的價值,你無需再搞清復(fù)雜的類載入器的樹關(guān)系或者圖關(guān)系。就算類路徑關(guān)系簡單,你還有可能面臨版本沖突問題。所以如果有辦法隔離類載入器會非常有用。不過有時候使用一個jar包的不同版本也意味著編碼上可能有些問題,是不是意外著是時候把代碼重構(gòu)一下,變成兩個獨立的服務(wù),這樣就可以有一個簡潔漂亮扁平的類載入器?
如果一個Java應(yīng)用服務(wù)器進程現(xiàn)在只啟動了一個靜態(tài)已知的Java代碼集合,應(yīng)用服務(wù)器的想法會變成一個幫助你進行代碼注入以及包含你所需模塊服務(wù)的方法,這就聽起來更像是一個框架而非我們原本意外的一個Java 應(yīng)用服務(wù)器。
許多Java開發(fā)者學會了如何使用應(yīng)用服務(wù)器,并且在Docker的世界中仍會繼續(xù)使用,這一點很好。但是與此同時我也看到,他們對此的使用真在消減,因為許多應(yīng)用服務(wù)器本來在過去幫我們完成的事情,現(xiàn)在Docker、Kubernetes及相關(guān)框架可以用一種更簡單、更高效的方式幫我們完成。
Docker和云給我們帶來的一個巨大的好處就是,開發(fā)者可以選擇他們想要使用的技術(shù),他們可以為合適的工作選擇適當?shù)墓ぞ?,并且可以把他們的技術(shù)用同樣的方法進行管理和提高給用戶,無論使用的是何種語言何種框架。你可以在最初使用你知道的技術(shù),隨著時代的變化遷移到更輕量級的替代中。
在fabric8項目中,我們確實不知道你想要使用何種應(yīng)用服務(wù)器或者框架,所以Camel Boot、CDI 、Spring Boot 、 Karaf 、Tomcat 、 Vertx、Wildfly這些我們在quickstarts中都支持。感謝Kubernetes,我們可以提高同樣的供應(yīng)、管理以及工具化經(jīng)驗,無論你選擇的應(yīng)用服務(wù)器或框架到底是什么。舉個例子,如果你使用fabric8 V2開始一個新的Camel項目,我們強烈建議你使用Camel Boot工具或者嘗試使用Spring Boot Quickstarts。
我越來越多的看見Java用戶選擇像Camel Boot、CDI、Dropwizard、Vertx或者Spring Boot 這些更輕量級的框架,并且隨著時間越來越少使用Java應(yīng)用服務(wù)器。盡管我們依然需要使用依賴注入和框架。