java 检查远程服务器状态

Java 故障诊断 - 远程调试应用

我的一个朋友最近遇到了一个问题,他正在实现的软件的某个特定部分非常慢。 通常,当我们遇到这些类型的性能问题时,我们怀疑是 I/O 接口导致的(例如,到数据库的连接或对文件的读写)。 还记得在前面的文章中说过,这样的接口经常会降低应用程序的速度,所以它们很可疑。 但在我朋友的案例中,接口不是问题。

性能问题是由于简单地生成了一个随机值(存储在数据库中的通用唯一标识符[UUID])。 操作系统使用硬件资源(例如鼠标移动、键盘等)来收集随机性,称为(entropy)。 该应用程序使用这种随机性来生成随机值。 但是,当我们在虚拟机或容器等虚拟化环境中部署应用程序时(这在今天的应用程序部署中很常见), 操作系统可以创建其熵的来源更少。因此,有时熵不足以让应用程序创建它需要的随机值。 这种情况会导致性能问题,在某些情况下,可能会对应用程序的安全性产生负面影响。

如果不直接联系问题发生的环境,这种类型的问题可能真的很难调查。 对于这样的场景,远程调试可以成为解决方案。 您只能在特定的环境中检查某些情况。 假设你的客户端发现了一个问题,但当你在计算机上运行应用程序时,这个问题没有发生。 您肯定不能简单地告诉您的客户“它在我的机器上可以工作”来解决这个问题。

当您无法在计算机上重现问题时,您需要连接到发生问题的环境。 虽然有时你没有任何其他选择,不得不采取具有挑战性的方法来修复无法重新创建的问题,但在其他时候, 环境对远程调试是开放的。本章的主题是远程调试,即调试安装在外部环境中的应用(见图 1)。


本文首先讨论什么是远程调试,什么时候可以使用它,什么时候不应该使用这个方法。 然后,为了应用这种技术,我们将研究一个问题。 你将学习如何配置应用以进行远程调试,以及如何在远程环境中连接和使用 debugger。

什么是远程调试?

首先将讨论什么是远程调试,何时使用,何时避免。 远程调试无非是将前面文章中学到的调试技术应用到一个不在本地系统上运行,而是在外部环境中运行的应用程序上。 为什么需要在远程环境中使用这些技术?为了回答这个问题,让我们简要回顾一下典型的软件开发过程。

当开发者实现一个应用程序时,他们并不是为他们的本地系统编写的。 一个应用程序的最终目的是将其部署在生产环境中,帮助用户解决各种业务问题。 此外,在实现软件时,我们通常不会直接在用户环境,或生产环境中部署应用程序。 相反,我们使用类似的环境来粗略地测试我们需要实现的功能和修正,然后再把它们安装到正式使用真实数据的环境中。

如图 2 所示,开发团队在开发应用程序时至少要使用三种环境:

  • 开发环境 (dev) — 一个类似于应用程序将被部署的环境。开发人员主要使用这个环境来测试他们在本地系统上开发后实现的新功能和修正。
  • 用户验收测试环境 (UAT) — 一旦在开发环境中测试成功,应用程序将被安装在用户验收测试环境中。 用户可以测试新的实现和修复,并在应用交付到有真实数据的环境中之前确认它们的工作。
  • 生产环境 (prod) — 在用户确认新的实现方案按预期工作,并且他们觉得使用起来很舒服之后,该应用就被安装在生产环境中。


但是,如果一个实现在你的本地计算机上工作,但在另一个环境中却表现得不同呢? 你可能会想,一个应用程序怎么会有不同的工作方式。 即使是使用同一个编译过的应用程序,我们也可以观察到应用程序在两个不同环境中的行为差异。 造成这些差异的一些原因包括以下几点:

  • 应用程序环境中可用的数据是不同的。不同的环境使用不同的数据库实例、不同的配置文件等等。
  • 安装应用程序的操作系统不一样。
  • 部署的编排方式可能不同。例如,一个环境可以使用虚拟机进行部署,而另一个环境则使用容器化解决方案。
  • 权限设置在每个环境中可能不同。
  • 环境可能有不同的资源(分配的内存或CPU)。

这些只是许多可以使一个特定的输出或行为不同的事情中的一部分。 上一次我遇到这样的问题(不久前),应用程序产生了不同的输出,原因是向应用程序在实现的用例中使用的 web 服务发送了一个请求。 由于安全问题,我们不能在开发环境中使用相同的端点,我们也不能连接到应用程序在出现问题的环境中使用的端点。 这些情况使得调查具有挑战性(说实话,在我们开始调试之前,我们甚至没有考虑到一个端点是导致我们问题的原因)。

在这类情况下,远程调试确实可以帮助你更快地了解软件的行为。 然而,请记住一个关键的建议:永远不要在生产环境中使用远程调试(图 3)。 另外,请确保你总是了解你所使用的环境之间的主要区别。



提示

注意环境之间的差异,可以让你了解可能出现的问题。 它甚至可以为您节省研究问题的时间,只需了解这些细节就可以根据经验为您提供问题的答案。

你将了解到,你需要在应用程序执行过程中附加一个名为 agent 的软件,以启用远程调试。 附加调试 agent 的一些后果(以及为什么你不应该在生产环境中这样做)包括:

  • agent 会减慢应用程序的执行;这种减慢可能会导致性能问题。
  • agent 需要通过网络与 debugger 工具进行通信。为了实现这一点,你需要使特定的端口可用,这可能会导致漏洞问题。
  • 如果应用程序的同一部分正在其他地方同时使用,调试特定的代码会干扰功能。
  • 有时调试可能会无限期地阻塞应用程序,并迫使您重新启动进程。

在远程环境中进行调查

我们考虑调试一个在远程环境中运行的应用程序。 我将在第1小节中首先描述这个场景。 然后,在第2小节中,使用提供的一个应用程序(https://gitee.com/tunte/java-troubleshooting-sample.git 项目 da-ch4-ex1), 我们将讨论如何启动一个应用程序进行远程调试, 以及如何使用你在前面文章中学到的技术将 debugger 附加到远程运行的应用程序。

1 场景

假设你在一个团队工作,负责实现和维护一个大型的应用程序,许多客户用它来管理他们的产品库存。 最近,你的团队实现了一项新的功能,帮助你的客户轻松管理他们的成本。 团队在开发环境中成功地测试了该行为,并在UAT环境中安装了该应用,以便让用户在将该功能转移到生产中之前对其进行验证。 然而,负责测试新功能的人告诉你,显示新数据的 web 界面什么都没有。

你很担心,看了一下,很快发现问题不在前端。 但后端的一个端点似乎表现得很奇怪。 当端点在UAT环境中被调用时,HTTP 响应状态码是 OK,但应用程序并没有在HTTP响应中返回数据(图 4)。 你检查了日志,但那里也没有显示。 由于你无法在本地或开发环境中观察到这个问题,你决定在 UAT 环境中远程连接你的 debugger,找到这个问题的原因。

注意

即使我们讨论调试一个在远程环境中运行的应用程序,为了使例子更简单, 我们使用本地系统来运行应用程序,并远程连接到该系统。 出于这个原因,你会在图中看到,我使用 “localhost” 来访问运行应用程序的环境。 在现实世界中,应用程序将运行在一个不同的系统上,它将被识别为一个IP地址或DNS名称。


2 在远程环境中查找问题

我们使用远程调试来调查 1 小节中描述的案例研究。 我们首先配置并运行应用程序以连接到远程 debugger,然后附加 debugger,开始我们的调查。

该应用程序可能已经在实际情况下运行了,而且很可能还没有配置允许远程调试。 因此,我们从启动应用程序开始,以便您了解远程调试的全景,并了解这种方法的先决条件。

当启动你想要远程调试的应用程序时,你需要确保在执行过程中附加了 debugger agent。 要将 debugger agent 附加到Java应用程序执行,需要在 Java 命令行中添加参数 -agentlib:jdwp,如图 5 所示。 您必须指定要附加 debugger 工具的端口号。 基本上,调试 agent 充当服务器,监听 debugger 工具连接到配置的端口, 并允许该工具运行调试操作(在断点上暂停执行、step over、step into 等)。



你可以复制这个命令:

Java -jar -agentlib:jdwp=transport=dt_socket, server=y,suspend=n,address=*: app.jar

注意命令中指定的几个配置:

  • transport=dt_socket 配置 debugger 工具与 debugger agent 通信的方式。 dt_socket 配置意味着我们使用 TCP/IP 在网络上建立通信。这始终是您在 agent 和工具之间建立通信的方式。
  • server=y 表示 agent 连接到应用程序执行后充当服务器。代理等待 debugger 工具连接到它,并通过它控制应用程序的执行。 您将使用 server=n 配置连接到 debugger 代理,而不是启动 debugger agent。
  • suspend=n 告诉应用程序无需等待 debugger 工具连接即可启动。 如果你想在连接 debugger 之前阻止应用程序启动,你需要使用 suspend=y。 在我们的例子中,我们有一个web应用程序,在调用它的某个端点时出现了问题,因此我们需要应用程序在调用该端点之前启动。 如果我们正在调查服务器启动过程中的问题,我们很可能需要使用 suspend=y 来允许应用程序仅在连接 debugger 工具后启动。
  • address=*: 告诉 agent 打开系统上的端口,debugger 工具将连接到该端口与 agent 通信。 端口值必须在系统上没有被使用,并且网络需要允许 debugger 工具和 agent 之间的通信(端口需要在网络上打开)。

图 6 展示了这个应用,启动时附加了 debugger agent。 注意,在命令执行完毕后,控制台打印的消息告诉我们代理正在监听配置的 端口。


一旦你的远程应用连接了 debugger agent,你就可以连接 debugger 来调查问题了。 请记住,我们假设网络已配置为允许两个应用程序( debugger 工具和 debugger agent)之间通信。 在我们的例子中,这两个都是在本地主机上运行的,所以对于我们的演示来说,这样的网络配置不是问题。

但在实际场景中,您应该始终确保在开始调试之前能够建立通信。 在大多数情况下,如果不允许通信,您可能需要基础设施团队中的某人帮助您打开所需的端口。 请记住,出于安全原因,端口通常默认关闭。

接下来,我们将研究如何使用 IntelliJ IDEA 社区将 debugger 附加到远程应用程序上。 在远程环境中运行应用程序的调试器步骤如下:

  1. 添加一个新的运行配置。
  2. 配置 debugger agent 的远程地址(IP地址和端口)。
  3. 开始调试应用程序。

图 7 展示了如何打开 Edit Configurations 部分,添加一个新的正在运行的配置。



图 8 展示了如何添加一个正在运行的配置。


因为我们想要连接到一个远程 debugger agent,所以需要添加一个新的远程调试配置,如图 9 所示。


配置 debugger agent 的地址,如图 所示。 在我们的例子中,我们在与 debugger 相同的系统上运行应用程序,因此我们使用 localhost。 在现实世界中,如果应用程序运行在不同的系统上,则必须使用该系统的IP地址。 我们使用端口 让 agent 监听并与 debugger 工具连接。



别忘了,我们把 debugger 工具连接到了 debugger agent,它会打开 端口(如图 所示) 。不要混淆 debugger agent 打开的端口()和我们的web应用程序端口()。



配置好后,启动 debugger(如图 所示)。debugger 将开始与附加到应用程序的 debugger agent “对话”,并允许你控制执行。



现在你可以像前面文章中学习的那样使用 debugger 了。注意代码的版本(如图 所示)。 在本地调试应用程序时,你知道IDE会编译应用程序,然后将 debugger 附加到新编译的代码上。 然而,当你连接到一个远程应用程序时,你不能再确定你的源代码是否与你附加 debugger 的远程应用程序的编译代码相对应。 如果团队开始了新的任务,你需要调查的代码可能在相同的类中被修改、添加或删除了。 使用不同的源代码版本可能会导致奇怪和令人困惑的 debugger 行为。 例如,debugger 可能会显示你正在浏览空行,甚至是方法或类之外的行。 执行 stack 跟踪也可能与预期的执行不一致。



幸运的是,今天我们使用源代码版本控制软件,如Git或SVN,因此我们总是可以确定创建我们部署的应用程序的源代码版本。 在调试之前,你需要确保你的源代码与你想要远程调查的应用程序中的源代码相同。 使用源代码版本控制工具查找源代码的确切版本。

让我们在引起关注的第一行添加一个断点: ProductService 类的第行,如图 所示。 在这里,应用程序应该从数据库中选择数据,以HTTP响应返回。 首先,我想确定是否正确地从数据库中检索数据,所以我暂停了这一行的执行,并 step over 查看结果。



添加断点后,使用 Postman(或类似的工具)发送带有意外行为的HTTP请求(如图 所示)。 Postman(可以从https://www.postman.com/downloads/下载)是一个简单的工具,你可以使用它来调用给定的端点, 最近它已经成为开发人员最喜欢的用于此目的的工具之一。 Postman 有一个友好的用户界面,但如果你更喜欢使用命令行,也可以选择其他工具,比如 cURL。 为了简单起见,我使用 Postman。



请注意,Postman 并没有立即显示HTTP响应。 相反,你看到请求仍在等待,因为 debugger 在你用断点标记的那一行暂停了应用程序,如图 所示。 现在你可以开始使用导航操作来研究这个问题。



使用 step over 操作,你会看到应用程序抛出异常,而不是从数据库返回数据(如图 所示)。现在你可以开始发现问题了:

  1. 实现此功能的开发人员使用基本类型来表示数据库中可以接受 null 值的列。 由于Java中的基本类型不是对象类型,不能保存值 null,因此应用程序会抛出异常。
  2. 开发人员使用 printStackTrace() 方法打印异常消息,这没什么用,因为您无法轻松地为各种环境配置输出。 这可能是你无法在日志中看到任何内容的原因(下一篇将进一步讨论)。
  3. 这个问题并没有发生在本地或开发环境中,因为在数据库中该字段没有 null 值。

很明显,代码需要重构,也许应该在下一次回顾会议上与团队讨论增强代码评审过程的问题。 尽管如此,你很高兴你找到了问题的原因并知道如何解决它。



在 Eclipse IDE 中创建远程配置

我使用 IntelliJ IDEA 作为本书示例的主要 IDE。但是,正如我在前面几章中提到的,本书并不是关于如何使用某个 IDE。您可以使用我们讨论的各种工具来应用我们讨论的技术。例如,您可以使用Eclipse等其他ide进行远程调试。

下图展示了如何在 Eclipse IDE 中添加新的调试配置。



在 Eclipse 中添加新的调试配置

要在 Eclipse IDE 中添加新的调试配置,请选择 Run > Debug Configurations。 您可以配置调试配置,将其附加到控制远程应用程序的调试 agent。

就像在 IntelliJ IDEA 中一样,你需要配置 debugger 工具所连接的调试 agent 的地址(IP地址和端口)。



添加一个新的远程Java应用程序调试配置,并设置调试器代理的地址。然后,您可以保存配置并使用调试功能远程连接到应用程序进行调试。

添加配置后,启动 debugger 并添加断点,以便在你想开始研究代码的地方暂停执行。

结语

有时,运行中的应用程序的特定意外行为只发生在应用程序执行的特定环境中。当这种情况发生时,调试将变得更具挑战性。

你可以为在远程环境中执行的Java应用程序使用 debugger,满足以下条件:

    • 该应用程序启动时应该附带一个 debugger agent。
    • 网络配置应该允许 debugger 工具和远程环境中附加到应用程序的 debugger agent 之间通信。

远程调试允许您通过附加到在远程环境中运行的进程来使用与本地调试相同的调试技术。

在调试在远程环境中运行的应用程序之前,请确保 debugger 使用了创建你正在研究的应用程序的相同源代码的副本。 如果您没有确切的源代码,并且在与您的调查相关的应用程序部分进行了更改,debugger 可能会表现出奇怪的行为, 您的远程调查将变得更加困难而不是有用。

原文链接:,转发请注明来源!