Spring Boot和Docker实践中遇到的问题

Spring Boot和Docker实践

先来看下spring boot 官网给出的Dockerfile示例:

FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD gs-spring-boot-docker-0.1.0.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]

解释:
1. 选用alpine-oraclejdk8:slim: alpine的jdk。Alpine Linux, 一个只有5M的Docker镜像。直接让我的spring boot镜像从610.9MB减小到259.7MB。但docker镜像分层,对于pull和push影响不大。谨慎使用,关于它,还是争论不断:Hacker News
2. /tmp: tomcat默认工作目录
3. ADD:把可执行jar包添加镜像命名为app.jar。这步不建议这么做,在有的情况下不方便查看正在运行的jar包。
4. sh -c ‘touch /app.jar’:这步作用是改变文件时间
5. ENTRYPOINT:指定参数启动jar。这步有两个重点,后续详解。

1. Docker file中使用maven变量

通过maven插件完成变量传递:下面是我的第一次改进后的Dockerfile

FROM frolvlad/alpine-oraclejdk8:slim
MAINTAINER erdaoya
ADD @project.build.finalName@.jar @project.build.finalName@.jar
RUN sh -c 'touch /@project.build.finalName@.jar'
CMD ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=docker  -jar /@project.build.finalName@.jar" ]

使用@变量@的方式获取maven变量,但还需配置插件,否则不生效:

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>${resources.plugin.version}</version>
    <executions>
        <execution>
            <id>prepare-dockerfile</id>
            <phase>validate</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}/docker</outputDirectory>
                <resources>
                    <resource>
                        <directory>${project.basedir}/src/main/docker</directory>
                        <filtering>true</filtering>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

maven docker 插件:spotify/docker-maven-plugin

2. 内存限制

在docker-compose中传递JAVA_OPTS变量,不能限制java进程占用的内存。如下:

image: erdaoya/cloud-service-client
    ports:
      - "9005:9005"
    environment:
       JAVA_OPTS=-Xmx128m

被各种网上示例坑的,这个问题按照官网示例,在java -jar 加上$JAVA_OPTS后生效。其实tomcatcatalina.sh中,执行java时已经传$JAVA_OPTS参数,所以它能读取环境变量。

3. Docker stop 不能优雅停止服务

3.1 问题

我使用spring cloud,main方法如下:

@SpringBootApplication
@EnableDiscoveryClient
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

本地使用IDE启动或者直接执行java -jar xxx.jarctrl + c停止服务时,可以看到以下日志。

2017-03-21 18:58:14.212  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@3ee0b4f7: startup date [Tue Mar 21 18:57:33 CST 2017]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@7a3793c7
2017-03-21 18:58:14.214  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] c.n.e.EurekaDiscoveryClientConfiguration : Unregistering application cloud-service-client with eureka with status DOWN
2017-03-21 18:58:14.215  WARN [cloud-service-client,,,] 7158 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : Saw local status change event StatusChangeEvent [timestamp=1490093894215, current=DOWN, previous=UP]
2017-03-21 18:58:14.215  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : Shutting down DiscoveryClient ...
2017-03-21 18:58:14.216  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : Unregistering ...
2017-03-21 18:58:14.225  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_CLOUD-SERVICE-CLIENT/10.69.6.86:cloud-service-client:9005 - deregister  status: 200

......

2017-03-21 18:58:14.753  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2017-03-21 18:58:14.754  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans
2017-03-21 18:58:14.754  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] o.s.s.c.ThreadPoolTaskScheduler          : Shutting down ExecutorService 'taskScheduler'
2017-03-21 18:58:14.779  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@7037a680: startup date [Tue Mar 21 18:57:44 CST 2017]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@3ee0b4f7
2017-03-21 18:58:14.780  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] c.n.e.EurekaDiscoveryClientConfiguration : Unregistering application cloud-service-client with eureka with status DOWN
2017-03-21 18:58:14.781  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] c.n.e.EurekaDiscoveryClientConfiguration : Unregistering application cloud-service-client with eureka with status DOWN
2017-03-21 18:58:14.781  INFO [cloud-service-client,,,] 7158 --- [       Thread-8] c.n.e.EurekaDiscoveryClientConfiguration : Unregistering application cloud-service-client with eureka with status DOWN****

其实就是调用了ShutdownHook, Unregistering from Eureka。

但如果通过docker 启动,执行docker stop 容器ID,并不会主动下线服务,一段时间后,eureka才主动剔除(默认90秒)。

3.2 尝试一

猜想是java进程并没有接收SIGTERM信号,而是直接被KILL掉。

$ docker stop --help
Usage:	docker stop [OPTIONS] CONTAINER [CONTAINER...]
Stop one or more running containers
Options:
      --help       Print usage
  -t, --time int   Seconds to wait for stop before killing it (default 10)

docker stop 命令会向容器发送SIGTERM信号,如果10秒后还停不掉,直接kill。

加长时间看看:

docker stop -t 30 容器ID`

等30秒后看日志,还是跟原来一样,直接被kill,没有unregistry

3.2 尝试二

$ docker kill --help
Usage:	docker kill [OPTIONS] CONTAINER [CONTAINER...]
Kill one or more running containers
Options:
      --help            Print usage
  -s, --signal string   Signal to send to the container (default "KILL")

docker kill默认Kill,相当于kill -9 PID,但可以指定信号

换成docker kill 指定SIGTERM试试:

docker kill -s SIGTERM 容器ID

发现这种方式无法停止的docker容器。

通过docker exec进入容器,发现后台起了两个java进程,如下:

root@8f5948746f35:/# ps -ef|grep java
root         1     0  0 10:49 ?        00:00:00 sh -c java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=docker  -jar /cloud-service-client-1.0-SNAPSHOT.jar
root         5     1 14 10:49 ?        00:00:56 java -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=docker -jar /cloud-service-client-1.0-SNAPSHOT.jar
root        85    80  0 10:55 ?        00:00:00 grep java

意思是,有一个linux shell 主进程,PID为1。他有一个java子进程PID为5

手动kill -s SIGTERM 1无法停止sh -c xx进程,但对PID 5却有用,那问题就很明显了:SIGTERM不能停止sh 进程,现在需要解决的问题是,java进程应该是主进程,PID为1。

3.2 尝试三:解决

Dockerfile中,RUN,CMD,ENTRYPOINT都有两种写法:

#shell形式
CMD command param1 param2
# exec形式
CMD ["executable", "param1", "param2"]

shell形式和exec的形式的本质区别在于shell形式提供了默认的指令/bin/sh -c,所以其指定的command将在shell的环境下运行。因此指定command的pid将不会是1,因为pid为1的是shell,command进程是shell的子进程。而exec指定的命令不由shell启动,因此也就无法使用shell中的环境变量。

根据以上结论,修改Dockerfile如下:

FROM frolvlad/alpine-oraclejdk8:slim
MAINTAINER erdaoya
ADD @project.build.finalName@.jar @project.build.finalName@.jar
RUN sh -c 'touch /@project.build.finalName@.jar'
ENV JAVA_OPTS=""
CMD exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=docker -jar /@project.build.finalName@.jar

启动spring boot 的docker容器:

$ docker run  -e "JAVA_OPTS=-Xmx128m" 镜像ID/NAME
$ docker exec -it 容器ID /bin/bash
root@6a2e0ed56517:/# ps -ef|grep java |grep -v "grep"
root         1     0 25 15:01 ?        00:01:05 java -Xmx128m -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=docker -jar /cloud-service-client-1.0-SNAPSHOT.jar

可以看到,主进程就是java进程,JAVA_OPTS 传递成功。但其实不用通过java限制,限制Docker容器资源也能达到目的。

退出容器后,使用docker stop命令,根据日志可以看到,java进程接到了SIGTERM信号,调用shutdownHook完成了下线工作,优雅停止,问题解决。

CONTENTS