Docker-compose编排微服务顺序启动解决方案

一、前言

docker-compose可以方便组合多个 docker 容器服务, 但是, 当容器服务之间存在依赖关系时, docker-compose 并不能保证服务的启动顺序。docker-compose 中的 depends_on 配置是容器的启动顺序, 并不是容器中服务的启动顺序。本章将详细叙述如何解决 docker-compose 顺序启动微服务的问题。

二、解决方案

经过两天的努力,大致总结出了三种解决顺序启动微服务的方案:

  • 足够的容错和重试机制,比如从配置中心获取配置文件,服务消费者可以不断的重试直到连上为止,这里就用到了docker-compose 中的 restart配置,docker-compose.yml如下:
 version: "3"
  services:
    # 指定服务名称
    #服务注册与发现中心
    simonEureka:
      image: simon/eureka-server:2.0.1-SNAPSHOT
      hostname: simonEureka
      ports:
        - "8100:8100"
    #配置中心    
    simonConfig:
      image: simon/config-server:2.0.1-SNAPSHOT
      hostname: simonConfig
      ports:
        - "8101:8101"
      depends_on:
        - simonEureka
      restart: always
    #路由网关  
    apigateway:
      image: simon/apigateway:2.0.1-SNAPSHOT
      ports:
        - "8102:8102"
      depends_on:
        - simonEureka
        - simonConfig
      restart: always
    #监控平台  
    admin:
      image: simon/admin:2.0.1-SNAPSHOT
      ports:
        - "8103:8103"
      depends_on:
        - simonEureka
        - simonConfig
      restart: always
  • docker-compose.yml进行拆分,分成两部分部署, 将要先启动的服务放在一个docker-compose中,后启动的服务放在第二个docker-compose中,启动两次,两者使用同一个网络,启动命令示例:

    $ docker-compose -f docker-compose-commond.yml up
    
  • 同步等待,使用shell脚本阻止当前服务启动,直到所需依赖的服务全部启动之后再启动当前服务。

下面我将详细的讲述第三种解决顺序启动问题的方案。部署的微服务清单如下:

服务名 端口 服务说明 依赖服务 启动优先级(优先级越高越先启动)
eureka-service 8100 服务注册与发现 --- 1
config-server 8101 配置中心 eureka-server 2
apigateway 8102 网关服务 eureka-server,config-server 3
admin 8103 监控服务 eureka-server,config-server 3

2.1 各微服务镜像构建配置

由于各微服务的镜像构建配置差不多,这里只列举配置中心的配置:

            <!-- Docker maven plugin -->
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>1.0.0</version>
                <configuration>
                    <imageName>simon/${project.artifactId}:${project.version}</imageName>
                    <!--<dockerDirectory>src/main/docker</dockerDirectory>-->
                    <forceTags>true</forceTags>
                    <baseImage>java</baseImage>
                    <!--安装镜像所需要的软件-->
                    <runs>
                        <!--同步 /etc/apt/sources.list 和 /etc/apt/sources.list.d 中列出的源的索引,这样才能获取到最新的软件包-->
                        <run>["apt-get","update"]</run>
                        <!--安装netcat-->
                        <run>["apt-get","-y","install","netcat"]</run>
                    </runs>
                    <entryPoint>["java","-jar","/${project.build.finalName}.jar"]</entryPoint>
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                </configuration>
            </plugin>

runs标签表示在构建镜像的时候,会顺序执行标签run中的命令,因为后面顺序启动微服务需要镜像中包含netcat,所以在构建镜像的时候要进行安装。

2.2 同步等待脚本和使用

下面一共提供两种脚本,但前提是镜像中必须如上一节安装netcat

2.2.1 检测服务是否启动的脚本wait-for.sh

#!/bin/sh

TIMEOUT=15
QUIET=0

echoerr() {
  if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
  exitcode="$1"
  cat << USAGE >&2
Usage:
  $cmdname host:port [-t timeout] [-- command args]
  -q | --quiet                        Do not output any status messages
  -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
  -- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
  exit "$exitcode"
}

wait_for() {
  for i in `seq $TIMEOUT` ; do
    nc -z "$HOST" "$PORT" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
        exec "$@"
      fi
      exit 0
    fi
    sleep 1
  done
  echo "Operation timed out" >&2
  exit 1
}

while [ $# -gt 0 ]
do
  case "$1" in
    *:* )
    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
    shift 1
    ;;
    -q | --quiet)
    QUIET=1
    shift 1
    ;;
    -t)
    TIMEOUT="$2"
    if [ "$TIMEOUT" = "" ]; then break; fi
    shift 2
    ;;
    --timeout=*)
    TIMEOUT="${1#*=}"
    shift 1
    ;;
    --)
    shift
    break
    ;;
    --help)
    usage 0
    ;;
    *)
    echoerr "Unknown argument: $1"
    usage 1
    ;;
  esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
  echoerr "Error: you need to provide a host and port to test."
  usage 2
fi

wait_for "$@"

查看使用示例输入一下命令:

./wait-for.sh --help

示例:

$ ./wait-for.sh www.baidu.com:80 -- echo "baidu is up"

baidu is up

2.2.2 docker-compose.yml

#docker compose编排微服务脚本
version: "3"
services:
  # 指定服务名称
  simonEureka:
    image: simon/eureka-server:2.0.1-SNAPSHOT
    hostname: simonEureka
    ports:
      - "8100:8100"
  simonConfig:
    image: simon/config-server:2.0.1-SNAPSHOT
    hostname: simonConfig
    ports:
      - "8101:8101"
    volumes:
      - "./wait-for.sh:/wait-for.sh"
    entrypoint: "sh /wait-for.sh ibaseEureka:8100 -- java -jar /config-server.jar"  
  • 关于配置中心和服务注册中心的配置有疑惑的可以查看Docker Compose编排微服务关于localhost问题的描述;
  • 实际使用中, 也可以将 wait-for.sh 打包到发布的镜像之中, 不用通过 volumes 配置来加载wait-for.sh脚本;
  • entrypoint配置会覆盖maven docker插件entrypoint标签的配置而执行,这里就是控制服务启动顺序的关键配置。

2.2.3 检测服务是否启动的脚本entrypoint.sh

#!/bin/bash
#set -x
#******************************************************************************
# @file    : entrypoint.sh
# @author  : simon
# @date    : 2018-08-28 15:18:43
#
# @brief   : entry point for manage service start order
# history  : init
#******************************************************************************

: ${SLEEP_SECOND:=2}

wait_for() {
    echo Waiting for $1 to listen on $2...
    while ! nc -z $1 $2; do echo waiting...; sleep $SLEEP_SECOND; done
}

declare DEPENDS
declare CMD

while getopts "d:c:" arg
do
    case $arg in
        d)
            DEPENDS=$OPTARG
            ;;
        c)
            CMD=$OPTARG
            ;;
        ?)
            echo "unkonw argument"
            exit 1
            ;;
    esac
done

for var in ${DEPENDS//,/}
do
    host=${var%:*}
    port=${var#*:}
    wait_for $host $port
done

eval $CMD
#避免执行完命令之后退出容器
tail -f /dev/null

这个脚本有 2 个参数,:

  • -d: 需要等待的服务和端口,例如:simonEureka:8080,simonEureka:8080,simonConfig:8081;
  • -c: 等待的服务和端口启动之后, 自己的启动命令,例如:java -jar eureka.jar

2.2.4 docker-compose.yml

#docker compose编排微服务脚本
version: "3"
services:
  # 指定服务名称
  simonEureka:
    image: simon/eureka-server:2.0.1-SNAPSHOT
    hostname: simonEureka
    ports:
      - "8100:8100"
  simonConfig:
    image: simon/config-server:2.0.1-SNAPSHOT
    hostname: simonConfig
    ports:
      - "8101:8101"
    depends_on:
      - simonEureka
    volumes:
      - "./entrypoint.sh:/entrypoint.sh"
    environment:
      SLEEP_SECOND: 4
    tty: true
    entrypoint: /entrypoint.sh -d simonEureka:8100 -c 'java -jar /config-server.jar';
  apigateway:
    image: simon/apigateway:2.0.1-SNAPSHOT
    ports:
      - "8102:8102"
    depends_on:
      - simonEureka
      - simonConfig
    volumes:
      - "./entrypoint.sh:/entrypoint.sh"
    environment:
      SLEEP_SECOND: 4
    tty: true
    entrypoint: /entrypoint.sh -d simonEureka:8100,simonConfig:8101 -c 'java -jar /apigateway.jar';
  admin:
    image: simon/admin:2.0.1-SNAPSHOT
    ports:
      - "8103:8103"
    depends_on:
      - simonEureka
      - simonConfig
    volumes:
      - "./entrypoint.sh:/entrypoint.sh"
    environment:
      SLEEP_SECOND: 4
    tty: true
    entrypoint: /entrypoint.sh -d simonEureka:8100,simonConfig:8101 -c 'java -jar /admin.jar';
  • 关于配置中心和服务注册中心的配置有疑惑的可以查看Docker Compose编排微服务关于localhost问题的描述;
  • 实际使用中, 也可以将 entrypoint.sh 打包到发布的镜像之中, 不用通过 volumes 配置来加载entrypoint.sh脚本;
  • entrypoint配置会覆盖maven docker插件entrypoint标签的配置而执行,这里就是控制服务启动顺序的关键配置。

启动服务

$ docker-compose up
[root@simon simon2.0]# docker-compose up
Starting simon20_simonEureka_1 ... done
Starting simon20_simonConfig_1 ... done
Starting simon20_admin_1       ... done
Starting simon20_apigateway_1  ... done
Attaching to simon20_simonEureka_1, simon20_simonConfig_1, simon20_admin_1, simon20_apigateway_1
simonConfig_1  | Waiting for simonEureka to listen on 8100...
simonConfig_1  | waiting...
admin_1        | Waiting for simonEureka to listen on 8100...
admin_1        | waiting...
apigateway_1   | Waiting for simonEureka to listen on 8100...
apigateway_1   | waiting...
......

其它服务都在等待simonEureka服务启动,这样就实现了服务的顺序启动,最终所有服务全部启动,如下是注册服务信息:

服务注册信息

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容