本文是“Microservices June 微服务之月 2023”系列教程之一,旨在帮助您将概念付诸实践。
7 月 1 日前免费注册线上教学项目 NGINX 微服务之月,并在 8 月 1 日前按要求完成课程,即可获得 NGINX 独家纪念礼品以及结课证书。
本文末尾包括本实验的验收标准,想要获取礼品和证书的同学,请在 8 月 1 日前随单元小测提交实验结果。
本系列教程包括:
- 如何部署和配置微服务(本文)
- 如何安全地管理容器中的 Secrets
- 如何利用 Docker、Kubernetes 和 Gitlab 实现微服务自动化部署和 CI/CD
- 如何借助可观测性管理混沌而复杂的微服务
所有应用都需要配置,但配置微服务时的考虑因素可能与配置单体应用时不同。我们可以参考十二要素应用的要素 3(将配置存储在环境中)获取适用于这两种应用的指导,不过这些指导可以针对微服务应用进行调整。具体而言,我们能够调整我们定义服务配置的方式,为服务提供配置,并将服务作为配置值提供给其他可能依赖它的服务。
如欲从概念上了解如何针对微服务调整要素 3,特别是有关配置文件、数据库和服务发现的最佳实践,请阅读我们的博文《微服务应用配置最佳实践》。本文可帮助您学以致用。
注:我们旨在通过本教程阐释一些核心概念,而非展示如何在生产环境中正确部署微服务。虽然我们会用到真正的“微服务”架构,但需要做以下几点说明:
- 本教程没有使用 Kubernetes 或 Nomad 等容器编排框架。这可确保您在学习微服务概念时不会被某个框架的具体细节所困扰。本文介绍的模式可移植到运行这些框架的系统中。
- 服务为方便理解(而非软件工程的严谨性)做了优化,请重点关注服务在系统中的作用及其通信模式,而非代码细节。如欲了解更多信息,请查看各个服务的 README 文件。
教程概述
本教程介绍了要素 3 概念如何应用于微服务应用。您将通过四个挑战探究一些常见的微服务配置模式,并使用这些模式部署和配置服务:
-
在挑战 1 和挑战 2 中,您将了解第一种模式,这涉及到微服务应用的配置位置。现有三个典型位置:
- 应用代码
- 应用的部署脚本
- 部署脚本访问的外部来源
- 在挑战 3 中再设置两种模式:通过作为反向代理的 NGINX 将应用暴露给外界,并使用 Consul 实现服务发现。
- 在挑战 4 中实施最后一个模式:将一个微服务实例用作“作业运行程序”,执行不同于其常用功能的一次性操作(此处为模拟数据库迁移)。
本教程用到了以下四种技术:
- Messenger ——一个简单的聊天 API,具有消息存储功能,专为本教程而创建。
- NGINX 开源版——通往 Messenger 服务和整个系统的入口点
- Consul——动态服务注册表和键值存储
- RabbitMQ——一个常用的开源消息代理,支持服务异步通信
请观看课程回放,简单了解本教程。虽然具体步骤与本文并非完全一致,但有助于概念的理解。
准备工作和设置
准备工作
若在自己的环境中完成本教程的学习,您需要:
- 一个兼容 Linux/Unix 的环境
- 基本了解 Linux 命令行、JavaScript 和
bash
(本教程会提供并解释所有代码和命令,因此即使您知识有限也无妨) - Docker 和 Docker Compose
-
Node.js 19.x 或更高版本
curl
(已安装在大多数系统上)- 教程概述提到了四种技术:Messenger(将在下一节中下载)、NGINX 开源版、Consul 和 RabbitMQ。
设置
- 开启终端会话(在后面的介绍中被称作应用终端)。
-
在主目录下,创建 mj-2023-unit1 目录,并将本教程会用到的 GitHub 代码库复制到其中。(您也可以使用其他目录名称,相应修改指令即可)。
注:本教程中省略了 Linux 命令行提示符,以便您将命令复制和粘贴到终端。
mkdir ~/mj-2023-unit1 cd ~/mj-2023-unit1 git clone https://jihulab.com/microservices-june-2023-unit1/platform.git --branch main git clone https://jihulab.com/microservices-june-2023-unit1/messenger.git --branch main
-
切换到平台代码库并启动 Docker Compose:
cd platform docker compose up -d --build
这将同时启动 RabbitMQ 和 Consul——两者将在后面的挑战中用到。
-d
标记指示 Docker Compose 在容器启动时与之分离(否则容器将始终与您的终端保持连接)。--build
标记指示 Docker Compose 在启动时重建所有镜像。这可确保您正在运行的镜像通过任何潜在的文件变更保持更新。
-
切换到 Messenger 代码库并启动 Docker Compose:
cd ../messenger docker compose up -d --build
这将为 Messenger 服务启动 PostgreSQL 数据库,下面我们称之为 messenger-database。
挑战 1:定义应用层微服务配置
在此挑战中,您需要在本教程中介绍的三个位置中的第一个位置配置:应用层。(挑战 2 将涉及到第二个和第三个位置:部署脚本和外部来源。)
十二要素应用特别排除了应用层配置,因为该配置无需在不同的部署环境(十二要素应用称之为部署)之间进行切换。尽管如此,为了完整起见,我们还是涵盖了所有三种类型。在开发、构建和部署服务的过程中,您需要以不同的方式处理每种类型。
Messenger 服务使用 Node.js 编写而成,入口点位于 Messenger 代码中的 app/index.mjs。文件中的这一行:
app.use(express.json());
是应用层配置的一个示例。它通过配置 Express 框架将 application/json
类型的请求正文反序列化为 JavaScript 对象。
该逻辑与应用代码紧密耦合,但不是十二要素应用所认为的“配置”。不过,在软件中一切都视情况而定,不是吗?
在接下来的两节中,您需要修改该行以实施两个应用层配置示例。
示例 1
在此示例中,设置 Messenger 服务可接受的请求正文的最大大小。该大小限值通过 express.json 函数的 limit
参数进行设置,请参见 Express API 文档。此处,您要把 limit
参数添加到上述 Express 框架的 JSON 中间件配置中。
-
在您常用的文本编辑器中,打开 app/index.mjs 并将:
app.use(express.json())
替换为:
app.use(express.json({ limit: "20b" }));
-
在应用终端(您在“设置”中使用的),切换到应用目录并启动 Messenger 服务:
cd app npm install node index.mjs messenger_service listening on port 4000
-
开启第二个终端会话(在后面的指令中被称作客户端终端),并向 Messenger 服务发送
POST
请求。错误消息表明,该请求已被成功处理,虽然请求正文的大小不到第一步中设置的 20 字节上限,但 JSON 有效载荷的内容不正确:curl -d '{ "text": "hello" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations ... { "error": "Conversation must have 2 unique users" }
-
发送稍长的消息正文(同样在客户端终端)。输出远多于第三步中的输出,其中一条错误消息提示此次请求正文的大小超过了 20 个字节:
curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations ... \”PayloadTooLargeError: request entity too large"
示例 2
本示例使用了 convict
——一个支持您在单个文件中定义整个配置“模式”的库。它还阐明了十二要素应用中要素 3 的两条准则:
- 将配置存储在环境变量中——您需要修改应用,以便使用环境变量 (
JSON_BODY_LIMIT
) 设置正文最大大小,而非在应用代码中进行硬编码。 - 明确定义服务配置——这是要素 3 针对微服务的调整。如果您不熟悉这个概念,我们建议您花点时间阅读我们的博文“微服务应用配置最佳实践”。
该示例还设置了一些您会在挑战 2 中用到的“管道”:为演示在部署脚本中指定配置,您将在此挑战中创建的 Messenger 部署脚本会设置将插入到此处应用代码中的 JSON_BODY_LIMIT
环境变量。
-
打开
convict
配置文件 app/config/config.mjs,并在amqpport
密钥之后添加以下内容作为新密钥:jsonBodyLimit: { doc: `The max size (with unit included) that will be parsed by the JSON middleware. Unit parsing is done by the https://www.npmjs.com/package/bytes library. ex: "100kb"`, format: String, default: null, env: "JSON_BODY_LIMIT", },
当您在下面的第三步中使用
JSON_BODY_LIMIT
环境变量在命令行上设置正文最大大小时,convict
库负责解析 JSON_BODY_LIMIT 环境变量:- 从正确的环境变量中提取值
- 检查变量的类型 (
String
) - 允许在应用中通过
jsonBodyLimit
键对其进行访问
-
在 app/index.mjs 中,将:
app.use(express.json({ limit: "20b" }));
替换为
app.use(express.json({ limit: config.get("jsonBodyLimit") }));
-
在应用终端(在示例 1 的第二步中您在此启动了 Messenger 服务),按下
Ctrl+c
停止该服务。然后,再启动该服务,使用JSON_BODY_LIMIT
环境变量将正文最大大小设置为 27 字节:^c JSON_BODY_LIMIT=27b node index.mjs
以上示例展示了如何动态修改配置——您已从在应用代码中硬编码一个值(此处为大小限制)转为使用环境变量对其进行设置,正如十二要素应用所推荐的。
如上所述,在挑战 2 中,
JSON_BODY_LIMIT
环境变量的使用将成为第二个配置位置的示例,此时您使用 Messenger 服务的部署脚本来设置环境变量,而不是在命令行上进行设置。 -
在客户端终端,重复示例 1 第四步中的
curl
命令(使用更大的请求正文)。因为您现在已将大小上限增加到 27 字节,因此请求正文不会再超过限值,您将收到错误消息——请求已被处理,但 JSON 有效载荷的内容不正确:curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations { "error": "Conversation must have 2 unique users" }
您可以根据需要关闭客户端终端。在本教程的其余部分中,您将在应用终端发出所有命令。
-
在应用终端,按下
Ctrl+c
停止 Messenger 服务(在上面第三步,您已在该终端停止并重启此服务)。^c
-
停止 messenger-database。您可以安然地忽略显示的错误消息,因为平台代码库中定义的基础架构元素仍在使用网络。在 Messenger 代码库的根目录下运行此命令。
docker compose down ...failed to remove network mm_2023....
挑战 2:为服务创建部署脚本
乍一看,您可能会误认为是“不要将配置签入源代码控制”。在此挑战中,您将为微服务环境实施一个通用模式,它看似违反这一规则,但实际上在遵守该规则的前提下提供了对微服务环境至关重要的流程改进。
在这个挑战中,您将创建部署脚本来模拟基础架构即代码以及为微服务提供配置的部署清单的功能,修改脚本以使用外部配置来源,设置密钥,然后运行脚本来部署服务及其基础架构。
您要在 Messenger 代码库的新建基础架构目录中创建部署脚本。名为“基础架构”(或该名称的一些变体)的目录是现代微服务架构中的一种常见模式,用于存储以下内容:
- 基础架构即代码(例如 Terraform、AWS CloudFormation、Google Cloud Deployment Manager 和 Azure Resource Manager)
- 容器编排系统的配置(例如 Helm 图表和 Kubernetes 清单)
- 与应用部署相关的任何其他文件
该模式的优势包括:
- 它将服务部署和服务特定基础架构(如数据库)部署的所有权分配给了拥有服务的团队。
- 该团队可确保对其中任何元素的变更都经过其开发流程(代码审查、CI 等)。
- 该团队可轻松更改服务及其支持基础架构的部署方式,而不依赖外部团队的帮助。
如前所述,我们编写本教程的目的并非演示如何构建真正的系统,而且您在此挑战中部署的脚本也不像真正的生产系统。更确切地说,本教程介绍了在处理微服务相关基础架构部署时的一些核心概念和工具、特定配置所解决的问题,同时还将脚本抽象为尽可能少的特定工具。
创建初始部署脚本
-
在应用终端,在 Messenger 代码库的根目录下创建一个基础架构目录,并创建文件以包含 Messenger 服务和 messenger-database 的部署脚本。(根据具体环境,您可能需要在
chmod
命令前添加sudo
前缀:mkdir infrastructure cd infrastructure touch messenger-deploy.sh chmod +x messenger-deploy.sh touch messenger-db-deploy.sh chmod +x messenger-db-deploy.sh
-
在您常用的文本编辑器中,打开 messenger-deploy.sh 并添加以下内容,从而为 Messenger 服务创建初始部署脚本:
#!/bin/bash set -e JSON_BODY_LIMIT=20b docker run \ --rm \ -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \ messenger
这个脚本目前尚不完整,但说明了几个概念:
- 它通过直接在部署脚本中引用该配置为环境变量赋值。
- 它在
docker
run
命令上使用-e
标记,在运行时将环境变量注入容器。
似乎没有必要这样设置环境变量的值,但这意味着无论这个部署脚本变得多么复杂,您只需快速浏览下脚本的开头,就能知道配置数据是如何提供给部署的。
此外,虽然实际部署脚本可能不会明确调用 docker
run
命令,但这个示例脚本展示了 Kubernetes 清单等如何解决核心问题。当使用 Kubernetes 等容器编排系统时,部署会启动容器,而且衍生自 Kubernetes 配置文件的应用配置会被提供给该容器。因此,我们可以将此示例部署文件视为部署脚本的最小版本,其作用与框架特定的部署文件(如 Kubernetes 清单)相同。
在实际开发环境中,您可能会将此文件签入源代码控制并对其执行代码审查。这允许团队的其他成员对设置进行注释,有助于避免错误配置的值引发意外行为。例如,在这张截图中,一位团队成员正确地指出,传入 JSON 请求正文的 20 字节上限(用 JSON_BODY_LIMIT
设置)太低。
修改部署脚本以从外部源查询配置值
在这部分的挑战中,您要为微服务的配置设置第三个位置,即在部署时查询的外部源。与硬编码值相比,最好动态注册值并在部署时从外部来源获取这些值,因为硬编码值必须不断更新,并可能引发故障。有关讨论,请阅读我们的博文“微服务应用配置最佳实践”。
此时,有两个基础架构组件在后台运行,提供 Messenger 服务所需的辅助服务:
App/config/config.mjs 中 Messenger 服务的 convict
模式定义了与这些外部配置部分相对应的必要环境变量。在这一节中,您要对这两个组件进行设置以提供配置,即在通常可访问的位置设置变量的值,以便 Messenger 服务在部署时可以查询到它们。
RabbitMQ 和 messenger-database 所需的连接信息被注册在 Consul 键/值 (KV) 存储中——这是所有服务在部署时均可访问的一个通用位置。Consul KV 存储并非存储这类数据的标准位置,本教程为了简单起见才使用该位置。
-
将 infrastructure/messenger-deploy.sh(在上一节的第二步中创建)的内容替换为以下内容:
#!/bin/bash set -e # 此配置需要新的提交才能更改 NODE_ENV=production PORT=4000 JSON_BODY_LIMIT=100kb # 通过从 # 系统中提取信息进行 Postgres 数据库配置 POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true) PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true) PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true) # 通过从系统中提取信息进行 RabbitMQ 配置 AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true) AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-port?raw=true) docker run \ --rm \ -e NODE_ENV="${NODE_ENV}" \ -e PORT="${PORT}" \ -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \ -e PGUSER="${POSTGRES_USER}" \ -e PGPORT="${PGPORT}" \ -e PGHOST="${PGHOST}" \ -e AMQPPORT="${AMQPPORT}" \ -e AMQPHOST="${AMQPHOST}" \ messenger
此脚本举例说明了两种类型的配置:
- 部署脚本中直接指定配置——它设置了部署环境 (
NODE_ENV
) 和端口 (PORT
),并将JSON_BODY_LIMIT
更改为 100 KB,该值比 20 字节更切合实际。 - 从外部源查询配置——它从 Consul KV 存储中获取
POSTGRES_USER
、PGPORT
、PGHOST
、AMQPHOST
和AMQPPORT
环境变量的值。通过以下两个步骤设置 Consul KV 存储中环境变量的值。
- 部署脚本中直接指定配置——它设置了部署环境 (
-
打开 messenger-db-deploy.sh 并添加以下内容,从而为 messenger-database 创建初始部署脚本。
#!/bin/bash set -e PORT=5432 POSTGRES_USER=postgres docker run \ -d \ --rm \ --name messenger-db \ -v db-data:/var/lib/postgresql/data/pgdata \ -e POSTGRES_USER="${POSTGRES_USER}" \ -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \ -e PGPORT="${PORT}" \ -e PGDATA=/var/lib/postgresql/data/pgdata \ --network mm_2023 \ postgres:15.1 # 向 Consul 注册有关数据库的详细信息 curl -X PUT http://localhost:8500/v1/kv/messenger-db-port \ -H "Content-Type: application/json" \ -d "${PORT}" curl -X PUT http://localhost:8500/v1/kv/messenger-db-host \ -H "Content-Type: application/json" \ -d 'messenger-db' # 这与上面的 "--name"标记相匹配 # (主机名) curl -X PUT http://localhost:8500/v1/kv/messenger-db-application-user \ -H "Content-Type: application/json" \ -d "${POSTGRES_USER}"
除了定义在部署时可由 Messenger 服务查询的配置以外,该脚本还说明了与“创建初始部署脚本”中的 Messenger 服务初始脚本相同的两个概念)。
- 它直接在部署脚本中指定某些配置,本例中是向 PostgreSQL 数据库告知所要运行的端口以及默认用户的用户名。
- 它运行带
-e
标记的 Docker,以在运行时将环境变量注入容器。它还将运行中容器的名称设置为 messenger-db,这将成为您在“设置”第二步中启动平台服务时创建的 Docker 网络中的数据库的主机名。
-
在实际部署中,通常是平台团队(或类似团队)在平台代码库中处理 RabbitMQ 等服务的部署和维护,正如您在 Messenger 代码库中对 messenger-database 执行的操作。然后,平台团队确保该基础架构的位置可被依赖于它的服务发现。在本教程中,自行设置 RabbitMQ 值:
curl -X PUT --silent --output /dev/null --show-error --fail \ -H "Content-Type: application/json" \ -d "rabbitmq" \ http://localhost:8500/v1/kv/amqp-host curl -X PUT --silent --output /dev/null --show-error --fail \ -H "Content-Type: application/json" \ -d "5672" \ http://localhost:8500/v1/kv/amqp-port
(您可能想知道为什么使用
amqp
来定义 RabbitMQ 变量,这是因为 AMQP 是 RabbitMQ 使用的协议)。
在部署脚本中设置密钥
Messenger 服务的部署脚本中只缺少一个(关键)数据,即 messenger-database 的密码!
注:密钥管理不是本教程的重点内容,所以为了简单起见,在部署文件中定义密钥。切勿在实际开发、测试或生产环境中这样做,因为这存在巨大的安全风险。
如欲了解正确的密钥管理,请阅读“Microservices June 微服务之月 2023 第二单元:微服务 Secretss 管理与配置基础入门”。(提示:密钥管理工具是唯一真正安全的密钥存储方法)。
-
将 infrastructure/messenger-db-deploy.sh 的内容替换为以下内容,并将 messenger-databasee 的密码密钥存储在 Consul KV 存储中:
#!/bin/bash set -e PORT=5432 POSTGRES_USER=postgres # 注:切勿在真实部署中这样做。 Store passwords # 只能在加密的密钥存储中。 POSTGRES_PASSWORD=postgres docker run \ --rm \ --name messenger-db-primary \ -d \ -v db-data:/var/lib/postgresql/data/pgdata \ -e POSTGRES_USER="${POSTGRES_USER}" \ -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \ -e PGPORT="${PORT}" \ -e PGDATA=/var/lib/postgresql/data/pgdata \ --network mm_2023 \ postgres:15.1 echo "Register key messenger-db-port\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-port \ -H "Content-Type: application/json" \ -d "${PORT}" echo "Register key messenger-db-host\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-host \ -H "Content-Type: application/json" \ -d 'messenger-db-primary' # 这与上面的“--name”标记相匹配 # 在我们的设置中,这代表主机名 echo "Register key messenger-db-application-user\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-application-user \ -H "Content-Type: application/json" \ -d "${POSTGRES_USER}" echo "Register key messenger-db-password-never-do-this\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \ -H "Content-Type: application/json" \ -d "${POSTGRES_PASSWORD}" printf "\nDone registering postgres details with Consul\n"
-
将 infrastructure/messenger-deploy.sh 的内容替换为以下内容,以便从 Consul KV 存储中获取 messenger-database 密码密钥:
#!/bin/bash set -e # 此配置需要新的提交才能更改 NODE_ENV=production PORT=4000 JSON_BODY_LIMIT=100kb # 通过从 # 系统中提取信息进行 Postgres 数据库配置 POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true) PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true) PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true) # 注:切勿在真实部署中这样做。 Store passwords # 只能在加密的密钥存储中。 PGPASSWORD=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true) # 通过从系统中提取信息进行 RabbitMQ 配置 AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true) AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-port?raw=true) docker run \ --rm \ -d \ -e NODE_ENV="${NODE_ENV}" \ -e PORT="${PORT}" \ -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \ -e PGUSER="${POSTGRES_USER}" \ -e PGPORT="${PGPORT}" \ -e PGHOST="${PGHOST}" \ -e PGPASSWORD="${PGPASSWORD}" \ -e AMQPPORT="${AMQPPORT}" \ -e AMQPHOST="${AMQPHOST}" \ --network mm_2023 \ messenger
运行部署脚本
-
切换到 Messenger 代码库的 app 目录,并为 Messenger 服务构建 Docker 镜像:
cd ../app docker build -t messenger .
-
验证是否只有属于平台服务的容器正在运行:
docker ps --format '{{.Names}}' consul-server consul-client rabbitmq
-
切换到 Messenger 代码库的根目录,并部署 messenger-database 和 Messenger 服务:
cd .. ./infrastructure/messenger-db-deploy.sh ./infrastructure/messenger-deploy.sh
messenger-db-deploy.sh 脚本启动 messenger-database,并在系统(此处为 Consul KV 存储)中注册相应的信息。
然后,messenger-deploy.sh 脚本启动应用,并从系统(还是 Consul KV 存储)中提取 messenger-db-deploy.sh 注册的配置。
提示:如果容器启动失败,则移除部署脚本中
docker
run
命令(-d
\
行)的第二个参数,并重新运行脚本。然后,该容器在前台启动,这意味着它的日志将出现在终端中,并可能会识别问题。在解决这个问题时,还原-d
\
行,以便实际容器在后台运行。 -
向应用发送简单的健康检查请求,以验证部署是否成功:
curl localhost:4000/health curl: (7) Failed to connect to localhost port 4000 after 11 ms: Connection refused
糟糕,失败!事实证明,还有一项关键配置未执行,即未将 Messenger 服务暴露在整个系统中。它在 mm_2023 网络内正常运行,但该网络只能从 Docker 内部访问。
-
停止正在运行的容器,以准备在下一个挑战中创建新镜像。
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))
挑战 3:将服务暴露给外界
在生产环境中,您通常不会直接暴露服务。相反,您需要遵循常见的微服务模式,将反向代理服务部署在主服务的前面。
在这个挑战中,您要通过设置服务发现,将 Messenger 服务暴露给外界:注册新的服务信息,并在其他服务访问时动态更新这些信息。为此,您会用到以下技术:
- Consul,一个动态服务注册表;Consul 模板,一套基于 Consul 数据动态更新文件的工具
- NGINX 开源版,作为反向代理和负载均衡器,为您的 Messenger 服务(将由在容器中运行的应用的多个单独实例组成)提供单个入口点
如欲了解有关服务发现的更多信息,请阅读我们的博文“微服务应用配置最佳实践”之“将服务作为配置提供”。
设置 Consul
Messenger 代码库中的 app/consul/index.mjs 文件包含在启动时向 Consul 注册 Messenger 服务以及在正常关闭时进行注销所需的全部代码。它暴露了一个函数 register
,该函数会将任何新部署的服务都注册到 Consul 的服务注册表。
-
在您常用的文本编辑器中,打开 app/index.mjs 并在其他导入语句后添加以下代码片段,以便从 app/consul/index.mjs 中导入
register
函数:import { register as registerConsul } from "./consul/index.mjs";
然后,修改脚本末尾的
SERVER
START
部分(如图所示),以在应用启动后调用registerConsul()
:/* ================= SERVER START ================== */ app.listen(port, async () => { console.log(`messenger_service listening on port ${port}`); registerConsul(); }); export default app;
-
在 app/config/config.mjs 中打开
convict
模式并在示例 2 的第一步中添加的jsonBodyLimit
键之后添加以下配置值。consulServiceName: { doc: "The name by which the service is registered in Consul. If not specified, the service is not registered", format: "*", default: null, env: "CONSUL_SERVICE_NAME", }, consulHost: { doc: "The host where the Consul client runs", format: String, default: "consul-client", env: "CONSUL_HOST", }, consulPort: { doc: "The port for the Consul client", format: "port", default: 8500, env: "CONSUL_PORT", },
这配置了新注册服务的名称,并定义了 Consul 客户端的主机名和端口。下一步,您要修改 Messenger 服务的部署脚本,以引用这个新的 Consul 连接和服务注册信息。
-
打开 infrastructure/messenger-deploy.sh 并将其内容替换为以下内容,以便在 Messenger 服务配置中添加您在上一步中设置的 Consul 连接和服务注册信息:
#!/bin/bash set -e # 此配置需要新的提交才能更改 NODE_ENV=production PORT=4000 JSON_BODY_LIMIT=100kb CONSUL_SERVICE_NAME="messenger" # 由于我们无法在一无所知的情况下查询 Consul, # 因此每个主机都包含 Consul 主机和端口 CONSUL_HOST="${CONSUL_HOST}" CONSUL_PORT="${CONSUL_PORT}" # 通过从 # 系统中提取信息进行 Postgres 数据库配置 POSTGRES_USER=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-application-user?raw=true") PGPORT=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-port?raw=true") PGHOST=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-host?raw=true") # 注:切勿在真实部署中这样做。 Store passwords # 只能在加密的密钥存储中。 PGPASSWORD=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true") # 通过从系统中提取信息进行 RabbitMQ 配置 AMQPHOST=$(curl -X GET "http://localhost:8500/v1/kv/amqp-host?raw=true") AMQPPORT=$(curl -X GET "http://localhost:8500/v1/kv/amqp-port?raw=true") docker run \ --rm \ -d \ -e NODE_ENV="${NODE_ENV}" \ -e PORT="${PORT}" \ -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \ -e PGUSER="${POSTGRES_USER}" \ -e PGPORT="${PGPORT}" \ -e PGHOST="${PGHOST}" \ -e PGPASSWORD="${PGPASSWORD}" \ -e AMQPPORT="${AMQPPORT}" \ -e AMQPHOST="${AMQPHOST}" \ -e CONSUL_HOST="${CONSUL_HOST}" \ -e CONSUL_PORT="${CONSUL_PORT}" \ -e CONSUL_SERVICE_NAME="${CONSUL_SERVICE_NAME}" \ --network mm_2023 \ messenger
需要注意以下几点:
CONSUL_SERVICE_NAME
环境变量告知 Messenger 服务实例使用什么名称向 Consul 进行注册。CONSUL_HOST
和CONSUL_PORT
环境变量面向在部署脚本运行位置运行的 Consul 客户端。
注:在真实部署中,这是一个必须在团队之间达成一致的配置示例——负责 Consul 的团队必须在所有环境中提供
CONSUL_HOST
和CONSUL_PORT
环境变量,因为如果没有这些连接信息,服务将无法查询 Consul。 -
在应用终端,切换到应用目录,停止所有正在运行的 Messenger 服务实例,并重建 Docker 镜像,以加入新的服务注册代码:
cd app docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}")) docker build -t messenger .
-
在浏览器中导航到 http://localhost:8500 查看实时 Consul 用户界面(尽管并无值得特别注意的信息)。
-
在 Messenger 代码库的根目录下,运行部署脚本以启动一个 Messenger 服务实例:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
-
在浏览器的 Consul 用户界面中,点击标题栏中的“Services(服务)”,验证是否有一个 Messenger 服务正在运行。
-
再运行部署脚本几次,以启动更多 Messenger 服务实例。在 Consul 用户界面中验证它们是否正在运行。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
设置 NGINX
下一步,添加 NGINX 开源版作为反向代理和负载均衡器,将传入的流量路由至所有正在运行的 Messenger 实例。
-
在应用终端,将目录切换到 Messenger 代码库的根目录,并创建一个名为 load-balancer 的目录和以下三个文件:
mkdir load-balancer cd load-balancer touch nginx.ctmpl touch consul-template-config.hcl touch Dockerfile
Dockerfile 定义了 NGINX 和 Consul 模板运行所在的容器。Consul 模板还使用了另外两个文件,以便在 Messenger 服务在其服务注册表中发生变化(服务实例增加或减少)时,动态地更新 NGINX 上游。
-
打开在第一步中创建的 nginx.ctmpl 文件,并添加以下 NGINX 配置片段,Consul 模板将使用该片段动态地更新 NGINX 上游组。
upstream messenger_service { {{- range service "messenger" }} server {{ .Address }}:{{ .Port }}; {{- end }} } server { listen 8085; server_name localhost; location / { proxy_pass http://messenger_service; add_header Upstream-Host $upstream_addr; } }
此段代码会将向 Consul 注册的每个 Messenger 服务实例的 IP 地址和端口号都添加到 NGINX messenger_service 上游组。NGINX 将传入的请求代理到动态定义的上游服务实例组。
-
打开在第一步中创建的 consul-template-config.hcl 文件,并添加以下配置:
consul { address = "consul-client:8500" retry { enabled = true attempts = 12 backoff = "250ms" } } template { source = "/usr/templates/nginx.ctmpl" destination = "/etc/nginx/conf.d/default.conf" perms = 0600 command = "if [ -e /var/run/nginx.pid ]; then nginx -s reload; else nginx; fi" }
该 Consul 模板配置指示它重新渲染源模板(在上一步中创建的 NGINX 配置片段),把它放在指定目标位置,最后运行指定的命令(指示 NGINX 重新加载其配置)。
实际上,这意味着每次在 Consul 中注册、更新或注销服务实例时,都会新建一个 default.conf 文件。然后,NGINX 不中断地重新加载其配置,以确保 NGINX 可将流量发送至一组正常运行的最新服务器(Messenger 服务实例)。
-
打开在第一步中创建的 Dockerfile 文件,并添加以下内容,以构建 NGINX 服务。(在本教程中您无需理解 Dockerfile,行内注释只为了方便参考。)
FROM nginx:1.23.1 ARG CONSUL_TEMPLATE_VERSION=0.30.0 # 为 Consul 集群的位置设置一个环境 # 变量。 默认情况下,它尝试解析到 consul-client:8500 # 如果 Consul 在同一主机内 # 作为容器运行,并链接到该 NGINX 容器(又名 # consul),就会发生这种情况。但如果我们想要解析到另一个地址, # 也可以在容器启动时 # 覆盖这个环境变量。 ENV CONSUL_URL consul-client:8500 # 下载指定版本的 Consul 模板 ADD https://releases.hashicorp.com/consul-template/${CONSUL_TEMPLATE_VERSION}/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip /tmp RUN apt-get update \ && apt-get install -y --no-install-recommends dumb-init unzip \ && unzip /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip -d /usr/local/bin \ && rm -rf /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip COPY consul-template-config.hcl ./consul-template-config.hcl COPY nginx.ctmpl /usr/templates/nginx.ctmpl EXPOSE 8085 STOPSIGNAL SIGQUIT CMD ["dumb-init", "consul-template", "-config=consul-template-config.hcl"]
-
构建 Docker 镜像:
docker build -t messenger-lb .
-
切换到 Messenger 目录的根目录,创建一个名为 messenger-load-balancer-deploy.sh 的文件作为 NGINX 服务的部署文件(就像您在整个教程中部署的其他服务一样)。根据具体环境,您可能需要在
chmod
命令前添加sudo
前缀:cd .. touch infrastructure/messenger-load-balancer-deploy.sh chmod +x infrastructure/messenger-load-balancer-deploy.sh
-
打开 messenger-load-balancer-deploy.sh 并添加以下内容:
#!/bin/bash set -e # 由于我们无法在一无所知的情况下查询 Consul, # 因此每个主机都包含 Consul 主机和端口 CONSUL_HOST="${CONSUL_HOST}" CONSUL_PORT="${CONSUL_PORT}" docker run \ --rm \ -d \ --name messenger-lb \ -e CONSUL_URL="${CONSUL_HOST}:${CONSUL_PORT}" \ -p 8085:8085 \ --network mm_2023 \ messenger-lb
-
一切就绪后,部署 NGINX 服务:
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-load-balancer-deploy.sh
-
查看您能否从外部访问 Messenger 服务:
curl -X GET http://localhost:8085/health OK
成功!NGINX 现在正在对所有已创建的 Messenger 服务实例进行负载均衡。这点不难看出,因为
X-Forwarded-For
请求头显示的 Messenger 服务 IP 地址与上一节第八步中 Consul 用户界面中的 IP 地址相同。
挑战 4:使用服务作为作业运行程序迁移数据库
大型应用经常使用“作业运行程序”,其中小型工作进程可用于修改数据等一次性任务(例如 Sidekiq 和 Celery)。这些工具通常需要额外的支持基础架构,例如 Redis 或 RabbitMQ。在这种情况下,您将 Messenger 服务本身用作“作业运行程序”来运行一次性任务。这是合理的,因为它非常小,完全能够与数据库及其所依赖的其他基础架构交互,并与提供流量的应用完全分开运行。
这种做法具备三大优势:
- 作业运行程序(包括它运行的脚本)要经过与生产服务完全相同的检查和审查流程。
- 可轻松更改数据库用户等配置值,以增强生产部署的安全性。举例来说,您可以作为只能写入和查询现有表格的“低权限”用户来运行生产服务。您可配置一个不同的服务实例,作为能够创建和删除表格的更高权限的用户对数据库结构进行更改。
- 一些团队从同时处理服务生产流量的实例中运行作业。这很危险,因为作业问题会影响到容器中的应用正在执行的其他功能。我们采用微服务的初衷正是避免此类情况,不是吗?
在这个挑战中,您可通过更改一些数据库配置值和迁移 Messenger 数据库,使用新值并测试其性能,从而探索如何修改工件(artifact)以填补新角色。
迁移 Messenger 数据库
在实际生产部署中,您可能会创建两个具有不同权限的用户:一个是“应用用户”,一个是“迁移器用户”。为了简单起见,在本例中您要使用默认用户作为应用用户,并创建一个具有超级用户权限的迁移器用户。实际上,应该花更多的时间根据每个用户的角色确定所需的最小特定权限。
-
在应用终端,创建一个具有超级用户权限的新 PostgreSQL 用户。
echo "CREATE USER messenger_migrator WITH SUPERUSER PASSWORD 'migrator_password';" | docker exec -i messenger-db-primary psql -U postgres
-
打开数据库部署脚本 (infrastructure/messenger-db-deploy.sh) 并替换其内容以添加新用户的凭证。
注:重申一下,在真实部署中,切勿把数据库凭证等密钥放在部署脚本或者除密钥管理工具以外的任何其他地方。详情请阅读“Microservices June 2023 第二单元:微服务 Secretss 管理与配置基础入门”。
#!/bin/bash set -e PORT=5432 POSTGRES_USER=postgres # 注:切勿在真实部署中这样做。 Store passwords # 只能在加密的密钥存储中。 # 因为在本教程中我们关注其他概念, # 因此在为了方便起见,此处这样设置密码。 POSTGRES_PASSWORD=postgres # 迁移用户 POSTGRES_MIGRATOR_USER=messenger_migrator # 注:同上,切勿在真实部署中这样做。 POSTGRES_MIGRATOR_PASSWORD=migrator_password docker run \ --rm \ --name messenger-db-primary \ -d \ -v db-data:/var/lib/postgresql/data/pgdata \ -e POSTGRES_USER="${POSTGRES_USER}" \ -e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \ -e PGPORT="${PORT}" \ -e PGDATA=/var/lib/postgresql/data/pgdata \ --network mm_2023 \ postgres:15.1 echo "Register key messenger-db-port\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-port \ -H "Content-Type: application/json" \ -d "${PORT}" echo "Register key messenger-db-host\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-host \ -H "Content-Type: application/json" \ -d 'messenger-db-primary' # 这与上面的“--name”标记相匹配 # 在我们的设置中,这代表主机名 echo "Register key messenger-db-application-user\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-application-user \ -H "Content-Type: application/json" \ -d "${POSTGRES_USER}" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \ -H "Content-Type: application/json" \ -d "${POSTGRES_PASSWORD}" echo "Register key messenger-db-application-user\n" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-migrator-user \ -H "Content-Type: application/json" \ -d "${POSTGRES_MIGRATOR_USER}" curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this \ -H "Content-Type: application/json" \ -d "${POSTGRES_MIGRATOR_PASSWORD}" printf "\nDone registering postgres details with Consul\n"
此更改只是将迁移器用户添加到了在数据库部署之后在 Consul 中设置的用户组。
-
在名为 messenger-db-migrator-deploy.sh(同样,您可能需要在
chmod
命令前添加sudo
前缀)的基础架构目录下创建一个新文件。touch infrastructure/messenger-db-migrator-deploy.sh chmod +x infrastructure/messenger-db-migrator-deploy.sh
-
打开 messenger-db-migrator-deploy.sh 并添加以下内容:
#!/bin/bash set -e # 此配置需要新的提交才能更改 NODE_ENV=production PORT=4000 JSON_BODY_LIMIT=100kb CONSUL_SERVICE_NAME="messenger-migrator" # 由于我们无法在一无所知的情况下查询 Consul, # 因此每个主机都包含 Consul 主机和端口 CONSUL_HOST="${CONSUL_HOST}" CONSUL_PORT="${CONSUL_PORT}" # 获取迁移器用户名和密码 POSTGRES_USER=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-migrator-user?raw=true") PGPORT=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-port?raw=true") PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true) # 注:切勿在真实部署中这样做。 Store passwords # 只能在加密的密钥存储中。 PGPASSWORD=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this?raw=true") # 通过从系统中提取信息进行 RabbitMQ 配置 AMQPHOST=$(curl -X GET "http://localhost:8500/v1/kv/amqp-host?raw=true") AMQPPORT=$(curl -X GET "http://localhost:8500/v1/kv/amqp-port?raw=true") docker run \--rm \ -d \ --name messenger-migrator \ -e NODE_ENV="${NODE_ENV}" \ -e PORT="${PORT}" \ -e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \ -e PGUSER="${POSTGRES_USER}" \ -e PGPORT="${PGPORT}" \ -e PGHOST="${PGHOST}" \ -e PGPASSWORD="${PGPASSWORD}" \ -e AMQPPORT="${AMQPPORT}" \ -e AMQPHOST="${AMQPHOST}" \ -e CONSUL_HOST="${CONSUL_HOST}" \ -e CONSUL_PORT="${CONSUL_PORT}" \ -e CONSUL_SERVICE_NAME="${CONSUL_SERVICE_NAME}" \ --network mm_2023 \ messenger
该脚本的最终形式与您在“设置 Consul” 的第三步中创建的 infrastructure/messenger-deploy.sh 脚本非常相似。主要区别在于,
CONSUL_SERVICE_NAME
是messenger-migrator
而非Messenger
,PGUSER
对应于您在上面第一步中创建的“迁移器”超级用户。务必确保
CONSUL_SERVICE_NAME
为messenger-migrator
。如果把它设置为Messenger
,NGINX 将自动轮换该服务以接收 API 调用,而实际上它不应处理任何流量。该脚本以迁移器的角色部署了一个短期实例。这可以防止任何迁移问题影响主 Messenger 服务实例的流量服务。
-
重新部署 PostgreSQL 数据库。因为您在本教程中使用的是
bash
脚本,因此需要停止和重启数据库服务。在生产应用中,您通常只需运行一个基础架构即代码脚本,便可仅添加已更改的元素。docker stop messenger-db-primary CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-deploy.sh
-
部署 PostgreSQL 数据库迁移器服务。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-migrator-deploy.sh
-
验证实例是否按预期运行:
docker ps --format "{{.Names}}" ... messenger-migrator
您也可以在 Consul 用户界面中验证数据库迁移器服务是否已正确地在 Consul 中注册为 messenger-migrator(同样,它不会以 Messenger 的名义注册,因为它不处理流量):
-
现在执行最后一步——运行数据库迁移脚本!这些脚本并不像任何真正的数据库迁移脚本,但它们确实使用 messenger-migrator 服务来运行数据库特定的脚本。迁移数据库后,停止 messenger-migrator 服务:
docker exec -i -e PGDATABASE=postgres -e CREATE_DB_NAME=messenger messenger-migrator node scripts/create-db.mjs docker exec -i messenger-migrator node scripts/create-schema.mjs docker exec -i messenger-migrator node scripts/create-seed-data.mjs docker stop messenger-migrator
测试 Messenger 服务的实际应用
现在您已将 Messenger 数据库迁移至最终格式,终于可以实际运行 Messenger 服务了!为此,您需要对 NGINX 服务运行一些基本的 curl
查询(您已在“设置 NGINX”中将 NGINX 配置为系统的入口点)。
下面一些命令使用 jq
库对 JSON 输出进行格式化。您可按需进行安装,也可以从命令行中删除它。
-
创建对话:
curl -d '{"participant_ids": [1, 2]}' -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations' { "conversation": { "id": "1", "inserted_at": "YYYY-MM-DDT06:41:59.000Z" } }
-
向 ID 为 1 的用户的对话发送一条消息:
curl -d '{"content": "This is the first message"}' -H "User-Id: 1" -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations/1/messages' | jq { "message": { "id": "1", "content": "This is the first message", "index": 1, "user_id": 1, "username": "James Blanderphone", "conversation_id": 1, "inserted_at": "YYYY-MM-DDT06:42:15.000Z" } }
-
使用来自另一个用户(ID 为 2)的消息进行回复:
curl -d '{"content": "This is the second message"}' -H "User-Id: 2" -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations/1/messages' | jq { "message": { "id": "2", "content": "This is the second message", "index": 2, "user_id": 2, "username": "Normalavian Ropetoter", "conversation_id": 1, "inserted_at": "YYYY-MM-DDT06:42:25.000Z" } }
-
提取消息:
curl -X GET 'http://localhost:8085/conversations/1/messages' | jq { "messages": [ { "id": "1", "content": "This is the first message", "user_id": "1", "channel_id": "1", "index": "1", "inserted_at": "YYYY-MM-DDT06:42:15.000Z", "username": "James Blanderphone" }, { "id": "2", "content": "This is the second message", "user_id": "2", "channel_id": "1", "index": "2", "inserted_at": "YYYY-MM-DDT06:42:25.000Z", "username": "Normalavian Ropetoter" } ] }
清理
整个教程下来,您创建了大量容器和镜像!使用下面的命令来删除您不想保留的任何 Docker 容器和镜像。
-
删除任何正在运行的 Docker 容器:
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}")) docker rm $(docker stop messenger-db-primary) docker rm $(docker stop messenger-lb)
-
删除平台服务:
# From the platform repository docker compose down
-
删除本教程中使用的所有 Docker 镜像:
docker rmi messenger docker rmi messenger-lb docker rmi postgres:15.1 docker rmi hashicorp/consul:1.14.4 docker rmi rabbitmq:3.11.4-management-alpine
后续步骤
您可能会想:“看似简单的设置,其实操作起来很麻烦。”没错!迁移至以微服务为主的架构需要细致地设计和配置您的服务。虽然过程很复杂,但您还是取得了不错的成果:
- 设置了一个易于其他团队理解的以微服务为主的配置。
- 在所涉及的各种服务的扩展和使用方面,为微服务系统设置了一定的灵活性。
如欲进一步了解有关微服务的更多内容,请阅读“Microservices June 微服务之月 2023 第二单元:微服务 Secretss 管理与配置基础入门”,以深入浅出地了解微服务环境中的密钥管理。
实验验收标准
请同学们在动手实验的时候注意按照此验收标准进行截图并放到 Word 文档中,在参加单元小测时上传文档以便我们做实验验收。
启动:准备部署环境
检查点:成功部署 postgres、rabbitmq、consul 服务端、consul 客户端
挑战 1:定义应用层微服务配置
检查点:修改 Payload 包大小参数生效,并成功验证
客户端收到“PayloadTooLargeError”:
服务端记录“PayloadTooLargeError:
检查点:解除 Payload 大小限制后,报出另一个错误
挑战 2:为服务创建部署脚本
检查点:成功启动 messenger 数据库
检查点:成功在 Consul 中配置 messenger 启动依赖参数
挑战 3:将服务暴露给外界
检查点:启动四个 messenger 实例,并成功注册到 Consul
检查点:成功构建 NGINX 镜像并启动
检查点:NGINX 自动发现 messenger 实例并添加到 upstream 中
挑战 4:使用服务作为作业运行程序迁移数据库
检查点:设置新的迁移数据库配置到 Consul
检查点:成功启动数据库迁移工具容器
检查点:成功创建迁移库,并创建数据表
检查点:成功发送第一条消息,消息时间需要为实验时间
检查点:成功查询历史消息