Kubernetes-网络指南-全-
Kubernetes 网络指南(全)
原文:
zh.annas-archive.org/md5/3867b7bdb6d63766bfd7a42801293ed1译者:飞龙
前言
只是另一个封包
自从第一台两台计算机通过电缆连接在一起后,网络已经成为我们基础设施中至关重要的一部分。现在的网络具有多层复杂性来支持多种用例,而像 Mesosphere 和 Kubernetes 这样的项目和容器的出现并没有改变这一点。尽管 Kubernetes 的贡献者们试图为开发者们抽象出这些网络复杂性,但计算机科学就是这样,抽象层叠加抽象。Kubernetes 及其网络 API 是另一个抽象,使得部署应用程序变得更加简单和快速。那么,对于那些需要管理 Kubernetes 的管理员呢?本书旨在揭开 Kubernetes 引入的抽象背后的神秘面纱,指导管理员穿越复杂的层级,并帮助你意识到 Kubernetes 不仅仅是另一个封包。
本书的受众
根据 451 Research,全球应用容器市场预计从 2019 年的 21 亿美元增长到 2022 年的 42 亿美元。容器市场的这种爆炸性增长凸显了 IT 专业人员在部署、管理和故障排除容器方面的需求。
本书旨在供新的网络、Linux 或集群管理员从头到尾阅读,同时也可供更有经验的 DevOps 工程师跳转到需要提升技能的特定主题使用。网络、Linux 和集群管理员需要熟悉如何在规模上操作 Kubernetes。
在本书中,读者将找到导航运行 Kubernetes 网络所需复杂层级的信息。本书将揭示 Kubernetes 引入的抽象,以便开发者在本地、云端和托管服务上的部署中有类似的体验。负责生产集群操作和网络运行时间的工程师可以使用本书来弥补他们在这些抽象知识方面的知识差距。
What You Will Learn
通过本书的最后,读者将了解以下内容:
-
Kubernetes 网络模型
-
容器网络接口(CNI)项目及如何为其集群选择 CNI 项目
-
驱动 Kubernetes 的网络和 Linux 基元
-
驱动 Kubernetes 群集的抽象之间的关系
此外,读者还将能够做到以下几点:
-
部署和管理适用于 Kubernetes 集群的生产规模网络
-
在 Kubernetes 集群内部解决与网络相关的应用问题
本书中使用的约定
本书使用以下排版惯例:
斜体
指示新术语、网址、电子邮件地址、文件名和文件扩展名。
Constant width
用于程序列表,以及段落中引用程序元素,如变量或函数名,数据库,数据类型,环境变量,语句和关键字。
等宽粗体
显示用户应该按字面输入的命令或其他文本。
等宽斜体
显示应替换为用户提供的值或由上下文确定的值的文本。
小贴士
这个元素表示提示或建议。
注意
这个元素表示一般的注释。
警告
这个元素表示警告或注意事项。
使用代码示例
附加材料(代码示例、练习等)可在https://github.com/strongjz/Networking-and-Kubernetes下载。
如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您要复制大量代码,否则无需征得我们的许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发 O’Reilly 图书的示例需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书的大量示例代码整合到您产品的文档中需要许可。
我们感谢,但通常不需要署名。署名通常包括标题,作者,出版社和 ISBN。例如:“Networking and Kubernetes by James Strong and Vallery Lancey (O’Reilly). Copyright 2021 Strongjz tech and Vallery Lancey, 978-1-492-08165-4.”
如果您认为您对代码示例的使用超出了合理使用范围或上述许可,请随时通过permissions@oreilly.com与我们联系。
O’Reilly 在线学习
注意
超过 40 年来,O’Reilly Media为公司的成功提供技术和商业培训、知识和见解。
我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深入学习路径、交互式编码环境以及 O’Reilly 和 200 多家其他出版商的大量文本和视频。有关更多信息,请访问http://oreilly.com。
如何联系我们
请将有关本书的评论和问题发送给出版商:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-998-9938(美国或加拿大)
-
707-829-0515(国际或本地)
-
707-829-0104(传真)
我们为这本书制作了一个网页,列出勘误、示例和任何额外信息。您可以访问这个页面https://oreil.ly/NetKubernetes。
发送电子邮件至bookquestions@oreilly.com,评论或询问关于本书的技术问题。
关于我们的书籍和课程的新闻和信息,请访问http://oreilly.com。
在 Facebook 上找到我们:http://facebook.com/oreilly。
在 Twitter 上关注我们:http://twitter.com/oreillymedia。
在 YouTube 上关注我们:http://youtube.com/oreillymedia。
致谢
作者们要感谢 O’Reilly Media 团队在帮助他们完成他们的第一本书的过程中的支持。Melissa Potter 在推动这一项目进展中功不可没。我们也要感谢 Thomas Behnken 在他的 Azure 专业知识方面给予的帮助。
詹姆斯: 卡伦,感谢你对我的信任,感谢你在他不相信自己时帮助他相信自己。温克,你是我进入这个领域工作的原因,我会永远感激你。安妮,我在学习英语时走了很长一段路,应该用大写字母来感谢你。詹姆斯还要感谢他生活中所有支持他的其他老师和教练。
瓦莱瑞: 我要感谢 SIG-Network 中的友好面孔,帮助我开始进入上游 Kubernetes。
最后,作者们要感谢 Kubernetes 社区;没有他们,这本书就不会存在。我们希望这能帮助所有希望采用 Kubernetes 的工程师进一步扩展知识。
第一章:网络介绍
“有罪直到证明无罪。” 这是网络及其监督者的口头禅。 在本章的开篇中,我们将浏览网络技术和标准的发展,简要介绍网络的主要理论,并介绍我们的 Golang Web 服务器,这将是本书中 Kubernetes 和云中网络示例的基础。
让我们从…开始。
网络历史
我们今天所知的互联网是庞大的,海量电缆横跨海洋和山脉,连接城市的延迟低于以往任何时候。 巴雷特·里昂(Barrett Lyon)的“Mapping the Internet”,显示在图 1-1,展示了它的真实广度。 那幅图像展示了构成互联网的网络之间的所有连接。 网络的目的是从一个系统向另一个系统交换信息。 这对于一个分布式全球系统来说是一个巨大的要求,但互联网并不总是全球化的; 它始于一个概念模型,随着时间的推移逐渐建立起来,成为里昂视觉上令人惊叹的艺术作品中的庞然大物。 学习网络时需要考虑许多因素,例如最后一英里,客户家庭与其互联网服务提供商网络之间的连接,一直到互联网地缘政治格局的扩展。 互联网已融入我们社会的结构中。 在本书中,我们将讨论网络的运作方式以及 Kubernetes 如何为我们抽象它们。

图 1-1. 巴雷特·里昂,“Mapping the Internet”,2003
表格 1-1 简要概述了我们深入探讨前的网络历史。
表格 1-1. 网络历史简要概述
| 年份 | 事件 |
|---|---|
| 1969 | ARPANET 首次连接测试 |
| 1969 | Telnet 1969 请求评论(RFC)15 起草 |
| 1971 | FTP RFC 114 起草 |
| 1973 | FTP RFC 354 起草 |
| 1974 | 由文特·瑟夫(Vint Cerf)、约根·达拉尔(Yogen Dalal)和卡尔·阳光(Carl Sunshine)起草的 TCP RFC 675 发布 |
| 1980 | 开放系统互联模型的开发开始 |
| 1981 | IP RFC 760 起草 |
| 1982 | NORSAR 和伦敦大学学院离开 ARPANET,并开始在 SATNET 上使用 TCP/IP |
| 1984 | ISO 7498 开放系统互联模型(OSI 模型)发布 |
| 1991 | 在阿尔·戈尔(Al Gore)的帮助下通过了国家信息基础设施(NII)法案 |
| 1991 | Linux 首个版本发布 |
| 2015 | Kubernetes 首个版本发布 |
在其最早期的形式中,网络是由政府运营或赞助的;在美国,国防部(DOD)赞助了先进研究项目署网络(ARPANET),早在阿尔·戈尔进入政界之前,这对后面将要讨论的话题非常重要。1969 年,ARPANET 在加州大学洛杉矶分校、斯坦福研究所增强研究中心、加州大学圣塔芭芭拉分校和犹他大学计算机学院部署。这些节点之间的通信直到 1970 年才完成,当时开始使用网络控制协议(NCP)。NCP 导致了像 Telnet 和文件传输协议(FTP)等第一个计算机对计算机协议的开发和使用。
ARPANET 和 NCP 的成功,第一个支持 ARPANET 的协议,导致了 NCP 的失败。它无法满足网络和连接的各种网络的需求。1974 年,文特·瑟夫(Vint Cerf)、约根·达拉尔(Yogen Dalal)和卡尔·阳光(Carl Sunshine)开始起草 RFC 675,用于传输控制协议(TCP)。(您将在几段后了解更多关于 RFC 的信息。)TCP 随后成为了网络连接的标准。TCP 允许在不同类型的网络之间交换数据包。1981 年,《Internet Protocol》(IP),在 RFC 791 中定义,帮助将 TCP 的职责拆分为一个单独的协议,增加了网络的模块化。在接下来的几年中,包括国防部在内的许多组织采用了 TCP 作为标准。到 1983 年 1 月,TCP/IP 已成为 ARPANET 上唯一批准的协议,取代了早期的 NCP,因为它的多功能性和模块化。
一个竞争的标准组织,国际标准化组织(ISO),开发并发布了 ISO 7498,“开放系统互联参考模型”,详细描述了 OSI 模型。随着其出版,也推出了支持它的协议。不幸的是,OSI 模型协议从未获得广泛应用,并输给了 TCP/IP 的流行。然而,OSI 模型仍然是理解网络分层方法的优秀学习工具。
1991 年,阿尔·戈尔发明了互联网(实际上是他帮助通过了国家信息基础设施[NII]法案),这有助于创建了互联网工程任务组(IETF)。如今,互联网的标准由 IETF 管理,这是一个由网络领域的领先专家和公司(如思科和瞻博)组成的开放联盟。RFC 由互联网协会和互联网工程任务组发布。RFC 通常由个人或工程师和计算机科学家小组撰写,详细描述了他们的过程、操作和互联网功能的应用。
IETF 的 RFC 有两种状态:
提议标准
协议规范已经得到足够的社区支持,被视为标准。设计稳定且广为人知。提议的标准可以部署、实施和测试。然而,可能会被进一步考虑。
互联网标准
根据 RFC 2026:“总的来说,互联网标准是一个稳定的规范,技术上具备了充分的理解,有多个独立且可互操作的实施经验,享有显著的公众支持,在互联网的某些部分被认为是有用的。”
注意
草案标准是 2011 年停止使用的第三类分类。
数千种互联网标准定义了如何实施网络的各个方面的协议,包括无线、加密和数据格式等。每个标准都是由开源项目的贡献者和大型组织如思科私下实施的。
自从最初的连接测试以来,近 50 年来发生了很多事情。网络变得更加复杂和抽象,所以让我们从 OSI 模型开始。
OSI 模型
OSI 模型是描述两个系统如何在网络上通信的概念框架。OSI 模型将跨网络发送数据的责任分解为多个层次。这对于教育目的来说描述了每个层次之间的关系及数据如何在网络上传输。有趣的是,它本来是一套用于驱动网络的协议套件,但输给了 TCP/IP。
以下是概述 OSI 模型和协议的 ISO 标准:
-
ISO/IEC 7498-1,“基本模型”
-
ISO/IEC 7498-2,“安全架构”
-
ISO/IEC 7498-3,“命名与寻址”
-
ISO/IEC 7498-4,“管理框架”
ISO/IEC 7498-1 描述了 OSI 模型试图传达的内容:
5.2.2.1 开放系统互联参考模型中的基本结构技术是分层。根据这种技术,每个开放系统被视为由一组有序的(N)子系统逻辑组成…相邻的(N)子系统通过它们的公共边界进行通信。同等级(N)的(N)子系统共同形成开放系统中的(N)层。每个开放系统中有且仅有一个(N)子系统用于第 N 层。一个(N)子系统由一个或多个(N)实体组成。每个(N)层中都存在实体。同一(N)层中的实体称为对等(N)实体。请注意,最高层没有(N+1)层在其上面,最低层没有(N-1)层在其下面。
OSI 模型的描述是一种复杂而准确的方式,类似于蛋糕或洋葱,来表达网络具有层次结构。OSI 模型将网络的责任分解为七个不同的层次,每个层次具有不同的功能,以帮助从一个系统向另一个系统传输信息,如图 1-2 所示。每一层将信息从其下面的层次封装起来;这些层次是应用、表示、会话、传输、网络、数据链路和物理层。在接下来的几页中,我们将详细介绍每一层的功能以及它们如何在两个系统之间发送数据。

图 1-2. OSI 模型层
每一层从前一层接收数据并封装它以创建其协议数据单元(PDU)。PDU 用于描述每一层的数据。PDU 也是 TCP/IP 的一部分。会话层的应用程序被视为 PDU 的“数据”,为通信准备应用程序信息。传输使用端口来区分本地系统上负责数据的进程。网络层的 PDU 是数据包。数据包是在网络之间路由的不同数据片段。数据链路层是帧或段。每个数据包被分割成帧,检查错误并通过本地网络发送。物理层以比特形式在介质上传输帧。接下来我们将详细介绍每一层:
应用
应用层是 OSI 模型的顶层,也是最终用户每天与之交互的层。这一层不是实际应用程序所在的地方,但它为像 web 浏览器或 Office 365 这样使用它的应用程序提供接口。最大的接口是 HTTP;你可能正在通过 O'Reilly 的 web 服务器上的网页阅读这本书。我们每天使用的应用层的其他例子包括 DNS、SSH 和 SMTP。这些应用程序负责显示和安排通过网络请求和发送的数据。
表示
该层通过在应用程序和网络格式之间进行转换提供了对数据表示的独立性。它可以称为语法层。该层允许两个系统使用不同的数据编码并仍然在它们之间传递数据。加密也在此层进行,但这是一个更复杂的故事,我们将其保留给“TLS”。
会话
会话层负责连接的双工性,换句话说,即同时发送和接收数据。它还建立了执行会话检查点、挂起、重新启动和终止会话的过程。它建立、管理和终止本地和远程应用程序之间的连接。
传输
传输层在应用程序之间传输数据,为上层提供可靠的数据传输服务。传输层通过流量控制,分段和重新组合以及错误控制来控制给定连接的可靠性。一些协议是状态和连接导向的。该层跟踪段并重新传输失败的段。它还确认成功的数据传输并在没有错误发生时发送下一个数据。TCP/IP 在这一层有两个协议:TCP 和用户数据报协议(UDP)。
网络
网络层实现从一个网络上的主机到另一个网络上的主机的可变长度数据流的传输方式,并保持服务质量。网络层执行路由功能,并可能在报告传递错误时执行分段和重组。路由器在这一层操作,通过相邻网络发送数据。几个管理协议属于网络层,包括路由协议,组播组管理,网络层信息,错误处理和网络层地址分配,我们将在"TCP/IP"中进一步讨论。
数据链路
此层负责同一网络上的主机到主机的传输。它定义了创建和终止两个设备之间连接的协议。数据链路层在网络主机之间传输数据,并提供检测和可能纠正物理层错误的手段。数据链路帧作为第 2 层的 PDU 不会跨越本地网络的边界。
物理
物理层通过插入交换机的以太网线缆来进行可视化表示。这一层将数字比特形式的数据转换为电气、无线电或光信号。可以将这一层视为物理设备,如电缆、交换机和无线接入点。该层还定义了电线信号协议。
注意
有许多助记符可以记住 OSI 模型的层次;我们最喜欢的是"All People Seem To Need Data Processing"。
表 1-2 总结了 OSI 层。
表 1-2. OSI 层详细信息
| 层号 | 层名 | 协议数据单元 | 功能概述 |
|---|---|---|---|
| 7 | 应用 | 数据 | 高级 API 和应用协议,如 HTTP,DNS 和 SSH。 |
| 6 | 展示 | 数据 | 字符编码,数据压缩和加密/解密。 |
| 5 | 会话 | 数据 | 这里管理节点之间的连续数据交换:发送多少数据,何时发送更多。 |
| 4 | 传输 | 段,数据报 | 在网络上的端点之间传输数据段,包括分段,确认和多路复用。 |
| 3 | 网络 | 包 | 为网络上所有端点结构化和管理寻址,路由和流量控制。 |
| 2 | 数据链路 | 帧 | 在物理层连接的两个节点之间传输数据帧。 |
| 1 | 物理层 | 位 | 在介质上发送和接收位流。 |
OSI 模型分解了在两个主机之间通过网络发送数据包所需的所有必要功能。在 1980 年代末和 1990 年代初,它输给了 TCP/IP,在 DOD 和所有其他主要网络参与者中成为标准。ISO 7498 中定义的标准简要展示了当时大多数人认为复杂、低效且在某种程度上不可实施的实现细节。高层次上的 OSI 模型仍允许学习网络的人理解网络中的基本概念和挑战。此外,这些术语和功能在下一节涵盖的 TCP/IP 模型以及最终在 Kubernetes 抽象中使用。Kubernetes 服务根据其操作的层级来分解每个功能,例如层 3 的 IP 地址或层 4 的端口;您将在第四章中了解更多信息。接下来,我们将通过一个示例深入了解 TCP/IP 套件。
TCP/IP
TCP/IP 创建了一个异构网络,具有独立于操作系统和架构差异的开放协议。无论主机运行 Windows、Linux 还是其他操作系统,TCP/IP 都允许它们通信;TCP/IP 并不关心您在应用层运行 Apache 还是 Nginx 作为您的 Web 服务器。与 OSI 模型类似的责任分离使这一点成为可能。在图 1-3 中,我们比较了 OSI 模型和 TCP/IP。

图 1-3. OSI 模型与 TCP/IP 的比较
这里我们详细探讨了 OSI 模型与 TCP/IP 之间的差异:
应用层
在 TCP/IP 中,应用层包括在 IP 网络中进程间通信所使用的通信协议。应用层标准化通信,并依赖底层传输层协议建立主机间的数据传输。较低的传输层还管理网络通信中的数据交换。此层的应用程序在 RFC 中定义;在本书中,我们将继续以 HTTP,RFC 7231 作为应用层的示例。
传输
TCP 和 UDP 是传输层的主要协议,为应用程序提供主机到主机的通信服务。传输协议负责面向连接的通信、可靠性、流量控制和多路复用。在 TCP 中,窗口大小管理流量控制,而 UDP 不管理拥塞流,并且被认为是不可靠的;你可以在“UDP”中了解更多信息。每个端口标识负责处理来自网络通信的信息的主机进程。HTTP 使用 80 号端口进行非安全通信和 443 号端口进行安全通信。服务器上的每个端口标识其流量,并且发送方在本地生成一个随机端口来标识自己。管理端口号分配的机构是互联网分配号码管理局(IANA);共有 65535 个端口。
互联网
互联网或网络层负责在网络之间传输数据。对于传出的数据包,它选择下一个跳点主机,并通过将其传递给适当的链路层将其传输到该主机。一旦数据包被目标接收,互联网层将把数据包载荷传递给适当的传输层协议。
IP 根据最大传输单元(MTU)对数据包进行分段或重组;这是 IP 数据包的最大大小。IP 不保证数据包的正确到达。由于跨不同网络的数据包传输本质上是不可靠且容易出错的,这种负担在通信路径的端点而不是网络上。提供服务可靠性的功能位于传输层。校验和确保接收到的数据包信息准确,但此层不验证数据完整性。IP 地址用于标识网络上的数据包。
链路
TCP/IP 模型中的链路层包括仅在主机连接到的本地网络上运行的网络协议。数据包不会路由到非本地网络;这是互联网层的角色。以太网是该层的主要协议,并且主机通过链路层地址或通常是它们网络接口卡上的媒体访问控制地址来识别。一旦主机通过地址解析协议 9(ARP)确定了这些数据,远程网络上发送的数据由互联网层处理。该层还包括在两个互联网层主机之间移动数据包的协议。
物理层
物理层定义了用于网络的硬件组件。例如,物理网络层规定了通信介质的物理特性。TCP/IP 的物理层详细描述了硬件标准,如 IEEE 802.3,即以太网网络介质的规范。RFC 1122 的物理层有几种解释,与其他层一起包含在内;我们为完整性而添加这些。
在本书中,我们将使用最小的 Golang Web 服务器(也称为 Go)来展示从示例 1-1 开始的各种网络组件层次,从 Linux 系统调用的tcpdump到展示 Kubernetes 如何抽象系统调用的过程。本节将使用它来演示在应用程序、传输、网络和数据链路层各层发生的情况。
应用程序
如前所述,应用程序是 TCP/IP 堆栈中的最高层;在数据通过网络发送之前,用户与数据交互的地方。在我们的示例演示中,我们将使用超文本传输协议(HTTP)和简单的 HTTP 事务来演示 TCP/IP 堆栈的每一层发生了什么。
HTTP
HTTP 负责发送和接收超文本标记语言(HTML)文档——你知道,网页。互联网上大部分我们看到和做的事情都是通过 HTTP 完成的:亚马逊购物、Reddit 帖子和推特都使用 HTTP。客户端将向我们的最小 Golang Web 服务器(来自示例 1-1)发出 HTTP 请求,它将发送一个带有“Hello”文本的 HTTP 响应。该 Web 服务器在 Ubuntu 虚拟机中本地运行,以测试完整的 TCP/IP 堆栈。
注意
查看示例代码存储库获取完整说明。
示例 1-1. Go 中的最小 Web 服务器
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(w, "Hello")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe("0.0.0.0:8080", nil)
}
在我们的 Ubuntu 虚拟机中,我们需要启动我们的最小 Web 服务器;或者,如果您在本地安装了 Golang,您可以直接运行以下命令:
go run web-server.go
让我们分解每个 TCP/IP 堆栈层的请求。
cURL 是我们 HTTP 请求示例中的请求客户端。通常情况下,对于网页,客户端将是 Web 浏览器,但我们使用 cURL 来简化并显示命令行。
注意
cURL 旨在通过 URL 上传和下载指定的数据。它是一个客户端程序(c),用于请求 URL 上的数据并返回响应。
在示例 1-2 中,我们可以看到 cURL 客户端正在进行的每个 HTTP 请求的各个部分以及响应。让我们审视所有这些选项和输出是什么。
示例 1-2. 客户端请求
○ → curl localhost:8080 -vvv 
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 
> GET / HTTP/1.1 
> Host: localhost:8080 
> User-Agent: curl/7.64.1 
> Accept: */* 
>
< HTTP/1.1 200 OK 
< Date: Sat, 25 Jul 2020 14:57:46 GMT 
< Content-Length: 5 
< Content-Type: text/plain; charset=utf-8 
<
* Connection #0 to host localhost left intact Hello* Closing connection 0 
curl localhost:8080 -vvv:这是打开与本地运行的 Web 服务器localhost在 TCP 端口 8080 上的连接的curl命令。-vvv设置输出的冗长程度,以便我们可以看到请求中发生的一切。此外,TCP_NODELAY指令 TCP 连接发送数据而无需延迟,这是客户端可设置的众多选项之一。
已连接 到 localhost (::1) 端口 8080:成功了!cURL 连接到了本地主机上的 Web 服务器,并通过端口 8080 进行了连接。
Get / HTTP/1.1: HTTP 有多种方法用于检索或更新信息。在我们的请求中,我们正在执行 HTTP GET 来检索我们的“Hello”响应。斜杠是下一个部分,Uniform Resource Locator (URL),它指示我们将客户端请求发送到服务器的位置。此头部的最后部分是服务器正在使用的 HTTP 版本,1.1。
Host: localhost:8080: HTTP 有多个选项可用于发送关于请求的信息。在我们的请求中,cURL 进程已设置了 HTTP Host 头。客户端和服务器可以在 HTTP 请求或响应中传输信息。HTTP 头部包含其名称后跟一个冒号(:),然后是其值。
User-Agent: cURL/7.64.1: 用户代理是指示代表最终用户发出 HTTP 请求的计算机程序的字符串;在我们的情况下是 cURL。此字符串通常标识浏览器、其版本号和其主机操作系统。
Accept: */*: 此头部指示 Web 服务器客户端理解的内容类型。表 1-3 展示了可以发送的常见内容类型示例。
HTTP/1.1 200 OK: 这是我们请求的服务器响应。服务器用 HTTP 版本和响应状态码进行响应。服务器可能会有多种响应。状态码 200 表示响应成功。1XX 表示信息性响应,2XX 表示成功,3XX 表示重定向,4XX 表示请求存在问题,5XX 通常指服务器问题。
Date: Sat, July 25, 2020, 14:57:46 GMT: Date 头字段表示消息生成的日期和时间。发送者生成该值作为消息生成的大致日期和时间。
Content-Length: 5: Content-Length 头部指示发送到接收者的消息正文大小,以字节为单位;在我们的情况下,消息为 5 字节。
Content-Type: text/plain; charset=utf-8: Content-Type 实体头部用于指示资源的媒体类型。我们的响应指示返回的是一个纯文本文件,使用 UTF-8 编码。
Hello* Closing connection 0: 这打印出我们的 Web 服务器的响应并关闭 HTTP 连接。
表 1-3. HTTP 数据的常见内容类型
| 类型 | 描述 |
|---|---|
| application | 任何不明确属于其他类型的二进制数据。常见示例包括 application/json、application/pdf、application/pkcs8 和 application/zip。 |
| audio | 音频或音乐数据。例如 audio/mpeg 和 audio/vorbis。 |
| font | 字体/字型数据。常见示例包括 font/woff、font/ttf 和 font/otf。 |
| image | 包括位图和矢量图像的图像或图形数据,例如动画 GIF 或 APNG。常见示例包括 image/jpg、image/png 和 image/svg+xml。 |
| model | 用于 3D 对象或场景的模型数据。示例包括 model/3mf 和 model/vrml。 |
| text | 仅包含人类可读内容、源代码或文本数据的文本。例如 text/plain、text/csv 和 text/html。 |
| video | 视频数据或文件,例如 video/mp4。 |
这是每个 HTTP 请求中发生的简单视图。今天,单个网页在短短几秒钟内进行大量请求!这是一个针对集群管理员的简要示例,说明 HTTP(以及其他七层应用程序)的操作方式。我们将继续建立对 TCP/IP 协议栈各层如何完成这些请求的了解,然后介绍 Kubernetes 如何完成相同的请求。所有这些数据都在第 7 层格式化和设置选项,但真正的重活儿在 TCP/IP 协议栈的更低层完成,我们将在接下来的部分讨论它们。
传输
传输层协议负责面向连接的通信、可靠性、流量控制和复用;这在 TCP 中是大部分正确的。我们将在以下部分描述差异。我们的 Golang Web 服务器是一个使用 HTTP 的第 7 层应用程序;HTTP 依赖的传输层是 TCP。
TCP
正如前面提到的,TCP 是一种面向连接的可靠协议,它提供流量控制和复用。TCP 被认为是面向连接的,因为它通过连接的生命周期管理连接状态。在 TCP 中,窗口大小管理流量控制,不像 UDP 那样管理拥塞流。此外,UDP 是不可靠的,数据可能无序到达。每个端口标识负责处理网络通信信息的主机进程。TCP 被称为主机到主机层协议。为了识别主机上负责连接的进程,TCP 使用 16 位端口号标识段。HTTP 服务器使用非安全通信的众所周知端口 80 和使用传输层安全性(TLS)进行安全通信的端口 443。请求建立新连接的客户端在本地使用 0–65534 范围内的源端口。
要了解 TCP 如何执行复用,请回顾一个简单的 HTML 页面检索:
-
在 Web 浏览器中键入网页地址。
-
浏览器打开连接以传输页面。
-
浏览器为页面上的每个图像打开连接。
-
浏览器为外部 CSS 打开另一个连接。
-
每个连接使用不同的虚拟端口集。
-
所有页面资源同时下载。
-
浏览器重建页面。
让我们通过 TCP 段头部中提供的信息来了解 TCP 如何管理多路复用:
源端口(16 位)
这标识了发送端口。
目的端口(16 位)
这标识了接收端口。
序列号(32 位)
如果设置了 SYN 标志,则这是初始序列号。第一个数据字节的序列号以及对应 ACK 中的确认号是此序列号加 1。它还用于重新组装到达的乱序数据。
确认号(32 位)
如果设置了 ACK 标志,则此字段的值是发送方期望的下一个确认号。这确认接收到所有之前的字节(如果有的话)。每端的第一个 ACK 确认了另一端的初始序列号本身,但尚未发送任何数据。
数据偏移量(4 位)
这指定了 TCP 头部的大小,以 32 位字为单位。
保留(3 位)
这是未来使用的,并应设置为零。
标志(9 位)
TCP 头部定义了九个 1 位字段:
-
NS-ECN-nonce:掩蔽保护。
-
CWR:拥塞窗口减少;发送方降低了其发送速率。
-
ECE:ECN Echo;发送方接收到先前的拥塞通知。
-
URG:紧急;紧急指针字段有效,但很少使用。
-
ACK:确认;确认号字段有效,在建立连接后始终打开。
-
PSH:推送;接收方应尽快将此数据传递给应用程序。
-
RST:重置连接或连接中止,通常是因为错误。
-
SYN:同步序列号以启动连接。
-
FIN:段的发送方完成向其对等体发送数据。
注意
NS 位字段在 RFC 3540 中进一步解释,“使用非掩码的鲁棒显式拥塞通知(ECN)信号”。该规范描述了 ECN 的可选增强部分,以提高对标记数据包的恶意或意外掩蔽的鲁棒性。
窗口大小(16 位)
这是接收窗口的大小。
校验和(16 位)
校验和字段用于检查 TCP 头部的错误。
紧急指针(16 位)
这是从序列号指示的最后紧急数据字节的偏移量。
选项
变量 0-320 位,以 32 位为单位。
填充
TCP 头部填充用于确保 TCP 头部结束,并且数据从 32 位边界开始。
数据
这是在此段中发送的应用数据片段。
在图 1-4 中,我们可以看到所有提供关于 TCP 流的 TCP 段头部的元数据。

图 1-4. TCP 段头部
这些字段帮助管理两个系统之间的数据流。图 1-5 显示了 TCP/IP 协议栈的每个步骤如何将数据从一个主机上的一个应用程序通过在层 1 和层 2 通信的网络,发送到目标主机上。

图 1-5. tcp/ip 数据流
在接下来的部分中,我们将展示 TCP 如何利用这些字段通过三次握手初始化连接。
TCP 握手
TCP 使用三次握手(详见 图 1-6)来创建连接,通过沿途交换各种选项和标志的信息:
-
请求节点通过 SYN 包发送连接请求以启动传输。
-
如果接收节点正在监听发送方请求的端口,则接收节点将以 SYN-ACK 回复,确认已收到请求节点。
-
请求节点返回一个 ACK 包,交换信息并让对方知道节点可以相互发送信息。

图 1-6. TCP 三次握手
现在连接已经建立。数据可以通过物理介质传输,在网络之间路由,以找到本地目的地,但是终端如何知道如何处理信息?在本地和远程主机上,创建了一个套接字来跟踪这个连接。套接字只是通信的逻辑端点。在 第二章 中,我们将讨论 Linux 客户端和服务器如何处理套接字。
TCP 是一种有状态的协议,跟踪连接在其生命周期中的状态。连接状态取决于发送方和接收方在连接流中的协商。连接状态关注的是谁在 TCP 流中发送和接收数据。TCP 在 TCP 段头部使用 9 位 TCP 标志进行复杂的状态转换,您可以在 图 1-7 中看到。
TCP 连接状态包括:
LISTEN(服务器)
表示等待来自任何远程 TCP 和端口的连接请求
SYN-SENT(客户端)
表示在发送连接请求后等待匹配的连接请求确认
SYN-RECEIVED(服务器)
表示在收到并发送连接请求后等待确认连接请求的确认连接请求
ESTABLISHED(服务器和客户端都是)
表示已建立连接;接收到的数据可以传递给用户——连接传输阶段的中间状态
FIN-WAIT-1(服务器和客户端都是)
表示等待远程主机的连接终止请求
FIN-WAIT-2(服务器和客户端都是)
表示等待来自远程 TCP 的连接终止请求
CLOSE-WAIT(服务器和客户端都是)
表示等待本地用户的连接终止请求
CLOSING(服务器和客户端都是)
表示等待远程 TCP 的连接终止请求确认。
LAST-ACK(服务器和客户端均是如此)
表示等待先前发送到远程主机的连接终止请求的确认。
TIME-WAIT(无论是服务器还是客户端)
表示等待足够的时间以确保远程主机收到其连接终止请求的确认。
CLOSED(服务器和客户端均是如此)
表示没有任何连接状态

图 1-7. TCP 状态转换图
Example 1-3 是 Mac 的 TCP 连接示例,显示了它们的状态以及连接两端的地址。
示例 1-3. TCP 连接状态
○ → netstat -ap TCP
Active internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp6 0 0 2607:fcc8:a205:c.53606 g2600-1407-2800-.https ESTABLISHED
tcp6 0 0 2607:fcc8:a205:c.53603 g2600-1408-5c00-.https ESTABLISHED
tcp4 0 0 192.168.0.17.53602 ec2-3-22-64-157..https ESTABLISHED
tcp6 0 0 2607:fcc8:a205:c.53600 g2600-1408-5c00-.https ESTABLISHED
tcp4 0 0 192.168.0.17.53598 164.196.102.34.b.https ESTABLISHED
tcp4 0 0 192.168.0.17.53597 server-99-84-217.https ESTABLISHED
tcp4 0 0 192.168.0.17.53596 151.101.194.137.https ESTABLISHED
tcp4 0 0 192.168.0.17.53587 ec2-52-27-83-248.https ESTABLISHED
tcp6 0 0 2607:fcc8:a205:c.53586 iad23s61-in-x04..https ESTABLISHED
tcp6 0 0 2607:fcc8:a205:c.53542 iad23s61-in-x04..https ESTABLISHED
tcp4 0 0 192.168.0.17.53536 ec2-52-10-162-14.https ESTABLISHED
tcp4 0 0 192.168.0.17.53530 server-99-84-178.https ESTABLISHED
tcp4 0 0 192.168.0.17.53525 ec2-52-70-63-25..https ESTABLISHED
tcp6 0 0 2607:fcc8:a205:c.53480 upload-lb.eqiad..https ESTABLISHED
tcp6 0 0 2607:fcc8:a205:c.53477 text-lb.eqiad.wi.https ESTABLISHED
tcp4 0 0 192.168.0.17.53466 151.101.1.132.https ESTABLISHED
tcp4 0 0 192.168.0.17.53420 ec2-52-0-84-183..https ESTABLISHED
tcp4 0 0 192.168.0.17.53410 192.168.0.18.8060 CLOSE_WAIT
tcp6 0 0 2607:fcc8:a205:c.53408 2600:1901:1:c36:.https ESTABLISHED
tcp4 0 0 192.168.0.17.53067 ec2-52-40-198-7..https ESTABLISHED
tcp4 0 0 192.168.0.17.53066 ec2-52-40-198-7..https ESTABLISHED
tcp4 0 0 192.168.0.17.53055 ec2-54-186-46-24.https ESTABLISHED
tcp4 0 0 localhost.16587 localhost.53029 ESTABLISHED
tcp4 0 0 localhost.53029 localhost.16587 ESTABLISHED
tcp46 0 0 *.16587 *.* LISTEN
tcp6 56 0 2607:fcc8:a205:c.56210 ord38s08-in-x0a..https CLOSE_WAIT
tcp6 0 0 2607:fcc8:a205:c.51699 2606:4700::6810:.https ESTABLISHED
tcp4 0 0 192.168.0.17.64407 do-77.lastpass.c.https ESTABLISHED
tcp4 0 0 192.168.0.17.64396 ec2-54-70-97-159.https ESTABLISHED
tcp4 0 0 192.168.0.17.60612 ac88393aca5853df.https ESTABLISHED
tcp4 0 0 192.168.0.17.58193 47.224.186.35.bc.https ESTABLISHED
tcp4 0 0 localhost.63342 *.* LISTEN
tcp4 0 0 localhost.6942 *.* LISTEN
tcp4 0 0 192.168.0.17.55273 ec2-50-16-251-20.https ESTABLISHED
现在我们更了解 TCP 如何构建和跟踪连接,让我们使用一个名为 tcpdump 的命令行工具,来回顾在传输层上使用 TCP 的我们的 Web 服务器的 HTTP 请求。为了实现这一目标,我们使用一个叫做 tcpdump 的命令行工具。
tcpdump
tcpdump打印出与布尔表达式匹配的网络接口上数据包内容的描述。tcpdump 手册页
tcpdump 允许管理员和用户显示系统上处理的所有数据包,并根据许多 TCP 段头部细节进行过滤。在请求中,我们过滤所有目的端口为 8080 的网络接口 lo0 上的数据包;这是 Mac 上的本地回环接口。我们的 Web 服务器运行在 0.0.0.0:8080。图 1-8. Figure 1-8 显示了 tcpdump 收集数据的位置,参考完整的 TCP/IP 栈,位于网络接口卡(NIC)驱动程序和第二层之间。

图 1-8. tcpdump 数据包捕获
注意
回环接口是设备上的逻辑虚拟接口。回环接口不像以太网接口那样是物理接口。回环接口始终处于活动状态,即使主机上的其他接口关闭,它们也始终可用。
tcpdump 的一般输出格式将包含以下字段:tos, TTL, id, offset, flags, proto, length 和 options。让我们来回顾一下这些:
tos
服务类型字段。
TTL
存活时间;如果为零则不报告。
id
IP 标识字段。
offset
分段偏移字段;无论是否是分段数据报的一部分,都会打印此字段。
flags
DF(不分片)标志,指示数据包不能用于传输时分片。当未设置时,表示数据包可以分片。MF(更多分片)标志指示有包含更多分片的数据包,当未设置时,表示不再有分片。
proto
协议 ID 字段。
length
总长度字段。
options
IP 选项。
支持校验和卸载和 IP、TCP 和 UDP 校验和在传输到线路之前在 NIC 上计算的系统。由于我们在 NIC 之前运行tcpdump数据包捕获,所以像cksum 0xfe34 (incorrect -> 0xb4c1)这样的错误会出现在示例 1-4 的输出中。
要生成示例 1-4 的输出,请在另一个终端打开并在环回接口上开始tcpdump跟踪,仅针对 TCP 和端口 8080;否则,您将看到许多与我们示例无关的其他数据包。您需要使用升级特权来跟踪数据包,这意味着在这种情况下使用sudo。
示例 1-4. tcpdump
○ → sudo tcpdump -i lo0 tcp port 8080 -vvv 
tcpdump: listening on lo0, link-type NULL (BSD loopback),
capture size 262144 bytes 
08:13:55.009899 localhost.50399 > localhost.http-alt: Flags [S],
cksum 0x0034 (incorrect -> 0x1bd9), seq 2784345138,
win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 587364215 ecr 0,
sackOK,eol], length 0 
08:13:55.009997 localhost.http-alt > localhost.50399: Flags [S.],
cksum 0x0034 (incorrect -> 0xbe5a), seq 195606347,
ack 2784345139, win 65535, options [mss 16324,nop,wscale 6,nop,nop,
TS val 587364215 ecr 587364215,sackOK,eol], length 0 
08:13:55.010012 localhost.50399 > localhost.http-alt: Flags [.],
cksum 0x0028 (incorrect -> 0x1f58), seq 1, ack 1,
win 6371, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
v 08:13:55.010021 localhost.http-alt > localhost.50399: Flags [.],
cksum 0x0028 (incorrect -> 0x1f58), seq 1, ack
1, win 6371, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
08:13:55.010079 localhost.50399 > localhost.http-alt: Flags [P.],
cksum 0x0076 (incorrect -> 0x78b2), seq 1:79,
ack 1, win 6371, options [nop,nop,TS val 587364215 ecr 587364215],
length 78: HTTP, length: 78 
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
08:13:55.010102 localhost.http-alt > localhost.50399: Flags [.],
cksum 0x0028 (incorrect -> 0x1f0b), seq 1,
ack 79, win 6370, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
08:13:55.010198 localhost.http-alt > localhost.50399: Flags [P.],
cksum 0x00a1 (incorrect -> 0x05d7), seq 1:122,
ack 79, win 6370, options [nop,nop,TS val 587364215 ecr 587364215],
length 121: HTTP, length: 121 
HTTP/1.1 200 OK
Date: Wed, 19 Aug 2020 12:13:55 GMT
Content-Length: 5
Content-Type: text/plain; charset=utf-8
Hello[!http]
08:13:55.010219 localhost.50399 > localhost.http-alt: Flags [.], cksum 0x0028
(incorrect -> 0x1e93), seq 79,
ack 122, win 6369, options [nop,nop,TS val 587364215 ecr 587364215], length 0 
08:13:55.010324 localhost.50399 > localhost.http-alt: Flags [F.],
cksum 0x0028 (incorrect -> 0x1e92), seq 79,
ack 122, win 6369, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
08:13:55.010343 localhost.http-alt > localhost.50399: Flags [.],
cksum 0x0028 (incorrect -> 0x1e91), seq 122,
\ack 80, win 6370, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
08:13:55.010379 localhost.http-alt > localhost.50399: Flags [F.],
cksum 0x0028 (incorrect -> 0x1e90), seq 122,
ack 80, win 6370, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
08:13:55.010403 localhost.50399 > localhost.http-alt: Flags [.],
cksum 0x0028 (incorrect -> 0x1e91), seq 80, ack
123, win 6369, options [nop,nop,TS val 587364215 ecr 587364215],
length 0 
12 packets captured, 12062 packets received by filter
0 packets dropped by kernel. 
这是使用其命令及其所有选项进行tcpdump收集的开始。sudo数据包捕获了所需的升级特权。tcpdump是tcpdump二进制文件。-i lo0是我们想要捕获数据包的接口。dst port 8080是匹配表达式,即手册中讨论的内容;这里我们匹配所有发送到 TCP 端口 8080 的数据包,这是 Web 服务用于接收请求的端口。-v是详细选项,允许我们从tcpdump捕获中查看更多细节。
tcpdump的反馈告诉我们关于运行的tcpdump过滤器。
这是 TCP 握手中的第一个数据包。我们可以通过设置的标志位[S]和由 cURL 设置的序列号2784345138来判断这是 SYN,本地进程号为50399。
由tcpdump从localhost.http-alt进程中过滤出的 SYN-ACK 数据包,即 Golang Web 服务器。标志为[S.],因此是 SYN-ACK。数据包发送195606347作为下一个序列号,ACK 设置为2784345139以确认前一个数据包。
cURL 的确认数据包现在带有设置的 ACK 标志[.],ACK 和 SYN 号设置为 1,表示准备发送数据。
确认号设置为 1,以指示在开放数据推送中客户端的 SYN 标志的接收。
TCP 连接已建立;客户端和服务器均已准备好进行数据传输。接下来的数据包是我们的 HTTP 请求数据传输,标志设置为数据推送和 ACK[P.]。前面的数据包长度为零,但 HTTP 请求为 78 字节长,序列号为 1:79。
服务器确认数据传输的接收,设置了 ACK 标志[.],发送了序列号为 79 的确认号。
这个包是 HTTP 服务器对 cURL 请求的响应。数据推送标志被设置,[P.],并用 ACK 数字 79 确认了前一个包。设置数据传输的新序列号为 122,数据长度为 121 字节。
cURL 客户端用设置 ACK 标志的包确认接收该包,将确认号设置为 122,并将序列号设置为 79。
开始关闭 TCP 连接,客户端发送 FIN-ACK 包,[F.],确认接收了前一个包,编号为 122,并将新的序列号设为 80。
服务器将确认号增加到 80 并设置 ACK 标志。
TCP 要求发送方和接收方都设置 FIN 包以关闭连接。这是 FIN 和 ACK 标志被设置的包。
这是客户端的最终 ACK,确认号为 123。连接现在已关闭。
tcpdump 在退出时会告诉我们此次捕获的包数、在 tcpdump 期间捕获的包的总数,以及操作系统丢弃了多少包。
tcpdump 是网络工程师和集群管理员的一个优秀故障排除工具。能够在集群和网络的多个层面验证连通性是非常有价值的技能。你将在 第六章 中看到 tcpdump 的用处。
我们的示例是一个使用 TCP 的简单 HTTP 应用程序。所有这些数据都是以明文形式通过网络发送的。虽然这个示例是一个简单的 Hello World,但其他请求,如我们的银行登录,需要一些安全措施。传输层不会为通过网络传输的数据提供任何安全保护。TLS 在 TCP 上添加了额外的安全保护。让我们在下一节中深入了解。
TLS
TLS 为 TCP 添加了加密。TLS 是 TCP/IP 套件的一个附加组件,不被视为 TCP 基本操作的一部分。HTTP 事务可以在没有 TLS 的情况下完成,但在网络中不会受到窃听者的攻击。TLS 是一种协议组合,用于确保发送者和接收者之间的流量安全。TLS 与 TCP 类似,使用握手建立加密能力并交换加密密钥。以下步骤详细介绍了客户端和服务器之间的 TLS 握手,也可以在 图 1-9 中看到:
-
ClientHello:这包含客户端支持的密码套件和一个随机数。
-
ServerHello:此消息包含它支持的密码及一个随机数。
-
ServerCertificate:包含服务器的证书及其服务器公钥。
-
ServerHelloDone:这是 ServerHello 的结束。如果客户端收到要求其证书的请求,则发送 ClientCertificate 消息。
-
ClientKeyExchange:基于服务器的随机数,我们的客户端生成一个随机的预主密钥,用服务器的公钥证书加密后发送给服务器。
-
密钥生成:客户端和服务器从预主密钥生成一个主密钥,并交换随机值。
-
ChangeCipherSpec:现在客户端和服务器交换它们的 ChangeCipherSpec 以开始使用新密钥进行加密。
-
完成客户端:客户端发送完成消息以确认密钥交换和认证成功。
-
完成服务器:现在,服务器向客户端发送完成消息以结束握手。
Kubernetes 应用程序和组件将为开发人员管理 TLS,因此需要基本介绍;第五章 将进一步审视 TLS 和 Kubernetes。
如同我们的 Web 服务器、cURL 和 tcpdump 所示,TCP 是一种在主机之间发送数据的有状态可靠协议。它使用标志位、序列号和确认号的交互来在全球不可靠网络上传递数千条消息。然而,这种可靠性是有代价的。在我们设置的 12 个数据包中,只有两个是真正的数据传输。对于不需要像语音这样可靠性的应用程序来说,UDP 带来的开销提供了一种替代选择。现在我们理解了作为可靠连接导向协议的 TCP 如何运作,让我们来回顾一下 UDP 与 TCP 的区别。

图 1-9. TLS 握手
UDP
UDP 为那些不需要 TCP 提供的可靠性的应用程序提供了一种选择。UDP 对于能够承受数据包丢失的应用程序(例如语音和 DNS)非常适合。从网络角度来看,UDP 的开销很小,只有四个字段,没有数据确认,不像其冗长的兄弟 TCP。
它是事务导向的,适用于简单的查询和响应协议,如域名系统(DNS)和简单网络管理协议(SNMP)。UDP 将请求分割成数据报,因此适用于其他隧道协议(如虚拟专用网络(VPN))。它轻量且简单,非常适合在 DHCP 的情况下引导应用程序数据。数据传输的无状态性使 UDP 成为能够承受数据包丢失的应用程序(例如语音)的完美选择——你听到了吗?UDP 的不重新传输也使其成为流媒体视频的适当选择。
让我们看看 UDP 数据报中所需的少量头部信息(参见图 1-10):
源端口号(2 字节)
标识发送端口。源主机是客户端;端口号是临时的。UDP 端口有像 DNS 的 53 或 DHCP 的 67/68 这样的众所周知的数字。
目标端口号(2 字节)
标识接收端口并且是必需的。
长度(2 字节)
指定 UDP 头部和 UDP 数据的长度(以字节为单位)。最小长度为 8 字节,即头部的长度。
校验和(2 字节)
用于对头部和数据进行错误检查。在 IPv4 中是可选的,但在 IPv6 中是强制的,如果未使用则全部为零。
UDP 和 TCP 是通用的传输协议,帮助主机之间发送和接收数据。Kubernetes 在网络上支持这两种协议,服务允许用户通过服务来负载均衡许多 Pod。还需要注意的是,在每个服务中,开发人员必须定义传输协议;如果未定义,则默认使用 TCP。

图 1-10. UDP 头部
TCP/IP 堆栈中的下一层是互联网层——这些是可以在组成互联网的广阔网络上发送的数据包。让我们回顾一下如何完成这个过程。
网络
所有的 TCP 和 UDP 数据在 TCP/IP 网络层中作为 IP 数据包进行传输。互联网或网络层负责在网络之间传输数据。出站数据包选择下一跳主机,并通过传递适当的链路层细节将数据发送给该主机;数据包由主机接收,去封装后发送到适当的传输层协议上。在 IPv4 中,无论是发送还是接收,IP 根据 MTU 提供数据包的分段或重组;这是 IP 数据包的最大大小。
IP 不保证数据包的正确到达;因为跨不同网络的数据包传递本质上是不可靠和容易出错的,这个负担在通信路径的端点上而不是网络上。如前所述,提供服务可靠性是传输层的功能。每个数据包都有一个校验和来确保接收到的数据包信息准确,但此层不验证数据完整性。源和目标 IP 地址标识网络上的数据包,我们将在下一步讨论。
互联网协议
这个强大的数据包在 RFC 791 中定义,用于在网络间发送数据。图 1-11 展示了 IPv4 头部的格式。

图 1-11. IPv4 头部格式
让我们更详细地查看头部字段:
版本
IP 数据包中的第一个头部字段是四位版本字段。对于 IPv4,这总是等于四。
互联网头部长度(IHL)
IPv4 头部由于可选的第 14 个字段选项而具有可变大小。
服务类型
最初定义为服务类型(ToS),现在是区分服务代码点(DSCP),此字段指定不同的服务。DSCP 允许路由器和网络在拥塞时对数据包优先级进行决策。诸如 VoIP 等技术使用 DSCP 以确保通话优先于其他流量。
总长度
这是整个数据包大小(以字节为单位)。
标识
这是标识字段,用于唯一标识单个 IP 数据报的片段组。
标志
这用于控制或识别片段。从最重要到最不重要的顺序:
-
第 0 位:保留,设为零
-
第 1 位:不分段
-
第 2 位:更多片段
片段偏移
这指定相对于第一个未分段 IP 数据包的明显片段的偏移量。第一个片段的偏移量始终为零。
生存时间(TTL)
8 位生存时间字段有助于防止数据包在网络上循环。
协议
这用于 IP 数据包的数据部分。IANA 在 RFC 790 中列出了 IP 协议号的列表;一些知名协议也在表 1-4 中详细说明。
表 1-4. IP 协议号
| 协议号 | 协议名称 | 缩写 |
|---|---|---|
| 1 | 互联网控制消息协议 | ICMP |
| 2 | 互联网组管理协议 | IGMP |
| 6 | 传输控制协议 | TCP |
| 17 | 用户数据报协议 | UDP |
| 41 | IPv6 封装 | ENCAP |
| 89 | 开放最短路径优先 | OSPF |
| 132 | 流控制传输协议 | SCTP |
头部校验和(16 位)
IPv4 头部校验和字段用于错误检查。当数据包到达时,路由器计算头部的校验和;如果两个值不匹配,路由器将丢弃数据包。封装协议必须处理数据字段中的错误。UDP 和 TCP 均有校验和字段。
注意
当路由器接收到一个数据包时,它将 TTL 字段减一。因此,路由器必须计算新的校验和。
源地址
这是数据包发送者的 IPv4 地址。
注意
源地址可能会在传输过程中被网络地址转换设备更改;NAT 将在本章后面讨论,并在第三章中广泛讨论。
目标地址
这是数据包接收者的 IPv4 地址。与源地址一样,NAT 设备可以更改目标 IP 地址。
选项
头部中的可能选项是复制、选项类别、选项编号、选项长度和选项数据。
这里的关键组件是地址;这是网络识别的方式。它同时标识网络上的主机和整个网络本身(更多内容请参见“在网络中四处走动”)。理解如何识别 IP 地址对于工程师至关重要。首先,我们将回顾 IPv4,然后了解 IPv6 中的重大变化。
IPv4 地址对我们人类来说是点分十进制表示法;计算机将其读取为二进制字符串。图 1-12 详细描述了点分十进制表示法和二进制表示法。每个部分的长度为 8 位,共有四个部分,总长度为 32 位。IPv4 地址有两个部分:第一部分是网络,第二部分是网络上主机的唯一标识符。

图 1-12. IPv4 地址
在示例 1-5 中,我们有计算机网络接口卡的 IP 地址输出,我们可以看到其 IPv4 地址是192.168.1.2。该 IP 地址还有与之关联的子网掩码或网络掩码,以确定其所分配的网络。示例的子网是以点分十进制表示的netmask 0xffffff00,即255.255.255.0。
示例 1-5. IP 地址
○ → ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
options=400<CHANNEL_IO>
ether 38:f9:d3:bc:8a:51
inet6 fe80::8f4:bb53:e500:9557%en0 prefixlen 64 secured scopeid 0x6
inet 192.168.1.2 netmask 0xffffff00 broadcast 192.168.1.255
nd6 options=201<PERFORMNUD,DAD>
media: autoselect
status: active
子网提出了类地址分配的概念。最初,当分配 IP 地址范围时,一个范围被认为是 8 位、16 位或 24 位网络前缀与 24 位、16 位或 8 位主机标识符的组合。类 A 有 8 位用于主机,类 B 有 16 位,类 C 有 24 位。因此,类 A 有 2 的 16 次方个可用主机,即 16,777,216 个;类 B 有 65,536 个;类 C 有 256 个。每个类都有一个主机地址,第一个在其边界内,最后一个被指定为广播地址。图 1-13 为我们演示了这一点。
注意
还有其他两个类别,但它们在 IP 寻址中通常不使用。类 D 地址用于 IP 多播,类 E 地址保留用于实验。

图 1-13. IP 类
传统的地址分配在互联网上是不可扩展的,为了帮助缓解这个规模问题,我们开始使用无类域间路由(CIDR)范围来打破类别边界。与其说是在一个类地址范围内拥有 1600 多万个地址,不如说互联网实体只分配该范围的一个子范围。这有效地允许网络工程师将子网边界移动到类范围内的任意位置,从而使他们在 CIDR 范围内更加灵活,并有助于扩展 IP 地址范围。
在图 1-14 中,我们可以看到208.130.29.33 IPv4 地址的分解及其创建的层次结构。208.128.0.0/11 CIDR 范围从 IANA 分配给 ARIN。ARIN 进一步将子网分解为更小的子网,以满足其需求,导致网络上的单个主机208.130.29.33/32。

图 1-14. CIDR 示例
注意
IANA 负责全球 DNS 根、IP 寻址和其他互联网协议资源的协调。
最终,即使这种使用 CIDR 扩展 IPv4 地址范围的做法也导致了可分配的地址空间的枯竭,这促使网络工程师和 IETF 制定了 IPv6 标准。
在图 1-15 中,我们可以看到 IPv6 与 IPv4 不同,它使用十六进制缩短地址以便书写。它与 IPv4 类似,具有主机和网络前缀的特性。
IPv4 与 IPv6 最显著的差异是地址空间的大小。IPv4 有 32 位,而 IPv6 有 128 位来生成其地址。为了让这种大小差异更具体化,这些数字如下:
IPv4 有 4,294,967,296。
IPv6 有 340,282,366,920,938,463,463,374,607,431,768,211,456。

图 1-15. IPv6 地址
现在我们了解了如何识别网络中的单个主机以及它所属的网络,我们将探讨这些网络如何使用路由协议在彼此之间交换信息。
绕过网络
封包被寄出且数据准备好要送,但是我们的封包如何从我们的网络上的主机到达位于世界另一端的目标网络?这是路由的工作。有几种路由协议,但互联网依赖于边界网关协议(BGP),这是一种动态路由协议,用于管理互联网上边缘路由器之间的封包路由。对我们来说它是相关的,因为一些 Kubernetes 网络实现使用 BGP 在节点之间路由集群网络流量。在分隔的网络节点之间有一系列路由器。
如果我们参考图 1-1 中的互联网地图,互联网上的每个网络都被分配一个 BGP 自治系统号(ASN),用于指定代表互联网上通用和明确定义的路由策略的单个管理实体或公司。 BGP 和 ASN 允许网络管理员在互联网上宣布和总结其路由的同时,维护其内部网络路由的控制。表 1-5 列出了由 IANA 和其他区域实体管理的可用 ASN。^(1)
表 1-5. 可用的自治系统号(ASN)
| 编号 | 位 | 描述 | 参考 |
|---|---|---|---|
| 0 | 16 | 保留 | RFC 1930, RFC 7607 |
| 1–23455 | 16 | 公共 ASN | |
| 23456 | 16 | 保留用于 AS 池过渡 | RFC 6793 |
| 23457–64495 | 16 | 公共 ASN | |
| 64496–64511 | 16 | 用于文档/示例代码保留 | RFC 5398 |
| 64512–65534 | 16 | 保留供私人使用 | RFC 1930, RFC 6996 |
| 65535 | 16 | 保留 | RFC 7300 |
| 65536–65551 | 32 | 用于文档和示例代码保留 | RFC 4893, RFC 5398 |
| 65552–131071 | 32 | 保留 | |
| 131072–4199999999 | 32 | 公共 32 位 ASN | |
| 4200000000–4294967294 | 32 | 保留供私人使用 | RFC 6996 |
| 4294967295 | 32 | 保留 | RFC 7300 |
在 图 1-16 中,我们有五个 AS 号码,100–500. 位于 130.10.1.200 的主机想要到达位于 150.10.2.300 的目标主机。一旦位于主机 130.10.1.200 的本地路由器或默认网关接收到数据包,它将查找 BGP 为该路由确定的接口和路径。

图 1-16. BGP 路由示例
基于 图 1-17 中的路由表,AS 100 的路由器确定数据包属于 AS 300,并且首选路径是通过接口 140.10.1.1 出去。在 AS 200 上重复此操作,直到 AS 300 上的本地路由器接收到该数据包。这里的流程在 图 1-6 中描述,显示了网络之间的 TCP/IP 数据流动。需要对 BGP 有基本的理解,因为一些容器网络项目(如 Calico)使用它进行节点之间的路由;您将在 第 3 章 中了解更多信息。

图 1-17. 本地路由表
图 1-17 显示了一个本地路由表。在路由表中,我们可以看到一个数据包将基于目标 IP 地址发送的接口。例如,一个目标为 192.168.1.153 的数据包将通过本地网络的 link#11 网关发送,无需进行路由。192.168.1.254 是连接到我们互联网的网络中的路由器。如果目标网络不可知,则发送到默认路由。
注意
像所有的 Linux 和 BSD 操作系统一样,您可以在 netstat 的 man 页面中找到更多信息(man netstat)。Apple 的 netstat 源自 BSD 版本。在 FreeBSD Handbook 中可以找到更多信息。
路由器在互联网上持续通信,交换路由信息,并相互通知各自网络上的变化。BGP 负责处理大部分数据交换,但网络工程师和系统管理员可以使用 ICMP 协议和 ping 命令行工具来测试主机和路由器之间的连接。
ICMP
ping 是一种网络实用工具,用于使用 ICMP 在网络上测试主机之间的连接。在 示例 1-6 中,我们看到对 192.168.1.2 的 ping 测试成功,五个数据包均返回了 ICMP 回显回复。
示例 1-6. ICMP 回显请求
○ → ping 192.168.1.2 -c 5
PING 192.168.1.2 (192.168.1.2): 56 data bytes
64 bytes from 192.168.1.2: icmp_seq=0 ttl=64 time=0.052 ms
64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=0.089 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=64 time=0.142 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=64 time=0.050 ms
64 bytes from 192.168.1.2: icmp_seq=4 ttl=64 time=0.050 ms
--- 192.168.1.2 ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.050/0.077/0.142/0.036 ms
示例 1-7 显示了尝试到达主机 1.2.3.4 的 ping 失败的情况,超时。路由器和管理员将使用 ping 进行连接测试,并在测试容器连接时也很有用。在 第 2 和 第 3 章 中,当我们将我们的极简 Golang Web 服务器部署到容器和 Pod 中时,您将了解更多相关内容。
示例 1-7. ICMP 回显请求失败
○ → ping 1.2.3.4 -c 4
PING 1.2.3.4 (1.2.3.4): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
--- 1.2.3.4 ping statistics ---
4 packets transmitted, 0 packets received, 100.0% packet loss
与 TCP 和 UDP 一样,ICMP 数据包中有头部、数据和选项;它们在这里进行了审查,并在图 1-18 中显示:
类型
ICMP 类型。
代码
ICMP 子类型。
校验和
互联网校验和用于错误检查,由 ICMP 头部和数据计算,此字段的值为 0。
其余头部(4 字节字段)
内容根据 ICMP 类型和代码而变化。
数据
ICMP 错误消息包含一个数据部分,其中包括整个 IPv4 头。

图 1-18. ICMP 头部
注意
有些人认为 ICMP 是传输层协议,因为它不使用 TCP 或 UDP。根据 RFC 792,ICMP 定义了提供 IP 路由、诊断和错误功能的协议。虽然 ICMP 消息封装在 IP 数据报中,但 ICMP 处理被认为并通常作为 IP 层的一部分实现。ICMP 是 IP 协议 1,而 TCP 是 6,UDP 是 17。
此字段的值标识类型字段中的控制消息。代码字段为消息提供了额外的上下文信息。您可以在表 1-6 中找到一些标准的 ICMP 类型编号。
表 1-6. 常见的 ICMP 类型编号
| 编号 | 名称 | 参考 |
|---|---|---|
| 0 | 回显应答 | RFC 792 |
| 3 | 目标不可达 | RFC 792 |
| 5 | 重定向 | RFC 792 |
| 8 | 回显 | RFC 792 |
现在我们的数据包知道它们的源和目的地是哪些网络,是时候开始物理上将这些数据请求发送到网络上了;这是链路层的责任。
链路层
HTTP 请求已分成段,用于在互联网上进行路由,并且现在剩下的就是将数据发送到电线上。TCP/IP 堆栈的链路层包括两个子层:介质访问控制(MAC)子层和逻辑链路控制(LLC)子层。它们共同执行 OSI 模型的第 1 层和第 2 层,即数据链路层和物理层。链路层负责与本地网络的连接。第一个子层 MAC 负责访问物理介质。LLC 层有权管理流量控制和多路复用协议,通过 MAC 层发送和接收时进行解复用,如图 1-19 所示。IEEE 标准 802.3,即以太网,定义了用于封装 IP 数据包的协议。IEEE 802 是 LLC(802.2)、无线(802.11)和以太网/MAC(802.3)的总体标准。

图 1-19. 以太网解复用示例
与其他 PDU 一样,以太网具有头部和页脚,如图 1-20 所示。

图 1-20. 以太网头部和页脚
让我们详细审查这些:
前导码(8 字节)
交替的 1 和 0 位字符串指示接收主机即将收到帧。
目的 MAC 地址(6 字节)
MAC 目的地址;以太网帧的接收者。
源 MAC 地址(6 字节)
MAC 源地址;以太网帧源。
VLAN 标签(4 字节)
用于区分网络段上的流量的可选 802.1Q 标记。
Ether-type(2 字节)
指示封装在帧的有效载荷中的协议。
有效载荷(可变长度)
封装的 IP 数据包。
帧校验序列(FCS)或循环冗余校验(CRC)(4 字节)
帧校验序列(FCS)是一个四字节的循环冗余校验(CRC),允许在接收端检测整个帧中的损坏数据。 CRC 是以太网帧尾部的一部分。
图 1-21 显示 MAC 地址在制造时分配给网络接口硬件。 MAC 地址有两部分:组织单位标识符(OUI)和 NIC 特定部分。

图 1-21. MAC 地址
帧向网络层数据包的接收方指示类型。 表 1-7 详细介绍了常见的处理的协议。 在 Kubernetes 中,我们主要关注 IPv4 和 ARP 数据包。 IPv6 最近在 1.19 版本中引入到 Kubernetes 中。
表 1-7. 常见的 EtherType 协议
| EtherType | 协议 |
|---|---|
| 0x0800 | 互联网协议版本 4(IPv4) |
| 0x0806 | 地址解析协议(ARP) |
| 0x8035 | 反向地址解析协议(RARP) |
| 0x86DD | 互联网协议版本 6(IPv6) |
| 0x88E5 | MAC 安全(IEEE 802.1AE) |
| 0x9100 | 带有双标记的 VLAN 标记(IEEE 802.1Q)帧 |
当 IP 数据包到达目标网络时,目标 IP 地址通过 IPv4 的地址解析协议(IPv6 的邻居发现协议)解析为目标主机的 MAC 地址。 地址解析协议必须在以太网网络上管理从互联网地址到链路层地址的地址转换。 ARP 表用于快速查找已知主机,因此不必为主机要发送的每个帧发送 ARP 请求。 示例 1-8 显示了本地 ARP 表的输出。 网络上的所有设备都为此目的保留了 ARP 地址的缓存。
示例 1-8. ARP 表
○ → arp -a
? (192.168.0.1) at bc:a5:11:f1:5d:be on en0 ifscope [ethernet]
? (192.168.0.17) at 38:f9:d3:bc:8a:51 on en0 ifscope permanent [ethernet]
? (192.168.0.255) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
? (224.0.0.251) at 1:0:5e:0:0:fb on en0 ifscope permanent [ethernet]
? (239.255.255.250) at 1:0:5e:7f:ff:fa on en0 ifscope permanent [ethernet]
图 1-22 展示了本地网络上主机之间的交换。浏览器向目标服务器发出一个获取托管网站的 HTTP 请求。通过 DNS,它确定服务器的 IP 地址为10.0.0.1。为了继续发送 HTTP 请求,还需要服务器的 MAC 地址。首先,请求计算机查询缓存的 ARP 表,查找10.0.0.1是否存在服务器 MAC 地址的记录。如果找到 MAC 地址,则发送一个以服务器 MAC 地址为目的地地址的以10.0.0.1寻址的 IP 数据包的以太网帧到链路上。如果缓存没有命中10.0.0.2的记录,则请求计算机必须发送一个以广播地址FF:FF:FF:FF:FF:FF为目的地 MAC 地址的 ARP 请求消息。该消息被本地网络上的所有主机接受,请求10.0.0.1的答复。服务器用包含其 MAC 和 IP 地址的 ARP 响应消息回应。作为响应请求的一部分,服务器可能会将请求计算机的 MAC 地址插入其 ARP 表以供将来使用。请求计算机接收并缓存响应信息到其 ARP 表中,现在可以发送 HTTP 数据包了。
这也引出了本地网络上的一个关键概念,即广播域。广播域上的所有主机都接收来自主机的所有 ARP 消息。此外,所有帧都发送到广播上的所有节点,主机将目标 MAC 地址与自身的 MAC 地址进行比较。不属于自身的帧将被丢弃。随着网络上主机的增加,广播流量也随之增加。

图 1-22. ARP 请求
我们可以使用tcpdump来查看在本地网络上发生的所有 ARP 请求,如示例 1-9 所示。数据包捕获详细描述了 ARP 数据包,使用的以太网类型为Ethernet (len 6);以及更高层协议为IPv4。它还包括谁在请求 IP 地址的 MAC 地址,Request who-has 192.168.0.1 tell 192.168.0.12。
示例 1-9. ARP tcpdump
○ → sudo tcpdump -i en0 arp -vvv
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
17:26:25.906401 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:27.954867 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:29.797714 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:31.845838 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:33.897299 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:35.942221 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:37.785585 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:39.628958 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.13, length 28
17:26:39.833697 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:41.881322 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:43.929320 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:45.977691 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
17:26:47.820597 ARP, Ethernet (len 6), IPv4 (len 4),
Request who-has 192.168.0.1 tell 192.168.0.12, length 46
^C
13 packets captured
233 packets received by filter
0 packets dropped by kernel
为了进一步分割第二层网络,网络工程师可以使用虚拟局域网(VLAN)标记。在以太网帧头部内有一个可选的 VLAN 标签,用于区分 LAN 上的流量。使用 VLAN 来分割 LAN 并管理网络在同一个交换机或跨网络校园内的不同交换机上是很有用的。VLAN 之间的路由器过滤广播流量,启用网络安全性并减轻网络拥塞。它们对网络管理员来说在这些目的上是有用的,但是 Kubernetes 网络管理员可以使用 VLAN 技术的扩展版本,即虚拟可扩展局域网(VXLAN)。
图 1-23 展示了 VXLAN 是 VLAN 的扩展,允许网络工程师将层 2 帧封装到层 4 UDP 数据包中。VXLAN 增加了可扩展性,达到了 1600 万个逻辑网络,并允许在 IP 网络上实现层 2 邻接。这项技术用于 Kubernetes 网络以生成叠加网络,在后面的章节中您将更多了解。

图 1-23. VXLAN 数据包
Ethernet 还详细说明了用于传输帧的媒介规格,例如双绞线、同轴电缆、光纤、无线或其他尚未发明的传输介质,例如驱动 Philotic Parallax 即时通信装置的 γ 射线网络。^(2) Ethernet 甚至定义了在电线上使用的编码和信令协议;这超出了我们的讨论范围。
在网络角度,链路层还涉及多个其他协议。像前面讨论的层一样,我们仅仅触及了链路层的表面。我们将本书限制在对 Kubernetes 网络模型链路层所需了解的细节。
重新审视我们的 Web 服务器
我们对 TCP/IP 所有层的旅程完成了。图 1-24 概述了 TCP/IP 模型的每一层产生的所有头部和尾部,以便在互联网上传输数据。

图 1-24. TCP/IP PDU 全景
让我们回顾旅程,并再次提醒自己我们现在详细了解每一层正在发生的事情。示例 1-10 再次显示我们的 Web 服务器,示例 1-11 显示了本章前面 cURL 请求的内容。
示例 1-10. Go 中的最小 Web 服务器
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(w, "Hello")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe("0.0.0.0:8080", nil)
}
示例 1-11. 客户端请求
○ → curl localhost:8080 -vvv
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 25 Jul 2020 14:57:46 GMT
< Content-Length: 5
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Hello* Closing connection 0
我们从 Web 服务器在示例 1-10 等待连接开始。cURL 请求 HTTP 服务器的0.0.0.0端口 8080。cURL 从 URL 确定 IP 地址和端口号,并建立与服务器的 TCP 连接。一旦建立连接,通过 TCP 握手,cURL 发送 HTTP 请求。当 Web 服务器启动时,在 HTTP 服务器上创建一个名为 8080 的套接字,该套接字与 TCP 端口 8080 匹配;在 cURL 客户端的随机端口上也是如此。接下来,此信息发送到网络层,将源和目标 IP 地址附加到数据包的 IP 头部。在客户端的数据链路层,将 NIC 的源 MAC 地址添加到以太网帧中。如果目标 MAC 地址未知,则发出 ARP 请求以查找它。然后,使用 NIC 将以太网帧传输到 Web 服务器。
当 Web 服务器接收到请求时,它会创建包含 HTTP 响应的数据包。这些数据包通过使用请求数据包上的源 IP 地址经过互联网路由发送回 cURL 进程。一旦 cURL 进程接收到数据包,数据包就会从设备发送到驱动程序。在数据链路层,MAC 地址被移除。在网络协议层,IP 地址被验证,然后从数据包中移除。因此,如果应用程序需要访问客户端 IP,则需要在应用程序层存储它;HTTP 请求和 X-Forwarded-For 头部就是最好的例子。现在根据 TCP 数据确定套接字并移除它。然后将数据包转发到创建该套接字的客户端应用程序。客户端读取并处理响应数据。在本例中,套接字 ID 是随机的,对应于 cURL 进程。所有数据包都发送到 cURL 并组合成一个 HTTP 响应。如果我们使用了-O输出选项,响应将保存到文件中;否则,cURL 将响应输出到终端的标准输出。
哇,这真是一大口气,50 页和 50 年的网络发展被压缩成了两段话!我们回顾的网络基础只是一个开始,但如果您想要运行规模化的 Kubernetes 集群和网络,这些知识是必不可少的。
结论
本章中建模的 HTTP 事务每毫秒在全球范围内的互联网和数据中心网络中发生。这种规模是 Kubernetes 网络 API 帮助开发人员将复杂问题抽象为简单 YAML 的类型。了解问题的规模是我们掌握 Kubernetes 网络管理的第一步。通过我们的简单示例——Golang Web 服务器,并学习网络的第一原则,您可以开始处理流入和流出集群的数据包。
到目前为止,我们已经涵盖了以下内容:
-
网络的历史
-
OSI 模型
-
TCP/IP
在本章中,我们讨论了与网络相关的许多内容,但只涉及学习使用 Kubernetes 抽象所需的内容。关于 TCP/IP 有几本 O’Reilly 的书籍;TCP/IP 网络管理 由克雷格·亨特(O’Reilly)是关于 TCP 所有方面的深度阅读。
我们讨论了网络如何演变,走过了 OSI 模型,将其转化为 TCP/IP 协议栈,并通过该协议栈完成了一个示例 HTTP 请求。在下一章中,我们将详细讲解这是如何在 Linux 网络中为客户端和服务器实现的。
^(1) “自治系统(AS)编号”。 IANA.org. 2018-12-07. 检索于 2018-12-31.
^(2) 在电影《异星战场》中,他们使用“安西布尔”网络实现了跨银河瞬时通信。Philotic Parallax 即时通信器是安西布尔网络的官方名称。
第二章:Linux 网络
要理解 Kubernetes 中网络实现的实施,我们需要了解 Linux 网络的基础知识。最终,Kubernetes 是 Linux(或 Windows!)机器的复杂管理工具,这在处理 Kubernetes 网络堆栈时是不可忽视的。本章将概述 Linux 网络堆栈,重点关注在 Kubernetes 中值得注意的领域。如果您对 Linux 网络和网络管理非常熟悉,您可以快速浏览或跳过本章。
提示
本章介绍了许多 Linux 程序。可以通过man <program>访问手册页,以获取更多详细信息。
基础知识
让我们重新审视我们的 Go Web 服务器,在第一章中使用过。该 Web 服务器监听端口 8080,并对 HTTP 请求返回“Hello”至 / (参见示例 2-1)。
示例 2-1. Go 中的最小 Web 服务器
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(w, "Hello")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe("0.0.0.0:8080", nil)
}
警告
端口 1-1023(也称为众所周知的端口)需要 root 权限才能绑定。
程序应该始终以最低权限运行,这意味着典型的 Web 服务不应该以 root 用户身份运行。因此,许多程序会监听 1024 或更高端口(特别是端口 8080 是 HTTP 服务的常见选择)。在可能的情况下,监听非特权端口,并使用基础设施重定向(负载均衡器转发、Kubernetes 服务等)将外部可见的特权端口转发到监听非特权端口的程序上。
这样,攻击者利用服务中可能存在的漏洞时,无法获得过于广泛的权限。
假设该程序正在运行在 Linux 服务器上,并且外部客户端向 / 发起请求。服务器会发生什么?首先,我们的程序需要监听一个地址和端口。程序会为该地址和端口创建一个套接字并绑定到它。该套接字将接收发往指定地址和端口的请求 - 在我们的案例中是端口 8080 和任意 IP 地址。
注意
IPv4 中的 0.0.0.0 和 IPv6 中的 [::] 是通配地址。它们匹配其各自协议的所有地址,因此在套接字绑定时监听所有可用 IP 地址。
这对于暴露服务非常有用,而无需预先知道运行服务的机器将具有哪些 IP 地址。大多数网络暴露的服务都会绑定在这种方式上。
有多种方法可以检查套接字。例如,ls -lah /proc/<server proc>/fd 将列出套接字。本章末尾我们将讨论一些可以检查套接字的程序。
内核将给定的数据包映射到特定的连接,并使用内部状态机来管理连接状态。与套接字类似,连接可以通过各种工具进行检查,我们将在本章后面讨论。Linux 用文件表示每个连接。接受连接涉及内核向我们的程序发送的通知,然后我们能够流式传输文件中的内容。
回到我们的 Golang Web 服务器,我们可以使用strace来显示服务器正在执行的操作:
$ strace ./main
execve("./main", ["./main"], 0x7ebf2700 /* 21 vars */) = 0
brk(NULL) = 0x78e000
uname({sysname="Linux", nodename="raspberrypi", ...}) = 0
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
= 0x76f1d000
[Content cut]
因为strace捕获了服务器所做的所有系统调用,输出非常多。让我们将其减少到相关的网络系统调用。关键点已经突出显示,因为 Go HTTP 服务器在启动期间执行了许多系统调用:
openat(AT_FDCWD, "/proc/sys/net/core/somaxconn",
O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
epoll_create1(EPOLL_CLOEXEC) = 4 
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET,
{u32=1714573248, u64=1714573248}}) = 0
fcntl(3, F_GETFL) = 0x20000 (flags O_RDONLY|O_LARGEFILE)
fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
read(3, "128\n", 65536) = 4
read(3, "", 65532) = 0
epoll_ctl(4, EPOLL_CTL_DEL, 3, 0x20245b0) = 0
close(3) = 0
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3
close(3) = 0
socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3 
setsockopt(3, SOL_IPV6, IPV6_V6ONLY, [1], 4) = 0 
bind(3, {sa_family=AF_INET6, sin6_port=htons(0),
inet_pton(AF_INET6, "::1", &sin6_addr),
sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 5
setsockopt(5, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
bind(5, {sa_family=AF_INET6,
sin6_port=htons(0), inet_pton(AF_INET6,
"::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0),
sin6_scope_id=0}, 28) = 0
close(5) = 0
close(3) = 0
socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
setsockopt(3, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET6, sin6_port=htons(8080),
inet_pton(AF_INET6, "::", &sin6_addr),
sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0 
listen(3, 128) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 3,
{EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1714573248,
u64=1714573248}}) = 0
getsockname(3, {sa_family=AF_INET6, sin6_port=htons(8080),
inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0),
sin6_scope_id=0},
[112->28]) = 0
accept4(3, 0x2032d70, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN
(Resource temporarily unavailable)
epoll_wait(4, [], 128, 0) = 0
epoll_wait(4, 
打开文件描述符。
为 IPv6 连接创建 TCP 套接字。
在套接字上禁用IPV6_V6ONLY。现在,它可以同时监听 IPv4 和 IPv6。
将 IPv6 套接字绑定到监听端口 8080(所有地址)。
等待请求。
一旦服务器启动,我们看到strace输出在epoll_wait上暂停。
此时,服务器正在监听其套接字,并等待内核通知其有数据包。当我们向我们的监听服务器发出请求时,我们看到“Hello”消息:
$ curl <ip>:8080/
Hello
提示
如果您试图使用strace调试 Web 服务器的基础知识,您可能不想使用 Web 浏览器。对服务器发送的额外请求或元数据可能会导致服务器执行额外的工作,或者浏览器可能不会发出预期的请求。例如,许多浏览器会自动请求 favicon 文件。它们还会尝试缓存文件,重用连接,以及执行其他使得准确预测网络交互变得更加困难的操作。在需要简单或最小复制场景时,尝试使用诸如curl或telnet等工具。
在strace中,我们从我们的服务器进程看到以下内容:
[{EPOLLIN, {u32=1714573248, u64=1714573248}}], 128, -1) = 1
accept4(3, {sa_family=AF_INET6, sin6_port=htons(54202), inet_pton(AF_INET6,
"::ffff:10.0.0.57", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0},
[112->28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 5
epoll_ctl(4, EPOLL_CTL_ADD, 5, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET,
{u32=1714573120, u64=1714573120}}) = 0
getsockname(5, {sa_family=AF_INET6, sin6_port=htons(8080),
inet_pton(AF_INET6, "::ffff:10.0.0.30", &sin6_addr), sin6_flowinfo=htonl(0),
sin6_scope_id=0}, [112->28]) = 0
setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(5, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPINTVL, [180], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0
accept4(3, 0x2032d70, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN
(Resource temporarily unavailable)
检查完套接字后,我们的服务器向文件描述符写入响应数据(在 HTTP 协议中包装的“Hello”)。然后,Linux 内核(以及其他一些用户空间系统)将请求转换为数据包,并将这些数据包传输回我们的 cURL 客户端。
总结一下服务器在收到请求时的操作:
-
Epoll 返回并导致程序恢复。
-
服务器在本示例中看到来自
::ffff:10.0.0.57的连接 IP 地址。 -
服务器检查套接字。
-
服务器更改了
KEEPALIVE选项:打开了KEEPALIVE,并设置了 180 秒的间隔来进行KEEPALIVE探测。
这是从应用开发者角度来看 Linux 网络的鸟瞰图。要让所有内容正常工作,还有很多工作要做。我们将更详细地查看对 Kubernetes 用户特别相关的网络堆栈部分。
网络接口
计算机使用网络接口与外部世界通信。网络接口可以是物理的(例如,以太网网络控制器)或虚拟的。虚拟网络接口不对应物理硬件;它们是主机或虚拟机监视器提供的抽象接口。
IP 地址分配给网络接口。典型接口可能具有一个 IPv4 地址和一个 IPv6 地址,但可以将多个地址分配给同一接口。
Linux 本身具有网络接口的概念,可以是物理的(如以太网卡和端口)或虚拟的。如果运行ifconfig,您将看到所有网络接口及其配置(包括 IP 地址)的列表。
回环接口是用于同主机通信的特殊接口。127.0.0.1是回环接口的标准 IP 地址。发送到回环接口的数据包将不会离开主机,而在127.0.0.1上监听的进程只能由同一主机上的其他进程访问。请注意,使进程在127.0.0.1上监听不是安全边界。CVE-2020-8558 是过去 Kubernetes 的一个漏洞,在此漏洞中,kube-proxy规则允许某些远程系统访问127.0.0.1。回环接口通常缩写为lo。
提示
ip命令也可用于检查网络接口。
让我们看一个典型的ifconfig输出;参见示例 2-2。
示例 2-2. 一个带有一个物理网络接口(ens4)和回环接口的机器上的ifconfig输出
$ ifconfig
ens4: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1460
inet 10.138.0.4 netmask 255.255.255.255 broadcast 0.0.0.0
inet6 fe80::4001:aff:fe8a:4 prefixlen 64 scopeid 0x20<link>
ether 42:01:0a:8a:00:04 txqueuelen 1000 (Ethernet)
RX packets 5896679 bytes 504372582 (504.3 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 9962136 bytes 1850543741 (1.8 GB)
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
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 352 bytes 33742 (33.7 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 352 bytes 33742 (33.7 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
容器运行时在主机上为每个 pod 创建一个虚拟网络接口,因此在典型的 Kubernetes 节点上,列表会更长。我们将在第三章详细讨论容器网络。
桥接接口
桥接接口(如图 2-1 所示)允许系统管理员在单个主机上创建多个二层网络。换句话说,桥接功能类似于将主机上的网络接口连接起来的网络交换机,无缝连接它们。桥接允许具有各自网络接口的 pod 通过节点的网络接口与更广泛的网络交互。

图 2-1. 桥接口
注意
您可以在文档中了解有关 Linux 桥接的更多信息。
在示例 2-3 中,我们演示如何使用ip创建一个名为br0的桥接设备,并连接一个虚拟以太网(veth)设备veth和一个物理设备eth0。
示例 2-3. 创建桥接口并连接 veth 对
# # Add a new bridge interface named br0.
# ip link add br0 type bridge
# # Attach eth0 to our bridge.
# ip link set eth0 master br0
# # Attach veth to our bridge.
# ip link set veth master br0
桥接也可以使用 brctl 命令来管理和创建。示例 2-4 展示了一些可用的 brctl 选项。
示例 2-4. brctl 选项
$ brctl
$ commands:
addbr <bridge> add bridge
delbr <bridge> delete bridge
addif <bridge> <device> add interface to bridge
delif <bridge> <device> delete interface from bridge
setageing <bridge> <time> set ageing time
setbridgeprio <bridge> <prio> set bridge priority
setfd <bridge> <time> set bridge forward delay
sethello <bridge> <time> set hello time
setmaxage <bridge> <time> set max message age
setpathcost <bridge> <port> <cost> set path cost
setportprio <bridge> <port> <prio> set port priority
show show a list of bridges
showmacs <bridge> show a list of mac addrs
showstp <bridge> show bridge stp info
stp <bridge> <state> turn stp on/off
veth 设备是本地以太网隧道。Veth 设备成对创建,如图 2-1 所示,其中 pod 从 veth 看到 eth0 接口。在一对设备中传输的数据包会立即在另一对设备上接收到。如果任何一个设备关闭,该对的链接状态也会关闭。在 Linux 中添加桥接可以通过使用 brctl 命令或 ip 命令来完成。当命名空间需要与主机命名空间或彼此之间通信时,请使用 veth 配置。
示例 2-5 展示了如何设置 veth 配置。
示例 2-5. 创建 veth
# ip netns add net1
# ip netns add net2
# ip link add veth1 netns net1 type veth peer name veth2 netns net2
在 示例 2-5 中,我们展示了创建两个网络命名空间(与 Kubernetes 命名空间不同),net1 和 net2,以及一对 veth 设备的步骤,其中 veth1 分配给命名空间 net1,veth2 分配给命名空间 net2。这两个命名空间通过这对 veth 设备连接。分配一对 IP 地址后,您可以在两个命名空间之间进行 ping 和通信。
Kubernetes 与 CNI 项目协同使用此功能来管理容器网络命名空间、接口和 IP 地址。我们将在 第三章 中进一步探讨这一点。
内核中的数据包处理
Linux 内核负责在程序之间翻译数据包和一致的数据流。特别是,我们将关注内核如何处理连接,因为在 Kubernetes 中,路由和防火墙是非常依赖 Linux 底层数据包管理的关键部分。
Netfilter
Netfilter 自 Linux 2.3 起被包括在内,是数据包处理的关键组件。Netfilter 是一个内核钩子的框架,允许用户空间程序代表内核处理数据包。简而言之,程序注册到特定的 Netfilter 钩子,并且内核调用该程序来处理适用的数据包。该程序可以告诉内核对数据包做某些操作(如丢弃),或者修改后返回给内核。借助此功能,开发人员可以构建在用户空间运行的正常程序来处理数据包。Netfilter 与 iptables 共同创建,以分离内核和用户空间代码。
提示
netfilter.org 包含了关于 Netfilter 和 iptables 设计和使用的优秀文档。
Netfilter 有五个钩子,在 表 2-1 中展示。
Netfilter 在数据包通过内核的旅程中的特定阶段触发每个钩子。理解 Netfilter 的钩子是理解本章后面 iptables 的关键,因为 iptables 直接将其 chains 的概念映射到 Netfilter 钩子。
表 2-1. Netfilter 钩子
| Netfilter 钩子 | Iptables 链名称 | 描述 |
|---|---|---|
| NF_IP_PRE_ROUTING | PREROUTING | 触发条件:数据包从外部系统到达时。 |
| NF_IP_LOCAL_IN | INPUT | 触发条件:数据包的目标 IP 地址与此主机匹配时。 |
| NF_IP_FORWARD | NAT | 触发条件:数据包中源地址和目标地址均不匹配机器的 IP 地址(换句话说,这些数据包是此机器代表其他机器进行路由的)。 |
| NF_IP_LOCAL_OUT | OUTPUT | 触发条件:数据包由主机发起,并离开主机时。 |
| NF_IP_POST_ROUTING | POSTROUTING | 触发条件:任何数据包(无论源)离开主机时。 |
Netfilter 在数据包处理的特定阶段触发每个钩子,并且在特定条件下,我们可以通过流程图可视化 Netfilter 钩子,如图 2-2 所示。

图 2-2. 数据包通过 Netfilter 钩子的可能流向
我们可以从我们的流程图中推断,对于任何给定的数据包,只有特定的 Netfilter 钩子调用排列组合是可能的。例如,从本地进程发起的数据包总是会触发 NF_IP_LOCAL_OUT 钩子,然后是 NF_IP_POST_ROUTING 钩子。具体来说,数据包的 Netfilter 钩子流程取决于两个因素:数据包的源是否为主机,以及数据包的目标是否为主机。请注意,如果一个进程发送一个目标地址为相同主机的数据包,它会在“重新进入”系统之前触发 NF_IP_LOCAL_OUT 和 NF_IP_POST_ROUTING 钩子,然后触发 NF_IP_PRE_ROUTING 和 NF_IP_LOCAL_IN 钩子。
在某些系统中,可以通过写入虚假的源地址(即伪造数据包具有源地址和目标地址为 127.0.0.1)来伪造此类数据包。当这样的数据包到达外部接口时,Linux 通常会对其进行过滤。更广泛地说,当数据包到达接口时,如果数据包的源地址在该网络上不存在,则 Linux 会过滤这些数据包。具有“不可能的”源 IP 地址的数据包被称为火星数据包。在 Linux 中,可以禁用火星数据包的过滤。但是,如果主机上的任何服务假定来自本地主机的流量比外部流量更“可信”,这可能带来重大风险。这可能是一个常见的假设,例如在向主机公开 API 或数据库时不使用强认证。
注意
Kubernetes 至少有一个 CVE,即 CVE-2020-8558,其中来自另一台主机的数据包,源 IP 地址错误地设置为 127.0.0.1,可以访问本应仅本地可访问的端口。这意味着如果 Kubernetes 控制平面中的节点运行 kube-proxy,则该节点网络中的其他机器可以使用“信任认证”连接到 API 服务器,从而有效地控制整个集群。
这并不是火星包未被过滤的技术问题,因为有问题的包会来自回环设备,这与127.0.0.1处于同一网络。你可以在GitHub上查看报告的问题。
表 2-2 展示了不同数据包来源和目的地的 Netfilter 钩子顺序。
表 2-2. 关键 Netfilter 数据包流
| 数据包来源 | 数据包目的地 | 钩子(顺序) |
|---|---|---|
| 本地机器 | 本地机器 | NF_IP_LOCAL_OUT, NF_IP_LOCAL_IN |
| 本地机器 | 外部机器 | NF_IP_LOCAL_OUT, NF_IP_POST_ROUTING |
| 外部机器 | 本地机器 | NF_IP_PRE_ROUTING, NF_IP_LOCAL_IN |
| 外部机器 | 外部机器 | NF_IP_PRE_ROUTING, NF_IP_FORWARD, NF_IP_POST_ROUTING |
注意,来自机器自身的数据包将触发NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING,然后“离开”网络接口。它们将“重新进入”并像来自任何其他来源的数据包一样处理。
网络地址转换(NAT)仅在NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT钩子中影响本地路由决策(例如,一旦数据包到达NF_IP_LOCAL_IN钩子后,内核不再进行路由决策)。我们在iptables的设计中看到这一点,其中只能在特定的钩子/链中执行源和目标 NAT。
程序可以通过调用NF_REGISTER_NET_HOOK(Linux 4.13 之前的版本为NF_REGISTER_HOOK)注册一个钩子处理函数。每次数据包匹配时,钩子都会被调用。这就是像iptables这样的程序如何与 Netfilter 集成的方式,尽管你可能永远不需要自己这样做。
Netfilter 钩子可以触发几种操作,具体取决于返回值:
接受
继续处理数据包。
丢弃
丢弃数据包,不进行进一步处理。
排队
将数据包传递给用户空间程序。
偷窃
不再执行进一步的钩子,并允许用户空间程序接管数据包。
重复
使数据包“重新进入”钩子并重新处理。
钩子还可以返回变异的数据包。这允许程序执行诸如重新路由或伪装数据包、调整数据包 TTL 等操作。
Conntrack
Conntrack 是 Netfilter 的一个组件,用于跟踪(到达和离开)机器的连接状态。连接跟踪直接将数据包与特定连接关联起来。没有连接跟踪,数据包的流动将更加不透明。Conntrack 可以是一种责任,也可以是一种有价值的工具,或者两者兼而有之,这取决于它的使用方式。总体而言,对于处理防火墙或 NAT 的系统,Conntrack 至关重要。
连接跟踪允许防火墙区分响应和任意数据包。防火墙可以配置为允许作为现有连接一部分的入站数据包,但不允许不是连接一部分的入站数据包。例如,程序可以允许建立出站连接并执行 HTTP 请求,而远程服务器则不能通过其他方式发送数据或发起入站连接。
NAT 依赖于 Conntrack 来运行。iptables将 NAT 公开为两种类型:SNAT(源地址转换,其中iptables重新编写源地址)和 DNAT(目标地址转换,其中iptables重新编写目标地址)。NAT 非常普遍;你家中路由器使用 SNAT 和 DNAT 将流量从公共 IPv4 地址转发到网络上每个设备的本地地址的可能性极高。使用连接跟踪,数据包会自动与其连接关联,并且可以轻松地通过相同的 SNAT/DNAT 更改进行修改。这使得可以进行一致的路由决策,例如在负载均衡器中将连接“固定”到特定的后端或机器上。后一种示例在 Kubernetes 中非常相关,这是因为kube-proxy通过iptables实现了服务负载均衡。如果没有连接跟踪,每个数据包都需要确定性地重新映射到相同的目标,这是不可行的(假设可能的目标列表可能会改变…)。
Conntrack 通过一个由源地址、源端口、目标地址、目标端口和 L4 协议组成的元组来识别连接。这五个信息是识别任何给定 L4 连接所需的最小标识符。所有 L4 连接在连接的每一侧都有地址和端口;毕竟,互联网使用地址进行路由,计算机使用端口号进行应用映射。最后一个部分,L4 协议,存在是因为程序将在 TCP 或 UDP 模式下绑定到端口(在一个模式下绑定并不排除在另一个模式下绑定)。Conntrack 称这些连接为流。流包含有关连接及其状态的元数据。
Conntrack 将流存储在哈希表中,显示在图 2-3,使用连接元组作为键。键空间的大小是可配置的。更大的键空间需要更多内存来保存底层数组,但会导致较少的流哈希到相同的键并链接在链表中,从而实现更快的流查找时间。流的最大数量也是可配置的。一个严重的问题是当 Conntrack 的连接跟踪空间耗尽时,无法建立新的连接。还有其他的配置选项,如连接的超时时间。在典型系统上,默认设置通常就够用了。然而,一个经历大量连接的系统会耗尽空间。如果您的主机直接暴露在互联网上,用短暂或不完整的连接压倒 Conntrack 是导致拒绝服务(DOS)的简单方法。

图 2-3. Conntrack 流的结构
Conntrack 的最大大小通常在 /proc/sys/net/nf_conntrack_max 中设置,哈希表大小通常在 /sys/module/nf_conntrack/parameters/hashsize 中设置。
Conntrack 条目包含连接状态,其中一个是四个状态之一。需要注意的是,作为第 3 层(网络层)工具,Conntrack 状态与第 4 层(协议层)的状态是不同的。表 2-3 详细描述了这四种状态。
表 2-3. Conntrack 状态
| 状态 | 描述 | 示例 |
|---|---|---|
| NEW | 发送或接收到一个有效的数据包,但没有看到响应。 | 收到 TCP SYN 数据包。 |
| ESTABLISHED | 在两个方向上观察到数据包。 | 收到 TCP SYN 数据包,并发送了 TCP SYN/ACK 数据包。 |
| RELATED | 打开了一个附加连接,元数据表明它与原始连接“相关”。相关连接处理比较复杂。 | 一个 FTP 程序,有一个已建立的连接,打开了额外的数据连接。 |
| INVALID | 数据包本身无效,或者不正确匹配另一个 Conntrack 连接状态。 | 收到 TCP RST 数据包,没有先前的连接。 |
尽管 Conntrack 是内核中的一部分,但它可能未在您的系统上激活。必须加载特定的内核模块,并且必须有相关的 iptables 规则(基本上,如果没有任何需要 Conntrack 的东西,它通常不会激活)。Conntrack 需要内核模块 nf_conntrack_ipv4 处于激活状态。lsmod | grep nf_conntrack 将显示模块是否已加载,sudo modprobe nf_conntrack 将加载它。您可能还需要安装 conntrack 命令行界面(CLI)以查看 Conntrack 的状态。
当 Conntrack 处于活动状态时,conntrack -L 显示所有当前流。额外的 Conntrack 标志将过滤要显示的流。
让我们来看看 Conntrack 流的解剖,如下所示:
tcp 6 431999 ESTABLISHED src=10.0.0.2 dst=10.0.0.1
sport=22 dport=49431 src=10.0.0.1 dst=10.0.0.2 sport=49431 dport=22 [ASSURED]
mark=0 use=1
<protocol> <protocol number> <flow TTL> [flow state>]
<source ip> <dest ip> <source port> <dest port> [] <expected return packet>
预期的返回数据包的形式为<源 IP> <目标 IP> <源端口> <目标端口>。这是我们期望在远程系统发送数据包时看到的标识符。请注意,在我们的例子中,源和目标值在地址和端口方面是相反的。这通常是这样,但并非总是如此。例如,如果一台机器在路由器后面,发送到该机器的数据包将寻址到路由器,而来自该机器的数据包将具有机器地址,而不是路由器地址,作为源。
在来自机器10.0.0.2的前一个例子中,10.0.0.1已经建立了从端口 49431 到10.0.0.2端口 22 的 TCP 连接。您可能会认出这是一个 SSH 连接,尽管 Conntrack 无法显示应用级行为。
像grep这样的工具对于检查 Conntrack 状态和临时统计非常有用:
grep ESTABLISHED /proc/net/ip_conntrack | wc -l
路由
在处理任何数据包时,内核必须决定将该数据包发送到哪里。在大多数情况下,目标机器不会在同一网络中。例如,假设您正在尝试从个人计算机连接到1.2.3.4。1.2.3.4不在您的网络上;您的计算机最好的做法是将其传递给另一台更接近能够到达1.2.3.4的主机。路由表通过将已知子网映射到网关 IP 地址和接口来实现此目的。您可以使用route(或route -n以显示原始 IP 地址而不是主机名)列出已知路由。典型的机器将具有本地网络的路由和0.0.0.0/0的路由。请记住,子网可以表示为 CIDR(例如,10.0.0.0/24)或一个 IP 地址和一个掩码(例如,10.0.0.0和255.255.255.0)。
这是一台连接到互联网的本地网络上的机器的典型路由表:
# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.0.0.1 0.0.0.0 UG 303 0 0 eth0
10.0.0.0 0.0.0.0 255.255.255.0 U 303 0 0 eth0
在前面的例子中,对1.2.3.4的请求将被发送到10.0.0.1,通过eth0接口,因为1.2.3.4在第一条规则描述的子网中(0.0.0.0/0),而不在第二条规则描述的子网中(10.0.0.0/24)。子网由目标和genmask值指定。
Linux 更喜欢按照特定性(匹配子网有多“小”)和然后按照权重(route输出中的“metric”)来路由数据包。根据我们的例子,寻址到10.0.0.1的数据包将始终被发送到网关0.0.0.0,因为该路由匹配了一个更小的地址集。如果我们有两个具有相同特定性的路由,则较低度量的路由将被优先选择。
一些 CNI 插件大量使用路由表。
现在我们已经介绍了 Linux 内核处理数据包的一些关键概念,我们可以看看更高级别的数据包和连接路由是如何工作的。
高级路由
Linux 具有复杂的数据包管理能力。此类工具允许 Linux 用户创建防火墙、记录流量、路由数据包,甚至实现负载均衡。Kubernetes 利用这些工具之一来处理节点和 Pod 的连接性,以及管理 Kubernetes 服务。在本书中,我们将介绍在 Kubernetes 中最常见的三种工具。所有 Kubernetes 设置都将使用iptables的某些功能,但是有许多管理服务的方法。我们还将介绍 IPVS(在kube-proxy中具有内置支持)和 eBPF,后者被 Cilium(一种kube-proxy替代方案)使用。
在第四章中我们将引用本节内容,讨论服务和kube-proxy。
iptables
iptables是 Linux 系统管理员的重要工具,已经使用多年。iptables可用于创建防火墙和审计日志,修改和重定向数据包,甚至实现粗糙的连接分流。iptables使用 Netfilter,允许iptables拦截和修改数据包。
iptables规则可能变得非常复杂。有许多工具提供了更简单的界面来管理iptables规则;例如,像ufw和firewalld这样的防火墙。Kubernetes 组件(特别是kubelet和kube-proxy)以这种方式生成iptables规则。理解iptables对于了解大多数集群中 Pod 和节点的访问和路由至关重要。
注意
大多数 Linux 发行版正在用nftables替换iptables,这是一个类似但性能更好的工具,建立在 Netfilter 之上。一些发行版已经使用由nftables驱动的iptables版本。
Kubernetes 在iptables/nftables过渡中存在许多已知问题。我们强烈建议在可预见的未来不要使用基于nftables的iptables版本。
iptables有三个关键概念:表、链和规则。它们被认为具有层次结构:表包含链,链包含规则。
表根据其影响类型组织规则。iptables具有广泛的功能,这些功能被分组到表中。最常用的三个适用表是:Filter(用于防火墙相关规则)、NAT(用于 NAT 相关规则)和 Mangle(用于非 NAT 数据包修改规则)。iptables按特定顺序执行表,我们稍后会详细介绍。
链包含一系列规则。当数据包执行链时,按顺序评估链中的规则。链存在于表中,并根据 Netfilter 钩子组织规则。有五个内置的顶级链,每个链对应一个 Netfilter 钩子(回顾 Netfilter 与iptables的联合设计)。因此,选择在哪个链中插入规则决定了是否/何时对给定数据包评估规则。
规则是条件和操作的组合(称为目标)。例如,“如果数据包地址为端口 22,则丢弃它。” iptables 评估单个数据包,尽管链和表决定了规则将针对哪些数据包进行评估。
表 → 链 → 目标执行的具体细节很复杂,有无数复杂的图表可描述完整的状态机。接下来,我们将更详细地检查每个部分。
提示
在本节中继续时,参考之前的材料可能会有所帮助。表、链和规则的设计紧密相连,了解其中一个而不了解其他的是很难的。
iptables 表
iptables 中的表映射到特定的功能集,每个表“负责”特定类型的操作。更具体地说,一个表只能包含特定的目标类型,而许多目标类型只能在特定的表中使用。iptables 有五个表,列在表 2-4 中。
表 2-4. iptables 表
| 表 | 用途 |
|---|---|
| Filter | Filter 表处理数据包的接受和拒绝。 |
| NAT | NAT 表用于修改源或目标 IP 地址。 |
| Mangle | Mangle 表可对数据包头部进行通用编辑,但不适用于 NAT。它还可以使用 iptables 的元数据对数据包进行“标记”。 |
| Raw | Raw 表允许在连接跟踪和其他表处理之前进行数据包变异。它最常见的用途是为某些数据包禁用连接跟踪。 |
| 安全 | SELinux 使用安全表处理数据包。这在不使用 SELinux 的机器上不适用。 |
本书不会详细讨论安全表;但是,如果您使用 SELinux,您应该了解其用法。
iptables 按特定顺序执行表:Raw、Mangle、NAT、Filter。但是,执行顺序被链打破。Linux 用户通常接受“表包含链”的信条,但这可能会感到误导。执行顺序是链,然后是表。因此,例如,数据包将触发 Raw PREROUTING、Mangle PREROUTING、NAT PREROUTING,然后触发 INPUT 或 FORWARD 链中的 Mangle 表(取决于数据包)。我们将在下一节关于链的更详细部分中详细介绍这一点,以便更好地理解。
iptables 链
iptables 链是一组规则。当数据包触发或通过链时,每条规则会依次评估,直到数据包与“终止目标”(如DROP)匹配,或数据包达到链的末尾。
内建的“顶层”链包括 PREROUTING、INPUT、NAT、OUTPUT 和 POSTROUTING。这些链由 Netfilter 钩子支持。每个链对应一个钩子。 表 2-5 显示了链和钩子的对应关系。还有用户定义的子链用于帮助组织规则。
表 2-5. iptables 链和对应的 Netfilter 钩子
| iptables 链 | Netfilter 钩子 |
|---|---|
PREROUTIN |
NF_IP_PRE_ROUTING |
INPUT |
NF_IP_LOCAL_IN |
NAT |
NF_IP_FORWARD |
OUTPUT |
NF_IP_LOCAL_OUT |
POSTROUTING |
NF_IP_POST_ROUTING |
回到我们的 Netfilter 钩子顺序图,我们可以推断出特定数据包的 iptables 链执行和顺序图表(参见 图 2-4)。

图 2-4. 数据包通过 iptables 链的可能流程
再次,类似于 Netfilter,数据包穿越这些链的方式极少(假设数据包不会在途中被拒绝或丢弃)。我们以三台机器为例,它们的 IP 地址分别是 10.0.0.1、10.0.0.2 和 10.0.0.3。我们将从机器 1(IP 地址为 10.0.0.1)的视角来展示一些路由场景。我们在 表 2-6 中进行了详细的讨论。
表 2-6. 不同场景下执行的 iptables 链
| 数据包描述 | 数据包源 | 数据包目标 | 处理的表格 |
|---|---|---|---|
| 来自另一台机器的入站数据包。 | 10.0.0.2 |
10.0.0.1 |
PREROUTING、INPUT |
| 来自其他机器的入站数据包,目标不是本机。 | 10.0.0.2 |
10.0.0.3 |
PREROUTING、NAT、POSTROUTING |
| 本地发起的出站数据包,目标是另一台机器。 | 10.0.0.1 |
10.0.0.2 |
OUTPUT、POSTROUTING |
| 来自本地程序的数据包,目标是同一台机器。 | 127.0.0.1 |
127.0.0.1 |
OUTPUT、POSTROUTING(然后通过回环接口再次进入时为 PREROUTING、INPUT) |
提示
您可以使用 LOG 规则自行测试链的执行行为。例如:
iptables -A OUTPUT -p tcp --dport 22 -j LOG
--log-level info --log-prefix "ssh-output"
将在数据包通过 OUTPUT 链时记录 TCP 数据包到端口 22,日志前缀为 "ssh-output"。请注意,日志大小可能迅速变得难以管理。在重要主机上谨慎记录。
请记住,当数据包触发链时,iptables 按照以下顺序执行该链中的表格(具体是每个表格内的规则):
-
原始
-
管理
-
网络地址转换(NAT)
-
过滤
大多数链不包含所有表格;但是,相对执行顺序保持不变。这是为了减少冗余的设计决策。例如,原始表格用于操作“进入” iptables 的数据包,因此只有 PREROUTING 和 OUTPUT 链,符合 Netfilter 的数据包流程。每个链包含的表格详见 表 2-7。
表 2-7. 哪些 iptables 表(行)包含哪些链(列)
| Raw | Mangle | NAT | Filter | |
|---|---|---|---|---|
PREROUTING |
✓ | ✓ | ✓ | |
INPUT |
✓ | ✓ | ✓ | |
FORWARD |
✓ | ✓ | ||
OUTPUT |
✓ | ✓ | ✓ | ✓ |
POSTROUTING |
✓ | ✓ |
您可以自行列出与特定表相对应的链,使用iptables -L -t <table>命令:
$ iptables -L -t filter
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
对于 NAT 表有一个小注意事项:DNAT 可以在PREROUTING或OUTPUT中执行,而 SNAT 只能在INPUT或POSTROUTING中执行。
举个例子,假设我们有一个传入的数据包要发送到我们的主机。执行顺序如下:
-
PREROUTING-
Raw
-
Mangle
-
NAT
-
-
INPUT-
Mangle
-
NAT
-
Filter
-
现在我们已经了解了 Netfilter 钩子、表和链的相关内容,让我们最后再看一下数据包通过iptables的流程,如图 2-5 所示。

图 2-5。数据包通过iptables表和链的流程。一个圆圈表示iptables中存在的表/钩子组合。
所有iptables规则都属于一个表和链,其可能的组合在我们的流程图中表示为点。iptables根据数据包触发的 Netfilter 钩子的顺序评估链(及其中的规则),基于链表中的顺序进行评估。对于给定的链,iptables在每个存在的表中评估该链(请注意,某些链/表组合不存在,如 Filter/POSTROUTING)。如果我们追踪从本地主机起始的数据包的流程,则按以下顺序评估表/链对:
-
Raw/
OUTPUT -
Mangle/
OUTPUT -
NAT/
OUTPUT -
Filter/
OUTPUT -
Mangle/
POSTROUTING -
NAT/
POSTROUTING
子链
上述链是顶级或入口点链。然而,用户可以定义自己的子链,并使用 JUMP 目标执行它们。iptables以相同的方式执行这样的链,逐个目标地执行,直到匹配到终止目标为止。这对于逻辑分离或重用可以在多个上下文中执行的一系列目标非常有用(即,与将代码组织成函数的动机类似)。这样在链之间组织规则可以对性能产生重大影响。iptables实际上会对每个进出系统的数据包运行成十几甚至上百个if语句。这对数据包延迟、CPU 使用率和网络吞吐量都有可测量的影响。一个良好组织的链集合通过消除有效冗余的检查或操作来减少这种开销。然而,在具有许多 pod 的服务中,iptables的性能仍然是 Kubernetes 中的一个问题,这使得其他少量或不使用iptables的解决方案,如 IPVS 或 eBPF,更具吸引力。
让我们来看一下如何在示例 2-6 中创建新的链。
示例 2-6。用于 SSH 防火墙的iptables链样本
# Create incoming-ssh chain.
$ iptables -N incoming-ssh
# Allow packets from specific IPs.
$ iptables -A incoming-ssh -s 10.0.0.1 -j ACCEPT
$ iptables -A incoming-ssh -s 10.0.0.2 -j ACCEPT
# Log the packet.
$ iptables -A incoming-ssh -j LOG --log-level info --log-prefix "ssh-failure"
# Drop packets from all other IPs.
$ iptables -A incoming-ssh -j DROP
# Evaluate the incoming-ssh chain,
# if the packet is an inbound TCP packet addressed to port 22.
$ iptables -A INPUT -p tcp --dport 22 -j incoming-ssh
此示例创建一个名为incoming-ssh的新链路,用于评估进入端口 22 的 TCP 数据包。该链路允许来自两个特定 IP 地址的数据包通过,其他地址的数据包则被记录并丢弃。
链路过滤结束于默认操作,例如如果没有匹配到前面的目标,则丢弃数据包。如果没有指定默认值,链路将默认为ACCEPT。iptables -P <chain> <target>设置默认操作。
iptables 规则
规则有两部分:匹配条件和动作(称为目标)。匹配条件描述数据包属性。如果数据包匹配,则执行动作。如果数据包不匹配,则iptables将移动到下一条规则。
匹配条件检查给定数据包是否满足某些条件,例如,数据包是否具有特定的源地址。记住表/链路的操作顺序非常重要,因为先前的操作可以通过变异、丢弃或拒绝数据包来影响数据包。表 2-8 显示了一些常见的匹配类型。
表 2-8. 一些常见的iptables匹配类型
| 匹配类型 | 标志 | 描述 |
|---|---|---|
| 源 | -s, --src, --source |
匹配具有指定源地址的数据包。 |
| 目的地 | -d, --dest, --destination |
匹配具有指定目标源地址的数据包。 |
| 协议 | -p, --protocol |
匹配具有指定协议的数据包。 |
| 输入接口 | -i, --in-interface |
匹配通过指定接口输入的数据包。 |
| 输出接口 | -o, --out-interface |
匹配正在离开指定接口的数据包。 |
| 状态 | -m state --state <states> |
匹配处于指定逗号分隔状态的连接的数据包。这使用 Conntrack 状态(NEW,ESTABLISHED,RELATED,INVALID)。 |
注意
使用-m或--match,iptables可以使用扩展来进行匹配条件。扩展包括诸如在单个规则中指定多个端口(multiport)等便利功能,以及诸如 eBPF 交互等更复杂的功能。man iptables-extensions包含更多信息。
目标动作有两种类型:终止和非终止。终止目标将阻止iptables继续检查链路中的后续目标,实际上起到最终决定作用。非终止目标将允许iptables继续检查链路中的后续目标。ACCEPT,DROP,REJECT和RETURN都是终止目标。注意,ACCEPT和RETURN仅在其链路内为终止目标。也就是说,如果数据包在子链中击中了ACCEPT目标,则父链将继续处理,并可能丢弃或拒绝目标。示例 2-7 显示了一组规则,这些规则将拒绝到端口 80 的数据包,尽管在某个时刻匹配到了ACCEPT。为简化起见,已删除某些命令输出。
示例 2-7. 会拒绝之前接受的一些数据包的规则序列
$ iptables -L --line-numbers
链 INPUT (策略 ACCEPT)
num 目标 协议 选项 源 目的地
1 接受所有 所有 -- 任意地方 任意地方
2 拒绝 tcp -- 任意地方 任意地方
tcp dpt:80 reject-with icmp-port-unreachable
链 accept-all (1 references)
num 目标 协议 选项 源 目的地
1 所有 -- 任意地方 任意地方
表 2-9 总结了常见的目标类型及其行为。
表 2-9. 常见的iptables目标类型及其行为
| 目标类型 | 适用表格 | 描述 |
|---|---|---|
AUDIT |
所有 | 记录有关接受、丢弃或拒绝的数据包的数据。 |
ACCEPT |
过滤 | 允许数据包继续传输,无需进一步修改。 |
DNAT |
NAT | 修改目标地址。 |
DROPs |
过滤 | 丢弃数据包。对外部观察者来说,看起来就像从未接收到数据包一样。 |
JUMP |
所有 | 执行另一个链。一旦那个链执行完毕,父链的执行将继续。 |
LOG |
所有 | 记录数据包内容到内核日志中。 |
MARK |
所有 | 为数据包设置一个特殊整数,被 Netfilter 用作标识符。该整数可以在其他iptables决策中使用,但不会写入数据包本身。 |
MASQUERADE |
NAT | 修改数据包的源地址,用指定网络接口的地址替换。这类似于 SNAT,但不需要提前知道机器的 IP 地址。 |
REJECT |
过滤 | 丢弃数据包并发送拒绝原因。 |
RETURN |
所有 | 停止处理当前链(或子链)。请注意,这不是一个终止目标,如果有父链,那个链将继续处理。 |
SNAT |
NAT | 修改数据包的源地址,用固定地址替换。参见:MASQUERADE。 |
每种目标类型可能具有特定的选项,如端口或日志字符串,适用于规则。表 2-10 显示了一些示例命令及其解释。
表 2-10. iptables目标命令示例
| 命令 | 解释 |
|---|---|
| iptables -A INPUT -s 10.0.0.1 | 如果源地址是10.0.0.1,则接受进入的数据包。 |
| iptables -A INPUT -p ICMP | 允许所有进入的 ICMP 数据包。 |
| iptables -A INPUT -p tcp --dport 443 | 允许所有进入的 TCP 数据包到 443 端口。 |
| iptables -A INPUT -p tcp --dport 22 -j DROP | 拒绝所有进入的 TCP 数据包到 22 端口。 |
一个目标同时属于一个表和一个链,它控制iptables在给定数据包时(如果有的话)执行上述目标的时机。接下来,我们将总结所学内容,并实际查看iptables命令。
实际的 iptables
您可以使用iptables -L显示iptables链:
$ iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
警告
还有一个完全独立但几乎相同的程序 ip6tables,用于管理 IPv6 规则。iptables 和 ip6tables 规则是完全独立的。例如,使用 iptables 拒绝所有到 TCP 0.0.0.0:22 的数据包不会阻止 TCP [::]:22 的连接,反之亦然。
为简单起见,在本节中我们将仅提到 iptables 和 IPv4 地址。
--line-numbers 在链中为每条规则显示编号。这在插入或删除规则时非常有用。-I <chain> <line> 在指定的行号之前插入规则,之前的规则在该行上。
与 iptables 规则交互的典型命令格式为:
iptables [-t table] {-A|-C|-D} chain rule-specification
-A 是 追加,-C 是 检查,-D 是 删除。
警告
iptables 规则在重新启动后不会持久保存。iptables 提供了 iptables-save 和 iptables-restore 工具,可以手动使用或简单自动化捕获或重新加载规则。大多数防火墙工具在系统启动时都会自动创建自己的 iptables 规则,因此这一点并不突出。
iptables 可以伪装连接,使数据包看起来像是来自它们自己的 IP 地址。这对于向外界提供简化的外观非常有用。常见用例是为流量提供已知的主机,作为安全堡垒,或向第三方提供可预测的 IP 地址集合。在 Kubernetes 中,伪装可以使得 Pod 使用它们所在节点的 IP 地址,尽管 Pod 具有独特的 IP 地址。这在许多设置中是必要的,其中 Pod 具有内部 IP 地址,无法直接与互联网通信。MASQUERADE 目标类似于 SNAT;但不需要事先知道和指定 --source-address。而是使用指定接口的地址。在新源地址是静态的情况下,这比 SNAT 稍微低效,因为 iptables 必须不断获取地址:
$iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables 可以执行连接级负载均衡,或更准确地说是连接分流。该技术依赖于 DNAT 规则和随机选择(以防止每个连接都被路由到第一个 DNAT 目标):
$ iptables -t nat -A OUTPUT -p tcp --dport 80 -d $FRONT_IP -m statistic \
--mode random --probability 0.5 -j DNAT --to-destination $BACKEND1_IP:80
$ iptables -t nat -A OUTPUT -p tcp --dport 80 -d $FRONT_IP \
-j DNAT --to-destination $BACKEND2_IP:80
在前面的例子中,有 50% 的几率路由到第一个后端。否则,数据包会继续到下一个规则,该规则保证将连接路由到第二个后端。当增加更多后端时,数学计算会变得有些繁琐。要使路由到任何后端的概率相等,第 n 个后端必须有 1/n 的概率被路由到。如果有三个后端,概率分别为 0.3(重复)、0.5 和 1:
Chain KUBE-SVC-I7EAKVFJLYM7WH25 (1 references)
target prot opt source destination
KUBE-SEP-LXP5RGXOX6SCIC6C all -- anywhere anywhere
statistic mode random probability 0.25000000000
KUBE-SEP-XRJTEP3YTXUYFBMK all -- anywhere anywhere
statistic mode random probability 0.33332999982
KUBE-SEP-OMZR4HWUSCJLN33U all -- anywhere anywhere
statistic mode random probability 0.50000000000
KUBE-SEP-EELL7LVIDZU4CPY6 all -- anywhere anywhere
当 Kubernetes 使用 iptables 负载均衡服务时,会创建一个如上所示的链。仔细观察可以看到一个概率数字中的四舍五入误差。
使用 DNAT fan-out 进行负载均衡有几个注意事项。它对于给定后端的负载没有反馈,并且总是将应用层查询映射到相同的后端连接。由于 DNAT 结果持续连接的生命周期,如果长连接很常见,许多下游客户端可能会粘附到相同的上游后端,如果该后端的生命周期比其他后端长。以 Kubernetes 为例,假设一个 gRPC 服务只有两个副本,然后额外的副本进行了扩展。gRPC 会重用相同的 HTTP/2 连接,因此现有的下游客户端(使用 Kubernetes 服务而不是 gRPC 负载均衡)将保持连接到最初的两个副本,使 gRPC 后端之间的负载配置不均。因此,许多开发人员使用更智能的客户端(例如利用 gRPC 的客户端负载均衡),在服务器端和/或客户端强制定期重新连接,或者使用服务网格来外部化问题。我们将在第 4 和 5 章节中更详细地讨论负载均衡。
尽管 iptables 在 Linux 中被广泛使用,但在存在大量规则时可能变得缓慢,并且提供有限的负载均衡功能。接下来我们将看一下 IPVS,这是一个更适用于负载均衡的替代方案。
IPVS
IP 虚拟服务器(IPVS)是一个 Linux 连接(L4)负载均衡器。图 2-6 显示了 IPVS 在路由数据包中的作用的简单图示。

图 2-6. IPVS
iptables 可以通过随机路由连接来进行简单的 L4 负载均衡,随机性由各个 DNAT 规则上的权重塑造。IPVS 支持多种负载均衡模式(与 iptables 不同),这些模式在 表 2-11 中概述。这使得 IPVS 可以根据 IPVS 配置和流量模式更有效地分散负载,而不同于 iptables。
表 2-11. Kubernetes 中支持的 IPVS 模式
| 名称 | 简码 | 描述 |
|---|---|---|
| 轮询 | rr |
将后续连接发送到循环中的“下一个”主机。与 iptables 启用的随机路由相比,这会增加发送到给定主机的后续连接之间的时间。 |
| 最少连接 | lc |
将连接发送到当前具有最少打开连接的主机。 |
| 目标哈希 | dh |
根据连接的目标地址确定性地将连接发送到特定主机。 |
| 源哈希 | sh |
根据连接的源地址确定性地将连接发送到特定主机。 |
| 最短预期延迟 | sed |
将连接发送到具有最低连接权重比的主机。 |
| 永不排队 | nq |
将连接发送到任何没有现有连接的主机,否则使用“最短预期延迟”策略。 |
IPVS 支持数据包转发模式:
-
NAT 重写源和目的地地址。
-
DR 将 IP 数据报封装在 IP 数据报中。
-
IP 隧道直接通过重写数据帧的 MAC 地址将数据包路由到后端服务器。
当涉及 iptables 作为负载均衡器的问题时,有三个方面需要考虑:
集群中的节点数
即使 Kubernetes 在版本 v1.6 中已经支持 5,000 个节点,kube-proxy 配合 iptables 作为一个瓶颈限制了将集群扩展到 5,000 个节点。例如,在一个 5,000 个节点的集群中,如果我们有 2,000 个服务,每个服务有 10 个 Pod,这将导致每个工作节点上至少有 20,000 条 iptables 记录,这可能会使内核非常繁忙。
时间
在有 5,000 个服务(40,000 条规则)时,添加一条规则的时间为 11 分钟。对于 20,000 个服务(160,000 条规则),需要 5 小时。
延迟
访问服务的延迟(路由延迟);每个数据包必须遍历 iptables 列表直到匹配为止。添加/删除规则的延迟,插入和移除大量规则是一个大规模的繁重操作。
IPVS 还支持会话亲和性,这在服务中作为一个选项公开(Service.spec.sessionAffinity 和 Service.spec.sessionAffinityConfig)。在会话亲和时间窗口内的重复连接将路由到同一主机。这对于减少缓存未命中等场景很有用。在 Kubernetes 中,这也可以使任何模式下的路由实际上是有状态的(通过将来自同一地址的连接无限期地路由到同一主机),但在路由的粘性上不太绝对。
要创建一个基本的负载均衡器,使用两个权重相等的目的地,运行ipvsadm -A -t <address> -s <mode>。-A、-E 和 -D 分别用于添加、编辑和删除虚拟服务。小写形式 -a、-e 和 -d 分别用于添加、编辑和删除主机后端:
# ipvsadm -A -t 1.1.1.1:80 -s lc
# ipvsadm -a -t 1.1.1.1:80 -r 2.2.2.2 -m -w 100
# ipvsadm -a -t 1.1.1.1:80 -r 3.3.3.3 -m -w 100
您可以使用 -L 列出 IPVS 主机。显示每个虚拟服务器(唯一的 IP 地址和端口组合)及其后端:
# ipvsadm -L
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 1.1.1.1.80:http lc
-> 2.2.2.2:http Masq 100 0 0
-> 3.3.3.3:http Masq 100 0 0
-L 支持多个选项,如--stats,以显示额外的连接统计信息。
eBPF
eBPF 是一种允许在内核中运行特殊沙箱程序的编程系统,不像我们在 Netfilter 和iptables 中看到的那样需要在内核和用户空间之间来回传递。
在 eBPF 之前,有 Berkeley Packet Filter(BPF)。BPF 是一种在内核中使用的技术,用于分析网络流量等内容。BPF 支持过滤数据包,允许用户空间进程提供一个过滤器,指定要检查的数据包。其中一个 BPF 的用例是tcpdump,如图 2-7 所示。当您在tcpdump上指定过滤器时,它会将其编译为一个 BPF 程序并传递给 BPF。BPF 中的技术已扩展到其他进程和内核操作中。

图 2-7. tcpdump
一个 eBPF 程序可以直接访问系统调用。eBPF 程序可以直接监视和阻止系统调用,而无需通常的向用户空间程序添加内核钩子的方法。由于其性能特征,它非常适合编写网络软件。
Tip
您可以在其 网站 上了解更多关于 eBPF 的信息。
除了套接字过滤外,内核中支持的其他附加点如下:
Kprobes
内核组件的动态跟踪。
Uprobes
用户空间跟踪。
Tracepoints
内核静态跟踪。这些由开发人员编程到内核中,比 kprobes 更稳定,后者可能在内核版本之间变化。
perf_events
数据和事件的定时采样。
XDP
特殊的 eBPF 程序可以低于内核空间,访问驱动程序空间,直接处理数据包。
让我们以 tcpdump 为例。图 2-8 显示了 tcpdump 与 eBPF 的简化互动。

图 2-8. eBPF 示例
假设我们运行 tcpdump -i any。
字符串通过 pcap_compile 编译为 BPF 程序。然后内核将使用此 BPF 程序过滤我们指定的所有网络设备上通过的所有数据包,例如我们的 -I。
它将通过一个映射使这些数据可用于 tcpdump。映射是一种由 BPF 程序用于交换数据的键值对数据结构。
使用 eBPF 与 Kubernetes 的许多原因:
性能(哈希表与 iptables 列表)
对于每个添加到 Kubernetes 的服务,必须遍历的 iptables 规则列表呈指数增长。由于缺乏增量更新,每次添加新规则时都必须替换整个规则列表。这导致安装代表 20,000 个 Kubernetes 服务的 160,000 个 iptables 规则的总时长达到 5 小时。
跟踪
使用 BPF,我们可以收集 pod 和容器级网络统计信息。BPF 套接字过滤器并不新鲜,但 cgroup 中的 BPF 套接字过滤器是新的。引入于 Linux 4.10 的 cgroup-bpf 允许将 eBPF 程序附加到 cgroup。一旦附加,该程序将为进入或退出 cgroup 中任何进程的所有数据包执行。
使用 eBPF 对 kubectl exec 进行审计
使用 eBPF,您可以附加一个程序,记录在 kubectl exec 会话中执行的任何命令,并将这些命令传递给一个用户空间程序,记录这些事件。
安全性
Seccomp
安全计算,限制允许的系统调用。Seccomp 过滤器可以在 eBPF 中编写。
Falco
使用 eBPF 的开源容器本地运行时安全性。
Kubernetes 中 eBPF 最常见的用途是 Cilium、CNI 和服务实现。Cilium 取代了 kube-proxy,后者写入 iptables 规则以将服务的 IP 地址映射到相应的 pod。
通过 eBPF,Cilium 可以在内核中直接拦截和路由所有数据包,这样做更快,并允许应用级别(第 7 层)的负载均衡。我们将在 第 4 章 中讨论 kube-proxy。
网络故障排除工具
用 Linux 解决与网络相关的问题是一个复杂的主题,甚至可以轻松填写一本书。在本节中,我们将介绍一些关键的故障排除工具及其基本用法(表 2-12 提供了一个简单的工具和适用用例的备忘单)。把这一节看作是常见 Kubernetes 相关工具使用的起点。手册页、--help 和互联网可以进一步指导你。我们描述的工具之间存在重叠,因此你可能会发现学习某些工具(或工具功能)会有重复。某些工具更适合特定任务(例如,多个工具可以捕获 TLS 错误,但 OpenSSL 提供了最丰富的调试信息)。具体的工具使用可能取决于个人偏好、熟悉程度和可用性。
表 2-12. 常见故障排除案例和工具备忘单
| 情况 | 工具 |
|---|---|
| 检查连接性 | traceroute,ping,telnet,netcat |
| 端口扫描 | nmap |
| 检查 DNS 记录 | dig,在“检查连接性”中提到的命令 |
| 检查 HTTP/1 | cURL,telnet,netcat |
| 检查 HTTPS | OpenSSL,cURL |
| 检查监听程序 | netstat |
我们描述的一些网络工具可能不会预装在你选择的发行版中,但所有这些工具应该都可以通过你发行版的软件包管理器获得。在命令输出中,有时我们会使用 # Truncated 来表示我们已经省略了文本,以避免示例变得重复或过长。
安全警告
在深入工具细节之前,我们需要讨论安全性。攻击者可以利用此处列出的任何工具来探索和访问其他系统。关于此话题有很多强烈的意见,但我们认为最佳实践是在给定的机器上留下尽可能少的网络工具。
攻击者仍然可以下载工具本身(例如,通过从互联网下载二进制文件)或使用标准软件包管理器(如果他们具有足够的权限)。在大多数情况下,你只是在探索和利用之前引入了一些额外的摩擦。但是,在某些情况下,通过不预安装网络工具,你可以减少攻击者的能力。
Linux 文件权限包括一个称为setuid 位的内容,有时被网络工具使用。如果文件设置了 setuid 位,执行该文件将导致文件作为文件所有者而不是当前用户执行。你可以通过查看文件权限输出中的s而不是x来观察到这一点:
$ ls -la /etc/passwd
-rwsr-xr-x 1 root root 68208 May 28 2020 /usr/bin/passwd
这允许程序暴露有限的特权功能(例如,passwd 使用此功能允许用户更新其密码,而不允许对密码文件进行任意写入)。许多网络工具(如 ping、nmap 等)可能会在某些系统上使用 setuid 位来发送原始数据包、嗅探数据包等。如果攻击者下载自己的工具副本且无法获得 root 权限,则他们将无法像系统安装的具有设置 setuid 位的工具那样进行更多操作。
ping
ping 是一个简单的程序,用于向网络设备发送 ICMP ECHO_REQUEST 数据包。它是一种常见且简单的方式,用于从一个主机向另一个主机测试网络连通性。
ICMP 是一个第四层协议,类似于 TCP 和 UDP。Kubernetes 服务支持 TCP 和 UDP,但不支持 ICMP。这意味着对 Kubernetes 服务的 ping 测试将始终失败。相反,您需要使用 telnet 或更高级的工具如 cURL 来检查与服务的连通性。根据您的网络配置,个别的 pod 可能仍可通过 ping 访问。
警告
防火墙和路由软件可感知 ICMP 数据包,并可配置以过滤或路由特定 ICMP 数据包。通常而言,允许 ICMP 数据包有宽松规则是常见的,但不一定是必须的(或建议的)。一些网络管理员、网络软件或云服务提供商会默认允许 ICMP 数据包。
ping 的基本用法是简单的 ping <address>。地址可以是 IP 地址或域名。ping 将发送一个数据包,并在响应或超时时报告请求的状态。
默认情况下,ping 将无限发送数据包,并需要手动停止(例如,使用 Ctrl-C)。-c <count> 将使 ping 在关闭之前执行指定数量的数据包。关闭时,ping 还会打印汇总信息:
$ ping -c 2 k8s.io
PING k8s.io (34.107.204.206): 56 data bytes
64 bytes from 34.107.204.206: icmp_seq=0 ttl=117 time=12.665 ms
64 bytes from 34.107.204.206: icmp_seq=1 ttl=117 time=12.403 ms
--- k8s.io ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 12.403/12.534/12.665/0.131 ms
表 2-13 显示了常见的 ping 选项。
表 2-13. 有用的 ping 选项
| 选项 | 描述 |
|---|---|
| -c |
发送指定数量的数据包。在接收到最后一个数据包或超时后退出。 |
| -i |
设置发送数据包之间的等待间隔,默认为 1 秒。不推荐设置过低的值,因为 ping 可能会导致网络洪泛。 |
| -o | 收到 1 个数据包后退出。相当于 -c 1。 |
| -S |
使用指定的源地址发送数据包。 |
| -W |
设置接收数据包的等待间隔。如果 ping 在等待时间之后接收到数据包,它仍将计入最终的汇总信息。 |
traceroute
traceroute 显示从一个主机到另一个主机所采取的网络路径。这允许用户轻松验证和调试从一台机器到另一台机器的路径(或路由失败的位置)。
traceroute 发送具有特定 IP 生存时间值的数据包。回顾来自 第一章 的信息,每个处理数据包的主机都会将数据包的生存时间(TTL)减少 1,从而限制请求可以经过的主机数量。当主机接收到一个数据包并将 TTL 减少到 0 时,它会发送一个 TIME_EXCEEDED 包并丢弃原始数据包。TIME_EXCEEDED 响应包含数据包超时的机器的源地址。通过从 TTL 为 1 开始并为每个数据包增加 1 的方式,traceroute 能够从源地址到目的地址的每个主机获得响应。
traceroute 按行显示主机,从第一个外部机器开始。每行包含主机名(如果可用)、IP 地址和响应时间:
$traceroute k8s.io
traceroute to k8s.io (34.107.204.206), 64 hops max, 52 byte packets
1 router (10.0.0.1) 8.061 ms 2.273 ms 1.576 ms
2 192.168.1.254 (192.168.1.254) 2.037 ms 1.856 ms 1.835 ms
3 adsl-71-145-208-1.dsl.austtx.sbcglobal.net (71.145.208.1)
4.675 ms 7.179 ms 9.930 ms
4 * * *
5 12.122.149.186 (12.122.149.186) 20.272 ms 8.142 ms 8.046 ms
6 sffca22crs.ip.att.net (12.122.3.70) 14.715 ms 8.257 ms 12.038 ms
7 12.122.163.61 (12.122.163.61) 5.057 ms 4.963 ms 5.004 ms
8 12.255.10.236 (12.255.10.236) 5.560 ms
12.255.10.238 (12.255.10.238) 6.396 ms
12.255.10.236 (12.255.10.236) 5.729 ms
9 * * *
10 206.204.107.34.bc.googleusercontent.com (34.107.204.206)
64.473 ms 10.008 ms 9.321 ms
如果 traceroute 在超时之前从某一跳收不到响应,它会打印一个***。一些主机可能拒绝发送 TIME_EXCEEDED 包,或者沿途的防火墙可能阻止成功传递。
表 2-14 展示了常见的 traceroute 选项。
表 2-14. 有用的 traceroute 选项
| 选项 | 语法 | 描述 |
|---|---|---|
| 初始 TTL | -f <TTL>, -M <TTL> |
设置起始 IP TTL(默认值:1)。将 TTL 设置为 n 将导致 traceroute 不报告前 n-1 个路由到目的地的主机。 |
| 最大 TTL | -m <TTL> |
设置最大 TTL,即 traceroute 将尝试通过的最大主机数量。 |
| 协议 | -P <protocol> |
发送指定协议的数据包(TCP、UDP、ICMP,有时还有其他选项)。UDP 是默认协议。 |
| 源地址 | -s <address> |
指定出站数据包的源 IP 地址。 |
| 等待时间 | -w <seconds> |
设置等待探测响应的时间。 |
dig
dig 是一个 DNS 查询工具。您可以使用它从命令行进行 DNS 查询并显示结果。
dig 命令的一般形式是 dig [options] <domain>。默认情况下,dig 将显示 CNAME、A 和 AAAA 记录:
$ dig kubernetes.io
; <<>> DiG 9.10.6 <<>> kubernetes.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51818
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1452
;; QUESTION SECTION:
;kubernetes.io. IN A
;; ANSWER SECTION:
kubernetes.io. 960 IN A 147.75.40.148
;; Query time: 12 msec
;; SERVER: 2600:1700:2800:7d4f:6238:e0ff:fe08:6a7b#53
(2600:1700:2800:7d4f:6238:e0ff:fe08:6a7b)
;; WHEN: Mon Jul 06 00:10:35 PDT 2020
;; MSG SIZE rcvd: 71
要显示特定类型的 DNS 记录,运行 dig <domain> <type>(或 dig -t <type> <domain>)。这绝大多数是 dig 的主要用例:
$ dig kubernetes.io TXT
; <<>> DiG 9.10.6 <<>> -t TXT kubernetes.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16443
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;kubernetes.io. IN TXT
;; ANSWER SECTION:
kubernetes.io. 3599 IN TXT
"v=spf1 include:_spf.google.com ~all"
kubernetes.io. 3599 IN TXT
"google-site-verification=oPORCoq9XU6CmaR7G_bV00CLmEz-wLGOL7SXpeEuTt8"
;; Query time: 49 msec
;; SERVER: 2600:1700:2800:7d4f:6238:e0ff:fe08:6a7b#53
(2600:1700:2800:7d4f:6238:e0ff:fe08:6a7b)
;; WHEN: Sat Aug 08 18:11:48 PDT 2020
;; MSG SIZE rcvd: 171
表 2-15 展示了常见的 dig 选项。
表 2-15. 有用的 dig 选项
| 选项 | 语法 | 描述 |
|---|---|---|
| IPv4 | -4 |
仅使用 IPv4。 |
| IPv6 | -6 |
仅使用 IPv6。 |
| 地址 | -b <address>[#<port>] |
指定要进行 DNS 查询的地址。端口可以选择包括,前面带有 #。 |
| 端口 | -p <port> |
指定要查询的端口,如果 DNS 暴露在非标准端口上。默认为 53,DNS 标准端口。 |
| 域名 | -q <domain> |
要查询的域名。域名通常作为位置参数指定。 |
| 记录类型 | -t <type> |
要查询的 DNS 记录类型。也可以将记录类型指定为位置参数。 |
telnet
telnet 既是一种网络协议,也是使用该协议的工具。telnet 曾用于远程登录,类似于 SSH。由于 SSH 具有更好的安全性,因此 SSH 已成为主流,但 telnet 仍然非常适用于调试使用基于文本的协议的服务器。例如,使用 telnet,您可以连接到 HTTP/1 服务器并手动发出请求。
telnet 的基本语法是 telnet <address> <port>。这将建立连接并提供一个交互式命令行界面。按两次 Enter 将发送一个命令,这样可以轻松编写多行命令。按 Ctrl-J 退出会话:
$ telnet kubernetes.io
Trying 147.75.40.148...
Connected to kubernetes.io.
Escape character is '^]'.
> HEAD / HTTP/1.1
> Host: kubernetes.io
>
HTTP/1.1 301 Moved Permanently
Cache-Control: public, max-age=0, must-revalidate
Content-Length: 0
Content-Type: text/plain
Date: Thu, 30 Jul 2020 01:23:53 GMT
Location: https://kubernetes.io/
Age: 2
Connection: keep-alive
Server: Netlify
X-NF-Request-ID: a48579f7-a045-4f13-af1a-eeaa69a81b2f-23395499
要充分利用 telnet,您需要了解您使用的应用程序协议的工作方式。telnet 是一个经典的工具,用于调试运行 HTTP、HTTPS、POP3、IMAP 等服务器。
nmap
nmap 是一个端口扫描程序,允许您探索和检查网络上的服务。
nmap 的一般语法是 nmap [选项] <目标>,其中目标可以是域、IP 地址或 IP CIDR。nmap 的默认选项将快速而简要地总结主机上开放的端口:
$ nmap 1.2.3.4
Starting Nmap 7.80 ( https://nmap.org ) at 2020-07-29 20:14 PDT
Nmap scan report for my-host (1.2.3.4)
Host is up (0.011s latency).
Not shown: 997 closed ports
PORT STATE SERVICE
22/tcp open ssh
3000/tcp open ppp
5432/tcp open postgresql
Nmap done: 1 IP address (1 host up) scanned in 0.45 seconds
在前面的例子中,nmap 检测到三个开放的端口,并猜测每个端口上运行的服务。
提示
因为 nmap 可以快速显示远程机器上可访问的服务,它可以快速而轻松地发现不应该暴露的服务。出于这个原因,nmap 是攻击者的最爱工具。
nmap 有大量的选项,可以改变扫描行为和提供的详细级别。与其他命令一样,我们将总结一些关键选项,但我们强烈建议阅读 nmap 的帮助/手册页面。
表 2-16 显示常见的 nmap 选项。
表 2-16. 有用的 nmap 选项
| 选项 | 语法 | 描述 |
|---|---|---|
| 附加检测 | -A |
启用操作系统检测、版本检测等功能。 |
| 减少详细程度 | -d |
减少命令的详细程度。使用多个 d(例如 -dd)会增加效果。 |
| 增加详细程度 | -v |
增加命令的详细程度。使用多个 v(例如 -vv)会增加效果。 |
netstat
netstat 可以显示关于机器网络堆栈和连接的广泛信息:
$ netstat
Active internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 164 my-host:ssh laptop:50113 ESTABLISHED
tcp 0 0 my-host:50051 example-host:48760 ESTABLISHED
tcp6 0 0 2600:1700:2800:7d:54310 2600:1901:0:bae2::https TIME_WAIT
udp6 0 0 localhost:38125 localhost:38125 ESTABLISHED
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags Type State I-Node Path
unix 13 [ ] DGRAM 8451 /run/systemd/journal/dev-log
unix 2 [ ] DGRAM 8463 /run/systemd/journal/syslog
[Cut for brevity]
不带任何附加参数调用 netstat 将显示机器上所有连接的套接字。在我们的例子中,我们看到三个 TCP 套接字,一个 UDP 套接字和大量的 UNIX 套接字。输出包括连接的两端的地址(IP 地址和端口)。
我们可以使用 -a 标志来显示所有连接,或者使用 -l 来仅显示监听连接:
$ netstat -a
Active internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:ssh 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:postgresql 0.0.0.0:* LISTEN
tcp 0 172 my-host:ssh laptop:50113 ESTABLISHED
[Content cut]
netstat 的常见用途是检查哪个进程正在监听特定端口。为此,我们运行 sudo netstat -lp - -l 表示“监听”,-p 表示“程序”。可能需要使用 sudo 来查看 netstat 的所有程序信息。对于 -l 的输出显示服务正在监听的地址(例如 0.0.0.0 或 127.0.0.1)。
当我们寻找特定结果时,可以使用像 grep 这样的简单工具从 netstat 中获取清晰的输出:
$ sudo netstat -lp | grep 3000
tcp6 0 0 [::]:3000 [::]:* LISTEN 613/grafana-server
表 2-17 展示了常见的 netstat 选项。
表 2-17. 有用的 netstat 命令
| 选项 | 语法 | 描述 |
|---|---|---|
| 显示所有套接字 | netstat -a |
显示所有套接字,而不仅仅是打开的连接。 |
| 显示统计信息 | netstat -s |
显示网络统计信息。默认情况下,netstat 显示所有协议的统计信息。 |
| 显示监听套接字 | netstat -l |
显示正在监听的套接字。这是查找运行服务的简便方法。 |
| TCP | netstat -t |
-t 标志仅显示 TCP 数据。它可以与其他标志一起使用,例如 -lt(显示监听 TCP 的套接字)。 |
| UDP | netstat -u |
-u 标志仅显示 UDP 数据。它可以与其他标志一起使用,例如 -lu(显示监听 UDP 的套接字)。 |
netcat
netcat 是一个多功能工具,用于建立连接、发送数据或在套接字上侦听。作为一种“手动”运行服务器或客户端以详细检查发生情况的方式,netcat 可能与 telnet 类似,尽管 netcat 能够做更多事情。
提示
nc 在大多数系统上是 netcat 的别名。
netcat 可以作为 netcat <地址> <端口> 被调用以连接服务器。netcat 具有交互式的 stdin,允许您手动输入数据或将数据管道传输到 netcat。到目前为止,它非常类似于 telnet。
$ echo -e "GET / HTTP/1.1\nHost: localhost\n" > cmd
$ nc localhost 80 < cmd
HTTP/1.1 302 Found
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
[Content cut]
Openssl
OpenSSL 技术支持世界上大部分 HTTPS 连接。大部分 OpenSSL 的重要工作是通过语言绑定完成的,但它也有用于操作任务和调试的命令行界面。openssl 能够执行诸如创建密钥和证书、签署证书以及最相关于我们的测试 TLS/SSL 连接的操作。许多其他工具,包括本章中介绍的工具,都可以测试 TLS/SSL 连接。然而,openssl 以其丰富的功能和详细程度脱颖而出。
命令通常采用 openssl [子命令] [参数] [选项] 的形式。openssl 拥有大量的子命令(例如,openssl rand 允许您生成伪随机数据)。list 子命令允许您列出功能,具有一些搜索选项(例如,openssl list --commands 用于列出命令)。要了解有关单个子命令的更多信息,可以检查 openssl <子命令> --help 或其 man 页面(man openssl-<子命令> 或仅 man <子命令>)。
openssl s_client -connect 将连接到服务器并显示有关服务器证书的详细信息。以下是默认调用方式:
openssl s_client -connect k8s.io:443
CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = k8s.io
verify return:1
---
Certificate chain
0 s:CN = k8s.io
i:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
1 s:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
i:O = Digital Signature Trust Co., CN = DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
[Content cut]
-----END CERTIFICATE-----
subject=CN = k8s.io
issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 3915 bytes and written 378 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
如果你在使用自签名的 CA 证书,可以使用 -CAfile <path> 来指定该 CA。这将允许你建立和验证针对自签名证书的连接。
cURL
cURL 是一个支持多种协议(尤其是 HTTP 和 HTTPS)的数据传输工具。
提示
wget 是一个与 curl 类似的工具。一些发行版或管理员可能会安装它,而不是 curl。
cURL 命令的形式为 curl [options] <URL>。cURL 将 URL 的内容打印到标准输出,有时还会打印 cURL 特定的消息。默认行为是发起 HTTP GET 请求:
$ curl example.org
<!doctype html>
<html>
<head>
<title>Example Domain</title>
# Truncated
cURL 默认不会自动遵循重定向,比如 HTTP 301 或协议升级。使用 -L 标志(或 --location)将启用重定向跟随功能:
$ curl kubernetes.io
Redirecting to https://kubernetes.io
$ curl -L kubernetes.io
<!doctype html><html lang=en class=no-js><head>
# Truncated
使用 -X 选项执行特定的 HTTP 动词;例如,使用 curl -X DELETE foo/bar 进行 DELETE 请求。
你可以通过几种方式提供数据(用于 POST、PUT 等请求):
-
URL 编码格式:
-d "key1=value1&key2=value2" -
JSON 格式:
-d '{"key1":"value1", "key2":"value2"}' -
作为文件:
-d @data.txt
-H 选项添加显式头信息,尽管基本的头信息如 Content-Type 会自动添加:
-H "Content-Type: application/x-www-form-urlencoded"
下面是一些示例:
$ curl -d "key1=value1" -X PUT localhost:8080
$ curl -H "X-App-Auth: xyz" -d "key1=value1&key2=value2"
-X POST https://localhost:8080/demo
提示
cURL 在调试 TLS 问题时可能会有所帮助,但像 openssl 这样的更专业的工具可能更有帮助。
cURL 可以帮助诊断 TLS 问题。与可靠的浏览器一样,cURL 验证 HTTP 站点返回的证书链,并与主机的 CA 证书进行检查:
$ curl https://expired-tls-site
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
类似于许多程序,cURL 具有详细输出标志 -v,在调试 HTTP 等第 7 层协议时非常有价值:
$ curl https://expired-tls-site -v
* Trying 1.2.3.4...
* TCP_NODELAY set
* Connected to expired-tls-site (1.2.3.4) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, certificate expired (557):
* SSL certificate problem: certificate has expired
* Closing connection 0
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.haxx.se/docs/sslcerts.html
# Truncated
cURL 还有许多未涉及的其他功能,例如超时设置、自定义 CA 证书、自定义 DNS 等。
结论
本章为你提供了 Linux 网络的一个快速概览。我们主要关注了理解 Kubernetes 实现、集群设置限制以及调试与 Kubernetes 相关的网络问题所需的概念(无论是在 Kubernetes 上的工作负载还是 Kubernetes 本身)。本章内容并非详尽无遗,你可能会发现进一步学习是很有价值的。
接下来,我们将开始查看 Linux 中的容器及其与网络的交互。
第三章:容器网络基础知识
现在我们已经讨论了网络基础知识和 Linux 网络,接下来我们将讨论容器中网络是如何实现的。与网络类似,容器也有着悠久的历史。本章将回顾历史,讨论运行容器的各种选项,并探索可用的网络设置。目前,行业已经将 Docker 作为容器运行时的标准。因此,我们将深入探讨 Docker 网络模型,解释 CNI 与 Docker 网络模型的区别,并通过 Docker 容器的网络模式示例结束本章。
容器简介
在本节中,我们将讨论导致我们使用容器运行应用程序的演变过程。一些人会说容器不是真正的。它们只是操作系统内核底层技术的另一种抽象。技术上是正确的,但却忽略了技术的实质,也没有助于解决应用程序管理和部署这一艰难问题的路程。
应用程序
运行应用程序始终具有其挑战性。现在有很多种方法来提供应用程序:云端、本地部署,当然还有容器。应用程序开发人员和系统管理员面临许多问题,例如处理不同版本的库、完成部署的方法以及管理旧版本应用程序本身。长期以来,应用程序开发人员不得不处理这些问题。Bash 脚本和部署工具都有其缺点和问题。每家新公司都有自己的应用程序部署方式,因此每个新开发人员都必须学习这些技术。分工、权限控制和维护系统稳定性要求系统管理员限制开发人员对部署的访问。系统管理员还需管理同一主机上的多个应用程序,以提高该机器的效率,从而在开发人员希望部署新功能与系统管理员希望维护整个生态系统之间造成竞争。
通用操作系统支持尽可能多类型的应用程序,因此其内核包括各种驱动程序、协议库和调度程序。图 3-1 展示了一台机器,配备一种操作系统,但是有多种方法可以将应用程序部署到该主机上。应用程序部署是所有组织都必须解决的问题。

图 3-1 应用服务器
从网络的角度来看,一个操作系统只有一个 TCP/IP 堆栈。这一单一堆栈会在主机机器上创建端口冲突问题。系统管理员在同一台机器上托管多个应用程序,每个应用程序必须在其端口上运行。因此,现在,系统管理员、应用程序开发人员和网络工程师都必须共同协调这一切。增加部署清单中的任务包括创建故障排除指南并处理所有 IT 请求。虚拟化软件是提高一台主机机器效率、解决单一操作系统/网络堆栈问题的一种方法。
虚拟化软件
虚拟化软件从主机机器模拟硬件资源、CPU 和内存,创建客户操作系统或虚拟机。2001 年,VMware 发布了其 x86 虚拟化软件;早期版本包括 IBM 的 z/Architecture 和 FreeBSD jails。2003 年发布了第一个开源虚拟化软件 Xen,2006 年发布了基于内核的虚拟化软件(KVM)。虚拟化软件允许系统管理员与多个客户操作系统共享底层硬件;图 3-2 展示了这一点。这种资源共享提高了主机机器的效率,缓解了系统管理员的问题。
虚拟化软件还为每个应用开发团队提供了独立的网络堆栈,解决了共享系统上端口冲突的问题。例如,团队 A 的 Tomcat 应用可以在端口 8080 上运行,而团队 B 的应用也可以在端口 8080 上运行,因为每个应用现在都可以有其独立的客户操作系统和网络堆栈。但对于应用开发人员来说,仍然存在库版本、部署和其他问题。他们如何打包和部署应用程序所需的一切,同时保持虚拟化软件和虚拟机引入的效率?这一问题促使了容器的发展。

图 3-2. 虚拟化软件
容器
在 图 3-3 中,我们看到应用程序容器化的好处;每个容器都是独立的。应用程序开发人员可以使用他们需要的任何东西来运行他们的应用程序,而不依赖于底层库或主机操作系统。每个容器还有自己的网络堆栈。容器允许开发人员在保持主机机器效率的同时打包和部署应用程序。

图 3-3. 运行在主机操作系统上的容器
任何技术都有其变化、竞争和创新历史,容器也不例外。以下是学习容器时可能令人困惑的术语列表。首先,我们列出了容器运行时的区别,讨论了每个运行时的功能,并展示了它们与 Kubernetes 的关系。容器运行时的功能可以分为“高级”和“低级”:
容器
运行中的容器镜像。
图像
容器镜像是从注册服务器拉取并在本地作为挂载点使用的文件。
容器引擎
容器引擎通过命令行选项接受用户请求来拉取镜像并运行容器。
容器运行时
容器运行时是容器引擎中处理运行容器的底层软件组件。
基础镜像
容器镜像的起点;为了减少构建镜像的大小和复杂性,用户可以从基础镜像开始,并在其上进行增量更改。
镜像层
仓库通常被称为镜像或容器镜像,但实际上它们由一个或多个层组成。仓库中的镜像层以父子关系连接。每个镜像层代表其与父层之间的变化。
镜像格式
容器引擎具有自己的容器镜像格式,如 LXD、RKT 和 Docker。
注册表
注册表存储容器镜像,并允许用户上传、下载和更新容器镜像。
仓库
仓库可以等同于一个容器镜像。重要的区别在于仓库由层和有关镜像的元数据组成;这是清单。
标签
标签是容器镜像不同版本的用户定义名称。
容器主机
容器主机是运行带有容器引擎的系统。
容器编排
这就是 Kubernetes 的工作方式!它动态地为容器主机集群调度容器工作负载。
注意
Cgroups 和命名空间是用于创建容器的 Linux 原语;它们将在下一节中讨论。
“低级”功能的一个示例是为容器创建 cgroups 和命名空间,这是运行容器的最低要求。开发人员在处理容器时需要更多功能。他们需要构建和测试容器,并将其部署;这些被认为是“高级”功能。每个容器运行时都提供各种功能级别。以下是高级和低级功能的列表:
低级容器运行时功能
-
创建容器
-
运行容器
高级容器运行时功能
-
格式化容器镜像
-
构建容器镜像
-
管理容器镜像
-
管理容器实例
-
共享容器镜像
在接下来的几页中,我们将讨论实现前述功能的运行时。以下每个项目都有其优点和缺点,以提供高级和低级功能。一些项目因历史原因而值得了解,但已不复存在或已与其他项目合并:
低级容器运行时
LXC
用于创建 Linux 容器的 C API
runC
OCI 兼容容器的命令行界面
高级容器运行时
containerd
从 Docker 分离出来的容器运行时,一个毕业的 CNCF 项目
CRI-O
使用开放容器倡议(OCI)规范的容器运行时接口,一个孵化中的 CNCF 项目
Docker
开源容器平台
lmctfy
Google 容器化平台
rkt
CoreOS 容器规范
OCI
OCI 促进容器技术的通用、最小、开放标准和规范。
创建容器图像格式和运行时的正式规范的想法允许容器在所有主要操作系统和平台之间可移植,以确保没有不必要的技术障碍。 OCI 项目的三个价值观如下:
可组合
管理容器的工具应具有清晰的界面。它们也不应绑定到特定的项目、客户端或框架,并且应该在所有平台上工作。
去中心化
格式和运行时应由社区明确定义和开发,而不是一个组织。 OCI 项目的另一个目标是独立实现工具以运行相同的容器。
极简主义者
OCI 规范力求做好几件事,保持简洁和稳定,促进创新和实验。
Docker 捐赠了一个基础格式和运行时的草案。它还捐赠了用于 OCI 的参考实现代码。 Docker 使用 libcontainer 项目的内容,使其独立于 Docker 运行,并捐赠给 OCI 项目。该代码库是 runC,可以在 GitHub 找到。
让我们讨论几个早期的容器倡议及其能力。本节将结束 Kubernetes 在容器运行时及其如何协同工作的地方。
LXC
Linux 容器,LXC,创建于 2008 年。LXC 结合 cgroups 和命名空间,为运行应用程序提供了一个隔离的环境。LXC 的目标是尽可能接近标准 Linux 环境,而无需单独的内核。LXC 有独立的组件:liblxc 库、几种编程语言绑定、Python 版本 2 和 3、Lua、Go、Ruby、Haskell、一套标准工具和容器模板。
runC
runC 是最广泛使用的容器运行时,最初作为 Docker 的一部分开发,后来作为单独的工具和库进行提取。runC 是一个命令行工具,用于运行按照 OCI 格式打包的应用程序,并且是 OCI 规范的兼容实现。runC 使用的是 libcontainer,这与 Docker 引擎安装中的容器库相同。在 1.11 版本之前,Docker 引擎用于管理卷、网络、容器、镜像等。现在,Docker 架构有几个组件,而 runC 的特性包括以下内容:
-
全面支持 Linux 命名空间,包括用户命名空间
-
对 Linux 中所有可用的安全特性的本地支持
- SELinux、AppArmor、seccomp、控制组、能力降低、
pivot_root、UID/GID 降低等。
- SELinux、AppArmor、seccomp、控制组、能力降低、
-
Windows 10 容器的本地支持
-
计划为整个硬件制造商生态系统提供本地支持
-
由 Linux 基金会的 OCI 管理的正式指定的配置格式
containerd
containerd 是从 Docker 中拆分出来的一个高级运行时。containerd 是一个后台服务,作为各种容器运行时和操作系统的 API 门面。containerd 有各种组件提供高级功能。containerd 是 Linux 和 Windows 的服务,管理其主机系统的完整容器生命周期、镜像传输、存储、容器执行和网络附加。containerd 的客户端 CLI 工具是 ctr,用于开发和调试直接与 containerd 通信。containerd-shim 是允许无守护程序容器的组件。它作为容器进程的父进程驻留,以便完成几件事情。containerd 允许运行时(如 runC)在启动容器后退出。这样,我们就不需要为容器保持长时间运行的运行时进程。它还保持容器的标准 I/O 和其他文件描述符,如果 containerd 和 Docker 终止,则会关闭管道的父侧,容器会退出。如果 shim 未运行,则容器的退出状态也会向像 Docker 这样的高级工具报告,而不需要容器进程的实际父进程来执行此操作。
lmctfy
Google 在 2013 年以其开源 Linux 容器技术 lmctfy 开始。lmctfy 是一个高级容器运行时,提供创建和删除容器的能力,但目前已不再积极维护,并且被移植到现在的 containerd 中。lmctfy 提供了 API 驱动的配置,无需开发者关心 cgroups 和命名空间内部的细节。
rkt
rkt 在 2014 年作为 Docker 的替代品在 CoreOS 开始。它用 Go 语言编写,以 pod 作为基本计算单元,并允许为应用程序提供自包含环境。rkt 的原生镜像格式是 App Container Image (ACI),在 App Container 规范中定义;这已被 OCI 格式和规范支持所取代。它支持 CNI 规范,并且可以运行 Docker 镜像和 OCI 镜像。rkt 项目由维护者在 2020 年 2 月归档。
Docker
Docker 在 2013 年发布,解决了开发者在容器端到端运行中遇到的许多问题。它具备所有这些功能,供开发者创建、维护和部署容器:
-
格式化容器镜像
-
构建容器镜像
-
管理容器镜像
-
管理容器实例
-
共享容器镜像
-
运行容器
图 3-4 展示了 Docker 引擎及其各个组件的架构。Docker 最初是一个单体应用程序,将所有先前的功能构建成一个称为Docker 引擎的单一二进制文件。该引擎包含了允许开发人员构建、运行和推送容器和镜像的 Docker 客户端或 CLI。Docker 服务器作为守护进程运行,用于管理运行容器的数据卷和网络。客户端通过 Docker API 与服务器通信。它使用 containerd 管理容器生命周期,并使用 runC 生成容器进程。

图 3-4. Docker 引擎
在过去的几年中,Docker 已经将这个单体应用程序拆分成了单独的组件。要运行一个容器,Docker 引擎创建镜像并将其传递给 containerd。containerd 调用 containerd-shim,后者使用 runC 来运行容器。然后,containerd-shim 允许运行时(在这种情况下是 runC)在启动容器后退出。这样,我们可以运行无守护进程的容器,因为我们不需要为容器运行长时间运行的运行时进程。
Docker 为应用程序开发人员和系统管理员提供了关注点分离。它允许开发人员专注于构建他们的应用程序,而系统管理员专注于部署。Docker 提供了快速的开发周期;要测试我们 Web 应用程序的新版本的 Golang,我们可以更新基础镜像并对其运行测试。Docker 在本地运行、云端或任何其他数据中心中运行时提供了应用程序可移植性。其座右铭是构建、发布和随处运行。可以快速为可伸缩性而部署新的容器,并在一个主机上运行更多的应用程序,提高该主机的效率。
CRI-O
CRI-O 是基于 OCI 的 Kubernetes CRI 实现,OCI 是容器运行时引擎必须实现的一组规范。Red Hat 在 2016 年启动了 CRI 项目,并于 2019 年将其贡献给 CNCF。CRI 是一个插件接口,使 Kubernetes 能够通过Kubelet与满足 CRI 接口的任何容器运行时通信。在 Kubernetes 项目引入 CRI 后,CRI-O 的开发于 2016 年开始,CRI-O 1.0 于 2017 年发布。CRI-O 是一个轻量级的 CRI 运行时,作为基于 gRPC 和 Protobuf 的 UNIX 套接字上的专用于 Kubernetes 的高级运行时。图 3-5 指出了 CRI 在整个 Kubernetes 架构中的位置。CRI-O 在 Kubernetes 项目中提供稳定性,并致力于通过 Kubernetes 测试。

图 3-5. Kubernetes 的 CRI
在容器领域,已经涌现出许多公司、技术和创新。本节简要介绍了这方面的历史。行业已决定确保容器景观保持作为供所有人使用的开放 OCI 项目的状态。Kubernetes 在这方面的努力也为此做出了贡献,通过 CRI-O 接口的采用。理解容器的组件对所有容器部署的管理员和使用容器的开发者都至关重要。最近的一个例子是 Kubernetes 1.20 中,将停用 dockershim 支持。管理员使用 dockershim 的 Docker 运行时已被弃用,但开发者仍然可以使用 Docker 构建符合 OCI 标准的容器来运行。
注意
第一个 CRI 实现是 dockershim,它在 Docker 引擎前提供了一层抽象。
现在我们将更深入地了解支持容器技术。
容器基元
无论您使用 Docker 还是 containerd,runC 都会启动和管理它们的实际容器。在本节中,我们将从容器的角度审视 runC 为开发者处理的内容。我们的每个容器都有作为 Linux 原语的 控制组 和 命名空间。图 3-6 展示了这一外观的示例;cgroups 控制我们容器内核资源的访问,而命名空间则是独立的资源片段,可以与根命名空间(即主机)分开管理。

图 3-6. 命名空间和控制组
为了帮助巩固这些概念,让我们进一步深入控制组和命名空间。
控制组
简而言之,cgroup 是 Linux 内核的一个功能,用于限制、统计和隔离资源使用。最初在 Linux 2.6.24 中发布,cgroups 允许管理员控制不同 CPU 系统和内存的特定进程。cgroups 通过伪文件系统提供,并由 cgroups 中的核心内核代码维护。这些单独的子系统在内核中维护各种 cgroups:
CPU
可以保证进程至少有一定数量的 CPU 分享。
内存
这些设置了进程的内存限制。
磁盘 I/O
这些和其他设备通过设备 cgroup 子系统进行控制。
网络
这由 net_cls 维护,并标记离开 cgroup 的数据包。
lscgroup 是一个列出系统中当前所有 cgroups 的命令行工具。
runC 将在容器创建时创建这些 cgroup。cgroup 控制容器可以使用的资源量,而命名空间控制容器内的进程可以看到的内容。
命名空间
命名空间是 Linux 内核的特性,用于隔离和虚拟化一组进程的系统资源。以下是虚拟化资源的示例:
PID 命名空间
进程 ID,用于进程隔离
网络命名空间
管理网络接口和单独的网络堆栈
IPC 命名空间
管理对进程间通信(IPC)资源的访问
挂载命名空间
管理文件系统挂载点
UTS 命名空间
UNIX 时间共享;允许单个主机为不同的进程拥有不同的主机和域名
UID 命名空间
用户 ID;使用单独的用户和组分配隔离进程所有权
进程的用户和组 ID 在用户命名空间内外可能不同。在用户命名空间内,进程可以具有非特权用户 ID,同时在容器用户命名空间内具有用户 ID 0。进程在用户命名空间内具有 root 权限执行,但在命名空间外部进行操作时无特权。
示例 3-1 展示了如何检查进程的命名空间的示例。Linux 中所有进程的信息都在/proc文件系统中。PID 1 的 PID 命名空间是4026531836,列出所有命名空间显示 PID 命名空间 ID 匹配。
示例 3-1. 单个进程的命名空间
vagrant@ubuntu-xenial:~$ sudo ps -p 1 -o pid,pidns
PID PIDNS
1 4026531836
vagrant@ubuntu-xenial:~$ sudo ls -l /proc/1/ns
total 0
lrwxrwxrwx 1 root root 0 Dec 12 20:41 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Dec 12 20:41 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Dec 12 20:41 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Dec 12 20:41 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Dec 12 20:41 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Dec 12 20:41 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Dec 12 20:41 uts -> uts:[4026531838]
图 3-7 显示,这两个 Linux 原语有效地允许应用程序开发人员控制和管理其应用程序,与主机和其他应用程序分开,无论是在容器中还是通过在主机上本地运行。

图 3-7. Cgroups 和命名空间的综合作用
以下示例使用 Ubuntu 16.04 LTS Xenial Xerus。如果您想在您的系统上跟随操作,可以在本书的代码库中找到更多信息。该代码库包含用于构建 Ubuntu 虚拟机和 Docker 容器的工具和配置。让我们开始设置和测试我们的命名空间。
设置命名空间
图 3-8 概述了基本容器网络设置。在接下来的页面中,我们将详细介绍用于容器网络创建的所有 Linux 命令,这些命令由低级运行时完成。

图 3-8. 根网络命名空间和容器网络命名空间
以下步骤展示了如何创建图 3-8 所示的网络设置:
-
创建具有根网络命名空间的主机。
-
创建新的网络命名空间。
-
创建 veth 对。
-
将 veth 对的一侧移入新的网络命名空间。
-
处理新网络命名空间中 veth 对的一侧。
-
创建桥接口。
-
处理桥接口。
-
将桥接到主机接口。
-
将 veth 对的一侧附加到桥接口。
-
获利。
以下是创建网络命名空间、桥接和 veth 对以及将它们连接在一起所需的所有 Linux 命令:
$ echo 1 > /proc/sys/net/ipv4/ip_forward
$ sudo ip netns add net1
$ sudo ip link add veth0 type veth peer name veth1
$ sudo ip link set veth1 netns net1
$ sudo ip link add veth0 type veth peer name veth1
$ sudo ip netns exec net1 ip addr add 192.168.1.101/24 dev veth1
$ sudo ip netns exec net1 ip link set dev veth1 up
$ sudo ip link add br0 type bridge
$ sudo ip link set dev br0 up
$ sudo ip link set enp0s3 master br0
$ sudo ip link set veth0 master br0
$ sudo ip netns exec net1 ip route add default via 192.168.1.100
让我们深入一个示例并概述每个命令。
Linux 命令ip设置并控制网络命名空间。
注意
您可以在其man 页上找到更多关于ip的信息。
在示例 3-2 中,我们使用 Vagrant 和 VirtualBox 创建了一个新的 Ubuntu 安装,用于我们的测试目的。
示例 3-2. Ubuntu 测试虚拟机
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/xenial64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/xenial64' version '20200904.0.0' is up to date...
==> default: Setting the name of the VM:
advanced_networking_code_examples_default_1600085275588_55198
==> default: Clearing any previously set network interfaces...
==> default: Available bridged network interfaces:
1) en12: USB 10/100 /1000LAN
2) en5: USB Ethernet(?)
3) en0: Wi-Fi (Wireless)
4) llw0
5) en11: USB 10/100/1000 LAN 2
6) en4: Thunderbolt 4
7) en1: Thunderbolt 1
8) en2: Thunderbolt 2
9) en3: Thunderbolt 3
==> default: When choosing an interface, it is usually the one that is
==> default: being used to connect to the internet.
==> default:
default: Which interface should the network bridge to? 1
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
default: Adapter 2: bridged
==> default: Forwarding ports...
default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default: Warning: Connection reset. Retrying...
default:
default: Vagrant insecure key detected. Vagrant will automatically replace
default: this with a newly generated keypair for better security.
default:
default: Inserting generated public key within guest...
default: Removing insecure key from the guest if it's present...
default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
default: /vagrant =>
/Users/strongjz/Documents/code/advanced_networking_code_examples
参考书籍存储库的 Vagrantfile 以重现此过程。
注意
Vagrant 是由 HashiCorp 创建的本地虚拟机管理器。
在 Vagrant 启动我们的虚拟机后,我们可以使用 Vagrant 来ssh进入这个虚拟机:
$± |master U:2 ?:2 ✗| → vagrant ssh
Welcome to Ubuntu 16.04.7 LTS (GNU/Linux 4.4.0-189-generic x86_64)
vagrant@ubuntu-xenial:~$
IP 转发 是操作系统接受一个接口上的传入网络数据包,识别其目的地,然后相应地传送到该网络的能力。启用 IP 转发允许 Linux 机器接收并转发传入的数据包。作为普通主机的 Linux 机器通常不需要启用 IP 转发,因为它生成和接收 IP 流量用于自身目的。默认情况下,它是关闭的;让我们在我们的 Ubuntu 实例上启用它:
vagrant@ubuntu-xenial:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
vagrant@ubuntu-xenial:~$ sudo echo 1 > /proc/sys/net/ipv4/ip_forward
vagrant@ubuntu-xenial:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
通过我们安装的 Ubuntu 实例,我们可以看到我们没有任何额外的网络命名空间,所以让我们创建一个:
vagrant@ubuntu-xenial:~$ sudo ip netns list
ip netns 允许我们在服务器上控制命名空间。创建一个像输入ip netns add net1这样简单:
vagrant@ubuntu-xenial:~$ sudo ip netns add net1
当我们通过此示例工作时,我们可以看到我们刚刚创建的网络命名空间:
vagrant@ubuntu-xenial:~$ sudo ip netns list
net1
现在我们为容器的新网络命名空间创建了一个新的网络命名空间,我们需要一个 veth 对来在根网络命名空间和容器网络命名空间net1之间通信。
ip 再次允许管理员使用简单命令创建 veth 对。请从第二章记住,veth 成对出现,充当网络命名空间之间的通道,因此,一个端口的数据包会自动转发到另一个端口。
vagrant@ubuntu-xenial:~$ sudo ip link add veth0 type veth peer name veth1
小贴士
接口 4 和 5 是命令输出中的 veth 对。我们还可以看到它们彼此配对,veth1@veth0 和 veth0@veth1。
ip link list 命令验证了 veth 对的创建:
vagrant@ubuntu-xenial:~$ ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state
UNKNOWN mode DEFAULT group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc
pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 02:8f:67:5f:07:a5 brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc
pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 08:00:27:0f:4e:0d brd ff:ff:ff:ff:ff:ff
4: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc
noop state DOWN mode DEFAULT group default qlen 1000
link/ether 72:e4:03:03:c1:96 brd ff:ff:ff:ff:ff:ff
5: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc
noop state DOWN mode DEFAULT group default qlen 1000
link/ether 26:1a:7f:2c:d4:48 brd ff:ff:ff:ff:ff:ff
vagrant@ubuntu-xenial:~$
现在让我们将veth1移入之前创建的新网络命名空间:
vagrant@ubuntu-xenial:~$ sudo ip link set veth1 netns net1
ip netns exec 允许我们验证网络命名空间的配置。输出验证了veth1现在位于网络命名空间net1中:
vagrant@ubuntu-xenial:~$ sudo ip netns exec net1 ip link list
4: veth1@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state
DOWN mode DEFAULT group default qlen 1000
link/ether 72:e4:03:03:c1:96 brd ff:ff:ff:ff:ff:ff link-netnsid 0
网络命名空间是 Linux 内核中完全分离的 TCP/IP 堆栈。作为新接口并位于新网络命名空间中,veth 接口需要 IP 地址以便从net1命名空间中的根命名空间和主机向外传递数据包:
vagrant@ubuntu-xenial:~$ sudo ip netns exec
net1 ip addr add 192.168.1.100/24 dev veth1
与主机网络接口一样,它们需要“打开”:
vagrant@ubuntu-xenial:~$ sudo ip netns exec net1 ip link set dev veth1 up
状态现在过渡为LOWERLAYERDOWN。状态NO-CARRIER指向正确方向。以太网需要连接电缆才能连接;我们的上行 veth 对也尚未启动。veth1接口已上线且寻址,但实际上仍然处于“未连接”状态:
vagrant@ubuntu-xenial:~$ sudo ip netns exec net1 ip link list veth1
4: veth1@if5: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500
qdisc noqueue state LOWERLAYERDOWN mode DEFAULT
group default qlen 1000 link/ether 72:e4:03:03:c1:96
brd ff:ff:ff:ff:ff:ff link-netnsid 0
现在让我们打开配对的veth0端:
vagrant@ubuntu-xenial:~$ sudo ip link set dev veth0 up
vagrant@ubuntu-xenial:~$ sudo ip link list
5: veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 26:1a:7f:2c:d4:48 brd ff:ff:ff:ff:ff:ff link-netnsid 0
现在net1命名空间内的 veth 对为UP:
vagrant@ubuntu-xenial:~$ sudo ip netns exec net1 ip link list
4: veth1@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 72:e4:03:03:c1:96 brd ff:ff:ff:ff:ff:ff link-netnsid 0
veth 对的两端都报告为 up;我们需要将根命名空间的 veth 端连接到桥接接口。确保选择你正在使用的接口,在这种情况下是enp0s8;对其他情况可能不同:
vagrant@ubuntu-xenial:~$ sudo ip link add br0 type bridge
vagrant@ubuntu-xenial:~$ sudo ip link set dev br0 up
vagrant@ubuntu-xenial:~$ sudo ip link set enp0s8 master br0
vagrant@ubuntu-xenial:~$ sudo ip link set veth0 master br0
我们可以看到,enp0s8 和 veth0 报告是桥接接口 br0 的一部分,master br0 state up。
接下来,让我们测试对我们的网络命名空间的连接性:
vagrant@ubuntu-xenial:~$ ping 192.168.1.100 -c 4
PING 192.168.1.100 (192.168.1.100) 56(84) bytes of data.
From 192.168.1.10 icmp_seq=1 Destination Host Unreachable
From 192.168.1.10 icmp_seq=2 Destination Host Unreachable
From 192.168.1.10 icmp_seq=3 Destination Host Unreachable
From 192.168.1.10 icmp_seq=4 Destination Host Unreachable
--- 192.168.1.100 ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 6043ms
我们的新网络命名空间没有默认路由,因此不知道如何路由我们的 ping 请求的数据包:
$ sudo ip netns exec net1
ip route add default via 192.168.1.100
$ sudo ip netns exec net1 ip r
default via 192.168.1.100 dev veth1
192.168.1.0/24 dev veth1 proto kernel scope link src 192.168.1.100
让我们再试一次:
$ ping 192.168.2.100 -c 4
PING 192.168.2.100 (192.168.2.100) 56(84) bytes of data.
64 bytes from 192.168.2.100: icmp_seq=1 ttl=64 time=0.018 ms
64 bytes from 192.168.2.100: icmp_seq=2 ttl=64 time=0.028 ms
64 bytes from 192.168.2.100: icmp_seq=3 ttl=64 time=0.036 ms
64 bytes from 192.168.2.100: icmp_seq=4 ttl=64 time=0.043 ms
--- 192.168.2.100 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2997ms
$ ping 192.168.2.101 -c 4
PING 192.168.2.101 (192.168.2.101) 56(84) bytes of data.
64 bytes from 192.168.2.101: icmp_seq=1 ttl=64 time=0.016 ms
64 bytes from 192.168.2.101: icmp_seq=2 ttl=64 time=0.017 ms
64 bytes from 192.168.2.101: icmp_seq=3 ttl=64 time=0.016 ms
64 bytes from 192.168.2.101: icmp_seq=4 ttl=64 time=0.021 ms
--- 192.168.2.101 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2997ms
rtt min/avg/max/mdev = 0.016/0.017/0.021/0.004 ms
成功!我们已创建了桥接接口和 veth 对,将其中一个迁移到新的网络命名空间,并测试了连接性。示例 3-3 是我们完成这一操作时运行的所有命令的总结。
示例 3-3. 总结网络命名空间的创建
$ echo 1 > /proc/sys/net/ipv4/ip_forward
$ sudo ip netns add net1
$ sudo ip link add veth0 type veth peer name veth1
$ sudo ip link set veth1 netns net1
$ sudo ip link add veth0 type veth peer name veth1
$ sudo ip netns exec net1 ip addr add 192.168.1.101/24 dev veth1
$ sudo ip netns exec net1 ip link set dev veth1 up
$ sudo ip link add br0 type bridge
$ sudo ip link set dev br0 up
$ sudo ip link set enp0s3 master br0
$ sudo ip link set veth0 master br0
$ sudo ip netns exec net1 ip route add default via 192.168.1.100
对于不熟悉所有这些命令的开发人员来说,这是很多需要记住的东西,而且非常容易出错!如果桥接信息不正确,可能会导致整个网络部分发生网络环路。这些问题是系统管理员希望避免的,因此他们防止开发人员在系统上进行这些类型的网络更改。幸运的是,容器帮助减少了开发人员需要记住所有这些命令的负担,并减轻了系统管理员允许开发人员运行这些命令所带来的恐惧。
这些命令对于每次容器的 每次 创建和删除的网络命名空间 都是 必要的。在 示例 3-3 中的命名空间创建是容器运行时的工作。Docker 以其自己的方式管理这些。CNI 项目标准化了所有系统的网络创建。CNI 类似于 OCI,是开发人员标准化和优化容器生命周期特定任务的方式。在后面的部分中,我们将讨论 CNI。
容器网络基础知识
前一节展示了创建网络命名空间所需的所有命令。让我们来研究 Docker 是如何为我们完成这些操作的。我们只使用了桥接模式;容器网络还有几种其他模式。本节将部署多个 Docker 容器,检查它们的网络,并解释容器如何与主机外部和彼此通信。
让我们从讨论与容器一起工作时使用的几种网络“模式”开始:
无
无网络禁用容器的网络访问。当容器不需要网络访问时使用此模式。
桥接
在桥接网络中,容器在主机内部的私有网络中运行。与网络中的其他容器通信是开放的。与主机外的服务通信在退出主机之前经过网络地址转换(NAT)。当未指定 --net 选项时,桥接模式是网络的默认模式。
主机
在主机网络中,容器与主机共享相同的 IP 地址和网络命名空间。运行在这个容器内的进程具有与直接在主机上运行服务相同的网络功能。如果容器需要访问主机上的网络资源,这种模式非常有用。但是,使用这种网络模式容器将失去网络分段的好处。部署容器的人将需要管理和竞争运行在该节点上的服务的端口。
警告
主机网络驱动仅适用于 Linux 主机。Docker Desktop for Mac 和 Windows 或 Docker EE for Windows Server 不支持主机网络模式。
Macvlan
Macvlan 使用一个父接口。该接口可以是主机接口(如 eth0)、子接口,甚至是将以太网接口捆绑成单个逻辑接口的绑定主机适配器。与所有 Docker 网络一样,Macvlan 网络彼此分隔,提供网络内的访问,但不能在网络之间访问。Macvlan 允许物理接口使用多个 MAC 和 IP 地址,使用 Macvlan 子接口。Macvlan 有四种类型:私有、VEPA、桥接(Docker 默认使用)和透传。使用桥接时,使用 NAT 进行外部连接。与 Macvlan 不同,由于主机直接映射到物理网络,外部连接可以使用与主机相同的 DHCP 服务器和交换机。
警告
大多数云提供商阻止 Macvlan 网络。需要管理员访问网络设备。
IPvlan
IPvlan 类似于 Macvlan,但有一个重要区别:IPvlan 不为创建的子接口分配 MAC 地址。所有子接口共享父接口的接口 MAC 地址,但使用不同的 IP 地址。IPvlan 有两种模式,L2 或 L3。在 IPvlan 中,L2 或第二层模式类似于 Macvlan 桥接模式。IPvlan L3 或第三层模式伪装为子接口和父接口之间的第三层设备。
Overlay
Overlay 允许在容器集群中跨主机扩展同一网络。Overlay 网络实际上位于底层/物理网络之上。几个开源项目创建这些 Overlay 网络,我们将在本章后面讨论。
自定义
自定义桥接网络与桥接网络相同,但使用专门为该容器创建的桥接。一个使用案例是运行在数据库桥接网络上的容器。另一个容器可以在默认和数据库桥接上有一个接口,从而能够根据需要与两个网络通信。
容器定义的网络允许一个容器共享另一个容器的地址和网络配置。这种共享使得容器之间能够进行进程隔离,每个容器运行一个服务,但服务仍然可以在127.0.0.1上相互通信。
要测试所有这些模式,我们需要继续使用一个已安装 Docker 的 Vagrant Ubuntu 主机。Mac 和 Windows 上的 Docker 不支持主机网络模式,因此我们必须在 Linux 上进行这个示例。您可以使用书中代码库中提供的 Example 1-1 中的已配置机器,或者使用 Docker Vagrant 版本。如果您想手动操作,Ubuntu 上安装 Docker 的步骤如下:
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/xenial64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box
'ubuntu/xenial64' version '20200904.0.0' is up to date...
==> default: Setting the name of the VM:
advanced_networking_code_examples_default_1600085275588_55198
==> default: Clearing any previously set network interfaces...
==> default: Available bridged network interfaces:
1) en12: USB 10/100 /1000LAN
2) en5: USB Ethernet(?)
3) en0: Wi-Fi (Wireless)
4) llw0
5) en11: USB 10/100/1000 LAN 2
6) en4: Thunderbolt 4
7) en1: Thunderbolt 1
8) en2: Thunderbolt 2
9) en3: Thunderbolt 3
==> default: When choosing an interface, it is usually the one that is
==> default: being used to connect to the internet.
==> default:
default: Which interface should the network bridge to? 1
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
default: Adapter 2: bridged
==> default: Forwarding ports...
default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default: Warning: Connection reset. Retrying...
default:
default: Vagrant insecure key detected. Vagrant will automatically replace
default: this with a newly generated keypair for better security.
default:
default: Inserting generated public key within guest...
default: Removing insecure key from the guest if it's present...
default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
default: /vagrant =>
/Users/strongjz/Documents/code/advanced_networking_code_examples
default: + sudo docker run hello-world
default: Unable to find image 'hello-world:latest' locally
default: latest: Pulling from library/hello-world
default: 0e03bdcc26d7:
default: Pulling fs layer
default: 0e03bdcc26d7:
default: Verifying Checksum
default: 0e03bdcc26d7:
default: Download complete
default: 0e03bdcc26d7:
default: Pull complete
default: Digest:
sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
default: Status: Downloaded newer image for hello-world:latest
default:
default: Hello from Docker!
default: This message shows that your
default: installation appears to be working correctly.
default:
default: To generate this message, Docker took the following steps:
default: 1\. The Docker client contacted the Docker daemon.
default: 2\. The Docker daemon pulled the "hello-world" image
default: from the Docker Hub.
default: (amd64)
default: 3\. The Docker daemon created a new container from that image
default: which runs the executable that produces the output you are
default: currently reading.
default: 4\. The Docker daemon streamed that output to the Docker
default: client, which sent it to your terminal.
default:
default: To try something more ambitious, you can run an Ubuntu
default: container with:
default: $ docker run -it ubuntu bash
default:
default: Share images, automate workflows, and more with a free Docker ID:
default: https://hub.docker.com
default:
default: For more examples and ideas, visit:
default: https://docs.docker.com/get-started
现在我们已经启动了主机,让我们开始研究在 Docker 中可以使用的不同网络设置。示例 3-4 显示 Docker 在安装期间创建了三种网络类型:桥接、主机和无网络。
示例 3-4. Docker 网络
vagrant@ubuntu-xenial:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
1fd1db59c592 bridge bridge local
eb34a2105b0f host host local
941ce103b382 none null local
vagrant@ubuntu-xenial:~$
默认是 Docker 桥接,容器附加到其中并配备了172.17.0.0/16默认子网中的 IP 地址。示例 3-5 显示了 Ubuntu 的默认接口以及创建docker0桥接接口的 Docker 安装情况。
示例 3-5. Docker 桥接口
vagrant@ubuntu-xenial:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc
noqueue state UNKNOWN group default qlen 1 
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp0s3:
<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group
default qlen 1000 
link/ether 02:8f:67:5f:07:a5 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::8f:67ff:fe5f:7a5/64 scope link
valid_lft forever preferred_lft forever
3: enp0s8:
<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group
default qlen 1000 
link/ether 08:00:27:22:0e:46 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.19/24 brd 192.168.1.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet 192.168.1.20/24 brd 192.168.1.255 scope global secondary enp0s8
valid_lft forever preferred_lft forever
inet6 2605:a000:160d:517:a00:27ff:fe22:e46/64 scope global mngtmpaddr dynamic
valid_lft 604600sec preferred_lft 604600sec
inet6 fe80::a00:27ff:fe22:e46/64 scope link
valid_lft forever preferred_lft forever
4: docker0:
<NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group
default 
link/ether 02:42:7d:50:c7:01 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:7dff:fe50:c701/64 scope link
valid_lft forever preferred_lft forever
这是回环接口。
enp0s3 是我们 NAT 的虚拟桥接接口。
enp0s8 是主机接口;它位于与我们主机相同的网络上,并使用 DHCP 获取192.168.1.19地址的默认 Docker 桥接。
默认的 Docker 容器接口使用桥接模式。
示例 3-6 使用docker run命令启动了一个忙碌的 busybox 容器,并请求 Docker 返回容器的 IP 地址。 Docker 的默认 NAT 地址是172.17.0.0/16,我们的 busybox 容器得到的是172.17.0.2。
示例 3-6. Docker 桥接
vagrant@ubuntu-xenial:~$ sudo docker run -it busybox ip a
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
df8698476c65: Pull complete
Digest: sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a
Status: Downloaded newer image for busybox:latest
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
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
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
示例 3-7 中的主机网络显示,容器与主机共享相同的网络命名空间。我们可以看到接口与主机的相同;enp0s3、enp0s8 和 docker0 都存在于容器的ip a命令输出中。
示例 3-7. Docker 主机网络
vagrant@ubuntu-xenial:~$ sudo docker run -it --net=host busybox ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever`
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether 02:8f:67:5f:07:a5 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::8f:67ff:fe5f:7a5/64 scope link
valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether 08:00:27:22:0e:46 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.19/24 brd 192.168.1.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet 192.168.1.20/24 brd 192.168.1.255 scope global secondary enp0s8
valid_lft forever preferred_lft forever
inet6 2605:a000:160d:517:a00:27ff:fe22:e46/64 scope global dynamic
valid_lft 604603sec preferred_lft 604603sec
inet6 fe80::a00:27ff:fe22:e46/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue
link/ether 02:42:7d:50:c7:01 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:7dff:fe50:c701/64 scope link
valid_lft forever preferred_lft forever
根据先前设置的 veth 桥接示例,让我们看看当 Docker 为我们管理时,它是多么简单。为了查看这一点,我们需要一个进程来保持容器运行。以下命令启动一个 busybox 容器并进入sh命令行:
vagrant@ubuntu-xenial:~$ sudo docker run -it --rm busybox /bin/sh
/#
我们有一个回环接口lo,和一个连接到 veth12 的以太网接口eth0,Docker 的默认 IP 地址为172.17.0.2。由于我们之前的命令仅输出了一个ip a结果,容器随后退出,Docker 重新使用 IP 地址172.17.0.2用于运行的 busybox 容器:
/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
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
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
在容器的网络命名空间中运行ip r,我们可以看到容器的路由表也自动设置好了:
/ # ip r
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2
如果我们在新终端中打开并通过 vagrant ssh 进入我们的 Vagrant Ubuntu 实例,并运行 docker ps 命令,它将显示运行中的 busybox 容器的所有信息:
vagrant@ubuntu-xenial:~$ sudo docker ps
CONTAINER ID IMAGE COMMAND
3b5a7c3a74d5 busybox "/bin/sh"
CREATED STATUS PORTS NAMES
47 seconds ago Up 46 seconds competent_mendel
我们可以在同一主机的网络命名空间中看到 Docker 为容器 veth68b6f80@if11 设置的 veth 接口。它是 docker0 桥的成员,并且状态为 master docker0 state UP:
vagrant@ubuntu-xenial:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group
default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP
group default qlen 1000
link/ether 02:8f:67:5f:07:a5 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fe80::8f:67ff:fe5f:7a5/64 scope link
valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP
group default qlen 1000
link/ether 08:00:27:22:0e:46 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.19/24 brd 192.168.1.255 scope global enp0s8
valid_lft forever preferred_lft forever
inet 192.168.1.20/24 brd 192.168.1.255 scope global secondary enp0s8
valid_lft forever preferred_lft forever
inet6 2605:a000:160d:517:a00:27ff:fe22:e46/64 scope global mngtmpaddr dynamic
valid_lft 604745sec preferred_lft 604745sec
inet6 fe80::a00:27ff:fe22:e46/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
group default
link/ether 02:42:7d:50:c7:01 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:7dff:fe50:c701/64 scope link
valid_lft forever preferred_lft forever
12: veth68b6f80@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
master docker0 state UP group default
link/ether 3a:64:80:02:87:76 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::3864:80ff:fe02:8776/64 scope link
valid_lft forever preferred_lft forever
Ubuntu 主机的路由表显示 Docker 用于访问在主机上运行的容器的路由:
vagrant@ubuntu-xenial:~$ ip r
default via 192.168.1.1 dev enp0s8
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.1.0/24 dev enp0s8 proto kernel scope link src 192.168.1.19
默认情况下,Docker 不会将它创建的网络命名空间添加到 /var/run,而 ip netns list 期望新创建的网络命名空间在其中。现在让我们通过三个步骤列出 Docker 网络命名空间:
-
获取运行容器的 PID。
-
将网络命名空间软链接从
/proc/PID/net/到/var/run/netns。 -
列出网络命名空间。
docker ps 输出了需要在主机 PID 命名空间中检查的运行中容器的容器 ID:
vagrant@ubuntu-xenial:~$ sudo docker ps
CONTAINER ID IMAGE COMMAND
1f3f62ad5e02 busybox "/bin/sh"
CREATED STATUS PORTS NAMES
11 minutes ago Up 11 minutes determined_shamir
docker inspect 允许我们解析输出并获取主机进程的 PID。如果我们在主机 PID 命名空间中运行 ps -p,我们可以看到它正在运行 sh,跟踪我们的 docker run 命令:
vagrant@ubuntu-xenial:~$ sudo docker inspect -f '{{.State.Pid}}' 1f3f62ad5e02
25719
vagrant@ubuntu-xenial:~$ ps -p 25719
PID TTY TIME CMD
25719 pts/0 00:00:00 sh
1f3f62ad5e02 是容器 ID,25719 是运行 sh 的 busybox 容器的 PID,因此现在我们可以为 Docker 创建的容器网络命名空间创建一个符号链接,使其与 ip 期望的位置对应:
$ sudo ln -sfT /proc/25719/ns/net /var/run/netns/1f3f62ad5e02
注意
在使用示例中的容器 ID 和进程 ID 时,请记住它们可能在您的系统上有所不同。
现在 ip netns exec 命令返回与 docker exec 命令相同的 IP 地址 172.17.0.2:
vagrant@ubuntu-xenial:~$ sudo ip netns exec 1f3f62ad5e02 ip a
1: lo:
<LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
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
13: eth0@if14:
<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
我们可以通过 docker exec 验证并在 busybox 容器内运行 ip an。IP 地址、MAC 地址和网络接口都与输出匹配:
vagrant@ubuntu-xenial:~$ sudo docker exec 1f3f62ad5e02 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
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
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
Docker 启动我们的容器;创建网络命名空间、veth 对和 docker0 桥(如果尚不存在);然后在每次容器创建和删除时都将它们附加在一起,只需一个命令!从应用程序开发者的角度来看,这非常强大。无需记住所有这些 Linux 命令,并且可能在主机上破坏网络。这次讨论主要集中在单个主机上。Docker 如何在集群中协调容器之间的通信将在下一节讨论。
Docker 网络模型
Libnetwork 是 Docker 的容器网络架构,其设计哲学体现在容器网络模型(CNM)中。Libnetwork 实现了 CNM,并通过 Docker 引擎中的 API 管理 Linux 上运行的所有容器的网络命名空间。网络组件是同一网络上的多个端点的集合。端点是网络上的主机。网络控制器通过 Docker 引擎的 API 管理所有这些。
在端点上,Docker 使用iptables进行网络隔离。容器公开一个端口以供外部访问。容器不会收到公共 IPv4 地址,而是接收私有 RFC 1918 地址。运行在容器上的服务必须逐端口暴露,并且容器端口必须映射到主机端口以避免冲突。当 Docker 启动时,它在主机上创建一个虚拟桥接口docker0,并为其分配来自私有 1918 范围的随机 IP 地址。该桥接像物理桥接一样在两个连接设备之间传递数据包。每个新容器都会自动附加到docker0桥接上;图 3-9 展示了这一过程,与我们在前面部分展示的方法类似。

图 3-9. Docker 桥接
CNM 将网络模式映射到我们已经讨论过的驱动程序。以下是网络模式及其 Docker 引擎等效的列表:
桥接
默认的 Docker 桥接(参见图 3-9,我们之前的示例中展示过这一点)
自定义或远程
用户定义的桥接,或允许用户创建或使用其插件
覆盖
覆盖
Null
无网络选项
桥接网络用于在同一主机上运行的容器之间通信。与在不同主机上运行的容器通信可以使用覆盖网络。Docker 使用本地和全局驱动程序的概念。例如,本地驱动程序(如桥接)以主机为中心,不进行跨节点协调,这是全局驱动程序(如 Overlay)的工作。全局驱动程序依赖于 libkv,一个键值存储抽象层,在多台机器之间协调。CNM 不提供键值存储,因此需要外部存储如 Consul、etcd 和 Zookeeper。
接下来的部分将深入讨论启用覆盖网络的技术。
覆盖网络
到目前为止,我们的示例都是在单个主机上,但是大规模生产应用程序并不在单个主机上运行。为了容器在不同节点上通信,需要解决几个问题,如如何在主机之间协调路由信息、端口冲突和 IP 地址管理等。一个帮助容器在主机之间路由的技术是 VXLAN。在图 3-10 中,我们可以看到在物理 L3 网络上运行的一个层 2 覆盖网络创建了一个 VXLAN。
我们在第一章简要讨论了 VXLAN,但是在这里有必要更详细地解释数据传输如何实现容器间通信。

图 3-10. VXLAN 隧道
VXLAN 是 VLAN 协议的扩展,创建了 1600 万个唯一标识符。在 IEEE 802.1Q 下,给定以太网网络上的 VLAN 最大数量为 4094。在物理数据中心网络上的传输协议是 IP 加 UDP。VXLAN 定义了一种 MAC-in-UDP 封装方案,其中原始第二层帧增加了一个 VXLAN 头部,并包装在一个 UDP IP 数据包中。图 3-11 显示了 IP 数据包封装在 UDP 数据包及其头部中。
VXLAN 数据包是一个 MAC-in-UDP 封装的数据包。第二层帧增加了一个 VXLAN 头,并放置在 UDP-IP 数据包中。VXLAN 标识符为 24 位。这就是为什么 VXLAN 可以支持 1600 万个段的原因。
图 3-11 是第一章的详细版本。我们在两个主机上有 VXLAN 隧道端点(VTEP),它们附加到主机的桥接口,并连接到容器的桥接。VTEP 执行数据帧的封装和解封装。VTEP 对等体交互确保数据转发到相关目标容器地址。离开容器的数据使用 VXLAN 信息封装,并通过 VXLAN 隧道传输,由对等 VTEP 解封装。
覆盖网络(Overlay networking)允许容器在网络上进行跨主机通信。CNM 仍然存在其他问题,使其与 Kubernetes 不兼容。Kubernetes 的维护者决定使用 CoreOS 发起的 CNI 项目。这个项目比 CNM 更简单,不需要守护进程,并且设计成跨平台的。

图 3-11. VXLAN 隧道详细信息
容器网络接口(Container Network Interface)
CNI 是容器运行时与网络实现之间的软件接口。在实现 CNI 时有很多选择;我们将讨论几个显著的选择。CNI 最初是 CoreOS 的 rkt 项目的一部分;现在是 CNCF 项目。CNI 项目包括规范和用于开发 Linux 容器中配置网络接口的插件的库。CNI 关注容器的网络连接性,通过在容器创建时分配资源,并在删除时移除它们。CNI 插件负责将网络接口关联到容器网络命名空间,并对主机进行任何必要的更改。然后,它为接口分配 IP 并为其设置路由。图 3-12 概述了 CNI 架构。容器运行时使用主机的网络信息配置文件;在 Kubernetes 中,Kubelet 也使用这个配置文件。CNI 和容器运行时相互通信,并对配置的 CNI 插件应用命令。

图 3-12. CNI 架构
有几个开源项目实现了带有各种功能和功能的 CNI 插件。以下是几个概述:
Cilium
Cilium 是用于保护应用程序容器之间网络连接的开源软件。Cilium 是一个 L7/HTTP 感知的 CNI,可以使用基于身份的安全模型在 L3-L7 上执行网络策略,与网络寻址分离。它由 Linux 技术 eBPF 提供支持。
Flannel
Flannel 是为 Kubernetes 设计的一种简单配置层 3 网络结构的方法。Flannel 专注于网络。Flannel 使用 Kubernetes 集群的现有etcd数据存储来存储其状态信息,以避免提供专用存储。
Calico
根据 Calico 的说法,“将灵活的网络功能与随处可运行的安全执行相结合,提供具有本地 Linux 内核性能和真正云原生可伸缩性的解决方案。” 它具有完整的网络策略支持,并与其他 CNI 配合良好。Calico 不使用覆盖网络。相反,Calico 配置了一个使用 BGP 路由协议在主机之间路由数据包的层 3 网络。Calico 还可以与 Istio,一个服务网格,集成,以解释和执行集群内工作负载的策略,无论是在服务网格层还是网络基础设施层。
AWS
AWS 拥有自己的 CNI 的开源实现,即 AWS VPC CNI。通过直接位于 AWS 网络上,它提供高吞吐量和可用性。通过在 AWS 网络上运行,提供了低延迟,因为没有额外的覆盖网络和在 AWS 网络上运行的最小网络抖动。集群和网络管理员可以应用现有的 AWS VPC 网络和安全最佳实践来构建在 AWS 上的 Kubernetes 网络。他们可以实现这些最佳实践,因为 AWS CNI 包括使用原生 AWS 服务的能力,如用于分析网络事件和模式的 VPC 流日志,用于流量管理的 VPC 路由策略,以及用于网络流量隔离的安全组和网络访问控制列表。我们将在第六章中更多地讨论 AWS VPC CNI。
注意
Kubernetes.io 网站提供了一个可用的 CNI 选项列表。
还有许多 CNI 的选项,最佳决定权在于集群管理员、网络管理员和应用程序开发人员,以最好地决定哪种 CNI 解决了他们的业务用例。在后续章节中,我们将讨论用例并部署几种以帮助管理员做出决定。
在我们的下一节中,我们将使用 Golang Web 服务器和 Docker 来演示容器连接性的例子。
容器连接性
就像我们在前一章中进行的实验一样,我们将使用 Go 极简 Web 服务器来演示容器连接性的概念。当我们在 Ubuntu 主机上将 Web 服务器部署为容器时,我们将解释容器级别发生了什么。
以下是我们将要讨论的两种网络场景:
-
Docker 主机上的容器之间的连接
-
在不同主机上的容器之间的连接
Golang Web 服务器硬编码在端口 8080 上运行,http.ListenAndServe("0.0.0.0:8080", nil),正如我们在 示例 3-8 中所见。
示例 3-8. Go 中的最小 Web 服务器
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(w, "Hello")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe("0.0.0.0:8080", nil)
}
为了配置我们的最小化 Golang Web 服务器,我们需要从 Dockerfile 创建它。示例 3-9 显示了我们的 Golang Web 服务器的 Dockerfile。Dockerfile 包含了指定构建镜像时要执行的指令。它从 FROM 指令开始,指定基础镜像应该是什么。RUN 指令指定要执行的命令。注释以 # 开始。请记住,如果改变了镜像的状态,Dockerfile 中的每一行都会创建一个新层。开发人员需要在为镜像创建很多层和 Dockerfile 的可读性之间找到平衡。
示例 3-9. Golang 最小化 Web 服务器的 Dockerfile
FROM golang:1.15 AS builder 
WORKDIR /opt 
COPY web-server.go . 
RUN CGO_ENABLED=0 GOOS=linux go build -o web-server . 
FROM golang:1.15 
WORKDIR /opt 
COPY --from=0 /opt/web-server . 
CMD ["/opt/web-server"] 
由于我们的 Web 服务器是用 Golang 编写的,我们可以在容器中编译我们的 Go 服务器,以将镜像大小减小到仅包含编译后的 Go 二进制文件。我们首先使用版本为 1.15 的 Golang 基础镜像来启动我们的 Web 服务器。
WORKDIR 将工作目录设置为后续所有命令要从中运行的目录。
COPY 复制 web-server.go 文件,该文件定义了我们应用的工作目录。
RUN 指示 Docker 在构建容器中编译我们的 Golang 应用程序。
现在,为了运行我们的应用程序,我们定义 FROM 作为应用程序的基础镜像,同样使用 golang:1.15;我们可以通过使用像 alpine 这样的其他小型镜像进一步减小镜像的最终大小。
作为一个新的容器,我们再次将工作目录设置为 /opt。
COPY 在此将从构建容器复制编译好的 Go 二进制文件到应用容器中。
CMD 指示 Docker 运行我们的应用程序的命令是启动我们的 Web 服务器。
在将应用程序容器化时,开发人员和管理员应该遵守一些 Dockerfile 最佳实践:
-
每个 Dockerfile 只应使用一个
ENTRYPOINT。ENTRYPOINT或CMD告诉 Docker 在运行的容器内部启动什么进程,因此应该只有一个运行中的进程;容器完全依赖于进程隔离。 -
为了减少容器的层次,开发人员应该使用
&&和\将类似的命令合并为一个命令。每个新命令在 Dockerfile 中都会为 Docker 容器镜像添加一个新层,从而增加其存储空间。 -
使用缓存系统来改进容器的构建时间。如果一个层没有变化,它应该位于 Dockerfile 的顶部。缓存是语句顺序至关重要的一部分。首先添加最不可能更改的文件,然后添加最有可能更改的文件。
-
使用多阶段构建可以显著减小最终镜像的大小。
-
不要安装不必要的工具或软件包。这样做可以减少容器的攻击面和大小,从而降低从注册表到运行容器的主机的网络传输时间。
让我们构建我们的 Golang Web 服务器并审查执行此操作的 Docker 命令。
docker build指示 Docker 根据 Dockerfile 指令构建我们的镜像:
$ sudo docker build .
Sending build context to Docker daemon 4.27MB
Step 1/8 : FROM golang:1.15 AS builder
1.15: Pulling from library/golang
57df1a1f1ad8: Pull complete
71e126169501: Pull complete
1af28a55c3f3: Pull complete
03f1c9932170: Pull complete
f4773b341423: Pull complete
fb320882041b: Pull complete
24b0ad6f9416: Pull complete
Digest:
sha256:da7ff43658854148b401f24075c0aa390e3b52187ab67cab0043f2b15e754a68
Status: Downloaded newer image for golang:1.15
---> 05c8f6d2538a
Step 2/8 : WORKDIR /opt
---> Running in 20c103431e6d
Removing intermediate container 20c103431e6d
---> 74ba65cfdf74
Step 3/8 : COPY web-server.go .
---> 7a36ec66be52
Step 4/8 : RUN CGO_ENABLED=0 GOOS=linux go build -o web-server .
---> Running in 5ea1c0a85422
Removing intermediate container 5ea1c0a85422
---> b508120db6ba
Step 5/8 : FROM golang:1.15
---> 05c8f6d2538a
Step 6/8 : WORKDIR /opt
---> Using cache
---> 74ba65cfdf74
Step 7/8 : COPY --from=0 /opt/web-server .
---> dde6002760cd
Step 8/8 : CMD ["/opt/web-server"]
---> Running in 2bcb7c8f5681
Removing intermediate container 2bcb7c8f5681
---> 72fd05de6f73
Successfully built 72fd05de6f73
用于我们测试的 Golang 最小 Web 服务器具有容器 ID 72fd05de6f73,这不容易阅读,因此我们可以使用docker tag命令为其提供一个友好的名称:
$ sudo docker tag 72fd05de6f73 go-web:v0.0.1
docker images返回本地可用镜像的列表以运行。我们有一个来自 Docker 安装测试的测试镜像和用于测试我们的网络设置的 busybox。如果本地没有可用的容器,则会从注册表下载;网络加载时间会影响这一点,因此我们需要尽可能小的镜像:
$ sudo docker images
REPOSITORY TAG IMAGE ID SIZE
<none> <none> b508120db6ba 857MB
go-web v0.0.1 72fd05de6f73 845MB
golang 1.15 05c8f6d2538a 839MB
busybox latest 6858809bf669 1.23MB
hello-world latest bf756fb1ae65 13.3kB
docker ps显示我们主机上运行的容器。从我们的网络命名空间示例中,我们仍然有一个正在运行的 busybox 容器:
$ sudo docker ps
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
1f3f62ad5e02 busybox "/bin/sh" Up 11 minutes determined_shamir
docker logs将打印出容器从标准输出产生的任何日志;目前,我们的 busybox 镜像没有打印出我们可以查看的任何内容:
vagrant@ubuntu-xenial:~$ sudo docker logs 1f3f62ad5e02
vagrant@ubuntu-xenial:~$
docker exec允许开发者和管理员在 Docker 容器内执行命令。我们之前在调查 Docker 网络设置时就这样做了:
vagrant@ubuntu-xenial:~$ sudo docker exec 1f3f62ad5e02 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
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
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
vagrant@ubuntu-xenial:~$
注意
在Docker CLI 文档中可以找到更多有关 Docker CLI 的命令。
在前一节中,我们将 Golang Web 服务器构建为一个容器。为了测试连通性,我们还将使用dnsutils镜像,该镜像被 Kubernetes 端到端测试使用。该镜像可以从 Kubernetes 项目的gcr.io/kubernetes-e2e-test-images/dnsutils:1.3获取。
镜像名称将从 Google 容器注册表复制 Docker 镜像到我们的本地 Docker 文件系统:
$ sudo docker pull gcr.io/kubernetes-e2e-test-images/dnsutils:1.3
1.3: Pulling from kubernetes-e2e-test-images/dnsutils
5a3ea8efae5d: Pull complete
7b7e943444f2: Pull complete
59c439aa0fa7: Pull complete
3702870470ee: Pull complete
Digest: sha256:b31bcf7ef4420ce7108e7fc10b6c00343b21257c945eec94c21598e72a8f2de0
Status: Downloaded newer image for gcr.io/kubernetes-e2e-test-images/dnsutils:1.3
gcr.io/kubernetes-e2e-test-images/dnsutils:1.3
现在我们的 Golang 应用程序可以作为容器运行,我们可以探索容器网络的各种场景。
容器与容器
我们首先介绍两个运行在同一主机上的容器之间的通信。我们从启动dnsutils镜像并进入 shell 开始:
$ sudo docker run -it gcr.io/kubernetes-e2e-test-images/dnsutils:1.3 /bin/sh
/ #
默认的 Docker 网络设置使dnsutils镜像能够连接到互联网:
/ # ping google.com -c 4
PING google.com (172.217.9.78): 56 data bytes
64 bytes from 172.217.9.78: seq=0 ttl=114 time=39.677 ms
64 bytes from 172.217.9.78: seq=1 ttl=114 time=38.180 ms
64 bytes from 172.217.9.78: seq=2 ttl=114 time=43.150 ms
64 bytes from 172.217.9.78: seq=3 ttl=114 time=38.140 ms
--- google.com ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 38.140/39.786/43.150 ms
/ #
Golang Web 服务器从默认的 Docker 桥接开始;在一个单独的 SSH 连接中,然后我们的 Vagrant 主机上,我们使用以下命令启动 Golang Web 服务器:
$ sudo docker run -it -d -p 80:8080 go-web:v0.0.1
a568732bc191bb1f5a281e30e34ffdeabc624c59d3684b93167456a9a0902369
-it 选项用于交互式进程(例如 shell);我们必须使用 -it 分配一个 TTY 给容器进程。-d 在分离模式下运行容器;这允许我们继续使用终端并输出完整的 Docker 容器 ID。-p 在网络方面可能是最重要的选项;它在主机和容器之间创建端口连接。我们的 Golang Web 服务器在端口 8080 上运行,并在主机上的端口 80 上暴露该端口。
docker ps 验证我们现在运行了两个容器:Go Web 服务器容器在主机端口 80 上暴露了端口 8080,并且在我们的 dnsutils 容器中运行的 Shell:
vagrant@ubuntu-xenial:~$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
906fd860f84d go-web:v0.0.1 "/opt/web-server" 4 minutes ago Up 4 minutes
25ded12445df dnsutils:1.3 "/bin/sh" 6 minutes ago Up 6 minutes
PORTS NAMES
0.0.0.0:8080->8080/tcp frosty_brown
brave_zhukovsky
让我们使用 docker inspect 命令获取 Golang Web 服务器容器的 Docker IP 地址:
$ sudo docker inspect
-f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
906fd860f84d
172.17.0.2
在 dnsutils 镜像上,我们可以使用 Golang Web 服务器的 Docker 网络地址 172.17.0.2 和容器端口 8080:
/ # wget 172.17.0.2:8080
Connecting to 172.17.0.2:8080 (172.17.0.2:8080)
index.html 100% |*******************************************|
5 0:00:00 ETA
/ # cat index.html
Hello/ #
每个容器都可以通过 docker0 桥接和容器端口互相访问,因为它们位于同一台 Docker 主机和相同网络上。Docker 主机通过路由到容器的 IP 地址和端口来访问容器的 IP 地址:
vagrant@ubuntu-xenial:~$ curl 172.17.0.2:8080
Hello
但是对于来自 docker run 命令的 Docker IP 地址和主机端口,则不起作用:
vagrant@ubuntu-xenial:~$ curl 172.17.0.2:80
curl: (7) Failed to connect to 172.17.0.2 port 80: Connection refused
vagrant@ubuntu-xenial:~$ curl 172.17.0.2:8080
Hello
现在进行反向操作,使用回环接口,我们演示主机只能在主机端口 80 上暴露的 Web 服务器上访问网页服务器,而不能在 Docker 端口 8080 上进行访问:
vagrant@ubuntu-xenial:~$ curl 127.0.0.1:8080
curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
vagrant@ubuntu-xenial:~$ curl 127.0.0.1:80
Hellovagrant@ubuntu-xenial:~$
现在回到 dnsutils 上,情况也是一样的:在 Docker 网络上的 dnsutils 镜像中,使用 Go Web 容器的 Docker IP 地址,只能使用 Docker 端口 8080,而不能使用暴露的主机端口 80:
/ # wget 172.17.0.2:8080 -qO-
Hello/ #
/ # wget 172.17.0.2:80 -qO-
wget: can't connect to remote host (172.17.0.2): Connection refused
现在,为了表明这是一个完全分开的堆栈,让我们尝试 dnsutils 回环地址以及 Docker 端口和暴露的主机端口:
/ # wget localhost:80 -qO-
wget: can't connect to remote host (127.0.0.1): Connection refused
/ # wget localhost:8080 -qO-
wget: can't connect to remote host (127.0.0.1): Connection refused
预期中都不起作用;dnsutils 镜像有单独的网络堆栈,不共享 Go Web 服务器的网络命名空间。了解为什么不起作用对于理解 Kubernetes 至关重要,因为 Pod 是共享相同网络命名空间的一组容器。现在我们将研究两个容器如何在两个分离的主机上进行通信。
容器到容器分开的主机
我们之前的示例向我们展示了如何在本地系统上运行容器网络,但是两个容器如何在分开的主机网络上通信呢?在本例中,我们将在分开的主机上部署容器,并调查这一点以及与在同一主机上的不同之处。
让我们启动第二个 Vagrant Ubuntu 主机 host-2,并像我们的 Docker 主机一样 SSH 进入它。我们可以看到我们的 IP 地址与运行 Golang Web 服务器的 Docker 主机不同:
vagrant@host-2:~$ ifconfig enp0s8
enp0s8 Link encap:Ethernet HWaddr 08:00:27:f9:77:12
inet addr:192.168.1.23 Bcast:192.168.1.255 Mask:255.255.255.0
inet6 addr: fe80::a00:27ff:fef9:7712/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:65630 errors:0 dropped:0 overruns:0 frame:0
TX packets:2967 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:96493210 (96.4 MB) TX bytes:228989 (228.9 KB)
我们可以通过 Docker 主机的 IP 地址 192.168.1.20 访问我们在 docker run 命令选项中暴露的端口 80 的 Web 服务器。端口 80 在 Docker 主机上暴露,但无法通过容器端口 8080 和主机 IP 地址访问:
vagrant@ubuntu-xenial:~$ curl 192.168.1.20:80
Hellovagrant@ubuntu-xenial:~$
vagrant@host-2:~$ curl 192.168.1.20:8080
curl: (7) Failed to connect to 192.168.1.20 port 8080: Connection refused
vagrant@ubuntu-xenial:~$
如果 host-2 尝试使用 Docker 端口或主机端口到达容器的 IP 地址,情况也是如此。请记住,Docker 使用私有地址范围 172.17.0.0/16。
vagrant@host-2:~$ curl 172.17.0.2:8080 -t 5
curl: (7) Failed to connect to 172.17.0.2 port 8080: No route to host
vagrant@host-2:~$ curl 172.17.0.2:80 -t 5
curl: (7) Failed to connect to 172.17.0.2 port 80: No route to host
vagrant@host-2:~$
要使主机路由到 Docker IP 地址,需要使用叠加网络或一些 Docker 外部的外部路由。路由也是 Kubernetes 外部的,许多 CNI 在解决此问题时提供帮助,并且这在部署集群时会进行探讨,参见 第六章。
前面的例子使用了 Docker 默认的网络桥接方式,将端口暴露到主机上。这就是 host-2 能够与运行在 Docker 主机上的 Docker 容器通信的方法。本章仅仅触及了容器网络的表面。还有许多抽象概念需要探索,比如整个集群的入站和出站流量、服务发现,以及集群内外的路由。后续章节将继续深入探讨这些容器网络基础知识。
结论
在这个容器网络介绍中,我们深入了解了容器如何发展,帮助应用程序部署,并通过允许在主机上容纳和分割多个应用程序来提高主机效率。我们回顾了容器的悠久历史,以及已经来去的各种项目。容器通过 Linux 内核中的命名空间和控制组(cgroups)进行动力驱动和管理。我们了解了容器运行时为应用程序开发人员维护的抽象,并学习了如何自己部署它们。理解这些 Linux 内核抽象对于决定部署哪种 CNI、以及其权衡和好处至关重要。管理员现在对容器运行时如何管理 Linux 网络抽象有了基础的理解。
我们已经完成了容器网络的基础知识!我们的知识从使用简单的网络堆栈扩展到在容器内部运行不同的不相关堆栈。了解命名空间、端口如何暴露以及通信流程,使管理员能够快速解决网络问题,并防止其在运行在 Kubernetes 集群中的应用程序中造成停机时间。快速解决端口问题或者测试主机、容器或网络之间的端口是否开放,是任何网络工程师必备的技能,也是开发人员解决其容器问题不可或缺的技能。Kubernetes 建立在这些基础之上,并为开发人员提供了抽象。下一章将审视 Kubernetes 如何创建这些抽象,并将它们整合到 Kubernetes 网络模型中。
第四章:Kubernetes 网络介绍
现在我们已经涵盖了 Linux 和容器网络的关键组成部分,我们准备更详细地讨论 Kubernetes 网络。在本章中,我们将讨论 pod 如何在集群内外部连接。我们还将涵盖 Kubernetes 内部组件如何连接。在下一章节中,将讨论更高级别的网络抽象,如服务和入口的发现和负载均衡。
Kubernetes 网络致力于解决这四个网络问题:
-
高度耦合的容器到容器通信
-
Pod 到 Pod 的通信
-
Pod 到服务的通信
-
外部到服务的通信
Docker 网络模型默认使用虚拟桥接网络,每台主机定义一个私有网络,容器连接到这个网络。容器的 IP 地址分配为私有 IP 地址,这意味着运行在不同机器上的容器无法相互通信。开发者必须映射主机端口到容器端口,然后通过 Docker 代理流量以跨节点到达。在这种情况下,避免容器之间端口冲突由 Docker 管理员负责;通常这是系统管理员的职责。Kubernetes 网络处理方式不同。
Kubernetes 网络模型
Kubernetes 网络模型原生支持多主机集群网络。pod 默认可以与彼此通信,无论它们部署在哪个主机上。Kubernetes 依赖于 CNI 项目来遵守以下要求:
-
所有容器必须在没有 NAT 的情况下相互通信。
-
节点可以在没有 NAT 的情况下与容器通信。
-
容器的 IP 地址与其所看到的容器外部相同。
Kubernetes 中的工作单位称为 pod。一个 pod 包含一个或多个容器,它们总是在同一个节点上调度和运行。“一起”运行的连接允许将服务的单个实例分开成不同的容器。例如,开发者可以选择在一个容器中运行一个服务,在另一个容器中运行日志转发器。在不同的容器中运行进程允许它们有单独的资源配额(例如,“日志转发器不能使用超过 512 MB 的内存”)。它还通过减少构建容器所需的范围,允许容器的构建和部署机制分离。
下面是一个最小化的 Pod 定义。我们省略了许多选项。Kubernetes 管理各种字段,例如 pod 的状态,这些字段是只读的:
apiVersion: v1
kind: Pod
metadata:
name: go-web
namespace: default
spec:
containers:
- name: go-web
image: go-web:v0.0.1
ports:
- containerPort: 8080
protocol: TCP
Kubernetes 用户通常不直接创建 Pod。相反,用户创建高级工作负载,如部署,根据某些预期的规范管理 Pod。在部署的情况下,如图 4-1 所示,用户指定了 Pod 的模板,以及希望存在的 Pod 数量(通常称为副本)。还有几种其他管理工作负载的方式,如 ReplicaSets 和 StatefulSets,我们将在下一章中进行审查。一些提供对中间类型的抽象,而其他一些直接管理 Pod。还有第三方工作负载类型,以自定义资源定义(CRDs)的形式存在。Kubernetes 中的工作负载是一个复杂的话题,我们只会尝试涵盖非常基础和适用于网络堆栈的部分。

图 4-1. 部署与 Pod 的关系
Pod 本身是临时的,意味着它们会被删除并替换为新版本。Pod 的短寿命是对开发人员和操作员的主要意外和挑战,他们熟悉更半永久、传统物理或虚拟机的情况。本地磁盘状态、节点调度和 IP 地址在 Pod 生命周期中都会定期更换。
一个 Pod 拥有独特的 IP 地址,该 IP 地址被 Pod 内所有容器共享。赋予每个 Pod 一个 IP 地址的主要动机是消除端口号的约束。在 Linux 中,每个地址、端口和协议只能由一个程序监听。如果 Pod 没有独特的 IP 地址,那么同一节点上的两个 Pod 可能会竞争相同的端口(例如两个 Web 服务器,都试图监听端口 80)。如果它们相同,就需要运行时配置来修复,比如 --port 标志。或者,需要通过丑陋的脚本更新配置文件以适应第三方软件的情况。
在某些情况下,第三方软件根本无法在自定义端口上运行,这将需要更复杂的解决方案,例如节点上的 iptables DNAT 规则。Web 服务器还存在一个额外问题,即期望在其软件中使用传统端口号,如 HTTP 的 80 和 HTTPS 的 443。偏离这些惯例需要通过负载均衡器进行反向代理或使下游使用者了解各种端口(对内部系统比对外系统更容易)。一些系统,如 Google 的 Borg,使用这种模型。Kubernetes 选择了每个 Pod 分配 IP 地址的模型,以便开发人员更容易接受并更轻松地运行第三方工作负载。不幸的是,为每个 Pod 分配和路由 IP 地址增加了 Kubernetes 集群的相当复杂性。
警告
默认情况下,Kubernetes 将允许任何 Pod 之间的任何流量。这种被动的连接性意味着,在集群中的任何 Pod 都可以连接到同一集群中的任何其他 Pod。这可能很容易被滥用,特别是如果服务未使用身份验证或攻击者获取了凭证。
更多信息,请参阅“流行的 CNI 插件”。
使用其自身 IP 地址创建和删除的 Pod 可能会对不理解此行为的初学者造成问题。假设我们在 Kubernetes 上运行一个小型服务,形式为具有三个 Pod 副本的部署。当有人更新部署中的容器映像时,Kubernetes 执行滚动升级,删除旧的 Pod 并使用新容器映像创建新的 Pod。这些新的 Pod 可能会有新的 IP 地址,导致旧的 IP 地址无法访问。手动在配置或 DNS 记录中引用 Pod IP 并使其无法解析可能是初学者的常见错误。这是服务和端点试图解决的错误,并将在下一章讨论。
在显式创建 Pod 时,可以指定 IP 地址。StatefulSets 是一种内置的工作负载类型,旨在处理诸如数据库等工作负载,它们保持 Pod 身份概念,并使新 Pod 具有与其替换的 Pod 相同的名称和 IP 地址。还有第三方 CRD 的其他示例形式,可以编写用于特定网络目的的 CRD。
注意
自定义资源是由编写者定义的 Kubernetes API 扩展。它允许软件开发人员在 Kubernetes 环境中定制其软件的安装方式。您可以在文档中找到有关编写 CRD 的更多信息。
每个 Kubernetes 节点都运行一个名为Kubelet的组件,它管理节点上的 Pod。Kubelet 中的网络功能来自节点上 CNI 插件的 API 交互。CNI 插件负责管理 Pod IP 地址和单个容器网络的配置。在前一章中,我们提到了 CNI 的名义接口部分;CNI 定义了管理容器网络的标准接口。将 CNI 设计为接口的原因是为了拥有可互操作的标准,在其中存在多个 CNI 插件实现。CNI 插件负责分配 Pod IP 地址并维护所有(适用的)Pod 之间的路由。Kubernetes 并未随附默认的 CNI 插件,这意味着在标准的 Kubernetes 安装中,Pod 无法使用网络。
让我们开始讨论通过 CNI 启用 Pod 网络以及不同的网络布局。
节点和 Pod 网络布局
集群必须有一组 IP 地址来控制分配给 pod 的 IP 地址,例如 10.1.0.0/16。节点和 pod 必须在此 IP 地址空间中具有 L3 连通性。从 第 1 章 中回忆,在 L3 中,互联网层的连通性意味着带有 IP 地址的数据包可以路由到具有该 IP 地址的主机。重要的是要注意,传递 数据包 的能力比创建连接(L4 概念)更为基础。在 L4 中,防火墙可以选择允许从主机 A 到 B 的连接,但拒绝从主机 B 到 A 发起的连接。必须允许 A 到 B 和 B 到 A 的 L4 连接,在 L3 中,A 到 B 的连接。没有 L3 连通性,TCP 握手将不可能进行,因为 SYN-ACK 无法传递。
一般情况下,pod 没有 MAC 地址。因此,无法与 pod 建立 L2 连接。CNI 将为 pod 确定这一点。
Kubernetes 对外部世界的 L3 连通性没有任何要求。尽管大多数集群具有互联网连接,但出于安全原因,有些集群更加孤立。
我们将广泛讨论入口(离开主机或集群的流量)和出口(进入主机或集群的流量)。在这里使用的“入口”不应与 Kubernetes 入口资源混淆,后者是一种特定的 HTTP 机制,用于将流量路由到 Kubernetes 服务。
对于构建集群网络,一般有三种方法,以及许多变体:孤立、扁平和岛屿网络。我们将在此处讨论一般方法,然后在后面的章节中讨论 CNI 插件的具体实现细节。
孤立网络
在孤立的集群网络中,节点在更广泛的网络上是可路由的(即,不属于集群的主机可以访问集群中的节点),但是 pod 不可以。图 4-2 显示了这样一个集群。请注意,pod 不能访问集群外的其他 pod(或任何其他主机)。
因为集群不能从更广泛的网络路由,多个集群甚至可以使用相同的 IP 地址空间。请注意,如果外部系统或用户需要访问 Kubernetes API,则 Kubernetes API 服务器必须从更广泛的网络可路由。许多托管 Kubernetes 的提供者都有这样的“安全集群”选项,其中集群与互联网之间没有直接的流量。
如果集群的工作负载允许/需要这样的设置,例如批处理集群,那么将该集群与本地集群隔离对安全性来说可能是很好的。然而,并不是所有的集群都适合这种方式。大多数集群需要访问和/或被外部系统访问,例如必须支持对更广泛互联网有依赖的服务的集群。负载均衡器和代理可以用来突破这一障碍,并允许互联网流量进入或离开孤立的集群。

图 4-2. 同一网络中的两个隔离集群
扁平网络
在扁平网络中,所有的 Pod 都有一个 IP 地址,可以从更广泛的网络路由到达。除非有防火墙规则,网络上的任何主机都可以路由到集群内外的任何 Pod。这种配置在网络简单性和性能方面有许多优势。Pod 可以直接连接网络中的任意主机。
请注意在图 4-3 中,两个集群之间没有两个节点的 Pod CIDR 重叠,因此不会有两个 Pod 被分配相同的 IP 地址。由于更广泛的网络可以路由到每个 Pod IP 地址到该 Pod 的节点,因此网络上的任何主机都可以与任何 Pod 通信。
这种开放性允许具有足够服务发现数据的任何主机决定哪个 Pod 将接收这些数据包。集群外的负载均衡器可以负载均衡 Pod,例如另一个集群中的 gRPC 客户端。

图 4-3. 同一扁平网络中的两个集群
外部 Pod 流量(以及当连接的目的地是特定 Pod IP 地址时的传入 Pod 流量)具有低延迟和低开销。任何形式的代理或数据包重写都会带来延迟和处理成本,这些成本虽小但不可忽视(尤其是在涉及多个后端服务的应用架构中,每次延迟都会累积)。
不幸的是,这种模型要求每个集群都有一个大且连续的 IP 地址空间(即 IP 地址范围内的每个 IP 地址都在您的控制之下)。Kubernetes 对于 Pod IP 地址(每个 IP 系列)需要一个单一的 CIDR。这种模型可以通过私有子网(如 10.0.0.0/8 或 172.16.0.0/12)来实现;然而,如果使用公共 IP 地址,特别是 IPv4 地址,那就要困难得多且更加昂贵。管理员需要使用 NAT 来连接运行在私有 IP 地址空间中的集群与互联网。
除了需要大量的 IP 地址空间外,管理员还需要一个易于编程的网络。CNI 插件必须分配 Pod IP 地址,并确保存在到特定 Pod 节点的路由。
在云服务提供商环境中,私有子网上的扁平网络很容易实现。绝大多数云服务提供商网络都提供大型私有子网,并具有用于 IP 地址分配和路由管理的 API(甚至预先存在的 CNI 插件)。
岛屿网络
岛屿集群网络在高层次上是隔离和扁平网络的结合体。
在孤岛集群设置中,如 图 4-4 所示,节点与更广泛的网络具有 L3 连接,但是 Pod 没有。来自和发往 Pod 的流量必须通过某种形式的代理经过节点。这通常是通过 Pod 离开节点时的iptables源 NAT 来实现的。这种设置被称为伪装,使用 SNAT 将包的源地址从 Pod 的 IP 地址重写为节点的 IP 地址(有关 SNAT 的复习,请参考第 2 章)。换句话说,数据包看起来是从节点发出的,而不是从 Pod 发出的。
共享 IP 地址同时使用 NAT 会隐藏各个 Pod 的 IP 地址。在集群边界上基于 IP 地址的防火墙和识别变得困难。在集群内部,仍然可以明确哪个 IP 地址对应哪个 Pod(因此也是哪个应用程序)。在其他集群中的 Pod 或更广泛网络上的其他主机将不再具有该映射关系。基于 IP 地址的防火墙和白名单本身并不足以提供安全性,但它们是一层有价值且有时是必需的保护层。
现在让我们看看如何使用kube-controller-manager配置任何这些网络布局。控制平面 指确定发送数据包或帧所使用路径的所有功能和过程。数据平面 指根据控制平面逻辑从一个接口转发数据包/帧的所有功能和过程。

图 4-4. “孤岛网络”配置中的两个示例
kube-controller-manager 配置
kube-controller-manager 在一个二进制和一个进程中运行大多数独立的 Kubernetes 控制器,其中大多数 Kubernetes 逻辑存在。从高层次来看,Kubernetes 中的控制器是指观察资源并采取行动以同步或强制执行特定状态(要么是期望的状态,要么反映当前状态作为状态)。Kubernetes 有许多控制器,它们通常“拥有”特定的对象类型或特定的操作。
kube-controller-manager 包括多个控制器,用于管理 Kubernetes 网络堆栈。特别是管理员在这里设置集群 CIDR。
kube-controller-manager 由于运行了大量的控制器,所以有大量的标志。 表 4-1 突出了一些显著的网络配置标志。
表 4-1. kube-controller-manager 选项
| 标志 | 默认值 | 描述 |
|---|---|---|
--allocate-node-cidrs |
true | 设置是否应在云提供商上为 Pod 分配和设置 CIDR。 |
--CIDR-allocator-type string |
RangeAllocator | 要使用的 CIDR 分配器类型。 |
--cluster-CIDR |
用于分配 Pod IP 地址的 CIDR 范围。要求--allocate-node-cidrs为 true。如果kube-controller-manager启用了IPv6DualStack,--cluster-CIDR接受一个逗号分隔的 IPv4 和 IPv6 CIDR 对。 |
|
--configure-cloud-routes |
true | 设置是否应由 allocate-node-cidrs 分配 CIDR,并在云提供商上配置。 |
--node-CIDR-mask-size |
24 用于 IPv4 集群,64 用于 IPv6 集群 | 集群中节点 CIDR 的掩码大小。Kubernetes 将为每个节点分配 2^(node-CIDR-mask-size) 个 IP 地址。 |
--node-CIDR-mask-size-ipv4 |
24 | 集群中节点 CIDR 的掩码大小。在双栈集群中使用此标志允许 IPv4 和 IPv6 设置。 |
--node-CIDR-mask-size-ipv6 |
64 | 集群中节点 CIDR 的掩码大小。在双栈集群中使用此标志允许 IPv4 和 IPv6 设置。 |
--service-cluster-ip-range |
集群中服务的 CIDR 范围,用于分配服务的 ClusterIP。需要 --allocate-node-cidrs 为 true。如果 kube-controller-manager 启用了 IPv6DualStack,--service-cluster-ip-range 可接受逗号分隔的 IPv4 和 IPv6 CIDR 对。 |
提示
所有 Kubernetes 二进制文件在在线文档中都有它们标志的文档。在 文档 中查看所有 kube-controller-manager 选项。
现在我们已经讨论了 Kubernetes 控制平面中的高级网络架构和网络配置,让我们更详细地看一下 Kubernetes 工作节点如何处理网络。
Kubelet
Kubelet 是在集群中每个工作节点上运行的单个二进制文件。在高层次上,Kubelet 负责管理调度到节点上的任何 Pod,并为节点及其上的 Pod 提供状态更新。然而,Kubelet 主要作为节点上其他软件的协调器。Kubelet 管理容器网络实现(通过 CNI)和容器运行时(通过 CRI)。
注意
我们定义工作节点为能够运行 Pod 的 Kubernetes 节点。某些集群技术上在受限制的工作节点上运行 API 服务器和 etcd。这种设置可以让控制平面组件与典型工作负载一样自动化管理,但也会暴露额外的故障模式和安全漏洞。
当控制器(或用户)在 Kubernetes API 中创建一个 Pod 时,它最初只存在于 Pod API 对象中。Kubernetes 调度器会监视这样的 Pod,并尝试选择一个有效的节点来调度该 Pod。对此调度有多个约束条件。我们的 Pod 及其 CPU/内存请求不能超过节点上未请求的 CPU/内存余量。有许多选择项可用,例如与带标签节点或其他带标签 Pod 的亲和性/反亲和性或节点上的污点。假设调度器找到满足所有 Pod 约束条件的节点,则调度器将该节点的名称写入我们 Pod 的 nodeName 字段。假设 Kubernetes 将 Pod 调度到 node-1:
apiVersion: v1
kind: Pod
metadata:
name: example
spec:
nodeName: "node-1"
containers:
- name: example
image: example:1.0
Kubelet 在 node-1 上监视所有调度到该节点的 pod。相应的 kubectl 命令将是 kubectl get pods -w --field-selector spec.nodeName=node-1。当 Kubelet 观察到我们的 pod 存在但不在节点上时,它会创建它。我们将跳过 CRI 的详细信息和容器本身的创建。一旦容器存在,Kubelet 就会向 CNI 发出 ADD 调用,告诉 CNI 插件创建 pod 网络。我们将在下一节介绍接口和插件。
Pod 就绪和探针
Pod 就绪是一个额外指示,表明 pod 是否准备好提供流量服务。Pod 的就绪性决定了该 pod 地址是否从外部源在 Endpoints 对象中显示。其他管理 pod 的 Kubernetes 资源,如部署,考虑 pod 的就绪性以进行决策,例如在滚动更新期间推进。在滚动部署期间,新的 pod 变为就绪,但服务、网络策略或负载均衡器尚未准备好接受新的 pod,可能会导致服务中断或后端容量的丢失。值得注意的是,如果 pod 规范包含任何类型的探针,Kubernetes 将默认为所有三种类型的成功。
用户可以在 pod 规范中指定 pod 的就绪检查。从那里,Kubelet 执行指定的检查,并根据成功或失败更新 pod 的状态。
探测器影响 pod 的 .Status.Phase 字段。以下是 pod 各个阶段及其描述的列表:
等待中
pod 已被集群接受,但一个或多个容器尚未设置并准备好运行。这包括等待调度的 pod 的时间以及通过网络下载容器镜像的时间。
运行中
pod 已被调度到一个节点,并且所有的容器都已经被创建。至少有一个容器仍在运行或正在启动或重新启动过程中。请注意,某些容器可能处于失败状态,例如 CrashLoopBackoff 状态。
已成功
所有 pod 中的容器都以成功终止,并且将不会重新启动。
失败
所有 pod 中的容器已经终止,并且至少有一个容器以失败状态终止。换句话说,该容器要么以非零状态退出,要么被系统终止。
未知
由于无法确定 pod 的状态的某些原因,这一阶段通常是由于与应运行 pod 的 Kubelet 通信错误引起的。
Kubelet 对 pod 中各个容器执行多种类型的健康检查:存活探针 (livenessProbe)、就绪探针 (readinessProbe) 和 启动探针 (startupProbe)。Kubelet(以及节点本身)必须能够连接到该节点上运行的所有容器,以执行任何 HTTP 健康检查。
每个探测器有三种结果之一:
成功
容器通过了诊断。
失败
容器未通过诊断。
未知
诊断失败,因此不应采取任何行动。
探针可以是执行探针,尝试在容器内部执行二进制文件,TCP 探针或 HTTP 探针。如果探针失败次数超过failureThreshold,Kubernetes 将视为检查失败。其影响取决于探针类型。
当容器的就绪探针失败时,Kubelet 不会终止它。相反,Kubelet 会将失败写入到 Pod 的状态中。
如果活跃探针失败,Kubelet 将终止容器。如果滥用或配置不当,活跃探针可能会导致意外故障。活跃探针的预期用例是告知 Kubelet 何时重新启动容器。然而,作为人类,我们很快学会,“出了问题就重启”是一种危险的策略。例如,假设我们创建一个活跃探针,加载我们 Web 应用的主页。进一步假设系统中的某些变化(超出容器代码范围)导致主页返回 404 或 500 错误。这种情况有很多频繁的原因,如后端数据库故障、必需服务故障或功能标志更改导致的错误。在这些情况中,活跃探针将重新启动容器。最好的情况下,这将毫无帮助;重新启动容器不能解决系统其他地方的问题,并且可能会迅速恶化问题。Kubernetes 拥有容器重启退避(CrashLoopBackoff),会在重新启动失败的容器时添加递增的延迟。如果 Pod 数量足够多或故障发生足够迅速,应用可能会从首页错误变成完全不可用。根据应用程序,Pod 在重新启动时可能也会丢失缓存数据;在假设的恶化期间可能会很费力或无法获取。因此,谨慎使用活跃探针。当 Pod 使用它们时,它们仅依赖于它们正在测试的容器,没有其他依赖性。许多工程师具有特定的健康检查端点,这些端点提供最小的验证标准,如“PHP 正在运行并提供我的 API”。
启动探针可以在活跃探针生效之前提供宽限期。在启动探针成功之前,活跃探针不会终止容器。一个示例用例是允许容器启动需要多分钟,但如果启动后变得不健康,则快速终止容器。
在 示例 4-1 中,我们的 Golang Web 服务器具有一个活跃探针,在端口 8080 上对路径 /healthz 执行 HTTP GET,而就绪探针使用相同端口上的 /。
示例 4-1. Golang 极简 Web 服务器的 Kubernetes PodSpec
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: go-web
spec:
containers:
- name: go-web
image: go-web:v0.0.1
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
此状态不影响 Pod 本身,但其他 Kubernetes 机制对其做出反应。一个关键示例是 ReplicaSets(以及由此延伸的部署)。如果就绪探针失败,ReplicaSet 控制器将计算 Pod 为未就绪,当太多新的 Pod 不健康时导致部署停止。Endpoints/EndpointsSlice控制器也会对就绪探针失败做出反应。如果 Pod 的就绪探针失败,Pod 的 IP 地址将不在端点对象中,并且服务将不会将流量路由到它。我们将在下一章节更详细地讨论服务和端点。
startupProbe会告知 Kubelet 容器内的应用程序是否已启动。此探针优先于其他探针。如果 Pod 规范中定义了startupProbe,则会禁用所有其他探针。一旦startupProbe成功,Kubelet 将开始运行其他探针。但是如果启动探针失败,Kubelet 会终止容器,并根据重启策略执行容器。与其他探针一样,如果不存在startupProbe,默认状态为成功。
探测配置选项:
初始延迟秒数
容器启动后多少秒开始执行存活或就绪探针。默认为 0;最小为 0。
周期秒数
探测频率。默认为 10;最小为 1。
超时秒数
多少秒后探针超时。默认为 1;最小为 1。
成功阈值
探测失败后必须连续成功的最小次数。默认为 1;存活探针和启动探针必须为 1;最小为 1。
失败阈值
当探针失败时,Kubernetes 将尝试此次数后放弃。对于存活探针,放弃意味着容器将重新启动。对于就绪探针,Pod 将标记为未就绪。默认为 3;最小为 1。
应用开发者还可以使用就绪门来帮助确定 Pod 内部应用程序何时准备就绪。自 Kubernetes 1.14 以来可用且稳定,要使用就绪门,清单编写者将在 Pod 规范中添加就绪门,以指定 Kubelet 用于 Pod 准备就绪的额外条件列表。这是在就绪门的ConditionType属性中完成的。ConditionType是 Pod 条件列表中具有匹配类型的条件。就绪门由 Pod 的status.condition字段的当前状态控制,如果 Kubelet 在 Pod 的status.conditions字段中找不到这样的条件,则条件的状态默认为 False。
正如您在以下示例中所看到的,feature-Y就绪门为 true,而feature-X为 false,因此 Pod 的状态最终为 false:
kind: Pod
…
spec:
readinessGates:
- conditionType: www.example.com/feature-X
- conditionType: www.example.com/feature-Y
…
status:
conditions:
- lastProbeTime: null
lastTransitionTime: 2021-04-25T00:00:00Z
status: "False"
type: Ready
- lastProbeTime: null
lastTransitionTime: 2021-04-25T00:00:00Z
status: "False"
type: www.example.com/feature-X
- lastProbeTime: null
lastTransitionTime: 2021-04-25T00:00:00Z
status: "True"
type: www.example.com/feature-Y
containerStatuses:
- containerID: docker://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ready : true
负载均衡器(如 AWS ALB)可以在发送流量之前使用就绪探针作为 Pod 生命周期的一部分。
Kubelet 必须能够连接到 Kubernetes API 服务器。在 图 4-5 中,我们可以看到集群中所有组件进行的连接:
CNI
Kubelet 中的网络插件,用于为 pod 和服务获取 IP。
gRPC
从 API 服务器到 etcd 的通信 API。
Kubelet
所有 Kubernetes 节点都有一个 Kubelet,确保任何分配给它的 pod 都在运行并配置为所需状态。
CRI
Kubelet 中编译的 gRPC API,允许 Kubelet 使用 gRPC API 与容器运行时进行通信。容器运行时提供者必须将其适配到 CRI API,以允许 Kubelet 使用 OCI 标准(runC)与容器进行通信。CRI 包括协议缓冲区和 gRPC API 和库。

图 4-5. 组件之间的集群数据流
pod 和 Kubelet 之间的通信是通过 CNI 可能的。在下一节中,我们将通过几个流行的 CNI 项目的示例讨论 CNI 规范。
CNI 规范
CNI 规范本身非常简单。根据规范,CNI 插件必须支持四种操作:
ADD
将容器添加到网络中。
DEL
从网络中删除容器。
CHECK
如果容器的网络出现问题,则返回错误。
VERSION
报告关于插件的版本信息。
Tip
完整的 CNI 规范可以在 GitHub 上找到。
在 图 4-6 中,我们可以看到 Kubernetes(或作为 CNI 项目指向容器编排器的 runtime)如何通过执行二进制文件调用 CNI 插件操作。Kubernetes 通过 stdin 向命令提供任何配置,并通过 stdout 接收命令的输出。CNI 插件通常具有非常简单的二进制文件,这些文件作为 Kubernetes 调用的包装器,而二进制文件通过 HTTP 或 RPC API 调用持久后端。CNI 维护者已讨论过在频繁启动 Windows 进程时基于性能问题将其更改为 HTTP 或 RPC 模型。
Kubernetes 每次只使用一个 CNI 插件,尽管 CNI 规范允许多插件设置(即为容器分配多个 IP 地址)。Multus 是一个 CNI 插件,通过作为多个 CNI 插件的扇出器,绕过 Kubernetes 中的此限制。
Note
截至撰写本文时,CNI 规范版本为 0.4. 多年来并未有大幅变化,并且未来看起来也不太可能发生变化——规范的维护者计划很快发布 1.0 版本。

图 4-6. CNI 配置
CNI 插件
CNI 插件有两个主要责任:为 Pod 分配唯一的 IP 地址并确保在 Kubernetes 中存在到每个 Pod IP 地址的路由。这些责任意味着集群所在的主要网络决定了 CNI 插件的行为。例如,如果 IP 地址太少或无法将足够的 IP 地址附加到节点上,则集群管理员将需要使用支持覆盖网络的 CNI 插件。硬件堆栈或所使用的云提供商通常决定了哪些 CNI 选项适合使用。第六章将讨论主要云平台及其网络设计如何影响 CNI 的选择。
要使用 CNI,请在 Kubelet 的启动参数中添加 --network-plugin=cni。默认情况下,Kubelet 从目录 /etc/cni/net.d/ 读取 CNI 配置,并期望在 /opt/cni/bin/ 中找到 CNI 二进制文件。管理员可以使用 --cni-config-dir=<directory> 覆盖配置位置,使用 --cni-bin-dir=<directory> 覆盖 CNI 二进制文件目录。
注意
托管的 Kubernetes 提供和许多 Kubernetes 的“发行版”都预配置了 CNI。
CNI 网络模型有两大类:平面网络和覆盖网络。在平面网络中,CNI 驱动程序使用集群网络的 IP 地址,通常需要集群中有很多 IP 地址可用。在覆盖网络中,CNI 驱动程序在 Kubernetes 内部创建一个次要网络,该网络使用集群网络(称为底层网络)发送数据包。覆盖网络在集群内创建虚拟网络。在覆盖网络中,CNI 插件封装数据包。我们在第三章中详细讨论了覆盖网络。覆盖网络增加了相当多的复杂性,不允许集群网络上的主机直接连接到 Pod。但是,覆盖网络允许集群网络更小,因为只需为节点分配 IP 地址。
CNI 插件通常也需要一种方式在节点之间传递状态。插件采取不同的方法,例如将数据存储在 Kubernetes API 中或专用数据库中。
CNI 插件还负责调用 IPAM 插件进行 IP 地址分配。
IPAM 接口
CNI 规范还有第二个接口,即 IP 地址管理(IPAM)接口,以减少在 CNI 插件中 IP 分配代码的重复。IPAM 插件必须确定并输出接口 IP 地址、网关和路由,如示例 4-2 所示。IPAM 接口与 CNI 类似:它是一个二进制文件,通过标准输入接收 JSON 输入,并通过标准输出返回 JSON 输出。
示例 4-2. CNI 0.4 规范文档中的 IPAM 插件输出示例
{
"cniVersion": "0.4.0",
"ips": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>" (optional)
},
...
],
"routes": [ (optional)
{
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
},
...
]
"dns": { (optional)
"nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional)
"search": <list-of-search-domains> (optional)
"options": <list-of-options> (optional)
}
}
当部署 CNI 时,现在我们将回顾集群管理员可供选择的几个选项。
热门的 CNI 插件
Cilium 是一种开源软件,用于在应用程序容器之间透明地保护网络连接。Cilium 是一个 L7/HTTP 感知的 CNI,可以使用基于身份的安全模型在 L3-L7 上执行网络策略,与网络寻址分离。我们在第二章中讨论的 Linux 技术 eBPF 是 Cilium 的动力来源。本章稍后我们将深入探讨NetworkPolicy对象;现在只需知道它们实际上是面向 Pod 的防火墙。
Flannel 专注于网络,并且是为 Kubernetes 设计的第 3 层网络结构的简单易用方式。如果集群需要网络策略等功能,管理员必须部署其他 CNI,例如 Calico。Flannel 使用 Kubernetes 集群的现有etcd来存储其状态信息,以避免提供专用数据存储。
根据 Calico 的说法,它“结合了灵活的网络功能和运行任何地方的安全执行,提供具有本地 Linux 内核性能和真正云原生可伸缩性的解决方案。” Calico 不使用覆盖网络。相反,Calico 配置一个第 3 层网络,使用 BGP 路由协议在主机之间路由数据包。Calico 还可以与服务网格 Istio 集成,在服务网格和网络基础设施层面解释和执行工作负载的策略。
表 4-2 提供了主要 CNI 插件的简要概述,供选择。
表 4-2. 主要 CNI 插件的简要概述
| 名称 | NetworkPolicy 支持 | 数据存储 | 网络设置 |
|---|---|---|---|
| Cilium | 是 | etcd 或 consul | Ipvlan(beta), veth, L7 感知 |
| Flannel | 否 | etcd | 第 3 层 IPv4 覆盖网络 |
| Calico | 是 | etcd 或 Kubernetes API | 使用 BGP 的第 3 层网络 |
| Weave Net | 是 | 没有外部集群存储 | 网格覆盖网络 |
注意
KIND、Helm 和 Cilium 的完整运行说明可在书籍的 GitHub 存储库中找到。
让我们在示例 4-3 中使用我们的 Golang Web 服务器测试部署 Cilium。我们将需要一个 Kubernetes 集群来部署 Cilium。我们发现用于本地测试部署集群最简单的方法之一是 KIND,它代表 Kubernetes in Docker。它将允许我们使用一个 YAML 配置文件创建一个集群,然后使用 Helm 将 Cilium 部署到该集群。
示例 4-3. 适用于 Cilium 本地部署的 KIND 配置
kind: Cluster 
apiVersion: kind.x-k8s.io/v1alpha4 
nodes: 
- role: control-plane 
- role: worker 
- role: worker 
- role: worker 
networking: 
disableDefaultCNI: true 
指定我们正在配置一个 KIND 集群
KIND 配置的版本
集群中的节点列表
一个控制平面节点
工作节点 1
工作节点 2
工作节点 3
KIND 网络配置选项
禁用默认网络选项,以便部署 Cilium
注意
在文档中可以找到配置 KIND 集群等的说明。
使用 KIND 集群配置 YAML,我们可以使用以下命令使用 KIND 创建该集群。如果这是您第一次运行它,下载工作和控制平面 Docker 镜像将需要一些时间:
$ kind create cluster --config=kind-config.yaml
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.18.
2) Preparing nodes
✓ Writing configuration Starting control-plane
Installing StorageClass Joining worker nodes Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a question, bug, or feature request?
Let us know! https://kind.sigs.k8s.io/#community ߙ⊭---
Always verify that the cluster is up and running with kubectl.
$ kubectl cluster-info --context kind-kind
Kubernetes master -> control plane is running at https://127.0.0.1:59511
KubeDNS is running at
https://127.0.0.1:59511/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump.'
注意
直到 Cilium 部署网络,集群节点将保持 NotReady 状态。这对于集群是正常行为。
现在我们的集群在本地运行,我们可以开始使用 Helm 安装 Cilium,这是一个 Kubernetes 部署工具。根据其文档,Helm 是安装 Cilium 的首选方式。首先,我们需要添加 Cilium 的 Helm 仓库。可选地,您可以下载 Cilium 的 Docker 镜像,最后指示 KIND 将 Cilium 镜像加载到集群中:
$ helm repo add cilium https://helm.cilium.io/
# Pre-pulling and loading container images is optional.
$ docker pull cilium/cilium:v1.9.1
kind load docker-image cilium/cilium:v1.9.1
现在,Cilium 的先决条件已完成,我们可以使用 Helm 在我们的集群中安装它。Cilium 有许多配置选项,Helm 使用 --set NAME_VAR=VAR 配置选项:
$ helm install cilium cilium/cilium --version 1.10.1 \
--namespace kube-system
NAME: Cilium
LAST DEPLOYED: Fri Jan 1 15:39:59 2021
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble.
Your release version is 1.10.1.
For any further help, visit https://docs.cilium.io/en/v1.10/gettinghelp/
Cilium 在集群中安装了几个部件:代理、客户端、运营商和 cilium-cni 插件:
代理
Cilium 代理 cilium-agent 在集群中的每个节点上运行。代理通过 Kubernetes API 接受配置,描述网络、服务负载均衡、网络策略以及可见性和监控需求。
客户端(CLI)
Cilium CLI 客户端(Cilium)是与 Cilium 代理一起安装的命令行工具。它与同一节点上的 REST API 进行交互。CLI 允许开发人员检查本地代理的状态和状态。它还提供了访问 eBPF 映射以直接验证其状态的工具。
运营商
运营商负责在集群中管理职责,应按集群而不是按节点处理。
CNI 插件
CNI 插件 (cilium-cni) 与节点的 Cilium API 进行交互,触发配置以为 pods 提供网络、负载均衡和网络策略。
我们可以通过 kubectl -n kube-system get pods --watch 命令在集群中观察所有这些组件的部署情况:
$ kubectl -n kube-system get pods --watch
NAME READY STATUS
cilium-65kvp 0/1 Init:0/2
cilium-node-init-485lj 0/1 ContainerCreating
cilium-node-init-79g68 1/1 Running
cilium-node-init-gfdl8 1/1 Running
cilium-node-init-jz8qc 1/1 Running
cilium-operator-5b64c54cd-cgr2b 0/1 ContainerCreating
cilium-operator-5b64c54cd-tblbz 0/1 ContainerCreating
cilium-pg6v8 0/1 Init:0/2
cilium-rsnqk 0/1 Init:0/2
cilium-vfhrs 0/1 Init:0/2
coredns-66bff467f8-dqzql 0/1 Pending
coredns-66bff467f8-r5nl6 0/1 Pending
etcd-kind-control-plane 1/1 Running
kube-apiserver-kind-control-plane 1/1 Running
kube-controller-manager-kind-control-plane 1/1 Running
kube-proxy-k5zc2 1/1 Running
kube-proxy-qzhvq 1/1 Running
kube-proxy-v54p4 1/1 Running
kube-proxy-xb9tr 1/1 Running
kube-scheduler-kind-control-plane 1/1 Running
cilium-operator-5b64c54cd-tblbz 1/1 Running
现在我们已经部署了 Cilium,我们可以运行 Cilium 连通性检查,以确保它正常运行:
$ kubectl create ns cilium-test
namespace/cilium-test created
$ kubectl apply -n cilium-test \
-f \
https://raw.githubusercontent.com/strongjz/advanced_networking_code_examples/
master/chapter-4/connectivity-check.yaml
deployment.apps/echo-a created
deployment.apps/echo-b created
deployment.apps/echo-b-host created
deployment.apps/pod-to-a created
deployment.apps/pod-to-external-1111 created
deployment.apps/pod-to-a-denied-cnp created
deployment.apps/pod-to-a-allowed-cnp created
deployment.apps/pod-to-external-fqdn-allow-google-cnp created
deployment.apps/pod-to-b-multi-node-clusterip created
deployment.apps/pod-to-b-multi-node-headless created
deployment.apps/host-to-b-multi-node-clusterip created
deployment.apps/host-to-b-multi-node-headless created
deployment.apps/pod-to-b-multi-node-nodeport created
deployment.apps/pod-to-b-intra-node-nodeport created
service/echo-a created
service/echo-b created
service/echo-b-headless created
service/echo-b-host-headless created
ciliumnetworkpolicy.cilium.io/pod-to-a-denied-cnp created
ciliumnetworkpolicy.cilium.io/pod-to-a-allowed-cnp created
ciliumnetworkpolicy.cilium.io/pod-to-external-fqdn-allow-google-cnp created
连通性测试将部署一系列 Kubernetes 部署,这些部署将使用各种连接路径。连接路径包括具有和不具有服务负载均衡以及各种网络策略组合。Pod 名称指示了连接变体,并且就绪性和存活性门指示了测试的成功或失败:
$ kubectl get pods -n cilium-test -w
NAME READY STATUS
echo-a-57cbbd9b8b-szn94 1/1 Running
echo-b-6db5fc8ff8-wkcr6 1/1 Running
echo-b-host-76d89978c-dsjm8 1/1 Running
host-to-b-multi-node-clusterip-fd6868749-7zkcr 1/1 Running
host-to-b-multi-node-headless-54fbc4659f-z4rtd 1/1 Running
pod-to-a-648fd74787-x27hc 1/1 Running
pod-to-a-allowed-cnp-7776c879f-6rq7z 1/1 Running
pod-to-a-denied-cnp-b5ff897c7-qp5kp 1/1 Running
pod-to-b-intra-node-nodeport-6546644d59-qkmck 1/1 Running
pod-to-b-multi-node-clusterip-7d54c74c5f-4j7pm 1/1 Running
pod-to-b-multi-node-headless-76db68d547-fhlz7 1/1 Running
pod-to-b-multi-node-nodeport-7496df84d7-5z872 1/1 Running
pod-to-external-1111-6d4f9d9645-kfl4x 1/1 Running
pod-to-external-fqdn-allow-google-cnp-5bc496897c-bnlqs 1/1 Running
现在 Cilium 管理我们集群的网络,我们将在本章稍后使用它来进行 NetworkPolicy 的概述。并非所有的 CNI 插件都支持 NetworkPolicy,这在选择插件时是一个重要的细节。
kube-proxy
kube-proxy 是 Kubernetes 中另一个每节点的守护程序,就像 Kubelet 一样。kube-proxy 在集群内提供基本的负载均衡功能。它实现了服务,并依赖于 Endpoints/EndpointSlices,这两个我们将在下一章节关于网络抽象中详细讨论的 API 对象。可能会对该部分进行参考,但以下是相关的简要解释:
-
Services 定义了一组 pod 的负载均衡器。
-
Endpoints(和 endpoint slices)列出一组就绪的 pod IP 地址。它们自动从一个服务创建,并且与服务具有相同的 pod 选择器。
大多数类型的服务都有一个服务的 IP 地址,称为集群 IP 地址,这个地址在集群外部不能路由。kube-proxy 负责将请求路由到服务的集群 IP 地址到健康的 pod。kube-proxy 是 Kubernetes 服务中目前最常见的实现,但是也有替代 kube-proxy 的选择,比如替代模式 Cilium。我们在 第二章 中关于路由的大部分内容也适用于 kube-proxy,特别是在调试服务连接或性能时。
注意
集群 IP 地址通常不能从集群外部路由。
kube-proxy 有四种模式,这些模式改变了它的运行模式和具体功能集:userspace、iptables、ipvs 和 kernelspace。你可以使用 --proxy-mode <mode> 来指定模式。值得注意的是,所有模式在某种程度上都依赖于 iptables。
userspace 模式
第一个且最古老的模式是 userspace 模式。在 userspace 模式中,kube-proxy 运行一个 web 服务器,并将所有服务 IP 地址路由到该 web 服务器,使用 iptables。Web 服务器终止连接并代理请求到服务端点中的 pod。userspace 模式不再常用,我们建议除非有明确的理由,否则避免使用它。
iptables 模式
iptables 模式完全使用 iptables。它是默认模式,也是最常用的(部分原因可能是 IPVS 模式最近才稳定并普及,而 iptables 是熟悉的 Linux 技术)。
iptables 模式执行连接扇出,而不是真正的负载均衡。换句话说,iptables 模式将一个连接路由到一个后端的 pod,所有使用该连接的请求将会发送到同一个 pod,直到连接终止。在理想情况下(例如,相同连接中的连续请求可以从后端 pod 的本地缓存中受益),这很简单且行为可预测。但在处理长连接时(例如 HTTP/2 连接,特别是 gRPC 的传输),则可能会变得不可预测。假设你有两个 pod,X 和 Y,为一个服务提供服务,而你在正常的滚动更新期间将 X 替换为 Z。老旧的 pod Y 仍然具有所有现有的连接,再加上半数需要在 pod X 关闭时重新建立的连接,导致 pod Y 承载更多的流量。类似的情况有很多,导致流量不平衡。
请回想我们在 第二章 的“实际 iptables”部分中的示例。在其中,我们展示了 iptables 可以配置为具有 IP 地址列表和随机路由概率,使得连接随机分布在所有 IP 地址之间。给定一个有健康后端 pod 10.0.0.1、10.0.0.2、10.0.0.3 和 10.0.0.4 的服务,kube-proxy 将创建顺序规则以以下方式路由连接:
-
25% 的连接将到
10.0.0.1。 -
33.3% 的未路由连接将到
10.0.0.2。 -
50% 的未路由连接将到
10.0.0.3。 -
所有未路由的连接都会到
10.0.0.4。
这可能看起来不直观,并导致一些工程师认为 kube-proxy 在路由流量时出现问题(特别是因为当服务按预期工作时,很少有人会关注 kube-proxy)。关键细节在于,每个路由规则都适用于之前未路由的连接。最终的规则将所有连接路由到 10.0.0.4(因为连接必须去“某个地方”),半最终的规则有 50% 的机会将连接路由到 10.0.0.3 作为两个 IP 地址的选择,以此类推。路由的随机分数总是计算为 1 / 剩余 IP 地址的数量。
这是集群中 kube-dns 服务的 iptables 转发规则。在我们的示例中,kube-dns 服务的集群 IP 地址是 10.96.0.10。此输出已经经过过滤和重新格式化以便清晰查看:
$ sudo iptables -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target prot opt source destination
/* kube-system/kube-dns:dns cluster IP */ udp dpt:domain
KUBE-MARK-MASQ udp -- !10.217.0.0/16 10.96.0.10
/* kube-system/kube-dns:dns cluster IP */ udp dpt:domain
KUBE-SVC-TCOU7JCQXEZGVUNU udp -- anywhere 10.96.0.10
/* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:domain
KUBE-MARK-MASQ tcp -- !10.217.0.0/16 10.96.0.10
/* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:domain
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- anywhere 10.96.0.10 ADDRTYPE
match dst-type LOCAL
/* kubernetes service nodeports; NOTE: this must be the
last rule in this chain */
KUBE-NODEPORTS all -- anywhere anywhere
为 kube-dns 设置了一对 UDP 和 TCP 规则。我们将重点关注 UDP 规则。
第一个 UDP 规则将来自于非 pod IP 地址(10.217.0.0/16 是默认的 pod 网络 CIDR)的服务连接标记为伪装。
下一个 UDP 规则以 KUBE-SVC-TCOU7JCQXEZGVUNU 作为其目标链条。让我们仔细看看:
$ sudo iptables -t nat -L KUBE-SVC-TCOU7JCQXEZGVUNU
Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references)
target prot opt source destination
/* kube-system/kube-dns:dns */
KUBE-SEP-OCPCMVGPKTDWRD3C all -- anywhere anywhere statistic mode
random probability 0.50000000000
/* kube-system/kube-dns:dns */
KUBE-SEP-VFGOVXCRCJYSGAY3 all -- anywhere anywhere
这里我们看到一个有 50% 执行机会的链条,以及另一个将执行的链条。如果我们检查第一个链条,我们会看到它路由到 10.0.1.141,这是我们两个 CoreDNS pod 中的一个 IP 地址之一:
$ sudo iptables -t nat -L KUBE-SEP-OCPCMVGPKTDWRD3C
Chain KUBE-SEP-OCPCMVGPKTDWRD3C (1 references)
target prot opt source destination
/* kube-system/kube-dns:dns */
KUBE-MARK-MASQ all -- 10.0.1.141 anywhere
/* kube-system/kube-dns:dns */ udp to:10.0.1.141:53
DNAT udp -- anywhere anywhere
ipvs 模式
ipvs 模式使用 IPVS 进行连接负载平衡,详见第二章,而不是使用iptables。ipvs 模式支持六种负载均衡模式,可以使用--ipvs-scheduler指定:
-
rr: 轮询 -
lc: 最少连接 -
dh: 目的哈希 -
sh: 源哈希 -
sed: 最短预期延迟 -
nq: 永不排队
Round-robin (rr) 是默认的负载均衡模式。它与iptables模式的行为最接近(即无论 Pod 状态如何,连接都会相对均匀地建立),尽管iptables模式实际上并不执行循环轮询路由。
kernelspace 模式
kernelspace 是最新的,仅适用于 Windows 的模式。它为 Kubernetes 在 Windows 上提供了一种替代userspace模式的选择,因为iptables和ipvs是特定于 Linux 的。
现在我们已经介绍了 Kubernetes 中 Pod 之间流量的基础知识,让我们来看看NetworkPolicy和保护 Pod 之间流量的方法。
NetworkPolicy
Kubernetes 的默认行为是允许集群网络中任意两个 Pod 之间的流量。这种行为是一种有意设计的选择,以便于采纳和配置的灵活性,但在实践中却是极其不可取的。允许任何系统进行(或接收)任意连接会带来风险。攻击者可以探测系统,潜在地利用截获的凭据或找到弱化或缺失的认证机制。允许任意连接还会使得通过受损工作负载从系统中渗透数据变得更加容易。总之,我们强烈不建议在真实集群中运行没有NetworkPolicy的情况。由于所有 Pod 可以与所有其他 Pod 通信,我们强烈建议应用所有者使用NetworkPolicy对象以及其他应用层安全措施,例如认证令牌或互相认证传输层安全(mTLS),来进行任何网络通信。
NetworkPolicy 是 Kubernetes 中的一种资源类型,包含基于允许的防火墙规则。用户可以添加NetworkPolicy对象来限制与 Pod 的连接。NetworkPolicy资源作为 CNI 插件的配置,CNI 插件负责确保 Pod 之间的连接性。Kubernetes API 声明NetworkPolicy对于 CNI 驱动是可选的,这意味着一些 CNI 驱动程序不支持网络策略,如表 4-3 所示。如果开发人员在使用不支持NetworkPolicy对象的 CNI 驱动程序时创建了NetworkPolicy,它不会影响 Pod 的网络安全性。一些 CNI 驱动程序,如企业产品或公司内部 CNI 驱动程序,可能会引入它们自己的NetworkPolicy等效物。一些 CNI 驱动程序可能也会对NetworkPolicy规范有稍微不同的“解释”。
表 4-3. 常见 CNI 插件及其 NetworkPolicy 支持
| CNI 插件 | 支持 NetworkPolicy |
|---|---|
| Calico | 是,并支持额外的插件特定策略 |
| Cilium | 是的,并支持额外的特定插件策略 |
| Flannel | 否 |
| Kubenet | 否 |
示例 4-4 详细介绍了一个 NetworkPolicy 对象,其中包含一个 pod 选择器、入口规则和出口规则。该策略将适用于与匹配选择器标签的 NetworkPolicy 同一命名空间中的所有 pods。这种选择器标签的用法与其他 Kubernetes API 保持一致:一个规范通过它们的标签而不是它们的名称或父对象来识别 pods。
示例 4-4. 一个 NetworkPolicy 的广泛结构
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: demo
namespace: default
spec:
podSelector:
matchLabels:
app: demo
policyTypes:
- Ingress
- Egress
ingress: []NetworkPolicyIngressRule # Not expanded
egress: []NetworkPolicyEgressRule # Not expanded
在深入探讨 API 之前,让我们通过创建一个简单的示例来演示如何创建一个 NetworkPolicy 来减少某些 pods 的访问范围。假设我们有两个不同的组件:demo 和 demo-DB。在 图 4-7 中,因为没有现有的 NetworkPolicy,所有 pods 可以与所有其他 pods 通信(包括假设的无关 pods,未显示)。

图 4-7. 没有 NetworkPolicy 对象的 Pods
让我们限制 demo-DB 的访问级别。如果我们创建以下选择 demo-DB pods 的 NetworkPolicy,demo-DB pods 将无法发送或接收任何流量:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: demo-db
namespace: default
spec:
podSelector:
matchLabels:
app: demo-db
policyTypes:
- Ingress
- Egress
在 图 4-8 中,我们现在可以看到标有 app=demo 的 pods 不再能够创建或接收连接。

图 4-8. 标有 app:demo-db 标签的 Pods 无法接收或发送流量
对大多数工作负载来说,没有网络访问是不理想的,包括我们的示例数据库。我们的 demo-db 应该(仅)能够从 demo pods 接收连接。为此,我们必须向 NetworkPolicy 添加一个入口规则:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: demo-db
namespace: default
spec:
podSelector:
matchLabels:
app: demo-db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: demo
现在 demo-db pods 只能从 demo pods 接收连接。此外,demo-db pods 不能创建连接(如 图 4-9 所示)。

图 4-9. 标有 app:demo-db 标签的 Pods 无法创建连接,它们只能接收来自 app:demo pods 的连接
警告
如果用户无意或恶意更改标签,他们可以改变 NetworkPolicy 对象应用于所有 pods 的方式。在我们之前的示例中,如果攻击者能够在同一命名空间中的某个 pod 上编辑 app: demo-DB 标签,那么我们创建的 NetworkPolicy 将不再适用于该 pod。类似地,如果攻击者能够在同一命名空间中的另一个 pod 上添加标签 app: demo,他们可以从受损的 pod 获得访问权限。
先前的示例只是一个示例;使用 Cilium,我们可以为我们的 Golang web 服务器创建这些 NetworkPolicy 对象。
使用 Cilium 的 NetworkPolicy 示例
我们的 Golang Web 服务器现在连接到一个没有 TLS 的 Postgres 数据库。此外,在没有 NetworkPolicy 对象的情况下,网络上的任何 pod 都可以嗅探 Golang Web 服务器与数据库之间的流量,这是一个潜在的安全风险。接下来将部署我们的 Golang Web 应用程序及其数据库,然后部署 NetworkPolicy 对象,只允许从 Web 服务器连接到数据库。使用与 Cilium 安装相同的 KIND 集群,让我们使用以下的 YAML 和 kubectl 命令来部署 Postgres 数据库:
$ kubectl apply -f database.yaml
service/postgres created
configmap/postgres-config created
statefulset.apps/postgres created
在这里,我们将我们的 Web 服务器部署为 Kubernetes 部署到我们的 KIND 集群中:
$ kubectl apply -f web.yaml
deployment.apps/app created
为了在集群网络内运行连通性测试,我们将部署并使用一个具有基本网络工具如 ping 和 curl 的 dnsutils pod:
$ kubectl apply -f dnsutils.yaml
pod/dnsutils created
由于我们没有部署具有入口的服务,我们可以使用 kubectl port-forward 来测试连接到我们的 Web 服务器的连通性:
kubectl port-forward app-5878d69796-j889q 8080:8080
注意
关于 kubectl port-forward 的更多信息可以在 文档 中找到。
现在从我们的本地终端,我们可以访问我们的 API:
$ curl localhost:8080/
Hello
$ curl localhost:8080/healthz
Healthy
$ curl localhost:8080/data
Database Connected
让我们测试从其他 pod 内部连接到我们集群中的 Web 服务器的连通性。为此,我们需要获取我们 Web 服务器 pod 的 IP 地址:
$ kubectl get pods -l app=app -o wide
NAME READY STATUS RESTARTS AGE IP NODE
app-5878d69796-j889q 1/1 Running 0 87m 10.244.1.188 kind-worker3
现在我们可以从 dnsutils pod 测试到 Web 服务器的 L4 和 L7 连通性:
$ kubectl exec dnsutils -- nc -z -vv 10.244.1.188 8080
10.244.1.188 (10.244.1.188:8080) open
sent 0, rcvd 0
从我们的 dnsutils 中,我们可以测试层 7 的 HTTP API 访问:
$ kubectl exec dnsutils -- wget -qO- 10.244.1.188:8080/
Hello
$ kubectl exec dnsutils -- wget -qO- 10.244.1.188:8080/data
Database Connected
$ kubectl exec dnsutils -- wget -qO- 10.244.1.188:8080/healthz
Healthy
我们也可以在数据库 pod 上进行测试。首先,我们必须获取数据库 pod 的 IP 地址,10.244.2.189。我们可以使用 kubectl 结合标签和选项来获取这些信息:
$ kubectl get pods -l app=postgres -o wide
NAME READY STATUS RESTARTS AGE IP NODE
postgres-0 1/1 Running 0 98m 10.244.2.189 kind-worker
再次使用 dnsutils pod 来测试通过默认端口 5432 连接到 Postgres 数据库的连通性:
$ kubectl exec dnsutils -- nc -z -vv 10.244.2.189 5432
10.244.2.189 (10.244.2.189:5432) open
sent 0, rcvd 0
由于没有设置网络策略,所有人都可以使用这个端口。现在让我们用 Cilium 网络策略来限制这一点。以下命令将部署网络策略,以便测试安全的网络连接。首先,限制对数据库 pod 的访问,只允许来自 Web 服务器的流量。应用只允许从 Web 服务器 pod 到数据库的网络策略:
$ kubectl apply -f layer_3_net_pol.yaml
ciliumnetworkpolicy.cilium.io/l3-rule-app-to-db created
Cilium 对象的 Cilium 部署创建的资源可以像使用 kubectl 获取 pod 一样检索。通过 kubectl describe ciliumnetworkpolicies.cilium.io l3-rule-app-to-db,我们可以看到通过 YAML 部署的规则的所有信息:
$ kubectl describe ciliumnetworkpolicies.cilium.io l3-rule-app-to-db
Name: l3-rule-app-to-db
Namespace: default
Labels: <none>
Annotations: API Version: cilium.io/v2
Kind: CiliumNetworkPolicy
Metadata:
Creation Timestamp: 2021-01-10T01:06:13Z
Generation: 1
Managed Fields:
API Version: cilium.io/v2
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.:
f:kubectl.kubernetes.io/last-applied-configuration:
f:spec:
.:
f:endpointSelector:
.:
f:matchLabels:
.:
f:app:
f:ingress:
Manager: kubectl
Operation: Update
Time: 2021-01-10T01:06:13Z
Resource Version: 47377
Self Link:
/apis/cilium.io/v2/namespaces/default/ciliumnetworkpolicies/l3-rule-app-to-db
UID: 71ee6571-9551-449d-8f3e-c177becda35a
Spec:
Endpoint Selector:
Match Labels:
App: postgres
Ingress:
From Endpoints:
Match Labels:
App: app
Events: <none>
应用了网络策略后,dnsutils pod 无法再访问数据库 pod;我们可以从 dnsutils pod 尝试连接到 DB 端口的超时中看到这一点:
$ kubectl exec dnsutils -- nc -z -vv -w 5 10.244.2.189 5432
nc: 10.244.2.189 (10.244.2.189:5432): Operation timed out
sent 0, rcvd 0
command terminated with exit code 1
虽然 Web 服务器 pod 仍然连接到数据库 pod,但 /data 路由将 Web 服务器连接到数据库,并且 NetworkPolicy 允许这样做:
$ kubectl exec dnsutils -- wget -qO- 10.244.1.188:8080/data
Database Connected
$ curl localhost:8080/data
Database Connected
现在让我们应用层 7 策略。Cilium 是层 7 感知的,所以我们可以阻止或允许特定的 HTTP URI 路径请求。在我们的示例策略中,我们允许 / 和 /data 上的 HTTP GET 请求,但不允许 /healthz 上的请求;让我们测试一下:
$ kubectl apply -f layer_7_netpol.yml
ciliumnetworkpolicy.cilium.io/l7-rule created
我们可以看到策略像 Kubernetes API 中的其他对象一样应用:
$ kubectl get ciliumnetworkpolicies.cilium.io
NAME AGE
l7-rule 6m54s
$ kubectl describe ciliumnetworkpolicies.cilium.io l7-rule
Name: l7-rule
Namespace: default
Labels: <none>
Annotations: API Version: cilium.io/v2
Kind: CiliumNetworkPolicy
Metadata:
Creation Timestamp: 2021-01-10T00:49:34Z
Generation: 1
Managed Fields:
API Version: cilium.io/v2
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.:
f:kubectl.kubernetes.io/last-applied-configuration:
f:spec:
.:
f:egress:
f:endpointSelector:
.:
f:matchLabels:
.:
f:app:
Manager: kubectl
Operation: Update
Time: 2021-01-10T00:49:34Z
Resource Version: 43869
Self Link:/apis/cilium.io/v2/namespaces/default/ciliumnetworkpolicies/l7-rule
UID: 0162c16e-dd55-4020-83b9-464bb625b164
Spec:
Egress:
To Ports:
Ports:
Port: 8080
Protocol: TCP
Rules:
Http:
Method: GET
Path: /
Method: GET
Path: /data
Endpoint Selector:
Match Labels:
App: app
Events: <none>
正如我们所见,/ 和 /data 可用,但 /healthz 不可用,这正是我们从 NetworkPolicy 中期望的。
$ kubectl exec dnsutils -- wget -qO- 10.244.1.188:8080/data
Database Connected
$kubectl exec dnsutils -- wget -qO- 10.244.1.188:8080/
Hello
$ kubectl exec dnsutils -- wget -qO- -T 5 10.244.1.188:8080/healthz
wget: error getting response
command terminated with exit code 1
这些小例子展示了 Cilium 网络策略如何在集群内强制执行网络安全。我们强烈建议管理员选择支持网络策略并强制开发人员使用网络策略的 CNI。网络策略是命名空间的,如果团队有类似的设置,集群管理员可以并且应该强制开发人员为增加安全性定义网络策略。
我们使用了 Kubernetes API 的两个方面,标签和选择器;在下一节中,我们将提供更多关于它们如何在集群内使用的示例。
选择 Pods
直到被 NetworkPolicy 选中,Pods 是不受限制的。如果选中,则 CNI 插件仅在匹配规则允许的情况下允许 Pod 的入口或出口。NetworkPolicy 包含一个 spec.policyTypes 字段,其中包含策略类型(入口或出口)的列表。例如,如果我们选择一个具有列出入口但没有列出出口的 NetworkPolicy 的 Pod,则将限制入口而不限制出口。
spec.podSelector 字段将决定将 NetworkPolicy 应用于哪些 Pod。一个空的 label selector (podSelector: {}) 将选择命名空间中的所有 Pod。我们将在稍后详细讨论标签选择器。
NetworkPolicy 对象是 命名空间 对象,这意味着它们存在于特定的命名空间并且应用于该命名空间。spec.podSelector 字段只能在与 NetworkPolicy 相同的命名空间中选择 Pod。这意味着选择 app: demo 将仅在当前命名空间中应用,并且在另一个命名空间中具有 app: demo 标签的任何 Pod 将不受影响。
有多种解决方法可以实现默认防火墙行为,包括以下几种:
-
为每个命名空间创建一个拒绝所有流量的
NetworkPolicy对象,这将要求开发人员添加额外的NetworkPolicy对象来允许所需的流量。 -
添加一个定制的 CNI 插件,故意违反默认开放的 API 行为。多个 CNI 插件具有额外的配置,暴露出这种类型的行为。
-
创建入学政策要求工作负载具有
NetworkPolicy。
NetworkPolicy 对象严重依赖于标签和选择器;因此,让我们深入探讨更复杂的示例。
LabelSelector 类型
这是本书第一次在资源中看到 LabelSelector。它是 Kubernetes 中一个无处不在的配置元素,在下一章中将多次提到,因此当你到达那里时,回顾本节可能会有所帮助。
Kubernetes 中的每个对象都有一个 metadata 字段,类型为 ObjectMeta。这为每种类型提供了相同的元数据字段,如标签。标签是键-值字符串对的映射:
metadata:
labels:
colour: purple
shape: square
LabelSelector 通过现有标签(或不存在)标识一组资源。 Kubernetes 中很少有资源会通过名称引用其他资源。 相反,大多数资源(如 NetworkPolicy 对象、服务、部署和其他 Kubernetes 对象)使用标签匹配与 LabelSelector。 LabelSelector 也可以在 API 和 kubectl 调用中使用,并避免返回无关的对象。 LabelSelector 有两个字段:matchExpressions 和 matchLabels。 空 LabelSelector 的正常行为是选择范围内的所有对象,例如,与 NetworkPolicy 相同命名空间中的所有 pod。 matchLabels 是两者中较简单的一个。 matchLabels 包含一个键值对映射。 要使对象匹配,每个键必须存在于对象上,并且该键必须具有相应的值。 通常情况下,具有单个键(例如 app=example-thing)的 matchLabels 就足以作为选择器。
在 示例 4-5 中,我们可以看到一个匹配对象,其标签既有 colour=purple 又有 shape=square。
示例 4-5. matchLabels 示例
matchLabels:
colour: purple
shape: square
matchExpressions 更强大但也更复杂。 它包含一个 LabelSelectorRequirement 列表。 所有要求必须为真,对象才能匹配。 表 4-4 显示了 matchExpressions 的所有必需字段。
表 4-4. LabelSelectorRequirement 字段
| Field | 描述 |
|---|---|
| key | 此需求与之比较的标签键。 |
| operator | Exists, DoesNotExist, In, NotIn 中的一个。Exists: 如果存在具有该键的标签,则匹配对象,无论其值如何。NotExists: 如果不存在具有该键的标签,则匹配对象。In: 如果存在具有该键的标签,并且该值是提供的值之一,则匹配对象。NotIn: 如果不存在具有该键的标签,或者该键的值不是提供的值之一,则匹配对象。 |
| values | 问题中关键字的字符串值列表。 当运算符为 In 或 NotIn 时,必须为空。 当运算符为 Exists 或 NotExists 时,不得为空。 |
让我们来看两个 matchExpressions 的简短示例。
我们先前 matchLabels 示例中的 matchExpressions 等效示例显示在 示例 4-6 中。
示例 4-6. matchExpressions 示例 1
matchExpressions:
- key: colour
operator: In
values:
- purple
- key: shape
operator: In
values:
- square
在 示例 4-7 中的 matchExpressions 将匹配颜色不等于红色、橙色或黄色,并具有形状标签的对象。
示例 4-7. matchExpressions 示例 2
matchExpressions:
- key: colour
operator: NotIn
values:
- red
- orange
- yellow
- key: shape
operator: Exists
现在我们已经涵盖了标签,我们可以讨论规则。 规则将在识别出匹配后强制执行我们的网络策略。
规则
NetworkPolicy 对象包含不同的入口和出口配置部分,分别包含入口规则和出口规则的列表。 NetworkPolicy 规则充当例外,或者说是默认阻止由策略中选择的 pod 引起的“允许列表”。规则不能阻止访问;它们只能添加访问。如果多个 NetworkPolicy 对象选择一个 pod,则每个 NetworkPolicy 对象中的所有规则都适用。可能有意义为相同的一组 pod 使用多个 NetworkPolicy 对象(例如,在一个策略中声明应用程序允许,而在另一个策略中声明基础设施允许,如遥测导出)。但是,请记住它们不 需要 是单独的 NetworkPolicy 对象,并且有太多 NetworkPolicy 对象会变得难以理解。
警告
为了支持来自 Kubelet 的健康检查和存活检查,CNI 插件必须始终允许来自 pod 节点的流量。
如果攻击者可以访问节点(即使没有管理员权限),则可能会滥用标签。攻击者可以伪造节点的 IP 并传递带有节点 IP 地址作为源的数据包。
入口规则和出口规则是 NetworkPolicy API 中的离散类型(NetworkPolicyIngressRule 和 NetworkPolicyEgressRule)。但是,它们在功能上结构相同。每个 NetworkPolicyIngressRule/NetworkPolicyEgressRule 包含一个端口列表和一个 NetworkPolicyPeers 列表。
NetworkPolicyPeer 有四种方式让规则引用网络实体:ipBlock、namespaceSelector、podSelector 和组合。
ipBlock 对于允许与外部系统之间的流量很有用。它可以在规则中单独使用,而无需 namespaceSelector 或 podSelector。ipBlock 包含一个 CIDR 和一个可选的 except CIDR。except CIDR 将排除一个子 CIDR(它必须在 CIDR 范围内)。在 示例 4-8 中,我们允许来自范围 10.0.0.0 到 10.0.0.255 中所有 IP 地址的流量,但排除 10.0.0.10。示例 4-9 允许来自任何命名空间中标记为 group:x 的所有 pod 的流量。
示例 4-8. 允许流量示例 1
from:
- ipBlock:
- cidr: "10.0.0.0/24"
- except: "10.0.0.10"
示例 4-9. 允许流量示例 2
#
from:
- namespaceSelector:
- matchLabels:
group: x
在 示例 4-10 中,我们允许来自任何命名空间中标记为 service: x 的所有 pod 的流量。podSelector 的行为类似于我们之前讨论的 spec.podSelector 字段。如果没有 namespaceSelector,它将选择与 NetworkPolicy 相同命名空间中的 pod。
示例 4-10. 允许流量示例 3
from:
- podSelector:
- matchLabels:
service: y
如果我们指定了namespaceSelector和podSelector,则规则将选择所有具有指定 pod 标签的所有命名空间中的 pod,并且具有指定命名空间标签。保持命名空间范围较小是常见且被安全专家强烈推荐的做法;典型的命名空间范围是每个应用程序或服务组或团队。在示例 4-11 中展示了第四种选项,带有命名空间和 pod 选择器。此选择器行为类似于命名空间和 pod 选择器的 AND 条件:pod 必须具有匹配的标签,并且位于具有匹配标签的命名空间中。
示例 4-11. 允许流量示例 4
from:
- namespaceSelector:
- matchLabels:
group: monitoring
podSelector:
- matchLabels:
service: logscraper
请注意,这在 API 中是一种独特的类型,尽管 YAML 语法看起来非常相似。由于to和from部分可以有多个选择器,一个字符就可能导致AND和OR之间的区别,因此编写策略时务必小心。
我们之前关于 API 访问的安全警告在这里同样适用。如果用户可以自定义其命名空间上的标签,他们可以使另一个命名空间中的NetworkPolicy适用于他们的命名空间,这是不期望的。在我们之前的选择器示例中,如果用户可以在任意命名空间上设置标签group: monitoring,他们可能会发送或接收他们不应该的流量。如果相关的NetworkPolicy只有一个命名空间选择器,那么该命名空间标签就足以匹配策略。如果NetworkPolicy选择器中还有一个 pod 标签,用户将需要设置 pod 标签以匹配策略选择。然而,在典型的设置中,服务所有者将在该服务的命名空间上授予 pod 的创建/更新权限(直接在 pod 资源上或间接通过像部署这样的资源定义 pod)。
典型的NetworkPolicy看起来可能是这样的:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: store-api
namespace: store
spec:
podSelector:
matchLabels: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
app: frontend
podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
app: downstream-1
podSelector:
matchLabels:
app: downstream-1
- namespaceSelector:
matchLabels:
app: downstream-2
podSelector:
matchLabels:
app: downstream-2
ports:
- protocol: TCP
port: 8080
在这个例子中,我们的store命名空间中的所有 pod 只能从标记为app: frontend的命名空间中的 pod 接收连接。这些 pod 只能与其 pod 和命名空间都有app: downstream-1或app: downstream-2的 pod 建立连接。在这些情况下,只允许到端口 8080 的流量。最后,请记住,此策略并不保证与downstream-1或downstream-2匹配的策略(请参阅下一个示例)。接受这些连接并不排除我们命名空间中 pod 的其他策略,添加额外的异常情况:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: store-to-downstream-1
namespace: downstream-1
spec:
podSelector:
app: downstream-1
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
app: store
ports:
- protocol: TCP
port: 8080
尽管它们是“稳定”的资源(即网络/ v1 API 的一部分),我们认为NetworkPolicy对象仍然是 Kubernetes 网络安全的早期版本。配置NetworkPolicy对象的用户体验有些粗糙,而默认的开放行为是极不理想的。目前有一个工作组讨论NetworkPolicy的未来以及 v2 API 将包含什么内容。
CNIs 及其部署者使用标签和选择器来确定哪些 Pod 受网络限制的影响。正如我们在许多先前的示例中所看到的,它们是 Kubernetes API 的重要组成部分,开发人员和管理员都必须全面了解如何使用它们。
NetworkPolicy 对象是集群管理员工具箱中的一个重要工具。它们是控制内部集群流量的唯一工具,原生于 Kubernetes API。我们将讨论服务网格,这将为管理员增加更多工具,以确保和控制工作负载,详见“服务网格”。
接下来我们将讨论另一个重要工具,以便管理员能够了解其在集群内部的工作方式:域名系统(DNS)。
DNS
DNS 对于任何网络都是关键基础设施。在 Kubernetes 中也不例外,因此有必要进行简要概述。在接下来的“服务”部分中,我们将看到它们在 DNS 上的依赖程度以及为什么 Kubernetes 发行版如果没有提供遵循规范的 DNS 服务,则不能宣称为符合 Kubernetes 分发。但首先,让我们回顾一下 Kubernetes 内部 DNS 的工作方式。
注意
我们不会在本书中详细阐述整个规范。如果读者对此感兴趣,可以在GitHub 上找到更多信息。
KubeDNS 在较早版本的 Kubernetes 中使用。KubeDNS 在单个 Pod 内包含多个容器:kube-dns、dnsmasq 和 sidecar。kube-dns 容器监视 Kubernetes API,并根据 Kubernetes DNS 规范提供 DNS 记录,dnsmasq 提供缓存和 stub 域支持,sidecar 提供指标和健康检查。从 Kubernetes 1.13 版本后,现在使用单独的组件 CoreDNS。
CoreDNS 和 KubeDNS 之间有几个区别:
-
为简单起见,CoreDNS 作为单个容器运行。
-
CoreDNS 是一个复制和增强 Kube-DNS 功能的 Go 进程。
-
CoreDNS 被设计为一个通用的 DNS 服务器,与 Kubernetes 兼容,并且其可扩展的插件能够执行超出 Kubernetes DNS 规范所提供的功能。
图 4-10 显示了 CoreDNS 的组件。它以默认副本数为 2 运行部署,并且为了运行,CoreDNS 需要访问 API 服务器,一个 ConfigMap 用于保存其 Corefile,一个服务来使 DNS 可用于集群,并一个部署来启动和管理其 Pod。所有这些也在 kube-system 命名空间中运行,与集群中的其他关键组件一起。

图 4-10. CoreDNS 组件
与大多数配置选项一样,Pod 如何进行 DNS 查询是在 Pod 规范的 dnsPolicy 属性下定义的。
如示例 4-12 所示,Pod 规范中的 dnsPolicy 是 ClusterFirstWithHostNet。
示例 4-12. 带有 DNS 配置的 Pod 规范
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: default
spec:
containers:
- image: busybox:1.28
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
name: busybox
restartPolicy: Always
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
有四种选项的dnsPolicy会显著影响 pod 内部 DNS 解析的工作方式:
默认
pod 从运行其的节点继承名称解析配置。
ClusterFirst
任何不匹配集群域后缀的 DNS 查询,例如 www.kubernetes.io,都会发送到从节点继承的上游名称服务器。
ClusterFirstWithHostNet
对于使用hostNetwork运行的 pod,管理员应将 DNS 策略设置为ClusterFirstWithHostNet。
无
所有 DNS 设置使用 pod 规范中的dnsConfig字段。
如果为none,开发人员必须在 pod 规范中指定名称服务器。nameservers:是一个 IP 地址列表,pod 将使用它作为 DNS 服务器。最多可以指定三个 IP 地址。searches:是用于在 pod 中进行主机名查找的 DNS 搜索域列表。Kubernetes 允许最多六个搜索域。以下是一个这样的示例规范:
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: busybox
spec:
containers:
- image: busybox:1.28
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
name: busybox
dnsPolicy: "None"
dnsConfig:
nameservers:
- 1.1.1.1
searches:
- ns1.svc.cluster-domain.example
- my.dns.search.suffix
其他在options字段中,该字段是一个对象列表,每个对象可能具有name属性和value属性(可选)。
所有这些生成的属性与从 DNS 策略中的resolv.conf合并。常规查询选项使 CoreDNS 通过以下搜索路径:
<service>.default.svc.cluster.local
↓
svc.cluster.local
↓
cluster.local
↓
The host search path
主机搜索路径来自 pod DNS 策略或 CoreDNS 策略。
在 Kubernetes 中查询 DNS 记录可能会导致许多请求,并增加应用程序等待 DNS 请求响应的延迟。CoreDNS 有一个名为 Autopath 的解决方案。Autopath 允许服务器端搜索路径完成。它通过去除集群搜索域并在 CoreDNS 服务器上执行查找来快速完成客户端的搜索路径解析;当它找到答案时,它将结果存储为 CNAME,并以一个查询返回,而不是五个。
使用 Autopath 确实会增加 CoreDNS 的内存使用量。确保将 CoreDNS 副本的内存与集群的大小相匹配。确保适当设置 CoreDNS pod 的内存和 CPU 请求。要监视 CoreDNS,它会导出多个指标,这里列出了它暴露的几个:
coredns 构建信息
CoreDNS 本身的信息
DNS 请求总数
总查询计数
DNS 请求持续时间(秒)
处理每个查询的持续时间
DNS 请求大小(字节)
请求大小(字节)
coredns 插件已启用
指示插件是否在每个服务器和区域基础上启用
将 pod 的度量标准与 CoreDNS 的度量标准结合起来,插件管理员将确保 CoreDNS 在集群内保持健康且运行良好。
提示
这只是可用指标的简要概述。完整列表可在此处找到。
通过插件启用 Autopath 和其他指标。这使得 CoreDNS 能够专注于其 DNS 任务,但仍可通过插件框架进行扩展,类似于 CNI 模式。在 Table 4-5 中,我们看到当前可用插件的列表。作为开源项目,任何人都可以贡献插件。有几个特定于云的插件,如 router53,可以从 AWS route53 服务中提供区域数据。
Table 4-5. CoreDNS 插件
| Name | 描述 |
|---|---|
| auto | 启用从 RFC 1035 风格的主文件中自动选取的区域数据。 |
| autopath | 允许服务器端搜索路径完成。autopath [ZONE…] RESOLV-CONF。 |
| bind | 覆盖服务器应绑定到的主机。 |
| cache | 启用前端缓存。cache [TTL] [ZONES…]。 |
| chaos | 允许响应 TXT 查询中的 CH 类。 |
| debug | 禁用崩溃时的自动恢复,以便获得漂亮的堆栈跟踪。text2pcap。 |
| dnssec | 对提供的数据进行即时的 DNSSEC 签名。 |
| dnstap | 启用到 dnstap 的日志记录。http://dnstap.info golang: go get -u -v github.com/dnstap/golang-dnstap/dnstap。 |
| erratic | 用于测试客户端行为的实用插件。 |
| errors | 启用错误日志记录。 |
| etcd | 从 etcd 版本 3 实例读取区域数据。 |
| federation | 通过 kubernetes 插件解析联合查询。 |
| file | 从 RFC 1035 风格的主文件中提供区域数据。 |
| forward | 方便地将 DNS 消息代理到上游解析器。 |
| health | 启用健康检查端点。 |
| host | 从 /etc/hosts 样式文件中提供区域数据。 |
| kubernetes | 从 Kubernetes 集群中读取区域数据。 |
| loadbalancer | 随机化 A、AAAA 和 MX 记录的顺序。 |
| log enables | 查询日志输出到标准输出。 |
| loop detect | 简单的转发循环检测并停止服务器。 |
| metadata | 启用元数据收集器。 |
| metrics | 启用 Prometheus 指标。 |
| nsid | 将此服务器的标识符添加到每个回复中。RFC 5001。 |
| pprof | 在 /debug/pprof 端点下发布运行时分析数据。 |
| proxy | 提供基本的反向代理和强大的负载均衡器。 |
| reload | 允许自动重新加载已更改的 Corefile。优雅的重新加载。 |
| rewrite | 执行内部消息重写。重写名称 foo.example.com foo.default.svc.cluster.local。 |
| root | 简单地指定查找区域文件的根目录。 |
| router53 | 从 AWS route53 提供区域数据。 |
| secondary | 启用从主服务器检索的区域。 |
| template | 根据传入查询动态生成响应。 |
| tls | 配置用于 TLS 和 gRPC 服务器的服务器证书。 |
| trace | 启用基于 OpenTracing 的 DNS 请求跟踪。 |
| whoami | 返回解析器的本地 IP 地址、端口和传输协议。 |
注意
CoreDNS 插件的全面列表可在此处查看。
CoreDNS 具有极高的配置能力,并与 Kubernetes 模型兼容。我们只是触及了 CoreDNS 能力的表面;如果您想了解更多关于 CoreDNS 的信息,我们强烈推荐阅读Learning CoreDNS,作者是 John Belamaric 和 Cricket Liu(O’Reilly)。
CoreDNS 允许 Pod 确定用于访问集群内外的应用程序和服务器的 IP 地址。在下一节中,我们将更深入地讨论集群中 IPv4 和 IPv6 的管理。
IPv4/IPv6 双栈
Kubernetes 对在 IPv4/IPv6“双栈”模式下运行仍在不断演进的支持,允许集群同时使用 IPv4 和 IPv6 地址。Kubernetes 对在 IPv6-only 模式下运行集群已经有了稳定的支持;然而,在 IPv6-only 模式下运行是与仅支持 IPv4 的客户端和主机通信的障碍。双栈模式是允许 IPv6 采用的重要桥梁。我们将尝试描述截至 Kubernetes 1.20 时双栈网络在 Kubernetes 中的当前状态,但请注意,此功能可能会在后续版本中发生重大变化。双栈支持的完整 Kubernetes 增强提案(KEP)可在GitHub上找到。
警告
在 Kubernetes 中,如果设计尚未最终确定,可扩展性/测试覆盖率/可靠性不足,或者在现实世界中尚未充分证明自己,那么一个功能被标记为“alpha”。Kubernetes 增强提案(KEPs)设定了一个单独功能升级到 beta 然后稳定的标准。与所有 alpha 功能一样,Kubernetes 默认禁用双栈支持,必须显式启用该功能。
IPv4/IPv6 功能使得 Pod 网络可以实现以下功能:
-
每个 Pod 具有单独的 IPv4 和 IPv6 地址
-
IPv4 和 IPv6 服务
-
通过 IPv4 和 IPv6 接口进行 Pod 集群出口路由
作为一个 alpha 功能,管理员必须启用 IPv4/IPv6 双栈;为此,必须为您的集群配置网络组件的IPv6DualStack功能门。以下是双栈集群网络选项的列表:
kube-apiserver
-
feature-gates="IPv6DualStack=true" -
service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>
kube-controller-manager
-
feature-gates="IPv6DualStack=true" -
cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR> -
service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR> -
node-cidr-mask-size-ipv4|--node-cidr-mask-size-ipv6默认为/24 用于 IPv4 和/64 用于 IPv6
kubelet
feature-gates="IPv6DualStack=true"
kube-proxy
-
cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR> -
feature-gates="IPv6DualStack=true"
当集群开启 IPv4/IPv6 时,服务现在有一个额外的字段,开发者可以选择ipFamilyPolicy来部署他们的应用程序:
SingleStack
单栈服务。控制平面为服务分配集群 IP,使用首个配置的服务集群 IP 范围。
PreferDualStack
仅在集群启用双栈时使用。此设置将与 SingleStack 具有相同的行为。
RequireDualStack
从 IPv4 和 IPv6 地址范围中分配服务集群 IP 地址。
ipFamilies
一个定义单栈使用哪个 IP 簇或定义双栈 IP 簇顺序的数组;您可以通过在服务上设置此字段来选择地址簇。允许的值为 ["IPv4"]、["IPv6"] 和 ["IPv4","IPv6"](双栈)。
注意
从 1.21 版开始,默认启用 IPv4/IPv6 双栈。
这里是一个示例服务清单,其中 PreferDualStack 设置为 PreferDualStack:
apiVersion: v1
kind: Service
metadata:
name: my-service
labels:
app: MyApp
spec:
ipFamilyPolicy: PreferDualStack
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
结论
Kubernetes 网络模型是集群内部网络设计的基础。运行在节点上的 CNI 实现了 Kubernetes 网络模型中设定的原则。该模型并不定义网络安全性;Kubernetes 的可扩展性允许 CNI 通过网络策略来实现网络安全。
CNI、DNS 和网络安全是集群网络的重要部分;它们连接了第二章中涵盖的 Linux 网络和第三章和第五章中分别涵盖的容器和 Kubernetes 网络。
选择正确的 CNI 需要从开发者和管理员的角度进行评估。需要明确需求并测试 CNIs。我们认为,没有关于网络安全和支持其的 CNI 的讨论,集群就不完整。
DNS 是必不可少的;完整的设置和顺畅运行的网络需要网络和集群管理员在他们的集群中熟练地扩展 CoreDNS。大量 Kubernetes 问题源于 DNS 和 CoreDNS 的配置错误。
在讨论第六章中的云网络和管理员在设计和部署生产集群网络时的选项时,本章中的信息将非常重要。
在我们的下一章中,我们将深入探讨 Kubernetes 如何利用所有这些内容来支持其抽象化。
第五章:Kubernetes 网络抽象
之前,我们涵盖了大量的网络基础知识,以及 Kubernetes 中如何将流量从 A 点传输到 B 点。在本章中,我们将讨论 Kubernetes 中的网络抽象,主要是服务发现和负载均衡。特别值得注意的是,这是关于服务和入口的章节。由于它们试图解决多种用例,这两个资源都因选项繁多而复杂。它们是 Kubernetes 网络栈中最显著的部分,因为它们定义了在 Kubernetes 上部署的应用程序的基本网络特性。这是开发人员与其应用程序的网络堆栈交互的地方。
本章将涵盖 Kubernetes 网络抽象的基本示例及其工作原理的详细信息。为了跟上进度,您需要以下工具:
-
Docker
-
KIND
-
Linkerd
你需要熟悉kubectl exec和Docker exec命令。如果你不熟悉,我们的代码仓库会包含我们讨论的所有命令,所以不要太担心。我们还将使用来自第二章和第三章的ip和netns。请注意,这些工具大多用于调试和显示实现细节;在正常操作期间,您不一定需要它们。
Docker、KIND 和 Linkerd 的安装可以在它们各自的网站上找到,我们还在书籍的代码仓库中提供了更多信息。
提示
kubectl 是本章示例中的一个关键工具,也是操作员与集群及其网络进行交互的标准工具。您应该熟悉kubectl create、apply、get、delete和exec命令。在Kubernetes 文档中了解更多信息或运行kubectl [command] --help。
本章将探讨这些 Kubernetes 网络抽象:
-
有状态副本集
-
终结点
- Endpoint slices
-
服务
-
NodePort
-
集群
-
无头
-
外部
-
LoadBalancer
-
-
入口
-
入口控制器
-
入口规则
-
-
服务网格
- Linkerd
要探索这些抽象,我们将按照以下步骤将示例部署到我们的 Kubernetes 集群中:
-
使用启用了入口的 KIND 集群进行部署。
-
探索有状态副本集。
-
部署 Kubernetes 服务。
-
部署一个入口控制器。
-
部署 Linkerd 服务网格。
这些抽象是 Kubernetes API 为开发人员和管理员提供的核心内容,用于在集群内程序控制通信流动。理解和掌握如何部署这些抽象对于集群内任何工作负载的成功至关重要。通过这些示例,您将了解在特定情况下为您的应用程序使用哪些抽象。
使用 KIND 集群配置 YAML,我们可以使用下一节中的命令使用 KIND 创建该集群。如果这是第一次运行它,下载工作和控制平面 Docker 镜像将需要一些时间。
注意
以下示例假设您仍然从上一章节运行本地的 KIND 集群,以及 Golang Web 服务器和用于测试的dnsutils镜像。
StatefulSets
StatefulSets 是 Kubernetes 中的工作负载抽象,用于像管理部署一样管理 Pod。与部署不同,StatefulSets 为需要这些功能的应用程序添加以下功能:
-
稳定且唯一的网络标识符
-
稳定的持久存储
-
有序、优雅的部署和扩展
-
有序、自动化的滚动更新
部署资源更适合于不具备这些要求的应用程序(例如,将数据存储在外部数据库的服务)。
我们的 Golang 最小 Web 服务器的数据库使用了 StatefulSet。数据库具有服务、用于 Postgres 用户名、密码、测试数据库名称的 ConfigMap,以及运行 Postgres 的容器的 StatefulSet。
现在让我们部署它:
kubectl apply -f database.yaml
service/postgres created
configmap/postgres-config created
statefulset.apps/postgres created
让我们来分析使用 StatefulSet 时 DNS 和网络带来的影响。
要测试集群内的 DNS,我们可以使用dnsutils镜像;该镜像是gcr.io/kubernetes-e2e-test-images/dnsutils:1.3,用于 Kubernetes 测试:
kubectl apply -f dnsutils.yaml
pod/dnsutils created
kubectl get pods
NAME READY STATUS RESTARTS AGE
dnsutils 1/1 Running 0 9s
配置为两个 Pod 的副本,我们看到 StatefulSet 依次部署postgres-0和postgres-1,具有分别为 10.244.1.3 和 10.244.2.3 的 IP 地址:
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
dnsutils 1/1 Running 0 15m 10.244.3.2 kind-worker3
postgres-0 1/1 Running 0 15m 10.244.1.3 kind-worker2
postgres-1 1/1 Running 0 14m 10.244.2.3 kind-worker
这是我们的无头服务 Postgres 的名称,客户端可以用来查询返回端点 IP 地址:
kubectl get svc postgres
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgres ClusterIP <none> 5432/TCP 23m
使用我们的dnsutils镜像,我们可以看到 StatefulSets 的 DNS 名称将返回这些 IP 地址以及 Postgres 服务的集群 IP 地址:
kubectl exec dnsutils -- host postgres-0.postgres.default.svc.cluster.local.
postgres-0.postgres.default.svc.cluster.local has address 10.244.1.3
kubectl exec dnsutils -- host postgres-1.postgres.default.svc.cluster.local.
postgres-1.postgres.default.svc.cluster.local has address 10.244.2.3
kubectl exec dnsutils -- host postgres
postgres.default.svc.cluster.local has address 10.105.214.153
StatefulSets 试图模拟一组固定的持久化机器。作为有状态工作负载的通用解决方案,特定行为可能在特定用例中令人沮丧。
用户常遇到的一个问题是,在使用.spec.updateStrategy.type: RollingUpdate和.spec.podManagementPolicy: OrderedReady时,更新需要手动干预。使用这些设置,如果更新后的 Pod 永远无法准备就绪,用户必须手动干预。
此外,StatefulSets 需要一个服务(最好是无头服务),负责管理 Pod 的网络标识,并且最终用户负责创建此服务。
StatefulSets 有许多配置选项,并且存在许多第三方替代方案(既通用有状态工作负载控制器,也特定软件的工作负载控制器)。
StatefulSets 在 Kubernetes 中针对特定用例提供功能。它们不应用于日常应用程序部署。在本节后面,我们将讨论更适合普通部署的网络抽象。
在接下来的章节中,我们将探讨 endpoints 和 endpoint slices,这是 Kubernetes 服务的支柱。
Endpoints
Endpoints 帮助识别为其提供服务的 pods 正在运行的内容。Endpoints 是由 services 创建和管理的。我们稍后将单独讨论 services,以避免一次性涵盖太多新内容。现在,让我们只说服务包含标准标签选择器(在 第四章 中介绍),定义 endpoints 中的 pods。
在 图 5-1 中,我们可以看到流量被定向到节点 2 上的 endpoint,pod 5。

图 5-1. 服务中的 Endpoints
让我们讨论如何在集群中创建和维护此 endpoint 的更好视图。
每个 endpoint 包含一个端口列表(适用于所有 pods)和两个地址列表:ready 和 unready:
apiVersion: v1
kind: Endpoints
metadata:
labels:
name: demo-endpoints
subsets:
- addresses:
- ip: 10.0.0.1
- notReadyAddresses:
- ip: 10.0.0.2
ports:
- port: 8080
protocol: TCP
如果通过了 pod 的就绪检查,地址将列在 .addresses 中。如果没有通过,地址将列在 .notReadyAddresses 中。这使得 endpoints 成为一个 服务发现 工具,您可以观察 Endpoints 对象以查看所有 pods 的健康状态和地址:
kubectl get endpoints clusterip-service
NAME ENDPOINTS
clusterip-service 10.244.1.5:8080,10.244.2.7:8080,10.244.2.8:8080 + 1 more...
我们可以使用 kubectl describe 查看所有地址的更好视图:
kubectl describe endpoints clusterip-service
Name: clusterip-service
Namespace: default
Labels: app=app
Annotations: endpoints.kubernetes.io/last-change-trigger-time:
2021-01-30T18:51:36Z
Subsets:
Addresses: 10.244.1.5,10.244.2.7,10.244.2.8,10.244.3.9
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 8080 TCP
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
让我们移除 app 标签,看看 Kubernetes 的反应。在另一个终端中,运行此命令。这将允许我们实时查看 pods 的变化:
kubectl get pods -w
在另一个单独的终端中,让我们用 endpoints 做同样的事情:
kubectl get endpoints -w
现在我们需要获取要从 Endpoints 对象中删除的 pod 名称:
kubectl get pods -l app=app -o wide
NAME READY STATUS RESTARTS AGE IP NODE
app-5586fc9d77-7frts 1/1 Running 0 19m 10.244.1.5 kind-worker2
app-5586fc9d77-mxhgw 1/1 Running 0 19m 10.244.3.9 kind-worker3
app-5586fc9d77-qpxwk 1/1 Running 0 20m 10.244.2.7 kind-worker
app-5586fc9d77-tpz8q 1/1 Running 0 19m 10.244.2.8 kind-worker
使用 kubectl label,我们可以修改 pod 的 app-5586fc9d77-7frts 的 app=app 标签:
kubectl label pod app-5586fc9d77-7frts app=nope --overwrite
pod/app-5586fc9d77-7frts labeled
对于相同的原因,endpoints 和 pods 的观察命令都会看到一些变化:移除 pod 上的标签 app=app。endpoint 控制器将注意到 pod 的变化,部署控制器也会。所以 Kubernetes 做了 Kubernetes 擅长的事情:使实际状态反映所需状态:
kubectl get pods -w
NAME READY STATUS RESTARTS AGE
app-5586fc9d77-7frts 1/1 Running 0 21m
app-5586fc9d77-mxhgw 1/1 Running 0 21m
app-5586fc9d77-qpxwk 1/1 Running 0 22m
app-5586fc9d77-tpz8q 1/1 Running 0 21m
dnsutils 1/1 Running 3 3h1m
postgres-0 1/1 Running 0 3h
postgres-1 1/1 Running 0 3h
app-5586fc9d77-7frts 1/1 Running 0 22m
app-5586fc9d77-7frts 1/1 Running 0 22m
app-5586fc9d77-6dcg2 0/1 Pending 0 0s
app-5586fc9d77-6dcg2 0/1 Pending 0 0s
app-5586fc9d77-6dcg2 0/1 ContainerCreating 0 0s
app-5586fc9d77-6dcg2 0/1 Running 0 2s
app-5586fc9d77-6dcg2 1/1 Running 0 7s
部署有四个 pods,但我们重新标记的 pod 仍然存在:app-5586fc9d77-7frts:
kubectl get pods
NAME READY STATUS RESTARTS AGE
app-5586fc9d77-6dcg2 1/1 Running 0 4m51s
app-5586fc9d77-7frts 1/1 Running 0 27m
app-5586fc9d77-mxhgw 1/1 Running 0 27m
app-5586fc9d77-qpxwk 1/1 Running 0 28m
app-5586fc9d77-tpz8q 1/1 Running 0 27m
dnsutils 1/1 Running 3 3h6m
postgres-0 1/1 Running 0 3h6m
postgres-1 1/1 Running 0 3h6m
Pod app-5586fc9d77-6dcg2 现在已成为部署和 endpoint 对象的一部分,带有 IP 地址 10.244.1.6:
kubectl get pods app-5586fc9d77-6dcg2 -o wide
NAME READY STATUS RESTARTS AGE IP NODE
app-5586fc9d77-6dcg2 1/1 Running 0 3m6s 10.244.1.6 kind-worker2
像往常一样,我们可以使用 kubectl describe 查看详细的完整图片:
kubectl describe endpoints clusterip-service
Name: clusterip-service
Namespace: default
Labels: app=app
Annotations: endpoints.kubernetes.io/last-change-trigger-time:
2021-01-30T19:14:23Z
Subsets:
Addresses: 10.244.1.6,10.244.2.7,10.244.2.8,10.244.3.9
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 8080 TCP
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
对于大型部署,那个 endpoint 对象可能会变得非常庞大,以至于实际上会减慢集群中的变更速度。为了解决这个问题,Kubernetes 的维护者提出了 endpoint slices。
Endpoint Slices
你可能会问,它们与 endpoints 有何不同?这正是我们开始深入研究 Kubernetes 网络的地方。
在典型集群中,Kubernetes 在每个节点上运行 kube-proxy。kube-proxy 负责使服务正常工作的每个节点部分,通过处理路由和 出站 负载均衡到服务中的所有 pod。为了做到这一点,kube-proxy 监视集群中的所有 endpoints,以便了解所有服务应路由到的适用 pod。
现在,想象一下我们有一个 大 的集群,有数千个节点和成千上万个 pod。这意味着成千上万个 kube-proxy 在监视 endpoints。当 Endpoints 对象中的地址发生变化(比如滚动更新、扩展、驱逐、健康检查失败或其他任何原因),更新后的 Endpoints 对象会推送到所有正在监听的 kube-proxy。由于 pod 的数量更多,意味着 Endpoints 对象更大且变化更频繁,这加剧了问题。这最终会对 etcd、Kubernetes API 服务器和网络本身造成压力。Kubernetes 的扩展限制是复杂的,并且取决于具体的标准,但 endpoint 监视是具有数千节点的集群中常见的问题。据传闻,许多 Kubernetes 用户认为 endpoint 监视是集群大小的最终瓶颈。
这个问题是 kube-proxy 设计的一个功能,期望任何 pod 都能立即路由到任何服务而无需通知。Endpoint 切片是一种方法,允许 kube-proxy 的基本设计继续存在,同时大大减少在使用大型服务的大集群中的监视瓶颈。
Endpoint 切片与 Endpoints 对象有类似的内容,但也包括一个 endpoints 数组:
apiVersion: discovery.k8s.io/v1beta1
kind: EndpointSlice
metadata:
name: demo-slice-1
labels:
kubernetes.io/service-name: demo
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 80
endpoints:
- addresses:
- "10.0.0.1"
conditions:
ready: true
endpoints 和 endpoint 切片之间的显著区别不在于架构,而在于 Kubernetes 如何处理它们。对于“常规” endpoints,Kubernetes 服务为服务中的所有 pod 创建一个 endpoint。一个服务创建 多个 endpoint 切片,每个切片包含一部分 pod;图 5-2 描述了这个子集。服务的所有 endpoint 切片的并集包含服务中的所有 pod。这样,IP 地址的变化(由于新的 pod、已删除的 pod 或 pod 的健康状态变化)将导致向观察者传输的数据量大大减少。由于 Kubernetes 没有事务 API,同一个地址可能会暂时出现在多个切片中。任何消费 endpoint 切片的代码(如 kube-proxy)必须能够处理这种情况。
使用 --max-endpoints-per-slice kube-controller-manager 标志设置 endpoint 切片中地址的最大数目。当前默认值为 100,最大值为 1000。Endpoint 切片控制器在创建新的切片之前会尝试填充现有的 endpoint 切片,但不会重新平衡 endpoint 切片。
端点片段控制器将端点镜像到端点片段,以允许系统在将端点视为事实来源的同时继续编写端点。这种行为的确切未来以及端点总体的未来尚未最终确定(但作为 v1 资源,端点将在大幅通知后被废弃)。有四个例外情况会阻止镜像:
-
没有对应的服务。
-
对应的服务资源选择 Pod。
-
Endpoints对象具有标签endpointslice.kubernetes.io/skip-mirror: true。 -
Endpoints对象具有注释control-plane.alpha.kubernetes.io/leader。

图 5-2. Endpoints与EndpointSlice对象
您可以通过获取以.metadata.labels."kubernetes.io/service-name"中的所需名称为过滤条件的端点片段来获取特定服务的所有端点片段。
警告
自 Kubernetes 1.17 以来,端点片段一直处于 Beta 状态。在撰写本文时,Kubernetes 1.20 仍然如此。Beta 资源通常不会有重大更改,并最终升级为稳定的 API,但这并不保证。如果直接使用端点片段,请注意未来的 Kubernetes 版本可能会进行重大更改而没有提前警告,或者这里描述的行为可能会发生变化。
在集群中运行的一些端点可以通过kubectl get endpointslice来查看:
kubectl get endpointslice
NAME ADDRESSTYPE PORTS ENDPOINTS
clusterip-service-l2n9q IPv4 8080 10.244.2.7,10.244.2.8,10.244.1.5
+ 1 more...
如果我们想要关于端点片段clusterip-service-l2n9q的更多细节,可以使用kubectl describe来查看它:
kubectl describe endpointslice clusterip-service-l2n9q
Name: clusterip-service-l2n9q
Namespace: default
Labels:
endpointslice.kubernetes.io/managed-by=endpointslice-controller.k8s.io
kubernetes.io/service-name=clusterip-service
Annotations: endpoints.kubernetes.io/last-change-trigger-time:
2021-01-30T18:51:36Z
AddressType: IPv4
Ports:
Name Port Protocol
---- ---- --------
<unset> 8080 TCP
Endpoints:
- Addresses: 10.244.2.7
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/app-5586fc9d77-qpxwk
Topology: kubernetes.io/hostname=kind-worker
- Addresses: 10.244.2.8
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/app-5586fc9d77-tpz8q
Topology: kubernetes.io/hostname=kind-worker
- Addresses: 10.244.1.5
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/app-5586fc9d77-7frts
Topology: kubernetes.io/hostname=kind-worker2
- Addresses: 10.244.3.9
Conditions:
Ready: true
Hostname: <unset>
TargetRef: Pod/app-5586fc9d77-mxhgw
Topology: kubernetes.io/hostname=kind-worker3
Events: <none>
在输出中,我们可以看到通过TargetRef支持端点片段的 Pod。Topology信息为我们提供了 Pod 部署到的工作节点的主机名。最重要的是,Addresses返回端点对象的 IP 地址。
理解端点和端点片段很重要,因为它们标识了负责服务的 Pod,无论部署的类型如何。在本章后面,我们将详细讨论如何使用端点和标签进行故障排除。接下来,我们将调查所有的 Kubernetes 服务类型。
Kubernetes 服务
Kubernetes 中的服务是集群内的负载均衡抽象。由.spec.Type字段指定四种类型的服务。每种类型提供不同形式的负载均衡或发现,我们将分别介绍这四种类型。这四种类型是:ClusterIP、NodePort、LoadBalancer 和 ExternalName。
服务使用标准的 Pod 选择器来匹配 Pod。服务包含所有匹配的 Pod。服务创建一个端点(或端点片段)来处理 Pod 的发现:
apiVersion: v1
kind: Service
metadata:
name: demo-service
spec:
selector:
app: demo
对于所有的服务示例,我们将使用 Golang 最小化 Web 服务器。我们已经为应用程序添加了功能,在 REST 请求中显示主机和 Pod IP 地址。
Figure 5-3 概述了我们作为集群中单个 Pod 的网络状态。我们即将探讨的网络对象将在某些情况下将我们的应用 Pod 暴露到集群外部,在其他情况下,允许我们扩展应用程序以满足需求。回顾第 3 和第四章节中的内容,运行在 Pod 内部的容器共享网络命名空间。此外,每个 Pod 都会创建一个暂停容器来管理命名空间。
注意
暂停容器是 Pod 内所有运行容器的父容器。它持有并共享 Pod 的所有命名空间。您可以在 Ian Lewis 的博文中详细了解有关暂停容器的信息。

第 5-3 图。主机上的 Pod
在部署服务之前,我们必须首先部署 Web 服务器,这些服务将路由流量到它,如果我们尚未部署:
kubectl apply -f web.yaml
deployment.apps/app created
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
app-9cc7d9df8-ffsm6 1/1 Running 0 49s 10.244.1.4 kind-worker2
dnsutils 1/1 Running 0 49m 10.244.3.2 kind-worker3
postgres-0 1/1 Running 0 48m 10.244.1.3 kind-worker2
postgres-1 1/1 Running 0 48m 10.244.2.3 kind-worker
让我们从 NodePort 开始看每种类型的服务。
NodePort
NodePort 服务为外部软件(如负载均衡器)提供了一种简单的方式来将流量路由到 Pod。该软件只需知道节点 IP 地址和服务的端口。NodePort 服务在所有节点上公开一个固定端口,该端口将流量路由到适用的 Pod。NodePort 服务使用.spec.ports.[].nodePort字段指定要在所有节点上打开的端口,用于对应 Pod 上的端口:
apiVersion: v1
kind: Service
metadata:
name: demo-service
spec:
type: NodePort
selector:
app: demo
ports:
- port: 80
targetPort: 80
nodePort: 30000
如果 nodePort 字段为空,则 Kubernetes 会自动选择一个唯一的端口。kube-controller-manager 中的 --service-node-port-range 标志设置端口的有效范围为 30000–32767。手动指定的端口必须在此范围内。
使用 NodePort 服务,外部用户可以连接到任何节点上的 nodeport,并被路由到托管该服务的 Pod 的节点;Figure 5-4 展示了这一点。服务将流量定向到节点 3,iptables 规则将流量转发到托管 Pod 的节点 2。这有点低效,因为典型的连接将被路由到另一个节点上的 Pod。

第 5-4 图。NodePort 流量流向
图 5-4 需要我们讨论服务的一个属性,即 externalTrafficPolicy。ExternalTrafficPolicy 表示服务将如何将外部流量路由到节点本地或集群范围的端点。Local 会保留客户端源 IP,避免 LoadBalancer 和 NodePort 类型服务的第二跳,但可能导致流量分布不均衡。Cluster 会隐藏客户端源 IP,并可能导致第二跳到另一个节点,但应具有良好的整体负载平衡。Cluster 值意味着对于每个工作节点,kube-proxy iptable 规则都设置为将流量路由到集群中任何位置支持服务的 pod,就像我们在 图 5-4 中展示的那样。
Local 值意味着 kube-proxy iptable 规则仅在运行相关 pod 的工作节点上设置,以将流量路由到工作节点本地。使用 Local 还允许应用开发人员保留用户请求的源 IP。如果将 externalTrafficPolicy 设置为值 Local,kube-proxy 将仅代理请求到节点本地端点,并且不会将流量转发到其他节点。如果没有本地端点,则发送到节点的数据包将被丢弃。
让我们扩展我们的 web 应用程序的部署以进行更多测试:
kubectl scale deployment app --replicas 4
deployment.apps/app scaled
kubectl get pods -l app=app -o wide
NAME READY STATUS IP NODE
app-9cc7d9df8-9d5t8 1/1 Running 10.244.2.4 kind-worker
app-9cc7d9df8-ffsm6 1/1 Running 10.244.1.4 kind-worker2
app-9cc7d9df8-srxk5 1/1 Running 10.244.3.4 kind-worker3
app-9cc7d9df8-zrnvb 1/1 Running 10.244.3.5 kind-worker3
当有四个 pod 运行时,集群中每个节点将有一个 pod:
kubectl get pods -o wide -l app=app
NAME READY STATUS IP NODE
app-5586fc9d77-7frts 1/1 Running 10.244.1.5 kind-worker2
app-5586fc9d77-mxhgw 1/1 Running 10.244.3.9 kind-worker3
app-5586fc9d77-qpxwk 1/1 Running 10.244.2.7 kind-worker
app-5586fc9d77-tpz8q 1/1 Running 10.244.2.8 kind-worker
现在让我们部署我们的 NodePort 服务:
kubectl apply -f services-nodeport.yaml
service/nodeport-service created
kubectl describe svc nodeport-service
Name: nodeport-service
Namespace: default
Labels: <none>
Annotations: Selector: app=app
Type: NodePort
IP: 10.101.85.57
Port: echo 8080/TCP
TargetPort: 8080/TCP
NodePort: echo 30040/TCP
Endpoints: 10.244.1.5:8080,10.244.2.7:8080,10.244.2.8:8080
+ 1 more...
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
要测试 NodePort 服务,我们必须获取一个工作节点的 IP 地址:
kubectl get nodes -o wide
NAME STATUS ROLES INTERNAL-IP OS-IMAGE
kind-control-plane Ready master 172.18.0.5 Ubuntu 19.10
kind-worker Ready <none> 172.18.0.3 Ubuntu 19.10
kind-worker2 Ready <none> 172.18.0.4 Ubuntu 19.10
kind-worker3 Ready <none> 172.18.0.2 Ubuntu 19.10
集群外部的通信将在每个工作节点和节点工作的 IP 地址上开放一个值为 NodePort 的端口 30040。
我们可以看到我们的 pod 在集群中每个主机上都是可访问的:
kubectl exec -it dnsutils -- wget -q -O- 172.18.0.5:30040/host
NODE: kind-worker2, POD IP:10.244.1.5
kubectl exec -it dnsutils -- wget -q -O- 172.18.0.3:30040/host
NODE: kind-worker, POD IP:10.244.2.8
kubectl exec -it dnsutils -- wget -q -O- 172.18.0.4:30040/host
NODE: kind-worker2, POD IP:10.244.1.5
同样重要的是考虑其限制。如果 NodePort 部署无法分配所请求的端口,则会失败。此外,端口必须在使用 NodePort 服务的所有应用程序之间进行跟踪。手动选择端口可能会引发端口冲突的问题(特别是在将工作负载应用于多个可能没有相同空闲 NodePorts 的集群时)。
使用 NodePort 服务类型的另一个缺点是负载均衡器或客户端软件必须知道节点 IP 地址。静态配置(例如运营人员手动复制节点 IP 地址)可能会随时间变化而过时(特别是在云提供商上),因为 IP 地址会发生变化或节点被替换。可靠的系统会自动填充节点 IP 地址,可以通过观察已分配给集群的机器或从 Kubernetes API 中列出节点来实现。
NodePort 是服务的最早形式。我们将看到其他服务类型在其架构中使用 NodePort 作为基础结构。不应仅使用 NodePort,因为客户端需要知道主机和节点的 IP 地址以进行连接请求。我们将在本章后面讨论云网络时看到如何使用 NodePort 来启用负载均衡器。
接下来是服务的默认类型,ClusterIP。
ClusterIP
pod 的 IP 地址与 pod 的生命周期共享,因此不适合客户端用于请求。服务有助于克服这种 pod 网络设计。ClusterIP 服务提供了一个内部负载均衡器,具有一个单一的 IP 地址,映射到所有匹配的(并且就绪的)pod。
服务的 IP 地址必须在 API 服务器中 service-cluster-ip-range 中设置的 CIDR 范围内。您可以手动指定一个有效的 IP 地址,或者将 .spec.clusterIP 置空以自动分配一个。ClusterIP 服务地址是一个虚拟 IP 地址,仅在内部可路由。
kube-proxy 负责将 ClusterIP 服务地址路由到所有适用的 pod。在“正常”配置中,kube-proxy 执行 L4 负载平衡,可能不足以满足需求。例如,旧的 pod 可能由于客户端积累了更多的长连接而看到更多的负载。或者,少数客户端的大量请求可能导致负载不均匀分布。
ClusterIP 的一个特定用例示例是当工作负载需要同一集群内的负载均衡器时。
在 图 5-5 中,我们可以看到部署了一个 ClusterIP 服务。服务名称为 App,具有选择器,或者 App=App1。有两个支持该服务的 pod。Pod 1 和 Pod 5 匹配服务的选择器。

图 5-5. Cluster IP 示例服务
让我们深入到使用我们的 KIND 集群的命令行中的示例中。
我们将为我们的 Golang web 服务器部署一个 ClusterIP 服务以供使用:
kubectl apply -f service-clusterip.yaml
service/clusterip-service created
kubectl describe svc clusterip-service
Name: clusterip-service
Namespace: default
Labels: app=app
Annotations: Selector: app=app
Type: ClusterIP
IP: 10.98.252.195
Port: <unset> 80/TCP
TargetPort: 8080/TCP
Endpoints: <none>
Session Affinity: None
Events: <none>
ClusterIP 服务名称在网络中是可解析的:
kubectl exec dnsutils -- host clusterip-service
clusterip-service.default.svc.cluster.local has address 10.98.252.195
现在我们可以使用 Cluster IP 地址 10.98.252.195 和服务名称 clusterip-service 或直接使用 pod IP 地址 10.244.1.4 和端口 8080 来访问主机 API 端点:
kubectl exec dnsutils -- wget -q -O- clusterip-service/host
NODE: kind-worker2, POD IP:10.244.1.4
kubectl exec dnsutils -- wget -q -O- 10.98.252.195/host
NODE: kind-worker2, POD IP:10.244.1.4
kubectl exec dnsutils -- wget -q -O- 10.244.1.4:8080/host
NODE: kind-worker2, POD IP:10.244.1.4
ClusterIP 服务是服务的默认类型。在默认状态下,我们应该探索 ClusterIP 服务为我们抽象出了什么。如果您回忆起第二章和第三章,此列表类似于在 Docker 网络中设置的内容,但现在我们还有 iptables 用于所有节点的服务:
-
查看 VETH 对并与 pod 匹配。
-
查看网络命名空间并与 pod 匹配。
-
验证节点上的 PID 并与 pod 匹配。
-
将服务与
iptables规则匹配。
为了探索这一点,我们需要知道 pod 部署到哪个工作节点,即 kind-worker2:
kubectl get pods -o wide --field-selector spec.nodeName=kind-worker2 -l app=app
NAME READY STATUS RESTARTS AGE IP NODE
app-9cc7d9df8-ffsm6 1/1 Running 0 7m23s 10.244.1.4 kind-worker2
注意
您的容器 ID 和名称将不同。
因为我们正在使用 KIND,我们可以使用 docker ps 和 docker exec 从运行的工作节点 kind-worker-2 中获取信息:
docker ps
CONTAINER ID COMMAND PORTS NAMES
df6df0736958 "/usr/local/bin/entr…" kind-worker2
e242f11d2d00 "/usr/local/bin/entr…" kind-worker
a76b32f37c0e "/usr/local/bin/entr…" kind-worker3
07ccb63d870f "/usr/local/bin/entr…" 0.0.0.0:80->80/tcp, kind-control-plane
0.0.0.0:443->443/tcp,
127.0.0.1:52321->6443/tcp
kind-worker2 容器 ID 是 df6df0736958;KIND 很“kind”地用名称标记了每个容器,所以我们可以通过其名称 kind-worker2 引用每个工作节点:
让我们查看我们的 pod app-9cc7d9df8-ffsm6 的 IP 地址和路由表信息:
kubectl exec app-9cc7d9df8-ffsm6 ip r
default via 10.244.1.1 dev eth0
10.244.1.0/24 via 10.244.1.1 dev eth0 src 10.244.1.4
10.244.1.1 dev eth0 scope link src 10.244.1.4
我们 pod 的 IP 地址是10.244.1.4,运行在eth0@if5接口上,其默认路由为10.244.1.1。这与 pod veth45d1f3e8@if5上的接口 5 匹配:
kubectl exec app-9cc7d9df8-ffsm6 ip a
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
inet6 ::1/128 scope host
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 ::
5: eth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
group default
link/ether 3e:57:42:6e:cd:45 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.244.1.4/24 brd 10.244.1.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::3c57:42ff:fe6e:cd45/64 scope link
valid_lft forever preferred_lft forever
让我们也从node ip a输出中检查网络命名空间:
docker exec -it kind-worker2 ip a
<trimmerd>
5: veth45d1f3e8@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
state UP group default
link/ether 3e:39:16:38:3f:23 brd <>
link-netns cni-ec37f6e4-a1b5-9bc9-b324-59d612edb4d4
inet 10.244.1.1/32 brd 10.244.1.1 scope global veth45d1f3e8
valid_lft forever preferred_lft forever
netns list确认网络命名空间与我们的 pod 匹配,接口与主机接口,cni-ec37f6e4-a1b5-9bc9-b324-59d612edb4d4:
docker exec -it kind-worker2 /usr/sbin/ip netns list
cni-ec37f6e4-a1b5-9bc9-b324-59d612edb4d4 (id: 2)
cni-c18c44cb-6c3e-c48d-b783-e7850d40e01c (id: 1)
让我们看看那个网络命名空间内运行的进程。为此,我们将使用docker exec在托管该 pod 及其网络命名空间的节点kind-worker2上运行命令:
docker exec -it kind-worker2 /usr/sbin/ip netns pid
cni-ec37f6e4-a1b5-9bc9-b324-59d612edb4d4
4687
4737
现在我们可以grep每个进程 ID 并检查它们在做什么:
docker exec -it kind-worker2 ps aux | grep 4687
root 4687 0.0 0.0 968 4 ? Ss 17:00 0:00 /pause
docker exec -it kind-worker2 ps aux | grep 4737
root 4737 0.0 0.0 708376 6368 ? Ssl 17:00 0:00 /opt/web-server
4737是运行在kind-worker2上的我们的 Web 服务器容器的进程 ID。
4687是我们的暂停容器,持有所有我们的命名空间。
现在让我们看看工作节点上的iptables会发生什么变化:
docker exec -it kind-worker2 iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
/* kubernetes service portals */
KUBE-SERVICES all -- anywhere anywhere ctstate NEW
/* kubernetes externally-visible service portals */
KUBE-EXTERNAL-SERVICES all -- anywhere anywhere ctstate NEW
KUBE-FIREWALL all -- anywhere anywhere
Chain FORWARD (policy ACCEPT)
target prot opt source destination
/* kubernetes forwarding rules */
KUBE-FORWARD all -- anywhere anywhere
/* kubernetes service portals */
KUBE-SERVICES all -- anywhere anywhere ctstate NEW
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
/* kubernetes service portals */
KUBE-SERVICES all -- anywhere anywhere ctstate NEW
KUBE-FIREWALL all -- anywhere anywhere
Chain KUBE-EXTERNAL-SERVICES (1 references)
target prot opt source destination
Chain KUBE-FIREWALL (2 references)
target prot opt source destination
/* kubernetes firewall for dropping marked packets */
DROP all -- anywhere anywhere mark match 0x8000/0x8000
Chain KUBE-FORWARD (1 references)
target prot opt source destination
DROP all -- anywhere anywhere ctstate INVALID
/*kubernetes forwarding rules*/
ACCEPT all -- anywhere anywhere mark match 0x4000/0x4000
/*kubernetes forwarding conntrack pod source rule*/
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
/*kubernetes forwarding conntrack pod destination rule*/
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
Chain KUBE-KUBELET-CANARY (0 references)
target prot opt source destination
Chain KUBE-PROXY-CANARY (0 references)
target prot opt source destination
Chain KUBE-SERVICES (3 references)
target prot opt source destination
Kubernetes 正在管理大量的表格。
我们可以深入了解我们部署的服务所负责的iptables。让我们检索已部署的clusterip-service的 IP 地址。我们需要这个来找到匹配的iptables规则:
kubectl get svc clusterip-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
clusterip-service ClusterIP 10.98.252.195 <none> 80/TCP 57m
现在使用服务的clusterIP,10.98.252.195,来查找我们的iptables规则:
docker exec -it kind-worker2 iptables -L -t nat | grep 10.98.252.195
/* default/clusterip-service: cluster IP */
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.98.252.195 tcp dpt:80
/* default/clusterip-service: cluster IP */
KUBE-SVC-V7R3EVKW3DT43QQM tcp -- anywhere 10.98.252.195 tcp dpt:80
列出链KUBE-SVC-V7R3EVKW3DT43QQM上的所有规则:
docker exec -it kind-worker2 iptables -t nat -L KUBE-SVC-V7R3EVKW3DT43QQM
Chain KUBE-SVC-V7R3EVKW3DT43QQM (1 references)
target prot opt source destination
/* default/clusterip-service: */
KUBE-SEP-THJR2P3Q4C2QAEPT all -- anywhere anywhere
KUBE-SEP-将包含服务的端点,KUBE-SEP-THJR2P3Q4C2QAEPT。
现在我们可以看到iptables中这个链的规则是什么:
docker exec -it kind-worker2 iptables -L KUBE-SEP-THJR2P3Q4C2QAEPT -t nat
Chain KUBE-SEP-THJR2P3Q4C2QAEPT (1 references)
target prot opt source destination
/* default/clusterip-service: */
KUBE-MARK-MASQ all -- 10.244.1.4 anywhere
/* default/clusterip-service: */
DNAT tcp -- anywhere anywhere tcp to:10.244.1.4:8080
10.244.1.4:8080是服务端点之一,也就是支持服务的 pod,这与kubectl get ep clusterip-service的输出确认一致:
kubectl get ep clusterip-service
NAME ENDPOINTS AGE
clusterip-service 10.244.1.4:8080 62m
kubectl describe ep clusterip-service
Name: clusterip-service
Namespace: default
Labels: app=app
Annotations: <none>
Subsets:
Addresses: 10.244.1.4
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 8080 TCP
Events: <none>
现在,让我们探索 ClusterIP 服务的限制。ClusterIP 服务用于集群内部流量,并且面临与端点相同的问题。随着服务规模的增长,对其的更新将变慢。在第二章中,我们讨论了通过将 IPVS 作为kube-proxy的代理模式来减轻这一问题。我们将在本章后面讨论如何通过 Ingress 和其他服务类型 LoadBalancer 将流量引入集群。
ClusterIP 是默认的服务类型,但还有几种其他特定类型的服务,如无头服务和 ExternalName。ExternalName 是一种帮助访问集群外服务的特定类型服务。我们简要介绍了 StatefulSets 中的无头服务,现在让我们深入了解这些服务。
无头
无头服务不是服务的正式类型(即没有.spec.type: Headless)。无头服务是一种带有.spec.clusterIP: "None"的服务。这与仅不设置集群 IP 地址是不同的,后者使 Kubernetes 自动分配集群 IP 地址。
当 ClusterIP 设置为 None 时,该服务不支持任何负载平衡功能。相反,它只会为所选和就绪的所有 pod 分配一个Endpoints对象,并将服务 DNS 记录指向它们。
无头服务提供了一种通用的方式来监视端点,无需与 Kubernetes API 交互。获取 DNS 记录比与 Kubernetes API 集成简单得多,对于第三方软件可能无法实现。
无头服务允许开发人员在部署中部署多个 Pod 的副本。与 ClusterIP 服务返回单个 IP 地址不同,查询返回所有端点的 IP 地址。然后由客户端选择使用哪个。为了看到这一点,请扩展我们 Web 应用程序的部署:
kubectl scale deployment app --replicas 4
deployment.apps/app scaled
kubectl get pods -l app=app -o wide
NAME READY STATUS IP NODE
app-9cc7d9df8-9d5t8 1/1 Running 10.244.2.4 kind-worker
app-9cc7d9df8-ffsm6 1/1 Running 10.244.1.4 kind-worker2
app-9cc7d9df8-srxk5 1/1 Running 10.244.3.4 kind-worker3
app-9cc7d9df8-zrnvb 1/1 Running 10.244.3.5 kind-worker3
现在让我们部署无头服务:
kubectl apply -f service-headless.yml
service/headless-service created
DNS 查询将返回所有四个 Pod IP 地址。使用我们的dnsutils镜像,我们可以验证这一点:
kubectl exec dnsutils -- host -v -t a headless-service
Trying "headless-service.default.svc.cluster.local"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45294
;; flags: qr aa rd; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;headless-service.default.svc.cluster.local. IN A
;; ANSWER SECTION:
headless-service.default.svc.cluster.local. 30 IN A 10.244.2.4
headless-service.default.svc.cluster.local. 30 IN A 10.244.3.5
headless-service.default.svc.cluster.local. 30 IN A 10.244.1.4
headless-service.default.svc.cluster.local. 30 IN A 10.244.3.4
Received 292 bytes from 10.96.0.10#53 in 0 ms
查询返回的 IP 地址也与服务的端点匹配。使用kubectl describe确认了端点的情况:
kubectl describe endpoints headless-service
Name: headless-service
Namespace: default
Labels: service.kubernetes.io/headless
Annotations: endpoints.kubernetes.io/last-change-trigger-time:
2021-01-30T18:16:09Z
Subsets:
Addresses: 10.244.1.4,10.244.2.4,10.244.3.4,10.244.3.5
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 8080 TCP
Events: <none>
无头服务有一个特定的用例,通常不用于部署。正如我们在“StatefulSets”中提到的,如果开发人员需要让客户端决定使用哪个端点,则无头是部署的适当服务类型。无头服务的两个示例是集群数据库和在代码中构建了客户端负载均衡逻辑的应用程序。
我们的下一个示例是 ExternalName,它有助于迁移到集群外的服务。它还在集群 DNS 内部提供其他 DNS 优势。
ExternalName 服务
ExternalName 是一种特殊类型的服务,它没有选择器,而是使用 DNS 名称。
当查找主机ext-service.default.svc.cluster.local时,集群 DNS 服务返回了database.mycompany.com的 CNAME 记录:
apiVersion: v1
kind: Service
metadata:
name: ext-service
spec:
type: ExternalName
externalName: database.mycompany.com
如果开发人员将应用程序迁移到 Kubernetes,但其依赖项保留在集群外部,ExternalName 服务允许他们定义一个内部集群的 DNS 记录,无论服务实际运行在哪里。
DNS 将尝试如下示例中显示的搜索:
kubectl exec -it dnsutils -- host -v -t a github.com
Trying "github.com.default.svc.cluster.local"
Trying "github.com.svc.cluster.local"
Trying "github.com.cluster.local"
Trying "github.com"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 55908
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;github.com. IN A
;; ANSWER SECTION:
github.com. 30 IN A 140.82.112.3
Received 54 bytes from 10.96.0.10#53 in 18 ms
例如,ExternalName 服务允许开发人员将一个服务映射到一个 DNS 名称。
现在如果我们这样部署外部服务:
kubectl apply -f service-external.yml
service/external-service created
github.com 的 A 记录从external-service查询返回:
kubectl exec -it dnsutils -- host -v -t a external-service
Trying "external-service.default.svc.cluster.local"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11252
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;external-service.default.svc.cluster.local. IN A
;; ANSWER SECTION:
external-service.default.svc.cluster.local. 24 IN CNAME github.com.
github.com. 24 IN A 140.82.112.3
Received 152 bytes from 10.96.0.10#53 in 0 ms
外部服务的 CNAME 返回 github.com:
kubectl exec -it dnsutils -- host -v -t cname external-service
Trying "external-service.default.svc.cluster.local"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36874
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;external-service.default.svc.cluster.local. IN CNAME
;; ANSWER SECTION:
external-service.default.svc.cluster.local. 30 IN CNAME github.com.
Received 126 bytes from 10.96.0.10#53 in 0 ms
通过 DNS 记录发送流量到无头服务是可能的,但不建议这样做。DNS 作为负载均衡的方式并不理想,因为软件在处理返回多个 IP 地址的 A 或 AAAA DNS 记录时采取的方法很不同(通常是简单或不直观的方法),例如,软件通常会选择响应中的第一个 IP 地址并/或者缓存并重复使用同一个 IP 地址。如果需要能够发送流量到服务的 DNS 地址,请考虑(标准的)ClusterIP 或 LoadBalancer 服务。
使用无头服务的“正确”方法是查询服务的 A/AAAA DNS 记录,并在服务器端或客户端负载均衡器中使用该数据。
我们讨论的大多数服务都是用于集群网络的内部流量管理。在接下来的章节中,我们将会审查如何通过 LoadBalancer 和 Ingress 类型服务将请求路由到集群中。
负载均衡器
负载均衡器服务将服务暴露给集群网络之外的外部。它们结合了 NodePort 服务的行为和外部集成,如云服务提供商的负载均衡器。值得注意的是,负载均衡器服务处理 L4 流量(不像 Ingress 处理 L7 流量),因此它们适用于任何 TCP 或 UDP 服务,只要所选的负载均衡器支持 L4 流量。
配置和负载均衡选项极大地依赖于云服务提供商。例如,有些支持.spec.loadBalancerIP(需要不同的设置),而有些则会忽略它:
apiVersion: v1
kind: Service
metadata:
name: demo-service
spec:
selector:
app: demo
ports:
- protocol: TCP
port: 80
targetPort: 8080
clusterIP: 10.0.5.1
type: LoadBalancer
一旦负载均衡器被配置完成,其 IP 地址将被写入.status.loadBalancer.ingress.ip。
负载均衡器服务非常适用于将 TCP 或 UDP 服务暴露给外部。流量将通过其公共 IP 地址和 TCP 端口 80 进入负载均衡器,由spec.ports[*].port定义,并路由到集群 IP 地址10.0.5.1,然后进入容器目标端口 8080,spec.ports[*].targetPort。示例中未显示的是.spec.ports[*].nodePort;如果未指定,Kubernetes 会为服务选择一个。
提示
服务的spec.ports[*].targetPort必须与您的 Pod 容器应用程序的spec.container[*].ports.containerPort及其协议匹配。否则,在 Kubernetes 网络中会像缺少分号一样。
在图 5-6 中,我们可以看到负载均衡器类型是如何在其他服务类型基础上构建的。云负载均衡器将决定如何分发流量;我们将在下一章节深入讨论这一点。

图 5-6. 负载均衡器服务
让我们继续通过 LoadBalancer 服务扩展我们的 Golang Web 服务器示例。
因为我们是在本地机器上运行,而不是在像 AWS、GCP 或 Azure 这样的服务提供商上,所以我们可以使用 MetalLB 作为负载均衡器服务的示例。MetalLB 项目旨在允许用户为其集群部署裸金属负载均衡器。
此示例已从KIND 示例部署修改而来。
我们的第一步是为 MetalLB 部署一个单独的命名空间:
kubectl apply -f mlb-ns.yaml
namespace/metallb-system created
MetalLB 成员还需要一个用于加入负载均衡器集群的秘钥;让我们现在为他们在我们的集群中部署一个:
kubectl create secret generic -n metallb-system memberlist
--from-literal=secretkey="$(openssl rand -base64 128)"
secret/memberlist created
现在我们可以部署 MetalLB 了!
kubectl apply -f ./metallb.yaml
podsecuritypolicy.policy/controller created
podsecuritypolicy.policy/speaker created
serviceaccount/controller created
serviceaccount/speaker created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
role.rbac.authorization.k8s.io/config-watcher created
role.rbac.authorization.k8s.io/pod-lister created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/config-watcher created
rolebinding.rbac.authorization.k8s.io/pod-lister created
daemonset.apps/speaker created
deployment.apps/controller created
如您所见,它部署了许多对象,现在我们等待资源的部署完成。我们可以在metallb-system命名空间中使用--watch选项监视资源的部署:
kubectl get pods -n metallb-system --watch
NAME READY STATUS RESTARTS AGE
controller-5df88bd85d-mvgqn 0/1 ContainerCreating 0 10s
speaker-5knqb 1/1 Running 0 10s
speaker-k79c9 1/1 Running 0 10s
speaker-pfs2p 1/1 Running 0 10s
speaker-sl7fd 1/1 Running 0 10s
controller-5df88bd85d-mvgqn 1/1 Running 0 12s
要完成配置,我们需要为 MetalLB 提供一个它控制的 IP 地址范围。此范围必须在 Docker KIND 网络上:
docker network inspect -f '{{.IPAM.Config}}' kind
[{172.18.0.0/16 172.18.0.1 map[]} {fc00:f853:ccd:e793::/64 fc00:f853:ccd:e793::1 map[]}]
172.18.0.0/16是我们在本地运行的 Docker 网络。
我们希望我们的 LoadBalancer IP 范围来自这个子类。例如,我们可以通过创建 ConfigMap 配置 MetalLB,使用 172.18.255.200 到 172.18.255.250:
ConfigMap 将如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 172.18.255.200-172.18.255.250
让我们部署它,这样我们就可以使用 MetalLB:
kubectl apply -f ./metallb-configmap.yaml
现在我们为我们的 web 应用程序部署一个负载均衡器:
kubectl apply -f services-loadbalancer.yaml
service/loadbalancer-service created
为了好玩,让我们将 web 应用程序部署扩展到 10 个,如果你有资源的话:
kubectl scale deployment app --replicas 10
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
app-7bdb9ffd6c-b5x7m 2/2 Running 0 26s 10.244.3.15 kind-worker
app-7bdb9ffd6c-bqtf8 2/2 Running 0 26s 10.244.2.13 kind-worker2
app-7bdb9ffd6c-fb9sf 2/2 Running 0 26s 10.244.3.14 kind-worker
app-7bdb9ffd6c-hrt7b 2/2 Running 0 26s 10.244.2.7 kind-worker2
app-7bdb9ffd6c-l2794 2/2 Running 0 26s 10.244.2.9 kind-worker2
app-7bdb9ffd6c-l4cfx 2/2 Running 0 26s 10.244.3.11 kind-worker2
app-7bdb9ffd6c-rr4kn 2/2 Running 0 23m 10.244.3.10 kind-worker
app-7bdb9ffd6c-s4k92 2/2 Running 0 26s 10.244.3.13 kind-worker
app-7bdb9ffd6c-shmdt 2/2 Running 0 26s 10.244.1.12 kind-worker3
app-7bdb9ffd6c-v87f9 2/2 Running 0 26s 10.244.1.11 kind-worker3
app2-658bcd97bd-4n888 1/1 Running 0 35m 10.244.2.6 kind-worker3
app2-658bcd97bd-mnpkp 1/1 Running 0 35m 10.244.3.7 kind-worker
app2-658bcd97bd-w2qkl 1/1 Running 0 35m 10.244.3.8 kind-worker
dnsutils 1/1 Running 1 75m 10.244.1.2 kind-worker3
postgres-0 1/1 Running 0 75m 10.244.1.4 kind-worker3
postgres-1 1/1 Running 0 75m 10.244.3.4 kind-worker
现在我们可以测试配置的负载均衡器。
随着我们应用程序在负载均衡器后面部署更多副本,我们需要负载均衡器的外部 IP,172.18.255.200:
kubectl get svc loadbalancer-service
NAME TYPE CLUSTER-IP EXTERNAL-IP
PORT(S) AGE
loadbalancer-service LoadBalancer 10.99.24.220 172.18.255.200
80:31276/TCP 52s
kubectl get svc/loadbalancer-service -o=jsonpath='{.status.loadBalancer.ingress[0].ip}'
172.18.255.200
因为 Docker for Mac 或 Windows 不会将 KIND 网络暴露给主机,所以无法直接访问 Docker 私有网络上的 172.18.255.200 LoadBalancer IP。
我们可以通过将 Docker 容器附加到 KIND 网络并模拟 cURL 负载均衡器来模拟它。
提示
如果你想进一步了解这个问题,这里有一篇很棒的 博客文章。
我们将使用另一个称为 nicolaka/netshoot 的出色的网络 Docker 镜像在本地运行,附加到 KIND Docker 网络,并发送请求到我们的 MetalLB 负载均衡器。
如果我们运行多次,我们可以看到负载均衡器正在有效地将流量路由到不同的 pod:
docker run --network kind -a stdin -a stdout -i -t nicolaka/netshoot
curl 172.18.255.200/host
NODE: kind-worker, POD IP:10.244.2.7
docker run --network kind -a stdin -a stdout -i -t nicolaka/netshoot
curl 172.18.255.200/host
NODE: kind-worker, POD IP:10.244.2.9
docker run --network kind -a stdin -a stdout -i -t nicolaka/netshoot
curl 172.18.255.200/host
NODE: kind-worker3, POD IP:10.244.3.11
docker run --network kind -a stdin -a stdout -i -t nicolaka/netshoot
curl 172.18.255.200/host
NODE: kind-worker2, POD IP:10.244.1.6
docker run --network kind -a stdin -a stdout -i -t nicolaka/netshoot
curl 172.18.255.200/host
NODE: kind-worker, POD IP:10.244.2.9
每次新请求时,metalLB 服务都会将请求发送到不同的 pod。LoadBalancer 与其他服务一样,使用选择器和标签来选择 pod,我们可以在 kubectl describe endpoints loadbalancer-service 中看到这一点。Pod IP 地址与我们从 cURL 命令的结果匹配:
kubectl describe endpoints loadbalancer-service
Name: loadbalancer-service
Namespace: default
Labels: app=app
Annotations: endpoints.kubernetes.io/last-change-trigger-time:
2021-01-30T19:59:57Z
Subsets:
Addresses:
10.244.1.6,
10.244.1.7,
10.244.1.8,
10.244.2.10,
10.244.2.7,
10.244.2.8,
10.244.2.9,
10.244.3.11,
10.244.3.12,
10.244.3.9
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
service-port 8080 TCP
Events: <none>
重要的是要记住,LoadBalancer 服务需要特定的集成,并且如果没有云提供商支持或手动安装软件如 MetalLB,将无法工作。
它们通常不是 L7 负载均衡器,因此无法智能处理 HTTP(S) 请求。负载均衡器与工作负载是一对一映射,这意味着发送到该负载均衡器的所有请求必须由相同的工作负载处理。
提示
虽然它不是网络服务,但重要的是提到 Horizontal Pod Autoscaler 服务,它将根据 CPU 利用率扩展副本控制器、部署、ReplicaSet 或 StatefulSet 中的 pod。
我们可以根据用户的需求扩展我们的应用程序,而无需任何人员的配置更改。Kubernetes 和 LoadBalancer 服务会为开发人员、系统和网络管理员处理所有这些。
我们将在下一章节看到如何通过云服务进一步扩展这一点。
服务总结
如果出现端点或服务问题,这里有一些故障排除提示:
-
删除 pod 上的标签允许其继续运行,并更新端点和服务。端点控制器将从端点对象中删除未标记的 pod,并重新部署另一个 pod;这将允许您调试特定未标记的 pod 的问题,但不会对最终客户的服务产生不利影响。在开发过程中,我经常使用这个方法,我们在上一节的示例中也这样做了。
-
有两个探测器将 pod 的健康状态传达给 Kubelet 和 Kubernetes 环境的其余部分。
-
YAML 配置很容易弄乱,因此请确保比较服务和 pod 上的端口,并确保它们匹配。
-
我们在第三章讨论了网络策略,这也可以阻止 pod 之间及服务之间的通信。如果您的集群网络正在使用网络策略,请确保为应用程序流量正确设置它们。
-
还要记得使用诊断工具,如
dnsutilspod;集群网络上的netshootpod 是有用的调试工具。 -
如果端点在集群中启动时间过长,可以在 Kubelet 上配置几个选项来控制其对 Kubernetes 环境中变化的响应速度:
--kube-api-qps设置 Kubelet 在与 Kubernetes API 服务器通信时使用的每秒查询速率;默认为 5。
--kube-api-burst临时允许 API 查询突发到这个数字;默认值为 10。
--iptables-sync-period这是刷新
iptables规则的最大间隔时间(例如 5 秒,1 分钟,2 小时 22 分钟)。必须大于 0;默认为 30 秒。--ipvs-sync-period duration这是刷新 IPVS 规则的最大间隔时间。必须大于 0;默认为 30 秒。
-
建议针对较大的集群增加这些选项,但也请记住,这会增加 Kubelet 和 API 服务器的资源使用量,所以请注意。
这些提示可以帮助减轻问题,并且随着集群中服务和 pod 的数量增长,了解这些是很好的。
各种类型的服务展示了 Kubernetes 中网络抽象的强大。我们深入研究了如何为工具链的每一层配置这些服务。希望要将应用程序部署到 Kubernetes 的开发人员现在具备了选择合适服务的知识。不再需要网络管理员手动更新负载均衡器的 IP 地址,Kubernetes 会为他们管理这些。
我们刚刚触及了服务可能性的表面。随着 Kubernetes 的每个新版本,都有调整选项和运行服务的配置。为您的用例测试每个服务,并确保您使用适当的服务来优化 Kubernetes 网络上的应用程序。
LoadBalancer 服务类型是唯一允许流量进入集群的类型,通过负载均衡器公开 HTTP(S) 服务,供外部用户连接使用。Ingress 支持基于路径的路由,允许不同的 HTTP 路径由不同的服务提供。接下来的部分将讨论 Ingress 及其作为管理集群资源连接的替代方法。
Ingress
Ingress 是 Kubernetes 特有的 L7(HTTP)负载均衡器,可以从外部访问,与集群内部的 L4 ClusterIP 服务形成对比。这通常是暴露 HTTP(S) 工作负载给外部用户的典型选择。Ingress 可以是 API 或基于微服务架构的单一入口点。流量可以根据请求中的 HTTP 信息路由到服务。Ingress 是一个配置规范(具有多种实现),用于将 HTTP 流量路由到 Kubernetes 服务。图 5-7 概述了 Ingress 的组件。

图 5-7. Ingress 架构
使用 Ingress 管理集群中的流量需要两个必要组件:控制器和规则。控制器管理 Ingress pods,而部署的规则定义了流量的路由方式。
Ingress 控制器和规则
我们称 Ingress 实现为 Ingress 控制器。在 Kubernetes 中,控制器是负责管理典型资源类型并使实际状态与期望状态匹配的软件。
有两种常见类型的控制器:外部负载均衡器控制器和内部负载均衡器控制器。外部负载均衡器控制器创建一个存在于集群“外部”的负载均衡器,如云提供商的产品。内部负载均衡器控制器部署一个运行在集群内部的负载均衡器,并不直接解决将消费者路由到负载均衡器的问题。集群管理员运行内部负载均衡器的方式有多种,例如在特定节点子集上运行负载均衡器,并以某种方式将流量路由到这些节点。选择内部负载均衡器的主要动机是降低成本。Ingress 的内部负载均衡器可以为多个 Ingress 对象路由流量,而外部负载均衡器控制器通常每个 Ingress 需要一个负载均衡器。由于大多数云提供商按负载均衡器收费,支持集群内部的单个云负载均衡器比多个云负载均衡器更便宜。但要注意,这会增加操作开销、延迟和计算成本,所以务必确保节省的资金是值得的。许多公司有优化无关紧要的云支出项目的不良习惯。
让我们看一下 Ingress 控制器的规范。与 LoadBalancer 服务一样,大多数规范是通用的,但不同的 Ingress 控制器具有不同的功能并接受不同的配置。我们将从基础知识开始:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: basic-ingress
spec:
rules:
- http:
paths:
# Send all /demo requests to demo-service.
- path: /demo
pathType: Prefix
backend:
service:
name: demo-service
port:
number: 80
# Send all other requests to main-service.
defaultBackend:
service:
name: main-service
port:
number: 80
前面的例子代表了一个典型的 Ingress。它将流量发送到 /demo 的一个服务,将所有其他流量发送到另一个服务。Ingress 有一个“默认后端”,如果没有匹配的规则,请求将路由到它。这可以在许多 Ingress 控制器中的控制器配置本身中配置(例如通用的 404 页面),并且许多支持 .spec.defaultBackend 字段。Ingress 支持多种指定路径的方法。目前有三种:
精确
仅匹配特定路径及该路径(包括结尾的 / 或缺失的 /)。
前缀
匹配所有以给定路径开头的路径。
ImplementationSpecific
允许从当前 Ingress 控制器获得自定义语义。
当请求匹配多个路径时,将选择最具体的匹配。例如,如果有 /first 和 /first/second 的规则,任何以 /first/second 开头的请求将转发到 /first/second 的后端。如果路径匹配精确路径和前缀路径,请求将转发到精确规则的后端。
Ingress 还可以在规则中使用主机名:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-host-ingress
spec:
rules:
- host: a.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: service-a
port:
number: 80
- host: b.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: service-b
port:
number: 80
在这个例子中,我们从一个服务向 a.example.com 提供流量,从另一个服务向 b.example.com 提供流量。这类似于 Web 服务器中的虚拟主机。您可能希望使用主机规则来使用单个负载均衡器和 IP 来为多个唯一域名提供服务。
Ingress 具有基本的 TLS 支持:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress-secure
spec:
tls:
- hosts:
- https-example.com
secretName: demo-tls
rules:
- host: https-example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-service
port:
number: 80
TLS 配置引用一个 Kubernetes 密钥,位于 .spec.tls.[*].secretName。Ingress 控制器期望在 .data."tls.crt" 和 .data."tls.key" 中提供 TLS 证书和密钥,如下所示:
apiVersion: v1
kind: Secret
metadata:
name: demo-tls
type: kubernetes.io/tls
data:
tls.crt: cert, encoded in base64
tls.key: key, encoded in base64
提示
如果您不需要手动管理传统颁发的证书,可以使用 cert-manager 自动获取和更新证书。
我们前面提到,Ingress 只是一个规范,存在极大的不同实现。可以在单个集群中使用多个 Ingress 控制器,使用 IngressClass 设置。Ingress 类表示一个 Ingress 控制器,因此表示特定的 Ingress 实现。
警告
Kubernetes 中的注解必须是字符串。因为 true 和 false 有不同的非字符串含义,所以不能在不加引号的情况下设置注解为 true 或 false。"true" 和 "false" 都是有效的。这是一个长期存在的问题,在设置默认优先级类时经常会遇到。
IngressClass 在 Kubernetes 1.18 中引入。在 1.18 之前,使用 kubernetes.io/ingress.class 注解 Ingress 是一种常见的约定,但依赖于所有已安装的 Ingress 控制器是否支持它。Ingress 可以通过在 .spec.ingressClassName 中设置类名来选择一个 Ingress 类。
警告
如果设置了多个默认的入口类别,默认情况下,Kubernetes 将不允许您创建没有入口类别或从现有入口中移除入口类别。您可以使用准入控制来防止将多个入口类别标记为默认。
入口仅支持 HTTP(S) 请求,如果您的服务使用不同的协议(例如,大多数数据库使用其自己的协议),这是不够的。某些入口控制器,如 NGINX 入口控制器,支持 TCP 和 UDP,但这不是标准。
现在我们来部署一个入口控制器,以便我们可以向我们的 Golang Web 服务器示例添加入口规则。
当我们部署 KIND 群集时,我们必须添加几个选项,以便我们可以部署入口控制器:
-
extraPortMappings 允许本地主机通过端口 80/443 请求入口控制器。
-
Node-labels 仅允许入口控制器在匹配标签选择器的特定节点上运行。
具有入口控制器的选择有很多。与其他组件不同,Kubernetes 系统没有默认控制器或启动控制器。Kubernetes 社区支持 AWS、GCE 和 Nginx 入口控制器。Table 5-1 概述了几个入口的选项。
表 5-1. 入口控制器选项简要列表
| 名称 | 商业支持 | 引擎 | 协议支持 | SSL 终止 |
|---|---|---|---|---|
| Ambassador 入口控制器 | 是 | Envoy | gRPC, HTTP/2, WebSockets | 是 |
| 社区版入口 Nginx | 否 | NGINX | gRPC, HTTP/2, WebSockets | 是 |
| NGINX Inc. 入口 | 是 | NGINX | HTTP, Websocket, gRPC | 是 |
| HAProxy 入口 | 是 | HAProxy | gRPC, HTTP/2, WebSockets | 是 |
| Istio 入口 | 否 | Envoy | HTTP, HTTPS, gRPC, HTTP/2 | 是 |
| 用于 Kubernetes 的 Kong 入口控制器 | 是 | Lua 在 Nginx 之上 | gRPC, HTTP/2 | 是 |
| Traefik Kubernetes 入口 | 是 | Traefik | HTTP/2, gRPC 和 WebSockets | 是 |
在选择群集的入口时需要考虑一些事项:
-
协议支持:您是否需要超过 TCP/UDP 的更多支持,例如 gRPC 集成或 WebSocket?
-
商业支持:您是否需要商业支持?
-
高级功能:您的应用程序是否需要 JWT/oAuth2 认证或断路器功能?
-
API 网关功能:您是否需要一些 API 网关功能,例如速率限制?
-
流量分发:您的应用程序是否需要支持特殊的流量分发,例如金丝雀 A/B 测试或镜像?
对于我们的示例,我们选择使用 NGINX 入口控制器的社区版。
提示
要选择更多入口控制器,请访问 kubernetes.io 维护的列表。
让我们将 NGINX 入口控制器部署到我们的 KIND 群集中:
kubectl apply -f ingress.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
configmap/ingress-nginx-controller created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
service/ingress-nginx-controller-admission created
service/ingress-nginx-controller created
deployment.apps/ingress-nginx-controller created
validatingwebhookconfiguration.admissionregistration.k8s.io/
ingress-nginx-admission created
serviceaccount/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
与所有部署一样,我们必须等待控制器准备就绪,然后才能使用它。使用以下命令,我们可以验证我们的入口控制器是否准备好供使用:
kubectl wait --namespace ingress-nginx \
> --for=condition=ready pod \
> --selector=app.kubernetes.io/component=controller \
> --timeout=90s
pod/ingress-nginx-controller-76b5f89575-zps4k condition met
控制器已部署到集群中,现在我们准备为我们的应用编写入口规则。
部署入口规则
我们的 YAML 清单定义了几个入口规则,用于我们的 Golang Web 服务器示例:
kubectl apply -f ingress-rule.yaml
ingress.extensions/ingress-resource created
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-resource <none> * 80 4s
使用 describe 我们可以看到映射到 ClusterIP 服务和 pod 的所有后端:
kubectl describe ingress
Name: ingress-resource
Namespace: default
Address:
Default backend: default-http-backend:80 (<error:
endpoints "default-http-backend" not found>)
Rules:
Host Path Backends
---- ---- --------
*
/host clusterip-service:8080 (
10.244.1.6:8080,10.244.1.7:8080,10.244.1.8:8080)
Annotations: kubernetes.io/ingress.class: nginx
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 17s nginx-ingress-controller Scheduled for sync
我们的入口规则仅适用于 /host 路由,并将请求路由到我们的 clusterip-service:8080 服务。
我们可以使用 cURL 来测试 http://localhost/host:
curl localhost/host
NODE: kind-worker2, POD IP:10.244.1.6
curl localhost/healthz
现在我们可以看到入口规则有多么强大;让我们部署第二个部署和 ClusterIP 服务。
我们的新部署和服务将用于响应 /data 的请求:
kubectl apply -f ingress-example-2.yaml
deployment.apps/app2 created
service/clusterip-service-2 configured
ingress.extensions/ingress-resource-2 configured
现在 /host 和 /data 都可以工作,但将会路由到不同的服务:
curl localhost/host
NODE: kind-worker2, POD IP:10.244.1.6
curl localhost/data
Database Connected
由于入口在第 7 层工作,有许多其他选项可以用来路由流量,例如主机头和 URI 路径。
对于更高级的流量路由和发布模式,需要在集群网络中部署服务网格。让我们接下来深入探讨这一点。
服务网格
使用默认选项的新群集有一些限制。所以,让我们了解这些限制是什么,以及服务网格如何解决其中的一些限制。服务网格 是一个 API 驱动的基础设施层,用于处理服务间的通信。
从安全角度来看,集群内部所有流量在 pod 之间都是未加密的,运行服务的每个应用团队必须单独为每个服务配置监控。我们已经讨论了服务类型,但我们还没有讨论如何更新它们的 pod 部署。服务网格支持的不仅仅是基本的部署类型;它们支持滚动更新和重建,就像 Canary 一样。从开发者的角度来看,将故障注入网络是有用的,但默认的 Kubernetes 网络部署不直接支持。通过服务网格,开发者可以添加故障测试,而不仅仅是杀死 pod,还可以使用服务网格来注入延迟——同样,每个应用程序都必须构建故障测试或断路器。
在默认的 Kubernetes 集群网络中,服务网格增强或提供了几个功能:
服务发现
服务网格管理服务发现,不再依赖 DNS,消除了在每个单独应用程序中实现服务发现的需要。
负载均衡
服务网格增加了更先进的负载均衡算法,如最小请求、一致性哈希和区域感知。
通信韧性
服务网格可以通过在应用程序中不必实现重试、超时、断路或速率限制来增加应用程序的通信韧性。
安全性
服务网格可以提供以下功能:服务之间的端到端加密通过 mTLS *授权策略,授权哪些服务可以与其他服务通信,不仅限于 Kubernetes 网络策略的第 3 和第 4 层。
可观测性
服务网格通过丰富的第 7 层指标、添加跟踪和警报来增强可观测性。
路由控制
集群中的流量转移和镜像。
API
所有这些都可以通过服务网格实现提供的 API 进行控制。
让我们看一下图 5-8 中服务网格的几个组件。

图 5-8. 服务网格组件
流量根据组件或流量的目的地处理不同。进出集群的流量由网关管理。前端、后端和用户服务之间的流量都使用双向 TLS(mTLS)进行加密,并由服务网格处理。即使控制平面关闭且无法更新网格,服务和应用程序流量也不会受到影响。
在部署服务网格时有几个选项可供选择;以下是其中几个要点:
-
Istio
-
使用带有 Envoy 代理的 Go 控制平面。
-
这是一个由 Lyft 最初发布的基于 Kubernetes 的本地解决方案。
-
-
Consul
-
使用 HashiCorp Consul 作为控制平面。
-
Consul Connect 在每个节点上安装一个代理作为 DaemonSet,它与处理流量路由和转发的 Envoy Sidecar 代理通信。
-
-
AWS App Mesh
-
这是 AWS 管理的解决方案,实现了自己的控制平面。
-
没有 mTLS 或流量策略。
-
使用 Envoy 代理作为数据平面。
-
-
Linkerd
-
Linkerd 代理也使用 Go 控制平面。
-
没有流量转移和分布式跟踪。
-
是一个仅限于 Kubernetes 的解决方案,这导致移动部件较少,意味着 Linkerd 总体上复杂性较低。
-
我们认为服务网格的最佳用例是服务之间的 mTLS。开发人员的其他高级用例包括断路器和 API 的故障测试。对于网络管理员来说,可以使用服务网格部署高级路由策略和算法。
让我们看一个服务网格的例子。如果您还没有安装,请首先安装 Linkerd CLI。
如果您使用 Mac,您可以选择 cURL、bash 或 brew:
curl -sL https://run.linkerd.io/install | sh
OR
brew install linkerd
linkerd version
Client version: stable-2.9.2
Server version: unavailable
这个预检查列表将验证我们的集群是否能运行 Linkerd:
linkerd check --pre
kubernetes-api
--------------
√ can initialize the client
√ can query the Kubernetes API
kubernetes-version
------------------
√ is running the minimum Kubernetes API version
√ is running the minimum kubectl version
pre-kubernetes-setup
--------------------
√ control plane namespace does not already exist
√ can create non-namespaced resources
√ can create ServiceAccounts
√ can create Services
√ can create Deployments
√ can create CronJobs
√ can create ConfigMaps
√ can create Secrets
√ can read Secrets
√ can read extension-apiserver-authentication configmap
√ no clock skew detected
pre-kubernetes-capability
-------------------------
√ has NET_ADMIN capability
√ has NET_RAW capability
linkerd-version
---------------
√ can determine the latest version
√ cli is up-to-date
Status check results are √
Linkerd CLI 工具可以将 Linkerd 安装到我们的 KIND 集群中:
linkerd install | kubectl apply -f -
namespace/linkerd created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-identity created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-identity created
serviceaccount/linkerd-identity created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-controller created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-controller created
serviceaccount/linkerd-controller created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-destination created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-destination created
serviceaccount/linkerd-destination created
role.rbac.authorization.k8s.io/linkerd-heartbeat created
rolebinding.rbac.authorization.k8s.io/linkerd-heartbeat created
serviceaccount/linkerd-heartbeat created
role.rbac.authorization.k8s.io/linkerd-web created
rolebinding.rbac.authorization.k8s.io/linkerd-web created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-web-check created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-web-check created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-web-admin created
serviceaccount/linkerd-web created
customresourcedefinition.apiextensions.k8s.io/serviceprofiles.linkerd.io created
customresourcedefinition.apiextensions.k8s.io/trafficsplits.split.smi-spec.io
created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-proxy-injector created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-proxy-injector
created
serviceaccount/linkerd-proxy-injector created
secret/linkerd-proxy-injector-k8s-tls created
mutatingwebhookconfiguration.admissionregistration.k8s.io
/linkerd-proxy-injector-webhook-config created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-sp-validator created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-sp-validator
created
serviceaccount/linkerd-sp-validator created
secret/linkerd-sp-validator-k8s-tls created
validatingwebhookconfiguration.admissionregistration.k8s.io
/linkerd-sp-validator-webhook-config created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-tap created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-tap-admin created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-tap created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-tap-auth-delegator
created
serviceaccount/linkerd-tap created
rolebinding.rbac.authorization.k8s.io/linkerd-linkerd-tap-auth-reader created
secret/linkerd-tap-k8s-tls created
apiservice.apiregistration.k8s.io/v1alpha1.tap.linkerd.io created
podsecuritypolicy.policy/linkerd-linkerd-control-plane created
role.rbac.authorization.k8s.io/linkerd-psp created
rolebinding.rbac.authorization.k8s.io/linkerd-psp created
configmap/linkerd-config created
secret/linkerd-identity-issuer created
service/linkerd-identity created
service/linkerd-identity-headless created
deployment.apps/linkerd-identity created
service/linkerd-controller-api created
deployment.apps/linkerd-controller created
service/linkerd-dst created
service/linkerd-dst-headless created
deployment.apps/linkerd-destination created
cronjob.batch/linkerd-heartbeat created
service/linkerd-web created
deployment.apps/linkerd-web created
deployment.apps/linkerd-proxy-injector created
service/linkerd-proxy-injector created
service/linkerd-sp-validator created
deployment.apps/linkerd-sp-validator created
service/linkerd-tap created
deployment.apps/linkerd-tap created
serviceaccount/linkerd-grafana created
configmap/linkerd-grafana-config created
service/linkerd-grafana created
deployment.apps/linkerd-grafana created
clusterrole.rbac.authorization.k8s.io/linkerd-linkerd-prometheus created
clusterrolebinding.rbac.authorization.k8s.io/linkerd-linkerd-prometheus created
serviceaccount/linkerd-prometheus created
configmap/linkerd-prometheus-config created
service/linkerd-prometheus created
deployment.apps/linkerd-prometheus created
secret/linkerd-config-overrides created
与入口控制器和 MetalLB 一样,我们可以看到许多组件安装在我们的集群中。
Linkerd 可以使用linkerd check命令验证安装。
它将验证 Linkerd 安装的大量检查,包括但不限于 Kubernetes API 版本、控制器、Pod 和配置以运行 Linkerd,以及运行 Linkerd 所需的所有服务、版本和 API:
linkerd check
kubernetes-api
--------------
√ can initialize the client
√ can query the Kubernetes API
kubernetes-version
------------------
√ is running the minimum Kubernetes API version
√ is running the minimum kubectl version
linkerd-existence
-----------------
√ 'linkerd-config' config map exists
√ heartbeat ServiceAccount exists
√ control plane replica sets are ready
√ no unschedulable pods
√ controller pod is running
√ can initialize the client
√ can query the control plane API
linkerd-config
--------------
√ control plane Namespace exists
√ control plane ClusterRoles exist
√ control plane ClusterRoleBindings exist
√ control plane ServiceAccounts exist
√ control plane CustomResourceDefinitions exist
√ control plane MutatingWebhookConfigurations exist
√ control plane ValidatingWebhookConfigurations exist
√ control plane PodSecurityPolicies exist
linkerd-identity
----------------
√ certificate config is valid
√ trust anchors are using supported crypto algorithm
√ trust anchors are within their validity period
√ trust anchors are valid for at least 60 days
√ issuer cert is using supported crypto algorithm
√ issuer cert is within its validity period
√ issuer cert is valid for at least 60 days
√ issuer cert is issued by the trust anchor
linkerd-webhooks-and-apisvc-tls
-------------------------------
√ tap API server has valid cert
√ tap API server cert is valid for at least 60 days
√ proxy-injector webhook has valid cert
√ proxy-injector cert is valid for at least 60 days
√ sp-validator webhook has valid cert
√ sp-validator cert is valid for at least 60 days
linkerd-api
-----------
√ control plane pods are ready
√ control plane self-check
√ [kubernetes] control plane can talk to Kubernetes
√ [prometheus] control plane can talk to Prometheus
√ tap api service is running
linkerd-version
---------------
√ can determine the latest version
√ cli is up-to-date
control-plane-version
---------------------
√ control plane is up-to-date
√ control plane and cli versions match
linkerd-prometheus
------------------
√ prometheus add-on service account exists
√ prometheus add-on config map exists
√ prometheus pod is running
linkerd-grafana
---------------
√ grafana add-on service account exists
√ grafana add-on config map exists
√ grafana pod is running
Status check results are √
现在我们的 Linkerd 安装看起来一切正常,我们可以将我们的应用程序添加到服务网格中:
kubectl -n linkerd get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
linkerd-controller 1/1 1 1 3m17s
linkerd-destination 1/1 1 1 3m17s
linkerd-grafana 1/1 1 1 3m16s
linkerd-identity 1/1 1 1 3m17s
linkerd-prometheus 1/1 1 1 3m16s
linkerd-proxy-injector 1/1 1 1 3m17s
linkerd-sp-validator 1/1 1 1 3m17s
linkerd-tap 1/1 1 1 3m17s
linkerd-web 1/1 1 1 3m17s
让我们打开 Linkerd 控制台,调查我们刚刚部署的内容。我们可以使用 linkerd dashboard & 启动控制台。
这将把控制台代理到我们的本地机器上,位于 http://localhost:50750 可用:
linkerd viz install | kubectl apply -f -
linkerd viz dashboard
Linkerd dashboard available at:
http://localhost:50750
Grafana dashboard available at:
http://localhost:50750/grafana
Opening Linkerd dashboard in the default browser
提示
如果您在访问仪表板时遇到问题,可以运行 linkerd viz check 并在 Linkerd 文档 中找到更多帮助。
我们可以在 图 5-9 中看到之前练习中部署的所有对象。
我们的 ClusterIP 服务不是 Linkerd 服务网格的一部分。我们需要使用代理注入器将我们的服务添加到网格中。它通过观察可以添加到 pod 规范中的特定注释来实现这一点,可以通过 Linkerd 的 inject 或手动方式添加。

图 5-9. Linkerd 仪表板
让我们清理一些旧练习资源,以便更清晰:
kubectl delete -f ingress-example-2.yaml
deployment.apps "app2" deleted
service "clusterip-service-2" deleted
ingress.extensions "ingress-resource-2" deleted
kubectl delete pods app-5586fc9d77-7frts
pod "app-5586fc9d77-7frts" deleted
kubectl delete -f ingress-rule.yaml
ingress.extensions "ingress-resource" deleted
我们可以使用 Linkerd CLI 将适当的注释注入到我们的部署规范中,以便它成为网格的一部分。
我们首先需要获取我们的应用程序清单,cat web.yaml,然后使用 Linkerd 将注释注入,linkerd inject -,最后将它们应用回 Kubernetes API,kubectl apply -f -:
cat web.yaml | linkerd inject - | kubectl apply -f -
deployment "app" injected
deployment.apps/app configured
如果我们描述我们的应用部署,我们可以看到 Linkerd 为我们注入了新的注释,注释: linkerd.io/inject: enabled:
kubectl describe deployment app
Name: app
Namespace: default
CreationTimestamp: Sat, 30 Jan 2021 13:48:47 -0500
Labels: <none>
Annotations: deployment.kubernetes.io/revision: 3
Selector: app=app
Replicas: 1 desired | 1 updated | 1 total | 1 available |
0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=app
Annotations: linkerd.io/inject: enabled
Containers:
go-web:
Image: strongjz/go-web:v0.0.6
Port: 8080/TCP
Host Port: 0/TCP
Liveness: http-get http://:8080/healthz delay=5s timeout=1s period=5s
Readiness: http-get http://:8080/ delay=5s timeout=1s period=5s
Environment:
MY_NODE_NAME: (v1:spec.nodeName)
MY_POD_NAME: (v1:metadata.name)
MY_POD_NAMESPACE: (v1:metadata.namespace)
MY_POD_IP: (v1:status.podIP)
MY_POD_SERVICE_ACCOUNT: (v1:spec.serviceAccountName)
DB_HOST: postgres
DB_USER: postgres
DB_PASSWORD: mysecretpassword
DB_PORT: 5432
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: app-78dfbb4854 (1/1 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 4m4s deployment-controller Scaled down app-5586fc9d77
Normal ScalingReplicaSet 4m4s deployment-controller Scaled up app-78dfbb4854
Normal Injected 4m4s linkerd-proxy-injector Linkerd sidecar injected
Normal ScalingReplicaSet 3m54s deployment-controller Scaled app-5586fc9d77
如果我们在仪表板中导航到应用程序,我们可以看到我们的部署现在是 Linkerd 服务网格的一部分,如 图 5-10 所示。

图 5-10. Web 应用部署 Linkerd 仪表板
CLI 还可以为我们显示统计信息:
linkerd stat deployments -n default
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
app 1/1 100.00% 0.4rps 1ms 1ms 1ms 1
再次,让我们扩展我们的部署:
kubectl scale deploy app --replicas 10
deployment.apps/app scaled
在 图 5-11 中,我们导航到 Web 浏览器并打开 此链接,这样我们就可以实时查看统计数据。选择默认的命名空间,在资源中选择我们的部署/应用。然后点击“为 Web 启动”以开始显示指标。
在单独的终端中,让我们使用 netshoot 镜像,但这次在我们的 KIND 集群内运行:
kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot -- /bin/bash
If you don't see a command prompt, try pressing enter.
bash-5.0#

图 5-11. Web 应用仪表板
让我们发送几百个查询,查看统计数据:
bash-5.0#for i in `seq 1 100`;
do curl http://clusterip-service/host && sleep 2;
done
在我们的终端中,我们可以看到所有的存活探针、就绪探针以及我们的 /host 请求。
tmp-shell 是我们的 netshoot bash 终端,其中运行着我们的 for 循环。
10.244.2.1、10.244.3.1 和 10.244.2.1 是运行我们探测的主机的 Kubelet:
linkerd viz stat deploy
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
app 1/1 100.00% 0.7rps 1ms 1ms 1ms 3
我们的示例仅展示了服务网格的可观察功能。Linkerd、Istio 等等还有许多可用于开发人员和网络管理员控制、监控和故障排除集群网络内运行服务的选项。与入口控制器一样,提供了许多选项和功能。由您和您的团队决定哪些功能和特性对您的网络重要。
结论
Kubernetes 网络世界功能丰富,团队可以通过多种选项部署、测试和管理其 Kubernetes 集群。每个新的添加都会给集群操作增加复杂性和开销。我们已经为开发人员、网络管理员和系统管理员提供了 Kubernetes 提供的抽象视图。
从内部流量到集群外部流量,团队必须选择最适合其工作负载的抽象方法。这并不是一件小事,现在你已经掌握了开始这些讨论的知识。
在我们的下一章中,我们将把我们的 Kubernetes 服务和网络学习带到云端!我们将探索每个云提供商提供的网络服务,并了解它们如何集成到其 Kubernetes 管理服务中。
第六章:Kubernetes 和云网络
云及其服务的使用增长迅速:77% 的企业在某种程度上使用公共云,81% 可以比本地更快地进行创新。随着云中的流行和创新,将 Kubernetes 运行在云中是一个逻辑的步骤。每个主要云提供商都有其自己的托管 Kubernetes 服务,使用其云网络服务。
在本章中,我们将探讨主要云服务提供商 AWS、Azure 和 GCP 提供的网络服务,重点关注它们对在特定云环境中运行 Kubernetes 集群所需网络的影响。所有提供商还都有一个 CNI 项目,通过与其云网络 API 的集成视角,使运行 Kubernetes 集群更加顺畅,因此有必要探索这些 CNI。阅读本章后,管理员将了解云提供商如何在其云网络服务的基础上实现其托管 Kubernetes。
Amazon Web Services
亚马逊网络服务(AWS)已经从简单队列服务(SQS)和简单存储服务(S3)扩展到超过 200 多种服务。Gartner 研究将 AWS 定位在其 2020 年云基础设施与平台服务的魔力象限图的领导者象限中。许多服务都建立在其他基础服务之上。例如,Lambda 使用 S3 进行代码存储,使用 DynamoDB 进行元数据存储。AWS CodeCommit 使用 S3 进行代码存储。EC2、S3 和 CloudWatch 集成到亚马逊弹性 MapReduce 服务中,创建一个托管数据平台。AWS 网络服务也是如此。高级服务如对等连接和端点使用核心网络基础组件构建。了解这些基础组件,这些组件使 AWS 能够构建全面的 Kubernetes 服务,对管理员和开发人员至关重要。
AWS 网络服务
AWS 拥有许多服务,允许用户扩展和保护其云网络。Amazon Elastic Kubernetes Service(EKS)充分利用了 AWS 云中可用的这些网络组件。我们将讨论 AWS 网络组件的基础知识,以及它们与部署 EKS 集群网络的关系。本节还将讨论几种其他开源工具,这些工具使集群和应用程序部署变得简单。第一个是eksctl,这是一个 CLI 工具,用于部署和管理 EKS 集群。正如我们在之前的章节中所看到的,运行集群需要许多组件,在 AWS 网络中也是如此。eksctl将为集群和网络管理员在 AWS 中部署所有组件。接下来,我们将讨论 AWS VPC CNI,它允许集群使用原生的 AWS 服务来扩展 Pod 并管理其 IP 地址空间。最后,我们将研究 AWS 应用负载均衡器入口控制器,它自动化、管理和简化了在 AWS 网络上运行应用程序负载均衡器和入口的部署。
虚拟私有云
AWS 网络的基础是虚拟私有云(VPC)。大多数 AWS 资源将在 VPC 内部工作。VPC 网络是由管理员定义的孤立虚拟网络,仅用于其帐户及其资源。在图 6-1 中,我们可以看到一个 VPC,其定义了一个 CIDR 为192.168.0.0/16的单一范围。VPC 内的所有资源将使用该范围的私有 IP 地址。AWS 不断增强其服务提供;现在,网络管理员可以在 VPC 中使用多个不重叠的 CIDR。Pod IP 地址也将来自 VPC CIDR 和主机 IP 地址;关于这一点更多内容请参阅“AWS VPC CNI”。每个 AWS 区域设置一个 VPC;您可以在每个区域拥有多个 VPC,但 VPC 仅在一个区域中定义。

图 6-1. AWS 虚拟私有云
区域和可用区
AWS 资源是由边界定义的,例如全局、区域或可用区。AWS 网络包括多个区域;每个 AWS 区域由多个隔离且物理上分离的可用区(AZ)组成,位于地理区域内。一个 AZ 可以包含多个数据中心,如图 6-2 所示。一些区域可能包含六个 AZ,而较新的区域可能只包含两个。每个 AZ 直接连接到其他 AZ,但与另一个 AZ 的故障是隔离的。这种设计对于多个原因都很重要:高可用性、负载均衡和子网都会受到影响。在一个区域中,负载均衡器将通过多个 AZ 路由流量,这些 AZ 有单独的子网,因此为应用程序提供了高可用性。

图 6-2. AWS 区域网络布局
注意
AWS 区域和 AZ 的最新列表可以在文档中找到。
子网
VPC 由多个子网组成,这些子网来自 CIDR 范围并部署到单个 AZ。需要高可用性的应用程序应在多个 AZ 中运行,并且可以使用任何可用的负载均衡器进行负载平衡,如“区域和可用区”中讨论的。
如果路由表有通往互联网网关的路由,则子网是公共的。在图 6-3 中,有三个公共子网和私有子网。私有子网没有直接通往互联网的路由。这些子网用于内部网络流量,如数据库。在部署网络架构时,VPC CIDR 范围的大小和公共与私有子网的数量是设计考虑因素。VPC 最近的改进允许多个 CIDR 范围,有助于减少设计选择不良的影响,因为现在网络工程师可以简单地向预配的 VPC 添加另一个 CIDR 范围。

图 6-3. VPC 子网
让我们讨论那些有助于定义子网是公共还是私有的组件。
路由表
每个子网恰好与一个路由表关联。如果未明确关联,则主路由表是默认的路由表。在 VPC 内部部署应用程序的开发人员必须了解如何操作路由表,以确保流量按预期流动。
以下是主路由表的规则:
-
主路由表无法删除。
-
网关路由表不能设为主路由表。
-
主路由表可以替换为自定义路由表。
-
管理员可以在主路由表中添加、移除和修改路由。
-
本地路由是最具体的。
-
子网可以明确关联到主路由表。
有特定目标的路由表;以下是它们的列表及其区别的描述:
主路由表
此路由表自动控制未明确关联到任何其他路由表的所有子网的路由。
自定义路由表
网络工程师为特定应用程序流量创建和定制的路由表。
边缘关联
用于将入站 VPC 流量路由至边缘设备的路由表。
子网路由表
与子网相关联的路由表。
网关路由表
与互联网网关或虚拟专用网关相关联的路由表。
每个路由表有几个组件确定其职责:
路由表关联
路由表与子网、互联网网关或虚拟专用网关之间的关联。
规则
定义表的路由条目列表;每条规则都有目标、目的地、状态和传播标志。
目的地
想要传输流量的 IP 地址范围(目标 CIDR)。
目标
发送目标流量的网关、网络接口或连接;例如,互联网网关。
状态
路由表中路由的状态:活动或黑洞。黑洞状态表示路由的目标不可用。
传播
路由传播允许虚拟私有网关自动向路由表传播路由。此标志指示您该路由是否通过传播添加。
本地路由
用于 VPC 内部通信的默认路由。
在图 6-4 中,路由表中有两条路由。所有目标为11.0.0.0/16的流量保持在 VPC 内的本地网络上。所有其他流量,0.0.0.0/0,都经由 Internet 网关igw-f43c4690到达,使其成为公共子网。

图 6-4. 路由表
弹性网络接口
弹性网络接口(ENI)是 VPC 中的一个逻辑网络组件,相当于虚拟网络卡。ENI 包含一个 IP 地址,用于实例,它们在弹性上意味着可以在保留其属性的同时关联和取消关联到实例。
ENI 具有以下属性:
-
主要私有 IPv4 地址
-
次要私有 IPv4 地址
-
每个私有 IPv4 地址一个弹性 IP(EIP)地址
-
在启动实例时,可以将一个公共 IPv4 地址自动分配给网络接口
eth0。 -
一个或多个 IPv6 地址
-
一个或多个安全组
-
MAC 地址
-
源/目标检查标志
-
描述
ENI 的一个常见用例是创建仅从公司网络访问的管理网络。AWS 服务如 Amazon WorkSpaces 使用 ENI 允许访问客户 VPC 和 AWS 管理的 VPC。Lambda 可以通过提供并附加到 ENI 来访问 VPC 内的资源,如数据库。
在本节后面,我们将看到 AWS VPC CNI 如何与 IP 地址一起使用和管理 ENI。
弹性 IP 地址
EIP 地址是用于 AWS 云中动态网络寻址的静态公共 IPv4 地址。EIP 与任何 VPC 中的任何实例或网络接口相关联。借助 EIP,应用程序开发人员可以通过将地址重新映射到另一个实例来掩盖实例的故障。
EIP 地址是 ENI 的属性,并通过更新附加到实例的 ENI 与实例关联。将 EIP 与 ENI 关联而不是直接与实例关联的优势在于,所有网络接口属性可以一次性从一个实例移动到另一个实例。
以下规则适用:
-
一个 EIP 地址一次可以与单个实例或网络接口关联。
-
EIP 地址可以从一个实例或网络接口迁移到另一个实例。
-
最多五个 EIP 地址的(软)限制。
-
不支持 IPv6。
类似 NAT 和 Internet 网关的服务使用 EIP 以保持 AZ 之间的一致性。其他网关服务(如堡垒)可以从使用 EIP 中受益。子网可以自动为 EC2 实例分配公共 IP 地址,但该地址可能会更改;使用 EIP 可以防止这种情况发生。
安全控制
AWS 网络中有两个基本安全控制:安全组和网络访问控制列表(NACL)。根据我们的经验,许多问题源于安全组和 NACL 的配置错误。开发人员和网络工程师需要理解两者之间的区别以及对它们的更改影响。
安全组
安全组在实例或网络接口级别操作,并充当这些设备的防火墙。安全组是一组需要彼此和网络上其他设备共享网络访问的网络设备。在图 6-5 中,我们可以看到安全组跨 AZ 运行。安全组有两张表,用于入站和出站流量。安全组是有状态的,因此如果允许入站流量,则允许出站流量。每个安全组都有一系列规则,定义了流量的过滤器。在做出转发决策之前,会评估每条规则。

图 6-5. 安全组
以下是安全组规则组件的列表:
源/目的地
流量检查的源(入站规则)或目的地(出站规则):
-
IPv4 或 IPv6 地址的个体或范围
-
另一个安全组
-
其他 ENIs、网关或接口
协议
被过滤的第 4 层协议是哪个,6(TCP),17(UDP)和 1(ICMP)
端口范围
正在过滤的协议的特定端口
描述
用户定义字段,以通知其他人安全组的意图
安全组类似于我们在前几章中讨论的 Kubernetes 网络策略。它们是一种基本的网络技术,应始终用于保护 AWS VPC 中的实例。EKS 部署了几个安全组,用于 AWS 管理的数据平面与您的工作节点之间的通信。
网络访问控制列表
网络访问控制列表的操作方式与其他防火墙中的操作方式类似,因此网络工程师将熟悉它们。在图 6-6 中,您可以看到每个子网都有一个默认的 NACL 与之关联,并且与 AZ 绑定,与安全组不同。过滤规则必须在两个方向上明确定义。默认规则非常宽松,允许在两个方向上的所有流量。用户可以为子网定义自己的 NACL 以增加安全层级,如果安全组过于开放。默认情况下,自定义 NACL 会拒绝所有流量,因此在部署时添加规则;否则,实例将失去连接。
这是 NACL 的组成部分:
规则编号
规则从编号最低的规则开始评估。
类型
交通类型,如 SSH 或 HTTP。
协议
任何具有标准协议号的协议:TCP/UDP 或 ALL。
端口范围
流量的监听端口或端口范围。例如,HTTP 流量的 80 端口。
来源
仅入站规则;流量的 CIDR 范围源。
目的地
只有出站规则;流量的目的地。
允许/拒绝
是否允许或拒绝指定的流量。

图 6-6. NACL
NACL 为可能受保护免于安全组缺乏或配置错误的子网增加了一层额外的安全性。
表 6-1 总结了安全组和网络 ACL 之间的基本区别。
表 6-1. 安全组和 NACL 比较表
| 安全组 | 网络 ACL |
|---|---|
| 操作在实例级别。 | 操作在子网级别。 |
| 仅支持允许规则。 | 支持允许和拒绝规则。 |
| 有状态:返回流量始终允许,不受任何规则限制。 | 无状态:返回流量必须按规则显式允许。 |
| 所有规则在进行转发决策之前都会被评估。 | 规则按顺序处理,从最低编号规则开始。 |
| 适用于实例或网络接口。 | 所有规则适用于其关联的所有子网中的所有实例。 |
了解 NACL 和安全组之间的区别至关重要。由于安全组未允许特定端口上的流量或某人未在 NACL 上添加出站规则,AWS 网络连接问题经常会出现。在排除 AWS 网络问题时,开发人员和网络工程师都应该将这些组件添加到其排查列表中进行检查。
到目前为止,我们讨论的所有组件都管理 VPC 内部的流量。以下服务管理来自客户请求进入 VPC 并最终到运行在 Kubernetes 集群内的应用程序的流量:网络地址转换设备、互联网网关和负载均衡器。让我们深入了解一下这些服务。
网络地址转换设备
当 VPC 内部的实例需要互联网连接,但不能直接连接到实例时,使用网络地址转换(NAT)设备。应该运行在 NAT 设备后面的实例的示例包括数据库实例或其他中间件,这些中间件需要运行应用程序。
在 AWS 中,网络工程师有几种选项可以运行 NAT 设备。他们可以管理自己部署的 EC2 实例作为 NAT 设备,也可以使用 AWS 托管服务 NAT 网关(NAT GW)。无论选择哪种方式,都需要在多个可用区部署公共子网以实现高可用性和 EIP。NAT GW 的限制是部署后其 IP 地址不能更改。此外,该 IP 地址将是与互联网网关通信时使用的源 IP 地址。
在 图 6-7 中的 VPC 路由表中,我们可以看到两个路由表的存在,以建立到互联网的连接。主路由表有两条规则,一个是用于 VPC 内部的本地路由,另一个是目标为 NAT GW ID 的 0.0.0.0/0 路由。私有子网的数据库服务器将通过其路由表中的 NAT GW 规则路由流量到互联网。
EKS 中的 Pod 和实例将需要流出 VPC,因此必须部署 NAT 设备。您选择的 NAT 设备将取决于网络设计的操作开销、成本或可用性要求。

图 6-7. VPC 路由图
互联网网关
互联网网关是 VPC 网络中的 AWS 托管服务和设备,允许 VPC 中的所有设备连接到互联网。以下是确保在 VPC 中访问互联网的步骤:
-
部署并附加一个互联网网关(IGW)到 VPC。
-
在子网的路由表中定义一条路由,将互联网流量引导到互联网网关(IGW)。
-
验证网络访问控制列表(NACLs)和安全组规则允许流向和从实例流动的流量。
所有这些都显示在 VPC 路由中,来自图 6-7。我们看到了为 VPC 部署的 IGW,一个自定义的路由表设置,将所有流量0.0.0.0/0路由到 IGW。Web 实例具有 IPv4 互联网可路由地址,198.51.100.1-3。
弹性负载均衡器
现在从互联网流入的流量,并且客户端可以请求访问运行在 VPC 内的应用程序,我们需要扩展和分发请求的负载。AWS 为开发者提供了几种选项,具体取决于应用程序负载和网络流量的需求。
弹性负载均衡器有四个选项:
经典
经典负载均衡器为 EC2 实例提供基本的负载均衡。它在请求和连接级别操作。经典负载均衡器功能有限,不适用于容器。
应用程序
应用程序负载均衡器具有第 7 层感知能力。流量路由是通过特定于请求的信息,如 HTTP 头或 HTTP 路径来进行的。应用程序负载均衡器与应用程序负载均衡器控制器一起使用。ALB 控制器允许开发人员无需使用控制台或 API,只需几行 YAML 代码即可自动化部署和管理 ALB。
网络
网络负载均衡器在第 4 层运行。流量可以基于传入 TCP/UDP 端口路由到运行该端口服务的个体主机。网络负载均衡器还允许管理员使用 EIP 部署它们,这是网络负载均衡器独有的功能。
网关
网关负载均衡器管理 VPC 级别的设备流量。可以使用网关负载均衡器处理的网络设备包括深度数据包检查或代理。网关负载均衡器被添加到 AWS 服务中,但不在 EKS 生态系统内使用。
AWS 负载均衡器在处理不仅限于容器的工作负载时具有几个重要属性:
规则
(仅 ALB)您为监听器定义的规则决定负载均衡器如何将所有请求路由到目标组中的目标。
监听器
检查来自客户端的请求。它们支持 HTTP 和 HTTPS 在端口 1-65535 上。
目标
EC2 实例、IP 地址、Pods 或运行应用程序代码的 Lambda。
目标组
用于将请求路由到注册的目标。
健康检查
确保目标仍能接受客户请求。
每个 ALB 的这些组件在图 6-8 中概述。当请求进入负载均衡器时,侦听器将持续检查是否有与其定义的协议和端口匹配的请求。每个侦听器都有一组规则,定义了如何处理请求。规则将有一个操作类型来确定如何处理请求:
authenticate-cognito
(HTTPS 侦听器)使用 Amazon Cognito 对用户进行身份验证。
authenticate-oidc
(HTTPS 侦听器)使用符合 OpenID Connect 的身份提供者对用户进行身份验证。
fixed-response
返回自定义 HTTP 响应。
forward
将请求转发到指定的目标组。
重定向
将请求从一个 URL 重定向到另一个 URL。
具有最低顺序值的动作首先执行。每个规则必须包括以下动作之一:转发、重定向或固定响应。在图 6-8 中,我们有目标组,这些组将成为我们转发规则的接收者。目标组中的每个目标都将进行健康检查,因此负载均衡器将知道哪些实例是健康且准备好接收请求。

图 6-8. 负载均衡器组件
现在我们对 AWS 如何构建其网络组件有了基本的理解,我们可以开始看到 EKS 如何利用这些组件来构建和保护托管的 Kubernetes 集群和网络。
Amazon Elastic Kubernetes Service
Amazon Elastic Kubernetes Service(EKS)是 AWS 的托管 Kubernetes 服务。它允许开发人员、集群管理员和网络管理员快速部署生产规模的 Kubernetes 集群。利用云的扩展性和 AWS 网络服务,一个 API 请求可以部署许多服务,包括我们在前面章节中审查的所有组件。
EKS 是如何实现这一点的?与 AWS 发布的任何新服务一样,EKS 变得功能更加丰富且易于使用。EKS 现在支持在 EKS Anywhere 上进行本地部署,在 EKS Fargate 上进行无服务器部署,甚至支持 Windows 节点。EKS 集群可以通过 AWS CLI 或控制台传统部署。eksctl是 Weaveworks 开发的命令行工具,迄今为止是部署运行 EKS 所需组件的最简单方法。我们的下一节将详细介绍运行 EKS 集群的要求以及eksctl如何为集群管理员和开发人员完成这一任务。
让我们讨论 EKS 集群网络的组件。
EKS 节点
EKS 中的工作节点有三种类型:EKS 管理的节点组、自管理节点和 AWS Fargate。管理员的选择在于他们想要承担多少控制和运营开销。
管理的节点组
Amazon EKS 管理的节点组为您创建和管理 EC2 实例。所有托管节点都作为由 Amazon EKS 管理的 EC2 自动扩展组的一部分进行预配。所有资源,包括 EC2 实例和自动扩展组,都在您的 AWS 帐户内运行。托管节点组的自动扩展组跨越您创建组时指定的所有子网。
自管理节点组
Amazon EKS 节点在您的 AWS 帐户中运行,并通过 API 端点连接到集群的控制平面。您将节点部署到一个节点组中。节点组是部署在 EC2 自动扩展组中的 EC2 实例集合。节点组中的所有实例必须执行以下操作:
-
必须是相同的实例类型
-
必须运行相同的 Amazon Machine Image
-
使用相同的 Amazon EKS 节点 IAM 角色
Fargate
Amazon EKS 通过使用由 AWS 构建的控制器将 Kubernetes 与 AWS Fargate 集成,使用 Kubernetes 提供的上游可扩展模型。在 Fargate 上运行的每个 Pod 都有自己的隔离边界,不与另一个 Pod 共享底层内核、CPU、内存或弹性网络接口。您也不能使用安全组来管理运行在 Fargate 上的 Pod。
实例类型还影响集群网络。在 EKS 中,节点上可以运行的 Pod 数量由实例可以运行的 IP 地址数量定义。我们在 “AWS VPC CNI” 和 “eksctl” 中进一步讨论这个问题。
节点必须能够与 Kubernetes 控制平面和其他 AWS 服务进行通信。IP 地址空间对于运行 EKS 集群至关重要。节点、Pod 和所有其他服务将使用 VPC CIDR 地址范围进行组件通信。EKS VPC 需要一个 NAT 网关用于私有子网,并且这些子网需要标记以供 EKS 使用:
Key – kubernetes.io/cluster/<cluster-name>
Value – shared
每个节点的放置将决定 EKS 运行的网络“模式”;这对你的子网和 Kubernetes API 的流量路由有设计考量。
EKS 模式
图 6-9 概述了 EKS 的组件。Amazon EKS 控制平面为每个集群在您的 VPC 中创建高达四个跨帐户弹性网络接口。EKS 使用两个 VPC,一个用于 Kubernetes 控制平面,包括 Kubernetes API 主节点、API 负载均衡器和根据网络模型的 etcd;另一个是客户 VPC,用于在其中运行您的 Pod 的 EKS 工作节点。在 EC2 实例的引导过程中,启动 Kubelet。节点的 Kubelet 将联系 Kubernetes 集群端点注册节点。它连接到 VPC 外的公共端点或 VPC 内的私有端点。kubectl 命令连接到 EKS VPC 中的 API 端点。最终用户可以访问运行在客户 VPC 中的应用程序。

图 6-9. EKS 通信路径
根据 Kubernetes 组件的控制和数据平面运行位置,有三种配置 EKS 集群控制流量和 Kubernetes API 端点的方式。
网络模式如下:
仅公共
所有内容均在公共子网中运行,包括工作节点。
仅私有
仅在私有子网中运行,Kubernetes 无法创建面向 Internet 的负载均衡器。
混合
公共和私有的组合。
公共端点是默认选项;它是公共的,因为 API 端点的负载均衡器位于公共子网上,如 图 6-10 所示。来自集群 VPC 内的 Kubernetes API 请求(例如当工作节点连接到控制平面时)离开客户 VPC,但不离开 Amazon 网络。使用公共端点时需要考虑的一个安全问题是 API 端点位于公共子网上,并且可以通过 Internet 访问。

图 6-10. EKS 仅公共网络模式
图 6-11 显示了私有端点模式;所有访问集群 API 的流量必须来自集群的 VPC。API 服务器没有 Internet 访问权限;任何 kubectl 命令必须来自 VPC 内或连接的网络。集群的 API 端点通过公共 DNS 解析为 VPC 内的私有 IP 地址。

图 6-11. EKS 仅私有网络模式
当启用公共和私有端点时,从 VPC 内的任何 Kubernetes API 请求都通过客户 VPC 中的 EKS 管理的 ENI 与控制平面通信,如 图 6-12 所示。集群 API 仍可通过 Internet 访问,但可以使用安全组和 NACLs 进行限制。
注意
请参阅 AWS 文档 了解更多部署 EKS 的方法。
确定运行模式是管理员们需要做出的关键决策。它会影响应用程序流量、负载均衡器的路由以及集群的安全性。在 EKS 部署集群时,还有许多其他要求。eksctl 是一种工具,可以帮助管理所有这些要求。但是eksctl 是如何做到的呢?

图 6-12. EKS 公共和私有网络模式
eksctl
eksctl 是由 Weaveworks 开发的命令行工具,是部署运行 EKS 所需的所有组件最简单的方式。
注意
关于 eksctl 的所有信息都可以在其 网站 上找到。
eksctl 默认使用以下默认参数创建集群:
-
一个自动生成的集群名称
-
两个 m5.large 工作节点
-
使用官方 AWS EKS AMI
-
Us-west-2 默认的 AWS 区域
-
一个专用的 VPC
一个带有 192.168.0.0/16 CIDR 范围的专用 VPC,默认情况下,eksctl 将创建 8 个 /19 子网:三个私有子网、三个公共子网和两个保留子网。eksctl 还将部署一个 NAT GW,允许位于私有子网中的节点进行通信,并部署一个 Internet Gateway,以便访问所需的容器镜像和与 Amazon S3 以及 Amazon ECR API 的通信。
为 EKS 集群设置了两个安全组:
Ingress inter node group SG
允许节点在所有端口上彼此通信
控制平面安全组
允许控制平面和工作节点组之间的通信
公共子网中的节点组将禁用 SSH。初始节点组中的 EC2 实例会获得公共 IP,并且可以通过高级别端口进行访问。
默认情况下,一个包含两个 m5.large 节点的节点组是 eksctl 的默认设置。但是一个节点可以运行多少个 Pod 呢?AWS 提供了一个基于节点类型、接口数量和支持的 IP 地址数量的公式。该公式如下:
(Number of network interfaces for the instance type ×
(the number of IP addresses per network interface - 1)) + 2
使用上述公式和 eksctl 的默认实例大小,一个 m5.large 实例最多支持 29 个 Pod。
警告
系统 Pod 会计入最大 Pod 数量。CNI 插件和 kube-proxy Pod 在集群中的每个节点上运行,因此您只能额外部署 27 个 Pod 到一个 m5.large 实例中。CoreDNS 也会在集群中的节点上运行,这进一步减少了节点可以运行的最大 Pod 数量。
运行集群的团队必须决定集群大小和实例类型,以确保不会因为节点和 IP 限制而导致部署问题。如果没有可用具有 Pod IP 地址的节点,Pod 将处于“等待”状态。EKS 节点组的扩展事件也可能触及 EC2 实例类型限制并引发级联问题。
所有这些网络选项都可以通过 eksctl 配置文件进行配置。
注意
eksctl 中的 VPC 选项详见 eksctl 文档。
我们已经讨论过节点大小对于 Pod IP 地址分配及其可运行数量的重要性。一旦节点部署完成,AWS VPC CNI 将管理节点的 Pod IP 地址分配。让我们深入了解一下 CNI 的内部工作原理。
AWS VPC CNI
AWS 有自己的 CNI 开源实现。AWS VPC CNI 用于 Kubernetes 插件在 AWS 网络上提供高吞吐量和可用性、低延迟以及最小的网络抖动。网络工程师可以应用现有的 AWS VPC 网络和安全最佳实践来构建在 AWS 上的 Kubernetes 集群。这包括使用像 VPC 流日志、VPC 路由策略和安全组进行网络流量隔离。
注意
AWS VPC CNI 的开源代码可以在 GitHub 上找到。
AWS VPC CNI 有两个组成部分:
CNI 插件
当调用时,CNI 插件负责连接主机和 Pod 的网络堆栈。它还配置接口和虚拟以太网对。
ipamd
长时间运行的节点本地 IPAM 守护程序负责维护一组可用 IP 地址,并为 Pod 分配 IP 地址。
图 6-13 演示了 VPC CNI 对节点的影响。在 AWS 中,客户 VPC 在子网 10.200.1.0/24 中提供给我们此子网中的 250 个可用地址。我们的集群中有两个节点。在 EKS 中,托管节点作为守护进程运行 AWS CNI。在我们的示例中,每个节点只有一个运行的 pod,在 ENI 上有一个次要 IP 地址,即 10.200.1.6 和 10.200.1.8,分别对应每个 pod。当工作节点首次加入集群时,只有一个 ENI 及其所有地址在 ENI 中。当第三个 pod 被调度到节点 1 时,ipamd 为该 pod 分配 IP 地址。在这种情况下,10.200.1.7 是节点 2 上的 pod 4 的同样情况。
当工作节点首次加入集群时,只有一个 ENI 及其所有地址在 ENI 中。没有任何配置时,ipamd 总是尝试保持额外的一个 ENI。当节点上运行的多个 pod 数量超过单个 ENI 上的地址数量时,CNI 后端开始分配新的 ENI。CNI 插件通过为 EC2 实例分配多个 ENI,然后将次要 IP 地址附加到这些 ENI 上来工作。此插件允许 CNI 尽可能多地为每个实例分配 IP。

图 6-13. AWS VPC CNI 示例
AWS VPC CNI 高度可配置。此列表仅包括一些选项:
AWS_VPC_CNI_NODE_PORT_SUPPORT
指定是否在工作节点的主要网络接口上启用了 NodePort 服务。这需要额外的 iptables 规则,并且需要将内核的反向路径过滤器设置为宽松模式。
AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG
工作节点可以配置在公共子网中,因此您需要将 pod 配置为部署在私有子网中,或者如果 pod 的安全需求与其他节点上运行的需求不同,将其设置为 true 将启用这一功能。
AWS_VPC_ENI_MTU
默认值:9001。用于配置附加 ENI 的 MTU 大小。有效范围为 576 到 9001。
WARM_ENI_TARGET
指定 ipamd 守护进程应尝试保持供分配给节点上的 pod 的弹性网络接口及其所有可用 IP 地址的数量。默认情况下,ipamd 尝试保持一个弹性网络接口及其所有 IP 地址供 pod 分配使用。每个网络接口的 IP 地址数量因实例类型而异。
AWS_VPC_K8S_CNI_EXTERNALSNAT
指定是否应使用外部 NAT 网关来提供次要 ENI IP 地址的 SNAT。如果设置为 true,则不会应用 SNAT iptables 规则和外部 VPC IP 规则,并且如果已经应用了这些规则,则将其删除。如果需要允许来自外部 VPN、直接连接和外部 VPC 的入站通信,并且您的 pod 不需要通过互联网网关直接访问互联网,则禁用 SNAT。
例如,如果您的带有私有 IP 地址的 Pod 需要与其他人的私有 IP 地址空间通信,您可以使用以下命令启用 AWS_VPC_K8S_CNI_EXTERNALSNAT:
kubectl set env daemonset
-n kube-system aws-node AWS_VPC_K8S_CNI_EXTERNALSNAT=true
注意
EKS Pod 网络的所有信息都可以在 EKS 文档 中找到。
AWS VPC CNI 允许在 AWS 网络中对 EKS 的网络选项进行最大控制。
还有 AWS ALB 入口控制器,使在 AWS 云网络上管理和部署应用程序变得顺畅和自动化。让我们接着深入了解。
AWS ALB 入口控制器
让我们通过 图 6-14 中的示例来了解 AWS ALB 与 Kubernetes 的工作原理。要了解什么是入口控制器,请查看 第五章。
让我们讨论 ALB 入口控制器的所有组成部分:
-
ALB 入口控制器会监视来自 API 服务器的入口事件。当满足要求时,它将开始创建 ALB 的过程。
-
在 AWS 中为新的入口资源创建 ALB。这些资源可以是集群内部或外部的。
-
为每个在入口资源中描述的唯一 Kubernetes 服务在 AWS 中创建目标组。
-
为入口资源注释中详细说明的每个端口创建侦听器。如果未指定,默认设置 HTTP 和 HTTPS 流量的默认端口。每个服务的 NodePort 服务创建用于我们的健康检查的节点端口。
-
为入口资源中指定的每个路径创建规则。这确保将流量路由到正确的 Kubernetes 服务的特定路径。

图 6-14. AWS ALB 示例
流量如何到达节点和 Pod 受 ALB 运行的两种模式之一的影响:
实例模式
入口流量从 ALB 开始,并通过每个服务的 NodePort 到达 Kubernetes 节点。这意味着从入口资源引用的服务必须通过 type:NodePort 暴露才能被 ALB 访问。
IP 模式
入口流量从 ALB 开始,直接到达 Kubernetes Pod。CNIs 必须支持通过 ENI 上的辅助 IP 地址直接访问的 Pod IP 地址。
AWS ALB 入口控制器允许开发人员管理其网络需求,如应用程序组件。在流水线中不需要其他工具集。
AWS 网络组件与 EKS 紧密集成。了解它们的基本工作方式的选项对于所有希望在 AWS 上使用 EKS 扩展其应用程序的人来说是基本的。子网的大小,节点在这些子网中的放置位置,当然还有节点的大小将影响您可以在 AWS 网络上运行多大规模的 Pod 和服务的网络。使用像 eksctl 这样的开源工具与托管服务(如 EKS)将大大减少运行 AWS Kubernetes 集群的操作开销。
在 AWS EKS 集群上部署应用程序
让我们逐步部署一个 EKS 集群来管理我们的 Golang Web 服务器:
-
部署 EKS 集群。
-
部署 Web 服务器应用程序和 LoadBalancer。
-
验证。
-
部署 ALB Ingress Controller 并验证。
-
清理。
部署 EKS 集群。
让我们部署一个 EKS 集群,使用当前和最新版本 EKS 支持的版本 1.20:
export CLUSTER_NAME=eks-demo
eksctl create cluster -N 3 --name ${CLUSTER_NAME} --version=1.20
eksctl version 0.54.0
using region us-west-2
setting availability zones to [us-west-2b us-west-2a us-west-2c]
subnets for us-west-2b - public:192.168.0.0/19 private:192.168.96.0/19
subnets for us-west-2a - public:192.168.32.0/19 private:192.168.128.0/19
subnets for us-west-2c - public:192.168.64.0/19 private:192.168.160.0/19
nodegroup "ng-90b7a9a5" will use "ami-0a1abe779ecfc6a3e" [AmazonLinux2/1.20]
using Kubernetes version 1.20
creating EKS cluster "eks-demo" in "us-west-2" region with un-managed nodes
will create 2 separate CloudFormation stacks for cluster itself and the initial
nodegroup
if you encounter any issues, check CloudFormation console or try
'eksctl utils describe-stacks --region=us-west-2 --cluster=eks-demo'
CloudWatch logging will not be enabled for cluster "eks-demo" in "us-west-2"
you can enable it with
'eksctl utils update-cluster-logging --enable-types={SPECIFY-YOUR-LOG-TYPES-HERE
(e.g. all)} --region=us-west-2 --cluster=eks-demo'
Kubernetes API endpoint access will use default of
{publicAccess=true, privateAccess=false} for cluster "eks-demo" in "us-west-2"
2 sequential tasks: { create cluster control plane "eks-demo",
3 sequential sub-tasks: { wait for control plane to become ready, 1 task:
{ create addons }, create nodegroup "ng-90b7a9a5" } }
building cluster stack "eksctl-eks-demo-cluster"
deploying stack "eksctl-eks-demo-cluster"
waiting for CloudFormation stack "eksctl-eks-demo-cluster"
<truncate>
building nodegroup stack "eksctl-eks-demo-nodegroup-ng-90b7a9a5"
--nodes-min=3 was set automatically for nodegroup ng-90b7a9a5
deploying stack "eksctl-eks-demo-nodegroup-ng-90b7a9a5"
waiting for CloudFormation stack "eksctl-eks-demo-nodegroup-ng-90b7a9a5"
<truncated>
waiting for the control plane availability...
saved kubeconfig as "/Users/strongjz/.kube/config"
no tasks
all EKS cluster resources for "eks-demo" have been created
adding identity
"arn:aws:iam::1234567890:role/
eksctl-eks-demo-nodegroup-ng-9-NodeInstanceRole-TLKVDDVTW2TZ" to auth ConfigMap
nodegroup "ng-90b7a9a5" has 0 node(s)
waiting for at least 3 node(s) to become ready in "ng-90b7a9a5"
nodegroup "ng-90b7a9a5" has 3 node(s)
node "ip-192-168-31-17.us-west-2.compute.internal" is ready
node "ip-192-168-58-247.us-west-2.compute.internal" is ready
node "ip-192-168-85-104.us-west-2.compute.internal" is ready
kubectl command should work with "/Users/strongjz/.kube/config",
try 'kubectl get nodes'
EKS cluster "eks-demo" in "us-west-2" region is ready
在输出中,我们可以看到 EKS 正在创建一个节点组,eksctl-eks-demo-nodegroup-ng-90b7a9a5,包含三个节点:
ip-192-168-31-17.us-west-2.compute.internal
ip-192-168-58-247.us-west-2.compute.internal
ip-192-168-85-104.us-west-2.compute.internal
它们都位于一个 VPC 内,有三个公共子网和三个私有子网,跨三个 AZ:
public:192.168.0.0/19 private:192.168.96.0/19
public:192.168.32.0/19 private:192.168.128.0/19
public:192.168.64.0/19 private:192.168.160.0/19
警告
我们使用了 eksctl 的默认设置,并且将 k8s API 部署为公共端点,{publicAccess=true, privateAccess=false}。
现在我们可以在集群中部署我们的 Golang Web 应用程序,并使用 LoadBalancer 服务暴露它。
部署测试应用程序。
您可以分别或一起部署应用程序。dnsutils.yml 是我们的 dnsutils 测试 Pod,database.yml 是用于 Pod 连通性测试的 Postgres 数据库,web.yml 是 Golang Web 服务器和 LoadBalancer 服务:
kubectl apply -f dnsutils.yml,database.yml,web.yml
让我们运行 kubectl get pods 来查看所有的 Pod 是否运行正常:
kubectl get pods -o wide
NAME READY STATUS IP NODE
app-6bf97c555d-5mzfb 1/1 Running 192.168.15.108 ip-192-168-0-94
app-6bf97c555d-76fgm 1/1 Running 192.168.52.42 ip-192-168-63-151
app-6bf97c555d-gw4k9 1/1 Running 192.168.88.61 ip-192-168-91-46
dnsutils 1/1 Running 192.168.57.174 ip-192-168-63-151
postgres-0 1/1 Running 192.168.70.170 ip-192-168-91-46
现在检查 LoadBalancer 服务:
kubectl get svc clusterip-service
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
clusterip-service LoadBalancer 10.100.159.28
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com 80:32671/TCP 29m
服务还有端点:
kubectl get endpoints clusterip-service
NAME ENDPOINTS AGE
clusterip-service 192.168.15.108:8080,192.168.52.42:8080,192.168.88.61:8080 58m
我们应该验证应用程序是否可以在集群内访问,使用 ClusterIP 和端口 10.100.159.28:8080;服务名和端口 clusterip-service:80;最后是 Pod IP 和端口 192.168.15.108:8080:
kubectl exec dnsutils -- wget -qO- 10.100.159.28:80/data
Database Connected
kubectl exec dnsutils -- wget -qO- 10.100.159.28:80/host
NODE: ip-192-168-63-151.us-west-2.compute.internal, POD IP:192.168.52.42
kubectl exec dnsutils -- wget -qO- clusterip-service:80/host
NODE: ip-192-168-91-46.us-west-2.compute.internal, POD IP:192.168.88.61
kubectl exec dnsutils -- wget -qO- clusterip-service:80/data
Database Connected
kubectl exec dnsutils -- wget -qO- 192.168.15.108:8080/data
Database Connected
kubectl exec dnsutils -- wget -qO- 192.168.15.108:8080/host
NODE: ip-192-168-0-94.us-west-2.compute.internal, POD IP:192.168.15.108
数据库端口可以从 dnsutils 到达,使用 Pod IP 和端口 192.168.70.170:5432,服务名和端口 postgres:5432:
kubectl exec dnsutils -- nc -z -vv -w 5 192.168.70.170 5432
192.168.70.170 (192.168.70.170:5432) open
sent 0, rcvd 0
kubectl exec dnsutils -- nc -z -vv -w 5 postgres 5432
postgres (10.100.106.134:5432) open
sent 0, rcvd 0
集群内的应用程序已经运行起来了。让我们从集群外部测试一下。
验证 Golang Web 服务器的 LoadBalancer 服务。
kubectl 将返回我们测试所需的所有信息,包括 ClusterIP、外部 IP 和所有端口:
kubectl get svc clusterip-service
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
clusterip-service LoadBalancer 10.100.159.28
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com 80:32671/TCP 29m
使用负载均衡器的外部 IP:
wget -qO-
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com/data
Database Connected
让我们测试负载均衡器,并向后端发起多个请求:
wget -qO-
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com/host
NODE: ip-192-168-63-151.us-west-2.compute.internal, POD IP:192.168.52.42
wget -qO-
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com/host
NODE: ip-192-168-91-46.us-west-2.compute.internal, POD IP:192.168.88.61
wget -qO-
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com/host
NODE: ip-192-168-0-94.us-west-2.compute.internal, POD IP:192.168.15.108
wget -qO-
a76d1c69125e543e5b67c899f5e45284-593302470.us-west-2.elb.amazonaws.com/host
NODE: ip-192-168-0-94.us-west-2.compute.internal, POD IP:192.168.15.108
再次运行 kubectl get pods -o wide 将验证我们的 Pod 信息是否与负载均衡器请求匹配:
kubectl get pods -o wide
NAME READY STATUS IP NODE
app-6bf97c555d-5mzfb 1/1 Running 192.168.15.108 ip-192-168-0-94
app-6bf97c555d-76fgm 1/1 Running 192.168.52.42 ip-192-168-63-151
app-6bf97c555d-gw4k9 1/1 Running 192.168.88.61 ip-192-168-91-46
dnsutils 1/1 Running 192.168.57.174 ip-192-168-63-151
postgres-0 1/1 Running 192.168.70.170 ip-192-168-91-46
我们还可以检查节点端口,因为 dnsutils 正在我们的 VPC 内的 EC2 实例上运行,它可以在私有主机 ip-192-168-0-94.us-west-2.compute.internal 上进行 DNS 查询,并且 kubectl get service 命令给出了我们的节点端口,32671:
kubectl exec dnsutils -- wget -qO-
ip-192-168-0-94.us-west-2.compute.internal:32671/host
NODE: ip-192-168-0-94.us-west-2.compute.internal, POD IP:192.168.15.108
在我们的集群中,一切似乎在外部和本地都运行良好。
部署 ALB Ingress 并验证。
在部署的某些部分,我们需要知道我们正在部署的 AWS 账户 ID。让我们将其放入一个环境变量中。要获取您的账户 ID,您可以运行以下命令:
aws sts get-caller-identity
{
"UserId": "AIDA2RZMTHAQTEUI3Z537",
"Account": "1234567890",
"Arn": "arn:aws:iam::1234567890:user/eks"
}
export ACCOUNT_ID=1234567890
如果集群还没有设置,我们需要为集群设置 OIDC 提供程序。
这一步是为了给在集群中运行的 Pod 使用 SA 的 IAM 权限赋予 IAM 权限:
eksctl utils associate-iam-oidc-provider \
--region ${AWS_REGION} \
--cluster ${CLUSTER_NAME} \
--approve
对于 SA 角色,我们需要创建一个 IAM 策略,来确定 ALB 控制器在 AWS 中的权限:
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document iam_policy.json
现在我们需要创建 SA 并将其附加到我们创建的 IAM 角色:
eksctl create iamserviceaccount \
> --cluster ${CLUSTER_NAME} \
> --namespace kube-system \
> --name aws-load-balancer-controller \
> --attach-policy-arn
arn:aws:iam::${ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy \
> --override-existing-serviceaccounts \
> --approve
eksctl version 0.54.0
using region us-west-2
1 iamserviceaccount (kube-system/aws-load-balancer-controller) was included
(based on the include/exclude rules)
metadata of serviceaccounts that exist in Kubernetes will be updated,
as --override-existing-serviceaccounts was set
1 task: { 2 sequential sub-tasks: { create IAM role for serviceaccount
"kube-system/aws-load-balancer-controller", create serviceaccount
"kube-system/aws-load-balancer-controller" } }
building iamserviceaccount stack
deploying stack
waiting for CloudFormation stack
waiting for CloudFormation stack
waiting for CloudFormation stack
created serviceaccount "kube-system/aws-load-balancer-controller"
我们可以通过以下方法查看 SA 的所有细节:
kubectl get sa aws-load-balancer-controller -n kube-system -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn:
arn:aws:iam::1234567890:role/eksctl-eks-demo-addon-iamserviceaccount-Role1
creationTimestamp: "2021-06-27T18:40:06Z"
labels:
app.kubernetes.io/managed-by: eksctl
name: aws-load-balancer-controller
namespace: kube-system
resourceVersion: "16133"
uid: 30281eb5-8edf-4840-bc94-f214c1102e4f
secrets:
- name: aws-load-balancer-controller-token-dtq48
TargetGroupBinding CRD 允许控制器将 Kubernetes 服务端点绑定到 AWS 的TargetGroup:
kubectl apply -f crd.yml
customresourcedefinition.apiextensions.k8s.io/ingressclassparams.elbv2.k8s.aws
configured
customresourcedefinition.apiextensions.k8s.io/targetgroupbindings.elbv2.k8s.aws
configured
现在我们准备用 Helm 部署 ALB 控制器。
设置版本环境进行部署:
export ALB_LB_VERSION="v2.2.0"
现在部署它,添加eks Helm 仓库,获取集群运行的 VPC ID,最后通过 Helm 部署。
helm repo add eks https://aws.github.io/eks-charts
export VPC_ID=$(aws eks describe-cluster \
--name ${CLUSTER_NAME} \
--query "cluster.resourcesVpcConfig.vpcId" \
--output text)
helm upgrade -i aws-load-balancer-controller \
eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=${CLUSTER_NAME} \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set image.tag="${ALB_LB_VERSION}" \
--set region=${AWS_REGION} \
--set vpcId=${VPC_ID}
Release "aws-load-balancer-controller" has been upgraded. Happy Helming!
NAME: aws-load-balancer-controller
LAST DEPLOYED: Sun Jun 27 14:43:06 2021
NAMESPACE: kube-system
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
AWS Load Balancer controller installed!
我们可以在这里观看部署日志:
kubectl logs -n kube-system -f deploy/aws-load-balancer-controller
现在使用 ALB 部署我们的入口:
kubectl apply -f alb-rules.yml
ingress.networking.k8s.io/app configured
通过kubectl describe ing app输出,我们可以看到 ALB 已经部署。
我们还可以看到 ALB 的公共 DNS 地址、实例规则和支持服务的端点。
kubectl describe ing app
Name: app
Namespace: default
Address:
k8s-default-app-d5e5a26be4-2128411681.us-west-2.elb.amazonaws.com
Default backend: default-http-backend:80
(<error: endpoints "default-http-backend" not found>)
Rules:
Host Path Backends
---- ---- --------
*
/data clusterip-service:80 (192.168.3.221:8080,
192.168.44.165:8080,
192.168.89.224:8080)
/host clusterip-service:80 (192.168.3.221:8080,
192.168.44.165:8080,
192.168.89.224:8080)
Annotations: alb.ingress.kubernetes.io/scheme: internet-facing
kubernetes.io/ingress.class: alb
Events:
Type Reason Age From
Message
---- ------ ---- ----
-------
Normal SuccessfullyReconciled 4m33s (x2 over 5m58s) ingress
Successfully reconciled
是时候测试我们的 ALB 了!
wget -qO- k8s-default-app-d5e5a26be4-2128411681.us-west-2.elb.amazonaws.com/data
Database Connected
wget -qO- k8s-default-app-d5e5a26be4-2128411681.us-west-2.elb.amazonaws.com/host
NODE: ip-192-168-63-151.us-west-2.compute.internal, POD IP:192.168.44.165
清理工作:
当您完成与 EKS 的工作和测试后,请确保删除应用程序的 Pod 和服务,以确保一切都已删除:
kubectl delete -f dnsutils.yml,database.yml,web.yml
清理 ALB:
kubectl delete -f alb-rules.yml
删除 ALB 控制器的 IAM 策略:
aws iam delete-policy
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy
验证没有残留的 EBS 卷来自于测试应用程序的 PVC。删除任何发现的为 Postgres 测试数据库的 PVC 的 EBS 卷:
aws ec2 describe-volumes --filters
Name=tag:kubernetes.io/created-for/pv/name,Values=*
--query "Volumes[].{ID:VolumeId}"
验证没有运行的负载均衡器,无论是 ALB 还是其他类型的:
aws elbv2 describe-load-balancers --query "LoadBalancers[].LoadBalancerArn"
aws elb describe-load-balancers --query "LoadBalancerDescriptions[].DNSName"
让我们确保删除集群,以免因没有运行的集群而产生费用:
eksctl delete cluster --name ${CLUSTER_NAME}
我们部署了一个服务负载均衡器,每个服务都会在 AWS 中部署一个经典 ELB。ALB 控制器允许开发者使用 ALB 或 NLB 通过入口来外部暴露应用程序。如果我们要将应用程序扩展到多个后端服务,入口允许我们使用一个负载均衡器,并基于第 7 层信息进行路由。
在下一节中,我们将以与 AWS 相同的方式探索 GCP。
Google Compute Cloud(GCP)
2008 年,Google 宣布推出 App Engine,一个平台即服务,用于部署 Java、Python、Ruby 和 Go 应用程序。像其竞争对手一样,GCP 已扩展了其服务提供。云服务提供商致力于区分其产品,因此没有两个产品完全相同。尽管如此,许多产品确实有许多共同点。例如,GCP Compute Engine 是一个基础设施即服务,用于运行虚拟机。GCP 网络由 25 个云区域、76 个区域和 144 个网络边缘位置组成。利用 GCP 网络和 Compute Engine 的规模,GCP 推出了 Google Kubernetes Engine,其容器即服务平台。
GCP 网络服务:
在 GCP 上,托管和非托管的 Kubernetes 集群共享相同的网络原则。托管或非托管集群中的节点都作为 Google Compute Engine 实例运行。GCP 的网络是 VPC 网络。GCP VPC 网络像 AWS 一样,包含 IP 管理、路由、防火墙和对等连接功能。
GCP 网络分为不同的层级供客户选择;有高级和标准层级。它们在性能、路由和功能上有所不同,因此网络工程师必须决定哪种适合其工作负载。高级层级为您的工作负载提供最高性能。VPC 网络中所有与互联网和实例之间的流量尽可能通过 Google 网络路由。如果您的服务需要全球可用性,应使用高级层级。请记住,默认情况下高级层级是默认设置,除非您进行配置更改。
标准层级是一种成本优化的层级,互联网和 VPC 网络中的 VM 之间的流量通常通过互联网一般路由。网络工程师应为完全托管在一个区域内的服务选择此层级。标准层级不能保证性能,因为它受到所有工作负载在互联网上共享的性能的限制。
GCP 网络不同于其他提供商,其拥有所谓的全局资源。全局意味着用户可以在同一项目的任何区域访问这些资源。这些资源包括 VPC、防火墙及其路由等。
注意
查看GCP 文档以获取网络层的更全面概述。
区域和区域
区域是独立的地理区域,包含多个区域。区域资源通过在该区域的多个区域部署来提供冗余。区域是区域内资源的部署区域。一个区域通常是一个区域内的数据中心,管理员应将其视为单一故障域。在容错应用程序部署中,最佳实践是在区域内的多个区域部署应用程序,对于高可用性,应在各个区域部署应用程序。如果某个区域不可用,所有区域资源将不可用,直到所有者恢复服务。
虚拟专用云
VPC 是为 GCP 项目内的资源提供连接性的虚拟网络。与账户和订阅一样,项目可以包含多个 VPC 网络,默认情况下,新项目将使用默认自动模式 VPC 网络,并在每个区域包括一个子网。自定义模式 VPC 网络可以不包含子网。正如前文所述,VPC 网络是全局资源,不与任何特定区域或区域关联。
VPC 网络包含一个或多个区域子网。子网具有区域、CIDR 和全局唯一名称。您可以为子网使用任何 CIDR,包括与另一个私有地址空间重叠的 CIDR。特定选择的子网 CIDR 将影响您可以访问的 IP 地址和可以互连的网络。
警告
Google 创建了一个“默认”VPC 网络,为每个区域随机生成子网。一些子网可能与另一个 VPC 网络的子网重叠(例如另一个 Google Cloud 项目中的默认 VPC 网络),这将阻止对等连接。
VPC 网络支持对等连接和共享 VPC 配置。对等连接 VPC 网络允许一个项目中的 VPC 路由到另一个项目中的 VPC,使它们位于同一个 L3 网络上。您不能与任何重叠的 VPC 网络进行对等连接,因为某些 IP 地址存在于两个网络中。共享 VPC 允许另一个项目使用特定的子网,例如创建属于该子网的机器。VPC 文档提供更多信息。
小贴士
对等连接 VPC 网络是标准的,因为组织通常会将不同的团队、应用程序或组件分配给其 Google Cloud 项目。对等连接对于访问控制、配额和报告有益。一些管理员也可能在一个项目中创建多个 VPC 网络出于类似的原因。
子网
子网是 VPC 网络中的部分,具有一个主要 IP 范围和零个或多个次要范围的能力。子网是区域资源,每个子网定义了一系列 IP 地址。一个区域可以拥有多个子网。在创建子网时有两种模式:自动或自定义。当您创建自动模式的 VPC 网络时,将自动在其中的每个区域创建一个子网,使用预定义的 IP 范围。当您定义自定义模式的 VPC 网络时,GCP 不会提供任何子网,使管理员可以控制范围。自定义模式的 VPC 网络适用于企业和网络工程师在生产环境中使用。
Google Cloud 允许您为内部和外部 IP 地址“预留”静态 IP 地址。用户可以将保留的 IP 地址用于 GCE 实例、负载均衡器和我们的其他产品。保留的内部 IP 地址有一个名称,可以自动生成或手动分配。保留内部静态 IP 地址可以防止在不使用时随机自动分配。
保留外部 IP 地址类似;虽然您可以请求自动分配的 IP 地址,但您不能选择要保留的 IP 地址。因为您正在保留一个全球可路由的 IP 地址,在某些情况下会产生费用。您不能保护分配给您的外部 IP 地址作为短暂 IP 地址的自动分配。
路由和防火墙规则
部署 VPC 时,您可以使用防火墙规则根据部署的规则允许或拒绝与应用实例之间的连接。每个防火墙规则可以应用于入站或出站连接,但不能同时应用于两者。实例级别是 GCP 强制执行规则的地方,但配置与 VPC 网络配对,您不能在 VPC 网络之间共享防火墙规则,包括对等网络。VPC 防火墙规则是有状态的,因此当 TCP 会话启动时,防火墙规则允许类似 AWS 安全组的双向流量。
云负载均衡
Google Cloud 负载均衡器(GCLB)在 GCP 中提供全分布式、高性能、可扩展的负载均衡服务,具有各种负载均衡器选项。通过 GCLB,您可以获得一个 Anycast IP,它覆盖全球所有后端实例,包括多区域故障转移。此外,软件定义的负载均衡服务使您能够将负载均衡应用于您的 HTTP(S)、TCP/SSL 和 UDP 流量。您还可以使用 SSL 代理和 HTTPS 负载均衡终止 SSL 流量。内部负载均衡使您能够为内部实例构建高可用的内部服务,而无需将任何负载均衡器暴露到互联网上。
绝大多数 GCP 用户使用 GCP 的负载均衡器与 Kubernetes Ingress。GCP 提供内部和外部负载均衡器,支持 L4 和 L7。GKE 集群默认为 Ingress 和 type: LoadBalancer 服务创建 GCP 负载均衡器。
要将应用程序暴露在 GKE 集群外部,GKE 提供内置的 GKE Ingress 控制器和 GKE 服务控制器,代表 GKE 用户部署 Google Cloud 负载均衡器。GKE 提供三种不同的负载均衡器以控制访问并尽可能均匀地分布入站流量到您的集群中。您可以配置一个服务同时使用多种类型的负载均衡器:
外部负载均衡器
管理来自集群外部和 VPC 网络外部的流量。外部负载均衡器使用与 Google Cloud 网络关联的转发规则将流量路由到 Kubernetes 节点。
内部负载均衡器
管理来自同一 VPC 网络内部的流量。与外部负载均衡器类似,内部负载均衡器使用与 Google Cloud 网络关联的转发规则将流量路由到 Kubernetes 节点。
HTTP 负载均衡器
专门用于 HTTP 流量的外部负载均衡器。它们使用 Ingress 资源而不是转发规则将流量路由到 Kubernetes 节点。
当您创建一个入口对象时,GKE 入口控制器根据入口清单和相关的 Kubernetes 服务规则清单配置 Google Cloud HTTP(S)负载均衡器。客户端向负载均衡器发送请求。负载均衡器是一个代理;它选择一个节点并将请求转发到该节点的 NodeIP:NodePort 组合。节点使用其iptables NAT 表来选择一个 pod。正如我们在早期章节中学到的,kube-proxy在该节点上管理iptables规则。
当入口创建负载均衡器时,负载均衡器是“pod 感知的”,而不是路由到所有节点(并依赖服务将请求路由到 pod),负载均衡器会路由到单个 pod。它通过跟踪底层的Endpoints/EndpointSlice对象(如第五章中所述)并使用单个 pod IP 地址作为目标地址来实现这一点。
集群管理员可以使用集群内的入口提供者,例如 ingress-Nginx 或 Contour。在这种设置中,负载均衡器指向运行入口代理的适用节点,该代理从那里将请求路由到适用的 pod。对于具有许多入口的集群来说,这种设置更便宜,但会产生性能开销。
GCE 实例
GCE 实例具有一个或多个网络接口。网络接口具有网络和子网络、私有 IP 地址和公共 IP 地址。私有 IP 地址必须是子网的一部分。私有 IP 地址可以是自动和临时的、自定义和临时的或静态的。外部 IP 地址可以是自动和临时的或静态的。您可以向 GCE 实例添加更多网络接口。附加网络接口不需要在同一个 VPC 网络中。例如,您可能有一个实例,它在具有不同安全级别的两个 VPC 之间进行桥接。让我们讨论一下 GKE 如何使用这些实例并管理赋予 GKE 力量的网络服务。
GKE
Google Kubernetes Engine(GKE)是谷歌的托管 Kubernetes 服务。GKE 运行一个隐藏的控制平面,无法直接查看或访问。您只能访问特定的控制平面配置和 Kubernetes API。
GKE 公开了围绕诸如机器类型和集群扩展等方面的广泛集群配置。它仅显示一些与网络相关的设置。在撰写本文时,NetworkPolicy 支持(通过 Calico)、每个节点的最大 pod 数(kubelet 中的maxPods,kube-controller-manager中的--node-CIDR-mask-size)和 pod 地址范围(kube-controller-manager中的--cluster-CIDR)是可定制的选项。无法直接设置apiserver/kube-controller-manager标志。
GKE 支持公共和私有集群。私有集群不向节点分配公共 IP 地址,这意味着节点只能在您的私有网络内访问。私有集群还允许您将对 Kubernetes API 的访问限制为特定的 IP 地址。GKE 通过创建 节点池 来使用自动管理的 GCE 实例来运行工作节点。
GCP GKE 节点
GKE 节点的网络配置与在 GKE 上管理的 Kubernetes 集群的网络配置类似。GKE 集群定义 节点池,这是一组具有相同配置的节点。此配置包含 GCE 特定设置以及一般 Kubernetes 设置。节点池定义(虚拟)机器类型、自动缩放和 GCE 服务帐户。您还可以为每个节点池设置自定义污点和标签。
集群存在于一个 VPC 网络上。单个节点可以具有其网络标记,以制定特定的防火墙规则。任何运行版本为 1.16 或更高版本的 GKE 集群都将拥有 kube-proxy DaemonSet,因此集群中的所有新节点将自动启动 kube-proxy。子网的大小将影响集群的大小。因此,在部署可扩展的集群时,请注意子网大小。有一个公式可以用来计算给定 netmask 可支持的节点的最大数量 N,使用 S 表示 netmask 大小,其有效范围为 8 到 29:
N = 2(32 -S) - 4
计算所需的 netmask 大小 S,以支持最多 N 个节点:
S = 32 - ⌈log2(N + 4)⌉
表 6-2 还概述了集群节点和随子网大小如何缩放。
表 6-2. 随子网大小缩放的集群节点规模
| 子网主 IP 范围 | 最大节点数 |
|---|---|
| /29 | 子网主 IP 范围的最小大小:4 节点 |
| /28 | 12 节点 |
| /27 | 28 节点 |
| /26 | 60 节点 |
| /25 | 124 节点 |
| /24 | 252 节点 |
| /23 | 508 节点 |
| /22 | 1,020 节点 |
| /21 | 2,044 节点 |
| /20 | 在自动模式网络中子网主 IP 范围的默认大小:4,092 节点 |
| /19 | 8,188 节点 |
| /8 | 子网主 IP 范围的最大大小:16,777,212 节点 |
如果使用 GKE 的 CNI,veth 对中的一端附加到其命名空间中的 pod,并连接到 Linux 桥接设备 cbr0.1 的另一端,与我们在第 2 和 3 章节中概述的方式完全一致。
集群跨越区域或区域边界;区域集群仅具有单个控制平面的副本。当您部署集群时,GKE 有两种集群模式:VPC-native 和基于路由的。使用别名 IP 地址范围的集群称为 VPC-native 集群。在 VPC 网络中使用自定义静态路由的集群称为 基于路由的集群。表 6-3 概述了创建方法与集群模式的映射。
表 6-3. 使用集群创建方法的集群模式
| 集群创建方法 | 集群网络模式 |
|---|---|
| Google Cloud 控制台 | VPC 原生 |
| REST API | 基于路由 |
| gcloud v256.0.0 及更高或 v250.0.0 及更低 | 基于路由 |
| gcloud v251.0.0–255.0.0 | VPC 原生 |
当使用 VPC 原生时,管理员还可以利用网络端点组(NEG),表示由负载均衡器服务的一组后端。 NEGs 是由 NEG 控制器管理的 IP 地址列表,并由 Google Cloud 负载均衡器使用。 NEG 中的 IP 地址可以是虚拟机的主要或次要 IP 地址,这意味着它们可以是 pod IP。这使得容器原生负载均衡能够通过 Google Cloud 负载均衡器直接向 pod 发送流量。
VPC 原生集群有几个好处:
-
Pod IP 地址在集群的 VPC 网络内本地可路由。
-
在创建 pod 之前,网络中预留了 pod IP 地址。
-
Pod IP 地址范围依赖于自定义静态路由。
-
防火墙规则仅适用于集群节点上的 pod IP 地址范围,而不适用于任何 IP 地址。
-
GCP 云网络连接到本地的扩展到 pod IP 地址范围。
图 6-15 显示了 GKE 与 GCE 组件的映射。

图 6-15. NEG 到 GCE 组件的映射
这里是 NEGs 带给 GKE 网络的改进列表:
改善的网络性能
容器原生负载均衡器直接与 pod 进行通信,连接的网络跳数较少,延迟和吞吐量都得到了改善。
增强的可见性
使用容器原生负载均衡时,您可以查看从 HTTP 负载均衡器到 pod 的延迟。可以看到从 HTTP 负载均衡器到每个 pod 的延迟,这在基于节点 IP 的容器原生负载均衡中是汇总的。这增加的可见性使得在 NEG 级别更容易排除故障您的服务。
支持高级负载均衡
容器原生负载均衡为 GKE 提供了对多个 HTTP 负载均衡功能的原生支持,例如与 Google Cloud Armor、Cloud CDN 和身份验证代理的集成。它还具有用于准确流量分发的负载均衡算法。
与主要提供商的大多数托管 Kubernetes 提供的解决方案一样,GKE 与 Google Cloud 提供紧密集成。虽然驱动 GKE 的许多软件是不透明的,但它使用标准资源,如可以像其他 GCP 资源一样检查和调试的 GCE 实例。如果您确实需要管理自己的集群,您将错过一些功能,例如容器感知负载均衡。
值得注意的是,与 AWS 和 Azure 不同,GCP 尚不支持 IPv6。
最后,我们将看一下 Azure 上的 Kubernetes 网络。
Azure
就像其他云提供商一样,Microsoft Azure 提供了各种企业就绪的网络解决方案和服务。在讨论 Azure AKS 网络如何工作之前,我们应该讨论 Azure 部署模型。多年来,Azure 经历了一些重大迭代和改进,导致两种不同的部署模型,这些模型在资源部署和管理方式上存在差异,可能会影响用户如何利用这些资源。
第一个部署模型是经典部署模型。这个模型是 Azure 的初始部署和管理方法。所有资源都是独立存在的,无法逻辑分组。这很麻烦;用户必须为解决方案的每个组件创建、更新和删除,导致错误、遗漏资源以及额外的时间、精力和成本。最后,这些资源甚至无法轻松标记以便搜索,增加了解决方案的难度。
2014 年,微软推出了 Azure 资源管理器作为第二个模型。这种新模型是微软推荐的模型,甚至建议您应该使用 Azure 资源管理器(ARM)重新部署您的资源。这个模型的主要变化是引入了资源组。资源组是资源的逻辑分组,允许对资源进行跟踪、标记和组配置,而不是单独进行配置。
现在我们了解了如何在 Azure 中部署和管理资源的基础知识,我们可以讨论 Azure 网络服务提供的服务和与 Azure Kubernetes 服务(AKS)及非 Azure Kubernetes 服务的互动方式。
Azure 网络服务
Azure 网络服务的核心是虚拟网络,也称为 Azure Vnet。Vnet 建立了一个隔离的虚拟网络基础设施,用于连接您部署的 Azure 资源,如虚拟机和 AKS 集群。通过附加资源,Vnet 将您的部署资源连接到公共互联网以及您的本地基础设施。除非更改配置,所有 Azure Vnet 都可以通过默认路由与互联网通信。
在图 6-16 中,Azure Vnet 具有单个 CIDR 为192.168.0.0/16。像其他 Azure 资源一样,Vnets 需要订阅将 Vnet 放入资源组。可以配置 Vnet 的安全性,但某些选项(如 IAM 权限)是从资源组和订阅继承的。Vnet 限制在指定的区域内。一个区域内可以存在多个 Vnet,但一个 Vnet 只能存在于一个区域内。

图 6-16. Azure Vnet
Azure 骨干基础设施
Microsoft Azure 利用全球分布的数据中心和区域网络。这种分布的基础是 Azure 区域,它包括一组在延迟定义区域内的数据中心,通过低延迟的专用网络基础设施连接。一个区域可以包含任意数量符合这些标准的数据中心,但每个区域通常包含两到三个。包含至少一个 Azure 区域的任何区域都称为 Azure 地理位置。
可用区进一步划分区域。可用区是物理位置,可以包含由独立电源、冷却和网络基础设施维护的一个或多个数据中心。区域与其可用区的关系经过设计,以确保单个可用区故障不会使整个服务区域崩溃。区域中的每个可用区与该区域中的其他可用区连接,但不依赖于不同的区域。可用区使得 Azure 能够为支持的服务提供 99.99% 的可用性。一个区域可以包含多个可用区,如 图 6-17 所示,这些可用区可以进一步包含大量数据中心。

图 6-17. 区域
由于 Vnet 在区域内,而区域被划分为可用区,因此部署的 Vnet 也跨区域的可用区。如 图 6-18 所示,在部署基础设施以实现高可用性时,最佳做法是利用多个可用区进行冗余。可用区使得 Azure 能够为支持的服务提供 99.99% 的可用性。Azure 允许使用负载均衡器用于跨这些冗余系统的网络。

图 6-18. 具有可用区的 Vnet
注意
Azure 文档 提供了 Azure 地理位置、区域和可用区的最新列表。
子网
资源 IP 不直接从 Vnet 分配。而是子网划分并定义了 Vnet。子网从 Vnet 接收其地址空间。然后,在每个子网中为已配置的资源分配私有 IP。这是 AKS 集群和 pod 的 IP 地址分配的地方。与 Vnet 类似,Azure 子网跨可用区,如 图 6-19 所示。

图 6-19. 跨可用区的子网
路由表
正如前面所述,路由表管理子网通信或指示网络流量发送的方向数组。每个新创建的子网都配备有一个默认的路由表,其中包含一些默认的系统路由。这些路由无法删除或更改。系统路由包括到定义子网的 Vnet 的路由,以及默认设置为无效的10.0.0.0/8和192.168.0.0/16的路由,最重要的是到互联网的默认路由。互联网的默认路由允许任何新创建的具有 Azure IP 的资源默认与互联网通信。这种默认路由是 Azure 与某些其他云服务提供商之间的重要区别,需要足够的安全措施来保护每个 Azure Vnet。
图 6-20 显示了一个新创建的 AKS 设置的标准路由表。其中包括代理池的路由及其 CIDR 和它们的下一跳 IP。下一跳 IP 是路由表为该路径定义的下一跳,下一跳类型设置为虚拟设备,这在此案例中是负载均衡器。这些默认系统路由并不在路由表中显示。理解 Azure 的默认网络行为对于安全、故障排除和规划至关重要。

图 6-20. 路由表
某些系统路由称为可选的默认路由,仅在启用功能(如 Vnet 对等连接)时才会影响。Vnet 对等连接允许全球任何位置的 Vnet 通过 Azure 全局基础设施骨干建立私有连接以进行通信。
自定义路由还可以填充路由表,边界网关协议可以创建或使用用户定义的路由。用户定义的路由非常重要,因为它们允许网络管理员定义超出 Azure 默认建立的路由,例如代理或防火墙路由。自定义路由还会影响系统默认路由。虽然无法更改默认路由,但具有更高优先级的客户路由可以覆盖它。一个例子是使用用户定义的路由将流向互联网的流量发送到虚拟防火墙设备的下一跳,而不是直接发送到互联网。图 6-21 定义了一个名为 Google 的自定义路由,其下一跳类型为互联网。只要设置优先级正确,这个自定义路由将把流量发送到互联网的默认系统路由,即使其他规则重定向剩余的互联网流量。

图 6-21. 带有自定义路由的路由表
路由表也可以单独创建,然后用于配置子网。这对于为多个子网维护单个路由表特别有用,尤其是涉及许多用户定义的路由时。一个子网只能关联一个路由表,但一个路由表可以关联多个子网。配置用户创建的路由表和作为子网默认创建的路由表的规则是相同的。它们具有相同的默认系统路由,并且将随着生效而更新相同的可选默认路由。
虽然路由表中的大多数路由将使用 IP 范围作为源地址,但 Azure 已经开始引入使用服务标记作为源的概念。服务标记是表示 Azure 后端内的一组服务 IP 的短语,例如 SQL.EastUs,这是一个描述美国东部 Microsoft SQL 平台服务提供的 IP 地址范围的服务标记。通过这个功能,可能可以定义一个从一个 Azure 服务(如 AzureDevOps)到另一个服务(如 Azure AppService)的路由,而不需要知道任何 IP 范围。
注意
Azure 文档中列出了可用的服务标记列表。
公共和私有 IP 地址
Azure 将 IP 地址分配为独立的资源,这意味着用户可以创建公共 IP 或私有 IP 而不将其附加到任何内容。这些 IP 地址可以命名并构建在允许未来分配的资源组中。这是准备 AKS 集群扩展的关键步骤,因为您希望确保为可能的 pod 保留足够的私有 IP 地址,如果决定利用 Azure CNI 进行网络连接。Azure CNI 将在后面的部分中讨论。
IP 地址资源,包括公共和私有 IP 地址,也被定义为动态或静态。静态 IP 地址保留不变,而动态 IP 地址如果未分配给资源(如虚拟机或 AKS pod)则可以更改。
网络安全组
NSG 用于配置虚拟网络(Vnets)、子网和网络接口卡(NIC),具有入站和出站安全规则。这些规则过滤流量,并确定流量是否允许继续传输或被丢弃。NSG 规则灵活,可以根据源 IP 地址、目标 IP 地址、网络端口和网络协议来过滤流量。一个 NSG 规则可以使用一个或多个这些过滤项,并且可以应用多个 NSG。
NSG 规则可以具有以下任何组件来定义其过滤:
优先级
这是 100 到 4096 之间的数字。数字越小,优先级越高,第一个匹配的规则将被使用。一旦找到匹配项,将不再评估其他规则。
源/目标
被检查流量的源(入站规则)或目标(出站规则)。源/目标可以是以下任何一种:
-
单个 IP 地址
-
CIDR 块(即,10.2.0.0/24)
-
Microsoft Azure 服务标记
-
应用安全组
协议
TCP、UDP、ICMP、ESP、AH 或 Any。
方向
入站或出站流量的规则。
端口范围
可以在此处指定单个端口或范围。
操作
允许或拒绝流量。
图 6-22 显示了一个 NSG 的示例。

图 6-22. Azure NSG
在配置 Azure 网络安全组时需要注意几点。首先,不能存在两个或更多具有相同优先级和方向的规则。只要优先级或方向匹配,就可以进行匹配,但其他方面则不能。其次,在资源管理器部署模型中可以使用端口范围,但在经典部署模型中不能。此限制还适用于源/目标的 IP 地址范围和服务标记。第三,在指定 Azure 资源作为源/目标的 IP 地址时,如果资源同时具有公共和私有 IP 地址,则应使用私有 IP 地址。Azure 在此过程之外执行从公共到私有 IP 地址的转换,因此在处理时选择私有 IP 地址是正确的选择。
虚拟网络之外的通信
到目前为止描述的概念主要涉及单个 Vnet 内的 Azure 网络。这种类型的通信在 Azure 网络中至关重要,但远非唯一类型。大多数 Azure 实施需要与虚拟网络之外的其他网络进行通信,包括但不限于本地网络、其他 Azure 虚拟网络和互联网。这些通信路径需要与内部网络处理相同的许多考虑因素,并使用许多相同的资源,但也有一些不同之处。本节将扩展一些这些差异。
Vnet 互连可以使用全局虚拟网络互连将位于不同区域的 Vnet 连接起来,但在某些服务(如负载均衡器)上存在约束。
注意
有关这些约束的列表,请参阅 Azure 文档。
到互联网的 Azure 外部通信使用不同的资源集。如前所述,公共 IP 可以在 Azure 中创建并分配给资源。资源在 Azure 内部网络中使用其私有 IP 地址进行所有网络通信。当来自资源的流量需要从内部网络退出到互联网时,Azure 将私有 IP 地址转换为资源分配的公共 IP。此时,流量可以离开到互联网。针对 Azure 资源的公共 IP 地址的传入流量在 Vnet 边界处将其转换为资源分配的私有 IP 地址,并从此使用私有 IP 地址完成其余通向目标的流量。这种流量路径是为什么诸如 NSG 的所有子网规则都使用私有 IP 地址定义的原因。
NAT 也可以在子网上配置。如果配置了,具有启用 NAT 的子网上的资源无需公共 IP 地址即可与互联网通信。NAT 在子网上启用,以允许仅出站的互联网流量,使用从预配置的公共 IP 地址池中获取的公共 IP。NAT 将使资源能够路由到互联网以进行更新或安装等请求,并返回所请求的流量,但防止这些资源对互联网可访问。需要注意的是,当配置了 NAT 时,它将优先于所有其他出站规则,并替换子网的默认互联网目的地。NAT 还默认使用端口地址转换(PAT)。
Azure 负载均衡器
现在您有了在网络外部进行通信并使通信回流到 Vnet 的方法,需要一种方式来保持这些通信线路可用。Azure 负载均衡器通常用于通过将流量分发到后端资源池而不是单个资源来实现此目的。Azure 中有两种主要的负载均衡器类型:标准负载均衡器和应用网关。
Azure 标准负载均衡器是第 4 层系统,根据诸如 TCP 和 UDP 的第 4 层协议分发传入流量,这意味着流量基于 IP 地址和端口路由。这些负载均衡器过滤来自互联网的传入流量,但也可以将来自一个 Azure 资源的流量负载均衡到一组其他 Azure 资源。标准负载均衡器使用零信任网络模型。该模型要求 NSG 打开要由负载均衡器检查的流量。如果附加的 NSG 不允许该流量,则负载均衡器将不尝试路由该流量。
Azure 应用网关与标准负载均衡器类似,它们分发传入流量,但不同之处在于它们在第 7 层执行此操作。这允许检查传入的 HTTP 请求,以便基于 URI 或主机头部进行过滤。应用网关还可以用作 Web 应用程序防火墙,以进一步安全和过滤流量。此外,应用网关还可用作 AKS 集群的入口控制器。
负载均衡器,无论是标准还是应用网关,都具有一些基本概念,应予考虑:
前端 IP 地址
根据使用情况可为公共或私有 IP 地址,此 IP 地址用于定位负载均衡器及其平衡的后端资源。
SKU
像其他 Azure 资源一样,这定义了负载均衡器的“类型”,因此也定义了可用的不同配置选项。
后端池
这是负载均衡器分发流量的资源集合,例如一组虚拟机或 AKS 集群中的 Pod。
健康探针
这些是负载均衡器用于确保后端资源可用于流量的方法,例如返回 OK 状态的健康端点:
监听器
一个配置,告诉负载均衡器期望的流量类型,例如 HTTP 请求。
规则
确定如何路由该监听器的传入流量。
图 6-23 展示了 Azure 负载均衡器架构中的一些主要组件。流量进入负载均衡器并与监听器进行比较,以确定负载均衡器是否平衡流量。然后根据规则评估流量,最终发送到后端池。具有适当响应健康探测的后端池资源将处理流量。

图 6-23. Azure 负载均衡器组件
图 6-24 展示了 AKS 如何使用负载均衡器。
现在我们已经对 Azure 网络有了基本了解,我们可以讨论 Azure 如何在其托管 Kubernetes 提供中使用这些构建,即 Azure Kubernetes 服务(AKS)。

图 6-24. AKS 负载均衡
Azure Kubernetes 服务
与其他云提供商一样,微软理解了利用 Kubernetes 的力量的必要性,因此推出了 Azure Kubernetes 服务作为 Azure Kubernetes 提供。AKS 是 Azure 的托管服务提供,因此处理大部分管理 Kubernetes 的开销。Azure 处理健康监控和维护等组件,使开发和运维工程师有更多时间利用 Kubernetes 的可扩展性和强大功能来解决问题。
AKS 可以使用 Azure CLI、Azure PowerShell、Azure 门户以及其他基于模板的部署选项(如 ARM 模板和 HashiCorp 的 Terraform)来创建和管理集群。在 AKS 中,Azure 管理 Kubernetes 主节点,因此用户只需处理节点代理。这使得 Azure 能够将 AKS 的核心作为免费服务提供,用户只需支付节点代理和存储、网络等外围服务的费用。
Azure 门户允许轻松管理和配置 AKS 环境。图 6-25 显示了新配置的 AKS 环境的概述页面。在此页面上,您可以看到许多关键集成和属性的信息和链接。在基本信息部分中可见集群的资源组、DNS 地址、Kubernetes 版本、网络类型以及节点池的链接。
图 6-26 放大了概述页面的属性部分,用户可以在此找到额外的信息和相应组件的链接。大部分数据与基本信息部分中的信息相同。但是,可以在此查看 AKS 环境组件的各种子网 CIDR,例如 Docker 桥接和 pod 子网。

图 6-25. Azure 门户 AKS 概述

图 6-26. Azure 门户 AKS 属性
在 AKS 内创建的 Kubernetes pod 附加到虚拟网络,并可以通过抽象访问网络资源。每个 AKS 节点上的 kube-proxy 创建此抽象,此组件允许入站和出站流量。此外,AKS 通过简化如何对虚拟网络进行变更来使 Kubernetes 管理更加流畅。在特定变更发生时,AKS 会自动配置网络服务。例如,向 pod 打开网络端口也会触发相应的更改以打开这些端口所附的 NSG。
默认情况下,AKS 将创建一个具有公共 IP 的 Azure DNS 记录。但默认网络规则阻止了公共访问。私有模式可以创建集群以使用没有公共 IP 的方式,并且仅允许集群内部使用来阻止公共访问。此模式将使集群仅能从 Vnet 内部访问。默认情况下,标准 SKU 将创建一个 AKS 负载均衡器。如果通过 CLI 部署,则可以在部署期间更改此配置。未包含在集群中的资源将在单独生成的资源组中创建。
当在 AKS 中利用 kubenet 网络模型时,以下规则为真:
-
节点从 Azure 虚拟网络子网接收 IP 地址。
-
Pod 从逻辑上不同的地址空间中接收 IP 地址,而不是从节点中。
-
流量的源 IP 地址会切换到节点的主要地址。
-
为了使 pod 能够在 Vnet 上访问 Azure 资源,已配置 NAT。
需要注意的是,只有节点会接收可路由的 IP;而 pod 不会。
虽然 kubenet 是在 Azure Kubernetes 服务内部管理 Kubernetes 网络的一种简单方式,但并非唯一方式。与其他云提供商一样,Azure 在管理 Kubernetes 基础设施时也允许使用 CNI。我们在下一节来讨论 CNI。
Azure CNI
Microsoft 为 Azure 和 AKS 提供了自己的 CNI 插件,即 Azure CNI。这与 kubenet 的一个显著区别是,pod 接收可路由的 IP 信息,并且可以直接访问。这种差异增加了 IP 地址空间规划的重要性。每个节点可以使用的 pod 的最大数量和为此使用保留的许多 IP 地址。
注意
Azure 容器网络的更多信息可以在 GitHub 上找到。
使用 Azure CNI 后,Vnet 内部的流量不再通过节点的 IP 地址进行 NAT 转发,而是直接到 pod 的 IP 地址本身,正如 图 6-27 所示。外部流量(例如互联网流量)仍然会经过节点的 IP 地址进行 NAT 转发。Azure CNI 仍然为这些项执行后端 IP 地址管理和路由,因为在同一个 Azure Vnet 上的所有资源默认可以相互通信。
Azure CNI 也可以用于 AKS 之外的 Kubernetes 部署。虽然需要在 Azure 通常会处理的集群上进行额外的工作,但这使您可以利用 Azure 网络和其他资源,同时保持对 AKS 下通常管理的 Kubernetes 方面的更多控制。

图 6-27. Azure CNI
Azure CNI 还提供了一个额外的好处,即在保持 AKS 基础架构的同时分离职责。Azure CNI 在单独的资源组中创建网络资源。处于不同的资源组中可以更好地控制 Azure 资源管理部署模型中资源组级别的权限。不同的团队可以访问 AKS 的某些组件,如网络,而无需访问应用程序部署等其他组件。
Azure CNI 并非利用额外 Azure 服务增强 Kubernetes 网络基础架构的唯一方式。接下来的部分将讨论使用 Azure 应用网关作为控制入口到 Kubernetes 集群的手段。
应用网关入口控制器
Azure 允许在 AKS 集群部署内部部署应用网关作为应用网关入口控制器 (AGIC)。这种部署模型消除了在 AKS 基础架构外部维护辅助负载均衡器的需要,从而减少了维护开销和错误点。AGIC 在集群中部署其 Pod。然后,它监视集群的其他方面以进行配置更改。当检测到变更时,AGIC 更新 Azure 资源管理器模板,配置负载均衡器,然后应用更新后的配置。图 6-28 说明了这一过程。

图 6-28. Azure AGIC
对于 AGIC 的使用,AKS SKU 有限制,仅支持 Standard_v2 和 WAF_v2,但这些 SKU 还具有自动缩放能力。使用这种形式的入口的用例,如需要高可扩展性,有潜力让 AKS 环境扩展。Microsoft 支持使用 Helm 和 AKS 插件作为 AGIC 的部署选项。这些是两种选项之间的关键区别:
-
使用 AKS 插件时,无法编辑 Helm 部署值。
-
Helm 支持被禁止目标配置。AGIC 可以配置应用网关仅针对 AKS 实例,而不影响其他后端组件。
-
作为托管服务的 AKS 插件将自动更新到当前版本和更安全的版本。Helm 部署将需要手动更新。
即使 AGIC 被配置为 Kubernetes 入口资源,它仍然能够带来整个集群标准的第 7 层应用程序网关的全部优势。应用程序网关服务,如 TLS 终结、URL 路由和 Web 应用程序防火墙功能,都可以作为 AGIC 的一部分配置到集群中。
尽管许多 Kubernetes 和网络基础在各云服务提供商之间是通用的,但 Azure 通过其面向企业的资源设计和管理,为 Kubernetes 网络提供了自己的特色。无论您是需要一个使用基本设置和 kubenet 的单个集群,还是通过部署的负载均衡器和应用程序网关实现高级网络的大规模部署,微软的 Azure Kubernetes 服务都可以提供可靠的托管 Kubernetes 基础设施。
在 Azure Kubernetes 服务上部署应用程序
部署 Azure Kubernetes 服务集群是开始探索 AKS 网络的基本技能之一。本节将介绍如何创建一个示例集群,并将来自 第一章 的 Golang Web 服务器示例部署到该集群。我们将使用 Azure 门户、Azure CLI 和 kubectl 的组合来执行这些操作。
在开始集群部署和配置之前,我们应该讨论 Azure 容器注册表 (ACR)。ACR 是您在 Azure 中存储容器镜像的位置。在本例中,我们将使用 ACR 作为将要部署的容器镜像的位置。要将图像导入到 ACR,您需要将图像在本地计算机上可用。一旦图像可用,我们就需要为 ACR 准备好它。
首先,识别您想要将图像存储在的 ACR 仓库,并使用 docker login <acr_repository>.azurecr.io 从 Docker CLI 登录。在本例中,我们将使用 ACR 仓库 tjbakstestcr,因此命令将是 docker login tjbakstestcr.azurecr.io。接下来,使用 <acr_repository>.azurecr.io\<imagetag> 对本地想要导入到 ACR 的图像进行标记。在本例中,我们将使用当前标记为 aksdemo 的图像。因此,标记将是 tjbakstestcr.azure.io/aksdemo。要标记图像,请使用命令 docker tag <local_image_tag> <acr_image_tag>。本示例将使用命令 docker tag aksdemo tjbakstestcr.azure.io/aksdemo。最后,使用 docker push tjbakstestcr.azure.io/aksdemo 将图像推送到 ACR。
注意
您可以在官方 文档 中找到有关 Docker 和 Azure 容器注册表的额外信息。
一旦镜像位于 ACR 中,最后一个先决条件是设置一个服务主体。在开始之前设置这一点更容易,但您也可以在创建 AKS 集群期间执行此操作。Azure 服务主体是 Azure Active Directory 应用程序对象的表示。服务主体通常用于通过应用程序自动化与 Azure 交互。我们将使用服务主体允许 AKS 集群从 ACR 拉取 aksdemo 镜像。服务主体需要访问您存储图像的 ACR 仓库。您需要记录您要使用的服务主体的客户端 ID 和密钥。
注意
在 文档 中可以找到有关 Azure Active Directory 服务主体的额外信息。
现在,我们在 ACR 中有我们的镜像和服务主体的客户端 ID 和密钥,可以开始部署 AKS 集群。
部署 Azure Kubernetes Service 集群
现在是部署我们的集群的时候了。我们将从 Azure 门户开始。转到 portal.azure.com 登录。登录后,您应该看到一个仪表板,顶部有一个搜索栏,用于定位服务。从搜索栏中,我们将键入 kubernetes 并从下拉菜单中选择 Kubernetes 服务选项,如 图 6-29 所示。

图 6-29. Azure Kubernetes 搜索
现在我们在 Azure Kubernetes 服务页面上。使用过滤器和查询查看部署的 AKS 集群。这也是创建新 AKS 集群的屏幕。在屏幕顶部附近,我们将选择 Create,如 图 6-30 所示。这将导致一个下拉菜单出现,在其中我们将选择“创建 Kubernetes 集群”。

图 6-30. 创建 Azure Kubernetes 集群
接下来,我们将从“创建 Kubernetes 集群”屏幕中定义 AKS 集群的属性。首先,我们将通过选择将部署集群的订阅来填充“项目详细信息”部分。有一个下拉菜单,可以更轻松地搜索和选择。在本例中,我们使用 tjb_azure_test_2 订阅,但只要您有访问权限,任何订阅都可以使用。接下来,我们必须定义将用于分组 AKS 集群的资源组。这可以是现有的资源组,也可以是新建的。在本例中,我们将创建一个名为 go-web 的新资源组。
完成项目详情部分后,我们将转到集群详情部分。在这里,我们将定义集群的名称,例如本示例中将为“go-web”。区域、可用性区域和 Kubernetes 版本字段也在此部分定义,并将具有可以更改的预定义默认值。然而,对于本例,我们将使用默认的“(US) West 2”区域,没有可用性区域,并且默认的 Kubernetes 版本为 1.19.11。
注意
不是所有的 Azure 区域都有可选择的可用性区域。如果可用性区域是部署的 AKS 架构的一部分,应考虑适当的区域。您可以在可用性区域的文档中找到更多关于 AKS 区域的信息。
最后,我们将通过选择节点大小和节点数来完成“创建 Kubernetes 集群”屏幕的主节点池部分。例如,我们将保持 DS2 v2 的默认节点大小和 3 个默认节点数。虽然大多数虚拟机大小在 AKS 中可供使用,但也有一些限制。图 6-31 显示了我们已经选择填写的选项。
注意
您可以在文档中找到更多关于 AKS 限制的信息,包括受限节点大小。
单击“下一步:节点池”按钮以转到节点池选项卡。此页面允许为 AKS 集群配置额外的节点池。例如,我们将在此页面保留默认设置,并通过点击屏幕底部的“下一步:认证”按钮转到认证页面。

图 6-31. Azure Kubernetes 创建页面
图 6-32 显示了认证页面,我们将在此页面定义 AKS 集群连接到附加的 Azure 服务(如我们在本章前面讨论过的 ACR)所使用的认证方法。“系统分配的托管标识”是默认的认证方法,但我们将选择“服务主体”单选按钮。
如果您在本节开头没有创建服务主体,可以在此处创建一个新的服务主体。如果在此阶段创建服务主体,则必须返回并授予该服务主体访问 ACR 的权限。然而,由于我们将使用先前创建的服务主体,因此我们将点击“配置服务主体”链接并输入客户端 ID 和密钥。

图 6-32. Azure Kubernetes 认证页面
其余配置将暂时保持默认设置。要完成 AKS 集群的创建,我们将点击“Review + create”按钮。这将带我们到验证页面。如 Figure 6-33 所示,如果一切都被正确定义,验证将在屏幕顶部返回一个“Validation Passed”消息。如果有配置错误,则会显示“Validation Failed”消息。只要验证通过,我们将审查设置并点击 Create。

Figure 6-33. Azure Kubernetes 验证页面
您可以从 Azure 屏幕顶部的通知钟查看部署状态。Figure 6-34 显示了我们示例部署正在进行中的情况。此页面包含可用于与 Microsoft 进行故障排除的信息,例如部署名称、开始时间和关联 ID。
我们的示例完全没有问题地部署完成,如 Figure 6-35 所示。现在 AKS 集群已部署,我们需要连接并配置它以便与我们的示例 Web 服务器一起使用。

Figure 6-34. Azure Kubernetes 部署进度

Figure 6-35. Azure Kubernetes 部署完成
连接和配置 AKS
我们现在将转向从命令行操作示例go-web AKS 集群。要从命令行管理 AKS 集群,我们将主要使用kubectl命令。Azure CLI 有一个简单的命令az aks install-cli,用于安装kubectl程序以便使用。不过,在使用kubectl之前,我们需要访问集群。命令az aks get-credentials --resource-group <resource_group_name> --name <aks_cluster_name>用于访问 AKS 集群。对于我们的示例,我们将使用az aks get-credentials --resource-group go-web --name go-web来访问我们的go-web资源组中的go-web集群。
接下来,我们将附加包含我们的aksdemo镜像的 Azure 容器注册表。命令az aks update -n <aks_cluster_name> -g <cluster_resource_group_name> --attach-acr <acr_repo_name>将命名的 ACR 存储库附加到现有的 AKS 集群。对于我们的示例,我们将使用命令az aks update -n tjbakstest -g tjbakstest --attach-acr tjbakstestcr。我们的示例运行片刻后,将生成 Example 6-1 中显示的输出。
Example 6-1. AttachACR 输出
{- Finished ..
"aadProfile": null,
"addonProfiles": {
"azurepolicy": {
"config": null,
"enabled": false,
"identity": null
},
"httpApplicationRouting": {
"config": null,
"enabled": false,
"identity": null
},
"omsAgent": {
"config": {
"logAnalyticsWorkspaceResourceID":
"/subscriptions/7a0e265a-c0e4-4081-8d76-aafbca9db45e/
resourcegroups/defaultresourcegroup-wus2/providers/
microsoft.operationalinsights/
workspaces/defaultworkspace-7a0e265a-c0e4-4081-8d76-aafbca9db45e-wus2"
},
"enabled": true,
"identity": null
}
},
"agentPoolProfiles": [
{
"availabilityZones": null,
"count": 3,
"enableAutoScaling": false,
"enableNodePublicIp": null,
"maxCount": null,
"maxPods": 110,
"minCount": null,
"mode": "System",
"name": "agentpool",
"nodeImageVersion": "AKSUbuntu-1804gen2containerd-2021.06.02",
"nodeLabels": {},
"nodeTaints": null,
"orchestratorVersion": "1.19.11",
"osDiskSizeGb": 128,
"osDiskType": "Managed",
"osType": "Linux",
"powerState": {
"code": "Running"
},
"provisioningState": "Succeeded",
"proximityPlacementGroupId": null,
"scaleSetEvictionPolicy": null,
"scaleSetPriority": null,
"spotMaxPrice": null,
"tags": null,
"type": "VirtualMachineScaleSets",
"upgradeSettings": null,
"vmSize": "Standard_DS2_v2",
"vnetSubnetId": null
}
],
"apiServerAccessProfile": {
"authorizedIpRanges": null,
"enablePrivateCluster": false
},
"autoScalerProfile": null,
"diskEncryptionSetId": null,
"dnsPrefix": "go-web-dns",
"enablePodSecurityPolicy": null,
"enableRbac": true,
"fqdn": "go-web-dns-a59354e4.hcp.westus.azmk8s.io",
"id":
"/subscriptions/7a0e265a-c0e4-4081-8d76-aafbca9db45e/
resourcegroups/go-web/providers/Microsoft.ContainerService/managedClusters/go-web",
"identity": null,
"identityProfile": null,
"kubernetesVersion": "1.19.11",
"linuxProfile": null,
"location": "westus",
"maxAgentPools": 100,
"name": "go-web",
"networkProfile": {
"dnsServiceIp": "10.0.0.10",
"dockerBridgeCidr": "172.17.0.1/16",
"loadBalancerProfile": {
"allocatedOutboundPorts": null,
"effectiveOutboundIps": [
{
"id":
"/subscriptions/7a0e265a-c0e4-4081-8d76-aafbca9db45e/
resourceGroups/MC_go-web_go-web_westus/providers/Microsoft.Network/
publicIPAddresses/eb67f61d-7370-4a38-a237-a95e9393b294",
"resourceGroup": "MC_go-web_go-web_westus"
}
],
"idleTimeoutInMinutes": null,
"managedOutboundIps": {
"count": 1
},
"outboundIpPrefixes": null,
"outboundIps": null
},
"loadBalancerSku": "Standard",
"networkMode": null,
"networkPlugin": "kubenet",
"networkPolicy": null,
"outboundType": "loadBalancer",
"podCidr": "10.244.0.0/16",
"serviceCidr": "10.0.0.0/16"
},
"nodeResourceGroup": "MC_go-web_go-web_westus",
"powerState": {
"code": "Running"
},
"privateFqdn": null,
"provisioningState": "Succeeded",
"resourceGroup": "go-web",
"servicePrincipalProfile": {
"clientId": "bbd3ac10-5c0c-4084-a1b8-39dd1097ec1c",
"secret": null
},
"sku": {
"name": "Basic",
"tier": "Free"
},
"tags": {
"createdby": "tjb"
},
"type": "Microsoft.ContainerService/ManagedClusters",
"windowsProfile": null
}
此输出是 AKS 集群信息的 CLI 表示。这意味着附加成功。现在我们可以访问 AKS 集群并附加了 ACR 后,可以将示例 Go Web 服务器部署到 AKS 集群上。
部署 Go Web 服务器
我们将部署示例 6-2 中显示的 Golang 代码 Example 6-2。正如本章前面提到的,此代码已构建为 Docker 镜像,并存储在 tjbakstestcr 仓库的 ACR 中。我们将使用以下部署 YAML 文件来部署应用程序。
示例 6-2. Golang 极简 Web 服务器的 Kubernetes Podspec
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: go-web
spec:
containers:
- name: go-web
image: go-web:v0.0.1
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
解析这个 YAML 文件,我们可以看到我们正在创建两个 AKS 资源:一个部署(deployment)和一个服务(service)。部署被配置为创建一个名为go-web的容器,以及一个容器端口 8080。部署还引用了aksdemo ACR 镜像,具体在这行 image: tjbakstestcr.azurecr.io/aksdemo 中指定将部署到容器中的镜像。服务也被配置为名为 go-web。YAML 指定服务是一个负载均衡器,监听端口 8080 并指向go-web应用。
现在我们需要将应用程序发布到 AKS 集群。命令kubectl apply -f <yaml_file_name>.yaml将应用程序发布到集群。从输出中我们可以看到两个东西被创建:deployment.apps/go-web和service/go-web。当我们运行命令kubectl get pods时,我们可以看到如下输出:
○ → kubectl get pods
NAME READY STATUS RESTARTS AGE
go-web-574dd4c94d-2z5lp 1/1 Running 0 5h29m
现在应用程序已部署,我们将连接到它以验证其正常运行。当默认的 AKS 集群启动时,会随之部署一个带有公共 IP 地址的负载均衡器。我们可以通过门户找到该负载均衡器和公共 IP 地址,但是 kubectl 提供了一条更简单的路径。命令 kubectl get [.keep-together]#service go-web 生成了这样的输出:
○ → kubectl get service go-web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
go-web LoadBalancer 10.0.3.75 13.88.96.117 8080:31728/TCP 21h
在这个输出中,我们看到外部 IP 地址为 13.88.96.117。因此,如果一切部署正确,我们应该能够使用命令 curl 13.88.96.117:8080 来 cURL 13.88.96.117 的端口 8080。正如我们从这个输出中看到的,我们已经成功部署了:
○ → curl 13.88.96.117:8080 -vvv
* Trying 13.88.96.117...
* TCP_NODELAY set
* Connected to 13.88.96.117 (13.88.96.117) port 8080 (#0)
> GET / HTTP/1.1
> Host: 13.88.96.117:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 25 Jun 2021 20:12:48 GMT
< Content-Length: 5
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 13.88.96.117 left intact
Hello* Closing connection 0
进入网页浏览器并导航到 http://13.88.96.117:8080 也是可以的,如图所示 Figure 6-36。

图 6-36. Azure Kubernetes Hello 应用程序
AKS 结论
在本节中,我们将一个示例 Golang web 服务器部署到 Azure Kubernetes Service 集群。我们使用了 Azure 门户,az cli 和 kubectl 来部署和配置集群,然后部署应用程序。我们利用了 Azure 容器注册表来托管我们的 Web 服务器镜像。我们还使用了一个 YAML 文件来部署应用程序,并用 cURL 和 Web 浏览进行了测试。
结论
当涉及到为 Kubernetes 集群提供网络服务时,每个云提供商都有其微妙的差异。Table 6-4 强调了其中一些差异。在选择云服务提供商和运行的托管 Kubernetes 平台时,有很多因素可以选择。本章的目标是教育管理员和开发人员,在管理 Kubernetes 上的工作负载时需要做出的选择。
表 6-4. 云网络和 Kubernetes 总结
| AWS | Azure | GCP | |
|---|---|---|---|
| 虚拟网络 | VPC | Vnet | VPC |
| 网络范围 | 区域 | 区域 | 全球 |
| 子网边界 | 区域 | 区域 | 区域 |
| 路由范围 | 子网 | 子网 | VPC |
| 安全控制 | NACL/SecGroups | 网络安全组/应用程序安全组 | 防火墙 |
| IPv6 | 是 | 是 | 否 |
| Kubernetes 管理 | eks | aks | gke |
| 入口 | AWS ALB 控制器 | Nginx-Ingress | GKE 入口控制器 |
| 云自定义 CNI | AWS VPC CNI | Azure CNI | GKE CNI |
| 负载均衡器支持 | ALB L7、L4 w/NLB 和 Nginx | L4 Azure 负载均衡器、L7 w/Nginx | L7、HTTP(S) |
| 网络策略 | 是(Calico/Cilium) | 是(Calico/Cilium) | 是(Calico/Cilium) |
我们涵盖了许多层次,从 OSI 基础到为我们的集群在云中运行的网络。集群管理员、网络工程师和开发人员都需要做出许多决策,比如子网大小、选择的 CNI 以及负载均衡器类型等。理解所有这些以及它们对集群网络的影响是本书的基础。这只是你在大规模管理集群旅程的开始。我们已经涵盖了管理 Kubernetes 集群可用的网络选项。存储、计算甚至如何将工作负载部署到这些集群上都是你现在需要做出的决策。O'Reilly 图书馆有大量的书籍可以帮助,如生产 Kubernetes(Rosso 等著)中,你将了解在使用 Kubernetes 时通向生产环境的路径,以及黑客攻击 Kubernetes(Martin 和 Hausenblas 著),介绍如何加固 Kubernetes 并审查 Kubernetes 集群中的安全弱点。
希望本指南能帮助您轻松做出这些网络选择。我们受到 Kubernetes 社区的启发,并期待看到您在 Kubernetes 提供的抽象之上构建的内容。


浙公网安备 33010602011771号