第 11 章:健康的微服务
本章内容概览:
- 确保微服务的稳健运行技术
- 利用可观测性来理解微服务的行为
- 微服务与 Kubernetes 的调试方法
- 可靠性与容错机制
在软件开发中,错误的发生是无可避免的。代码可能存在缺陷,硬件、软件和网络也可能出现故障。所有类型的应用程序(包括微服务)都有可能遇到这些问题。微服务应用程序因其复杂性更加难以调试,并且随着应用程序的扩展,问题定位变得更加困难。维护的微服务数量越多,其中一些微服务在特定时刻出现问题的可能性也就越大。
虽然我们无法完全避免这些问题,但我们可以尽力减少它们带来的影响。一个良好设计的应用程序会预见到问题的发生,并采取措施减轻其影响,尽管问题的具体性质可能无法预测。
随着应用程序日益复杂,我们需要采用各种技术来解决问题并维护微服务的健康。行业内已经发展出许多最佳实践和模式来处理这些挑战。本章将介绍一些最有效的技术,遵循这些指南能够使我们的应用程序运行更加平稳、更为可靠,减少压力,并在问题出现时更容易进行恢复。
本章没有具体的实操内容;GitHub 上没有相关示例代码,因此不会涉及代码的具体操作。可以将本章内容视为技术工具箱,在未来开发微服务应用程序时可供参考和使用。
11.1 维护微服务的健康
一个健康的微服务应用程序由健康的单个微服务组成。健康的微服务不会经常崩溃、遇到 bug、CPU 过载或内存耗尽等问题。要监控应用程序的健康状况,我们需要注意以下几点:
- 观察微服务的行为,了解它们的历史和当前状态。
- 当问题出现时迅速响应,保护我们的客户。
- 对问题进行排序,优先解决最严重的问题。
- 对问题进行调试,并根据需要采取修复措施。
以 FlixTube 的元数据微服务为例,图 11.1 展示了生产环境中健康微服务的基础设施。注意,这里有多个微服务副本,请求通过负载均衡器平均分配到各个微服务实例。如果某个微服务实例出现故障,其他副本可以在故障实例重启期间接管其工作。
这种冗余设计确保了微服务和应用程序的持续可靠性。在本章中,你将了解如何在 Kubernetes 上复制微服务以及其他促进容错和故障恢复的技术。
即使没有严重到失效的程度,微服务也可能会遇到问题。我们如何知道微服务内部发生了什么?它不应该是一个黑盒。我们通常需要某种遥测服务(如图 11.1 所示)来记录来自各个微服务的事件,以便我们能以一种易于理解的方式对数据进行可视化。
我们可以采取哪些措施来确保微服务保持健康?就像真正的医生一样,我们需要知道如何“测量患者的体温”。我们可以利用多种技术来诊断微服务的状态和行为。

表 11.1 列出了本章将介绍的主要技术,帮助我们了解微服务的行为。
表 11.1 理解微服务行为的技术
| 技术 | 描述 |
|---|---|
| 日志记录 | 输出有关我们的微服务行为的信息,以显示事情的发生情况 |
| 错误处理 | 拥有管理和恢复错误的策略 |
| 自动健康检查 | 配置 Kubernetes 自动检测我们的微服务中的问题 |
| 可观测性 | 输出和记录我们可以用来理解微服务之间交互的遥测数据 |
当某些事情出错时会发生什么?我们如何修复它?应对已发生的问题需要进行调查和调试。在本章中,你将学习如何找到问题的根源并进行修复。
11.2 监控与管理微服务
将应用程序部署到生产环境仅是开始。此后,我们需要持续监控应用程序的运行状态,尤其是在部署新代码更新后。
对应用程序行为的透明了解至关重要;如果我们不了解内部发生的情况,就无法有效解决问题。在本节中,我们将探讨监控微服务行为的几种关键技术:
- 日志记录
- 异常处理
- 自动化健康检查
- 可观测性
11.2.1 日志记录的开发实践
在开发过程中,控制台日志记录是我们理解微服务持续行为的基本工具。通过日志记录,我们生成一个显示应用程序中发生的关键事件、活动和操作的文本流。
应用程序产生的日志可以被看作是其生命周期内的一份历史记录。我们可以在开发和生产环境中利用控制台日志记录。图 11.2 展示了元数据微服务在开发过程中的控制台日志记录示例。

每个微服务(如同每个进程)都具有两个日志输出流:
- 标准输出
- 标准错误
在 JavaScript 中,我们将日志记录输出到标准输出通道,如下:
console.log("有用的信息在这里");
我们将错误信息输出到标准错误通道,如下:
console.error("错误信息在这里");
注意 如果你使用的是 JavaScript 以外的其他语言,它们也会有相应的功能来实现标准输出和错误输出。
直接向控制台输出日志是简单且有效的。尽管许多开发人员使用复杂的日志系统来管理微服务日志,但简洁的控制台日志记录方法通常已足够。
应该记录什么内容?
考虑到日志记录通常需要开发人员显式添加且总是可选的,我们应如何选择记录哪些内容?以下是一些指导原则:
- 应记录:
- 应用程序中的关键事件及其详细信息
- 重要操作的成功与失败
- 应避免记录:
- 可从其他来源轻松获取的信息
- 任何机密或敏感数据
- 用户的任何个人信息
如果你发现自己因为过多的日志细节而感到不适,可以随时停止记录不必要的信息。对于每条日志,只需问自己一个问题:没有这条信息我能否继续工作?如果答案是肯定的,那么删除它。
然而,一般而言,更丰富的日志记录通常更有益。当你需要在生产环境中调试问题时,尽可能多的信息可以帮助你理解问题的根源。回溯日志是理解导致问题的事件顺序的关键。
一旦问题发生,你将无法再添加更多的日志记录!当然,如果你能够隔离并复现问题,你可以补充日志,但这通常也不容易实现。因此,丰富的日志记录是有利的,因为在遇到问题时,你希望拥有尽可能多的信息来帮助你解决问题。
11.2.2 错误处理
在编程中,错误是不可避免的现象,用户常常因此受到影响。这可以视为编程的一条基本法则。以下是一些常见的错误类型:
- 运行时错误(例如,异常导致微服务崩溃)
- 输入错误的数据(可能来源于故障的传感器或人为的数据输入错误)
- 代码被以意外的方式或组合使用
- 第三方依赖项失败(如 RabbitMQ)
- 外部依赖项失败(如 Azure Storage)
正确处理这些错误至关重要。我们必须制定策略,优雅地处理和恢复错误,以最大限度减少对用户和业务的影响。当错误发生时,我们的应用程序会如何反应?我们必须深入考虑这些问题,并为我们的应用程序设计周密的错误处理策略。
在我们的 JavaScript 代码中,我们通常预见到潜在的错误,并利用异常、回调或 Promise 来处理这些错误。这些情形下,我们通常知道该如何应对。我们可以尝试重试失败的操作,或者尝试修正问题并重新执行操作;如果没有可行的自动修正方案,我们可能需要向用户报告错误或通知我们的运维团队。
有时错误是可预见的,但也有时是意外的。我们可能没有捕获到某些类型的错误,可能是因为我们不知道它们会发生,或者是因为某些类型的错误(如硬盘故障)非常罕见,不值得专门处理。但为了更加安全,我们必须考虑到我们甚至无法预见的错误!
我们需要一种通用的策略来处理这些意外的错误。对于任何进程,包括微服务,基本上有两种主要选择:“中止并重启”或“继续运行”。你可以在图 11.3 中看到这些错误处理策略的示例。

中止并重启
这种策略通过拦截意外错误,并以重启进程作为响应。这种做法的一个简单实现是忽视我们不关心的错误。任何未通过 try/catch 语句显式处理的异常都将导致进程终止。
这是一个简单的错误处理策略,实质上是让意外的错误发生,并允许 Node.js 在响应中终止我们的程序。
在生产环境中,如果微服务被中止,我们将依赖 Kubernetes 自动为我们重启它,这是 Kubernetes 的默认行为(可配置)。
继续运行
“继续运行”策略通过捕获意外错误并允许进程继续执行来响应。我们可以在 Node.js 中通过处理 uncaughtException 事件来实现这一点:
process.on("uncaughtException", err => {
console.error("未捕获的异常:");
console.error(err && err.stack || err);
});
通过这种方式处理 uncaughtException 事件,我们可以对意外错误进行显式控制。在这种情况下,Node.js 不会执行默认的进程终止操作。进程将尽力继续运行,尽管我们只能希望错误未使进程处于不稳定状态。
将错误打印到标准错误通道意味着它可以被我们的生产日志系统捕获,并进行后续讨论。然后我们可以将此错误报告给运维团队,以确保它不被忽略。
中止并重启:版本 2
现在我们已经了解如何在 Node.js 中处理未捕获的异常,我们可以实现一个改进的中止并重启策略:
process.on("uncaughtException", err => {
console.error("未捕获的异常:");
console.error(err && err.stack || err);
process.exit(1);
});
在这段代码中,我们显式控制对意外错误的处理。如之前所述,我们记录错误,让运维团队能够注意到。然后,我们通过调用 process.exit 显式终止程序。
我们传递一个非零退出代码给 exit 函数,这是标准的约定,表示进程因错误而终止。我们可以使用不同的非零错误代码来表示不同类型的错误。
我们应该采用哪种错误处理策略?
是否重启还是继续运行是一个值得考虑的问题。许多开发人员倾向于“中止并重启”,因为在大多数情况下,允许进程终止是明智的选择,因为在崩溃后尝试恢复微服务可能导致状态受损。
采用“中止并重启”的策略,我们可以监控导致崩溃的情况,从而识别出哪些微服务存在需要解决的问题。如果结合了良好的错误报告,这将是一个可靠的默认策略。
然而,有时“继续运行”可能更适合。对于一些关键的微服务(例如处理客户数据的服务),我们需要仔细评估中止进程可能带来的影响。
例如,考虑 FlixTube 的视频上传微服务。这个服务是否可以在任何时候中止?在任何给定时刻,它可能正在处理来自多个用户的多个视频上传。如果这个服务中止,可能会导致用户的上传数据丢失,这种情况是否可以接受?我认为不可接受,但这取决于你的具体情况。并没有一个唯一正确的答案。
注意 在选择错误处理策略时,虽然默认选择“中止并重启”通常是较好的策略,但在某些情况下,“继续运行”可能更合适。
11.2.3 利用 Docker Compose 进行日志管理
在使用 Docker Compose 进行开发时,我们可以在终端窗口中观察到所有微服务的日志,这些日志会被自动汇聚成一个流。这种方式对开发者来说极为有用,因为它可以随时提供对应用程序活动的全面了解,如图 11.4 所示。

将日志重定向到文件
这里有一个实用的技巧:在运行 Docker Compose 时,我们可以将输出重定向到一个日志文件中,同时还能在终端显示。可以使用 tee 命令同时做到这两点。
docker-compose up --build 2>&1 | tee debug.log
- 这是我们自第 4 章以来一直在使用的标准 Docker Compose 命令。
- 该语法将标准输出和标准错误进行了重定向。
- 通过管道将输出传递给
tee命令。 tee命令会将输入同时复制到终端和指定的文件。
现在,我们可以在 Visual Studio Code (VS Code) 中打开日志文件(本例中为 debug.log),并自由浏览。例如,如果我们在搜索数据库相关的问题,可以搜索包含“database”这个词的日志。
我甚至建议在日志中添加特殊的标识符(字符序列),以便区分不同子系统或特定微服务的日志。这样做可以更容易地搜索或过滤我们关心的日志类型。
11.2.4 在 Kubernetes 中进行基础日志记录
使用 Docker Compose 在开发环境中运行微服务时,我们可以在本地轻松查看应用程序的日志。但从在 Kubernetes 上远程运行的生产微服务中获取日志稍微复杂一些。我们需要能够从集群中提取日志,并将其拉回到开发机器上进行分析。
KUBECTL
从第 6 章开始接触 kubectl,现在我们将再次使用它来获取在 Kubernetes 上运行的特定微服务的日志。假设我们正在运行第 10 章末尾的 FlixTube(可以启动它并跟随操作)。假如我们想从元数据微服务的一个实例中获取日志。
鉴于我们可能有多个元数据微服务实例(虽然目前还没有,但我们将在本章稍后讨论如何创建副本),我们需要找到 Kubernetes 为我们感兴趣的特定微服务分配的唯一名称。我们实际上需要的是 pod 的名称。回想一下第 6 章,Kubernetes pod 是容纳我们容器的实体。一个 pod 实际上可以运行多个容器,尽管在 FlixTube 项目中,我们目前每个 pod 只运行一个容器。在第 6.10.3 节中复习如何使用 kubectl 对你的集群进行身份验证。验证后,我们可以使用 get pods 命令查看集群中所有 pod 的列表:
> kubectl get pods
NAME READY STATUS RESTARTS AGE
azure-storage-57bd889b85-sf985 1/1 Running 0 33m
database-7d565d7488-2v7ks 1/1 Running 0 33m
gateway-585cc67b67-9cxvh 1/1 Running 0 33m
history-dbf77b7d5-qw529 1/1 Running 0 33m
metadata-55bb6bdf58-7pjn2 1/1 Running 0 33m <-- 包含我们元数据微服务实例的 pod 的唯一名称
rabbit-f4854787f-nt2ht 1/1 Running 0 33m
video-streaming-68bfcd94bc-wvp2v 1/1 Running 0 33m
video-upload-86957d9c47-vs9lz 1/1 Running 0 33m
从列表中找到元数据微服务的 pod 名称,并记录其唯一名称。在本例中,名称是 metadata-55bb6bdf58-7pjn2。现在,我们可以使用 logs 命令来检索元数据微服务的日志。虽然这次可能没有太多内容可看,但了解如何执行此操作很重要。
kubectl logs metadata-55bb6bdf58-7pjn2 # 我们正在获取日志的 Pod 的唯一名称
Waiting for rabbit:5672。
Connected!
[email protected] start /usr/src/app # 从微服务检索到的控制台日志
node ./src/index.js
Microservice online.
记得将 pod 名称替换为实际运行的微服务的名称。由于 Kubernetes 自动生成唯一名称,因此你的元数据微服务的名称可能与此示例中的名称不同。以下是通用命令模板:
kubectl logs <pod-name>
只需插入你希望检索日志的 pod 的特定名称即可。
STERN
虽然 kubectl logs 命令是从 Kubernetes 中获取微服务日志的一个有效起点,但每次使用时都需要指定微服务的确切名称,这样做略显繁琐。
一个更优的工具是 Stern,它允许使用部分名称来检索日志,使得整个过程更加方便。例如,要获取元数据微服务的日志,只需简单执行以下命令:
stern metadata
Stern 将自动匹配所有以 metadata 为前缀的 pod 名称。如果存在多个元数据微服务副本,Stern 会聚合所有副本的日志输出,这显著优于 kubectl logs。此外,Stern 将持续输出日志直到被手动终止(使用 Ctrl-C)。其灵活性和易用性使 Stern 成为管理 Kubernetes 日志的首选工具。更多关于 Stern 的安装和使用,可访问其代码库:
https://github.com/stern/stern
。
KUBERNETES DASHBOARD
另一种查看 Kubernetes 中的日志(及其他信息)的方法是使用 Kubernetes 仪表板。该仪表板提供了一种直观方式来检视和探索你的集群,虽然它并不默认安装在所有 Kubernetes 环境中。
例如,在 Azure 上,仪表板不是安装的默认组件。如果你在其他环境下使用 Kubernetes,可能会发现仪表板已经预装。安装和使用仪表板的步骤并不复杂,可以在 Kubernetes 官方网站找到相关指南: http://mng.bz/OPKa 。
通过仪表板,我们可以快速查看任何 pod 的详细状态,如图 11.5、11.6 和 11.7 所示。这些视图不仅显示日志,还包括 CPU 和内存使用等有用信息,有助于全面了解微服务的运行状态。



11.2.5 Kubernetes 日志聚合
在管理生产环境的 Kubernetes 集群时,许多开发人员和运维专家发现,日志聚合——即查看集群中所有微服务生成的合并日志——是极为重要的功能。随着应用规模的扩大,单独跟踪每个微服务的日志变得越来越复杂和耗时。
遗憾的是,Kubernetes 本身不提供内置的日志聚合解决方案。因此,我们需要依赖独立的解决方案来实现这一功能。虽然自行开发和维护一个自定义的日志聚合服务是可能的,但这通常并不易于实现且难以维护。一种更有效的方法是采用第三方的企业级日志记录或可观测性解决方案。如果你对自行实现 Kubernetes 日志聚合服务感兴趣,可以参考我的博客文章,详见: http://www.codecapers.com.au/kubernetes-log-aggregation 。
11.2.6 企业级日志记录、监控和警报系统
对于大型企业级微服务监控,常见的解决方案包括使用 Fluentd、Elasticsearch 和 Kibana 的组合。此外,对于监控指标,Prometheus 和 Grafana 也是专业的选择。这些都是企业级的专业解决方案,用于监控和警报,但它们配置复杂,资源消耗较大,因此在决定将它们集成到你的应用中之前,需要慎重考虑。
这里不会深入介绍这些技术,因为它们的详细配置和使用超出了本书的范围。目前,只需对这些工具有一个基本的认识。
FLUENTD
Fluentd 是一个用 Ruby 编写的开源数据收集器和日志记录服务。它可以配置在集群中作为一个容器运行,用于将日志数据转发到外部的日志管理系统。
Fluentd 非常灵活,支持多种插件,其中一个插件可以将日志数据转发到 Elasticsearch。更多关于 Fluentd 的信息,请访问其官方网站:
ELASTICSEARCH
Elasticsearch 是一个基于 Java 的开源搜索引擎,用于存储、搜索和分析大量数据。它常用于日志和监控数据的存储及检索。详细信息可访问其官方网站: www.elastic.co/elasticsearch 。
KIBANA
Kibana 是建立在 Elasticsearch 基础上的开源数据可视化工具,允许用户创建和分享图形化的数据视图。Kibana 的优势在于它能够设置和触发基于数据的警报。
Kibana 的付费版本支持更多功能,如电子邮件通知和自定义 Webhooks。了解更多关于 Kibana 的信息,请访问: www.elastic.co/kibana 。
你可以在 www.elastic.co/demos 上找到 Kibana 的演示仪表板,并查看支持的通知功能: http://mng.bz/YRda 。
PROMETHEUS
Prometheus 是一个开源的监控和时间序列数据库系统,特别适用于与 Kubernetes 集成。它由云原生计算基金会(CNCF)支持,确保了其广泛的可靠性和社区支持。
Prometheus 可以配置为定期从微服务中抓取指标,并在发现问题时自动触发警报。更多关于 Prometheus 的信息,请访问: prometheus.io 。
GRAFANA
虽然 Prometheus 在数据收集和警报方面表现出色,但它在数据可视化方面功能有限。这是 Grafana 发挥作用的地方,它可以轻松地连接到 Prometheus,提供丰富的数据可视化和交互式仪表板。
更多关于 Grafana 的信息,请访问: grafana.com 。
11.2.7 微服务的可观测性
可观测性 不仅仅是日志聚合和监控,而是一种技术,用于揭示分布式应用程序的行为细节。通过从应用程序中输出详细的遥测数据,可观测性为我们提供了深入理解应用行为的窗口。当前的行业标准是使用 OpenTelemetry 协议。
实施可观测性的工具包括 Honeycomb、Datadog、New Relic 和 Sumo Logic 等,它们帮助我们捕获遥测数据并观察微服务之间的交互。
可观测性能帮助我们全面了解系统状况,对诊断问题和识别问题源头至关重要。它允许我们查询和可视化遥测数据,深入探究问题的具体原因,例如,确定问题是否与特定用户或地区相关。
更多关于如何在微服务中实施 OpenTelemetry 和 Honeycomb 的信息,可以参考以下资源:

11.2.8 利用 Kubernetes 健康检查自动重启微服务
Kubernetes 提供了强大的自动健康检查功能,允许系统自动检测并重启状态不佳的微服务。尽管 Kubernetes 默认会将崩溃或异常退出的 pod 标记为不健康并自动进行重启,但有时候,我们可能需要自定义这一流程以满足特定需求。
我们可以通过定义 readiness 探针和 liveness 探针来让 Kubernetes 监测微服务的健康状态。readiness 探针 用于确认微服务已启动并准备好接收请求,而 liveness 探针 则用来检测微服务是否仍在正常运行并响应请求。图 11.9 展示了这两种探针的应用。

这两种探针可用于优化我们在第 5 章首次尝试将历史记录微服务连接到 RabbitMQ 服务器时遇到的问题(见第 5.8.5 节)。问题在于,历史记录微服务(或任何依赖上游依赖项的微服务)需要等待依赖项(本例中为 RabbitMQ)启动并可用后才能进行连接。
如果微服务过早尝试连接,可能会触发异常并导致进程崩溃。如果历史记录微服务能够等待 RabbitMQ 变得可用,那将更为理想。虽然第 5 章中使用 wait-port npm 模块作为一种解决方法,但那是一个临时的解决方案。现在,使用 Kubernetes,我们有了一个更加优雅的解决策略。
这种问题通常只在微服务首次启动时发生。一旦生产应用程序稳定运行且 RabbitMQ 服务器已启动,新的依赖于 RabbitMQ 的微服务可以无需等待即刻加入。但这并不意味着问题已经完全解决:
- 当 RabbitMQ 崩溃并由 Kubernetes 自动重启时,会发生什么?
- 如果我们需要暂时关闭 RabbitMQ 进行升级或维护,又会怎样?
在这些情况下,RabbitMQ 的离线将会中断所有依赖它的微服务的连接。这些微服务的默认反应(除非进行特别处理)是抛出未处理的异常,这可能导致微服务崩溃。现在,所有依赖 RabbitMQ 的微服务都会在 RabbitMQ 停机期间不断地崩溃和重启。
同样的情况也适用于 RabbitMQ 以外的任何系统依赖项。我们通常希望任何服务都可以安全地下线,并让依赖它的下游服务等待其重新上线。当服务重新上线时,下游服务可以恢复正常运行。
通过配置 readiness 和 liveness 探针,我们可以更优雅地管理这些情况。例如,我们可以设置:
- 一个
/ready路由,在 RabbitMQ 可用时返回状态码 200 —— 这告诉 Kubernetes 微服务已准备好接收流量。 - 一个
/alive路由,在 RabbitMQ 不可用时返回错误代码 —— 这将触发 Kubernetes 重启微服务,但由于/ready路由,重启的微服务不会进入准备就绪状态,直到 RabbitMQ 再次上线。
这种策略不仅防止了服务不断的崩溃和重启,而且避免了需要在微服务内部编写复杂的错误处理逻辑。要了解更多关于 pod 生命周期和探针的详细信息,请参考 Kubernetes 官方文档: http://mng.bz/z0PA 。
11.3 调试微服务
拥有一种监控或可观测性工具后,我们能够可视化应用程序的行为,从而了解其当前状态和历史活动。这些信息在问题发生时尤其宝贵。
一旦问题出现,我们需要像侦探一样进行分析,利用收集的数据提出问题,追踪线索以找到问题根源,并确定问题发生的原因。这个过程涉及到一系列实验,目的是锁定问题所在。
通常,我们无法解决问题,除非找到了问题的根源。当然,偶尔我们可能会在不完全了解原因的情况下偶然找到解决方案。但能够准确地识别问题源头是至关重要的,这样我们才能确保所采用的解决策略真正解决了问题,而不只是临时掩盖了问题。
调试 是一种追踪问题源头并随后应用修复的过程。调试微服务与调试任何其他类型的应用程序相似;它是一门结合艺术与科学的技术。我们需要提出能导致问题的假设,并设计实验来验证这些假设,希望能找到问题的答案。通常,发现问题的正确描述比解决问题本身更为困难。
调试通常是一个挑战,特别是在涉及多个微服务的分布式应用程序中,定位单个进程中的问题已经足够复杂,更不用说在多个互相交互的微服务中找到问题了。
你可能已经意识到,找到问题的真正根源通常是调试过程中最难的部分。这就像在海洋中寻找针一样困难。如果你知道在哪里寻找,你就更有可能快速定位到问题。这也是为什么熟悉特定代码库的开发人员通常能比那些不熟悉的人更快地定位错误。
一旦确定了问题的根源,接下来就是进行修复。幸运的是,修复错误通常(但并非总是)比发现它们要简单得多。
11.3.1 调试过程
在理想的情况下,所有问题都将在开发与测试阶段被发现并修复。如果拥有完备的测试实践和自动化测试套件,那么许多错误将在产品投入使用前被检出。尽可能在开发阶段解决问题显然更为理想,因为在开发环境中调试要比在生产环境中容易得多。
调试代码通常遵循以下流程:
- 评估问题。
- 收集证据。
- 减少对客户的影响。
- 问题隔离。
- 问题复现。
- 问题修复。
- 思考如何防止类似问题再次发生。
调试既是艺术也是科学,不是一个严格定义的流程。我们有时需要不断迭代,通过这些步骤以不可预测的方式前进。但为了说明,我们可以假设这是一个直接的、线性的问题解决过程。
评估问题
通常,我们不只面临一个单一问题。我们经常会遇到多个问题,因此需要根据它们对客户及业务的影响程度来进行优先级排序。
评估 过程来源于医学界,医生会根据患者的紧急程度来决定治疗的优先顺序。同样地,我们可以根据问题的严重性来排序,优先解决那些最严重的问题。
收集证据
在评估问题并确定最紧迫的问题后,下一步是尽可能全面地收集证据。这包括所有能帮助我们快速定位错误的相关信息。从问题发生地点附近开始调试,可以更有效地缩小问题范围。我们需要迅速掌握尽可能多的问题相关信息。收集的证据包括:
- 日志和错误报告
- 系统中相关请求路径的跟踪(见图 11.8)
- 用户反馈的错误信息
- 来自 Stern、kubectl、Kubernetes 仪表板或我们的可观测性软件的信息
- 可能出现的崩溃调用堆栈
- 涉及的代码版本、分支或标签
- 最近部署的代码或微服务
我们必须迅速行动收集这些信息,因为通常接下来我们需要尽快消除问题,以维护客户利益。
减轻对客户的影响
在寻找问题原因或尝试解决之前,我们首要任务是确保问题不会对客户造成负面影响。若客户受到影响,我们必须立即采取措施进行修正。
此时,我们并不关注问题的根本原因或长期解决方案。我们需要的是最快的恢复方法,以保障客户所依赖的功能正常运作。快速响应以解决问题将获得客户的认可和感激。减轻影响的方法包括:
- 如果问题由近期的代码更新引起,撤销这次更新并在生产环境中重新部署代码。在微服务架构中,这通常更为简单,因为如果我们确定问题由某个更新的微服务引起,我们可以轻松还原该微服务到之前的稳定版本,例如恢复到容器注册表中的较早镜像。
- 如果问题由客户暂时不需要的新功能或更新功能引起,我们可以禁用该功能以恢复应用程序的正常运行。
- 如果问题来源于非核心微服务,我们可以暂时下线该服务。
必须强调,这一步骤的重要性不可小觑!解决问题可能需要数小时、数天甚至数周(在最坏的情况下)。我们无法预知解决所需的具体时间,不能让客户长时间等待。客户可能会因此转投我们的竞争者。
更糟糕的是,客户等待的压力可能导致我们在解决问题时做出糟糕的决策。在压力之下施行的修复措施可能会导致更多错误,从而加剧问题。
为了保护客户和自身的利益,我们必须暂时置问题一旁,寻找最快的方法来恢复应用程序的正常状态(如图 11.10 所示)。这样可以减轻压力,让客户继续使用我们的应用,并为我们赢得解决问题的宝贵时间。
重现问题
在确保应用程序恢复正常服务后,我们需要寻找并解决故障的根源。为此,我们必须能够重现问题。只有在我们能够一致并可靠地复现问题时,才能确信问题已被解决。我们的目标是构建一个能够可靠触发错误的 测试用例 ——一个详细记录的步骤序列。
理想情况下,我们应在开发环境中复现这一错误,这样我们就能更轻松地进行实验来追踪问题源头。然而,有些问题复杂到无法在开发环境中轻易重现,尤其是当应用程序规模庞大(如多个微服务组成)以至于无法完全运行在单一计算机上时。

因此,我们必须在 测试环境 中重现问题。这个环境虽然与生产环境相似,但仅用于测试,不面向客户。在测试环境中调试虽然类似生产环境,但同样充满挑战,最终,我们仍希望能够在开发环境中复现问题。
在测试环境中,我们可以运行实验,进一步探究涉及问题的应用程序组件,并安全地排除无关组件。通过逐步排除,我们可以将应用缩减至能在开发环境中运行的规模。然后,我们可以将测试环境迁移到开发计算机上。在第 12 章中,我们将详细讨论测试环境的创建。
如果我们正在执行自动化测试,现在是编写自动化测试来验证问题是否已解决的时刻。当然,这些测试最初会失败——这正是它们的用途。后续我们将利用这些测试作为可靠的工具确认问题是否真正被解决。通过编写自动化测试,我们也能确保问题可以被一次又一次地准确复现。
隔离问题
一旦在开发环境中复现了问题,我们就开始着手隔离问题。通过反复实验和剥离应用程序,我们逐步缩小范围,直至找到问题的确切源头。
我们实际上是在逐渐缩小可能隐藏问题的范围,逐步削减直至问题原因显现无疑。我们采用的是 分而治之 的策略,如图 11.11 所示。

微服务架构在这一过程中发挥了重要作用。我们的应用程序已被良好地拆分成易于隔离的组件,这使得拆解整个应用变得简单。通常,只需在 Docker Compose 文件中注释掉相应的微服务即可轻松移除它。每次移除一个微服务,问自己:问题还能否被复现?
- 是的。 很好,你刚缩小了问题的范围。
- 不。 也好,你可能已将该微服务与问题关联起来。
无论结果如何,你都在一步步接近问题的根源。
有时,我们能迅速定位到问题来源。但其他时候,调试可能变成一场耗时且令人沮丧的挑战。这取决于我们的经验、对代码库的熟悉程度、是否曾遇到类似问题,以及问题本身的复杂性。
注意 在最困难的情况下,调试需要毅力、耐心和坚持。不要害怕寻求帮助。被困在无法解决的问题中是极其痛苦的。
如果你能找到问题的起点,那么你已占据了优势。你也许能够基于合理的猜测迅速锁定问题原因。如果猜测成功,你可以省去许多步骤,直接聚焦问题核心。然而,如果你不确定从何处开始,或猜测错误,你就必须更科学地进行调试,并严格遵循整个流程。
修复问题
你已经定位到了问题的根源,现在只剩下修复它的工作了!幸运的是,修复问题通常比发现它们更为直接。一旦你识别出出错的代码,通常就能迅速构思出解决方案。有时候,这可能需要一些创新思维,但请记住,最艰难的部分——找到问题的症结所在——已经完成。你已经成功地从茫茫大海中捞出了那根针,接下来就是考虑最佳的解决策略。
如果你已经进行了自动化测试并编写了可以触发问题的失败测试案例,那么你就拥有了一个方便且可靠的标准来确认错误何时被修复。即便修复过程变得棘手,至少你有了一种方法可以验证问题是否真正得到解决。在你通过迭代和实验找到解决方案的过程中,这个测试将是一个极其有用的工具。
反思如何防止未来出现此问题
每次我们解决一个问题后,都应该花时间进行反思,探讨我们可以采取哪些措施,以防止类似问题将来再次发生,或者如何能更迅速地识别并解决这类问题。反思是我们个人和团队持续优化和改进开发流程的关键。
我们可能已经编写了一个防止同一问题重复发生的自动化测试。然而,我们应当追求的不仅是解决这一个具体的问题。我们应该寻求那些能够根除类似问题的实践和习惯。
我们投入于升级开发和测试流程的精力,应当基于问题的性质及其可能造成的影响。
我们应该提出的问题包括:
- 这类问题是否有可能再次发生,需要我们主动进行预防?
- 这个问题的潜在影响是否足够严重,使得我们应当采取措施预防?
回答这些问题将帮助我们决定应该投入多少资源来预防未来可能出现的类似问题。
11.3.2 在生产环境中调试微服务
有时不得不在生产环境中调试微服务。如果问题无法在测试或开发环境中复现,那么在生产环境中进行调试将成为我们唯一的选择。
检查 Pod 的日志
在调试任何微服务时,首先要做的就是检查其日志中是否有相关错误信息。这是一个基本步骤,但值得重申,你可以通过以下命令查看日志:
kubectl logs <pod-name>
如果你使用了 Stern,可以这样操作:
stern <pod-name>
获取前一个实例的日志也很有帮助:
kubectl logs --previous <pod-name>
此命令可以让我们获取崩溃前微服务的日志。检查这些日志中的错误或警告通常能初步判断出故障原因。
查看所有 pod 的状态可以帮助我们确定是否有 pod 处于错误状态或频繁重启:
kubectl get pods
比如,在尝试让视频流微服务在集群中正常运行时,我遇到了以下错误:
这些错误(ErrImagePull 和 ImagePullBackOff)表明 Kubernetes 集群无法从容器仓库中拉取镜像。这是一个常见问题,通常是由于配置集群与容器仓库的连接出错所致(如需帮助,请参考第 6 章第 6.11.3 节的 Azure 配置指南)。
以下命令可以提供特定 pod(例如 metadata 微服务)及其可能遇到的错误的详细信息:
kubectl get pod metadata-55bb6bdf58-7pjn2 -o yaml
kubectl describe pod metadata-55bb6bdf58-7pjn2
记得将 pod 名称替换为你所关注的特定 pod:
kubectl get pod <pod-name> -o yaml
kubectl describe pod <pod-name>
这些命令也可以用来获取有关 Kubernetes 中部署、服务及其他资源的详细信息:
kubectl get <resource-type> <resource-name> -o yaml
kubectl describe <resource-type> <resource-name>
此外,你也可以使用 Kubernetes 仪表板(见第 11.2.4 节)直观地获取任何相同信息。
访问 Shell
为了深入检查超出日志信息范围的问题,我们可以通过 kubectl 打开进入任何 pod 的 shell(终端),前提是这些 pod 安装了 shell。只要我们知道 pod 的名称(通过 kubectl get pods 查看),例如包含 metadata 微服务的 pod,我们可以通过以下方式开启一个 shell:
kubectl exec --stdin --tty metadata-55bb6bdf58-7pjn2 -- bash
事实上,我们可以使用此命令在 pod 内部运行任何命令。命令的一般格式如下:
kubectl exec --stdin --tty <pod-name> -- <command>
为了方便操作,我通常使用命令的简化版本:
kubectl exec -it <pod-name> -- <command>
要注意,进入生产环境中的微服务可能带来风险,任何不慎操作都可能导致问题加剧!切勿随意更改生产环境中的微服务,避免造成不可预测的后果。
端口转发
为了更好的安全性,我们的大多数微服务默认仅在 Kubernetes 集群内部可访问。但有时,我们需要外部访问这些微服务以进行测试或问题诊断。通过端口转发,我们可以在不公开微服务的情况下,从开发机器上测试内部服务。例如,将 metadata 微服务在本地端口 6000 上暴露:
kubectl port-forward metadata-55bb6bdf58-7pjn2 6000:80
运行 port-forward 命令时,我们可以向 http://localhost:6000 发送请求,这些请求会被转发到集群中 metadata 微服务的 80 端口。
这是一个宝贵的技术,可用于远程测试我们的微服务,确保它们正确响应请求。我们还可以利用此技术连接到集群中运行的其他服务,如 RabbitMQ 仪表板。通用格式如下:
kubectl port-forward <pod-name> [<local-port>:]<pod-port>
检查 Pod 的环境变量
一个常见的部署错误是遗漏或错误配置环境变量。我经常遇到这样的烦恼。
最好的防御措施是,微服务在缺少必要环境变量时应报错;我们在第 2 章第 2.6.6 节讨论过这一点。因此,你可以检查微服务的日志来确认是否报告了缺少的环境变量。
你还可以通过查看微服务实际设置的所有环境变量值来检查是否有配置错误:
kubectl describe pod metadata-55bb6bdf58-7pjn2
这将提供大量关于微服务的信息,你可以快速浏览找到环境变量的名称和值。
提示 我经常使用 grep 命令从详细输出中筛选出特定的环境变量。
记得替换为你感兴趣的 pod 名称:
kubectl describe pod <pod-name>
检查服务名称和端口号
尝试使用错误的服务名称和端口号进行微服务间通信是另一个常见问题。例如,如果 gateway 微服务向 metadata 微服务发送请求失败,你应检查两端是否使用了正确的服务名称(例如“metadata”)和端口号(例如 80)。
首先检查调用微服务的代码或环境变量(在此示例中为 gateway 微服务),确保它使用了正确的服务名称和端口号。其次,检查被调用微服务的 Kubernetes 服务配置,确认实际的名称和端口号是否与你的设想相符。
你可以使用以下命令查看所有服务的详细信息:
kubectl get services
要获取特定服务的详细信息,请使用以下命令:
kubectl get services <service-name> -o yaml
kubectl describe service <service-name>
11.4 可靠性与恢复
尽管不可能完全避免故障,但我们可以采取多种措施来管理应用程序中的问题,确保在面对故障时服务能持续运行。一旦应用程序上线,我们希望它能在一定的可靠性标准下运行。有许多策略可以帮助我们构建出结构健壮、高可靠的系统。本节将概述一些实用的方法和技术,以构建能够从故障中迅速恢复的容错系统。
11.4.1 实施防御性编程
首先,我们需要采用防御性编程的方式进行编码。这种方法预设可能会发生错误——尽管我们无法预知这些错误会是什么。编程时应始终考虑以下情况:
- 代码可能会接收到不正确的输入。
- 代码中可能潜藏着尚未发现的漏洞。
- 将来可能会引入新的漏洞。
- 我们依赖的组件(如 RabbitMQ)也可能不是百分之百可靠,偶尔也会出现问题。
采用防御性思维编程,我们将自然寻找方式让代码在面对意外时能更优雅地运行。容错的实现从编码开始,源于每个微服务的内部。
11.4.2 实施防御性测试
正如我们在第 9 章中讨论的,测试对于构建弹性和可靠的系统至关重要。在这里,我想强调的是,测试不应仅限于“正常”代码路径。我们还应该测试软件是否能妥善处理错误情况。这是继防御性编程之后的下一步。
我们需要进行防御性测试,编写能够主动挑战我们代码的测试用例。这有助于我们识别哪些代码区域需要进一步加固。我们的代码应能优雅地恢复,有效地报告错误,并妥善处理异常情况。
11.4.3 保护我们的数据
所有应用程序都需要处理用户数据,我们必须采取适当措施,在出现故障时保护这些数据。即使发生意外故障,我们也要确保最关键的数据不会遭到破坏或丢失。虽然漏洞不可避免,但数据丢失是不应该发生的。
并非所有数据的重要性都相同。系统内部生成的数据(因此可再生产的数据)相较于从客户处获得的数据,重要性可能较低。虽然所有数据都重要,但我们需要重点保护的是原始数据。
保护数据的首要步骤是进行备份,并且这些备份应该是自动化的。大多数云服务提供商都提供了可启用的备份解决方案。
注意 不要忘记练习从备份中恢复!如果我们无法从备份中恢复,那么备份本身就毫无用处。
在最坏的情况下,我们应能够从备份中恢复丢失或损坏的数据。业界有一句话:如果数据不至少存在于三个地方,那么就等于不存在。以下是一些保护数据的其他建议:
- 数据捕获后应立即安全地记录。
- 永远不要编写可能覆盖原始数据的代码。
- 永远不要编写可能删除原始数据的代码。
捕获数据的代码是应用程序中最关键的部分之一,应当受到适当的重视。这部分代码应当经过极其严格的测试,并尽可能保持简洁,因为简单的代码可以减少潜在漏洞和安全问题。
我们避免覆盖或删除原始数据,是因为代码中的漏洞可能轻易地损坏或销毁数据。既然我们知道漏洞是不可避免的,我们就应该有防御性思维,预见到可能发生的意外。有关处理和保护数据的更多信息,请参考我在《使用 JavaScript 进行数据整理》一书中的讨论(Manning, 2018)。
11.4.4 实现复制和冗余

当任何一个微服务发生故障时,负载均衡器会立即将传入的请求重定向到其他实例。同时,Kubernetes 会重新启动故障实例。这种冗余措施意味着即使面对间歇性故障,我们也能保持服务的连续性。通过复制实现冗余不仅提高了性能,而且增强了系统的可靠性。我们将在第 12 章更详细地讨论这个话题。
尽管我们的系统能够处理故障,但我们不应对这些故障掉以轻心。所有的故障都应被记录并随后进行调查。我们可以使用第 11.3 节中讨论的调试过程来识别并修复故障的根本原因。
在 Kubernetes 中实现复制
到目前为止,我们为 FlixTube 部署的每个微服务都只有一个实例。对于学习用途的应用程序(如 FlixTube)或在开发微服务应用的初期,这种做法是可以接受的。然而,这样做的容错能力可能并不理想。
不过,这很容易改进,因为 Kubernetes 让我们能够轻松创建副本。惊人的是,只需在我们已编写的 Kubernetes 部署 YAML 文件中更改一个字段的值——这展示了基础设施即代码的强大能力。
我们可以简单地通过调整 Kubernetes 部署中任何微服务的 replicas 属性来增加副本数量。你可以在清单 11.2(添加到第 10 章/scripts/cd/gateway.yaml)中看到 gateway 微服务的一个示例,这是对第 10 章中 YAML 代码的更新。
副本数量已从一个增加到三个。应用这一更改后,我们的 gateway 微服务将有三个副本来处理请求,而不再只有一个。通过这一小改动,我们显著提升了应用程序的可靠性和容错能力。
清单 11.2 为 gateway 微服务增加副本
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
spec:
replicas: 3 # 将副本数量设置为 3。应用此部署将创建三个 gateway 微服务的副本。
selector:
matchLabels:
app: gateway
template:
metadata:
labels:
app: gateway
spec:
--snip--
11.4.5 故障隔离与优雅降级
微服务架构的一个显著优势是能够实现故障隔离。然而,在实际应用中需要谨慎操作。我们的目标是在集群中隔离问题,尽量减少对用户的影响。
通过适当的策略,我们的应用程序可以优雅地处理故障,防止这些问题在用户端显现。我们需要采用的工具包括超时、重试、断路器和隔板等,这些将在后续部分详细介绍。
例如,假设视频上传微服务出现故障,无法正常运作。在我们努力修复并恢复服务的同时,我们的客户还希望继续使用我们的产品。如果没有适当的预防措施,这些错误可能会传播到前端,导致服务中断,严重影响客户体验。
相反,我们应该实施一系列保护措施来防止广泛的用户中断。图 11.13 展示了这一策略的效果。图中上半部分显示了错误如何传播至用户并造成问题。图 11.13 的下半部分则展示了理想的处理方式:网关阻止了错误传播,从而将故障限制在集群内部。

如果采用单体架构,其中一个组件(如视频上传组件)的故障通常会导致整个应用崩溃,进而使客户无法使用服务。而在微服务架构中,一个服务的故障可以被隔离,整个应用依然能够运行,尽管性能可能有所下降。
这种故障隔离的概念被称为隔板模式,其原理类似于船只使用的防水隔板。当船体某部分泄露时,隔板可以防止水流扩散到其他部分,从而避免船只沉没。这在现实世界中是一种有效的故障隔离方式,其在微服务应用中的应用也是类似的。
11.4.6 实现容错的基本技巧
这里是一些你可以立即采用的基本技巧,用于在微服务应用中实现容错和故障隔离。
超时
在本书中,我们通过使用 Node.js 的内置 http.request 函数和 Axios 库在微服务之间进行内部 HTTP 请求。通常这些请求在集群内迅速得到响应。然而,有时内部微服务可能会停止响应。
未来,我们还可能需要对外部服务发起请求。设想一下,我们想将 FlixTube 与 Dropbox 集成,以便导入新视频。在对外部服务如 Dropbox 发起请求时,我们无法控制其响应时间。外部服务可能偶尔进行维护,因此可能间歇性地不响应我们的请求。
我们必须考虑如何处理这些无响应的请求。如果一个请求在短时间内无法完成,我们希望能在某个设定的最大时间后自动终止该请求。如果不设置终止,请求可能会耗费很长时间,甚至永远不会结束。我们不能让客户等待这么久!我们宁可快速终止请求并告知客户发生了问题,也不愿让他们无休止地等待。
我们可以通过设置超时来解决这个问题。超时是指在请求自动终止并返回错误之前可以经过的最长时间。为请求设置超时可以帮助控制应用程序对故障的反应,实现快速失败。与慢速失败相比,快速失败使我们能够更迅速地响应问题,避免浪费客户的时间。
设置 Axios 超时
查阅 Axios 文档,我们得知其默认超时设置为无限,意味着没有明确设置时,Axios 请求可能会无限期执行而不会自动中断。显然,我们需要为使用 Axios 进行的所有请求设置一个明确的超时限制。
虽然可以为每次请求单独设置超时,但这种方法效率低且易出错。幸运的是,Axios 允许我们为所有请求统一设置默认超时。
清单 11.3 设置 Axios HTTP 请求的默认超时
const axios = require("axios");
// 设置所有请求的默认超时时间为 2500 毫秒,即 2.5 秒
axios.defaults.timeout = 2500;
实施重试机制
我们知道,即使是最可靠的服务,HTTP 请求也有可能失败。我们无法控制外部服务,也无法审查它们的代码,因此很难预测它们的可靠性。
一个简单有效的解决方案是对失败的操作进行重试,希望在后续尝试中能够成功。图 11.14 展示了这个概念。在这个例子中,假设 FlixTube 的视频存储微服务正在尝试从 Azure 存储检索视频。有时,这类请求可能会因为不明的连接错误而失败。如图 11.14 所示,尽管最初两次请求因间歇性连接问题失败,但第三次尝试最终成功了。

假设网络是完全可靠的,这是分布式计算中的一个常见误区,因此我们必须采取措施来应对请求失败的情况。在 JavaScript 中实现重试逻辑并不复杂。在清单 11.4 中,你可以看到一个用于多次尝试执行操作(如 HTTP 请求)的重试函数示例。
清单 11.4 还包含了一个实用的 sleep 函数,用于在尝试之间进行暂停。立即重试请求并没有太大意义,因为如果太过急迫,请求可能会再次失败。在这种情况下,我们会在下一次尝试前给予一段时间的间隔。
清单 11.5 展示了如何使用重试函数来包装一个 HTTP GET 请求的示例。在这个例子中,我们允许请求重试三次,每次之间暂停 5 毫秒。
清单 11.4 实现 JavaScript 中的重试函数
async function sleep(timeMS) {
return new Promise((resolve) => {
setTimeout(() => { resolve(); }, timeMS);
});
}
async function retry(operation, maxAttempts, waitTimeMS) {
let lastError;
while (maxAttempts-- > 0) {
try {
const result = await operation();
return result;
} catch (err) {
lastError = err;
if (maxAttempts >= 1) {
await sleep(waitTimeMS);
}
}
}
throw lastError;
}
清单 11.5 使用重试函数的示例
await retry(
() => axios.get("https://example.com/resource"),
3,
5
);
11.4.7 高级容错技术
虽然我们已经探讨了一些简单的技术来提升应用程序的可靠性和弹性,但还有更多高级技术可以进一步增强容错能力和故障恢复速度。
虽然这些内容略超出本书的基本范畴,我仍希望简要分享一些高级技术。这些方法在你构建更复杂的应用程序架构时将非常有用。
作业队列
作业队列 是许多先进应用程序架构中的常见组件,这与我们在 RabbitMQ 中看到的消息队列类似,但功能更为强大。
作业队列用于管理那些需要大量处理的任务。想象一下,在未来版本的 FlixTube 中,每个上传的视频都需要进行大量处理,如生成缩略图或转换视频格式以便在移动设备上播放。这些都是视频上传后需要完成的、CPU 和存储密集型的任务,但并不需要立即执行。
假设有 1000 个用户几乎同时上传视频。假如我们尚未实施弹性扩展(将在第 12 章讨论),那么如何管理这些视频上传带来的巨大处理负载?作业队列在这里发挥作用。你可以在图 11.15 中看到它的工作机制。

作业队列将待执行的任务记录在数据库中,这增强了对故障的容忍性。即使整个应用程序崩溃并重启,只要数据库仍然存在,就可以重新加载作业队列并继续处理未完成的任务。单个任务也可能失败,例如,处理它的微服务崩溃,但因为失败的任务不会被标记为完成,它们会在稍后自动重试。
此外,作业队列可以优化处理性能。我们可以将负载分散在更长的时间内进行,而不是一次性处理 1000 个上传的视频。它还可以在非高峰时段进行这些任务,意味着我们不必为一次性大规模处理支付额外的计算费用。
断路器
断路器 类似于超时的更高级形式。它具备内置智能,能够识别问题发生的时刻,并更智能地处理这些问题。图 11.16 展示了断路器的工作原理。

在正常操作中,断路器保持开启状态,允许请求通行(1)。如果对某个资源的请求突然失败(2),断路器会切换到关闭状态(3)。在关闭状态下,断路器会立即拒绝所有新请求,防止进一步的失败。
断路器类似于一个“超级”超时机制。它能够识别上游系统出现的问题,并立即拒绝新的请求,而不是继续尝试。
这种快速失败策略正是我们采用超时的原因。快速失败总比慢速失败更好,因为它允许我们更迅速地响应问题。
断路器会定期自动检查(延时可配置),查看上游服务是否已恢复正常。一旦服务恢复,断路器就会切换回开启状态(4),允许新的请求通过。实现断路器比实现简单的超时或重试机制要复杂得多,但如果你需要更高级的容错技术,它绝对值得考虑。
11.5 深入学习
至此,你已经掌握了多种技术,以确保微服务的健康与可靠性!如果想深入了解生产环境中的可观测性、日志记录与监控,请参考以下推荐书籍:
- 《Cloud Observability in Action》作者:Michael Hausenblas(Manning,2023 年)
- 《Observability Engineering》作者:Charity Majors、Liz Fong-Jones、George Miranda(O’Reilly 2022 年)
- 《Software Telemetry》作者:Jamie Riedesel(Manning,2021 年)
- 《Logging in Action》作者:Phil Wilkins(Manning,2022 年)
- 《Elasticsearch in Action, 2nd ed.》作者:Madhusudhan Konda(Manning,2023 年)
还有一本关于应用崩溃测试的杰出著作:
- 《Chaos Engineering》作者:Mikolaj Pawlikowski(Manning,2021 年)
总结
微服务应用程序通常比传统单体应用更为复杂,这种额外的复杂性意味着发现和解决持续性问题可能更具挑战性,这是每个开发者都必须面对的现实。
为了维持应用的健康,我们需要执行以下关键操作:
- 观察应用的行为,了解其历史和当前状态。
- 在问题出现时采取措施,以保护我们的用户。
- 对问题进行分级处理,优先解决最严重的问题。
- 调试并根据需要修复问题。
以下方法可以帮助我们监控和管理微服务的健康状况:
日志记录
异常处理
自动健康检查
可观测性技术
我们可以利用
kubectl、Stern 或 Kubernetes 仪表板查看 Kubernetes 中的 pod 日志。可观测性是展示我们分布式应用行为的复杂和详尽的技术,它使我们能够:
- 深入理解应用行为,提出问题并找到正在发生的事件的答案。
- 将微服务间的联系串联起来,追踪请求和消息链,真正洞悉内部发生的事。
与单体应用相比,微服务应用可以展现出更强的容错性。单个微服务的崩溃可以被局限在集群内部,从而保护我们的用户不受影响。
这里有一些有效的容错技术:
- 利用 Kubernetes 健康检查自动重启失败的微服务。
- 通过冗余管理崩溃,意味着如果我们有多个备用的微服务实例,当其中一个崩溃时,总有另一个准备接管并继续工作。
- 设置超时,以确保挂起的网络请求能及时中止。
- 自动重试失败的网络请求。
- 利用作业队列确保关键任务得以完成,即使在微服务处理任务时发生崩溃。
- 使用断路器,这是一种结合了超时和重试的高级技术。