← 返回蜂巢洞察

当无法在本地重现生产环境中出现的问题时,该如何进行诊断呢?

每个开发者最终都会遇到同样的令人沮丧的问题。 有客户报告称,你的应用程序在生产环境中出现了故障。你在开发机器上使用完全相同的工作流程进行测试,但一切运行得都非常正常。你的团队成员也无法重现这个问题。自动化测试也通过了,代码中也没有任何明显的变动能够解释这种故障现象。 然而,客户们仍然在遇到这个bug。 这类问题是最难解决的,因为问题的根源往往并不在于代码本身,而在于代码运行的环境。配置差异、基础设施问题、流量模式的变化、操作系统不同、依赖关系的差异,或者生产环境中的数据差异,都可能导致那些在开发阶段根本不会出现的问题。 事实是:这些困难大多是我们自己造成的。你管理的每一台服务器、你搭建的每一个

每个开发者最终都会遇到同样的令人沮丧的问题。

有客户报告称,你的应用程序在生产环境中出现了故障。你在开发机器上使用完全相同的工作流程进行测试,但一切运行得都非常正常。你的团队成员也无法重现这个问题。自动化测试也通过了,代码中也没有任何明显的变动能够解释这种故障现象。

然而,客户们仍然在遇到这个bug。

这类问题是最难解决的,因为问题的根源往往并不在于代码本身,而在于代码运行的环境。配置差异、基础设施问题、流量模式的变化、操作系统不同、依赖关系的差异,或者生产环境中的数据差异,都可能导致那些在开发阶段根本不会出现的问题。

事实是:这些困难大多是我们自己造成的。你管理的每一台服务器、你搭建的每一个日志处理流程、以及你手动维护的每一份配置文件,都在无形中增加了系统的复杂性。而当你面临生产环境中的问题时,这种复杂性就会带来巨大的麻烦——尤其是在系统出现故障、客户正在等待解决方案的时候。

幸运的是,那些仅在生产环境中才会出现的bug是可以被系统地排查出来的。在这篇文章中,你将学习如何利用日志、指标、分布式追踪以及环境分析等方法来解决这些问题。同时,你也会了解到:当应用程序运行在平台即服务(PaaS)环境下时,出问题时进行调试会变得容易得多,因为这些问题的“代价”其实是由其他人来承担的。

我们将涵盖的内容:

为什么生产环境中的表现会不同?

许多开发者认为,生产环境只不过是他们本地开发环境的放大版而已。

实际上,生产环境往往存在很大的差异。

一个生产环境中的应用程序可能会运行在多台服务器或容器上,并且这些服务器或容器会通过负载均衡器进行连接。该应用程序可能需要访问包含数百万条记录的数据库,与第三方API进行交互,使用分布式缓存系统,处理后台任务,同时还要为数千名并发用户提供服务。

即使是一些看似微小的差异,也可能会引发意想不到的问题。

想象一下,在本地测试API时,你使用了像“John Smith”这样的简单英文名称,一切都很顺利。但在生产环境中,如果客户输入了一个包含表情符号或带重音字符的名称,就会导致编码问题,而这种问题在之前的测试中根本没有被发现。

或者,你的应用程序可能认为某个环境变量总是存在的,因为所有开发人员的机器上都配置了这个变量。但在部署过程中,这个变量被意外遗漏了,从而导致生产环境中的请求失败。

代码本身并没有发生变化,变化发生在环境配置上。

注意这些故障的共同点:它们都不是业务逻辑方面的问题,而是环境配置导致的问题。你们团队自己管理并手动配置的每一项基础设施,都可能成为环境设置与代码预期出现差异的根源。你管理的基础设施越多,这类问题出现的可能性就越大。

认识到生产环境与开发环境的差异,是诊断这些问题的第一步。

以事实为依据,而非凭猜测开始行动

当生产环境出现问题时,人们往往会立刻想要去修改代码。

但请抵制这种冲动。

解决复杂问题的最快方法是在进行任何修改之前先收集足够的证据。

首先可以尝试回答以下这些问题:

  • 这个问题是什么时候开始的?

  • 它是在部署之后立刻出现的吗?

  • 它是影响所有客户,还是只影响一小部分用户?

  • 是不是所有的应用程序实例都会出现这个问题?

  • 在问题出现的同时,基础设施的监控数据有没有发生变化?

每一个问题的答案都能帮助缩小排查范围。

但有一点很少有人会告诉你:你回答这些问题的速度,在很大程度上取决于你的基础设施配置,而不是你的调试技巧。

如果部署相关的信息存储在不同的系统中,日志数据又在另一个系统里,而监控数据又储存在第三个系统中,那么即使只是回答第一个问题,也需要登录三个工具并手动比对时间戳。这样的排查过程可能会在开始之前就陷入停滞,原因并不是因为问题本身难以解决,而是因为你的工具使用起来非常不便。

与其猜测可能出什么问题,不如先整理出一套事件发展的时间线,这样就能更快地找到问题的根源。

良好的调试工作实际上是一种调查过程,而不是实验。而调查的效率高低,完全取决于你是否能够方便地获取所需的证据。

日志能告诉你发生了什么

在发生故障时,应用程序日志通常是获取信息的首选来源。

遗憾的是,许多应用程序生成的日志几乎不包含任何有用的信息。

处理请求时出现错误。这样的日志几乎没有任何价值。

时间戳:2026-07-13T09:41:17Z 请求ID:91df72 客户ID:48291 端点:POST /orders 数据库:OrdersDB 处理耗时:3.2秒 异常类型:TimeoutException相比之下,这个日志就包含了很多有用的信息:

时间戳:2026-07-13T09:41:17Z 请求ID:91df72 客户ID:48291 端点:POST /orders 数据库:OrdersDB 处理耗时:3.2秒 异常类型:TimeoutException现在你可以知道故障发生的时间、哪位客户遇到了问题、哪个端点受到了影响、请求处理花了多长时间,以及导致故障的具体原因是什么。

那么,这些日志分别是如何生成的呢?第一种日志是开发者在编写代码时为了快速处理异常而添加的,通常是这样写的:catch (Exception) { logger.LogError("处理请求时出现错误."); } 虽然异常被捕获了,但关于这个异常的所有有用信息都被忽略了。日志中只记录了“发生了故障”,却没有说明“发生了什么故障”、“在哪个地方发生的”或“是哪位客户遇到了问题”。

catch (TimeoutException ex) { logger.LogError(ex, "为客户{CustomerId}在端点{Endpoint}上处理订单时出现了错误,耗时{Duration}秒", customerId, "POST /orders", stopwatch.Elapsed.TotalSeconds); } 第二种日志并不是由什么高级工具生成的,而是开发者在编写代码时就考虑到了未来需要哪些信息供调查人员使用。

实际上,有用的日志是开发者通过一些有意识的习惯才生成出来的。

  • 一定要记录异常对象本身,而不仅仅是错误信息,这样异常的类型和堆栈跟踪信息才能被保留下来。

  • 要自动添加请求的相关信息。大多数Web框架都允许你在中间件中为每条日志添加请求ID或客户ID等信息,这样就无需在每一条日志记录中重复这些信息。例如,在ASP.NET Core中,日志系统就是这么设计的。

  • 要记录代码的具体执行内容,比如调用了哪个端点、依赖了哪些下游服务、以及这些操作花费了多少时间,因为这些都是调查人员首先会询问的信息。

catch (TimeoutException ex) { logger.LogError(ex, "为客户{CustomerId}在端点{Endpoint}上处理订单时出现了错误,耗时{Duration}秒", customerId, "POST /orders", stopwatch.Elapsed.TotalSeconds); } 下面是这些规则在代码中的体现:
catch (TimeoutException ex) { logger.LogError(ex, "为客户{CustomerId}在端点{Endpoint}上处理订单时出现了错误,耗时{Duration}秒", customerId, "POST /orders", stopwatch.Elapsed.TotalSeconds); } 一个很好的经验法则是:编写日志时,要考虑到六个月后那些从未看过这段代码的调试人员需要了解哪些信息。而这些人往往就是你自己。

catch (TimeoutException ex) { logger.LogError(ex, "为客户{CustomerId}在端点{Endpoint}上处理订单时出现了错误,耗时{Duration}秒", customerId, "POST /orders", stopwatch.Elapsed.TotalSeconds); } 注意,上面的日志中使用了{CustomerId}这样的命名占位符,而不是字符串插值。这就是结构化日志记录:所有的信息都不是被简单地合并成一句文本,而是作为独立的字段与日志内容一起存储,通常是以JSON格式保存的。上面的这条日志可能会被存储为如下形式:
{
  "message": "在尝试通过POST请求访问/orders接口为客户48291创建订单时,操作在3.2秒后失败",
  "CustomerId": 48291,
  "Endpoint": "POST /orders",
  "Duration": 3.2,
  "Exception": "TimeoutException"
}

结构化日志带来的最大好处就是便于搜索。对于纯文本日志来说,要查找某个客户的所有失败记录,就需要进行模糊匹配,而且这完全取决于运气;而使用结构化日志,监控系统就可以执行像`CustomerId = 48291 AND Exception = TimeoutException`这样的精确查询,从而在几秒钟内从数百万条记录中筛选出所需信息。Serilog这类库,以及.NET中的内置`ILogger`,都支持这种功能。

记录错误的目的并不仅仅是为了保存日志本身,而是要提供足够的上下文信息,这样那些负责调查问题的人就能立即开始提出正确的问题。

不过,这里也有一个需要注意的地方:如果无法找到这些日志,那么再好的日志也毫无用处。

在自我管理的环境中,日志往往分散在不同的服务器上,因此各团队不得不自行构建和维护数据汇总机制,才能让这些日志具备可搜索性。这样就会浪费大量的工程资源,而这些资源本应该被用于开发产品本身。

如果你的团队一直在维护自己的日志传输系统,那么就有必要思考这样一个问题:为什么我们还要自己来做这件事呢?

日志可以记录单个事件的具体情况,而指标则能够反映整个系统的运行状况。

假设用户反馈称你的应用程序每天下午都会变慢。如果只是阅读成千上万的日志记录,可能根本发现不了任何异常现象;

但通过指标仪表板,就能立刻看到CPU使用率在下午时分会飙升至90%以上,内存消耗量会持续增加,数据库响应延迟会在午餐后翻倍,而在流量高峰期HTTP错误率也会急剧上升。

让我们以最常见的开源方案为例来具体说明这一点:使用Prometheus来收集指标数据,然后利用Grafana来进行可视化展示。

这个流程分为三个步骤。首先,你的应用程序需要暴露自己的指标数据。大多数开发框架都提供了相应的功能。在ASP.NET Core中,只需添加prometheus-net包并配置一行代码,就能生成一个//metrics接口,用于报告请求总数、响应耗时以及错误数量等指标。

其次,Prometheus服务器会每隔几秒读取一次这些指标数据,并将其存储为时间序列数据。

最后,Grafana会将这些时间序列数据转化为可视化的仪表板。

一旦这样的系统建立起来,对于“每天下午应用程序变慢”这一问题的调查就不再需要靠猜测了。你只需打开Grafana,设置时间范围为过去三天,然后针对Prometheus执行如下查询:

rate(http_request_duration_seconds_sum[5m])
/ rate(http_request_duration_seconds_count[5m])

这个查询就能显示出你的应用程序的平均响应耗时变化情况。如果图表显示从下午1点开始到4点期间,延迟值一直在不断上升,那么你几乎可以在一分钟内确认这一规律的存在。如果再添加一个面板,在同一时间轴上展示CPU使用率或数据库连接数的变化情况,就能判断出是哪种资源首先出现了问题,从而找出问题的根源。

这些观察结果立刻为你的调查指明了方向。你不再需要思考从哪里开始,而是清楚地知道问题发生在什么时候,以及是哪个组件处于高压状态。

各种指标能将零散的故障转化为可识别的模式。

但是,这个监控面板并不是自动搭建起来的。必须有人来安装相关工具、配置数据输出模块、确定时间序列数据库的大小,并确保整个监控系统能够正常运行。

在许多团队中,监控系统本身也变成了另一个需要维护和调试的生产系统。对监控系统进行进一步的监控,实际上就是一种极其荒谬的“基础设施维护工作”;而这种情况也清楚地表明,你的团队承担了本不必承担的运营负担。

分布式追踪连接了所有服务

现代应用程序很少是由单一应用与单一数据库构成的。

一个客户请求在得到响应之前,可能会经过API网关、认证服务、订单处理服务、支付系统、库存管理系统、缓存系统、消息队列以及数据库等环节。

当出现问题时,究竟是哪个服务导致了延迟呢?

分布式追踪能够解答这个问题。一条追踪记录会完整地展现某个请求在系统中流动的整个过程。追踪中的每一個处理步骤——比如一次服务调用或一次数据库查询——都被称为跨度,每个跨度都会记录其开始时间以及耗时长度。

以下是在Jaeger或Zipkin等工具中查看追踪记录的实际示例:假设有客户反映结账流程超时了,你根据他们的请求ID查找追踪记录,就会看到如下这样的信息:

Trace 8f3ac21 — POST /checkout — total: 4.61s

api-gateway            ████                                    45ms
auth-service         ██                                      38ms
order-service        ████████████████████████████████  4.51s
    inventory-db query ██████████████████████████████████    4.29s  ⚠
payment-api        ███                                    210ms
response             █                                       12ms

阅读这样的追踪记录只需要几秒钟。在整个请求处理过程中,有4.29秒的时间被用于一次数据库查询操作。网关、认证服务和支付API都运行正常,因此没有人需要进一步调查它们,也不需要进行任何猜测。

从底层原理来看,这种机制之所以能够实现,是因为第一个服务会为请求分配一个唯一的追踪ID,并在后续的所有调用中都将这个ID作为头部信息传递下去。每个服务都会将自己对应的处理步骤记录在这个相同的ID下,这样追踪系统就能将整个请求的处理过程重新还原出来。

所有这些功能的开放标准是OpenTelemetry,它为各种主流编程语言提供了相应的开发工具包;在许多框架中,启用这一功能只需要编写几行配置代码,而无需进行繁琐的手动修改。

如果没有追踪功能,工程师们往往需要花费数小时来排查错误的服务。而有了追踪机制,那些运行速度最慢或出现故障的组件通常能在几秒钟内被识别出来。

问题在于,自行搭建追踪系统实际上是一项庞大的工程任务,而非简单的操作步骤。为每个服务添加追踪功能、部署数据收集工具以及存储追踪数据都需要投入大量的精力,正因如此,许多明知需要追踪功能的团队仍然没有实施这一措施。

当可观测性是需要通过人工构建才能实现的,而不是由平台直接提供的功能时,它往往就会长期被列在开发计划中,而各种问题却依然会如期出现。

尽可能真实地复现生产环境

有时候,日志和追踪数据并不足够。最终,你还是需要重新创建生产环境来进行测试。

这并不一定意味着要把生产环境的数据库复制到笔记本电脑上。相反,你需要找出不同环境之间的差异。

  • 生产环境中使用的是Linux系统,而开发人员使用的却是Windows或macOS吗?

  • 生产环境使用了Redis作为数据存储工具,而开发环境却没有使用它吗?

  • 两种环境中安装的运行时版本是否不同?

  • 请求是经过反向代理服务器转发的吗?

  • 生产环境处理的数据量是否远大于开发环境?

  • 生产环境会同时接收数百个请求,而开发环境却只接收一个请求吗?

每一个这样的差异都可能是导致问题的原因。

那么,该如何消除这些差异呢?有几种方法可以解决大部分问题:

将应用程序容器化

如果生产环境是使用容器来运行你的应用程序,那么在本地环境和测试环境中也应该使用相同的容器镜像。这样就可以一次性消除操作系统、运行时版本以及依赖项之间的差异,因为容器本身就会携带其运行环境。

将基础设施和配置定义为代码

像Redis、反向代理服务器这样的服务及其相关设置,应该通过配置文件(如`docker-compose.yml`、Kubernetes配置文件或Terraform脚本)来定义,而不是通过手动操作来进行配置。

当测试环境和生产环境都是根据相同的配置文件生成的时,它们就不可能出现不一致的情况。当你需要了解生产环境是否使用了反向代理服务器或什么运行时版本时,可以直接从配置文件中获取这些信息,而无需去询问负责设置服务器的人员。

使数据具有真实性

实际上,你很少需要使用真实的生产环境数据,而且出于隐私考虑,通常也不应该使用这类数据。你需要的是那些在“格式”上与生产环境数据相似的数据:数据量大致相同,数据结构也类似。

可以在测试环境中生成几百万条数据记录,并包括一些特殊的情况,比如带有重音符号的名称、表情符号、包含大量空值的记录以及非常长的字符串。

模拟生产环境下的流量

那种只有在上百个请求同时发起时才会出现的错误,如果你一次只测试一个请求,是永远无法发现的。像k6或JMeter这样的负载测试工具,可以通过简单的脚本在测试环境中模拟真实的并发场景,这样往往就能重现那些导致程序异常或连接池耗尽的问题。

你的测试环境越接近生产环境,就越有可能在客户遇到问题之前就发现那些仅在生产环境下才会出现的故障。

需要再次强调的是:当这两种环境都是人工构建的时候,保持它们之间的一致性实际上是一项长期需要持续维护的工作。因为只要其中某个环境进行了修改,而另一个环境却没有随之更新,两者之间就会产生差异。

而对于那些使用相同配置生成所有环境的团队来说,他们一开始就需要处理的生产环境特有的错误自然也就更少。

隔离环境变量

最有效的调试方法之一就是一次只修改一个变量。

假设你的应用程序只在生产环境中会出现故障,那么可能导致这种差异的因素可能包括操作系统版本、数据库引擎、容器配置、环境变量、内存限制、网络延迟或基础设施设置等等。

不要同时修改多个变量,而应该逐一进行测试。 举个实际的例子:假设某个API接口在生产环境中会崩溃,但在本地环境下可以正常运行。如果你发现有三处不同之处:生产环境使用的是PostgreSQL 16版本,而开发环境使用的是15版本;生产环境的容器内存限制为512MB,而开发环境则没有这个限制;此外,生产环境将环境变量设置为ENVIRONMENT=production,而开发环境没有这个设置。

不要一次性修改这三项内容,而应该逐一进行测试,同时确保其他所有配置都保持不变:
# 测试1:只改变数据库版本
docker run -d -p 5432:5432 postgres:16
# → 运行会出现故障的请求。仍然可以正常运行吗?如果是这样,说明问题出在数据库版本上,将版本改回15即可。

# 测试2:只改变内存限制
docker run --memory=512m my-app
# → 运行会出现故障的请求。如果出现了OutOfMemoryError错误,那么问题就出在内存限制上。

如果这个错误只在测试2中出现,那么你就找到了问题的根源;同时,这也意味着你已经排除了其他可能的原因。如果你同时改变了数据库版本和内存限制,然后发现程序仍然会崩溃,那你还是需要进一步判断到底是哪个设置导致了问题。

这种有条理的调试方法通常能比随意尝试更快地找到真正的问题原因。 另外,还需要注意一下那些环境变量:几乎每一个这样的变量存在的理由都是因为你的团队负责维护应用程序所依赖的基础设施。你个人需要管理的配置项越少,未来需要隔离的变量也就越少。

一个仅在生产环境中出现的简单错误

app.MapGet("/discount", () =>
{
    string region = Environment.GetEnvironmentVariable("REGION");

    if (region.ToLower() == "eu")
        return Results.Ok("20%折扣");

    return Results.OK("10%折扣");
});

在开发阶段,这段代码运行得非常正常。

但当应用程序投入生产环境后,客户开始报告出现HTTP 500错误。

最终,日志中显示出了以下异常信息:

NullReferenceException

一旦知道该从哪里查找问题根源,这个故障其实并不难解决。

产生问题的原因在于:生产环境中的配置忘记设置REGION环境变量了。当对一个空值调用ToLower()方法时,程序会立即崩溃。

解决方法很简单:

string region = Environment.GetEnvironmentVariable("REGION") ?? "US";

if (region.Equals("EU", StringComparison.OrdinalIgnoreCase))
    return Results.Ok("20%折扣");

这个教训不仅仅在于如何进行空值检查,更重要的是要明白:那些仅在生产环境中出现的故障,往往是由于配置差异造成的,而不是因为业务逻辑存在错误。

如果没有详细的日志记录,开发人员可能会花费大量时间去审查代码,却完全忽略了配置问题。

再进一步来说,这类故障的存在本身就说明:人们必须亲自去设置那些配置变量。配置管理的缺失其实是一种运营上的失误,而这种失误正是手工管理部署配置所带来的必然结果。

当你发现自己不得不编写运行手册来提醒别人应该在哪些服务器上设置哪些变量时,那就真的应该认真思考:“为什么我们还要自己来做这些事情呢?”

自行验证部署配置

并不是所有的生产环境问题都源于你的源代码。

实际上,部署相关的问题相当普遍。

  • 可能是因为容器镜像没有得到更新。

  • 也可能是因为配置文件丢失了。

  • 数据库迁移可能失败了。

  • 环境变量中可能包含了错误的值。

  • 某些必要的密钥可能没有被正确部署。

  • 可能发生了回滚操作,导致使用了旧版本的应用程序,而大家却完全没有察觉到这一点。

在认为自己的代码存在问题之前,先确认生产环境确实运行的是你打算部署的版本。

很多问题其实只是因为运行了错误的版本才导致的。

所有这些生产环境中的故障,实际上都是基础设施配置管理方面的问题,而不是编程错误。在那些自行开发的部署流程中,由于缺乏足够的验证机制,这些问题就更容易发生。

如果你的团队无法立刻回答“当前运行的是哪个版本?”这个问题,那就说明你们的部署系统正在不断产生需要后期去排查的故障。

你真的需要PaaS吗?

在探讨PaaS如何改变调试过程之前,有一个值得诚实地回答的问题:每个团队都真的需要它吗?

并不一定。使用PaaS其实是一种权衡。你放弃了对基础设施的控制,并为此支付平台使用费用;作为交换,你就无需再把工程时间花费在服务器管理、流程配置以及监控系统的搭建上了。这种权衡是否值得,取决于你的具体情况。事实上,也有一些合理的理由让人选择不使用PaaS:

  • 如果你的基础设施有特殊需求:例如需要GPU进行计算、使用定制内核、采用特殊的网络架构,或者某些软件依赖特定的硬件环境,那么这些需求可能并不符合PaaS平台的限制。

  • 合规性要求或数据存储规定迫使你必须保持对基础设施的完全控制:在一些受监管的行业里,企业必须严格规定所有系统在哪里运行以及如何运行。

  • 当你的业务规模达到一定程度时,使用PaaS反而会带来更高的成本:对于那些处理量非常大的应用来说,PaaS平台按资源计费的模式所导致的额外费用,可能会超过雇佣专门维护基础设施团队的成本。因此,一些大型企业会选择自行搭建内部平台——其实这些平台本质上就是他们自己的PaaS系统。

  • 如果基础设施本身就是你的核心产品:如果你从事的是托管服务、网络建设或基础设施相关工具的开发工作,那么自己管理基础设施才是符合业务发展需求的做法。

对于其他大多数团队来说,是否需要使用PaaS,可以通过以下几个问题来判断:

  • 当生产环境出现故障时,在第一个小时里,有多少时间被用来查找问题原因,又有多少时间被用来采取补救措施

  • 团队中是否有人会在完成本职工作之余,还负责维护日志处理流程、监控系统或部署脚本?

  • 你是否能够一眼就能清楚地知道当前生产环境中运行的是哪个版本的应用程序?

  • 你上一次因为服务器配置错误、变量设置不一致等原因导致生产环境出现问题而损失了一整天的时间,那是什么时候?

如果这些问题的答案让你感到不安——对于大多数中小型团队来说,情况确实如此——那么你就等于在为基础设施管理付出代价,但却没有从中获得任何实际收益。真正重要的不是公司的规模,而是你的工程资源被用在了哪里。无论是只有两个人的初创企业,还是拥有五十名员工的团队,只要没有人专门负责维护服务器,他们都会受益匪浅。

如果你现在还不确定是否需要使用PaaS,那么很可能你确实需要它。那些有正当理由必须自行管理基础设施的团队,往往很清楚自己为什么需要这样做。

为什么在PaaS上调试更容易

在诊断生产环境中的故障时,最困难的部分往往不是找到问题的根本原因,而是获取进行调查所需的信息。

在传统的基础设施架构中,日志文件分散保存在多台虚拟机、容器、负载均衡器以及后台任务程序中。当应用程序需要横向扩展时,一个客户请求可能会经过多台服务器才能完成处理。开发人员往往要花费大量时间通过SSH连接进入这些机器,寻找日志文件,并分析各种时间戳数据,而这些工作其实远比直接调试问题本身要耗费更多精力。

那一刻,就是“基础设施税”到期的时候了。在处理某个事件时,每花费一个小时来收集证据,就意味着你的团队在几个月前就已经选择了让这些设备由自己来维护和运营,因此也就相应地承受了这些时间上的损失。

平台即服务(PaaS)彻底改变了这种状况。

PaaS并不把每一台服务器都当作需要单独管理的独立设备来处理,而是将你的应用程序视为一个整体服务。所有实例的日志会自动被汇总到一处,各项指标也会持续被收集起来,而且平台本身就内置了健康检查功能。无论你的应用程序是在一个容器中运行,还是分布在五十个容器中,你都可以通过一个统一的控制面板来查看所有情况,而无需使用几十个终端。

当生产环境中出现问题的时候,你可以立刻回答一些关键问题:

  • 这个问题是发生在最近一次部署之后吗?

  • 是所有的应用程序实例都出现了故障,还是只有个别实例出现问题?

  • 在应用程序崩溃之前,CPU使用率或内存占用量是否有突然上升的趋势?

  • 是哪个版本引入了这些问题呢?

这些信息根本不需要人工去收集,因为平台本身就已经将这些数据准备好了。

许多PaaS工具还会记录应用程序的部署历史,这使得人们能够轻松地比较每次版本更新前后应用程序的行为变化。如果在2.8.1版本发布后错误率突然上升,那么原因就一目了然了。而回滚到之前的部署版本通常只需要几分钟时间,这样就能大大减少系统停机的时间。

基础设施的一致性也是PaaS的一大优势。

通过PaaS部署的应用程序每次都会使用相同的配置进行构建。开发者们就不需要担心某台服务器上的运行环境是否过时、是否缺少某个依赖库、操作系统版本是否正确,或者环境变量是否设置错误了。这种一致的环境能够有效地预防许多只有在生产环境中才会出现的故障。

还记得之前提到的那个与REGION配置相关的故障吗?在那种配置只需声明一次就能在整个系统中得到应用的平台上,这样的故障根本就不会发生。

或许最大的好处就是响应速度更快了。

在系统出现故障时,工程团队不必浪费宝贵的时间去从多个系统中收集数据。集中式的日志记录、内置的监控功能、分布式追踪机制、部署历史记录以及健康检查功能,都能让他们立即开始进行故障排查。

这一切最终都会体现在更低的平均解决时间、更短的系统停机时间,以及为开发人员和客户带来的更好使用体验上。

构建易于调试的应用程序

生产环境中出现故障是不可避免的。无论工程团队的经验多么丰富,复杂的系统总会出现一些意想不到的问题。

成熟的工程组织与其他组织的区别并不在于是否会出现故障,而在于他们能够多快理解并解决这些故障。

请编写具有实际意义的日志,这些日志应能提供具体的上下文信息,而不仅仅是通用的错误消息。持续收集各种指标数据,这样在用户开始抱怨之前,就能及时发现性能方面的变化趋势。为你的应用程序配备分布式追踪系统,这样就可以追踪请求在各个服务之间的流转过程。尽量让测试环境与生产环境保持一致,并且要像对待应用程序代码一样认真地处理基础设施配置问题。

同样重要的是,要选择一个能够简化调试流程的平台,而不是让调试变得更加困难。

那些依赖手动管理服务器的团队,在遇到问题时,往往需要花费第一个小时的时间来收集日志并连接相关设备。而使用现代PaaS平台的团队,则可以从一开始就获得所需的所有信息:他们可以将应用程序的部署情况与错误发生的情况联系起来,查看所有应用程序实例的日志数据,分析基础设施指标,并追踪出现故障的请求流程,而且这一切都无需离开任何一个监控界面。

要清楚地认识到自己的团队属于哪一类。如果你的工程师们除了需要开发产品之外,还负责维护日志处理流程、监控系统、测试环境的配置以及部署脚本,那么你实际上是在以“事故处理所耗费的时间”这种最昂贵的代价来支付基础设施维护的成本。除非运营基础设施本身就是你们的主要业务,否则这项工作完全可以交给专业的平台来完成。

虽然PaaS平台无法完全避免所有生产环境中的错误,但它确实可以消除很多导致这些错误难以诊断的复杂操作环节。这样一来,人们用于查找信息的时间就会减少,故障的根本原因也能更快地被找到,事故处理的速度也会加快,从而有更多的时间专注于软件的开发工作,而不是基础设施的管理。

当下一个生产环境中的问题出现时(而这几乎是不可避免的),你不会把太多时间花在“为什么我无法重现这个错误?”这样的问题上,而会更多地思考这样一个更重要的问题:“我们当初为什么要自己来做这些事情呢?”

希望你喜欢这篇文章。你可以通过在LinkedIn上与我联系

相关文章

技术实践

客户端与服务器之间的通信方式:关于HTTP/1.1、HTTP/2、REST、WebSockets、GraphQL、gRPC以及Protocol Buffers的全面指南

你曾经构建并使用过API。你知道什么是GET请求,JSON响应的具体格式,也知道如何添加Authorization头部信息。你使用过REST,可能还尝试过GraphQL,或许也听说过gRPC。 但你是否真正了解:当你的应用程序发送请求时,实际上会发生什么?哪些数据会通过网络传输?为什么HTTP/2能提高传输效率?既然HTTP已经足够使用了,为什么还需要WebSockets?从根本层面来说,Protocol Buffers与JSON有什么不同? 而在设计系统时,你该如何决定该采用哪种通信方式呢? 这本手册正是为解答这些问题而编写的。 这并不是一本针对API初学者的指南。它深入探讨了客户端与服务器

阅读全文
技术实践

如何利用Gemini构建人工智能功能:面向开发者的提示工程实用指南

大多数关于提示工程的教学教程都遵循相同的流程:安装SDK,输入API密钥,调用 generateContent 函数,然后打印输出结果。模型会生成一些看似合理的内容,之后教学教程也就结束了。 但当你真正尝试将这个系统投入实际使用时,才会发现其实真正的准备工作根本还没有开始。 “API返回的文本”与“让用户感到可信的实际功能”之间的差距,正是需要耗费大量精力去解决的地方。 这个差距中充满了各种棘手的问题:模型生成的内容听起来和其他聊天机器人没什么两样;它会编造用户从未说过的话;它返回的数据会被用Markdown格式包裹起来;系统会在凌晨2点出现故障;而对于那些只是想得到答案的用户来说,系统展示的

阅读全文
技术实践

如何使用 shadcn/ui 在 React 中构建一个可重复使用的日期时间选择器

日期和时间选择器这类组件,在设计文件中看起来可能很简洁,但一旦开始实际开发,就会发现它们会消耗大量的资源。你需要一个日历、一个时间选择器,以及一个能够保证这两者同步的状态管理系统,通常还需要范围选择功能以及对应的多语言版本。 本指南将介绍一些现成的选择器组件,你可以直接将这些组件应用到你的React项目中:组合型日期和时间选择器、日期范围选择器以及时间选择器。 所有这些组件都可以作为 Shadcn日期和时间选择器 组件使用,你只需通过一条CLI命令即可安装它们,而无需从头开始开发。 这些组件都是基于Radix和Base UI的基础架构构建的,下面介绍的版本是使用Base UI实现的。此外,这些

阅读全文