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 在容器中能正常运行。接下来咱们就须要让代码到生产环境流水线化,敬请期待下一章。

相关文章
相关标签/搜索