微服务应用容器化场景中常见问题总结
簡介:云原生技術(shù)棧是下一代應(yīng)用轉(zhuǎn)型的必然選擇,它包含了微服務(wù)架構(gòu),DevOps和容器技術(shù)。對于微服務(wù)架構(gòu)來說,應(yīng)用是“第一公民”,他逐漸蠶食原來底層軟件或者硬件的功能,例如服務(wù)注冊與發(fā)現(xiàn)以及負(fù)載均衡;而對于容器平臺(tái)來說,容器是“第一公民”,他提供了容器注冊與發(fā)現(xiàn)和負(fù)載均衡,同時(shí)容器技術(shù)將應(yīng)用和外面的世界做了隔離,這樣很多應(yīng)用運(yùn)行的假設(shè)就會(huì)失效。那當(dāng)微服務(wù)應(yīng)用運(yùn)行在容器中的時(shí)候,我們會(huì)遇到哪些常見問題?我們又該如何解決呢?
企業(yè)應(yīng)用在向微服務(wù)架構(gòu)轉(zhuǎn)型的過程中,微服務(wù)如何劃分是最基本的問題。我們可以通過業(yè)務(wù)架構(gòu)的梳理來理解業(yè)務(wù),并同時(shí)使用領(lǐng)域設(shè)計(jì)的方法進(jìn)行微服務(wù)的設(shè)計(jì)。其次,我們需要做系統(tǒng)設(shè)計(jì),系統(tǒng)設(shè)計(jì)會(huì)更關(guān)注性能、可用性、可擴(kuò)展性和安全性等;當(dāng)然,我們還需要做接口設(shè)計(jì),定義微服務(wù)之間的契約。
Java在企業(yè)中被廣泛應(yīng)用,當(dāng)前,選擇Spring Cloud作為微服務(wù)開發(fā)框架成為一個(gè)廣泛的趨勢。微服務(wù)架構(gòu)的復(fù)雜性需要容器技術(shù)來支撐,應(yīng)用需要容器化,并使用CaaS平臺(tái)來支撐微服務(wù)系統(tǒng)的運(yùn)行。
本文探討的主題是來自于企業(yè)級Java應(yīng)用在容器化過程中遇到的基礎(chǔ)問題(與計(jì)算和網(wǎng)絡(luò)相關(guān)),希望以小見大探討微服務(wù)轉(zhuǎn)型過程中遇到的挑戰(zhàn)。
容器內(nèi)存限制問題
讓我們來看一次事故,情況如下:當(dāng)一個(gè)Java應(yīng)用在容器中執(zhí)行的時(shí)候,某些情況下會(huì)容器會(huì)莫名其妙退出。
1.Dockfile如下:
FROM airdock/oracle-jdk:latest MAINTAINER Grissom Wang <grissom.wang@daocloud.io> ENV TIME_ZONE Asia/Shanghai RUN echo "$TIME_ZONE" > /etc/timezone WORKDIR /app RUN apt-get update COPY myapp.jar /app/ myapp.jarEXPOSE 8080CMD [ "java", "-jar", "myapp.jar" ]
2.運(yùn)行命令
3.日志分析
執(zhí)行docker logs container_id,出現(xiàn)了java.lang.OutOfMemoryError。
4.問題初步分析
因?yàn)槲覀冊趫?zhí)行容器的時(shí)候,對內(nèi)存做了限制,同時(shí)在Java啟動(dòng)參數(shù)重,沒有對內(nèi)存使用做限制,是不是這個(gè)原因?qū)е铝巳萜鞅桓傻裟?#xff1f;
當(dāng)我們執(zhí)行沒有任何參數(shù)設(shè)置(如上面的myapp)的 Java 應(yīng)用程序時(shí),JVM 會(huì)自動(dòng)調(diào)整幾個(gè)參數(shù),以便在執(zhí)行環(huán)境中具有最佳性能,但是在使用過程中我們逐步發(fā)現(xiàn),如果讓 JVM ergonomics (即JVM人體工程學(xué),用于自動(dòng)選擇和行為調(diào)整)對垃圾收集器、堆大小和運(yùn)行編譯器使用默認(rèn)設(shè)置值,運(yùn)行在容器中的 Java 進(jìn)程會(huì)與我們的預(yù)期表現(xiàn)嚴(yán)重不符(除了上訴的問題)。
首先我們來做一個(gè)實(shí)驗(yàn):
1.在我本機(jī)(Mac)上執(zhí)行docker info命令
Server Version: 17.06.0-ce Kernel Version: 4.9.31-moby Operating System: Alpine Linux v3.5OSType: linuxArchitecture: x86_64CPUs: 2Total Memory: 1.952GiBName: moby2.執(zhí)行docker run -it -m=100M –memory-swap=100M debian cat /proc/meminfo
MemTotal: ? ? ? 2047048 kB = 2GMemFree: ? ? ? ? 609416 kB = 600MMemAvailable: ? 1604928 kB = 1.6G雖然我們啟動(dòng)容器的時(shí)候,指定了容器的內(nèi)存限制,但是從容器內(nèi)部看到的內(nèi)存信息和主機(jī)上的內(nèi)存信息幾乎一致。
因此我們找到原因了:docker switches(-m,-memory和-memory-swap) 在進(jìn)程超過限制的情況下,會(huì)指示 Linux 內(nèi)核殺死該進(jìn)程。但 JVM 是完全不知道限制,因此會(huì)很有可能超出限制。在進(jìn)程超過限制的時(shí)候,容器進(jìn)程就被殺掉了!
解決問題的一種思路就是使用JVM參數(shù)來限制內(nèi)存的使用,這個(gè)需要根據(jù)JVM內(nèi)存參數(shù)的定義來做巧妙的設(shè)置,但是幸運(yùn)的是從Java SE 8u131和JDK 9開始,Java SE開始支持Docker CPU和內(nèi)存限制。
具體實(shí)現(xiàn)邏輯如下: 如果-XX:ParalllelGCThreads或-XX:CICompilerCount未指定為命令行選項(xiàng),則JVM將Docker CPU限制應(yīng)用于JVM在系統(tǒng)上看到的CPU數(shù)。然后JVM將調(diào)整GC線程和JIT編譯器線程的數(shù)量,就像它在裸機(jī)系統(tǒng)上運(yùn)行一樣,其CPU數(shù)量設(shè)置為Docker CPU限制。如果-XX:ParallelGCThreads或-XX:CICompilerCount指定為JVM命令行選項(xiàng),并且指定了Docker CPU限制,則JVM將使用-XX:ParallelGCThreads和-XX:CICompilerCount值。
對于Docker內(nèi)存限制、最大Java堆的設(shè)置還有一些工作要做。要在沒有通過-Xmx設(shè)置最大Java堆的情況下告知JVM要注意Docker內(nèi)存限制,需要兩個(gè)JVM命令行選項(xiàng):
-XX:+ UnlockExperimentalVMOptions-XX:+ UseCGroupMemoryLimitForHeap。-XX:+ UnlockExperimentalVMOptions是必需的,因?yàn)樵趯戆姹局?#xff0c;Docker內(nèi)存限制的透明標(biāo)識(shí)是目標(biāo)。當(dāng)使用這兩個(gè)JVM命令行選項(xiàng),并且未指定-Xmx時(shí),JVM將查看Linux cgroup配置,這是Docker容器用于設(shè)置內(nèi)存限制的方式,以透明地指定最大Java堆大小。 Docker容器也使用Cgroups配置來執(zhí)行CPU限制。
服務(wù)注冊與發(fā)現(xiàn)
在微服務(wù)的場景中,運(yùn)行的微服務(wù)實(shí)例將會(huì)達(dá)到成百上千個(gè),同時(shí)微服務(wù)實(shí)例存在失效,并在其他機(jī)器上啟動(dòng)以保證服務(wù)可用性的場景,因此用IP作為微服務(wù)訪問的地址會(huì)存在需要經(jīng)常更新的需求。
服務(wù)注冊與發(fā)現(xiàn)就應(yīng)用而生,當(dāng)微服務(wù)啟動(dòng)的時(shí)候,它會(huì)將自己的訪問Endpoint信息注冊到注冊中心,以便當(dāng)別的服務(wù)需要調(diào)用的時(shí)候,能夠從注冊中心獲得正確的Endpoint。
如果用Java技術(shù)棧開發(fā)微服務(wù)應(yīng)用,Spring Cloud(https://spring.io/)將會(huì)是大家首選的微服務(wù)開發(fā)框架。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/)。
在本機(jī)運(yùn)行如下命令:
java -jar target/discovery-client-demo-0.0.1-SNAPSHOT.jar該應(yīng)用會(huì)將自己的信息注冊到本地Eureka。我們可以通過打開Eureka的Dashboard,看到如下信息:
我們可以看到應(yīng)用的如下信息:
? ? 名稱:discovery-client-demo? ? IP: 10.8.0.67? ? Port: 9090但是我們在容器化場景中,遇到了問題。Dockerfile如下:
FROM airdock/oracle-jdk:latest MAINTAINER Grissom Wang <grissom.wang@daocloud.io>ENV TIME_ZONE Asia/Shanghai RUN echo "$TIME_ZONE" > /etc/timezone WORKDIR /app RUN apt-get update COPY target/discovery-client-demo-0.0.1-SNAPSHOT.jar /app/discovery-client-demo.jar EXPOSE 8080CMD [ "java", "-jar", "discovery-client-demo.jar" ]假設(shè)我們現(xiàn)在在本機(jī)使用Docker run命令執(zhí)行容器化discovery-client-demo,命令行如下:
docker run -d --name discovery-client-demo1 -e eureka.client.serviceUrl.defaultZone=http://10.8.0.67:8761/eureka/ -e spring.application.name=grissom ?-p 8080:8080 discovery-client-demo:latestdocker run -d --name discovery-client-demo2 -e eureka.instance.prefer-ip-address=true –e eureka.client.serviceUrl.defaultZone=http://10.8.0.67:8761/eureka/ -e spring.application.name=grissom2 ?-p 8081:8080 discovery-client-demo:latest前后兩個(gè)容器的區(qū)別在于,第二個(gè)指示應(yīng)用注冊的使用IP,而默認(rèn)是Hostname。
打開Eureka的Dashboard,我們可以看到如下信息:?
調(diào)用Eureka的Apps接口,我們可以看到更加詳細(xì)的信息:?
這個(gè)時(shí)候我們發(fā)現(xiàn)了問題,兩個(gè)應(yīng)用注冊的信息中,Hostname, IP和端口都使用了容器內(nèi)部的主機(jī)名,IP和端口(如8080)。這就意味著在Port-Mapping的場景下,應(yīng)用注冊到Eureka的時(shí)候,注冊信息無法被第三方應(yīng)用使用。
解決這個(gè)問題的第一個(gè)思路如下:?
我們對Eureka進(jìn)行擴(kuò)展,對所有客戶端過來的請求進(jìn)行攔截,然后從Docker Daemon中拿到正確的外部訪問信息來進(jìn)行替換,從而確保注冊到Eureka中的信息是外部能夠訪問的。
該方案需要做的工作:
-
需要同時(shí)對Eureka Client和Server改造,能夠在Client上報(bào)的信息中加入container ID等信息。?
-
Eureka服務(wù)端需要加入一個(gè)過濾器,需要對所有的注冊請求進(jìn)行處理,根據(jù)container ID將內(nèi)部hostname,IP和端口改為外部可以訪問的IP和端口。
該方案的挑戰(zhàn):
-
因?yàn)橹挥腥萜鞅粍?chuàng)建后才有ID,無法在啟動(dòng)參數(shù)中指定,因此應(yīng)用在容器啟動(dòng)的時(shí)候,無法拿到容器的ID。?
-
對于Eureka Server和Client的改造,無法貢獻(xiàn)回社區(qū),因此需要自己維護(hù)版本,存在極大的風(fēng)險(xiǎn)。
AWS的EC2 提供了實(shí)例元數(shù)據(jù)和用戶數(shù)據(jù)的API,它可以通過REST API的方式供運(yùn)行在EC2內(nèi)部的應(yīng)用使用。API形式如下:
http://169.254.169.254/latest/meta-data/?
其中IP地址是固定的一個(gè)IP地址。
因此我們也可以提供一個(gè)類似的元數(shù)據(jù)和用戶數(shù)據(jù)API,供容器內(nèi)部的應(yīng)用調(diào)用。同時(shí)為了減少對應(yīng)用的侵入,我們可以在應(yīng)用啟動(dòng)之前執(zhí)行一個(gè)腳本來獲取相應(yīng)的信息,并設(shè)置到環(huán)境變量中,供應(yīng)用啟動(dòng)后讀取并使用,流程如下:?
總結(jié)
本文總結(jié)了使用Java開發(fā)的企業(yè)級應(yīng)用在向微服務(wù)架構(gòu)應(yīng)用轉(zhuǎn)型過程中,在容器化運(yùn)行過程中遇到的常見問題、原因分析及解決方法。總結(jié)下來,由于容器技術(shù)的內(nèi)在特性,我們需要對應(yīng)用做一些改造,同時(shí)相應(yīng)的工具如JVM也需要對容器有更好的本地支持。
作者簡介:
-
王天青, DaoCloud首席架構(gòu)師,負(fù)責(zé)用新技術(shù)幫助企業(yè)做下一代應(yīng)用轉(zhuǎn)型。
-
?夏巖, DaoClozud微服務(wù)架構(gòu)師,幫助企業(yè)進(jìn)行微服務(wù)架構(gòu)轉(zhuǎn)型。
總結(jié)
以上是生活随笔為你收集整理的微服务应用容器化场景中常见问题总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 支付宝风险控制如何解除
- 下一篇: java 中negate()_Java