目录
解决方案#1:使用depends_on,条件和service_healthy
介绍
如果您使用Docker Compose在一台计算机上运行多个容器,您迟早会遇到需要确保服务A在服务B之前运行的情况。典型的例子是需要访问数据库的应用程序;如果这两个组合服务都通过命令docker-compose up启动,则此操作可能会失败,因为应用程序服务可能会在数据库服务之前启动,并且它不会找到能够处理其SQL语句的数据库。Docker Compose背后的人已经考虑过这个问题,并提供了depends_on指令来表达服务之间的依赖关系。
另一方面,仅仅因为数据库服务是在应用程序服务之前启动的,并不意味着数据库已准备好处理传入的连接(ready状态)。任何关系数据库系统都需要先启动自己的服务,然后才能处理传入连接(例如,检查 SQL Server启动步骤的简化视图),并且启动可能需要一段时间,因此除了指定其依赖项之外,我们还需要一种更好的机制来检测特定组合服务的ready状态。
在本文中,我将介绍几种受官方建议和其他来源启发的方法。每种方法都将使用自己的撰写文件,并且每个撰写文件中都至少包含两个服务:Java 8控制台应用程序和MySQL v5.7数据库;前者将使用普通的JDBC连接到后者,将读取一些元数据,然后将其打印到控制台。
所有撰写文件都将使用相同的Java应用程序 Docker镜像。
本文末尾还有一个奖励部分,所以也请检查一下!
重要事项
- 我的环境
- Windows 10 x64 专业版
- Docker v18.03.1-ce-win65(17513)
- Docker Compose v1.21.1,内部版本7641a569
- 本文使用的源代码可以在 GitHub 上找到
- 本文的.NET Core端口可在此处找到:GitHub - satrapu/iquest-keyboards-and-mice-brasov-2018: Resources for "How to Control Service Startup Order in Docker Compose" presentation @ iQuest K&M, Brașov, 2018.
- 以下所有命令都必须从以管理员身份运行的Powershell控制台执行
- 另外,由于我很懒,我在Docker Compose文件中嵌入了Linux shell命令,这绝对不是最佳实践,但由于本文的重点是服务启动顺序而不是Docker Compose文件最佳实践,请忍受——请检查.NET Core端口以获取正确的方法。
- 在通过 docker-compose up 启动任何撰写服务之前,我正在使用mvn、docker-compose down和docker-compose构建命令,以确保:
- 我将使用默认的Maven目标运行Java应用程序的最新版本;就我而言,这是clean compile assembly:single:
- 任何正在运行的撰写服务都将停止
- 将重新生成在撰写文件中声明的任何Docker镜像
- 上述撰写文件使用在.env 文件中声明的变量,内容如下:
mysql_root_password=<ENTER_A_PASSWORD_HERE>
mysql_database_name=jdbcwithdocker
mysql_database_user=satrapu
mysql_database_password=<ENTER_A_DIFFERENT_PASSWORD_HERE>
java_jvm_flags=-Xmx512m
java_debug_port=9876
# Use "suspend=y" to ensure the JVM will pause the application,
# waiting for a debugger to be attached
java_debug_settings=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9876
# The amount of time between two consecutive health state checks
# (used by docker-compose-using-healthcheck.yml)
healthcheck_interval=2s
# The maximum amount of time each healthcheck state try must end in
# (used inside docker-compose-using-healthcheck.yml)
healthcheck_timeout=5s
# The maximum amount of retries before giving up and considering
# the Docker container in an unhealthy state
# (used by docker-compose-using-port-checking.yml and docker-compose-using-api.yml)
healthcheck_retries=20
# The amount of time between two consecutive queries against the database
# (used by docker-compose-using-port-checking.yml)
check_db_connectivity_interval=2s
# The maximum amount of retries before giving up and considering
# the database is not able to process incoming connections
# (used by docker-compose-using-port-checking.yml)
check_db_connectivity_retries=20
# The Docker API version to use when querying for container metadata
# (used by docker-compose-using-api.yml)
docker_api_version=1.37
由于.env文件包含数据库密码等敏感内容,因此不应将其置于源代码管理之下。
解决方案#1:使用depends_on,条件和service_healthy
此解决方案使用此Docker组合文件:docker-compose-using-healthcheck.yml。
使用以下命令运行它:
mvn `
;docker-compose --file docker-compose-using-healthcheck.yml down --rmi local `
;docker-compose --file docker-compose-using-healthcheck.yml build `
;docker-compose --file docker-compose-using-healthcheck.yml up
从 1.12 版本开始,Docker添加了用于验证容器是否仍在工作的 HEALTHCHECK Dockerfile指令;自版本2.1以来,Docker Compose文件添加了对在表达服务依赖关系时使用运行状况检查的支持,如兼容性矩阵中所述。
我的数据库服务会将其运行状况检查定义为My SQL客户端命令,该命令将定期查询底层MySQL数据库是否已准备好通过 USE SQL语句处理传入连接:
...
db:
image: mysql:5.7.20
healthcheck:
test: >
mysql \
--host='localhost' \
--user='${mysql_database_user}' \
--password='${mysql_database_password}' \
--execute='USE ${mysql_database_name}' \
interval: ${healthcheck_interval}
timeout: ${healthcheck_timeout}
retries: ${healthcheck_retries}
...
请记住,USE语句并不是执行此类检查的唯一方法。例如,可以定期运行一个SQL脚本,该脚本将测试数据库是否可访问,以及数据库用户是否已被授予所有预期的权限(例如,可以对特定表执行INSERT等)。
一旦数据库服务达到“正常”状态,我的应用程序服务就会启动:
...
app:
image: satrapu/jdbc-with-docker-console-runner
...
depends_on:
db:
condition: service_healthy
...
如您所见,声明数据库和应用服务之间的依赖关系非常简单,就像执行运行状况检查一样。更好的是,这些东西是内置的Docker Compose。
现在坏消息是:由于Docker Swarm也使用Docker Compose文件格式,开发团队决定从compose file v3开始将此功能标记为过时,如此处所述;在此处查看此决定背后的更多原因。
depends_on、condition和service_healthy仅在使用较旧的撰写文件版本(v2.1至v2.4包括 v2.4)时可用。
请记住,Docker Compose可能会在未来版本中删除对这些版本的支持,但只要你同意在v3之前使用compose文件版本,此解决方案就非常易于理解和使用。
解决方案#2:使用Twist的端口检查
此解决方案使用此Docker组合文件:docker-compose-using-port-checking.yml。
使用以下命令运行它:
mvn `
;docker-compose --file docker-compose-using-port-checking.yml down --rmi local `
;docker-compose --file docker-compose-using-port-checking.yml build `
;docker-compose --file docker-compose-using-port-checking.yml up --exit-code-from check_db_connectivity check_db_connectivity `
;if ($LASTEXITCODE -eq 0) { docker-compose --file docker-compose-using-port-checking.yml up app } `
else { echo "ERROR: Failed to start service due to one of its dependencies!" }
这个解决方案的灵感来自Dariusz Pasciak的一篇文章,但我不只是检查MySQL端口3306是否打开(端口检查),就像Dariusz所做的那样:我正在使用check_db_connectivity compose服务中找到的MySQL客户端运行上述USE SQL语句,以确保底层数据库可以处理传入的连接(twist);此外,由于–exit-code-from check_db_connectivity compose选项,将评估check_db_connectivity compose服务的退出代码,如果不同于0(这标志着数据库服务处于所需的就绪状态),则会打印错误消息,并且应用服务将不会启动。
- Docker Compose将尝试启动check_db_connectivity服务,但它会看到它依赖于数据库服务:
...
db:
image: mysql:5.7.20
...
check_db_connectivity:
image: activatedgeek/mysql-client:0.1
depends_on:
- db
...
- Docker Compose将启动数据库服务
- 然后,Docker Compose将启动check_db_connectivity服务,这将启动一个循环检查,检查MySQL数据库是否可以处理传入连接
- Docker Compose将等待check_db_connectivity服务完成其循环,因为循环是服务入口点的一部分:
check_db_connectivity:
image: activatedgeek/mysql-client:0.1
entrypoint: >
/bin/sh -c "
sleepingTime='${check_db_connectivity_interval}'
totalAttempts=${check_db_connectivity_retries}
currentAttempt=1
echo \"Start checking whether MySQL database \
"${mysql_database_name}\" is up & running\" \
\"(able to process incoming connections)
each $$sleepingTime for a total amount of $$totalAttempts times\"
while [ $$currentAttempt -le $$totalAttempts ]; do
sleep $$sleepingTime
mysql \
--host='db' \
--port='3306' \
--user='${mysql_database_user}' \
--password='${mysql_database_password}' \
--execute='USE ${mysql_database_name}'
if [ $$? -eq 0 ]; then
echo \"OK: [$$currentAttempt/$$totalAttempts] MySQL database \
"${mysql_database_name}\" is up & running.\"
return 0
else
echo \"WARN: [$$currentAttempt/$$totalAttempts] MySQL database \"
${mysql_database_name}\" is still NOT up & running ...\"
currentAttempt=`expr $$currentAttempt + 1`
fi
done;
echo 'ERROR: Could not connect to MySQL database \"
${mysql_database_name}\" in due time.'
return 1"
- 然后,Docker Compose将启动应用服务;当此服务运行时,MySQL数据库能够处理传入连接。
app:
image: satrapu/jdbc-with-docker-console-runner
depends_on:
- db
此解决方案与前一个解决方案类似,因为应用程序服务会等到数据库服务进入特定状态,但随后不使用Docker Compose过时功能。
解决方案#3:调用Docker引擎API
此解决方案使用此Docker组合文件:docker-compose-using-api.yml。
使用以下命令运行它:
$Env:COMPOSE_CONVERT_WINDOWS_PATHS=1 `
;mvn `
;docker-compose --file docker-compose-using-api.yml down --rmi local `
;docker-compose --file docker-compose-using-api.yml build `
;docker-compose --file docker-compose-using-api.yml up
重要
在不包含COMPOSE_CONVERT_WINDOWS_PATHS环境变量的情况下运行上述命令将失败:
...
Creating jdbc-with-docker_app_1 ... error
ERROR: for jdbc-with-docker_app_1 Cannot create container for service app:
b'Mount denied:\nThe source path "\\\\var\\\\run\\\\docker.sock:/var/run/docker.sock"\nis
not a valid Windows path'
...
此处记录了此问题及其修复程序。
我真的很喜欢通过运行状况检查来表达组合服务之间的依赖关系的想法。由于depends_on的condition形式迟早会消失,我想过实现一些概念上相似的东西,一种方法是使用 Docker Engine API。
我的方法是通过向Docker API端点发出HTTP请求,从应用程序服务入口点内定期查询数据库服务的运行状况状态,并使用命令行JSON处理器 jq 解析响应;一旦数据库服务达到“正常”状态,Java应用程序就会启动。
首先,我将通过一个简单的 curl 命令获取包含有关所有正在运行的容器的信息的JSON文档。特别的是使用 unix-socket curl选项,因为这种套接字由Docker守护程序使用。此外,我需要将 docker.sock 作为一个数据卷公开给运行curl命令的容器,以允许它与本地Docker守护程序通信。
重要
共享本地Docker守护程序套接字时应小心,因为它可能会导致安全问题,如此处非常清楚地介绍的那样,因此在使用此方法之前请仔细考虑所有事项!
现在安全广告已经播放完毕,下面您可能会找到一个示例,说明用于列出在本地主机上运行的所有Docker容器的独立命令是什么样子——请注意,我是在Docker容器 byrnedo/alpine-curl 中运行curl的,而实际命令是从基于 openjdk:8-jre-alpine Docker镜像的容器中执行的:
# Ensure db service is running before querying its metadata
docker-compose --file docker-compose-using-api.yml up -d db `
;docker container run `
--rm `
-v /var/run/docker.sock:/var/run/docker.sock `
byrnedo/alpine-curl `
--silent `
--unix-socket /var/run/docker.sock `
http://v1.37/containers/json
输出将如下所示:
杰伦
[
...
[
{
"Id":"5d9108769de3641692a5d636aa361866f09e6403309e6262520447dae9115344",
"Names":[
"/jdbc-with-docker_db_1"
],
"Image":"mysql:5.7.20",
"ImageID":"sha256:7d83a47ab2d2d0f803aa230fdac1c4e53d251bfafe9b7265a3777bcc95163755",
"Command":"docker-entrypoint.sh mysqld",
"Created":1525887950,
"Ports":[
{
"IP":"0.0.0.0",
"PrivatePort":3306,
"PublicPort":32771,
"Type":"tcp"
}
],
"Labels":{
"com.docker.compose.config-hash":"cea84824338bc0ea6a7da437084f00a8bfc9647b91dd8de5e41694269498dec6",
"com.docker.compose.container-number":"1",
"com.docker.compose.oneoff":"False",
"com.docker.compose.project":"jdbc-with-docker",
"com.docker.compose.service":"db",
"com.docker.compose.version":"1.21.1"
},
"State":"running",
"Status":"Up 6 seconds (healthy)",
"HostConfig":{
"NetworkMode":"jdbc-with-docker_default"
},
"NetworkSettings":{
"Networks":{
"jdbc-with-docker_default":{
"IPAMConfig":null,
"Links":null,
"Aliases":null,
"NetworkID":"fd1c60a463a8b39dd3cb9b34c8e5792c069e18cd5076f6321f5554c10ec1765d",
"EndpointID":"b80cfc9c45e0816cd9af9507f76e3a0f9f1e203d2d2b0e081b8affc1293e8cf4",
"Gateway":"172.18.0.1",
"IPAddress":"172.18.0.2",
"IPPrefixLen":16,
"IPv6Gateway":"",
"GlobalIPv6Address":"",
"GlobalIPv6PrefixLen":0,
"MacAddress":"02:42:ac:12:00:02",
"DriverOpts":null
}
}
},
"Mounts":[
{
"Type":"volume",
"Name":"jdbc-with-docker_jdbc-with-docker-mysql-data",
"Source":"/var/lib/docker/volumes/jdbc-with-docker_jdbc-with-docker-mysql-data/_data",
"Destination":"/var/lib/mysql",
"Driver":"local",
"Mode":"rw",
"RW":true,
"Propagation":""
}
]
},
...
]
其次,我将使用各种 jq运算符和函数提取数据库服务的运行状况:
jq '.[] | select(.Names[] | contains("_db_")) |
select(.State == "running") | .Status | contains("healthy")'
# The output should be "true" in case the db service has reached the healthy state
- .[]:这将从给定的JSON文档中选择所有记录。
- select(.Names[] | contains(“_db_”)):这将选择其“Names”数组属性具有包含“_db_” string的记录的记录——由Docker Compose创建的Docker容器的名称包含服务名称;在我们的例子中,它是“db”。
- select(.State == “running”):这将仅选择正在运行的Docker容器。
- .Status | contains(“healthy”):这将选择“Status”属性的值,如果容器已达到正常状态,该属性应为“true”。
为了到达在Docker Compose文件中找到的最终jq命令,我已经使用 jq Playground 进行了实验。请注意,这不是从Docker JSON中提取运行状况的唯一方法——发挥您的想象力来想出更好的jq命令。
结论
在Docker Compose中控制服务启动顺序是我们不能忽视的,但我希望本文中介绍的方法可以帮助任何人了解从哪里开始。
我完全知道这些不是唯一的选择——例如,实现自动驾驶模式的ContainerPilot看起来非常有趣。另一种选择是在依赖服务中移动延迟启动逻辑(例如,让我的Java控制台应用程序使用具有更长超时的连接池来获取与MySQL数据库的连接),但这需要胶水代码来检查每个依赖项(一种用于MySQL,另一种用于缓存提供程序的方法,如Memcache等)。好消息是有很多选项,您只需要确定哪一个更适合您的用例。
资源
- Docker Compose
- Docker Compose command-line reference
- Docker Compose file reference
- Docker Engine API v1.37
- jq
- jq Manual
- jq Playground
- JDBC - The Java Tutorials
- Debugging Java in VS Code
https://www.codeproject.com/Articles/1260230/Controlling-Service-Startup-Order-in-Docker-Compos