微服務應用容器化場景中常見問題總結
簡介:雲原生技術棧是下一代應用轉型的必然選擇,它包含了微服務架構,DevOps和容器技術。對於微服務架構來說,應用是「第一公民」,他逐漸蠶食原來底層軟體或者硬體的功能,例如服務註冊與發現以及負載均衡;而對於容器平台來說,容器是「第一公民」,他提供了容器註冊與發現和負載均衡,同時容器技術將應用和外面的世界做了隔離,這樣很多應用運行的假設就會失效。那當微服務應用運行在容器中的時候,我們會遇到哪些常見問題?我們又該如何解決呢?
企業應用在向微服務架構轉型的過程中,微服務如何劃分是最基本的問題。我們可以通過業務架構的梳理來理解業務,並同時使用領域設計的方法進行微服務的設計。其次,我們需要做系統設計,系統設計會更關注性能、可用性、可擴展性和安全性等;當然,我們還需要做介面設計,定義微服務之間的契約。
Java在企業中被廣泛應用,當前,選擇Spring Cloud作為微服務開發框架成為一個廣泛的趨勢。微服務架構的複雜性需要容器技術來支撐,應用需要容器化,並使用CaaS平台來支撐微服務系統的運行。
本文探討的主題是來自於企業級Java應用在容器化過程中遇到的基礎問題(與計算和網路相關),希望以小見大探討微服務轉型過程中遇到的挑戰。
容器內存限制問題
讓我們來看一次事故,情況如下:當一個Java應用在容器中執行的時候,某些情況下會容器會莫名其妙退出。
1.Dockfile如下:
2.運行命令
3.日誌分析
執行docker logs container_id,出現了java.lang.OutOfMemoryError。
4.問題初步分析
因為我們在執行容器的時候,對內存做了限制,同時在Java啟動參數重,沒有對內存使用做限制,是不是這個原因導致了容器被幹掉呢?
當我們執行沒有任何參數設置(如上面的myapp)的 Java 應用程序時,JVM 會自動調整幾個參數,以便在執行環境中具有最佳性能,但是在使用過程中我們逐步發現,如果讓 JVM ergonomics (即JVM人體工程學,用於自動選擇和行為調整)對垃圾收集器、堆大小和運行編譯器使用默認設置值,運行在容器中的 Java 進程會與我們的預期表現嚴重不符(除了上訴的問題)。
首先我們來做一個實驗:
1.在我本機(Mac)上執行docker info命令
2.執行docker run -it -m=100M –memory-swap=100M debian cat /proc/meminfo
雖然我們啟動容器的時候,指定了容器的內存限制,但是從容器內部看到的內存信息和主機上的內存信息幾乎一致。
因此我們找到原因了:docker switches(-m,-memory和-memory-swap) 在進程超過限制的情況下,會指示 Linux 內核殺死該進程。但 JVM 是完全不知道限制,因此會很有可能超出限制。在進程超過限制的時候,容器進程就被殺掉了!
解決問題的一種思路就是使用JVM參數來限制內存的使用,這個需要根據JVM內存參數的定義來做巧妙的設置,但是幸運的是從Java SE 8u131和JDK 9開始,Java SE開始支持Docker CPU和內存限制。
具體實現邏輯如下: 如果-XX:ParalllelGCThreads或-XX:CICompilerCount未指定為命令行選項,則JVM將Docker CPU限制應用於JVM在系統上看到的CPU數。然後JVM將調整GC線程和JIT編譯器線程的數量,就像它在裸機系統上運行一樣,其CPU數量設置為Docker CPU限制。如果-XX:ParallelGCThreads或-XX:CICompilerCount指定為JVM命令行選項,並且指定了Docker CPU限制,則JVM將使用-XX:ParallelGCThreads和-XX:CICompilerCount值。
對於Docker內存限制、最大Java堆的設置還有一些工作要做。要在沒有通過-Xmx設置最大Java堆的情況下告知JVM要注意Docker內存限制,需要兩個JVM命令行選項:
-XX:+ UnlockExperimentalVMOptions是必需的,因為在將來版本中,Docker內存限制的透明標識是目標。當使用這兩個JVM命令行選項,並且未指定-Xmx時,JVM將查看Linux cgroup配置,這是Docker容器用於設置內存限制的方式,以透明地指定最大Java堆大小。 Docker容器也使用Cgroups配置來執行CPU限制。
服務註冊與發現
在微服務的場景中,運行的微服務實例將會達到成百上千個,同時微服務實例存在失效,並在其他機器上啟動以保證服務可用性的場景,因此用IP作為微服務訪問的地址會存在需要經常更新的需求。
服務註冊與發現就應用而生,當微服務啟動的時候,它會將自己的訪問Endpoint信息註冊到註冊中心,以便當別的服務需要調用的時候,能夠從註冊中心獲得正確的Endpoint。
如果用Java技術棧開發微服務應用,Spring Cloud(https://spring.io/)將會是大家首選的微服務開發框架。Spring Cloud Service Discovery就提供了這樣的能力,它底層可以使用Eureka(Netflix),ZooKeeper,ETCD。這裡我們以使用Spring Cloud Eureka為例(使用Spring Cloud Eureka的文檔可以參考http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/1.3.1.RELEASE/)。
在本機運行如下命令:
該應用會將自己的信息註冊到本地Eureka。我們可以通過打開Eureka的Dashboard,看到如下信息:
圖 1 物理機上運行Eureka Client
我們可以看到應用的如下信息:
但是我們在容器化場景中,遇到了問題。Dockerfile如下:
假設我們現在在本機使用Docker run命令執行容器化discovery-client-demo,命令行如下:
前後兩個容器的區別在於,第二個指示應用註冊的使用IP,而默認是Hostname。
打開Eureka的Dashboard,我們可以看到如下信息:
圖 2 容器中運行Eureka Client
調用Eureka的Apps介面,我們可以看到更加詳細的信息:
圖 3 註冊應用的詳細信息
這個時候我們發現了問題,兩個應用註冊的信息中,Hostname, IP和埠都使用了容器內部的主機名,IP和埠(如8080)。這就意味著在Port-Mapping的場景下,應用註冊到Eureka的時候,註冊信息無法被第三方應用使用。
解決這個問題的第一個思路如下:
圖 4 對Eureka進行擴展
我們對Eureka進行擴展,對所有客戶端過來的請求進行攔截,然後從Docker Daemon中拿到正確的外部訪問信息來進行替換,從而確保註冊到Eureka中的信息是外部能夠訪問的。
該方案需要做的工作:
需要同時對Eureka Client和Server改造,能夠在Client上報的信息中加入container ID等信息。
Eureka服務端需要加入一個過濾器,需要對所有的註冊請求進行處理,根據container ID將內部hostname,IP和埠改為外部可以訪問的IP和埠。
該方案的挑戰:
因為只有容器被創建後才有ID,無法在啟動參數中指定,因此應用在容器啟動的時候,無法拿到容器的ID。
對於Eureka Server和Client的改造,無法貢獻回社區,因此需要自己維護版本,存在極大的風險。
AWS的EC2 提供了實例元數據和用戶數據的API,它可以通過REST API的方式供運行在EC2內部的應用使用。API形式如下:
http://169.254.169.254/latest/meta-data/
其中IP地址是固定的一個IP地址。
因此我們也可以提供一個類似的元數據和用戶數據API,供容器內部的應用調用。同時為了減少對應用的侵入,我們可以在應用啟動之前執行一個腳本來獲取相應的信息,並設置到環境變數中,供應用啟動後讀取並使用,流程如下:
圖 5 方案流程
總結
本文總結了使用Java開發的企業級應用在向微服務架構應用轉型過程中,在容器化運行過程中遇到的常見問題、原因分析及解決方法。總結下來,由於容器技術的內在特性,我們需要對應用做一些改造,同時相應的工具如JVM也需要對容器有更好的本地支持。
作者簡介:
王天青, DaoCloud首席架構師,負責用新技術幫助企業做下一代應用轉型。
夏岩, DaoClozud微服務架構師,幫助企業進行微服務架構轉型。


TAG:CSDN雲計算 |