为什么 Docker Compose 的重启会导致容器输出越来越多次?

6
我正在编写一个基于Docker Compose的Web应用程序,其中包含多个后台系统 - HTTP API、HTTP代理和队列。所有内容都在PHP Alpine容器中进行,使用的是PHP 5.6或7.0。 最初,我在API容器内部使用Supervisor设置了队列,这很好用。但是,Supervisor/Python使容器变得比它们应该的更大(80M而不是25M),所以我将队列移动到了自己的容器中。它会存活约5分钟并退出以便重新启动,并且我使用了Supervisor中的自动重启系统,因此我已经切换到了Docker Compose中的重启系统。我正在使用Compose YAML格式的版本2。 当队列启动时,它会向stdout输出一个简单的消息:
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)

当我首次执行docker-compose up时,这是正常的。但是,在每次重新启动时,我会收到三条消息,然后变成五条等等,而且没有限制。如果我执行docker ps,它则显示只有一个队列正在运行:

halfer@machine:~/proximate-app$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
a9c94558769d        proximate-app       "/tmp/container-st..."   2 hours ago         Up 2 hours          0.0.0.0:8084->8084/tcp   app_instance
7e48d6aec459        proximate-api       "sh /tmp/bin/web-s..."   2 hours ago         Up 2 hours          0.0.0.0:8080->8080/tcp   api_instance
86c564becadf        proximate-queue     "sh /var/app/bin/c..."   2 hours ago         Up About a minute                            queue_instance
20c2145f80e4        proximate-proxy     "sh /var/proxy/con..."   2 hours ago         Up 2 hours          0.0.0.0:8081->8081/tcp   proxy_instance

以下是我的Compose文件:

version: '2'
services:

  proximate-app:
    container_name: "app_instance"
    image: proximate-app
    ports:
    - "8084:8084"
    links:
    - proximate-api

  # @todo Remove external ports once everything is happy
  proximate-api:
    container_name: "api_instance"
    image: proximate-api
    ports:
    - "8080:8080"
    links:
    - proximate-proxy
    - proximate-queue
    volumes:
    - proximate-volume:/remote/cache
    - proximate-q-volume:/remote/queue
    # Use time and TZ from the host, could alternatively use env vars and set it
    # manually in the container, see https://wiki.alpinelinux.org/wiki/Setting_the_timezone
    - /etc/localtime:/etc/localtime:ro
    - /etc/timezone:/etc/timezone:ro
    # Should perhaps pass this as a var to docker-compose so as not to hardwire it,
    # but it is fine for now
    environment:
    - PHP_TIMEZONE=Europe/London

  proximate-queue:
    container_name: "queue_instance"
    image: proximate-queue
    restart: always
    links:
    - proximate-proxy
    volumes:
    - proximate-volume:/remote/cache
    - proximate-q-volume:/remote/queue
    environment:
    - PROXY_ADDRESS=proximate-proxy:8081

  # @todo Remove external ports once everything is happy
  proximate-proxy:
    container_name: "proxy_instance"
    image: proximate-proxy
    ports:
    - "8081:8081"
    volumes:
    - proximate-volume:/remote/cache
    environment:
    - PROXY_LOG_PATH=/remote/cache/proxy.log

volumes:
  proximate-volume:
  proximate-q-volume:

相应的容器是proximate-queue

我很确定我的容器本身不负责这种怪异现象。我的Dockerfile如下:

ENTRYPOINT ["sh", "/var/app/bin/container-start.sh"]

这只是调用了一个启动脚本:

#!/bin/sh

php \
    /var/app/bin/queue.php \
    --queue-path /remote/queue \
    --proxy-address ${PROXY_ADDRESS}

这个命令会运行一个队列进程:

#!/usr/bin/env php
<?php

use Proximate\Service\File;
use Proximate\Service\SiteFetcher as SiteFetcherService;
use Proximate\Queue\Read as QueueReader;

$root = realpath(__DIR__ . '/..');
require_once $root . '/vendor/autoload.php';

$actions = getopt('p:q:', ['proxy-address:', 'queue-path:']);

$queuePath = isset($actions['queue-path']) ? $actions['queue-path'] : (isset($actions['q']) ? $actions['q'] : null);
$proxyAddress = isset($actions['proxy-address']) ? $actions['proxy-address'] : (isset($actions['p']) ? $actions['p'] : null);

if (!$queuePath || !$proxyAddress)
{
    $command = __FILE__;
    die(
        sprintf("Syntax: %s --proxy-address <proxy:port> --queue-path <queue-path>\n", $command)
    );
}

if (!file_exists($queuePath))
{
    die(
        sprintf("Error: the supplied queue path `%s` does not exist\n", $queuePath)
    );
}

echo sprintf(
    "Starting queue watcher (path=%s, proxying to %s)\n",
    $queuePath,
    $proxyAddress
);

$queue = new QueueReader($queuePath, new File());
$queue->
    setFetcher(new SiteFetcherService($proxyAddress))->
    process();
正如您所看到的,echo sprintf()是启动的通告,并且没有任何循环可以在我的端口执行此操作。这可能是Docker Compose的一个错误吗?我正在使用Ubuntu 14.04上的docker-compose version 1.9.0, build 2585387。 供参考,Docker Compose stdout类似于以下内容(队列中重复的行可见):
halfer@machine:~/proximate-app$ docker-compose up
Creating network "proximateapp_default" with the default driver
Creating proxy_instance
Creating queue_instance
Creating api_instance
Creating app_instance
Attaching to proxy_instance, queue_instance, api_instance, app_instance
proxy_instance     | Teeing proxy logs also to /remote/cache/proxy.log
proxy_instance     | [2017-05-10 09:18:42] stdout.INFO: Setting up queue at `/remote/cache/data` [] []
proxy_instance     | [2017-05-10 09:18:42] stdout.INFO: Starting proxy listener on 172.18.0.2:8081 [] []
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
api_instance       | PHP 7.0.16 Development Server started at Wed May 10 10:19:00 2017
app_instance       | PHP 5.6.29 Development Server started at Wed May 10 09:19:10 2017
app_instance       | PHP 5.6.29 Development Server started at Wed May 10 09:19:10 2017
queue_instance exited with code 0
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance exited with code 0
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)
queue_instance     | Starting queue watcher (path=/remote/queue, proxying to proximate-proxy:8081)

我可以尝试的一件事是让应用程序休眠并且不执行其他任何操作,以证明某些奇怪的退出处理程序或其他东西没有造成混乱。然而,我预计这将产生完全相同的结果。

更新

我已经用一个脚本替换了队列,该脚本会打印时间信息然后休眠20秒。这是我得到的结果:

halfer@machine:~/proximate-app$ docker-compose up
Creating network "proximateapp_default" with the default driver
Creating proxy_instance
Creating queue_instance
Creating api_instance
Creating app_instance
Attaching to proxy_instance, queue_instance, api_instance, app_instance
proxy_instance     | Teeing proxy logs also to /remote/cache/proxy.log
proxy_instance     | [2017-05-10 11:51:17] stdout.INFO: Setting up queue at `/remote/cache/data` [] []
proxy_instance     | [2017-05-10 11:51:17] stdout.INFO: Starting proxy listener on 172.18.0.2:8081 [] []
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:51:27 +0000. Microtime=1494417087.107185
api_instance       | PHP 7.0.16 Development Server started at Wed May 10 12:51:37 2017
app_instance       | PHP 5.6.29 Development Server started at Wed May 10 11:51:46 2017
app_instance       | PHP 5.6.29 Development Server started at Wed May 10 11:51:46 2017
queue_instance exited with code 0
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:51:27 +0000. Microtime=1494417087.107185
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:51:55 +0000. Microtime=1494417115.178871
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:52:22 +0000. Microtime=1494417142.409513
queue_instance exited with code 0
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:51:27 +0000. Microtime=1494417087.107185
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:51:55 +0000. Microtime=1494417115.178871
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:52:22 +0000. Microtime=1494417142.409513
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:52:49 +0000. Microtime=1494417169.612523
queue_instance     | Hello everyone! Time=Wed, 10 May 2017 11:53:17 +0000. Microtime=1494417197.826749

看起来情况是这样的:

  • 重启只报告每两次重启一次
  • 我的任务将需要确切的20秒钟,但Compose正在相当缓慢地重新启动它们(接近每30秒一次)。这对我来说并不太烦人,但可能是一个有用的观察结果
  • 在重复出现的行中,实际上是旧的重启报告。

到底发生了什么?


这个序列如何继续?1、3、5... - Robert
你的测试脚本是做什么的?如果它退出了,由于docker-compose.yml中的restart: always,它将会被重新启动。 - Robert
1
累计输出是由于容器每次退出时未被删除。因此,docker-compose会再次启动它,并显示从正在重新启动的同一容器中的docker logs -f - Robert
@Robert:回答第一个问题,更新中的简单测试脚本打印时间信息,休眠二十秒,然后退出。 - halfer
关于您上一个问题,我想知道是否需要提高我的Docker术语理解。据我所知,容器是图像加上当前文件系统状态的叠加层(可以运行或未运行)。因此,一旦容器完成一次运行,您所说的是容器在每次重启时不会从图像重新创建。在我的设计中,无论哪种方式都没有关系,所以我可以要求Docker重新创建一个新的容器,这样我只能看到新的日志吗? - halfer
显示剩余3条评论
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
4

我总结了我们发现的问题。

累计输出是由于容器每次退出时未被删除。因此,docker-compose会再次启动它,向您显示从重新启动的同一容器中的docker logs -f。这符合预期。

我没有找到一种方法可以在同一个docker-compose up中启动之前删除容器。

我喜欢你的基于队列的架构,但我认为如果你使用一个长期运行的容器而不是一个启动/退出/启动/退出的容器会更好。您可以在shell脚本中执行简单的while true; do ./queue-app-blabla.php; sleep 20; done来实现它。

任何需要重现这个问题的人,都可以使用以下docker-compose.yml

version: "2"

services:
  ui:
    image: ubuntu
    command: tail -f /dev/null
    links:
      - queue
  queue:
    image: ubuntu
    command: sh -c "date; echo Sleeping...; sleep 20; echo exit."
    restart: always

输出:

queue_1  | Mon May 15 04:32:12 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
docker_queue_1 exited with code 0
queue_1  | Mon May 15 04:32:12 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
queue_1  | Mon May 15 04:32:33 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
queue_1  | Mon May 15 04:32:54 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
docker_queue_1 exited with code 0
queue_1  | Mon May 15 04:32:12 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
queue_1  | Mon May 15 04:32:33 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
queue_1  | Mon May 15 04:32:54 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
queue_1  | Mon May 15 04:33:17 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.
queue_1  | Mon May 15 04:33:38 UTC 2017
queue_1  | Sleeping...
queue_1  | exit.

我的解决方案:

version: "2"

services:
  ui:
    image: ubuntu
    command: tail -f /dev/null
    links:
      - queue
  queue:
    image: ubuntu
    command: sh -c "while true; do date; echo Sleeping...; sleep 20; echo loop.; done"
    restart: always

输出:

Attaching to docker_queue_1, docker_ui_1
queue_1  | Mon May 15 04:36:16 UTC 2017
queue_1  | Sleeping...
queue_1  | loop.
queue_1  | Mon May 15 04:36:36 UTC 2017
queue_1  | Sleeping...
queue_1  | loop.
queue_1  | Mon May 15 04:36:56 UTC 2017
queue_1  | Sleeping...
queue_1  | loop.
queue_1  | Mon May 15 04:37:16 UTC 2017
queue_1  | Sleeping...
我认为更好的方式是观察容器的启动和工作过程,而不是反复启动/退出容器。Docker对于具有此行为的容器有特殊处理,详见Docker文档“每次重新启动之前都会添加一个递增的延迟时间(从100毫秒开始加倍),以防止服务器被洪水般的请求淹没。”但由于您的容器已经运行了>10秒,因此不适用于您的情况。
编辑: 检查可能的错误并退出。这可能会很有用:
#!/bin/bash

PERIOD=10 # seconds

while true; do
  echo Starting process...

  date # the process
  STATUS=$?

  if [ $STATUS -ne 0 ]; then
        echo "Error found. Exiting" >&2
        exit $STATUS
  fi

  echo End process. Sleeping ${PERIOD}s
  sleep $PERIOD
done

输出:

▶ ./script.sh
Starting process...
Mon May 15 11:43:06 ART 2017
End process. Sleeping 10s
Starting process...
Mon May 15 11:43:16 ART 2017
End process. Sleeping 10s
Starting process...
Mon May 15 11:43:26 ART 2017
End process. Sleeping 10s
Starting process...
Mon May 15 11:43:36 ART 2017
End process. Sleeping 10s
Starting process...
Mon May 15 11:43:46 ART 2017
End process. Sleeping 10s

感谢您的努力编写这篇文章,Robert!我的方法是运行队列一段时间,然后退出重新启动,这是基于长时间运行的PHP服务的一个好主意 - 它们曾经会泄漏内存,因此使用Cron或Supervisord定期重启通常被认为是一个好主意。公平地说,PHP的内存管理(特别是循环引用)现在要好得多了,但是旧习惯难改 - 无论如何,以新实例开始感觉很好。 - halfer
你使用shell循环的方法是不错的,所以我会尝试一下。唯一的缺点是Docker Compose无法检测到一系列失败(因此它可以使用你在最后一段提到的防洪策略,虽然我想我可以用Bash写一个基本的策略)。但是,我敢说如果我需要更复杂的设施,那么我应该转向Swarm或Kubernetes(而且我真的没有时间学习新系统,所以我现在很高兴使用Compose)。 - halfer
@halfer,关于可能的内存泄漏问题,请考虑以下:每个循环中PHP进程都会死亡,因此其资源将被内核释放。唯一长期存在的进程将是shell脚本。 - Robert
是的,非常正确。我会试一试!我会尝试报告结果,但似乎需要重新构建机器,可能需要几天时间。我认为Docker已经占用了我所有的INode,而且很难构建任何东西 :-) - halfer
祝你好运!:-) - Robert

2
使用Docker重启功能只为重新启动应该是一个服务的长时间运行的队列任务,这实际上是不必要的,因为操作系统已经具备你所需要的一切。 在使用docker重启功能之前,你自己重启了队列。但你写道,你放弃了Supervisor/Python,因为在你看来它太臃肿(~55M)。 所以我理解你想要保持最小化的占用空间。 让我们做到这一点。因此,我的建议是从最基本的操作系统开始。它应该拥有你队列处理管理所需的一切,因为这是操作系统的共性:调用和控制其他进程(为了保持列表简洁)。 即使对于带有Busybox的Alpine Linux PHP映像,其命令和shell也被减少(例如没有Bash),这一点也是正确的。 因此,在我的示例中,我假设你想每5分钟触发PHP脚本,然后再次启动它(如果没有超时,也可以这样做,且代码更少)。 Docker镜像附带了timeout命令,它可以做到这一点。你可以在Dockerfile entrypoint sh脚本中使用它。因此,PHP被调用并在300秒(五分钟)后终止。
# executed by sh command from BusyBox v1.24.2 (2017-01-18 14:13:46 GMT) multi-call binary
#
# usage: ENTRYPOINT ["sh", "docker-entrypoint.sh"]
set -eu

timed_queue() {
  set +e
  timeout \
    -t 300 \
    php -d max_execution_time=301 \
      -f /queue.php -- \
      --proxy-address "${PROXY_ADDRESS}" \
      --queue-path /remote/queue \
    ;
  retval=$?
  set -e
}

timed_queue
while [ $retval -eq 143 -o $retval -eq 0 ]; do
  timed_queue
done

使用命令行参数-t 300设置超时时间。如果超时,退出状态默认为143(SIGTERM)。

如果队列脚本成功结束,返回0。在两种情况下,脚本都会再次启动(带有超时设置)。

这需要对您的队列脚本进行额外的微小更改。您可以按照以下方式使用die命令来信号错误:

die(
    sprintf("Error: the supplied queue path `%s` does not exist\n", $queuePath)
);

不要将错误消息输出到标准输出,而是输出到标准错误输出并使用非零状态代码退出(例如1):

fprintf(STDERR, "Error: the supplied queue path `%s` does not exist\n", $queuePath);
die(1);

这将使脚本与操作系统兼容。在命令行中,退出状态为零(0)表示OK(即使用像您示例中的带有字符串消息的die("string")),但非零代码显示错误(首先使用1)。

完成后,您将获得重新启动和超时脚本的功能,几乎没有任何开销。无论是文件大小还是处理时间都是如此。我已经进行了一秒钟的超时测试,并且在重新启动时只显示了一些微秒的极小开销:

queue_instance     | info: 2017-05-19T14:00:24.34824700 queue.php started...
queue_instance     | info: 2017-05-19T14:00:25.35548200 queue.php started...
queue_instance     | info: 2017-05-19T14:00:26.34564400 queue.php started...
queue_instance     | info: 2017-05-19T14:00:27.35868700 queue.php started...
queue_instance     | info: 2017-05-19T14:00:28.34597300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:29.34139800 queue.php started...
queue_instance     | info: 2017-05-19T14:00:30.26049500 queue.php started...
queue_instance     | info: 2017-05-19T14:00:31.26174500 queue.php started...
queue_instance     | info: 2017-05-19T14:00:32.26322800 queue.php started...
queue_instance     | info: 2017-05-19T14:00:33.26352800 queue.php started...
queue_instance     | info: 2017-05-19T14:00:34.26533300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:35.26524300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:36.26743300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:37.26889500 queue.php started...
queue_instance     | info: 2017-05-19T14:00:38.27222300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:39.27209000 queue.php started...
queue_instance     | info: 2017-05-19T14:00:40.27620500 queue.php started...
queue_instance     | info: 2017-05-19T14:00:41.27985300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:42.28136100 queue.php started...
queue_instance     | info: 2017-05-19T14:00:43.28252200 queue.php started...
queue_instance     | info: 2017-05-19T14:00:44.28403600 queue.php started...
queue_instance     | info: 2017-05-19T14:00:45.28595300 queue.php started...
queue_instance     | info: 2017-05-19T14:00:46.28683900 queue.php started...
queue_instance     | info: 2017-05-19T14:00:47.28803800 queue.php started...
在您的情况下,微小的差异最有可能被忽略,因为您想运行脚本五分钟。如果进程崩溃(致命错误等),则PHP将具有退出代码127,因此当容器关闭时,您将收到通知。同样,如果Dockerfile入口点脚本中存在错误,例如未定义的环境变量,则也会发生同样的情况。所以,如果我理解您的意思正确,这就是您要寻找的内容。根据最终通过CMD或ENTRYPOINT调用的内容,可能需要更多的进程处理。或者进一步应用更多处理。一些参考资料:替代supervisord进行docker的方法dumb-init对于Docker有多重要?su-exec(plain c)gosu(使用go编写,MBs更多)run-parts (Busybox)

做得好,hakre,谢谢你。你猜对了——队列应该是长时间运行的,但由于我长期以来喜欢 PHP 后台进程定期重启,以清除任何内存泄漏,所以五分钟很好,但可以是任何合理的短时间。 - halfer
你提到了关于退出代码的一个好观点 - 我已经注意到了这一点,并已经尝试修复了一些问题。我肯定会看看 runit,但你和罗伯特建议的 shell 循环应该没问题。小容器,我来了! - halfer
在我看来,你不需要在你的情况下寻找runit,我只是想为未来的访问者提供更多的参考。另一个对于Docker镜像有趣的系统命令是run-parts,如果你喜欢使事情更加模块化的话。 - hakre
这里有另一种替代supervisord的工具,类似于runit;s6进程管理器 - hakre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,