精通-Ubuntu-服务器-全-
精通 Ubuntu 服务器(全)
原文:
annas-archive.org/md5/118307b747c51933266231837979f8d8译者:飞龙
前言
Linux 是一个令人兴奋的平台。你几乎可以在任何地方找到它——桌面、笔记本、手机、嵌入式设备,尤其是服务器。Linux 服务器为全球许多著名的技术提供动力,并且在物理和云服务器环境中具有庞大的存在。Linux 服务器具有可扩展性、稳定性和强大功能。
早在 1991 年,芬兰一位名为 Linus Torvalds 的计算机科学学生将 Linux 内核作为学校项目创建了。那个时候他并不知道,Linux 将帮助我们见证一些基础设施设计领域最激动人心的创新。诸如多核处理器、虚拟化、容器化和云计算(仅举几例)等技术,已经改变了全球的数据中心,而 Linux 一直是这些进步的一部分。
Ubuntu 是一个流行的 Linux 发行版,一直是 Linux 被广泛采用的重要推动力。自 2004 年首次发布以来,Ubuntu 使得 Linux 更加易于新手使用,同时为服务器管理员提供了强大的功能。Ubuntu 不仅可以在笔记本电脑和工作站上找到,还可以在服务器上运行。
Ubuntu Server 使管理员能够创建高效、灵活且高度可用的服务器,为组织提供开源的强大力量。作为 Ubuntu 管理员,我们身处良好的环境——根据 W3Techs 的数据,Ubuntu 是 Linux 系统中在网络上最广泛部署的发行版。随着 Ubuntu 22.04 的发布,这一平台变得更加令人兴奋!
在本书中,我们将深入探讨 Ubuntu Server,你将学习所有管理服务器所需的概念,并配置它们以执行各种有趣的任务,例如提供网页服务、管理虚拟机、运行容器、自动化配置、与其他用户共享文件,甚至在云中运行 Ubuntu。
我们将从第一章开始我们的旅程,在这一章中,我们将走过安装 Ubuntu Server 22.04 的过程,它将作为本书其余部分的基础。在接下来的旅程中,我们将学习如何管理用户、连接网络和控制进程。随后,我们将实现一些重要的技术,如 DHCP、DNS、Apache、MariaDB 等。我们甚至会在过程中设置自己的 Nextcloud 服务器。
最后,本书的结尾部分将讨论我们可以做些什么来排除故障,以及如何防止和从灾难中恢复。
本书适合谁阅读
本书适合具备中级或高级初学者 Linux 技能的读者,帮助他们了解如何使用 Ubuntu Server 设置服务器。本书假设读者已经掌握 Linux 的基础知识,例如编辑配置文件和运行基本命令。此外,本书假设读者具备基础的网络概念理解,如 DHCP、DNS、IP 地址等。并且假设读者理解基本网络硬件的用途,如路由器、交换机以及类似设备。
本书涵盖的内容
第一章,部署 Ubuntu 服务器,介绍了 Ubuntu 服务器的安装过程。本章将带你完成创建启动介质和安装过程。
第二章,管理用户和权限,全面介绍了用户管理。本章内容包括创建和删除用户、配置密码策略、使用sudo命令、进行组管理以及在用户之间切换。
第三章,管理软件包,引导读者完成搜索、安装和管理软件包的过程。内容包括管理 APT 仓库、安装软件包,甚至包括 Snap 软件包的介绍。
第四章,导航和基本命令,教授了在目录树中导航、查看日志文件内容以及浏览日志文件所需的基本命令。
第五章,管理文件和目录,在前一章的基础上扩展了知识,并通过讲解如何编辑、复制、移动和重命名文件,完善了你的必备命令工具集。
第六章,提升命令行效率,介绍了一些额外的技巧、窍门和方法,以增强读者在使用命令行时的效率。内容包括管理输出、查看 Bash 历史记录等。
第七章,控制和管理进程,教你如何管理服务器上正在运行的程序,并且如何停止不正常的进程。内容包括查看 htop、使用 systemd 以及管理作业。
第八章,监控系统资源,介绍了如何管理服务器上的重要系统资源,例如查看磁盘和内存使用情况、理解负载平均值以及它如何影响 CPU。
第九章,管理存储卷,介绍了存储卷的相关内容。你将学习如何查看磁盘使用情况、格式化卷、管理/etc/fstab文件、使用 LVM 等。此外,我们还将讨论如何管理交换空间(swap)。
第十章,连接到网络,探讨了 Ubuntu 中的网络配置,特别是如何连接到其他节点的资源。我们将讨论如何分配 IP 地址、通过 OpenSSH 连接其他节点以及名称解析。
第十一章,设置网络服务,重新探讨了更高级的网络概念。在本章中,读者将学习更多关于将我们的网络连接在一起的技术,如 DHCP 和 DNS。读者将设置自己的 DHCP 和 DNS 服务器,并安装 NTP。
第十二章,共享与传输文件,专注于如何与他人共享文件。内容包括 Samba 和 NFS 网络共享的设置,我们还将介绍如何使用rsync和scp手动传输文件。
第十三章,管理数据库,带领读者完成通过 MariaDB 设置和管理数据库的过程。读者将学习如何安装 MariaDB、如何设置数据库以及如何创建辅助数据库服务器。
第十四章,提供 Web 内容,介绍了如何使用 Apache 提供内容。此外,读者还将学习如何使用 SSL 证书来保护 Apache,如何管理模块,甚至在章节结束时搭建一个 Nextcloud 服务器。
第十五章,使用 Ansible 自动化服务器配置,将向读者展示如何设置一个用于存储配置管理脚本的 Git 仓库,如何使用强大的 Ansible 工具来自动化常见的管理任务,并且还会介绍如何使用ansible-pull。
第十六章,虚拟化,讲解了虚拟化的所有内容(不出所料!)。读者将学习如何设置自己的 KVM 安装,以及如何使用 virt-manager 管理虚拟机。
第十七章,运行容器,讨论了容器的相关内容,并展示了如何在 Docker 和 LXD 中管理容器。
第十八章,容器编排,教你如何将容器提升到一个新的水平,并通过 Kubernetes 的强大功能进行管理。你不仅会学到如何安装 Micro K8s,还会学会如何从零开始构建自己的 Kubernetes 集群。
第十九章,在云中部署 Ubuntu,展示了如何在云中启动 Ubuntu 服务器,并介绍了亚马逊网络服务(AWS)。
第二十章,使用 Terraform 自动化云部署,讲解了如何通过 Terraform 自动化构建云基础设施的过程。
第二十一章,保护服务器安全,介绍了读者可以采取的各种措施来加强 Ubuntu 服务器的安全性。话题包括减少攻击面、保护 OpenSSH、设置防火墙等内容。
第二十二章,故障排除 Ubuntu 服务器,包含了当我们的部署没有按照计划进行时,可以采取的措施。读者还将研究问题空间、查看系统日志并追踪网络问题。
第二十三章,防范灾难,向读者介绍了可以用于预防和恢复灾难的各种策略。这包括利用 Git 进行配置管理、实施备份计划等内容。
为了最大限度地利用本书
本书适合已经有一定 Linux 经验的读者,尽管不一定非要是 Ubuntu 的经验。理想情况下,读者应了解基本的 Linux 命令行技能,例如切换目录、列出内容以及以普通用户或 root 用户身份执行命令。还希望读者了解基础的网络概念,如 DHCP、DNS 的作用以及 IP 地址分配。即使您觉得自己需要复习,仍然应该阅读本书——开头几章会涵盖许多这些概念。
本书将带您了解我们可以部署 Ubuntu Server 的实际场景。这将包括安装过程、提供网页服务、设置数据库等内容。具体而言,目标是提高生产力。每一章都会教会读者一个新的、有价值的概念,使用与实际组织相关的实用示例。基本上,我们专注于完成任务,而不是仅仅专注于理论。尽管 Linux 及其许多发行版的理论非常有趣,但本书的目标是让您处于这样一个位置:当同事或客户要求您在基于 Ubuntu 的服务器上执行工作时,您能够顺利完成任务。因此,如果您的目标是快速上手 Ubuntu Server 并学习真正重要的概念,那么本书绝对适合您。
若要跟随本书的教程,您需要一台安装了 Ubuntu Server 的服务器、一台来自云服务提供商的虚拟 Ubuntu 实例,或者一台至少能够运行一个虚拟机的笔记本或台式机。
下载示例代码文件
本书的代码包托管在 GitHub 上,链接为 github.com/PacktPublishing/Mastering-Ubuntu-Server-Fourth-Edition
我们还有其他来自我们丰富书籍和视频目录的代码包,您可以在 github.com/PacktPublishing/ 查看。赶快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:static.packt-cdn.com/downloads/9781803234243_ColorImages.pdf
使用的约定
本书中使用了若干文本约定。
CodeInText:表示文本中的代码字词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 用户名。以下是一个例子:“总之,回到我们的/etc/passwd文件。第五列用于用户信息,最常见的是用户的名字和姓氏。”
一段代码的设置如下:
description: External access profile
devices:
eth0:
name: eth0
nictype: bridged
parent: br0
type: nic
当我们希望特别强调代码块中的某一部分时,相关的行或项目会被高亮显示:
- name: Start the apache2 services
ansible.builtin.service:
name: apache2
state: started
**enabled: true**
任何命令行输入或输出都如下所示:
sudo apt install docker.io
粗体:表示新术语、重要词汇或屏幕上出现的词语。例如,在菜单或对话框中出现的词汇会以这种方式显示在文本中。例如:“从管理面板中选择系统信息。”
警告或重要说明如下所示。
提示和技巧如下所示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中注明书名。如果您对本书的任何内容有疑问,请通过questions@packtpub.com与我们联系。
勘误:尽管我们已尽一切努力确保内容的准确性,但难免会有错误。如果您发现本书中有错误,欢迎向我们报告。请访问www.packtpub.com/submit-errata,点击提交勘误并填写表格。
盗版:如果您在互联网上发现任何非法的作品复制品,我们将非常感谢您提供相关的网址或网站名称。请通过copyright@packtpub.com联系并提供链接。
如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣写书或参与书籍的创作,请访问authors.packtpub.com。
分享您的想法
阅读完《Mastering Ubuntu Server, 第四版》后,我们希望听听您的想法!请点击这里直接访问亚马逊评论页面并分享您的反馈。
您的评价对我们和技术社区都非常重要,它将帮助我们确保提供优质的内容。
第一章:1
部署 Ubuntu Server
Ubuntu Server 是一款功能极其强大的 Linux 发行版,专为服务器和网络设备设计。无论你是在设置高端数据库还是小型办公室文件服务器,Ubuntu Server 的灵活性都能够满足并超越你的需求。在本书中,我们将介绍所有常见的使用案例,帮助你最大限度地发挥这个令人兴奋的平台的优势。Ubuntu Server 结合了现代开发框架和稳定性,硬件支持使得它可以安装在最新的服务器硬件上。
在本章中,我将引导你完成从头到尾部署 Ubuntu Server 的过程。我们将从一些最佳实践的讨论开始,然后我们将获取软件并创建我们的安装介质。接下来,我将为你提供一个逐步的安装过程。到本章结束时,你将拥有一个属于你自己的 Ubuntu Server 安装,可以在本书的其余部分中使用。此外,由于 Canonical(Ubuntu 的开发者)现在为 Raspberry Pi 提供官方支持,我们也将探讨如何设置 Raspberry Pi。
本章中,我们将涵盖:
-
技术要求
-
确定服务器的角色
-
选择服务器设备
-
获取安装介质
-
创建可启动的闪存驱动器
-
安装 Ubuntu Server
-
在 Raspberry Pi 上安装 Ubuntu Server
为了开始,我们将首先查看一些部署 Ubuntu 服务器的技术要求。
技术要求
为了跟随本书中的示例,你需要安装一个 Ubuntu Server 进行操作。一般来说,以下规格是成功安装 Ubuntu Server 的估计最低要求:
-
64 位 CPU
-
1 GB RAM
-
10 GB 硬盘(推荐 16 GB 或更多)
64 位 CPU 支持现在成为一个要求,唯一的例外是 Raspberry Pi 版本。这是因为 Canonical 不再为 32 位 PC 和服务器处理器提供 Ubuntu 版本。虽然这看起来像是一个缺点,但如今所有售出的计算机都支持 64 位操作系统,并且自 2003 年起,消费级 CPU 就已经具备 64 位能力。即使你有一台旧 PC,觉得它可能无法运行 64 位操作系统,实际上你会惊讶地发现,甚至是较旧的 Pentium IV(尽管已经很老)也支持这一点,所以这个要求应该不难满足。现在不用担心这些细节,我们将在本章后面详细讨论这些要求。
现在我们了解了 Ubuntu Server 的技术要求,接下来让我们考虑一下我们的服务器将在组织中扮演什么角色。
确定服务器的角色
你很兴奋要设置一个 Ubuntu Server 安装,这样你可以按照本书中的示例进行操作。同样重要的是了解在现实世界中如何执行典型的服务器推出。每台服务器都必须有一个目的或角色。这个角色可以是数据库服务器、Web 服务器、文件服务器等等。简而言之,角色是服务器为你或你的组织增加的价值。有时,服务器可能仅用于测试实验性代码的目的。这也很重要——拥有测试环境是一个非常常见(也是值得的)做法。
一旦你了解了服务器在组织中的角色,你可以为其实施计划。这个系统是关键任务吗?如果由于某种原因这个服务器出现故障,它将如何影响你的组织?根据这个问题的答案,你可能只需为这个任务设置一个服务器,或者你可能希望计划冗余,以使服务器不成为故障的中心点。一个例子是 DNS 服务器,它会影响同事解析本地主机名和访问所需资源的能力。在某些情况下,添加第二个 DNS 服务器以在主服务器出现故障时接管可能是合理的。
另一个需要考虑的项目是服务器上驻留数据的机密性对你的环境有多重要。这直接关系到我们即将执行的安装过程,因为你将被询问是否想要使用加密。Ubuntu Server 在安装过程中提供的加密称为静态加密,它指的是存储在服务器内部存储卷中的数据。如果你的服务器注定要存储机密数据(会计信息、信用卡号码、员工或客户记录等),你可能要考虑利用这个选项。
加密你的硬盘是一个非常好的主意,可以防止那些有本地访问权限的坏人窃取数据。只要攻击者没有你的加密密钥,他们就无法窃取这些机密信息。然而,值得一提的是,任何有物理访问权限的人都可以轻松地销毁数据(无论是否加密),所以请记住保持服务器房间上锁!
在这本书的这一部分,我绝对不是要求你创建详细的实施图表或类似的东西,而是要记住在设置新服务器时应始终包含在对话中的一些概念。它需要有存在的理由,必须了解服务器数据的关键性和机密性,然后相应地设置服务器。一旦你练习了这些概念以及安装过程,你可以制定自己的服务器推出计划,以便在你的组织中使用。总而言之,理解基础设施中每个组件的目的是一种很好的思维方式。
到此为止,我们已经了解了如何为我们的服务器确定角色以及它如何融入我们的组织。在接下来的部分,我们将实际查看安装 Ubuntu Server 的过程,这样我们至少可以有一台测试机器用于书中的示例。
为我们的服务器选择设备
我敢打赌,你一定很兴奋想要设置自己安装的 Ubuntu Server,并深入探索。但在我们开始之前,我们必须决定到底要安装在哪个设备上。就本书的目的而言,硬件没有特别要求。你只需要一个 Ubuntu Server 安装版本,如果可以,设置多个服务器也是有益的——它们不必都是同一种设备类型。拥有多个服务器将帮助你在后续章节中进行网络实验。但现在,关键是利用你现有的设备来启动 Ubuntu 安装。
特别地,以下列表包括了你可以考虑用于安装 Ubuntu Server 的最常见设备:
-
物理服务器
-
物理桌面
-
笔记本电脑
-
虚拟机
-
虚拟私人服务器
-
树莓派
让我们更详细地看看这些选项。
物理服务器
现在,找到价格合理的二手物理服务器非常容易。戴尔 PowerEdge 是一种非常常见的型号,特别是 R610 和 R710 是很好的选择,二手市场上很容易找到。这些服务器通常在公司升级到更新型号后进入转售市场。虽然 R610 和 R710 已经有些年头,但它们的配置仍然适合测试用途。如果你能以合理的价格找到更新型号(如 R720),那就更好了。
物理服务器的缺点是它们占用大量空间,且往往消耗大量电力(并且噪音较大)。确保在不使用时关闭它们,并查看你所在地区的电力费用——这些服务器的运行成本可以非常低,也可以非常高,具体取决于你的电价。
物理桌面
如果你没有物理服务器,可以考虑在桌面上运行 Ubuntu Server。一些电脑用户在升级到新的设备后,往往会继续保留旧的 PC。因此,与其让你的旧桌面电脑积灰,不如让它发挥作用?当然,你的老旧电脑可能无法运行当今的高端游戏,但对于我们的目的来说这并不重要。Ubuntu Server 在老旧硬件上也能运行得非常好。实际上,很多家庭学习者都使用小型机箱 PC(如英特尔 NUC)来实现这一目的。此外,使用物理桌面也相较于物理服务器有一些优势。它们通常比服务器硬件消耗更少的电力,且噪音也较小。
在生产服务器(用于你组织内部的 Ubuntu Server 安装)方面,台式电脑通常并不是一个好的选择。根据不同的型号,实际的服务器硬件可能会具备一些台式电脑所没有的额外硬件和功能。例如,真正的服务器构建通常会配备硬件 RAID、错误更正码 (ECC) 内存、多处理器等功能。尽管台式电脑机箱通常缺少这些功能,但如果你在旧电脑上安装 Ubuntu Server,它仍然是一个有效的服务器,和实际的服务器机箱没有什么不同——尽管它在数据中心的扩展性较差。不过,由于我们现在只是学习,所以这些对我们当前的使用场景并不重要。
笔记本电脑
另一个值得考虑的选择是将 Ubuntu Server 安装在笔记本电脑上。如果你有一台不再使用的旧笔记本电脑,它可能是学习 Ubuntu Server 的一个很好的选择。如果你决定使用笔记本电脑来完成这个任务,那么与使用台式电脑时相同的因素也适用于此。
然而,我之所以决定在这里单独列出笔记本电脑,是因为它们具备一些额外的优势,值得我们利用。对于真实的数据中心服务器机架,你通常会接入键盘、视频和鼠标 (KVM) 。这可能意味着你可以将物理显示器、键盘和鼠标连接到服务器,或者使用集成这三者的特殊设备。数据中心内的服务器还会配备不间断电源 (UPS),即使断电,它们也能继续运行一段时间。
当谈到笔记本电脑时,它具备了相同的功能。笔记本电脑内置了键盘、鼠标和显示器。如果笔记本电脑中安装的电池正常工作,那么你也就有了不间断电源(UPS)。正因为这些原因,笔记本电脑可能在整体上相较于台式电脑具有一定的优势。
然而,就像台式电脑一样,笔记本电脑通常不适合在实际的数据中心使用。对于我们的使用场景而言,我们只需要安装一个或多个 Ubuntu Server 实例来完成本书中的示例任务。为了这个目的,专门使用任何你当前不使用的电脑就足够了。
虚拟机
如果你无法访问物理机器,可以考虑使用虚拟机 (VM)。如今大多数计算机都支持运行虚拟机的能力。VirtualBox 是一个很好的解决方案,它易于使用,并且可以在所有主要操作系统上使用。就像 Ubuntu 本身一样,VirtualBox 是免费的,因此通常是开始的最低成本选项。而且,VirtualBox 允许你轻松创建 Ubuntu 安装的快照,这样你可以在进行本书中的示例之前创建一个时间点备份,并在需要时恢复它,以便重复执行任务。仅仅是快照功能,就使得虚拟机对我们的需求尤其有吸引力。
使用 VirtualBox 的缺点是,您需要至少分配 1GB RAM 给您的 Ubuntu 服务器 VM,并且您的 CPU 需要支持虚拟化扩展,如果您的设备支持,则需要在计算机设置中启用它。
可以在这里下载 VirtualBox:www.virtualbox.org。
虚拟专用服务器
诸如 Amazon Web Services、Google Cloud、Linode、Microsoft Azure、Digital Ocean 等的服务允许您在云中设置 Ubuntu 服务器,以便通过 OpenSSH 连接和管理。选择虚拟专用服务器(VPS)选项有一些好处;您无需为大型物理服务器寻找空间,也不必担心电力使用问题。另一个好处是,在本章中甚至无需经历安装过程;当您选择在其平台上部署 Ubuntu 服务器时,云提供商将为您执行此操作。然而,主要缺点是 VPS 实例并非免费——您需要查看在此类服务器上运行 Ubuntu 的相关成本,并决定这种成本是否合理。一些 VPS 服务允许您每月以至少 5 美元的价格设置实例,这可以低于在某些地区运行物理服务器所需的电费成本。
树莓派
树莓派单元正在迅速成为用户喜爱的服务器使用案例。它们价格低廉(某些型号甚至低于 40 美元),并且电力消耗非常少——你可以将它们 24/7 保持通电,几乎不会对电费产生显著影响。事实上,它们的功耗大约相当于充电一部高端手机所需的电力。另一个好处是,树莓派通常比入门级 VPS 更强大。最便宜的 VPS 实例通常只有 1 个 CPU 核心和 1GB RAM,但现代树莓派配备四核 CPU,并且具有 2GB、4GB 或 8GB 的 RAM,具体取决于购买的型号。这意味着树莓派可能比更便宜的 VPS 实例性能更好。树莓派的缺点是,由于它们使用 ARM CPU 而不是 x86,因此某些应用程序不可用。这意味着本书中的一些示例在树莓派上将无法运行(尽管大多数示例可以)。
一旦选择了要在其上安装 Ubuntu 服务器的设备,我们可以继续进行。如果选择使用 VPS,您可以跳转到下一章,第二章,管理用户和权限,因为您不需要详细介绍安装过程。对于树莓派,如果您选择了这个平台,您可以跳到本章末尾,查看专门设置的部分。对于其他所有设备,请继续阅读 Ubuntu 服务器 Live 安装程序的详细步骤。
获取安装媒体
是时候开始了!如果你决定使用物理服务器、桌面、笔记本电脑或虚拟机作为测试服务器,那么你需要完成安装过程来设置 Ubuntu。别担心——这非常简单,甚至比你想象的还要简单,因为与旧版本相比,整个过程的步骤要少得多。如果你选择使用 VPS 或树莓派,则无需经过这个过程,因为 VPS 提供商会为你完成这个步骤,而树莓派有一种完全不同的设置方法(我们将在本章后面的在树莓派上安装 Ubuntu部分介绍)。
假设你已经决定使用一个需要通过安装程序的设备,我们需要下载 Ubuntu Server,然后创建可启动的安装介质来安装它。如何操作很大程度上取决于你的硬件。你的设备有光驱吗?它能从 USB 启动吗?请查阅设备的文档以了解详细信息。
如果可以的话,建议使用闪存驱动器进行安装,最好是使用 USB 3.0 或更高版本,因为它比 USB 2.0 更快。推荐使用闪存驱动器的原因是它们通常比 DVD 更快。
然而,如果你的设备较旧,你将没有选择的余地,因为老旧设备根本无法从 USB 启动。一般来说,如果可能的话,使用闪存驱动器,如果没有选择,才选择 DVD。
过去,Ubuntu Server 的 ISO 镜像可以用来创建可启动的 CD 或 DVD。但现在,可写 CD 的空间不足以支持下载大小。因此,如果你选择将其刻录到可启动的光盘介质上,至少需要一个可写的 DVD。
不幸的是,典型数据中心中服务器的年龄差异会带来一些不可预测性,尤其是在如何启动安装介质方面。刚开始接触服务器时,所有标准的机架式服务器通常都配有 3.5 英寸软盘驱动器,甚至一些更高端的服务器还配有光驱。而如今,服务器通常既没有软盘驱动器,也没有光驱。如果服务器有光驱,它可能会长时间没有使用,直到下次有人尝试使用它时才发现它可能已经坏了。有些服务器可以从 USB 启动,有些则不行。继续之前,请查阅硬件的文档并做相应的计划。你的服务器功能决定了你需要创建哪种类型的介质。
无论我们计划创建可启动的 USB 还是 DVD,我们只需要下载一个文件。在浏览器中导航到以下网站开始:www.ubuntu.com/download/server。
在这个页面上,我们将通过点击选项 2 – 手动服务器安装按钮来下载 Ubuntu 22.04 LTS:

图 1.1:Ubuntu Server 22.04 下载页面
接下来,您将看到一个或多个版本的 Ubuntu Server 可供下载:

图 1.2:展示 Ubuntu Server 22.04 下载选项
截至本章撰写时,Ubuntu Server 22.04 是最新版本,并作为主要下载选项显示。根据您阅读本书的时间,页面上可能还列出了其他版本的 Ubuntu Server,例如 22.10 和 23.04。Ubuntu 每六个月发布一次新版本。然而,本书仅涵盖 长期支持(LTS)版本,因为它享有五年的支持。
相比之下,非 LTS 版本(或“过渡发布版”)仅支持九个月,因此不适合用作生产服务器。组织通常不会使用非 LTS 版本,除非是为了在正式发布之前测试即将推出的功能,所以为了我们的目的,我们将坚持使用 LTS 版本。下载完成后,我们将得到一个 ISO 镜像文件,可以用来创建可引导的安装介质。
如果您是在设置虚拟机,那么从 Ubuntu 下载页面下载的 ISO 文件就是您所需要的;您不需要创建可引导的 DVD 或闪存驱动器。在这种情况下,您只需要创建虚拟机,将 ISO 文件附加到虚拟光驱上并启动它。接下来,安装程序应该会自动启动,您可以按照本章稍后“安装 Ubuntu Server”部分中描述的安装步骤继续操作。由于在虚拟化解决方案之间,启动 ISO 镜像的过程有所不同,因此逐一详细说明每种方法会是一项非常困难的任务。幸运的是,这个过程通常很简单,您可以在虚拟化管理程序的文档中找到详细信息。在大多数情况下,过程就像是将下载的 ISO 镜像附加到虚拟机上并启动它一样简单。
如果您的设备不支持从 USB 启动,且您需要创建一个可引导的 DVD,通常只需要下载 ISO 文件,然后右键单击该文件。在操作系统的右键菜单中,您应该有一个类似“刻录到磁盘”的选项。这适用于 Windows 以及大多数安装了刻录应用程序的 Linux 图形桌面环境。
具体步骤因系统而异,主要是因为这里涉及大量的软件组合。例如,我见过许多 Windows 系统,其中右键点击菜单中的刻录 DVD 选项被已安装的 CD/DVD 刻录软件删除。在这种情况下,你必须首先打开 CD/DVD 刻录软件并找到从下载的 ISO 文件创建介质的选项。尽管我很想在这里列出完整的过程,但通常没有两台 Windows 电脑预装相同的 CD/DVD 刻录软件。最好的经验法则是尝试右键点击文件,看看是否有该选项,如果没有,就参考你所用应用程序的文档。请记住,数据光盘并不是我们需要的类型,所以一定要寻找从 ISO 镜像创建介质的选项,否则你的光盘将对我们的目的毫无用处。
到此时,你应该已经下载了 Ubuntu Server 的 ISO 镜像文件。如果你打算使用 DVD 来安装 Ubuntu,你也应该已经创建好它。在接下来的部分,我将概述如何创建一个可引导的闪存驱动器,用于安装 Ubuntu Server。
创建一个可引导的闪存驱动器
创建一个可引导的 USB 闪存驱动器来安装 Ubuntu 的过程,过去在不同平台间差异很大。具体步骤根据你的工作站或笔记本电脑当前使用的是 Linux、Windows 还是 macOS 而有所不同。幸运的是,现在有了一种更简单的方法。如今,我推荐使用Etcher来创建可引导的介质。Etcher 的优势在于它将方法抽象化,使得无论你使用哪种操作系统,过程都是相同的,而且它将过程简化到最简单的形式。
我喜欢的另一个功能是 Etcher 很安全;它能防止你在制作可引导介质的过程中破坏当前操作系统。过去,你可能会使用 Linux 上的 dd 命令将 ISO 文件写入闪存驱动器。然而,如果你设置 dd 命令不正确,可能会把 ISO 文件写入当前操作系统,从而擦除整个硬盘。Etcher 不会让你犯这样的错误。
在继续之前,你需要准备一只 USB 闪存驱动器,它必须是空的,或者是你不介意被清空的。这一过程将完全擦除其内容,所以确保该设备上没有你可能需要的信息。闪存驱动器的容量应至少为 2 GB 或更大。考虑到现在很难找到小于 4 GB 的闪存驱动器,这应该比较容易获取。
首先,前往www.balena.io/etcher/,从他们的网站下载最新版本的应用程序并打开它。启动后,窗口看起来会类似于以下截图:

图 1.3:使用 Etcher 创建可引导的闪存驱动器
此时,点击 从文件闪存,这将打开一个新窗口,允许你选择之前下载的 ISO 文件。一旦选择了 ISO,点击 打开:

图 1.4:使用 Etcher 选择 ISO 镜像
如果你的闪存驱动器已经插入到计算机中,Etcher 应该会自动检测到它。如果你连接了多个闪存驱动器,或者 Etcher 选择了错误的驱动器,你可以点击 更改 并选择你希望使用的闪存驱动器:

图 1.5:使用 Etcher 选择不同的闪存驱动器
最后,点击 继续 启动该过程。此时,闪存驱动器将被转换为 Ubuntu Server 安装介质,可以用来启动安装过程:

图 1.6:Etcher 正在写入闪存驱动器
几分钟后(时间长短取决于你的硬件),闪存 过程将完成,你将能够继续并开始一些安装操作。在继续之前,我们应该先快速讨论一下分区问题。
规划分区布局
分区 硬盘允许你将硬盘划分成多个部分,为不同的应用程序或用途分配专门的存储空间。例如,你可以为 Apache 网络服务器共享的文件分配一个分区,这样对其他分区的更改就不会影响它。你甚至可以为你的网络文件共享分配一个分区——可能性是无穷的。每个分区都会挂载(附加)到一个特定的目录,任何发送到该目录的文件都会被写入到该独立分区。你为分区挂载目录起的名字是任意的,实际上你叫什么名字并不重要。Linux 系统的存储灵活性使你在分区和目录命名上都可以发挥创意,因为 Linux 文件系统在将存储设备挂载到特定文件夹时提供了更大的灵活性。
诚然,我们有些急于求成了。毕竟,我们才刚刚开始,本章的重点是帮助你设置一个基础的 Ubuntu Server 安装,以便为接下来的内容奠定基础。在安装过程中,我们将接受默认设置。然而,本节的目的是给你提供一些选项供你以后参考。到某个时候,你可能会想要发挥创意,尝试不同的分区布局。
使用自定义分区,你可以做一些非常巧妙的事情。例如,经过正确配置后,你可以在不丢失用户数据、日志等的情况下清除并重新加载操作系统。这是因为 Ubuntu Server 允许你在安装过程中根据需要划分存储。如果你已有包含数据的分区,你可以选择保持不变,并将其带到新的安装中。你只需要将挂载目录路径设置为与之前相同,恢复配置文件,应用程序就会像什么都没发生一样继续工作。
现实世界中常见的自定义分区例子之一是将/home目录单独分割成一个分区。由于用户通常在这里存储文件,你可以配置服务器,以便重新加载操作系统时不会影响他们的文件。当他们在服务器刷新后重新登录时,所有的文件都会在原来的位置。你甚至可以将 Apache Web 服务器共享的文件放在它们自己的分区上,这样也能保持这些文件不被丢失。这里可以做很多创新的配置。
这可能不言而喻,但在重新安装 Ubuntu 时,你应该备份那些包含不希望被擦除数据的分区(即使你不打算格式化这些分区)。原因是,一旦出错(实际上只需勾选一个框),你就可能轻易擦除该分区上的所有数据。在刷新服务器时,始终备份你的数据。
使用独立分区的另一个理由可能是创建边界或限制。如果你在服务器上运行的某个应用程序容易占满大量存储空间,你可以将该应用程序指向一个独立的分区,且该分区的大小有限。例如,日志文件就是一个很好的应用场景。日志文件是任何系统管理员在存储方面的噩梦。
日志在帮助你找出为什么某个程序崩溃时非常有用,但如果不小心,它们会很快占满硬盘。在我的经验中,服务器因为日志文件占满了服务器上唯一分区的所有可用空间,导致服务器突然停顿。这个分区就是整个磁盘,应用程序没有其他的边界。
尽管有更好的方法来处理过度的日志记录(如日志轮转、磁盘配额等),但使用单独的分区无疑会有所帮助。如果应用程序的日志目录在一个单独的分区上,它只会填满那个分区,而不会影响整个硬盘,这虽然会导致一些问题,但不会影响整个服务器。作为管理员,你需要权衡这些策略的优缺点,避免服务器过载,并制定一个最适合你组织需求的分区方案。
维护服务器的成功在于高效管理资源、用户和安全性——一个好的分区方案无疑是其中的一部分。有时候,这只是为了让自己省事,万一需要重新加载操作系统时,能够减少工作量。为了跟上本书的内容,Ubuntu Server 的安装或分区方式并不重要。关键是要成功安装——你可以随时练习分区。毕竟,学习的一部分就是搭建平台,观察可能出现的问题,然后进行修复。
下面是一些关于分区的基本技巧:
-
至少需要为根文件系统(由正斜杠表示)创建一个分区。
-
/var目录包含大多数日志文件,因此它是一个很好的分区候选,原因如本节前面所述。 -
/home目录存储所有用户文件。将其分离成一个独立的分区是有益的,因为这样可以让你的用户文件在重新安装 Ubuntu 时得以保留。 -
如果你以前使用过 Linux,可能会熟悉 交换分区 的概念,它是一个特殊的分区,当内存满时可以作为虚拟内存来使用。不过在新的 Ubuntu 版本中,这已经不再是必需的——交换文件 将会自动创建。
在下一节的安装过程中,我们将选择默认的分区方案,让你快速入门。不过,我建议你在未来某个时候再次回到安装过程,尝试不同的分区方式。你可能会想出一些巧妙的方式来划分存储。但这并不是必须的——根据你的需求,所有内容放在一个分区也是可以的。
现在我们已经了解了分区的概念,我们应该已经涵盖了所有必要的主题,能够实际开始安装 Ubuntu Server。让我们现在开始吧。
安装 Ubuntu Server
到此为止,我们应该已经准备好开始安装 Ubuntu Server。在接下来的步骤中,我会带你一步步完成安装过程。
要开始,你只需要将安装介质插入服务器或设备,并按照屏幕上的说明打开启动菜单。启动过程开始时按的键因机器而异,但通常是F10、F11 或 F12。如果不确定,请参考你的文档,尽管大多数电脑和服务器都会在开始时提示你按哪个键。你可能在前几次会错过这个时机,这没关系——直到今天,我似乎仍然需要重启机器一两次才能及时按下正确的键。
一旦你成功启动设备并使用你的 Ubuntu Server 安装介质,安装程序的导航就相对简单了。你只需使用方向键上下移动,选择不同的选项,然后按Enter键确认选择。Esc键将允许你退出子菜单。一旦熟悉了操作,安装程序中的导航非常容易。
一旦安装程序启动,你将看到第一个选择屏幕。第一个选项,Ubuntu Server,允许你启动 Ubuntu Server 安装程序。第二个选项,测试内存,运行一个特殊程序,帮助你确定设备内存条是否有物理缺陷。我总是建议每年至少测试一次设备的内存,尤其是在首次安装操作系统之前。内存问题虽然很少见,但你会感到惊讶。为了保险起见,测试一下内存也许是个不错的主意。
如果你希望测试设备的内存,可以继续进行此操作。不过,为了继续下去,我们需要选择Ubuntu Server选项,以便启动安装程序:

图 1.7:Ubuntu Server 安装介质的初始启动屏幕
接下来,你将看到一个允许你选择语言的屏幕,如下图所示:

图 1.8:安装过程开始时的语言选择
如果你的语言是英语以外的其他语言,你可以在这里选择。一旦你对所选择的键盘布局满意,继续选择屏幕底部的完成。
选择语言后,你将进入一个屏幕,可以设置你的键盘配置。如果没有自动选择正确的键盘布局,你可以在这里进行更改。

图 1.9:设置你的键盘布局
接下来,我们将看到一个选项来安装Ubuntu Server,或者Ubuntu Server(最小化)。在这里,我们选择第一个选项。选择安装最小化版本的 Ubuntu Server 可能适合创建较小的安装,但对于本书而言,我们将专注于常规安装类型。

图 1.10:选择你的安装类型
接下来,Ubuntu Server 安装程序将尝试自动检测适合你的网络卡的参数。默认情况下,它应该通过动态主机配置协议(DHCP)自动检测合适的设置。我们将在后续章节中详细讲解 DHCP,特别是第十一章,设置网络服务。现在,默认设置应该是可以的。选择完成以继续。

图 1.11:Ubuntu Server 安装程序中的网络配置
接下来,屏幕将给我们一个机会来设置代理地址(如果需要的话)。大多数读者不需要这个功能,只有那些需要的用户可能已经从网络管理员那里获得了特定的代理设置。因此,我们选择完成继续:

图 1.12:Ubuntu Server 安装程序中的代理设置
下一屏幕会显示设置镜像地址的选项。正如我们在第三章《管理软件包》中进一步讨论的那样,这个地址指的是可以下载可安装应用程序(软件包)的在线源。在某些情况下,一个组织可能会托管自己的软件仓库。
因此,在这个界面上,如果你有自己的服务器,可以选择不同的服务器来提供软件。对于我们的需求,我们将选择完成以接受默认设置并继续:

图 1.13:Ubuntu Server 安装程序中的镜像设置
接下来,我们会看到存储设置。特别是,这里是我们可以配置硬盘布局以进行安装的地方。在本章早些时候,我提到过采用自定义分区方案是一种常见做法。如果你有首选的分区布局,这是你可以通过选择自定义存储布局来输入决策的屏幕。
然而,这样做有点跳过了书中的内容,因此我建议选择完成来接受默认设置,这将覆盖整个硬盘:

图 1.14:安装 Ubuntu Server 时配置存储
在告诉安装程序如何设置服务器上的存储设备后,我们会看到一个屏幕,汇总了我们在配置存储时的选择。由于我们选择了默认布局,这个屏幕会展示一个基本布局。如果你决定设置一个自定义的分区方案,你需要确保这里显示的选择与您设想的相符。如果你对这些内容感到困惑,不必太担心。我们将在第九章《管理存储卷》中更详细地讲解存储相关内容。现在,选择完成接受默认的存储布局:

图 1.15:安装 Ubuntu Server 时的存储配置总结屏幕
如果你还不知道,这个过程将会清除你设备硬盘上的所有数据,并用新的 Ubuntu Server 安装替代它。这意味着,如果你没有备份设备上可能保存的重要数据,你将永远丢失这些信息。接下来的屏幕将解释这是在擦除硬盘并安装 Ubuntu Server 之前的最后一步。如果你确定你的硬盘上没有重要数据,可以选择继续:

图 1.16:确认安装并擦除硬盘的数据
当我们安装 Ubuntu Server 时,它将为我们创建一个初始用户账户作为安装过程的一部分。此用户将用于配置和维护服务器。在安装程序的这一部分,我们将有机会为此用户命名并设置密码。
在这个屏幕上填写您的信息,就像我在下面的截图中所做的那样。需要记住的一件重要事情是,您在这里创建的用户将具有管理员访问权限。稍后您可以创建其他用户,但在这里创建的用户将拥有特殊权限。完成后,箭头移到Done并按Enter键:

图 1.17:Ubuntu Server 安装程序,配置用户详细信息
接下来,你将看到一个安装 OpenSSH 服务器的选项。我建议你启用它。在远程管理 Ubuntu 服务器时,OpenSSH 是我们最常用的工具,详细内容在第十章 连接到网络中有讲解。现在,让我们启用这个选项,这样以后我们就少一件事要做了。要这么做,你可以在光标位于选项上时按空格键,在其复选框中添加一个星号符号,表示选择。完成后,选择Done继续:

图 1.18:在安装 Ubuntu Server 时选择是否设置 OpenSSH
接下来,我们将看到一些常用应用程序的选择,这些应用程序可以在 Ubuntu Server 上运行。例如,我们在这个屏幕上有 Nextcloud、Docker 等选择。现在,我们会忽略这些,因为在本书中我们将手动设置许多服务。因此,现在选择Done继续:

图 1.19:特色应用程序选择
就这样了 —— 安装程序将继续进行,因为它已经从您那里获得了所有需要的信息。完成后,在屏幕底部您将看到一个重新启动选项;请继续选择它:

图 1.20:安装完成,待重启
此时,您的服务器将重新启动,然后 Ubuntu Server 应该会立即启动。恭喜!您现在拥有自己的 Ubuntu Server 安装!
除了特定服务器硬件和虚拟机之外,Ubuntu Server 也可以安装在树莓派上,在下一节我们将详细了解这一点。
在树莓派上安装 Ubuntu
Raspberry Pi 平台已成为业界一个非常有价值的资产,并且是一个有用的服务器平台。这些微型计算机现在配备了四核处理器和最多 8GB 的 RAM,非常节能,且其性能足够强大,可以真正将它们转化为实际的服务器。在我的实验室里,我的网络上有几台 Raspberry Pi,每台都负责执行特定的任务或功能。在很多情况下,你甚至无法察觉它们是硬件性能较低的设备。也许更令人惊讶的是,Raspberry Pi 4 能够超越一些低端云实例,在不需要高端 CPU 的情况下,为你提供一个强大的服务器,且没有每月费用。所有这些,都是在如此小巧的外形中——这些设备比你的咖啡杯垫还要小!
Ubuntu Server 支持 Raspberry Pi 2、3 和 4 型号。要开始,只需访问 Raspberry Pi 版本的 Ubuntu 官方下载页面:ubuntu.com/download/raspberry-pi。这将带你到如下界面:

图 1.21:官方 Ubuntu 网站上 Raspberry Pi 的下载区
一旦进入,你会看到多个可供下载的 Pi 版本列表。参考前面的截图查看可用选项,但请记住,Ubuntu 随时可能重新设计其网页。因此,作为经验法则,下载与您所拥有的 Raspberry Pi 类型匹配的版本,如果有 64 位版本,最好选择 64 位版本(Raspberry Pi 2 只提供 32 位版本)。
Pi 版本的下载文件将以压缩镜像的形式提供,可以直接写入 SD 卡。为了开始这个过程,打开之前下载的 Etcher。打开 Etcher 后,点击Flash from file:

图 1.22:Etcher,准备开始操作
现在,选择之前下载的文件并点击Open:

图 1.23:选择要写入 SD 卡的文件
接下来,将一张 SD 卡插入计算机,这张卡将专门用于 Ubuntu Server。Etcher 会完全擦除 SD 卡,因此请确保你不需要卡上的任何数据。如果你的电脑没有 SD 卡槽,可以使用 USB 卡读卡器。如果没有选择正确的设备,可以点击Select target来选择要使用的 SD 卡,如图 1.24所示。然后点击Continue:

图 1.24:为 Raspberry Pi 选择使用的 SD 卡
一旦你选择了要使用的镜像文件和目标 SD 卡,你就可以开始了。点击Flash!开始写入镜像的过程:

图 1.25:Etcher,准备开始刷写 SD 卡
现在,Etcher 将准备目标设备并写入源镜像文件。根据硬件速度,过程可能需要 5 到 15 分钟:

图 1.26:Etcher 正在进行中
一旦 Etcher 完成向 SD 卡写入数据,你可以将其从电脑中弹出并插入 Pi 中,该设备可以通过 SSH 远程访问。当它第一次启动完成后,你可以登录并像使用其他任何 Ubuntu Server 安装一样开始使用它(用户名和密码默认为ubuntu)。从那里,你可以为自己创建一个新用户,安装应用程序并进行实验。
摘要
在本章中,我们详细介绍了几种不同的安装场景。与大多数 Linux 发行版一样,Ubuntu Server 是可扩展的,可以安装在各种服务器类型上。物理设备、虚拟机甚至 Raspberry Pi 都有可用的 Ubuntu 版本。安装过程已逐步讲解,现在你应该已经有了自己的 Ubuntu Server 实例,可以根据需要进行配置。本章还涉及了确定服务器角色、创建可启动媒体的过程,以及如何在你的服务器设备上(以及 Raspberry Pi 上)设置 Ubuntu Server 的详细步骤。现在,你已经开始掌握 Ubuntu Server 了!
在下一章中,我们将向你展示如何管理用户。你将能够创建用户、删除用户、更改用户、管理密码过期等。我们还将讨论权限,以便你可以确定用户在服务器上可以执行的操作。到时见!
相关教程
- 从 LearnLinuxTV 安装 Ubuntu Server(视频教程):
linux.video/install-ubuntu-server
加入我们的 Discord 社区
加入我们社区的 Discord 频道,与作者和其他读者讨论:

第二章:管理用户和权限
在上一章中,我们设置了自己的 Ubuntu 服务器安装,现在我们可以开始学习如何维护它,从管理谁能够使用我们的服务器开始。
作为 Ubuntu 服务器的管理员,用户既可以是你最大的资产,也可能是你最大的头痛。在你的职业生涯中,你将添加无数的新用户,管理他们的密码,在他们离开公司时删除他们的帐户,并授予或撤销他们对网络资源的访问权限。即使在你是唯一用户的服务器上,你也会发现自己在管理用户帐户,因为即使是系统进程也是以用户身份运行的。要成功地管理 Linux 服务器,你还需要知道如何管理权限、创建密码策略并限制谁可以在机器上执行管理命令。在本章中,我们将深入探讨这些概念,以便你清楚地了解如何管理用户及其资源。
具体来说,我们将涵盖:
-
理解用户和组的目的
-
理解何时使用
root -
创建和删除用户
-
理解
/etc/passwd和/etc/shadow文件 -
使用
/etc/skel分发默认配置文件 -
在用户之间切换
-
管理组
-
管理密码和密码策略
-
使用
sudo配置管理员访问权限 -
设置文件和目录的权限
在第一部分,我们将简要讨论管理用户的性质。
理解用户和组的目的
当谈到服务器时,用户非常重要——如果没有用户可供服务,那么根本就不需要服务器。在 IT 领域,用户管理这一主题本身是相当广泛的。关于认证的单个方法已经有整本书籍被写成,并且存在围绕它的技术(如轻量级目录访问协议,或LDAP)。在本章中,我们将重点介绍如何管理存在于我们服务器本地的用户以及帮助定义他们权限的组。
由于 Ubuntu Server 是一个 Linux 发行版,它采用了 Unix 风格的用户帐户、组和权限管理方式。尽管我们的重点是 Ubuntu,但本章中你将学习到的许多与用户管理相关的命令也适用于其他平台。有一些命令允许你添加、删除和更改用户,还有一些命令可以改变权限。
在服务器的上下文中,用户指的是谁(或什么)能够使用该服务器。例如,你可能有一个名叫 Susan 的会计,或一个名叫 Haneef 的 IT 管理员,他们都需要访问服务器。也许 Susan 只需要访问一个文件共享目录来存取与会计相关的文件,而 Haneef 可能作为系统管理员需要更多的访问权限。我们在服务器上创建的用户帐户将代表实际使用服务器的人。
组(Groups)允许我们将对特定文件和目录的访问进行隔离。正如我们稍后将学到的,文件和目录有用户和组的分配。当与权限结合使用时,我们就可以管理用户在服务器上可以执行的操作。
然而,用户并不总是指人类。我们的服务器上也有系统用户,这些用户可能会被应用程序和运行中的进程用于后台或自动化任务。一个例子可能是备份任务,可能有一个备份用户在后台运行任务,将重要文件复制到其他位置。你现在不需要担心与系统相关的用户,只需要知道它们的存在。随着我们阅读本书,你将会看到更多此类的例子。
更高级的组织可能会有一个中央登录服务器,例如Active Directory(AD)或标准的 LDAP。除此之外,还有其他类似的技术。本书中我们不会深入讨论这些技术,但请记住,中央认证服务器是你所在组织可能会采用的一种选择,若你决定进一步了解这方面的内容。
然而,最强大的用户是root。这个特殊的用户赋予我们最大权限,但正如你将在下一节看到的那样,这也伴随着一定的风险。
理解何时使用 root 用户
在上一章中,我们设置了自己独立的 Ubuntu 服务器安装。在安装过程中,我们被要求创建一个作为系统管理员的用户账户。所以,到目前为止,我们的服务器上应该至少有两个用户:前面提到的管理员用户,以及root用户。我们当然可以创建其他具有不同访问权限的用户账户(我们将在本章中进行此操作),但在我们开始之前,关于你创建的管理员账户以及为你创建的root用户的讨论是必要的。
root用户账户存在于所有 Linux 发行版中,是地球上最强大的用户账户。root用户账户可以用来在服务器内做任何事情,真的是任何事情。如果想在文件系统中的几乎任何地方创建文件和目录?想要安装软件?这些操作都可以通过root轻松完成。甚至,root账户可以通过一次打错字或错误命令摧毁整个安装:如果你指示root删除整个硬盘上的所有文件,它将毫不犹豫地执行这一操作。在 Linux 系统中,假定你正在使用root时,意味着你知道自己在做什么。因此,在以root身份执行任何命令时,通常不会有确认提示。它会按照指示执行,不论好坏。
正因为如此,我用过的每一个 Linux 发行版都表示,或者至少强烈建议,你在安装过程中创建一个标准用户。Linux 社区普遍建议,管理员应拥有自己的账户,并在需要root权限来完成任务时切换到root。这种做法不太可能因不小心的输入错误或错误命令而毁掉你的服务器。一些管理员会始终严格使用root,但同样,建议仅在必须时使用root。
大多数发行版要求你在安装过程中设置一个root密码,以保护该账户。即便是基于 Debian 的 Ubuntu,也要求你在安装过程中设置一个root密码。Ubuntu 只是决定做一些不同的事情。这样做的原因是,与许多其他发行版不同,Ubuntu 默认会完全锁定root账户。虽然默认情况下root账户不可用,但你仍然可以在登录后启用root,或者切换到root用户。默认禁用只是意味着root账户不像平时那样容易访问。如果你需要启用该账户,我会在本章稍后介绍如何操作。
这一规则的例外是一些 VPS 提供商,比如 Linode,即使是在它们的 Ubuntu 服务器上,也会启用root账户。有时,root密码会被随机生成并通过电子邮件发送给你。不过,你仍然应该创建一个具有管理员权限的用户账户。
Ubuntu(包括其服务器版本)推荐使用sudo,而不是直接使用root。具体来说,sudo使你能够以提升的权限运行单个命令,而不需要一直以root身份登录。
使用 sudo 运行特权命令
我会在本章稍后介绍如何管理sudo,但现在请记住,sudo的目的是让你可以使用用户账户执行通常只有root才能做的事情。例如,作为一个普通用户,你不能像下面这样发出安装软件包的命令(别担心现在的apt命令,我们将在第三章,软件包管理中介绍):
apt install tmux
反而,你会收到一个错误信息:
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
但是,如果你在命令前加上sudo(假设你的用户账户有权限使用它),命令将正常工作:
sudo apt install tmux
当您使用sudo时,系统会要求您输入用户密码以确认,然后命令将被执行。后续带有sudo前缀的命令可能不会再提示您输入密码,因为系统会缓存您的密码一段时间,直到超时或终端关闭。理解这一点应该能够澄清在安装期间创建的用户帐户的实用性。我之前提到这个用户是一个管理帐户,但实际上它只是一个能够使用sudo的用户帐户。Ubuntu Server 在安装期间会自动为第一个创建的用户帐户授予sudo访问权限。
所期望的是,您将使用该帐户来管理系统,而不是root。当您创建额外的用户帐户时,默认情况下它们将无法访问sudo,除非您明确授予它们权限。
创建和删除用户
在 Ubuntu 中创建用户可以通过两个命令之一完成:adduser和useradd。这一开始可能会有些混淆,因为这两个命令执行的是相同的操作(方式不同),并且命名非常相似。我会首先介绍useradd命令,然后解释adduser的区别。您甚至可能更喜欢后者,但我们稍后会详细讨论。
使用useradd
首先,这里是useradd命令实际应用的一个例子:
sudo useradd -d /home/jdoe -m jdoe
使用此命令,我创建了一个名为jdoe的用户。通过-d选项,我澄清我希望为此用户创建一个主目录,并且在此之后,我指定/home/jdoe作为用户的主目录。-m标志告诉系统我希望在过程中创建主目录;否则,我将不得不自己创建目录。最后,我指定了我的新用户的用户名(在本例中为jdoe)。
随着我们在本书中的进展,会有需要root权限才能执行的命令。前面的命令就是一个例子。对于需要这种权限的命令,我会在命令前加上sudo。当您看到这些命令时,这意味着需要root权限才能运行。对于这些命令,您也可以登录为root(如果root已启用),或者切换到root以执行这些命令。但是,如我之前提到的,强烈建议使用sudo而不是使用root帐户。
现在,使用以下命令列出/home的存储:
ls -l /home
您应该会看到一个列出我们新用户的文件夹:

图 2.1:在我们创建第一个用户后列出/home目录的内容
那么如何为我们的用户设置密码呢?由于使用了 sudo,可能会要求输入当前用户的密码,但并没有要求为新用户设置密码。要为用户创建密码,我们可以使用 passwd 命令。passwd 命令默认允许你更改当前登录用户的密码,但如果以 root 用户身份或通过 sudo 执行,它也允许你为任何其他用户设置密码。如果你单独输入 passwd,命令会首先要求你输入当前密码,然后是新密码,最后再确认一次新密码。如果在命令前加上 sudo 并指定其他用户帐户,你就可以为任何用户设置密码。以下是此过程的输出示例:

图 2.2:更改用户的密码
正如你在上一个截图中看到的,当使用 passwd 命令输入密码时,你不会看到任何星号或其他类型的输出。这是正常的。虽然你不会看到输入的视觉指示,但你的输入已经被识别。
现在我们有了一个新用户,并且我们已经为该用户设置了密码。jdoe 用户现在可以使用我们选择的密码访问系统。默认情况下,该用户无法使用 sudo,但我们将在本章稍后讲解如何更改这一点。
使用 adduser
之前,我提到过 adduser 命令作为创建用户的另一种方式。这个命令的区别(和便捷性)一旦使用后就会立刻显现。去尝试一下吧;执行 adduser 并指定你想创建的用户的用户名。以下是该过程的示例:

图 2.3:使用 adduser 命令创建用户
在前述过程中,我执行了 sudo adduser dscully(修改用户的命令需要 sudo 或 root 权限),然后我被问到一系列关于如何创建用户的问题。我被要求输入密码(两次)、全名、房间号、工作电话 和 家庭电话。在 其他 字段中,我输入了评论 Trust no one,这是管理用户时非常好的心态。最后的提示(在最终确认之前)都是可选的:我不必输入 全名、房间号 等等。如果我想跳过这些提示,我可以直接按 Enter 键。真正必须的只有用户名和密码。
从输出中,我们可以看到adduser命令为我们做了不少工作。该命令默认将/home/dscully作为用户的主目录,帐户被分配了下一个可用的用户 ID(UID)和组 ID(GID),其值为1002,并且它还将/etc/skel中的文件复制到了我们新用户的home目录中。实际上,adduser和useradd命令都会从/etc/skel复制文件,但adduser在执行操作时更加详细。
如果你还不明白UID、GID和/etc/skel是什么,不用担心,我们稍后会讲解这些概念。
总而言之,adduser命令在创建用户时更加方便,它会提示你输入各种选项,而无需记住命令行选项。它还会提供关于它所做操作的详细信息。此时,你可能会想,既然adduser看起来如此方便,为什么还要使用useradd呢?不幸的是,adduser并不是所有 Linux 发行版都提供的。最好还是熟悉一下useradd,以防你在一个非 Ubuntu 的 Linux 系统上遇到问题。
你可能会觉得看到adduser命令到底是什么很有趣。它甚至不是一个二进制程序——它是一个Shell 脚本。Shell 脚本其实就是一个可以作为程序执行的文本文件。现在你不需要太担心脚本的内容,因为我们会在第六章中讲解,提高你的命令行效率。就adduser而言,它是一个用Perl编写的脚本,Perl 是一种有时用于管理任务的编程语言。由于它不是二进制文件,你甚至可以在文本编辑器中打开它,查看它在后台执行的所有代码。然而,请确保不要在带有root权限的文本编辑器中打开该文件,以免不小心保存更改并破坏脚本。以下命令将在 Ubuntu Server 系统中打开adduser脚本:
nano /usr/sbin/adduser
使用上下箭头以及Page Up和Page Down键在文件中滚动。当完成后,按下Ctrl + x退出文本编辑器。如果编辑器提示你保存更改,请不要保存。无论如何,你们这些眼睛尖锐的人可能会注意到,adduser脚本调用了useradd来执行实际的工作。所以,不管怎样,你实际上是直接或间接地使用了useradd。
既然我们知道了如何创建用户,了解如何删除它们也是很有用的。
删除用户
当用户不再需要访问系统时,删除或禁用帐户非常重要,因为未管理的帐户往往会成为安全隐患。要删除用户帐户,我们将使用userdel命令。
然而,在删除账户之前,你应该问自己一个非常重要的问题:你(或其他人)是否需要访问该用户的文件?大多数公司都有文件保留政策,详细说明了当员工离职时应该如何处理用户的数据。有时,这些文件会被复制到存档中进行长期存储。通常,经理、同事或新员工可能需要访问前用户的文件,或许是为了继续完成某个项目。理解这一政策对管理用户非常重要。如果你没有明确规定离职用户文件的保留要求,最好与管理团队合作制定一项政策。
默认情况下,userdel命令不会删除用户的home目录的内容。在这里,我们使用以下命令将dscully从系统中删除:
sudo userdel dscully
我们可以通过输入以下命令来查看dscully用户的文件是否仍然存在:
ls -l /home
前面的命令将会产生以下输出:

图 2.4:尽管我们已经删除了用户,但dscully的主目录仍然存在。
由于dscully的/home目录仍然存在,我们可以将该目录的内容移动到任何我们想要的地方。例如,如果我们有一个名为/store/file_archive的目录,我们可以轻松地将文件移动到那里:
sudo mv /home/dscully /store/file_archive
当然,最终存储的目录由你决定,但你明白我的意思。
如果你还不知道,你可以使用mkdir命令来创建一个新目录。你可以在任何你的登录用户有权限访问的目录中创建一个目录。以下命令将创建我在前面示例中提到的file_archive目录:
sudo mkdir -p /store/file_archive
-p标志只是会在父目录不存在时创建父目录。
如果你确实希望在删除账户时同时删除用户的主目录,只需添加-r选项。这样可以一并删除该用户及其数据:
sudo userdel -r dscully
如果在账户删除后仍需要删除/home目录(如果你第一次没有使用-r参数),可以使用rm -r命令像删除任何其他目录一样删除它:
sudo rm -r /home/dscully
可能不言而喻,但rm命令可能非常危险。如果你以root身份登录或在使用rm时使用了sudo,如果不小心,可能会轻易摧毁你整个已安装的系统。不要运行此命令,但作为一个假设性的示例,以下命令(乍一看似乎无害)很可能会完全摧毁你整个文件系统:
sudo rm -r / home/dscully
请注意错字:我不小心在第一个斜杠后面输入了一个空格。我实际上不小心告诉我的系统删除整个文件系统的内容。如果执行该命令,下次我们尝试启动服务器时,它可能甚至无法引导。所有用户和程序数据都将被清除。如果有一个单一的原因让我们保护 root 账户,那就是 rm 命令绝对是其中之一!
到目前为止,我们知道如何添加和删除用户。在下一节中,我们将更深入地了解密码。
理解 /etc/passwd 和 /etc/shadow 文件
现在我们知道如何在服务器上创建(和删除)用户账户,我们已经可以管理用户了。但是这些信息究竟存储在哪里?我们知道用户将其个人文件存储在 /home 中,但是是否有某种数据库可以追踪我们系统上的用户账户?实际上,用户账户信息存储在两个特殊的文本文件中:
-
/etc/passwd -
/etc/shadow
您可以使用以下命令显示这两个文件的内容。请注意,任何用户都可以查看 /etc/passwd 的内容,而只有 root 用户可以访问 /etc/shadow:
cat /etc/passwd
sudo cat /etc/shadow
随便看看这两个文件(只是不要做任何更改),我将帮助您理解它们。
理解 /etc/passwd 文件
首先,让我们来看一下 /etc/passwd 文件。接下来是我测试服务器上此文件的一些示例输出。为了简洁起见,我将输出限制为最后八行:

图 2.5:示例 /etc/passwd 文件
此文件中的每一行对应系统上的一个用户账户。条目被分割成由冒号(:)分隔的列。用户名在第一列,因此您可以看到我创建了用户 jay 和 jdoe。每个条目的下一列只是一个 x。稍后我会解释这意味着什么。现在,让我们跳到第三和第四列,它们分别引用 UID 和 GID。
在 Linux 系统上,用户账户和用户组实际上是通过它们的 ID 引用的。虽然我们更容易通过名称管理用户,但用户名和组名只是放在 UID 和 GID 上的标签,以帮助我们更轻松地识别它们。
例如,尝试记住 jdoe 在我们服务器上的 UID 1001 可能会很烦人。通过引用账户名 jdoe 来管理它对人类来说更容易,因为我们不太能记住数字,而是名字。但是对于 Linux 系统来说,每次我们引用用户 jdoe,实际上我们只是引用 UID 1001。当创建用户时,默认情况下系统会自动为账户分配下一个可用的 UID。如果您管理多个 Ubuntu 服务器,请注意 UID 在不同系统之间不会匹配,因此请记住 UID 在安装之间不会同步。
在我的例子中(如图 2.5所示),每个用户的 UID 与其 GID 相同。这只是我系统中的一个巧合,实际使用中不一定是这样。虽然我稍后会在本章讨论创建组的内容,但需要理解的是,创建组的过程与创建用户相似,都是通过分配下一个可用的 GID 来进行的,类似于为新用户分配下一个可用的 UID。当你创建一个用户时,该用户的主组与其用户名相同(除非你要求其它方式)。例如,当我创建了jdoe时,系统也自动创建了一个名为jdoe的组。
这就是你实际看到的内容——用户的 UID,以及用户主组的 GID。我们稍后会详细讨论组。
你可能还注意到,你系统中的/etc/passwd文件包含了比我们自己创建的用户更多的条目。这是完全正常的,因为 Linux 使用用户帐户来处理后台运行的各种进程和服务。你可能永远不会与默认帐户交互,尽管你或许会在某一天为某个进程创建自己的系统用户。例如,也许你会为自动化数据处理脚本创建一个数据处理帐户来运行。
好的,回到我们的/etc/passwd文件。第五列用于存储用户信息,通常是用户的名字和姓氏。在我的例子中,jdoe的第五列是空的,因为我通过useradd命令创建了jdoe,该命令没有提示我输入名字和姓氏。这个字段也被昵称为GECOS字段,你在阅读文档时可能会看到它以这种方式被提及。
在第六列,显示了每个用户的主目录。对于jdoe来说,它被设置为/home/jdoe。最后,我们为用户指定了默认的 shell 为/bin/bash。这个字段指的是用户将使用的默认 shell,当使用adduser命令创建帐户时,默认的 shell 是/bin/bash,而使用useradd命令创建时,默认是/bin/sh。(如果你没有偏好,/bin/bash对大多数人来说是最好的选择。)如果我们希望用户使用不同的 shell,可以在这里指定(不过本书并未涉及/bin/bash以外的 shell)。如果我们愿意,也可以将用户的 shell 更改为一个无效的值,从而完全禁止他们登录。这在需要快速禁用帐户时非常有用。
理解/etc/shadow 文件
既然这些已经解释清楚了,让我们来看看/etc/shadow文件。我们可以像其他文本文件一样使用cat命令显示其内容,但与/etc/passwd不同,我们需要root权限才能查看它。所以,赶紧显示这个文件的内容,我会带你逐步分析:
sudo cat /etc/shadow
这将显示以下输出:

图 2.6:示例/etc/shadow 文件
上面的截图,图 2.6,显示了我服务器上该文件的最后四行。首先,我们在第一列看到用户名——没有什么惊讶的。请注意,输出中没有显示每个用户的 UID。系统根据/etc/passwd文件知道哪个用户名对应哪个 UID,因此这里无需重复显示。第二列显示的似乎是一些乱码。实际上,这是整个文件中最重要的部分。那是用户密码的实际哈希值。
密码哈希是将实际密码转换为一个不同的字符串,代表原始密码。这是一个单向转换,因此你不能通过逆向工程哈希来找到实际密码。在/etc/passwd文件中,存储的是密码的哈希,而不是实际密码,这是出于安全考虑。
如果你还记得,在/etc/passwd文件中,每个用户条目在第二列都有一个x,我提到过稍后会解释这个。x表示的是用户的密码被加密,并且没有直接存储在/etc/passwd中,而是存储在/etc/shadow中。毕竟,/etc/passwd文件是所有人都可以查看的,因此如果任何人都能打开该文件并看到每个人的密码,那将极大地危及安全性。
在过去的日子里,你实际上可以将用户的密码存储在/etc/passwd中,但现在已经不再这么做了。每当你在现代 Linux 系统上创建用户账户时,用户的密码会被加密(在/etc/passwd的第二列会为该用户放置一个x),而实际的密码哈希会存储在/etc/shadow的第二列,以防止被窥视。希望现在你已经明白了这两个文件之间的关系。
记得我之前提到root用户账户默认是锁定的吗?好吧,让我们看看它是如何工作的。执行以下命令,查看/etc/shadow中的root用户账户条目:
sudo cat /etc/shadow | grep root
在我的系统上,我得到以下输出:

图 2.7:示例 /etc/shadow 文件
你应该立即注意到,root用户账户根本没有密码哈希。相反,密码哈希的位置会显示一个星号。实际上,在这里放置一个星号或感叹号是一种锁定账户的方法。更简单的方法是,你可以使用passwd -l命令来锁定一个账户,而无需编辑文件。但无论哪种方式,我们仍然可以随时切换到root账户(稍后我会向你展示如何做到这一点)。在第二列输入一个星号或感叹号会创建一个限制,使得我们不能直接从 shell 或通过网络登录该用户。我们必须先以普通用户身份登录系统,然后如果需要,还可以切换到该用户。
在讨论完密码哈希后,/etc/shadow文件中还有一些字段我们应该理解。这里有一行构造的示例:
mulder:$6$TPxx8Z.:16809:0:99999:7:::
继续讨论第三列,我们可以看到自Unix 纪元以来密码最后一次更改的天数。对于不清楚的人来说,Unix 纪元是 1970 年 1 月 1 日。因此,我们可以将该列解读为密码是在 Unix 纪元后第 16,809 天更改的。
个人而言,我喜欢使用以下命令来更方便地查看密码最后一次更改的时间:
sudo passwd -S <username>
这将导致输出类似于以下内容:

图 2.8:检查用户最后一次密码更改的日期
执行此命令后,你可以查看系统中任何账户的信息。第一列显然是用户名。第二列与密码的状态有关,在这种情况下是L,表示该用户的密码被锁定。如果密码已设置且可用,则显示P,如果用户没有密码,则显示NP。
该命令输出的第三列显示了用户最后一次密码更改的实际日期。第四列告诉我们用户在再次更改密码之前需要等待多少天。在这个例子中,jdoe可以随时更改密码,因为最小天数设置为0。我们稍后会在本章中讲解如何设置最小天数,但我会简要解释一下这个设置的含义。起初,要求用户等待一定天数才能更改密码似乎有些傻。然而,千万不要低估用户的反抗心理。当要求用户更改密码时,用户往往会为了满足历史要求而更改密码,但随后会迅速将其更改回原来的密码。通过设置最小天数,你强制要求用户在两次密码更改之间有一个等待期,这样就不那么方便用户直接更改回原来的密码。
第五列,顾名思义,是密码更改之间允许的最大天数。如果你要求用户在每隔一定天数后更改密码,你将在这一列看到相应的数字。默认情况下,这个值设置为99999天。这个天数远远超过人的寿命,所以实际上可以认为它是无限的。
继续讲解第六列,我们列出了在密码过期前,用户会被提醒需要更改密码的天数。在第七列中,我们设置了密码过期后可以过去多少天,在这种情况下,账户将会被禁用。对于我们的示例用户,暂时没有设置此项。最后,在第八列(该列不可见)中,我们可以看到自 Unix 纪元以来,账户将被禁用之前经过的天数(在我们的例子中,这里没有内容,因此没有设置禁用日期)。
我们稍后会讲解如何设置这些字段,但现在希望你能更好地理解/etc/shadow文件的内容。
如果在任何时候你需要进一步的说明,随时可以查看 Ubuntu 的手册页。手册页(man page 的缩写)可以提供关于命令和文件的更多信息。例如,以下命令会显示ls命令的手册页:
man ls
针对本节内容,你也可以检索/etc/shadow文件的手册页:
man passwd
man shadow
按下键盘上的q退出手册页。随时可以查看本书中任何命令的手册页,以便更深入了解。
现在我们已经完全理解了如何管理用户,我们还可以看看如何为他们的主目录提供默认文件。
使用 /etc/skel 分发默认配置文件
在典型的组织中,通常会有一些推荐的默认文件和配置供用户使用。
例如,在进行软件开发的公司中,通常会有推荐的文本编辑器和版本控制系统的设置。位于/etc/skel目录中的文件会在创建新用户时复制到他们的主目录中(假设你在设置用户时选择了创建主目录)。
事实上,你现在就可以亲自验证这一点。执行以下命令:
ls -la /etc/skel
现在,你应该能够查看/etc/skel目录的内容:

图 2.9:默认的 /etc/skel 文件
你可能已经知道如何列出目录中的文件,但我添加了-a选项,因为我想查看隐藏文件。默认情况下,/etc/skel中包含的文件是隐藏的(它们的文件名以句点开头)。我还加了-l参数,因为它显示的是长格式列表,我觉得这种格式更易读。
每次创建新用户并要求同时创建主目录时,这三个文件(如图 2.9所示)会被复制到用户的主目录中,除此之外,任何你在此处创建的文件也会一同复制。你可以通过列出已创建用户的主目录存储内容来验证这一点。一个用户主目录中的.bashrc文件应该和其他任何用户的相同,除非他们对其进行了修改。
掌握了这些知识后,创建新用户的默认文件应该变得非常简单。例如,你可以使用自己喜欢的文本编辑器创建一个名为welcome的文件,并将其放置在/etc/skel目录中。也许你会创建一个包含公司新员工帮助电话和信息的文件。然后,在你创建账户时,这个文件会自动复制到新用户的主目录中。用户登录后,会在其主目录中看到该文件并查看相关信息。更实际一点,如果你的公司有特定的编辑器设置,适合写代码,你也可以将这些文件包含在/etc/skel中,以确保用户遵循这些设置。事实上,你可以为公司使用的任何应用程序包含默认配置文件。
赶紧试试吧。随便创建一些随机文本文件,然后创建一个新用户,接着你会看到这些文件会自动进入你系统中新账户的主目录中。
现在我们已经有了多个用户,并且了解了如何管理它们的默认文件,我们可以看看如何在用户之间切换。
切换用户
现在我们系统中已经有了多个用户,我们需要知道如何在它们之间切换。当然,你可以随时以某个用户身份登录到服务器,但实际上,你可以在任何时候切换到任何用户账户,只要你知道该用户的密码或拥有sudo权限。
切换用户时,你将使用su命令。如果你输入su而不带任何选项,它将默认假设你要切换到root用户,并会要求你输入root密码。正如我之前提到的,Ubuntu 默认锁定了root账户,所以此时你可能没有root密码。
即使 Ubuntu 默认不为root创建密码,某些虚拟专用服务器(VPS)提供商会解锁root密码,并让你以root用户身份登录。解锁root账户并不是 Ubuntu 的标准做法,而是某些云服务提供商的定制。
解锁root账户其实非常简单;你只需要创建一个root密码。为此,你可以以任何拥有sudo权限的用户身份执行以下命令:
sudo passwd
命令将要求你创建并确认root密码。从此以后,你将能够像使用其他账户一样使用root账户。你可以直接以root身份登录,或者切换到root——现在它完全可用了。然而,实际上你并不需要解锁root账户就可以使用它。你当然可以解锁它,但有办法在不解锁root的情况下切换到root,通常最好保持root账户锁定,除非你有非常特殊的原因需要解锁它。以下命令将允许你从具有sudo权限的用户账户切换到root:
sudo su -
现在你将以root身份登录,并且可以执行任何你想要的命令,不会有任何限制。要返回到之前登录的账户,只需输入exit。你可以通过bash提示符开头的值来判断自己当前以哪个用户身份登录。
如果你想切换到root以外的账户怎么办?当然,你可以直接注销并以该用户身份重新登录。但其实你不需要这么做。只要知道账户的密码,下面的命令就能帮你完成切换:
su - <username>
Shell 会要求输入该用户的密码,然后你将以该用户的身份登录。同样,当你使用完账户后,输入exit将返回到你之前使用的账户。
如果你知道用户的密码,这个命令是没问题的,但通常你并不会知道。通常,在企业环境中,你会创建一个账户,强制用户在首次登录时更改密码,然后你将无法知道该用户的密码。
由于你拥有root和sudo权限,你可以随时更改他们的密码,然后以他们的身份登录。但如果他们的密码突然失效,他们肯定会发现问题——你不是在窃听吧?有了sudo权限,你可以使用sudo切换到任何你想要的用户,即使你不知道他们的密码。只需要在之前的命令前加上sudo,你只需要输入你自己账户的密码,而不是他们的:
sudo su - <username>
切换到另一个用户账户对于支持工作(尤其是故障排除权限问题)通常非常有帮助。举个例子,假设某个用户找你抱怨无法访问特定目录的内容,或者无法运行某个命令。在这种情况下,你可以登录到服务器,切换到他们的用户账户,尝试重现他们的问题。这样,你不仅可以亲自看到他们的问题,还可以测试你的修复是否解决了问题,然后再回复他们。
现在我们已经完全理解了用户账户,甚至知道如何在它们之间切换。在接下来的部分,我们将探讨用户组,它允许我们对用户进行分类。
管理用户组
现在我们理解了如何创建、管理和切换用户账户,我们还需要了解如何管理用户组。Linux 中的用户组概念与其他平台并没有太大区别,基本上是为了实现相同的目的。有了用户组,你可以更高效地控制用户对服务器资源的访问。通过将用户组分配给某个资源(如文件、目录等),你可以通过简单地将用户添加或移除该组来允许或拒绝他们的访问。
在 Linux 中,工作方式是每个文件或目录都有一个用户和一个组来对其进行拥有。这与 Windows 等平台不同,Windows 允许一个资源被多个组分配。而 Linux 是一对一的拥有:每个文件或目录只分配一个用户和一个组。如果你列出 Linux 系统上某个目录的内容,你可以亲眼看到这一点:
ls -l
以下是我其中一台服务器上某个目录的输出示例:
-rw-r--r-- 1 root bind 490 2022-04-15 22:05 named.conf
在这种情况下,我们可以看到 root 拥有这个文件,并且组 bind 也被分配给了它。暂时忽略其他字段;当我们讲解关于权限的章节时,我会详细解释。现在,只需记住,每个文件或目录都分配了一个用户和一个组。
虽然每个文件或目录只能有一个组分配,但任何用户帐户都可以是多个组的成员。仅执行 groups 命令而不带任何选项,可以告诉你当前登录用户是哪些组的成员。如果你在 groups 命令后添加用户名,你将看到该用户是哪些组的成员。可以尝试一下带或不带用户名的 groups 命令,了解一下其作用。
在 Ubuntu Server 平台上,你很可能会发现每个用户帐户都是一个名为与用户名相同的组的成员。正如我之前提到的,当你创建一个用户帐户时,你也在创建一个与用户同名的组。然而,在某些 Linux 发行版中,用户的主要组会默认是名为 users 的组。如果你在 Ubuntu 桌面平台上执行 groups 命令,你可能会看到更多的组。这是因为,面向服务器平台的 Linux 发行版通常更加精简,而桌面平台上的用户需要访问更多的对象,如打印机、声卡等。一些可以安装的包也会为服务器添加额外的系统用户。
如果你想知道你的服务器上存在哪些组,你只需要执行 cat 命令来查看 /etc/group 文件的内容。与我们之前介绍的 /etc/passwd 文件类似,/etc/group 文件包含了系统中已创建的组的信息。快去查看你系统上的这个文件:
cat /etc/group
以下是我其中一台服务器上该文件的示例输出:

图 2.10:/etc/group 文件的示例输出
和之前一样,文件中的列是用冒号分隔的,不过每行只有四列。在第一列,我们有组的名称,这一点并不意外。在第二列,我们可以存储组的密码,但这不常用,因为这样做实际上会带来安全风险。在第三列,我们有GID,它的概念与我们讨论用户时的UID类似。最后,在最后一列,我们(会)看到一个以逗号分隔的成员列表,列出每个组的成员。
有些条目根本没有显示任何组成员。每个用户确实是自己组的成员,所以虽然文件中没有明确写出这一点,但这是隐含的。如果你查看/etc/passwd文件中用户的条目,你会看到他们的主组(作为第三列显示,以GID的形式)引用了/etc/group文件中的一个组。
在系统中创建新组非常简单,也是分类用户及其权限的好方法。比如你可以为会计人员创建一个accounting组,为 IT 部门的员工创建admins组,为销售人员创建一个sales组。groupadd命令可以用来创建新组。
如果你愿意的话,也可以直接编辑/etc/group文件,手动添加一行新的组信息,不过我认为使用groupadd命令可以省去一些工作,还能确保组条目正确创建。直接编辑组和用户文件通常是不推荐的(而且一个拼写错误可能导致严重问题)。无论如何,接下来是使用groupadd命令创建新组的示例:
sudo groupadd admins
如果你在添加新组后再次查看/etc/group文件,你会看到文件中创建了一行新内容,并且为你选择了一个GID(第一个尚未使用的 GID)。删除组同样简单。只需要执行groupdel命令,后面跟上你想要删除的组名:
sudo groupdel admins
接下来,我们将了解usermod命令,它可以让你将用户与组关联起来。usermod命令可以说是瑞士军刀式的工具;它有很多功能(将用户添加到组只是其中之一)。如果我们想要把一个用户添加到admins组,可以执行以下命令:
sudo usermod -aG admins myuser
在这个例子中,我们提供了-a选项,意味着追加,然后紧接着使用-G,表示我们希望修改次要组成员资格。我把这两个选项合并为一个(-aG),但你也可以分别使用它们(-a -G)。我给出的示例只是将用户添加到附加的组,而不会替换他们的主组。
注意不要遗漏-a选项,因为这样做会将所有当前的组成员资格替换为新的组,这通常不是你想要的。-a选项意味着附加,即将该用户的现有组成员资格列表添加到新组中。
如果你想更改用户的主组,可以使用-g选项(使用小写的g,而不是我们之前使用的大写G):
sudo usermod -g <group-name> <username>
随时查看usermod命令的手册页,了解它允许你管理与用户相关的所有有用功能。你可以使用以下命令查看usermod命令的手册页:
man usermod
另一个例子是更改用户的/home目录。假设你的某个用户进行了改名,你希望更改他们的用户名,并将他们原来的home目录(以及文件)移动到新的位置。以下命令将完成这个操作:
sudo usermod -d /home/jsmith jdoe -m
sudo usermod -l jsmith jdoe
在这个例子中,我们将jdoe的主目录移动到/home/jsmith,在第二个例子中,我们将用户名从jdoe更改为jsmith。
如果你希望将用户从某个组中移除,可以使用gpasswd命令。gpasswd -d就可以完成这个操作:
sudo gpasswd -d <username> <grouptoremove>
实际上,gpasswd也可以代替usermod,将用户添加到组中:
sudo gpasswd -a <username> <group>
现在,你已经知道如何管理组了。通过有效地管理组,你将能够更好地管理服务器上的资源。当然,如果不解释如何管理权限,组将相对无用(否则,任何成员都无法访问资源)。在本章稍后的部分,我们将介绍权限管理,以便你能完整了解如何管理用户访问。
管理密码和密码策略
在本章中,我们已经讲解了一些密码管理的内容,因为我给了你一些passwd命令的例子。如果你还记得,passwd命令允许我们更改当前登录用户的密码。此外,使用passwd命令作为root(并提供用户名),我们可以更改系统上任何用户账户的密码。但这并不是这个命令唯一的功能。
锁定和解锁用户账户
我之前没有提到的关于passwd命令的一件事是,你可以用它来锁定和解锁用户账户。有许多原因可能需要这么做。例如,如果一个用户要去度假或休长假,你可能想要锁定他们的账户,以便他们不在外出期间使用它。毕竟,活跃账户越少,你的攻击面就越小。要锁定账户,使用-l选项:
sudo passwd -l <username>
要解锁账户,可以使用-u选项:
sudo passwd -u <username>
但是,锁定账户不会阻止用户登录,如果他们通过 SSH 使用公钥认证访问服务器。在这种情况下,你需要撤销他们使用 SSH 的权限。一种常见的做法是将 SSH 访问限制为某个特定组的成员。当你锁定账户时,只需将他们从该组中移除。如果你对 SSH 部分不太熟悉,不必过于担心。我们将在 第二十一章,保护你的服务器 中讨论如何保障 SSH 服务器的安全。目前只需要记住,你可以使用 passwd 来锁定或解锁账户,如果你使用 SSH,你将需要将用户锁定,以防止他们登录。
然而,密码管理不仅仅是 passwd 命令,我们还可以实施自己的策略,例如查看或调整密码过期的详细信息。
设置密码过期信息
之前我提到过,你可以为用户的密码设置过期日期(在我们讨论 /etc/shadow 文件时)。在这一节中,我们将介绍如何实际操作。具体来说,chage 命令为我们提供了这个能力。我们可以使用 chage 来修改用户密码的过期周期,但它也是比查看 /etc/shadow 文件更方便的方式来查看当前的过期信息。通过 chage 的 -l 选项,并提供用户名,我们可以看到相关信息:
sudo chage -l <username>
使用 sudo 或 root 并不是运行 chage 的必要条件。你可以在不提升权限的情况下查看自己用户名的过期信息。然而,如果你想查看除自己账户以外的任何用户账户的过期信息,你需要使用 sudo。
在接下来的示例中,我们可以看到来自一个示例用户账户的命令输出:

图 2.11:chage 命令的输出
在输出中,我们可以看到有关密码过期日期、密码更改的最大天数等信息。基本上,它包含了存储在 /etc/shadow 中的相同信息,但更易于阅读。
如果你希望更改这些信息,chage 将再次是首选工具。下面提供的第一个示例是非常常见的。当创建用户账户时,你肯定希望用户在首次登录时更改密码。
不幸的是,并不是每个人都乐意这样做。chage 命令允许你强制用户在首次登录时更改密码。基本上,你可以将他们的密码过期天数设置为 0,如下所示:
sudo chage -d 0 <username>
如果你再次对刚修改过的用户账户运行chage -l,你将立即看到该命令的结果:
sudo chage -l <username>
输出将显示有关密码更改的信息:

图 2.12:使用 chage 命令列出设置了密码过期周期的用户
要设置用户账户在一定天数后强制要求更改密码,以下操作可以实现:
sudo chage -M 90 <username>
在这个例子中,我将用户账户设置为在 90 天后过期并要求更改密码。当即将到达密码更改日期前 7 天时,用户登录时将看到一条警告信息。
如我之前提到的,用户通常会尽可能地绕过密码要求,可能在满足初始密码更改要求后,再把密码改回原来的密码。你可以使用 -m 参数设置最小天数,如下例所示,我们将其设置为 5 天:
sudo chage -m 5 dscully
设置最小密码年龄的诀窍是,将其设置为用户修改密码回原密码时感到不方便,但你仍然希望用户在需要时能够更改密码(因此也不要设置得太长)。如果用户想在最小天数还没到之前更改密码(例如,如果用户觉得他们的账户可能已经被泄露),他们可以随时让你为他们更改密码。然而,如果你让密码要求对用户来说过于不便,也可能会适得其反。
设置密码策略
接下来,我们应该讨论如何设置密码策略。毕竟,如果用户将密码更改为简单的密码(如 abc123),强制用户更改密码也没有多大意义。密码策略允许你强制用户遵守一些要求,例如密码长度、复杂性等。
为了方便,我们可以使用 Pluggable Authentication Module(PAM)。PAM 为我们提供了额外的认证功能,并且提供了我们可以使用的额外插件,扩展认证并添加更多功能。虽然本书没有详细讲解 PAM,但我建议你保持对它的记忆,以防你以后想添加更多的功能。
针对设置密码策略的主题,我们可以安装一个 PAM 模块来启用这个功能,这需要安装一个新的软件包:
sudo apt install libpam-cracklib
接下来,让我们看看以下文件,这是 Ubuntu 提供的。你可以使用文本编辑器(如 nano)打开它,我们需要对其进行编辑:
sudo nano /etc/pam.d/common-password
在修改与认证相关的配置文件(如密码要求、sudo 权限、SSH 等)时,一个非常重要的提示是,在进行更改时,始终保持一个 root shell 开放,并在另一个 shell 中测试这些更改。在你完全确认更改已经过充分测试之前,不要退出最初的 root 窗口。在测试策略时,确保不仅用户能够登录,管理员也能登录。否则,你可能会失去登录服务器并进行更改的能力。
要启用密码历史要求(即系统记住用户使用过的最后几个密码,防止他们重复使用),我们可以在文件中添加以下行:
password required pam_pwhistory.so remember=99 use_authok
在示例的 config 行中,我使用了 remember=99,这(你可能猜到)将使我们的系统记住每个用户的最后 99 个密码,并防止他们再次使用这些密码。如果你之前配置了密码的最小使用期限,例如 5 天,那么如果考虑到用户每 5 天更改一次密码 99 次,用户将需要 495 天才能返回到他们的原密码。这几乎不可能让用户重新使用他们的旧密码。
在 /etc/pam.d/common-password 文件中,另一个值得提及的部分是读取 difok=3 的配置项。此配置要求密码必须至少有三个字符与旧密码不同,才能被视为可接受。否则,密码会被认为与旧密码过于相似而被拒绝。你可以将此值更改为任何你喜欢的数字;默认值通常是 5,但在 Ubuntu 中,它将其默认设置为 3。此外,文件中还会看到 obscure 配置项,它防止使用简单密码(如常见的字典词汇等)。
设置密码策略是提高服务器安全性的一个好方法。然而,同样重要的是不要过度限制。为了在安全性和用户挫败感之间找到平衡,挑战在于制定足够的限制来提高安全性,同时尽量减少用户的挫败感。当然,单单提到“密码”这个词就足以让普通用户感到沮丧,所以你不可能让每个人都满意。但从整体系统安全的角度来看,我相信你的用户会感激你作为管理员已经采取了必要的预防措施,以确保他们(以及你们公司的)数据安全。最终,还是要用你最好的判断。
既然我们已经讨论了安全性问题,那么也应该看看如何配置 sudo 本身,这将在下一节中进行处理。
配置管理员访问权限使用 sudo
到目前为止,我们已经在本书中使用了很多次 sudo。此时,你应该已经意识到 sudo 允许你以其他用户的身份执行命令,默认情况下是 root 用户。然而,我们还没有正式讨论过它,也没有讨论如何修改哪些用户账户能够使用 sudo。
在所有 Linux 系统中,你应该用强密码保护你的 root 账户,并将其限制为尽可能少的人使用。在 Ubuntu 中,root 账户默认是锁定的,所以除非你通过设置密码解锁了它(或者你使用的是由 VPS 提供商提供的 Ubuntu 版本),否则无法用它登录系统。使用 sudo 是一种替代直接以 root 用户身份登录执行命令的方法,因此你可以通过 sudo 给管理员提供执行需要 root 权限的任务的权限,而不需要给他们 root 密码或解锁 root 账户。事实上,sudo 让你可以更加精细化管理。直接使用 root 基本上就是全有或全无——如果有人知道 root 密码并且 root 账户已启用,那么此人没有任何限制,可以随意操作。而使用 sudo 时,也可以出现类似情况,但你实际上可以限制某些用户只能使用特定的命令,从而限制他们在系统中能做的事情。例如,你可以给管理员提供安装软件更新的权限,但不允许他们重启服务器。
默认情况下,sudo 组的成员可以不受限制地使用 sudo。基本上,这个组的成员可以做任何 root 能做的事情(也就是一切)。在安装过程中,你创建的用户帐户已成为 sudo 组的成员。要为其他用户提供 sudo 权限,你只需要将他们添加到 sudo 组即可:
sudo usermod -aG sudo <username>
并非所有发行版默认使用 sudo 组,甚至有的不会自动安装 sudo。其他发行版需要你手动安装 sudo,并且可能使用另一个组(如 wheel)来管理对 sudo 的访问。
但再说一次,这样会给予那些用户一切权限,而这可能是你想要的,也可能不是。要实际配置 sudo,我们使用 visudo 命令。这个命令帮助你编辑 /etc/sudoers 文件,这是一个控制 sudo 访问的配置文件。虽然你可以用文本编辑器直接编辑 /etc/sudoers,但强烈不建议用这种方式配置 sudo。visudo 命令会检查你的修改是否遵循正确的语法,并帮助防止你不小心破坏文件。这是一个非常好的方法,因为如果你在 /etc/sudoers 文件中犯了错误,可能会导致没有人能够获得服务器的管理员控制权。虽然有办法从这样的错误中恢复,但显然,这并不是一个你想要面对的局面!所以,这里要记住的一点是,永远不要直接编辑 /etc/sudoers 文件;始终使用 visudo 来编辑它。
下面是 visudo 命令在你犯错误时显示的警告类型的示例:

图 2.13:visudo 命令显示错误
如果你看到这个错误,按 e 返回编辑文件,然后纠正错误。
这个过程的工作方式是,当你在终端运行visudo时,它会将你带入一个文本编辑器,并打开/etc/sudoers文件。然后,你可以像编辑其他文本文件一样,修改并保存文件。默认情况下,Ubuntu 在使用visudo时会打开nano文本编辑器。在nano中,你可以使用Ctrl + w保存更改,使用Ctrl + x退出文本编辑器。
所以,visudo允许你更改谁能够访问sudo。但你究竟如何进行这些更改呢?你可以滚动浏览visudo打开的/etc/sudoers文件,你应该能看到类似以下的行:
%sudo ALL=(ALL:ALL) ALL
这是启用任何属于sudo组的成员访问sudo权限的配置行。你可以将组名更改为你喜欢的任何名称,例如,你可能希望创建一个名为admins的组。如果你做了这个更改,确保在编辑/etc/sudoers文件或注销之前,实际上已经创建了该组,并将你自己和你的团队添加为该组的成员;如果你发现自己被锁定,无法访问服务器的管理员权限,那可就尴尬了。
当然,你不必通过组来启用访问权限。你实际上也可以指定一个用户名。在/etc/sudoers文件中,组名前会有一个%符号,而用户名则没有。例如,文件中也有如下行:
root ALL=(ALL:ALL) ALL
这里,我们调用了一个用户名(在这个例子中是root),但这一行的其他部分和我之前提到的行是一样的。虽然你当然可以复制这一行并粘贴一遍或多遍(将root替换为其他用户名),以授予其他人访问权限,但使用组的方式实际上是最好的方法。将用户添加或移出一个组(如sudo组)要比每次使用visudo要容易得多。
到此为止,你可能在想,/etc/sudoers配置行上的各个选项到底是什么意思。目前为止,两个例子都使用了ALL=(ALL:ALL) All。为了完全理解sudo,理解其他字段非常重要,所以让我们通过它们(再次使用root这一行作为例子)来逐一了解。
第一个ALL表示root可以从任何终端使用sudo。第二个ALL表示root可以使用sudo冒充任何其他用户。第三个ALL表示root可以冒充任何其他组。最后一个ALL表示该用户能够使用的命令;在这种情况下,是他们想使用的任何命令。
为了帮助理解,我将提供一些额外的示例。这里有一个假设的例子:
charlie ALL=(ALL:ALL) /sbin/reboot,/sbin/shutdown
在这里,我们允许用户charlie执行reboot和shutdown命令。如果用户charlie尝试做其他事情(例如安装软件包),他们将收到错误信息:
Sorry, user charlie is not allowed to execute '/usr/bin/apt install tmux' as root on ubuntu.
然而,如果charlie想要在服务器上使用reboot或shutdown命令,他们将能够执行这些操作,因为我们在为该用户设置sudo权限时明确指定了这些命令。我们可以进一步限制,通过将第一个ALL改为机器名称,在本例中为ubuntu,来引用我在示例中使用的服务器主机名。我还修改了charlie被允许执行的命令:
charlie ubuntu=(ALL:ALL) /usr/bin/apt
在编辑sudo权限时,最好使用命令的完整路径,而不是简化版本。例如,我们在这里使用了/usr/bin/apt,而不是仅仅使用apt。这是非常重要的,因为用户可以创建一个名为apt的脚本,做一些我们通常不允许他们做的恶意行为。通过使用完整路径,我们限制了用户只能使用该路径下存储的二进制文件。
现在,charlie只能使用apt。他们可以使用apt update、apt dist-upgrade以及apt的任何其他子命令。但如果他们尝试重启服务器、删除受保护的文件、添加用户,或者做其他我们未明确设置的操作,他们将被阻止。
但是,我们遇到了另一个问题。我们正在允许charlie模拟其他用户。鉴于安装软件包的上下文,这可能并不完全糟糕(模拟另一个用户在没有权限安装软件包的情况下是没有意义的),但除非我们确实需要这样做,否则允许这种行为是不合适的。在这种情况下,我们可以完全删除这一行中的(ALL:ALL),从而防止charlie使用sudo的-u选项以其他用户身份运行命令:
charlie ubuntu= /usr/bin/apt
另一方面,如果我们确实希望charlie能够模拟其他用户(但仅限于特定用户),我们可以通过设置这些值来指定charlie被允许代表的用户名和组:
charlie ubuntu=(dscully:admins) ALL
在这个例子中,charlie能够代表用户dscully和组admins运行命令。
当然,sudo远远不止我在这一节中提到的内容。关于sudo可以写出整本书(事实上,已经有很多了),但你在日常管理这款工具时,99%的需求都涉及如何为用户添加访问权限,并明确规定每个用户能够做什么。作为最佳实践,尽可能使用组(例如,你可以有一个apt组、一个reboot组等等),并尽量具体地规定谁能做什么。通过这种方式,你不仅可以保持root账户的私密性(或者更好的是禁用它),还可以提高服务器的责任性。
现在我们已经探索了如何授予sudo访问权限,接下来我们将看看权限设置,它让我们对用户能够访问的内容有更高的控制权。
设置文件和目录的权限
在本节中,我们之前在本章所做的所有用户管理工作都将汇聚在一起。我们已经学会了如何添加帐户、管理帐户并保护它们,但我们实际上还没有处理有关谁能够访问这些资源的管理工作。在本节中,我将简要概述 Ubuntu Server 中权限的工作原理,然后提供一些自定义权限的示例。
查看权限
我相信到现在你已经明白如何使用ls命令列出目录的内容。谈到查看权限时,-l标志特别有用,因为长格式列出的输出可以让我们查看对象的权限:
ls -l
以下是一些示例性的假设文件列表:
-rw-rw-rw- 1 doctor 5 Jan 11 12:52 welcome
-rw-r--r-- 1 root root 665 Feb 19 profile
-rwxr-xr-x 1 dalek dalek 35125 Nov 7 exterminate
在每一行中,我们可以看到几个信息字段。第一列是对象的权限字符串(例如,-rw-r—r--),稍后我们会更详细地讲解。我们还可以看到对象的链接数(第二列)。链接超出了本章的范围,但将在第五章、管理文件和目录中讨论。接着,文件的拥有者显示在第三列,文件所属的组显示在第四列,大小(以字节为单位)显示在第五列,文件最后修改的日期显示在第六列,最后是文件的名称。
请记住,根据你的 shell 配置,输出可能会有所不同,字段可能位于不同的位置。为了讨论权限,我们真正关注的是权限字符串,以及拥有者用户和组。在本例中,我们可以看到第一个文件(名为welcome)由名为doctor的用户拥有。第二个文件名为profile,由root拥有。最后,我们有一个名为exterminate的文件,拥有者是名为dalek的用户。
对于这些文件,我们有-rw-rw-rw-、-rw-r--r--和-rwxr-xr-x的权限字符串。如果你以前没有接触过权限,它们可能看起来很陌生,但其实当你把它们拆解开来时,理解起来非常简单。每个权限字符串可以分解为四个部分,如下表所示:
| 对象类型 | 用户 | 组 | 世界 |
|---|---|---|---|
| - | rw- |
rw- |
rw- |
| - | rw- |
r-- |
r-- |
| - | rwx |
rwx |
r-x |
我将每个示例权限字符串分解成了四个组。基本上,我将它们拆分到第一个字符处,然后每三个字符拆分一次。权限字符串的第一部分只有一个字符。在这些示例中,它都是一个破折号。这表示对象的类型是什么。是目录吗?文件吗?链接吗?在我们的例子中,这些权限字符串都是文件,因为权限字符串的第一个位置都是破折号。如果对象是目录,第一个字符应该是d,而不是-。如果对象是链接,那么该字段应该是l(小写字母L)。
在下一部分,每个对象的第二列有三个字符,分别是rw-、rw-和rwx。这表示适用于拥有文件的用户的权限。例如,以下是第一个权限字符串:
-rw-rw-rw- 1 doctor doctor 5 Jan 11 12:52 welcome
前面的代码输出的第三部分显示,doctor是拥有该文件的用户。因此,回到表格,权限字符串的第二列(rw-)特别适用于用户doctor。接下来,权限字符串的第三列也有三个字符,在这种情况下是rw-。这一部分权限字符串指的是拥有该文件的组。在本例中,该组也叫doctor,正如你在前面的代码输出中的第四列看到的那样。最后,权限字符串的最后一部分,在表格中可视化(再次是rw-),指的是world,也称为其他。这基本上指的是除了拥有文件的用户和组以外的任何人。因此,实际上其他每个人对该对象都至少拥有rw-权限。
单独来看,r代表读取,w代表写入。因此,我们可以读取第二列(rw-),这表示用户(doctor)有权限读取和写入该文件。第三列(再次是rw-)告诉我们,doctor组也有读取和写入该文件的权限。权限字符串的第四列相同,因此其他任何人也会对该文件拥有读取和写入权限。
我给出的第三个权限字符串看起来有点不同。这里再次展示:
-rwxr-xr-x 1 dalek dalek 35125 Nov 7 exterminate
在这里,我们看到设置了x属性。x属性表示能够将文件作为脚本执行。因此,考虑到这一点,我们知道该文件可能是一个脚本,可以被用户、组和其他人执行。鉴于文件名为exterminate,这有点可疑,如果它是一个真实文件,我们可能需要进一步调查。
如果某个权限未设置,则它会简单地显示为单个破折号,而不是r、w或x。这与表示某个权限被禁用是一样的。以下是一些示例:
-
rwx:对象为该字段设置了读取、写入和执行权限。 -
r-x:对象具有启用读取、禁用写入并启用执行权限。 -
r--: 对象具有读取权限,写入禁用,执行禁用 -
---: 对象没有启用此字段的任何权限
将这个讨论推向结尾,以下是更多的权限字符串:
-rw-r--r-- 1 sue accounting 35125 Nov 7 budget.txt
drwxr-x--- 1 bob sales 35125 Nov 7 annual_projects
在这些示例中的第一个,我们看到 sue 是 budget.txt 的所有用户,并且该文件被分配给一个会计组。该对象对 sue 可读可写,对其他所有人(group 和 world)也可读。这可能不好,因为这是一个预算文件,可能是机密的。我们稍后会更改它。
annual_projects 对象是一个目录,我们可以从第一列的 d 看出这一点。该目录由 bob 用户和 sales 组拥有。然而,由于这是一个目录,每个权限位具有不同的含义。在以下两张表中,我将分别列出这些位在文件和目录中的含义:
- 文件:
| Bit | 含义 |
|---|---|
r |
文件可以被读取 |
w |
文件可以被写入 |
x |
文件可以作为程序执行 |
- 目录:
| Bit | 含义 |
|---|---|
r |
目录内容可以查看 |
w |
目录内容可以被更改 |
x |
用户或组可以使用 cd 进入目录 |
如你所见,权限的读取方式根据其上下文不同而有所不同:它们适用于文件还是目录。在 annual_projects 目录的例子中,bob 对该目录有 rwx 权限。这意味着用户 bob 可以执行所有操作(查看内容、添加或删除内容,以及使用 cd 将当前 shell 目录切换到该目录)。对于一个组,sales 组的成员能够查看该目录的内容并进入该目录。然而,sales 组中的任何人都无法向目录中添加或删除项目。在该对象上,其他 没有设置任何权限。这意味着没有其他人可以对该对象进行任何操作,甚至无法查看其内容。
更改权限
所以,现在我们理解了如何读取文件和目录的权限。很好,但我们如何更改它们呢?如前所述,budget.txt 文件对所有人(其他)都是可读的。这不好,因为该文件是机密的。要更改对象的权限,我们将使用 chmod 命令。该命令允许我们以几种不同的方式更改文件和目录的权限。
首先,我们可以通过从其他字段中移除读取权限来简单地去除 sue 用户对预算文件的读取权限。我们可以通过以下示例来实现:
chmod o-r budget.txt
如果我们当前不在文件所在的目录中,需要提供完整路径:
chmod o-r /home/sue/budget.txt
如果你对不是自己拥有的文件使用 chmod 命令,你需要使用 sudo。
不管怎样,你应该已经理解了。以这个示例为例,我们将从other(o-r)移除r权限位。如果我们想要添加这个权限位,我们只需要用+代替-。以下是一些chmod实际应用的额外示例:
-
chmod u+rw <filename>:该对象在user列中添加了rw权限 -
chmod g+r <filename>:拥有组被赋予读取权限 -
chmod o-rw <filename>:Other移除rw权限位
此外,你还可以使用八进制值来管理和修改权限。这实际上是更改权限的最常用方法。我喜欢将其看作一个评分系统。虽然它并非真正的评分系统,但将每种访问类型看作有自己价值的方式,更容易理解。基本上,每个权限位(r、w 和 x)都有其对应的八进制值,如下所示:
-
读取:
4 -
写入:
2 -
执行:
1
使用这种风格时,结合这些八进制值时,你能得到的数字组合不多(每个值只能使用一次)。因此,我们可以通过以不同的组合添加(或不添加)这些数字来得到0、1、2、3、4、5、6和7。其中一些你几乎永远不会看到,比如一个对象有写入权限但没有读取权限。大多数情况下,你会看到0、4、5、6和7最常用与chmod。例如,如果我们将读取和写入加起来,我们得到6。如果我们将读取和执行加起来,我们得到5。如果我们将三者都加起来,我们得到7。如果我们不加任何权限,我们得到0。我们为每一列(User、Group和Other)重复这个过程,得出一个三位数的字符串。以下是一些示例:
-
600:User拥有读取和写入权限(4+2)。没有其他权限被设置。这与
-rw-------是相同的。 -
740:User拥有读取、写入和执行权限。Group拥有读取权限。Other没有任何权限。这与
-rwxr-----是相同的。 -
770:User和Group都拥有完全的访问权限(读取、写入和执行)。Other没有任何权限。这与
-rwxrwx---是相同的。 -
777:每个人都有所有权限。这与
-rwxrwxrwx是相同的。
回到chmod,我们可以在实践中使用这种编号系统:
-
chmod 600 filename.txt -
chmod 740 filename.txt -
chmod 770 filename.txt
希望你能理解。如果你想更改一个目录的权限,-R选项可能会对你有帮助。它使更改具有递归性,这意味着你不仅会更改该目录的权限,还会一次性更改它下面所有文件和子目录的权限:
chmod 770 -R mydir
虽然使用-R选项配合chmod可以节省一些时间,但如果你在更改权限的目录下有文件和子目录混合的情况,它也可能引发问题。前面的例子将权限770授予mydir及其所有内容。如果目录内有文件,这些文件将被赋予可执行权限给用户和组,因为7包括了执行位(值为1)。这可能不是你想要的效果。我们可以使用find命令来区分这些情况。虽然find命令超出了本章内容的范围,但通过以下命令的使用,应该能相对简单地理解它们的功能,以及它们如何有用:
find /path/to/dir/ -type f -exec chmod 644 {} \;
find /path/to/dir/ -type d -exec chmod 755 {} \;
基本上,在第一个例子中,find命令定位到/path/to/dir/路径下的所有文件(-type f),并对其执行chmod 644操作。第二个例子是定位该路径下的所有目录,并将它们的权限更改为755。这里没有详细介绍find命令,因为它很容易成为一章内容,但我将其包含在此,因为希望这些示例能对你有所帮助,并成为你自己有用命令列表中的一部分。
更改对象的所有权
最后,我们需要知道如何更改文件和目录的所有权。通常情况下,某个特定的用户可能需要访问某个对象,或者我们可能需要更改所有组。我们可以使用chown命令来更改文件或目录的用户和组所有权。例如,如果我们希望将文件的所有者更改为sue,我们可以执行以下命令:
sudo chown sue myfile.txt
在处理目录时,我们还可以使用-R标志来更改目录本身的所有权,以及它可能包含的所有文件和子目录的所有权:
sudo chown -R sue mydir
如果我们希望更改对象的组分配,我们可以按照以下语法执行:
sudo chown sue:sales myfile.txt
注意冒号(:)用来分隔用户和组。在这个命令中,我们确定了希望sue用户和sales组拥有该资源。我们还可以使用-R选项,如果目标是目录并且想要递归更改。
另一个值得了解的命令是chgrp命令,它允许你直接更改文件的组所有权。使用该命令时,你可以执行chgrp命令,并跟上你希望拥有该文件的组名,再加上文件名。例如,之前的chown命令可以简化为以下命令,因为我们只是修改了文件的组分配:
sudo chgrp sales myfile.txt
就像chown命令一样,我们也可以使用-R选项与chgrp命令一起使用,以便在处理目录时递归地更改权限。
好了,你现在应该能管理服务器上的文件和目录权限了。如果你以前没有在 Linux 系统上处理过权限,可能需要几次尝试才能掌握。最好的方法就是多练习。创建一些文件和目录(以及用户)并管理它们的权限。尝试删除用户对某个资源的访问权限,然后作为该用户尝试访问该资源,看看会遇到什么错误。修复这些错误并尝试更多示例。通过练习,你应该能很快掌握这项技能。
总结
在 Linux 管理和相关领域,管理用户和权限是你会经常做的事情。新用户会加入你的组织,而其他用户则会离开,因此这将成为你思维工具集的一部分。即使你是唯一使用服务器的人,考虑到进程如果无法访问其所需资源就无法运行,你也会发现自己需要管理应用程序的权限。
在这一章中,我们深入探讨了用户、组和权限的管理。我们学习了创建和删除用户、分配权限,以及使用 sudo 管理管理员访问权限。请在你的服务器上实践这些概念。当你掌握了这些技能,我们将在下一章见面,届时我们将讨论与软件包管理相关的所有内容。这将是一次史诗级的学习之旅。
相关视频
-
Linux 崩溃课程 – 管理用户 (LearnLinuxTV):
linux.video/lcc-users -
Linux 崩溃课程 – 理解文件与目录权限 (LearnLinuxTV):
linux.video/lcc-perm -
Linux 崩溃课程 – usermod (LearnLinuxTV):
linux.video/lcc-usermod -
Linux 崩溃课程 – sudo (LearnLinuxTV):
linux.video/lcc-sudo -
Linux 崩溃课程 – 用户账户与密码过期 (LearnLinuxTV):
linux.video/lcc-userexp -
Linux 崩溃课程 – 管理组 (LearnLinuxTV):
linux.video/lcc-groups
进一步阅读
-
文件权限(Ubuntu 社区 Wiki):
learnlinux.link/ubuntu-perms -
用户管理(Ubuntu 文档):
learnlinux.link/sec-users
加入我们的 Discord 社区
加入我们社区的 Discord 讨论空间,与作者及其他读者互动:

第三章:管理软件包
现在你已经设置好了服务器安装,并且知道如何管理服务器上的用户,是时候来了解如何管理软件了。Ubuntu 平台提供了大量的软件,从服务器管理到游戏应有尽有。事实上,在我写这章内容的时候,Ubuntu 的仓库中已经有超过 60,000 个软件包。这是非常多的软件包,在这一章里,我们将看看如何管理它们。我们将介绍如何安装、卸载和更新软件包,以及如何使用相关的工具。
在我们讲解这些概念时,我们将涵盖:
-
理解 Linux 包管理
-
理解 Debian 包和 Snap 包的区别
-
安装和卸载软件
-
查找软件包
-
管理软件包仓库
-
备份和恢复 Debian 软件包
-
清理孤立的 APT 软件包
-
利用硬件启用更新
为了开始,让我们了解一下 Ubuntu 中软件包的分发方式,以及包管理概念的基本原理。
理解 Linux 包管理
如今,应用商店在大多数平台上都非常流行;通常,你会有一个集中位置来获取应用程序,从中安装到你的设备上。即便是手机和平板电脑也利用一个集中的软件仓库,在其中软件被策划并提供。Android 平台有 Google Play,Apple 有它的 App Store,等等。对于那些已经使用 Linux 一段时间的人来说,这个概念并不新鲜。软件仓库的概念与应用商店类似,并且早在移动电话还没有彩屏之前,Linux 社区就已经有了这个概念。
Linux 从 90 年代开始就有了包管理功能,最初由Debian推广,然后是Red Hat。软件仓库通常以镜像的形式提供,服务器会订阅这些镜像。镜像在多个地理区域都有提供,所以通常情况下,你的 Ubuntu Server 安装会订阅离你最近的镜像。这些镜像里包含了你可以安装的软件包。许多软件包依赖其他软件包,因此在 Linux 平台上有各种工具可以自动处理这些依赖关系。并不是所有 Linux 发行版都具有包管理和依赖关系解决功能,但 Ubuntu 当然具备,因为它继承了 Debian 已经打下的基础。
这些镜像中的软件包在不断变化。传统上,一个被称为软件包维护者的个人负责一个或多个软件包,并将新版本提交到软件库进行审批,最终分发到镜像中。对于 Ubuntu 的软件库来说,负责维护软件包的是一组开发人员,而不仅仅是一个维护者。大多数情况下,软件包的新版本是为了修复安全漏洞,但没有其他新功能。由于 Ubuntu 的大多数软件包都是开源的,任何人都可以查看源代码,发现问题并报告。发现漏洞后,负责的团队会审查报告并发布更新版本进行修复。这个过程非常迅速,我曾见过在漏洞报告当天就发布了修复版本。可以说,Ubuntu 的开发人员在处理安全问题方面非常专业。
软件包的新版本有时也是功能更新,这些更新发布是为了引入一些新功能,这些新功能不一定与安全漏洞有关。这可能是桌面应用程序的新版本,例如 Firefox,或者服务器软件包的新版本,例如 MySQL。然而,大多数时候,版本差异较大的新软件包会等到下一个 Ubuntu 版本发布时才会推出。原因在于,过多的变化可能导致你的服务器不够稳定,可能会出现应用程序甚至整个操作系统崩溃的情况。相比之下,已知的稳定软件包是首选;然而,由于 Ubuntu 每六个月发布一次,你不必等得太久。
具体来说,Ubuntu 版本中的功能更新必须经过审批流程,才能在默认的软件库中发布,这些功能更新被特别称为稳定发布更新(SRUs)。关于这些更新的审批流程有一个完整的过程,但总体的理解是,在一个稳定的长期支持 Ubuntu 版本中,进行大版本更新应该有充分的理由。
作为服务器管理员,你经常需要在安全性和功能更新之间做出选择。安全更新是最重要的,它允许你在面对安全漏洞时修补服务器。有时,功能更新会在你的组织中变得必要,因为决定新增的功能可能会对你有所帮助,或者可能成为当前目标所必需的。在这一章中,我们不会重点讨论安装安全更新(我们会在第二十一章,保护你的服务器中处理这部分内容),但理解为什么会有新的软件包发布给你是非常重要的。
在 Ubuntu 中,软件包管理通常非常方便,安全更新和漏洞修复定期发布。只需一条命令(我们稍后会介绍),你就可以安装一个软件包及其所有依赖项。亲自处理过手动依赖解决的我可以告诉你,自动处理依赖项是非常棒的事情。
在 Linux 服务器上维护软件包的主要好处是,通常你不需要在互联网上搜索下载软件包,因为 Ubuntu 的仓库包含了你所需的大多数软件包。随着本章的深入,你将了解管理这些软件所需的一切。
理解 Debian 和 Snap 软件包之间的区别
现在,在我们深入了解如何管理软件包之前,实际上有两种完全不同类型的软件包可供选择,你应该理解它们之间的区别。撰写本文时,Linux 中的软件管理方式正处于一个十字路口。
传统上,每个发行版都有自己的软件包格式和用于管理它们的工具。Ubuntu 使用 Debian 软件包(软件包名以 .deb 结尾)作为主要的软件包格式,这一格式是 Ubuntu 从 Debian 发行版继承而来的(Ubuntu 是从 Debian 分支出来的,这意味着它以 Debian 为基础)。Ubuntu 和 Debian 使用 apt 和 dpkg 命令来管理软件包。另一方面,CentOS 和 Red Hat 等发行版使用 RPM 软件包 及 dnf 命令来管理它们。还有其他的发行版和软件包格式,但在本书中,我们将主要讨论 Ubuntu 中的内容。首先,让我们讨论一下 Debian 软件包。
Debian 软件包
Debian 软件包一直是 Ubuntu 中的主要软件包类型,至今为止 Ubuntu 的整个版本都在使用这种类型。当你在线搜索如何在 Ubuntu 中安装某个软件时,通常你会安装一个 Debian 软件包。这些软件包之所以被称为 Debian 软件包,是因为 Ubuntu 是基于 Debian 源构建的,并且使用相同的命令来安装这些软件包。所以,尽管 Ubuntu 不是 Debian(Debian 是一个完全不同的发行版),它们主要使用相同的软件包格式。
对于新手来说,命名可能会让人感到困惑,因为如果 Ubuntu 被认为是一个与 Debian 不同的发行版,那为什么要把它的软件包称为“Debian”软件包呢?Debian 软件包的文件名以 .deb 结尾,这种软件包格式起源于 Debian。Ubuntu 并没有开发自己的软件包类型;它使用与 Debian 相同的软件包格式。因此,无论我们是在 Debian 还是 Ubuntu 中安装软件包,使用像 apt 这样的命令时,Debian 软件包都是两者使用的软件包类型。
如果你在阅读本书之前已经使用过 Ubuntu,那么很可能你已经使用过 apt 系列命令来进行包管理。Debian 包非常适合与 apt 命令配合使用,因为它们易于操作,并且能够帮助你处理依赖关系的解析。然而,它们也带来了一些挑战和重大缺点。
首先,发行版的大部分内容由 Debian 包组成。这意味着 Linux 内核、系统包、库和安全更新都是在你安装 Ubuntu Server 时安装的 Debian 包。当你安装安全更新时,实际上是安装了 Debian 包。这个问题的原因在于,你将要安装的其他软件(例如 Apache、MariaDB 等)也是 Debian 包,这可能与系统包发生冲突,尤其是当一个包需要一个与另一个包冲突的前置包时。
这可能会导致无法安装任何软件包的情况。软件包维护者通常能够避免这种情况,因此如今冲突并不常见。
然而,使用 Debian 包时,如果系统库遭到损坏,实际上所有依赖于它的软件都会出现故障。Ubuntu 开发者对此非常关注,因此你通常不会遇到问题。但事实上,这对于 Ubuntu 的维护者来说是一项巨大的工作。
另一个与 Debian 包相关的问题是软件的可用性。当一个包的新主版本发布时,通常不会在下一次发行版发布之前提供给你。这意味着,如果你需要比当前 Ubuntu 版本中提供的更新版本的 PHP、Apache 或其他软件,通常是无法获得的。你通常需要等待下一次整个发行版的更新发布。有些例外情况,例如桌面版本的 Ubuntu 中的 Firefox。正如之前所提到的,新的主要包版本是规则的例外,来自 SRU 审批过程。虽然可用经过广泛测试的成熟软件能够提供更好的稳定性,但有时你可能需要比现有版本更新的软件,这可能会促使你考虑其他来源。毕竟,你不需要为了安装更新的应用程序而重新安装新版的 Windows 或 macOS!
通用包是 Linux 的一个新概念,旨在提供一种多种发行版都能识别的单一包格式。其理念是开发者只需要编译一个包(而不是为每个发行版编译独立的包),用户则只需下载一个包,而无需考虑其选择的 Linux 发行版。下一节将讨论一种名为 Snaps 的通用包类型。
Snap 包
当前,开发者需要创建多种包类型来支持 Linux。也许他们会为 Debian 本身和 Ubuntu 创建 Debian 包,然后为 CentOS、Red Hat Enterprise Linux 和 SuSE 创建 RPM 包。虽然你可能认为需要创建两种包类型并不算太糟糕,但考虑到每个基于 RPM 的发行版需要其特定的 RPM 包,你就能理解开发者每次发布软件时,需要创建五个或更多不同包的情况会是多么繁琐。
因此,推动采用一种单一的包格式,每个发行版都可以安装,并且这种格式与系统包类型无关。这个概念被称为 通用包,其目的是拥有一个标准的包格式,可以在任何 Linux 发行版上安装。因此,开发者只需要创建一个包,就能让他们的应用程序在所有流行的发行版上运行。
Linux 社区通常使用 distro 这个词作为 distribution(发行版)的缩写。
通用包的另一个好处是,它们内置了所有依赖项,因此冲突发生的可能性较小;应用程序所需的一切都包含在一个单独的包中。这非常好,因为使用通用包时,发生包冲突的可能性几乎是不存在的。
就像 IT 行业中的一切一样,我们无法集体满足并决定哪种技术是最合适的。因此,社区中对于多种类型的通用包哪一种最合适存在争论。我在本书中不会深入政治性讨论,因为每种技术都有其优缺点。但对于我们来说,尤其是关于 Ubuntu Server,实际上只有一种适合服务器安装的技术。用于通用包的竞争技术包括 Flatpak、AppImage 和 Snap 包。
Ubuntu 的开发商 Canonical 理解开发人员和用户在使用过程中遇到的痛点,并且一直在努力改变包管理的方式。为了解决这些问题,他们开发的通用包类型被称为 Snap 包。像所有通用包一样,Snap 包(或更简洁地称为 Snaps)对底层的 Debian 包没有任何影响,且是完全独立的,从而避免了与系统包发生冲突的可能性。这使得你可以安装比原本可用版本更新的应用版本。由于 Snaps 是与底层 Debian 包独立安装的,因此没有理由不安装它们。Snap 包在各方面都表现得更好,是一个很棒的概念。唯一的缺点可能是它们是较大的包,因为它们不仅包括应用程序本身,还将所有必需的库包含在一个单独的包中。然而,它们其实并没有那么大,不会导致磁盘空间问题。这些包的大小与 macOS 或 Windows 上的典型应用程序差不多。
那么,应该选择哪一个呢?其实这取决于你的使用场景。每种通用包类型在某些方面都表现得很好,但也有其缺点。Flatpaks 和 AppImages 也是很好的技术,但对于我们的使用场景,它们的短板在于不太支持服务器(非图形用户界面,即非GUI)应用程序。这意味着,这些包类型最适合安装你通常会在桌面版 Ubuntu 上找到的应用程序,例如音乐播放器、浏览器、图形化文本编辑器等等。Snap 包则支持图形用户界面和非图形用户界面应用。由于服务器安装通常根本没有图形用户界面,这就决定了我们的选择。或许未来 Flatpaks 和 AppImages 会更好地支持非 GUI 应用,但目前,选择通用包格式的问题非常简单。
然而,现阶段,我们安装的大多数软件包将是 Debian 包,因为 Snap 包的普及有些缓慢。它们在稳步进展,但行业如何接受它们还需观察。目前,最好是在 Snap 包可用时进行评估,并根据可用性、安全性、版本和支持情况选择最合适的包类型。
在了解了 Snaps 和 Debian 包之间的区别后,让我们通过一些例子来实际管理我们服务器上的软件。我们将查看搜索可用软件包的命令,然后我们将安装和卸载它们。
安装和卸载软件
在我们开始之前,我们需要对想要安装的应用程序进行一些研究。在 Ubuntu 中,安装软件有多种方法,所以最好的方法是通过访问我们想要安装的应用程序的官方网站来查看文档。通常,Google 搜索就能找到(只要确保检查域名并访问正确的网站)。大多数软件会提供适用于各种平台的安装说明,包括 Ubuntu。通常,它会告诉你通过apt install命令下载 Debian 软件包格式。其他时候,软件可能会提供 Snap 格式,甚至是 PPA 仓库(我们将在本章后面讨论 PPA 仓库)。让我们通过查看用于安装 Debian 软件包的apt命令,来开始我们的包管理之旅。
使用 apt 管理 Debian 软件包
APT,即高级包管理工具,是一套允许我们安装、删除和更新 Debian 软件包的工具。这个工具套件由多个子命令组成,接下来我们将逐一介绍。apt命令中最常用的变体是apt install。如果你曾经阅读过在 Ubuntu 上执行某些操作的说明,可能已经运行过这个命令来安装软件包。这正是它的功能:通过命令行安装 Ubuntu 的软件包。例如,以下命令将安装openssh-server软件包:
sudo apt install openssh-server
你还可以通过用空格分隔每个软件包来一次性安装多个软件包,而不必一个个地安装。以下示例将安装三个不同的软件包:
sudo apt install <package1> <package2> <package3>
在一些书籍、博客和文章中,你可能会看到更长版本的apt命令,比如apt-get install,而不仅仅是apt install。能够将诸如apt-get install这样的命令简化为apt install是 Debian 和 Ubuntu 中apt的一个相对较新的特性。两种方法都是有效的,但未来推荐使用简化后的apt命令。
那么,当你使用apt安装软件包时,究竟发生了什么?如果你之前已经执行过这个过程,你可能已经习惯了。但通常情况下,这个过程从apt计算依赖开始。大多数软件包需要其他软件包才能正常工作,因此apt会检查确保你请求的软件包以及它的依赖项都可用。
首先,你会看到apt希望对你的服务器进行的更改的总结。以在未配置的 Ubuntu 服务器上安装apache2软件包为例,我输入以下命令:
sudo apt install apache2
当它开始安装时,我在系统上看到以下输出:

图 3.1:在示例服务器上安装 Apache
即使我只要求安装apache2,apt仍然会通知我,它还需要安装apache2-bin、apache2-data、apache2-utils和libapr1(以及其他一些软件包),以满足apache2软件包的依赖。apt还建议我安装apache2-doc、apache2-suexec-pristine以及其他一些软件包,尽管它们是可选的,并不必要。你可以通过在apt install命令中添加--install-suggests选项来安装这些建议的软件包,但这并不总是一个好主意,因为它可能会安装许多你不需要的软件包。当然,你也可以通过使用apt命令单独选择性安装某些建议的软件包。
然而,大多数时候,你可能不想这样做;通常,最好将已安装的软件包保持在最小范围,只安装你需要的软件包。正如我们在第二十一章《保护你的服务器》中将讨论的那样,安装的软件包越少,服务器的攻击面就越小。
在通过apt安装或更新软件包时,你可能会看到一个消息,询问你是否愿意在过程中重启后台服务:

图 3.2:软件包安装/升级过程中的服务重启提示
我们将在第七章《控制和管理进程》中讨论进程管理相关的概念,所以现在不必太关注这个问题。为了让你暂时了解一下,所有 Linux 服务器上都在后台运行服务,每个服务提供特定的功能。重启服务将确保它以最新的补丁和调整重新启动,但任何连接到你重启的服务的用户都会被断开。现在,你可以在看到这个消息时随时按回车接受默认设置。
另一个常见的通过apt安装软件包时的选项是-y选项,它假定你选择是,自动跳过确认提示,询问你是否继续。例如,我之前的输出中包括了Do you want to continue? [Y/n]这一行。如果使用了-y选项,命令将会在没有任何确认的情况下继续安装软件包。这对于急于操作的管理员来说可能很有用,尽管我个人认为除非你在脚本中进行软件包安装,否则没有必要使用这个选项。事实上,有时它可能是一个不好的主意,因为假定是可能意味着你确认了一些你之后会后悔的操作。
Ubuntu Server 的另一个巧妙的默认设置是,它会自动配置大多数软件包,使其守护进程启动并启用,以便在每次启动时自动启动。以之前提到的apache2为例,一旦你安装了该软件包,apache2服务将启动,应用程序也会自动运行。
为了方便起见,这可能看起来是个不错的主意,但并非每个人都偏好这种自动化。如我所提到的,服务器上安装的包越多,攻击面就越大,而正在运行的服务(也称为守护进程或单元)每个都可能成为恶意分子入侵的途径,前提是存在安全漏洞。因此,一些发行版在安装包时不会自动启用和启动守护进程。然而,我个人认为,你只应安装那些你打算实际使用的包,因此,如果你特意去安装像apache2这样的包,那你大概率是希望开始使用它的。
当你使用apt命令安装包时,它会在本地数据库中查找你指定的包。如果没有找到,它会抛出一个错误。有时,这个错误可能是因为该包不可用,或者apt希望安装的版本已经不存在。Ubuntu 的仓库更新速度非常快;新版本的包几乎每天都会被添加。当一个包的新版本被添加时,它的旧版本可能会被删除。因此,建议你定期更新包源。这样做很简单,只需要使用以下命令:
sudo apt update
这个命令实际上并不会更新任何包;它仅仅是与本地镜像进行对接,查看是否有新的包被添加或移除,并更新本地索引。这个命令很有用,因为如果源不更新,包的安装可能会失败。在大多数情况下,症状表现为当apt在查找某个包时无法找到时,进程会报错,而这个包通常是正常情况下可用的。
移除包也非常简单,语法与安装包非常相似;你只需要将install关键字替换为remove即可:
sudo apt remove <package>
就像使用install选项一样,你也可以同时移除多个包。以下示例将移除三个包:
sudo apt remove <package1> <package2> <package3>
如果你不仅想移除一个包,还希望删除它的配置文件,你可以使用--purge选项:
sudo apt remove --purge <package>
这不仅会移除包,还会删除它的配置目录(应用程序通常会将它们的配置文件存储在/etc的子目录中)。
所以,这就总结了使用apt管理 Debian 包的基础知识。现在,让我们继续学习如何管理 Snap 包。
使用 snap 管理 Snap 包
要管理 Snap 包,我们使用snap命令。snap命令有几个选项,允许我们搜索、安装和移除服务器或工作站上的 Snap 包。
首先,我们可以使用snap find命令配合关键字来显示与该关键字匹配的可用 Snap 包列表:
snap find <keyword>
基本上,你只需要简单地输入snap find命令并加上搜索词来查找。例如,nmap应用程序就是一个很有用的工具,尤其是当我们在管理网络时:
snap find nmap
对于nmap,该工具在 Ubuntu 的默认仓库中可用,因此你无需使用 Snap 包进行安装。然而,通常 Snap 版本会比 APT 仓库中提供的版本更新,并且具有更多功能。如果我们希望安装 Snap 版本,可以使用以下命令:
sudo snap install nmap
使用install选项时,我们需要使用sudo,因为安装包的操作会对服务器进行更改。而对于简单的find命令,则可以省略sudo。
现在我们已安装了nmap,可以使用which命令检查nmap二进制文件的位置。我们可以使用which命令查找命令的二进制文件位置——如果安装了相应的包,它将显示二进制文件的路径。如果命令不可用,which命令将没有输出。因此,如果我们运行以下命令,我们现在应该能看到该二进制文件的路径打印到屏幕上:
which nmap
这将返回以下输出,显示nmap的 Snap 版本是从一个特殊位置运行的:

图 3.3:检查 nmap 二进制文件的位置
现在,当我们运行nmap时,实际上是从/snap/bin/nmap运行的。如果我们通过apt安装nmap,它将从/usr/bin/nmap运行。如果我们还从 Ubuntu 的 APT 仓库安装了nmap工具,那么我们可以随时通过指定要运行的二进制文件的完整路径来运行任意一个,因为 Snap 包与 APT 包是独立的。例如,如果我们通过snap和apt都安装了nmap,我们可以通过运行/usr/bin/nmap来运行 Ubuntu 的版本,通过运行/snap/bin/nmap来运行 Snap 版本。
移除已安装的 Snap 包很容易。我们只需使用remove选项:
sudo snap remove nmap
如果我们执行该命令,那么nmap(或我们指定的任何 Snap 包)将从系统中移除。
要更新一个包,我们使用refresh选项,并指定要更新的包名:
sudo snap refresh <package>
使用该命令,包将更新到最新版本。如果进一步操作,我们还可以尝试使用相同的命令更新服务器上的所有 Snap 包(无需指定包名):
sudo snap refresh
正如你所看到的,管理 Snap 包相当简单。使用snap命令套件,我们可以从服务器或工作站安装、更新或移除包。snap find命令允许我们查找新的 Snap 包以供安装。也许随着技术的发展,我们将会比安装 Debian 包更多地安装 Snap 包,但这仍有待观察。现在,有两个安装新软件的选择是一个不错的优势。
除了能够安装包之外,还有一些额外的技巧和注意事项,帮助我们搜索包。毕竟,如果你不知道有哪些包可用,你是无法安装的。我们将在下一节探讨如何搜索包。
搜索包
不幸的是,Ubuntu Server 中软件包的命名约定并不总是显而易见。更糟糕的是,即使是同一软件,不同的发行版中的软件包名称也往往大相径庭。尽管本书和其他在线教程会列出安装软件的确切步骤,但如果你独立操作,而不知道要安装的软件包的名称,那就没什么帮助了。在本节中,我将尽量揭开搜索软件包的神秘面纱。
在上一节中,我们已经讲解了如何搜索 Snap 软件包,所以这里就不再重复了。APT 工具套件也提供了搜索软件包的功能,使用的命令是apt search。我们可以使用以下命令,通过提供关键字来搜索软件包:
apt search <search term>
该命令的输出将显示与搜索条件匹配的软件包列表,包括它们的名称和描述。例如,如果你想安装 Apache 的 PHP 插件,但还不知道关联的包名,以下命令可以帮助你缩小范围:
apt search apache php
在输出中,我们会看到一个不止一项的软件包列表,但我们可以从输出中的软件包描述中推测出libapache2-mod-php很可能是我们需要的那个。然后,我们可以像平常一样使用apt安装它。
如果我们不确定这是否是我们想要的真正软件包,我们可以使用apt-cache show命令查看关于这个(或任何其他)软件包的更多信息:
apt-cache show libapache2-mod-php
该命令的输出在以下截图中进行了展示:

图 3.4:展示 Debian 软件包的信息
使用这个命令,我们可以看到关于我们考虑安装的软件包的更多细节。在这个例子中,我们了解到libapache2-mod-php软件包还依赖于 PHP 本身,这意味着如果我们安装这个软件包,我们不仅会得到 PHP 插件,还会安装 PHP 本身。
另一种搜索软件包的方法(如果你有可用的网页浏览器)是访问Ubuntu 软件包搜索页面:packages.ubuntu.com/,你可以通过该页面在其数据库中浏览当前支持版本的 Ubuntu 所有的软件包。虽然在工作中你不总是能访问网页浏览器,但当你能够使用时,这是一个非常有用的方式来搜索软件包、查看它们的依赖关系、描述等信息。
使用 apt search 命令以及 snap find 命令,应该能帮助你在确定要安装的包名时走得更远。软件包管理技能需要随着时间积累,因此不要指望立即就能知道应该安装哪些包。如果有疑问,只需进行 Google 搜索,研究你要运行的软件的文档,学习如何在 Ubuntu 上安装它。通常,安装说明会引导你使用正确的命令。本书中我们将讲解的示例,将引导你完成 Ubuntu Server 最常见的使用案例。
在这一点上,我们应该已经对不同类型的软件包及其管理方式有了清晰的理解。然而,有时我们需要在服务器上运行一些软件,而这些软件在标准仓库中没有可用的包。因此,在下一部分,我们将学习如何添加额外的仓库,从中安装软件。
管理软件包仓库
通常,Ubuntu 自带的仓库已经足够满足大多数通过 APT 安装的 Debian 包。但偶尔,你可能需要安装一个额外的仓库,以便使用 Ubuntu 通常不提供的软件,或者是比你通常能获取到的软件包版本更新的版本。添加额外的仓库允许你订阅额外的软件源,并像从其他源一样安装软件包。
然而,添加额外的仓库应当被视为最后的手段。当你安装额外的仓库时,实际上是在将你组织的服务器交给该仓库的作者信任。尽管我亲眼未曾见过这种情况,但理论上,软件作者可能会在软件包中加入后门或恶意软件(无论是有意还是无意的),并通过软件仓库提供给其他人。因此,你应该仅添加那些你有理由信任的来源的仓库。
此外,有时会发生仓库的维护者干脆放弃维护该仓库并消失。我亲眼见过这种情况。在这种情况下,仓库可能会下线(这会在apt事务中显示错误,表示无法连接到仓库),更糟糕的是,仓库保持在线,但从未提供安全更新,导致你的服务器易受到攻击。有时,你可能别无选择。你需要一个特定的应用程序,而 Ubuntu 默认并未提供。你唯一的选择可能是从源代码编译一个应用程序或添加一个仓库。这个决定由你做,但尽可能时要考虑安全问题。如果有疑虑,除非这是获取你所需内容的唯一方法,否则避免添加仓库。如果你添加了一个额外的仓库,且长时间没有看到来自该仓库的包更新,考虑这是一个红旗,调查该仓库是否已被弃用。已弃用的仓库应被移除,替换为其他可用仓库。
添加额外的仓库
软件仓库本质上是一个存储在文本文件中的 URL,存储在两个地方之一。主 Ubuntu 仓库列表存储在/etc/apt/sources.list中。在该文件中,你会找到多个仓库,供 Ubuntu 的包管理器拉取包。此外,扩展名为.list的文件会从/etc/apt/sources.list.d/目录中读取,并且在你使用apt时也会被使用。我将演示这两种方法。
这两个文件中的典型仓库行会类似于以下内容:
deb http://us.archive.ubuntu.com/ubuntu/ jammy main restricted
每行的第一部分将是deb或deb-src,指示apt命令是否会在那里找到二进制包(deb)或源代码包(deb-src)。接下来是apt用来访问仓库的实际 URL。在第三部分,我们有发行版的代号;在这个例子中,它是jammy(指的是 Ubuntu 22.04 的代号,Jammy Jellyfish)。
如果你还不知道,每个 Ubuntu 发行版的代号都是基于一种动物。通常,所选的动物会比较稀有。例如,上一个 LTS 版本的代号是“fossa”,它是一种来自马达加斯加的动物,长得像猫,但耳朵是弯曲的。这次的代号是“jellyfish”,我相信大多数读者已经熟悉这种动物。
接下来,每个仓库行的第四部分是Component,它表示该仓库是否包含自由和开源的软件,并且是否得到 Canonical(负责 Ubuntu 开发的公司)的官方支持。该组件可以是main、restricted、universe或multiverse。带有main组件的仓库包含官方支持的软件,这通常意味着这些软件包有源代码可用,因此 Ubuntu 开发者能够修复 bug。标记为restricted的软件仍然得到支持,但可能存在有问题的许可。universe软件包由社区提供支持,而不是 Canonical 本身。最后,multiverse软件包包含既不自由也不受支持的软件,使用这些软件需要自行承担风险。
正如您从查看服务器上的/etc/apt/sources.list文件中看到的那样,一个仓库行可以包含来自多个组件的软件。每个仓库 URL 可能包含来自多个组件的软件包,而您区分它们的方法是只订阅该仓库所需的组件。在我们之前的示例中,仓库行同时包括main和restricted组件。这意味着,在这个特定示例中,apt工具将从该仓库索引自由(main)和非自由(restricted)软件包。
您可以将新的仓库添加到/etc/apt/sources.list文件中(它仍然可以正常工作),但这通常不是首选方法。正如我之前提到的,apt会扫描/etc/apt/sources.list.d/目录,查找以.list扩展名结尾的文本文件。这些文本文件的格式与/etc/apt/sources.list文件相同,即每行包含一个额外的仓库,但这种方法使您能够通过创建文件来轻松添加新仓库,也可以通过删除该文件来删除仓库。
这种方法比直接编辑/etc/apt/sources.list文件更安全,因为直接编辑时总有可能打错字,导致您无法从甚至官方仓库下载软件包。
此外,您可能需要为新的仓库安装GNU 隐私保护(GnuPG)密钥,但此过程因应用程序而异。通常,文档会概述整个过程。此密钥的作用是保护您,确保您正在安装签名的软件包。并非所有开发者都以这种方式保护他们的应用程序,但这绝对是一种值得做的好事。
一旦您在服务器上安装了仓库(以及可能的密钥),您需要运行以下命令来更新软件包索引:
sudo apt update
如本章前面所提到的,这个命令更新你本地的缓存,显示远程服务器上有哪些可用的软件包。APT 只知道它数据库中的软件包,所以你需要在使用这个命令之前同步它,这样才能实际安装仓库中的软件。
添加个人软件包存档
在 Ubuntu 平台上,还有另一种类型的仓库,称为个人软件包存档(PPA)。PPA 本质上是 APT 仓库的另一种形式,你仍然可以像平常一样使用apt命令与它们的包进行交互。PPA 通常是非常小的仓库,通常只包含一个应用程序,服务于一个特定的用途。可以把 PPA 看作是迷你仓库。当一个厂商没有提供自己的仓库,而只将应用程序以源代码的形式提供,用户需要手动下载、编译并安装时,PPA 非常常见。通过 PPA 平台,任何人都可以从源代码编译一个包,并轻松地将其提供给其他人下载。
PPAs 和常规仓库一样,也存在相同的安全问题(你需要信任供应商等),但考虑到软件通常根本没有经过审计,它们的安全性可能更差。此外,如果 PPA 服务器出现故障,你将停止收到从中安装的应用程序的安全更新。只有在确实需要时,才使用 PPA。
有一个可能非常有吸引力的 PPA 用例,特别适用于那些标准仓库无法很好处理的服务器平台,尤其是在软件版本管理方面。正如我之前提到的,像 PHP 或 MySQL 这样的主要服务器组件,可能会随着每次 Ubuntu Server 的发布而锁定在特定的主版本中。如果你需要使用 Ubuntu Server,但所需的应用程序版本在你组织要求的版本中不可用时,该怎么办?过去,你基本上只能在发行版和软件包之间做出选择,一些组织甚至会使用不同的 Linux 发行版,仅仅是为了满足在特定版本下运行特定应用程序的需求。你当然可以从源代码编译应用程序(假设源代码是可用的),但这样会带来额外的麻烦,因为你需要负责自己编译新的安全补丁,每当有新补丁发布时。PPAs 可以让你访问那些通常在默认仓库中不可用的应用程序,和/或访问比通常提供的版本更新的软件包。这使得你,作为服务器管理员,能够选择最适合自己目标的方式。
PPAs 通常通过apt-add-repository命令添加到你的服务器中。语法通常是使用apt-add-repository命令,后接冒号,再跟上用户名,最后是 PPA 名称。以下命令是一个假设的示例:
sudo apt-add-repository ppa:username/myawesomesoftware-1.0
要开始这个过程,你可以访问 Ubuntu 的 PPA 网站,网址是launchpad.net/ubuntu/+ppas。在那里,你可以在可用的 PPA 中进行搜索。
在将 PPA 添加到服务器之前,最好先研究一下它是否得到了良好的维护。例如,如果 PPA 很长时间没有更新软件包,那么这就值得关注——大多数软件包都会有安全修复。如果一个软件包没有定期更新,已经变得“过时”,那么最好避免使用它,因为它可能会带来更多的负面影响。
一旦你找到一个想要添加到服务器的 PPA,你可以通过找到 PPA 的名称,然后使用 apt-add-repository 命令将其添加到你的服务器中。
然而,你应该查看一下 PPA 的页面,以防有不同的说明。大部分情况下,apt-add-repository 命令应该能够正常工作。每个 PPA 通常都有一套附带的说明,因此这里不需要进行猜测。
那么,apt-add-repository 命令到底做了什么呢?说实话,它并不特别神奇。当你安装 PPA 时,它本质上是自动化了将一个仓库文件添加到 /etc/apt/sources.list.d 目录并安装其密钥的过程。因此,你可以通过简单地删除其文件来卸载 PPA。
如果谨慎使用,PPA 是一个非常有用的功能。PPA 为 Ubuntu 提供了一种灵活的方式来添加通常不会提供的额外软件,尽管你需要关注这些仓库,确保它们在出现漏洞时得到妥善修补,并且仅在绝对必要时使用。始终优先选择来自 Ubuntu 默认仓库以及 Snaps 的软件包,但 PPA 为你提供了另一种选择,以防你在其他地方找不到所需的软件。
在你维护服务器一段时间后,或者完成了为特定目标设置服务器的工作,你会安装大量的软件包以适应其目的。导出已安装的软件包列表可以让你在需要时更容易地重建服务器,下一部分我们将讨论一种实现方法。
备份和恢复 Debian 软件包
随着你维护服务器,已安装的软件包列表会不断增加。如果因为某些原因,你需要重建服务器,你需要精确复现之前安装的所有内容,这可能会非常麻烦。通常建议你通过变更控制流程记录所有对服务器的修改,但至少,跟踪已安装的软件包是绝对必须的。在某些情况下,服务器可能只包含一两个额外的软件包来实现目标,但在其他情况下,你可能需要一个特定的软件和库组合才能让服务器像之前一样工作。幸运的是,dpkg 命令允许我们导出和导入要安装的软件包列表。
要导出已安装包的列表,我们可以使用以下命令:
dpkg --get-selections > packages.list
此命令将把包选择列表导出到一个标准文本文件。如果你打开它,你将看到已安装包的列表,每行一个。导出的文件中的典型行如下所示:
tmux install
使用这个列表,我们可以将选择的包重新导入到服务器中,如果我们需要重新安装 Ubuntu Server,或者导入到一个新服务器中,使其执行类似的功能。首先,在管理任何包之前,我们应该更新索引:
sudo apt update
接下来,我们需要确保安装了dselect包。dselect包在管理 Debian 包时提供了额外的功能。它的细节超出了本章的范围,但对于我们当前的目标,我们可以使用它从导出的列表中恢复包。在你的命令行提示符下,键入which dselect,你应该会看到类似如下的输出:
/usr/bin/dselect
如果你没有看到输出,你需要使用apt安装dselect包:
sudo apt install dselect
完成后,你可以导入之前保存的包列表,并重新安装服务器上缺失的包。以下命令将完成此过程:
sudo dselect update
sudo dpkg --set-selections < packages.list
sudo apt-get dselect-upgrade
通常,我们现在使用apt而不是apt-get,但奇怪的是,dselect-upgrade命令仅在apt-get下有效。
在你执行了这些命令后,包列表中包含但尚未安装的包将在你确认更改后安装。这种方法可以让你轻松地恢复之前在服务器上安装的包,如果因为某些原因需要重建服务器时,也可以为新服务器的配置提供相似的设置。
现在我们已经了解了如何导出和导入已安装包的列表,我们还可以看看如何清理不需要的包,确保我们的服务器尽可能不含多余的臃肿。
清理孤立的 apt 包
当你在服务器上管理包时,最终会遇到这样一种情况:系统中有一些已安装但没有被任何东西需要的包。这种情况发生在移除具有依赖关系的包时,或者安装包的依赖关系发生变化时。你可能还记得,当你安装一个需要其他包的包时,这些依赖包也会一并安装。但是,如果你删除了那个需要它们的包,这些依赖包不会被自动移除。
为了说明这种情况,如果我从我的一台服务器上移除apache2包,当我尝试安装其他东西时,会看到以下额外信息:

图 3.5:显示孤立包的输出
在这个例子中,我移除了apache2(这是在截图前完成的),然后我安装了tmux。我尝试安装的包是任意的;重要的是你在截图中看到的文字,那里写着The following packages were automatically installed and are no longer required。基本上,如果你的系统上有孤立的包,Ubuntu 会在你使用 APT 工具套件时定期提醒你。在这种情况下,我移除了apache2,因此所有为了支持apache2包而安装的依赖包都不再需要。
在截图中,我看到了一些 APT 认为我不再需要的包列表。它可能是对的,但我们需要进一步调查。按照输出中的指示,我们可以以root身份或使用sudo命令来移除这些包。这是保持已安装包清洁的好方法,但需要小心使用。如果你最近刚刚移除了某个包,手动清理可能是安全的。
虽然我们还没有讲解更新包的内容(我们将在第二十一章,保护你的服务器中进行讲解),但以后可能会遇到一种情况,你的内核版本过旧,可以通过autoremove选项进行清理。这些内核包会以与前面截图中显示的孤立包相同的方式显示,但名称中会包含linux-image。处理这些时要小心;在你验证新安装的内核工作正常并且服务器没有出现任何不必要或意外的行为之前,绝不要移除过时的内核。通常来说,当涉及到内核包时,你可能会想至少等一周才执行apt autoremove。
对于其他包,通常可以安全地通过apt autoremove命令移除,因为它们大多数是作为另一个已不在系统中的包的依赖而安装的。然而,在移除这些包之前,请务必仔细检查你是否真的想要移除它们。如果你不小心移除了某个包,可以随时重新安装它,并且作为附加好处,如果你重新安装了一个标记为自动移除的包,它以后不会再在输出中显示为孤立包。
如你所见,Ubuntu 在管理软件方面有许多选项,作为管理员,你可以选择最适合你的目标的方法。还有一种特殊类型的更新可以提高硬件支持,适用于硬件比你的 Ubuntu 版本更新的情况,或者为以前没有支持的硬件提供兼容性。
利用硬件启用更新
Linux 行业的一个问题是硬件支持。在各种 Linux 发行版中,这是一个问题,因为你可能会遇到这样的情况:你正在使用一台发布时配备了最新处理器和芯片组的服务器(甚至是台式机或笔记本),但你的 Linux 发行版还没有发布包含更新驱动程序的新版,无法支持它。与 Windows 等平台不同,硬件驱动通常直接集成在 Linux 内核中。所以,如果你使用的是旧版(该版本包含较旧的内核),你可能就无法获得硬件支持,直到下一个 Linux 发行版发布。
幸运的是,Ubuntu 想出了一个系统来解决这个问题,这也是它与其他发行版不同的许多优点之一。Ubuntu 提供了一组称为硬件启用(HWE)堆栈的更新,这是长期支持(LTS)版本的独有功能。我们在第一章、部署 Ubuntu 服务器中讨论过 LTS 版本和常规版本的区别。HWE 更新是可选的,但它们为当前 LTS 版本发布之后的较新硬件提供了额外的兼容性。新的 HWE 堆栈通常包括新的内核和驱动包。然而,由于驱动程序通常是直接集成到 Linux 内核中的,因此你还将获得对新视频卡以外的其他硬件(例如新发布的网卡)的支持。
HWE 更新通常从 LTS 版本的第二个点发布开始提供,然后每次随后的点发布中都会提供,直到下一个 LTS 版本发布。新的兼容性堆栈是从最新的非 LTS 版本回溯过来的,这意味着你可以在保持 LTS 版本的同时,享受最新非 LTS 版本的兼容性。例如,当 Ubuntu 20.04.2 在 2021 年 2 月 4 日发布时,它包含了从 Ubuntu 20.10 回溯过来的内核和驱动包。这意味着你可以在保持 20.04 LTS 的同时,利用 Ubuntu 20.10 的较新硬件支持,并享受 LTS 版本的更长支持周期。
截至撰写时,Ubuntu 22.04 是最新的 LTS 版本,因此尚未添加硬件启用。根据历史经验,硬件启用可能会像以往版本一样在 22.04 LTS 中展开。如果是这样,那么更新的包将在 Ubuntu 22.04.2 发布后提供。
一旦新的硬件启用堆栈(HWE)可用,你可以选择安装它,或者继续使用原来的 22.04 内核而不做任何更改。对于 Ubuntu 桌面版来说,喜欢玩电脑游戏的人会从这些更新中受益匪浅,因为新的驱动程序能在显卡方面提供更好的性能,并且支持更新的游戏硬件。就本书的范围而言,新的硬件启用对我们来说的好处比桌面用户少一些。原因是,如果你的服务器运行正常没有问题,那么实际上没有必要安装新的硬件启用堆栈,除非更新的内核包含你希望利用的新特性。
有时会遇到一个让人沮丧的情况,那就是 Ubuntu 报告说没有可用的网络卡,尽管你实际上有一张。通常在购买新服务器,特别是包含最新硬件的物理服务器时,我遇到这种情况最为频繁。当你启动一台崭新的服务器时,却发现根本无法建立网络连接,这种情况非常让人沮丧。这是需要更新兼容性堆栈的经典症状。也许你的网络卡是在当前 Ubuntu 版本发布之后才发布的。这种情况正好说明了硬件启用更新存在的原因,也解释了为什么你迟早可能需要使用它们。
如果你没有选择硬件启用更新,你的服务器将始终保持与首次发布你所安装的 LTS 版本时相同的硬件启用(内核、驱动程序等)。在这种情况下,只有在安装新的安全更新时,才会更新内核和相关软件包。稍后,你可以手动选择是否安装硬件启用更新。通常,只有当你向物理服务器添加了需要新内核的新硬件时,才会这样做。如果你的服务器运行正常,并且没有添加新硬件,那么可能没有必要安装新的硬件启用堆栈。
如果你决定使用这些更新,有两种方法可以实现。你可以在安装 Ubuntu Server 时选择更新的硬件启用堆栈,或者手动安装所需的软件包。在撰写本文时,Ubuntu 22.04 刚刚发布,因此是否会在 22.04.2 发布时提供更新的软件包尚未可知,但鉴于 Ubuntu 的历史,这种可能性很大。以下截图展示了 Ubuntu 20.04.4 安装程序的第一个界面。假设 Ubuntu 22.04 按照相同的计划进行,届时你很可能会看到类似下图所示的选项,一旦 22.04.2 发布:

图 3.6:Ubuntu 20.04.4 安装程序的主菜单
请注意使用 HWE 内核安装 Ubuntu Server选项。如果你选择此选项,那么你的 Ubuntu Server 安装将立即包含新的 HWE 软件包。
如果你已经安装了 Ubuntu 服务器,并且希望之后安装 HWE 更新,可以通过终端进行安装。例如,在 Ubuntu 20.04 中,你可以使用以下命令切换到 HWE 内核:
sudo apt install --install-recommends linux-generic-hwe-20.04
当 Ubuntu 22.04 发布更新的 HWE 堆栈时,很可能会使用类似的命令来安装它。如果你需要更新的 HWE 内核,请在那个时候参考 Ubuntu 文档页面中的说明。
概述
在本章中,我们对包管理的世界进行了快速入门。正如你所看到的,Ubuntu 服务器提供了大量的软件包和各种工具,我们可以利用它们来管理这些包。我们从讨论 Ubuntu 的包管理如何工作开始,接着介绍了安装软件包、搜索软件包和管理仓库的过程。同时,我们还涉及了 Snap 包,这是一种较新的技术,旨在增强 Ubuntu 上的软件分发。
在第四章,导航与基础命令中,我们将探讨一些基础命令,用于导航 Linux Shell,了解文件系统布局等。
相关视频
- Linux 快速入门 - APT (LearnLinuxTV):
linux.video/lcc-apt
进一步阅读
-
LTSEnablementStack (Ubuntu wiki):
learnlinux.link/lts-es -
Ubuntu 服务器指南:
ubuntu.com/server/docs
加入我们的 Discord 社区
加入我们社区的 Discord 频道,与作者和其他读者讨论:

第四章:导航和基本命令
在我们旅程的这个阶段,我们已经覆盖了许多内容——我们学会了如何部署 Ubuntu 服务器,如何管理用户,最近还学会了如何管理软件包。在继续之前,我们应该花点时间学习一些重要的概念和命令,这些内容将帮助我们建立更多的基础知识,这些知识将对本书的后续部分乃至更远的未来有所帮助。这些基础概念包括用于浏览 shell、Linux 文件系统结构、查看文件内容甚至查看日志文件的核心 Linux 命令。具体来说,本讨论将包括:
-
学习基本的 Linux 命令
-
理解 Linux 文件系统结构
-
查看文件内容
-
查看应用程序日志文件
让我们花点时间学习一些基本的 Linux 命令,这些命令将帮助我们增强命令行技能。
学习基本的 Linux 命令
在命令行上建立坚实的能力至关重要,这将有效地赋予任何系统管理员或工程师超能力。我们的新能力虽然无法让我们一跃而过高楼,但绝对能让我们像命令行忍者一样执行终端命令。虽然我们在这一部分不能完全掌握使用命令行的艺术(那只能通过多年的经验积累),但我们一定会变得更加自信。
首先,让我们来谈谈如何在 Linux 文件系统中从一个位置移动到另一个位置。具体来说,所谓“Linux 文件系统”指的是你在 Ubuntu 安装中默认的各种文件夹(也称为“目录”)的结构。Linux 文件系统包含许多重要的目录,每个目录都有其指定的用途,我们将在本章后续部分详细讨论这些目录。
在我们进一步探索之前,我们需要先学习如何从一个目录切换到另一个目录。我们将在本节中介绍的第一个与浏览文件系统相关的命令将帮助你明确当前所在的目录。为此,我们有 pwd 命令。它代表 print working directory,用于显示你当前所在的文件系统位置。如果你运行它,你可能会看到如下输出:

图 4.1:查看当前工作目录
在这个例子中,当我运行pwd时,输出告诉我当前工作目录是/home/jay。这个目录就是你的家目录,默认情况下,每个用户都有一个(正如我们在第二章,管理用户与权限中讨论的)。默认情况下,所有与你的用户账户相关的文件都会存储在这里。当然,你可以在任何你想要的位置创建文件,即使是在家目录之外,如果你有权限或者使用sudo。但是,仅仅因为你可以这样做,并不意味着你应该。正如你将在本章中学到的那样,Linux 文件系统几乎为所有内容都有一个指定的位置。但你的家目录,位于/home/<用户名>,是属于你的。你拥有它,你控制它——它是你在服务器上的家。在 2000 年代初期,带图形用户界面的 Linux 安装系统甚至通过一个房子图标来表示你的家目录。
通常,你在家目录中创建的文件,其权限字符串大致如下:
-rw-rw-r-- 1 jay jay 0 Jul 5 14:10 testfile.txt
我们已经讨论了权限,并且在第二章,管理用户与权限中讲解了如何读取权限字符串,但你可以看到,默认情况下,你在家目录中创建的文件由你自己、你的用户组拥有,并且可以被三类用户(用户、组、其他)读取。
要更改当前目录并导航到另一个目录,我们可以使用cd命令并指定目标路径:
cd /etc
现在,我还没有查看文件和目录布局,所以我随便选择了/etc目录。前面的斜杠表示文件系统的开始。稍后会详细讲解。现在,我们在/etc目录中,并且我们的命令提示符也发生了变化:

图 4.2:切换目录后,命令提示符和 pwd 命令的输出
正如你可能猜到的,cd命令代表切换目录,它用于在导航时将当前工作目录从一个目录切换到另一个目录。例如,你可以使用以下命令返回家目录:
cd /home/<user>
实际上,有多种方法可以返回家目录,以下截图展示了其中几种方法:

图 4.3:其他进入家目录的方法
第一个命令,cd -,其实并不专门与家目录有关。它是一个巧妙的技巧,可以让你返回到你最近所在的目录。对我来说,cd -命令把我带回到我之前所在的目录,而那个目录恰好是/home/jay。第二个命令,cd /home/jay,直接把我带到了家目录,因为我指定了完整路径。最后一个命令,cd ~,也把我带到了家目录。这是因为~是家目录路径的简写,因此你不需要每次都输入完整路径/home/<user>,只需使用~来代替。
另一个基本命令是ls。ls命令列出当前工作目录的内容。我们可能还没有在主目录中存放任何内容。但是,如果我们通过执行cd /etc(就像之前做的那样)导航到/etc,然后执行ls,我们会看到/etc目录中有许多文件。试试看吧:
cd /etc
ls
实际上,我们并不需要改变工作目录到/etc来列出内容。我们可以直接执行以下命令:
ls /etc
更好的是,我们可以运行:
ls -l /etc
这将以长列表的形式显示内容,我认为这样更容易理解。它会将每个目录或文件条目显示在单独的一行上,并附上权限字符串。但你可能已经记得ls和ls -l,这两者在第二章,用户与权限管理中提到过,所以我在这里不再详细说明。那例子中ls命令的-l部分被称为参数。我并不是指 Linux 社区中关于哪个命令行文本编辑器最好(无疑是 Vim)的永无休止的辩论,而是指在 Shell 命令中,参数可以让你覆盖默认设置,或者以某种方式为命令提供选项,就像在这个例子中,我们将ls的输出格式化为长列表。
rm命令是我们在第二章,用户与权限管理中提到的另一个命令,当时我们讨论了手动删除已从系统中移除的用户的主目录。因此,到现在为止,你可能已经很熟悉这个命令及其功能(它用于删除文件和目录)。这是一个潜在危险的命令,因为你可能会不小心删除不该删除的内容。在那一章中,我们使用了以下命令来删除用户dscully的主目录:
rm -r /home/dscully
如你所见,我们使用-r参数来改变rm命令的行为,默认情况下,rm不会删除目录,只会删除文件。-r参数指示rm递归地删除一切,甚至是目录。-r参数还会删除路径中的子目录,因此你在使用此命令时一定要小心。正如我在书中早些时候提到的,如果你与rm一起使用sudo,你甚至有可能删除整个 Ubuntu 系统!
rm提供的另一个选项是-f参数,代表force(强制),它告诉rm在删除前不要进行确认提示。这个参数不常用,它的使用案例超出了本章的范围。但请记住它的存在,万一需要的话。
另一个值得了解的基础命令是touch,它实际上有两个用途。首先,假设你在当前工作目录中有权限,touch命令将创建一个空文件(如果文件尚不存在的话)。其次,touch命令将更新文件或目录的修改时间(如果它已经存在的话):

图 4.4:使用touch命令进行实验
为了说明这一点,在相关的截图中,我运行了几个命令。首先,我运行了以下命令来创建一个空文件:
touch testfile.txt
之前那个文件并不存在,所以当我之后运行ls -l时,它显示了新创建的文件,大小为 0 字节。接下来,我在一分钟后再次运行了touch testfile.txt命令,你可以在截图中看到,修改时间从15:12变成了15:13。
关于查看文件内容,我们将在本章稍后讨论。我们肯定还需要学习更多的命令,以建立我们的基础。但现在,让我们暂时从基础概念中休息一下,来更好地理解 Linux 文件系统布局,这对我们稍后将要学习的一些命令来说是必不可少的。
理解 Linux 文件系统布局
如我之前提到的,Linux 安装中的每个目录都有一个指定的用途。这不是硬性规则,而是关于某些内容应该放置位置的强烈推荐。
你当然可以不遵循这些推荐;最终,你对你的安装拥有完全的控制权。但是,如果你习惯将文件放在奇怪的地方,可能会惹恼你的同事。在这一节中,我们将介绍最常见的目录并讨论它们的用途。
在 Linux 世界中,文件系统一词本身可能会让人感到困惑,因为它可以指代两个不同的事物——默认的目录结构,以及在格式化磁盘如硬盘或闪存驱动器时选择的实际文件系统(如 ext4、XFS 等)。在本节中,我们将快速了解 Linux 文件系统,重点关注默认的目录结构。
在 Linux 中(Ubuntu 使用 Linux 内核和相关工具,因此它是 Linux 的一个发行版),文件系统从一个单独的斜杠/开始。这被认为是文件系统的起点,目录和子目录从这里分支出来。例如,考虑/home目录。这个目录存在于文件系统的根级别,你可以通过它以斜杠开始来看到这一点。我的系统上的主目录是/home/jay,这意味着它是home目录中的jay目录,而这个目录位于文件系统的起始位置。
起初可能会感到困惑,但一旦习惯了,就会变得非常合乎逻辑。如果您熟悉 Microsoft Windows,那么您可以将 / 技术上视为 C: 驱动器。实际上比这复杂一些,但如果我们忽略一些怪癖,这个比较是成立的。为了更好地理解这一点,请在服务器上对几个目录执行 ls 命令。如果您执行 ls /,您将看到文件系统根目录下的所有目录。
在结果中您会看到 home 目录,以及许多其他目录。对于那些更喜欢视觉表现的人,以下截图展示了一个文件系统示例:

图 4.5:典型 Linux 文件系统部分的图表
正如您所见,Linux 文件系统类似于一棵树,有一个主干向外延伸,目录之间相互分支。这个默认的目录结构是 文件系统层次结构标准(FHS)的一部分,它是一组指南,定义了目录结构的布局方式。该规范定义了目录的名称、位置以及用途。发行版有时会违反这里的一些定义,但大部分时间还是遵循得比较严格的。这就是为什么您可能会在 Ubuntu 以外的 Linux 发行版上看到非常相似(如果不是完全相同)的目录结构。
那么,这为何重要呢?正如我所提到的,每个目录通常都有其用途。关于默认布局有时会有一些争议,并且偶尔会有一些更改。但就 Linux 而言,文件系统布局的更改频率通常低于其他方面。
FHS 的完整详细解释将非常庞大,但在本章末尾我包含了该规范的链接,如果您决定进一步了解它,可以查阅。然而,有一些目录确实是您必须了解的。以下是一些比较重要的。
| 目录 | 用途 |
|---|---|
/ |
文件系统的开始;所有目录都位于其下 |
/etc |
系统范围的应用配置 |
/home |
用户主目录 |
/root |
root 用户的主目录(root 用户在 /home 下没有目录) |
/media |
可移动媒体,如闪存驱动器 |
/mnt |
长时间挂载的卷 |
/opt |
额外的软件包(某些程序安装在这里,不太常见) |
/bin |
用户必需的二进制文件(如 ls、cp 等) |
/proc |
用于操作系统级组件的虚拟文件系统 |
/usr/bin |
大多数用户命令 |
/usr/lib |
库文件 |
/var/log |
日志文件 |
/etc目录值得额外讨论,因为你肯定会频繁地使用它。正如前表所述,这个目录包含了应用程序的配置文件,这些配置文件是系统范围内需要被遵守的。例如,如果你在服务器上运行 OpenSSH 守护进程,默认情况下你会监听 22 号端口的连接。(别担心,我们会在后续章节中进一步讨论这些具体概念。)OpenSSH 服务器的配置文件位于/etc/ssh目录中。由于 OpenSSH 是一个作为后台进程在系统上运行的服务,它的配置文件存储在该目录中的sshd_config文件内。如果你删除了提供 OpenSSH 的包,配置文件仍然会保留(默认情况下,删除包并不会删除配置文件),所以如果你稍后重新安装 OpenSSH,下次会有相同的配置。如果我们希望在卸载软件包时同时删除配置文件,可以使用apt remove命令的--purge选项,正如我们在第三章《管理软件包》中所看到的那样。
本书中会随着主题的推进讨论其他重要的目录。如果现在某些内容让你感到困惑,不用担心;随着时间的推移,你会理解的。这里的关键点是,系统中有很多目录,每个目录都有其特定的用途。如果你想了解某个特定目录的用途,可以查阅 FHS。如果你想知道应该在服务器上放置某些文件的位置,同样也可以查阅 FHS。但再强调一次,不必过于担心去查阅本书以外的资料,因为我们会在后续章节中覆盖所有必要的内容。
现在我们对默认的文件系统布局和一些常见目录的用途有了更好的理解。在下一节中,我们将探索如何查看存储在这些目录中的文件内容。
查看文件内容
Linux 文件系统包含许多目录和文件。对于文件,我们需要学习如何读取和操作它们,以完善我们的知识。我们将在下一章中覆盖更多关于文件管理的话题。现在,我们可以通过查看如何查看现有文件的内容来获得一些帮助。
我们可以使用cat命令将文件内容打印到屏幕上,并提供文件名作为参数。例如,以下命令可以用来查看当前工作目录中testfile.txt的内容,这是我们在本章讨论touch命令时创建的文件。虽然这个练习有些无意义,因为该文件为空,但它提供了一个很好的初步示例:
cat testfile.txt
由于文件为空,因此没有输出。那么,让我们看一个更实际的例子。以下是一个我们可以用cat命令查看的文件,它实际上包含了内容:
cat /etc/os-release
该命令的输出如下所示:

图 4.6:查看/etc/os-release 的内容
/etc/os-release文件是许多发行版中都存在的一个文件。它是一个特殊的文件,提供有关当前安装的 Linux 发行版的一些信息。如果你连接到一台 Linux 服务器并想知道它运行的是哪个发行版,查看该文件的内容是一种了解方法。你也可以通过以下命令查看以简化形式呈现的一些相同信息:
lsb_release -a
这个命令在各种发行版上都能工作,但我更喜欢使用/etc/os-release文件,因为它包含更多的信息。无论如何,这个练习的核心目的是演示cat命令允许你查看文件内容。差不多,还有其他命令也能实现同样的功能。我的意思是,你也可以尝试用more或less查看/etc/os-release的内容:
more /etc/os-release
less /etc/os-release
more命令使你更容易查看较大的文件,因此对于像/etc/os-release这样较短的文件,其优势不会立即显现。如果你使用more查看一个较长的文件,它将在填满屏幕后停止输出,并允许你按Enter键继续查看下一行。
less命令允许你做相同的事情,但它还允许你使用箭头键以及Enter键查看更多输出;它还允许你向前或向后滚动。因此,本质上,less命令比more命令提供了更多功能。
此外,我们还可以使用grep命令。它通常不用于仅仅查看文件内容,但它绝对是一个值得了解的好命令,能够帮助你查看你特别想要查看的部分,而不是整个文件。
如果你在首次安装 Ubuntu Server 时选择安装了 OpenSSH 服务器,你应该在安装中包含了它的配置文件,并且可以像平常一样使用cat命令查看该文件的内容:
cat /etc/ssh/sshd_config
当然,这将把该文件的内容打印到屏幕上,而文件的内容远远超过典型显示器一次性能够显示的内容。我们可能对某一特定行或单词感兴趣,因此需要能够将文件缩小到我们实际关心的部分。我们将在第十章《连接网络》中更详细地讨论 OpenSSH 服务器,所以目前不用担心这个配置文件的意义。假设我们只关心 OpenSSH 监听的端口,我们可以使用grep命令来打印出/etc/ssh/sshd_config文件中与此特定配置相关的行:
grep Port /etc/ssh/sshd_config
此命令将输出以下内容:

图 4.7:使用 grep 查看/etc/ssh/sshd_config 文件的内容,以查找包含“Port”的行
本质上,我们所做的是指示grep仅打印出/etc/ssh/sshd_config文件中包含Port字符串的行。在截图中,只有一行包含了该字符串的匹配,因此显示出来。相比于在文件的 123 行文本中滚动查找,看到这个输出无疑更有效,尤其是当我们只关心与端口相关的行时。
默认情况下,grep是区分大小写的。这意味着,如果我们使用grep查找“port”(小写的 P)匹配的行,我们将完全得不到任何输出。我们可以简单地添加-i参数来使搜索不区分大小写。
将grep命令与其他命令(例如cat)搭配使用是非常常见的:
cat /etc/ssh/sshd_config | grep Port
这是一个完全有效的命令,并且会做相同的事情。然而,它有些冗余。我们将在下一章介绍输出重定向的概念,但本质上,这个命令将cat命令的输出重定向作为grep命令的输入。使用cat先打印出文件,然后让grep抓取该文件的内容并搜索字符串,这是一个两步过程,而实际上只需要一步。但话说回来,它仍然是一个有效的命令。
直到今天,我仍然习惯性地使用cat命令配合grep来做同样的事情,因为这就是我刚开始使用 Linux 时,所有新用户被教导的方法。你甚至会在我的 YouTube 视频中看到我这么做——老习惯很难改掉!
日志文件是了解服务器后台发生情况的宝贵信息来源,特别是在故障排除时非常有用。接下来我们将转入关于如何查看这些文件以及与日志相关的命令的讨论。
查看应用程序日志文件
在本章的最后一节,我们来稍微探讨一下日志文件,因为它们包含了我们已经讨论过的一些概念,形成了一个完整的循环。我们回顾了默认目录结构,练习了查看文件,并学习了如何搜索文件中的字符串。稍后我们会更详细地讨论日志文件,但现在我们可以利用这些概念初步了解如何查看日志文件。
如果你还记得,在本章前面讨论 Linux 文件系统布局时,我们展示了一张表格,列出了其中一些最常见的目录。在那张表格中的项目中,我提到了/var/log目录。尽管日志记录方式正在过渡到另一种风格(更多内容会在第二十二章,《Ubuntu 服务器故障排除》中讲到),但我们在/var/log目录中会有一系列日志文件。自己运行一下ls命令,你会看到那里面有相当多的文件。虽然我不会在本章中详细讲解它们,但让我们来看一下/var/log/syslog。
这个文件的内容将包含相当多的行。这是系统日志,用于查看关于 Ubuntu 在你的服务器上运行时的背景信息消息,并且还会显示警告和错误。如果你遇到某个功能不正常的情况,你可能会在系统日志中看到一些输出,提供某种错误信息,你可以通过搜索引擎查找并尝试找到解决方法。例如,如果你的连接出现问题,你可以使用grep查看包含搜索词Network的行:
grep Network /var/log/syslog
这只是一个假设的示例,但它可能显示相关的行。根据你的需求调整搜索词,以便找到你想要的信息。
现在也是介绍head和tail命令的好时机。这些命令分别显示文件的前十行或最后十行。这对/var/log/syslog文件很有用,因为该文件非常大,你可能只对某些行感兴趣。你还可以使用-n选项调整head和tail命令显示的行数,指定你想要的行数。例如,要查看文件的最后100行:
tail -n 100 /var/log/syslog
也许更有用的是-f选项:
tail -f /var/log/syslog
这使得你能够在(几乎)实时中跟踪(查看)文件。使用-f选项时,终端会继续显示文件中新添加的行,因此你可以监控日志文件,举个例子,当某人尝试重现问题时。你可以按下键盘上的Ctrl + c来退出跟踪模式并返回到命令提示符。
当然,还有更多我们可以讲解的基础命令和概念,但我认为现在这些已经足够了。在下一章中,我们将进一步扩展这些内容。但现在,我建议你先练习这一章中的所有概念,直到你熟悉它们为止,再继续前进。
总结
Linux 命令比你能记住的要多。我们大多数人只记住我们喜欢的命令和命令的变体,随着学习和知识的扩展,你将会形成自己的命令菜单。在这一章中,我们介绍了许多基础命令,大部分是必需的。我们探讨了如grep、cat、cd、ls等命令。下一章本质上是这一章的延续,但我想将基础概念分为两章,而不是一章巨大的篇幅。
在下一章中,我们将通过更深入的文件管理知识来扩展我们的基础知识,包括编辑文件、输入/输出流和符号链接,甚至会揭示生活的秘密。嗯,也许不是最后一个,但下一章仍然会很精彩。到时见!
相关视频
- Linux 速成课程 – 浏览 Linux 文件系统(LearnLinuxTV):
linux.video/lcc-navigating
进一步阅读
-
文件系统层次结构标准:
learnlinux.link/fhs-doc
加入我们的 Discord 社区
加入我们的社区 Discord 空间,与作者和其他读者进行讨论:

第五章:管理文件和目录
在 第四章,导航和基本命令 中,我们开始深入了解 Linux 命令。我们回顾了最基本的命令,讲解了文件系统的布局,以及查看文件内容的各种方法。在这一章(以及下一章)中,我们将继续扩展命令行知识,提高使用终端的效率。这一次,我们将进一步讨论文件管理,研究输入/输出流,并了解符号链接。在这个过程中,我们还将涉及:
-
复制、移动和重命名文件和目录
-
使用 Nano 和 Vim 文本编辑器编辑文件
-
输入和输出流
-
使用符号链接和硬链接
让我们从一些方法开始,看看我们如何在文件系统中修改文件和目录,比如复制和移动它们。
复制、移动和重命名文件和目录
此时,你应该知道如何在文件系统中移动(例如 cd 命令)、检查目录内容(ls),甚至如何创建空文件(touch 命令)。我们也知道如何删除文件,例如对文件或目录执行 rm 命令。但直到现在,我们还没有深入了解如何在你的 Ubuntu 文件系统中移动文件。
首先,要复制文件或目录,我们使用 cp 命令。复制文件相对简单,类似于以下的命令:
cp file1 file2
在这个例子中,file2 是 file1 的精确副本。复制文件在许多情况下都很有用,以下是其中一些最常见的情况:
-
将文件复制到备份介质,例如外部硬盘或网络共享
-
在进行更改之前创建文件的副本,比如在编辑一个非常重要的文本文件之前
-
复制日志文件以进行时间点分析
让我们看一下最后一个要点作为另一个很好的例子。我们可以通过执行以下命令捕获系统日志并将其保存在当前工作目录中:
sudo cp /var/log/syslog /home/<username>/syslog
cp 命令相当简单:我们提供一个文件的路径来复制,然后输入目标路径和所需的文件名。由于 syslog 默认情况下可能无法被普通用户读取,我们还需要使用 sudo。在这个命令中,原始的 syslog 文件副本将保存在当前工作目录中。
在这个特定的例子中,日志文件不断地被写入。如果文件一直在扩展,有时很难排查发生在某个特定时间的问题。 但这并不是唯一的原因。我们当然不希望意外更改日志文件,并冒着污染或丢失重要信息的风险。
上一个命令实际上可以稍微简化一下:
sudo cp /var/log/syslog .
在这个示例中,我们移除了目标路径和名称,并用一个句点替代。这之所以有效,是因为句点表示我们的当前工作目录。这不仅适用于cp命令。实际上,每个目录中都有一个名为句点的目录,它本质上是当前目录的指针。因此,如果你已经进入了希望复制文件的目录,就不需要输入路径。如果你希望文件名和源文件相同,那么也不需要输入名称。
复制文件(以及移动文件,我们接下来将讨论)可能是破坏性的。如果目标路径和名称已经存在,那么目标文件将被覆盖。默认情况下,在目标文件被覆盖之前,你不会看到任何确认提示。和所有命令一样,请小心你所指示命令行解释器执行的操作。
当涉及到复制目录时,单独使用cp命令是无法完成的:
sudo cp /var/log/apt .
/var/log/apt目录包含记录通过apt命令执行的事务的日志文件。保持对其他管理员所安装内容的监控是很有用的。然而,在这个示例中,之前的命令会因为以下错误而失败:
cp: -r not specified; omitting directory '/var/log/apt'
错误信息会明确告诉你如何处理,它基本上是在告诉你目录默认情况下是被忽略的。为了复制目录,你需要加上-r选项。-r代表递归,这是许多 Linux 命令可能会使用的选项。它告诉命令行解释器,不仅要抓取你指定路径的对象,还要递归地包含子对象。因此,以下命令将有效:
sudo cp -r /var/log/apt .
使用该命令时,/var/log/apt目录及其内容将会存储在当前工作目录中。
当涉及到将文件或目录从一个地方移动到另一个地方时,我们使用mv命令。其语法几乎与cp命令完全相同。不同之处在于,我们不是复制文件或目录,而是移动它。就这点而言,它的工作原理可能是不言自明的。考虑以下示例命令:
mv file1 /path/to/new/directory/file1
mv file1 file2
在第一个示例中,我们假设file1位于当前工作目录中。我们将该文件抓取并移动到/path/to/new/directory,并在新目录中使用相同的文件名file1。就像使用cp命令一样,我们可以省略目标文件名,因为它保持不变。如果目标目录中已经存在同名文件,它将被覆盖。因此,mv命令也是潜在的破坏性命令,但相比于cp命令,其破坏性更大,因为你是在移动文件,而不是复制。
第二个mv命令稍微有点不同,因为在这个例子中我们正在重命名一个文件。在 Linux 中,没有专门的重命名命令,因此mv命令被用来完成这个任务。事实上,mv可以算是一把瑞士军刀,因为它具有多种功能。通过它,你可以移动文件或目录、重命名文件,甚至将一个文件移动到另一个位置以覆盖另一个文件。具体操作取决于源路径和目标路径。如果目标文件存在,它将被覆盖;如果目标文件不存在,文件将被重命名和/或移动到该路径。
在管理文件时,你肯定会遇到需要管理多个文件或目录的情况。值得注意的是,cp命令和mv命令都可以一次处理多个对象;例如,如果你有三个目录,比如dir1、dir2和dir3,并且需要将它们移动到一个新的子目录中。你可以执行三条mv命令分别移动每个目录,但你也可以用一条mv命令一次性将三个目录全部移动:
mv dir1 dir2 dir3 /path/to/new/location
cp和文件的情况相同;cp和mv命令都可以让你通过一个命令来移动或复制多个目录或文件。
现在我们知道如何移动文件了,接下来我们也应该了解如何编辑文件。在 Ubuntu 平台上,我们有许多可用的文本编辑器,其中 Nano 和 Vim 是最常用的。接下来的部分,我们将介绍这两种编辑器的基本操作。
使用 Nano 和 Vim 文本编辑器编辑文件
现在我们知道如何复制和移动文件了,接下来了解如何编辑文件会非常有用。Ubuntu 提供了多种文本编辑器,其中一些在命令行中可用,另一些则是在图形环境下使用的,比如桌面版的文本编辑器。
有些人可能会觉得命令行文本编辑器比图形用户界面(GUI)编辑器更复杂(老实说,确实可能如此),但它的主要优点是,无论你是否有 GUI,你都可以使用相同的编辑器。从某种意义上来说,这意味着非图形编辑器更具便携性,且更加可靠。几乎所有的 Ubuntu 安装都包括nano文本编辑器,你可以更多地依赖它,而不必担心某个特定的 GUI 编辑器是否可用。此外,vim编辑器也是另一个流行的选择。它比nano更为高级,但在我看来,功能也更强大。在接下来的部分,我们将一起学习nano和vim的使用。
使用 Nano 编辑器
nano编辑器虽然在功能上较为基础,但依然有相当多的用户群体。事实上,如果你还没有注意到,选择编辑器其实在 Linux 社区中能引发一场小小的辩论。启动nano编辑器非常简单,只需运行nano命令。如果你不提供文件名,nano将会启动一个空白的窗口,如下图所示:

图 5.1:nano文本编辑器,未选择文件
你可以马上开始输入,当你想保存文件时,只需按Ctrl + o即可。在nano屏幕的底部,你会看到快捷键的概述,包括我刚提到的保存文件的方法(它称之为写入文件)。如你所见,Ctrl + x是退出编辑器并返回命令行的方法。
我喜欢的一个技巧是(它也适用于vim),当你在使用nano时,按住Ctrl,然后按t再按z,这会让它消失。通常,你不需要按Ctrl和t来将进程放入后台,但在nano的情况下是个例外。
实际上,这就像是将其最小化。你可以通过执行fg来将nano窗口恢复回来,fg是foreground的缩写。能够在终端中将应用程序放入后台或恢复到前台是进程管理的一部分,实际上这是我们将在第七章,控制与管理进程中讨论的内容,但我们提前给你这个小技巧,帮助你顺利前进!在编辑文件时,有时将编辑器发送到后台然后稍后再回来是很有意义的。
如你所料,你也可以使用nano命令,并提供目标路径和文件名,使编辑器直接打开一个已打开的文件。例如:
sudo nano /var/log/syslog
这个命令会导致文件在nano中打开:

图 5.2:nano文本编辑器,打开了 syslog 文件
公平地说,直接编辑日志文件在实践中可能不是个好主意,但这个例子是有效的。syslog是你可能想要打开并检查的文件,但最好先通过ls -lh /var/log/syslog命令检查syslog文件的大小,确保它不是过大,否则会拖慢服务器速度。
抛开这些不谈,重点是你可以通过直接提供文件路径来用nano打开文件。在编辑器窗口内,你可以像在图形化编辑器中一样使用箭头键进行移动。你还可以通过按Ctrl + w来搜索特定的文本字符串,这在底部的操作列表中有提到。
此外,注意截图中以红色高亮显示的文字。它只是告诉我们我们没有权限编辑该文件。在那个示例中,我没有使用sudo,因为我并不打算对文件进行任何更改。
有时,我会在一个没有写权限的编辑器中打开文件,这样可以防止我不小心进行更改(如果没有sudo,我将无法保存文件)。要进行真正的更改,我可以关闭文件,然后使用sudo重新打开它。
无论如何,nano 编辑器相当简洁。它除了这些还有更多功能吗?当然有,但最重要的是了解如何打开文件、编辑文件并保存文件,这些我们在本节中已经讲解过了。当然,你可以通过窗口底部提供的操作来练习,超越正常使用。
现在,让我们来看一下 Vim。
使用 Vim 编辑
Vim 是我最喜欢的编辑器,也是我使用最频繁的编辑器。它稍微有些进阶,但并不会让人感到沮丧。默认情况下,它并没有安装。就个人而言,我更喜欢安装 vim-nox 包,可以通过执行以下命令进行安装:
sudo apt install vim-nox
你安装哪个版本的 vim 其实并不重要。每个版本都会添加一些自己的功能。以 vim-nox 为例,它内置了对脚本语言的支持,但在其他方面与标准的 vim 并没有太大区别。你在这里学到的概念并不限于这个版本。就像 nano 一样,你可以通过输入 vim 命令来调用 vim 编辑器,不带任何选项,或者带上文件路径,比如 vim /home/myuser/myfile.txt。如果没有选择文件,vim 会显示默认的帮助文本。它会给出一些默认命令,比如 :q 用于退出编辑器:

图 5.3:vim 文本编辑器,未打开文件
启动 vim 时,你会进入 命令模式,这是编辑器的多种模式之一。在命令模式下,你不能直接编辑文本。正如其名,命令模式允许你运行命令,这样可以以非常巧妙的方式操作文本。
如果你想像在 nano 中那样编辑文件,你需要切换到 插入模式。你可以通过按键盘上的 Insert 键,或 i 键来切换到插入模式。一旦进入插入模式,你可以像在任何其他编辑器中那样开始输入文本。你可以用方向键移动光标,并在任何地方插入文本。除了插入模式之外,你也可以使用 h、j、k 和 l 键代替方向键来进行导航(有些用户实际上更喜欢这种方式)。要退出插入模式,你可以按 Esc 键。那样就会回到命令模式。
一开始,vim 的不同模式可能会让新手有些困惑。对我来说,它有点像一种超能力。使用 vim,你有一个专门用于编辑文本的模式,还有一个专门用于操作文本的模式。谈到文本操作,这也是 vim 最强大的功能之一。比如,考虑以下这个假设文件:

图 5.4:vim 文本编辑器,显示一个示例文件
我不知道为什么,但总觉得这个文件有些不对劲。我们需要做一个非常重要的修改来尝试修复它。首先,你可以从截图中看到编辑器停留在了插入模式。要进行文本操作,我们需要先按 Esc 键返回到命令模式。一旦进入命令模式,我们就可以准备输入一个命令。我们要输入的命令是:
:%s/Windows/Linux/g
我不知道你怎么想,但就我个人而言,我认为在执行了那个命令后,文件看起来更好了:

图 5.5:运行示例命令后的 vim 文本编辑器
从技术上讲,Linux 是一个内核,而不是操作系统。Ubuntu 作为 Linux 的一个发行版,是我们最接近的操作系统。但暂且不提这一点。在我看来,这个版本的文件更准确!
这是一个更高级的 vim 使用示例,通常超出了本书的范围。我想给你展示这个强大编辑器的一个例子,查找和替换文本只是我们可以用它做的众多操作之一。有些人(包括我自己)甚至安装插件,把 vim 转变成 集成开发环境 (IDE)。在我们刚才使用的例子中,我们能够将所有出现的字符串替换成另一个字符串。这也是我们在命令模式下执行的任务之一,命令模式允许我们操控文本并输入更高级的命令。通过这个尽管有些牵强的例子,你可以立刻看到 vim 多模式的价值。
我已经使用 vim 大约十年了,因为我似乎永远也弄不明白如何退出它。好吧,公平地说,这个笑话现在有点过时了。如果你已经使用 Ubuntu 或其他 Linux 发行版一段时间了,可能会对这个笑话有些许反感。如果这是你第一次看到这个笑话,那我很高兴是我向你介绍了它——因为某种原因,vim 有个很难退出的声誉。但其实并不是真的,它其实非常简单。在命令模式下,你只需输入 :q 即可退出编辑器:
:q
如果你进行了更改,:q 命令将无法退出,但你可以通过添加感叹号强制退出,命令变成 :q!。如果你还想在退出时保存更改,可以加上 w,那么在 vim 中的命令就变成了:
:wq
本质上,我们是在同时退出编辑器(q)并写入文件(w)。在命令模式下,命令以冒号(:)开头,后跟实际命令。由于有太多命令可以使用,所以不可能在一章中全部讲完。那么我会提到一些我认为最重要的命令。
首先,实际上可以通过使用以下命令,在不退出 vim 的情况下运行一个 shell 命令:
:! <shell command>
感叹号允许你运行一个命令,然后输入实际的命令。例如:
:! ls -l /var/log
上一个示例会显示/var/log目录的内容,然后你可以按Enter键返回到vim。:sp命令是split的简写:
:sp /path/to/file
在这种情况下,vim能够在同一窗口中同时显示多个文件,通过有效地分割窗口来展示这两个文件。使用:split,或者简写为:sp(两者作用相同),它将把文件分成两个视图(每个视图中显示相同的文件),或者如果你提供了文件名,它会在另一个分割视图中显示一个独立的文件。这个命令会水平分割文件:

图 5.6:vim 文本编辑器,两个文件在同一窗口中打开
前面的截图显示了两个文件正在打开,/var/log/apt/history.log和/var/log/syslog。你可能还注意到,每个文件底部的状态栏显示了RO。正如你所猜测的,这表示只读,它显示在屏幕上是因为我正在查看只有root用户才能更改的文件,并且我没有使用sudo。
要在两个文件之间切换,我们可以按住Ctrl键的同时,连续按两次w键。这样,我们的插入点就会从一个分割窗口移动到另一个分割窗口。要退出每个独立的缓冲区(或者如果我们只打开了一个文件,就是退出编辑器本身),我们可以按Esc键返回到命令模式,然后输入:q或:q!命令退出而不保存更改,或者输入:wq命令退出并保存更改,就像以前一样。
:split的替代命令是:vsplit,或者简写为:vs。它与:split的作用相同,但会垂直分割窗口。考虑到现在大多数计算机显示器都是宽屏(甚至是超宽屏),垂直分割通常在实际使用中更加方便:

图 5.7:vim 文本编辑器,两个文件在垂直分割的窗口中打开
说实话,前面的截图效果看起来并不是特别好,原因在于书本中的一页宽度不足以真正展示这种分割的好处。你可以试试看,亲自体验一下。
到目前为止,我们已经介绍了vim的两种模式——命令模式和插入模式。但还有一种我们尚未讨论的常用模式——可视模式。可视模式允许你选择文本,然后执行如复制和粘贴等任务。
要做到这一点,确保你处于命令模式,然后将光标移动到你想要复制的文本的第一个字符。接着,按下键盘上的v键,并用箭头键移动选择区域。
你会发现,随着光标的移动,你会选中更多的文本。当你选中想要复制的所有文本后,按 y 键,选中的部分会消失。此时,你就相当于将选中的文本复制到了 vim 相当于剪贴板的地方,类似于桌面操作系统的剪贴板。然后,你可以(仍然在命令模式下)按 p 键,在光标所在位置粘贴文本。可能需要几次尝试才能掌握这个工作流程,但可以试试。如果你犯了错误,你可以在命令模式下按 u 键来 撤销,这样可以恢复任何更改。
再次强调,vim 是一个相对高级的编辑器。学习其基本功能相对简单,因此掌握如何打开、编辑、关闭和保存文件是你可以很快学会的。vim 的功能集非常庞大,尽管我已经使用它多年,我依然在不断学习新技巧。不过,为了确保你掌握基本操作,我们可以总结一下 vim 的基本工作流程,帮助你记忆。大致流程如下:
-
打开 Vim 可以通过单独输入
vim,或者指定文件名:vim <filename>。 -
你从 命令模式 开始。这个模式非常适合运行
vim命令。按 Insert 或 i 键可以切换到 插入模式。 -
在插入模式下,你可以通过键盘输入文本并用箭头键移动光标来编辑文本。
-
当你编辑完文件后,按 Esc 键返回命令模式。
-
如果想在不保存更改的情况下退出,可以输入
:q或:q!命令。如果你确实想保存更改,可以单独输入:w保存并保持在编辑器中,或者输入:wq同时保存并退出vim。
关于 vim 可以写整本书(实际上也有这样的书),所以本书不可能详细讲解所有内容。事实上,如果你有兴趣了解更多,我还专门制作了一系列关于 vim 的视频教程,地址是 www.learnlinux.tv。但现在,我会给你一些我认为你应该知道的有用技巧。
在命令模式下,你可以按 x 键删除光标所在位置的一个字符。你也可以连续按 d 键 两次 来删除整行。当你这么做时,这一行内容会被复制到粘贴缓冲区;你可以按 p 键将这一行粘贴出来。
此外,我曾多次提到,你可以通过按 i 或 Insert 键切换到插入模式。你也可以通过按 a 键(无论是否按住 Shift)来进入插入模式。如果不按 Shift,你将从光标右侧开始插入一个字符。对我来说,这个操作并不是很有用,但我发现自己经常输入大写的 A,它可以让你进入插入模式并将光标移至当前行的末尾。由于我常常需要在句子的末尾开始输入,这对我来说非常实用。
此外,在命令模式下,你可以按Shift + g立即跳转到文件的最后一行。或者,你也可以连续按两次g键跳转到文件的顶部。
另一个我喜欢的小技巧是启用行号。这在某些情况下非常有用,尤其是当日志文件中的错误消息提到与特定行相关的文件时。以下命令可以启用行号:
:set number
以下命令禁用行号:
:set nonumber
如果你希望默认情况下始终看到行号,可以编辑你主目录下的.vimrc文件。每次启动vim时,vim都会读取该文件。
请注意,.vimrc文件以点(.)开头,这意味着该文件是隐藏的。通常情况下,当你使用ls命令列出目录内容时,隐藏文件不会显示。使用ls命令的-a选项可以显示所有文件,包括那些通常隐藏的文件。
现在可以打开编辑器中的.vimrc文件:
vim ~/.vimrc
对你来说,这很可能是一个空文件,因为默认情况下.vimrc文件并不存在。我们可以在文件中添加许多命令来调整vim编辑器的行为,内容繁多,无法在一章中一一列举。不过,以下这一行非常有用,它确保了行号默认启用:
set number
在这一行前省略冒号是故意的;在.vimrc文件中并不需要冒号。从此以后,每次你打开新的vim会话时,行号将会默认启用。当然,你仍然可以像之前一样,通过vim中的:set nonumber命令来禁用行号。此外,你还可以通过.vimrc文件为vim添加许多其他自定义设置,但这里无法一一列举。目前,只需要知道,使用配置文件来定制vim是完全可行的。
现在我们知道了如何编辑文件,我们也应该更加深入地了解流,流使我们能够以多种方式操作输入和输出。
输入输出流
在我们对 Ubuntu Server 的探索过程中,我们已经在终端中做了不少工作。我们能够查看文件内容、向文件中插入文本等等。其实,在整个过程中,我们一直在使用流,只是没有意识到而已。在这一部分,我们将更详细地讨论这个话题。
如果你曾学习过计算机科学,你可能已经知道,输出指的是从计算机打印出来的内容(例如,文本打印到屏幕上,或从打印机打印到纸上),而输入则指的是输入到计算机中的数据,无论是在命令行、文件中还是其他地方。
Linux 将这个概念延伸得更远。在 Linux 中,流(streams)指的是一种处理输入和输出的特殊方式,除了输入流和输出流,我们还有第三种流,专门用于处理错误。
在 Linux 中,输出流被称为标准输出,而输入流被称为标准输入。这两个术语分别简写为stdout和stdin。将输入/输出这一简单概念扩展成一个独立概念的原因是,在 Linux 的 shell 中,我们可以以不同的方式处理这些流,并且可以对它们执行不同的任务。
到目前为止,我们在整本书中一直在处理标准输出。所有打印到终端的内容都是标准输出。例如,当你之前运行了sudo apt install vim-nox命令时,那条命令的结果(显示包安装状态的文字信息)就是标准输出。当你使用cat /var/log/syslog命令将/var/log/syslog的内容显示在屏幕上时,显示的内容就是标准输出。标准输出是你最常接触到的部分。
为了更好地理解标准输出的概念,让我们来看看重定向。以下命令就是一个例子:
cat /var/log/syslog > ~/logfile.txt
通过该命令,我们使用cat来显示/var/log/syslog的内容。但与其仅仅在屏幕上显示内容,我们使用>符号将标准输出重定向到一个文件~/logfile.txt。这意味着标准输出(显示文件内容)将完全不再显示,因为我们已经将其重定向到了文件中。类似地,我们也可以运行:
cat /var/log/syslog 1> ~/logfile.txt
请注意,我在大于符号前添加了一个1。标准输出通过文件描述符 1 来指定。因此,通过这个命令,我特别指定了我要将标准输出,仅标准输出,重定向到文件中。标准输出是默认的,因此我没有必要包括1。这就是为什么简单地使用大于符号就可以将标准输出重定向到文件的原因。
如果你想追加文件而不是完全覆盖它,可以使用两个大于符号(>>)来追加,而不是覆盖。例如,以下命令会将syslog文件的内容追加到logfile.txt文件的末尾,而不是覆盖整个文件:
cat /var/log/syslog >> ~/logfile.txt
标准输入也有一个文件描述符,即 0。标准输入是命令接收数据的方式。本质上,接受用户输入的命令通过接受stdin来获取数据。标准输入在例子中有点难以展示,但以下方式可以实现:
cat /var/log/syslog | grep -i <keyword>
使用这个命令,我正在获取 /var/log/syslog 文件的内容,并将其传递给 grep 命令,这样可以只显示包含特定术语的行。每个 syslog 文件都不会相同,因为每个服务器都可以有不同的配置。但如果你正在调查某个特定的应用或服务,你可以使用 grep 在日志中查找你认为合适的关键词。如果你加上 -i 选项,grep 将执行不区分大小写的搜索。在这个例子中,cat 命令的输出成为了 grep 命令的标准输入。这个命令可以用任何你选择的搜索词来执行。
我也可以运行:
cat < /var/log/syslog | grep Network
在这个例子中,我使用 小于号 符号将 /var/log/syslog 文件的内容重定向为 cat 命令的标准输入。cat 命令通常将传给它的文本打印到屏幕上作为标准输出,但在这里,我使用管道符号 | 来获取该输出,并将其作为标准输入传递给 grep 命令。
这个概念一开始可能有些令人困惑,但如果你持续练习,它一定会变得清晰。让我们再看一个例子,这样我们也能理解标准错误(stderr):
find / -name "syslog"
find 命令允许你查找符合特定条件的文件,例如在这个例子中查找名为 syslog 的文件。这里,我们正在搜索整个文件系统,因为我们从 / 开始了搜索。问题是我没有权限读取文件系统中的所有文件,并且没有使用 sudo。这将导致屏幕上出现许多错误,例如这些:
find: '/var/lib/netdata/health': Permission denied
find: '/var/lib/netdata/registry': Permission denied
find: '/var/lib/netdata/cloud.d': Permission denied
find: '/var/lib/udisks2': Permission denied
由于 find 命令被用来搜索整个文件系统,包括我没有权限查看的地方,我们的终端将被错误信息淹没。这些错误通过标准错误显示,标准错误的文件描述符是 2。如果我们想隐藏这些错误,可以这样做:
find / -name "syslog" 2> /dev/null
使用这个命令时,运行时不会显示任何错误。这是因为我们指示解释器捕获标准错误并将其重定向到 /dev/null。/dev/null 是一个特殊的设备,任何放到那里东西都会永远消失。如果你将某物移到或重定向到那里,它基本上就被删除了。由于标准错误的文件描述符是 2,我们将其与 大于号 符号结合形成 2>,这基本上指示 shell 执行重定向,但只重定向标准错误,保持标准输出不变。我们还可以选择在单个命令中将多个流重定向到不同的地方:
find / -name "syslog" 1> stdout.txt 2> stderr.txt
在这个变体中,我将成功的输出重定向到 stdout.txt,而错误信息则重定向到 stderr.txt。这样我们就可以完全控制成功和失败信息的输出位置。这在故障排除时非常有帮助,因为我们可能只想专注于错误信息,去掉成功的输出可以减少我们需要查看的行数。
我建议你尽量多练习这个概念;这绝对是你希望牢记的内容。你不必现在就完全掌握这个概念,但了解基本知识会为你提供一个很好的基础。
接下来,我们来讨论文件管理的另一个方面——链接。有时,你需要将一个东西链接到另一个东西,并且有几种方法可以实现这一点,同时也有一些最佳实践建议。
使用符号链接和硬链接
如果你已经使用图形操作系统超过一周,那么你可能对快捷方式这个概念非常熟悉。无论是在桌面上还是在菜单中,你都会看到指向文件和应用程序的快捷方式。这些快捷方式可能指向你的主页或个人目录、一个应用程序、单个文件等等。Linux 中也有相同的概念。
使用 Linux,我们可以将文件链接到其他文件,这使我们能够创建自己的快捷方式,这实际上类似于图形操作系统中的快捷方式,但不需要 GUI。这些快捷方式以 符号链接和硬链接 的形式存在,它们是两种不同的链接方式。符号链接和硬链接非常相似,但要解释它们,你首先需要理解 inode 的概念。
inode 是一个数据对象,包含有关文件系统中文件的元数据。尽管对 inode 概念的完整讲解会相当长,但可以将 inode 看作一种数据库对象,存储了你磁盘上实际存储项目的元数据。inode 中存储的信息包括文件的所有者、权限、最后修改日期和类型(是目录还是文件)等详细信息。inode 通过一个整数表示,你可以使用 ls 命令的 -i 选项查看。在我的系统中,我创建了两个文件:file1 和 file2。这两个文件分别是 inode 265416 和 266112。你可以在以下截图中看到运行 ls -i 命令的输出。这些信息很快就会派上用场:

图 5.8:ls -i 的输出
正如我之前提到的,Linux 中有两种类型的链接:符号链接和硬链接。虽然这两种链接的实现方式不同,但它们的目的几乎相同。基本上,链接允许我们在文件系统的其他地方引用某个文件。
举个实际的例子,让我们创建一个硬链接。在我的例子中,我在一个 test 目录下有几个文件,因此我可以为它们中的任何一个创建链接。要创建链接,我们将使用 ln 命令:
ln file1 file3
这里,我正在创建一个指向 file1 的硬链接(file3)。为了实验,你可以尝试在系统上创建一个文件的链接。如果我们再次使用 ls 命令并加上 -i 选项,我们会看到一些有趣的事情:

图 5.9:第二次 ls -I 命令的输出
如果仔细观察输出,你会发现file1和file3具有相同的 inode 编号。从本质上讲,硬链接是一个重复的目录项,两个目录项都指向相同的数据。在这种情况下,我们创建了一个指向另一个文件的硬链接。创建了这个硬链接后,我们可以将file3移动到文件系统中的另一个位置,它仍然会指向file1。然而,硬链接有几个限制。首先,你不能创建指向目录的硬链接,只能是文件。其次,硬链接不能被移动到不同的文件系统。这是有道理的,因为每个文件系统都有自己的 inode。在我的系统上,inode 265416当然不会指向另一个系统上的同一文件,假设该 inode 编号在另一个系统上存在的话。
为了克服这些限制,我们可以考虑使用软链接。软链接(也叫做符号链接或符号连接)是一个指向另一个目录或文件的条目。这与硬链接不同,因为硬链接是一个引用 inode 的重复条目,而符号链接引用的是特定的路径。符号链接不仅可以在不同的文件系统之间移动,我们还可以创建指向目录的符号链接。为了说明符号链接是如何工作的,我们来创建一个。在我的情况下,我将删除file3并将其重新创建为符号链接。我们将再次使用ln命令:
rm file3
ln -s file1 file3
使用ln的-s选项,我正在创建一个符号链接。首先,我通过rm命令删除了原始的硬链接(这不会影响原始文件file1),然后创建了一个名为file3的符号链接。如果我们再次使用ls -i,会发现file3的 inode 编号与file1不同:

图 5.10:创建符号链接后使用ls -i命令的输出
请注意,每个文件的 inode 编号都是不同的。此时,与硬链接相比,主要的区别应该变得显而易见。符号链接并不是原始文件的克隆;它只是指向原始文件路径的一个指针。你对file3执行的任何命令,实际上都是针对符号链接指向的目标执行的。而硬链接则直接指向文件。
在实践中,符号链接在服务器管理中非常有用。然而,重要的是不要疯狂地在整个文件系统中创建大量的符号链接。如果你是服务器上唯一的管理员,这自然不是问题,但如果你辞职,别人接替你的职位,他们会为弄清楚你所有的符号链接并映射它们指向的位置而头疼。当然,你可以为你的符号链接创建文档,但那样你就得追踪它们并不断更新文档。我的建议是,只有在没有其他选项时,或者这样做对你的组织有利并简化文件布局时,才创建符号链接。
回到符号链接与硬链接的话题,你可能在想应该使用哪一种以及何时使用它们。硬链接的主要好处是你可以将任一文件(链接或原始文件)移动到同一文件系统的任何位置,链接都不会断开。然而,符号链接并非如此;如果你移动原始文件,符号链接将指向一个在该位置已不存在的文件。硬链接基本上是指向同一对象的重复条目,因此它们有相同的 inode 号码,因此两者将具有相同的文件大小和内容。符号链接是指向文件路径的指针——仅此而已。
然而,尽管我刚才谈到了硬链接的几个好处,我实际上推荐在大多数使用场景中使用符号链接。符号链接可以跨文件系统,可以链接到目录,并且从输出中更容易判断它们指向哪里。如果你移动硬链接,可能会忘记它们最初的位置或哪个文件实际上指向哪个。没错,使用几个命令你可以轻松找到并映射它们。但总的来说,从长远来看,符号链接更为方便。只要你在移动原始文件时留意重新创建符号链接(并且仅在需要时使用它们),你就不会遇到问题。
总结
在这一章中,我们将终端操作技巧提升到一个新水平,并探讨了移动和复制文件等概念。接着,我们讨论了两个流行的文本编辑器,Nano 和 Vim。然后,我们深入探讨了流的概念,并以理解符号链接和硬链接的区别以及如何创建它们作为本章的结束。
在第六章,提升你的命令行效率中,我们将深入探讨一些命令行技巧,包括讨论 Bash 历史、编写基础脚本等内容。
相关视频
-
Linux Crash Course – 数据流(LearnLinuxTV):
linux.video/se-data-streams -
Linux Crash Course – nano(LearnLinuxTV):
linux.video/le-nano -
Linux Crash Course – 符号链接(LearnLinuxTV):
linux.video/se-symlinks -
Vim 文本编辑器教程系列:
learnlinux.link/vim
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者及其他读者进行讨论:

第六章:提高命令行效率
在本书中,我们一直在大量使用命令行。通过使用 shell,我们安装了软件包、创建了用户、编辑了配置文件等等。在上一章中,我们探讨了文件管理,以进一步提升我们的终端技能。这一章,我们将专门用一整章的内容讲解 shell,目标是提高我们的效率。在这里,我们将利用已经掌握的知识,加入一些有用的节省时间的技巧,介绍循环、变量等内容,甚至还会编写脚本。
在这一章中,我们将涵盖以下主题:
-
理解 Linux shell
-
理解 Bash 历史
-
学习一些有用的命令行技巧
-
理解变量
-
编写简单脚本
-
将所有内容结合起来:编写
rsync备份脚本
让我们从进一步讨论 Linux shell 开始,这将帮助我们更好地理解在输入命令时与服务器的交互方式。
理解 Linux shell
当谈到 Linux shell 时,理解这个术语的真正含义非常重要。我们在本书中反复使用命令行,但至今我们还没有正式讨论过实际输入命令的接口。
本质上,我们一直在通过一个命令解释器输入命令,这个命令解释器被称为Bourne Again Shell,简称Bash。Bash 只是你可以用来输入命令的许多不同shell之一。
还有其他选择,包括Zsh、Fish和ksh,但是 Bash 是大多数 Linux 发行版的默认命令 shell。它甚至可以在 macOS 上使用(尽管在该平台上默认的是 Zsh),通过安装 Windows Subsystem for Linux(WSL)还可以在 Windows 上使用。因此,通过理解 Bash 的基础知识,你的知识将与其他发行版和平台兼容。虽然学习其他 shell(如 Zsh)也很有趣,但如果你刚开始使用,Bash 无疑是最值得集中精力的。
你可能会想知道,那么,在哪儿配置你的用户账户使用的 shell。回想一下第二章《管理用户和权限》,我们查看过 /etc/passwd 文件。相信你还记得,这个文件保存了系统上所有用户账户的列表。你可以通过输入以下命令,查看这个文件来刷新一下记忆:
cat /etc/passwd
这将产生类似图 6.1中所示的输出:

图 6.1:示例 /etc/passwd 文件的最后几行
你看到每个条目的最后一个字段了吗?那是我们配置用户登录或启动新终端会话时启动哪个 shell 的地方。除非你已经更改过,否则你账户的条目应该是 /bin/bash。
你会在这个文件中看到其他变体,例如/bin/false或/usr/sbin/nologin。这些实际上是无效的 shell,当某个用户的默认 shell 设置为这些时,就会阻止该用户登录系统。虽然设置一个阻止登录的 shell 看起来很奇怪,但这种做法其实相当常见——并不是所有的用户账户都需要登录服务器。
系统账户与普通用户账户并存,这些账户是为后台工作创建的。系统用户不需要实际登录系统来执行工作,因此通常会进一步将系统用户的 shell 设置为无效的 shell,这样即使该账户被外部威胁者控制,也无法用来登录服务器(账户能做的事越少,就越安全)。
Shell 程序本身负责读取你输入的命令,并让 Linux 内核执行它们。某些 shell,特别是 Bash,具有额外的功能,如history,这些功能对于管理员非常有用。
理解 Bash 历史
说到 history,让我们深入探讨这个概念。默认情况下,Bash 会记录你在会话期间输入的所有命令,这样你就可以在需要时回忆起之前输入的命令。History 还有另一个用途,就是查看其他用户的活动。不过,由于用户可以编辑自己的历史记录来掩盖痕迹,因此这个功能在这个方面并不总是有用的。
如果你曾经按过 shell 中的上下箭头来回忆之前使用过的命令,你可能已经见过 Bash 的历史功能。如果你还不知道可以这么做,试试看吧。你会发现,按上下箭头可以浏览你之前使用过的命令。
另一个技巧是,你也可以在 shell 中直接输入history,并查看之前输入过的命令列表,如图 6.2所示:

图 6.2:history 命令的输出
此时,你可以从这个列表中复制并粘贴一个之前使用过的命令再次执行。实际上,还有一种更简单的方法。你注意到每个命令左侧的数字了吗?我们可以利用这个数字快速回忆起之前使用过的命令。在我的截图中,项566是我运行sudo apt update时的位置。如果我想再次运行相同的命令,只需输入以下命令:
!566
在这种情况下,我只输入了四个字符,就能够回忆起之前使用过的命令,这个命令执行的操作与输入以下内容相同:
sudo apt update
这节省了很多输入,真是太好了,因为我们管理员希望尽可能少打字(除非我们在写书)。
让我们来看一些额外的历史命令。首先,如果我们想从历史记录中删除某个条目,我们可以简单地执行以下命令:
history -d 566
在这个例子中,我们从 Bash 的历史记录中删除了条目563。要删除其他历史记录条目,只需将563替换为我们想要删除的条目的编号。你可能会想,为什么从历史记录中删除某些内容是必要的。这个问题的答案很简单:有时我们会犯错。也许我们打错了什么,而我们不希望某个初级管理员查看历史记录并重新执行一个无效的命令。更糟糕的是,如果我们不小心将密码保存到了历史记录中,那么它将会被所有人看到。我们肯定会想要删除那条记录,这样密码就不会以明文形式保存在历史文件中。一个非常常见的例子是 MySQL 或 MariaDB。当你进入 MySQL 或 MariaDB 的 shell 时,可以使用-p选项并在一行中输入密码。它可能是这样子的:
mariadb -u root -pSuperSecretPassword
这个命令看起来很有用,因为它可以让你通过一个命令以数据库服务器的root用户身份登录。然而,这也是我非常不喜欢的一点——我真的不喜欢有人在命令中直接写明密码。将root密码保存在 shell 历史记录中是一个巨大的安全隐患。这只是你不希望出现在 Bash 历史记录中的一个例子。我在这里的主要目的是提醒你,在输入命令时要考虑安全。如果你的命令历史中有潜在的敏感信息,你应该将其删除。事实上,你实际上可以输入一个命令,但不将它保存在历史记录中。只需在命令前加一个空格。如果你这样做,它将不会被记录在历史文件中。试试看,亲自体验一下吧。
在 Bash 中,前缀为空格的命令被忽略,实际上是 Ubuntu Server 默认启用的自定义选项。并非所有的发行版都默认启用此功能。如果你使用的发行版没有默认启用这个功能,可以将以下内容添加到你的.bashrc文件中(我们稍后会详细讲解这个文件)。
HISTCONTROL=ignoreboth
这一配置行还会导致重复的命令不被写入历史记录文件,从而可以压缩历史记录文件。
那么,你可能会问,这些历史记录信息到底存储在哪里呢?查看一下.bash_history文件,它位于你的主目录中(对于root用户来说是/root目录)。当你退出 shell 时,历史记录会被复制到这个文件中。如果你删除这个文件,实际上就是清空了历史记录。但我不建议你养成删除它的习惯。保留命令历史记录非常有用,特别是当你可能不记得上次是如何解决某个问题的时候。Bash 中的历史记录可以帮助你避免重复查找命令。要了解更多关于history命令的功能,查看相关的 man 页面,命令是man history。
学习使用命令行的新技巧,使我能更高效地工作,这是一个很棒的感觉,至少对我来说是这样。在接下来的部分,我们将探讨一些在使用 shell 时可以利用的有用技巧。
学习一些有用的命令行技巧
利用 shell 提高生产力的技巧是我最喜欢的事情之一,和音乐、电子游戏、健怡可乐一样。没有什么比你发现一个可以节省时间或提高效率的有用功能时的感觉更好了。我在这条路上发现了许多事情,真希望我早些知道它们。我写这本书的目标之一,就是教你我学得比我愿意承认的还要慢的那些东西。在这一部分,按顺序不分先后,我将介绍一些提高我工作流的技巧。
首先,在终端输入 !!(两个感叹号)将重复你上次使用的命令。单独使用这个功能看起来可能没什么特别的。毕竟,你可以按一次上箭头键,然后按 Enter 来回忆上一个命令并执行它。但是,当与 sudo 一起使用时,!! 变得更有趣了。想象一下,你输入了一个需要 root 权限的命令,但忘记加上 sudo。我们都犯过这个错误。实际上,直到我写这本书的时候,我已经用了 20 年的 Linux 了,但我仍然时不时忘记使用 sudo。当我们忘记加 sudo 时,我们必须重新输入命令。或者,我们可以直接这么做:
sudo !!
就这样,你用 sudo 前缀加上了之前使用的命令,而不必重新输入它。
说到避免不必要的输入,一个非常简单(但极其有用)的功能是Tab 完成。通常,Bash shell 可以自动完成你输入的部分命令。如果你开始输入命令或路径的几个字符,按下键盘上的Tab键,如果你输入的字符足以缩小结果范围,shell 会为你完成路径。你也可以连续按两次Tab,以查看与已输入字符匹配的所有可能选项。试试看吧。举个例子,你可以输入 ls 和你主目录的路径,故意漏掉一些字符,然后按下Tab,看看命令是否会自动完成。例如,我可以在终端输入以下内容:
ls /home/j
然后按下 Tab,它就自动完成了命令:
ls /home/jay
此外,还有一些特殊的键盘快捷键可以帮助你更快速地导航命令行。下面是一个包含一些最有用的键盘快捷键的表格:
| 键盘快捷键 | 结果 |
|---|---|
| Ctrl + a | 将光标移动到行首 |
| Ctrl + e | 将光标移动到行尾 |
| Ctrl + l | 清屏 |
| Ctrl + k | 删除光标到行尾的字符 |
| Ctrl + u | 删除当前行输入的所有内容(在输入密码时也能清除文本) |
| Ctrl + w | 删除光标左侧的一个单词 |
进一步探讨命令历史,我们还可以在 shell 中按 Ctrl + r 来启动搜索。按下这两个键后,我们可以开始输入一个命令,并且会看到一个与输入内容匹配的命令预览,随着输入更多字符,这个预览会进一步缩小范围。这是我很难描述的事情,截图也不太有帮助,所以不妨试试看。例如,按下 Ctrl + r 然后开始输入 sudo apt。你上次使用该命令时的记录应该会出现,你可以再次按 Ctrl + r,这样会看到包含这些字符的历史命令的更多实例。当你熟练掌握这一技巧时,它实际上比 history 命令更高效,但这需要一些时间来适应。
另一个有趣的技巧是,在文本编辑器中编辑你之前输入的命令。我知道这听起来很奇怪,但请耐心听我解释。假设你按了上箭头键,输入了一个非常长的命令,而你只想编辑其中的一部分,而不必执行整个命令,比如像这样:
sudo apt update && sudo apt install apache2
假设你想安装 nginx 而不是 apache2,但其余的命令是正确的。如果你按住 Ctrl 然后按 x 再按 e,命令会在文本编辑器中打开。在那里,你可以更改命令。修改完毕后,一旦保存文件,命令就会执行。坦白说,这通常只有在你有非常长的命令,需要更改其中的一部分时才有用。虽然这有点奇怪,但计算机本身也挺奇怪的。
你注意到之前命令中的两个 & 符号了吗?这是另一个有用的技巧;你实际上可以将多个命令链接在一起。在之前的命令示例中,我们告诉 shell 执行 sudo apt update。接着,我们告诉 shell 执行 sudo apt install apache2。双重和号被称为逻辑 AND 运算符,因此第二个命令只有在第一个命令成功时才会执行。如果第一个命令成功,第二个命令会紧接着执行。另一种链接命令的方法是:
sudo apt update; sudo apt install apache2
与分号的区别在于,我们告诉 shell 执行第二个命令,无论第一个命令是否成功。你可能会想,什么才算 shell 上的成功呢?一个显而易见的答案可能是“没有错误信息就是成功”。虽然这通常是对的,但 shell 使用退出码来编程地标识成功或失败。你可以通过在上一个命令完成后立即输入以下命令来查看命令的退出码:
echo $?
0的退出代码表示成功;其他任何代码则表示某种错误。不同的程序会为不同类型的失败分配不同的代码,但0始终表示成功。通过这个命令,我们实际上是在打印一个变量的内容。$?实际上是一个变量,在这种情况下它仅用于存储退出代码。echo命令本身可以用于向终端打印文本,但它常常被用来打印变量的内容(我们将在理解变量部分详细讨论这个)。
现在,到了我最喜欢的节省时间的小技巧——命令别名的时候了。别名的概念很简单:它允许你创建一个命令,实际上是另一个命令的别名。这让你可以将命令简化为一个单词或几个字母。比如,考虑这个命令:
alias install="sudo apt install"
当你输入前面的命令时,实际上不会有任何输出。但发生的事情是你现在拥有了一个新命令——install。这个命令通常是不可用的,你刚刚通过这个命令创建了它。
你可以通过运行alias命令来验证别名是否成功创建,这会显示当前在 shell 中存在的别名列表。如果你创建了新的别名,你应该能在输出中看到它。你还会看到输出中出现你没有创建的其他别名,这是因为 Ubuntu 默认设置了一些别名。事实上,连ls命令本身也是一个别名!
创建这个新的别名后,每次在命令行中执行install时,实际上是在执行sudo apt install。现在,安装软件包变得更简单了:
install tmux
就这样,你安装了tmux。你不需要输入sudo apt install tmux,你只是将命令中的前三个词简化成了install。事实上,你甚至可以将它进一步简化:
alias i='sudo apt install'
现在,你可以使用以下命令安装软件包:
i tmux
使用别名,你可以发挥很大的创造力。这是我个人最喜欢的一些别名。
查看消耗 CPU 最多的前 10 个进程:
alias cpu10='ps -L aux | sort -nr -k 3 | head -10'
查看消耗 RAM 最多的前 10 个进程:
alias mem10='ps -L aux | sort -nr -k 4 | head -10'
查看所有已挂载的文件系统,并以干净的标签页布局展示信息:
alias lsmount='mount | column -t'
通过简单地输入c来清除屏幕:
alias c=clear
你能想出其他的别名吗?想一想你可能经常使用的命令,并简化它。
但是有一个问题,那就是当你退出终端窗口时,你的别名会被清除。如何保留它们呢?这就引出了我的下一个提高效率的小技巧,编辑你的.bashrc文件。这个文件位于你的主目录中,并且每次你启动一个新的终端会话时都会被读取。你可以在其中添加所有的alias命令;只需将它们添加到文件中的某个位置(例如,文件末尾)。你需要包括整个命令,从alias开始,到命令的引号结束。如果你想复制我的示例别名,你可以将以下几行添加到你的.bashrc文件中的某个位置:
alias i='sudo apt install'
alias cpu10='ps -L aux | sort -nr -k 3 | head -10'
alias mem10='ps -L aux | sort -nr -k 4 | head -10'
alias lsmount='mount |column -t'
当然,我们还可以讨论更多节省时间的技巧,但 Bash 的复杂性足以让我们写一本书(而且很多人确实这么做了)。随着本章的进行,我会给你更多的提示。现在,给你一个最后的技巧,它会将你的工作目录切换回你之前所在的目录:
cd -
这个简单的命令在第四章,导航和基本命令中有提到,但值得再提一遍——不客气!接下来,让我们看看 shell 变量,它们允许我们存储信息,以便在其他命令中轻松访问。
理解变量
Bash 不仅仅是一个 shell。你可以说它非常类似于一个完整的编程语言,这样的说法并不错误。Bash 有一个内建的脚本引擎(稍后我们将讨论脚本编写),关于脚本语言和编程语言的区别存在很多争议,随着新语言的出现,这两者之间的界限也越来越模糊。
与任何脚本语言一样,Bash 也支持变量。Bash 中变量的概念非常简单,但我觉得有必要单独(相对简短地)讲解这一部分,以确保你理解基础知识。你可以通过如下命令设置变量:
myvar='Hello world!'
当 Bash 遇到等号后面的字符串时,它认为你正在创建一个变量。在这里,我们创建了一个名为myvar的变量,并将其设置为Hello world!然而,每次引用变量时,我们需要明确告诉 Bash 我们正在请求一个变量,方法是用美元符号($)前缀它。考虑一下这个命令:
echo $myvar
如果你像我一样设置了变量,执行该命令将会将Hello world!打印到stdout。echo命令在打印变量内容时非常有用。这里需要记住的关键是,当你设置一个变量时,不需要加$符号,但在获取变量时需要加上。另外,请记住等号两边不能有空格。
在使用各种 Linux 服务器时,你会看到变量名格式的不同变体。例如,你可能会看到全大写的变量名、驼峰命名法(MyVar)等其他变体。这些变体都是有效的,具体使用哪种形式取决于创建者的背景(开发者、管理员等),因此你可能会看到不同的变量命名方式。内建的变量通常会采用全大写的命名方式,这也非常常见。
变量也可以在 shell 的其他方面使用,而不仅仅是与echo一起。考虑一下这个:
mydir="/etc"
ls $mydir
在这里,我们将一个目录名称存储在变量中,并使用 ls 命令列出其内容。这看起来可能相对无用,但在编写脚本时,这将为你节省时间。任何你需要多次引用的内容,都应该放在变量中。这样,在脚本中,你只需修改一次该变量的内容,脚本中的所有地方都会引用到它。
还有一些在你的 shell 中自动存在的变量,这些是你没有明确设置的。执行以下命令来试试看:
env
哇!你应该能看到很多变量,特别是如果你在 Ubuntu 的桌面版本中输入它的话。
这些变量是由系统设置的,但仍然可以像其他变量一样通过 echo 访问。其中一些值得注意的包括 $SHELL(存储当前处理你的 shell 的二进制文件名)、$USER(存储当前用户名)和 $HOST(存储你设备的主机名)。这些变量可以随时访问,甚至在脚本中也能发挥作用。
我们已经在上一章讲解了标准输出(stdout)、标准错误(stderr)和标准输入(stdin)。我们在这里再次使用标准输入,当我们捕获输入并将其存储为变量时。尝试执行以下命令:
read age
当你运行此命令时,你会被带到一个空白行,没有任何提示说明你应该做什么。继续输入你的年龄,然后按 Enter。接下来,执行这个:
echo $age
在脚本中,你需要告知用户他们应该输入什么,所以你可能会使用类似于以下的命令:
echo "Please enter your age"
read age
echo "Your age is $age"
我们在上一章讨论了标准输入,这里我们可以再次看到它的应用,我们从用户获取输入并将其存储在一个变量中。
自动化是我们将在本书剩余部分多次探讨的主题,其中将包括更多高级主题,如配置管理。编写脚本是最简单的自动化形式,它让你能够将命令写入文本文件并逐一执行。这就是我们接下来要探讨的内容。
编写简单脚本
这是我们到目前为止讨论的所有内容开始汇聚的部分。编写脚本既有趣又有回报,因为它们允许你自动化大任务,或简化那些你反复执行的操作。关于脚本的最重要一点是:如果是你需要做多次的事情,确实应该将其写成脚本。这是一个非常好的习惯。
脚本是一个非常简单的概念;它只是一个包含 shell 要逐一执行命令的文本文件。专门为 Bash 执行编写的脚本被称为 Bash 脚本,这就是我们在本节中将要创建的脚本类型。
目前,我假设你已经在 Linux 上使用过文本编辑器。不管你是使用 Vim 还是 Nano。既然我们之前已经编辑过文本文件(我们在 第五章,文件和目录管理 中讲过),我假设你已经知道如何创建和编辑文件了。我们将使用文本编辑器来创建一个简单的脚本作为示例,使用以下命令:
nano ~/myscript.sh
如果你还不清楚,波浪线(~)只是指代用户主目录的快捷方式。因此,在我的系统上,前面的命令就相当于我输入了:
nano /home/jay/myscript.sh
在文件中,输入以下内容:
#!/bin/bash
echo "My name is $USER"
echo "My home directory is $HOME"
保存文件并退出编辑器。为了将此文件作为脚本运行,我们需要将它标记为可执行:
chmod +x ~/myscript.sh
要执行它,我们只需要调用文件的路径和文件名:
~/myscript.sh
输出应该类似于以下内容:
My name is jay
My home directory is /home/jay
第一行 #!/bin/bash 可能会让你觉得奇怪,如果你以前没见过它。通常,带有井号(#)符号的行会被解释器忽略。但是第一行是一个例外。我们在第一行看到的 #!/bin/bash 叫做 哈希 bang 或 shebang。基本上,它只是告诉内核应该使用哪个解释器来执行脚本中的命令。如果我们写的是 Python 脚本,可能会使用 #!/usr/bin/python。但是因为我们编写的是 Bash 脚本,所以使用了 #!/bin/bash。
后面的几行是简单的打印语句。每一行都使用了系统变量,所以你不需要声明这些变量,因为它们已经存在。在这里,我们打印了当前用户的用户名和主目录。
脚本的概念变得更加有价值,当你开始考虑那些你日常做的事情,你可以用自动化来替代它们时。作为一名有效的 Linux 管理员,采用自动化思维方式非常重要。再说一遍,如果你需要做某项工作超过一次,那就写个脚本吧。这里有另一个示例脚本,帮助你理解这个概念。这一次,这个脚本将会稍微有点用:
#!/bin/bash
sudo apt install -y apache2
sudo apt install -y libapache2-mod-php8.1
sudo a2enmod php8.1
sudo systemctl restart apache2
我们在这里做的,理论上是脚本化了一个 Web 服务器的设置。我们可以进一步扩展这个脚本,让它将网站内容复制到 /var/www/html,启用配置文件等等。但从上面的脚本来看,你可能已经看出,脚本可以帮助你减少工作量。这个脚本可以是一个高级的 Web 服务器安装脚本,你可以将它直接复制到新服务器上然后运行。
请注意,示例中使用了apt的-y选项。如果你之前没有注意到,这会自动回答yes,以应对在过程中可能出现的提示。脚本通常是非交互式的,这意味着可能没有管理员在旁边处理提示问题。另外,使用a2enmod命令启用php8.1实际上并不必要,因为它在安装libapache2-mod-php8.1包时已经会自动启用。但我想你明白我的意思;我们希望在脚本中明确写出,给出我们希望的具体状态。
现在,让我们更深入一点编写脚本。之前的脚本只是安装了一些包,实际上我们可能也可以通过复制和粘贴命令到 shell 中轻松完成。让我们把这个脚本做得更进一步。让我们编写一个条件语句。这是之前脚本的修改版本:
#!/bin/bash
# Install Apache if it's not already present
if [ ! -f /usr/sbin/apache2 ]; then
sudo apt install -y apache2
sudo apt install -y libapache2-mod-php8.1
sudo a2enmod php8.1
sudo systemctl restart apache2
fi
现在它变得有趣起来了。哈希标记之后的第一行是一个注释,告诉我们脚本的作用:
# Install Apache if it's not already present
注释被解释器忽略,但它们有助于让我们知道代码块在做什么。
接下来,我们开始一个if语句:
if [ ! -f /usr/sbin/apache2 ]; then
Bash,像任何脚本语言一样,支持分支,if语句就是实现分支的一种方式。在这里,它正在检查apache2二进制文件是否存在。这里的-f选项表示我们正在查找一个文件。我们可以将其更改为-d来检查目录是否存在。感叹号表示取反。它基本上意味着我们在检查某个东西是否不存在。如果我们想检查某个东西是否存在,我们将省略感叹号。基本上,我们正在设置脚本,如果 Apache 已经安装,就不执行任何操作。在这种情况下,在方括号中我们只是执行一个 shell 命令,然后检查结果。if语句中的命令只是安装包。
最后,我们用if的反向词(fi)来结束我们的if语句。如果你忘记这么做,脚本将会失败。
关于if语句的概念,我们也可以比较值。请看以下例子:
#!/bin/bash
myvar=1
if [ $myvar -eq 1]; then
echo "The variable equals 1"
fi
通过这个脚本,我们只是检查一个变量的内容,并在其等于某个特定数字时采取行动。注意,我们在创建变量时没有使用引号,因为这里只是设置了一个数字(整数)。只有当我们想把变量值设置为字符串时,才会使用引号。如果if语句不匹配,我们也可以采取行动:
#!/bin/bash
myvar=10
if [ $myvar -eq 1]; then
echo "The variable equals 1"
else
echo "The variable doesn't equal 1"
fi
这是一个愚蠢的例子,我知道,但它有助于说明如何在 Bash 中创建if/else逻辑块。if语句检查变量是否等于1。它不是,所以执行else块。
命令中的-eq部分类似于大多数编程语言中的==。它是在检查某个值是否等于某个值。或者,我们可以使用-ne(不等于)、-gt(大于)、-ge(大于或等于)、-lt(小于)等。
在这一点上,我建议你暂停阅读,进一步练习脚本编写(练习是将概念牢记在心的关键)。尝试以下挑战:
-
要求用户输入信息,例如他们的年龄,并将其保存到一个变量中。如果用户输入的数字小于 30,告诉他们他们还年轻。如果数字大于或等于 30,使用
echo打印出一个告诉他们已经老了的语句。 -
编写一个脚本,将一个文件从一个地方复制到另一个地方。让脚本先检查文件是否存在,如果文件不存在,使用
else语句打印错误信息。 -
想想我们在本书中已经学习过的任何主题,并尝试将其自动化。
现在,让我们来看看另一个概念,那就是循环。循环的基本思想就是反复执行某个操作,直到满足某个条件。考虑以下示例脚本:
#!/bin/bash
myvar=1
while [ $myvar -le 15 ]
do
echo $myvar
((myvar++))
done
让我们逐行分析脚本,以理解它在做什么。
myvar=1
通过这个新脚本,我们创建了一个控制变量,名为myvar,并将其设置为1。
while [ $myvar -le 15 ]
接下来,我们设置一个while循环。while循环会持续执行,直到满足某个条件。在这里,我们告诉它重复执行代码块中的语句,直到$myvar等于15。事实上,如果你输入不正确的条件,while循环可能会永远执行下去,这就是所谓的无限循环。无限循环是危险的,可能导致服务器停止响应。如果你用了-ge 0,你就创建了一个这样的循环。
do
使用do,我们告诉for循环准备开始执行某些操作。
echo $myvar
在这里,我们打印出当前$myvar变量的内容——没有什么令人惊讶的地方。
((myvar++))
通过这个语句,我们使用了所谓的增量器来将变量的值增加1。双重括号告诉 shell 我们正在进行一个算术运算,因此解释器不会误认为我们在处理字符串。
done
当我们编写完while循环时,必须用done来关闭代码块。如果你正确输入了脚本,它应该从1计数到15。
另一种类型的循环是for循环。for循环会对集合中的每个项执行一条语句。例如,你可以让for循环对目录中的每个文件执行一个命令。考虑这个例子:
#!/bin/bash
turtles='Donatello Leonardo Michelangelo Raphael'
for t in $turtles
do
echo $t
done
让我们深入探讨一下我们在这里做了什么:
turtles='Donatello Leonardo Michelangelo Raphael'
在这里,我们创建了一个列表并将其填充上名字。每个名字都是列表中的一个项目。我们将这个列表命名为turtles。我们可以像查看任何其他变量一样,使用echo查看该列表的内容:
echo $turtles
接下来,让我们看看如何设置for循环:
for t in $turtles
现在,我们告诉解释器准备对列表中的每一项执行某些操作。这里的t是任意的,我们可以使用任何字母,甚至是更长的字符串。我们只是设置了一个临时变量,用来保存脚本正在处理的当前项。
do
使用do,我们告诉for循环准备开始执行某些操作。
echo $t
现在,我们将当前t的值打印到stdout。
done
就像我们在使用while循环时做的那样,我们输入done来让解释器知道这是for循环的结束。实际上,我们刚刚创建了一个for循环,用来独立地打印列表中的每一项:
Donatello
Leonardo
Michelangelo
Raphael
我们在列表中包含了四个乌龟的名字,我们能够遍历它们并逐个打印出来。
尽管我喜欢乌龟(尤其是忍者神龟),但是这个脚本对于服务器管理来说并不是特别实用或有用。接下来,我们将编写一个实际上非常有用的脚本。
把所有内容放在一起——编写一个 rsync 备份脚本
让我们用一个 Bash 脚本来结束这一章,它不仅非常实用,还能帮助你提高技能。rsync工具是我最喜欢的工具之一;它在将数据从一个地方复制到另一个地方时非常有用,也有助于设置备份任务。让我们使用以下的rsync命令来练习自动化:
rsync -avb --delete --backup-dir=/backup/incremental/08-17-2022 /src /target
这个示例rsync命令使用了-a(归档)选项,它保留了文件的元数据(例如时间戳和所有者)在目标中复制的文件。-v选项提供了详细输出,这样我们可以准确看到rsync正在做什么。-b选项启用了备份模式,这意味着如果目标文件将被源文件覆盖,那么目标文件的旧版本将被重命名以防被覆盖。将这三个选项结合在一起,我们简化为-avb,而不是输入-a -v -b。--delete选项告诉rsync删除目标中不存在于源中的任何文件(由于我们使用了-b,被删除的文件会被保留)。
--backup-dir选项告诉rsync,每当一个文件会被这样重命名(或删除)时,改为将其复制到另一个目录。在这种情况下,我们将所有可能被覆盖的文件发送到/backup/incremental/08-16-2022目录。
让我们编写这个rsync任务的脚本。我们可以立刻修复的一个问题是目录中日期的存在,这个日期是我们在--backup-dir中使用的目录路径的一部分。日期每天都会变化,所以我们不应该将其写死在代码中。因此,让我们从解决这个问题开始脚本:
#/bin/bash
curdate=$(date +%m-%d-%Y)
我们正在创建一个名为curdate的变量。我们将其设置为$(date +%m-%d-%Y)命令的输出。你可以在终端窗口中执行date +%m-%d-%Y来查看它具体做了什么。在这种情况下,将命令(例如date)放在括号中,并在前面加上美元符号意味着我们在子 Shell中执行该命令。命令会运行,我们将捕获该命令的结果并存储在curdate变量中。
接下来,让我们确保rsync已经安装,如果没有安装,则进行安装:
if [ ! -f /usr/bin/rsync ]; then
sudo apt install -y rsync
fi
在这里,我们仅仅是检查是否没有安装rsync。如果没有安装,我们将通过apt进行安装。这与我们在本章前面检查apache2是否存在的方式类似。
现在,我们添加最后一行:
rsync -avb --delete --backup-dir=/backup/incremental/$curdate /src /target
如果你还没有发现,Bash 中的变量魔力现在肯定能让你看得清楚。我们在命令中包含了$curdate,它被设置为当前实际日期。当我们把这些拼凑在一起时,我们的脚本如下所示:
#/bin/bash
curdate=$(date +%m-%d-%Y)
if [ ! -f /usr/bin/rsync ]; then
sudo apt install -y rsync
fi
rsync -avb --delete --backup-dir=/backup/incremental/$curdate /src /target
这个脚本在运行时会执行一个rsync任务,将内容从/src复制到/target。(确保更改这些目录,以匹配你想要备份的源目录和目标复制目录。)这样做的好处是,/target可以是一个外部硬盘或网络共享。因此,简而言之,你可以实现自动化的夜间备份。由于我们使用了-b选项并配合--backup-dir,这个备份将允许你从/backup/incremental目录中恢复文件的先前版本。你可以自由地发挥创意,决定在哪里放置先前的文件版本,以及将备份存储在哪里。
当然,不要忘记将脚本标记为可执行文件,前提是它被保存为像backup.sh这样的文件名:
chmod +x backup.sh
此时,你可以将此脚本放入定时任务中以自动运行。为了做到这一点,最好将脚本放在一个可以找到的中心位置,比如/usr/local/bin:
sudo mv backup.sh /usr/local/bin
你可以考虑为这个脚本创建一个定时任务,使其定期运行。我们将在第七章,控制和监控进程中详细介绍这一点,紧接着就是本章内容。通过定时任务,你可以设置不同的任务在不同时间运行,从而让你的服务器基本上代替你完成工作。
总结
在本章中,我们深入探讨了与 Shell 命令相关的一些更高级的概念,如重定向、Bash 历史记录、命令别名、一些命令行技巧等等。与 Shell 的工作无疑是你将继续提升的技能,所以如果你在记忆这些知识时遇到困难,不必担心。在我从事 Linux 工作超过 20 年的过程中,我仍然在不断学习新的东西。本章的主要收获是为你提供一个起点,拓宽你的命令行技巧,并为未来的探索奠定基础。
在下一章,我们将探讨如何管理进程,包括作业管理、驯服不良进程等内容。我们下次见!
相关视频
-
Linux 入门课程 – Bash 历史(LearnLinuxTV):
linux.video/le-bash-history -
Linux 命令行技巧与窍门(LearnLinuxTV):
linux.video/cli-tips
深入阅读
-
Bash 的比较运算符:
learnlinux.link/c-ops -
Commandlinefu:
learnlinux.link/c-fu -
Bash 参考手册:
learnlinux.link/bash-man
加入我们在 Discord 上的社区
加入我们社区的 Discord 空间,与作者和其他读者讨论:

第七章:控制和管理进程
在典型的 Linux 服务器上,任何时候都可能有超过一百个进程在运行。这些进程的用途从系统服务(例如 网络时间协议(NTP)服务),到为其他进程提供服务的进程(如 Apache web 服务器)不等。作为 Ubuntu 服务器的管理员,你需要能够管理这些进程,以及管理它们所使用的资源。在本章中,我们将探讨进程管理,包括 ps 命令、管理作业控制命令等。
在我们学习这些概念的过程中,我们将涉及以下主题:
-
管理作业
-
理解
ps命令 -
更改进程优先级
-
处理异常进程
-
管理系统进程
-
使用
cron调度任务
为了开始探索进程管理,首先让我们来看一下作业管理。这不仅有助于我们更好地理解这些概念,还能帮助我们更好地理解后台和前台操作。
管理作业
直到现在,我们在 shell 中做的每一件事都直接呈现在我们面前,从执行到完成。我们安装了应用程序,运行了程序,走过了各种命令。每次,我们的 shell 控制权都会被夺走,直到上一个任务完成,我们才能开始下一个任务。例如,如果我们用 apt install 命令安装 vim-nox 包,我们只能眼睁睁看着 apt 为我们获取包并安装它。
在此期间,我们的光标消失,shell 会为我们完成任务,而不会让我们排队执行其他命令。我们可以随时打开一个新的 shell 会话到服务器,利用同时打开两个窗口来进行多任务处理,每个窗口执行不同的任务。但在使用命令行时,这可能不是最有效的多任务处理方式。
相反,我们实际上可以将一个进程放到后台,而不必等待它完成,然后可以将该进程带回前台,继续处理它,或者检查它是否成功完成。可以将这看作类似于窗口化桌面环境或 Windows 或 macOS 操作系统上的用户界面。我们可以在使用某个应用程序时,将它最小化以便不妨碍操作,再将其最大化以继续工作。实际上,这与在 Linux shell 中将进程放到后台的概念是一样的。
那么,究竟如何将进程放入后台或前台呢?这个概念有点难以解释。在我看来,学习新概念最简单的方法是亲自尝试,而我能想到的最简单的例子就是(再次)使用文本编辑器。我保证这次以文本编辑器为例不会无聊。事实上,这个例子非常有用,可能会成为你日常工作流的一部分。为了完成这个练习,你可以使用任何你喜欢的命令行文本编辑器,比如 Vim 或 Nano。在 Ubuntu Server 中,nano通常是默认安装的,因此如果你想使用它,你已经有了。如果你更喜欢使用 Vim,可以随时安装vim-nox包(如果你还没有安装的话):
sudo apt install vim-nox
你实际上可以安装vim而不是vim-nox,但我总是默认安装vim-nox,因为它内置支持脚本语言。
再次说明,随意使用你感到舒服的文本编辑器。在以下的例子中,我将使用nano,但如果你使用vim,只需每次看到nano时都将其替换为vim。
无论如何,为了查看后台操作的实际效果,打开你的文本编辑器。你可以选择打开一个文件,或者只启动一个空白会话。(如果不确定,可以输入nano并按Enter。)文本编辑器打开后,我们可以随时通过按下键盘上的Ctrl + z将其放入后台。
如果你使用vim而不是nano,你只能在非插入模式下将vim放到后台,因为它会捕获Ctrl + z,而不是将其传递给 shell。
你看到了发生了什么吗?你会立刻被带离编辑器,返回到 shell,这样你就可以继续执行命令了。你应该会看到类似以下的输出:
[1]+ Stopped nano
在这里,我们可以看到进程的job编号、其状态以及进程名称。即使文本编辑器的进程显示状态为Stopped,它仍然在运行。你可以通过以下命令确认这一点:
ps au | grep nano
在我的情况下,我看到nano进程正在运行,PID 为43231:
jay 43231 0.0 0.1 5468 3632 pts/0 T 11:27 0:00 nano
此时,我可以执行其他命令,浏览我的文件系统,并完成其他工作。当我想将文本编辑器恢复到前台时,我可以使用fg命令将进程放到前台,这样它就会恢复运行。如果我有多个后台进程,fg命令会将我最近操作的那个进程恢复到前台。
我给你举了一个ps命令的例子,展示进程仍在后台运行,但实际上有一个专门的命令用于此目的,那就是jobs命令。
如果你执行jobs命令,你会看到输出中列出所有后台运行的进程:

图 7.1:在将两个nano进程放入后台后运行jobs命令
输出显示我有两个正在使用的nano会话,一个在修改file1.txt,另一个在修改file2.txt。如果我执行fg命令,它将把编辑file2.txt的nano会话调回前台,因为那是我最近在使用的会话。不过,这不一定是我想要返回编辑的文件。由于我左侧有作业 ID,我可以通过fg命令使用其 ID 来调回特定的后台进程:
fg 1
了解如何将进程放入后台可以极大地提高工作效率。例如,假设我正在编辑一个服务器应用程序的配置文件,如 Apache。在编辑配置文件时,我需要查阅 Apache 的文档(手册页面),因为我忘记了某个语法。我可以打开一个新的 Shell 和 SSH 会话,查看另一个窗口中的文档。如果打开太多的 Shell 窗口,可能会变得非常混乱。将当前的nano会话放入后台,查看文档,然后用fg命令将进程调回前台继续工作,所有操作都可以在一个 SSH 会话中完成,这样会更加简便!
要将一个进程放入后台,你不一定需要使用Ctrl + z;实际上,你可以在执行命令时,直接通过在命令末尾加上&符号(&)来将进程放入后台。为了演示这个操作,我将使用htop作为示例。虽然这可能不是最实际的例子,但它可以有效地展示如何启动一个进程并立即将其放入后台。
我们可能还没有安装htop,但现在如果没有安装,请随意安装此软件包(如果尚未安装),然后使用&符号运行它:
sudo apt install htop
htop &
如你所知,第一个命令将在我们的服务器上安装htop软件包。第二个命令会打开htop,但会立即将其放入后台。当它被放入后台时,我会看到它的作业 ID 和进程 ID(接下来会详细讲解)。现在,在任何时候,我都可以使用fg将htop调回前台。由于我刚刚将其放入后台,fg会将htop带回前台,因为它被视为最近的任务。如你所知,如果它不是最近的任务,我可以通过fg命令引用它的作业 ID 来将其带回,即使它不是我最近使用的任务。试着练习一下,使用命令中的&符号并将其带回前台。以htop为例,这种操作很有用,启动它、将其放入后台,然后在需要检查服务器性能时随时将其带回前台。
不过请记住,当你退出 Shell 时,所有后台进程都会关闭。如果你在文本编辑器中有未保存的工作,你将会丢失正在处理的内容。因此,如果你使用后台进程,在注销之前,最好先执行jobs命令检查是否有待处理的任务仍在运行。
此外,你可能会注意到,一些应用程序能够平稳地置于后台运行,而其他应用程序则不能。以使用文本编辑器和htop为例,这些应用会在后台保持暂停状态,允许我们执行其他任务,然后再返回这些命令。然而,一些应用即使被置于后台,仍可能会定期在主窗口输出诊断文本。为了更好地控制你的 Bash 会话,你可以学习如何使用多路复用器,如tmux或screen,让这些进程在它们自己的会话中运行,这样就不会打断你的工作。虽然本书不打算详细介绍如何使用tmux这类程序,但如果你感兴趣,它是一个非常有用的工具。
能够将进程置于后台和前台运行,使我们能更有效地管理命令行上的任务,这绝对是有用的。现在,我们可以扩展这一点,看看如何查看服务器上的其他进程,包括那些我们没有手动启动的进程,比如文本编辑器。在接下来的部分中,我们将详细了解ps命令,它可以帮助我们理解服务器上实际运行的进程。
理解ps命令
在管理我们的服务器时,我们需要了解哪些进程正在运行,并且如何管理它们。在本章后续的部分中,我们将学习如何启动、停止和监控进程。但在讲解这些概念之前,我们首先需要能够确定服务器上实际运行的进程。ps命令可以帮助我们完成这项工作。
使用ps查看正在运行的进程
当ps命令单独执行时,它会显示由调用该命令的用户运行的进程列表:

图 7.2:当以普通用户身份运行ps命令且没有选项时的输出
在图 7.2中,你可以看到当我作为自己的用户运行ps命令且没有任何选项时,它显示了我以自己身份运行的进程列表。在这个例子中,我有一个vim会话正在运行(在后台),并且在最后一行,我们还看到了ps本身,它也包含在输出中。
在输出的左侧,你会看到每个正在运行的进程的编号。这被称为进程 ID(PID),我们在管理作业部分中提到过。为了继续之前,PID 是你应该熟悉的内容,所以我们不妨现在就来讲解一下。
服务器上运行的每个进程都会分配一个 PID,这使它与系统上的其他进程区分开来。你可能会理解一个进程是vim、top或其他某个名称。但是,我们的服务器通过进程的 ID 来识别它们。当你打开一个程序或启动一个进程时,内核会为其分配一个 PID。当你管理服务器时,你会发现知道 PID 非常有用,特别是对于本章将要介绍的命令。例如,如果你想终止一个表现不正常的进程,一个典型的工作流程是先找到该进程的 PID,然后在杀死进程时引用该 PID(稍后我会展示如何做)。实际上,PID 比单纯分配给运行进程的数字要复杂,但本章的主要目的是帮助你记住这个概念。
如果你知道进程的名称,你也可以使用pidof命令来查找进程的 PID。例如,我曾向你展示过一个vim进程的截图,PID 为1385。你也可以通过运行以下命令来做到这一点:
pidof vim
输出将给出进程的 PID,而无需使用ps命令。
配置ps的参数
继续使用ps命令,你可以通过提供几个有用的参数来改变输出的方式。如果你使用a选项,你会看到比正常情况更多的信息:
ps a
这将产生如下所示的输出:

图 7.3:ps a命令的输出
使用ps a,我们看到的输出与之前相同,但附加了更多的信息,并且顶部有了列标题。现在我们看到了PID、TTY、STAT、TIME和COMMAND的标题。从这个新的输出中,你可以看到我正在运行的vim进程正在编辑一个名为testfile.txt的文件。这很有用,因为如果我有多个vim会话打开,而其中一个会话表现异常,我可能想知道需要停止的是哪一个。
我们已经看到了PID和COMMAND字段,尽管我们没有看到顶部的正式标题。我们已经讨论过PID列,所以我就不再详细介绍了。COMMAND字段告诉我们实际正在运行的命令,如果我们想确保管理的是正确的进程,或者想查看某个用户正在运行的程序,这非常有用(稍后我会演示如何显示其他用户的进程)。
STAT 字段是新的;我们在单独运行 ps 时并没有看到它。STAT 字段告诉我们进程的状态码,表示该进程当前所处的状态。状态可以是不可中断的睡眠(D)、僵尸进程(Z)、停止(T)、可中断的睡眠(S)和运行队列(R)。还有分页(W),但现在不再使用,因此不需要讨论。不可中断的睡眠是一种进程状态,通常表示该进程在等待输入,无法处理额外的信号(我们稍后会简要介绍信号)。僵尸进程(也称为僵尸进程)在所有实际情况下已经完成了任务,但仍在等待父进程执行清理操作。僵尸进程实际上并没有运行,但会保留在进程列表中,通常会自行关闭。如果这样的进程无限期地保留在列表中并没有关闭,它就可能成为 kill 命令的候选对象,我们稍后会讨论该命令。停止的进程通常是已被发送到后台的进程,这将在下一节中讨论。可中断的睡眠表示程序处于空闲状态:它在等待输入,以便唤醒。
TTY 列告诉我们进程附加到哪个 TTY。TTY 是指打字机,这是一个来自不同历史时期的术语。过去,在大型主机时代,用户使用“终端”来操作计算机——这种终端设备由显示器和键盘组成,通过电缆与主机相连。这些设备只能显示从主机接收到的输出,并接收键盘上输入的数据。打字机就是用来指代这类设备的术语。显然,现在我们不再使用这样的设备,但从虚拟的角度来看,概念是相似的。
在我们的服务器上,我们使用键盘向一个设备发送输入,然后该设备将输出显示到另一个设备上。在我们的例子中,输入设备是我们的键盘,输出设备是我们的屏幕,屏幕要么直接连接到服务器,要么连接到我们的计算机,计算机通过像 SSH 这样的服务与服务器相连。在 Linux 系统中,大多数进程运行在 TTY 上,TTY 是(从实际应用的角度来说)一个终端,它接收输入并管理输出,类似于虚拟意义上的打字机。终端是我们与服务器交互的方式。
在图 7.3中,我们看到一个进程正在tty1的 TTY 上运行,其他进程则在pts/0上运行。我们看到的 TTY 是实际的终端设备,而pts指代的是一个虚拟(伪)终端设备。我们的服务器实际上能够运行多个tty会话,通常是 1 到 7 个。每个 TTY 会话都可以运行自己的程序和进程。为了更好地理解这一点,尝试按下Ctrl + Alt + 任意功能键,从F1到F7(如果你插入了物理键盘到物理服务器)。每按一次,你应该看到屏幕清空,然后转到另一个终端。每个终端都是独立的。每个功能键代表一个特定的 TTY,所以按下Ctrl + Alt + F6时,你会把显示切换到 TTY 6。
本质上,你是在从 TTY 1 切换到 TTY 7,每个 TTY 都可以容纳自己正在运行的进程。如果你再次运行ps a,你会看到你在这些 TTY 上启动的任何进程会以tty会话的形式显示在输出中,例如tty2或tty4。你在终端模拟器中启动的进程会被标记为pts,因为它们并不在一个实际的 TTY 上运行,而是运行在伪 TTY 上。
这是一个看似复杂但实际上很简单的讨论(TTY 或伪 TTY),但通过掌握这些知识,你应该能够区分一个进程是在实际服务器上运行还是通过一个终端外壳运行。
接下来,让我们看看ps命令输出中的TIME字段。这个字段表示 CPU 为该特定进程分配的总时间。然而,在我提供的截图中,每个进程的时间显示为0:00。刚开始可能会有些困惑。就我而言,特别是vim进程自从我截图以来已经运行了大约 15 分钟,但它们现在仍然显示为0:00的利用时间。实际上,这并不是进程运行的时间,而是进程与 CPU 进行交互的时间。对于vim而言,每个进程只是一个打开了文件的缓冲区。为了进行对比,我现在正在写这章的 Linux 机器的进程 ID 为759,时间为92:51。PID 759属于我的 X 服务器,它提供了我的图形用户界面(GUI)和窗口功能。然而,当前这台笔记本的正常运行时间为 6 天 22 小时,也就是大约 166 小时,这与 PID 759在TIME列中报告的时间并不相同。因此,我们可以推测,尽管我的笔记本已经连续运行了 6 天,但 X 服务器实际利用的 CPU 时间仅为 92 小时 51 分钟。总结一下,TIME列指的是进程需要 CPU 的时间以便计算某些东西,它不一定等于进程已经运行的时间,也不等于图形进程在屏幕上显示的时间。
让我们继续探讨ps命令,并查看一些额外的选项。首先,让我们看看当我们在之前的例子中添加u选项时,会得到什么结果,下面是相应的命令:
ps au
这将产生类似于以下内容的输出:

图 7.4:ps au命令的输出
当你运行时,你应该立刻注意到与ps a命令的不同。使用这种变体,你会看到列出的是由你的用户 ID 以及其他用户运行的进程。当我运行时,我看到输出中列出了我的用户(jay)的进程,以及一个root的进程。u选项将是你可能会常用的选项,因为在大多数管理服务器的情况下,你可能更关心的是监控你的用户在做些什么。但是,ps命令最常用的变体可能是以下这种:
ps aux
加上 x 选项后,我们不再将输出限制于 TTY 内的进程(无论是本地的还是伪终端的)。结果是我们将看到更多的进程,包括那些不与我们自己启动的进程相关的系统级进程。试试看吧。不过在实践中,ps aux 命令最常与 grep 一起使用,以查找特定的进程或字符串。例如,假设你想查看所有 nginx 工作进程的列表。为此,你可以执行如下命令:
ps aux | grep nginx
在这里,我们像之前一样执行 ps aux 命令,但我们将输出通过管道传输到 grep,并只查找包含字符串 nginx 的输出行。在实践中,这是我常用 ps 的方式,也是我注意到很多其他管理员常用的方式。使用 ps aux 后,我们可以看到更多的输出,然后通过管道传输到 grep 来用搜索条件进一步缩小范围。然而,如果我们只想显示包含特定字符串的进程,我们也可以执行以下命令:
ps u -C nginx
这将输出匹配 nginx 的进程列表及相关详细信息。ps 命令的另一个有用变体是通过按 CPU 使用率排序来排序进程:
ps aux --sort=-pcpu
不幸的是,该命令会显示大量的输出,我们需要滚动回到顶部才能看到最上面的进程。根据你的终端配置,你可能无法回滚太多(甚至根本无法回滚),因此以下命令将进一步缩小输出范围:
ps aux --sort=-pcpu | head -n 5
这可真有用!在这个示例中,我使用 ps aux 命令并加上 --sort 选项,按照 CPU 使用百分比(-pcpu)排序。然后,我将输出传送到 head 命令,指示它只显示前五行(-n 5)。本质上,这给了我自开机以来使用最多 CPU 的前五个进程列表。事实上,我也可以做到同样的事情,只不过是按使用最多内存来排序:
ps aux --sort=-pmem | head -n 5
如果你想确定哪些进程表现异常并占用大量内存或 CPU,那么这些命令将帮助你缩小范围。ps 命令是管理员工具箱中非常有用的命令。你可以在我提供的示例之外自由实验,想要了解更多技巧,你可以查阅 ps 命令的手册页。实际上,ps 命令的手册页第二部分(在 examples 下)提供了更多有趣的示例,供你尝试。
现在我们已经知道如何检查正在运行的进程,在接下来的部分中,我们将了解如何更改进程的优先级,确保更重要的进程能够获得 CPU 更多的关注。
更改进程的优先级
Linux 系统上的进程可以以更改后的优先级运行,从而让某些进程拥有更高的优先级,其他进程则拥有较低的优先级。这为你作为管理员提供了充分的权限,确保系统中最重要的进程得到足够的优先级。为此,我们有专门的命令:nice和renice。这些命令允许你启动一个具有特定优先级的进程,或者更改已经运行中的进程的优先级。
现在,管理员们手动编辑进程的优先级已经不像以前那么常见了。拥有 32 个核心(甚至更多)的处理器已经不是什么稀奇事,几百 GB 的内存也是如此。如今的服务器无疑比以前更强大,且不像过去的机器那样资源紧张。许多服务器(例如虚拟机)和容器被专门用于执行单一任务,因此进程调优可能已经不再那么重要。然而,数据处理公司和使用深度学习功能的公司可能仍然需要进行某些细节调整。
无论是否优先处理进程对你有直接用处,至少理解这一概念是个好主意,免得有一天你需要增加或减少某个进程的优先级时能够应对。让我们重新查看一下ps命令,这次加上-l参数:
ps -l
该命令的输出将如下所示:

图 7.5:ps -l命令的输出
从ps -l命令的输出中,可以注意到PRI和NI列。PRI表示优先级,NI表示“niceness”值,我们稍后将在本节中详细讨论。在这个例子中,我运行的每个进程的PRI都是80,NI是0。我没有改变或调整这些值;这些是我启动没有特别调整的进程时的默认值。PRI的值80是所有进程的初始值,并会随着“niceness”值的增加或减少而变化。
正如我所提到的,我们有专门的命令来允许我们更改优先级,分别是nice和renice。选择使用哪个命令,取决于进程是否已经在运行。关于图 7.5中的进程,我们需要使用renice来更改它们的优先级,因为这些进程都已经在运行。如果我们想从一开始就启动一个具有特定优先级的进程,我们应该使用nice。
例如,让我们改变我正在运行的vim会话的进程。确实,这是一个有些无聊的例子,因为vim并不是一个非常重要的进程。在现实中,你会优先处理那些真正重要的进程。以我为例,既然vim进程的 PID 是1789,那么我需要运行的命令来改变优先级将是这个:
renice -n 10 -p 1789
该命令的输出将如下所示:

图 7.6:使用 renice 更改进程优先级
如果我们再次运行ps -l,可以看到vim的新的 nice 值:

图 7.7:更改进程优先级后ps -l命令的输出
现在,vim的新的 nice 值10在NI列中显示,PRI值也增加到90。现在,这个vim实例将以比其他任务更低的优先级运行,因为 nice 值越高,优先级越低。注意,当我更改优先级时,并没有使用sudo。在这个例子中,这是可以的,因为我只是增加了进程的 nice 值,这是允许的。但是,让我们尝试在没有sudo的情况下减少 nice 值,使用以下命令:
renice -n 5 -p 1789
正如你在以下输出中看到的,我并没有成功:

图 7.8:尝试降低进程的优先级
我尝试将 nice 值从 10 降低到 5 被阻止了。如果我能够降低 nice 值,那么我的进程将以更高的优先级运行。相反,我收到了一个Permission denied错误。所以,本质上,用户可以增加他们进程的 nice 值,但不能减少,哪怕是自己启动的进程。如果你希望减少 nice 值,必须使用sudo。所以,本质上,如果你想“更友善”一些,你可以继续进行。如果你想“更严苛”一些,你需要root权限。此外,用户无法更改他们不拥有的进程的优先级。因此,如果你试图使用renice更改另一个用户正在运行的任务的 nice 值,你将收到Operation not permitted错误。
此时,我们知道如何使用renice重新调整正在运行的进程的优先级。现在,让我们看一下如何使用nice启动一个具有特定优先级的新进程。请看以下命令:
nice -n 10 vim
在这里,我们启动了一个新的vim实例,但一开始就将优先级设置为特定的值。如果我们稍后想再次更改vim的优先级,我们需要使用renice。正如我之前提到的,nice用于启动一个具有特定优先级的新进程,而renice则用于更改已存在进程的优先级。在这个例子中,我们通过一个命令启动了vim并将其 nice 值设置为10。
将像vim这样的文本编辑器的优先级改变可能看起来是一个奇怪的测试用例,确实是这样。但vim编辑器是无害的,因为我们改变它的优先级导致系统崩溃的可能性极小。我想不出有什么实际理由需要重新调整像文本编辑器这样的程序的优先级。不过值得注意的是,你可以改变运行在服务器上的进程的优先级。在真实的服务器上,你可能有一个重要的进程,它生成报告并且必须按时交付。或者你可能有一个进程生成一个数据导出,客户需要这些数据来完成按时交付。所以,从更广阔的视角来看,你可以用对你或你的组织真正重要的进程名来替换vim。
你可能会好奇,nice和renice命令中的nice是什么意思。nice值本质上指的是一个进程对其他用户的“友好程度”。nice 值越高,优先级越低。所以,值为 20 比值为 10 更“友好”。在这种情况下,nice 值为 20 的进程以较低的优先级运行,因此对系统中的其他进程更加友好。nice 值的范围是从-20 到 19。nice 值为-20 的进程优先级最高,而 19 是最低的优先级。整个系统比这个简单的描述要复杂得多。尽管我将 nice 值称作优先级,但它实际上并不是。nice 值用于计算实际的优先级。不过现在,如果我们将 nice 值简化为代表优先级,并且假设 nice 值越高优先级越低,那就足够了。
到目前为止,我们一直在使用nice和renice命令,并结合-n选项直接设置 nice 值。不过值得注意的是,你可以简化renice命令,省略-n选项:
renice 10 42467
该命令将进程的 nice 值设置为正数 10,类似于我们之前的其他示例。如果我们想提高优先级,也可以使用负数来设置 nice 值:
sudo renice -10 42467
虽然省略-n选项并不会大幅减少我们打字的工作量,但现在你知道这是一个可行的选项。与这个例子不同的是,由于我在降低优先级值(稍后会详细介绍),我需要使用sudo。
在使用nice命令时,我们也可以省略-n选项,但在这方面命令的行为略有不同。下面的命令将无法正常工作:
nice 15 vim
nice的语法略有不同,所以直接给它一个正数并不会像在renice中那样有效。为此,我们需要在数字前面加上一个短横线:
nice -15 vim
当你查看这个命令时,可能会认为我们在应用一个负数。实际上,情况并非如此。由于nice的语法不同,我们使用的-15值实际上会转换为正数 15。我们在值前加上连字符是为了告诉nice我们正在将该值作为选项应用。如果我们确实想在不使用-n选项的情况下使用负值与nice命令配合使用,我们需要使用两个连字符:
nice --10 vim
我认为这两个命令在使用-n选项时的语法差异有点令人困惑,因此我建议直接在nice和renice中都使用-n选项,因为这样它们会更加一致:
nice -n 10 vim
sudo nice -n -10 vim
renice -n 10 42467
sudo renice -n -10 42467
这些示例展示了如何使用-n选项分别操作nice和renice命令,并设置正值和负值。由于这两个命令中-n选项的使用方式相同,因此可能更容易将其记住,而不是关注具体的细节。如前所述,我在设置负值时使用了sudo,因为只有root用户才能将进程的优先级设置为低于0。如果你试图这样做,你将收到以下错误:
nice: cannot set niceness: Permission denied
这种类型的保护是有一定重要性的,因为你可能会遇到一些用户,他们认为自己的进程是最重要的,试图将它们的优先级提升到-19。归根结底,最好由系统管理员来决定哪些进程可以将优先级设置为负值。
作为 Ubuntu 服务器的管理员,决定哪些进程应该运行,且运行时的优先级是多少,是你的责任。然后,你将确定最佳的方式以实现合适的系统状态,调优进程优先级可能是其中的一部分。如果没有别的,学习nice和renice命令将为你的工具集增添另一项实用功能。
处理不正常的进程
关于ps命令,到目前为止你已经知道如何显示服务器上正在运行的进程,并且可以通过字符串或资源使用情况来缩小输出范围。但你实际上能做些什么呢?尽管我们不愿承认,但有时服务器上运行的进程会失败或表现不正常,这时你可能需要重启它们。如果某个进程无法正常关闭,你可能需要终止该进程。在本节中,我们介绍了kill和killall命令来执行这一操作。
kill命令接受 PID 作为参数,并尝试优雅地关闭一个进程。在一个典型的工作流中,当你需要终止一个无法自行关闭的进程时,你首先会使用ps命令找到该进程的 PID。然后,知道 PID 后,你可以尝试kill该进程。例如,如果 PID 为31258的进程需要被终止,你可以执行以下命令:
sudo kill 31258
如果一切顺利,进程将结束。你可以重新启动它,或者通过查看日志来调查它为什么失败。
为了更好地理解kill命令的作用,你首先需要理解Linux 信号的基础知识。信号由管理员和开发者使用,可以通过内核、另一个进程或手动命令发送给一个进程。信号指示进程执行请求或更改,有时甚至是完全终止。例如,SIGHUP信号告诉进程它们的控制终端已经退出。一个可能发生这种情况的情况是你打开了一个终端模拟器,里面运行着几个进程。如果你关闭终端窗口(而没有停止你正在运行的进程),这些进程将会收到SIGHUP信号,基本上告诉它们退出(本质上意味着 shell 已退出或挂起)。
其他示例包括SIGINT(当应用程序在前台运行时,按下Ctrl + c会停止它),以及SIGTERM,当发送到进程时,请求它干净地终止。还有一个例子是SIGKILL,它强制进程以不干净的方式终止。除了名称,每个信号还由一个值表示,例如SIGTERM的值是15,SIGKILL的值是9。逐一介绍所有信号超出了本章的范围(信号的高级主题主要对开发者有用),但如果你感兴趣的话,可以通过查阅 man 页查看更多信息:
man 7 signal
在本节中,我们最关注的两种信号是SIGTERM(15)和SIGKILL(9)。当我们想要停止一个进程时,我们向它发送其中一个信号,而kill命令正是允许我们做到这一点。默认情况下,kill命令会发送信号15(SIGTERM),告诉进程干净地终止。如果成功,进程将释放其内存并优雅地关闭。通过我们之前的kill命令,我们向进程发送了信号15,因为我们没有明确指定发送哪个信号。
使用SIGKILL(9)终止进程被认为是一种极端的最后手段。当你向一个进程发送信号9时,相当于把地毯从它下面撕掉,或者用一根炸丨药棍把它炸飞。进程会被强制关闭,完全没有反应的时间,因此,这是一种你应该避免使用的手段,除非你已经尝试了所有能想到的办法。理论上,发送信号9可能会导致文件损坏、内存问题或其他异常情况。至于我,虽然我自己使用过它,但从未遇到过长期的损害,但理论上是有可能发生的,因此你只应该在极端情况下使用它。一个可能需要使用此信号的情况是有关defunct或僵尸进程的情况,当它们无法自行关闭时。这些进程基本上已经“死了”,通常是等待其父进程来回收它们。
如果父进程从未尝试这样做,它们将一直保留在进程列表中。单从这个角度看,这可能并不算什么大问题,因为这些进程技术上并没有做任何事情。但如果它们的存在引发了问题,而你又无法终止它们,可以尝试向进程发送SIGKILL信号。消除僵尸进程应该不会带来任何危害,但你还是希望它们能有时间被清除。
要向进程发送9信号,你需要使用kill命令的-9选项。尽管不言而喻,但还是要确保你执行的是正确的进程 ID。
sudo kill -9 31258
就这样,PID 为31258的进程会毫无痕迹地消失。它正在写入的任何数据将会处于悬空状态,并且会立即从内存中删除。如果出于某种原因,该进程仍然继续运行(这非常罕见),你可能需要重启服务器来彻底清除它,这种情况我只在极为罕见的几个案例中见过。例如,僵尸进程就是这样一种情况,它出现在进程列表中,但无论发送什么信号,它都不会受到影响,因为这种进程根本不会被调度占用 CPU 时间。总的来说,如果kill -9不能清除进程,那么就没有什么能做到的了。
另一种终止进程的方法是使用killall命令,这比kill命令更安全(至少在没有其他原因的情况下,误终止错误进程的可能性较小)。与kill类似,killall允许你向进程发送SIGTERM信号,但与kill不同,你可以通过进程名称来执行。此外,killall不仅会终止一个进程,它会终止所有与给定名称匹配的进程。要使用killall,你只需执行killall并指定一个进程的名称:
sudo killall myprocess
和kill命令一样,你也可以向进程发送9信号:
sudo killall -9 myprocess
同样,只有在必要时才使用该方法。实际上,你可能不会经常使用killall -9(甚至从未使用过),因为多个同名进程被锁住的情况很少发生。如果确实需要发送9信号,尽量使用kill命令。
kill和killall命令在进程卡住的情况下非常有用,但这些命令最好不是经常使用的。进程卡住通常发生在应用程序遇到无法恢复的情况时,因此,如果你经常需要终止进程,可能需要检查负责服务的软件包是否有更新,或者检查服务器是否存在硬件问题。
在下一节中,我们将查看在后台运行并为我们或我们的用户提供服务的系统进程,例如 web 服务器进程或 DHCP 服务器。
管理系统进程
系统进程,也称为守护进程,是在服务器后台运行的程序,通常会在服务器启动时自动启动。我们通常不会直接管理这些服务,因为它们在后台运行并执行其职责,无论是否需要我们的输入。例如,如果我们的服务器是一个 DHCP 服务器并且运行isc-dhcp-server进程,那么该进程将在后台运行,监听 DHCP 请求并为它们分配新的 IP 地址。通常,当我们安装一个作为服务运行的应用程序时,Ubuntu 会配置它在启动时自动启动,因此我们不需要手动启动它。假设该服务没有出现问题,它将继续执行其工作,直到我们告诉它停止。在 Linux 中,服务由 init 系统管理,也称为 PID 1,因为 Linux 系统的 init 系统总是获得该 PID。在近年来,Ubuntu Server 中进程管理的方式发生了很大变化。Ubuntu 已经切换到systemd作为其init系统,之前一直使用 Upstart,直到几年前。Ubuntu 16.04 是首个使用systemd的 LTS 版本,并且这一标准在 Ubuntu 22.04 中继续沿用。由于systemd已经成为标准一段时间,我们将重点介绍与其一起使用的命令来管理服务。旧的init系统正在逐渐淘汰。
在systemd中,服务被称为单元,但在实际使用中,“服务”、“守护进程”和“单元”这几个术语本质上是相同的。自从 20 多年前开始使用 Linux 以来,我依然习惯性地将systemd的单元称为服务。为了帮助我们管理这些“单元”,systemd包括了systemctl命令,它允许你启动、停止并查看服务器上单元的状态。为了帮助说明这一点,我将以 OpenSSH 为例。单元的名称并不重要,因为systemctl命令的语法无论我们操作的是哪个单元名称都是相同的。你可以使用systemctl启动、停止或重新启动 Apache 实例、数据库服务器,甚至用它重新启动整个网络堆栈。systemctl命令如果没有任何选项或参数,会默认使用list-units选项,列出单元的列表到你的 shell 中。不过,这可能会有点杂乱,所以如果你已经知道你要查找的单元名称,你可以将输出通过管道传递给grep,并搜索字符串。在你可能不知道单元的确切名称,但知道部分名称的情况下,这非常方便:
systemctl | grep ssh
如果你想检查某个单元的健康状况,最好的方法是使用status关键字,它会显示有关该单元的一些非常有用的信息。这些信息包括该单元是否正在运行、是否已启用(意味着它已配置为在启动时自动启动),以及该单元的最新日志条目:
systemctl status ssh
这个命令将产生类似以下的输出:

图 7.9:使用 systemctl 检查单元的状态
大多数情况下,你实际上可以在不需要 root 权限的情况下检查单元的状态,但你可能看不到所有可用的信息。在截图中,你可以看到几个关于 ssh 服务的日志条目,但某些单元在没有 sudo 权限的情况下不会显示这些条目。特别是对于 ssh 单元,我们在使用或不使用 sudo 时都会看到日志条目。
你可能在截图中注意到的另一件事是,ssh 单元的名称实际上是 ssh.service,但你不需要包含 .service 部分,因为默认情况下它是隐含的。有时,在使用 systemctl 查看进程状态时,输出可能会被压缩以节省屏幕空间。为了避免这种情况并查看完整的日志条目,可以添加 -l 选项:
systemctl status -l ssh
另一件需要注意的事情是单元的 vendor preset。大多数包含 systemd 服务文件的 Ubuntu 软件包会自动启用它,但其他发行版通常默认不会启动和启用单元(例如 CentOS)。以 ssh 为例,你可以看到 vendor preset 被设置为 enabled。这意味着一旦安装 openssh-server 包,ssh.service 单元将自动启用。你可以通过检查 Active 行来确认这一点(例如输出中显示 active (running)),它告诉我们该单元正在运行。Loaded 行则明确指出该单元是 enabled,所以我们知道下次启动服务器时,ssh 将会自动加载。虽然在 Ubuntu 中安装包时,systemd 单元通常会被自动启用并启动,但这仍然会有所不同。在安装新软件包时,确保检查该单元的状态,以便了解它的设置。
启动和停止单元同样简单;你只需将 systemctl 中使用的关键字更改为 start 或 stop,即可实现所需的效果:
sudo systemctl stop ssh
sudo systemctl start ssh
还有一些其他关键字,例如 restart(它一次性处理前两个命令示例的功能),一些单元甚至提供 reload,允许你在不关闭整个应用程序的情况下激活新的配置设置。一个有用的例子是 Apache,它向本地或外部用户提供网页服务。如果你停止 Apache,所有用户将与网站断开连接。如果你添加了一个新的网站,可以使用 reload 而不是 restart,这样可以在不打断现有连接的情况下激活你所做的任何新配置。我们将在第十四章《提供网页内容》中查看 Apache,所以现在不必过多担心 Apache。它只是一个具有附加功能的单元的好例子,并不是所有单元都有 reload 选项,因此你应当查阅提供该单元的应用程序文档,以确保这一点。
由于我在之前的示例中提到了启动和停止 OpenSSH 单元的操作,有一个有趣的补充是,执行这些操作不会中断当前的 SSH 会话,如果你已经打开了 SSH 会话。停止 ssh 服务不会断开你的连接。现有的连接会被保持,停止 SSH 只会阻止新的连接建立。因此,与其他单元(例如 Apache)不同,SSH 在重新启动时不会中断现有连接。
如我之前所提到的,如果你希望一个单元在服务器启动时自动启动,则该单元需要被启用。大多数单元默认是自动启用的,但如果你遇到一个没有启用的单元,你可以通过 enable 关键字来启用它:
sudo systemctl enable ssh
禁用一个单元同样也很简单:
sudo systemctl disable ssh
你可以将启用一个单元的过程与启动它的过程结合在一起:
sudo systemctl enable --now ssh
--now 参数告诉 systemctl 在启用单元后立即启动它,而不是等待下次启动时才执行,或者需要你在单独的命令中运行 start 参数。
虽然 systemd 主要用于管理单元,但它实际上是一个管理 Linux 系统多个方面的平台,包括 DNS 解析、网络等。systemd 甚至还处理日志记录,并且提供了 journalctl 命令,我们可以使用它来查看日志信息(这也是为什么 systemctl status ssh 的输出能够显示日志条目的原因)。
我们在第四章《导航和基本命令》中已经讨论过一些日志记录内容,接下来在第二十二章《故障排除 Ubuntu 服务器》中,我们将更详细地讨论这方面内容(其中还会包括对 journalctl 命令的进一步讨论)。
目前,只需要理解 systemd 在管理系统中的各个方面非常广泛。对于本章而言,如果你了解如何启动、停止、启用、禁用并检查单元的状态,那么你现在就可以继续往下进行。
使用 cron 调度任务
在本章前面,我们已经学习了如何启动进程并使其在后台运行,以及确保它们在服务器启动时立即启动。在某些情况下,您可能需要一个应用程序在特定时间执行任务,而不是始终在后台运行。这就是cron的用武之地。通过cron,您可以设置一个进程、程序或脚本在特定时间运行,甚至到分钟级别。每个用户可以拥有自己的cron配置(称为crontab),可以执行任何用户通常可以执行的功能。root用户也有一个crontab,允许执行系统范围的管理任务。每个crontab包含一个cron任务列表(每行一个),我们马上就会介绍。要查看用户的crontab,我们可以使用crontab命令:
crontab -l
使用-l选项,crontab命令将显示执行命令的用户的作业列表。如果以root身份执行它,您将看到root帐户的crontab。如果以用户jdoe身份执行它,您将看到jdoe的crontab,依此类推。如果您想查看除您自己以外的用户的crontab,您可以使用-u选项并指定一个用户,但需要以root或sudo权限执行才能查看其他用户的crontab:
sudo crontab -u jdoe -l
默认情况下,没有用户具有crontab,直到您创建一个或多个任务。因此,当您检查当前用户时,您可能会看到类似以下的输出:
no crontab for jdoe
要创建一个cron任务,请首先登录为您希望该任务在其下运行的用户帐户。然后,发出以下命令:
crontab -e
如果您的系统上有多个文本编辑器,您可能会看到类似以下的输出:

图 7.10:选择与crontab命令一起使用的编辑器
在这种情况下,当您创建您的cron任务时,您只需按下对应于您想使用的文本编辑器的数字。要设置一个指定编辑器的环境变量,并用单个命令编辑您的crontab,以下命令将准确完成此操作:
EDITOR=vim crontab -e
在这个例子中,您可以用您喜欢的文本编辑器替换vim。此时,您应该进入一个文本编辑器,并打开您的crontab文件。每个用户的默认crontab文件都包含一些有用的注释,这些注释提供了关于cron如何工作的一些有用信息。要添加一个新的任务,您需要滚动到文件的底部(在所有注释之后)并插入一行。格式在这里非常重要,文件中的示例注释会给出每行布局的一些线索。特别是这部分:
m h dom mon dow command
每个cron任务有六个字段,每个字段之间至少由一个空格或制表符分隔。如果你使用多个空格或制表符,cron足够智能,能够正确解析文件。在第一个字段中,我们设置任务执行的分钟数。在第二个字段中,我们设置 24 小时制的小时数,范围从 0 到 23。第三个字段表示月份中的天数。在该字段中,你可以填写 5(5 号)、23(23 号)等。第四个字段对应于月份,比如 3 代表 3 月,12 代表 12 月。第五个字段是星期几,数字从 0 到 6,分别表示星期日到星期六。最后一个字段是要执行的命令。以下是几个crontab行的示例:
3 0 * * 4 /usr/local/bin/cleanup.sh
* 0 * * * /usr/bin/apt update
0 1 1 * * /usr/local/bin/run_report.sh
在第一个例子中,位于/usr/local/bin的cleanup.sh脚本将在每周四凌晨 12:03 运行。我们知道这一点,因为分钟列设置为3,小时列设置为0(午夜),日期列为4(星期四),而命令列显示了完全限定的命令/usr/local/bin/cleanup.sh。
命令的完全限定是什么意思?基本上,命令完全限定意味着完全写出负责该命令的二进制文件的完整路径。在第二个例子中,我们本可以简单地输入apt update命令,并且可能能够正常运行。然而,不包括程序的完整路径被认为是糟糕的cron习惯。虽然没有完全限定的命令可能仍然能成功运行,但其成功与否取决于调用该命令的用户路径中是否能找到该应用程序。并非所有服务器都以相同的方式设置,因此根据外壳的设置,可能无法运行。如果包含完整路径,则无论底层外壳如何配置,任务都应能正常运行。
如果你不知道完全限定的命令是什么,只需使用which命令。这个命令,当与你想要运行的命令名称一起使用时,将返回该命令在系统中的完全限定路径。
继续第二个例子,我们正在运行/usr/bin/apt update来每天凌晨 12 点更新服务器的仓库索引。每一行的星号代表任何,因此分钟列为*时,意味着此任务可以在任何分钟执行。基本上,我们唯一明确的是小时字段,我们将其设置为0,表示凌晨 12 点。
在第三个例子中,我们在每个月的第一天凌晨 01:00 运行/usr/local/bin/run_report.sh脚本。如果你注意到,我们将第三列(日)设置为1,这与 2 月 1 日、3 月 1 日等相同。这个任务将在每月的第一天执行,但只有在当前时间也是凌晨 01:00 时才会执行,因为我们填充了第一列和第二列,分别代表分钟和小时。
一旦你编辑并保存了用户的crontab,cron会被更新,从那时起,它将在你选择的时间执行任务。crontab将根据你服务器上的当前时间和日期执行,所以你要确保时间是正确的,否则你的任务会在意外的时间执行。你可以通过简单地运行date命令查看你服务器上的当前日期和时间。
要掌握使用cron创建作业,最好的方法(一如既往)是多加练习。第二个示例cron作业可能是一个不错的实验对象,因为更新你的仓库索引不会带来任何坏处。
总结
在本章中,我们学习了如何管理进程。我们首先查看了ps命令,可以用它来查看当前正在运行的进程列表。我们还了解了如何管理作业,以及如何终止一些因某些原因而不正常的进程。我们还讨论了如何改变进程的优先级,以确保我们可以完全控制哪些进程获得更多的处理时间,并且我们还学会了如何使用cron安排任务在稍后的时间和日期执行。
在第八章,监控系统资源中,我们将探讨一些方法,通过它们我们可以关注服务器上可用的资源,在这里我们将学习如何检查磁盘使用情况,理解内存使用情况和交换空间,同时也会看一些可以让资源管理变得轻松的工具。
相关视频
-
开始使用 tmux(LearnLinuxTV):
linux.video/tmux-guide -
Linux 中的后台与前台进程管理(LearnLinuxTV):
linux.video/bg-fg
进一步阅读
-
Ham Vocke, tmux 快速简易指南:
learnlinux.link/tmux-article -
Tmux 备忘单和快速参考:
learnlinux.link/tcs -
crontab 大师:
learnlinux.link/ctg
加入我们的 Discord 社区
加入我们的社区 Discord 空间,与作者和其他读者讨论:

第八章:监控系统资源
在上一章中,我们学习了如何管理服务器上运行的任务。我们现在知道如何查看后台运行的任务,如何启用或禁用某个单元在启动时自动启动,以及如何安排任务在未来某个时间运行。但是,为了有效管理服务器执行的任务,我们还需要时刻关注系统资源。如果我们内存不足,磁盘已满,或者 CPU 过载,那么本来高效处理任务的服务器可能会突然停滞。在这一章中,我们将探讨这些资源及如何监控它们。
我们关于资源管理的讨论将包括:
-
查看磁盘使用情况
-
监控内存使用情况
-
理解负载平均值
-
使用
htop查看资源使用情况
我们服务器上的一个非常重要的资源是存储,跟踪可用磁盘空间等内容至关重要——即使是你能购买到的最强大服务器,也无法在没有空闲磁盘空间的情况下正常运行。我们将在下一节中探讨一些监控磁盘使用情况的方法。
查看磁盘使用情况
时刻关注你的存储空间非常重要,因为没有人愿意在半夜接到电话,说服务器遇到了问题,尤其是那种本可以轻松避免的问题,例如文件系统空间即将满了。在 Linux 系统上管理存储很简单,一旦你掌握了相关工具,这一过程就会变得容易。接下来我将介绍一些在这一节中最有用的工具,特别是我们将看看如何回答“是什么占用了所有磁盘空间?”这个问题,这是处理磁盘使用情况时最常遇到的问题。
首先,让我们来看一下 df 命令。
使用 df
df 命令通常会是你在不知道哪个卷或挂载点即将满时的起点。执行它后,它会给出一个高层次的概述,因此在你想弄清楚是谁或什么占用了所有空间时,它不一定会非常有用。然而,当你只想列出所有挂载的卷,并查看每个卷剩余的空间时,df 是非常合适的。默认情况下,它会以字节为单位显示信息。不过,我发现使用 df 时加上 -h 选项更加方便,这样可以显示更具可读性的输出,读起来也更加轻松。试试看:
df -h
这应该会生成类似以下内容的输出:

图 8.1:df -h 命令的输出
输出的结果会根据系统中与磁盘和挂载点相关联的类型有所不同。在截图中,你会看到根文件系统位于/dev/mapper/ubuntu--vg-ubuntu--lv。我们知道这一点是因为在“挂载点”这一列下,我们看到挂载点被设置为一个单一的正斜杠(/)。正如我们在第四章《导航和基本命令》中讨论的那样,这个单一的正斜杠表示文件系统的起始位置(也称为根文件系统)。在我的情况下,这是一个 LVM 卷,因此我们看到一个从/dev/mapper开始的长设备名称。现在不必担心 LVM,我们稍后会讨论这个话题。不过目前,记住单一的正斜杠指的是文件系统的起点,左侧的设备名称则指的是实际挂载在那里设备的名称。
实际设备名称因服务器不同而异,且也取决于你在安装过程中是否选择使用 LVM。除了从/dev/mapper开始的长路径外,你可能会看到设备名称如/dev/sda1、/dev/xvda1、/dev/nvme0n1p1或其他变种。设备名称是根据底层存储设备的硬件类型生成的,例如用于 NVME 硬盘的/dev/nvme...命名约定,标准 SATA 硬盘则使用/dev/sdaN,依此类推。
实际底层存储硬件的设备类型并不那么重要;真正重要的是你能够识别出哪个设备最有可能变满。在示例截图中,根文件系统使用了35%的可用空间。在这种情况下,我们并不担心空间会被用完。
如果你确实发现某个重要的存储卷已满或接近满,那么你就能确定需要关注哪个卷,接下来我们会探索更多方法,帮助你获取有关哪些内容占用了空间的更多信息。
然而,有时即使存储卷看似有足够的可用空间,它仍然可能被视为已满。这是因为在 Linux 系统中,存储的数据和数据的大小并不是唯一需要考虑的因素。我们还需要考虑 inode。
那么,inode 究竟是什么?为什么这种东西会导致磁盘被报告为已满,实际上它并没有满呢?可以将 inode 看作一种数据库对象,它包含了你存储的实际项目的元数据。存储在 inode 中的信息包括文件的所有者、权限、最后修改日期和类型(是目录还是文件)。尽管元数据有它的好处,但 inode 的问题在于,每个存储设备上可用的 inode 数量是有限的。如果一个存储设备达到 inode 限制,那么该磁盘仍然被视为已满,无法接受更多数据。
实际上,这种情况的症状是,像df这样的命令会显示磁盘有可用空间,但当你尝试将新文件保存到设备时,会看到一个错误,提示你无法保存,因为磁盘已满。如果你不了解 inode 的存在,那么这种情况可能会让人感到困惑。
尽管似乎有 inode 限制会给存储带来不便,实际上,存储卷上的 inode 限制通常非常高,并且很难达到。通常情况下,如果 inode 限制被触及,意味着服务器本身可能存在更大的问题,导致它达到这个限制。例如,可能是服务器存在问题,保存了比应有的更多文件,例如异常数量的日志文件或排队的电子邮件消息文件。
幸运的是,确定特定存储卷是否快用完 inode 非常简单——与其使用df命令的-h选项,不如改用-i选项。-i选项将显示 inode 计数,而不是基于大小的存储度量。为了帮助说明这一点,我会展示一些来自我服务器的输出,帮助你了解这是什么样子的:
df -i
我系统上该命令的输出如下:

图 8.2:df -i命令的输出
在这个例子中,示例服务器上的根文件系统总共有999424个 inode,其中84223个已被使用,915201个是空闲的。在我的情况下,我有很多可用的 inode。不过,我建议你把df -h和df -i命令记住。不论你遇到的存储空间问题是与实际空间还是 inode 使用相关,使用这两个命令就能知道具体是哪个问题。
假设你有的存储快满了(或者已经满了),你该如何准确定位是哪个东西占用了所有空间呢?有一些额外的工具可以帮助你缩小范围。接下来我们就来探讨这一点。
深入了解磁盘使用情况
调查占用你磁盘空间的具体文件的下一步是找出哪些文件特别占用了这些空间。在这个阶段,你可以使用多种工具来进行调查。我要提到的第一个工具是du命令,它能够显示一个目录占用了多少空间。使用du命令对目录和子目录进行扫描将帮助你缩小问题范围。像df一样,我们也可以与du命令一起使用-h选项,以使输出更容易阅读。默认情况下,du会扫描当前 shell 附加的工作目录,并列出该目录中的每一项,显示每项所占用的总空间,并在最后给出总结。
du命令只能扫描调用该命令的用户有权限访问的目录。如果你以非 root 用户身份运行该命令,可能无法获取完整的信息。此外,当前工作目录中的文件和子目录越多,执行此命令的时间也会越长。如果你大致知道资源占用的地方在哪里,可以尝试cd进入文件系统树中更深的目录,缩小搜索范围,从而减少命令执行的时间。du -h的输出通常会比实际需要的更多,可能需要多个屏幕才能查看完。为了简化这一过程,我最喜欢的命令变体是:
du -hsc *
基本上,你需要在尽可能接近问题所在的目录中运行du -hsc *。正如我们所知道的,-h选项会让输出结果更易读(基本上是以兆字节、千兆字节等形式呈现)。-s选项会提供一个总结,而-c则会显示当前工作目录中所使用的总空间。下面的截图显示了我电脑中的输出结果:

图 8.3:du -hsc *命令的示例输出
为了让这个示例更有趣,我从我的个人桌面上截图,但无论使用哪个设备,最终的命令和语法是一样的。如你所见,du -hsc *提供的信息是一个简洁的总结。从输出中,我们可以清楚地看到当前工作目录中的每个子目录占用了多少空间。例如,我的projects目录当前占用了 2.2GB 的空间,而 ISO 镜像文件占用了 53GB 的空间。
到这一步,我们已经知道当前工作目录顶部的哪些目录占用了最多的空间。但我们仍然需要缩小范围,查找这些目录中究竟是 哪些 项目在占用这些空间。为了深入分析,我们可以 cd 进入这些大目录中的任何一个,再次运行 du 命令。经过几次操作后,我们应该能够缩小到这些目录中最大的文件,并决定如何处理它们。也许我们可以清理不必要的文件,或者添加另一个磁盘。一旦知道了是什么占用了我们的空间,我们就可以决定如何处理。
在阅读到本书的这一部分时,你可能已经认为我有一种奇怪的习惯,总是喜欢把最好的留到最后。你猜对了。我想通过介绍我最喜欢的应用之一来结束这一节,那就是 NCurses 磁盘使用情况 工具(简称 ncdu)。ncdu 命令是那些经常处理磁盘空间问题的管理员学会喜爱和欣赏的工具之一。通过一次执行,这个命令不仅能给你提供占用空间的概览,还能让你在无需反复运行命令和手动浏览目录树的情况下,浏览并深入查看结果。你只需执行一次,然后可以导航结果,深入挖掘直到你需要的程度。
要使用 ncdu,你需要安装它,因为它默认不随 Ubuntu 提供:
sudo apt install ncdu
安装完成后,只需在你选择的任何起始目录中执行 ncdu。完成后,只需按 q 键退出。像 du 一样,ncdu 只能扫描调用用户有权限访问的目录。你可能需要以 root 身份运行它,以获得准确的磁盘使用情况。
你可能想考虑在使用 ncdu 时添加 -x 选项。此选项会将扫描限制在当前文件系统内,这意味着它不会扫描网络挂载点或其他存储设备;它只会关注你开始扫描的设备。这可以避免你浪费时间扫描与问题无关的区域。
执行时,ncdu 将从其起始位置开始扫描每个目录。完成后,它会提供一个基于菜单的布局,允许你浏览结果:

图 8.4:ncdu 实时展示
再次说明,我从我的桌面截图中获取了这张图,截图的目录是在我的 home 目录中。ncdu 的作用是展示从当前目录开始的磁盘使用情况,并且它会按使用空间从高到低排列结果。在 ncdu 内部移动时,你可以通过键盘上的上下箭头来移动你的选择(长白色高亮显示)。
如果你在目录上按下Enter,ncdu将切换到显示该目录的摘要,你可以根据需要继续深入查看。实际上,你还可以按d删除项目和整个文件夹。因此,ncdu不仅允许你找出占用空间的内容,还允许你采取行动!
有时候,磁盘上占用空间的内容很明显,ncdu可能并不总是必要的。一般来说,你会通过df -h开始调查,查看哪个存储卷空间不足。然后,你进入该目录并执行另一个命令,如du -hsc *,以查看哪个目录占用了最多的空间。如果从du的输出中无法立即识别问题所在,那么可以考虑使用像ncdu这样的工具,进一步深入查找。
尽管监控存储至关重要,我们还需要时刻关注空闲内存。接下来,我们将看看如何监控我们服务器的内存。
内存使用情况监控
我经常忘记事情。即便车钥匙几乎总是就在我的口袋里,我也经常忘记它们在哪里。即使已经使用 Linux 超过 20 年,我还是经常忘记在需要时使用sudo命令。幸运的是,计算机的记忆比我强大,但如果我们不有效地管理它,我们服务器上的内存就像我忘记把刚洗完的衣服放进干衣机一样,毫无用处。
了解 Linux 如何管理内存实际上是一个相对复杂的话题,因为对于初学者来说,理解内存到底有多少是空闲的,可能会是一个难以克服的障碍。你会很快发现,一旦解释清楚,Linux 如何管理你服务器的内存其实是相当简单的。
了解服务器内存
为了监控我们服务器的内存使用情况,我们可以使用free命令,这个命令可以让我们查看在任何给定时刻内存的消耗情况。若不带任何选项地执行free命令,输出将以千字节为单位显示:

图 8.5:free命令的输出结果
我最喜欢的这个命令的变种是free -m,它以兆字节为单位显示内存的使用量。你也可以使用free g以吉字节为单位显示输出,但在大多数服务器上,输出的精度不足。依我看,添加-m选项能让free命令的输出更易于阅读:

图 8.6:free -m命令的输出结果
由于一切都以兆字节为单位显示,对我来说,阅读起来要容易得多。
刚开始看,可能会觉得这个服务器只有277 MB 的空闲内存。你会在第一行和第三列的free下看到这个数值。实际上,你真正需要关注的数字是available列下的数字,这里是2943 MB。那才是实际可用的内存。因为这个服务器总共有3925 MB 的 RAM(你会在第一行的total下看到),这意味着大部分内存是空闲的,这台服务器其实并没有在高负荷运行。
若要真正理解这些数字,需要一些额外的解释。你完全可以在此时停止阅读这一部分,只要记住available列表示的是应用程序可以使用的空闲内存量即可。不过,事情并没有那么简单。
从技术上讲,当你查看我的输出时,服务器实际上只有277 MB 的空闲内存。available列下列出的内存量实际上是被系统用作缓存的,但如果任何应用程序需要它,它将被释放。如果一个应用程序启动并需要较大内存来运行,内核会从这个缓存中提供一些内存。
Linux 与大多数现代系统一样,遵循“未使用的 RAM 就是浪费的 RAM”这一理念。未被任何进程使用的 RAM 会分配给所谓的文件系统缓存,用于提高服务器的运行效率。当数据需要写入存储设备时,并不会直接立即写入。相反,这些数据会先写入文件系统缓存(一个预留的 RAM 部分),然后在后台同步到存储设备。这样做使得服务器更高效的原因在于,存储在 RAM 中的数据相较于从硬盘读取,要写入和读取得更快。应用程序和服务可以在后台同步数据到硬盘,而不需要你等待。此外,这个缓存也适用于读取数据,比如当你第一次打开一个文件时,它的内容会被缓存。如果你再次读取该文件,系统会从 RAM 中获取,而不是每次都从存储设备加载。若你刚保存了一个新文件并立即访问它,很可能它仍然在缓存中,会从缓存中提取,而不是直接从硬盘读取。
为了理解图 8.6中显示的所有列,我将在下表中列出每列的含义:
| 列 | 含义 |
|---|---|
total |
服务器上安装的总内存量。 |
used |
使用的内存(来自任何来源)。计算方法如下:used = total - free - buffers/cache。 |
free |
没有被任何东西使用的内存,无论是缓存还是其他。 |
shared |
tmpfs以及其他共享资源使用的内存。 |
buff/cache |
用于缓冲区和缓存的内存量。 |
available |
应用程序可以使用的空闲内存。 |
您可能已经注意到在 图 8.6 中,还列出了一个名为 swap 的资源的内存使用情况。我们也来看看这个。我们将专门为它保留下一节,以确保我们理解它是什么,以及它对我们有什么作用。
管理 swap
swap 是我们从不想要使用的东西之一,但总是希望确保它是可用的。这有点像汽车保险,没有人会为购买它感到兴奋,但如果发生不好的事情,我们确实希望有它。甚至在管理员之间,关于 swap 在今天是否仍然相关还存在一些争论。无论别人怎么说,它绝对是相关的,因为它就像一种安全网。(而且现在磁盘空间更便宜了,所以将一些存储空间专门用于这项任务真的不是什么大问题,所以我们也可以这样做。)
那么它是什么? swap 基本上是一个在服务器内存饱和时充当 RAM 的分区或文件。如果我们正确管理服务器,希望永远不需要它,因为 swap 存储在硬盘上,比 RAM 慢几个数量级。但是,如果服务器出现问题,内存使用量飙升,swap 可能会防止服务器崩溃。当内存满时,内存不足杀手(OOM Killer)也可能自动激活,以终止使用大部分内存的行为不端进程,但我们尽可能不希望依赖它,而是确保有足够的 swap 以防内存耗尽。
在 Ubuntu 中,默认实现 swap 的方式是通过一个 swap 文件。在早期版本的 Ubuntu(具体来说是 16.04 及之前的版本),它是通过一个 swap 分区实现的。事实上,如果你有一个已经升级到 Ubuntu 22.04 的现有服务器,并且它在旧版 Ubuntu 上启动过,你可能仍然有一个 swap 分区,即使你正在运行最新的发布版。在 16.04 之后进行的新安装的 Ubuntu 将使用 swap 文件。
使用 swap 文件比使用 swap 分区好吗?我会说是的,它更受欢迎 - 尽管在性能方面你不会注意到任何差异。无论在服务器上 swap 是以 swap 文件还是分区的形式,它都不改变 swap 使用硬盘且比 RAM 慢的事实。与 swap 分区相比,swap 文件的一个好处是,它比 swap 分区更容易扩展或收缩。
总之,考虑到 swap 文件是未来首选的方法(并且是新的默认设置),我不会再覆盖创建 swap 分区的过程,因为没有必要再这样做了。
你的服务器的swap文件在/etc/fstab文件中声明(我们将在第九章,管理存储卷中更详细地讨论/etc/fstab文件)。在大多数情况下,你在安装过程中已经为你创建了swap文件。当然,如果出于某种原因你没有swap文件,你可以稍后添加一个。在一些云实例提供商的情况下,默认情况下你可能不会获得swap文件。在这种情况下,你需要自己创建一个swap文件(我们将在本节稍后讨论这个过程),然后使用swapon命令来激活它:
sudo swapon -a
当运行swapon -a命令时,它会在/etc/fstab中找到你的swap文件(如果那里列出了它),挂载并激活它供使用。这个命令的反向操作是swapoff -a,它会停用你的swap文件。除非你计划删除swap文件并创建一个更大的文件,否则通常不需要禁用swap。如果你发现服务器的swap不足,可能需要采取这种操作。
虽然有swap通常是一个好主意,但实际上有些应用程序更倾向于服务器完全没有swap。没有swap并不是一个常见的要求,但 Kubernetes 就是一个可能需要完全禁用swap的情况的好例子。事实上,如果你启用了swap,Kubernetes 的安装过程可能会报错(或者可能会失败)。对于 Kubernetes 集群来说,集群内的每个单独服务器本身就是一个特殊的情况,每个服务器都专门用于运行容器(这正是 Kubernetes 的功能;更多内容将在第十八章,容器编排中介绍)。
当你检查空闲内存时(提示:执行free -m),无论你是否有swap,都会看到swap被列出,但当swap被停用时,你会看到所有大小总计为零。
那么,如果你希望使用swap并且当前没有swap文件,应该如何创建一个呢?首先,你需要创建一个实际的文件来用作swap。它可以存储在任何地方,但通常/swapfile是理想的选择。你可以使用fallocate命令来创建这个实际的文件。fallocate命令会强制文件为特定大小:
sudo fallocate -l 4G /swapfile
在这里,我正在创建一个 4 GB 的swap文件,但你可以根据需要设置任何大小,以满足你的需求。接下来,我们需要准备这个文件以便用作swap。首先,我们需要修复文件的权限,因为我们需要这个文件比大多数文件更具限制性:
sudo chmod 0600 /swapfile
然后,我们可以使用mkswap命令将此文件转换为实际的swap文件:
sudo mkswap /swapfile
现在,我们已经在根文件系统上存储了一个方便的swap文件。接下来,我们需要挂载它。像往常一样,推荐将其添加到/etc/fstab文件中。以下是一个示例条目:
/swapfile none swap sw 0 0
从这一点开始,我们可以通过之前提到的swapon命令来激活新的swap文件:
sudo swapon -a
运行该命令后,swap文件应该已激活并正在使用。您可以通过运行free -m命令,查看是否列出了swap文件,并且其大小大于 0 来验证这一点。尽管我真心希望您不需要使用swap,但根据我的经验,这只是时间问题。了解在需要时如何添加并激活swap绝对是一个好习惯,不过大多数情况下,您应该没问题,因为在大多数平台上,首次安装 Ubuntu 时,swap会自动创建。如果您出于某种原因需要手动创建swap,我总是建议在服务器上至少设置 2GB,或者根据您的使用情况适当增加。
您应当密切关注swap的使用情况。当内存开始接近满时,服务器会开始使用swap文件。即使大部分 RAM 空闲,swap也会被部分使用,这很正常。但如果大量swap被使用,就应该调查一下(可能某个进程使用了比平常更多的内存)。
实际上,您可以控制服务器何时开始使用swap。Linux 服务器使用swap的频率被称为其swappiness。默认情况下,Linux 服务器的swappiness值通常设置为60。您可以通过以下命令验证这一点:
cat /proc/sys/vm/swappiness
swappiness值越高,服务器越有可能使用swap。如果将swappiness值设置为100,服务器会更频繁地使用swap;如果将其设置为0,则会很少使用swap。该值大致与使用的 RAM 百分比相关。
要实时更改此值,您可以执行以下命令:
sudo sysctl vm.swappiness=30
执行该命令后,swappiness值的更改将立即生效。然而,一旦重启服务器,该值将恢复默认。为了使更改保持生效,请使用您选择的文本编辑器打开以下文件:
/etc/sysctl.conf
在该文件中,通常不会默认包含与swappiness对应的行,但您可以手动添加。为此,请在文件末尾添加类似以下内容的行并保存:
vm.swappiness = 30
更改此值是性能调优领域中的多种技术之一。虽然默认值60对大多数情况可能足够,但可能会遇到运行性能密集型应用程序的情况,在这种情况下,您无法承受过度使用swap。在这种情况下,您可以尝试不同的swappiness值,并在性能测试中使用最适合的值。
在下一部分中,我们将关注另一个重要的指标:负载平均值。负载平均值可以帮助我们了解 CPU 的忙碌程度,从而更好地判断何时服务器过载,可能需要采取措施。
理解负载平均值
另一个在监控性能时需要理解的非常重要的话题是负载平均值,它是一系列数字,表示服务器在特定时间段内 CPU 使用率的趋势。你可能已经见过这些数字,因为负载平均值会出现在多个地方。例如,如果你运行htop工具,屏幕上会显示负载平均值。此外,如果你执行uptime命令,你也可以在该命令的输出中看到负载平均值。你还可以通过查看存储负载平均值的文本文件来查看它:
cat /proc/loadavg
就个人而言,我习惯使用uptime命令来查看负载平均值。uptime命令的主要功能是显示服务器运行的时间,并且每次你关机或重启服务器时,时间会重置。但是,除了显示服务器开机的时长,uptime命令还会显示当前服务器的负载平均值。
理解负载平均值一开始可能有点困惑,但你很快就会意识到它并不像看起来那么复杂。负载平均值是一组三个数字,每个数字对应一个时间段。从左到右,这些数字分别对应 1 分钟、5 分钟和 15 分钟。一个典型的负载平均值可能是以下这样的:
0.36, 0.29, 0.31
在这个例子中,我们在 1 分钟、5 分钟和 15 分钟的时间段内分别有0.36、0.29和0.31的负载平均值。特别地,每个数字表示在给定时间段内,有多少任务在等待 CPU 处理。因此,这些数字非常理想。服务器并不繁忙,因为几乎没有任务在任何时刻等待 CPU(每个数字都小于 1)。这与诸如总体 CPU 使用百分比之类的指标不同,你可能在其他平台的任务管理器中见过,甚至在像htop这样的 Linux 工具中也能看到。虽然查看 CPU 使用百分比可能有用,但问题在于,CPU 使用率会不断从高使用率变到低使用率,你可以通过运行htop一段时间来亲自观察。当任务进行某种处理时,你可能会看到 CPU 核心使用率飙升到 100%,然后迅速下降到较低的数字。然而,这并不能告诉你太多。而负载平均值则展示了在三个给定时间段内的使用趋势,这在判断服务器的 CPU 是否高效运行或是否因无法处理的工作负载而陷入困境时更加准确。
然而,主要的问题是,你应该在什么时候开始担心,这实际上取决于你服务器上安装了什么样的 CPU。你的服务器将有一个或多个 CPU,每个 CPU 都有一个或多个核心。对于 Linux 系统来说,这些核心无论是物理的还是虚拟的,都是一样的(即 CPU)。在我的案例中,我之前输出数据的机器有一个四核的 CPU。
你服务器上的 CPU 越多,它在任何给定时间能够处理的任务就越多,这也意味着它能够处理更高的负载平均值。
当某个时间段的负载平均值等于系统上的 CPU 数量时,这意味着你的服务器达到了 100% 的容量。它正在处理的任务数与它能够处理的任务数相等。例如,如果你有一个 8 核的 CPU,而负载平均值在某个时间段是 8,那么 CPU 在该时间段内就是 100% 满负荷运行的。如果你的负载平均值持续高于可用核心数,那时你可能就需要关注这个情况。偶尔达到服务器满载是可以接受的,但如果一直处于满载状态,那就需要引起警惕了。
我不太愿意使用陈词滥调的例子来充分说明这个概念,但我忍不住,所以还是用了这个。Linux 服务器上的负载平均值就相当于超市的结账区。超市会有多个收银台开放,顾客可以在这里支付并完成购物。在我所在地区典型商店的经验中,你会看到大约 20 个收银台,但每次只有两个收银员在工作,不过在这个例子中,我们假设每个收银台都有一个收银员在操作。
每个收银员一次只能处理一个顾客。如果等待结账的顾客多于收银员,排队就会开始积压,顾客会感到沮丧。在有四个收银员且此时有四个顾客的情况下,收银员将达到最大承载量,但这并不是什么大问题,因为没有其他顾客在等候。可能会加剧这个问题的是某个顾客使用支票支付和/或使用几打优惠券,这样结账过程就会变得更加耗时(类似于资源密集型进程)。如果有四个收银员,而有六个顾客在等待,那就意味着有两个顾客超出了商店同时能够处理的数量。在这种情况下,商店的结账区域将超出容量。这基本上就是负载平均值的工作原理。每个收银员就是一个 CPU,每个顾客就是一个需要 CPU 时间的进程。
就像收银员一样,每个 CPU 一次只能处理一个任务,有些任务会占用 CPU 更长时间。如果任务的数量恰好和 CPU 数量一样,那就无需担心。但如果排队开始积压,我们可能需要调查是什么导致了这种延迟。为了控制局面,我们可以雇佣一个额外的收银员(增加一个 CPU),或者请一个不满的顾客离开(终止一个进程)。
让我们看一个其他的负载平均值示例:
1.87, 1.53, 1.22
在这种情况下,我们不需要担心,因为我们的假设服务器有 4 个 CPU,而且在 1 分钟、5 分钟和 15 分钟的时间段内,没有任何一个 CPU 处于满负荷状态。即使负载始终高于 1,我们仍然有 CPU 资源剩余,所以这并不是什么大问题。如果我们拥有 AMD 的那些非常强大的 Threadripper CPU(可以包含大量的核心),那么这些数字就代表着极其低的负载。回到我们的超市比喻,前面例子中的负载平均值相当于有四个收银员在每一分钟内平均为几乎两名顾客提供服务。如果这台服务器只有一个 CPU,我们可能需要搞清楚是什么导致了队伍的积压。
虽然你可能理性地认为低负载平均值是件好事,但实际上,根据具体情况,它可能代表着一个非常大的问题。当我们部署服务器时,我们是为了完成某种工作。
无论这“工作”是托管一个应用程序,还是运行任务处理数据,我们的服务器需要在执行某些工作,否则我们就是在浪费钱。如果服务器的负载平均值下降到异常低的值,这可能意味着一个本应始终运行的服务已失败并退出。例如,如果你的数据库服务器的负载通常在 1.x 范围内,突然降到 0.x,这可能意味着你要么流量确实减少了,要么数据库服务器服务已经停止运行。这就是为什么总是建议为服务器制定基准,以便判断什么是正常的,什么不是。基准通常指的是资源使用情况。如果资源使用量显著高于或低于基准,这无论如何都是潜在的关注点。
总的来说,如果你还没有遇到过,作为 Linux 管理员,你会非常熟悉负载均值。它是你服务器利用情况的时间快照,帮助你理解服务器什么时候运行高效,什么时候出现问题。如果服务器无法应对你分配的工作负载,可能是时候考虑增加核心数(如果可以的话)或将工作负载分配到其他服务器上。在排查资源利用问题、规划升级或设计集群时,整个过程通常是从了解服务器的负载均值开始的,这样你可以为其指定的任务有效地规划基础设施。
现在我们已经讲解了需要监控的重要资源,以确保我们的服务器保持健康,让我们来看看一个有用的工具,它能让资源使用情况更加易于理解。
使用 htop 查看资源使用情况
想要查看服务器的整体性能时,没有什么比htop更好的工具了。虽然它通常不是默认安装的,但htop是我推荐每个人尽早安装的工具,因为它在查看服务器资源利用情况时是不可或缺的。事实上,它有如此的实用性,以至于我在本章的前面就已经多次提到过它,即便我们在本节才开始讨论它。它真的是一个很棒的工具。
如果你还没有安装htop,只需使用apt安装即可:
sudo apt install htop
当你在终端提示符下运行htop时,你会看到htop应用程序的完整显示。在某些情况下,以root身份运行htop可能会更有利,因为这样你可以获得额外的选项,比如能够终止进程,尽管这并不是必需的:

图 8.7:运行 htop
在htop显示的顶部,你会看到每个核心的进度条(我截图时使用的服务器只有一个核心),以及内存和swap的进度条。此外,顶部还会显示你的Uptime、Load average和当前正在运行的Tasks数量。htop显示的下半部分会列出服务器上正在运行的进程,字段中会显示每个进程消耗的内存或 CPU,以及正在运行的命令、运行该命令的用户和其进程 ID(PID)。我们在第七章《控制和管理进程》中讨论过 PID。要滚动查看进程列表,你可以按Page Up或Page Down,或者使用箭头键。此外,htop还支持鼠标操作,你可以点击顶部的列标题来按该标准对进程列表进行排序。例如,如果你点击MEM%或CPU%,进程列表将按内存或 CPU 使用率排序。显示的内容会每 2 秒更新一次。
htop 工具也是可以定制的。例如,如果您更喜欢不同的颜色方案,可以按 F2 进入 Setup mode,然后在左侧导航到 Colors,选择六种提供的颜色方案之一。其他选项包括添加额外的计量表、增加或删除列等。对于多核服务器,我特别觉得有用的一个调整是能够添加一个 CPU 平均负载条。通常,htop 会显示服务器上每个核心的计量表,但如果您有多个核心,您可能也会对平均值感兴趣。为此,请再次进入 Setup mode(按 F2),然后在 Meters 高亮时,右箭头选择 CPU average,接着按 F5 将其添加到左侧列。您还可以添加其他计量表,如 Load average、Battery 等。
根据您的环境,功能键可能在终端程序中无法正常工作,例如 htop,因为这些键可能已被映射到其他功能。例如,F10 用于退出 htop,但如果 F10 被映射到终端仿真器中的某个功能,这可能不起作用,使用虚拟机解决方案(如 VirtualBox)也可能会导致某些键无法正常工作。您还可以通过鼠标导航 htop,即使是在 SSH 连接中。这也意味着,您可以点击右下角的 Quit 来退出应用程序。
这是一个配置了 CPU 平均负载计量表的 htop 示例:

图 8.8:添加了 CPU 平均负载计量表的 htop
打开 htop 后,您将看到系统中每个用户的进程列表。当您遇到不知道是哪个用户/进程导致极端负载的情况时,这种方式非常理想。不过,一个非常实用的技巧(如果您想查看特定用户)是按下 u 键,这将打开 Show processes of: 菜单。在此菜单中,您可以使用上下箭头键选择一个特定用户,并按 Enter 仅显示该用户的进程。这将大大缩小进程列表。
另一个有用的视图是 树状视图,它允许您查看按父子关系组织的进程列表,而不仅仅是一个平坦的列表。在实际操作中,进程通常是由另一个进程生成的。事实上,Linux 中的所有进程都是从至少一个其他进程派生出来的,而这个视图直接显示了这种关系。在您停止一个进程,但它会立即重新生成的情况下,您需要知道该进程的父进程,以便阻止它自我恢复。按 F5 将切换到树状视图模式,再次按 F5 将禁用树状视图。
启用树状视图后,htop 将显示如下:

图 8.9:启用树状视图的 htop
正如我提到的,htop 默认每 2 秒更新一次其统计信息。就个人而言,我觉得这个更新频率是可以接受的,但如果你想改变刷新速度,可以在调用 htop 时使用 -d 选项,然后设置刷新间隔(以十分之一秒为单位)。例如,要使 htop 每 7 秒更新一次,可以通过以下命令启动 htop:
htop -d 70
要使用 htop 杀死一个进程,可以用上下箭头键选择你想要结束的进程,然后按 F9。会弹出一个新菜单,列出你可以通过 htop 发送给该进程的信号。如我们之前讨论过的,SIGTERM 将尝试优雅地终止该进程,而 SIGKILL 则会强制结束它。一旦选中你想发送的信号,按 Enter 发送,或者按 Esc 取消操作。
正如你所见,htop 非常有用,并且(在大多数情况下)已经取代了以前在许多管理员中流行的老旧 top 命令。top 命令在 Ubuntu 服务器中默认可用,值得一看,哪怕只是与 htop 做个比较。和 htop 一样,top 命令也会列出你服务器上正在运行的进程及其资源使用情况。虽然没有漂亮的仪表盘,且定制化较少,但 top 命令能完成相同的任务。然而,在大多数情况下,htop 可能是你未来的最佳选择。
总结
在本章中,我们学习了如何监控服务器的资源使用情况。我们首先了解了可以用来查看磁盘使用情况的命令,并学习了如何监控内存使用情况。我们还讨论了 swap,包括它是什么,为什么你需要它,以及如何在需要时手动创建 swap 文件。接着,我们看了负载平均值,并通过查看 htop 来结束本章,这是我最喜欢的一个工具,可以全方位查看服务器的资源使用情况。
在 第九章,管理存储卷 中,我们将深入探讨存储。在本章中,我们学习了如何查看使用情况,而在下一章中,我们将研究有关存储的更多高级概念,如格式化卷、添加额外的卷,甚至是 LVM。到时见!
相关视频
-
Linux 快速入门 – du 命令(LearnLinuxTV):
linux.video/du -
Linux 快速入门 – htop(LearnLinuxTV):
linux.video/htop -
Linux 快速入门 – 负载平均值(LearnLinuxTV):
linux.video/loadavg -
Linux 快速入门 – 理解内存使用情况(LearnLinuxTV):
linux.video/mem -
精选 Linux 工具 - ncdu(LearnLinuxTV):
linux.video/ncdu
进一步阅读
- Linux 吃掉了我的 RAM:
learnlinux.link/ate-ram
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第九章:管理存储卷
在我们服务器的存储管理中,似乎永远也不够用。虽然硬盘的容量每年都在增长,而且大容量硬盘比以往更便宜,但我们的服务器很快就会消耗掉所有可用空间。作为服务器管理员,我们总是尽力为服务器选择充足的存储空间,但随着业务需求的变化,无论我们如何规划,一个成功的企业始终会需要更多的存储空间。在管理服务器的过程中,你很可能会遇到需要增加额外存储的情况。但存储管理不仅仅是每次磁盘满了就添加新磁盘。提前规划同样重要,像逻辑卷管理器(LVM)这样的技术,只要尽早使用,就能极大简化你的工作。
LVM 本身就是本章将要讨论的一个概念,它能为你提供更多的灵活性来管理服务器。我还将带你了解其他一些概念,这些概念在你管理服务器存储和卷时无疑会派上用场。具体来说,本讨论将包括:
-
将额外的存储卷添加到文件系统
-
格式化和分区存储设备
-
挂载和卸载存储卷
-
理解
/etc/fstab文件 -
备份和恢复存储卷
-
使用 LVM
当你的服务器磁盘空间不足时,一个可能的解决方案是添加一个额外的存储卷。因此,本章的首要任务是探讨我们如何实现这一目标。
添加额外的存储卷
在某个时刻,你可能会遇到需要为服务器增加额外存储的情况。在物理服务器上,我们可以添加额外的硬盘;而在虚拟或云服务器上,我们可以添加额外的虚拟磁盘。无论是哪种情况,为了充分利用额外的存储,我们需要确定设备名称、格式化它并挂载。
在 LVM 的案例中(我们将在本章后面讨论),我们有机会扩展现有的卷,通常不需要重新启动服务器。但在添加新设备时,仍然有一个整体流程需要遵循。当你向系统添加额外存储时,应该问自己以下问题:
你需要多少存储空间? 如果你要添加一个虚拟磁盘,通常可以根据需要设定其大小,只要你在虚拟化平台的存储池中有足够的剩余空间。
它连接后,设备名称是什么? 当新磁盘连接到服务器时,系统会检测到并分配一个设备名称。在大多数情况下,会使用 /dev/sda、/dev/sdb 等命名方式。在其他情况下(如虚拟磁盘),命名可能不同,如 /dev/vda、/dev/xda,甚至其他名称。命名方案通常以字母结尾,每增加一个磁盘,字母就会递增。
你希望如何格式化存储设备? 在撰写本文时,ext4 文件系统是最常见的选择。然而,对于不同的工作负载,你可能需要考虑其他选项(例如 XFS)。如果不确定,使用 ext4 是比较稳妥的选择,但一定要了解其他选项,看看它们是否能带来对你使用案例的好处。ZFS 也是一个可以考虑的选项,尽管与其他选择相比,它相对较新。我们将在下一节中讨论格式化内容,格式化和分区存储设备。
对你来说这可能是常识,但文件系统这个词在 Linux 系统中有多重含义,具体取决于其上下文,这可能会让新手感到困惑。像我们这样的 Linux 管理员通常会用文件系统一词来讨论典型 Linux 系统中的文件和目录结构。然而,这个词也被用来描述如何格式化硬盘以供该发行版使用(例如,ext4 文件系统)。在本章中,我们将主要关注后者。
你希望它挂载在哪里? 新硬盘需要对系统和可能的用户可访问,因此你需要将其挂载(附加)到文件系统中的某个目录,以便你的用户或应用程序能够使用它。在本章中我们还将讨论的 LVM(逻辑卷管理),你可能希望将它附加到现有的存储组。你可以为新卷创建自己的目录,但我将在本章稍后讨论一些常见的位置。我们将在 挂载和卸载卷 部分中介绍挂载和卸载的过程。
让我们考虑一下前两个问题的答案。关于你应该增加多少空间,你需要研究你的应用程序或组织的需求,并找到一个合理的数量。对于物理硬盘,你基本上没有选择,除了决定购买哪种硬盘。对于虚拟硬盘,你可以更加节省,因为你可以添加一个小的硬盘来满足你的需求(以后可以随时增加更多)。
LVM 在虚拟硬盘上的主要优势是能够在不重启服务器的情况下扩展文件系统。例如,你可以从一个 30 GB 的卷开始,然后通过增加额外的 10 GB 虚拟硬盘来按 10 GB 的增量进行扩展。这种方法显然比一次性添加一个 200 GB 的卷要好,尤其是当你不确定这些空间是否都会被用到时。
LVM 也可以在物理服务器上使用,但通常还是需要重启,因为你需要打开机箱并物理连接硬盘。一些服务器支持热插拔,可以让你在不关闭服务器的情况下添加或移除物理硬盘,这是一个很大的优势。
接下来,可以使用fdisk -l命令找到设备名称。fdisk命令通常用于创建和删除分区,但它也能帮助我们确定新磁盘获得的设备名称。使用fdisk -l命令将显示相关信息,但你需要以root身份运行,或者使用sudo:
sudo fdisk -l
执行此命令会产生类似于以下的输出:

图 9.1:fdisk -l命令的输出
我总是建议在连接新设备之前和之后都运行fdisk -l。这样,哪个设备名称代表新设备就会更加明显。
另一个技巧是使用以下命令,该命令会随着你添加磁盘而自动更新输出:
dmesg --follow
只需启动命令,连接磁盘,观察输出。当完成时,按Ctrl + c键返回命令提示符。
你还可以通过lsblk命令找到新磁盘的设备名称。lsblk的好处是你不需要root权限,且它返回的信息是简化版的:

图 9.2:lsblk命令的输出
在一台典型的服务器上,第一个磁盘(基本上就是你安装了 Ubuntu Server 的磁盘)将被命名为/dev/sda,而额外的磁盘将被分配下一个可用的名称,例如/dev/sdb、/dev/sdc,以此类推(具体取决于你使用的硬盘类型)。如今,非易失性内存快速接口(NVMe)硬盘越来越常见,因此你可能会看到类似/dev/nvme0n1的设备名称。你还需要知道分区编号。磁盘的设备名称后面会有数字,表示单独的分区。例如,/dev/sda的第一个分区将被命名为/dev/sda1,而/dev/sdc的第二个分区将被命名为/dev/sdc2。这些数字是递增的,通常很容易预测。正如我之前提到的,你的设备命名规则可能会因服务器而异,尤其是当你使用独立磁盘冗余阵列(RAID)控制器或虚拟化主机(如 VMware 或 XenServer)时。如果你尚未在新磁盘上创建分区,你将不会看到名称末尾的任何分区编号。
现在,你已经添加并命名了一个额外的存储卷,我们可以继续进行设置过程。我们需要决定将其挂载到哪里,以及它的用途是什么。但在我们能够挂载存储设备之前,我们需要先在它上面创建至少一个分区,然后再对其进行格式化。我们将在下一节中处理这两个步骤。
格式化和分区存储设备
一旦你安装了物理磁盘或虚拟磁盘,你就能充分利用额外的存储空间。但为了使用磁盘,必须先对其进行格式化。为了确保我们格式化的是正确的磁盘,我们需要找到该设备的名称。正如你从上一节中了解到的,Linux 发行版使用特定的命名规则来命名磁盘。所以你应该已经知道新磁盘的设备名称。正如前面所解释的,你可以使用 sudo fdisk -l 命令来查看服务器上连接的存储设备的详细信息:
sudo fdisk –l
这将产生类似以下内容的输出:

图 9.3:使用 fdisk -l 查看服务器上的存储设备列表
在我的情况下,设备 /dev/sdb 是全新的——我刚刚将其添加到服务器中。由于我在本章中使用的是虚拟机示例,新的磁盘显示为 QEMU HARDDISK 型号。目前它没有任何分区;注意我们在它上方看到了一些与不同硬盘和分区相关的行,比如 /dev/sda3。而在 /dev/sdb 的描述下方没有类似的行。如果我们在该设备上有一个或多个分区,它们将会显示在输出中。
此时,我们已经知道哪个存储设备是新的——毫无疑问,在上一个示例中,它是 /dev/sdb。我们始终需要确保不要尝试格式化或重新分区错误的设备,否则可能会丢失数据。在这种情况下,我们可以看到 /dev/sdb 没有分区(而且在我添加它之前,这个卷并不存在),所以很明显我们要操作的磁盘是哪一个。现在我们可以在其上创建一个或多个分区,继续准备它以供使用。
创建分区
要在该设备上创建实际的分区,我们将使用带 sudo 的 fdisk 命令,并将设备的名称作为选项。在我的例子中,我将执行以下操作来处理磁盘 /dev/sdb:
sudo fdisk /dev/sdb
请注意,我在这里没有包括分区编号,因为 fdisk 是直接与磁盘交互的(而且我们还没有创建任何分区)。在本节中,我假设你有一个尚未分区的磁盘,或者你愿意清除的磁盘。当正确执行时,fdisk 会显示一条介绍信息并给出一个提示符:

图 9.4:fdisk 的主提示符
此时,你可以按下键盘上的 m 键来查看可以执行的命令菜单。在这个示例中,我将带你完成第一次设置新磁盘所需的命令。
我相信这不言而喻,但请注意 fdisk 可能带来的破坏性后果。如果你在错误的磁盘上运行 fdisk,可能会导致无法恢复的数据丢失。管理员通常会将像 fdisk 这样的工具熟记到一定程度,以至于它们的使用变成了肌肉记忆。但总是要花时间确保你正在对正确的磁盘运行这些命令。
在继续创建新分区之前,需要对主引导记录(MBR)和GUID 分区表(GPT)分区表进行一些讨论。在新硬盘上创建分区表时,你可以选择使用 MBR 或 GPT 分区表。GPT 是较新的标准,而 MBR 已经存在很长时间了,如果你长时间从事服务器工作,可能一直在使用 MBR。
你可能会看到 MBR 被称为 DOS,这是指较旧的分区结构。正如你可能已经知道的,DOS是磁盘操作系统(Disk Operating System)的缩写,但在本章节中我们并不是在指这个操作系统,而是指 IBM 几十年前提出的分区结构。例如,在使用fdisk时,它会将 MBR 分区结构称为 DOS。在本章节中,我们会尽可能使用 MBR 来指代较旧的分区样式,以避免混淆。
使用 MBR 分区表时,你需要考虑一些限制。首先,MBR 只允许你创建最多四个主分区。此外,它还将你限制在大约 2 TB 的磁盘容量内。如果你的磁盘容量是 2 TB 或更小,这不会成为问题。然而,大于 2 TB 的硬盘越来越常见。
另一方面,GPT 没有 2 TB 的限制,所以如果你有一个非常大的硬盘,那么 MBR 和 GPT 之间的选择几乎已经为你决定好了。此外,GPT 没有最多四个主分区的限制,因为使用 GPT 分区表的fdisk可以创建最多 128 个主分区。毫无疑问,GPT 正迅速成为新的标准!GPT 成为默认分区表只是时间问题,所以除非你有充分的理由,否则我推荐如果有选择的话使用 GPT。
当你第一次进入fdisk提示符时,你可以按o来创建 MBR 风格的分区布局,或者按g来创建更新的 GPT 风格的分区布局。正如我之前提到的,这是一个可能破坏性很大的过程,所以请确保你正在对正确的硬盘使用这个工具!确保按下与你选择的分区风格相对应的键,然后按Enter,这样我们就可以继续了。一旦你按下g或o,你应该会看到一条确认信息,表示你已创建了一个新的分区表。
接下来,在你做出选择并创建了 MBR 或 GPT 分区表之后,我们就可以继续了。然后,在fdisk提示符下,输入n,告诉fdisk你想要创建一个新分区。接着,你会被问到是否想要创建主分区或扩展分区(如果你选择了 MBR)。使用 MBR 时,你会想选择主分区作为第一个分区,然后可以使用扩展分区来创建更多的分区。如果你选择了 GPT,这个提示不会出现,因为它会将你的分区创建为主分区。
接下来出现的提示会要求你输入分区号,默认选择下一个可用的编号。按Enter接受默认值。随后,你将被要求输入分区的第一个扇区(按Enter接受默认值2,048),然后下一个提示会询问你要使用的最后一个扇区。如果你按Enter接受默认的最后一个扇区,分区将包含设备上剩余的所有空闲空间。如果你希望创建多个分区,可以在最后一个扇区提示时不接受默认值。你可以通过输入+符号后跟要使用的兆字节或吉比字节的数字,并且在数字后加上M表示兆字节,或者加上G表示吉比字节,来明确新分区的大小。例如,你可以输入+20G来创建一个 20 GiB 的分区。注意,+符号后面没有空格,20和G之间也没有空格。
此时,你将返回到fdisk提示符。要保存更改并退出fdisk,按下w然后按Enter。现在,如果你以root身份运行fdisk -l命令,你应该会看到你创建的新分区。以下是我某台服务器上fdisk命令的示例输出,给你一个完整过程的概念:

图 9.5:fdisk命令的示例运行
如果你犯了错误或者想要重新调整分区布局,你可以再次进入fdisk提示符,然后按g创建一个新的 GPT 布局,或者按o创建一个新的 MBR 布局。然后,重新按照步骤分区你的磁盘。可以多次练习,直到熟练掌握这个过程。
格式化分区
在你为新磁盘创建好分区布局并且满意后,你就可以开始格式化它了。现在我已经在新磁盘上创建了一个分区布局,运行sudo fdisk -l的输出将有所不同:

图 9.6:创建分区后运行sudo fdisk -l的另一个示例
请注意,现在我们添加了分区/dev/sdb1,它可以在输出中看到。接下来,我们可以继续格式化它。为此,我们使用mkfs命令。此命令需要特定的语法,输入mkfs后跟一个句点(.),然后输入你希望将目标格式化为的文件系统类型。以下示例将/dev/sdb1格式化为 ext4:
sudo mkfs.ext4 /dev/sdb1
你的输出将与以下截图中的内容类似:

图 9.7:使用 ext4 文件系统格式化卷
如果你选择了其他文件系统类型而不是 ext4,你可以在使用mkfs时替换为所选文件系统类型。以下示例将创建一个 XFS 文件系统:
sudo mkfs.xfs /dev/sdb1
一些文件系统,例如 XFS,默认不被支持,可能需要安装额外的软件包才能使用它们。以 XFS 为例,需要安装xfsprogs软件包。
现在,我们已经创建了一个或多个分区并格式化了它们,我们准备在服务器上挂载新创建的分区。在接下来的部分中,我将带你逐步了解如何挂载和卸载存储卷。
挂载与卸载卷
现在你已经为服务器添加了一个新的存储卷并格式化了它,你可以挂载这个新设备,以便开始使用它。为此,我们使用mount命令。这个命令允许你将一个存储设备(甚至是一个网络共享)附加到服务器上的本地目录。在挂载之前,目录必须是空的。mount命令,稍后我们将通过一个示例进行实践,基本上只需要你指定一个位置(目录)来挂载设备。但我们该将卷挂载到哪里呢?
通常,在你的 Ubuntu Server 安装中,默认会创建两个目录,用于挂载卷:/mnt 和 /media。虽然没有硬性规定媒体必须挂载到哪里,但这两个目录是文件系统层次结构标准(FHS)的一部分,FHS 在《第四章,导航和基本命令》中已经提到过。/mnt 和 /media 目录的目的在这个规范中已有定义。FHS 定义 /mnt 为暂时挂载文件系统的挂载点,而 /media 则是可移动媒体的挂载点。
简单来说,这意味着 /mnt 的预定用途是存放你通常大部分时间都挂载的存储卷,例如额外的硬盘、虚拟硬盘和网络附加存储。FHS 文档在描述 /mnt 时使用了“暂时”的术语,但实际上,这通常是你期望会存在一段时间的挂载位置。至于 /media,FHS 基本上是在指示可移动媒体(如闪存驱动器、CD-ROM 媒体、外部硬盘等)应当挂载在这里。
然而,重要的是要指出,FHS 所指示的挂载额外卷的位置只是一个建议。(也许是一个强烈的建议,但毕竟只是建议。)没有人会强迫你遵循它,世界的命运也不取决于你的选择。使用mount命令,你可以将额外的存储挂载到任何没有挂载或没有文件的地方。你甚至可以创建一个目录 /kittens 并在那里挂载你的磁盘,除了同事们的几声笑声,你不会遭遇任何后果。
通常,组织会自行制定额外磁盘挂载的位置方案。尽管我个人遵循 FHS 标准,但我曾在过去与一家公司合作时,遇到过一个自定义布局的例子。该公司在其服务器上使用了/store目录来挂载存储,这是他们在每台服务器上自行创建的目录。你使用什么样的方案由你决定;我唯一的建议是尽可能在不同服务器之间保持一致,至少为了保持理智。
mount命令通常需要以root身份执行。虽然有办法绕过这一点(你可以允许普通用户挂载卷,但我们暂时不讨论这个问题),通常情况下,只有root用户才能或应该挂载卷。正如我所提到的,你需要一个地方来挂载这些卷,因此,为了方便操作,我们可以使用以下命令创建一个名为/mnt/vol1的目录:
sudo mkdir /mnt/vol1
当你创建了一个目录(就像我这样做的)或决定使用一个现有目录后,你可以使用类似下面的命令来挂载一个卷:
sudo mount /dev/sdb1 /mnt/vol1
在这个例子中,我将设备/dev/sdb1挂载到目录/mnt/vol1。
当然,你需要调整命令,以引用你想要挂载的设备和你想要挂载的位置。提醒一下,如果你不记得服务器上有哪些设备,可以使用fdisk –l列出它们。
通常,mount命令要求你指定一个-t选项,后面跟着指定的类型。在我的情况下,如果我使用了-t选项,那么mount命令应该是以下内容,因为我的磁盘格式化为ext4:
sudo mount /dev/sdb1 -t ext4 /mnt/vol1
一个有用的技巧是在挂载设备之前和之后执行df –h命令。
虽然这个命令通常用于检查不同挂载点的可用空间,但它会显示挂载的设备列表,因此你可以在挂载设备后对比结果,确认设备是否已挂载。
在这个例子中,我使用了-t选项,并指定了我格式化该设备时使用的文件系统类型。在第一个例子中,我没有使用这个选项。这是因为,在大多数情况下,mount命令能够自动识别设备使用的文件系统类型并相应调整。因此,大多数情况下,你不需要使用-t选项。过去,你几乎总是需要它,但现在操作起来更简单了。我之所以提到这一点,是因为如果你在尝试挂载文件系统时遇到错误,提示无效的文件系统类型,你可能需要指定这个选项。可以查看mount命令的手册页,了解更多关于不同选项的信息。
当你使用完一个卷后,可以使用umount命令卸载它(单词unmount中缺失的n是故意的):
sudo umount /mnt/vol1
umount命令也需要以root身份或通过sudo运行,它可以让你将存储设备从文件系统中断开。为了确保此命令成功执行,卷不能正在被使用。如果正在使用,你可能会收到设备或资源忙碌的错误消息。如果你在卸载后执行df -h,你应该会看到文件系统不再出现在输出中,意味着它已经不再挂载。
手动挂载设备的缺点是,它们在下次服务器启动时不会自动重新挂载。为了确保挂载点在每次服务器启动时都可用,你需要编辑/etc/fstab文件,接下来我会引导你完成这个步骤。
理解/etc/fstab 文件
/etc/fstab文件是你 Linux 系统中的一个非常关键的文件。你可以编辑此文件,列出你希望在启动时自动挂载的其他卷。然而,这个文件的主要作用也是挂载你的主文件系统,所以如果在编辑时出错,可能会导致服务器无法启动(完全无法启动)。一定要小心。
分析/etc/fstab 文件的内容
当你的系统启动时,它会查看/etc/fstab文件以确定根文件系统的位置。此外,swap区域的位置也会从这个文件中读取,并在启动时挂载。系统还会读取此文件中列出的其他挂载点,一行一个,并进行挂载。基本上,几乎所有你能想到的存储都可以添加到这个文件中并自动挂载。即使是来自 Windows 服务器的网络共享也可以在这里添加。它不会对你评判(除非你打错字)。
作为示例,以下是我某台机器上/etc/fstab文件的内容:

图 9.8:查看/etc/fstab 文件的内容
当你安装 Ubuntu Server 时,/etc/fstab文件会为你创建,并为安装过程中创建的每个分区添加一行。在我用于获取示例fstab内容的服务器上,只有一个根文件系统分区,你还可以看到 swap 文件的位置。
每个分区通常使用通用唯一标识符(UUID)来标识,而不是你可能更习惯的/dev/sdaX命名约定,尤其是当你以前处理过存储设备时。在我的输出中,你可以看到 UUID dm-uuid-LVM-H8VEs7qDbMgv...,它指的是我的根文件系统,你还可以看到我有一个位于/swap.img的swap文件。
UUID 的概念已经存在了一段时间,但并没有任何限制阻止你用实际的分区名称(例如/dev/sda1或类似的名称)替换 UUID。如果你这么做,服务器依然会启动,你可能不会注意到什么不同(前提是没有打错字)。
现在,由于设备的名称可能会根据设备的物理位置(例如硬盘插入了哪个串行高级技术附件(SATA)端口,外部硬盘连接到哪个 USB 端口,等等)或它们的排序(虚拟磁盘的情况)而发生变化,因此 UUID 比常见的设备名称更受青睐。
再加上可移动媒体可以随时插入或移除的事实,你会遇到一种情况:你无法确定每个设备在任何时刻会被分配什么名称。例如,现在你的外部硬盘可能被命名为/dev/sdb1,但下次挂载时,如果你连接的其他设备占用了/dev/sdb1这个名称,它可能就不再是这个名字。这时 UUID 的概念就非常有用了。设备的 UUID 不会因为你重新排列硬盘顺序而改变(但如果重新格式化卷,它会改变)。如图 9.8所示,你可以使用blkid命令轻松列出卷的 UUID:
blkid
输出将显示附加到你系统上的每个设备的 UUID,你可以在每次向服务器添加新卷时使用此命令列出 UUID。这也是将新卷添加到/etc/fstab文件的第一步。虽然我之前说过使用 UUID 不是必须的,但强烈推荐使用,因为它可以避免你以后遇到麻烦。
每行fstab条目都被分为几列,每列由空格或制表符分隔。没有规定需要多少个空格来分隔每列;在大多数情况下,空格仅用于对齐每列,使其更易读。但至少需要一个空格。
在示例的fstab文件的第一列中,我们有设备标识符,它可以是每个设备的 UUID 或标签,用来区分其他设备。(你可以在使用mkfs命令格式化设备时,使用-L参数为设备添加标签。)在第二列中,我们有设备挂载位置。对于根文件系统,挂载点是/,这是 Linux 文件系统的起点,正如你所知。截图中的第三项(swap)的挂载点是none,这意味着这个设备不需要挂载点。第三列中,我们有文件系统类型,前两项是ext4,第三项是swap类型。
在第四列中,我们列出了每个挂载点的选项,并用逗号分隔。在这个例子中,每一行的选项只有一个。对于根文件系统,我们有一个选项errors=remount-ro,表示如果发生错误,系统会将文件系统重新挂载为只读模式。此类问题虽然罕见,但在出现故障时,能让系统尽量以只读模式继续运行。swap分区只有一个选项sw。这里可以使用许多其他选项,因此可以参考手册页获取完整的选项列表。在这一节中,我们会介绍一些常见的选项。
第五列和第六列分别指的是dump和pass,在我的系统中,每一行的值为0和0。dump分区几乎总是0,可以与备份工具一起使用来确定是否需要备份文件系统(0表示不备份,1表示备份)。在大多数情况下,只需将其保持为0,因为现在很少有工具会使用这个选项。pass字段指的是fsck检查文件系统的顺序。fsck工具用于扫描硬盘中的文件系统错误,尤其是在系统故障或计划扫描时。pass的可能值是0、1或2。如果是0,则文件系统不会用fsck检查。如果设置为1,则首先检查该分区。pass为2的分区优先级较低,最后被检查。一般来说,建议将主文件系统设置为1,其他分区设置为2。云服务器提供商使用0作为两个字段的值并不少见,这可能是因为如果磁盘确实要进行例行检查,启动时间会显著增加。而在云环境中,无法等待很长时间来启动服务器。
现在我们已经理解了典型fstab条目的所有列,可以开始将另一个卷添加到fstab文件中了。
添加到/etc/fstab 文件
要将另一个卷添加到fstab文件中,我们首先需要知道我们要添加的卷的UUID(假设它是硬盘或虚拟磁盘)。同样,我们可以使用blkid命令来完成这项操作:
blkid /dev/sdb1
注意,我使用了/dev/sdb1设备名称作为参数。这是因为我想专门获取我们添加的新设备的 UUID。该命令的输出将返回该设备的 UUID,然后我们可以将其添加到/etc/fstab文件中。记下这个 UUID,因为我们稍后会用到它。接下来,我们需要确定将卷挂载到哪里。现在可以创建一个目录,或者使用现有的目录。例如,可以创建目录/mnt/extra_storage来用于挂载:
sudo mkdir /mnt/extra_storage
到这里,我们应该已经拥有了所有信息,可以向fstab文件添加新的条目。为此,我们需要在文本编辑器中打开文件,然后在所有其他条目之后创建新的一行。如果没有偏好的编辑器,可以使用nano编辑器:
sudo nano /etc/fstab
例如,向 /etc/fstab 文件添加 /dev/sdb 条目后的内容如下所示:

图 9.9:向 /etc/fstab 文件添加新条目后的内容
在我的示例中,我创建了一行注释,简要说明了额外卷的用途(额外存储)。留下注释总是个好主意,这样其他管理员就能了解额外存储的目的。接着,我创建了一行,包含卷的 UUID、卷的挂载点、文件系统类型、defaults 选项,以及 dump/pass 的值 0 和 0。
defaults 选项是我之前未提及的。通过在 fstab 中使用 defaults 作为挂载选项,你的挂载将会一次性获得几个有用的选项,而无需单独列出它们。defaults 包含的选项有以下几项,值得解释:
-
rw:设备将被挂载为读写模式 -
exec:允许在此卷中的文件作为程序执行 -
auto:在启动时自动挂载设备 -
nouser:只有root用户能够挂载文件系统 -
async:输出到设备应为异步
根据你的需求,defaults 包含的选项可能适合,也可能不适合。你可以单独调用这些选项,以逗号分隔,选择你需要的选项。例如,对于 rw,你可能不希望用户能够更改内容。实际上,除非用户有非常强烈的需求来修改文件,否则我强烈建议你使用 ro(只读)选项。我就是在经历了一次整个卷完全被清空的事件后学到的这一点(而且没有人承认清空了内容)。这个卷中包含了非常重要的公司数据。从那时起,我要求所有内容都使用 ro,并创建了一个单独的 rw 挂载点,只有极少数(非常负责任的)人员才能访问。
exec 选项也可能不是最理想的。例如,如果你的磁盘卷是用于存储文件和备份的,你可能不希望在该位置运行脚本。通过使用 exec 的反向选项(noexec),你可以防止脚本运行,从而创建一个用户可以在该卷上存储文件,但无法执行存储在其中的程序的情况。
另一个值得解释的选项是 auto。auto 选项基本上告诉系统每次启动时自动挂载该卷,或者当你输入以下命令时自动挂载:
sudo mount -a
执行 sudo mount -a 时,会挂载 /etc/fstab 文件中所有设置了 auto 选项的条目。如果你为挂载使用了 defaults 选项,那么这些条目也会被挂载,因为 defaults 隐含了 auto。这样,你就可以在不重启服务器的情况下挂载所有应该挂载的文件系统(该命令可以随时安全执行,因为它不会中断任何已经挂载的内容)。
auto选项的反义词是noauto,可以替代使用。如你所猜测,带有noauto选项的fstab条目不会自动挂载,也不会在运行mount -a时被挂载。相反,带有此选项的条目需要手动挂载。
你可能会疑惑,既然使用noauto会使挂载失效,那么在/etc/fstab中包含这样的条目有什么意义呢?为了更好地解释这一点,下面是一个使用了noauto的fstab条目示例:
UUID=e51bcc9e-45dd-45c7 /mnt/ext_disk ext4 rw,noauto 0 0
假设我有一个外部磁盘,只有在进行备份时才会挂载。我不希望这个设备在启动时自动挂载(因为我可能并不总是把它连接到服务器),所以我使用了noauto选项。但由于我在/etc/fstab中确实有这个设备的条目,一旦连接了它,我可以随时通过以下命令轻松挂载:
sudo mount /mnt/ext_disk
请注意,我不需要包括设备名称或选项;只需提供挂载的目标路径即可。由于我在/etc/fstab文件中为一个设备添加了挂载路径/mnt/ext_disk,因此mount命令知道我指的是哪个设备。这避免了每次挂载设备时都需要输入设备名称和选项。因此,除了在启动时挂载设备外,/etc/fstab文件还成为了一个方便的地方,用来声明可能按需使用但并非总是连接的设备。
在我们继续之前,我想讲解最后一个选项users。当在/etc/fstab中与挂载一起使用时,它允许普通用户(非root用户)挂载和卸载文件系统。这样,使用此选项的挂载就不再需要root或sudo权限。请谨慎使用此选项,但如果你的设备存储的是非关键数据,且你不介意用户在挂载和卸载时拥有完全控制权限,它会非常有用。
虽然一开始可能会觉得通过文本文件来控制哪些设备挂载到系统上有些奇怪,但我想你会发现,能够查看一个文件以找出所有需要挂载的内容以及挂载位置,是非常方便的。只要管理员将所有按需设备添加到此文件中,它就可以成为一个方便的地方,用于概览服务器上正在使用的文件系统。作为额外的好处,你还可以使用mount命令(不加选项)来查看系统列出的所有挂载内容。试试看,然后我们在下一部分再见。
备份和恢复卷
由于我们正在处理服务器,存储在存储设备上的数据无疑非常重要。虽然在典型的环境中拥有一些用于测试的服务器是正常的,但我们的服务器通常承担着非常重要的任务。我可以根据亲身经历告诉你,永远不要对存储设备过于信任。事实上,我建议你根本不要信任它们。我认为所有存储设备都是临时的,因为硬盘是会坏的。如果你的重要数据仅仅存储在一个设备上,那是非常不安全的。在这一节中,我将讨论一些与备份相关的非常重要的话题。
首先,考虑一下 RAID 卷。在本章中我们没有讨论它,因为虽然这项技术仍然有其优势,但它已不再像以前那样流行。别误会,我并不是说 RAID 已经过时,它依然有其应用场景,只是它不再像过去那样普及。RAID 允许你将多个磁盘以不同的配置组合在一起,这样可以降低丢失数据的风险。
例如,RAID 1 级别确保两块硬盘始终保持相同的数据。如果其中一块硬盘发生物理故障,那么你实际上并没有丢失任何数据。当你在 RAID 中更换故障磁盘时,系统会使用新磁盘重建阵列,然后你将再次从可扩展性中获益。RAID 5 允许你使用多块硬盘,能够获得更多的存储空间,而 RAID 6 则与 RAID 5 类似,不过它允许两块磁盘在丢失数据之前故障,而不是只有一块。通常,RAID 的不同级别之间的区别就是允许多少块磁盘故障,超过这个数就会导致问题。
然而,RAID 存在一些严重的问题。最糟糕的是,它并不是备份解决方案。虽然它没有宣传自己是备份工具,但许多管理员错误地认为在使用 RAID 时他们的数据是安全的。事实上,RAID 提供的保护级别是非常有限的。如果发生雷暴天气,电源浪涌突破了你的浪涌保护器并烧坏了一块硬盘,那么很可能另一块硬盘也会被烧坏。一般来说,导致一块硬盘故障的环境因素很可能会导致其他硬盘故障。更糟的是,如果一个罪犯闯入你的服务器房,抢走了你的服务器并带走了它,那么小偷不光拿走了服务器,还带走了 RAID 中的所有磁盘,所以在各种情况下,RAID 可能并不能救你。RAID 确实有一定的好处,但更多的是一种便利,而非解决方案。
可靠的备份应该存储在服务器以外的地方。备份距离源服务器越远越好。如果你把备份存放在服务器室外的抽屉里,那肯定比一直连接外部硬盘更好(外部硬盘和内部硬盘一样,也可能会受到电力波动的影响)。但如果一个可怕的风暴摧毁了你的整个建筑,那么把备份硬盘存放在同一物理位置就会适得其反。
这可能看起来有些夸张。但实际上,我并不是这样。这些情况确实会发生。成功的备份方案具有韧性,并且能够让你迅速恢复服务器运行。备份你的数据更为重要,因为有些公司如果丢失了重要文件,甚至可能会倒闭,而这些文件可能包括那些能让公司维持运营的设计图纸。作为系统管理员,你需要制定一个备份方案,以应对尽可能多的情境。
一个有效的备份常规应包括多个层次。拥有外部硬盘作为备份是一个有用的方案,但为什么不准备多个呢,以防其中一个硬盘故障?也许你可以将其中一个备份存放在远离本地的地方,并每周交换本地和远程备份硬盘。此外,你还可以使用类似rsync的命令定期将服务器上的文件复制到远程服务器。你甚至可以考虑云备份方案,这也是一个很好的补充。
在这一节中,我无法为你的组织提供具体的备份方案,因为你的备份系统的布局将取决于组织的需求,而这些需求在不同公司之间是不同的。但我可以给你一些建议:
-
确保定期测试你的备份。仅仅有备份是不够的——备份必须有效!定期尝试从备份中恢复数据,以测试其有效性。
-
在你的备份方案中至少应有三层备份,其中至少有一层是离线的。这可以是外部硬盘、网络附加存储、云存储、将数据镜像到另一个位置的服务器,或者任何最符合你组织需求的方式。
-
考虑加密。尽管这超出了本章的范围,但如果你的备份落入错误的人手中,受到保护的数据可能会泄漏,并且被不希望看到这些信息的人读取。
-
检查你组织的政策,并确保你的备份方案符合相关要求。并非所有公司都有这样的方案,但如果你的公司有,这非常关键。考虑备份保留期(备份需要保存多长时间)以及备份更新的频率。如果你没有相关政策,可以咨询律师,了解你所在行业是否有法律要求的保留期限。
最重要的是,关键在于确保数据安全。到目前为止,在本书中,我们已经看过如何创建和挂载额外的卷,甚至在前面快速介绍过rsync。你已经学习了一些可以作为备份方案一部分的工具,在本书结束之前,你还将学习更多的方法。目前,在继续阅读本书时,请牢记这些要点,并考虑你学到的每一项新技能,看看是否可以将其作为备份方案的一部分应用,当然,如果适用的话。
LVM是我最喜欢的技术之一,它为我们的存储提供了额外的灵活性。事实上,我们现在就来看看它。
使用 LVM
你的组织需求会随着时间变化。虽然作为服务器管理员,我们总是尽力在配置资源时考虑长期增长,但预算和政策的变化似乎总是会给我们带来障碍。LVM 是我相信你会逐渐感激的技术。事实上,像 LVM 这样的技术正是让 Linux 在可扩展性和云部署方面成为赢家的原因。有了 LVM,你可以在线调整文件系统的大小,而无需重启服务器。
以以下场景为例。假设你在虚拟化的生产服务器上运行一个应用程序——这个服务器如此重要,以至于停机会给你的组织带来严重的经济损失。当服务器最初设置时,也许你为应用程序的存储目录分配了一个 100GB 的分区,认为它永远不会需要更多空间。现在,随着业务的增长,存储空间不仅用得非常多,而且即将用完!你该怎么办?如果服务器最初配置了 LVM,你可以添加一个额外的存储卷,将它添加到 LVM 池中,并扩展你的分区,所有这一切都无需重启服务器!另一方面,如果你没有使用 LVM,你就不得不找到一个服务器的维护窗口,使用传统方式添加更多存储,这意味着服务器会暂时无法访问。
对于物理服务器,你可以安装额外的硬盘并保持备用,尽管服务器不是虚拟化的,但仍然可以通过在线扩展文件系统来获益。此外,如果你的服务器支持热插拔,你仍然可以在不停机的情况下添加额外的存储卷。
正因如此,我必须强调,在虚拟服务器上的存储卷中,尽可能始终使用 LVM。让我再重复一遍:当你设置虚拟服务器时,一定要在存储卷上使用 LVM!如果你不这么做,最终当你可用空间开始耗尽,且不得不在周末工作以添加新磁盘时,你会后悔的。
这个过程可能涉及手动将数据从一个磁盘同步到另一个磁盘,然后将用户迁移到新的磁盘。相信我,这可不是一个愉快的经历。你可能现在觉得不需要使用 LVM,但你永远不知道。
开始使用 LVM
在通过 Ubuntu 安装程序设置新服务器时,你会在安装过程中看到使用 LVM 的选项。但是,更重要的是你的存储卷使用 LVM,指的是用户和应用程序存储数据的卷。如果你希望根文件系统也能受益于 LVM 的功能,LVM 是 Ubuntu Server 根文件系统的一个不错选择。为了开始使用 LVM,我们需要理解一些概念,特别是卷组、物理卷和逻辑卷。
卷组是赋予所有你希望与该 LVM 实现一起使用的物理卷和逻辑卷的命名空间。基本上,卷组是包含你整个 LVM 设置的最高级别名称。可以将其视为一种容器,能够容纳磁盘。例如,一个名为vg-accounting的卷组可能会用于会计部门存储文件的地方。它将包含这些用户使用的物理卷和逻辑卷。需要注意的是,你并不局限于仅使用一个卷组;你可以有多个卷组,每个卷组都有自己的磁盘和卷。
物理卷是一个物理或虚拟硬盘,它是一个卷组的成员。例如,假设的vg-accounting卷组可能包含三个 100 GB 的硬盘,每个硬盘都被视为物理卷。请记住,即使这些硬盘是虚拟的,在 LVM 的上下文中,它们仍然被称为物理卷。基本上,任何由卷组拥有的块设备都是物理卷。
最后,逻辑卷在概念上类似于分区。逻辑卷可以占用磁盘的一部分或全部,但与标准分区不同,逻辑卷也可以跨多个磁盘。例如,一个逻辑卷可以包括三个 100 GB 的磁盘,并配置为总共获得 300 GB 的存储空间。挂载后,用户可以像在标准磁盘的单个分区中一样存储文件。当卷空间满时,你可以添加一个额外的磁盘,然后扩展分区以增加其大小。用户会将其视为一个单一的存储区域,尽管它可能由多个磁盘组成。
卷组可以取任何你喜欢的名字,但我总是给它们取以vg-开头并以描述其用途的名称结尾的名字。如前所述,你可以有多个卷组。因此,你可以在同一台服务器上有vg-accounting、vg-sales、vg-techsupport(等等)。然后,你将物理卷分配给每个卷组。例如,你可以向服务器添加一个 500 GB 的磁盘,并将其分配给vg-sales。从此之后,vg-sales卷组拥有该磁盘。你可以以任何适合你的方式划分物理卷。然后,你可以使用这些物理卷创建逻辑卷,这些逻辑卷将供用户使用。
我认为在学习新概念时,最好通过实际示例来操作,所以我会带你走一遍这样的场景。在我的例子中,我通过 VirtualBox 在我的机器上创建了一个本地的 Ubuntu Server 虚拟机,然后在安装了发行版之后,我又添加了四个额外的 20 GB 磁盘。
如果你没有带有多个空闲物理磁盘的服务器,虚拟化是一个非常好的方式来尝试学习 LVM。
要在尚未使用 LVM 的服务器上开始使用 LVM,你首先需要至少有一个额外的(未使用的)卷,并安装所需的包,这些包可能已安装,也可能未安装在你的服务器上。要检查你的服务器上是否已安装所需的lvm2包,请执行以下命令:
apt search lvm2 |grep installed
如果没有安装(前一个命令的输出不包括[installed,automatic]),则可以使用以下命令安装lvm2包及其依赖项:
sudo apt install lvm2
接下来,我们需要盘点我们可用的磁盘。你可以像我们之前做过的那样使用fdisk -l命令列出它们。在我的例子中,我为服务器添加了几个新磁盘,现在我有/dev/sdb、/dev/sdc、/dev/sdd和/dev/sde可以使用。根据硬件或虚拟化平台的不同,你的磁盘名称会有所不同,因此确保相应地调整以下所有命令。
首先,我们需要配置每个磁盘以供 LVM 使用,将每个磁盘设置为物理卷。注意,我们不需要格式化存储设备,甚至不需要使用fdisk来设置它,在开始配置 LVM 的过程中,格式化实际上是稍后的步骤。pvcreate命令是我们用来配置磁盘以供 LVM 使用的第一个命令。因此,我们需要对所有希望用于此目的的驱动器运行pvcreate命令。例如,如果我有四个磁盘希望用于 LVM,我会运行以下命令来设置它们:
sudo pvcreate /dev/sdb
sudo pvcreate /dev/sdc
sudo pvcreate /dev/sdd
sudo pvcreate /dev/sde
如此类推,根据你计划使用的磁盘数量。
为了确认你是否正确按照步骤操作,可以使用pvdisplay命令以root身份查看你在服务器上可用的物理卷:

图 9.10:在示例服务器上运行pvdisplay命令的输出
截图中只显示了一个卷,因为它需要格式化以适应这页内容。如果你向上滚动,pvdisplay命令将显示更多输出。尽管我们有一些物理卷可以使用,但它们并没有被分配到卷组中。事实上,我们甚至还没有创建卷组。现在我们可以使用vgcreate命令来创建卷组,在命令中我们将为卷组指定名称,并将第一个磁盘分配给它:
sudo vgcreate vg-test /dev/sdb
在这里,我正在创建一个名为vg-test的卷组,并将我之前准备的一个物理卷(/dev/sdb)分配给它。现在我们的卷组已经创建完成,我们可以使用vgdisplay命令并加上sudo来查看它的详细信息,包括已分配磁盘的数量(现在应该是1):

图 9.11:在示例服务器上运行 vgdisplay 命令的输出
到这一步,如果你像我一样创建了四个虚拟磁盘,你会发现还有三个磁盘没有被分配到卷组。别担心,我们稍后再处理它们。现在暂时不需要考虑它们,我们有其他概念需要继续操作。
此时我们只需要做的就是创建一个逻辑卷并格式化它。我们的卷组可以包含我们分配给它的整个磁盘或其中的一部分。通过以下命令,我将从我添加到卷组的虚拟磁盘中创建一个 5 GB 的逻辑卷:
sudo lvcreate -n myvol1 -L 5g vg-test
这个命令看起来可能很复杂,但其实并不复杂。在这个例子中,我通过-n选项为我的逻辑卷命名为myvol1。由于我只想分配 5 GB 的空间,所以我使用了-L选项,然后用5g表示 5 GB。最后,我指定了这个逻辑卷将被分配到的卷组名称。你可以使用sudo运行lvdisplay命令来查看这个卷的相关信息:

图 9.12:在示例服务器上运行 lvdisplay 命令的输出
到这一步,我们在设置 LVM 方面应该已经完成了所有必要的操作。但在我们能够使用卷之前,仍然需要格式化它,类似于非 LVM 磁盘的处理。
格式化逻辑卷
接下来,我们需要格式化我们的逻辑卷,以便它可以使用。然而,像往常一样,我们需要知道设备的名称,以便知道我们要格式化的是哪个。使用 LVM 这一点很容易。lvdisplay命令已经给我们提供了这个信息;你可以在输出中看到它(在图 9.12中的第三行,位于LV Path下)。让我们用ext4文件系统来格式化它:
sudo mkfs.ext4 /dev/vg-test/myvol1
现在这个设备可以像其他硬盘一样挂载了。我将它挂载到/mnt/lvm/myvol1,但你也可以使用任何你喜欢的目录名称:
sudo mount /dev/vg-test/myvol1 /mnt/lvm/myvol1
为了检查我们的工作,执行df -h命令以确保我们的卷已经挂载并显示正确的大小。我们现在只有一个包含单个磁盘的 LVM 配置,因此这并不是特别有用。我给它分配的 5 GB 可能不会持续太久,但我们仍有一些剩余的空间可以使用,这些空间尚未被利用。通过以下lvextend命令,我可以调整我的逻辑卷大小以占用物理卷的剩余空间:
sudo lvextend -n /dev/vg-test/myvol1 -l +100%FREE
在这种情况下,+100%FREE是指定我们想要将剩余空间的全部用于逻辑卷的参数。如果操作正确,你应该看到类似以下的输出:
Logical volume vg-test/myvol1 successfully resized.
现在,我的逻辑卷正在使用我分配给它的整个物理卷。不过要小心,因为如果我有多个物理卷分配给它,那个命令会将这些物理卷上的所有空间都占用,从而使逻辑卷的大小是所有磁盘可用空间的总和。你不一定总是想这样做,但由于我只有一个物理卷,所以我不介意。现在,使用df -h命令再次检查你的空闲空间:
df -h
不幸的是,它没有显示我们为卷分配的额外空间。df命令的输出仍然显示的是卷调整前的大小。这是因为,尽管我们有了更大的逻辑卷,并且它已经分配了所有空间,但我们并没有实际调整驻留在该逻辑卷上的ext4文件系统的大小。为了做到这一点,我们将使用resize2fs命令:
sudo resize2fs /dev/mapper/vg--test-myvol1
前面命令中的双连字符是故意的,所以确保你正确输入了命令。
如果执行正确,你应该看到类似以下的输出:
The filesystem on /dev/mapper/vg--test-myvol1 is now 5241856 (4k) blocks long.
现在,当你执行df -h时,应该能看到新增的空间已经可用。最酷的部分是,我们在不重启服务器的情况下调整了整个文件系统的大小。在这种情况下,如果我们的用户已经使用了大部分的空闲空间,我们就能为他们提供更多空间而不会打断他们的工作。
然而,你可能还有其他尚未分配给卷组的物理卷。在我的例子中,我创建了四个物理卷,目前只在 LVM 配置中使用了其中一个。我们可以使用vgextend命令将额外的物理卷添加到我们的卷组中。在我的例子里,我将对剩下的三个磁盘执行此操作。如果你有额外的物理卷,可以按照我使用的命令来添加,但将我的设备名称替换为你的设备名称:
sudo vgextend vg-test /dev/sdc
sudo vgextend vg-test /dev/sdd
sudo vgextend vg-test /dev/sde
你应该看到类似以下的确认信息:
Volume group "vg-test" successfully extended
现在当你运行pvdisplay时,你应该能看到附加的物理卷,它们之前并未显示在此处。现在我们的 LVM 配置中有了额外的磁盘,提供了更多的选择。
我们可以立即将所有额外的空间分配给我们的逻辑卷,并像之前一样进行扩展。然而,我认为最好是保留一部分空间给我们的用户。这样,如果我们的用户再次用尽所有可用空间,我们就可以有一个紧急的备用空间,在我们找到长期解决方案的过程中,能够应急使用。此外,LVM 快照(我们很快会讨论)要求你在 LVM 设置中有未分配的空间。
以下示例命令将为逻辑卷添加额外的 10GB 空间:
sudo lvextend -L+10g /dev/vg-test/myvol1
最后,将空闲空间提供给文件系统:
sudo resize2fs /dev/vg-test/myvol1
对于非常大的卷,调整大小可能需要一些时间。如果你没有立即看到新增的空间,你可能会看到它逐渐增加,每隔几秒钟,直到所有新的空间完全分配完毕。
使用 LVM 删除卷
最后,你可能会好奇如何删除逻辑卷或卷组。对于这些操作,你可以使用lvremove或vgremove命令。不言而喻,这些命令是有破坏性的,但在你想删除一个逻辑卷或卷组时,它们是非常有用的。要删除逻辑卷,以下语法就能完成任务:
sudo lvremove vg-test/myvol1
基本上,你所做的就是给lvremove命令传递你的卷组名称,一个斜杠,然后是你想删除的该组中的逻辑卷名称。要删除整个卷组,下面的命令和语法应该是相当直观的:
sudo vgremove vg-test
你只能在逻辑卷未被使用时将其删除,这可能不会是你经常做的事情,但如果你需要废弃 LVM 组件,那么有一些命令可以帮助你做到这一点。
希望到现在为止,你已经被 LVM 的强大功能打动了。它为你的服务器存储提供了其他平台只能梦寐以求的灵活性。LVM 的灵活性是 Linux 在云市场中表现出色的众多原因之一。如果你之前没有使用过 LVM,这些概念可能一开始会让人难以理解。但得益于虚拟化,玩转 LVM 变得很容易。我建议你多练习创建、修改和销毁卷组和逻辑卷,直到你熟悉它。如果这些概念现在还不清楚,随着实践它们会变得更容易理解。
在这一节中,你看到了一些 LVM 如何为你带来好处的方法;它使你能够将服务器的存储提升到一个新的水平,甚至可以按需扩展和增长。然而,LVM 还有更多的技巧,它甚至允许你创建快照。我们接下来会介绍这个有用的功能。
理解 LVM 快照
LVM 快照允许你在某一时刻捕获一个逻辑卷并将其保留。创建快照后,你可以像挂载其他任何逻辑卷一样挂载它,甚至在出现故障时,将你的卷组还原到快照状态。实际上,如果你想测试一些可能对存储在卷中的文件产生风险的更改,但又希望有保障以防万一出错,你可以随时撤销更改并恢复到原始状态,LVM 快照正好能实现这一点。LVM 快照要求你在卷组中有一些未分配的空间。
然而,LVM 快照绝对不是一种可行的备份方式。在大多数情况下,这些快照最适合用作运行测试或在正式系统上部署更改之前,测试实验性软件的临时存储区域。在 Ubuntu 的安装过程中,你有机会创建 LVM 配置。因此,如果你为根文件系统使用了 LVM,你可以使用快照测试安全更新如何影响你的服务器。如果新的更新开始导致问题,你可以随时还原。当你完成测试后,你应该合并或删除快照。
那么,为什么我称 LVM 快照为临时解决方案,而不是备份呢?首先,和我们之前讨论的类似,如果备份存储在与备份目标相同的服务器上,那么备份就不安全。始终重要的是至少将服务器的备份保存到外部位置,最好是异地保存。但更糟糕的是,如果你的快照开始占用卷组中所有可用空间,它可能会损坏并停止工作。因此,这是一个你需要小心使用的功能,作为测试手段,在实验完成后要么还原,要么删除快照。不要让 LVM 快照长期存在。
当你使用 LVM 创建快照时,实际上是创建了一个新的逻辑卷,它是原始逻辑卷的克隆。最初,这个快照不会占用任何空间。但随着你运行服务器并操作卷中的文件,原始的数据块在你更改时会被复制到快照中,以保留原始逻辑卷。如果你不注意使用情况,可能会丢失数据,如果不小心,逻辑卷会填满。
为了通过例子展示,以下命令将创建一个myvol1逻辑卷的快照(称为mysnapshot):
sudo lvcreate -s -n mysnapshot -L 4g vg-test/myvol1
你应该会看到以下输出:
Logical volume "mysnapshot" created.
在这个例子中,我们使用了lvcreate命令,使用了-s选项(快照)和-n选项(允许我们为快照命名),我们指定了mysnapshot作为快照名称。我们还使用了-L选项来指定快照的最大大小,在这种情况下设置为 4GB。最后,我们给出卷组和逻辑卷名称,用斜杠(/)分隔。从这里,我们可以使用lvs命令来监控其大小。
由于我们在创建快照时创建了一个新的逻辑卷,因此可以像挂载正常的逻辑卷一样挂载它。如果我们只想提取一个文件而无需恢复整个文件系统,这非常有用。
那么,如何恢复快照呢?快照的一个主要优势是可以“回滚”到快照创建时的状态。本质上,这允许你测试服务器的更改,然后撤销这些更改。要回滚到某个快照,我们可以使用lvconvert命令:
sudo lvconvert --merge vg-test/mysnapshot
输出将类似于以下内容:
Merging of volume mysnapshot started.
myvol1: Merged: 100.0%
然而,需要注意的是,与在线调整逻辑卷大小不同,我们不能在快照正在使用时将其合并。如果这样做,变化将在下次挂载时生效。因此,你可以在合并前卸载逻辑卷,或在合并后卸载并重新挂载。之后,你会发现在下次运行lvs命令时,快照会被删除。
由于无法合并(回滚)正在使用的快照,如果快照是根文件系统的快照,必须重启服务器才能完成回滚。
如果你想让快照成为永久性的,即最终确定自快照创建以来所做的所有更改,我们可以使用lvremove命令。对于本节中的示例快照,我们可以使用以下命令使快照永久生效:
sudo lvremove vg-test/mysnapshot
正如你从命令的名称中可以推断的那样,lvremove命令删除快照。删除快照的操作实际上是使其更改生效,而前面提到的lvconvert命令则是回滚到快照创建时的状态。
LVM 快照无疑是一个有用的功能,尽管它不应被视为备份解决方案。我最喜欢使用这些快照的场景是在安装所有可用更新之前先拍摄根文件系统的快照。重启后,更新生效,我可以选择删除快照(如果一切正常),或者如果更新导致问题,则恢复到快照。即便如此,LVM 快照是一个在需要时可以使用的额外工具。
总结
高效管理服务器存储将确保系统持续平稳运行,因为满载的文件系统肯定会导致服务器崩溃。幸运的是,Linux 服务器提供了一个非常强大的存储管理工具集,其中一些工具是其他平台羡慕的。作为 Linux 服务器管理员,我们从 LVM 等技术和ncdu等工具中受益,还有很多其他工具。在本章中,我们探索了这些工具以及如何管理存储。我们涵盖了如何格式化、分区、挂载和卸载卷,以及管理fstab文件、LVM、监控磁盘使用情况等内容。
在我们 Ubuntu Server 系列的下一集里,我们将介绍如何连接到网络。我们将配置服务器的主机名,演示如何通过 OpenSSH 连接到其他服务器,并且了解 IP 地址配置。
相关视频
-
Linux Crash Course – fstab:
linux.video/fstab -
Linux Essentials Crash Course – 格式化和挂载存储卷:
linux.video/format-mount -
LVM 深度解析:
linux.video/lvm
深入阅读
- Ubuntu LVM 文档:
learnlinux.link/u-lvm
加入我们的社区在 Discord
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十章:连接到网络
Linux 网络席卷了 IT 行业。许多组织在其数据中心使用 Linux,包括物理服务器和云服务器。Ubuntu Server 是运行关键任务应用程序最受欢迎的选择之一,但如果没有稳定的网络将基础设施的各个组件连接起来,即使是最强大的服务器硬件也将无效。
到目前为止,在本书中我们一直使用单一的 Ubuntu Server 实例。在这里,我们开始对 Linux 网络进行两部分的探讨。本章将讨论与初始网络连接和远程管理相关的主题。我们将在第十一章、设置网络服务中继续学习更多的网络话题,届时我们将构建和配置额外的组件,使您的服务器能够更有效地进行通信,从而建立一个强大的基础网络,满足您未来多年的需求。
在我们 Ubuntu 探险的这一章节中,我们将讨论:
-
设置主机名
-
管理网络接口
-
分配静态 IP 地址
-
理解 Linux 名称解析
-
开始使用 OpenSSH
-
开始使用 SSH 密钥管理
-
通过配置文件简化 SSH 连接
在我们探索网络的过程中,首先应该为每台 Ubuntu 服务器分配一个唯一的身份;基本上,我们应该给它们一个名字,以便帮助区分它们之间的差异。
设置主机名
在安装过程中,您被要求为您的服务器创建一个主机名。具体来说,在初始设置过程中,字段被标记为 Your server's name。那时,我们的目标只是简单地设置一个 Ubuntu Server 实例,以便在本书中进行示例演示。现在,您可能考虑更改服务器的主机名。
当我们使用 OpenSSH 远程管理服务器(正如本章后面将要做的那样)时,主机名会显示在命令行上。如果所有服务器的名称都相同,这可能会让人感到非常困惑。更重要的是,服务器的主机名赋予了它一个身份。在实际的 Ubuntu Server 生产部署中,每台服务器应该有其特定的用途,并相应地命名。通常,组织会有自己的命名方案。比如,公司的 web 服务器可能被命名为类似于 webserver-01,或者使用完全限定的域名,例如 webserver-01.example.com。
在本书中,我不会假设使用任何特定的命名方案,因此,当我们讨论更改主机名时,你可以根据需要调整名称。如果你没有命名方案(但想要创建一个),可以尽情发挥创意。我见过许多不同的命名方式,从以卡通人物命名服务器(谁不想有一台名为daffy-duck的服务器?)到希腊神话中的神祇。一些公司选择更为简单的命名方案,使用由连字符分隔的一系列字符,其中包含服务器所在机架的代码,以及服务器的用途。如果你还没有命名方案,可以自己创造一个。不管你最终选择什么,我都不会评价你。
正如我之前提到的,服务器的主机名就是它的身份标识。它向网络中的其他设备标识你的服务器。虽然像ubuntu这样的简单主机名对于只有一个主机的情况是可以接受的,但如果在你的网络中每台 Ubuntu 服务器都保留默认的主机名,很快就会变得混乱。为每台服务器命名一个描述性的名称有助于你区分它们。但是,服务器的名称不仅仅是主机名,更多内容将在第十一章,设置网络服务中讨论,当我们讲解 DNS 时会详细说明。现在,我们将介绍如何查看和配置主机名,等到我们讲到 DNS 时,你可以将主机名正式化并进行 DNS 分配。
那么,如何查看你的主机名呢?一种方法是直接查看你的 shell 提示符;你可能已经注意到,主机名已经包含在提示符中。虽然你可以通过许多方式自定义 shell 提示符,但默认情况下会显示当前的主机名。然而,根据你为服务器命名的方式,它可能会或不会显示完整的名称。通常,默认的提示符(如果你在想的话,它被称为PS1 提示符)只显示主机名,直到第一个句点为止。例如,如果你的主机名是dev.mycompany.org,那么提示符只会显示dev。要查看完整的主机名,只需输入hostname命令:

图 10.1:hostname 命令的输出
更改主机名是相当简单的。我们可以使用hostnamectl命令作为root用户或通过sudo来执行。例如,如果我想将主机名从dev.mynetwork.org更改为dev2.mynetwork.org,我会执行以下命令:
sudo hostnamectl set-hostname dev2.mynetwork.org
这很简单,但这个命令究竟做了什么呢?嗯,我很想给你一个复杂的概述,但实际上它只是更改了一个文本文件的内容(具体来说,是/etc/hostname文件)。你可以通过在更改前后使用cat命令查看这个文件的内容来验证这一点:
cat /etc/hostname
你会看到这个文件只包含你的主机名。
一旦你更改了主机名,执行某些命令后,可能会看到类似以下的错误信息:
unable to resolve host dev.mynetwork.org
此错误意味着计算机无法再解析您的本地主机名。这是因为/etc/hostname文件并不是唯一存放您主机名的文件;它还被引用于/etc/hosts。不幸的是,hostnamectl命令不会为您更新/etc/hosts,因此您需要自己编辑该文件以消除错误。以下是一个示例服务器上/etc/hosts文件的示例:

图 10.2:来自/etc/hosts文件的示例内容
前两个条目在此示例中指代本地机器本身。本地主机地址,也称为环回地址,允许机器直接访问自身。如果您对127.0.0.1地址使用ping命令,回复将来自执行该命令的机器,而不是网络中的另一台主机。在第一行上,我们有以下内容:
127.0.0.1 localhost
如果您要使用任何网络命令尝试与本地服务器通信,例如 ping 127.0.0.1或localhost,则该行上的/etc/hosts文件声明此通信是针对底层服务器本身的。
在示例截图的第二行中,我们有以下内容:
127.0.1.1 dev.mynetwork.org dev
根据您的配置,例如是否使用物理服务器、虚拟化平台或云服务器提供商,该行可能存在,也可能不存在。如果缺少该行,您可以添加它,但我们稍后会更详细地讨论这个问题。
本质上,该特定行标识出本地服务器还可以通过 IP 地址127.0.1.1、完全限定域名dev.mynetwork.org以及简化形式dev访问。完全限定域名包括服务器名称(在此例中为dev)以及组织的域名(例如此示例中的mynetwork.org)。这使您可以通过使用名称dev.mynetwork.org或简化形式dev直接从该服务器 ping 本地服务器。
如果您没有要与服务器一起使用的域名,可以在/etc/hosts文件中省略完全限定的域名。因此,在我们的示例中,如果没有域名,该行将如下所示:
127.0.1.1 dev
回到我们更改服务器主机名的示例,我提到您可以使用hostnamectl命令来执行此操作,但该命令不会为您更新/etc/hosts文件,它只会更新/etc/hostname文件。最佳做法是同时更新/etc/hosts文件以保持一致。您可以完全避免使用hostnamectl命令,并手动编辑/etc/hosts和/etc/hostname文件,这实际上是我更喜欢的方法。如果我必须手动编辑文本文件,无论是否使用hostnamectl命令,我认为最好还是使用文本编辑器来处理。
主要的收获是,给你的服务器赋予一个合理且与其在网络中角色相匹配的身份。在典型的组织中,你会有 Web 服务器、文件服务器、数据库服务器等等。一个一致且合乎逻辑的命名方案会使得所有事情变得更加简单。
现在我们已经学会了如何为服务器赋予身份,接下来可以学习如何管理网络接口。
管理网络接口
网络对于服务器基础设施至关重要。如果没有网络,服务器无法相互通信,用户也无法访问它们。为了让服务器连接到网络,需要安装网络接口。大多数服务器会安装标准的有线以太网适配器,使你能够插入网络电缆并将其连接到交换机。如果我们的服务器硬件已被 Ubuntu 正确识别,那么这一过程基本上是自动完成的。然而,自动配置并不总是理想的。也许我们希望自定义 IP 地址或与连接相关的设置。
首先,我们需要了解如何查看当前网络卡在服务器中生效的连接参数。这是本节的主要目标。我们可以使用两个基本命令来实现:ip(推荐使用)和 ifconfig(这是在 Ubuntu 的早期版本中使用的方法,现在不再推荐使用)。
我们可以通过 ip 命令查看和管理我们的网络接口信息。例如,我们可以使用 ip addr show 来查看当前分配的 IP 地址:
ip addr show
这将产生类似于以下截图的输出:

图 10.3:使用 ip addr show 命令查看 IP 信息
输入该命令后,你应该会看到与可用的网络接口及其当前状态相关的输出。另外,你可以将命令简化为 ip a(无论哪种方式,输出都会相同)。从输出中,我们可以看到一些有用的信息,比如每个设备的 IP 地址(如果有的话),以及它的 MAC 地址。
使用 ip 命令,我们还可以管理接口的状态。我们可以将设备关闭(阻止它连接到网络),然后再将其启动:
sudo ip link set enp0s3 down
sudo ip link set enp0s3 up
在这个例子中,我只是切换了接口 enp0s3 的状态。首先,我将其关闭,然后再将其重新启动。
启动和关闭接口是好的,但那种命名规则是什么情况呢?对于那些习惯于早期版本使用的命名方案(如 eth0、wlan0 等)的用户来说,Ubuntu 22.04 使用的命名规则可能会显得有些奇怪。由于 Ubuntu 基于 Debian,因此它采用了从 Debian 9.0 开始引入的新命名规则。
新的命名约定被引入是为了使接口命名更加可预测。虽然你可能认为像eth0这样的名称比enp0s3更容易记住,但这个变化有助于使名称在启动时保持一致。当你向 Linux 系统添加新的网络接口时,其他接口名称发生变化的可能性始终存在。
例如,如果你在一台服务器上安装了较旧的 Linux 版本,并且该服务器有一个网络卡(eth0),然后你添加了第二个网络卡(命名为eth1),那么如果在下次启动时接口名称顺序发生了变化,可能会导致你的配置出现问题。假设一个接口连接到了互联网,另一个连接到了交换机(基本上,你拥有一个互联网网关)。如果接口顺序错乱,整个办公室的互联网连接将会中断,因为你编写的防火墙规则应用到了错误的接口上。显然,这并不是一种愉快的体验!
过去,Ubuntu 的早期版本(以及 Debian,甚至 CentOS)选择使用udev来使网络接口名称保持不变,以解决这个问题。如今,这已经不再必要,但我还是想在这里提一下,以防你碰到的是一个老版本的服务器。这些老服务器通过存储在以下文件中的配置来实现接口名称的固定:
/etc/udev/rules.d/70-persistent-net-rules
这个文件在一些流行的 Linux 发行版的早期版本中存在(包括 Ubuntu),作为解决该问题的一个变通方法。这个文件包含了一些信息,用来识别网络接口的特定属性,因此每次启动时,它都会使用相同的名称。因此,你所熟悉的eth0始终会是eth0。如果你使用的是较旧版本的 Ubuntu 服务器,应该能够自己查看到这个文件。下面是这个文件在老旧安装中的一些示例输出:
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*",
TTR{address}=="01:22:4e:a5:f2:ec", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"
如你所见,它是通过卡的 MAC 地址来识别eth0的。但是如果我想将这台机器的镜像拍摄并重新部署到另一台服务器上,这就成了一个小问题。这是一个常见的做法——我们管理员通常不会轻易从头开始,除非没有选择。如果另一台服务器与新服务器的需求足够相似,克隆它将是一个可行的选项。然而,当我们将镜像恢复到另一台服务器时,/etc/udev/rules.d/70-persistent-net-rules文件也会一起迁移。我们很可能会发现,新的服务器的第一个网络接口会被命名为eth1,即使我们只有一个接口。
这是因为文件已经将设备指定为eth0(它引用了系统中不存在的设备),所以我们需要自己修正这个文件以重新占用eth0。我们可以通过编辑rules文件,删除包含系统中不存在的网卡的那一行,然后将剩余行中的设备名称改回eth0来完成这个操作。
新的命名方案从systemd v197及更高版本开始生效(如果你之前没有了解过,本书早些部分提到过,systemd是 Ubuntu 用于管理进程和各种资源的底层框架)。大多数情况下,新的命名规范参考了网络卡在系统总线上的物理位置。
因此,它获得的名称除非你真的将网卡移除并将其放入系统板上的其他插槽,或者在虚拟化管理程序中更改虚拟网络设备的位置,否则无法更改。
简单概述一下网络名称的组成:en表示以太网,wl表示无线网络。因此,我们知道我之前提到的示例接口(enp0s3)代表的是一个有线网卡。p表示使用的是哪条总线,p0表示系统的第一条 PCI 总线(编号从 0 开始)。接下来是s3,它代表的是 PCI 插槽 3。把这些放在一起,enp0s3代表的是系统第一条总线上放置在 PCI 插槽 3 的有线网络接口卡。新的命名规范的详细内容甚至可以单独成章,但希望这能给你一个大概的了解。网上有更多关于这个细节的文档,如果你有兴趣了解更深入的信息,可以查看进一步阅读部分。这里需要强调的重点是,由于新的命名方案基于网卡的物理位置,因此它不太可能会突然改变。实际上,只要你不物理地更换网卡在机箱内的位置,它是不会改变的。
回到我们如何管理网络接口,另一个值得讨论的命令是ifconfig。
ifconfig 命令是 net-tools 工具套件的一部分,该套件大部分已被弃用。它的替代工具是 iproute2 套件,其中包括我们已经讨论过的 ip 命令。总结来说,这意味着你应该使用 iproute2 套件中的命令,而不是 ifconfig 之类的命令。问题是,如今大多数管理员仍然使用 ifconfig,而且这种趋势没有减弱的迹象。事实上,net-tools 套件已被推荐弃用多年,许多今天出厂的 Linux 发行版仍然默认安装这个套件。那些没有安装它的发行版,提供它作为额外的软件包供你安装。在 Ubuntu Server 22.04 中,net-tools 包不再默认安装,但如果你想手动安装,它仍然可用。不过,我不推荐安装它,因为它已被弃用,不再应该使用。
诸如 ifconfig 之类的命令之所以在被弃用后仍然存在,通常归结于 改变是困难的 这种心态,但仍有相当多的脚本和程序在使用 ifconfig,因此在这里讨论它是有价值的。即使你立即停止使用 ifconfig,并从今以后改用 ip,你仍然会在旅途中遇到这个命令,所以了解一些例子还是很有帮助的。如果你碰到老旧的服务器,了解旧命令也能帮你解决问题。
首先,当单独执行 ifconfig 且没有任何选项时,它会打印有关你的网络接口的信息,就像我们之前使用 ip addr show 所做的那样。这看起来很简单。
如果你在使用普通用户时无法通过 ifconfig 查看接口信息,可以尝试使用完整路径的命令(包括完整路径):
/usr/sbin/ifconfig
/usr/sbin 目录可能在你的 $PATH 中,也可能不在(这是一个 shell 查找命令的目录集合),所以如果你的系统无法识别 ifconfig,使用完整路径命令应该能够输出你想要的结果,如下所示:

图 10.4:使用 ifconfig 命令查看接口信息
其次,就像我们之前练习的 ip 命令一样,我们也可以通过 ifconfig 来禁用或启用一个接口:
sudo ifconfig enp0s3 down
sudo ifconfig enp0s3 up
当然,ip 和 ifconfig 还有其他选项和变化,所以如果你需要更多信息,可以随时查阅它们的手册页。在本节中,主要需要记住的是如何查看当前的 IP 分配,以及如何启用或禁用一个接口。
尽管我们的网络接口非常有用,但如果没有分配 IP 地址,它们就毫无用处。虽然网络通常会使用 DHCP 来处理这项工作,但在下一节中,我们将看看如何分配静态 IP 地址。
分配静态 IP 地址
对于服务器来说,确保 IP 地址固定且不发生变化非常重要。如果 IP 地址发生变化(例如没有保留的动态租约),用户将会遇到中断,服务将会失败,甚至整个网站可能无法访问。当你安装 Ubuntu Server 时,它会从 DHCP 服务器获取一个动态分配的租约,但在你配置好服务器并准备投入生产之前,重要的是要立即设置一个永久的 IP 地址。这个规则的唯一例外是基于 Ubuntu 的 VPS。提供这些服务器的云服务商会有自动系统为你的新 VPS 声明一个 IP 地址,并且已经配置好使其保持不变。但对于你自己管理的虚拟或物理服务器来说,除非你在安装时已经配置了静态 IP 地址,否则它们一开始会是动态地址。
在大多数情况下,你的办公室或组织会有一个 IP 地址方案,其中会列出可用于静态分配的 IP 地址范围。如果你没有这样的方案,重要的是要创建一个,这样当你添加更多服务器时,会减少后续的工作量。我们将在第十一章,设置网络服务中讨论如何设置 DHCP 服务器和 IP 地址方案,但现在我会给你一些快速提示。你的 DHCP 服务器通常会有一个 IP 地址范围,自动分配给任何请求分配的主机。在为服务器设置静态 IP 时,你需要确保所选择的 IP 地址不在 DHCP 服务器分配的范围内,这样就不会在网络中产生重复的 IP 地址。例如,如果你的 DHCP 服务器分配的 IP 范围是从10.10.10.100到10.10.10.150,你就应该选择一个不包含在该范围内的 IP 地址来给服务器使用。
给网络主机(包括你的服务器)分配固定地址有两种方法。我之前提到过第一种方法,即使用静态 IP 分配。使用这种方法,你可以任意选择一个没有被任何设备使用的 IP 地址,然后配置你的 Ubuntu 服务器使用该地址。在这种情况下,服务器不会向网络的 DHCP 服务器请求 IP 地址,而是直接使用你分配给它的地址。这就是我将在本节中讲解的方法。
将服务器分配固定地址的另一种方法是使用静态租约。这也被称为DHCP 保留,但我更倾向于使用前者的术语。通过这种方法,您配置 DHCP 服务器将特定的 IP 地址分配给特定的主机。换句话说,您的服务器将从本地 DHCP 服务器请求 IP 地址,您的 DHCP 服务器被指示每次请求时都给服务器一个特定的地址。这是我喜欢的方法,因为它使得您的 DHCP 服务器成为网络上分配的 IP 地址的唯一真实来源。我会在第十一章,设置网络服务中详细介绍这一点。
然而,并非总是有选择的余地。作为 Linux 管理员,您可能并不负责 DHCP 服务器。在许多组织中,管理服务器的管理员通常与管理网络的个人不同。
如果您无权设计网络,您将使用由网络管理员提供的 IP 地址,并根据他们给出的参数配置您的 Ubuntu 服务器来使用它。
在过去几年中,我们用来自定义服务器 IP 地址的方法已经从过去的方式改变了。自 Ubuntu 17.10 发布以来,即 2017 年,配置 IP 地址设置现在通过 Netplan 完成。过去,我们会通过 NetworkManager 配置网络,但这只在 Ubuntu 桌面版中默认安装。通过 Netplan,网络接口的配置文件现在以 YAML 格式存储在/etc/netplan目录中。本书不涵盖 YAML 格式本身的解释,但其语法非常易于理解,因此您在配置网络接口时无需深入理解这种格式。如果列出/etc/netplan目录的内容,您应该至少看到一个文件,通常命名为00-installer-config.yaml或50-cloud-init.yaml。文件可能以不同的名称保存,因此请检查/etc/netplan目录的内容以查看文件在您那里的名称。在我的一台服务器上,我在/etc/netplan/00-installer-config.yaml文件中看到以下内容:
# This is the network config written by 'subiquity'
network:
ethernets:
enp0s3:
dhcp4: true
version: 2
我们已经可以从这个默认文件中获取一些明显的信息。首先,开头的注释提到了subiquity,这是 Ubuntu 服务器安装程序的官方名称,在您使用从 ISO 文件创建的引导介质安装分发时使用。
更重要的是,我们可以看到这台特定服务器配置为使用 DHCP 来获取 IP 地址,这可以从以下一行中看出:
dhcp4: true
我们还可以看出,这个配置文件与接口enp0s3相关。综合来看,这个文件告诉我们接口enp0s3已配置为通过 DHCP 自动获取 IP 地址。如果我们想将此配置更改为静态 IP 地址,首先应备份文件:
sudo cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak
这样,如果我们犯了错误,我们可以通过将备份文件重命名为原文件名来轻松恢复原始文件。这是一个很好的做法,适用于我们编辑的任何文件。能够恢复之前的配置是我们进行任何更改时的最佳实践。我们需要做的第一项更改是删除以下这一行(或者将其改为false):
dhcp4: true
实质上,要设置静态 IP,我们将用特定于我们配置的详细信息替换那一行。这是一个为静态 IP 地址配置的文件示例:
# This is the network config written by 'subiquity'
network:
ethernets:
enp0s3:
**addresses:** **[****192.168.100.50****/24****]**
**gateway4:****192.168.100.1**
**nameservers:**
**addresses:** **[****192.168.100.1****,** **192.168.100.2****]**
version: 2
在这个示例中,我已经将四行加粗,这些行是替代dhcp4: true行所添加的。首先,我们设置实际的 IP 地址:
addresses: [192.168.100.50/24]
在这里,我使用了一个示例 IP 地址192.168.100.50/24。在你的环境中,你需要确保选择的 IP 地址位于你希望服务器加入的网络范围内。正如我之前提到的,你选择的 IP 地址不能在自动分配 IP 地址的 DHCP 范围内。如果你的网络上 DHCP 服务器的示例范围是从192.168.100.100到192.168.100.150,那么前面的 IP 地址就没有问题。这里选择的192.168.100.50在这个范围外,因此我们不需要担心会有其他设备被分配到这个地址。
我们还添加了/24来声明 IP 地址是 24 位子网的一部分,这是相当标准的,除非你的网络管理员设置了更大的范围。/24网络等同于 C 类网络,如果这个说法更熟悉的话。这也解决了子网掩码的问题,我们这里不需要设置,因为/24意味着子网掩码为255.255.255.0(如果你更熟悉传统的网络分类样式,它会像 IP 地址一样显示子网掩码)。我们将在第十一章,设置网络服务中讨论子网问题;不过,完整的子网划分和 TCP/IP 协议的介绍更适合放在专门讲解网络概念的书中,因此我们不会进一步展开讨论。
接下来,我们还需要设置网关:
gateway4: 192.168.100.1
在网络配置中,网关指的是你的出站连接所通过的设备,这通常是路由器或防火墙,具体取决于你的网络设置。此值需要与网络中实际的默认网关地址匹配,默认网关地址通常与 IP 地址的最后一部分为.1。如果不确定,你可以检查连接到相同网络的另一台设备的 IP 地址,它应该是相同的。
最后一部分允许我们配置服务器用来查找外部域名的 DNS 服务器:
nameservers:
addresses: [192.168.100.1, 192.168.100.2]
示例配置仅仅是一个示例——所有值必须与适合你网络的设置相匹配。通常,DNS 服务器的 IP 地址与网关地址相同,但并非总是如此。有时,网络管理员会为 DNS 服务器设置自定义 IP 方案。在示例中我还添加了一个次级 DNS 服务器 192.168.100.2,但如果不需要,可以删除第二个 IP 地址。
一旦确认文件中的值是合适的,我们需要应用并测试这些更改:
-
如果你正在使用虚拟机,你可能想要从虚拟机控制台进行更改
-
如果你正在更新一台物理机器,你可能需要连接显示器和键盘。
-
尽管我们在本章稍后讨论 OpenSSH,如果你已经知道如何通过 OpenSSH 连接到服务器,你可能不希望在 OpenSSH 中更改网络配置,因为一旦激活这些更改,你的连接将会断开。
慢慢来,仔细检查一切,确保没有输入错误,这样就不会遇到无法连接的服务器问题。
要使这些更改生效,你可以运行以下命令:
sudo netplan apply
当你运行之前的命令时,它会告诉你文件中是否有错误,如果没有错误则会应用更改。新的 IP 地址会立即生效。
如果在配置网络时使用像 OpenSSH 这样的远程连接,你可以通过使用 tmux(一个流行的终端多路复用器)来解决断开连接和网络无法正确重启的问题。关于 tmux 的完整教程超出了本书的范围,但在这个场景中它对我们非常有帮助,因为即使我们与服务器的连接丢失,它也能保持命令在后台运行。要使用它,首先安装该软件包:
sudo apt install tmux
然后,只需在你的终端提示符下输入 tmux 来激活 tmux。
从此时起,tmux 就负责你的会话。如果你在 tmux 中运行一个命令,它会继续运行,无论你是否已连接到它。要看到这一点,首先进入 tmux,然后执行 top 命令。当 top 正在运行时,从 tmux 断开连接。为此,在键盘上按 Ctrl + b,松开后,再按 d。你将退出 tmux,但是如果你输入 tmux a 命令重新连接会话,你会发现即使断开了连接,top 仍然在运行。按照这种逻辑,你可以在执行 sudo netplan apply 命令之前启动 tmux。
很可能,你仍然会被从 shell 中断开,因为激活网络更改的过程会使网络接口关闭并重新启动,但使用tmux时,命令会在后台完成。然后,你可以重新连接到服务器,并运行tmux a以重新加入你的tmux会话。
tmux工具非常强大,当正确使用时,它可以显著提升你在使用 Linux shell 时的工作效率。虽然本书无法详细讲解完整教程,但我强烈建议你深入了解如何使用它,你可以在这里找到更多信息:www.packtpub.com/hardware-and-creative/getting-started-tmux。如果你需要一些指导,可以查看 LearnLinuxTV 上的视频教程,链接将在本章末尾提供。
网络重启后,你应该能够立即重新连接到服务器,并通过执行ip a来查看新的 IP 分配是否已生效。如果由于某种原因无法重新连接到服务器,可能是你在编辑 Netplan 配置文件时犯了错误。请仔细检查该文件,确保没有错误。但只要你按照步骤操作,并为你的接口和网络输入了正确的值,就应该能够成功配置静态 IP。
现在,我们已经有了实际的网络——我们已经为服务器命名并配置了网络接口。我们还应了解 Ubuntu 中的名称解析工作原理,这是服务器通过名称查找其他服务器的过程。
理解 Linux 名称解析
在第十一章,设置网络服务中,我们将讨论为本地名称解析设置 DNS 服务器的过程。但在此之前,理解 Linux 如何解析名称是很重要的。你们中的大多数人可能都知道域名系统(DNS)的概念,它将人类可理解的域名映射到 IP 地址。这使得浏览网络(以及互联网)变得更加轻松。然而,DNS 并不是 Linux 服务器在解析名称时首先使用的工具。
欲了解 Ubuntu Server 在解析名称时检查资源的顺序,欢迎查看/etc/nsswitch.conf文件。该文件中有一行以hosts开头。以下是我服务器中该行的输出:
hosts: files mdns4_minimal [NOTFOUND=return] dns mymachines
在这种情况下,服务器配置为首先检查本地文件,如果未找到请求的信息,则检查 DNS。这是默认顺序,我认为没有理由在这里进行更改(但你当然可以)。具体来说,服务器将检查的文件是/etc/hosts。如果在那里找不到所需内容,它将转向 DNS(基本上,它会检查我们之前通过 Netplan 配置的 DNS 服务器,或者由 DHCP 提供的默认服务器)。
nsswitch.conf文件中还有许多其他行,但由于超出了本节的讨论范围,我在这里不再讨论。
我们在讨论主机名时简要提到了/etc/hosts文件,它告诉我们的服务器如何解析自身(它有一个主机名映射到本地主机 IP 127.0.0.1),但你也可以在这里创建其他名称到 IP 的映射。
例如,如果我有一台服务器(myserver.mydomain.org)的 IP 地址是10.10.96.124,我可以在/etc/hosts中添加以下行,使得每次都能将我的机器解析到该 IP,而不需要查询 DNS 服务器:
10.10.96.124 myserver.mydomain.org
然而,实际上,这通常不是一个非常方便的配置名称解析的方法。别误会,我并不是说你不能在这个文件中列出你的服务器和它们的 IP 地址,你的服务器肯定能解析这些名称。问题在于,这种方法很难维护。名称映射仅适用于你在其上修改了/etc/hosts文件的服务器;其他服务器无法受益,因为它们只会检查自己的/etc/hosts文件。你可以在每个服务器的hosts文件中添加服务器列表,但那会很麻烦。这也是为什么拥有一个中央 DNS 服务器对任何网络都有好处,尤其是在解析本地资源名称时。
然而,/etc/hosts文件偶尔仍在企业中作为快速的临时解决方案使用,最终你可能因某种原因需要使用这种方法。使用这种手动名称解析方法的一个非常常见的原因是在测试替换服务器的情况下。此时,你可以将/etc/hosts文件配置为与原始服务器相同的名称,但使用新服务器的 IP 地址。完成测试并确认新服务器正常运行后,你就可以将全网的 DNS 名称替换为指向新 IP 地址。
在旧版 Ubuntu 服务器上,/etc/resolv.conf文件包含了系统用于解析名称的 DNS 服务器的 IP 地址。如果你想覆盖服务器的 DNS 服务器,你需要修改这个文件。尽管这个文件在 Ubuntu 22.04 中仍然存在,但它仅用于将查询重定向到systemd-resolved,这是一个在后台运行并根据系统通过 DHCP 接收到的设置或你在 Netplan 中配置的设置应用 DNS 配置的 systemd 单元。为了完整起见,这里简要概述一下旧版本中该文件的语法,以防你需要在这种服务器上工作。以下是该文件的示例:
nameserver 10.10.96.1
nameserver 10.10.96.2
在这个例子中,/etc/resolv.conf文件输出使用的是10.10.96.1和10.10.96.2这两台服务器。因此,服务器首先会检查/etc/hosts文件,看看是否有与正在查询的资源匹配的记录,如果没有,它将继续检查/etc/resolv.conf,以确定下一个要检查的服务器。在这个例子中,服务器将检查10.10.96.1。
传统服务器上的/etc/resolv.conf文件通常不需要进行实际更改,因为它是由 NetworkManager 自动生成的。NetworkManager 是一个帮助你管理网络接口的服务;然而,在 Ubuntu Server 的多个版本中,它已经不再使用了。尽管你通常不需要手动编辑/etc/resolv.conf文件,但在老旧服务器上查看它可能很有用,这样你就可以知道分配了哪些 DNS 服务器,以防你在排查网络问题时需要用到这些信息。
如今,现代的 Ubuntu 服务器使用systemd-resolved来处理名称解析。如果你想查看在较新的 Ubuntu Server 安装中分配了哪些 DNS 服务器,你可以简单地查看之前我们在静态 IP 配置时使用的 Netplan 配置文件,但如果使用的是 DHCP,resolvectl命令会告诉你当前服务器正在指向的 DNS 服务器。它的输出将类似于以下截图所示:

图 10.5:查看服务器当前的 DNS 分配
在典型的企业 Linux 网络中,你会设置一个本地 DNS 服务器来解析内部资源,然后将请求转发到公共 DNS 服务器,以防你试图访问某个非内部的资源。我们将在第十一章,设置网络服务中讲解这个内容,但你现在应该了解在 Ubuntu Server 上名称解析的工作原理。
作为 Linux 管理员,我们可能需要管理大量服务器,而且我们管理的服务器往往不在与我们相同的物理位置。OpenSSH 是一个强大的远程管理工具,接下来我们将深入探讨它。
开始使用 OpenSSH
OpenSSH可能是管理 Linux 服务器时最有用的工具。在所有可用的无数工具中,这是我建议每个人尽早开始练习的工具。从技术角度来看,我可能更适合在第十一章,设置网络服务中加入一节内容来介绍如何设置 OpenSSH,但这个工具非常实用,我们应该尽早开始使用它。
OpenSSH 允许你在其他 Linux 服务器上打开命令行界面,使你能够像在服务器面前一样执行命令。对于我们这样的 Linux 管理员来说,这非常方便。我们可能需要管理数十台、数百台,甚至上千台服务器。
借助 OpenSSH,我们可以在不离开椅子的情况下管理整个服务器架构。在本节中,我将为你提供关于 OpenSSH 的一些信息,并介绍如何安装它,最后通过一些实际使用的例子来结束这一节。
安装 OpenSSH
OpenSSH 由两个组件组成:在后台运行、接受 SSH 连接的服务器守护进程,以及运行在笔记本、工作站或其他服务器上的客户端,它使你能够连接到 SSH 服务器并运行命令。如今,所有操作系统都提供了 OpenSSH 客户端,可以用来连接到服务器,因此这一要求大概率已经满足。对于 Linux,大多数发行版默认提供了 OpenSSH 客户端。你可以通过在命令行提示符下运行which ssh来验证这一点。如果客户端已经安装,你的输出应该是/usr/bin/ssh。
如果由于某种原因,你没有安装此软件包,并且在运行前一个命令时没有收到任何输出(这种情况比较罕见),你可以使用以下命令来安装 OpenSSH 客户端:
sudo apt install openssh-client
根据你在安装过程中做出的选择,Ubuntu 服务器很可能已经安装了 OpenSSH 服务器。如果你不记得在最初安装时是否选择了安装它,你可以在命令行提示符下运行which sshd命令,你应该会看到输出/usr/sbin/sshd。你也可以执行systemctl status ssh,如果服务器组件存在并且正在运行,那么你的服务器已经准备好接受 SSH 连接:

图 10.6:验证 OpenSSH 服务是否在服务器上运行
如果你的服务器没有安装 OpenSSH 服务器组件,你可以使用以下命令来安装它:
sudo apt install openssh-server
然而,强大的力量伴随巨大的责任。尽管 OpenSSH 非常强大,但任何监听连接的服务都有可能被滥用。外部入侵者发现弱点或漏洞,进而控制你的服务器,情况将变得非常糟糕。因此,像所有在服务器上运行的服务一样,只有在需要时才应该启动它。由于 OpenSSH 极为有用(而且它是远程管理的标准方法),因此几乎很难不使用它。
你可以利用许多方法来保护此类服务并帮助加固它。下一节将介绍其中一种方法,我们在本书结束之前,还将再次讨论与 OpenSSH 相关的安全问题。具体来说,在第二十一章《保护你的服务器》中,我将带你逐步了解可以采取的各种配置更改,帮助最小化外部不法分子入侵服务器并造成破坏的威胁。
保护 OpenSSH 实际上并不困难,可能只需要几分钟的时间。因此,您可以绕道阅读该章节中关于保护 OpenSSH 的部分,读完后再回来。现在,至少确保服务器上有安全的、随机生成的密码。如果 OpenSSH 可以通过公共互联网访问,而您的用户有弱密码,那么这绝对不是一个好情况。
完成所有这些后,我们就可以开始实际使用 OpenSSH 了。
使用 OpenSSH 执行命令
在您已安装 openssh-server 软件包到目标机器(您希望远程控制的那台机器)之后,如果它尚未启动,您需要启动它。默认情况下,Ubuntu 的 openssh-server 软件包会在安装后自动配置为启动并启用。就像我们之前做的那样,您可以通过以下命令验证所需的服务是否正在运行:
systemctl status ssh
如果 OpenSSH 作为守护进程在您的服务器上运行,您应该看到输出信息,告知它处于 active (running) 状态。如果不是,您可以使用以下命令启动它:
sudo systemctl start ssh
如果 systemctl status ssh 命令的输出显示守护进程被禁用(意味着它不会在服务器启动时自动启动),您可以使用以下命令启用它:
sudo systemctl enable ssh
当 OpenSSH 服务器已启动并正在运行时,您的服务器现在应该在监听连接。为了验证这一点,可以使用以下命令列出监听的端口,将输出限制为 SSH:
sudo ss -tunlp |grep ssh
ss 命令允许我们查看正在服务器上运行并监听连接的进程列表。它还会显示进程监听的端口。
我们将在 第二十一章,保护您的服务器 中更详细地探讨此命令。但现在,这个命令应该会输出类似以下内容:

图 10.7:检查 SSH 所需端口是否在监听
如果由于某种原因,您的服务器没有显示有 SSH 服务器在监听,仔细检查您是否已经启动了守护进程。默认情况下,SSH 服务器会在端口22上监听连接。可以通过修改/etc/ssh/sshd_config文件中的端口声明来更改这一设置,但这是后续章节的内容。虽然我现在不打算详细讲解如何编辑此文件,但请注意,它是守护进程的默认配置文件。每次 OpenSSH 启动或重启时,它都会读取此文件中的配置值。
要使用 SSH 连接到服务器,只需执行 ssh 命令,后面跟上您要连接的服务器的名称或 IP 地址:
ssh 192.168.1.120
默认情况下,ssh 命令将使用你当前登录的用户名进行连接。如果你想使用不同的用户名,可以在 ssh 命令中指定它,通过在 IP 地址或主机名之前加入用户名,并跟随 @ 符号:
ssh fmulder@192.168.1.120
除非你另行指定,ssh 命令假定你的目标主机监听的是端口 22。如果不是,你可以通过 -p 选项后跟端口号来指定不同的端口:
ssh -p 2242 fmulder@192.168.1.120
一旦你连接到目标机器,你就可以像直接站在它面前一样运行 shell 命令并管理系统。你将拥有与你登录时相同的权限,如果你在该服务器上有权限,你还可以使用 sudo 来执行管理员命令。
基本上,如果你在服务器面前时能够做的任何事情,你都能通过 SSH 来做。当你完成会话时,只需在 shell 提示符下输入 exit,或按下键盘上的 Ctrl + d。
当你退出 OpenSSH 连接时,任何在后台运行的进程都会被终止。请确保在退出连接之前,恢复你可能正在运行的后台进程,并完成相关工作。我们在第七章、控制和监控进程中讨论过如何在后台运行进程。
如你所见,OpenSSH 是一款神奇的工具,它能够让你从任何允许 SSH 访问的地方远程管理你的服务器。但要确保阅读第十一章、设置网络服务中的相关部分,关于如何保证它的安全。在接下来的章节中,我们将讨论 SSH 密钥管理,它带来了便利性,同时也能提升安全性。
开始使用 SSH 密钥管理
当你通过 SSH 连接到主机时,系统会要求你输入密码,验证通过后便会建立连接。不过,你可以选择通过公钥认证而不是输入密码进行验证。这种方法的核心优点是增加了安全性,因为在连接服务器的过程中,系统密码不会被传输。当你创建 SSH 密钥对时,你会生成两个文件,一个公钥和一个私钥。这两个文件在数学上是相互关联的,因此,如果你连接到一台已保存你公钥的服务器,服务器会知道是你在连接,因为只有你(并且只有你)拥有与公钥匹配的私钥。这种方法比密码验证更加安全,我强烈建议你使用它。为了最大化通过密钥认证带来的安全性,你实际上可以在服务器上禁用基于密码的认证,这样你的 SSH 密钥就成为唯一的登录方式。通过禁用基于密码的认证并仅使用密钥,你可以大幅提升服务器的安全性。我们将在第二十一章、保护你的服务器中讲解这一部分。
生成公钥和私钥
首先,你需要生成密钥。在你的工作站或笔记本电脑(即你用来连接服务器的设备)上,使用ssh-keygen命令,以普通用户账号进行操作。下面的截图展示了这一过程的大致样子:

图 10.8:生成 SSH 密钥对
首先,你会被要求输入保存密钥文件的目录,默认路径是/home/<user>/.ssh。接下来,系统会询问是否设置密码短语,这一步是可选的。虽然这会增加通过密钥进行身份验证的步骤,但我建议你设置一个密码短语(与系统密码不同),因为它大大提高了安全性(如果你设置了密码短语,密钥没有密码短语就无法使用)。如果你不想设置密码短语,可以直接按Enter键跳过。
该命令的作用是,在你的home目录下创建一个名为.ssh的目录(如果它还不存在)。在该目录中,它会创建两个文件,id_rsa和id_rsa.pub。id_rsa文件是你的私钥,它永远不应该离开你的计算机、传给其他用户,或者存储在任何外部介质上。如果你的私钥泄露,你的密钥对就不再可信。默认情况下,私钥的拥有者是创建它的用户,且rw权限只授予文件所有者。
公钥则不同,它可以离开你的计算机,并且不需要像私钥那样严格保护。它的权限更加宽松,所有人都可以读取,只有所有者可以写入。你可以通过执行ls -l /home/<user>/.ssh来查看这一点:

图 10.9:列出.ssh 目录的内容,显示新创建密钥的权限
公钥是实际复制到其他服务器上的密钥,以便通过这样的密钥对进行登录。当你登录到一个包含你公钥的服务器时,服务器会检查公钥与私钥是否在数学上匹配,如果匹配,它会允许你登录。如果在创建密钥时设置了密码短语,系统也会要求你输入它。但在我们实际使用密钥之前,我们需要将公钥复制到目标服务器上。
将你的公钥复制到远程服务器
要实际将公钥传输到目标服务器,可以使用ssh-copy-id命令,下面是我在例子中使用的命令:
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.1.150
使用该命令时,将 IP 地址替换为目标服务器的实际 IP 地址,或替换为目标服务器的 主机名。你首先会被要求通过密码登录,然后密钥会被复制过去。从此以后,你将通过密钥登录,如果因某种原因密钥关系被破坏,你会被要求输入密码。以下是这个过程的示例,如果我要将我的密钥复制到名为 myserver.mycompany.org 的服务器时的步骤:

图 10.10:使用 ssh-copy-id 命令将公钥复制到服务器
那么,ssh-copy-id 命令到底做了什么呢?你的公钥到底被复制到了哪里?其实,执行此命令后,如果目标服务器的 home 目录下没有 .ssh 目录,它会自动创建一个。在该目录内,如果没有现成的 authorized_keys 文件,它会创建一个。你机器上 ~/.ssh/id_rsa.pub 文件的内容将被复制到目标服务器的 ~/.ssh/authorized_keys 文件中。每添加一个新的密钥(例如,你从多台机器连接到该服务器时),密钥会被添加到 authorized_keys 文件的末尾,每个密钥占一行。
使用 ssh-copy-id 命令仅仅是为了方便;你完全可以将 id_rsa.pub 文件的内容复制并手动粘贴到目标服务器的 authorized_keys 文件中。那种方法其实也能正常工作。
当你连接到已经设置了密钥关系的服务器时,SSH 会检查该服务器上 ~/.ssh/authorized_keys 文件的内容,寻找一个与本机私钥(~/.ssh/id_rsa)在数学上匹配的密钥。如果这两个密钥匹配,你将被授予访问权限。如果你设置了密码短语,则在打开公钥时,会要求你输入密码短语。
如果你决定在密钥中不设置密码短语,实际上就是在设置无需密码的身份验证,这意味着在身份验证时,你不需要输入任何内容。
使用 SSH 代理
当我们之前创建 SSH 密钥时提到过,设置密码短语是可选的,但它是一个好主意。使用密码短语可以提高 OpenSSH 密钥对的安全性。如果一个 OpenSSH 密钥落入错误的人手中,只要他们不知道密码短语,就无法使用它。然而,我们也会失去一些便利,因为每次使用密钥时都需要输入密码短语。没有密码短语的 OpenSSH 密钥允许我们连接到服务器并直接登录,而不需要输入任何东西。使用SSH 代理,你实际上可以在第一次使用密码短语时将其缓存,这样每次连接时就不需要再输入密码短语了。这实际上让你在享受密码短语增加的安全性的同时,仍然保持了一定的便利性。最棒的是,如果你的笔记本电脑或台式机能够使用 OpenSSH 客户端连接到远程系统,那么你应该已经在系统中安装了 SSH 代理。例如,如果我们在工作站或笔记本电脑上使用 Linux 或 macOS 的某种版本,ssh-agent命令会是可用的。
ssh-agent是通过在终端后台启动来使用的。然后,我们可以使用密码短语“解锁”我们的密钥,解锁后的密钥将被存储在内存中,当我们尝试连接到已经将公钥复制到的服务器时,系统会自动使用这个解锁的密钥。要启动它,在你用来启动连接的机器上(也就是你的工作站)输入以下命令:
eval $(ssh-agent)
这个命令将启动一个 SSH 代理,它将继续在你的 shell 后台运行。但它现在还没有给我们带来任何好处——所以我们需要将一个 SSH 密钥添加到正在运行的代理中。ssh-add命令允许我们将一个 SSH 密钥添加到正在运行的ssh-agent中。为此,我们可以将ssh-add命令与公钥的路径作为参数一起使用:
ssh-add ~/.ssh/id_rsa
到此时,你将被要求输入密码短语。只要你正确输入,它将保持解锁状态,未来的连接无需再次输入密码短语,直到你关闭该 shell 或注销。现在,你已经在后台运行了ssh-agent,并且密钥已经解锁,使用带有密码短语的密钥变得更加简单,你将输入更少的内容。
更改 OpenSSH 密钥的密码短语
有时候,你可能想要更改与密钥相关的密码短语。如果你想这样做,可以使用-p参数与ssh-keygen命令配合使用。如果你在最初创建密钥时没有选择添加密码短语,那个参数也可以用来添加。就是这么简单。你输入的命令就像以下这样简单:
ssh-keygen -p
一旦你输入该命令,按 Enter 键接受默认文件(id_rsa),除非你想更改的密钥有不同的名称,届时你可以键入该密钥的路径和名称。接下来,你将被要求输入当前的密码短语(如果没有密码短语,可以留空),然后再输入两次新密码短语。整个过程如下所示:

图 10.11:更改 SSH 密码短语
如果你从未使用过 SSH,这些概念可能需要一些练习。最好的练习方法是设置多个 Ubuntu Server 安装(可能是几个虚拟机),并练习使用 SSH 连接到它们,同时通过 ssh-copy-id 命令将你的密钥部署到每台机器上。实际上,一旦掌握了,它会变得相当简单。
用配置文件简化 SSH 连接
在我们离开 OpenSSH 话题之前,还有一个便捷的技巧,那就是创建 SSH 的本地配置文件。这个文件必须存储在你的主目录的 .ssh 文件夹中,并命名为 config。在我的情况下,这个文件的完整路径如下所示:
/home/jay/.ssh/config
这个文件默认是不存在的,但如果它被找到,SSH 每次使用客户端时都会解析它,你将能够从中受益。请打开这个文件并在文本编辑器中编辑,例如 nano:
nano /home/your_username/.ssh/config
这个 config 文件允许你为经常连接的服务器输入配置,从而自动简化 ssh 命令。以下是该文件中的示例内容,帮助我说明它的作用:
host myserver
Hostname 192.168.1.23
Port 22
User jdoe
Host nagios
Hostname nagios.mynetwork.org
Port 2222
User nagiosuser
在示例内容中,我列出了两个主机,myserver 和 nagios。对于每个主机,我都标明了如何通过名称或 IP 地址(Hostname 行)来访问它,以及连接时使用的 Port 和 User 账户。如果我通过文件中列出的名称使用 ssh 连接到这两个主机中的任何一个,它会使用我在那里存储的值,例如:
ssh nagios
这个命令比手动设置所有选项要短得多。考虑到我有一个 SSH 的 config 文件,这个命令实际上和我手动识别连接详细信息后输入的命令是一样的,手动输入的命令如下所示:
ssh -p 2222 nagiosuser@nagios.mynetwork.org
我相信你能看出,输入第一个命令比第二个命令要简单得多。通过 SSH 的 config 文件,我可以让一些细节自动应用。因为我已经在文件中说明了我的 nagios 服务器位于 nagios.mynetwork.org,它的 SSH 用户是 nagiosuser,并且它监听的是 2222 端口,所以即使我只输入了 ssh nagios,它也会自动使用这些值。此外,你还可以覆盖这个条目。如果你在使用 ssh 命令时提供了不同的用户名,它会使用你提供的用户名,而不是 config 文件中写的用户名。
在第一个示例中(针对 myserver 服务器),我为连接提供了 IP 地址,而不是主机名。在没有目标服务器的 DNS 条目的情况下,这非常有用。通过这个示例,我不必记住 myserver 的 IP 地址是 192.168.1.23。我只需要执行 ssh myserver,系统就会为我处理好这一切。
config 文件中的每个服务器名称都是任意的,不必与目标服务器的主机名匹配。我本可以将第一个服务器命名为 potato,它仍然会将我路由到 192.168.1.23,所以我可以创建任何我想要的命名快捷方式,无论是最方便我记住的名字还是最容易记住的名称。如你所见,在你的主目录中维护一个包含最常用 SSH 连接的 config 文件,肯定能帮助你保持井井有条,并且让你更容易连接。
总结
在本章中,我们通过多个示例讲解了如何连接到其他网络。我们从配置主机名开始,管理网络接口,分配静态 IP 地址,并了解 Linux 中的名称解析如何工作。本章的相当一部分内容专门讲解了 OpenSSH,这是一个非常有用的工具,允许你远程管理服务器。在 第二十一章《保护你的服务器》中,我们将再次探讨 OpenSSH,重点讲解如何增强其安全性。总的来说,我们才刚刚触及到这个工具的表面。关于 SSH 已经写了整本书,但本章中的示例应该足以让你开始高效使用它。关键在于:练习,练习,再练习!
在下一章中,我们将讨论如何管理软件包。我们将通过添加和删除软件包、添加附加软件库等内容进行讲解!
相关视频
-
OpenSSH 指南(LearnLinuxTV):
linux.video/ssh-guide -
tmux 指南(LearnLinuxTV):
linux.video/tmux-guide -
OpenSSH 客户端配置文件(LearnLinuxTV):
linux.video/ssh-config-file -
入门 OpenSSH 密钥管理(LearnLinuxTV):
linux.video/ssh-key-mgmt
进一步阅读
-
Netplan 常见问题解答:
learnlinux.link/npf -
Ubuntu SSH 密钥文档:
learnlinux.link/u-ssh-keys -
网络接口名称(来自 Debian 的 wiki):
learnlinux.link/net-int-nam
加入我们社区的 Discord
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十一章:设置网络服务
在第十章,连接到网络中,我们讨论了与网络相关的一些重要基础知识。我们了解了如何设置主机名、管理网络接口、配置连接等内容。在本章中,我们将再次讨论网络,特别是设置将作为网络基础的资源。本章的主要内容将集中在设置 DHCP 和 DNS 服务器上,这两个组件在任何网络中都是非常重要的。此外,我们还将探讨如何设置一个服务器作为我们网络的互联网网关。
在此过程中,我们将讨论以下主题:
-
规划你的 IP 地址方案
-
使用
isc-dhcp-server提供 IP 地址 -
使用
bind设置 DNS -
设置互联网网关
作为 Linux 管理员,你可能会被要求设计整个组织的网络布局,也可能不会;通常会有一个已经存在的网络需要管理。在下一节中,我们将讨论如何创建这样的布局,即使仅仅是为了理解这种计划包含了什么内容。
规划你的 IP 地址方案
设计你网络的整体布局是一个非常重要的过程,它为以后的成功或失败奠定了基础。这个设计必须考虑到组织的需求、高效的通信方式,以及网络服务的隔离,以确保你的服务器只能与它们应该访问的资源通信。作为一名管理 Ubuntu 服务器的 Linux 管理员,并非总是你能够参与网络布局的设计。你很可能“继承”了前任管理员设计的网络,或者你的工作被分隔开,你只负责管理服务器,网络则由其他人负责。
由于本书主要是教你如何管理 Ubuntu 服务器,我们不会涵盖网络管理员需要了解的所有内容,但这两个角色之间有一些重叠。作为 Linux 管理员,你可能会负责设计网络,也可能不会,但至少,你需要了解网络的整体布局,以及你的服务器如何融入其中。
在这一部分中,我们将讨论网络布局中最重要的部分——IP 地址规划。规划 IP 地址方案是一项重要的任务,它为许多其他工作奠定基础。即使这项布局设计不由你来完成,了解现有布局的细节也能帮助你更好地理解你所维护的服务器如何与其他设备进行通信。规划你的 IP 布局需要估算有多少设备将需要连接到网络,并确保能够支持它们。此外,一个好的规划还需要考虑潜在的增长,并允许未来扩展。影响这一点的主要因素是用户基础的规模。也许你正在一个只有少数几个人的小办公室工作,或者你所在的是一个拥有数千个用户和数百台虚拟机的大公司。即使你的组织只是一个小型办公室,我总是建议假设公司未来某天会爆炸式增长,并设计你的网络以具备应对这一增长的潜力。
通常,大多数现成的路由器和网络设备都配备集成的动态主机配置协议(DHCP)服务器,并默认使用类 C(/24)网络。实质上,这意味着如果你完全不做任何配置,你的地址数量将被限制为 254 个。对于一个小型办公室来说,这看起来可能已经足够了。毕竟,如果你的公司连 254 个员工都没有,这个数字看起来可能有些过剩。正如我之前提到的,潜在的增长始终是需要考虑的因素。但即使不考虑这一点,IP 地址的消耗速度比你想象的要快——即使是内部地址也不例外。普通用户可能每人就消耗三个 IP 地址,有时甚至更多。
例如,假设某个用户不仅有一台笔记本电脑(笔记本可能同时有有线和无线接口,每个都占用一个 IP 地址),或许他还拥有一部手机(通常配有 Wi-Fi),以及一部基于 IP 的语音(VoIP)电话(这又占用一个地址)。如果这个用户成功说服上司让他同时拥有台式电脑和笔记本电脑,那么这个用户将会拥有五个 IP 地址。突然间,254 个地址似乎就不那么多了。
一个非常好的现实世界示例,说明可用 IP 地址数量可能成为问题的情况,是一家位于繁忙城市的小餐馆为顾客提供免费 Wi-Fi。为顾客设计访客网络的人可能会认为,如果每天只有大约一百个顾客,/24网络就足够了。虽然这个逻辑看起来是合理的,但考虑到如果餐馆位于繁忙街道旁,开着 Wi-Fi 的手机经过时可能会抢到一个地址,而且如果 DHCP 地址配置为 24 小时过期,那么大部分时间餐馆实际顾客将没有可用的 IP 地址。如果你曾尝试连接餐馆的 Wi-Fi 网络,连接后却无法上网,这可能就是未有效设计网络布局的一个现实痛点。
解决这个问题的明显方法是将网络划分为子网。虽然我不会深入讲解如何将网络进行子网划分(这超出了本书的范围),但在这里提到它是因为它确实是你应该考虑的内容。在下一节中,我将解释如何在单一网络上设置自己的 DHCP 服务器。但是,如果你需要扩展地址空间,可以通过更新 DHCP 配置轻松完成。在制定 IP 地址布局时,始终假设最坏的情况并提前规划。虽然扩展 DHCP 服务器可能是一个简单的任务,但规划新的 IP 方案部署是非常耗时的,老实说,也很麻烦。
当我设置一个新网络时,我喜欢将地址空间分成几个类别。首先,我通常会专门为 DHCP 预留一组 IP 地址。这些地址将在客户端连接时分配,通常它们会过期并需要在大约一天后续约。然后,我会为网络设备预留一块 IP 地址,为服务器预留另一块 IP 地址,以此类推。在一个典型的/24网络中,我可能会决定采用如下方案(假设这是一个没有计划增长的小型办公室):
Network: 192.168.1.0/24
Network equipment: 192.168.1.1 - 192.168.1.10
Servers: 192.168.1.11 - 192.168.1.99
DHCP: 192.168.1.100 - 192.168.1.240
Reservations: 192.168.1.241 - 192.168.1.254
由于我提到过计划未来的增长是个好主意,/24网络可能会受到限制,无法支持太多的增长。我选择这个方案是为了让本章内容保持简单,便于讲解。但在真实的公司网络中,你可能需要考虑比上述方案提供的更多的 IP 地址。
如何获得更多的 IP 地址?看看网络地址后面的数字,在第一行中是/24。这个数字配置了 IP 地址空间的大小,本质上决定了我们有多少可用的 IP 地址。这个数字被称为CIDR表示法,代表无类域间路由。类似于子网掩码,改变这个数字会导致不同数量的 IP 地址。例如,如果你将网络中的/24改为/22,你将立即拥有 1,022 个可用地址,而不是 254 个。如果再将它降低到/20,数量会增加到 4,094 个。现在,你不必过于担心这些。你可以集中精力记住,数字越大,IP 地址就越少。它可以一直增加到/32,这时你只会得到一个 IP 地址。如果你想进一步实验子网划分,我们在进一步阅读部分提供了一个子网计算器。
以下是常见的 CIDR 值及其对可用 IP 地址数量的影响:
| CIDR | 总可用 IP 地址 |
|---|---|
/32 |
1 |
/24 |
254 |
/16 |
65,534 |
/8 |
16,777,214 |
当然,没有一个 IP 地址方案适合所有人。我之前提供的方案只是一个假设的例子,所以除非它符合你的需求,否则不应将我的方案复制并应用到你的网络中。在本章剩余部分,我将使用这个方案,因为它作为示例是有效的。为了说明我的示例实施方案,我们从一个 24 位网络开始,192.168.1.0/24。地址192.168.1.0指的是网络本身,这个 IP 地址不能分配给客户端。该子网中第一个可用的 IP 地址是192.168.1.1。这个块中的最后一个 IP 地址(192.168.1.255)也不能分配,因为它被称为广播地址。发送到广播地址的任何信息实际上会发送到该块中的每个 IP 地址,因此我们不能将其用于任何用途,除非是广播。总结来说,请记住,结尾为.0的 IP 地址无法使用,结尾为.255的 IP 地址也是如此。
从技术上讲,你可以分配以.0或.255结尾的 IP 地址,具体取决于 CIDR 值。然而,由于这些结尾值通常用于表示网络本身和广播地址,因此一些应用程序和硬件可能不接受这些值。因此,实际上你应该将这些结尾值视为不可分配的。
回到本节之前提到的示例 IP 布局,我为网络设备预留了一组 IP 地址,范围是192.168.1.1到192.168.1.10。符合这一类别的典型设备包括受管交换机、路由器、无线接入点等。这些设备通常具有集成的 Web 控制台用于远程管理,因此最好为它们分配静态 IP 地址。这样,我就可以使用这些 IP 地址访问这些设备。我喜欢将网络设备设置为第一个分配静态 IP 地址的设备,以便它们在每个 IP 地址的最后一个数字上获得最小的号码。这只是个人偏好。
在示例布局中,我们为服务器定义了 IP 地址192.168.1.11到192.168.1.99。这看起来像是为服务器分配了不少地址,确实如此。然而,随着虚拟化技术的兴起以及部署服务器变得如此简单,这个地址范围可能会比你预想的更快用完。可以根据需要进行调整。
现在我们有了 DHCP 地址池,它包含了192.168.1.101到192.168.1.240的地址。这些 IP 地址可以分配给任何连接到我们网络的设备。通常,我喜欢将这些分配设置为一天后过期,以防止某些临时设备占用 IP 地址过长时间,这可能导致设备之间争夺 DHCP 租约。在这种情况下,你需要清除 DHCP 租约来重置所有内容,而我觉得这样做太麻烦了。当我们进入设置 DHCP 服务器的部分时,我会向你展示如何设置过期时间。
最后,我们为 DHCP 预留地址定义了192.168.1.241到192.168.1.254的地址范围。我通常将预留的 DHCP 地址称为静态租约,但这两个术语的含义是相同的。这些地址将由 DHCP 分配,但每个拥有静态租约的设备每次都会获得相同的 IP 地址。你不需要将这些地址单独放入一个池中,因为 DHCP 不会分配相同的地址两次。不过,将它们分开还是一个好主意,至少可以通过查看 IP 地址来辨别它是静态租约,因为它属于某个假设的地址范围。静态租约适用于那些不一定是服务器但仍然需要可预测 IP 地址的设备。
一个例子可能是管理员的桌面 PC。也许他们希望能够通过 VPN 连接到办公室,并且能够轻松地在网络上找到自己的电脑并进行连接。如果 IP 是动态分配的,而不是静态分配的,他们就很难找到自己的设备。
在划分完 IP 地址后,接下来的任务是确保它们得到准确的文档记录。如果你在设计服务时没有专注于文档记录,后期肯定会后悔。你可以考虑设置一个私人 Wiki 服务器,例如。
另一种方法是创建一个电子表格来跟踪静态 IP 分配。如果你没有更好的解决方案,这也是可以接受的;不需要太复杂。在通常的组件中,如设备信息和 IP 地址,我还会在电子表格中包括每个设备的 MAC 地址,这在我们设置 DHCP 服务器的下一节中将非常有用:

图 11.1:一个示例 IP 地址布局电子表格
从我的示例布局中可以看出,使用 24 位网络时可用地址的数量相当有限。然而,这个布局足以作为我们后续章节的示例。此时,只需考虑对你所在组织来说重要的因素,并确保你创建的任何网络都具有可扩展性,能够满足你的需求。
现在我们已经有了 IP 布局(如果之前没有的话),我们可以开始设置 DHCP 服务器,这将是最终分配这些 IP 的服务。
设置 DHCP 服务器以提供 IP 地址
你现在购买的大多数网络设备通常会自带 DHCP 服务器,并允许你通过 Web 控制台进行配置。通常,这样完全没问题,能够满足你的需求。然而,根据我的经验,你购买的网络设备可能存在不稳定的情况。有些设备很棒,但有些则不那么令人印象深刻。一个主要的问题是,制造商通常会提前停止对硬件的支持,这会让你的网络暴露于未修补的漏洞之中。因此,始终购买尽可能具有未来适应性的网络硬件是非常重要的。或者,你可以自己设置路由器,配置所需的功能。虽然管理负担会落在你身上,但这能给你提供最大的灵活性。Ubuntu 服务器是很好的 DHCP 服务器,自己搭建服务器其实比看起来要简单。接下来,我们将在本节中详细讨论这一过程。
首先,提供 DHCP 服务的服务器必须具备静态 IP 地址。这意味着你需要使用 Netplan 来配置静态 IP 分配。静态租约在这里不起作用,因为 DHCP 服务器无法为自己分配 IP 地址。
此外,分配给服务器的静态 IP 地址必须与计划提供的地址处于同一网络内。否则,即使我们配置了服务,它也无法启动。
如果你还没有设置静态 IP 地址,第十章,连接网络,有一节内容会带你完成这个过程。
一旦你分配了静态 IP 地址,下一步就是安装isc-dhcp-server软件包:
sudo apt install isc-dhcp-server
安装完isc-dhcp-server软件包后,使用以下命令检查守护进程的状态:
systemctl status isc-dhcp-server
你可能会注意到它失败了:

图 11.2:默认情况下isc-dhcp-server失败
如果启动失败,不必担心。Ubuntu 默认会启动大多数通过软件包安装的服务。有时,某些服务需要先配置才能运行。在 isc-dhcp-server 的情况下,它需要一个有效的配置才能启动,但我们还没有进行任何配置。我们需要配置 isc-dhcp-server 服务使其能正常工作,所以现在先停止该服务。
一旦我们完成了配置的添加,就可以开始了:
sudo systemctl stop isc-dhcp-server
配置 IPv6 网络不是我们要讨论的内容,但我们刚安装的 DHCP 服务器软件包也包括了一个 IPv6 对应版本。让我们停止并禁用这个服务,因为我们不会使用它:
sudo systemctl stop isc-dhcp-server6
sudo systemctl disable isc-dhcp-server6
现在,你已经安装了 isc-dhcp-server 软件包,它会在 /etc/dhcp/dhcpd.conf 目录下为你提供一个默认的配置文件。这个文件包含一些默认配置,其中有一些示例设置被注释掉了。你可以查看这个文件,了解一些你可以配置的设置。我们将从头开始创建自己的 dhcpd.conf 文件。所以,当你查看完后,将现有文件重命名,以便以后如果需要参考它时可以找到:
sudo mv /etc/dhcp/dhcpd.conf /etc/dhcp/dhcpd.conf.orig
现在,我们准备创建自己的 dhcpd.conf 文件。用你喜欢的文本编辑器打开 /etc/dhcp/dhcpd.conf。由于文件已经不存在(我们已将其移动),我们应该从一个空文件开始。这是一个示例 dhcpd.conf 文件,我会逐一解释它的工作原理,帮助你理解它是如何运作的:
default-lease-time 43200;
max-lease-time 86400;
option subnet-mask 255.255.255.0;
option broadcast-address 192.168.1.255;
option domain-name "local.lan";
authoritative;
subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.100 192.168.1.240;
option routers 192.168.1.1;
option domain-name-servers 192.168.1.1;
}
一如既往,将我使用的值更改为与你的网络相匹配的值。我会解释每一行,这样你就能理解它如何影响你的 DHCP 服务器配置。
default-lease-time 43200;
当设备连接到你的网络并请求一个 IP 地址时,如果设备没有明确要求更长的租期,租期到期时间将设置为 default-lease-time 中的秒数。在这里,我将其设置为 43200 秒,相当于半天。这基本上意味着设备每隔 43200 秒就需要续租其 IP 地址,除非它请求更长的租期。
max-lease-time 86400;
虽然前面的设置规定了设备在没有请求特定租期时的默认租期,max-lease-time 是设备可以拥有的最大租期。在这种情况下,我将其设置为一天(86400秒)。因此,从这个 DHCP 服务器获取 IP 地址的设备不能在没有先续租的情况下,持有其租期超过这个时间。
option subnet-mask 255.255.255.0;
使用这个设置,我们通知客户端,它们的子网掩码应该设置为255.255.255.0,这是一个默认的 24 位网络。如果你打算对网络进行子网划分,你需要在这里填写不同的值。如果你只需要一个 24 位的网络,255.255.255.0是没问题的。
option broadcast-address 192.168.1.255;
使用这个设置,我们告诉客户端使用 192.168.1.255 作为广播地址,这是子网中的最后一个地址,不能分配给主机。
option domain-name "local.lan";
在这里,我们设置了所有连接到服务器的主机的域名为 local.lan。域名将添加到主机名的末尾。例如,如果一个主机名为 muffin 的工作站从我们的 DHCP 服务器获取了一个 IP 地址,它将被称为 muffin.local.lan。如果你有自己的组织域名,可以将其更改为相应的域名,或者如果没有域名,也可以保持原样。
authoritative;
使用 authoritative 设置(与 not authoritative 相对),我们将 DHCP 服务器声明为网络的权威服务器。除非你打算有多个 DHCP 服务器,否则在你的 config 文件中应该包括 authoritative; 选项。我们不会使用 not authoritative; 选项,因为它超出了本章的范围。
现在,我们进入了 DHCP 配置文件中最重要的部分。以下代码块详细说明了将提供给客户端的具体信息:
subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.100 192.168.1.240;
option routers 192.168.1.1;
option domain-name-servers 192.168.1.1;
}
这个代码块可能不言自明,但我们基本上是在声明 192.168.1.0 网络的地址池。我们声明了一个从 192.168.1.100 到 192.168.1.240 的 IP 地址范围可以供客户端使用。现在,当我们的 DHCP 服务器为客户端分配地址时,它将从这个池中选择一个地址。对于地址池(range),你可以根据需要进行扩展或缩小。例如,你可能需要比我示例范围中允许的 140 个地址更多的地址,因此你可以将其更改为类似 192.168.1.50 到 192.168.1.250 的范围。可以自由尝试。
我们还提供了默认网关(option routers)和 DNS 服务器(option domain-name-servers)为 192.168.1.1。这是假设你的路由器和本地 DNS 服务器都列在该地址上,所以请确保根据需要进行更改。否则,任何从你的服务器获取 DHCP 租约的设备将无法连接到任何东西。
现在我们已经准备好了配置文件,但 DHCP 服务器可能仍然不会启动,直到我们声明一个接口来监听请求。你可以通过编辑 /etc/default/isc-dhcp-server 文件来做到这一点,在文件底部你会看到类似以下的行:
INTERFACESv4=""
只需在引号中键入接口的名称:
INTERFACESv4="enp0s3"
如果你忘记了,列出服务器上接口详细信息的命令是 ip addr show,或者它的简写版本 ip a。
现在我们已经配置了 DHCP 服务器,我们应该能够启动它:
sudo systemctl start isc-dhcp-server
接下来,运行以下命令检查没有错误:
sudo systemctl status isc-dhcp-server
守护进程应该报告它是 active (running),类似于以下截图所示:

图 11.3:成功启动后检查 isc-dhcp-server 进程的状态
假设一切顺利,你的 DHCP 服务器应该正在运行。当 IP 租约分配给客户端时,它会记录在 /var/lib/dhcp/dhcpd.leases 文件中。当你的 DHCP 服务器运行时,它还会将信息记录到服务器的系统日志中,该日志位于 /var/log/syslog。要查看你的 DHCP 服务器的完整功能,你可以通过以下命令查看日志的实时写入:
sudo tail -f /var/log/syslog
我们在书中之前讨论过 tail 命令的 -f 标志。这个选项是必不可少的,作为服务器管理员,你可能会经常使用它。使用 -f 选项时,你可以实时查看日志的写入内容,而不需要手动刷新它。你可以按 Ctrl + c 来中断文件查看。
当你的 DHCP 服务器运行时,每当收到 DHCP 请求或租约提供给客户端时,你会看到通知出现在 syslog 文件中。一个典型的 DHCP 请求日志类似于以下内容:
Oct 5 22:07:36 myserver dhcpd: DHCPDISCOVER from 52:54:00:88:f8:bc via enp0s3
Oct 5 22:07:36 myserver dhcpd: DHCPOFFER on 192.168.1.103 to 51:52:01:87:f7:bc via enp0s3
活跃和之前的 DHCP 租约会存储在 /var/lib/dhcp/dhcpd.leases 文件中,该文件中的一个典型租约条目看起来类似于以下内容:
lease 192.168.1.138 {
starts 0 2022/10/05 16:37:30;
ends 0 2022/10/06 16:42:30;
cltt 0 2022/20/06 16:37:30;
binding state active;
next binding state free;
rewind binding state free;
hardware ethernet 32:6e:92:01:1f:7f;
}
当一个新设备连接到你的网络并从新的 DHCP 服务器获取 IP 地址时,你应该会看到租约信息出现在该文件中。这个文件非常有用,因为每当你连接一个新设备时,你不需要直接询问设备本身来找出它的 IP 地址。你只需查看 /var/lib/dhcp/dhcpd.leases 文件。如果设备公开了它的主机名,你会在它的租约条目中看到该信息。一个典型的使用场景是将 Raspberry Pi 连接到你的网络。插上电源并启动后,你可以在 dhcpcd.leases 文件中看到它的 IP 地址,然后通过 SSH 使用该 IP 地址连接,而无需连接显示器查看它获得了哪个 IP 地址。同样,你也可以查看你插入的新的网络设备的临时 IP 地址,从而能够连接并进行配置。
如果你在设置 isc-dhcp-server 守护进程时遇到任何问题,请仔细检查你是否在静态 IP 分配和 /etc/dhcp/dhcpd.conf 文件中设置了所有正确匹配的值。例如,你的服务器必须与分配给客户端的 IP 地址处于同一网络中。只要一切匹配,你应该没问题,服务也应该能够正常启动。
接下来,我们来看看网络中的另一个重要服务——DNS。
添加 DNS 服务器
我相信你们大多数人都熟悉 域名系统 (DNS) 服务器的作用。最简单的定义是,它是一项负责将 IP 地址与域名或主机名匹配的服务。当你连接到互联网时,浏览时会不断发生域名到 IP 地址的匹配。毕竟,使用域名连接到 www.google.com/ 比记住其 IP 地址要容易得多。
当你连接到互联网时,你的工作站或服务器将连接到外部 DNS 服务器,以便找出你尝试访问的网站的 IP 地址。
在你的组织内部运行本地 DNS 服务器也非常常见。其好处是你可以解析本地主机名,而外部 DNS 服务器则无法知道这些内容。例如,如果你有一个内部网网站,打算让同事们访问,给每个人提供一个本地域名让他们访问要比让大家记住它的 IP 地址更加容易。使用本地 DNS 服务器,你将创建一个被称为 区域文件 的文件,该文件包含网络内正在使用的主机和 IP 地址的信息,这样本地设备就可以解析它们。如果本地 DNS 服务器无法处理你的请求(例如请求访问外部网站),服务器会将请求转发到外部 DNS 服务器,然后外部 DNS 服务器将执行该请求。
虽然 DNS 是一个广泛的话题,可能需要一些时间才能掌握,但基本的理解就足以让你在网络中使用自定义的 DNS 服务器。在本节中,我将向你展示如何设置你自己的 DNS 服务器,使你的设备能够解析本地主机名,从而大大提升你的网络效率。
使用 BIND 设置外部 DNS
要搭建你自己的 DNS 服务器,我们首先需要在服务器上安装 伯克利互联网名称守护进程(BIND)包:
sudo apt install bind9
现在,我们应该已经在服务器上运行了 bind9 服务,尽管它此时还没有实际配置做什么。bind 最基本的功能是充当 缓存名称服务器,这意味着该服务器本身并不会匹配任何名称。相反,它会缓存来自外部服务器的响应——在某些情况下,这实际上可以提高你网络的性能。我们稍后会为 bind 配置实际的主机,但设置一个缓存名称服务器是开始的好方法。
为此,打开 /etc/bind/named.conf.options 文件,使用你喜欢的文本编辑器。
在文件中,你应该看到一段类似于以下内容的文本:
// forwarders {
// 0.0.0.0;
// };
取消注释这些行。对于这个配置文件来说,斜杠是注释符号,所以去掉它们。然后,我们可以添加几个外部 DNS 服务器的 IP 地址。你可以使用 互联网服务提供商(ISP)的 DNS 服务器 IP 地址,或者你也可以直接使用 Google 的 DNS 服务器(8.8.8.8 和 8.8.4.4):
forwarders {
8.8.8.8;
8.8.4.4;
};
保存文件后,重启 bind9 服务:
sudo systemctl restart bind9
为了确保一切正常运行,请检查服务的状态:
systemctl status bind9
它应该报告active (running)。只要您输入了所有内容,现在应该有一个工作中的 DNS 服务器。当然,我们还没有添加任何 DNS 名称来解析,但我们会做到这一点。现在,您只需要配置网络上的其他设备以使用您的新 DNS 服务器。这样做的最简单方法是重新配置我们在上一节中设置的isc-dhcp-server服务。记住指定服务器向客户端分配地址池的部分吗?它还包含一个声明客户端将使用的 DNS 服务器的部分。以下是该部分,关键行用粗体标出:
subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.100 192.168.1.240;
option routers 192.168.1.1;
**option domain-name-servers 192.168.1.1;**
}
要配置网络上的设备使用您的新 DNS 服务器,您只需更改配置option domain-name-servers 192.168.1.1;以指向正在设置的新 DNS 服务器的 IP 地址。当客户端请求 DHCP 租约(或尝试更新现有租约)时,它们将自动配置为使用新的 DNS 服务器。
刚刚设置的缓存名称服务器将会首先为使用它的主机检查他们尝试查找的任何主机名。如果他们查找的网站或主机不在您的本地网络中,他们的请求将被转发到您为bind配置的转发地址。
在我的示例中,我使用了 Google 的 DNS 服务器,因此如果您使用我的配置,您的主机将首先检查您的本地服务器,然后在解析外部名称时检查 Google 的服务器。根据您的网络硬件和配置,您甚至可能会看到轻微的性能提升。这是因为您刚刚设置的 DNS 服务器正在缓存针对它的任何查找。例如,如果客户端在 Web 浏览器中查找www.packtpub.com,您的 DNS 服务器将转发该请求,因为该站点在本地不存在,它还会记住结果。下次您的网络内部的客户端查找该站点时,响应将更快,因为您的 DNS 服务器已将其缓存。
要自行查看,请在使用您的新 DNS 服务器的节点(设备)上两次执行以下命令:
dig www.packtpub.com
如果您的安装中没有dig,可以将其作为dnsutils软件包的一部分安装:
sudo apt install dnsutils
在响应中,查找末尾附近给出查询时间的行。它看起来类似于以下内容:
;; Query time: 98 msec
再次运行时,查询时间应该更低:
;; Query time: 1 msec
这是您的缓存名称服务器在起作用!即使我们还没有设置任何区域文件来解析您的内部服务器,您的 DNS 服务器已经为您的网络增添了价值。您刚刚为我们后续的配置奠定了基础。
设置内部 DNS 并添加主机
现在,让我们向 DNS 服务器添加一些主机,以便我们可以开始充分利用它。bind的配置文件位于/etc/bind/named.conf。除了一些已注释的行外,它还包含以下三行配置:
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";
正如您所见,默认的bind配置分为几个配置文件。在这里,它包括另外三个:named.conf.options、named.conf.local和named.conf.default-zones(我们已经编辑过其中的第一个)。为了解析本地名称,我们需要创建一个区文件,这本质上是一个包含一些配置、主机列表及其 IP 地址的文本文件。为了做到这一点,我们需要告诉bind在哪里找到我们即将创建的区文件。在/etc/bind/named.conf.local中,我们需要在文件末尾添加像以下的代码块:
zone "local.lan" IN {
type master;
file "/etc/bind/net.local.lan";
};
注意到区域被命名为local.lan,这与我们在 DHCP 服务器配置中为我们的域指定的名称相同。在可能的情况下保持一致是最好的。如果您使用的域名与我在示例中使用的不同,请确保它在这里也匹配。在这个区块中,我们正在创建一个master区文件,并告知bind它可以在/etc/bind目录中找到一个名为net.local.lan的文件。这应该是我们需要对named.conf.local文件做的唯一更改;我们只会创建一个区文件(在本节中)。保存此文件后,您需要创建/etc/bind/net.local.lan文件。因此,请打开文本编辑器中的该文件。因为我们还没有创建它,所以它应该是空的。以下是这个区文件的示例,完全填写了一些样例配置:
$TTL 1D
@ IN SOA local.lan. hostmaster.local.lan. (
202208161; serial
8H ; refresh
4H ; retry
4W ; expire
1D ) ; minimum
IN A 192.168.1.1
;
@ IN NS hermes.local.lan.
fileserv IN A 192.168.1.3
hermes IN A 192.168.1.1
mailserv IN A 192.168.1.5
mail IN CNAME mailserv.
web01 IN A 192.168.1.7
随意编辑此文件以匹配您的配置。您可以编辑文件末尾的主机列表,以匹配您网络中的主机,因为我包含的只是示例。您还应确保该文件与您网络的 IP 方案匹配。接下来,我将逐行介绍每行以帮助您更深入理解此配置文件中每行的责任:
$TTL 1D
生存时间(TTL)确定记录在 DNS 服务器中可以被缓存的时间长度。如果您回忆一下之前我们用dig命令练习时,您会看到第二次查询同一域名时,查询时间比第一次运行命令时少。这是因为您的 DNS 服务器缓存了结果,但它不会永远保留它。在某个时刻,查找将会过期。在缓存的结果过期后再次查找同一域名时,您的服务器将会再次从 DNS 服务器获取结果。在我的示例中,我使用了 Google 的 DNS 服务器。这意味着在某个时候,您的服务器将会在记录过期后再次向这些服务器查询。
使用授权开始(SOA)行,我们确定我们的 DNS 服务器对local.lan域具有权威性:
@ IN SOA local.lan. hostmaster.local.lan. (
我们还将hostmaster@local.lan设置为该服务器的责任方的电子邮件地址,但我们在这里以不同的格式输入它,以便bind使用(hostmaster.local.lan)。显然这是一个虚假的地址,但对于内部 DNS 服务器来说,它的有效性并不重要。
在区域文件的所有配置行中,serial绝对是最让我们头疼的:
202208161; serial
这是因为仅仅在我们对区域文件做任何更改时(更改 IP 地址、添加或删除主机等),更新区域文件是不够的;我们还需要记得至少递增一次序列号。如果我们不这样做,bind不会知道我们做了任何更改,因为它会首先查看序列号,然后再检查文件的其他部分。
这样做的问题是你我都是人类,容易忘记事情。我多次忘记更新serial,并且当 DNS 服务器拒绝解析最近添加的新主机时,我感到非常沮丧。一旦我记得是因为没有递增序列号,问题就解决了。因此,你必须记住,每当你对任何区域文件进行更改时,都需要递增序列号。
格式并不重要;我使用的是202208161,这只是年份、两位数的月份、两位数的日期,再加上一个额外的数字,以防我们在一天内做多次更改(这种情况有时会发生)。只要你每次修改区域文件时递增一次序列号,不管使用什么格式,你都会处于一个不错的状态。不过,我在这里给出的样本格式在业界其实非常常见。
这些值控制着辅助 DNS 服务器多久会被指示检查更新:
8H ; refresh
4H ; retry
4W ; expire
1D ) ; minimum
使用示例中的刷新值,我们指示任何辅助 DNS 服务器每八小时检查一次,看看是否更新了区域记录。retry字段规定了辅助服务器在发生错误时,应该等待多久再进行检查。该部分的最后两个选项,expire和minimum分别设置了区域文件的最小和最大寿命。正如我之前提到的,关于 DNS 与bind的全面讨论足以成书。现在,我建议你先使用这些值,直到你有理由进行更多的实验。在这里,我们标识了名称服务器本身:
IN A 192.168.1.1
@ IN NS hermes.local.lan.
在我的情况下,服务器叫做hermes,位于192.168.1.1。
接下来,在我们的文件中,我们会有几个主机条目,以便通过名称在网络上解析我们的资源:
fileserv IN A 192.168.1.3
hermes IN A 192.168.1.1
mailserv IN A 192.168.1.5
mail IN CNAME mailserv.
web01 IN A 192.168.1.7
在这个例子中,我有三个主机:fileserv、mailserv 和 web01。在这个例子中,这些都是地址记录,意味着每当我们的服务器被请求解析这些名称时,它将返回相应的 IP 地址。如果我们的 DNS 服务器被设置为某台机器的主要 DNS 服务器,询问fileserv时,它将返回192.168.1.3,而询问web01时,它将返回192.168.1.7。
mail条目是特殊的,因为它不是一个地址记录,而是一个规范名称(CNAME)记录。在这种情况下,它只是指向mailserv。本质上,CNAME 记录的作用就是:它创建一个指向另一个资源的指针。在这种情况下,如果有人尝试访问名为mail的服务器,我们会将他们重定向到实际的mailserv服务器。请注意,在 CNAME 记录中,我们不是输入一个 IP 地址,而是输入它所链接资源的主机名。
此外,你还应该注意到我将 DNS 服务器本身(hermes)也添加到了文件中。你可以在上面的第二行看到它。我发现如果你不这样做,DNS 服务器可能会抱怨并拒绝加载该文件。
现在我们已经有了区域文件,我们应该能够开始使用它。首先,我们需要重新启动bind9服务:
sudo systemctl restart bind9
命令执行完成后,检查是否有任何错误:
systemctl status bind9
你应该看到服务状态为active (running),此外,你还应该看到一行提示你区域文件的序列号已经加载。如果你看到服务未运行和/或区域文件未加载,检查状态时的输出应该会提供一些具体的信息,帮助你找到正确的方向。如果没有,你也可以查看系统日志,查找与bind相关的线索:
cat /var/log/syslog | grep bind9
我见过的最常见的错误通常是因为文件中没有保持一致。例如,如果你使用的是不同的 IP 方案(如10.10.10.0/24),你需要确保没有忘记将我示例中的 IP 地址替换为适当的方案。如果一切顺利,你应该能够将网络中的设备指向使用这个新的 DNS 服务器。确保你不仅测试网络内设备的 ping 通情况,还要测试外部资源,比如网站。如果 DNS 服务器正常工作,它应该能够解析你本地的名称,然后如果它没有在本地找到所需的内容,它将把请求转发到外部 DNS 服务器(我们设置为转发器的两个服务器)。此外,你还需要确保网络防火墙中的53端口(UDP)是开放的,因为这是 DNS 使用的端口。这个问题极为罕见,但我确实见过它发生。
为了进一步测试我们的 DNS 服务器,我们可以使用 dig 命令,正如我们在实验缓存时所做的那样。尝试对你本地局域网的服务器进行 dig 测试,以及对不在你局域网内的 DNS 地址进行测试(将第一个域名更改为你局域网中的实际域名):
dig webserv.local.lan
dig www.packtpub.com
你应该会看到类似以下的响应:
;; Query time: 1 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Sat Feb 10 10:00:59 EST 2022
;; MSG SIZE rcvd: 83
你在这里要找的是本地资源和外部网站现在都能被解析。你可能会注意到,输出中使用的 DNS 服务器很可能会显示为本地地址,就像我的输出一样,而不是我们刚刚设置的 DNS 服务器的地址。实际上,你可以忽略这个问题。如今,大多数 Linux 发行版使用本地解析器,它实际上会将 DNS 查询结果缓存到你的本地计算机中。你的计算机仍然在使用我们设置的 DNS 服务器,只是你的计算机和 DNS 服务器之间多了一个额外的层级。你可以通过以下命令验证这一点:
resolvectl
输出将显示实际响应 DNS 查询的服务器的 IP 地址。
接下来,让我们来看看设置互联网网关的过程。如果你的网络上没有路由器或防火墙作为你互联网连接和内网之间的设备,这将是一个值得考虑的选项。
设置互联网网关
既然我们已经在设置网络服务,那不妨继续设置一个路由器作为我们网络的网关。在网络中,网关是你用来从一个网络路由到另一个网络的设备。在这种情况下,互联网网关将是你本地网络和提供互联网连接的设备(例如有线调制解调器)之间的设备。一个典型网络中的网关通常是一个商业路由器或防火墙,它通常还会提供 DNS、DHCP 和路由服务。
如果你的网络中已经有这样一个设备提供这些服务,那么你无需做任何操作。你可以跳过这一部分。但如果你想自己设置一个路由器,请继续进行。
如果你想继续设置一个路由器,那么首先要做的就是决定网络中哪个设备将承担这个任务。通常,管理员会将 DNS、DHCP 和路由服务集成到同一台服务器上,所以你甚至可以使用之前用于处理 DNS 和 DHCP 示例的相同设备来完成本节的任务。为了使设备作为网关,它应该至少有两个网络接口,一个连接到你的 ISP 设备(例如有线调制解调器),另一个接口连接到你的网络交换机,其他服务器将连接到该交换机。连接到 ISP 设备的接口应该使用 DHCP,因此它将直接从 ISP 获得 IP 地址。如果相关的话,这个接口可能需要一个由 ISP 提供的静态 IP 地址。
根据你拥有的互联网连接类型,Linux 本身可能可以替代你的互联网调制解调器所连接的任何设备。一个很好的例子是你办公室或家庭路由器可能使用的有线调制解调器。在这种情况下,调制解调器提供互联网连接,然后路由器允许你网络上的其他设备访问它。在某些情况下,你的调制解调器和路由器甚至可能是同一个设备。因此,根据你拥有的硬件,这种网络设置方法可能会有效或无效。但如果你有合适的硬件,你将能够轻松使用 Ubuntu Server 管理整个网络堆栈。
为什么你可能想要创建自己的互联网网关?一个潜在的原因是,商业路由器和防火墙设备通常不会提供安全补丁。新的漏洞总是不断被发现,如果你的路由器或防火墙不再得到制造商的支持,它可能会让外部威胁进入你的网络。通过使用 Ubuntu 搭建互联网网关,你将受益于 Canonical 提供的定期更新。只要你使用的 Ubuntu 版本仍然受到支持(例如 Ubuntu 22.04 的 LTS 版本),你将享受到一个更安全的平台。如果没有其他原因,我们可以在其上安装 Ubuntu 的物理服务器通常比商业设备拥有更强大的 CPU,这意味着由于 CPU 瓶颈导致的网络性能下降不太可能发生。
幸运的是,设置互联网网关非常简单。实际上,我们只需要执行一个命令来设置接口之间的路由,这在技术上就是设置互联网网关所需要的全部内容。但在开始之前,同样需要记住的是,如果你设置了互联网网关,你需要特别注意安全。位于你的网络和调制解调器之间的设备将会成为一个持续的攻击目标,就像其他任何网关设备一样。当谈到商业路由器时,它们也会持续受到攻击。
然而,在大多数情况下,它们都会内建某种默认的安全性或防火墙。坦率地说,常见路由设备中内置的安全功能非常差,而且当有人想要破解时,它们中的大多数都很容易被攻破。关键是,这些设备一开始就有某种安全措施(不论好坏),而你自建的互联网网关在你添加安全措施之前根本没有任何安全性。
当你设置一个互联网网关时,你需要特别注意设置防火墙,限制 SSH 访问,使用非常强的密码,保持安全补丁的更新,并安装认证监控工具,如fail2ban。我们将在第二十一章《保护你的服务器》中深入探讨这些主题。不过,我之所以现在提到这个,是因为如果你设置了互联网网关,你可能想绕道立刻阅读该章节,以确保正确地保护它。
无论如何,我们继续。正如我之前提到的,一个合适的互联网网关会有两个以太网端口。在第一个端口,你将插入你的电缆调制解调器或互联网设备,第二个端口连接到交换机。不过,默认情况下,这些接口之间的路由是禁用的,所以流量不能从一个以太网端口流动到另一个。要解决这个问题,请使用以下命令:
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
其实就这样。通过这一条命令,你就将服务器变成了路由器。然而,这个更改不会在重启后生效。要使其永久生效,请在编辑器中打开/etc/sysctl.conf文件:
sudo nano /etc/sysctl.conf
查找以下行:
#net.ipv4.ip_forward=1
通过去掉行前的哈希符号来取消注释该行,并保存文件。做了这个更改后,即使重启后,服务器也将允许接口之间的路由。在本章中,我们讨论的所有主题中,这个可能是最简单的。然而,我再次提醒你,如果你的服务器是面向互联网的前端设备,务必确保它的安全,因为计算机安全的学生总喜欢在现实生活中的 Linux 服务器上练习。通过良好的安全措施,你能确保他们不会轻易攻击你,或者至少让他们更加难以突破。
从这里开始,你只需要将网络交换机连接到另一个网络接口,然后你就可以将其他有线以太网设备和无线接入点连接到交换机。现在,Ubuntu Server 已经在管理你的整个网络!
总结
在本章中,我们探讨了额外的网络主题。我们从规划网络的 IP 地址方案开始,这样你就可以为不同类型的节点(如服务器和网络设备)创建分组,并为 DHCP 规划地址池。我们还处理了设置 DHCP 和 DNS 服务器的过程,这为我们在配置网络服务时提供了额外的灵活性,例如在为 DHCP 定义自定义 IP 方案时,以及通过名称解析网络中设备的主机名。我们以讨论设置互联网网关作为面向互联网的路由器结束了本章内容。
在下一章,我们将讨论如何通过网络共享和传输文件。这将包括涵盖 NFS 和 Samba 共享,以及使用scp、rsync和sshfs。敬请期待!
进一步阅读
-
理解 IP 子网划分的 8 个步骤:
learnlinux.link/8-ip-sub -
IP 子网计算器:
learnlinux.link/s-calc
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者及其他读者进行讨论:

第十二章:共享和传输文件
在前一章中,我们看到了设置一些网络服务(如 DHCP 和 DNS)涉及的过程。这些是网络的两个重要组成部分,但你可以在你的网络上提供许多不同类型的应用程序和资源,以进一步增强它。文件服务器就是其中的一个例子,它可以为用户提供一个集中存储关键文件的地方,甚至可以增强协作。
或许你之前使用过文件服务器,甚至在不同平台上设置过文件服务器。在 Ubuntu 服务器上,不仅有多种存储文件的方法,还可以通过网络链路将文件从一个节点传输到另一个节点。在本章中,我们将探讨如何使用 Samba 和 NFS 设置中央文件服务器,以及如何使用scp和rsync等工具在节点之间传输文件。我们还将讨论一些情况,在这些情况下,一种解决方案可能比另一种更适合你。在讨论这些概念时,我们将涵盖以下主题:
-
文件服务器考虑因素
-
通过 Samba 与 Windows 用户共享文件
-
设置 NFS 共享
-
使用
rsync传输文件 -
使用
scp传输文件
在我们开始配置服务器以使其能够与其他用户共享文件之前,我们首先应该了解一下我们可以选择的可用选项,以便为我们的用例选择最佳技术。
文件服务器考虑因素
在设置文件服务器方面,过程是设置某种守护程序以接受连接并共享特定目录,并确保适当的用户能够访问这些目录。您还将实施权限以确定谁可以访问特定目录,以及他们将拥有的访问类型(读/写、只读等)。在决定如何共享文件时,通常是在两种常见技术之间进行选择,即Samba和NFS。
总的来说,没有什么可以阻止您在单个服务器上托管 Samba 和 NFS 共享。这两种技术实际上可以在同一设备上共存。然而,这两种流行的解决方案各自更适合特定的用例。在开始设置文件服务器之前,我们应该先了解 Samba 和 NFS 之间的区别,以便能够就我们的环境选择更合适的一个。作为一个经验法则,Samba 非常适合混合环境(其中既有 Windows 客户端也有 Linux 客户端),而 NFS 更适合在 Linux 或 Unix 环境中使用,但实际情况比这复杂得多。
Samba 是许多环境中的一个好方案,因为它允许你与 Windows、Linux 和 macOS 机器共享文件。基本上,只要你赋予权限,几乎所有人都能访问你的共享。之所以能够这样,是因为 Samba 是 服务器消息块(SMB)协议的重新实现,而该协议主要由 Windows 系统使用。然而,你不需要使用 Windows 平台也能访问 Samba 共享,因为许多平台都支持这一协议。
你可能会想知道,为什么我在这一章会讲解两种不同的解决方案。毕竟,如果 Samba 共享几乎能被所有设备和用户访问,为什么还要考虑其他的呢?尽管 Samba 有许多优点,但也有一些缺点。首先,权限的处理方式差异很大,因此你需要以特定的方式配置共享,以防止那些不应访问机密数据的用户进入。而 NFS 完全支持标准 UNIX 权限,因此你只需配置一次权限。如果权限和机密性对你来说很重要,可能需要更仔细地考虑 NFS。
说 Windows 系统无法访问 NFS 共享并不准确,因为某些版本实际上是可以的。默认情况下,Windows 的任何版本都不直接支持 NFS,但一些版本提供了一个插件,可以让你安装它以启用此支持。如果你安装了 NFS 服务,从那时起,你的 Windows 安装将能够访问 NFS 共享。
关于全 Linux 环境或只有 Linux 机器需要访问共享的情况,NFS 是一个不错的选择,因为它与操作系统的集成更加紧密。权限可以更容易地进行控制,并且根据硬件的不同,性能可能更高。具体的环境会帮助你做出决定。也许你会为混合环境选择 Samba,或为全 Linux 环境选择 NFS。也可能你会同时设置 NFS 和 Samba,为每个平台提供共享。我建议你学习并实践这两种方案,因为无论如何,你在职业生涯的某个时刻都将使用到这两种解决方案。
在继续设置 Samba 和 NFS 的部分之前,我建议您首先确定您的文件系统中哪个位置作为文件共享的父目录。这并非必需,但我认为这样更有利于组织。并没有一个固定的地方来存储您的共享文件,但我个人喜欢在 root 文件系统下创建一个 /share 目录,并在其中创建子目录用于我的网络共享。例如,我可以创建 /share/documents、/share/public 等用于 Samba 共享的子目录。关于 NFS,我通常在 /exports 下创建共享目录。您可以选择如何设置您的目录结构。在阅读本章的其余部分时,请确保根据您的需要更改示例路径。
通过 Samba 向 Windows 用户共享文件
在这一部分,我将向您介绍如何设置您自己的 Samba 文件服务器。我还将讲解一个示例配置,以帮助您入门,以便您可以添加自己的共享。首先,我们需要确保在我们的服务器上安装了 samba 软件包:
sudo apt install samba
当您安装 samba 软件包时,您的服务器上将安装一个新的守护程序 smbd。smbd 守护程序将自动启动和启用。同时,您还将获得一个位于 /etc/samba/smb.conf 的默认 Samba 配置文件。目前,我建议停止 samba,因为我们还没有配置它:
sudo systemctl stop smbd
由于我们将从头开始配置 Samba,我们应该从一个空白的配置文件开始。让我们备份原始文件,而不是直接覆盖它。默认文件包含一些有用的注释和示例,所以我们应该保留它以备将来参考:
sudo mv /etc/samba/smb.conf /etc/samba/smb.conf.orig
现在,我们可以开始一个全新的配置。虽然这不是必需的,但我喜欢将我的 Samba 配置分成两个文件,/etc/samba/smb.conf 和 /etc/samba/smbshared.conf。你不一定要这样做,但我认为这样可以使配置更清晰、更易于阅读。首先,这是一个 /etc/samba/smb.conf 文件的示例:
[global]
server string = File Server
workgroup = WORKGROUP
security = user
map to guest = Bad User
name resolve order = host bcast wins
include = /etc/samba/smbshared.conf
正如您所见,这是一个非常简短的文件。基本上,我们只包括了设置 Samba 文件服务器所需的行。接下来,我将解释每一行及其作用。
[global]
使用 [global] 部分,我们声明了配置的全局部分,其中包含将影响整个 Samba 的设置。还会有额外的部分用于单独的共享,我们稍后会讨论到。
server string = File Server
server string 在某种程度上是 File Server 的描述字段。如果您以前从 Windows 计算机浏览网络,可能已经看到这个字段。无论您在此处键入什么内容,都将显示在 Windows Explorer 中服务器名称下方。这不是必需的,但是有这个字段还是挺不错的。
workgroup = WORKGROUP
在这里,我们设置了 workgroup,它与 Windows PC 上的 workgroup 完全相同。简而言之,工作组是一个命名空间,用于描述一组计算机。在 Windows 系统中浏览网络共享时,你会看到一组工作组列表,然后在该工作组中看到一台或多台计算机。简而言之,这是用来逻辑地将你的节点进行分组的方法。你可以将它设置为任何你喜欢的名称。如果你已经在组织中有一个工作组,你应该在这里设置它,以匹配其他计算机的工作组名称。如果你没有自定义工作组名称,Windows PC 上的默认工作组名称就是 WORKGROUP。
security = user
这个设置配置 Samba 使用用户名和密码进行服务器身份验证。在这里,我们将 security 模式设置为 user,这意味着我们使用本地用户进行身份验证,而不是使用 ads(Active Directory)或 domain(域控制器)等其他选项,这些选项都不在本书的范围内。
map to guest = Bad User
这个选项配置 Samba 将未认证的用户视为访客用户。基本上,未认证的用户仍然可以访问共享,但他们将拥有访客权限,而不是完全权限。如果你不希望这样,你可以从文件中省略这一行。请注意,如果你省略了这一行,你需要确保服务器和客户端计算机两边的用户帐户名称是相同的。理想情况下,我们希望使用基于目录的身份验证,但那超出了本书的范围。
name resolve order = host bcast wins
name resolve order 设置配置 Samba 如何解析主机名。在这个设置中,我们首先使用广播名称,然后是 wins。由于 wins 已经基本上被弃用(并被 DNS 取代),我们在这里包含它只是为了兼容遗留网络。
include = /etc/samba/smbshared.conf
记得我提到过我通常会将我的 Samba 配置分成两个文件吗?在这一行,我正在调用第二个 /etc/samba/smbshared.conf 文件。smbshared.conf 文件的内容将在这里插入,就像我们只有一个文件一样。我们还没有创建 smbshared.conf 文件。接下来让我们来创建它。这是一个示例 smbshared.conf 文件:
[Documents]
path = /share/documents
force user = myuser
force group = users
public = yes
writable = no
[Public]
path = /share/public
force user = myuser
force group = users
create mask = 0664
force create mode = 0664
directory mask = 0777
force directory mode = 0777
public = yes
writable = yes
正如你所看到的,我将共享声明分离到它们自己的文件中。我们可以在 smbshared.conf 中看到一些有趣的内容。首先,我们有两个段落,[Documents] 和 [Public]。每个段落都是一个共享名称,它将允许 Windows 用户通过 //servername/share-name 访问共享。在这种情况下,这个文件将为我们提供两个共享://servername/Documents 和 //servername/Public。Public 共享对所有人可写,而 Documents 共享则限制为只读。Documents 共享具有以下选项:
path = /share/documents
这是共享的路径,它必须存在于服务器的文件系统中。在这种情况下,当用户在 Windows 系统上从 //servername/Documents 读取文件时,他们实际上是在从托管共享的 Ubuntu 服务器上的 /share/documents 读取数据。
force user = myuser
force group = users
这两行基本上是绕过了用户所有权。当用户访问此共享时,他们会被当作 myuser 而不是他们实际的用户帐户。通常,你会希望设置 LDAP 或 Active Directory 来管理你的用户帐户,并处理它们与 Ubuntu 服务器的映射,但本书并未详细讨论基于目录的用户访问,因此我提供了 force 选项作为一个简单的起点。你在这里设置的用户帐户必须存在于服务器上。
public = yes
writable = no
通过这两行配置,我们设置了连接到此共享后的用户可以做什么。在这种情况下,public = yes 意味着共享是公开可用的,尽管 writable = no 防止任何人对该共享的内容进行更改。如果你想与他人共享文件,但又想限制访问并阻止任何人修改内容,这是很有用的。
Public 共享具有一些在 Documents 共享中没有的额外设置:
create mask = 0664
force create mode = 0664
directory mask = 0777
force directory mode = 0777
通过这些选项,我正在设置如何处理当新内容添加到共享时,文件和目录的权限。目录将被赋予 777 权限,而文件将被赋予 664 权限。是的,这些权限非常开放;请注意,共享名为 Public,意味着无论如何都应有完全访问权限,它的预期用途是存放那些不涉及机密或限制的数据:
public = yes
writable = yes
就像我在之前的共享中做的那样,我设置了共享为公开可用,但这次我还配置它允许用户进行更改。
为了利用此配置,我们需要启动 Samba 守护进程。在此之前,我们要再次检查我们在 smbshared.conf 文件中输入的目录是否存在,所以如果你使用我的示例,你需要创建 /share/documents 和 /share/public:
sudo mkdir /share
sudo mkdir /share/documents
sudo mkdir /share/public
此外,强制用户中提到的用户帐户和强制组中提到的组必须都存在并且对共享目录拥有所有权:
sudo chown -R jay:users /share
此时,使用 testparm 命令是个好主意,它将测试我们的 Samba 配置文件的语法。它不一定能发现我们可能犯的每一个错误,但它是一个快速检查配置是否正常的好命令。此命令首先会检查语法,然后会将整个配置打印到终端,供你查看。如果你在这里没有看到错误,那么可以继续启动服务:
sudo systemctl start smbd
然后,检查状态以确保守护进程正在运行:
sudo systemctl status smbd
这将产生类似以下内容的输出:
图 12.1:检查 smbd 守护进程的状态
这应该就是全部了;你现在应该有 Documents 和 Public 共享在你的文件服务器上,Windows 用户应该能够访问这些共享。实际上,你的 Linux 机器也应该能够访问这些共享。在 Windows 上,Windows 资源管理器能够浏览你网络上的文件共享。如果有疑问,尝试同时按下 Windows 键和 r 键,打开 运行 对话框,然后输入共享的 通用命名约定(UNC)路径(例如 \\servername\Documents 或 \\servername\Public)。你应该能够看到存储在这两个目录中的任何文件。对于 Public 共享,你也应该能够在那里创建新文件。
在 Linux 系统中,如果安装了桌面环境,大多数桌面环境都带有支持浏览网络共享的文件管理器。由于有多种不同的桌面环境可供选择,方法因发行版或配置的不同而有所不同。通常,大多数 Linux 文件管理器都会在文件管理器中有一个网络链接,允许你轻松浏览本地共享。以 Ubuntu 为例,你可能需要通过 apt 安装 smbclient 和 cifs-utils,如果遇到错误,可以尝试安装这些软件包。在以下截图中,一台 Ubuntu 22.04 桌面客户端正在浏览本地 Samba 服务器上的共享:

图 12.2:从 Linux 客户端浏览 Samba 共享
如果你的文件管理器没有显示服务器上可用的共享,你也可以通过在 /etc/fstab 文件中为其添加条目来访问 Samba 共享,例如以下内容:
//myserver/share/documents /mnt/documents cifs username=myuser,noauto 0 0
然后,假设本地目录存在(示例中的fstab行是/mnt/documents),你应该能够使用以下命令挂载该共享:
sudo mount /mnt/documents
在 fstab 条目中,我包含了 noauto 选项,这样系统在启动时不会自动挂载 Samba 共享(你需要手动使用 mount 命令来挂载)。如果你希望在启动时自动挂载 Samba 共享,可以将 noauto 改为 auto。但是,如果由于某种原因,托管 Samba 共享的服务器无法访问,你可能会在启动时遇到错误,这就是为什么我更倾向于使用 noauto 选项。
如果你希望在不添加 fstab 条目的情况下挂载 Samba 共享,以下示例命令应该能够实现;只需更改共享名称和挂载点,以匹配你的本地配置:
sudo mount -t cifs //myserver/Documents -o username=myuser /mnt/documents
从这里开始,随意进行实验。你可以根据需要添加额外的共享,并根据自己的需求自定义 Samba 实现。在下一节中,我们将探讨 NFS。
设置 NFS 共享
Samba 的替代方案是NFS。它是一种非常好的方法,用于从 Linux 或 Unix 服务器向另一台 Linux 或 Unix 服务器共享文件。正如我在本章前面提到的,Windows 系统也可以访问 NFS 共享,但这需要启用一个附加组件。因此,在 Linux 或 Unix 环境中,NFS 是首选,因为它完全支持 Linux 和 Unix 风格的权限。正如我们之前深入了解 Samba 时看到的,我们基本上强制要求所有共享目录都以特定用户身份进行访问,虽然这种做法有些混乱,但它是最简单的 Samba 服务器设置示例,而不需要我们进一步讲解复杂的 Windows Active Directory 部署。Samba 当然可以支持基于用户的访问限制,并且在集中的目录服务器上会有很大的优势,尽管那几乎会成为一本书!NFS 在非混合环境中更具集成性。
之前,我们在文件系统中设置了一个父目录来存放我们的 Samba 共享,NFS 也应该做同样的事情。虽然对于 Samba 来说,拥有一个专门的父目录并不是强制要求(我让你这么做是为了保持整洁,但你并不一定需要这么做),但 NFS 确实希望有一个自己的目录来存放所有共享。虽然 NFS 也不是严格要求这样做,但这样做有额外的好处,我将在本节结束前进行说明。在我的例子中,我将使用/exports作为示例,因此你应该确保该目录或你为 NFS 选择的目录已经存在:
sudo mkdir /exports
接下来,让我们在服务器上安装所需的 NFS 软件包。以下命令将安装 NFS 及其依赖项:
sudo apt install nfs-kernel-server
一旦你安装了nfs-kernel-server软件包,nfs-kernel-server守护进程将自动启动。它还会创建一个默认的/etc/exports文件(这是 NFS 读取共享信息的主要文件),但该文件并不包含任何有用的设置,仅包含一些注释行。让我们备份/etc/exports文件,因为我们将创建我们自己的文件:
sudo mv /etc/exports /etc/exports.orig
为了设置 NFS,首先让我们创建一些将与其他用户共享的目录。在 NFS 中,每个共享目录被称为export。我将使用以下目录作为示例,但你可以导出任何你喜欢的目录:
/exports/backup
/exports/documents
/exports/public
在/etc/exports文件中(我们正在重新创建这个文件),我将插入以下四行:
/exports *(ro,fsid=0,no_subtree_check)
/exports/backup 192.168.1.0/255.255.255.0(rw,no_subtree_check)
/exports/documents 192.168.1.0/255.255.255.0(ro,no_subtree_check)
/exports/public 192.168.1.0/255.255.255.0(rw,no_subtree_check)
第一行是我们的导出根目录,稍后我会详细介绍。接下来的三行是单独的共享或导出。/backup、/documents和/public目录是从/exports父目录共享的。在这种情况下,目录在一行中被调用后,我们还会设置哪些网络能够访问它们(在我们的例子中是192.168.1.0/255.255.255.0)。这意味着如果你从不同的网络连接,访问将被拒绝。每台连接的机器必须是192.168.1.0/24网络的成员才能继续操作(所以确保你修改这个以匹配你的 IP 方案)。最后,我们为每个导出项包含一些选项,例如rw,no_subtree_check。
至于这些选项的作用,第一个(rw)是相当直观的。在这里,我们可以设置其他节点是否可以对导出中的数据进行更改。在我给出的例子中,/documents导出是只读的(ro),而其他的则允许读写。
每个示例中的下一个选项是no_subtree_check。这个选项已知可以提高可靠性,并且默认情况下通常会隐式启用。然而,如果不包括它,NFS 在重启时可能会发出警告,但不会真正阻止其正常工作。具体来说,这个选项禁用了所谓的子树检查,以前这个功能曾出现过一些稳定性问题。通常,当一个目录被导出时,NFS 可能还会扫描父目录,这有时会导致问题,尤其是在处理打开的文件句柄时。
还有一些其他选项可以包含在export中,你可以通过查看export的手册页面了解更多内容:
man exports
在实际使用中,你会经常看到一个选项是no_root_squash。通常,系统中的root用户会被映射为另一个系统中的nobody用户,出于安全原因。在大多数情况下,一个系统对另一个系统拥有root权限是一个不好的主意。no_root_squash选项禁用这一点,它允许一端的root用户在另一端被当作root用户处理。就我个人而言,我想不出这种设置有什么用处(甚至不推荐使用),但我在实际中确实经常看到这个选项,所以我觉得有必要提一下。再次提醒,查看export的手册页面以获取更多关于你可以传递给导出的附加选项的详细信息。
接下来,在我们完成 NFS 设置之前,还有一个文件需要编辑。/etc/idmapd.conf文件用于映射一个节点上的权限到另一个节点。在第二章,管理用户和权限中,我们讨论了每个用户都有一个分配给他们的 ID(UID)。然而,问题在于,从一个系统到另一个系统,用户的 UID 通常是不同的。例如,用户jdoe在服务器 A 上的 UID 可能是1001,但在服务器 B 上是1007。对于 NFS 来说,这就非常混淆了,因为 UID 用于引用权限。通过idmapd映射 ID 可以保持一致,并确保正确地转换每个用户,尽管它必须在每个节点上正确且一致地配置。基本上,只要你在每个服务器和客户端上使用相同的域名,并且在每个节点上正确配置/etc/idmapd.conf文件,你就应该没问题。
要配置此项,打开/etc/idmapd.conf文件,并用文本编辑器查找类似于以下内容的选项:
# Domain = localdomain
首先,删除该行前的#符号,以取消注释。然后,将域名更改为与你网络中其他部分使用的域名匹配。只要它在每个节点上相同,你可以保持默认值不变,但如果你记得在第十一章,设置网络服务中,我们在 DHCP 配置中使用了local.lan作为示例域名,所以最好确保在每个地方使用相同的域名——甚至包括 DHCP 提供的域名。基本上,只要尽量保持一致,你整体会更轻松。你还需要在每个将访问文件服务器的节点上编辑/etc/idmapd.conf文件,以确保它们也配置得相同。
在我们配置了/etc/exports和/etc/idmapd.conf文件,并假设你已经在文件系统上创建了导出的目录后,我们应该准备好重启 NFS 以激活我们的配置:
sudo systemctl restart nfs-kernel-server
重启 NFS 后,你应该通过systemctl检查守护进程的输出,以确保没有错误:
systemctl status nfs-kernel-server
只要没有错误,我们的 NFS 服务器应该就可以正常工作了。现在,我们只需要学习如何在另一台系统上挂载这些共享。与 Samba 不同,使用 Linux 文件管理器并浏览网络默认不会显示 NFS 导出;我们需要手动挂载它们。客户端机器,假设它们是基于 Debian 的(例如 Ubuntu),需要安装nfs-common软件包才能访问这些导出。
它可能已经安装好了,但如果没有,我们可以像安装其他软件包一样使用apt进行安装:
sudo apt install nfs-common
客户端安装完成后,我们现在可以使用mount命令在客户端挂载 NFS 导出。例如,针对我们的documents导出,我们可以使用以下变体的mount命令来完成这项工作:
sudo mount myserver:/documents /mnt/documents
将myserver替换为你的服务器主机名或 IP 地址,将documents替换为服务器上实际共享的名称,将/mnt/documents替换为你在本地服务器上希望挂载共享的路径。从此以后,你应该能够访问文件服务器上documents导出的内容。然而请注意,服务器上的导出目录是/exports/documents,但我们在示例mount命令中只要求/documents,而没有提供完整路径。之所以这样有效,是因为我们指定了/exports作为导出根目录。为了避免你来回翻看,这里是/etc/exports文件中的第一行,我们在其中指定了导出根目录:
/exports *(ro,fsid=0,no_subtree_check)
通过导出根目录,我们基本上设置了 NFS 导出的基准目录。我们将其设置为只读(ro),因为我们不希望任何人对/exports目录本身进行更改。/exports中的其他目录有自己的权限,因此会在每个导出的基础上覆盖ro设置,因此没有必要将我们的导出根目录设置为其他任何东西,除了只读。设置了导出根目录后,我们在挂载时无需调用导出的完整路径,只需要目录名。这就是为什么我们可以从myserver:/documents挂载 NFS 导出,而不必输入完整路径。虽然这样可以节省一些输入,但它也有用,因为从用户的角度来看,他们不需要了解服务器上的底层文件系统。用户完全不需要记住服务器从/exports共享了一个文档目录的事实;他们只关心如何访问他们的数据。另一个好处是,如果我们需要将导出根目录移到其他目录(例如在维护期间),用户不需要修改他们的配置来引用新的位置;他们只需要卸载并重新挂载导出即可。
所以,到这个时候,你的文件服务器将导出三个目录,并且你可以根据需要随时添加其他目录。然而,每当你添加一个新的导出时,它不会被 NFS 自动添加并读取。你可以重启 NFS 来激活新的导出,但在用户可能已经连接到它们的情况下,这样做其实并不是一个好主意,因为这会打断他们的访问。幸运的是,下面的命令可以让 NFS 重新读取/etc/exports文件,而不会干扰现有的连接。这样,你可以立即激活新的导出,而不必等待用户完成他们正在进行的工作:
sudo exportfs -a
处理完这一部分后,你应该能够从你的 Ubuntu 服务器导出一个目录,然后将该导出挂载到另一台 Linux 机器上。随时练习创建和挂载导出,直到你掌握为止。此外,在查阅 exports 的 man 页面后,你还应该熟悉 /etc/exports 文件中允许的一些附加选项和设置。
正如你在这一部分中看到的,/etc/exports 文件通常是设置基本 NFS 文件服务器所需的全部。然而,你还应该知道,Ubuntu 22.04 中新增了一个配置文件 /etc/nfs.conf。这个文件的目的是调整 NFS 服务器的细节配置。虽然本章范围不包括 /etc/nfs.conf 文件的讲解,但查看这个文件仍然值得,这样你可以了解有哪些可用的选项。有关更多信息,请参见本章末尾的进一步阅读部分。
当你已经不能再忍受更多的 NFS 实践时,我们将转向几种方法,让你能够在不需要设置中间服务或守护进程的情况下,从一个节点复制文件到另一个节点。
使用 rsync 传输文件
在 Linux 和 Unix 世界中,成千上万的工具和实用程序中,rsync 是最受欢迎的工具之一。这是一个你可以用来轻松将数据从一个地方复制到另一个地方的工具,并且提供了许多选项,可以让你非常精确地控制数据的传输方式。它的许多使用案例包括在保留权限的同时复制文件、在备份替换文件时复制文件,甚至设置增量备份。如果你还不知道如何使用 rsync,你可能会想要多加练习,因为它将成为你作为 Linux 管理员的职业生涯中不可或缺的工具,而且 Linux 社区普遍假设你已经知道如何使用它。幸运的是,rsync 并不难学。大多数管理员可以在一个小时或更短的时间内学会基本用法,而无数可用选项会让你即使多年后仍能学到新的技巧。
另一个使rsync灵活的方面是,你可以以多种方式操作源目录和目标目录。我之前提到过,rsync是一个可以用来将数据从一个地方复制到另一个地方的工具。这么做的好处在于,源和目标目录实际上可以是你想要的任何地方,只要你有适当的访问权限。例如,rsync最常见的用途是将数据从一个服务器的目录复制到另一个服务器的目录,跨越网络进行传输。然而,你甚至不需要使用网络;你也可以将数据从同一台服务器的一个目录复制到另一个目录。虽然刚开始看起来这似乎不是一个非常有用的操作,但想一想,目标目录可能是一个挂载点,指向备份磁盘,或者是一个 NFS 共享,实际上存在于另一台服务器上。这也可以反向操作:如果需要,你也可以将数据从网络位置复制到本地目录。
要开始练习rsync,我建议你找到一些示例文件进行操作。也许你有一些可以使用的文档、MP3 文件、视频、文本文件,或者基本上任何你手头有的数据。重要的是要先制作这些数据的副本。如果我们犯了错误,可能会覆盖掉一些内容,所以在练习时最好使用数据的副本,或者是你不太在意的数据。如果你没有任何文件可以使用,你可以创建一些文本文件。这个练习的目的是从一个地方复制文件到另一个地方;其实你复制什么或者将它发送到哪里并不重要。我会带你逐步进行一些rsync的示例,难度会逐渐增加。前几个示例会向你展示如何备份home目录,但后面的示例可能会有潜在的破坏性,因此你可能希望使用示例文件,直到你掌握技巧。
这是我们的第一个示例:
sudo rsync -r /home/myuser /backup
通过这个命令,我们使用rsync(作为root)将myuser目录的home目录内容复制到备份目录/backup(确保目标目录存在)。在示例中,我使用了-r选项,意味着rsync也会递归地抓取目录。你现在应该能在/backup目录中看到/home/myuser目录的副本。
然而,我们遇到了一点问题。如果你查看/backup/myuser目录的权限,你会看到目标目录中的所有内容现在都归root所有。这并不是一个好现象;当你备份一个用户的home目录时,你应该保留他们的权限。此外,你还应该尽可能保留所有的元数据,包括时间戳等信息。我们再试试rsync的另一种变体。不要担心/backup已经包含我们之前备份的myuser home 目录。我们再次执行备份,但这次我们将使用-a选项:
sudo rsync -a /home/myuser /backup
这一次,我们将 -r 选项替换为 -a(归档模式),它尽可能保留所有元数据(在大多数情况下,它应该能确保一切都是精确的复制)。你现在应该注意到的是,备份中的权限与我们复制的用户 home 目录中的权限匹配。文件的时间戳也将一致。这是因为每次运行 rsync 时,它都会复制与上次运行时不同的内容。我们第一次备份的文件已经存在,但权限不正确。当我们运行第二个命令时,rsync 只需要复制不同的内容,因此它为文件应用了正确的权限。如果自上次运行命令以来源目录中有新文件被添加,那么新文件或更新的文件也会被复制过去。
archive 模式(我们在前一个命令中使用的 -a 选项)实际上非常流行;你可能在旅途中经常看到它。-a 选项实际上是一个包装选项,它同时包括以下所有选项:
-rlptgoD
如果你对这些选项的具体作用感到好奇,可以查看 rsync 的手册页,获取更详细的信息。总结来说,-r 选项表示递归地复制数据(这一点我们已经知道),-l 选项复制符号链接,-p 保留权限,-g 保留组所有权,-o 保留所有者,-D 保留设备文件。如果把这些选项组合起来,我们就得到了 -rlptgoD。因此,-a 实际上等同于 -rlptgoD。我觉得 -a 更容易记住。
archive 模式很好用,但如果能看到 rsync 在运行时的状态是不是更好呢?添加 -v 选项并再次尝试该命令:
sudo rsync -av /home/myuser /backup
这一次,rsync 会在终端显示它运行时的操作 (-v 激活了 详细模式) 。这是我最喜欢的 rsync 命令变体之一,因为我喜欢复制所有内容并保留所有元数据,同时也能看到 rsync 在工作时的状态。
如果我告诉你 rsync 默认支持 SSH,你信吗?这是真的!通过使用 rsync,你可以轻松地从一个节点将数据复制到另一个节点,甚至通过 SSH。所有选项都适用,因此你不需要做任何不同的操作,只需将 rsync 指向另一台服务器,而不是本地服务器上的其他目录:
sudo rsync -av /home/myuser admin@192.168.1.5:/backup
在这个例子中,我将 myuser 用户的 home 目录复制到服务器 192.168.1.5 上的 /backup 目录。我以 admin 用户身份连接到另一台服务器。确保你根据需要更改用户账户和 IP 地址,同时确保你使用的账户有访问 /backup 目录的权限。当你运行此命令时,系统应该会提示你输入 SSH 密码,就像你通过普通 SSH 连接到服务器时一样。连接建立后,文件将被复制到目标服务器和目录。
现在,我们将进入一些更酷的示例(其中一些可能具有破坏性),我们可能不想使用实际的home目录来做这些,除非是一个测试账户,并且你不在乎其内容。正如我之前提到的,你应该有一些测试文件来进行练习。在练习时,只需将我的目录替换为你的目录。这里有另一个值得尝试的变体:
sudo rsync -av --delete /src /target
现在我向你介绍--delete选项。这个选项允许你同步两个目录。让我解释一下为什么这个选项很重要。直到现在,我们的每个rsync例子都在将文件从 A 点复制到 B 点,但我们没有删除任何文件。例如,假设你已经使用rsync将 A 点的内容复制到了 B 点。然后,你从 A 点删除了一些文件。当你再次使用rsync将文件从 A 点复制到 B 点时,你在 A 点删除的文件不会在 B 点被删除,它们仍然会存在。这是因为默认情况下,rsync只是将数据从一个位置复制到另一个位置,但并不删除任何东西。使用--delete选项后,你实际上是在同步这两个位置,因此你告诉rsync通过删除目标位置中已不再存在于源位置的文件来使它们一致。
接下来,我们将添加-b(备份)选项:
sudo rsync -avb --delete /src /target
这个选项特别有用。通常,当/src上的文件被更新并复制到/target时,/target上的文件将被新版本覆盖。但如果你不想替换任何文件呢?-b选项会重命名在目标位置被覆盖的文件,因此你仍然可以保留原始文件。如果你添加了--backup-dir选项,事情会变得更有趣:
sudo rsync -avb --delete --backup-dir=/backup/incremental /src /target
现在,我们像之前一样将文件从/src复制到/target,但是我们现在将替换的文件发送到/backup/incremental目录。这意味着,当一个文件要被替换时,原始文件将被复制到/backup/incremental。之所以能这样工作,是因为我们使用了-b选项(备份),但我们也使用了--backup-dir选项,这意味着被替换的文件不会被重命名;它们将简单地被移动到指定目录中。这使我们能够有效地进行增量备份。
在我们之前的例子基础上,我们可以使用 Bash shell 本身使增量备份工作得更好。考虑这些命令:
CURDATE=$(date +%m-%d-%Y)
sudo rsync -avb --delete --backup-dir=/backup/incremental/$CURDATE /src /target
通过这个例子,我们获取当前日期并将其存储在一个变量中(CURDATE)。在命令的rsync部分,我们使用该变量作为--backup-dir选项的值。这会将替换的文件复制到一个以命令运行时的date命名的backup目录中。基本上,如果今天的日期是08-17-2022,那么执行的命令将与我们运行以下命令的结果相同:
sudo rsync -avb --delete --backup-dir=/backup/incremental/08-17-2022 /src /target
希望你能看到 rsync 的灵活性,以及它如何不仅用于在目录和/或节点之间复制文件,还能作为备份解决方案(前提是你有一个远程目标来复制文件)。最棒的是,这只是个开始。如果你查阅 rsync 的手册页,你会发现还有许多选项可以进一步定制它。多加练习,你应该很快就能掌握它。
使用 SCP 传输文件
一个有用的替代方案是 Secure Copy(SCP)工具,它与 OpenSSH 客户端一起打包提供。它允许你快速地将文件从一个节点复制到另一个节点。虽然 rsync 也允许你通过 SSH 将文件复制到其他网络节点,但 SCP 更适合用于一次性任务;rsync 更适合更复杂的工作。如果你的目标是将单个文件或少量文件发送到另一台机器,SCP 是一个很好的工具,可以帮助你完成这项任务。如果没有别的,它也是你管理工具箱中的另一个好帮手。要使用 SCP,我们将使用 scp 命令。由于你很可能已经安装了 OpenSSH 客户端,因此应该已经可以使用 scp 命令了。如果你执行 which scp,你应该看到如下输出:
/usr/bin/scp
如果你没有看到任何输出,确保已安装 openssh-client 包。
使用 SCP 与 rsync 非常相似。该命令需要一个源、一个目标和一个文件名。要将一个文件从本地机器传输到另一台机器,生成的命令可能如下所示:
scp myfile.txt jdoe@192.168.1.50:/home/jdoe
在这个例子中,我们将 myfile.txt 文件(位于当前工作目录中)复制到位于 192.168.1.50 的服务器。如果目标服务器被 DNS 识别,我们本可以使用 DNS 名称而不是 IP 地址。此命令将作为用户 jdoe 连接到服务器,并将文件放入该用户的 home 目录。实际上,我们可以稍微简化一下这个命令:
scp myfile.txt jdoe@192.168.1.50:
请注意,我省略了目标路径 /home/jdoe。由于如果你没有为 scp 命令提供目标路径,默认会将文件放入 home 目录,因此我可以省略目标路径。所以,无论我是否明确包含 home 目录的路径,myfile.txt 文件最终都会出现在 /home/jdoe。如果我想将文件复制到其他地方,我就必须指定目标位置。确保在复制文件时至少包含冒号,因为如果不包括它,你将把文件复制到当前工作目录,而不是目标目录。
scp 命令也可以反向工作:
scp jdoe@192.168.1.50:myfile.txt .
在这个示例中,我们假设myfile.txt位于用户jdoe的home目录中。此命令将把该文件复制到我们本地机器的当前工作目录,因为我将本地路径指定为一个点(它对应于我们的当前工作目录)。在反向使用scp时并不总是实用的,因为你必须提前知道目标机器上文件的存储位置才能进行传输。
在我们之前的scp示例中,我们只复制了一个文件。如果我们想传输或下载整个目录及其内容,我们需要使用-r选项,这样可以执行递归复制:
scp -r /home/jdoe/downloads/linux_iso jdoe@192.168.1.50:downloads
在这个示例中,我们正在将本地文件夹/home/jdoe/downloads/linux_iso复制到远程机器192.168.1.50。由于我们使用了-r选项,scp将传输linux_iso文件夹及其所有内容。在远程端,我们再次通过用户jdoe连接。请注意,目标路径仅为downloads。由于scp默认使用用户的home目录,这将把linux_iso目录从源机器复制到目标机器的/home/jdoe/downloads目录下。以下命令将产生完全相同的结果:
scp -r /home/jdoe/downloads/linux_iso jdoe@192.168.1.50:/home/jdoe/downloads
home目录并不是scp命令假设的唯一路径。它还假设 SSH 在远程机器的端口22上监听。由于可以将服务器的 SSH 端口更改为其他端口,端口22可能并不是实际使用的端口。如果你需要为scp指定一个不同的端口,请使用-P选项:
scp -P 2222 -r /home/jdoe/downloads/linux_iso jdoe@192.168.1.50:downloads
在这个示例中,我们通过端口2222连接到远程机器。如果你已经配置了 SSH 监听不同的端口,请相应地更改端口号。
尽管端口22始终是 OpenSSH 的默认端口,但一些管理员常常会将其更改为其他端口。虽然更改 SSH 端口在安全性方面并不会带来很大的好处(一个强力端口扫描仍然可以找到你的 SSH 守护进程),但这是一个相对容易的修改,而且将其设置为稍微难以发现一点是有益的。我们将在第二十一章中进一步讨论这个问题,保护你的服务器。
与 Linux 世界中的大多数命令一样,scp命令支持详细模式。如果你想查看scp命令在复制多个文件时的进度,可以添加-v选项:
scp -rv /home/jdoe/downloads/linux_iso jdoe@192.168.1.50:downloads
好了,就是这样。scp命令并不是特别复杂或高级,但它非常适合那些你想要一次性从一个节点将文件复制到另一个节点的情况。由于它通过 SSH 复制文件,你可以从中受益于其安全性,并且它也很好地与现有的 SSH 配置集成。这种集成的一个例子是,scp会识别你的~/.ssh/config文件(如果你有的话),所以你可以进一步简化命令。试着多练习一下,在下一个部分中,我们将介绍 OpenSSH 的另一个技巧。
概要
在这一章中,我们探讨了多种访问远程资源的方式。几乎每个网络都有一个用于存储文件的中心位置,我们探索了使用 NFS 和 Samba 完成这一目标的两种方式。NFS 和 Samba 在数据中心中各有其用途,并且是我们可以将服务器上的资源提供给需要访问的用户的非常有用的方式。我们还讨论了rsync和scp,这两个无需设置永久共享即可传输数据的优秀实用工具。
接下来是 第十三章,管理数据库。现在我们的 Ubuntu 服务器网络上运行了各种有用的服务,看看如何服务数据库才是正经事。具体来说,我们将研究 MariaDB。到时见!
相关视频
-
教程
scp:linux.video/scp -
使用
rsync备份 Linux 服务器:linux.video/rsync-backup -
使用
rsync传输文件:linux.video/rsync-transfer
进一步阅读
-
Ubuntu
rsync文档:learnlinux.link/u-rsync -
Ubuntu Samba 文档:
learnlinux.link/u-smb -
服务 – NFS(包含 22.04 版本中 NFS 更改的额外信息):
learnlinux.link/u-nfs-1 -
Ubuntu NFS 文档:
learnlinux.link/u-nfs-2 -
Active Directory 集成:
learnlinux.link/u-ad
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十三章:管理数据库
Linux 平台一直是托管数据库的热门选择。考虑到如今大多数流行网站的运行依赖于数据库,这是服务器必须承担的一个非常重要的角色。Ubuntu Server 也是非常受欢迎的选择,其稳定性为托管社区带来了重要的优势。这一次,我们将关注 MariaDB,MySQL 的一个流行分支。目标不是提供 MySQL 语法的完整讲解(因为那将是一本完整的书),而是专注于如何使用 MariaDB 设置和维护数据库服务器,甚至讲解如何在其之间设置主/从关系。如果你已经对数据库架构有了扎实的理解,这一章依然能让你受益,因为我们将特别讨论 Ubuntu 对 MariaDB 的实现,它的配置与其他平台有所不同。
在我们设置自己的 MariaDB 服务器时,我们将涵盖以下主题:
-
设置数据库服务器的准备工作
-
安装 MariaDB
-
理解 MariaDB 配置文件
-
管理 MariaDB 数据库
-
设置从数据库服务器
就像设置任何服务器一样,我们首先需要考虑目标,并根据目标进行规划。因此,在接下来的部分,我们将讨论在准备数据库服务器时需要考虑的一些更高层次的细节。
设置数据库服务器的准备工作
在我们开始设置数据库服务器之前,有一些琐事需要处理。随着本章的进行,我们将使用 MariaDB 设置一个基础的数据库服务器。我相信我的许多读者都对 MySQL 比较熟悉。MySQL 是一个经过验证的成熟解决方案,至今仍在许多数据中心中使用,并且在可预见的未来,仍将继续如此。很有可能你经常访问的一些热门网站,在其后台就使用了 MySQL。所以你可能会想,为什么不讲解 MySQL,而是选择 MariaDB 呢?
本书聚焦于 MariaDB 有两个原因。首先,Linux 社区的大多数用户正在转向 MariaDB(稍后会详细讨论),而且它也是 MySQL 的一个直接替代品。这意味着你已经为 MySQL 编写的任何数据库或脚本,很可能也能在 MariaDB 上正常运行,除非遇到某些特殊情况。反过来,你在 MariaDB 上练习的命令,也应该能在 MySQL 服务器上正常使用。这一点非常好,因为许多 MySQL 安装仍然在许多数据中心使用,而你也能支持这些安装。大多数情况下,当现有的基础设施可以迁移到 MariaDB 时,几乎没有理由继续使用 MySQL,而 Linux 社区也正朝着这个方向发展。
为什么会有这种变化?一段时间以前,各种 Linux 发行版开始从 MySQL 切换到 MariaDB。红帽(Red Hat)就是这样一个例子;它在 Red Hat Enterprise Linux 的第 7 版中就切换到了 MariaDB。其他发行版,如 Arch Linux 和 Fedora,也走上了同样的道路。这部分是由于对现在拥有 MySQL 的 Oracle 公司的不信任。当 Oracle 成为 MySQL 所有者时,开源社区对 MySQL 的未来以及其许可证提出了一些严重的问题。我不会对 Oracle、MySQL 的未来或与其未来相关的任何政治问题进行任何推测,因为这与本书无关(而且我不喜欢企业戏剧)。然而,事实是许多发行版正在向 MariaDB 迈进,而这似乎是未来。它是一项伟大的技术,基于多个与当前争议无关的原因,我强烈推荐它比 MySQL 更好。
MariaDB 不仅仅是 MySQL 的一个分支。它本身是一个非常称职的数据库服务器。现有的 MySQL 实现兼容性使其更易于采用。但更重要的是,MariaDB 对 MySQL 进行了一些非常有价值的改变和改进,这些改变只会使您受益。
你所喜爱的 MySQL 中的一切,都可以在 MariaDB 中找到,再加上一些独有的尖端功能。但更好的是,MariaDB 提供了比普通的 MySQL 更为高效和先进的额外集群选项。
所以,希望我已经向您展示了 MariaDB 的价值。最终,您是否真正使用它将取决于您组织的需求。我见过一些组织选择坚持使用 MySQL,即使只是因为他们熟悉它。我可以理解,如果一个解决方案在您的数据中心中已经证明有效,那么如果您的数据库堆栈运行良好,则没有理由改变它。因此,虽然我将讨论如何使用 MariaDB,但本章的示例可能也适用于 MySQL。如果有疑问,本章推荐使用 MariaDB。
关于您的服务器,一个良好的实施计划至关重要(一如既往)。我不会在这个方面花太多时间,因为我知道您可能已经在本书的一两段中提到了冗余的重要性(而且我相信在最后一页之前,我还会再提几次冗余)。此时,您可能只是在设置一个实验环境或测试服务器,以便在生产中运用这些概念之前练习这些技能。但是当您最终将数据库服务器投入生产时,计划长期稳定性至关重要。数据库服务器应定期备份、冗余(我又在说冗余了),并定期打补丁。在本章后面的部分,我将指导您设置一个次要的数据库服务器,它将处理冗余部分。
然而,光这样做还不够,因为定期备份非常重要。有很多工具可以帮助你完成这项任务,比如mysqldump,你也可以拍摄虚拟机的快照(前提是你不是使用物理服务器)。这两种解决方案都是有效的,取决于你的环境。作为一个曾经为客户恢复丢失的数据库服务器(客户没有备份或冗余)而浪费了一整天时间的人,我的目标只是希望你避免那种头疼的经历。
至于数据库服务器需要多少资源,这完全取决于你的环境。MariaDB 本身并不会占用大量资源,但和 MySQL 一样,你的使用量依赖于你的工作负载。你可能有几十个客户连接,或者几千个,甚至更多。但我肯定会推荐的一点是,为存储数据库文件的分区使用逻辑卷管理器(LVM)。从长远来看,这无疑会为你节省很多麻烦。正如我们在第九章《管理存储卷》中讨论的,LVM 使得扩展文件系统变得非常简单,特别是在虚拟机上。
如果你的数据库服务器运行在虚拟机上,当数据库分区快要满时,你可以向卷组中添加磁盘并扩展它,这样你的客户就永远不会察觉到会出现问题。如果没有 LVM,你需要关闭服务器,添加新的卷,使用rsync将数据库服务器文件传输到新位置,然后再启动服务器。根据数据库的大小,这个过程可能会持续几个小时。为了省事,使用 LVM 吧。
说完这些,我们可以开始设置 MariaDB 了。对于学习和测试目的,你可以使用几乎任何你喜欢的服务器:物理服务器、虚拟服务器或虚拟私人服务器(VPS)。一旦你准备好了,我们就可以开始了!
安装 MariaDB
现在我们进入了有趣的部分,安装 MariaDB。为了开始,我们将安装mariadb-server软件包:
sudo apt install mariadb-server
如果你的组织倾向于继续使用 MySQL,安装的软件包是mysql-server:
sudo apt install mysql-server
虽然你可能会想尝试同时对比 MySQL 和 MariaDB 的不同之处,但我不推荐在同一台服务器上从 MariaDB 切换到 MySQL(或反之)。我见过一些服务器在安装了其中一个软件包后又切换到另一个软件包时,出现了非常奇怪的配置问题(即便是清除配置之后)。大多数情况下,最好每台服务器只选择一个解决方案并坚持使用。一般来说,如果你需要支持旧的数据库,应该使用 MySQL。对于全新的安装,建议使用 MariaDB。
安装mariadb-server软件包后,请检查确保服务已经启动并启用。默认情况下,它应该已经在运行:
systemctl status mariadb
接下来,我们需要为我们的 MariaDB 安装增加一些安全设置(尽管我们使用的是 MariaDB,但以下命令的名称并没有更新,仍然包含mysql):
sudo mysql_secure_installation
到此为止,我们还没有设置root密码,因此当脚本要求输入密码时,直接按Enter即可。脚本还会问你一些其他问题。你将被询问是否希望切换到unix_socket 认证。这个问题的默认答案是是,但我建议你回答否。这样可以确保使用密码认证,这对于兼容性来说更好。虽然切换到unix_socket 认证有安全上的好处,但对于我们的目的来说,密码认证已经足够。如果你决定深入学习数据库管理,未来切换到unix_socket 认证可能会更好。
接下来,mysql_secure_installation脚本会询问你是否要设置root密码。MariaDB 的root用户与系统上的root用户不同,你肯定应该为它设置一个密码。所以当出现这个问题时,按y表示你想要创建一个root密码,然后输入该密码两次。
在设置root密码后,脚本会询问你是否希望删除匿名用户,并且禁用远程访问数据库服务器。你应该对这两个问题都回答是。后者尤为重要,因为几乎没有任何情况下允许 MySQL/MariaDB 的公共访问是一个好主意。即使你为外部用户托管网站,这些用户只需要访问网站,而不是数据库服务器。网站本身将根据需要与数据库本地交互;外部连接是不必要的。(如果你有一个独立的数据库服务器并且该服务器不能公开访问互联网,那么你应该在这里回答是——只要确保数据库服务器不能被公开访问)。大多数情况下,继续执行,回答脚本询问的所有问题yes即可(除非你有充分的理由不这样做)。
执行完mysql_secure_installation后,整个过程如下所示,我已经为每个步骤包含了我推荐的回答(“y”表示是,“n”表示否):
Enter current password for root (enter for none):
Switch to unix_socket authentication [Y/n] n
Set root password? [Y/n] y
Remove anonymous users? [Y/n] y
Disallow root login remotely? [Y/n] y
Remove test database and access to it? [Y/n] y
Reload privilege tables now? [Y/n] y
到目前为止,我们已经拥有了一个完全功能性的数据库服务器。之前的命令让我们能够应用一些基本的安全设置,现在我们的数据库服务器已经可以使用了。为了连接到它并进行管理,我们将使用mariadb命令访问 MariaDB 的命令行,在这里我们将输入命令来管理我们的数据库。实际上,有两种方法可以连接到这个命令行。第一种方法是简单地使用带有sudo的mariadb命令:
sudo mariadb
这个特定的命令有效,因为如果你以 root 身份使用 mariadb 命令(我们在这个例子中使用了 sudo),密码会被绕过。实际上,我们甚至没有输入用户名;如果你使用 sudo 访问 MariaDB,默认会假定是 root。这是连接的最简单方式。然而,如果你使用过其他 Linux 发行版,可能更习惯于另一种身份验证方法:输入用户名和密码。在这种情况下,命令将如下所示(但默认情况下不会起作用):
mariadb -u root -p
当该命令正确执行时,它会要求你输入 root 密码,然后允许你进入 shell。
推荐创建一个不同的用户来管理你的 MariaDB 安装,因为在大多数情况下不推荐以 root 身份登录。我们将在本章稍后创建其他用户,但现在,root 账户是唯一可用的账户。通常的做法是使用 root 账户进行初步设置,然后为后续的管理工作创建一个不同的用户。不过,root 账户仍然常用于服务器维护,因此请根据自己的判断使用它。
现在我们已经可以访问 MariaDB shell,我们可以用它做些什么呢?我们在这个 shell 中执行的命令可以让我们进行诸如创建和删除数据库和用户、添加表格等操作。mariadb 命令来自 mariadb-client-10.6 包,该包在我们安装 mariadb-server 时作为依赖项一起安装。单独输入 mariadb 命令而不加任何选项,将连接到我们本地机器上的数据库服务器。这个工具还允许我们连接到外部数据库服务器,以便远程管理它们,稍后我们会讨论这个功能。
MariaDB shell 提示符将如下所示:
MariaDB [(none)]>
我们稍后会深入讨论 MariaDB 命令和用户管理。现在,你可以退出 shell。要退出,你可以键入 exit 并按 Enter 键,或者按 Ctrl + d 键。
现在,我们的 MariaDB 服务器已经准备好。虽然你现在可以继续进入下一部分,但你可能想考虑在另一台机器上按照这些步骤设置另一个 MariaDB 服务器。如果你有空间为另一个虚拟机提供位置,现在做这件事可能是个好主意,因为稍后我们会设置一个备用数据库服务器。
理解 MariaDB 配置文件
现在我们已经安装了 MariaDB,让我们快速了解一下它的配置是如何存储的。尽管在本章中我们不会更改太多配置(除了添加与设置次要数据库实例相关的参数),但了解配置文件存放位置是一个好主意,因为将来开发者可能会要求你调整数据库配置。这可能涉及更改存储引擎、缓冲区大小或其他无数设置。关于性能调优的完整指南超出了本书的范围,但了解如何读取 MariaDB 的设置会很有帮助,因为 Ubuntu 的实现方式相当独特。
MariaDB 的配置文件存储在 /etc/mysql 目录中。在该目录下,你默认会看到以下文件:
debian.cnf
debian-start
mariadb.cnf
my.cnf
my.cnf.fallback
你还会看到以下目录:
conf.d
mariadb.conf.d
MariaDB 启动时读取的配置文件是 /etc/mysql/mariadb.cnf 文件。想要配置守护进程时,你会首先查看这个文件,但我们很快就会讲到。/etc/mysql/debian-start 文件实际上是一个脚本,用于在 MariaDB 启动时设置默认值,例如设置一些环境变量。它还定义了一个任务,当 mariadb 进程崩溃或退出时执行,并允许检查崩溃的表。
debian-start 脚本还加载了 /etc/mysql/debian.cnf 文件,该文件为 mariadb 守护进程设置了一些客户端设置。以下是该文件中的一些值:
[client]
host = localhost
user = root
[mysql_upgrade]
host = localhost
user = root
这些值的默认设置已经足够,通常没有必要更改它们。实际上,这个文件设置了默认的用户、主机和套接字位置。如果你之前在其他平台上使用过 MySQL,你可能已经在 /etc/my.cnf 文件中见过许多这样的设置,它通常是 mariadb 守护进程的标准文件。在 Ubuntu Server 上使用 MariaDB 时,你会发现文件的默认布局发生了显著变化。
/etc/mysql/mariadb.cnf 文件设置了 MariaDB 的全局默认值。然而,在 Ubuntu 的实现中,这个默认文件只是包含了 /etc/mysql/conf.d 和 /etc/mysql/mariadb.conf.d 目录中的配置文件。在这些目录中,有许多文件以 .cnf 扩展名结尾。许多文件包含了通常会出现在单一文件中的默认配置值,但 Ubuntu 将这些设置模块化到单独的文件中。为了本书的目的,当我们需要设置主从服务器关系时,将编辑 /etc/mysql/conf.d/mysql.cnf 文件。
其他配置文件与本书的内容无关,它们的当前值已经足够满足我们的需求。在性能调优方面,你可以考虑创建一个新的以.cnf结尾的配置文件,文件中包含特定的调优值,这些值可以通过你想运行的软件包的文档或开发者给出的需求来提供。
要获取更多关于这些配置文件是如何被读取的信息,你可以参考/etc/mysql/mariadb.cnf文件,文件顶部包含一些有用的内容,详细说明了这些配置文件被读取的顺序及其目的。以下是该文件中这些注释的摘录:
# The MariaDB/MySQL tools read configuration files in the following order:
# 1\. "/etc/mysql/mariadb.cnf" (this file) to set global defaults,
# 2\. "/etc/mysql/conf.d/*.cnf" to set global options.
# 3\. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options.
# 4\. "~/.my.cnf" to set user-specific options.
如我们所见,当 MariaDB 启动时,它首先读取/etc/mysql/mariadb.cnf文件,然后是存储在/etc/mysql/conf.d目录中的.cnf文件,接着是存储在/etc/mysql/mariadb.conf.d目录中的.cnf文件,最后是任何可能存在于用户home目录中的.my.cnf文件,其中包含用户特定的设置。
在 Ubuntu 的实现中,当启动时读取/etc/mysql/mariadb.cnf文件时,系统将立即扫描/etc/mysql/conf.d和/etc/mysql/mariadb.conf.d的内容,因为/etc/mysql/mariadb.cnf文件包含以下几行:
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/
如你所见,配置文件检查的顺序是先检查mariadb.cnf文件,然后检查/etc/mysql/conf.d和/etc/mysql/mariadb.conf.d目录。
一开始这可能会有点令人困惑,因为在 Ubuntu Server 中 MariaDB 的默认配置基本上由重定向到其他文件的配置文件组成。但要点是,任何你进行的非 MariaDB 专属的配置更改(基本上是与 MySQL 本身兼容的配置)应放在以.cnf结尾的配置文件中,然后存储在/etc/mysql/conf.d目录中。
如果你想添加的配置是 MariaDB 独有的功能(但与 MySQL 本身不兼容),则应将配置文件放在/etc/mysql/mariadb.conf.d目录中。对于我们的目的,当我们设置主从复制时,我们将编辑/etc/mysql/conf.d/mysql.cnf文件,因为我们使用的方法并不特定于 MariaDB。
在本节中,我们讨论了 MariaDB 配置及其与其他平台实现的不同之处。配置文件的呈现方式并不是 Ubuntu 实现 MariaDB 的唯一不同点;还有其他差异。下一节中,我们将查看 Ubuntu 实现与其他 Linux 发行版实现之间的几种额外差异。
管理 MariaDB 数据库
现在我们的 MariaDB 服务器已经启动并运行,我们终于可以着手管理它了。在本节中,我将演示如何使用mariadb命令连接到数据库服务器,这将使我们能够创建数据库、删除(丢弃)它们,并管理用户和权限。
首先,我们需要为 MariaDB 创建一个管理员用户。root账户已经存在,作为默认的管理员账户,但允许其他人使用该账户并不安全。相反,为了管理我们的数据库,创建一个与root分开的管理员账户会更有意义。因此,我们将从管理用户开始讨论数据库管理。在 MariaDB 中管理的用户是特定于 MariaDB 的,它们与实际系统上的用户账户是分开的。
要管理和与数据库交互,我们需要进入 MariaDB shell,创建数据库用户时也是如此。现在,由于我们只有root账户,我们需要以root身份访问当前的 MariaDB 实例,以便设置管理员用户。如果你按照本章前面讨论的内容设置了标准认证,你可以通过标准方式访问提示符:
mariadb -u root -p
或者,更简单的方式是:
sudo mariadb
一旦进入 MariaDB shell,提示符会变成如下所示:
MariaDB [(none)]>
现在,我们可以创建新的管理员用户。我在示例中将其命名为admin,但你可以使用任何你喜欢的名字。在我曾经工作的一个公司里,我们在服务器上使用了用户名velociraptor作为管理员用户,因为没有什么比迅猛龙更强大(而且它们可以开门)。你可以随意使用一个聪明的名字,但一定要记住它。使用非标准用户名有一个附加的安全优势,就是通过模糊化提高安全性;这个名字不会是入侵者预期的。
这是在 MariaDB 中创建新用户的命令(将命令中的用户名和密码替换为你所希望的凭据):
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
在 MySQL 语法方面,命令不区分大小写(尽管数据参数区分大小写),但通常会将指令大写,以便与数据区分开来。在本章的其余部分,我们将在 Linux shell 中执行一些命令,在 MariaDB shell 中执行其他命令。我会告诉你每个命令需要在哪个 shell 中执行,但如果你感到困惑,只需要记住,只有 MariaDB 命令是大写的。
使用前面的命令,我们创建了admin用户,并将其限制为localhost。这是很重要的,因为我们不想将admin账户暴露给全世界。我们还执行了刷新权限操作,这会导致 MariaDB 重新加载其权限信息。每次添加用户或修改权限时,都应该运行FLUSH PRIVILEGES命令。我可能不会每次都提到需要运行此命令,所以你现在可以记住它并养成一个习惯。
如我之前所提到的,前面的命令创建了admin用户,但只允许它从localhost连接。这意味着管理员必须先登录到服务器本身,才能使用admin账户登录 MariaDB。以下命令是一个变体,允许从其他任何位置远程登录:
CREATE USER 'admin'@'%' IDENTIFIED BY 'password';
您能看到%符号替代localhost吗?这基本上意味着任何地方,这表示我们正在创建一个可以从任何来源(甚至外部节点)登录的用户。然而,通过使用第一个命令将用户限制为localhost,我们让服务器变得更加安全。您还可以限制访问特定的网络,如果您确实需要远程允许数据库管理员访问服务器的话,这样做是非常必要的:
CREATE USER 'admin'@'192.168.1.%' IDENTIFIED BY 'password';
这样稍微好一些,但仍然不如将登录限制为localhost安全。正如您所看到的,%字符基本上是一个通配符,因此您可以通过它来限制访问,只允许特定 IP 甚至特定子网的访问。
到目前为止,我们所做的只是创建了一个新用户;我们还没有赋予该用户任何权限。我们可以使用GRANT命令创建一组权限(也称为授权)。首先,我们让管理员用户在从localhost调用时可以完全访问数据库服务器:
GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost';
FLUSH PRIVILEGES;
现在,我们有了一个可以用来管理服务器数据库的管理员账户。我们可以使用这个账户来管理服务器,而不是使用root账户。任何已登录的 Linux 用户只要知道密码,就可以访问并管理数据库服务器。要以我们创建的admin用户身份进入 MariaDB 命令行,以下命令可以实现:
mariadb -u admin -p
输入密码后,您将以admin身份登录 MariaDB。
此外,您实际上可以直接将密码提供给mariadb命令,而无需系统提示输入密码:
mariadb -u admin -p<password>
请注意,-p选项和实际密码之间没有空格(尽管通常-u选项和用户名之间会有空格)。尽管一次性提供用户名和密码非常方便,但我不建议您使用这种方法。这是因为任何您输入的 Linux 命令都会被保存到历史记录中,因此任何人都可以查看您的命令历史记录,并且能看到明文的密码。我之所以在这里提到它,是因为我发现许多管理员会这么做,尽管他们不应该这么做。至少现在您了解了这种方法以及它为何不正确。
我们创建的admin账户仅供需要在服务器上管理数据库的系统管理员使用。该账户的密码不应提供给任何不必要的人员,除非是工作人员或确实需要的管理员。可以向 MariaDB 服务器添加其他用户,每个用户可以拥有不同的访问权限。请记住,我们的admin账户可以管理数据库,但不能管理用户。这一点很重要,因为你可能不应该允许除了服务器管理员以外的任何人创建用户。你仍然需要以root身份登录来管理用户权限。
为需要能够读取数据但不能修改数据的员工创建 MariaDB 只读用户也许会很有用。在 MariaDB 命令行界面(作为root用户)中,我们可以执行以下命令来有效地创建一个只读用户:
GRANT SELECT ON *.* TO 'readonlyuser'@'localhost' IDENTIFIED BY 'password';
通过此命令(并在之后刷新权限),我们做了两件事。首先,我们创建了一个新用户,并且通过一个命令设置了该用户的权限。第二,我们创建了一个只读用户,允许其查看数据库但不能管理它(我们限制了其SELECT权限)。这样更加安全。实际上,最好将只读用户限制在特定数据库内。这在开发环境中尤为典型,在这种环境中,应用程序通过网络连接到数据库并需要从中读取信息。我们很快会讨论这种场景。
接下来,让我们创建一个数据库。在 MariaDB 提示符下,执行:
CREATE DATABASE mysampledb;
很简单。现在我们的服务器上应该有一个名为mysampledb的数据库。要列出服务器上的所有数据库(并确认我们的数据库已正确创建),我们可以执行以下命令:
SHOW DATABASES;
这将产生类似以下的输出:

图 13.1:列出 MariaDB 数据库
输出将显示一些为我们创建的系统数据库,但我们的新数据库应该会出现在其中。我们也可以同样容易地列出用户:
SELECT HOST, USER, PASSWORD FROM mysql.user;
输入此命令将产生类似以下的输出:

图 13.2:列出 MariaDB 用户
在典型的场景中,当安装一个需要自己数据库的应用程序时,我们会先创建数据库,然后为该数据库创建一个用户。我们通常希望只授予该用户对该数据库的权限,并且只授予其运行所需的最低权限。我们已经创建了mysampledb数据库,因此如果我们想为该数据库创建一个只读访问的用户,我们可以通过以下命令实现:
GRANT SELECT ON mysampledb.* TO 'appuser'@'localhost' IDENTIFIED BY 'password';
通过一个命令,我们不仅创建了用户appuser,还为其设置了密码,并允许其在mysampledb数据库上拥有SELECT权限。这相当于只读访问。如果我们的用户需要完全访问权限,我们可以使用以下命令:
GRANT ALL ON mysampledb.* TO 'appuser'@'localhost' IDENTIFIED BY 'password';
为了再次确认我们正确执行了命令,我们可以使用以下命令来显示特定用户的权限:
SHOW GRANTS FOR 'appuser'@'localhost';
现在,我们的appuser拥有对mysampledb数据库的完全访问权限。当然,只有在绝对必要时,我们才应该为数据库提供完全访问权限。我们还可以提供其他权限,例如DELETE(是否允许用户从数据库表中删除行)、CREATE(控制用户是否可以向数据库添加行)、INSERT(控制用户是否可以向表格添加新行)、SELECT(允许用户从数据库中读取信息)、DROP(允许用户完全删除数据库)以及ALL(授予用户所有权限)。我们可以授予或拒绝其他权限;有关更多详细信息,请查看 MariaDB 文档。为了满足你正在安装的软件,所需授予用户的权限类型将取决于该软件的文档。始终参考你正在尝试安装的应用程序的安装说明,以确定它需要哪些权限才能运行。
如果你想删除用户访问权限,可以使用以下命令来实现(将myuser替换为你想删除的用户账户):
DROP USER 'myuser'@'localhost'
现在,让我们回到数据库部分。既然我们已经创建了mysampledb数据库,那么我们可以做些什么呢?当然是添加表格和数据行!没有实际数据,数据库是没有意义的,所以我们可以通过一些示例来添加数据,看看它是如何工作的。首先,以具有对mysampledb数据库完全权限的用户身份登录 MariaDB Shell。现在,我们可以开始有趣的部分,修改内容。以下是一些你可以参考的示例:
USE mysampledb;
USE命令允许我们选择要使用的数据库。MariaDB 的提示符会从MariaDB [(none)]>变为MariaDB [mysampledb]>。这个变化非常有用,因为 MariaDB 的提示符会变化,指示我们当前正在使用哪个数据库。我们基本上是告诉MariaDB,对于我们接下来要执行的所有命令,我们希望它们作用于mysampledb数据库。
现在,我们可以在数据库中CREATE一个表格。你可以随意命名你的表格,因为我们只是在练习。我会将我的表命名为Employees:
CREATE TABLE Employees (Name char(15), Age int(3), Occupation char(15));
我们可以通过展示数据库中的列来验证这个命令,以确保它显示的是我们预期的内容:
SHOW COLUMNS IN Employees;
使用这个命令,我们创建了一个名为Employees的表格,包含三列(Name、Age和Occupation)。要向此表格中添加新数据,我们可以使用以下INSERT命令:
INSERT INTO Employees VALUES ('Joe Smith', '26', 'Ninja');
INSERT命令示例将一名新员工添加到我们的Employees表中。当我们使用INSERT时,我们会为每一列插入所有数据。在这个例子中,我们有一名名叫Joe的员工,他26岁,职业是忍者。你可以随意添加更多员工;只需要编写额外的INSERT语句,并为每一列提供数据。当你完成时,可以使用以下命令来显示该表中的所有数据:
SELECT * FROM Employees;

图 13.3:列出表中的数据库行
要删除一条记录,以下命令将完成所需操作:
DELETE FROM Employees WHERE Name = 'Joe Smith';
基本上,我们使用DELETE FROM命令,指定我们希望删除的表名(在这个例子中是Employees),然后使用WHERE来提供一些搜索条件,以便缩小我们的命令范围。
DROP命令允许我们删除表或整个数据库,因此使用时需要小心。我并不建议你删除我们刚刚创建的数据库,因为我们会在后续示例中使用它。但如果你真的想删除Employees表,可以使用:
DROP TABLE Employees;
或者使用这个命令来删除整个数据库:
DROP DATABASE mysampledb;
当然,MariaDB 及其 MySQL 语法远不止我提供的这些示例,但这些应该足够帮助你完成本书中的示例。尽管我非常希望给你提供完整的 MySQL 语法讲解,但这将会使本章的篇幅过长。如果你希望将技能提升到本章示例之外的层次,可以找到许多专门讲解此主题的优秀书籍。
在我结束本节之前,我认为你应该了解如何备份和恢复数据库。为此,我们可以使用mysqldump命令,它的语法非常简单,正如你将看到的那样。
首先,退出 MariaDB 命令行并返回标准 Linux 命令行。由于我们在本章之前已经创建了一个admin用户,所以我们将使用该用户来进行备份操作:
mysqldump -u admin -p --databases mysampledb > mysampledb.sql
在这个示例中,我们使用mysqldump来创建mysampledb数据库的副本,并将其存储在名为mysampledb.sql的文件中。由于 MariaDB 要求我们登录,我们使用-u选项和用户名admin,以及-p选项来进行身份验证,后者会提示我们输入密码。--databases选项是必要的,因为默认情况下,mysqldump不会包含database create语句。然而,--databases选项强制执行这一点,这样可以更方便地恢复。假设我们能够正确认证,mysampledb数据库的内容将被导出到mysampledb.sql文件中。由于这个数据库可能只包含一个表和几行数据,导出过程应该非常快速。较大的生产数据库可能需要几个小时来导出。
恢复备份相当简单。我们可以使用mariadb命令,将备份文件作为输入源:
sudo mariadb -u admin -p < mysampledb.sql
所以,你看到了。mysqldump命令在备份数据库时确实非常方便。在接下来的部分,我们将讨论如何设置次级数据库服务器。
设置次级数据库服务器
冗余是一件了不起的事情。如果主服务器因某些原因发生故障,你可以通过准备一个次级数据库服务器来保持应用程序的运行,以防原服务器发生故障。当然,你也可以定期备份数据库服务器并在必要时恢复,但由于数据库总是在变化,保持备份是非常困难的,因此备份很容易很快过时。次级数据库服务器使你能够拥有一个始终保持最新状态的副本。这并不意味着你不再需要备份,但它确实为你在遇到问题时提供了另一种恢复选项。
行业正在逐步避免使用“主(Master)”和“从(Slave)”这样的术语来描述主服务器和从服务器。在本章及之后的部分,我们将使用“主(Primary)”和“次(Secondary)”来描述一个主数据库服务器将数据复制到另一个的关系。因此,尽管 Ubuntu 22.04 内部仍然使用旧的术语,我们在讨论操作时将使用“primary”和“secondary”这些术语。只需记住,命名方式未来可能会改为新的命名方案。
到目前为止,我们已经有一个数据库服务器。为了设置次级数据库实例,你所需要做的就是设置另一台物理服务器或虚拟机,并像我们之前一样安装mariadb-server包。如果你已经按照本章早些时候的建议设置了两台数据库服务器,你就可以开始了。如果没有,随时启动另一台虚拟机,并按照安装 MariaDB部分的过程进行操作,其中涵盖了 MariaDB 的初始设置。如果你还没有设置另一台服务器,请去设置一台。在你的两台服务器中,一台应被指定为主服务器,另一台为次级服务器,因此请记下每台服务器的 IP 地址。
首先,我们将开始处理主服务器。我们需要编辑你希望作为主服务器的服务器上的/etc/mysql/conf.d/mysql.cnf文件。当前,文件中仅包含以下一行:
[mysql]
在这行下面,添加一个空行,然后输入以下代码:
[mysqld]
log-bin
binlog-do-db=mysampledb
server-id=1
在此配置中,我们首先启用了二进制日志,这是主/从服务器正常运行所必需的。二进制日志记录了所有数据库变更的记录,这使得次级数据库实例能够重现对主服务器所做的更改。这些二进制日志记录了对数据库所做的更改,然后将其传输到次级服务器。
我们需要编辑的另一个配置文件是/etc/mysql/mariadb.conf.d/50-server.cnf。在这个文件中,我们有以下一行:
bind-address = 127.0.0.1
在此默认设置下,mysql守护进程仅监听localhost(127.0.0.1)的连接,这会有问题,因为我们需要从另一台机器(即从服务器)进行连接。将这一行更改为以下内容:
bind-address = 0.0.0.0
接下来,我们需要访问主服务器上的 MariaDB shell 并执行以下命令:
GRANT REPLICATION SLAVE ON *.* to 'replicate'@'192.168.1.204' identified by 'password';
在这里,我们创建一个名为replicate的复制用户,并允许其从 IP 地址192.168.1.204连接到我们的主服务器。确保将该 IP 地址更改为与你的从服务器相匹配的 IP 地址,但如果你已经配置了域名,也可以使用类似%.mydomain的主机名标识符,这等同于允许任何以.mydomain结尾的主机名。此外,我们为该用户设置密码为password,所以你可以根据自己的密码要求自定义密码(确保记下密码)。
现在,我们应该重启mariadb守护进程,以便我们对mysql.cnf文件所做的更改生效:
sudo systemctl restart mariadb
接下来,我们将设置从服务器。但在此之前,我们需要考虑一个可能会使过程更加简单的因素。在生产环境中,很可能主服务器仍然在写入数据。如果在设置从服务器时不必担心主数据库的变化,设置从服务器的过程会更加简单。以下命令,在 MariaDB shell 中执行时,将锁定数据库并防止额外的更改:
FLUSH TABLES WITH READ LOCK;
如果你完全确定不会向主服务器写入任何数据,可以跳过这一步。
接下来,我们应该使用mysqldump来确保在开始同步之前,主服务器和从服务器包含相同的数据。如果它们已经同步,开始时的过程会更加顺利,而不是在稍后尝试镜像数据库。使用我们在前一节中使用的mysqldump,创建主服务器数据库的转储文件,然后将该转储文件导入到从服务器。传输转储文件最简单的方法是使用rsync或scp。然后,在从服务器实例上,使用mariadb导入该文件。
在主服务器上备份数据库的命令如下:
mysqldump -u admin -p --databases mysampledb > mysampledb.sql
将mysampledb.sql文件传输到从服务器后,你可以将备份导入到从服务器:
mariadb -u root -p < mysampledb.sql
同样在从服务器上,我们需要编辑/etc/mysql/conf.d/mysql.cnf,然后将以下代码添加到文件末尾(确保在[mysql]后添加一个空行):
[mysqld]
server-id=2
虽然本书的范围不包括此内容,但你可以设置多个从数据库服务器。如果你这么做,每个服务器都需要一个独特的server-id。
在继续之前,请确保你已经重启了从服务器上的mariadb服务:
sudo systemctl restart mariadb
从从服务器的root MariaDB shell 中,输入以下命令。根据需要相应地更改命令中的 IP 地址:
CHANGE MASTER TO MASTER_HOST="192.168.1.184", MASTER_USER='replicate', MASTER_PASSWORD='password';
现在我们已经完成了同步配置,可以解锁主服务器的表。在主服务器上,在 MariaDB shell 中执行以下命令:
UNLOCK TABLES;
现在,我们可以检查从服务器的状态,看看它是否在运行。
在从服务器的 MariaDB shell 中,执行以下命令:
SHOW SLAVE STATUS \G;
这里我们添加了\G,它会将输出格式改为垂直显示,而不是横向显示。
如果一切顺利,我们应该在输出中看到以下行:
Slave_IO_State: Waiting for master to send event
如果从服务器没有运行(Slave_IO_State为空),请执行以下命令:
START SLAVE;
接下来,再次检查从服务器进程的状态以进行验证:
SHOW SLAVE STATUS \G;
从现在起,你在主服务器上添加的任何数据都应该会被同步到从服务器。为了测试,在主服务器上的mysampledb数据库的Employees表中添加一条新记录:
USE mysampledb;
INSERT INTO Employees VALUES ('Optimus Prime', '100', 'Transformer');
在从服务器上,检查相同的数据库和表,查看新值是否出现。可能需要一两秒:
USE mysampledb;
SELECT * FROM Employees;
如果在运行SHOW SLAVE STATUS \G时看到Slave_IO_State行中有任何错误,或者数据库没有正确同步,以下是一些你可以尝试的方法。首先,确保主数据库服务器在0.0.0.0端口3306上监听连接。
为了测试这一点,运行这个ss命令的变体,看看mariadb进程在哪个端口上监听(它列出为mysqld):
ss -tulpn | grep mysqld
输出应类似于以下内容:
tcp LISTEN 0 80 0.0.0.0:3306 0.0.0.0:* users:(("mysqld",pid=9768,fd=23))
如果你看到服务正在127.0.0.1:3306上监听,这意味着它只接受来自本地主机的连接。在本节早些时候,我提到过更改/etc/mysql/mariadb.conf.d/50-server.cnf文件中的bind地址。确保你已经完成了这一操作并重启了mariadb。在我的测试中,曾经发生过一个情况,在我做出这个更改后,mariadb服务被锁定了,尝试重启进程没有任何反应(我最终不得不重启整个服务器,这通常不是必需的)。一旦服务器重新启动,它就会开始监听来自网络的连接。
如果在从服务器上运行SHOW SLAVE STATUS \G时收到关于认证的错误,确保你已经在主服务器上运行了FLUSH PRIVILEGES。即使你已经运行过,也请再次执行以确保。同时,仔细检查你是否在使用正确的用户名、IP 地址和密码进行同步。为了方便起见,这里是我们在主服务器上运行的授予复制权限的命令:
GRANT REPLICATION SLAVE ON *.* to 'replicate'@'192.168.1.204' identified by 'password';
FLUSH PRIVILEGES
这是我们在从服务器上运行的命令:
CHANGE MASTER TO MASTER_HOST="192.168.1.184", MASTER_USER='replicate', MASTER_PASSWORD='password';
最后,确保你的主数据库和从数据库包含相同的数据库和表格。如果从服务器上没有该数据库,主服务器将无法更新从服务器上的数据库。如果需要复习,可以回到我关于mysqldump的示例。你只需要使用一次mysqldump并将数据库导入到从服务器中,因为一旦你启用了复制,主服务器上的任何更改都应当会同步到从服务器。如果你在使用mysqldump命令时遇到困难,你可以手动在从服务器上创建mysampledb和Employees表格,这就是同步开始所需的所有内容。
同步应该会在一分钟内开始,但你可以在从服务器上执行STOP SLAVE,然后执行START SLAVE,强制它重新同步,而不需要等待。
到此为止,一切应该都已经完成了。此时,你应该拥有完全可用的主数据库和从数据库服务器。为了进一步练习,尝试添加额外的数据库、表格和用户,并向你的数据库中插入新行。
值得一提的是,我们在这里创建的用户不会同步到从服务器,因此如果你希望这些用户也出现在从服务器上,可以使用我们在本章前面提到的命令在从服务器上创建用户。
总结
根据你的技能水平,你要么是第一次学习 SQL 数据库的管理员,要么是一个经验丰富的老手,想了解如何在 Ubuntu 服务器上实现数据库服务器。在本章中,我们深入探讨了 Ubuntu 如何实现这项技术,并完成了自己的数据库服务器的设置。我们还练习了一些 MariaDB 的语法示例,如创建数据库,以及设置用户及其权限。我们还设置了主从服务器以实现数据复制。
数据库管理是一个广泛的主题,我们这里只是触及了表面。能够管理 MySQL 和 MariaDB 数据库无疑是一项非常受欢迎的技能。如果你以前没有使用过这些数据库,本章将为你提供一个良好的基础,帮助你开始研究。
在下一章中,我们将使用我们的数据库服务器作为 Nextcloud 的基础,并在我们设置 Web 服务器的过程中进行配置。完成这些数据库概念的练习后,前往第十四章,提供 Web 内容,在那里我们将进入 Web 托管的世界。
进一步阅读
- 配置 MariaDB 选项文件:
mariadb.com/kb/en/configuring-mariadb-with-option-files/
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十四章:提供 Web 内容
Ubuntu Server 的灵活性使其成为一个极好的平台,用来托管你组织的 web 存在。在本章中,我们将讨论 Apache 和 NGINX,它们是目前互联网上领先的 web 服务器软件。我们将介绍两者的安装、配置和扩展,并用传输层安全性(TLS)来保护它们。此外,我们还将讨论如何安装 Nextcloud,这是一个非常适合搭建私人云环境的解决方案,供你的组织用来协作和共享文件。在处理与在 Ubuntu Server 上托管 web 内容相关的概念时,我们将覆盖以下内容:
-
安装与配置 Apache
-
安装额外的 Apache 模块
-
用 TLS 保护 Apache
-
安装与配置 NGINX
-
设置与配置 Nextcloud
为了让我们入门,我们首先来看一下 Apache 的配置,以及一些基本配置。
安装与配置 Apache
了解任何技术的最佳方法就是直接上手。本章开始时我们将安装 Apache。但首先,究竟什么是 Apache?对于那些还不清楚的人,Apache 是一个流行的应用程序,通常运行在 Linux 和 Unix 服务器上,用来向用户提供网页。它在后台运行,响应请求 URL 的用户并提供 HTML 页面。
安装 Apache 非常简单;实际上只需要安装 apache2 包:
sudo apt install apache2
默认情况下,Ubuntu 会在安装 apache2 包后立即启动并启用 apache2 守护进程。你可以通过以下命令自行确认:
systemctl status apache2
事实上,此时,你已经拥有了一个(在实际操作中)完全功能的 web 服务器。如果你打开一个网页浏览器并输入你刚刚安装了 Apache 的服务器的 IP 地址,你应该会看到 Apache 的示例网页:

图 14.1:Apache 提供的默认示例网页
就这样,你已经正式提供了 web 内容。你所需要做的就是安装 apache2 包,你的服务器就转变成了一个 web 服务器。本章结束,接下来可以继续进行。
当然,Apache 不仅仅是安装并展示示例网页那么简单。虽然你可以将示例网页中的内容替换为自己的内容,并顺利完成托管任务,但 Apache 的功能远不止于此。例如,在 /etc/apache2 目录中有多个配置文件,它们控制着网站的托管方式,以及 Apache 会查找哪些目录以找到需要托管的网页。Apache 还有插件,我们也会介绍它们。
Apache 提供网页的目录称为文档根目录,默认路径为 /var/www/html。在该目录内,你会看到一个 index.xhtml 文件,这实际上是当你访问一个未修改的 Apache 服务器时看到的默认页面。本质上,这是一个测试页面,旨在向你展示服务器正在工作,并提供一些关于默认配置的信息。
然而,你并不限于在一个服务器上托管单个网站。Apache 支持虚拟主机的概念,这允许你从单个服务器上服务多个网站。每个虚拟主机由一个单独的配置文件组成,该文件根据名称或 IP 地址进行区分。
例如,你可以拥有一个 Apache 服务器,使用一个 IP 地址托管两个不同的网站,如 acmeconsulting.com 和 acmesales.com。这些是假设的网站,但你明白意思。为了设置这个,你需要为 acmeconsulting.com 和 acmesales.com 创建单独的配置文件,并将它们存储在 Apache 配置目录中。每个配置文件都会包括一个 <VirtualHost> 部分,在其中你会放置一个标识符,比如名称或 IP 地址,用于区分不同的网站。当请求到来时,Apache 会根据匹配的标准,将 acmeconsulting.com 或 acmesales.com 服务到用户的浏览器。每个站点的配置文件通常以 .conf 为文件扩展名,并存储在 /etc/apache2/sites-available 目录中。我们稍后会更详细地讲解这些内容,所以如果你现在还不明白也不用担心。
设置新站点(虚拟主机)的基本工作流程通常如下:
-
网页开发者创建网站及相关文件
-
这些文件被上传到 Ubuntu 服务器,通常位于
/var/www的子目录中,或是管理员选择的其他目录 -
在将必要的文件添加到文档根目录后,管理员会确保
www-data用户拥有该目录中的所有文件(在 Apache 的情况下) -
服务器管理员为该站点创建一个配置文件并将其复制到
/etc/apache2/sites-available目录中 -
管理员启用该站点并重新加载 Apache
在 Debian 和 Ubuntu 中,启用虚拟主机的方式与其他平台有所不同。实际上,有两个专门的命令来处理这个目的:a2ensite用于启用站点,a2dissite用于禁用站点。例如,你在 CentOS 等发行版中是找不到这些命令的。每个站点的配置文件存储在/etc/apache2/sites-available/目录中,我们使用a2ensite命令来启用每个配置。假设要在 Ubuntu 服务器上托管一个 URL 为acmeconsulting.com的站点,我们将创建/etc/apache2/sites-available/acmeconsulting.com.conf配置文件,并使用以下命令启用站点:
sudo a2ensite acmeconsulting.com.conf
sudo systemctl reload apache2
我的示例中没有使用绝对路径;只要你将配置文件复制到正确的位置,a2ensite和a2dissite命令就会知道在哪里找到它。
如果我们出于某些原因想禁用站点,我们可以对该站点的配置文件执行a2dissite命令:
sudo a2dissite acmeconsulting.com.conf
sudo systemctl reload apache2
如果你对背后如何运作感到好奇,当运行a2ensite命令时,它会针对配置文件创建一个符号链接,并将其存储在/etc/apache2/sites-enabled目录中。当你运行a2dissite来禁用站点时,这个符号链接会被删除。
默认情况下,Apache 会使用它在/etc/apache2/sites-enabled目录中找到的任何配置文件。在启用或禁用站点后,你需要刷新 Apache 的配置,这时就用到了reload选项。这个命令不会重启 Apache 本身(所以正在使用你现有站点的用户不会受到干扰),但它确实给了 Apache 一个重新加载配置文件的机会。如果你将reload替换为restart,Apache 将执行完全重启。你只有在遇到 Apache 问题或启用新插件时才需要执行重启,但在大多数情况下,生产系统上更推荐使用reload选项。
Apache 的主配置文件位于/etc/apache2/apache2.conf。你可以查看这个文件的内容,文件中的注释很好地概述了 Apache 配置的布局。这个文件中的以下几行特别值得关注:
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
如你所见,这就是 Ubuntu 如何配置 Apache 以在/etc/apache2/sites-enabled目录中查找已启用站点的方式。存储在该目录下的任何以.conf为扩展名的文件都会被 Apache 读取。如果你愿意,实际上可以移除这些行,这样 Apache 就会像在其他平台上那样运行,a2ensite和a2dissite命令也将不再有任何用途。然而,最好保留 Ubuntu 实现的框架,因为将配置文件分开是有逻辑意义的,并有助于简化配置。本章将沿用 Ubuntu 的配置管理方式。
如果你只托管一个站点,则不需要额外的虚拟主机。如果你没有更改 Apache 配置,/var/www/html中的内容将由默认虚拟主机提供。这就是 Apache 附带的示例网站所在的地方。如果你只需要托管一个站点,你可以删除存储在此目录中的默认index.xhtml文件,并将其替换为你网站所需的文件。如果你希望自己进行测试,可以先备份默认的index.xhtml文件,然后使用一些标准的 HTML 创建一个新的文件。你应该看到默认页面变更为你刚才添加到文件中的内容。
000-default.conf文件是特殊的,因为它基本上是控制默认 Apache 示例网站的配置文件。如果你查看/etc/apache2/sites-available和/etc/apache2/sites-enabled目录的内容,你会看到000-default.conf配置文件存储在sites-available中,并在sites-enabled中创建了符号链接。这表明,默认情况下,这个站点已包含在 Apache 中,并且它的配置文件在安装 Apache 时即已启用。实际上,如果你只打算在服务器上托管一个网站,那么000-default.conf配置文件就是你需要的全部。该文件的内容如下,但我已去除文件中的注释以节省页面空间:
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
正如你所看到的,这个默认的虚拟主机告诉 Apache 在端口80上监听请求,并在请求到达时从/var/www/html提供内容。开头的<VirtualHost>声明在端口80上监听所有请求(星号是一个通配符),因此基本上它处理所有从端口80进入服务器的网页流量。ServerAdmin子句指定了如果站点出现问题时,错误信息中显示的电子邮件地址。
DocumentRoot设置告诉 Apache 查找哪个目录以便找到要提供给该虚拟主机的文件。/var/www/html是默认值,但一些管理员选择自定义此设置。该文件还包含了发送日志信息的位置。访问日志包含与传入的 HTTP 请求相关的信息,默认情况下存储在/var/log/access.log中。错误日志存储在/var/log/error.log,包含了你在有人访问你的网站时遇到问题时可以使用的信息。${APACHE_LOG_DIR}变量默认等同于/var/log,并且这个设置存储在/etc/apache2/envvars文件中,以防你出于某些原因希望更改此设置(例如,你希望使用自定义的日志目录)。
如果你希望通过创建一个额外的虚拟主机在同一台服务器上托管另一个站点,可以使用与原始文件相同的框架,并进行一些额外的自定义。虚拟主机文件存储在/etc/apache2/sites-available目录下,文件名以.conf结尾。以下是一个假设网站acmeconsulting.com的示例。这样的虚拟主机文件可能会保存为/etc/apache2/sites-available/acmeconsulting.com.conf:
<VirtualHost **192.168.1.104:80**>
ServerAdmin webmaster@localhost
**DocumentRoot /var/www/acmeconsulting**
ErrorLog ${APACHE_LOG_DIR}/acmeconsulting.com-error.log
CustomLog ${APACHE_LOG_DIR}/acmeconsulting.com-access.log combined
</VirtualHost>
为了节省时间,我通常会复制另一个虚拟主机文件,甚至是默认的那个,然后根据需要进行修改。在这个具体的例子中,我强调了一些重要的不同之处。首先,在这个虚拟主机中,我不是监听所有进入80端口的连接;相反,我是专门监听指向192.168.1.104的80端口的传入流量。这是因为这个服务器有两个网卡,因此有两个 IP 地址。通过虚拟主机,我能够根据请求来自哪个 IP 地址,提供不同的网站。
接下来,我将DocumentRoot设置为/var/www/acmeconsulting。每个虚拟主机应该有自己的独立DocumentRoot,以将每个站点与其他站点分开。在我的服务器上,我通常会禁用或删除示例虚拟主机(即默认DocumentRoot为/var/www/html的那个)。相反,我使用/var/www作为基础目录,每个虚拟主机将其自己的目录作为该基础目录的子目录。
另一个我觉得有用的改变是给每个虚拟主机设置独立的日志文件。通常,Apache 会使用/var/log/apache2/error.log和/var/log/apache2/access.log来存储所有站点的日志条目。如果你的服务器上只有一个站点,那没问题。但当你托管多个站点时,我发现给每个站点设置独立的日志文件很有用。这样,当你遇到某个站点的问题时,你不需要浏览与该站点无关的日志条目来查找你需要的内容。在我的示例中,我将网站名称插入到日志文件名中,因此这个虚拟主机的错误日志记录在/var/log/apache2/acmeconsulting.com-error.log文件中,访问日志则写入/var/log/apache2/acmeconsulting.com-access.log。这些日志文件将在你重新加载 Apache 时自动创建。
对于只有一个 IP 地址的服务器,你仍然可以设置多个虚拟主机。你可以通过名称来区分虚拟主机,而不是通过 IP 来区分。这在 Ubuntu 的虚拟私人服务器(VPS)安装中很常见,在这种情况下,你通常会收到 VPS 提供商分配给你的单个 IP 地址。对于基于名称的虚拟主机,我们会在配置中使用 ServerName 选项。请参考以下示例,看看如何操作。在这个示例中,我将基于名称的虚拟主机添加到它们各自的文件中。我将文件命名为 000-virtual-hosts.conf,并将其存储在该目录中。其内容如下:
<VirtualHost *:80>
ServerName acmeconsulting.com
DocumentRoot /var/www/acmeconsulting
</VirtualHost>
<VirtualHost *:80>
ServerName acmesales.com
DocumentRoot /var/www/acmesales
</VirtualHost>
对于每个虚拟主机,我都会声明一个与之匹配的 ServerName 和 DocumentRoot。在第一个示例中,任何请求 acmeconsulting.com 的流量都会被提供一个 DocumentRoot,其路径为 /var/www/acmeconsulting。第二个示例则查找来自 acmesales.com 的流量,并将其引导到 /var/www/acmesales。你可以根据需要在此列出尽可能多的虚拟主机,只要你的服务器有足够的资源来处理每个站点的流量,就可以托管任意数量的虚拟主机。
如果你使用带有虚拟主机的域名,那么只有在你设置了网络,使得文件中引用的域名能够解析到你服务器的 IP 地址时,才会生效。根据你的配置方式,有多种方法可以实现这一点。如果你使用 VPS 提供商,如 DigitalOcean 或 Linode,你的服务器已经有了一个 IP 地址,你只需要在 DNS 服务器上编辑A 记录,将其指向该 IP 地址。(DNS 记录的各种类型,如 A 记录,已在第十一章,网络服务配置中讲解。)
如果你正在运行自己的 DNS 服务器,你可以在那儿添加 A 记录。如果你使用的是外部 DNS 提供商,你需要登录到账户的仪表板并在那儿添加 A 记录。为了测试,你可以在本地工作站(而不是服务器)编辑 /etc/hosts 文件,将其指向你的新 web 服务器。如果你没有使用 VPS 提供商,你需要在防火墙中转发 80 端口,指向你的内部 web 服务器。由于存在许多不同类型的防火墙,这超出了本书的范围,因此无法全面覆盖所有防火墙模型。
在本章继续进行时,我们将为 Apache 执行一些额外的配置。但此时,你应该已经了解了如何在 Ubuntu 服务器中配置 Apache 的基础知识。如果想要额外练习,随时可以创建更多虚拟主机并为其提供不同的页面。
安装额外的 Apache 模块
Apache 提供了可以安装的额外模块来扩展其功能。这些模块可以提供额外的特性,比如添加对 Python 或 PHP 的支持。Ubuntu 实现的 Apache 包含两个特定的命令,用于启用和禁用模块,分别是 a2enmod 和 a2dismod。Apache 模块通常通过 Ubuntu 仓库中的包进行安装。要查看可用于 Apache 的模块列表,可以运行以下命令:
apt search libapache2-mod
在结果中,你会看到各种可用的模块包,如 libapache2-mod-python(添加对 Python 的支持)和 libapache2-mod-php8.1(添加对 PHP 8.1 的支持)等许多模块。安装 Apache 模块与安装其他软件包一样,通过 apt install 命令进行。在 PHP 支持的情况下,我们可以通过以下命令安装所需的包:
sudo apt install libapache2-mod-php8.1
仅仅安装模块包不足以让模块在 Apache 中可用。必须启用模块,Apache 才能使用它们。如前所述,我们可以使用 a2enmod 和 a2dismod 命令分别启用或禁用模块。你可以使用以下命令查看 Apache 内置模块的列表:
apache2 -l
输出中显示的模块是 Apache 内置的模块,因此你不需要启用它们。如果输出中列出了你网站所需的模块,那么就大功告成了。
要查看所有已安装并准备启用的模块列表,可以单独运行 a2enmod 命令,无需指定任何选项:

图 14.2:a2enmod 命令显示可用的 Apache 模块列表
a2enmod 命令的输出结束时会询问你是否希望启用任何模块:
Which module(s) do you want to enable (wildcards ok)?
如果你愿意,可以输入任何其他想要启用的模块名称,然后按 Enter。或者,你可以不输入任何内容直接按 Enter,返回到提示符。
如果你为 a2enmod 命令指定了模块名作为选项,它将为你启用该模块。要启用 PHP 8.1(稍后我们需要使用),可以运行以下命令:
sudo a2enmod php8.1
然而,如果你安装了一个额外模块的包,它很可能在安装过程中就已经为你启用了。在 Debian 和 Ubuntu 中,守护进程和模块在其包安装后通常会自动启用,Apache 也不例外。以我使用的 libapache2-mod-php8.1 包为例,安装该包后,模块应该已经为你启用:
sudo a2enmod php8.1
如果模块已经启用,当你尝试使用 a2enmod 启用它时,会看到类似以下的输出:
Module php8.1 already enabled
如果模块尚未启用,我们将看到以下输出:
Enabling module php8.1\.
To activate the new configuration, you need to run:
systemctl restart apache2
如指示,为了使模块的启用生效,我们需要重启 Apache。请注意,重启 Apache 会导致其托管的任何站点在此过程中不可用。在禁用模块方面,命令语法非常相似。要执行此操作,您将使用a2dismod命令以及要禁用的模块的名称:
sudo a2dismod php8.1
启用已经先前启用的模块将产生类似以下输出:
Module php8.1 disabled.
要激活新配置,您需要运行:
systemctl restart apache2
您在 Apache 服务器上安装和启用的模块将取决于您网站的需求。例如,如果您需要支持 Python,您将需要安装libapache2-mod-python包。如果您安装了第三方包,如 WordPress 或 Drupal,您需要参考这些包的文档,以获取安装和运行所需模块的列表。一旦您拥有这样的列表,您将知道需要安装哪些包和启用哪些模块。
使用 TLS 安全 Apache
现在,确保您的组织网站通过 HTTPS 加密并可用是个好主意。通过安装签名证书来保护和加密 Web 流量,历史上已经实现了 Web 流量的加密,通过利用安全套接层(SSL)或更近期的传输层安全(TLS)。这两种方法使用加密技术来保护和加密 Web 流量。这两种方法有所不同,但最终结果是相同的。从现在开始,推荐使用 TLS,因为它提供了额外的安全强度,尽管目前仍然可以看到使用 SSL,因为它还没有完全淘汰。
设置并受益于 TLS 并不是很难,而且将帮助保护您的组织免受潜在的常见漏洞攻击。虽然使用 TLS 不能保护您免受所有野外利用的攻击,但它确实提供了一层您想要受益的保护。不仅如此,您的客户现在几乎都希望您保护他们的通信。在本节中,我们将看看如何在我们的 Apache 安装中使用 TLS。我们将通过启用它、生成证书以及配置 Apache 使用这些证书来工作,既可以使用单站点配置,也可以使用虚拟主机配置。
默认情况下,Ubuntu 的 Apache 配置会在端口80上监听流量,但不会在端口443(HTTPS)上。您可以通过运行以下命令来检查:
sudo ss -tulpn | grep apache
结果将类似于以下内容,并显示 Apache 监听的端口,这默认仅为端口80:
tcp LISTEN 0 511 *:80 *:* u
sers:(("apache2",pid=11521,fd=4),("apache2",pid=11520,fd=4),
("apache2",pid=11519,fd=4),("apache2",pid=11518,fd=4),("apache2",pid=11517,fd=4),
("apache2",pid=11513,fd=4))
如果服务器也监听端口443,我们将会在输出中看到以下内容:
tcp LISTEN 0 511 *:443 *:*
要启用对 HTTPS 流量的支持,我们首先需要启用ssl模块:
sudo a2enmod ssl
接下来,我们需要重启 Apache:
sudo systemctl restart apache2
除了我们之前讨论的示例网站之外,Ubuntu 的默认 Apache 实现还包括另一个站点配置文件 /etc/apache2/sites-available/default-ssl.conf。与示例站点不同,默认情况下该文件并未启用。这个配置文件与示例站点配置类似,但它监听端口 443 上的连接,并包含与 TLS 相关的附加配置项。以下是该文件的内容,我们已去除注释,以便节省页面空间:
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ssl-cert-
snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-
snakeoil.key
<FilesMatch ".(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory /usr/lib/cgi-bin>
SSLOptions +StdEnvVars
</Directory>
</VirtualHost>
</IfModule>
我们已经在本章前面讨论过 ServerAdmin、DocumentRoot、ErrorLog 和 CustomLog 选项,但该文件中还有一些我们尚未看到的附加选项。在第一行中,我们可以看到该虚拟主机监听的是端口 443。我们还看到这里列出了 _default_,而不是 IP 地址。_default_ 选项仅适用于未指定的流量,这意味着任何进入端口 443 但未在其他虚拟主机中标识的流量。此外,SSLEngine on 选项启用了 TLS 流量。紧接着,我们可以看到 TLS 证书文件和密钥文件的相关选项,我们稍后会详细讨论。
我们还看到有一个 <Directory> 子句,它允许我们对某个目录应用特定的选项。在这个例子中,/usr/lib/cgi-bin 目录应用了 SSLOptions +StdEnvVars 设置,该设置启用了默认的环境变量,以供 TLS 使用。通过 <FilesMatch> 选项,这个设置也应用于扩展名为 .cgi、.shtml、.phtml 或 .php 的文件。BrowserMatch 选项允许你为特定浏览器设置选项,尽管这超出了本章的讨论范围。现在,只需要记住,如果你想为特定浏览器应用设置,是可以做到的。
默认情况下,default-ssl.conf 文件是未启用的。为了使用其配置选项,我们需要启用它,可以通过 a2ensite 命令来完成,就像启用任何其他虚拟主机一样:
sudo a2ensite default-ssl.conf
虽然我们刚刚启用了 TLS,但我们的网站还不安全。我们需要安装 TLS 证书来保护我们的 Web 服务器。我们可以通过两种方式来实现这一点:使用自签名证书或由证书颁发机构签名的证书。这两种方式的实现非常相似,我会讨论这两种方法。对于测试目的,自签名证书是可以的。在生产环境中,自签名证书技术上是可以使用的,但大多数浏览器默认不会信任它们,访问时会出现错误提示。因此,在生产环境中最好避免使用自签名证书。使用自签名证书的网站用户需要绕过错误页面才能继续访问网站,看到这个错误提示可能会让他们避开你的网站。你可以将证书安装到每个用户的 Web 浏览器中,但这会很麻烦。在生产环境中,最好使用由证书供应商签名的证书。
另一种在服务器上设置证书的方法是 Let’s Encrypt,这是一个流行的(且免费的)加密 Web 流量的服务。你可以参考 letsencrypt.org/docs 网站上的说明,以及本章末尾提到的示例文章。
在我们进行这个过程时,我会首先指导你如何通过自签名证书设置 TLS,以便你了解整个过程。我们将创建证书并将其安装到 Apache 中。你不一定需要创建一个网站来完成这个过程,因为你可以直接使用 Apache 附带的示例网站来作为概念验证。如果我们完成这个过程后,还会讨论安装由证书颁发机构签名的证书。
为了开始,我们需要一个目录来存放我们的证书。我将在示例中使用 /etc/apache2/certs,不过你可以使用任何你喜欢的目录,只要记得在 Apache 配置文件中更新你选择的路径和文件名:
sudo mkdir /etc/apache2/certs
对于自签名证书和密钥,我们可以通过以下命令生成这一对文件。可以根据你的网站名称自由更改密钥和证书文件的名称:
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/certs/mysite.key -out /etc/apache2/certs/mysite.crt
系统会提示你输入一些信息来生成证书。根据提示逐一回答问题。以下是你将被询问的问题清单,以及我为每个问题提供的回答。根据你的服务器、环境、组织名称和位置修改答案:
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Michigan
Locality Name (eg, city) []:Detroit
Organization Name (eg, company) [Internet Widgits Pty Ltd]:My Company
Organizational Unit Name (eg, section) []:IT
Common Name (e.g. server FQDN or YOUR name) []:myserver.mydomain.com
Email Address []:webmaster@mycompany.com
现在,你应该看到在 /etc/apache2/certs 目录下创建了两个文件,分别是 mysite.crt 和 mysite.key,它们代表证书和私钥。文件生成后,接下来要做的是配置 Apache 使用这些文件。在 /etc/apache2/sites-available/default-ssl.conf 文件中,查找以下两行:
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
通过在两行前加上#符号,将这两行注释掉:
# SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
# SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
接下来,在你刚刚注释掉的两行下方添加以下两行。如果你使用了自己的命名规则,记得替换目标目录和证书文件名:
SSLCertificateFile /etc/apache2/certs/mysite.crt
SSLCertificateKeyFile /etc/apache2/certs/mysite.key
为了让 Apache 应用新的配置,请重新加载apache2守护进程:
sudo systemctl reload apache2
配置完成后,我们虽然还没完全完成,但已经接近尾声。我们还有少量配置需要添加。但在此之前,让我们先回到安装由证书颁发机构签发的 TLS 证书的话题。安装签名 TLS 证书的过程基本相同,主要的区别在于证书文件的请求和获取方式。一旦你获得了这些证书文件,你需要将它们复制到文件服务器上,并像我们刚才做的那样配置 Apache。为了开始获取签名 TLS 证书的过程,你需要创建一个证书签名请求(CSR)。CSR 基本上是一个证书请求文件,你需要将其提供给证书颁发机构,启动签发证书的流程。你可以通过以下命令轻松生成 CSR:
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr
使用生成的 CSR 文件,你可以请求一个签名证书。CSR 文件应该已经在你的当前工作目录中。整个过程在不同的供应商之间有所不同,但大多数情况下,它是相当直接的。你需要将 CSR 文件发送给他们,支付费用,填写他们网站上的一两份表格,证明你是相关网站的所有者,然后供应商会将所需文件发送给你。听起来可能有些复杂,但证书颁发机构通常会引导你完成整个过程,并明确告诉你他们需要你提供的内容。一旦你完成这个过程,证书颁发机构会将证书文件发送给你,你然后将其安装到服务器上。一旦你在/etc/apache2/sites-available/default-ssl.conf中配置了SSLCertificateFile和SSLCertificateKeyFile选项,指向新的证书文件,并重新加载 Apache,你应该就可以顺利进行。
还有一个额外的步骤,我们需要执行才能正确设置。在这一点上,我们的证书文件应该已经正确安装,但我们还需要告知 Apache 何时应用它们。如果你还记得,apache2包提供的default-ssl.conf文件是用来处理任何没有被虚拟主机明确识别的流量(即<VirtualHost _default_:443>选项)。我们需要确保我们的 Web 服务器在请求 TLS 时能够处理我们现有网站的流量。我们可以在该文件中添加ServerName选项,以确保我们的网站支持 TLS。
将以下选项添加到/etc/apache2/sites-available/default-ssl.conf文件中,紧跟在<VirtualHost _default_:443>下方:
ServerName mydomain.com:443
现在,当流量通过端口443进入服务器,并请求与ServerName选项中输入的域名匹配的域时,应该会为客户端创建一个安全的浏览会话。你应该能在地址栏中看到绿色的锁形图标(这取决于你的浏览器),它表示你的会话是安全的。如果你使用的是自签名证书,可能会首先看到一个错误提示,你需要跳过它,而且你可能不会看到绿色锁形图标。这并不意味着加密没有生效;它只是意味着浏览器对证书持怀疑态度,因为它没有由一个已知的证书颁发机构签名。你的会话仍然会是加密的。
如果你计划在 HTTPS 上托管多个网站,可能需要考虑为每个网站使用一个单独的虚拟主机文件。实现这一点的一个简单方法是使用/etc/apache2/sites-available/default-ssl.conf文件作为模板,并将DocumentRoot更改为托管该站点文件的目录。此外,确保更新SSLCertificateFile和SSLCertificateKeyFile选项,指向该站点的证书文件,并将ServerName设置为与你的站点对应的域名。以下是一个使用 TLS 的假设站点的虚拟主机文件示例。我已将从正常的default-ssl.conf文件中更改的行标出:
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName acmeconsulting.com:443
ServerAdmin webmaster@localhost
DocumentRoot /var/www/acmeconsulting
**ErrorLog ${APACHE_LOG_DIR}/acmeconsulting.com-error.log**
**CustomLog ${APACHE_LOG_DIR}/acmeconsulting.com-access.**
**log combined**
**SSLEngine on**
**SSLCertificateFile /etc/apache2/certs/acmeconsulting/acme.**
**crt**
**SSLCertificateKeyFile /etc/apache2/certs/acmeconsulting/acme.**
**key**
<FilesMatch ".(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory /usr/lib/cgi-bin>
SSLOptions +StdEnvVars
</Directory>
</VirtualHost>
</IfModule>
基本上,我所做的就是创建了一个新的虚拟主机配置文件(使用现有的default-ssl.conf文件作为模板)。我将这个新文件命名为acme-consulting.conf,并将其存储在/etc/apache2/sites-available目录中。我将VirtualHost行更改为监听端口443上的所有流量。添加了ServerName acmeconsulting.com:443这一行,使得这个文件负责处理针对acmeconsulting.com在端口443上的流量。我还将DocumentRoot设置为/var/www/acmeconsulting。此外,我自定义了错误日志和访问日志,以便更容易找到与这个新站点相关的日志信息,因为它的日志条目会被记录到专门的文件中。
根据我的经验,当设置一个打算托管多个网站的 Web 服务器时,像我在示例中使用的那种模块化方法是最有效的。对于每个站点,我通常会为其提供独立的文档根目录、证书文件和日志文件。即使你只打算在服务器上托管一个站点,采用这种模块化方法仍然是一个好主意,因为你以后可能还会想托管其他站点。
所以,以上就是了。你现在应该理解如何在 Apache 中设置安全的虚拟主机。
安装和配置 NGINX
Apache 并不是唯一能够让你在服务器上托管网页内容的技术。NGINX 也具有相同的功能,并且正在迅速获得人气。Apache 仍然是一个很好的选择,即使那是你选择的 web 服务器软件,至少了解一下 NGINX 并学习其基础知识也是个好主意。NGINX 本身也是一个代理服务器,但也能够托管网页内容,这就是它与 Apache 竞争的原因。
在我们继续之前,我想先提一下,你实际上只能在一个 Web 服务器上运行一个 Web 服务器服务。如果你一直跟着操作到现在,你已经有一个功能正常的 Apache Web 服务器。如果你同时安装 NGINX,它可能不会启动,因为它想监听的端口(端口 80 和/或 443)已经被占用。你可以在同一台服务器上运行两个,但那超出了本书的范围。理想情况下,你应该选择使用其中一个。因此,为了继续这一部分,你要么删除 Apache,要么为测试 NGINX 设置一个单独的 Web 服务器。我推荐后者,因为在本章后面,我们将讨论如何托管 Nextcloud,并且我们将使用 Apache 来实现这一目标。如果你现在删除了 Apache,你将不得不重新安装它,才能继续这一部分。理论上,你只需要在启动 nginx 之前停止 apache2 进程,但两个资源共享同一台服务器,涉及许多变量,可能会发生冲突。
要开始使用 NGINX,只需安装它:
sudo apt install nginx
就像 Apache 一样,如果我们在浏览器中输入服务器的 IP 地址,我们将看到一个示例页面,但这次是 NGINX 的版本,而不是 Apache 自带的版本。与之相比,它确实看起来很单调,但它能够正常工作:
图 14.3:NGINX 示例页面
nginx 的默认配置文件存储在 /etc/nginx 目录中。你可以浏览这些文件,以大致了解配置文件的呈现方式。与 Apache 类似,这里也有 sites-enabled 和 sites-available 目录,它们具有相同的功能。
就像 Apache 一样,sites-available 目录存储着可以启用的站点的配置文件,而 sites-enabled 目录存储着已启用的站点的配置文件。不过,与 Apache 不同的是,我们没有专门的命令来启用这些站点。我们需要手动链接它们。尽管我们还没有查看 NGINX 的配置文件,但假设我们已经创建了以下配置文件:
/etc/nginx/sites-available/acmesales.com
要启用该站点,我们需要为它创建一个符号链接,并将该链接存储在 /etc/nginx/sites-enabled 目录中:
sudo ln -s /etc/nginx/sites-available/acmesales.com /etc/nginx/sites-enabled/acmesales.com
然后,我们可以重新加载 nginx:
sudo systemctl reload nginx
目前,名为 default 的站点配置文件存在于 /etc/nginx/sites-available 目录中,并且在 /etc/nginx/sites-enabled 中已经存在对它的符号链接。如果我们只想托管一个站点,我们只需要替换 NGINX 提供的默认内容,它位于 /var/www/html 目录下(与 Apache 相同),然后用我们站点的内容替换。刷新页面后,我们就可以开始使用了。
如果我们想要从一台服务器提供多个站点,那么 default 文件是创建额外虚拟主机的一个很好的起点。我们可以从将它复制到一个新名称开始:
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/acmesales.com
显然,acmesales.com 是一个示例,所以你可以随意将其命名为你希望的任何名称。
现在,我们可以编辑这个文件并更改它以提供额外的内容。首先,只有一个站点可以被称为 默认 站点。在 NGINX 中,默认站点是在没有其他站点匹配请求时进行响应的站点。因此,我们需要从新复制的配置中删除 default_server 的两处出现。找到这些行:
listen 80 default_server;
listen [::]:80 default_server;
将它们改成这样:
listen 80;
listen [::]:80;
接下来,我们需要调整 server_name 选项,以指向我们新站点的名称。添加这一行:
server_name acmesales.com www.acmesales.com;
现在,我们需要将文档根目录更改为存储新站点文件的目录。找到这一行:
root /var/www/html;
然后将其更改为:
root /var/www/acmesales.com;
到目前为止,最终的文件应该如下所示:
server {
listen 80;
listen [::]:80;
root /var/www/acmesales.com;
index index.xhtml index.htm index.nginx-debian.xhtml;
server_name acmesales.com www.acmesales.com;
location / {
try_files $uri $uri/ =404;
}
}
你可能会发现,NGINX 配置文件的配置格式比 Apache 更简单。我也觉得这是事实,而且我注意到我用 NGINX 配置的网站通常比 Apache 配置的网站配置文件行数要少。
到目前为止,假设你在 /var/www/acmesales.com 中有必要的内容,并且配置文件正确,当你重新加载 nginx 后,新站点应该会响应。但 TLS 呢?我建议我们始终保护我们的网站,无论我们使用哪种解决方案来提供它。使用 NGINX 时,我们可以轻松添加这个功能。证书文件本身无论是使用 Apache 还是 NGINX 都是相同的。如果你还没有创建证书文件,请参考本章中我们创建证书文件的部分。假设你已经有了证书文件,我们只需要对配置做一些额外的修改。
首先,我们将前两行更改为监听 443 端口并使用 TLS,而不是标准的 80 端口:
listen 443 ssl;
listen [::]:443 ssl;
接下来,我们将在 location 部分之前添加以下两行:
ssl_certificate /etc/certs/cert.pem;
ssl_certificate_key /etc/certs/cert.key;
ssl_session_timeout 5m;
为了使其正常工作,你需要调整路径和 cert 文件的名称,确保它们与服务器上设置的名称匹配。到目前为止,整个文件应该如下所示:
server {
listen 443 ssl;
listen [::]:443 ssl;
root /var/www/html;
index index.xhtml index.htm index.nginx-debian.xhtml;
server_name acmesales.com www.acmesales.com;
ssl_certificate /etc/certs/cert.pem;
ssl_certificate_key /etc/certs/cert.key;
ssl_session_timeout 5m;
location / {
try_files $uri $uri/ =404;
}
}
最后,一个潜在的问题是用户可能会通过端口80访问我们的网站,而不是使用 HTTPS。我们可以告诉 NGINX 自动将这些人转发到我们网站的安全版本。为此,我们可以编辑默认配置文件(/etc/nginx/sites-available/default),并在两个 listen 指令之后添加以下行:
return 301 https://$host$request_uri;
现在,任何用户访问我们网站的 HTTP 版本时,他们会自动重定向到安全的 HTTPS 版本。
既然我们已经了解了如何通过 Apache 和 NGINX 提供 Web 内容,让我们来看看如何设置我们自己的 Nextcloud 服务器。
设置和配置 Nextcloud
我想我们可以以一个有趣的活动来结束这一章:设置我们自己的 Nextcloud 服务器。Nextcloud 是一个非常有用的 Web 应用程序,对任何组织都很方便。即使您不在公司网络中工作,Nextcloud 对于单个用户也是一个很好的工具。您可以用它在计算机之间同步文件,存储并同步联系人,跟踪您正在处理的任务,从邮件服务器获取电子邮件等。要完成这个活动,您需要一个 Web 服务器。Nextcloud 支持多种不同的 Web 服务器平台,但在本例中,我们将使用 Apache。
您还需要安装 MySQL 或 MariaDB,因为 Nextcloud 需要自己的数据库。我们在第十三章《管理数据库》中介绍了 MariaDB 数据库的安装和管理。在本节中,我会给您所有需要设置数据库的命令,但如果有任何命令让您感到困惑,请参考第十三章《管理数据库》。
要开始,我们需要下载 Nextcloud。为此,请访问该项目的网站 www.nextcloud.com 并导航到 下载 部分。该网站的布局可能会时常变化,但在写作时,第一个要点击的链接是一个标有 获取 Nextcloud 的按钮,它会将您带到下载页面。根据写作时的信息,Nextcloud 的直接下载页面的 URL 当前是 nextcloud.com/install。
到达后,寻找 压缩文件 标题,并展开它。在下面,您应该能看到一个按钮,写着 为服务器下载,但现在不要点击它。相反,右键点击它,然后点击 复制链接地址 或根据您使用的浏览器选择类似的选项。
这应该将下载链接复制到您的剪贴板。如果 Nextcloud 网站的布局自发布以来有所变化,您实际上只是需要找到 ZIP 文件的 URL 来下载 Nextcloud,然后将其复制到您的计算机剪贴板中。
接下来,打开一个 SSH 会话连接到您的 Web 服务器。确保您当前在家目录下,并执行以下命令:
wget <URL of Nextcloud>
要获取 Nextcloud URL,只需在终端中键入 wget 后粘贴 URL。你的完整命令将类似于以下内容:
wget https://download.nextcloud.com/server/releases/latest.zip
该命令将把 Nextcloud 软件下载到当前工作目录。接下来,我们需要解压该归档文件:
unzip latest.zip
如果你收到提示 unzip 命令不可用的错误消息,可能需要安装它:
sudo apt install unzip
现在,让我们将新解压的 nextcloud 目录移动到 /var/www/html:
sudo mv nextcloud /var/www/html/nextcloud
为了使 Nextcloud 正常运行,Apache 用于提供内容的用户帐户需要对其拥有完全访问权限。我们可以使用以下命令将 www-data 用户设置为 nextcloud 目录的所有者:
sudo chown www-data:www-data -R /var/www/html/nextcloud
现在,你应该已经将 Nextcloud 软件所需的文件安装到了 /var/www/nextcloud 目录下。为了使其正常工作,Apache 需要一个配置文件,该文件将 /var/www/nextcloud 作为其文档根目录。我们可以在以下位置创建所需的文件:
/etc/apache2/sites-available/nextcloud.conf
包含在该文件中的示例内容如下:
Alias /nextcloud "/var/www/html/nextcloud/"
<Directory /var/www/html/nextcloud/>
Options +FollowSymlinks
AllowOverride All
<IfModule mod_dav.c>
Dav off
</IfModule>
SetEnv HOME /var/www/html/nextcloud
SetEnv HTTP_HOME /var/www/html/nextcloud
</Directory>
类似于我们之前讨论的 Apache 配置,这里我们为 Nextcloud 添加了一个配置文件,该文件设置了一个别名,将 /nextcloud 指向 www.mydomain.com/nextcloud。本质上,它允许通过你的域名访问 Nextcloud,并且在域名后加上 /nextcloud。文件的其他部分禁用了 WebDAV(这是一种允许 Web 服务器充当文件服务器的方式,但在我们的情况下不需要),然后启用了环境变量来设置 HOME 和 HTTP_HOME 为 Nextcloud 的文档根目录,在我们的情况下是 /var/www/html/nextcloud。
接下来,我们启用新站点:
sudo a2ensite nextcloud.conf
接下来,我们需要对 Apache 进行一些更改。首先,我们需要确保安装了 libapache2-mod-php8.1 包,因为 Nextcloud 需要 PHP,但我们还需要一些额外的包。你可以使用以下命令安装 Nextcloud 的先决条件包:
sudo apt install libapache2-mod-php8.1 php8.1-curl php8.1-gd php8.1-intl php8.1-mbstring php8.1-mysql php8.1-xml php8.1-zip
接下来,重启 Apache,以便它能够利用新的 PHP 插件:
sudo systemctl restart apache2
此时,我们需要一个 MySQL 或 MariaDB 数据库供 Nextcloud 使用。该数据库可以存在于另一台服务器上,或者你也可以在安装了 Nextcloud 的同一台服务器上共享它。如果你还没有设置 MariaDB,第十三章《管理数据库》中已有详细说明。此时假设你已经安装并运行了 MariaDB。
使用你的 root 用户或具有完全 root 权限的用户登录到 MariaDB 实例。你可以通过以下命令创建 Nextcloud 数据库:
CREATE DATABASE nextcloud;
接下来,我们需要为 Nextcloud 向 MariaDB 添加一个新用户,并赋予该用户对 nextcloud 数据库的完全访问权限。我们可以通过以下命令来完成这两个操作:
GRANT ALL ON nextcloud.* to 'nextcloud'@'localhost' IDENTIFIED BY 'super_secret_password';
确保将 super_secret_password 更改为一个非常强的(最好是随机生成的)密码。务必将此密码保存在安全的地方。
现在我们拥有了配置 Nextcloud 所需的一切。你现在应该能够在网页浏览器中访问你的 Nextcloud 实例。只需输入类似于以下的 URL,将示例 IP 地址替换为你服务器的 IP 地址:
http://172.16.250.133/nextcloud
如果你使用的是子域名,并为 Nextcloud 配置了自己的虚拟主机,那么该 URL 将类似于以下内容:
http://nextcloud.yourdomain.com/nextcloud
你应该看到一个页面,要求你配置 Nextcloud:

图 14.4:Nextcloud 配置页面
如果你没有看到这个页面,请确保 /var/www/html/nextcloud 目录可以通过 Apache 访问。还要确保你为 Nextcloud 配置了一个合适的虚拟主机,并将该目录作为其文档根目录。
这个页面会要求你提供一些信息。首先,你会看到 用户名 和 密码。这并不是让你输入一个已存在的账户,而是要求你设置一个全新的管理员账户。这不应是你日常使用的账户,而应是一个只在需要添加用户和维护系统时使用的管理员账户。请注意,它不会要求你确认密码,因此你需要确保输入的是你认为自己输入的密码。为了安全起见,最好先在文本编辑器中输入密码,然后将密码复制粘贴到 密码 框中,以确保不会将自己锁定。
数据文件夹 默认为 /var/www/html/nextcloud/data。这个默认设置通常是可以接受的,但如果你已将服务器配置为使用单独的数据分区,可以在这里进行配置。如果你打算在 Nextcloud 服务器上存储大量数据,设置一个独立的分区可能是一个好主意。如果是这样,你可以在这里进行设置,否则保持默认设置。
在接下来的部分中,你将被要求填写我们之前创建的 Nextcloud 数据库的信息。数据库用户 和 数据库密码 将使用我们在为 Nextcloud 设置 MariaDB 数据库时创建的值。在我的示例中,我使用了 nextcloud 作为用户名和 数据库名。密码则是我们在设置数据库用户账户并授予权限时所使用的密码。最后,数据库服务器默认是 localhost,这在你将数据库设置在与 Nextcloud 服务器同一台机器上时是正确的。如果不是,请在这里输入你的数据库服务器的地址,如果它在其他地方。以下截图展示了填充完整的表单,其中包含了我们迄今为止在本节中使用的示例值:

图 14.5:Nextcloud 配置页面
就这样!如果一切顺利,Nextcloud 会在后台自动设置好,你接着会进入主屏幕。由于你目前只创建了一个admin账户,我建议你为自己以及任何希望访问你 Nextcloud 服务器的朋友或同事创建账户。为此,请进入 Nextcloud 页面右上角,在那里你会看到一个类似齿轮的图标。点击该图标后,你将看到一个用户选项:

图 14.6:Nextcloud 菜单
在用户页面,你可以添加其他用户来访问 Nextcloud。点击新建用户按钮。只需在页面顶部填写用户名和密码字段,然后点击蓝色的勾选图标即可完成操作:

图 14.7:向 Nextcloud 添加新用户
作为管理员用户,你可以启用或禁用用户使用的各种应用。Nextcloud 默认启用了基本的应用套件,如文件共享和照片插件。你还可以启用更多应用来扩展其功能。在 Nextcloud 主屏幕的右上角,你会看到一个类似齿轮的图标,点击后可以找到应用链接,点击进入即可添加更多功能。你可以自由启用其他应用来扩展 Nextcloud 的能力。我个人必备的一些应用包括笔记和任务。
现在,你已经拥有了自己的 Nextcloud 服务器。我认为 Nextcloud 是一个非常实用的平台。一些 Linux 桌面环境(例如 GNOME)内置了集成功能,允许你将 Nextcloud 账户直接添加到桌面,这样就可以与电脑同步日历和联系人。Linux、Windows 和 macOS 上也有独立的客户端应用可以下载,它们可以从我们下载 Nextcloud 的同一网址获取。
我相信你一定同意,Nextcloud 是一个非常有用的工具。如果想了解如何使用 Nextcloud,请查看手册。事实上,它可以在应用程序内的文件应用中找到。
总结
在本章内容中,我们探讨了如何使用 Apache 提供网页服务。我们首先安装并配置了 Apache,然后添加了额外的模块。我们还讨论了虚拟主机的概念,它允许我们在单个服务器上提供多个网站,即使我们只有一个网络接口。接着,我们演示了如何使用 TLS 安全化 Apache 服务器。通过 Apache,我们可以使用自签名证书,或者我们也可以从供应商那里购买 TLS 证书。我们讨论了这两种可能性。最后,我们还设置了 NGINX,它是一款功能强大的应用程序,正在变得越来越流行。我们以安装 Nextcloud 的指南结束了这一章,Nextcloud 作为一个应用,我相信你会发现它非常有用。
在我们旅程的下一章,我们将深入了解如何使用 Ansible 自动化服务器配置,这非常有趣。
相关视频
- Nextcloud 设置教程:
linux.video/nextcloud-setup
进一步阅读
-
NGINX 文档:
nginx.org/en/docs/ -
来自 Ubuntu 社区 Wiki 的 NGINX 文档:
help.ubuntu.com/community/Nginx -
Apache HTTP 服务器文档:
httpd.apache.org/docs/ -
Nextcloud 管理手册:
docs.nextcloud.com/server/latest/admin_manual/ -
Certbot 使用说明:
certbot.eff.org/instructions
加入我们社区的 Discord
加入我们社区的 Discord 空间,与作者和其他读者一起讨论:

第十五章:使用 Ansible 自动化服务器配置
如今,拥有成百上千台服务器组成组织基础设施并不罕见。随着用户基数的增加,我们能够扩展环境以满足客户需求。当我们扩展资源并增加额外服务器时,配置和设置这些服务器所花费的时间会显著增加。设置新服务器所花费的时间可能成为一项重大负担——特别是当我们需要在短时间内创建数百台服务器时。随着工作负载的增加,我们需要有一个解决方案来管理我们的基础设施,并尽可能以最小的工作量快速部署新资源。在本章中,我们将探讨配置管理的概念以及自动化部署。听起来很复杂,但其实并不难——你会惊讶于自动化配置有多么简单。
在本章中,我们将覆盖以下内容:
-
理解配置管理的需求
-
为什么选择 Ansible?
-
创建 Git 仓库
-
开始使用 Ansible
-
让你的服务器按照你的指令行事
-
将一切整合在一起——自动化部署 Web 服务器
-
使用 Ansible 的拉取方式
在介绍中,我已经给出了你可能希望将自动化集成到工作流中的一些示例,并在你的环境中实现有效的解决方案。在接下来的部分,我们将更详细地探讨自动化的需求,然后再开始实际操作。
理解配置管理的需求
当我刚开始从事 IT 行业时,那个时候的环境和今天有很大的不同。服务器都是物理服务器,每当你需要一台新服务器时,你需要打电话给供应商并订购一台。
你可能等待了一到两周才等到服务器搭建并发货给你。收到服务器后,你把它安装到机架上,设置操作系统,然后安装所需的应用程序。你会测试这台服务器一段时间,确保软件、硬件和驱动程序的组合是稳定可靠的。过了一段时间,你就可以将新服务器投入生产使用。
如今,系统管理员仍然经常需要购买和安装硬件,就像我在前面提到的那样。然而,随着虚拟机和容器的出现,我们安装的物理硬件通常只是托管虚拟资源的催化剂。过去,我们为每个用例配备一台物理服务器,这意味着我们需要非常大的服务器机房。但在现代,你可能有一台具有数十个核心的服务器,能够运行成百上千个虚拟机。但配置问题依然存在——设置操作系统和应用程序的过程是一个非常耗时的工作。
随着技术环境的变化,对自动化的需求增加。服务器需要快速高效地部署。在典型数据中心中,由于服务器数量庞大,每次需要变更时,逐一连接每台服务器并进行配置变得越来越不现实。例如,当安全漏洞成为新闻时,通常的管理员需要手动在每台服务器上安装补丁。这可能需要几天甚至几周的时间。这显然效率低下。
为了更好地应对这一问题,配置管理的概念变得非常流行。通过配置管理,管理员可以编写某种代码(例如脚本),然后使用工具在每台服务器上执行它。配置管理也被称为基础设施即代码(IaC),基本上让管理员为各种类型的服务器定义一组指南,并自动配置服务器以满足这些要求。这种自动化节省了大量的工作。
配置管理在配置新服务器时也发挥作用。假设为特定类型的服务器定义一些规则,并使其按照这些规则生效。在配置过程中,您希望它安装的应用程序会被自动安装,配置文件被复制,用户被创建,防火墙规则被设置,所有这些都按照您的规范自动完成。更简单地说,假设您只需要通过一个命令就能设置一个类似于 Web 服务器的环境。无需安装 Apache 或做任何手动工作。您只需要请求一个服务器,您所设置的配置管理解决方案将会处理其余的工作。
IaC,基本上是配置管理的一个花哨术语,本质上就是在服务器上自动运行脚本。在本书中,我们已经介绍过自动化。在第六章,提升命令行效率中,我们编写了一个简单的脚本,可以用来备份服务器。同样的思路也可以用于服务器的配置,只需在服务器上线时让其运行一个脚本。对于现有的服务器,您可以只修改一次并将该变更应用到您管理的每台服务器,甚至是某些子集。
配置管理工具,如 Chef、Puppet 等,在这里发挥作用。这些解决方案中的每一个都具有一种特定类型的脚本语言,旨在从零开始简化资源的配置过程。使用这些工具时,通常会有某种程序(或本地安装的代理)来解释来自中央服务器的指令,并在其客户端上执行这些指令。每个解决方案都相对智能,它将确定需要完成的任务并执行相关步骤。
如果满足某个要求,指令会被跳过。如果缺少必要的资源,它将被适当配置。一个这样的配置管理解决方案是 Ansible,我们将在本章中使用它。
为什么选择 Ansible?
在本章中,我将向你展示如何设置 Ansible,接着我们将使用它来自动化一些配置任务。到本章结束时,你将理解一些基本概念,可以开始在你的组织中自动化部署过程。你可能会问,为什么是 Ansible,而不是其他解决方案,比如 Chef 或 Puppet?
一些配置管理解决方案在资源角度上比较沉重。对于其他解决方案,你通常会有一个中央服务器,它将运行一个主程序。这个程序会定期与每个受控服务器通过与安装在每台服务器上的代理进行通信,检查它们的状态。然后,代理将从中央服务器接收指令并执行。
这意味着你需要维持一台具有适度 CPU 和内存要求的服务器,并且通信过程中的客户端代理也需要消耗宝贵的 CPU 来执行指令。这种资源利用可能会对主服务器和客户端服务器造成很大负担。
Ansible 与其他解决方案的最大不同在于,它根本不需要代理。通常会有一个服务器,但它不需要运行任何资源密集型的软件。整个配置过程通过 SSH 完成,因此如果你不想维护一个中央服务器,你甚至可以从你的工作站执行指令。通常,管理员会在每台服务器上创建一个用户帐户,然后中央 Ansible 服务器(或工作站)将通过 SSH 执行命令来更新每台机器的配置。由于每台服务器上都没有安装代理,整个过程的 CPU 消耗要少得多。当然,Ansible 给服务器下达的指令肯定会导致 CPU 使用,但肯定比其他解决方案要少得多。
Ansible 通常通过创建一个清单文件来设置,这个文件包含了一份资源(服务器)列表(形式为主机名或 IP 地址,Ansible 将被指示连接并配置)。如果你想添加一台新服务器,只需要确保该服务器上存在一个特定的用户帐户,然后将它添加到清单中。如果你想移除它,只需要删除清单文件中与该服务器对应的行。非常简单。
然而,Ansible 的一个神奇之处在于,你完全不必运行中央服务器,如果你不想的话。你可以将 Ansible 配置存储在 Git 仓库中,然后让每台服务器从仓库中下载代码并在本地运行。这意味着,如果你有一个动态环境,服务器时常增减(这在云部署中非常常见),你不必担心维护一个清单文件。只需要指示每台服务器下载代码并自行配置。这就是 Ansible 的pull 方法,我将在后面展示给你。
虽然 Chef 和 Puppet 等解决方案各有优点,且使用起来确实很有趣,但我相信你会发现,Ansible 在扩展性方面更优秀,且能让你对这些主机的配置拥有更多的控制权。虽然如何实施 Ansible 完全取决于你,但它给予你的创作自由无与伦比。我已经使用 Ansible 一段时间了,仍在不断发现新的使用方式。这是一项能与您共同成长的技术。
创建 Git 仓库
本章中的示例,建议你创建一个 Git 仓库来存储你的 Ansible 代码。这不是强制性的,因为你可以通过其他方式托管代码,但强烈推荐这么做。尤其是在本章末尾讲解 Ansible 的 pull 方法时,创建 Git 仓库显得尤为重要。如果你已经知道如何使用 GitHub,可以跳过这一部分。
虽然本书无法对 Git 进行全面讲解,但基本概念足以帮助你跟上本书的内容。对于 Git,你可以简单地在服务器上安装git软件包来托管你的代码,但 GitHub 可能是最简单的入门方式。额外的好处是,GitHub 上有许多优秀的项目,你可以从中受益,浏览这些项目的代码是熟悉不同脚本语言和编程语言语法规则的好方法。对于我们的目的,我们将使用 GitHub 作为存储 Ansible 代码的中央位置。
这可能是显而易见的,但 GitHub 是一个公开资源。你上传到服务的任何代码默认都可以供所有人查看。因此,提交到仓库时需要注意你包含的信息。确保不包含任何个人身份信息、密码、蓝图、API 密钥或任何你不希望公众知道的关于你或你组织的信息。你可以创建一个私有仓库来隐藏机密信息,但即便如此,最好不要上传受保护的信息(无论仓库是否为私有)。
要开始,请在 www.github.com 创建一个帐户(如果尚未拥有)。这是一个免费的过程。确保在这里创建一个相对安全的密码。创建完帐户后,单击 新建仓库,然后给它命名(简单地称为 ansible 即可):

图 15.1:在 GitHub 上创建一个 Ansible 仓库
在示例截图中,我创建了一个 公共 仓库,这意味着任何人都可以查看代码。如果你喜欢,也可以创建 私有 仓库。由于在本书的示例中不会包含仓库中的机密信息,因此我们现在不需要担心这个问题。
创建仓库后,我们需要下载它到本地,使用 创建仓库。为此,我们需要安装 git 软件包:
sudo apt install git
接下来,我们应该设置本地的 Git 客户端,以便我们可以填写我们的姓名和电子邮件地址,否则 Git 很可能会抱怨。为此,我们可以使用以下命令,并将引号中的内容替换为你的信息:
git config user.email "you@example.com"
git config user.name "John Doe"
要下载我们的仓库,以下操作将奏效(如果看到关于仓库为空的警告,请忽略):
git clone https://github.com/myusername/ansible.git
现在你已经将 Git 仓库下载到本地。目前,该仓库不包含任何有用的内容。要在仓库中创建文件,只需将工作目录更改为克隆时下载的仓库文件夹内部,并在其中创建任何你想要的文件。默认情况下,Git 不会关心你在仓库内创建的任何文件,直到你添加它们。例如,我们可以使用以下命令创建一个测试文件并提交到仓库:
echo "this is a test" > testfile.txt
git add testfile.txt
git commit -m "initial commit"
使用这些命令,我们使用 echo 创建了一个包含一些文本的测试文件。然后,我们使用 git add 命令告诉 Git 我们希望这个文件成为仓库的一部分。最后,我们通过 git commit 完成了我们的更改,同时使用了 -m 标志并附上了关于提交的消息。此时,这些更改仅存在于本地。要将更改推送回 GitHub,我们从仓库目录内部使用以下命令:
git push origin main
通过按照屏幕提示(GitHub 用户名和密码),我们的更改将被放置在我们的实际仓库内部。
那么,这如何帮助我们进行配置管理呢?通常,管理员用于配置服务器的代码会保存在一个仓库中以确保安全。如果本地的代码丢失了,只需从仓库中重新克隆即可。GitHub 是我们存放代码的安全地方,因为我们可以合理地确信,代码不会丢失,因为该服务非常稳定(不过你仍然可能希望创建一个本地备份以确保安全)。无论你是使用 Ansible、Chef、Puppet 还是其他工具,将代码保存在 Git 仓库中都是常见做法。就 Ansible 而言,这将直接影响本章最后一节,因为我们将使用 ansible-pull 命令,它需要一个仓库 URL。
在实践中,当我们创建 Ansible 剧本时,应该将这些更改提交回仓库。我不会特别要求你这么做,但请在接下来的操作中牢记这一点。当你创建一个新的剧本时,将其添加到仓库中,然后提交。如果你对现有文件进行了更改,记得提交这些更改。确保使用 git push 命令将更改推送回仓库。例如,如果你在仓库中创建了一个名为 myplaybook.yml 的文件,你将执行类似以下的命令:
git add myplaybook.yml
git commit -m "insert message about the commit here"
git push origin main
在继续之前,不妨先练习一下。即使你在生产环境中不使用 Ansible,了解 Git 的基础知识也非常宝贵,因为你几乎肯定会在未来的某个时刻需要它。
开始使用 Ansible
了解 Ansible 的第一件事是它在不断变化。新版本带有令人兴奋的功能,定期发布,而且完全没有放缓的迹象。这个技术引起了很多的关注,因此它也在不断改进。
我之所以提到这一点,是因为尽管本书中的示例是在 Ubuntu 22.04 上编写并测试的,但 Ansible 的新版本会定期发布,且这些版本不仅包含新功能,还包括语法更改。对于本书的需求,仓库中提供的 Ansible 版本应该完全足够。然而,如果你查看网上的 Ansible 剧本示例,它们可能是为较新版本(甚至较旧版本)编写的。如果你遇到针对特定示例的 Ansible 问题,排查的一个好方法是对比教程所用的 Ansible 版本与已安装的版本。对于 Ubuntu 22.04,提供的 Ansible 版本是 2.10.x。你可以从官方网站安装更高版本的 Ansible,但为了本书的目的,我们将使用默认仓库中的版本。
继续通过 apt 安装 ansible:
sudo apt install ansible
现在你应该已经能够使用ansible-playbook命令了,这是我们在探索 Ansible 时将使用的主要命令。Ansible 还提供了其他一些命令,但我们不关心它们。
为了能够跟上本章的其余内容,建议你至少有两台服务器来操作;更多的服务器会更好。如果你有像 VirtualBox 这样的虚拟机(VM)解决方案,只需创建更多的虚拟机。为了节省时间,可以考虑克隆现有虚拟机几次(只是确保不要通过过度分配资源来超载你的计算机/服务器)。
Ansible 最常见的工作流大致是这样的:你有一台安装了 Ansible 的主服务器或工作站。虽然客户端不需要安装代理,但它们需要安装并配置 OpenSSH,因为 Ansible 通过 SSH 进行通信。为了简化操作,建议每台机器上都有一个专门的 Ansible 用户,并且服务器上的 Ansible 用户应能无密码连接到每台机器。你可以随便为 Ansible 用户起个名字;你可以使用ansible,也可以起个更有创意的名字。我们在第十章,连接到网络中介绍了如何创建 SSH 密钥,如果需要回顾,可以参考该章节。创建用户的内容在第二章,管理用户和权限中有所讲解。总之,以下是你为了设置 Ansible 环境需要做的几件事:
-
在中央服务器或工作站上安装 Ansible
-
在每台你想管理配置的机器上创建一个 Ansible 用户
-
在中央服务器或本地机器上创建相同的用户
-
在服务器上设置 Ansible 用户,以便它能够通过 SSH 连接到客户端且无需密码。
-
配置客户端机器上的
sudo,使得 Ansible 用户可以在没有密码的情况下执行sudo命令
在之前的章节中,我们讲解了如何创建用户和 SSH 密钥,但还没有讲解最后一点。假设你将 Ansible 用户命名为ansible,创建以下文件:
/etc/sudoers.d/ansible
在该文件中,放入以下内容:
ansible ALL=(ALL) NOPASSWD: ALL
接下来,我们需要确保该文件的所有者是root:
sudo chown root:root /etc/sudoers.d/ansible
最后,我们需要调整文件的权限:
sudo chmod 440 /etc/sudoers.d/ansible
现在可以开始测试了。在服务器上切换到ansible用户:
sudo su - ansible
然后,测试一下,通过 SSH 在远程机器上执行一个命令:
ssh 192.168.1.123 sudo ls /etc
这是如何工作的呢?你可能知道也可能不知道,如果你使用 SSH 执行单个命令,其实不一定需要建立一个持久连接。在这个例子中,我们首先切换到ansible用户。然后,我们连接到192.168.1.123(或者客户端的 IP 地址),并告诉它执行sudo ls /etc。用sudo执行ls命令可能看起来像个傻事,但这非常有用——它让你可以测试sudo是否能正常工作,而不会做出任何潜在危险的操作。列出目录的内容是最无害的操作之一。
这看起来可能像是一个繁琐的过程,但请确保你从系统管理员的角度思考——这些设置步骤是可以自动化的。以我为例,我有一个 Bash 脚本,会在每台服务器上运行,设置所需的用户、密钥和专门用于 Ansible 的 sudo 权限。每当我想将新服务器添加到 Ansible 时,只需要在该机器上运行一次脚本,从此以后,Ansible 将自动处理剩余部分。
正常情况下,命令应该执行并打印 /etc 目录的内容,而不会提示你输入密码。如果这不起作用,请确保你已经完成了所有推荐的步骤。每台机器上应该有一个 ansible 用户,该用户应具备无需密码的 sudo 权限,因为我们已经在 /etc/sudoers.d 中为该用户创建了一个文件。如果 SSH 部分失败,请检查 /var/log/auth.log 日志文件,里面会保存相关错误的线索。一旦你满足了这些要求,就可以开始使用 Ansible 自动化了!
让你的服务器按照你的意愿工作
作为服务器管理员,我们都是控制狂。没有什么比执行一个命令,看到每一台服务器都服从并执行它更让人兴奋了。既然我们已经设置好 Ansible,接下来正是要做这件事。我假设你现在已经有了一些要配置的机器,它们都已经配置好通过 SSH 与中央服务器进行通信。另外,正如我之前提到的,我强烈推荐你使用类似 Git 的工具来存储你的配置文件,但这部分并不是本节的必要内容。
设置清单文件并配置 Ansible 设置
首先,我们需要一个清单文件,这是一种特殊的文本文件,Ansible 需要从中读取信息以确定连接到哪些服务器。在以前的版本中,安装 ansible 包的过程中会为你提供一些默认的配置,位于 /etc/ansible 目录下。但在 Ubuntu 22.04 中,至少对于默认仓库中提供的 Ansible 版本,情况发生了变化。对于我们的目的来说,这没关系——反正我们是要创建一个空的配置文件。
首先,让我们创建一个目录,在“控制器”服务器上用于配置管理(即我们用来控制或“配置”其他节点的服务器):
sudo mkdir /etc/ansible
之后,我们可以创建一个空的清单文件:
sudo touch /etc/ansible/hosts
事实上,有一种方法可以避免创建清单文件,稍后我们将在本章中讨论这一点。
我们还应该确保只有 Ansible 用户帐户可以读取该文件。执行以下命令以更改文件的所有权(如果你使用的是不同的用户帐户,请将 ansible 替换为你选择的帐户名):
sudo chown ansible /etc/ansible/hosts
接下来,修改文件的权限,使得只有所有者可以查看或更改该文件:
sudo chmod 600 /etc/ansible/hosts
接下来,让我们编辑/etc/ansible/hosts文件并填充它。如果在安装 Ansible 时确实为你创建了默认的hosts文件,你可以简单地清空该文件,因为我们无论如何都会创建自己的文件。我们将在这个文件中填入我们希望管理的服务器的 IP 地址。如果原始的主机文件已经存在,欢迎你先备份它。现在,我们只包括了我们希望用 Ansible 控制的节点的 IP 地址,因此该文件最终会看起来像下面这样:
192.168.1.145
192.168.1.125
192.168.1.166
就是这样;它只是一个简单的 IP 地址列表。我敢打赌你原本以为会有一些包含各种语法要求的长配置吧?抱歉让你失望了。你需要做的就是将你想要管理的服务器的 IP 地址列表复制到这个文件中。如果你已经为你想配置的机器设置了 DNS 名称,你也可以使用它们:
myhost1.mydomain.com
myhost2.mydomain.com
myhost3.mydomain.com
由于 Ansible 既能理解 IP 地址,也能理解 DNS 名称,我们可以使用其中任意一种,或两者结合起来设置我们的库存文件。我们还可以在库存文件中将主机按不同角色进行划分,但这超出了本书的范围。如果你希望进一步深入了解 Ansible 的知识,我推荐学习关于 Ansible 角色的内容(有关更多信息,请参见相关视频部分)。
如果你决定不将库存文件存储在/etc/ansible/hosts,你必须告诉 Ansible 它的存放位置。Ansible 还有另一个重要的文件,那就是它的配置文件,位于/etc/ansible/ansible.cfg。在这个文件中,我们可以微调 Ansible,以便获得最佳性能。虽然我们不会详细讨论这个文件,但要知道,通过微调配置设置,你可以显著提高 Ansible 的性能,并且 Ansible 每次运行时都会读取配置文件中的设置。在我们的案例中,如果我们希望将库存文件存储在/etc/ansible/hosts以外的地方,我们需要在这个文件中添加以下两行(很可能,你需要创建这个文件,因为此时它可能还不存在):
[defaults]
inventory = /path/to/hosts
remote_user = jay
在第一个设置中,我们基本上是在告诉 Ansible 去哪里找到它的库存文件。我们还可以在ansible.cfg文件中添加更多的配置项来进一步配置它,但目前这些就是我们需要配置的内容。在第二行中,我们设置了一个默认用户,用于 Ansible 的剧本,这个用户必须在你希望使用 Ansible 管理的任何服务器上存在。
与库存文件类似,Ansible 还会检查本地目录中是否存在一个名为 ansible.cfg 的文件来获取其配置,因此你也可以将配置文件包含在 Git 仓库中,然后从仓库目录内执行 Ansible 命令。这是可行的,因为 Ansible 会检查当前工作目录中是否存在配置文件,并在找到该文件时使用它。不过,你可能需要小心将配置文件包含在 Git 仓库中。虽然它不像库存文件那样私密,但它可能包含敏感信息。因此,如果文件中包含了任何私密信息(例如加密密钥),你可能需要将文件保存在 /etc/ansible/ansible.cfg 目录,并在 Git 仓库之外管理。
现在我们可以测试一下 Ansible 是否已经正常工作。幸运的是,这也很简单。只需要执行以下命令:
ansible all -m ping
结果应该类似于以下内容:

图 15.2:测试 Ansible
根据你设置的服务器数量,你应该会看到输出出现一次或多次。你可能认为这个命令只做了一个简单的 ping 测试,但在 Ansible 中,ping 的含义与通常不同。它实际上是在尝试与服务器建立连接,以测试其可用性。如果失败了,双重检查一下主机是否可以通过 SSH 访问。这里的成功意味着 Ansible 能够通过 SSH 与主机通信。既然通信已经建立,我们就可以开始构建一些实际的配置了。
配置客户端服务器
Ansible 使用一种叫做 playbook 的工具来存储配置。Playbook 本质上是一个 YAML 格式的文件,包含 Ansible 可以理解的指令,这些指令会被解释为一组针对主机执行的任务。关于 YAML 的完整指南我们在这里不会深入讨论——你不需要精通这个格式,甚至不需要完全理解它就可以在 Ansible 中使用。随着使用的深入,这些内容会自然掌握。这里需要注意的是,YAML 仅仅是 Ansible 使用的格式,它并非 Ansible 特有的文件格式。Playbook 基本上就是一系列以 YAML 格式书写的指令集合,每个单独的指令称为一个 play。
你可以用一种类似于体育运动的比喻来理解这一点,比如足球。虽然我对足球一无所知,但我知道足球教练有一本包含他们希望球员执行的动作的战术手册,每个球员的动作就是一场比赛。这里的概念也是一样的。
让我们编写第一个剧本。在你的本地 Ansible 目录中创建一个名为 packages.yml 的文件。你可以将以下内容填入文件中(确保包含短横线):
---
- hosts: all
become: true
tasks:
- name: Install htop
ansible.builtin.apt:
name: htop
我们可以使用以下命令运行这个剧本:
ansible-playbook packages.yml
这将产生类似于以下内容的输出:

图 15.3:Ansible 运行示例
就这样,库存文件中的所有主机都会安装htop包。你安装哪个包其实并不重要,只要它在软件库中存在;我只是用htop作为一个简单的示例。但一旦你运行它,你应该能看到所做更改的概览。Ansible 会告诉你有多少项更新,多少任务失败,多少目标在运行剧本时无法访问。
让我们仔细看看这个示例剧本中的指令做了什么。开头的短横线是 YAML 格式的一部分,所以我们其实不需要深入讨论。空格在 YAML 格式中非常重要,因为你需要保持一致。
在我的示例中,我在每个标题下面插入了两个空格。标题以短横线开始:
- hosts: all
在这里,我们声明希望将命令应用于哪些主机。我在这里添加了all,这基本上是对库存文件中的每一台主机运行配置。在高级用法中,你实际上可以在 Ansible 中创建roles(角色),并将主机分配到不同的角色中,例如 Web 服务器、数据库服务器等等。然后,你可以只将配置应用于特定角色内的主机。我们在本章不打算深入讨论这个内容,但只需知道这是可行的。
下一行是become:
become: true
这一行基本上是 Ansible 用来描述sudo的术语。我们告诉 Ansible 使用sudo来执行命令,因为安装包需要root权限。接下来的剧本部分如下:
tasks:
这一行开始了下一部分,那里是我们放置各个任务的地方。接下来,我们为我们的新任务命名(Ansible 称任务为“play”):
- name: Install htop
使用name,我们为该剧本指定一个名称。这不是必需的,但你应该始终包含它。这样做的重要性在于,无论我们在这里输入什么,如果启用了日志记录,它都会出现在日志中,并且会在剧本运行时打印到终端。我们在这里应该写得具有描述性,因为这对于剧本执行失败时,我们需要在成百上千行的日志文件中找到它时,肯定会有所帮助。接下来,我们使用apt模块并告诉它安装一个包,这里以htop为例:
ansible.builtin.apt:
name: htop
我们使用ansible.builtin.apt模块仅仅是因为 Ubuntu 使用apt命令来管理包,但也有针对所有流行 Linux 发行版的模块。Ansible 对各个发行版的包管理器的支持实际上非常广泛。所有主要的发行版,如 Red Hat、Fedora、openSUSE、Arch Linux 和 Debian,都得到支持(而这些只是我在实验室中使用过的发行版)。如果你想对运行非 Ubuntu 发行版的服务器执行剧本,只需将ansible.builtin.apt调整为其他命令,如ansible.builtin.dnf。
由于 Ansible 允许你使用来自 Galaxy 的额外资源,Galaxy 是一个专门为 Ansible 提供额外功能的站点,因此我们使用 ansible.builtin.apt 而不仅仅是 apt,以明确表示我们使用的是内建模块,而不是来自外部资源的模块。
这个新的命名方案自从本书上一版发布以来才出现。旧版本的 playbooks 应该依然可以正常工作,但由于命名方案自那时起发生了变化,我们将使用新的命名风格继续。
当然,你可以通过简单地在现有 playbook 中添加更多的 play 来增加额外的软件包:
---
- hosts: all
become: true
tasks:
- name: Install htop
ansible.builtin.apt:
name: htop
- name: Install git
ansible.builtin.apt:
name: git
- name: Install vim-nox
ansible.builtin.apt:
name: vim-nox
然而,这并不是一个非常高效的方法。我将展示如何将多个相似的 play 合并到一个 play 中。当然,你不必这样做,但我相信你会同意,这种方法看起来更简洁:
---
- hosts: all
become: true
tasks:
- name: Install packages
ansible.builtin.apt:
name:
- git
- htop
- vim-nox
使用新格式时,我们只包含一个 play 来安装多个软件包。如果你有编程经验,这就像是 for 循环 的概念。对于我们列出的每个软件包,它都会执行 ansible.builtin.apt 模块。如果我们想添加额外的软件包,只需要在列表中添加新的软件包。非常简单。
我们还可以将文件复制到我们的主机上。考虑下面这个示例 playbook,我将它命名为 copy_files.yml:
---
- hosts: all
become: true
tasks:
- name: copy SSH motd
ansible.builtin.copy:
src: motd
dest: /etc/motd
然后,你可以通过以下命令运行这个 playbook:
ansible-playbook copy_files.yml
在相同目录下,创建一个名为 motd 的文件,并将任意文本放入其中。你输入到文件中的内容并不重要,但这个文件特别用作每次用户登录服务器时打印的消息。当你运行 playbook 时,它会将这个文件复制到你配置的服务器目标位置。由于我们创建了一个今天消息(motd),所以下次登录服务器时,我们应该看到新消息。
到现在为止,你可能已经意识到 Ansible 是多么有用。没错,我们只是安装了几个软件包并复制了一个文件。我们本来可以轻松地自己完成这些任务,而不需要 Ansible,但这仅仅是个开始。Ansible 让你能够自动化一切,我们必须从某个地方开始。你不仅可以让它安装软件包和复制文件,还可以用它启动服务、应用配置文件模板,等等——它会让你惊讶。事实上,你可以用它自动化设置一个 Web 服务器,一个用户的工作站……随你选择!
把它们整合在一起——自动化 Web 服务器部署
说到自动化设置 Web 服务器,为什么不直接做这个呢?这将是另一个简单的例子,但如果我们展示 Ansible 的更多功能,它将对你非常有帮助。我们将设置一个 playbook 来执行以下任务:
-
安装 Apache
-
启动
apache2服务 -
为新站点复制一个 HTML 文件
首先,让我们设置一个 playbook 来简单地安装 Apache。我把它命名为 apache.yml,但这个名字是随意的:
---
- hosts: all
become: true
tasks:
- name: Install Apache
ansible.builtin.apt:
name: apache2
没有什么惊讶的;我们此时已经安装了一个软件包。接下来,让我们添加一个指令来启动 apache2 服务:
---
- hosts: all
become: true
tasks:
- name: Install Apache
ansible.builtin.apt:
name: apache2
**- name: Start the apache2 services**
**ansible.builtin.service:**
**name: apache2**
**state: started**
到目前为止,语法应该是自解释的。Ansible 有一个 service 模块,可以用来启动主机上的服务。在这个例子中,我们启动 apache2 服务(虽然在 apache2 安装时服务已经自动启动,但这样做至少可以确保它已经启动)。你确实需要提前知道服务的名称,但你无需关注需要在后台使用哪个工具来启动服务。Ansible 已经知道如何在所有流行的发行版上启动服务,并为你处理后台的具体操作。
启动 apache2 的剧本可能显得有些多余,因为在 Ubuntu 服务器上安装的大多数软件包都会在安装完成后自动启动相关服务(apache2 也不例外)。但是在编写自动化代码时,清晰和明确地表达所期望的最终结果是非常重要的。尽管 apache2 在 Ansible 安装完包后会自动启动,但我们还是添加了一个 service 剧本,以明确它需要处于运行状态,这样任何查看它的人都会知道期望的结果是什么。此外,我们可以通过在代码的这一部分再加一行来确保 apache2 被设置为 enabled(注意那一行加粗的部分):
- name: Start the apache2 services
ansible.builtin.service:
name: apache2
state: started
**enabled: true**
我会留给你决定是否包含那一行额外的内容,但关键是当编写自动化脚本时,要尽可能清晰和直接,以避免对最终结果产生任何混淆。
好的,这很简单。让我们为 Apache 创建一个简单的网页,让它为我们提供服务。网页不需要太花哨,我们只是想看到它是否能正常工作。在与其他 Ansible 文件相同的工作目录中,创建一个名为 index.xhtml 的文件,并在其中写入以下内容:
<html>
<title>Ansible is awesome!</title>
<body>
<p>Ansible is amazing. With just a small text file, we automated the setup of a web server!</p>
</body>
</html>
如你所见,这个 HTML 文件相当简单,但对于我们的需求来说,它完全足够了。接下来,让我们在 Apache 的剧本中添加另一条指令:
---
- hosts: all
become: true
tasks:
- name: Install Apache
ansible.builtin.apt:
name: apache2
- name: Start the apache2 services
ansible.builtin.service:
name: apache2
state: started
**- name: Copy index.xhtml**
**ansible.builtin.copy:**
**src: index.xhtml**
**dest: /var/www/html/index.xhtml**
使用 copy 模块,我们可以将文件从本地 Ansible 目录复制到服务器。我们只需要提供源文件路径(src)和目标路径(dest)。
让我们继续执行新的剧本:
ansible-playbook apache.yml
这将产生如下所示的输出:

图 15.4:使用 Ansible 安装 Apache 并复制默认站点文件
几分钟内,你应该已经至少配置了一个由 Ansible 管理的 web 服务器。在真实的生产环境中,你只会在特定角色的服务器上运行这些指令,但角色超出了本章的讨论范围。从我们创建的简单剧本中,你应该能够看到这款强大软件的威力:

图 15.5:Ansible 提供的网页示例
在我们对 Ansible 配置管理的探索过程中,我们已经安装了软件包、启动了服务并复制了文件。诚然,这些操作不算多,但它们正是你练习基础操作所需的。Ansible 的文档包含了如何做各种操作的指南,你将能够利用它提供的各种模块来执行不同的任务。
为了更深入地探索 Ansible,我建议你考虑一下那些你日常做的、可以从自动化中获益的事情。像安装安全更新、创建用户账户、设置密码和启用服务自动启动等任务,都是很适合开始使用的自动化操作。
此外,你可能还想考虑为 ansible 用户添加一个简单的 cron 作业,每小时左右运行一次 playbook。我们在第七章《控制和管理进程》中介绍了 cron 作业。添加 cron 作业不会带来额外的资源开销,因为 Ansible 实际上不会做太多工作,除非你添加了新的命令。在更高级的使用中,你可能希望让 Ansible 从代码库中检出代码,并在配置发生变化时应用配置。这样,你只需要将更改提交到 Git 仓库,所有服务器将在下一次计划时间下载并运行该配置。我最喜欢 Ansible 的一点是,它很容易上手,但你会不断发现新的使用方式并从中受益。
使用 Ansible 的拉取方法
在上一节中我们设置的 Ansible 配置方法,如果我们有一系列特定的服务器需要管理,效果非常好。要添加新服务器,我们只需要在新主机上创建用户账户并配置 SSH,然后将其添加到清单文件中。如果我们要停用那台服务器,只需要将其从清单文件中删除。这种方法在静态环境下非常有效,静态环境中部署的服务器通常会保持一段时间。然而,在动态环境中,这种方法可能效果不佳。
动态环境在云计算中非常典型。在云计算中,通常会有一个或多个虚拟服务器为你的公司或用户提供服务。这些服务器可能随时上线或下线。在动态环境中,服务器会根据需要上线以处理负载,并且随着负载的减少,服务器也会自动下线。因此,你永远不知道服务器何时上线,而在这种环境下手动配置服务器是低效的。
因此,Ansible 的清单文件可能不适合动态基础设施。虽然确实有方法可以让 Ansible 的清单在这种环境中工作,实际上你可以用一个可执行脚本替换清单文件,这个脚本可以进行 API 调用并根据需要自定义你的基础设施。但是,这超出了本书的范围,而且还有一种更简单的方法。
如我们所见,Ansible 使用清单文件并连接到文件中列出的每一台服务器。然而,Ansible 也有 拉取模式,在这种模式下,除了有一个中央服务器连接到其他机器,每台处于拉取模式的服务器实际上会运行 Ansible 来操作它自身。依我看,这是使用 Ansible 的一种很棒的方式,但它似乎没有得到应有的关注。首先,我将解释它的工作原理,然后我们可以通过一个实际的例子来学习。
使用拉取模式时,你需要将 Ansible playbooks 存放在一个 Git 仓库中。这个仓库必须能够从你将管理的服务器访问。例如,如果你将 Git 仓库存放在 GitHub 上,你需要确保服务器能够从外部访问 GitHub。如果你在内部托管自己的 Git 服务器,你需要确保服务器能够通过防火墙或你可能设置的任何安全规则访问它。
拉取模式是通过 ansible-pull 命令实现的,该命令与 Ansible 一起捆绑。语法如下所示:
ansible-pull -U https://github.com/myusername/ansible.git
当然,你需要将网址替换为你实际的 Git 仓库的 HTTP 或 HTTPS 地址。基本上就是这样。ansible-pull 命令仅需要 -U 选项(即 URL 的简写)以及指向 Git 仓库的 URL。
为了使这个方法生效,你需要在仓库中有一个特殊命名的 playbook,local.yml。如果你没有在 Ansible 中声明特定的 playbook,它将期望在仓库的根目录中找到一个名为 local.yml 的 playbook。如果你选择为主 playbook 使用一个不同于 local.yml 的名字,那么你需要指定它:
ansible-pull -U https://github.com/myusername/ansible.git myplaybook.yml
在这个例子中,ansible-pull 命令会将位于指定 URL 的 Git 仓库缓存到本地,并运行仓库中的 myplaybook.yml 这个 playbook。你可能会发现 Ansible 会抱怨找不到清单文件,尽管这正是 ansible-pull 命令的整个目的。你可以忽略这个错误。这个问题可能会在未来的某个版本中得到修复,但截至本文写作时,如果它没有检测到清单文件,会打印一个警告。
理论部分讲解完毕后,让我们通过一个实际的例子来操作。如果你到目前为止都在跟着学习,我们在上一节中创建了一个自动化部署假设性 Web 服务器的 playbook。我们可以重用那段代码。不过,最佳实践是将文件命名为local.yml,因此你可以简单地将我们之前创建的apache.yml playbook 重命名为local.yml。我们需要对文件做一个小修改,我在这里已将其标出:
---
**- hosts: localhost**
become: true
tasks:
- name: Install Apache
ansible.builtin.apt: name=apache2
- name: Start the apache2 services
ansible.builtin.service:
name: apache2
state: started
- name: Copy index.xhtml
ansible.builtin.copy:
src: index.xhtml
dest: /var/www/html/index.xhtml
由于我们是在本地执行 playbook(没有 SSH),我们将 hosts: 行修改为指向 localhost,以告知 Ansible 我们希望在本地执行命令,而不是远程执行。现在,我们可以将这个 playbook 推送到我们的 Git 仓库,并直接从仓库 URL 执行它。
在执行任何 playbook 时,请仔细注意 hosts: 行。如果你使用的是 pull 方法,这一行需要从 hosts: all 改为 hosts: localhost,原因是我们是直接在本地主机上执行 playbook,而不是通过远程 SSH 连接来执行。如果你没有做这个修改,你会看到类似下面的错误:
ERROR! Specified hosts and/or --limit does not match any hosts
在运行 playbook 之前,你需要先切换到你的 Ansible 用户,因为该 playbook 需要以具有 sudo 权限的用户身份运行,因为它将执行系统级命令:
sudo su - ansible
然后,执行 playbook:
ansible-pull -U https://github.com/myusername/ansible.git
如果我们保持文件名为 apache.yml,我们只需要指定该文件名即可:
ansible-pull -U https://github.com/myusername/ansible.git apache.yml
请记住,由于ansible-pull直接在本地主机上执行 playbook,因此该 playbook 必须由具有sudo权限的用户来执行。如果sudo没有配置为允许该用户无需密码运行,playbook 执行时将会失败,因为它不是交互式的(它不会询问密码)。你也可以在ansible-pull命令前加上sudo并在执行前输入密码,但如果你设置为通过 cron 自动运行的话,这种方式就行不通了。
如果一切按计划进行,playbook 仓库应该已经缓存到服务器,并且指令会被执行。如果出现错误,Ansible 会相当善于提供逻辑性错误信息。只要用户有权限在服务器上执行特权命令,仓库中包含所需的文件,并且服务器能够访问并下载仓库(没有防火墙阻止),那么 playbook 就会正确运行。
在生产环境中实施拉取方法时,针对不同类型的服务器有几种方法可以实现。一种方法是为每种服务器类型单独创建一个 playbook。例如,你可能会有一个 Apache 的 playbook,以及专门用于数据库服务器、文件服务器、用户工作站等的 playbook。然后,根据你部署的服务器类型,在调用ansible-pull命令时指定相应的 playbook。如果你使用的是像云计算这样的服务,你实际上可以为每个服务器提供一个创建时要执行的脚本。你可以指示服务在每次创建新服务器时自动运行ansible-pull命令。例如,在 AWS 中,你可以使用一种叫做用户数据的功能,在服务器第一次启动时执行一个脚本。这可以避免你手动配置任何内容。为了使其工作,你需要首先包含一个命令来安装 Ansible 本身,接着第二个命令是ansible-pull命令,并附上仓库的 URL。这仅仅是两行代码,就可以完全自动化 Ansible 的安装和 playbook 的应用。提前考虑这些可能性,帮助你理解自动化如何为你带来诸多好处。
虽然正确有效地配置新服务器非常重要,但维护现有服务器同样重要。当你需要安装新软件包或应用安全更新时,最后你想做的事就是手动连接到每一台服务器,逐个更新它们。ansible-pull命令也能简化管理;你只需再次运行命令。每次运行ansible-pull时,它会下载仓库中的最新代码并运行。如果有任何更改,它们会被应用,已经应用的部分则会被跳过。例如,如果你在 playbook 中包含了安装apache2包的任务,Ansible 在第二次运行 playbook 时不会重新安装这个包,因为该需求已经满足,它会跳过这一任务。
ansible-pull的一个值得了解的技巧是-o选项。这个选项会确保只有在仓库中有实际更改时,仓库内的 playbook 才会被运行。如果你没有提交任何更改,Ansible 将跳过整个过程。如果你将ansible-pull命令设置为定期通过 cron 运行,比如每小时运行一次,这个功能非常有用。
如果你没有包含-o选项,Ansible 将每小时运行整个 playbook。这将无端消耗宝贵的 CPU 资源。使用-o选项时,Ansible 将仅使用必要的 CPU 资源来检查仓库是否有更改。仅当你实际提交了更改到仓库时,playbook 才会被运行。
本章节中对 Ansible 的介绍非常基础,我们仅使用了所需组件的核心部分。通过深入研究 Ansible,您将发现更高级的技术,以及一些更聪明的实施方式。例如自动化防火墙规则、安全补丁和用户密码,还可以让 Ansible 在成功配置服务器时发送电子邮件(甚至在失败时也可以)。基本上,您可以手动执行的任何任务,都可以用 Ansible 自动化。在未来,我建议以自动化的思维方式进行服务器管理。正如我之前多次提到的,如果您需要重复执行某项任务,就自动化它。Ansible 是自动化服务器管理的最佳途径之一。
总结
在本章中,我们介绍了使用 Ansible 进行配置管理的内容。Ansible 是一项越来越受欢迎的令人兴奋的技术。它为您提供了像 Chef 或 Puppet 等配置管理工具的全部功能,但没有所有的资源开销。它允许您几乎自动化所有事务。在我们的探索过程中,我们介绍了安装软件包、复制文件和启动服务的过程。在章节的末尾附近,我们通过一个示例介绍了如何使用 Ansible 来配置一个简单的 Web 服务器,甚至探讨了在动态环境中非常有用的拉取方法。这些概念构成了可以扩展到自动化更复杂部署的知识基础。
下一章会很有趣:我们将使用 KVM 搭建我们自己的虚拟化服务器。这是我最喜欢的话题之一,我相信你也会喜欢。到时见!
相关视频
-
Git Essentials (LearnLinuxTV):
linux.video/git-essentials -
使用 Ansible 入门 (LearnLinuxTV):
linux.video/learn-ansible -
使用 Ansible 配置工作站/笔记本电脑 (LearnLinuxTV):
linux.video/ansible-workstation -
Ansible Vault (LearnLinuxTV):
linux.video/ansible-vault -
Ansible Pull (LearnLinuxTV):
linux.video/ansible-pull
进一步阅读
-
Ansible 文档:
learnlinux.link/ansible-docs -
Ansible 角色文档:
learnlinux.link/reuse-roles -
Ansible 配置文件文档文章:
learnlinux.link/a-config -
ansible-pull文档:learnlinux.link/a-pull -
如何使用 Ansible 管理您的工作站配置:
learnlinux.link/a-ws -
Git 基础:
learnlinux.link/git-book -
设置 Git (GitHub):
learnlinux.link/setup-git
加入我们社区的 Discord
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十六章:虚拟化
在过去几十年里,信息技术领域发生了许多重要的进展,几项技术的出现真正颠覆了整个技术行业。相信很少有人会争辩,互联网无疑是迄今为止最具革命性的技术,但另一项在 IT 领域产生范式转变的技术是虚拟化。这个概念改变了我们维护数据中心的方式,使我们能够将工作负载划分为多个从单台服务器运行的小型虚拟机。这使我们能够更加充分地利用硬件。由于 Ubuntu 内置了 Linux 内核的最新进展,因此虚拟化支持实际上已经集成在其中。只需安装一些软件包,便能让我们与虚拟化功能进行交互,并在 Ubuntu 服务器上创建虚拟机,无需昂贵的许可协议或支持合同。在本章中,我将引导你设置基于 Ubuntu 的虚拟化解决方案。在这个过程中,我将介绍以下内容:
-
前提条件和考虑事项
-
设置虚拟机服务器
-
创建虚拟机
-
桥接虚拟机网络
-
通过克隆简化虚拟机的创建
-
通过命令行管理虚拟机
为了开始,我们需要一台服务器来完成这项任务,我们首先会讨论一些在设置服务器时需要考虑的事项。
前提条件和考虑事项
我相信你们中的许多人已经使用过虚拟化解决方案。事实上,我敢打赌,很多读者在使用本书时,可能已经在像 VirtualBox、Parallels、VMware 等解决方案中运行着虚拟机(VM)。这些应用程序及类似的软件非常适合在台式机或笔记本电脑上测试 Ubuntu 或其他操作系统。在本节中,我们将设置一台虚拟机服务器,它可以作为一个集中式服务器,用来运行虚拟机。
这比你想象的要容易——Ubuntu 内置了虚拟化功能。它以一对动态组合的形式存在,包括基于内核的虚拟机(KVM)和快速模拟器(QEMU),两者共同形成一个虚拟化套件,使 Ubuntu(以及 Linux 一般)能够运行虚拟机,而无需第三方解决方案。KVM 是内置于 Linux 内核中的功能,它在后台执行魔法,处理内核中需要的低级指令,帮助将任务分隔到物理主机和来宾虚拟机之间。QEMU 同样重要,它模拟了通常出现在物理服务器中的硬件组件。KVM 和 QEMU 的结合组成了虚拟化解决方案,可以在 Ubuntu 服务器上启用,将其转变为虚拟机的宿主。
公平地说,你可以在 Ubuntu Server 上设置类似 VirtualBox 的虚拟化方案来完成相同的任务,最终得到一个集中式的虚拟化服务器。这完全是可行的,运行 VirtualBox 的方式没有问题,很多人就是这么做的。但通过使用内建的系统,还是有一些改进的,KVM 提供了一个非常快速的接口,通过它可以与 Linux 内核进行交互,从而以接近本机的速度运行虚拟机,具体速度取决于你的使用场景。QEMU/KVM(以下简称 KVM)几乎是最接近本机的虚拟化方式了。
我敢打赌你已经迫不及待地想开始了,但在我们深入之前,有几件事需要考虑。首先,在这本书中我带你完成的所有活动中,从硬件角度来看,设置我们自己的虚拟化解决方案将是最昂贵的。你计划运行的虚拟机(VM)越多,服务器需要的资源就越多(特别是内存)。幸运的是,现在大多数电脑都至少配备了 8 GB 的内存,16 GB 或更多内存则相当常见。使用大多数现代电脑,你应该能够在不产生太大影响的情况下运行虚拟机。根据你使用的机器类型,CPU 和内存可能会成为瓶颈,尤其是对于老旧硬件来说。
对于本章的目的,建议你拥有一台能够支持 VM 扩展的处理器的 PC 或服务器。现在大多数电脑的 CPU 都提供这个功能,尽管有些可能不支持。为了确认,你可以在打算托管 KVM 虚拟机的机器上运行以下命令,以找出你的 CPU 是否支持虚拟化扩展:
egrep -c '(vmx|svm)' /proc/cpuinfo
结果为 1 或更高表示你的 CPU 支持虚拟化扩展,结果为 0 表示不支持:

图 16.1:检查 CPU 是否支持虚拟化
即使你的 CPU 支持虚拟化扩展,通常情况下,今天大多数消费者 PC 甚至一些服务器都默认禁用了这些扩展。要启用这些扩展,你可能需要进入计算机的 BIOS 设置界面并启用相关选项。根据你的 CPU 和芯片组,这个选项可能会被命名为类似“虚拟化支持”的东西,或者使用更技术化的名称,如 VT-x、AMD-V 或其他术语。不幸的是,我无法为你提供如何启用虚拟化扩展的详细步骤,因为每台机器的操作可能有所不同。如果不确定,请查阅硬件的文档。
最后一个提示:我相信很多人都在使用 VirtualBox,因为它似乎是测试 Linux 发行版的一个非常流行的解决方案(而且确实如此,它非常棒!)。然而,你不能在同一台机器上同时运行 VirtualBox 和 KVM 虚拟机。你当然可以在同一台机器上安装这两种解决方案,但你不能同时启动一个 VirtualBox 虚拟机,再试图启动一个 KVM 虚拟机。你的 CPU 的虚拟化扩展一次只能与一个解决方案一起工作。
另一个需要考虑的问题是服务器可用空间的大小,因为虚拟机可能会占用相当多的空间。KVM 虚拟机镜像的默认目录是/var/lib/libvirt/images。如果你的 /var 目录属于 root 文件系统的一部分,你可能没有很多空间可以使用。有一个技巧是,你可以将一个外部存储卷挂载到这个目录,这样你就可以把虚拟机磁盘镜像存储在另一个卷中。或者你可以创建一个符号链接,将这个目录指向其他位置。我们在第五章《管理文件和目录》中讨论过符号链接。选择权在你。如果你的 root 文件系统至少有 10 GB 可用空间,那么你应该能够创建至少一个虚拟机,而无需配置存储。我认为假设每个虚拟机至少需要 10 GB 硬盘空间是一个合理的估计。
设置虚拟机服务器
在所有讨论完成后,我们开始设置虚拟化服务器的过程。即使 KVM 是内建在 Linux 内核中的,我们仍然需要安装一些软件包,以便正确地与其进行接口。具体来说,我们需要安装一些 libvirt 软件包以及 QEMU 本身。libvirt 本身为我们提供了管理虚拟化平台的权限,它为我们提供了一组管理虚拟机的有用工具。
这些软件包将需要一些依赖项,因此安装过程可能需要几分钟:
sudo apt install bridge-utils libvirt-clients libvirt-daemon-system qemu-system-x86
现在你将在服务器上启动一个额外的服务,libvirtd。一旦你完成安装 KVM 的软件包,这个服务就会启动并启用。你可以随时查看它,自己确认一下:
systemctl status libvirtd
你应该能看到服务状态的信息,类似于以下内容:

图 16.2:安装 KVM 相关软件包后检查 libvirtd 单元的状态
让我们暂时停止这个服务,因为我们还需要做一些额外的配置:
sudo systemctl stop libvirtd
接下来,我们需要确保服务器上有两个必需的组,kvm 和 libvirt。我们安装的软件包很可能已经在服务器上添加了这两个组,所以可以随时检查一下 /etc/group 文件,看看它们是否已经存在。如果没有,你可以使用 groupadd 命令来创建它们:
sudo groupadd kvm
sudo groupadd libvirt
我们的主要用户账户应该是这两个组的成员。如果你的用户尚未是这两个组的成员,可以将你的用户添加到所需的组中(将用户名 jay 替换为你的用户名):
sudo usermod -aG kvm jay
sudo usermod -aG libvirt jay
此时,你最好注销并重新登录一次,以确保你的用户组变更已生效。
为了确保我们能够正确管理虚拟化,我们应当确保 kvm 组的用户有权访问 /var/lib/libvirt/images 目录,以便他们能够访问存储在该目录中的数据。首先,我们将 kvm 组应用到这个文件夹:
sudo chown :kvm /var/lib/libvirt/images
然后,我们将设置 /var/lib/libvirt/images 的权限,使得 kvm 组中的任何人都可以修改其中的内容:
sudo chmod g+rw /var/lib/libvirt/images
在安装了初始包并设置了权限后,我们现在可以启动 libvirtd 服务:
sudo systemctl start libvirtd
接下来,检查服务的状态,以确保没有错误:
sudo systemctl status libvirtd
现在我们已经配置好服务器,我们可以设置工作站,使其能够连接到服务器并管理我们已设置的虚拟化实现。我们将安装一个工具,提供一个图形用户界面(GUI),通过该界面我们可以执行与虚拟机相关的管理任务。我们将使用的工具叫做虚拟机管理器,简称 virt-manager。这个工具会安装在 Linux 工作站上,因此你需要在运行桌面版 Linux 的笔记本或台式机上安装它。如果你有一台运行 Debian 或 Ubuntu 的电脑,可以使用以下命令来安装所需的包:
sudo apt install ssh-askpass virt-manager
如果你使用的是除 Ubuntu、Debian 或基于它们的 Linux 发行版之外的其他 Linux 发行版,你可能需要查阅该发行版的文档,以便安装 virt-manager。如果你的工作站根本没有运行 Linux,我们稍后将在本章的 通过命令行管理虚拟机 部分讨论一套可以用于管理虚拟机的命令行工具。如果这些方法都不奏效,你可以在运行 Linux 的虚拟机中安装这个工具。
接下来,在你的管理机器上打开 virt-manager。它应该位于桌面环境的 应用程序 菜单中,通常在 系统工具 部分下的 虚拟机管理器 中。如果你找不到它,可以直接在终端中运行 virt-manager。第一次启动时,你可能会看到以下错误:

图 16.3:第一次启动 virt-manager 时可能出现的错误
如果你看到这个错误,直接忽略它,不必担心。默认情况下,virt-manager 会尝试连接到本地计算机上运行的 libvirtd 实例。除非你本地也在运行 KVM 虚拟机并且已经设置好了,否则这个尝试会失败。但这对我们没有影响,因为我们将使用 virt-manager 来管理服务器上的虚拟机。
一旦你打开了 virt-manager,你将看到主窗口,其外观与下图类似:

图 16.4:virt-manager 应用程序
virt-manager 工具特别有用,因为它可以帮助我们管理远程和本地的 KVM 服务器。通过这个工具,你可以创建与任何 KVM 服务器的连接,包括一个或多个外部服务器,或者如果你在笔记本电脑或台式机上运行 KVM,则可以连接到 localhost。要创建一个新的连接,点击 文件,然后选择 添加连接。此时会出现一个新界面,在这里我们可以填写希望连接的 KVM 服务器的详细信息:

图 16.5:向 virt-manager 添加新连接
在 添加连接 窗口中,输入连接的详细信息。在截图中,你可以看到我首先勾选了 通过 SSH 连接到远程主机,这将选择 SSH 作为我的连接方式,jay 作为我的 用户名,并且我在 主机名 字段中输入了我的 KVM 服务器的 IP 地址(172.16.250.19)。请根据你的 KVM 服务器填写具体的连接信息。请记住,为了使其正常工作,这里输入的用户名需要能够通过 SSH 访问服务器,并且必须具有访问虚拟化管理程序的权限(也就是之前加入 kvm 和 libvirtd 组的用户),并且服务器上必须运行 libvirtd 服务。如果满足所有这些要求,点击 连接 后,你将成功设置与 KVM 服务器的连接。你可能会看到一个弹出对话框,询问是否继续连接(yes/no)。如果是,请键入 yes 并按 Enter 键。
无论哪种情况,你都会被提示输入 KVM 服务器的密码;请输入密码并按 Enter 键。现在你应该能够在 virt-manager 应用程序中看到一个连接列表。在下面的截图中,你可以看到我添加的连接,它是列表中的第二个连接。第一个连接是 localhost,因为我除了在远程服务器上安装 KVM 外,还在本地笔记本上运行 KVM:

图 16.6:添加新连接后的 virt-manager 界面
我们快要测试 KVM 服务器了。但首先,我们需要为 ISO 镜像创建一个存储组,用于在虚拟机上安装操作系统。当我们创建虚拟机时,可以将 ISO 镜像从我们的 ISO 存储组附加到虚拟机上,从而允许它安装操作系统。
要创建这个存储组,如果 virt-manager 尚未打开,请先打开它。右键点击服务器连接的条目,然后点击 详情。你将看到一个新窗口,显示有关 KVM 服务器的详细信息。点击 存储 标签:

图 16.7:设置新存储池时的第一个界面
起初,你只会看到我们之前编辑的默认连接。现在,我们可以添加 ISO 存储池。点击左下角的加号符号来创建新的存储池:

图 16.8:virt-manager 应用程序的存储选项卡
在名称字段中,输入ISO。你实际上可以随意命名,但考虑到它将存储 ISO 镜像,命名为 ISO 比较合适。在目标路径字段中,设置为/var/lib/libvirt/images/ISO,除非你的文件系统中有其他目录用于虚拟机存储。点击完成以最终确定我们的更改。我们还应该更新该目录的权限,使其归正确的用户所有,并且kvm组的成员能够对其进行读写操作:
sudo chown root:kvm /var/lib/libvirt/images/ISO
sudo chmod g+rw /var/lib/libvirt/images/ISO
恭喜!你现在已经拥有了一个完全配置的 KVM 服务器,用于创建和管理虚拟机。我们的服务器有地方存储虚拟机以及 ISO 镜像。你也应该能够像我们在本节中做的那样,使用virt-manager连接到这个实例。接下来,我将带你完成设置第一个虚拟机的过程。在开始之前,我建议你将一些 ISO 镜像复制到你的 KVM 服务器上。你使用哪个 ISO 镜像并不重要——任何操作系统镜像都可以。如果有疑问,你可以像我们在第一章《部署 Ubuntu 服务器》中做的那样,重新下载 Ubuntu Server 22.04,我们在设置初始安装时也用的就是这个镜像。
在选择并下载 ISO 文件后,通过scp或rsync将其复制到服务器,并将其移入/var/lib/libvirt/images/ISO目录。这两个工具在第十二章《共享和传输文件》中都有介绍。文件复制完成后,你现在应该拥有所需的所有文件。
创建虚拟机
现在,到了测试你新的虚拟机服务器并创建虚拟机的时候了。此时,我假设以下情况成立:
-
你能够通过
virt-manager连接到你的 KVM 服务器 -
你已经将一个或多个 ISO 镜像复制到服务器
-
你的存储目录至少有 10GB 可用空间
-
KVM 服务器拥有足够的空闲内存,可以分配给你打算创建的虚拟机
现在,打开virt-manager,让我们开始吧!
在virt-manager中,右键点击你的服务器连接,然后点击新建,开始创建新的虚拟机。默认选项会选择本地安装介质(ISO 镜像或 CDROM);保持此选择不变,然后点击前进:

图 16.9:设置新虚拟机时的第一个屏幕
在下一屏幕中,点击浏览打开另一个窗口,在该窗口中选择你已下载的 ISO 镜像:

图 16.10:创建新虚拟机并设置虚拟机选项
如果你点击你的 ISO 存储池,你应该能看到你已下载的 ISO 镜像列表:

图 16.11:在创建虚拟机时选择 ISO 镜像
如果这里没有显示任何 ISO 镜像,您可能需要点击刷新图标。在我的示例服务器中,我添加了一个 Ubuntu Server 22.04 的安装镜像,您可以在列表中看到它。同样,您可以使用任何您喜欢的操作系统。点击 ISO 镜像名称以将其高亮显示,然后点击选择卷来确认选择。接着,点击前进以继续到下一个屏幕。
接下来,系统会要求您为虚拟机分配 RAM 和 CPU 资源:

图 16.12:为新虚拟机调整 RAM 和 CPU 数量
对于大多数没有 GUI 的 Linux 发行版,2,048 MB 足够了(除非您的工作负载需要更多)。对于轻量级工作负载,一个 CPU 核心就足够了,但如果您打算运行的应用程序文档推荐更多的资源,可以考虑增加更多。您在这里选择的资源将取决于主机上可用的资源。完成资源分配后,点击前进。
接下来,您将为虚拟机的虚拟硬盘分配空闲磁盘空间:

图 16.13:为新虚拟机分配存储资源
设置磁盘镜像大小,根据虚拟机的用途,选择合适的空间。完成后,点击前进。
最后,您将为虚拟机命名:

图 16.14:为新虚拟机命名
这不会是虚拟机的主机名;它只是您在virt-manager中看到虚拟机时显示的名称。当您点击完成时,虚拟机会启动,并自动引导至您在过程初期附加到虚拟机的安装 ISO。然后,该操作系统的安装过程将开始:

图 16.15:在虚拟机中安装 Ubuntu Server
当您点击虚拟机窗口时,它将抢占您的键盘和鼠标,并将其专用于该窗口。按下Ctrl和Alt键同时释放控制权,并重新获得对键盘和鼠标的完全控制。
很遗憾,我无法为您详细讲解虚拟机操作系统的安装过程,因为可能有数百种操作系统供您选择。如果您正在安装另一个 Ubuntu Server 实例,可以参考第一章,部署 Ubuntu Server,我们在其中已经讲解了这一过程。这个过程在虚拟机中是一样的。从这里开始,您应该能够创建尽可能多的虚拟机,并为其分配所需的资源。
接下来,我们将探讨一些与虚拟机网络相关的概念。
桥接虚拟机网络
除非配置桥接网络,否则你的 KVM 虚拟机将使用它们自己的网络。这意味着你的虚拟机将获得它们自己网络中的 IP 地址,而不是你的网络。默认情况下,每台机器将属于192.168.122.0/24网络,IP 地址范围从192.168.122.2到192.168.122.254。如果你在个人笔记本或台式机上使用 KVM 虚拟机,这种行为可能已经足够。如果你从虚拟机运行所在的相同计算机连接,你将能够通过虚拟机的 IP 地址进行 SSH 连接。如果这满足了你的使用需求,则无需进行进一步的配置。
桥接网络允许虚拟机从网络中的 DHCP 服务器获取 IP 地址,而不是使用内部 IP 地址,这将使你能够从网络中的任何其他计算机与虚拟机进行通信。如果你正在为小型办公室或组织设置一个中央虚拟机服务器来支持基础设施,这种使用方式更为优选,因为你的 DHCP 服务器可以成为你组织中所有 IP 地址的单一真实来源。通过在虚拟机服务器上设置桥接网络,每个虚拟机将被视为任何其他网络设备。你只需要一个有线网络接口,因为无线网卡通常不支持桥接网络。
最后一点非常重要。一些网络卡不支持桥接,如果你的网络卡不支持,你将无法在虚拟机服务器上使用桥接,除非更换网络卡。在继续之前,你可能需要查看你设备厂商的文档,以确保你的网络卡支持桥接。根据我的经验,大多数由 Intel 制造的有线网卡支持桥接,而大多数无线网卡则不支持。确保在更改 Netplan 配置文件之前备份该文件,以便在桥接无法使用时可以恢复到原始版本。
要设置桥接网络,我们需要在服务器上创建一个新的网络接口(即用于托管虚拟机的接口)。使用sudo在文本编辑器中打开/etc/netplan/00-installer-config.yaml文件。我们在第十章《连接到网络》中已经讨论过这个文件,因此在这里我不会过多讲解。基本上,这个文件包含了我们每个网络接口的配置,接下来我们将在这里添加新的桥接接口。
确保备份原始的 Netplan 配置文件,然后用以下内容替换文件内容。如果你的有线接口名称不同,请确保将文件中的enp0s3(接口名称)替换为你的实际接口名称。文件中有两个该名称的出现。
如果你正在阅读本书的数字版本,强烈建议你不要复制粘贴以下代码,而是手动输入或从书籍代码包的 GitHub URL 复制。原因是 YAML 格式对空格非常挑剔,如果你混用了空格和制表符,文件可能无法正常工作。当 Netplan 出现错误时,通常很难确切找出它抱怨的是什么,但空格问题往往是罪魁祸首,即使错误输出没有提示你注意到这一点。
在配置此文件时请慢慢来。如果你犯了一个小错误,服务器重启后你很可能无法访问网络:
network:
ethernets:
enp0s3:
dhcp4: false
bridges:
br0:
interfaces: [enp0s3]
dhcp4: true
parameters:
stp: false
forward-delay: 0
在你进行更改后,你可以立即应用新的设置,或者简单地重启服务器。如果你有显示器和键盘连接到服务器,以下命令是激活新配置的最简单方式:
sudo netplan apply
如果你通过 SSH 连接到服务器,重启网络配置可能会导致服务器无法访问,因为一旦网络停止,SSH 连接很可能会断开。这将中断连接并防止网络重新启动。如果你知道如何使用screen或tmux,你可以在其中运行restart命令;否则,重启服务器可能会更简单。
网络重启或服务器重启后,检查你是否仍能访问网络资源,例如能否 ping 网站并访问其他网络节点。如果可以,说明一切正常。如果遇到任何问题,确保你正确编辑了 Netplan 配置文件。
现在,当你运行ip addr show时,你应该能看到一个额外的网络接口列出。该接口将被命名为br0。br0接口应该从你的 DHCP 服务器获取 IP 地址,代替原来的enp0s3接口(或你系统上相应的名称)。从此以后,你将能够使用br0为虚拟机提供网络,而不是内部网络。内部的 KVM 网络仍然可用,但你可以在创建新虚拟机时选择使用br0。
如果你已经创建了一个虚拟机,并希望将其切换为使用桥接网络,可以按照以下步骤进行转换:
-
首先,打开
virt-manager并双击你的虚拟机。一个新窗口将打开,显示虚拟机的图形控制台。 -
顶部第二个按钮(呈蓝色圆形)将打开虚拟硬件详情标签,你可以在该标签下配置虚拟机的多种设置,例如 CPU 数量、RAM 大小、启动设备顺序等。
-
在屏幕左侧的选项中,会有一个显示NIC并显示虚拟机网络卡 MAC 地址部分的选项。如果你点击它,你可以通过在列表中选择它来配置虚拟机使用你新的桥接网络。
-
最后,点击应用。你可能需要重启虚拟机以使更改生效:

图 16.16:配置虚拟机使用桥接 br0
在创建全新虚拟机时,你还需要执行一个额外步骤,以配置虚拟机使用桥接网络。在流程的最后一步,你需要为虚拟机设置名称(如图 16.14所示),你会在窗口底部看到高级选项。展开这些选项,你就可以设置网络名称。在该部分的下拉菜单中选择指定共享设备名称,并将桥接名称设置为br0。现在,你可以点击完成,像之前一样完成虚拟机的设置,启动时它将使用你的桥接网络。
从此以后,你不仅会拥有一个完全配置好的 KVM 服务器或实例,还将拥有一个可以作为你网络中完全合格成员的解决方案。你的虚拟机将能够从 DHCP 服务器获取 IP 地址,并能直接与其他网络节点通信。如果你的 KVM 服务器非常强大,你甚至可以将其他网络设备整合进虚拟机中以节省空间,这基本上就是虚拟化的目的所在。
在接下来的部分,我们将通过讨论创建模板的过程来简化这一流程,模板可以作为设置新虚拟机时的预配置起始点。
简化虚拟机创建过程,通过克隆
现在我们有了一个 KVM 服务器,能够迅速启动一群虚拟机来执行我们的任务,我们可以尝试找到一些巧妙的方法来自动化设置新虚拟机的工作量。每次创建新虚拟机时,我们都需要再次经历操作系统的完整安装过程。虽然这个过程不复杂,但我们完全可以将其简化。
大多数主流虚拟化解决方案都包括创建虚拟机模板的功能。通过模板,我们可以一次性创建并完全配置好虚拟机。然后,我们可以将其转换为模板,并将其用作所有未来虚拟机的基础,这些虚拟机将使用相同的操作系统。这将节省大量时间。你可能还记得在第一章中安装 Ubuntu Server 时需要浏览的一些界面。试想一下,之后就不再需要重复这个过程了(或者至少不需要那么频繁地重复)。
不幸的是,尽管 QEMU/KVM 非常强大,但它没有模板功能。这一显著的功能缺失是一个不小的障碍,但幸运的是,我们这些 Linux 管理员非常聪明,完全可以绕过这个问题,创造出一个基本等同于模板的解决方案。
举个例子,看看以下截图:

图 16.17:带有列出模板的虚拟机管理器
在截图中,你可以看到两个虚拟机,ubuntu22.04和ubuntu-server-template。尽管它的名字会让你认为它是一个模板,但后者根本不是模板,它只是一个虚拟机。它与其他虚拟机没有什么不同,唯一不同的是它没有运行。但它确实是一个巧妙的变通方法(如果我说的没有错的话)。如果我想创建一个新虚拟机,我只需右击它,然后点击克隆。
以下窗口将会出现:

图 16.18:克隆虚拟机
当我在这个窗口点击克隆时,在给新虚拟机命名后,我就制作了它的副本作为我的新虚拟机。它将使用原始虚拟机作为基础,而我已经配置好了它。由于 Ubuntu Server 已经安装在“模板”上,我就不需要再做这些工作了。
如果你为生产环境创建虚拟机模板,强烈建议你了解cloud-init,它可以帮助将 Ubuntu 安装程序进行通用化,包括重新生成其 SSH 主机密钥和机器 ID。cloud-init 超出了本书的范围,但如果你想深入研究虚拟机模板生成的话,它绝对是必不可少的。
想一想你在设置新 Ubuntu Server 实例后手动执行的任务。通过将基本虚拟机作为模板使用,你可以将你经常实施的任何调整或定制直接包含到该虚拟机中,因此每次克隆时,所有这些工作都会自动完成。只要你保持基本虚拟机的更新,你就可以从中生成尽可能多的虚拟机,并且能够在最小的配置步骤下完成。
在本章中,我们使用了很多virt-manager来定制虚拟机,虽然它是一个非常好的工具,但我们也应该了解如何在没有它的情况下管理我们的基础设施。在下一节中,我们将看看一些通过命令行管理虚拟机的示例。
通过命令行管理虚拟机
在本章中,我向你展示了如何使用virt-manager管理虚拟机。如果你有一台运行 Linux 操作系统的带图形界面的辅助计算机,这非常有用。但是,如果没有这样的计算机,并且你希望执行一些简单的任务,比如重启虚拟机或检查哪些虚拟机正在服务器上运行,应该怎么办呢?
在虚拟机服务器本身,你可以访问virsh命令集,这将允许你即使没有图形界面也能管理虚拟机。要使用这些命令,只需通过 SSH 连接到存储虚拟机的机器。接下来是一些简单的示例,帮助你入门。第一个示例如下:
virsh list
这个命令将返回如下所示的输出:

图 16.19:使用 virsh list 命令显示正在运行的虚拟机
通过一个命令,我们就能列出服务器上运行的虚拟机。在示例截图中,你可以看到我有一台虚拟机在运行。如果你也想查看未运行的虚拟机实例,只需在命令中添加 --all 选项。
我们可以通过以下任何命令来管理虚拟机的状态:
-
virsh start vm-name -
virsh shutdown vm-name -
virsh suspend vm-name -
virsh resume vm-name -
virsh destroy vm-name -
virsh undefine vm-name
virsh 的命令语法非常简单。通过查看之前的命令列表,你应该能准确理解它们的作用。virsh 命令允许我们进行如 start、shutdown、suspend 和 resume 等操作来管理虚拟机。virsh destroy 命令可能会造成破坏性影响,因为我们会在需要突然停止虚拟机时使用它。其效果本质上就像是拔掉物理服务器的电源线;它会立即停止虚拟机实例。只有在处理无法响应的虚拟机时,才应运行该命令。最后,virsh undefine 命令会删除虚拟机,但你还需要使用 rm 命令删除任何关联的磁盘文件。磁盘文件的默认目录是 /var/lib/libvirt/images,因此你可以查看该目录,查找属于你已删除虚拟机的磁盘文件(它们的文件名与虚拟机相同)。
然而,virsh 不仅仅能做这些。实际上,我们还可以使用 virsh 一套命令来创建虚拟机。如果你不使用 Linux 作为工作站操作系统,或者由于某种原因无法使用 virt-manager,学习如何创建虚拟机是一个不错的主意。不过,手动创建虚拟机磁盘镜像和配置超出了本章的范围。主要目标是让你熟悉如何通过 virsh 管理虚拟机,这些简单的基础知识将帮助你进一步拓展学习。
总结
在本章中,我们回顾了虚拟化,特别是使用 QEMU/KVM。我介绍了 KVM 的安装和配置过程,使我们的虚拟化服务器能够顺利运行。我们还介绍了如何创建桥接网络,以便让虚拟机能被网络中的其他设备访问,并创建了我们的第一台虚拟机。此外,虽然 QEMU/KVM 没有自己的模板解决方案,我们通过自定义的方式解决了这一问题,创建了属于我们自己的方案。
在 第十七章,运行容器 中,我们将讨论容器化技术,涵盖 Docker 和 LXD。敬请期待!
相关视频
- cloud-init 指南(LearnLinuxTV):
linux.video/cloud-init
进一步阅读
-
Ubuntu
virsh文档:learnlinux.link/u-virsh -
掌握 KVM 虚拟化,由 Vedran Dakic 等人编著(Packt 出版):
learnlinux.link/kvm-book
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者及其他读者进行讨论:

第十七章:运行容器
信息技术行业总是让我感到惊讶。当虚拟化的概念出现时,它彻底改变了数据中心。虚拟化使我们能够在一台服务器上运行许多小型虚拟机(VMs),有效地将我们的服务器机架中的设备进行整合。就在我们以为这已经是极限时,容器化的概念掀起了 IT 界的风暴,允许我们构建便捷的、可移植的软件实例,这不仅改善了我们部署应用程序的方式,还改变了我们开发应用程序的方式。在这一章中,我们将探索令人兴奋的容器化世界。这个探索将包括:
-
什么是容器化?
-
理解 Docker 和 LXD 之间的区别
-
安装 Docker
-
管理 Docker 容器
-
使用 Dockerfile 自动创建 Docker 镜像
-
管理 LXD 容器
首先,让我们探索容器化是什么,它与虚拟化有何不同,以及如何实施这项技术的一些注意事项。
什么是容器化?
在上一章中,我们介绍了虚拟化。虚拟化允许我们在一台物理硬件上运行多个虚拟服务器。我们将 CPU、RAM 和磁盘空间分配给这些虚拟机,它们的运行方式就像是实际的服务器。实际上,从所有实用角度来看,虚拟机就是一台真实的服务器。
然而,虚拟机也有一些弱点。或许最明显的问题是,您分配给虚拟机的某些资源可能会被浪费。例如,假设您为虚拟机分配了 512 MB 的 RAM。如果该应用程序很少使用超过 100 MB 的内存,那么意味着大多数时候,剩余的 412 MB 内存可以用来做其他事情,但它却处于空闲状态。CPU 使用率也可以说是类似的情况。如今,虚拟机解决方案确实有共享未使用资源的方法,但从根本上讲,资源效率是该平台的一个天然弱点。
与虚拟机不同,容器并不是真正的服务器。至少,从硬件角度来说,它们并不像你通常认为的那样。虚拟机通常有一个或多个虚拟化的 CPU,而容器与宿主共享 CPU。虚拟机还有自己的内核,而容器则共享宿主的内核。尽管如此,容器仍然是隔离的。就像虚拟机无法访问宿主的文件系统一样,容器也不能访问(除非你明确设置允许访问)。
那么,什么是容器呢?最好将容器看作是一个文件系统,而不是虚拟机。容器本身包含一个与其所基于的发行版匹配的文件结构。例如,一个基于 Ubuntu Server 的容器,将具有与在虚拟机或物理硬件上安装的真正 Ubuntu Server 相同的文件系统布局。想象一下,将所有 Ubuntu 安装的文件和文件夹复制过来,放入一个单独的隔离目录中,然后执行文件系统的二进制内容作为一个程序,而没有实际运行操作系统。
公平地说,这种描述是对容器在 Ubuntu 服务器上实际运行方式的过度简化,因为该技术利用 Linux 内核的功能来隔离容器的各个组件与系统其他部分。然而,关于这些技术的详细讨论超出了本书的范围。但理解容器内存在这样的隔离性是你需要记住的事情,因为将容器内运行的进程与主机服务器上运行的其他进程分开是一个重要的优势。
容器化的另一个优势是便捷性。通过容器,你可以把它传给你开发团队的其他成员,然后当每个人都同意它已经准备好时,就可以将容器推向生产环境。容器本身将在每个工作站上以完全相同的方式运行,无论工作站使用的是哪个操作系统。公平地说,你可以在任何数量的主机上导入和导出虚拟机,但容器让这个过程变得极其简单。事实上,便捷性是该技术设计的核心。
容器化的概念并不一定是新的。当 Docker 出现时,它让 IT 界为之震动,但它并不是第一个提供容器化的解决方案。LXC 和其他技术比它更早出现。然而,它的确是一种聪明的营销策略,凭借一个听起来很酷的品牌,使得容器化成为了主流的流行趋势。不过,我并不是说 Docker 全是炒作,它是一项很棒的技术,具有许多优点。它绝对值得使用,甚至你可能会发现自己更喜欢它而不是虚拟机。
容器化的主要区别在于,每个容器通常只做一件事。例如,一个容器可能承载一个网站,或者包含一个单一的应用程序。虚拟机通常用于完成多项任务,比如一个托管十个网站的 web 服务器。而容器则一般用于完成一项任务,尽管根据不同的实现方式,你可能会看到一些容器打破这一常规。
什么时候应该使用容器?我建议你在运行 Web 应用程序或某种服务,并且能够从节省资源中受益时考虑使用容器。事实上,并非所有应用程序都能在容器中运行得很好,但至少值得考虑。每当你运行一个通常通过 Web 浏览器访问的应用程序时,它可能更适合放在容器中而不是虚拟机中。作为管理员,你很可能会试验不同的工具,并根据你的发现决定最佳工具。
现在我们已经理解了容器的核心概念,让我们来探讨两种容器技术之间的区别。
理解 Docker 和 LXD 之间的区别
在本章中,我们将探讨 Docker 和 LXD,并查看在这两者中运行容器的示例。但在我们开始之前,了解一些将每个解决方案与其他解决方案区分开来的因素是个好主意。
Docker可能是我读者中大多数人听说过的技术。现在似乎没有哪个 IT 会议不提到它。Docker 无处不在,并且可以在几乎任何平台上运行。关于 Docker 有很多文档资源,你可以利用它们来进行部署。Docker 采用了分层的容器化方法。你对容器所做的每个更改都会创建一个新的层,而这些层可以作为其他容器的基础,从而节省磁盘空间。稍后我们会详细介绍。
LXD(发音为 Lex-D)源自LXC,因此在讨论 LXD 之前,理解 LXC 非常重要。LXC(发音为 Lex-C)是Linux 容器的缩写,是另一种容器化实现,类似于 Docker。这项技术像类似的解决方案一样,使用 Linux 内核的控制组(cgroups)特性,能够将进程隔离开并彼此隔离。这增强了安全性,因为进程不应该能够读取其他进程的数据,除非有充分的理由。
LXC 进一步推动了隔离的概念,通过创建一种完全基于在隔离环境中运行应用程序的虚拟化实现,这种环境与操作系统的环境相匹配。你几乎可以在今天的每个 Linux 发行版上运行 LXC 容器。
LXD 也适用于许多 Linux 发行版,但在 Ubuntu 中被视为一等公民。这是因为 Canonical(Ubuntu 背后的公司)在其开发中发挥了重要作用,并且还提供商业支持。由于使 LXD 本身工作的软件是通过snap包分发的,这基本上意味着任何能够安装snap包的 Linux 发行版都应该能够安装 LXD。
LXD 在 LXC 的基础上增加了额外的功能,这些功能在 LXC 中是没有的,比如快照、ZFS 支持和迁移。LXD 并没有替代 LXC;它实际上利用 LXC 提供其基础技术。或许,最好的理解方式是把 LXD 看作是在 LXC 之上增加了一个管理层,提供了更多的功能。
LXD/LXC 和 Docker 有什么不同?主要区别在于,尽管它们都是容器解决方案,并且以非常相似的方式解决了相同的目标,LXD 更像一个实际的虚拟机,而 Docker 则更加努力地与其区分开来。相比之下,Docker 容器是事务性的(正如我之前提到的),你通常会有一个在启动容器时运行的 ENTRYPOINT 命令。从本质上讲,LXD 有一个你可以从主机操作系统直接访问的文件系统,并且采用了更简单的容器化方法。你可以把 LXC 想象成一种机器容器,它非常接近虚拟机,而 Docker 容器则是一个应用容器,为运行应用程序提供所需的基础。尽管有这些区别,这些技术可以以相同的方式使用,并支持相同的使用场景。
你应该什么时候使用 Docker,什么时候使用 LXD?实际上,我建议你同时练习这两者,因为它们并不难学。在本章中,我们将介绍这些技术的基础知识。但为了回答这个问题,有一些使用场景可能会使其中一种技术比另一种更合适。Docker 是一个更通用的工具。你可以在 Linux、macOS,甚至 Windows 上运行 Docker 容器。因此,如果你想创建一个可以在任何地方运行的容器,它是一个不错的选择。LXD 通常更适合 Linux 环境,尽管 Docker 在 Linux 上也运行得很好。如今,容器解决方案运行在哪个操作系统上已经不那么重要了,因为大多数人使用容器服务来运行容器,而不是自己管理的实际服务器。未来,如果你深入容器化领域,可能会发现自己完全放弃操作系统,直接在像亚马逊的弹性容器服务(ECS)这样的服务中运行容器,这是少数几种允许你在不管理底层服务器的情况下运行容器的云服务之一。
Docker 的另一个好处是Docker Hub,你可以用它下载别人创建的容器,甚至上传自己的容器供他人使用。这里的好处在于,如果有人已经解决了你要实现的目标,你可以利用他们的成果,而不是从头开始,他们也可以从你的工作中受益。这节省了时间,通常比手动创建解决方案要好。
在将第三方资源投入使用之前,始终确保审计它们的安全性。这包括(但不限于)由第三方开发的容器。你应该了解容器镜像是如何构建的、设置的安全性如何,以及是否有任何内置内容可能会带来安全风险。基本上,一些管理员会心安理得地接受一个未经审计的容器镜像,但这种做法非常有风险。永远不要部署一个未经安全审计的容器镜像。
既然我们不仅理解了核心概念,还了解了这两种容器化标准之间的区别,那么让我们来看看 Docker。
安装 Docker
安装 Docker 非常快速且简单,以至于几乎不需要单独列出一个章节。在上一章中,我们为了让 基于内核的虚拟机(KVM)虚拟化服务器启动并运行,还需要安装多个软件包,并且调整一些配置文件。相比之下,安装 Docker 轻松得多,你只需要安装 docker.io 软件包:
sudo apt install docker.io
是的,仅此而已。安装 Docker 绝对比设置 KVM 更简单,就像我们在上一章所做的那样。Ubuntu 将 Docker 包含在默认的仓库中,所以只需安装这一软件包及其依赖项。你现在会在你的机器上安装一个名为 docker 的新服务。为了能够使用它,服务必须正在运行。你可以使用以下命令来检查它是否已经在运行:
systemctl status docker
查看上一条命令的输出,检查 docker 服务是否正在运行。你还应该检查服务是否已启用。如果没有,你可以通过以下命令同时启动并启用该服务:
systemctl enable --now docker
我们现在也可以使用 docker 命令来管理容器了。默认情况下,它确实需要 root 权限,因此你需要使用 sudo 来执行它。为了简化操作,我建议你在继续之前将用户帐户添加到 docker 组中。这样,你就不需要每次执行 docker 命令时都使用 sudo。以下命令将把你的用户帐户添加到相应的组:
sudo usermod -aG docker <yourusername>
登出并重新登录后,你将能够更轻松地管理 Docker。
你可以通过运行 groups 命令来验证你的组成员身份,执行该命令时不需要任何选项,命令输出中应该会显示你的用户是 docker 组的成员。
好了,就这样。Docker 已经安装完毕,你的用户帐户也已经是 docker 组的成员,所以你可以开始使用了。哇,真是太简单了!
既然我们已经安装了 Docker,那么就开始使用它吧。这是一项有趣的技术,在接下来的章节中,我们将探索一些示例。
管理 Docker 容器
现在 Docker 已经安装并运行了,让我们来试试它。安装 Docker 后,我们现在可以使用 docker 命令,该命令有各种子命令可以执行与容器相关的不同功能。首先,让我们尝试 docker search:
docker search ubuntu
使用 Docker,容器是从镜像创建的。我们可以使用许多现有的容器镜像,或者我们可以自己构建。docker search 命令允许我们搜索已经存在并且已经提供给我们的容器镜像。一旦我们选择了一个镜像,我们可以将其下载到本地并从中创建容器实例。
管理员可以搜索(和下载)现有的容器的能力只是 Docker 提供给我们的众多优秀功能之一。虽然我们肯定可以构建自己的容器镜像(我们将在本章节中这样做),但有时使用现有的容器镜像可能比从头开始创建更合理。
例如,您可以安装一个名为 nginx 的 NGINX 容器。这实际上是一个官方的容器镜像,所以应该是可信的。如果您在 Docker Hub 网站上查找该镜像,会看到 DOCKER OFFICIAL IMAGE 的字样。如果我们想部署一个运行 NGINX 的容器,通过官方镜像可以节省大量时间,特别是与从头开始创建相比。毕竟,如果不必要,为什么要重新发明轮子呢?
然而,即使容器镜像来自一个可信的源,您仍然应该对其进行审计。以 NGINX 为例,我们可以相当有信心地认为该镜像是安全的,不包含任何不需要的对象,比如恶意软件。但是,当涉及到安全性时,没有 100% 的可信度,所以我们仍然应该进行审计。
那么这是如何工作的呢?docker search 命令将搜索 Docker Hub,这是一个在线仓库,用于存储供他人下载和使用的容器。您可以根据其他应用程序或者其他发行版(如 Fedora 或 AlmaLinux)搜索容器。该命令将返回符合您搜索条件的 Docker 镜像列表。
那么我们用这些镜像做什么呢?在 Docker 中,镜像是其最接近虚拟机或硬件镜像的等价物。它是一个快照,包含特定操作系统或 Linux 发行版的文件系统,以及作者包含的一些更改,使其执行特定任务。可以下载该镜像并自定义以满足您的需求。如果您希望贡献上游,可以选择将您的定制镜像上传回 Docker Hub。您下载的每个镜像都将存储在您的计算机上,这样您每次创建新容器时就不必重新下载它。
要拉取 Docker 镜像以供使用,我们可以使用docker pull命令,后面跟着我们在search命令的输出中看到的某个镜像名称:
docker pull ubuntu
使用前面的命令,我们正在从 Docker Hub 拉取最新的 Ubuntu 容器镜像。镜像现在将被存储在本地,并且我们可以从中创建新容器。该过程将类似于以下截图:

图 17.1:下载 Ubuntu 容器镜像
如果你对本地保存的镜像感到好奇,可以执行docker images来获取你在服务器上存储的 Docker 容器镜像列表:
docker images
输出将类似于这样:

图 17.2:列出已安装的 Docker 镜像
注意输出中的IMAGE ID。如果由于某些原因你想删除一个镜像,可以使用docker rmi命令来删除,并且需要使用该 ID 作为参数来告诉命令删除哪个镜像。如果我要删除截图中显示的 ID 的镜像,语法会像这样:
docker rmi d2e4e1f51132
一旦你将容器镜像下载到服务器上,你可以通过运行docker run命令来创建一个新容器,后跟你的镜像名称和要在镜像中运行的应用程序。从 Docker 容器中运行的应用程序被称为ENTRYPOINT,这是一个用来描述特定容器配置为运行的应用程序的术语。不过,你并不局限于ENTRYPOINT,并非所有容器都有ENTRYPOINT。你可以在容器中运行任何通常能够在该发行版中运行的命令。以我们之前下载的 Ubuntu 容器镜像为例,我们可以使用以下命令运行bash,以便获取提示符并输入我们希望运行的任何命令:
docker run -it ubuntu /bin/bash
一旦你运行了该命令,你就会从容器内与 Shell 提示符进行交互。在这里,你可以运行你通常会在实际 Ubuntu 机器中运行的命令,比如安装新包、修改配置文件等。尽管如此,可以继续在容器中进行操作,然后我们将继续进行更多关于其实际工作的理论讲解。
在继续更多示例之前,我们应该先弄清楚 Docker 中可能让人困惑的一些方面。最有可能让 Docker 新手感到困惑的是容器是如何创建和销毁的。当你对已下载的镜像执行docker run命令时,你实际上是在创建一个容器。因此,你通过docker pull命令下载的镜像并不是一个实际的容器,而是当你运行它的实例时,它才会成为容器。当容器内执行的命令完成后,容器就会消失。因此,如果你在容器中运行/bin/bash并安装了一堆包,那么当你退出容器时,这些包会被清除。
你可以把 Docker 镜像看作是容器的“蓝图”,可以用它来创建运行中的容器。你运行的每个容器都有一个容器 ID,用来区分它与其他容器。如果你想删除一个持久化的容器,例如,你需要使用docker rm命令引用这个 ID。这与用来删除容器镜像的docker rmi命令非常相似。
要查看容器 ID,首先你需要退出当前运行的容器。有两种方法可以做到这一点。首先,你可以按Ctrl + d断开连接,或者输入exit并按Enter键。当你退出容器时,你实际上是在移除它(Docker 容器通常只在运行时存在)。当你运行docker ps命令时(这是你每次想查看系统中容器列表时使用的命令),你不会看到它的列出。相反,你可以添加-a选项,查看所有容器,包括那些已经停止的。
那么,你可能会想,如何退出一个容器并保持它不消失。要做到这一点,在你连接到容器时,按下Ctrl + p,然后按下q(按这两个字母时不要松开Ctrl键)。这样你就会退出容器,而当你运行docker ps命令时(即使没有使用-a选项),你会看到它仍然在运行。
docker ps命令值得关注。它的输出将提供关于你服务器上容器的非常有用的信息,包括之前提到的CONTAINER ID。此外,输出还会包含容器的IMAGE、容器创建时运行的COMMAND、容器的CREATED时间和其STATUS,以及你可能转发的任何PORTS。输出还会显示每个容器的随机生成名称,这些名称通常非常有趣。当我在写这一部分时,我的容器的代码名分别是tender_cori、serene_mcnulty和high_goldwasser。这只是 Docker 的众多奇特之处之一,有些名字甚至非常搞笑。
docker ps -a命令的主要输出是CONTAINER ID、COMMAND和STATUS。我们已经讨论过的 ID 可以让你引用特定的容器,以便对其执行命令。COMMAND让你知道正在运行的是什么命令。在我们的例子中,当我们启动容器时,执行了/bin/bash命令。
如果我们有任何停止的容器,可以使用docker start命令恢复容器,给它传递容器 ID 作为参数。你的命令将会类似这样:
docker start d2e4e1f51132
输出将仅返回容器的 ID,然后将你带回到你的 Shell 提示符——不是容器的 Shell 提示符,而是服务器的 Shell 提示符。此时你可能会想,如何返回到容器的 Shell 提示符呢?我们可以使用docker attach来实现:
docker attach d2e4e1f51132
docker attach命令很有用,因为它允许你将你的 shell 附加到一个已经在运行的容器。大多数时候,容器会自动启动,而不是像我们之前所做的那样以/bin/bash启动。如果出现问题,我们可能想使用类似docker attach的命令来浏览运行中的容器,查找错误信息。这非常有用。
说到有用的命令,另一个很棒的命令是docker info。这个命令会提供有关 Docker 实现的信息,例如让你知道系统上有多少个容器,通常这应该是你执行docker run命令的次数,除非你用docker rm清理了之前运行的容器。随便看看它的输出,看看能学到什么。
更深入地了解容器的主题时,理解 Docker 容器是什么以及不是很重要。容器不是在后台运行的服务,至少从本质上来说不是。容器是一个命名空间的集合,比如它的文件系统或用户命名空间。正如本章早些时候讨论的,容器通过利用 Linux 内核中的技术与服务器的其余部分隔离开来。当你在容器中没有运行任何进程时,它就没有理由继续运行,因为它的命名空间是空的。因此,它会停止。如果你希望以类似于服务的方式运行容器(即它在后台持续运行),你需要以分离模式运行容器。基本上,这是一种告诉容器运行该进程,并且在你告诉它停止之前不停止运行的方式。下面是一个创建容器并以分离模式运行的例子:
docker run -dit ubuntu /bin/bash
运行前一个命令后,Docker 会打印出一个容器 ID,然后返回到你的命令提示符。你可以使用docker ps命令查看容器是否正在运行,然后使用docker attach命令结合容器 ID 连接到容器并执行命令。
通常,我们使用-it选项来创建容器。这是我们之前几个例子中使用的选项。-i选项启用交互模式,而-t选项为我们提供一个伪终端。命令的最后,我们告诉容器运行 Bash shell。-d选项让容器在后台运行。
看似没有实际任务执行的后台 Bash shell 可能显得相对没有用。但这些只是一些简单的例子,帮助你熟悉 Docker。一个更常见的用例可能是运行一个特定的应用程序。实际上,你甚至可以通过在容器中安装和配置 Apache 来为一个网站提供服务,包括设置虚拟主机。那么问题就变成了:如何在浏览器中访问容器内 Apache 的实例?答案是端口重定向,Docker 也支持这一功能。我们来试试吧。
首先,让我们以分离模式创建一个新的容器。我们还将把容器中的80端口重定向到主机的8080端口:
docker run -dit -p 8080:80 ubuntu /bin/bash
该命令将输出一个容器 ID。这个 ID 会比你习惯看到的要长得多。因为当我们运行docker ps -a时,它只显示缩短的容器 ID。你不需要使用整个容器 ID 进行附加;只要它足够长,与你的其他 ID 不同,就可以仅使用其中的一部分:
docker attach dfb3e
这里,我已经附加到一个 ID 以dfb3e开头的容器。这将把我的 Shell 连接到容器中的 Bash Shell。
让我们安装 Apache。我们之前做过这件事,但你会看到一些不同之处。首先,如果你简单地运行以下命令来安装apache2包,就像我们通常做的那样,它可能会因为一两个原因失败:
sudo apt install apache2
这里有两个问题,第一个是sudo在 Ubuntu 容器中默认没有包含,因此它甚至无法识别命令中的sudo部分。当你运行docker attach时,你实际上是以root用户附加到容器中,因此缺少sudo不会成为问题。第二,容器中的仓库索引可能已经过时,甚至根本没有。这意味着容器中的apt甚至找不到apache2包。为了解决这个问题,我们将首先更新仓库索引:
apt update
然后,使用以下命令安装apache2:
apt install apache2
在安装包时,你可能会被要求设置时区或地理位置。如果是这样,请根据提示输入相应信息。
现在,我们已经在容器中安装了 Apache。我们不需要担心配置默认的示例网页或让它看起来更漂亮。我们只是想验证它是否正常工作。让我们启动服务:
/etc/init.d/apache2 start
运行该命令后,Apache 应该已经在容器内运行。
之前的命令绝对不是我们通常启动服务的方式。通常,我们会使用像systemctl start apache2这样的命令,但容器内并没有实际的初始化系统,因此运行systemctl命令将无法像通常那样工作。始终参考你试图运行的容器可能存在的任何文档,了解如何启动其中可能包含的应用程序。
Apache 应该在容器内运行。现在,按下Ctrl + p,然后按q(在按这两个字母时,保持按住Ctrl键)退出容器,但让它继续在后台运行。你应该能够通过在浏览器中导航到localhost:8080来访问容器的示例 Apache 网页。你应该看到 Apache 的默认It works!页面。
恭喜,你已经在容器内成功运行了一个应用程序:

图 17.3:默认的 Apache 启动页面,运行在容器内
随着你对 Docker 知识的深入,你可能会更想了解ENTRYPOINT的概念。ENTRYPOINT是启动 Docker 容器中应用程序的首选方式。在我们到目前为止的示例中,我们使用了/bin/bash作为ENTRYPOINT。虽然这完全有效,但ENTRYPOINT通常是一个配置好的 Bash 脚本,用于启动所需的应用程序,并由容器启动。
我们的 Apache 容器在后台愉快地运行,响应主机上8080端口的 HTTP 请求。但现在我们该怎么办呢?我们可以从它创建我们自己的镜像,以便以后简化部署。公平地说,我们在容器内仅安装了 Apache,因此它并没有为我们节省太多工作。在实际的生产环境中,可能会有一个容器正在运行,且需要执行许多命令来设置它。使用镜像后,我们可以将所有这些工作都“烘焙”到镜像中,这样每次我们想创建容器时,就不需要再运行任何设置命令。要创建一个容器镜像,我们可以先通过运行docker ps命令获取正在运行的容器 ID。一旦获取到容器 ID,我们可以使用docker commit命令来创建该容器的新镜像:
docker commit <Container ID> ubuntu/apache-server:1.0
该命令将返回我们新镜像的 ID。要查看我们机器上所有可用的 Docker 镜像,我们可以运行docker images命令,Docker 将返回一个列表。你应该能看到我们下载的原始 Ubuntu 镜像,以及我们刚刚创建的镜像。我们首先会看到镜像来源的仓库列,在我们的例子中是 Ubuntu。接下来,我们看到标签。我们原始的 Ubuntu 镜像(即我们用docker pull下载的那个)标签是latest。当我们最初下载它时并没有指定标签,它只是默认使用了latest。此外,我们还会看到两个镜像的 ID 以及它们的大小。
要从我们的新镜像创建一个新的容器,我们只需要使用docker run,但是需要指定新镜像的标签和名称。请注意,可能已经有一个容器在监听8080端口,所以如果该容器没有停止,这个命令可能会失败:
docker run -dit -p 8080:80 ubuntu/apache-server:1.0 /bin/bash
说到停止容器,我可能也应该展示一下如何操作。正如你可能猜到的,命令是docker stop,后跟容器 ID:
docker stop <Container ID>
这将向容器发送SIGTERM信号,如果容器在延迟后没有自行停止,将接着发送SIGKILL信号。
诚然,Apache 容器的示例相对简单,但它能够有效地展示一个实际有用的工作容器。在继续之前,先想一想在你的组织中可以用 Docker 做哪些事情。它看起来是一个非常简单的概念(确实如此),但它让你能够做一些非常强大的事情。也许你想尝试将你组织的内网页面或某些应用程序容器化。Docker 的概念确实很简单,但只要有足够的想象力,它可以带来长远的影响。
在结束这一部分之前,我将给你一个我在之前的工作中如何实现容器的个人示例。在这个组织里,我与一些嵌入式 Linux 软件工程师合作,他们每个人都有自己喜欢的 Linux 发行版。有些人喜欢 Ubuntu,其他人喜欢 Debian,还有一些甚至使用 Gentoo。单纯来看,这并不一定是个问题——有时尝试其他发行版是很有趣的。但对开发人员来说,平台的改变可能会引入不一致性,这对软件项目来说是不利的。不同的 Linux 发行版中包含的构建工具各不相同,因为它们都提供不同版本的开发包和库。这个特定组织开发的应用程序只能在 Debian 中正确编译,而较新的编译器版本会对应用程序造成问题。我的解决方案是为每个开发人员提供一个基于 Debian 的 Docker 容器,并将他们所需的所有构建工具打包其中。到这个时候,无论他们的工作站上运行的是哪个发行版,都不再重要。容器在他们的操作系统上都相同。无论底层操作系统是什么,每个人都拥有相同的工具。这使每个开发人员能够自由选择自己喜欢的 Linux 发行版(甚至是 macOS),并且不会影响他们的工作能力。我相信,你也可以为实现容器化想出一些聪明的用例。
现在我们已经了解了 Docker 的基础知识,接下来让我们看看如何自动化构建容器的过程。
使用 Dockerfile 自动化创建 Docker 镜像
我在本书中之前提到过,任何值得让服务器做超过一次的事情都应该自动化,而构建 Docker 容器也不例外。Dockerfile 是一种通过创建包含一组指令的文本文件来自动化 Docker 镜像构建的简便方法。Docker 可以读取这个文件,执行其中的命令,并构建一个容器。真是神奇。
设置 Dockerfile 的最简单方法是创建一个目录,最好给它起一个描述性名称,以便你能知道要创建什么样的镜像(不过你可以随意命名),然后在其中创建一个名为 Dockerfile 的文本文件。为了快速示范,把这段文本复制到你的 Dockerfile 中,我会解释它是如何工作的:
FROM ubuntu
MAINTAINER Jay <jay@somewhere.net>
# Avoid confirmation messages
ARG DEBIAN_FRONTEND=noninteractive
# Update the container's packages
RUN apt update; apt dist-upgrade -y
# Install apache2 and vim
RUN apt install -y apache2 vim-nox
# Start Apache
ENTRYPOINT apache2ctl -D FOREGROUND
让我们逐行了解这个 Dockerfile,帮助我们更好地理解它在做什么:
FROM ubuntu
我们需要一个镜像来作为我们新镜像的基础,因此我们使用 Ubuntu 作为起点。
这将导致 Docker 从 Docker Hub 下载 ubuntu:latest 镜像,前提是我们还没有在本地下载过它。如果我们本地已经有它,Docker 会直接使用本地缓存的版本。
MAINTAINER Jay <myemail@somewhere.net>
在这里,我们设置镜像的维护者。基本上,我们在声明镜像的作者。这是可选项,如果不想包括,也可以省略。
# Avoid confirmation messages
以井号符号(#)开头的行会被忽略,因此我们可以在 Dockerfile 中添加注释。推荐这样做,以便让其他人清楚地了解你的 Dockerfile 在做什么。
ARG DEBIAN_FRONTEND=noninteractive
在这里,我们设置了一个环境变量,在这种情况下,它将环境设置为noninteractive。这样做的原因是,在构建 Docker 容器时安装软件包的过程应该是自动的;如果在安装软件包时出现提示并询问你问题,你的输入将无法传递,过程将会挂起。
通过这个环境变量,我们明确表示我们希望处于noninteractive模式,以便任何出现的问题都能使用默认答案,而不会提示我们。
RUN apt update; apt dist-upgrade -y
通过RUN命令,我们告诉 Docker 在创建镜像时执行一个特定的命令。在这种情况下,我们更新镜像的仓库索引并进行完整的软件包更新,以确保生成的镜像尽可能是最新的。-y选项用于在安装包时抑制任何确认请求。尽管我们之前设置了noninteractive模式,apt仍然会尝试交互式确认更改,而-y选项会抑制这一点。
RUN apt install -y apache2 vim-nox
接下来,我们安装了apache2和vim-nox。vim-nox包不是必须的,但我个人喜欢确保所有服务器和容器都安装了它。我在这里主要是为了展示你可以在一行中安装多个包。
ENTRYPOINT apache2ctl -D FOREGROUND
我之前提到过ENTRYPOINT的概念,这就是我们在容器启动时明确应该运行哪个应用程序的地方。apache2ctl命令是一个用于 Apache 的包装命令,它允许管理员控制运行 Apache 守护进程的细节。对该命令的详细讲解超出了本章的范围,但我们在这里使用它是因为我们希望 Apache 能随着容器自动启动,而apache2ctl是实现这一点的方法之一,而不依赖于systemctl(容器中没有这个命令)。
很好,现在我们有了 Dockerfile。那么接下来我们该怎么做?当然是将它转化为镜像!为此,我们可以使用docker build命令,可以在包含 Dockerfile 的目录中执行。以下是使用docker build命令创建一个标签为packt/apache-server:1.0的镜像的示例:
docker build -t learnlinuxtv/apache-server:1.0 .
一旦你运行该命令,你会看到 Docker 为你创建镜像,执行你要求的每个命令。镜像将按你的要求设置。基本上,我们只是自动化了创建这个我们在本节中作为示例使用的 Apache 容器的整个过程。如果出现任何问题,Docker 会在你的 Shell 中打印错误信息。你可以在 Dockerfile 中修复错误并重新运行,它将从上次停止的地方继续。
完成后,我们可以从新的镜像创建一个容器:
docker run -dit -p 8080:80 learnlinuxtv/apache-server:1.0
几乎在运行容器后立即,示例 Apache 网站将可以通过主机上的localhost:8080访问。通过使用 Dockerfile,你可以自动化创建 Docker 镜像的过程。这很简单,对吧?使用 Dockerfile,你可以做的事情远不止这些;可以随时浏览 Docker 官方文档,了解更多内容。探索是关键,赶紧试一试,进行一些实验吧。
管理 LXD 容器
在解决了 Docker 之后,让我们来看看如何使用 LXD 运行容器。我们直接开始,安装所需的包:
sudo snap install lxd
正如你所看到的,安装 LXD 和安装 Docker 一样简单。实际上,使用 LXD 管理容器也非常直观,正如你很快会看到的那样。安装 LXD 后,我们将获得 lxc 命令,这是我们用来管理 LXD 容器的命令。不过,在开始之前,我们应该将用户账户添加到 lxd 组:
sudo usermod -aG lxd <yourusername>
确保你退出并重新登录,以使更改生效。就像 Docker 的 docker 组一样,lxd 组将允许我们的用户账户管理 LXD 容器。
接下来,我们需要初始化我们的 LXD 安装。我们将通过 lxd init 命令来完成:
lxd init
该过程将与以下截图类似:

图 17.4:使用 lxd init 命令设置 LXD
lxd init 命令会问我们一系列关于如何设置 LXD 的问题。大多数默认设置都可以,存储池的大小我只是使用了默认的 30 GB,但你可以根据需要使用任何大小。在设置过程中,我将 ipv6 设置为 none,因为我的网络不使用它,并且我决定通过网络启用 lxd。
即使我们为大多数问题选择了默认值,这些设置也会给你一个关于 LXD 可用的一些不同选项的大致共识。例如,我们可以看到 LXD 支持存储池的概念,这是它的一个很酷的功能。
在这里,我们正在创建一个默认的存储池,文件系统格式为 zfs,这是一种用于实际硬盘的文件系统。在设置过程中,LXD 会设置存储池、网络桥接、IP 地址方案,基本上是我们开始所需的一切。
现在 LXD 已经安装并设置完成,我们可以配置第一个容器了:
lxc launch ubuntu:22.04 mycontainer
通过这个简单的命令,LXD 现在将下载此容器的根文件系统,并为我们进行设置。完成后,我们将有一个基于 Ubuntu 22.04 运行并可供使用的 LXD 容器。这与 Docker 不同,Docker 默认只设置一个镜像,需要我们手动运行它。在此过程中,我们为容器命名为 mycontainer。到目前为止,过程应该很容易跟随。
你可能会想,既然我们在学习 LXD,为什么还使用了 lxc 命令来创建容器呢?正如我之前提到的,LXD 是对 LXC 的改进,因此它使用 lxc 命令进行管理。特定于 LXD 层的命令将是 lxd,而任何与容器管理相关的操作则由 lxc 来完成。
在管理容器时,你将需要执行几种常见操作,例如列出容器、启动容器、停止容器、删除容器等等。lxc 命令集非常简单直观。以下是列出一些常用命令的表格,我相信你会同意这些命令的语法非常合逻辑。对于每个例子,你只需将 <container> 替换为你创建的容器的名称:
| 目标 | 命令 |
|---|---|
列出容器 |
lxc list |
启动容器 |
lxc start <container> |
停止容器 |
lxc stop <container> |
删除容器 |
lxc delete <container> |
列出已下载的镜像 |
lxc image list |
删除镜像 |
lxc image delete <image_name> |
了解完所有基本操作后,我们可以进入容器并进行一些操作。要打开我们刚刚创建的容器的 shell,可以执行以下命令:
lxc exec mycontainer bash
在前面的命令中,exec 告诉容器我们要执行一个命令,mycontainer 是我们要在其上执行命令的容器名称,而具体的命令是 bash。执行该命令后,它会立即以 root 身份从容器中运行 bash。从这里,你可以根据需要配置容器,比如安装软件包、设置服务,或做其他任何使容器符合你需求的操作。事实上,定制容器以便重新部署的过程比 Docker 更加简单。
与 Docker 不同,退出容器时不会丢失更改,而且你无需以特定的方式退出容器来避免丢失更改。在 LXD 中,我们也不需要处理层(layers),这点你可能会觉得有些不同(Docker 容器中的层可以加速部署,但如果之前运行的容器没有被清理,层数就会显得混乱)。
我们用来创建容器的 Ubuntu 镜像包括一个默认的用户帐户 ubuntu。这与一些 VPS 提供商类似,后者也默认包含 ubuntu 用户帐户(例如 Amazon EC2)。如果你更喜欢以此用户登录,而非 root,可以使用以下命令:
lxc exec mycontainer -- su --login ubuntu
ubuntu 用户有 sudo 权限,因此你可以毫不困难地执行特权任务。
要退出容器,你可以按键盘上的Ctrl + d,或者直接输入exit。你可以随意登录到容器中进行一些更改和实验。一旦你设置好了容器,按照你的喜好,你可能希望容器在启动服务器时自动启动。其实,这个操作非常简单:
lxc config set mycontainer boot.autostart 1
使用前面的命令,我们将boot.autostart设置为1,这将开启这个特定的功能。对于熟悉编程的朋友来说,1代表“开启”,0代表“关闭”,就像布尔变量一样。在设置了这个配置值之后,你新创建的容器将在每次服务器启动时自动启动。
现在,让我们玩得开心一点。你可以随意在容器中安装apache2包。与 Docker 类似,我发现你可能需要先运行apt update来更新软件包列表,因为我曾见过在一个新容器中安装包失败,仅仅是因为索引信息已经过时。所以,为了安全起见,请先运行此命令:
sudo apt update && sudo apt install apache2
现在,你应该已经在容器中安装并运行了 Apache。接下来,我们需要获取容器的 IP 地址。没错,你没看错,LXD 为其容器分配了自己的 IP 地址空间,这非常方便。只需运行ip addr show(与在普通服务器中运行的命令相同),它会显示 IP 地址信息。在运行容器的同一台机器上,你可以访问这个 IP 地址来查看默认的 Apache 网页。如果你在没有图形用户界面的服务器上运行容器,可以使用curl命令来验证它是否正常工作:
curl <container_ip_address>
虽然我们在容器中运行了 Apache,但我们可以看到它现在还不是很有用。网页仅能从托管容器的机器上访问。如果我们希望本地网络中的用户,甚至是外部互联网的用户都能访问我们的网站,这样就没什么帮助了。我们可以设置防火墙规则来路由流量,但还有一种更简单的方法——为外部访问创建一个配置文件。
我之前提到过,尽管 LXD 是一种容器化技术,但它与虚拟机共享一些功能集,基本上可以在非虚拟机环境中提供类似虚拟机的功能。通过 LXD,我们可以创建一个配置文件,允许它从你的 DHCP 服务器获取 IP 地址,并像物理设备一样通过局域网直接路由流量到容器。
在继续之前,你需要在服务器上设置桥接连接。这是通过 Netplan 软件完成的,在前一章节中有提到。如果你列出你的网络接口(ip addr show),你应该能看到一个br0连接。如果你没有配置这个,参考第十六章,虚拟化,并查阅其中的桥接虚拟机网络部分。创建好这个连接后,你就可以继续了。
一些网络卡不支持桥接,尤其是某些 Wi-Fi 网络卡。如果你的硬件无法创建桥接,以下部分可能对你无效。请查阅你硬件的文档,确保你的网络卡支持桥接。
为了创建我们需要的配置文件,以启用容器的外部访问,我们将使用以下命令:
lxc profile create external
我们应该看到类似如下的输出:
Profile external created
接下来,我们需要编辑刚刚创建的配置文件。以下命令将打开配置文件,以便你进行编辑:
lxc profile edit external
在配置文件中,我们将用以下内容替换其文本:
description: External access profile
devices:
eth0:
name: eth0
nictype: bridged
parent: br0
type: nic
从此时起,我们可以使用以下命令,通过这个配置文件启动新容器:
lxc launch ubuntu:22.04 mynewcontainer -p default -p external
注意我们正在应用两个配置文件,default 和 external。我们这样做是为了先加载 default 中的值,然后加载第二个配置文件,以便它覆盖任何可能存在的冲突参数。
不过我们已经有一个容器了,或许你会好奇如何编辑现有的容器来利用我们的新配置文件。这很简单:
lxc profile add mycontainer external
从此时起,假设你的服务器上的主机桥接已经正确配置,容器应该可以通过本地 LAN 访问。你应该能够托管一个资源,比如网站,并让其他人能够访问它。这个资源可以是本地内网站点,甚至是面向互联网的网站。
就 LXD 的入门而言,基本上就是这些。LXD 非常简单易用,其命令结构也非常合乎逻辑且容易理解。只需要几个简单的命令,我们就能创建一个容器,甚至让它对外可访问。Canonical 在线上提供了很多示例和教程,可以帮助你进一步深入了解,但根据目前所学,你应该已经掌握了足够的实用知识,可以在你的组织中部署这个解决方案了。
总结
容器是一种托管应用程序的绝佳方式。相比虚拟机,你可以在硬件上启动更多容器,这无疑可以节省资源。虽然并非所有应用都能在容器中运行,但容器是一个非常实用的工具。在本章中,我们探讨了 Docker 和 LXD。虽然 Docker 更适用于跨平台应用,但 LXD 更简单易用,同时非常灵活。我们首先讨论了这两者之间的区别,然后实验了创建容器并管理容器的方法。
在下一章,我们将进一步扩展对容器的了解,探索编排技术,使我们能够更高效地管理多个容器。这将是将所有与容器相关的概念结合在一起的一章。
相关视频
-
Docker Essentials 教程系列(LearnLinuxTV):
linux.video/docker-essentials -
开始使用 LXD(LearnLinuxTV):
linux.video/lxd-guide
进一步阅读
-
Docker 文档页面:
learnlinux.link/d-docs -
LXD 简介:
learnlinux.link/lxd-intro -
LXD 文档:
learnlinux.link/c-lxd
加入我们社区的 Discord
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十八章:容器编排
在上一章中,我们开始学习关于容器化的概念。我们了解了容器是什么,它们与虚拟机(VMs)的区别,以及如何运行两种不同类型的容器(Docker 和 LXD)。如你所知,容器通常较为轻量(这意味着你可以在同一硬件上运行更多的容器,而不是虚拟机),并且容易通过逻辑性较强的命令语法进行管理,例如使用 docker run myapp 启动一个名为 myapp 的容器。根据你的组织规模,你可能只需要运行一个或两个容器来满足需求,或者你计划扩展到数百个容器。虽然维护少量容器相对简单,但随着数量的增加,容器的管理难度也会急剧增加。
在本章中,我们将开始探讨容器编排的概念,它可以帮助我们更好地维护我们运行的容器。如果没有这种编排,责任就落在你这个管理员身上,确保你的容器正常运行。虽然仍然需要有人对关键任务容器负责,但编排为我们提供了可以用来更高效管理容器的工具。此外,编排允许我们创建一个易于扩展的集群,从而更好地应对用户或客户的需求。在探讨这一话题时,我们将着重于:
-
容器编排
-
为 Kubernetes 测试准备实验环境
-
使用 MicroK8s
-
设置 Kubernetes 集群
-
通过 Kubernetes 部署容器
激动吗?我知道我很激动——容器化是一个非常有趣的学习和工作内容。在本章中,我们甚至会创建我们自己的集群!但在此之前,让我们先确保理解什么是容器编排。
容器编排
在上一章中,我们介绍了如何在服务器上运行容器的基础知识,特别提到了 Docker,它将在本章中发挥非常重要的作用。我们了解了如何拉取 Docker 镜像,以及如何使用镜像创建容器。关于 Docker,还有许多更高级的概念可以学习,但理解基本内容已足够满足本章的范围。既然我们已经知道如何运行容器,那么接下来就逻辑地探讨如何更高效地管理它们。
传统上,作为管理员,你需要确保组织中的关键应用程序和服务始终保持健康和可用。如果某个关键资源因任何原因停止工作,你需要负责将其恢复到健康状态。无论我们是在物理服务器、虚拟机(VM)还是容器中使用应用程序,这一需求都没有变化——生产应用程序必须始终可用,且停机时间要尽量减少或没有停机时间。针对容器来说,编排帮助我们更高效地管理它们。编排不仅让我们可以在一个地方管理容器,还提供了额外的工具,帮助我们更智能地处理负载并从故障中恢复。
举个例子:假设你在一家组织工作,这个组织有一个重要的网站需要始终在线可用,且当前网站是部署在虚拟机中的。作为管理员,你通过将其运行在 Docker 容器中来测试应用,发现它不仅与在虚拟机中的功能相同,而且在容器中运行时需要的服务器内存更少,响应速度也比之前更快。你公司里的人都对这个结果印象深刻,将公司网站迁移到容器内运行的项目也圆满成功。
现在,假设你的组织准备发布公司产品的全新版本。预计在新版本发布后的几周内,网站的访问需求将增加十倍。为了解决这个问题,你可以根据预计的负载,启动任意数量的额外容器,然后设置负载均衡器将流量均匀分配到这些容器之间。当新版本发布的热度平息后,你可以删除新增的容器,恢复到原来的容器数量。我们还没有讨论负载均衡器,但它们对在多个节点之间分配流量非常有用。这个功能已经内置于 Kubernetes 中,因此你无需额外安装任何东西就能利用这一功能。
如果你组织的新产品更新计划在凌晨 1:00 上线,那么你需要确保在那个时间醒来,并执行必要的命令来启动额外的容器和部署负载均衡器。然后你需要观察这些容器一段时间,确保它们稳定运行。也许你已经在测试环境中进行过测试,所以你有足够的信心认为维护过程会顺利进行。在成功启动容器后,你在日历中设置了提醒,在两周后重新登录,撤销这些更改。
虽然这种方法在技术上可以工作并实现成功的部署,但它并不高效。当然,你可以在凌晨 1 点登录以启动额外的容器并部署负载均衡器,但这真的是时间的有效利用吗?如果你那时非常困倦,并犯了一个错误,导致在一个重要发布时出现故障怎么办?而当发布窗口结束,负载恢复正常时,你可能根本不会看到原本打算用来提醒自己并降低容器数量的警报。
在这种情况下,你可能会面临一笔昂贵的账单,因为你的服务器将使用额外的能量来运行那些已经不再需要的容器。更糟糕的是,手动管理容器意味着它无法应对负载意外增加的情况。尽管我们有最好的意图,但任何手动运行的过程都可能不顺利;毕竟我们只是人类。
使用容器编排,我们基本上将运行容器的过程委托给一个应用程序,该应用程序会根据需求增加额外的容器,并在需求减少时删除不需要的容器。编排还简化了应用程序的设置过程,通过提供我们可以使用的工具,确保一定数量的容器至少在最小限度内运行,并在负载和需求激增时设定最大容器数。这使得我们能够拥有一个能够自动增长和收缩的基础设施,以匹配用户的需求。此外,容器编排还允许我们在发生故障时自动修复。如果某个容器遇到问题并因某种原因失败,它将被删除并从镜像中重新创建。
如你所见,编排为我们提供了额外的价值,它实际上可以节省时间和精力。公平地说,文中提到的一些功能是容器化本身的一部分,并不一定特定于编排,但后者确实让我们能够更高效地管理这些工具。容器编排适合每个人吗?没有技术能满足 100%的使用场景,但它肯定值得考虑。如果你只运行一个容器,并且没有计划运行另一个,那么像编排这样的技术就显得过于复杂——管理集群所花的时间会比管理容器本身更多。请凭你的判断做出决定。
如果你决定实施容器化,Kubernetes(通常缩写为K8s)是一个非常流行的容器编排解决方案,也是我们将在本章中探讨的内容。Kubernetes 允许我们为应用程序创建部署,提供我们可以利用的精细控制,以决定应用程序的实际行为。如果容器因任何原因停止工作,它可以自动重新启动容器,确保运行的容器数量满足需求,并将我们的工作负载分布到多个工作节点上,从而避免任何一个服务器因运行过多容器而不堪重负。Kubernetes 非常流行,且不仅限于 Ubuntu Server。然而,像 Kubernetes 这样的技术在 Ubuntu 中的使用是我们可以扩展平台的许多方式之一。
你需要什么才能设置一个 Kubernetes 集群?这在此时可能是一个合乎逻辑的问题。在下一部分,我们将讨论在决定如何设置之前需要考虑的一些事项。
为 Kubernetes 测试准备实验环境
在一个组织中,规划整个 Kubernetes 集群的部署可能相当复杂——你可能需要购买硬件,还需要分析现有的环境,了解容器化如何融入其中。你可能希望运行的一些应用程序不适合容器;有些根本不支持在容器中运行。假设你已经查看了你希望在容器中运行的应用程序的文档,并得出结论认为这种技术是被支持的,下一步就是采购硬件(如果你还没有可用的运行场所),然后设置集群。
在本书中,针对我们来说,我们不需要联系服务器供应商并提交采购订单来简单地测试技术。如果你实际参与了容器编排在你组织中的部署,那么这是一个很有趣的项目。但是为了本书的目的,我们将讨论一些建立个人实验室所需的细节,以便设置 Kubernetes 并开始学习该平台。
设置实验室以测试软件时,最好的经验法则是尽量使用现有的资源。为了创建一个 Kubernetes 集群,我们需要一台 Ubuntu 机器作为控制器,另外一台或多台服务器作为工作节点。正如你将在本书后续部分学到的,Kubernetes 的目标是将容器调度到工作节点上的Pod中。Kubernetes 中的 Pod 是一组一个或多个容器,每个容器都在一个 Pod 中运行。有了多个工作节点,你可以通过运行更多的 Pod(从而运行更多的容器)并将负载分配到多个工作节点之间,从而获得对应用程序如何提供给最终用户的最大控制。
那么,在什么类型的设备上运行 Kubernetes 进行测试和学习呢?对于这个问题,我们可以选择和 第一章、部署 Ubuntu Server 中讨论的相同选项——可以使用虚拟机、物理服务器、备用台式机或笔记本电脑,也可以使用树莓派(或这些设备的任意组合)。再次强调,使用你现有的设备即可。对于组织的部署,你可能会选择使用虚拟化服务器作为控制器和工作节点,或者使用物理服务器。说实话,我最喜欢的 Kubernetes 平台之一就是树莓派。我已经运行了一个生产环境中的 Kubernetes 集群,完全由树莓派 4 单元组成,而且取得了完全成功。如果没有其他选择,购买几台树莓派的成本相对较低。你甚至可以选择使用云服务提供商,尽管这样做超出了本章的范围,因为云服务提供商通常有自己的工具来管理集群。
一般而言,推荐拥有一个控制器节点和若干工作节点。一个工作节点就足够了,但在本章中,我将演示如何设置两个工作节点,以便更好地理解平台的扩展性。在你的环境中,你可以使用一个控制器和工作节点,或者根据需要设置任意数量的工作节点。Kubernetes 最棒的地方之一就是你不必固定最初设置的工作节点数量,随着时间推移,你可以向集群中添加更多节点。一个组织可能一开始只会有几个工作节点,但随着需求的增加,可以逐步添加更多节点。
就资源而言,Kubernetes 的要求相对较低。被指定为控制器的节点应至少具有两个 CPU 或一个 CPU,且至少有两个核心。显然,如果你有更多的 CPU,尤其是四核 CPU 来作为控制器,那会更好,但两个核心是最低要求。工作节点每个可以有一个 CPU 核心,但它们的核心越多,能够运行的 Pods 就越多。至于 RAM,我建议每个节点至少 2 GB 内存,但如果每个节点的内存更高,那会更好。
重要的是,集群中节点的 IP 地址不能发生变化。如果集群节点的 IP 地址发生变化,可能会导致节点无法互相找到。在 第十章、连接到网络 中,我们学习了如何设置静态 IP 地址,这是防止 IP 地址变化的好方法。作为替代方案,如果你愿意,可以在 DHCP 服务器中设置静态租约。无论你使用哪种解决方案,关键是要确保集群节点的 IP 地址不发生变化。
对于某些读者来说,更容易的设置方法是 MicroK8s,它甚至允许你在单台机器上设置 Kubernetes。如果你的唯一目标是设置一个简单的测试安装,它是最好的且最简单的入门方法之一。MicroK8s 不推荐用于在组织中运行生产集群,但它绝对是学习的好方式。我确实建议如果可以的话,按照标准的 Kubernetes 程序进行多节点设置,但在下一节中,我们将讲解如何设置 MicroK8s,供那些希望使用这种方法的你参考。
使用 MicroK8s
如果你没有多台机器,或者你的笔记本电脑或台式机内存不足以在虚拟化软件(如 VirtualBox)中运行多个节点,MicroK8s 是一个简单的方式来设置 Kubernetes 实例,用于测试平台并通过本章的示例。MicroK8s 实际上是由 Ubuntu 的开发商 Canonical 提供的。这也说明了 Kubernetes 对 Ubuntu 发行版的重要性,作为其创始人,Canonical 致力于为该平台做出贡献。MicroK8s 可用于 Linux、macOS 以及 Windows。因此,无论你在笔记本电脑或台式机上运行的是哪个操作系统,你都应该能够安装并使用它。如果没有其他目的,它至少能为你提供一个优秀的 Kubernetes 测试安装,帮助你学习。
要进行设置,请按照下面与电脑操作系统相匹配的子章节操作。
在 Linux 上安装 MicroK8s
对于 Linux,MicroK8s 作为 snap 包分发。我们在第三章《管理软件包》中介绍过 snap 包,它是一个很好的跨发行版软件包管理解决方案。如果你在电脑上运行的是较新的 Ubuntu 版本,那么你应该已经支持 snap 包,可以继续安装 MicroK8s。如果你在电脑上运行的是 Ubuntu 之外的 Linux 发行版,那么默认情况下可能没有 snap 命令。如果不确定,你可以使用 which 命令检查该命令是否可用:
which snap
如果你确实能够安装 snap 包,则输出应该类似于以下内容:

图 18.1:检查 snap 命令是否可用
如果你运行 which snap 时没有输出,说明你的发行版没有安装对这种包类型的支持。Canonical 将 snap 命令提供给了 Ubuntu 之外的发行版,因此,如果你使用的是其他发行版,通常只需要安装所需的软件包。
Canonical 在以下网站上提供了更多信息,展示了如何为多个发行版启用 snap 的过程:snapcraft.io/docs/installing-snapd。
例如,该网站的文档提供了以下命令来安装 Fedora 所需的软件包:
sudo dnf install snapd
本质上,只要按照与你所使用的 Linux 发行版相关的 snap 设置说明操作,整个过程应该足够简单。如果出于某些原因你的发行版不支持,你可以使用本书中已经介绍的相同 Ubuntu 安装版本,该版本已内置 snap 支持。
一旦你确认你的电脑已支持 snap,或者成功启用该功能,你可以使用以下命令安装 MicroK8s:
sudo snap install microk8s --classic
如果成功,你应该会在终端中看到确认消息,表明过程已成功完成:

图 18.2:在 Linux 安装中设置 MicroK8s
现在我们已经在你的 Linux 电脑上安装了 MicroK8s,可以继续本章内容。或者,你也可以查看下一部分,了解 macOS 的安装过程。
在 macOS 上安装 MicroK8s
在 macOS 上,安装 MicroK8s 的过程类似。Mac 上的安装将使用 Homebrew,这是 macOS 的一个附加工具。Homebrew 并不是 Kubernetes 或 MicroK8s 特有的,它让你能够在 Mac 上安装通常无法获得的额外软件包,MicroK8s 只是其中之一。Homebrew 本质上是一个命令行工具,语法类似于 Ubuntu 的 apt 命令。
要安装 Homebrew,请在浏览器中访问 brew.sh,所需的安装命令就在网站上。我可以在此页面直接插入安装 Homebrew 的命令,但安装过程可能会有所变化,因此最好从官方网站获取最新的命令。写这篇文章时,页面如下所示:

图 18.3:Homebrew 网站
在截图中,你会注意到命令以白色文字显示在黑色高亮的背景上。由于命令比这页宽,所以被截断了一些。你应该能在网站上看到完整的命令,可以从那里复制并粘贴到你的 Mac 终端中。
越来越多的应用开发者在其网站上提供直接复制到终端的命令来安装他们的应用程序。这非常方便,可以快速设置应用程序。但在将此类命令粘贴到终端之前,你应始终进行调查和检查(无论你的操作系统是什么)。有可能外部行为者劫持了网站上的命令,诱使你执行某些恶意操作。你可以使用 wget 命令下载其命令将要运行的脚本,检查其内容,确保它没有做恶意的事。由于这种软件部署方式越来越常见,我建议养成在运行命令之前检查命令的习惯(尤其是当命令使用了 sudo 时)。
一旦您在 Mac 上安装了 Homebrew,您可以使用以下命令继续通过 Homebrew 安装 MicroK8s:
brew install ubuntu/microk8s/microk8s
一旦该命令完成,您应该已经在您的 Mac 上安装了 MicroK8s。在下一个小节中,我们将看到 Windows 上相同的过程。
在 Windows 上安装 MicroK8s
如果您想在运行 Windows 10 的笔记本电脑或台式电脑上尝试 MicroK8s,可以使用一个专门的安装程序来帮助您安装和运行它。在 MicroK8s 网站上,您应该会看到一个大大的绿色按钮,上面写着 下载 MicroK8s for Windows,点击它后,您就可以下载安装程序:

图 18.4:MicroK8s 网站,包含下载 Windows 安装程序的按钮
下载完成后,您可以启动安装程序,它包含多个不同的部分,所有部分都可以选择默认选项。点击 下一步,然后点击 安装 按钮开始实际安装:

图 18.5:Windows 上的 MicroK8s 安装程序
安装开始后,另一个安装程序也会同时启动,要求您选择一个虚拟化管理程序(hypervisor):

图 18.6:Windows 上的 MicroK8s 安装程序,选择虚拟化管理程序
在这里,您可以保持默认选项为 Microsoft Hyper-V,然后点击 下一步。您可以保持其余选项为默认,并点击 下一步,直到出现任何剩余的提示。根据您的计算机配置,可能需要您重新启动才能完成所需组件的安装,然后才能完成安装过程。如果出现重新启动的提示,请按照提示操作,然后再次启动安装程序。
您将看到以下窗口,要求您设置 MicroK8s 的参数,具体包括使用多少虚拟 CPU、提供多少内存、磁盘空间等:

图 18.7:Windows 上的 MicroK8s 安装程序,分配资源
您也可以选择该页面的默认选项并点击 下一步。完成该过程后,点击 完成 按钮退出安装程序。
与 MicroK8s 进行交互
此时,如果您决定使用 MicroK8s,您应该已经在您的计算机上安装了它。如何与它进行交互取决于您的操作系统。在每种情况下,microk8s 命令(我们稍后会介绍)用于控制 MicroK8s。不同平台上用于输入 microk8s 命令的应用程序是不同的。
在 Windows 上,您可以使用操作系统内置的命令提示符应用程序。安装 MicroK8s 后,microk8s 命令可以被识别并使用。当您单独输入 microk8s 命令时,它应该会显示基本的使用信息:

图 18.8:在 Windows 命令提示符中执行没有选项的 microk8s 命令
对于 macOS,你可以使用操作系统自带的终端应用程序。不过,在你实际使用 MicroK8s 之前,可能需要完成另一个步骤。如果你输入 microk8s 命令并收到错误提示,说明 multipass 支持需要设置,你可以运行以下命令来修复它:
microk8s install
如果你感兴趣,Multipass 是 Canonical 也创建的一项技术,它允许你快速设置一个 Ubuntu 实例进行测试。它并不特定于 MicroK8s 或 Kubernetes,但在我们的案例中,它在后台用于促进 MicroK8s 的运行。Multipass 在本书中没有详细讲解,但如果你认为能从快速设置 Ubuntu 实例以测试应用程序和配置中获益,值得了解一下。它适用于所有主流操作系统。
对于 Linux,如果你已经完成了设置 MicroK8s 的过程,你应该能够立即开始使用它。打开你发行版的终端应用程序,尝试输入 microk8s 命令,看是否能被识别。如果可以,你就可以继续进行下一步了。
要检查 MicroK8s 的状态,可以使用以下命令来概览各个组件。在运行之前,请注意命令开头的 sudo。默认情况下,如果你的操作系统是 Linux,则需要使用 sudo。
如果你使用的是 Windows 11 或 macOS,则不需要 sudo。这个差异与底层操作系统如何处理权限有关,不同操作系统的处理方式不同。因此,如果你不是在 PC 上使用 Linux,完全可以省略 microk8s 命令中的 sudo:
sudo microk8s kubectl get all --all-namespaces
暂时不用担心 Kubernetes 中各个组件的细节,我们将在本章的后续部分介绍你需要了解的内容。目前,只要你在运行前面的命令并检查状态时没有出现错误,你就可以继续进行操作。
默认情况下,我们的 MicroK8s 安装仅包括运行所需的插件。我们可以启用其他插件,例如 storage,它使我们能够将主机(底层操作系统)上的路径暴露给 Kubernetes;gpu,它允许我们在容器中使用 NVIDIA GPU;还有其他插件。目前我们不需要担心这些插件,但至少应该启用 dns 插件,它会在我们的集群中设置 DNS。它不是必需的,但如果没有它,在后续进行名称解析时可能会出现问题,因此我们现在最好启用它:
sudo microk8s enable dns
你应该看到类似以下的输出:

图 18.9:在 Linux 安装中启用 MicroK8s 的 DNS 插件
在我的测试中,我发现启用插件的命令似乎会暂停一段时间,且没有输出。不要关闭窗口;它应该会继续执行,并最终显示有关安装插件过程的信息。如果发生这种情况,只需稍等片刻。
使用前面的命令,我们已经启用了dns插件。正如我提到的,还有其他插件可用,但现在这些就足够了。在这一点上,请记住,你可以通过插件扩展 MicroK8s,如果你愿意,可以在稍后深入探索。插件的列表会在本章末尾的链接中提供。
如前所述,如果你正在使用 Linux 运行 MicroK8s,那么你在之前使用的microk8s命令时需要使用sudo。如果你希望去掉 Linux 上使用sudo的要求,可以通过将你的用户添加到microk8s组来实现。你可以通过以下命令完成此操作:
sudo usermod -a -G microk8s <yourusername>
在你注销并重新登录后,你应该能够运行microk8s命令而无需使用sudo。
正如我们在接下来的章节中会发现的,kubectl命令通常用于与 Kubernetes 集群交互并管理它。kubectl,即Kube Control的缩写,是你用来执行许多集群任务的标准工具。我们稍后会进一步探讨它。但具体到 MicroK8s,重要的是要理解它使用的是自己特定版本的kubectl。因此,在 MicroK8s 中,你需要运行microk8s kubectl而不是单纯的kubectl来与集群交互。MicroK8s 有一个单独的kubectl实现,这让你可以针对实际的集群(直接使用kubectl命令)或专门针对你的 MicroK8s 安装(将kubectl命令前缀加上microk8s)进行操作,从而避免它们之间的冲突。在本章的实际示例中,我会单独提到kubectl,所以你需要记得在相应的命令前加上microk8s。
现在我们已经在自己的笔记本电脑或台式机上安装了 MicroK8s,我们可以继续本书中的示例。你可以跳到本章稍后的通过 Kubernetes 部署容器部分开始使用 Kubernetes。不过,在下一节中,我们将看看如何手动设置 Kubernetes 集群,而不使用 MicroK8s,这更接近实际生产环境中的操作流程。
设置 Kubernetes 集群
在前面的章节中,我们设置了 MicroK8s,它为我们提供了一个在单台机器上运行的 Kubernetes 集群,这对于测试目的非常有用。这可能就是你学习 Kubernetes 并了解它如何工作的全部所需。如果可以的话,我仍然建议你手动设置一个集群,这样你能更深入地了解各个组件如何协同工作。这正是我们在本节中要做的事情。
在我们开始之前,调整一下我们的思维状态是很重要的。在我们到目前为止的所有活动中,手动设置 Kubernetes 集群是最复杂的。Kubernetes 本身由许多组件和设置组成。如果任何一个组件不正确或设置错误,整个过程可能会失败。在这一节中,我们花了大量精力和注意力来确保(尽可能)该过程能够顺利进行,已经将其分解为只需的组件以简化一切。但是,如果在尝试时遇到问题,过程不顺利,不要担心——如果我们遇到问题,修复问题正是学习的最佳方式。所以最重要的是享受过程,保持耐心。
典型的 Kubernetes 集群是什么样子?在实际的数据中心生产安装中,将 Kubernetes 安装在多台服务器上是司空见惯的。通常,其中一台将作为控制节点,然后你可以根据需要添加任意多的工作节点。
随着你的需求扩展,你可以添加更多的服务器来提供更多的工作节点给你的集群。设置一个控制器 Kubernetes 节点,然后单独设置工作节点是一个很好的方式来看到实际关系的运作。而这正是我们将在本节中做的事情。
当我指导你完成这个过程时,我会利用三个虚拟机来做这件事。第一个将是控制器,剩下的两个将是工作节点。在你那边,你决定使用多少个工作节点并不重要。你可以只有一个工作节点,也可以有十几个——你想设置多少个都可以。如果你设置的节点比我多,那么你只需为额外的节点重复本节中的命令即可。关于你那边的节点,可以随意使用物理机或虚拟机的任何组合,只要符合你可用设备的资源。
在我们开始之前,你需要确保每台机器上都已经安装了 Ubuntu Server 并且安装了所有更新。如果你还没有为自己创建用户,现在也是个好时机。接下来的部分将假设你已经在每台机器上安装了 Ubuntu。在你安装完 Ubuntu 后,还是将每个节点的主机名配置好会更方便区分它们。例如,我打算将作为控制器使用的节点命名为 controller。对于工作节点,我将分别命名为 node-01 和 node-02。你可以根据自己的需求命名,但我特别提到这一点是为了确保你给它们起个名字,因为默认情况下它们在命令提示符中显示的主机名都一样,这可能会让人感到困惑。
现在你已经准备好了所有所需的东西,让我们来设置 Kubernetes 吧!
初步设置
在本节中,我们将在实际开始构建集群之前完成一些杂项设置任务。这些基本上是集群正常运行的前置条件。
我们首先需要确保每台计划用于集群的服务器都有一个静态租约或静态 IP 地址,并且该地址不会发生变化。你可以随时更改服务器的 IP 地址,但在使用 Kubernetes 时,如果你之后更改了 IP 地址,还需要执行一些额外的步骤。因此,我建议在我们构建集群之前,先设置静态租约或静态 IP 地址。这样,你以后就不需要担心这个问题了。这个过程在第十一章《设置网络服务》中有介绍,我在这里就不再重复了。请确保在你这一侧设置好静态租约或静态 IP 地址,然后继续我们的设置步骤。
接下来,如果你选择的设备中包括使用 Raspberry Pi 构建的服务器,那么有一些特定于 Pi 的特殊要求。如果你使用一个或多个 Raspberry Pi,请确保完成此步骤(如果你没有使用 Raspberry Pi,请跳过此步骤)。
要设置 Raspberry Pi 特定的启动参数,请在你选择的编辑器中打开以下文件:
sudo nano /boot/firmware/cmdline.txt
你在这个文件中将要做的,是添加一些 cgroup 选项来控制 Kubernetes 如何与设备的硬件交互。你需要将以下选项添加到文件中现有行的末尾。不要创建新行,而是将这些选项添加到该文件中第一行(且唯一一行)的末尾:
`cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1 swapaccount=1`
保存并退出文件,这应该是我们针对 Raspberry Pi 所做的唯一特定更改。
为了构建 Kubernetes 集群,我们需要设置一个叫做 容器运行时 的东西。容器运行时是 Kubernetes 在集群内运行容器的方式。如果没有运行时,Kubernetes 甚至无法运行一个容器。虽然 Kubernetes 是一个容器编排解决方案,但它并不强制要求你使用哪种容器运行时。推荐的运行时是 containerd,所以我们将在我们的集群中使用它。其他容器运行时包括(但不限于)Docker、CRI-O 等。我们在本书中已经使用过 Docker,但对于我们的集群,我们将使用推荐的 containerd 运行时。
至于我们可以运行的容器,实际上没有区别——例如 containerd 可以像我们之前做的那样运行 Docker 容器。因此,尽管我们将选择不同的容器运行时,但最终结果应该不会有任何区别。
在这一部分,我将让你运行的命令应该在你计划与 Kubernetes 一起使用的每个实例上执行,无论你是在操作目标控制器还是节点;这些命令需要在每个实例上运行。后续会有针对控制器和节点的特定命令,但如果我希望你只对其中一个执行,我会提到目标。
我们要做的第一件事是安装containerd包:
sudo apt install containerd
包安装完毕后,我们将检查它的状态,确保它正在运行:
systemctl status containerd
我们应该会看到单元正在运行:

图 18.10:检查 containerd 运行时的状态
不过,我们还没完全完成containerd的配置。它需要一些默认配置才能正常工作,但默认情况下并不会自动提供示例配置。让我们从为服务器添加一个目录来存放配置文件开始,创建这个配置文件:
sudo mkdir /etc/containerd
接下来,我们将使用以下命令为containerd创建配置文件:
containerd config default | sudo tee /etc/containerd/config.toml
那个命令不仅会为containerd创建默认配置文件,还会将配置内容打印到屏幕上,以便我们查看默认值是什么。然而,尽管大部分默认设置都可以使用,我们还是需要对这个文件做一些调整。打开你喜欢的编辑器,并编辑/etc/containerd/config.toml文件。
对这个文件的第一个更改是将 cgroup 驱动程序设置为systemd。为此,查找以下行:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
在几行之后,你会看到以下这一行:
SystemdCgroup = false
将该行更改为true:
SystemdCgroup = true
到目前为止,我们暂时完成了containerd的配置。接下来,让我们继续配置一些重要的系统调整。第一个调整就是禁用交换分区。虽然通常不建议在没有交换分区的情况下运行服务器,但设置 Kubernetes 集群是一个例外。事实上,当我们稍后初始化集群时,如果交换分区已启用,过程会中止。所以现在我们就来禁用它。
首先,我们将运行以下命令来禁用交换分区:
sudo swapoff -a
做完这些之后,如果我们运行free -m,我们应该会看到交换分区的值全为零:

图 18.11:运行 free -m 命令检查交换分区是否已禁用
然而,我们刚刚运行的禁用交换分区的命令并不是永久性的更改。当我们重启服务器时,交换分区会自动重新启用。为了彻底禁用交换分区,我们需要编辑/etc/fstab文件,并注释掉启用交换分区的那一行。
下图显示了交换分区的一个示例配置行,但它通过在该行前添加#字符被注释掉了:

图 18.12:/etc/fstab文件,交换分区行已被注释掉
此时,swap 应该已关闭,考虑到我们已在/etc/fstab文件中注释掉了 swap 行,下一次启动服务器时 swap 将不会重新启用。
接下来,我们需要对/etc/sysctl.conf文件进行更改以启用桥接。这类似于我们在第十一章,设置网络服务中配置互联网网关时所做的工作。一旦我们在编辑器中打开/etc/sysctl.conf文件,我们需要取消注释以下行:
#net.ipv4.ip_forward=1
它现在应该是这样的:
net.ipv4.ip forward=1
在取消注释该行后,下一次启动服务器时应该启用桥接。
接下来,我们将创建以下文件,用于确保在服务器启动时加载必需的内核模块:
sudo nano /etc/modules-load.d/k8s.conf
在该文件内,我们将添加以下内容:
br_netfilter
这实际上就是这个文件的全部内容,所以保存并退出编辑器。br_netfilter模块有助于为我们最终的 Kubernetes 集群提供网络支持,并且需要启用才能使集群正常运行。通过创建/etc/modules-load.d/k8s.conf文件,我们确保在启动服务器时此内核模块会自动加载。
在我们继续之前,先花点时间确保到目前为止我们已完成所有需要做的事情。此时,在每台你打算与 Kubernetes 一起使用的服务器上(包括控制器和节点),你应该已经完成了以下工作:
-
安装了所有更新
-
设置了主机名
-
在每个节点上设置静态 IP 或租约
-
设置特定于 Raspberry Pi 的启动选项(如果你使用的是 Raspberry Pi)
-
安装了
containerd -
创建了
/etc/containerd/config.toml文件用于containerd,并设置了 cgroup 驱动程序 -
禁用了 swap
-
添加了
/etc/modules-load.d/k8s.conf文件,文件内列出了br_netfilter模块 -
编辑了
/etc/sysctl.conf文件以启用桥接
上述所有内容应该在你计划用于集群的每台服务器上完成。一旦确认已经完成这些任务,我们应该重启每个节点:
sudo reboot
一旦节点完成重启,我们就可以继续安装 Kubernetes 了。
安装 Kubernetes
现在终于到了开始构建集群的时候,我们将首先安装所需的软件包。为此,我们需要添加适当的仓库,以便能够访问安装 Kubernetes 所需的软件包。要添加仓库,我们将首先添加仓库的密钥,以便我们的服务器将其视为受信资源:
sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
要添加实际的仓库,我们将运行以下命令:
echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
添加仓库后,我们将更新本地索引:
sudo apt update
你可能会注意到,仓库的 URL 引用的是xenial,而不是 Ubuntu 22.04 的实际代号jammy(简写为 Jammy Jellyfish)。在写这篇文章时,还没有专门为 Ubuntu 22.04 提供的 Kubernetes 仓库,但这个过程依然可以顺利进行。可能在你读这篇文章时,已经会有专门为 22.04 提供的仓库了。但目前,我们可以在我们的仓库文件中使用上面提到的这一行。
接下来,我们可以安装 Kubernetes 所需的包:
sudo apt install kubeadm kubectl kubelet
我们正在安装三个包,kubeadm、kubectl和kubelet:
-
kubeadm:
kubeadm包为我们提供了可以用来“引导”集群的工具。我们可以使用这个工具初始化一个新的集群,加入一个节点到现有的集群中,或者将集群升级到更新的版本。 -
kubectl:这个包为我们提供了 Kubernetes 命令行工具
kubectl。我们将使用这个工具与我们的集群进行交互和管理。 -
kubelet:Kubernetes 的
kubelet作为一个代理,促进节点之间的通信。它还公开了可以用于启用额外通信和功能的 API 端点。
与之前我们在设置集群时执行的所有命令不同,以下命令将仅在你指定的控制节点上输入和运行(在运行之前,请阅读接下来的段落,了解如何自定义该命令):
sudo kubeadm init --control-plane-endpoint=172.16.250.216 --node-name controller --pod-network-cidr=10.244.0.0/16
之前的kubeadm init命令需要在执行之前稍作自定义。有几个地方需要你更改,以确保它们与你的环境匹配。实际上,我已经将你应该自定义的设置加粗了。我现在也将描述这些特定设置的更多细节。
--control-plane-endpoint=172.16.250.216:对于这个选项,这里提到的 IP 地址应该与你的服务器分配的 IP 地址相同。我填入的是我测试服务器使用的值,但你应该确保这个值与你实际的 IP 地址匹配。由于这是一个非常具体的设置,这也是我建议你在将节点加入集群之前先确定好节点的 IP 地址的原因之一。
--node-name controller:在设置我的测试服务器时,我将其主机名简化为controller。在你的环境中,你可以将其设置为与你的控制节点主机名匹配。
请注意,我没有加粗命令中的第二个 IP 地址10.244.0.0/16。这样做的原因是,你不应更改这个特定的 IP 声明。它实际上是Pod 网络的 IP 方案,这是 Kubernetes 的内部网络,外部无法访问。你当然可以将其自定义为自己的网络方案,但那样的话你还需要更改其他设置。由于这是一个专用的内部网络,通常不需要更改它,因此我建议保持原样。
我之前提到的kubeadm init命令,在你自定义之后,将初始化集群并为其分配一个 Pod 网络。如果初始化过程成功,你会在终端的最后看到一个join命令。如果你看到了它,那么恭喜你!你已经成功搭建了一个 Kubernetes 集群。虽然它现在还不是一个非常可用的集群,因为此时我们还没有添加任何工作节点。但从定义上来说,现在你已经拥有了一个 Kubernetes 集群。在我的控制节点上,我看到以下输出:

图 18.13:Kubernetes 集群成功初始化,显示一个 join 命令
过程完成后显示给你的join命令是非常特别的——你可以将这个命令复制并粘贴到工作节点的命令提示符中,以将其加入集群,但现在先不要这么做。此时,请复制这个命令并将其保存在一个安全的地方。你不希望其他人看到它,因为它包含一个特定于你集群的哈希值,允许他人将节点加入到该集群。技术上讲,显示我的join命令及哈希值是我在一本会被许多人看到和阅读的书中做的最后一件事,但由于这只是一个没有实际价值的测试集群,我不介意你看到它。
此外,在join命令的同一输出中,还有三个额外的命令,我们应该在控制节点上运行。如果你滚动终端窗口,回到join命令之前的位置,你应该看到类似以下的输出:

图 18.14:Kubernetes 初始化过程的输出,显示要运行的附加命令
对于这三个命令,你可以直接将它们逐个复制并粘贴回终端。请继续操作。实际上,你所做的事情是为kubectl创建一个本地配置目录,放在你的用户主目录下。然后,你会从/etc/kubernetes复制admin.conf文件,并将其存储在你主目录中新创建的.kube文件夹中,文件名改为config。最后,你会将.kube目录的所有权更改为你的用户账户所有。这将允许你的用户账户通过kubectl命令管理 Kubernetes,而无需以root身份登录或使用sudo。
Kubernetes 本身由多个Pods组成,你的容器将在其中运行。我们还没有部署任何容器,我们稍后会进行。但即便我们自己没有部署任何容器,实际上已经有一些容器在运行。你可以亲自验证一下:
kubectl get pods --all-namespaces
在我的端,我看到以下内容:

图 18.15:检查我们 Kubernetes 集群中正在运行的 Pods 状态
注意,这些 Pods 的命名空间是kube-system。这是一个特殊的命名空间,其中运行与 Kubernetes 集群本身相关的 Pods。我们并没有明确要求这些 Pods 运行,它们是作为集群的一部分运行,并提供必要的功能。另一个重要的列是STATUS。在截图中,我们看到大多数 Pod 处于Running状态。不过,前两个 Pods 处于Pending状态,这意味着它们还没有准备好。这其实是预期中的情况,因为我们还没有部署覆盖网络,而coredns Pods 的运行需要这个网络。
覆盖网络是虚拟网络,它是在另一网络之上创建的,用作一个网络。这个概念不仅仅适用于 Kubernetes,但在 Kubernetes 中,覆盖网络管理节点之间的通信。
其他 Pods 也可能显示为Pending状态,当它们完成设置后,应该会自动切换为Running状态。这个过程所需的时间取决于硬件的速度,但通常只需几分钟。
其他节点将在设置完成后最终变为Running状态——但它们也可能会崩溃并重新启动。由于还没有配置覆盖网络,Pods 之间还无法完全通信。因此,如果看到一些错误,不用担心。
让我们部署一个覆盖网络,以便让集群更加稳定:
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
当我们再次检查状态时,应该能看到一个额外的 Pod 在运行(kube-flannel-ds),之前处于Pending状态的 Pods 应该现在是Running状态:

图 18.16:再次检查 Kubernetes 集群中运行的 Pods 状态
那么,我们到底做了什么呢?Flannel是一个可以在 Kubernetes 上运行的网络层。网络是 Kubernetes 正常运行所必需的,因为集群中的 Pods 需要能够相互通信。Kubernetes 中可以实现多种不同的网络模型,而 Flannel 只是其中一个可用的选项。如果你使用的是云服务提供商,如 AWS,通常会为你选择网络模型。由于我们是从零开始构建集群,因此必须选择一个网络模型——Kubernetes 本身并不附带网络模型。
在手动创建的集群中设置网络可能是一个复杂的任务,Flannel 的设置很简单(我们只需要部署它),而且它的默认配置能够满足 Kubernetes 的需求,并让我们快速启动。虽然还有其他网络层的选择,但 Flannel 对于我们当前的需求已经足够好了。
接下来,到了见证奇迹发生的时候,将工作节点加入集群。我们可以使用以下命令检查所有节点的状态:
kubectl get nodes
目前,我们输出中只显示了控制器,因为我们还没有添加任何节点:

图 18.17:检查集群中节点的状态
要将一个工作节点添加到我们的集群中,我们可以在指定为工作节点的节点上输入我们之前看到的join命令。如果你还记得,在我们首次初始化集群时,终端会显示join命令。命令大致如下,我已稍微缩短以适应本页(在开头添加了sudo):
sudo kubeadm join 172.16.250.216:6443 --token zu5u3x.p45x0qkjl37ine6i \
--discovery-token-ca-cert-hash sha256...1360c
你应该将加入命令视为私密信息,不能展示给任何人,也不应将其上传到 Git 仓库或文档服务器。原因是,它可能会被外部人员用来将某些东西加入到你的集群中。就我而言,我截断了哈希值,因此无法复制,但请记住这一点。
对你来说,命令会非常不同。命令中的 IP 地址是控制节点的 IP,显然你这边会有所不同。哈希值也会不同。基本上,只需复制你在初始化集群时在控制节点上获得的join命令,并将其粘贴到每个工作节点中。你应该能在输出中看到节点已成功添加到集群的消息:

图 18.18:成功将一个工作节点添加到 Kubernetes 集群中
在你在每个工作节点上运行了加入命令(无论你决定创建多少个工作节点)之后,你可以再次在控制节点上运行kubectl get nodes命令,验证新节点是否出现在列表中。我添加了两个节点,所以我在我的端看到以下输出:

图 18.19:kubectl的输出显示工作节点已添加到集群中
一旦你计划部署的所有节点都显示STATUS为Ready,那么你的集群设置就完成了!
如果你在将节点加入集群时遇到任何问题,可以尝试重新生成加入令牌。经过一段时间,原始令牌可能会过期,证书也将失效。要重新生成加入命令,可以从控制节点运行以下命令:
kubeadm token create --print-join-command
这将打印一个全新的加入命令,你可以用它来替代原始命令。
此时,我们的集群已存在,并且至少有一个工作节点准备好为我们服务。下一步是实际使用集群并部署一个容器。这正是我们将在下一节中做的。
通过 Kubernetes 部署容器
现在是时候看到我们的工作成果了,我们可以成功地使用我们创建的集群。在这一点上,你应该已经设置好了 MicroK8s,或者像我们在上一节中所做的那样手动创建了一个集群。无论哪种情况,结果是相同的:我们有了一个可以用来部署容器的集群。
请记住,如果你使用的是 MicroK8s,根据你设置 MicroK8s 的方式,可能需要在kubectl命令前添加microk8s。如果你使用 MicroK8s 且没有简化microk8s kubectl为kubectl,就需要自己加上microk8s。
Kubernetes 使用以 YAML 格式创建的文件来接收指令。这听起来熟悉吗?在 第十五章,使用 Ansible 自动化服务器配置 中,我们使用了 YAML 文件,因为 Ansible playbook 就是以这种格式编写的。YAML 并不局限于 Ansible,它被许多不同的应用和服务使用,Kubernetes 也会识别 YAML 格式,以此作为部署指令的载体。下面是一个示例 YAML 文件,帮助我们入门。这个文件特别用于在集群中启动一个 NGINX 容器:
apiVersion: v1
kind: Pod
metadata:
name: nginx-example
labels:
app: nginx
spec:
containers:
- name: nginx
image: linuxserver/nginx
ports:
- containerPort: 80
name: "nginx-http"
在我展示如何运行之前,让我们先浏览一下这个文件,了解其中的内容。
apiVersion: v1
kind: Pod
首先,我们确定了我们打算使用的 API 版本,然后设置了我们要部署的 kind 类型。在这种情况下,我们部署的是一个 pod,即 Kubernetes 中容器运行的环境。一个 Pod 可以运行一个或多个容器,而一个工作节点可以同时运行一个或多个 Pod。
metadata:
name: nginx-example
labels:
app: nginx
接下来,我们添加一些元数据。元数据使我们能够设置特定于部署的特殊参数。我们自定义的第一个元数据项是 name,在这种情况下,我们将 Pod 命名为 nginx-example。我们还可以使用元数据设置标签,标签是一个“名称:值”键值对,它允许我们为 Pod 添加额外的值,以便稍后引用。在此情况下,我们创建了一个名为 app 的标签,并将其设置为 nginx。这个名称是任意的;如果我们愿意,也可以将其命名为 potato。将其设置为 nginx 是一个描述性标签,能够让其他人清楚地知道我们打算在此运行什么。
spec:
containers:
- name: nginx image: linuxserver/nginx
接下来,spec 部分允许我们指定希望在 Pod 中运行的内容以及运行方式。我们希望在 Pod 中运行一个容器,具体来说是一个我们将命名为 nginx 的容器,该容器来自名为 linuxserver 的镜像仓库,我们通过该仓库请求名为 nginx 的容器。
我们从中获取容器镜像的注册表值得额外说明一下。这个特别的注册表位于 linuxserver.io,它是一个专门的服务,提供可供我们下载和使用的容器镜像。该网站有一个文档部分,向我们提供关于他们提供的每个容器镜像的信息。为什么要使用 linuxserver.io 注册表?原因是他们的服务提供了各种支持多种架构的容器镜像,包括 x86 和 ARM。后者尤其重要,因为如果你使用的是 Raspberry Pi 单元作为集群节点,它们将无法使用为 x86 创建的容器镜像。如果你尝试运行不支持 ARM 的容器镜像,那么容器将在 Pi 上启动失败。由于 linuxserver.io 提供支持多种架构的容器镜像,无论你决定使用哪种设备来构建集群,它们都应该可以正常工作。无论你是在使用 x86 物理服务器还是虚拟机作为工作节点,我们从该注册表中获取的 nginx 容器应该都能正常运行。
ports:
- containerPort: 80
name: "nginx-http"
在接下来的几行中,我们正在设置一个 80 的容器端口,这是 Web 服务器的标准端口。当 NGINX 运行时,它会监听此端口,而 NGINX 就是我们打算在容器内运行的服务。我们给这个端口声明赋予了一个名称,叫做 nginx-http。我们可以在随后的 YAML 文件中引用该名称(如果我们有多个文件),而这样比每个文件中都输入相同端口要更为高效。通过名称引用端口,与脚本或编程语言中的变量类似,并不算特别复杂。
在继续之前,有一点关于如何在 MicroK8s 集群中部署资源与在实际服务器上手动设置的集群中部署资源的差异。接下来的命令假设你已经手动设置了集群。如果你在 Windows 或 macOS 上使用 MicroK8s,你需要将任何你创建的部署文件复制到作为 MicroK8s 安装的一部分创建的 MicroK8s 虚拟机中。如果你试图将文件保存在本地并像接下来的段落中那样进行部署,操作将失败,因为该文件在虚拟机上无法找到。要将部署文件复制到 MicroK8s 虚拟机并使其能够部署,你可以使用以下命令将文件先复制到虚拟机:
multipass transfer <filename.yml> microk8s-vm:
在我们进行操作时,请记住,每当我们部署文件时,首先需要确保将其传输过去。另外,手动创建的集群使用 kubectl 命令,而 MicroK8s 需要将 kubectl 命令前缀加上 microk8s,因此此类命令应变为 microk8s kubectl,而不仅仅是 kubectl。
现在我们可以开始将容器部署到集群中了。假设你将 YAML 文件命名为 pod.yml,你可以在控制节点上使用以下命令进行部署:
kubectl apply -f pod.yml
如前所述,kubectl命令允许我们控制集群。-f选项接受文件作为输入,我们指向的是我们创建的 YAML 文件。
一旦你运行了这个命令,你就可以使用以下命令检查部署的状态:
kubectl get pods
对我来说,当我运行时,我看到如下内容:

图 18.20:检查容器部署的状态
从我的输出中,我可以看到这个过程是成功的。STATUS显示为Running。我们没有看到的是 Pod 运行在哪个工作节点上。虽然知道它是Running很不错,但我们想知道它在哪儿运行?我们可以通过在命令末尾添加-o wide选项来获得更多信息:
kubectl get pods -o wide
输出包含更多信息,信息量大到截图无法完全显示在这页上。不过,重点是,在这个命令版本的额外字段中,有一个node字段,它显示了哪个工作节点正在处理这个部署。我们还会看到分配给 Pod 的 IP 地址。
我们可以使用显示的 Pod 的 IP 地址来访问运行在容器内的应用程序。在我的例子中,我的 Pod 被分配了10.244.1.2的 IP 地址,因此我可以使用curl命令访问默认的 NGINX 网页:
curl 10.244.1.2
在终端运行命令后的输出应该是默认 NGINX 网页的 HTML。请注意,curl命令可能在你的 Ubuntu 安装中不可用,因此你可能需要先安装所需的软件包:
sudo apt install curl
不过我们有一个潜在的问题:如果你希望能够从集群外的机器访问运行在集群内部的应用程序,那是不行的。默认情况下,没有任何机制可以将来自你局域网的流量路由到你的集群。这意味着在我的例子中,我的 Pod 被分配的10.244.1.2的 IP 地址是由 Pod 网络提供的,而我网络中的路由器并不理解这个 IP 地址方案,所以试图从局域网的其他机器访问它将会失败。有趣的是,你可以从集群内的任何其他节点访问该应用程序。在我的例子中,NGINX Pod 运行在第一个工作节点上。但是,即使 Pod 没有运行在那儿,我实际上也可以从工作节点#2 运行先前的curl命令,而且我会得到完全相同的输出。这是因为 Pod 网络是集群范围的;它不是特定于某一个节点的。
10.244.1.2的 IP 地址对于整个集群来说是唯一的,所以如果我运行另一个容器,它不会得到相同的 IP 地址,而且集群中的每个节点都知道如何将流量内部路由到该网络中的 IP。
不允许外部设备访问集群内的应用程序对安全性来说非常重要。毕竟,黑客无法侵入他们连路由都无法到达的容器。但是运行 Kubernetes 集群的主要目的是让应用程序可以在我们的网络上访问,那我们该怎么做呢?对于新手来说,这可能是一个非常混乱的话题,特别是当我们手动设置集群时。如果我们使用像 AWS 或 Google Cloud 这样的云服务,它们会在其 Kubernetes 实现上添加一层额外的功能,便于在集群内外路由流量。由于我们是手动搭建集群的,因此没有类似的机制。
当涉及到构建旨在处理我们局域网与 Kubernetes 集群之间网络连接的服务和网络组件时,这是一个广泛的话题,可能需要几章内容来讲解。但对于我们来说,一个简单的解决方案是创建一个NodePort 服务。这里有两个新概念,一个是服务(Service),另一个是 NodePort。对于服务(Service)来说,Pod 并不是我们在集群中唯一可以部署的内容。我们可以部署几种不同的东西,而服务是一种暴露 Kubernetes Pod 的方法,NodePort 则是一种特定类型的服务,它为我们提供了一种特定的方式来访问 Pod。NodePort 的作用是将 Pod 内部运行的端口暴露到每个集群节点上的端口。
这是一个可以部署的文件,用于为我们的 Pod 创建一个 NodePort 服务:
apiVersion: v1
kind: Service
metadata:
name: nginx-example
spec:
type: NodePort
ports:
- name: http
port: 80
nodePort: 30080
targetPort: nginx-http
selector:
app: nginx
如你所见,kind是Service;这次我们正在将服务部署到集群中,以补充我们之前部署的 Pod。服务的type是NodePort,我们将 Pod 中的端口80映射到集群中的端口30080。我随意选择了端口30080。使用 NodePort 时,我们可以使用端口30000到32767。此文件中的selector设置为nginx,这与我们在创建 Pod 时使用的选择器相同。选择器允许我们通过给 Kubernetes 资源分配一个名称来“选择”它们,从而方便我们以后引用它们。
让我们部署我们的服务:
kubectl apply -f service-nodeport.yml
假设我们已正确输入了 YAML 文件中的所有内容,可以使用以下命令检索在集群中运行的服务的状态:
kubectl get service
如果一切顺利,你应该会看到类似以下的输出:

图 18.21:检查服务部署的状态
在输出中,我们应该看到service部署的状态,这是屏幕截图中的第二行。我们还可以看到它的端口映射,显示端口30080应该暴露到外部。为了测试它是否正常工作,我们可以在我们局域网中的一台机器上打开 Web 浏览器,它应该能够访问控制器的 IP 地址,并使用端口30080:

图 18.22:通过 Web 浏览器访问集群中 NGINX 容器提供的网页
另一个有趣的好处是,我们实际上并不需要使用我们控制节点的 IP 地址,我们也可以使用工作节点的 IP 地址,并查看相同的默认页面。 这样做的原因是,Pod 内部将端口30080映射到集群范围内的端口80,就像内部 Pod 网络也是集群范围的一样。 访问一个资源与访问任何其他资源是一样的,因为请求将被定向到运行与匹配请求的 Pod 的任何节点。
当涉及到移除 Pod 或服务时,假设您想要将运行在您集群中的某些东西下架,那么删除的语法相对来说非常简单。 例如,要删除我们的nginx-example Pod,您可以运行以下命令:
kubectl delete pod nginx-example
同样地,要删除我们的服务,我们可以运行以下命令:
kubectl delete service nginx-example
此时,您不仅拥有一个工作的集群,还可以部署容器,并设置它们可以从 Pod 网络外部访问。 从这里开始,我建议您稍微练习一下,并尝试运行额外的容器和应用程序,以便稍微玩一下。
总结
在本章中,我们将容器化提升到了一个新的水平,并实施了 Kubernetes。 Kubernetes 为我们的容器提供了编排功能,使我们能够更智能地管理我们运行的容器并实现服务。 Kubernetes 本身是一个非常广泛的主题,我们可以探索许多其他功能和优势。 但是,为了在 Kubernetes 上进行设置并运行容器的目标,我们做了我们需要做的事情。 我建议您继续学习 Kubernetes 并扩展您的知识,因为这是一个非常值得深入挖掘的主题。
谈到值得学习的主题,在下一章中,我们将学习如何在云中部署 Ubuntu! 具体来说,我们将开始使用 Amazon Web Services,这是一个非常流行的云平台。
相关视频
- 设置 Kubernetes 集群(LearnLinuxTV):
linux.video/setup-k8s
进一步阅读
-
MicroK8s 网站:
learnlinux.link/mk8s -
MicroK8s 插件:
learnlinux.link/mk8s-addons
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第十九章:在云端部署 Ubuntu
到目前为止,在每一章中,我们一直在使用安装在本地虚拟机、物理计算机或服务器上的 Ubuntu 实例,甚至是 Raspberry Pi。我们已经学会了如何在这些设备上部署 Ubuntu,甚至还深入到虚拟机和容器的部署。这些本地设备一直为我们提供了良好的服务,但云计算的概念已经变得非常流行,尤其是自本书上一版以来。在本章中,我们将探索如何在云端运行 Ubuntu。具体来说,我们将部署一个 Ubuntu 实例在亚马逊云服务(AWS)上,这是一种非常流行的云计算平台。虽然我们不会详细讲解 AWS(它是一个庞大而复杂的平台),但你一定会对在云端部署资源的感觉有所了解,这对你入门已足够。
本次探索将涉及以下主题:
-
理解本地基础设施与云基础设施的区别
-
在考虑将云计算作为潜在解决方案时需要注意的重要事项
-
熟悉一些基础的 AWS 概念
-
创建 AWS 账户
-
选择区域
-
部署 Ubuntu 作为 AWS EC2 实例
-
创建并部署 Ubuntu AMI
-
使用自动扩展实现 Ubuntu EC2 部署的自动扩展
-
降低成本:了解如何节省开支并做出具有成本效益的决策
-
进一步探索云计算:额外的资源来扩展你的知识
由于云计算的思维方式与我们习惯的方式有所不同,我们首先将介绍一些章节,帮助我们理解这种差异,以及在选择将 Ubuntu 实现到云端之前应考虑的一些因素。在下一节中,我们将探讨云计算的概念如何与本地硬件有所不同。
理解本地基础设施与云基础设施的区别
正如本章开头所提到的,我们至今一直仅使用本地安装的 Ubuntu。即使我们在数据中心的虚拟机上运行 Ubuntu,它仍然被视为本地安装,即使它不是运行在物理硬件上。简而言之,本地安装是指在我们本地运行的安装,无论其基础服务器类型如何。
在云计算中,第一个区别可能是比较显而易见的:它正好与资源本地化的概念相反。使用云实例的 Ubuntu 时,实际上是在别人的硬件上运行。大多数时候,我们并不知道云实例运行在什么样的服务器上——当我们订阅云服务提供商的服务并支付费用来在该平台上运行服务器时,我们就像使用虚拟机一样访问操作系统,几乎不需要了解底层数据中心的情况。尽管使用云实例会有一些持续的费用,但我们不需要担心监控硬件组件或更换损坏的物理设备。对于云服务来说,这些问题由别人来解决。
你可以选择各种云服务提供商,其中AWS、Google Cloud Platform(GCP)和Microsoft Azure是比较流行的选择。这三大平台都非常流行。它们各自有优缺点,甚至在平台上资源管理方面有所不同。在大多数情况下,这里没有绝对对错的选择,重要的是研究每个平台并为你和你的组织做出合适的决定。
除了云服务提供商之外,还有虚拟私人服务器(VPS)提供商。以前,完整的云服务提供商和 VPS 提供商之间有较大的区别——但随着时间的推移,这两种平台之间的界限越来越模糊。大多数情况下,VPS 提供商是标准云服务提供商的较轻或“简化”版。它们提供相同的核心功能,例如允许你构建虚拟机——但通常不像完整的云服务提供商那样提供那么多功能。如今,VPS 提供商正在定期推出新功能,并在功能方面赶上了云服务提供商。流行的 VPS 提供商包括DigitalOcean和Linode等。实际上,连亚马逊自己也进入了 VPS 业务,提供一种名为Amazon Lightsail的 VPS 替代方案。
对于你的组织,你应该在完全云提供商和 VPS 提供商之间选择哪一个?许多管理员会毫不犹豫地回答这个问题,并推荐使用完全云提供商。我当然能理解他们的观点——为什么要选择一个“轻量级”平台,而可以使用一个具备所有功能的解决方案呢?然而,根据我与许多组织的经验,我不建议在没有首先确认 VPS 提供商是否拥有你所需要的所有功能的情况下选择完全云提供商。原因在于,拥有更多功能的权衡之一就是复杂性增加。复杂性的增加意味着你需要承担更多的责任来管理该平台上的资源。而这种复杂性可能会带来非常大的管理开销。因此,我不建议像 AWS 或 Azure 这样的完全云提供商给 IT 人员有限的小型组织。对于这些组织,VPS 提供商是最合适的选择,因为管理负担较小。对于有一支管理员团队的大型组织来说,处理像 AWS 这样的服务不成问题。
正如我提到的,我们将在本章中讨论 AWS。即使你没有考虑将资源迁移到云上,练习这项技术仍然是值得的。至少,你可以对另一个平台有一些熟悉,这将增加你的技能和市场竞争力。而且我不知道你怎么样,但就我个人而言,我真的很喜欢了解新技术和学习新的东西。
然而,也许我有些超前了。在你将任何资源迁移到云之前,现在最重要的问题是,创建云资源是否符合你的目标。这正是我们将在下一部分探讨的内容。我们还将探讨云计算中的一些思维方式差异。
考虑云计算作为潜在解决方案时的重要事项
在选择注册某个提供商之前,首先要确保创建云资源对你或你的组织来说是一个好主意。IT 专业人员常常会对新兴趋势感到兴奋,以至于可能会犯错,尝试使用这样的服务,即使这样做并不合适。最重要的是,作为一名管理员,应该利用最合适的工具来完成你想做的事情,而不是仅仅因为对某项技术感到兴奋就去使用它。云计算无疑是很棒的,但对于某些使用场景来说,它并不合适。这与容器化类似:容器化是一项令人兴奋的技术,但某些应用程序在这个平台上运行并不好。它需要经过反复试验。
云计算有一些显著的好处。就物理服务器而言,硬件最终会出现故障。这不是“是否”会发生的问题,而是“何时”发生的问题。所有硬件最终都会故障。即使你拥有的硬件短期内没有发生故障,并且能够持续很长时间,它也会被更强大、更高效的硬件所淘汰。管理物理服务器时,最终需要更换硬件。
这对于云计算也是适用的;这些服务运行的硬件在发生故障或变得过时时(无论哪种情况先发生)都需要被替换。不同之处在于,当这种情况发生时,你(作为管理员)的责任大大减少。你无需再订购新服务器,替换零部件,甚至无需关注硬件。保持硬件运行的责任完全由云服务或 VPS 提供商承担。你在云平台上运行的虚拟机可能运行在一台已经使用了几年时间的物理服务器上,第二天可能会被迁移到一台全新的硬件上。你甚至可能没有意识到它曾经被迁移过。
然而,权衡的关键可能在于成本。我之所以将成本作为一个“潜在”的权衡因素提到,是因为你是否选择购买物理服务器还是支付每月的云服务器费用,最终取决于哪个方案能带来更好的投资回报率(ROI)。为了更好地理解这一点,可以考虑一个只使用本地硬件的数据中心的组织,完全不使用云资源。假设每 3 年左右,他们必须用更先进的硬件替换一些关键的服务器;而每 5-10 年,替换不太重要的服务器。还需要支付全职管理员的薪资,这些管理员需要具备管理物理硬件的专长,并维护冷却系统,确保有房间可作为数据中心使用。同时,还需要考虑自己运行数据中心的电费。将这些费用加总起来——这些开销平均会给组织带来多大成本?
对于物理基础设施来说,每次需要服务器时,你都需要支付一次性的费用来购买服务器。而云计算则不需要你购买物理服务器硬件,而是每个月支付费用,以便能够使用云基础设施。当你把云资源的月度费用加起来时,这样的开销会对组织造成多大负担?它的成本会比运行物理硬件高还是低?
通常,是否在云中运行服务器能节省成本(或最终变得过于昂贵)取决于你希望在该平台上使用的服务类型。在云中运行更多虚拟机通常是相对便宜的,并且与运行自己的物理基础设施相比,可能会带来成本节省。然而,云中可能会变得过于昂贵的一个例子就是存储。
如果你的组织存储的数据量不大,那么存储成本就不会是个问题。许多公司完全满意让员工使用 Google Drive 之类的共享存储,甚至根本不需要云存储。一些组织,尤其是软件开发公司,拥有庞大的存储需求。对于这些公司来说,可能需要存储数十甚至数百 TB 的数据,以及数据进出公司的带宽费用。如果你试图将这么大量的数据迁移到云服务提供商那里,你不再需要管理存储硬件,但云存储的费用将大幅增加,可能比继续使用物理硬件的成本还要贵得多。我个人曾看到过存储成本每月额外增加数万美元。
另一个需要考虑的因素是稳定性。人们常常认为,使用云服务提供商可以实现更稳定的基础设施。这个观点一开始似乎有道理,因为使用云服务提供商意味着你不再需要管理硬件。云资源更稳定这一说法确实有一定的道理,但事情并不那么简单。云服务提供商确实会经历停机,而且这种情况发生的频率比你想象的还要高。
云服务提供商通常会宣传较高的正常运行时间,通常用“9”来衡量。例如,截至本书出版准备时,AWS 提供 99.999%的正常运行时间。这听起来很棒,不是吗?在你对云服务提供商的正常运行时间声明感到兴奋之前,了解他们究竟是如何计算这一正常运行时间的也很重要。如果上游提供商(如后端互联网资源)发生故障,服务完全不可用,他们可能不会把这一情况计入停机时间,依然声称保持同样的正常运行时间。云服务提供商的大规模故障并不罕见;例如,2017 年亚马逊的 S3 服务由于一名工程师输入命令错误而发生了重大故障,导致许多互联网上的服务无法提供给用户。因此,虽然云服务提供商的故障发生率通常低于硬件故障的发生率,但仍需记住,停机会时有发生。我们永远不能假设任何服务(无论是我们的还是其他的)是万无一失的。
自动化是云计算中的一个重要考虑因素。如果你的资源遇到问题,最好有一种自动化的方式来重新部署你的关键服务。在物理服务器上,你可以按需求设置服务器并将硬盘镜像保存,以防服务器发生故障。云服务提供商基本上也为你提供相同的服务,允许你创建重要服务器的镜像,以便将来需要时重新部署。
如果你选择了云服务提供商,务必定期备份并保存镜像文件。同时,确保至少将最新的备份保存在本地,远离云服务提供商,因为如果云服务提供商发生故障,你也会失去备份。更简洁地说,我建议你不要对云服务提供商的稳定性过于自信。无论其营销团队如何宣传,始终假设你的基础设施最终会失败,且要做好准备,防止其发生。
我对云服务提供商稳定性的警告,并不是要吓唬你远离使用它们,而是希望你能够像管理物理基础设施一样,保持良好的云使用习惯。简单来说,要在服务器管理中取得成功,你必须假设云服务提供商的故障与本地设备一样容易发生,并且要确保在遇到问题时你已经做好了充分的准备。云服务提供商实际上为你提供了所有必要的工具,以构建一个稳定、可复制、可恢复的基础设施。如果你有效地利用这些工具,应该不必担心任何问题。我们将在接下来的内容中深入探讨这些概念,实际上,你甚至可以让云服务器在问题发生时自动恢复,这完全取决于你如何设计你的解决方案。
同时,了解关于数据保存、云中可以存储什么类型的信息或你所在地区可能存在的任何相关政策或法律也非常重要。(GDPR 是法律要求的一个很好的例子。)如果有疑问,可以咨询公司管理层、安全团队和人力资源部门,这将是你在设计云解决方案时,了解与数据相关的政策和法律的一个好起点。
在接下来的章节中,我们将探讨一些特定于 AWS 的概念,这样我们就能为后续章节中构建实际的云解决方案打下更坚实的知识基础。
熟悉一些基本的 AWS 概念
如前所述,AWS 是多个竞争的云服务提供商之一。在本章中选择 AWS,是因为与其他任何提供商相比,这个平台要求管理员在管理基础设施时采取完全不同的心态。这种不同的心态即使在 AWS 之外也是健康的,因此它在我们旅程的这个阶段代表了一个合理的进化。
到目前为止,我们一直将服务器的安装看作是宠物,意味着我们希望它们一直存在,确保它们健康,如果出了问题,我们会尽力修复。我们希望尽可能长时间地保持服务器的正常运行。我们希望能够依赖它们,这有助于我们的组织——客户和用户更愿意使用一个稳定、最小化或没有停机时间的网站或服务。
最后一点,最小化停机时间,无论我们以什么样的心态管理基础设施,这一点都不会改变。在这个行业中,停机和服务中断是非常不好的,这一点始终不变。不同之处在于我们如何处理这个问题,以及我们如何去恢复。事实上,我们甚至可以尽最大努力在出现问题时自动恢复。如果客户从未注意到曾经发生过问题,那就更好了。
使用 AWS 时,心态是不同的——我们将服务器视为一次性使用品,而非宠物。这一观点一开始可能让人感到有些意外,但如果我们有效地利用所提供的工具,就可以构建一个可扩展的基础设施。这个概念特别被称为自动扩展,是 AWS 的一个非常重要的方面。通过自动扩展,资源会根据需求的增加和减少自动创建和销毁。例如,假设你的网页服务器接收到的访问量比平常多,且其 CPU 已经接近最大负载。此时,自动扩展会自动启动一个新的网页服务器,并通过负载均衡器将客户端按需分配到不同的实例,从而分担负载。你可以设置可以启动的最大实例数量,然后当负载减少时,之前为应对负载而启动的服务器会自动被删除。这意味着你可以设计你的云解决方案,使得在任何时刻都能拥有恰好所需数量的服务器。
自动伸缩之上的另一个层级是自动修复。顾名思义,自动修复意味着,如果你的服务器出现问题,它会自动“修复”。在云计算中,这意味着实例将被销毁并从已知的良好镜像或模板中重新创建。这是 AWS(或者几乎所有类似实现)的终极目标:拥有不仅可扩展,而且能够自我恢复的基础设施。例如,也许你有一对处理客户端请求的 Web 服务器。其中一台遇到问题并未通过某种测试,测试表明该服务器不可靠。这样的测试被称为健康检查。通过自动修复,未通过健康检查的服务器会被认为是不健康的并被删除。借助自动伸缩,你还可以为你的应用设置最小服务器数量。如果服务器数量低于最低要求,则会创建一台新服务器来替代它。根据你的架构设计,客户可能会注意到应用在少数服务器上运行时的性能下降,但这只是暂时的。当新服务器上线时,一切会恢复正常。
在 AWS 中,自动伸缩和自动修复是非常重要的。我曾见过许多管理员没有利用平台的这些功能,这绝不应该发生。如果没有这些高可用性功能,你的服务器随时可能会停止运行。这可能比你想象的更严重。AWS 的物理服务器遍布全球,和其他物理服务器一样,硬件故障不仅是可能的,而且经常发生。如果你的应用运行在 AWS 数据中心的某台物理主机上,而该主机发生硬件故障,在更换硬件时,可能会导致你的服务器被删除。使用自动伸缩,这就不是问题了。一台新的虚拟服务器将上线,替代被移除的服务器。
但如果没有自动伸缩,这可能会导致你需要手动重建服务器。不过不用担心,我们将在本章中讲解自动伸缩,所以在我们讲解完之前,你一定会理解如何实现它。
高可用性相关的概念不限于 AWS,其他云平台也有类似的功能。不同云服务提供商之间的主要区别在于它们为这些功能所使用的营销术语,但你可以在每个平台上设置类似的基础设施。不过,AWS 确实非常注重这一方面,因此学习这些概念是很重要的。但即使你在公司生产环境中没有使用 AWS,能够轻松从灾难中恢复的概念仍然很重要,且是可以实现的。至少,你应该考虑为重建服务器实现自动化,这样如果发生问题时,你就不必做那么多手动操作。
回到 AWS:该平台内有许多服务可供使用,这些服务提供了各种功能。实际上,平台内有一百多种不同的服务。因此,我们无法在本章中覆盖每一项服务。即使是在专门讲解 AWS 的书籍中,覆盖所有服务也是一件非常困难的事情。但是,不要让服务的数量让你感到不知所措;你只需要学习与实现你在平台上目标相关的服务。例如,如果你的组织不开发多人电子游戏,那么学习 GameLift 服务对你完全没有帮助。在本章中,我们将重点介绍启动并运行一个基本的云端 Web 服务器所需的服务。以下服务和术语将会讨论:
-
虚拟私有云 (VPC):VPC 是对你整个云网络和资源的高级抽象。你所配置的每一台服务器和相关服务都会被部署在 VPC 中。你可以把 VPC 看作是你整体的网络。在你的组织中,可能有各种路由器、网关、防火墙、虚拟机、打印机或其他网络连接设备。你组织的网络本质上就是 AWS 中的 VPC,是一个完整网络的软硬件版本。
-
弹性计算云 (EC2):EC2 是 AWS 中运行虚拟机的服务。AWS 中的独立虚拟机被称为 EC2 实例。你可以把这个服务看作是 AWS 版的 VMware ESXi、Microsoft Hyper-V、Proxmox 或你更熟悉的虚拟机平台。EC2 实例就像虚拟机一样,具有分配的内存和 CPU,并运行操作系统。在本章结束之前,我们将在 EC2 实例中运行 Ubuntu。
-
弹性块存储 (EBS):随着你对 AWS 了解的深入,你会发现即使是最简单的组件,似乎也附加了一个营销术语。EBS 提供块存储,本质上是我们一直在使用的相同类型的存储。所以简而言之,EBS 卷就是硬盘。当你创建一个 EC2 实例时,服务器的操作系统将从 EBS 卷中运行,你可以相应地设置卷的大小。我们将在本章稍后详细讲解。
-
弹性负载均衡器 (ELB):正如你从名称中可以猜到的,ELB 是 AWS 的负载均衡器,相当于提供类似的功能。这使得你可以让多个 EC2 实例为你的应用提供服务,并且你可以创建一个 ELB 来在这些实例之间路由流量。ELB 实际上是 EC2 的一个特性,而不是独立的服务。
-
身份与访问管理 (IAM):IAM 是 AWS 中的工具,用于创建和管理用户账户,确定用户权限,甚至创建 API 密钥,以便程序化访问和管理 AWS。基本上,它是你处理与用户权限相关的一站式服务,无论“用户”是人类还是脚本。
-
Route 53:虽然我们本书不会涉及 Route 53,但我建议你至少了解它,以防未来需要。如果你决定在生产环境中使用 AWS,Route 53 将简化 DNS 条目的管理过程,并且还能注册新的域名。如果你的组织是一个托管服务提供商,你可能会经常使用这个服务。
-
简单存储服务 (S3):亚马逊的 S3 服务是我们本章不会涉及的另一项服务,但了解它的存在以及它的用途是个好主意,以防你以后需要用到它。S3 实际上是一个非常流行的服务,提供对象存储。
对象存储是一种新的存储类型,区别于你添加到服务器上的磁盘(虚拟或物理),你需要格式化并挂载它。虽然你仍然可以将 S3 挂载到服务器上,但它没有文件系统(如 ext4 或 STON),也不理解权限。它仅仅是一个名称-对象对,你在其中存储文件,并且每个文件都有一个名称。使用 S3 时,你创建“存储桶”,每个存储桶可以存储文件。每个存储桶的名称必须唯一。如果你想将可下载文件提供给客户或存储备份文件,S3 非常有用。
-
弹性 Kubernetes 服务 (EKS):在上一章中,我们介绍了 Kubernetes,甚至设置了我们自己的集群。AWS 有自己的 Kubernetes 解决方案,称为EKS。虽然本书不会涵盖它,但如果你想继续使用 Kubernetes,并希望将容器运行在托管服务中,而不是管理自己的集群,这个服务是值得考虑的。EKS 结合了 Kubernetes 与 AWS 平台的灵活性,因此它是一个非常有用的服务。
-
安全组:默认情况下,来自公共互联网的许多 AWS 资源访问是禁用的;安全组用于确定什么可以访问 AWS 中的资源,你可以根据 IP 地址和端口来允许或拒绝访问。关于 EC2 实例,默认情况下允许出站访问,但每个端口都被阻止入站。你可以创建一个安全组,允许特定的 IP 访问实例,从而提高安全性。稍后我们会看到一个例子。
现在我们已经有了基本的了解,可以开始构建一个运行在 AWS 上的云应用程序了。
创建 AWS 账户
正如上一节所提到的,AWS 中的 VPC 代表了你整体网络的高层次抽象。我们创建的所有资源都将运行在 VPC 内部。因此,我们需要首先创建 VPC,才能创建 EC2 实例并部署 Ubuntu。
然而,在我们创建 VPC 之前,我们需要一个 AWS 账户。在本章之前,我通常建议你使用手头可用的硬件来创建 Ubuntu 安装,以便与平台进行配合。这次,我们将使用一个实际的云服务提供商,虽然这会有一定费用。虽然新账户可以在有限时间内免费使用一些组件,但账单的管理由你——读者来负责。我们将在本章稍后部分详细讨论费用。但作为一个初步的指导原则,现在你应该始终选择最便宜的选项。如果有免费的实例类型可用,选择它。当然,如果你打算在组织中部署实际的资源进行生产使用,那么你需要根据使用案例选择合适的实例类型。对于我们而言,我们只是第一次学习这个平台,所以一定要选择最低成本的资源,完成后记得删除所有内容。
最后一条尤为重要——免费套餐并不是无限的,如果你忘记删除某些东西,可能会在某个时候收到账单。我建议你保持一个记录,列出你在 AWS 中创建的所有内容,这样如果不打算继续使用平台时,你就可以有清单来删除这些内容。
你还应该删除任何快照/存储卷,或者你创建的其他任何东西。更好的做法是,如果你不打算在生产中使用 AWS 账户,你可以直接删除账户,以免它继续向你收费。如果有疑问,请务必查阅 AWS 内部关于账单的文档。
注册 AWS 账户
现在是时候创建我们自己的 AWS 账户了。在本节(以及本章剩余的各节中),我将带你逐步完成导航 AWS 的每个步骤。需要记住的是,我们将要使用的AWS 管理控制台有时会发生频繁变化。事实上,自从本书上一版写成以来,控制台至少已经变动了两次,甚至不算在第三版时,界面就在写作本章之前发生过变化。这里的截图和操作步骤是截至本章编写时准确的,但界面可能会在某个时候再次更新,甚至很可能会更新。如果截图或布局发生了变化,你仍然可以按照这里的说明操作,因为用词通常不会变化,变化的只是布局。既然如此,让我们开始吧。
首先,访问aws.amazon.com,你应该会在右上角看到一个橙色的按钮,上面标有创建 AWS 账户。发布后页面的布局可能会有所变化,但你应该能在页面的某个位置看到创建新账户的按钮:

图 19.1:AWS 主页面
当你点击按钮以创建新账户时,系统会弹出一个表单,要求你提供基本信息。你可能被要求填写的一些字段示例如图 19.2中所示,尽管实际要求提供的信息可能会有所不同。根据要求填写每个字段,然后点击继续:

图 19.2:注册新的 AWS 账户
在点击继续后,系统会出现另一个页面,要求你填写更多信息,如全名、公司名称、地址等。这个过程还会要求你提供信用卡信息,所以请逐步完成每个页面并输入所需信息。完成整个过程后,你将看到一个选择框,在其中你可以选择你的支持计划:

图 19.3:在注册 AWS 账户时选择支持计划
如果在过程中系统要求你选择一个支持计划,请选择最适合你使用场景的计划。如果你打算使用 AWS 创建生产实例供你的组织使用,开发者或商业计划可能会为你提供额外的价值。如果你仅仅是为了学习 AWS 并通过本章中的示例进行操作,请选择基础计划。
一旦完成了创建新账户的过程,可能需要一些时间才能使你的新账户准备好。当账户准备好后,你应该会收到一封邮件,告知你可以开始使用。收到这封邮件后,你将能够登录。为此,访问aws.amazon.com,然后点击登录到控制台按钮,这会将你带到另一个页面,在该页面你可以输入账户登录信息:

图 19.4:作为 Root 用户登录
在这里,你可以选择以 root 用户或 IAM 用户身份登录。我们还没有创建任何 IAM 用户,所以此时只有 root 用户。root 用户通过使用你在注册过程中提供的相同电子邮件地址登录来访问。请输入该电子邮件地址并点击下一步,然后在下一个页面输入你的密码。
如果一切顺利,你应该已经登录并看到AWS 管理控制台:

图 19.5:AWS 管理控制台主界面
现在你已访问到管理控制台,你可以开始使用 AWS。在我们开始创建云资源之前,我们应该实施一些基本的安全措施来保护我们的账户。
实施基本用户安全
在继续之前,我们需要采用一些非常重要的安全最佳实践,来确保能够安全地进行 AWS 账户身份验证。虽然我们新建的账户很可能是用来跟随本章示例进行测试的账户,但我们应该养成始终保护我们的 AWS 账户的习惯,无论它实际上有多重要。
我们可以从保护根账户开始,因为它是黑客的常见攻击目标。具体来说,我们应该为该账户启用双因素身份验证。这样,外部威胁要访问该账户将变得更加困难,因为他们除了需要您的密码外,还需要获取您的第二个认证因素。由于我们此时已经以根用户身份登录,因此可以立即设置此功能。
在管理控制台中,您会在屏幕顶部看到一个搜索框。如果您已经知道要配置的服务名称,可以开始在搜索框中输入该名称,如果您的查询与可用服务匹配,它会显示在列表中。如果您不知道要使用的服务名称,可以点击控制台左上角的服务,以查看所有可用服务的完整列表。
要设置第二因素身份验证,我们将访问 IAM 服务。您可以开始在搜索框中输入IAM,它应该会显示为一个可用选项。点击它继续:

图 19.6:在 AWS 管理控制台中使用搜索功能定位 IAM 服务
当 IAM 仪表板出现时,您可能会看到一个安全警告,提醒根用户未启用多因素身份验证(MFA)。虽然启用此功能是可选的,但我建议您启用它。
MFA 对于确保任何对您组织重要的账户的安全至关重要,尤其是像 AWS 这样的云计算提供商账户。MFA 将增强账户的身份验证安全性,要求在用户能够访问账户之前提供额外的验证因素。根账户是 AWS 中最需要保护的账户,因为它对所有可用服务都有完全访问权限。要启用 MFA,点击标有添加 MFA的按钮(如果页面布局发生变化,文案可能会有所不同):

图 19.7:IAM 仪表板,显示推荐设置 MFA 的警报
接下来,点击MFA展开它(如果尚未展开),然后点击激活 MFA按钮:

图 19.8:设置 MFA(续)
下一屏将给您一个选择,可以选择使用哪种设备来辅助 MFA。如果您没有物理硬件密钥,如 YubiKey,默认选项(虚拟 MFA 设备)是一个不错的选择。如果您没有其他偏好,选择虚拟 MFA 设备选项并点击继续:

图 19.9:选择要设置的 MFA 设备类型
要设置虚拟 MFA 设备,您需要下载一个第二因素认证应用程序来完成此操作。Google Authenticator 是一个流行的选择,但我推荐使用 Authy。两者都完全可接受,但 Authy 还提供桌面应用程序,并且如果您的主设备由于某种原因无法访问,还可以恢复您的账户。Authy 与 Google Authenticator 兼容,因此通常可以与提供 Google Authenticator 选项的服务一起使用。要继续,请显示二维码,用手机应用扫描它,然后输入应用生成的两个随后的值。
最后,点击分配 MFA以完成过程:

图 19.10:设置 MFA 设备
现在我们已经为根账户启用了双因素身份验证,我们应该立即停止使用该账户。这实际上是 AWS 的最佳实践;建议为将在您的 AWS 账户上工作的人创建个人账户,给予他们完成所需任务的权限。我们将在第二十一章《保护您的服务器》中讨论最小权限原则,但现在开始考虑这一概念也无妨。现在,我们要做的是为自己创建一个管理员账户,用来替代根账户。如果需要,我们可以随时使用新账户创建其他用户。
要创建一个新的管理员账户,我们将再次使用 IAM 控制台。在那里,您会看到左侧有一个用户链接,然后可以点击蓝色的添加用户按钮开始该过程:

图 19.11:为 AWS 设置新管理员用户
下一屏幕将要求您输入所需的用户名,并设置访问类型。对于用户名,您可以根据需要给用户命名。对于访问类型,我们将选择AWS 管理控制台访问。点击下一步:权限继续:

图 19.12:为 AWS 设置新管理员用户
接下来,我们将为我们的用户设置适当的权限。第三个图标,标记为直接附加现有策略,是我们在这里做出的第一个选择,然后在下面,我们会在管理员访问的复选框旁边打勾,然后点击下一步:标签:

图 19.13:为 AWS 设置新管理员用户(续)
我们可以通过将字段留空并点击下一步:审查来跳过标签屏幕。
下一屏幕将为我们提供一个概览,如果一切看起来正确,我们可以点击创建用户:

图 19.14:为 AWS 设置新管理员用户(续)
最后,我们会看到一条确认信息,显示我们的用户已创建。如果在过程中选择了随机生成密码,你也会看到一个按钮,用于检索该密码:

图 19.15:为 AWS 设置新管理员用户(续)
现在你可以使用新用户和提供的密码登录 AWS 管理控制台。你还需要 账户 ID,该 ID 可在此页面的 URL 中找到(它位于 https:// 后面的数字)。为了方便起见,你可以点击显示的 URL 来自动填充账户 ID。
未来,我建议你使用我们为管理 AWS 账户创建的新用户,因为除非万不得已,否则不应使用 root 账户。此外,我还建议你按照我们之前的步骤为新用户设置 MFA(多因素身份验证)。
选择区域
如前所述,AWS 中的 VPC 是你整体网络的高级抽象。你可以拥有多个 VPC,这类似于管理多个物理网络的概念。实际上,我们的账户中已经为我们创建了 VPC,因此无需再创建。在未来,记住,如果你需要多个 VPC,可以选择创建额外的 VPC。在我们的账户中,每个 区域 都有一个默认的 VPC,所以选择使用哪个 VPC 取决于哪个区域最适合我们的使用需求。
对于生产环境使用,你需要在 AWS 中创建离客户尽可能近的实例。例如,假设你的组织的客户主要位于美国东部,那么 AWS 中有一个被标记为 US East 的区域,在这种情况下它将是一个明显的选择。不过,你不仅仅局限于美国的区域;在全球范围内也有许多区域可供选择,比如德国、中国和加拿大(等其他地区)。简而言之,你应该尽量在离客户最近的地方创建资源。如果你没有特别偏好,可以选择离你最近的区域。
虽然超出了本书的范围,但 AWS 提供了一项名为 CloudFront 的服务,它充当 内容分发网络(CDN),你可以使用它将资源分发到多个边缘位置,从而确保用户从最接近他们地理位置的地方获取内容。对于制作媒体内容的组织来说,这尤其有价值。如果这对你有帮助,建议你深入了解 CloudFront。
此外,各个区域通常会有多个可用区,这可以让你更接近你的目标受众。例如,关于美国东部区域,它包含两个可用区,一个位于弗吉尼亚,另一个位于俄亥俄。可用区不仅能让我们更接近客户,还为我们提供了更多的冗余选项。例如,如果一个可用区由于某种原因发生故障,你可以将客户流量引导到另一个可用区。可用区有特定的命名规则,包括区域名称和该区域内的可用区名称。以美国东部为例,那里有两个可用区,分别标记为us-east-1(弗吉尼亚)和us-east-2(俄亥俄)。不过,并非所有区域都有多个可用区。目前,加拿大只有一个区域和一个可用区:ca-central-1。
除了可用区外,还有本地区,本地区旨在让你能够将资源设置得比可用区更接近你的客户。如果你的应用程序对网络延迟非常敏感,比如运行一个在线游戏的服务器,本地区是一个很好的选择。我们在本书中不会详细讨论本地区,因为这是 AWS 的一个全新产品,截至本书出版准备时,只有两个本地区。亚马逊计划在未来增加更多的本地区,因此到你阅读本书时,可能会有更多的本地区可用。如果你的组织提供对网络延迟敏感的服务,这是一个你可能需要关注的功能,随着它向更多地区推广。
目前,唯一需要考虑的因素是哪个区域能为你的客户提供尽可能接近的服务。不过,在跟随本书示例操作时,选择离你地理位置最近的区域是个不错的选择。
现在我们已经选择了区域,接下来如何在云端创建一个实际的 Ubuntu 实例呢?这正是我们在下一节要做的事情。
部署 Ubuntu 作为 AWS EC2 实例
在大量讨论之后,是时候在云端创建一个实际的 Ubuntu 部署了。这将使我们能够看到 AWS 服务的实际操作,并为我们提供一些 EC2 服务的实际经验。这需要两个独立的步骤:第一个是创建所需的 IAM 角色,第二个是创建我们的实例。我们先确保理解 IAM 角色的要求,然后设置角色并创建我们的新实例。
为 Session Manager 设置 IAM 角色
会话管理器 是 AWS 中的一项服务,我们可以用它来访问实例的命令提示符。它实际上是 系统管理器(Systems Manager)的一部分,而不是独立的服务。如果你想访问会话管理器,你需要搜索系统管理器,你会在其下找到会话管理器作为一项服务。你很快就会看到这一点。
为什么我们要使用会话管理器?就像使用任何其他 Linux 服务器一样,我们仍然可以使用 OpenSSH 连接到我们即将创建的 EC2 实例,就像在本书中处理非 AWS 实例时做的那样。
使用 OpenSSH 是没有问题的;在正确的设置下,它可以是一个非常安全的选择。实际上,我们将在 第二十一章,保护你的服务器 中探索更好地保护它的方法。对于 AWS,我们可以使用会话管理器作为 OpenSSH 的替代方案,这个值得学习的替代方案提供了额外的安全性,因为其后台安全性不需要我们自己管理。此外,我们还可以通过 AWS 控制台控制对它的访问。
默认情况下,会话管理器是完全无法访问的。它需要在 Ubuntu 服务器中安装特定的包才能工作,并且还需要启用特定的权限。Ubuntu 的所需软件包默认已预安装,因此第一个要求在我们创建实例时会立即自动处理。
对于添加权限的第二个要求,我们需要创建一个 IAM 角色,以便我们即将创建的 EC2 实例能够与会话管理器服务进行通信。我之前提到过会话管理器默认是无法访问的,这就是原因——它缺少所需的权限,直到我们添加它们。为了添加所需的权限,我们将访问 AWS 控制台中的 IAM 服务,这与我们之前创建用户帐户时使用的相同。IAM 本身有许多技巧,不仅仅是允许我们创建用户。它还允许我们创建 IAM 角色,这使我们能够为整个对象添加权限。例如,我们可以创建一个具有所需权限的角色,然后将该角色附加到任何 EC2 实例上,以立即使其能够通过会话管理器进行连接。
让我们开始设置会话管理器所需的 IAM 角色。返回到我们已经操作过几次的 AWS 控制台中的 IAM 部分,我们将创建所需的角色。
在窗口左侧的 IAM 菜单中,点击 角色,然后点击标有 创建角色 的蓝色按钮:

图 19.16:创建一个 IAM 角色以启用会话管理器
在下一个屏幕上,确保选择 AWS 服务,然后在下面的菜单中选择 EC2 作为服务。点击 下一步 继续:

图 19.17:创建一个 IAM 角色以启用会话管理器(续)
接下来,我们可以将策略附加到我们的角色。在出现的搜索框中,我们可以输入一个关键词来缩小列表,然后点击我们希望附加到角色上的策略旁边的复选框。对于所有内置策略的全面概述以及它们的用途超出了我们的范围,但简要来说,AWS 中的每项服务都有预构建的策略,可以附加到角色上,从而访问各种功能。针对我们的需求,我们将把AmazonSSMFullAccess策略添加到我们正在创建的角色中。
这样做的目的将在我们创建 EC2 实例时变得更加清晰,所以现在,点击下一步继续:

图 19.18:将 AmazonSSMFullAccess 策略附加到我们的角色
接下来出现的屏幕将让我们有机会添加一个或多个标签。我们暂时跳过这一部分,但你可以在这里自由添加任何你想要的标签。标签可以让你为资源附加信息,且不限于 IAM 角色。你可以根据需要添加任何你认为相关的描述性信息。标签仅仅是键:值对,所以这里没有特定的命名规则。如果你希望添加标签,可以添加,完成后点击下一步:审核。
最终的屏幕会让我们回顾到目前为止选择的设置,并提供一个选项来为角色命名,若需要,也可以添加描述。虽然这不是必须的,但我建议为角色命名,以便日后更容易识别。当你完成后,点击创建角色:

图 19.19:为我们的角色添加名称和描述
当涉及到设置 IAM 角色时,我们已经准备好——角色已创建,我们可以继续使用它。接下来,是时候创建我们的 Ubuntu 实例了。
在 AWS 中创建 Ubuntu 服务器实例
现在是时候看看我们的工作成果,并创建我们的 Ubuntu 实例了。在 AWS 控制台中,我们首先应该访问 EC2 服务以开始。你可以通过在控制台的搜索框中输入服务名称来轻松找到任何服务;所以如果你开始输入 EC2,应该会在列表中看到EC2。点击它后,点击屏幕左侧的实例。这样做之后,你会看到一个屏幕,上面有一个标有启动实例的按钮:

图 19.20:EC2 服务的主窗口
通常,EC2 控制台中的实例部分会显示所有服务器实例的列表,但除非你已经提前阅读过,否则我们目前还没有任何实例,所以这个窗口是空白的。当你点击启动实例时,你会在列表中看到各种操作系统。但为了我们的目的,Ubuntu 显示在快速开始部分,所以我们选择它:

图 19.21:EC2 的 Ubuntu 选项
在下方,确保选择了t2.micro,旁边应该会显示免费套餐适用。这就是我们想要使用的实例类型:

图 19.22:选择实例类型
在选择实例类型页面上,会有许多实例类型供您选择。我选择了t2.micro实例类型,建议您也选择这个类型。它符合免费套餐的条件,这是您在账户创建后的前 12 个月内可以访问的一个特别套餐。对于生产服务器来说,您很可能不会选择这个实例类型,因为它会非常慢——它只有 1 个 CPU 和 1 GiB 的内存。而且它是一个可突发的实例类型,这意味着其速度会根据使用情况波动。它能够在繁忙的工作负载下进行突发处理,但它的能力取决于它在特定时间内获得的 CPU 积分。对于这个章节来说,详细的解释超出了范围,但如果您打算在生产环境中使用 AWS,建议您了解各种可用的实例类型。尽管截图中没有显示,您在此页面上也应该能看到费用。但我们现在仍将利用免费套餐。
在页面下方,您将生成一个新的密钥对,假设您之前还没有。此密钥将用于通过 SSH 连接到实例。要生成新的密钥,点击创建新密钥对:

图 19.23:实例创建过程中密钥对设置
接下来出现的页面将允许我们配置密钥,例如设置加密选项。为了我们的目的,我们可以在给密钥命名后保留默认设置,然后点击创建密钥对:

图 19.24:自定义我们的 SSH 密钥
上一步将触发浏览器下载您生成的 SSH 密钥。请务必将其保存在安全的地方。您可以使用刚刚下载的密钥通过 OpenSSH 连接到实例,命令类似于以下内容:
ssh -i /path/to/key.pem ubuntu@<Instance Public IP>
对我而言,如果我添加我的密钥路径以及实例列出的公共 IP 地址,那么命令将变成这样:
ssh -i /home/jay/downloads/jay_ssh.pem ubuntu@54.81.234.225
从这里开始,管理服务器只是与其 Shell 进行交互并输入命令的问题。
不过,算了,我有点急于表达。在您保护好密钥之后,我们可以通过向下滚动继续到下一部分。接下来我们将处理网络设置:

图 19.25:为我们的新 EC2 实例设置选项
此屏幕上的第一个考虑是我们将允许从哪些位置进行 OpenSSH 连接。默认选项 0.0.0.0/0 允许从任何地方进行连接(并且甚至有相应标签)。正如我们在第二十一章《保护您的服务器》中将要讨论的,公开允许 SSH 连接是一件糟糕的事,因为外部威胁者可能会尝试利用这一点来访问服务器。对于我们现在创建的实例,我们可以主张允许来自任何地方的连接,因为这是一个用于学习的测试实例(并且我们将在本章结束前删除该实例)。然而,养成将 OpenSSH 访问限制为特定 IP 地址的习惯可能是一个更好的选择。为此,您可以从下拉菜单中选择我的 IP选项,这将把 OpenSSH 访问限制为您的公共 IP 地址。
如果您选择限制访问,请记住,除非您的公共 IP 是静态的,否则它可能会随时更改。当它更改时,您将无法通过 OpenSSH 访问您的 EC2 实例。您可以通过更新实例的安全组,包含您的新 IP 地址来恢复访问,但这种不便是为增加安全性所付出的微小代价。
在同一部分中,我们可以选择允许 OpenSSH 之外的流量:

图 19.26:在 AWS 上创建 EC2 实例时的网络设置
另外的选项,HTTPS 和 HTTP,如果您计划在实例上设置网站,至关重要。如果您在 Web 服务器上使用 OpenSSH,通常会限制它的访问,同时允许公共internet对端口 80 和 443 进行完全访问。原因是,OpenSSH 给您提供了管理服务器的权限,因此我们肯定不希望公开该访问权限。对于网站,我们很可能希望公众能够访问它,这就是为什么我们允许这两个选项的原因。只有在您确实计划设置 Web 服务器的情况下,才应该勾选这两个选项。对于我们的需求,我们应该允许这些选项,因为稍后我们将安装 Apache。
接下来,我们将为实例配置存储:

图 19.27:创建 EC2 实例时的存储选项
默认情况下,您的 EC2 实例将配置为 8 GiB 的存储空间。如屏幕上高亮显示的消息所示,如果您符合条件,您可以在免费套餐中获得最多 30 GB 的 EBS 存储空间。因此,您可以考虑将此存储空间设置得更高。您还可以向实例添加额外的存储卷,这在您希望在块存储设备之间划分存储时会有所帮助。在本章的使用案例中,我们不需要这么做。
在该部分下方,屏幕的最后一部分允许你配置高级详细信息。这里有相当多的额外选项,但我们只会忽略其中除了一个的所有选项。在最底部,有一个大文本框,标有用户数据。这是我们唯一会更改的高级详细信息选项:

图 19.28:为我们的 EC2 实例添加用户数据
用户数据部分常常被忽视,但它非常有用。那么它到底是做什么的呢?如果我自己不太了解的话,我可能会认为这里是添加与用户相关的信息。但事实并非如此。用户数据允许你添加一系列命令,甚至是一个完整的脚本,在实例创建时自动执行。
你现在可以想到,在设置服务器时,任何你可能执行的命令或调整的配置。与其在实例上线后手动执行设置命令,不如简化这个过程,在你第一次登录之前就能自动完成大部分工作。
在我的例子中,我已将以下代码添加到用户数据字段中:
#!/bin/bash
apt update
apt dist-upgrade -y
apt install -y apache2
如果你认为我添加的代码像是一个 Bash 脚本,那么你是对的——这正是它的用途。我在用户数据字段中添加了四行 Bash 语句,以便自动执行一些命令。代码应该是相对简单的:我设置它先更新存储库索引,然后执行完整的系统升级。
这很重要;我们始终希望我们的服务器以最新的补丁启动。作为概念验证,我添加了一条安装 Apache 的命令。请注意,我在所有 apt 命令中都加入了 -y 选项。这会自动对 apt 可能提出的任何问题回答“是”,因为我们没有显示器连接到这台服务器,无法自己回答问题。如果没有这个选项,用户数据将无法应用。
无论如何,关键时刻到了——是时候启动我们的实例了,我们可以通过点击页面底部的启动实例按钮来完成。如果我们没有在过程中遗漏什么,我们的新 Ubuntu Server 云实例即将启动!
此时,我们应该能在我们账户中的 EC2 实例列表中看到新的实例,并且它将需要一些时间才能准备好使用。当状态显示实例为运行中时,意味着它已经准备好可以连接了:

图 19.29:检查我们 EC2 实例的状态
为了设置连接以便我们管理实例,我们需要更改实例的IAM 角色。如果您还记得,我们之前创建了一个 IAM 角色,这里我们将把该角色附加到新的实例上。这样可以确保角色中的设置应用到实例,从而使我们能够访问会话管理器,连接到该实例。要更改 IAM 角色,我们需要勾选实例左侧的框,然后点击屏幕顶部的操作。在弹出的菜单中,点击安全,然后点击修改 IAM 角色:

图 19.30:导航到 EC2 实例的 IAM 角色设置
接下来的屏幕是我们将更改 IAM 角色为我们之前创建的角色的地方:

图 19.31:更改实例的 IAM 角色
点击更新 IAM 角色后,您将返回当前 EC2 实例的列表。如果点击实例名称旁边的蓝色实例 ID 文本,您将进入一个可以自定义实例其他选项的屏幕:

图 19.32:额外的 EC2 实例设置
在页面上的各种项目中,我们特别关注的是连接按钮。如果我们点击它,我们将开始设置远程连接的过程,以便管理实例。在出现的屏幕上,点击会话管理器标签:

图 19.33:准备连接到实例的会话
一旦点击连接按钮,新的浏览器窗口将会出现,您可以使用它向实例发出命令:

图 19.34:在 Web 浏览器中打开的实例会话
在图 19.34中,我输入了以下命令以显示实例中部署的 Ubuntu 版本的详细信息:
cat /etc/os-release
/etc/os-release文件包含在所有 Ubuntu 安装中,从输出中可以看到,它包含一些关于我们正在运行的 Ubuntu 版本的信息。该命令是直接输入到会话管理器窗口中的,以表明连接实际上已经成功,并且我们现在可以直接在 Web 浏览器中配置实例!
如果您还记得,我们之前添加了用户数据,其中包括一个安装 Apache 的命令。如果将 EC2 实例的公共 IP 地址输入到 Web 浏览器中,您应该能看到默认的 Apache 网页:

图 19.35:在 EC2 实例上运行的 Apache 默认网页
恭喜!您已经成功将 Ubuntu Server 部署到云端,现在已经有一个实际运行的 Web 服务器了。就这么简单。使用会话管理器也很简单;您只需要右键点击服务器,点击连接,然后继续构建实例。这太棒了!
然而,不太理想的是,当服务器出现问题时,你不得不重新开始并从头构建它。在接下来的部分,我们将探讨如何创建一个服务器镜像,利用它来部署定制版的 Ubuntu。
创建并部署 Ubuntu AMI
我知道的几乎所有云平台都有类似的功能,可以用来创建实例硬盘的镜像。镜像可以用来创建原始服务器的副本,并且作为一个起点,这样如果服务器需要重建,我们就不必从头开始。在 AWS 中,镜像被称为Amazon 机器镜像(AMIs)。从本质上讲,AMIs 并没有什么特别的地方;如果你之前使用过磁盘镜像,它们是一样的。至于 AMIs 中包含什么内容,你可以(而且应该)在这方面发挥想象力——任何你在部署新服务器时手动设置或配置的内容,都可以考虑纳入镜像,镜像中包含的自定义项越多,后续节省的时间就越多。
让我们来看一下这个过程,并创建我们刚刚设置好的服务器镜像。我们可以考虑先关闭服务器,尽管这不是必需的。最好在服务器关闭时创建 AMI,而不是在服务器运行时创建。当服务器关闭时,磁盘上不会有任何写操作,所以我们不必担心在关键写入操作中间捕获 AMI 时出现数据损坏。在创建运行中的服务器的 AMI 时遇到问题的可能性非常小,但如果可以的话,我建议你还是先关闭服务器,以确保安全。
在 AWS 的 EC2 控制台中,你可以右键点击实例进入会话管理器,然后可以通过命令行界面直接关闭它:
sudo poweroff
在 AWS 中,实例关闭可能需要一两分钟的时间。你可以过一会儿刷新页面,状态应该会变为已停止:

图 19.36:检查 EC2 实例的状态
一旦实例关闭,你可以右键点击它,开始创建 AMI 的过程。将鼠标悬停在镜像和模板上,然后点击创建镜像:

图 19.37:在 EC2 实例上运行的 Apache 默认网页
接下来,我们可以为我们的 AMI 输入一些详细信息,给它一个名称和描述。这些信息将帮助你合作的其他人了解该镜像的用途,同时也能帮助你日后记得为什么要创建这个镜像。页面上还有其他选项,但我们可以保持默认设置。现在,名称和描述就足够了。完成后,点击创建镜像继续:

图 19.38:创建新的 AMI
相信与不信,这就是全部内容。创建 AMI 的过程非常简单,只需要几个步骤。你现在应该看到一个确认屏幕,告诉你镜像正在创建中:

图 19.39:创建新 AMI 时的确认
如果你点击包含 AMI ID 的下划线文本,你将被引导到 EC2 控制台中的 AMI 部分,那里将显示你创建的镜像:

图 19.40:我们新创建的 AMI,已可使用
在 EC2 控制台的 AMI 部分,你应该看到列表已缩小到仅显示我们刚刚创建的 AMI。我们总共有一个 AMI,除非你为练习创建了多个 AMI。最后,状态列应该显示可用,表示该 AMI 已准备好使用。如果没有,稍等一会儿,稍后刷新页面。有时需要几分钟的时间。但关于创建 AMI,就是这些!
既然我们已经有了一个 AMI,我们该如何使用它呢?其实这更简单了。只需右键点击 AMI 列表中的 AMI,然后点击启动。你将看到我们之前创建实例时所用的相同启动设置,不过这次,我们使用的是自定义的 AMI,而不是提供给我们的那个 AMI。现在,我们拥有了一个内置 Apache 的自定义 Ubuntu AMI,可以用来简化我们的过程。不过请记住,我们的原始实例仍然是停止状态。你可以返回 EC2 实例列表,右键点击该实例,然后选择启动,但这不是必需的;我们将很快展示一个有趣的自动化示例。
在下一节中,我们将探讨自动扩展(Auto Scaling)的概念。
使用自动扩展进行 Ubuntu EC2 部署的自动扩展
如果我们为组织维护一台或多台服务器,有时很难预测服务器的需求量。以一个热门新闻网站为例,某些文章可能比其他文章更受欢迎,如果某篇文章在网上病毒式传播,那么请求量可能会在短时间内激增几个数量级。过去,跟上客户需求是一个非常繁琐的过程,可能导致需要购买一台配置更强大硬件的全新服务器。而现在,借助云中的实例,我们有了更多的灵活性,可以自动化上线更多服务器的过程。这正是我们将在本节中研究的内容。
在我们开始之前,请记住,我们在 AWS 中实际上并没有一台热门服务器;我们只有一台当前运行 Apache 的简单测试服务器。我们可以模拟某些情况,但自动扩展(Auto Scaling)是一个需要一定实践才能完全利用的功能。不过我们肯定会在这里得到一个有效的示例。
但另一个重要的事情是,运行的实例越多,潜在的费用就越高。我们将在下一节中探讨如何降低成本,但作为一个通用的经验法则,删除你不使用的任何东西。在本章的示例中,我们已经设置了自己的 EC2 实例。这很好:我们能够实践一些关于 AWS 的概念并付诸实践。但如果我们留下不需要的实例运行,就可能会收到意外账单。建议你在本章结束时为自己写个提醒,删除测试 AWS 账户中的所有内容,这样就不用担心这个问题了。
接下来,Auto Scaling 的一个要求是,我们必须有一个 AMI 可供使用,用来在线启动额外的服务器。由于我们已经在前一节中完成了 AMI 的创建,所以这个要求已经满足。如果你还没有完成前一节的内容,确保在继续之前完成它。设置 Auto Scaling 的过程涉及一些步骤,我们将在本章中为每个步骤单独设置小节。
创建启动模板
在本章的早些时候,我们演示了创建新 EC2 实例的过程。我们选择了启动实例选项,然后在多个屏幕中配置了各种设置。我们选择了 Ubuntu 作为平台,添加了用户数据,并设置了 IAM 角色(以及其他内容)。启动模板的作用是让我们能够自动化这些选择。启动模板使我们能够自动化整个启动过程。
在 EC2 控制台的左侧菜单中,会有一个名为启动模板的链接。点击它。点击后,你可以点击标有创建启动模板的橙色按钮。然后,你会看到一个需要填写的表单,在表单中选择启动模板的所有默认选项。该页面没有截图,因为它非常长,无法容纳在一页上。因此,我将在下面提供相关选项,附上简短的描述以及建议设置该选项的推荐值:
-
启动模板名称:这只是一个给启动模板的名称;设置为你认为最合适的名称。请注意,名称中不能包含空格。
-
模板版本描述:对于描述,你可以添加一些你认为与启动模板目的相关的细节。
-
Amazon 机器镜像(AMI):选择你在前一节中创建的 AMI。如果你点击我的 AMI标签,应该能看到它,并确保选择了由我拥有。这将大大缩小 AMI 的列表,因为我们只创建了一个 AMI,所以应该很容易找到。
-
密钥对(登录):当你之前创建 EC2 实例时,它要求你创建一个 OpenSSH 密钥对。如果你下拉此列表,应该可以找到相同的密钥对。选择那个相同的密钥。
-
实例类型:如果你记得,之前我们选择了 t2.micro 作为实例类型。这也是该字段的一个不错选择,因为 t2.micro 符合免费层的条件。
-
安全组:之前我们添加安全组时,已经设置允许 OpenSSH 和 Apache。你可以继续选择相同的安全组。
-
IAM 实例配置文件:这个选项稍微隐藏一点,但只要你展开页面底部的高级设置,就可以看到。虽然这个选项是可选的,但建议选择我们之前创建的 IAM 配置文件,以确保每个由启动模板创建的实例都可以访问 Session Manager。
设置好所有细节后,点击页面底部的创建启动模板。现在我们已经创建并配置了启动模板,可以将其作为正在构建的自动伸缩功能的一部分使用。
创建自动伸缩组
我们的下一个步骤是创建自动伸缩组,这个过程比设置启动模板要简单。自动伸缩组是与整个应用程序相关的一组实例。我们将把启动模板添加到该组,并用它来定制一些要求,比如在线实例的数量。
返回 EC2 仪表盘,你会在左侧菜单的底部找到创建自动伸缩组的选项。进入后,给它命名:

图 19.41:为自动伸缩组命名
在同一页面的下方,我们将选择之前创建的启动模板。请继续操作,然后点击下一步:

图 19.42:为我们的新自动伸缩组选择启动模板
在下一个页面上,会出现几个额外的选项部分。第一个部分将让你配置一些网络设置。对于 VPC,可以保持默认设置,选择第一个可用区即可:

图 19.43:创建自动伸缩组(续)
接下来,下一部分会询问我们对实例类型的偏好。在我的选择中,我选择了手动添加实例类型,然后选择了t2.nano作为实例类型:

图 19.44:创建自动伸缩组(续)
一旦你完成了选择实例类型,当前页面不再需要我们做其他操作,点击下一步继续。这将带你进入下一个部分,询问你是否要创建负载均衡器。在此页面的第一部分,选择附加到新的负载均衡器:

图 19.45:创建自动伸缩组(续)
负载均衡器允许我们在多台服务器之间路由客户端请求,因此最终用户只会看到一个端点,但在负载均衡器后面我们可以有多个服务器来提供客户端连接。这是设置我们自动扩展测试站点的关键步骤,因此我们一定要配置这个选项。
在选择创建新负载均衡器后,我们可以在下一部分配置其他选项。我们大部分选项会保持默认设置,但有些地方需要注意:
-
负载均衡器类型:默认情况下,应该选择应用负载均衡器。如果不是,请确保在继续之前选择该选项。
-
负载均衡器名称:这只是你负载均衡器的名称;设置为你认为最合适的名称。请注意,这个字段对于字符类型的接受有一定限制,所以最好保持名称简单。
-
可用区和子网:我们至少需要两个可用区用于负载均衡器,其中一个已经为你选择。通过启用列表中的下一个可用区,可以快速完成这一部分。
屏幕上还有其他选项,但接下来我们要做的是创建目标组,这个选项就在可用区选项的下方。在那里,我们将选择创建目标组:

图 19.46:创建自动扩展组(续)
目标组是一个逻辑分组,其中包含为共同目标服务的 EC2 实例。负载均衡器会将流量发送到目标组,组内的实例将响应请求。如果某个实例不是目标组的成员,它就不会成为我们应用的一部分。一个组织有多个应用程序并不罕见,因此也会有多个目标组。创建目标组后,你可以选择接受默认名称,然后我们就可以完成自动扩展组的配置。点击下一步继续,这将带我们进入可以配置扩展策略的部分。

图 19.47:创建自动扩展组(续)
这就是魔法发生的地方:我们可以选择在任何时刻运行的实例的最小数量,以及允许应用扩展到的最大实例数量。现在可以将每个字段保持为1。
这应该是我们为自动扩展组配置所需的全部内容。你可以点击跳过到审核,通过快捷方式直接进入过程的最后屏幕,那里会显示你选择的概览。如果你对显示的设置满意,可以通过点击创建自动扩展组来完成该过程。
到此为止,我们的自动扩展组已创建,我们应该一切就绪。最初,我们将看到该组中有0个实例,因此当前实例数会显示为0。
更新容量完成后,它将自动启动一个新的 EC2 实例,以满足我们始终保持一个实例在线的要求。如果我们检查我们的 EC2 实例列表,现在应该会看到一个新的实例:

图 19.48:作为我们自动扩展配置的一部分,创建了一个新实例
如您在截图中看到的,原始的 EC2 实例状态为停止。我们之前停止了它,以便创建该实例的 AMI。在我的案例中,创建 AMI 后我没有再次启动该实例,所以它完全处于停止状态。自动扩展配置继续进行,并创建了一个新实例,因为它的要求是至少有一个实例在运行,而这个要求没有得到满足。
在我们实现最后一个必要组件之前,让我们先花一点时间了解一下我们已经设置好的内容。如果我们增加自动扩展组中所需的实例数,它将立即为我们启动一个新实例。尽管高级用法更适合写成一本专门讲解 AWS 的书,但我们可以设置,当实例的 CPU 达到某个阈值时,自动扩展会触发并启动另一个实例。
目前只有一个实例,我们还没有启用自动扩展,但我们可以轻松启用这一功能。我们自动获得的另一个好处是自动修复,即使只有一个实例,我们也能享受这个好处。如果我们唯一的运行实例出现问题,自动扩展将自动启动一个新的实例来替代它。
在这种情况下,网站将在新实例创建期间停机几分钟,但几分钟的停机时间总比我们自己手动替换服务器要好。如果我们将所需实例数设置为大于 1,当其中一个服务器出现故障时,其他服务器会继续承担负载,同时新的实例上线。这些都是能够充分利用的令人惊叹的好处,并且会立即为我们带来额外的安心。若要自己测试,可以通过右键单击正在运行的 EC2 实例并选择终止来删除它。该实例应会被终止,并且几分钟内会出现一个新实例来替代它。
由于我们正在使用负载均衡器,这会改变我们访问在实例上运行的应用程序的方式。在本例中,应用程序是 Apache(我们在构建 AMI 时安装了 Apache,因此每个实例都会自动设置 Apache)。通常,我们可以通过访问实例的公网 IP 地址来访问应用程序。实际上,我们仍然可以这样做 —— 对于负载均衡器设置中的其他实例也是如此。但为了充分利用我们构建的整体解决方案,我们应该通过负载均衡器的DNS 名称来访问应用程序。这将确保我们的请求通过负载均衡器路由,而不是直接发送到某个特定的服务器。如果你进入管理控制台的负载均衡器部分,你应该能在那里看到 DNS 名称。以后,你在实例上运行的应用程序或服务应该通过该 DNS 名称来提供给用户访问。
恭喜!到目前为止,你已经在 AWS 中创建了一个完整的负载均衡解决方案,该方案能够自动从故障中恢复。可以自由进行更多实验,完成后,考虑删除本章中创建的测试组件,以避免未来的费用。
说到成本,下一节我们将进一步讨论如何管理成本并控制账单。
降低成本:理解如何节省开支并做出高效的决策
正如你刚才看到的,我们必须实现许多组件和配置,以在 AWS 中构建负载均衡解决方案。随着我们逐步扩展 AWS 基础设施并实现更多解决方案,我们也应该密切关注账单。虽然我们现在可以使用免费层,但生产环境中的应用通常需要比免费层提供的实例更强大的实例,而免费层本身也不是永久有效的。更重要的是,我们还应该了解如何检查账单趋势,确保不会无意间实现某些昂贵的操作,或者因运行不再需要的服务而浪费资金。
在本节中,我们将探讨一些关于计费的概念。虽然本章的范围无法深入探讨计费的所有细节,但接下来的子章节将为你提供一些基本建议,帮助你避免意外的费用。
查看计费信息
在 AWS 中,计费部分是一个独立的服务,类似于 EC2 和 S3 等其他服务。你可以从服务列表中找到计费区域。进入后,你将看到计费与成本管理仪表盘,它会展示你当前的支出,并允许你查看当前和过去的计费信息:

图 19.49:查看计费与成本管理仪表盘
由于我目前正在使用的账户是在大约一周前创建的,并且我只在免费层中使用实例,所以目前没有费用。但是,如果我确实产生了费用,我会在仪表板主页上看到当前余额。此外,我将收到发送到主要电子邮件账户的每月对账单中的相同信息。
然而,我不建议等账单到达后再检查您的总额。为了有效管理计费,您应该手动检查这个仪表板,这可能使您在月底之前捕捉到错误,从而节省您的金钱。左侧的链接将为您提供额外的计费信息,并提供访问以前账单的方法。
添加一个计费警报
在本节中,我将为您提供一些关于如何帮助控制 AWS 账单的高级技巧。
在前面的部分中,我建议您定期检查账单,而不仅仅是在月度发票到达时检查。虽然这是一个好建议(如果我这么说的话),但现实情况是,像我们这样的服务器管理员忙碌,可能不会记得定期检查账单。这就是为什么我们通常设置系统警报来在我们的服务器遇到问题时通知我们。同样,我们实际上可以在我们的 AWS 账户内设置一个警报,以通知我们如果我们的账单过高。事实上,尽管我们一直在使用的 AWS 账户可能只是作为一个测试账户创建的,但特别重要的是我们添加一个计费警报,以便在我们的实验出现误配置导致我们产生不希望的费用时能够得到警告。
在本章结束时的进一步阅读部分,我包含了一篇 AWS 文档的链接,详细介绍了如何设置计费警报,我强烈推荐您启用它们。如果您的组织正在使用 AWS 账户运行实际的生产服务器,建议在那里启用计费警报。事实上,确保在您管理的每个 AWS 账户中创建计费警报也非常重要。另一个需要考虑的重要因素是删除不再需要的备份,这也可以降低成本。
删除不需要的备份
这可能看起来很简单,但您会惊讶——我曾经为或咨询过的每个 AWS 相关组织都遇到过备份失控导致大额支出的问题。我个人目睹过的浪费金钱的记录是一家公司在其账户中有超过 23,000 个不必要的快照,堆积了超过 5 年的时间。我不记得确切的金额,但这个错误使他们每个月损失成千上万美元多年!
备份本身非常重要,前一个例子如果该组织有法律要求,强制要求保留所有备份一段特定时间(如 5 年),或许能理解。而且备份,你现在应该已经知道,对于组织遇到某些问题并需要恢复过去的数据来说至关重要。但这里的普遍规则是,确保当你添加自动备份功能时,也要实施(并定期测试)自动清理程序。
只在需要时运行 EC2 实例
许多组织全天候全年无休地进行业务,而其他组织则只在日间工作时间进行业务。虽然这可能不是一眼就能看出来的原因,但可以考虑一个事实:当 EC2 实例关闭时,你不会为其收费。无论其状态如何,你都会为存储付费,但如果实例未运行,则不会为该实例本身收费。如果你的组织有只在特定时段使用的服务器,可以考虑在该时段之外停止实例,并在需要时重新启动它。
AWS 中有额外的功能,允许你自动安排实例仅在特定时间运行,因此有方法确保你的基础设施在需要时可用。在使用 AWS 时,请确保记住这一点;如果你正在创建一个新服务器,它是否需要 24/7 不间断运行,还是只需要在某些特定时间运行?你会惊讶于这可以节省多少费用。
停止或终止不需要的 EC2 实例
与前一部分关于仅在必要时运行实例的建议类似,如果实例完全不需要,考虑将其彻底删除。除非你正在使用非常特定类型的实例,否则不会对不存在的 EC2 实例收费。养成使用完就删除的习惯。如果你认为未来可能需要某个服务器但又不确定,考虑为该服务器创建 AMI 然后将其移除。尽管 AMI 本身会产生费用,但这会比运行实际服务器便宜。
这些建议不仅仅适用于 EC2 实例;AWS 中的其他服务如果不进行清理也会产生费用。确保关注其他按使用量计费的组件和服务,例如 RDS、S3、EBS 卷等。
所以,就这些内容来说——通过一些基本的建议,你应该能控制你的账单。即使你犯了错误,只要你设置了账单提醒,你应该会收到通知并能够迅速纠正问题。我理解如果一些账单建议让你感到有些不知所措,但你会逐渐适应的。AWS 有很多组件会构成账单,但只要你设置了提醒并删除不使用的项目,就不会有问题。
我们在本章中已经讲了很多内容,但接下来该做什么呢?在接下来的章节中,我会提供一些关于如何继续学习并将你的 AWS 技能提升到更高水平的建议。
进一步探索云计算:更多资源帮助你提升知识
AWS 是一个庞大的服务平台,我们在本章中只触及了它的表面。我们在之前的章节中创建了一个简单的负载均衡应用,接下来的章节中,我们还会学习如何自动化创建云资源。如果你觉得本章内容有趣,想要更深入地使用 AWS 并提升技能,我想提供一些额外的建议,希望能帮到你。
在线培训和实验室
网上有很多资源可以帮助你扩展知识。其中一些资源是免费的,例如 AWS 网站的一部分提供免费的实践培训:aws.amazon.com/training/digital/
虽然你可能已经知道 YouTube 在培训视频方面的价值,但它仍然是一个很好的知识来源。(你甚至可能已经偶然发现了我的 YouTube 频道——Learn Linux TV。)
YouTube 上有很多可以提供培训的视频,但这并不是唯一的视频内容来源;Packt 出版公司也提供视频培训课程,此外,Udemy 也有一些很棒的内容。
总体来说,培训资料是丰富的,但我建议你先从我上面提到的 AWS 提供的免费培训开始,并参考亚马逊在aws.training上提供的额外培训内容。
认证
虽然需要付出一些努力,但获得一个或多个 AWS 认证将引领你走上一条可以更深入了解该平台的道路。此外,拥有 AWS 认证的人在 IT 行业中需求量很大,因此获得认证是一个不错的选择。我推荐你了解AWS 认证云从业者证书,它是一个更适合刚入门的人的初级认证。
在你提升自己的专业知识后,可能会考虑获得AWS 认证系统运营管理员证书,它更具挑战性,但能进一步提升你的知识。
继续尝试和学习
即使您决定不去获得认证,也要继续在平台上进行实验。亲自操作技术并定期使用它通常是学习和保持技能的最佳方式。尝试创建其他类型的基础设施,构建和重建测试实例,最重要的是,享受过程。对很多人来说,学习某样东西最好的方式就是亲身体验。我还建议您关注任何关于云计算和相关技术的博客。
AWS 文档
AWS 提供的文档写得非常好且详尽。您可以仅凭 AWS 文档了解所需的所有信息。这些文档页面超出了您对如此大型服务通常期待的质量水平;亚马逊非常重视 AWS 文档。这些文档页面可以在这里找到: docs.aws.amazon.com/index.xhtml。
用于学习 AWS 的新资源定期发布,所以在学习平台的过程中,保持关注新书籍、培训视频等。我相信,提升技能、探索 AWS 平台的所有功能将非常有趣。
总结
这一章是到目前为止全书中最具挑战性的一章,您已经完成了许多任务。在这一章中,您学习了 AWS,搭建了自己的云服务器,设置了自动伸缩,以确保服务器能自动从灾难中恢复,甚至配置了负载均衡器,以便在多个实例之间进行路由。请确保在继续之前,花些时间消化这些知识,我也建议您在进入下一章之前,花些额外的时间深入了解 AWS。
说到下一章,我们将再次与 AWS 合作,但这次我们将专注于学习 Terraform,这是一个很棒的工具,它将使我们能够从零开始自动化构建云资源。这个过程会非常有趣。
进一步阅读
-
亚马逊弹性计算云文档:
learnlinux.link/ec2-docs -
您 VPC 的安全组:
learnlinux.link/vpc-docs -
自动伸缩文档:
learnlinux.link/as-docs -
亚马逊机器映像(AMIs):
learnlinux.link/ami-docs -
亚马逊弹性 Kubernetes 服务文档:
learnlinux.link/eks-docs -
AWS 计费警报文档:
learnlinux.link/cw-docs
加入我们在 Discord 上的社区
加入我们社区的 Discord 空间,与作者和其他读者讨论:

第二十章:使用 Terraform 自动化云部署
上一章尤为有趣:我们能够在云中部署 Ubuntu,利用亚马逊网络服务(AWS)。在云中部署基础设施非常强大,允许我们完成通常无法实现(或非常繁琐)的事情。我们可以在几分钟内启动 Ubuntu 实例,甚至设置自动恢复,以应对通常会导致完全服务中断的情况。
这一次,我们将再次处理云部署,并了解一个名为Terraform的强大工具,它将允许我们自动化我们的云资源的配置。在第十五章,使用 Ansible 自动化服务器配置中,我们已经探讨了自动化的概念,并了解了 Ansible 的基础知识。Terraform 使我们能够将我们的自动化提升到一个新的水平,甚至可以直接与 AWS 等服务提供商进行交互。
在本章中,我们将探讨以下概念:
-
为什么自动化基础设施很重要
-
简介 Terraform 及其如何在您的工作流中适用
-
安装 Terraform
-
自动化 EC2 实例部署
-
使用 Terraform 管理安全组
-
使用 Terraform 销毁未使用的资源
-
将 Ansible 与 Terraform 结合以实现完整的部署解决方案
为什么自动化基础设施的构建如此重要?这样做有许多好处,我们将在下一节中看到其中一些好处。
为什么自动化基础设施很重要
关于基础设施自动化的话题非常广泛,它很容易就能值得一本书来讲述。事实上,不仅有专门的书籍,还有整个在线课程也是专门讲这个的。您可以使用许多不同的实用程序,每个都有其优缺点。我们有配置管理工具,如 Ansible、Chef 和 Puppet。我们在本书的早期已经看过 Ansible,并通过一些示例来了解其强大之处。当我们早些时候使用它时,我相信您立即看到了好处——不必手动构建解决方案是一件美妙的事情。
不得不强调不需要手动构建解决方案的重要性。也许最明显的好处是它可以节省你数小时甚至数天的工作时间。当我刚开始从事 IT 工作时,设置服务器总是一个手动任务。当然,你可以创建一个 Bash 脚本并通过这种方式自动化一些任务,但专门设计用于自动化的工具将更有效地处理任务。一个通常会因为设置大量服务器而感到不知所措的 IT 员工,通过自动化可以更快地完成相同的任务。并且通过节省的所有时间,IT 员工可以专注于其他任务,而不是将大部分时间花在一个任务上。
自动化的另一个好处是人为错误的可能性大大降低。当你构建自动化解决方案时,犯错误是不可避免的。你在编写脚本时可能会输入错误,导致语法错误,或者某些东西没有按照你预期的方式创建。但在你花时间构建并验证自动化脚本没有错误后,你可以反复运行它们,每次基础设施都会以相同的方式创建。对比每次手动设置服务器以实施新解决方案时,错误可能会发生的频率,你可以想象有多少次可能需要修正的错误,其中一些甚至可能直到后期才被发现。
自动化还有一个你可能没有预料到的好处——灾难恢复。虽然我们将在第二十三章《防止灾难》中详细讨论灾难恢复,但现在值得一提的是,制定一个有效的自动化解决方案将使恢复过程更加迅速。作为管理员,最糟糕的噩梦之一就是想象对组织至关重要的服务器某天可能会出现故障,但这却是生活的一个事实。
我们的组织可能有一个非常复杂的应用程序,包含一个或多个 Web 服务器、负载均衡器、安全设置等。手动重建这样的解决方案可能需要几个小时。但通过自动化,你只需要运行你的脚本,就能在几分钟内重建相同的解决方案。自动化本身无法保护你免于丢失数据(这将是一个更可怕的问题),但至少它可以帮助你比手动操作更快地配置替代资源。不仅如此,我推测你的客户(以及你的上司)会更希望你的组织的应用程序在几分钟内恢复上线,而不是几个小时或几天。
此外,你的自动化脚本可以作为一种活文档形式存在。即使你不打算重新配置你的服务器和相关基础设施,其他管理员也可以查看你的自动化脚本,更好地理解整体解决方案中包含哪些组件,这样如果他们接管了基础设施的管理工作,也能更快地上手。
自动化是那种我可能不需要费力推销的东西,因为如果你已经有过 IT 工作经验,你自然知道手动重建服务器是多么繁琐。
有时候,可能会感觉我们要完成的任务比工作日里的小时数还多。但是通过自动化,我们可以找回一些时间,甚至可能稍微降低一点压力。而且这并不是我们第一次使用自动化;我们在本书的早些章节已经看过 Ansible,所以你大概已经非常清楚它的好处。不过,在本章中,我们将实现比 Ansible 更低层次的自动化,我们将使用一个叫做Terraform的解决方案。你可能会问,Terraform 是什么?在科学领域,地球化学是一项令人惊叹的过程,它将一个无法居住的星球转变为一个能够支持我们已知生命的星球。但在我们的用途中,Terraform 是一个我们可以用来自动化整个云计算实现的强大工具。接下来的部分,我们会更详细地定义它。
Terraform 介绍以及它如何融入你的工作流
Terraform 是由一家名为 Hashicorp 的公司创建的一个令人惊叹的工具,它可以在比 Ansible、Puppet 或其他配置管理解决方案更低的层次上自动化你的基础设施。事实上,Terraform 通常不会替代这些工具,而是与它们互补。使用配置管理工具时,我们通常需要首先创建初始服务器并设置操作系统,然后才能实现它们。虽然 Ansible 实际上有方法可以用来创建基础设施组件,但这超出了本书的范围。
不仅如此,虽然 Ansible 可以创建某些类型的基础设施,但这并不是它最擅长的。为了理解像 Terraform 这样的工具的作用,最好将 Terraform 看作是用来创建事物的存在,而 Ansible 则是用来处理已经存在的事物,确保它们的配置正确。
在 Terraform 本身方面,它让你能够利用一个非常棒的概念——基础设施即代码。在上一章,我们在 AWS 中设置了一个完整的负载均衡应用。我们创建了一个 EC2 实例,以及一个 AMI,然后建立了负载均衡器和自动扩展功能。尽管这个过程非常有趣,但它是手动的。如果在过程中犯了错误,你必须去修复它们。完成后,你的解决方案已经创建并正常运行。Terraform 让我们能够编写代码,表示我们想要的最终状态。当它运行时,它会检查云提供商并进行库存检查。如果我们在脚本中添加的内容在云提供商处不存在,它会确保当前状态与我们代码中的最终状态一致。我们甚至可以在首次登录 AWS 后,不再登录,直接提供整个云解决方案。
在选择自动化工具时,一个重要的考虑因素是工具是否跨平台。许多云服务提供商都有内置工具,可以实现 Terraform 所做的相同功能。例如,AWS 有一个叫做 CloudFormation 的功能,允许你像使用 Terraform 一样脚本化基础设施的构建。但问题在于,CloudFormation 只针对 AWS 特定设计。你无法使用该服务在微软 Azure 或 Google Cloud 中构建基础设施。跨平台工具可以在任何环境中运行。我们之前在本书中已经看到过 Ansible:Ansible 不在乎你配置的服务器是位于 AWS,还是即使它们是机架中的物理机器。对 Ansible 来说,Ubuntu 就是 Ubuntu,无论它运行在哪个环境中。这让你可以在多个环境中使用相同的工具,而无需为每个环境重新创建一套自动化脚本。Terraform 同样也是一个跨平台工具。
为什么工具是否跨平台很重要?如果你需要维护多个完全不同的工具来执行相同的任务,那就是浪费时间。如果你能够学会使用一个工具,并且能够在你支持的每个环境中使用它,那么维护负担会小得多。这也是我一直建议避免使用平台特定工具的原因,比如 AWS 中的 CloudFormation。AWS 里甚至还有一个名为 OpsWorks 的工具,它用于与 Ansible 相同的目的(配置管理),但同样只限于 AWS。
一个典型的组织会在公司发展的不同阶段多次进行调整。一个 100%使用 AWS 基础设施的组织,有一天可能会决定支持其他云服务提供商。有时候,正是因为遇到了合适的客户或情况,才会促使公司考虑在一个通常不考虑的项目中使用其他云服务提供商。
也有可能是因为当前平台发生了变动导致成本增加,或者其他原因,企业可能会更换主要的服务提供商。如果你使用跨平台工具,那么在更换服务提供商时,你大部分的工具仍然可以带走。此外,能够支持多个服务提供商不仅使你成为一个更强大的管理员,还能为你的组织提供额外的价值。
不过,Terraform 在不同云平台之间并不会 100% 一致。其语法会根据云服务提供商的不同而有所变化。目前,似乎还没有一个可以在不同环境之间 100% 可移植的基础设施即代码(IaC)解决方案。但考虑到像 CloudFormation 这样的解决方案在其他平台间是完全不可转移的,Terraform 相比之下仍然占优,因为它是一个可以在多个服务提供商间使用的工具。Terraform 的通用工作原理在每个提供商之间都保持一致,因此即便你更换服务提供商,使用它依然能够节省时间。
Terraform 是如何工作的?我们将在下一节安装 Terraform,并在随后的节中使用它来部署 EC2 实例。但简而言之,Terraform 是一个你可以下载到本地笔记本电脑或台式机的工具,使用它将脚本文件转化为实际的基础设施。它支持许多不同的云平台,如 AWS、GCP 等。它甚至支持虚拟私人服务器(VPS)提供商,如 Digital Ocean 和 Linode。Terraform 将每个平台称为提供商,并允许你在 Terraform 中下载适当的插件,以支持你选择的提供商。
正如本章后面所提到的,Terraform 允许你先测试配置,并预览它将进行的更改。然后,如果你接受这些更改,它将连接到你的服务提供商,并根据你在代码中定义的方式创建基础设施。尽管我们本章不会涉及版本控制,但典型的组织会将其 Terraform 代码存储在 Git 仓库或其他版本控制系统中,以防止代码被意外删除。在典型的组织中,一个或多个管理员将与 Terraform 代码一起工作,并将他们的更改推送到仓库中。
在下一节中,我们将详细介绍安装 Terraform 的过程,这样我们就可以准备好一切,开始围绕我们的基础设施构建自动化。
安装 Terraform
运行 Terraform 并使用它来配置云资源的过程通常是在本地笔记本电脑或台式机上启动的。Terraform 本身是从其官方网站下载的,并且适用于所有主流操作系统。
与大多数应用程序不同,Terraform 没有安装程序。Terraform 是直接从你下载的文件运行的;没有需要经过的安装过程。如果你愿意,你可以将其安装到系统范围内,但你也可以从任何你希望的目录中运行它。Terraform 的下载文件位于以下网址:www.terraform.io/。
到达该网站后,你应该能看到一个 下载 按钮:

图 20.1:Terraform 网站
点击 下载 按钮后,你会看到一个新页面,提供适用于六种不同操作系统的 Terraform,包括常见的 Linux、macOS 和 Windows。大多数情况下,它会自动选择你访问该网站时使用的操作系统。例如,以下是下载 macOS 版本时页面的样子:

图 20.2:Terraform 网站,macOS 下载页面
此时,你只需下载适合你操作系统的 Terraform 版本。如今大多数电脑都是 64 位的,所以选择要下载的版本应该很简单。如果你想在树莓派上运行 Terraform,请选择适用于 Linux 的 Arm 版本。下载后,你将得到一个本地的 ZIP 文件,解压后,你会找到一个名为terraform的二进制文件,这就是你实际需要的全部。
你将能够直接在操作系统的命令提示符中运行terraform:

图 20.3:在终端窗口中运行 terraform,无任何选项
在截图中,我输入了下载并解压后的terraform文件的完整路径,该文件保存在我的主目录下的downloads文件夹中。我没有使用任何选项运行它,因此它打印出了帮助页面。
如果你想系统范围内安装它,可以将terraform移到/usr/local/bin目录下,如果你使用的是 Linux 或 macOS:
sudo mv terraform /usr/local/bin
/usr/local/bin目录被 Linux 和 macOS 都识别为会搜索二进制文件的目录。这个概念被称为你的$PATH,它是一个特殊的变量,包含了你个人资料设置中定义的所有目录,用于执行命令时查找。这种将新目录添加到$PATH的方法在不同操作系统间有所不同,但对于 macOS 和 Linux 来说,/usr/local/bin已经被识别,因此当你将terraform复制到该目录时,你应该能够直接在终端中输入terraform,而不需要每次都输入完整的路径。这个操作是可选的,但它使得操作更简便。
为了让 Terraform 能够与 AWS 配合使用,我们需要为它生成一个 API 密钥。这是在 AWS 管理控制台中完成的,你现在应该登录到控制台,以便我们创建所需的内容。在上一章中,我们讨论了 IAM,这是 AWS 中的一项服务,它不仅允许你为其他管理员创建用户账户,还能为程序化访问创建密钥。后者将是我们允许 Terraform 连接到我们的 AWS 账户,并代表我们执行任务的方式。
在 AWS 管理控制台的 IAM 部分,点击左侧菜单中的用户链接,然后点击该页面上你应该能看到的蓝色添加用户按钮。接下来会出现如下表单:

图 20.4:创建用于运行 Terraform 的 IAM 用户
在我的情况下,我决定将用户命名为terraform-provisioner,但你可以使用任何你喜欢的名称。我勾选了程序访问旁边的框,并且没有勾选第二个框,因为我不希望该用户能够登录到控制台。点击下一步:权限继续。在接下来的页面中,我们将设置用户能够访问的策略:

图 20.5:为 Terraform 创建 IAM 用户(继续)
在这个界面中,点击直接附加现有策略的框以选中它,并勾选下面的框来为该对象添加AdministratorAccess策略。这是授予 Terraform 与 AWS 交互权限的策略。
点击下一步:标签继续,然后在下一个界面,你可以跳过添加标签(除非你想添加它们),接着点击下一步:审查,然后点击创建用户完成该过程。
最终显示的界面应该会报告过程成功,并且下载 .csv按钮可以让你下载密钥。你也可以通过点击显示按钮查看密钥,如我在图 20.6中所做的那样:

图 20.6:为 Terraform 创建 IAM 用户(最终界面)
不过,我想先提醒你一些关于密钥的注意事项。首先,无论你是下载密钥还是点击显示按钮查看密钥,这是你唯一一次看到它的机会。你将不会再有机会下载完整的密钥。我建议你下载密钥并将其存储在一个安全的地方。你应该保护密钥,不让任何人访问它,绝对不要将它上传到版本控制仓库或任何其他公开资源中。你也绝对不要在书中以明文形式展示密钥,让很多人都能看到。如果这个密钥落入不法分子之手,那么任何拥有它的人都能与你的 AWS 账户交互。请小心保管这个密钥。我之所以在这里展示它,是因为我想让你看到整个过程的实际情况。在这本书的出版过程完成之前,我会从我的 AWS 账户中删除它。至于你,绝对不要让这个密钥泄漏!
现在我们有了一切所需的资源,可以继续使用 Terraform 构建 AWS 资源了。在下一节中,我们将创建一个 EC2 实例。
自动化 EC2 实例部署
让我们来看一个示例 Terraform 配置文件,它将帮助我们构建一个 EC2 实例:
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "my-server-1" {
ami = "ami-09d56f8956ab235b3"
associate_public_ip_address = "true"
instance_type = "t2.micro"
key_name = "jay_ssh"
vpc_security_group_ids = [ "sg-0597d57383be308b0" ]
tags = {
Name = "Web Server 1"
}
}
Terraform 文件以 .tf 文件扩展名保存,至于文件名,你可以随意命名。我将它命名为 terraform_example_1.tf。文件名中的下划线并不是必须的,但它能使命令行操作更方便,因为你不需要对空格进行转义。我将我的 terraform_example_1.tf 文件放在一个专门的目录中,这是推荐的做法。你的 Terraform 配置文件应该与其他文件分开,因此为这些文件创建一个专门的目录是理想的。
至于实际的代码,让我们一节一节地来看:
provider "aws" {
region = "us-east-1"
}
provider 块告诉 Terraform 我们将使用的提供程序类型。我们在这里设置为 aws。 正如前面提到的,Terraform 能够与各种云提供商合作,其中 AWS 只是其中之一。在此之下,我们将 region 变量设置为 us-east-1。建议您设置与上一章节使用的区域相同,这将使我们的过程更加简单,因为我们已经有一些可以重复使用的资源。
resource "aws_instance" "my-server-1" {
在这里,我们开始一个新的资源块。每个提供程序都有自己的构建块(资源),特别是针对 AWS,我们可以使用 aws_instance。在这一行中,我们还给实例命名,并称其为 my-server-1。
请注意,这是我们在 Terraform 中提供的名称,而不是在 AWS 中实际使用的名称。在 Terraform 中,我们希望有一些名称来引用此特定的 AWS 实例,以便在其他地方需要引用时使用。
ami = "ami-09d56f8956ab235b3"
接下来,我们选择要用于我们实例的 AMI。如前一章节讨论的,AMI 是我们可以用来在 AWS 中构建服务器的映像。我在这里使用的实例 ID 是与 AWS 默认提供的官方 Ubuntu 22.04 AMI 相关联的。AMI 是特定于创建它们的区域,因此这里的实例 ID 是特定于 us-east-1 的。
如果您也在使用 us-east-1,则可以直接使用上述的 AMI ID(只要未来 AWS 没有用新的替换它)。如果有疑问,您可以进入 AWS 控制台,然后导航到 EC2 控制台,并按照手动创建基于 Ubuntu 的 EC2 实例的过程,从那里复制 AMI ID。甚至更简单的方法是,您可以使用由 Canonical 直接提供的 Amazon EC2 AMI Locator 来查找要使用的 AMI ID:cloud-images.ubuntu.com/locator/ec2/。
您可以按 Ubuntu 版本和位置过滤该列表。这样,您可以找到符合您选择区域的 Ubuntu 22.04 AMI 的 AMI ID。将示例代码中的 AMI ID 更改为您希望使用的 AMI ID。
您可能会注意到 ami 和 = "ami-09d56f8956ab235b3" 之间有很多空格。在 Terraform 语法中,将每行的等号对齐是一种常见的做法,这样可以使代码看起来更整洁。虽然不是必需的,如果您不完全对齐,也不会出现问题,但有些人可能会认为整体脚本看起来更清晰。
associate_public_ip_address = "true"
通过这行,我们决定在我们的实例中使用公共 IP 地址,如果我们希望能够远程访问它,则需要这样做。
instance_type = "t2.micro"
在这里,我们设置了我们新创建的服务器的所需实例类型。如前一章节讨论的,有多种实例类型可用,每种类型都有不同的成本。t2.micro 实例类型符合免费层条件,这就是我选择它的原因。
key_name = "jay_ssh"
在前一章中,当我们手动创建 EC2 实例时,其中的一部分过程是创建 OpenSSH 密钥。你创建的密钥已经注册到你的 AWS 账户,因此你可以通过你给它命名的名称来引用它。我将它命名为jay_ssh,但你需要将其更改为你为自己的密钥起的名字。你可以在 AWS 的 EC2 控制台中查看你的 OpenSSH 密钥列表;在菜单中有一个叫做密钥对的部分,如果你忘记了密钥的名字,可以在那里找到它。
vpc_security_group_ids = [ "sg-0597d57383be308b0" ]
在上一章中,我们创建了一个安全组,允许 Apache 和 OpenSSH 与我们的实例通信。当你创建安全组时,它会被指定一个安全组 ID。我在示例中使用的安全组 ID 是系统为我生成的,因此对你无效。如果你访问 EC2 控制台中的安全组部分,你可以找到你创建的安全组的 ID。它的 ID 应该以sg-开头,后面跟着一串字符。将你的 ID 替换我示例中的 ID。
tags = {
Name = "Web Server 1"
}
在示例的最后部分,我们正在设置一个标签。如前一章所述,AWS 允许你创建标签,这些标签可以附加到实例上,提供关于其预期用途的附加信息。Name标签是一个特殊的标签,它会更改你在 AWS EC2 列表中看到的实例名称。你可以将其命名为任何你喜欢的名字。
此时,我们应该已经准备好运行 Terraform,使用我们的 Terraform 文件作为蓝图来创建实例。首先,我们需要为 Terraform 设置访问密钥和秘密访问密钥。在你的终端中,你可以输入以下命令来完成此操作:
export AWS_ACCESS_KEY_ID="AKIAVNXBZU2OBNWQQ7ET"
export AWS_SECRET_ACCESS_KEY="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"
这些命令只是从你的终端运行,并创建包含所需密钥的环境变量。当 Terraform 运行时,它会查找AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY变量的存在,通过导出它们,我们让它们在当前会话中可用。实际上,你也可以将密钥直接添加到 Terraform 文件中,但我们不建议这么做,因为如果我们将整个文件上传到公共场所,密钥可能会被泄露。你也可以在 Terraform 文件中设置变量来包含这些密钥,但那超出了本章的讨论范围。
导出变量后,我们需要初始化 Terraform,以确保它拥有与 AWS 交互所需的所有组件:
terraform init
使用这个示例,如果terraform工具不在共享的 $PATH 路径中,你可以添加完整路径,例如 /usr/local/bin,这是我之前提到的推荐位置。如果你将它复制到/usr/local/bin,那么你应该可以直接输入 terraform,而不需要完整路径。
terraform init 命令指示 Terraform 进行初始化。它会查看您当前工作目录中的所有 Terraform 文件,并查找 provider 行。在我们的案例中,这一行位于文件的开头。我们设置为 aws。这将触发 Terraform 下载 AWS 的提供程序插件:

图 20.7:初始化 Terraform
从图 20.7中可以看到,当我在本地运行该命令时,它下载了 AWS 提供程序,以便进行后续使用。
接下来,我们应该运行一个叫做Terraform 计划的命令。运行该命令会指示 Terraform 不进行任何更改,而是检查您的语法并确保您没有输入错误:
terraform plan
terraform plan 命令不仅仅是检查语法,它还会连接到您的提供者,在我们这个例子中是 AWS,进行清单盘点,并将配置文件中的更改与提供者的当前状态进行比较。如果无法连接到提供者,则会返回错误。如果连接成功,Terraform 会列出它本来会做出的所有更改,前提是您指示它执行这些任务。在 plan 模式下,它永远不会实际执行任何指令,而只是为您提供预览。
如果由于某种原因 Terraform 无法连接到您的 AWS 账户,您应确保之前已运行过两个 export 命令,并且使用了正确的值。如果您关闭了终端窗口,您需要重新运行这些 export 命令,因为这些环境变量不会在终端会话之间持久化。如果成功,Terraform 计划将会运行:

图 20.8:运行 Terraform 计划
在图 20.8中,我已经省略了大量输出。如果您的计划运行成功,Terraform 会提供有关所有更改的概览,这些更改本来会在您实际指示它来配置基础设施时发生。
如果您想要实际执行这些更改,您可以运行 Terraform apply 命令。然而,在执行之前,始终养成先查看计划输出的习惯。请注意输出中的以下一行:
Plan: 1 to add, 0 to change, 0 to destroy
在我们的例子中,它不会销毁或更改任何内容,但如果运行,它会添加一些内容。如果您向上滚动,您可以找到更多关于它可能会做出的更改的详细信息,特别是如果我们运行 apply 的话。特别注意它可能想要销毁的内容。对于某些更改,Terraform 可能认为有必要删除某些内容并从头开始重新创建。如果您使用 Terraform 更新现有服务器,您很可能不希望删除该服务器。在这种情况下,请不要继续运行 apply。在您继续并实际让它执行任务之前,始终仔细审查 Terraform 想要做的更改before。
接下来,假设我们对更改已经感到满意,我们将继续执行 apply。请记住,虽然通过 Terraform 运行计划时,它会查找并报告语法错误,但计划过程通过且没有报告语法错误并不意味着没有错误。在实际运行之前,Terraform 能做的检查有限,因此可能存在它只能在 apply 过程中捕捉到的错误。正如你可以猜到的,运行 apply 的命令非常显而易见:
terraform apply
当运行时,terraform apply 命令会再次进行健康检查,重新显示更改的数量,并询问你是否希望继续:

图 20.9:运行 terraform apply 命令
接下来,输入 yes 并按 Enter。在运行过程中,你实际上可以看到正在创建的资源出现在 AWS 控制台中,随着它们的配置完成。在这里,以创建 EC2 实例为例,我们可以看到新的实例出现在列表中:

图 20.10:由 Terraform 创建的 EC2 实例在 AWS 中的实例列表中显示
如果一切顺利,Terraform 本身会报告进程成功:

图 20.11:成功的 terraform apply 过程
现在我们已经在 AWS 中创建了一个 EC2 实例,并且是通过自动化实现的。虽然目前它对我们帮助不大,但这只是一个概念验证。我们可以通过 Terraform 做很多事情。
不过,缺少了一样东西——安全性!我们还应该自动化将安全组添加到实例的过程,这样我们就能获取连接并管理实例所需的访问权限。现在我们能够访问该实例,但很可能它还没有外部互联网访问权限。在下一节中,我们将为实例配置安全组,这样我们就可以配置哪些端口是开放的,哪些 IP 地址能够与我们的实例进行通信。
使用 Terraform 管理安全组
安全组,如你在前一章中学到的,允许你控制哪些资源可以与其他资源进行通信。在上一节中,我们复用了上次创建的安全组,但理解如何从头开始创建一个安全组也是很有用的。
这是更新后的示例 Terraform 文件,添加了一些新的代码:
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "my-server-1" {
ami = "ami-09d56f8956ab235b3"
associate_public_ip_address = "true"
instance_type = "t2.micro"
key_name = "jay_ssh"
vpc_security_group_ids = [ "${aws_security_group.external_access.id}" ]
tags = {
Name = "Web Server 1"
}
}
resource "aws_security_group" "external_access" {
name = "my_sg"
description = "Allow OpenSSH and Apache"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ "172.11.59.105/32" ]
description = "Home Office IP"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [ "172.11.59.105/32" ]
description = "Home Office IP"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
我在文件中添加了一个全新的章节,但在进入这一部分之前,我还修改了之前示例中的一行。它是第十行:
vpc_security_group_ids = [ "${aws_security_group.external_access.id}" ]
之前,我们将文件中的这一行的安全组 ID 设置为已经存在的安全组 ID,也就是我们在上一章创建的那个安全组 ID。接下来我所添加的配置将在文件的后面创建一个新的安全组,并且这里我将安全组 ID 设置为一个变量。${aws_security_group.external_access.id}变量在这里被 Terraform 视为输出变量。我们使用输出变量来表示安全组 ID,因为我们无法预知安全组 ID 是什么,因为新的安全组还没有创建。所以,我们在这里使用输出变量,并通过.id来引用我们将要创建的安全组(external_access)的 ID,这样一旦安全组创建完成,我们就可以引用它的 ID。这样,我们就可以在这里引用一个安全组并将其分配给实例,而不需要事先知道它的 ID。
在文件的后面,我们开始了一个新部分:
resource "aws_security_group" "external_access" {
通过这一行,我们告诉 Terraform 我们希望创建另一个新的资源,这次是一个安全组。我们给这个安全组起了一个名为external_access的名字,这只是 Terraform 中的一个引用名称,并不是它在 AWS 中实际使用的名字。
name = "my_sg"
这里,我们为安全组指定了它的实际名称,这是我们在 AWS 中看到的名称,而不是 Terraform 内部的名称。
description = "Allow OpenSSH and Apache"
对于description行,这里没有什么令人惊讶的地方:我们给它一个描述,用来解释其用途和安全组将要执行的功能。与我们在上一章手动创建的安全组类似,我们将通过这个安全组打开 OpenSSH 和 Apache。
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ "172.11.59.105/32" ]
description = "Home Office IP"
}
ingress块允许我们设置一个端口,允许从该端口接收连接;在这种情况下,我们允许从22端口接收连接,正如你可能已经知道的,22端口是 OpenSSH 的默认端口。我们不希望将此端口对所有公开互联网开放,因此我们只允许来自172.11.59.105/32 IP 地址的流量进入该端口。
在你的情况下,你可以将其替换为你家庭办公室或组织的公共 IP 地址。
第二个ingress块与第一个相同,只是这次它允许连接到80端口用于 Apache:
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [ "172.11.59.105/32" ]
description = "Home Office IP"
}
我们还添加了一个egress安全组规则,因为如果没有这个规则,我们的实例将无法访问互联网:
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
和之前的示例一样,我们需要先运行一个计划,然后再运行 apply,将我们的新代码变成现实。具体的操作我交给你来完成;只要没有输入错误,它应该会应用更改并添加新的安全组。在 AWS 中,你应该能在控制台看到新的安全组,并且它应该已应用到你的 EC2 实例上。除非你重复使用了上一章节中带有 Apache 的 AMI,否则你无法通过端口 80 连接到该实例,因为 Apache 很可能没有安装,但我添加了它来给你一个示例。
在这个阶段,我建议你先尝试一下我们目前的 Terraform 脚本,熟悉它的语法。你可以随意增加一些额外内容;可以参考 Terraform 文档,了解你可以使用 Terraform 创建的其他资源。
恭喜你成功使用 Terraform 来配置基础设施。现在,让我们用 Terraform 来销毁资源。
使用 Terraform 销毁未使用的资源
尽管 Terraform 的主要目的是创建基础设施,但它也可以用于删除基础设施。这一功能被称为Terraform destroy。使用 destroy 命令时,Terraform 会尝试删除配置文件中定义的所有基础设施。在此阶段,我们的配置文件创建了一个 EC2 实例以及一个安全组。如果我们运行 destroy,这两个资源都将被删除。
使用 Terraform 删除基础设施可能不会像创建资源那样频繁。但是,destroy 功能的一个价值在于,你可以通过删除文件中定义的所有内容来“重置”一个测试环境。然后,你可以使用同一脚本重新创建所有资源。在我这里,我通过不断打破和修复事物来加速学习。你真的不应该在生产环境中运行 destroy,尤其是对于那些你关心的基础设施,但如果你只是在一个没有重要实例的测试账户中使用 Terraform,那么你可以在学习过程中反复构建和拆解你的测试资源。另一个好处是,一个组织可以先在测试账户中为客户测试 Terraform 构建,然后再将其实施到生产环境中,你可以在为客户执行实际工作之前验证一切是否能够正确构建。
在 Terraform 中执行 destroy 和之前的示例一样简单:
terraform destroy
就像之前一样,我们首先会获得确认,然后它会告诉我们当运行 destroy 任务时,Terraform 想要删除哪些内容:

图 20.12:准备运行 terraform destroy 来删除资源
当你使用 destroy 选项运行 Terraform 时,请特别注意它想要删除的内容。截图没有显示完整的输出;它相当长。与 apply 类似,如果你向上滚动,你会看到输出中会包含有关如果我们同意继续,哪些内容将被删除的详细信息。如果你输入 yes 并按下 Enter 键,标识出的资源将被销毁,并且你会收到一条确认消息,确认任务已完成:

图 20.13:销毁之前预配置资源后的最终确认
terraform 命令的基本用法结构清晰;我们了解了如何运行 plan 和 apply,现在也知道如何销毁资源,以便从一个干净的状态重新开始。学习 Terraform 时的大部分时间将花在学习其配置文件的语法上,但这会随着时间的推移而掌握。在我们旅程的这一阶段,我们应该已经打下了一个坚实的基础,能够在此基础上进行构建。
然而,我们还没有完成!在本章中我多次提到 Ansible,提醒你我们曾经用它来配置服务器。但如果我告诉你我们可以将 Terraform 和 Ansible 结合使用呢?我们当然可以,并且将在下一节中做到这一点。
将 Ansible 与 Terraform 结合使用,提供完整的部署解决方案
自动化工具的最大优点之一是它们通常可以结合使用,从而提供共享的好处。Ansible 是我最喜欢的工具之一:你可以自动化安装软件包、创建用户、复制文件或任何其他你能想到的任务。如果你能够在命令行上执行某个任务,Ansible 很可能能够自动化这个过程。Terraform,如你刚才看到的,非常擅长创建新的基础设施并自动化服务器、网络以及 AWS 和其他平台的初始设置。如果我们将这两者结合起来,效果会更好。
我发现 Terraform 和 Ansible 的组合非常合适。根据我的经验,结合这两种解决方案效果很好;我们可以使用 Terraform 创建初始的服务器和基础设施,然后用 Ansible 自动化未来的增强功能。但实际上,比这更好的是;我们可以配置 Terraform 来实际启动初始的 Ansible 运行,这样我们只需运行一个脚本。在 Terraform 创建基础设施后,额外的设置配置将交给 Ansible 来处理。这是一个很棒的组合。
它是如何工作的?在上一章中,我们探讨了用户数据的概念,它是 AWS 中的一项功能,允许你在实例创建时运行一个脚本。我们用它来安装所有补丁,然后继续安装 Apache。我们讲解的示例是一个简单的 Bash 脚本,本身并不算很令人兴奋。当然,它确实有效,但我们可以实现更好的解决方案。你知道吗?我们已经做到了。在第十五章,使用 Ansible 自动化服务器配置中,我们能够利用 Ansible Pull,这是一种特殊的 Ansible 模式,允许我们从仓库中拉取代码,并在我们的实例上本地运行。我们编写的 Ansible playbook 为我们安装了 Apache,就像我们在上一章中的 Bash 脚本做的一样。作为回顾,我们可以运行以下命令来触发 Ansible Pull:
ansible-pull -U https://github.com/myusername/ansible.git
当然,这要求已经安装了 Ansible,而且仓库需要已经存在。如果你已经跟随这一章的内容并且仍然拥有我们创建的仓库,那么你已经具备了将 Ansible 与 Terraform 结合使用所需的一切。
为了避免你需要翻回第十五章,使用 Ansible 自动化服务器配置,这里是我们最终得到的 local.yml 文件:
---
- hosts: localhost
become: true
tasks:
- name: Install Apache
apt: name=apache2
- name: Start the apache2 services
service:
name: apache2
state: started
- name: Copy index.xhtml
copy:
src: index.xhtml
dest: /var/www/html/index.xhtml
如你所见,这个 playbook 正在安装 Apache,启动它,并且将 index.xhtml 文件复制过去,以替换默认的网页。用 Terraform 实现这一点相当简单。以下是我们的 Terraform 脚本,新增的一行已加粗显示:
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "my-server-1" {
ami = "ami-0dba2cb6798deb6d8"
associate_public_ip_address = "true"
instance_type = "t2.micro"
key_name = "jay_ssh"
vpc_security_group_ids = [ "${aws_security_group.external_access.id}" ]
user_data = file("bootstrap.sh")
tags = {
Name = "Web Server 1"
}
}
resource "aws_security_group" "external_access" {
name = "my_sg"
description = "Allow OpenSSH and Apache"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ "173.10.59.105/32" ]
description = "Home Office IP"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [ "173.10.59.105/32" ]
description = "Home Office IP"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
文件的新内容在第 #11 行。我们引用了一个启动脚本,在这个脚本中,我们将添加任何我们希望在新创建的实例上运行的命令:
user_data = file("bootstrap.sh")
bootstrap.sh 文件需要与 Terraform 配置文件位于同一目录下。不过该文件尚未创建,所以请创建它,并在其中放入以下内容:
#!/bin/bash
sudo apt update
sudo apt install -y ansible
sudo ansible-pull -U https://github.com/myusername/ansible.git
我们没有对文件进行过于复杂的修改,但我们所做的新增内容带来了巨大的好处。user_data 选项允许我们利用 AWS 内置的相同用户数据功能,并安排在实例首次创建时运行命令。在这个示例中,我们利用 user_data 选项对新实例运行一系列命令,安装 Ansible,然后启动 ansible-pull 来下载包含 Ansible playbook 的仓库并在本地运行。这个 playbook 本身是在第十五章,使用 Ansible 自动化服务器配置中设置的,所以我们只是利用我们以前已经创建的内容,让 Terraform 在启动实例时为我们触发 Ansible 作业。
这就带我们来到了本章的结束。我希望使用 Terraform 设置自动化的过程是一次有趣的体验;我自己确实很享受与它一起工作。
总结
有许多配置管理和资源提供工具可用于自动化我们的基础设施构建。在这一章中,我们介绍了 Terraform,并且将其与我们已经在使用的 Ansible 结合起来。通过使用 Terraform,我们能够自动化在 AWS 中创建一个 EC2 实例,并设置一个安全组来控制如何访问它。Terraform 是一个非常庞大的话题,本章中的概念只是一个开始。你可以用 Terraform 做很多事情,我强烈推荐你继续练习,尽快掌握它。
在下一章中,我们将学习一些可以用来为我们的 Ubuntu 服务器添加额外安全性的方式。虽然没有任何服务器是百分百安全的,但我们可以实施一些基本的安全措施,使服务器被攻破的可能性降低。这将是一个非常重要的章节,你一定不想错过。
加入我们在 Discord 的社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第二十一章:保护你的服务器
每个月似乎都会有关于公司服务器被攻破的新报道。在一些情况下,整个数据库最终会在互联网上公开,这可能包括敏感的用户信息,帮助不法分子窃取身份。Linux 是一个非常安全的平台,但它的安全性取决于管理员的设置。安全补丁会定期发布,但只有在安装后才有价值。OpenSSH 是远程管理必不可少的工具,但它也是威胁者试图攻入服务器的热门目标。备份是必不可少的,但如果不定期测试或它们落入错误的人手中,备份可能毫无用处。在某些情况下,甚至是你的员工也可能造成有意或无意的损害。在本章中,我们将探讨一些保护服务器免受威胁的方法。
在本章中,我们将覆盖:
-
降低攻击面
-
理解并响应常见漏洞和暴露(CVE)
-
安装安全更新
-
使用 Canonical Livepatch 服务自动安装补丁
-
加固 OpenSSH
-
安装和配置 Fail2ban
-
MariaDB 最佳实践:确保数据库服务器安全
-
设置防火墙
-
使用Linux 统一密钥设置(LUKS)加密和解密磁盘
-
锁定 sudo
首先,让我们讨论一下如何降低攻击面。
降低攻击面
你的 Ubuntu 服务器安装上很可能会运行一个或多个重要的应用程序,其中一些可能对外部互联网开放。例如,Web 服务器非常常见,因为 Web 服务器的主要目标就是提供一个用户可以访问的网站。
任何可以从你组织外部访问的应用程序,都是威胁者可能利用的入侵点。服务器的攻击面本质上是所有潜在可利用项目的列表。在安全方面,了解哪些应用程序必须远程访问,哪些应用程序可以被锁定非常重要。每一个你锁定的应用程序都会降低它被外部威胁控制的可能性。锁定这些东西的过程就是我们所说的降低攻击面。
理想情况下,在一个完美的世界里,我们会禁止所有外部连接到我们的所有服务器。威胁行为者无法侵入完全不可从外部访问的服务器。这并不意味着没有任何威胁,因不满的员工总是潜在的风险。但一个完全无法访问的服务器是最安全的。然而,完全禁止所有外部连接通常是不可行的。如果你的公司提供一个流行的公共网站,那么它必须对外开放。然而,如果你在服务器上运行的应用仅供内部用户使用,那么如果可能的话,你应该将其锁定。尽可能地,最好实施一种策略,即除非有业务需要,否则外部连接默认总是被禁止。
我们所说的“禁止”是什么意思?你可以通过多种方式禁止外部访问你服务器上的应用程序。最有效的方法是完全卸载该应用。如果根本没有安装应用程序,它就不可能成为问题。可能不用说,你应该卸载那些不必要的应用,但运行服务器的核心目的是为用户提供资源,因此你总会在服务器上运行一些应用程序(否则根本就没有必要搭建服务器)。除了移除应用程序,你还可以利用防火墙来仅允许特定的连接。实际上,我们将在本章后面讨论如何设置防火墙。
最重要的是,在新服务器部署后,管理员应该始终进行安全检查,确保它的安全性达到最佳状态。没有管理员能够想到所有的安全细节,即使是我们中最优秀的也可能犯错,但我们始终应该尽最大努力确保服务器的安全。确保服务器安全的方式有很多,但首先要做的是降低攻击面。这意味着你应该尽可能关闭漏洞,限制外部人员可能访问的内容。简而言之,如果某个服务不需要外部访问,就把它锁定。如果根本不需要,就直接移除它。
要开始检查你的攻击面,首先要做的是查看哪些端口正在监听网络连接。当攻击者试图侵入你的服务器时,他们几乎肯定会先进行端口扫描。他们会列出哪些端口在监听连接,确定哪些应用程序在这些端口上监听,然后尝试已知的漏洞列表来获得访问权限。要检查哪些端口正在你的服务器上监听,你可以使用ss命令进行简单的端口查询:
sudo ss -tulpn
sudo命令部分是可选的,但如果你包括了sudo,你将在输出中看到更多的信息。通常我会在这里包含一张截图,但信息太多,无法适应这一页面。从输出中,你将看到一个监听连接的端口列表。如果端口监听在0.0.0.0上,那么它正在监听来自任何网络的连接。
这可能是个坏消息。如果端口监听在127.0.0.1上,那么它实际上并未接受外部连接。花点时间用ss命令检查你的其中一台服务器,看看哪些服务正在监听外部连接。
了解了你的服务器正在监听哪些端口后,你可以决定如何处理每个端口。有些端口可能是必需的,因为服务器的整体目的是提供服务,而这通常意味着通过网络进行通信。所有这些合法端口应该以某种方式得到保护,这通常意味着在查看相关文档并遵循最佳实践(取决于特定服务)后配置该服务,或者启用防火墙,这将在设置防火墙部分中讨论。如果有任何端口不再需要,你应该关闭它们。你可以停止其守护进程并禁用它,或者直接移除相应的包。我通常选择后者,因为如果我改变主意,只需要重新安装该包即可。
OpenSSH 是你几乎总会在服务器上运行的服务。正如你已经非常清楚的,它是一个远程管理的好工具。但尽管它非常有用,它通常会成为任何攻击者试图侵入服务器时的首要目标。
我们不希望删除这个服务,因为这是我们希望利用的功能。我们该怎么办呢?别担心,我将在本章稍后专门讲解如何加固 OpenSSH。我现在提到这一点是为了确保你意识到,降低攻击面绝对需要对 OpenSSH 进行至少基本的安全调整。此外,我还会在本章中介绍 Fail2ban,它可以帮助为 OpenSSH 增加额外的安全层。
正如我之前提到的,我非常支持删除不需要的软件包。你安装的软件包越多,你的攻击面就越大。移除任何不绝对必要的东西是很重要的。即使某个软件包没有列为开放端口,它仍然可能在漏洞链中被利用。如果攻击者使用漏洞链,那意味着他们首先攻破一个服务,然后利用另一个(可能不相关)软件包中的漏洞来提升他们的权限并试图获得完全访问权限。因此,我必须强调,你应该删除服务器上不需要的任何软件包。获取所有已安装软件包列表的一个简单方法是使用以下命令:
dpkg --get-selections > installed_packages.txt
该命令将生成一个文本文件,文件中包含你在服务器上安装的所有软件包的列表。花点时间查看它。有任何你确定不需要的东西突出显示吗?你很可能不知道每个软件包的用途,可能有数百个或更多。文本文件中包含的许多软件包是分发所必需的,如果你希望服务器在下次重新启动时能够启动,这些软件包是不能移除的。如果你不确定某个软件包是否可以删除,可以在 Google 上做些研究。如果仍然不确定,也许你应该保持这个软件包不变,转而检查其他的软件包。通过在服务器上进行这样的操作,你或许永远也记不住每个软件包和库的具体用途,但你仍然能找到一些可以清理的东西。最终,你会得出一份典型的软件包列表,大部分服务器都不需要这些软件包,你可以确保在每次设置新服务器时删除它们。你甚至可以整理一份不需要的软件包列表,然后创建一个 Ansible playbook 来确保它们不会被安装。
在尝试清理不需要的软件包时,一个有用的技巧是使用以下命令检查是否有其他软件包依赖于你想要移除的软件包:
apt-cache rdepends <package-name>
例如,我在一个测试服务器上运行了针对tmux软件包的命令,但你可以使用任何你喜欢的软件包名称作为参数来检查是否有其他软件包依赖于它:
apt-cache rdepends tmux
我在我的终端上收到的输出如下:

图 21.1:在 Ubuntu 服务器上更新软件包
使用前一个命令的输出,你可以轻松识别是否有其他软件包依赖于你想要删除的软件包。在示例输出中,我们可以看到tmux实际上是作为ubuntu-server软件包的依赖项被安装的。这意味着tmux很可能是默认安装在你的服务器上的,但这可能会根据你是否自己安装了 Ubuntu Server 或是否使用了云镜像而有所不同。云服务提供商并不总是以相同的方式配置 Ubuntu Server 镜像。但至少,你可以识别出依赖关系,并做出是否可以安全删除软件包的更明智决定。
即使输出显示某个软件包没有依赖关系,除非你理解它提供的功能以及删除该软件包可能对系统产生的影响,否则你仍然不应删除它。你可以随时通过 Google 搜索软件包名称来获取更多详细信息,但至少你应该查看开放的端口,并优先关注这些端口,因为开放端口对服务器的安全性影响更大。我们将在本章稍后的设置防火墙部分更详细地讨论这个问题。
另一个重要的考虑因素是确保只使用强密码。这点可能不言而喻,因为我相信你已经理解了强密码的重要性。然而,最近我看到一些黑客攻击事件是由于管理员为他们面向外部的数据库或 Web 控制台设置了弱密码,因此你永远无法预料。最重要的规则是,如果必须有一个服务监听外部连接,那么该服务的密码必须是强密码,并且最好是随机生成的。当然,某些守护进程并没有与之关联的密码(例如 Apache;它不需要身份验证就能让某人查看80端口上的网页)。然而,如果某个守护进程确实需要身份验证,它的密码应该非常强大。OpenSSH 就是一个例子。如果你必须允许外部访问 OpenSSH,那么该用户账户的密码应该是一个强大且随机生成的密码。否则,它很可能会在几周内被大量自动扫描的机器人接管。实际上,最好完全禁用 OpenSSH 中的密码验证,这一点我们将在本章稍后讲解。禁用密码验证会大大增强 OpenSSH 的安全性。
最后,重要的是对所有用户账户采用最小权限原则。你可能已经从书中提到的多个方面感受到我对用户的怀疑。虽然我总是希望对每个人持最好的看法,但有时最大的威胁往往来自内部(如不满的员工、意外删除关键文件等)。因此,尽可能地限制用户账户的权限,只允许他们访问执行工作所需的资源。这可能包括但不限于:
-
将用户添加到最少的组中
-
默认将所有网络共享设置为只读(用户不能删除他们没有权限删除的文件)
-
定期审计所有服务器,检查是否有用户账户长时间未登录
-
为用户账户设置到期时间,并要求用户重新申请以维持账户状态(这可以防止账户长期闲置)
-
允许用户账户访问尽可能少的系统目录(如果可能,最好是没有访问权限)
-
限制
sudo命令仅对特定命令有效(稍后在本章中会详细讲解)
最重要的是,确保记录下你为提高服务器安全性所做的每一项更改。在你列出一份清单后,可以将其转化为一个安全检查清单,作为加固服务器的基准。然后,你可以设置提醒,定期扫描服务器是否存在未使用的用户账户、不必要的组成员身份或新开放的端口。
现在,你应该已经有了一些降低攻击面的方法。同时,了解与安全漏洞相关的最新趋势和通告也很重要。在接下来的部分中,我们将讨论常见漏洞与暴露(CVE),它将帮助你更好地理解现实中威胁的性质。
理解并应对 CVE
我已经提到了一些可以用来保护服务器免受常见威胁的措施,稍后在本章中我还会提供更多建议。但是,如何知道何时有漏洞需要修补?如何判断何时需要采取行动?我在本章中提到的最佳实践虽然有效,但总有一天,可能会遇到一些超出生成强密码或锁定端口的安全问题。
最重要的是保持关注新闻。订阅一些报告安全漏洞的站点,稍后我还会将这些站点列在本章的进一步阅读部分。当安全漏洞被公开时,通常会在这些站点报道,并分配 CVE 编号,安全研究人员会记录他们的发现。
CVE(公共漏洞和暴露)可以在专门的在线目录中找到,这些目录详细列出了安全漏洞及其相关信息。实际上,许多 Linux 发行版(包括 Ubuntu)都维护着自己的 CVE 目录,列出了特定于其平台的漏洞。在这样的页面上,你可以看到你的发行版版本易受哪些 CVE 的影响,哪些已得到响应,以及为了应对这些漏洞需要安装哪些更新。
通常,当发现安全漏洞时,CVE 标识符会立即分配给该漏洞,即使还没有确定解决方法。在我的情况下,每当发现一个漏洞时,我通常会关注这个 CVE 页面,等待该页面更新,并提供解决该漏洞的方法。
通常,修补漏洞需要安装安全更新,这些更新由 Ubuntu 的安全团队创建,用于解决漏洞。在某些情况下,新的更新可能需要重新启动服务器,或者至少需要重新启动某些正在运行的服务,这意味着我可能需要等待一个维护窗口来执行修复。
我推荐查看 Ubuntu CVE 跟踪器,网址为 ubuntu.com/security/cves。在这个网站上,Canonical(Ubuntu 的开发商)会更新与 Ubuntu 平台相关的 CVE 信息。在那里,你可以找到已知的漏洞列表以及解决这些漏洞所需的步骤。没有一种固定的规则来保护你的服务器,但关注 CVE 是一个很好的起点。在接下来的章节中,我们将讨论安装安全更新,这是最常见的修复方法。
安装安全更新
既然我提到过多次更新包,我们来正式讨论一下这个话题。Ubuntu 会非常频繁地发布更新包,有时甚至是每天都会发布。这些更新主要包括最新的安全更新,但也可能包括新功能。由于 Ubuntu 22.04 是 LTS 版本,安全更新比功能更新更为常见。
在你的服务器上安装最新的更新是一项非常重要的实践,但遗憾的是,由于各种原因,并不是所有管理员都能够做到这一点。
安装后,安全更新通常不会对服务器进行太多更改,除非是帮助保持服务器抵御最新威胁的安全性。然而,安全更新有时也可能导致其他功能出现问题,尽管这种情况比较罕见,但我也见过这种情况发生。对于生产服务器而言,保持更新通常是很困难的,因为在负责公司大部分利润的服务器上进行任何更改都可能对公司造成灾难性后果。如果服务器宕机,可能会非常昂贵。但另一方面,如果你的服务器被攻破,而你的公司成为 CNN 黑客新闻的焦点,你肯定会后悔没有及时保持软件包更新!
一个快乐的数据中心的关键是,在安装任何更新之前先进行测试。许多管理员会设立一个系统,使得更新会从一个环境逐步升级到下一个环境。例如,有些管理员可能会创建他们生产服务器的虚拟克隆,更新它们,然后观察是否会出现任何故障。如果没有任何问题,那么这些更新就会被允许在生产服务器上安装。
在集群环境中,管理员可能只更新一个生产服务器,看看它受到了怎样的影响,然后安排时间更新其他服务器。在工作站的情况下,我见过一些策略,先选择特定用户进行安全更新,然后再将这些更新推送到其他用户群体中。我并不一定建议你把用户当作实验品,但每个组织都是不同的,找到合适的更新安装平衡非常重要。尽管这些更新代表着变更,但 Ubuntu 开发者之所以愿意进行这些麻烦的工作,还是有原因的。这些更新修复了很多问题,其中一些是安全问题,而这些问题已经在你阅读本文时被恶意利用。
要开始安装安全更新,第一步是更新本地的存储库索引。如前所述,执行的命令是 sudo apt update。这会指示你的服务器检查所有已订阅的存储库,看是否有新增的包,或有过时的包被移除。然后,你可以开始实际的更新过程。
有两个命令可以用来更新包。你可以运行 sudo apt upgrade 或 sudo apt dist-upgrade。
不同之处在于,运行 apt upgrade 命令不会移除任何包,且它是最安全的使用方式。然而,这个命令也不会拉取任何新的依赖包。基本上,apt upgrade 命令只是更新已经安装在你服务器上的任何包,而不会添加或移除任何内容。由于这个命令不会安装任何新东西,也就意味着你的服务器将不会安装更新的内核。
apt dist-upgrade 命令会更新所有可用的软件包。它会确保服务器上的所有软件包都被更新,即使这意味着需要安装一个之前不需要的新的依赖包。如果为了满足依赖关系需要移除某个包,它也会这样做。如果有更新的内核可用,它也会被安装。如果你使用这个命令,执行前请先花点时间查看提议的变更,这样在执行过程中你可以确认这些变更。
一般来说,dist-upgrade变体应该代表你的最终目标,但这不一定是你应该开始的地方。更新内核很重要,因为你发行版的内核会像其他包一样接收安全更新。所有的包最终都应该更新,即使这意味着某些包会被移除,因为它们不再需要,或者会安装一些新的包。
当你开始更新过程时,它将看起来像下面这样:

图 21.2:在 Ubuntu 服务器上更新包
在更新过程正式开始之前,你将获得一个它想要执行操作的概述。在我的情况下,它想要升级11个包。如果你输入Y然后按Enter,更新过程将开始。此时,我需要保持终端窗口打开;在更新过程中关闭它是危险的。在包管理任务中途关闭终端窗口可能会导致包损坏或部分安装。
假设这个过程成功完成,我们可以运行apt dist-upgrade命令来更新剩余部分——特别是那些由于安装新包或移除现有包而被暂时保留的包。在我的情况下没有这类包,但在这种情况下,你可能会看到一些文本,表示某些升级被保留,这在apt upgrade中是正常的。此时,你将运行sudo apt dist-upgrade来安装第一个命令未能安装的剩余更新。
关于更新内核,这个过程值得进一步讨论。有些发行版在更新内核时非常冒险。例如,Arch Linux 就是这样,任何时候只有一个内核被安装。因此,当这个内核更新时,你真的需要重启机器,这样它才能正确使用这个内核(有时,当你安装新内核后有待重启时,系统的某些组件可能会遇到问题)。
另一方面,Ubuntu 在处理内核升级方面非常高效。当你在 Ubuntu 中更新内核时,它不会替换你当前正在运行的内核。相反,它会将更新后的内核与现有的内核一起安装。
实际上,这些内核会继续堆叠,新的内核安装时,旧的内核不会被移除。当新的 Ubuntu 内核版本被安装时,GNU GRUB 引导加载程序会自动更新,以便在你下次重启时启动新的内核。
在你重启之前,你可以继续使用当前的内核,直到你需要为止,而且应该不会察觉到任何区别。唯一的区别是,在你重启之前,你没有利用新内核的额外安全补丁,你可以在下次维护窗口期间进行重启。这个更新方法之所以很棒,是因为如果遇到新内核无法启动或出现某些问题的情况,你可以在启动过程中按Esc键,这时你将能够浏览你所有已安装的内核列表。通过这个列表,你可以选择使用之前的(已知且可用的)内核,继续像更新内核之前一样使用服务器。这是一个非常有价值的安全保障!
在你更新完服务器上的软件包后,你可能希望重新启动服务,以便利用新的安全更新。对于内核更新,你需要重启整个服务器才能利用新的内核更新,但其他更新则不需要重启。相反,如果你重启相关服务,通常就可以了(如果更新本身没有触发服务的重启)。例如,如果你的 DNS 服务(bind9)被更新,你只需要执行以下命令来重启服务:
sudo systemctl restart bind9
除了保持软件包更新外,了解如何在发生故障时回滚更新的软件包也同样重要。你可以通过手动重新安装旧版本的软件包来从这种情况中恢复。以前下载的软件包存储在以下目录中:
/var/cache/apt/archives
在那里,你应该能够找到作为更新过程一部分下载的实际软件包。如果你需要将更新的软件包恢复到先前安装的版本,可以使用dpkg命令手动安装该软件包。一般来说,语法类似于以下内容:
sudo dpkg -i /path/to/package.deb
更准确地说,你可以使用如下命令重新安装之前下载的软件包,以较旧的 Linux 内核为例:
sudo dpkg -i /var/cache/apt/archives/linux-image-5.15.0-30-generic_5.15.0-30.31_amd64.deb
然而,使用dpkg命令时,依赖关系不会自动处理,因此如果缺少目标软件包所需的依赖软件包,软件包仍然会被安装,但会有未解决的依赖关系需要修复。你可以尝试使用apt来解决这个问题:
sudo apt -f install
apt -f install命令将尝试修复已安装的软件包,寻找缺失的软件包(但已安装的软件包需要的),并提供安装缺失依赖的选项。如果无法找到缺失的依赖,它会提供删除依赖缺失的软件包的选项,前提是无法通过其他方式解决问题。
好了,事情就是这样。此时,你应该已经不仅仅能够安装软件包,而且能够保持它们的更新。Ubuntu 中还有一个功能,你可以利用它来享受实时补丁的概念,从而自动修补服务器的内核。接下来,我们将在下一节中讨论这个内容。
使用 Canonical Livepatch 服务自动安装补丁
在前一节中,我提到过,如果你的更新包括内核更新,你将需要重启服务器才能使新的内核生效。虽然这通常是正确的,但 Canonical 为 Ubuntu 提供了一项Livepatch服务,使得更新可以在不重启的情况下进行并应用。这是一个游戏规则的改变,因为它能够在你不做任何事情(甚至不需要重启)的情况下,保持正在运行的内核得到修补。这对安全性是一个巨大的好处,因为它让你在不需要立即安排服务器重启的情况下,享受到最新的安全补丁。
然而,这项服务并不是免费的,默认情况下也不包含在 Ubuntu 中。尽管如此,你可以在不付费的情况下,在三台服务器上安装 Livepatch 服务,所以它仍然是你可能想要考虑的东西。如果你愿意,你甚至可以在 Ubuntu 的桌面版上使用这项服务。由于你可以在三台服务器上免费使用这项服务,我认为没有理由不在你的最关键资源上也受益于此。
尽管你通常不需要重启服务器就能利用 Livepatch 服务的补丁,但根据漏洞的性质,可能会有一些例外情况。过去曾有一些漏洞需要复杂的变更,甚至订阅了这项服务的服务器也需要重启。不过,这种情况是例外,而不是常规。大多数时候,如果你在使用 Livepatch,重启根本不是你需要担心的事情。大多数情况下,你的服务器会将所有补丁应用并直接插入到正在运行的内核中,这是非常棒的。
需要注意的一点是,这并不意味着你不需要通过apt安装更新。实时补丁直接插入内核,但它们并不是永久性的。你仍然需要定期通过常规方式安装所有的软件包更新。至少,实时补丁可以让你在重启时不那么着急。如果一个漏洞在星期一被揭露,但你直到星期天才能重启服务器,那也没什么大问题。
由于 Livepatch 服务需要订阅,你需要创建一个账户才能开始使用它。你可以在auth.livepatch.canonical.com/开始这个过程。
这个过程将要求你创建一个 Ubuntu One 账户 (login.ubuntu.com/),这是 Canonical 的集中登录系统。你需要输入电子邮件地址,选择密码,然后在过程结束时,你将获得一个用于 Livepatch 服务的令牌,它将是一个随机字符的字符串。
现在你有了一个令牌,你可以选择最重要的三台服务器。在每台服务器上,你可以运行以下命令来开始使用:
sudo snap install canonical-livepatch
sudo canonical-livepatch enable <token>
相信与否,这就是全部内容。考虑到 Livepatch 服务的强大,你可能会以为它的设置过程很复杂。最耗时的部分是注册一个新账户,实际上只需要两个命令就能在服务器上设置这个服务。你可以通过以下命令检查 Livepatch 的状态:
sudo canonical-livepatch status
根据你所在组织的预算,你可能会决定支付费用来使用此服务,这样你就能在三台以上的服务器上受益。这个选择绝对值得考虑。如果你决定选择这个选项,需要联系 Canonical 了解更多的支持信息。
目前,我们应该换个思路,讨论一些可以改进 OpenSSH 安全性的方法。我在本章中提到过几次,OpenSSH 是外部威胁行为者的常见攻击目标,因此在下一节中,我们将仔细审视这一点。
安全加固 OpenSSH
OpenSSH 是一个非常有用的工具,它使我们可以像坐在控制台前一样,从远程位置配置服务器。在云资源的情况下,通常这是访问我们服务器的唯一方式。考虑到 OpenSSH 本身的性质(远程管理),它成为了那些寻求制造麻烦的不法分子非常有吸引力的目标。如果我们只是将 OpenSSH 放任不管,它这个有用的工具可能会变成我们的噩梦。
幸运的是,配置 OpenSSH 本身是非常简单的。然而,大量的配置选项可能会让没有多少经验的人感到困惑。虽然查看 OpenSSH 的文档是一个好主意,但在本节中,我们将先来看看你需要重点关注的一些常见配置选项。
OpenSSH 的配置文件位于 /etc/ssh/sshd_config,我们在 第十章,连接到网络 中提到过。这个文件就是我们本节要重点讨论的文件,因为我要给你的一些配置选项都需要放入这个文件中。
在本节中的每一个调整步骤,确保你首先浏览该文件,查看设置是否已经存在并做出相应的更改。如果文件中没有该设置,请添加它。修改完成后,重启 OpenSSH 守护进程非常重要:
sudo systemctl restart ssh
继续并在编辑器中打开此文件,我们将进行一些调整。
一个非常简单的调整是改变 OpenSSH 监听的端口号,默认是端口22。由于这是黑客首先尝试的端口,改变它是有道理的,而且这是一个非常容易的修改。不过,我不希望你认为仅仅因为你更改了 OpenSSH 的端口,它就神奇地被隐藏了并且无法被检测到。一个持续的威胁者仍然能够通过对你的服务器进行端口扫描来找到这个端口。然而,既然这个修改如此简单,为什么不做呢?要更改它,只需在/etc/ssh/sshd_config文件中查找端口号,并将其从默认的22更改:
Port 65332
我能想到的唯一缺点是更改 SSH 端口后,你需要记得在使用 SSH 时指定端口号,并且必须将更改通知任何使用该服务器的人。要指定端口,我们使用ssh命令中的-p选项:
ssh -p 65332 myhost
如果你使用的是scp,你需要改用大写的P:
scp -P 65332 myfile myserver:/path/to/dir
尽管更改端口号不会让你的服务器成为万无一失的堡垒,但我们不应低估这么做的价值。在一个假设的例子中,假如攻击者正在扫描互联网上的服务器寻找开放的22端口,他们可能会跳过你的服务器,继续扫描下一个。只有那些有决心的攻击者,专门想要入侵你的服务器,才会扫描其他端口来寻找它。这也能保持你的日志文件干净;你只会看到那些进行积极端口扫描的恶意行为,而不是那些随机的机器人寻找开放端口。
如果你的服务器面向互联网,这将导致日志中出现的条目大大减少!OpenSSH 会在授权日志中记录连接尝试,日志位置是/var/log/auth.log。你可以随时查看该日志文件,了解典型的日志记录内容。
另一个值得提到的更改是 OpenSSH 监听的协议。大多数今天在仓库中提供的 OpenSSH 版本默认使用协议 2。这就是你想要的。协议 2 比协议 1 安全得多。你绝不应该在生产环境中允许使用协议 1。除非你出于某种原因更改了它,否则你很可能已经在服务器上使用了协议 2 的默认设置。我在这里提到它是以防你仍在使用较老的服务器,它们默认使用旧协议。如今,任何现代版本的 Linux 发行版中的 OpenSSH 都始终使用协议 2。如果你确实有使用协议 1 的旧服务器,你可以通过在/etc/ssh/sshd_config文件中找到以下行进行调整:
Protocol 1
将 OpenSSH 切换到使用协议 2 只需将该行中的1改为2,然后重启 OpenSSH 服务器:
sudo systemctl restart ssh
接下来,我将为您提供两个调整选项,价格只需一个。有两个设置涉及允许哪些用户和组通过 SSH 登录:AllowUsers和AllowGroups,分别是。默认情况下,您创建的每个用户都可以通过 SSH 登录到您的服务器。关于root,默认情况下实际上是不允许的(稍后详细介绍)。但是您创建的每个用户都被允许进入。但是,只有必须访问的用户才能进入。有两种方法可以实现这一点。
一种选项是使用AllowUsers。使用AllowUsers选项,您可以明确设置哪些用户可以登录到您的服务器上。有了AllowUsers存在(默认情况下在config文件中找不到),您的服务器将不允许任何未经特别指定的用户使用 SSH。您可以用空格分隔每个用户:
AllowUsers larry moe curly
就个人而言,我发现AllowGroups更易于管理。它与AllowUsers基本相同,但与组一起使用。如果存在,它将限制 OpenSSH 连接到属于此组的用户。要使用它,您首先需要创建相关的组(您可以按照自己的逻辑命名):
sudo groupadd sshusers
然后,您将使一个或多个用户成为该组的成员:
sudo usermod -aG sshusers myuser
一旦您添加了该组并使一个或两个用户成为该组的成员,请添加以下内容到您的/etc/ssh/sshd_config文件,用您的示例组替换:
AllowGroups admins sshusers gremlins
只使用一个组也是可以的。只需确保在注销前将自己添加到组中;否则,您将被锁定。我建议您只使用AllowUsers或AllowGroups中的一个。我认为使用AllowGroups更容易,因为您将永远不需要再次触及sshd_config文件;您只需将用户帐户添加或删除到组中以控制访问。只要您知道,AllowUsers将覆盖AllowGroups。
另一个重要选项是PermitRootLogin,它控制root用户帐户是否能够建立 SSH 连接。这应该始终设置为no。通常情况下,默认设置为prohibit-password,这意味着root可以使用密钥认证,但不接受密码。我也不认为有任何理由这样做。在我看来,您应该关闭此功能。使root能够通过网络连接登录到您的服务器绝不是一个好主意。这始终是攻击者尝试使用的第一个用户帐户:
PermitRootLogin no
在 SSH 的no-root规则中有一个例外情况。一些云服务器提供商,例如 Linode,默认情况下可能要求您以root身份登录。这并不是很典型,但有些提供商是这样设置的。在这种情况下,我建议创建一个具有sudo访问权限的常规用户,然后禁止root登录。
我的下一个建议虽然设置起来不容易,但绝对值得。默认情况下,OpenSSH 允许用户通过密码进行身份验证。这是我在所有服务器上第一个禁用的功能。允许用户通过密码建立连接意味着攻击者也能暴力破解你的服务器。如果不允许密码认证,他们就无法这么做。问题是,在禁用 SSH 密码认证之前,你首先需要配置并测试一种备用的身份验证方式,通常是公钥认证。这是我们在第十章《连接到网络》中讲解过的内容。基本上,你可以在本地工作站生成一对 SSH 密钥,并将该密钥添加到服务器上的 authorized_keys 文件中,这样就可以无需密码登录。再说一遍,如果你还没做过,参考第十章《连接到网络》。
如果你禁用 OpenSSH 的密码认证,那么公钥认证将是唯一的登录方式。如果有人尝试连接到你的服务器而没有合适的密钥,服务器将立即拒绝其访问。如果启用了密码认证并且你有密钥关系,那么如果用户的密钥没有安装,服务器将会要求他们输入密码。在我看来,在你设置好公钥加密方式的访问后,应当禁用密码认证(但一定要先进行测试):
PasswordAuthentication no
就这些——这些是我推荐的最有效的 OpenSSH 安全性调整。虽然还有更多的措施可以采取,但这些设置是你最能从中受益的。在接下来的部分,我们将增加一个额外的安全层,那就是 Fail2ban。通过 Fail2ban 保护 OpenSSH,加上我在本节中提到的这些调整,攻击者将很难攻破你的服务器。为了方便你,这里列出了我在本节中提到的所有 OpenSSH 配置选项:
Port 65332
Protocol 2
AllowUsers larry moe curly
AllowGroups admins sshusers gremlins
PermitRootLogin no
PasswordAuthentication no
随着 OpenSSH 安全性得到了进一步增强,我们现在在服务器安全性方面应该更加有信心了。然而,我们每进行一次安全改进或调整,所带来的保护也只是有限的。保护措施越多,安全性就越强。在接下来的部分,我们将介绍 Fail2ban,它能够大大增强服务器的安全性。
安装和配置 Fail2ban
Fail2ban,我爱你!Fail2ban 是那种一旦我了解到它有多么有价值,就会觉得怎么之前没有使用它的工具。Fail2ban 能够监视你的日志文件,查找认证失败的记录。你可以设置允许来自任何特定 IP 地址的失败次数,如果失败次数超过了允许的数量,Fail2ban 会屏蔽该 IP 地址。它具有高度的可配置性,能够增强服务器的安全性。
安装和配置 Fail2ban 相对来说是比较简单的。首先,安装其软件包:
sudo apt install fail2ban
安装后,fail2ban守护进程将启动,并配置为在启动时自动启动。配置fail2ban只是创建一个配置文件的过程。但这是 Fail2ban 的一个有趣之处:你不应使用其默认的config文件。默认文件是/etc/fail2ban/jail.conf。这个文件的问题在于,当你安装安全更新时,如果这些安全更新包括 Fail2ban 本身,它可能会被覆盖。为了解决这个问题,Fail2ban 还会读取/etc/fail2ban/jail.local文件(如果存在)。它永远不会替换这个文件,并且jail.local文件的存在将优先于jail.conf文件。最简单的方式是复制jail.conf文件并将其保存为jail.local:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
接下来,我将讲解一些你应该配置的非常重要的设置,所以请在文本编辑器中打开你刚才复制的/etc/fail2ban/jail.local文件。第一个需要更改的配置项位于第 92 行左右,并且被注释掉了:
#ignoreip = 127.0.0.1/8 ::1
首先,取消注释。然后,你应该添加你不希望被 Fail2ban 封锁的其他网络。基本上,这可以帮助你避免在不小心触发 Fail2ban 时被锁定。Fail2ban 是无情的;它会封锁符合其封锁标准的任何服务,而且它不会犹豫。这包括封锁你自己。为了解决这个问题,将公司网络以及任何你不希望被封锁的 IP 地址添加到此处。确保保留localhost IP 不变:
Ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 192.168.1.245/24
在这个示例中,我添加了192.168.1.0/24网络,以及单个 IP 地址192.168.1.245/24。将你的网络添加到这一行中,确保你不会把自己锁定在外面。
接下来,第 101 行包含bantime选项。这个选项决定了当 Fail2ban 封锁主机时,主机会被封锁多少秒。此选项默认值为10m,即 10 分钟:
bantime = 10m
将这个数字更改为你认为合理的任何值,或者保持其默认值,这也是可以的。如果主机被封锁,它将被封锁这个特定的分钟数,然后最终将被允许再次访问。
接下来,我们来看maxretry设置:
maxretry = 5
这是指定 Fail2ban 采取行动之前需要发生的失败次数。如果它监视的服务达到这里设置的次数,就结束了!该 IP 将被封锁,持续时间为bantime选项中设置的分钟数。
如果你认为5次失败不合理,你可以更改这个设置。我建议将其设置为7,对于那些在你的网络上坚持认为自己输入了正确密码,但一直输入相同(错误)密码的用户。希望他们能在第七次尝试之前意识到自己的错误,避免需要打电话给技术支持。
跳到大约第 272 行左右,我们看到了Jails部分。从这里开始,config文件将列出几个可以配置的监狱,这基本上是 Fail2ban 需要关注的对象。第一个是[sshd],它配置了 OpenSSH 守护进程的保护。在[sshd]下,查找以下选项:
port = ssh
port等于ssh基本上意味着它默认使用端口22。如果你更改了 SSH 端口,请修改此设置以反映新的端口号。这里有两个类似的地方,一个在[sshd]下,另一个在[sshd-ddos]下:
port = 65332
在我们继续之前,我要强调的是,我们每次修改配置后,都应该测试 Fail2ban 是否正常工作。为此,重启 Fail2ban,然后检查它的状态:
sudo systemctl restart fail2ban
sudo systemctl status -l fail2ban
状态应始终为active (running)。如果是其他任何状态(比如failed),则表示 Fail2ban 不喜欢你的配置中的某些内容。通常,这意味着 Fail2ban 的状态将显示它已经退出。因此,在整个过程中,确保在每次修改后重启 Fail2ban,并确保它没有报错。status命令将显示 Fail2ban 日志文件中的内容,方便你查看。
重启 Fail2ban 后,另一个有用的命令是:
sudo fail2ban-client status
该命令的输出将显示你已启用的所有监狱。如果你在config文件中启用了一个新的监狱,你应该能在该命令的输出中看到它。
那么,如何启用一个监狱呢?默认情况下,所有监狱都被禁用,只有 OpenSSH 的监狱是启用的。要启用一个监狱,请在/etc/fail2ban/jail.local文件中的config块内加入以下内容:
enabled = true
如果你想启用apache-auth监狱,找到其部分,并在其段落下方添加enabled = true。例如,在你添加enabled行后,apache-auth将如下所示:
[apache-auth]
enabled = true
port = http,https
logpath = %(apache_error_log)
在这个示例中,enabled = true这一部分在默认文件中并不存在。我添加了它。现在我已经启用了一个新的监狱,我们应该重启fail2ban:
sudo systemctl restart fail2ban
接下来,检查其状态,确保它在启动时没有出错:
sudo systemctl status -l fail2ban
假设一切顺利,我们应该能在以下命令的输出中看到新监狱:
sudo fail2ban-client status
在我的测试服务器上,启用apache-auth后,输出结果如下:
Status
|- Number of jail: 2
'- Jail list: apache-auth, sshd
如果你为未安装的服务启用了一个监狱,Fail2ban 可能无法启动。在我的示例中,我在启用apache2监狱之前,确实在服务器上安装了apache2。如果没有安装,Fail2ban 可能会退出并抱怨找不到 Apache 的日志文件。这也是我建议在启用任何监狱后测试 Fail2ban 的原因。如果 Fail2ban 发现它不喜欢某个配置,或者它所需的某些文件缺失,它可能会停止工作。然后,它就无法为你提供保护了,这可不好。
Fail2ban 的基本操作顺序是浏览监狱的config文件,查看哪些监狱可能对你有帮助。如果你的服务器上运行着一个守护进程,那么有可能有一个监狱是专门为它设计的。如果有,启用它并查看 Fail2ban 是否能够正常启动。如果没有问题,那就很好。如果它无法正常重启,检查状态输出,看看它在抱怨什么。
你可能需要做的一件事是将enabled = true这一行添加到[sshd]和[sshd-ddos]中。没错,[sshd]的监狱默认已经启用,但由于它在config文件中并没有特别标明,所以我对此不太信任。因此,你最好还是添加一个enabled行来确保安全。你可能还会从其他一些监狱中受益。如果你使用SSL和Apache,启用[apache-modsecurity]。同时,考虑启用[apache-shellshock],以便在此过程中潜在地保护 Apache 免受 Shellshock 漏洞的影响。如果你正在运行自己的邮件服务器并且有Roundcube在运行,启用[roundcube-auth]和[postfix]。有很多默认的监狱可以供你使用!
像所有的安全应用程序一样,Fail2ban 并不会自动让你的服务器对所有攻击免疫,但它是你可以添加到安全策略中的一个有用的附加层。就 OpenSSH 的监狱而言,Fail2ban 的价值不言而喻,这实际上是你应该启用的最基本内容。快去在你的服务器上试试 Fail2ban 吧——只要确保你也将自己的网络添加到之前提到的Ignoreip列表中,以防你不小心输入错误的 SSH 密码太多次而可能把自己锁住。Fail2ban 不分青红皂白;它会封锁任何人。一旦你完成了全面配置,我相信你会同意 Fail2ban 是你服务器的一个值得信赖的盟友。
我之前提到过,每个在你的计算机上监听连接的服务都有可能成为攻击目标。虽然不可能逐一讲解你可能在服务器上运行的每个服务以及如何保护它们,但我们需要考虑保护我们的数据库服务器(如果有的话),因为组织通常会在这里存储重要数据。接下来我们将学习一些方法,可以帮助我们更好地保护 MariaDB。
MariaDB 最佳实践:安全的数据库服务器
MariaDB 和 MySQL 是非常有用的资源,值得你使用。然而,如果配置不当,它们也可能被用来攻击你。幸运的是,它的安全性并不难把握,但在制定安全设计时,你需要考虑一些有关数据库服务器的要点。
第一点对大多数人来说可能很明显,我之前也提到过,但我还是提一下,以防万一。您的数据库服务器不应该能通过互联网访问。我理解在开发网络时有一些特殊情况,某些应用可能需要通过互联网访问 MySQL 数据库。然而,如果您的数据库服务器可以通过互联网访问,坏人将会尽全力攻击并试图入侵。如果您的 MariaDB 或 MySQL 版本存在任何漏洞,他们很可能会成功入侵。
在大多数组织中,实施数据库服务器的一个好方法是只允许内部服务器访问。这意味着虽然您的 Web 服务器显然可以通过互联网访问,但它的后台数据库应该存在于您内部网络上的另一台服务器,并且仅接受来自 Web 服务器的通信。如果您的数据库服务器是 VPS 或云实例,它应该特别配置为仅接受来自 Web 服务器的通信,因为 VPS 机器默认是可以通过互联网访问的。因此,如果您的 Web 服务器被攻破,您的数据库服务器仍然有可能受到攻击,但如果数据库服务器存在于一个独立且受限的服务器上,被入侵的可能性就会小得多。
一些 VPS 提供商,如 DigitalOcean 和 Linode,提供本地网络功能,您可以利用这一功能来让数据库服务器只能在本地网络中访问,而不是允许它通过互联网访问。如果您的 VPS 提供商提供本地网络功能,您应该毫不犹豫地使用它,并拒绝来自外部网络的流量。
关于限制哪些服务器能够访问数据库服务器,我们可以使用一些方法来实现。首先,我们可以利用/etc/hosts.allow和/etc/hosts.deny文件。通过/etc/hosts.deny文件,我们可以阻止来自某些网络或特定服务的流量。通过/etc/hosts.allow文件,我们允许特定流量。之所以这样工作,是因为在/etc/hosts.allow中列出的 IP 地址会覆盖/etc/hosts.deny中的设置。因此,基本上,如果您在/etc/hosts.deny中拒绝所有流量,并在/etc/hosts.allow中允许某些资源,实际上就是在说:拒绝所有流量,除非是我在/etc/hosts.allow中明确允许的资源。
要进行此更改,首先我们需要编辑/etc/hosts.allow文件。默认情况下,此文件除了提供一些有用的注释外,没有其他配置。在文件中,我们可以列出希望始终能够访问服务器的资源。确保在此文件中包含您的 Web 服务器,并确保立即添加您将用于 SSH 连接到机器的 IP 地址;否则,一旦我们编辑/etc/hosts.deny文件,您将无法再访问服务器。
下面是一些hosts.allow的示例条目,并附有对每个示例规则作用的描述。
第一个示例规则允许 IP 地址为192.168.1.50的机器访问服务器:
ALL: 192.168.1.50
这个规则允许任何来自192.168.1.0/24网络的机器访问服务器:
ALL: 192.168.1.0/255.255.255.0
在这个规则中,我们有一个不完整的 IP 地址。这充当了一个通配符,意味着任何以 192.168.1 开头的 IP 地址都被允许:
ALL: 192.168.1.
这个规则允许一切。你绝对不想这样做:
ALL: ALL
我们还可以允许特定的守护进程。这里,我允许来自任何以192.168.1开头的 IP 地址的 OpenSSH 流量:
ssh: 192.168.1.
在你的端,如果你希望使用这种安全方法,请添加你希望接受通信的数据库服务器上的资源。确保至少添加另一个具有 OpenSSH 访问权限的服务器的 IP 地址,这样你就可以管理该机器。你也可以像之前的示例一样,添加所有内部 IP 地址的规则。设置好之后,我们可以编辑/etc/hosts.deny文件。
/etc/hosts.deny 文件使用与 /etc/hosts.allow 相同的语法。为了完成这个小练习,我们可以通过以下规则阻止任何不在 /etc/hosts.allow 文件中的流量:
ALL: ALL
/etc/hosts.allow 和 /etc/hosts.deny 文件并不是完整的安全层,但它们是保护数据库服务器的一个很好的第一步,尤其是当服务器可能包含敏感的用户或财务信息时。它们也并不特定于 MariaDB,但我在这里提到它们是因为数据库通常包含的数据,如果泄露,可能会对你的组织造成严重影响,甚至让某些公司破产。数据库服务器应该仅仅由需要使用它的应用程序访问。
另一个需要考虑的点是用户安全。我们在第十三章《管理数据库》中讲解了如何创建数据库用户。在那一章中,我们详细讲解了如何使用 MySQL 命令创建用户并使用GRANT进行授权,将这两者结合在一个命令中。这是我使用的示例:
GRANT SELECT ON mysampledb.* TO 'appuser'@'localhost' IDENTIFIED BY 'password';
这里需要注意的是,我们允许名为appuser的用户访问mysampledb数据库。如果你仔细看这个命令,我们还指定了只有当连接来自localhost时才允许访问。如果我们尝试远程访问这个数据库,是不允许的。
这是一个很好的默认设置。但在某些时候,你可能需要从不同的服务器访问数据库。也许你的 Web 服务器和数据库服务器是分开的机器,这是企业中常见的做法。你可以这样做:
GRANT SELECT ON mysampledb.* TO 'appuser'@'%' IDENTIFIED BY 'password';
然而,在我看来,这是一种非常糟糕的做法。MySQL GRANT 命令中的 % 字符是一个通配符,类似于其他命令中的 *。在这里,我们基本上是告诉 MariaDB 或 MySQL 实例接受来自任何网络的这个用户的连接。几乎没有正当理由这样做。我听说一些管理员会用这样的论点来辩解:他们的公司防火墙不允许外部流量,所以允许来自任何机器的 MySQL 流量应该没问题。然而,这个逻辑在你考虑到如果攻击者确实获得了你网络中任何机器的访问权限时,它们就可以立刻攻击你的数据库服务器时就会崩溃。如果某个内部员工对管理层感到愤怒,想要摧毁数据库,他们就能从自己的工作站访问它。如果某个员工的工作站被恶意软件感染,而这些恶意软件专门针对数据库服务器,那么它可能会找到你的数据库服务器,并尝试进行暴力破解。我可以举出很多例子,说明为什么允许任何机器访问你的数据库服务器是一个糟糕的主意。总之,不要这样做!
如果我们想允许某个特定的 IP 地址访问,可以使用以下方法:
GRANT SELECT ON mysampledb.* TO 'appuser'@'192.168.1.50' IDENTIFIED BY 'password';
在之前的示例中,只有 IP 地址为 192.168.1.50 的服务器或工作站可以使用 appuser 账户访问数据库。这样好多了。当然,你也可以允许整个子网:
GRANT SELECT ON mysampledb.* TO 'appuser'@'192.168.1.% IDENTIFIED BY 'password';
在这里,任何以 192.168.1 开头的 IP 地址都被允许。老实说,我并不喜欢允许整个子网访问。但根据你的网络设计,可能有十几个机器需要访问。希望你允许的子网不是用户工作站所在的子网!
最后,另一个需要考虑的因素是数据库服务器软件的安全补丁。我知道我经常提到更新,但正如我之前提到的,这些更新是有原因的。开发者不会仅仅因为无聊而发布企业软件的补丁;这些更新通常修复了真实的问题,而这些问题正被现实中的攻击者利用,就在你阅读本文时。请定期安装更新。我理解,服务器应用程序的更新可能会让一些人感到害怕,因为每次更新都伴随一定风险,可能会影响到业务。但作为管理员,你有责任制定一个安全补丁的推广计划,并确保及时安装这些补丁。当然,这很艰难,通常需要在非工作时间进行。但我最不愿意看到的,就是又看到一家公司的数据库服务器内容被泄露,并被随意发布到网上。一个好的安全设计包括定期的补丁更新。
现在我们的数据库服务器更加安全了,还有一个值得深入探讨的主题,那就是实施防火墙。市面上有几种不同的防火墙解决方案,但 UFW 是一个非常好的选择。它易于设置,且非常有效。在接下来的章节中,我会介绍如何实施它。
设置防火墙
防火墙在网络和安全设计中是一个非常重要的组成部分。防火墙非常容易实现,但有时很难做到完美实施。防火墙的问题在于,它们有时会给那些不熟悉最佳管理方式的人带来虚假的安全感。没错,拥有防火墙是好的,但单纯拥有防火墙本身并不足够。
虚假的安全感出现在某些人认为仅仅因为安装并启用了防火墙就能得到保护,但他们往往也会允许从任何网络访问内部端口。考虑一下 Windows XP 引入的防火墙,它在 Windows XP Service Pack 2 中默认启用。是的,这是一个好的开始,但用户每次看到某些东西请求访问时只是点击了允许按钮,这实际上违背了拥有防火墙的整个目的。如今,Windows 的实现方式更好,但它曾经带来的虚假安全感依然存在。防火墙不是一个“设置完就忘记”的解决方案!
防火墙通过允许或拒绝来自其他网络的端口访问来工作。大多数优秀的防火墙默认会阻止外部流量。当用户或管理员启用某个服务时,他们会为该服务打开一个端口。然后,该服务被允许访问。这在理论上很好,但问题在于,管理员在打开端口时经常会允许来自任何地方的访问。如果管理员这样做,他们与没有防火墙几乎没有区别。如果您需要通过 OpenSSH 访问服务器,您可能会打开端口22(或者任何 OpenSSH 监听的端口)来允许其通过防火墙。但如果您仅仅允许该端口,它也会对其他所有人开放。
当配置正确时,防火墙只允许来自特定地方的端口访问。例如,与其允许整个网络访问端口22以使用 OpenSSH,不如只允许来自特定 IP 地址或子网的流量访问端口22?现在我们有了思路!在我看来,通常允许所有流量通过某个端口是一个糟糕的主意,尽管某些服务确实需要这样做(例如,向您的 Web 服务器发送的 Web 流量)。如果可以的话,在打开端口时只允许来自特定网络的流量。这就是防火墙使用案例的真正亮点所在。
在 Ubuntu Server 中,简单防火墙(UFW)是一个非常有用的防火墙配置工具。顾名思义,它使得防火墙管理变得轻松无比。首先,安装ufw包:
sudo apt install ufw
默认情况下,UFW 是非活动的。这是件好事,因为在我们配置好防火墙之前,我们不希望它被启用。ufw包有自己的命令来检查其状态:
sudo ufw status
除非您已经配置了防火墙,否则状态会显示为非活动。
安装完ufw包后,首先我们要做的是启用 SSH 流量,这样我们在启用防火墙时就不会被锁定:
sudo ufw allow from 192.168.1.156 to any port 22
你可能已经从这个示例中看出 UFW 的语法是多么简单。通过这个示例,我们允许192.168.1.156的 IP 地址通过 TCP 和 UDP 访问端口22。在你的情况下,你需要相应地更改 IP 地址,以及如果你不使用 OpenSSH 默认端口,修改端口号。any选项指的是任何协议(TCP 或 UDP)。
你还可以按子网允许流量:
sudo ufw allow from 192.168.1.0/24 to any port 22
虽然我不推荐这样做,但你可以允许特定 IP 的所有流量访问你服务器上的任何内容。如果必须使用,请小心:
sudo ufw allow from 192.168.1.50
既然我们已经配置了防火墙以允许通过 OpenSSH 访问,你还应该允许任何其他端口或 IP 地址,以便服务器能够高效运行。例如,如果你的服务器是 Web 服务器,你将希望允许80和443端口的流量。这是少数几个例外之一,你可能希望允许任何网络的流量,前提是你的 Web 服务器在互联网上提供外部页面:
sudo ufw allow 80
sudo ufw allow 443
ufw命令还有其他多种使用模式;有关更多信息,请参考主页面(manpages.ubuntu.com/manpages/focal/man8/ufw.8.xhtml)。简而言之,这些示例应该能让你允许特定端口的流量通过,以及通过特定的网络和 IP 地址。一旦完成防火墙配置,我们可以启用它:
sudo ufw enable
Firewall is active and enabled on system startup
正如输出所示,我们的防火墙已激活,并将在每次重启服务器时自动启动。
UFW 软件包基本上是iptables防火墙的一个易于使用的前端,并且它作为 Ubuntu 的默认防火墙。到目前为止,我们在本节中执行的命令触发了iptables命令,管理员可以使用该命令手动设置防火墙。iptables的详细操作不在本章范围内,且实际上没有必要,因为 Ubuntu 将 UFW 作为其首选的防火墙管理工具,而这正是你在管理 Ubuntu 服务器上的防火墙时应该使用的工具。
通过精心规划的防火墙实施,你可以更好地保护你的 Ubuntu 服务器免受外部威胁。最好是,你打开的每个端口都应该只能从特定的机器访问,唯一的例外是那些用于为外部网络提供数据或资源的服务器。像所有安全解决方案一样,防火墙不能让你的服务器变得刀枪不入,但它确实增加了一个额外的层次,攻击者必须绕过这个层次才能造成伤害。
如果你的公司存储敏感信息,确保存储在这些数据下方的部分是加密的非常重要。接下来,我们将介绍Linux 统一密钥设置(LUKS),它将帮助我们加密和解密磁盘。
使用 LUKS 加密和解密磁盘
安全的一个重要方面是加密,很多人甚至没有考虑过。如你所知,备份对于业务的连续性至关重要。如果服务器故障或资源停止运行,备份将是你的救命稻草。但如果你的备份介质被盗或者不慎落入他人之手,会发生什么呢?如果你的备份没有加密,任何人都可以查看其内容。有些数据并不敏感,因此并不总是需要加密。但包含个人身份信息、公司机密或者其他一旦泄露会带来任何不良后果的内容都应该加密。在本节中,我将指导你在外部备份驱动器上设置LUKS加密。
在继续之前,我想简要提到对于您的发行版来说完整磁盘加密的重要性。尽管本节将介绍如何加密外部磁盘,但也可以对整个 Linux 安装的卷进行加密。对于 Ubuntu 来说,在安装过程中,无论是服务器版还是工作站版,都可以选择全磁盘加密选项。这在涉及到移动设备(如经常被盗的笔记本电脑)时尤为重要。如果计划在笔记本电脑上存储不能泄露的机密数据,应在安装期间选择加密整个 Ubuntu 安装选项。否则,任何知道如何引导 Live OS 光盘并挂载硬盘的人都可以查看您的数据。我曾见过未加密的公司笔记本电脑被盗的情况,这并不是一种美好的体验。
无论如何,我们重新回到加密外部卷的话题。为了加密磁盘,我们需要安装cryptsetup软件包:
sudo apt install cryptsetup
cryptsetup实用程序允许我们加密和解密磁盘。为了继续,您需要一个可以安全格式化的外部磁盘,因为加密磁盘将删除存储在其中的任何数据。这可以是外部硬盘或闪存驱动器。两者可以完全相同地处理。此外,您还可以使用相同的过程来加密连接到虚拟机或服务器的次要内部硬盘。我假设您不关心驱动器上保存的内容,因为设置加密的过程将擦除它。
如果你在使用外部磁盘,请在将其插入计算机或服务器之前,以root用户身份使用fdisk -l命令或使用lsblk命令查看连接到计算机或服务器的硬盘列表。插入外部磁盘或闪存驱动器后,请再次运行命令以确定可移动介质的设备标识。
在我的示例中,我使用了/dev/sdb,但您应该使用您设备分配的任何标识。这很重要,因为您不希望擦除您的root分区或现有的数据分区!
首先,我们需要使用cryptsetup来格式化我们的磁盘:
sudo cryptsetup luksFormat /dev/sdb
你将收到以下警告:
WARNING!
========
This will overwrite data on /dev/sdb irrevocably.
Are you sure? (Type uppercase yes):
输入YES并按Enter键继续。接下来,你将被要求输入密码短语。此密码短语将用于解锁驱动器。确保使用一个良好的、随机生成的密码,并将其保存在安全的地方。如果你丢失了它,将无法解锁驱动器。你将被要求确认密码短语。
一旦命令完成,我们就可以格式化我们的加密磁盘。此时,它没有文件系统,因此我们需要创建一个。首先,使用以下命令打开磁盘:
sudo cryptsetup luksOpen /dev/sdb backup_drive
backup_drive的名称可以是你想要的任何名字;它只是你用来引用磁盘的一个任意名称。此时,磁盘将附加到/dev/mapper/disk_name,其中disk_name是你在上一个命令中为磁盘指定的名称(在我的例子中是backup_drive)。接下来,我们可以对磁盘进行格式化。以下命令将在加密磁盘上创建一个 ext4 文件系统:
sudo mkfs.ext4 -L "backup_drive" /dev/mapper/backup_drive
-L选项允许我们为驱动器添加标签,因此你可以随意将该标签更改为你喜欢的驱动器名称。
在格式化完成后,我们现在可以挂载磁盘:
sudo mount /dev/mapper/backup_drive /media/backup_drive
mount命令将挂载位于/dev/mapper/backup_drive的加密磁盘,并将其附加到一个挂载点,例如在我的示例中是/media/backup_drive。目标挂载目录必须已经存在。磁盘挂载后,你可以像操作任何其他卷一样将数据保存到设备上。完成后,你可以使用以下命令卸载设备:
sudo umount /media/backup_drive
sudo cryptsetup luksClose /dev/mapper/backup_drive
首先,我们像平常一样卸载卷。然后,我们告诉cryptsetup关闭该卷。要重新挂载它,我们可以执行以下命令:
sudo cryptsetup luksOpen /dev/sdb backup_drive
sudo mount /dev/mapper/backup_drive /media/backup_drive
这些命令中的第一个应该会提示你输入密码短语。如果成功,你可以使用第二个命令来挂载该卷。
如果我们希望更改密码短语,可以使用以下命令。为了使此操作生效,磁盘必须未挂载或未打开:
sudo cryptsetup luksChangeKey /dev/sdb -S 0
该命令会要求你输入当前的密码短语,然后要求你输入两次新的密码短语。
请记住,在输入新密码短语时,你必须非常小心,以免将自己锁定在驱动器之外。
这基本上就是全部。使用cryptsetup工具,你可以为存储最敏感信息设置自己的 LUKS 加密卷。如果磁盘落入不法之手,情况将不会像磁盘未加密时那么糟糕。破解一个 LUKS 加密的卷需要相当大的努力,而这通常是不可行的。
在下一部分中,我们将探讨如何锁定sudo。由于sudo是一个基本命令,使我们能够以其他用户身份执行任务,我们将确保将其锁定。
锁定sudo
我们在本书中一直在使用sudo命令。实际上,我们在第二章,用户与权限管理中深入探讨了它。因此,我在这里不会详细讨论sudo,但是有些内容值得重复,因为sudo对安全性有直接影响。
首先,应该尽可能限制对sudo的访问权限。拥有完全sudo访问权限的用户是一个威胁,明摆着。只要某个拥有完全sudo访问权限的人在执行rm命令时犯了一个小错误,就可能导致数据丢失或使整个服务器无法使用。毕竟,拥有完全sudo访问权限的用户可以做任何root能够做的事(也就是所有事情)。
默认情况下,在安装过程中创建的用户将成为sudo组的成员。该组成员对sudo命令有完全访问权限。因此,除非绝对必要,否则不应将任何用户加入该组。在第二章,用户与权限管理中,我谈到了如何使用visudo命令控制sudo的访问权限;如果需要复习,可以参考该章。简而言之,你可以限制sudo的访问权限,只允许特定命令,而不是让用户做任何事情。例如,如果用户需要关闭或重启服务器,你可以通过以下设置只授予他们执行这些任务(而且仅限这些任务)的权限:
charlie ALL=(ALL:ALL) /usr/sbin/reboot,/usr/sbin/shutdown
大多数情况下,如果用户需要访问sudo,只需授予他们完成工作所需的特定命令的权限。如果用户需要访问可移动媒体,授予他们对mount和umount命令的sudo访问权限。如果他们需要安装新软件,授予他们对apt命令集的访问权限,等等。你给用户的权限越少越好。这一切都回到了我们在本章开头提到的最小权限原则。
尽管本节中的大部分信息对于已经阅读过第二章,用户与权限管理的人来说并不新鲜,但sudo访问权限是很多人在考虑安全时往往忽略的一个方面。拥有完全访问权限的sudo命令相当于给某人提供了对整个服务器的完全访问权限。因此,在加强网络安全时,保持这一点非常重要。
总结
在本章中,我们探讨了可以增强服务器安全性的方法。单一的章节或书籍永远无法给出所有可能配置的安全设置的完整清单,但我们在本章中的示例是一个很好的起点。过程中,我们讨论了降低攻击面和最小权限原则的概念。我们还研究了 OpenSSH 的安全性,这是许多攻击者试图利用的常见服务。
我们还研究了 Fail2ban,它是一个有用的守护进程,当发生一定次数的身份验证失败时,可以阻止其他节点。我们还讨论了如何使用 UFW 工具配置防火墙。由于数据盗窃也不幸很常见,我们介绍了如何加密备份磁盘。
在下一章,我们将探讨在服务器出现故障时的故障排除方法。
进一步阅读
-
Landscape 文档:
learnlinux.link/ls-docs -
Fail2ban 手册:
learnlinux.link/f2b-man -
sshd_config文件指南:learnlinux.link/sshd-config-doc -
Ubuntu CVE 跟踪器:
learnlinux.link/u-cve -
密码干草堆(了解你的密码有多安全):
learnlinux.link/haystack -
SECURITY NOW(一个非常有价值的安全播客):
learnlinux.link/sn-podcast -
ShieldsUP!(一个查看路由器哪些端口开放的有用工具):
learnlinux.link/grc-su
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

第二十二章:故障排查 Ubuntu 服务器
到目前为止,我们已经涵盖了很多关于 Ubuntu 服务器的话题,并且做了一些非常有趣的项目。我们已经搭建了 Web 服务器,构建了自动化流程,甚至在云端创建了基础设施。随着你所实现的应用程序和服务逐渐老化,你的组织可能会越来越依赖它们。但是,如果你所在的组织依赖的某个服务突然不可用时会发生什么呢?当事情没有按计划进行时,你该怎么办?
尽管我们无法考虑到每一个可能出现的问题,但在遇到问题时,有一些常见的地方可以寻找线索。在本章中,我们将探讨一些常见的起始点和技术,帮助你在排查服务器问题时提供帮助。建立扎实的故障排除技能是一个重要的目标,通过本章探讨的概念,你将能够顺利掌握这一技能。
在本章中,我们将涵盖:
-
评估范围
-
进行根本原因分析
-
查看系统日志
-
跟踪网络问题
-
排查资源问题
-
诊断故障的 RAM
故障排查的第一步是分析问题,并确定问题的严重性。在接下来的部分,我们将探讨如何进行这一步。
评估范围
当服务器或网络出现问题时,你的系统会表现出一个或多个症状。也许某个应用程序比正常情况下慢得多,也许用户无法访问网络,或者某台服务器完全故障。任何时候都可能出现各种问题,跟上排查进度可能是一个挑战。
一旦你识别出问题的症状,接下来的目标是确定问题的整体范围。本质上,这意味着尽可能确定问题最可能出在哪个地方,以及有多少个系统和服务受到了影响。有时候,根本原因很明显。例如,如果你的计算机没有从 DHCP 服务器获取 IP 地址,你会立刻知道要开始检查该服务器的日志,查看它是否有能力(或无法)执行指定的任务。在其他情况下,原因可能并不那么明显。也许你有一个偶尔出现问题的应用程序,但它并不是你能够可靠复现的。在这种情况下,可能需要进行一些深入挖掘,才能了解问题的范围有多大。有时,罪魁祸首可能是你最不期待的那个。
网络上的每个组件都会与其他组件协同工作,或者至少应该是这样。一个 Linux 服务器网络,就像任何其他网络一样,是一个由服务(守护进程)组成的集合,这些服务互相补充,并且通常相互依赖。例如,DHCP 为所有主机分配 IP 地址,但它也会分配默认的 DNS 服务器。如果你的 DNS 服务器出现问题,那么 DHCP 服务器本质上会为客户端分配一个无法工作的 DNS 服务器。识别问题空间意味着,在你识别出症状后,还需要努力理解网络中每个组件如何对问题产生影响或如何受问题影响。
关于范围,我们需要确定问题的影响范围,以及有多少用户或系统受到了影响。可能只是一个用户受到影响,或者是整个子网。这将帮助你确定问题的优先级,并决定是否需要立即解决,还是可以等到稍后再处理。通常,优先级排序是解决问题的一半;有时候,用户甚至会认为他们的问题比其他人的更重要。请凭借你的最佳判断来处理。
在识别问题范围时,你需要尽可能回答以下问题:
-
这个问题的症状是什么?
-
这个问题是什么时候首次发生的?
-
网络中是否在那个时候做过任何更改?
-
这个问题之前发生过吗?如果有,上次是如何解决的?
-
哪些服务器或节点受到了这个问题的影响?
-
有多少用户受到影响?
如果问题仅限于一台机器,那么一些很好的排查起点是检查谁登录了服务器,以及最近输入了哪些命令。我经常通过检查登录用户(或最近登录的用户)的 Bash 历史记录来找到罪魁祸首。每个用户帐户的主目录中应该都有一个.bash_history 文件。这个文件包含了最近输入的命令。检查这个文件,看看是否有人最近修改了什么。我数不清有多少次仅凭这一点就直接找到了答案。更妙的是,有时候 Bash 历史记录直接指向了解决方案。如果之前发生过类似的问题并且有人已经修复过,往往他们的努力已经记录在 Bash 历史记录中,你可以通过查看它,知道之前的人是如何解决问题的。
要查看 Bash 历史记录,你可以查看用户主目录中的.bash_history 文件内容,或者直接以该用户身份执行 history 命令。
此外,如果你检查当前登录到服务器的用户,可能能够确定是否有人已经在处理某个问题,或者也许他们所做的事情导致了问题的发生。如果你输入w命令,你可以看到谁目前登录到服务器。此外,当你运行此命令时,还会看到登录用户的 IP 地址。因此,如果你不清楚w命令列出的用户账户对应谁,你可以在你的 DHCP 服务器中检查 IP 地址,找出这个 IP 地址属于谁,然后直接询问那个人。在理想的世界中,其他管理员在处理某些问题时会发一封部门邮件,确保大家都知情。不幸的是,很多人并没有这样做。通过检查已登录的用户以及他们的 Bash 历史记录,你已经走在了确定问题源头的道路上。
在识别问题范围和范围后,你可以开始缩小问题范围,帮助找到原因。有时,罪魁祸首会显而易见。如果一个网站停止工作,而你注意到最近你的 Web 服务器上的 Apache 配置发生了变化,那么你可以通过调查更改和谁做了这些更改来攻克这个问题。
如果问题是网络问题,例如用户无法访问网站,潜在的问题范围要大得多。你的互联网网关可能出现故障,DNS 或 DHCP 服务器可能宕机,你的互联网服务提供商可能出现问题,或者也许你的财务部门只是忘了支付互联网账单。只要你能够确定一个潜在的目标列表来集中进行故障排除,你就离找到问题不远了。在本章中,我会谈到一些常见的问题以及如何处理这些问题。
了解问题的范围帮助我们理解问题的严重程度以及受影响的系统和用户数量,有时,调查范围本身可能会引导你找到问题的根本原因。如果你还不知道潜在的原因,可以进行根本原因分析,尝试找出问题的源头。这就是我们接下来要探讨的内容。
进行根本原因分析
一旦你解决了服务器或网络上的问题,你会立即为自己解决问题的能力感到骄傲。解决了一个问题后,成为技术部门的英雄是一种非常棒的感觉。但你还没有完成。下一步是考虑如何防止这个问题再次发生。查看问题是如何开始的,并采取一些步骤以帮助防止问题再次发生,这一点非常重要。这就是所谓的根本原因分析。根本原因分析可能是你向经理提交的报告,或是你在知识库系统中记录的内容,也可能只是你自己记录的备忘录。无论哪种方式,它都是一次重要的学习机会。
一个好的根本原因分析有多个方面。首先,它会展示导致问题发生的事件。然后,它会列出你为解决问题所采取的步骤。如果问题可能会再次发生,你应该包括如何防止未来再次发生类似问题的信息。
根本原因分析的问题在于,很少能做到百分之百准确。有时候,根本原因可能是显而易见的。例如,假设有个叫做Bob的用户删除了一个包含公司重要文件的整个目录。如果你登录到服务器并查看日志,你会看到Bob不仅在事件发生时登录了服务器,而且他的 Bash 历史记录显示他运行了rm -rf /work/important-files命令。此时,案件就解决了。你已经弄清楚了问题发生的原因和责任人,并且可以从最近的备份中恢复文件。但根本原因通常不像这样简单明了。
我亲身遇到过的一个例子是,一对虚拟机(VM)服务器发生了“围栏”问题。我曾在一家公司工作时,我们基于 Citrix 的虚拟机服务器(它们是一个集群的一部分)同时宕机,导致每个 Linux 虚拟机也随之宕机。当我给它们接上显示器时,看到它们不停地重启。等到服务器稳定下来后,我开始深入调查。我在 Citrix XenServer 的文档中读到,集群的机器数量不能少于三台,因为这会导致像我所经历的那种情况。我们这个集群只有两台服务器,所以我得出结论,服务器的配置不当,如果公司想要建立集群,就需要一台第三台服务器。
问题在于,这个根本原因分析并不是百分之百完美的。服务器出现问题是因为需要第三台服务器吗?文档确实提到过三台服务器是最低配置,但无法确定这就是问题发生的真正原因。然而,不仅在问题发生时我没有监控服务器,而且我也不是设置服务器的那个人;那个人已经离开公司。虽然无法得出绝对结论,但我的根本原因分析是合理的,因为这是最有可能的解释(即我们没有遵循最佳实践)。有人可能会反驳我的根本原因分析,称“但是这些服务器运行了好几年也没问题。”这倒是真的,但在处理技术问题时,没有什么是绝对的。有时候,你根本无法确知。唯一能做的就是确保一切按照厂商设定的指南正确配置。
一个好的根本原因分析在逻辑上应尽可能严密,尽管不一定是万无一失的。将系统事件与症状关联起来通常是一个很好的第一步,但并不一定是完美的。在调查症状、解决问题并记录你所做的修复工作之后,有时根本原因分析会自动显现。其他时候,你需要查看文档,确保出现故障的服务器或守护进程的配置已按照最佳实践进行实施。在最坏的情况下,你可能无法确切知道问题是如何发生的,也不清楚如何防止它发生,但仍然应该进行文档记录,以便日后发现其他细节。没有文档记录,你永远无法从这个情况中获得任何经验。
根本原因分析应包括以下细节:
-
问题的描述
-
哪个应用程序或硬件出现了故障
-
问题首次出现的日期和时间
-
你在调查问题时发现的内容
-
你为解决问题所采取的措施
-
导致问题发生的事件、配置或故障
根本原因分析应该作为一次学习经验。根据问题的性质,它可能作为不应做的事情或需要改进的做法的范例。以我的虚拟机服务器故障为例,故事的教训是遵循 Citrix 的最佳实践,使用三台服务器而非两台来搭建集群。其他时候,最终结果可能是另一个技术员没有遵循正确的指示或犯了错误,这很不幸。如果将来问题再次发生,你将能够回顾并记得上次发生了什么,以及你做了什么来解决问题。这非常有价值,因为我们都是人,时间一长容易忘记重要的细节。在一个组织中,根本原因分析对于向利益相关者展示你不仅能解决问题,还能合理防止问题再次发生,非常重要。
日志文件通常是寻找线索的好地方,因为系统和应用程序事件的相关信息通常会存储在其中。在下一节中,我们将更详细地探讨日志文件。
查看系统日志
如果你在寻找根本原因时遇到困难,或者你只是想获取更多有关发生问题的信息,可以考虑查看日志文件。Linux 有强大的日志记录功能,许多你可能正在运行的应用程序会在事件发生时写入日志文件。如果出现问题,你可能会在应用程序的日志中找到相关信息。
查看日志有两种主要方法。从历史上看,在大部分 Ubuntu 使用期间,你只需检查存储在/var/log目录中的日志文件即可。该目录中的文件是标准文件和目录,因此你可以使用以往查看文本文件内容的命令来查看/var/log目录中的日志文件内容。这种查看日志文件的方法正在逐步被淘汰;然而,大多数应用程序今天仍然将它们的日志文件存储在该目录中。
查看应用程序日志的较新方法是使用journalctl命令。此命令是systemd的一部分,专门用于查看日志。要使用journalctl命令检查正在运行的服务的状态,你需要提供-u选项,以及你想检查的服务名称:
journalctl -u ssh
通过这个例子,我们尝试查看ssh服务的日志信息:

图 22.1:通过journalctl命令查看日志信息
-u选项是必需的,它告诉journalctl命令你想要检查某个服务。因此,在之前的示例中,我们提供了ssh作为我们想要查找日志信息的服务名称。服务的单元(或服务)名称与使用systemctl命令启动、停止或重启服务时的名称相同。我建议在检查 Linux 系统的日志信息时,首先考虑使用journalctl命令。
如果在-u选项之外再加上-f选项,输出将会继续滚动,随着你检查的特定服务的新日志信息被添加到日志中。如果你想跟踪服务发生的新事件,这非常有用。
然而,并不是所有服务都通过journalctl记录日志,因此了解查看日志文件的传统方法也很重要。在/var/log目录中,你会看到一些日志文件,可以查看这些文件,不同服务器之间会有所不同,具体取决于安装了哪些应用程序。在很多情况下,已安装的应用程序会在/var/log中的某个位置创建自己的日志文件,可能是在日志文件中,或者是在/var/log的子目录下的日志文件。例如,一旦你安装了 Apache,它会在/var/log/apache2目录中创建日志文件,查看这些日志可能会给你一些线索,帮助你了解问题是否与 web 服务器有关。这些被称为应用程序日志,即由应用程序而非发行版创建的日志文件。还有系统日志,它们是由发行版创建的日志文件,用于查看系统事件。
查看存储在/var/log目录中的日志文件可以通过几种方法完成。一个方法是使用cat命令,并指定日志文件的路径和文件名。例如,可以使用以下命令查看 Apache 的访问日志:
cat /var/log/apache2/access.log
一些日志文件是受限的,需要root权限才能访问。如果在尝试查看日志时遇到权限拒绝错误,可以在本节中的任何命令前加上sudo来查看该文件。
cat命令的一个问题是它会打印出整个文件,无论文件有多大。它会在你的终端上滚动,如果文件很大,你将无法看到完整内容。此外,如果你的服务器在性能上已经有些吃力,使用cat实际上可能会暂时占用服务器,尤其是在日志文件非常大的情况下。这会导致你失去对 shell 的控制,直到文件打印停止。你可以按Ctrl + c来停止打印日志文件,但服务器可能忙得无法响应Ctrl + c,并且无论如何都会显示整个文件。
另一种方法是使用tail命令。默认情况下,tail命令显示文件的最后十行:
tail /var/log/apache2/access.log
如果你希望查看超过最后十行的内容,可以使用-n选项来指定不同的行数。要查看最后100行,可以使用以下命令:
tail -n 100 /var/log/apache2/access.log
也许tail命令最有用的特性之一就是-f选项,它允许你跟踪日志文件。基本上,这意味着当日志文件有新的条目被写入时,它会在你面前滚动,就像实时查看日志文件一样。
tail -f /var/log/apache2/access.log
一旦开始使用follow选项,你会想知道自己以前是怎么没有它的。如果你正遇到一个能重现的问题,你可以观察该应用程序的日志文件,并看到日志条目随着问题的重现而出现。以 DHCP 服务器未能向客户端提供 IP 地址为例,你可以查看/var/log/syslog文件的输出(isc-dhcp-server守护进程没有自己的日志文件),并看到当客户端尝试重新获取 DHCP 租约时出现的任何错误,从而让你看到问题的发生过程。
另一个有用的查看日志的命令是less。less命令允许你通过键盘上的 Page Up 和 Page Down 键滚动查看日志文件,这使得它比cat命令在查看日志文件时更加实用。你可以按q退出文件:
less /var/log/apache2/access.log
现在你已经了解了一些查看这些文件的方法,那么应该检查哪些文件呢?不幸的是,并没有统一的规则,因为每个应用程序的日志处理方式不同。一些守护进程有自己的日志文件,存储在/var/log目录下。
因此,一个好的检查位置是该目录,查看是否有一个以守护进程名称命名的日志文件。有些守护进程甚至没有自己的日志文件,而是使用/var/log/syslog。你可以尝试在查看文件内容时使用grep来查找与你正在排查的守护进程相关的消息。关于isc-dhcp-server守护进程,以下命令将会把syslog缩小到来自该特定守护进程的消息:
cat /var/log/syslog | grep dhcp
在排查安全问题时,你肯定会想查看的日志文件是授权日志,该日志位于/var/log/auth.log。你需要使用root账户或sudo来查看该文件。授权日志包含有关对服务器进行身份验证尝试的信息,包括来自服务器本身的登录以及通过 OpenSSH 的登录。这样做有几个原因,其中之一是如果服务器上发生了严重问题,你可以查看在那个时间点谁登录了服务器。此外,如果你或你的某个用户在通过 OpenSSH 访问服务器时遇到问题,你可能需要查看授权日志来寻找线索,因为 OpenSSH 失败的更多信息会被记录在其中。通常,ssh命令可能会抱怨密钥文件的权限不正确,这会给出公钥认证无法正常工作的问题答案,因为 OpenSSH 对其文件有特定的权限要求。例如,私钥文件(通常是/home/<user>/.ssh/id_rsa)不应该被除了其拥有者外的任何人读取或写入。如果是这种情况,你会在/var/log/auth.log中看到类似的错误信息。
检查/var/log/auth.log的另一个使用场景是安全性,因为大量的登录尝试可能表明有入侵企图。(希望你已经安装了 Fail2ban,我们在上一章讨论过。)异常高的密码尝试失败次数可能意味着有人正在尝试通过暴力破解登录服务器。这肯定是一个需要关注的警告,你应该立即封锁他们的 IP 地址。
系统日志,位于/var/log/syslog,包含了许多不同内容的日志信息。它本质上是 Ubuntu 日志的瑞士军刀。如果某个守护进程没有自己的日志文件,很可能它的日志会被写入此文件。此外,关于 cron 作业的信息也会写入此处,因此当 cron 作业未能正确执行时,这里是一个可以检查的地方。负责从 DHCP 服务器获取 IP 地址的dhclient守护进程同样非常重要。
你将能够从dhclient事件中看到系统日志中的 IP 地址更新情况,也可以看到无法获取 IP 地址时的失败消息。此外,systemd init守护进程本身也会在这里记录日志,这使得你可以看到与服务器启动以及它尝试运行的应用程序相关的消息。
另一个有用的日志文件是/var/log/dpkg.log,它记录了与安装和升级软件包相关的日志条目。如果服务器在你通过网络推送更新后开始出现问题,你可以查看这个日志,查看最近更新了哪些软件包。这个日志不仅会提供已更新或安装的软件包列表,还会提供安装发生时的时间戳。如果某个用户安装了未经授权的应用程序,你可以将该日志与认证日志进行关联,确定当时谁登录了系统,然后查看该用户的 Bash 历史记录以确认。
日志文件通常会在一段时间后通过名为logrotate的工具进行轮转。在/var/log目录中,你会看到一些带有.gz扩展名的日志文件,这意味着原始的日志文件已经被压缩并重命名,并且一个新的日志文件已经在原位置创建。例如,你会在/var/log目录中看到系统日志的syslog文件,但你也会看到一些带有数字和.gz扩展名的文件,如syslog.2.gz。这些是压缩后的日志文件。通常,你需要先解压它们,然后通过本节中提到的任意方法查看这些日志。更简单的方法是使用zcat命令,它允许你立即查看压缩文件:
zcat /var/log/syslog.2.gz
还有zless,它的作用与less命令类似。
另一个有用的查看日志信息的命令是dmesg。与其他日志文件不同,dmesg本身就是一个命令,你可以从文件系统中的任何位置执行它。dmesg命令允许你查看来自 Linux 内核环形缓冲区的日志条目,这在排查硬件问题时非常有用(例如查看哪些硬盘被内核识别)。在排查硬件问题时,系统日志也很有帮助,但使用dmesg命令可能是一个好的检查起点。
如前所述,在 Ubuntu 系统上,有两种类型的日志文件,系统日志和应用程序日志。系统日志,如auth.log和dpkg.log,记录了重要的系统事件,并且不特定于某个应用程序。应用程序日志在安装其父软件包时一起安装,例如 Apache 或 MariaDB。应用程序日志会将日志条目写入其自己的日志文件。
一些你安装的守护进程不会创建自己的应用程序日志,例如isc-dhcp-server。由于没有统一的规则规定应用程序日志存放位置,因此查找日志文件的第一步是查看你想查看日志条目的应用程序是否创建了自己的日志文件。如果没有,可能是使用了系统日志。
在面对一个问题时,同时查看日志文件并尝试重现问题非常重要。使用follow模式与tail命令(tail -f)对于此目的非常有效,因为你可以在尝试重现问题时,观察日志文件生成新的条目。这种技术在几乎任何你处理不正常运行的守护进程时都能很好地发挥作用。这个技术也能帮助你缩小硬件问题的范围。例如,我曾处理过一个 Ubuntu 系统,当我插入闪存驱动器时,什么也没发生。当我在插拔闪存驱动器时查看日志时,我看到系统日志更新并识别了每一次插入和移除。因此,很明显,Linux 内核本身识别到了硬件并准备使用它。这帮助我将问题缩小到桌面环境未能更新以显示插入的闪存驱动器,而我的硬件和 USB 端口完全正常。通过一个命令,我能够确定问题是软件问题,而不是硬件相关的问题。
如你所见,Ubuntu 包含了非常有用的日志文件,这些文件能帮助你排查服务器问题。通常,当你遇到问题时,查看相关的日志条目,然后进行 Google 搜索,往往会得到有用的答案,或者至少会找到一个 bug 报告,告诉你这个问题不仅限于你或你的配置。
希望你的搜索结果能直接带你找到答案,或者至少找到一个解决方法。然后,你可以继续处理问题,直到它被解决。
那么,网络问题呢?追踪网络问题的根本原因可能特别具有挑战性,但实际上并不像看起来那么难。在接下来的部分,我们将介绍几种追踪网络问题的方法。
追踪网络问题
今天,TCP/IP 网络对于世界的重要性真是令人惊讶。在现代计算中使用的所有协议中,它无疑是最为广泛的。但当它无法正常工作时,它也是最令人头疼的情况之一。幸运的是,Ubuntu 提供了非常实用的工具,帮助你找出问题所在。
首先,让我们看看连接性。毕竟,如果你无法连接到网络,你的服务器基本上就没用了。在大多数情况下,Ubuntu 几乎能够无误地识别所有网络卡,并且如果它能够连接到一个 DHCP 服务器,它会自动将你的服务器或工作站连接到网络。
在排查问题时,先处理显而易见的东西。以下内容看起来可能是显而易见的,但你会惊讶地发现有多少人会忽略这些显而易见的事情。我假设你已经检查过网络电缆是否在两端插好。关于电缆的另一个方面是,有时候网络电缆本身会出现故障,需要更换。你应该可以使用电缆测试仪来检查电缆是否能传递干净的信号。
路由问题有时可能很难排查,但通过逐个测试每个目的地点,你通常能找到问题所在。路由问题的典型症状可能包括无法访问另一个子网内的设备,或者尽管能够访问内部设备,却无法访问互联网。要调查潜在的路由问题,首先检查你的路由表。你可以使用ip route命令来查看,运行此命令将打印出当前的路由表信息:

图 22.2:在 Ubuntu 服务器上查看路由表
在这个例子中,你可以看到所有流量的默认网关是10.10.10.1。这是表格中的第一项,告诉我们所有到达目标0.0.0.0(即所有流量)的数据都通过10.10.10.1发送。只要 ICMP 流量没有被禁用,你应该能够 ping 通这个默认网关,并且应该能够 ping 通子网内的其他节点。
要开始排查路由问题,首先使用打印出的路由表中的信息进行几个 ping 测试。首先,尝试 ping 你的默认网关。如果无法 ping 通,那么问题就在这里。如果可以 ping 通,接下来尝试运行traceroute命令。
这个命令默认并不可用,但你只需要安装traceroute包,所以下希望你已经在服务器上安装了它。如果已安装,你可以对主机运行traceroute,比如外部 URL,来查找连接中断的位置。traceroute命令应该显示你与目标之间的每一个跳跃点。每个“跳跃”基本上是一个默认网关。你会依次穿过每个网关,直到最终到达目标。使用traceroute命令,你可以看到链条中断的地方。很可能,你会发现问题可能并不出现在你的网络上,而是出现在你的互联网服务提供商那一端。
DNS 问题并不常见,但通过一些技巧,你应该能够解决它们。DNS 故障的症状通常表现为主机无法通过名称访问内部或外部资源。
确定问题出在内部主机还是外部主机(或者两者都有)应该有助于你判断问题是不是出在 DNS 服务器,或者可能是你的互联网服务提供商的 DNS 服务器。
定位 DNS 问题源的第一步是 ping 网络中的已知 IP 地址,最好是默认网关。如果你能 ping 通它,但无法通过名称 ping 通网关,那么你可能遇到了 DNS 问题。你可以通过使用 nslookup 命令对域名进行查询来确认潜在的 DNS 问题,例如:
nslookup myserver.local
此外,请确保尝试 ping 外部资源,例如网站。这将帮助你缩小问题的范围。
你还需要了解你的主机正在向哪个 DNS 服务器发送查询请求。过去,查找分配给主机的 DNS 服务器就像查看 /etc/resolv.conf 文件内容一样简单。然而,现在这个文件通常会指向本地解析器,而不会显示实际的服务器请求目标。要找出分配给主机的真实 DNS 服务器,可以使用以下命令:
resolvectl status
它们是你预期的 DNS 服务器吗?如果不是,你可以通过临时删除该文件中的错误名称服务器条目,并将其替换为正确的 IP 地址来解决这个问题。我建议将其作为临时解决方案而非永久解决方案的原因是,接下来你需要调查无效的 IP 地址是如何出现在此文件中的。通常,这些地址是由你的 DHCP 服务器分配的。只要你的 DHCP 服务器发送适当的名称服务器列表,你就不应该遇到这个问题。如果你使用的是静态 IP 地址,那么可能是你的 Netplan 配置文件中存在错误。
在无法解析外部网站时,定位 DNS 问题的一个有用方法是暂时切换本地计算机的 DNS 提供商。通常,你的计算机会使用外部 DNS 提供商,例如来自 ISP 的 DNS。你设置外部 DNS 服务器的过程是我们在第十一章,设置网络服务中提到的,具体来说是 bind9 守护进程配置中的转发器部分。bind9 守护进程使用的转发器是它无法基于内部主机列表解析请求时发送流量的地方。
你可以通过将本地工作站的 DNS 名称服务器更改为 Google 的 8.8.8.8 和 8.8.4.4 来绕过此问题。如果在更改名称服务器后能够访问外部资源,那么你可以合理地认为是转发器导致了问题。
我实际上曾经遇到过这样一种情况:一个网站更改了其 IP 地址,但 ISP 的 DNS 服务器未能及时更新,导致一些客户无法访问他们工作所需的网站。将所有人切换到备用名称服务器(通过调整我们在第十一章,设置网络服务中提到的 forwarders 选项)是他们解决此问题的最简单方法。
在检查服务器解析 DNS 记录的能力时,一些额外的工具需要考虑,例如dig和nslookup。你应该能够使用这两个命令来测试服务器的 DNS 设置。两个命令都可以使用主机名或域名作为选项。dig命令将向你展示有关 DNS 区域文件中负责 IP 地址或域名的地址(A)记录的信息。host命令应返回你尝试连接的主机的 IP 地址。以下是一些示例输出:

图 22.3:dig 和 host 命令的输出
硬件支持在网络中也至关重要。如果 Linux 内核不支持你的网络硬件,你很可能会遇到这种情况:当你插入网络电缆时,发行版无法识别或没有任何反应,或者在无线网络的情况下,尽管有一个或多个网络,但却看不到任何附近的网络。与 Windows 平台不同,Linux 在硬件支持方面通常是集成在内核中的。虽然也有例外,但发行版随附的 Linux 内核通常支持与其自身同龄或更早的硬件。
在 Ubuntu 22.04 LTS(该版本于 2022 年 4 月发布)的情况下,它能够支持截至 2022 年初发布的硬件和更早的硬件。未来的 Ubuntu Server 版本将发布硬件支持更新,这将使 Ubuntu Server 22.04 能够支持新发布的硬件和芯片组。通常,Ubuntu 会在支持的发行版生命周期内发布几个版本更新,例如 22.04.1、22.04.2 等等。只要你使用的是最新版本,你就能获得 Ubuntu 在当时提供的最新硬件支持。
在其他情况下,硬件支持可能依赖于外部内核模块。如果缺少硬件驱动程序,当你遇到无法识别的网络硬件时,应该首先尝试使用搜索引擎查找该硬件。通常,搜索词 <硬件名称> Ubuntu 就能解决问题。但你该搜索什么呢?要查找你的网络设备的硬件字符串,可以尝试使用 lspci 命令:
lspci | grep -i net
lspci 命令列出了连接到服务器 PCI 总线的硬件。这里,我们使用该命令并结合不区分大小写的 grep 搜索 net 这个词:
lspci |grep -i net
这将返回服务器上可用的网络组件列表。例如,在我的机器上,我得到以下输出:
01:00.1 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller (rev 12)
02:00.0 Network controller: Intel Corporation Wireless 8260 (rev 3a)
如你所见,我的机器上有一块有线和无线网卡。如果其中一个无法工作,我可以通过在线搜索硬件字符串和关键字 Ubuntu 来查找信息,这应该会给我关于我硬件的相关结果。如果需要安装某个软件包,搜索结果通常会给我一些关于需要安装哪个软件包的线索。
然而,如果没有网络访问,最坏的情况是我可能需要从另一台计算机下载软件包,并通过闪存驱动器将其传输到服务器。这肯定不是一件有趣的事情,但如果最新的 Ubuntu 安装媒体尚未完全支持你的硬件,这种方法是有效的。
另一个潜在的问题点是 DHCP。当它运行正常时,DHCP 是一项神奇的技术。当它停止工作时,则可能令人沮丧。但一般来说,DHCP 问题往往是由于可用的 IP 地址不足、DHCP 守护进程(isc-dhcp-server)未运行、配置无效或主机的时钟不同步(所有服务器应该安装 ntp 包)。
如果你的服务器无法通过 DHCP 获取 IP 地址,并且你的网络使用的是基于 Linux 的 DHCP 服务器,请检查系统日志(/var/log/syslog)中与 dhcpd 相关的事件。不幸的是,我从未找到过可以直接显示 DHCP 服务器剩余 IP 地址租约数的命令,但如果你用完了租约,系统日志中很可能会看到与池耗尽相关的日志条目。此外,系统日志还会显示你的节点尝试获取 IP 地址时的尝试记录。你可以使用 tail -f 命令实时监控系统日志,以观察与 DHCP 租约相关的任何事件。
在某些情况下,DHCP 租约不可用的问题可能是由于启用了非常长的租约时间。一些管理员会给客户端设置最长一周的租约时间,而这通常是不必要的。对于大多数网络来说,一天的租约时间就足够了,但最终,租约时间的决定权在于你自己。在第十一章,设置网络服务中,我们已经讨论了如何配置 DHCP 服务器,因此如果你需要回顾如何配置 isc-dhcp-server 守护进程,可以参考那一章。
尽管在面对 DHCP 问题时,这可能不是你首先想到的原因,但主机时钟不同步实际上可能会加剧问题。DHCP 请求在客户端和服务器上都会被 时间戳,因此,如果其中一方的时钟误差较大,时间戳也会出错,从而导致 DHCP 服务器混淆。令人惊讶的是,我曾经遇到过这种情况相当频繁。我建议尽早在你的网络中标准化 NTP 配置。时钟不同步不仅会影响 DHCP 服务,文件同步工具也需要准确的时间。如果你确保所有客户端都安装了 NTP,并且它是最新且正常运行的,你应该能确保一切顺利。使用配置管理工具(例如 Ansible)确保 NTP 不仅配置正确,还在网络中所有机器上正常运行,将对你大有裨益。
当然,网络方面可能出现许多问题,但这里的信息应该涵盖大多数问题。总的来说,排查网络问题通常围绕着 ping 测试展开。尝试 ping 默认网关、使用traceroute跟踪失败的端点,以及排查 DNS 和 DHCP 问题,可以解决大多数问题。当然,硬件故障,如网卡损坏或电缆问题,也会显现出来。
我们的服务器利用存储、CPU、内存以及其他资源为我们提供价值并服务于我们的客户。在接下来的章节中,我们将详细介绍如何检查这些资源。
排查资源问题
我不知道其他人怎么样,但似乎我大多数排查服务器问题的时间通常都花在定位资源问题上。这里所说的资源指的是 CPU、内存、磁盘、输入/输出等。一般来说,问题通常出在用户存储了过多的大文件,某个进程异常消耗大量 CPU,或者服务器内存不足。在本节中,我们将讨论一些你在管理 Ubuntu 服务器时可能会遇到的常见问题。
首先,让我们回顾一下与存储相关的话题。在第九章 管理存储卷中,我们已经讨论过一些相关的概念,很多这些概念也适用于故障排除。因此,我不会在这里花太多时间讲解这些概念,但在排查存储问题时,复习这些内容是很有必要的。首先,每当用户抱怨无法在服务器上写入新文件时,以下两个命令是你应该首先运行的。你可能已经非常熟悉这些命令了,但它们值得重复:
df -h
df -i
第一个 df 命令的变体会以人类可读的格式(-h 选项)提供关于驱动器空间使用情况的信息,这些信息会以兆字节和吉字节为单位打印出来。
第二个命令中的-i选项会提供关于正在使用和可用 inode 的信息。你需要运行此命令的原因是,在 Linux 系统中,如果没有剩余的 inode,系统会报告存储已满,即使仍然有大量的可用空间。通常,存储介质可用的 inode 数量是非常充裕的,限制也很难达到。然而,如果一个服务不断地创建新的日志文件,或者邮件守护进程失控并生成大量未发送的邮件,你会惊讶于 inode 会这么快耗尽。
当然,一旦你发现存储已满,接下来的逻辑问题是,是什么占用了我所有的空闲空间?df 命令会列出存储卷及其大小,至少会告诉你哪个磁盘或分区需要关注。正如我在第九章《管理存储卷》中提到的,我最喜欢用的定位存储占用的命令是 ncdu 命令。虽然默认没有安装,ncdu 是一个非常棒的工具,可以检查存储的主要占用位置。如果单独运行,ncdu 会扫描服务器的整个文件系统。相反,我建议加上 -x 选项,这样它只会限制扫描特定文件夹。例如,如果服务器的 /home 分区已满,你可能希望运行以下命令来查找哪个目录占用了最多的空间:
sudo ncdu -x /home
-x 选项会导致 ncdu 不跨越文件系统。这意味着如果你在扫描的文件夹内挂载了另一个磁盘,它将不会触及这个磁盘。使用 -x 选项时,ncdu 只关注你指定的目标。
如果你无法使用 ncdu,也可以使用 du 命令,不过需要更多的操作。例如,du -h 命令会显示当前工作目录的使用情况,且以人类可读的数字呈现。它默认不会像 ncdu 那样遍历目录树,因此你需要在每个子目录上运行它,直到手动找到占用最多文件的目录。du 命令的一个非常有用的变种,昵称为 ducks,如下所示。它将显示当前工作目录中最大的前 15 个目录:
du -cksh * | sort -hr | head -n 15
另一个可能出现的存储卷问题是与文件系统完整性相关。大多数时候,这些问题似乎只有在出现电力问题时才会出现,比如服务器意外断电。根据服务器和在设置存储卷时所使用的格式(以及其他几个因素),电力问题的处理方式因安装而异。在大多数情况下,文件系统检查(fsck)会在下次启动时自动发生。如果没有发生,并且你遇到了一些无法解释的存储问题,建议手动进行文件系统检查。安排文件系统检查其实非常简单:
sudo touch /forcefsck
上面的命令会在文件系统的根目录创建一个空文件 forcefsck。当服务器重启并看到这个文件时,它会触发对该存储卷的文件系统检查,然后删除该文件。
如果你想检查根存储卷以外的文件系统,可以在其他地方创建 forcefsck 文件。例如,如果你的服务器有一个单独的 /home 分区,你可以在该位置创建文件来检查该存储卷:
sudo touch /home/forcefsck
文件系统检查通常会相对较快地完成,除非有需要修复的问题。根据问题的性质,可能会快速修复,或者可能需要较长时间。我见过一些非常严重的完整性问题,修复需要超过四个小时,但我也见过其他问题只需几秒钟就能修复。有时它会非常快速地完成,在启动过程中会快速滚动,以至于你可能会错过看到它的机会。在大容量的情况下,你可能希望安排fsck检查在非工作时间进行,以防扫描时间较长。
关于内存问题,free -m命令将为你提供服务器上可用内存和交换空间的概述。它不会告诉你究竟是什么正在使用你的所有内存,但你可以用它来查看是否面临内存耗尽的危险。free命令的输出中的free列将显示剩余多少内存,并允许你决定何时采取行动:

图 22.4:检查 Ubuntu 服务器上可用内存
在第八章中,监控系统资源,我们介绍了htop命令,它帮助我们回答“什么”正在使用我们的资源的问题。安装了htop后,你可以通过按下F6然后选择一个新的排序字段(如PERCENT_CPU或PERCENT_MEM)来按 CPU 或内存使用率对进程列表进行排序。这将让你了解到服务器上哪些内容正在消耗资源,从而让你可以做出相应的决策。你的解决方案可能因进程而异,从增加服务器内存到调整应用程序以降低内存使用上限都有可能。但当htop的结果与你观察到的使用情况不符时,你该怎么办呢?例如,如果你的负载平均值很高,但没有进程似乎占用大量 CPU?
在本书中,我还没有讨论过的一个命令是iotop。虽然不是默认安装的,但iotop实用程序绝对是必备的,因此我建议你安装iotop包。iotop本身需要以root或使用sudo来运行:
sudo iotop
iotop命令将允许你查看数据写入或从磁盘读取的量。输入/输出(IO)绝对会对系统的负载产生影响,而并非所有的资源监控工具都会显示这种使用情况。如果你看到负载平均值很高,但资源监视器中没有任何内容可以解释这一使用情况,请检查 IO。
iotop实用程序是一个很好的方式,因为如果数据在写入磁盘时受到限制,这可能会导致 IO 中的严重开销,从而减慢其他进程的速度。即使没有其他影响,它也能让你知道哪个进程在出现问题,以便你需要终止它:

图 22.5:在 Ubuntu 服务器上运行的 iotop 实用程序
iotop窗口会自动刷新,根据高亮的列排序进程。要改变高亮列,你只需要按键盘上的左箭头和右箭头。你可以按照IO、SWAPIN、DISK WRITE、DISK READ等列来排序进程。完成后,按q退出应用程序。
本节中介绍的工具在识别瓶颈资源问题时非常有用。在发现问题根源后,你的解决方法将取决于守护进程。可能是配置无效,或者守护进程遇到故障需要重新启动。通常,查看日志可能会帮助你找到守护进程出现问题的原因。在存储空间满了的情况下,几乎没有什么工具能比得上ncdu,它几乎总能直接指向问题所在。像htop和iotop这样的工具也能让你查看关于资源使用的更多信息,而htop甚至允许你直接在应用程序内按F9键杀死一个表现不正常的进程。
当系统内存(RAM)发生物理故障时,你该怎么办?这种情况比你想象的要更常见。在接下来的章节中,我们将探讨一种方法,来测试我们的 RAM 是否存在故障。
诊断有缺陷的 RAM
所有的服务器和计算组件最终都会出现故障,但有一些硬件似乎比其他组件更容易出现问题。风扇、电源和硬盘绝对是管理员更换的常见设备,但有缺陷的内存也是一个我相信你最终会遇到的情况。
尽管内存条发生故障是有可能的,但我把它放在本章的最后一节,因为不幸的是,我无法给出一份明确的症状清单,帮助你判断内存是否是问题的根源。RAM 问题本质上是非常神秘的,每次我遇到这类问题时,总是通过排除其他所有可能的原因后,才最终发现是内存坏了。正因如此,现在我通常会先测试服务器或工作站的内存,因为这非常简单。即使内存问题与某个问题无关,检查它也是值得的,因为它可能以后会成为问题。
大多数 Linux 发行版(包括 Ubuntu)在安装介质中都带有Memtest86+。无论你是创建可启动的 CD 还是闪存驱动器,都可以从 Ubuntu Server 的安装介质中选择内存测试选项。当你首次从 Ubuntu Server 介质启动时,你会看到一个位于屏幕底部的图标,表示你可以按一个键来显示菜单(如果不按键,安装程序会自动启动)。接下来,你需要选择语言,然后会显示安装菜单。在菜单中会有一个测试内存的选项:

图 22.6:Ubuntu 安装程序的主菜单,显示了一个“测试内存”选项
其他版本的 Ubuntu,如 Ubuntu 桌面版或其任何衍生版,也提供了测试内存的选项。即使你没有服务器版的安装介质,你也可以使用你手头上的任何版本。
当你从安装介质中选择测试内存选项时,Memtest86+程序会立即开始工作并测试你的内存(按Esc键退出测试)。根据你的工作站或服务器安装的内存量,测试可能需要很长时间,可能需要几分钟甚至几个小时才能完成。一般来说,当你的机器存在有缺陷的 RAM 时,你会在相对较短的时间内看到一堆错误,通常是在前 5-10 分钟内。
如果在 15 分钟内没有出现错误,通常说明你的系统状态良好。根据我的经验,每次遇到有缺陷的内存时,我都会在 15 分钟内或更短时间内看到错误(通常是在 5 分钟内)。不过,从理论上讲,你的内存模块可能存在一个小问题,这个问题可能在 15 分钟之后才会出现,所以如果你有时间,最好让测试完成。
主要的问题是:什么时候在机器上运行Memtest86+。根据我的经验,内存有问题的症状在不同机器上几乎从来都不相同。通常,你会遇到服务器无法正常启动、应用程序意外关闭、应用程序根本无法启动,或者某个应用程序行为不正常的情况。在我看来,当你遇到一个问题,看起来不那么直接明了时,应该进行内存测试。此外,你可能还想在将服务器投入生产之前,先测试一下内存。这样,你可以确保它在开始时尽可能不含硬件问题。如果你安装了新的内存模块,请务必立即测试 RAM。
如果测试报告了错误,接下来你需要找出哪个内存模块有问题。这可能会比较困难,因为一些服务器可能安装了十几个内存模块。为了缩小范围,如果可能的话,你应该独立测试每个内存模块,直到找出哪个模块是有缺陷的。即使你找到了有问题的模块,也应该继续测试其他模块。原因是,多个内存模块同时出现问题并非不可能,因为导致第一个模块出现故障的情况可能也影响了其他模块。
另外,我想给你提供一个关于内存的建议:当你发现某个内存条有问题时,如果可以的话,最好清空硬盘并重新开始。我理解这并非总是可行的,而且你可能已经花了很多时间来设置服务器。有些服务器可能需要几周才能重建,具体取决于它们的工作负载。但至少要记住,任何经过有缺陷的 RAM 的数据都有可能变得损坏。
这意味着,如果数据在写入磁盘之前曾存放在有缺陷的 RAM 区域,那么静态数据(存储在硬盘上的数据)可能会损坏。当服务器或工作站遇到有缺陷的 RAM 时,你就不能再信任它了。我将如何处理这种情况留给你决定(希望你永远不会遇到这种问题),但在你计划行动方案时,记住这一点。就个人而言,我在硬件出现此类问题后不再信任任何操作系统的安装。
我还建议你在遇到奇怪问题时检查服务器主板上的电容器。虽然这与内存问题不一定相关,但我在此提到它是因为当电容器损坏时,症状与内存故障基本相同。我不是让你去买一个电压表或做任何电工工作,但有时候,打开服务器机箱,拿手电筒照一下电容器,看是否有任何电容器泄漏液体或膨胀,确实是有意义的。我之所以提这个,是因为我曾经花了几个小时排查一台机器(不止一次),测试内存、硬盘并查看系统日志,却没有发现明显的原因,后来再检查硬件时才发现主板上的电容器在泄漏。如果我早早检查电容器,可能会省去不少时间。这其实就是你需要做的:快速扫一眼主板,看看有没有什么不对劲的地方。
总结
虽然 Ubuntu 通常是一个非常稳定和安全的平台,但了解如何应对可能出现的问题并做好准备是很重要的。在本章中,我们讨论了当服务器无法正常运行时,可以进行的常见故障排除。我们首先评估了问题的范围,这有助于我们了解有多少用户或服务器受到了影响。然后,我们查看了 Ubuntu 的日志文件,这些日志文件包含了大量信息,帮助我们定位问题并缩小问题范围。我们还讨论了几种可能出现的网络问题,例如 DHCP、DNS 和路由问题。我们当然无法在问题发生之前预测它们,也无法提前为每一种可能发生的问题做好准备。然而,将合理的逻辑和常识应用于问题处理中,将大大帮助我们找出根本原因。
在下一章中,我们将讨论如何防止灾难的发生,并在灾难发生时如何进行恢复。我们下章见!
进一步阅读
- 在 Ubuntu 服务器中报告错误:
learnlinux.link/report-bugs
加入我们在 Discord 上的社区
加入我们社区的 Discord 讨论区,与作者及其他读者交流:

第二十三章:防止灾难
在企业网络中,灾难随时可能发生。作为管理员,尽管我们总是尽力设计尽可能稳定和容错的服务器系统,但最重要的是我们如何应对灾难的发生。尽管服务器硬件非常稳定,但服务器的任何组件都有可能随时出现故障。在灾难面前,我们需要一个计划。当磁盘发生故障时,你如何恢复数据?当服务器突然无法启动时,你该怎么办?这些只是我们将探讨的部分问题,我们将介绍几种防止和恢复灾难的方法。在最后一章中,我们将涵盖以下主题:
-
防止灾难
-
使用 Git 进行配置管理
-
实施备份计划
-
使用可启动的恢复介质
我们将通过一些防止灾难的技巧开始本章内容。
防止灾难
在本章进行时,我们将探讨如何从灾难中恢复。然而,如果我们能从一开始就防止灾难的发生,那就更好了。虽然我们肯定不能防止所有可能发生的灾难,但有一个良好的计划,并严格按照计划执行,可以大大降低灾难发生的可能性。一个好的灾难恢复计划应该包括一系列实施新服务器和管理现有服务器时需要遵循的准则。
该计划可能包括一些信息,如经过认证的硬件列表(例如在某个环境中已知能高效运行的硬件配置),以及用户的规则和规定、确保物理和软件安全的指南列表、对终端用户的适当培训以及变更控制方法。这些概念在书中早些时候已提到,但从灾难预防的角度来看,值得再次强调。
首先,我们在第二十一章《保护你的服务器》中讨论过最小权限原则。其思想是给用户尽可能少的权限。这对安全性非常重要,因为你希望确保只有经过专门培训的人员,才能访问并修改他们需要的资源。数据丢失是常有的事。为了充分利用这一原则,你需要创建一组用户组,作为整体安全设计的一部分。列出你公司中的部门和岗位,以及每个岗位需要执行的活动类型。创建与这些活动相对应的系统组。例如,创建一个accounting-ro(只读)和accounting-rw(读写)组,用来分类你会计部门中的用户,这些用户应该只具有读取或读写数据的权限。如果你只是在管理一个家庭文件服务器,注意不要让默认情况下就有读写权限的开放网络共享。通过让用户尽可能少做,你可以立即防止许多灾难的发生。
在第二章,用户与权限管理(以及第二十一章,保护你的服务器)中,我们讨论了 sudo 命令的最佳实践。虽然 sudo 命令非常有用,但它经常被滥用。默认情况下,任何是 sudo 组成员的人都可以使用 sudo 执行任何操作。我们讨论了如何限制 sudo 访问特定命令,这是始终推荐的做法。只有受信任的管理员才应该拥有完全的 sudo 访问权限。其他人应该仅在真正需要时才获得 sudo 权限,而且只有在执行他们工作所必需的命令时才应授予此权限。拥有 sudo 完全访问权限的用户可以删除整个文件系统,因此这一权限绝不应轻视。
关于网络共享,尽可能默认设置为只读总是最好的选择。这不仅是因为用户可能会不小心删除数据,还因为应用程序也有可能出现故障并删除数据。通过只读共享,文件的修改或删除是不可能的。可以为需要修改权限的人创建额外的读写共享,但如果可能的话,始终默认设置为只读。
尽管我花了很多时间讨论软件方面的安全性,但物理安全同样重要。就本书而言,物理安全并没有太多涉及,因为我们的主题专门是 Ubuntu Server,而你在 Ubuntu 上安装的任何东西都不会增加服务器的物理安全性。值得注意的是,物理安全和操作系统、应用程序、数据文件的安全一样重要。
只需要有人在服务器室里绊倒一根网络电缆,就能干扰整个子网或使生产应用程序下线。服务器室应该上锁,只有受信任的管理员可以访问设备。我相信这不言而喻,也许听起来很显然,但我曾在几家公司工作,它们并没有保护好服务器室。把重要设备放在未经授权人员触手可及的地方,绝不会带来好结果。
在这一节中,我提到了第二十一章,保护你的服务器,已经好几次了。灾难预防计划的大多数内容都涉及安全性。这包括但不限于确保及时安装安全更新,使用如故障监视器和防火墙等安全应用程序,以及确保 OpenSSH 设置安全。我在这里不会再重复这些概念,因为我们已经讨论过了,但本质上,安全性是灾难预防计划中的一个非常重要部分。毕竟,用户无法破坏他们无法访问的东西,如果你设计了一个注重安全的网络,黑客也更难侵入。
有效的灾难预防包括一系列指南,涵盖用户管理、服务器管理、应用程序安装、安全性和流程文档等内容。完整的灾难预防解说本身将是一本完整的书。我在本节的目标是为您提供一些可以用来开始制定自己计划的想法。灾难预防计划不是一次性创建的,而是随着您对安全性和需要注意的事项的了解而不断创建和完善。
服务器上的配置文件决定了服务和应用程序的行为,而备份可以帮助您恢复它们的状态。在管理文件状态方面,Git 是一个非常强大的工具,我们将在接下来详细讨论它。
利用 Git 进行配置管理
服务器上最有价值的资产之一是其配置。这仅次于服务器存储的数据。通常,当我们在服务器上实施新技术时,我们会花费大量时间编辑服务器上的各种配置文件,以使其尽可能地发挥作用。这可能包括从 Apache 虚拟主机文件到 DHCP 服务器配置、DNS 区域文件等任意数量的内容。如果服务器遇到灾难,唯一的补救措施是完全重建它,那么我们最不想做的就是从头开始重新设计所有这些配置。这就是 Git 的用武之地。
在典型的开发环境中,由工程师团队开发的应用程序可以通过 Git 进行管理,每个工程师都可以贡献到托管其软件源代码的存储库中。使 Git 如此有用的其中一点是,您能够立即返回文件的以往版本,因为它保存了存储库内所有文件更改的历史记录。
Git 不仅对软件工程师有用,它也是我们在服务器上跟踪配置文件变化的非常有用的工具。对于我们的用例,我们可以使用它记录对配置文件的更改并将其推送到中央服务器进行备份。当我们进行配置更改时,我们将更改推送回我们的 Git 服务器。如果由于某种原因我们需要在服务器故障后恢复配置,我们只需从 Git 上下载我们的配置文件即可重新配置到新服务器上。这种方法的另一个有用之处是,如果管理员实施了破坏服务的配置文件更改,我们可以简单地回滚到已知的工作正常的提交,并立即恢复服务。您甚至可以将日志文件中的更改与 Git 存储库中大致相同时进行的更改进行关联,这样可以更容易地缩小问题的根本原因。
服务器上的配置管理非常重要,事实上,我强烈建议每个 Linux 管理员都利用版本控制来进行配置管理。虽然一开始可能会觉得有点棘手,但一旦练习过后,实际上是非常容易上手的。完成将 Git 用于跟踪所有服务器配置文件后,你会惊讶于没有它时你是如何生活的。我们在第十五章《使用 Ansible 自动化服务器配置》中简要介绍了 Git,我带你通过创建 GitHub 仓库来托管 Ansible 配置。然而,使用 GitHub 并不是使用版本控制的必要条件。虽然它确实非常方便,但并不是必需的。要让服务器作为 Git 服务器工作,唯一需要的组件就是 git 包的存在:
sudo apt install git
就这样,你现在已经有了一个 Git 服务器。这可能看起来过于简单(确实是),但要将 Git 设置为网络上的中央资源并没有太多要求。由于 Git 默认使用 OpenSSH,你将能够在安装了 git 包并且对网络上的客户端可访问的服务器上存储仓库。
如果由于某种原因你没有额外的服务器作为 Git 服务器,你可以在另一个实例上安装 git 包,并将该功能添加到现有服务器中。我个人的偏好是让每个服务器专注于一个任务(并且做好这个任务),但有时候组织的预算可能不允许为每个服务配置独立的服务器。
现在,想象一个对你很重要的配置目录,你希望将其纳入版本控制。一个好的例子是 Web 服务器上的 /etc/apache2 目录。这个目录将在本节的示例中使用。但你当然不限于此,任何你不想丢失的配置目录都是一个合适的候选。如果你选择使用不同的配置路径,请在我的示例中将路径更改为该路径。
在服务器上,创建一个目录来存放你的仓库。我将在示例中使用 /git:
sudo mkdir /git
接下来,你需要将该目录的所有权修改为你在 Ubuntu 服务器上使用的管理员用户。通常,这个用户是在安装操作系统时创建的用户。实际上,你可以使用任何用户,只要确保该用户能够使用 OpenSSH 访问你的 Git 服务器。将 /git 目录的所有权更改为此用户。我在 Git 服务器上的用户名是 jay,所以在我的例子中,我会使用以下命令更改所有权:
sudo chown jay:jay /git
接下来,我们将在/git目录内创建我们的 Git 仓库。对于 Apache,我将在/git目录内为其创建一个裸仓库。裸仓库基本上是一个 Git 仓库的骨架,里面没有任何实际数据,只有一些默认配置使其能够作为一个 Git 文件夹。要创建裸仓库,cd进入/git目录并执行:
git init --bare apache2
你应该会看到以下输出:
Initialized empty Git repository in /git/apache2/
目前我们只需要在服务器上做这些操作,就可以实现 Apache 仓库的目的。在你的客户端(存放你想要放入版本控制的配置文件的服务器)上,我们将通过克隆来复制这个裸仓库。为此,请在你的 Apache 服务器上创建一个/git目录(或你备份的任何服务器),就像我们之前做的那样。然后,cd进入该目录,并使用以下命令克隆你的仓库:
git clone 192.168.1.101:/git/apache2
在该命令中,将 IP 地址替换为你的 Git 服务器的 IP 地址或其主机名,如果你为其创建了 DNS 条目。你应该会看到如下输出,警告我们已经克隆了一个空的仓库:
warning: You appear to have cloned an empty repository
这没关系,我们实际上还没有向仓库中添加任何内容。如果你cd进入我们刚才克隆的目录并列出其存储内容,你会发现它是一个空目录。如果使用ls -a来查看隐藏目录,你会看到里面有一个.git目录。在.git目录内,我们将有 Git 配置项,这些配置项使得这个仓库能够正常工作。例如,.git目录中的配置文件包含有关远程服务器位置的信息。我们不会操作这个目录;我只是想给你一个简要的概述,说明它的用途。
请注意,如果你删除了克隆仓库中的.git目录,那么这基本上就会移除版本控制,使该目录变成一个普通目录。
不管怎样,我们继续。我们应该先对当前的/etc/apache2目录进行备份,以防在将其转为版本控制时出现错误:
sudo cp -rp /etc/apache2 /etc/apache2.bak
然后,我们可以将/etc/apache2目录的所有内容移入我们的仓库:
sudo mv /etc/apache2/* /git/apache2
/etc/apache2目录现在是空的。此时要小心不要重新启动 Apache;它无法看到配置文件,会启动失败。请删除(现在已经空的)/etc/apache2目录:
sudo rm /etc/apache2
现在,让我们确保 Apache 的文件由root用户拥有。但问题是,如果我们像平常一样使用chown命令来更改所有权,我们也会将.git目录的所有权更改为root。我们不希望这样,因为负责推送更改的用户应该是.git文件夹的所有者。以下命令将会把文件的所有权更改为root,但不会影响像.git这样的隐藏目录:
sudo find /git/apache2 -name '.?*' -prune -o -exec chown root:root {} +
当你现在列出你的代码库目录的内容时,你应该看到所有的文件都被root拥有,除了.git目录,它应该被你的管理用户账户所拥有。
接下来,创建一个符号链接到你的 Git 仓库,这样apache2守护程序就能找到它:
sudo ln -s /git/apache2 /etc/apache2
在这一点上,你应该能看到一个位于/etc/apache2的 Apache 符号链接。如果你列出/etc的内容并使用apache2作为关键词搜索,你应该能看到它作为一个符号链接:
ls -l /etc | grep apache2
目录列表看起来应该类似于以下内容:
lrwxrwxrwx 1 root root 37 2020-06-25 20:59 apache2 -> /git/apache2
如果重新加载 Apache,不应该有任何变化,它应该能够找到与之前相同的配置文件,因为它在/etc目录中的目录映射到/git/apache2,其中包括之前相同的文件:
sudo systemctl reload apache2
如果你没有看到任何错误,一切应该都设置好了。否则,请确保你正确创建了符号链接。
接下来,我们来看看主要内容。我们已经将 Apache 的文件复制到我们的代码库中,但尚未将这些更改推送回 Git 服务器。为了完成这一步,我们需要将位于我们的/git/apache2目录内的文件关联到版本控制中。这么做的原因是,文件只放在git仓库文件夹中是不足以让 Git 关心它们的。我们必须告诉 Git 关注这些单独的文件。我们可以通过在该目录内输入以下命令来将 Apache 的每个文件添加到我们的 Git 仓库中:
git add .
这基本上告诉 Git 将目录中的所有内容添加到版本控制中。你实际上可以这样做来添加一个单独的文件:
git add <filename>
在这种情况下,我们想要添加所有内容,所以我们使用一个句点来代替目录名称,以添加当前整个目录。
如果你在 Git 仓库内运行git status命令,你应该会看到输出,指示 Git 有尚未提交的新文件。一个Git 提交简单地在本地完成变更。基本上,它打包了你当前的变更以准备复制到服务器。要创建到目前为止添加的所有文件的提交,cd进入你的/git/apache2目录,并运行以下内容以准备一个新的提交:
git commit -a -m "My first commit."
使用这个命令,-a选项告诉 Git 你想要包括所有已经变更的内容到你的仓库中。-m选项允许你附加一个提交消息,实际上这是必须的。如果你不使用-m选项,它会打开你的默认文本编辑器,并允许你从那里添加评论。
最后,我们可以将我们的更改push回 Git 服务器:
git push origin master
默认情况下,git命令套件使用 OpenSSH,因此我们的git push命令应该创建一个 SSH 连接返回我们的服务器并将文件推送到那里。你将无法检查 Git 服务器上的 Git 目录的内容,因为它不会包含与原始目录相同的文件结构。不过,每当你拉取一个 Git 仓库时,结果的目录结构将与你离开它时一样。
从此之后,如果你需要将一个仓库恢复到另一台服务器上,你所需要做的就是执行 Git 克隆。要将仓库克隆到当前工作目录中,执行以下命令:
git clone 192.168.1.101:/git/apache2
现在,每次你修改配置文件时,你可以执行 git commit,然后将更改推送到服务器上,以确保内容的安全:
git commit -a -m "Updated config files."git push origin master
现在我们知道了如何创建仓库、将更改推送到服务器并拉取更改。最后,我们还需要了解如何在配置被修改并出现无效文件时恢复更改。首先,我们需要定位一个已知的正常工作提交。我最喜欢的方法是使用 tig 命令。为了使其工作,必须安装 tig 包,但它是一个非常实用的工具:
sudo apt install tig
tig 命令(其实是 git 的逆向命令)为我们提供了一个半图形化的界面,以便浏览我们的 Git 提交记录。要使用它,只需在 Git 仓库中执行 tig 命令。在以下示例截图中,我已经在我的一台服务器上的 Git 仓库中执行了 tig:

图 23.1:使用 tig 命令查看一个示例脚本仓库的示例
在使用 tig 时,你会看到 Git 提交的列表,以及每个提交的日期和注释。要查看其中的一个,按 上箭头 和 下箭头 来更改选择,然后按 Enter 键查看你想查看的提交。你会看到一个新窗口,显示 commit hash(这是一个由字母和数字组成的长字符串),以及该提交中哪些文件的行被添加或删除。要恢复某个提交,你首先需要找到你想恢复的提交,并获取其提交哈希值。tig 命令非常适合用来查找这些信息。在大多数情况下,你需要恢复的提交是发生变更之前的那个。在我的示例截图中,我在 2020 年 9 月 26 日修复了语法问题。如果我想恢复那个文件,我应该恢复到它下面的那个提交。我可以通过高亮那个条目并按 Enter 键来获取提交哈希。它会显示在窗口的顶部。然后,我可以按 q 键退出 tig,然后恢复到该提交:
git checkout <commit hash>
就这样,整个仓库的目录树会立刻恢复到坏提交发生之前的状态。我可以重新启动或重新加载该仓库的守护进程,它就会恢复正常。
此时,你需要测试应用程序,确保问题已经完全修复。在一些时间过去并完成测试后,你可以将更改永久化。首先,我们切换回最新的提交:
git checkout main
然后,我们永久地切换回被认为正常工作的提交:
git revert --no-commit <commit hash>
然后,我们可以提交我们恢复的 Git 仓库,并将其推送回服务器:
git commit -a -m "The previous commit broke the application. Reverting."git push origin master
如你所见,Git 是在管理服务器上的配置文件时非常有用的工具。这对灾难恢复有益,因为如果进行了不好的更改导致守护进程崩溃,你可以轻松地回退更改。如果服务器出现故障,你可以通过再次克隆仓库几乎立即重建配置。当然,Git 远不止我们在本节中所讨论的内容,如果你希望将知识提升到更高的水平,可以考虑阅读一些相关书籍。不过,在管理配置时,使用 Git 你需要了解的只是如何将文件加入版本控制、更新文件以及将它们克隆到新服务器。有些服务可能不适合通过 Git 来管理。例如,通过 Git 管理整个 MariaDB 数据库将是一场噩梦,因为这样的用例开销太大,并且数据库条目可能变化太快,Git 无法跟上。使用你的判断力。如果你有一些配置文件,它们只偶尔会被修改,那么它们将是 Git 的理想管理对象。
备份是一些人往往在为时已晚时才意识到重要性的事情。数据丢失可能对组织造成灾难性影响,因此实施一个可靠的备份计划至关重要。在下一节中,我们将探讨这个计划包括哪些内容。
实施备份计划
创建一个可靠的备份计划是你作为服务器管理员所做的最重要的事情之一。即使你仅在家里使用 Ubuntu Server 作为个人文件服务器,备份也是至关重要的。在我的职业生涯中,我见过许多硬盘故障。我经常听到关于哪个硬盘厂商在耐用性方面优于其他厂商的争论,但我见过如此多的硬盘故障,已不再相信任何品牌。所有硬盘最终都会发生故障,这只是时间问题。当它们故障时,通常会非常严重,几乎没有简单的方法从中恢复数据。管理数据的明智方法是,任何硬盘或服务器都可能会失败,但这并不重要,因为你可以从其他来源(如备份或次要服务器)恢复数据。
没有一种最佳的备份解决方案,因为这完全取决于你需要保护的数据类型,以及你可用的软件和硬件资源。例如,如果你管理的是对公司至关重要的数据库,你应该定期进行备份。如果你有另一台服务器可用,可以设置一个复制的次要服务器,以确保你的主数据库不是单点故障。并不是每个人都有备用的服务器,所以有时候你必须利用现有的资源。
这可能意味着你需要做出一些妥协,例如定期创建数据库服务器存储卷的快照,或者定期将重要数据库的备份导出到外部存储设备。
rsync工具是服务器管理员手中最有价值的软件之一。它使我们能够做一些非常棒的事情。在某些情况下,它甚至能为我们节省不少钱。例如,在线备份解决方案的优势在于,我们可以使用它们将重要文件的副本存储在异地。然而,根据数据量的不同,这些解决方案可能相当昂贵。使用rsync,我们可以以类似的方式备份数据,不仅能将当前文件复制到备份目标,还能进行增量备份。如果我们有另一台服务器可以接收备份,那就更好了。
我曾管理过一家公司,他们不想订阅在线备份解决方案。为了解决这个问题,我们将一台服务器设置为rsync的备份点。我们设置rsync将备份发送到一台包含大量文件的次级服务器。完成初始备份后,这台次级服务器被送到了我们其他州的一个办公室。从那时起,我们每周只需要运行rsync,备份自上次备份以来所有发生变化的文件。通过互联网将文件通过rsync发送到另一个站点的速度相当慢,但由于初始备份在服务器送到那里之前已经完成,因此每周备份的仅仅是增量数据。这不仅展示了rsync的强大功能以及我们如何配置它来完成与付费解决方案相似的工作,同时也展示了如何充分利用现有资源的经验。
由于我们已经在第十二章,共享与传输文件中讲解过rsync,所以在这里不会再重复太多的内容。但既然我们在讨论备份,还是值得再次提到--backup-dir选项。这个选项允许你将通常会被替换的文件复制到另一个位置。作为示例,以下是我在第十二章,共享与传输文件中提到的rsync命令:
CURDATE=$(date +%m-%d-%Y)
export $CURDATE
sudo rsync -avb --delete --backup-dir=/backup/incremental/$CURDATE /src /target
这个命令是创建rsync备份脚本主题的一部分。第一个命令只是捕获今天的日期,并将其存储在一个名为$CURDATE的变量中。在实际的rsync命令中,我们会引用这个变量。-b选项(-avb选项字符串的一部分)告诉rsync对任何通常会被替换的文件进行备份。如果rsync要用新版本替换目标文件,它将在覆盖之前将原始文件重命名并移动。--backup-dir选项告诉rsync,当它即将覆盖文件时,要将文件放到其他地方,而不是复制到新名称。在这个选项中,我们提供一个路径,指定通常会被替换的文件应该复制到哪里。在这种情况下,备份目录包含了$CURDATE变量,这样每天的备份目录都会不同。例如,如果在 2022 年 8 月 16 日执行备份,使用我提供的命令作为示例,备份目录的路径将如下所示:
/backup/incremental/8-16-2022
这本质上允许你保留差异。/src中的文件仍然会被复制到/target,但你指定的--backup-dir目录将包含在当天被替换前的原始文件。
在我的服务器上,我经常使用rsync的--backup-dir选项。我通常会设置一个外部备份驱动器,并创建以下三个文件夹:
-
current -
archive -
logs
current目录始终包含我服务器上文件的当前快照。我备份磁盘上的archive目录是我指定--backup-dir选项的位置。在该目录下,将有以备份日期命名的文件夹。logs目录包含来自备份的日志文件。基本上,我会将rsync命令的输出重定向到该目录中的日志文件,每个日志文件都以相同的$CURDATE变量命名,因此每次备份运行时我也会有一个备份日志。我可以轻松查看任何日志,以了解在那次备份中哪些文件被修改,然后遍历归档文件夹以找到文件的原始副本。我发现这种方法非常有效。当然,备份是通过多个备份磁盘执行的,每周轮换一次,并且总有一个备份是异地存储的。始终保持异地备份是至关重要的,以防发生可能危及本地站点的情况。
rsync工具是你可以利用的众多工具之一,用来创建你自己的备份方案。你制定的计划将主要取决于你想要保护的数据类型以及你愿意承受的停机时间。
理想情况下,我们会有一个完整的备用站点,里面的服务器是我们生产服务器的完整复制品,一旦出现问题可以立即投入生产,但这也是非常昂贵的,是否能够实现这样的常规将取决于你的预算。然而,Ubuntu 提供了许多出色的工具,你可以利用它们来制定适合自己的系统。如果没有其他选择,至少可以利用rsync的强大功能,将备份存储到外部磁盘和/或外部站点。
在恢复物理服务器时,USB 恢复介质是一个非常有价值的工具,例如将可启动的 ISO 镜像写入的闪存驱动器。在下一节中,我们将对此进行详细探讨。
使用可启动恢复介质
实时介质的概念非常棒,因为我们可以从安装在设备上的操作系统启动到一个完全不同的工作环境,并在不干扰主机系统上已安装软件的情况下执行任务。例如,Ubuntu 的桌面版本提供了一个完整的计算环境,我们可以在其中不仅测试硬件和排除故障,还可以像在安装系统上一样浏览网页。在灾难恢复方面,实时介质成为了救命稻草。
作为管理员,我们会遇到一个接一个的问题。这也为我们的职业安全提供了保障。计算机常常在最不经意的时候发生故障(而且似乎每个假期都会发生)。我们的服务器和台式机可能随时会出现故障,而直播媒体让我们能够通过从已知的正常工作环境进行故障排除,将硬件问题与软件问题区分开来。
在直播媒体方面,Ubuntu 的桌面版本是我最喜欢的之一。虽然它主要面向希望在笔记本或台式机上安装 Ubuntu 的终端用户,但作为管理员,我们可以利用它启动那些通常无法启动的计算机,甚至从故障磁盘中恢复数据。例如,我曾使用 Ubuntu 直播媒体从失败的 Windows 和 Linux 系统中恢复数据,方法是通过直播媒体启动计算机,并利用网络连接将数据从故障计算机转移到网络共享。通常,当计算机或服务器无法启动时,其磁盘上的数据仍然可以访问。假设在安装时磁盘没有被加密,你应该可以通过使用像 Ubuntu 直播媒体这样的直播媒体访问服务器或工作站上的数据。
有时,某些级别的故障需要我们使用不同的工具。虽然 Ubuntu 的直播媒体非常棒,但它并不适用于所有情况。一种情况是磁盘故障。通常,你可以使用 Ubuntu 的直播媒体从故障磁盘中恢复数据,但如果磁盘问题过于严重,Ubuntu 媒体也会难以从中访问数据。像 Clonezilla 这样的工具专门用于处理硬盘,可能是更好的选择。
直播媒体完全可以救场。特别是 Ubuntu 直播镜像,它是一个很好的启动磁盘,可以随时使用,因为它为你提供了一个非常广泛的环境,可以用来进行系统故障排除和数据恢复。
使用 Ubuntu 直播镜像的最佳方面之一是,你根本不需要处理底层操作系统和软件集。你可以通过启动已知正常工作的桌面跳过这两者,然后将任何重要文件从驱动器直接复制到网络共享。Ubuntu 直播媒体的另一个重要功能是内存测试选项。计算机上经常出现奇怪故障时,可以追溯到内存故障。除了让你安装 Ubuntu 之外,直播媒体是一个集多种工具于一体的瑞士军刀,你可以利用这些工具从灾难中恢复系统。即使没有其他功能,你也可以使用直播媒体来判断问题是软件相关还是硬件相关。如果问题只能在已安装的环境中重现,而在直播会话中没有重现,很可能是配置问题导致的。如果系统在直播环境中也出现异常,它可能帮助你识别硬件问题。不管怎样,每个优秀的管理员都应该拥有直播媒体,以便在需要时进行故障排除和数据恢复。
概述
在本章中,我们探讨了几种可以防止和恢复灾难的方法。制定一个合理的预防和恢复计划是高效管理服务器的关键。我们需要确保在服务器发生故障时,能够准备好我们最重要数据的备份,同时还应该保留我们最重要配置的备份。理想情况下,我们总是会有一个热备站点,预配置好的服务器可以在主要服务器发生故障时启用,但开源软件的一个好处是我们可以使用众多工具来创建合理的恢复计划。在本章中,我们探讨了如何利用 rsync 作为一种有用的工具来创建差异备份,我们还研究了如何设置一个 Git 服务器,用于配置管理,这也是任何合理的预防计划的关键部分。我们还讨论了在诊断问题时,实时介质的重要性。
随着这一章的结束,本书也画上了句号。编写这本书是一次非常愉快的经历。当 Ubuntu 16.04 还在开发时,我激动地写下了第一版,编写第二版并更新它以涵盖 Ubuntu 18.04 是一项有趣的项目,第三版则涵盖了 20.04,而我更高兴的是能够再次更新这本书,针对 22.04 发布了最新版本。我想感谢每一位读者,感谢你们抽出时间阅读本书。此外,我还要感谢我 YouTube 频道的所有观众,Learn Linux TV (www.learnlinux.tv),因为如果没有我的观众帮助使我的频道变得如此受欢迎,我可能根本没有机会写下这本书。
我还要感谢 Packt 出版社给我机会编写一本关于我最喜欢的技术之一的书。写这本书真的是一种荣幸。当我在 2002 年第一次接触 Linux 时,我从未想过自己会成为一名作者,教授下一代 Linux 管理员行业的技巧。我祝愿你们每个人都好运,并希望这本书对你和你的职业有所帮助。
要获取更多内容,务必访问 learnlinux.tv,这里有更多的内容。我有许多免费的培训视频可供观看。
进一步阅读
-
Pro Git 由 Scott Chacon 和 Ben Straub 编写:
learnlinux.link/git-book -
RAID 术语和概念简介:
learnlinux.link/raid-concepts
加入我们社区的 Discord 讨论区
加入我们社区的 Discord 空间,与作者和其他读者一起讨论:


订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及帮助你规划个人发展和推进职业生涯的行业领先工具。更多信息,请访问我们的网站。
第二十四章:为什么订阅?
-
使用来自 4,000 多位行业专业人士的实用电子书和视频,减少学习时间,增加编程时间
-
提高你的学习效率,利用专门为你打造的技能计划
-
每月获得一本免费电子书或视频
-
完全可搜索,便于快速访问重要信息
-
复制粘贴、打印和收藏内容
在 www.packt.com,你还可以阅读一系列免费的技术文章,注册各种免费的新闻通讯,并获得 Packt 图书和电子书的独家折扣和优惠。
你可能喜欢的其他书籍
如果你喜欢这本书,你可能会对 Packt 的其他书籍感兴趣:
Linux 管理最佳实践
Scott Alan Miller
ISBN: 9781800568792
-
了解如何构思系统管理员角色
-
了解风险评估在管理中的关键价值
-
将技术技能应用于 IT 业务环境
-
探索与 Linux 特定系统技术相关的最佳实践
-
理解系统管理最佳实践背后的逻辑
-
发展从重启到备份再到灾难恢复的创新思维
-
优先排序、分类和规划灾难及恢复
-
探索管理职责背后的心理学
Linux 命令行和 Shell 脚本技巧
Vedran Dakic
Jasmin Redzepagic
ISBN: 9781800205192
-
了解命令行、文本编辑器和 Shell 脚本的入门知识
-
专注于正则表达式、文件处理和自动化复杂任务
-
自动化常见的管理任务
-
精通网络和系统安全脚本
-
掌握代码库管理和基于网络的文件同步
-
使用循环、参数、函数和数组进行任务自动化
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并立即申请。我们与成千上万的开发人员和技术专家合作,帮助他们与全球技术社区分享他们的见解。你可以提交一般申请,申请我们正在招聘作者的特定热门话题,或者提交你自己的想法。
分享你的想法
现在你已经完成了《精通 Ubuntu 服务器》第四版,我们很想听听你的想法!如果你是从 Amazon 购买的这本书,请点击这里直接前往 Amazon 评论页面并分享你的反馈,或在你购买书籍的网站上留下评论。
你的评论对我们和技术社区都非常重要,将帮助我们确保提供优质的内容。


浙公网安备 33010602011771号