Spring Boot 容器化踩坑與解決方案(1)

自從2017年開始玩 Kubernetes 和 Spring Boot到如今,已經在這條不歸路上走了2年多,中間踩了一系列的小坑。在這裏統一總結一下具體解決方案。
預計會分紅4章左右的內容,本期主要是總結一些關於配置,日誌,鏡像的問題。下一期主要是關於持續集成的,而後是關於監控的。最後是關於集羣的。

Spring Profile 與 環境變量

咱們知道在基於Docker的DevOps中,咱們應當儘量保證多環境一個鏡像。以確保各環境下的代碼統一問題。根據咱們的實際狀況,咱們沒有采用配置中心方案,而採用環境變量的方案來實現。html

Spring Boot 默認狀況下,支持多環境配置。咱們能夠經過Spring Profile 完成各類不一樣環境或者不一樣集羣的配置區分。java

具體可使用環境變量SPRING_PROFILES_ACTIVE來指定使用那個環境配置。具體命令以下:linux


docker run -d -p 8080:8080 -e 「SPRING_PROFILES_ACTIVE=dev」 –name test testImage:latest複製代碼


咱們內部通常採用多文件管理配置,環境劃分紅5個。分別是local,dev,test,pre,pro,分別對應本地調試,開發環境,測試環境,預發環境,正式環境。一共產生5個配置文件,分別是applicaiton.yaml,applicaiton-local.yaml,applicaiton-dev.yaml,applicaiton-test.yaml,applicaiton-pre.yaml,applicaiton-prod.yaml。在applicaiton.yaml中咱們放公共配置,例如jackson的配置,部分kafka,mybatis的配置。對於MySQL,Kafka鏈接配置等保存在個環境配置中。默認狀況下環境選擇local。在各環境部署時,經過環境變量覆寫來作配置切換。git

採用這種方式後,咱們還面臨另一個問題,像是線上MySQL鏈接地址會直接暴露給有代碼訪問權限的人,這就十分危險了,因此對於這些配置,咱們默認也是採用環境變量注入。正式環境的配置信息,通常只有運維才能知道,在運維配置的時候,讓他們來注入。github

舉個例子:redis

spring.redis.host=${REDIS_HOST}
spring.redis.port=${REDIS_PORT}
spring.redis.timeout=30000

docker run -d -p 8080:8080 -e "SPRING_PROFILES_ACTIVE=dev" -e "REDIS_HOST=127.0.0.1" -e "REDIS_PORT=3306" --name test testImage:latest複製代碼


在咱們的代碼中,還存在一些其它狀況,須要根據環境變量來判斷是否須要配置Bean。例如swagger咱們不想在生產環境中開啓。對於這種狀況,咱們採用@Profile來肯定是否須要初始化該Bean。
舉個例子:
spring

@Component
@Profile("dev")
public class DatasourceConfigForDev


@Configuration
@EnableSwagger2
@Profile( "dev")
public class SwaggerConfig {
}複製代碼

Spring Boot 容器化後的日誌

在實際使用中,咱們使用 Kubernetes 來作容器調度,使用ES來存儲日誌。目前在應用日誌收集這塊,常規的方案一共有4種,docker

第一種應用日誌直接經過網絡傳遞到日誌收集組件,而後再交給ES。例如logstash-logback-encoder的LogstashSocketAppender,若是日誌量太大,能夠先輸入到消息通道中,再由日誌收集器收集。這種方式會加大應用佔用的CPU和內存資源,還須要一個相對穩定的網絡環境。apache

第二種方式,是將日誌輸出到固定目錄,並將這個目錄掛載到本地或者網絡存儲上,在由日誌收集器處理。這種方式,會致使日誌中缺乏關於Kubernetes的pod信息。須要採用其它方式補回。tomcat

第三種方式,是將日誌直接輸出到console,而後交由Docker記錄日誌,再經過日誌收集器收集。因爲一臺主機中,跑着各類類型不一樣的容器,若是不作特殊處理,解析日誌的成本就會很是很是高。

第四種方式,每一個應用單獨掛一個輔助容器,用來完成日誌解析與收集。會多佔用一些資源。只要輔助容器中的日誌收集工具選擇的好,確實是最好方案。

基於上面的集中方案,咱們根據本身的狀況選擇了第三種,爲了不在收集過程當中各類日誌解析工做,咱們但願日誌輸出時儘量爲Json格式。在這裏咱們使用logstash-logback-encoder來解決,輸出固定結構的JSON。配合上面的解析多環境配置,咱們建立了一個logback-kubernetes.xml,對於須要在容器中運行的環境,經過配置指定使用logback-kubernetes.xml作日誌配置文件。這樣在本地開發的時候,咱們就能夠愉快的使用Spring Boot的默認日誌了。

關於 Java 在容器中運行的問題

咱們目前使用Java 8,JDK選擇了 openJDK。至於爲何選擇openJDK,最主要的緣由是最開始的時候,咱們還沒封裝內部鏡像,跟着教程走,就進入了openJDK陣營(當時oracle還沒開始在docker hub上發佈oracle jdk的鏡像),如今看來應該小開心一下,貌似日以後只能使用openJDK了。畢竟Java 11的新受權模式,咱們仍是須要考慮一下是否使用。

在 Java 8u131 之前,因爲 JVM 沒法識別是在容器中運行,沒辦法根據容器限定的CPU,內存自動分配運行時候的參數,常常致使咱們出現OOM kill的問題(咱們也嘗試過手動分配,堆區內存還相對好限制,非堆區不太好限制。對於部分java應用,須要反覆調試。沒辦法作通用化處理和自動擴容縮容)。後來咱們找到了https://github.com/fabric8io-images/java/tree/master/images/jboss/openjdk8/jdk,這個鏡像能夠根據能夠自動訪問cgroup獲取cpu和內存信息,計算出一個相對合理的jvm配置參數。咱們根據這個思路,也建立了咱們內部的對應腳本(監控體系不同),可是這個配置過程不太透明。

到JRE 8u131 之後,JVM新增了-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,能夠用來識別容器中的內存限制(原理你們能夠百度,這裏就不講了)。考慮到通常狀況下,咱們CPU不會佔滿,內存會成爲主要瓶頸,因此咱們封裝了新的鏡像。鏡像大體以下:


FROM alpine:3.8

ENV LANG="en_US.UTF-8" \
    LANGUAGE="en_US.UTF-8" \
    LC_ALL="en_US.UTF-8" \
    TZ="Asia/Shanghai"

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/' /etc/apk/repositories \
    && apk add --no-cache tzdata curl ca-certificates \
    && echo "${TZ}" > /etc/TZ \
    && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
    && rm -rf /tmp/* /var/cache/apk/*

ENV JAVA_VERSION_MAJOR=8 \
    JAVA_VERSION_MINOR=181 \
    JAVA_VERSION_BUILD=13 \
    JAVA_VERSION_BUILD_STEP=r0 \
    JAVA_PACKAGE=openjdk \
    JAVA_JCE=unlimited \
    JAVA_HOME=/usr/lib/jvm/default-jvm \
    DEFAULT_JVM_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2 -XX:+UseG1GC"

RUN apk add --no-cache openjdk8-jre=${JAVA_VERSION_MAJOR}.${JAVA_VERSION_MINOR}.${JAVA_VERSION_BUILD}-${JAVA_VERSION_BUILD_STEP} \
    && echo "securerandom.source=file:/dev/urandom" >> /usr/lib/jvm/default-jvm/jre/lib/security/java.security \
    && rm -rf /tmp/*  /var/cache/apk/*複製代碼


到此咱們的Java基礎鏡像就算是封裝完畢了,也相對比較好的解決了Java 運行在容器裏的一些問題。至於往後的升級問題,Java 8u191 和 Java 11 已經根治資源限制問題,有時間單獨講(又給本身挖坑),因此不須要考慮,有不怕死的趕快幫忙試試Java 11.

具體的一些鏡像信息能夠參考:https://github.com/XdaTk/DockerImages


關於 Spring Boot 與 Tomcat APR

對於Spring Boot的容器,咱們這裏使用Tomcat,試用過一段時間的undertow,確實在內存佔用上會小一些。可是因爲監控尚未完善,因此咱們暫時的主力仍是Tomcat。若是有人升級到Spring Boot 2.0之後,可能會注意到啓動的時候,會出現一條關於Tomcat APR的WARN日誌。至於什麼是APR,你們能夠參考一下http://tomcat.apache.org/tomcat-9.0-doc/apr.html

爲了性能,咱們決定切換到APR模式下。咱們在上面提到的Java鏡像的基礎上,繼續封裝了一遍。

FROM xdatk/openjdk:8.181.13-r0 as native

ENV TOMCAT_VERSION="9.0.13" \
    APR_VERSION="1.6.3-r1" \
    OPEN_SSL_VERSION="1.0.2p-r0"
ENV TOMCAT_BIN="https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz"

RUN apk add --no-cache apr-dev=${APR_VERSION} openssl-dev=${OPEN_SSL_VERSION} openjdk8=${JAVA_VERSION_MAJOR}.${JAVA_VERSION_MINOR}.${JAVA_VERSION_BUILD}-${JAVA_VERSION_BUILD_STEP} wget unzip make g++ \
    && cd /tmp \
    && wget -O tomcat.tar.gz ${TOMCAT_BIN} \
    && tar -xvf tomcat.tar.gz \
    && cd apache-tomcat-*/bin \
    && tar -xvf tomcat-native.tar.gz \
    && cd tomcat-native-*/native \
    && ./configure --with-java-home=${JAVA_HOME} \
    && make \
    && make install


FROM xdatk/openjdk:8.181.13-r0
ENV TOMCAT_VERSION="9.0.13" \
    APR_VERSION="1.6.3-r1" \
    OPEN_SSL_VERSION="1.0.2p-r0" \
    APR_LIB=/usr/local/apr/lib

COPY --from=native ${APR_LIB} ${APR_LIB}

RUN apk add --no-cache apr=${APR_VERSION} openssl=${OPEN_SSL_VERSION}複製代碼

實測下來,會有些許性能提示。

以上,咱們基本保證了spring boot 在容器中能正常運行。接下來咱們就須要讓代碼到生產環境流水線化,敬請期待下一章。

相關文章
相關標籤/搜索