背景
这个问题其实由来已久,尤其是在微服务部署过程中体现的尤为明显,就是服务的启动顺序问题。
一个很常见的场景就是微服务启动过程中,应用服务会依赖于配置中心、注册中心、数据库、redis等基础服务,所以这些基础服务应当优先于应用服务启动。
还有就是在服务器重启时由于容器设置了自动重启,如果不控制启动顺序的话就就会导致有些服务去读取配置中心配置时,由于配置中心尚未完全启动成功导致应用服务启动中断或者失败。
综上,这个问题还是有必要研究下彻底解决掉的,于是有了这篇记录实践过程的文章。
解决方案
看到这个问题时有以下三种思路:
- 使用docker-compose提供的depends_on:
- 被依赖的服务确实会优先启动,
- 不能保证应用服务启动时被依赖的服务已经完全启动成功
- 把docker-compose文件拆开:
- 拆开成基础服务和应用服务,人工确保先后顺序。
- 不能解决服务器重启后服务自动按顺序启动
- 使用脚本监听依赖服务端口: 等依赖服务端口可以监听到时再启动应用服务
- 脚本需要在实际执行命令前执行,且需要明确必备依赖服务及端口
通过网络的搜索,大致也就这三种解决思路,对比下来还是第三种比较靠谱也解决的比较彻底,本文也是基于这种思路完成的实践。
PS:
当然以上结论并不绝对,有朋友有其他的思路或者看法也欢迎评论区留言。
实践
脚本监听端口的方式有多种,网上查到的比较多的是开源的wait-for.sh脚本,当然也完全可以手动完成或者借助AI的力量自行完成脚本。
本文我们就是使用的wait-for.sh脚本,后续也会贴出这个shell脚本。
场景
在一个docker-compose.yml中定义了三个服务,mysql, nacos, gateway
其中nacos依赖于mysql,gateway依赖于nacos,所以启动顺序优先级应当是mysql > nacos > gateway
编写docker-compose.yml
services:
mysql:
image: mysql:8.0.16
container_name: nacos-mysql
restart: always
volumes:
- ./mysql/conf:/etc/mysql/conf.d
- ./mysql/logs:/var/log/mysql
- ./mysql/data:/var/lib/mysql
environment:
- "MYSQL_ROOT_PASSWORD=xxxx"
- "TZ=Asia/Shanghai"
ports:
- 30306:3306
nacos:
hostname: nacos
restart: always
image: nacos/nacos-server:2.0.4
container_name: nacos
privileged: true
env_file:
- .env
ports:
- "8105:8848"
gateway:
image: java:8
container_name: gateway
restart: always
cap_add:
- SYS_PTRACE
depends_on:
- nacos
links:
- nacos
volumes:
- ./gateway:/data
working_dir: /data
#command: sh -c "/data/wait-for.sh nacos:8848 -t 60 -- java -Xmx512m -Xms512m -Xmn448m -jar cnhqd-examine-gateway.jar --spring.cloud.nacos.server.addr=nacos:8848 --spring.cloud.nacos.discovery.server-addr=nacos:8848 --spring.cloud.nacos.config.server-addr=nacos:8848"
command: sh -c "/data/wait-for.sh nacos:8848 -t 60 -- java -Xmx512m -Xms512m -Xmn448m -jar gateway.jar --spring.profiles.active=nacos"
说明如下:
- 关于nacos和mysql的配置可以参考【工作记录】基于docker+mysql部署单机版nacos2.0.4@20240219-CSDN博客
- gateway服务其实就是简单的集成了springcloudgateway网关服务,同时注册到了nacos中,代码如有必要后续再补充,不是本文重点。
- 在gateway服务的command配置中使用到了wait-for.sh脚本,通过-t配置了超时时间,通过links链接了本文件中的nacos服务,需要注意脚本目录及权限
wait-for.sh
脚本内容如下:
#!/bin/sh
# The MIT License (MIT)
#
# Copyright (c) 2017 Eficode Oy
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
VERSION="2.2.4"
set -- "$@" -- "$TIMEOUT" "$QUIET" "$PROTOCOL" "$HOST" "$PORT" "$result"
TIMEOUT=15
QUIET=0
# The protocol to make the request with, either "tcp" or "http"
PROTOCOL="tcp"
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$0 host:port|url [-t timeout] [-- command args]
-q | --quiet Do not output any status messages
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
Defaults to 15 seconds
-v | --version Show the version of this tool
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
case "$PROTOCOL" in
tcp)
if ! command -v nc >/dev/null; then
echoerr 'nc command is missing!'
exit 1
fi
;;
http)
if ! command -v wget >/dev/null; then
echoerr 'wget command is missing!'
exit 1
fi
;;
esac
TIMEOUT_END=$(($(date +%s) + TIMEOUT))
while :; do
case "$PROTOCOL" in
tcp)
nc -w 1 -z "$HOST" "$PORT" > /dev/null 2>&1
;;
http)
wget --timeout=1 --tries=1 -q "$HOST" -O /dev/null > /dev/null 2>&1
;;
*)
echoerr "Unknown protocol '$PROTOCOL'"
exit 1
;;
esac
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 7 ] ; then
for result in $(seq $(($# - 7))); do
result=$1
shift
set -- "$@" "$result"
done
TIMEOUT=$2 QUIET=$3 PROTOCOL=$4 HOST=$5 PORT=$6 result=$7
shift 7
exec "$@"
fi
exit 0
fi
if [ $TIMEOUT -ne 0 -a $(date +%s) -ge $TIMEOUT_END ]; then
echo "Operation timed out" >&2
exit 1
fi
sleep 1
done
}
while :; do
case "$1" in
http://*|https://*)
HOST="$1"
PROTOCOL="http"
shift 1
;;
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-v | --version)
echo $VERSION
exit
;;
-q | --quiet)
QUIET=1
shift 1
;;
-q-*)
QUIET=0
echoerr "Unknown option: $1"
usage 1
;;
-q*)
QUIET=1
result=$1
shift 1
set -- -"${result#-q}" "$@"
;;
-t | --timeout)
TIMEOUT="$2"
shift 2
;;
-t*)
TIMEOUT="${1#-t}"
shift 1
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
-*)
QUIET=0
echoerr "Unknown option: $1"
usage 1
;;
*)
QUIET=0
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then
echoerr "Error: invalid timeout '$TIMEOUT'"
usage 3
fi
case "$PROTOCOL" in
tcp)
if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
;;
http)
if [ "$HOST" = "" ]; then
echoerr "Error: you need to provide a host to test."
usage 2
fi
;;
esac
wait_for "$@"
Footer
具体代码不做过多解析,主要完成的就是参数解析,接口监听然后就是等待,感兴趣的朋友可以自行研究
问题与解决
-
遇到的第一个问题是容器启动时提示找不到nc命令,是因为容器内部没有netcat这个工具,一个解决方案是在基础镜像的基础上安装上netcat这个工具,然后重新打包镜像并修改docker-compose中的基础镜像即可。
-
遇到的另一个比较奇怪的问题就是在java -jar命令前添加了sh wait-for.sh后配置的environment环境变量不生效了,目前想到的一个解决方案是在java -jar启动应用时配置spring.profiles.active=nacos,然后在jar包同级目录新建bootstrap-nacos.yml配置文件,内容如下:
spring: cloud: nacos: server-addr: nacos:8848 discovery: server-addr: nacos:8848 config: server-addr: nacos:8848
以上是我在实际操作过程中遇到的问题和想到的解决方案,如果大家在过程中再遇到别的问题或者有更好的解决思路的话,欢迎评论区留言交流。
小结
本文基于实际场景通过使用wait-for.sh脚本实现了docker-compose中服务按顺序启动的目标,作为记录的同时也希望能帮助到需要的朋友。
针对以上内容有任何疑问或者更好的思路,欢迎评论区留言~~
创作不易,欢迎一键三连~~~