Docker 容器终极之书第三版(四)

原文:annas-archive.org/md5/951792ab738574a4713e2995dc6f7c0c

译者:飞龙

协议:CC BY-NC-SA 4.0

第十二章:12

发送日志和监控容器

在上一章中,我们介绍了 Docker Compose 工具。我们了解到,该工具主要用于在单一 Docker 主机上运行和扩展多服务应用。通常,开发人员和 CI 服务器使用单主机,它们是 Docker Compose 的主要用户。我们看到,该工具使用 YAML 文件作为输入,文件以声明式方式描述应用。我们探讨了该工具可用于的许多有用任务,例如构建和推送镜像,只是列举其中最重要的一些。

本章讨论了日志记录和监控为何如此重要,并展示了如何收集容器日志并将其发送到中央位置,在那里聚合的日志可以被解析以提取有用信息。

你还将学习如何为应用添加监控,使其暴露指标,以及如何抓取并再次将这些指标发送到中央位置。最后,你将学习如何将这些收集到的指标转换为图形仪表盘,用于监控容器化应用。

我们将使用 Filebeat 作为示例,从 Docker 将日志默认指向的/var/lib/docker/containers位置收集日志。在 Linux 上这非常简单。幸运的是,在生产环境或类似生产的系统中,我们通常会选择 Linux 作为操作系统。

在 Windows 或 Mac 机器上收集指标,较之 Linux 机器,稍微复杂一些。因此,我们将生成一个特殊的 Docker Compose 堆栈,包括 Filebeat,可以通过将标准日志输出重定向到一个文件,并将该文件的父文件夹映射到 Docker 卷来在 Mac 或 Windows 计算机上运行。这个卷随后会挂载到 Filebeat 上,Filebeat 再将日志转发到 Elasticsearch。

本章涵盖以下主题:

  • 为什么日志记录和监控如此重要?

  • 发送容器和 Docker 守护进程日志

  • 查询集中日志

  • 收集和抓取指标

  • 监控容器化应用

阅读完本章后,你应该能够完成以下操作:

  • 为你的容器定义日志驱动

  • 安装代理以收集并发送容器和 Docker 守护进程日志

  • 在聚合日志中执行简单的查询,找出有趣的信息

  • 为你的应用服务添加监控,使其暴露基础设施和业务指标

  • 将收集到的指标转换为仪表盘以监控你的容器

技术要求

本章相关的代码可以在github.com/PacktPublishing/The-Ultimate-Docker-Container-Book/tree/main/sample-solutions/ch12找到。

在我们开始之前,确保你已经准备好一个文件夹,用于存放你将在本章中实现的代码。

进入你克隆的代码库所在的文件夹,这个文件夹通常是位于你home文件夹中的The-Ultimate-Docker-Container-Book文件夹:

$ cd ~/The-Ultimate-Docker-Container-Book

创建一个名为ch12的子文件夹并进入该文件夹:

$ mkdir ch12 && cd ch12

不再多说,让我们深入探讨第一个话题:集装箱和守护进程日志。

为什么日志记录和监控很重要?

在处理生产环境或任何类似生产环境的分布式关键任务应用程序时,获取尽可能多的应用内部运行状况的洞察是至关重要的。你是否有机会调查过飞机的驾驶舱或核电站的指挥中心?飞机和电厂都是高度复杂的系统,提供关键任务服务。如果飞机坠毁或电厂意外停运,至少可以说会有很多人受到负面影响。因此,驾驶舱和指挥中心充满了仪器,显示着系统某些部分的当前状态或过去的状态。你在这里看到的,是一些放置在系统关键部分的传感器的视觉表现,这些传感器不断地收集诸如温度或流量等数据。

类似于飞机或电厂,我们的应用程序需要配备“传感器”,这些传感器能够感知我们应用服务或其运行基础设施的“温度”。我将“温度”一词加上了引号,因为它只是一个占位符,代表应用中真正重要的事物,比如某个 RESTful 接口每秒的请求数,或是对同一接口的请求的平均延迟。

我们收集到的结果值或读数,比如请求的平均延迟,通常被称为指标。我们的目标应该是暴露尽可能多的应用服务的有意义的指标。指标可以是功能性指标或非功能性指标。功能性指标是与应用服务的业务相关的值,例如,如果服务是电子商务应用的一部分,则每分钟的结账次数,或者如果我们谈论的是流媒体应用,则过去 24 小时内最受欢迎的 5 首歌曲。

非功能性指标是一些重要的值,这些值与应用程序用于的业务类型无关,例如某个特定 Web 请求的平均延迟、某个接口每分钟返回的 4xx 状态码数量,或者某个服务消耗的 RAM 或 CPU 周期数。

在一个分布式系统中,每个部分都暴露着指标,应该有一个总服务定期收集并聚合来自各个组件的值。或者,每个组件应该将其指标转发到一个中央指标服务器。只有当我们高度分布式系统中所有组件的指标可以在一个中央位置进行检查时,它们才有价值。否则,监控系统就变得不可能。这就像飞机驾驶员在飞行过程中不需要亲自检查飞机的每个重要部件一样;所有必要的读数都会收集并显示在驾驶舱中。

今天,最流行的服务之一是Prometheus,它用于暴露、收集和存储指标。它是一个开源项目,并已捐赠给云原生计算基金会CNCF)。Prometheus 与 Docker 容器、Kubernetes 以及许多其他系统和编程平台具有一流的集成。在本章中,我们将使用 Prometheus 演示如何为一个简单的服务添加指标暴露功能。

在下一节中,我们将向您展示如何将容器和 Docker 守护进程日志发送到一个中央位置。

发送容器和 Docker 守护进程日志

在容器化的世界中,了解 Docker 环境生成的日志对于保持系统健康和正常运行至关重要。本节将概述您将遇到的两种关键日志类型:发送的容器日志Docker 守护进程日志

发送容器日志

当应用程序在容器中运行时,它们会生成日志信息,这些信息提供了有关其性能和潜在问题的宝贵洞察。

可以使用docker logs命令来访问容器日志,后面跟上容器的 ID 或名称。这些日志可以帮助开发人员和系统管理员诊断问题、监控容器活动,并确保已部署应用程序的顺利运行。集中管理和分析容器日志对于优化资源使用、识别性能瓶颈以及排除应用程序问题至关重要。

管理运输容器日志的一些最佳实践包括以下内容:

  • 配置日志轮换和保留策略以防止过度使用磁盘空间

  • 使用日志管理系统将多个容器的日志集中管理

  • 设置日志过滤和警报机制,以识别关键事件和异常

让我们详细了解这些建议,从日志轮换和保留策略开始。

配置日志轮换和保留策略

配置容器日志的日志轮换和保留策略对于防止过度使用磁盘空间并保持最佳性能非常重要。以下是如何为 Docker 容器日志设置这些策略的逐步指南。

配置日志驱动程序

Docker 支持多种日志驱动程序,如 json-filesyslogjournald 等。要配置日志驱动程序,你可以选择全局设置整个 Docker 守护进程的日志驱动程序,或者为每个容器单独设置。在此示例中,我们将使用 json-file 日志驱动程序,这是 Docker 的默认驱动程序。

全局设置日志驱动程序

要全局设置日志驱动程序,请编辑 /etc/docker/daemon.json 配置文件(如果文件不存在,则创建它),并执行以下操作:

  1. 打开 Docker Desktop 的仪表板并导航至 设置,然后选择 Docker 引擎。你应该看到类似于以下内容的界面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.1 – Docker 守护进程配置

  1. 分析现有的配置,并在其中添加以下键值对(如果尚未存在):

    "log-driver": "json-file"
    

这里,(简化后的)结果将如下所示:

{  ...
  "experimental": true,
  "features": {
    "buildkit": true
  },
  "metrics-addr": "127.0.0.1:9323",
  "log-driver": "json-file"
}
  1. 重新启动 Docker 守护进程以应用更改。
本地设置日志驱动程序

如果你更倾向于为单个容器设置日志驱动程序而不是全局设置,请在启动容器时使用 --log-driver 选项:

docker run --log-driver=json-file <image_name>

现在,让我们学习如何指定日志轮换和保留策略。

设置日志轮换和保留策略

我们可以通过为日志驱动程序指定 max-sizemax-file 选项来配置日志轮换和保留策略:

  • max-size:此选项限制每个日志文件的大小。当日志文件达到指定大小时,Docker 会创建一个新文件并开始记录。例如,要将每个日志文件限制为 10 MB,设置 max-size=10m

  • max-file:此选项限制要保留的日志文件数量。当达到限制时,Docker 会删除最旧的日志文件。例如,要只保留最近的五个日志文件,设置 max-file=5

要全局设置这些选项,请将它们添加到 /etc/docker/daemon.json 配置文件中。我们可以在之前添加的 log-driver 节点后面添加 log-opts 部分:

{  ...
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

我们建议你通过 Docker Desktop 的仪表板再次修改守护进程配置。修改配置后,请重新启动 Docker 守护进程以应用更改。

要为单个容器设置这些选项,请在启动容器时使用 --log-opt 选项:

docker run --log-driver=json-file \    --log-opt max-size=10m \
    --log-opt max-file=5 \
    <image_name>

通过配置日志轮换和保留策略,你可以防止磁盘空间的过度使用,并保持 Docker 环境的正常运行。记得根据你的具体使用情况和存储容量选择合适的 max-sizemax-file 值。

使用日志管理系统

使用日志管理系统将多个容器的日志集中管理,对于在 Docker 环境中进行高效监控和故障排除至关重要。这使得你可以将所有容器的日志集中分析,找出模式或问题。在本章中,我们将使用Elasticsearch, Logstash 和 KibanaELK)Stack 作为示例日志管理系统。

ELK Stack

ELK Stack,也称为 Elastic Stack,是一组开源软件产品,旨在促进大规模数据的摄取、存储、处理、搜索和可视化。

ELK 是 Elasticsearch、Logstash 和 Kibana 的缩写,它们是该堆栈的主要组件。

Elasticsearch:Elasticsearch 是一个分布式的、基于 REST 的搜索和分析引擎,建立在 Apache Lucene 之上。它提供了一个可扩展的、近实时的搜索平台,具备强大的全文搜索功能,同时支持聚合和分析。Elasticsearch 通常用于日志和事件数据分析、应用程序搜索,以及各种需要高性能搜索和索引功能的用例。

Logstash:Logstash 是一个灵活的服务器端数据处理管道,能够摄取、处理并将数据转发到多个输出,包括 Elasticsearch。Logstash 支持多种输入源,如日志文件、数据库和消息队列,并可以在转发数据前使用过滤器进行转换和增强。Logstash 通常用于收集和规范化来自不同源的日志和事件,使得在 Elasticsearch 中分析和可视化数据更加容易。

Kibana:Kibana 是一个基于 Web 的数据可视化和探索工具,提供了与 Elasticsearch 数据交互的用户界面。Kibana 提供多种可视化类型,如柱状图、折线图、饼图和地图,并支持创建自定义仪表板来展示和分析数据。Kibana 还包括 Dev Tools 用于 Elasticsearch 查询测试、监控和警报功能,并支持机器学习集成。

请注意,以下描述适用于 Linux 系统。如果你恰好是那些在开发机器上原生运行 Linux 的幸运人之一,那就直接开始第 1 步 – 在 Linux 上设置 ELK Stack吧。

如果你使用的是 Mac 或 Windows 机器进行工作,我们已经创建了详细的步骤说明,教你如何测试设置。特别需要注意的是第 2 步 – 安装和配置 Filebeat。请查看与你的设置相匹配的部分并尝试一下。

第 1 步 – 在 Linux 上设置 ELK Stack

使用 Docker 容器部署 ELK,或将其直接安装在你的系统上。详细的安装说明,请参考官方的 ELK Stack 文档:www.elastic.co/guide/index.xhtml

确保 Elasticsearch 和 Kibana 配置正确并正在运行。通过使用 web 浏览器访问 Kibana 仪表板来验证这一点。

步骤 2 – 安装并配置 Filebeat

Filebeat 是一个轻量级的日志传输工具,可以将日志从 Docker 容器转发到 ELK Stack。你可以在 Docker 主机上安装 Filebeat,并配置它以收集容器日志:

  1. 使用官方安装指南安装 Filebeat,针对你的操作系统进行安装。你可以在这里找到相关文档:www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation-configuration.xhtml

  2. 通过编辑 filebeat.yml 配置文件来配置 Filebeat(通常位于 Linux 系统的 /etc/filebeat 中)。添加以下配置以收集 Docker 容器日志:

    filebeat.inputs:- type: container  paths:    - '/var/lib/docker/containers/*/*.log'
    
  3. 配置输出将日志转发到 Elasticsearch。将 <elasticsearch_host><elasticsearch_port> 替换为适当的值:

    output.elasticsearch:  hosts: ["<elasticsearch_host>:<elasticsearch_port>"]
    
  4. 保存配置文件并启动 Filebeat:

    $  sudo systemctl enable filebeat$  sudo systemctl start filebeat
    

请注意,这种配置仅适用于 Linux 系统。在 Mac 或 Windows 上,由于 Docker 在两个系统上都运行在虚拟机中,因此访问这个虚拟机中的 Docker 日志稍微复杂一些。如果你希望在 Mac 或 Windows 机器上本地安装 Filebeat,请查阅相关文档,因为这超出了本书的范围。

或者,我们可以将 Filebeat 运行在容器中,与 ELK Stack 并行使用。

这是一个完整的 Docker Compose 文件,将在 Linux 计算机上运行 ELK Stack 和 Filebeat:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.2 – ELK Stack 和 Filebeat 的 Docker Compose 文件

现在我们已经学习了如何在 Linux 计算机或服务器上运行 Filebeat,接下来我们想展示如何在 Mac 或 Windows 计算机上使用 Filebeat,这在开发过程中非常重要。

在 Mac 或 Windows 计算机上运行示例

上面的示例无法在 Mac 或 Windows 计算机上运行,因为 Docker 是透明地运行在虚拟机中,因此 Docker 日志文件将无法在 /var/lib/docker/containers 中找到。

我们可以通过一种变通方法来解决这个问题:我们可以配置所有容器将各自的日志写入一个属于 Docker 卷的文件中。然后,我们可以将这个卷挂载到 Filebeat 容器中,而不是在前面的 Docker Compose 文件的第 44 行做的那样。

这是一个示例,使用一个简单的 Node.js/Express.js 应用程序来演示这个过程。请按照以下步骤操作:

  1. ch12 章节文件夹中创建一个名为 mac-or-windows 的文件夹。

  2. 在这个文件夹内,创建一个名为 app 的子文件夹,并进入该文件夹。

  3. app 文件夹内,使用以下命令初始化 Node.js 应用程序:

    $ npm init
    

接受所有默认设置。

  1. 使用以下命令安装 Express.js:

    $ npm install --save express
    
  2. 修改 package.json 文件,并添加一个名为 start 的脚本,值设置为 node index.js

  3. 向文件夹中添加一个名为 index.js 的文件,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.3 – index.js 应用文件

这个简单的 Express.js 应用程序有两个路由,//test。它还包含中间件,用于记录传入的请求,并在处理特定路由或出现 404 Not Found 错误时记录日志。

  1. 向文件夹中添加一个名为 entrypoint.sh 的脚本文件,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.4 – 示例应用的 entrypoint.sh 文件

该脚本将用于运行我们的示例应用程序,并将其日志重定向到指定的 LOGGING_FILE

使用以下命令将前面的文件设为可执行文件:

$ chmod +x ./entrypoint.sh
  1. 向文件夹中添加一个 Dockerfile,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.5 – 示例应用的 Dockerfile

  1. mac-or-windows 文件夹中添加一个名为 docker-compose.yml 的文件,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.6 – Mac 或 Windows 使用场景的 Docker Compose 文件

请注意第 9 行中的环境变量,它定义了由 Node.js/Express.js 应用生成的日志文件的名称和位置。还请注意第 11 行中的卷映射,这将确保日志文件被导入到 Docker 的 app_logs 卷中。然后,这个卷会挂载到第 25 行的 filebeat 容器中。通过这种方式,我们确保 Filebeat 能够收集日志并将其转发到 Kibana。

  1. 此外,向 mac-or-windows 文件夹中添加一个名为 filebeat.yml 的文件,包含以下 Filebeat 配置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.7 – Mac 或 Windows 上的 Filebeat 配置

  1. docker-compose.yml 文件所在的文件夹内,使用以下命令构建 Node.js 应用镜像:

    $ docker compose build app
    
  2. 现在,你已经准备好运行整个栈了,像这样:

    $ docker compose up --detach
    
  3. 使用 REST 客户端访问 http://localhost:3000http://localhost:3000/test 端点几次,以使应用生成一些日志输出。

现在,我们准备好在 Kibana 中集中查看收集到的日志了。

第 3 步 – 在 Kibana 中可视化日志

通过 Web 浏览器访问 Kibana 仪表板,网址为 http://localhost:5601

如需更多详细信息,请参阅本章后面关于 查询集中日志 部分的内容。这里是一个简要概述。

进入 filebeat-*)开始分析收集到的日志。

进入 Discover 部分,搜索、筛选并可视化来自 Docker 容器的日志。

配置好 Kibana 仪表板后,你应该会看到如下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.8 – 由 Filebeat 提供的 Kibana 中的应用日志

按照这些步骤,你将拥有一个集中式日志管理系统,能够汇总来自多个 Docker 容器的日志,帮助你高效地分析和监控容器化应用程序。需要注意的是,还有其他日志管理系统和日志传输工具,如 Splunk、Graylog 和 Fluentd,设置这些系统的过程类似,但可能需要不同的配置步骤。

设置日志过滤和警报机制

设置日志过滤和警报机制有助于你集中精力处理重要的日志信息,减少噪音,并主动响应潜在问题。在这里,我们将使用 ELK Stack 配合 ElastAlert 插件来演示日志过滤和警报。

第 1 步 – 设置 Elastic Stack

首先,按照 设置 ELK Stack 部分提供的说明,设置 Elastic Stack 进行集中式日志记录。这包括在 Docker 容器中运行 Elasticsearch、Logstash 和 Kibana。

第 2 步 – 使用 Logstash 设置日志过滤

配置 Logstash 根据特定条件(如日志级别、关键词或模式)过滤日志。更新你的 logstash.conf 文件,在 filter 部分添加适当的过滤器。例如,要根据日志级别过滤日志,你可以使用以下配置:

filter {  if [loglevel] == "ERROR" {
    mutate {
      add_tag => ["error"]
    }
  }
}

此配置检查日志级别是否为 ERROR,并将 error 标签添加到日志事件中。重启 Logstash 容器以应用新配置:

docker restart logstash
第 3 步 – 配置 ElastAlert 以进行警报

ElastAlert 是一个简单的框架,用于警报在 Elasticsearch 存储的数据中发现的异常、峰值或其他感兴趣的模式。让我们来设置它:

  1. 克隆 ElastAlert 仓库并导航到 ElastAlert 目录:

    git clone https://github.com/Yelp/elastalert.gitcd elastalert
    
  2. 安装 ElastAlert:

    pip install elastalert
    
  3. 为 ElastAlert 创建一个配置文件 config.yaml,并使用以下内容更新它:

    es_host: host.docker.internales_port: 9200rules_folder: rulesrun_every:  minutes: 1buffer_time:  minutes: 15alert_time_limit:  days: 2
    
  4. 创建一个 rules 目录,并定义你的警报规则。例如,要为带有 error 标签的日志创建警报,可以在 rules 目录中创建一个名为 error_logs.yaml 的文件,内容如下:

    name: Error Logsindex: logstash-*type: frequencynum_events: 1timeframe:  minutes: 1filter:- term:    tags: "error"alert:- "email"email:- "you@example.com"
    

这个规则会在 1 分钟内,如果至少有一个带有 error 标签的日志事件,触发邮件警报。

  1. 启动 ElastAlert:

    elastalert --config config.yaml --verbose
    

现在,ElastAlert 将根据你定义的规则监控 Elasticsearch 数据,并在满足条件时发送警报。

第 4 步 – 监控和响应警报

配置好日志过滤和警报机制后,你可以集中精力处理关键日志信息,并主动响应潜在问题。监控你的电子邮件或其他配置的通知渠道,接收警报并调查根本原因,以提高应用程序的可靠性和性能。

不断完善你的 Logstash 过滤器和 ElastAlert 规则,以减少噪音,检测重要的日志模式,并更有效地响应潜在问题。

在下一节中,我们将讨论如何传输 Docker 守护进程日志。

传输 Docker 守护进程日志

Docker 守护进程日志涉及 Docker 平台的整体功能。Docker 守护进程负责管理所有 Docker 容器,其日志记录了系统范围的事件和消息。这些日志有助于识别与 Docker 守护进程本身相关的问题,如网络问题、资源分配错误和容器编排挑战。

根据操作系统的不同,Docker 守护进程日志的位置和配置可能有所不同。例如,在 Linux 系统上,守护进程日志通常位于/var/log/docker.log,而在 Windows 系统上,它们位于%programdata%\docker\logs\daemon.log

注意

Mac 上的守护进程日志将在下一节中介绍。

要有效管理 Docker 守护进程日志,可以考虑以下最佳实践:

  • 定期查看守护进程日志,以识别潜在问题和异常

  • 设置日志轮换和保留策略以管理磁盘空间使用

  • 使用日志管理系统集中管理并分析日志,以更好地查看整体 Docker 环境。

总之,运输容器和 Docker 守护进程日志在监控和维护健康的 Docker 环境中起着至关重要的作用。通过有效地管理这些日志,系统管理员和开发人员可以确保最佳性能,最小化停机时间,并及时解决问题。

Mac 上的 Docker 守护进程日志

在安装了 Docker Desktop 的 Mac 上,你可以使用 macOS 日志工具提供的log stream命令查看 Docker 守护进程日志。按照以下步骤操作:

  1. 打开终端应用程序。

  2. 运行以下命令:

    log stream --predicate 'senderImagePath CONTAINS "Docker"'
    

该命令将显示与 Docker Desktop 相关的日志的实时流,包括 Docker 守护进程日志。你可以通过按Ctrl + C来停止日志流。

  1. 或者,你可以使用以下命令以文件格式查看 Docker 守护进程日志:

    log show --predicate 'senderImagePath CONTAINS "Docker"' \    --style syslog --info \    --last 1d > docker_daemon_logs.log
    

该命令将在当前目录创建一个名为docker_daemon_logs.log的文件,文件包含过去 1 天的 Docker 守护进程日志。你可以更改--last 1d选项来指定不同的时间范围(例如,--last 2h表示过去 2 小时)。使用任何文本编辑器打开docker_daemon_logs.log文件以查看日志。

请注意,执行这些命令可能需要管理员权限。如果遇到权限问题,请在命令前加上sudo

Windows 计算机上的 Docker 守护进程日志

在安装了 Docker Desktop 的 Windows 11 机器上,Docker 守护进程日志以文本文件的形式存储。你可以通过以下步骤访问这些日志:

  1. 打开文件资源管理器。

  2. 导航到以下目录:

    C:\ProgramData\DockerDesktop\service
    

在该目录中,你将找到包含 Docker 守护进程日志的DockerDesktopVM.log文件。

  1. 使用任何文本编辑器打开DockerDesktopVM.log文件以查看日志。

请注意,C:\ProgramData文件夹可能默认是隐藏的。要在文件资源管理器中显示隐藏的文件夹,请点击查看选项卡并勾选隐藏的项目复选框。

另外,您可以使用 PowerShell 阅读日志:

  1. 打开 PowerShell。

  2. 执行以下命令:

    Get-Content -Path "C:\ProgramData\DockerDesktop\service\DockerDesktopVM.log" -Tail 50
    

此命令将显示 Docker 守护进程日志文件的最后 50 行。您可以更改-Tail后的数字来显示不同数量的行。

接下来,我们将学习如何查询集中式日志。

查询集中式日志

一旦您的容器化应用程序日志被收集并存储在 ELK Stack 中,您就可以使用 Elasticsearch 的查询领域特定语言DSL)查询集中式日志,并在 Kibana 中可视化结果。

第 1 步 - 访问 Kibana

Kibana 提供了一个用户友好的界面来查询和可视化 Elasticsearch 数据。在提供的 docker-compose.yml 文件中,Kibana 可以通过端口 5601 进行访问。打开您的浏览器并导航到 http://localhost:5601

第 2 步 - 设置索引模式

在查询日志之前,您需要在 Kibana 中创建一个索引模式,以识别包含日志数据的 Elasticsearch 索引。按照以下步骤创建索引模式:

  1. 第一次访问 Kibana 时,系统会要求您添加集成。由于我们使用 Filebeat 来发送日志,因此可以安全地忽略此请求。

  2. 相反,请在视图的左上角找到“汉堡菜单”,并点击它。

  3. 在左侧导航菜单中找到管理选项卡并选择堆栈管理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.9 - Kibana 中的管理选项卡

  1. Kibana部分,点击索引模式

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.10 - Kibana 的索引模式条目

  1. 点击创建索引 模式按钮。

  2. 输入与您的 Logstash 索引匹配的索引模式。例如,如果您的 Logstash 配置使用logstash-%{+YYYY.MM.dd}索引模式,请在名称字段中输入logstash-*

  3. @``timestamp字段中。

  4. 点击创建 索引模式

现在,我们已经准备好查询我们的容器日志。

第 3 步 - 在 Kibana 中查询日志

现在,您已经准备好使用 Kibana 的发现功能来查询日志。按照以下步骤操作:

  1. 再次在视图的左上角找到“汉堡菜单”,并点击它。

  2. 找到分析选项卡并选择发现

  3. 从左上角的下拉菜单中选择您之前创建的索引模式。

  4. 使用右上角的时间过滤器选择一个特定的时间范围进行查询。

  5. 要搜索特定的日志条目,请在搜索框中输入查询并按Enter。Kibana 使用 Elasticsearch 查询 DSL 执行搜索。

以下是一些示例查询:

  • 要查找包含error一词的日志:error

  • 要查找具有特定字段值的日志:container.name: "my-container"

  • 要使用通配符搜索(例如,查找以“app”开头的container.name日志):container.name: "app*"

  • 要使用布尔运算符进行更复杂的查询:error AND container.name: "my-container"

第 4 步 – 可视化日志

您可以在 Kibana 中创建可视化和仪表盘,以更有效地分析日志。要创建可视化,请按以下步骤操作:

  1. 点击左侧导航菜单中的可视化选项卡。

  2. 点击创建 可视化按钮。

  3. 选择一个可视化类型(例如,饼图、条形图、折线图等)。

  4. 选择您之前创建的索引模式。

  5. 通过选择字段和聚合类型来配置可视化。

  6. 点击保存以保存您的可视化。

您可以创建多个可视化并将它们添加到仪表盘,以全面查看您的日志数据。要创建一个仪表盘,请执行以下操作:

  1. 点击左侧导航菜单中的仪表盘选项卡。

  2. 点击创建 仪表盘按钮。

  3. 点击添加以将可视化添加到仪表盘。

  4. 根据需要调整可视化的大小和重新排列位置。

  5. 点击保存以保存您的仪表盘。

现在,您可以集中查看容器化应用程序的日志,并可以使用 Kibana 查询、分析和可视化这些日志。

在接下来的章节中,我们将学习如何收集和抓取 Docker 和您的应用程序暴露的指标。

收集和抓取指标

要从运行在安装了 Docker Desktop 的系统上的容器中收集和抓取指标,您可以使用 Prometheus 和容器顾问cAdvisor)。Prometheus 是一个强大的开源监控和告警工具集,而 cAdvisor 为容器用户提供有关其运行容器的资源使用情况和性能特征的理解。

在本节中,我们将提供逐步指南,帮助您设置 Prometheus 和 cAdvisor,从容器中收集和抓取指标。

第 1 步 – 在 Docker 容器中运行 cAdvisor

cAdvisor 是一个由 Google 开发的工具,用于收集、处理和导出容器指标。让我们来看看:

  1. 在章节文件夹ch12中,创建一个名为metrics的新子文件夹:

    mkdir metrics
    
  2. 在此文件夹中,创建一个名为docker-compose.yml的文件,并将以下代码片段添加到其中:

    version: '3.8'services:  cadvisor:    image: gcr.io/cadvisor/cadvisor:v0.45.0    container_name: cadvisor    restart: always    ports:    - 8080:8080    volumes:    - /:/rootfs:ro    - /var/run:/var/run:rw    - /sys:/sys:ro    - /var/lib/docker/:/var/lib/docker:ro
    
  3. 使用以下命令在 Docker 容器中运行 cAdvisor:

    docker compose up cadvisor --detach
    

v0.45.0替换为 cAdvisor 仓库中最新的版本。

该命令挂载主机系统所需的目录,并在端口8080上暴露 cAdvisor 的 Web 界面。

注意

版本低于此处显示的版本将无法运行,例如,在配备 M1 或 M2 处理器的 Mac 上。

  1. 您可以通过在浏览器中导航到http://localhost:8080访问 cAdvisor 的 Web 界面。

第 2 步 – 设置并运行 Prometheus

接下来,让我们按照以下逐步说明设置 Prometheus:

  1. metrics文件夹中创建一个名为prometheus的子文件夹。

  2. 在这个新文件夹中,创建一个名为prometheus.yml的配置文件,内容如下:

    global:  scrape_interval: 15sscrape_configs:  - job_name: 'prometheus'    static_configs:      - targets: ['localhost:9090']  - job_name: 'cadvisor'    static_configs:      - targets: ['host.docker.internal:8080']
    

此配置指定了全局抓取间隔和两个抓取作业:一个用于 Prometheus 本身,另一个用于运行在端口8080上的 cAdvisor。

  1. docker-compose.yml文件的末尾添加以下片段:

    prometheus:  image: prom/prometheus:latest  container_name: prometheus  restart: always  ports:    - 9090:9090  volumes:    - ./prometheus:/etc/prometheus    - prometheus_data:/prometheus
    

此指令挂载了prometheus.yml配置文件,并在端口9090上公开了 Prometheus。

  1. 前述的prometheus服务使用了名为prometheus_data的卷。要定义这一点,请将以下两行添加到docker-compose.yml文件的末尾:

    volumes:  prometheus_data:
    
  2. 您可以通过浏览器访问http://localhost:9090来访问 Prometheus Web 界面。

一旦 Prometheus 启动并运行,您可以验证它是否成功从 cAdvisor 获取指标:

  1. http://localhost:9090打开 Prometheus Web 界面。

  2. 在顶部导航栏中点击状态,然后选择目标

  3. 确保prometheuscadvisor目标都列为UP

现在,Prometheus 可以收集和存储运行在您的 Docker Desktop 系统上的容器的指标。您可以使用 Prometheus 内置的表达式浏览器查询指标或设置 Grafana 进行高级可视化和仪表板:

  1. query text字段中输入类似container_start_time_seconds的内容,以获取所有容器的启动时间值。

  2. 要细化查询并仅获取 cAdvisor 容器的值,请输入container_start_time_seconds{job="cadvisor"}

请注意,在query text字段中,您可以获得智能感知(IntelliSense),当您不记得命令及其参数的所有细节时,这非常方便。

在继续之前,请使用以下命令停止 cAdvisor 和 Prometheus:

docker compose down -v

在本章的最后一节,您将学习如何使用 Grafana 等工具监控容器化应用程序。

监控容器化应用程序

监控容器化应用程序对理解应用程序的性能、资源使用情况和潜在瓶颈至关重要。本节将详细介绍使用 Prometheus、Grafana 和 cAdvisor 监控容器化应用程序的逐步过程。

第一步 – 设置 Prometheus

按照上一节的说明设置 Prometheus 和 cAdvisor,以从运行在 Docker Desktop 上的容器中收集和抓取指标。

第二步 – 使用 Prometheus 指标为您的应用程序进行仪表化

要监控容器化应用程序,您需要使用 Prometheus 指标为应用程序进行仪表化。这涉及向应用程序代码添加 Prometheus 客户端库,并在 HTTP 端点(通常为/metrics)上公开指标。

从官方列表prometheus.io/docs/instrumenting/clientlibs/中选择适合您应用程序编程语言的适当 Prometheus 客户端库。

在遵循库的文档和示例时,将库添加到您的应用程序中。

暴露/metrics端点,这将由 Prometheus 进行抓取。

使用 Kotlin 和 Spring Boot 的示例

要从 Kotlin 和 Spring Boot API 暴露 Prometheus 指标,您需要遵循以下步骤:

  1. 创建一个新的 Kotlin Spring Boot 项目。

  2. 添加必要的依赖项。

  3. 实现 API 并暴露 Prometheus 指标。

  4. 暴露 actuator 端点。

  5. 创建一个 Dockerfile。

  6. 与 Docker Compose 文件集成。

步骤 1 – 创建一个新的 Kotlin Spring Boot 项目

你可以使用 Spring Initializr(start.spring.io/)来创建一个新的 Kotlin Spring Boot 项目。将构件命名为kotlin-api,然后选择 Kotlin 作为语言,选择打包类型(JAR 或 WAR),并添加必要的依赖项。对于此示例,在依赖项部分选择WebActuatorPrometheus

下载生成的项目并解压缩。

步骤 2 – 验证必要的依赖项

在你的build.gradle.kts文件中,确保包含以下依赖项:

implementation("org.springframework.boot:spring-boot-starter-web")implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
步骤 3 – 实现 API 并暴露 Prometheus 指标

定位到src/main/kotlin/com/example/kotlinapi/子文件夹中的 Kotlin KotlinApiApplication.kt文件,并将其现有内容替换为以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.11 – KotlinApiApplication.kt 文件中的代码

如果你不想自己输入示例代码,也可以在sample-solutions/ch12/kotlin-api子文件夹中找到这段代码。

在此示例中,实现了一个简单的 REST API,只有一个端点/。该端点递增计数器并将计数作为 Prometheus 指标api_requests_total暴露。

将以下行添加到application.properties文件,以使用不同于默认端口8080的端口,8080端口已被我们堆栈中的 cAdvisor 占用。在我们的示例中,端口为7000

server.port=7000
步骤 4 – 暴露指标

将以下行添加到application.properties文件:

management.endpoints.web.exposure.include=health,info,metrics,prometheus

注意

上述配置应全部放在一行上。由于空间限制,这里显示为两行。

这将暴露相应的指标,在/actuator/health/actuator/info/actuator/metrics/actuator/prometheus端点上。

步骤 5 – 创建 Dockerfile

在项目根目录中创建一个multistage Dockerfile,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12.12 – Kotlin API 的 Dockerfile

在这个multistage Dockerfile 中,我们有两个阶段:

  • 使用gradle:jdk17基础镜像来构建 Kotlin Spring Boot 应用程序。它设置工作目录,复制源代码,并运行 Gradle build命令。此阶段使用AS关键字命名为build

  • openjdk:17-oracle基础镜像用于运行时环境,它是一个没有 JDK 的较小镜像。它从构建阶段复制构建的 JAR 文件,并将入口点设置为运行 Spring Boot 应用程序。

这个多阶段的 Dockerfile 允许你一次性构建 Kotlin Spring Boot 应用程序并创建最终的运行时镜像。它还通过排除不必要的构建工具和工件,帮助减少最终镜像的大小。

第六步 – 与 Docker Compose 文件集成

更新你现有的docker-compose.yml文件,以便它包含 Kotlin Spring Boot API 服务,该服务位于kotlin-api子文件夹中:

version: '3.8'services:
  # ... other services (Elasticsearch, Logstash, Kibana, etc.) ...
  kotlin-spring-boot-api:
    build: ./kotlin-api
    container_name: kotlin-spring-boot-api
    ports:
      - 7000:7000

现在,你可以运行docker compose up -d来构建并启动 Kotlin Spring Boot API 服务以及其他服务。API 将通过8080端口访问,Prometheus 的度量标准可以被收集。

接下来,我们将配置 Prometheus 以抓取我们设置中的所有度量数据,包括我们刚创建的 Kotlin API。

第三步 – 配置 Prometheus 抓取你的应用程序指标

更新你在前一部分中提到的prometheus.yml配置文件,以便它包括一个新的抓取任务,针对你的应用程序。例如,由于我们的 Kotlin API 示例应用程序在 Docker 容器中运行并在7000端口暴露度量标准,我们将以下内容添加到scrape_configs部分:

- job_name: 'kotlin-api'  static_configs:
    - targets: ['host.docker.internal:7000']
  metrics_path: /actuator/prometheus

第四步 – 设置 Grafana 进行可视化

Grafana 是一个流行的开源可视化和分析工具,可以与 Prometheus 集成,创建适用于你的容器化应用程序的交互式仪表板:

  1. 在前一部分的docker-compose.yml中,添加以下代码段以定义 Grafana 服务:

    grafana:  image: grafana/grafana:latest  container_name: grafana  restart: always  ports:    - 3000:3000  volumes:    - grafana_data:/var/lib/grafana
    
  2. volumes:部分,添加一个名为grafana_data的卷。

  3. 使用以下命令运行 cAdvisor、Prometheus 和 Grafana:

    docker compose up --detach
    
  4. 通过在浏览器中导航到http://localhost:3000,你可以访问 Grafana。默认的用户名是admin,默认的密码也是admin

  5. 添加Prometheus作为数据源。

  6. 点击左侧边栏中的齿轮图标(Configuration)。

  7. 选择Data Sources,然后点击Add data source

  8. 选择http://host.docker.internal:9090作为 URL。

  9. 点击Save & Test以验证连接。

  10. 创建仪表板和面板以可视化你的应用程序指标。

  11. 点击左侧边栏中的**+图标(Create),然后选择Dashboard**。

  12. 点击Add new panel开始为你的度量数据创建面板。

  13. 使用查询编辑器基于你的应用程序指标构建查询,并自定义可视化类型、外观和其他设置。

  14. 点击右上角的磁盘图标保存仪表板。

使用 Grafana,你可以创建交互式仪表板,提供容器化应用程序的实时性能、资源使用情况和其他关键指标的洞察。

第五步 – 设置告警(可选)

Grafana 和 Prometheus 可以根据你的应用程序指标设置告警。这可以帮助你在问题影响用户之前主动处理问题:

  1. 在 Grafana 中,创建一个新面板或编辑现有面板。

  2. 在面板编辑器中切换到Alert标签页。

  3. 点击创建警报并配置警报规则、条件和通知设置。

  4. 保存面板和仪表板。

您可能还需要配置 Grafana 的通知渠道,通过电子邮件、Slack、PagerDuty 或其他支持的服务发送警报。要做到这一点,请按照以下步骤操作:

  1. 在 Grafana 中,点击左侧边栏的铃铛图标(警报)。

  2. 选择通知渠道并点击添加渠道

  3. 填入您偏好的通知服务所需的信息,然后点击保存

现在,当您的面板中指定的警报条件满足时,Grafana 将通过配置的渠道发送通知。

第 6 步 – 监控您的容器化应用程序

配置了 Prometheus、Grafana 和 cAdvisor 后,您现在可以有效地监控您的容器化应用程序。请密切关注您的 Grafana 仪表板,设置适当的警报规则,并利用收集的数据识别性能瓶颈,优化资源使用,并改善应用程序的整体健康状况。

记得通过不断完善您的监控设置来持续迭代和改进,精炼应用程序的仪表化,调整警报规则,并随着应用程序的发展和增长,向仪表板中添加新的可视化内容。

总结

在本章中,我们了解了为什么记录日志并将其发送到中央位置是很重要的。接着我们展示了如何在本地计算机上设置 ELK Stack,它可以作为日志的集线器。我们生成了这个堆栈的一个特殊版本,其中包括 Filebeat,它可以通过重定向标准日志输出到一个文件并将其父文件夹映射到 Docker 卷,进而在 Mac 或 Windows 计算机上运行。在生产或类生产系统中,应用程序运行在 Linux 服务器或虚拟机上,因此 Filebeat 可以直接从 Docker 将日志收集到默认位置 /var/lib/docker/containers

我们还学习了如何使用 Prometheus 和 Grafana 来抓取、收集并集中显示您应用程序的指标,并在仪表板上展示这些数据。我们使用了一个简单的 Kotlin 应用程序,暴露了一个计数器来演示这一过程。

最后,我们简要提到了如何根据收集的指标值定义警报。

在下一章中,我们将介绍容器编排器的概念。它将教我们为什么需要编排器,以及编排器的工作原理。该章节还将概述最流行的编排器,并列出它们的一些优缺点。

问题

这里有几个问题,您应该尝试回答它们以自我评估您的学习进度:

  1. Docker 容器日志是什么?它们为什么重要?

  2. Docker 中的守护进程日志是什么?它与容器日志有何不同?

  3. 如何监控 Docker 容器?

  4. 如何查看正在运行的 Docker 容器的日志?

  5. 对于记录和监控 Docker 容器,有哪些最佳实践?

  6. 如何从多个 Docker 容器收集日志?

答案

这里是本章问题的一些示例答案:

  1. Docker 容器日志是由容器内运行的应用程序生成的事件和消息记录。它们对于监控性能、故障排除问题以及确保 Docker 容器中部署的应用程序平稳运行至关重要。

  2. Docker 中的守护程序日志指的是由管理 Docker 容器的 Docker 守护程序生成的日志文件。这些日志记录了与 Docker 平台整体功能相关的系统范围事件和消息。相比之下,容器日志是针对单个容器及其应用程序的特定日志。

  3. 可以通过多种方法监控 Docker 容器,包括命令行工具如 docker stats、第三方监控解决方案如 Prometheus,以及 Docker 的内置 API。这些工具帮助跟踪资源使用情况、性能指标和容器的健康状态。

  4. 您可以使用 docker logs 命令查看运行中 Docker 容器的日志,后面跟上容器的 ID 或名称。该命令检索容器生成的日志消息,有助于诊断问题或监视容器的活动。

  5. 记录和监控 Docker 容器的一些最佳实践包括以下几点:

    • 使用日志管理系统集中日志

    • 配置日志轮转和保留策略

    • 设置日志过滤和警报机制

    • 使用内置和第三方工具组合监控容器

    • 定期检查异常的日志和指标

  6. 要从多个 Docker 容器收集日志,您可以使用日志管理系统,如 ELK Stack 或 Splunk。您还可以使用 Fluentd 或 Logspout 等工具,将所有容器的日志聚合并转发到集中的日志管理系统,进行分析和可视化。

第十三章:13

介绍容器编排

在上一章中,我们展示了如何收集容器日志并将其发送到集中位置,在那里聚合的日志可以解析出有用信息。我们还学习了如何对应用程序进行监控,使其暴露出指标,并且这些指标可以被抓取并再次发送到集中位置。最后,本章教我们如何将这些收集到的指标转换为图形化的仪表盘,来监控容器化的应用程序。

本章介绍了编排器的概念。它教我们为什么需要编排器,以及它们是如何在概念上工作的。本章还将概述一些最流行的编排器,并列出它们的优缺点。

本章将涵盖以下内容:

  • 什么是编排器,我们为什么需要它们?

  • 编排器的任务

  • 流行的编排器概述

完成本章后,你将能够做以下事情:

  • 列举三到四个编排器负责的任务

  • 列出两到三个最流行的编排器

  • 用你自己的话向一个感兴趣的外行解释,并通过适当的类比说明为什么我们需要容器编排器

什么是编排器,我们为什么需要它们?

第九章《学习分布式应用架构》中,我们了解了常用的构建、运输和运行高度分布式应用的模式和最佳实践。现在,如果我们的分布式应用是容器化的,那么我们将面临与非容器化分布式应用相同的问题或挑战。这些挑战中有一些是在那一章中讨论过的,诸如服务发现、负载均衡、扩展等。

类似于 Docker 对容器所做的事情——通过引入容器标准化软件的打包和运输——我们希望有某种工具或基础设施软件来处理所有或大多数已经提到的挑战。这款软件就是我们所说的容器编排器,或者我们也称它们为编排引擎。

如果我刚才说的内容对你来说还没有太大意义,那么让我们换个角度来看。想象一下一个演奏乐器的艺术家。他们可以单独为观众演奏美妙的音乐——只是艺术家和他们的乐器。但现在,假设有一支由多位音乐家组成的管弦乐队。把他们都放在一个房间里,给他们交代交响乐的乐谱,让他们演奏,并且离开房间。如果没有指挥,这群非常有才华的音乐家将无法和谐地演奏这首曲子;它听起来更像是一种杂音。只有当管弦乐队有一个指挥,来编排这些音乐家,乐队的音乐才能让我们的耳朵享受。

现在,我们不再是音乐家,而是容器;不再是各种乐器,而是具有不同运行需求的容器主机。与音乐在不同速度下演奏不同,我们的容器也以特定的方式相互通信,并且需要根据对应用程序的负载变化进行扩展或缩减。就此而言,容器调度器的角色与乐团指挥非常相似。它确保集群中的容器和其他资源协调一致地工作。

我希望你现在能更清楚地理解容器调度器是什么,以及为什么我们需要它。假设你已经明白了这个问题,我们现在可以问自己,调度器如何实现预期的结果,即确保集群中的所有容器和谐工作。答案是,调度器必须执行非常具体的任务,类似于乐团指挥也有一套任务,用来驾驭并同时提升乐团的表现。

调度器的任务

那么,我们期望一个值得投资的调度器为我们执行哪些任务呢?让我们详细看看。以下列表展示了在撰写时,企业用户通常期望从调度器获得的最重要任务。

调整期望状态

使用调度器时,你会告诉它(最好是以声明性方式)如何运行给定的应用程序或应用服务。我们在第十一章《使用 Docker Compose 管理容器》中学习了什么是声明性和命令式的区别。描述我们要运行的应用服务的声明性方式包括元素,例如使用哪个容器镜像、运行多少实例、打开哪些端口等。我们称之为应用服务的声明属性,这就是所谓的期望状态。

所以,当我们首次告诉调度器基于声明创建一个新的应用服务时,调度器将确保根据请求在集群中调度足够多的容器。如果容器镜像在目标节点上尚不可用,调度器将确保从镜像仓库下载它们。接下来,容器将按照所有设置(如网络连接或暴露的端口)启动。调度器会尽力使集群的实际情况与声明完全匹配。

一旦我们的服务按要求启动并运行,也就是说,它已经在期望状态下运行,那么调度器将继续监控它。每当调度器发现服务的实际状态与期望状态不一致时,它将尽力再次调整,使实际状态与期望状态保持一致。

那么,实际状态和期望状态之间可能出现什么样的差异呢?假设服务的一个副本,也就是其中一个容器,由于 bug 等原因崩溃了;那么编排器会发现实际状态与期望状态在副本数量上有所不同:少了一个副本。编排器会立即在另一个集群节点上调度一个新的实例来替代崩溃的实例。另一种差异可能是,如果服务被缩减了规模,应用服务可能会运行过多实例。在这种情况下,编排器会随机杀死需要的实例,以实现实际实例数量和期望数量之间的平衡。还有一种差异是,当编排器发现应用服务的某个实例正在运行一个错误的(可能是旧的)底层容器镜像版本时。到现在为止,你应该能理解了,对吧?

因此,取而代之的是我们主动监控集群中运行的应用服务并纠正任何与期望状态的偏差,我们将这一繁琐的任务委托给编排器。这在我们使用声明性方式而非命令式方式描述应用服务的期望状态时效果最好。

复制和全局服务

在由编排器管理的集群中,我们可能会想要运行两种截然不同类型的服务。它们分别是复制服务和全局服务。复制服务是指要求在特定数量的实例上运行的服务,比如 10 个实例。全局服务则是指要求在集群中的每个工作节点上恰好运行一个实例的服务。我在这里使用了“工作节点”这个术语。在由编排器管理的集群中,通常有两种类型的节点:管理节点和工作节点。

管理节点通常是由编排器专门用于管理集群的,不运行任何其他工作负载。而工作节点则运行实际的应用程序。因此,编排器会确保对于全局服务,无论工作节点有多少,每个节点上都将运行一个实例。我们不关心实例的数量,而只关心每个节点上都必须保证运行一个实例。

我们可以再次完全依赖编排器来处理这个问题。在一个复制服务中,我们始终可以确保找到精确所需数量的实例,而在全局服务中,我们可以确保每个工作节点上都会运行恰好一个实例。编排器将始终尽最大努力来保证这个期望的状态。在 Kubernetes 中,全局服务也被称为DaemonSet

服务发现

当我们以声明性方式描述一个应用服务时,我们绝不应该告诉协调器不同实例应运行在哪些集群节点上。我们将让协调器决定哪个节点最适合执行这个任务。

当然,从技术上讲,可以指示协调器使用非常确定的放置规则,但这将是一种反模式,除非在非常特殊的边缘情况下,否则根本不推荐使用。

所以,如果我们现在假设协调引擎完全自由地决定应用服务的各个实例放置的位置,而且实例可能会崩溃并由协调器重新调度到不同的节点,那么我们就会意识到,试图追踪各个实例在任何给定时刻的运行位置是徒劳的。更好的是,我们根本不应该尝试去了解这一点,因为这并不重要。

好吧,你可能会说,如果我有两个服务,A 和 B,而服务 A 依赖于服务 B;那么服务 A 的任何实例不应该知道它可以在哪里找到服务 B 的实例吗?

在这里,我必须大声而清楚地说——不,应该不了。这种知识在高度分布式和可扩展的应用程序中并不理想。相反,我们应该依赖于协调器来提供我们所需的信息,以便访问我们依赖的其他服务实例。这有点像电话时代早期,我们不能直接拨打朋友的电话,而是必须拨打电话公司的总机,由接线员将我们转接到正确的目的地。在我们的案例中,协调器充当了接线员的角色,将来自服务 A 实例的请求路由到可用的服务 B 实例。整个过程被称为服务发现。

路由

到目前为止,我们已经了解,在一个分布式应用程序中,我们有许多交互的服务。当服务 A 与服务 B 进行交互时,是通过数据包的交换来实现的。这些数据包需要以某种方式从服务 A 传输到服务 B。将数据包从源头传输到目的地的过程也称为路由。作为应用程序的作者或操作员,我们期望调度器来承担这个路由任务。正如我们在后续章节中看到的,路由可以在不同的层级上进行。这就像现实生活中的情况。假设你在一家公司的一栋办公楼里工作。现在,你有一份文件需要转交给公司里的另一位员工。内部邮政服务会将文件从你的发件箱中取走,并送到同一建筑物内的邮局。如果目标人工作在同一栋楼,那么文件可以直接转交给该人。如果目标人则工作在同一区块的另一栋楼,文件将转交给目标楼的邮局,然后由内部邮政服务将其分发给收件人。第三种情况是,如果文件是送给在不同城市甚至不同国家的公司分支机构的员工,那么文件将被转交给外部邮政服务(如 UPS),它将文件运输到目标地点,从那里,再次由内部邮政服务接管并将其送达收件人。

在容器中运行的应用服务之间路由数据包时,也会发生类似的事情。源容器和目标容器可以位于同一个集群节点上,这相当于两名员工在同一栋建筑物中工作。

目标容器可能运行在不同的集群节点上,这相当于两名员工在同一区块的不同建筑物中工作。最后,第三种情况是,当数据包来自集群外部,必须路由到集群内部运行的目标容器。

所有这些情况,以及更多的情况,都必须由调度器来处理。

负载均衡

在高可用的分布式应用中,所有组件都必须是冗余的。这意味着每个应用服务必须以多个实例运行,这样即使一个实例失败,整个服务仍然能够正常运行。

为确保所有服务实例都在实际工作,而不是空闲着,你需要确保服务请求被均等地分配到所有实例。将工作负载分配到服务实例的过程称为负载均衡。存在多种算法来决定如何分配工作负载。

通常,负载均衡器使用所谓的轮询算法,确保工作负载在实例间均匀分配,采用循环算法进行分配。我们再次期望调度器处理来自一个服务到另一个服务或从外部源到内部服务的负载均衡请求。

扩展性

当我们在由调度器管理的集群中运行容器化的分布式应用时,我们也希望能够轻松处理预期的或意外的工作负载增加。为了应对增加的工作负载,我们通常只是调度该服务的额外实例来应对增加的负载。负载均衡器随后会自动配置,以便在更多的可用目标实例上分配工作负载。

但是在实际场景中,工作负载会随时间变化。如果我们看看像亚马逊这样的购物网站,它可能在晚上的高峰时段负载很高,因为那时每个人都在家在线购物;在像“黑色星期五”这样的特殊日子,负载可能会异常巨大;而在清晨,流量可能非常少。因此,服务不仅需要能够扩展,当工作负载减少时,也需要能够缩减。

我们还期望调度器在扩展时能够合理地分配服务实例。当扩展时,将所有服务实例调度到同一个集群节点上并不是明智之举,因为如果该节点宕机,整个服务都会宕机。负责容器部署的调度器需要考虑避免将所有实例都放置在同一个计算机机架上,因为如果机架的电源供应出现故障,整个服务也会受到影响。此外,关键服务的实例应分布在多个数据中心,以避免因故障而导致服务中断。所有这些决策,以及更多的决策,都由调度器负责。

在云中,通常使用“可用区”一词,而不是计算机机架。

自愈

如今,调度器已经非常复杂,可以为我们维护健康的系统做很多工作。调度器监控集群中所有运行的容器,并且会自动用新的实例替换崩溃或未响应的容器。调度器监控集群节点的健康状况,如果某个节点变得不健康或宕机,它会将该节点从调度循环中移除。原本在这些节点上的工作负载会自动重新调度到其他可用节点上。

所有这些活动,调度器监控当前状态并自动修复损坏或调节到期望状态,导致了所谓的自愈系统。

在大多数情况下,我们不需要主动干预和修复损坏。调度器会自动为我们完成这一任务。然而,有一些情况是调度器在没有我们帮助的情况下无法处理的。设想一个场景,我们有一个服务实例运行在容器中。容器已经启动并运行,从外部看起来完全健康,但内部运行的应用程序处于不健康状态。应用程序没有崩溃;它只是无法按原设计正常工作了。调度器怎么可能知道这一点呢?它根本无法知道!每个应用服务的健康或无效状态意味着完全不同的事情。换句话说,健康状态是依赖于服务的。只有服务的开发者或运维人员才知道在该服务的上下文中,什么才算健康。

现在,调度器定义了接缝或探针,应用服务可以通过这些与调度器通信,告知其所处的状态。探针主要有两种基本类型:

  • 服务可以告知调度器它是否健康

  • 服务可以告知调度器它是否准备就绪或暂时不可用

服务如何确定前述问题的答案完全取决于该服务。调度器只定义了它将如何询问,比如通过 HTTP GET 请求,或它期望什么类型的答案,比如 OKNOT OK

如果我们的服务实现了逻辑来回答前述的健康或可用性问题,那么我们就拥有了一个真正的自愈系统,因为调度器可以终止不健康的服务实例,并用新的健康实例替换它们,且可以将暂时不可用的服务实例从负载均衡器的轮询中移除。

数据持久化和存储管理

数据持久化和存储管理是容器编排中的关键环节。它们确保数据在容器重启和故障后得以保留,使得应用能够维持其状态,并按预期继续运行。

在容器化环境中,数据存储可以分为两大类——临时存储和持久存储:

  • 临时存储:这种存储与容器的生命周期相关。当容器终止或失败时,存储在临时存储中的数据将丢失。临时存储适用于临时数据、缓存或其他可以重新生成的非关键性信息。

  • 持久存储:持久存储将数据与容器的生命周期解耦,使得数据即使在容器终止或失败后仍能持久存在。这种存储类型对于保存关键应用数据至关重要,比如用户生成的内容、数据库文件或配置数据。

容器编排引擎通过提供将持久存储附加到容器的机制来处理数据持久性和存储管理。这些机制通常涉及存储卷的创建和管理,存储卷可以根据需要挂载到容器中。

大多数容器编排引擎支持多种类型的存储后端,包括块存储、文件存储和对象存储。它们还提供与流行存储解决方案的集成,如基于云的存储服务、网络附加存储和分布式存储系统(如 Ceph 或 GlusterFS)。

另外,容器编排引擎处理存储的提供和管理,自动化执行如卷创建、调整大小和删除等任务。它们还允许用户定义存储类和策略,使得在分布式环境中管理存储资源变得更加容易。

总之,容器编排引擎中的数据持久性和存储管理确保应用程序在容器重启和故障期间保持其状态。它们提供将持久存储附加到容器的机制,并自动化存储提供和管理任务,从而简化了在容器化环境中管理存储资源的过程。

零停机时间部署

如今,对于需要更新的关键任务应用程序,停机时间越来越难以 justify。不仅意味着错失机会,还可能导致公司声誉受损。使用该应用程序的客户已不再愿意接受不便,并会迅速流失。

此外,我们的发布周期越来越短。在过去,我们每年只有一到两个新版本发布,而如今,许多公司每周甚至每天都会更新应用程序多次。

解决这个问题的方法是提出一个零停机时间的应用更新策略。编排器需要能够批量更新单个应用服务。这也被称为滚动更新。在任何给定时间,只有某个服务的一个或少数几个实例被停用,并由该服务的新版本替换。只有在新实例正常运行,且没有出现任何意外错误或不良表现的情况下,才会更新下一批实例。这个过程会一直重复,直到所有实例都被替换为新版本。如果由于某种原因更新失败,我们期望编排器能自动将更新后的实例回滚到先前的版本。

其他可能的零停机时间部署方式包括蓝绿部署和金丝雀发布。在这两种情况下,服务的新版本将与当前的活跃版本并行安装。但最初,新版本仅在内部可访问。操作人员可以对新版本进行冒烟测试,当新版本似乎运行良好时,在蓝绿部署的情况下,路由器将从当前的蓝色版本切换到新的绿色版本。在一段时间内,新的绿色版本将受到密切监控,如果一切正常,旧的蓝色版本可以被停用。另一方面,如果新的绿色版本没有按预期工作,那么只需将路由器切换回旧的蓝色版本,就可以实现完全回滚。

在金丝雀发布的情况下,路由器的配置方式是将 1%的总体流量引导到服务的新版本,而 99%的流量仍然经过旧版本。新版本的行为会被密切监控,并与旧版本的行为进行比较。如果一切正常,经过新服务的流量百分比会稍微增加。这个过程会重复,直到 100%的流量都通过新服务。如果新服务运行了一段时间且一切正常,旧服务可以被停用。

大多数编排器至少支持开箱即用的滚动更新类型的零停机时间部署。蓝绿部署和金丝雀发布通常很容易实现。

亲和性和位置感知

有时,某些应用服务需要在其运行的节点上有专用硬件的支持。例如,I/O 密集型服务需要附加高性能固态硬盘SSD)的集群节点,而一些用于机器学习等服务需要加速处理单元APU)。

编排器允许我们为每个应用服务定义节点亲和性。然后,编排器将确保其调度器仅在满足所需条件的集群节点上调度容器。

应避免在特定节点上定义亲和性;这样做会引入单点故障,从而影响高可用性。始终将多个集群节点定义为应用服务的目标。

一些调度引擎还支持所谓的定位感知或地理感知。这意味着你可以要求调度器在一组不同的位置之间均匀分配服务实例。例如,你可以定义一个数据中心标签,可能包括西部、中心和东部值,并将该标签应用于所有集群节点,标签值对应各节点所在的地理区域。然后,你可以指示调度器使用该标签来实现特定应用服务的地理感知。在这种情况下,如果你请求该服务的九个副本,调度器将确保三个副本分别部署到三个数据中心的节点——西部、中心和东部。

地理感知甚至可以分层定义;例如,你可以将数据中心作为顶级区分符,然后是可用区。

地理感知或位置感知用于减少由于电力供应故障或数据中心停机而导致的故障概率。如果应用实例分布在节点、可用区甚至数据中心之间,那么一切都同时宕机的可能性极小。总会有一个区域保持可用。

安全

目前,IT 安全是一个非常热门的话题。网络战争已经达到前所未有的高峰。大多数高知名度公司都曾成为黑客攻击的受害者,且代价非常高昂。

每个首席信息官CIO)或首席技术官CTO)最怕的噩梦之一,就是早晨醒来听到新闻报道说他们的公司成为黑客攻击的受害者,敏感信息被盗或被泄露。

为了应对大多数安全威胁,我们需要建立一个安全的软件供应链,并加强深度安全防御。让我们来看一下你可以期待企业级调度器的一些任务。

安全通信和加密节点身份

首先,我们要确保由调度器管理的集群是安全的。只有受信任的节点才能加入集群。每个加入集群的节点都会获得一个加密节点身份,节点之间的所有通信必须加密。为此,节点可以使用互信传输层安全MTLS)。为了相互验证集群节点,使用证书。这些证书会定期自动轮换,或者根据请求轮换,以防止证书泄露时保护系统。

集群中发生的通信可以分为三种类型。你可以说有三种通信平面——管理平面、控制平面和数据平面:

  • 管理平面由集群管理器或主节点使用,用于例如调度服务实例、执行健康检查,或创建和修改集群中的任何其他资源,如数据卷、密钥或网络。

  • 控制平面用于在集群中的所有节点之间交换重要的状态信息。例如,这些信息会用来更新集群中的本地 IP 表,以便进行路由。

  • 数据平面是实际的应用服务相互通信和交换数据的地方。

通常,编排器主要关注于确保管理和控制平面的安全。数据平面的安全则交由用户负责,尽管编排器可能会协助这一任务。

安全网络和网络策略

在运行应用服务时,并非每个服务都需要与集群中的其他所有服务进行通信。因此,我们希望能够将服务进行沙箱化,只在同一网络沙箱中运行那些必须互相通信的服务。所有其他服务以及来自集群外部的所有网络流量都应该无法访问这些沙箱化的服务。

至少有两种方式可以实现基于网络的沙箱化。我们可以使用软件定义网络SDN)将应用服务分组,或者我们可以拥有一个平坦的网络,并使用网络策略来控制谁可以访问特定的服务或服务组。

基于角色的访问控制(RBAC)

编排器必须履行的最重要任务之一(仅次于安全性)是为集群及其资源提供 RBAC,以便其达到企业级的可用性。

RBAC 定义了系统中的主体、用户或用户组(按团队等组织方式)如何访问和操作系统资源。它确保未授权的人员无法对系统造成任何损害,也不能看到他们不应该知道或不应访问的系统资源。

一个典型的企业可能有如开发、QA、生产等用户组,并且每个组可能有一个或多个用户。开发人员 John Doe 是开发组的成员,因此他可以访问专属于开发团队的资源,但他不能访问生产团队的资源,比如 Ann Harbor 的资源。反过来,Ann 也无法干预开发团队的资源。

实现 RBAC 的一种方式是通过定义授权。授权是主体、角色和资源集合之间的关联。这里,角色包含一组对资源的访问权限。这些权限可以是创建、停止、删除、列出或查看容器;部署新的应用服务;列出集群节点或查看集群节点的详细信息;以及其他许多操作。

资源集合是集群中一组逻辑上相关的资源,例如应用服务、机密、数据卷或容器。

机密

在我们的日常生活中,我们有很多秘密。秘密是指那些不应公开的资料,比如你用来访问在线银行账户的用户名和密码组合,或者你手机的密码或健身房储物柜的密码。

在编写软件时,我们也经常需要使用秘密。例如,我们需要一个证书来认证我们的应用服务与我们想要访问的外部服务,或者我们需要一个令牌来认证和授权我们的服务在访问某些 API 时。

过去,为了方便,开发者通常会将这些值硬编码在代码中,或者放在一些外部配置文件中以明文形式存储。这样,这些非常敏感的信息就能被广泛的用户访问,而实际上,这些人根本不应该有机会看到这些秘密。

幸运的是,如今的编排工具提供了所谓的“秘密”功能,以一种高度安全的方式处理敏感信息。秘密可以由授权或信任的人员创建。这些秘密的值会被加密并存储在高可用的集群状态数据库中。由于秘密是加密的,因此它们在静态存储时是安全的。一旦授权的应用服务请求一个秘密,该秘密只会被转发到实际运行该特定服务实例的集群节点,并且秘密值永远不会存储在节点上,而是以 tmpfs 基于 RAM 的卷的形式挂载到容器中。

只有在各自的容器内部,秘密值才以明文形式可用。我们已经提到过,秘密在静态存储时是安全的。一旦服务请求它们,集群管理器(或主节点)会解密秘密并通过网络将其发送到目标节点。那么,秘密在传输过程中如何保持安全呢?好吧,我们之前学习过,集群节点使用 MTLS 进行通信,因此,虽然秘密以明文传输,但由于数据包会被 MTLS 加密,秘密依然是安全的。因此,秘密在静态存储和传输过程中都是安全的。只有被授权使用秘密的服务才能访问这些秘密值。

Kubernetes 中的秘密

请注意,尽管 Kubernetes 中使用的秘密相对安全,但文档仍然建议将其与更加安全的服务结合使用,即像 AWS Secrets Manager 或 Hashicorp 的 Vault 这样的秘密管理器。

内容信任

为了增强安全性,我们希望确保只有受信任的镜像在我们的生产集群中运行。一些编排器允许我们配置集群,以便它只能运行签名的镜像。内容信任和镜像签名都关乎确保镜像的作者是我们期望的人,即我们信任的开发者或者更好的情况是我们信任的 CI 服务器。此外,通过内容信任,我们希望确保我们得到的镜像是新鲜的,并且不是旧的,可能存在漏洞的镜像。最后,我们希望确保在传输过程中镜像不能被恶意黑客篡改。后者通常称为中间人攻击MITM攻击)。

通过在源头签署镜像并在目标处验证签名,我们可以保证我们想要运行的镜像没有被篡改。

反向正常运行时间

在安全上下文中,我想讨论的最后一点是反向正常运行时间。这是什么意思呢?想象一下,你已经配置并确保了一个生产集群的安全性。在这个集群上,你正在运行公司的几个关键应用程序。现在,一个黑客成功找到了你软件堆栈中的一个安全漏洞,并且已经获取了对一个集群节点的根访问权限。单单这已经够糟糕的了,更糟糕的是,这个黑客现在可以掩盖他们在这个节点上的存在,并假装是机器的根用户,然后利用它作为基础攻击集群中的其他节点。

在 Linux 或任何 Unix 类型的操作系统中,根访问权限意味着你可以在该系统上执行任何操作。这是一个人能够拥有的最高级别的访问权限。在 Windows 中,相当于这个角色的是管理员角色。

但是如果我们利用容器是短暂的,集群节点可以快速配置,通常情况下只需几分钟就能完成?我们只需在每个集群节点运行一定正常运行时间后,例如一天,就杀死它们。编排器被指示排空节点,然后将其从集群中排除。一旦节点离开集群,它将被拆除并由新配置的节点替换。

这样,黑客就失去了他们的基础,问题已被消除。尽管这个概念目前还没有广泛推广,但对我来说,这似乎是提高安全性的一个巨大步骤,并且据我与在这一领域工作的工程师讨论,实施起来并不困难。

内省

到目前为止,我们已经讨论了很多编排器的责任和它完全自主执行的任务。然而,还需要人员操作者能够查看和分析集群上当前运行的内容,以及个别应用程序的状态或健康情况。为了做到这一点,我们需要 introspection 的可能性。编排器需要以易于消化和理解的方式展示关键信息。

协调器应当从所有集群节点收集系统指标,并使操作员可以访问这些数据。指标包括 CPU、内存和磁盘使用情况、网络带宽消耗等。这些信息应该易于在每个节点的基础上获取,也可以以聚合的形式提供。

我们还希望协调器能让我们访问由服务实例或容器生成的日志。更进一步,如果我们拥有正确的授权,协调器应该为我们提供exec权限,让我们可以访问每个容器。通过 exec 访问容器后,我们就可以调试表现异常的容器。

在高度分布式的应用程序中,每个请求都会通过多个服务,直到完全处理完成,追踪请求是一个非常重要的任务。

理想情况下,协调器应支持我们实施追踪策略,或者为我们提供一些良好的指南。

最后,人类操作员在监控系统时,最好能使用一个图形化展示所有收集的指标、日志和跟踪信息的界面。这里我们指的是仪表盘。每个合格的协调器都应该提供至少一个基本的仪表盘,图形化展示最关键的系统参数。

然而,人类操作员并不是唯一关注自省的人。我们还需要能够将外部系统与协调器连接,以便消耗这些信息。需要有一个可用的 API,通过它外部系统可以访问集群状态、指标和日志等数据,并使用这些信息做出自动化决策,比如创建呼叫器或电话警报、发送电子邮件,或者在系统超过某些阈值时触发警报。

流行编排器概述

截至目前,市面上有许多编排引擎在使用,但也有一些明显的赢家。第一的位置无疑是 Kubernetes,它遥遥领先。第二位是 Docker 的 SwarmKit,其后是 Apache Mesos、AWS 的Elastic Container ServiceECS)和微软的Azure Container ServiceACS)。

Kubernetes

Kubernetes 最初由谷歌设计,后来捐赠给了Cloud Native Computing FoundationCNCF)。Kubernetes 的设计借鉴了谷歌的专有系统 Borg,Borg 已经在超大规模的环境中运行容器多年。Kubernetes 是谷歌尝试重新开始的结果,彻底从头开始设计一个系统,结合了 Borg 中所有学到的经验教训。

与 Borg(一个专有技术)不同,Kubernetes 从一开始就开源了。谷歌做出这个选择非常明智,因为它吸引了大量来自公司外部的贡献者,并且在短短几年内,围绕 Kubernetes 形成了一个更为庞大的生态系统。你可以理直气壮地说,Kubernetes 是容器编排领域的宠儿。没有其他编排工具能够产生如此大的热度,并吸引如此多的有才华的人们,他们愿意以贡献者或早期采纳者的身份为项目的成功做出有意义的贡献。

在这方面,Kubernetes 在容器编排领域给我的感觉很像 Linux 在服务器操作系统领域的地位。Linux 已经成为服务器操作系统的事实标准。所有相关的公司,如微软、IBM、亚马逊、Red Hat,甚至 Docker,都已经接受了 Kubernetes。

有一点是不可否认的:Kubernetes 从一开始就是为大规模可扩展性而设计的。毕竟,它的设计是基于 Google Borg 的。

一个可能对 Kubernetes 提出的负面意见是,它仍然很复杂,至少在写作时是这样。对于新手来说,存在一个显著的门槛。第一步陡峭,但一旦你与这个编排工具合作了一段时间,所有一切都会变得清晰。整体设计经过深思熟虑,并且执行得非常好。

在 Kubernetes 1.10 版本中,其正式发布GA)是在 2018 年 3 月,解决了与其他编排工具(如 Docker Swarm)相比的大多数初期不足之处。例如,安全性和保密性现在不仅仅是事后考虑,而是系统的核心部分。

新功能以惊人的速度被实现。新版本大约每三个月发布一次,确切地说,大约每 100 天发布一次。大多数新功能都是由需求驱动的,也就是说,使用 Kubernetes 编排其关键应用的公司可以提出他们的需求。这使得 Kubernetes 成为企业级可用的工具。如果认为这个编排工具仅适合初创公司,而不适合风险规避的大型企业,那就是错误的。恰恰相反。我的这个观点是基于这样的事实:像微软、Docker 和 Red Hat 这样的公司,其客户大多是大型企业,已经完全接受了 Kubernetes,并为其提供企业级支持,特别是在其产品中使用并集成 Kubernetes。

Kubernetes 支持 Linux 和 Windows 容器。

Docker Swarm

众所周知,Docker 推广并使软件容器商品化。Docker 没有发明容器,但标准化了容器并使其广泛可用,尤其是通过提供免费的镜像仓库——Docker Hub。最初,Docker 主要关注开发者和开发生命周期。然而,开始使用并喜爱容器的公司,很快也希望不仅在新应用的开发或测试阶段使用它们,还希望在生产环境中运行这些应用。

起初,Docker 在这个领域没有什么可以提供的,因此其他公司填补了这个空白,为用户提供帮助。但不久之后,Docker 认识到,市场上对一个简单而强大的编排工具有着巨大的需求。Docker 的第一次尝试是推出名为 Classic Swarm 的产品。它是一个独立的产品,使用户能够创建一个 Docker 主机集群,用于以高度可用和自我修复的方式运行和扩展其容器化应用。

然而,Docker Classic Swarm 的设置过程非常困难,涉及很多复杂的手动步骤。客户非常喜欢这个产品,但却因其复杂性而感到困惑。因此,Docker 决定做得更好。它重新开始设计并提出了 SwarmKit。

SwarmKit 于 2016 年 DockerCon 在西雅图发布,并成为 Docker Engine 最新版本的一个组成部分。是的,你没听错,SwarmKit 曾经是并且直到今天依然是 Docker Engine 的核心组成部分。因此,如果你安装了一个 Docker 主机,那么你就自动获得了 SwarmKit。

SwarmKit 的设计考虑了简易性和安全性。它的核心理念是,设置一个 Swarm 应该几乎是微不足道的,而且 Swarm 在默认情况下必须具有高度的安全性。Docker Swarm 假设最小权限原则。

安装一个完整的、高可用性的 Docker Swarm 实际上非常简单,只需在集群中的第一个节点上运行 docker swarm init,该节点将成为所谓的领导节点,然后在其他所有节点上运行 docker swarm join <join-token><join-token> 是在初始化时由领导节点生成的。在最多 10 个节点的 Swarm 中,整个过程不到 5 分钟。如果自动化执行,所需时间更短。

正如我之前提到的,当 Docker 设计和开发 SwarmKit 时,安全性是最重要的需求之一。容器通过依赖 Linux 内核命名空间和 cgroups,以及 Linux 系统调用白名单 (seccomp) 和支持 Linux 能力以及 Linux 安全模块 (LSM) 来提供安全性。现在,在这些基础上,SwarmKit 添加了 MTLS 和在静态和传输过程中都加密的密钥。

此外,Swarm 定义了所谓的 容器网络模型 (CNM),该模型支持软件定义网络(SDN),为运行在 Swarm 上的应用服务提供沙箱功能。Docker SwarmKit 支持 Linux 和 Windows 容器。

Apache Mesos 和 Marathon

Apache Mesos 是一个开源项目,最初旨在让一群服务器或节点从外部看起来像一个单一的大型服务器。Mesos 是一款简化计算机集群管理的软件。Mesos 的用户不需要关心单个服务器,而只需假设他们拥有一个巨大的资源池,这些资源池对应着集群中所有节点的资源总和。

在 IT 术语中,Mesos 已经算是相当老旧了,至少与其他调度器相比是这样。它最早在 2009 年公开展示,但那时当然并没有设计用于运行容器,因为 Docker 当时还不存在。类似于 Docker 对容器的处理,Mesos 使用 Linux 的 cgroups 来隔离资源,如 CPU、内存或磁盘 I/O,以供单个应用程序或服务使用。

Mesos 实际上是其他有趣的服务的底层基础设施,许多有意思的服务都是建立在它之上的。从容器的角度来看,Marathon 非常重要。Marathon 是一个运行在 Mesos 之上的容器调度器,能够扩展到数千个节点。

Marathon 支持多种容器运行时,例如 Docker 或它自有的 Mesos 容器。它不仅支持无状态的应用服务,还支持有状态的应用服务,例如 PostgreSQL 或 MongoDB 等数据库。类似于 Kubernetes 和 Docker SwarmKit,它支持本章前面提到的许多特性,如高可用性、健康检查、服务发现、负载均衡和位置感知等,这些只是其中一些最重要的功能。

尽管 Mesos,以及在某种程度上 Marathon,是相当成熟的项目,但它们的应用范围相对有限。它似乎在大数据领域最为流行,也就是用于运行数据处理服务,如 Spark 或 Hadoop。

亚马逊 ECS

如果你正在寻找一个简单的调度器,并且已经深度融入 AWS 生态系统,那么亚马逊的 ECS 可能是适合你的选择。值得指出的是 ECS 有一个非常重要的限制:如果你选择了这个容器调度器,那么你就会被锁定在 AWS 平台中。你将无法轻松地将运行在 ECS 上的应用程序移植到其他平台或云端。

亚马逊将其 ECS 服务推广为一个高度可扩展、快速的容器管理服务,使得在集群中运行、停止和管理 Docker 容器变得更加容易。除了运行容器外,ECS 还提供直接访问许多其他 AWS 服务,这些服务可以从运行在容器内部的应用程序服务中访问。这种与许多流行 AWS 服务的紧密无缝集成,使得 ECS 对于那些寻找简便方法以在一个强大且高度可扩展环境中启动和运行其容器化应用的用户非常有吸引力。亚马逊还提供了其自有的私人镜像注册表。

使用 AWS ECS,您可以通过 Fargate 完全管理底层基础设施,使您能够专注于部署容器化应用程序,且无需关心如何创建和管理节点集群。ECS 支持 Linux 和 Windows 容器。

总结来说,ECS 简单易用、可扩展性强,并且与其他流行的 AWS 服务集成得很好,但它不如 Kubernetes 或 Docker SwarmKit 那样强大,而且仅在亚马逊 AWS 平台上可用。

AWS EKS

亚马逊弹性 Kubernetes 服务EKS)是 AWS 提供的一项托管 Kubernetes 服务。它简化了使用 Kubernetes 进行容器化应用程序的部署、管理和扩展,使开发人员和运维团队能够专注于构建和运行应用程序,而无需管理 Kubernetes 控制平面的负担。

EKS 与各种 AWS 服务无缝集成,例如弹性负载均衡、Amazon RDS 和 Amazon S3,使得构建一个完全托管、可扩展且安全的容器化应用程序基础设施变得轻而易举。它还支持 Kubernetes 生态系统,允许用户利用现有的工具、插件和扩展来管理和监控他们的应用程序。

使用亚马逊 EKS,Kubernetes 控制平面由 AWS 自动管理,确保高可用性、自动更新和安全补丁。用户只需负责管理其工作节点,这些节点可以通过 Amazon EC2 实例或 AWS Fargate 进行部署。

微软 ACS 和 AKS

与我们之前提到的 ECS 类似,我们可以对微软的 ACS 作出同样的评价。它是一个简单的容器编排服务,如果你已经深度投资于 Azure 生态系统,那么它是有意义的。我应该和我提到亚马逊 ECS 时一样说:如果你选择了 ACS,那么你就把自己锁定在微软的产品中。从 ACS 迁移容器化应用程序到其他平台或云环境将并非易事。

ACS 是微软的容器服务,支持多种编排器,如 Kubernetes、Docker Swarm 和 Mesos DC/OS。随着 Kubernetes 的越来越流行,微软的关注焦点显然已转向该编排器。微软甚至重新品牌化其服务,并将其命名为Azure Kubernetes ServiceAKS),以便将焦点集中在 Kubernetes 上。

AKS 在 Azure 中为您管理托管的 Kubernetes、Docker Swarm 或 DC/OS 环境,使您能够专注于您希望部署的应用程序,无需担心配置基础设施。微软用自己的话说如下:

“AKS 使得部署和管理容器化应用程序变得快速且简单,无需容器编排的专业知识。它还通过按需提供、升级和扩展资源,消除了持续运营和维护的负担,而无需让您的应用程序停机。”

总结

本章展示了为何首先需要编排器,以及它们是如何在概念上工作的。它指出了在写作时最突出的编排器,并讨论了不同编排器之间的主要共性和差异。

下一章将介绍 Docker 的原生编排器 SwarmKit。它将详细讲解 SwarmKit 用来在集群中部署和运行分布式、弹性、稳健和高可用应用的所有概念和对象——无论是在本地还是在云中。

深入阅读

以下链接提供了一些关于编排相关主题的深入见解:kubernetes.Io/

问题

回答以下问题以评估你的学习进度:

  1. 什么是容器编排引擎?

  2. 为什么我们需要容器编排引擎?

  3. 容器编排引擎的主要任务是什么?

  4. 一些流行的容器编排引擎有哪些?

  5. 容器编排如何提高应用的可靠性?

  6. 容器编排引擎如何帮助应用扩展?

  7. Kubernetes 和 Docker Swarm 之间的主要区别是什么?

  8. 容器编排引擎如何处理服务发现?

答案

下面是一些问题的可能答案:

  1. 容器编排引擎是一个自动化部署、扩展、管理和网络化容器的系统。它帮助开发人员和运维团队管理大量容器,确保它们在分布式环境中跨多个主机高效可靠地运行。

  2. 随着应用中容器和服务数量的增加,手动管理变得越来越困难。容器编排引擎自动化了容器管理的过程,能够实现高效的资源利用、高可用性、容错能力以及容器化应用的无缝扩展。

  3. 容器编排引擎的主要任务包括以下几个方面:

    • 容器部署:根据资源需求和约束将容器部署到合适的主机

    • 扩展:根据应用需求自动增加或减少容器的数量

    • 负载均衡:将网络流量分配到各个容器,确保最佳性能

    • 服务发现:使容器能够找到并与彼此通信

    • 健康监控:监控容器健康状况并自动替换不健康的容器

    • 数据持久性和存储管理:管理存储卷并确保数据在容器重启和故障后保持持久性。

    • 安全性和访问控制:管理容器安全、网络策略和访问控制。

  4. 一些流行的容器编排引擎包括 Kubernetes、Docker Swarm、Apache Mesos、Microsoft ACS、Microsoft AKS 和 Amazon ECS。

  5. 容器编排引擎通过确保容器部署在适当的主机上、监控容器健康状况,并自动替换不健康或失败的容器,从而提高应用程序的可靠性。它们还通过在容器之间分配网络流量来帮助保持应用程序的可用性,使系统能够优雅地处理故障和流量高峰。

  6. 容器编排引擎可以根据需求、资源使用情况和预定义规则,自动扩展应用程序,添加或移除容器。这确保了应用程序能够处理不同级别的流量和工作负载,同时优化资源使用。

  7. Kubernetes 和 Docker Swarm 都是容器编排引擎,但它们有一些关键区别:

    • Kubernetes 功能更丰富且灵活,提供广泛的功能和可扩展性。Docker Swarm 更简单,易于设置,注重易用性并与 Docker 生态系统的集成。

    • Kubernetes 使用声明式方法,允许用户描述系统的期望状态,而 Docker Swarm 则使用更具命令式的方法。

    • 相比 Docker Swarm,Kubernetes 的学习曲线更陡峭,而 Docker Swarm 的学习曲线较浅,对于已经熟悉 Docker 的用户来说更加简单直接。

    • 相比 Docker Swarm,Kubernetes 拥有更大的社区、更广泛的文档和更多的第三方集成。

  8. 容器编排引擎通过提供容器间发现和通信的机制来处理服务发现。它们通常为容器分配唯一的网络地址或主机名,并维护这些地址的注册表。容器可以使用这些地址与应用程序中的其他服务进行通信。一些编排引擎还提供内置的负载均衡和基于 DNS 的服务发现,简化这一过程。

第十四章:14

介绍 Docker Swarm

在上一章中,我们介绍了编排工具。就像乐团中的指挥,编排工具确保我们所有的容器化应用服务能够和谐地一起工作,并为共同的目标做出贡献。这些编排工具有很多责任,我们已经详细讨论过。最后,我们简要概述了市场上最重要的容器编排工具。

本章介绍了 Docker 的原生编排工具SwarmKit。详细阐述了 SwarmKit 用来在集群中部署和运行分布式、弹性、稳健和高可用应用程序的所有概念和对象,不论是在本地环境还是云环境中。本章还介绍了 SwarmKit 如何通过使用软件定义网络SDN)来隔离容器,从而确保应用程序的安全。我们将学习如何在本地、一个叫做Play with DockerPWD)的特殊环境中,及在云中创建 Docker Swarm。最后,我们将部署一个由多个与 Docker Swarm 相关的服务组成的应用程序。

本章将讨论以下主题:

  • Docker Swarm 架构

  • 堆栈、服务和任务

  • 多主机网络

  • 创建 Docker Swarm

  • 部署第一个应用程序

完成本章后,你将能够做以下事情:

  • 在白板上勾画出高可用 Docker Swarm 的关键部分

  • 用两三句简单的语言向感兴趣的外行解释什么是(Swarm)服务

  • 在 AWS、Azure 或 GCP 上创建一个高可用性的 Docker Swarm,其中包括三个管理节点和两个工作节点

  • 成功部署一个像 Nginx 这样的复制服务到 Docker Swarm 中

  • 扩展和缩减运行中的 Docker Swarm 服务

  • 检索复制的 Docker Swarm 服务的聚合日志

  • 为一个由至少两个交互服务组成的示例应用程序编写一个简单的堆栈文件

  • 将堆栈部署到 Docker Swarm

让我们开始吧!

Docker Swarm 架构

从 30,000 英尺的高度来看,Docker Swarm 的架构由两部分组成:一个奇数个管理节点的 Raft 共识组,以及一个通过 Gossip 网络相互通信的工作节点组,这个网络也被称为控制平面。下图展示了这一架构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.1 – Docker Swarm 的高级架构

管理节点负责管理 Swarm,而工作节点则执行部署到 Swarm 中的应用程序。每个管理节点都有 Swarm 完整状态的副本,存储在本地的 Raft 存储中。管理节点同步地相互通信,它们的 Raft 存储始终保持同步。

另一方面,工作节点为了可扩展性原因是异步地相互通信的。在一个 Swarm 中,工作节点的数量可以达到数百,甚至数千个。

现在我们对 Docker Swarm 有了一个高层次的概览,接下来让我们更详细地描述 Docker Swarm 的所有组成部分。

Swarm 节点

Swarm 是一个节点集合。我们可以将节点分类为物理计算机或虚拟机VM)。如今,物理计算机通常被称为裸金属。人们用“裸金属”来区分与虚拟机上的运行。

当我们在这样的节点上安装 Docker 时,我们称这个节点为 Docker 主机。以下图示更清楚地展示了节点和 Docker 主机的概念:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.2 – Docker Swarm 节点的裸金属和虚拟机类型

要成为 Docker Swarm 的成员,节点必须是 Docker 主机。Docker Swarm 中的节点可以有两种角色:它可以是管理节点,也可以是工作节点。管理节点做它名字所暗示的工作;它们管理 Swarm。而工作节点则执行应用程序负载。

从技术上讲,管理节点也可以是工作节点,因此可以运行应用程序负载——尽管不推荐这样做,特别是当 Swarm 是一个运行关键任务应用程序的生产系统时。

Swarm 管理节点

每个 Docker Swarm 至少需要包含一个管理节点。出于高可用性的考虑,我们应该在 Swarm 中有多个管理节点。这对于生产环境或类似生产的环境尤为重要。如果我们有多个管理节点,那么这些节点将使用 Raft 共识协议一起工作。Raft 共识协议是一种标准协议,通常用于多个实体需要共同工作,并始终需要就接下来执行哪个操作达成一致的场景。

为了良好运作,Raft 共识协议要求在所谓的共识组中有一个奇数数量的成员。因此,我们应该始终有 1、3、5、7 等个管理节点。在这样的共识组中,总是会有一个领导者。在 Docker Swarm 中,第一个启动 Swarm 的节点最初会成为领导者。如果领导者离开,剩余的管理节点会选举出一个新的领导者。共识组中的其他节点被称为跟随者。

Raft 领导者选举

Raft 使用心跳机制来触发领导者选举。当服务器启动时,它们会首先作为跟随者存在。只要服务器接收到来自领导者或候选者的有效远程过程调用RPCs),它就保持在跟随者状态。领导者会定期向所有跟随者发送心跳,以维持其权威。如果跟随者在一段时间内没有收到任何通信(该时间段称为选举超时),它就假设没有可行的领导者,并开始选举一个新的领导者。在选举过程中,每台服务器都会启动一个随机选择的计时器。当计时器触发时,服务器将自己从跟随者变为候选者。同时,它会增加 term 值,并向所有对等节点发送投票请求,等待回应。

在 Raft 共识算法的上下文中,“term”对应于一次选举轮次,并作为系统的逻辑时钟,使 Raft 能够检测到过时的信息,如过期的领导者。每次发起选举时,term 值都会增加。

当服务器收到投票请求时,只有在候选者的 term 值较高或候选者的 term 与自身相同时,服务器才会投票。否则,投票请求将被拒绝。每个对等节点每个 term 只能投给一个候选者,但如果它收到的投票请求的 term 值比之前投票的候选者更高,它将放弃之前的投票。

在 Raft 和许多其他分布式系统的上下文中,“日志”指的是状态机日志或操作日志,而不是传统的应用程序日志。

如果候选者在下一个计时器触发前没有获得足够的票数,当前投票将作废,候选者将以更高的 term 值开始新一轮选举。一旦候选者获得大多数对等节点的票数,它就会将自己从候选者转为领导者,并立即广播其权威,以防止其他服务器开始领导者选举。领导者会定期广播这一信息。现在,假设我们因为维护原因关闭了当前的领导者节点,剩余的管理节点将选举新的领导者。当之前的领导者节点重新上线时,它将变为跟随者,而新的领导者将继续担任领导者职务。

所有共识组成员彼此之间同步通信。每当共识组需要做出决策时,领导者会向所有跟随者请求同意。如果大多数管理节点给予肯定回答,领导者就会执行任务。这意味着,如果我们有三个管理节点,那么至少一个跟随者必须同意领导者。如果我们有五个管理节点,那么至少两个跟随者必须同意领导者。

由于所有管理节点必须与领导节点同步通信以在集群中做出决策,随着我们形成共识组的管理节点数量增加,决策过程变得越来越慢。Docker 的推荐做法是在开发、演示或测试环境中使用一个管理节点。在小型到中型 Swarm 中使用三个管理节点,在大型到超大型 Swarm 中使用五个管理节点。在 Swarm 中使用超过五个管理节点几乎是没有必要的。

管理节点不仅负责管理 Swarm,还负责维护 Swarm 的状态。我们说的“状态”是什么意思?当我们谈论 Swarm 的状态时,我们指的是关于它的所有信息——例如,Swarm 中有多少个节点,每个节点的属性是什么,如名称或 IP 地址。我们还指的是哪些容器在 Swarm 中的哪个节点上运行等等。而 Swarm 的状态中不包含的是由在 Swarm 上运行的容器中的应用服务所产生的数据。这些被称为应用数据,绝对不属于由管理节点管理的状态:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.3 – 一个 Swarm 管理节点共识组

所有的 Swarm 状态都存储在每个管理节点上的高性能键值存储kv-store)中。没错,每个管理节点都存储整个 Swarm 状态的完整副本。这种冗余使 Swarm 具有高度可用性。如果一个管理节点发生故障,其余的管理节点都可以快速访问完整的状态。

如果一个新的管理节点加入共识组,那么它会与该组的现有成员同步 Swarm 状态,直到它拥有完整的副本。在典型的 Swarm 中,这种复制通常非常快速,但如果 Swarm 很大并且上面运行着许多应用程序,它可能需要一些时间。

Swarm 工作节点

正如我们之前提到的,Swarm 工作节点的任务是托管和运行包含实际应用服务的容器,这些是我们希望在集群中运行的服务。它们是 Swarm 的“工作马”。理论上,管理节点也可以是工作节点。但正如我们所说的,这在生产系统中并不推荐。在生产系统中,我们应该让管理节点专职管理。

工作节点通过所谓的控制平面相互通信。它们使用 gossip 协议进行通信。这种通信是异步的,这意味着在任何给定的时刻,不是所有工作节点都处于完美同步状态。

现在,你可能会问——工作节点交换哪些信息?这些信息主要是服务发现和路由所需的信息,也就是关于哪些容器正在运行在什么节点上的信息,等等:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.4 – 工作节点之间的通信

在前面的示意图中,你可以看到工作节点之间是如何相互通信的。为了确保在大规模 Swarm 中传播(gossip)能够良好扩展,每个工作节点仅与三个随机邻居同步自己的状态。对于熟悉大 O 符号的人来说,这意味着使用传播协议的工作节点同步的扩展是 O(0)。

大 O 符号解释

大 O 符号是一种描述给定算法的速度或复杂度的方式。它告诉你一个算法将执行多少次操作。它用于传达一个算法的速度,这在评估他人的算法和自己算法时都非常重要。

例如,假设你有一个数字列表,并且你想在列表中查找一个特定的数字。你可以使用不同的算法来完成这个任务,比如简单查找或二分查找。简单查找会逐个检查列表中的数字,直到找到你要找的数字。另一方面,二分查找则会反复将列表分成两半,直到找到你要找的数字。

现在,假设你有一个包含 100 个数字的列表。对于简单查找,最坏情况下,你需要检查所有 100 个数字,所以需要 100 次操作。而对于二分查找,最坏情况下,你只需检查大约 7 个数字(因为 log2(100) 大约是 7),所以只需要 7 次操作。

在这个例子中,二分查找比简单查找要快。但如果你有一个包含 10 亿个数字的列表呢?简单查找需要进行 10 亿次操作,而二分查找只需要大约 30 次操作(因为 log2(10 亿) 大约是 30)。因此,随着列表的增大,二分查找比简单查找要快得多。

大 O 符号用于描述算法之间速度的差异。在大 O 符号中,简单查找被描述为 O(n),这意味着操作的数量随着列表大小(n)的增长呈线性增长。二分查找被描述为 O(log n),这意味着操作的数量随着列表大小的增长呈对数增长。

工作节点是被动的。它们除了运行由管理节点分配的工作负载外,通常不会主动做其他任何事情。不过,工作节点会确保以其最大能力运行这些工作负载。在本章稍后部分,我们将详细了解管理节点分配给工作节点的具体工作负载。

现在我们知道了 Docker Swarm 中的主节点和工作节点,我们将介绍堆栈、服务和任务。

堆栈、服务和任务

使用 Docker Swarm 而非单个 Docker 主机时,出现了范式的变化。我们不再谈论运行进程的单个容器,而是将其抽象为表示每个进程副本集合的服务,通过这种方式实现高可用性。我们也不再谈论拥有固定名称和 IP 地址的单个 Docker 主机来部署容器;现在我们将谈论部署服务的主机集群。我们不再关心单个主机或节点,我们不再给它赋予有意义的名称;每个节点对我们来说只是一个数字。

我们现在不再关心单个容器以及它们的部署位置——我们只关心通过服务定义的期望状态。我们可以尝试通过下面的图示来表示这一点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.5 – 容器被部署到已知的服务器上

与前面图示中的做法不同,之前我们将 web 容器部署到 IP 地址为 52.120.12.1 的 alpha 服务器,将支付容器部署到 IP 为 52.121.24.33 的 beta 服务器,现在我们切换到这个新的服务和 Swarm(或者更广义的集群)范式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.6 – 服务被部署到 Swarm 集群

在前面的图示中,我们看到一个 web 服务和一个库存服务都被部署到由多个节点组成的 Swarm 集群中。每个服务都有一定数量的副本:web 服务有五个副本,库存服务有七个副本。我们并不关心这些副本会在哪个节点上运行;我们只关心所请求的副本数量始终在 Swarm 调度器决定将它们放在哪些节点上时保持运行。

话虽如此,现在让我们介绍一下在 Docker Swarm 中服务的概念。

服务

Swarm 服务是一个抽象概念。它是我们希望在 Swarm 中运行的应用程序或应用服务的期望状态描述。Swarm 服务就像一个清单,描述以下内容:

  • 服务的名称

  • 用于创建容器的镜像

  • 运行的副本数量

  • 服务容器所连接的网络

  • 应映射的端口

拥有这个服务清单后,Swarm 管理器会确保如果实际状态与期望状态发生偏差时,始终会将它们调整回期望状态。所以,例如,如果某个服务的实例崩溃了,Swarm 管理器上的调度器就会在有空闲资源的节点上调度该服务的一个新实例,以便重新建立期望的状态。

那么,任务是什么呢?这就是我们接下来要学习的内容。

任务

我们已经了解到,服务对应的是应用服务应始终处于的期望状态的描述。该描述的一部分是服务应运行的副本数量。每个副本由一个任务表示。在这方面,Swarm 服务包含一个任务集合。在 Docker Swarm 中,任务是一个原子部署单元。服务的每个任务都由 Swarm 调度器部署到一个工作节点。任务包含工作节点运行基于镜像的容器所需的所有信息,而镜像是服务描述的一部分。在任务和容器之间,存在一对一的关系。容器是运行在工作节点上的实例,而任务是容器作为 Swarm 服务一部分的描述。

最后,让我们在 Docker Swarm 的背景下讨论一下栈。

现在我们对 Swarm 服务和任务有了很好的了解,接下来我们可以介绍栈。栈用于描述一组相关的 Swarm 服务,它们很可能是因为属于同一个应用程序而关联的。从这个意义上讲,我们也可以说栈描述的是一个由一个或多个服务组成的应用程序,我们希望在 Swarm 上运行这些服务。

通常,我们在一个使用 YAML 格式的文本文件中声明一个栈,并且该文件使用与已知的 Docker Compose 文件相同的语法。这导致了一种情况,人们有时会说栈是由 Docker Compose 文件描述的。更好的说法是,栈是在一个使用与 Docker Compose 文件相似语法的栈文件中描述的。

让我们尝试通过以下图示来说明栈、服务和任务之间的关系,并将其与栈文件的典型内容联系起来:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.7 – 显示栈、服务和任务之间关系的图示

在前面的图示中,我们可以看到右侧是一个示例栈的声明式描述。该栈包含三个服务,分别是 webpaymentsinventory。我们还看到,web 服务使用的是 example/web:1.0 镜像,并且有四个副本。在图示的左侧,我们看到栈包含了前面提到的三个服务。每个服务又包含了若干任务,副本数目就是任务的数量。在 web 服务的情况下,我们有一个包含四个任务的集合。每个任务包含将从其启动容器的镜像名称,一旦任务被调度到 Swarm 节点上,容器便会启动。

现在,既然你已经对 Docker Swarm 的主要概念有了很好的理解,比如节点、栈、服务和任务,让我们更仔细地看看在 Swarm 中使用的网络。

多主机网络

第十章使用单主机网络中,我们讨论了容器如何在单一 Docker 主机上进行通信。现在,我们有一个由多个节点或 Docker 主机构成的 Swarm 集群。位于不同节点上的容器需要能够相互通信。许多技术可以帮助我们实现这个目标。Docker 选择为 Docker Swarm 实现一个覆盖网络驱动程序。这个覆盖网络允许连接到同一覆盖网络的容器相互发现并自由通信。以下是覆盖网络工作原理的示意图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.8 – 覆盖网络

我们有两个节点或 Docker 主机,IP 地址分别为172.10.0.15172.10.0.16。我们选择的 IP 地址值并不重要;重要的是这两个主机有不同的 IP 地址,并且通过物理网络(网络电缆)连接,这个物理网络被称为底层网络

在左侧的节点上,我们有一个运行中的容器,IP 地址为10.3.0.2;在右侧的节点上,我们有另一个容器,IP 地址为10.3.0.5。现在,前者容器想要与后者容器通信。这个过程怎么实现呢?在第十章使用单主机网络中,我们已经看到当两个容器位于同一节点时,如何通过使用 Linux 桥接来实现这种通信。但 Linux 桥接仅在本地运行,无法跨节点工作。所以,我们需要其他机制。此时,Linux VXLAN 来到救援。VXLAN 从容器技术出现之前就已在 Linux 中可用。

VXLAN 解释

VXLAN,即虚拟扩展局域网,是一种网络协议,它通过使用 UDP 协议在 IP 网络上创建虚拟的二层域。它的设计目的是解决 IEEE 802.1q 中 VLAN ID 数量有限(4,096)的难题,通过将标识符的大小扩展到 24 位(16,777,216)。

简而言之,VXLAN 允许创建可以跨越不同物理位置的虚拟网络。例如,某些运行在不同主机上的虚拟机可以通过 VXLAN 隧道进行通信。这些主机可以位于不同的子网,甚至在全球不同的数据中心。从虚拟机的角度来看,同一 VXLAN 中的其他虚拟机在同一个二层域内。

图 14.8 中左侧的容器发送数据包时,桥接器意识到该数据包的目标不在此主机上。现在,每个参与 Overlay 网络的节点都会获得一个所谓的 VXLAN 隧道端点VTEP)对象,它会拦截数据包(此时的数据包是 OSI 第 2 层的数据包),并用一个包含目标主机的 IP 地址的标头将其包装起来(这将其转变为 OSI 第 3 层的数据包),然后通过 VXLAN 隧道发送。隧道另一端的 VTEP 会解包数据包并将其转发给本地桥接器,本地桥接器再将其转发给目标容器。

Overlay 驱动程序包含在 SwarmKit 中,并且在大多数情况下是 Docker Swarm 推荐的网络驱动程序。还有其他可以支持多节点的第三方网络驱动程序,可以作为插件安装在每个参与的 Docker 主机中。经过认证的网络插件可以从 Docker 商店获得。

很好,我们已经掌握了关于 Docker Swarm 的所有基础知识。那么,接下来我们就来创建一个。

创建 Docker Swarm

创建 Docker Swarm 几乎是微不足道的。它非常简单,以至于如果你了解编排器的工作原理,可能会觉得这几乎难以置信。但这是真的,Docker 在使 Swarm 变得简单而优雅方面做得非常出色。同时,Docker Swarm 已被证明在大企业使用时非常稳健且可扩展。

创建一个本地单节点 Swarm

所以,够了,别再想象了——让我们演示一下如何创建一个 Swarm。在最简单的形式下,一个完全运行的 Docker Swarm 只包含一个节点。如果你使用 Docker Desktop,甚至是 Docker Toolbox,那么你的个人电脑或笔记本电脑就是这样的一个节点。因此,我们可以从这里开始,并演示一些 Swarm 的最重要功能。

让我们初始化一个 Swarm。在命令行中,只需输入以下命令:

$ docker swarm init

经过极短的时间后,你应该看到如下输出:

Swarm initialized: current node (zqzxn4bur43lywp55fysnymd4) is now a manager.To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-57ayqfyc8cdg09hi9tzuztzcg2gk2rd6abu71ennaide3r20q5-21j3wpm8scytn9u5n1jrvlbzf 192.168.0.13:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

我们的计算机现在是一个 Swarm 节点。它的角色是管理者,并且是领导者(在管理者中是领导者,因为目前只有一个管理者)。虽然 docker swarm init 命令仅用了非常短的时间就完成,但在此期间,该命令做了很多事情。以下是其中的一些:

  • 它创建了一个根 证书 授权中心CA

  • 它创建了一个 kv-store,用来存储整个 Swarm 的状态

现在,在前面的输出中,我们可以看到一个命令,可以用来将其他节点加入我们刚刚创建的 Swarm。该命令如下:

$ docker swarm join --token <join-token> <IP address>:2377

这里,我们有以下内容:

  • <join-token> 是 Swarm 领导者在初始化 Swarm 时生成的令牌

  • <IP 地址> 是领导者的 IP 地址

尽管我们的集群保持简单,因为它只包含一个成员,但我们仍然可以要求 Docker CLI 列出 Swarm 的所有节点,使用 docker node ls 命令。这将类似于以下截屏:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.9 – 列出 Docker Swarm 的节点

在这个输出中,我们首先看到赋予节点的 ID。跟随 ID 的星号(*)表示这是执行了 docker node ls 命令的节点—基本上表明这是活动节点。然后,我们有节点的(人类可读的)名称及其状态、可用性和管理状态。正如前面提到的,这个 Swarm 的第一个节点自动成为了领导者,这在前面的截屏中已经显示出来了。最后,我们看到我们正在使用的 Docker Engine 版本。

要获取关于节点的更多信息,我们可以使用 docker node inspect 命令,如下截断输出所示:

$ docker node inspect node1[
    {
        "ID": "zqzxn4bur43lywp55fysnymd4",
        "Version": {
            "Index": 9
        },
        "CreatedAt": "2023-04-21T06:48:06.434268546Z",
        "UpdatedAt": "2023-04-21T06:48:06.955837213Z",
        "Spec": {
            "Labels": {},
            "Role": "manager",
            "Availability": "active"
        },
        "Description": {
            "Hostname": "node1",
            "Platform": {
                "Architecture": "x86_64",
                "OS": "linux"
            },
            "Resources": {
                "NanoCPUs": 8000000000,
                "MemoryBytes": 33737699328
            },
            "Engine": {
                "EngineVersion": "20.10.17",
                "Plugins": [
                    {
                        "Type": "Log",
                        "Name": "awslogs"
                    },
...
    }
]

该命令生成了大量信息,因此我们只呈现了输出的缩短版本。例如,在需要排除集群节点行为不端时,此输出非常有用。

在继续之前,请不要忘记使用以下命令关闭或解散该群集:

$ docker swarm leave --force

在接下来的部分,我们将使用 PWD 环境来生成和使用 Docker Swarm。

使用 PWD 生成 Swarm

要试验 Docker Swarm 而不必在本地计算机上安装或配置任何东西,我们可以使用 PWD。PWD 是一个可以通过浏览器访问的网站,提供了创建最多五个节点的 Docker Swarm 的能力。正如其名称所示,它绝对是一个游乐场,并且我们可以使用它的时间限制为每个会话四个小时。我们可以打开任意多个会话,但每个会话在四小时后会自动结束。除此之外,它是一个完全功能的 Docker 环境,非常适合玩弄 Docker 或展示一些功能。

现在访问该网站。在浏览器中导航至网站 labs.play-with-docker.com。您将看到一个欢迎和登录界面。使用您的 Docker ID 登录。成功登录后,您将看到一个类似以下截屏的界面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.10 – PWD 窗口

我们立即可以看到一个大计时器,从四小时开始倒计时。这就是我们在此会话中可以使用的时间。此外,我们看到一个**+ 添加新实例**链接。点击它以创建一个新的 Docker 主机。当你这样做时,你的屏幕应该如下截屏所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.11 – PWD 带有一个新节点

在左侧,我们可以看到新创建的节点及其 IP 地址(192.168.0.13)和名称(node1)。在右侧,上半部分显示了该新节点的一些附加信息,底部是一个终端窗口。是的,这个终端窗口用于在我们刚创建的节点上执行命令。该节点已安装 Docker CLI,因此我们可以在其上执行所有熟悉的 Docker 命令,比如 Docker 版本命令。试试看。

但现在我们想创建一个 Docker Swarm。在浏览器的终端中执行以下命令:

$ docker swarm init --advertise-addr=eth0

前述命令生成的输出与我们在创建本地 Docker Swarm 时看到的类似。需要注意的是 join 命令,它是我们希望用来将其他节点加入到刚创建的集群中的命令。

你可能注意到我们在 Swarm init 命令中指定了 --advertise-addr 参数。为什么在这里需要这个呢?原因是 PWD 生成的节点关联了多个 IP 地址。我们可以通过在节点上执行 ip 命令轻松验证这一点。这个命令会显示出确实存在两个端点,eth0eth1。因此,我们必须明确指定给新的 Swarm 管理节点使用哪个 IP 地址。在我们的例子中,是 eth0

在 PWD 中创建四个额外的节点,通过点击 node2node3node4node5 四次,它们将会在左侧列出。如果你点击左侧的某个节点,右侧将显示该节点的详细信息以及一个终端窗口。

选择每个节点(2 到 5),并在相应的终端中执行你从主节点(node1)复制过来的 docker swarm join 命令:

$ docker swarm join --token SWMTKN-1-4o1ybxxg7cv... 192.168.0.13:2377

这个节点作为工作节点加入了 Swarm。

一旦你将所有四个节点加入到 Swarm,切换回 node1 并列出所有节点:

$ docker node ls

这,毫不意外地,生成了如下输出(为了可读性稍作重新格式化):

ID           HOSTNAME STATUS  AVAIL. MANAGER ST. ENGINE VER.Nb16ey2p... *  node1   Ready  Active   Leader     20.10.17
Kdd0yv15...    node2   Ready  Active              20.10.17
t5iw0clx...    node3   Ready  Active              20.10.17
Nr6ngsgs...    node4   Ready  Active              20.10.17
thbiwgft...    node5   Ready  Active              20.10.17

仍然在 node1 上,我们现在可以提升,比如将 node2node3 提升为 Swarm 管理节点,以实现高度可用:

$ docker node promote node2 node3

这将生成以下输出:

Node node2 promoted to a manager in the swarm.Node node3 promoted to a manager in the swarm.

有了这个,我们在 PWD 上的 Swarm 就可以接受工作负载了。我们创建了一个高度可用的 Docker Swarm,包含三个管理节点,它们组成一个 Raft 共识组,以及两个工作节点。

在云中创建 Docker Swarm

到目前为止,我们创建的所有 Docker Swarm 都非常适合用于开发、实验或演示目的。不过,如果我们想创建一个可以作为生产环境的 Swarm,用来运行我们至关重要的应用程序,那么我们需要在云端或本地创建一个——我敢说——真正的 Swarm。在本书中,我们将演示如何在 AWS 中创建一个 Docker Swarm。

我们可以通过 AWS 控制台手动创建一个 Swarm:

  1. 登录到你的 AWS 账户。如果你还没有账户,可以创建一个免费的。

  2. 首先,我们创建一个 AWS aws-docker-demo-sg

    1. 导航到你的默认 VPC。

    2. 在左侧,选择 aws-docker-demo-sg,如前所述,并添加描述,例如 用于我们的 Docker 演示的 SG

    3. 现在,点击 sg-030d0...

    4. 类型:自定义 UDP,协议:UDP,端口范围:7946,来源:自定义

    5. 在值的选项中,选择刚刚创建的 SG。

    6. 类型:自定义 TCP,协议:TCP,端口范围:7946,来源:自定义

    7. 在值的选项中,选择刚刚创建的 SG。

    8. 类型:自定义 TCP,协议:TCP,端口范围:4789,来源:自定义

    9. 在值的选项中,选择刚刚创建的 SG。

    10. 类型:自定义 TCP,协议:TCP,端口范围:22,来源:我的 IP

    11. 这条规则是为了能够通过 SSH 从你的主机访问实例。

Docker Swarm 端口

TCP 端口 2377:这是 Swarm 模式的主要通信端口。Swarm 管理和编排命令通过此端口进行通信。它用于节点之间的通信,并在 Raft 共识算法中扮演着至关重要的角色,确保 Swarm 中的所有节点作为一个单一系统进行操作。

TCP 和 UDP 端口 7946:此端口用于节点之间的通信(容器网络发现)。它帮助 Swarm 中的节点交换有关在每个节点上运行的服务和任务的信息。

UDP 端口 4789:此端口用于覆盖网络流量。当你为服务创建覆盖网络时,Docker Swarm 使用此端口进行容器之间的数据流量传输。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.12 – AWS SG 的入站规则

  1. 完成后,点击 保存规则

  2. 进入 EC2 控制面板。

  3. 首先,我们为接下来要创建的所有 EC2 实例创建一个密钥对:

    1. 定位并点击 aws-docker-demo

    2. 确保私钥文件的格式为 .pem

    3. 点击 .pem 文件并保存在安全位置。

  4. 回到 EC2 控制面板,使用以下设置启动一个新的 EC2 实例:

    1. 将实例命名为 manager1

    2. 选择 t2.micro 作为实例类型。

    3. 使用我们之前创建的密钥对,名为 aws-docker-demo

    4. 选择我们之前创建的现有 SG,aws-docker-demo-sg

    5. 然后,点击 启动 按钮。

  5. 重复前一步骤,创建两个工作节点,分别命名为 worker1worker2

  6. 进入 EC2 实例列表。你可能需要等待几分钟,直到它们都准备好。

  7. manager1 实例开始,选择它并点击 ssh。仔细按照这些指令操作。

  8. 一旦连接到 manager1 实例,让我们安装 Docker:

    $ sudo apt-get update && sudo apt -y install docker.io
    

这可能需要几分钟时间才能完成。

  1. 现在,确保你可以在不使用 sudo 命令的情况下使用 Docker:

    $ sudo usermod -aG docker $USER
    
  2. 为了应用前面的命令,你需要快速退出 AWS 实例:

    $ exit
    

然后,立即使用 步骤 6 中的 ssh 命令重新连接。

  1. 回到 EC2 实例,确保你可以通过以下命令访问 Docker:

    $ docker version
    

如果一切安装和配置正确,您应该会看到 Docker 客户端和引擎的版本信息。

  1. 现在对另外两个 EC2 实例worker1worker2重复步骤 6步骤 10

  2. 现在回到您的manager1实例并初始化 Docker Swarm:

    $ docker swarm init
    

输出应该与您在本地或 PWD 上创建 Swarm 时看到的情况相同。

  1. 从前面的输出中复制docker swarm join命令。

  2. 转到每个工作节点并运行该命令。节点应该会返回以下内容:

    This node joined a swarm as a worker.
    
  3. 返回manager1节点并运行以下命令以列出 Swarm 的所有节点:

    $ docker node ls
    

您应该看到的内容类似于此:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.13 – AWS 上的 Swarm 节点列表

现在我们在(AWS)云中有了一个 Docker Swarm,让我们向其中部署一个简单的应用程序。

部署第一个应用程序

我们已经在各种平台上创建了一些 Docker Swarm。一旦创建,Swarm 在任何平台上的行为都是相同的。我们在 Swarm 上部署和更新应用程序的方式与平台无关。避免在使用 Swarm 时发生供应商锁定,一直是 Docker 的主要目标之一。Swarm 就绪的应用程序可以毫不费力地从本地运行的 Swarm 迁移到基于云的 Swarm。例如,技术上完全可以将 Swarm 的一部分运行在本地,另一部分运行在云端。当然,这样做时,我们必须考虑到地理上远距离节点之间可能带来的较高延迟。

现在我们已经有了一个高可用的 Docker Swarm,接下来是时候在上面运行一些工作负载了。我正在使用刚刚在 AWS 上创建的 Swarm。我们将首先通过创建一个服务来开始。为此,我们需要通过 SSH 连接到一个管理节点。我选择了manager1实例上的 Swarm 节点:

$ ssh -i "aws-docker-demo.pem" <public-dns-name-of-manager1>

我们通过创建一个服务来启动第一个应用程序的部署。

创建服务

服务可以作为堆栈的一部分或直接使用 Docker CLI 创建。让我们先来看一个定义单个服务的示例堆栈文件:

  1. 使用 Vi 编辑器创建一个名为stack.yml的新文件,并添加以下内容:

    version: "3.7"services:  whoami:    image: training/whoami:latest    networks:    - test-net    ports:    - 81:8000    deploy:      replicas: 6      update_config:        parallelism: 2        delay: 10s      labels:         app: sample-app         environment: prod-southnetworks:  test-net:    driver: overlay
    
  2. 通过先按Esc键,然后输入:wq,再按Enter键退出 Vi 编辑器。这将保存代码片段并退出 vi。

注意

如果您不熟悉 Vi 编辑器,也可以使用 nano 编辑器。

在前面的示例中,我们可以看到名为whoami的服务的期望状态:

  • 它基于training/whoami:latest镜像

  • 服务的容器连接到test-net网络

  • 容器端口8000已发布到端口81

  • 它运行了六个副本(或任务)

  • 在滚动更新期间,单个任务按批次更新,每批次包含两个任务,并且每个成功批次之间有 10 秒的延迟。

  • 服务(及其任务和容器)被分配了两个标签,appenvironment,值分别为sample-appprod-south

还有许多其他设置可以为服务定义,但上述设置是一些更为重要的设置。大多数设置都有有意义的默认值。例如,如果我们未指定副本数量,则 Docker 默认为 1。服务的名称和镜像当然是必需的。请注意,服务的名称在 Swarm 中必须是唯一的。

  1. 要创建上述服务,我们使用 docker stack deploy 命令。假设包含上述内容的文件名为 stack.yaml,我们有以下内容:

    $ docker stack deploy -c stack.yaml sample-stack
    

这里,我们创建了一个名为 sample-stack 的堆栈,包含一个服务,whoami

  1. 我们可以列出在我们的 Swarm 中所有的堆栈:

    $ docker stack ls
    

完成后,我们应该会看到如下内容:

NAME                       SERVICESsample-stack            1
  1. 我们可以列出在 Swarm 中定义的服务,如下所示:

    $ docker service ls
    

我们会得到如下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.14 – 列出在 Swarm 中运行的所有服务

在输出中,我们可以看到目前只有一个服务在运行,这是预期的结果。该服务有一个 ID。与您之前用于容器、网络或卷的 ID 格式不同,该 ID 是字母数字组合(而之前的 ID 格式总是 SHA-256)。我们还可以看到,服务名称是我们在堆栈文件中定义的服务名称与堆栈名称的组合,堆栈名称作为前缀使用。这是合理的,因为我们希望能够通过相同的堆栈文件将多个(不同名称的)堆栈部署到我们的 Swarm 中。为了确保服务名称唯一,Docker 决定将服务名称和堆栈名称组合起来。

在第三列中,我们看到模式是“复制模式”。副本的数量显示为 6/6。这告诉我们,六个副本中有六个正在运行。这对应于期望的状态。在输出中,我们还可以看到服务使用的镜像和服务的端口映射。

检查服务及其任务

在上述输出中,我们看不到已创建的六个副本的详细信息。

为了深入了解这一点,我们可以使用 docker service ps <service-id> 命令。如果我们对我们的服务执行此命令,将获得如下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.15 – whoami 服务的详细信息

在上面的输出中,我们可以看到六个任务的列表,这些任务对应我们请求的六个 whoami 服务副本。在 NODE 列中,我们还可以看到每个任务被部署到的节点。每个任务的名称是服务名称加上递增的索引。此外,注意到类似于服务本身,每个任务也会分配一个字母数字组合的 ID。

就我而言,显然任务 3 和 6,名称分别是 sample-stack_whoami.3sample-stack_whoami.6,已经部署到 ip-172-31-32-21,这是我们 Swarm 的领导节点。因此,我应该会在此节点上找到一个正在运行的容器。让我们看看如果列出 ip-172-31-32-21 上的所有容器会得到什么:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.16 – 节点 ip-172-31-32-21 上的容器列表

正如预期的那样,我们发现一个容器正在运行,来自 training/whoami:latest 镜像,容器名称是其父任务名称和 ID 的组合。我们可以尝试可视化在部署我们的示例堆栈时生成的所有对象层次结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.17 – Docker Swarm 堆栈的对象层次结构

一个堆栈可以由一个或多个服务组成。每个服务都有一组任务。每个任务与一个容器一一对应。堆栈和服务是在 Swarm 管理节点上创建和存储的。任务随后被调度到 Swarm 工作节点,在工作节点上创建相应的容器。我们还可以通过检查服务来获取更多关于服务的信息。执行以下命令:

$ docker service inspect sample-stack_whoami

这提供了关于服务所有相关设置的丰富信息。这些包括我们在 stack.yaml 文件中明确定义的设置,但也包括我们没有指定的设置,因此它们被分配了默认值。我们不会在此列出完整的输出,因为它太长,但我鼓励你在自己的机器上检查。我们将在 第十五章Swarm 路由网格 部分中详细讨论部分信息。

测试负载均衡

为了查看 Swarm 如何将传入请求负载均衡到我们的示例 whoami 应用程序,我们可以使用 curl 工具。多次执行以下命令并观察答案的变化:

$ for i in {1..7}; do curl localhost:81; done

这会产生如下输出:

I'm ae8a50b5b058I'm 1b6b507d900c
I'm 83864fb80809
I'm 161176f937cf
I'm adf340def231
I'm e0911d17425c
I'm ae8a50b5b058

请注意,在第六项之后,序列开始重复。这是因为 Docker Swarm 使用轮询算法来进行负载均衡。

服务日志

在之前的章节中,我们处理过容器生成的日志。在这里,我们专注于服务。记住,最终,一个具有多个副本的服务会运行多个容器。因此,我们可以预计,如果我们请求该服务的日志,Docker 会返回该服务所有容器日志的汇总。事实上,当我们使用 docker service logs 命令时,我们会看到这一点:

$ docker service logs sample-stack_whoami

这是我们得到的结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.18 – whoami 服务的日志

此时日志中没有太多信息,但足以讨论我们得到的内容。日志中每一行的第一部分总是包含容器的名称,并与日志条目来源的节点名称结合。然后,通过竖线(Listening on :8000)分隔。

使用docker service logs命令获取的聚合日志没有按特定方式排序。因此,如果事件的关联发生在不同的容器中,你应该在日志输出中添加能使这种关联成为可能的信息。

通常,这个时间戳是每个日志条目的时间标记。但是这必须在源头进行;例如,生成日志条目的应用程序也需要确保添加时间戳。

我们还可以通过提供任务 ID 来查询服务中单个任务的日志,而不是服务 ID 或名称。所以,假设我们通过以下方式查询任务 6 的日志:

$ docker service logs w90b8

这给我们以下输出:

sample-stack_whoami.6.w90b8xmkdw53@ip-172-31-32-21    | Listening on :8000

在下一节中,我们将研究 Swarm 如何重新协调所需状态。

重新协调所需状态

我们已经了解到,Swarm 服务是我们希望应用程序或应用服务运行在的所需状态的描述或清单。现在,让我们看看 Docker Swarm 如何在我们做出一些操作导致服务的实际状态与所需状态不同时,重新协调这个所需状态。最简单的方式就是强制终止服务的一个任务或容器。

让我们使用已调度到node-1的容器来执行这个操作:

$ docker container rm -f sample-stack_whoami.3\. nqxqs...

如果我们这么做,然后立刻运行docker service ps,我们将看到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.19 – Docker Swarm 在一个任务失败后重新协调所需状态

我们看到任务 2 因退出代码137失败,并且 Swarm 立即通过将失败的任务重新调度到具有空闲资源的节点上,协调了所需状态。在这种情况下,调度器选择了与失败任务相同的节点,但这并不总是如此。所以,在没有干预的情况下,Swarm 完全修复了问题,并且由于服务以多个副本运行,服务在任何时候都没有停机。

让我们尝试另一个失败场景。这一次,我们将关闭整个节点,看看 Swarm 如何反应。我们以节点ip-172-31-47-124为例,因为它上面运行了两个任务(任务 1 和任务 4)。为此,我们可以前往 AWS 控制台,在 EC2 仪表板中,停止名为ip-172-31-47-124的实例。

注意,我必须进入每个工作节点的详细信息,以找出哪个节点的主机名是ip-172-31-47-124;在我的案例中,它是worker2

回到主节点,我们现在可以再次运行docker service ps来查看发生了什么:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.20 – Swarm 重新调度所有失败节点的任务

在前面的截图中,我们可以看到,任务 1 已立即在节点ip-172-31-32-189上重新调度,而任务 4 则在节点ip-172-31-32-21上重新调度。即使是这种更为严重的故障,Docker Swarm 也能优雅地处理。

需要注意的是,如果节点ip-172-31-47-124在 Swarm 中重新上线,之前在其上运行的任务不会自动转移回该节点。

但是现在该节点已经准备好接受新的工作负载。

删除服务或堆栈

如果我们想从 Swarm 中删除一个特定的服务,可以使用docker service rm命令。另一方面,如果我们想从 Swarm 中删除一个堆栈,我们可以类比地使用docker stack rm命令。该命令会删除堆栈定义中的所有服务。以whoami服务为例,它是通过使用堆栈文件创建的,因此我们将使用后者命令:

$ docker stack rm sample-stack

这给出了以下输出:

Removing service sample-stack_whoamiRemoving network sample-stack_test-net

前面的命令将确保每个服务的所有任务都被终止,并且相应的容器在首先发送SIGTERM信号后被停止,如果失败,则在 10 秒超时后发送SIGKILL信号。

需要注意的是,停止的容器不会从 Docker 主机上删除。

因此,建议定期清理工作节点上的容器,以回收未使用的资源。可以使用docker container purge -f命令来实现这一目的。

问题:为什么在工作节点上保留停止或崩溃的容器而不自动删除它们是合理的?

部署多服务堆栈

第十一章,《使用 Docker Compose 管理容器》中,我们使用了一个由两个服务组成的应用程序,这些服务在 Docker Compose 文件中以声明方式描述。我们可以使用这个 Compose 文件作为模板,创建一个堆栈文件,使我们能够将相同的应用程序部署到 Swarm 中:

  1. 创建一个名为pets-stack.yml的新文件,并将以下内容添加到其中:

    version: "3.7"services:  web:    image: fundamentalsofdocker/ch11-web:2.0    networks:    - pets-net    ports:    - 3000:3000    deploy:      replicas: 3  db:    image: fundamentalsofdocker/ch11-db:2.0    networks:    - pets-net    volumes:    - pets-data:/var/lib/postgresql/datavolumes:  pets-data:networks:  pets-net:    driver: overlay
    

我们请求将 Web 服务配置为具有三个副本,并且两个服务都连接到覆盖网络pets-net

  1. 我们可以使用docker stackdeploy命令来部署此应用程序:

    $ docker stack deploy -c pets-stack.yml pets
    

这将产生以下输出:

Creating network pets_pets-netCreating service pets_db
Creating service pets_web

Docker 创建了pets_pets-net覆盖网络,然后创建了两个服务,pets_webpets_db

  1. 然后,我们可以列出pets堆栈中的所有任务:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.21 – 宠物堆栈中的所有任务列表

  1. 最后,让我们使用 curl 测试该应用程序,以获取一个包含宠物的 HTML 页面。确实,应用程序按预期工作,返回了期望的页面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.22 – 使用 curl 测试宠物应用程序

容器 ID 会出现在输出中,类似于 Delivered to you by container d01e2f1f87df。如果你多次运行 curl 命令,ID 应该在三个不同的值之间循环。这些是我们为 Web 服务请求的三个容器(或副本)的 ID。

  1. 一旦我们完成操作,就可以使用 docker stack rm pets 命令移除该 stack。

一旦我们在 AWS 上完成了 Swarm 的操作,就可以将其移除。

移除 AWS 中的 Swarm

为了清理 AWS 云中的 Swarm 并避免产生不必要的费用,我们可以使用以下命令:

$ for NODE in `seq 1 5`; do    docker-machine rm -f aws-node-${NODE}
done

接下来,让我们总结一下本章的内容。

总结

在本章中,我们介绍了 Docker Swarm,它是仅次于 Kubernetes 的第二大容器编排工具。我们研究了 Swarm 的架构,讨论了 Swarm 中运行的各种资源类型,如服务、任务等,并在 Swarm 中创建了服务。我们学习了如何在本地、在名为 PWD 的特殊环境中以及在云中创建 Docker Swarm。最后,我们部署了一个由多个与 Docker Swarm 相关的服务组成的应用程序。

在下一章中,我们将介绍路由网格,它提供了 Docker Swarm 中的第 4 层路由和负载均衡。之后,我们将演示如何将由多个服务组成的第一个应用程序部署到 Swarm 中。我们还将学习如何在更新 Swarm 中的应用程序时实现零停机时间,最后,我们将了解如何在 Swarm 中存储配置数据,以及如何使用 Docker secrets 保护敏感数据。敬请关注。

问题

为了评估你的学习进度,请尝试回答以下问题:

  1. 什么是 Docker Swarm?

  2. Docker Swarm 的主要组件有哪些?

  3. 如何初始化 Docker Swarm?

  4. 如何向 Docker Swarm 中添加节点?

  5. 在 Docker Swarm 中,Docker 服务是什么?

  6. 如何在 Docker Swarm 中创建和更新服务?

  7. 什么是 Docker Stack,它与 Docker Swarm 有什么关系?

  8. 如何在 Docker Swarm 中部署 Docker Stack?

  9. Docker Swarm 中的网络选项有哪些?

  10. Docker Swarm 如何处理容器的扩展和故障容忍?

答案

以下是前面问题的示例答案:

  1. Docker Swarm 是一个内置于 Docker 引擎的原生容器编排工具,它允许你创建、管理和扩展一组 Docker 节点,负责协调多个主机上容器的部署、扩展和管理。

  2. Docker Swarm 由两个主要组件组成:管理节点,负责管理集群的状态、协调任务并保持服务的期望状态;以及工作节点,执行任务并运行容器实例。

  3. 你可以通过在 Docker 主机上运行 docker swarm init 命令来初始化 Docker Swarm,该主机将成为 Swarm 的第一个管理节点。该命令会提供一个令牌,可以用来将其他节点加入到 Swarm 中。

  4. 要向 Docker Swarm 添加节点,请在新节点上使用docker swarm join命令,并提供令牌和现有管理节点的 IP 地址。

  5. Docker Service 是一个高级抽象,代表 Docker Swarm 中的容器化应用程序或微服务。它定义了应用程序的所需状态,包括容器镜像、副本数、网络和其他配置选项。

  6. 你可以使用docker service create命令创建一个新的服务,使用docker service update命令更新现有服务,并随后指定所需的配置选项。

  7. Docker Stack 是一组一起部署并共享依赖项的服务,这些依赖项在 Docker Compose 文件中定义。Docker Stack 可以在 Docker Swarm 中部署,以管理和编排多服务应用程序。

  8. 要在 Docker Swarm 中部署 Docker Stack,请使用docker stack deploy命令,后跟堆栈名称和 Docker Compose 文件的路径。

  9. Docker Swarm 支持多种网络选项,包括用于负载均衡和路由的默认入口网络、用于跨节点容器间通信的覆盖网络,以及用于特定用例的自定义网络。

  10. Docker Swarm 会通过调整副本数来自动管理容器的扩展,以符合在服务定义中指定的所需状态。它还会监控容器的健康状况,并替换任何失败的实例以保持容错性。

第十五章:15

在 Docker Swarm 上部署和运行分布式应用程序

在上一章中,我们详细介绍了 Docker 的原生调度器 SwarmKit。SwarmKit 是 Docker 引擎的一部分,一旦在系统中安装了 Docker,无需额外安装。我们了解了 SwarmKit 用于在集群中部署和运行分布式、弹性、稳健且高可用的应用程序的概念和对象,这些应用程序可以运行在本地或云端。我们还展示了 Docker 的调度器如何使用软件定义网络(SDN)保护应用程序。我们学习了如何在名为“Play with Docker”的特殊环境中以及在云端本地创建 Docker Swarm。最后,我们发现如何将一个由多个相关服务组成的应用程序部署到 Docker Swarm 上。

在本章中,我们将介绍路由网格,它提供第 4 层路由和负载均衡。接下来,我们将演示如何将一个由多个服务组成的第一个应用程序部署到 Swarm 中。我们还将学习如何在 Swarm 中更新应用程序时实现零停机,最后将介绍如何在 Swarm 中存储配置数据,以及如何使用 Docker 秘密保护敏感数据。

本章我们将讨论以下主题:

  • Swarm 路由网格

  • 零停机部署

  • 在 Swarm 中存储配置数据

  • 使用 Docker Secrets 保护敏感数据

完成本章后,您将能够做到以下几点:

  • 列出两到三种常见的部署策略,用于在不造成停机的情况下更新服务

  • 在不引起服务中断的情况下批量更新服务

  • 定义一个回滚策略,以防更新失败时用于恢复服务

  • 使用 Docker 配置存储非敏感配置数据

  • 使用 Docker 秘密与服务配合使用

  • 更新秘密的值而不引起停机

让我们开始吧!

Swarm 路由网格

如果你注意到的话,可能会在上一章中看到一些有趣的现象。我们已经部署了 pets 应用程序,并且该应用程序在三个节点——node-1node-2node-3 上安装了 Web 服务的实例。

然而,我们能够通过 localhost 访问 node-1 上的 Web 服务,并且从那里访问每个容器。这是如何实现的呢?这正是所谓的 Swarm 路由网格的功劳。路由网格确保,当我们发布服务的端口时,该端口会在 Swarm 的所有节点上发布。因此,任何访问 Swarm 节点并请求使用特定端口的网络流量,都将通过路由网格转发到其中一个服务容器。让我们看一下下面的图示,了解它是如何工作的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.1 – Docker Swarm 路由网格

在这种情况下,我们有三个节点,分别为172.10.0.15172.10.0.17172.10.0.33。在图的左下角,我们看到创建了一个包含两个副本的网页服务的命令。相应的任务已经被调度到Host BHost C上。task1 被安排在Host B,而task2 被安排在Host C

当在 Docker Swarm 中创建服务时,它会自动获得一个10.2.0.1

如果现在有一个来自外部的8080端口的请求,IP 表中会发现这个请求对应的是网页服务的 VIP。

现在,由于 VIP 并非真实目标,IPVS 服务将对与该服务关联的任务的 IP 地址进行负载均衡。在我们的案例中,它选择了10.2.0.3。最后,Ingress 网络(Overlay) 被用来将请求转发到Host C上的目标容器。

需要注意的是,外部负载均衡器将外部请求转发到哪个 Swarm 节点并不重要。路由网格始终会正确处理请求并将其转发到目标服务的某个任务。

我们已经学到了很多关于 Docker swarm 网络的知识。接下来我们要学习的主题是如何在不造成系统停机的情况下部署应用程序。

零停机部署

需要频繁更新的关键任务应用程序中,最重要的方面之一就是能够以不产生任何停机时间的方式进行更新。我们称之为零停机部署。更新中的应用程序必须始终保持完全可用。

常见的部署策略

实现这一目标有多种方法,其中一些方法如下:

  • 滚动更新

  • 蓝绿部署

  • 金丝雀发布

Docker Swarm 开箱即支持滚动更新。其他两种部署方式需要我们付出额外的努力才能实现。

滚动更新

在关键任务应用中,每个应用服务必须运行多个副本。根据负载,这个数量可以少至两到三个实例,也可以多至几十、几百甚至上千个实例。在任何给定时刻,我们希望所有运行中的服务实例中有明确的大多数。所以,如果我们有三个副本,我们希望至少有两个副本始终运行。如果我们有 100 个副本,我们可以接受最少 90 个副本可用。通过这种方式,我们可以定义在升级时可以停机的副本的批次大小。在第一个案例中,批次大小为 1,而在第二个案例中,批次大小为 10。

当我们停用副本时,Docker Swarm 会自动将这些实例从负载均衡池中移除,所有流量将会被负载均衡到剩余的活动实例上。因此,这些剩余的实例将会经历流量的轻微增加。在下面的图示中,在滚动更新开始之前,如果任务 A3想要访问服务 B,它可以通过 SwarmKit 将流量负载均衡到服务 B的任意一个三个任务中。一旦滚动更新开始,SwarmKit 就会停用任务 B1进行更新。

自动地,这个任务会被从目标池中移除。所以,如果任务 A3现在请求连接到服务 B,负载均衡将只从剩余的任务中选择,也就是任务 B2任务 B3。因此,这两个任务可能会暂时经历更高的负载:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.2 – 任务 B1 被停用以进行更新

停止的实例随后会被等量的新版本应用服务实例替代。一旦新实例启动并运行,我们可以让 Swarm 在给定时间内监控它们,确保它们是健康的。如果一切正常,那么我们可以继续通过停用下一批实例,并用新版本的实例替换它们。这个过程会一直重复,直到所有的应用服务实例都被替换。

在下面的图示中,我们可以看到服务 B任务 B1已经更新到版本 2。任务 B1的容器被分配了一个新的 IP 地址,并且它被部署到了一个具有空闲资源的另一个工作节点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.3 – 在滚动更新中,第一批任务正在更新

需要理解的是,当服务的任务被更新时,在大多数情况下,它会被部署到与原来不同的工作节点上,但只要相应的服务是无状态的,这应该是没问题的。如果我们有一个有状态的服务,它是位置或节点感知的,并且我们想要更新它,那么我们就必须调整方法,但这超出了本书的范围。

现在,让我们来看一下如何实际指示 Swarm 执行应用服务的滚动更新。当我们在stack文件中声明服务时,我们可以定义多个与此上下文相关的选项。让我们来看一个典型的stack文件片段:

version: "3.5"services:
  web:
    image: nginx:alpine
    deploy:
      replicas: 10
      update_config:
        parallelism: 2
        delay: 10s
...

在这个片段中,我们可以看到一个名为update_config的部分,其中包含parallelismdelay属性。parallelism定义了滚动更新时每次更新多少副本的批量大小。delay定义了 Docker Swarm 在更新单个批次之间将等待多长时间。在前面的例子中,我们有 10 个副本,每次更新 2 个实例,并且在每次成功更新后,Docker Swarm 会等待 10 秒。

让我们测试一下这样的滚动更新。导航到我们 sample-solutions 文件夹下的 ch14 子文件夹,使用 web-stack.yaml 文件创建一个已配置滚动更新的 web 服务。该服务使用基于 Alpine 的 Nginx 镜像,版本为 1.12-alpine。我们将把服务更新到更新版本,即 1.13-alpine

首先,我们将把这个服务部署到我们在 AWS 上创建的 Swarm 集群中。

让我们来看一下:

  1. 通过 SSH 登录到 AWS 上 Docker Swarm 的 master1 实例:

    $ ssh -i "aws-docker-demo.pem" <public-dns-of-manager1-instance>
    
  2. 使用 vinano 创建一个名为 web-stack.yml 的文件,并包含以下内容:

    version: "3.7"services:  whoami:    image: nginx:1.12-alpine    ports:    - 81:80    deploy:      replicas: 10      update_config:        parallelism: 2        delay: 10s
    
  3. 现在,我们可以使用 stack 文件来部署服务:

    $ docker stack deploy -c web-stack.yaml web
    

上述命令的输出如下所示:

Creating network web_defaultCreating service web_web
  1. 一旦服务部署完成,我们可以使用以下命令来监控它:

    $ watch docker stack ps web
    

我们将看到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.4 – 在 Swarm 中运行的 web 堆栈的 web 服务,包含 10 个副本

之前的命令将持续更新输出,并为我们提供滚动更新过程中发生的情况的概览。

  1. 现在,我们需要打开第二个终端并为我们 Swarm 的管理节点配置远程访问。完成后,我们就可以执行 docker 命令,更新 stack 的 web 服务镜像,也叫 web

    $ docker service update --image nginx:1.13-alpine web_web
    

上述命令会产生以下输出,表示滚动更新的进度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.5 – 显示滚动更新进度的屏幕

前面的输出表示前两批任务(每批有两个任务)已经成功,第三批即将准备好。

在第一个终端窗口中,我们正在查看 stack,现在我们应该能看到 Docker Swarm 如何每 10 秒更新一次服务,每批更新后看起来应该像以下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.6 – Docker Swarm 中服务的滚动更新

在前面的截图中,我们可以看到任务 2 和任务 10 的第一批已更新。Docker Swarm 正在等待 10 秒钟,以便继续进行下一批任务。

有趣的是,在这个特定案例中,SwarmKit 将新版本的任务部署到与之前版本相同的节点上。这是偶然的,因为我们有五个节点,每个节点上有两个任务。SwarmKit 总是尽力平衡节点之间的工作负载。

所以,当 SwarmKit 停止一个任务时,相应的节点的工作负载会比其他节点小,因此新的实例会被调度到该节点。通常,你无法期望在同一个节点上找到一个新的任务实例。你可以通过删除 stack(命令:docker stack rm web)并将副本数更改为七来尝试一下,然后重新部署并更新它。

一旦所有任务更新完成,我们的 docker stack ps web 命令的输出将类似于以下截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.7 – 所有任务已成功更新

请注意,SwarmKit 不会立即从相应节点中移除前版本任务的容器。这是有道理的,因为我们可能需要从这些容器中获取日志进行调试,或者可能需要使用 docker container inspect 获取它们的元数据。SwarmKit 会保留最近四个终止的任务实例,在清除较旧的实例之前,以避免系统被未使用的资源阻塞。

我们可以使用 --update-order 参数指示 Docker 在停止旧容器之前先启动新容器副本。这可以提高应用程序的可用性。有效值为 start-firststop-first

后者是默认设置。

一旦完成,我们可以使用以下命令拆除 stack

$ docker stack rm web

尽管使用 stack 文件来定义和部署应用程序是推荐的最佳实践,但我们也可以在 service create 语句中定义更新行为。如果我们只想部署一个单独的服务,这可能是更优的做法。让我们看看这样的 create 命令:

$ docker service create --name web \    --replicas 10 \
    --update-parallelism 2 \
    --update-delay 10s \
    nginx:alpine

这个命令定义了与前面的stack文件相同的期望状态。我们希望服务运行 10 个副本,并且希望滚动更新一次更新两项任务,并且每批任务之间有 10 秒的间隔。

健康检查

为了做出明智的决策,例如在 Swarm 服务的滚动更新过程中,判断刚安装的新服务实例批次是否正常运行,或者是否需要回滚,SwarmKit 需要一种方式来了解系统的整体健康状况。就其本身而言,SwarmKit(和 Docker)能够收集相当多的信息,但也有一定的限制。想象一下,一个包含应用程序的容器。从外部看,容器可能看起来非常健康并且运行良好,但这并不意味着容器内部运行的应用程序也同样运行良好。应用程序可能处于无限循环或损坏状态,但仍然在运行。然而,只要应用程序还在运行,容器就会继续运行,从外部看一切看起来都很完美。

因此,SwarmKit 提供了一个接口,我们可以通过它为 SwarmKit 提供一些帮助。我们,作为在 Swarm 中运行的容器应用服务的开发者,最清楚我们的服务是否处于健康状态。SwarmKit 让我们有机会定义一个命令来测试我们的应用服务的健康状态。这个命令具体执行什么操作对 Swarm 来说并不重要;它只需要返回 OKNOT OKtime out。后两种情况,即 NOT OK 或超时,会告诉 SwarmKit 正在调查的任务可能不健康。

这里,我故意用了“可能”这个词,稍后我们会看到为什么:

FROM alpine:3.6…
HEALTHCHECK --interval=30s \
    --timeout=10s
    --retries=3
    --start-period=60s
    CMD curl -f http://localhost:3000/health || exit 1
...

在前面的 Dockerfile 代码片段中,我们可以看到 HEALTHCHECK 关键字。它有几个选项或参数以及一个实际的命令,即 CMD。我们来讨论一下这些选项:

  • --interval:定义健康检查之间的等待时间。因此,在我们的案例中,调度器每 30 秒执行一次检查。

  • --timeout:此参数定义了如果健康检查未响应,Docker 应该等待多久才会超时并返回错误。在我们的示例中,设置为 10 秒。现在,如果某个健康检查失败,SwarmKit 会重试几次,直到放弃并将相应的任务标记为不健康,并允许 Docker 终止该任务并用新实例替换它。

  • 重试次数由 --retries 参数定义。在前面的代码中,我们希望进行三次重试。

  • 接下来是启动周期。某些容器启动需要一些时间(虽然这不是推荐的模式,但有时是不可避免的)。在此启动时间内,服务实例可能无法响应健康检查。通过启动周期,我们可以定义 SwarmKit 在执行第一次健康检查之前应等待多长时间,从而为应用程序初始化提供时间。为了定义启动时间,我们使用 --start-period 参数。在我们的例子中,我们在 60 秒后进行第一次检查。启动周期需要多长时间取决于应用程序及其启动行为。建议从一个较小的值开始,如果出现大量假阳性并且任务被多次重启,您可能希望增加时间间隔。

  • 最后,我们在最后一行使用 CMD 关键字定义实际的探测命令。在我们的例子中,我们定义了一个向 localhost 上的 /health 端点(端口为 3000)发送请求的探测命令。这个调用预计会有三种可能的结果:

    • 命令成功

    • 命令失败

    • 命令超时

后两个任务会被 SwarmKit 以相同的方式处理。这是调度器在告诉我们相应的任务可能处于不健康状态。我故意用了可能这个词,因为 SwarmKit 并不会立刻假设最坏的情况,而是认为这可能只是任务的暂时异常,任务会从中恢复。这也是我们需要--retries参数的原因。在这里,我们可以定义 SwarmKit 在假设任务确实不健康之前应尝试多少次,然后它会终止该任务并重新调度一个新实例到另一个空闲节点,以便恢复服务的期望状态。

为什么我们可以在探测命令中使用localhost?这是一个非常好的问题,原因是 SwarmKit 在探测一个运行在 Swarm 中的容器时,会在容器内部执行该探测命令(也就是说,它会执行类似docker container exec <containerID> <probing command>的操作)。因此,命令会在与容器内部应用程序相同的网络命名空间中执行。在下面的示意图中,我们可以看到一个服务任务从开始到结束的生命周期:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.8 – 服务任务与暂时性健康失败

首先,SwarmKit 会等到启动期结束后才开始探测。然后,我们进行第一次健康检查。不久之后,任务在探测时失败。它连续两次失败,但随后恢复。因此,健康检查 4是成功的,SwarmKit 保持任务运行。

在这里,我们可以看到一个任务永久失败的情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.9 – 任务的永久失败

我们刚刚学习了如何在其镜像的 Dockerfile 中定义服务的健康检查,但这并不是我们能做到的唯一方式。我们还可以在我们用来将应用程序部署到 Docker Swarm 中的stack文件中定义健康检查。以下是一个stack文件的简短示例:

version: "3.8"services:
  web:
    image: example/web:1.0
    healthcheck:
      test: ["CMD", "curl", "-f", http://localhost:3000/health]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
...

在前面的代码片段中,我们可以看到如何在stack文件中定义与健康检查相关的信息。首先,要明确的一点是,我们需要为每个服务单独定义健康检查。在应用程序层面或全局层面并没有健康检查。

与我们之前在 Dockerfile 中定义的相似,SwarmKit 用来执行健康检查的命令是curl -f http://localhost:3000/health。我们还定义了intervaltimeoutretriesstart_period,这四个键值对与我们在 Dockerfile 中使用的对应参数意义相同。如果在镜像中定义了与健康检查相关的设置,那么在stack文件中定义的设置将覆盖 Dockerfile 中的设置。

现在,让我们尝试使用一个已定义健康检查的服务:

  1. 使用vinano创建一个名为stack-health.yml的文件,内容如下:

    version: "3.8"services:  web:    image: nginx:alpine    deploy:      replicas: 3    healthcheck:      test: ["CMD", "wget", "-qO", "-", "http://localhost"]      interval: 5s      timeout: 2s      retries: 3      start_period: 15s
    
  2. 让我们部署这个:

    $ docker stack deploy -c stack-health.yml myapp
    
  3. 我们可以使用docker stack ps myapp查看每个集群节点上单个任务的部署情况。因此,在任何特定节点上,我们可以列出所有容器,找到我们堆栈中的一个。在我的示例中,任务 3 被部署到了节点ip-172-31-32-21,该节点恰好是主节点。

  4. 现在,列出该节点上的容器:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.10 – 显示正在运行的任务实例的健康状态

这个截图中有趣的地方是**状态(STATUS)**列。Docker,或者更准确地说,SwarmKit,已经识别到该服务定义了健康检查功能,并正在使用它来确定服务中每个任务的健康状况。

接下来,让我们看看如果发生问题会怎么样。

回滚

有时,事情并不会按预期进行。一个临时修复可能无意中引入了一个新 bug,或者新版本可能显著降低了组件的吞吐量,等等。在这种情况下,我们需要有一个计划 B,这通常意味着能够将更新回滚到先前的良好版本。

与更新一样,回滚必须发生,以确保应用程序没有中断;它需要实现零停机时间。从这个意义上说,回滚可以看作是一个反向更新。我们正在安装一个新版本,但这个新版本实际上是之前的版本。

与更新行为一样,我们可以在stack文件中或者在 Docker service create命令中声明,系统在需要执行回滚时应该如何行为。在这里,我们使用了之前的stack文件,不过这次添加了一些与回滚相关的属性:

version: "3.8"services:
  web:
    image: nginx:1.12-alpine
    ports:
    - 80:80
    deploy:
      replicas: 10
    update_config:
      parallelism: 2
      delay: 10s
      failure_action: rollback
      monitor: 10s
    healthcheck:
      test: ["CMD", "wget", "-qO", "-", http://localhost]
      interval: 2s
      timeout: 2s
      retries: 3
      start_period: 2s

我们可以创建一个名为stack-rollback.yaml的堆栈文件,并将前面的内容添加到其中。在这个内容中,我们定义了滚动更新的细节、健康检查和回滚时的行为。健康检查被定义为在初始等待时间 2 秒后,调度器开始每 2 秒轮询一次http://localhost上的服务,并在认为任务不健康之前重试 3 次。

如果我们算一下时间,那么至少需要 8 秒钟,如果任务因为 bug 而不健康,它才会被停止。所以,在deploy下,我们有一个新的条目叫做monitor。该条目定义了新部署的任务应该监控健康状况多长时间,以及是否继续进行滚动更新的下一个批次。在这个示例中,我们设定了 10 秒钟。这比我们计算出来的发现一个缺陷服务已被部署需要的 8 秒钟稍多一些,所以这是合适的。

我们还定义了一个新的条目,failure_action,它定义了在滚动更新过程中遇到故障(例如服务不健康)时编排器将采取的措施。默认情况下,该操作是停止整个更新过程,并将系统置于中间状态。系统并不会完全宕机,因为这是滚动更新,至少一些健康的实例仍然在运行,但运维工程师会更适合检查并修复问题。

在我们的案例中,我们已将该操作定义为回滚。因此,如果发生故障,SwarmKit 将自动将所有已更新的任务恢复到先前的版本。

蓝绿部署

第九章学习分布式应用架构中,我们以抽象的方式讨论了蓝绿部署是什么。结果发现,在 Docker Swarm 中,我们无法真正为任意服务实现蓝绿部署。Docker Swarm 中两项服务之间的服务发现和负载均衡是 Swarm 路由网格的一部分,无法(轻松)自定义。

如果服务 A想调用服务 B,Docker 会隐式地完成这一操作。Docker 会根据目标服务的名称,使用 Docker DNS 服务将该名称解析为 VIP 地址。当请求定向到 VIP 时,Linux IPVS 服务将再次在 Linux 内核 IP 表中查找 VIP,并将请求负载均衡到 VIP 所表示的服务任务的物理 IP 地址之一,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.11 – Docker Swarm 中服务发现和负载均衡是如何工作的

不幸的是,没有简单的方法来拦截这个机制并用自定义行为替换它,但这是实现服务 B的真正蓝绿部署所需要的,就如我们在第十七章中看到的那样,使用 Kubernetes 部署、更新和保护应用程序,Kubernetes 在这方面更为灵活。

话虽如此,我们始终可以以蓝绿方式部署面向公众的服务。我们可以使用interlock 2产品及其 7 层路由机制来实现真正的蓝绿部署。

金丝雀发布

从技术上讲,滚动更新是一种金丝雀发布(canary release),但由于它们缺乏插入自定义逻辑的接缝,滚动更新仅仅是金丝雀发布的一个非常有限的版本。

真正的金丝雀发布要求我们对更新过程有更细粒度的控制。此外,真正的金丝雀发布在 100%的流量都已经通过新版本时,才会停止使用旧版本的服务。从这个角度来看,它们像蓝绿部署(blue-green deployments)一样被处理。

在金丝雀发布场景中,我们不仅希望使用健康检查等因素作为决定是否将更多流量导向新版本服务的决定因素;我们还希望在决策过程中考虑外部输入,如通过日志聚合器收集和汇总的度量数据或追踪信息。作为决策依据的一个例子是是否符合服务水平协议SLA),即新版本的服务响应时间是否超出了容忍带。如果我们向现有服务添加新功能,但这些新功能降低了响应时间,就可能发生这种情况。

现在我们知道如何实现零停机时间部署应用程序,接下来我们想讨论如何在 Swarm 中存储应用程序使用的配置数据。

在 Swarm 中存储配置数据

如果我们想要在 Docker Swarm 中存储非敏感数据,比如配置文件,那么我们可以使用 Docker 配置。Docker 配置与 Docker 密钥非常相似,后者我们将在下一节讨论。主要的区别是配置值在静态时不会加密,而密钥会。像 Docker 密钥一样,Docker 配置只能在 Docker Swarm 中使用——也就是说,它们不能在非 Swarm 开发环境中使用。Docker 配置会直接挂载到容器的文件系统中。配置值可以是字符串或二进制值,最大支持 500 KB 大小。

使用 Docker 配置,您可以将配置与 Docker 镜像和容器分离。这样,您的服务就可以轻松地使用特定环境的值进行配置。生产环境的 Swarm 配置与临时环境的 Swarm 配置不同,临时环境的配置与开发或集成环境的配置也不同。

我们可以将配置添加到服务中,也可以从正在运行的服务中移除它们。配置甚至可以在 Swarm 中运行的不同服务之间共享。

现在,让我们创建一些 Docker 配置:

  1. 首先,我们从一个简单的字符串值开始:

    $ echo "Hello world" | docker config create hello-config –
    

请注意 docker config create 命令末尾的连字符。这意味着 Docker 期望从标准输入获取配置的值。这正是我们通过将 Hello world 值通过管道传递给 create 命令所做的。

上述命令的输出结果如下所示:

941xbaen80tdycup0wm01nspr

上述命令创建了一个名为 hello-config 的配置,值为“Hello world”。该命令的输出是此新配置在 Swarm 中存储的唯一 ID。

  1. 让我们看看结果,并使用 list 命令查看:

    $ docker config ls
    

这将输出以下内容(已被缩短):

ID       NAME        CREATED            UPDATEDrrin36..  hello-config  About a minute ago   About a minute ago

list 命令的输出将显示我们刚创建的配置的 IDNAME 信息,以及其 CREATED 和(最后)更新时间。然而,配置是非机密的。

  1. 因此,我们可以做更多的操作,甚至输出配置的内容,像这样:

    $ docker config inspect hello-config
    

输出看起来像这样:

[    {
        "ID": "941xbaen80tdycup0wm01nspr",
        "Version": {
            "Index": 557
        },
        "CreatedAt": "2023-05-01T15:58:15.873515031Z",
        "UpdatedAt": "2023-05-01T15:58:15.873515031Z",
        "Spec": {
            "Name": "hello-config",
            "Labels": {},
            "Data": "SGVsbG8gd29ybGQK"
        }
    }
]

嗯,有趣。在上述 JSON 格式的输出的Spec子节点中,我们有一个Data键,其值为SGVsbG8gd29ybGQK。我们刚刚说配置数据并没有在静止时加密?

  1. 结果证明,该值只是我们的字符串编码为base64,我们可以轻松验证:

    $ echo 'SGVsbG8gd29ybGQK' | base64 --decode
    

我们得到以下结果:

Hello world

到目前为止,一切都很顺利。

现在,让我们定义一个稍微复杂一些的 Docker 配置。假设我们正在开发一个 Java 应用程序。Java 传递配置数据到应用程序的首选方式是使用所谓的properties文件。properties文件只是一个包含键值对列表的文本文件。让我们看一下:

  1. 让我们创建一个名为my-app.properties的文件,并添加以下内容:

    username=pguserdatabase=productsport=5432dbhost=postgres.acme.com
    
  2. 保存文件并从中创建名为app.properties的 Docker 配置:

    $ docker config create app.properties ./my-app.properties
    

这给我们一个类似于这样的输出:

2yzl73cg4cwny95hyft7fj80u
  1. 为准备下一个命令,首先安装jq工具:

    $ sudo apt install –y jq
    
  2. 现在,我们可以使用这个(有点牵强的)命令来获取我们刚刚创建的配置的明文值:

    $ docker config inspect app.properties | jq .[].Spec.Data | xargs echo | base64 --decode
    

我们得到了这个输出:

username=pguserdatabase=products
port=5432
dbhost=postgres.acme.com

这正是我们预期的。

  1. 现在,让我们创建一个使用上述配置的 Docker 服务。为了简单起见,我们将使用nginx镜像来实现:

    $ docker service create \     --name nginx \    --config source=app.properties,target=/etc/myapp/conf/app.properties,mode=0440 \    nginx:1.13-alpine
    

这导致类似以下的输出:

svf9vmsjdttq4tx0cuy83hpgfoverall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged

在上述的service create命令中,有趣的部分是包含--config参数的那一行。通过这一行,我们告诉 Docker 使用名为app.properties的配置,并将其挂载为一个文件到容器内的/etc/myapp/conf/app.properties。此外,我们希望该文件被赋予0440的权限模式,以赋予所有者(root)和组读取权限。

让我们看看我们得到了什么:

$ docker service ps nginxID   NAME     IMAGE              NODE            DESIRED STATE   CURRENT STATE           ERROR      PORTS
pvj  nginx.1  nginx:1.13-alpine  ip-172-31-32-21   Running  Running 2 minutes ago

在上述输出中,我们可以看到唯一一个服务实例正在节点ip-172-31-32-21上运行。在这个节点上,我现在可以列出容器以获取nginx实例的 ID:

$ docker container lsCONTAINER ID    IMAGE                COMMAND                 CREATED         STATUS               PORTS …
44417e1a70a1    nginx:1.13-alpine    "nginx -g 'daemon of…"   5 minutes ago   Up 5 minutes         80/tcp …

最后,我们可以exec进入该容器,并输出位于/etc/myapp/conf/app.properties文件中的值:

$ docker exec 44417 cat /etc/my-app/conf/app.properties

请注意,在上述命令中,44417代表容器哈希的第一部分。

然后这将给我们预期的值:

username=pguserdatabase=products
port=5432
dbhost=postgres.acme.com

这一点毫不奇怪;这正是我们预期的。

Docker 配置当然也可以从集群中移除,但前提是它们没有被使用。如果我们尝试移除之前正在使用的配置,而没有先停止并移除服务,我们将会得到以下输出:

$ docker config rm app.properties

哦不,这不起作用,我们可以从以下输出看到:

Error response from daemon: rpc error: code = InvalidArgument desc = config 'app.properties' is in use by the following service: nginx

我们得到一个错误消息,Docker 告诉我们该配置正在被我们称为 nginx 的服务使用。这种行为在使用 Docker 卷时我们已经习惯了。

因此,首先我们需要移除服务,然后才能移除配置:

$ docker service rm nginxnginx

现在应该可以工作了:

$ docker config rm app.propertiesapp.properties

再次强调,非常重要的一点是 Docker 配置不应该用于存储诸如密码、访问密钥或关键秘密等机密数据。

在下一节中,我们将讨论如何处理机密数据。

使用 Docker 秘密保护敏感数据

秘密用于以安全的方式处理机密数据。Swarm 的秘密在静止时和传输中都是安全的。也就是说,当在管理节点上创建一个新秘密时,并且它只能在管理节点上创建,其值会被加密并存储在 raft 一致性存储中。这就是为什么它在静止时是安全的原因。如果一个服务被分配了秘密,那么管理节点会从存储中读取该秘密,解密后将其转发给所有请求该秘密的 swarm 服务实例的容器。由于 Docker Swarm 中的节点间通信使用 tmpFS 将数据传递到容器内。默认情况下,秘密会挂载到容器中的 /run/secrets 目录,但你可以将其更改为任何自定义文件夹。

需要注意的是,秘密在 Windows 节点上不会被加密,因为 Windows 上没有类似 tmpfs 的概念。为了实现与 Linux 节点相同的安全级别,管理员应该加密相应 Windows 节点的磁盘。

创建秘密

首先,让我们看看如何实际创建一个秘密:

$ echo "sample secret value" | docker secret create sample-secret -

该命令创建了一个名为 sample-secret 的秘密,其值为 sample secret value。请注意,docker secret create 命令末尾有一个连字符。这意味着 Docker 期望从标准输入中获取秘密的值。这正是我们通过将 sample secret value 管道传递给 create 命令所做的。

或者,我们也可以使用文件作为秘密值的来源:

  1. 创建一个 secret-value.txt 文件,如下所示:

    $ echo "other secret" > secret-value.txt
    
  2. 使用以下命令从这个文件创建 Docker 秘密:

    $ docker secret create other-secret ./secret-value.txt
    

在这里,名为 other-secret 的秘密值是从名为 ./secret-value.txt 的文件中读取的。

  1. 一旦秘密被创建,就无法访问其值。例如,我们可以列出所有的秘密,得到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.12 – 所有秘密的列表

在这个列表中,我们只能看到秘密的 IDNAME 信息,以及一些其他元数据,但秘密的实际值是不可见的。

  1. 我们还可以使用 inspect 命令来查看一个秘密的更多信息,例如查看 other-secret

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15.13 – 检查 swarm 秘密

即使在这里,我们也无法得到秘密的值。这显然是故意的:秘密就是秘密,因此必须保持机密。如果我们愿意,我们可以为秘密分配标签,甚至可以使用不同的驱动程序来加密和解密秘密,如果我们不满意 Docker 默认提供的加密方法。

使用秘密

秘密由在 swarm 中运行的服务使用。通常,秘密在创建服务时分配给该服务。因此,如果我们想运行一个名为 web 的服务并为其分配一个秘密,比如 api-secret-key,语法如下所示:

$ docker service create --name web \    --secret api-secret-key \
    --publish 8000:8000 \
    training/whoami:latest

该命令基于fundamentalsofdocker/whoami:latest镜像创建一个名为web的服务,将容器端口8000发布到所有集群节点的8000端口,并分配给它名为api-secret-key的机密。

这只有在api-secret-key机密已在集群中定义的情况下才有效;否则,将生成以下错误信息:

secret not found: api-secret-key.

因此,让我们现在创建这个机密:

$ echo "my secret key" | docker secret create api-secret-key -

现在,如果我们重新运行service create命令,它将成功执行。

现在,我们可以使用docker service ps web查找唯一的服务实例部署在哪个节点上,然后exec进入这个容器。在我的情况下,实例已部署到ip-172-31-32-21节点,这恰好是我正在使用的manager1 EC2 实例。否则,我需要先 SSH 到另一个节点。

然后,我使用docker container ls列出该节点上的所有容器,找到属于我的服务的实例并复制其容器 ID。接下来,我们可以运行以下命令,确保机密确实在容器内以包含机密值的预期文件名可用:

$ docker exec -it <container ID> cat /run/secrets/api-secret-key

再次说明,在我的情况下,生成的输出如下所示:

my secret key

这显然是我们预期的结果。我们可以看到机密以明文形式显示。

如果由于某种原因,Docker 在容器内挂载机密的默认位置不符合你的要求,你可以定义一个自定义位置。在以下命令中,我们将机密挂载到/app/my-secrets

$ docker service create --name web \    --name web \
    -p 8000:8000 \
    --secret source=api-secret-key,target=/run/my-secrets/api-secret-key \
    fundamentalsofdocker/whoami:latest

在这个命令中,我们使用了扩展语法来定义一个包括目标文件夹的机密。

在开发环境中模拟机密

在开发过程中,我们通常在机器上没有本地集群。然而,机密仅在集群中有效。那么,我们该怎么办呢?幸运的是,这个答案非常简单。

由于机密被当作文件处理,我们可以轻松地将包含机密的卷挂载到容器的预期位置,默认情况下,该位置是/run/secrets

假设我们在本地工作站上有一个名为./dev-secrets的文件夹。对于每个机密,我们都有一个与机密名称相同的文件,文件内容是该机密的未加密值。例如,我们可以通过在工作站上执行以下命令,模拟一个名为demo-secret、值为demo secret value的机密:

$ echo "demo secret value" > ./dev-secrets/sample-secret

然后,我们可以创建一个挂载此文件夹的容器,如下所示:

$ docker container run -d --name whoami \    -p 8000:8000 \
    -v $(pwd)/dev-secrets:/run/secrets \
    fundamentalsofdocker/whoami:latest

容器内运行的进程将无法区分这些挂载的文件和来源于机密的文件。因此,例如,demo-secret作为文件/run/secrets/demo-secret出现在容器内,并具有预期的值demo secret value。让我们在接下来的步骤中更详细地了解这一点:

  1. 为了测试这一点,我们可以在前面的容器中exec一个 shell:

    $ docker container exec -it whoami /bin/bash
    
  2. 现在,我们可以导航到/run/secrets文件夹并显示demo-secret文件的内容:

    /# cd /run/secrets/# cat demo-secretdemo secret value
    

接下来,我们将讨论秘密和遗留应用程序。

秘密和遗留应用程序

有时,我们想要容器化一个我们无法轻易更改,或者不想更改的遗留应用程序。这个遗留应用程序可能希望一个秘密值作为环境变量可用。那么现在我们该怎么处理呢?Docker 将秘密呈现为文件,但该应用程序期望它们以环境变量的形式存在。

在这种情况下,定义一个在容器启动时运行的脚本(所谓的入口点或启动脚本)是非常有帮助的。这个脚本将从相应的文件中读取秘密值,并定义一个与文件同名的环境变量,将读取到的值分配给该变量。对于名为demo-secret的秘密,其值应作为名为DEMO_SECRET的环境变量可用,启动脚本中的必要代码片段可以如下所示:

export DEMO_SECRET=$(cat /run/secrets/demo-secret)

类似地,假设我们有一个遗留应用程序,期望秘密值作为条目存在于位于/app/bin文件夹中的 YAML 配置文件中,名为app.config,其相关部分如下所示:

…secrets:
demo-secret: "<<demo-secret-value>>"
other-secret: "<<other-secret-value>>"
yet-another-secret: "<<yet-another-secret-value>>"
…

现在,我们的初始化脚本需要从秘密文件中读取秘密值,并将配置文件中相应的占位符替换为秘密值。对于demo_secret,可以如下所示:

file=/app/bin/app.confdemo_secret=$(cat /run/secret/demo-secret)
sed -i "s/<<demo-secret-value>>/$demo_secret/g" "$file"

在前面的代码片段中,我们使用sed工具来替换占位符,将其替换为实际值。我们可以对配置文件中的另外两个秘密使用相同的技巧。

我们将所有初始化逻辑放入一个名为entrypoint.sh的文件中,使其可执行,并且例如将其添加到容器文件系统的根目录中。然后,我们在 Dockerfile 中将此文件定义为ENTRYPOINT,或者我们也可以在docker containerrun命令中覆盖镜像的现有ENTRYPOINT

让我们做个示例。假设我们有一个在容器中运行的遗留应用程序,该容器由fundamentalsofdocker/whoami:latest镜像定义,并且该应用程序需要在名为whoami.conf的文件中定义一个名为db_password的秘密。

让我们来看看这些步骤:

  1. 我们可以在本地机器上定义一个名为whoami.conf的文件,其中包含以下内容:

    database:    name: demo    db_password: "<<db_password_value>>"others:    val1=123    val2="hello world"
    

重要的是这个代码片段的第 3 行。它定义了启动脚本必须将秘密值放置的位置。

  1. 让我们向本地文件夹添加一个名为entrypoint.sh的文件,并包含以下内容:

    file=/app/whoami.confdb_pwd=$(cat /run/secret/db-password)sed -i "s/<<db_password_value>>/$db_pwd/g" "$file" /app/http
    

前面脚本的最后一行源自原始 Dockerfile 中使用的启动命令。

  1. 现在,改变该文件的权限,使其可执行:

    $ sudo chmod +x ./entrypoint.sh
    
  2. 现在,我们定义一个从fundamentalsofdocker/whoami:latest镜像继承的 Dockerfile。向当前文件夹添加一个名为Dockerfile的文件,其中包含以下内容:

    FROM fundamentalsofdocker/whoami:latestCOPY ./whoami.conf /app/COPY ./entrypoint.sh /CMD ["/entrypoint.sh"]
    
  3. 让我们从这个 Dockerfile 中构建镜像:

    $ docker image build -t secrets-demo:1.0 .
    
  4. 一旦镜像构建完成,我们可以从中运行服务,但在此之前,我们需要在 Swarm 中定义密钥:

    $ echo "passw0rD123" | docker secret create demo-secret -
    
  5. 现在,我们可以创建一个使用以下密钥的服务:

    $ docker service create --name demo \--secret demo-secret \secrets-demo:1.0
    

更新密钥

有时,我们需要更新正在运行的服务中的密钥,因为密钥可能会被泄露或被恶意人员(如黑客)窃取。在这种情况下,一旦我们的机密数据被泄露给不可信的实体,它就必须被视为不安全的,因此我们需要更改它。

更新密钥与任何其他更新一样,都需要实现零停机时间。Docker SwarmKit 在这方面提供了支持。

首先,我们在 swarm 中创建一个新的密钥。建议在创建时使用版本控制策略。在我们的示例中,我们将版本作为密钥名称的后缀。我们最初使用名为 db-password 的密钥,现在这个密钥的新版本叫做 db-password-v2

$ echo "newPassw0rD" | docker secret create db-password-v2 -

假设原始服务使用密钥时是这样创建的:

$ docker service create --name web \    --publish 80:80
    --secret db-password
    nginx:alpine

容器内运行的应用程序可以访问位于 /run/secrets/db-password 的密钥。现在,SwarmKit 不允许我们在运行的服务中更新现有密钥,因此我们必须先删除现有的过时版本密钥,然后再添加新的版本。让我们使用以下命令开始删除:

$ docker service update --secret-rm db-password web

现在,我们可以使用以下命令添加新密钥:

$ docker service update \    --secret-add source=db-password-v2,target=db-password \
    web

请注意 --secret-add 命令的扩展语法,其中包含源和目标参数。

总结

在本章中,我们介绍了路由网格,它为 Docker Swarm 提供了第 4 层路由和负载均衡。然后,我们学习了 SwarmKit 如何在不需要停机的情况下更新服务。此外,我们还讨论了 SwarmKit 在零停机部署方面的当前限制。接着,我们展示了如何在 Swarm 中存储配置数据,在本章的最后部分,我们介绍了使用密钥作为提供机密数据给服务的安全方式。

在下一章中,我们将介绍目前最流行的容器编排工具——Kubernetes。我们将讨论在 Kubernetes 集群中定义和运行分布式、具有韧性、稳健性和高可用性的应用程序所使用的对象。此外,本章还将帮助我们了解 MiniKube,这是一个用于在本地部署 Kubernetes 应用程序的工具,并展示 Kubernetes 与 Docker Desktop 的集成。

问题

为了评估你的学习进度,请尝试回答以下问题:

  1. 用简洁的几句话向感兴趣的外行解释什么是零停机部署。

  2. SwarmKit 如何实现零停机部署?

  3. 与传统(非容器化)系统不同,为什么在 Docker Swarm 中回滚操作能够顺利进行?用简短的几句话解释一下。

  4. 描述 Docker 密钥的两到三项特性。

  5. 你需要推出一个新的库存服务版本。你的命令会是什么样子?以下是一些额外的信息:

    • 新的镜像名为acme/inventory:2.1

    • 我们希望使用滚动更新策略,批次大小为两个任务

    • 我们希望系统在每批次之后等待一分钟

  6. 你需要通过 Docker 机密更新一个名为inventory的现有服务,新的密码通过 Docker 机密提供。新的机密名为MYSQL_PASSWORD_V2。服务中的代码期望机密名为MYSQL_PASSWORD。更新命令是什么样子的?(请注意,我们不希望更改服务代码!)

答案

以下是前面问题的示例答案:

  1. 零停机部署意味着分布式应用中的一个服务的新版本可以更新到新的版本,而不需要应用停止工作。通常,使用 Docker SwarmKit 或 Kubernetes(如我们将看到的那样)是以滚动方式完成的。一个服务由多个实例组成,这些实例按批次更新,以确保大多数实例始终处于运行状态。

  2. 默认情况下,Docker SwarmKit 使用滚动更新策略来实现零停机部署。

  3. 容器是自包含的部署单元。如果部署了一个新的服务版本且未按预期工作,我们(或系统)只需回滚到之前的版本。之前的服务版本也是以自包含的容器形式进行部署的。从概念上讲,向前推进(更新)或向后回滚(撤销)没有区别。一个版本的容器被另一个版本替代。主机本身不会受到这种变化的影响。

  4. Docker 机密在静态存储时是加密的。它们仅传输到使用这些机密的服务和容器。由于 swarm 节点之间的通信使用 mTLS,因此机密是加密传输的。机密永远不会物理存储在工作节点上。

  5. 实现这一目标的命令如下:

    $ docker service update \    --image acme/inventory:2.1 \    --update-parallelism 2 \    --update-delay 60s \    inventory
    
  6. 首先,我们需要从服务中删除旧的机密,然后我们需要将新版本的机密添加进去(直接更新机密是不可能的):

    $ docker service update \    --secret-rm MYSQL_PASSWORD \    inventory$ docker service update \    --secret-add source=MYSQL_PASSWORD_V2, target=MYSQL_PASSWORD \    inventory
    

第四部分:Docker、Kubernetes 和云

这一部分介绍了当前最流行的容器编排工具。它介绍了用于在集群中定义和运行分布式、弹性、可靠和高可用性应用的核心 Kubernetes 对象。最后,它介绍了 minikube 作为一种在本地部署 Kubernetes 应用的方式,并涵盖了 Kubernetes 与 Mac 的 Docker 和 Docker Desktop 的集成。

  • 第十六章介绍 Kubernetes

  • 第十七章使用 Kubernetes 部署、更新和保护应用

  • 第十八章在云中运行容器化应用

  • 第十九章在生产环境中监控与排查应用程序故障

<think>好的,用户提到Docker无法关闭,我需要详细分析可能的原因和解决方案。首先,Docker关闭问题可能涉及多个方面,比如服务未正确停止、容器仍在运行、资源占用导致无法终止,或者权限问题。 第一步,用户可能尝试了基本的命令如`systemctl stop docker`但没成功。这时候要考虑是否有容器或进程仍在运行,阻止Docker正常停止。需要检查所有容器是否已经停止,使用`docker ps -a`查看,并强制停止仍在运行的容器。 接下来,如果服务无法停止,可能是systemd的问题。可以尝试重新加载systemd配置,或者强制杀死Docker进程。但需注意强制终止可能导致数据问题,需谨慎操作。 另外,资源占用也是一个常见原因,例如Docker占用了大量内存或CPU,导致无法正常关闭。使用`top`或`htop`查看资源使用情况,必要时手动终止相关进程。 还有可能是文件锁或套接字未释放,检查`/var/run/docker.sock`是否存在,尝试删除后重启服务。此外,安装的Docker版本是否存在已知的bug,需要更新到最新版本或应用补丁。 权限问题也不能忽视,确保用户有执行管理命令的权限,使用`sudo`或者加入docker用户组。 最后,如果所有方法无效,重启系统可能是最后的解决办法,但需告知用户这会终止所有运行中的容器和服务。 总结步骤时,需要结构清晰,分点列出可能的原因和对应的解决方法,确保用户能一步步排查问题。同时提醒用户注意数据安全和备份,避免强制操作导致的数据丢失。</think>当Docker无法正常关闭时,通常由以下原因导致。请按以下步骤排查: --- ### **一、常见原因分析** 1. **容器仍在运行** Docker默认要求所有容器停止后才能关闭服务。若存在未停止的容器,会导致`docker.service`无法终止。 2. **进程或资源占用** Docker守护进程(`dockerd`)或其子进程(如`containerd`)可能因资源锁或异常占用无法退出。 3. **权限问题** 未使用`sudo`或当前用户无操作Docker的权限。 4. **系统服务依赖冲突** 某些Linux发行版中,Docker可能与其他服务(如防火墙`ufw`)存在依赖关系。 --- ### **二、逐步解决方案** #### **步骤1:强制停止所有容器** ```bash # 停止所有运行中的容器 docker stop $(docker ps -q) # 若容器无法正常停止,强制删除(谨慎操作!会丢失未持久化的数据) docker rm -f $(docker ps -a -q) ``` #### **步骤2:关闭Docker服务** ```bash # 使用systemctl尝试停止服务 sudo systemctl stop docker # 若失败,强制终止进程(极端情况) sudo pkill -9 dockerd # 杀死dockerd进程 sudo pkill -9 containerd # 杀死containerd进程 ``` #### **步骤3:检查并释放资源** ```bash # 查看Docker相关进程是否残留 ps aux | grep -E 'dockerd|containerd' # 手动卸载Docker相关模块(适用于Linux) sudo umount /var/lib/docker/overlay2/* sudo rmmod overlay ``` #### **步骤4:处理系统级依赖** ```bash # 重新加载systemd配置 sudo systemctl daemon-reload # 重启Docker套接字(某些系统需要) sudo systemctl restart docker.socket ``` #### **步骤5:排查文件锁冲突** ```bash # 检查Docker套接字文件 ls -l /var/run/docker.sock # 若文件被锁定,尝试删除后重启服务(谨慎操作!) sudo rm -f /var/run/docker.sock sudo systemctl start docker ``` --- ### **三、终极方案:系统重启** 若上述方法均无效,重启操作系统可强制终止所有Docker进程: ```bash sudo reboot ``` ⚠️ **注意**:重启会导致所有未持久化的容器数据丢失。 --- ### **、预防措施** 1. **更新Docker版本** 老版本可能存在已知的关闭问题: ```bash sudo apt-get update && sudo apt-get upgrade docker-ce ``` 2. **配置容器自动清理** 在`/etc/docker/daemon.json`中添加: ```json { "live-restore": false, "shutdown-timeout": 30 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值