如何在 Docker Compose 中等待 MSSQL?

Posted

技术标签:

【中文标题】如何在 Docker Compose 中等待 MSSQL?【英文标题】:How to wait for MSSQL in Docker Compose? 【发布时间】:2020-06-17 17:11:29 【问题描述】:

我有一个依赖于 MSSQL 的 服务(一个 ASP.NET Core Web 应用程序)。这些服务是使用 Docker compose 编排的,我希望 docker compose 首先启动数据库并等待它准备好,然后再运行我的服务。为此,我将docker-compose.yml 定义为:

version: '3.7'

services:

  sql.data:
    container_name: db_service
    image: microsoft/mssql-server-linux:2017-latest
    healthcheck:
      test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "http://localhost:1433", "-U", "sa", "-P", "Pass_word", "-Q", "SELECT 1", "||", "exit 1"]

  my_service:
    container_name: my_service_container
    image: $DOCKER_REGISTRY-my_service
    build:
      context: .
      dockerfile: MyService/Dockerfile
    depends_on:
      - sql.data

通过此健康检查,Docker compose 不会等待数据库服务准备好,而是在之后立即启动 my_service,并且正如预期的那样,my_service 无法连接到数据库。部分日志是:

Recreating db_service ... done
Recreating my_service_container ... done
Attaching to db_service, my_service_container 
my_service_container | info: ...Context[0]
my_service_container |       Migrating database associated with context Context
my_service_container | info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
my_service_container |       Entity Framework Core 3.1.1 initialized 'Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: MigrationsAssembly=MyService
my_service_container | fail: Context[0]
my_service_container |       An error occurred while migrating the database used on context Context
my_service_container | Microsoft.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 40 - Could not open a connection to SQL Server)
...
exception details
...
my_service_container | ClientConnectionId:00000000-0000-0000-0000-000000000000
my_service_container exited with code 0
db_service | 2020-03-05 05:45:51.82 Server      Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64)
        Nov 30 2018 12:57:58
        Copyright (C) 2017 Microsoft Corporation
        Developer Edition (64-bit) on Linux (Ubuntu 16.04.5 LTS)
2020-03-05 05:45:51.82 Server      UTC adjustment: 0:00
2020-03-05 05:45:51.82 Server      (c) Microsoft Corporation.
2020-03-05 05:45:51.82 Server      All rights reserved.
2020-03-05 05:45:51.82 Server      Server process ID is 4120.
2020-03-05 05:45:51.82 Server      Logging SQL Server messages in file '/var/opt/mssql/log/errorlog'.
2020-03-05 05:45:51.82 Server      Registry startup parameters:
         -d /var/opt/mssql/data/master.mdf
         -l /var/opt/mssql/data/mastlog.ldf
         -e /var/opt/mssql/log/errorlog

如日志所示,docker compose 首先启动数据库,但在运行我的服务之前不等待它准备好。

我为healthcheck 尝试了不同的语法,例如:

test: /opt/mssql-tools/bin/sqlcmd -S http://localhost:1433 -U sa -P $SA_PASSWORD -Q "SELECT 1" || exit 1

但两者都没有按预期工作。

我在网上查了以下资源,但都无法解决问题:

*** answer github comment github sample

version 3.7 甚至支持此功能吗? because of this confusing comment


问题

对于等待 MSSQL 服务启动的最佳方式有什么想法吗?

【问题讨论】:

【参考方案1】:

您也可以延迟 docker 启动,直到 mssql 启动:

docker-compose.yaml

  mssql:
    image: mcr.microsoft.com/mssql/server:2017-latest
    ports:
      - 1433:1433
    environment:
      SA_PASSWORD: "t9D4:EHfU6Xgccs-"
      ACCEPT_EULA: "Y"
    networks:
      - backend
    command:
      - /bin/bash
      - -c
      - |
        /opt/mssql/bin/sqlservr
        curl -s https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh | bash /dev/stdin localhost:1433

【讨论】:

【参考方案2】:

使用 Kubernetes Deployment,以下探针成功识别就绪状态:

      containers:
        - name: mssql
          image: mcr.microsoft.com/mssql/server:2019-latest
          ports:
            - containerPort: 1433
          env:
            - name: ACCEPT_EULA
              value: 'Y'
          startupProbe:
            exec:
              command:
                - /bin/sh
                - '-c'
                - '/opt/mssql-tools/bin/sqlcmd -U sa -P "$SA_PASSWORD" -Q "SELECT \"READY\"" | grep -q "READY"'
            failureThreshold: 15
            periodSeconds: 10

如果您的 Kubernetes 版本为

说明: 无论查询是否返回任何内容,sqlcmd 都会返回“0”状态。但是,grep -q 根据是否存在“READY”一词返回 0 或 1。

我没有使用过 docker-compose,但我怀疑这个命令可以作为健康检查测试(假设 SA_PASSWORD 被注入到环境中),即:

    healthcheck:
      test:
        - /bin/sh
        - '-c'
        - '/opt/mssql-tools/bin/sqlcmd -U sa -P "$SA_PASSWORD" -Q "SELECT \"READY\"" | grep -q "READY"'
Configure Liveness, Readiness and Startup Probes Pod Lifecycle

【讨论】:

【参考方案3】:

这是一个完整的example

version: "3.8"

services:
  ms-db-server:
    image: mcr.microsoft.com/mssql/server
    environment: 
      - SA_PASSWORD=P@ssw0rd
      - ACCEPT_EULA=Y
    volumes:
      - ./data/db/mssql/scripts:/scripts/
    ports:
      - "1433:1433"
    #entrypoint: /bin/bash
    command:
      - /bin/bash
      - -c
      - |
        /opt/mssql/bin/sqlservr &
        pid=$$!

        echo "Waiting for MS SQL to be available ⏳"
        /opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -h-1 -V1 -U sa -P $$SA_PASSWORD -Q "SET NOCOUNT ON SELECT \"YAY WE ARE UP\" , @@servername"
        is_up=$$?
        while [ $$is_up -ne 0 ] ; do
          echo -e $$(date)
          /opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -h-1 -V1 -U sa -P $$SA_PASSWORD -Q "SET NOCOUNT ON SELECT \"YAY WE ARE UP\" , @@servername"
          is_up=$$?
          sleep 5
        done

        for foo in /scripts/*.sql
          do /opt/mssql-tools/bin/sqlcmd -U sa -P $$SA_PASSWORD -l 30 -e -i $$foo
        done
        echo "All scripts have been executed. Waiting for MS SQL(pid $$pid) to terminate."

        wait $$pid

  tempo:
    image: grafana/tempo:latest
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./etc/tempo-local.yaml:/etc/tempo.yaml
      - ./data/tempo-data:/tmp/tempo
    ports:
      - "14268"      # jaeger ingest, Jaeger - Thrift HTTP
      - "14250"      # Jaeger - GRPC
      - "55680"      # OpenTelemetry
      - "3100"       # tempo
      - "6831/udp"   # Jaeger - Thrift Compact
      - "6832/udp"   # Jaeger - Thrift Binary   

  tempo-query:
    image: grafana/tempo-query:latest
    command: ["--grpc-storage-plugin.configuration-file=/etc/tempo-query.yaml"]
    volumes:
      - ./etc/tempo-query.yaml:/etc/tempo-query.yaml
    ports:
      - "16686:16686"  # jaeger-ui
    depends_on:
      - tempo

  loki:
    image: grafana/loki:2.1.0
    command: -config.file=/etc/loki/loki-local.yaml
    ports:
      - "3101:3100"                                   # loki needs to be exposed so it receives logs
    environment:
      - JAEGER_AGENT_HOST=tempo
      - JAEGER_ENDPOINT=http://tempo:14268/api/traces # send traces to Tempo
      - JAEGER_SAMPLER_TYPE=const
      - JAEGER_SAMPLER_PARAM=1
    volumes:
      - ./etc/loki-local.yaml:/etc/loki/loki-local.yaml
      - ./data/loki-data:/tmp/loki

  nodejs-otel-tempo-api:
    build: .
    command: './wait-for.sh ms-db-server:1433 -- node ./dist/server.js'
    ports:
      - "5555:5555"
    environment:
      - OTEL_EXPORTER_JAEGER_ENDPOINT=http://tempo:14268/api/traces
      - OTEL_SERVICE_NAME=nodejs-opentelemetry-tempo
      - LOG_FILE_NAME=/app/logs/nodejs-opentelemetry-tempo.log
      - DB_USER=sa
      - DB_PASS=P@ssw0rd
      - DB_SERVER=ms-db-server
      - DB_NAME=OtelTempo
    volumes:
      - ./data/logs:/app/logs
      - ./etc/wait-for.sh:/app/bin/wait-for.sh   #https://github.com/eficode/wait-for
    depends_on:
      - ms-db-server
      - tempo-query

  promtail:
    image: grafana/promtail:master-ee9c629
    command: -config.file=/etc/promtail/promtail-local.yaml
    volumes:
      - ./etc/promtail-local.yaml:/etc/promtail/promtail-local.yaml
      - ./data/logs:/app/logs
    depends_on:
      - nodejs-otel-tempo-api
      - loki

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./etc/prometheus.yaml:/etc/prometheus.yaml
    entrypoint:
      - /bin/prometheus
      - --config.file=/etc/prometheus.yaml
    ports:
      - "9090:9090"
    depends_on:
      - nodejs-otel-tempo-api

  grafana:
    image: grafana/grafana:7.4.0-ubuntu
    volumes:
      - ./data/grafana-data/datasources:/etc/grafana/provisioning/datasources
      - ./data/grafana-data/dashboards-provisioning:/etc/grafana/provisioning/dashboards
      - ./data/grafana-data/dashboards:/var/lib/grafana/dashboards
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_DISABLE_LOGIN_FORM=true
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
      - tempo-query
      - loki

【讨论】:

【参考方案4】:

在搜索并尝试了许多不同的场景后,我能够使用以下作曲家文件添加等待。这是针对asp.net 核心解决方案的。关键是如果dockerfile 中指定了entrypoint,则必须覆盖它。另外,你需要确保将 "wait-for-it.sh" LF 保存为行尾而不是 CRLF,否则你会得到file not found 的错误。

dockerfile 应该有以下内容(从这里下载:https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh,确保保存文件):

COPY ./wait-for-it.sh /wait-for-it.sh
RUN chmod +x wait-for-it.sh

docker-compose.yml

version: '3.7'

services:

  vc-db:
    image: mcr.microsoft.com/mssql/server:latest
    ports:
      - "$DOCKER_SQL_PORT:-1433:1433"
    expose:  
      - 1433  
    environment: 
      - ACCEPT_EULA=Y
      - MSSQL_PID=Express
      - SA_PASSWORD=v!rto_Labs!
    networks:
      - virto

  vc-platform-web:
    image: virtocommerce/platform:$DOCKER_TAG:-latest
    ports:
      - "$DOCKER_PLATFORM_PORT:-8090:80"
    environment:
      - ASPNETCORE_URLS=http://+
    depends_on:
      - vc-db
    entrypoint: ["/wait-for-it.sh", "vc-db:1433", "-t", "120", "--", "dotnet", "VirtoCommerce.Platform.Web.dll"]
    networks:
      - virto

【讨论】:

如何连接内部mssql DB丢docker?当 docker 将运行时,我在一个带有主机的网络中拥有带有 MSSQL DB 的服务器。在我的简单烧瓶应用程序中,我使用 pyodbc 连接到数据库。在其他应用程序上,我使用带有 docker 的 sqlite db 文件,连接起来很简单。 我觉得这种“等待”和类似的解决方案是半解决方案:如果容器/sql-container 由于错误或任何原因而无法启动怎么办。我们要永远等待吗?或者如果我们使用超时,我们如何确定这个超时在给定的特定环境中是足够的? @EugeneGorbovoy 我认为等待相关服务可用是合理的,此时抛出错误是调度程序的工作。超时是一种技巧,因为我们真的不知道服务启动需要多长时间。【参考方案5】:

创建两个单独的 dockerfile(例如):

    Mssql.Dockerfile App.Dockerfile

在 docker-compose.yml 中设置序列

Mssql.Dockerfile

FROM mcr.microsoft.com/mssql/server AS base

ENV ACCEPT_EULA=Y
ENV SA_PASSWORD=Password123

COPY . .
COPY ["Db/Scripts/*", "Db/Scripts/"]
VOLUME ./Db:/var/opt/mssql/data

HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=10 \
    CMD /opt/mssql-tools/bin/sqlcmd -S . -U sa -P Password123 -i Db/Scripts/SetupDb.sql || exit 1

App.Dockerfile:

    FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
    WORKDIR /app
    EXPOSE 80
    EXPOSE 443

    FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
    WORKDIR /src
    COPY ["AspNetCoreWebApplication/AspNetCoreWebApplication.csproj", "AspNetCoreWebApplication/"]
    COPY ["WebApp.Data.EF/WebApp.Data.EF.csproj", "WebApp.Data.EF/"]
    COPY ["WebApp.Service/WebApp.Service.csproj", "WebApp.Service/"]

    RUN dotnet restore "AspNetCoreWebApplication/AspNetCoreWebApplication.csproj"
    COPY . .
    WORKDIR "/src/AspNetCoreWebApplication"
    RUN dotnet build "AspNetCoreWebApplication.csproj" -c Release -o /app/build
    FROM build AS publish
    RUN dotnet publish "AspNetCoreWebApplication.csproj" -c Release -o /app/publish

    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "AspNetCoreWebApplication.dll"]

Docker-compose.yml:

version: '3.7'

services:
    api:
        image: aspnetcore/mentoring_api
        container_name: mentoring_api
        build:
            context: .
            dockerfile: App.Dockerfile
        ports:
            - 8081:80
        expose: 
            - 8081
        environment:
            ASPNETCORE_ENVIRONMENT: Development
        depends_on:
            - sqlserver
    sqlserver:
        image: aspnetcore/mentoring_db
        container_name: mentoring_db
        build:
            context: .
            dockerfile: Mssql.Dockerfile
        ports:
            - "1433:1433"
        expose: 
            - 1433
        environment:
            - ACCEPT_EULA=Y
            - SA_PASSWORD=Password123
        volumes:
            - ./Db:/var/opt/mssql/data

注意: 连接字符串将如下所示:"Server=sqlserver;Database=Northwind;Trusted_Connection=False;User Id=sa;Password=Password123;MultipleActiveResultSets=true"

【讨论】:

您设置了 ENV SA_PASSWORD 但随后在 HEALTHCHECK 中写出密码 嘿@caduceus,为什么这是个问题? @ShaharShokrani 如果您不打算使用变量,为什么还要设置它? 在您之前的评论中,您是否打算在 ENVCMD 中不需要密码? 如果没有通过健康检查的CMD 提供的密码,不确定健康检查是否能正常工作,也许我错了,但我认为两者都是必要的,值得检查。【参考方案6】:

当您使用depends_on 时,docker-compose 只会以更高的优先级启动您的基础服务,而不会等待启动服务。

有一些有用的外部程序可以帮助您等待特定的服务(端口),然后运行另一个服务。

vishnubob/wait-for-it 是其中之一,它会阻止执行流程,直到您的特定端口准备好。 另一个不错的选择是eficode/wait-for,它已经为 docker-compose 做好了准备。

使用示例(根据 eficode/wait-for docs)

version: '2'

services:
  db:
    image: postgres:9.4

  backend:
    build: backend
    # Blocks execution flow util db:5432 is ready (Or you can use localhost instead)
    command: sh -c './wait-for db:5432 -- npm start'
    depends_on:
      - db

-- 更新--

假设您有一个依赖于 PostgreSQL 等数据库的 Python 应用程序,并且您的应用程序将使用以下命令运行:python app.py 正如Official Docker Document 所说,将vishnubob/wait-for-it 放入您的图像中(在您的其他项目文件中,如app.py

现在只需将此行放入您的docker-compose.yml

version: "3"
services:
  web:
    build: .
    ports:
      - "80:8000"
    depends_on:
      - "db"
    # This command waits until `db:5432` respond (5432 is default PostgreSQL port)
    # then runs our application by this command: `python app.py`
    command: ["./wait-for-it.sh", "db:5432", "--", "python", "app.py"]
  db:
    image: postgres

注意:不要忘记将此命令放在您的图像文件中的 Dockerfile 中:

# Copy wait-for-it.sh into our image
COPY wait-for-it.sh wait-for-it.sh
# Make it executable, in Linux
RUN chmod +x wait-for-it.sh

【讨论】:

能否提供一个完整的功能示例?例如,./wait-for 命令应该安装在db 图像上? 我在镜像文件夹的根目录下添加了 wait-for-it.sh 文件,在构建时,Docker 20.10.2 说:chmod: wait-for-it.sh: Operation not allowed在 Windows 10 19.09 上。有什么想法吗? @JoanComasFdz 是你的容器镜像 Windows,chmod 是一个 linux 命令 @AjayBhasy 我在 Windows 10 1909 和 Docker Desktop 上使用 linux 容器 @JoanComasFdz 我将脚本文件添加到我的项目中,导致它被复制到发布文件夹,然后在入口点之前的 Dockerfile 的发布部分中,我运行了这个命令并且它工作了【参考方案7】:

您可以编写一个简单的脚本,该脚本将在您的应用程序的容器中启动。例如,您可以使用sleep N 设置延迟(其中 N 是启动数据库所需的时间),或者您可以使用until 循环,在该循环中您可以尝试连接到您的数据库,并且在可能的情况下,您可以启动您的应用程序。

我知道这不是一个完美的解决方案,但是当我遇到类似问题时它帮助了我

【讨论】:

检查应用程序内部的连接是个好主意,但 docker 文档本身建议使用 wait-for-it 和替代脚本。 当然,我不反对这个,我只是提供了一个简单(不完美)的解决方案

以上是关于如何在 Docker Compose 中等待 MSSQL?的主要内容,如果未能解决你的问题,请参考以下文章

Docker Compose 在启动 Y 之前等待容器 X

在创建其他服务之前,等待在docker compose中准备好mysql服务

docker-compose 等待启动,直到挂载主机文件系统

sh 等待应用程序从https://docs.docker.com/compose/startup-order/完全在docker容器中运行

Docker compose 等待数据库服务初始化

使用docker-compose时,Healthcheck根本不起作用(我的服务在启动前不等待Kafka启动)