將Java 應用容器化改造並遷移到Kubernetes 平台

為了能夠適應容器雲平臺的管理模式和管理理念,應用系統需要完成容器化的改造過程。對於新開發的應用,建議直接基於微服務架構進行容器化的應用開發;對於已經運行多年的傳統應用系統,也應該逐步將其改造成能夠部署到容器雲平臺上的容器化應用。本文針對傳統的Java 應用,對如何將應用進行容器化改造和遷移到Kubernetes 平臺上進行說明。

要將傳統Java 應用改造遷移到Kubernetes 平臺上運行,通常要經過以下幾個步驟。

(1)進行應用代碼改造,要考慮配置文件、多實例部署下的分佈式架構問題,並對程序代碼和架構做出相應的改造。

(2)進行容器化改造,選擇合適的基礎鏡像並打包生成新的應用鏡像,使得應用能以容器方式部署、運行。

(3)進行Kubernetes 建模與部署,採用合適的Kubernetes 資源對象建模Java 應用,最終發佈到Kubernetes 平臺上實現應用的自動化運維。

接下來以一個傳統的Java 應用改造遷移過程為例,來說明上述步驟中的細節。

1 Java 應用的容器化改造遷移

我們的目標是搭建一個簡單的學員分數管理系統(Study Application),應用界面與架構如下圖。

將Java 應用容器化改造並遷移到Kubernetes 平臺

Study Application 是一個典型的J2EE 系統,為了方便理解,並沒有採用額外的框架技術,而是採用了MySQL 數據庫,將JSP 作為Web 頁面,並通過JDBC 進行數據庫操作,整個系統以標準方式部署在Tomcat 的webapp 目錄下。

下圖所示是Study Application的目錄結構與說明。

將Java 應用容器化改造並遷移到Kubernetes 平臺

下面是在index.jsp 中訪問數據庫的關鍵代碼, 數據庫連接的配置信息被放在jdbc.properties 屬性文件中,便於在不同的環境下修改:

將Java 應用容器化改造並遷移到Kubernetes 平臺

Class.forName("com.mysql.jdbc.Driver");

java.util.Properties pps = new java.util.Properties();

pps.load(new java.io.FileInputStream("jdbc.properties"));

String ip=pps.getProperty("mysql_ip");

String user=pps.getProperty("user");

String password=pps.getProperty("password");

System.out.println("Connecting to database...");

conn =

java.sql.DriverManager.getConnection("jdbc:mysql://"+ip+":3306"+"?useUnicode=tru

e&characterEncoding=UTF-8", user,password)

stmt = conn.createStatement();

String sql = "show databases like 'HPE_APP'";

rs =stmt.executeQuery(sql);

我們知道,應用在以容器化運行以後,是不建議進入容器裡修改配置文件的(在多實例情況下很難保持配置文件同步更新),因此,需要修改從jdbc.properties 屬性文件中獲取數據庫連接的以上代碼,根據容器環境的要求,將其改為從環境變量中獲取,改造後的代碼如下:

將Java 應用容器化改造並遷移到Kubernetes 平臺

String ip=System.getenv("mysql_ip");

String user=System.getenv("user");

String password=System.getenv("password");

改造後的代碼基本達到了容器化的要求,但對於一個完整的應用來說,由於還存在用戶Session 會話保持的問題,因此還需要實現分佈式的Session 會話機制,才能做到多實例部署,此時可以考慮採用Spring Session 框架來改造、升級我們的單體應用。對於大部分RESTful 服務,由於不需要會話保持功能,因此可以直接多副本部署,多個實例可以同時提供服務。

2 Java 應用的容器鏡像構建

接下來,我們需要將自己的Java 應用打包為Docker 鏡像,以容器方式啟動並提供服務。在打包鏡像時,需要注意以下幾個關鍵問題。

(1)需要注意基礎鏡像的選擇問題。選擇基礎鏡像的兩個原則:標準化與精簡化。儘可能選擇Docker 官方發佈的基礎鏡像,這些基礎鏡像通常符合標準化與精簡化這兩個目標。比如,它們都有Dockerfile 源文件,我們可以獲知此鏡像是如何製作的,並可以在此基礎上實現諸如軟件版本、性能優化、日誌及安全等方面的特殊定製,然後打包為公司級別的內部標準鏡像,供各個項目使用。

(2)需要注意業務進程的啟動方式。與在物理機上將自己的程序放到後臺運行的方式不同,在容器化時,我們需要將自己的業務進程放到前臺運行。這樣一來,當業務進程由於某種原因而停止時,容器也隨之銷燬,我們就能及時觀察到這種嚴重故障,並做出相應的行動來恢復系統。目前有一些系統在容器化的過程中採用了supervisord 這樣的工具,將業務的主進程和輔助進程放到後臺啟動,並交給supervisord 監管,這種做法雖然在一定程度上也能實現自動重啟故障進程的目標,但它將問題隱藏得更深,即使業務進程由於特殊故障始終無法重啟成功,運維人員也發現不了問題,因此不建議採用這種方式啟動業務進程。

(3)需要注意程序的日誌輸出問題。在物理機上運行業務進程時,我們通常會把程序日誌輸出到指定的文件中,以便更好地排查故障。但在容器化以後,我們需要改變這種做法,將程序的日誌直接輸出在容器的屏幕上(或者說控制檯Console 上),此時Docker 會將這些輸出日誌存放到容器之外的特定文件中,第三方的日誌收集工具(例如Elasticsearch)就可以方便採集這些日誌並實現集中化的日誌搜索和分析功能。此外,Docker 也提供了統一的log 命令來查看容器的日誌,這推進了系統運維的標準化。Java 中常用的Log4j 及Slf4j日誌框架都支持把日誌輸出到控制檯的配置方式,在打包應用時,需要對日誌的配置文件做出相應的修改。

(4)需要注意文件操作的問題。當業務進程運行在物理機上時,它看到的文件系統就是物理機的文件系統;但當業務進程運行在容器中時,它所訪問的文件系統就是一種特殊的、被隔離的、分層模式的虛擬文件系統,在這種情況下,頻繁進行I/O 操作的性能比較低。為了解決這個問題,容器可以使用Volume 將頻繁進行操作的目錄映射到容器外部(通常是物理機上);同時,Volume 也是容器與外部交換文件的重要工具,因此在製作鏡像和運行容器時,需要考慮Volume 映射的問題,對於在程序運行過程中產生的大量臨時文件和被頻繁讀寫的文件,或者在需要跟外界交換文件時,可以選擇掛載Volume。

下圖是Study Application 打包鏡像的示意圖及對應的Dockerfile 源碼。

將Java 應用容器化改造並遷移到Kubernetes 平臺

Study Application 的鏡像繼承了tomcat:9-alpine 這個官方的基礎鏡像,這個鏡像基於Alpine Linux,如果對比一下,我們會發現,基於alpine 的鏡像不到5MB,而基於Ubuntu或CentOS 的鏡像都在100MB 以上。此外,從Study Application 的Dockerfile 來看,製作Java 類型應用的Docker 鏡像是很方便的一件事,通常只需幾行代碼。

3 在Kubernetes 上建模與部署

在應用容器化後,就可以在Kubernetes 上建模與部署了,在建模的過程中,我們需要考慮一些關鍵問題,這些問題及其答案如下。

(1)將業務進程建模為Pod 還是RC?

對於這個問題,最重要的判斷依據是該進程提供的是有狀態服務還是無狀態服務。對於無狀態服務,比如大多數REST 接口的服務,通常是可以在任意節點上啟動並提供服務的,例如我們這裡的Web 應用程序就符合無狀態服務。但對於有狀態服務,比如MySQL服務,我們通常不能這麼做,因為它依賴本地存儲的數據庫文件。對於有狀態服務,我們通常只能將業務進程建模為Pod,這是因為RC 控制的Pod 實例可以從一臺節點飄到另一臺節點上,如果我們能夠通過共享存儲解決Pod 的狀態問題,則也可以把某些有狀態服務的進程建模為RC,這種做法與StatefulSet 很類似。

(2)我們是否需要在Pod 的基礎上,繼續建模對應的Service?

這主要取決於此Pod 是否會被其他業務進程(或終端用戶)所訪問,對於不會被其他業務進程所訪問的Pod,我們無須建模對應的Service。實際上,在一個分佈式系統中,大多數進程都會被建模為Service 並對應一個微服務,如果某個服務還需要被終端用戶訪問,則往往還需要“導出”外網訪問地址,比如NodePort 端口。對於無須外部訪問的Service,還可以考慮建模為Headless Service,在這種情況下,該Service 不會分配一個虛擬的ClusterIP,通信效率更高。

(3)是否需要考慮應用的數據存儲問題?

如果只是本機存儲,則可以直接使用Kubernetes Volume 資源對象;如果希望有遠程存儲功能,則可以考慮使用PV(Persistent Volume)。這樣一來,不管Pod 被調度到哪臺機器,都可以繼續訪問原來的存儲數據。如果希望系統自動管理共享存儲的空間,則可以考慮建模對應的StorageClass。

(4)是否需要考慮應用的配置問題?

我們知道,在幾乎所有應用開發中,都會涉及配置文件的管理問題,比如StudyApplication 中的數據庫配置信息,常見的互聯網應用還有緩存中間件、消息隊列、全文檢索等一系列中間件的配置文件。而在分佈式情況下,發佈在多個節點上的Pod 副本都需要訪問同一份配置文件,這也加大了配置管理的難度,為此業內的一些大公司專門開發了自己的一套配置管理中心,如360 的Qcon、百度的Disconf 等,但這些解決方案都比較複雜而且有侵入性。Kubernetes 則提供了無侵入的更簡單的方案,這就是ConfigMap,我們可以把任意數量的配置文件放入ConfigMap 中,實現集中化管理,然後通過環境變量的方式將配置數據傳遞到Pod 裡,或者通過Volume 方式掛載到Pod 內。

在Study Application 中,Web 應用在Kubernetes 上的建模如圖6-4 所示。我們通過定義一個RC 來控制Web 的Pod 實例,數據庫連接信息則通過環境變量傳遞到Pod 裡,然後定義一個Service,並且通過NodePort 方式暴露到集群外供用戶訪問,即可完成這個Java應用的容器化改造工作。


本文選自新書《Kubernetes權威指南:企業級容器雲實戰

本書通過全新的視角,針對容器雲領域現下的熱點和技術難點,給出了基於Kubernetes的企業級容器雲落地指南,為企業傳統IT轉型和業務上雲提供助力。


分享到:


相關文章: