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" ]
解释:
- 选用alpine-oraclejdk8:slim: alpine的jdk。
Alpine Linux
, 一个只有5M的Docker镜像。直接让我的spring boot镜像从610.9MB减小到259.7MB。但docker镜像分层,对于pull和push影响不大。谨慎使用,关于它,还是争论不断:Hacker News - /tmp: tomcat默认工作目录
- ADD:把可执行jar包添加镜像命名为app.jar。这步不建议这么做,在有的情况下不方便查看正在运行的jar包。
- sh -c ’touch /app.jar’:这步作用是改变文件时间
- 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.jar
,ctrl + 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完成了下线工作,优雅停止,问题解决。