将-Linux-迁移到微软-Azure-全-
将 Linux 迁移到微软 Azure(全)
原文:
zh.annas-archive.org/md5/DFC4E6F489A560394D390945DB597424
译者:飞龙
前言
Red Hat Enterprise Linux 是一种广泛流行的 Linux 发行版,用于从云到企业大型计算机的各种场景。如果包括 CentOS 等下游发行版,那么 Red Hat Enterprise Linux 发行版的采用率甚至更大。
与大多数事物一样,总会有人负责解决运行 Red Hat Enterprise Linux 的各种系统的问题。《Red Hat Enterprise Linux 故障排除指南》旨在为运行 Red Hat Enterprise Linux 系统的基本到高级的故障排除实践和命令提供指导。
本书旨在为您提供解决各种情景所需的步骤和知识。本书中的示例使用真实世界的问题和真实世界的解决方案。
虽然本书中的示例是情境性的,但本书也可以作为与 Linux 相关主题和命令的参考。它为读者提供了参考故障排除步骤和特定命令来解决复杂问题的能力。
本书涵盖的内容
第一章,“故障排除最佳实践”,涵盖了高层次的故障排除过程。通过将故障排除过程与科学方法相提并论,本书将解释如何分解问题以确定根本原因,无论问题有多复杂。
第二章,“故障排除命令和有用信息的来源”,为读者提供了有关有用信息的常见位置的简单介绍。它还将提供一个基本的 Linux 命令参考,可用于排除许多类型的问题。
第三章,“故障排除 Web 应用程序”,利用第一章学到的过程和第二章学到的命令来解决一个复杂的问题。本章中概述的问题是“通过示例”意味着本章的流程旨在引导您完成整个故障排除过程,从头到尾。
第四章,“故障排除性能问题”,涉及性能问题和一些最复杂的故障排除问题。通常,用户的感知与预期的性能水平相比会使问题变得更加复杂。在本章中,将再次使用第二章讨论的工具和信息来解决真实世界的性能问题。
第五章,“网络故障排除”,讨论了网络是任何现代系统的关键组成部分。本章将涵盖配置和诊断 Linux 网络所需的核心命令。
第六章,“诊断和纠正防火墙问题”,涵盖了 Linux 防火墙的复杂性,是第五章的延续。本章将介绍和强调诊断 Linux 软件防火墙所需的命令和技术。
第七章,“文件系统错误和恢复”,教会您恢复文件系统可能意味着丢失和保留数据之间的区别。本章将介绍一些核心的 Linux 文件系统概念,并演示如何恢复只读文件系统。
第八章,“硬件故障排除”,开始涉及故障排除硬件问题的过程。本章将指导您恢复失败的硬盘驱动器。
第九章,“使用系统工具排除应用程序问题”,探讨了系统管理员的角色不仅是排除操作系统问题,还包括应用程序问题。本章将向您展示如何利用常见系统工具来识别应用程序问题的根本原因。
第十章,“理解 Linux 用户和内核限制”,演示了 Red Hat Enterprise Linux 有许多组件可以防止用户过载系统。本章将探讨这些组件,并解释如何修改它们以允许合法的资源利用。
第十一章,“从常见故障中恢复”,将指导您排除内存不足的情况。这种情况在使用频繁的环境中非常常见,且很难排除故障。本章将涵盖不仅如何排除此问题,还将解释为什么会发生此问题。
第十二章,“意外重启的根本原因分析”,将对前几章学到的故障排除过程和命令进行测试。本章将指导您对意外重启的服务器进行根本原因分析。
本书所需内容
尽管本书可以独立存在,但读者将受益于拥有 Red Hat Enterprise Linux 7 系统,并且可以使用该操作系统。当您能够在测试系统上执行这些命令和资源时,您将更有效地学习本书中讨论的命令和资源。
虽然本书中涵盖的许多命令、过程和资源可以在其他 Linux 发行版中使用,但强烈建议读者使用 Red Hat 的下游发行版,如 CentOS 7,如果读者无法获得 Red Hat Enterprise Linux 7。
本书的受众
如果您是一名熟练的 RHEL 管理员或顾问,并且希望提高故障排除技能和对 Red Hat Enterprise Linux 的了解,那么本书非常适合您。我们期望您具有良好的知识水平和对基本 Linux 命令的理解。
约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。
文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 用户名显示如下:“在合理范围内,不需要包括执行的每个cd
或ls
命令。”
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68
任何命令行输入或输出都以以下形式书写:
# yum install man-pages
新术语和重要单词以粗体显示。屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"我们将在屏幕上看到一条消息,上面写着还在这里?"。
注意
警告或重要说明会出现在这样的框中。
提示
提示和技巧会以这种方式出现。
读者反馈
我们始终欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发你真正能充分利用的标题。
要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>
,并在主题中提及书名。
如果你在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请参阅我们的作者指南 www.packtpub.com/authors。
客户支持
既然你已经是 Packt 图书的自豪所有者,我们有一些事情可以帮助你充分利用你的购买。
下载示例代码
你可以从 www.packtpub.com
的账户中下载你购买的所有 Packt Publishing 图书的示例代码文件。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support
并注册,将文件直接发送到你的邮箱。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误是难免的。如果你在我们的书中发现错误——也许是文本或代码中的错误——我们将不胜感激,如果你能向我们报告。通过这样做,你可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果你发现任何勘误,请访问 www.packtpub.com/submit-errata
报告,选择你的书,点击勘误提交表链接,并输入你的勘误详情。一旦你的勘误被验证,你的提交将被接受,并勘误将被上传到我们的网站或添加到该书的勘误列表下的勘误部分。
要查看先前提交的勘误,请访问 www.packtpub.com/books/content/support
并在搜索框中输入书名。所需信息将出现在勘误部分下。
盗版
互联网上的盗版行为是跨所有媒体持续存在的问题。在 Packt,我们非常重视对我们版权和许可的保护。如果你在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。
请通过 <copyright@packtpub.com>
联系我们,并附上涉嫌盗版材料的链接。
我们感谢你帮助保护我们的作者和我们提供有价值内容的能力。
问题
如果你对本书的任何方面有问题,可以通过 <questions@packtpub.com>
联系我们,我们将尽力解决问题。
第一章:故障排除最佳实践
这一章,也就是第一章,可能是最重要但技术含量最低的。本书中的大多数章节涵盖了特定问题和解决这些问题所需的命令。然而,这一章将涵盖一些可以应用于任何问题的故障排除最佳实践。
你可以把这一章看作是应用实践背后的原则。
故障排除风格
在介绍故障排除的最佳实践之前,了解不同的故障排除风格是很重要的。根据我的经验,我发现人们倾向于使用以下三种故障排除风格:
-
数据收集者
-
受过教育的猜测者
-
适应者
每种风格都有其优点和缺点。让我们来看看这些风格的特点。
数据收集者
我喜欢称第一种故障排除风格为数据收集者。数据收集者通常是利用系统化方法解决问题的人。系统化的故障排除方法通常具有以下特点:
-
向报告问题的相关方提出具体问题,期望得到详细答案
-
运行命令以识别大多数问题的系统性能
-
在采取行动之前运行预定义的一系列故障排除步骤
这种风格的优势在于,无论是什么级别的工程师或管理员使用它都是有效的。通过系统地处理问题,收集每个数据点,并在执行任何解决方案之前了解结果,数据收集者能够解决他们可能不熟悉的问题。
这种风格的弱点在于数据收集通常不是解决问题的最快方法。根据问题的不同,收集数据可能需要很长时间,而且其中一些数据可能并不是找到解决方案所必需的。
受过教育的猜测者
我喜欢称第二种故障排除风格为受过教育的猜测者。受过教育的猜测者通常是利用直觉方法解决问题的人。直觉方法通常具有以下特点:
-
用最少的信息确定问题的原因
-
在解决问题之前运行几个命令
-
利用以前的经验来确定根本原因
这种故障排除风格的优势在于它能让你更快地找到解决方案。面对问题时,这种类型的故障排除者倾向于从经验中汲取,并且需要最少的信息来找到解决方案。
这种风格的弱点在于它严重依赖经验,因此在有效之前需要时间。在专注于解决问题时,这种故障排除者可能还会尝试多种行动来解决问题,这可能会使人觉得受过教育的猜测者并没有完全理解手头的问题。
适应者
还有一种第三种经常被忽视的故障排除风格;这种风格同时利用了系统化和直觉化的风格。我喜欢称这种风格为适应者。适应者有一种个性,使其能够在系统化和直觉化的故障排除风格之间切换。这种结合的风格通常比数据收集者风格更快,比受过教育的猜测者风格更注重细节。这是因为他们能够应用适合手头任务的故障排除风格。
选择适当的风格
虽然很容易说一个方法比另一个更好,但事实是选择适当的故障排除风格在很大程度上取决于个人。了解哪种故障排除风格最适合你自己的个性是很重要的。通过了解哪种风格更适合你,你可以学习和使用适合该风格的技术。你也可以学习和采用其他风格的技术,以应用你通常会忽略的故障排除步骤。
本书将展示数据收集者和有经验的猜测者两种故障排除风格,并定期强调哪种个性风格最适合这些步骤。
故障排除步骤
故障排除是一个既严格又灵活的过程。故障排除过程的严格性基于需要遵循基本步骤的事实。在这方面,我喜欢将故障排除过程等同于科学方法,科学方法有一系列必须遵循的特定步骤。
故障排除过程的灵活性在于可以按任何有意义的顺序遵循这些步骤。与科学方法不同,故障排除过程通常旨在快速解决问题。有时,为了快速解决问题,您可能需要跳过一步或按顺序执行它们。例如,在故障排除过程中,您可能需要解决即时问题,然后确定该问题的根本原因。
以下列表包括构成故障排除过程的五个步骤。每个步骤可能还包括几个子任务,这些子任务可能与问题相关也可能不相关。重要的是要以一颗谨慎的心态遵循这些步骤,因为并非每个问题都可以归入同一类别。以下步骤旨在作为最佳实践使用,但与所有事物一样,应根据手头的问题进行调整:
-
理解问题陈述。
-
建立假设。
-
试错。
-
寻求帮助。
-
文档。
理解问题陈述
使用科学方法,第一步是建立问题陈述,这另一种说法是:确定并理解实验的目标。使用故障排除过程,第一步是理解报告的问题。我们越了解问题,解决问题就越容易。
我们可以执行一些任务来更好地理解问题。这第一步是数据收集者个性的显著特点。数据收集者天生倾向于在进入下一步之前收集尽可能多的数据,而有经验的猜测者通常倾向于迅速完成这一步,然后转移到下一步,这有时可能导致错过关键信息。
适应者倾向于了解哪些数据收集步骤是必要的,哪些是不必要的。这使他们能够像数据收集者一样收集数据,但不会花费时间收集对问题没有价值的数据。
这个故障排除步骤中的子任务是提出正确的问题。
提出问题
无论是通过人工还是自动化流程(如工单系统),报告问题的人通常是信息的重要来源。
工单
当他们收到一个工单时,有经验的猜测者个性通常会阅读工单的标题,假设问题并转移到理解问题的下一个阶段。数据收集者个性通常会打开工单并阅读工单的所有细节。
虽然这取决于工单和监控系统,但通常工单中可能包含有用的信息。除非问题是常见问题,并且您能够从标题中理解所有信息,通常最好阅读工单描述。即使是少量信息也可能有助于解决特别棘手的问题。
人类
然而,从人类那里收集额外信息可能是不一致的。这在很大程度上取决于所支持的环境。在某些环境中,报告问题的人可以提供解决问题所需的所有细节。在其他环境中,他们可能不理解问题,只是解释症状。
无论哪种排除故障风格最适合你的个性,能够从报告问题的人那里获得重要信息是一项重要的技能。直觉性问题解决者,如受过教育的猜测者或适应者,往往比数据收集者个性更容易找到这个过程,不是因为这些个性一定更擅长从人们那里获取细节,而是因为他们能够在较少的信息中识别模式。然而,数据收集者可以通过准备好提出故障排除问题来从报告问题的人那里获得他们需要的信息。
注意
不要害怕问显而易见的问题
我的第一份技术工作是在一个网络托管技术支持呼叫中心。在那里,我经常接到用户的电话,他们不想执行基本的故障排除步骤,只是希望问题升级。这些用户只是觉得他们已经自己执行了所有的故障排除步骤,并且已经发现了超出一级支持范围的问题。
虽然有时这是真的,但更多的时候,问题是一些他们忽视的基本问题。在那个角色中,我很快就学会了,即使用户不愿意回答基本或显而易见的问题,但在一天结束时,他们只是希望他们的问题得到解决。如果这意味着经历重复的步骤,那也没关系,只要问题得到解决。
即使在今天,作为高级工程师的升级点,我发现很多时候工程师(即使在他们的故障排除经验丰富的情况下)也会忽视简单的基本步骤。
有时问一些看似基本的简单问题是一个很好的时间节省者;所以不要害怕问。
尝试复制问题
收集信息和理解问题的最佳方法之一是亲身体验。当问题被报告时,最好是复制问题。
虽然用户可能是很多信息的来源,但他们并不总是最可靠的;用户经常会遇到错误并忽视它,或者在报告问题时简单地忘记传达错误。
通常,我会问用户如何重新创建问题。如果用户能够提供这些信息,我就能看到任何错误,并经常更快地确定问题的解决方案。
注意
有时无法复制问题
尽管复制问题通常是最好的,但并非总是可能的。每天,我与许多团队合作;有时,这些团队在公司内部,但很多时候它们是外部供应商。在关键问题时,我偶尔会看到有人发表类似于“如果我们无法复制它,我们就无法排除故障”的笼统声明。
尽管复制问题有时是找到根本原因的唯一方法,但我经常听到这种说法被滥用。复制问题应该被视为一种工具;它只是你排除故障工具箱中的众多工具之一。如果它不可用,那么你只能用其他工具。
无法找到解决方案和由于无法复制问题而不尝试找到解决方案之间存在显著的区别。后者不仅没有帮助,而且也不专业。
运行调查命令
很可能,你正在阅读这本书是为了学习排除故障红帽企业 Linux 系统的技术和命令。理解问题陈述的第三个子任务就是这样——运行调查命令以确定问题的原因。然而,在执行调查命令之前,重要的是要知道之前的步骤是有逻辑顺序的。
首先询问报告问题的用户一些基本问题的细节是最佳实践,然后在获得足够的信息后,复制问题。一旦问题被复制,下一个逻辑步骤就是运行必要的命令来排除故障和调查问题的原因。
在故障排除过程中,经常会发现自己需要返回到之前的步骤。在你确定了一些关键错误之后,你可能会发现自己必须向最初的报告者询问额外的信息。在故障排除过程中,不要害怕向后退几步,以便更清楚地了解手头的问题。
建立假设
使用科学方法,一旦问题陈述被制定出来,就是建立假设的时候了。在故障排除过程中,当你确定了问题,收集了关于问题的信息,比如错误、系统当前状态等,也是建立你认为引起或正在引起问题的原因的时候。
然而,有些问题可能不需要太多的假设。通常日志文件中的错误或系统当前状态可能会解释问题发生的原因。在这种情况下,你可以简单地解决问题,然后继续进行文档步骤。
对于那些不是非常明显的问题,你需要提出一个根本原因的假设。这是必要的,因为形成假设之后的下一步是尝试解决问题。如果你至少没有根本原因的理论,那么解决问题就会很困难。
以下是一些可以用来帮助形成假设的技术。
整理模式
在进行前面步骤的数据收集时,你可能会开始看到一些模式。模式可以是一些简单的东西,比如多个服务中相似的日志条目,发生的故障类型(比如,多个服务下线),甚至是系统资源利用率的反复波动。
这些模式可以用来制定问题的理论。为了强调这一点,让我们通过一个真实的情景来看一下。
你正在管理一个既运行 Web 应用程序又接收电子邮件的服务器。你有一个监控系统检测到 Web 服务出现错误并创建了一个工单。在调查工单时,你还接到一个电子邮件用户的电话,称他们收到了电子邮件反弹回来的消息。
当你要求用户向你读出错误时,他们提到“设备上没有剩余空间”。
让我们来分析一下这个情景:
-
我们的监控解决方案的工单告诉我们 Apache 已经停止了
-
我们还收到了来自电子邮件用户的报告,其中的错误表明文件系统已满
这一切可能意味着 Apache 已经停止,因为文件系统已满吗?可能。我们应该调查一下吗?当然!
这是我以前遇到过的事情吗?
上面的分析引出了形成假设的下一个技术。这可能听起来很简单,但经常被忘记。“我以前见过类似的情况吗?”
在先前的情景中,电子邮件反弹回来的错误报告通常表明文件系统已满。我们怎么知道的?很简单,我们以前见过。也许我们以前在电子邮件反弹回来时见过同样的错误,或者我们在其他服务中见过这个错误。关键是,这个错误是熟悉的,而且这个错误通常意味着一件事。
记住常见错误对于直觉类型的人(如有经验的猜测者和适应者)来说可能非常有用;这是他们自然而然会做的事情。对于数据收集者来说,一个方便的技巧是保留一张常见错误的参考表。
提示
根据我的经验,大多数数据收集者倾向于保留一组包含常见命令或程序步骤的笔记。添加常见错误和这些错误背后的含义是系统思维者(如数据收集者)更快地建立假设的好方法。
总的来说,建立假设对所有类型的故障排除者都很重要。这是直觉思维者(如有经验的猜测者和适应者)擅长的领域。通常情况下,这些类型的故障排除者会更早地提出假设,即使有时这些假设并不总是正确的。
试错法
在科学方法中,一旦形成假设,下一个阶段就是实验。在故障排除中,这相当于尝试解决问题。
有些问题很简单,可以使用标准程序或经验步骤解决。然而,其他问题并不那么简单。有时,假设结果是错误的,或者问题最终比最初想象的更复杂。
在这种情况下,可能需要多次尝试解决问题。我个人喜欢把这看作是类似于试错。一般来说,你可能对问题出了什么问题(假设)有一个想法,以及如何解决它的想法。你试图解决它(试验),如果不起作用(错误),你就会转向下一个可能的解决方案。
首先创建一个备份
对于那些担任新角色的 Linux 系统管理员,如果我只能给出一个建议,那就是大多数人都是通过艰难的方式学到的:在进行更改之前备份所有内容。
作为系统管理员,我们经常发现自己需要更改配置文件或删除一些不需要的文件以解决问题。不幸的是,我们可能认为自己知道需要删除或更改的内容,但并不总是正确。
如果已经进行了备份,那么更改可以简单地恢复到先前的状态,但是没有备份。因此,撤销更改并不那么容易。
备份可以包括许多内容,可以是使用诸如rdiff-backup
之类的完整系统备份,VM 快照,或者简单地创建文件副本。
提示
对于那些有兴趣看到这个提示在实践中的程度的人,只需在任何有四名以上系统管理员并且已经存在数年的服务器上运行以下命令:
$ find /etc –name "*.bak"
寻求帮助
在许多情况下,问题在这一点上已经解决了,但就像故障排除过程中的每一步一样,这取决于手头的问题。虽然寻求帮助并不完全是故障排除的步骤,但如果你无法自己解决问题,这往往是下一个逻辑步骤。
在寻求帮助时,通常有六种资源可用:
-
书籍
-
团队维基或运行手册
-
谷歌
-
手册
-
红帽内核文档
-
人们
书籍
书籍(比如这本书)对于参考特定类型问题的命令或故障排除步骤是很好的。其他专门针对特定技术的书籍对于参考该技术的工作原理也是很好的。在以前的几年里,看到一位高级管理员手边放着一整排技术书籍是很常见的。
在今天的世界中,由于书籍更频繁地以数字格式出现,它们甚至更容易用作参考。数字格式使它们可以被搜索,并允许读者比传统的印刷版本更快地找到特定部分。
团队维基或运行手册
在团队维基变得普遍之前,许多运营团队都有名为运行手册的实体书籍。这些书是运营团队每天使用的流程和程序列表,用于保持生产环境正常运行。有时,这些运行手册会包含有关配置新服务器的信息,有时它们会专门用于故障排除。
在今天的世界中,这些运行手册大多被团队维基所取代,这些维基通常具有相同的内容,但是在线的。它们也往往是可搜索的,更容易保持最新,这意味着它们通常比传统的印刷运行手册更相关。
团队维基和运行手册的好处在于它们不仅可以解决特定于您环境的问题,而且还可以解决这些问题。有许多配置服务(如 Apache)的方法,外部系统对这些服务创建依赖的方式更多。
在某些环境中,当出现问题时,您可能只需简单地重新启动 Apache,但在其他情况下,您可能实际上需要经历几个先决步骤。如果在重新启动服务之前需要遵循特定的流程,最佳做法是将流程记录在团队 Wiki 或 Runbook 中。
谷歌
谷歌是系统管理员如此常用的工具,以至于曾经有特定的搜索门户网站可用,如google.com/linux
,google.com/microsoft
,google.com/mac
和google.com/bsd
。
谷歌已经停用了这些搜索门户,但这并不意味着系统管理员使用谷歌或任何其他搜索引擎进行故障排除的次数减少了。
事实上,在今天的世界中,在技术面试中听到“我会谷歌一下”这样的话并不罕见。
对于那些刚开始使用谷歌进行系统管理任务的人,一些建议是:
- 如果您复制并粘贴完整的错误消息(删除服务器特定的文本),您可能会找到更相关的结果:
例如,搜索kdumpctl: No memory reserved for crash kernel返回 600 个结果,而搜索memory reserved for crash kernel返回 449,000 个结果。
-
您可以通过搜索
man
然后是一个命令,比如man netstat
,找到任何 man 页面的在线版本。 -
您可以用双引号包裹错误以细化搜索结果,使其包含相同的错误。
-
通常以问题的形式询问您要找的内容通常会得到教程。例如,如何在 RHEL 7 上重新启动 Apache?
虽然谷歌可能是一个很好的资源,但结果应该始终持保留态度。在谷歌上搜索错误时,您可能会找到一个建议的命令,它提供了很少的解释,只是简单地说“运行这个命令就会修复它”。在运行这些命令时要非常谨慎,重要的是您在执行系统上的任何命令之前都应该熟悉该命令。您应该在执行之前了解命令的作用。
Man 页面
当谷歌不可用,甚至有时候它可用时,关于命令或 Linux 的最佳信息来源通常是man 页面。man 页面是核心 Linux 手册文档,可以通过man
命令访问。
例如,要查找netstat
命令的文档,只需运行以下命令:
$ man netstat
NETSTAT(8)
Linux System Administrator's Manual
NETSTAT(8)
NAME
netstat - Print network connections, routing tables, interface statistics, masquerade connections, and multicast memberships
正如您所看到的,这个命令不仅输出了netstat
命令的信息,还包含了使用信息的快速概要,如下所示:
SYNOPSIS
netstat [address_family_options] [--tcp|-t] [--udp|-u] [--udplite|-U] [--raw|-w] [--listening|-l] [--all|-a] [--numeric|-n] [--numeric-hosts]
[--numeric-ports] [--numeric-users] [--symbolic|-N] [--extend|-e[--extend|-e]] [--timers|-o] [--program|-p] [--verbose|-v] [--continuous|-c]
[--wide|-W] [delay]
此外,它提供了每个标志的详细描述及其作用:
--route , -r
Display the kernel routing tables. See the description in route(8) for details. netstat -r and route -e produce the same output.
--groups , -g
Display multicast group membership information for IPv4 and IPv6.
--interfaces=iface , -I=iface , -i
Display a table of all network interfaces, or the specified iface.
一般来说,核心系统和库的基本手册页面是通过man-pages
软件包分发的。特定命令的 man 页面,如top
,netstat
或ps
,是作为该命令的安装软件包的一部分分发的。这是因为个别命令和组件的文档留给了软件包维护者。
这意味着有些命令的文档水平可能不如其他命令。然而,总的来说,man 页面是非常有用的信息来源,可以回答大多数日常问题。
阅读 man 页面
在前面的例子中,我们可以看到netstat
的 man 页面包含了一些信息部分。一般来说,man 页面有一个一致的布局,其中包含一些常见的部分,这些部分可以在大多数 man 页面中找到。以下是一些常见部分的简单列表:
-
名称
-
概要
-
描述
-
示例
名称
名称部分通常包含命令的名称和对命令的非常简要的描述。以下是ps
命令的 man 页面中的名称部分:
NAME
ps - report a snapshot of the current processes.
概要
命令的 man 页面的概要部分通常会列出命令,后面是可能的命令标志或选项。这一部分的一个很好的例子可以在netstat
命令的概要中看到:
SYNOPSIS
netstat [address_family_options] [--tcp|-t] [--udp|-u] [--raw|-w] [--listening|-l] [--all|-a] [--numeric|-n] [--numeric-hosts] [--numeric-ports]
[--numeric-users] [--symbolic|-N] [--extend|-e[--extend|- e]] [--timers|-o] [--program|-p] [--verbose|-v] [--continuous|-c]
cat command's man page:
DESCRIPTION
Concatenate FILE(s), or standard input, to standard output.
-A, --show-all
equivalent to -vET
-b, --number-nonblank
number nonempty output lines, overrides -n
描述部分非常有用,因为它不仅仅是查找选项。这部分通常是你会找到关于命令细微差别的文档的地方。
示例
通常 man 页面还会包括使用命令的示例:
EXAMPLES
cat f - g
Output f's contents, then standard input, then g's infocontents.
cat command's man page. We can see, in this example, how to use cat to read from files and standard input in one command.
这部分通常是我发现如何使用我以前多次使用的命令的新方法的地方。
其他部分
除了前面的部分,你可能还会看到诸如另请参阅、文件、作者和历史等部分。这些部分也可能包含有用的信息;但并非每个 man 页面都会有这些部分。
Info 文档
除了 man 页面,Linux 系统通常还包含info 文档,这些文档旨在包含超出 man 页面范围的额外文档。与 man 页面类似,info 文档包含在一个命令包中,文档的质量/数量可以因包而异。
调用 info 文档的方法类似于 man 页面,只需执行 info
命令,然后跟上你想查看的主题:
$ info gzip
GNU Gzip: General file (de)compression
**************************************
This manual is for GNU Gzip (version 1.5, 10 June 2014), and documents commands for compressing and decompressing data.
Copyright (C) 1998-1999, 2001-2002, 2006-2007, 2009-2012 Free
Software Foundation, Inc.
引用更多命令
除了使用 man 页面和 info 文档查找命令之外;这些工具也可以用来查看系统调用或配置文件等其他项目的文档。
举例来说,如果你使用 man
来搜索术语 signal
,你会看到以下内容:
$ man signal
SIGNAL(2)
Linux Programmer's Manual
SIGNAL(2)
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
DESCRIPTION
The behavior of signal() varies across UNIX versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead. See Portability below.
signal() sets the disposition of the signal signum to handler, which is either SIG_IGN, SIG_DFL, or the address of a programmer-defined function (a "signal handler").
Signal
是一个非常重要的系统调用和 Linux 的核心概念。知道可以使用 man
和 info
命令来查找核心 Linux 概念和行为在故障排除过程中非常有用。
安装 man 页面
基于 Red Hat Enterprise Linux 的发行版通常包括 man-pages
包;如果你的系统没有安装 man-pages
包,你可以使用 yum
命令安装它:
# yum install man-pages
Red Hat 内核文档
除了 man 页面,Red Hat 发行版还有一个叫做kernel-doc的包。这个包包含了关于系统内部工作原理的大量信息。
内核文档是一组文本文件,放置在 /usr/share/doc/kernel-doc-<kernel-version>/
中,并按其涵盖的主题进行分类。这个资源对于更深入的故障排除非常有用,比如调整内核可调整参数或理解 ext4
文件系统如何利用日志。
默认情况下,kernel-doc
包未安装,但可以使用 yum
命令轻松安装:
# yum install kernel-doc
人员
无论是朋友还是团队领导,在向他人寻求帮助时都有一定的礼仪。以下是人们在被要求解决问题时通常期望的事情列表。当我被要求帮助时,我希望你能:
-
尝试自己解决问题:在升级问题时,最好至少尝试遵循故障排除过程中的理解问题和形成假设步骤。
-
记录你尝试过的内容:文档是升级问题或寻求帮助的关键。你记录的步骤和发现的错误越详细,其他人就越快地能够识别和解决问题。
-
解释你认为问题是什么以及报告了什么:当你升级问题时,首先要指出的是你的假设。通常这可以帮助下一个人迅速找到可能的解决方案,而不必进行数据收集活动。
-
提及最近这个系统是否发生了其他事情:问题通常是成对出现的,突出系统或受影响系统上发生的所有因素是很重要的。
上述列表虽然不全面,但每个关键信息都可以帮助下一个人有效地解决问题。
跟进
在升级问题时,最好跟进其他人,了解他们做了什么以及如何做的。这很重要,因为它会向你询问的人表明你愿意学习更多,这往往会导致他们花时间解释他们是如何解决和识别问题的。
这样的互动将使你获得更多知识,并帮助建立你的系统管理技能和经验。
文档
文档编制是故障排除过程中的关键步骤。在整个过程中,关键是要注意并记录正在执行的操作。为什么记录很重要?主要有三个原因:
-
在升级问题时,你记录的信息越多,就能传递更多信息给其他人
-
如果问题是一个再次发生的问题,文档可以用于更新团队 Wiki 或运行手册
-
如果在你的环境中进行根本原因分析(RCA),所有这些信息都将需要进行 RCA
根据环境的不同,文档可以是从保存在本地系统文本文件中的简单笔记到票务系统所需的笔记。每个工作环境都不同,但一个普遍规则是没有太多的文档。
对于数据收集者来说,这一步相当自然。因为大多数数据收集者的个性通常会为自己保留相当多的笔记。对于受过教育的猜测者来说,这一步可能看起来是不必要的。然而,对于任何再次发生或需要升级的问题,文档都是至关重要的。
应该记录哪些信息?以下列表是一个很好的起点,但与大多数故障排除中的事情一样,它取决于环境和问题:
-
问题陈述,你所理解的
-
导致问题的假设。
-
信息收集步骤中收集的数据:
-
找到的具体错误
-
相关系统指标(例如,CPU、内存和磁盘利用率)
-
信息收集步骤中执行的命令(在合理范围内,不需要包括每个执行的
cd
或ls
命令) -
尝试解决问题时采取的步骤,包括执行的具体命令
如果前面的项目有很好的记录,如果问题再次发生,将文档移至团队 Wiki 相对简单。这样做的好处是,其他需要在问题再次发生时解决相同问题的团队成员可以使用 Wiki 文章。
文档编制的三个原因之一是在根本原因分析期间使用文档,这将引出我们下一个话题——建立根本原因分析。
根本原因分析
根本原因分析是在事件发生后进行的过程。RCA 过程的目标是确定事件的根本原因,并确定可能的纠正措施,以防止同样的事件再次发生。这些纠正措施可能是简单的,比如建立用户培训,重新配置所有 Web 服务器上的 Apache。
RCA 过程并不局限于技术领域,在航空和职业安全等领域也是一种广泛实践的过程。在这些领域,事件往往不仅仅是几台计算机离线。这些事件可能会危及人的生命。
一个良好 RCA 的解剖结构
不同的工作环境可能以不同的方式实施 RCA 过程,但归根结底,每个良好的 RCA 都有一些关键要素:
-
问题的报告情况
-
问题的实际根本原因
-
事件和采取的行动的时间表
-
任何关键数据点
-
防止事件再次发生的行动计划
问题的报告情况
故障排除过程中的第一步之一是确定问题;这些信息对 RCA 非常重要。根据问题的原因,其重要性可能有所不同。有时,这些信息将显示问题是否被正确识别。大多数时候,它可以作为问题影响的估计。
了解问题的影响可能非常重要,对于一些公司和问题,这可能意味着收入损失;对于其他公司,这可能意味着损害其品牌,或者根据问题的严重程度,可能什么都不意味着。
问题的实际根本原因
根本原因分析的这一要素在其重要性上相当不言而喻。然而,有时可能无法确定根本原因。在本章和第十二章中,我将讨论如何处理无法获得完整根本原因的问题。
事件和采取的行动的时间表
如果我们以航空事件为例,很容易看出事件时间表的重要性,比如飞机何时起飞,何时乘客登机,以及维护人员何时完成评估,这些都可能很有用。技术事件的时间表也可能非常有用,因为它可以用来确定影响的持续时间以及采取关键行动的时间。
一个良好的时间表应该包括事件的时间和主要事件。以下是技术事件的时间表示例:
-
08:00,Joe B.打电话给 NOC 热线,报告 Tempe 的电子邮件服务器中断
-
08:15,John C.登录到 Tempe 的电子邮件服务器,注意到它们的可用内存不足。
-
08:17,根据 Runbook,John C.开始逐个重新启动 Tempe 的电子邮件服务器
验证根本原因的任何关键数据点
除了事件时间表之外,RCA 还应包括关键数据点。再次以航空事故为例,关键数据点可能是事故期间的天气条件,参与人员的工作时间,或飞机的状况。
我们的时间表示例包括一些关键数据点,其中包括:
-
事故时间:08:00
-
电子邮件服务器的状况:可用内存不足
-
受影响的服务:电子邮件
无论数据点是独立的还是在时间表内,都很重要确保这些数据点在 RCA 中得到充分记录。
防止事故再次发生的行动计划
执行根本原因分析的整个目的是确定为什么发生了事故以及防止再次发生的行动计划。
不幸的是,我看到许多 RCA 忽视了这一点。RCA 流程在实施良好时可能很有用;然而,当实施不当时,它们可能会变成浪费时间和资源。
通常,对于实施不良的情况,您会发现需要对每个大或小的事件进行 RCA。这样做的问题在于,它会导致 RCA 的质量降低。只有在事件造成重大影响时才应执行 RCA。例如,硬件故障是无法预防的,您可以使用诸如smartd
之类的工具主动识别硬盘故障,但除了更换它们之外,您并不能总是防止它们发生故障。要求对每次硬件故障和更换进行 RCA 是 RCA 流程实施不当的一个例子。
当工程师需要确定像硬件故障这样常见的问题的根本原因时,他们忽视了根本原因的过程。当工程师忽视某种类型的事件的 RCA 过程时,它可能会扩散到其他类型的事件,导致 RCA 的质量下降。
根本原因分析应该只针对具有重大影响的事件。轻微事件或例行事件不应该有根本原因分析的要求;但是,它们应该被跟踪。通过跟踪已更换的硬盘数量以及这些硬盘的品牌和型号,可以识别硬件质量问题。对于重置用户密码等例行事件也是如此。通过跟踪这些类型的事件,可以识别可能的改进领域。
建立根本原因
为了更好地理解根本原因分析过程,让我们使用在生产环境中看到的一个假设问题。
注意
在写入文件时,一个 Web 应用程序崩溃了
登录系统后,你发现应用程序崩溃是因为应用程序尝试写入的文件系统已满。
注意
根本原因并不总是显而易见的原因
问题的根本原因是文件系统满了吗?不是。虽然文件系统满可能导致应用程序崩溃,但这被称为一个促成因素。促成因素,如文件系统满,可以纠正,但这不会防止问题再次发生。
在这一点上,重要的是要确定文件系统为什么会满。进一步调查后,你发现是因为一位同事禁用了一个删除旧应用程序文件的定时任务。在禁用了定时任务后,文件系统上的可用空间逐渐减少。最终,文件系统被 100%利用。
在这种情况下,问题的根本原因是禁用的定时任务。
有时你必须牺牲根本原因分析
让我们看另一个假设情况,一个问题导致了停机。由于问题造成了重大影响,它绝对需要进行根本原因分析。问题是,为了解决问题,你需要执行一项活动,这将消除进行准确根本原因分析的可能性。
这些情况有时需要判断,是选择忍受停机更长时间,还是解决停机并牺牲进行根本原因分析的机会。不幸的是,对于这些情况没有单一答案,正确答案取决于问题和受影响的环境。
提示
在处理财务系统时,我经常发现自己不得不做出这个决定。对于关键任务系统,答案几乎总是恢复服务优先于进行根本原因分析。然而,只要可能,总是首选首先捕获数据,即使这些数据不能立即审查。
了解你的环境
本章的最后一节是我能提出的最重要的最佳实践之一。最后一节涵盖了了解你的环境的重要性。
有些人认为系统管理员的工作止步于系统上安装的应用程序,系统管理员只应关注操作系统及其组件,如网络或文件系统。
我不赞同这种观点。事实上,通常情况下,系统管理员会开始比创建它的开发团队更好地了解应用程序在生产中的工作方式。
根据我的经验,为了真正支持服务器,你必须了解在该服务器内运行的服务和应用程序。例如,在许多企业环境中,系统管理员被期望处理 Web 服务器的配置和管理(例如,Apache 和 Nginx)。然而,同一系统管理员不被期望管理 Apache 后面的应用程序(例如,Java 和 C)。
Apache 与 Java 应用程序有何不同?答案实际上是没有什么不同;归根结底,它们都是在服务器上运行的应用程序。我看到许多管理员一旦问题与应用程序有关,就会简单地放手不管。然而,如果问题与 Apache 有关,他们就会迅速采取行动。
最后,如果这些管理团队与开发团队合作,问题可以更快地得到解决。管理员有责任了解并帮助解决系统上加载的任何软件的问题,无论是操作系统分发的软件还是后来由应用团队安装的软件。
总结
在本章中,您了解到故障排除有两种主要风格,直觉式(有经验的猜测者)和系统化(数据收集者)。我们介绍了哪些故障排除步骤对这两种风格最有效,以及一些人(适应者)可以利用这两种风格的故障排除。
在本书的后续章节中,当我们排除现实生活中的场景时,我将利用本章讨论的过程中突出的直觉和系统化的故障排除步骤。
本章没有涉及技术细节;下一章将充满技术细节,我们将介绍和探讨用于故障排除的常见 Linux 命令。
第二章:故障排除命令和有用信息的来源
在第一章中,我们介绍了故障排除的最佳实践和涉及的高级流程。第一章是对故障排除的 20,000 英尺视图,而本章开始深入具体内容。
本章将回顾常见的故障排除命令以及查找有用信息的常见位置。在本书中,我们将使用 Red Hat Enterprise Linux 的 7 版(也称为 RHEL)。本章引用的所有命令都是 RHEL 7 默认安装包中包含的命令。
我们将引用默认安装的命令,因为我发现自己曾经处于这样的情况,我本可以使用特定的命令立即识别问题,但是这个命令对我不可用。通过将本章限制为默认命令,您可以确保本章涵盖的故障排除步骤不仅与大多数 RHEL 7 安装相关,而且与以前的版本和其他 Linux 发行版相关。
查找有用信息
在开始探索故障排除命令之前,我首先想要介绍有用信息的位置。有用信息是一个模糊的术语,几乎每个文件、目录或命令都可以提供有用信息。我真正打算介绍的是几乎可以找到几乎任何问题的信息的位置。
日志文件
日志文件通常是查找故障排除信息的第一个地方。每当服务或服务器遇到问题时,检查错误日志文件通常可以迅速回答许多问题。
默认位置
默认情况下,RHEL 和大多数 Linux 发行版将其日志文件保存在/var/log/
中,这实际上是由 Linux 基金会维护的文件系统层次结构标准(FHS)的一部分。但是,虽然/var/log/
可能是默认位置,并非所有日志文件都位于那里(en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard
)。
虽然/var/log/httpd/
是 Apache 日志的默认位置,但可以通过 Apache 的配置文件更改此位置。当 Apache 安装在标准 RHEL 软件包之外时,这是非常常见的。
与 Apache 一样,大多数服务允许自定义日志位置。在/var/log
之外找到专门用于日志文件的自定义目录或文件系统并不罕见。
常见日志文件
以下表格是常见日志文件的简要列表,以及您可以在其中找到的内容的描述。
提示
请记住,此列表特定于 Red Hat Enterprise Linux 7,而其他 Linux 发行版可能遵循类似的约定,但不能保证。
日志文件 | 描述 |
---|---|
/var/log/messages |
默认情况下,此日志文件包含所有INFO 或更高优先级的 syslog 消息(除电子邮件)。 |
| /var/log/secure
| 此日志文件包含与身份验证相关的消息项,例如:
-
SSH 登录
-
用户创建
-
Sudo 违规和权限提升
|
/var/log/cron |
此日志文件包含crond 执行的历史记录,以及cron.daily 、cron.weekly 和其他执行的开始和结束时间。 |
---|---|
/var/log/maillog |
这个日志文件是邮件事件的默认日志位置。如果使用 postfix,这是所有与 postfix 相关的消息的默认位置。 |
/var/log/httpd/ |
此日志目录是 Apache 日志的默认位置。虽然这是默认位置,但并不是所有 Apache 日志的保证位置。 |
/var/log/mysql.log |
这个日志文件是 mysqld 的默认日志文件。与httpd 日志一样,这是默认的,可以很容易地更改。 |
/var/log/sa/ |
此目录包含默认每 10 分钟运行一次的sa 命令的结果。我们将在本章的后续部分以及本书的整个过程中更多地利用这些数据。 |
对于许多问题,要审查的第一个日志文件之一是/var/log/messages
日志。在 RHEL 系统上,这个日志文件接收所有INFO
优先级或更高级别的系统日志。一般来说,这意味着发送到syslog
的任何重要事件都会在这个日志文件中被捕获。
以下是可以在/var/log/messages
中找到的一些日志消息的示例:
Dec 24 18:03:51 localhost systemd: Starting Network Manager Script Dispatcher Service...
Dec 24 18:03:51 localhost dbus-daemon: dbus[620]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher'
Dec 24 18:03:51 localhost dbus[620]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher'
Dec 24 18:03:51 localhost systemd: Started Network Manager Script Dispatcher Service.
Dec 24 18:06:06 localhost kernel: e1000: enp0s3 NIC Link is Down
Dec 24 18:06:06 localhost kernel: e1000: enp0s8 NIC Link is Down
Dec 24 18:06:06 localhost NetworkManager[750]: <info> (enp0s3): link disconnected (deferring action for 4 seconds)
Dec 24 18:06:06 localhost NetworkManager[750]: <info> (enp0s8): link disconnected (deferring action for 4 seconds)
Dec 24 18:06:10 localhost NetworkManager[750]: <info> (enp0s3): link disconnected (calling deferred action)
Dec 24 18:06:10 localhost NetworkManager[750]: <info> (enp0s8): link disconnected (calling deferred action)
Dec 24 18:06:12 localhost kernel: e1000: enp0s3 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX
Dec 24 18:06:12 localhost kernel: e1000: enp0s8 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX
Dec 24 18:06:12 localhost NetworkManager[750]: <info> (enp0s3): link connected
Dec 24 18:06:12 localhost NetworkManager[750]: <info> (enp0s8): link connected
Dec 24 18:06:39 localhost kernel: atkbd serio0: Spurious NAK on isa0060/serio0\. Some program might be trying to access hardware directly.
Dec 24 18:07:10 localhost systemd: Starting Session 53 of user root.
Dec 24 18:07:10 localhost systemd: Started Session 53 of user root.
Dec 24 18:07:10 localhost systemd-logind: New session 53 of user root.
正如我们所看到的,在这个示例中有不止一条日志消息可能在故障排除问题时非常有用。
寻找不在默认位置的日志
很多时候日志文件不在/var/log/
中,这可能是因为有人修改了日志位置到默认位置之外的某个地方,或者仅仅是因为相关服务默认使用另一个位置。
一般来说,有三种方法可以找到不在/var/log/
中的日志文件。
检查 syslog 配置
如果您知道某个服务正在使用 syslog 进行日志记录,查找其消息写入的日志文件的最佳位置是rsyslog配置文件。rsyslog 服务有两个配置位置。第一个是/etc/rsyslog.d
目录。
/etc/rsyslog.d
目录是自定义 rsyslog 配置的包含目录。第二个是/etc/rsyslog.conf
配置文件。这是 rsyslog 的主配置文件,包含许多默认的 syslog 配置。
以下是/etc/rsyslog.conf
的默认内容示例:
#### RULES ####
# Log all kernel messages to the console.
# Logging much else clutters up the screen.
#kern.* /dev/console
# Log anything (except mail) of level info or higher.
# Don't log private authentication messages!
*.info;mail.none;authpriv.none;cron.none /var/log/messages
# The authpriv file has restricted access.
authpriv.* /var/log/secure
# Log all the mail messages in one place.
mail.* -/var/log/maillog
# Log cron stuff
cron.* /var/log/cron
通过审查这个文件的内容,很容易确定哪些日志文件包含所需的信息,如果不行,至少可以确定 syslog 管理的日志文件的可能位置。
检查应用程序的配置
并非每个应用程序都使用 syslog;对于那些不使用的应用程序,找到应用程序的日志文件的最简单方法之一是阅读应用程序的配置文件。
从配置文件中查找日志文件位置的一种快速有用的方法是使用grep
命令在文件中搜索单词log
:
$ grep log /etc/samba/smb.conf
# files are rotated when they reach the size specified with "max log size".
# log files split per-machine:
log file = /var/log/samba/log.%m
# maximum size of 50KB per log file, then rotate:
max log size = 50
grep command is used to search the /etc/samba/smb.conf file for any instance of the pattern "log".
在审查上述grep
命令的输出后,我们可以看到 samba 的配置日志位置是/var/log/samba/log.%m
。需要注意的是,在这个例子中,%m
实际上是在创建文件时用“机器名称”替换的。这实际上是 samba 配置文件中的一个变量。这些变量对每个应用程序都是唯一的,但这种动态配置值的方法是一种常见的做法。
其他例子
以下是使用grep
命令在 Apache 和 MySQL 配置文件中搜索单词“log
”的示例:
$ grep log /etc/httpd/conf/httpd.conf
# ErrorLog: The location of the error log file.
# logged here. If you *do* define an error logfile for a <VirtualHost>
# container, that host's errors will be logged there and not here.
ErrorLog "logs/error_log"
$ grep log /etc/my.cnf
# log_bin
log-error=/var/log/mysqld.log
在这两种情况下,这种方法能够识别服务日志文件的配置参数。通过前面的三个例子,很容易看出搜索配置文件的效果有多好。
使用 find 命令
我们将在本章后面深入介绍的find
命令是另一种查找日志文件的有用方法。find
命令用于在目录结构中搜索指定的文件。查找日志文件的快速方法是简单地使用find
命令搜索以“.log
”结尾的任何文件:
# find /opt/appxyz/ -type f -name "*.log"
/opt/appxyz/logs/daily/7-1-15/alert.log
/opt/appxyz/logs/daily/7-2-15/alert.log
/opt/appxyz/logs/daily/7-3-15/alert.log
/opt/appxyz/logs/daily/7-4-15/alert.log
/opt/appxyz/logs/daily/7-5-15/alert.log
上述通常被认为是最后的解决方案,大多数情况下是在之前的方法没有产生结果时使用。
提示
执行find
命令时,最好的做法是非常具体地指定要搜索的目录。当针对非常大的目录执行时,服务器的性能可能会下降。
配置文件
如前所述,应用程序或服务的配置文件可以是信息的绝佳来源。虽然配置文件不会提供特定的错误,比如日志文件,但它们可以提供关键信息(例如启用/禁用的功能、输出目录和日志文件位置)。
默认系统配置目录
一般来说,大多数 Linux 发行版的系统和服务配置文件位于/etc/
目录中。但这并不意味着每个配置文件都位于/etc/
目录中。事实上,应用程序通常会在应用程序的home
目录中包含一个配置目录。
那么,如何知道何时在/etc/
而不是应用程序目录中查找配置文件?一个经验法则是,如果软件包是 RHEL 发行版的一部分,可以安全地假设配置位于/etc/
目录中。其他任何东西可能存在于/etc/
目录中,也可能不存在。对于这些情况,你只需要去寻找它们。
查找配置文件
在大多数情况下,可以使用ls
命令对/etc/
目录进行简单的目录列表,以找到系统配置文件:
$ ls -la /etc/ | grep my
-rw-r--r--. 1 root root 570 Nov 17 2014 my.cnf
drwxr-xr-x. 2 root root 64 Jan 9 2015 my.cnf.d
ls to perform a directory listing and redirects that output to grep in order to search the output for the string "my". We can see from the output that there is a my.cnf configuration file and a my.cnf.d configuration directory. The MySQL processes use these for its configuration. We were able to find these by assuming that anything related to MySQL would have the string "my" in it.
使用 rpm 命令
如果配置文件是作为 RPM 软件包的一部分部署的,可以使用rpm
命令来识别配置文件。为此,只需执行带有-q
(查询)标志和-c
(configfiles)标志的rpm
命令,然后跟上软件包的名称:
$ rpm -q -c httpd
/etc/httpd/conf.d/autoindex.conf
/etc/httpd/conf.d/userdir.conf
/etc/httpd/conf.d/welcome.conf
/etc/httpd/conf.modules.d/00-base.conf
/etc/httpd/conf.modules.d/00-dav.conf
/etc/httpd/conf.modules.d/00-lua.conf
/etc/httpd/conf.modules.d/00-mpm.conf
/etc/httpd/conf.modules.d/00-proxy.conf
/etc/httpd/conf.modules.d/00-systemd.conf
/etc/httpd/conf.modules.d/01-cgi.conf
/etc/httpd/conf/httpd.conf
/etc/httpd/conf/magic
/etc/logrotate.d/httpd
/etc/sysconfig/htcacheclean
/etc/sysconfig/httpd
rpm
命令用于管理 RPM 软件包,在故障排除时非常有用。在下一节中,我们将进一步介绍这个命令,以探索故障排除的命令。
使用 find 命令
与查找日志文件类似,要在系统上查找配置文件,可以利用find
命令。在搜索日志文件时,find
命令用于搜索所有文件名以“.log
”结尾的文件。在下面的例子中,find
命令用于搜索所有文件名以“http
”开头的文件。这个find
命令应该至少返回一些结果,这些结果将提供与 HTTPD(Apache)服务相关的配置文件:
# find /etc -type f -name "http*"
/etc/httpd/conf/httpd.conf
/etc/sysconfig/httpd
/etc/logrotate.d/httpd
前面的例子搜索了/etc
目录;然而,这也可以用于搜索任何应用程序的主目录以查找用户配置文件。与搜索日志文件类似,使用find
命令搜索配置文件通常被认为是最后的手段,不应该是第一个使用的方法。
proc 文件系统
proc
文件系统是一个非常有用的信息来源。这是由 Linux 内核维护的一个特殊文件系统。proc
文件系统可用于查找有关运行进程以及其他系统信息的有用信息。例如,如果我们想要识别系统支持的文件系统,我们可以简单地读取/proc/filesystems
文件:
$ cat /proc/filesystems
nodev sysfs
nodev rootfs
nodev bdev
nodev proc
nodev cgroup
nodev cpuset
nodev tmpfs
nodev devtmpfs
nodev debugfs
nodev securityfs
nodev sockfs
nodev pipefs
nodev anon_inodefs
nodev configfs
nodev devpts
nodev ramfs
nodev hugetlbfs
nodev autofs
nodev pstore
nodev mqueue
nodev selinuxfs
xfs
nodev rpc_pipefs
nodev nfsd
这个文件系统非常有用,包含了关于运行系统的大量信息。proc 文件系统
将在本书的故障排除步骤中使用。它在故障排除各种问题时以不同的方式使用,从特定进程到只读文件系统。
故障排除命令
本节将介绍经常使用的故障排除命令,这些命令可用于从系统或运行的服务中收集信息。虽然不可能涵盖每个可能的命令,但所使用的命令确实涵盖了 Linux 系统的基本故障排除步骤。
命令行基础知识
本书中使用的故障排除步骤主要基于命令行。虽然可能可以从图形桌面环境执行许多这些操作,但更高级的项目是命令行特定的。因此,本书假设读者至少具有对 Linux 的基本理解。更具体地说,本书假设读者已经通过 SSH 登录到服务器,并熟悉基本命令,如cd
、cp
、mv
、rm
和ls
。
对于那些可能不太熟悉的人,我想快速介绍一些基本的命令行用法,这将是本书所需的基本知识。
命令标志
许多读者可能熟悉以下命令:
$ ls -la
total 588
drwx------. 5 vagrant vagrant 4096 Jul 4 21:26 .
drwxr-xr-x. 3 root root 20 Jul 22 2014 ..
-rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c
大多数人应该认识到这是ls
命令,用于执行目录列表。可能不熟悉的是命令的“-la”部分是什么或者做什么。为了更好地理解这一点,让我们单独看一下 ls 命令:
$ ls
app.c application app.py bomber.py index.html lookbusy-1.4 lookbusy-1.4.tar.gz lotsofiles
先前执行的ls
命令与以前的看起来非常不同。这是因为后者是ls
的默认输出。命令标志允许用户更改命令的默认行为,为其提供特定选项。
实际上,“-la”标志是两个单独的选项,“-l”和“-a”;它们甚至可以分开指定:
$ ls -l -a
total 588
drwx------. 5 vagrant vagrant 4096 Jul 4 21:26 .
drwxr-xr-x. 3 root root 20 Jul 22 2014 ..
-rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c
ls –la is exactly the same as ls –l –a. For common commands, such as the ls command, it does not matter if the flags are grouped or separated, they will be parsed in the same way. Throughout this book, examples will show both grouped and ungrouped. If grouping or ungrouping is performed for any specific reason it will be called out; otherwise, the grouping or ungrouping used within this book is used for visual appeal and memorization.
除了分组和取消分组,本书还将以长格式显示标志。在前面的例子中,我们显示了标志-a
,这被称为短标志。这个选项也可以以长格式--all
提供:
$ ls -l --all
total 588
drwx------. 5 vagrant vagrant 4096 Jul 4 21:26 .
drwxr-xr-x. 3 root root 20 Jul 22 2014 ..
-rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c
“-a”和--all
标志本质上是相同的选项;它可以简单地以短格式和长格式表示。
一个重要的事情要记住的是,并非每个短标志都有长形式,反之亦然。每个命令都有自己的语法,有些命令只支持短形式,其他命令只支持长形式,但许多命令都支持两种形式。在大多数情况下,长标志和短标志都将在命令的手册页面中得到记录。
管道命令输出
本书中将多次使用的另一种常见的命令行实践是将输出“传输”。具体来说,例如以下示例:
$ ls -l --all | grep app
-rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c
-rwxrwxr-x. 1 vagrant vagrant 29390 May 18 00:47 application
-rw-rw-r--. 1 vagrant vagrant 1198 Jun 10 17:03 app.py
在前面的例子中,ls -l --all
的输出被传输到grep
命令。通过在两个命令之间放置|
或管道字符,第一个命令的输出被“传输”到第二个命令的输入。将执行ls
命令的示例;随后,grep
命令将搜索该输出中的任何app
模式的实例。
在本书中,将经常使用将输出传输到grep
,因为这是将输出修剪为可维护大小的简单方法。许多时候,示例还将包含多个级别的管道:
$ ls -la | grep app | awk '{print $4,$9}'
vagrant app.c
vagrant application
vagrant app.py
在前面的代码中,ls -la
的输出被传输到grep
的输入;然而,这一次,grep
的输出也被传输到awk
的输入。
虽然许多命令可以进行管道传输,但并非每个命令都支持这一点。一般来说,接受来自文件或命令行的用户输入的命令也接受管道输入。与标志一样,命令的手册页面可用于确定命令是否接受管道输入。
收集一般信息
在长时间管理相同服务器时,您开始记住关于这些服务器的关键信息。例如物理内存的数量,文件系统的大小和布局,以及应该运行的进程。但是,当您不熟悉所讨论的服务器时,收集这种类型的信息总是一个好主意。
本节中的命令是用于收集此类一般信息的命令。
w-显示谁登录了以及他们在做什么
在我系统管理职业生涯的早期,我有一个导师告诉我:我每次登录服务器时都会运行 w。这个简单的提示实际上在我的职业生涯中一次又一次地非常有用。w
命令很简单;当执行时,它将输出系统正常运行时间,平均负载以及谁登录了:
# w
04:07:37 up 14:26, 2 users, load average: 0.00, 0.01, 0.05
USER TTY LOGIN@ IDLE JCPU PCPU WHAT
root tty1 Wed13 11:24m 0.13s 0.13s -bash
root pts/0 20:47 1.00s 0.21s 0.19s -bash
当与不熟悉的系统一起工作时,这些信息可能非常有用。即使您熟悉该系统,输出也可能很有用。通过这个命令,您可以看到:
- 上次系统重启时:
04:07:37 up 14:26
:这些信息可能非常有用;无论是像 Apache 服务宕机的警报,还是用户因为被系统锁定而打进来。当这些问题是由意外重启引起时,报告的问题通常不包括这些信息。通过运行w
命令,很容易看到自上次重启以来经过的时间。
- 系统的平均负载:
平均负载:0.00, 0.01, 0.05
:平均负载是系统健康的一个非常重要的衡量标准。总结一下,平均负载是一段时间内处于等待
状态的进程的平均数量。w
命令输出中的三个数字代表不同的时间。
这些数字从左到右依次是 1 分钟、5 分钟和 15 分钟。
-
谁登录了以及他们在运行什么:
-
USER TTY LOGIN@ IDLE JCPU PCPU WHAT
-
root tty1 Wed13 11:24m 0.13s 0.13s -bash
w
命令提供的最后一条信息是当前登录的用户以及他们正在执行的命令。
这基本上与who
命令的输出相同,包括已登录的用户、他们登录的时间、他们已经空闲了多长时间,以及他们的 shell 正在运行的命令。列表中的最后一项非常重要。
在与大团队合作时,往往会有多个人响应一个问题或工单是很常见的。在登录后立即运行w
命令,你将看到其他用户在做什么,避免你覆盖其他人已经采取的故障排除或纠正步骤。
rpm – RPM 软件包管理器
rpm
命令用于管理Red Hat 软件包管理器(RPM)。使用这个命令,你可以安装和删除 RPM 软件包,以及搜索已安装的软件包。
在本章的前面,我们看到rpm
命令可以用来查找配置文件。以下是我们可以使用rpm
命令查找关键信息的几种额外方式。
列出所有安装的软件包
在故障排除服务时,一个关键的步骤是确定服务的版本以及它是如何安装的。要列出系统上安装的所有 RPM 软件包,只需执行带有-q
(查询)和-a
(所有)的rpm
命令:
# rpm -q -a
kpatch-0.0-1.el7.noarch
virt-what-1.13-5.el7.x86_64
filesystem-3.2-18.el7.x86_64
gssproxy-0.3.0-9.el7.x86_64
hicolor-icon-theme-0.12-7.el7.noarch
rpm
命令是一个非常多样化的命令,有很多标志。在前面的例子中使用了-q
和-a
标志。-q
标志告诉rpm
命令正在进行的操作是一个查询;你可以把它想象成进入了“搜索模式”。-a
或--all
标志告诉rpm
命令列出所有软件包。
一个有用的功能是在前面的命令中添加--last
标志,因为这会导致rpm
命令按安装时间列出软件包,最新的排在最前面。
列出软件包部署的所有文件
另一个有用的rpm
功能是显示特定软件包部署的所有文件:
# rpm -q --filesbypkg kpatch-0.0-1.el7.noarch
kpatch /usr/bin/kpatch
kpatch /usr/lib/systemd/system/kpatch.service
在前面的例子中,我们再次使用-q
标志来指定我们正在运行一个查询,以及--filesbypkg
标志。--filesbypkg
标志将导致rpm
命令列出指定软件包部署的所有文件。
当试图确定服务的配置文件位置时,这个例子非常有用。
使用软件包验证
在这第三个例子中,我们将使用rpm
的一个非常有用的功能——验证。rpm
命令有能力验证指定软件包部署的文件是否已经被更改。为了做到这一点,我们将使用-V
(验证)标志:
# rpm -V httpd
S.5....T. c /etc/httpd/conf/httpd.conf
在前面的例子中,我们只是运行了带有-V
标志的rpm
命令,后面跟着一个软件包名称。由于-q
标志用于查询,-V
标志用于验证。使用这个命令,我们可以看到只有/etc/httpd/conf/httpd.conf
文件被列出;这是因为rpm
只会输出已经被更改的文件。
在这个输出的第一列中,我们可以看到文件失败的验证检查。虽然这一列起初有点神秘,但 rpm 手册中有一个有用的表格(如下列表所示),解释了每个字符的含义:
-
S
: 这意味着文件大小不同 -
M
: 这意味着模式不同(包括权限和文件类型) -
5
: 这意味着摘要(以前是MD5 校验和
)不同 -
D
: 这意味着设备主/次编号不匹配 -
L
: 这意味着readLink(2)
路径不匹配 -
U
: 这意味着用户所有权不同 -
G
: 这意味着组所有权不同 -
T
: 这意味着mTime
不同 -
P
: 这意味着caPabilities
不同
使用这个列表,我们可以看到httpd
.conf
文件大小、MD5
校验和和mtime
(修改时间)与httpd.rpm
部署的不同。这意味着很可能httpd.conf
文件在安装后被修改过。
虽然rpm
命令一开始可能不像是一个故障排除命令,但前面的例子显示了它实际上是一个多么强大的故障排除工具。通过这些例子,很容易识别重要文件以及这些文件是否已经从部署版本中修改。
df - 报告文件系统空间使用情况
df
命令在故障排除文件系统问题时非常有用。df
命令用于输出已挂载文件系统的空间利用情况:
# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/rhel-root 6.7G 1.6G 5.2G 24% /
devtmpfs 489M 0 489M 0% /dev
tmpfs 498M 0 498M 0% /dev/shm
tmpfs 498M 13M 485M 3% /run
tmpfs 498M 0 498M 0% /sys/fs/cgroup
/dev/sdb1 212G 58G 144G 29% /repos
/dev/sda1 497M 117M 380M 24% /boot
在前面的例子中,df
命令包括了-h
标志。这个标志会导致df
命令以“人类可读”的格式打印任何大小值。默认情况下,df
会简单地以千字节打印这些值。从例子中,我们可以快速看到所有挂载文件系统的当前使用情况。具体来说,如果我们看输出,我们可以看到/filesystem
目前使用了 24%:
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/rhel-root 6.7G 1.6G 5.2G 24% /
这是一个非常快速简单的方法来识别任何文件系统是否已满。此外,df
命令还非常有用,可以显示已挂载的文件系统的详细信息以及它们被挂载到哪里。从包含/filesystem
的那一行,我们可以看到底层设备是/dev/mapper/rhel-root
。
通过这个命令,我们能够识别出两个关键信息。
显示可用的 inode
df
的默认行为是显示已使用的文件系统空间量。但是,它也可以用来显示每个文件系统可用、已使用和空闲的inodes数量。要输出 inode 利用率,只需在执行df
命令时添加-i
(inode)标志:
# df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/mapper/rhel-root 7032832 44318 6988514 1% /
devtmpfs 125039 347 124692 1% /dev
仍然可以使用-h
标志与df
一起以人类可读的格式打印输出。但是,使用-i
标志,这将把输出缩写为M
表示百万,K
表示千,依此类推。这个输出很容易与兆字节或千字节混淆,所以一般情况下,我不会在与其他用户/管理员共享输出时使用人类可读的 inode 输出。
free - 显示内存利用率
执行free
命令时,将输出系统上可用内存和已使用内存的统计信息:
$ free
total used free shared buffers cached
Mem: 1018256 789796 228460 13116 3608 543484
-/+ buffers/cache: 242704 775552
Swap: 839676 4 839672
从前面的例子可以看出,free
命令的输出提供了总可用内存、当前使用的内存量和空闲内存量。free
命令是识别系统内存当前状态的一种简单快速的方式。
然而,free
的输出一开始可能有点令人困惑。
所谓的空闲,并不总是空闲
Linux 与其他操作系统相比,利用内存的方式不同。在前面的输出中,您将看到有 543,484 KB 被列为缓存。这个内存,虽然在技术上被使用,实际上是可用内存的一部分。系统可以根据需要重新分配这个缓存内存。
一个快速简单的方法来查看实际使用或空闲的内容可以在输出的第二行看到。前面的输出显示系统上有 775,552 KB 的内存可用。
/proc/meminfo 文件
在以前的 RHEL 版本中,free
命令的第二行是识别可用内存量的最简单方法。但是,随着 RHEL 7,/proc/meminfo
文件已经进行了一些改进。其中一个改进是增加了MemAvailable统计信息:
$ grep Available /proc/meminfo
MemAvailable: 641056 kB
/proc/meminfo
文件是位于/proc
文件系统中的许多有用文件之一。该文件由内核维护,包含系统当前的内存统计信息。在排除内存问题时,此文件非常有用,因为它包含的信息比free
命令的输出要多得多。
ps – 报告当前运行进程的快照
ps
命令是任何故障排除活动的基本命令。执行此命令将输出运行进程的列表:
# ps
PID TTY TIME CMD
15618 pts/0 00:00:00 ps
17633 pts/0 00:00:00 bash
ps
命令有许多标志和选项,可显示有关运行进程的不同信息。以下是一些在故障排除期间有用的ps
命令示例。
以长格式打印每个进程
以下ps
命令使用-e
(所有进程)、-l
(长格式)和-f
(完整格式)标志。这些标志将导致ps
命令不仅打印每个进程,还将以提供相当多有用信息的格式打印它们:
# ps -elf
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
1 S root 2 0 0 80 0 - 0 kthrea Dec24 ? 00:00:00 [kthreadd]
在ps -elf
的前面输出中,我们可以看到kthreadd
进程的许多有用信息,例如父进程 ID(PPID)、优先级(PRI)、niceness 值(NI)和运行进程的驻留内存大小(SZ)。
我发现前面的示例是一个非常通用的ps
命令,可以在大多数情况下使用。
打印特定用户的进程
前面的示例可能会变得非常庞大,使得难以识别特定进程。此示例使用-U
标志来指定用户。这会导致ps
命令打印作为指定用户运行的所有进程;在以下情况下是后缀:
ps -U postfix -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 89 1546 1536 0 80 0 - 23516 ep_pol ? 00:00:00 qmgr
4 S 89 16711 1536 0 80 0 - 23686 ep_pol ? 00:00:00 pickup
需要注意的是,–U
标志也可以与其他标志结合使用,以提供有关运行进程的更多信息。在前面的示例中,-l
标志再次用于以长格式打印输出。
按进程 ID 打印进程
如果进程 ID 或 PID 已知,可以通过指定–p
(进程 ID)标志来进一步缩小进程列表:
# ps -p 1236 -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 1236 1 0 80 0 - 20739 poll_s ? 00:00:00 sshd
当与–L
(显示带有 LWP 列的线程)或–m
(显示进程后的线程)标志结合使用时,这可能特别有用,这些标志用于打印进程线程。在排除多线程应用程序故障时,-L
和-m
标志可能至关重要。
打印带有性能信息的进程
ps
命令允许用户使用-o
(用户定义格式)标志自定义打印的列:
# ps -U postfix -o pid,user,pcpu,vsz,cmd
PID USER %CPU VSZ CMD
1546 postfix 0.0 94064 qmgr -l -t unix -u
16711 postfix 0.0 94744 pickup -l -t unix -u
–o
选项允许使用许多自定义列。在前面的版本中,我选择了与 top 命令中打印的类似的选项。
top 命令是最受欢迎的 Linux 故障排除命令之一。它用于按 CPU 使用率(默认情况下)显示前几个进程。在本章中,我选择省略 top 命令,因为我认为ps
命令比 top 命令更基本和灵活。随着对ps
命令的熟悉,学习和理解 top 命令将变得容易。
网络
网络对于任何系统管理员来说都是一项基本技能。没有正确配置的网络接口,服务器就没有多大用处。本节中的命令专门用于查找网络配置和当前状态。这些命令是必须学习的,因为它们不仅对故障排除有用,而且对日常设置和配置也很有用。
ip – 显示和操作网络设置
ip
命令用于管理网络设置,如接口配置、路由和基本上与网络相关的任何内容。虽然这些通常不被认为是故障排除任务,但ip
命令也可以用于显示系统的网络配置。如果无法查找网络详细信息,如路由或设备配置,将很难排除与网络相关的问题。
以下示例展示了使用ip
命令识别关键网络配置设置的各种方法。
显示特定设备的 IP 地址配置
ip
命令的一个核心用途是查找网络接口并显示其配置。为了做到这一点,我们将使用以下命令:
# ip addr show dev enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:6e:35:18 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 45083sec preferred_lft 45083sec
inet6 fe80::a00:27ff:fe6e:3518/64 scope link
valid_lft forever preferred_lft forever
在前面的ip
命令中,提供的第一个选项addr
(地址)用于定义我们要查找的信息类型。第二个选项show
告诉ip
显示第一个选项的配置。第三个选项dev
(设备)后面跟着所讨论的网络接口设备;enp0s3
。如果省略了第三个选项,ip
命令将显示所有网络设备的地址配置。
对于那些有经验的 RHEL 之前版本的人来说,设备名称enp0s3
可能看起来有点奇怪。这个设备遵循了systemd
引入的较新的网络设备命名方案。从 RHEL 7 开始,网络设备将使用基于设备驱动程序和 BIOS 详细信息的设备名称。
要了解更多关于 RHEL 7 的新命名方案的信息,请参考以下 URL:
显示路由配置
ip
命令还可以用于显示路由配置。这些信息对于排除服务器之间的连接问题至关重要。
# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15
192.168.56.0/24 dev enp0s8 proto kernel scope link src 192.168.56.101
前面的ip
命令使用route
选项,后面跟着show
选项来显示此服务器的所有定义路由。与前面的例子一样,也可以通过添加dev
(设备)选项后跟设备名称来限制此输出到特定设备:
# ip route show dev enp0s3
default via 10.0.2.2 proto static metric 1024
10.0.2.0/24 proto kernel scope link src 10.0.2.15
显示指定设备的网络统计信息
前面的例子展示了查找当前网络配置的方法,而这个命令使用-s
(统计)标志来显示指定设备的网络统计信息:
# ip -s link show dev enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:6e:35:18 brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped overrun mcast
109717927 125911 0 0 0 0
TX: bytes packets errors dropped carrier collsns
3944294 40127 0 0 0 0
在前面的例子中,使用了link
(网络设备)选项来指定统计信息应该限制在指定的设备上。
显示的统计信息在排除丢包或识别哪个接口具有更高的网络利用率时非常有用。
netstat - 网络统计
netstat
命令是任何系统管理员工具包中的基本工具。这可以通过netstat
命令普遍适用于即使不传统使用命令行进行管理的操作系统来看出。
打印网络连接
netstat
的主要用途之一是打印现有的已建立的网络连接。这可以通过简单执行netstat
来完成;然而,如果使用了-a
(所有)标志,输出也将包括监听端口:
# netstat -na
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:44969 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 192.168.56.101:22 192.168.56.1:50122 ESTABLISHED
tcp6 0 0 ::1:25 :::* LISTEN
虽然在前面的netstat
中使用了-a
(所有)标志来打印所有监听端口,但-n
标志用于强制输出为数字格式,例如打印 IP 地址而不是 DNS 主机名。
前面的例子将在第五章网络故障排除中大量使用,我们将在那里进行网络连接故障排除。
打印所有监听 tcp 连接的端口
我曾经看到许多情况下,一个服务正在运行,并且可以通过ps
命令看到;但是,客户端连接的端口没有绑定和监听。在解决服务的连接问题时,以下netstat
命令非常有用:
# netstat -nlp --tcp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1536/master
tcp 0 0 0.0.0.0:44969 0.0.0.0:* LISTEN 1270/rpc.statd
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1215/rpcbind
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1236/sshd
tcp6 0 0 ::1:25 :::* LISTEN 1536/master
tcp6 0 0 :::111 :::* LISTEN 1215/rpcbind
tcp6 0 0 :::22 :::* LISTEN 1236/sshd
tcp6 0 0 :::46072 :::* LISTEN 1270/rpc.statd
前面的命令非常有用,因为它结合了三个有用的选项:
-
–l
(监听)告诉netstat
只列出正在监听的套接字 -
--tcp
告诉netstat
将输出限制为 TCP 连接 -
–p
(程序)告诉netstat
列出在该端口上监听的进程的 PID 和名称
延迟
netstat
的一个经常被忽视的选项是利用延迟功能。通过在命令的末尾添加一个数字,netstat
将持续运行,并在执行之间休眠指定的秒数。
如果执行以下命令,netstat
命令将每五秒打印所有正在监听的 TCP 套接字:
# netstat -nlp --tcp 5
延迟功能在调查网络连接问题时非常有用。因为它可以很容易地显示应用程序何时为新连接绑定端口。
性能
虽然我们稍微提到了使用free
和ps
等命令来解决性能问题,但本节将展示一些非常有用的命令,这些命令可以回答“为什么慢”的古老问题。
iotop - 一个简单的类似 top 的 I/O 监视器
iotop
命令是 Linux 上相对较新的命令。在以前的 RHEL 版本中,虽然可用,但默认情况下未安装iotop
命令。iotop
命令提供了一个类似 top 命令的界面,但它不是显示哪些进程正在使用最多的 CPU 时间或内存,而是显示按 I/O 利用率排序的进程:
# iotop
Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND
1536 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % master -w
1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd --switched-root --system --deserialize 23
与以前的一些命令不同,iotop
非常专门用于显示正在使用 I/O 的进程。但是,有一些非常有用的标志可以改变iotop
的默认行为。例如–o
(仅)告诉iotop
仅打印使用 I/O 的进程,而不是其默认行为打印所有进程。另一组有用的标志是-q
(安静)和–n
(迭代次数)。
连同-o
标志一起,这些标志可以用来告诉iotop
仅打印使用 I/O 的进程,而不清除下一次迭代的屏幕:
# iotop -o -q -n2
Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND
Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
22965 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.03 % [kworker/0:3]
如果我们看一下前面的示例输出,我们可以看到iotop
命令的两个独立迭代。但是,与以前的示例不同,输出是连续的,允许我们看到每次迭代时使用 I/O 的进程。
默认情况下,iotop
迭代之间的延迟是 1 秒;但是,可以使用-d
(延迟)标志进行修改。
iostat - 报告 I/O 和 CPU 统计信息
iotop
显示正在使用 I/O 的进程,iostat
显示正在被利用的设备:
# iostat -t 1 2
Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/25/2014 _x86_64_ (1 CPU)
12/25/2014 03:20:10 PM
avg-cpu: %user %nice %system %iowait %steal %idle
0.11 0.00 0.17 0.01 0.00 99.72
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 0.38 2.84 7.02 261526 646339
sdb 0.01 0.06 0.00 5449 12
dm-0 0.33 2.77 7.00 254948 644275
dm-1 0.00 0.01 0.00 936 4
12/25/2014 03:20:11 PM
avg-cpu: %user %nice %system %iowait %steal %idle
0.00 0.00 0.99 0.00 0.00 99.01
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 0.00 0.00 0.00 0 0
sdb 0.00 0.00 0.00 0 0
dm-0 0.00 0.00 0.00 0 0
dm-1 0.00 0.00 0.00 0 0
前面的iostat
命令使用-t
(时间戳)标志在每个报告中打印时间戳。两个数字是间隔和计数值。在前面的示例中,iostat
以一秒的间隔运行,总共迭代两次。
iostat
命令对诊断与 I/O 相关的问题非常有用。但是,输出通常会产生误导。当执行时,第一个报告中提供的值是系统上次重启以来的平均值。随后的报告是自上一个报告以来的。在这个例子中,我们执行了两个报告,相隔一秒。您可以看到第一个报告中的数字比第二个报告中的数字要高得多。
因此,许多系统管理员简单地忽略第一个报告,但他们并不完全理解为什么。因此,对于不熟悉iostat
的人来说,对第一个报告中的值做出反应并不罕见。
iostat
命令有一个-y
标志(省略第一个报告),这实际上会导致iostat
省略第一个报告。这是一个很好的标志,可以教给那些可能不太熟悉使用iostat
的用户。
操纵输出
iostat
命令也有一些非常有用的标志,允许您操纵它呈现数据的方式。例如-p
(设备)标志允许您将统计信息限制为指定的设备,或者-x
(扩展统计)将打印扩展统计信息:
# iostat -p sda -tx
Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/25/2014 _x86_64_ (1 CPU)
12/25/2014 03:38:00 PM
avg-cpu: %user %nice %system %iowait %steal %idle
0.11 0.00 0.17 0.01 0.00 99.72
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.01 0.02 0.13 0.25 2.81 6.95 51.70 0.00 7.62 1.57 10.79 0.85 0.03
sda1 0.00 0.00 0.02 0.02 0.05 0.02 3.24 0.00 0.24 0.42 0.06 0.23 0.00
sda2 0.01 0.02 0.11 0.19 2.75 6.93 65.47 0.00 9.34 1.82 13.58 0.82 0.02
前面的示例使用了-p
标志来指定sda
设备,-t
标志来打印时间戳,-x
标志来打印扩展统计信息。在测量特定设备的 I/O 性能时,这些标志非常有用。
vmstat - 报告虚拟内存统计信息
iostat
用于报告有关磁盘 I/O 性能的统计信息,而vmstat
用于报告有关内存使用和性能的统计信息:
# vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 4 225000 3608 544900 0 0 3 7 17 28 0 0 100 0 0
0 0 4 224992 3608 544900 0 0 0 0 19 19 0 0 100 0 0
0 0 4 224992 3608 544900 0 0 0 0 6 9 0 0 100 0 0
vmstat
的语法与iostat
非常相似,您可以在命令行参数中提供间隔和报告计数。与iostat
一样,第一个报告实际上是自上次重启以来的平均值,随后的报告是自上一个报告以来的。不幸的是,与iostat
命令不同,vmstat
命令没有包含一个标志来省略第一个报告。因此,在大多数情况下,简单地忽略第一个报告是合适的。
虽然vmstat
可能不包括省略第一个报告的标志,但它确实有一些非常有用的标志;例如-m
(slabs),这会导致vmstat
以定义的间隔输出系统的slabinfo
,以及-s
(stats),它会打印系统的内存统计的扩展报告:
# vmstat -stats
1018256 K total memory
793416 K used memory,
290372 K active memory
360660 K inactive memory
224840 K free memory
3608 K buffer memory
544908 K swap cache
839676 K total swap
4 K used swap
839672 K free swap
10191 non-nice user cpu ticks
67 nice user cpu ticks
11353 system cpu ticks
9389547 idle cpu ticks
556 IO-wait cpu ticks
33 IRQ cpu ticks
4434 softirq cpu ticks
0 stolen cpu ticks
267011 pages paged in
647220 pages paged out
0 pages swapped in
1 pages swapped out
1619609 interrupts
2662083 CPU context switches
1419453695 boot time
59061 forks
前面的代码是使用-s
或--stats
标志的示例。
sar - 收集、报告或保存系统活动信息
一个非常有用的实用程序是sar
命令,sar
是sysstat
软件包附带的实用程序。sysstat
软件包包括各种实用程序,用于收集磁盘、CPU、内存和网络利用率等系统指标。默认情况下,这个收集将每 10 分钟运行一次,并作为cron
作业在/ettc/cron.d/sysstat
中执行。
虽然sysstat
收集的数据可能非常有用,但在高性能环境中有时会删除这个软件包。因为系统利用率统计数据的收集可能会增加系统的利用率,导致性能下降。要查看sysstat
软件包是否已安装,只需使用 rpm 命令和-q
(查询)标志:
# rpm -q sysstat
sysstat-10.1.5-4.el7.x86_64
使用 sar 命令
sar
命令允许用户查看sysstat
实用程序收集的信息。当不带标志执行sar
命令时,将打印当天的 CPU 统计信息:
# sar | head -6
Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/25/2014 _x86_64_ (1 CPU)
12:00:01 AM CPU %user %nice %system %iowait %steal %idle
12:10:02 AM all 0.05 0.00 0.20 0.01 0.00 99.74
12:20:01 AM all 0.05 0.00 0.18 0.00 0.00 99.77
12:30:01 AM all 0.06 0.00 0.25 0.00 0.00 99.69
每天午夜,systat
收集器将创建一个新文件来存储收集的统计信息。要引用该文件中的统计信息,只需使用-f
(文件)标志来针对指定的文件运行sar
:
# sar -f /var/log/sa/sa13
Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/13/2014 _x86_64_ (1 CPU)
10:24:43 AM LINUX RESTART
10:30:01 AM CPU %user %nice %system %iowait %steal %idle
10:40:01 AM all 2.99 0.00 0.96 0.43 0.00 95.62
10:50:01 AM all 9.70 0.00 2.17 0.00 0.00 88.13
11:00:01 AM all 0.31 0.00 0.30 0.02 0.00 99.37
11:10:01 AM all 1.20 0.00 0.41 0.01 0.00 98.38
11:20:01 AM all 0.01 0.00 0.04 0.01 0.00 99.94
11:30:01 AM all 0.92 0.07 0.42 0.01 0.00 98.59
11:40:01 AM all 0.17 0.00 0.08 0.00 0.00 99.74
11:50:02 AM all 0.01 0.00 0.03 0.00 0.00 99.96
在前面的代码中,指定的文件是/var/log/sa/sa13
;这个文件包含了当月第 13 天的统计信息。
sar
命令有许多有用的标志,远远不止在本章中列出的。以下列出了一些非常有用的标志:
-
-b:这会打印类似于
iostat
命令的 I/O 统计信息 -
-n ALL
:这会打印所有网络设备的网络统计信息 -
-R
:这会打印内存利用率统计信息 -
-A
:这会打印所有收集的统计信息。它基本上等同于运行sar -bBdHqrRSuvwWy -I SUM -I XALL -m ALL -n ALL -u ALL -P ALL
虽然sar
命令显示了许多统计信息,但我们已经涵盖了诸如iostat
或vmstat
之类的命令。sar
命令最大的好处在于能够回顾过去的统计数据。当排除发生在短时间内或已经被缓解的性能问题时,这种能力是至关重要的。
总结
在本章中,您了解到日志文件、配置文件和/proc
文件系统是故障排除过程中的关键信息来源。我们还介绍了许多基本故障排除命令的基本用法。
在阅读本章的过程中,您可能已经注意到,相当多的命令也用于日常生活中的非故障排除目的。如果我们回顾一下来自第一章 故障排除最佳实践 的故障排除过程,第一步包括信息收集。
虽然这些命令本身可能无法解释问题,但它们可以帮助收集有关问题的信息,从而实现更准确和快速的解决方案。熟悉这些基本命令对于您在故障排除过程中取得成功至关重要。
在接下来的几章中,我们将使用这些基本命令来解决现实世界中的问题。下一章将重点解决与基于 Web 的应用程序相关的问题。
第三章:故障排除 Web 应用程序
在本书的第一章和第二章中,我们介绍了故障排除过程、信息的常见位置和有用的故障排除命令。在本章中,我们将通过一个示例问题来运行,以演示多种故障排除和补救步骤。特别是,我们将看一下解决基于 Web 的应用程序问题所需的步骤。
在本章中,我将逐步介绍故障排除过程的每一步,并解释每一步背后的原因。虽然本章涵盖的问题可能不是一个非常常见的问题,但重要的是看待解决问题的过程和使用的工具。本章中使用的过程和工具可以应用于大多数 Web 应用程序问题。
一个小小的背景故事
在本书的每一章中,您都会找到一个示例问题,涵盖了常见的故障排除主题。虽然本书的重点是展示解决这些类型问题所需的命令和概念,但展示解决问题的过程也是很重要的。为了做到这一点,我们将探讨这些问题,就好像我们是最近加入新公司的新系统管理员一样。
每个问题都会以稍微不同的方式呈现,但每个问题都将以报告问题的方式开始。
报告的问题
在新公司开始新角色时,我们被指派接听公司网络运营中心(NOC)的电话。在这个角色中,我们将专注于解决公司环境中的问题,并且期望能够非常快速地解决问题。对于我们的第一个问题,我们接到了一个电话;在电话的另一端是一个有问题的业务用户。突然间,我们的博客显示的是一个安装页面,而不是我们的帖子!
既然我们已经有了一个报告的问题,让我们开始逐步进行故障排除过程。
数据收集
如果我们回顾一下第一章,故障排除最佳实践,故障排除过程的第一步是理解问题陈述。在本节中,我们将探讨问题是如何报告的,并尝试收集任何数据,以找到问题的根本原因。
在这个示例中,我们是通过电话通知的问题。这实际上是幸运的,因为我们有一个最终用户在电话那头,可以向他/她提问以获取更多信息。
在要求报告问题的人提供更多信息之前,让我们先看看已经得到的回答。突然间,我们的博客显示的是一个安装页面,而不是我们的帖子!
一开始,你可能觉得这个问题陈述模糊不清;这是因为它确实模糊不清。然而,在这个简单的句子中仍然包含了相当多有用的信息。如果我们分析一下报告的问题,我们就可以更好地理解问题。
-
“我们的博客显示的是一个安装页面”
-
“突然间”
-
“不是我们的帖子!”
从这三个部分中,我们可以假设以下内容:
-
博客显示了一个意外的页面
-
这个博客以前显示过帖子
-
在某个时候,这种情况发生了变化,而且似乎是最近发生的
虽然以上内容对于确定是否存在问题以及问题的相关性是一个不错的开始,但它还不足以让我们得出假设。
提问
为了制定假设,我们需要更多信息。获取这些信息的一种方法是询问报告问题的人。为了获取更多信息,我们将向业务用户提出以下问题:
- 你上次看到博客工作是什么时候?
昨晚。
- 博客的地址是什么?
http://blog.example.com
- 你有收到其他错误吗?
没有。
虽然以上问题还不足以确定问题,但它们给了我们一个开始寻找问题的起点。
复制问题
如前所述,在第一章中,故障排除最佳实践中找到信息的最佳方法之一是复制问题。在这种情况下,似乎我们可以通过简单地访问提供的地址来复制问题。
在前面的屏幕截图中,我们可以看到博客的表现与用户描述的一样。当我们访问提供的 URL 时,我们看到了一个默认的 WordPress 安装界面。
这是否给我们提供了关于问题原因的任何线索?不,实际上并没有,除非我们之前见过这个问题。虽然这可能不告诉我们问题的原因,但它确实确认了用户报告的问题是可以重现的。这一步还告诉了我们正在排查的软件的名称:WordPress。
WordPress 是最流行的开源博客平台之一。在本章中,我们假设我们没有管理 WordPress 的经验,需要通过在线来源找到我们需要的关于这个 Web 应用程序的任何信息。
了解环境
由于我们是新的系统管理员,在这一点上,我们对这个环境知之甚少,这意味着我们对这个博客是如何部署的知之甚少。事实上,我们甚至不知道它是从哪个服务器运行的。
这个博客托管在哪里?
然而,我们知道的一件事是,我们公司管理的所有服务器的 IP 都在 192.168.0.0/16 子网内。为了确定这是否是我们可以解决的问题,我们首先需要确定博客是否在我们公司管理的服务器上。如果这个博客不是在我们公司管理的服务器上,我们的故障排除选项可能会受到限制。
确定博客托管位置的一种方法是简单地查找blog.example.com
地址的 IP 地址。
使用 nslookup 查找 IP
有许多方法可以查找 DNS 名称的 IP 地址;我们将讨论的命令是nslookup
命令。要使用这个命令,只需执行nslookup
,然后是要查找的 DNS 名称:例如,对于这个例子,是blog.example.com
。
$ nslookup blog.example.com
Server: 192.0.2.1
Address: 192.0.2.1#53
Non-authoritative answer:
Name: blog.example.com
Address: 192.168.33.11
在前面的输出中,对于那些不熟悉nslookup
的人来说,结果可能有点令人困惑。
Non-authoritative answer:
Name: blog.example.com
Address: 192.168.33.11
我们知道前面的信息是nslookup
查询的结果。这个块表示blog.example.com
域的地址是192.168.33.11
。nslookup
的输出的第一个块只是告诉我们使用了哪个 DNS 服务器来查找这些信息。
Server: 192.0.2.1
Address: 192.0.2.1#53
从这个块中我们可以看到使用的 DNS 服务器是192.0.2.1
。
ping、dig 或其他工具呢?
有许多命令可以用来查找这个域的 IP 地址。我们可以使用dig
、host
,甚至ping
。我们选择nslookup
命令的原因是,它大多数情况下都包含在大多数操作系统中。因此,无论您需要从 Windows、Mac 还是 Linux 桌面查找 IP 地址,您都可以使用nslookup
命令。
然而,nslookup
命令的一个注意事项是,它专门使用 DNS 来查找地址。它不尊重/etc/hosts
中的值或/etc/nsswitch.conf
中指定的任何其他名称服务。这是我们将在后面的章节中更多探讨的内容;现在,我们将假设192.168.33.11
的 IP 地址是正确的。
好的,它在我们的环境中;现在怎么办?
由于我们正在使用 Linux 服务器,管理该服务器的最常见方式是通过Secure Shell(SSH)。SSH 是一种安全的网络服务,允许用户远程访问服务器的 shell。对于本书,我们将假设您已经熟悉通过 SSH 登录服务器。无论您使用 SSH 命令行客户端还是像 PuTTY 这样的桌面客户端,我们假设您能够使用 SSH 登录服务器。
在这种情况下,我们使用的是一个具有自己 shell 环境的笔记本电脑。要登录到我们的服务器,我们只需从终端窗口执行ssh
命令。
$ ssh vagrant@blog.example.com
vagrant@blog.example.com's password:
登录后,我们执行的第一个信息收集命令是w
命令。
$ w
18:32:17 up 2 days, 12:05, 1 user, load average: 0.11, 0.08, 0.07
USER TTY LOGIN@ IDLE JCPU PCPU WHAT
vagrant pts/1 00:53 2.00s 0.00s 0.08s sshd: vagrant [priv]
在第二章中,故障排除命令和有用信息的来源,我们介绍了w
命令,并提到它是第一个执行的命令。我们可以在w
命令的输出中看到相当多有用的信息。
从这个输出中,我们可以确定以下内容:
-
当前只有 1 个用户登录(这是我们的登录会话)
-
问题的服务器已经运行了 2 天
-
负载平均值很低,这表明正常
总的来说,乍一看,服务器似乎表现正常。问题是昨晚开始的,这表明问题并不是在 2 天前的重启之后开始的。负载平均值低,因此在这一点上可以安全地假设问题与系统负载无关。
安装并运行了哪些服务?
由于我们以前从未登录过这台服务器,并且对这个环境完全不熟悉,我们应该首先找出这台服务器上运行着哪些服务。
由于我们从安装页面得知博客是 WordPress 博客,我们可以搜索 Google 关于它所需服务的信息。我们可以通过使用搜索词“WordPress 安装要求”来做到这一点。
这个搜索字符串返回了以下 URL 作为第一个结果:wordpress.org/about/requirements/
。这个页面包含了 WordPress 的安装要求,并列出了以下内容:
-
PHP 5.2.4
-
MySQL 5.0 或更高版本
-
要么是 Apache 要么是 Nginx Web 服务器
从我们可以访问安装页面这一事实,我们可以假设已安装并且部分工作的是一个 Web 服务器和 PHP。然而,最好的做法是验证而不是假设。
验证 Web 服务器
由于 WordPress 推荐使用Apache或Nginx Web 服务器,我们首先需要确定安装了哪一个,更重要的是,确定这个 WordPress 应用程序正在使用哪一个。
以下是几种识别已安装和正在运行的 Web 服务器的方法:
-
我们可以使用
rpm
来查看已安装的软件包。 -
我们可以使用
ps
来查看正在运行的进程 -
我们可以简单地通过浏览器访问一个不存在的页面,看看错误页面显示的是哪个 Web 服务器在运行
-
我们还可以进入
/var/logs
并查看周围存在或不存在的日志文件
所有这些方法都是有效的,并且都有自己的好处。在这个例子中,我们将使用一个第五种方法(之前没有提到过),它将回答关于这个服务器上 Web 服务器配置的两个问题。
这种方法的第一步将是确定哪个进程在端口 80 上监听。
$ su -
# netstat -nap | grep 80
tcp6 0 0 :::80 :::* LISTEN 952/httpd
unix 3 [ ] STREAM CONNECTED 17280 1521/master
如第二章中所讨论的,故障排除命令和有用信息的来源,netstat
命令可以用来确定使用-na
标志的端口。如果我们简单地添加-p
(端口)标志到netstat
,我们还可以看到每个端口上监听的进程。
提示
为了确定每个端口上监听的进程,必须以超级用户级别权限执行netstat
命令。因此,我们使用su
命令在执行netstat
之前切换到root用户。
在本书中,任何以$
开头的命令都是作为非特权用户运行的,而以#
开头的命令是作为 root 用户执行的。
端口 80 是 HTTP 请求的默认端口;因此,如果我们回顾一下复制问题的步骤,我们可以看到使用的地址是http://blog.example.com
。由于这是一个 HTTP 地址,没有指定不同的端口,这意味着提供 WordPress 安装页面的服务正在监听 80 端口。
从netstat
命令的输出中,我们可以看到进程 952 正在监听 80 端口。netstat
输出还显示进程 952 正在运行httpd
二进制文件。在 RHEL 系统上,这个httpd
二进制文件往往是 Apache。
我们可以使用ps
命令和第二章, 故障排除命令和有用信息来源中讨论的–elf
标志来验证这一点。我们还将使用grep
命令搜索ps
命令的输出,搜索字符串"952":
$ ps -elf | grep 952
4 S root 952 1 0 80 0 - 115050 poll_s Jan11 ? 00:00:07 /usr/sbin/httpd -DFOREGROUND
5 S apache 5329 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
5 S apache 5330 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
5 S apache 5331 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
5 S apache 5332 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
5 S apache 5333 952 0 80 0 - 119196 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
通过上面的输出,我们可以看到进程 952 及其子进程是在apache用户下运行的。这证实了所使用的软件很可能是 Apache,但为了更加谨慎,我们可以使用httpd
二进制文件和–version
标志来打印 Web 服务器软件的版本。
$ httpd -version
Server version: Apache/2.4.6
Server built: Jul 23 2014 14:48:00
httpd
二进制文件的输出显示,它实际上是 Apache Web 服务器,符合 WordPress 的要求。
到目前为止,我们已经发现了关于此服务器使用的 Web 服务器的以下事实:
-
Web 服务器是 Apache
-
Apache 进程正在运行
-
Apache 版本是 2.4.6
-
Apache 进程正在监听 80 端口
也可以通过其他方法识别相同的信息,比如使用rpm
。这种方法的好处是,如果服务器安装了两个 Web 服务器服务,我们就知道这些服务中的哪一个正在监听 80 端口。这也告诉我们哪个服务提供了 WordPress 安装页面。
验证数据库服务
常见的 WordPress 实现是在一个服务器上同时运行 Apache、PHP 和 MySQL 服务。然而,有时 MySQL 服务会从另一个服务器或多个服务器上运行。为了更好地了解环境,我们应该检查这个环境是在本地运行 MySQL 还是从另一个服务器运行。
为了检查这一点,我们将再次使用ps
命令;不过这一次,我们将使用grep
来搜索与字符串"mysql"匹配的进程:
$ ps -elf | grep mysql
4 S mysql 2045 1 0 80 0 - 28836 wait Jan12 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe --basedir=/usr
0 S mysql 2203 2045 0 80 0 - 226860 poll_s Jan12 ? 00:00:42 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --log- error=/var/log/mariadb/mariadb.log --pid- file=/var/run/mariadb/mariadb.pid -- socket=/var/lib/mysql/mysql.sock
从前面的输出中可以看出,实际上有一个 MySQL 进程正在运行。还需要注意的是,ps
输出显示mysqld
进程正在使用以下选项:–log-error=/var/log/mariadb/mariadb.log
。
这一点很重要,有两个原因:第一,这是mysqld
进程的日志文件的位置,第二,这个日志文件是MariaDB的,与 MySQL 不同。
我们可以通过使用rpm
和egrep
命令来确认是否安装了 MySQL 或 MariaDB。
$ rpm -qa | egrep "(maria|mysql)"
php-mysql-5.4.16-23.el7_0.3.x86_64
mariadb-5.5.40-2.el7_0.x86_64
mariadb-server-5.5.40-2.el7_0.x86_64
mariadb-libs-5.5.40-2.el7_0.x86_64
egrep
命令类似于grep
;但是,它接受正则表达式形式的搜索字符串。在上面的命令中,我们使用egrep
来搜索字符串"mariadb
"或字符串"mysql
"。从前面的输出中,我们可以看到这台服务器实际上安装了 MariaDB,但没有安装 MySQL。
有了这些信息,我们可以假设正在运行的mysqld
进程实际上是一个 MariaDB 二进制文件。我们可以使用rpm
命令和–q
(查询)以及–l
(列出所有文件)标志来验证这一点。
$ rpm -ql mariadb-server | grep "libexec/mysqld"
/usr/libexec/mysqld
我们可以从rpm
命令的输出中看到,运行的/usr/libexec/mysqld
二进制文件是作为mariadb-server软件包的一部分部署的。这表明运行的数据库进程实际上是 MariaDB,并且是通过 mariadb-server 软件包安装的。
到目前为止,我们已经发现了关于此服务器上运行的数据库服务的以下事实:
-
数据库服务实际上是 MariaDB
-
MariaDB 正在运行
-
此服务的日志文件位于
/var/log/mariadb/
虽然 MariaDB 是 MySQL 的可替代品,但 WordPress 的要求中并未将其列为首选数据库服务。重要的是要注意这种差异,因为它可能确定报告的问题的根本原因。
验证 PHP
由于我们知道 WordPress 需要 PHP,我们还应该检查它是否已安装。我们可以再次使用rpm
命令验证这一点。
$ rpm -qa | grep php
php-mbstring-5.4.16-23.el7_0.3.x86_64
php-mysql-5.4.16-23.el7_0.3.x86_64
php-enchant-5.4.16-23.el7_0.3.x86_64
php-process-5.4.16-23.el7_0.3.x86_64
php-xml-5.4.16-23.el7_0.3.x86_64
php-simplepie-1.3.1-4.el7.noarch
php-5.4.16-23.el7_0.3.x86_64
php-gd-5.4.16-23.el7_0.3.x86_64
php-common-5.4.16-23.el7_0.3.x86_64
php-pdo-5.4.16-23.el7_0.3.x86_64
php-PHPMailer-5.2.9-1.el7.noarch
php-cli-5.4.16-23.el7_0.3.x86_64
php-IDNA_Convert-0.8.0-2.el7.noarch
php-getid3-1.9.8-2.el7.noarch
PHP 本身并不是设计为像 Apache 或 MySQL 那样作为服务运行,而是作为 Web 服务器模块。但是,可以使用诸如php-fpm
之类的服务作为应用程序服务器。这允许 PHP 作为服务运行,并由上游 Web 服务器调用。
要检查此服务器是否运行php-fpm
或任何其他用于前端 PHP 的服务,我们可以再次使用ps
和grep
命令。
$ ps -elf | grep php
0 S root 6342 5676 0 80 0 - 28160 pipe_w 17:53 pts/0 00:00:00 grep --color=auto php
通过使用ps
命令,我们没有看到任何特定的 PHP 服务;然而,当访问博客时,我们能够看到安装页面。这表明 PHP 配置为直接通过 Apache 运行。我们可以通过再次执行带有-M
(模块)标志的httpd
二进制文件来验证这一点。
$ httpd -M | grep php
php5_module (shared)
-M
标志将告诉httpd
二进制文件列出所有加载的模块。在此列表中包括php5_module
,这意味着 Apache 的此安装能够通过php5_module
运行 PHP 应用程序。
已安装并正在运行的服务摘要
在这一点上,我们已经从我们的数据收集中确定了以下内容:
-
已安装并运行了 Apache 的 WordPress 要求
-
MariaDB 似乎满足了 WordPress 对 MySQL 的要求,已安装并运行
-
已安装并似乎正在运行 WordPress 的 PHP 要求
-
看起来 WordPress 部署在单服务器设置中,而不是多服务器设置中
我们暂时可以假设这些事实意味着问题不是由缺少 WordPress 要求引起的。
通过收集所有这些数据点,我们不仅了解了我们正在解决故障的环境,还排除了这个问题的几种可能原因。
寻找错误消息
现在已经确定了已安装和配置的服务,我们知道从哪里开始查找错误或有用的消息。在数据收集的下一阶段,我们将浏览这些服务的各种日志文件,以尝试识别可能指示此问题原因的任何错误。
Apache 日志
由于 Apache 在进行 Web 请求时调用 PHP,最有可能包含与 PHP 相关错误的日志文件是 Apache 错误日志。RHEL 的httpd
软件包的默认日志位置是/var/log/httpd/
。但是,我们还不知道运行的httpd
服务是否是 RHEL 打包版本。
查找 Apache 日志的位置
由于我们不知道 Apache 日志的位置,我们需要找到它们。查找日志文件的一种方法是简单地在/var/log
中查找与所讨论服务的名称匹配的任何文件或文件夹。然而,这种解决方案对于我们的例子来说太简单了。
要找到httpd
日志文件的位置,我们将使用第二章中讨论的一种方法,故障排除命令和有用信息来源,并搜索服务的配置文件。/etc
文件夹是系统配置文件的默认文件夹。它也是服务配置的标准位置。因此,可以相当安全地假设/etc/
文件夹将包含httpd
服务的配置文件或文件夹。
# cd /etc/httpd/
# ls -la
total 20
drwxr-xr-x. 5 root root 86 Jan 7 23:29 .
drwxr-xr-x. 79 root root 8192 Jan 13 16:10 ..
drwxr-xr-x. 2 root root 35 Jan 7 23:29 conf
drwxr-xr-x. 2 root root 4096 Jan 7 23:29 conf.d
drwxr-xr-x. 2 root root 4096 Jan 7 23:29 conf.modules.d
lrwxrwxrwx. 1 root root 19 Jan 7 23:29 logs -> ../../var/log/httpd
lrwxrwxrwx. 1 root root 29 Jan 7 23:29 modules -> ../../usr/lib64/httpd/modules
lrwxrwxrwx. 1 root root 10 Jan 7 23:29 run -> /run/httpd
在前面的命令中,我们可以看到我们可以切换到包含多个配置文件的/etc/httpd
文件夹。由于我们不知道哪个配置文件包含日志配置,我们可能需要花费相当长的时间阅读每个配置文件。
为了加快这个过程,我们可以使用grep
命令来搜索所有文件中的字符串“log
”。由于/etc/httpd/
文件夹包含子文件夹,我们可以简单地添加-r
(递归)标志,使grep
命令搜索这些子文件夹中包含的文件。
# grep -r "log" /etc/httpd/*
./conf/httpd.conf:# with "/", the value of ServerRoot is prepended -- so 'log/access_log'
./conf/httpd.conf:# server as '/www/log/access_log', whereas '/log/access_log' will be
./conf/httpd.conf:# interpreted as '/log/access_log'.
./conf/httpd.conf:# container, that host's errors will be logged there and not here.
./conf/httpd.conf:ErrorLog "logs/error_log"
./conf/httpd.conf:# LogLevel: Control the number of messages logged to the error_log.
./conf/httpd.conf:<IfModule log_config_module>
./conf/httpd.conf: <IfModule logio_module>
./conf/httpd.conf: # define per-<VirtualHost> access log files, transactions will be
./conf/httpd.conf: # logged therein and *not* in this file.
./conf/httpd.conf: #CustomLog "logs/access_log" common
./conf/httpd.conf: # If you prefer a log file with access, agent, and referer information
./conf/httpd.conf: CustomLog "logs/access_log" combined
./conf.modules.d/00-base.conf:LoadModule log_config_module modules/mod_log_config.so
./conf.modules.d/00-base.conf:LoadModule logio_module modules/mod_logio.so
./conf.modules.d/00-base.conf:#LoadModule log_debug_module modules/mod_log_debug.so
提示
While there is quite a bit of output from the preceding grep command, if we review the returned data, we can see that there are actually two log files defined for the httpd service: logs/access_log and logs/error_log.
./conf/httpd.conf:ErrorLog "logs/error_log"
./conf/httpd.conf: CustomLog "logs/access_log" combined
定义的日志使用相对路径logs/
;这个路径是相对于运行文件夹的httpd
服务。在这种情况下,这意味着日志文件夹实际上是/etc/httpd/logs
;然而,这并不总是这种情况。要验证是否是这种情况,我们可以简单地在/etc/httpd
文件夹中使用ls
命令进行文件夹列表。
# ls -la /etc/httpd | grep logs
lrwxrwxrwx. 1 root root 19 Jan 7 23:29 logs -> ../../var/log/httpd
从ls
命令中,我们可以看到/etc/httpd/logs
存在;然而,这不是一个文件夹,而是一个符号链接到/var/log/httpd/
。这意味着这两个日志文件,即access_log
和error_log
,实际上位于/var/log/httpd/
文件夹内。
审查日志
现在我们知道日志文件的位置,我们可以搜索这些日志文件以获取任何有用的信息。为此,我们将使用tail
命令。
tail
命令是一个有用的命令,可以用来读取文件的最后部分。默认情况下,当tail
没有任何标志地执行时,该命令将打印指定文件的最后 10 行。
对于我们的故障排除,我们不仅想看到最后 10 行数据,还想观察文件是否有任何新的数据被追加。为此,我们可以使用-f
(跟踪)标志,告诉tail
跟踪指定的文件。
# tail -f logs/access_log logs/error_log
==> logs/access_log <==
192.168.33.1 - - [12/Jan/2015:04:39:08 +0000] "GET /wp-includes/js/wp-util.min.js?ver=4.1 HTTP/1.1" 200 981 "http://blog.example.com/wp-admin/install.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
"http://blog.example.com/wp-admin/install.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
192.168.33.1 - - [12/Jan/2015:04:39:08 +0000] "GET /wp-admin/js/password-strength-meter.min.js?ver=4.1 HTTP/1.1" 200 737 "http://blog.example.com/wp-admin/install.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
::1 - - [13/Jan/2015:16:08:33 +0000] "GET / HTTP/1.1" 302 - "-" "curl/7.29.0"
192.168.33.11 - - [13/Jan/2015:16:10:19 +0000] "GET / HTTP/1.1" 302 - "-" "curl/7.29.0"
==> logs/error_log <==
[Sun Jan 11 06:01:03.679890 2015] [auth_digest:notice] [pid 952] AH01757: generating secret for digest authentication ...
[Sun Jan 11 06:01:03.680719 2015] [lbmethod_heartbeat:notice] [pid 952] AH02282: No slotmem from mod_heartmonitor
[Sun Jan 11 06:01:03.705469 2015] [mpm_prefork:notice] [pid 952] AH00163: Apache/2.4.6 (CentOS) PHP/5.4.16 configured -- resuming normal operations
[Sun Jan 11 06:01:03.705486 2015] [core:notice] [pid 952] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'
提示
RHEL 7 实现的tail
命令实际上可以同时跟踪多个文件。要做到这一点,只需在执行命令时指定您希望读取或跟踪的所有文件。上面是使用tail
同时读取两个文件的示例。
虽然最后 10 行没有立即由 PHP 错误引起的错误,但这并不一定意味着这些文件不会显示我们需要的错误。由于这是一个基于 Web 的应用程序,我们可能需要加载应用程序以触发任何错误。
我们可以简单地打开我们的浏览器,再次导航到http://blog.example.com
。然而,对于这个例子,我们将利用一个非常有用的故障排除命令:curl
。
使用 curl 调用我们的 Web 应用程序
curl
命令可以用作客户端来访问许多不同类型的协议,从 FTP 到 SMTP。这个命令在故障排除 Web 应用程序时特别有用,因为它可以用作 HTTP 客户端。
在故障排除 Web 应用程序时,您可以使用curl
命令向指定的 URL 发出HTTP
,GET
或POST
请求,当以-v
(详细)标志的详细模式放置时,可以产生相当多的有趣信息。
$ curl -v http://blog.example.com
* About to connect() to blog.example.com port 80 (#0)
* Trying 192.168.33.11...
* Connected to blog.example.com (192.168.33.11) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: blog.example.com
> Accept: */*
>
< HTTP/1.1 302 Found
< Date: Tue, 13 Jan 2015 21:10:51 GMT
< Server: Apache/2.4.6 PHP/5.4.16
< X-Powered-By: PHP/5.4.16
< Expires: Wed, 11 Jan 1984 05:00:00 GMT
< Cache-Control: no-cache, must-revalidate, max-age=0
< Pragma: no-cache
< Location: http://blog.example.com/wp-admin/install.php
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host blog.example.com left intact
前面的输出显示了我想要强调的四个关键信息。
* Connected to blog.example.com (192.168.33.11) port 80 (#0)
前一行显示了当我们访问名为blog.example.com
的页面时,我们实际上去了192.168.33.11
服务器。虽然我们已经确定blog.example.com
解析为192.168.33.11
,但这行确认了这个命令的输出产生了来自预期系统的数据。
< HTTP/1.1 302 Found
第二个关键信息显示了 Web 服务器提供的 HTTP 状态代码。
在这种情况下,Web 服务器以302
状态代码回复,用于指示临时重定向。当浏览器请求页面并且 Web 服务器以 302 状态代码回复时,浏览器知道将最终用户重定向到另一个页面。
< Location: http://blog.example.com/wp-admin/install.php
下一个页面由Location HTTP 标头确定。这个标头由 Web 服务器分配,以及 302 的 HTTP 状态代码将导致任何浏览器将最终用户重定向到/wp-admin/install.php
页面。
这解释了为什么当我们导航到blog.example.com
时会看到一个安装页面,因为 Web 服务器只是以 302 重定向简单地响应。
< X-Powered-By: PHP/5.4.16
第四个关键信息是 HTTP 头X-Powered-By;这是 PHP 添加的 HTTP 头。当请求的页面被 PHP 处理时,PHP 会添加这个头,这意味着我们的 curl 请求实际上是由 PHP 处理的。
更重要的是,我们可以看到 PHP 的版本(5.4.16)符合 WordPress 规定的最低要求。
请求非 PHP 页面
当请求一个非 PHP 页面时,我们可以看到 Web 服务器的回复中没有添加X-Powered-By头。我们可以通过请求一个无效的 URL 来验证这一点。
# curl -v http://192.168.33.11/sdfas
* About to connect() to 192.168.33.11 port 80 (#0)
* Trying 192.168.33.11...
* Connected to 192.168.33.11 (192.168.33.11) port 80 (#0)
> GET /sdfas HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 192.168.33.11
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Tue, 13 Jan 2015 21:18:57 GMT
< Server: Apache/2.4.6 PHP/5.4.16
< Content-Length: 203
< Content-Type: text/html; charset=iso-8859-1
当我们请求一个非 PHP 页面时,我们可以看到得到的输出中没有 X-Powered-By 头。这表明 Web 服务器没有将此页面处理为 PHP。
X-Powered-By 头的存在告诉我们,当我们请求blog.example.com
页面时,它是由 PHP 处理的。这也意味着 302 的 HTTP 状态码是 WordPress 提供的响应。这一信息很重要,因为它意味着 PHP 很可能在没有任何问题的情况下处理页面,至少目前来看,排除了 PHP 作为报告问题的可能根本原因。
我们可以通过查看从上述 Web 请求生成的任何日志条目来进一步验证这一点。
审查生成的日志条目
当使用curl
进行上述请求时,我们应该已经导致新的日志消息被追加到两个httpd
日志中。由于我们使用tail
命令持续跟踪日志文件,我们可以返回到我们的终端并查看新的消息。
==> logs/access_log <==
192.168.33.11 - - [13/Jan/2015:23:22:17 +0000] "GET / HTTP/1.1" 302 - "-" "curl/7.29.0"
在我们对博客 URL 的 HTTP 请求之后,两个日志中唯一的条目是前面的一个。然而,这只是一个信息日志消息,而不是一个可以解释问题的错误。然而,信息日志消息也是一个关键的数据点。如果 PHP 代码或处理出现问题,类似以下的错误消息将会生成。
[Tue Jan 13 23:24:31.339293 2015] [:error] [pid 5333] [client 192.168.33.11:52102] PHP Parse error: syntax error, unexpected 'endif' (T_ENDIF) in /var/www/html/wp-includes/functions.php on line 2574
PHP 错误的缺失实际上证实了 PHP 正在按预期工作。这与curl
的结果结合起来,让我们有信心地假设 PHP 不是根本原因。
我们从 httpd 日志中学到了什么
虽然httpd
服务日志可能没有显示出可以解释为什么出现这个问题的错误,但它们已经帮助我们排除了一个可能的原因。在故障排除过程中,你经常会发现自己在找到问题的确切原因之前排除了许多可能的原因。前面提到的故障排除步骤就是这样,因此排除了可能的原因。
验证数据库
早些时候在检查哪些服务正在运行时,我们发现 MariaDB 服务正在运行。然而,我们没有验证我们是否可以访问该服务,或者 WordPress 应用程序是否可以访问这个数据库服务。
为了验证我们是否可以访问 MariaDB 服务,我们可以简单地使用mysql
命令。
# mysql
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 28
Server version: 5.5.40-MariaDB MariaDB Server
Copyright (c) 2000, 2014, Oracle, Monty Program Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
mysql
命令实际上是一个 MariaDB 客户端命令。当以root用户的身份从命令行运行(如上所示)时,默认情况下,mysql
命令将作为 MariaDB 的 root 用户登录到 MariaDB 服务。虽然这是默认行为,但是可以配置 MariaDB 服务禁止直接的根登录。
上述结果暗示了 MariaDB 允许直接的根登录,这表明 MariaDB 服务本身正在运行并接受连接。它们没有透露的是 WordPress 应用程序是否能够访问数据库。
为了确定这一点,我们需要使用与应用程序相同的用户名和密码登录到 MariaDB 服务。
验证 WordPress 数据库
为了使用与 WordPress 相同的凭据连接到 MariaDB 服务,我们需要获取这些凭据。我们可以向报告问题的人请求这些详细信息,但作为业务用户,他们很可能不知道。即使他们每天都在使用 WordPress,通常数据库用户名和密码是由一个人配置并且只在安装过程中使用。
这意味着我们必须自己找到这些信息。一种方法是查看 WordPress 的配置,因为每个连接到数据库的 Web 应用程序都必须从某个地方获取登录凭据,而最常见的方法是将它们存储在配置文件中。
这种方法的一个有趣挑战是,本章假设我们对 WordPress 知之甚少。找到 WordPress 存储其数据库凭据的位置将会有些棘手;特别是因为我们也不知道 WordPress 应用程序安装在哪里。
查找 WordPress 的安装路径
我们知道的是,WordPress 是一个由httpd
服务提供的 Web 应用程序。这意味着httpd
服务将在其配置文件的某个地方定义安装路径。
httpd
的默认配置是从默认文件夹中为单个域提供服务。默认文件夹可能会因发行版而有所不同,但一般来说,对于 RHEL 系统,它位于/var/www/html
下。
可以配置httpd
来为多个域提供服务;这是通过虚拟主机配置完成的。此时,我们不知道这个系统是配置为托管多个域还是单个域。
检查默认配置
在默认的单域配置中,指向服务器 IP 的任何和所有域都将提供相同的.html
或.php
文件。通过虚拟主机,您可以配置 Apache 根据请求所涉及的域来提供特定的.html
或.php
文件。
我们可以通过执行简单的grep
命令来确定httpd
服务是配置为虚拟主机还是单域。
# grep -r "DocumentRoot" /etc/httpd/
/etc/httpd/conf/httpd.conf:# DocumentRoot: The folder out of which you will serve your
/etc/httpd/conf/httpd.conf:DocumentRoot "/var/www/html"
/etc/httpd/conf/httpd.conf: # access content that does not live under the DocumentRoot.
由于/etc/httpd
文件夹有多个子文件夹,我们再次使用了-r
(递归)标志来进行grep
。该命令在整个/etc/httpd
文件夹结构中搜索DocumentRoot字符串。
DocumentRoot 是 Apache 配置项,指定包含指定域的.html
或.php
文件的本地文件夹。对于配置为多个域的系统,DocumentRoot
设置将出现多次,而对于单域配置,只会出现一次。
从上面的输出中,我们可以看到在这台服务器上,DocumentRoot
只定义了一次,并设置为/var/www/html
。由于这是 RHEL 系统的默认设置,可以相当安全地假设httpd
服务配置为单域配置。
为了验证这是否是 WordPress 的安装文件夹,我们可以简单地执行ls
命令来列出此路径中的文件和文件夹。
# ls -la /var/www/html/
total 156
drwxr-xr-x. 5 root root 4096 Jan 9 22:54 .
drwxr-xr-x. 4 root root 31 Jan 7 23:29 ..
-rw-r--r--. 1 root root 418 Jan 9 21:48 index.php
-rw-r--r--. 1 root root 4951 Jan 9 21:48 wp-activate.php
drwxr-xr-x. 9 root root 4096 Jan 9 21:48 wp-admin
-rw-r--r--. 1 root root 271 Jan 9 21:48 wp-blog-header.php
-rw-r--r--. 1 root root 5008 Jan 9 21:48 wp-comments-post.php
-rw-r--r--. 1 root root 3159 Jan 9 22:01 wp-config.php
-rw-r--r--. 1 root root 2726 Jan 9 21:48 wp-config-sample.php
drwxr-xr-x. 6 root root 77 Jan 9 21:48 wp-content
-rw-r--r--. 1 root root 2956 Jan 9 21:48 wp-cron.php
drwxr-xr-x. 10 root root 4096 Jan 13 23:25 wp-includes
-rw-r--r--. 1 root root 2380 Jan 9 21:48 wp-links-opml.php
-rw-r--r--. 1 root root 2714 Jan 9 21:48 wp-load.php
-rw-r--r--. 1 root root 33435 Jan 9 21:48 wp-login.php
-rw-r--r--. 1 root root 8252 Jan 9 21:48 wp-mail.php
-rw-r--r--. 1 root root 11115 Jan 9 21:48 wp-settings.php
-rw-r--r--. 1 root root 25152 Jan 9 21:48 wp-signup.php
-rw-r--r--. 1 root root 4035 Jan 9 21:48 wp-trackback.php
-rw-r--r--. 1 root root 3032 Jan 9 21:48 xmlrpc.php
从ls
命令的输出中,我们可以看到 WordPress 实际上是安装在/var/www/html/
中的。我们可以根据大量的.php
文件以及这些文件的"wp-
"命名方案来得出这个结论。然而,下一步将对此进行确认。
查找数据库凭据
现在我们已经确定了安装文件夹,我们只需要在 WordPress 应用程序的配置文件中找到数据库凭据。不幸的是,我们对 WordPress 并不是很熟悉,也不知道这些文件中哪些包含数据库凭据。
那么,我们要如何找到它们呢?当然是通过谷歌搜索。
正如我们在第一章中所介绍的,故障排除最佳实践,Google 可以成为系统管理员的好朋友。由于 WordPress 是一个常见的开源应用程序,很可能会有在线帮助文档,涵盖了如何配置或至少恢复数据库密码的内容。
要开始,我们只需通过 Google 搜索WordPress 数据库配置。在搜索 Google 时,我们发现其中一个最初的结果链接到 WordPress 论坛,一个用户询问在 WordPress 中如何找到数据库详细信息。
第一个答案是查看wp-config.php
文件。
提示
虽然对于流行的开源项目来说,通过谷歌搜索这种类型的信息很容易,但对于闭源应用程序来说也同样有效,因为很多时候即使闭源应用程序也有在线文档,并被谷歌索引。
要获取数据库详细信息,我们可以使用less
命令读取wp-config.php
文件。less
命令是一个简单的命令,允许用户通过命令行读取文件。对于大文件来说,这特别有用,因为它会缓冲输出,而不是像cat
命令一样简单地将所有内容倒出到屏幕上。
# less /var/www/html/wp-config.php
// ** MySQL settings - You can get this information from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'wordpress');
/** MySQL database username */
define('DB_USER', 'wordpress');
/** MySQL database password */
define('DB_PASSWORD', 'password');
/** MySQL hostname */
define('DB_HOST', 'localhost');
通过阅读配置文件,我们可以清楚地看到数据库凭据,这些凭据方便地位于文件的顶部。以下是我们可以从该文件中提取的详细信息列表:
- WordPress 正在尝试使用的数据库的
NAME
(wordpress
)
define('DB_NAME', 'wordpress');
- WordPress 正在尝试连接的
HOST
(localhost
)
define('DB_HOST', 'localhost');
- WordPress 正在尝试进行身份验证的
USER
(wordpress
)数据库
define('DB_USER', 'wordpress');
PASSWORD
(password
)它用于身份验证
define('DB_PASSWORD', 'password');
有了上述详细信息,我们可以以与 WordPress 应用程序相同的方式连接到 MariaDB 服务。这将是我们故障排除过程中的关键步骤。
以 WordPress 用户身份连接
现在我们有了数据库凭据,我们可以使用mysql
命令再次测试连接性。要使用特定的用户名和密码连接到 MariaDB,我们需要使用mysql
命令的-u
(用户)和-p
(密码)标志。
# mysql –uwordpress -p
Enter password: Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 30
Server version: 5.5.40-MariaDB MariaDB Server
MariaDB [(none)]>
在上述命令中,我们可以看到我们在-u
标志后添加了用户名,但没有在-p
后包含密码。由于我们没有包含密码,mysql
客户端在我们按回车键后简单地要求输入密码。虽然可以在-p
后包含密码,但从安全的角度来看,这被认为是一种不好的做法。最好让mysql
客户端要求输入密码,以减少密码被查看命令历史的人泄露的机会。
从mysql
客户端连接,我们可以看到通过使用与 WordPress 相同的凭据,我们能够登录到 MariaDB 服务。这很重要,因为无法连接到数据库服务将影响 WordPress 应用程序,并可能是报告的问题的可能原因。
验证数据库结构
由于我们可以使用 WordPress 凭据连接到 MariaDB 服务,接下来我们应该验证数据库结构是否存在且完整。
提示
在本节中,我们将从 MariaDB 命令行界面执行结构化查询语言(SQL)语句。这些语句不是 shell 命令,而是 SQL 查询。
SQL 是与 MySQL、MariaDB、Postgres 和 Oracle 等关系数据库交互的标准语言。虽然 SQL 不一定是每个管理员都需要了解的语言,但我建议任何支持大量数据库的系统管理员至少应该了解 SQL 的基础知识。
如果您支持的环境没有专门的数据库管理员来管理数据库和数据库服务,这一点尤其重要。
要验证的第一项是数据库本身是否已创建并可访问。我们可以通过使用show databases
查询来做到这一点。
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| test |
| wordpress |
+--------------------+
3 rows in set (0.00 sec)
我们可以看到 WordPress 数据库实际上在这个输出中列出,这意味着它是存在的。为了验证 WordPress 数据库是可访问的,我们将使用use
SQL 语句。
MariaDB [(none)]> use wordpress;
Database changed
通过Database changed
的结果,似乎我们已经确认了数据库本身是创建并且可访问的。那么,这个数据库中的表呢?我们可以通过使用show tables
查询来验证数据库表是否已经创建。
MariaDB [wordpress]> show tables;
+-----------------------+
| Tables_in_wordpress |
+-----------------------+
| wp_commentmeta |
| wp_comments |
| wp_links |
| wp_options |
| wp_postmeta |
| wp_posts |
| wp_term_relationships |
| wp_term_taxonomy |
| wp_terms |
| wp_usermeta |
| wp_users |
+-----------------------+
11 rows in set (0.00 sec)
从结果来看,似乎存在相当多的表。
由于我们对 WordPress 还很陌生,有可能我们会缺少一个表,甚至不知道。由于 WordPress 在网上有很多文档,我们很可能会通过搜索WordPress 数据库表列表来找到一个表列表,这返回了 WordPress 文档页面上非常有用的数据库描述:(codex.wordpress.org/Database_Description
)
通过比较show tables
查询的输出和数据库描述页面,我们发现没有缺少表。
由于我们知道哪些表是存在的,我们应该检查这些表是否可访问;我们可以通过运行select
查询来做到这一点。
MariaDB [wordpress]> select count(*) from wp_users;
ERROR 1017 (HY000): Can't find file: './wordpress/wp_users.frm' (errno: 13)
终于,我们找到了一个错误!
然而,我们发现的错误非常有趣,因为它不是您通常从 SQL 查询中看到的错误。事实上,这个错误似乎表明存在一个包含表数据的文件的问题。
从数据库验证中我们学到了什么
在这一点上,经过验证数据库之后,我们学到了以下内容:
-
MariaDB 可以被 root 用户和 WordPress 应用程序访问
-
正在访问的数据库是由 WordPress 用户创建并可访问的
-
在查询数据库表中的一个时,会显示一个错误
有了这些信息,我们可以通过建立一个假设来进入故障排除过程的下一步。
建立假设
在故障排除过程的这个阶段,我们将收集到的所有信息,并用它来建立一个关于为什么出现问题以及如何解决问题的想法。
首先,让我们首先回顾一下我们从数据收集步骤中学到的东西。
-
一个已经建立的博客网站目前显示的是一个设计为仅在博客软件的初始安装期间显示的页面
-
这个博客使用开源软件 WordPress
-
WordPress 是用 PHP 编写的,并且利用了 Apache 和 MariaDB 服务
-
Apache 和 PHP 都正常工作,没有显示错误
-
WordPress 安装在
/var/www/html
目录下 -
MariaDB 服务正在运行并接受连接
-
WordPress 应用程序能够连接到数据库服务
-
当从数据库表中读取数据时,我们收到一个错误,表明包含数据库数据的文件存在问题
我们可以从所有这些数据点中得出的假设是:
在某个时候,MariaDB 的数据文件,特别是 WordPress 数据库,对 MariaDB 服务是不可访问的。当 WordPress 连接到数据库时,它似乎无法查询表;因此,它认为应用程序尚未安装。由于 WordPress 认为应用程序尚未安装,它呈现了一个安装页面。
我们可以根据以下关键信息点来制定这个假设:
-
我们看到的唯一错误是来自 MariaDB 的错误。
-
这个错误不是典型的 SQL 错误,消息本身表明存在访问数据库文件的问题。
-
在 Apache 日志中没有 PHP 错误。
-
关于 WordPress 环境的其他一切似乎都是正确的。
现在我们已经形成了一个假设,我们需要通过尝试解决问题来验证这一点。这将引导我们到故障排除过程的第三阶段:试错。
解决问题
在这个阶段,我们将尝试解决这个问题。为了做到这一点,让我们看看这些数据文件是什么,它们用于什么。
理解数据库数据文件
除了仅内存数据库之外,大多数数据库都有一种用于在文件系统上存储数据的文件;这通常被称为持久存储。MariaDB 和 MySQL 也不例外。
根据使用的数据库存储引擎,可能会有一个大文件或多个具有不同文件扩展名的文件。无论文件类型或文件存储在何处/如何存储,最终,如果这些文件无法访问,数据库将出现问题。
查找 MariaDB 数据文件
由于我们对这个环境还很陌生,我们目前不知道 MariaDB 数据文件存储在哪里。确定这些文件的位置将是纠正问题的第一步。识别数据文件夹的一种方法是查看数据库服务的配置文件。
由于/etc
文件夹是大多数(但不是所有)配置文件的所在地,这是我们应该首先查找的地方。
# ls -la /etc/ | grep -i maria
为了确定正确的配置文件,我们可以使用ls
命令列出/etc
文件夹,并使用grep
命令在结果中搜索包含maria
字符串的内容。上述的grep
命令使用了-i
(不区分大小写)标志,这会导致grep
搜索大写和小写字符串。如果文件夹或文件具有混合大小写名称,这可能会有所帮助。
由于我们的命令没有输出,所以在名称中没有包含maria
的文件夹或文件。这意味着 MariaDB 服务的配置要么命名为我们不期望的名称,要么不在/etc/
文件夹中。
由于 MariaDB 应该是 MySQL 的一个可替换的解决方案,我们还应该检查是否有一个名为mysql
的文件夹或文件。
# ls -la /etc/ | grep –i mysql
看起来也没有与这个名称匹配的文件夹或文件。
我们可以使用ls
命令来花费几个小时来寻找 MariaDB 的配置文件。幸运的是,有一种更快的方法来找到配置文件。
由于 MariaDB 是通过 RPM 软件包安装的,我们可以使用rpm
命令列出软件包部署的所有文件和文件夹。早些时候在检查 MariaDB 的安装方式时,rpm
命令显示了多个 MariaDB 软件包。我们感兴趣的软件包是mariadb-server
软件包。该软件包安装了 MariaDB 服务以及默认配置文件。
早些时候,我们使用了rpm
的-q
和-l
标志列出了该软件包部署的所有文件。如果我们只想限制查询到配置文件,我们可以使用-q
和-c
标志。
$ rpm -qc mariadb-server /etc/logrotate.d/mariadb
/etc/my.cnf.d/server.cnf
/var/log/mariadb/mariadb.log
从上面可以看出,mariadb-server
软件包部署了三个配置文件。mariadb.log
和logrotate.d
文件不太可能包含我们正在寻找的信息,因为它们与日志记录过程有关。
这留下了/etc/my.cnf.d/server.cnf
文件。我们可以使用cat
命令读取这个文件。
# cat /etc/my.cnf.d/server.cnf
#
# These groups are read by the MariaDB server.
# Use it for options that only the server (but not clients) should see
#
# See the examples of server my.cnf files in /usr/share/mysql/
#
# this is read by the standalone daemon and embedded servers
[server]
# this is only for the mysqld standalone daemon
[mysqld]
# this is only for embedded server
[embedded]
# This group is only read by MariaDB-5.5 servers.
# If you use the same .cnf file for MariaDB of different versions,
# use this group for options that older servers don't understand
[mysqld-5.5]
# These two groups are only read by MariaDB servers, not by MySQL.
# If you use the same .cnf file for MySQL and MariaDB,
# you can put MariaDB-only options here
[mariadb]
[mariadb-5.5]
不幸的是,这个文件也不包含我们希望的数据文件夹的详细信息。然而,这个文件确实给了我们一个线索,告诉我们接下来应该去哪里找。
server.conf
文件的父文件夹是/etc/my.cnf.d
文件夹。文件夹名称末尾的.d
很重要,因为这种命名约定在 Linux 中有特殊用途。.d
(点 D)文件夹类型旨在允许用户简单地为服务添加一个文件或多个文件,以进行自定义配置。当服务启动时,该文件夹中的所有文件都会被读取,并应用配置。
这允许用户配置服务而无需编辑默认配置文件;他们可以通过在.d
文件夹中创建一个新文件来简单地添加他们想要添加的配置。
重要的是要注意,这是一种配置方案,并非每个服务都支持这种方案。然而,似乎 MariaDB 服务确实支持这种方案。
然而,有趣的是,这个.d
文件夹的名称。通常,.d
配置文件夹的命名约定是服务名称或文件夹用途后跟.d
。您可以在/etc/cron.d
或/etc/http/conf.d
文件夹中看到这一点。MariaDB 的.d
文件夹的名称表明主配置文件可能被命名为my.cnf
。
如果我们检查这样的文件是否存在,我们会发现确实存在。
# ls -la /etc/ | grep my.cnf
-rw-r--r--. 1 root root 570 Nov 17 12:28 my.cnf
drwxr-xr-x. 2 root root 64 Jan 9 18:20 my.cnf.d
这个文件似乎是主 MariaDB 配置文件,希望其中包含数据文件夹配置。要找出,我们可以使用cat
命令读取这个文件。
# cat /etc/my.cnf
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
# Settings user and group are ignored when systemd is used.
# If you need to run mysqld under a different user or group,
# customize your systemd unit file for mariadb according to the
# instructions in http://fedoraproject.org/wiki/Systemd
[mysqld_safe]
log-error=/var/log/mariadb/mariadb.log
pid-file=/var/run/mariadb/mariadb.pid
#
# include all files from the config folder
#
!includedir /etc/my.cnf.d
正如预期的那样,这个文件实际上包含了数据文件夹的配置。
datadir=/var/lib/mysql
有了这些信息,我们现在可以对 WordPress 数据库的数据文件的当前状态进行故障排除。
解决数据文件问题
如果我们切换到/var/lib/mysql
文件夹并使用ls
命令列出文件夹内容,我们会看到相当多的数据库数据文件/文件夹。
# cd /var/lib/mysql/
# ls -la
total 28712
drwxr-xr-x. 6 mysql mysql 4096 Jan 15 00:20 .
drwxr-xr-x. 29 root root 4096 Jan 15 05:40 ..
-rw-rw----. 1 mysql mysql 16384 Jan 15 00:20 aria_log.00000001
-rw-rw----. 1 mysql mysql 52 Jan 15 00:20 aria_log_control
-rw-rw----. 1 mysql mysql 18874368 Jan 15 00:20 ibdata1
-rw-rw----. 1 mysql mysql 5242880 Jan 15 00:20 ib_logfile0
-rw-rw----. 1 mysql mysql 5242880 Jan 9 21:39 ib_logfile1
drwx------. 2 mysql mysql 4096 Jan 9 21:39 mysql
srwxrwxrwx. 1 mysql mysql 0 Jan 15 00:20 mysql.sock
drwx------. 2 mysql mysql 4096 Jan 9 21:39 performance_schema
drwx------. 2 mysql mysql 6 Jan 9 21:39 test
drwx------. 2 mysql mysql 4096 Jan 9 22:55 wordpress
看起来,这台服务器上创建的每个数据库都存在于/var/lib/mysql/
下的一个文件夹中。从ls
输出中还可以看出,这些文件夹处于正常状态。由于问题出在 WordPress 数据库上,我们将专注于这个数据库,切换到wordpress
文件夹。
# cd wordpress/
# ls -la
total 156
drwx------. 2 mysql mysql 4096 Jan 9 22:55 .
drwxr-xr-x. 6 mysql mysql 4096 Jan 15 00:20 ..
-rw-rw----. 1 mysql mysql 65 Jan 9 21:45 db.opt
----------. 1 root root 8688 Jan 9 22:55 wp_commentmeta.frm
----------. 1 root root 13380 Jan 9 22:55 wp_comments.frm
----------. 1 root root 13176 Jan 9 22:55 wp_links.frm
----------. 1 root root 8698 Jan 9 22:55 wp_options.frm
----------. 1 root root 8682 Jan 9 22:55 wp_postmeta.frm
----------. 1 root root 13684 Jan 9 22:55 wp_posts.frm
----------. 1 root root 8666 Jan 9 22:55 wp_term_relationships.frm
----------. 1 root root 8668 Jan 9 22:55 wp_terms.frm
----------. 1 root root 8768 Jan 9 22:55 wp_term_taxonomy.frm
----------. 1 root root 8684 Jan 9 22:55 wp_usermeta.frm
----------. 1 root root 8968 Jan 9 22:55 wp_users.frm
在执行ls
命令后,我们可以看到这个文件夹中的文件有些不寻常。
突出的问题只是所有的.frm
文件都具有000
的文件模式。这意味着所有者、组或其他 Linux 用户都无法读取或写入这些文件。这包括 MariaDB 运行的用户。
如果我们回顾一下从 MariaDB 收到的错误,我们会发现错误似乎支持这样的假设,即无效的权限实际上导致了问题。要纠正这个错误,我们只需要将权限重置为正确的值。
由于我们对 MariaDB 还很陌生,目前我们不知道这些值应该是什么。
幸运的是,有一种简单的方法可以弄清楚权限应该是什么:只需查看另一个数据库的文件权限。
如果我们回顾一下/var/lib/mysql
文件夹的列表输出,我们会发现有几个文件夹。其中至少一个其他文件夹也应该是数据库的数据文件夹。要确定我们的.frm
文件应该具有什么权限,我们只需要找到其他.frm
文件。
# find /var/lib/mysql -name "*.frm" -ls
134481927 12 -rw-rw---- 1 mysql mysql 9582 Jan 9 21:39 /var/lib/mysql/mysql/db.frm
134481930 12 -rw-rw---- 1 mysql mysql 9510 Jan 9 21:39 /var/lib/mysql/mysql/host.frm
134481933 12 -rw-rw---- 1 mysql mysql 10630 Jan 9 21:39 /var/lib/mysql/mysql/user.frm
134481936 12 -rw-rw---- 1 mysql mysql 8665 Jan 9 21:39 /var/lib/mysql/mysql/func.frm
134481939 12 -rw-rw---- 1 mysql mysql 8586 Jan 9 21:39 /var/lib/mysql/mysql/plugin.frm
134481942 12 -rw-rw---- 1 mysql mysql 8838 Jan 9 21:39 /var/lib/mysql/mysql/servers.frm
134481945 12 -rw-rw---- 1 mysql mysql 8955 Jan 9 21:39 /var/lib/mysql/mysql/tables_priv.frm
134481948 12 -rw-rw---- 1 mysql mysql 8820 Jan 9 21:39 /var/lib/mysql/mysql/columns_priv.frm
134481951 12 -rw-rw---- 1 mysql mysql 8770 Jan 9 21:39 /var/lib/mysql/mysql/help_topic.frm
134309941 12 -rw-rw---- 1 mysql mysql 8700 Jan 9 21:39 /var/lib/mysql/mysql/help_category.frm
find
命令是一个非常有用的故障排除命令,可以在许多不同的情况下使用。在我们的示例中,我们使用find
命令通过–name
标志搜索/var/lib/mysql
文件夹中以“.frm
”结尾的任何文件。–ls
(文件夹列表)标志告诉find
命令以长列表格式打印它找到的任何文件,这将显示每个文件的权限,而无需运行第二个命令。
从find
命令的输出中,我们可以看到.frm
文件的权限设置为-rw-rw----
;这个的数字表示是660
。这些权限似乎适用于我们的数据库表,并允许所有者和组读写这些文件。
为了重置 WordPress 数据文件的权限,我们将使用chmod
命令。
# chmod -v 660 /var/lib/mysql/wordpress/*.frm
mode of '/var/lib/mysql/wordpress/wp_commentmeta.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_comments.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_links.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_options.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_postmeta.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_posts.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_term_relationships.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_terms.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_term_taxonomy.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_usermeta.frm' changed from 0000 (---------) to 0660 (rw-rw----)
mode of '/var/lib/mysql/wordpress/wp_users.frm' changed from 0000 (---------) to 0660 (rw-rw----)
在前面的命令中,使用了–v
(详细)标志与chmod
,以便我们可以看到每个文件的权限在命令执行时的变化。
验证
现在权限已经设置好,我们可以再次使用 SQLselect
查询进行验证。
MariaDB [wordpress]> select count(*) from wp_users;
ERROR 1017 (HY000): Can't find file: './wordpress/wp_users.frm' (errno: 13)
从上面的查询中,我们可以看到 MariaDB 访问这些文件仍然存在错误。这意味着我们可能没有纠正所有数据文件的问题。
# ls -la
total 156
drwx------. 2 mysql mysql 4096 Jan 9 22:55 .
drwxr-xr-x. 6 mysql mysql 4096 Jan 15 00:20 ..
-rw-rw----. 1 mysql mysql 65 Jan 9 21:45 db.opt
-rw-rw----. 1 root root 8688 Jan 9 22:55 wp_commentmeta.frm
-rw-rw----. 1 root root 13380 Jan 9 22:55 wp_comments.frm
-rw-rw----. 1 root root 13176 Jan 9 22:55 wp_links.frm
-rw-rw----. 1 root root 8698 Jan 9 22:55 wp_options.frm
-rw-rw----. 1 root root 8682 Jan 9 22:55 wp_postmeta.frm
-rw-rw----. 1 root root 13684 Jan 9 22:55 wp_posts.frm
-rw-rw----. 1 root root 8666 Jan 9 22:55 wp_term_relationships.frm
-rw-rw----. 1 root root 8668 Jan 9 22:55 wp_terms.frm
-rw-rw----. 1 root root 8768 Jan 9 22:55 wp_term_taxonomy.frm
-rw-rw----. 1 root root 8684 Jan 9 22:55 wp_usermeta.frm
-rw-rw----. 1 root root 8968 Jan 9 22:55 wp_users.frm
通过查看ls
命令的输出,我们可以看到与示例.frm
文件的另一个不同之处。
134481927 12 -rw-rw---- 1 mysql mysql 9582 Jan 9 21:39 /var/lib/mysql/mysql/db.frm
wordpress
文件夹中文件的所有者和组权限设置为root
,而其他.frm
文件的所有者和组为mysql
用户。
660
的权限意味着只有文件的所有者和组成员可以访问它。对于我们的 WordPress 文件,这意味着只有 root 用户和 root 组的任何成员可以访问这些文件。
由于 MariaDB 以mysql
用户运行,MariaDB 服务仍然无法访问这些文件。我们可以使用chown
命令重置所有权和组成员。
# chown -v mysql.mysql ./*.frm
changed ownership of './wp_commentmeta.frm' from root:root to mysql:mysql
changed ownership of './wp_comments.frm' from root:root to mysql:mysql
changed ownership of './wp_links.frm' from root:root to mysql:mysql
changed ownership of './wp_options.frm' from root:root to mysql:mysql
changed ownership of './wp_postmeta.frm' from root:root to mysql:mysql
changed ownership of './wp_posts.frm' from root:root to mysql:mysql
changed ownership of './wp_term_relationships.frm' from root:root to mysql:mysql
changed ownership of './wp_terms.frm' from root:root to mysql:mysql
changed ownership of './wp_term_taxonomy.frm' from root:root to mysql:mysql
changed ownership of './wp_usermeta.frm' from root:root to mysql:mysql
changed ownership of './wp_users.frm' from root:root to mysql:mysql
现在文件的所有权和组成员是mysql
,我们可以重新运行我们的查询,看看问题是否已解决。
MariaDB [wordpress]> select count(*) from wp_users;
count(*)
1
最终,我们通过查询 WordPress 数据库表解决了错误。
最终验证
由于我们已解决了数据库错误,并且在故障排除过程中没有发现其他错误,下一个验证步骤是查看博客是否仍然显示安装屏幕。
通过从浏览器访问http://blog.example.com
,我们现在可以看到我们不再收到安装页面,而是博客的首页。此时,问题似乎已经解决了。
一般来说,当处理由某人报告的问题时,最佳做法是让最初报告问题的人验证一切是否已恢复到预期状态。我见过许多情况,其中一个事件是由多个问题引起的,虽然更明显的问题很快得到解决,但其他问题往往被忽视。让用户验证我们已经解决了整个问题将有助于确保一切都已真正解决。
对于这种情况,当我们询问报告问题的业务用户是否问题已解决时,他/她回答说一切看起来都修复了。谢谢!
摘要
在本章中,我们通过使用在现实世界中可能经常发生的问题来走过了故障排除过程。我们详细介绍了故障排除过程的步骤 1、2 和 3,以收集数据、建立假设和解决问题;这些步骤在第一章故障排除最佳实践中有详细介绍。我们还使用了在第二章故障排除命令和有用信息来源中学到的几个命令和日志文件,以及一些新的命令。
虽然学习本章中使用的命令对于任何与 Web 应用程序一起工作的系统管理员来说都很重要,但更重要的是看我们遵循的过程。我们开始处理问题时对环境或应用程序没有先验知识,但通过一些基本的数据收集和反复试验,我们可以解决问题。
在下一章中,我们将使用相同的故障排除过程和类似的工具来解决性能问题。
第四章:故障排除性能问题
在第三章中,我们通过使用第一章中介绍的故障排除方法论,以及第二章中找到的几个基本故障排除命令和资源,来解决了 Web 应用程序的问题。
性能问题
在本章中,我们将继续在第三章中涵盖的情景,我们是一家新公司的新系统管理员。当我们到达开始我们的一天时,一位同事要求我们调查一个服务器变慢的问题。
在要求详细信息时,我们的同事只能提供主机名和被认为“慢”的服务器的 IP。我们的同行提到一个用户报告了这个问题,而用户并没有提供太多细节。
在这种情况下,与第三章中讨论的情况不同,我们没有太多信息可以开始。似乎我们也无法向用户提出故障排除问题。作为系统管理员,需要用很少的信息来排除问题并不罕见。事实上,这种类型的情况非常普遍。
它很慢
“它很慢”很难排除故障。关于服务器或服务变慢的投诉最大的问题是,“慢”是相对于遇到问题的用户而言的。
在处理任何关于性能的投诉时,重要的区别是环境设计的基准。在某些环境中,系统以 30%的 CPU 利用率运行可能是一种常规活动,而其他环境可能会保持系统以 10%的 CPU 利用率运行,30%的利用率会表示问题。
在排除故障和调查性能问题时,重要的是回顾系统的历史性能指标,以确保您对收集到的测量值有上下文。这将有助于确定当前系统利用率是否符合预期或异常。
性能
一般来说,性能问题可以分为五个领域:
-
应用程序
-
CPU
-
内存
-
磁盘
-
网络
任何一个领域的瓶颈通常也会影响其他领域;因此,了解每个领域是一个好主意。通过了解如何访问和交互每个资源,您将能够找到消耗多个资源的问题的根本原因。
由于报告的问题没有包括任何性能问题的细节,我们将探索和了解每个领域。完成后,我们将查看收集的数据并查看历史统计数据,以确定性能是否符合预期,或者系统性能是否真的下降了。
应用程序
在创建性能类别列表时,我按照我经常看到的领域进行了排序。每个环境都是不同的,但根据我的经验,应用程序通常是性能问题的主要来源。
虽然本章旨在涵盖性能问题,第九章,“使用系统工具排除应用程序”专门讨论了使用系统工具排除应用程序问题,包括性能问题。在本章中,我们将假设我们的问题与应用程序无关,并专注于系统性能。
CPU
CPU 是一个非常常见的性能瓶颈。有时,问题严格基于 CPU,而其他时候,增加的 CPU 使用率是另一个问题的症状。
调查 CPU 利用率最常见的命令是 top 命令。这个命令的主要作用是识别进程的 CPU 利用率。在第二章,“故障排除命令和有用信息的来源”中,我们讨论了使用ps
命令进行这种活动。在本节中,我们将使用 top 和 ps 来调查 CPU 利用率,以解决我们的速度慢的问题。
Top – 查看所有内容的单个命令
top
命令是系统管理员和用户运行的第一批命令之一,用于查看整体系统性能。原因在于 top 不仅显示了负载平均值、CPU 和内存的详细情况,还显示了利用这些资源的进程的排序列表。
top
最好的部分是,当不带任何标志运行时,这些详细信息每 3 秒更新一次。
以下是不带任何标志运行时top
输出的示例。
top - 17:40:43 up 4:07, 2 users, load average: 0.32, 0.43, 0.44
Tasks: 100 total, 2 running, 98 sleeping, 0 stopped, 0 zombie
%Cpu(s): 37.3 us, 0.7 sy, 0.0 ni, 62.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 469408 total, 228112 used, 241296 free, 764 buffers
KiB Swap: 1081340 total, 0 used, 1081340 free. 95332 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3023 vagrant 20 0 7396 720 504 S 37.6 0.2 91:08.04 lookbusy
11 root 20 0 0 0 0 R 0.3 0.0 0:13.28 rcuos/0
682 root 20 0 322752 1072 772 S 0.3 0.2 0:05.60 VBoxService
1 root 20 0 50784 7256 2500 S 0.0 1.5 0:01.39 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
3 root 20 0 0 0 0 S 0.0 0.0 0:00.24 ksoftirqd/0
5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H
6 root 20 0 0 0 0 S 0.0 0.0 0:00.04 kworker/u2:0
7 root rt 0 0 0 0 S 0.0 0.0 0:00.00 migration/0
8 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_bh
9 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcuob/0
10 root 20 0 0 0 0 S 0.0 0.0 0:05.44 rcu_sched
top
的默认输出中显示了相当多的信息。在本节中,我们将专注于 CPU 利用率信息。
%Cpu(s): 37.3 us, 0.7 sy, 0.0 ni, 62.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
top
命令输出的第一部分显示了当前 CPU 利用率的详细情况。列表中的每一项代表了 CPU 的不同使用方式。为了更好地理解输出结果,让我们来看看每个数值的含义:
-
us – User:这个数字表示用户模式中进程所消耗的 CPU 百分比。在这种模式下,应用程序无法访问底层硬件,必须使用系统 API(也称为系统调用)来执行特权操作。在执行这些系统调用时,执行将成为系统 CPU 利用率的一部分。
-
sy – System:这个数字表示内核模式执行所消耗的 CPU 百分比。在这种模式下,系统可以直接访问底层硬件;这种模式通常保留给受信任的操作系统进程。
-
ni – Nice user processes:这个数字表示由设置了 nice 值的用户进程所消耗的 CPU 时间百分比。
us%
值特指那些未修改过 niceness 值的进程。 -
id – Idle:这个数字表示 CPU 空闲的时间百分比。基本上,它是 CPU 未被利用的时间。
-
wa – Wait:这个数字表示 CPU 等待的时间百分比。当有很多进程在等待 I/O 设备时,这个值通常很高。I/O 等待状态不仅指硬盘,而是指所有 I/O 设备,包括硬盘。
-
hi – Hardware interrupts:这个数字表示由硬件中断所消耗的 CPU 时间百分比。硬件中断是来自系统硬件(如硬盘或网络设备)的信号,发送给 CPU。这些中断表示有事件需要 CPU 时间。
-
si - 软件中断:这个数字是被软件中断消耗的 CPU 时间的百分比。软件中断类似于硬件中断;但是,它们是由运行进程发送给内核的信号触发的。
-
st - 被窃取:这个数字特别适用于作为虚拟机运行的 Linux 系统。这个数字是被主机从这台机器上窃取的 CPU 时间的百分比。当主机机器本身遇到 CPU 争用时,通常会出现这种情况。在一些云环境中,这也可能发生,作为强制执行资源限制的一种方法。
我之前提到top
的输出默认每 3 秒刷新一次。CPU 百分比行也每 3 秒刷新一次;top
将显示自上次刷新间隔以来每个状态的 CPU 时间百分比。
这个输出告诉我们关于我们的问题的什么?
如果我们回顾之前top
命令的输出,我们可以对这个系统了解很多。
%Cpu(s): 37.3 us, 0.7 sy, 0.0 ni, 62.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
从前面的输出中,我们可以看到 CPU 时间的37.3%
被用户模式下的进程消耗。另外0.7%
的 CPU 时间被内核执行模式下的进程使用;这是基于us
和sy
的值。id
值告诉我们剩下的 CPU 没有被利用,这意味着总体上,这台服务器上有充足的 CPU 可用。
top
命令显示的另一个事实是 CPU 时间没有花在等待 I/O 上。我们可以从wa
值为0.0
看出。这很重要,因为它告诉我们报告的性能问题不太可能是由于高 I/O。在本章后面,当我们开始探索磁盘性能时,我们将深入探讨 I/O 等待。
来自 top 的单个进程
top
命令输出中的 CPU 行是整个服务器的摘要,但 top 还包括单个进程的 CPU 利用率。为了更清晰地聚焦,我们可以再次执行 top,但这次,让我们专注于正在运行的top
进程。
$ top -n 1
top - 15:46:52 up 3:21, 2 users, load average: 1.03, 1.11, 1.06
Tasks: 108 total, 3 running, 105 sleeping, 0 stopped, 0 zombie
%Cpu(s): 34.1 us, 0.7 sy, 0.0 ni, 65.1 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem: 502060 total, 220284 used, 281776 free, 764 buffers
KiB Swap: 1081340 total, 0 used, 1081340 free. 92940 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3001 vagrant 20 0 7396 720 504 R 98.4 0.1 121:08.67 lookbusy
3002 vagrant 20 0 7396 720 504 S 6.6 0.1 19:05.12 lookbusy
1 root 20 0 50780 7264 2508 S 0.0 1.4 0:01.69 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.01 kthreadd
3 root 20 0 0 0 0 S 0.0 0.0 0:00.97 ksoftirqd/0
5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H
6 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kworker/u4:0
7 root rt 0 0 0 0 S 0.0 0.0 0:00.67 migration/0
这次执行top
命令时,使用了-n
(数字)标志。这个标志告诉top
只刷新指定次数,这里是 1 次。在尝试捕获top
的输出时,这个技巧可能会有所帮助。
如果我们回顾上面top
命令的输出,我们会看到一些非常有趣的东西。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3001 vagrant 20 0 7396 720 504 R 98.4 0.1 121:08.67 lookbusy
默认情况下,top
命令按照进程利用的 CPU 百分比对进程进行排序。这意味着列表中的第一个进程是在该时间间隔内消耗 CPU 最多的进程。
如果我们看一下进程 ID 为3001
的顶部进程,我们会发现它正在使用 CPU 时间的98.4%
。然而,根据 top 命令的系统范围 CPU 统计数据,CPU 时间的65.1%
处于空闲状态。这种情况实际上是许多系统管理员困惑的常见原因。
%Cpu(s): 34.1 us, 0.7 sy, 0.0 ni, 65.1 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
一个单个进程如何使用几乎 100%的 CPU 时间,而系统本身显示 CPU 时间的 65%是空闲的?答案其实很简单;当top
在其标题中显示 CPU 利用率时,比例是基于整个系统的。然而,对于单个进程,CPU 利用率的比例是针对一个 CPU 的。这意味着我们的进程 3001 实际上几乎使用了一个完整的 CPU,而我们的系统很可能有多个 CPU。
通常会看到能够利用多个 CPU 的进程显示的百分比高于 100%。例如,完全利用三个 CPU 的进程将显示 300%。这也可能会让不熟悉top
命令服务器总体和每个进程输出差异的用户感到困惑。
确定可用 CPU 数量
先前,我们确定了这个系统必须有多个可用的 CPU。我们没有确定的是有多少个。确定可用 CPU 数量的最简单方法是简单地读取/proc/cpuinfo
文件。
# cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 58
model name : Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz
stepping : 9
microcode : 0x19
cpu MHz : 2348.850
cache size : 6144 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 2
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 5
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl pni ssse3 lahf_lm
bogomips : 4697.70
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management:
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 58
model name : Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz
stepping : 9
microcode : 0x19
cpu MHz : 2348.850
cache size : 6144 KB
physical id : 0
siblings : 2
core id : 1
cpu cores : 2
apicid : 1
initial apicid : 1
fpu : yes
fpu_exception : yes
cpuid level : 5
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl pni ssse3 lahf_lm
bogomips : 4697.70
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management:
/proc/cpuinfo
文件包含了关于系统可用 CPU 的大量有用信息。它显示了 CPU 的类型到型号,可用的标志,CPU 的速度,最重要的是可用的 CPU 数量。
系统中每个可用的 CPU 都将在cpuinfo
文件中列出。这意味着您可以简单地在cpuinfo
文件中计算处理器的数量,以确定服务器可用的 CPU 数量。
从上面的例子中,我们可以确定这台服务器有 2 个可用的 CPU。
线程和核心
使用cpuinfo
来确定可用 CPU 数量的一个有趣的注意事项是,当使用具有多个核心并且是超线程的 CPU 时,细节有点误导。cpuinfo
文件将 CPU 上的核心和线程都报告为它可以利用的处理器。这意味着即使您的系统上安装了一个物理芯片,如果该芯片是一个四核超线程 CPU,cpuinfo
文件将显示八个处理器。
lscpu – 查看 CPU 信息的另一种方法
虽然/proc/cpuinfo
是许多管理员和用户用来确定 CPU 信息的方法;在基于 RHEL 的发行版上,还有另一条命令也会显示这些信息。
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 2
On-line CPU(s) list: 0,1
Thread(s) per core: 1
Core(s) per socket: 2
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 58
Model name: Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz
Stepping: 9
CPU MHz: 2348.850
BogoMIPS: 4697.70
L1d cache: 32K
L1d cache: 32K
L2d cache: 6144K
NUMA node0 CPU(s): 0,1
/proc/cpuinfo
和lscpu
命令之间的一个区别是,lscpu
使得很容易识别核心、插槽和线程的数量。从/proc/cpuinfo
文件中识别这些信息通常会有点困难。
ps – 通过 ps 更深入地查看单个进程
虽然top
命令可以用来查看单个进程,但我个人认为ps
命令更适合用于调查运行中的进程。在第二章中,我们介绍了ps
命令以及它如何用于查看运行进程的许多不同方面。
在本章中,我们将使用ps
命令更深入地查看我们用top
命令确定为利用最多 CPU 时间的进程3001
。
$ ps -lf 3001
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
1 S vagrant 3001 3000 73 80 0 - 1849 hrtime 01:34 pts/1 892:23 lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80
在第二章中,我们讨论了使用ps
命令来显示运行中的进程。在前面的例子中,我们指定了两个标志,这些标志在第二章中显示,-l
(长列表)和–f
(完整格式)。在本章中,我们讨论了这些标志如何为显示的进程提供额外的细节。
为了更好地理解上述进程,让我们分解一下这两个标志提供的额外细节。
-
当前状态:
S
(可中断睡眠) -
用户:
vagrant
-
进程 ID:
3001
-
父进程 ID:
3000
-
优先级值:
80
-
优先级级别:
0
-
正在执行的命令:
lookbusy –cpu-mode-curve –cpu-curve-peak 14h –c 20-80
早些时候,使用top
命令时,这个进程几乎使用了一个完整的 CPU,这意味着这个进程是导致报告的缓慢的嫌疑对象。通过查看上述细节,我们可以确定这个进程的一些情况。
首先,它是进程3000
的子进程;这是我们通过父进程 ID 确定的。其次,当我们运行ps
命令时,它正在等待一个任务完成;我们可以通过进程当前处于可中断睡眠状态来确定这一点。
除了这两项之外,我们还可以看出该进程没有高调度优先级。我们可以通过查看优先级值来确定这一点,在这种情况下是 80。调度优先级系统的工作方式如下:数字越高,进程在系统调度程序中的优先级越低。
我们还可以看到 niceness 级别设置为0
,即默认值。这意味着用户没有调整 niceness 级别以获得更高(或更低)的优先级。
这些都是收集有关进程的重要数据点,但单独来看,它们并不能回答这个进程是否是报告的缓慢的原因。
使用 ps 来确定进程的 CPU 利用率
由于我们知道进程3001
是进程3000
的子进程,我们不仅应该查看进程3000
的相同信息,还应该使用ps
来确定进程3000
利用了多少 CPU。我们可以通过使用-o
(选项)标志和ps
来一次完成所有这些。这个标志允许您指定自己的输出格式;它还允许您查看通过常见的ps
标志通常不可见的字段。
在下面的命令中,使用-o
标志来格式化ps
命令的输出,使用前一次运行的关键字段并包括%cpu
字段。这个额外的字段将显示进程的 CPU 利用率。该命令还将使用-p
标志来指定进程3000
和进程3001
。
$ ps -o state,user,pid,ppid,nice,%cpu,cmd -p 3000,3001
S USER PID PPID NI %CPU CMD
S vagrant 3000 2980 0 0.0 lookbusy --cpu-mode curve --cpu- curve-peak 14h -c 20-80
R vagrant 3001 3000 0 71.5 lookbusy --cpu-mode curve --cpu- curve-peak 14h -c 20-80
虽然上面的命令非常长,但它展示了-o
标志有多么有用。在给定正确的选项的情况下,只用ps
命令就可以找到大量关于进程的信息。
从上面命令的输出中,我们可以看到进程3000
是lookbusy
命令的另一个实例。我们还可以看到进程3000
是进程2980
的子进程。在进一步进行之前,我们应该尝试识别与进程3001
相关的所有进程。
我们可以使用ps
命令和--forest
标志来做到这一点,该标志告诉ps
以树状格式打印父进程和子进程。当提供-e
(所有)标志时,ps
命令将以这种树状格式打印所有进程。
提示
默认情况下,ps
命令只会打印与执行命令的用户相关的进程。-e
标志改变了这种行为,以打印所有可能的进程。
下面的输出被截断,以特别识别lookbusy
进程。
$ ps --forest -eo user,pid,ppid,%cpu,cmd
root 1007 1 0.0 /usr/sbin/sshd -D
root 2976 1007 0.0 \_ sshd: vagrant [priv]
vagrant 2979 2976 0.0 \_ sshd: vagrant@pts/1
vagrant 2980 2979 0.0 \_ -bash
vagrant 3000 2980 0.0 \_ lookbusy --cpu-mode curve - -cpu-curve-peak 14h -c 20-80
vagrant 3001 3000 70.4 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80
vagrant 3002 3000 14.6 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80
从上面的ps
输出中,我们可以看到 ID 为3000
的lookbusy
进程产生了两个进程,分别是3001
和3002
。我们还可以看到当前通过 SSH 登录的 vagrant 用户启动了lookbusy
进程。
由于我们还使用了-o
标志和ps
来显示 CPU 利用率,我们可以看到进程3002
正在利用单个 CPU 的14.6%
。
提示
重要的是要注意,ps
命令还显示了单个处理器的 CPU 时间百分比,这意味着利用多个处理器的进程可能具有高于 100%的值。
把它们都放在一起
现在我们已经通过命令来识别系统的 CPU 利用率,让我们把它们放在一起总结一下找到的东西。
用 top 快速查看
我们识别与 CPU 性能相关的问题的第一步是执行top
命令。
$ top
top - 01:50:36 up 23:41, 2 users, load average: 0.68, 0.56, 0.48
Tasks: 107 total, 4 running, 103 sleeping, 0 stopped, 0 zombie
%Cpu(s): 34.5 us, 0.7 sy, 0.0 ni, 64.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 502060 total, 231168 used, 270892 free, 764 buffers
KiB Swap: 1081340 total, 0 used, 1081340 free. 94628 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3001 vagrant 20 0 7396 724 508 R 68.8 0.1 993:06.80 lookbusy
3002 vagrant 20 0 7396 724 508 S 1.0 0.1 198:58.16 lookbusy
12 root 20 0 0 0 0 S 0.3 0.0 3:47.55 rcuos/0
13 root 20 0 0 0 0 R 0.3 0.0 3:38.85 rcuos/1
2718 vagrant 20 0 131524 2536 1344 R 0.3 0.5 0:02.28 sshd
从top
的输出中,我们可以识别以下内容:
-
总体而言,系统大约 60%–70%的时间处于空闲状态
-
有两个正在运行
lookbusy
命令/程序的进程,其中一个似乎正在使用单个 CPU 的 70% -
鉴于这个单独进程的 CPU 利用率和系统 CPU 利用率,所涉及的服务器很可能有多个 CPU
-
我们可以用
lscpu
命令确认存在多个 CPU -
进程 3001 和 3002 是该系统上利用 CPU 最多的两个进程
-
CPU 等待状态百分比为 0,这意味着问题不太可能与磁盘 I/O 有关
通过 ps 深入挖掘
由于我们从top
命令的输出中确定了进程3001
和3002
是可疑的,我们可以使用ps
命令进一步调查这些进程。为了快速进行调查,我们将使用ps
命令和-o
和--forest
标志来用一个命令识别最大可能的信息。
$ ps --forest -eo user,pid,ppid,%cpu,cmd
root 1007 1 0.0 /usr/sbin/sshd -D
root 2976 1007 0.0 \_ sshd: vagrant [priv]
vagrant 2979 2976 0.0 \_ sshd: vagrant@pts/1
vagrant 2980 2979 0.0 \_ -bash
vagrant 3000 2980 0.0 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80
vagrant 3001 3000 69.8 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80
vagrant 3002 3000 13.9 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80
从这个输出中,我们可以确定以下内容:
-
进程 3001 和 3002 是进程 3000 的子进程
-
进程 3000 是由
vagrant
用户启动的 -
lookbusy
命令似乎是一个利用大量 CPU 的命令 -
启动
lookbusy
的方法并不表明这是一个系统进程,而是一个用户运行的临时命令。
根据上述信息,vagrant
用户启动的lookbusy
进程有可能是性能问题的根源。如果这个系统通常的 CPU 利用率较低,这是一个合理的根本原因的假设。然而,考虑到我们对这个系统不太熟悉,lookbusy
进程几乎使用了整个 CPU 也是可能的。
考虑到我们对系统的正常运行条件不太熟悉,我们应该在得出结论之前继续调查性能问题的其他可能来源。
内存
在应用程序和 CPU 利用率之后,内存利用率是性能下降的一个非常常见的来源。在 CPU 部分,我们广泛使用了top
,虽然top
也可以用来识别系统和进程的内存利用率,但在这一部分,我们将使用其他命令。
free – 查看空闲和已用内存
如第二章中所讨论的,故障排除命令和有用信息来源 free
命令只是简单地打印系统当前的内存可用性和使用情况。
当没有标志时,free
命令将以千字节为单位输出其值。为了使输出以兆字节为单位,我们可以简单地使用-m
(兆字节)标志执行free
命令。
$ free -m
total used free shared buffers cached
Mem: 490 92 397 1 0 17
-/+ buffers/cache: 74 415
Swap: 1055 57 998
free
命令显示了关于这个系统以及内存使用情况的大量信息。为了更好地理解这个命令,让我们对输出进行一些分解。
由于输出中有多行,我们将从输出标题之后的第一行开始:
Mem: 490 92 397 1 0 17
这一行中的第一个值是系统可用的物理内存总量。在我们的情况下,这是 490 MB。第二个值是系统使用的内存量。第三个值是系统上未使用的内存量;请注意,我使用了“未使用”而不是“可用”这个术语。第四个值是用于共享内存的内存量;除非您的系统经常使用共享内存,否则这通常是一个较低的数字。
第五个值是用于缓冲区的内存量。Linux 通常会尝试通过将频繁使用的磁盘信息放入物理内存来加快磁盘访问速度。缓冲区内存通常是文件系统元数据。缓存内存,也就是第六个值,是经常访问文件的内容。
Linux 内存缓冲区和缓存
Linux 通常会尝试使用“未使用”的内存来进行缓冲和缓存。这意味着为了提高效率,Linux 内核将频繁访问的文件数据和文件系统元数据存储在未使用的内存中。这使得系统能够利用本来不会被使用的内存来增强磁盘访问,而磁盘访问通常比系统内存慢。
这就是为什么第三个值“未使用”内存通常比预期的数字要低的原因。
然而,当系统的未使用内存不足时,Linux 内核将根据需要释放缓冲区和缓存内存。这意味着即使从技术上讲,用于缓冲区和缓存的内存被使用了,但在需要时它从技术上讲是可用的。
这将我们带到了 free 输出的第二行。
-/+ buffers/cache: 74 415
第二行有两个值,第一个是Used列的一部分,第二个是Free或“未使用”列的一部分。这些值是在考虑缓冲区和缓存内存的可用或未使用内存值之后得出的。
简单来说,第二行的已使用值是从第一行的已使用内存值减去缓冲区和缓存值得到的结果。对于我们的示例,这是 92 MB(已使用)减去 17 MB(cached)。
第二行的 free 值是第一行的 Free 值加上缓冲区和缓存内存的结果。使用我们的示例数值,这将是 397 MB(free)加上 17 MB(cached)。
交换内存
free
命令的输出的第三行是用于交换内存的。
Swap: 1055 57 998
在这一行中,有三列:可用、已使用和空闲。交换内存的值相当容易理解。可用交换值是系统可用的交换内存量,已使用值是当前分配的交换量,而空闲值基本上是可用交换减去已分配的交换量。
有许多环境不赞成分配大量的交换空间,因为这通常是系统内存不足并使用交换空间来补偿的指标。
free 告诉我们关于我们系统的信息
如果我们再次查看 free 的输出,我们可以确定关于这台服务器的很多事情。
$ free -m
total used free shared buffers cached
Mem: 490 105 385 1 0 25
-/+ buffers/cache: 79 410
Swap: 1055 56 999
我们可以确定实际上只使用了很少的内存(79 MB)。这意味着总体上,系统应该有足够的内存可用于进程。
然而,还有一个有趣的事实,在第三行显示,56 MB 的内存已被写入交换空间。尽管系统当前有大量可用内存,但已经有 56 MB 被写入交换空间。这意味着在过去的某个时刻,这个系统可能内存不足,足够低到系统不得不将内存页面从物理内存交换到交换内存。
检查 oomkill
当 Linux 系统的物理内存耗尽时,它首先尝试重用分配给缓冲区和缓存的内存。如果没有额外的内存可以从这些来源中回收,那么内核将从物理内存中获取旧的内存页面并将它们写入交换内存。一旦物理内存和交换内存都被分配,内核将启动内存不足杀手(oomkill)进程。oomkill
进程旨在找到使用大量内存的进程并将其杀死(停止)。
一般来说,在大多数环境中,oomkill
进程是不受欢迎的。一旦调用,oomkill
进程可以杀死许多不同类型的进程。无论进程是系统的一部分还是用户级别的,oomkill
都有能力杀死它们。
对于可能影响内存利用的性能问题,检查oomkill
进程最近是否被调用是一个很好的主意。确定oomkill
最近是否运行的最简单方法是简单地查看系统的控制台,因为这个进程的启动会直接记录在系统控制台上。然而,在云和虚拟环境中,控制台可能不可用。
另一个确定最近是否调用了oomkill
的好方法是搜索/var/log/messages
日志文件。我们可以通过执行grep
命令并搜索字符串Out of memory
来做到这一点。
# grep "Out of memory" /var/log/messages
对于我们的示例系统,最近没有发生oomkill
调用。如果我们的系统调用了oomkill
进程,我们可能会收到类似以下消息:
# grep "Out of memory" /var/log/messages
Feb 7 19:38:45 localhost kernel: Out of memory: Kill process 3236 (python) score 838 or sacrifice child
在第十一章中,从常见故障中恢复,我们将再次调查内存问题,并深入了解oomkill
及其工作原理。对于本章,我们可以得出结论,系统尚未完全耗尽其可用内存。
ps - 检查单个进程的内存利用率
到目前为止,系统上的内存使用似乎很小,但是我们从 CPU 验证步骤中知道,运行lookbusy
的进程是可疑的,可能导致性能问题。由于我们怀疑lookbusy
进程存在问题,我们还应该查看这些进程使用了多少内存。为了做到这一点,我们可以再次使用带有-o
标志的ps
命令。
$ ps -eo user,pid,ppid,%mem,rss,vsize,comm | grep lookbusy
vagrant 3000 2980 0.0 4 7396 lookbusy
vagrant 3001 3000 0.0 296 7396 lookbusy
vagrant 3002 3000 0.0 220 7396 lookbusy
vagrant 5380 2980 0.0 8 7396 lookbusy
vagrant 5381 5380 0.0 268 7396 lookbusy
vagrant 5382 5380 0.0 268 7396 lookbusy
vagrant 5383 5380 40.7 204812 212200 lookbusy
vagrant 5531 2980 0.0 40 7396 lookbusy
vagrant 5532 5531 0.0 288 7396 lookbusy
vagrant 5533 5531 0.0 288 7396 lookbusy
vagrant 5534 5531 34.0 170880 222440 lookbusy
然而,这一次我们以稍有不同的方式运行了我们的ps
命令,因此得到了不同的结果。这一次执行ps
命令时,我们使用了-e
(everything)标志来显示所有进程。然后将结果传输到grep
,以便将它们缩小到只匹配lookbusy
模式的进程。
这是使用ps
命令的一种非常常见的方式;事实上,这比在命令行上指定进程 ID 更常见。除了使用grep
之外,这个ps
命令示例还介绍了一些新的格式选项。
-
%mem:这是进程正在使用的系统内存的百分比。
-
rss:这是进程的常驻集大小,基本上是指进程使用的不可交换内存量。
-
vsize:这是虚拟内存大小,它包含进程完全使用的内存量,无论这些内存是物理内存的一部分还是交换内存的一部分。
-
comm:此选项类似于 cmd,但不显示命令行参数。
ps
示例显示了有趣的信息,特别是以下几行:
vagrant 5383 5380 40.7 204812 212200 lookbusy
vagrant 5534 5531 34.0 170880 222440 lookbusy
似乎已经启动了几个额外的lookbusy
进程,并且这些进程正在利用系统内存的 40%和 34%(通过使用%mem
列)。从 rss 列中,我们可以看到这两个进程正在使用总共 490MB 物理内存中的约 374MB。
看起来这些进程在我们开始调查后开始利用大量内存。最初,我们的 free 输出表明只使用了 70MB 内存;然而,这些进程似乎利用了更多。我们可以通过再次运行 free 来确认这一点。
$ free -m
total used free shared buffers cached
Mem: 490 453 37 0 0 3
-/+ buffers/cache: 449 41
Swap: 1055 310 745
事实上,我们的系统现在几乎利用了所有的内存;事实上,我们还使用了 310MB 的交换空间。
vmstat - 监控内存分配和交换
由于这个系统的内存利用率似乎有所波动,有一个非常有用的命令可以定期显示内存分配和释放以及换入和换出的页面数。这个命令叫做vmstat
。
$ vmstat -n 10 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
5 0 204608 31800 0 7676 8 6 12 6 101 131 44 1 55 0 0
1 0 192704 35816 0 2096 1887 130 4162 130 2080 2538 53 6 39 2 0
1 0 191340 32324 0 3632 1590 57 3340 57 2097 2533 54 5 41 0 0
4 0 191272 32260 0 5400 536 2 2150 2 1943 2366 53 4 43 0 0
3 0 191288 34140 0 4152 392 0 679 0 1896 2366 53 3 44 0 0
在上面的示例中,vmstat
命令是使用-n
(一个标题)标志执行的,后面跟着延迟时间(10 秒)和要生成的报告数(5)。这些选项告诉vmstat
仅为此次执行输出一个标题行,而不是为每个报告输出一个新的标题行,每 10 秒运行一次报告,并将报告数量限制为 5。如果省略了报告数量的限制,vmstat
将简单地持续运行,直到使用CTRL+C停止。
vmstat
的输出一开始可能有点压倒性,但如果我们分解输出,就会更容易理解。vmstat
的输出有六个输出类别,即进程、内存、交换、IO、系统和 CPU。在本节中,我们将专注于这两个类别:内存和交换。
-
内存
-
swpd
:写入交换的内存量 -
free
:未使用的内存量 -
buff
:用作缓冲区的内存量 -
cache
:用作缓存的内存量 -
inact
:非活动内存量 -
active
:活动内存量 -
交换
-
si
:从磁盘交换的内存量 -
so
:交换到磁盘的内存量
现在我们已经了解了这些值的定义,让我们看看vmstat
的输出告诉我们关于这个系统内存使用情况的信息。
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
5 0 204608 31800 0 7676 8 6 12 6 101 131 44 1 55 0 0
1 0 192704 35816 0 2096 1887 130 4162 130 2080 2538 53 6 39 2 0
如果我们比较vmstat
输出的第一行和第二行,我们会看到一个相当大的差异。特别是,我们可以看到在第一个间隔中,缓存内存是7676
,而在第二个间隔中,这个值是 2096。我们还可以看到第一行中的si
或交换入值是 8,而第二行中是 1887。
这种差异的原因是,vmstat
的第一个报告总是自上次重启以来的统计摘要,而第二个报告是自上一个报告以来的统计摘要。每个后续的报告将总结前一个报告,这意味着第三个报告将总结自第二个报告以来的统计数据。vmstat
的这种行为经常会让新的系统管理员和用户感到困惑;因此,它通常被认为是一种高级故障排除工具。
由于vmstat
生成第一个报告的方法,通常的做法是丢弃它并从第二个报告开始。我们将遵循这一原则,特别关注第二个和第三个报告。
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
5 0 204608 31800 0 7676 8 6 12 6 101 131 44 1 55 0 0
1 0 192704 35816 0 2096 1887 130 4162 130 2080 2538 53 6 39 2 0
1 0 191340 32324 0 3632 1590 57 3340 57 2097 2533 54 5 41 0 0
在第二个和第三个报告中,我们可以看到一些有趣的数据。
最引人注目的是,从第一个报告的生成时间到第二个报告的生成时间,交换了 1,887 页,交换出了 130 页。第二个报告还显示,只有 35 MB 的内存是空闲的,缓冲区中没有内存,缓存中有 2 MB 的内存。根据 Linux 内存的利用方式,这意味着系统上实际上只有 37 MB 的可用内存。
这种低可用内存量解释了为什么我们的系统已经交换了大量页面。我们可以从第三行看到这种趋势正在持续,我们继续交换了相当多的页面,我们的可用内存已经减少到大约 35 MB。
从这个vmstat
的例子中,我们可以看到我们的系统现在已经用尽了物理内存。因此,我们的系统正在从物理 RAM 中取出内存页面并将其写入我们的交换设备。
把所有东西放在一起
现在我们已经探索了用于故障排除内存利用的工具,让我们把它们都放在一起来解决系统性能缓慢的问题。
用 free 查看系统的内存利用
给我们提供系统内存利用快照的第一个命令是free
命令。这个命令将为我们提供在哪里进一步查找任何内存利用问题的想法。
$ free -m
total used free shared buffers cached
Mem: 490 293 196 0 0 18
-/+ buffers/cache: 275 215
Swap: 1055 183 872
从free
的输出中,我们可以看到目前有 215 MB 的内存可用。我们可以通过第二行的free
列看到这一点。我们还可以看到,总体上,这个系统有 183 MB 的内存已经被交换到我们的交换设备。
观察 vmstat 的情况
由于系统在某个时候已经进行了交换(或者说分页),我们可以使用vmstat
命令来查看系统当前是否正在进行交换。
这次执行vmstat
时,我们将不指定报告值的数量,这将导致vmstat
持续报告内存统计,类似于 top 命令的输出。
$ vmstat -n 10
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
4 0 188008 200320 0 19896 35 8 61 9 156 4 44 1 55 0 0
4 0 188008 200312 0 19896 0 0 0 0 1361 1314 36 2 62 0 0
2 0 188008 200312 0 19896 0 0 0 0 1430 1442 37 2 61 0 0
0 0 188008 200312 0 19896 0 0 0 0 1431 1418 37 2 61 0 0
0 0 188008 200280 0 19896 0 0 0 0 1414 1416 37 2 61 0 0
2 0 188008 200280 0 19896 0 0 0 0 1456 1480 37 2 61 0 0
这个vmstat
输出与我们之前的执行不同。从这个输出中,我们可以看到虽然有相当多的内存被交换,但系统目前并没有进行交换。我们可以通过si
(交换入)和 so(交换出)列中的 0 值来确定这一点。
实际上,在这次vmstat
运行期间,内存利用率似乎很稳定。每个vmstat
报告中,free
内存值都相当一致,缓存和缓冲内存统计也是如此。
使用 ps 找到内存利用最多的进程
我们的系统有 490MB 的物理内存,free
和vmstat
都显示大约 215MB 的可用内存。这意味着我们系统内存的一半以上目前被使用;在这种使用水平下,找出哪些进程正在使用我们系统的内存是一个好主意。即使没有别的,这些数据也将有助于显示系统当前的状态。
要识别使用最多内存的进程,我们可以使用ps
命令以及 sort 和 tail。
# ps -eo rss,vsize,user,pid,cmd | sort -nk 1 | tail -n 5
1004 115452 root 5073 -bash
1328 123356 root 5953 ps -eo rss,vsize,user,pid,cmd
2504 525652 root 555 /usr/sbin/NetworkManager --no-daemon
4124 50780 root 1 /usr/lib/systemd/systemd --switched-root --system --deserialize 23
204672 212200 vagrant 5383 lookbusy -m 200MB -c 10
上面的例子使用管道将ps
的输出重定向到 sort 命令。sort 命令执行数字(-n
)对第一列(-k 1
)的排序。这将对输出进行排序,将具有最高rss
大小的进程放在底部。在sort
命令之后,输出也被管道传递到tail
命令,当指定了-n
(数字)标志后跟着一个数字,将限制输出只包括指定数量的结果。
提示
如果将命令与管道一起链接的概念是新的,我强烈建议练习这一点,因为它对日常的sysadmin
任务以及故障排除非常有用。我们将在本书中多次讨论这个概念,并提供示例。
204672 212200 vagrant 5383 lookbusy -m 200MB -c 10
从ps
的输出中,我们可以看到进程 5383 正在使用大约 200MB 的内存。我们还可以看到该进程是另一个lookbusy
进程,再次由 vagrant 用户生成。
从free
,vmstat
和ps
的输出中,我们可以确定以下内容:
-
系统当前大约有 200MB 的可用内存
-
虽然系统目前没有交换,但过去曾经有过,根据我们之前从
vmstat
看到的情况,我们知道它最近进行了交换 -
我们发现进程
5383
正在使用大约 200MB 的内存 -
我们还可以看到进程
5383
是由vagrant
用户启动的,并且正在运行lookbusy
进程 -
使用
free
命令,我们可以看到这个系统有 490MB 的物理内存
根据以上信息,似乎由vagrant
用户执行的lookbusy
进程不仅是 CPU 的可疑使用者,还是内存的可疑使用者。
磁盘
磁盘利用率是另一个常见的性能瓶颈。一般来说,性能问题很少是由于磁盘空间的问题。虽然我曾经看到由于大量文件或大文件的性能问题,但一般来说,磁盘性能受到写入和读取磁盘的限制。因此,在故障排除性能问题时,了解文件系统是否已满很重要,但仅仅根据文件系统的使用情况并不总是能指示是否存在问题。
iostat - CPU 和设备输入/输出统计
iostat
命令是用于故障排除磁盘性能问题的基本命令,类似于 vmstat,它提供的使用和信息都是相似的。像vmstat
一样,执行iostat
命令后面跟着两个数字,第一个是报告生成的延迟,第二个是要生成的报告数。
$ iostat -x 10 3
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/08/2015 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
43.58 0.00 1.07 0.16 0.00 55.19
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 12.63 3.88 8.47 3.47 418.80 347.40 128.27 0.39 32.82 0.80 110.93 0.47 0.56
dm-0 0.00 0.00 16.37 3.96 65.47 15.82 8.00 0.48 23.68 0.48 119.66 0.09 0.19
dm-1 0.00 0.00 4.73 3.21 353.28 331.71 172.51 0.39 48.99 1.07 119.61 0.54 0.43
avg-cpu: %user %nice %system %iowait %steal %idle
20.22 0.00 20.33 22.14 0.00 37.32
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91
dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70
dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46
avg-cpu: %user %nice %system %iowait %steal %idle
18.23 0.00 15.56 29.26 0.00 36.95
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.10 7.10 697.50 440.10 74747.60 42641.75 206.38 74.13 66.98 0.64 172.13 0.58 66.50
dm-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-1 0.00 0.00 697.40 405.00 74722.00 40888.65 209.74 75.80 70.63 0.66 191.11 0.61 67.24
在上面的例子中,提供了-x
(扩展统计)标志以打印扩展统计信息。扩展统计非常有用,并提供了额外的信息,对于识别性能瓶颈至关重要。
CPU 详情
iostat
命令将显示 CPU 统计信息以及 I/O 统计信息。这是另一个可以用来排除 CPU 利用率的命令。当 CPU 利用率指示高 I/O 等待时间时,这是特别有用的。
avg-cpu: %user %nice %system %iowait %steal %idle
20.22 0.00 20.33 22.14 0.00 37.32
以上是从top
命令显示的相同信息;在 Linux 中找到多个输出类似信息的命令并不罕见。由于这些细节已在 CPU 故障排除部分中涵盖,我们将专注于iostat
命令的 I/O 统计部分。
审查 I/O 统计
要开始审查 I/O 统计,让我们从前两份报告开始。我在下面包括了 CPU 利用率,以帮助指示每份报告的开始位置,因为它是每份统计报告中的第一项。
avg-cpu: %user %nice %system %iowait %steal %idle
43.58 0.00 1.07 0.16 0.00 55.19
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 12.63 3.88 8.47 3.47 418.80 347.40 128.27 0.39 32.82 0.80 110.93 0.47 0.56
dm-0 0.00 0.00 16.37 3.96 65.47 15.82 8.00 0.48 23.68 0.48 119.66 0.09 0.19
dm-1 0.00 0.00 4.73 3.21 353.28 331.71 172.51 0.39 48.99 1.07 119.61 0.54 0.43
avg-cpu: %user %nice %system %iowait %steal %idle
20.22 0.00 20.33 22.14 0.00 37.32
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91
dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70
dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46
通过比较前两份报告,我们发现它们之间存在很大的差异。在第一个报告中,sda
设备的%util
值为0.56
,而在第二个报告中为65.91
。
这种差异的原因是,与vmstat
一样,第一次执行iostat
的统计是基于服务器最后一次重启的时间。第二份报告是基于第一份报告之后的时间。这意味着第二份报告的输出是基于第一份报告生成之间的 10 秒。这与vmstat
中看到的行为相同,并且是其他收集性能统计信息的工具的常见行为。
与vmstat
一样,我们将丢弃第一个报告,只看第二个报告。
avg-cpu: %user %nice %system %iowait %steal %idle
20.22 0.00 20.33 22.14 0.00 37.32
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91
dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70
dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46
从上面,我们可以确定这个系统的几个情况。最重要的是 CPU 行中的%iowait
值。
avg-cpu: %user %nice %system %iowait %steal %idle
20.22 0.00 20.33 22.14 0.00 37.32
早些时候在执行 top 命令时,等待 I/O 的时间百分比相当小;然而,在运行iostat
时,我们可以看到 CPU 实际上花了很多时间等待 I/O。虽然 I/O 等待并不一定意味着等待磁盘,但这个输出的其余部分似乎表明磁盘活动相当频繁。
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91
dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70
dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46
扩展统计输出有许多列,为了使这个输出更容易理解,让我们分解一下这些列告诉我们的内容。
-
rrqm/s:每秒合并和排队的读取请求数
-
wrqm/s:每秒合并和排队的写入请求数
-
r/s:每秒完成的读取请求数
-
w/s:每秒完成的写入请求数
-
rkB/s:每秒读取的千字节数
-
wkB/s:每秒写入的千字节数
-
avgr-sz:发送到设备的请求的平均大小(以扇区为单位)
-
avgqu-sz:发送到设备的请求的平均队列长度
-
await:请求等待服务的平均时间(毫秒)
-
r_await:读取请求等待服务的平均时间(毫秒)
-
w_await:写入请求等待服务的平均时间(毫秒)
-
svctm:此字段无效,将被删除;不应被信任或使用
-
%util:在此设备服务 I/O 请求时所花费的 CPU 时间百分比。设备最多只能利用 100%
对于我们的示例,我们将专注于r/s
,w/s
,await
和%util
值,因为这些值将告诉我们关于这个系统的磁盘利用率的很多信息,同时保持我们的示例简单。
经过审查iostat
输出后,我们可以看到sda
和dm-1
设备都具有最高的%util
值,这意味着它们最接近达到容量。
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91
dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46
从这份报告中,我们可以看到sda
设备平均完成了 764 次读取(r/s
)和 808 次写入(w/s
)每秒。我们还可以确定这些请求平均需要 39 毫秒(等待时间)来完成。虽然这些数字很有趣,但并不一定意味着系统处于异常状态。由于我们对这个系统不熟悉,我们并不一定知道读取和写入的水平是否出乎意料。然而,收集这些信息是很重要的,因为这些统计数据是故障排除过程中数据收集阶段的重要数据。
从iostat
中我们可以看到另一个有趣的统计数据是,sda
和dm-1
设备的%util
值都约为 66%。这意味着在第一次报告生成和第二次报告之间的 10 秒内,66%的 CPU 时间都是在等待sd
或dm-1
设备。
识别设备
对于磁盘设备来说,66%的利用率通常被认为是很高的,虽然这是非常有用的信息,但它并没有告诉我们是谁或什么在利用这个磁盘。为了回答这些问题,我们需要弄清楚sda
和dm-1
到底被用来做什么。
由于iostat
命令输出的设备通常是磁盘设备,识别这些设备的第一步是运行mount
命令。
$ mount
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
devtmpfs on /dev type devtmpfs (rw,nosuid,seclabel,size=244828k,nr_inodes=61207,mode=755)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,seclabel)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,seclabel,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,nodev,seclabel,mode=755)
tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,seclabel,mode=755)
configfs on /sys/kernel/config type configfs (rw,relatime)
/dev/mapper/root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,seclabel)
mqueue on /dev/mqueue type mqueue (rw,relatime,seclabel)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
mount
命令在没有任何选项的情况下运行时,将显示所有当前挂载的文件系统。mount
输出中的第一列是已经挂载的设备。在上面的输出中,我们可以看到sda
设备实际上是一个磁盘设备,并且它有一个名为sda1
的分区,挂载为/boot
。
然而,我们没有看到dm-1
设备。由于这个设备没有出现在mount
命令的输出中,我们可以通过另一种方式,在/dev
文件夹中查找dm-1
设备。
系统上的所有设备都被呈现为/dev
文件夹结构中的一个文件。dm-1
设备也不例外。
$ ls -la /dev/dm-1
brw-rw----. 1 root disk 253, 1 Feb 1 18:47 /dev/dm-1
虽然我们已经找到了dm-1
设备的位置,但我们还没有确定它的用途。然而,关于这个设备,有一件事引人注目,那就是它的名字dm-1
。当设备以dm
开头时,这表明该设备是由设备映射器创建的逻辑设备。
设备映射器是一个 Linux 内核框架,允许系统创建虚拟磁盘设备,这些设备“映射”回物理设备。这个功能用于许多特性,包括软件 RAID、磁盘加密和逻辑卷。
设备映射器框架中的一个常见做法是为这些特性创建符号链接,这些符号链接指向单个逻辑设备。由于我们可以用ls
命令看到dm-1
是一个块设备,通过输出的第一列的“b”值(brw-rw----.
),我们知道dm-1
不是一个符号链接。我们可以利用这些信息以及 find 命令来识别任何指向dm-1
块设备的符号链接。
# find -L /dev -samefile /dev/dm-1
/dev/dm-1
/dev/rhel/root
/dev/disk/by-uuid/beb5220d-5cab-4c43-85d7-8045f870ba7d
/dev/disk/by-id/dm-uuid-LVM-qj3iMeektIlL3Z0g4WMPMJRbzacnpS9IVOCzB60GSHCEgbRKYW9ZKXR5prUPEE1e
/dev/disk/by-id/dm-name-root
/dev/block/253:1
/dev/mapper/root
在前面的章节中,我们使用 find 命令来识别配置和日志文件。在上面的例子中,我们使用了-L
(跟随链接)标志,后面跟着/dev
路径和--samefile
标志,告诉 find 搜索/dev
文件夹结构,搜索任何符号链接的文件,以识别任何与/dev/dm-1
相同的文件。
--samefile
标志标识具有相同inode
号的文件。当命令中包含-L
标志时,输出包括符号链接,而这个例子似乎返回了几个结果。最引人注目的符号链接文件是/dev/mapper/root
;这个文件之所以引人注目,是因为它也出现在挂载命令的输出中。
/dev/mapper/root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
看起来/dev/mapper/root
似乎是一个逻辑卷。在 Linux 中,逻辑卷本质上是存储虚拟化。这个功能允许您创建伪设备(作为设备映射器的一部分),它映射到一个或多个物理设备。
例如,可以将四个不同的硬盘组合成一个逻辑卷。逻辑卷然后可以用作单个文件系统的磁盘。甚至可以在以后通过使用逻辑卷添加另一个硬盘。
确认/dev/mapper/root
设备实际上是一个逻辑卷,我们可以执行lvdisplay
命令,该命令用于显示系统上的逻辑卷。
# lvdisplay
--- Logical volume ---
LV Path /dev/rhel/swap
LV Name swap
VG Name rhel
LV UUID y1ICUQ-l3uA-Mxfc-JupS-c6PN-7jvw-W8wMV6
LV Write Access read/write
LV Creation host, time localhost, 2014-07-21 23:35:55 +0000
LV Status available
# open 2
LV Size 1.03 GiB
Current LE 264
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 253:0
--- Logical volume ---
LV Path /dev/rhel/root
LV Name root
VG Name rhel
LV UUID VOCzB6-0GSH-CEgb-RKYW-9ZKX-R5pr-UPEE1e
LV Write Access read/write
LV Creation host, time localhost, 2014-07-21 23:35:55 +0000
LV Status available
# open 1
LV Size 38.48 GiB
Current LE 9850
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 253:1
从lvdisplay
的输出中,我们可以看到一个名为/dev/rhel/root
的有趣路径,这个路径也存在于我们的find
命令的输出中。让我们用ls
命令来查看这个设备。
# ls -la /dev/rhel/root
lrwxrwxrwx. 1 root root 7 Aug 3 16:27 /dev/rhel/root -> ../dm-1
在这里,我们可以看到/dev/rhel/root
是一个指向/dev/dm-1
的符号链接;这证实了/dev/rhel/root
与/dev/dm-1
是相同的,这些实际上是逻辑卷设备,这意味着这些并不是真正的物理设备。
要显示这些逻辑卷背后的物理设备,我们可以使用pvdisplay
命令。
# pvdisplay
--- Physical volume ---
PV Name /dev/sda2
VG Name rhel
PV Size 39.51 GiB / not usable 3.00 MiB
Allocatable yes (but full)
PE Size 4.00 MiB
Total PE 10114
Free PE 0
Allocated PE 10114
PV UUID n5xoxm-kvyI-Z7rR-MMcH-1iJI-D68w-NODMaJ
我们可以从pvdisplay
的输出中看到,dm-1
设备实际上映射到sda2
,这解释了为什么dm-1
和sda
的磁盘利用率非常接近,因为对dm-1
的任何活动实际上都是在sda
上执行的。
谁在向这些设备写入?
现在我们已经找到了 I/O 的利用情况,我们需要找出谁在利用这个 I/O。找出哪些进程最多地写入磁盘的最简单方法是使用iotop
命令。这个工具是一个相对较新的命令,现在默认包含在 Red Hat Enterprise Linux 7 中。然而,在以前的 RHEL 版本中,这个命令并不总是可用的。
在采用iotop
之前,查找使用 I/O 最多的进程的方法涉及使用ps
命令并浏览/proc
文件系统。
ps - 使用 ps 命令识别利用 I/O 的进程
在收集与 CPU 相关的数据时,我们涵盖了ps
命令的输出中的状态字段。我们没有涵盖的是进程可能处于的各种状态。以下列表包含了ps
命令将显示的七种可能的状态:
-
不间断睡眠(
D
):进程通常在等待 I/O 时处于睡眠状态 -
运行或可运行(
R
):运行队列上的进程 -
可中断睡眠(
S
):等待事件完成但不阻塞 CPU 或 I/O 的进程 -
已停止(
T
):被作业控制系统停止的进程,如 jobs 命令 -
分页(
P
):当前正在分页的进程;但是,在较新的内核上,这不太相关 -
死亡(
X
):已经死亡的进程,不应该出现在运行ps
时 -
僵尸(
Z
):已终止但保留在不死状态的僵尸进程
在调查 I/O 利用率时,重要的是要识别状态列为D
的不间断睡眠。由于这些进程通常在等待 I/O,它们是最有可能过度利用磁盘 I/O 的进程。
为了做到这一点,我们将使用ps
命令和-e
(所有)、-l
(长格式)和-f
(完整格式)标志。我们还将再次使用管道将输出重定向到grep
命令,并将输出过滤为只显示具有D
状态的进程。
# ps -elf | grep " D "
1 D root 13185 2 2 80 0 - 0 get_re 00:21 ? 00:01:32 [kworker/u4:1]
4 D root 15639 15638 30 80 0 - 4233 balanc 01:26 pts/2 00:00:02 bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
从上面的输出中,我们看到有两个进程目前处于不间断睡眠状态。一个进程是kworker
,这是一个内核系统进程,另一个是bonnie++
,是由 root 用户启动的进程。由于kworker
进程是一个通用的内核进程,我们将首先关注bonnie++
进程。
为了更好地理解这个过程,我们将再次运行ps
命令,但这次使用--forest
选项。
# ps -elf –forest
4 S root 1007 1 0 80 0 - 20739 poll_s Feb07 ? 00:00:00 /usr/sbin/sshd -D
4 S root 11239 1007 0 80 0 - 32881 poll_s Feb08 ? 00:00:00 \_ sshd: vagrant [priv]
5 S vagrant 11242 11239 0 80 0 - 32881 poll_s Feb08 ? 00:00:02 \_ sshd: vagrant@pts/2
0 S vagrant 11243 11242 0 80 0 - 28838 wait Feb08 pts/2 00:00:01 \_ -bash
4 S root 16052 11243 0 80 0 - 47343 poll_s 01:39 pts/2 00:00:00 \_ sudo bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
4 S root 16053 16052 32 80 0 - 96398 hrtime 01:39 pts/2 00:00:03 \_ bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
通过审查上述输出,我们可以看到bonnie++
进程实际上是进程16052
的子进程,后者是11243
的另一个子进程,后者是vagrant
用户的 bash shell。
前面的ps
命令告诉我们,进程 ID 为16053
的bonnie++
进程正在等待 I/O 任务。但是,这并没有告诉我们这个进程正在使用多少 I/O;为了确定这一点,我们可以读取/proc
文件系统中的一个特殊文件,名为io
。
# cat /proc/16053/io
rchar: 1002448848
wchar: 1002438751
syscr: 122383
syscw: 122375
read_bytes: 1002704896
write_bytes: 1002438656
cancelled_write_bytes: 0
每个运行的进程在/proc
中都有一个与进程id
同名的子文件夹;对于我们的示例,这是/proc/16053
。这个文件夹由内核维护,用于每个运行的进程,在这些文件夹中存在许多包含有关运行进程信息的文件。
这些文件非常有用,它们实际上是ps
命令信息的来源之一。其中一个有用的文件名为io
;io
文件包含有关进程执行的读取和写入次数的统计信息。
从 cat 命令的输出中,我们可以看到这个进程已经读取和写入了大约 1GB 的数据。虽然这看起来很多,但可能是在很长一段时间内完成的。为了了解这个进程向磁盘写入了多少数据,我们可以再次读取这个文件以捕捉差异。
# cat /proc/16053/io
cat: /proc/16053/io: No such file or directory
然而,当我们第二次执行 cat 命令时,我们收到了一个错误,即io
文件不再存在。如果我们再次运行ps
命令并使用grep
在输出中搜索bonnie++
进程,我们会发现bonnie++
进程正在运行;但是,它是一个新的进程,具有新的进程ID
。
# ps -elf | grep bonnie
4 S root 17891 11243 0 80 0 - 47343 poll_s 02:34 pts/2 00:00:00 sudo bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
4 D root 17892 17891 33 80 0 - 4233 sleep_ 02:34 pts/2 00:00:02 bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
由于bonnie++
子进程是短暂的进程,通过读取io
文件来跟踪 I/O 统计可能会非常困难。
iotop - 一个用于磁盘 I/O 的类似 top 的命令
由于这些进程频繁启动和停止,我们可以使用iotop
命令来确定哪些进程最多地利用了 I/O。
# iotop
Total DISK READ : 102.60 M/s | Total DISK WRITE : 26.96 M/s
Actual DISK READ: 102.60 M/s | Actual DISK WRITE: 42.04 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
16395 be/4 root 0.00 B/s 0.00 B/s 0.00 % 45.59 % [kworker/u4:0]
18250 be/4 root 101.95 M/s 26.96 M/s 0.00 % 42.59 % bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
在iotop
的输出中,我们可以看到一些有趣的 I/O 统计信息。通过iotop
,我们不仅可以看到系统范围的统计信息,比如每秒的总磁盘读取和总磁盘写入,还可以看到单个进程的许多统计信息。
从每个进程的角度来看,我们可以看到bonnie++
进程正在以 101.96 MBps 的速度从磁盘读取数据,并以 26.96 MBps 的速度向磁盘写入数据。
16395 be/4 root 0.00 B/s 0.00 B/s 0.00 % 45.59 % [kworker/u4:0]
18250 be/4 root 101.95 M/s 26.96 M/s 0.00 % 42.59 % bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
iotop
命令与 top 命令非常相似,它会每隔几秒刷新报告的结果。这样做的效果是实时显示 I/O 统计信息。
提示
诸如top
和iotop
之类的命令在书本格式中很难展示。我强烈建议在具有这些命令的系统上执行这些命令,以了解它们的工作方式。
整合起来
现在我们已经介绍了一些用于故障排除磁盘性能和利用率的工具,让我们在解决报告的缓慢时将它们整合起来。
使用 iostat 来确定是否存在 I/O 带宽问题
我们将首先运行的命令是iostat
,因为这将首先为我们验证是否确实存在问题。
# iostat -x 10 3
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
38.58 0.00 3.22 5.46 0.00 52.75
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 10.86 4.25 122.46 118.15 11968.97 12065.60 199.78 13.27 55.18 0.67 111.67 0.51 12.21
dm-0 0.00 0.00 14.03 3.44 56.14 13.74 8.00 0.42 24.24 0.51 121.15 0.46 0.80
dm-1 0.00 0.00 119.32 112.35 11912.79 12051.98 206.89 13.52 58.33 0.68 119.55 0.52 12.16
avg-cpu: %user %nice %system %iowait %steal %idle
7.96 0.00 14.60 29.31 0.00 48.12
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.70 0.80 804.49 776.85 79041.12 76999.20 197.35 64.26 41.41 0.54 83.73 0.42 66.38
dm-0 0.00 0.00 0.90 0.80 3.59 3.19 8.00 0.08 50.00 0.00 106.25 19.00 3.22
dm-1 0.00 0.00 804.29 726.35 79037.52 76893.81 203.75 64.68 43.03 0.53 90.08 0.44 66.75
avg-cpu: %user %nice %system %iowait %steal %idle
5.22 0.00 11.21 36.21 0.00 47.36
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 1.10 0.30 749.40 429.70 84589.20 43619.80 217.47 76.31 66.49 0.43 181.69 0.58 68.32
dm-0 0.00 0.00 1.30 0.10 5.20 0.40 8.00 0.00 2.21 1.00 18.00 1.43 0.20
dm-1 0.00 0.00 749.00 391.20 84558.40 41891.80 221.80 76.85 69.23 0.43 200.95 0.60 68.97
从iostat
的输出中,我们可以确定以下信息:
-
该系统的 CPU 目前花费了相当多的时间在等待 I/O,占 30%–40%。
-
看起来
dm-1
和sda
设备是利用率最高的设备 -
从
iostat
来看,这些设备的利用率为 68%,这个数字似乎相当高
基于这些数据点,我们可以确定存在潜在的 I/O 利用率问题,除非 68%的利用率是预期的。
使用 iotop 来确定哪些进程正在消耗磁盘带宽
现在我们已经确定了大量的 CPU 时间被用于等待 I/O,我们现在应该关注哪些进程最多地利用了磁盘。为了做到这一点,我们将使用iotop
命令。
# iotop
Total DISK READ : 100.64 M/s | Total DISK WRITE : 23.91 M/s
Actual DISK READ: 100.67 M/s | Actual DISK WRITE: 38.04 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
19358 be/4 root 0.00 B/s 0.00 B/s 0.00 % 40.38 % [kworker/u4:1]
20262 be/4 root 100.35 M/s 23.91 M/s 0.00 % 33.65 % bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
363 be/4 root 0.00 B/s 0.00 B/s 0.00 % 2.51 % [xfsaild/dm-1]
32 be/4 root 0.00 B/s 0.00 B/s 0.00 % 1.74 % [kswapd0]
从iotop
命令中,我们可以看到进程20262
,它正在运行bonnie++
命令,具有高利用率以及大量的磁盘读写值。
从iotop
中,我们可以确定以下信息:
-
系统的每秒总磁盘读取量为 100.64 MBps
-
系统的每秒总磁盘写入量为 23.91 MBps
-
运行
bonnie++
命令的进程20262
正在读取 100.35 MBps,写入 23.91 MBps -
比较总数,我们发现进程
20262
是磁盘读写的主要贡献者
鉴于上述情况,似乎我们需要更多地了解进程20262
的信息。
使用 ps 来更多地了解进程
现在我们已经确定了一个使用大量 I/O 的进程,我们可以使用ps
命令来调查这个进程的详细信息。我们将再次使用带有--forest
标志的ps
命令来显示父进程和子进程的关系。
# ps -elf --forest
1007 0 80 0 - 32881 poll_s Feb08 ? 00:00:00 \_ sshd: vagrant [priv]
5 S vagrant 11242 11239 0 80 0 - 32881 poll_s Feb08 ? 00:00:05 \_ sshd: vagrant@pts/2
0 S vagrant 11243 11242 0 80 0 - 28838 wait Feb08 pts/2 00:00:02 \_ -bash
4 S root 20753 11243 0 80 0 - 47343 poll_s 03:52 pts/2 00:00:00 \_ sudo bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
4 D root 20754 20753 52 80 0 - 4233 sleep_ 03:52 pts/2 00:00:01 \_ bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp
使用ps
命令,我们可以确定以下内容:
-
用
iotop
识别的bonnie++
进程20262
不见了;然而,其他bonnie++
进程存在 -
vagrant
用户已经通过使用sudo
命令启动了父bonnie++
进程 -
vagrant
用户与早期观察中讨论的 CPU 和内存部分的用户相同
鉴于上述细节,似乎vagrant
用户是我们性能问题的嫌疑人。
网络
性能问题的最后一个常见资源是网络。有许多工具可以用来排除网络问题;然而,这些命令中很少有专门针对网络性能的。大多数这些工具都是为深入的网络故障排除而设计的。
由于第五章,网络故障排除专门用于解决网络问题,本节将专门关注性能。
ifstat - 查看接口统计
在网络方面,有大约四个指标可以用来衡量吞吐量。
-
接收数据包:接口接收的数据包数量
-
发送数据包:接口发送的数据包数量
-
接收数据:接口接收的数据量
-
发送数据:接口发送的数据量
有许多命令可以提供这些指标,从ifconfig
或ip
到netstat
都有。一个非常有用的专门输出这些指标的实用程序是ifstat
命令。
# ifstat
#21506.1804289383 sampling_interval=5 time_const=60
Interface RX Pkts/Rate TX Pkts/Rate RX Data/Rate TX Data/Rate
RX Errs/Drop TX Errs/Drop RX Over/Rate TX Coll/Rate
lo 47 0 47 0 4560 0 4560 0
0 0 0 0 0 0 0 0
enp0s3 70579 1 50636 0 17797K 65 5520K 96
0 0 0 0 0 0 0 0
enp0s8 23034 0 43 0 2951K 18 7035 0
0 0 0 0 0 0 0 0
与vmstat
或iostat
类似,ifstat
生成的第一个报告是基于服务器上次重启以来的统计数据。这意味着上面的报告表明enp0s3
接口自上次重启以来已接收了 70,579 个数据包。
当第二次执行ifstat
时,结果将与第一个报告有很大的差异。原因是第二个报告是基于自第一个报告以来的时间。
# ifstat
#21506.1804289383 sampling_interval=5 time_const=60
Interface RX Pkts/Rate TX Pkts/Rate RX Data/Rate TX Data/Rate
RX Errs/Drop TX Errs/Drop RX Over/Rate TX Coll/Rate
lo 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
enp0s3 23 0 18 0 1530 59 1780 80
0 0 0 0 0 0 0 0
enp0s8 1 0 0 0 86 10 0 0
0 0 0 0 0 0 0 0
在上面的例子中,我们可以看到我们的系统通过enp0s3
接口接收了 23 个数据包(RX Pkts)并发送了 18 个数据包(TX Pkts
)。
通过ifstat
命令,我们可以确定以下关于我们的系统的内容:
-
目前的网络利用率相当小,不太可能对整个系统造成影响
-
早期显示的
vagrant
用户的进程不太可能利用大量网络资源
根据ifstat
所见的统计数据,在这个系统上几乎没有网络流量,不太可能导致感知到的缓慢。
对我们已经确定的内容进行快速回顾
在继续之前,让我们回顾一下到目前为止我们从性能统计数据中学到的东西:
注意
vagrant
用户一直在启动运行bonnie++
和lookbusy
应用程序的进程。
lookbusy
应用程序似乎要么一直占用整个系统 CPU 的 20%–30%。
这个服务器有两个 CPU,lookbusy
似乎一直占用一个 CPU 的大约 60%。
lookbusy
应用程序似乎也一直使用大约 200 MB 的内存;然而,在故障排除期间,我们确实看到这些进程几乎使用了系统的所有内存,导致系统交换。
在启动bonnie++
进程时,vagrant
用户的系统经历了高 I/O 等待时间。
在运行时,bonnie++
进程利用了大约 60%–70%的磁盘吞吐量。
vagrant
用户正在执行的活动似乎对网络利用率几乎没有影响。
比较历史指标
从迄今为止我们了解到的所有事实来看,我们下一个最佳行动方案似乎是建议联系vagrant
用户,以确定lookbusy
和bonnie++
应用程序是否应该以如此高的资源利用率运行。
尽管先前的观察显示了高资源利用率,但这种利用率水平可能是预期的。在开始联系用户之前,我们应该首先审查服务器的历史性能指标。在大多数环境中,都会有一些服务器性能监控软件,如 Munin、Cacti 或许多云 SaaS 提供商之一,用于收集和存储系统统计信息。
如果您的环境使用了这些服务,您可以使用收集的性能数据来将以前的性能统计与我们刚刚收集到的信息进行比较。例如,在过去 30 天中,CPU 性能从未超过 10%,那么lookbusy
进程可能在那个时候没有运行。
即使您的环境没有使用这些工具之一,您仍然可以执行历史比较。为此,我们将使用一个默认安装在大多数 Red Hat Enterprise Linux 系统上的工具;这个工具叫做sar
。
sar – 系统活动报告
在第二章,故障排除命令和有用信息来源中,我们简要讨论了使用sar
命令来查看历史性能统计信息。
当安装了部署sar
实用程序的sysstat
软件包时,它将部署/etc/cron.d/sysstat
文件。在这个文件中有两个cron
作业,运行sysstat
命令,其唯一目的是收集系统性能统计信息并生成收集信息的报告。
$ cat /etc/cron.d/sysstat
# Run system activity accounting tool every 10 minutes
*/2 * * * * root /usr/lib64/sa/sa1 1 1
# 0 * * * * root /usr/lib64/sa/sa1 600 6 &
# Generate a daily summary of process accounting at 23:53
53 23 * * * root /usr/lib64/sa/sa2 -A
当执行这些命令时,收集的信息将存储在/var/log/sa/
文件夹中。
# ls -la /var/log/sa/
total 1280
drwxr-xr-x. 2 root root 4096 Feb 9 00:00 .
drwxr-xr-x. 9 root root 4096 Feb 9 03:17 ..
-rw-r--r--. 1 root root 68508 Feb 1 23:20 sa01
-rw-r--r--. 1 root root 40180 Feb 2 16:00 sa02
-rw-r--r--. 1 root root 28868 Feb 3 05:30 sa03
-rw-r--r--. 1 root root 91084 Feb 4 20:00 sa04
-rw-r--r--. 1 root root 57148 Feb 5 23:50 sa05
-rw-r--r--. 1 root root 34524 Feb 6 23:50 sa06
-rw-r--r--. 1 root root 105224 Feb 7 23:50 sa07
-rw-r--r--. 1 root root 235312 Feb 8 23:50 sa08
-rw-r--r--. 1 root root 105224 Feb 9 06:00 sa09
-rw-r--r--. 1 root root 56616 Jan 23 23:00 sa23
-rw-r--r--. 1 root root 56616 Jan 24 20:10 sa24
-rw-r--r--. 1 root root 24648 Jan 30 23:30 sa30
-rw-r--r--. 1 root root 11948 Jan 31 23:20 sa31
-rw-r--r--. 1 root root 44476 Feb 5 23:53 sar05
-rw-r--r--. 1 root root 27244 Feb 6 23:53 sar06
-rw-r--r--. 1 root root 81094 Feb 7 23:53 sar07
-rw-r--r--. 1 root root 180299 Feb 8 23:53 sar08
sysstat
软件包生成的数据文件使用遵循“sa<两位数的日期>
”格式的文件名。例如,在上面的输出中,我们可以看到“sa24
”文件是在 1 月 24 日生成的。我们还可以看到这个系统有从 1 月 23 日到 2 月 9 日的文件。
sar
命令是一个允许我们读取这些捕获的性能指标的命令。本节将向您展示如何使用sar
命令来查看与iostat
、top
和vmstat
等命令之前查看的相同统计信息。然而,这次sar
命令将提供最近和历史信息。
CPU
要使用sar
命令查看 CPU 统计信息,我们可以简单地使用–u
(CPU 利用率)标志。
# sar -u
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU)
12:00:01 AM CPU %user %nice %system %iowait %steal %idle
12:10:02 AM all 7.42 0.00 13.46 37.51 0.00 41.61
12:20:01 AM all 7.59 0.00 13.61 38.55 0.00 40.25
12:30:01 AM all 7.44 0.00 13.46 38.50 0.00 40.60
12:40:02 AM all 8.62 0.00 15.71 31.42 0.00 44.24
12:50:02 AM all 8.77 0.00 16.13 29.66 0.00 45.44
01:00:01 AM all 8.88 0.00 16.20 29.43 0.00 45.49
01:10:01 AM all 7.46 0.00 13.64 37.29 0.00 41.61
01:20:02 AM all 7.35 0.00 13.52 37.79 0.00 41.34
01:30:01 AM all 7.40 0.00 13.36 38.60 0.00 40.64
01:40:01 AM all 7.42 0.00 13.53 37.86 0.00 41.19
01:50:01 AM all 7.44 0.00 13.58 38.38 0.00 40.60
04:20:02 AM all 7.51 0.00 13.72 37.56 0.00 41.22
04:30:01 AM all 7.34 0.00 13.36 38.56 0.00 40.74
04:40:02 AM all 7.40 0.00 13.41 37.94 0.00 41.25
04:50:01 AM all 7.45 0.00 13.81 37.73 0.00 41.01
05:00:02 AM all 7.49 0.00 13.75 37.72 0.00 41.04
05:10:01 AM all 7.43 0.00 13.30 39.28 0.00 39.99
05:20:02 AM all 7.24 0.00 13.17 38.52 0.00 41.07
05:30:02 AM all 13.47 0.00 11.10 31.12 0.00 44.30
05:40:01 AM all 67.05 0.00 1.92 0.00 0.00 31.03
05:50:01 AM all 68.32 0.00 1.85 0.00 0.00 29.82
06:00:01 AM all 69.36 0.00 1.76 0.01 0.00 28.88
06:10:01 AM all 70.53 0.00 1.71 0.01 0.00 27.76
Average: all 14.43 0.00 12.36 33.14 0.00 40.07
如果我们从上面的头信息中查看,我们可以看到带有-u
标志的sar
命令与iostat
和 top CPU 详细信息相匹配。
12:00:01 AM CPU %user %nice %system %iowait %steal %idle
从sar -u
的输出中,我们可以发现一个有趣的趋势:从 00:00 到 05:30,CPU I/O 等待时间保持在 30%–40%。然而,从 05:40 开始,I/O 等待时间减少,但用户级 CPU 利用率增加到 65%–70%。
尽管这两个测量并没有明确指向任何一个过程,但它们表明 I/O 等待时间最近已经减少,而用户 CPU 时间已经增加。
为了更好地了解历史统计信息,我们需要查看前一天的 CPU 利用率。幸运的是,我们可以使用–f
(文件名)标志来做到这一点。–f
标志将允许我们为sar
命令指定一个历史文件。这将允许我们有选择地查看前一天的统计信息。
# sar -f /var/log/sa/sa07 -u
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/07/2015 _x86_64_ (2 CPU)
12:00:01 AM CPU %user %nice %system %iowait %steal %idle
12:10:01 AM all 24.63 0.00 0.71 0.00 0.00 74.66
12:20:01 AM all 25.31 0.00 0.70 0.00 0.00 73.99
01:00:01 AM all 27.59 0.00 0.68 0.00 0.00 71.73
01:10:01 AM all 29.64 0.00 0.71 0.00 0.00 69.65
05:10:01 AM all 44.09 0.00 0.63 0.00 0.00 55.28
05:20:01 AM all 60.94 0.00 0.58 0.00 0.00 38.48
05:30:01 AM all 62.32 0.00 0.56 0.00 0.00 37.12
05:40:01 AM all 63.74 0.00 0.56 0.00 0.00 35.70
05:50:01 AM all 65.08 0.00 0.56 0.00 0.00 34.35
0.00 76.07
Average: all 37.98 0.00 0.65 0.00 0.00 61.38
在 2 月 7 日的报告中,我们可以看到 CPU 利用率与我们之前的故障排除所发现的情况有很大的不同。一个突出的问题是,在 7 日的报告中,没有 CPU 时间花费在 I/O 等待状态。
然而,我们看到用户 CPU 时间根据一天中的时间波动从 20%到 65%不等。这可能表明预期会有更高的用户 CPU 时间利用率。
内存
要显示内存统计信息,我们可以使用带有-r
(内存)标志的sar
命令。
# sar -r
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU)
12:00:01 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
12:10:02 AM 38228 463832 92.39 0 387152 446108 28.17 196156 201128 0
12:20:01 AM 38724 463336 92.29 0 378440 405128 25.59 194336 193216 73360
12:30:01 AM 38212 463848 92.39 0 377848 405128 25.59 9108 379348 58996
12:40:02 AM 37748 464312 92.48 0 387500 446108 28.17 196252 201684 0
12:50:02 AM 33028 469032 93.42 0 392240 446108 28.17 196872 205884 0
01:00:01 AM 34716 467344 93.09 0 380616 405128 25.59 195900 195676 69332
01:10:01 AM 31452 470608 93.74 0 384092 396660 25.05 199100 196928 74372
05:20:02 AM 38756 463304 92.28 0 387120 399996 25.26 197184 198456 4
05:30:02 AM 187652 314408 62.62 0 19988 617000 38.97 222900 22524 0
05:40:01 AM 186896 315164 62.77 0 20116 617064 38.97 223512 22300 0
05:50:01 AM 186824 315236 62.79 0 20148 617064 38.97 223788 22220 0
06:00:01 AM 182956 319104 63.56 0 24652 615888 38.90 226744 23288 0
06:10:01 AM 176992 325068 64.75 0 29232 615880 38.90 229356 26500 0
06:20:01 AM 176756 325304 64.79 0 29480 615884 38.90 229448 26588 0
06:30:01 AM 176636 325424 64.82 0 29616 615888 38.90 229516 26820 0
Average: 77860 424200 84.49 0 303730 450102 28.43 170545 182617 29888
再次,如果我们查看sar
的内存报告标题,我们可以看到一些熟悉的值。
12:00:01 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
从这份报告中,我们可以看到在 05:40 时,系统突然释放了 150MB 的物理内存。从kbcached
列可以看出,这 150MB 的内存被分配给了磁盘缓存。这是基于 05:40 时,缓存内存从 196MB 下降到 22MB 的事实。
有趣的是,这与 CPU 利用率的变化在 05:40 也是一致的。如果我们希望回顾历史内存利用情况,我们也可以使用带有-f
(文件名)标志和-r
(内存)标志。然而,由于我们可以看到 05:40 有一个相当明显的趋势,我们现在将重点放在这个时间上。
磁盘
要显示今天的磁盘统计信息,我们可以使用-d
(块设备)标志。
# sar -d
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU)
12:00:01 AM DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util
12:10:02 AM dev8-0 1442.64 150584.15 146120.49 205.67 82.17 56.98 0.51 74.17
12:10:02 AM dev253-0 1.63 11.11 1.96 8.00 0.06 34.87 19.72 3.22
12:10:02 AM dev253-1 1402.67 150572.19 146051.96 211.47 82.73 58.98 0.53 74.68
04:20:02 AM dev8-0 1479.72 152799.09 150240.77 204.80 81.27 54.89 0.50 73.86
04:20:02 AM dev253-0 1.74 10.98 2.96 8.00 0.06 31.81 14.60 2.54
04:20:02 AM dev253-1 1438.57 152788.11 150298.01 210.69 81.84 56.83 0.52 74.38
05:30:02 AM dev253-0 1.00 7.83 0.17 8.00 0.00 3.81 2.76 0.28
05:30:02 AM dev253-1 1170.61 123647.27 122655.72 210.41 69.12 59.04 0.53 62.20
05:40:01 AM dev8-0 0.08 1.00 0.34 16.10 0.00 1.88 1.00 0.01
05:40:01 AM dev253-0 0.11 0.89 0.00 8.00 0.00 1.57 0.25 0.00
05:40:01 AM dev253-1 0.05 0.11 0.34 8.97 0.00 2.77 1.17 0.01
05:50:01 AM dev8-0 0.07 0.49 0.28 11.10 0.00 1.71 1.02 0.01
05:50:01 AM dev253-0 0.06 0.49 0.00 8.00 0.00 2.54 0.46 0.00
05:50:01 AM dev253-1 0.05 0.00 0.28 6.07 0.00 1.96 0.96 0.00
Average: DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util
Average: dev8-0 1215.88 125807.06 123583.62 205.11 66.86 55.01 0.50 60.82
Average: dev253-0 2.13 12.48 4.53 8.00 0.10 44.92 17.18 3.65
Average: dev253-1 1181.94 125794.56 123577.42 210.99 67.31 56.94 0.52 61.17
默认情况下,sar
命令将打印设备名称为“dev<major>-<minor>
”,这可能有点令人困惑。如果添加了-p
(持久名称)标志,设备名称将使用持久名称,与挂载命令中的设备匹配。
# sar -d -p
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 08/16/2015 _x86_64_ (4 CPU)
01:46:42 AM DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util
01:48:01 AM sda 0.37 0.00 3.50 9.55 0.00 1.86 0.48 0.02
01:48:01 AM rhel-swap 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
01:48:01 AM rhel-root 0.37 0.00 3.50 9.55 0.00 2.07 0.48 0.02
即使名称以不可识别的格式显示,我们也可以看到dev253-1
似乎在 05:40 之前有相当多的活动,磁盘tps
(每秒事务)从 1170 下降到 0.11。磁盘 I/O 利用率的大幅下降似乎表明今天在 05:40 发生了相当大的变化。
网络
要显示网络统计信息,我们需要使用带有-n DEV
标志的sar
命令。
# sar -n DEV
Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU)
12:00:01 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
12:10:02 AM enp0s3 1.51 1.18 0.10 0.12 0.00 0.00 0.00
12:10:02 AM enp0s8 0.14 0.00 0.02 0.00 0.00 0.00 0.07
12:10:02 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:20:01 AM enp0s3 0.85 0.85 0.05 0.08 0.00 0.00 0.00
12:20:01 AM enp0s8 0.18 0.00 0.02 0.00 0.00 0.00 0.08
12:20:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
12:30:01 AM enp0s3 1.45 1.16 0.10 0.11 0.00 0.00 0.00
12:30:01 AM enp0s8 0.18 0.00 0.03 0.00 0.00 0.00 0.08
12:30:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
05:20:02 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
05:30:02 AM enp0s3 1.23 1.02 0.08 0.11 0.00 0.00 0.00
05:30:02 AM enp0s8 0.15 0.00 0.02 0.00 0.00 0.00 0.04
05:30:02 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
05:40:01 AM enp0s3 0.79 0.78 0.05 0.14 0.00 0.00 0.00
05:40:01 AM enp0s8 0.18 0.00 0.02 0.00 0.00 0.00 0.08
05:40:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
05:50:01 AM enp0s3 0.76 0.75 0.05 0.13 0.00 0.00 0.00
05:50:01 AM enp0s8 0.16 0.00 0.02 0.00 0.00 0.00 0.07
05:50:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
06:00:01 AM enp0s3 0.67 0.60 0.04 0.10 0.00 0.00 0.00
在网络统计报告中,我们看到整天都没有变化。这表明,总体上,这台服务器从未出现与网络性能瓶颈相关的问题。
通过比较历史统计数据来回顾我们所学到的内容
通过使用sar
查看历史统计数据和使用ps
、iostat
、vmstat
和top
等命令查看最近的统计数据后,我们可以得出关于我们的“性能慢”的以下结论。
由于我们被同事要求调查这个问题,我们的结论将以电子邮件回复的形式发送给这位同事。
嗨鲍勃!
我调查了一个用户说服务器“慢”的服务器。看起来用户 vagrant 一直在运行两个主要程序的多个实例。第一个是 lookbusy 应用程序,似乎始终使用大约 20%–40%的 CPU。然而,至少有一个实例中,lookbusy 应用程序还使用了大量内存,耗尽了物理内存并迫使系统大量交换。然而,这个过程并没有持续很长时间。
第二个程序是 bonnie++应用程序,似乎利用了大量的磁盘 I/O 资源。当 vagrant 用户运行 bonnie++应用程序时,它占用了大约 60%的 dm-1 和 sda 磁盘带宽,导致了大约 30%的高 I/O 等待。通常,这个系统的 I/O 等待为 0%(通过 sar 确认)。
看起来 vagrant 用户可能正在运行超出预期水平的应用程序,导致其他用户的性能下降。
总结
在本章中,我们开始使用一些高级的 Linux 命令,这些命令在第二章中进行了探索,例如iostat
和vmstat
。我们还对 Linux 中的一个基本实用程序ps
命令非常熟悉,同时解决了一个模糊的性能问题。
在第三章中,故障排除 Web 应用程序,我们能够从数据收集到试错的完整故障排除过程,而在本章中,我们的行动主要集中在数据收集和建立假设阶段。发现自己只是在解决问题而不是执行纠正措施是非常常见的。有许多问题应该由系统的用户而不是系统管理员来解决,但管理员的角色仍然是识别问题的来源。
在第五章中,网络故障排除,我们将解决一些非常有趣的网络问题。网络对于任何系统都至关重要;问题有时可能很简单,而有时则非常复杂。在下一章中,我们将探讨网络和如何使用诸如netstat
和tcpdump
之类的工具来排除网络问题。
第五章:网络故障排除
在第三章中,故障排除 Web 应用程序,我们深入研究了故障排除 Web 应用程序;虽然我们解决了一个复杂的应用程序错误,但我们完全跳过了 Web 应用程序的网络方面。在本章中,我们将调查一个报告的问题,这将引导我们了解 DNS、路由,当然还有 RHEL 系统的网络配置等概念。
对于任何 Linux 系统管理员来说,网络是一项必不可少的技能。引用一位过去的讲师的话:
没有网络的服务器对每个人都是无用的。
作为系统管理员,您管理的每台服务器或台式机都将有某种网络连接。无论这种网络连接是在隔离的公司网络内还是直接连接到互联网,都涉及到网络。
由于网络是一个如此关键的主题,本章将涵盖网络和网络连接的许多方面;然而,它不会涵盖防火墙。防火墙故障排除和配置实际上将在第六章中进行,诊断和纠正防火墙问题。
数据库连接问题
在第三章中,故障排除 Web 应用程序,我们正在解决公司博客的问题。在本章中,我们将再次解决这个博客;然而,今天的问题有点不同。
到达当天后,我们接到一位开发人员的电话,他说:“WordPress 博客返回了一个无法连接到数据库的错误”。
数据收集
根据我们一直遵循的故障排除过程,下一步是尽可能收集关于问题的数据。信息的最佳来源之一是报告问题的人;对于这种情况,我们将问两个基本问题:
-
我如何复制问题并查看错误?
-
最近 WordPress 应用有什么变化吗?
当被问及时,开发人员表示我们只需在网页浏览器中访问博客就可以看到错误。在第二个问题上,开发人员告诉我们,数据库服务最近从 Web 服务器移动到了一个新的专用数据库服务器。他还提到这个移动是在几天前发生的,并且应用程序一直到今天都在工作。
由于数据库服务是几天前移动的,而且应用程序直到今天早上都在工作,所以这个改变不太可能引起问题。然而,我们不应该排除这种可能性。
复制问题
正如前几章讨论的,关键的数据收集任务是复制问题。我们这样做不仅是为了验证报告的问题是否确实存在,还为了找出可能没有被报告的任何其他错误。
由于开发人员表示我们可以直接访问博客来复制这个问题,我们将在网页浏览器中进行操作。
似乎我们可以很容易地复制这个问题。根据这个错误,似乎应用程序只是在说它在建立数据库连接时出现了问题。虽然这本身并不意味着问题与网络有关,但也可能是。问题也可能只是数据库服务本身的问题。
为了确定问题是网络问题还是数据库服务问题,我们首先需要找出应用程序配置为连接到哪个服务器。
查找数据库服务器
与上一章类似,我们将通过查看应用程序配置文件来确定应用程序使用的服务器。根据我们在第三章中的先前故障排除,故障排除 Web 应用程序,我们知道 WordPress 应用程序托管在blog.example.com
上。首先,我们将登录到博客的 Web 服务器并查看 WordPress 配置文件。
$ ssh blog.example.com -l vagrant
vagrant@blog.example.com's password:
Last login: Sat Feb 28 18:49:40 2015 from 10.0.2.2
[blog]$
提示
由于我们将针对多个系统执行命令,因此本章的示例将在命令行提示中包含主机名,如blog
或db
。
我们在第三章中学到,WordPress 数据库配置存储在/var/www/html/wp-config.php
文件中。为了快速搜索该文件以获取数据库信息,我们可以使用grep
命令搜索字符串DB
,因为在我们先前的事件中,该字符串出现在数据库配置中。
[blog]$ grep DB wp-config.php
define('DB_NAME', 'wordpress');
define('DB_USER', 'wordpress');
define('DB_PASSWORD', 'password');
define('DB_HOST', 'db.example.com');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
通过上述内容,我们可以看到该应用程序当前配置为连接到db.example.com
。简单的第一步故障排除是尝试手动连接到数据库。手动测试数据库连接的简单方法是使用telnet
命令。
测试连接
telnet
命令是一个非常有用的网络和网络服务故障排除工具,因为它旨在简单地建立到指定主机和端口的基于 TCP 的网络连接。在我们的例子中,我们将尝试连接到主机db.example.com
的端口3306
。
端口3306
是 MySQL 和 MariaDB 的默认端口;在上一章中,我们已经确定了这个 Web 应用程序需要这两个数据库服务中的一个。由于在wp-config.php
文件的配置中没有看到特定的端口,我们将假设数据库服务正在运行在这个默认端口上。
从 blog.example.com 进行 Telnet
首先,我们将从博客服务器本身执行telnet
命令。从应用程序运行的同一服务器进行测试非常重要,因为这样可以在应用程序接收到错误的相同网络条件下进行测试。
为了使用 telnet 连接到我们的数据库服务器,我们将执行telnet
命令,后面跟着我们希望连接到的主机名(db.example.com
)和端口(3306
)。
[blog]$ telnet db.example.com 3306
Trying 192.168.33.12...
telnet: connect to address 192.168.33.12: No route to host
Telnet 连接似乎失败了。有趣的是提供的错误;无法连接到主机错误似乎清楚地指示了潜在的网络问题。
从我们的笔记本电脑进行 Telnet
由于从博客服务器的连接尝试失败,并指示存在与网络相关的问题,我们可以尝试从我们的笔记本电脑进行相同的连接,以确定问题是在博客服务器端还是db
服务器端。
为了从我们的笔记本电脑测试这种连接,我们可以再次使用telnet
命令。尽管我们的笔记本电脑不一定运行 Linux 操作系统,但我们仍然可以使用这个命令。原因是telnet
命令是一个跨平台实用程序;在本章中,我们将利用几个跨平台命令。虽然这样的命令可能不多,但一般来说,有几个命令适用于大多数操作系统,包括那些传统上没有广泛命令行功能的系统。
虽然一些操作系统已经从默认安装中删除了telnet
客户端,但该软件仍然可以安装。在我们的例子中,笔记本电脑正在运行 OS X,该系统目前部署了telnet
客户端。
[laptop]$ telnet db.example.com 3306
Trying 10.0.0.50...
Connected to 10.0.0.50.
Escape character is '^]'.
Connection closed by foreign host.
看起来我们的笔记本也无法连接到数据库服务;然而,这次错误不同。这次似乎表明连接尝试被远程服务关闭。我们也没有看到来自远程服务的消息,这表明连接从未完全建立。
使用telnet
命令建立端口可用性的一个注意事项是,telnet
命令将显示连接为已连接;然而,此时连接可能并没有真正建立。在使用 telnet 时的一般规则是,在收到来自远程服务的消息之前,不要假设连接成功。在我们的例子中,我们没有收到来自远程服务的消息。
Ping
由于博客服务器和我们的笔记本都无法从“db”服务器进行 telnet 连接,我们应该检查问题是否仅限于数据库服务或整个服务器的连接。测试服务器之间的连接的一个工具是ping
命令,就像telnet
命令一样是一个跨平台实用程序。
要使用ping
命令测试与主机的连接性,我们只需执行命令,然后跟随我们希望ping
的主机。
[blog]$ ping db.example.com
PING db.example.com (192.168.33.12) 56(84) bytes of data.
From blog.example.com (192.168.33.11) icmp_seq=1 Destination Host Unreachable
From blog.example.com (192.168.33.11) icmp_seq=2 Destination Host Unreachable
From blog.example.com (192.168.33.11) icmp_seq=3 Destination Host Unreachable
From blog.example.com (192.168.33.11) icmp_seq=4 Destination Host Unreachable
^C
--- db.example.com ping statistics ---
6 packets transmitted, 0 received, +4 errors, 100% packet loss, time 5008ms
ping
命令的错误似乎与telnet
命令的错误非常相似。为了更好地理解这个错误,让我们首先更好地了解ping
命令的工作原理。
首先,在执行任何其他操作之前,ping
命令将尝试解析提供的主机名。这意味着在执行任何其他操作之前,我们的 ping 执行尝试识别db.example.com
的 IP 地址。
PING db.example.com (192.168.33.12) 56(84) bytes of data.
从结果中,我们可以看到ping
命令将此主机解析为192.168.33.12
。一旦 ping 有了 IP 地址,它将向该 IP 发送一个ICMP
回显请求网络数据包。在这种情况下,这意味着它正在向192.168.33.12
发送一个ICMP
回显请求。
ICMP 是一种用作控制系统的网络协议。当远程主机,比如192.168.33.12
接收到ICMP
回显请求网络数据包时,它应该发送一个ICMP
回显回复网络数据包回到请求的主机。这种活动允许两个主机通过进行简单的网络版本的“乒乓球”来验证网络连接。
From blog.example.com (192.168.33.11) icmp_seq=1 Destination Host Unreachable
如果我们的ICMP
回显请求数据包从192.168.33.12
服务器没有传输过来,我们的ping
命令就不会有任何输出。然而,我们收到了一个错误;这意味着另一端的系统是开启的,但两个主机之间的连接存在问题,阻止了完全的双向交流。
围绕这个问题出现的一个问题是,这个错误是否适用于博客服务器的所有网络连接,还是仅限于blog
和db
服务器之间的通信。我们可以通过向另一个通用地址执行ping
请求来测试这一点。由于我们的系统连接到互联网,我们可以简单地使用一个常见的互联网域名。
# ping google.com
PING google.com (216.58.216.46) 56(84) bytes of data.
64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=1 ttl=63 time=23.5 ms
64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=2 ttl=63 time=102 ms
64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=3 ttl=63 time=26.9 ms
64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=4 ttl=63 time=25.6 ms
64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=5 ttl=63 time=25.6 ms
^C
--- google.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4106ms
rtt min/avg/max/mdev = 23.598/40.799/102.156/30.697 ms
前面的例子是一个工作的ping
请求和回复的例子。在这里,我们不仅可以看到Google.com解析为的 IP,还可以看到返回的ping
请求。这意味着,当我们的博客服务器发送一个“ICMP 回显请求”时,远程服务器216.58.216.46
会发送一个“ICMP 回显回复”。
故障排除 DNS
除了网络连接之外,ping
和telnet
命令告诉我们的另一件有趣的事情是db.example.com
主机名的 IP 地址。然而,当我们从我们的笔记本执行这些操作时,结果似乎与从博客服务器执行这些操作时不同。
从博客服务器,我们的telnet
尝试连接到192.168.33.12
,与我们的ping
命令相同的地址。
[blog]$ telnet db.example.com 3306
Trying 192.168.33.12...
However, from the laptop, our telnet tried to connect to 10.0.0.50, a completely different IP address.
[laptop]$ telnet db.example.com 3306
Trying 10.0.0.50...
原因很简单;看起来我们的笔记本得到了与我们的博客服务器不同的 DNS 结果。然而,如果是这种情况,这可能意味着我们的问题可能只是与 DNS 问题有关。
使用 dig 检查 DNS
DNS 是现代网络的重要组成部分。我们当前的问题就是它重要性的一个完美例子。在 WordPress 配置文件中,我们的数据库服务器设置为db.example.com
。这意味着在应用服务器建立数据库连接之前,必须首先查找 IP 地址。
在许多情况下,可以相当安全地假设ping
识别的 IP 地址很可能是 DNS 呈现的 IP 地址。然而,并非总是如此,正如我们可能很快发现的那样。
dig
命令是一个非常有用的 DNS 故障排除命令;它非常灵活,可以用来执行许多不同类型的 DNS 请求。要验证db.example.com
的 DNS,我们只需执行dig
命令,然后跟上我们要查询的主机名:db.example.com
。
[blog]$ dig db.example.com
; <<>> DiG 9.9.4-RedHat-9.9.4-14.el7_0.1 <<>> db.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15857
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;db.example.com. IN A
;; ANSWER SECTION:
db.example.com. 15 IN A 10.0.0.50
;; Query time: 39 msec
;; SERVER: 10.0.2.3#53(10.0.2.3)
;; WHEN: Sun Mar 01 20:51:22 UTC 2015
;; MSG SIZE rcvd: 59
如果我们查看dig
返回的数据,我们可以看到 DNS 名称db.example.com
解析为192.168.33.12
,而不是10.0.0.50
。我们可以在dig
命令的输出的ANSWER SECTION
中看到这一点。
;; ANSWER SECTION:
db.example.com. 15 IN A 10.0.0.50
dig
的一个非常有用的选项是指定要查询的服务器。在之前执行的dig
中,我们可以看到服务器10.0.2.3
是提供10.0.0.50
地址的服务器。
;; Query time: 39 msec
;; SERVER: 10.0.2.3#53(10.0.2.3)
由于我们对这个 DNS 服务器不熟悉,我们可以通过查询谷歌的公共 DNS 服务器来进一步验证返回的结果。我们可以通过在 DNS 服务器 IP 或主机名后面添加@
来实现这一点。在下面的例子中,我们请求8.8.8.8
,这是谷歌公共 DNS 基础设施的一部分。
[blog]$ dig @8.8.8.8 db.example.com
; <<>> DiG 9.9.4-RedHat-9.9.4-14.el7_0.1 <<>> @8.8.8.8 example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42743
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;db.example.com. IN A
;; ANSWER SECTION:
db.example.com. 18639 IN A 10.0.0.50
;; Query time: 39 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sun Mar 01 22:14:53 UTC 2015
;; MSG SIZE rcvd: 56
It seems that Google's public DNS has the same results as 10.0.2.3.
使用 nslookup 查找 DNS
另一个用于故障排除 DNS 的好工具是nslookup
。nslookup
命令已经存在了相当长的时间。实际上,它是另一个跨平台命令,几乎存在于所有主要操作系统上。
要使用nslookup
进行简单的 DNS 查找,我们只需运行命令,然后跟上要查询的 DNS 名称,类似于dig
。
[blog]$ nslookup db.example.com
Server: 10.0.2.3
Address: 10.0.2.3#53
Non-authoritative answer:
Name: db.example.com
Address: 10.0.0.50
dig
命令可以用于查询特定的 DNS 服务器。这可以通过两种方法来实现。第一种方法是在命令的末尾添加服务器地址。
[blog]$ nslookup db.example.com 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: db.example.com
Address: 10.0.0.50
第二种方法是在交互模式下使用nslookup
。要进入交互模式,只需执行nslookup
而不使用其他选项。
# nslookup
>
进入交互模式后,通过输入server <dns 服务器>
来指定要使用的服务器。
# nslookup
> server 8.8.8.8
Default server: 8.8.8.8
Address: 8.8.8.8#53
>
最后,要查找 DNS 名称,我们只需输入要查询的域。
# nslookup
> server 8.8.8.8
Default server: 8.8.8.8
Address: 8.8.8.8#53
> db.example.com
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: db.example.com
Address: 10.0.0.50
>
To leave the interactive mode, simply type exit.
> exit
那么为什么使用nslookup
而不是dig
呢?虽然dig
命令非常有用,但它不是一个跨平台命令,通常只存在于 Unix 和 Linux 系统上。另一方面,nslookup
命令是跨平台的,可以在大多数环境中找到,而dig
命令可能不可用。作为系统管理员,熟悉许多命令是很重要的,能够使用任何可用的命令来执行任务是非常有用的。
dig
和nslookup
告诉了我们什么?
现在我们已经使用dig
和nslookup
来查询 DNS 名称db.example.com
,让我们回顾一下我们找到了什么。
-
域
db.example.com
实际上解析为10.0.0.50
-
ping
命令返回了域db.example.com
的192.168.33.12
地址。
ping
命令返回一个地址,而 DNS 返回另一个地址,这是怎么回事?一个可能的原因是/etc/hosts
文件中的配置。这是我们可以用简单的grep
命令快速验证的事情。
[blog]$ grep example.com /etc/hosts
192.168.33.11 blog.example.com
192.168.33.12 db.example.com
关于/etc/hosts
的一点说明
在创建诸如Bind这样的 DNS 服务器之前,本地的hosts
文件被用来管理域名到 IP 的映射。这个文件包含了系统需要连接的每个域地址的列表。然而,随着网络从几个主机发展到成千上万甚至数百万个主机,这种方法随着时间的推移变得复杂起来。
在 Linux 和大多数 Unix 发行版中,hosts
文件位于/etc/hosts
。默认情况下,/etc/hosts
文件中的任何条目都将覆盖 DNS 请求。这意味着,默认情况下,如果/etc/hosts
文件中存在域到 IP 的映射,系统将使用该映射,而不会从另一个 DNS 系统中获取相同的域。
这是 Linux 的默认行为;但是,我们可以通过阅读/etc/nsswitch.conf
文件来检查该服务器是否使用此默认配置。
[blog]$ grep hosts /etc/nsswitch.conf
hosts: files dns
nsswitch.conf
文件是一个允许管理员配置要使用哪些后端系统来查找用户、组、网络组、主机名和服务等项目的配置。例如,如果我们想要配置系统使用ldap
来查找用户组,我们可以通过更改/etc/nsswitch.conf
文件中的值来实现。
[blog]$ grep group /etc/nsswitch.conf
group: files sss
根据前面grep
命令的输出,博客系统配置为首先使用本地组文件,然后使用 SSSD 服务来查找用户组。要将ldap
添加到此配置中,只需按所需顺序(即“ldap 文件 sss”)将其添加到列表中。
对于由hosts
配置指定的 DNS,似乎我们的服务器配置为首先基于文件查找主机,然后再查找 DNS。这意味着我们的系统会在通过 DNS 查找域之前优先使用/etc/hosts
文件。
DNS 总结
现在我们已经确认了 DNS 和/etc/hosts
文件,我们知道有人配置了此应用服务器,使其认为db.example.com
解析为192.168.33.12
。这是一个错误还是一种在不使用 DNS 的情况下连接到数据库服务器的方式?
此时,现在还为时过早,但我们知道主机192.168.33.12
没有向我们的博客服务器发送“ICMP 回显应答”来响应我们的“ICMP 回显请求”。
从另一个位置进行 ping
在处理网络问题时,最好尝试从多个位置或服务器进行连接。这对于数据收集类型的故障排除者可能似乎是显而易见的,但是受过教育的猜测型故障排除者可能会忽视这一极其有用的步骤。
在我们的示例中,我们将从笔记本电脑运行一个测试ping
到192.168.33.12
。
[laptop]$ ping 192.168.33.12
PING 192.168.33.12 (192.168.33.12): 56 data bytes
64 bytes from 192.168.33.12: icmp_seq=0 ttl=64 time=0.573 ms
64 bytes from 192.168.33.12: icmp_seq=1 ttl=64 time=0.425 ms
64 bytes from 192.168.33.12: icmp_seq=2 ttl=64 time=0.461 ms
^C
--- 192.168.33.12 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.425/0.486/0.573/0.063 ms
从ping
请求的结果来看,我们的笔记本电脑似乎能够无问题地连接到192.168.33.12
。
这告诉我们什么?实际上告诉我们很多!它告诉我们所讨论的服务器正在运行;它还确认了存在连接问题,特别是在blog.example.com
和db.example.com
之间。如果问题是由于db.example.com
服务器宕机或配置错误引起的,我们的笔记本电脑也会受到影响。
然而事实并非如此。实际上恰恰相反;似乎我们的笔记本电脑与服务器之间的连接正常工作。
使用 cURL 测试端口连接
早些时候,当使用telnet
从我们的笔记本电脑测试 MariaDB 端口时,telnet
命令正在测试服务器10.0.0.50
。然而,根据/etc/hosts
配置,似乎期望的数据库服务器是192.168.33.12
。
为了验证数据库服务实际上是否正常运行,我们应该使用192.168.33.12
地址执行相同的telnet
测试。但是,这一次我们将使用curl
而不是telnet
来执行此测试。
我见过许多环境(尤其是最近)禁止安装telnet
客户端或默认情况下不执行安装。对于这样的环境,有一些可以测试端口连接的工具是很重要的。如果 telnet 不可用,可以使用curl
命令作为替代。
在第三章中,“故障排除 Web 应用程序”,我们使用curl
命令请求网页。实际上,curl
命令可以与许多不同的协议一起使用;我们在这种情况下感兴趣的协议是 Telnet 协议。
以下是使用curl
从我们的笔记本连接到db.example.com
服务器的端口3306
的示例。
[laptop]$ curl -v telnet://192.168.33.12:3306
* Rebuilt URL to: telnet://192.168.33.12:3306/
* Hostname was NOT found in DNS cache
* Trying 192.168.33.12...
* Connected to 192.168.33.12 (192.168.33.12) port 3306 (#0)
* RCVD IAC 106
^C
从示例中,似乎不仅笔记本能够连接到端口3306
的服务器,而且curl
命令还收到了来自RCVD IAC 106
服务的消息。
在进行 Telnet 测试时,使用curl
时,需要使用-v
(详细)标志将 curl 置于详细模式。没有详细标志,curl
将简单地隐藏连接细节,而连接细节正是我们要寻找的。
在前面的例子中,我们可以看到从我们的笔记本成功连接;为了进行比较,我们可以使用相同的命令从博客服务器测试连接。
[blog]$ curl -v telnet://192.168.33.12:3306
* About to connect() to 192.168.33.12 port 3306 (#0)
* Trying 192.168.33.12...
* No route to host
* Failed connect to 192.168.33.12:3306; No route to host
* Closing connection 0
curl: (7) Failed connect to 192.168.33.12:3306; No route to host
连接尝试失败,正如预期的那样。
从上面使用curl
的测试中,我们可以确定数据库服务器正在监听并接受端口3306
上的连接;但是,博客服务器无法连接到数据库服务器。我们不知道的是问题是在博客服务器端还是在数据库服务器端。要确定连接的哪一端存在问题,我们需要查看网络连接的详细信息。为此,我们将使用两个命令,第一个是netstat
,第二个是tcpdump
。
使用 netstat 显示当前网络连接
netstat
命令是一个非常全面的工具,可以用于排除网络问题的许多方面。在这种情况下,我们将使用两个基本标志来打印现有的网络连接。
[blog]# netstat -na
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:52903 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 10.0.2.16:22 10.0.2.2:50322 ESTABLISHED
tcp 0 0 192.168.33.11:22 192.168.33.1:53359 ESTABLISHED
tcp6 0 0 ::1:25 :::* LISTEN
tcp6 0 0 :::57504 :::* LISTEN
tcp6 0 0 :::111 :::* LISTEN
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
udp 0 0 0.0.0.0:5353 0.0.0.0:*
udp 0 0 0.0.0.0:68 0.0.0.0:*
udp 0 0 0.0.0.0:111 0.0.0.0:*
udp 0 0 0.0.0.0:52594 0.0.0.0:*
udp 0 0 127.0.0.1:904 0.0.0.0:*
udp 0 0 0.0.0.0:49853 0.0.0.0:*
udp 0 0 0.0.0.0:53449 0.0.0.0:*
udp 0 0 0.0.0.0:719 0.0.0.0:*
udp6 0 0 :::54762 :::*
udp6 0 0 :::58674 :::*
udp6 0 0 :::111 :::*
udp6 0 0 :::719 :::*
raw6 0 0 :::58 :::*
在前面的例子中,我们使用了-n
(无 dns)标志执行了netstat
命令,告诉netstat
不要查找 IP 的 DNS 主机名或将端口号转换为服务名称,以及-a
(全部)标志,告诉netstat
打印监听和非监听套接字。
这些标志的效果类似于netstat
,显示所有应用程序绑定的所有网络连接和端口。
示例netstat
命令显示了相当多的信息。为了更好地理解这些信息,让我们更仔细地检查一下输出。
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
The second column Recv-Q is a count of bytes received but not copied by the application by using this socket. This is basically the number of bytes waiting between the kernel receiving the data from the network and the application accepting it.
shows the local host address as 127.0.0.1 and the port as 25.
第五列是Foreign Address或远程地址。此列列出了远程服务器的 IP 和端口。由于我们之前使用的示例类型,这被列为 IP0.0.0.0
和端口*
,这是一个通配符,表示任何内容。
第六列,我们的最后一列,是状态套接字。对于 TCP 连接,状态将告诉我们 TCP 连接的当前状态。对于我们之前的例子,状态列为LISTEN
;这告诉我们列出的套接字用于接受 TCP 连接。
如果我们将所有列放在一起,这一行告诉我们,我们的服务器正在通过 IP127.0.0.1
监听端口25
上的新连接,并且这是基于 TCP 的连接。
使用 netstat 来监视新连接
现在我们对netstat
的输出有了更多的了解,我们可以使用它来查找应用程序服务器到数据库服务器的新连接。要使用netstat
监视新连接,我们将使用netstat
经常被忽视的一个功能。
与vmstat
命令类似,可以将netstat
置于连续模式中,每隔几秒打印相同的输出。要做到这一点,只需在命令的末尾放置间隔。
在下一个例子中,我们将使用相同的netstat
标志,间隔为5
秒;但是,我们还将将输出导向到grep
并使用grep
来过滤端口3306
。
[blog]# netstat -na 5 | grep 3306
tcp 0 1 192.168.33.11:59492 192.168.33.12:3306 SYN_SENT
tcp 0 1 192.168.33.11:59493 192.168.33.12:3306 SYN_SENT
tcp 0 1 192.168.33.11:59494 192.168.33.12:3306 SYN_SENT
除了运行netstat
命令,我们还可以在浏览器中导航到blog.example.com
地址。我们可以这样做,以强制 Web 应用程序尝试连接到数据库。
一般来说,Web 应用程序对数据库有两种类型的连接,一种是持久连接,它们始终保持与数据库的连接,另一种是非持久连接,只有在需要时才建立。由于我们不知道这个 WordPress 安装使用哪种类型,因此在这种类型的故障排除中,假设它们是非持久的更安全。这意味着,为了触发数据库连接,必须有流量到 WordPress 应用程序。
从netstat
的输出中,我们可以看到对数据库的连接尝试,而且不仅仅是任何数据库,而是192.168.33.12
上的数据库服务。这些信息证实,当 Web 应用程序尝试建立连接时,它使用的是hosts
文件中的 IP,而不是来自 DNS。直到这一点,我们怀疑这是基于telnet
和ping
,但没有证据表明应用程序的连接。
然而,有趣的事实是netstat
输出显示 TCP 连接处于SYN_SENT
状态。这个SYN_SENT
状态是在首次建立网络连接时使用的状态。netstat
命令可以打印许多不同的连接状态;每个状态告诉我们连接所处的过程中的位置。这些信息对于识别网络连接问题的根本原因至关重要。
netstat
状态的详细说明
在深入研究之前,我们应该快速查看一下不同的netstat
状态及其含义。以下是netstat
使用的所有状态的完整列表:
-
ESTABLISHED
:连接已建立,可用于数据传输 -
SYN_SENT
:TCP 套接字正在尝试与远程主机建立连接 -
SYN_RECV
:已从远程主机接收到 TCP 连接请求 -
FIN_WAIT1
:TCP 连接正在关闭 -
FIN_WAIT2
:TCP 连接正在等待远程主机关闭连接 -
TIME_WAIT
:套接字在关闭后等待任何未完成的网络数据包 -
CLOSE
:套接字不再被使用 -
CLOSE_WAIT
:远程端已关闭其连接,本地套接字正在关闭 -
LAST_ACK
:远程端已启动关闭连接,本地系统正在等待最终确认 -
LISTEN
:套接字正在用于监听传入连接 -
CLOSING
:本地和远程套接字都已关闭,但并非所有数据都已发送 -
UNKNOWN
:用于处于未知状态的套接字
从上面的列表中,我们可以确定应用程序到数据库的连接从未变为ESTABLISHED
。这意味着应用程序服务器在SYN_SENT
状态下开始连接,但从未转换到下一个状态。
使用 tcpdump 捕获网络流量
为了更好地理解网络流量,我们将使用第二个命令来查看网络流量的详细信息——tcpdump
。在这里,netstat
命令用于打印套接字的状态;tcpdump
命令用于创建网络流量的“转储”或“跟踪”。这些转储允许用户查看捕获的网络流量的所有方面。
通过tcpdump
,可以查看完整的 TCP 数据包细节,从数据包头部到实际传输的数据。tcpdump
不仅可以捕获这些数据,还可以将捕获的数据写入文件。数据写入文件后,可以保存或移动,并且稍后可以使用tcpdump
命令或其他网络数据包分析工具(例如wireshark
)进行读取。
以下是运行tcpdump
捕获网络流量的简单示例。
[blog]# tcpdump -nvvv
tcpdump: listening on enp0s3, link-type EN10MB (Ethernet), capture size 65535 bytes
16:18:04.125881 IP (tos 0x10, ttl 64, id 20361, offset 0, flags [DF], proto TCP (6), length 156)
10.0.2.16.ssh > 10.0.2.2.52618: Flags [P.], cksum 0x189f (incorrect -> 0x62a4), seq 3643405490:3643405606, ack 245510335, win 26280, length 116
16:18:04.126203 IP (tos 0x0, ttl 64, id 9942, offset 0, flags [none], proto TCP (6), length 40)
10.0.2.2.52618 > 10.0.2.16.ssh: Flags [.], cksum 0xbc71 (correct), seq 1, ack 116, win 65535, length 0
16:18:05.128497 IP (tos 0x10, ttl 64, id 20362, offset 0, flags [DF], proto TCP (6), length 332)
10.0.2.16.ssh > 10.0.2.2.52618: Flags [P.], cksum 0x194f (incorrect -> 0xecc9), seq 116:408, ack 1, win 26280, length 292
16:18:05.128784 IP (tos 0x0, ttl 64, id 9943, offset 0, flags [none], proto TCP (6), length 40)
10.0.2.2.52618 > 10.0.2.16.ssh: Flags [.], cksum 0xbb4d (correct), seq 1, ack 408, win 65535, length 0
16:18:06.129934 IP (tos 0x10, ttl 64, id 20363, offset 0, flags [DF], proto TCP (6), length 156)
10.0.2.16.ssh > 10.0.2.2.52618: Flags [P.], cksum 0x189f (incorrect -> 0x41d5), seq 408:524, ack 1, win 26280, length 116
16:18:06.130441 IP (tos 0x0, ttl 64, id 9944, offset 0, flags [none], proto TCP (6), length 40)
10.0.2.2.52618 > 10.0.2.16.ssh: Flags [.], cksum 0xbad9 (correct), seq 1, ack 524, win 65535, length 0
16:18:07.131131 IP (tos 0x10, ttl 64, id 20364, offset 0, flags [DF], proto TCP (6), length 140)
在前面的示例中,我为tcpdump
命令提供了几个标志。第一个标志–n
(无 dns)告诉tcpdump
不要查找它找到的任何 IP 的主机名。其余的标志–vvv
(详细)告诉tcpdump
非常“非常”详细。tcpdump
命令有三个详细级别;每个添加到命令行的–v
都会增加使用的详细级别。在前面的示例中,tcpdump
处于最详细的模式。
前面的示例是运行tcpdump
的最简单方式之一;然而,它并没有捕获我们需要的流量。
查看服务器的网络接口
当在具有多个网络接口的系统上执行tcpdump
时,除非定义了接口,否则该命令将选择最低编号的接口进行连接。在前面的示例中,选择的接口是enp0s3
;然而,这可能不是用于数据库连接的接口。
在使用tcpdump
来调查我们的网络连接问题之前,我们首先需要确定用于此连接的网络接口;为了做到这一点,我们将使用ip
命令。
[blog]# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff
在高层次上,ip
命令允许用户打印、修改和添加网络配置。在上面的示例中,我们告诉ip
命令通过使用show links
参数来“显示”所有可用的“链接”。显示的链接实际上是为该服务器定义的网络接口。
什么是网络接口?
在谈论物理服务器时,网络接口通常是物理以太网端口的表示。如果我们假设前面示例中使用的机器是一台物理机器,我们可以假设enp0s3
和enp0s8
链接是物理设备。然而,实际上,上述机器是一台虚拟机。这意味着这些设备逻辑上连接到这台虚拟机;但是,这台机器的内核并不知道,甚至不需要知道这种区别。
例如,在这本书中,大多数接口(除了“lo
”或回环接口)都直接与物理(或虚拟物理)网络设备相关。然而,也有可能创建虚拟接口,这允许您创建多个接口,这些接口链接回单个物理接口。一般来说,这些接口以“:
”或“.
”作为原始设备名称的分隔符。如果我们要为enp0s8
创建一个虚拟接口,它看起来会像enp0s8:1
。
查看设备配置
从ip
命令的输出中,我们可以看到有三个定义的网络接口。在了解哪个接口用于我们的数据库连接之前,我们首先需要更好地了解这些接口。
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT
lo
或回环接口是列表中的第一个。在 Linux 或 Unix 上工作了足够长时间的人都会对回环接口非常熟悉。回环接口旨在为系统的用户提供一个本地网络地址,只能用于连接回本地系统。
这个特殊的接口允许位于同一台服务器上的应用程序通过 TCP/IP 进行交互,而无需将其连接外部网络。它还允许这些应用程序在没有网络数据包离开本地服务器的情况下进行交互,从而使其成为非常快速的网络连接。
传统上,回环接口 IP 的已知地址是127.0.0.1
。然而,就像本书中的其他内容一样,我们将在假设其为真之前先验证这些信息。我们可以使用ip
命令来显示回环接口的定义地址来做到这一点。
[blog]# ip addr show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
在显示可用接口的前面示例中,使用了“link show
”选项;为了显示 IP 地址,可以使用“addr show
”选项。ip
命令打印项目的语法在整个过程中都遵循这个相同的方案。
前面的例子还指定了我们感兴趣的设备的名称;这限制了输出到指定的设备。如果我们在前面的命令中省略设备名称,它将简单地打印出所有设备的 IP 地址。
那么,上面的内容告诉我们关于 lo 接口的什么呢?其中一件事是,lo
接口正在监听 IPv4 地址127.0.0.1
;我们可以在下一行看到这一点。
inet 127.0.0.1/8 scope host lo
这意味着,如果我们想通过环回接口连接到这个主机,我们可以通过定位127.0.0.1
来实现。然而,ip
命令还显示了在这个接口上定义的第二个 IP。
inet6 ::1/128 scope host
这告诉我们::1
的 IPv6 地址也绑定到了 lo 接口。这个地址用于相同的目的作为127.0.0.1
,但它是为IPv6
通信设计的。
通过ip
命令提供的上述信息,我们可以看到lo
或环回接口被按预期定义。
在这台服务器上定义的第二个接口是enp0s3
;这个设备,不像 lo,要么是一个物理设备,要么是一个虚拟化的物理接口。之前执行的ip
link show 命令已经告诉我们关于这个接口的很多信息。
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
The device is in an **up** state: `state UP`The MTU size is **1500**: `mtu 1500`The MAC address is **08:00:27:20:5d:4b**: `link/ether 08:00:27:20:5d:4b`
从这些信息中,我们知道接口已经启动并且可以被利用。我们还知道 MTU 大小设置为默认的 1500,并且可以轻松地识别 MAC 地址。虽然 MTU 大小和 MAC 地址可能与这个问题无关,但在其他情况下它们可能非常有用。
然而,对于我们当前的任务,即确定用于数据库连接的接口,我们需要确定绑定到这个接口的 IP 是哪些。
[blog]# ip addr show enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 49655sec preferred_lft 49655sec
inet6 fe80::a00:27ff:fe20:5d4b/64 scope link
valid_lft forever preferred_lft forever
从前面的输出中,我们可以看到enp0s3
接口正在监听 IPv4 IP 10.0.2.15
(inet 10.0.2.15/24
)以及 IPv6 IP f380::a00:27ff:fe20:5d4b
(inet6 fe80::a00:27ff:fe20:5d4b/64
)。这是否告诉我们连接到192.168.33.12
是通过这个接口?不,但也不意味着不是。
这告诉我们enp0s3
接口被用于连接到10.0.2.15/24
网络。这个网络可能能够路由到192.168.33.12
的地址;在做出这个决定之前,我们应该首先审查下一个接口的配置。
这个系统上的第三个接口是enp0s8
;它也是一个物理或虚拟网络设备,从ip
link show 命令提供的信息中,我们可以看到它与enp0s3
有类似的配置。
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff
从这个输出中,我们可以看到enp0s8
接口也处于UP
状态,并且具有默认的 MTU 大小为 1500。我们还可以确定这个接口的 MAC 地址,这在这个时候并不是特别需要;然而,以后可能会变得有用。
然而,如果我们看一下在这台服务器上定义的 IP,与enp0s3
设备相比,有一个显著的不同。
[blog]# ip addr show enp0s8
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.11/24 brd 192.168.33.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fe7f:fd54/64 scope link
valid_lft forever preferred_lft forever
我们可以看到enp0s8
接口正在监听 IPv4 地址192.168.33.11
(inet 192.168.33.11/24
)和 IPv6 地址fe80::a00:27ff:fe7f:fd54
(inet6 fe80::a00:27ff:fe7f:fd54/64
)。
这是否意味着enp0s8
接口被用于连接到192.168.33.12
?实际上,可能是的。
enp0s8
定义的子网是192.168.33.11/24
,这意味着这个接口连接到一个跨越192.168.33.0
到192.168.33.255
的 IP 范围的设备网络。由于数据库服务器的IP 192.168.33.12
在这个范围内,很可能是通过enp0s8
接口进行与这个地址的通信。
在这一点上,我们可以“怀疑”enp0s8
接口被用于与数据库服务器进行通信。虽然这个接口可能被配置为与包含192.168.33.12
的子网进行通信,但完全有可能通过使用定义的路由强制通过另一个接口进行通信。
为了检查是否定义了路由并强制通过另一个接口进行通信,我们将再次使用ip
命令。然而,对于这个任务,我们将使用ip
命令的“route get
”选项。
[blog]# ip route get 192.168.33.12
192.168.33.12 dev enp0s8 src 192.168.33.11
cache
当使用“route get
”参数执行时,ip
命令将特别输出用于路由到指定 IP 的接口。
从前面的输出中,我们可以看到blog.example.com
服务器实际上是使用enp0s8
接口路由到 192.168.33.12 地址,即db.example.com
的 IP。
到目前为止,我们不仅使用ip
命令确定了这台服务器上存在哪些网络接口,还使用它确定了网络数据包到达目标主机所需的接口。
ip
命令是一个非常有用的工具,最近被计划用来替代诸如ifconfig
和route
之类的旧命令。如果你通常熟悉使用ifconfig
等命令,但对ip
命令不太熟悉,那么建议你回顾一下上面介绍的用法,因为最终ifconfig
命令将被弃用。
指定 tcpdump 的接口
现在我们已经确定了与db.example.com
通信所使用的接口,我们可以通过使用tcpdump
开始我们的网络跟踪。如前所述,我们将使用-nvvv
标志将tcpdump
置于非常“非常”详细的模式,而不进行主机名解析。然而,这一次,我们将指定tcpdump
从enp0s8
接口捕获网络流量;我们可以使用-i
(接口)标志来实现这一点。我们还将使用-w
(写入)标志将捕获的数据写入文件。
[blog]# tcpdump -nvvv -i enp0s8 -w /var/tmp/chapter5.pcap
tcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 65535 bytes
48 packets captured
当我们首次执行tcpdump
命令时,屏幕上输出了相当多的内容。当要求将其输出保存到文件时,tcpdump
不会将捕获的数据输出到屏幕上,而是会持续显示捕获的数据包的计数器。
一旦我们让tcpdump
将捕获的数据保存到文件中,我们需要复制问题以尝试生成数据库流量。我们将通过与netstat
命令相同的方法来实现这一点:简单地在 Web 浏览器中导航到blog.example.com
。
当我们导航到 WordPress 网站时,我们应该看到捕获的数据包
计数器在增加;这表明tcpdump
已经看到了流量并进行了捕获。一旦计数器达到一个合理的数字,我们就可以停止tcpdump
的捕获。要做到这一点,只需在命令行上按下Ctrl + C;一旦停止,我们应该看到类似以下的消息:
^C48 packets captured
48 packets received by filter
0 packets dropped by kernel
读取捕获的数据
现在我们已经将捕获的网络跟踪
保存到文件中,我们可以使用这个文件来调查数据库流量。将这些数据保存在文件中的好处是我们可以多次读取这些数据,并通过过滤器来减少输出。此外,当对实时网络流进行tcpdump
时,我们可能只能捕获一次流量,再也捕获不到了。
为了读取保存的数据,我们可以使用-r
(读取)标志后跟要读取的文件名来运行tcpdump
。
我们可以通过使用以下命令打印我们捕获的所有48
个数据包的数据包头信息来开始。
[blog]# tcpdump -nvvv -r /var/tmp/chapter5.pcap
然而,这个命令的输出可能会让人感到不知所措;为了找到问题的核心,我们需要缩小tcpdump
的输出范围。为此,我们将使用 tcpdump 的过滤器功能来对捕获的数据进行过滤。特别是,我们将使用host
过滤器将输出过滤到特定的 IP 地址。
[blog]# tcpdump -nvvv -r /var/tmp/chapter5.pcap host 192.168.33.12
reading from file /var/tmp/chapter5.pcap, link-type EN10MB (Ethernet)
03:33:05.569739 IP (tos 0x0, ttl 64, id 26591, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3543), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53696341 ecr 0,nop,wscale 6], length 0
03:33:06.573145 IP (tos 0x0, ttl 64, id 26592, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3157), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53697345 ecr 0,nop,wscale 6], length 0
03:33:08.580122 IP (tos 0x0, ttl 64, id 26593, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x2980), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53699352 ecr 0,nop,wscale 6], length 0
通过在tcpdump
命令的末尾添加host 192.168.33.12
,输出被过滤为只与主机 192.168.33.12 相关的流量。这是通过host
过滤器实现的。tcpdump
命令有许多可用的过滤器;然而,在本章中,我们主要将利用主机过滤器。我强烈建议经常解决网络问题的人熟悉tcpdump
过滤器。
在运行tcpdump
(与上面类似)时,重要的是要知道每一行都是通过指定接口发送或接收的一个数据包。下面的例子是一个完整的tcpdump
行,本质上是通过enp0s8
接口传递的一个数据包。
03:33:05.569739 IP (tos 0x0, ttl 64, id 26591, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3543), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53696341 ecr 0,nop,wscale 6], length 0
如果我们看一下前面的行,我们可以看到这个数据包是从192.168.33.11
发送到192.168.33.12
的。我们可以从以下部分看到这一点:
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S]
192.168.33.11 to 192.168.33.12. We can identify this by the first and the second IPs in this snippet. Since 192.168.33.11 is the first IP, it is the source of the packet, and the second IP (192.168.33.12) is then the destination.
192.168.33.11.37785 > 192.168.33.12.mysql
192.168.33.11 was from the local port 37785 to a remote port of 3306. We can infer this as the fifth dot in the source address is 37785 and "mysql" is in the target address. The reason that tcpdump has printed "mysql" is that by default it will map common service ports to their common name. In this case, it mapped port 3306 to mysql and simply printed mysql. This can be turned off on the command line by using two –n flags (i.e. -nn) to the tcpdump command.
tcpdump output will have a section for flags. When the flags set on a packet are only S, this means that the packet is the initial SYN packet.
这个数据包是一个SYN
数据包实际上告诉了我们关于这个数据包的很多信息。
关于 TCP 的快速入门
传输控制协议(TCP)是互联网通信中最常用的协议之一。它是我们每天依赖的许多服务的选择协议。从用于加载网页的 HTTP 协议到所有 Linux 系统管理员最喜欢的SSH
,这些协议都是在 TCP 协议之上实现的。
虽然 TCP 被广泛使用,但它也是一个相当高级的话题,每个系统管理员都应该至少有基本的了解。在本节中,我们将快速介绍一些 TCP 基础知识;这绝不是一个详尽的指南,但足以理解我们问题的根源。
要理解我们的问题,我们必须首先了解 TCP 连接是如何建立的。在 TCP 通信中,通常有两个重要的参与方,即客户端和服务器。客户端是连接的发起者,并将发送一个SYN
数据包作为建立 TCP 连接的第一步。
当服务器接收到一个SYN
数据包并愿意接受连接时,它会向客户端发送一个同步确认(SYN-ACK)数据包。这是为了让服务器确认它已经收到了原始的SYN
数据包。
当客户端接收到这个SYN-ACK
数据包时,它会回复服务器一个ACK
,有时也称为SYN-ACK-ACK
。这个数据包的想法是让客户端确认它已经收到了服务器的确认。
这个过程被称为三次握手,是 TCP 的基础。这种方法的好处是,每个系统都确认它接收到的数据包,因此不会有关于客户端和服务器是否能够来回通信的问题。一旦进行了三次握手,连接就会转移到已建立的状态。在这种状态下可以使用其他类型的数据包,比如推送(PSH)数据包,用于在客户端和服务器之间传输信息。
TCP 数据包的类型
说到其他类型的数据包,重要的是要知道确定一个数据包是SYN
数据包还是ACK
数据包的组件只是在数据包头中设置一个标志。
在我们捕获的数据的第一个数据包上,只有SYN
标志被设置;这就是为什么我们会看到输出如Flags [S]
的原因。这是第一个数据包被发送并且该数据包只有SYN
标志被设置的一个例子。
一个SYN-ACK
数据包是一个SYN
和ACK
标志被设置的数据包。这通常在tcpdump
中看到的是[S.]
。
以下是在使用tcpdump
进行故障排除活动中常见的数据包标志的表格。这绝不是一个完整的列表,但它确实给出了常见数据包类型的一个大致概念。
-
SYN- [S]
:这是一个同步数据包,从客户端发送到服务器的第一个数据包。 -
SYN-ACK- [S.]
:这是一个同步确认数据包;这些数据包标志用于指示服务器接收到客户端的SYN
请求。 -
ACK- [.]
:确认数据包被服务器和客户端用来确认接收到的数据包。在初始的SYN
数据包发送后,所有后续的数据包都应该设置确认标志。 -
PSH- [P]
: 这是一个推送数据包。它旨在将缓冲的网络数据推送到接收方。这是实际传输数据的数据包类型。 -
PSH-ACK- [P.]
: 推送确认数据包用于确认先前的数据包并向接收方发送数据。 -
FIN- [F]
:FIN
或完成数据包用于告诉服务器没有更多数据,可以关闭已建立的连接。 -
FIN-ACK- [F.]
: 完成确认数据包用于确认先前的完成数据包已被接收。 -
RST- [R]
: 重置数据包用于源系统希望重置连接时使用。一般来说,这是由于错误或目标端口实际上不处于监听状态。 -
RST-ACK -[R.]
: 重置确认数据包用于确认先前的重置数据包已被接收。
现在我们已经探讨了不同类型的数据包,让我们把它们联系起来,快速回顾一下之前捕获的数据。
[blog]# tcpdump -nvvv -r /var/tmp/chapter5.pcap host 192.168.33.12
reading from file /var/tmp/chapter5.pcap, link-type EN10MB (Ethernet)
03:33:05.569739 IP (tos 0x0, ttl 64, id 26591, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3543), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53696341 ecr 0,nop,wscale 6], length 0
03:33:06.573145 IP (tos 0x0, ttl 64, id 26592, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3157), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53697345 ecr 0,nop,wscale 6], length 0
03:33:08.580122 IP (tos 0x0, ttl 64, id 26593, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x2980), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53699352 ecr 0,nop,wscale 6], length 0
If we look at just the IP addresses and the flags from the captured data, from each line, it becomes very clear what the issue is.
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S],
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S],
192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S],
如果我们分解这三个数据包,我们可以看到它们都来自源端口37785
,目标端口为 3306。我们还可以看到这些数据包是SYN
数据包。这意味着我们的系统发送了 3 个SYN
数据包,但从目标端口,即192.168.33.12
,没有收到SYN-ACK
。
这告诉我们关于与主机192.168.33.12
的网络连接的什么?它告诉我们要么远程服务器192.168.33.12
从未收到我们的数据包,要么它收到了并且我们从未能收到SYN-ACK
回复。如果问题是由于数据库服务器不接受我们的数据包,我们将期望看到一个RST
或重置
数据包。
审查收集的数据
此时,现在是时候盘点我们收集的信息和我们目前所知的信息了。
我们已经确定的第一条关键信息是博客服务器(blog.example.com
)无法连接到数据库服务器(db.example.com
)。我们已经确定的第二条关键信息是 DNS 名称db.example.com
解析为10.0.0.50
。但是,在blog.example.com
服务器上还有一个/etc/hosts
文件条目覆盖了 DNS。由于 hosts 文件,当 Web 应用程序尝试连接到db.example.com
时,它实际上连接到了192.168.33.12
。
我们还确定了主机192.168.33.11
(blog.example.com
)在访问 WordPress 应用程序时向192.168.33.12
发送初始的SYN
数据包。然而,服务器192.168.33.12
要么没有接收到这些数据包,要么没有回复这些数据包。
在我们的调查过程中,我们审查了博客服务器的网络配置,并确定它似乎已正确设置。我们可以通过简单使用 ping 命令向每个网络接口的子网内的 IP 发送 ICMP 回显来对此进行额外验证。
[blog]# ip addr show enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 62208sec preferred_lft 62208sec
inet6 fe80::a00:27ff:fe20:5d4b/64 scope link
valid_lft forever preferred_lft forever
对于enp0s3
接口,我们可以看到绑定的 IP 地址是10.0.2.16
,子网掩码为/24
或255.255.255.0
。通过这种设置,我们应该能够与该子网内的其他 IP 进行通信。以下是使用 ping 命令测试与10.0.2.2
的连通性的输出。
[blog]# ping 10.0.2.2
PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data.
64 bytes from 10.0.2.2: icmp_seq=1 ttl=63 time=0.250 ms
64 bytes from 10.0.2.2: icmp_seq=2 ttl=63 time=0.196 ms
64 bytes from 10.0.2.2: icmp_seq=3 ttl=63 time=0.197 ms
^C
--- 10.0.2.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.196/0.214/0.250/0.027 ms
这表明enp0s3
接口至少可以连接到其子网内的其他 IP。对于enp0s8
,我们可以使用另一个 IP 执行相同的测试。
[blog]# ip addr show enp0s8
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.11/24 brd 192.168.33.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fe7f:fd54/64 scope link
valid_lft forever preferred_lft forever
从上述命令中,我们可以看到enp0s8
的 IP 为192.168.33.11
,子网掩码为/24
或255.255.255.0
。如果我们可以使用 ping 命令与192.168.33.11/24
子网内的任何其他 IP 进行通信,那么我们可以验证该接口也已正确配置。
# ping 192.168.33.1
PING 192.168.33.1 (192.168.33.1) 56(84) bytes of data.
64 bytes from 192.168.33.1: icmp_seq=1 ttl=64 time=0.287 ms
64 bytes from 192.168.33.1: icmp_seq=2 ttl=64 time=0.249 ms
64 bytes from 192.168.33.1: icmp_seq=3 ttl=64 time=0.260 ms
64 bytes from 192.168.33.1: icmp_seq=4 ttl=64 time=0.192 ms
^C
--- 192.168.33.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3028ms
rtt min/avg/max/mdev = 0.192/0.247/0.287/0.034 ms
从结果中,我们可以看到对 IP192.168.33.1
的连接正常工作。因此,这意味着,至少在基本方面,enp0s8
接口已正确配置。
有了所有这些信息,我们可以假设blog.example.com
服务器已正确配置,并且可以连接到其配置的网络。从这一点开始,如果我们想要更多关于我们问题的信息,我们需要从db.example.com
(192.168.33.12
)服务器获取。
看看对方的情况
虽然可能并非总是可能的,但在处理网络问题时,最好从对话的两端进行故障排除。在我们之前的例子中,我们有两个构成我们网络对话的系统,即客户端和服务器。到目前为止,我们已经从客户端的角度看了一切;在本节中,我们将从服务器的角度来看这次对话的另一面。
识别网络配置
在前一节中,我们在查看博客服务器的网络配置之前经历了几个步骤。在数据库服务器的情况下,我们已经知道问题与网络有关,特别是 IP 为192.168.33.12
。既然我们已经知道问题与哪个 IP 相关,我们应该做的第一件事是确定这个 IP 绑定到哪个接口。
我们将再次使用ip
命令和addr show
选项来执行此操作。
[db]# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 86304sec preferred_lft 86304sec
inet6 fe80::a00:27ff:fe20:5d4b/64 scope link
valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:c9:d3:65 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.12/24 brd 192.168.33.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fec9:d365/64 scope link
valid_lft forever preferred_lft forever
在之前的例子中,我们使用addr show
选项来显示与单个接口关联的 IP。然而,这次通过省略接口名称,ip
命令显示了所有 IP 以及这些 IP 绑定到的接口。这是一种快速简单的方法,可以显示与这台服务器关联的 IP 地址和接口。
从前面的命令中,我们可以看到数据库服务器与应用服务器的配置类似,都有三个接口。在深入之前,让我们更好地了解服务器的接口,并看看我们可以从中识别出什么信息。
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
这台服务器上的第一个接口是环回接口lo
。如前所述,这个接口对于每台服务器来说都是通用的,只用于本地网络流量。这个接口不太可能与我们的问题有关。
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 86304sec preferred_lft 86304sec
inet6 fe80::a00:27ff:fe20:5d4b/64 scope link
valid_lft forever preferred_lft forever
对于第二个接口enp0s3
,数据库服务器的配置与博客服务器非常相似。在 Web 应用服务器上,我们也有一个名为enp0s3
的接口,这个接口也在10.0.2.0/24
网络上。
由于博客和数据库服务器之间的连接似乎是针对 IP192.168.33.12
,因此enp0s3
不是一个需要关注的接口,因为enp0s3
接口的 IP 是10.0.2.16
。
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:c9:d3:65 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.12/24 brd 192.168.33.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fec9:d365/64 scope link
valid_lft forever preferred_lft forever
另一方面,第三个网络设备enp0s8
确实绑定了 IP192.168.33.12
。enp0s8
设备的设置也与博客服务器上的enp0s8
设备类似,因为这两个设备似乎都在192.168.33.0/24
网络上。
通过之前的故障排除,我们知道我们的 Web 应用程序所针对的 IP 是 IP 192.168.33.12。通过ip
命令,我们已经确认 192.168.33.12 通过enp0s8
接口绑定到了这台服务器上。
从 db.example.com 测试连接
现在我们知道数据库服务器有预期的网络配置,我们需要确定这台服务器是否正确连接到192.168.33.0/24
网络。最简单的方法是执行一个我们之前在博客服务器上执行过的任务;使用ping
连接到该子网上的另一个 IP。
[db]# ping 192.168.33.1
PING 192.168.33.1 (192.168.33.1) 56(84) bytes of data.
64 bytes from 192.168.33.1: icmp_seq=1 ttl=64 time=0.438 ms
64 bytes from 192.168.33.1: icmp_seq=2 ttl=64 time=0.208 ms
64 bytes from 192.168.33.1: icmp_seq=3 ttl=64 time=0.209 ms
^C
--- 192.168.33.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.208/0.285/0.438/0.108 ms
通过上面的输出,我们可以看到数据库服务器能够联系到192.168.33.0/24
子网上的另一个 IP。在故障排除时,我们曾试图从博客服务器连接到数据库服务器,但测试失败了。一个有趣的测试是验证当数据库服务器发起连接到博客服务器时,连接是否也失败。
[db]# ping 192.168.33.11
PING 192.168.33.11 (192.168.33.11) 56(84) bytes of data.
From 10.0.2.16 icmp_seq=1 Destination Host Unreachable
From 10.0.2.16 icmp_seq=2 Destination Host Unreachable
From 10.0.2.16 icmp_seq=3 Destination Host Unreachable
From 10.0.2.16 icmp_seq=4 Destination Host Unreachable
^C
--- 192.168.33.11 ping statistics ---
6 packets transmitted, 0 received, +4 errors, 100% packet loss, time 5005ms
从数据库服务器运行ping
命令到博客服务器的 IP(192.168.33.11),我们可以看到 ping 已经回复目标主机不可达。这与我们从博客服务器尝试连接时看到的错误相同。
如前所述,除了网络连接问题之外,ping 失败的原因还有很多;为了确保存在连接问题,我们还应该使用telnet
测试连接。我们知道博客服务器正在接受到 Web 服务器的连接,因此简单地 telnet 到 Web 服务器的端口应该明确告诉我们从数据库服务器到 Web 服务器是否存在任何连接。
运行telnet
时,我们需要指定要连接的端口。我们知道 Web 服务器正在运行,当我们导航到http://blog.example.com
时,我们会得到一个网页。基于这些信息,我们可以确定使用默认的 HTTP 端口并且正在监听。有了这些信息,我们还知道我们可以简单地使用 telnet 连接到端口80
,这是HTTP
通信的默认端口。
[db]# telnet 192.168.33.11 80
-bash: telnet: command not found
但是,在这台服务器上,未安装telnet
。这没关系,因为我们可以像在之前的示例中那样使用curl
命令。
[db]# curl telnet://192.168.33.11:80 -v
* About to connect() to 192.168.33.11 port 80 (#0)
* Trying 192.168.33.11...
* No route to host
* Failed connect to 192.168.33.11:80; No route to host
* Closing connection 0
curl: (7) Failed connect to 192.168.33.11:80; No route to host
从curl
命令的输出中,我们可以看到无论是博客服务器还是数据库服务器发起连接,通信问题都存在。
使用 netstat 查找连接
在之前的部分中,当从博客服务器进行故障排除时,我们使用netstat
查看了到数据库服务器的开放 TCP 连接。现在我们已经登录到数据库服务器,我们可以使用相同的命令从数据库服务器的角度查看连接的状态。为此,我们将使用指定的间隔运行netstat
;这会导致netstat
每 5 秒打印一次网络连接统计,类似于vmstat
或top
命令。
在netstat
命令运行时,我们只需刷新浏览器,使 WordPress 应用程序再次尝试数据库连接。
[db]# netstat -na 5 | grep 192.168.33.11
在我喜欢称为“连续模式”的情况下运行netstat
命令,并使用grep
过滤博客服务器的 IP(192.168.33.11),我们无法看到任何 TCP 连接或连接尝试。
在许多情况下,这似乎表明数据库服务器从未收到来自博客服务器的 TCP 数据包。我们可以通过使用tcpdump
命令在enp0s8
接口上捕获所有网络流量来确认是否是这种情况。
使用 tcpdump 跟踪网络连接
早些时候学习tcpdump
时,我们了解到它默认使用编号最低的接口。这意味着,为了捕获连接尝试,我们必须使用-i
(接口)标志来跟踪正确的接口enp0s8
。除了告诉tcpdump
监视enp0s8
接口外,我们还将让tcpdump
将其输出写入文件。我们这样做是为了尽可能多地捕获数据,并稍后使用tcpdump
命令多次分析数据。
[db]# tcpdump -i enp0s8 -w /var/tmp/db-capture.pcap
tcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 65535 bytes
现在tcpdump
正在运行,我们只需要再次刷新浏览器。
^C110 packets captured
110 packets received by filter
0 packets dropped by kernel
在刷新浏览器并看到捕获的数据包
计数器增加后,我们可以通过在键盘上按Ctrl + C来停止tcpdump
。
一旦tcpdump
停止,我们可以使用-r
(读取)标志读取捕获的数据;但是,这将打印tcpdump
捕获的所有数据包。在某些环境中,这可能是相当多的数据。因此,为了将输出修剪为仅有用的数据,我们将使用port
过滤器告诉tcpdump
仅输出从端口 3306 发起或针对端口 3306 的捕获流量,默认的 MySQL 端口。
我们可以通过在tcpdump
命令的末尾添加port 3306
来实现这一点。
[db]# tcpdump -nnvvv -r /var/tmp/db-capture.pcap port 3306
reading from file /var/tmp/db-capture.pcap, link-type EN10MB (Ethernet)
03:11:03.697543 IP (tos 0x10, ttl 64, id 43196, offset 0, flags [DF], proto TCP (6), length 64)
192.168.33.1.59510 > 192.168.33.12.3306: Flags [S], cksum 0xc125 (correct), seq 2335155468, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 1314733695 ecr 0,sackOK,eol], length 0
03:11:03.697576 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.12.3306 > 192.168.33.1.59510: Flags [S.], cksum 0xc38c (incorrect -> 0x5d87), seq 2658328059, ack 2335155469, win 14480, options [mss 1460,sackOK,TS val 1884022 ecr 1314733695,nop,wscale 6], length 0
03:11:03.697712 IP (tos 0x10, ttl 64, id 61120, offset 0, flags [DF], proto TCP (6), length 52)
192.168.33.1.59510 > 192.168.33.12.3306: Flags [.], cksum 0xb4cd (correct), seq 1, ack 1, win 4117, options [nop,nop,TS val 1314733695 ecr 1884022], length 0
03:11:03.712018 IP (tos 0x8, ttl 64, id 25226, offset 0, flags [DF], proto TCP (6), length 127)
然而,在使用上述过滤器的同时,似乎这个数据库服务器不仅仅被 WordPress 应用程序使用。从tcpdump
输出中,我们可以看到端口3306
上的流量不仅仅是博客服务器。
为了进一步清理此输出,我们可以向tcpdump
命令添加主机过滤器,以仅过滤出我们感兴趣的流量:来自主机192.168.33.11
的流量。
[db]# tcpdump -nnvvv -r /var/tmp/db-capture.pcap port 3306 and host 192.168.33.11
reading from file /var/tmp/db-capture.pcap, link-type EN10MB (Ethernet)
04:04:09.167121 IP (tos 0x0, ttl 64, id 60173, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x4111 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9320053 ecr 0,nop,wscale 6], length 0
04:04:10.171104 IP (tos 0x0, ttl 64, id 60174, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x3d26 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9321056 ecr 0,nop,wscale 6], length 0
04:04:12.175107 IP (tos 0x0, ttl 64, id 60175, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x3552 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9323060 ecr 0,nop,wscale 6], length 0
04:04:16.187731 IP (tos 0x0, ttl 64, id 60176, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x25a5 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9327073 ecr 0,nop,wscale 6], length 0
在这里,我们使用and
运算符告诉tcpdump
只打印到/从端口3306
和到/从主机192.168.33.11
的流量。
tcpdump
命令有许多可能的过滤器和运算符;然而,在所有这些中,我建议熟悉基于端口和主机的过滤,因为这些将足够满足大多数情况。
如果我们分解前面捕获的网络跟踪,我们可以看到一些有趣的信息;为了更容易发现,让我们将输出修剪到只显示正在使用的 IP 和标志。
04:04:09.167121 IP
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S],
04:04:10.171104 IP
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S],
04:04:12.175107 IP
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S],
04:04:16.187731 IP
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S],
从这些信息中,我们可以看到从blog.example.com
(192.168.33.11
)发送的SYN
数据包,并到达db.example.com
(192.168.33.12
)。然而,我们看不到返回的SYN-ACKS
。
这告诉我们,我们至少已经找到了网络问题的源头;服务器db.example.com
没有正确地回复从博客服务器收到的数据包。
现在的问题是:什么会导致这种问题?导致此类问题的原因有很多;但是,一般来说,这样的问题是由网络配置设置中的错误配置引起的。根据我们收集的信息,我们可以假设数据库服务器只是配置错误。
然而,有几种方法可以通过错误配置导致这种类型的问题。为了确定可能的错误配置,我们可以使用tcpdump
命令在此服务器上捕获所有网络流量。
在以前的tcpdump
示例中,我们总是指定单个要监视的接口。在大多数情况下,这对于问题是适当的,因为它减少了tcpdump
捕获的数据量。在非常活跃的服务器上,tcpdump
数据的几分钟可能非常庞大,因此最好将数据减少到只有必需的部分。
然而,在某些情况下,例如这种问题,告诉tcpdump
捕获所有接口的网络流量是有用的。为此,我们只需指定any
作为要监视的接口。
[db]# tcpdump -i any -w /var/tmp/alltraffic.pcap
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
现在我们有tcpdump
捕获并保存所有接口上的所有流量,我们需要再次刷新浏览器,以强制 WordPress 应用程序尝试数据库连接。
^C440 packets captured
443 packets received by filter
0 packets dropped by kernel
经过几次尝试,我们可以再次按Ctrl + C停止tcpdump
。将捕获的网络数据保存到文件后,我们可以开始调查这些连接尝试的情况。
由于tcpdump
捕获了大量数据包,我们将再次使用“主机”过滤器将结果限制为与192.168.33.11
之间的网络流量。
[db]# tcpdump -nnvvv -r /var/tmp/alltraffic.pcap host 192.168.33.11
reading from file /var/tmp/alltraffic.pcap, link-type LINUX_SLL (Linux cooked)
15:37:51.616621 IP (tos 0x0, ttl 64, id 8389, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], cksum 0x34dd (correct), seq 4225047048, win 14600, options [mss 1460,sackOK,TS val 3357389 ecr 0,nop,wscale 6], length 0
15:37:51.616665 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], cksum 0xc396 (incorrect -> 0x3609), seq 1637731271, ack 4225047049, win 14480, options [mss 1460,sackOK,TS val 3330467 ecr 3357389,nop,wscale 6], length 0
15:37:51.616891 IP (tos 0x0, ttl 255, id 2947, offset 0, flags [none], proto TCP (6), length 40)
192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], cksum 0x10c4 (correct), seq 4225047049, win 0, length 0
15:37:52.619386 IP (tos 0x0, ttl 64, id 8390, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], cksum 0x30f2 (correct), seq 4225047048, win 14600, options [mss 1460,sackOK,TS val 3358392 ecr 0,nop,wscale 6], length 0
15:37:52.619428 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], cksum 0xc396 (incorrect -> 0x1987), seq 1653399428, ack 4225047049, win 14480, options [mss 1460,sackOK,TS val 3331470 ecr 3358392,nop,wscale 6], length 0
15:37:52.619600 IP (tos 0x0, ttl 255, id 2948, offset 0, flags [none], proto TCP (6), length 40)
192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], cksum 0x10c4 (correct), seq 4225047049, win 0, length 0
通过捕获的数据,似乎我们已经找到了预期的SYN-ACK
。为了更清晰地展示这一点,让我们将输出修剪到仅包括正在使用的 IP 和标志。
15:37:51.616621 IP
192.168.33.11.47339 > 192.168.33.12.3306: Flags [S],
15:37:51.616665 IP
192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.],
15:37:51.616891 IP
192.168.33.11.47339 > 192.168.33.12.3306: Flags [R],
15:37:52.619386 IP
192.168.33.11.47339 > 192.168.33.12.3306: Flags [S],
15:37:52.619428 IP
192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.],
15:37:52.619600 IP
192.168.33.11.47339 > 192.168.33.12.3306: Flags [R],
通过更清晰的图片,我们可以看到一系列有趣的网络数据包正在传输。
15:37:51.616621 IP
192.168.33.11.47339 > 192.168.33.12.3306: Flags [S],
第一个数据包是从192.168.33.11
到192.168.33.12
的端口3306
的SYN
数据包。这与我们之前使用tcpdump
执行捕获的数据包类型相同。
15:37:51.616665 IP
192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.],
然而,我们以前没有看到第二个数据包。在第二个数据包中,我们看到它是一个SYN-ACK
(由Flags [S.]
标识)。SYN-ACK
是从端口3306
的192.168.33.12
发送到端口47339
的192.168.33.11
(发送原始SYN
数据包的端口)。
乍一看,这似乎是一个正常的SYN
和SYN-ACK
握手。
15:37:51.616891 IP
192.168.33.11.47339 > 192.168.33.12.3306: Flags [R],
然而,第三个数据包很有趣,因为它清楚地表明了一个问题。第三个数据包是一个RESET
数据包(由Flags [R]
标识),从博客服务器192.168.33.11
发送。关于这个有趣的事情是,当在博客服务器上执行tcpdump
时,我们从未捕获到RESET
数据包。如果我们在博客服务器上再次执行tcpdump
,我们可以再次看到这个。
[blog]# tcpdump -i any port 3306
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
15:24:25.646731 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2551514 ecr 0,nop,wscale 6], length 0
15:24:26.648706 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2552516 ecr 0,nop,wscale 6], length 0
15:24:28.652763 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2554520 ecr 0,nop,wscale 6], length 0
15:24:32.660123 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2558528 ecr 0,nop,wscale 6], length 0
15:24:40.676112 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2566544 ecr 0,nop,wscale 6], length 0
15:24:56.724102 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2582592 ecr 0,nop,wscale 6], length 0
从前面的tcpdump
输出中,我们从未看到博客服务器上的SYN-ACK
或RESET
数据包。这意味着RESET
要么是由另一个系统发送的,要么是在tcpdump
捕获之前被博客服务器的内核拒绝了SYN-ACK
数据包。
当tcpdump
命令捕获网络流量时,它是在内核处理这些网络流量之后进行的。这意味着如果由于任何原因内核拒绝了数据包,它将不会通过tcpdump
命令看到。因此,博客服务器的内核在tcpdump
能够捕获它们之前可能会拒绝来自数据库服务器的返回数据包。
通过在数据库上执行tcpdump
,我们还发现了另一个有趣的点,即如果我们查看在enp0s8
上执行的tcpdump
,我们看不到SYN-ACK
数据包。然而,如果我们告诉tcpdump
查看我们使用的所有接口,tcpdump
也会显示SYN-ACK
数据包来自192.168.33.12
。这表明SYN-ACK
是从另一个接口发送的。
为了确认这一点,我们可以再次运行tcpdump
,限制捕获经过enp0s8
接口的数据包。
[db]# tcpdump -nnvvv -i enp0s8 port 3306 and host 192.168.33.11
04:04:09.167121 IP (tos 0x0, ttl 64, id 60173, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x4111 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9320053 ecr 0,nop,wscale 6], length 0
04:04:10.171104 IP (tos 0x0, ttl 64, id 60174, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x3d26 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9321056 ecr 0,nop,wscale 6], length 0
通过这次对tcpdump
的执行,我们只能再次看到来自博客服务器的SYN
数据包。然而,如果我们对所有接口运行相同的tcpdump
,我们不仅应该看到SYN
数据包,还应该看到SYN-ACK
数据包。
[db]# tcpdump -nnvvv -i any port 3306 and host 192.168.33.11
15:37:51.616621 IP (tos 0x0, ttl 64, id 8389, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], cksum 0x34dd (correct), seq 4225047048, win 14600, options [mss 1460,sackOK,TS val 3357389 ecr 0,nop,wscale 6], length 0
15:37:51.616665 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], cksum 0xc396 (incorrect -> 0x3609), seq 1637731271, ack 4225047049, win 14480, options [mss 1460,sackOK,TS val 3330467 ecr 3357389,nop,wscale 6], length 0
返回到192.168.33.11
的SYN-ACK
数据包源自192.168.33.12
。早些时候,我们确定了这个 IP 绑定到网络设备enp0s8
。然而,当我们使用tcpdump
查看所有发送的数据包时,SYN-ACK
并没有被捕获从enp0s8
出去。这意味着SYN-ACK
数据包是从另一个接口发送的。
路由
SYN
数据包如何到达一个接口,而SYN-ACK
却从另一个接口返回呢?一个可能的答案是这是由于数据库服务器路由定义的错误配置。
支持网络的每个操作系统都维护着一个称为路由表的东西。这个路由表是一组定义的网络路由,数据包应该经过的路由。为了给这个概念提供一些背景,让我们以enp0s3
和enp0s8
两个接口为例。
# ip addr show enp0s8
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:c9:d3:65 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.12/24 brd 192.168.33.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fec9:d365/64 scope link
valid_lft forever preferred_lft forever
# ip addr show enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff
inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3
valid_lft 65115sec preferred_lft 65115sec
inet6 fe80::a00:27ff:fe20:5d4b/64 scope link
valid_lft forever preferred_lft forever
如果我们查看这两个接口,我们知道enp0s8
接口连接到192.168.33.0/24
(inet 192.168.33.12/24
)网络,而enp0s3
接口连接到10.0.2.0/24
(inet 10.0.2.16/24
)网络。
如果我们要连接到 IP 10.0.2.19,数据包不应该从enp0s8
接口出去,因为这些数据包的最佳路由应该是通过enp0s3
接口路由。这是最佳路由的原因是enp0s3
接口已经是10.0.2.0/24
网络的一部分,其中包含 IP10.0.2.19
。
enp0s8
接口是不同网络(192.168.33.0/24
)的一部分,因此是不太理想的路由。事实上,enp0s8
接口甚至可能无法路由到10.0.2.0/24
网络。
即使enp0s8
可能是一个不太理想的路由,内核在没有路由表中对应条目的情况下是不知道这一点的。为了更深入地了解我们的问题,我们需要查看数据库服务器上的路由表。
查看路由表
在 Linux 中,有几种方法可以查看当前的路由表;在本节中,我将介绍两种方法。第一种方法将利用netstat
命令。
要使用netstat
命令查看路由表,只需使用-r
(route)或--route
标志运行它。在下面的例子中,我们还将使用-n
标志防止netstat
执行 DNS 查找。
[db]# netstat -rn
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 enp0s3
10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3
169.254.0.0 0.0.0.0 255.255.0.0 U 0 0 0 enp0s8
192.168.33.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s8
192.168.33.11 10.0.2.1 255.255.255.255 UGH 0 0 0 enp0s3
虽然netstat
可能不是打印路由表的最佳 Linux 命令,但在这个例子中使用它有一个非常特殊的原因。正如我在本章和本书中早些时候提到的,netstat
命令是一个几乎存在于每台现代服务器、路由器或台式机上的通用工具。通过了解如何使用netstat
查看路由表,您可以在安装了netstat
的任何操作系统上执行基本的网络故障排除。
一般来说,可以肯定地说netstat
命令是可用的,并且可以为您提供系统网络状态和配置的基本细节。
与其他实用程序(如ip
命令)相比,netstat
的格式可能有点晦涩。然而,前面的路由表向我们展示了相当多的信息。为了更好地理解,让我们逐条分解输出的路由。
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 enp0s3
正如你所看到的,netstat
命令的输出有多列,确切地说有八列。第一列是Destination
列。这用于定义路由范围内的目标地址。在前面的例子中,目的地是0.0.0.0
,这实际上是一个通配值,意味着任何东西都应该通过这个表项进行路由。
第二列是Gateway
。网关地址是利用这条路由的网络数据包应该发送到的下一跳。在这个例子中,下一跳或网关地址设置为10.0.2.2
;这意味着通过这个表项进行路由的任何数据包将被发送到10.0.2.2
,然后应该将数据包路由到下一个系统,直到它们到达目的地。
第三列是Genmask
,本质上是一种陈述路由的“一般性
”的方式。另一种思考这一列的方式是作为netmask
;在前面的例子中,“genmask
”设置为0.0.0.0
,这是一个开放范围。这意味着任何地方的数据包都应该通过这个路由表项进行路由。
第四列是Flag
列,用于提供有关这条路由的具体信息。例子中的U
值表示此路由使用的接口处于上行状态。G
值用于显示路由使用了网关地址。在前面的例子中,我们可以看到我们的路由使用了网关地址;然而,并非这个系统的所有路由都是这样。
第五、第六和第七列在 Linux 服务器上并不经常使用。MSS
列用于显示为这条路由指定的最大分段大小。值为 0 意味着此值设置为默认值且未更改。
Window
列是 TCP 窗口大小,表示单个突发接受的最大数据量。同样,当值设置为 0 时,将使用默认大小。
第七列是irtt
,用于指定这条路由的初始往返时间。内核将通过设置初始往返时间重新发送从未得到响应的数据包;您可以增加或减少内核认为数据包丢失之后的时间。与前两列一样,值为 0 意味着使用这条路由的数据包将使用默认值。
第八和最后一列是IFace
列,是利用这条路由的数据包应该使用的网络接口。在前面的例子中,这是enp0s3
接口。
默认路由
我们例子中的第一条路由实际上是我们系统的一个非常特殊的路由。
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 enp0s3
如果我们查看这条路由的详细信息和每列的定义,我们可以确定这条路由是服务器的默认路由。默认路由是一种特殊路由,在没有其他路由取代它时“默认”使用。简而言之,如果我们有要发送到诸如172.0.0.10
的地址的数据包,这些数据包将通过默认路由发送。
这是因为我们的数据库服务器路由表中没有其他指定 IP172.0.0.10
的路由。因此,系统只需通过默认路由发送数据包到这个 IP,这是一个万能路由。
我们可以通过目的地址为0.0.0.0
来确定第一条路由是服务器的默认路由,这基本上意味着任何东西。第二个指示是Genmask
为0.0.0.0
,这与目的地一起意味着任何 IPv4 地址。
默认路由通常使用网关地址,因此网关为destination
和genmask
设置通配符是明确表明上述路由是默认路由的迹象。
非默认路由通常看起来像这样:
10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3
上述路由的目的地是 10.0.2.0,genmask
为 255.255.255.0;这基本上意味着 10.0.2.0/24 网络中的任何内容都会匹配这条路由。
由于这条路由的范围是10.0.2.0/24
,很可能是由enp0s3
接口配置添加的。我们可以根据enp0s3
接口配置来确定这一点,因为它连接到10.0.2.0/24
网络,这是这条路由的目标。默认情况下,Linux 会根据网络接口的配置自动添加路由。
10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3
这条路由是内核确保10.0.2.0/24
网络的通信通过enp0s3
接口进行的一种方式,因为这条路由将取代默认路由。在网络路由中,将始终使用最具体的路由。由于默认路由是通配符,而这条路由是特定于10.0.2.0/24
网络的,因此这条路由将用于网络中的任何内容。
利用 IP 显示路由表
审查路由表的另一个工具是ip
命令。从本章中使用的情况可以看出,ip
命令是一个非常全面的实用工具,几乎可以用于现代 Linux 系统上的任何网络相关事务。
ip
命令的一个用途是添加、删除或显示网络路由配置。要显示当前的路由表,只需执行带有route show
选项的ip
命令。
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
169.254.0.0/16 dev enp0s8 scope link metric 1003
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1
虽然学习使用netstat
命令对于非 Linux 操作系统很重要,但ip
命令是任何 Linux 网络故障排除或配置的基本工具。
使用ip
命令来排除故障路由时,我们甚至可能会发现它比netstat
命令更容易。一个例子是查找默认路由。当ip
命令显示默认路由时,它使用单词"default"作为目的地,而不是 0.0.0.0,这种方法对于新系统管理员来说更容易理解。
阅读其他路由也更容易。例如,之前在netstat
中查看路由时,我们的示例路由如下:
10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3
使用ip
命令,相同的路由以以下格式显示:
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
在我看来,ip
route show 的格式比netstat -rn
命令的格式简单得多。
寻找路由错误配置
现在我们知道如何查看服务器上的路由表,我们可以使用ip
命令查找可能导致数据库连接问题的任何路由。
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
169.254.0.0/16 dev enp0s8 scope link metric 1003
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1
在这里,我们可以看到系统上定义了五条路由。让我们分解这些路由,以更好地理解它们。
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
我们已经介绍了前两条路由,不会再次复习。
169.254.0.0/16 dev enp0s8 scope link metric 1003
第三条路由定义了所有来自169.254.0.0/16
(169.254.0.0
到169.254.255.255
)的流量通过enp0s8
设备发送。这是一个非常广泛的路由,但很可能不会影响我们到 IP192.168.33.11
的路由。
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1
然而,第四和第五条路由将改变网络数据包到 192.168.33.11 的路由方式。
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
第四条路由定义了所有流量到192.168.33.0/24
(192.168.33.0
到192.168.33.255
)网络都从enp0s8
接口路由,并且源自192.168.33.12
。这条路由似乎也是由enp0s8
接口的配置自动添加的;这与enp0s3
添加的早期路由类似。
由于enp0s8
设备被定义为192.168.33.0/24
网络的一部分,将该网络的流量路由到这个接口是合理的。
192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1
然而,第五条路由定义了所有流量到特定 IP192.168.33.11
(博客服务器的 IP)都通过enp0s3
设备发送到10.0.2.1
的网关。这很有趣,因为第五条路由和第四条路由有非常冲突的配置,因为它们都定义了对192.168.33.0/24
网络中的 IP 该怎么做。
更具体的路由获胜
正如前面提到的,路由网络数据包的“黄金法则”是更具体的路由总是获胜。如果我们查看路由配置,我们有一个路由,它表示192.168.33.0/24
子网中的所有流量应该从enp0s8
设备出去。还有第二条路由,它明确表示192.168.33.11
应该通过enp0s3
设备出去。IP192.168.33.11
适用于这两条规则,但系统应该通过哪条路由发送数据包呢?
答案总是更具体的路由。
由于第二条路由明确定义了所有流量到192.168.33.11
都从enp0s3
接口出去,内核将通过enp0s3
接口路由所有返回的数据包。这种情况不受192.168.33.0/24
或甚至默认路由的影响。
我们可以通过使用带有route get
选项的ip
命令来看到所有这些情况。
[db]# ip route get 192.168.33.11
192.168.33.11 via 10.0.2.1 dev enp0s3 src 10.0.2.16
cache
带有route get
选项的ip
命令将获取提供的 IP 并输出数据包将经过的路由。
当我们使用这个命令与192.168.33.11
一起使用时,我们可以看到ip
明确显示数据包将通过enp0s3
设备。如果我们使用相同的命令与其他 IP 一起使用,我们可以看到默认路由和192.168.33.0/24
路由是如何使用的。
[db]# ip route get 192.168.33.15
192.168.33.15 dev enp0s8 src 192.168.33.12
cache
[db]# ip route get 4.4.4.4
4.4.4.4 via 10.0.2.2 dev enp0s3 src 10.0.2.16
cache
[db]# ip route get 192.168.33.200
192.168.33.200 dev enp0s8 src 192.168.33.12
cache
[db]# ip route get 169.254.3.5
169.254.3.5 dev enp0s8 src 192.168.33.12
cache
我们可以看到,当提供一个特定路由定义的子网内的 IP 地址时,将采用这个特定路由。然而,当 IP 没有被特定路由定义时,将采用默认路由。
假设
现在我们了解了到192.168.33.11
的数据包是如何路由的,我们应该调整我们之前的假设,以反映192.168.33.11
到enp0s3
的路由是不正确的,并且导致了我们的问题。
基本上,正在发生的事情(我们通过tcpdump
看到了这一点)是,当数据库服务器(192.168.33.12
)从博客服务器(192.168.33.11
)接收到网络数据包时,它是通过enp0s8
设备到达的。然而,当数据库服务器发送回复数据包(SYN-ACK
)到 Web 应用服务器时,数据包是通过enp0s3
接口发送出去的。
由于enp0s3
设备连接到10.0.2.0/24
网络,似乎数据包被10.0.2.0/24
网络上的另一个系统或设备拒绝(RESET
)。很可能,这是由于这是异步路由的一个典型例子。
异步路由是指数据包到达一个接口,但在另一个接口上回复。在大多数网络配置中,默认情况下是被拒绝的,但在某些情况下可以被启用;然而,这些情况并不是非常常见。
在我们的情况下,由于enp0s8
接口是192.168.33.0/24
子网的一部分,启用异步路由是没有意义的。我们的数据包到192.168.33.11
应该简单地通过enp0s8
接口路由。
反复试验
现在我们已经确定了数据收集的问题,并建立了我们的假设可能的原因,我们可以开始下一个故障排除步骤:使用试错法来纠正问题。
删除无效路由
为了纠正我们的问题,我们需要删除针对192.168.33.11
的无效路由。为此,我们将再次使用ip
命令,这次使用route del
选项。
[db]# ip route del 192.168.33.11
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
169.254.0.0/16 dev enp0s8 scope link metric 1003
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
在前面的例子中,我们使用了ip
命令和route del
选项来删除针对单个 IP 的路由。我们可以使用相同的命令和选项来删除针对子网定义的路由。以下示例将删除169.254.0.0/16
网络的路由:
[db]# ip route del 169.254.0.0/16
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
从ip route show
的执行中,我们可以看到192.168.33.11
不再存在冲突的路由。问题是:这个修复了我们的问题吗?唯一确定的方法是测试它,为此我们可以简单地刷新加载了博客错误页面的浏览器。
看来我们成功地纠正了问题。如果我们现在执行tcpdump
,我们可以验证博客和数据库服务器能够通信。
[db]# tcpdump -nnvvv -i enp0s8 port 3306
tcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 65535 bytes
16:14:05.958507 IP (tos 0x0, ttl 64, id 7605, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.47350 > 192.168.33.12.3306: Flags [S], cksum 0xa9a7 (correct), seq 4211276877, win 14600, options [mss 1460,sackOK,TS val 46129656 ecr 0,nop,wscale 6], length 0
16:14:05.958603 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.12.3306 > 192.168.33.11.47350: Flags [S.], cksum 0xc396 (incorrect -> 0x786b), seq 2378639726, ack 4211276878, win 14480, options [mss 1460,sackOK,TS val 46102446 ecr 46129656,nop,wscale 6], length 0
16:14:05.959103 IP (tos 0x0, ttl 64, id 7606, offset 0, flags [DF], proto TCP (6), length 52)
192.168.33.11.47350 > 192.168.33.12.3306: Flags [.], cksum 0xdee0 (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 46129657 ecr 46102446], length 0
16:14:05.959336 IP (tos 0x8, ttl 64, id 24256, offset 0, flags [DF], proto TCP (6), length 138)
192.168.33.12.3306 > 192.168.33.11.47350: Flags [P.], cksum 0xc3e4 (incorrect -> 0x99c9), seq 1:87, ack 1, win 227, options [nop,nop,TS val 46102447 ecr 46129657], length 86
16:14:05.959663 IP (tos 0x0, ttl 64, id 7607, offset 0, flags [DF], proto TCP (6), length 52)
前面的输出是我们从一个健康的连接中期望看到的。
在这里,我们看到四个数据包,第一个是来自blog.example.com
(192.168.33.11
)的SYN
(Flags [S]
),接着是来自db.example.com
(192.168.33.12
)的SYN-ACK
(Flags [S.]
),以及来自blog.example.com
(192.168.33.12
)的ACK
(或SYN-ACK-ACK
)(Flags [.]
)。这三个数据包是完成的 TCP 三次握手。第四个数据包是一个PUSH
(Flags [P.]
)数据包,这是实际的数据传输。所有这些都是良好工作的网络连接的迹象。
配置文件
现在我们已经从路由表中删除了无效的路由,我们可以看到博客正在工作;这意味着我们已经完成了,对吗?不,至少还没有。
当我们使用ip
命令删除路由时,我们从活动路由表中删除了路由,但没有从整个系统中删除路由。如果我们重新启动网络,或者简单地重新启动服务器,这个无效的路由将重新出现。
[db]# service network restart
Restarting network (via systemctl): [ OK ]
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
169.254.0.0/16 dev enp0s8 scope link metric 1003
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1
这是因为当系统启动时,它会根据一组文件中的配置来配置网络。ip
命令用于操作实时网络配置,而不是这些网络配置文件。因此,使用ip
命令进行的任何更改都不是永久性的,而只是暂时的,直到系统下一次读取和应用网络配置为止。
为了完全从网络配置中删除这个路由,我们需要修改网络配置文件。
[db]# cd /etc/sysconfig/network-scripts/
在基于 Red Hat 企业 Linux 的系统上,网络配置文件大多存储在/etc/sysconfig/network-scripts
文件夹中。首先,我们可以切换到这个文件夹并执行ls -la
来识别当前的网络配置文件。
[db]# ls -la
total 228
drwxr-xr-x. 2 root root 4096 Mar 14 14:37 .
drwxr-xr-x. 6 root root 4096 Mar 14 23:42 ..
-rw-r--r--. 1 root root 195 Jul 22 2014 ifcfg-enp0s3
-rw-r--r--. 1 root root 217 Mar 14 14:37 ifcfg-enp0s8
-rw-r--r--. 1 root root 254 Apr 2 2014 ifcfg-lo
lrwxrwxrwx. 1 root root 24 Jul 22 2014 ifdown -> ../../../usr/sbin/ifdown
-rwxr-xr-x. 1 root root 627 Apr 2 2014 ifdown-bnep
-rwxr-xr-x. 1 root root 5553 Apr 2 2014 ifdown-eth
-rwxr-xr-x. 1 root root 781 Apr 2 2014 ifdown-ippp
-rwxr-xr-x. 1 root root 4141 Apr 2 2014 ifdown-ipv6
lrwxrwxrwx. 1 root root 11 Jul 22 2014 ifdown-isdn -> ifdown-ippp
-rwxr-xr-x. 1 root root 1642 Apr 2 2014 ifdown-post
-rwxr-xr-x. 1 root root 1068 Apr 2 2014 ifdown-ppp
-rwxr-xr-x. 1 root root 837 Apr 2 2014 ifdown-routes
-rwxr-xr-x. 1 root root 1444 Apr 2 2014 ifdown-sit
-rwxr-xr-x. 1 root root 1468 Jun 9 2014 ifdown-Team
-rwxr-xr-x. 1 root root 1532 Jun 9 2014 ifdown-TeamPort
-rwxr-xr-x. 1 root root 1462 Apr 2 2014 ifdown-tunnel
lrwxrwxrwx. 1 root root 22 Jul 22 2014 ifup -> ../../../usr/sbin/ifup
-rwxr-xr-x. 1 root root 12449 Apr 2 2014 ifup-aliases
-rwxr-xr-x. 1 root root 859 Apr 2 2014 ifup-bnep
-rwxr-xr-x. 1 root root 10223 Apr 2 2014 ifup-eth
-rwxr-xr-x. 1 root root 12039 Apr 2 2014 ifup-ippp
-rwxr-xr-x. 1 root root 10430 Apr 2 2014 ifup-ipv6
lrwxrwxrwx. 1 root root 9 Jul 22 2014 ifup-isdn -> ifup-ippp
-rwxr-xr-x. 1 root root 642 Apr 2 2014 ifup-plip
-rwxr-xr-x. 1 root root 1043 Apr 2 2014 ifup-plusb
-rwxr-xr-x. 1 root root 2609 Apr 2 2014 ifup-post
-rwxr-xr-x. 1 root root 4154 Apr 2 2014 ifup-ppp
-rwxr-xr-x. 1 root root 1925 Apr 2 2014 ifup-routes
-rwxr-xr-x. 1 root root 3263 Apr 2 2014 ifup-sit
-rwxr-xr-x. 1 root root 1628 Oct 31 2013 ifup-Team
-rwxr-xr-x. 1 root root 1856 Jun 9 2014 ifup-TeamPort
-rwxr-xr-x. 1 root root 2607 Apr 2 2014 ifup-tunnel
-rwxr-xr-x. 1 root root 1621 Apr 2 2014 ifup-wireless
-rwxr-xr-x. 1 root root 4623 Apr 2 2014 init.ipv6-global
-rw-r--r--. 1 root root 14238 Apr 2 2014 network-functions
-rw-r--r--. 1 root root 26134 Apr 2 2014 network-functions-ipv6
-rw-r--r--. 1 root root 30 Mar 13 02:20 route-enp0s3
从目录列表中,我们可以看到几个配置文件。然而,一般来说,我们主要只对以ifcfg-
开头的文件和以route-
开头的文件感兴趣。
以ifcfg-
开头的文件用于定义网络接口;这些文件的命名约定是“ifcfg-<设备名称>”;例如,要查看enp0s8
的配置,我们可以读取ifcfg-enp0s8
文件。
[db]# cat ifcfg-enp0s8
NM_CONTROLLED=no
BOOTPROTO=none
ONBOOT=yes
IPADDR=192.168.33.12
NETMASK=255.255.255.0
DEVICE=enp0s8
PEERDNS=no
我们可以看到,这个配置文件定义了用于这个接口的 IP 地址和Netmask
。
"route-
"文件用于定义系统的路由配置。这个文件的约定与接口文件的约定相似,即"route-<设备名称>
"。在文件夹列表中,只有一个路由文件route-enp0s3
。这是定义不正确路由的最可能位置。
[db]# cat route-enp0s3
192.168.33.11/32 via 10.0.2.1
一般来说,除非定义了静态路由(静态定义的路由),否则"route-*
"文件是不存在的。我们可以看到这里只定义了一个路由在这个文件中,这意味着路由表中定义的所有其他路由都是根据接口配置动态配置的。
在上面的例子中,route-enp0s3
文件中定义的路由没有指定接口。因此,接口将根据文件名来定义;如果相同的条目出现在route-enp0s8
文件中,网络服务将尝试在enp0s8
接口上定义路由。
为了确保这个路由不再出现在路由表中,我们需要从这个文件中删除它;或者,在这种情况下,因为它是唯一的路由,我们应该完全删除这个文件。
[db]# rm route-enp0s3
rm: remove regular file 'route-enp0s3'? y
决定删除文件和路由取决于所支持的环境;如果您不确定这是否是正确的操作,应该询问能告诉您事先是否正确的人。在这个例子中,我们将假设可以删除这个网络配置文件。
重新启动网络服务后,我们应该看到路由消失。
[db]# service network restart
Restarting network (via systemctl): [ OK ]
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16
169.254.0.0/16 dev enp0s8 scope link metric 1003
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
现在路由已经消失,网络配置已经重新加载,我们可以安全地说我们已经解决了问题。我们可以通过再次加载网页来验证这一点,以确保博客正常工作。
总结
如果我们回顾一下本章,我们对在 Linux 上解决网络连接问题学到了很多。我们学会了如何使用netstat
和tcpdump
工具来查看传入和传出的连接。我们了解了 TCP 的三次握手以及/etc/hosts
文件如何取代 DNS 设置。
在本章中,我们涵盖了许多命令,虽然我们对每个命令及其功能都有一个相当好的概述,但有一些命令我们只是浅尝辄止。
诸如tcpdump
之类的命令就是一个很好的例子。在本章中,我们使用了tcpdump
相当多,但这个工具的功能远不止我们在本章中使用的那些。在本书中涵盖的所有命令中,我个人认为tcpdump
是一个值得花时间学习的工具,因为它是一个非常有用和强大的工具。我用它解决了许多问题,有时这些问题不是特定于网络,而是特定于应用程序的。
在下一章中,我们将继续保持这种网络动力,解决防火墙问题。我们可能会看到一些在本章中使用的相同命令在下一章中再次出现,但这没关系;这只是显示了理解网络和故障排除工具的重要性。
第六章:诊断和纠正防火墙问题
在上一章中,我们发现了如何使用telnet
、ping
、curl
、netstat
、tcpdump
和ip
等命令来解决与网络相关的问题。您还了解了TCP 协议的工作原理,以及如何使用DNS将域名转换为 IP。
在本章中,我们将再次解决与网络相关的问题;然而,这一次我们将了解 Linux 的软件防火墙iptables
的工作原理以及如何解决防火墙引起的网络问题。
诊断防火墙
第五章网络故障排除是关于网络和如何排除网络配置错误的。在本章中,我们将把这个讨论扩展到防火墙。在解决防火墙问题时,我们可能会使用与第五章网络故障排除相同的一些命令,并重复很多相同的过程。这是因为每当你使用防火墙来保护系统时,你都会阻止某些类型的网络流量,防火墙的配置错误可能会影响系统的任何网络流量。
我们将像其他章节一样,从解决报告的问题开始这一章。
似曾相识
在第五章网络故障排除中,我们的故障排除是在一位开发人员打来电话报告公司的博客报告了数据库连接错误后开始的。经过故障排除,我们发现这个错误是由于数据库服务器上的静态路由配置错误造成的。然而,今天(几天后),我们再次接到同一开发人员报告相同的问题。
当开发人员访问http://blog.example.com
时,他收到一个错误,指出存在数据库连接问题。又来了!
由于数据收集的第一步是复制问题,我们应该做的第一件事是在我们自己的浏览器上打开公司的博客。
事实上,似乎同样的错误再次出现了;现在要找出原因。
从历史问题中解决问题
数据收集器的第一反应可能是简单地按照第五章网络故障排除中的相同故障排除步骤进行。然而,适配器和受过教育的猜测故障排除者知道几天前的问题是由于静态路由,他们会首先登录到数据库服务器并检查是否存在相同的静态路由。
也许有人只是错误地重新添加了它,或者路由没有完全从系统的配置文件中删除:
[db]# ip route show
default via 10.0.2.2 dev enp0s3 proto static metric 1024
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15
169.254.0.0/16 dev enp0s8 scope link metric 1003
192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12
然而,不幸的是,我们的运气并不那么好;从ip
命令的结果中,我们可以看到来自第五章网络故障排除的静态路由不存在。
由于路由不存在,我们需要重新从第一步开始,检查博客服务器是否能够连接到数据库服务器。
基本故障排除
我们应该进行的第一个测试是从博客服务器到数据库服务器的简单 ping。这将很快回答这两台服务器是否能够进行通信:
[blog]$ ping db.example.com
PING db.example.com (192.168.33.12) 56(84) bytes of data.
64 bytes from db.example.com (192.168.33.12): icmp_seq=1 ttl=64 time=0.420 ms
64 bytes from db.example.com (192.168.33.12): icmp_seq=2 ttl=64 time=0.564 ms
64 bytes from db.example.com (192.168.33.12): icmp_seq=3 ttl=64 time=0.562 ms
64 bytes from db.example.com (192.168.33.12): icmp_seq=4 ttl=64 time=0.479 ms
^C
--- db.example.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
rtt min/avg/max/mdev = 0.420/0.506/0.564/0.062 ms
从ping
命令的结果中,我们可以看到博客服务器可以与数据库服务器通信,或者说,博客服务器向数据库服务器发送了ICMP 回显请求并收到了ICMP 回显回复。我们可以测试的下一个连接是到端口3306
,即 MySQL 端口的连接。
我们将使用telnet
命令测试这种连接:
[blog]$ telnet db.example.com 3306
Trying 192.168.33.12...
telnet: connect to address 192.168.33.12: No route to host
然而,telnet
失败了。这表明博客服务器与数据库服务器上的数据库服务实际上存在问题。
验证 MariaDB 服务
现在我们已经确定了博客服务器无法与数据库服务器通信,我们需要确定原因。在假设问题严格是与网络相关之前,我们应该首先验证数据库服务是否正在运行。为了做到这一点,我们只需登录到数据库服务器并检查正在运行的数据库进程。
我们可以使用多种方法来验证数据库进程是否在运行。在下面的例子中,我们将再次使用ps
命令:
[db]$ ps -elf | grep maria
0 S mysql 1529 1123 0 80 0 - 226863 poll_s 12:21 ? 00:00:04 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --log-error=/var/log/mariadb/mariadb.log --pid-file=/var/run/mariadb/mariadb.pid --socket=/var/lib/mysql/mysql.sock
通过ps
命令,我们能够看到正在运行的MariaDB进程。在前面的例子中,我们使用了ps -elf
命令来显示所有进程,然后使用grep
命令来过滤输出以找到 MariaDB 服务。
从结果来看,数据库服务实际上是在运行的;但这并不能确定这个进程是否在端口3306
上接受连接。为了验证这一点,我们可以使用netstat
命令来识别服务器上正在监听的端口:
[db]$ netstat -na | grep LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:46788 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp6 0 0 ::1:25 :::* LISTEN
tcp6 0 0 :::111 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 :::49464 :::* LISTEN
从netstat
命令中,我们可以看到系统上有很多端口是打开的,端口3306
就是其中之一。
由于我们知道博客服务器无法与端口3306
建立连接,我们可以再次从多个地方测试连接。第一个地方是数据库服务器本身,第二个地方是我们的笔记本电脑,就像我们在第五章 网络故障排除中所做的那样。
由于数据库服务器没有安装telnet
客户端,我们可以使用curl
命令来执行这个测试:
[blog]$ curl -v telnet://localhost:3306
* About to connect() to localhost port 3306 (#0)
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3306 (#0)
R
* RCVD IAC EC
提示
在本书中,我会反复强调知道执行任务的多种方法是很重要的。telnet
是一个非常简单的例子,但这个概念适用于系统管理员执行的每一个任务。
既然我们已经确定了数据库服务器可以从本地服务器访问,我们现在可以从我们的笔记本电脑上测试:
[laptop]$ telnet 192.168.33.12 3306
Trying 192.168.33.12...
telnet: connect to address 192.168.33.12: Connection refused
telnet: Unable to connect to remote host
从我们的笔记本电脑上看,连接到数据库服务是不可用的,但如果我们测试另一个端口,比如22
会发生什么呢?
[laptop]$ telnet 192.168.33.12 22
Trying 192.168.33.12...
Connected to 192.168.33.12.
Escape character is '^]'.
SSH-2.0-OpenSSH_6.4
^]
telnet>
这是一个有趣的结果;从笔记本电脑上,我们能够连接到端口22
,但无法连接到端口3306
。既然端口22
在笔记本电脑上是可用的,那么在博客服务器上呢?
[blog]$ telnet db.example.com 22
Trying 192.168.33.12...
Connected to db.example.com.
Escape character is '^]'.
SSH-2.0-OpenSSH_6.4
^]
这些结果非常有趣。在上一章中,当我们的连接问题是由于错误配置的静态路由时,博客服务器和数据库服务器之间的所有通信都中断了。
然而,在这个问题的情况下,博客服务器无法连接到端口3306
,但它可以在端口22
上与数据库服务器通信。使这个问题更有趣的是,在数据库服务器上本地,端口3306
是可用的并且接受连接。
这些关键信息是指示我们的问题可能实际上是由于防火墙引起的第一个迹象。现在可能还为时过早使用数据收集器,但是一个适配器或有经验的猜测故障排除者可能已经在这一点上形成了一个假设,即这个问题是由于防火墙引起的。
使用 tcpdump 进行故障排除
在第五章 网络故障排除中,我们广泛使用了tcpdump
来识别我们的问题;我们能否用tcpdump
来判断问题是防火墙问题?也许可以,我们肯定可以使用tcpdump
来更好地查看问题。
首先,我们将从博客服务器捕获到端口22
的连接(我们知道这个连接是有效的)。tcpdump
将在数据库服务器上过滤端口22
运行;我们还将使用-i
(接口)标志和any
选项,使tcpdump
捕获所有网络接口上的流量:
[db]# tcpdump -nnnvvv -i any port 22
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
一旦tcpdump
运行起来,我们可以从博客服务器发起到端口22
的连接,看看一个完整的健康连接是什么样子的:
03:03:15.670771 IP (tos 0x10, ttl 64, id 17278, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.34133 > 192.168.33.12.22: Flags [S], cksum 0x977b (correct), seq 2193487479, win 14600, options [mss 1460,sackOK,TS val 7058697 ecr 0,nop,wscale 6], length 0
03:03:15.670847 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.12.22 > 192.168.33.11.34133: Flags [S.], cksum 0xc396 (correct), seq 3659372781, ack 2193487480, win 14480, options [mss 1460,sackOK,TS val 7018839 ecr 7058697,nop,wscale 6], length 0
03:03:15.671295 IP (tos 0x10, ttl 64, id 17279, offset 0, flags [DF], proto TCP (6), length 52)
192.168.33.11.34133 > 192.168.33.12.22: Flags [.], cksum 0x718b (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 7058697 ecr 7018839], length 0
从捕获的数据中,我们可以看到一个标准的健康连接。我们可以看到连接来自 IP192.168.33.11
,即博客服务器的 IP。我们还可以看到连接通过端口22
到达了 IP192.168.33.12
。我们可以从以下行中看到所有这些信息:
192.168.33.11.34133 > 192.168.33.12.22: Flags [S], cksum 0x977b (correct), seq 2193487479, win 14600, options [mss 1460,sackOK,TS val 7058697 ecr 0,nop,wscale 6], length 0
从第二个捕获的数据包中,我们可以看到数据库服务器对博客服务器的SYN-ACK回复:
192.168.33.12.22 > 192.168.33.11.34133: Flags [S.], cksum 0x0b15 (correct), seq 3659372781, ack 2193487480, win 14480, options [mss 1460,sackOK,TS val 7018839 ecr 7058697,nop,wscale 6], length 0
我们可以看到SYN-ACK
回复来自192.168.33.12
IP 地址到192.168.33.11
IP 地址。到目前为止,TCP 连接似乎正常,第三个捕获的数据包肯定证实了这一点:
192.168.33.11.34133 > 192.168.33.12.22: Flags [.], cksum 0x718b (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 7058697 ecr 7018839], length 0
第三个数据包是来自博客服务器的SYN-ACK-ACK。这意味着不仅博客服务器的SYN
数据包到达并得到SYN-ACK
的回复,数据库服务器的SYN-ACK
数据包也被博客服务器接收,并得到了SYN-ACK-ACK
的回复。这是端口22
的完整三次握手。
现在,让我们来看看到端口3306
的连接。为此,我们将使用相同的tcpdump
命令,这次将端口更改为3306
:
[db]# tcpdump -nnnvvv -i any port 3306
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
在tcpdump
运行时,我们可以从博客服务器使用telnet
建立连接:
[blog]$ telnet db.example.com 3306
Trying 192.168.33.12...
telnet: connect to address 192.168.33.12: No route to host
如预期的那样,telnet
命令未能连接;让我们看看tcpdump
在此期间是否捕获了任何数据包:
06:04:25.488396 IP (tos 0x10, ttl 64, id 44350, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.55002 > 192.168.33.12.3306: Flags [S], cksum 0x7699 (correct), seq 3266396266, win 14600, options [mss 1460,sackOK,TS val 12774740 ecr 0,nop,wscale 6], length 0
事实上,看起来tcpdump
确实捕获了一个数据包,但只有一个。
捕获的数据包是从192.168.33.11
(博客服务器)发送到192.168.33.12
(数据库服务器)的SYN
数据包。这表明来自博客服务器的数据包到达了数据库服务器;但我们看不到回复数据包。
正如您在上一章中学到的,当我们对tcpdump
应用过滤器时,我们经常会错过一些东西。在这种情况下,我们正在过滤tcpdump
以查找从端口3306
发送或接收的流量。由于我们知道问题的服务器是博客服务器,我们可以通过使用tcpdump
的主机过滤器来更改我们的过滤器,以捕获来自博客服务器 IP192.168.33.11
的所有流量。我们可以通过使用tcpdump
的主机过滤器来实现这一点:
[db]# tcpdump -nnnvvv -i any host 192.168.33.11
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
再次运行tcpdump
,我们可以再次从博客服务器使用telnet
发起连接:
[blog]$ telnet db.example.com 3306
Trying 192.168.33.12...
telnet: connect to address 192.168.33.12: No route to host
同样,预期地,telnet 连接失败了;然而,这次我们可以从tcpdump
中看到更多信息:
06:16:49.729134 IP (tos 0x10, ttl 64, id 23760, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.55003 > 192.168.33.12.3306: Flags [S], cksum 0x9be6 (correct), seq 1849431125, win 14600, options [mss 1460,sackOK,TS val 13518981 ecr 0,nop,wscale 6], length 0
06:16:49.729199 IP (tos 0xd0, ttl 64, id 40207, offset 0, flags [none], proto ICMP (1), length 88)
192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68
这一次我们实际上可以看到相当多有用的信息,直接表明我们的问题是由于系统防火墙引起的。
看起来tcpdump
能够捕获两个数据包。让我们分析一下它能够捕获到的内容,以更好地理解发生了什么:
06:16:49.729134 IP (tos 0x10, ttl 64, id 23760, offset 0, flags [DF], proto TCP (6), length 60)
192.168.33.11.55003 > 192.168.33.12.3306: Flags [S], cksum 0x9be6 (correct), seq 1849431125, win 14600, options [mss 1460,sackOK,TS val 13518981 ecr 0,nop,wscale 6], length 0
第一个数据包与我们之前看到的一样,是从博客服务器到数据库服务器端口3306
的简单SYN
请求。然而,第二个数据包非常有趣:
06:16:49.729199 IP (tos 0xd0, ttl 64, id 40207, offset 0, flags [none], proto ICMP (1), length 88)
192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68
第二个数据包甚至不是基于 TCP 的数据包,而是一个ICMP数据包。在第五章网络故障排除中,我们讨论了 ICMP 回显请求和回复数据包,以及它们如何被ping
命令用于识别主机是否可用。然而,ICMP 用于的不仅仅是ping
命令。
理解 ICMP
ICMP 协议被用作跨网络发送消息的控制协议。回显请求和回显回复消息只是这种协议的一个例子。这种协议也经常用于通知其他系统的错误。
在这种情况下,数据库服务器正在向博客服务器发送一个 ICMP 数据包,通知它 IP 主机192.168.33.12
无法访问:
proto ICMP (1), length 88)
192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68
数据库服务器不仅说它是不可达的,还告诉博客服务器不可达的原因是因为连接被管理上禁止了。这种回复是防火墙是连接问题的来源的明显迹象,因为通常管理上禁止是防火墙会使用的消息类型。
理解连接被拒绝
当尝试连接到不可用的服务或未被监听的端口时,Linux 内核会发送一个回复。然而,这个回复是一个 TCP 重置,告诉远程系统重置连接。
通过在运行tcpdump
时连接到无效端口,我们可以看到这一点。在博客服务器上,如果我们运行tcpdump
,端口5000
目前没有被使用。使用port
过滤器,我们将看到所有到该端口的流量:
[blog]# tcpdump -vvvnnn -i any port 5000
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
通过在端口 5000 上捕获所有流量,我们现在可以使用 telnet 尝试连接:
[laptop]$ telnet 192.168.33.11 5000
Trying 192.168.33.11...
telnet: connect to address 192.168.33.11: Connection refused
telnet: Unable to connect to remote host
实际上我们已经看到了一些不同的东西。之前,当我们在数据库服务器上对端口3306
执行telnet
时,telnet
命令打印了不同的消息。
telnet: connect to address 192.168.33.12: No route to host
这是因为之前进行 telnet 连接时,服务器收到了 ICMP 目的地不可达数据包。
然而,这次发送了不同的回复。我们可以在tcpdump
捕获的数据包中看到这个回复:
06:57:42.954091 IP (tos 0x10, ttl 64, id 47368, offset 0, flags [DF], proto TCP (6), length 64)
192.168.33.1.53198 > 192.168.33.11.5000: Flags [S], cksum 0xca34 (correct), seq 1134882056, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 511014642 ecr 0,sackOK,eol], length 0
06:57:42.954121 IP (tos 0x10, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40)
192.168.33.11.5000 > 192.168.33.1.53198: Flags [R.], cksum 0xd86e (correct), seq 0, ack 1134882057, win 0, length 0
这次,发送回来的数据包是一个 TCP 重置:
192.168.33.11.5000 > 192.168.33.1.53198: Flags [R.],
重置数据包通常是由于简单的连接错误导致的问题,因为这是客户端尝试连接不再可用的端口时的标准 TCP 响应。
重置数据包也可以由拒绝连接的应用程序发送。然而,ICMP 目的地不可达通常是防火墙拒绝数据包时会收到的回复;也就是说,如果防火墙服务配置为回复的话。
迄今为止你学到的内容的快速总结
到目前为止,我们已经确定博客服务器能够通过端口22
与数据库服务器建立连接。与我们之前的情况不同,这个连接实际上能够执行完整的三次握手。然而,博客服务器无法通过端口3306
与数据库服务器执行三次握手。
当博客服务器尝试通过端口 3306 与数据库服务器建立连接时,数据库服务器会发送一个 ICMP 目的地不可达的数据包回到博客服务器。这个数据包实际上是告诉博客服务器,对数据库的连接尝试被拒绝了。然而,数据库服务是启动的,并且在端口 3306 上监听(通过netstat
验证)。除了端口被监听外,如果我们从数据库服务器本身telnet
到端口 3306,连接是建立的。
考虑到所有这些数据点,可能是数据库服务器启用了防火墙服务并阻止了对端口 3306 的连接。
使用 iptables 管理 Linux 防火墙
在管理 Linux 中的防火墙服务时,有许多选项,最流行的是iptables
和ufw
。对于 Ubuntu 发行版,ufw
是默认的防火墙管理工具;然而,总体而言,iptables
是跨多个 Linux 发行版中最流行的。然而,这两者本身只是Netfilter的用户界面。
Netfilter 是 Linux 内核中的一个框架,允许数据包过滤以及网络和端口转换。诸如iptables
命令之类的工具只是与netfilter
框架交互,以应用这些规则。
在本书中,我们将集中在使用iptables
命令和服务来管理我们的防火墙规则。它不仅是最流行的防火墙工具,而且在基于 Red Hat 的操作系统中已经是默认的防火墙服务很长一段时间了。即使在 Red Hat Enterprise Linux 7 中出现了更新的firewalld
服务,这只是一个管理iptables
的服务。
验证 iptables 是否在运行
由于我们怀疑问题是由系统防火墙配置引起的,我们应该首先检查防火墙是否正在运行以及定义了什么规则。由于iptables
作为一个服务运行,第一步就是简单地检查该服务的状态:
[db]# ps -elf | grep iptables
0 R root 4189 3220 0 80 0 - 28160 - 16:31 pts/0 00:00:00 grep --color=auto iptables
以前,当我们去检查一个服务是否在运行时,我们通常会使用ps
命令。这对于 MariaDB 或 Apache 等服务非常有效;然而,iptables
是不同的。因为iptables
只是一个与netfilter
交互的命令,iptables
服务不像大多数其他服务那样是一个守护进程。事实上,当你启动iptables
服务时,你只是应用了保存的netfilter
规则,当你停止服务时,你只是刷新了这些规则。我们将在本章稍后探讨这个概念,但现在我们只是检查iptables
服务是否在运行:
[db]# service iptables status
Redirecting to /bin/systemctl status iptables.service
iptables.service - IPv4 firewall with iptables
Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled)
Active: active (exited) since Wed 2015-04-01 16:36:16 UTC; 4min 56s ago
Process: 4202 ExecStop=/usr/libexec/iptables/iptables.init stop (code=exited, status=0/SUCCESS)
Process: 4332 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS)
Main PID: 4332 (code=exited, status=0/SUCCESS)
Apr 01 16:36:16 db.example.com systemd[1]: Starting IPv4 firewall with iptables...
Apr 01 16:36:16 db.example.com iptables.init[4332]: iptables: Applying firewall rules: [ OK ]
Apr 01 16:36:16 db.example.com systemd[1]: Started IPv4 firewall with iptables.
随着 Red Hat Enterprise Linux 7 的发布,Red Hat 已经迁移到了systemd
,它取代了标准的init
系统。随着这一迁移,服务命令不再是管理服务的首选命令。这个功能已经将systemd
的控制命令移动到了systemctl
命令。
对于 RHEL 7,至少service
命令仍然可以执行;然而,这个命令只是systemctl
的一个包装器。以下是使用systemctl
命令检查iptables
服务状态的命令。在本书中,我们将使用systemctl
命令而不是传统的 service 命令:
[db]# systemctl status iptables.service
iptables.service - IPv4 firewall with iptables
Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled)
Active: active (exited) since Wed 2015-04-01 16:36:16 UTC; 26min ago
Process: 4202 ExecStop=/usr/libexec/iptables/iptables.init stop (code=exited, status=0/SUCCESS)
Process: 4332 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS)
Main PID: 4332 (code=exited, status=0/SUCCESS)
Apr 01 16:36:16 db.example.com systemd[1]: Starting IPv4 firewall with iptables...
Apr 01 16:36:16 db.example.com iptables.init[4332]: iptables: Applying firewall rules: [ OK ]
Apr 01 16:36:16 db.example.com systemd[1]: Started IPv4 firewall with iptables.
从前面的systemctl
输出中,我们可以看到当前iptables
服务是活动的。我们可以从systemctl
输出的第三行来识别这一点:
Active: active (exited) since Wed 2015-04-01 16:36:16 UTC; 26min ago
当iptables
服务没有运行时,情况看起来会有很大不同:
[db]# systemctl status iptables.service
iptables.service - IPv4 firewall with iptables
Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled)
Active: inactive (dead) since Thu 2015-04-02 02:55:26 UTC; 1s ago
Process: 4489 ExecStop=/usr/libexec/iptables/iptables.init stop (code=exited, status=0/SUCCESS)
Process: 4332 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS)
Main PID: 4332 (code=exited, status=0/SUCCESS)
Apr 01 16:36:16 db.example.com systemd[1]: Starting IPv4 firewall with iptables...
Apr 01 16:36:16 db.example.com iptables.init[4332]: iptables: Applying firewall rules: [ OK ]
Apr 01 16:36:16 db.example.com systemd[1]: Started IPv4 firewall with iptables.
Apr 02 02:55:26 db.example.com systemd[1]: Stopping IPv4 firewall with iptables...
Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Setting chains to policy ACCEPT: nat filter [ OK ]
Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Flushing firewall rules: [ OK ]
Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Unloading modules: [ OK ]
Apr 02 02:55:26 db.example.com systemd[1]: Stopped IPv4 firewall with iptables.
从上面的例子中,systemctl
显示iptables
服务处于非活动状态:
Active: inactive (dead) since Thu 2015-04-02 02:55:26 UTC; 1s ago
systemctl
的一个好处是,在使用状态选项运行时,输出包括来自服务的日志消息:
Apr 02 02:55:26 db.example.com systemd[1]: Stopping IPv4 firewall with iptables...
Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Setting chains to policy ACCEPT: nat filter [ OK ]
Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Flushing firewall rules: [ OK ]
Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Unloading modules: [ OK ]
Apr 02 02:55:26 db.example.com systemd[1]: Stopped IPv4 firewall with iptables.
从上面的代码中,我们可以看到iptables
服务停止过程中使用的所有状态消息。
显示正在执行的 iptables 规则
现在我们知道iptables
服务是活动和运行的,我们还应该查看已定义和正在执行的iptables
规则。为此,我们将使用iptables
命令和-L
(列表)和-n
(数字)标志:
[db]# iptables -L -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited
ACCEPT tcp -- 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
Chain FORWARD (policy ACCEPT)
target prot opt source destination
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
在执行iptables
时,标志-L
和-n
不会合并。与大多数其他命令不同,iptables
有一个特定的格式,需要一些标志与其他标志分开。在这种情况下,-L
标志与其他选项分开。我们可以给-n
添加-v
(详细)选项,但不能添加-L
。以下是使用详细选项执行的示例:
[db]# iptables -L -nv
从iptables -L -n
的输出中,似乎在这台服务器上有相当多的iptables
规则。让我们分解这些规则,以便更好地理解它们。
理解 iptables 规则
在我们进入单个规则之前,我们应该首先了解一下iptables
和防火墙的一些一般规则。
顺序很重要
要知道的第一个重要规则是顺序很重要。如果我们查看iptables -L -n
返回的数据,我们可以看到有多个规则,这些规则的顺序决定了如何解释这些规则。
我喜欢把iptables
想象成一个清单;当接收到一个数据包时,iptables
会从上到下检查清单。当它找到一个符合条件的规则时,就会应用该规则。
这是人们在使用iptables
时经常犯的一个最常见的错误,即在从上到下的顺序之外放置规则。
默认策略
通常情况下,iptables
有两种用法,即除非明确阻止,否则允许所有流量,或者除非明确允许,否则阻止所有流量。这些方法被称为默认允许和默认拒绝策略。
根据 Linux 防火墙的期望用途,可以使用任一策略。然而,通常情况下,默认拒绝策略被认为是更安全的方法,因为这个策略要求为所讨论的服务器的每种访问类型添加一条规则。
分解 iptables 规则
由于iptables
从上到下处理规则,为了更好地理解现有的规则,我们将从下到上查看iptables
规则:
Chain FORWARD (policy ACCEPT)
target prot opt source destination
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited
我们看到的第一条规则是FORWARD
链的所有协议从任何源到任何目的地进行REJECT
。这是否意味着iptables
将阻止所有东西?是的,但只针对正在转发的数据包。
iptables
命令将网络流量类型分类为表和链。表包括正在执行的高级操作,如过滤、网络地址转换或更改数据包。
在每个表中,还有几个“链”。链用于定义要应用规则的流量类型。在FORWARD
链的情况下,这匹配正在转发的流量,通常用于路由。
应用规则的下一个链是INPUT
链:
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited
ACCEPT tcp -- 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
这个链适用于进入本地系统的流量;基本上,这些规则只适用于到达系统的流量:
ACCEPT tcp -- 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
如果我们查看链中的最后一条规则,我们可以看到它明确定义了系统应该接受源 IP 在192.168.0.0/16
网络中,目标 IP 为 0.0.0.0/0 的 TCP 流量,就像netstat
一样是一个通配符。这条规则的最后部分定义了这条规则仅适用于目标端口为3306
的新连接。
简单来说,这条规则将允许 192.168.0.0/16 网络中的任何 IP 访问数据库服务器的任何本地 IP 的 3306 端口。
特别是这条规则应该允许来自我们博客服务器(192.168.33.11)的流量,但是上面的规则呢?
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited
前面的规则明确规定系统应该从源 IP 为0.0.0.0/0
到目标 IP 为0.0.0.0/0
的所有协议进行REJECT
,并回复一个 ICMP 数据包,说明主机被禁止。根据我们之前的网络故障排除,我们知道0.0.0.0/0
网络是所有网络的通配符。
这意味着这条规则将拒绝所有流量到系统,有效地使我们的系统使用“默认拒绝”策略。然而,这并不是定义“默认拒绝”策略的常见方法。
如果我们查看这个链规则集的顶部,我们会看到以下内容:
Chain INPUT (policy ACCEPT)
这本质上是说INPUT
链本身具有ACCEPT
策略,这意味着链本身使用“默认允许”策略。但是,这个链中有一条规则将拒绝所有流量。
这意味着,虽然链的策略在技术上不是默认拒绝,但这条规则实际上实现了相同的效果。除非在此规则之前明确允许流量,否则流量将被拒绝,有效地使链成为“默认拒绝”策略。
在这一点上,我们有一个有趣的问题;INPUT
链中的最后一条规则明确允许从 192.168.0.0/16 源网络到 3306 端口(MariaDB
端口)的流量。然而,上面的规则拒绝了从任何地方到任何地方的所有流量。如果我们花一点时间记住iptables
是基于顺序的,那么我们很容易看出这可能是一个问题。
问题可能只是允许端口 3306 的规则是在阻止所有流量的规则之后定义的;基本上,数据库流量被默认拒绝规则阻止。
然而,在我们根据这些信息采取行动之前,我们应该继续查看iptables
规则,因为可能还有另一条规则定义了对抗这两条底部规则的规则:
ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
INPUT
链中倒数第三条规则确实解释了为什么 SSH 流量按预期工作。该规则明确说明,当连接是针对端口22
的新连接时,系统应该从任何来源到任何目的地接受所有 TCP 协议流量。
这条规则基本上定义了所有新的 TCP 连接到端口22
都是允许的。由于它在默认拒绝规则之前,这意味着在任何情况下都不会被该规则阻止端口22
的新连接。
如果我们查看INPUT
链中倒数第四条规则,我们会看到一条非常有趣的规则:
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
这条规则似乎告诉系统应该从任何 IP(0.0.0.0/0
)接受所有协议到任何 IP(0.0.0.0/0
)。如果我们查看这条规则并应用顺序很重要的逻辑;那么这条规则应该允许我们的数据库流量。
不幸的是,iptables
输出有时可能会误导,因为这条规则没有显示规则的一个关键部分;接口:
[db]# iptables -L -nv
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
394 52363 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
如果我们在iptables
命令中添加-v
(详细)标志,我们可以看到更多信息。特别是,我们可以看到一个名为“in”的新列,它代表接口:
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
如果我们再仔细看一下这条规则,我们会发现接口列显示该规则仅适用于loopback
接口上的流量。由于我们的数据库流量是在enp0s8
接口上的,数据库流量不符合这条规则:
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0
倒数第五条规则非常相似,只是它专门允许从任何 IP 到任何 IP 的所有 ICMP 流量。这解释了为什么我们的ping请求有效,因为这条规则将允许 ICMP 回显请求和回显回复通过防火墙。
然而,倒数第六条规则与其他规则有很大不同:
36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
这条规则确实说明系统应该从任何 IP(0.0.0.0/0)接受所有协议到任何 IP(0.0.0.0/0);但是该规则仅限于RELATED
和ESTABLISHED
数据包。
在审查端口22
的iptables
规则时,我们可以看到该规则仅限于NEW
连接。这基本上意味着用于启动到端口22
的新连接的数据包,如SYN
和SYN-ACK-ACK
是允许的。
当规则说明ESTABLISHED
状态被允许时,iptables
将允许属于已建立的 TCP 连接的数据包:
这意味着新的 SSH 连接是由端口22
的规则允许的。
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
然后,一旦 TCP 连接建立,它将被以下规则允许:
36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
整合规则
现在我们已经查看了所有的iptables
规则,我们可以对为什么我们的数据库流量无法工作做出合理的猜测。
在iptables
规则集中,我们可以看到拒绝所有流量的规则在允许端口3306上的数据库连接之前被定义:
394 52363 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
由于系统无法启动新连接,它们无法建立连接,这将被以下规则允许:
36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
我们可以通过查看定义的规则来确定所有这些,但这也需要对iptables
有相当了解。
还有另一种相对较简单的方法来确定哪些规则正在阻止或允许流量。
查看 iptables 计数器
通过iptables
的详细输出,我们不仅可以看到规则适用的接口,还可以看到两列非常有用。这两列是pkts和bytes:
[db]# iptables -L -nv
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
41 2360 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
pkts
列是iptables
详细输出中的第一列,该列包含规则应用的数据包数。如果我们看前面的规则,我们可以看到这条规则已经应用于41
个数据包。bytes
列是第二列,用于表示规则应用的字节数。对于我们前面的例子,该规则已经应用了 2,360 字节。
我们可以使用iptables
中的数据包和字节计数器来识别应用于我们的数据库流量的规则。为此,我们只需要通过刷新浏览器并运行iptables –L –nv
来触发数据库活动,以识别哪些规则的计数器增加了。我们甚至可以通过使用iptables
命令后跟–Z
(清零)标志来清除当前值,使这更加容易:
[db]# iptables –Z
如果我们重新执行iptables
的详细列表,我们可以看到计数器对于除了ESTABLISHED
和RELATED
规则(每个连接都会匹配的规则,包括我们的 SSH 会话)之外的所有内容都是0
:
[db]# iptables -L -nv
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
7 388 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
清除这些值后,我们现在可以刷新我们的网络浏览器并启动一些数据库流量:
[db]# iptables -L -nv
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
53 3056 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
45 4467 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
如果我们再次以详细模式运行iptables –L
,我们可以看到,事实上,正如我们所怀疑的,数据包被默认的拒绝规则拒绝了。我们可以通过这个事实看到,自从我们使用–Z
标志将计数器清零以来,这条规则已经拒绝了45
个数据包。
使用-Z
标志和计数器是一种非常有用的方法;然而,在某些情况下可能不起作用。在繁忙的系统和规则众多的系统上,仅仅使用计数器来显示匹配的规则可能会很困难。因此,重要的是要建立对iptables
的经验,了解其复杂性。
纠正 iptables 规则排序
更改iptables
可能有点棘手,不是因为它难以使用(尽管命令语法有点复杂),而是因为修改iptables
规则有两个步骤。如果忘记了其中一步(这经常发生),问题可能会意外地持续存在。
iptables 规则的应用方式
当iptables
服务启动时,启动脚本不会像系统上的其他服务那样启动守护进程。iptables
服务所做的就是简单地应用保存规则文件(/etc/sysconfig/iptables
)中定义的规则。
然后这些规则被加载到内存中,它们成为活动规则。这意味着,如果我们只是重新排列内存中的规则,而不修改保存的文件,下次服务器重新启动时,我们的更改将会丢失。
另一方面,如果我们只修改了保存的文件,但没有重新排列内存中的iptables
规则,我们的更改将不会生效,直到下一次重新启动iptables
服务。
我经常看到这两种情况发生,有人简单地忘记了其中一步。这种情况会给他们正在处理的问题带来更多的复杂性。
修改 iptables 规则
对于这种情况,我们将选择一种既执行又易于记忆的简单方法。我们首先编辑/etc/sysconfig/iptables
文件,其中包含所有定义的iptables
规则。然后重新启动iptables
服务,这将导致当前规则被清除,并应用/etc/sysconfig/iptables
文件中的新规则。
要编辑iptables
文件,我们可以简单地使用vi
:
[db]# vi /etc/sysconfig/iptables
# Generated by iptables-save v1.4.21 on Mon Mar 30 02:27:35 2015
*nat
:PREROUTING ACCEPT [10:994]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT
# Completed on Mon Mar 30 02:27:35 2015
# Generated by iptables-save v1.4.21 on Mon Mar 30 02:27:35 2015
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [140:11432]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A INPUT -p tcp -m state --state NEW -m tcp --src 192.168.0.0/16 -- dport 3306 -j ACCEPT
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT
# Completed on Mon Mar 30 02:27:35 2015
这个文件的内容与iptables -L
的输出有些不同。前面的规则实际上只是可以附加到iptables
命令的选项。例如,如果我们想要添加一个允许流量到端口 22 的规则,我们可以简单地复制并粘贴前面的规则,加上-dport 22
,并在前面加上iptables
命令。以下是这个命令的示例:
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
当iptables
服务脚本添加iptables
规则时,它们也只是简单地将这些规则附加到iptables
命令上。
从iptables
文件的内容中,我们可以看到需要重新排序的两个规则:
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A INPUT -p tcp -m state --state NEW -m tcp --src 192.168.0.0/16 -- dport 3306 -j ACCEPT
为了解决我们的问题,我们只需将这两个规则更改为以下内容:
-A INPUT -p tcp -m state --state NEW -m tcp --src 192.168.0.0/16 -- dport 3306 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
更改完成后,我们可以通过按Esc然后在 vi 中输入:wq
来保存并退出文件。
测试我们的更改
现在文件已保存,我们应该可以简单地重新启动iptables
服务,规则将生效。唯一的问题是,如果我们没有正确编辑iptables
文件会怎么样?
我们当前的iptables
配置有一个规则,阻止除了上面允许的连接之外的所有流量。如果我们不小心将该规则放在允许端口 22 之前会怎么样?这意味着当我们重新启动iptables
服务时,我们将无法建立 SSH 连接,而且由于这是我们管理此服务器的唯一方法,这个简单的错误可能会产生严重后果。
在对iptables
进行更改时,应始终谨慎。即使只是重新启动iptables
服务,最好还是查看/etc/sysconfig/iptables
中保存的规则,以确保没有意外的更改会将用户和您自己锁定在管理系统之外。
为了避免这种情况,我们可以使用screen
命令。screen
命令用于打开伪终端,即使我们的 SSH 会话断开,它也会继续运行。即使断开是由于防火墙更改引起的。
要启动screen
,我们只需执行命令screen
:
[db]# screen
一旦我们进入screen
会话,我们将做的不仅仅是重新启动iptables
。我们实际上将编写一个bash
一行命令,重新启动iptables
,将输出打印到屏幕上,以确保我们的会话仍然有效,等待两分钟,然后最终停止iptables
服务:
[db]# systemctl restart iptables; echo "still here?"; sleep 120; systemctl stop iptables
当我们运行此命令时,我们将看到两种情况中的一种,要么我们的 SSH 会话将关闭,这很可能意味着我们的iptables
规则中有错误,要么我们将在屏幕上看到一条消息,上面写着还在这里吗?。
如果我们看到还在这里吗?的消息,这意味着我们的iptables
规则没有锁定我们的 SSH 会话:
[db]# systemctl restart iptables.service; echo "still here?"; sleep 120; systemctl stop iptables.service
still here?
由于命令已完成且我们的 SSH 会话未终止,我们现在可以简单地重新启动iptables
,而不用担心被锁定在外面。
提示
在规则生效后建立新的 SSH 会话总是一个好主意,而不是结束之前的 SSH 会话。这可以验证您是否可以发起新的 SSH 会话,如果不起作用,您仍然可以使用旧的 SSH 会话来解决问题。
当我们这次重新启动iptables
时,我们的新规则将生效:
# systemctl restart iptables.service
# iptables -L -nv
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
15 852 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
现在,我们可以看到接受端口3306
流量的规则在默认拒绝规则之前。如果我们刷新浏览器,我们还可以验证iptables
的更改是否纠正了问题。
看起来是这样的!
如果我们再次查看详细模式下的iptables
列表,我们还可以看到我们的规则匹配得有多好:
# iptables -L -nv
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
119 19352 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
2 120 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306
39 4254 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
从iptables
的统计数据中,我们可以看到有两个数据包匹配了我们的规则。这与网站的正常运行一起,意味着我们对规则进行的微小更改对iptables
允许或拒绝的内容产生了巨大影响。
总结
在本章中,我们遇到了一个看似简单的网络问题,即我们的博客应用程序连接到其数据库。在数据收集阶段,我们使用了诸如netstat
和tcpdump
之类的命令来检查网络数据包,并很快发现博客服务器收到了一个 ICMP 数据包,表明数据库服务器拒绝了博客服务器的 TCP 数据包。
从那时起,我们怀疑问题是防火墙问题,经过使用iptables
命令调查后,我们注意到防火墙规则是无序的。
之后,我们能够使用“试错”阶段来解决问题。这个特定的问题是一个非常常见的问题,我个人在许多不同的环境中都见过。这主要是由于对iptables
的工作原理以及如何正确定义规则的知识不足。虽然本章只涵盖了iptables
中一种类型的配置错误,但本章中使用的一般故障排除方法可以应用于大多数情况。
在第七章中,《文件系统错误和恢复》,我们将开始探讨文件系统错误以及如何从中恢复 - 这是一个棘手的话题,一个错误的命令可能意味着数据丢失,这是任何系统管理员都不想看到的。
第七章:文件系统错误和恢复
在第五章网络故障排除和第六章诊断和纠正防火墙问题中,我们使用了许多工具来排除由于错误配置的路由和防火墙导致的网络连接问题。网络相关问题非常常见,这两个示例问题也是常见的情况。在本章中,我们将专注于与硬件相关的问题,并从排除文件系统错误开始。
就像其他章节一样,我们将从发现的错误开始,排除问题直到找到原因和解决方案。在这个过程中,我们将发现许多用于排除文件系统问题的不同命令和日志。
诊断文件系统错误
与之前的章节不同,那时最终用户向我们报告了问题,这一次我们自己发现了问题。在数据库服务器上执行一些日常任务时,我们尝试创建数据库备份,并收到以下错误:
[db]# mysqldump wordpress > /data/backups/wordpress.sql
-bash: /data/backups/wordpress.sql: Read-only file system
这个错误很有趣,因为它不一定来自mysqldump
命令,而是来自写入/data/backups/wordpress.sql
文件的 bash 重定向。
如果我们看一下错误,它非常具体,我们试图将备份写入的文件系统是只读
的。只读
是什么意思?
只读文件系统
在 Linux 上定义和挂载文件系统时,你有很多选项,但有两个选项最能定义文件系统的可访问性。这两个选项分别是rw
表示读写,ro表示只读。当文件系统以读写选项挂载时,这意味着文件系统的内容可以被读取,并且具有适当权限的用户可以向文件系统写入新文件/目录。
当文件系统以只读模式挂载时,这意味着用户可以读取文件系统,但新的写入请求将被拒绝。
使用mount
命令列出已挂载的文件系统
由于我们收到的错误明确指出文件系统是只读的,我们下一个逻辑步骤是查看服务器上已挂载的文件系统。为此,我们将使用mount
命令:
[db]# mount
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
devtmpfs on /dev type devtmpfs (rw,nosuid,seclabel,size=228500k,nr_inodes=57125,mode=755)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,seclabel)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,seclabel,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,nodev,seclabel,mode=755)
tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,seclabel,mode=755)
selinuxfs on /sys/fs/selinux type selinuxfs (rw,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=33,pgrp=1,timeout=300,minproto=5,maxproto=5,direct)
mqueue on /dev/mqueue type mqueue (rw,relatime,seclabel)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,seclabel)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
sunrpc on /var/lib/nfs/rpc_pipefs type rpc_pipefs (rw,relatime)
nfsd on /proc/fs/nfsd type nfsd (rw,relatime)
/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
192.168.33.13:/nfs on /data type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13)
mount
命令在处理文件系统时非常有用。它不仅可以用于显示已挂载的文件系统(如前面的命令所示),还可以用于附加(或挂载)和卸载文件系统。
已挂载的文件系统
称文件系统为已挂载的文件系统是一种常见的说法,表示文件系统已连接到服务器。对于文件系统,它们通常有两种状态,要么是已连接(已挂载),内容对用户可访问,要么是未连接(未挂载),对用户不可访问。在本章的后面,我们将使用mount
命令来介绍挂载和卸载文件系统。
mount
命令不是查看已挂载或未挂载文件系统的唯一方法。另一种方法是简单地读取/proc/mounts
文件:
[db]# cat /proc/mounts
rootfs / rootfs rw 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0
devtmpfs /dev devtmpfs rw,seclabel,nosuid,size=228500k,nr_inodes=57125,mode=755 0 0
securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev 0 0
devpts /dev/pts devpts rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0
tmpfs /sys/fs/cgroup tmpfs rw,seclabel,nosuid,nodev,noexec,mode=755 0 0
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=33,pgrp=1,timeout=300,minproto=5,maxproto=5,direct 0 0
mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0
hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,relatime 0 0
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
sunrpc /var/lib/nfs/rpc_pipefs rpc_pipefs rw,relatime 0 0
nfsd /proc/fs/nfsd nfsd rw,relatime 0 0
/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0
192.168.33.13:/nfs /data nfs4 rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13 0 0
实际上,/proc/mounts
文件的内容与mount
命令的输出非常接近,主要区别在于每行末尾的两个数字列。为了更好地理解这个文件和mount
命令的输出,让我们更仔细地看一下/proc/mounts
中/boot
文件系统的条目:
/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0
/proc/mounts
文件有六列数据——设备、挂载点、文件系统类型、选项,以及两个未使用的列,用于向后兼容。为了更好地理解这些值,让我们更好地理解这些列。
第一列设备指定了用于文件系统的设备。在前面的例子中,/boot 文件系统所在的设备是/dev/sda1。
从设备的名称(sda1)可以识别出一个关键信息。这个设备是另一个设备的分区,我们可以通过设备名称末尾的数字来识别。
这个设备,从名称上看似乎是一个物理驱动器(假设是硬盘),名为/dev/sda;这个驱动器至少有一个分区,其设备名称为/dev/sda1。每当一个驱动器上有分区时,分区会被创建为自己的设备,每个设备都被分配一个编号;在这种情况下是 1,这意味着它是第一个分区。
使用 fdisk 列出可用分区
我们可以通过使用 fdisk 命令来验证这一点:
[db]# fdisk -l /dev/sda
Disk /dev/sda: 42.9 GB, 42949672960 bytes, 83886080 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x0009c844
Device Boot Start End Blocks Id System
/dev/sda1 * 2048 1026047 512000 83 Linux
/dev/sda2 1026048 83886079 41430016 8e Linux LVM
fdisk 命令可能很熟悉,因为它是一个用于创建磁盘分区的跨平台命令。但它也可以用来列出分区。
在前面的命令中,我们使用了-l(列出)标志来列出分区,然后是我们想要查看的设备/dev/sda。然而,fdisk 命令显示的不仅仅是这个驱动器上可用的分区。它还显示了磁盘的大小:
Disk /dev/sda: 42.9 GB, 42949672960 bytes, 83886080 sectors
我们可以从 fdisk 命令打印的第一行中看到这一点,根据这一行,我们的设备/dev/sda 的大小为 42.9GB。如果我们看输出的底部,还可以看到在这个磁盘上创建的分区:
Device Boot Start End Blocks Id System
/dev/sda1 * 2048 1026047 512000 83 Linux
/dev/sda2 1026048 83886079 41430016 8e Linux LVM
从前面的列表中,看起来/dev/sda 有两个分区,/dev/sda1 和/dev/sda2。使用 fdisk,我们已经能够识别出关于这个文件系统物理设备的许多细节。如果我们继续查看/proc/mounts 的详细信息,我们应该能够识别出一些其他非常有用的信息,如下所示:
/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0
前一行中的第二列挂载点标注了这个文件系统挂载到的路径。在这种情况下,路径是/boot;/boot 本身只是根文件系统上的一个目录。然而,一旦存在于设备/dev/sda1 上的文件系统被挂载,/boot 现在就是它自己的文件系统。
为了更好地理解这个概念,我们将使用 mount 和 umount 命令来挂载和卸载/boot 文件系统:
[db]# ls /boot/
config-3.10.0-123.el7.x86_64
grub
grub2
initramfs-0-rescue-dee83c8c69394b688b9c2a55de9e29e4.img
initramfs-3.10.0-123.el7.x86_64.img
initramfs-3.10.0-123.el7.x86_64kdump.img
initrd-plymouth.img
symvers-3.10.0-123.el7.x86_64.gz
System.map-3.10.0-123.el7.x86_64
vmlinuz-0-rescue-dee83c8c69394b688b9c2a55de9e29e4
vmlinuz-3.10.0-123.el7.x86_64
如果我们在/boot 路径上执行一个简单的 ls 命令,我们可以看到这个目录中有很多文件。从/proc/mounts 文件和 mount 命令中,我们知道/boot 上有一个文件系统挂载:
[db]# mount | grep /boot
/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
为了卸载这个文件系统,我们可以使用 umount 命令:
[db]# umount /boot
[db]# mount | grep /boot
umount 命令的任务非常简单,它卸载已挂载的文件系统。
提示
前面的命令是卸载文件系统可能是危险的示例。一般来说,您应该首先验证文件系统在卸载之前是否正在被访问。
/boot 文件系统现在已经卸载,当我们执行 ls 命令时会发生什么?
# ls /boot
/boot 路径仍然有效。但现在它只是一个空目录。这是因为/dev/sda1 上的文件系统没有挂载;因此,该文件系统上存在的任何文件目前在这个系统上都无法访问。
如果我们使用 mount 命令重新挂载文件系统,我们将看到文件重新出现:
[db]# mount /boot
[db]# ls /boot
config-3.10.0-123.el7.x86_64
grub
grub2
initramfs-0-rescue-dee83c8c69394b688b9c2a55de9e29e4.img
initramfs-3.10.0-123.el7.x86_64.img
initramfs-3.10.0-123.el7.x86_64kdump.img
initrd-plymouth.img
symvers-3.10.0-123.el7.x86_64.gz
System.map-3.10.0-123.el7.x86_64
vmlinuz-0-rescue-dee83c8c69394b688b9c2a55de9e29e4
vmlinuz-3.10.0-123.el7.x86_64
正如我们所看到的,当 mount 命令给出路径参数时,该命令将尝试挂载该文件系统。然而,当没有给出参数时,mount 命令将简单地显示当前挂载的文件系统。
在本章的后面,我们将探讨使用 mount 以及它如何理解文件系统应该在何处以及如何挂载;现在,让我们来看一下/proc/mounts 输出中的下一列:
/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0
第三列文件系统类型表示正在使用的文件系统类型。在许多操作系统中,特别是 Linux,通常可以使用多种类型的文件系统。在上面的情况下,我们的引导文件系统设置为xfs
,这是 Red Hat Enterprise Linux 7 的新默认文件系统。
在使用xfs
之前,旧版本的 Red Hat 默认使用ext3
或ext4
文件系统。Red Hat 仍然支持ext3/4
文件系统和其他文件系统,因此/proc/mounts
文件中可能列出了许多不同的文件系统类型。
对于/boot
文件系统,了解文件系统类型并不立即有用;然而,在我们深入研究这个问题时,可能需要知道如何查找底层文件系统的类型:
/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0
第四列选项显示了文件系统挂载的选项。
当文件系统被挂载时,可以为该文件系统指定特定选项,以改变文件系统的默认行为。在上面的例子中,提供了相当多的选项;让我们分解这个列表,以更好地理解指定了什么:
-
**inode64**
:这使文件系统能够创建大于 32 位长度的索引节点号 -
noquota:这禁用了对该文件系统的磁盘配额和强制执行**
从描述中可以看出,这些选项可以极大地改变文件系统的行为。在排除任何文件系统问题时,查看这些选项也非常重要:
**/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0**
/proc/mounts
输出的最后两列,表示为0 0
,实际上在/proc/mounts
中没有使用。这些列实际上只是为了与/etc/mtab
向后兼容而添加的,/etc/mtab
是一个类似的文件,但不像/proc/mounts
那样被认为是最新的。
这两个文件之间的区别在于它们的用途。/etc/mtab
文件是为用户或应用程序设计的,用于读取和利用,而/proc/mounts
文件是由内核本身使用的。因此,/proc/mounts
文件被认为是最权威的版本。
回到故障排除
如果我们回到手头的问题,我们在向/data/backups
目录写入备份时收到了错误。使用mount
命令,我们可以确定该目录存在于哪个文件系统上:
**# mount | grep "data"**
**192.168.33.13:/nfs on /data type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13)**
现在我们更好地理解了mount
命令的格式,我们可以从上面的命令行中识别出一些关键信息。我们可以看到,此文件系统的设备设置为(192.168.33.13:/nfs
),mount
点(要附加的路径)设置为(/data
),文件系统类型为(nfs4
),并且文件系统设置了相当多的选项。
**# NFS - 网络文件系统
查看/data
文件系统,我们可以看到文件系统类型设置为nfs4
。这种文件系统类型意味着文件系统是一个网络文件系统(NFS)。
NFS 是一种允许服务器与其他远程服务器共享导出目录的服务。nfs4
文件系统类型是一种特殊的文件系统,允许远程服务器访问此服务,就像它是一个标准文件系统一样。
文件系统类型中的4
表示要使用的版本,这意味着远程服务器要使用 NFS 协议的第 4 版。
提示
目前,NFS 最流行的版本是版本 3 和版本 4,版本 4 是 Red Hat Enterprise Linux 6 和 7 的默认版本。版本 3 和版本 4 之间有相当多的区别;然而,这些区别都不足以影响我们的故障排除方法。如果您在使用 NFS 版本 3 时遇到问题,那么您很可能可以按照我们将在本章中遵循的相同类型的步骤进行操作。
现在我们已经确定了文件系统是 NFS 文件系统,让我们看看它挂载的选项:
192.168.33.13:/nfs on /data type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13)
从我们收到的错误来看,文件系统似乎是“只读”的,但如果我们查看列出的选项,第一个选项是rw
。这意味着 NFS 文件系统本身已被挂载为“读写”,这应该允许对此文件系统进行写操作。
为了测试问题是与路径/data/backups
还是挂载的文件系统/data
有关,我们可以使用touch
命令来测试在此文件系统中创建文件:
# touch /data/file.txt
touch: cannot touch '/data/file.txt': Read-only file system
甚至touch
命令也无法在此文件系统上创建新文件。这清楚地表明文件系统存在问题;唯一的问题是是什么导致了这个问题。
如果我们查看此文件系统挂载的选项,没有任何导致文件系统为“只读”的原因;这意味着问题很可能不在于文件系统的挂载方式,而是其他地方。
由于问题似乎与 NFS 文件系统的挂载方式无关,而且这个文件系统是基于网络的,下一个有效的步骤将是验证与 NFS 服务器的网络连接。
NFS 和网络连接
就像网络故障排除一样,我们的第一个测试将是 ping NFS 服务器,看看是否有响应;但问题是:我们应该 ping 哪个服务器?
答案在文件系统挂载的设备名称中(192.168.33.13:/nfs
)。挂载 NFS 文件系统时,设备的格式为<nfs 服务器>:<共享目录>
。在我们的示例中,这意味着我们的/data
文件系统正在从服务器192.168.33.13
挂载/nfs
目录。为了测试连接性,我们可以简单地ping
IP 192.168.33.13
:
[db]# ping 192.168.33.13
PING 192.168.33.13 (192.168.33.13) 56(84) bytes of data.
64 bytes from 192.168.33.13: icmp_seq=1 ttl=64 time=0.495 ms
64 bytes from 192.168.33.13: icmp_seq=2 ttl=64 time=0.372 ms
64 bytes from 192.168.33.13: icmp_seq=3 ttl=64 time=0.364 ms
64 bytes from 192.168.33.13: icmp_seq=4 ttl=64 time=0.337 ms
^C
--- 192.168.33.13 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3001ms
rtt min/avg/max/mdev = 0.337/0.392/0.495/0.060 ms
从ping
结果来看,NFS 服务器似乎是正常的;但 NFS 服务呢?我们可以通过使用curl
命令telnet
到 NFS 端口来验证与 NFS 服务的连接。但首先,我们需要确定应连接到哪个端口。
在早期章节中排除数据库连接问题时,我们主要使用了众所周知的端口;由于 NFS 使用了几个不太常见的端口,我们需要确定要连接的端口:
这样做的最简单方法是在/etc/services
文件中搜索端口:
[db]# grep nfs /etc/services
nfs 2049/tcp nfsd shilp # Network File System
nfs 2049/udp nfsd shilp # Network File System
nfs 2049/sctp nfsd shilp # Network File System
netconfsoaphttp 832/tcp # NETCONF for SOAP over HTTPS
netconfsoaphttp 832/udp # NETCONF for SOAP over HTTPS
netconfsoapbeep 833/tcp # NETCONF for SOAP over BEEP
netconfsoapbeep 833/udp # NETCONF for SOAP over BEEP
nfsd-keepalive 1110/udp # Client status info
picknfs 1598/tcp # picknfs
picknfs 1598/udp # picknfs
shiva_confsrvr 1651/tcp shiva-confsrvr # shiva_confsrvr
shiva_confsrvr 1651/udp shiva-confsrvr # shiva_confsrvr
3d-nfsd 2323/tcp # 3d-nfsd
3d-nfsd 2323/udp # 3d-nfsd
mediacntrlnfsd 2363/tcp # Media Central NFSD
mediacntrlnfsd 2363/udp # Media Central NFSD
winfs 5009/tcp # Microsoft Windows Filesystem
winfs 5009/udp # Microsoft Windows Filesystem
enfs 5233/tcp # Etinnae Network File Service
nfsrdma 20049/tcp # Network File System (NFS) over RDMA
nfsrdma 20049/udp # Network File System (NFS) over RDMA
nfsrdma 20049/sctp # Network File System (NFS) over RDMA
/etc/services
文件是许多 Linux 发行版中包含的静态文件。它用作查找表,将网络端口映射到简单易读的名称。从前面的输出中,我们可以看到nfs
名称映射到 TCP 端口2049
;这是 NFS 服务的默认端口。我们可以利用这个端口来测试连接性,如下所示:
[db]# curl -vk telnet://192.168.33.13:2049
* About to connect() to 192.168.33.13 port 2049 (#0)
* Trying 192.168.33.13...
* Connected to 192.168.33.13 (192.168.33.13) port 2049 (#0)
我们的telnet
似乎成功了;我们可以进一步验证它,使用netstat
命令:
[db]# netstat -na | grep 192.168.33.13
tcp 0 0 192.168.33.12:756 192.168.33.13:2049 ESTABLISHED
看起来连接性不是问题,如果我们的问题与连接性无关,也许是 NFS 共享的配置有问题。
我们实际上可以使用一个命令验证 NFS 共享的设置和网络连接性——showmount
。
使用showmount
命令
showmount
命令可用于显示通过-e
(显示导出)标志导出的目录。此命令通过查询指定主机上的 NFS 服务来工作。
对于我们的问题,我们将查询192.168.33.13
上的 NFS 服务:
[db]# showmount -e 192.168.33.13
Export list for 192.168.33.13:
/nfs 192.168.33.0/24
showmount
命令的格式使用两列。第一列是共享的目录。第二个是共享该目录的网络或主机名。
在前面的示例中,我们可以看到从此主机共享的目录是/nfs
目录。这与设备名称192.168.33.13:/nfs
中列出的目录相匹配。
/nfs
目录正在共享的网络是192.166.33.0/24
网络,正如我们在网络章节中学到的那样,它是192.168.33.0
到192.168.33.255
的缩写。我们已经知道从以前的故障排除中,我们所在的数据库服务器位于该网络中。
我们还可以看到自从之前执行netstat
命令以来,这并没有改变:
[db]# netstat -na | grep 192.168.33.13
tcp 0 0 192.168.33.12:756 192.168.33.13:2049 ESTABLISHED
netstat
命令的第四列显示了在已建立
的 TCP 连接中使用的本地 IP 地址。根据前面的输出,我们可以看到192.168.33.12
地址是我们的数据库服务器的 IP(在前几章中已经看到)。
到目前为止,关于这个 NFS 共享的一切看起来都是正确的,从这里开始,我们需要登录到 NFS 服务器继续故障排除。
NFS 服务器配置
一旦登录到 NFS 服务器,我们应该首先检查 NFS 服务是否正在运行:
[db]# systemctl status nfs
nfs-server.service - NFS server and services
Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled)
Active: active (exited) since Sat 2015-04-25 14:01:13 MST; 17h ago
Process: 2226 ExecStart=/usr/sbin/rpc.nfsd $RPCNFSDARGS (code=exited, status=0/SUCCESS)
Process: 2225 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS)
Main PID: 2226 (code=exited, status=0/SUCCESS)
CGroup: /system.slice/nfs-server.service
使用systemctl
,我们可以简单地查看服务状态;从前面的输出来看,这是正常的。这是可以预料的,因为我们能够telnet
到 NFS 服务并使用showmount
命令来查询它。
探索/etc/exports
由于 NFS 服务正在运行且正常,下一步是检查定义了哪些目录被导出以及它们如何被导出的配置;/etc/exports
文件:
[nfs]# ls -la /etc/exports
-rw-r--r--. 1 root root 40 Apr 26 08:28 /etc/exports
[nfs]# cat /etc/exports
/nfs 192.168.33.0/24(rw,no_root_squash)
这个文件的格式实际上与showmount
命令的输出类似。
第一列是要共享的目录,第二列是要与之共享的网络。然而,在这个文件中,在网络定义之后还有额外的信息。
网络/子网列后面跟着一组括号,里面包含各种NFS
选项。这些选项与我们在/proc/mounts
文件中看到的挂载选项非常相似。
这些选项可能是我们只读
文件系统的根本原因吗?很可能。让我们分解这两个选项以更好地理解:
-
rw
:这允许在共享目录上进行读取和写入 -
no_root_squash
:这禁用了root_squash
;root_squash
是一个将 root 用户映射到匿名用户的系统
不幸的是,这两个选项都不能强制文件系统处于只读
模式。事实上,根据这些选项的描述,它们似乎表明这个 NFS 共享应该处于读写
模式。
在对/etc/exports
文件执行ls
时,出现了一个有趣的事实:
[nfs]# ls -la /etc/exports
-rw-r--r--. 1 root root 40 Apr 26 08:28 /etc/exports
/etc/exports
文件最近已经被修改。我们的共享文件系统实际上是以只读
方式共享的,但是最近有人改变了/etc/exports
文件,将文件系统导出为读写
方式。
这种情况是完全可能的,实际上,这是 NFS 的一个常见问题。NFS 服务并不会不断地读取/etc/exports
文件以寻找更改。事实上,只有在服务启动时才会读取这个文件。
对/etc/exports
文件的任何更改都不会生效,直到重新加载服务或使用exportfs
命令刷新导出的文件系统为止。
识别当前的导出
一个非常常见的情况是,有人对这个文件进行了更改,然后忘记运行命令来刷新导出的文件系统。我们可以使用exportfs
命令来确定是否是这种情况:
[nfs]# exportfs -s
/nfs 192.168.33.0/24(rw,wdelay,no_root_squash,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)
当给出-s
(显示当前导出)标志时,exportfs
命令将简单地列出现有的共享目录,包括目录共享的选项。
从前面的输出可以看出,这个文件系统与许多未在/etc/exports
中列出的选项共享。这是因为通过 NFS 共享的所有目录都有一个默认的选项列表,用于管理目录的共享方式。在/etc/exports
中指定的选项实际上是用来覆盖默认设置的。
为了更好地理解这些选项,让我们分解它们:
-
rw
:这允许在共享目录上进行读取和写入。 -
wdelay
:这会导致 NFS 在怀疑另一个客户端正在进行写入时暂停写入请求。这旨在减少多个客户端连接时的写入冲突。 -
no_root_squash
:这禁用了root_squash
,它是一个将 root 用户映射到匿名用户的系统。 -
no_subtree_check
:这禁用了subtree
检查;子树检查实质上确保对导出子目录的目录的请求将遵守子目录更严格的策略。 -
sec=sys
:这告诉 NFS 使用用户 ID 和组 ID 值来控制文件访问的权限和授权。 -
secure
:这确保 NFS 只接受客户端端口低于 1024 的请求,实质上要求它来自特权 NFS 挂载。 -
no_all_squash
:这禁用了all_squash
,用于强制将所有权限映射到匿名用户和组。
似乎这些选项也没有解释“只读”文件系统。这似乎是一个非常棘手的故障排除问题,特别是当 NFS 服务似乎配置正确时。
从另一个客户端测试 NFS
由于 NFS 服务器的配置似乎正确,客户端(数据库服务器)也似乎正确,我们需要缩小问题是在客户端还是服务器端。
我们可以通过在另一个客户端上挂载文件系统并尝试相同的写入请求来做到这一点。根据配置,似乎我们只需要另一个服务器在192.168.33.0/24
网络中执行此测试。也许我们之前章节中的博客服务器是一个好的客户端选择?
提示
在某些环境中,对这个问题的答案可能是否定的,因为 Web 服务器通常被认为比数据库服务器不太安全。但是,由于这只是本书的一个测试环境,所以可以接受。
一旦我们登录到博客服务器,我们可以测试是否可以使用showmount
命令看到挂载:
[blog]# showmount -e 192.168.33.13
Export list for 192.168.33.13:
/nfs 192.168.33.0/24
这回答了两个问题。第一个是 NFS 客户端软件是否安装;由于showmount
命令存在,答案很可能是“是”。
第二个问题是 NFS 服务是否可以从博客服务器访问,这也似乎是肯定的。
为了测试挂载,我们将简单地使用mount
命令:
[blog]# mount -t nfs 192.168.33.13:/nfs /mnt
要使用mount
命令挂载文件系统,语法是:mount -t <文件系统类型> <设备> <挂载点>
。在上面的示例中,我们只是将192.168.33.13:/nfs
设备挂载到了/mnt
目录,文件系统类型为nfs
。
在运行命令时,我们没有收到任何错误,但为了确保文件系统被正确挂载,我们可以使用mount
命令,就像我们之前做的那样:
[blog]# mount | grep /mnt
192.168.33.13:/nfs on /mnt type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.11,local_lock=none,addr=192.168.33.13)
从mount
命令的输出中,似乎mount
请求成功,并且处于“读写”模式,这意味着mount
选项类似于数据库服务器上使用的选项。
现在我们可以尝试使用touch
命令在文件系统中创建文件来测试文件系统:
# touch /mnt/testfile.txt
touch: cannot touch '/mnt/testfile.txt': Read-only file system
看起来问题不在客户端的配置上,因为即使我们的新客户端也无法写入这个文件系统。
提示
作为提示,在前面的示例中,我将/nfs
共享挂载到了/mnt
。/mnt
目录被用作通用挂载点,通常被认为是可以使用的。但是,最好的做法是在挂载到/mnt
之前确保没有其他东西挂载到/mnt
。
使挂载永久化
当前,即使我们使用mount
命令挂载了 NFS 共享,这个挂载的文件系统并不被认为是持久的。下次系统重新启动时,NFS 挂载将不会重新挂载。
这是因为在系统启动时,启动过程的一部分是读取/etc/fstab
文件并mount
其中定义的任何文件系统。
为了更好地理解这是如何工作的,让我们看一下数据库服务器上的/etc/fstab
文件:
[db]# cat /etc/fstab
#
# /etc/fstab
# Created by anaconda on Mon Jul 21 23:35:56 2014
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
/dev/mapper/os-root / xfs defaults 1 1
UUID=be76ec1d-686d-44a0-9411-b36931ee239b /boot xfs defaults 1 2
/dev/mapper/os-swap swap swap defaults 0 0
192.168.33.13:/nfs /data nfs defaults 0 0
/etc/fstab
文件的内容实际上与/proc/mounts
文件的内容非常相似。/etc/fstab
文件中的第一列用于指定要挂载的设备,第二列是要挂载到的路径或挂载点,第三列只是文件系统类型,第四列是mount
文件系统的选项。
然而,这些文件在/etc/fstab
文件中的最后两列是不同的。这最后两列实际上是有意义的。在fstab
文件中,第五列由dump
命令使用。
dump
命令是一个简单的备份实用程序,它读取/etc/fstab
以确定要备份的文件系统。当执行 dump 实用程序时,任何值设置为0
的文件系统都不会被备份。
尽管这个实用程序在今天并不经常使用,但/etc/fstab
文件中的这一列是为了向后兼容而保留的。
/etc/fstab
文件中的第六列对今天的系统非常重要。此列用于表示在引导过程中执行文件系统检查或fsck
的顺序(通常在故障后)。
文件系统检查或fsck
是一个定期运行的过程,检查文件系统中的错误并尝试纠正它们。这是我们将在本章稍后介绍的一个过程。
卸载/mnt 文件系统
由于我们不希望 NFS 共享的文件系统保持挂载在博客服务器的/mnt
路径上,我们需要卸载文件系统。
我们可以像之前对/boot
文件系统所做的那样,使用umount
命令来执行此操作:
[blog]# umount /mnt
[blog]# mount | grep /mnt
从博客服务器上,我们只需使用umount
,然后是客户端的/mnt
挂载点来卸载
NFS挂载
。现在我们已经这样做了,我们可以回到 NFS 服务器继续排除故障。
再次排除 NFS 服务器故障
由于我们确定即使新客户端也无法写入/nfs
共享,我们现在已经缩小了问题很可能是在服务器端而不是客户端。
早些时候,在排除 NFS 服务器故障时,我们几乎检查了关于 NFS 的所有内容。我们验证了服务实际上正在运行,可以被客户端访问,/etc/exports
中的数据是正确的,并且当前导出的目录与/etc/exports
中的内容匹配。此时,只剩下一个地方需要检查:日志
文件。
默认情况下,NFS 服务没有像 Apache 或 MariaDB 那样拥有自己的日志文件。相反,RHEL 系统上的此服务利用syslog
设施;这意味着我们的日志将在/var/log/messages
中。
messages
日志是基于 Red Hat Enterprise Linux 的 Linux 发行版中非常常用的日志文件。实际上,默认情况下,除了 cron 作业和身份验证之外,RHEL 系统上的每条高于 info 日志级别的 syslog 消息都会发送到/var/log/messages
。
由于 NFS 服务将其日志消息发送到本地syslog
服务,因此其消息也包含在messages
日志中。
查找 NFS 日志消息
如果我们不知道 NFS 日志被发送到/var/log/messages
日志文件中怎么办?有一个非常简单的技巧来确定哪个日志文件包含 NFS 日志消息。
通常,在 Linux 系统上,所有系统服务的日志文件都位于/var/log
中。由于我们知道系统上大多数日志的默认位置,我们可以简单地浏览这些文件,以确定哪些文件可能包含 NFS 日志消息:
[nfs]# cd /var/log
[nfs]# grep -rc nfs ./*
./anaconda/anaconda.log:14
./anaconda/syslog:44
./anaconda/anaconda.xlog:0
./anaconda/anaconda.program.log:7
./anaconda/anaconda.packaging.log:16
./anaconda/anaconda.storage.log:56
./anaconda/anaconda.ifcfg.log:0
./anaconda/ks-script-Sr69bV.log:0
./anaconda/ks-script-lfU6U2.log:0
./audit/audit.log:60
./boot.log:4
./btmp:0
./cron:470
./cron-20150420:662
./dmesg:26
./dmesg.old:26
./grubby:0
./lastlog:0
./maillog:112386
./maillog-20150420:17
./messages:3253
./messages-20150420:11804
./sa/sa15:1
./sa/sar15:1
./sa/sa16:1
./sa/sar16:1
./sa/sa17:1
./sa/sa19:1
./sa/sar19:1
./sa/sa20:1
./sa/sa25:1
./sa/sa26:1
./secure:14
./secure-20150420:63
./spooler:0
./tallylog:0
./tuned/tuned.log:0
./wtmp:0
./yum.log:0
grep
命令递归(-r
)搜索每个文件中的字符串"nfs
",并输出包含字符串的行数的文件名及计数(-c
)。
在前面的输出中,有两个日志文件包含了最多数量的字符串"nfs
"。第一个是maillog
,这是用于电子邮件消息的系统日志;这不太可能与 NFS 服务相关。
第二个是messages
日志文件,正如我们所知,这是系统默认的日志文件。
即使没有关于特定系统日志记录方法的先验知识,如果您对 Linux 有一般了解,并且熟悉前面的示例中的技巧,通常可以找到包含所需数据的日志。
既然我们知道要查找的日志文件,让我们浏览一下/var/log/messages
日志。
阅读/var/log/messages
由于这个log
文件可能相当大,我们将使用tail
命令和-100
标志,这会导致tail
只显示指定文件的最后100
行。通过将输出限制为100
行,我们应该只看到最相关的数据:
[nfs]# tail -100 /var/log/messages
Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device.
md/raid1:md127: Operation continuing on 1 devices.
Apr 26 10:25:55 nfs kernel: md: unbind<sdb1>
Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1)
Apr 26 10:27:20 nfs kernel: md: bind<sdb1>
Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127
Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk.
Apr 26 10:27:20 nfs kernel: md: using maximum available idle IO bandwidth (but not more than 200000 KB/sec) for recovery.
Apr 26 10:27:20 nfs kernel: md: using 128k window, over a total of 511936k.
Apr 26 10:27:20 nfs kernel: md: md127: recovery done.
Apr 26 10:27:41 nfs nfsdcltrack[4373]: sqlite_remove_client: unexpected return code from delete: 8
Apr 26 10:27:59 nfs nfsdcltrack[4375]: sqlite_remove_client: unexpected return code from delete: 8
Apr 26 10:55:06 nfs dhclient[3528]: can't create /var/lib/NetworkManager/dhclient-05be239d-0ec7-4f2e-a68d-b64eec03fcb2-enp0s3.lease: Read-only file system
Apr 26 11:03:43 nfs chronyd[744]: Could not open temporary driftfile /var/lib/chrony/drift.tmp for writing
Apr 26 11:55:03 nfs rpc.mountd[4552]: could not open /var/lib/nfs/.xtab.lock for locking: errno 30 (Read-only file system)
Apr 26 11:55:03 nfs rpc.mountd[4552]: can't lock /var/lib/nfs/xtab for writing
即使100
行也可能相当繁琐,我已将输出截断为只包含相关行。这显示了相当多带有字符串"nfs
"的消息;然而,并非所有这些消息都来自 NFS 服务。由于我们的 NFS 服务器主机名设置为nfs
,因此来自该系统的每个日志条目都包含字符串"nfs
"。
然而,即使如此,我们仍然看到了一些与NFS
服务相关的消息,特别是以下行:
Apr 26 10:27:41 nfs nfsdcltrack[4373]: sqlite_remove_client: unexpected return code from delete: 8
Apr 26 10:27:59 nfs nfsdcltrack[4375]: sqlite_remove_client: unexpected return code from delete: 8
Apr 26 11:55:03 nfs rpc.mountd[4552]: could not open /var/lib/nfs/.xtab.lock for locking: errno 30 (Read-only file system)
Apr 26 11:55:03 nfs rpc.mountd[4552]: can't lock /var/lib/nfs/xtab for writing
这些日志条目的有趣之处在于其中一个明确指出服务rpc.mountd
由于文件系统为只读
而无法打开文件。然而,它试图打开的文件/var/lib/nfs/.xtab.lock
并不是我们 NFS 共享的一部分。
由于这个文件系统不是我们 NFS 的一部分,让我们快速查看一下这台服务器上挂载的文件系统。我们可以再次使用mount
命令来做到这一点:
[nfs]# mount
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
devtmpfs on /dev type devtmpfs (rw,nosuid,seclabel,size=241112k,nr_inodes=60278,mode=755)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
selinuxfs on /sys/fs/selinux type selinuxfs (rw,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=33,pgrp=1,timeout=300,minproto=5,maxproto=5,direct)
mqueue on /dev/mqueue type mqueue (rw,relatime,seclabel)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,seclabel)
sunrpc on /var/lib/nfs/rpc_pipefs type rpc_pipefs (rw,relatime)
nfsd on /proc/fs/nfsd type nfsd (rw,relatime)
/dev/mapper/md0-root on / type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
/dev/md127 on /boot type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
/dev/mapper/md0-nfs on /nfs type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
与另一台服务器一样,有相当多的挂载文件系统,但我们并不对所有这些感兴趣;只对其中的一小部分感兴趣。
/dev/mapper/md0-root on / type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
/dev/md127 on /boot type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
/dev/mapper/md0-nfs on /nfs type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
前面的三行是我们应该感兴趣的行。这三个挂载的文件系统是我们系统定义的持久文件系统。如果我们查看这三个持久文件系统,我们可以找到一些有趣的信息。
/
或根文件系统存在于设备/dev/mapper/md0-root
上。这个文件系统对我们的系统非常重要,因为看起来这台服务器配置为在根文件系统(/
)下安装整个操作系统,这是一种相当常见的设置。这个文件系统包括了问题文件/var/lib/nfs/.xtab.lock
。
/boot
文件系统存在于设备/dev/md127
上,根据名称判断,这很可能是使用 Linux 软件 RAID 系统的阵列设备。/boot
文件系统和根文件系统一样重要,因为/boot
包含了服务器启动所需的所有文件。没有/boot
文件系统,这个系统很可能无法重新启动,并且在下一次系统重启时会发生内核崩溃。
最后一个文件系统/nfs
使用了/dev/mapper/md0-nfs
设备。根据我们之前的故障排除,我们确定了这个文件系统是通过 NFS 服务导出的文件系统。
只读文件系统
如果我们回顾错误和mount
的输出,我们将开始在这个系统上识别一些有趣的错误:
Apr 26 11:55:03 nfs rpc.mountd[4552]: could not open /var/lib/nfs/.xtab.lock for locking: errno 30 (Read-only file system)
错误报告称,.xtab.lock
文件所在的文件系统是只读
的:
/dev/mapper/md0-root on / type xfs (ro,relatime,seclabel,attr2,inode64,noquota)
从mount
命令中,我们可以看到问题的文件系统是/
文件系统。在查看/
或根文件系统的选项后,我们可以看到这个文件系统实际上是使用ro
选项挂载的。
实际上,如果我们查看这三个文件系统的选项,我们会发现/
、/boot
和/nfs
都是使用ro
选项挂载的。rw
挂载文件系统为读写
,ro
选项挂载文件系统为只读
。这意味着目前这些文件系统不能被任何用户写入。
所有三个定义的文件系统都以只读
模式挂载是相当不寻常的配置。为了确定这是否是期望的配置,我们可以检查/etc/fstab
文件,这是之前用来识别持久文件系统的同一个文件:
[nfs]# cat /etc/fstab
#
# /etc/fstab
# Created by anaconda on Wed Apr 15 09:39:23 2015
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
/dev/mapper/md0-root / xfs defaults 0 0
UUID=7873e886-78d5-46cc-b4d9-0c385995d915 /boot xfs defaults 0 0
/dev/mapper/md0-nfs /nfs xfs defaults 0 0
/dev/mapper/md0-swap swap swap defaults 0 0
从/etc/fstab
文件的内容来看,这些文件系统并没有配置为以只读
模式挂载。相反,这些文件系统是以“默认”选项挂载的。
在 Linux 上,xfs
文件系统的“默认”选项将文件系统挂载为“读写”模式,而不是“只读”模式。如果我们查看数据库服务器上的/etc/fstab
文件,我们可以验证这种行为:
[db]# cat /etc/fstab
#
# /etc/fstab
# Created by anaconda on Mon Jul 21 23:35:56 2014
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
/dev/mapper/os-root / xfs defaults 1 1
UUID=be76ec1d-686d-44a0-9411-b36931ee239b /boot xfs defaults 1 2
/dev/mapper/os-swap swap swap defaults 0 0
192.168.33.13:/nfs /data nfs defaults 0 0
在数据库服务器上,我们可以看到/
或根文件系统的文件系统选项也设置为“默认”。然而,当我们使用mount
命令查看文件系统选项时,我们可以看到rw
选项以及一些其他默认选项被应用:
[db]# mount | grep root
/dev/mapper/os-root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
这证实了三个持久文件系统的“只读”状态不是期望的配置。
识别磁盘问题
如果/etc/fstab
文件系统被特别配置为以“读写”方式挂载文件系统,并且mount
命令显示文件系统以“只读”模式挂载。这清楚地表明所涉及的文件系统可能在引导过程的一部分挂载后被重新挂载。
正如我们之前讨论的,当 Linux 系统引导时,它会读取/etc/fstab
文件并挂载所有定义的文件系统。但是,挂载文件系统的过程就此停止。默认情况下,没有持续监视/etc/fstab
文件进行更改并挂载或卸载修改后的文件系统的过程。
实际上,看到新创建的文件系统未挂载但在/etc/fstab
文件中指定是很常见的,因为有人在编辑/etc/fstab
文件后忘记使用mount
命令将其挂载。
然而,很少见到文件系统被挂载为“只读”,但之后fstab
被更改。
实际上,对于我们的情况来说,这并不容易实现,因为/etc/fstab
是不可访问的,因为/
文件系统是“只读”的:
[nfs]# touch /etc/fstab
touch: cannot touch '/etc/fstab': Read-only file system
这意味着我们的文件系统处于“只读”模式,是在这些文件系统最初被挂载后执行的。
实际上,导致这种状态的罪魁祸首实际上是我们之前浏览的日志消息:
Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device.
md/raid1:md127: Operation continuing on 1 devices.
Apr 26 10:25:55 nfs kernel: md: unbind<sdb1>
Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1)
Apr 26 10:27:20 nfs kernel: md: bind<sdb1>
Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127
Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk.
Apr 26 10:27:20 nfs kernel: md: using maximum available idle IO bandwidth (but not more than 200000 KB/sec) for recovery.
Apr 26 10:27:20 nfs kernel: md: using 128k window, over a total of 511936k.
Apr 26 10:27:20 nfs kernel: md: md127: recovery done.
从/var/log/messages
日志文件中,我们实际上可以看到在某个时候,软件 RAID(md
)存在问题,标记磁盘/dev/sdb1
为失败。
在 Linux 中,默认情况下,如果物理磁盘驱动器失败或以其他方式对内核不可用,Linux 内核将以“只读”模式重新挂载驻留在该物理磁盘上的文件系统。正如前面的错误消息中所述,sdb1
物理磁盘和md127
RAID 设备的故障似乎是文件系统变为“只读”的根本原因。
由于软件 RAID 和硬件问题是下一章的主题,我们将推迟故障排除 RAID 和磁盘问题至第八章,“硬件故障排除”。
恢复文件系统
现在我们知道文件系统为何处于“只读”模式,我们可以解决它。将文件系统从“只读”模式强制转换为“读写”模式实际上非常容易。但是,由于我们不知道导致文件系统进入“只读”模式的故障的所有情况,我们必须小心谨慎。
从文件系统错误中恢复可能非常棘手;如果操作不当,我们很容易陷入破坏文件系统或以其他方式导致部分甚至完全数据丢失的情况。
由于我们有多个文件系统处于“只读”模式,我们将首先从/boot
文件系统开始。我们之所以从/boot
文件系统开始,是因为这从技术上讲是最好的文件系统来体验数据丢失。由于/boot
文件系统仅在服务器引导过程中使用,我们可以确保在/boot
文件系统恢复之前不重新启动此服务器。
在可能的情况下,最好在采取任何行动之前备份数据。在接下来的步骤中,我们将假设/boot
文件系统定期备份。
卸载文件系统
为了恢复这个文件系统,我们将执行三个步骤。在第一步中,我们将卸载/boot
文件系统。在采取任何其他步骤之前卸载文件系统,我们将确保文件系统不会被主动写入。这一步将大大减少在恢复过程中文件系统损坏的机会。
但是,在卸载文件系统之前,我们需要确保没有应用程序或服务正在尝试写入我们正在尝试恢复的文件系统。
为了确保这一点,我们可以使用lsof
命令。 lsof
命令用于列出打开的文件;我们可以浏览此列表,以确定/boot
文件系统中是否有任何文件是打开的。
如果我们只是运行没有选项的lsof
,它将打印所有当前打开的文件:
[nfs]# lsof
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd DIR 253,1 4096 128 /
通过向lsof
添加“-r”(重复)标志,我们告诉它以重复模式运行。然后我们可以将此输出传输到grep
命令,其中我们可以过滤在/boot
文件系统上打开的文件的输出:
[nfs]# lsof -r | grep /boot
如果前面的命令一段时间内没有产生任何输出,可以安全地继续卸载文件系统。如果命令打印出任何打开的文件,最好找到适当的进程读取/写入文件系统并在卸载文件系统之前停止它们。
由于我们的例子在/boot
文件系统上没有打开的文件,我们可以继续卸载/boot
文件系统。为此,我们将使用umount
命令:
[nfs]# umount /boot
幸运的是,umount
命令没有出现错误。如果文件正在被写入,我们在卸载时可能会收到错误。通常,此错误包括一条消息,指出设备正忙。为了验证文件系统已成功卸载,我们可以再次使用mount
命令:
[nfs]# mount | grep /boot
现在/boot
文件系统已经卸载,我们可以执行我们恢复过程的第二步。我们现在可以检查和修复文件系统。
文件系统检查与 fsck
Linux 有一个非常有用的文件系统检查命令,可以用来检查和修复文件系统。这个命令叫做fsck
。
然而,fsck
命令实际上并不只是一个命令。每种文件系统类型都有其自己的检查一致性和修复问题的方法。 fsck
命令只是一个调用相应文件系统的适当命令的包装器。
例如,当对ext4
文件系统运行fsck
命令时,实际执行的命令是e2fsck
。 e2fsck
命令用于ext2
到ext4
文件系统类型。
我们可以以两种方式调用e2fsck
,直接或间接通过fsck
命令。在这个例子中,我们将使用fsck
方法,因为这可以用于 Linux 支持的几乎所有文件系统。
要使用fsck
命令简单地检查文件系统的一致性,我们可以不带标志运行它,并指定要检查的磁盘设备:
[nfs]# fsck /dev/sda1
fsck from util-linux 2.20.1
e2fsck 1.42.9 (4-Feb-2014)
cloudimg-rootfs: clean, 85858/2621440 files, 1976768/10485504 blocks
在前面的例子中,我们可以看到文件系统没有发现任何错误。如果有的话,我们会被问及是否希望e2fsck
实用程序来纠正这些错误。
如果我们愿意,我们可以通过传递“-y”(是)标志使fsck
自动修复发现的问题:
[nfs]# fsck -y /dev/sda1
fsck from util-linux 2.20.1
e2fsck 1.42 (29-Nov-2011)
/dev/sda1 contains a file system with errors, check forced.
Pass 1: Checking inodes, blocks, and sizes
Inode 2051351 is a unknown file type with mode 0137642 but it looks
like it is really a directory.
Fix? yes
Pass 2: Checking directory structure
Entry 'test' in / (2) has deleted/unused inode 49159\. Clear? yes
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/sda1: ***** FILE SYSTEM WAS MODIFIED *****
/dev/sda1: 96/2240224 files (7.3% non-contiguous), 3793508/4476416 blocks
此时,e2fsck
命令将尝试纠正它发现的任何错误。幸运的是,从我们的例子中,错误能够被纠正;然而,也有时候情况并非如此。
fsck 和 xfs 文件系统
当对xfs
文件系统运行fsck
命令时,结果实际上是完全不同的:
[nfs]# fsck /dev/md127
fsck from util-linux 2.23.2
If you wish to check the consistency of an XFS filesystem or
repair a damaged filesystem, see xfs_repair(8).
xfs
文件系统不同于ext2/3/4
文件系统系列,因为每次挂载文件系统时都会执行一致性检查。这并不意味着您不能手动检查和修复文件系统。要检查xfs
文件系统,我们可以使用xfs_repair
实用程序:
[nfs]# xfs_repair -n /dev/md127
Phase 1 - find and verify superblock...
Phase 2 - using internal log
- scan filesystem freespace and inode maps...
- found root inode chunk
Phase 3 - for each AG...
- scan (but don't clear) agi unlinked lists...
- process known inodes and perform inode discovery...
- agno = 0
- agno = 1
- agno = 2
- agno = 3
- process newly discovered inodes...
Phase 4 - check for duplicate blocks...
- setting up duplicate extent list...
- check for inodes claiming duplicate blocks...
- agno = 0
- agno = 1
- agno = 2
- agno = 3
No modify flag set, skipping phase 5
Phase 6 - check inode connectivity...
- traversing filesystem ...
- traversal finished ...
- moving disconnected inodes to lost+found ...
Phase 7 - verify link counts...
No modify flag set, skipping filesystem flush and exiting.
使用-n
(不修改)标志后跟要检查的设备执行xfs_repair
实用程序时,它只会验证文件系统的一致性。在这种模式下运行时,它根本不会尝试修复文件系统。
要以修复文件系统的模式运行xfs_repair
,只需省略-n
标志,如下所示:
[nfs]# xfs_repair /dev/md127
Phase 1 - find and verify superblock...
Phase 2 - using internal log
- zero log...
- scan filesystem freespace and inode maps...
- found root inode chunk
Phase 3 - for each AG...
- scan and clear agi unlinked lists...
- process known inodes and perform inode discovery...
- agno = 0
- agno = 1
- agno = 2
- agno = 3
- process newly discovered inodes...
Phase 4 - check for duplicate blocks...
- setting up duplicate extent list...
- check for inodes claiming duplicate blocks...
- agno = 0
- agno = 1
- agno = 2
- agno = 3
Phase 5 - rebuild AG headers and trees...
- reset superblock...
Phase 6 - check inode connectivity...
- resetting contents of realtime bitmap and summary inodes
- traversing filesystem ...
- traversal finished ...
- moving disconnected inodes to lost+found ...
Phase 7 - verify and correct link counts...
Done
从前面的xfs_repair
命令的输出来看,我们的/boot
文件系统似乎不需要任何修复过程。
这些工具是如何修复文件系统的?
你可能会认为使用fsck
和xfs_repair
等工具修复这个文件系统非常容易。原因很简单,这是因为xfs
和ext2/3/4
等文件系统的设计。xfs
和ext2/3/4
家族都是日志文件系统;这意味着这些类型的文件系统会记录对文件系统对象(如文件、目录等)所做的更改。
这些更改将保存在日志中,直到更改提交到主文件系统。xfs_repair
实用程序只是查看这个日志,并重放未提交到主文件系统的最后更改。这些文件系统日志使文件系统在意外断电或系统重新启动等情况下非常有韧性。
不幸的是,有时文件系统的日志和诸如xfs_repair
之类的工具并不足以纠正情况。
在这种情况下,还有一些更多的选项,比如以强制模式运行修复。然而,这些选项应该总是保留作为最后的努力,因为它们有时会导致文件系统损坏。
如果你发现自己有一个损坏且无法修复的文件系统,最好的办法可能就是重新创建文件系统并恢复备份,如果有备份的话...
挂载文件系统
现在/boot
文件系统已经经过检查和修复,我们可以简单地重新挂载它以验证数据是否正确。为此,我们可以简单地运行mount
命令,然后跟上/boot
:
[nfs]# mount /boot
[nfs]# mount | grep /boot
/dev/md127 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
当文件系统在/etc/fstab
文件中定义时,可以只使用mount
点调用mount
和umount
命令。这将导致这两个命令根据/etc/fstab
文件中的定义来mount
或unmount
文件系统。
从mount
的输出来看,我们的/boot
文件系统现在是读写
而不是只读
。如果我们执行ls
命令,我们也应该仍然看到我们的原始数据:
[nfs]# ls /boot
config-3.10.0-229.1.2.el7.x86_64 initrd-plymouth.img
config-3.10.0-229.el7.x86_64 symvers-3.10.0-229.1.2.el7.x86_64.gz
grub symvers-3.10.0-229.el7.x86_64.gz
grub2 System.map-3.10.0-229.1.2.el7.x86_64
initramfs-0-rescue-3f370097c831473a8cfec737ff1d6c55.img System.map-3.10.0-229.el7.x86_64
initramfs-3.10.0-229.1.2.el7.x86_64.img vmlinuz-0-rescue-3f370097c831473a8cfec737ff1d6c55
initramfs-3.10.0-229.1.2.el7.x86_64kdump.img vmlinuz-3.10.0-229.1.2.el7.x86_64
initramfs-3.10.0-229.el7.x86_64.img vmlinuz-3.10.0-229.el7.x86_64
initramfs-3.10.0-229.el7.x86_64kdump.img
看来我们的恢复步骤取得了成功!现在我们已经用/boot
文件系统测试过它们,我们可以开始修复/nfs
文件系统了。
修复其他文件系统
修复/nfs
文件系统的步骤实际上与/boot
文件系统的步骤基本相同,只有一个主要的区别,如下所示:
[nfs]# lsof -r | grep /nfs
rpc.statd 1075 rpcuser cwd DIR 253,1 40 592302 /var/lib/nfs/statd
rpc.mount 2282 root cwd DIR 253,1 4096 9125499 /var/lib/nfs
rpc.mount 2282 root 4u REG 0,3 0 4026532125 /proc/2280/net/rpc/nfd.export/channel
rpc.mount 2282 root 5u REG 0,3 0 4026532129 /proc/2280/net/rpc/nfd.fh/channel
使用lsof
检查/nfs
文件系统上的打开文件时,我们可能看不到 NFS 服务进程。然而,很有可能 NFS 服务在lsof
命令停止后会尝试访问这个共享文件系统中的文件。为了防止这种情况,最好(如果可能的话)在对共享文件系统进行任何更改时停止 NFS 服务:
[nfs]# systemctl stop nfs
一旦 NFS 服务停止,其余步骤都是一样的:
[nfs]# umount /nfs
[nfs]# xfs_repair /dev/md0/nfs
Phase 1 - find and verify superblock...
Phase 2 - using internal log
- zero log...
- scan filesystem freespace and inode maps...
- found root inode chunk
Phase 3 - for each AG...
- scan and clear agi unlinked lists...
- process known inodes and perform inode discovery...
- agno = 0
- agno = 1
- agno = 2
- agno = 3
- process newly discovered inodes...
Phase 4 - check for duplicate blocks...
- setting up duplicate extent list...
- check for inodes claiming duplicate blocks...
- agno = 0
- agno = 1
- agno = 2
- agno = 3
Phase 5 - rebuild AG headers and trees...
- reset superblock...
Phase 6 - check inode connectivity...
- resetting contents of realtime bitmap and summary inodes
- traversing filesystem ...
- traversal finished ...
- moving disconnected inodes to lost+found ...
Phase 7 - verify and correct link counts...
done
文件系统修复后,我们可以简单地按如下方式重新挂载它:
[nfs]# mount /nfs
[nfs]# mount | grep /nfs
nfsd on /proc/fs/nfsd type nfsd (rw,relatime)
/dev/mapper/md0-nfs on /nfs type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
重新挂载/nfs
文件系统后,我们可以看到选项显示为rw
,这意味着它是可读写
的。
恢复/
(根)文件系统
/
或root
文件系统有点不同。它不同,因为它是包含大部分 Linux 软件包、二进制文件和命令的顶层文件系统。这意味着我们不能简单地卸载这个文件系统,否则就会丢失重新挂载它所需的工具。
因此,我们实际上将使用mount
命令重新挂载/
文件系统,而无需先卸载它:
[nfs]# mount -o remount /
为了告诉mount
命令卸载然后重新挂载文件系统,我们只需要传递-o
(选项)标志,后面跟着选项remount
。-o
标志允许您从命令行传递文件系统选项,如rw
或ro
。当我们重新挂载/
文件系统时,我们只是传递重新挂载文件系统选项:
# mount | grep root
/dev/mapper/md0-root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
如果我们使用mount
命令来显示已挂载的文件系统,我们可以验证/
文件系统已重新挂载为读写
访问。由于文件系统类型为xfs
,重新挂载应该导致文件系统执行一致性检查和修复。如果我们对/
文件系统的完整性有任何疑问,下一步应该是简单地重新启动 NFS 服务器。
如果服务器无法挂载/
文件系统,xfs_repair
实用程序将自动调用。
验证
目前,我们可以看到 NFS 服务器的文件系统问题已经恢复。我们现在应该验证我们的 NFS 客户端能否写入 NFS 共享。但在这之前,我们还应该先重新启动之前停止的 NFS 服务:
[nfs]# systemctl start nfs
[nfs]# systemctl status nfs
nfs-server.service - NFS server and services
Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled)
Active: active (exited) since Mon 2015-04-27 22:20:46 MST; 6s ago
Process: 2278 ExecStopPost=/usr/sbin/exportfs -f (code=exited, status=0/SUCCESS)
Process: 3098 ExecStopPost=/usr/sbin/exportfs -au (code=exited, status=1/FAILURE)
Process: 3095 ExecStop=/usr/sbin/rpc.nfsd 0 (code=exited, status=0/SUCCESS)
Process: 3265 ExecStart=/usr/sbin/rpc.nfsd $RPCNFSDARGS (code=exited, status=0/SUCCESS)
Process: 3264 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS)
Main PID: 3265 (code=exited, status=0/SUCCESS)
CGroup: /system.slice/nfs-server.service
一旦 NFS 服务启动,我们可以使用touch
命令从客户端进行测试:
[db]# touch /data/testfile.txt
[db]# ls -la /data/testfile.txt
-rw-r--r--. 1 root root 0 Apr 28 05:24 /data/testfile.txt
看起来我们已经成功解决了问题。
另外,如果我们注意到对 NFS 共享的请求花费了很长时间,可能需要在客户端上卸载并重新挂载 NFS 共享。如果 NFS 客户端没有意识到 NFS 服务器已重新启动,这是一个常见问题。
总结
在本章中,我们深入探讨了文件系统的挂载方式,NFS 的配置以及文件系统进入只读
模式时应该采取的措施。我们甚至进一步手动修复了一个物理磁盘设备出现问题的文件系统。
在下一章中,我们将进一步解决硬件故障的问题。这意味着查看硬件消息日志,解决硬盘 RAID 集的故障以及许多其他与硬件相关的故障排除步骤。
第八章:硬件故障排除
在上一章中,我们确定了我们的 NFS 上的文件系统被挂载为只读。为了确定原因,我们围绕 NFS 和文件系统进行了大量的故障排除。我们使用了诸如showmount
查看可用的 NFS 共享和mount
命令显示已挂载的文件系统等命令。
一旦我们确定了问题,我们就能够使用fsck
命令执行文件系统检查和恢复文件系统。
在本章中,我们将继续从第七章文件系统错误和恢复的路径,并调查硬件设备故障。本章将涵盖许多日志文件和工具,这些工具不仅可以确定硬件故障是否发生,还可以确定为什么发生。
从日志条目开始
在第七章文件系统错误和恢复中,当查看/var/log/messages
日志文件以识别 NFS 服务器文件系统的问题时,我们注意到了以下消息:
Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device.
md/raid1:md127: Operation continuing on 1 devices.
Apr 26 10:25:55 nfs kernel: md: unbind<sdb1>
Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1)
Apr 26 10:27:20 nfs kernel: md: bind<sdb1>
Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127
Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk.
Apr 26 10:27:20 nfs kernel: md: using maximum available idle IO bandwidth (but not more than 200000 KB/sec) for recovery.
Apr 26 10:27:20 nfs kernel: md: using 128k window, over a total of 511936k.
Apr 26 10:27:20 nfs kernel: md: md127: recovery done.
前面的消息表明 RAID 设备/dev/md127
发生了故障。由于上一章主要关注文件系统本身的问题,我们没有进一步调查 RAID 设备的故障。在本章中,我们将进行调查以确定原因和解决方法。
为了开始调查,我们应该首先查看原始日志消息,因为这些消息可以告诉我们关于 RAID 设备状态的很多信息。
首先,让我们将消息分解成以下几个小节:
Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device.
md/raid1:md127: Operation continuing on 1 devices.
第一条日志消息实际上非常有意义。显示的第一个关键信息是消息所涉及的 RAID 设备(md/raid1:md127)
。
通过这个设备的名称,我们已经知道了很多。我们知道的第一件事是,这个 RAID 设备是由 Linux 的软件 RAID 系统多设备驱动程序(md)创建的。该系统允许 Linux 将两个独立的磁盘应用 RAID。
由于本章主要涉及 RAID,我们应该首先了解 RAID 是什么以及它是如何工作的。
什么是 RAID?
独立磁盘冗余阵列(RAID)通常是一个软件或硬件系统,允许用户将多个磁盘作为一个设备使用。RAID 可以以多种方式配置,从而实现更大的数据冗余或性能。
这种配置通常被称为 RAID 级别。不同类型的 RAID 级别提供不同的功能,以更好地了解 RAID 级别。让我们探索一些常用的 RAID 级别。
RAID 0 – 分区
RAID 0 是最简单的 RAID 级别之一。RAID 0 的工作原理是将多个磁盘组合起来作为一个磁盘。当数据写入 RAID 设备时,数据被分割,部分数据被写入每个磁盘。为了更好地理解这一点,让我们举一个简单的例子。
- 如果我们有一个由五个 500GB 驱动器组成的简单 RAID 0 设备,我们的 RAID 设备将是所有五个驱动器的大小——2500GB 或 2.5TB。如果我们要向 RAID 设备写入一个 50MB 的文件,文件的 10MB 数据将同时写入每个磁盘。
这个过程通常被称为分区。在同样的情况下,当从 RAID 设备中读取那个 50MB 文件时,读取请求也将同时由每个磁盘处理。
将文件分割并同时处理每个磁盘的部分可以提供更好的写入或读取请求性能。事实上,因为我们有五个磁盘,请求速度会提高 5 倍。
一个简单的类比是,如果你有五个人以相同的速度建造一堵墙,他们将比一个人建造同样的墙快五倍。
虽然 RAID 0 提供了性能,但它并不提供任何数据保护。如果这种 RAID 中的单个驱动器失败,该驱动器的数据将不可用,这种故障可能导致完全的数据丢失。
RAID 1 - 镜像
RAID 1 是另一种简单的 RAID 级别。与 RAID 0 不同,RAID 1 中的驱动器是镜像的。RAID 1 通常由两个或更多个驱动器组成。当数据被写入 RAID 设备时,数据会完整地写入每个设备。
这个过程被称为镜像,因为数据基本上在所有驱动器上都是镜像的:
-
使用与之前相同的场景,如果我们在 RAID 1 配置中有五个 500GB 磁盘驱动器,总磁盘大小将为 500GB。当我们将相同的 50MB 文件写入 RAID 设备时,每个驱动器将获得该 50MB 文件的副本。
-
这也意味着写请求的速度将只有 RAID 中最慢的驱动器那么快。对于 RAID 1,每个驱动器必须在被视为完成之前完成写请求。
-
然而,读请求可以由 RAID 1 中的任何一个驱动器提供。因此,RAID 1 有时可以更快地提供读请求,因为每个请求可以由 RAID 中的不同驱动器执行。
RAID 1 提供了最高级别的数据弹性,因为在故障期间只需要一个磁盘驱动器保持活动。使用我们的五盘场景,我们可以丢失五个磁盘中的四个并且仍然重建和使用 RAID。这就是为什么在数据保护比磁盘性能更重要时应该使用 RAID 1 的原因。
RAID 5 - 条带化与分布式奇偶校验
RAID 5是一个难以理解的 RAID 级别的例子。RAID 5 通过将数据条带化到多个磁盘上来工作,就像 RAID 0 一样,但它还包括奇偶校验。奇偶校验数据是通过对写入 RAID 设备的数据执行异或运算而生成的特殊数据。生成的数据可以用于从另一个驱动器重建丢失的数据。
- 使用与之前相同的例子,我们在 RAID 5 配置中有五个 500GB 硬盘驱动器,如果我们再次写入一个 50MB 的文件,每个磁盘将接收 10MB 的数据;这与 RAID 0 完全相同。然而,与 RAID 0 不同的是,每个磁盘还会写入奇偶校验数据。由于额外的奇偶校验数据,RAID 可用的总数据大小是四个驱动器的总和,其中一个驱动器的数据分配给奇偶校验。在我们的情况下,这意味着可用的磁盘空间为 2TB,其中 500GB 用于奇偶校验。
通常有一个误解,即奇偶校验数据是写入专用驱动器的 RAID 5。事实并非如此。只是奇偶校验数据的大小是一个完整磁盘的空间。然而,这些数据是分布在所有磁盘上的。
使用 RAID 5 而不是 RAID 0 的原因是,如果单个驱动器失败,数据可以被重建。RAID 5 的唯一问题是,如果两个驱动器失败,RAID 无法重建,可能导致数据丢失。
RAID 6 - 双分布式奇偶校验条带化
RAID 6本质上与 RAID 5 相同类型的 RAID;但是,奇偶校验数据是双倍的。通过加倍奇偶校验数据,RAID 可以在最多两个磁盘故障时存活。由于奇偶校验是双倍的,如果我们将五个 500GB 硬盘驱动器放入 RAID 6 配置中,可用的磁盘空间将是 1.5TB,即 3 个驱动器的总和;另外 1TB 的数据空间将被两组奇偶校验数据占用。
RAID 10 - 镜像和条带化
RAID 10(通常称为 RAID 1 + 0)是另一种非常常见的 RAID 级别。RAID 10 本质上是 RAID 1 和 RAID 0 的组合。使用 RAID 10,每个磁盘都有一个镜像,并且数据被条带化到所有镜像驱动器上。为了解释这一点,我们将使用与上面类似的例子;但是,我们将使用六个 500GB 驱动器。
- 如果我们要写入一个 30MB 的文件,它将被分成 10MB 的块,并分别写入三个 RAID 设备。这些 RAID 设备是 RAID 1 的镜像。基本上,RAID 10 是许多 RAID 1 设备在 RAID 0 配置中一起条带化。
RAID 10 配置在性能和数据保护之间取得了良好的平衡。为了发生完全故障,镜像的两侧都必须失败;这意味着 RAID 1 的两侧都会失败。
考虑到 RAID 中的磁盘数量,这种情况发生的可能性比 RAID 5 的可能性要小。从性能的角度来看,RAID 10 仍然受益于条带化方法,并且能够将单个文件的不同块写入到每个磁盘,从而提高写入速度。
RAID 10 也受益于具有相同数据的两个磁盘;与 RAID 1 一样,当发出读取请求时,任何一个磁盘都可以处理该请求,从而允许每个磁盘独立处理并发的读取请求。
RAID 10 的缺点是,虽然它通常可以满足或超过 RAID 5 的性能,但通常需要更多的硬件来实现这一点,因为每个磁盘都是镜像的,你会失去一半的总磁盘空间。
以我们之前的例子,我们在 RAID 10 配置中使用六个 500GB 驱动器的可用空间将是 1.5TB。简单来说,它是我们磁盘容量的 50%。这个相同的容量在 RAID 5 中使用 4 个驱动器也是可用的。
回到排除故障我们的 RAID
现在我们对 RAID 和不同的配置有了更好的理解,让我们回到调查我们的错误。
Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device.
md/raid1:md127: Operation continuing on 1 devices.
从前面的错误中,我们可以看到我们的 RAID 设备是md127。我们还可以看到这个设备是一个 RAID 1 设备(md/raid1
)。表明操作在 1 个设备上继续的消息意味着镜像的第二部分仍然可用。
好消息是,如果镜像的两侧都不可用,RAID 将完全失败并导致更严重的问题。
既然我们现在知道受影响的 RAID 设备、使用的 RAID 类型,甚至是失败的硬盘,我们对这次故障有了相当多的信息。如果我们继续查看/var/log/messages
中的日志条目,我们甚至可以找到更多信息:
Apr 26 10:25:55 nfs kernel: md: unbind<sdb1>
Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1)
Apr 26 10:27:20 nfs kernel: md: bind<sdb1>
Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127
Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk.
前面的消息很有趣,因为它们表明 Linux 软件 RAID 服务 MD 尝试恢复 RAID:
Apr 26 10:25:55 nfs kernel: md: unbind<sdb1>
在日志的这一部分的第一行中,似乎设备sdb1
已从 RAID 中移除:
Apr 26 10:27:20 nfs kernel: md: bind<sdb1>
然而,第三行表明设备sdb1
已重新添加到 RAID 或“绑定”到 RAID。
第四和第五行显示 RAID 开始了恢复步骤:
Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127
Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk.
RAID 恢复的工作原理
早些时候,我们讨论了各种 RAID 级别如何能够通过奇偶校验数据或镜像数据重建和恢复丢失的设备数据。
当 RAID 设备失去其中一个驱动器,并且该驱动器被替换或重新添加到 RAID 时,无论是软件还是硬件 RAID 管理器都将开始重建数据。重建的目标是重新创建应该在丢失的驱动器上的数据。
如果 RAID 是镜像 RAID,将从可用的镜像磁盘读取数据并写入替换的磁盘。
对于基于奇偶校验的 RAID,重建将基于 RAID 中已经条带化的存活数据和奇偶校验数据。
在奇偶校验 RAID 的重建过程中,任何额外的故障都可能导致重建失败。对于基于镜像的 RAID,只要有一份完整的数据副本用于重建,故障可以发生在任何磁盘上。
在我们捕获的日志消息的末尾,我们可以看到重建成功了:
Apr 26 10:27:20 nfs kernel: md: md127: recovery done.
根据前一章中日志消息的结尾,RAID设备/dev/md127
是健康的。
检查当前的 RAID 状态
虽然/var/log/messages
是查看服务器上发生了什么的好方法,但这并不一定意味着这些日志消息与 RAID 的当前状态准确无误。
为了查看 RAID 设备的当前状态,我们可以运行一些命令。
我们将使用的第一个命令是mdadm
命令:
[nfs]# mdadm --detail /dev/md127
/dev/md127:
Version : 1.0
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 511936 (500.02 MiB 524.22 MB)
Raid Devices : 2
Total Devices : 1
Persistence : Superblock is persistent
Intent Bitmap : Internal
Update Time : Sun May 10 06:16:10 2015
State : clean, degraded
Active Devices : 1
Working Devices : 1
Failed Devices : 0
Spare Devices : 0
Name : localhost:boot
UUID : 7adf0323:b0962394:387e6cd0:b2914469
Events : 52
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
2 0 0 2 removed
mdadm
命令用于管理基于 Linux MD 的 RAID。在上述命令中,我们指定了标志--detail
,后跟一个 RAID 设备。这告诉mdadm
打印指定 RAID 设备的详细信息。
mdadm
命令不仅可以打印状态,还可以用于执行 RAID 活动,如创建、销毁或修改 RAID 设备。
为了理解--detail
标志的输出,让我们将上面的输出分解如下:
/dev/md127:
Version : 1.0
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 511936 (500.02 MiB 524.22 MB)
Raid Devices : 2
Total Devices : 1
Persistence : Superblock is persistent
第一部分告诉我们关于 RAID 本身的很多信息。需要注意的重要项目是Creation Time
,在这种情况下是Wed April 15th
上午 9:39。这告诉我们 RAID 首次创建的时间。
Raid Level
也被记录下来,就像我们在/var/log/messages
中看到的那样是 RAID 1。我们还可以看到Array Size
,告诉我们 RAID 设备将提供的总可用磁盘空间(524 MB)和在这个 RAID 数组中使用的Raid Devices
的数量,这种情况下是两个设备。
组成这个 RAID 的设备数量很重要,因为它可以帮助我们了解这个 RAID 的状态。
由于我们的 RAID 由总共两个设备组成,如果任何一个设备失败,我们知道我们的 RAID 将面临完全失败的风险,如果剩下的磁盘丢失。然而,如果我们的 RAID 由三个设备组成,我们将知道即使丢失两个磁盘也不会导致完全的 RAID 失败。
仅从mdadm
命令的前半部分,我们就可以看到关于这个 RAID 的相当多的信息。从后半部分,我们将找到更多关键信息,如下所示:
Intent Bitmap : Internal
Update Time : Sun May 10 06:16:10 2015
State : clean, degraded
Active Devices : 1
Working Devices : 1
Failed Devices : 0
Spare Devices : 0
Name : localhost:boot
UUID : 7adf0323:b0962394:387e6cd0:b2914469
Events : 52
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
2 0 0 2 removed
Update Time
很有用,因为它显示了此 RAID 更改状态的最后时间,无论该状态更改是添加磁盘还是重建。
这个时间戳可能很有用,特别是如果我们试图将其与/var/log/messages
或其他系统事件中的日志条目相关联。
另一个重要的信息是RAID Device State
,对于我们的例子来说,是 clean, degraded。降级状态意味着 RAID 有一个失败的设备,但 RAID 本身仍然是功能性的。Degraded 只是意味着功能性但不是最佳状态。
如果我们的 RAID 设备正在进行重建或恢复,我们也会看到这些状态列出。
在当前状态输出下,我们可以看到四个设备类别,告诉我们关于用于此 RAID 的硬盘的信息。第一个是Active Devices
;这告诉我们当前在 RAID 中活动的驱动器数量。
第二个是Working Devices
;这告诉我们工作驱动器的数量。通常,Working Devices
和Active Devices
的数量将是相同的。
列表中的第四项是Failed Devices
;这是当前标记为失败的设备数量。尽管我们的 RAID 目前有一个失败的设备,但这个数字是0
。有一个有效的原因,但我们稍后会解释这个原因。
列表中的最后一项是Spare Devices
的数量。在一些 RAID 系统中,您可以创建备用设备,用于在发生诸如驱动器故障之类的事件中重建 RAID。
这些备用设备可能会派上用场,因为 RAID 系统通常会自动重建 RAID,从而降低 RAID 完全失败的可能性。
通过mdadm
的输出的最后两行,我们可以看到组成 RAID 的驱动器的信息。
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
2 0 0 2 removed
从输出中,我们可以看到我们有一个磁盘设备/dev/sda1
,目前处于活动同步状态。我们还可以看到另一个设备已从 RAID 中移除。
总结关键信息
从mdadm --detail
的输出中,我们可以看到/dev/md127
是一个 RAID 设备,其 RAID 级别为 1,目前处于降级状态。我们可以从详细信息中看到,降级状态是由于组成 RAID 的驱动器之一目前被移除。
使用/proc/mdstat 查看 md 状态
另一个查看 MD 当前状态的有用位置是/proc/mdstat
;这个文件和/proc
中的许多文件一样,是由内核不断更新的。如果我们使用cat
命令来读取这个文件,我们可以快速查看服务器当前的 RAID 状态:
[nfs]# cat /proc/mdstat
Personalities : [raid1]
md126 : active raid1 sda2[0]
7871488 blocks super 1.2 [2/1] [U_]
bitmap: 1/1 pages [4KB], 65536KB chunk
md127 : active raid1 sda1[0]
511936 blocks super 1.0 [2/1] [U_]
bitmap: 1/1 pages [4KB], 65536KB chunk
unused devices: <none>
/proc/mdstat
的内容有些晦涩,但如果我们分解它们,它包含了相当多的信息。
Personalities : [raid1]
第一行的Personalities
告诉我们这个系统上内核当前支持的 RAID 级别。对于我们的例子,它是 RAID 1:
md126 : active raid1 sda2[0]
7871488 blocks super 1.2 [2/1] [U_]
bitmap: 1/1 pages [4KB], 65536KB chunk
接下来的几行是/dev/md126
的当前状态,这是系统上另一个我们还没有看过的 RAID 设备。这三行实际上可以给我们提供关于md126
的相当多的信息;事实上,它们给了我们和mdadm --detail
几乎相同的信息。
md126 : active raid1 sda2[0]
在第一行中,我们可以看到设备名称md126
。我们可以看到 RAID 的当前状态是活动的。我们还可以看到这个 RAID 设备的 RAID 级别是 RAID 1。最后,我们还可以看到组成这个 RAID 的磁盘设备;在我们的例子中,只有sda2
。
第二行还包含以下关键信息:
7871488 blocks super 1.2 [2/1] [U_]
具体来说,最后两个值对我们当前的任务最有用,[2/1]
显示了分配给这个 RAID 的磁盘设备数量以及可用的数量。从例子中的值我们可以看到,期望有 2 个驱动器,但只有 1 个可用。
最后一个值[U_]
显示了组成这个 RAID 的驱动器的当前状态。状态 U 表示正常,"_"表示不正常。
在我们的例子中,我们可以看到一个磁盘设备是正常的,另一个是不正常的。
根据以上信息,我们能够确定 RAID 设备/dev/md126
目前处于活动状态;它正在使用 RAID 级别 1,目前有两个磁盘中的一个不可用。
如果我们继续查看/proc/mdstat
文件,我们可以看到md127
的状态也类似。
使用/proc/mdstat 和 mdadm
在查看/proc/mdstat
和mdadm --detail
之后,我们可以看到两者提供了类似的信息。根据我的经验,我发现同时使用mdstat
和mdadm
是有用的。/proc/mdstat
文件通常是我快速查看系统上所有 RAID 设备的快照的地方,而mdadm
命令通常是我用来获取更深入的 RAID 设备详细信息的地方(例如备用驱动器的数量、创建时间和最后更新时间等细节)。
识别更大的问题
之前在使用mdadm
查看md127
的当前状态时,我们发现 RAID 设备md127
有一个磁盘被移出服务。在查看/proc/mdstat
时,我们发现还有另一个 RAID 设备/dev/md126
,也有一个磁盘被移出服务。
我们还可以看到的另一个有趣的事实是,RAID 设备/dev/md126
是一个存活的磁盘:/dev/sda1
。这很有趣,因为/dev/md127
的存活磁盘是/dev/sda2
。如果我们记得之前的章节,/dev/sda1
和/dev/sda2
只是来自同一物理磁盘的两个分区。考虑到两个 RAID 设备都有一个丢失的驱动器,而我们的日志表明/dev/md127
曾经将/dev/sdb1
移除并重新添加。很可能/dev/md127
和/dev/md126
都在使用/dev/sdb
的分区。
由于/proc/mdstat
对于 RAID 设备只有两种状态,正常或不正常,我们可以使用--detail
标志来确认第二个磁盘是否真的已经从/dev/md126
中移除:
[nfs]# mdadm --detail /dev/md126
/dev/md126:
Version : 1.2
Creation Time : Wed Apr 15 09:39:19 2015
Raid Level : raid1
Array Size : 7871488 (7.51 GiB 8.06 GB)
Used Dev Size : 7871488 (7.51 GiB 8.06 GB)
Raid Devices : 2
Total Devices : 1
Persistence : Superblock is persistent
Intent Bitmap : Internal
Update Time : Mon May 11 04:03:09 2015
State : clean, degraded
Active Devices : 1
Working Devices : 1
Failed Devices : 0
Spare Devices : 0
Name : localhost:pv00
UUID : bec13d99:42674929:76663813:f748e7cb
Events : 5481
Number Major Minor RaidDevice State
0 8 2 0 active sync /dev/sda2
2 0 0 2 removed
从输出中,我们可以看到/dev/md126
的当前状态和配置与/dev/md127
完全相同。根据这个信息,我们可以假设/dev/md126
曾经将/dev/sdb2
作为其 RAID 的一部分。
由于我们怀疑问题可能只是一个硬盘出了问题,我们需要验证这是否真的是这种情况。第一步是确定是否真的存在/dev/sdb
设备;这样做的最快方法是使用ls
命令在/dev
中执行目录列表:
[nfs]# ls -la /dev/ | grep sd
brw-rw----. 1 root disk 8, 0 May 10 06:16 sda
brw-rw----. 1 root disk 8, 1 May 10 06:16 sda1
brw-rw----. 1 root disk 8, 2 May 10 06:16 sda2
brw-rw----. 1 root disk 8, 16 May 10 06:16 sdb
brw-rw----. 1 root disk 8, 17 May 10 06:16 sdb1
brw-rw----. 1 root disk 8, 18 May 10 06:16 sdb2
从这个ls
命令的结果中,我们可以看到实际上有一个sdb
、sdb1
和sdb2
设备。在进一步之前,让我们更清楚地了解一下/dev
。
理解/dev
/dev
目录是一个特殊的目录,其中的内容是内核在安装时创建的。该目录包含特殊文件,允许用户或应用程序与物理设备,有时是逻辑设备进行交互。
如果我们看一下之前ls
命令的结果,我们可以看到在/dev
目录中有几个以sd
开头的文件。
在上一章中,我们学到以sd
开头的文件实际上被视为 SCSI 或 SATA 驱动器。在我们的情况下,我们有/dev/sda
和/dev/sdb
;这意味着在这个系统上有两个物理 SCSI 或 SATA 驱动器。
额外的设备/dev/sda1
、/dev/sda2
、/dev/sdb1
和/dev/sdb2
只是这些磁盘的分区。实际上,对于磁盘驱动器,以数字结尾的设备名称通常是另一个设备的分区,就像/dev/sdb1
是/dev/sdb
的分区一样。虽然当然也有一些例外,但通常在排除磁盘驱动器故障时,做出这种假设是安全的。
不仅仅是磁盘驱动器
/dev/
目录中包含的不仅仅是磁盘驱动器。如果我们查看/dev/
,我们实际上可以看到一些常见的设备。
[nfs]# ls -F /dev
autofs hugepages/ network_throughput snd/ tty21 tty4 tty58 vcs1
block/ initctl| null sr0 tty22 tty40 tty59 vcs2
bsg/ input/ nvram stderr@ tty23 tty41 tty6 vcs3
btrfs-control kmsg oldmem stdin@ tty24 tty42 tty60 vcs4
bus/ log= port stdout@ tty25 tty43 tty61 vcs5
cdrom@ loop-control ppp tty tty26 tty44 tty62 vcs6
char/ lp0 ptmx tty0 tty27 tty45 tty63 vcsa
console lp1 pts/ tty1 tty28 tty46 tty7 vcsa1
core@ lp2 random tty10 tty29 tty47 tty8 vcsa2
cpu/ lp3 raw/ tty11 tty3 tty48 tty9 vcsa3
cpu_dma_latency mapper/ rtc@ tty12 tty30 tty49 ttyS0 vcsa4
crash mcelog rtc0 tty13 tty31 tty5 ttyS1 vcsa5
disk/ md/ sda tty14 tty32 tty50 ttyS2 vcsa6
dm-0 md0/ sda1 tty15 tty33 tty51 ttyS3 vfio/
dm-1 md126 sda2 tty16 tty34 tty52 uhid vga_arbiter
dm-2 md127 sdb tty17 tty35 tty53 uinput vhost-net
fd@ mem sdb1 tty18 tty36 tty54 urandom zero
full mqueue/ sdb2 tty19 tty37 tty55 usbmon0
fuse net/ shm/ tty2 tty38 tty56 usbmon1
hpet network_latency snapshot tty20 tty39 tty57 vcs
从这个ls
的结果中,我们可以看到/dev
目录中有许多文件、目录和符号链接。
以下是一些常见的有用的设备或目录列表:
- /dev/cdrom:这通常是一个指向
cdrom
设备的符号链接。CD-ROM 的实际设备遵循类似硬盘的命名约定,它以sr
开头,后面跟着设备的编号。我们可以用ls
命令看到/dev/cdrom
符号链接指向哪里:
[nfs]# ls -la /dev/cdrom
lrwxrwxrwx. 1 root root 3 May 10 06:16 /dev/cdrom -> sr0
-
/dev/console:这个设备不一定与特定的硬件设备(如
/dev/sda
或/dev/sr0
)相关联。控制台设备用于与系统控制台进行交互,这可能是实际的监视器,也可能不是。 -
/dev/cpu:实际上,这是一个目录,其中包含系统上每个 CPU 的附加目录。在这些目录中有一个
cpuid
文件,用于查询有关 CPU 的信息:
[nfs]# ls -la /dev/cpu/0/cpuid
crw-------. 1 root root 203, 0 May 10 06:16 /dev/cpu/0/cpuid
- /dev/md:这是另一个目录,其中包含指向实际 RAID 设备的用户友好名称的符号链接。如果我们使用
ls
,我们可以看到该系统上可用的 RAID 设备:
[nfs]# ls -la /dev/md/
total 0
drwxr-xr-x. 2 root root 80 May 10 06:16 .
drwxr-xr-x. 20 root root 3180 May 10 06:16 ..
lrwxrwxrwx. 1 root root 8 May 10 06:16 boot -> ../md127
lrwxrwxrwx. 1 root root 8 May 10 06:16 pv00 -> ../md126
- /dev/random和/dev/urandom:这两个设备用于生成随机数据。
/dev/random
和/dev/urandom
设备都会从内核的熵池中提取随机数据。这两者之间的一个区别是,当系统的熵计数较低时,/dev/random
设备将等待直到重新添加足够的熵。
正如我们之前学到的,/dev/
目录中有一些非常有用的文件和目录。然而,回到我们最初的问题,我们已经确定/dev/sdb
存在,并且有两个分区/dev/sdb1
和/dev/sdb2
。
然而,我们还没有确定/dev/sdb
最初是否是两个当前处于降级状态的 RAID 设备的一部分。为了做到这一点,我们可以利用dmesg
设施。
使用 dmesg 查看设备消息
dmesg
命令是一个用于排除硬件问题的好命令。当系统初始启动时,内核将识别该系统可用的各种硬件设备。
当内核识别这些设备时,信息被写入内核的环形缓冲区。这个环形缓冲区本质上是内核的内部日志。dmesg
命令可以用来打印这个环形缓冲区。
以下是dmesg
命令的一个示例输出;在这个示例中,我们将使用head
命令将输出缩短到只有前 15 行:
[nfs]# dmesg | head -15
[ 0.000000] Initializing cgroup subsys cpuset
[ 0.000000] Initializing cgroup subsys cpu
[ 0.000000] Initializing cgroup subsys cpuacct
[ 0.000000] Linux version 3.10.0-229.1.2.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) ) #1 SMP Fri Mar 27 03:04:26 UTC 2015
[ 0.000000] Command line: BOOT_IMAGE=/vmlinuz-3.10.0-229.1.2.el7.x86_64 root=/dev/mapper/md0-root ro rd.lvm.lv=md0/swap crashkernel=auto rd.md.uuid=bec13d99:42674929:76663813:f748e7cb rd.lvm.lv=md0/root rd.md.uuid=7adf0323:b0962394:387e6cd0:b2914469 rhgb quiet LANG=en_US.UTF-8 systemd.debug
[ 0.000000] e820: BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000001ffeffff] usable
[ 0.000000] BIOS-e820: [mem 0x000000001fff0000-0x000000001fffffff] ACPI data
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[ 0.000000] NX (Execute Disable) protection: active
[ 0.000000] SMBIOS 2.5 present.
[ 0.000000] DMI: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
我们将输出限制为只有 15 行,是因为dmesg
命令会输出大量的数据。换个角度来看,我们可以再次运行命令,但这次将输出发送到wc -l
,它将计算打印的行数:
[nfs]# dmesg | wc -l
597
正如我们所看到的,dmesg
命令返回了597
行。阅读内核环形缓冲区的所有 597 行并不是一个快速的过程。
由于我们的目标是找出关于/dev/sdb
的信息,我们可以再次运行dmesg
命令,这次使用grep
命令来过滤输出到/dev/sdb
相关的信息:
[nfs]# dmesg | grep -C 5 sdb
[ 2.176800] scsi 3:0:0:0: CD-ROM VBOX CD-ROM 1.0 PQ: 0 ANSI: 5
[ 2.194908] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
[ 2.194951] sd 0:0:0:0: [sda] Write Protect is off
[ 2.194953] sd 0:0:0:0: [sda] Mode Sense: 00 3a 00 00
[ 2.194965] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 2.196250] sd 1:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
[ 2.196279] sd 1:0:0:0: [sdb] Write Protect is off
[ 2.196281] sd 1:0:0:0: [sdb] Mode Sense: 00 3a 00 00
[ 2.196294] sd 1:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 2.197471] sda: sda1 sda2
[ 2.197700] sd 0:0:0:0: [sda] Attached SCSI disk
[ 2.198139] sdb: sdb1 sdb2
[ 2.198319] sd 1:0:0:0: [sdb] Attached SCSI disk
[ 2.200851] sr 3:0:0:0: [sr0] scsi3-mmc drive: 32x/32x xa/form2 tray
[ 2.200856] cdrom: Uniform CD-ROM driver Revision: 3.20
[ 2.200980] sr 3:0:0:0: Attached scsi CD-ROM sr0
[ 2.366634] md: bind<sda1>
[ 2.370652] md: raid1 personality registered for level 1
[ 2.370820] md/raid1:md127: active with 1 out of 2 mirrors
[ 2.371797] created bitmap (1 pages) for device md127
[ 2.372181] md127: bitmap initialized from disk: read 1 pages, set 0 of 8 bits
[ 2.373915] md127: detected capacity change from 0 to 524222464
[ 2.374767] md127: unknown partition table
[ 2.376065] md: bind<sdb2>
[ 2.382976] md: bind<sda2>
[ 2.385094] md: kicking non-fresh sdb2 from array!
[ 2.385102] md: unbind<sdb2>
[ 2.385105] md: export_rdev(sdb2)
[ 2.387559] md/raid1:md126: active with 1 out of 2 mirrors
[ 2.387874] created bitmap (1 pages) for device md126
[ 2.388339] md126: bitmap initialized from disk: read 1 pages, set 19 of 121 bits
[ 2.390324] md126: detected capacity change from 0 to 8060403712
[ 2.391344] md126: unknown partition table
在执行前面的示例时,使用了-C
(上下文)标志来告诉grep
将五行上下文包含在输出中。通常情况下,当grep
不带标志运行时,只会打印包含搜索字符串(本例中为"sdb
")的行。将上下文标志设置为五,grep
命令将打印包含搜索字符串的每一行之前和之后的 5 行。
这种使用grep
的方法使我们不仅能看到包含字符串sdb
的行,还能看到之前和之后的行,这些行可能包含额外的信息。
现在我们有了这些额外的信息,让我们来分解一下,以更好地理解它告诉我们的内容:
[ 2.176800] scsi 3:0:0:0: CD-ROM VBOX CD-ROM 1.0 PQ: 0 ANSI: 5
[ 2.194908] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
[ 2.194951] sd 0:0:0:0: [sda] Write Protect is off
[ 2.194953] sd 0:0:0:0: [sda] Mode Sense: 00 3a 00 00
[ 2.194965] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 2.196250] sd 1:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
[ 2.196279] sd 1:0:0:0: [sdb] Write Protect is off
[ 2.196281] sd 1:0:0:0: [sdb] Mode Sense: 00 3a 00 00
[ 2.196294] sd 1:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[ 2.197471] sda: sda1 sda2
[ 2.197700] sd 0:0:0:0: [sda] Attached SCSI disk
[ 2.198139] sdb: sdb1 sdb2
[ 2.198319] sd 1:0:0:0: [sdb] Attached SCSI disk
前面的信息似乎是关于/dev/sdb
的标准信息。我们可以从这些消息中看到关于/dev/sda
和/dev/sdb
的一些基本信息。
从前面的信息中我们可以看到一个有用的东西是这些驱动器的大小:
[ 2.194908] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
[ 2.196250] sd 1:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
我们可以看到每个驱动器的大小为8.58
GB。虽然这些信息一般来说是有用的,但对于我们目前的情况并不适用。然而,有用的是前面代码片段的最后四行:
[ 2.197471] sda: sda1 sda2
[ 2.197700] sd 0:0:0:0: [sda] Attached SCSI disk
[ 2.198139] sdb: sdb1 sdb2
[ 2.198319] sd 1:0:0:0: [sdb] Attached SCSI disk
这最后的四行显示了/dev/sda
和/dev/sdb
上的可用分区,以及一条消息说明每个磁盘都已经Attached
。
这些信息非常有用,因为它在最基本的层面上告诉我们这两个驱动器正在工作。这对于/dev/sdb
来说是一个问题,因为我们怀疑 RAID 系统已经将其移出了服务。
到目前为止,dmesg
命令已经给了我们一些有用的信息;让我们继续查看数据,以更好地理解这些磁盘。
[ 2.200851] sr 3:0:0:0: [sr0] scsi3-mmc drive: 32x/32x xa/form2 tray
[ 2.200856] cdrom: Uniform CD-ROM driver Revision: 3.20
[ 2.200980] sr 3:0:0:0: Attached scsi CD-ROM sr0
前面的三行在我们排除 CD-ROM 设备问题时可能有用。然而,对于我们的磁盘问题,它们并不有用,只是因为grep
的上下文设置为 5 而包含在内。
然而,以下的行将会告诉我们关于我们的磁盘驱动器的很多信息:
[ 2.366634] md: bind<sda1>
[ 2.370652] md: raid1 personality registered for level 1
[ 2.370820] md/raid1:md127: active with 1 out of 2 mirrors
[ 2.371797] created bitmap (1 pages) for device md127
[ 2.372181] md127: bitmap initialized from disk: read 1 pages, set 0 of 8 bits
[ 2.373915] md127: detected capacity change from 0 to 524222464
[ 2.374767] md127: unknown partition table
[ 2.376065] md: bind<sdb2>
[ 2.382976] md: bind<sda2>
[ 2.385094] md: kicking non-fresh sdb2 from array!
[ 2.385102] md: unbind<sdb2>
[ 2.385105] md: export_rdev(sdb2)
[ 2.387559] md/raid1:md126: active with 1 out of 2 mirrors
[ 2.387874] created bitmap (1 pages) for device md126
[ 2.388339] md126: bitmap initialized from disk: read 1 pages, set 19 of 121 bits
[ 2.390324] md126: detected capacity change from 0 to 8060403712
[ 2.391344] md126: unknown partition table
dmesg 输出的最后一部分告诉了我们关于 RAID 设备和/dev/sdb
的很多信息。由于数据量很大,我们需要将其分解以真正理解其中的内容:
The first few lines show use information about /dev/md127.
[ 2.366634] md: bind<sda1>
[ 2.370652] md: raid1 personality registered for level 1
[ 2.370820] md/raid1:md127: active with 1 out of 2 mirrors
[ 2.371797] created bitmap (1 pages) for device md127
[ 2.372181] md127: bitmap initialized from disk: read 1 pages, set 0 of 8 bits
[ 2.373915] md127: detected capacity change from 0 to 524222464
[ 2.374767] md127: unknown partition table
/dev/md126; however, there is a bit more information included with those messages:
[ 2.376065] md: bind<sdb2>
[ 2.382976] md: bind<sda2>
[ 2.385094] md: kicking non-fresh sdb2 from array!
[ 2.385102] md: unbind<sdb2>
[ 2.385105] md: export_rdev(sdb2)
[ 2.387559] md/raid1:md126: active with 1 out of 2 mirrors
[ 2.387874] created bitmap (1 pages) for device md126
[ 2.388339] md126: bitmap initialized from disk: read 1 pages, set 19 of 121 bits
[ 2.390324] md126: detected capacity change from 0 to 8060403712
[ 2.391344] md126: unknown partition table
前面的消息看起来与/dev/md127
的消息非常相似;然而,有几行消息在/dev/md127
的消息中没有出现:
[ 2.376065] md: bind<sdb2>
[ 2.382976] md: bind<sda2>
[ 2.385094] md: kicking non-fresh sdb2 from array!
[ 2.385102] md: unbind<sdb2>
如果我们看这些消息,我们可以看到/dev/md126
尝试在 RAID 阵列中使用/dev/sdb2
;然而,它发现该驱动器不是新的。非新的消息很有趣,因为它可能解释了为什么/dev/sdb
没有被包含到 RAID 设备中。
总结 dmesg 提供的信息
在 RAID 集中,每个磁盘都维护每个写请求的事件计数。RAID 使用这个事件计数来确保每个磁盘接收了适当数量的写请求。这使得 RAID 能够验证整个 RAID 的一致性。
当 RAID 重新启动时,RAID 管理器将检查每个磁盘的事件计数,并确保它们是一致的。
从前面的消息中,看起来/dev/sda2
的事件计数可能比/dev/sdb2
高。这表明/dev/sda1
上发生了一些写操作,而/dev/sdb2
上从未发生过。这对于镜像阵列来说是异常的,也表明了/dev/sdb2
存在问题。
当设备名称发生变化时,我们如何检查事件计数是否不同?使用mdadm
命令,我们可以显示每个磁盘设备的事件计数。
使用 mdadm 来检查超级块
为了查看事件计数,我们将使用mdadm
命令和--examine
标志来检查磁盘设备:
[nfs]# mdadm --examine /dev/sda1
/dev/sda1:
Magic : a92b4efc
Version : 1.0
Feature Map : 0x1
Array UUID : 7adf0323:b0962394:387e6cd0:b2914469
Name : localhost:boot
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Raid Devices : 2
Avail Dev Size : 1023968 (500.07 MiB 524.27 MB)
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 1023872 (500.02 MiB 524.22 MB)
Super Offset : 1023984 sectors
Unused Space : before=0 sectors, after=96 sectors
State : clean
Device UUID : 92d97c32:1f53f59a:14a7deea:34ec8c7c
Internal Bitmap : -16 sectors from superblock
Update Time : Mon May 11 04:08:10 2015
Bad Block Log : 512 entries available at offset -8 sectors
Checksum : bd8c1d5b - correct
Events : 60
Device Role : Active device 0
Array State : A. ('A' == active, '.' == missing, 'R' == replacing)
--examine
标志与--detail
非常相似,不同之处在于--detail
用于打印 RAID 设备的详细信息。--examine
用于打印组成 RAID 的单个磁盘的 RAID 详细信息。--examine
打印的详细信息实际上来自磁盘上的超级块详细信息。
当 Linux RAID 利用磁盘作为 RAID 设备的一部分时,RAID 系统会在磁盘上保留一些空间用于超级块。这个超级块简单地用于存储关于磁盘和 RAID 的元数据。
在前面的命令中,我们只是打印了/dev/sda1
的 RAID 超级块信息。为了更好地理解 RAID 超级块,让我们看一下--examine
标志提供的详细信息:
/dev/sda1:
Magic : a92b4efc
Version : 1.0
Feature Map : 0x1
Array UUID : 7adf0323:b0962394:387e6cd0:b2914469
Name : localhost:boot
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Raid Devices : 2
这个输出的第一部分提供了相当多有用的信息。例如,魔术数字被用作超级块头。这是一个用来指示超级块开始的值。
另一个有用的信息是Array UUID
。这是这个磁盘所属的 RAID 的唯一标识符。如果我们打印md127
的 RAID 的详细信息,我们可以看到/dev/sda1
的 Array UUID 和md127
的 UUID 是匹配的:
[nfs]# mdadm --detail /dev/md127 | grep UUID
UUID : 7adf0323:b0962394:387e6cd0:b2914469
当 Linux RAID 利用磁盘作为 RAID 设备的一部分时,RAID 系统会在磁盘上保留一些空间用于超级块。这个超级块简单地用于存储关于磁盘和 RAID 的元数据。
底部的三行Creation Time
、RAID Level
和RAID Devices
在与--detail
输出一起使用时也非常有用。
这第二段信息对于确定磁盘设备的信息非常有用:
Avail Dev Size : 1023968 (500.07 MiB 524.27 MB)
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 1023872 (500.02 MiB 524.22 MB)
Super Offset : 1023984 sectors
Unused Space : before=0 sectors, after=96 sectors
State : clean
Device UUID : 92d97c32:1f53f59a:14a7deea:34ec8c7c
State of the RAID. This state matches the state we see from the --detail output of /dev/md127.
[nfs]# mdadm --detail /dev/md127 | grep State
State : clean, degraded
--examine
输出的下一部分信息对我们的问题非常有用:
Internal Bitmap : -16 sectors from superblock
Update Time : Mon May 11 04:08:10 2015
Bad Block Log : 512 entries available at offset -8 sectors
Checksum : bd8c1d5b - correct
Events : 60
Device Role : Active device 0
Array State : A. ('A' == active, '.' == missing, 'R' == replacing)
在这一部分中,我们可以看到Events
信息,显示了这个磁盘上的当前事件计数值。我们还可以看到/dev/sda1
的Array State
值。A
的值表示从/dev/sda1
的角度来看,它的镜像伙伴丢失了。
当我们检查/dev/sdb1
下超级块的详细信息时,我们会看到Array State
和Events
值的一个有趣的差异:
[nfs]# mdadm --examine /dev/sdb1
/dev/sdb1:
Magic : a92b4efc
Version : 1.0
Feature Map : 0x1
Array UUID : 7adf0323:b0962394:387e6cd0:b2914469
Name : localhost:boot
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Raid Devices : 2
Avail Dev Size : 1023968 (500.07 MiB 524.27 MB)
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 1023872 (500.02 MiB 524.22 MB)
Super Offset : 1023984 sectors
Unused Space : before=0 sectors, after=96 sectors
State : clean
Device UUID : 5a9bb172:13102af9:81d761fb:56d83bdd
Internal Bitmap : -16 sectors from superblock
Update Time : Mon May 4 21:09:30 2015
Bad Block Log : 512 entries available at offset -8 sectors
Checksum : cd226d7b - correct
Events : 48
Device Role : Active device 1
Array State : AA ('A' == active, '.' == missing, 'R' == replacing)
从结果来看,我们已经回答了关于/dev/sdb1
的很多问题。
我们最初的问题是/dev/sdb1
是否是 RAID 的一部分。从这个设备具有 RAID 超级块并且可以通过mdadm
打印信息的事实来看,我们可以肯定是。
Array UUID : 7adf0323:b0962394:387e6cd0:b2914469
通过查看Array UUID
,我们还可以确定这个设备是否是/dev/md127
的一部分,正如我们所怀疑的那样:
[nfs]# mdadm --detail /dev/md127 | grep UUID
UUID : 7adf0323:b0962394:387e6cd0:b2914469
看起来/dev/sdb1
在某个时候是/dev/md127
的一部分。
我们需要回答的最后一个问题是/dev/sda1
和/dev/sdb1
之间的Events
值是否不同。从/dev/sda1
的--examine
信息中,我们可以看到事件计数设置为 60。在前面的代码中,从/dev/sdb1
的--examine
结果中,我们可以看到事件计数要低得多——48:
Events : 48
鉴于这种差异,我们可以确定/dev/sdb1
比/dev/sda1
落后 12 个事件。这是一个非常重要的差异,也是 MD 拒绝将/dev/sdb1
添加到 RAID 数组的一个合理原因。
有趣的是,如果我们查看/dev/sdb1
的Array State
,我们可以看到它仍然认为自己是/dev/md127
数组中的一个活动磁盘:
Array State : AA ('A' == active, '.' == missing, 'R' == replacing)
这是因为由于设备不再是 RAID 的一部分,它不会被更新为当前状态。我们也可以从更新时间中看到这一点:
Update Time : Mon May 4 21:09:30 2015
/dev/sda1
的“更新时间”要新得多;因此,应该比磁盘/dev/sdb1
更可信。
检查/dev/sdb2
现在我们知道了/dev/sdb1
未被添加到/dev/md127
的原因,我们应该确定是否对/dev/sdb2
和/dev/md126
也是如此。
由于我们已经知道/dev/sda2
是健康的并且是/dev/md126
数组的一部分,我们将专注于捕获其“事件”值:
[nfs]# mdadm --examine /dev/sda2 | grep Events
Events : 7517
与/dev/sda1
相比,/dev/sda2
的事件计数相当高。从中我们可以确定/dev/md126
可能是一个非常活跃的 RAID 设备。
现在我们知道了事件计数,让我们来看看/dev/sdb2
的详细信息:
[nfs]# mdadm --examine /dev/sdb2
/dev/sdb2:
Magic : a92b4efc
Version : 1.2
Feature Map : 0x1
Array UUID : bec13d99:42674929:76663813:f748e7cb
Name : localhost:pv00
Creation Time : Wed Apr 15 09:39:19 2015
Raid Level : raid1
Raid Devices : 2
Avail Dev Size : 15742976 (7.51 GiB 8.06 GB)
Array Size : 7871488 (7.51 GiB 8.06 GB)
Data Offset : 8192 sectors
Super Offset : 8 sectors
Unused Space : before=8104 sectors, after=0 sectors
State : clean
Device UUID : 01db1f5f:e8176cad:8ce68d51:deff57f8
Internal Bitmap : 8 sectors from superblock
Update Time : Mon May 4 21:10:31 2015
Bad Block Log : 512 entries available at offset 72 sectors
Checksum : 98a8ace8 - correct
Events : 541
Device Role : Active device 1
Array State : AA ('A' == active, '.' == missing, 'R' == replacing)
同样,从我们能够从/dev/sdb2
打印超级块信息的事实中,我们已经确定这个设备实际上是 RAID 的一部分:
Array UUID : bec13d99:42674929:76663813:f748e7cb
如果我们将/dev/sdb2
的“数组 UUID”与/dev/md126
的UUID
进行比较,我们还将看到它实际上是该 RAID 数组的一部分:
[nfs]# mdadm --detail /dev/md126 | grep UUID
UUID : bec13d99:42674929:76663813:f748e7cb
这回答了我们关于/dev/sdb2
是否是md126
RAID 的一部分的问题。如果我们查看/dev/sdb2
的事件计数,我们也可以回答为什么它目前不是该 RAID 的一部分的问题:
Events : 541
看起来这个设备错过了发送到md126
RAID 的写事件,因为/dev/sda2
的“事件”计数为 7517,而/dev/sdb2
的“事件”计数为 541。
到目前为止我们学到的内容
到目前为止,我们已经采取了一些故障排除步骤,收集了一些关键数据。让我们走一遍我们学到的东西,以及我们可以从这些发现中推断出什么:
- 在我们的系统上,我们有两个 RAID 设备。
使用mdadm
命令和/proc/mdstat
的内容,我们能够确定该系统有两个 RAID 设备—/dev/md126
和/dev/md127
。
- 两个 RAID 设备都是 RAID 1,缺少一个镜像设备。
通过mdadm
命令和dmesg
输出,我们能够确定两个 RAID 设备都设置为 RAID 1 设备。此外,我们还能够看到两个 RAID 设备都缺少一个磁盘;缺少的设备都是来自/dev/sdb
硬盘的分区。
/dev/sdb1
和/dev/sdb2
的事件计数不匹配。
通过mdadm
命令,我们能够检查/dev/sdb1
和/dev/sdb2
设备的superblock
详细信息。在此期间,我们能够看到这些设备的事件计数与/dev/sda
上的活动分区不匹配。
因此,RAID 不会将/dev/sdb
设备重新添加到它们各自的 RAID 数组中。
- 磁盘
/dev/sdb
似乎是正常的。
虽然 RAID 没有将/dev/sdb1
或/dev/sdb2
添加到各自的 RAID 数组中,但这并不意味着设备/dev/sdb
有故障。
从dmesg
中的消息中,我们没有看到/dev/sdb
设备本身的任何错误。我们还能够使用mdadm
来检查这些驱动器上的分区。从到目前为止我们所做的一切来看,这些驱动器似乎是正常的。
重新将驱动器添加到数组
/dev/sdb
磁盘似乎是正常的,除了事件计数的差异外,我们看不到 RAID 拒绝设备的任何原因。我们的下一步将是尝试将已移除的设备重新添加到它们的 RAID 数组中。
我们将首先尝试这样做的第一个 RAID 是/dev/md127
:
[nfs]# mdadm --detail /dev/md127
/dev/md127:
Version : 1.0
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 511936 (500.02 MiB 524.22 MB)
Raid Devices : 2
Total Devices : 1
Persistence : Superblock is persistent
Intent Bitmap : Internal
Update Time : Mon May 11 04:08:10 2015
State : clean, degraded
Active Devices : 1
Working Devices : 1
Failed Devices : 0
Spare Devices : 0
Name : localhost:boot
UUID : 7adf0323:b0962394:387e6cd0:b2914469
Events : 60
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
2 0 0 2 removed
重新添加驱动器的最简单方法就是简单地使用mdadm
的-a
(添加)标志。
[nfs]# mdadm /dev/md127 -a /dev/sdb1
mdadm: re-added /dev/sdb1
上述命令将告诉mdadm
将设备/dev/sdb1
添加到 RAID 设备/dev/md127
中。由于/dev/sdb1
已经是 RAID 数组的一部分,MD 服务只是重新添加磁盘并同步来自/dev/sda1
的丢失事件。
如果我们使用--detail
标志查看 RAID 的详细信息,我们就可以看到这一点:
[nfs]# mdadm --detail /dev/md127
/dev/md127:
Version : 1.0
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 511936 (500.02 MiB 524.22 MB)
Raid Devices : 2
Total Devices : 2
Persistence : Superblock is persistent
Intent Bitmap : Internal
Update Time : Mon May 11 16:47:32 2015
State : clean, degraded, recovering
Active Devices : 1
Working Devices : 2
Failed Devices : 0
Spare Devices : 1
Rebuild Status : 50% complete
Name : localhost:boot
UUID : 7adf0323:b0962394:387e6cd0:b2914469
Events : 66
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
1 8 17 1 spare rebuilding /dev/sdb1
从前面的输出中,我们可以看到与之前示例的一些不同之处。一个非常重要的区别是重建状态
:
Rebuild Status : 50% complete
通过mdadm --detail
,我们可以看到驱动器重新同步的完成状态。如果在此过程中有任何错误,我们也将能够看到。如果我们看底部的三行,我们还可以看到哪些设备是活动的,哪些正在重建。
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
1 8 17 1 spare rebuilding /dev/sdb1
几秒钟后,如果我们再次运行mdadm --detail
,我们应该看到 RAID 设备已经重新同步:
[nfs]# mdadm --detail /dev/md127
/dev/md127:
Version : 1.0
Creation Time : Wed Apr 15 09:39:22 2015
Raid Level : raid1
Array Size : 511936 (500.02 MiB 524.22 MB)
Used Dev Size : 511936 (500.02 MiB 524.22 MB)
Raid Devices : 2
Total Devices : 2
Persistence : Superblock is persistent
Intent Bitmap : Internal
Update Time : Mon May 11 16:47:32 2015
State : clean
Active Devices : 2
Working Devices : 2
Failed Devices : 0
Spare Devices : 0
Name : localhost:boot
UUID : 7adf0323:b0962394:387e6cd0:b2914469
Events : 69
Number Major Minor RaidDevice State
0 8 1 0 active sync /dev/sda1
1 8 17 1 active sync /dev/sdb1
现在我们可以看到两个驱动器都列为active sync
状态,而 RAID 的State
只是clean
。
上述输出是一个正常的 RAID 1 设备应该看起来的样子。在这一点上,我们可以认为/dev/md127
的问题已经解决。
添加新的磁盘设备
有时你会发现自己处于一个情况,你的磁盘驱动实际上是有故障的,实际的物理硬件必须被替换。在这种情况下,一旦重新创建分区/dev/sdb1
和/dev/sdb2
,设备可以简单地使用与之前相同的步骤添加到 RAID 中。
当执行命令mdadm <raid device> -a <disk device>
时,mdadm
首先检查磁盘设备是否曾经是 RAID 的一部分。
它通过读取磁盘设备上的超级块信息来执行此操作。如果设备以前曾是 RAID 的一部分,它会简单地重新添加并开始重建以重新同步驱动器。
如果磁盘设备以前从未参与过 RAID,它将被添加为备用设备,如果 RAID 处于降级状态,备用设备将被用来使 RAID 恢复到干净状态。
当磁盘没有被清洁添加时
在以前的工作环境中,当我们更换硬盘时,硬盘总是在用于替换生产环境中故障硬盘之前进行质量测试。通常,这种质量测试涉及创建分区并将这些分区添加到现有的 RAID 中。
因为这些设备已经在它们上面有一个 RAID 超级块,mdadm
会拒绝将这些设备添加到 RAID 中。可以使用mdadm
命令清除现有的 RAID超级块
:
[nfs]# mdadm --zero-superblock /dev/sdb2
上述命令将告诉mdadm
从指定的磁盘中删除 RAID超级块
信息—在本例中是/dev/sdb2
:
[nfs]# mdadm --examine /dev/sdb2
mdadm: No md superblock detected on /dev/sdb2.
使用--examine
,我们可以看到现在设备上没有超级块。
--zero-superblock
标志应谨慎使用,只有当设备数据不再需要时才使用。一旦删除了这些超级块信息,RAID 将把这个磁盘视为空白磁盘,在任何重新同步过程中,现有数据将被覆盖。
一旦超级块被移除,同样的步骤可以执行以将其添加到 RAID 阵列中:
[nfs]# mdadm /dev/md126 -a /dev/sdb2
mdadm: added /dev/sdb2
观察重建状态的另一种方法
之前我们使用mdadm --detail
来显示md127
的重建状态。另一种查看这些信息的方法是通过/proc/mdstat
:
[nfs]# cat /proc/mdstat
Personalities : [raid1]
md126 : active raid1 sdb2[2] sda2[0]
7871488 blocks super 1.2 [2/1] [U_]
[>....................] recovery = 0.0% (1984/7871488) finish=65.5min speed=1984K/sec
bitmap: 1/1 pages [4KB], 65536KB chunk
md127 : active raid1 sdb1[1] sda1[0]
511936 blocks super 1.0 [2/2] [UU]
bitmap: 0/1 pages [0KB], 65536KB chunk
unused devices: <none>
过一会儿,RAID 将完成重新同步;现在,两个 RAID 阵列都处于健康状态:
[nfs]# cat /proc/mdstat
Personalities : [raid1]
md126 : active raid1 sdb2[2] sda2[0]
7871488 blocks super 1.2 [2/2] [UU]
bitmap: 0/1 pages [0KB], 65536KB chunk
md127 : active raid1 sdb1[1] sda1[0]
511936 blocks super 1.0 [2/2] [UU]
bitmap: 0/1 pages [0KB], 65536KB chunk
unused devices: <none>
总结
在前一章中,第七章,文件系统错误和恢复,我们注意到在/var/log/messages
日志文件中出现了一个简单的 RAID 故障消息。在本章中,我们使用了数据收集器
方法来调查故障消息的原因。
在使用 RAID 管理命令mdadm
进行调查后,我们发现了几个处于降级状态的 RAID 设备。使用dmesg
,我们能够确定哪些硬盘设备受到影响,以及这些硬盘在某个时候被移出了服务。我们还发现硬盘的事件计数不匹配,阻止了硬盘的自动重新添加。
我们通过dmesg
验证了设备没有物理故障,并选择将它们重新添加到 RAID 阵列中。
本章重点介绍了 RAID 和磁盘故障,但/var/log/messages
和dmesg
都可以用于排除其他设备故障。然而,对于除硬盘以外的设备,解决方案通常是简单的更换。当然,像大多数事情一样,这取决于所经历的故障类型。
在下一章中,我们将展示如何排除自定义用户应用程序的故障,并使用系统工具进行一些高级故障排除。
第九章:使用系统工具来排除应用程序问题
在上一章中,我们讨论了故障排除硬件问题。具体来说,您学会了当硬盘从 RAID 中移除并且无法读取时该怎么做。
在本章中,我们将回到排除应用程序问题,但与之前的例子不同,我们将不再排除像 WordPress 这样的流行开源应用程序。在本章中,我们将专注于一个自定义应用程序,这将比一个知名应用程序更难排除故障。
开源与自制应用程序
流行的开源项目通常有在线社区或错误/问题跟踪器。正如我们在第三章中所经历的,故障排除 Web 应用程序,这些资源对于排除应用程序问题非常有用。通常,问题已经在这些社区中报告或询问过,其中大多数帖子也包含了问题的解决方案。
这些解决方案被发布在互联网上的开放论坛上;应用程序的任何错误也可以直接在谷歌上搜索。大多数情况下,搜索结果会显示多个可能的答案。当一个流行的开源应用程序的错误在谷歌上产生零搜索结果时,这是一个非常罕见的情况。
然而,对于自定义应用程序,应用程序错误可能并不总是可以通过快速的谷歌搜索来解决。有时,应用程序会提供通用错误,比如权限被拒绝或文件未找到。然而,有时候它们不会产生错误,或者产生特定于应用程序的错误,比如我们今天将要处理的问题。
面对开源工具中不明确的错误时,您总是可以在某个在线网站上寻求帮助。然而,对于自定义应用程序,您可能并不总是有机会询问开发人员错误的含义。
有时,系统管理员需要在开发人员几乎没有帮助的情况下修复应用程序。
当出现这种情况时,管理员手头有很多工具可供使用。在今天的章节中,我们将探索其中一些工具,当然,也会排除自定义应用程序的故障。
当应用程序无法启动时
对于本章的问题,我们将像处理大多数其他问题一样开始,但今天,我们不是收到警报或电话,而是被另一位系统管理员问了一个问题。
系统管理员正在尝试在博客 Web 服务器上启动一个应用程序。当他们尝试启动应用程序时,它似乎正在启动;然而,在最后,它只是打印出一个错误消息并退出。
对于这种情况,我们的第一个反应当然是故障排除过程中的第一步——复制它。
另一位系统管理员告诉我们,他们通过执行以下步骤来启动应用程序:
-
以
vagrant
用户登录服务器 -
移动到目录
/opt/myapp
-
运行脚本
start.sh
在进一步进行之前,让我们尝试同样的步骤:
$ whoami
vagrant
$ cd /opt/myapp/
$ ls -la
total 8
drwxr-xr-x. 5 vagrant vagrant 69 May 18 03:11 .
drwxr-xr-x. 4 root root 50 May 18 00:48 ..
drwxrwxr-x. 2 vagrant vagrant 24 May 18 01:14 bin
drwxrwxr-x. 2 vagrant vagrant 23 May 18 00:51 conf
drwxrwxr-x. 2 vagrant vagrant 6 May 18 00:50 logs
-rwxr-xr-x. 1 vagrant vagrant 101 May 18 03:11 start.sh
$ ./start.sh
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting service: [Failed]
在前面的步骤中,我们按照之前的管理员的步骤进行,并得到了相同的结果。应用程序似乎未能启动。
在前面的示例中,使用whoami
命令显示我们以vagrant
用户登录。在处理应用程序时,这个命令非常方便,因为它可以用来确保正确的系统用户执行启动过程。
我们可以从前面的启动尝试中看到,应用程序未能启动,并显示以下消息:
Starting service: [Failed]
然而,我们需要知道为什么它无法启动,以及进程是否真的失败了
回答关于进程是否真正失败的问题实际上非常简单。为了做到这一点,我们可以简单地检查应用程序的退出代码,方法是在执行start.sh
脚本后打印$?
变量,如下所示:
$ echo $?
1
退出代码
在 Linux 和 Unix 系统上,程序在终止时有能力向其父进程传递一个值。这个值被称为退出代码。正在终止或“退出”的程序使用退出代码来告诉调用它的进程该程序是成功还是失败。
对于 POSIX 系统(如 Red Hat Enterprise Linux),标准约定是程序成功退出时以 0 状态代码退出,失败时以非零状态代码退出。由于我们前面的示例以状态代码 1 退出,这意味着应用程序以失败退出。
为了更好地理解退出代码,让我们编写一个快速的脚本来执行一个成功的任务:
$ cat /var/tmp/exitcodes.sh
#!/bin/bash
touch /var/tmp/file.txt
这个快速的小 shell 脚本执行一个任务,它在文件/var/tmp/file.txt
上运行touch
命令。如果该文件存在,touch 命令只会更新该文件的访问时间。如果文件不存在,touch 命令将创建它。
由于/var/tmp
是一个具有开放权限的临时目录,这个脚本在 vagrant 用户执行时应该是成功的:
$ /var/tmp/exitcodes.sh
执行命令后,我们可以通过使用 BASH 特殊变量$?
来查看退出代码。这个变量是 BASH shell 中的一个特殊变量,只能用于读取上一个程序的退出代码。这个变量是 BASH shell 中的几个特殊变量之一,只能读取,不能写入。
要查看我们脚本的退出状态,我们可以将$?
的值echo
到屏幕上:
$ echo $?
0
看起来这个脚本返回了0
退出状态。这意味着脚本成功执行,很可能更新或创建了文件/var/tmp/file.txt
。我们可以通过对文件本身执行ls -la
来验证文件是否已更新:
$ ls -la /var/tmp/file.txt
-rw-rw-r--. 1 vagrant vagrant 0 May 25 14:25 /var/tmp/file.txt
从ls
命令的输出中,看起来文件最近已更新或创建。
前面的示例展示了脚本成功时会发生什么,但是当脚本失败时会发生什么呢?通过前面脚本的修改版本,我们可以很容易地看到脚本失败时会发生什么:
$ cat /var/tmp/exitcodes.sh
#!/bin/bash
touch /some/directory/that/doesnt/exist/file.txt
修改后的版本将尝试在不存在的目录中创建文件。该脚本将因此失败并以指示失败的退出代码退出:
$ /var/tmp/exitcodes.sh
touch: cannot touch '/some/directory/that/doesnt/exist/file.txt': No such file or directory
从脚本的输出中,我们可以看到touch
命令失败了,但是退出代码呢?
$ echo $?
1
退出代码还显示了脚本失败了。退出代码的标准是成功为0
,任何非零值都表示失败。一般来说,你会看到0
或1
的退出代码。然而,一些应用程序会使用其他退出代码来指示特定的失败:
$ somecommand
-bash: somecommand: command not found
$ echo $?
127
例如,如果我们从 BASH shell 执行一个不存在的命令,提供的退出代码将是127
。这个退出代码是一个用来指示命令未找到的约定。以下是用于特定目的的退出代码列表:
-
0
:成功 -
1
:发生了一般性失败 -
2
:对 shell 内置的误用 -
126
:无法执行调用的命令 -
127
:命令未找到 -
128
:传递给exit
命令的无效参数 -
130
:使用Ctrl + C键停止命令 -
255
:提供的退出代码超出了0 - 255
范围
这个列表是退出代码的一个很好的通用指南。然而,由于每个应用程序都可以提供自己的退出代码,你可能会发现一个命令或应用程序提供的退出代码不在上述列表中。对于开源应用程序,你通常可以查找退出代码的含义。然而,对于自定义应用程序,你可能有也可能没有查找退出代码含义的能力。
脚本失败了,还是应用程序失败了?
关于 shell 脚本和退出码的一个有趣的事情是,当执行 shell 脚本时,该脚本的退出码将是最后一个执行的命令的退出码。
为了更好地理解这一点,我们可以再次修改我们的测试脚本:
$ cat /var/tmp/exitcodes.sh
#!/bin/bash
touch /some/directory/that/doesnt/exist/file.txt
echo "It works"
前面的命令应该产生一个有趣的结果。touch
命令将失败;然而,echo 命令将成功。
这意味着当执行时,即使touch
命令失败,echo
命令也成功,因此命令行的退出码应该显示脚本成功:
$ /var/tmp/exitcodes.sh
touch: cannot touch '/some/directory/that/doesnt/exist/file.txt': No such file or directory
It works
$ echo $?
0
前面的命令是一个不优雅处理错误的脚本的例子。如果我们依赖这个脚本仅通过退出码来提供正确的执行状态,我们将得到错误的结果。
对于系统管理员来说,对于未知脚本持有一些怀疑态度总是好的。我发现许多情况(并且自己写了一些)脚本没有错误检查。因此,我们应该执行的第一步是验证退出码 1 是否确实来自正在启动的应用程序。
为了做到这一点,我们需要阅读启动脚本:
$ cat ./start.sh
#!/bin/bash
HOMEDIR=/opt/myapp
$HOMEDIR/bin/application --deamon --config $HOMEDIR/conf/config.yml
从外观上看,启动脚本非常基础。看起来脚本只是将$HOMEDIR
变量设置为/opt/myapp
,然后通过运行命令$HOMEDIR/bin/application
来运行应用程序。
提示
在将$HOMEDIR
的值设置为/opt/myapp
之后,您可以假设将来对$HOMEDIR
的任何引用实际上是值/opt/myapp
。
从前面的脚本中,我们可以看到最后执行的命令是应用程序,这意味着我们收到的退出码来自应用程序而不是其他命令。这证明我们收到了这个应用程序的真实退出状态。
启动脚本确实为我们提供了比仅提供退出码的命令更多的信息。如果我们看一下应用程序的命令行参数,我们可以更多地了解这个应用程序:
$HOMEDIR/bin/application --deamon --config $HOMEDIR/conf/config.yml
这是实际在start.sh
脚本中启动应用程序的命令。该脚本正在使用参数--daemon
和--config /opt/myapp/conf/config.yml
运行命令/opt/myapp/bin/application
。虽然我们可能对这个应用程序了解不多,但我们可以做一些假设。
我们可以假设--daemon
标志导致这个应用程序使自己成为守护进程。在 Unix 和 Linux 系统上,作为后台进程持续运行的进程被称为守护进程。
通常,守护进程是一个不需要用户输入的服务。一些容易识别的守护进程的例子是 Apache 或 MySQL。这些进程在后台运行并提供服务,而不是在用户的桌面或 shell 中运行。
通过前面的标志,我们可以安全地假设一旦成功启动,这个进程就被设计为在后台运行。
基于命令行参数,我们可以做出另一个假设,即文件/opt/myapp/conf/config.yml
被用作应用程序的配置文件。考虑到标志被命名为--config
,这似乎很简单明了。
前面的假设很容易识别,因为标志使用长格式--option
。然而,并非所有应用程序或服务都使用命令行标志的长格式。通常,这些是单字符标志。
虽然每个应用程序都有自己的命令行标志,并且可能因应用程序而异,但常见的标志,如--config
和--deamon
通常被缩写为-c
和-d
或-D
。如果我们的应用程序提供了单字符标志,它看起来会更像下面的样子:
$HOMEDIR/bin/application -d -c $HOMEDIR/conf/config.yml
即使使用了缩短的选项,我们仍然可以安全地确定-c
指定了一个配置文件。
配置文件中包含大量信息
我们知道这个应用程序正在使用配置文件/opt/myapp/conf/config.yml
。如果我们读取这个文件,我们可能会找到关于应用程序以及它正在尝试执行的任务的信息:
$ cat conf/config.yml
port: 25
debug: True
logdir: /opt/myapp/logs
这个应用程序的配置文件非常简短,但其中包含了相当多有用的信息。第一个配置项很有趣,因为它似乎指定端口25
作为应用程序使用的端口。不知道这个应用程序具体做什么,这个信息并不立即有用,但以后可能对我们有用。
第二项似乎表明应用程序处于调试模式。通常应用程序或服务可能有一个debug
模式,导致它们记录或输出调试信息以进行故障排除。在我们的情况下,似乎调试选项已启用,因为这个项目的值是True
。
第三项和最后一项是一个看起来是日志的目录路径。日志文件对于故障排除应用程序总是有用的。通常情况下,您可以在日志文件中找到有关应用程序问题的信息。如果应用程序处于debug
状态,这对我们的应用程序似乎是正确的情况。
由于我们的应用似乎处于debug
模式,并且我们知道日志目录的位置。我们可以检查日志目录是否有在应用启动过程中创建的日志文件:
$ ls -la /opt/myapp/logs/
total 4
drwxrwxr-x. 2 vagrant vagrant 22 May 30 03:51 .
drwxr-xr-x. 5 vagrant vagrant 53 May 30 03:49 ..
-rw-rw-r--. 1 vagrant vagrant 454 May 30 03:54 debug.out
如果我们在日志目录中运行ls -la
,我们可以看到一个debug.out
文件。根据名称,这个文件很可能是应用程序的调试输出,但不一定是应用程序的主要日志文件。然而,这个文件可能比标准日志文件更有用,因为它可能包含应用程序启动失败的原因:
$ cat debug.out
Configuration file processed
--------------------------
Starting service: [Failed]
Configuration file processed
--------------------------
Starting service: [Success]
- - - - - - - - - - - - - - - - - - - - - - - - -
Proccessed 5 messages
Proccessed 5 messages
Configuration file processed
--------------------------
Starting service: [Failed]
Configuration file processed
--------------------------
Starting service: [Failed]
根据这个文件的内容,似乎这个文件包含了多次执行该应用程序的日志。我们可以根据重复的模式看到这一点。
Configuration file processed
--------------------------
这似乎是每次应用程序启动时打印的第一项。我们总共可以看到这些行四次;很可能,这意味着这个应用程序过去至少启动了四次。
在这个文件中,我们可以看到一个重要的日志消息:
Starting service: [Success]
看起来,这个应用程序第二次启动时应用程序启动成功。然而,之后每次启动都失败。
在启动过程中观看日志文件
由于调试文件的内容不包括时间戳,很难知道调试输出是否是在我们启动应用程序时编写的,还是在以前的启动过程中编写的。
由于我们不知道哪些行是在我们上次尝试时写入的,而不是其他尝试,我们需要尝试确定每次启动应用程序时写入了多少日志条目。为此,我们可以使用tail
命令与-f
或--follow
标志:
$ tail -f debug.out
- - - - - - - - - - - - - - - - - - - - - - - - -
Proccessed 5 messages
Proccessed 5 messages
[Failed]
Configuration file processed
--------------------------
Starting service: [Failed]
Configuration file processed
--------------------------
Starting service: [Failed]
当首次使用-f
(跟踪)标志启动tail
命令时,将打印文件的最后 10 行。如果没有使用任何标志运行,这也是tail
的默认行为。
然而,-f
标志并不仅仅停留在最后 10 行。当使用-f
标志运行时,tail
将持续监视指定文件的新数据。一旦tail
看到指定文件写入新数据,数据将被写入tail
的输出。
通过对debug.out
文件运行tail -f
,我们将能够识别应用程序写入的任何新的调试日志。如果我们再次执行start.sh
脚本,我们应该看到应用程序在启动过程中打印的任何可能的调试数据:
$ ./start.sh
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting service: [Failed]
start.sh
脚本的输出与上次相同,这在这一点上并不奇怪。然而,现在我们正在观看debug.out
文件,我们可能会找到一些有用的东西:
Configuration file processed
--------------------------
Starting service: [Failed]
从tail
命令中,我们可以看到前面三行是在执行start.sh
时打印的。虽然这本身并不能解释为什么应用程序无法启动,但它确实告诉了我们一些有趣的东西:
$ cat debug.out
Configuration file processed
--------------------------
Starting service: [Failed]
Configuration file processed
--------------------------
Starting service: [Success]
- - - - - - - - - - - - - - - - - - - - - - - - -
Processed 5 messages
Processed 5 messages
Configuration file processed
--------------------------
Starting service: [Failed]
Configuration file processed
--------------------------
Starting service: [Failed]
Configuration file processed
--------------------------
Starting service: [Failed]
考虑到当应用程序无法启动时,“失败”消息会从之前的命令中打印出来,我们可以看到start.sh
脚本执行的最后三次都失败了。然而,在那之前的实例是成功的。
到目前为止,我执行了启动脚本两次,另一位管理员执行了一次。这可以解释我们在debug.out
文件末尾看到的三次失败。有趣的是,在这些失败之前,应用程序成功启动了。
这很有趣,因为它表明应用程序的先前实例可能正在运行。
检查应用程序是否已经在运行
这种问题的一个非常常见的原因是应用程序已经在运行。有些应用程序应该只启动一次,在完成启动之前,应用程序本身会检查是否有另一个实例正在运行。
一般来说,如果是这种情况,我们期望应用程序会在屏幕上或debug.out
文件中打印错误。然而,并非每个应用程序都有适当的错误处理或消息传递。这对于定制应用程序尤其如此,似乎也适用于我们正在处理的应用程序。
目前,我们假设我们的问题是由应用程序的另一个实例引起的。这是基于调试消息和以往经验的一个有根据的猜测。虽然我们还没有任何确凿的事实告诉我们是否有另一个实例正在运行,但这种情况是相当常见的。
这种情况是一个有经验的猜测者利用以往的经验来建立根本原因的假设的完美例子。当然,在形成假设之后,我们的下一步是验证它是否正确。即使我们的假设最终被证明是错误的,我们至少可以排除我们问题的一个潜在原因。
由于我们目前的假设是我们可能已经有一个应用程序的实例在运行,我们可以通过执行 ps 命令来验证它:
$ ps -elf | grep application
0 S vagrant 7110 5567 0 80 0 - 28160 pipe_w 15:22 pts/0 00:00:00 grep --color=auto application
从中可以看出,我们的假设可能是不正确的。然而,之前的命令只是执行进程列表,并在输出中搜索任何包含单词“应用程序”的实例。虽然这个命令可能足够了,但是一些应用程序在启动过程中(特别是那些变成守护进程的应用程序)会启动另一个进程,这个进程可能不匹配字符串“应用程序”。
由于我们一直以vagrant
用户启动应用程序,即使应用程序变成守护进程,进程也会以 vagrant 用户的身份运行。使用相同的命令,我们还可以搜索以vagrant
用户身份运行的进程列表:
$ ps -elf | grep vagrant
4 S root 4230 984 0 80 0 - 32881 poll_s May30 ? 00:00:00 sshd: vagrant [priv]
5 S vagrant 4233 4230 0 80 0 - 32881 poll_s May30 ? 00:00:00 sshd: vagrant@pts/1
0 S vagrant 4234 4233 0 80 0 - 28838 n_tty_ May30 pts/1 00:00:00 -bash
4 S root 5563 984 0 80 0 - 32881 poll_s May31 ? 00:00:00 sshd: vagrant [priv]
5 S vagrant 5566 5563 0 80 0 - 32881 poll_s May31 ? 00:00:01 sshd: vagrant@pts/0
0 S vagrant 5567 5566 0 80 0 - 28857 wait May31 pts/0 00:00:00 -bash
0 R vagrant 7333 5567 0 80 0 - 30839 - 14:58 pts/0 00:00:00 ps -elf
0 S vagrant 7334 5567 0 80 0 - 28160 pipe_w 14:58 pts/0 00:00:00 grep --color=auto vagrant
这个命令给了我们更多的输出,但不幸的是,这些进程中没有一个是我们正在寻找的应用程序。
检查打开的文件
之前的进程列表命令没有提供任何结果,表明我们的应用程序的实例正在运行。然而,在假设它实际上没有运行之前,我们应该进行最后一次检查。
由于我们知道我们正在处理的应用程序似乎安装在/opt/myapp
中,我们可以在该目录中看到配置文件和日志。可以很肯定地假设所讨论的应用程序可能会打开/opt/myapp
目录中的一个或多个文件。
一个非常有用的命令是lsof命令。通过这个命令,我们可以列出系统上所有打开的文件。虽然这一开始可能听起来不太强大,但让我们详细看看这个命令,了解它实际上可以提供多少信息。
当运行lsof
命令时,权限变得非常重要。当不带任何参数执行lsof
时,该命令将打印出它能识别的每个进程的所有打开文件的列表。如果我们以非特权用户(如“vagrant
”用户)身份运行此命令,输出将只包含作为 vagrant 用户运行的进程。然而,如果我们以 root 用户身份运行该命令,该命令将打印系统上所有进程的打开文件。
为了更好地理解这意味着多少文件,我们将运行lsof
命令并将输出重定向到wc -l
命令,这将计算输出中提供的行数:
# lsof | wc -l
3840
从wc
命令中,我们可以看到当前系统上有3840
个文件打开。现在,其中一些文件可能是重复的,因为可能有多个进程打开同一个文件。然而,当前系统上打开文件的数量非常大。为了进一步了解,这个系统也是一个相当未被充分利用的系统,一般并没有运行很多应用程序。如果在一个充分利用的系统上执行上述命令后,打开文件的数量呈指数级增长,也不要感到惊讶。
由于查看3840
个打开文件并不是很实际,让我们通过查看lsof
输出的前 10 个文件来更好地理解lsof
。我们可以通过将命令的输出重定向到head
命令来实现这一点,head
命令将默认打印 10 行,就像tail
命令一样。然而,tail
命令打印最后 10 行,而head
命令打印前 10 行:
# lsof | head
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd DIR 253,1 4096 128 /
systemd 1 root rtd DIR 253,1 4096 128 /
systemd 1 root txt REG 253,1 1214408 67629956 /usr/lib/systemd/systemd
systemd 1 root mem REG 253,1 58288 134298633 /usr/lib64/libnss_files-2.17.so
systemd 1 root mem REG 253,1 90632 134373166 /usr/lib64/libz.so.1.2.7
systemd 1 root mem REG 253,1 19888 134393597 /usr/lib64/libattr.so.1.1.0
systemd 1 root mem REG 253,1 113320 134298625 /usr/lib64/libnsl-2.17.so
systemd 1 root mem REG 253,1 153184 134801313 /usr/lib64/liblzma.so.5.0.99
systemd 1 root mem REG 253,1 398264 134373152 /usr/lib64/libpcre.so.1.2.0
正如我们所看到的,以 root 身份执行的lsof
命令能够为我们提供相当多有用的信息。让我们只看一下输出的第一行,以了解lsof
显示了什么:
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd DIR 253,1 4096 128 /
lsof
命令打印 10 列,每个打开文件。
第一列是COMMAND
列。这个字段包含打开文件的可执行文件的名称。当识别哪些进程打开了特定文件时,这是非常有用的。
对于我们的用例,这将告诉我们哪些进程打开了我们感兴趣的文件,并可能告诉我们正在寻找的应用程序的进程名称。
第二列是PID
列。这个字段和第一个一样有用,因为它显示了打开显示的文件的应用程序的进程 ID。如果实际上正在运行,这个值将允许我们将应用程序缩小到特定的进程。
第三列是TID
列,在我们的输出中是空白的。这一列包含了所讨论进程的线程 ID。在 Linux 中,多线程应用程序能够生成线程,这些线程也被称为轻量级进程。这些线程类似于常规进程,但能够共享资源,如文件描述符和内存映射。你可能听说过这些被称为线程或轻量级进程,但它们本质上是一样的。
为了看到TID
字段,我们可以在lsof
命令中添加-K
(显示线程)标志。这将导致lsof
打印所有轻量级进程以及完整进程。
lsof
输出的第四列是USER
字段。这个字段将打印打开文件的进程的用户名或UID
(如果找不到用户名)。重要的是要知道,这个字段是进程正在执行的用户,而不是文件本身的所有者。
例如,如果作为rotot
运行的进程打开了一个由vagrant
拥有的文件,lsof
中的 USER 字段将显示 root。这是因为lsof
命令用于显示哪些进程打开了文件,并且用于显示有关进程的信息,而不一定是文件。
理解文件描述符
第五列非常有趣,因为这是文件描述符(FD)的字段;这是一个棘手的 Unix 和 Linux 主题。
文件描述符是 POSIX 应用程序编程接口(API)的一部分,这是所有现代 Linux 和 Unix 操作系统遵循的标准。从程序的角度来看,文件描述符是一个由非负数表示的对象。这个数字被用作内核在每个进程基础上管理的打开文件表的标识符。
由于内核在每个进程级别上维护这个数据,数据包含在/proc
文件系统中。我们可以通过在/proc/<process id>/fd
目录中执行ls -la
来查看这个打开文件表:
# ls -la /proc/1/fd
total 0
dr-x------. 2 root root 0 May 17 23:07 .
dr-xr-xr-x. 8 root root 0 May 17 23:07 ..
lrwx------. 1 root root 64 May 17 23:07 0 -> /dev/null
lrwx------. 1 root root 64 May 17 23:07 1 -> /dev/null
lrwx------. 1 root root 64 Jun 1 15:08 10 -> socket:[7951]
lr-x------. 1 root root 64 Jun 1 15:08 11 -> /proc/1/mountinfo
lr-x------. 1 root root 64 Jun 1 15:08 12 -> /proc/swaps
lrwx------. 1 root root 64 Jun 1 15:08 13 -> socket:[11438]
lr-x------. 1 root root 64 Jun 1 15:08 14 -> anon_inode:inotify
lrwx------. 1 root root 64 May 17 23:07 2 -> /dev/null
lrwx------. 1 root root 64 Jun 1 15:08 20 -> socket:[7955]
lrwx------. 1 root root 64 Jun 1 15:08 21 -> socket:[13968]
lrwx------. 1 root root 64 Jun 1 15:08 22 -> socket:[13980]
lrwx------. 1 root root 64 May 17 23:07 23 -> socket:[13989]
lrwx------. 1 root root 64 Jun 1 15:08 24 -> socket:[7989]
lrwx------. 1 root root 64 Jun 1 15:08 25 -> /dev/initctl
lrwx------. 1 root root 64 Jun 1 15:08 26 -> socket:[7999]
lrwx------. 1 root root 64 May 17 23:07 27 -> socket:[6631]
lrwx------. 1 root root 64 May 17 23:07 28 -> socket:[6634]
lrwx------. 1 root root 64 May 17 23:07 29 -> socket:[6636]
lr-x------. 1 root root 64 May 17 23:07 3 -> anon_inode:inotify
lrwx------. 1 root root 64 May 17 23:07 30 -> socket:[8006]
lr-x------. 1 root root 64 Jun 1 15:08 31 -> anon_inode:inotify
lr-x------. 1 root root 64 Jun 1 15:08 32 -> /dev/autofs
lr-x------. 1 root root 64 Jun 1 15:08 33 -> pipe:[10502]
lr-x------. 1 root root 64 Jun 1 15:08 34 -> anon_inode:inotify
lrwx------. 1 root root 64 Jun 1 15:08 35 -> anon_inode:[timerfd]
lrwx------. 1 root root 64 Jun 1 15:08 36 -> socket:[8095]
lrwx------. 1 root root 64 Jun 1 15:08 37 -> /run/dmeventd-server
lrwx------. 1 root root 64 Jun 1 15:08 38 -> /run/dmeventd-client
lrwx------. 1 root root 64 Jun 1 15:08 4 -> anon_inode:[eventpoll]
lrwx------. 1 root root 64 Jun 1 15:08 43 -> socket:[11199]
lrwx------. 1 root root 64 Jun 1 15:08 47 -> socket:[14300]
lrwx------. 1 root root 64 Jun 1 15:08 48 -> socket:[14300]
lrwx------. 1 root root 64 Jun 1 15:08 5 -> anon_inode:[signalfd]
lr-x------. 1 root root 64 Jun 1 15:08 6 -> /sys/fs/cgroup/systemd
lrwx------. 1 root root 64 Jun 1 15:08 7 -> socket:[7917]
lrwx------. 1 root root 64 Jun 1 15:08 8 -> anon_inode:[timerfd]
lrwx------. 1 root root 64 Jun 1 15:08 9 -> socket:[7919]
这是systemd
进程的文件描述符表。正如你所看到的,有一个数字,这个数字与一个文件/对象相关联。
这个输出中不容易表示的是这是一个不断变化的过程。当一个文件/对象被关闭时,文件描述符号就可以被内核重新分配给一个新的打开的文件/对象。根据进程打开和关闭文件的频率,如果我们重复相同的 ls 命令,我们可能会在这个表中看到完全不同的一组打开文件。
有了这个,我们期望lsof
中的 FD 字段总是显示一个数字。然而,lsof
输出中的 FD 字段实际上可以包含不止文件描述符号。这是因为lsof
实际上显示的不仅仅是文件。
当执行时,lsof
命令将打印许多不同类型的打开对象;并非所有这些都是文件。我们之前lsof
命令输出的第一行就是一个例子:
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd DIR 253,1 4096 128 /
前面的项目不是一个文件,而是一个目录。因为这是一个目录,FD 字段显示cwd
,用于表示打开对象的当前工作目录。这实际上与打开对象为文件时打印的输出非常不同。
为了更好地显示区别,我们可以通过将文件作为lsof
的参数来运行lsof
命令来针对特定文件运行lsof
:
# lsof /dev/null | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root 0u CHR 1,3 0t0 23 /dev/null
systemd 1 root 1u CHR 1,3 0t0 23 /dev/null
systemd 1 root 2u CHR 1,3 0t0 23 /dev/null
systemd-j 436 root 0r CHR 1,3 0t0 23 /dev/null
systemd-j 436 root 1w CHR 1,3 0t0 23 /dev/null
systemd-j 436 root 2w CHR 1,3 0t0 23 /dev/null
lvmetad 469 root 0r CHR 1,3 0t0 23 /dev/null
systemd-u 476 root 0u CHR 1,3 0t0 23 /dev/null
systemd-u 476 root 1u CHR 1,3 0t0 23 /dev/null
在上面的输出中,我们不仅能够看到许多进程打开了/dev/null
,而且每行的FD
字段也有很大不同。如果我们看第一行,我们可以看到systemd
进程打开了/dev/null
,而FD
字段的值是0u
。
当lsof
显示一个标准文件的打开对象时,FD
字段将包含与内核表中该打开文件相关联的文件描述符号,本例中为0
。
如果我们回顾一下/proc/1/fd
目录,我们实际上可以在内核表中看到这个表示:
# ls -la /proc/1/fd/0
lrwx------. 1 root root 64 May 17 23:07 /proc/1/fd/0 -> /dev/null
文件描述符号可能会跟随两个值,这取决于文件的打开方式以及它是否被锁定。
第一个潜在的值显示了文件的打开模式。从我们的例子中,这由0u
值中的u
表示。小写的u
表示文件同时以读写方式打开。
以下是lsof
将显示的潜在模式列表:
-
r
:小写的r
表示文件只能读取 -
w
:小写的w
表示文件只能写入打开 -
u
:小写的u
表示文件同时以读写方式打开 -
:空格用于表示文件打开的模式未知,并且当前文件上没有锁 -
-
:连字符用于表示文件打开的模式未知,并且当前文件上有锁
最后两个值实际上非常有趣,因为它们将我们带到文件描述符号后的第二个潜在值。
Linux 和 Unix 系统上的进程在打开文件时允许请求文件被锁定。有多种类型的锁,这也在lsof
输出中显示出来:
master 1586 root 10uW REG 253,1 33 135127929 /var/spool/postfix/pid/master.pid
在前面的示例中,FD
字段包含10uW
。根据先前的示例,我们知道 10 是文件描述符号,u
表示此文件已打开以进行读写,但W
是新的。这个 W 显示了进程对该文件的锁的类型;对于这个示例来说,是写锁。
与文件打开模式一样,从lsof
中可以看到许多不同类型的锁。以下是lsof
显示的可能锁的列表:
-
N
:用于 Solaris 未知类型的 NFS 锁 -
r
:这是对文件的部分读取锁 -
R
:这是对整个文件的读取锁 -
w
:这是对文件的部分写锁 -
W
:这是对整个文件的写锁 -
u
:这是任意长度的读写锁 -
U
:未知类型的读写锁 -
x
:这是 SCO Openserver Xenix 对部分文件的锁 -
X
:这是 SCO Openserver Xenix 对整个文件的锁
您可能会注意到有几种可能的锁并非特定于 Linux。这是因为lsof
是一种广泛用于 Linux 和 Unix 的工具,并支持许多 Unix 发行版,如 Solaris 和 SCO。
现在我们已经了解了lsof
如何显示实际文件的FD
字段,让我们看看它如何显示不一定是文件的打开对象:
iprupdate 595 root cwd DIR 253,1 4096 128 /
iprupdate 595 root rtd DIR 253,1 4096 128 /
iprupdate 595 root txt REG 253,1 114784 135146206 /usr/sbin/iprupdate
iprupdate 595 root mem REG 253,1 2107600 134298615 /usr/lib64/libc-2.17.so
通过这个,我们可以在这个列表中看到很多不同的FD
值,比如cwd
、rtd
、txt
和mem
。我们已经从之前的示例中知道,cwd
用于显示当前工作目录
,但其他的都是新的。实际上,根据打开的对象,可能有许多不同的可能文件类型。以下列表包含了所有可能的值,如果不使用文件描述符号,则可以显示:
-
cwd
:当前工作目录 -
Lnn
:AIX 系统的库引用(nn
是一个数值) -
err
:文件描述符信息错误 -
jld
:FreeBSD 监禁目录 -
ltx
:共享库文本 -
Mxx
:十六进制内存映射(xx 是类型编号) -
m86
:DOS 合并映射文件 -
mem
:内存映射文件 -
mmap
:内存映射设备 -
pd
:父目录 -
rtd
:根目录 -
tr
:内核跟踪文件 -
txt
:程序文本 -
v86
:VP/ix 映射文件
我们可以看到FD
字段有许多可能的值。既然我们已经看到了可能的值,让我们看一下前面的示例,以更好地理解显示的打开项目的类型:
iprupdate 595 root cwd DIR 253,1 4096 128 /
iprupdate 595 root rtd DIR 253,1 4096 128 /
iprupdate 595 root txt REG 253,1 114784 135146206 /usr/sbin/iprupdate
iprupdate 595 root mem REG 253,1 2107600 134298615 /usr/lib64/libc-2.17.so
前两行很有趣,因为它们都是针对"/
"目录。但是,第一行显示"/
"目录为cwd
,这意味着它是当前工作目录。第二行显示"/
"目录为rtd
,这意味着这也是iprupdate
程序的根目录。
第三行显示/usr/sbin/iprupdate
是程序本身,因为它的FD
字段值为txt
。这意味着打开的文件是程序的代码。第四行打开项目/usr/lib64/libc-2.17.so
显示了一个mem
的 FD。这意味着文件/usr/lib64/libc-2.17.so
已被读取并放入内存中供iprupdate
进程使用。这意味着这个文件可以被当作内存对象访问。这对于诸如libc-2.17.so
之类的库文件是一种常见做法。
回到lsof
输出
现在我们已经彻底探讨了lsof
输出的FD
字段,让我们转到第六列,即TYPE
字段。该字段显示正在打开的文件类型。由于可能的类型相当多,要在这里列出它们可能有点棘手;但是,您可以在lsof
手册页中找到这些信息,该手册页可以在线访问,也可以通过"man lsof
"命令访问。
虽然我们不会列出每种可能的文件类型,但我们可以快速查看一下从我们的示例系统中捕获的一些文件类型:
systemd 1 root mem REG 253,1 160240 134296681 /usr/lib64/ld-2.17.so
systemd 1 root 0u CHR 1,3 0t0 23 /dev/null
systemd 1 root 6r DIR 0,20 0 6404 /sys/fs/cgroup/systemd
systemd 1 root 7u unix 0xffff88001d672580 0t0 7917 @/org/freedesktop/systemd1/notify
第一个示例项显示TYPE
为REG
。这种TYPE
非常常见,因为被列出的项目是一个Regular
文件。第二个示例项显示Character special file (CHR)。CHR 表示特殊文件,它们表现为文件,但实际上是设备的接口。列出的/dev/null
就是一个字符文件的完美例子,因为它被用作输入到空。任何写入/dev/null
的内容都会被清空,如果您读取此文件,将不会收到任何输出。
第三项显示DIR
,这应该不足为奇,DIR
代表目录。这是一个非常常见的TYPE
,因为许多进程在某个级别上都需要打开一个目录。
第四项显示了unix
,表明此打开项目是 Unix 套接字文件。Unix 套接字文件是用作进程通信的输入/输出设备的特殊文件。这些文件应该经常出现在lsof
输出中。
正如我们从前面的示例中看到的,在 Linux 系统上有几种不同类型的文件。
现在我们已经查看了lsof
输出中的第六列,即TYPE
列,让我们快速看一下第七列,即DEVICE
列:
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd DIR 253,1 4096 128 /
如果我们看前面的项目,我们可以看到DEVICE
列的值为253,1
。这些数字代表此项目所在设备的主设备号和次设备号。Linux 中的主设备号和次设备号被系统用来确定如何访问设备。主设备号,在本例中为253
,用于确定系统应该使用哪个驱动程序。一旦选择了驱动程序,次设备号,在我们的情况下为 1,然后用于进一步确定如何访问这个设备。
提示
主设备号和次设备号实际上是 Linux 及其设备使用的重要部分。虽然我们不会在本书中深入讨论这个主题,但我建议您多了解一些,因为这些信息在故障排除硬件设备问题时非常有用。
systemd 1 root mem REG 253,1 160240 134296681 /usr/lib64/ld-2.17.so
systemd 1 root 0u CHR 1,3 0t0 12 /dev/null
现在我们已经探索了DEVICE
列,让我们来看一下lsof
输出的第八列,SIZE/OFF
。SIZE/OFF
列用于显示打开项目的大小或偏移量。偏移通常与套接字文件和字符文件一起显示。当此列包含偏移量时,它将以"0t
"开头。在上面的示例中,我们可以看到字符文件/dev/null
的偏移值为0t0
。
SIZE
值用于指代常规文件等打开项目的大小。这个值实际上是文件的大小(以字节为单位)。例如,我们可以看到/usr/lib64/ld-2.17.so
的SIZE
列为160240
。这意味着这个文件大约有 160 KB 大小。
lsof
输出中的第九列是NODE
列:
httpd 3205 apache 2w REG 253,1 497 134812768 /var/log/httpd/error_log
httpd 3205 apache 4u IPv6 16097 0t0 TCP *:http (LISTEN)
对于常规文件,NODE
列将显示文件的inode编号。在文件系统中,每个文件都有一个 inode,这个 inode 被用作包含所有单个文件元数据的索引。这些元数据包括文件在磁盘上的位置、文件权限、创建时间和修改时间等。与主设备号和次设备号一样,我建议深入了解 inode 及其包含的内容,因为 inode 是文件在 Linux 系统上存在的核心组件。
您可以从前面示例中的第一项看到,/var/log/httpd/error_log
的 inode 是134812768
。
然而,第二行显示NODE
为 TCP,这不是一个 inode。它显示 TCP 的原因是因为打开项目是 TCP 套接字,它不是文件系统上的文件。与TYPE
列一样,NODE
列将根据打开项目而改变。然而,在大多数系统上,您通常会看到一个 inode 编号、TCP 或 UDP(用于 UDP 套接字)。
lsof
输出中的第十列是非常容易理解的,因为我们已经多次引用过它。第十列是NAME
字段,就像它听起来那样简单;它列出了打开项目的名称:
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd DIR 253,1 4096 128 /
使用 lsof 来检查是否有先前运行的进程
现在我们对lsof
的工作原理和它如何帮助我们有了更多了解,让我们使用这个命令来检查是否有任何正在运行的应用程序实例。
如果我们只是以 root 用户身份运行lsof
命令,我们将看到系统上所有打开的文件。然而,即使我们将输出重定向到less
或grep
等命令,输出也可能会非常庞大。幸运的是,lsof
允许我们指定要查找的文件和目录:
# lsof /opt/myapp/conf/config.yml
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
less 3494 vagrant 4r REG 253,1 45 201948450 /opt/myapp/conf/config.yml
正如我们所看到的,通过指定前面的命令中的一个文件,我们将输出限制为具有该文件打开的进程。
如果我们指定一个目录,输出是类似的:
# lsof /opt/myapp/
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 3474 vagrant cwd DIR 253,1 53 25264 /opt/myapp
less 3509 vagrant cwd DIR 253,1 53 25264 /opt/myapp
从中我们可以看到两个进程打开了/opt/myapp
目录。我们可以限制lsof
的输出的另一种方法是指定+D
(目录内容)标志,后跟一个目录。这个标志将告诉lsof
查找该目录及其以下的任何打开项目。
例如,我们看到当使用lsof
针对配置文件时,less
进程已经打开了它。我们还可以看到,当用于/opt/myapp/
目录时,两个进程打开了该目录。
我们可以使用+D
标志一次查看所有这些项目:
# lsof +D /opt/myapp/
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 3474 vagrant cwd DIR 253,1 53 25264 /opt/myapp
less 3509 vagrant cwd DIR 253,1 53 25264 /opt/myapp
less 3509 vagrant 4r REG 253,1 45 201948450 /opt/myapp/conf/config.yml
这也会显示位于/opt/myapp
目录下的任何其他项目。由于我们要检查应用程序是否有另一个实例正在运行,让我们看一下前面的lsof
输出,并看看可以学到什么:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 3474 vagrant cwd DIR 253,1 53 25264 /opt/myapp
第一个打开的项目显示了一个bash
进程,以vagrant
用户身份运行,具有当前工作目录的文件描述符。这一行很可能是我们自己的bash
进程,目前正在/opt/myapp
目录中,当前正在执行/opt/myapp/conf/config.yml
文件上的less
命令。
我们可以通过使用ps
命令并grep
字符串3474
来检查这一点,bash
命令的进程 ID:
# ps -elf | grep 3474
0 S vagrant 3474 3473 0 80 0 - 28857 wait 20:09 pts/1 00:00:00 -bash
0 S vagrant 3509 3474 0 80 0 - 27562 n_tty_ 20:14 pts/1 00:00:00 less conf/config.yml
0 S root 3576 2978 0 80 0 - 28160 pipe_w 21:08 pts/0 00:00:00 grep --color=auto 3474
在这种情况下,我选择使用grep
命令,因为我们还将能够看到引用进程 ID3474
的任何子进程。也可以通过运行以下命令来执行相同的操作,而不使用grep
命令:
# ps -lp 3474 --ppid 3474
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 3474 3473 0 80 0 - 28857 wait pts/1 00:00:00 bash
0 S 1000 3509 3474 0 80 0 - 27562 n_tty_ pts/1 00:00:00 less
总的来说,两种方法都会产生相同的结果;然而,第一种方法更容易记住。
如果我们查看进程列表输出,我们可以看到bash
命令实际上与我们的 shell 相关,因为它的子进程是我们知道在另一个窗口中正在运行的less
命令。
我们还可以看到less
命令的进程 ID:3509
。相同的进程 ID 在lsof
输出中显示了less
命令:
less 3509 vagrant cwd DIR 253,1 53 25264 /opt/myapp
less 3509 vagrant 4r REG 253,1 45 201948450 /opt/myapp/conf/config.yml
由于输出只显示我们自己的进程,可以安全地假设在后台没有运行先前的应用程序实例。
了解更多关于应用程序的信息
我们现在知道问题不是另一个此应用程序实例正在运行。在这一点上,我们应该尝试并识别更多关于这个应用程序以及它在做什么的信息。
在尝试查找有关此应用程序的更多信息时,首先要做的是查看应用程序的文件类型。我们可以使用file
命令来做到这一点:
$ file bin/application
bin/application: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0xbc4685b44eb120ff2252e21bd735933d51409ffa, not stripped
file
命令是一个非常有用的命令,因为这个命令将识别指定文件的文件类型。在前面的例子中,我们可以看到"application
"文件是一个已编译的二进制文件。我们可以看到它是由这个特定的输出编译的:ELF 64 位 LSB 可执行文件
。
这行还告诉我们应用程序是作为 64 位应用程序编译的。这很有趣,因为 64 位应用程序和 32 位应用程序之间有很多区别。一个非常常见的情况是由于 64 位应用程序可以消耗的资源量;32 位应用程序通常比 64 位版本受限得多。
另一个常见问题是尝试在 32 位内核上执行 64 位应用程序。我们尚未验证是否在 64 位内核上运行;如果我们试图在 32 位内核上运行 64 位可执行文件,我们肯定会收到一些错误。
尝试在 32 位内核上执行 64 位应用程序时出现的错误类型非常具体,不太可能是我们问题的原因。尽管这不太可能是原因,我们可以使用uname -a
命令来检查内核是否为 64 位内核:
$ uname -a
Linux blog.example.com 3.10.0-123.el7.x86_64 #1 SMP Mon Jun 30 12:09:22 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
从uname -a
命令的输出中,我们可以看到内核实际上是 64 位内核,因为存在这个字符串"x86_64
"。
使用 strace 跟踪应用程序
由于我们知道应用程序是编译后的二进制文件,没有源代码,这使得在应用程序内部阅读代码相当困难。然而,我们可以追踪应用程序执行的系统调用,以查看是否能找到任何关于它为何无法启动的信息。
什么是系统调用?
系统调用是应用程序和内核之间的主要接口。简而言之,系统调用是请求内核执行操作的方法。
大多数应用程序不需要担心系统调用,因为系统调用通常由低级库(如 GNU C 库)调用。虽然程序员不需要担心系统调用,但重要的是要知道应用程序执行的每个操作都归结为某种系统调用。
这很重要,因为我们可以追踪这些系统调用来确定应用程序到底在做什么。就像我们使用tcpdump
来追踪系统上的网络流量一样,我们可以使用一个叫做strace
的命令来追踪进程的系统调用。
为了感受strace
,让我们使用strace
对之前的exitcodes.sh
脚本进行系统调用跟踪。为此,我们将运行strace
命令,然后是exitcodes.sh
脚本。
执行时,strace
命令将启动,然后执行exitcodes.sh
脚本。在exitcodes.sh
脚本运行时,strace
命令将打印exitcodes.sh
脚本中提供的每个系统调用和参数:
$ strace /var/tmp/exitcodes.sh
execve("/var/tmp/exitcodes.sh", ["/var/tmp/exitcodes.sh"], [/* 26 vars */]) = 0
brk(0) = 0x261a000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f890bd12000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0
mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000
close(3) = 0
open("/lib64/libtinfo.so.5", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@\316\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=174520, ...}) = 0
mmap(NULL, 2268928, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f890b8c9000
mprotect(0x7f890b8ee000, 2097152, PROT_NONE) = 0
mmap(0x7f890baee000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f890baee000
close(3) = 0
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\16\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=19512, ...}) = 0
这只是strace
的输出的一小部分。完整的输出实际上有好几页长。然而,exitcodes.sh
脚本并不是很长。事实上,它只是一个简单的三行脚本:
$ cat /var/tmp/exitcodes.sh
#!/bin/bash
touch /some/directory/that/doesnt/exist/file.txt
echo "It works"
这个脚本很好地展示了高级编程语言(如 bash)提供了多少重要的功能。现在我们知道exitcodes.sh
脚本的作用,让我们来看一下它执行的一些系统调用。
我们将从前八行开始:
execve("/var/tmp/exitcodes.sh", ["/var/tmp/exitcodes.sh"], [/* 26 vars */]) = 0
brk(0) = 0x261a000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f890bd12000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0
mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000
close(3) = 0
由于系统调用非常广泛,有些系统调用很难理解。我们将把重点放在常见且较容易理解的系统调用上。
我们将要检查的第一个系统调用是access()
系统调用:
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
大多数系统调用都有一个名字,大致解释它执行的功能。access()
系统调用也不例外,因为这个系统调用用于检查调用它的应用程序是否有足够的权限打开指定的文件。在前面的例子中,指定的文件是/etc/ld.so.preload
。
关于strace
的一个有趣的事情是它不仅显示系统调用,还显示返回值。在我们前面的示例中,access()
系统调用收到了一个返回值-1
,这是错误的典型值。当返回值是错误时,strace
还会提供错误字符串。在这种情况下,access()
调用收到了错误-1 ENOENT (No such file or directory)
。
前面的错误非常容易理解,因为似乎文件/etc/ld.so.preload
根本不存在。
下一个系统调用是一个经常见到的系统调用;它就是open()
系统调用:
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open()
系统调用执行了它所说的内容,它用于打开(或创建并打开)文件或设备。从前面的示例中,我们可以看到指定的文件是/etc/ld.so.cache
文件。我们还可以看到传递给这个系统调用的参数之一是"O_RDONLY
"。这个参数告诉open()
调用以只读模式打开文件。
即使我们不知道O_RDONLY
参数告诉打开命令以只读模式打开文件,这个名字几乎是自我描述的。对于那些不够自我描述的系统调用,可以通过相当快速的谷歌搜索找到相关信息,因为系统调用都有很好的文档记录:
fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0
下一个要看的系统调用是fstat()
系统调用。这个系统调用将获取文件的状态。这个系统调用提供的信息包括诸如 inode 号、用户所有权和文件大小等内容。单独看,fstat()
系统调用可能看起来并不重要,但当我们看下一个系统调用mmap()
时,它提供的信息可能就很重要了。
mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000
这个系统调用可以用来将文件映射或取消映射到内存中。如果我们看一下fstat()
行和mmap()
行,我们会看到两个相符的数字。fstat()
行有st_size=24646
,这是提供给mmap()
的第二个参数。
即使不知道这些系统调用的细节,也很容易得出这样的假设,即mmap()
系统调用将文件从fstat()
调用映射到内存中。
前面示例中的最后一个系统调用非常容易理解:
close(3) = 0
close()
系统调用只是关闭打开的文件或设备。考虑到我们之前打开了文件/etc/ld.so.cache
,这个close()
系统调用被用来关闭那个文件是很合理的。在我们回到调试应用程序之前,让我们快速看一下最后四行放在一起的内容:
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0
mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000
close(3)
当我们看这四个系统调用时,我们可以开始看到一个模式。open()
调用用于打开/etc/ld.so.cache
文件,并返回值为3
。fstat()
命令提供了3
作为输入,并得到了st_size=24646
作为输出。mmap()
函数被给予了24646
和3
作为输入,close()
函数被提供了3
作为输入。
考虑到open()
调用的输出是3
,并且这个值在这四个系统调用中被多次使用,可以安全地得出结论,即这个数字3
是打开文件/etc/ld.so.cache
的文件描述符号。有了这个结论,我们也可以相当肯定,前面的四个系统调用执行了打开文件/etc/ld.so.cache
、确定文件大小、将文件映射到内存,然后关闭文件描述符的操作。
正如你所看到的,仅仅通过四个简单的系统调用就得到了相当多的信息。让我们将刚学到的知识付诸实践,使用strace
来跟踪应用程序进程。
使用 strace 来确定应用程序为什么无法启动
早些时候,当我们运行strace
时,我们只是提供了一个要执行的命令。这是你可以调用strace
的一种方式,但如果进程已经在运行,你该怎么办呢?嗯,strace
也可以跟踪正在运行的进程。
在跟踪现有进程时,我们可以使用-p
(进程)标志加上要跟踪的进程 ID 来启动strace
。这会导致strace
绑定到该进程并开始跟踪它。为了跟踪我们的应用程序启动,我们将使用这种方法。
为了做到这一点,我们将在后台执行start.sh
脚本,然后对start.sh
脚本的进程 ID 运行strace
:
$ ./start.sh &
[1] 3353
通过在命令行的末尾添加&,我们告诉启动脚本在后台运行。输出提供了正在运行的脚本的进程 ID,3353
。然而,在另一个窗口中作为 root 用户,我们可以使用以下命令对该进程进行跟踪:
# strace -o /var/tmp/app.out -f -p 3353
Process 3353 attached
Process 3360 attached
前面的命令比只有-p
和进程 ID 多了一些选项。我们还添加了-o /var/tmp/app.out
参数。这个选项告诉strace
将跟踪的数据保存到输出文件/var/tmp/app.out
中。我们之前运行的strace
提供了相当多的输出;通过指定数据应该写入文件,数据将更容易搜索。
我们添加的另一个新选项是-f
;这个参数告诉strace
跟踪子进程。由于启动脚本启动了应用程序,应用程序本身被认为是启动脚本的子进程。在前面的例子中,我们可以看到strace
附加到了两个进程。我们可以假设第二个进程收到了进程 ID3360
,这一点很重要,因为在浏览跟踪输出时我们需要引用该进程 ID:
# less /var/tmp/app.out
让我们开始阅读strace
输出并尝试识别发生了什么。在浏览输出时,我们将限制它只包括对识别我们问题有用的部分:
3360 execve("/opt/myapp/bin/application", ["/opt/myapp/bin/application", "--deamon", "--config", "/opt/myapp/conf/config.yml"], [/* 28 vars */]) = 0
看起来有趣的第一个系统调用是execve()
系统调用。这个特定的execve()
调用似乎是在执行/opt/myapp/bin/application
二进制文件。
需要指出的一个重要事项是,通过这个输出,我们可以看到系统调用之前有一个数字。这个数字3360
是执行系统调用的进程 ID。只有在 strace 命令跟踪多个进程时才会显示进程 ID。
The next group of system calls that seem important are the following:
3360 open("/opt/myapp/conf/config.yml", O_RDONLY) = 3
3360 fstat(3, {st_mode=S_IFREG|0600, st_size=45, ...}) = 0
3360 fstat(3, {st_mode=S_IFREG|0600, st_size=45, ...}) = 0
3360 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd0528df000
3360 read(3, "port: 25\ndebug: True\nlogdir: /op"..., 4096) = 45
3360 read(3, "", 4096) = 0
3360 read(3, "", 4096) = 0
从前面的一组中,我们可以看到应用程序以只读方式打开了config.yml
文件,并且没有收到错误。我们还可以看到read()
系统调用(似乎是从文件描述符 3 读取)正在读取config.yml
文件。
3360 close(3) = 0
文件的更下方显示,使用close()
系统调用关闭了这个文件描述符。这个信息很有用,因为它告诉我们我们能够读取config.yml
文件,而我们的问题与配置文件的权限无关:
3360 open("/opt/myapp/logs/debug.out", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
3360 lseek(3, 0, SEEK_END) = 1711
3360 fstat(3, {st_mode=S_IFREG|0664, st_size=1711, ...}) = 0
3360 fstat(3, {st_mode=S_IFREG|0664, st_size=1711, ...}) = 0
3360 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd0528df000
3360 write(1, "- - - - - - - - - - - - - - - - "..., 52) = 52
如果我们继续,我们还可以看到我们的配置也在生效,因为进程已经使用open()
调用打开了debug.out
文件进行写入,并使用write()
调用写入了它。
对于有许多日志文件的应用程序,上述的系统调用等可以用于识别可能不太明显的日志消息。
在浏览系统调用时,您可以大致了解生成消息的上下文以及可能的原因。这个上下文可能会根据问题的严重程度非常有用。
3360 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 4
3360 bind(4, {sa_family=AF_INET, sin_port=htons(25), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)
3360 open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 5
3360 fstat(5, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
3360 write(1, "Starting service: [Failed]\n", 27) = 27
3360 write(3, "Configuration file processed\r\n--"..., 86) = 86
3360 close(3) = 0
说到上下文,前面的系统调用明确解释了我们的问题,一个系统调用。虽然strace
文件包含了许多返回错误的系统调用,但其中大部分都像下面这样:
3360 stat("/usr/lib64/python2.7/encodings/ascii", 0x7fff8ef0d670) = -1 ENOENT (No such file or directory)
这是相当常见的,因为它只是意味着进程尝试访问一个不存在的文件。然而,在跟踪文件中,有一个错误比其他的更显眼:
3360 bind(4, {sa_family=AF_INET, sin_port=htons(25), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)
前面的系统调用bind()
是一个绑定套接字的系统调用。前面的例子似乎是在绑定网络套接字。如果我们回想一下我们的配置文件,我们知道指定了端口25
:
# cat /opt/myapp/conf/config.yml
port: 25
在系统调用中,我们可以看到字符串sin_port=htons(25)
,这可能意味着这个绑定系统调用正在尝试绑定到端口25
。从提供的返回值中,我们可以看到bind()
调用收到了一个错误。该错误的消息表明“地址已经在使用”。
由于我们知道应用程序配置为以某种方式利用端口25
,并且我们可以看到一个bind()
系统调用,因此可以推断出这个应用程序可能之所以没有启动,只是因为端口25
已经被另一个进程使用,这在这一点上是我们的新假设。
解决冲突
正如您在网络章节中学到的,我们可以通过快速的netstat
命令来验证进程是否使用端口25
:
# netstat -nap | grep :25
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1588/master
tcp6 0 0 ::1:25 :::* LISTEN 1588/master
当我们以 root 用户身份运行netstat
并添加-p
标志时,该命令将包括每个 LISTEN-ing 套接字的进程 ID 和进程名称。从中,我们可以看到端口25
实际上正在被使用,而进程 1588 是正在监听的进程。
为了更好地了解这个进程是什么,我们可以再次利用ps
命令:
# ps -elf | grep 1588
5 S root 1588 1 0 80 0 - 22924 ep_pol 13:53 ? 00:00:00 /usr/libexec/postfix/master -w
4 S postfix 1616 1588 0 80 0 - 22967 ep_pol 13:53 ? 00:00:00 qmgr -l -t unix -u
4 S postfix 3504 1588 0 80 0 - 22950 ep_pol 20:36 ? 00:00:00 pickup -l -t unix -u
看起来postfix
服务是在端口25
上监听,这并不奇怪,因为这个端口通常用于 SMTP 通信,而 postfix 是一个电子邮件服务。
现在的问题是,后缀应该在这个端口上监听,还是应用程序?不幸的是,对于这个问题没有简单的答案,因为它确实取决于系统和它们正在做什么。
为了这个练习,我们将假设答案是自定义应用程序应该使用端口25
,而后缀不应该运行。
为了阻止后缀在端口25
上监听,我们将首先使用systemctl
命令停止后缀:
# systemctl stop postfix
这将停止后缀服务,下一个命令将禁止它在下次重新启动时再次启动:
# systemctl disable postfix
rm '/etc/systemd/system/multi-user.target.wants/postfix.service'
禁用后缀服务是解决此问题的重要步骤。目前,我们认为问题是由自定义应用程序和后缀之间的端口冲突引起的。如果我们不禁用后缀服务,下次系统重新启动时它将被重新启动。这将阻止自定义应用程序的启动。
虽然这可能看起来很基础,但我想强调这一步的重要性,因为在许多情况下,我曾见过一个问题反复发生,只是因为第一次解决它的人没有禁用一个服务。
如果我们运行systemctl
状态命令,我们现在可以看到后缀服务已停止并禁用:
# systemctl status postfix
postfix.service - Postfix Mail Transport Agent
Loaded: loaded (/usr/lib/systemd/system/postfix.service; disabled)
Active: inactive (dead)
Jun 09 04:05:42 blog.example.com systemd[1]: Starting Postfix Mail Transport Agent...
Jun 09 04:05:43 blog.example.com postfix/master[1588]: daemon started -- version 2.10.1, configuration /etc/postfix
Jun 09 04:05:43 blog.example.com systemd[1]: Started Postfix Mail Transport Agent.
Jun 09 21:14:14 blog.example.com systemd[1]: Stopping Postfix Mail Transport Agent...
Jun 09 21:14:14 blog.example.com systemd[1]: Stopped Postfix Mail Transport Agent.
通过停止postfix
服务,我们现在可以再次启动应用程序,看看问题是否已解决。
$ ./start.sh
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting service: [Success]
- - - - - - - - - - - - - - - - - - - - - - - - -
Proccessed 5 messages
Proccessed 5 messages
Proccessed 5 messages
看起来问题实际上是通过停止postfix
服务解决的。我们可以通过启动过程中打印的[Success]
消息来看到这一点。如果我们再次运行lsof
命令,也可以看到这一点:
# lsof +D /opt/myapp/
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 3332 vagrant cwd DIR 253,1 53 25264 /opt/myapp
start.sh 3585 vagrant cwd DIR 253,1 53 25264 /opt/myapp
start.sh 3585 vagrant 255r REG 253,1 111 25304 /opt/myapp/start.sh
applicati 3588 root cwd DIR 253,1 53 25264 /opt/myapp
applicati 3588 root txt REG 253,1 36196 68112463 /opt/myapp/bin/application
applicati 3588 root 3w REG 253,1 1797 134803515 /opt/myapp/logs/debug.out
现在应用程序正在运行,我们可以看到几个进程在/opt/myapp
目录中有打开的项目。我们还可以看到其中一个进程是带有进程 ID3588
的应用程序命令。为了更好地了解应用程序正在做什么,我们可以再次运行lsof
,但这次我们只搜索进程 ID3588
打开的文件:
# lsof -p 3588
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
applicati 3588 root cwd DIR 253,1 53 25264 /opt/myapp
applicati 3588 root rtd DIR 253,1 4096 128 /
applicati 3588 root txt REG 253,1 36196 68112463 /opt/myapp/bin/application
applicati 3588 root mem REG 253,1 160240 134296681 /usr/lib64/ld-2.17.so
applicati 3588 root 0u CHR 136,2 0t0 5 /dev/pts/2
applicati 3588 root 1u CHR 136,2 0t0 5 /dev/pts/2
applicati 3588 root 2u CHR 136,2 0t0 5 /dev/pts/2
applicati 3588 root 3w REG 253,1 1797 134803515 /opt/myapp/logs/debug.out
applicati 3588 root 4u sock 0,6 0t0 38488 protocol: TCP
-p
(进程)标志将lsof
输出过滤到特定进程。在这种情况下,我们将输出限制为刚刚启动的自定义应用程序。
applicati 3588 root 4u sock 0,6 0t0 38488 protocol: TCP
在最后一行中,我们可以看到应用程序有一个 TCP 套接字打开。根据应用程序的状态消息和lsof
的结果,可以非常肯定地说应用程序已经启动并且启动正确。
总结
我们遇到了一个应用程序问题,并使用了常见的 Linux 工具,如lsof
和strace
来找到根本原因,即端口冲突。更重要的是,我们在没有关于应用程序或其尝试执行的任务的先前知识的情况下做到了这一点。
通过本章的示例,我们可以很容易地看到,拥有基本 Linux 工具的访问权限和知识,再加上对故障排除过程的理解,可以使您能够解决几乎任何问题,无论是应用程序问题还是系统问题。
在下一章中,我们将研究 Linux 用户和内核限制,以及它们有时可能会引起问题。
第十章:理解 Linux 用户和内核限制
在上一章中,我们使用了lsof
和strace
等工具来确定应用程序问题的根本原因。
在本章中,我们将再次确定应用程序相关问题的根本原因。但是,我们还将专注于学习和理解 Linux 用户和内核的限制。
一个报告的问题
就像上一章专注于自定义应用程序的问题一样,今天的问题也来自同一个自定义应用程序。
今天,我们将处理应用支持团队报告的一个问题。然而,这一次支持团队能够为我们提供相当多的信息。
我们在第九章中处理的应用程序,使用系统工具来排除应用程序问题,现在通过端口 25
接收消息并将其存储在队列目录中。定期会运行一个作业来处理这些排队的消息,但是这个作业似乎不再工作。
应用支持团队已经注意到队列中积压了大量消息。然而,尽管他们已经尽可能地排除了问题,但他们卡住了,需要我们的帮助。
为什么作业失败了?
由于报告的问题是定时作业不起作用,我们应该首先关注作业本身。在这种情况下,我们有应用支持团队可以回答任何问题。所以,让我们再多了解一些关于这个作业的细节。
背景问题
以下是一系列快速问题,应该能够为您提供额外的信息:
-
作业是如何运行的?
-
如果需要,我们可以手动运行作业吗?
-
这个作业执行什么?
这三个问题可能看起来很基础,但它们很重要。让我们首先看一下应用团队提供的答案:
- 作业是如何运行的?
作业是作为 cron 作业执行的。
- 如果需要,我们可以手动运行作业吗?
是的,可以根据需要手动执行作业。
- 这个作业执行什么?
作业以 vagrant 用户身份执行/opt/myapp/bin/processor 命令。
前面的三个问题很重要,因为它们将为我们节省大量的故障排除时间。第一个问题关注作业是如何执行的。由于报告的问题是作业不起作用,我们还不知道问题是因为作业没有运行还是作业正在执行但由于某种原因失败。
第一个问题的答案告诉我们,这个作业是由在 Linux 上运行的cron 守护程序crond
执行的。这很有用,因为我们可以使用这些信息来确定作业是否正在执行。一般来说,有很多方法可以执行定时作业。有时执行定时作业的软件在不同的系统上运行,有时在同一个本地系统上运行。
在这种情况下,作业是由crond
在同一台服务器上执行的。
第二个问题也很重要。就像我们在上一章中需要手动启动应用程序一样,我们可能也需要对这个报告的问题执行这个故障排除步骤。根据答案,似乎我们可以根据需要多次执行这个命令。
第三个问题很有用,因为它不仅告诉我们正在执行哪个命令,还告诉我们要注意哪个作业。cron 作业是一种非常常见的调度任务的方法。一个系统通常会有许多已调度的 cron 作业。
cron 作业是否在运行?
由于我们知道作业是由crond
执行的,我们应该首先检查作业是否正在执行。为此,我们可以在相关服务器上检查 cron 日志。例如,考虑以下日志:
# ls -la /var/log/cron*
-rw-r--r--. 1 root root 30792 Jun 10 18:05 /var/log/cron
-rw-r--r--. 1 root root 28261 May 18 03:41 /var/log/cron-20150518
-rw-r--r--. 1 root root 6152 May 24 21:12 /var/log/cron-20150524
-rw-r--r--. 1 root root 42565 Jun 1 15:50 /var/log/cron-20150601
-rw-r--r--. 1 root root 18286 Jun 7 16:22 /var/log/cron-20150607
具体来说,在基于 Red Hat 的 Linux 系统上,我们可以检查/var/log/cron
日志文件。我在前一句中指定了“基于 Red Hat 的”是因为在非 Red Hat 系统上,cron 日志可能位于不同的日志文件中。例如,基于 Debian 的系统默认为/var/log/syslog
。
如果我们不知道哪个日志文件包含 cron 日志,有一个简单的技巧可以找到它。只需运行以下命令行:
# grep -ic cron /var/log/* | grep -v :0
/var/log/cron:400
/var/log/cron-20150518:379
/var/log/cron-20150524:86
/var/log/cron-20150601:590
/var/log/cron-20150607:248
/var/log/messages:1
/var/log/secure:1
前面的命令将使用grep
在/var/log
中的所有日志文件中搜索字符串cron
。该命令还将搜索Cron
、CRON
、cRon
等,因为我们在grep
命令中添加了-i
(不区分大小写)标志。这告诉grep
在不区分大小写的模式下搜索。基本上,这意味着任何匹配单词cron
的地方都会被找到,即使单词是大写或混合大小写。我们还在grep
命令中添加了-c
(计数)标志,这会导致它计算它找到的实例数:
/var/log/cron:400
如果我们看第一个结果,我们可以看到grep
在/var/log/cron
中找到了 400 个“cron”单词的实例。
最后,我们将结果重定向到另一个带有-v
标志和:0
的grep
命令。这个grep
将获取第一次执行的结果,并省略(-v)任何包含字符串:0
的行。这对于将结果限制为只有包含其中的cron
字符串的文件非常有用。
从前面的结果中,我们可以看到文件/var/log/cron
中包含了最多的“cron”单词实例。这一事实本身就是/var/log/cron
是crond
守护程序的日志文件的一个很好的指示。
既然我们知道哪个日志文件包含了我们正在寻找的日志消息,我们可以查看该日志文件的内容。由于这个日志文件非常大,我们将使用less
命令来读取这个文件:
# less /var/log/cron
由于这个日志中包含了相当多的信息,我们只会关注能帮助解释问题的日志条目。以下部分是一组有趣的日志消息,应该能回答我们的作业是否正在运行:
Jun 10 18:01:01 localhost CROND[2033]: (root) CMD (run-parts /etc/cron.hourly)
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2042]: finished 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0yum-hourly.cron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2048]: finished 0yum-hourly.cron
Jun 10 18:05:01 localhost CROND[2053]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
Jun 10 18:10:01 localhost CROND[2086]: (root) CMD (/usr/lib64/sa/sa1 1 1)
Jun 10 18:10:01 localhost CROND[2087]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
Jun 10 18:15:01 localhost CROND[2137]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
Jun 10 18:20:01 localhost CROND[2147]: (root) CMD (/usr/lib64/sa/sa1 1 1)
前面的日志消息显示了相当多的行。让我们分解日志以更好地理解正在执行的内容。考虑以下行:
Jun 10 18:01:01 localhost CROND[2033]: (root) CMD (run-parts /etc/cron.hourly)
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2042]: finished 0anacron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0yum-hourly.cron
Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2048]: finished 0yum-hourly.cron
前几行似乎不是我们正在寻找的作业,而是cron.hourly
作业。
在 Linux 系统上,有多种方法可以指定 cron 作业。在 RHEL 系统上,/etc/
目录下有几个以cron
开头的目录:
# ls -laF /etc/ | grep cron
-rw-------. 1 root root 541 Jun 9 2014 anacrontab
drwxr-xr-x. 2 root root 34 Jan 23 15:43 cron.d/
drwxr-xr-x. 2 root root 62 Jul 22 2014 cron.daily/
-rw-------. 1 root root 0 Jun 9 2014 cron.deny
drwxr-xr-x. 2 root root 44 Jul 22 2014 cron.hourly/
drwxr-xr-x. 2 root root 6 Jun 9 2014 cron.monthly/
-rw-r--r--. 1 root root 451 Jun 9 2014 crontab
drwxr-xr-x. 2 root root 6 Jun 9 2014 cron.weekly/
cron.daily
、cron.hourly
、cron.monthly
和cron.weekly
目录都是可以包含脚本的目录。这些脚本将按照目录名称中指定的时间运行。
例如,让我们看一下/etc/cron.hourly/0yum-hourly.cron
:
# cat /etc/cron.hourly/0yum-hourly.cron
#!/bin/bash
# Only run if this flag is set. The flag is created by the yum-cron init
# script when the service is started -- this allows one to use chkconfig and
# the standard "service stop|start" commands to enable or disable yum-cron.
if [[ ! -f /var/lock/subsys/yum-cron ]]; then
exit 0
fi
# Action!
exec /usr/sbin/yum-cron /etc/yum/yum-cron-hourly.conf
前面的文件是一个简单的bash
脚本,crond
守护程序将每小时执行一次,因为它在cron.hourly
目录中。一般来说,这些目录中包含的脚本是由系统服务放在那里的。不过,这些目录也对系统管理员开放,可以放置他们自己的脚本。
用户 crontabs
如果我们继续查看日志文件,我们可以看到一个与我们自定义作业相关的条目:
Jun 10 18:10:01 localhost CROND[2087]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
这一行显示了应用支持团队引用的processor
命令。这一行必须是应用支持团队遇到问题的作业。日志条目告诉我们很多有用的信息。首先,它为我们提供了传递给这个作业的命令行选项:
/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null
它还告诉我们作业是以vagrant
身份执行的。不过,这个日志条目告诉我们最重要的是作业正在执行。
既然我们知道作业正在执行,我们应该验证作业是否成功。为了做到这一点,我们将采取一种简单的方法,手动执行作业:
$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Traceback (most recent call last):
File "app.py", line 28, in init app (app.c:1488)
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'
我们应该从 cron 任务的末尾省略> /dev/null
,因为这将把输出重定向到/dev/null
。这是一种常见的丢弃 cron 作业输出的方法。对于此手动执行,我们可以利用输出来帮助解决问题。
一旦执行,作业似乎会失败。它不仅失败了,而且还产生了一个错误消息:
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'
这个错误很有趣,因为它似乎表明应用程序打开了太多文件。这有什么关系呢?
了解用户限制
在 Linux 系统上,每个进程都受到限制。这些限制是为了防止进程使用太多的系统资源。
虽然这些限制适用于每个用户,但是可以为每个用户设置不同的限制。要检查vagrant
用户默认设置的限制,我们可以使用ulimit
命令:
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 3825
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 3825
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
当我们执行ulimit
命令时,我们是以 vagrant 用户的身份执行的。这很重要,因为当我们以任何其他用户(包括 root)的身份运行ulimit
命令时,输出将是该用户的限制。
如果我们查看ulimit
命令的输出,我们可以看到有很多可以设置的限制。
文件大小限制
让我们来看一下并分解一些关键限制:
file size (blocks, -f) unlimited
第一个有趣的项目是文件大小
限制。这个限制将限制用户可以创建的文件的大小。vagrant 用户的当前设置是无限制
,但如果我们将这个值设置为一个较小的数字会发生什么呢?
我们可以通过执行ulimit -f
,然后跟上要限制文件的块数来做到这一点。例如,考虑以下命令行:
$ ulimit -f 10
将值设置为10
后,我们可以通过再次运行ulimit -f
来验证它是否生效,但这次不带值:
$ ulimit -f
10
现在我们的限制设置为 10 个块,让我们尝试使用dd
命令创建一个 500MB 的文件:
$ dd if=/dev/zero of=/var/tmp/bigfile bs=1M count=500
File size limit exceeded
关于 Linux 用户限制的一个好处是通常提供的错误是不言自明的。我们可以从前面的输出中看到,dd
命令不仅无法创建文件,还收到了一个错误,指出文件大小限制已超出。
最大用户进程限制
另一个有趣的限制是最大进程
限制:
max user processes (-u) 3825
这个限制防止用户一次运行太多的进程。这是一个非常有用和有趣的限制,因为它可以轻松地防止一个恶意应用程序接管系统。
这也可能是您经常会遇到的限制。这对于启动许多子进程或线程的应用程序尤其如此。要查看此限制如何工作,我们可以将设置更改为10
:
$ ulimit -u 10
$ ulimit -u
10
与文件大小限制一样,我们可以使用ulimit
命令修改进程限制。但这次,我们使用-u
标志。每个用户限制都有自己独特的标志与ulimit
命令。我们可以在ulimit -a
的输出中看到这些标志,当然,每个标志都在ulimit
的 man 页面中引用。
既然我们已经将我们的进程限制为10
,我们可以通过运行一个命令来看到限制的执行:
$ man ulimit
man: fork failed: Resource temporarily unavailable
通过 SSH 登录 vagrant 用户,我们已经在使用多个进程。很容易遇到10
个进程的限制,因为我们运行的任何新命令都会超出我们的登录限制。
从前面的例子中,我们可以看到当执行man
命令时,它无法启动子进程,因此返回了一个错误,指出资源暂时不可用
。
打开文件限制
我想要探索的最后一个有趣的用户限制是打开文件
限制:
open files (-n) 1024
打开文件
限制将限制进程打开的文件数不超过定义的数量。此限制可用于防止进程一次打开太多文件。当防止应用程序占用系统资源过多时,这是一种很有用的方法。
像其他限制一样,让我们看看当我们将这个限制减少到一个非常不合理的数字时会发生什么:
$ ulimit -n 2
$ ls
-bash: start_pipeline: pgrp pipe: Too many open files
ls: error while loading shared libraries: libselinux.so.1: cannot open shared object file: Error 24
与其他示例一样,我们在这种情况下收到了一个错误,即Too many open files
。但是,这个错误看起来很熟悉。如果我们回顾一下从我们的计划作业收到的错误,我们就会明白为什么。
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'
将我们的最大打开文件数设置为2
后,ls
命令产生了一个错误;这个错误与我们的应用程序之前执行时收到的完全相同的错误消息。
这是否意味着我们的应用程序试图打开的文件比我们的系统配置允许的要多?这是一个很有可能的情况。
更改用户限制
由于我们怀疑open files
限制阻止了应用程序的执行,我们可以将其限制设置为更高的值。但是,这并不像执行ulimit -n
那样简单;执行时得到的输出如下:
$ ulimit -n
1024
$ ulimit -n 5000
-bash: ulimit: open files: cannot modify limit: Operation not permitted
$ ulimit -n 4096
$ ulimit -n
4096
在我们的示例系统上,默认情况下,vagrant 用户被允许将open files
限制提高到4096
。从前面的错误中我们可以看到,任何更高的值都被拒绝;但是像大多数 Linux 一样,我们可以改变这一点。
limits.conf 文件
我们一直在使用和修改的用户限制是 Linux 的 PAM 系统的一部分。PAM 或可插拔认证模块是一个提供模块化认证系统的系统。
例如,如果我们的系统要使用 LDAP 进行身份验证,pam_ldap.so
库将用于提供此功能。但是,由于我们的系统使用本地用户进行身份验证,因此pam_localuser.so
库处理用户身份验证。
如果我们阅读/etc/pam.d/system-auth
文件,我们可以验证这一点:
$ cat /etc/pam.d/system-auth
#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth required pam_env.so
auth sufficient pam_unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= 1000 quiet_success
auth required pam_deny.so
account required pam_unix.so
account sufficient pam_localuser.so
account sufficient pam_succeed_if.so uid < 1000 quiet
account required pam_permit.so
password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=
password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password required pam_deny.so
session optional pam_keyinit.so revoke
session required pam_limits.so
-session optional pam_systemd.so
session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session required pam_unix.so
如果我们看一下前面的例子,我们可以看到pam_localuser.so
与account
一起列在第一列:
account sufficient pam_localuser.so
这意味着pam_localuser.so
模块是一个sufficient
模块,允许账户被使用,这基本上意味着如果他们有正确的/etc/passwd
和/etc/shadow
条目,用户就能够登录。
session required pam_limits.so
如果我们看一下前面的行,我们可以看到用户限制是在哪里执行的。这行基本上告诉系统pam_limits.so
模块对所有用户会话都是必需的。这有效地确保了pam_limits.so
模块识别的用户限制在每个用户会话上都得到执行。
这个 PAM 模块的配置位于/etc/security/limits.conf
和/etc/security/limits.d/
中。
$ cat /etc/security/limits.conf
#This file sets the resource limits for the users logged in via PAM.
# - core - limits the core file size (KB)
# - data - max data size (KB)
# - fsize - maximum filesize (KB)
# - memlock - max locked-in-memory address space (KB)
# - nofile - max number of open files
# - rss - max resident set size (KB)
# - stack - max stack size (KB)
# - cpu - max CPU time (MIN)
# - nproc - max number of processes
# - as - address space limit (KB)
# - maxlogins - max number of logins for this user
# - maxsyslogins - max number of logins on the system
# - priority - the priority to run user process with
# - locks - max number of file locks the user can hold
# - sigpending - max number of pending signals
# - msgqueue - max memory used by POSIX message queues (bytes)
# - nice - max nice priority allowed to raise to values: [-20, 19]
# - rtprio - max realtime priority
#
#<domain> <type> <item> <value>
#
#* soft core 0
#* hard rss 10000
#@student hard nproc 20
#@faculty soft nproc 20
#@faculty hard nproc 50
#ftp hard nproc 0
#@student - maxlogins 4
当我们阅读limits.conf
文件时,我们可以看到关于用户限制的相当多有用的信息。
在这个文件中,列出了可用的限制以及该限制强制执行的描述。例如,在前面的命令行中,我们可以看到open files
限制的数量:
# - nofile - max number of open files
从这一行我们可以看到,如果我们想改变用户可用的打开文件数,我们需要使用nofile
类型。除了列出每个限制的作用,limits.conf
文件还包含了为用户和组设置自定义限制的示例:
#ftp hard nproc 0
通过这个例子,我们可以看到我们需要使用什么格式来设置限制;但我们应该将限制设置为多少呢?如果我们回顾一下作业中的错误,我们会发现错误列出了/opt/myapp/queue
目录中的一个文件:
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'
可以肯定地说,应用程序正在尝试打开此目录中的文件。因此,为了确定这个进程需要打开多少文件,让我们通过以下命令行找出这个目录中有多少文件存在:
$ ls -la /opt/myapp/queue/ | wc -l
492304
前面的命令使用ls -la
列出queue/
目录中的所有文件和目录,并将输出重定向到wc -l
。wc
命令将从提供的输出中计算行数(-l
),这基本上意味着在queue/
目录中有 492,304 个文件和/或目录。
鉴于数量很大,我们应该将打开文件
限制数量设置为500000
,足以处理queue/
目录,以防万一再多一点。我们可以通过将以下行附加到limits.conf
文件来实现这一点:
# vi /etc/security/limits.conf
在使用vi
或其他文本编辑器添加我们的行之后,我们可以使用tail
命令验证它是否存在:
$ tail /etc/security/limits.conf
#@student hard nproc 20
#@faculty soft nproc 20
#@faculty hard nproc 50
#ftp hard nproc 0
#@student - maxlogins 4
vagrant soft nofile 100000
vagrant hard nofile 500000
# End of file
更改这些设置并不意味着我们的登录 shell 立即具有500000
的限制。我们的登录会话仍然设置了4096
的限制。
$ ulimit -n
4096
我们还不能将其增加到该值以上。
$ ulimit -n 9000
-bash: ulimit: open files: cannot modify limit: Operation not permitted
为了使我们的更改生效,我们必须再次登录到我们的用户。
正如我们之前讨论的,这些限制是由 PAM 设置的,在我们的 shell 会话登录期间应用。由于这些限制是在登录期间设置的,所以我们仍然受到上次登录时采用的先前数值的限制。
要获取新的限制,我们必须注销并重新登录(或生成一个新的登录会话)。在我们的例子中,我们将注销我们的 shell 并重新登录。
$ ulimit -n
100000
$ ulimit -n 501000
-bash: ulimit: open files: cannot modify limit: Operation not permitted
$ ulimit -n 500000
$ ulimit -n
500000
如果我们看一下前面的命令行,我们会发现一些非常有趣的东西。
当我们这次登录时,我们的文件限制数量被设置为100000
,这恰好是我们在limits.conf
文件中设置的soft
限制。这是因为soft
限制是每个会话默认设置的限制。
hard
限制是该用户可以设置的高于soft
限制的最高值。我们可以在前面的例子中看到这一点,因为我们能够将nofile
限制设置为500000
,但不能设置为501000
。
未来保护定时作业
我们将soft
限制设置为100000
的原因是因为我们计划在未来处理类似的情况。将soft
限制设置为100000
,运行这个定时作业的 cron 作业将被限制为 100,000 个打开文件。然而,由于hard
限制设置为500000
,某人可以在他们的登录会话中手动运行具有更高限制的作业。
只要queue
目录中的文件数量不超过 500,000,就不再需要任何人编辑/etc/security/limits.conf
文件。
再次运行作业
现在我们的限制已经增加,我们可以尝试再次运行作业。
$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Traceback (most recent call last):
File "app.py", line 28, in init app (app.c:1488)
IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt'
我们再次收到了一个错误。然而,这次错误略有不同。
在上一次运行中,我们收到了以下错误。
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'
然而,这一次我们收到了这个错误。
IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt'
这种差异非常微妙,但在第二次运行时,我们的错误说明了系统中打开的文件太多,而我们第一次运行时没有包括in system
。这是因为我们遇到了不同类型的限制,不是用户限制,而是系统限制。
内核可调参数
Linux 内核本身也可以对系统设置限制。这些限制是基于内核参数定义的。其中一些参数是静态的,不能在运行时更改;而其他一些可以。当内核参数可以在运行时更改时,这被称为可调参数。
我们可以使用sysctl
命令查看静态和可调内核参数及其当前值。
# sysctl -a | head
abi.vsyscall32 = 1
crypto.fips_enabled = 0
debug.exception-trace = 1
debug.kprobes-optimization = 1
dev.hpet.max-user-freq = 64
dev.mac_hid.mouse_button2_keycode = 97
dev.mac_hid.mouse_button3_keycode = 100
dev.mac_hid.mouse_button_emulation = 0
dev.parport.default.spintime = 500
dev.parport.default.timeslice = 200
由于有许多参数可用,我使用head
命令将输出限制为前 10 个。我们之前收到的错误提到了系统上的限制,这表明我们可能遇到了内核本身施加的限制。
唯一的问题是我们如何知道哪一个?最快的答案当然是搜索谷歌。由于有很多内核参数(我们正在使用的系统上有 800 多个),简单地阅读sysctl –a
的输出并找到正确的参数是困难的。
一个更现实的方法是简单地搜索我们要修改的参数类型。我们的情况下,一个例子搜索可能是Linux 参数最大打开文件数
。如果我们进行这样的搜索,很可能会找到参数以及如何修改它。然而,如果谷歌不是一个选择,还有另一种方法。
一般来说,内核参数的名称描述了参数控制的内容。
例如,如果我们要查找禁用 IPv6 的内核参数,我们首先会搜索net
字符串,如网络:
# sysctl -a | grep -c net
556
然而,这仍然返回了大量结果。在这些结果中,我们可以看到字符串ipv6
。
# sysctl -a | grep -c ipv6
233
尽管如此,还是有相当多的结果;但是,如果我们添加一个搜索字符串disable
,我们会得到以下输出:
# sysctl -a | grep ipv6 | grep disable
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.enp0s3.disable_ipv6 = 0
net.ipv6.conf.enp0s8.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0
我们终于可以缩小可能的参数范围。但是,我们还不完全知道这些参数的作用。至少目前还不知道。
如果我们通过/usr/share/doc
进行快速搜索,可能会找到一些解释这些设置作用的文档。我们可以通过使用grep
在该目录中进行递归搜索来快速完成这个过程。为了保持输出简洁,我们可以添加-l
(列出文件),这会导致grep
只列出包含所需字符串的文件名:
# grep -rl net.ipv6 /usr/share/doc/
/usr/share/doc/grub2-tools-2.02/grub.html
在基于 Red Hat 的 Linux 系统中,/usr/share/doc
目录用于系统手册之外的额外文档。如果我们只能使用系统本身的文档,/usr/share/doc
目录是首要检查的地方之一。
查找打开文件的内核参数
由于我们喜欢用较困难的方式执行任务,我们将尝试识别潜在限制我们的内核参数,而不是在 Google 上搜索。这样做的第一步是在sysctl
输出中搜索字符串file
。
我们搜索file
的原因是因为我们遇到了文件数量的限制。虽然这可能不会提供我们要识别的确切参数,但至少搜索会让我们有个起点:
# sysctl -a | grep file
fs.file-max = 48582
fs.file-nr = 1088 0 48582
fs.xfs.filestream_centisecs = 3000
事实上,搜索file
可能实际上是一个非常好的选择。仅仅根据参数的名称,对我们可能感兴趣的两个参数是fs.file-max
和fs.file-nr
。在这一点上,我们不知道哪一个控制打开文件的数量,或者这两个参数是否有任何作用。
要了解更多信息,我们可以搜索doc
目录。
# grep -r fs.file- /usr/share/doc/
/usr/share/doc/postfix-2.10.1/README_FILES/TUNING_README:
fs.file-max=16384
看起来一个名为TUNING_README
的文档,位于 Postfix 服务文档中,提到了我们至少一个值的参考。让我们查看一下文件,看看这个文档对这个内核参数有什么说法:
* Configure the kernel for more open files and sockets. The details are
extremely system dependent and change with the operating system version. Be
sure to verify the following information with your system tuning guide:
o Linux kernel parameters can be specified in /etc/sysctl.conf or changed
with sysctl commands:
fs.file-max=16384
kernel.threads-max=2048
如果我们阅读文件中列出我们的内核参数周围的内容,我们会发现它明确指出了配置内核以获得更多打开文件和套接字的参数。
这个文档列出了两个内核参数,允许更多的打开文件和套接字。第一个被称为fs.file-max
,这也是我们在sysctl
搜索中识别出的一个。第二个被称为kernel.threads-max
,这是相当新的。
仅仅根据名称,似乎我们想要修改的可调参数是fs.file-max
参数。让我们看一下它的当前值:
# sysctl fs.file-max
fs.file-max = 48582
我们可以通过执行sysctl
命令,后跟参数名称(如前面的命令行所示)来列出此参数的当前值。这将简单地显示当前定义的值;看起来设置为48582
,远低于我们当前的用户限制。
提示
在前面的例子中,我们在一个 postfix 文档中找到了这个参数。虽然这可能很好,但并不准确。如果您经常需要在本地搜索内核参数,最好安装kernel-doc
软件包。kernel-doc
软件包包含了大量信息,特别是关于可调参数的信息。
更改内核可调参数
由于我们认为fs.file-max
参数控制系统可以打开的最大文件数,我们应该更改这个值以允许我们的作业运行。
像大多数 Linux 上的系统配置项一样,有更改此值的临时和重新启动的选项。之前我们将limits.conf
文件设置为允许 vagrant 用户能够以软
限制打开 100,000 个文件,以硬
限制打开 500,000 个文件。问题是我们是否希望这个用户能够正常操作打开 500,000 个文件?还是应该是一次性任务来纠正我们目前面临的问题?
答案很简单:这取决于情况!
如果我们看一下我们目前正在处理的情况,所讨论的工作已经有一段时间没有运行了。因此,队列中积压了大量的消息。但是这些并不是正常的情况。
早些时候,当我们将用户限制设置为 100,000 个文件时,我们这样做是因为这对于这个作业来说是一个相当合适的值。考虑到这一点,我们还应该将内核参数设置为略高于100000
的值,但不要太高。
对于这种情况和环境,我们将执行两个操作。第一个是配置系统默认允许125,000 个打开文件。第二个是将当前参数设置为525,000 个打开文件,以便成功运行预定的作业。
永久更改可调整的值
由于我们想要将fs.file-max
的值默认更改为125000
,我们需要编辑sysctl.conf
文件。sysctl.conf
文件是一个系统配置文件,允许您为可调整的内核参数指定自定义值。在系统每次重新启动时,该文件都会被读取并应用其中的值。
为了将我们的fs.file-max
值设置为125000
,我们只需将以下行追加到这个文件中:
# vi /etc/sysctl.conf
fs.file-max=125000
现在我们已经添加了我们的自定义值,我们需要告诉系统应用它。
如前所述,sysctl.conf
文件在重新启动时生效,但是我们也可以随时使用sysctl
命令和-p
标志将设置应用到这个文件。
# sysctl -p
fs.file-max = 125000
给定-p
标志后,sysctl
命令将读取并将值应用到指定的文件,或者如果没有指定文件,则应用到/etc/sysctl.conf
。由于我们在-p
标志后没有指定文件,sysctl
命令将应用到/etc/sysctl.conf
中添加的值,并打印修改的值。
让我们通过再次执行sysctl
来验证它是否被正确应用。
# sysctl fs.file-max
fs.file-max = 125000
事实上,似乎值已经被正确应用了,但是将它设置为525000
呢?
临时更改可调整的值
虽然更改/etc/sysctl.conf
到一个更高的值,然后应用并恢复更改可能很简单。但是有一个更简单的方法可以临时更改可调整的值。
当提供-w
选项时,sysctl
命令允许修改可调整的值。为了看到这一点,我们将使用它将fs.file-max
值设置为525000
。
# sysctl -w fs.file-max=525000
fs.file-max = 525000
就像我们应用sysctl.conf
文件的值一样,当我们执行sysctl –w
时,它打印了应用的值。如果我们再次验证它们,我们会看到值被设置为525000
个文件:
# sysctl fs.file-max
fs.file-max = 525000
最后再次运行作业
现在我们已经将 vagrant 用户的打开文件
限制设置为500000
,整个系统设置为525000
。我们可以再次手动执行这个作业,这次应该会成功:
$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Added 492304 to queue
Processing 492304 messages
Processed 492304 messages
这次作业执行时没有提供任何错误!我们可以从作业的输出中看到/opt/myapp/queue
中的所有文件都被处理了。
回顾一下
现在我们已经解决了问题,让我们花点时间看看我们是如何解决这个问题的。
打开文件太多
为了排除我们的问题,我们手动执行了一个预定的 cron 作业。如果我们回顾之前的章节,这是一个复制问题并亲自看到的一个典型例子。
在这种情况下,作业没有执行它应该执行的任务。为了找出原因,我们手动运行了它。
在手动执行期间,我们能够识别出以下错误:
IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt'
这种错误非常常见,是由作业超出用户限制而导致的,该限制阻止单个用户打开太多文件。为了解决这个问题,我们在/etc/security/limits.conf
文件中添加了自定义设置。
这些更改将我们用户的“打开文件”限制默认设置为100000
。我们还允许用户通过hard
设置临时将“打开文件”限制增加到500000
:
IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt'
修改这些限制后,我们再次执行了作业,并遇到了类似但不同的错误。
这次,“打开文件”限制是系统本身施加的,这种情况下对系统施加了全局限制,即 48000 个打开文件。
为了解决这个问题,我们在/etc/sysctl.conf
文件中设置了永久设置为125000
,并临时将该值更改为525000
。
从那时起,我们能够手动执行作业。然而,自从我们改变了默认限制以来,我们还为这个作业提供了更多资源以正常执行。只要没有超过 10 万个文件的积压,这个作业将来都应该能够正常执行。
稍微整理一下。
说到正常执行,为了减少内核对打开文件的限制,我们可以再次执行sysctl
命令,并加上-p
选项。这将把值重置为/etc/sysctl.conf
文件中定义的值。
# sysctl -p
fs.file-max = 125000
这种方法的一个注意事项是,sysctl -p
只会重置/etc/sysctl.conf
中指定的值;默认情况下只包含少量可调整的值。如果修改了/etc/sysctl.conf
中未指定的值,sysctl -p
方法将不会将该值重置为默认值。
总结
在本章中,我们对 Linux 中强制执行的内核和用户限制非常熟悉。这些设置非常有用,因为任何利用许多资源的应用程序最终都会遇到其中之一。
在下一章中,我们将专注于一个非常常见但非常棘手的问题。我们将专注于故障排除和确定系统内存耗尽的原因。当系统内存耗尽时,会有很多后果,比如应用程序进程被终止。
第十一章:从常见故障中恢复
在上一章中,我们探讨了 Linux 服务器上存在的用户和系统限制。我们看了看现有的限制以及如何更改应用程序所需的默认值。
在本章中,我们将运用我们的故障排除技能来处理一个资源耗尽的系统。
报告的问题
今天的章节,就像其他章节一样,将以某人报告问题开始。报告的问题是 Apache 不再在服务器上运行,该服务器为公司的博客blog.example.com
提供服务。
报告问题的另一位系统管理员解释说,有人报告博客宕机,当他登录服务器时,他发现 Apache 不再运行。在那时,我们的同行不确定接下来该怎么做,并请求我们的帮助。
Apache 真的宕机了吗?
当报告某个服务宕机时,我们应该做的第一件事是验证它是否真的宕机。这本质上是我们故障排除过程中的为自己复制步骤。对于 Apache 这样的服务,我们也应该相当快地验证它是否真的宕机。
根据我的经验,我经常被告知服务宕机,而实际上并非如此。服务器可能出现问题,但技术上并没有宕机。上线或宕机的区别会改变我们需要执行的故障排除步骤。
因此,我总是首先执行的步骤是验证服务是否真的宕机,还是服务只是没有响应。
为了验证 Apache 是否真的宕机,我们将使用ps
命令。正如我们之前学到的,这个命令将打印当前运行的进程列表。我们将将这个输出重定向到grep
命令,以检查是否有httpd
(Apache)服务的实例在运行:
# ps -elf | grep http
0 S root 2645 1974 0 80 0 - 28160 pipe_w 21:45 pts/0 00:00:00 grep --color=auto http
从上述ps
命令的输出中,我们可以看到没有以httpd
命名的进程在运行。在正常情况下,我们期望至少看到几行类似于以下示例的内容:
5 D apache 2383 1 0 80 0 - 115279 conges 20:58 ? 00:00:04 /usr/sbin/httpd -DFOREGROUND
由于在进程列表中找不到httpd
进程,我们可以得出结论,Apache 实际上在这个系统上宕机了。现在的问题是,为什么?
为什么它宕机了?
在简单地启动 Apache 服务解决问题之前,我们将首先弄清楚为什么 Apache 服务没有运行。这是一个称为根本原因分析(RCA)的过程,这是一个正式的过程,用于了解最初导致问题的原因。
在下一章中,我们将对这个过程非常熟悉。在本章中,我们将保持简单,专注于为什么 Apache 没有运行。
我们要查看的第一个地方是/var/log/httpd
中的 Apache 日志。在之前的章节中,我们在排除其他与 Web 服务器相关的问题时了解了这些日志。正如我们在之前的章节中看到的,应用程序和服务日志在确定服务发生了什么事情方面非常有帮助。
由于 Apache 不再运行,我们对最近发生的事件更感兴趣。如果服务遇到致命错误或被停止,应该在日志文件的末尾显示相应的消息。
因为我们只对最近发生的事件感兴趣,所以我们将使用tail
命令显示error_log
文件的最后 10 行。error_log
文件是第一个要检查的日志,因为它是发生异常的最可能地方:
# tail /var/log/httpd/error_log
[Sun Jun 21 20:51:32.889455 2015] [mpm_prefork:notice] [pid 2218] AH00163: Apache/2.4.6 PHP/5.4.16 configured -- resuming normal operations
[Sun Jun 21 20:51:32.889690 2015] [core:notice] [pid 2218] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'
[Sun Jun 21 20:51:33.892170 2015] [mpm_prefork:error] [pid 2218] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting
[Sun Jun 21 20:53:42.577787 2015] [mpm_prefork:notice] [pid 2218] AH00170: caught SIGWINCH, shutting down gracefully [Sun Jun 21 20:53:44.677885 2015] [core:notice] [pid 2249] SELinux policy enabled; httpd running as context system_u:system_r:httpd_t:s0
[Sun Jun 21 20:53:44.678919 2015] [suexec:notice] [pid 2249] AH01232: suEXEC mechanism enabled (wrapper: /usr/sbin/suexec)
[Sun Jun 21 20:53:44.703088 2015] [auth_digest:notice] [pid 2249] AH01757: generating secret for digest authentication ...
[Sun Jun 21 20:53:44.704046 2015] [lbmethod_heartbeat:notice] [pid 2249] AH02282: No slotmem from mod_heartmonitor
[Sun Jun 21 20:53:44.732504 2015] [mpm_prefork:notice] [pid 2249] AH00163: Apache/2.4.6 PHP/5.4.16 configured -- resuming normal operations
[Sun Jun 21 20:53:44.732568 2015] [core:notice] [pid 2249] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'
从error_log
文件内容中,我们可以看到一些有趣的信息。让我们快速浏览一下一些更具信息量的日志条目。
[Sun Jun 21 20:53:42.577787 2015] [mpm_prefork:notice] [pid 2218] AH00170: caught SIGWINCH, shutting down gracefully
前一行显示 Apache 进程在Sunday, Jun 21
的20:53
被关闭。我们可以看到错误消息清楚地说明了优雅地关闭
。然而,接下来的几行似乎表明 Apache 服务只在2
秒后重新启动:
[Sun Jun 21 20:53:44.677885 2015] [core:notice] [pid 2249] SELinux policy enabled; httpd running as context system_u:system_r:httpd_t:s0
[Sun Jun 21 20:53:44.678919 2015] [suexec:notice] [pid 2249] AH01232: suEXEC mechanism enabled (wrapper: /usr/sbin/suexec)
[Sun Jun 21 20:53:44.703088 2015] [auth_digest:notice] [pid 2249] AH01757: generating secret for digest authentication ...
[Sun Jun 21 20:53:44.704046 2015] [lbmethod_heartbeat:notice] [pid 2249] AH02282: No slotmem from mod_heartmonitor
[Sun Jun 21 20:53:44.732504 2015] [mpm_prefork:notice] [pid 2249] AH00163: Apache/2.4.6 PHP/5.4.16 configured -- resuming normal operations
关机日志条目显示了一个进程 ID 为2218
,而前面的五行显示了一个进程 ID 为2249
。第五行还声明了恢复正常运行
。这四条消息似乎表明 Apache 进程只是重新启动了。很可能,这是 Apache 的优雅重启。
Apache 的优雅重启是在修改其配置时执行的一个相当常见的任务。这是一种在不完全关闭和影响 Web 服务的情况下重新启动 Apache 进程的方法。
[Sun Jun 21 20:53:44.732568 2015] [core:notice] [pid 2249] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'
然而,这 10 行告诉我们最有趣的事情是,Apache 打印的最后一个日志只是一个通知。当 Apache 被优雅地停止时,它会在error_log
文件中记录一条消息,以显示它正在被停止。
由于 Apache 进程不再运行,并且没有日志条目显示它是正常关闭或非正常关闭,我们得出结论,无论 Apache 为什么不运行,它都没有正常关闭。
如果一个人使用apachectl
或systemctl
命令关闭了服务,我们会期望看到类似于之前例子中讨论的消息。由于日志文件的最后一行没有显示关闭消息,我们只能假设这个进程是在异常情况下被终止或终止的。
现在,问题是是什么导致了 Apache 进程以这种异常方式终止?
Apache 发生了什么事情的线索可能在于 systemd 设施,因为 Red Hat Enterprise Linux 7 服务,比如 Apache,已经被迁移到了 systemd。在启动时,systemd
设施会启动任何已经配置好的服务。
当systemd
启动的进程被终止时,这个活动会被systemd
捕获。根据进程终止后发生的情况,我们可以使用systemctl
命令来查看systemd
是否捕获了这个事件:
# systemctl status httpd
httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled)
Active: failed (Result: timeout) since Fri 2015-06-26 21:21:38 UTC; 22min ago
Process: 2521 ExecStop=/bin/kill -WINCH ${MAINPID} (code=exited, status=0/SUCCESS)
Process: 2249 ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND (code=killed, signal=KILL)
Main PID: 2249 (code=killed, signal=KILL)
Status: "Total requests: 1649; Current requests/sec: -1.29; Current traffic: 0 B/sec"
Jun 21 20:53:44 blog.example.com systemd[1]: Started The Apache HTTP Server.
Jun 26 21:12:55 blog.example.com systemd[1]: httpd.service: main process exited, code=killed, status=9/KILL
Jun 26 21:21:20 blog.example.com systemd[1]: httpd.service stopping timed out. Killing.
Jun 26 21:21:38 blog.example.com systemd[1]: Unit httpd.service entered failed state.
systemctl status
命令的输出显示了相当多的信息。由于我们在之前的章节中已经涵盖了这个问题,我将跳过这个输出的大部分,只看一些能告诉我们 Apache 服务发生了什么的部分。
看起来有趣的前两行是:
Process: 2249 ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND (code=killed, signal=KILL)
Main PID: 2249 (code=killed, signal=KILL)
在这两行中,我们可以看到进程 ID 为2249
,这也是我们在error_log
文件中看到的。这是在6 月 21 日星期日
启动的 Apache 实例的进程 ID。我们还可以从这些行中看到,进程2249
被终止了。这似乎表明有人或某物终止了我们的 Apache 服务:
Jun 21 20:53:44 blog.example.com systemd[1]: Started The Apache HTTP Server.
Jun 26 21:12:55 blog.example.com systemd[1]: httpd.service: main process exited, code=killed, status=9/KILL
Jun 26 21:21:20 blog.example.com systemd[1]: httpd.service stopping timed out. Killing.
Jun 26 21:21:38 blog.example.com systemd[1]: Unit httpd.service entered failed state.
如果我们看一下systemctl
状态输出的最后几行,我们可以看到systemd
设施捕获的事件。我们可以看到的第一个事件是 Apache 服务在6 月 21 日 20:53
启动。这并不奇怪,因为它与我们在error_log
中看到的信息相符。
然而,最后三行显示 Apache 进程随后在6 月 26 日 21:21
被终止。不幸的是,这些事件并没有准确显示 Apache 进程被终止的原因或是谁终止了它。它告诉我们的是 Apache 被终止的确切时间。这也表明systemd
设施不太可能停止了 Apache 服务。
那个时候还发生了什么?
由于我们无法从 Apache 日志或systemctl status
中确定原因,我们需要继续挖掘以了解是什么导致了这个服务的停止。
# date
Sun Jun 28 18:32:33 UTC 2015
由于 26 号已经过去了几天,我们有一些有限的地方可以寻找额外的信息。我们可以查看/var/log/messages
日志文件。正如我们在前面的章节中发现的,messages
日志包含了系统中许多不同设施的各种信息。如果有一个地方可以告诉我们那个时候系统发生了什么,那就是那里。
搜索 messages 日志
messages
日志非常庞大,在其中有许多日志条目:
# wc -l /var/log/messages
21683 /var/log/messages
因此,我们需要过滤掉与我们的问题无关或不在我们问题发生时的日志消息。我们可以做的第一件事是搜索日志中 Apache 停止的那一天的消息:June 26
:
# tail -1 /var/log/messages
Jun 28 20:44:01 localhost systemd: Started Session 348 of user vagrant.
从前面提到的tail
命令中,我们可以看到/var/log/messages
文件中的消息格式是日期、主机名、进程,然后是消息。日期字段是一个三个字母的月份,后面跟着日期数字和 24 小时时间戳。
由于我们的问题发生在 6 月 26 日,我们可以搜索这个日志文件中字符串"Jun 26
"的任何实例。这应该提供所有在 26 日写入的消息:
# grep -c "Jun 26" /var/log/messages
17864
显然这仍然是相当多的日志消息,太多了,无法全部阅读。鉴于这个数量,我们需要进一步过滤消息,也许可以按进程来过滤:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort -n | uniq -c | sort -nk 1 | tail
39 Jun 26 journal:
56 Jun 26 NetworkManager:
76 Jun 26 NetworkManager[582]:
76 Jun 26 NetworkManager[588]:
78 Jun 26 NetworkManager[580]:
79 Jun 26 systemd-logind:
110 Jun 26 systemd[1]:
152 Jun 26 NetworkManager[574]:
1684 Jun 26 systemd:
15077 Jun 26 kernel:
上面的代码通常被称为bash一行代码。这通常是一系列命令,它们将它们的输出重定向到另一个命令,以提供一个单独的命令无法执行或生成的功能或输出。在这种情况下,我们有一个一行代码,它显示了 6 月 26 日记录最多的进程。
分解这个有用的一行代码
上面提到的一行代码一开始可能有点复杂,但一旦我们分解这个一行代码,它就变得容易理解了。这是一个有用的一行代码,因为它使得在日志文件中识别趋势变得更容易。
让我们分解这个一行代码,以更好地理解它的作用:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | uniq -c | sort -nk 1 | tail
我们已经知道第一个命令的作用;它只是在/var/log/messages
文件中搜索字符串"Jun 26
"的任何实例。其他命令是我们以前没有涉及过的命令,但它们可能是有用的命令。
cut 命令
这个一行代码中的cut
命令用于读取grep
命令的输出,并只打印每行的特定部分。要理解它是如何工作的,我们应该首先运行在cut
命令结束的一行代码:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
Jun 26 systemd:
前面的cut
命令通过指定分隔符并按该分隔符切割输出来工作。
分隔符是用来将行分解为多个字段的字符;我们可以用-d
标志来指定它。在上面的例子中,-d
标志后面跟着"\
";反斜杠是一个转义字符,后面跟着一个空格。这告诉cut
命令使用一个空格字符作为分隔符。
-f
标志用于指定应该显示的fields
。这些字段是分隔符之间的文本字符串。
例如,让我们看看下面的命令:
$ echo "Apples:Bananas:Carrots:Dried Cherries" | cut -d: -f1,2,4
Apples:Bananas:Dried Cherries
在这里,我们指定":
"字符是cut
的分隔符。我们还指定它应该打印第一、第二和第四个字段。这导致打印了 Apples(第一个字段)、Bananas(第二个字段)和 Dried Cherries(第四个字段)。第三个字段 Carrots 被省略了。这是因为我们没有明确告诉cut
命令打印第三个字段。
现在我们知道了cut
是如何工作的,让我们看看它是如何处理messages
日志条目的。
这是一个日志消息的样本:
Jun 28 21:50:01 localhost systemd: Created slice user-0.slice.
当我们执行这个一行代码中的cut
命令时,我们明确告诉它只打印第一、第二和第五个字段:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5
Jun 26 systemd:
通过在我们的cut
命令中指定一个空格字符作为分隔符,我们可以看到这会导致cut
只打印每个日志条目的月份、日期和程序。单独看可能并不那么有用,但随着我们继续查看这个一行代码,cut 提供的功能将变得至关重要。
sort 命令
接下来的sort
命令在这个一行代码中实际上被使用了两次:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | head
Jun 26 audispd:
Jun 26 audispd:
Jun 26 audispd:
Jun 26 audispd:
Jun 26 audispd:
Jun 26 auditd[539]:
Jun 26 auditd[539]:
Jun 26 auditd[542]:
Jun 26 auditd[542]:
Jun 26 auditd[548]:
这个命令实际上很简单,它的作用是对cut
命令的输出进行排序。
为了更好地解释这一点,让我们看下面的例子:
# cat /var/tmp/fruits.txt
Apples
Dried Cherries
Carrots
Bananas
上面的文件再次包含几种水果,这一次它们不是按字母顺序排列的。然而,如果我们使用sort
命令来读取这个文件,这些水果的顺序将会改变:
# sort /var/tmp/fruits.txt
Apples
Bananas
Carrots
Dried Cherries
正如我们所看到的,现在的顺序是按字母顺序排列的,尽管水果在文件中的顺序是不同的。sort
的好处在于它可以用几种不同的方式对文本进行排序。实际上,在我们的一行命令中sort
的第二个实例中,我们使用-n
标志对文本进行了数字排序:
# cat /var/tmp/numbers.txt
10
23
2312
23292
1212
129191
# sort -n /var/tmp/numbers.txt
10
23
1212
2312
23292
129191
uniq 命令
我们的一行命令包含sort
命令的原因很简单,就是为了对发送到uniq -c
的输入进行排序:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | uniq -c | head
5 Jun 26 audispd:
2 Jun 26 auditd[539]:
2 Jun 26 auditd[542]:
3 Jun 26 auditd[548]:
2 Jun 26 auditd[550]:
2 Jun 26 auditd[553]:
15 Jun 26 augenrules:
38 Jun 26 avahi-daemon[573]:
19 Jun 26 avahi-daemon[579]:
19 Jun 26 avahi-daemon[581]:
uniq
命令可以用来识别匹配的行,并将这些行显示为单个唯一行。为了更好地理解这一点,让我们看看下面的例子:
$ cat /var/tmp/duplicates.txt
Apple
Apple
Apple
Apple
Banana
Banana
Banana
Carrot
Carrot
我们的示例文件"duplicates.txt
"包含多个重复的行。当我们用uniq
读取这个文件时,我们只会看到每一行的唯一内容:
$ uniq /var/tmp/duplicates.txt
Apple
Banana
Carrot
这可能有些有用;但是,我发现使用-c
标志,输出可能会更有用:
$ uniq -c /var/tmp/duplicates.txt
4 Apple
3 Banana
2 Carrot
使用-c
标志,uniq
命令将计算它找到每行的次数。在这里,我们可以看到有四行包含单词苹果。因此,uniq
命令在单词苹果之前打印了数字 4,以显示这行有四个实例:
$ cat /var/tmp/duplicates.txt
Apple
Apple
Orange
Apple
Apple
Banana
Banana
Banana
Carrot
Carrot
$ uniq -c /var/tmp/duplicates.txt
2 Apple
1 Orange
2 Apple
3 Banana
2 Carrot
uniq
命令的一个注意事项是,为了获得准确的计数,每个实例都需要紧挨在一起。当我们在苹果行的组之间添加单词橙子时,可以看到会发生什么。
把所有东西都联系在一起
如果我们再次看看我们的命令,现在我们可以更好地理解它在做什么:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | uniq -c | sort -n | tail
39 Jun 26 journal:
56 Jun 26 NetworkManager:
76 Jun 26 NetworkManager[582]:
76 Jun 26 NetworkManager[588]:
78 Jun 26 NetworkManager[580]:
79 Jun 26 systemd-logind:
110 Jun 26 systemd[1]:
152 Jun 26 NetworkManager[574]:
1684 Jun 26 systemd:
15077 Jun 26 kernel:
上面的命令将过滤并打印/var/log/messages
中与字符串"Jun 26
"匹配的所有日志消息。然后输出将被发送到cut
命令,该命令打印每行的月份、日期和进程。然后将此输出发送到sort
命令,以将输出排序为相互匹配的组。排序后的输出然后被发送到uniq -c
,它计算每行出现的次数并打印一个带有计数的唯一行。
然后,我们添加另一个sort
来按uniq
添加的数字对输出进行排序,并添加tail
来将输出缩短到最后 10 行。
那么,这个花哨的一行命令到底告诉我们什么呢?嗯,它告诉我们kernel
设施和systemd
进程正在记录相当多的内容。实际上,与其他列出的项目相比,我们可以看到这两个项目的日志消息比其他项目更多。
然而,systemd
和kernel
在/var/log/messages
中有更多的日志消息可能并不奇怪。如果有另一个写入许多日志的进程,我们将能够在一行输出中看到这一点。然而,由于我们的第一次运行没有产生有用的结果,我们可以修改一行命令来缩小输出范围:
Jun 26 19:51:10 localhost auditd[550]: Started dispatcher: /sbin/audispd pid: 562
如果我们看一下messages
日志条目的格式,我们会发现在进程之后,可以找到日志消息。为了进一步缩小我们的搜索范围,我们可以在输出中添加一点消息。
我们可以通过将cut
命令的字段列表更改为"1,2,5-8
"来实现这一点。通过在5
后面添加"-8
",我们发现cut
命令显示从 5 到 8 的所有字段。这样做的效果是在我们的一行命令中包含每条日志消息的前三个单词:
# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5-8 | sort | uniq -c | sort -n | tail -30
64 Jun 26 kernel: 131055 pages RAM
64 Jun 26 kernel: 5572 pages reserved
64 Jun 26 kernel: lowmem_reserve[]: 0 462
77 Jun 26 kernel: [ 579]
79 Jun 26 kernel: Out of memory:
80 Jun 26 kernel: [<ffffffff810b68f8>] ? ktime_get_ts+0x48/0xe0
80 Jun 26 kernel: [<ffffffff81102e03>] ? proc_do_uts_string+0xe3/0x130
80 Jun 26 kernel: [<ffffffff8114520e>] oom_kill_process+0x24e/0x3b0
80 Jun 26 kernel: [<ffffffff81145a36>] out_of_memory+0x4b6/0x4f0
80 Jun 26 kernel: [<ffffffff8114b579>] __alloc_pages_nodemask+0xa09/0xb10
80 Jun 26 kernel: [<ffffffff815dd02d>] dump_header+0x8e/0x214
80 Jun 26 kernel: [ pid ]
81 Jun 26 kernel: [<ffffffff8118bc3a>] alloc_pages_vma+0x9a/0x140
93 Jun 26 kernel: Call Trace:
93 Jun 26 kernel: [<ffffffff815e19ba>] dump_stack+0x19/0x1b
93 Jun 26 kernel: [<ffffffff815e97c8>] page_fault+0x28/0x30
93 Jun 26 kernel: [<ffffffff815ed186>] __do_page_fault+0x156/0x540
93 Jun 26 kernel: [<ffffffff815ed58a>] do_page_fault+0x1a/0x70
93 Jun 26 kernel: Free swap
93 Jun 26 kernel: Hardware name: innotek
93 Jun 26 kernel: lowmem_reserve[]: 0 0
93 Jun 26 kernel: Mem-Info:
93 Jun 26 kernel: Node 0 DMA:
93 Jun 26 kernel: Node 0 DMA32:
93 Jun 26 kernel: Node 0 hugepages_total=0
93 Jun 26 kernel: Swap cache stats:
93 Jun 26 kernel: Total swap =
186 Jun 26 kernel: Node 0 DMA
186 Jun 26 kernel: Node 0 DMA32
489 Jun 26 kernel: CPU
如果我们还增加tail
命令以显示最后 30 行,我们可以看到一些有趣的趋势。非常有趣的第一行是输出中的第四行:
79 Jun 26 kernel: Out of memory:
似乎kernel
打印了以术语"Out of memory
"开头的79
条日志消息。虽然这似乎有点显而易见,但似乎这台服务器可能在某个时候耗尽了内存。
接下来的两行看起来也支持这个理论:
80 Jun 26 kernel: [<ffffffff8114520e>] oom_kill_process+0x24e/0x3b0
80 Jun 26 kernel: [<ffffffff81145a36>] out_of_memory+0x4b6/0x4f0
第一行似乎表明内核终止了一个进程;第二行再次表明出现了内存耗尽的情况。这个系统可能已经耗尽了内存,并在这样做时终止了 Apache 进程。这似乎非常可能。
当 Linux 系统内存耗尽时会发生什么?
在 Linux 上,内存的管理方式与其他操作系统有些不同。当系统内存不足时,内核有一个旨在回收已使用内存的进程;这个进程称为内存耗尽终结者(oom-kill)。
oom-kill
进程旨在终止使用大量内存的进程,以释放这些内存供关键系统进程使用。我们将稍后讨论oom-kill
,但首先,我们应该了解 Linux 如何定义内存耗尽。
最小空闲内存
在 Linux 上,当空闲内存量低于定义的最小值时,将启动 oom-kill 进程。这个最小值当然是一个名为vm.min_free_kbytes
的内核可调参数。该参数允许您设置系统始终可用的内存量(以千字节为单位)。
当可用内存低于此参数的值时,系统开始采取行动。在深入讨论之前,让我们首先看看我们系统上设置的这个值,并重新了解 Linux 中的内存管理方式。
我们可以使用与上一章相同的sysctl
命令查看当前的vm.min_free_kbytes
值:
# sysctl vm.min_free_kbytes
vm.min_free_kbytes = 11424
当前值为11424
千字节,约为 11 兆字节。这意味着我们系统的空闲内存必须始终大于 11 兆字节,否则系统将启动 oom-kill 进程。这似乎很简单,但正如我们从第四章中所知道的那样,Linux 管理内存的方式并不一定那么简单:
# free
total used free shared buffers cached
Mem: 243788 230012 13776 60 0 2272
-/+ buffers/cache: 227740 16048
Swap: 1081340 231908 849432
如果我们在这个系统上运行free
命令,我们可以看到当前的内存使用情况以及可用内存量。在深入讨论之前,我们将分解这个输出,以便重新理解 Linux 如何使用内存。
total used free shared buffers cached
Mem: 243788 230012 13776 60 0 2272
在第一行中,我们可以看到系统总共有 243MB 的物理内存。我们可以在第二列中看到目前使用了 230MB,第三列显示有 13MB 未使用。系统测量的正是这个未使用的值,以确定当前是否有足够的最小所需内存空闲。
这很重要,因为如果我们记得第四章中所说的,我们使用第二个“内存空闲”值来确定有多少内存可用。
total used free shared buffers cached
Mem: 243788 230012 13776 60 0 2272
-/+ buffers/cache: 227740 16048
在free
的第二行,我们可以看到系统在考虑缓存使用的内存量时的已使用和空闲内存量。正如我们之前学到的,Linux 系统非常积极地缓存文件和文件系统属性。所有这些缓存都存储在内存中,我们可以看到,在运行这个free
命令的瞬间,我们的缓存使用了 2,272 KB 的内存。
当空闲内存(不包括缓存)接近min_free_kbytes
值时,系统将开始回收一些用于缓存的内存。这旨在允许系统尽可能地缓存,但在内存不足的情况下,为了防止 oom-kill 进程的启动,这个缓存变得可丢弃:
Swap: 1081340 231908 849432
free
命令的第三行将我们带到 Linux 内存管理的另一个重要步骤:交换。正如我们从前一行中看到的,当执行这个free
命令时,系统将大约 231MB 的数据从物理内存交换到交换设备。
这是我们期望在运行内存不足的系统上看到的情况。当free
内存开始变得稀缺时,系统将开始获取物理内存中的内存对象并将它们推送到交换内存中。
系统开始执行这些交换活动的侵略性取决于内核参数vm.swappiness
中定义的值:
$ sysctl vm.swappiness
vm.swappiness = 30
在我们的系统上,swappiness
值目前设置为30
。这个可调参数接受 0 到 100 之间的值,其中 100 允许最激进的交换策略。
当swappiness
值较低时,系统会更倾向于将内存对象保留在物理内存中尽可能长的时间,然后再将它们移动到交换设备上。
快速回顾
在进入 oom-kill 之前,让我们回顾一下当 Linux 系统上的内存开始变得紧张时会发生什么。系统首先会尝试释放用于磁盘缓存的内存对象,并将已使用的内存移动到交换设备上。如果系统无法通过前面提到的两个过程释放足够的内存,内核就会启动 oom-kill 进程。
oom-kill 的工作原理
如前所述,oom-kill 进程是在空闲内存不足时启动的一个进程。这个进程旨在识别使用大量内存并且对系统操作不重要的进程。
那么,oom-kill 是如何确定这一点的呢?嗯,实际上是由内核确定的,并且不断更新。
我们在前面的章节中讨论了系统上每个运行的进程都有一个在/proc
文件系统中的文件夹。内核维护着这个文件夹,里面有很多有趣的文件。
# ls -la /proc/6689/oom_*
-rw-r--r--. 1 root root 0 Jun 29 15:23 /proc/6689/oom_adj
-r--r--r--. 1 root root 0 Jun 29 15:23 /proc/6689/oom_score
-rw-r--r--. 1 root root 0 Jun 29 15:23 /proc/6689/oom_score_adj
前面提到的三个文件与 oom-kill 进程及每个进程被杀死的可能性有关。我们要看的第一个文件是oom_score
文件:
# cat /proc/6689/oom_score
40
如果我们cat
这个文件,我们会发现它只包含一个数字。然而,这个数字对于 oom-kill 进程非常重要,因为这个数字就是进程 6689 的 OOM 分数。
OOM 分数是内核分配给一个进程的一个值,用来确定相应进程对 oom-kill 的优先级高低。分数越高,进程被杀死的可能性就越大。当内核为这个进程分配一个值时,它基于进程使用的内存和交换空间的数量以及对系统的重要性。
你可能会问自己,“我想知道是否有办法调整我的进程的 oom 分数。” 这个问题的答案是肯定的,有!这就是另外两个文件oom_adj
和oom_score_adj
发挥作用的地方。这两个文件允许您调整进程的 oom 分数,从而控制进程被杀死的可能性。
目前,oom_adj
文件将被淘汰,取而代之的是oom_score_adj
。因此,我们将只关注oom_score_adj
文件。
调整 oom 分数
oom_score_adj
文件支持从-1000 到 1000 的值,其中较高的值将增加 oom-kill 选择该进程的可能性。让我们看看当我们为我们的进程添加 800 的调整时,我们的 oom 分数会发生什么变化:
# echo "800" > /proc/6689/oom_score_adj
# cat /proc/6689/oom_score
840
仅仅通过改变内容为 800,内核就检测到了这个调整并为这个进程的 oom 分数增加了 800。如果这个系统在不久的将来内存耗尽,这个进程绝对会被 oom-kill 杀死。
如果我们将这个值改为-1000,这实际上会排除该进程被 oom-kill 杀死的可能性。
确定我们的进程是否被 oom-kill 杀死
现在我们知道了系统内存不足时会发生什么,让我们更仔细地看看我们的系统到底发生了什么。为了做到这一点,我们将使用less
来读取/var/log/messages
文件,并寻找kernel: Out of memory
消息的第一个实例:
Jun 26 00:53:39 blog kernel: Out of memory: Kill process 5664 (processor) score 265 or sacrifice child
有趣的是,“内存不足”日志消息的第一个实例是在我们的 Apache 进程被杀死之前的 20 小时。更重要的是,被杀死的进程是一个非常熟悉的进程,即上一章的“处理器”cronjob。
这一条日志记录实际上可以告诉我们关于该进程以及为什么 oom-kill 选择了该进程的很多信息。在第一行,我们可以看到内核给了处理器进程一个265
的分数。虽然不是最高分,但我们已经看到 265 分很可能比此时运行的大多数进程的分数都要高。
这似乎表明处理器作业在这个时候使用了相当多的内存。让我们继续查看这个文件,看看在这个系统上可能发生了什么其他事情:
Jun 26 00:54:31 blog kernel: Out of memory: Kill process 5677 (processor) score 273 or sacrifice child
在日志文件中再往下看一点,我们可以看到处理器进程再次被杀死。似乎每次这个作业运行时,系统都会耗尽内存。
为了节约时间,让我们跳到第 21 个小时,更仔细地看看我们的 Apache 进程被杀死的时间:
Jun 26 21:12:54 localhost kernel: Out of memory: Kill process 2249 (httpd) score 7 or sacrifice child
Jun 26 21:12:54 localhost kernel: Killed process 2249 (httpd) total-vm:462648kB, anon-rss:436kB, file-rss:8kB
Jun 26 21:12:54 localhost kernel: httpd invoked oom-killer: gfp_mask=0x200da, order=0, oom_score_adj=0
看起来messages
日志一直都有我们的答案。从前面几行可以看到进程2249
,这恰好是我们的 Apache 服务器进程 ID:
Jun 26 21:12:55 blog.example.com systemd[1]: httpd.service: main process exited, code=killed, status=9/KILL
在这里,我们看到systemd
检测到该进程在21:12:55
被杀死。此外,我们可以从消息日志中看到 oom-kill 在21:12:54
针对该进程进行了操作。在这一点上,毫无疑问,该进程是被 oom-kill 杀死的。
系统为什么耗尽了内存?
在这一点上,我们能够确定 Apache 服务在内存耗尽时被系统杀死。不幸的是,oom-kill 并不是问题的根本原因,而是一个症状。虽然它是 Apache 服务停止的原因,但如果我们只是重新启动进程而不做其他操作,问题可能会再次发生。
在这一点上,我们需要确定是什么导致系统首先耗尽了内存。为了做到这一点,让我们来看看消息日志文件中“内存不足”消息的整个列表:
# grep "Out of memory" /var/log/messages* | cut -d\ -f1,2,10,12 | uniq -c
38 /var/log/messages:Jun 28 process (processor)
1 /var/log/messages:Jun 28 process (application)
10 /var/log/messages:Jun 28 process (processor)
1 /var/log/messages-20150615:Jun 10 process (python)
1 /var/log/messages-20150628:Jun 22 process (processor)
47 /var/log/messages-20150628:Jun 26 process (processor)
32 /var/log/messages-20150628:Jun 26 process (httpd)
再次使用cut
和uniq -c
命令,我们可以在消息日志中看到一个有趣的趋势。我们可以看到内核已经多次调用了 oom-kill。我们可以看到即使今天系统也启动了 oom-kill 进程。
现在我们应该做的第一件事是弄清楚这个系统有多少内存。
# free -m
total used free shared buffers cached
Mem: 238 206 32 0 0 2
-/+ buffers/cache: 203 34
Swap: 1055 428 627
使用free
命令,我们可以看到系统有238
MB 的物理内存和1055
MB 的交换空间。然而,我们也可以看到只有34
MB 的内存是空闲的,系统已经交换了428
MB 的物理内存。
很明显,对于当前的工作负载,该系统分配的内存根本不够。
如果我们回顾一下 oom-kill 所针对的进程,我们可以看到一个有趣的趋势:
# grep "Out of memory" /var/log/messages* | cut -d\ -f10,12 | sort | uniq -c
1 process (application)
32 process (httpd)
118 process (processor)
1 process (python)
在这里,很明显,被最频繁杀死的两个进程是httpd
和processor
。我们之前了解到,oom-kill 根据它们使用的内存量来确定要杀死的进程。这意味着这两个进程在系统上使用了最多的内存,但它们到底使用了多少内存呢?
# ps -eo rss,size,cmd | grep processor
0 340 /bin/sh -c /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null
130924 240520 /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
964 336 grep --color=auto processor
使用ps
命令来专门显示rss和size字段,我们在第四章中学到了,故障排除性能问题,我们可以看到processor
作业使用了130
MB 的常驻内存和240
MB 的虚拟内存。
如果系统只有238
MB 的物理内存,而进程使用了240
MB 的虚拟内存,最终,这个系统的物理内存会不足。
长期和短期解决问题
像本章讨论的这种问题可能有点棘手,因为它们通常有两种解决路径。有一个长期解决方案和一个短期解决方案;两者都是必要的,但一个只是临时的。
长期解决方案
对于这个问题的长期解决方案,我们确实有两个选择。我们可以增加服务器的物理内存,为 Apache 和 Processor 提供足够的内存来完成它们的任务。或者,我们可以将 processor 移动到另一台服务器上。
由于我们知道这台服务器经常杀死 Apache 服务和processor
任务,很可能是系统上的内存对于执行这两个角色来说太低了。通过将processor
任务(以及很可能是其一部分的自定义应用程序)移动到另一个系统,我们将工作负载移动到一个专用服务器。
基于处理器的内存使用情况,增加新服务器的内存也可能是值得的。似乎processor
任务使用了足够的内存,在目前这样的低内存服务器上可能会导致内存不足的情况。
确定哪个长期解决方案最好取决于环境和导致系统内存不足的应用程序。在某些情况下,增加服务器的内存可能是更好的选择。
在虚拟和云环境中,这个任务非常容易,但这并不总是最好的答案。确定哪个答案更好取决于你所使用的环境。
短期解决方案
假设两个长期解决方案都需要几天时间来实施。就目前而言,我们的系统上 Apache 服务仍然处于停机状态。这意味着我们的公司博客也仍然处于停机状态;为了暂时解决问题,我们需要重新启动 Apache。
然而,我们不应该只是用systemctl
命令简单地重新启动 Apache。在启动任何内容之前,我们实际上应该首先重新启动服务器。
当大多数 Linux 管理员听到“让我们重启”这句话时,他们会感到沮丧。这是因为作为 Linux 系统管理员,我们很少需要重启系统。我们被告知在更新内核之外重启 Linux 服务器是一件不好的事情。
在大多数情况下,我们认为重新启动服务器不是正确的解决方案。然而,我认为系统内存不足是一个特殊情况。
我认为,在启动 oom-kill 时,应该在完全恢复到正常状态之前重新启动相关系统。
我这样说的原因是 oom-kill 进程可以杀死任何进程,包括关键的系统进程。虽然 oom-kill 进程确实会通过 syslog 记录被杀死的进程,但 syslog 守护程序只是系统上的另一个可以被 oom-kill 杀死的进程。
即使 oom-kill 没有在 oom-kill 杀死许多不同进程的情况下杀死 syslog 进程,要确保每个进程都正常运行可能会有些棘手。特别是当处理问题的人经验较少时。
虽然你可以花时间确定正在运行的进程,并确保重新启动每个进程,但简单地重新启动服务器可能更快,而且可以说更安全。因为你知道在启动时,每个定义为启动的进程都将被启动。
虽然并非每个系统管理员都会同意这种观点,但我认为这是确保系统处于稳定状态的最佳方法。但重要的是要记住,这只是一个短期解决方案,重新启动后,除非有变化,系统可能会再次出现内存不足的情况。
对于我们的情况,最好是在服务器的内存增加或作业可以移至专用系统之前禁用processor
作业。然而,在某些情况下,这可能是不可接受的。像长期解决方案一样,防止再次发生这种情况是情境性的,并取决于你所管理的环境。
由于我们假设短期解决方案是我们示例的正确解决方案,我们将继续重新启动系统:
# reboot
Connection to 127.0.0.1 closed by remote host.
系统恢复在线后,我们可以使用systemctl
命令验证 Apache 是否正在运行。
# systemctl status httpd
httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled)
Active: active (running) since Wed 2015-07-01 15:37:22 UTC; 1min 29s ago
Main PID: 1012 (httpd)
Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec"
CGroup: /system.slice/httpd.service
├─1012 /usr/sbin/httpd -DFOREGROUND
├─1439 /usr/sbin/httpd -DFOREGROUND
├─1443 /usr/sbin/httpd -DFOREGROUND
├─1444 /usr/sbin/httpd -DFOREGROUND
├─1445 /usr/sbin/httpd -DFOREGROUND
└─1449 /usr/sbin/httpd -DFOREGROUND
Jul 01 15:37:22 blog.example.com systemd[1]: Started The Apache HTTP Server.
如果我们在这个系统上再次运行free
命令,我们可以看到内存利用率要低得多,至少直到现在为止。
# free -m
total used free shared buffers cached
Mem: 238 202 35 4 0 86
-/+ buffers/cache: 115 122
Swap: 1055 0 1055
总结
在本章中,我们运用了我们的故障排除技能,确定了影响公司博客的问题以及这个问题的根本原因。我们能够运用在之前章节学到的技能和技术,确定 Apache 服务已经停止。我们还确定了这个问题的根本原因是系统内存耗尽。
通过调查日志文件,我们发现系统上占用最多内存的两个进程是 Apache 和一个名为processor
的自定义应用程序。此外,通过识别这些进程,我们能够提出长期建议,以防止此问题再次发生。
除此之外,我们还学到了当 Linux 系统内存耗尽时会发生什么。
在下一章中,我们将把你到目前为止学到的一切付诸实践,通过对一个无响应系统进行根本原因分析。
第十二章:意外重启的根本原因分析
在本章中,我们将对您在之前章节中学到的故障排除方法和技能进行测试。我们将对最困难的真实场景之一进行根本原因分析:意外重启。
正如我们在第一章中讨论的,故障排除最佳实践,根本原因分析比简单的故障排除和解决问题要复杂一些。在企业环境中,您会发现每个导致重大影响的问题都需要进行根本原因分析(RCA)。这是因为企业环境通常有关于应该如何处理事件的成熟流程。
一般来说,当发生重大事件时,受到影响的组织希望避免再次发生。即使在技术环境之外的许多行业中也可以看到这一点。
正如我们在第一章中讨论的,故障排除最佳实践,一个有用的根本原因分析具有以下特征:
-
问题的报告方式
-
问题的实际根本原因
-
事件和采取的行动的时间线
-
任何关键数据点
-
防止事件再次发生的行动计划
对于今天的问题,我们将使用一个事件来构建一个样本根本原因分析文档。为此,我们将使用您在之前章节中学到的信息收集和故障排除步骤。在做所有这些的同时,您还将学会处理意外重启,这是确定根本原因的最糟糕的事件之一。
意外重启困难的原因在于系统重启时通常会丢失您需要识别问题根本原因的信息。正如我们在之前的章节中所看到的,我们在问题发生期间收集的数据越多,我们就越有可能确定问题的原因。
在重启期间丢失的信息往往是确定根本原因和未确定根本原因之间的区别。
深夜警报
随着我们在章节中的进展和为最近的雇主解决了许多问题,我们也在获得他们对我们能力的信任。最近,我们甚至被放在了值班轮换中,这意味着如果在工作时间之后出现问题,我们的手机将通过短信收到警报。
当然,值班的第一个晚上我们收到了一个警报;这个警报不是一个好消息。
警报:blog.example.com 不再响应 ICMP Ping
当我们被加入到值班轮换中时,我们的团队负责人告诉我们,任何在工作时间之后发生的重大事件都必须进行根本原因分析。这样做的原因是为了让我们组中的其他人学习和了解我们是如何解决问题以及如何防止再次发生的。
正如我们之前讨论的,有用的根本原因分析的关键组成部分之一是列出事情发生的时间。我们时间线中的一个重大事件是我们收到警报的时间;根据我们的短信消息,我们可以看到我们在 2015 年 7 月 5 日 01:52 收到了警报,或者说;7 月 5 日凌晨 1:52(欢迎来到值班!)。
确定问题
从警报中,我们可以看到我们的监控系统无法对我们公司的博客服务器执行ICMP
ping。我们应该做的第一件事是确定我们是否可以ping
服务器:
$ ping blog.example.com
PING blog.example.com (192.168.33.11): 56 data bytes
64 bytes from 192.168.33.11: icmp_seq=0 ttl=64 time=0.832 ms
64 bytes from 192.168.33.11: icmp_seq=1 ttl=64 time=0.382 ms
64 bytes from 192.168.33.11: icmp_seq=2 ttl=64 time=0.240 ms
64 bytes from 192.168.33.11: icmp_seq=3 ttl=64 time=0.234 ms
^C
--- blog.example.com ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.234/0.422/0.832/0.244 ms
看起来我们能够 ping 通相关服务器,所以也许这是一个虚警?以防万一,让我们尝试登录系统:
$ ssh 192.168.33.11 -l vagrant
vagrant@192.168.33.11's password:
$
看起来我们能够登录,系统正在运行;让我们开始四处看看,检查是否能够确定任何问题。
正如在之前的章节中介绍的,我们总是运行的第一个命令是w
:
$ w
01:59:46 up 9 min, 1 user, load average: 0.00, 0.01, 0.02
USER TTY LOGIN@ IDLE JCPU PCPU WHAT
vagrant pts/0 01:59 2.00s 0.03s 0.01s w
在这种情况下,这个小习惯实际上效果很好。通过w
命令的输出,我们可以看到这台服务器只运行了9
分钟。看来我们的监控系统无法 ping 通我们的服务器,因为它正在重新启动。
提示
我们应该注意到我们能够确定服务器在登录后重新启动;这将是我们时间表中的一个关键事件。
有人重新启动了这台服务器吗?
虽然我们刚刚确定了警报的根本原因,但这并不是问题的根本原因。我们需要确定服务器为什么重新启动。服务器不经常(至少不应该)自行重新启动;有时可能只是有人在未告知其他人的情况下对该服务器进行维护。我们可以使用last
命令查看最近是否有人登录到该服务器:
$ last
vagrant pts/0 192.168.33.1 Sun Jul 5 01:59 still logged in
joe pts/1 192.168.33.1 Sat Jun 6 18:49 - 21:37 (02:48)
bob pts/0 10.0.2.2 Sat Jun 6 18:16 - 21:37 (03:21)
billy pts/0 10.0.2.2 Sat Jun 6 17:09 - 18:14 (01:05)
doug pts/0 10.0.2.2 Sat Jun 6 15:26 - 17:08 (01:42)
last
命令的输出从顶部开始显示最新的登录。这些数据来自/var/log/wtmp
,用于存储登录详细信息。在last
命令的输出末尾,我们看到以下行:
wtmp begins Mon Jun 21 23:39:24 2014
这告诉我们wtmp
日志文件的历史记录;这是一个非常有用的信息。如果我们想查看特定数量的登录,我们可以简单地添加“-n”标志,后面跟上我们希望看到的登录数量。
这通常是非常有用的;但是,由于我们不知道最近在这台机器上有多少次登录,我们将使用默认设置。
从我们收到的输出中,我们可以看到最近没有人登录到这台服务器。除非有人亲自按下电源按钮或拔掉系统,否则我们可以假设没有人重新启动服务器。
提示
这是我们时间表中应该使用的另一个事实/事件。
日志告诉我们什么?
由于没有人重新启动这台服务器,我们的下一个假设是这台服务器是由软件或硬件问题重新启动的。我们下一个合乎逻辑的步骤是查看系统日志文件,以确定发生了什么事情:
01:59:46 up 9 min, 1 user, load average: 0.00, 0.01, 0.02
less command to read /var/log/messages:
Jul 5 01:48:01 localhost auditd[560]: Audit daemon is low on disk space for logging
Jul 5 01:48:01 localhost auditd[560]: Audit daemon is suspending logging due to low disk space.
Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12!
Jul 5 01:50:02 localhost watchdog[608]: shutting down the system because of error -3
Jul 5 01:50:12 localhost rsyslogd: [origin software="rsyslogd" swVersion="7.4.7" x-pid="593" x-info="http://www.rsyslog.com"] exiting on signal 15.
Jul 5 01:50:32 localhost systemd: Time has been changed
Jul 5 01:50:32 localhost NetworkManager[594]: <info> dhclient started with pid 722
Jul 5 01:50:32 localhost NetworkManager[594]: <info> Activation (enp0s3) Stage 3 of 5 (IP Configure Start) complete.
Jul 5 01:50:32 localhost vboxadd-service: Starting VirtualBox Guest Addition service [ OK ]
Jul 5 01:50:32 localhost systemd: Started LSB: VirtualBox Additions service.
Jul 5 01:50:32 localhost dhclient[722]: Internet Systems Consortium DHCP Client 4.2.5
Jul 5 01:50:32 localhost dhclient[722]: Copyright 2004-2013 Internet Systems Consortium.
Jul 5 01:50:32 localhost dhclient[722]: All rights reserved.
Jul 5 01:50:32 localhost dhclient[722]: For info, please visit https://www.isc.org/software/dhcp/
Jul 5 01:50:32 localhost dhclient[722]:
Jul 5 01:50:32 localhost NetworkManager: Internet Systems Consortium DHCP Client 4.2.5
Jul 5 01:50:32 localhost NetworkManager: Copyright 2004-2013 Internet Systems Consortium.
Jul 5 01:50:32 localhost NetworkManager: All rights reserved.
Jul 5 01:50:32 localhost NetworkManager: For info, please visit https://www.isc.org/software/dhcp/
Jul 5 01:50:32 localhost NetworkManager[594]: <info> (enp0s3): DHCPv4 state changed nbi -> preinit
Jul 5 01:50:32 localhost dhclient[722]: Listening on LPF/enp0s3/08:00:27:20:5d:4b
Jul 5 01:50:32 localhost dhclient[722]: Sending on LPF/enp0s3/08:00:27:20:5d:4b
Jul 5 01:50:32 localhost dhclient[722]: Sending on Socket/fallback
Jul 5 01:50:32 localhost dhclient[722]: DHCPREQUEST on enp0s3 to 255.255.255.255 port 67 (xid=0x3ae55b57)
由于这里有相当多的信息,让我们稍微分解一下我们看到的内容。
第一个任务是找到一个清楚写在启动时的日志消息。通过识别写在启动时的日志消息,我们将能够确定在重新启动之前和之后写入了哪些日志。我们还将能够确定我们的根本原因文档的启动时间:
Jul 5 01:50:12 localhost rsyslogd: [origin software="rsyslogd" swVersion="7.4.7" x-pid="593" x-info="http://www.rsyslog.com"] exiting on signal 15.
Jul 5 01:50:32 localhost systemd: Time has been changed
Jul 5 01:50:32 localhost NetworkManager[594]: <info> dhclient started with pid 722
Jul 5 01:50:32 localhost NetworkManager[594]: <info> Activation (enp0s3) Stage 3 of 5 (IP Configure Start) complete.
看起来有希望的第一个日志条目是NetworkManager
在01:50:32
的消息。这条消息说明NetworkManager
服务已启动dhclient
。
dhclient
进程用于发出 DHCP 请求并根据回复配置网络设置。这个过程通常只在网络被重新配置或在启动时调用:
Jul 5 01:50:12 localhost rsyslogd: [origin software="rsyslogd" swVersion="7.4.7" x-pid="593" x-info="http://www.rsyslog.com"] exiting on signal 15.
如果我们查看前一行,我们可以看到在 01:50:12,rsyslogd
进程正在“退出信号 15”。这意味着在关机期间发送了终止信号给rsyslogd
进程,这是一个非常标准的过程。
我们可以确定在 01:50:12 服务器正在关机过程中,在 01:50:32 服务器正在启动过程中。这意味着我们应该查看 01:50:12 之前的所有内容,以确定系统为什么重新启动。
提示
关机时间和启动时间也将需要用于我们的根本原因时间表。
从之前捕获的日志中,我们可以看到在 01:50 之前有两个进程写入了/var/log/messages
;auditd
和看门狗进程。
Jul 5 01:48:01 localhost auditd[560]: Audit daemon is low on disk space for logging
Jul 5 01:48:01 localhost auditd[560]: Audit daemon is suspending logging due to low disk space.
让我们首先看一下auditd
进程。我们可以在第一行看到“磁盘空间不足”的消息。我们的系统是否因为磁盘空间不足而遇到问题?这是可能的,我们现在可以检查一下:
# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 39G 39G 32M 100% /
devtmpfs 491M 0 491M 0% /dev
tmpfs 498M 0 498M 0% /dev/shm
tmpfs 498M 6.5M 491M 2% /run
tmpfs 498M 0 498M 0% /sys/fs/cgroup
/dev/sda1 497M 104M 394M 21% /boot
看起来文件系统已经满了,但这本身通常不会导致重新启动。考虑到第二个auditd
消息显示守护程序正在暂停记录;这也不像是重新启动过程。让我们继续查看,看看我们还能识别出什么:
Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12!
Jul 5 01:50:02 localhost watchdog[608]: shutting down the system because of error -3
看门狗
进程的接下来两条消息很有趣。第一条消息指出服务器的loadavg
高于指定的阈值。第二条消息非常有趣,因为它明确指出了“关闭系统”。
看门狗
进程可能重新启动了这台服务器吗?也许是的,但首要问题是,看门狗
进程是什么?
了解新的进程和服务
在查看messages
日志时发现一个从未使用或见过的进程并不罕见:
# ps -eo cmd | sort | uniq | wc -l
115
即使在我们的基本示例系统上,进程列表中有 115 个独特的命令。特别是当你加入一个新版本,比如写作时的 Red Hat Enterprise Linux 7(较新的版本)。每个新版本都带来新的功能,甚至可能意味着默认运行新的进程。要跟上这一切是非常困难的。
就我们的例子而言,看门狗
就是这种情况之一。在这一点上,除了从名称中推断出它是观察事物之外,我们不知道这个进程的作用。那么我们如何了解更多关于它的信息呢?好吧,我们要么谷歌一下,要么查看man
:
$ man watchdog
NAME
watchdog - a software watchdog daemon
SYNOPSIS
watchdog [-F|--foreground] [-f|--force] [-c filename|--config-file filename] [-v|--verbose] [-s|--sync] [-b|--softboot] [-q|--no-action]
DESCRIPTION
The Linux kernel can reset the system if serious problems are detected. This can be implemented via special watchdog hardware, or via a slightly less reliable software-only watchdog inside the kernel. Either way, there needs to be a daemon that tells the kernel the system is working fine. If the daemon stops doing that, the system is reset.
watchdog is such a daemon. It opens /dev/watchdog, and keeps writing to it often enough to keep the kernel from resetting, at least once per minute. Each write delays the reboot time another minute. After a minute of inactivity the watchdog hardware will cause the reset. In the case of the software watchdog the ability to reboot will depend on the state of the machines and interrupts.
The watchdog daemon can be stopped without causing a reboot if the device /dev/watchdog is closed correctly, unless your kernel is compiled with the CONFIG_WATCHDOG_NOWAYOUT option enabled.
根据man
页面,我们已经确定看门狗
服务实际上用于确定服务器是否健康。如果看门狗
无法做到这一点,它可能会重新启动服务器:
Jul 5 01:50:02 localhost watchdog[608]: shutting down the system because of error -3
从这条日志消息中看来,看门狗
软件是导致重新启动的原因。是不是因为文件系统已满,看门狗
才重新启动了系统?
如果我们继续阅读man
页面,我们将看到另一条有用的信息,如下所示:
TESTS
The watchdog daemon does several tests to check the system status:
· Is the process table full?
· Is there enough free memory?
· Are some files accessible?
· Have some files changed within a given interval?
· Is the average work load too high?
在这个列表的最后一个“测试”中,它指出看门狗
守护程序可以检查平均工作负载是否过高:
Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12!
根据man
页面和前面的日志消息,似乎看门狗
并不是因为文件系统而重新启动服务器,而是因为服务器的负载平均值。
提示
在继续之前,让我们注意到在 01:50:02,看门狗
进程启动了重新启动。
是什么导致了高负载平均值?
虽然我们已经确定了重新启动服务器的原因,但我们仍然没有找到问题的根本原因。我们仍然需要弄清楚是什么导致了高负载平均值。不幸的是,这被归类为重新启动期间丢失的信息。
如果系统仍然经历着高负载平均值,我们可以简单地使用top
或ps
来找出哪些进程正在使用最多的 CPU 时间。然而,一旦系统重新启动,任何导致高负载平均值的进程都将被重新启动。
除非这些进程再次导致高负载平均值,否则我们无法确定来源。
$ w
02:13:07 up 23 min, 1 user, load average: 0.00, 0.01, 0.05
USER TTY LOGIN@ IDLE JCPU PCPU WHAT
vagrant pts/0 01:59 3.00s 0.26s 0.10s sshd: vagrant [priv]
然而,我们能够确定负载平均值开始增加的时间和增加到多高。随着我们进一步调查,这些信息可能会有用,因为我们可以用它来确定问题开始出现的时间。
要查看负载平均值的历史视图,我们可以使用sar
命令:
$ sar
幸运的是,看起来sar
命令的收集间隔设置为每2
分钟。默认值为 10 分钟,这意味着我们通常会看到每 10 分钟的一行:
01:42:01 AM all 0.01 0.00 0.06 0.00 0.00 99.92
01:44:01 AM all 0.01 0.00 0.06 0.00 0.00 99.93
01:46:01 AM all 0.01 0.00 0.06 0.00 0.00 99.93
01:48:01 AM all 33.49 0.00 2.14 0.00 0.00 64.37
01:50:05 AM all 87.80 0.00 12.19 0.00 0.00 0.01
Average: all 3.31 0.00 0.45 0.00 0.00 96.24
01:50:23 AM LINUX RESTART
01:52:01 AM CPU %user %nice %system %iowait %steal %idle
01:54:01 AM all 0.01 0.00 0.06 0.00 0.00 99.93
01:56:01 AM all 0.01 0.00 0.05 0.00 0.00 99.94
01:58:01 AM all 0.01 0.00 0.05 0.00 0.00 99.94
02:00:01 AM all 0.03 0.00 0.10 0.00 0.00 99.87
从输出中可以看出,在01:46
,这个系统几乎没有 CPU 使用率。然而,从01:48
开始,用户空间的 CPU 利用率达到了33
%。
此外,似乎在01:50
,sar
能够捕获到 CPU 利用率达到99.99
%,其中用户使用了87.8
%,系统使用了12.19
%。
提示
以上都是我们在根本原因总结中可以使用的好事实。
有了这个,我们现在知道我们的问题是在01:44
和01:46
之间开始的,我们可以从 CPU 使用情况中看出。
让我们使用-q
标志来查看负载平均值,看看负载平均值是否与 CPU 利用率匹配:
# sar -q
Again, we can narrow events down even further:
01:42:01 AM 0 145 0.00 0.01 0.02 0
01:44:01 AM 0 145 0.00 0.01 0.02 0
01:46:01 AM 0 144 0.00 0.01 0.02 0
01:48:01 AM 14 164 4.43 1.12 0.39 0
01:50:05 AM 37 189 25.19 9.14 3.35 0
Average: 1 147 0.85 0.30 0.13 0
01:50:23 AM LINUX RESTART
01:52:01 AM runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked
01:54:01 AM 0 143 0.01 0.04 0.02 0
01:56:01 AM 1 138 0.00 0.02 0.02 0
01:58:01 AM 0 138 0.00 0.01 0.02 0
02:00:01 AM 0 141 0.00 0.01 0.02 0
通过负载平均的测量,我们可以看到即使在01:46
时 CPU 利用率很高,一切都很平静。然而,在接下来的01:48
运行中,我们可以看到运行队列为 14,1 分钟负载平均值为 4。
运行队列和负载平均值是什么?
由于我们正在查看运行队列和负载平均值,让我们花一点时间来理解这些值的含义。
在一个非常基本的概念中,运行队列值显示了处于等待执行状态的进程数量。
更多细节,请考虑一下 CPU 及其工作原理。单个 CPU 一次只能执行一个任务。如今大多数服务器都有多个核心,有时每台服务器还有多个处理器。在 Linux 上,每个核心和线程(对于超线程 CPU)都被视为单个 CPU。
每个 CPU 都能一次执行一个任务。如果我们有两个 CPU 服务器,我们的服务器可以同时执行两个任务。
假设我们的双 CPU 系统需要同时执行四个任务。系统可以执行其中两个任务,但另外两个任务必须等到前两个任务完成后才能执行。当出现这种情况时,等待的进程将被放入“运行队列”。当系统中有进程在运行队列中时,它们将被优先处理,并在 CPU 可用时执行。
在我们的sar
捕获中,我们可以看到 01:48 时运行队列值为 14;这意味着在那一刻,有 14 个任务在运行队列中等待 CPU。
负载平均值
负载平均值与运行队列有些不同,但并不完全相同。负载平均值是在一定时间内的平均运行队列值。在我们前面的例子中,我们可以看到ldavg-1
(这一列是最近一分钟的平均运行队列长度)。
运行队列值和 1 分钟负载平均值可能会有所不同,因为由sar
报告的运行队列值是在执行时的值,而 1 分钟负载平均值是 60 秒内的运行队列平均值。
01:46:01 AM 0 144 0.00 0.01 0.02 0
01:48:01 AM 14 164 4.43 1.12 0.39 0
01:50:05 AM 37 189 25.19 9.14 3.35 0
高运行队列的单次捕获未必意味着存在问题,特别是如果 1 分钟负载平均值不高的话。然而,在我们的例子中,我们可以看到在01:48
时,我们的运行队列中有 14 个任务在队列中,在01:50
时,我们的运行队列中有 37 个任务在队列中。
另外,我们可以看到在01:50
时,我们的 1 分钟负载平均值为 25。
根据与 CPU 利用率的重叠,似乎大约在 01:46 - 01:48 左右,发生了导致 CPU 利用率高的事件。除了这种高利用率外,还有许多需要执行但无法执行的任务。
提示
我们应该花一点时间记录下我们在sar
中看到的时间和值,因为这些将是根本原因总结所必需的细节。
调查文件系统是否已满
早些时候,我们注意到文件系统已经满了。不幸的是,我们安装的sysstat
版本没有捕获磁盘空间使用情况。一个有用的事情是确定文件系统填满的时间与我们的运行队列开始增加的时间相比:
Jul 5 01:48:01 localhost auditd[560]: Audit daemon is low on disk space for logging
Jul 5 01:48:01 localhost auditd[560]: Audit daemon is suspending logging due to low disk space.
从我们之前看到的日志消息中,我们可以看到auditd
进程在01:48
识别出低磁盘空间。这与我们看到运行队列急剧增加的时间非常接近。
这正建立在一个假设的基础上,即问题的根本原因是文件系统填满,导致一个进程要么启动了许多 CPU 密集型任务,要么阻塞了 CPU 以执行其他任务。
虽然这是一个合理的理论,但我们必须证明它是真实的。我们可以更接近证明这一点的方法之一是确定在这个系统上利用了大部分磁盘空间的是什么:
# du -k / | sort -nk 1 | tail -25
64708 /var/cache/yum/x86_64/7/epel
67584 /var/cache/yum/x86_64/7/base
68668 /usr/lib/firmware
75888 /usr/lib/modules/3.10.0-123.el7.x86_64/kernel/drivers
80172 /boot
95384 /usr/share/locale
103548 /usr/lib/locale
105900 /usr/lib/modules/3.10.0-123.el7.x86_64/kernel
116080 /usr/lib/modules
116080 /usr/lib/modules/3.10.0-123.el7.x86_64
148276 /usr/bin
162980 /usr/lib64
183640 /var/cache/yum
183640 /var/cache/yum/x86_64
183640 /var/cache/yum/x86_64/7
184396 /var/cache
285240 /usr/share
317628 /var
328524 /usr/lib
1040924 /usr
2512948 /opt/myapp/logs
34218392 /opt/myapp/queue
36731428 /opt/myapp
36755164 /opt
38222996 /
前面的一行代码是一个非常有用的方法,用于识别哪些目录或文件使用了最多的空间。
du 命令
前面的一行命令使用了sort
命令,你在第十一章中学到了有关sort
命令的知识,从常见故障中恢复,对du
的输出进行排序。du
命令是一个非常有用的命令,可以估算给定目录使用的空间量。
例如,如果我们想知道/var/tmp
目录使用了多少空间,我们可以很容易地通过以下du
命令来确定:
# du -h /var/tmp
0 /var/tmp/systemd-private-Wu4ixe/tmp
0 /var/tmp/systemd-private-Wu4ixe
0 /var/tmp/systemd-private-pAN90Q/tmp
0 /var/tmp/systemd-private-pAN90Q
160K /var/tmp
du
的一个有用属性是,默认情况下,它不仅会列出/var/tmp
,还会列出其中的目录。我们可以看到有几个目录里面什么都没有,但/var/tmp/
目录包含了 160 kb 的数据。
# du -h /var/tmp/
0 /var/tmp/systemd-private-Wu4ixe/tmp
0 /var/tmp/systemd-private-Wu4ixe
0 /var/tmp/systemd-private-pAN90Q/tmp
0 /var/tmp/systemd-private-pAN90Q
4.0K /var/tmp/somedir
164K /var/tmp/
注意
重要的是要知道/var/tmp
的大小是/var/tmp
中的内容的大小,其中包括其他子目录。
为了说明前面的观点,我创建了一个名为somedir
的目录,并在其中放了一个 4 kb 的文件。我们可以从随后的du
命令中看到,/var/tmp
目录现在显示已使用 164 kb。
du
命令有很多标志,可以让我们改变它输出磁盘使用情况的方式。在前面的例子中,由于-h
标志的存在,这些值以人类可读的格式打印出来。在一行命令中,由于-k
标志的存在,这些值以千字节表示:
2512948 /opt/myapp/logs
34218392 /opt/myapp/queue
36731428 /opt/myapp
36755164 /opt
38222996 /
如果我们回到一行命令,我们可以从输出中看到,在/
中使用的 38 GB 中,有 34 GB 在/opt/myapp/queue
目录中。这个目录对我们来说非常熟悉,因为我们在之前的章节中曾解决过这个目录的问题。
根据我们以往的经验,我们知道这个目录用于排队接收自定义应用程序接收的消息。
考虑到这个目录的大小,有可能在重新启动之前,自定义应用程序在这台服务器上运行,并填满了文件系统。
我们已经知道这个目录占用了系统上大部分的空间。确定这个目录中最后一个文件的创建时间将会很有用,因为这将给我们一个大致的应用上次运行的时间范围:
# ls -l
total 368572
drwxrwxr-x. 2 vagrant vagrant 40 Jun 10 17:03 bin
drwxrwxr-x. 2 vagrant vagrant 23 Jun 10 16:55 conf
drwxrwxr-x. 2 vagrant vagrant 49 Jun 10 16:40 logs
drwxr-xr-x. 2 root root 272932864 Jul 5 01:50 queue
-rwxr-xr-x. 1 vagrant vagrant 116 Jun 10 16:56 start.sh
我们实际上可以通过在/opt/myapp
目录中执行ls
来做到这一点。从前面的输出中,我们可以看到queue/
目录上次修改是在 7 月 5 日 01:50。这与我们的问题非常吻合,至少证明了在重新启动之前自定义应用程序是在运行的。
提示
这个目录上次更新的时间戳以及这个应用程序运行的事实都是我们在总结中要记录的项目。
根据前面的信息,我们可以在这一点上安全地说,在事故发生时,自定义应用程序正在运行,并且已经创建了足够的文件来填满文件系统。
我们还可以说,在文件系统达到 100%利用率时,服务器的负载平均值突然飙升。
根据这些事实,我们可以提出一个假设;我们目前的工作理论是,一旦应用程序填满了文件系统,它就不再能创建文件。这可能导致相同的应用程序阻塞 CPU 时间或产生许多 CPU 任务,从而导致负载平均值升高。
为什么队列目录没有被处理?
由于我们知道自定义应用程序是文件系统问题的根源,我们还需要回答为什么。
在之前的章节中,你学到了这个应用程序的队列目录是由作为vagrant
用户运行的cronjob
处理的。让我们通过查看/var/log/cron
日志文件来看一下上次运行该 cron 作业的时间:
Jun 6 15:28:01 localhost CROND[3115]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null)
根据/var/log/cron
目录的记录,作业上次运行的时间是6 月 6 日
。这个时间线大致与这个进程被移动到另一个系统的时间相吻合,之后服务器就没有内存了。
处理器作业是否停止了但应用程序没有停止?可能是,我们知道应用程序正在运行,但让我们检查一下processor
作业。
我们可以使用crontab
命令检查处理器作业是否已被删除:
# crontab -l -u vagrant
#*/4 * * * * /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null
-l
(列出)标志将导致crontab
命令打印或列出为执行它的用户定义的 cronjobs。当添加-u
(用户)标志时,它允许我们指定要列出 cronjobs 的用户,在这种情况下是vagrant
用户。
从列表中看来,processor
作业并没有被删除,而是被禁用了。我们可以看到它已被禁用,因为该行以#
开头,这用于在crontab
文件中指定注释。
这基本上将工作变成了一条注释,而不是一个计划任务。这意味着crond
进程不会执行这项工作。
您所学到的内容的检查点
在这一点上,让我们对我们能够确定和收集的内容进行一次检查点。
登录系统后,我们能够确定服务器已经重新启动。我们能够在/var/log/messages
中看到watchdog
进程负责重新启动服务器:
Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12!
根据/var/log/messages
中的日志消息,看门狗进程因负载过高而重新启动了服务器。从sar
中,我们可以看到负载平均值在几分钟内从 0 上升到 25。
在进行调查时,我们还能够确定服务器的/
(根)文件系统已满。不仅满了,而且有趣的是,在系统重新启动前几分钟它大约使用了 100%。
文件系统处于这种状态的原因是因为/opt/myapp
中的自定义应用程序仍在运行并在/opt/myapp/queue
中创建文件。然而,清除此队列的作业未运行,因为它已在 vagrant 用户的crontab
中被注释掉。
基于此,我们可以说我们问题的根本原因很可能是由于文件系统填满,这是由于应用程序正在运行但未处理消息造成的。
有时你不能证明一切
在这一点上,我们已经确定了导致负载平均值升高的几乎所有原因。由于我们没有在事件发生时运行的进程的快照,我们无法确定是自定义应用程序。根据我们能够收集到的信息,我们也无法确定是因为文件系统填满而触发的。
我们可以通过在另一个系统中复制此场景来测试这个理论,但这不一定是在周末凌晨 2:00 要做的事情。通常,将问题复制到这个程度通常是作为后续活动来执行的。
在这一点上,根据我们找到的数据,我们可以相当肯定地确定根本原因。在许多情况下,这是你能得到的最接近的,因为你可能没有时间收集数据,或者根本没有数据来确定根本原因。
防止再次发生
由于我们对发生的原因有了相当自信的假设,现在我们可以继续进行我们根本原因分析的最后一步;防止问题再次发生。
正如我们在本章开头讨论的那样,所有有用的根本原因分析报告都包括一个行动计划。有时,这个行动计划是在问题发生时立即执行的。有时,这个计划是作为长期解决方案稍后执行的。
对于我们的问题,我们将采取即时行动和长期行动。
即时行动
我们需要采取的第一个即时行动是确保系统的主要功能健康。在这种情况下,服务器的主要功能是为公司的博客提供服务。
通过在浏览器中访问博客地址很容易检查。从前面的截图中我们可以看到博客正在正常工作。为了确保,我们也可以验证 Apache 服务是否正在运行:
# systemctl status httpd
httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled)
Active: active (running) since Sun 2015-07-05 01:50:36 UTC; 3 days ago
Main PID: 1015 (httpd)
Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec"
CGroup: /system.slice/httpd.service
├─1015 /usr/sbin/httpd -DFOREGROUND
├─2315 /usr/sbin/httpd -DFOREGROUND
├─2316 /usr/sbin/httpd -DFOREGROUND
├─2318 /usr/sbin/httpd -DFOREGROUND
├─2319 /usr/sbin/httpd -DFOREGROUND
├─2321 /usr/sbin/httpd -DFOREGROUND
└─5687 /usr/sbin/httpd -DFOREGROUND
Jul 05 01:50:36 blog.example.com systemd[1]: Started The Apache HTTP Server.
从这个情况来看,我们的 Web 服务器自重启以来一直在线,这很好,因为这意味着博客自重启以来一直在工作。
提示
有时,根据系统的重要性,甚至在调查问题之前,首先验证系统是否正常运行可能是很重要的。与任何事情一样,这实际上取决于环境,因为关于哪个先来的硬性规定并不是绝对的。
现在我们知道博客正在正常工作,我们需要解决磁盘已满的问题。
# ls -la /opt/myapp/queue/ | wc -l
495151
与之前的章节一样,似乎queue
目录中有很多等待处理的消息。为了正确清除这些消息,我们需要手动运行processor
命令,但还需要进行一些额外的步骤:
# sysctl -w fs.file-max=500000
fs.file-max = 500000
我们必须采取的第一步是增加系统一次可以打开的文件数量。我们根据过去使用 processor 应用程序和大量消息的经验得知这一点。
# su - vagrant
$ ulimit -n 500000
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7855
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 500000
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 4096
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
第二步是增加对vagrant
用户施加的用户限制;具体来说,是增加打开文件数量的限制。这一步需要在我们执行processor
命令的同一个 shell 会话中执行。完成这一步后,我们可以手动执行processor
命令来处理排队的消息:
$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml
Initializing with configuration file /opt/myapp/conf/config.yml
- - - - - - - - - - - - - - - - - - - - - - - - - -
Starting message processing job
Added 495151 to queue
Processing 495151 messages
Processed 495151 messages
现在消息已经被处理,我们可以使用df
命令重新检查文件系统利用率:
# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 39G 3.8G 35G 10% /
devtmpfs 491M 0 491M 0% /dev
tmpfs 498M 0 498M 0% /dev/shm
tmpfs 498M 13M 485M 3% /run
tmpfs 498M 0 498M 0% /sys/fs/cgroup
/dev/sda1 497M 104M 394M 21% /boot
正如我们所看到的,/
文件系统的利用率已降至10%
。
为了确保我们不会再次填满这个文件系统,我们验证自定义应用程序当前是否已停止:
# ps -elf | grep myapp
0 R root 6535 2537 0 80 0 - 28160 - 15:09 pts/0 00:00:00 grep --color=auto myapp
由于我们看不到以应用程序命名的任何进程在运行,我们可以确信该应用程序当前未运行。
长期行动
这带我们来到我们的长期行动。长期行动是我们将在根本原因总结中推荐的行动,但此刻不会采取的行动。
建议的第一个长期行动是永久删除该系统中的自定义应用程序。由于我们知道该应用程序已迁移到另一个系统,因此在这台服务器上不再需要。但是,删除该应用程序不是我们应该在凌晨 2 点或在验证它是否真的不再需要之前就进行的事情。
第二个长期行动是调查添加监控解决方案,可以定期对运行中的进程和这些进程的 CPU/状态进行快照。如果在这次根本原因分析调查中有这些信息,我们将能够毫无疑问地证明哪个进程导致了高负载。由于这些信息不可用,我们只能做出合理的猜测。
再次强调,这不是我们想在深夜电话中处理的任务,而是标准工作日的事情。
根本原因分析示例
现在我们已经获得了所有需要的信息,让我们创建一个根本原因分析报告。实际上,这份报告可以是任何格式,但我发现以下内容比较有效。
问题总结
2015 年 7 月 5 日凌晨 1:50 左右,服务器blog.example.com
意外重启。由于服务器负载平均值过高,watchdog
进程启动了重启过程。
经过调查,高负载平均值似乎是由一个自定义的电子邮件应用程序引起的,尽管它已经迁移到另一台服务器,但仍处于运行状态。
根据可用数据,似乎应用程序占用了根文件系统的 100%。
虽然我无法获得重启前的进程状态,但似乎高负载平均值也可能是由于同一应用程序无法写入磁盘而引起的。
问题详情
事件报告的时间为 2015 年 7 月 5 日01:52
事件的时间线将是:
-
在
01:52
收到了一条短信警报,说明blog.example.com
通过 ICMP ping 不可访问。 -
执行的第一步故障排除是对服务器进行 ping:
-
ping 显示服务器在线
-
在
01:59
登录服务器并确定服务器已重新启动。 -
搜索
/var/log/messages
文件,并确定watchdog
进程在01:50:12
重新启动了服务器: -
watchdog
在01:50:02
开始了重新启动过程 -
在调查过程中,我们发现在事件发生时没有用户登录
-
服务器在
01:50:32
开始了引导过程 -
在调查过程中,发现服务器在
01:48:01
已经没有可用的磁盘空间。 -
该系统的负载平均值在大约相同的时间开始增加,达到
01:50:05
时为 25。 -
我们确定
/opt/myapp/queue
目录在01:50
最后修改,并包含大约 34GB 的数据,导致 100%的磁盘利用率: -
这表明自定义电子邮件应用程序一直在服务器重新启动之前运行
-
我们发现自 6 月 6 日以来
processor
作业没有运行,这意味着消息没有被处理。
根本原因
由于自定义应用程序在未通过 cron 执行processor
作业的情况下运行,文件系统达到 100%利用率。收集的数据表明这导致了高负载平均值,触发了watchdog
进程重新启动服务器。
行动计划
我们应该采取以下步骤:
-
验证 Apache 正在运行并且
Blog
是可访问的 -
验证系统重新启动后自定义应用程序未在运行
-
在 02:15 手动执行了处理器作业,解决了磁盘空间问题
需要采取进一步行动
-
从服务器中删除自定义应用程序,以防止应用程序意外启动
-
调查添加进程列表监视,以捕获在类似问题期间利用 CPU 时间的进程:
-
将有助于解决类似情况
正如您在前面的报告中所看到的,我们有一个高层次的时间线,显示了我们能够确定的内容,我们如何确定的,以及我们采取的解决问题的行动。这是一个良好的根本原因分析的所有关键组成部分。
总结
在本章中,我们介绍了如何应对一个非常困难的问题:意外的重新启动。我们使用了本书中看到的工具和方法来确定根本原因并创建根本原因报告。
我们在整本书中大量使用日志文件;在本章中,我们能够使用这些日志文件来识别重新启动服务器的进程。我们还确定了watchdog
决定重新启动服务器的原因,这是由于高负载平均值。
我们能够使用sar
、df
、du
和ls
等工具来确定高负载平均值的时间和原因。这些工具都是您在整本书中学到的命令。
通过本章,我们涵盖了本书中早期涵盖的许多示例。您学会了如何解决 Web 应用程序、性能问题、自定义应用程序和硬件问题。我们使用了真实世界的示例和解决方案。
尽管本书涵盖了相当多的主题,但本书的目标是向您展示如何解决红帽企业 Linux 系统的故障排除问题。示例可能很常见,也可能有些罕见,但这些示例中使用的命令是在故障排除过程中日常使用的命令。所涵盖的主题都提供了与 Linux 相关的核心能力,并将为您提供解决本书未直接涵盖的问题所需的知识。