第 4 章:微服务的数据管理
本章内容概览:
- 利用 Docker Compose 在开发和测试环境中构建和运行微服务应用
- 为应用添加文件存储功能
- 为应用集成数据库服务
在构建任何应用程序时,我们常常需要处理数据存储和文件管理,有时两者兼须。微服务架构亦是如此。我们需要数据库来存储应用程序生成或更新的动态数据,同时也需要存储空间来保存应用程序的静态资产或用户上传的文件。
在本章中,我们将向 FlixTube 示例应用添加文件存储和数据库支持。首先,我们将集成文件存储解决方案,确保 FlixTube 有一个可靠的地方来保存视频文件。我们将引入一个全新的微服务,专门负责视频存储,以清晰区分流媒体播放和视频存储职责。本章还将创建我们的第二个微服务。
接着,我们将引入一个数据库来存储视频的元数据。当前,这些数据仅包括视频的路径等基本信息,但设置数据库只是起步。一旦我们建立起数据库,就能为所有微服务提供持久化的数据存储解决方案。
通过在我们的应用中添加数据库服务器和第二个微服务,我们将迈出整合多个容器的重要一步。在第 2 章中,我们建立了第一个微服务;在第 3 章中,我们使用 Docker 在容器中实例化了第一个微服务。本章将拓展我们的应用,支持多容器运行,这意味着我们需要引入一个新工具。
4.1 新工具
本章介绍了两种为微服务应用提供数据存储的方式:文件存储和数据库。存在多种方法和工具可以实现这些功能,选择哪一种取决于项目特性、团队、公司和客户的具体需求。
在本书的示例中,我选择了 MongoDB 作为数据库,并使用 Azure Storage 作为文件存储解决方案。此外,我们还将升级开发环境,使其能够同时运行多个容器。虽然可以通过反复使用 Docker 的 build 和 run 命令来实现,但这种方法并不适合较大的应用。想象一下,如果你需要这样运行十个微服务,将会是多么繁琐!因此,我们需要一种更高效的方式来管理开发中的多个微服务。本章将引入 Docker Compose。表 4.1 列出了本章中将介绍的新工具。
表 4.1 本章介绍的工具
| 工具 | 版本 | 用途 |
|---|---|---|
| Docker Compose | 包含在 Docker 24.0.5 中 | Docker Compose 作为 Docker 的一部分提供(在第 3 章中已安装),帮助我们配置、构建、运行和管理多个容器,适合开发环境中使用,但不推荐在生产环境中使用。 |
| Azure Storage | Azure Storage 提供云存储服务,可以通过 Azure 门户、API 或命令行工具管理。我们将演示如何通过 Azure 门户上传一个视频,并使用 Node.js Azure Storage SDK 来读取它。 | |
| @azure/storage-blob | 12.15.0 | 这个 npm 包允许我们通过 JavaScript 从 Azure Storage 检索文件。 |
| MongoDB | 7.0.0 | MongoDB 是一种流行的 NoSQL 数据库,以其轻量、易配置及使用著称,非常适合微服务架构。 |
| mongodb | 6.0.0 | 这是 MongoDB 的 Node.js 驱动程序,是一个 npm 包,我们使用它来使 JavaScript 能够与数据库交互。 |
4.2 获取代码
为了跟随本章内容,你需要下载或克隆代码库。你可以通过以下链接下载代码的 zip 文件: http://mng.bz/n16e 。你也可以使用 Git 克隆代码库,操作如下:
git clone https://github.com/bootstrapping-microservices-2nd-edition/chapter-4
如果你在代码中遇到任何问题,可以在 GitHub 的对应仓库中提出问题。有关安装和使用 Git 的更多帮助,请参考第 2 章。
4.3 使用 Docker Compose 开发微服务
在上一章结束时,我们在开发计算机上的一个容器中成功运行了一个微服务,并通过网页浏览器进行了测试。图 4.1 展示了我们当前的设置。

然而,仅有一个微服务的应用还不能真正称之为微服务架构!现在是时候扩展我们的应用,增加更多的容器,使其成为一个真正的微服务应用。为了在开发和测试环境中构建和运行我们的微服务应用,我们将开始使用 Docker Compose。
我们在本章中扩展到多个容器,因为我们需要添加一个数据库(虽然它是一个容器,但不是我们通常所说的微服务),同时我们还需要添加一个新的微服务来处理文件存储(这将是另一个容器)。从一个容器(我们的视频流微服务)开始,到本章结束时,我们将有三个容器运行,如图 4.2 所示。
为了构建、运行和管理不断增长的应用,我们可以通过多次运行各种 Docker 命令来应付过去(每个镜像或容器重复一次)。然而,这在开发过程中很快变得乏味,因为我们需要多次停止并重新启动应用。随着应用继续增长,这个问题只会变得更糟,因为我们将继续为其添加容器。因此,我们需要一个更好的工具。

4.3.1 为什么选择 Docker Compose?
在开发环境中管理多个容器可能非常复杂。虽然你将在第 6 章和第 7 章中学习如何使用 Kubernetes 在生产环境中管理容器,但 Kubernetes 主要设计用于管理运行在多台计算机的集群上的大型系统。我们确实可以在本地使用 Docker Desktop 中集成的 Kubernetes(这是自本书第一版以来新增的功能),但它相对资源密集(如果你不怕你的电脑性能下降,尝试在笔记本电脑上运行它),而且在处理多个项目时可能不够便捷。
我们将在第 6 章深入学习 Kubernetes 并探讨如何在开发和生产中使用它。所以不用担心——你不会错过任何内容,我们将很快涉及到 Kubernetes。目前,为了简化开发过程,我们可以使用 Docker Compose 来在开发机上“模拟”Kubernetes 的部分功能,这通常比直接使用 Kubernetes 更简单。此外,Docker Compose 现在已随 Docker 一起安装,不再需要单独安装(这也是自第一版以来的一个变化)。
那么,为什么选择 Docker Compose 呢?正如 Docker 让我们能够构建、运行和管理单个微服务那样,Docker Compose 提供了一种便捷的方法,在开发中构建、运行和管理多个微服务。在开发和测试过程中,我们经常需要频繁启动和重启整个应用,而这个应用最终将包括多个微服务。我们还需要在每次微小的开发迭代后测试代码的更改。我们之前介绍过几种方式来执行这些任务:
- 打开多个终端窗口(每个微服务一个),并分别使用 Node.js 或其他技术栈来运行每个微服务(正如第 2 章中所述)。
- 使用 Docker 分别构建和运行每个容器(如第 3 章中所述)。
虽然这些方法对于处理单个微服务很有帮助,但在管理整个微服务应用时,它们可能效率不高。
如果只依靠这些方法来运行个别微服务,随着应用规模的扩大,我们将花费更多时间来管理这些过程,从而减少了真正的开发时间。这种情况会减慢我们的迭代速度,降低生产效率,最终可能影响我们的动力。
因此,我们需要一种更有效的方式来在开发过程中管理应用,这正是 Docker Compose 发挥作用的场景。Docker Compose v2 是用 Go 语言编写的开源工具(而 v1 是用 Python 编写的),你可以在这里找到其代码: https://github.com/docker/compose 。
4.3.2 创建我们的 Docker Compose 文件
Docker Compose 的核心是它的配置文件。这个文件可以被看作是指导如何组建本地微服务应用的脚本。
定义:Docker Compose 文件是一个脚本,它定义了如何将多个 Docker 容器组合成一个应用。
想起我们在第 3 章中创建的 Dockerfile,它是一个用于构建单个镜像的脚本。Docker Compose 文件扩展了这个概念,使我们能够编排整个应用的创建,涵盖一组 Dockerfile。Docker Compose 解析这个文件并部署一个运行中的应用,如图 4.3 所示。

为了熟悉 Docker Compose 的使用,我们从创建一个只有一个容器的简单应用开始,然后逐步增加更多的容器。
本章的示例将展示如何使用 Docker Compose 来实例化第 2 章中创建的视频流微服务。这继续了第 3 章的工作。你可以选择更新之前的示例或使用第 4 章的代码库中的预设示例代码。图 4.4 展示了示例项目的结构。

首先,我们需要把微服务的 Dockerfile 和代码移动到一个新的名为 video-streaming 的子目录中,以符合微服务的名称。这样的调整是因为我们正在构建一个将很快包含多个微服务的应用,因此每个微服务都应该放在自己的子目录中。
接下来,我们来创建名为 docker-compose.yaml 的 Docker Compose 文件,放在微服务应用的根目录中。清单 4.1 展示了我们的首个 Docker Compose 文件。你可以手动输入这段代码,或者从第 4 章代码库的示例目录中使用 Visual Studio Code (VS Code) 加载它。
清单 4.1 我们的微服务的 Docker Compose 文件
version: '3.8' # 使用 Docker Compose 文件版本 3.8
services: # 定义包含的服务
video-streaming: # 我们的视频流微服务配置
build: ./video-streaming # 指定构建镜像所需的上下文和 Dockerfile 位置
container_name: video-streaming # 为实例化的容器命名
ports: # 映射端口
- "4000:80" # 将容器的 80 端口映射到主机的 4000 端口
environment: # 设置环境变量
- PORT=80 # 设置微服务在容器内的 HTTP 服务器使用的端口
restart: on-failure # 设置容器崩溃时的重启策略
清单 4.1 是一个 Docker Compose 文件,用于配置并实例化单个容器:我们的视频流微服务。构建部分直接指定了包含 Dockerfile 的子目录,这是 Docker Compose 查找构建指令的方式。文件中还配置了端口绑定和环境变量,这些操作在 Docker 单独运行时需要手动进行,而现在通过 Docker Compose 自动管理。
YAML 是 Docker Compose 文件的格式,它不仅易于人类阅读,也易于机器解析,使其成为理想的配置文件格式。通过这样的配置,Docker Compose 文件简化了开发过程中容器管理的复杂性,使我们可以专注于开发而非容器维护。
4.3.3 启动我们的微服务应用程序
到目前为止,我们已经设置好了一个 Docker Compose 文件,用于构建和运行我们的视频流微服务,并重新利用了第 3 章中加入的 Dockerfile。现在,我们将测试这个配置。
本节我们将使用 Docker Compose 来启动单个服务。尽管目前这与直接使用 Docker 单独运行服务相比没有太多优势,但这只是一个开始。我们很快就会扩展 Docker Compose 文件以包括多个容器,并用它来启动一个不断增长的应用程序。
首先,打开一个终端并切换到包含 Docker Compose 文件的目录。如果你正在跟随第 4 章的代码库,那么切换到 chapter-4/example-1 目录。接着执行以下命令:
cd chapter-4/example-1
docker compose up --build
up 命令指示 Docker Compose 启动我们的微服务应用程序。--build 参数指示 Docker Compose 在启动容器之前构建每个镜像。
技术上来说,第一次使用 up 命令时不必加 --build 参数,因为它会自动构建镜像。但是在随后的运行中,如果不加 --build 参数,Docker Compose 将使用之前构建的镜像,这是快速重启的方式。如果你修改了微服务的代码并且没有使用 --build 参数,那么这些更改不会被包括在新的容器实例中。因此,除非使用了 --build 参数,否则重启应用程序可能不会反映最近的代码更改。
如果没有意识到这一点,可能会导致在未更新代码的旧镜像上浪费时间进行测试。因此,我建议在每次运行 up 命令时都添加 --build 参数,确保任何代码更改都能被包括进去。
执行 up 命令后,你会看到 Docker Compose 下载基础镜像层的过程。下载完成后,你将看到视频流微服务的日志输出,如下所示:
video-streaming |
video-streaming | > [email protected] start /usr/src/app
video-streaming | > node ./src/index.js
video-streaming |
video-streaming | Microservice online
输出中的容器名称有助于你识别输出来自哪个容器。虽然目前我们只有一个容器在运行,但当应用中有多个容器时,这种标识就非常重要了。
微服务现已启动,并可以进行测试。打开浏览器并导航至 http://localhost:4000/video 来检查视频是否按预期播放。
目前,只有一个微服务运行,并不能真正称之为一个微服务架构的应用程序。但随着我们已经设置好了 Docker Compose,未来向应用程序添加新的服务会变得非常简单。在继续学习 Docker Compose 管理应用程序的更多细节之前,我们将了解其管理单个容器的能力。
虽然目前还没有扩展到多个容器,Docker Compose 已经在处理单个容器的场景中提供了更高效的工作流。使用 up 命令能节省执行单独的 Docker build 和 run 命令的时间。
这些时间节省可能看似微不足道,但正如你将看到的,当有多个微服务需要构建和启动时,up 命令的效率将大大提高。想象一下,如果你有 10 个微服务,up 命令能节省多少时间——一次性构建并运行所有服务,而无需分别执行 10 个 build 命令和 10 个 run 命令。
提示:
docker compose up命令可能是你在本书中学到的最重要的工具。你将经常使用它来开发和测试你的应用程序,我会确保你不会忘记它!
4.3.5 关闭应用程序
你可以通过两种方式停止你的应用程序。如果你在上一节中打开了第二个终端,可以使用它来调用停止命令:
docker compose stop
另一种停止应用程序的方式是在你首次调用 up 命令的终端中按 Ctrl-C。不过,这种方法有一些问题。
第一个问题是你必须小心只按一次 Ctrl-C。如果只按一次,应用程序将优雅地停止,并耐心等待所有容器停止。但如果你像我一样不耐烦,倾向于重复按 Ctrl-C 直到过程完成,这样实际上会中止关闭过程,可能导致部分或所有容器依然运行。
第二个问题是,停止应用程序并不会移除容器。相反,它们将保留在停止状态,以便我们可以检查。这对于调试崩溃的容器非常方便!我们将在第 11 章中更多讨论调试容器。不过,现在更有用的是,我们可以使用 down 命令移除容器并将开发环境恢复到干净状态。通常,我会加上 --volumes 参数:
docker compose down --volumes
--volumes 参数会删除每个微服务的卷。如果不使用这个参数,容器可能会在你重启应用程序时恢复其旧的文件系统。如果你想确保整个应用程序有一个干净的重启,始终使用 --volumes 参数。
因此,我建议总是使用 down 命令来关闭应用程序。尽管按 Ctrl-C 可以解锁终端,但它是不可靠的,而 down 命令则使 stop 命令变得多余。
提示 在按 Ctrl-C 后养成使用
down命令的习惯。
我们可以结合使用 up 和 down 命令轻松地重启应用程序,以便将更新的代码或依赖项引入应用程序。可以按以下方式链接这些命令:
docker compose down --volumes && docker compose up --build
Shell 脚本
你可能觉得输入这些复杂命令很繁琐,可以考虑创建 shell 脚本来简化最常用的命令。比如,可以将 docker compose up --build 封装在一个名为 up.sh 的 shell 脚本中。
通常,当我需要反复运行这些长命令时,会创建更容易运行的 shell 脚本;至少在一天中多次运行命令时会这样做。以下是我常用的一些 shell 脚本:
down.sh用于docker compose down --volumesreboot.sh用于docker compose down --volumes && docker compose up --build
我们将在第 8 章中更多讨论 shell 脚本的价值。
注意 Shell 脚本通常不适用于 Windows。不过,你可以在 WSL2 Linux 终端下使用 shell 脚本。否则,你可以将这些 shell 脚本转换为批处理文件(.bat 文件),这些文件类似于 Windows 版本的 shell 脚本。
我们现在已经为使用 Docker Compose 开发和测试微服务应用程序奠定了良好的基础。你将在第 5 章和第 9 章中了解更多关于使用 Docker Compose 的信息。
4.3.6 为什么用 Docker Compose 进行开发,而不用于生产?
让我们暂停一下,考虑为什么我们在开发中使用 Docker Compose 而不在生产中使用。Docker Compose 似乎是定义微服务应用的好工具,那么为什么不在生产中使用呢?为什么我们选择只在生产中使用 Kubernetes 而不同时在开发和生产中使用,特别是现在本地 Kubernetes 安装已经与 Docker Desktop 捆绑在一起了呢?
我们在开发中不使用 Kubernetes,因为使用 Docker Compose 启动一个多微服务的应用程序进行开发和测试更简单:只需一个配置文件和一个命令即可启动整个应用程序。如果用本地 Kubernetes 安装来做,我们需要调用多个命令来部署多个配置文件。当然,有些开发者更喜欢使用本地 Kubernetes 实例而不是 Docker Compose 进行开发,我们将在第 6 章中探讨这一点。届时,你将掌握多种微服务开发和测试技术。直接在 Kubernetes 上开发是可能的,但通常不是我的首选。在第 6 章之后,你可以自行决定希望采用哪种方式。
使用 Docker Compose 还使得更换项目变得更加容易——这对我来说非常有用,因为我经常在为不同公司承包或在同一公司内更换项目时使用。如果我们需要快速放弃一个项目并开始另一个,只需调用第一个项目的 down 命令,然后在下一个项目上调用 up 命令即可。你甚至可能喜欢为单个项目创建多个子配置(例如,用于关注不同微服务集合的不同设置),然后可以使用 up 和 down 命令在应用程序的不同配置之间轻松切换。
我们不能在生产中使用 Docker Compose,因为这样做存在问题。我们需要在云中创建虚拟机(VM),在其上安装 Docker,复制应用程序代码到那里,然后在 Docker Compose 下启动应用程序。这当然是可能的,但它很笨拙,复杂,难以自动化,最终不具备良好的扩展性(我们将在第 12 章讨论 Kubernetes 的扩展性)。
你将在第 6 章和第 7 章中了解更多关于 Kubernetes 的信息,但这里我只是解释为什么 Docker Compose 是开发的最佳选择,而不是生产的最佳选择。当然,选择的策略取决于你的情况、项目和公司。不要将我的话当作福音,根据实际情况选择最合适的工具。
4.4 为应用程序添加文件存储
随着我们已经能够使用 Docker Compose 轻松管理多个容器,这为我们打开了进入本章主题——数据管理的大门。
为了满足我们的应用程序对文件存储和数据库的需求,我们首先引入文件存储。我们的应用程序需要存储视频文件,常见的做法是利用大型云服务提供商的解决方案。考虑到本书的示例中我们使用的是 Azure,我们将采用 Azure Storage 作为我们的存储解决方案。
注意 许多应用程序,包括示例中的 FlixTube,都需要文件存储功能。存储文件的方式有多种,但使用像 Azure Storage、AWS S3 或 Google Cloud Storage 这样的外部云存储服务是最常见的方法。
尽管我们可以直接将我们的视频流微服务与存储提供商连接,但我们选择不这么做。相反,我们将遵循良好的设计原则——关注点分离和单一职责原则,创建一个新的微服务,专门负责作为文件存储提供商的中介层。图 4.5 展示了添加新的视频存储微服务后,我们的应用程序结构如何变化。
图 4.5 描述了视频存储微服务如何充当视频流微服务和外部云存储之间的中介。我们将在本节的后面进一步讨论为何要分离这些服务的原因。目前,让我们接受这是引入第二个微服务并正式使我们的应用程序变成一个微服务应用(尽管是小型的)的合理步骤。

4.4.1 使用 Azure Storage
Azure Storage 是由 Microsoft 提供的一种云存储服务,我们将利用它来扩展我们应用程序的存储功能。假设你已经根据第 3 章的指引注册了 Azure 账户,本节我们将继续使用该账户创建存储空间并上传测试视频。此外,我们还会创建一个新的微服务,专门用于从这个存储空间中检索视频。
定义 Azure Storage 是一个由 Microsoft Azure 提供的服务,用于在云中存储私有或公开访问的文件。你可以将文件上传至 Azure Storage,并通过 Azure Storage API 访问这些文件。
尽管 Azure Storage 允许托管私有与公开文件,我们选择使用其私有功能,以防止外界直接下载我们的视频。相反,我们想通过我们的前端应用程序控制视频的访问。新微服务的代码将利用 Azure 进行身份验证,并使用可在 npm 上找到的 @azure/storage-blob 包来检索视频。
为何选择 Azure Storage?
尽管我们有众多文件存储选项,选择 Azure Storage 主要是因为我们已经设置了 Azure 环境。同理,使用 AWS S3 或 Google Cloud Storage 也同样方便。这里的选择对本书的目标来说影响不大,因为不同云供应商需要使用不同的存储 API,这会使为视频存储微服务编写的代码略有不同。
注意 本章展示了如何使用 Azure 进行外部云存储。若使用不同的 API,代码会稍有不同,但微服务的结构本质上相同。
创建 Azure Storage 账户
在我们上传测试视频之前,首先需要创建一个 Azure Storage 账户。登录到 Azure 门户网站 https://portal.azure.com/ ,在左侧菜单中点击“Create a resource”,然后搜索“storage account”。

选择 Microsoft 的存储账户选项,然后点击“Create”。现在可以填写新存储账户的详细信息,如图 4.7 所示。
你需要选择或创建一个资源组,然后为存储账户命名。其余设置可保留默认。
填写完详细信息后,点击“Review + Create”。如果所有细节验证无误,点击“Create”即可完成存储账户的创建。如有错误,按提示进行修改。

创建完毕后,等待系统通知你存储账户已成功部署。然后,你可以点击通知中的“转到资源”按钮,或在全局列表中查找你的资源。
在 Azure 门户中打开你的存储账户后,点击左侧菜单中的“访问密钥”。这里你会找到用于身份验证的详细信息。记下存储账户的名称和其中一个密钥。

上传视频至 Azure Storage
创建存储账户后,你现在可以上传测试视频了。在 Azure 门户中打开你的存储账户,点击左侧菜单中的“Container”。你会看到一个空的容器列表,如图 4.9 所示。

首先需要澄清,本段提到的容器并非微服务应用中运行的那种。在 Azure 存储中的容器可视为一个文件夹,用于存储文件。
在工具栏中点击“+ 容器”按钮,即可创建你的第一个容器。接着输入一个容器名称,你可以随意命名,但为了与后续示例代码保持一致,建议命名为“videos”。你也可以选择访问级别,我们将使用默认的私有访问级别。点击确认后,容器即创建成功。
现在,你应该能在列表中看到名为“videos”的容器。点击它,进一步查看详情。在查看新容器的内容时,会看到如图 4.10 所示的提示消息。如果你想知道什么是 blob,它其实就是一个文件。目前还没有上传任何文件。下一步,让我们开始上传文件。

点击工具栏中的上传按钮,选择要上传的视频文件(如果你没有视频文件,可以使用第 4 章代码仓库中“example-1”子目录下的示例视频)。上传完成后,视频文件会显示在文件列表中,如图 4.11 所示。

现在已经成功上传了测试视频到 Azure 存储,我们可以开始创建一个新的视频存储微服务。这将是你的第二个微服务,功能为一个 REST API,用于从存储提供商处检索视频。
注意: 我们原本可以直接将视频流微服务与云存储集成,但我们选择在另一个微服务后设置一个抽象层。这样做的好处是,以后若需要更换存储方案,将会更加简单,并且能够支持多个存储提供商。
首先,你需要为这个新微服务创建一个新的目录。你可以从头开始,新建一个目录,或者直接在 VS Code 中打开第 4 章代码仓库的“example-2”。我们将新目录命名为 azure-storage,以明确它与 Azure 存储的关联。若将来要添加其他存储提供商,可以根据需要给它们命名,如 aws-storage 或 google-storage。图 4.12 展示了“example-2”项目的结构。

在新微服务中,打开一个终端并切换到 azure-storage 目录。如果你是从头开始创建,需要新建一个 package.json 文件,并按照第 2 章的方式安装 express 包。接下来,安装 @azure/storage-blob 包:
npm install --save @azure/storage-blob
如果你是按照第 4 章的代码仓库“example-2”进行的,那么所需的文件、代码及 Dockerfile 都已准备就绪。直接在 Node.js 下运行新微服务前,先切换到 azure-storage 目录并安装依赖:
npm install
清单 4.2 展示了新微服务的代码。在运行此代码之前,让我们先了解其功能。
清单 4.2 - 从 Azure Storage 检索视频的微服务
const express = require("express");
const { BlobServiceClient, StorageSharedKeyCredential } = require("@azure/storage-blob");
const PORT = process.env.PORT;
const STORAGE_ACCOUNT_NAME = process.env.STORAGE_ACCOUNT_NAME;
const STORAGE_ACCESS_KEY = process.env.STORAGE_ACCESS_KEY;
function createBlobService() {
const sharedKeyCredential = new StorageSharedKeyCredential(
STORAGE_ACCOUNT_NAME, STORAGE_ACCESS_KEY
);
const blobService = new BlobServiceClient(
`https://${STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
sharedKeyCredential
);
return blobService;
}
const app = express();
app.get("/video", async (req, res) => {
const videoPath = req.query.path;
const containerName = "videos";
const blobService = createBlobService();
const containerClient = blobService.getContainerClient(containerName);
const blobClient = containerClient.getBlobClient(videoPath);
const properties = await blobClient.getProperties();
res.writeHead(200, {
"Content-Length": properties.contentLength,
"Content-Type": "video/mp4",
});
const response = await blobClient.download();
response.readableStreamBody.pipe(res);
});
app.listen(PORT);
在这里,我们利用 @azure/storage-blob 包(通过 npm 安装的 Azure Storage SDK)和 Express 创建了一个 HTTP 服务器,与第 2 章创建的服务器相同。
在配置这个微服务时,有两个关键的环境变量需要设置:STORAGE_ACCOUNT_NAME 和 STORAGE_ACCESS_KEY,这两个变量用来存储 Azure 存储账户的认证信息。你需要将这些环境变量设置为你自己存储账户的认证详情。这些认证信息会被辅助函数 createBlobService 使用,以创建访问存储 SDK 的 API 对象。
清单 4.2 中的核心是 HTTP GET 请求路径 /video,通过它我们可以从存储中检索视频。该路由处理器会将视频从 Azure 存储直接流式传输到 HTTP 响应中。
独立测试新微服务
在将这个微服务整合到应用程序之前,建议先进行独立测试。虽然可以直接将其整合并测试,但随着应用程序的扩大和复杂化,整体的集成测试将变得更加困难。
在整个应用程序测试前,单独测试微服务可以更有效,因为它允许你快速启动或重新加载单个微服务(适用于开发和测试的周期),而无需对整个应用程序做相同的操作。因此,应该培养先单独测试微服务,再测试整个应用程序的习惯。
在运行(和测试)新微服务之前,你需要配置相关的环境变量。在 macOS、Linux 或 WSL2 终端中,可使用以下命令:
export PORT=3000
export STORAGE_ACCOUNT_NAME=<你的存储账户名称>
export STORAGE_ACCESS_KEY=<你的存储账户的访问密钥>
在 Windows 终端中,命令如下:
set PORT=3000
set STORAGE_ACCOUNT_NAME=<你的存储账户名称>
set STORAGE_ACCESS_KEY=<你的存储账户的访问密钥>
确保使用你之前创建的存储账户的名称和密钥。根据第 2 章的讨论,可以选择以生产模式或开发模式运行微服务。以生产模式运行的命令如下:
cd chapter-4/example-2/azure-storage
npm start
或者,为了实现实时重载,以开发模式运行的命令如下:
npm run start:dev
实时重载在快速开发中非常重要,因为它允许你更改代码并自动重启微服务。在下一章中,你将学习如何将实时重载扩展到整个微服务应用中。目前,让我们在开发和测试单个微服务期间使用它。
当微服务运行时,你可以打开浏览器并访问 http://localhost:3000/video?path=SampleVideo_1280x720_1mb.mp4 。如果你使用了不同的视频名称,需要相应调整此 URL 中的视频名称。现在,你应该能看到视频在播放,这次是从你的 Azure 存储账户流式传输的。
我们将在第 9 章中更深入地讨论微服务测试。现在,让我们继续将这个新微服务整合到应用程序中。
4.4.2 更新视频流微服务
整合新微服务到应用程序的第一步是更新视频流微服务。提醒一下,我们在第 3 章结束时有一个视频流微服务,它从文件系统加载测试视频。现在,我们将更新这个微服务,使它的视频加载任务由新的 azure-storage 微服务承担。
我们通过更新视频流微服务,将存储任务委托给另一个微服务。我们在这里分离关注点,使视频流微服务仅负责向用户流式传输视频,不再涉及存储的具体细节。
清单 4.3 (chapter-4/example-2/video-streaming/src/index.js) 展示了对视频流微服务的更改。阅读清单中的代码,了解我们如何将 HTTP 请求中的视频转发给新的视频存储微服务。
清单 4.3 更新的视频流微服务
const express = require("express");
const http = require("http");
const PORT = process.env.PORT;
const VIDEO_STORAGE_HOST = process.env.VIDEO_STORAGE_HOST;
const VIDEO_STORAGE_PORT = parseInt(process.env.VIDEO_STORAGE_PORT);
const app = express();
app.get("/video", (req, res) => {
const forwardRequest = http.request(
{
host: VIDEO_STORAGE_HOST,
port: VIDEO_STORAGE_PORT,
path: '/video?path=SampleVideo_1280x720_1mb.mp4',
method: 'GET',
headers: req.headers
},
forwardResponse => {
res.writeHead(forwardResponse.statusCode, forwardResponse.headers);
forwardResponse.pipe(res);
}
);
req.pipe(forwardRequest);
});
app.listen(PORT);
在清单 4.3 中,我们利用 Node.js 内置的 HTTP 库将一个微服务的 HTTP 请求转发到另一个微服务,然后将返回的响应流式传输给客户端。这种方式可能初听起来有些复杂,但现在不必过于担心。在下一章中,我们将进一步探讨微服务之间的通信。
注意,我们此时在存储中硬编码了视频的路径。这只是一个临时步骤,我们很快会解决这个问题。同时,为了使这段代码工作,你必须以这个特定的文件名上传了测试视频。如果上传了不同的文件名,请更新代码以匹配该文件名。
更新视频流微服务后,我们应当独立测试它。由于它依赖于视频存储微服务,这可能稍显困难。如果我们拥有模拟依赖的工具和技术,就能更好地完成这一任务。
模拟是一种测试技术,用于用一个假的或模拟的代替品来替换实际的依赖。我们现在还没有这些技术,但这是我们将在第 9 章探讨的内容。你将看到一个模拟微服务的示例。现在,让我们继续完成集成,并检查整个应用程序,虽然目前还很简单,但是否按预期工作。
4.4.3 将新微服务添加到 Docker Compose 文件
至此,我们已经完成了不少工作。我们创建了一个 Azure 存储账户并上传了测试视频,接着开发了第二个微服务 azure-storage,这是一个抽象的存储提供商的 REST API。之后,我们对视频流微服务进行了更新,使其不再直接从文件系统加载视频,而是通过视频存储微服务来检索视频。
注意 Docker Compose 文件的一大优点在于它简化了定义和管理一系列容器的流程。这是管理微服务应用的一个非常便捷的方式!
现在,要将新微服务集成到我们的应用程序中并进行测试,我们需要在 Docker Compose 文件中新增一个部分来定义这个微服务。图 4.13 展示了在添加了第二个微服务和数据库服务器之后,Docker Compose 文件的最新样貌。从图中可以看到,左侧的 Docker Compose 文件被分为三部分,对应右侧的三个容器。

Docker Compose 文件可以看作是一种聚合的 Dockerfile,它用于描述和管理多个容器。这种聚合方式使得将每个微服务的多个 Dockerfile 绑定在一起成为可能。
清单 4.4 (chapter-4/example-2/docker-compose.yaml) 展示了更新后的 Docker Compose 文件,其中添加了 azure-storage 微服务。请注意,我们在第 4.4.1 节中设置的环境变量 STORAGE_ACCOUNT_NAME 和 STORAGE_ACCESS_KEY 正在此处得到重复使用。
清单 4.4 将新微服务添加到 Docker Compose 文件
version: '3'
services:
azure-storage:
image: azure-storage
build:
context: ./azure-storage
dockerfile: Dockerfile
container_name: video-storage
ports:
- "4000:80"
environment:
- PORT=80
- STORAGE_ACCOUNT_NAME=${STORAGE_ACCOUNT_NAME}
- STORAGE_ACCESS_KEY=${STORAGE_ACCESS_KEY}
restart: "no"
video-streaming:
image: video-streaming
build:
context: ./video-streaming
dockerfile: Dockerfile
container_name: video-streaming
ports:
- "4001:80"
environment:
- PORT=80
- VIDEO_STORAGE_HOST=video-storage
- VIDEO_STORAGE_PORT=80
restart: "no"
如果你在新的终端窗口中操作,需要重新设置这些环境变量,然后使用 Docker Compose 来运行更新后的应用程序。
此时,你可能会问:为什么容器名称设置为 video-storage 而不是 azure-storage?尽管我们将微服务命名为 azure-storage,但容器被命名为 video-storage,这是一种有意的抽象。视频流微服务并不关心它从何处检索视频!从微服务的角度看,这些视频同样可以存储在任何其他地方,例如 AWS S3 或 Google Cloud Storage。
通过将容器命名为 video-storage,我们实现了一个与底层存储提供商无关的连接方式。这是一个良好的应用程序架构实践。这样,当我们未来可能更换 azure-storage 为 aws-storage 或 google-storage 时,视频流微服务不需要进行任何修改。这种未来变动的自由度是非常重要的,表明我们正在充分利用微服务架构的优势。
4.4.4 测试更新后的应用程序
我们已经更新了 Docker Compose 文件,以包括两个微服务。现在,我们可以开始启动应用程序并测试新的微服务了。在此之前,请确保根据第 4.4.1 节的说明设置了环境变量 STORAGE_ACCOUNT_NAME 和 STORAGE_ACCESS_KEY,这些是连接到 Azure 存储账户的关键。
在本地计算机上启动整个应用程序的命令如下:
cd chapter-4/example-2
docker compose up --build
与以前不同的是,现在我们启动了两个容器,而不仅仅是一个。以下是一些示例输出:
video-streaming | > [email protected] start /usr/src/app
video-streaming | > node ./src/index.js
video-streaming |
video-storage |
video-storage | > [email protected] start /usr/src/app
video-storage | > node ./src/index.js
video-storage |
video-streaming | Forwarding video requests to video-storage:80.
video-streaming | Microservice online.
video-storage | Serving videos from...
video-storage | Microservice online.
请注意输出中每个容器名称的左侧打印。这是所有容器的聚合日志流,左侧的名称帮助我们区分每个微服务的输出。
注意 我们使用单个命令启动具有多个容器的应用程序,这样能够同时测试多个微服务。
现在我们添加了第二个微服务,正是 Docker Compose 显示其价值的时候。我们也可以在没有 Docker Compose 的情况下启动应用程序,例如:
- 使用两个终端:在一个终端直接使用 Node.js 运行视频流微服务,在另一个终端运行 azure-storage 微服务。这需要两个终端和两个命令。
- 使用 Docker 运行两个容器:在这种情况下,我们必须分别为每个微服务执行
docker build和docker run命令。这需要一个终端和四个命令。
显然,重复输入命令不是一个理想的选择。相反,Docker Compose 允许我们通过单一命令启动应用程序,并可扩展至任意数量的容器。
只需想象,假设应用程序发展到了包含 10 个微服务的阶段。没有 Docker Compose,我们将不得不输入至少 20 个命令来构建和启动应用程序。有了 Docker Compose,只需一个命令就能构建和运行不断增长的应用程序,无论需要多少容器,都只需要一个命令。
此时,我们有两个测试的机会。首先,至少需要测试视频流微服务,因为目前它是唯一面向客户的端点。为此,请打开浏览器并导航至 http://localhost:4001/video 。
再次打开这个页面,你将看到熟悉的测试视频。实际上,测试视频流微服务也相当于同时测试了两个微服务,因为视频流微服务依赖于视频存储微服务。虽然你可以到此为止,但为了全面性测试,我们还应该单独测试视频存储微服务。
回顾一下清单 4.4,我们已经将视频存储微服务的端口设置为 4000。你可以导航到这个端口,并直接从视频存储微服务观看视频。这个微服务期望我们通过 URL 指定视频的存放路径。让我们导航至 http://localhost:4000/video?path=SampleVideo_1280x720_1mb.mp4 并测试视频存储微服务。
请注意,像这样从外部测试内部微服务通常只在开发中可能。一旦这个微服务被部署到生产环境,其 REST API 将只在 Kubernetes 集群内部可用,以保持私密性,因为我们不希望外部直接访问视频存储。这是微服务的一个安全特性,允许我们控制哪些微服务对外公开,以此来限制对于应用程序的直接访问。我们将在第 12 章进一步讨论安全性。
现在,我们已经为应用程序添加了外部文件存储,并在此过程中将其扩展到两个微服务。在庆祝之前,让我们考虑一下设计理论。
4.4.5 云存储与集群存储
此时,如果你对 Kubernetes 有所了解,你可能会问为什么我们没有使用 Kubernetes 卷进行文件存储,而是选择了云存储。这是一个基于项目、业务和客户需求的重要设计决策。
我们选择云存储而不是集群存储,因为它简单、开发效率高、成本低廉且易于管理。这些都是云存储的优点,也是许多公司选择云存储的原因。此外,我们目前还未涉及 Kubernetes,所以在本书的这一部分,我们不使用 Kubernetes 卷。但还有一个更重要的原因:我通常更倾向于使用云存储而不是集群存储。
我们可以在 Kubernetes 集群中存储应用程序的文件和数据,但我更喜欢让生产集群保持无状态,这样可以随时销毁和重建任何集群而不担心丢失数据。稍后,这种设计使我们能够使用蓝绿部署来进行生产部署,这一点我们将在第 12 章中讨论。我们可以轻松构建一个新的、更新的应用程序实例,并与旧版本并行运行。
为了升级客户到新版本,我们可以切换域名系统 (DNS) 记录,使主机名现在指向新实例。这为我们提供了一种低风险的方式来对应用程序进行重大升级。这种方法之所以低风险,不是因为不会出现问题,而是因为如果出现问题,我们可以迅速将 DNS 切换回旧实例,这样客户就可以(几乎立即)回到之前的(并且可能是正常工作的)版本。
4.4.6 我们实现了什么?
恭喜!我们已经成功运行了一个小型的微服务应用程序!这是一个重要的成就。通过使用 Docker Compose,我们建立了一个可以轻松添加新微服务并扩展应用程序的框架。请花一点时间为自己鼓掌——这是一个重要的里程碑!
我们完成了什么?我们为应用程序增加了文件存储功能。现在,我们的微服务具备在外部云存储中保存文件的能力,为应用程序提供了一个托管视频的空间。你可能会好奇,现在我们如何允许用户上传他们自己的视频。虽然我们还未实现视频上传功能,但不用担心,我们将在第 10 章中介绍这一功能。
我们还增加了第二个微服务。利用 Docker Compose,现在可以通过添加新的容器来继续扩展应用程序。我们会在稍后添加数据库服务器时再次利用这一特性。
添加第二个微服务是对存储提供者的一个抽象层。这是一个有益的设计选择。现在我们可以在影响应用程序最小的情况下更换或替换视频存储微服务为其他存储提供者,甚至可以在应用程序运行过程中进行更换!将来,我们可能还会希望同时运行多个存储微服务。如果符合产品需求,我们可以同时升级以支持 Azure Storage、AWS S3 和 Google Cloud Storage!
存储操作的细节已被限制在视频存储微服务内部。这意味着我们可以独立于应用程序的其他部分更改细节,而不会引起连锁反应。这种设计初看可能显得有些多余,但随着应用程序的增长,其重要性将日益凸显。
注意 最终,应用程序将演变成许多微服务之间相互通信的复杂网络。任何一个微服务的变化都可能在整个应用程序中引起连锁反应的问题。通过仔细构建微服务之间的接口来最小化它们的耦合,我们可以最大化微服务架构的优势。
按照功能将微服务分开,这被称为关注点分离(在第 1 章中已提到),这非常重要——每个微服务应负责其自己的职责范围。我们还遵循了单一责任原则(也在第 1 章中提到),即每个微服务只负责一个任务。目前,我们的微服务各司其职:
- 视频流微服务负责向用户流式传输视频。
- 视频存储微服务负责定位和检索存储中的视频。
这样的服务分离有助于确保每个微服务都保持小巧、简洁和易于管理。
4.5 向我们的应用程序添加数据库
数据管理的另一半涉及到数据库。几乎所有应用程序都需要一种数据库来存储动态数据,FlixTube 也不例外。
注意 本章并非旨在教授数据库设计或数据工程;它仅作为如何将 MongoDB 这种数据库与你的微服务集成的示例。
我们首先需要的是每个视频的元数据存储。我们将开始使用数据库来存储每个视频的路径,解决之前在视频流微服务中存在的硬编码视频文件路径的问题。
注意 几乎所有的应用程序都需要某种形式的数据库来存储将由应用程序更新的数据。
图 4.14 展示了我们添加数据库后应用程序的新样子。除了已有的两个微服务的容器外,我们还将添加一个托管 MongoDB 数据库的新容器。从图中可以看到,只有视频流微服务连接到数据库;视频存储微服务则不需要数据库。

4.5.1 为什么选择 MongoDB?
MongoDB 是最受欢迎的 NoSQL 数据库之一,使用 Docker 启动 MongoDB 数据库,我们几乎可以立即在开发中使用它。我们只需要指定数据库镜像的名称,Docker 就会从 Docker Hub 拉取并在我们的开发计算机上实例化它。
注意 MongoDB 易于使用,提供了一个灵活的数据库,可以存储无模式的结构化数据,并具有丰富的查询 API。
那么,为什么选择 MongoDB 而不是其他数据库呢?根据我的经验,即使是手动下载和安装 MongoDB 也比传统数据库要简单;有了 Docker,这一过程变得更加简单。MongoDB 以高性能和极高的可扩展性著称,适用于存储丰富的结构化数据。
我经常处理不可预测的数据,很难预料接下来会遇到什么。MongoDB 不强迫我定义一个固定的模式,这是我非常欣赏的一点。当然,如果你使用对象关系映射(ORM)库,例如 Mongoose( www.npmjs.com/package/mongoose ),也可以在 MongoDB 中定义模式。
MongoDB 还易于在多种编程语言中进行查询和更新,得到良好的支持,拥有丰富的文档和众多示例。作为一个开源数据库,你可以在这里找到其代码: https://github.com/mongodb/mongo 。
4.5.2 在开发中集成数据库服务器
与本章早些时候添加视频存储微服务的方式相同,我们将使用 Docker Compose 在开发环境中集成数据库。现在,在 example-3 项目中,我们将在 Docker Compose 文件中添加一个新的容器来托管一个数据库服务器。我们只需设置一个服务器,但该服务器可以托管多个数据库,为未来添加更多微服务时轻松创建更多数据库做好准备。如图 4.15 所示,example-3 的项目布局与 example-2 大致相同,不同之处在于现在我们增加了一个包含一些将添加到我们数据库中的测试数据的 JSON 文件。

将数据库服务器添加到 Docker Compose 文件中
要将数据库服务器添加到我们的应用程序中,我们只需更新 Docker Compose 文件。Docker Compose 使得向应用程序添加数据库变得非常简单。我们只需要在 Docker Compose 文件中添加几行配置,指定数据库的公共 Docker 镜像并设置一些基本配置,即可瞬间得到一个运行的数据库。
清单 4.5 添加 MongoDB 数据库
version: '3' # 向我们的微服务应用程序添加 MongoDB 数据库服务器
services:
db: # 定义使用的镜像名称和版本。这是从 Docker Hub 检索的公共 MongoDB 镜像。
image: mongo:7.0.0
container_name: db # 定义在我们的应用程序中实例化的容器的名称。我们的微服务将使用这个名称来连接到数据库。
ports:
- "4000:27017" # 将 MongoDB 的标准端口 27017 映射到主机操作系统上的 4000 端口。我们可以通过端口 4000 在开发机上与数据库进行交互。
restart: always # 设置始终重启策略。这能确保如果 MongoDB 发生崩溃,它会自动重新启动。
azure-storage:
image: azure-storage
build:
context: ./azure-storage
dockerfile: Dockerfile
container_name: video-storage
ports:
- "4001:80"
environment:
- PORT=80
- STORAGE_ACCOUNT_NAME=${STORAGE_ACCOUNT_NAME}
- STORAGE_ACCESS_KEY=${STORAGE_ACCESS_KEY}
restart: "no"
video-streaming:
image: video-streaming
build:
context: ./video-streaming
dockerfile: Dockerfile
container_name: video-streaming
ports:
- "4002:3000"
environment:
- PORT=80
- DBHOST=mongodb://db:27017 # 配置微服务以连接到数据库
- DBNAME=video-streaming # 设置微服务用于其数据库的名称
- VIDEO_STORAGE_HOST=video-storage
- VIDEO_STORAGE_PORT=80
restart: "no"
这次更新的视频流微服务已经去除了对视频路径的硬编码。我们现在通过数据库中的 ID 来引用视频文件,虽然看似可以直接通过存储路径来引用视频,但这实际上并不是一个好主意,让我们来看看原因。
假设我们通过路径来定位视频,如果将来需要重新整理文件系统,移动视频至新的位置,就会非常麻烦。因为不仅是视频路径会改变,其他多个数据库和记录也需要更新视频引用,包括一个记录视频信息的元数据数据库,例如视频类型。而且,我们未来还需要一个数据库来记录每个视频的推荐和观看数据。
为了便于这些数据库统一引用视频,最好是只记录每个视频的 ID,这样即便存储路径发生变动,也不会影响到微服务和数据库的运行。
此外,使用视频 ID 代替路径还能简化问题。视频存放路径往往很长,通常我们也不希望这类内部信息泄露出去,因为这可能为潜在的攻击者提供线索。因此,保持这类信息的机密性是至关重要的。
将测试数据加载到我们的数据库中
我们已经在 Docker Compose 文件中添加了数据库支持,并更新了视频流微服务以使用这个数据库。我们即将开始测试这些更新。
在测试更新的代码之前,我们需要先将一些测试数据导入数据库。虽然应用程序最终会允许用户上传自己的视频并填充数据库,但目前还没有实现这一功能。
我们可以用多种方法导入测试数据,例如编写自定义脚本(书中后面会有示例)、使用 MongoDB Shell 或使用 Studio 3T(以前称为 Robo 3T 和 Robomongo),后者是一个出色的 MongoDB 界面工具,非常适合 Windows、macOS 和 Linux 系统。
详细的下载和安装 Studio 3T 的步骤,请访问其官网 https://studio3t.com/ 。通过 Studio 3T,你可以轻松查看、创建和编辑数据库、集合及其记录。
在我们加载示例数据之前,我们需要先启动数据库。如果你还没启动应用程序,请打开终端,执行以下命令:
cd chapter-4/example-3
docker compose up --build
注意 请确保在包含更新后的 Docker Compose 文件的目录下运行这个命令。你可以在第 4 章的 example-3 子目录中找到这个文件。
启动应用后,我们的 MongoDB 数据库服务器将在容器中运行。因为我们已将 MongoDB 的 27017 端口映射到开发机的 4000 端口,你现在可以通过访问 localhost:4000 来连接数据库。
现在,让我们加载一些测试数据。你可以通过 Studio 3T 创建一个名为 video-streaming 的数据库,添加一个名为 videos 的集合,并使用以下 JSON 文档插入数据:
清单 4.7 使用 Studio 3T 加载数据记录
{
"_id": { "$oid": "5d9e690ad76fe06a3d7ae416" },
"videoPath": "SampleVideo_1280x720_1mb.mp4"
}
第 9 章我们将继续讨论模拟和数据库固件。现在,让我们看看如何测试应用程序。
注意 如果你想进一步学习,也可以通过 MongoDB Shell 从终端加载数据库固件。这是一种强大的方式来操作数据库中的数据。你可以在 www.mongodb.com/docs/mongodb-shell/ 上了解更多关于 MongoDB Shell 的信息。
测试我们更新后的应用程序
此时,如果你愿意,可以先在 Node.js 环境下单独测试微服务。在集成之前单独测试每个微服务总是一个好习惯。如果你按照指南操作并在 Node.js 环境下进行测试,请记得安装 mongodb 驱动包:
npm install --save mongodb
为了简化流程,我们将跳过单独测试微服务的步骤,直接在 Docker Compose 环境下运行集成代码。
你应该已经启动了应用程序。我们需要它来将测试数据加载到数据库中。如果还没有运行,请现在启动它:
cd chapter-4/example-3
docker compose up --build
现在,你可以像往常一样使用网络浏览器来测试应用程序。这次,我们需要提供想要观看的视频的 ID。在测试数据中指定的 ID 是一个长数字串,我们需要将它添加到 URL 中来测试更新后的应用程序。打开你的浏览器,导航到此 URL: http://localhost:4002/video?id=5d9e690ad76fe06a3d7ae416 。如果你更改了测试数据中的 ID,同样需要在此 URL 中进行更新。现在你应该可以看到测试视频在播放了。到目前为止,你应该对这个视频已经很熟悉了!
4.5.3 在生产环境中部署数据库服务器
目前为止,我们讨论的都是在应用程序的开发环境中添加数据库服务器。这对当前阶段足够了,因为我们还未涉及如何将应用程序部署到生产环境,这将在第 6、7 和 8 章详细探讨。不过,现在我们可以简要探讨一下在生产环境中部署数据库服务器的方法。
虽然 Docker Compose 让我们在开发阶段轻松地集成数据库服务器,但生产环境又应该如何处理呢?对于生产环境,我推荐使用位于 Kubernetes 集群外部的数据库。这种做法保持了集群的无状态特性,正如我们在 4.4.5 节所讨论的,这意味着我们可以随时重建集群而不会对数据造成任何风险。
建立生产环境的 Kubernetes 集群后,我们可以像使用 Docker Compose 那样轻松部署 MongoDB 数据库。实际上,我们将在第 6 章具体实现这一点,因为这是将数据库服务器融入生产环境中最简单的方法。
此外,我还推荐将数据库从集群中独立出来。你可以选择在单独的虚拟机上运行它,或者更好的选择是,使用外部托管的数据库服务。将数据库独立出来主要是为了维护生产环境集群的无状态性,这样就可以不用担心数据丢失的问题,同时还能并行运行应用程序的多个版本。
选择托管数据库服务还有其他优点,比如安全性。数据库服务提供商将负责管理和维护,包括数据的保护和备份。如果我们在大型企业工作,可能内部就有相应的管理措施;但如果我们在小型企业或初创公司工作,获取尽可能多的外部帮助会非常有益。
4.5.4 每个微服务一个数据库还是整个应用一个数据库?
到目前为止,我们的数据库服务器只配置了一个数据库。但现在我们已经准备好可以创建更多的数据库。
你可能已经注意到,我们将数据库命名为“video-streaming”,与使用它的微服务名称相对应。这表明了我们整本书将遵循的原则:每个微服务应有自己的数据库。这样做是为了在微服务中封装数据,类似于面向对象编程(OOP)中的数据封装。
那我们真的需要为每个微服务配置一个独立的数据库吗?坚持这一原则是非常有价值的。尽管你的数据库可能托管在同一服务器上,但请确保每个微服务都拥有自己的数据库实例。如果你选择共享数据库或让数据库成为微服务间的集成点,那么你就可能引入架构和可扩展性问题。
我们通过限制数据的访问——只让封装数据的代码访问数据,从而帮助我们在未来安全地调整数据结构,因为数据的改动可以在微服务内部解决。如果我们精心设计 REST API,这将是另一种避免在应用程序的其他部分引起连锁反应和问题的技术。在设计微服务时所投入的精力相当于对更优秀的应用程序架构的投资。
你可能认为在微服务间共享数据库是一种便捷的数据共享方式。但把数据库用作微服务间的集成点或接口其实是个坏主意,因为这会增加应用程序的脆弱性,减少扩展性。这也限制了我们发展独立微服务的能力。
有时候,你可能会因为性能或其他原因考虑共享数据库。在一些复杂的情况下,打破规则有时是必须的。但在我们的应用程序中引入这种反模式之前,需要仔细考虑这样做的原因和必要性。我们将在第 12 章更多地探讨数据库和扩展性问题。
4.5.5 我们实现了什么?
我们已为应用程序集成了数据库服务。现在,我们有两种不同的方法来管理应用数据:通过外部云存储来保存文件,通过数据库来存储数据。我们有效利用 Docker Compose 管理了由多个容器组成的应用程序,并将应用升级为包含两个微服务和一个数据库。
我们在一个名为视频存储的微服务后设置了一个抽象层,这个微服务的任务是从存储中检索视频。这种设置允许我们未来更换存储提供者,而不会对应用程序造成重大干扰。
我们创建了一个数据库服务器,并为视频流微服务设置了一个数据库。我们遵守了每个微服务应有自己的数据库的原则,并且未来可以在服务器上轻松添加更多数据库来继续遵循这一规则。
我们还简要地看到了一个微服务如何与另一个微服务进行通信。视频流微服务收到的 HTTP GET 请求被转发到视频存储微服务。这是微服务间进行请求或任务委托的一种简单而有效的通信方式。在下一章中,我们将深入探讨这种以及其他微服务间的通信方式。此外,你还将扩展你对 Docker Compose 的技能,并学习如何实现实时重载,使其适用于整个应用程序。
4.6 Docker Compose 回顾
在本章中,我们更深入地了解了 Docker Compose 的价值,它帮助我们管理在开发机上不断增长的应用程序的复杂性。即使是运行单个容器,Docker Compose 也很有用,因为它帮助我们捕获并记录配置细节。随着我们逐步增加容器数量,Docker Compose 的价值更加显著。我们可以向应用程序添加任意数量的容器,记录所有容器的配置细节,无论容器有多少,都可以通过单一命令来集中管理。
图 4.16 展示了在 Docker Compose 环境下应用程序的生命周期。我们使用up命令启动应用程序及其所有微服务,并用down命令来销毁应用程序,将开发计算机恢复到初始状态。

在结束本章之前,请参考表 4.2,快速回顾一下你学到的 Docker Compose 命令。当你需要使用 Docker Compose 时,可以回到这里寻找帮助。
表 4.2 Docker Compose 命令回顾
| 命令 | 描述 |
|---|---|
docker-compose up --build | 构建并启动由多个容器组成的应用程序。 |
docker-compose ps | 列出由 Docker Compose 文件指定的应用程序的运行容器。 |
docker-compose stop | 停止应用程序中的所有容器,但保留停止的容器以供检查。 |
docker-compose down | 停止并销毁应用程序,使开发计算机处于干净状态。 |
4.7 深入拓展你的学习
本章仅初步探索了两个重要主题:我们为应用程序引入了一个新的微服务,并将其连接到了 Azure Storage 账户。同时,我们也为应用程序集成了 MongoDB 数据库。Azure 和 MongoDB 都是各自领域的庞大技术生态。我们将在第 6、7 和 10 章进一步深入探讨 Azure,现在我将为你提供一些资源,帮助你在这些技术领域进行更深入的学习:
- 《Microsoft Azure 实战》作者 Lars Klint(Manning,预计 2024 年春季出版)
- 《学习 Azure 的午餐时间,第二版》,作者 Iain Foulds(Manning, 2020)
- 《MongoDB 实战,第二版》作者 Kyle Banker, Peter Bakkum 等(Manning, 2016)
若想了解更多关于 JavaScript 数据处理的知识,可以参考我的早期著作:
- 《用 JavaScript 处理数据》作者 Ashley Davis(Manning, 2018)
关于 Docker Compose 的更多信息,请参阅在线文档:
- Docker Compose 官方文档: https://docs.docker.com/compose/
- Compose 文件说明: https://docs.docker.com/compose/compose-file/
- Compose 命令参考: https://docs.docker.com/compose/reference/
在本章中,我们使用 Docker Compose 升级了应用程序至多个微服务,并增加了数据管理功能。在接下来的章节中,你将详细学习微服务间如何进行通信。你还将进一步提升你的 Docker Compose 技能,并学习如何实现实时重新加载功能,使其覆盖整个应用程序。
总结
- Docker Compose 是 Docker 的一个子命令,它使我们能够通过编写脚本来构建和运行多个容器。这是一种在开发和测试阶段模拟 Kubernetes 并运行多微服务应用的便捷方式。
- Docker Compose 文件是一种配置由多个 Docker 容器组成的分布式应用程序的脚本。它整合了一组 Dockerfiles。
- 命令
docker compose up --build用于构建并启动分布式应用程序。 - 命令
docker compose down用于关闭应用程序。 - 云文件存储是一种在云平台(如 Azure、AWS 或 GCP)提供的服务中存储应用程序文件的方法。
- Azure Storage 是 Microsoft Azure 提供的云文件存储服务。
- MongoDB 是一种易于使用、灵活且可扩展的数据库解决方案,非常适合微服务架构。
- 在开发阶段,我们可以在 Docker Compose 环境下轻松实例化一个 MongoDB 服务器,并在所有运行中的微服务之间共享它。
- 在微服务架构中,虽然可以共享数据库服务器,但我们应尽量遵循每个微服务只使用一个数据库的规则。优秀的微服务应在其 REST API 后封装其数据库,不与任何其他服务共享数据。因此,为了低成本和维护的便利,尽管只需一个数据库服务器,但应确保每个微服务在该服务器上都拥有自己的数据库实例。
- 虽然在 Kubernetes 集群中可以存储文件和运行数据库,但对于生产环境,我们更倾向于拥有一个无状态的集群。将文件和数据保持在集群外不仅更安全、更灵活,也意味着我们可以在不担心丢失文件和数据的情况下轻松地销毁和重建集群。