系统上运行的进程通常是一个黑匣子。您对其中发生的事情没有太多了解。哦,有各种各样的工具可以用来推断事情(查看系统调用、内存消耗、网络连接等),但默认情况下……这是一个谜。
RavenDB 是一个数据库。它可以在无人值守的情况下长时间运行,并且主要是自行运行。这意味着当您查看它时,您希望能够尽快弄清楚系统到底发生了什么。为此,我们在 RavenDB 中包含了许多公开系统内部状态的功能。从跟踪每个 I/O 及其持续时间,到提供有关各种任务的成本和投入量的详细统计数据。
这些功能对于准确了解 RavenDB 在特定时间点发生的情况非常宝贵。当然,没有什么比打开调试器和检查系统状态的能力更好的了。但这是只有在开发中才能真正做到的事情。显然,这不是可以在生产中完成的事情。或者可以吗?
从 RavenDB 3.0 开始,我们实际上只有这个功能,能够要求 RavenDB 以开发人员应该非常熟悉的格式捕获并显示自己的状态。当我们创建 RavenDB 4.0 时,我们能够在 Windows 上实现此功能(需要付出一些代价),但在 Linux 上完全无法实现。
在 Windows 上,如果一个进程属于同一用户,则一个进程可以调试另一个进程(有点过于简单化,但已经足够好了)。在 Linux 上,情况要复杂得多。如果调试器以 root 身份运行或者是被调试进程的父进程,则进程通常只能调试另一个进程。
另一个复杂之处是我们正在使用,这是一个很棒的库,它允许我们内省实时进程(以及许多其他事情)。它才支持 Linux ……当我们获得最基本的支持后,我们就立即采取行动,看看如何将该功能也引入 Linux。我们的许多用户都在 Linux 上运行生产系统,并且能够查看系统并说:“嗯。我想知道它在做什么”,然后我们认为能够分辨出来是对 RavenDB 的重大推动。
我花了很多时间去奋斗,并且学习了比我想知道的更多关于 Linux 上调试权限如何工作的知识。但我们成功了(详情如下)。您可以在实时 Linux 服务器上看到它的样子:
您所看到这里有一个索引线程对空间数据进行一些工作。我们将通过查看 CPU 时间和作业名称的能力来进一步增强此视图。我们的想法是,您将查看并获得足够的洞察力,而无需检查日志或尝试推断正在发生的事情。你可以直接说出来。
现在,我们来了解其工作原理的具体细节。我们更改了 Windows 和 Linux 上的实现,以使用被动附加到进程,这要快得多。一旦我们转向被动连接,我们尝试的第一件事就是自我调试。
这是一个足够好的功能,而且非常优雅。我们自己调试,提取堆栈跟踪并显示数据。不幸的是,这在 Linux 上不起作用。进程无法自行调试。Linux 中的所有调试都基于ptrace()系统调用,并且其权限是指定的。我无法想象让进程自行调试的安全隐患。毕竟,它已经可以做流程能做的任何事情,因为它就是流程。但我想这是一个深奥的场景,没有人注意到,然后的反应是,使用解决方法。
通常的解决方法是有一个进程可以生成 RavenDB,然后就可以对其进行调试。这是……有可能的,但这将是我们部署方式的重大转变,而不是我想做的事情。还有 ptrace_scope 标志,它应该控制这种行为。至少在我的测试中,通过此标志禁用安全检查绝对没有任何作用。
当然,以 root 身份运行是可行的。然后进程崩溃了。在 Linux 上,当尝试调试自己的进程时,如果抛出异常,调试器和被调试者之间似乎会发生有趣的交互。以至于它会破坏 CoreCLR 状态并终止进程。某种程度上,这是一个有趣的错误追踪。Linux 有一个可以使用的 PR_SET_PTRACER 选项形式的逃生口。但是,不幸的是,您无法指定自己的流程。再加上严重的崩溃,使得自调试成为不可能的事情。
但我仍然想要这个功能,并且不改变我们做事的方式太多。
这就是我们最终所做的。我们有一个单独的进程来捕获堆栈跟踪。当您要求 RavenDB 获取其堆栈跟踪时,它将生成此进程,但要求其等待。然后,它将授予新进程调试 RavenDB 所需的权限并指示其继续。此时,调试器子进程将捕获堆栈跟踪并将其发送回 RavenDB。RavenDB 将重置权限,使用我们可以从进程内部提供的附加信息增强堆栈跟踪并将其显示给用户。
实际的调试器进程也用setcap进行标记,以为其提供所需的附加权限。这种分离意味着我们将这些权限隔离到可以调用和关闭的单一用途工具,而不会增加 RavenDB 的攻击面。