【工作记录】基于wait-for.sh控制docker-compose中服务启动顺序的一次实践@20240219

背景

这个问题其实由来已久,尤其是在微服务部署过程中体现的尤为明显,就是服务的启动顺序问题。

一个很常见的场景就是微服务启动过程中,应用服务会依赖于配置中心、注册中心、数据库、redis等基础服务,所以这些基础服务应当优先于应用服务启动。

还有就是在服务器重启时由于容器设置了自动重启,如果不控制启动顺序的话就就会导致有些服务去读取配置中心配置时,由于配置中心尚未完全启动成功导致应用服务启动中断或者失败。

综上,这个问题还是有必要研究下彻底解决掉的,于是有了这篇记录实践过程的文章。

解决方案

看到这个问题时有以下三种思路:

  1. 使用docker-compose提供的depends_on:
    • 被依赖的服务确实会优先启动,
    • 不能保证应用服务启动时被依赖的服务已经完全启动成功
  2. 把docker-compose文件拆开:
    • 拆开成基础服务和应用服务,人工确保先后顺序。
    • 不能解决服务器重启后服务自动按顺序启动
  3. 使用脚本监听依赖服务端口: 等依赖服务端口可以监听到时再启动应用服务
    • 脚本需要在实际执行命令前执行,且需要明确必备依赖服务及端口

通过网络的搜索,大致也就这三种解决思路,对比下来还是第三种比较靠谱也解决的比较彻底,本文也是基于这种思路完成的实践。

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"

说明如下:

  1. 关于nacos和mysql的配置可以参考【工作记录】基于docker+mysql部署单机版nacos2.0.4@20240219-CSDN博客
  2. gateway服务其实就是简单的集成了springcloudgateway网关服务,同时注册到了nacos中,代码如有必要后续再补充,不是本文重点。
  3. 在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

具体代码不做过多解析,主要完成的就是参数解析,接口监听然后就是等待,感兴趣的朋友可以自行研究

问题与解决

  1. 遇到的第一个问题是容器启动时提示找不到nc命令,是因为容器内部没有netcat这个工具,一个解决方案是在基础镜像的基础上安装上netcat这个工具,然后重新打包镜像并修改docker-compose中的基础镜像即可。

  2. 遇到的另一个比较奇怪的问题就是在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中服务按顺序启动的目标,作为记录的同时也希望能帮助到需要的朋友。

针对以上内容有任何疑问或者更好的思路,欢迎评论区留言~~

创作不易,欢迎一键三连~~~

你可能感兴趣的:(docker,容器,运维)