原文:
annas-archive.org/md5/55f0ee1b5d0f6f58bdd7da1ffd9f7954译者:飞龙
第五章:管理 Linux 中的服务
在本章中,我们将深入解释服务(作为守护进程在后台运行的程序)。我们将解释 init 脚本和 systemd 单元。我们还将介绍管理服务的 Alpine Linux rc 命令。
本章内容包括以下主题:
-
详细了解 Linux 服务
-
关于 Upstart 的简要介绍,作为一种替代方案
技术要求
本章内容,你需要一台可以执行特权命令的 Linux 系统,可以使用 sudo 或直接跳到 root 账户(尽管我们特别推荐使用前者)。你还需要一款 Linux 文本编辑器,能够生成纯文本文件。如果你打算在 Windows 系统上编辑,请使用支持保存 Unix 文件的文本编辑器。我们推荐在命令行中使用你喜欢的命令行文本编辑器进行编辑:vim、emacs、joe、nano,或者任何你习惯使用的编辑器。
详细了解 Linux 服务
除非你在桌面上运行某种低级别的嵌入式设备——我们强烈怀疑这一点——否则你的操作系统会管理大量任务,为你创建一个舒适且高效的环境。无论是 Mac OS X、Linux、Windows 还是 FreeBSD,它们都运行许多后台程序,这些程序共同提供一个有用的系统。服务器版本的操作系统也是如此。后台程序或后台进程(在 Unix 和 Linux 中称为“守护进程”)是指不与任何输入(键盘、鼠标等)或输出(显示器、终端等)连接的程序。这样,它们即使在没有人登录系统时也能开始工作,并且在用户注销时继续工作。它们还可以在一个永远无法登录系统的用户的权限下运行,从而使它们的执行更加安全。
你的 Linux 系统上运行的服务数量,很大程度上取决于发行版,甚至更大程度上取决于系统的用途。
Linux 服务管理的历史
正如你可以想象的那样,管理系统服务——即那些让你的计算机能够使用的程序——是一项复杂的任务。运行这些服务的软件必须稳定且强大。这一点,以及系统启动不频繁,尤其是在服务器上,促使了 Linux 采用的解决方案得以持续数十年。随着许多线程 CPU 的出现并广泛使用,启动服务的需求日益增加,且更需要智能化管理,促成了新 init 系统的多个实现,我们将在接下来的部分中介绍。
systemd
systemd是一个用于 Linux 的服务管理器,能够管理与操作系统一起启动的服务。它取代了传统的init脚本。它负责启动和停止系统服务,管理系统状态,以及记录系统事件。它已经成为许多流行 Linux 发行版的默认init系统,包括 CentOS、Fedora Linux、红帽企业 Linux(RHEL)和 Ubuntu Linux。
这个服务管理器负责控制系统本身的初始化(Linux 操作系统所需的服务)、启动和停止系统服务,以及管理系统资源。它提供了另一种管理服务和其他系统组件的方式,并且允许系统管理员以比init更标准化的方式配置和自定义系统行为。
systemd的一个关键特性是能够并行启动服务,这可以显著减少系统的启动时间。它还包括一些用于管理和监控系统服务的工具。
另外,systemd受到赞扬的另一点是它最终为 Linux 世界带来的服务配置统一性。在每个 Linux 发行版中,systemd配置文件都被送到相同的路径,并且看起来相同。然而,根据二进制文件的安装路径,仍然存在一些差异。systemd还更擅长判断进程是否正在运行,这使得我们更不容易遇到因为过时的文件无法启动进程的情况。
systemd的一个主要优势是它对依赖关系的意识。服务(在systemd控制下的运行程序)配置包含了关于它依赖的所有其他服务的信息,还可以指向依赖于它的服务。而且,服务可以向systemd告知它需要运行的目标:如果您的服务需要网络运行,您可以将此信息写入配置中,systemd会确保只有在网络正确配置后才会启动您的守护进程。
以下是作为systemd一部分提供的一些工具和实用程序的列表:
-
systemd:这是主要的系统和服务管理器。它是控制服务和其他系统组件初始化和管理的主要程序。 -
systemctl:这是一个命令行工具,用于管理系统服务和其他系统组件。它可以用于启动、停止、重启、启用和禁用服务,还可以查看服务和其他系统组件的状态。 -
journalctl:用于查看和操作系统日志,日志由systemd管理。它可以用来查看日志消息,根据各种标准过滤日志消息,并将日志数据导出到文件。 -
coredumpctl:这是一个实用工具,顾名思义,它有助于从systemd的日志中检索核心转储。 -
systemd-analyze:这个工具可以用来分析系统的启动性能。它衡量系统启动所需的时间,以及识别潜在的瓶颈和性能问题的时间。 -
systemd-cgls:这是一个用于查看系统上控制组层级的命令行工具。systemd用于管理系统资源并将进程相互隔离。 -
systemd-delta:这是一个用于分析systemd提供的默认配置文件与对这些文件所做的本地修改之间差异的命令行工具。 -
systemd-detect-virt:这是一个用于检测系统运行的虚拟化环境的命令行工具。它可以用来判断系统是运行在虚拟机(VM)、容器中,还是裸机上。 -
systemd-inhibit:这是一个命令行工具,用于防止执行某些系统操作,例如暂停或关闭系统。 -
systemd-nspawn:这是一个用于在轻量级容器中运行进程的命令行工具。它可以用来创建和管理容器,以及在容器内执行进程。
这只是 systemd 提供的一些常见工具和实用程序的列表。还有很多其他工具,但我们在这里不再讨论它们。
目标
在 systemd 中,目标 是系统可以处于的特定状态,并通过一个符号名称表示。目标用于定义系统的高级行为,通常用于将一组相关的服务和其他系统组件组合在一起。
例如,multi-user.target 是一个表示已准备好提供多用户访问的系统,启用了网络和其他服务;graphical.target 是一个表示已准备好显示图形登录界面的系统,启用了图形桌面环境和相关服务。
目标通常在单元文件中定义,这些文件是描述系统组件属性和行为的配置文件。当目标被激活时,systemd 将启动所有与该目标相关的服务和其他系统组件。
systemd 包括许多预定义的目标,涵盖了广泛的常见系统状态,管理员还可以定义自定义目标以满足其系统的特定需求。目标可以通过 systemctl 命令激活,或者通过修改系统启动时设置的默认目标来激活。
下面是一些预定义的 systemd 目标的示例:
-
poweroff.target:表示一个正在关机或已关闭的系统。 -
rescue.target:表示一个处于救援模式的系统,启用了最小化的服务。 -
multi-user.target:表示一个已经准备好提供多用户访问、启用网络和其他服务的系统。 -
graphical.target:表示一个已经准备好显示图形登录屏幕的系统,启用了图形桌面环境和相关服务。 -
reboot.target:表示一个正在重启的系统。 -
emergency.target:表示一个运行在紧急模式下的系统,仅启用最基本的服务。
这是一个定义自定义目标的systemd单元文件示例:
[Unit]
Description=Unit File With Custom Target
[Install]
WantedBy=multi-user.target
这个单元文件定义了一个名为Custom Target的目标,旨在作为multi-user.target的一部分被激活。WantedBy指令指定,当multi-user.target被激活时,目标应该被激活。
这是另一个名为custom.target的systemd单元文件示例,它定义了一个自定义目标:
[Unit]
Description=My simple service
[Install]
WantedBy=multi-user.target
以下是一个使用我们custom.target目标的单元文件:
[Service]
ExecStart=/usr/local/bin/my-simple-service
Type=simple
[Install]
WantedBy=custom.target
这个单元文件定义了一个名为Unit File With Custom Target的目标和一个名为My simple service的服务。ExecStart指令指定了启动服务时应使用的命令,而Type指令指定了服务的类型。服务单元中[Install]部分的WantedBy指令指定,当custom.target被激活时,服务应该被激活。
现在,既然我们已经简单介绍了单元文件,让我们深入了解它们,看看能用它们做些什么。
单元文件
单元文件通常存储在 Linux 操作系统文件系统中的/lib/systemd/系统目录中。此目录中的文件不应以任何方式修改,因为当通过软件包管理器升级服务时,这些文件将会被包中的文件替换。
相反,要修改特定服务的单元文件,请在/etc/systemd/system目录中创建自定义单元文件。此etc目录中的文件将优先于默认位置的文件。
systemd能够使用以下方式提供单元激活:
-
systemd。最简单的情况是,如果您的服务使用了网络,您需要添加一个与multi-user.target或network.target的依赖关系。 -
/etc/systemd/system目录。 -
模板:您还可以定义模板单元文件。这些特殊单元可用于创建同一通用单元的多个实例。
-
private/tmp或网络访问,并限制内核功能。 -
路径:您可以基于文件系统中某个文件或目录的活动或可用性来启动一个单元。
-
套接字:这是 Linux 操作系统中的一种特殊类型的文件,它允许两个进程之间进行通信。通过使用此功能,您可以延迟启动服务,直到相关的套接字被访问。您还可以创建一个单元文件,在启动过程的早期仅创建一个套接字,并创建一个单独的单元文件来使用此套接字。
-
总线:您可以使用 D-Bus 提供的总线接口来激活单元。D-Bus 只是一个用于进程间通信(IPC)的消息总线,最常用于 GNOME 或 KDE 等图形用户界面中。
-
dev文件,位于/dev目录下)。这将利用一种被称为udev的机制,udev是一个 Linux 子系统,用于提供设备事件。
一旦你启动了一个服务,你可能希望通过查看日志文件来检查它是否正常运行。这项工作由 journald 完成。
日志记录
每个由 systemd 管理的服务都会将其日志发送到 journald——systemd 的一个特殊部分。管理这些日志有一个专用的命令行工具:journalctl。在最简单的形式下,运行 journalctl 命令会输出所有系统日志,最新的日志会显示在最上面。虽然日志的格式类似于 syslog——这是 Linux 上用于收集日志的传统工具——但 journald 捕获了更多数据。它收集了启动过程、内核日志等信息。
启动日志默认是临时的。这意味着它们不会在系统重启之间保存。然而,也可以将它们永久记录下来。下面是两种方法:
-
创建一个特殊的目录。当
journald在系统启动时检测到该目录时,它会将日志保存在该目录中:sudo mkdir -p /var/log/journal。 -
编辑
journald配置文件并启用持久化启动日志。使用你喜欢的编辑器打开/etc/systemd/journald.conf文件,在[Journal]部分,将Storage选项修改为如下所示:[Journal]Storage=persistent
journald 可以通过使用 -u service.name 选项按服务过滤日志——也就是说,journalctl -u httpd.service 只会打印来自 httpd 守护进程的日志。你也可以通过 man journalctl 命令来查看如何在指定的时间范围内打印日志,或从多个服务中打印日志。
在本节中,我们介绍了 Linux 世界中最常用的服务软件——systemd。在下一节中,我们将探讨 OpenRC——一种用于 Alpine Linux 的系统,Alpine Linux 是云端容器首选的 Linux 发行版。
OpenRC
Alpine Linux 使用另一种用于管理系统服务的系统——init 系统,最初为 Gentoo Linux 开发。它旨在轻量、简单且易于维护。OpenRC 使用纯文本配置文件,使其易于定制和配置。它也很容易通过自定义脚本和程序进行扩展。OpenRC 灵活,可以在各种系统上使用,从嵌入式设备到服务器。
以下是 OpenRC 在 Alpine Linux 中的使用示例:
-
ssh和cron。你可以使用rc-service命令来启动、停止或检查服务的状态。例如,要启动ssh服务,可以运行rc-servicessh start。 -
自定义系统初始化和关机:OpenRC 允许你编写自定义脚本来定制系统在启动或关机过程中的行为。这些脚本会在启动过程中的特定点执行,用于设置自定义配置或执行其他任务。
-
rc-update命令用于将服务添加或移除到不同的运行级别。例如,要使服务在启动时启动,可以运行rc-update add <service> boot。
要启动服务,请使用以下rc-service命令:
admin@myhome:~$ rc-service <service> start
要停止服务,请使用以下rc-service命令:
admin@myhome:~$ rc-service <service> stop
要检查服务的状态,请使用以下rc-service命令:
admin@myhome:~$ rc-service <service> status
要启用服务在启动时启动,请使用以下rc-update命令:
admin@myhome:~$ rc-update add <service> default
要禁止服务在启动时启动,请使用以下rc-update命令:
admin@myhome:~$ rc-update del <service> default
在此上下文中,默认指的是 Alpine Linux 系统的默认运行级别。运行级别通常用于定义系统的行为。大多数 Linux 发行版都有几个预定义的运行级别,每个运行级别对应一组特定的服务,这些服务会被启动或停止。
在 Alpine Linux 中,以下是默认的运行级别:
-
default:这是默认的运行级别,用于系统正常启动时。此运行级别中启动的服务包括网络、SSH 和系统日志。 -
boot:此运行级别在系统启动时使用。此运行级别中启动的服务包括系统控制台、系统时钟和内核。 -
single:此运行级别在系统启动到单用户模式时使用。此运行级别中启动的服务仅包含一组最小服务,包括系统控制台和系统时钟。 -
shutdown:此运行级别在系统关闭时使用。此运行级别中停止的服务包括网络、SSH 和系统日志。
OpenRC 使用与我们在本章前面提到的 SysV init非常相似的方式来定义服务操作。像start、stop、restart和status这样的命令是在 Bash 脚本中定义的。以下是一个基本的服务示例:
# Name of the service
name="exampleservice"
# Description of the service
description="This is my example service"
# Start command
start() {
# Add your start commands
}
# Stop command
stop() {
# Add your stop command here
}
# Restart command
restart() {
stop
start
}
要创建一个新的服务,可以将此文件复制到新文件并根据需要修改name、description、start、stop和restart函数。start函数应包含启动服务的命令,stop函数应包含停止服务的命令,restart函数应停止并重新启动服务。它们可以与 SysV init中的相同。
在 OpenRC 中,init脚本通常存储在/etc/init.d目录中。这些脚本用于启动和停止服务以及管理系统的运行级别。
要为 OpenRC 创建一个新的init脚本,可以在/etc/init.d目录中创建一个新文件并使其可执行。
创建init脚本后,可以使用rc-update命令将其添加到默认运行级别,这将导致服务在启动时启动。例如,要将exampleservice服务添加到默认运行级别,可以运行以下命令:
admin@myhome:~$ rc-update add exampleservice default
在大多数情况下,我们将在 Docker 环境中使用 Alpine Linux,在这种环境下 OpenRC 的使用不多,但它仍然对某些边缘案例的使用有所帮助。我们将在第八章和第九章中更详细地讨论 Docker。
在本节中,我们已经介绍了 OpenRC,它是管理 Alpine Linux 中系统服务的软件。在下一节中,我们将简要介绍一种过时的 SysV init形式,这种形式可能出现在较老或最小化的 Linux 发行版中。
SysV init
如前所述,init进程是系统中最重要的、持续运行的进程。它负责在系统启动时或管理员请求时启动系统服务,在系统关机时或请求时停止系统服务,且按正确的顺序执行。它还负责在请求时重新启动服务。由于init将代表 root 用户执行代码,因此必须确保其经过充分的稳定性和安全性测试。
旧的init系统的一个迷人特点是它的简单性。服务的启动、停止和重启是通过脚本来管理的——这些脚本必须由应用程序作者、该应用程序的发行版包所有者或系统管理员编写——如果该服务不是通过包安装的。脚本简单且易于理解、编写和调试。
然而,随着软件复杂性的增加,旧的init系统的局限性变得越来越明显。启动一个简单的服务还可以,但启动由多个程序组成的应用变得更加困难,尤其是当它们之间的依赖关系变得更加重要时。init缺乏对它所管理的服务启动依赖关系的观察。
旧的init系统越来越不适合现代系统的另一个原因是串行启动:它无法并行启动服务,从而抵消了现代多核 CPU 的优势。是时候寻找一个更适合新时代的系统了。
一个典型的init系统由以下几个组件组成:
-
一个包含启动/停止脚本的
/etc/init.d或/etc/rc.d/init.d目录。 -
一个
/etc/inittab文件,用于定义运行级别并设置默认的运行级别。 -
一个
/etc/rcX.d目录,包含所有应在运行级别X中启动或停止的服务的脚本,其中X是从0(零)到6的数字。我们将在下一段中详细介绍。
/etc/init.d/ 目录包含用于启动、停止和重启服务的 Shell 脚本。该脚本接受一个参数,可以是 start、stop 或 restart。传递给脚本的每个参数都会执行相应的函数(通常函数名称与参数相同:start、stop 或 restart),该函数会执行一系列步骤来正确启动、停止或重启指定的服务。系统最终进入的启动过程的最终状态被称为运行级别。运行级别决定了服务是否正在启动或停止,并且如果是启动服务,哪些服务会被启动。
为了确定要调用的操作类型,会在与相关运行级别相对应的目录中创建一个指向脚本的链接。
假设我们希望系统最终进入运行级别 3。如果我们希望在该运行级别启动我们的服务,我们会创建一个指向 /etc/rc.d/my_service 脚本的链接,该链接指向 /etc/rc3.d/ 目录。链接的名称决定了操作的类型和顺序。因此,如果我们希望服务在 01 到 49 之间的数字之后启动,我们会将其命名为 /etc/rc.3/S50my_service。字母 S 告诉 init 系统启动该服务,数字 50 告诉它在所有较低数字的服务启动之后再启动该服务。请注意,编号更多的是一种框架,并不能保证在 50 之前有所有其他数字的脚本。停止服务也是如此。在确定停止系统的默认运行级别后(通常是 0),会为服务脚本创建一个适当的 symlink。
前述框架的主要问题在于它完全没有考虑依赖关系。确保守护进程所依赖的服务正在运行的唯一方法是将其脚本化,放入 start 函数中。在包含多个服务的复杂应用程序中,这可能会导致更大的启动/停止脚本。
管理员和开发人员提出的另一个与 init 脚本相关的问题是,关于如何编写这些脚本有多个标准,并且有多种不同的工具与 init 脚本相关。基本上,每个主要的 Linux 发行版都有自己编写这些脚本的方式和自己的辅助函数库。我们来看看在 Slackware Linux 和 Debian/GNU Linux 上启动相同的 My Service 服务的 init 脚本。这也是编写 Shell 脚本的入门章节派上用场的地方。
在 Slackware 和 Debian 两种情况下,为了简洁起见,我们将删去一些原始内容,只保留最重要的部分。请不用担心,因为这两种发行版都提供了完全注释的示例脚本。
以下的 init 脚本将在 Slackware Linux 环境中工作。脚本以头部开始,我们在头部声明服务名称和一些重要的路径:
#!/bin/bash
#
# my_service Startup script for the My Service daemon
#
# chkconfig: 2345 99 01
# description: My Service is a custom daemon that performs some important functions.
# Source function library.
. /etc/rc.d/init.d/functions
# Set the service name.
SERVICE_NAME=my_service
# Set the path to the service binary.
SERVICE_BINARY=/usr/local/bin/my_service
# Set the path to the service configuration file.
CONFIG_FILE=/etc/my_service/my_service.conf
# Set the user that the service should run as.
SERVICE_USER=my_service
# Set the process ID file.
PIDFILE=/var/run/my_service.pid
# Set the log file.
LOGFILE=/var/log/my_service.log
好的一点是,Slackware 提供的示例脚本有很好的注释。我们需要声明守护进程的二进制文件路径。我们还需要声明服务将以哪个用户和组身份运行,这实际上决定了它的文件系统权限。
接下来的部分定义了服务的所有重要操作:start、stop 和 restart。通常会有其他操作,但为了简洁我们将其省略:
start() {
echo -n "Starting $SERVICE_NAME: "
# Check if the service is already running.
if [ -f $PIDFILE ]; then
echo "already running"
return 1
fi
# Start the service as the specified user and group.
daemon --user $SERVICE_USER --group $SERVICE_GROUP $SERVICE_BINARY -c $CONFIG_FILE -l $LOGFILE -p $PIDFILE
# Write a lock file to indicate that the service is running.
touch $LOCKFILE
echo "done"
}
start 函数利用 pid 文件检查服务是否正在运行。pid 文件是一个包含服务 PID 的文本文件。它提供关于服务主进程及其状态的信息。然而有一个问题,服务可能已经停止运行,但 pid 文件仍然存在。这会导致 start 函数无法实际启动服务。
在检查到 pid 文件不存在后,这意味着服务没有运行,一个名为 daemon 的特殊工具被用来以用户和组权限启动进程,并指向配置文件、日志文件和 pid 文件的位置。
该函数通过 bash echo 命令来传达它的操作,echo 命令会打印出给定的文本。如果是自动执行,echo 命令的输出将根据日志 daemon 配置记录到系统日志中:
stop() {
echo -n "Stopping $SERVICE_NAME: "
# Check if the service is running.
if [ ! -f $PIDFILE ]; then
echo "not running"
return 1
fi
# Stop the service.
killproc -p $PIDFILE $SERVICE_BINARY
# Remove the lock file.
rm -f $LOCKFILE
# Remove the PID file.
rm -f $PIDFILE
echo "done"
}
同样,stop 函数使用 pid 文件检查服务是否正在运行。没有 pid 文件而服务仍在运行的可能性几乎为零。检查完成后,一个名为 killproc 的特殊命令被用来终止该进程。在函数的最后部分,脚本执行一些清理任务,清除 pid 文件和 lock 文件:
restart() {
stop
start
}
restart 函数非常简单。它重用已经定义的 start 和 stop 函数,准确执行它所说的:重启服务。如果服务配置需要重新加载二进制文件,这通常很有用:
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
*)
echo "Usage: $0 {start|stop|restart|uninstall}"
esac
脚本的最后部分评估我们想要执行的操作——启动、停止或重启服务——并调用相应的函数。如果我们要求它执行一个它不识别的操作,脚本会打印出使用说明。
然而,以下脚本是为 Debian Linux 环境设计的:
#!/bin/bash
#
# my_service Startup script for the My Service daemon
#
# chkconfig: 2345 99 01
# description: My Service is a custom daemon that performs some important functions.
# Source function library.
. /lib/lsb/init-functions
# Set the service name.
SERVICE_NAME=my_service
# Set the path to the service binary.
SERVICE_BINARY=/usr/local/bin/my_service
# Set the path to the service configuration file.
CONFIG_FILE=/etc/my_service/my_service.conf
# Set the user that the service should run as.
SERVICE_USER=my_service
# Set the group that the service should run as.
SERVICE_GROUP=my_service
同样,脚本以一个头部部分开始,定义了稍后将在 start 和 stop 部分使用的路径。通常会有更多的行,但为了简洁,我们将它们省略了:
start() {
log_daemon_msg "Starting $SERVICE_NAME"
# Check if the service is already running.
if [ -f $PIDFILE ]; then
log_failure_msg "$SERVICE_NAME is already running"
log_end_msg 1
return 1
fi
# Start the service as the specified user and group.
start-stop-daemon --start --background --user $SERVICE_USER --group $SERVICE_GROUP --make-pidfile --pidfile $PIDFILE --startas $SERVICE_BINARY -- -c $CONFIG_FILE -l $LOGFILE
# Write a lock file to indicate that the service is running.
touch $LOCKFILE
log_end_msg 0
}
start 函数与 Slackware 版本中的类似。但你会注意到一些微妙的差异,如下所示:
-
这里使用了
start-stop-daemon辅助函数来管理运行中的服务,而不是使用daemon和killproc。 -
这里使用了专门的日志记录函数,而不是简单的 echo:
log_daemon_msg、log_failure_msg和log_end_msg。 -
start-stop-daemon函数接受特殊标志来确定操作(start和stop),并将程序从终端中分离,使其有效地成为系统服务(--background),如下面所示:
stop() {
log_daemon_msg "Stopping $SERVICE_NAME"
# Check if the service is running.
if [ ! -f $PIDFILE ]; then
log_failure_msg "$SERVICE_NAME is not running"
log_end_msg 1
return 1
fi
# Stop the service.
start-stop-daemon --stop --pidfile $PIDFILE
# Remove the lock file.
rm -f $LOCKFILE
# Remove the PID file.
rm -f $PIDFILE
log_end_msg 0
}
stop函数与 Slackware 的stop函数非常相似,具有与start函数相似的差异。
剩下的脚本包含restart函数和任务评估部分并不太有趣,因此我们已将其省略。
正如你可能还记得在关于systemd的章节中提到的,它解决了其中的一些问题。SysV 在现代系统中不常见,因此你通常需要处理的是systemd。
然而,还有另一个替代 SysV init的工具,叫做 Upstart。
关于 Upstart,一种替代方案,简要说明
Upstart 是一个基于事件的替代方案,替代传统的 SysV init系统,用于管理和控制系统上的服务和守护进程。Upstart 在 Ubuntu 6.10及其后续版本中被引入,旨在提高启动时间,简化系统配置,并提供更灵活的系统服务管理方式。现在,它已在大多数 Linux 发行版中被systemd取代。
Upstart 用于管理系统的初始化过程,并启动、停止和监督任务与服务。它设计上比传统的init守护进程更灵活高效,并提供关于任务和服务状态的更多信息。
所有可以由systemd和/或cron管理的内容已成为行业标准,所以如果没有充分的理由使用它们,或者你已经有一个使用 Upstart 的系统,我们不建议你将其作为默认选择。
概述
在本章中,我们讨论了系统服务——即 Unix 和 Linux 世界中的守护进程——以及最常用的管理它们的软件。我们解释了这些是什么,如何检查它们的状态,以及如何控制它们。在下一章中,我们将深入探讨 Linux 网络。
第六章:Linux 中的网络
网络是一个复杂的话题,无论在哪个操作系统上都是如此。就灵活性而言,Linux 在配置、内核功能和命令行工具的众多可能性上可能让人感到非常不知所措,这些工具可以帮助我们配置这些选项。在本章中,我们将为这个话题奠定基础,以便您可以在其他出版物中查找有关特定主题的更多信息。在本章中,我们将涵盖以下主题:
-
Linux 中的网络
-
ISO/OSI 作为网络标准
-
防火墙
-
高级话题
Linux 中的网络
在 Linux 中,网络是通过内核实现的,这意味着它是操作系统的一部分。内核包括几个组件,这些组件协同工作以实现网络功能,包括设备驱动程序、协议实现和系统调用。
当用户想要通过网络发送或接收数据时,他们可以使用 Linux 中任何可用的网络应用程序,如ping、traceroute、telnet或ssh。这些应用程序使用系统调用与内核通信,并请求通过网络发送或接收数据。
内核通过设备驱动程序与网络硬件进行通信,设备驱动程序是软件程序,使得内核能够访问和控制硬件。不同类型的网络硬件需要不同的驱动程序,例如以太网或 Wi-Fi。
内核还实现了几个网络协议,这些协议是定义如何在网络上传输和格式化数据的规则和标准。Linux 中常用的协议包括 TCP、UDP 和 IP(版本4和版本6)。
ISO/OSI 作为网络标准
关于网络的任何讨论总是从国际标准化组织/开放系统互联(ISO/OSI)定义的参考模型开始。ISO/OSI 参考模型是一个概念模型,定义了一个网络框架,用于在七层中实现协议。它是一个框架,让我们可以将系统(计算机或其他)之间的通信与其实际的物理和软件结构区分开来看。
在 Linux 中,OSI 模型是通过一系列软件组件来实现的,这些组件负责执行每一层的功能。这些组件共同作用,实现 Linux 中的网络功能。
在 Linux 中实现的 OSI 模型的七层如下:
-
物理层
-
数据链路
-
网络层
-
传输层
-
会话层
-
表示层
-
应用层
在运行在云端的系统中,您将访问到 Linux 内核中实现的所有层。这些层包括网络层、传输层、会话层、表示层和应用层。为了调试网络连接、检查统计信息并找出任何其他可能的问题,Linux 提供了一个控制台工具。让我们逐一查看每一层,研究我们可以在 Linux 中使用哪些命令行工具来检查它们。
要了解更多关于 OSI 模型的信息,可以参考 osi-model.com/。我们将在接下来的子节中深入探讨这些层并对其进行解释。
物理层
这一层负责通过通信通道传输原始比特,并通过设备驱动程序来实现,这些驱动程序控制网络硬件,例如以太网标准,如 10BASE-T、10BASE2、10BASE5、100BASE-TX、100BASE-FX、100BASE-T、1000BASE-T 等。由于我们将专注于软件实现以及如何在 Linux 控制台上与其交互,因此在这里不会进一步讨论这一层。你可以在网上找到大量关于电缆、硬件设备和网络的信息。
数据链路层 – MAC,VLAN
数据链路层负责在网络上的设备之间提供可靠的链路。它分为逻辑链路控制(LLC)子层和媒体访问控制(MAC)子层。
数据链路层将来自网络层(第 3 层)的原始数据转换为可以通过物理链路传输的格式。它还提供错误检测和校正、流量控制以及 MAC 功能。
LLC 子层为网络层提供一致的接口,不论使用何种类型的物理网络。它还提供流量控制和错误校正服务。
MAC 子层控制对物理网络的访问并提供寻址服务。它使用 MAC 地址,这是分配给网络上每个设备的唯一标识符,确保数据能传送到正确的目的地。
数据链路层还包括使用协议,如以太网、PPP 和帧中继,以在网络上提供设备间的通信。它还提供流量和错误控制机制—例如,它使用循环冗余校验(CRC)进行错误检测,并使用滑动窗口进行流量控制。
有多个 Linux 命令行工具可用于调试数据链路层问题。以下是一些示例:
-
ifconfig:此命令可用于查看网络接口的状态及其相关的 IP 地址、子网掩码和 MAC 地址。它还可以用来配置网络接口,例如设置 IP 地址或启用或禁用接口。 -
ping:此命令可用于测试网络中主机的可达性。它向指定主机发送一个互联网控制消息协议(ICMP)回显请求数据包,并等待回显响应。如果主机响应,则表明主机可达,并且数据链路层工作正常。 -
traceroute、tracepath和mtr:这些命令可以用来追踪数据包从源到目的地的路径。它们还可以用来识别任何可能导致问题的网络跳数或设备。此外,tracepath可以测量mtr,而mtr则提供更多关于网络健康的信息。 -
arp:此命令可用于查看和操作地址解析协议(ARP)缓存。ARP 用于将 IP 地址映射到本地网络上的 MAC 地址。此命令可以用来验证 ARP 缓存中是否有正确的 IP-MAC 地址映射。 -
ethtool:此命令可用于查看和配置以太网接口的高级设置,如链路速度、双工模式和自动协商设置。 -
tcpdump:此命令可用于实时捕获和分析网络数据包。它可以用来排查诸如数据包丢失、延迟包和网络拥堵等问题。
我们将在本章的接下来的部分中深入探讨前述工具,因为大多数工具可以用于同时查看多个 OSI 层。
网络层 – IPv4 和 IPv6
每个公有(互联网)或私有(你的办公室或家里)网络上的设备都有一个唯一地址,用于识别它并与之连接。当你向一个网站发送请求时,你的设备会使用其地址向目标服务器发送消息。服务器随后会使用其地址向你的设备发送消息以响应。这一过程就是设备通过互联网互相通信的方式。我们使用的地址有两种类型:IPv6 和 IPv4。v4(版本4)和v6(版本6)是你可以使用的地址数量。
IPv4 是最广泛使用的 IP 版本,但只有大约 43 亿个唯一地址可用。然而,这些地址不足以支持不断增加的连接到互联网的设备数量。为了解决这个问题,开发了一种新的 IP 版本——IPv6,提供了更大的地址空间。
IPv4 地址是 32 位数字,通常以点分十进制表示,包含四个范围从0到255的八位字节。例如,192.168.0.1或1.2.3.4都是有效的 IPv4 地址。
IPv6 地址是 128 位数字(在十六进制中介于zero和FFFF之间,等于十进制值65535),以十六进制表示,采用八组四个十六进制数字,并以冒号分隔。IPv6 地址的示例为2001:0db8:bad:f00d:0000:dead:beef:7331。
我们在这里主要关注 IPv4,因为它通常更容易理解,但类似的原则也适用于 IPv6,因此稍后我们将在本章中学到的内容更容易重新应用到 IPv6 环境中。
子网、类别和网络掩码
子网 是通过将一个较大的网络划分成更小的网络而创建的网络片段。这么做的原因有很多,包括安全性、组织结构以及高效利用 IP 地址。在公共互联网网络中,每个组织都有自己的一部分网络——一个子网。
当一个网络被划分成子网时,IP 地址中的主机部分(标识网络中具体设备的部分)会被分成两部分:一部分标识子网,另一部分标识子网内的主机 IP 地址。子网掩码是应用于 IP 地址的二进制表示,用于确定 IP 地址中哪一部分标识子网,哪一部分标识主机。
假设一个使用 IP 地址范围 192.168.11.0/24(或 192.168.11.0/255.255.255.0 的十进制形式)的网络。/24 部分表示 24 位网络部分和 8 位主机部分。
这意味着该网络的 IP 地址有 24 位(或地址中的前三个数字)用于 网络 部分,8 位(或最后一个数字)用于 主机 部分。所以,对于这个网络,你将有一个可用的地址范围 192.168.11.0 - 192.168.11.255,其中 192.168.11.0 是网络地址,192.168.11.255 是广播地址。
广播网络地址 是一种特殊类型的 IP 地址,用于向特定网络或子网中的所有主机发送信息。广播地址是网络或子网 IP 地址范围中的最高地址,并与子网掩码一起使用,以标识广播域。当主机将数据包发送到广播地址时,该数据包将被送到同一网络或子网中的所有主机。需要注意的是,广播数据包不会离开当前的子网网络——它们仅在本地网络或子网内起作用。
在标准化 CIDR 之前,IP 地址根据网络需要支持的主机数量被分为不同的类别(A、B 和 C)。这些类别是根据 IP 地址的前导位来定义的,每个类别有不同数量的位用于网络部分和主机部分。目前我们主要使用 CIDR,但一些网络地址仍然存在。例如,10.0.0.0、12.0.0.0 或 15.0.0.0 通常具有 255.0.0.0 或 /8 网络掩码。以下是你可能会遇到的一些其他网络:
-
10.0.0.0/8、12.0.0.0/8和15.0.0.0/8 -
172.16.0.0/16、172.17.0.0/16和172.18.0.0/16 -
192.168.0.0/24、192.168.1.0/24和192.168.2.0/24
这些例子并不是实际的网络,而仅仅是通过查看 IP 地址的前导位和子网掩码来识别网络类别的表示——这并不是一种规则,你可以在你的基础设施中创建更小(或更大)的网络。
你可以在网上找到许多计算器,帮助你更好地理解网络地址是如何工作的。这里有一些例子:
既然我们已经学习了子网、类别和网络掩码,那么接下来让我们进入下一个小节。
网络配置和控制台工具
通过你目前所掌握的知识,你可以轻松地使用一些在现代 Linux 环境中都可用的命令行工具来检查你的 Linux 网络配置。
这里是一些用于网络配置的基本控制台工具:
-
iproute2包,取代了ifconfig和route命令 -
ifconfig -
route -
ip -
netplan
让我们一起了解这些工具的语法以及使用它们时可能实现的功能。
ifconfig
在 Linux 中,通常可用的命令之一是ifconfig。这个工具用于配置网络接口。它可以用来显示接口的状态、为接口分配 IP 地址、设置网络掩码以及设置默认网关。ifconfig(来自 net-tools 包)在近年来被 iproute2 工具集所取代;这个包中最著名的命令是 ip。
以下是 ifconfig 命令的示例输出:
admin@myhome:~$ ifconfig
eth0: flags=4163<UP,BRODCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
RX packets 17433 bytes 26244858 (26.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8968 bytes 488744 (488.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
当没有任何额外选项时,执行 ifconfig 会列出系统中所有可用的接口,并显示一些基本信息,如接口名称(这里是 eth0 和 lo)、网卡的 MAC 地址(设备的物理地址)、网络配置(IP 地址、网络掩码和广播地址)、接收和发送的包,以及其他信息。这将让你一眼查看网络状态。
回环设备(在上面的示例中命名为 lo)是一个虚拟网络接口,用于将网络数据包发送回同一个发送源的主机。它也被称为回环接口,并由 lo 或 lo0 表示。
回环设备的主要作用是为主机提供一种稳定且一致的方式,通过网络栈与自身进行通信,而不需要依赖任何物理网络接口。
回环接口通常用于测试、故障排除和一些系统与应用程序功能,以及进程间通信(IPC),尤其是在同一主机上运行的进程之间。
使用 ifconfig 还可以让你启用或禁用某些接口并配置它们的设置。要使配置保持有效,你需要将其保存到/etc/network/interfaces 文件中:
auto lo
iface lo inet loopback
# eth0 network device
auto eth0
iface eth0 inet static
address 172.17.0.2
netmask 255.255.0.0
gateway 172.17.0.1
dns-nameservers 8.8.8.8 4.4.4.4
auto eth1
allow-hotplug eth1
iface eth1 inet dhcp
在前面的例子中,我们设置了一个自动回环设备,分别为eth0和eth1。eth0接口具有静态网络配置,并将在系统启动时像lo一样进行配置。eth1接口具有动态网络配置,它是通过allow-hotplug配置获取的,这意味着该设备将在 Linux 内核检测到后启动。
需要知道的是,在编辑/etc/network/interfaces文件后,您需要在 Debian Linux 或 Ubuntu Linux 中使用ifup或ifdown工具,或者在 Alpine Linux 中使用ifupdown工具。或者,您可以通过在 Debian Linux、Ubuntu Linux 或 RHEL/CentOS 中使用systemctl restart网络来重启网络。必须在 Alpine Linux 中使用rc-service网络restart命令。
要使用ifconfig手动配置设备,您需要运行以下命令:
admin@myhome:~$ ifconfig eth0 192.168.1.2 netmask 255.255.255.0
admin@myhome:~$ ifconfig eth0 up
这将为eth0接口配置一个192.168.1.2的 IP 地址,网络的子网掩码为255.255.255.0(或 CIDR 表示法中的/24)。
在 Debian Linux 和 Ubuntu Linux 系统中,您可以使用ifup和ifdown代替ifconfig up和ifconfig down命令,或者在 Arch Linux 系统中使用ifupdown。
route
route命令用于查看和操作 IP 路由表。它用于确定网络数据包在给定目标 IP 地址下的发送位置。
在不带任何选项的情况下调用route命令将显示系统中的当前路由表:
admin@myhome:~$ route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
要向路由表添加新条目,请使用以下命令:
admin@myhome:~$ sudo route add default gw 172.17.0.1 eth0
只能有一个默认路由。要添加自定义路由,请执行以下操作:
admin@myhome:~$ sudo route add -net 192.168.1.0 netmask 255.255.255.0 gw 192.168.1.1 dev eth0
del命令用于从路由表中删除条目,类似于前面的示例:
admin@myhome:~$ route del -net 192.168.1.0 gw 192.168.1.1 netmask 255.255.255.0 dev eth0
最后,flush命令会删除路由表中的所有条目,这意味着您将失去所有网络连接,如果您连接到远程机器,您将无法再对其进行操作。
使用ifconfig和route命令还有更多的可能性,但正如我们之前所说,两个命令都被iproute2包(iproute的继任者)取代,该包包括ip命令。
iproute2
更高级的命令用于操作路由和网络设备的配置是ip,它可以用来执行更广泛的任务,如创建和删除接口、添加和删除路由以及显示网络统计信息。
让我们来看看使用iproute2时可以执行的最常见命令:
-
ip addr或ip a:此命令显示有关网络接口及其 IP 地址的信息。它还支持子命令;例如,ip addr add命令可用于向接口添加 IP 地址,而ip route add可用于向路由表中添加路由。 -
ip link或ip l:此命令显示有关网络接口及其链路层设置的信息。 -
ip route或ip r:此命令显示 IP 路由表。 -
ip -s link(或ip -s l):这将显示关于网络接口的统计信息。
以下是运行ip link和ip addr命令的输出:
admin@myhome:~$ sudo ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/tunnel6 :: brd :: permaddr 4ec4:2248:2903::
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
admin@myhome:~$ sudo ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1000
link/tunnel6 :: brd :: permaddr 4ec4:2248:2903::
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
两个命令打印出非常相似的信息,但ip addr除了提供物理接口的信息外,还添加了有关网络配置的信息。ip link命令用于控制接口状态。与ifconfig up eth0启用接口类似,ip link set dev eth0 up也会做同样的事情。
要使用iproute2配置网络接口,你需要执行以下命令:
admin@myhome:~$ sudo ip addr add 172.17.0.2/255.255.0.0 dev eth0
admin@myhome:~$ sudo ip link set dev eth0 up
要将接口设置为默认路由,请使用以下命令:
admin@myhome:~$ sudo ip route add default via 172.17.0.1 dev eth0
要仅检查eth0接口的状态,你可以执行以下命令:
admin@myhome:~$ sudo ip addr show dev eth0
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid
0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
netplan
这个工具是 Ubuntu 中引入的新网络配置工具,并且在 Debian 中也得到了支持。它是一个 YAML 配置文件,可用于管理网络接口、IP 地址和其他网络设置。它首次在 Ubuntu 17.10 中作为传统/etc/network/interfaces文件的替代品被引入,随后也被其他发行版(如 Debian 和 Fedora)采纳。Ubuntu 18.04 及更高版本默认安装了netplan,其他发行版如 Debian 10 和 Fedora 29 及更高版本也默认包含了netplan。
要使用 Netplan,你首先需要在/etc/netplan/目录中创建一个配置文件。该文件应具有.yaml扩展名,并且应该命名为具有描述性的名称,如01-eth0.yaml或homenetwork.yaml。
样本配置如下所示:
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: true
该配置定义了一个单一的网络接口eth0,它使用 DHCP 获取 IP 地址。renderer键指定了 netplan 使用的网络管理器(在本例中为networkd)。version键用于表示正在使用的netplan版本。networkd是systemd系统和服务管理器的一部分,用于网络管理的守护进程。
为eth0接口配置静态 IP 地址的配置文件将如下所示:
network:
version: 2
renderer: networkd
ethernets:
eth0:
addresses: [192.168.0.2/24]
gateway4: 192.168.0.1
nameservers:
addresses: [8.8.8.8, 4.4.4.4]
该配置文件定义了一个以太网接口(eth0)及其静态 IP 地址。此接口还定义了网关和 DNS 服务器。注意,我们使用了 CIDR 表示法,而不是十进制表示法。
一旦你保存了配置文件,为了应用更改,可以运行以下命令:
admin@myhome:~$ sudo netplan apply
你还可以使用以下命令检查配置文件是否有语法错误:
admin@myhome:~$ sudo netplan --debug generate
你可以使用以下命令检查网络接口的当前状态:
admin@myhome:~$ sudo netplan --debug try
最后,如果你想检查网络接口的状态而不应用配置,可以使用以下命令:
admin@myhome:~$ sudo netplan --debug networkd try
要查看与网络接口启动时的错误相关的日志,可以使用 dmesg 命令查看内核消息,包括与网络接口相关的消息。您可以使用 dmesg | grep eth0 来过滤与 eth0 接口相关的日志。其他位置包括 /var/log/messages 文件,journalctl(例如,journalctl -u systemd-networkd.service 命令),以及包含 netplan 生成的日志的 /var/log/netplan/。
在日常操作中,编辑 /etc/network/interfaces 文件或 netplan 配置比手动配置接口更常见,但是知道如何临时更改网络配置以进行测试或调试问题非常有用。
接下来,我们将介绍传输层。
传输层 – TCP 和 UDP
在 OSI 模型的传输层中,我们更多地关注传输控制协议(TCP)和 IP,它们是现代互联网的基础。此外,我们还将深入了解用户数据报协议(UDP)。我们在本章的网络层 – IPv4 和 IPv6 小节中讨论了 IP,因此这里我们只会稍微加深对该协议的了解。
TCP 用于通信,需要服务之间可靠的双向通信。UDP 是一种无状态协议,不需要持续连接。
使用 三次握手 建立了 TCP 连接:
-
客户端向服务器发送一个
SYN(同步)包,以初始化连接。 -
服务器接收到
SYN包,并向客户端发送一个SYN-ACK(同步-确认)包,以确认连接已建立。 -
客户端接收到
SYN-ACK包,并向服务器发送一个ACK(确认)包,以完成三次握手。
一旦完成三次握手,设备就可以通过已建立的 TCP 连接开始互相发送数据。
TCP 和 UDP 协议的连接是通过向连接的服务器端口发送数据包来初始化的。端口号包含在 IP 数据包头中,与 IP 地址一起,以便目标设备知道将数据发送到哪个进程。
端口是一个整数,范围从0到65535。它用于标识设备上运行的特定进程,并与其他服务区分开来。端口号会附加在 IP 地址后面。因此,如果多个程序在同一 IP 地址上监听连接,另一端可以准确知道它想要与哪个程序通信。假设有两个进程正在运行并监听:一个 WWW 服务器和一个 SSH 服务器。WWW 服务器通常会监听80或443端口,SSH 服务器会监听22端口。假设系统的 IP 地址是192.168.1.1。要连接到 SSH 服务器,我们会将 IP 地址与端口地址配对(通常写作192.168.1.1:22)。服务器会知道该传入的连接需要由 SSH 进程处理。与 TCP 不同,UDP 连接并不会建立与服务器机器的连接。相反,设备可以在已知端口上相互发送 UDP 数据报(数据包)。
当设备发送 UDP 数据报时,它会在数据包头中包含目标 IP 地址和端口号。接收设备检查数据包头中的目标 IP 地址和端口号,以确定将数据发送到哪个进程。
由于 UDP 是无连接的,因此不能保证数据会被目标设备接收,或者接收的顺序与发送的顺序相同。也没有错误检查或丢失数据包的重传。UDP 通常用于需要低开销和快速通信的服务。最著名的使用 UDP 进行通信的服务是域名系统(DNS)。
端口号有三种类型:
-
/etc/services文件。这些是为特定服务保留的端口号,例如HTTP流量的80端口或 DNS 流量的53端口。 -
1和1024。 -
短暂端口是用于临时连接的端口号,并由操作系统动态分配,例如用于 TCP 连接。
有各种工具可供查看机器上 TCP 和 UDP 流量的详细信息。你还可以设置防火墙,以便控制从网络访问你机器的权限。
netstat
要查看从你的机器发起的所有 TCP 连接,可以使用netstat命令。它可以用于查看打开的连接列表,并显示系统网络流量的统计信息。
要查看系统上所有打开的连接列表,可以使用netstat -an命令。这将显示所有当前连接的列表,包括本地和远程 IP 地址及端口,以及连接状态(例如,监听、已建立等)。
使用netstat -s,你可以查看系统上每个连接的统计信息。这将显示关于系统网络流量的各种统计数据,包括发送和接收的包数量、错误数量等。
要查看 netstat 中的所有 UDP 连接,你可以使用 netstat -an -u 命令。它会显示所有当前的 UDP 连接,包括本地和远程的 IP 地址与端口,以及连接的状态。
或者,你也可以使用 netstat -an -u | grep "udp" | grep "0.0.0.0:*" 命令,只显示处于监听状态的 UDP 连接。这个命令过滤 netstat -an -u 的输出,只显示包含 "UDP" 和 "0.0.0.0:*" 的行,这表示一个监听中的 UDP 连接。
你还可以使用其他选项,例如 -p 显示每个连接所属进程的进程 ID 和名称,-r 显示路由表,-i 显示特定接口的统计信息。
tcpdump
tcpdump 是一个命令行数据包分析器,允许你通过显示传输或接收的网络数据包来捕获和分析网络流量。tcpdump 可以用来排查网络问题、分析网络性能以及监控网络安全。你可以在特定接口上捕获数据包,基于各种标准过滤数据包,并将捕获的包保存到文件中以供后续分析。
要捕获并显示 eth0 接口上的所有网络流量,你可以使用 -i 选项,后跟接口名称。要捕获并显示 eth0 接口上的所有网络流量,你需要运行 sudo tcpdump -i eth0。
你还可以通过使用 -w 选项将捕获的数据包保存到文件中,以便以后分析——例如,tcpdump -i eth0 -w all_traffic.pcap。这将把 eth0 接口上捕获的所有数据包保存到文件中。
默认情况下,tcpdump 会无限期地捕获数据包。
要捕获特定时间内的数据包,你可以使用 -c 选项,后跟要捕获的数据包数量——例如,sudo tcpdump -i eth0 -c 100。该命令会捕获并显示 100 个数据包,然后退出。
你还可以通过使用像 port、ip、host 等过滤器来筛选流量——例如,sudo tcpdump -i eth0 'src host 192.168.1.2 and (tcp or udp)'。这个过滤器捕获所有源 IP 地址为 192.168.1.2 且为 TCP 或 UDP 的数据包。
为了展示 tcpdump 更高级的用法,让我们只捕获 SYN 数据包(查看所有正在建立的连接)。你可以通过使用 tcp[tcpflags] & (tcp-syn) != 0 过滤器来实现。这个过滤器检查数据包的 TCP 头部中是否设置了 SYN 标志。
这是一个命令示例,它捕获并显示 eth0 接口上的所有 SYN 数据包:
sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn) != 0'
你还可以通过使用 -w 选项将捕获的数据包保存到文件中,以便后续分析,如下所示:
sudo tcpdump -i eth0 -w syn_packets.pcap 'tcp[tcpflags] & (tcp-syn) != 0'
这将把所有捕获的 SYN 数据包保存到 syn_packets.pcap 文件中。
你也可以指定一个更复杂的过滤器,如下所示:
sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn) != 0 and src host 192.168.1.2'
这个过滤器仅捕获源 IP 地址为 192.168.1.2 的 SYN 数据包。
Wireshark
另一个类似于tcpdump的流行工具是 Wireshark。它既可以在无头模式下使用(仅在命令行中),也可以通过图形界面使用:
-
要使用 Wireshark 显示
eth0接口上的所有流量,你可以使用sudo wireshark -i eth0命令。这将启动 Wireshark 并监听eth0接口上的流量。你还可以使用-k标志立即开始捕获,并使用-w标志将捕获的流量写入文件:sudo wireshark -k -i eth0 -w output_file.pcap。 -
如果你想仅显示
SYN数据包,如同我们在tcpdump示例中所展示的那样,你可以运行sudo wireshark -i eth0 -f "tcp.flags.syn == 1"命令。前面的命令使用了一个过滤器,"tcp.flags.syn == 1",这表示我们只想看到标记为SYN的TCP协议标志。你也可以在 Wireshark 的图形界面版本中使用此过滤器,方法是进入捕获菜单,选择选项,然后在捕获过滤器字段中输入过滤器,再开始捕获。
或者,在捕获流量后,你也可以通过点击过滤字段中的"tcp.flags.syn==1"并按Enter来应用该过滤器。
ngrep
下一个非常有用的工具是ngrep,它类似于tcpdump和 Wireshark。与我们之前提到的其他工具不同,它的使用更加简单,而且允许你(类似于grep)在网络数据包中搜索字符串。
例如,要监视GET HTTP请求,你可以使用以下命令:
admin@myhome:~$ ngrep -d eth0 -q -W byline "GET" "tcp and port 80"
interface: en0 (192.168.1.0/255.255.255.0)
filter: ( tcp and port 80 ) and ((ip || ip6) || (vlan && (ip || ip6)))
match: GET
# Later output will contain actual GET requests, removed for readability
该命令将在eth0接口上监听,并仅匹配80端口上的TCP数据包(HTTP的默认端口)。-q选项告诉ngrep保持安静,不显示数据包摘要信息,而-W则告诉ngrep将每个数据包的数据打印在单独的行上。此时,我们可以进入会话层。
会话层
会话层,正如我们之前提到的,它负责在设备之间建立、维护和终止连接。这意味着它设置、协调和终止应用程序之间的对话、交换或连接。会话层通过使用令牌管理和检查点等技术,确保数据可靠地传输并按正确顺序传送。它还负责解决会话中可能出现的任何冲突,例如当两个应用程序试图同时发起会话时。简而言之,会话层在网络中建立、维护和终止设备之间的连接。
换句话说,会话层是我们已经讨论过的下层与接下来几节将要介绍的上层之间的粘合剂。
解决会话问题的最佳工具是与你正在使用的服务相关的日志。如果你遇到 FTP 连接问题,可能需要查看你管理的机器上运行的客户端和/或服务器的日志。如果日志不足以理解你要解决的问题,之前介绍的工具也可以提供帮助。
表示层 – SSL 和 TLS
ASCII)或 8 位(EBCDIC)整数。我们还在这一层处理加密和压缩。表示层还确保数据格式正确,便于应用程序处理。它充当应用程序与数据之间的中介,使得应用程序不受接收数据特定格式的影响。
对于这一层,最常见的加密标准是安全套接字层(SSL)和传输层安全性(TLS),你可能需要调试和修复这两个协议的问题。
TLS是一个广泛使用的协议,用于确保网络通信的安全性。它是SSL的继任者,用于加密和验证通过网络(如互联网)传输的数据。
TLS 通过在两个设备之间建立一个安全的tunnel(隧道)来工作,例如 Web 服务器和 Web 浏览器。该隧道用于在两个设备之间以加密格式传输数据,使得攻击者难以拦截和读取数据。
建立 TLS 连接的过程包括几个步骤:
-
握手:客户端和服务器交换信息,以建立共享的加密方法和密钥,确保连接的安全。
-
认证:服务器通过提供包含服务器身份和公钥信息的数字证书向客户端进行身份验证。客户端可以使用这些信息验证服务器的身份。
-
密钥交换:客户端和服务器交换公钥,以建立一个共享的秘密密钥,该密钥将用于加密和解密数据。
-
数据加密:一旦共享的秘密密钥建立,客户端和服务器就可以使用对称加密算法开始加密数据。
-
数据传输:然后,数据通过安全连接进行传输,并通过握手过程中建立的加密保护。
TLS 有多个版本,其中最新的版本被认为更为安全。然而,旧系统可能不支持最新版本。可用的 TLS 版本如下:
-
TLS 1.0:这是该协议的第一个版本,于 1999 年发布。
-
TLS 1.1:该版本于 2006 年发布。
-
TLS 1.2:该版本于 2008 年发布,增加了对新加密算法的支持,并做了其他多个安全性增强。
-
TLS 1.3:该版本于 2018 年发布,采用了前向保密性(forward secrecy),使得攻击者更难解密捕获的数据。
SSL 是一种广泛用于保障网络通信安全的协议,如互联网通信。它由 Netscape 在 1990 年代开发,后来被 TLS 协议所取代。
和 TLS 一样,SSL 通过在两个设备之间建立一个安全的tunnel(隧道)来工作,比如网页服务器和网页浏览器。这个隧道用于在两个设备之间传输加密格式的数据,使得攻击者很难拦截和读取数据。目前不建议再使用 SSL,最好使用最新版本的 TLS,但你可能仍然会遇到使用 SSL 的系统。
调试 SSL 和 TLS 问题的最佳工具是openssl命令。你可以用它测试与服务器的 SSL 连接。例如,你可以使用以下命令测试与服务器在端口(通常是443,这是 HTTPS 的常用端口)的连接:
admin@myhome:~$ openssl s_client -connect myhome:443
你可以使用openssl命令检查 SSL 证书的详细信息,包括过期日期、发行机构和公钥。例如,你可以使用以下命令来检查证书的详细信息:
admin@myhome:~$ openssl s_client -connect myhome:443 -showcerts
这将允许你检查服务器提供的证书是否有效,并且是否符合你的预期。这比使用网页浏览器要快得多。
使用openssl命令,你还可以检查服务器支持哪些加密算法。例如,你可以使用以下命令检查服务器支持的加密算法:
admin@myhome:~$ openssl s_client -connect myhome:443 -cipher 'ALL:eNULL'
# Cut most of the output for readability
SSL handshake has read 5368 bytes and written 415 bytes
---
New, TLSv1/SSLv3, Cipher is AEAD-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.3
Cipher : AEAD-AES256-GCM-SHA384
Session-ID:
Session-ID-ctx:
Master-Key:
Start Time: 1674075742
Timeout : 7200 (sec)
Verify return code: 0 (ok)
---
closed
此外,openssl还内置了诊断工具,可以检测系统中已知的漏洞:
admin@myhome:~$ openssl s_client -connect myhome:443 -tlsextdebug -status
CONNECTED(00000006)
140704621852864:error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version:/AppleInternal/Library/BuildRoots/aaefcfd1-5c95-11ed-8734-2e32217d8374/Library/Caches/com.apple.xbs/Sources/libressl/libressl-3.3/ssl/tls13_lib.c:151:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 5 bytes and written 303 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.3
Cipher : 0000
Session-ID:
Session-ID-ctx:
Master-Key:
Start Time: 1674075828
Timeout : 7200 (sec)
Verify return code: 0 (ok)
---
还值得注意的是,SSL 和 TLS 都使用公钥加密。在这种加密方法中,我们会创建两个文件:一个私钥和一个公钥。它们共同构成一对。公钥用于加密数据,而私钥用于解密数据。私钥应始终保密,正如它的名字所示。这种加密方法基于大素数的数学特性,被认为是非常安全的。
在 TLS 的情况下,公钥加密在握手阶段用于建立客户端与服务器之间的安全连接。这个过程如下:
-
服务器生成公钥和私钥。公钥作为服务器数字证书的一部分发送给客户端。
-
客户端生成一个会话密钥,用于加密发送到服务器的数据。服务器的公钥用于加密会话密钥。一旦服务器收到加密的数据,它使用私钥解密数据并恢复会话密钥。
-
一旦会话密钥被解密,它就会用于加密客户端与服务器之间发送的数据。
接下来的最后一层是 OSI 模型的顶层。在接下来的章节中,我们将介绍应用层,涵盖诸如 HTTP 和 FTP 等协议,这些协议通常用于浏览网页和共享文件。
应用层 – HTTP 和 FTP
应用层是 OSI 网络模型的第七层,也是最高层。它提供软件应用程序与网络之间的接口,使得应用程序能够访问网络的通信服务。应用层定义了特定于应用程序的协议和服务,例如文件传输(文件传输协议(FTP))、电子邮件(简单邮件传输协议(SMTP)和互联网消息访问协议(IMAP))以及广泛使用的网络服务(超文本传输协议(HTTP))。
让我们更仔细地看看 HTTP。这个通信协议用于在万维网上传输数据。它基于客户端-服务器模型,其中 Web 浏览器(客户端)向 Web 服务器发送请求,服务器则返回响应。
当用户在浏览器中输入统一资源定位符(URL)时,浏览器会向与该 URL 关联的网络服务器发送 HTTP 请求。URL 是你在浏览器地址输入框中看到的内容,通常位于浏览器窗口的顶部,例如,google.com。
请求包含方法(如GET、POST、PUT、PATCH或DELETE),指示浏览器希望服务器执行的操作类型,并可能包含其他信息,如POST或PUT请求的数据。
然后,网络服务器处理请求,之后服务器会发送回一个 HTTP 响应,包含状态码(如200表示成功或404表示未找到)以及浏览器请求的任何数据,例如构成网站的 HTML 和 CSS。
一旦浏览器收到响应,它会解析 HTML、CSS,通常还包括 JavaScript,以便将网站显示给用户。
HTTP 是一种无状态协议,这意味着每个请求都是独立的,服务器不会保留任何关于先前请求的信息。然而,许多 Web 应用程序使用 cookies 或其他技术来维持多个请求之间的状态。
HTTP 1.1 版本引入了新特性,如持久连接、主机头字段和字节服务,这些改进提高了协议的整体性能,并使其更适合重负载使用场景。该协议的最新版本是 2.0,详细描述在 RFC 7540 中(https://www.rfc-editor.org/rfc/rfc7540.xhtml),该版本于 2015 年发布,并在 2020 年由 RFC 8740(www.rfc-editor.org/rfc/rfc8740.xhtml)更新。
要解决 HTTP 问题,您可以使用任何调试网络问题的工具,例如 tcpdump 或 ngrep。有多种控制台和图形界面工具可供调试 HTTP。最常见的控制台工具是 wget 和 curl,图形界面工具包括 Postman 或 Fiddler。浏览器中也会内置调试工具,例如 Firefox 或 Chrome 开发者工具。
我们现在将重点关注控制台工具,因此首先来看看 wget。这个工具本用于下载文件,但我们仍然可以用它来调试 HTTP。首先,我们将展示请求和响应的详细信息:
admin@myhome:~$ wget -d https://google.com
DEBUG output created by Wget 1.21.3 on darwin22.1.0.
Reading HSTS entries from /home/admin/.wget-hsts
URI encoding = 'UTF-8'
Converted file name 'index.xhtml' (UTF-8) -> 'index.xhtml' (UTF-8)
--2023-01-20 14:11:02-- https://google.com/
Resolving google.com (google.com)... 216.58.215.78
Caching google.com => 216.58.215.78
Connecting to google.com (google.com)|216.58.215.78|:443... connected.
Created socket 5.
Releasing 0x0000600000d2dac0 (new refcount 1).
Initiating SSL handshake.
Handshake successful; connected socket 5 to SSL handle 0x00007fca4a008c00
certificate:
subject: CN=*.google.com
issuer: CN=GTS CA 1C3,O=Google Trust Services LLC,C=US
X509 certificate successfully verified and matches host google.com
---request begin---
GET / HTTP/1.1
Host: google.com
User-Agent: Wget/1.21.3
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
---request end---
HTTP request sent, awaiting response...
# Rest of the output omitted
您可能需要发送带有特定头部的 HTTP 请求。为此,您可以使用 --header 选项:
admin@myhome:~$ wget --header='<header-name>: <header-value>' <url>
POST 是一种特殊的 HTTP 请求——它使用 URL,但也需要一些请求数据,因为它的目的是向您的系统发送数据,无论是用户名和密码还是文件。要发送带有准备数据的 HTTP POST 请求,可以使用 --post-data 选项,如下所示:
admin@myhome:~$ wget --post-data=<data> <url>
一个更强大的调试 HTTP 问题的工具是 curl。要发送 HTTP GET 请求并显示响应,您可以使用以下命令:
admin@myhome:~$ curl linux.com
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
要发送 HTTP POST 请求并显示响应,您可以使用 -X POST 选项和 -d 选项:
admin@myhome:~$ curl -X POST -d <data> <url>
使用 -X 选项,您可以发送其他类型的请求,例如 PATCH 或 DELETE。
要发送带有特定头部的 HTTP 请求,您可以使用 -H 选项:
admin@myhome:~$ curl -H '<header-name>: <header-value>' <url>
若要仅显示响应头部,请使用以下代码:
admin@myhome:~$ curl -I linux.com
HTTP/1.1 301 Moved Permanently
Connection: keep-alive
Content-Length: 162
Content-Type: text/html
Location: https://linux.com/
Server: nginx
X-Pantheon-Styx-Hostname: styx-fe3-b-7f84d5c76-q4hpv
X-Styx-Req-Id: f1409a84-9832-11ed-abcb-7611ac88195f
Cache-Control: public, max-age=86400
Date: Fri, 20 Jan 2023 13:14:42 GMT
X-Served-By: cache-chi-kigq8000084-CHI, cache-fra-eddf8230050-FRA
X-Cache: HIT, HIT
X-Cache-Hits: 37, 1
X-Timer: S1674220483.602573,VS0,VE1
Vary: Cookie, Cookie
Age: 62475
Accept-Ranges: bytes
Via: 1.1 varnish, 1.1 varnish
您还可以通过使用 -v 选项显示请求和响应的详细信息,如下所示:
admin@myhome:~$ curl -v <url>
通过发送不同类型的请求并分析响应,您可以使用 wget 或 curl 来调试各种 HTTP 问题,如连接问题、响应错误和性能问题。像往常一样,您可以参考这两个工具的完整文档,以加深对它们使用方法的理解。
在本节中,我们介绍了 ISO/OSI 标准定义的几层网络层。每一层都标准化了网络通信不同元素的功能。接下来,我们将讨论防火墙。
防火墙
防火墙 是一种安全措施,根据预定义的规则和策略控制进出网络流量。它通常放置在受保护的网络和互联网之间,其主要目的是阻止未经授权的访问,同时允许授权的通信。防火墙可以是硬件基础的,也可以是软件基础的,且可以使用多种技术,如数据包过滤、状态检测和应用层过滤等来控制网络流量。在这一部分,我们将介绍一种适用于 Linux 系统的防火墙。
要控制 Linux 防火墙,您需要使用 iptables、ufw、nftables 或 firewalld。数据包过滤已内置在 Linux 内核中,因此这些命令行工具将与其交互。
iptables
iptables 是控制防火墙的最冗长工具,这意味着它没有太多的抽象,但理解基本概念很重要,这样我们才能继续使用更用户友好的工具。
如前所述,iptables 允许你创建规则来过滤和操作网络数据包,并可以根据各种标准(如 IP 地址、MAC 地址、端口和协议)控制进出网络的流量。
iptables 使用多个概念来组织规则并将其划分为功能部分:表、链、规则和目标。最一般的概念是用表来组织规则。
我们可以使用三种表:filter、nat 和 mangle。filter 表用于过滤进出数据包,nat 表用于网络地址转换,mangle 表用于高级数据包修改。
每个表包含一组链,用于组织规则。例如,filter 表包含三条预定义链:INPUT、OUTPUT 和 FORWARD。INPUT 链用于接收的数据包,OUTPUT 链用于发送的数据包,FORWARD 链用于转发的数据包。
每个链包含一组规则,这些规则用于匹配数据包并决定如何处理它们。每条规则都有一个匹配条件和一个动作。例如,某条规则可能会匹配来自特定 IP 地址的数据包并将其丢弃,或者它可能会匹配发送到特定端口的数据包并允许通过。
每条规则都有一个目标,它是当规则的匹配条件满足时应执行的操作。最常见的目标是 ACCEPT、DROP 和 REJECT。ACCEPT 表示允许数据包通过防火墙,DROP 表示丢弃数据包而不向对端反馈任何信息,REJECT 表示主动拒绝数据包,使得远端知道该端口的访问已被拒绝。
iptables 的默认表会将规则添加到 filter 表中,默认情况下,每个链(INPUT、OUTPUT 和 FORWARD)的默认策略设置为 ACCEPT。你还可以创建额外的表,并将数据包定向到该表进行后续处理。通常的做法是将至少 FORWARD 和 INPUT 策略设置为 DROP:
admin@myhome:~$ sudo iptables -P INPUT DROP
admin@myhome:~$ sudo iptables -P FORWARD DROP
同时,我们可以允许所有回环接口访问并设置为 ACCEPT:
admin@myhome:~$ sudo iptables -A INPUT -i lo -j ACCEPT
此外,所有处于 ESTABLISHED 或 RELATED 状态的数据包应当被接受,否则我们将失去所有已建立的连接或正在建立中的连接:
admin@myhome:~$ sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
为了允许 HTTP 和 HTTPS 流量,我们可以执行以下操作:
admin@myhome:~$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
admin@myhome:~$ sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
允许 SSH 流量是一个好主意,这样我们可以远程登录到这台机器:
admin@myhome:~$ sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
这里有一些 iptables 中常用的其他选项:
-
-A或--append:将规则追加到链的末尾 -
-I或--insert:在链中的特定位置插入一条规则 -
-D或--delete:从链中删除一条规则 -
-P或--policy:设置链的默认策略 -
-j或--jump:指定规则的目标 -
-s或--source:根据源 IP 地址或网络匹配数据包 -
-d或--destination:根据目标 IP 地址或网络匹配数据包 -
-p或--protocol:根据协议匹配数据包(例如,TCP、UDP 或 ICMP) -
-i或--in-interface:根据入站接口匹配数据包 -
-o或--out-interface:根据出站接口匹配数据包 -
--sport或--source-port:根据源端口匹配数据包 -
--dport或--destination-port:根据目标端口匹配数据包 -
-m或--match:添加匹配扩展,允许你根据其他标准(如连接状态、数据包长度等)匹配数据包
在处理 iptables 时有许多其他功能可用,例如设置 NAT、接口绑定、TCP 多路径等。我们将在本章的 高级主题 部分讨论其中一些内容。
nftables
iptables 以其冗长和缺乏内置抽象而闻名。
nftables 使用逻辑结构来组织规则,该结构包括表、链、规则和判决。表作为规则的顶级容器,并且在对规则进行分类时发挥重要作用。nftables 提供几种表类型:ip、arp、ip6、bridge、inet 和 netdev。
在每个表中,有不同的链,帮助进一步组织规则,按类别分为:filter、route 和 nat。
每个链包含单独的规则,这些规则作为匹配数据包和确定后续操作的标准。规则由匹配条件和判决组成。例如,规则可以匹配来自特定 IP 地址的数据包,并指示防火墙丢弃它们,或者它可以匹配前往特定端口的数据包,并决定接受它们。
让我们为传入和转发的数据包设置默认策略为“丢弃”(停止处理数据包并不作响应):
sudo nft add rule ip filter input drop
sudo nft add rule ip filter forward drop
此外,通常做法是允许所有环回接口访问:
sudo nft add rule ip filter input iifname "lo" accept
为确保已建立的和相关的连接被允许,你可以运行以下命令:
sudo nft add rule ip filter input ct state established,related accept
你可以运行以下命令以允许 HTTP 和 HTTPS 流量:
sudo nft add rule ip filter input tcp dport {80, 443} accept
最后,为了启用远程访问的 SSH 流量,你可以使用以下命令:
sudo nft add rule ip filter input tcp dport 22 accept
以下是一些在 nftables 中常用的选项:
-
add:将规则追加到链的末尾 -
insert:将规则插入到链中的特定位置 -
delete:从链中删除规则 -
chain:指定规则的目标 -
ip saddr:根据源 IP 地址或网络匹配数据包 -
ip daddr:根据目标 IP 地址或网络匹配数据包 -
ip protocol:根据协议匹配数据包(例如,TCP、UDP 或 ICMP) -
iifname:根据入站接口匹配数据包 -
oifname:根据出站接口匹配数据包 -
tcp sport:根据源端口匹配数据包 -
tcp dport:根据目标端口匹配数据包 -
ct state:添加一个匹配扩展,允许根据连接状态、数据包长度等其他标准匹配数据包
nftables被视为iptables的替代品,但在现代系统中,二者都常常被使用。接下来,我们将介绍一些更抽象、更易于使用的工具。
ufw
ufw是 Linuxiptables防火墙的前端,为其提供了一个简单易用的管理界面。ufw设计上非常易于使用,它会根据你指定的配置选项自动为你设置iptables规则。它比直接使用iptables更加用户友好,适用于常见任务。
在开始使用ufw之前,你需要启用它,这样你添加或移除的所有规则在系统重启后都会持久保存。只需运行以下命令:
admin@myhome:~$ sudo ufw enable
要使用ufw打开 TCP 端口80和443,你可以使用以下命令:
admin@myhome:~$ sudo ufw allow 80/tcp
admin@myhome:~$ sudo ufw allow 443/tcp
或者,你也可以使用一个命令同时打开这两个端口:
admin@myhome:~$ sudo ufw allow 80,443/tcp
一旦你打开了端口,你可以通过检查ufw的状态来验证更改:
admin@myhome:~$ sudo ufw status
ufw可在所有主要 Linux 发行版上使用,包括 Debian Linux、Ubuntu Linux、Arch Linux 和 Fedora Linux。但在某些情况下,你需要安装它,因为它并非系统的默认组成部分。
firewalld
你可以使用的另一个管理 Linux 防火墙的工具是firewalld。这是一个旨在简化防火墙动态配置的程序。firewalld的一个重要特点是区域(zones),它允许你声明不同级别的信任关系,针对不同的接口和网络。它默认包含在许多流行的 Linux 发行版中,如 Red Hat Enterprise Linux、Fedora、CentOS 和 Debian。其他一些 Linux 发行版,如 Ubuntu,默认并不包含firewalld,但你可以在这些系统上安装并使用它。
要使用firewalld打开 TCP 端口80和443,你可以使用firewall-cmd命令行工具。以下是打开这些端口的命令:
admin@myhome:~$ sudo firewall-cmd --add-port=80/tcp --permanent
admin@myhome:~$ sudo firewall-cmd --add-port=443/tcp --permanent
你可以使用一个命令同时打开这两个端口:
admin@myhome:~$ sudo firewall-cmd --add-port=80/tcp --add-port=443/tcp --permanent
添加端口后,你需要重新加载防火墙以使更改生效:
admin@myhome:~$ sudo firewall-cmd --reload
你还可以使用以下命令检查端口的状态:
admin@myhome:~$ sudo firewall-cmd --list-ports
无论你使用什么工具来配置防火墙,设置默认规则策略为DROP,并只允许你希望系统处理的流量,始终是个好主意。本章篇幅有限,无法涵盖很多相关话题,但了解网络配置时的一些可能性总是有帮助的。
高级话题
在本节中,我们将介绍网络功能的更高级用法。某些功能非常常见(如端口转发或 NAT),而有些则不太为人所知。我们将从最常见的功能开始,它们是你最有可能经常遇到的,然后再介绍一些更高级和不太为人知的功能。
NAT
网络地址转换(NAT)是一种将一个网络映射到另一个网络的技术。其最初的目的在于简化整个网络段的路由,而无需更改数据包中每个主机的地址。
源地址转换(SNAT)是一种改变数据包源 IP 地址的 NAT 类型。它用于允许私有网络上的主机通过单个公共 IP 地址访问互联网。
目标地址转换(DNAT)是一种改变数据包目标 IP 地址的 NAT 类型。它用于根据目标 IP 地址将传入的流量转发到特定的内部主机。通常用于允许外部客户端通过公共 IP 地址访问内部主机上运行的服务。
要使用iptables设置 NAT,你可以使用以下基本命令:
admin@myhome:~$ echo 1 > /proc/sys/net/ipv4/ip_forward
这将启用转发,使得你的机器能够将数据包从一个接口转发到另一个接口。这个特定的命令通过使用proc文件系统动态更新 Linux 内核配置。你也可以使用sysctl命令实现相同的功能:
admin@myhome:~$ sudo sysctl -w net.ipv4.ip_forward=1
要为本地网络如192.168.10.0/24配置 NAT,你需要以root用户身份运行以下命令:
admin@myhome:~$ sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
admin@myhome:~$ sudo iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
admin@myhome:~$ sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
请注意,eth0是连接到互联网的接口,eth1是连接到内部网络的接口。
MASQUERADE目标用于在 Linux 路由器上实现 NAT。当数据包通过路由器并发送到互联网时,MASQUERADE目标会将数据包的源地址更改为路由器的公共 IP 地址。这使得内部网络中的设备能够使用路由器的公共 IP 地址作为源地址与互联网中的设备通信,有效地将内部网络隐藏于互联网之外。
MASQUERADE目标通常用于nat表的POSTROUTING链,通常应用于连接到互联网的接口。它仅适用于动态分配的 IP 地址(DHCP),通常用于家庭路由器的情况。
端口转发
端口转发是一种将网络流量从一个网络地址和端口定向到另一个地址和端口的技术。这对于将传入流量定向到计算机或网络设备上运行的特定服务或应用程序非常有用。这有助于从远程位置访问私有网络上的服务或应用程序,或者使私有网络上运行的服务或应用程序对外界可访问。
本质上,它是 NAT 的另一种用途,因为你将更改到达你机器的数据包的目标 IP(以及端口)。
要将进入eth0接口的 TCP 端口80的数据包转发到内部 IP192.168.10.101的端口8080,可以使用以下命令:
admin@myhome:~$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.10.101:8080
admin@myhome:~$ sudo iptables -t nat -A POSTROUTING -j MASQUERADE
这里我们需要使用MASQUERADE,因为我们希望隐藏内部 IP 地址。
接口绑定
active-backup、balance-rr 和 802.3ad。active-backup模式意味着当主设备发生故障时,两个绑定接口中的一个作为备用。balance-rr将同时使用两个接口,并采用轮询策略。802.3ad绑定创建共享相同技术规格(速度和双工设置)的聚合组。您可以在官方 Linux 内核网站上阅读更多关于模式和绑定设置的信息:www.kernel.org/doc/Documentation/networking/bonding.txt
TCP 多路径
TCP 多路径指的是使用多个路径在网络中两个点之间发送和接收数据,而不是仅使用一个路径。这可以通过允许当一个路径不可用时进行故障切换,并通过在多个路径之间进行负载均衡,从而提高网络的可靠性和性能。可以通过多种技术实现这一点,例如使用设备上的多个接口或通过网络使用多个路由路径。
使用iproute2软件包配置多路径非常简单。要在 Linux 上使用eth0和eth1接口配置多路径,您需要运行以下命令:
admin@myhome:~$ ip route add default scope global nexthop via 192.168.1.1 dev eth0 nexthop via 192.168.2.1 dev eth1 weight 1
此命令创建了一个新的默认路由,使用eth0和eth1接口,并设置权重为1。在此示例中使用的 IP 地址(192.168.1.1和192.168.2.1)应替换为eth0和eth1接口上下一跳路由器的实际 IP 地址。
当运行ip route show时,您将看到一个新的多路径路由。要开始使用它,您需要更改默认路由:
admin@myhome:~$ ip route change default scope global nexthop via 192.168.1.1 dev eth0 nexthop via 192.168.2.1 dev eth1 weight 1
您可以在www.multipath-tcp.org/上阅读更多关于多路径及其使用方法的信息。
BGP
边界网关协议 (BGP) 是一种路由协议,用于在单个自治系统 (AS) 内部或多个自治系统之间分发路由信息。BGP 用于在互联网骨干路由器以及企业网络中构建路由表。
BGP 路由器与其邻居交换路由信息,这些邻居可以是同一自治系统或不同自治系统中的其他 BGP 路由器。当 BGP 路由器从其邻居接收路由信息时,它使用一套规则和策略来决定将哪些路由添加到其路由表中并广告给邻居。这使得 BGP 能够支持多个通向目标的路径,并根据距离、成本或优先级等各种因素选择最佳路径。
BGP 被认为是一种路径向量协议,因为它交换关于通往目标的完整路径的信息,而不仅仅是下一个跳点。这使得 BGP 能够支持高级功能,如路由策略和流量工程。
在 Linux 机器上使用 BGP 有多种方式,具体取决于您的使用场景和网络环境。
BIRD 是一款支持 BGP 和其他路由协议的 Linux 路由守护进程。你可以配置 BIRD,使其充当 BGP 发言人并与其他 BGP 路由器交换路由信息。BIRD 可以安装在大多数 Linux 发行版上,并可以通过简单的配置文件进行配置。
Quagga 是另一款支持 BGP、OSPF 和其他路由协议的 Linux 开源路由软件套件。你可以配置 Quagga 使其充当 BGP 发言人并与其他 BGP 路由器交换路由信息。Quagga 可以安装在大多数 Linux 发行版上,并可以通过命令行接口或配置文件进行配置。
Free Range Routing(FRR)是一款支持 BGP、OSPF 和其他路由协议的 Linux 路由软件套件;它是 Quagga 的一个分支。
你可以在以下的 Linux Journal 文章中阅读更多关于 BGP 和使用 BIRD 的内容:www.linuxjournal.com/content/linux-advanced-routing-tutorial。
总结
本章介绍了你在 DevOps 团队工作中可能会遇到的一些基本网络主题。这是一个起点和基础,帮助你理解处理容器内部运行的服务时与网络相关的主题。你可能还会想通过阅读关于 IPv6 协议的资料来扩展你的知识,IPv6 尚未完全取代 IPv4。
在下一章中,我们将重点介绍现代组织中主要使用的版本控制系统(VCS):Git。
第七章:Git,您的 DevOps 之门
Git 是一个免费的开源版本控制系统(VCS),被软件开发者和团队广泛使用,用于跟踪代码库的更改并进行协作。它允许多人在同一个代码库上工作而不会覆盖彼此的更改,并记录每次对代码所做的更改,使得在必要时可以轻松回滚到以前的版本。
Git 是由 Linus Torvalds 于 2005 年为 Linux 内核的开发而创建的,后来它成为了软件行业版本控制的事实标准。全球数百万开发者使用 Git,且它得到了一个庞大且活跃的开源社区的支持。
在本章中,我们将介绍最常用的 Git 命令以及如何使用它们。我们将从设置 Git 仓库并进行首次提交的基础开始,然后继续讲解更高级的话题,如分支和合并。
本章将涵盖以下内容:
-
基本的 Git 命令
-
本地与远程 Git 仓库
-
GitFlow 和 GitHub Flow
技术要求
在本章中,您需要一台具有 Bash shell 的系统。您需要确保该系统已安装 Git 命令或能够安装它。我们推荐使用 Linux 或 macOS 系统,但也可以在 Windows 上设置功能齐全的 Bash 和 Git。本书并不涉及该环境的安装。
基本的 Git 命令
您可以使用很多 Git 命令,但最常用的一些命令包括:
-
git config:此命令用于配置本地 Git 环境。配置可以是全局性的,这时配置值会保存在.gitconfig文件中,位于您的家目录下。值也可以仅在某个仓库中设置,并保存在该仓库内。 -
git init:此命令初始化一个新的 Git 仓库。当您在某个目录中运行此命令时,它会在项目根目录下创建一个新的.git目录,用于跟踪项目文件所做的更改。 -
git clone:此命令创建一个远程 Git 仓库的本地副本。当您运行此命令时,它会创建一个与仓库同名的新目录,并将所有文件及其历史记录克隆到该目录中。 -
git add:此命令用于暂存文件以供提交。当您在 Git 仓库中修改文件时,这些更改不会被自动跟踪。您必须使用git add命令告诉 Git 跟踪您所做的更改。 -
git commit:此命令将您的更改保存到 Git 仓库中。当您运行此命令时,它会打开一个文本编辑器,让您编写提交信息,即您所做更改的简短描述。在编写并保存提交信息后,您所做的更改将被保存到仓库中,并创建一个新的提交。 -
git push:此命令将本地提交推送到远程仓库。当你运行此命令时,它会将所有本地提交推送到远程仓库,更新项目历史,并使你的更改对其他开发者可见。 -
git pull:此命令从远程仓库获取更新并将其合并到本地仓库。当你运行此命令时,它会从远程仓库获取最新的更改,并将它们合并到本地仓库中,使你的项目副本保持最新。 -
git branch:此命令用于在 Git 仓库中创建、列出或删除分支。分支允许你同时处理项目的多个版本,通常用于功能开发或修复 bug。 -
git checkout或git switch:此命令用于在分支之间切换或恢复工作目录中的文件。当你运行此命令时,它会将工作目录切换到你指定的分支,或将指定的文件恢复到先前提交的状态。 -
git merge:此命令将一个分支合并到另一个分支。当你运行此命令时,它会将指定分支中的更改合并到当前分支,并创建一个新的提交,表示合并后的更改。 -
git stash:此命令用于暂时存储你还未准备好提交的更改。当你运行此命令时,它会将你的更改保存到一个临时区域,并将工作目录恢复到上一次提交的状态。稍后,你可以使用git stash apply命令将存储的更改恢复到工作目录。
配置本地 Git 环境
在使用 Git 命令之前,至少有两个选项需要设置。它们是你的姓名和电子邮件地址。可以通过git config命令来完成此操作。接下来,我将演示如何设置 Git 用户的姓名和电子邮件地址。我们将设置全局变量。除非专门为某个仓库单独设置,否则这些全局变量将在每个本地克隆的仓库中默认使用:
admin@myhome:~$ git config –global user.name "Damian Wojsław"
admin@myhome:~$ git config –global user.email damian@example.com
现在,当我们查看~/.gitconfig文件时,会看到以下部分:
# This is Git's per-user configuration file
[user]
name = Damian Wojsław
email = damian@example.com
还有更多配置选项,如默认编辑器等,但它们超出了本节内容的范围。
设置本地 Git 仓库
在你开始使用 Git 之前,需要创建一个仓库(也称为repo)。仓库是 Git 存储项目所有文件和元数据的目录。
要创建一个新的仓库,你可以使用git init命令。此命令会创建一个新的目录,并在其中生成一个.git子目录,包含仓库所需的所有文件。
例如,要在当前目录中创建一个新的仓库,你可以运行以下命令:
admin@myhome:~$ mkdir git-repository && cd git-repository
admin@myhome:~/git-repository$ git init
这会在当前目录中创建一个新的仓库,并设置必要的文件和元数据。一旦你设置了 Git 仓库,就可以开始向其中添加和提交文件。
要将一个文件添加到代码库中,你可以使用git add命令。这个命令会将文件添加到暂存区,暂存区是一个包含下次提交将包括的更改的列表。
要将一个名为main.c的文件添加到暂存区,你可以运行以下命令:
admin@myhome:~/git-repository$ git add main.py
你也可以通过用空格分隔文件或目录的名称来一次性添加多个文件:
admin@myhome:~/git-repository$ git add main.py utils.py directory/
要提交暂存区中的更改,你可以使用git commit命令。这会创建一个新的提交,其中包括暂存区中的所有更改。
每个提交都需要有提交信息。这是对你所做更改的简短描述。要指定提交信息,你可以使用-m选项,后跟消息内容:
admin@myhome:~/git-repository$ git commit -m "Added main.py and additional utils"
你也可以使用git commit命令而不加-m选项,这样会打开一个文本编辑器,你可以在其中编写更详细的提交信息。
在你进行 Git 操作的每个步骤中,你都可以使用git status命令。这用于查看 Git 代码库的当前状态。它显示哪些文件已被修改、添加或删除,以及它们是否已准备好提交。
有许多种格式化提交信息的方法,几乎每个项目都有自己的约定。一般来说,一个好的做法是添加一个来自问题跟踪系统的issue ID,然后是一个简短的描述,描述字符数不超过 72 个。第二行应留空,第三行跟随更详细的描述。以下是这样的提交信息示例:
[TICKET-123] Adding main.py and utils.py
main.py contains the root module of the application and is a default entry point for a Docker image.
utils.py contains helper functions for setting up the environment and reading configuration from the file.
Co-authors: other-commiter@myproject.example
当你运行git status时,它将显示已修改的文件列表,以及代码库中任何未跟踪的文件。它还会显示当前分支和暂存区的状态。
以下是git status命令输出的示例:
admin@myhome:~/git-repository$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: main.py
deleted: root/__init__.py
Untracked files:
(use "git add <file>..." to include in what will be committed)
ui/main.py
no changes added to commit (use "git add" and/or "git commit -a")
在这个例子中,git status显示了两个已修改的文件(main.py和__init__.py)以及一个未跟踪的文件(main.py)。它还指出当前分支是main,并且该分支与origin/main分支保持同步。
git status是一个有用的工具,可以帮助你了解当前代码库的状态,并识别哪些文件已经被修改并需要提交。你可以使用它快速查看已做的更改,并看到哪些文件已经准备好提交。它还可以显示你当前所在的分支,以及你是否与远程代码库保持同步。
git revert命令用于撤销已经对代码库所做的更改。它通过创建一个新的提交来撤销之前提交所引入的更改。
例如,假设你已经向你的代码库提交了几次更改,现在你想撤销上次提交所引入的更改。你可以使用git revert命令来实现:
admin@myhome:~/git-repository$ git revert HEAD
这将创建一个新的提交,撤销上次提交所引入的更改。你的代码库的提交历史现在看起来就像是上次提交从未发生过一样。
你也可以使用 git revert 命令撤销特定提交所引入的更改。为此,你需要指定你想撤销的提交的哈希值:
admin@myhome:~/git-repository$ git revert abc123
这将创建一个新的提交,撤销由 abc123 哈希值的提交所引入的更改。
需要注意的是,git revert 并不会删除提交或销毁历史记录。相反,它会创建一个新的提交,撤销之前提交所引入的更改。这意味着更改仍然存在于仓库中,但在当前分支的状态下不可见。
如果你需要永久删除提交,可以使用 git reset 命令或 git filter-branch 命令。然而,这些命令可能会永久销毁历史,因此使用时需要非常小心。
现在我们已经了解如何在本地仓库上工作,接下来我们可以讨论远程和本地仓库的副本。
本地与远程 Git 仓库
在 Git 中,仓库是文件及其历史的集合,以及用于管理仓库的配置文件。仓库可以是本地的,也可以是远程的。
执行 git init 命令或使用 git clone 命令克隆一个现有仓库时,你正在创建一个本地仓库。本地仓库对于在没有互联网连接时或当你希望将项目的副本保存在自己机器上时非常有用。
当你执行 git push 命令时,你正在用本地更改更新远程仓库。远程仓库对于与其他开发者合作非常有用,因为它们允许多人共同工作并互相共享更改。
Git 使用分布式版本控制系统(VCS),这意味着每个开发者的本地机器上都有仓库的完整副本。这允许开发者在本地工作,完成后将更改推送到远程仓库,与他人共享。此外,即使开发者没有连接到互联网,也可以在项目上进行协作。
git clone 命令用于创建一个远程 Git 仓库的本地副本。当你运行此命令时,它会创建一个与仓库同名的新目录,并将所有文件及其历史克隆到该目录中。
这是你可能如何使用 git clone 命令的示例:
admin@myhome:~/git-repository$ git clone https://github.com/user/repo.git
这将创建一个名为 repo 的新目录,并将位于 github.com/user/repo.git 的仓库克隆到该目录中。
你还可以通过将目录名作为参数添加,来指定本地目录的不同名称:
admin@myhome:~/git-repository$ git clone https://github.com/user/repo.git my-local-repo
这将创建一个名为 my-local-repo 的新目录,并将仓库克隆到其中。
如果你使用像 GitHub 这样的 Git 托管服务,你也可以使用仓库 URL 的简写版本:
admin@myhome:~/git-repository$ git clone git@github.com:user/repo.git
这将使用 SSH 协议克隆该仓库,这要求你为你的账户配置了一个 SSH 密钥。
git clone命令是创建远程仓库本地副本的有用方法,无论是开始一个新项目还是为现有项目做贡献。它允许你在本地工作,准备好后将更改推送回远程仓库。
与远程仓库交互
git pull命令用于从远程仓库获取更新,并将其合并到你的本地仓库中。它是git fetch命令(用于从远程仓库下载更新)和git merge命令(用于将更新合并到本地仓库)的一种组合。
git fetch命令从远程仓库下载更新,但不会将其合并到本地仓库中。相反,它将更新存储在一个名为remote-tracking branches的临时区域中。你可以使用git fetch命令更新你的远程跟踪分支,查看有哪些更改可用,但你需要使用git merge命令将这些更改实际合并到本地仓库中。
下面是一些你在使用git fetch时可能会用到的常见关键字:
-
origin:这是你从中克隆的远程仓库的默认名称。你可以使用origin来指定你想要从中获取更新的远程仓库。也可以更改默认名称并添加多个远程仓库。 -
main或master:master是 Git 仓库中默认分支的名称。main是 GitHub 平台引入的新默认名称。 -
REMOTE_HEAD:这是一个特殊的引用,指向远程仓库中分支的head提交。你可以使用REMOTE_HEAD来获取远程仓库当前检出的分支的更新。 -
HEAD:这是一个特殊的引用,指向当前分支在本地仓库中的head提交。
这是使用git fetch更新origin仓库中main分支的一个示例:
admin@myhome:~/git-repository$ git fetch origin main
From gitlab.com:gstlt/git-repository
* branch main -> FETCH_HEAD
这将为origin仓库中的master分支下载更新,并将其存储在origin/master远程跟踪分支中。
然后,你可以使用git merge命令将更新合并到本地仓库中:
admin@myhome:~/git-repository$ git merge origin/main
Already up to date.
这将把origin/master远程跟踪分支的更新合并到你的本地master分支中。在这种情况下,我们没有需要合并的内容。
或者,你可以使用git pull命令将这两个步骤合并为一个命令来执行:
admin@myhome:~/git-repository$ git pull origin main
Already up to date.
这将从origin仓库获取更新,并将它们合并到本地仓库中,除非你已经按照本章开头的建议配置了 Git 客户端——在这种情况下,Git 将尝试在远程仓库的main分支上进行变基。
git rebase和git merge是用于将一个分支的更改合并到另一个分支中的命令。然而,它们的工作方式略有不同,对你的仓库也有不同的影响。
git rebase是一个用于将一个分支的一系列提交应用到另一个分支的命令。当你运行git rebase时,它会查看目标分支中有而源分支中没有的提交,并将它们逐个应用到源分支。这种操作的效果是重新播放源分支上的提交,并将其应用到目标分支之上。
例如,假设你在代码库中有两个分支:main和develop。main代表主开发分支,develop代表你正在处理的某个功能。如果你对develop分支进行几次提交,然后运行git rebase main,Git 会将这些提交一个接一个地应用到main分支,就好像你直接在main分支上做了这些提交一样。
然而,git merge是一个用于将一个分支的更改合并到另一个分支的命令。当你运行git merge时,它会查看源分支中的更改,并将它们以单个提交的形式应用到目标分支。这种操作的效果是,在目标分支上创建一个新的提交,将源分支中的所有更改合并进来。
例如,你有与 rebase 示例中相同的两个分支:main和develop。如果你对develop分支进行几次提交,然后运行git merge develop,Git 会在main分支上创建一个新的提交,将develop分支中的所有更改合并进来。
git rebase和git merge都是将一个分支的更改合并到另一个分支的有用工具,但它们对代码库有不同的影响。git rebase保持线性历史,并避免不必要的额外(合并)提交,但如果目标分支自源分支创建以来已有修改,它也可能会引发冲突。
git merge是合并更改的更直接方式,但它可能会创建许多合并提交,这会使得代码库的历史记录变得更加难以阅读,即使使用图形化工具也一样。
在合并或将本地代码库与远程版本进行 rebase 之前,检查两者之间的差异是很有用的。为此,我们可以使用git diff命令。
什么是 git diff 命令?
git diff命令用于比较对代码库所做的更改。它显示文件的两个版本或代码库中两个分支(本地或远程)之间的差异。
以下是git diff命令的一些常见用法。
比较文件当前状态和最新提交之间的差异:
admin@myhome:~/git-repository$ git diff path/to/file
这将显示文件当前状态与最后一次提交的版本之间的差异。
比较两个提交之间的差异:
admin@myhome:~/git-repository$ git diff hash1 hash2
这将显示你提供的两个提交之间的差异。
比较一个分支和其上游分支之间的差异:
admin@myhome:~/git-repository$ git diff branch-name @{u}
这将显示指定分支与其上游对应分支之间的差异。
比较暂存区与最新提交之间的差异:
admin@myhome:~/git-repository$ git diff --staged
这将显示已添加到暂存区与最新提交之间的差异。
如果你已经添加了一些文件准备提交,但在创建提交之前想再次确认,可以使用以下命令:
admin@myhome:~/git-repository$ git diff --cached
这将显示你执行了git add命令的所有文件的更改。
要比较同一仓库的本地和远程分支,你需要引用远程分支,可以通过执行git diff来实现:
admin@myhome:~/git-repository$ git diff main origin/main
origin/main指的是一个远程分支,其中远程仓库被命名为“origin”。
git diff对于理解 Git 仓库中的变化以及这些变化如何在提交时被合并非常有用。你可以使用git diff命令的各种选项和参数来指定要比较的文件版本或分支。
既然我们已经讨论了如何比较仓库中的更改,这里有一个特别的场景,这项技能非常有用:在重新基准(rebase)或合并(merge)更改时解决仓库中的冲突。
查看提交历史
Git 会记录每一次对仓库进行的提交,你可以使用git log命令查看提交历史。这将显示仓库中所有提交的列表,包括提交信息和提交日期。
git log命令的常见用法包括以下几种:
-
使用
git log查看仓库的完整提交历史,包括已做的更改及这些更改的原因。当你在一个项目中工作并且需要理解它是如何随着时间演变时,这非常有帮助。 -
使用
git log查看受影响文件的提交历史,并查看哪个提交引入了这个 bug。 -
使用
git log查看受影响文件的历史,并查看哪些更改可能导致了问题。
你可以使用多种选项配合git log来定制其输出并筛选显示的提交。例如,你可以使用--author选项仅显示由特定人员提交的内容,或者使用--since选项仅显示过去一个月内的提交。
下面是一些使用git log命令的示例。
要显示当前仓库的提交历史,请调用git log命令:
admin@myhome:~/git-repository$ git log
commit 9cc1536fb04be3422ce18a6271ab83f419840ae3 (HEAD -> main, origin/main, origin/HEAD)
Author: Grzegorz Adamowicz <grzegorz@devopsfury.com>
Date: Wed Jan 4 10:13:26 2023 +0100
Add first version of the table schema
commit fb8a64f0fd7d21c5df7360cac427668c70545c83
Author: Grzegorz Adamowicz <grzegorz@devopsfury.com>
Date: Tue Jan 3 22:02:25 2023 +0100
Add testing data, mock Azure class, remove not needed comments
commit ae0ac170f01142dd80bafcf40fd2616cd1b1bc0b
Author: Grzegorz Adamowicz <grzegorz@devopsfury.com>
Date: Tue Dec 27 14:50:47 2022 +0100
Initial commit
你可以通过运行以下命令查看特定文件的提交历史:
admin@myhome:~/git-repository$ git log path/to/file
使用此命令,你可以显示特定分支的提交历史:
admin@myhome:~/git-repository$ git log branch-name
这将显示指定分支的提交历史,仅显示该分支上的提交。
你可以使用此命令显示一段提交范围内的提交历史:
admin@myhome:~/git-repository$ git log hash1..hash2
这将显示hash1提交 ID 和hash2提交 ID 之间的提交历史,仅显示发生在该范围内的提交。
git log --oneline将以紧凑的格式显示提交历史,仅显示每个提交的提交哈希值和消息:
admin@myhome:~/git-repository$ git log --oneline
使用此命令,你可以显示包含差异的提交历史:
admin@myhome:~/git-repository$ git log -p
这将显示提交历史,并展示每个提交的差异,显示每个提交中所做的更改。
这些只是你可以使用git log命令的一些示例。你可以在 Git 文档中找到更多关于可用选项的信息。
在下一部分,我们将探讨在将更改合并回main分支之前如何缩短我们的 Git 历史记录。如果我们在本地开发分支中有很长的提交历史,这会非常有用。
分支管理
git branch和git switch是两个用于管理 Git 仓库中分支的命令。
git branch是一个用于创建、列出或删除仓库中分支的命令。你可以通过指定分支名称作为参数来创建一个新的分支,像这样:
admin@myhome:~/git-repository$ git branch new-branch
这将创建一个名为new-branch的新分支,该分支基于当前分支。
你可以使用git branch命令配合-a选项列出仓库中的所有分支,包括本地分支和远程跟踪分支:
admin@myhome:~/git-repository$ git branch -a
你可以使用git branch命令配合-d选项删除一个分支,示例如下:
admin@myhome:~/git-repository$ git branch -d old-branch
这将删除old branch,但仅在它已经完全合并到上游分支的情况下。如果你希望删除此分支,可以添加--force选项,或使用-D选项,它是--delete --force Git 分支的别名。
最后,要删除一个远程分支,我们需要使用git push命令:
admin@myhome:~/git-repository$ git push origin --delete old-branch
这是一个具有破坏性的命令,无法撤销,因此在使用时应该谨慎操作。
git switch是一个用于在仓库中切换分支的命令。你可以通过指定分支名称作为参数来切换到不同的分支,像这样:
admin@myhome:~/git-repository$ git switch new-branch
这将切换到new-branch分支,并将其设置为当前分支。
git branch和git switch命令都允许你创建新分支、列出可用分支,并根据需要在分支之间切换。在接下来的部分,我们将介绍两种与 Git 工作相关的方法,称为工作流:git workflow和github workflow。
使用交互式变基合并提交
要使用git rebase压缩提交,你首先需要确定你想要压缩的提交范围。通常通过指定范围内第一个提交的哈希值和最后一个提交的哈希值来完成此操作。例如,如果你想压缩仓库中的最后三个提交,可以使用以下命令:
admin@myhome:~/git-repository$ git rebase -i HEAD~3
这将打开一个编辑器窗口,显示最近三次提交的列表,并附带一些说明。每个提交在文件中占据一行,行首是pick。要压缩某个提交,你需要将pick改为squash或s。
以下是文件可能的示例:
pick abc123 Add feature X
pick def456 Fix bug in feature X
pick ghi789 Add test for feature X
若要将第二个和第三个提交压缩到第一个提交中,你需要将文件更改为如下所示:
pick abc123 Add feature X
squash def456 Fix bug in feature X
squash ghi789 Add test for feature X
在做出更改后,你可以保存并关闭文件。Git 将应用这些更改,并向你展示另一个编辑器窗口,你可以在此输入合并提交的新提交消息。
需要注意的是,使用git rebase压缩提交可能是一个具有破坏性的操作,因为它会永久改变仓库的提交历史。一般建议在使用git rebase时小心,并确保在使用前有仓库的备份。
如果你想撤销git rebase操作所做的更改,可以使用git rebase --abort命令来丢弃这些更改,并将仓库恢复到先前的状态。
在成功压缩提交后,你可以将其推送到远程仓库,但必须使用git push --force命令,这将忽略你刚才重写的提交历史。这是一个破坏性操作,无法撤销,因此在使用--force选项推送更改前,请务必进行多次确认。
解决 Git 冲突
当你尝试合并或变基(rebase)存在冲突更改的分支时,可能会发生冲突。这通常是因为两个分支中相同的代码行都有修改,而 Git 无法自动解决这些冲突。
当在合并或变基过程中发生冲突时,Git 会在受影响的文件中标记冲突的行,你需要手动解决这些冲突才能继续操作。
下面是一个在合并过程中解决冲突的示例。
运行git merge来合并两个分支:
admin@myhome:~/git-repository$ git merge branch-to-merge
Git 将检测到任何冲突,并标记受影响文件中的冲突行。冲突的行将被<<<<<<<、=======和>>>>>>>标记包围:
<<<<<<< HEAD
something = 'this is in our HEAD';
=======
something = 'this is in our branch-to-merge';
>>>>>>> branch-to-merge
打开受影响的文件并解决冲突,选择你想保留的代码版本。你可以保留当前分支(HEAD)的版本,或是你正在合并的分支(branch-to-merge)的版本:
something = 'this is in our branch-to-merge';
使用git add暂存已解决的文件:
admin@myhome:~/git-repository$ git add path/to/file
继续合并或变基操作,运行git rebase --continue或git merge --continue:
admin@myhome:~/git-repository$ git merge --continue
在合并或变基过程中解决冲突可能是一个繁琐的过程,但它是使用 Git 时非常重要的一部分。通过仔细审查冲突的更改并选择正确的代码版本,你可以确保仓库的一致性并避免错误。
另外,定期将本地仓库与源分支同步,在合并或变基之前是一个好主意,以防您遇到无法解决的冲突或其他问题。这有助于您在过程中恢复任何可能发生的错误或意外。
还有一些非常棒的图形化工具,可以以更互动的方式解决冲突。如果您使用的是KDiff3、WinMerge、Meld或其他类似工具。
在本节中,我们已解释了分支、仓库、本地和远程仓库、合并和变基的概念。接下来,我们将研究浏览仓库的历史更改并进行修改。
GitFlow 和 GitHub Flow
develop和master。develop分支用于开发新特性,而master分支表示当前的生产就绪状态。还有几个支持分支,如feature、release和hotfix,用于管理软件的开发、发布和维护过程。
使用拉取请求(pull requests)向master分支进行合并。没有单独的develop分支,master分支始终被认为是当前的生产就绪版本。
另一种非常相似的分支模型是GitLab Flow。它用于管理软件项目的开发和维护,专门设计用于与 GitLab 一起使用,GitLab 是一个基于 Web 的 Git 仓库管理工具,提供源代码管理(SCM)、持续集成(CI)等功能。
在 GitLab Flow 模型中,所有开发都在分支中进行,新特性通过合并请求合并到master分支。没有单独的develop分支,master分支始终被认为是当前的生产就绪版本。然而,GitLab Flow 确实包含一些附加功能和工具,如使用受保护分支和合并请求审批的功能,这可以帮助团队执行开发最佳实践并保持高水平的代码质量。
在下一节中,我们将深入研究如何使用配置文件根据我们的需求来配置 Git。
全局 Git 配置 - .gitconfig
.gitconfig文件是一个用于设置 Git 全局选项的配置文件。它通常位于用户的home目录中,可以使用任何文本编辑器进行编辑。
您可能想在.gitconfig中包含的一些常见选项如下:
-
user.name和user.email:这些选项指定将与您的 Git 提交关联的用户名和电子邮件地址。正确设置这些选项非常重要,因为它们将用于识别您作为提交的作者。 -
color.ui:此选项启用或禁用 Git 中的彩色输出。您可以将此选项设置为auto,以便在 Git 在支持彩色输出的终端中运行时启用彩色输出,或者将其设置为true或false,分别表示始终启用或禁用彩色输出。 -
core.editor:此选项指定 Git 在需要你输入提交信息或其他输入时使用的文本编辑器。你可以将此选项设置为你喜欢的文本编辑器命令,如nano、vi或emacs。 -
merge.tool:此选项指定 Git 在合并分支时解决冲突的工具。你可以将此选项设置为可视化合并工具的命令,如kdiff3、meld或tkdiff。 -
push.default:此选项指定当你没有指定分支时,git push命令的行为。你可以将此选项设置为simple,它会将当前分支推送到远程的同名分支,或者设置为upstream,它会将当前分支推送到它正在跟踪的远程分支。 -
alias.*:这些选项允许你为 Git 命令创建别名。例如,你可以将alias.st设置为status,这将允许你使用git st命令代替git status。
以下是一个示例.gitconfig文件,使用了前面提到的选项,并在每个部分后面添加了注释:
[user]
name = Jane Doe
email = jane@doe.example
[color]
ui = always
user部分定义了每个提交的作者用户名和电子邮件。color部分启用颜色,以便提高可读性。ui = always将始终启用颜色,适用于所有输出类型(无论是机器消费还是其他)。其他可能的选项有true、auto、false和never。
alias部分允许你在使用 Git 时简化一些长命令。它将在左侧定义一个别名,用于 Git 命令,你可以将它添加到等号后面。
这是一个示例:
[alias]
ci = commit
我们将ci命令定义为commit。在将此内容添加到你的.gitconfig文件中后,你将获得另一个 Git 命令:git ci,它实际上会执行git commit命令。你可以为日常使用的常见 Git 命令添加别名。以下命令告诉 Git 去哪里查找默认的.gitignore文件:
[core]
excludesfile = ~/.gitignore
以下的push设置更改了默认的推送行为,这将要求你指定要将本地分支推送到哪个远程分支:
[push]
default = current
通过指定current选项,我们指示 Git 尝试将本地分支推送到一个与本地分支名称完全相同的远程分支。其他选项在这里列出:
-
nothing:不尝试推送任何内容。 -
matching:将所有本地和远程名称相同的分支视为匹配并推送所有匹配的分支。 -
upstream:将当前分支推送到上游分支。 -
simple:这是默认选项。如果本地分支的名称与远程上游分支的名称不同,它将拒绝推送到上游。
当运行git pull命令时,Git 会尝试将远程提交合并到你的本地分支。默认情况下,它会尝试进行上游合并,这可能会导致不必要的合并提交。这将改变默认行为为rebase:
[pull]
rebase = false
更多信息请参见 本地与远程 Git 仓库 部分。
使用 .gitignore 配置文件忽略某些文件
.gitignore 文件是一个配置文件,用来告诉 Git 在跟踪仓库中的更改时,哪些文件或目录需要被忽略。如果你有一些由构建过程生成的文件,或者是特定于本地环境的文件,或者是与项目无关且不需要被 Git 跟踪的文件,这个功能会非常有用。
下面是一些你可能会在 .gitignore 文件中包含的文件和目录类型的示例:
-
*.tmp、*.bak或*.swp文件。 -
*.exe、*.jar、*.war文件,以及bin/、obj/或dist/目录。 -
如果你使用
npm,则为node_modules/目录;如果你使用 Composer,则为vendor/目录。 -
如果你使用的是 JetBrains IDEs,则为
idea/目录;如果你使用的是 Visual Studio Code,则为.vscode/目录。 -
敏感信息:包含密码或 API 密钥的文件。我们强烈建议特别小心忽略这些文件,以防它们被提交到仓库中。这将为你避免许多麻烦和不必要的风险。现在,像 GitHub 的 Dependabot 这样的机器人会在这些敏感文件进入你的仓库时发出警报(甚至阻止提交),但最好还是在开发过程早期就避免这些问题。
这是一个示例 .gitignore 文件,它会忽略一些常见类型的文件:
# Temporary files or logs
*.tmp
*.bak
*.swp
*.log
# Build artifacts
bin/
obj/
dist/
venv/
*.jar
*.war
*.exe
# Dependencies
node_modules/
vendor/
requirements.txt
# IDE-specific files
.idea/
.vscode/
# Sensitive information
secrets.txt
api_keys.json
也可以使用通配符和 .gitignore 文件来处理某些目录中需要特殊规则的情况,而这些规则不希望在全局范围内生效。
最后,你可以将你全站范围的 .gitignore 文件放入你的主目录,以确保不会提交任何本地开发所需的文件。
总结
Git 是一个非常强大的工具,很难将其所有功能浓缩到一本书中。在这一章中,我们学习了作为 DevOps 工程师在日常工作中最基本的任务,掌握了处理大多数问题所需的技能。
在下一章,我们将重点介绍 Docker 和容器化,并将在那里将你迄今为止获得的所有技能付诸实践。
第八章:Docker 基础
本章我们介绍 DevOps 工具包的一个基础构件——容器。我们将解释虚拟化和容器之间的区别,并展示这两种解决方案的优缺点。此外,我们还将展示如何根据工作负载选择适合的解决方案。
本章覆盖的主要主题如下:
-
虚拟化与容器化
-
Docker 架构
-
Docker 命令
-
Dockerfile
-
Docker 镜像注册表
-
Docker 网络
技术要求
本章你需要一台安装了 Docker 引擎的 Linux 系统。我们这里不会涉及安装步骤。不同的 Linux 发行版提供 Docker 的方式不同。我们将使用 Docker 引擎版本 20.10.23。由于本章的所有示例都非常基础,较旧版本的 Docker 很可能也能正常工作。不过,如果你在跟随我们的示例时遇到问题,更新 Docker 到我们这个版本应当是排查问题的第一步。
虚拟化与容器化
本节我们将解释虚拟化和容器化是什么,它们之间的主要区别是什么。
虚拟化
虚拟化是一种在另一台计算机内运行完整模拟计算机的技术。完整意味着它模拟了物理计算机所具有的所有硬件:主板、BIOS、处理器、硬盘、USB 端口等。模拟意味着它完全是软件的产物。这台计算机在物理上并不存在,因此被称为虚拟计算机。为了存在,虚拟机(VM)需要一台真实的物理计算机来模拟它。物理计算机被称为宿主机或虚拟化管理程序(Hypervisor)。
所以,我有一台物理计算机。它非常强大。我为什么要在它上运行虚拟机呢?显而易见,虚拟机的性能会比主机差:毕竟,主机需要为自己分配 RAM、CPU 和硬盘空间。与物理机相比,虚拟机的性能也会有一些小幅下降(因为我们实际上是在运行一个模拟完整硬件的程序)。
理由因使用场景而异,但有很多。
你可能想要运行一个与你自己的操作系统不同的完整操作系统,用来测试一些软件,运行你当前操作系统中没有的软件,或者是为了忠实地重建你的应用程序开发环境。你可能想尽可能精确地重建一个生产环境来测试你的应用程序。这些都是使用虚拟机的有效且非常流行的理由。
让我们来看看虚拟化的优势:
-
隔离:正如前面所提到的,虚拟机(VMs)表现为完全功能的计算机。对运行中的操作系统来说,它们创造了与物理机器分离的假象。我们在虚拟机中运行的任何程序都不应能访问主机计算机(除非明确允许),实际上,除了几个由于编程错误导致的事故,虚拟机一直提供安全的环境。这种隔离在恶意软件分析、运行需要独立服务器的工作负载等场景中是一个非常好的解决方案。举例来说,如果一个虚拟机运行一个单独的 WWW 服务器,那么服务器的安全漏洞可能会使攻击者获取操作系统的访问权限,从而让他们可以自由操作。但由于其他基础设施组件(例如数据库)是运行在独立的虚拟机中的,这个问题只能局限于 WWW 服务器。
-
调优:借助足够强大的主机,可以对其资源进行分区,以确保每个运行中的虚拟机都有保证的内存、硬盘空间和 CPU。
-
操作系统简化:当运行各种工作负载时,比如数据库、WWW 服务器和邮件服务器,维持单一服务器同时运行这些服务的复杂性会迅速增加。每安装一个软件就需要安装额外的软件(例如库文件和辅助程序)。不同程序所需的库可能会引发不兼容的问题(尤其是当我们安装的不是操作系统开发者发布的软件,即所谓的第三方程序时)。在少数情况下,甚至操作系统自带的软件之间也可能存在不兼容的问题,使得在一个操作系统上安装它们变得不可能或非常困难。维护这样的系统可能会变得麻烦,并需要大量的排查工作。现代的虚拟机管理软件通过克隆、快照、黄金镜像等方式缓解了许多系统管理的难题。
-
自动化:现代虚拟化软件提供了许多功能,促进了多层次的系统管理自动化。快照——即系统的某一时刻快照——允许在任何时刻回滚到之前的系统状态。这使得轻松回退到最后一个已知的良好状态成为可能,避免了不想要的变化。克隆可以让我们基于另一个已经运行并配置好的虚拟机来配置新的虚拟机。黄金镜像是虚拟机的存档镜像,我们可以轻松快速地导入并启动,完全省略了安装过程,并将配置限制到绝对最小化。这也使得环境的可靠重建成为可能。
-
加速:正确设置虚拟机工作流可以让我们在几分钟内启动一个新的操作系统,配备自己的服务器或桌面硬件,而不是几小时。这为测试环境、远程桌面解决方案等开辟了新的可能性。
上述列表并非详尽无遗,但应该能清楚地展示为什么虚拟化成为数据中心和托管公司宠爱的技术。我们可以廉价租用的各种服务器,正是虚拟化使公司能够对硬件进行分区的直接结果。
尽管这一解决方案非常出色,但它并不是万灵药,并不是所有东西都应该虚拟化。而且,为每个软件单独运行一个虚拟机,很容易导致资源利用率的开销。100 个虚拟服务器不仅会使用分配给操作系统的 CPU 和 RAM,还会有一部分被用来做主机机器的管理工作。每个虚拟服务器都会占用操作系统所需的磁盘空间,即使它可能与同一服务器上其他 99 个虚拟机是完全一样的——这是一种空间、RAM 和 CPU 的浪费。而且,启动一个新的虚拟机也需要一些时间。诚然,如果一切配置和自动化得当,启动时间会比设置一台新硬件机器要短,但仍然需要时间。
在虚拟化广泛普及之前,操作系统开发人员试图提供一些技术,使系统管理员能够隔离各种工作负载。主要目标是数据中心,在那里一个物理服务器对于一个工作负载(如数据库或 WWW 服务器)来说已经太多,但运行多个工作负载又存在风险(如安全性、稳定性等)。随着虚拟化的普及,人们很快意识到有时使用虚拟化就像用大炮打麻雀。当你只想运行一个小程序时,采购一个全新的服务器(即便是虚拟的)和整个操作系统是没有意义的。因此,通过不断创新出更好的进程隔离特性,容器应运而生。
容器化
容器是轻量级的虚拟环境,允许我们以隔离的方式运行单个进程。一个理想配置的容器只包含运行应用程序所需的软件和库。主机操作系统负责操作硬件、管理内存和其他外围任务。容器的主要假设是它并不模拟一个独立的操作系统或独立的服务器。容器中的进程或用户可以轻松发现自己被隔离在其中。缺点是容器不会模拟硬件。例如,你不能用它们来测试新驱动程序。优点是单个容器的硬盘空间占用可能仅为几个兆字节,只需要运行进程所需的内存。
因此,启动一个容器只需要应用程序启动所需的时间。启动时间——BIOS、硬件测试和操作系统启动时间——都被缩短了。所有应用程序不需要的软件可以并且应该被省略。由于容器镜像的体积较小,它们的重新分发时间几乎可以忽略不计,启动时间几乎是瞬时的,构建时间和过程也大大简化。这使得环境的重建变得更加容易。反过来,这使得测试环境的设置变得更简单,而且通常会频繁地部署软件的新版本——有时一天可以进行几千次部署。应用程序的扩展变得更加容易和快速。
上述变化引入了另一种运行应用程序的方法。上述变化的逻辑结果是,容器的维护方式与操作系统不同。你不会在容器内升级软件——你会部署一个包含新版本软件的容器,替换掉过时的版本。这就导致了一个假设:你不应该在容器内保存数据,而是将数据保存在运行时挂载到容器的文件系统中。
Linux 容器化的代表性技术是Docker。Docker 所做的一件事可能帮助推动了这场革命,那就是创建了一个容器镜像共享的生态系统。
在 Docker 中,容器镜像是一个简单的存档,包含了应用程序所需的所有二进制文件和库以及一些配置文件。由于镜像体积通常较小,且镜像内部从不包含数据,因此允许人们共享他们构建的镜像是合乎逻辑的。Docker 有一个镜像中心(称为 Docker Hub),提供了一个漂亮的 WWW 界面,以及用于搜索、下载和上传镜像的命令行工具。这个中心允许对镜像进行评分,并给作者提供评论和反馈。
既然我们已经知道容器化是什么,我们可以更深入地了解 Docker 如何在内部工作以及它的运作原理。让我们来看看 Docker 的构成。
Docker 的构成
Docker 包含多个组件:
-
命令行工具 – Docker
-
主机
-
对象
-
注册表
Docker CLI 工具——docker——是管理容器和镜像的主要工具。它用于构建镜像、从注册表中拉取镜像、上传镜像到注册表、运行容器、与容器互动、设置运行时选项,并最终销毁容器。它是一个命令行工具,通过 API 与 Docker 主机进行通信。默认情况下,假设在主机上调用 docker 命令,但这并非严格要求。一个 docker CLI 工具可以管理多个主机。
主机更有趣。主机运行dockerd——一个负责实际执行通过docker工具下达的操作的守护进程。正是在这里存储容器镜像。主机还提供诸如网络、存储以及容器本身等资源。
dockerd守护进程是容器的心脏。它是一个在主机上运行的后台进程,负责管理容器。dockerd管理容器的创建与管理,提供与守护进程交互的 API,管理卷、网络和镜像分发,提供管理镜像和容器的接口,并存储和管理容器与镜像的元数据。它还负责在 Docker Swarm 模式下管理其他进程之间的通信。
OverlayFS
OverlayFS首次作为 Linux 内核版本 3.18 的一部分于 2014 年 8 月发布。最初它是为了提供一种比之前的存储驱动程序Another UnionFS(AUFS)更高效和灵活的方式来处理容器存储。OverlayFS 被认为是 UnionFS 的下一代,而 UnionFS 是当时 Docker 使用的存储驱动程序。
从 Docker 版本 1.9.0 开始,这个文件系统被作为内置存储驱动程序包含在 Docker 中。从那时起,OverlayFS 成为大多数 Linux 发行版中 Docker 的默认存储驱动程序,并且在各种容器编排平台(如 Kubernetes 和 OpenShift)中得到广泛使用。
OverlayFS 是 Linux 的一个文件系统,允许一个目录覆盖另一个目录。它允许创建一个由两个不同目录组成的虚拟文件系统:一个下层目录和一个上层目录。上层目录包含用户可见的文件,而下层目录包含隐藏的基础文件。
当访问上层目录中的文件或目录时,OverlayFS 首先在上层目录中查找,如果没有找到,再查找下层目录。如果在上层目录中找到该文件或目录,则使用该版本;如果在下层目录中找到,则使用该版本。
该机制允许创建覆盖文件系统,在这种文件系统中,上层目录可以用于添加、修改或删除下层目录中的文件和目录,而无需修改下层目录本身。这在容器化场景中非常有用,其中上层可以用于存储在容器中所做的更改,而下层则包含容器的基础镜像。
什么是镜像?
Docker 镜像是一个预构建的包,包含了在容器中运行软件所需的所有文件和设置。它包括你的应用程序代码或二进制文件、运行时、系统工具、库和所有需要的配置文件。一旦镜像构建完成,它可以用于启动一个或多个容器,这些容器是提供一致方式运行软件的隔离环境。
启动容器时,你必须选择一个程序作为容器的主进程来运行。如果这个进程退出,整个容器也会被终止。
构建一个 Docker 镜像通常涉及创建一个 Dockerfile,这是一个包含构建镜像指令的脚本。Dockerfile 指定了使用的基础镜像、需要安装的任何附加软件、需要添加到镜像中的文件以及需要应用的任何配置设置。
在构建镜像时,Docker 会读取 Dockerfile 中的指令并执行我们在 Dockerfile 中准备的步骤。
一旦镜像构建完成,它可以被保存并用于启动一个或多个容器。构建镜像的过程也可以通过使用 Jenkins、GitHub 或 GitLab 等工具来自动化,这些工具可以在每次对代码库进行更改时自动构建和测试新的镜像。
结果镜像由一个唯一的 ID(SHA-256 哈希)组成,这是镜像内容和元数据的哈希值,它还可以有一个标签,这是一个可读的字符串,可以用来引用镜像的特定版本。UnionFS 负责在运行容器时合并所有内容。
要检查镜像的元数据和内容部分,可以运行以下命令:
admin@myhome:~$ docker pull ubuntu
admin@myhome:~$ docker inspect ubuntu
[
{
"Id": "sha256:6b7dfa7e8fdbe18ad425dd965a1049d984f31cf0ad57fa6d5377cca355e65f03",
"RepoTags": [
"ubuntu:latest"
],
"RepoDigests": [
"ubuntu@sha256:27cb6e6ccef575a4698b66f5de06c7ecd61589132d5a91d098f7f3f9285415a9"
],
"Created": "2022-12-09T01:20:31.321639501Z",
"Container": "8bf713004e88c9bc4d60fe0527a509636598e73e3ad1e71a9c9123c863c17c31",
"Image": sha256:070606cf58d59117ddc1c48c0af233d6761addbcd4bf9e8e39fd10eef13c1bb7",
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/f2c75e37be7af790f0823f6e576ec511396582ba71defe5a3ad0f661a632f11e/merged",
"UpperDir": "/var/lib/docker/overlay2/f2c75e37be7af790f0823f6e576ec511396582ba71defe5a3ad0f661a632f11e/diff",
"WorkDir": "/var/lib/docker/overlay2/f2c75e37be7af790f0823f6e576ec511396582ba71defe5a3ad0f661a632f11e/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": ["sha256:6515074984c6f8bb1b8a9962c8fb5f310fc85e70b04c88442a3939c026dbfad3"
]
},
}
]
这里有很多信息,所以我们已经去除了不必要的输出,只保留了我们要关注的信息。你可以看到一个镜像 ID,GraphDriver部分下合并的所有目录,以及RootFS sha256层。RootFS包含了当我们在容器内启动一个进程时,由 UnionFS 创建的整个文件系统。
什么是容器运行时?
容器运行时(或容器引擎)是一个在你的系统上运行容器的软件组件。容器运行时从 Docker 注册表加载容器镜像,监控系统资源,为容器分配系统资源,并管理其生命周期。
有许多正在使用的运行时容器。最著名并且在笔记本电脑上使用得最多的容器运行时是containerd——你可能已经在你的系统上安装了它。它是一个高性能的容器运行时,旨在嵌入到更大的系统中。许多云服务提供商使用它,它也是 Kubernetes 的默认运行时。
LXC 是一个使用 Linux 命名空间和 cgroups 提供容器隔离的运行时。它被认为比 Docker(containerd)更加轻量级和高效,但也更难使用。
另一个有趣的运行时是 Open Container 接口容器运行时(CRI-O)。CRI-O 完全符合 Open Container Initiative (OCI) 规范,这意味着它可以运行任何符合 OCI 规范的容器镜像。此外,它设计为与 Kubernetes Pods 原生兼容,这使得它在与 Kubernetes 的集成上优于其他运行时。
Rocket (rkt) 是一种替代的容器运行时,旨在比 Docker 更加安全和高效。它使用 App Container (appc) 镜像格式,并且架构比 Docker 更加简洁。它也不是很常用。
其他值得注意的容器引擎包括 run Open Container (runC),这是一个低级别的容器引擎,提供了创建和管理容器的基本功能,以及由 AWS 开发的 Firecracker。
cgroups
Linux cgroups(即 控制组)是 Linux 内核的一项功能,允许管理和隔离一组进程的系统资源。Cgroups 允许系统管理员为特定的进程组分配资源,如 CPU、内存和网络带宽,并监控和控制它们的使用。
这可以用于限制特定应用程序或用户使用的资源,或者在共享系统上隔离不同类型的工作负载。
默认情况下,Docker 不会限制容器内应用程序的 CPU 或内存使用。启用此功能非常简单,不需要直接与 cgroups 或内核设置交互——Docker 守护进程会为我们处理这些。
你可以通过在 docker run 命令中使用 --memory 或 -m 选项来限制 Docker 容器可以使用的内存量。
例如,使用以下命令以 500 MB 的内存限制运行 alpine 镜像:
admin@myhome:~$ docker run --memory 500m alpine /bin/sh
你可以通过使用适当的后缀(b、k、m 或 g)来指定内存限制,单位可以是字节、千字节、兆字节或吉字节。
当你限制容器的内存时,Docker 还会限制容器可以使用的交换内存量。默认情况下,交换内存的限制是内存限制的两倍。也可以通过使用 --memory-swap 或 --memory-swappiness 选项来限制交换内存。
通过使用 CPU 配额限制(--cpus 或 -c 选项),可以限制 Docker 容器内应用程序可以使用的 CPU 时间。CPU 配额是容器可以使用的 CPU 时间的相对度量。默认情况下,容器会被分配一定数量的 CPU 配额,容器可以根据其分配的份额来消耗 CPU 时间。例如,如果一个容器有 0.5 个 CPU 配额,那么如果没有其他容器消耗 CPU,它最多可以使用 50% 的 CPU 时间。
其他可用的选项如下:
-
--cpuset-cpus:此选项允许你指定容器可以使用的 CPU 核心范围,例如0-1表示使用前两个核心,或者0,2表示使用第一个和第三个核心。 -
--cpu-shares:允许你为 Docker 容器设置 CPU 时间限制。它指定了容器在给定时间内可以使用的 CPU 时间(以微秒为单位)。时间段由--cpu-period选项指定。 -
--cpu-quota和--cpu-period:--cpu-quota是 CPU 时间限制(以微秒为单位),--cpu-period是 CPU 时间周期的长度(以微秒为单位)。
--cpu-quota 和 --cpu-period 选项允许你为容器指定比 --cpus 和 --cpuset-cpus 选项更精确的 CPU 时间限制。如果你需要更精确地限制容器的 CPU 时间,以防止性能问题或确保应用程序可靠运行,这些选项非常有用。
在这一节中,我们讲解了容器运行时及其工作原理。接下来,我们将探讨 containerd 守护进程的命令行界面,以便更轻松、强大地与所有 Docker 组件交互。
Docker 命令
containerd 守护进程使用套接字文件或网络。
你可以使用的最常见命令如下:
-
build:允许你使用 Dockerfile 构建新的 Docker 镜像 -
run:启动一个新的容器 -
start:重新启动一个或多个停止的容器 -
stop:停止一个或多个正在运行的容器 -
login:用于访问私有注册表 -
pull:从注册表下载镜像或仓库 -
push:将镜像或仓库上传到注册表 -
build:帮助从提供的 Dockerfile 创建镜像 -
images:列出你机器上的所有镜像 -
ps:列出所有正在运行的容器 -
exec:在正在运行的容器中执行命令 -
logs:显示容器的日志 -
rm:删除一个或多个容器 -
rmi:删除一个或多个镜像 -
network:用于管理 Docker 网络 -
volume:用于管理卷
docker build
docker build 命令用于从 Dockerfile 构建 Docker 镜像。基本语法如下:
docker build [OPTIONS] PATH | URL | -
PATH 是包含 Dockerfile 的目录路径。
URL 是指向包含 Dockerfile 的 Git 仓库的 URL。
-(破折号)用于从 stdin 的内容构建镜像,因此你可以将 Dockerfile 内容从先前命令的输出管道传输给它,例如,从模板生成它。
要从当前目录中的 Dockerfile 构建镜像,你需要运行以下命令:
admin@myhome:~$ docker build .
你也可以使用一个特定标签来构建镜像,如以下示例所示:
admin@myhome:~$ docker build -t my-image:1.0 .
你也可以将 --build-arg 参数传递给构建命令,以将构建时的变量传递给 Dockerfile:
admin@myhome:~$ docker build --build-arg VAR1=value1 -t my-image:1.0 .
docker run
当你运行一个容器时,本质上是将一个 Docker 镜像拿来并在该环境中执行一个进程。镜像是一个容器的蓝图或快照;它是一个只读模板,包含创建容器的指令。正在运行的容器是该镜像的一个实例,但有自己的状态。
docker run命令用于从 Docker 镜像启动一个容器。例如,要从myimage镜像启动一个容器并运行/bin/bash,你可以运行以下命令:
admin@myhome:~$ docker run myimage /bin/bash
你也可以向run命令传递选项,如下例所示:
admin@myhome:~$ docker run -d -p 8080:80 --name containername myimage
此命令以分离模式启动容器(-d选项;它将容器置于后台),将容器中的端口80映射到主机的端口8080(-p 8080:80),并将容器命名为containername(--``name containername)。
你还可以向容器传递环境变量:
admin@myhome:~$ docker run -e VAR1=value1 -e VAR2=value2 myimage:latest
Docker 容器在被终止后不会存储数据。为了使数据持久化,你需要使用容器本身以外的存储。在最简单的设置中,这可以是容器外部文件系统中的一个目录或文件。
有两种方法可以实现:创建一个 Docker 卷:
admin@myhome:~$ docker volume create myvolume
要使用此卷进行数据持久化,你需要在启动容器时挂载它:
admin@myhome:~$ docker run –v myvolume:/mnt/volume myimage:latest
你还可以绑定挂载本地文件夹(-v选项。在这种情况下,你不需要运行 Docker 卷创建命令:
admin@myhome:~$ docker run -v /host/path:/mnt/volume myimage:latest
你还可以使用-``w选项来指定容器内的工作目录:
admin@myhome:~$ docker run -w /opt/srv my-image
其他有用的选项如下:
-
--rm:此选项将在容器停止后删除该容器 -
-P,--publish-all:此选项将所有暴露的端口(Dockerfile中的EXPOSE选项)发布到一个随机的本地端口 -
--network:此选项将容器连接到指定的 Docker 网络
你可以通过执行docker run --``help命令来查看更多可用选项。
docker start
docker start命令用于启动一个或多个已停止的 Docker 容器。例如,要启动一个容器,你可以运行以下命令:
admin@myhome:~$ docker start mycontainer
mycontainer是你想要启动的容器的名称或 ID。你可以使用docker ps命令查看所有正在运行和已停止的容器;稍后我们会详细介绍。你也可以一次启动多个容器。为此,你可以列出它们的名称或 ID,用空格隔开。
要启动多个容器,你可以运行以下命令:
admin@myhome:~$ docker start mycontainer othercontainer lastcontainer
要附加到容器的进程,以便查看其输出,在启动容器时使用-a选项:
admin@myhome:~$ docker start -a mycontainer
docker stop
此命令用于停止后台运行的容器。该命令的语法与启动容器时相同,区别在于你可以使用的可用选项。
要一次停止多个容器,你可以列出它们的名称或 ID,用空格隔开。
admin@myhome:~$ docker stop mycontainer
要停止多个容器,你可以运行以下命令:
admin@myhome:~$ docker stop mycontainer othercontainer lastcontainer
您还可以使用-t选项来指定等待容器停止的时间(以秒为单位),然后发送SIGKILL信号。例如,要在停止容器之前等待10秒,请运行以下命令:
admin@myhome:~$ docker stop -t 10 mycontainer
您也可以使用--time或-t来指定在发送SIGKILL信号之前等待容器停止的时间。
默认情况下,docker stop命令会向容器发送SIGTERM信号,这样容器中的进程就有机会进行正常关闭。如果容器在默认的 10 秒超时后仍未停止,将会发送SIGKILL信号强制停止。
docker ps
该命令用于列出正在运行或已停止的容器。当您没有任何选项地运行docker ps命令时,它将显示正在运行的容器列表,并附带它们的容器 ID、名称、镜像、命令、创建时间和状态:
admin@myhome:~$ docker ps
可以使用-a选项查看所有容器:
admin@myhome:~$ docker ps -a
CONTAINER ID IMAGE COMMAND
STATUS PORTS NAMES
a1e83e89948e ubuntu:latest "/bin/bash -c 'while…" 29 seconds ago Up 29 seconds pedantic_mayer
0e17e9729e9c ubuntu:latest "bash" About a minute ago Exited (0) About a minute ago angry_stonebraker
aa3665f022a5 ecr.aws.com/pgsql/server:latest "/opt/pgsql/bin/nonr…" 5 weeks ago Exited (255) 8 days ago 0.0.0.0:1433->1433/tcp db-1
您可以使用--quiet或-q选项只显示容器 ID,这在脚本编写中可能会很有用:
admin@myhome:~$
docker ps --quiet
您可以使用--filter或-f选项根据某些标准过滤输出:
admin@myhome:~$
docker ps --filter "name=ubuntu"
要检查容器使用的磁盘空间,您需要使用-s选项:
admin@myhome:~$ docker ps -s
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE
a1e83e89948e ubuntu:latest "/bin/bash -c 'while…" 14 seconds ago Up 13 seconds pedantic_mayer 0B (virtual 77.8MB)
这个容器不会占用额外的磁盘空间,因为它没有保存任何数据。
docker login
docker login命令用于登录 Docker 注册表。注册表是您存储和分发 Docker 镜像的地方。最常用的注册表是 Docker Hub,但您也可以使用其他注册表,如 AWS Elastic Container Registry(ECR)、Project Quay 或 Google Container Registry。
默认情况下,docker login会连接到 Docker Hub 注册表。如果您想登录到其他注册表,您可以将服务器 URL 作为参数指定:
admin@myhome:~$ docker login quay.io
当您运行docker login命令时,它会提示您输入用户名和密码。如果您在注册表中没有帐户,可以通过访问注册表的网站来创建一个帐户。
登录后,您将能够从注册表推送和拉取镜像。
您还可以使用--username或-u选项来指定您的用户名,使用--password或-p来指定密码,但由于安全原因,不推荐在命令行中这样做。
您也可以使用--password-stdin或-P选项通过stdin传递密码:
admin@myhome:~$ echo "mypassword" | docker login --username myusername --password-stdin
它可以是任何命令的输出。例如,要登录到 AWS ECR,您可以使用以下命令:
admin@myhome:~$ aws ecr get-login-password | docker login --username AWS --password-stdin 1234567890.dkr.ecr.region.amazonaws.com
您还可以使用--token或-t选项来指定您的令牌:
admin@myhome:~$ docker login --token usertokenwithrandomcharacters
登录后,您将能够从注册表推送和拉取镜像。
docker pull
要拉取 Docker 镜像,您可以使用docker pull命令,后跟镜像名称和标签。默认情况下,pull会拉取标签latest(latest 是标签的名称,或镜像的版本)。
例如,使用以下命令从 Docker Hub 拉取alpine镜像的最新版本:
admin@myhome:~$ docker pull alpine
使用以下命令拉取特定版本的alpine镜像,例如版本 3.12:
admin@myhome:~$ docker pull alpine:3.12
你还可以通过在镜像名称中指定注册表 URL 来从不同的注册表拉取镜像。
例如,使用以下命令从私有注册表拉取镜像:
admin@myhome:~$ docker pull myregistry.com/myimage:latest
docker push
在构建新镜像后,你可以将该镜像推送到镜像注册表。默认情况下,push将尝试将其上传到 Docker Hub 注册表:
admin@myhome:~$ docker push myimage
要推送特定版本的镜像,例如版本 1.0,你需要在本地将镜像标记为版本 1.0,然后推送到注册表:
admin@myhome:~$ docker tag myimage:latest myimage:1.0
admin@myhome:~$ docker push myimage:1.0
你还可以通过在镜像名称中指定注册表 URL,将镜像推送到不同的注册表:
admin@myhome:~$ docker push myregistry.com/myimage:latest
在推送镜像之前,你需要使用docker login命令登录到你要推送镜像的注册表。
docker image
这是一个用于管理镜像的命令。docker image的常见用例如下所示:
要列出你机器上可用的镜像,可以使用docker image ls命令:
admin@myhome:~$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 6b7dfa7e8fdb 7 weeks ago 77.8MB
mcr.microsoft.com/mssql/server 2017-latest a03c94c3147d 4 months ago 1.33GB
mcr.microsoft.com/azure-functions/python 3.0.15066-python3.9-buildenv b4f18abb38f7 2 years ago 940MB
你也可以使用docker images命令执行相同的操作:
admin@myhome:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 6b7dfa7e8fdb 7 weeks ago 77.8MB
mcr.microsoft.com/mssql/server 2017-latest a03c94c3147d 4 months ago 1.33GB
mcr.microsoft.com/azure-functions/python 3.0.15066-python3.9-buildenv b4f18abb38f7 2 years ago 940MB
要从 Docker 注册表拉取镜像,请使用以下命令:
admin@myhome:~$ docker image pull ubuntu
当你指定镜像名称时,可以使用完整的仓库名称(例如,docker.io/library/alpine)或仅使用镜像名称(例如,alpine),如果镜像位于默认仓库(Docker Hub)中。另请参阅之前部分中讨论的docker pull命令。
也可以构建镜像:
admin@myhome:~$ docker image build -t <image_name> .
另请参阅有关docker build命令的部分,了解更多关于构建镜像的详细信息。
要为镜像创建标签,应运行以下命令:
admin@myhome:~$ docker image tag <image> <new_image_name>
最后,你可以删除一个镜像:
admin@myhome:~$ docker image rm <image>
请参阅有关docker rmi命令的部分,它是此命令的别名。
另一种删除镜像的选项是docker image prune –命令。此命令将删除所有未使用的镜像(悬空镜像)。
docker exec
docker exec允许你在运行中的 Docker 容器中执行命令。基本语法如下:
docker exec CONTAINER COMMAND ARGUMENTS
在前面的示例中,术语的含义如下:
-
CONTAINER是要在其中运行命令的容器的名称或 ID -
COMMAND是要在容器中运行的命令 -
ARGUMENTS表示命令的任何附加参数(这是可选的)
例如,要在名为my_container的容器中运行ls命令,可以使用以下命令:
admin@myhome:~$ docker exec mycontainer ls
docker logs
docker logs用于获取 Docker 容器生成的日志:
docker logs CONTAINER_NAME_OR_ID
你可以传递给命令的其他选项如下:
-
--details, -a:显示日志提供的额外详细信息 -
--follow, -f:跟踪日志输出 -
--since, -t:仅显示自某个日期以来的日志(例如,2013-01-02T13:23:37) -
--tail, -t:从日志的末尾显示的行数(默认all)
它的使用示例如下:
admin@myhome:~$ docker logs CONTAINER_ID
docker rm
docker rm用于删除一个或多个 Docker 容器:
docker rm CONTAINER_NAME_OR_ID
它的使用示例如下:
admin@myhome:~$ docker rm CONTAINER_ID
要列出所有容器,请使用docker ps -a命令。
docker rmi
从本地拉取或构建的镜像可能会占用大量磁盘空间,因此检查和删除未使用的镜像是很有用的。docker rmi用于删除一个或多个 Docker 镜像。
以下是它的用法:
docker rmi IMAGE
它的使用示例如下:
admin@myhome:~$ docker rmi IMAGE_ID
docker network
docker network命令用于管理 Docker 网络。除了常见的操作(创建、删除和列出),还可以将运行中的 Docker 容器连接(和断开连接)到不同的网络。
也可以通过选择专用的网络插件来扩展 Docker。这里有多个选择,我们只列出一些网络插件及其简短描述。插件也可以用于更高级的设置,如 Kubernetes 集群:
-
Contiv-VPP (
contivpp.io/) 使用向量数据包处理(VPP)技术,提供一个高效、可扩展和可编程的容器网络解决方案,适用于企业和服务提供商环境,在这些环境中,高性能和可扩展的网络是必需的。 -
Weave Net (
www.weave.works/docs/net/latest/overview/) 允许容器之间进行通信,无论它们在哪个主机上运行。Weave Net 创建一个跨多个主机的虚拟网络,使得容器能够以高可用性、冗余和负载均衡的方式进行部署。 -
Calico (
www.tigera.io/tigera-products/calico/) 是最知名的插件之一。它采用纯 IP 驱动的网络方式,提供了简洁性和可扩展性。Calico 允许管理员定义和执行网络策略,例如根据源、目的地和端口允许或拒绝特定的流量。Calico 设计用于大规模部署,并支持虚拟和物理网络。
使用docker network命令时的常见操作如下:
- 创建新网络:
admin@myhome:~$ docker network create mynetwork
- 检查网络:
admin@myhome:~$ docker network inspect mynetwork
- 删除网络:
admin@myhome:~$ docker network rm mynetwork
- 列出网络:
admin@myhome:~$ docker network ls
- 连接容器到网络:
admin@myhome:~$ docker network connect mynetwork CONTAINER_NAME_OR_ID
- 从网络中断开容器:
admin@myhome:~$ docker network disconnect mynetwork CONTAINER_NAME_OR_ID
docker volume
docker volume命令用于管理 Docker 中的卷。通过这个命令,你可以列出可用的卷,清理未使用的卷,或者创建一个以便稍后使用。
Docker 支持多种卷驱动程序,包括以下几种:
-
local:默认驱动程序;使用 UnionFS 将数据存储在本地文件系统上 -
awslogs:使应用程序生成的日志能够存储在 Amazon CloudWatch Logs 中 -
cifs:允许你将 SMB/CIFS(Windows)共享挂载为 Docker 卷 -
GlusterFS:将 GlusterFS 分布式文件系统挂载为 Docker 卷 -
NFS:将网络文件系统(NFS)挂载为 Docker 卷
还有许多其他驱动程序可供选择。可用驱动程序的列表可以在官方 Docker 文档中找到:docs.docker.com/engine/extend/legacy_plugins/。
以下是其使用示例:
-
dockervolume ls -
docker volumecreate <volume-name> -
docker volumeinspect <volume-name> -
docker volumerm <volume-name> -
docker volumecreate myvolumedocker run -vmyvolume:/opt/data alpine
在本节中,我们已经学习了如何使用命令行界面与所有 Docker 组件进行交互。到目前为止,我们一直在使用公开可用的 Docker 镜像,但现在是时候学习如何构建自己的镜像了。
Dockerfile
Dockerfile 本质上是一个具有预定结构的文本文件,包含一组构建 Docker 镜像的指令。Dockerfile 中的指令指定了从哪个基础镜像开始(例如,Ubuntu 20.04),安装哪些软件以及如何配置镜像。Dockerfile 的目的是自动化构建 Docker 镜像的过程,以便镜像能够轻松重现和分发。
Dockerfile 的结构是一个命令列表(每行一个命令),Docker(准确来说是containerd)使用这些命令来构建镜像。每个命令在镜像的 UnionFS 中创建一个新层,最终生成的镜像是所有层的联合体。我们管理的层越少,最终生成的镜像就越小。
Dockerfile 中最常用的命令如下:
-
FROM -
COPY -
ADD -
EXPOSE -
CMD -
ENTRYPOINT -
RUN -
LABEL -
ENV -
ARG -
VOLUME -
USER -
WORKDIR
你可以在官方 Docker 文档网站上找到完整的命令列表:docs.docker.com/engine/reference/builder/。
让我们逐一了解上述列表,理解每个命令的作用以及何时使用它。
FROM
Dockerfile 从FROM命令开始,该命令指定要从哪个基础镜像开始:
FROM ubuntu:20.04
你还可以使用as关键字为此构建命名,并跟上自定义名称:
FROM ubuntu:20.04 as builder1
docker build 将尝试从公共 Docker Hub 注册表下载 Docker 镜像,但也可以使用其他注册表,或使用私有注册表。
COPY 和 ADD
COPY命令用于将文件或目录从主机复制到容器文件系统。以下是一个示例:
COPY . /var/www/html
你还可以使用ADD命令将文件或目录添加到 Docker 镜像中。ADD具有比COPY更多的功能。它可以自动解压 TAR 压缩文件,并检查源字段中是否有 URL,如果找到 URL,它将从该 URL 下载文件。最后,ADD命令还具有--chown选项,用于设置目标中文件的所有权。通常情况下,建议在大多数情况下使用COPY,只有在需要ADD提供的附加功能时才使用它。
EXPOSE
Dockerfile 中的 EXPOSE 命令告知 Docker 容器在运行时监听指定的网络端口。它并不会实际发布端口,而是用来向用户提供容器计划发布哪些端口的信息。
例如,如果一个容器在端口 80 上运行 Web 服务器,你应该在 Dockerfile 中包含以下行:
EXPOSE 80
你可以指定端口是监听 TCP 还是 UDP – 在指定端口号后,添加一个斜杠和 TCP 或 UDP 关键字(例如,EXPOSE 80/udp)。如果只指定端口号,则默认使用 TCP。
EXPOSE 命令并不实际发布端口。要使端口可用,你需要在运行 docker run 命令时使用 -p 或 --publish 选项来发布端口:
admin@myhome:~$ docker run -p 8080:80 thedockerimagename:tag
这将把主机上的端口 8080 映射到容器中的端口 80,这样所有到达端口 8080 的流量都会被转发到容器中运行的 Web 服务器的端口 80。
不管 EXPOSE 命令如何,你都可以在运行容器时发布不同的端口。EXPOSE 用于告知用户容器计划发布哪些端口。
ENTRYPOINT 和 CMD
接下来是 ENTRYPOINT 命令,它在 Dockerfile 中指定容器启动时应始终执行的命令。它不能通过传递给 docker run 命令的任何命令行选项来覆盖。
ENTRYPOINT 命令用于将容器配置为可执行文件。它类似于 CMD 命令,但用于配置容器作为可执行文件运行。通常用于指定容器启动时应运行的命令,例如命令行工具或脚本。
例如,如果你有一个运行 Web 服务器的容器,你可能会使用 ENTRYPOINT 命令来指定启动 Web 服务器的命令:
ENTRYPOINT ["nginx", "-g", "daemon off;"]
如果你想使用不同的参数运行容器,可以使用 CMD 命令设置默认参数,这些参数可以在容器启动时被覆盖:
ENTRYPOINT ["/usr/bin/python"]
CMD ["main.py","arg1","arg2"]
CMD 用于指定从镜像启动容器时应执行的命令。以下是一个例子:
CMD ["nginx", "-g", "daemon off;"]
这里的经验法则是,如果你希望应用程序接受自定义参数,你可以使用 ENTRYPOINT 启动一个进程,使用 CMD 向其传递参数。这样,你可以通过命令行传递不同的选项,使进程更灵活地执行不同的操作。
RUN
Dockerfile 中的 RUN 命令用于在容器内执行命令。每次执行时,它都会在镜像中创建一个新层。
RUN 命令用于安装软件包、创建目录、设置环境变量,以及执行设置容器内环境所需的任何其他操作。
例如,你可以使用 RUN 命令来安装一个软件包:
RUN apt-get update && apt-get install -y python3 python3-dev
你可以使用 RUN 命令来创建一个目录:
RUN mkdir /var/www
你可以使用 RUN 命令设置环境变量:
RUN echo "export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> ~/.bashrc
值得注意的是,Dockerfile 中 RUN 命令的顺序很重要,因为每个命令都会在镜像中创建一个新层,最终的镜像是所有层的联合体。因此,如果你期望某些软件包在后续过程中安装,必须在使用它们之前进行安装。
LABEL
LABEL 命令用于向镜像添加元数据。它基本上是将数据的键值对添加到镜像中。这些数据可以用于存储信息,如镜像的版本、维护者以及你在组织中可能需要的其他相关信息。以下是该命令的示例:
LABEL maintainer="Chris Carter <chcarter@your.comain.tld>"
你也可以在一行中添加多个标签:
LABEL maintainer="Chris Carter <chcarter@your.comain.tld>" version="0.2"
使用 docker inspect 命令可以查看添加到镜像上的标签:
admin@myhome:~$ docker inspect --format='{{json .Config.Labels}}' <image_name_or_ID>
使用 LABEL 命令向镜像添加元数据可以帮助用户理解镜像的目的或他们应该向谁询问细节,并有助于管理镜像。
ENV 和 ARG
ENV 命令用于以以下格式设置环境变量:
ENV <key>=<value>
另一方面,ARG 命令用于定义构建时变量。这些变量可以通过 --build-arg 标志传递给 docker build 命令,并且它们的值可以在 Dockerfile 中使用。
ARG 命令用于定义类似于 ENV 格式的构建时变量:
ARG <name>[=<default value>]
ARG 命令创建的变量仅在构建过程中可访问,而 ENV 命令创建的环境变量则可以被容器内运行的所有进程访问。
在下一章节,我们将详细介绍 Docker 镜像的构建过程,其中 ARG 和 ENV 被一起使用,以便在构建阶段之间持久化 ENV 变量。
VOLUME
另一个命令是 VOLUME。通过它,你可以配置容器在特定位置为卷创建挂载点。卷是一种将数据存储在容器文件系统外部的方式,这意味着即使容器被删除或重新创建,数据仍然可以保持。以下是该命令:
VOLUME /opt/postgresql_data
使用以下方式指定多个目录:
VOLUME /opt/postgresql_data /opt/postgresql_xferlog
或者,以下方式也有效:
VOLUME ["/opt/postgresql_data", "/opt/postgresql_xferlog"]
如果目录中有标记为 volume 的数据,当使用 docker run 命令运行 Docker 时,将使用该目录的内容创建一个新的卷。这样,即使容器被终止或以其他方式停止,确保在该 Docker 容器运行期间创建的数据不会丢失。这对于数据库尤为重要,正如我们在前面的示例中所建议的那样。
USER
Dockerfile 中的 USER 命令用于设置容器运行时的默认用户。默认情况下,容器以 root 用户身份运行;建议以没有 root 权限的自定义用户身份运行容器。
USER 命令用于设置容器运行时的用户,且可以选择设置用户所属的组。例如,你可以使用 USER 命令以 webserver 用户身份运行容器:
USER webserver
你还可以指定用户和组:
USER webserver:webserver
也可以设置用户 ID 和组 ID,而不是使用名称:
USER 1001:1001
USER 命令仅设置容器的默认用户,但你在运行容器时可以覆盖该设置:
admin@myhome:~$ docker run --user=root webserver-image
出于安全原因,以非 root 用户身份运行应用程序是最佳实践。如果攻击者获得容器访问权限,这样可以限制潜在的损害,因为运行具有完全权限的进程在宿主机上也会以相同的 UID(此处为 root)运行。
WORKDIR
Dockerfile 中的 WORKDIR 命令用于设置容器的当前工作目录。工作目录是容器文件系统中所有后续 RUN、CMD、ENTRYPOINT、COPY 和 ADD 命令执行的位置。
你可以使用 WORKDIR 命令将工作目录设置为 /usr/local/app:
WORKDIR /usr/local/app
使用 WORKDIR 时,你在使用其他命令时无需设置文件的完整路径,还可以将应用程序位置参数化(使用 ARG 或 ENV)。
现在我们已经熟悉了 Dockerfile 以及如何构建 Docker 镜像,接下来了解如何以某种方式存储这个新镜像是很有用的。Docker 镜像注册表正是用于这个目的。我们将在下一节讨论注册表。
Docker 镜像注册表
Docker 镜像注册表用于托管 Docker 镜像。Docker 镜像通过标签组织,用户可以访问并下载这些标签。使用这些镜像可以在宿主机上创建并运行容器。镜像仓库可以托管在本地或远程服务器上,例如 Docker Hub,这是 Docker 提供的公共仓库。你还可以创建自己的私有镜像仓库,在组织内共享和分发镜像。
当你从 Docker 镜像仓库拉取镜像时,镜像由多个层组成。每一层代表 Dockerfile 中用于构建镜像的一条指令。这些层按顺序叠加在一起,创建最终的镜像。每一层是只读的,并具有唯一的 ID。
得益于 UnionFS,Docker 注册表在多个镜像和容器之间共享公共层,从而减少所需的磁盘空间。当容器修改文件时,它会在基础镜像上方创建一个新层,而不是修改基础镜像中的文件。这使得回滚到容器的先前状态变得容易,并且使镜像具有很高的可移植性。
根据你使用的云解决方案(例如 AWS 的 ECR 或 GCP 的 Google Container Registry)或 SaaS 解决方案(Docker Hub 是最受欢迎的 - hub.docker.com),你可以使用多个镜像仓库。也有一些开放源代码的授权解决方案可用:
-
Harbor:可以在
goharbor.io/访问。 -
Portus:可以在
port.us.org/访问。 -
Docker Registry:可以在
docs.docker.com/registry/访问。 -
Project Quay:可以在
quay.io访问,也可以在 GitHub 上访问:github.com/quay/quay
在本节中,我们已经了解了 Docker 镜像注册表,便于将 Docker 镜像存储在远程位置。在下一节中,我们将深入探讨 Docker 网络及其扩展。
Docker 网络
Docker 网络有四种类型:none、bridge、host 和 overlay。
Bridge是 Docker 中的默认网络模式。处于同一桥接网络中的容器可以相互通信。简而言之,它创建了一个虚拟网络,在该网络中,容器被分配了 IP 地址,并可以通过这些地址进行通信,而网络之外的任何事物都无法访问这些地址。在Host网络中,容器使用主机的网络栈。这意味着容器共享主机的 IP 地址和网络接口。
Overlay模式允许你创建一个跨越多个 Docker 主机的虚拟网络。不同主机上的容器可以相互通信,就像它们在同一主机上一样。它在运行 Docker Swarm 时非常有用。
使用 Docker 命令行,你可以创建任何这些类型的自定义网络。
None 网络
None 网络是 Docker 中的一种特殊网络模式,它禁用容器的所有网络功能。当容器在none网络模式下运行时,它无法访问任何网络资源,也不能与其他容器或主机进行通信。
要在none网络模式下运行容器,你可以在运行docker run命令时使用--network none选项。
例如,要在none网络模式下启动一个运行nginx镜像的容器,你可以运行以下命令:
admin@myhome:~$ docker run --network none -d ubuntu:20
None网络适用于运行不需要任何网络连接的工作负载,例如在连接的卷中处理数据。
桥接模式
在创建容器时使用bridge 模式时,还会创建一个虚拟接口,并将其附加到虚拟网络。每个容器将被分配一个唯一的 IP 地址,允许它与其他容器和主机进行通信。
主机机器充当容器的网关,在容器与外部网络之间路由流量。当一个容器想要与另一个容器或主机机器通信时,它将数据包发送到虚拟网络接口。虚拟网络接口然后将数据包路由到正确的目的地。
默认情况下,它是一个172.17.0.0/16网络,并且与您机器中的桥接设备docker0连接。在这个网络中,容器与主机机器之间的所有流量都是允许的。
如果在执行 docker run 命令时未使用 --network 选项选择网络,则所有容器都将附加到默认的桥接网络。
您可以使用以下命令列出所有可用的网络:
admin@myhome:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
6c898bde2c0c bridge bridge local
926b731b94c9 host host local
b9f266305e10 none null local
要获取更多关于网络的信息,您可以使用以下命令:
admin@myhome:~$ docker inspect bridge
[
{
"Name": "bridge",
"Id": "6c898bde2c0c660cd96c3017286635c943adcb152c415543373469afa0aff13a",
"Created": "2023-01-26T16:51:30.720499274Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
让我们进入 HOST 模式。
主机模式
在 主机网络模式 下,容器共享主机的网络栈和网络接口。这意味着容器使用您的机器的 IP 地址和网络设置,并且可以直接访问与其运行的机器相同的网络资源,包括其他容器。
运行在主机网络模式下的容器也可以直接监听主机机器的端口(绑定到它)。
主机网络模式的主要优点之一是提供更好的性能,因为容器不需要经过额外的网络栈。
与其他网络模式相比,这种模式的安全性较低,因为容器可以直接访问主机的网络资源,并能够监听主机接口上的连接。
覆盖网络
覆盖网络是由管理节点创建的,管理节点负责维护网络配置并管理工作节点的成员资格。管理节点创建一个虚拟网络交换机,并为网络中的每个容器分配 IP 地址。
每个工作节点运行 Docker 引擎和容器网络驱动程序,负责将该主机上的容器连接到虚拟网络交换机。容器网络驱动程序还确保数据包被正确封装并路由到正确的目的地。
当一个主机上的容器想要与另一个主机上的容器通信时,它将数据包发送到虚拟网络交换机。交换机然后将数据包路由到正确的主机,容器网络驱动程序在目标主机上解封装数据包并将其传送到目标容器。
覆盖网络使用 虚拟扩展局域网(VXLAN)协议来封装 IP 数据包,并使得在多个主机之间创建二层网络成为可能。
总结
在本章中,我们介绍了现代 DevOps 引领的基础设施的主要组成部分之一,那就是容器。我们描述了最突出的容器技术——Docker。我们还介绍了运行 Docker 容器和构建自定义容器的基础知识。在下一章中,我们将基于这些知识,介绍更高级的 Docker 主题。
417

被折叠的 条评论
为什么被折叠?



