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