读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第4章使用Docker部署我们的微服务
PART I
Getting Started with Microservice Development Using Spring Boot
在这一部分中,您将学习如何使用 Spring Boot 的一些最重要的特性来开发微服务。
本部分包括以下章节:
- 第 1 章,微服务简介
- 第二章,Spring Boot简介
- 第 3 章,创建一组协作微服务
- 第 4 章,使用 Docker 部署我们的微服务
- 第 5 章,使用 OpenAPI 添加 API 描述
- 第 6 章,添加持久性
- 第 7 章,开发响应式微服务
Deploying Our Microservices Using Docker
在本章中,我们将开始使用 Docker 并将我们的微服务放入容器中!
在本章结束时,我们将对我们的微服务环境运行完全自动化的测试,这些测试将我们所有的微服务作为 Docker 容器启动,除了 Docker 引擎之外不需要任何基础设施。我们还将运行许多测试以验证微服务是否按预期协同工作,最后关闭所有微服务,不留下我们执行的测试的痕迹。
能够以这种方式测试多个协作的微服务非常有用。作为开发人员,我们可以验证微服务是否可以在我们本地的开发人员机器上运行。我们还可以在构建服务器中运行完全相同的测试,以自动验证对源代码的更改不会破坏系统级别的测试。此外,我们不需要分配专门的基础设施来运行这些类型的测试。在接下来的章节中,我们将看到如何将数据库和队列管理器添加到我们的测试环境中,所有这些都将作为 Docker 容器运行。
但是,这并不能取代对自动化单元和集成测试的需求,这些测试单独测试单个微服务。它们和以往一样重要。
对于生产用途,正如我们在本书前面提到的,我们需要一个容器编排器,例如 Kubernetes。我们将在本书的后面部分回到容器编排器和 Kubernetes。
本章将涵盖以下主题:
- Docker 简介
- 码头工人和Java。 Java 在历史上对容器不是很友好,但是随着 Java 10 的变化,让我们看看 Docker 和 Java 是如何结合在一起的!
- 将 Docker 与一个微服务一起使用
- 使用 Docker Compose 管理微服务环境
- 协作微服务的自动化测试
技术要求
有关如何安装本书中使用的工具以及如何访问本书源代码的说明,请参阅:
- 第 21 章适用于 macOS
- 第 22 章 适用于 Windows
本章代码示例均来自$BOOK_HOME/Chapter04
中的源码。
如果您想查看本章中应用于源代码的更改,即查看添加对 Docker 的支持所做的工作,您可以将其与 Chapter 3 的源代码进行比较,创建一组协作微服务。您可以使用您最喜欢的 diff
工具并比较两个文件夹 $ BOOK_HOME/Chapter03/2-basic-rest-services
和 $BOOK_HOME/Chapter04
。
Docker 简介
正如我们在 第 2 章,Spring Boot 简介中已经提到的,Docker容器概念作为虚拟机的轻量级替代品在 2013 年非常流行。快速回顾一下:容器实际上是在 Linux 主机中处理的,该主机使用 Linux 命名空间 来提供容器之间的隔离, 和 Linux Control Groups (cgroups) 用于限制数量允许容器消耗的 CPU 和内存。
与使用管理程序在每个虚拟机中运行操作系统的完整副本的虚拟机相比,容器中的开销只是虚拟机中开销的一小部分。这导致更快的启动时间和显着降低的占用空间。然而,容器并不被认为与虚拟机一样安全。看看下面的图表:

图 4.1:虚拟机与容器
该图说明了虚拟机和容器的资源使用情况之间的差异,表明同类型的服务器可以运行比虚拟机更多的容器。主要的好处是容器不需要像虚拟机那样运行自己的操作系统实例。
运行我们的第一个 Docker 命令
让我们尝试通过使用 Docker 的 run
命令启动 Ubuntu 服务器来启动 容器:
使用上述命令,我们要求 Docker 创建一个运行 Ubuntu 的容器,该容器基于 Ubuntu 官方 Docker 镜像的最新版本。使用了 -it
选项,以便我们可以使用终端与容器交互,而 --rm
选项告诉 Docker 在我们退出终端会话后删除容器;否则,容器将保持在 Docker 引擎中并处于 Exited
状态。
我们第一次使用我们自己没有构建的 Docker 镜像时,Docker 会从一个 Docker 注册中心下载它,默认是 Docker Hub (https://hub.docker.com)。这需要一些时间,但是对于该 Docker 映像的后续使用,容器将在几秒钟内启动!
下载 Docker 映像并启动容器后,Ubuntu 服务器应以如下提示响应:

图 4.2:Ubuntu 服务器响应
例如,我们可以通过询问它运行的 Ubuntu 版本来试用容器:
它应该响应如下内容:

图 4.3:Ubuntu 版本响应
我们可以使用 exit
命令离开 容器并验证 Ubuntu 容器不再退出使用 docker ps -a
命令。我们需要使用 -a
选项来查看停止的容器;否则,只显示正在运行的容器。
如果您喜欢 CentOS 而不是 Ubuntu,请随意尝试使用 docker run --rm -it centos
命令。一旦 CentOS 服务器开始在其容器中运行,例如,您可以使用 cat /etc/redhat-release
命令。它应该响应如下内容:

图 4.4:CentOS 版本响应
使用 exit
命令离开容器以将其移除。
如果在某个时候,您发现 Docker 引擎中有很多不需要的容器,并且您想获得一张白纸,也就是说,将它们全部清除,您可以运行以下命令:
docker rm -f
命令停止并删除容器 ID 为 指定给命令。 docker ps -aq
命令列出了 Docker 引擎中所有正在运行和停止的容器的容器 ID。 -q
选项减少了 docker ps
命令,以便它只列出容器 ID。
现在我们已经了解了 Docker 是什么,我们可以继续考虑在 Docker 中运行 Java 时可能面临的问题。
在 Docker 中运行 Java 的挑战
在过去的 年里,已经有很多尝试让 Java 在 Docker 中以一种好的方式工作。最重要的是,在内存和 CPU 的使用方面,Java 历来并不擅长遵守为 Docker 容器设置的限制。
目前,Java 的官方 Docker 镜像来自 OpenJDK 项目:https://hub.docker.com/_/openjdk/。我们将使用 AdoptOpenJDK 项目中的替代 Docker 映像。它包含来自 OpenJDK 项目的相同二进制文件,但提供的 Docker 镜像变体比来自 OpenJDK 项目的 Docker 镜像更能满足我们的需求。
在本节中,我们将使用包含完整 JDK (Java 开发Kit) 及其所有工具。当我们在 Using Docker with one microservice 部分开始将我们的微服务打包到 Docker 镜像中时,我们将使用一个更紧凑的 Docker 镜像 基于 JRE (Java Runtime Environment),仅包含运行时所需的 Java 工具。
如前所述,早期版本的 Java 并不太擅长使用 Linux cgroups 为 Docker 容器指定配额。他们只是忽略了这些设置。因此,Java 并没有根据容器中的可用内存在 JVM 内分配内存,而是分配内存,就好像它可以访问 Docker 主机中的所有内存一样。当尝试分配比允许更多的内存时,Java 容器被主机杀死,并显示“内存不足”错误消息。同样,Java 分配与 Docker 主机中可用 CPU 内核总数相关的 CPU 相关资源(例如线程池),而不是为运行 JVM 的容器提供的 CPU 内核数。
在 Java SE 9 中,提供了对基于容器的 CPU 和内存限制的初始支持,在 Java SE 10 中得到了很大改进。
让我们看看 Java SE 16 如何响应我们在运行它的容器上设置的限制!
在以下测试中,我们将在 MacBook Pro 上的虚拟机中运行 Docker 引擎,充当 Docker 主机。 Docker 主机配置为使用 12 个 CPU 内核和 16 GB 内存。
我们将首先了解如何将可用 CPU 的数量限制为运行 Java 的容器。之后,我们将对限制内存做同样的事情。
限制可用 CPU
让我们首先找出有多少可用处理器,即CPU 内核,Java 在不应用任何约束的情况下可以看到。我们可以通过将 Java 语句 Runtime.getRuntime().availableprocessors()
发送到 Java CLI 工具 jshell
。我们将使用包含完整 Java 16 JDK 的 Docker 映像在容器中运行 jshell
。此图像的 Docker 标记是 adoptopenjdk:16
。该命令如下所示:
此命令会将字符串 Runtime.getRuntime().availableProcessors()
发送到将使用 jshell
。我们将得到以下响应:

图 4.5:显示可用 CPU 内核数量的响应
12
核心的响应符合预期,因为 Docker 主机配置为使用 12 个 CPU 核心。让我们继续并使用 --cpus 3
Docker 选项将 Docker 容器限制为只允许使用三个 CPU 内核,然后询问 JVM关于它看到的可用处理器数量:
JVM 现在响应 Runtime.getRuntime().availableProcessors()$1 ==> 3
,即Java SE 16 尊重容器中的设置,因此能够正确配置线程池等CPU相关资源!
限制可用内存
关于可用的内存量,让我们询问JVM它认为它可以为堆分配的最大大小。我们可以通过使用 -XX:+PrintFlagsFinal
Java 选项向 JVM 询问额外的运行时信息,然后使用 grep
命令过滤掉MaxHeapSize
参数,像这样:
为 Docker 主机分配 16 GB 内存后,我们将得到以下响应:

图 4.6:显示 MaxHeapSize 的响应
在没有 JVM 内存限制的情况下,即不使用 JVM 参数 -Xmx
,Java 将分配四分之一的容器可用内存用于它的堆。因此,我们希望它最多为其堆分配 4 GB。从前面的屏幕截图中,我们可以看到响应为 4,198,498,304 字节。这等于 4,198,498,304 / 10242 = 4004 MB,接近预期的 4 GB。
如果我们使用 Docker 选项 -m=1024M
将 Docker 容器限制为仅使用最多 1 GB 的内存,我们预计会看到更低的内存最大内存分配。运行命令:
将导致响应 268,435,456 字节,等于 268,435,456 / 10242 = 256 MB。 256 MB 是 1 GB 的四分之一,所以这与预期的一样。
像往常一样,我们可以自己在 JVM 上设置最大堆大小。例如,如果我们想允许 JVM 使用总 1 GB 中的 600 MB 用于其堆,我们可以使用 JVM 选项指定 - Xmx600m
比如:
JVM 将响应 629,145,600 字节 = 600 * 10242 = 600 MB,再次符合预期。
让我们以“out of memory”测试结束,以确保它确实有效!
我们将在 JVM 中使用 jshell
分配一些内存,该 JVM 运行在已分配 1 GB 内存的容器中;也就是说,它的最大堆大小为 256 MB。
首先,尝试分配一个 100 MB 的字节数组:
该命令将响应 $1 ==>
,这意味着它运行良好!
通常, jshell
将打印出命令产生的值,但是 100 MB 的字节全部设置为零有点太多了,无法打印,所以我们什么也得不到。
现在,让我们尝试分配一个大于最大堆大小的字节数组,例如 500 MB:
JVM 发现它无法执行该操作,因为它遵循最大内存的容器设置并立即响应 Exception java.lang.OutOfMemoryError: Java heap space
。伟大的!
因此,总而言之,我们现在已经看到 Java 如何尊重可用 CPU 的设置及其容器的内存。让我们继续为其中一个微服务构建我们的第一个 Docker 镜像!
将 Docker 与一个微服务一起使用
现在我们了解了 Java 在容器中的工作原理,我们可以开始将 Docker 与我们的一个微服务一起使用。在我们可以将微服务作为 Docker 容器运行之前,我们需要将其打包到 Docker 映像中。要构建 Docker 映像,我们需要一个 Dockerfile,因此我们将从它开始。接下来,我们需要为我们的微服务配置一个特定于 Docker 的配置。由于在容器中运行的微服务与其他微服务是隔离的——它有自己的 IP 地址、主机名和端口——与它在与其他微服务在同一主机上运行时相比,它需要不同的配置。
例如,由于其他微服务不再运行在同一台主机上,因此不会发生端口冲突。在 Docker 中运行时,我们可以为所有微服务使用默认端口 8080
,而不会出现端口冲突的风险。另一方面,如果我们需要与其他微服务通信,我们不能再像在运行它们时那样使用 localhost
同一个主机。
在容器中运行微服务不会影响微服务中的源代码,只会影响它们的配置!
为了处理在没有 Docker 的情况下在本地运行以及将微服务作为 Docker 容器运行时所需的不同配置,我们将使用 Spring 配置文件。自第 3 章,创建一组协作微服务以来,我们一直使用默认的 Spring 配置文件在没有 Docker 的情况下在本地运行。现在,我们将创建一个名为 docker
的新 Spring 配置文件,以在我们将微服务作为 Docker 中的容器运行时使用。
源代码的变化
我们将使用 product
微服务启动 ,该微服务可以在 < code class="Code-In-Text--PACKT-">$BOOK_HOME/Chapter04/microservices/product-service/。在下一节中,我们也会将其应用于其他微服务。
首先,我们在属性文件 application.yml
的末尾添加 Docker 的 Spring 配置文件:
Spring 配置文件可用于指定特定于环境的配置,在这种情况下,该配置仅在 Docker 容器中运行微服务时使用。其他示例是特定于 dev
、test
和 生产
环境。配置文件中的值会覆盖默认配置文件中的值。使用 yaml
文件,可以将多个 Spring 配置文件放在同一个文件中,用 ---
。
我们现在唯一更改的参数是正在使用的 端口;在容器中运行微服务时,我们将使用默认端口 8080
。
接下来,我们将创建用于构建 Docker 映像的 Dockerfile。正如在第 2 章,Spring Boot 简介中提到的,Dockerfile 可以很简单:
需要注意的一些事项是:
- Docker 镜像将基于 OpenJDK 的官方 Docker 镜像并使用版本 16。
- 端口
8080
将暴露给其他 Docker 容器。 - fat-jar 文件将从 Gradle 构建库
build/libs
添加到 Docker 映像。 - Docker基于这个Docker镜像启动容器的命令是
java -jar /app.jar
。
这种简单的方法有几个缺点:
- 我们使用的是 Java SE 16 的完整 JDK,包括编译器和其他开发工具。这使得 Docker 镜像变得不必要地大,并且从安全的角度来看,我们不想在镜像中引入不必要的工具。因此,我们更愿意使用 Java SE 16 JRE(Java 运行时环境)的基础镜像,它只包含运行 Java 程序所需的程序和库。不幸的是,OpenJDK 项目没有为 Java SE 16 JRE 提供 Docker 镜像。
- 当 Docker 容器启动时,fat-jar 文件需要一些时间来解包。更好的方法是在构建 Docker 映像时解包 fat-jar。
- The fat-jar file is very big, as we will see below, some 20 MB. If we want to make repeatable changes to the application code in the Docker images during development, this will result in suboptimal usage of the Docker build command. Since Docker images are built in layers, we will get one very big layer that needs to be replaced each time, even in the case where only a single Java class is changed in the application code.
更好的方法是将内容分成不同的层,其中不经常更改的文件放在第一层,变化最多的文件放在最后一层。这将导致很好地使用 Docker 的层缓存机制。对于某些应用程序代码更改时未更改的第一个稳定层,Docker 将简单地使用缓存而不是重建它们。这将导致更快地构建微服务的 Docker 映像。
关于 OpenJDK 项目中缺少 Java SE 16 JRE 的 Docker 映像,还有其他开源项目将 OpenJDK 二进制文件打包到 Docker 映像中。 使用最广泛的项目之一是 AdoptOpenJDK (https://adoptopenjdk.net)。 2020 年 6 月,AdoptOpenJDK 项目决定加入 Eclipse 基金会。 AdoptOpenJDK 提供其 Docker 映像的完整 JDK 版本和最小化 JRE 版本。
在处理 Docker 镜像中 fat-jar 文件的次优打包时,Spring Boot 在 v2.3.0 中解决了这个问题,使得将 fat-jar 文件的内容提取到多个文件夹中成为可能。默认情况下,Spring Boot 在提取 fat-jar 文件后会创建以下文件夹:
dependencies
,包含所有依赖为jar-filesspring-boot-loader
,包含知道如何启动 Spring Boot 应用程序的 Spring Boot 类snapshot-dependencies
,包含快照依赖项,如果有的话application
,包含应用程序类文件和资源
Spring Boot 文档建议按照上面列出的顺序为每个文件夹创建一个 Docker 层。将基于 JDK 的 Docker 镜像替换为基于 JRE 的镜像并添加指令以将 fat-jar 文件分解到 Docker 镜像中的适当层后,Dockerfile 如下所示:
为了处理 Dockerfile 中 fat-jar 文件的提取 ,我们使用 multi-stage build,这意味着有一个第一步,名为 builder
,用于处理提取。第二阶段构建将在运行时使用的实际 Docker 映像,从第一阶段根据需要选择文件。使用这种技术,我们可以处理 Dockerfile 中的所有打包逻辑,同时将最终 Docker 映像的大小保持在最小:
- The first stage starts with the line:
从这一行中,我们可以看到使用了来自 AdoptOpenJDK 项目的 Docker 映像,并且它包含用于 v16_36 的 Java SE JRE。我们还可以看到这个阶段被命名为
builder
。 builder
阶段将工作目录设置为extracted
和将 Gradle 构建库中的 fat-jar 文件build/libs
添加到该文件夹。builder
阶段然后运行命令java -Djarmode=layertools -jar app.jar extract
,它将执行将 fat-jar 文件提取到其工作目录extracted
文件夹中。- The next and final stage starts with the line:
它使用与第一阶段相同的基本 Docker 映像,并将文件夹
application
作为其工作目录。它将分解的文件从builder
阶段逐个文件夹复制到应用程序
文件夹。这会为每个文件夹创建一个图层,如上述 所述。参数--from=builder
用于指示 Docker 从builder
阶段。 - 在暴露适当的端口后,在这种情况下为
8080
,Dockerfile 通过告诉 Docker 运行什么 Java 类来启动分解中的微服务来结束格式,即org.springframework.boot.loader.JarLauncher
。
在了解了源代码所需的更改之后,我们准备构建我们的第一个 Docker 映像。
构建 Docker 镜像
要构建 Docker 镜像,我们首先需要构建我们的部署工件,即 fat-jar-file,用于 产品-服务
:
由于我们只想构建 product-service
及其依赖的项目(api
和 util projects
),我们不使用普通的 build
命令,用于构建所有微服务。相反,我们使用一个变体告诉 Gradle 只构建 product-service
项目: :microservices:product-service:build
。
我们可以在 Gradle 构建库中找到 fat-jar 文件,build/libs
。命令 ls -l microservices/product-service/build/libs
将报告如下内容:

图 4.7:查看 fat-jar 文件详细信息
如您所见,JAR 文件大小接近 20 MB——难怪它们被称为 fat-jar 文件!
如果你对它的实际内容感到好奇,可以使用命令unzip -l microservices/product-service/build/libs/product-service-查看1.0.0-SNAPSHOT.jar
。
接下来,我们将构建 Docker 镜像并将其命名为 product-service
,如下所示:
Docker 将使用 当前目录中的 Dockerfile 来构建 Docker 镜像。该图像将使用名称 product-service
进行标记,并本地存储在 Docker 引擎中。
使用以下命令验证我们是否按预期获得了 Docker 映像:
预期输出如下:

图 4.8:验证我们构建了 Docker 镜像
现在我们已经构建了镜像,让我们看看如何启动服务。
启动服务
让我们使用以下命令将 product
微服务作为 容器启动:
这是我们可以从命令中推断出来的:
docker run
:docker run
命令将启动容器并在终端中显示日志输出。只要容器运行,终端就会被锁定。- 我们已经看到了
--rm
选项;一旦我们使用 Ctrl + C 从终端停止执行,它将告诉 Docker 清理容器。 -p8080:8080
选项将端口8080
映射到将容器移植到 Docker 主机中的8080
,这样就可以从外部调用它。对于在本地 Linux 虚拟机中运行 Docker 的 Docker Desktop for Mac,该端口也将被端口转发到 macOS,它在本地主机 。请记住,我们只能将一个容器映射到 Docker 主机中的特定端口!
- 使用
-e
选项,我们可以为容器指定环境变量,在本例中为SPRING_PROFILES_ACTIVE=docker
。SPRING_PROFILES_ACTIVE
环境变量用于告诉 Spring 要使用哪些配置文件。在我们的例子中,我们希望 Spring 使用docker
配置文件。 - 最后,我们有
product-service
,这是我们在上面构建的 Docker 镜像的名称,Docker 将使用它来启动容器。
预期输出如下:

图 4.9:产品微服务启动后的输出
- Spring 使用的配置文件是
docker
。在输出中查找以下配置文件处于活动状态:docker
以验证这一点。 - 容器分配的端口是
8080
。在输出中查找Netty started on port(s): 8080
以验证这一点。 - 一旦写入日志消息
Started ProductServiceApplication
,微服务就可以接受请求了!
我们可以使用 localhost 上的端口 8080
与微服务通信,如前所述。在另一个终端窗口中尝试以下命令:
以下是预期的输出:

图 4.10:请求产品 3 的信息
这类似于我们从上一章收到的输出,但有一个主要区别:我们现在有 "service Address":"9dc086e4a88b/172.17 的内容.0.2:8080"
,端口是8080
,和预期一样,IP地址,172.17.0.2
,是从 Docker 中的内部网络分配给容器的 IP 地址——但是主机名在哪里,9dc086e4a88b
,从哪里来?
向 Docker 询问所有正在运行的容器:
我们将看到如下内容:

图 4.11:所有正在运行的容器
从 前面的输出中我们可以看到,主机名相当于容器的 ID,如果您想了解哪个容器实际响应了您的请求,这很高兴!
通过使用 Ctrl + C 命令在终端中停止容器来完成此操作。完成此操作后,我们现在可以继续运行分离的容器。
分离运行容器
好的, 很棒,但是如果我们不想从启动容器的位置锁定终端怎么办?在大多数情况下,为每个正在运行的容器锁定终端会话是不方便的。是时候学习如何启动容器detached了——在不锁定终端的情况下运行容器!
我们可以通过添加 -d
选项并同时使用 --name
选项。给它一个名字是可选的,如果我们不给它一个名字,Docker 会生成一个名字,但是它更容易使用我们决定的名字向分离的容器发送命令。 --rm
选项不再需要,因为我们将在完成后显式停止并删除容器:
如果我们再次运行 docker ps
命令,我们将看到我们的新容器,名为 my-prd-srv
:

图 4.12:以分离方式启动容器
但是我们如何从容器中获取日志输出呢?
满足 docker
logs
命令:
-f
选项告诉命令跟随日志输出,即当所有当前日志输出都已写入时不结束命令终端,还要等待更多的输出。如果您希望看到很多不想看到的旧日志消息,您还可以添加 --tail 0
选项,以便您只会看到新的日志消息。或者,您可以使用 --since
选项 并指定绝对时间戳或相对时间,例如 --since 5m
,查看最多五分钟前的日志消息。
尝试使用新的 curl
请求。您应该会看到一条新的日志消息已写入终端的日志输出。
通过停止和移除容器来结束它:
-f
选项强制 Docker 移除容器,即使它正在运行。 Docker 会在容器移除之前自动停止它。
现在我们知道如何将 Docker 与微服务一起使用,我们可以了解如何在 Docker Compose 的帮助下管理微服务环境。
使用 Docker Compose 管理微服务环境
我们已经了解了如何将单个微服务作为 Docker 容器运行,但是如何管理整个微服务系统呢?
正如我们前面提到的,这就是 docker-compose
的目的。通过使用单个命令,我们可以构建、启动、记录和停止一组作为 Docker 容器运行的协作微服务。
源代码的变化
为了能够使用Docker Compose,我们需要创建一个配置文件,docker-compose.yml< /code>,它描述了 Docker Compose 将为我们管理的微服务。我们还需要为剩余的微服务设置 Dockerfile,并为每个微服务添加一个特定于 Docker 的 Spring 配置文件。所有四个微服务都有自己的 Dockerfile,但它们看起来都与前一个相同。
说到Spring Profiles,三个核心服务,product-
, recommendation-
,和review-service
,有相同的docker
配置文件,仅指定作为容器运行时应使用默认端口 8080
。
对于 product-composite-service
,事情有点复杂,因为它需要知道在哪里可以找到核心服务。当我们在 localhost 上运行所有服务时,它被配置为使用 localhost 和单独的端口号,7001
-7003
,用于每个核心服务。在 Docker 中运行时,每个服务都有自己的主机名,但可以在相同的端口号 8080
上访问。在这里,product-composite-service
docker 配置文件> 看起来如下:
此配置存储在属性文件 application.yml
中。
主机名、product
、recommendation
和review
来自哪里?
这些在 docker-compose.yml
文件中指定,该文件位于 $BOOK_HOME/Chapter04
文件夹。它看起来像这样:
- 微服务的名称。这也将是内部 Docker 网络中容器的主机名。
- 一个构建指令,指定在哪里可以找到用于构建 Docker 映像的 Dockerfile。
- 内存限制为 512 MB。在本书的范围内,对于我们所有的微服务来说,512 MB 应该足够了。对于本章,它可以设置为较低的值,但是随着我们在接下来的章节中为微服务添加更多功能,它们的内存需求将会增加。
- 将为容器设置的环境变量。在我们的例子中,我们使用这些来指定要使用的 Spring 配置文件。
对于 product-composite
服务,我们还将指定端口映射——我们将公开其端口以便可以访问 来自 Docker 外部。其他微服务将无法从外部访问。接下来,我们将了解如何启动微服务环境。
在第 10 章,使用 Spring Cloud Gateway 将微服务隐藏在边缘服务器之后,和 第11,保护对 API 的访问,我们将详细了解如何锁定和保护对微服务系统环境的外部访问。
启动微服务环境
完成所有 必要的代码更改后,我们可以构建 Docker 映像,启动微服务环境,并运行一些测试以验证它是否按预期工作。为此,我们需要执行以下操作:
- 首先,我们使用 Gradle 构建部署工件,然后使用 Docker Compose 构建 Docker 镜像:
- Then, we need to verify that we can see our Docker images, as follows:
我们应该看到以下输出:
图 4.13:验证我们的 Docker 镜像
- Start up the microservices landscape with the following command:
-d
选项将使 Docker Compose 以分离模式运行容器,与 Docker 相同。
docker compose logs
命令支持同样的-f
和 --tail
选项为 docker logs
,如前面描述过。
Docker Compose logs
命令还支持将日志输出限制为一组容器。只需在 logs
命令之后添加您希望查看其日志输出的容器的名称。例如,仅查看 product
和 review< 的日志输出/code> 服务,使用
docker-compose logs -f product review
。
当所有四个微服务都报告它们已启动时,我们就可以尝试微服务环境了。查找以下内容:

图 4.14:启动所有四个微服务
请注意,每条日志消息都以产生输出的容器名称为前缀!
现在,我们准备运行 一些测试来验证它是否按预期工作。与我们在上一章中直接在 localhost 上运行它时相比,在 Docker 中调用复合服务时,我们需要进行的唯一更改是端口号。我们现在使用端口 8080
:
我们将得到相同类型的响应:

图 4.15:调用复合服务
但是,有一个很大的区别 - serviceAddresses
在响应中报告的主机名和端口:

图 4.16:查看 serviceAddresses
在这里,我们可以看到已分配给每个 Docker 容器的 主机名和 IP 地址。
我们完成了;现在只剩下一步了:
前面的命令将关闭微服务环境。到目前为止,我们已经看到了如何手动测试运行 bash 命令的协作微服务。在下一节中,我们将了解如何增强测试脚本以自动化这些手动步骤。
协作微服务的自动化测试
Docker Compose 在手动管理一组微服务时非常有用。在本节中,我们将更进一步,将 Docker Compose 集成到我们的测试脚本 test-em-all.bash
中。测试脚本将自动启动微服务环境,运行所有必需的测试以验证微服务环境是否按预期工作,最后将其拆除,不留任何痕迹。
测试脚本可以在 $BOOK_HOME/Chapter04/test-em-all.bash
找到。
在测试脚本运行测试套件之前,它将检查测试脚本调用中是否存在 start
参数。如果找到,它将使用以下代码重新启动容器:
之后,测试脚本将等待 product-composite
服务以 OK 响应:
waitForService
函数使用 curl
。重复发送请求,直到
curl
响应它从请求中获得成功响应。该函数在每次尝试之间等待 3
秒,并在 100< 后放弃/code> 尝试,以失败停止脚本。
接下来,所有测试都像以前一样执行。之后,如果脚本在调用参数中找到 stop
参数,它将拆除景观:
测试脚本还将默认端口从我们在不使用 Docker 运行微服务时使用的 7000
更改为 8080
,由我们的 Docker 容器使用。
让我们试试吧!要启动景观,运行测试,然后将其拆除,请运行以下命令:
以下是针对启动和关闭阶段的测试运行的一些示例输出。实际测试的输出已被删除(它们与上一章相同):

图 4.17:测试运行的示例输出
运行 这些测试后,我们可以继续了解如何对失败的测试进行故障排除。
对测试运行进行故障排除
如果 正在运行的测试 ./test-em-all.bash start stop
失败,按照以下步骤可以帮助您识别问题并在问题解决后恢复测试:
- First, check the status of the running microservices with the following command:
如果所有微服务都已启动并运行且健康,您将收到以下输出:
图 4.18:检查正在运行的微服务的状态
- If any of the microservices do not have a status of
Up
, check their log output for any errors by using thedocker-compose logs
command. For example, you would use the following command if you wanted to check the log output for theproduct
service:在这个阶段,要记录错误并不容易,因为微服务是如此简单。相反,这里是 Chapter 6 中
product
微服务的示例错误日志,添加持久性。假设在其日志输出中找到以下内容:图 4.19:日志输出中的示例错误信息
从上面的日志输出来看,很明显
product
微服务无法访问其 MongoDB 数据库。鉴于数据库也作为由同一个 Docker Compose 文件管理的 Docker 容器运行,因此可以使用docker-compose logs
命令查看内容数据库有问题。如果需要,您可以使用
docker-compose restart
命令重新启动失败的容器。例如,如果要重新启动product
微服务,可以使用以下命令:如果容器丢失,例如由于崩溃,您可以使用
docker-compose up -d --scale
命令启动它.例如,您可以对product
微服务使用以下 命令:如果日志输出中的错误表明 Docker 磁盘空间不足,可以使用以下命令回收部分空间:
- 一旦所有微服务都启动并运行并且健康,再次运行测试脚本,但不启动微服务:
测试现在应该运行良好!
- 完成测试后,请记住拆除系统环境:
最后,关于从源代码构建运行时工件和 Docker 映像,然后在 Docker 中执行所有测试的组合命令的提示:
概括
在本章中,我们看到了如何使用 Docker 来简化协作微服务环境的测试。
我们了解了自 v10 以来 Java SE 如何遵守我们对容器设置的关于允许使用多少 CPU 和内存的约束。我们还看到了将基于 Java 的微服务作为 Docker 容器运行所需的时间。感谢 Spring 配置文件,我们可以在 Docker 中运行微服务,而无需进行任何代码更改。
最后,我们看到了 Docker Compose 如何帮助我们使用单个命令来管理协作微服务的格局,无论是手动还是自动,当与 test-em-all.bash
。
在下一章中,我们将研究如何使用 OpenAPI/Swagger 描述添加一些 API 文档。
问题
- 虚拟机和 Docker 容器之间的主要区别是什么?
- Docker 中命名空间和 cgroup 的用途是什么?
- 如果 Java 应用程序不遵守容器中的最大内存设置并且分配的内存超出允许的范围,会发生什么情况?
- 我们如何使基于 Spring 的应用程序作为 Docker 容器运行而不需要修改其源代码?
- 为什么以下 Docker Compose 代码片段不起作用?