0%

以下是Kubernetes patterns手册中为初学者总结的必须知道的十大设计模式。熟悉这些模式将有助于理解Kubernetes的基本概念,这反过来又将有助于讨论和设计基于Kubernetes的应用程序。

Kubernetes中有许多重要的概念,但下面这些是最重要的概念:

top_10_kubernetes_patterns

为了帮助理解,这些模式被组织成以下几个类别,灵感来自Gang of Four’s的设计模式。

基本模式

这些模式代表了容器化应用程序必须遵守的原则和最佳实践,以便成为优秀的云公民。不管应用程序的性质如何,我们都应该遵循这些准则。遵循这些原则将有助于确保我们的应用程序适用于Kubernetes上的自动化。

健康探测模式

Health Probe要求每个容器都应该实现特定的API,以帮助平台以最健康的方式观察和管理应用程序。为了完全自动化,云本地应用程序必须具有高度的可观察性,允许推断其状态,以便Kubernetes可以检测应用程序是否已启动并准备好为请求提供服务。这些观察结果会影响Pods的生命周期管理以及将流量路由到应用程序的方式。

可预测需求模式

可预测的需求解释了为什么每个容器都应该声明它的资源配置文件,并且只限于指定的资源需求。在共享云环境中成功部署应用程序、管理和共存的基础依赖于识别和声明应用程序的资源需求和运行时依赖性。此模式描述应该如何声明应用程序需求,无论它们是硬运行时依赖项还是资源需求。声明的需求对于Kubernetes在集群中的应用程序找到合适的位置至关重要。

自动放置模式

自动放置解释了如何影响多节点集群中的工作负载分布。放置是Kubernetes调度器的核心功能,用于为满足容器资源请求的节点分配新的pod,并遵守调度策略。该模式描述了Kubernetes调度算法的原理以及从外部影响布局决策的方式。

结构模式

拥有良好的云本地容器是第一步,但还不够。下一步是重用容器并将它们组合成Pod以实现预期的结果。这一类中的模式侧重于结构化和组织Pod中的容器,以满足不同的用例。

Init Container模式

Init容器为初始化相关的任务和主应用程序容器引入了一个单独的生命周期。Init容器通过为不同于主应用程序容器的初始化相关任务提供单独的生命周期来实现关注点的分离。这个模式引入了一个基本的Kubernetes概念,当需要初始化逻辑时,这个概念在许多其他模式中使用。

Sidecar模式

Sidecar描述了如何在不改变容器的情况下扩展和增强已有容器的功能。此模式是基本的容器模式之一,它允许单用途容器紧密地协作。

行为模式

这些模式描述了管理平台确保的pod的生命周期保证。根据工作负载的类型,Pod可以作为批处理作业一直运行到完成,也可以计划定期运行。它可以作为守护程序服务或单例运行。选择正确的生命周期管理原语将帮助我们运行具有所需保证的Pod。

批处理模式

批处理作业描述如何运行一个独立的原子工作单元直到完成。此模式适用于在分布式环境中管理独立的原子工作单元。

有状态服务模式

有状态服务描述如何使用Kubernetes创建和管理分布式有状态应用程序。这类应用程序需要持久身份、网络、存储和普通性等特性。StatefulSet原语为这些构建块提供了强有力的保证,非常适合有状态应用程序的管理。

服务发现模式

服务发现解释了客户端如何访问和发现提供应用程序服务的实例。为此,Kubernetes提供了多种机制,这取决于服务使用者和生产者位于集群上还是集群外。

高级模式

此类别中的模式更复杂,代表更高级别的应用程序管理模式。这里的一些模式(比如Controller)是永不过时的,Kubernetes本身就是建立在这些模式之上的。

Controller模式

控制器是一种模式,它主动监视和维护一组处于所需状态的Kubernetes资源。Kubernetes本身的核心由一组控制器组成,这些控制器定期监视并协调应用程序的当前状态与声明的目标状态。这个模式描述了如何利用这个核心概念为我们自己的应用程序扩展平台。

Operator模式

Operator是一个控制器,它使用CustomResourceDefinitions将特定应用程序的操作知识封装为算法和自动化形式。Operator模式允许我们扩展控制器模式以获得更大的灵活性和表现力。Kubernetes的Operator越来越多,这种模式正成为操作复杂分布式系统的主要形式。

总结

今天,Kubernetes是最流行的容器编排平台。它由所有主要的软件公司共同开发和支持,并作为一项服务由所有主要的云提供商提供。Kubernetes支持Linux和Windows系统,以及所有主要的编程语言。该平台还可以编排和自动化无状态和有状态的应用程序、批处理作业、周期性任务和无服务器工作负载。这里描述的模式是Kubernetes附带的一组更广泛的模式中最常用的模式,如下所示。

KubernetePatternsLevels

Kubernetes是新的应用程序可移植层。如果你是一个软件开发人员或架构师,Kubernetes很可能会以这样或那样的形式成为你生活的一部分。学习这里描述的Kubernetes模式将改变我们对这个平台的看法。我相信Kubernetes和由此产生的概念将成为面向对象编程概念的基础。

这里的模式试图创建类似Gang of Four的设计模式,但是用于容器编排。阅读这篇文章一定不是结束,而是你的Kubernetes之旅的开始。

Init模式

初始化逻辑通常在编程语言中很常见。在面向对象编程语言中,我们有构造函数的概念。构造函数是一个函数(或方法),每当对象被实例化时都会被调用。构造器的目的是“准备”对象以完成它应该做的工作。例如,它设置变量的默认值,创建数据库连接对象,确保对象正确运行所需的先决条件的存在。例如,如果创建了一个user对象,那么它至少需要用户的用户名、名和姓,这样它才能正常工作。不同语言之间的构造函数实现是不同的。但是,所有这些都只被调用一次,并且只在对象实例化时调用。

初始化模式的目的是将对象与其初始化逻辑解耦。因此,如果一个对象需要一些种子数据输入到数据库中,这就属于构造函数逻辑而不是应用程序逻辑。这允许我们更改对象的“启动”方式,而不影响其“工作”方式。

Kubernetes使用相同的模式。虽然对象是面向对象语言的原子单元,但是Kubernetes有Pods。因此,如果我们有一个应用程序在需要一些初始化逻辑的容器上运行,那么将此工作交给另一个容器是一个很好的做法。Kubernetes有一种用于特定作业的容器类型:init containers。

Init Containers

在Kubernetes中,init容器是在同一个Pod中的其他容器之前启动和执行的容器。它的目的是为Pod上托管的主应用程序执行初始化逻辑。例如,创建必要的用户帐户、执行数据库迁移、创建数据库模式等等。

Init Containers设计注意事项

在创建init容器时,我们应该考虑一些注意事项:

  • 它们总是比Pod里的其他容器先执行。因此,它们不应该包含需要很长时间才能完成的复杂逻辑。启动脚本通常很小而且简洁。如果我们发现在init容器中添加了太多的逻辑,那就应该考虑将它的一部分移到应用程序容器本身。
  • Init容器按顺序启动和执行。除非成功完成其前置容器,否则不会调用init容器。因此,如果启动任务很长,可以考虑将其分成若干步骤,每个步骤都由init容器处理,以便知道哪些步骤失败。
  • 如果任何init容器失败,整个Pod将重新启动(除非将restartPolicy设置为Never)。重新启动Pod意味着重新执行所有容器,包括任何init容器。因此,我们可能需要确保启动逻辑能够容忍多次执行而不会导致重复。例如,如果数据库迁移已经完成,那么应该忽略再次执行迁移命令。
  • 在一个或多个依赖项可用之前,init容器是延迟应用程序初始化的一个很好的候选者。例如,如果我们的应用程序依赖于一个施加了API请求速率限制的API,可能需要等待一段时间才能从该API接收响应。在应用程序容器中实现此逻辑可能很复杂;因为它需要与运行状况和准备状态探测相结合。一种更简单的方法是创建一个init容器,该容器等待API准备好后才能成功退出。只有在init容器成功完成其工作之后,应用程序容器才会启动。
  • Init容器不能像应用程序容器那样使用liveness和readiness探针。原因是它们注定要成功启动和退出,就像Jobs和CronJobs的行为一样。
  • 同一个Pod内的所有容器共享相同的卷和网络。我们可以使用此特性在应用程序及其init容器之间共享数据。

Init Containers的“请求”和“限制”行为

正如我们刚刚讨论的,init容器总是在同一个Pod上的其他应用程序容器之前启动。因此,调度程序为init容器的资源和限制提供了更高的优先级。这种行为必须被彻底考虑,因为它可能会导致不期望的结果。例如,如果我们有一个init容器和一个应用程序容器,并且将init容器的资源和限制设置为高于应用程序容器的资源和限制,那么只有在存在满足init容器要求的可用节点时,才会调度整个Pod。换句话说,即使有一个未使用的节点可以运行应用程序容器,如果init容器具有该节点可以处理的更高的资源先决条件,那么Pod也不会部署到该节点。因此,在定义init容器的请求和限制时,应该尽可能严格。作为最佳实践,除非绝对需要,否则不要将这些参数设置为高于应用程序容器的值。

场景01:初始化数据库

在这个场景中,我们为MySQL数据库提供服务。此数据库用于测试应用程序。它不一定要包含真实的数据,但是它必须有足够的数据种子,这样我们就可以测试应用程序的查询速度。我们使用init容器来处理下载SQL转储文件并将其还原到数据库中,该数据库托管在另一个容器中。这种情况可以说明如下:

init

yaml定义文件可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
name: mydb
labels:
app: db
spec:
initContainers:
- name: fetch
image: mwendler/wget
command: ["wget","--no-check-certificate","https://sample-videos.com/sql/Sample-SQL-File-1000rows.sql","-O","/docker-entrypoint-initdb.d/dump.sql"]
volumeMounts:
- mountPath: /docker-entrypoint-initdb.d
name: dump
containers:
- name: mysql
image: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "example"
volumeMounts:
- mountPath: /docker-entrypoint-initdb.d
name: dump
volumes:
- emptyDir: {}
name: dump

上面的定义创建了一个Pod,它承载两个容器:init容器和application容器。让我们看看这个定义有趣的方面:

  • init容器负责下载包含数据库转储的SQL文件。我们使用mwendler/wget映像,因为我们只需要wget命令。

  • 下载的SQL的目标目录是MySQL镜像用来执行SQL文件的目录(/docker-entrypoint-initdb.d)。此行为内置到我们在应用程序容器中使用的MySQL镜像中。

  • init容器将/docker-entrypoint-initdb.d挂载到一个emptyDir卷。因为两个容器托管在同一个Pod上,所以它们共享相同的卷。因此,数据库容器可以访问emptyDir卷上的SQL文件。

如果没有Init Containers会发生什么

在这个例子中,我们使用初始化模式作为最佳实践。如果我们在不使用init模式的情况下实现相同的逻辑,那么我们必须基于mysql基本镜像创建一个新映像,安装wget,然后使用它下载SQL文件。这种方法的缺点是:

  • 如果需要对下载逻辑进行任何更改,则需要创建一个新镜像,将其推送到定义文件中并更改其引用。这增加了维护自定义镜像的负担。

  • 它在DB容器及其启动逻辑之间创建了一个紧密耦合的关系,这使得应用程序更难管理,并且增加了引入错误和bug的可能性。

场景02:延迟应用程序启动

init容器的另一个常见用例是当我们需要应用程序等待另一个服务完全运行(响应请求)时。以下定义演示了这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
initContainers:
- name: init-myservice
image: busybox:1.28
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
containers:
- name: myapp-container
image: busybox:1.28
command: ['sh', '-c', 'echo The app is running! && sleep 3600']

所以,假设在myapp容器上运行的应用程序必须依赖myservice正常后才能正常工作。我们需要延迟myapp直到myservice准备好。我们通过使用一个简单的nslookup命令(第11行)来实现这一点,该命令不断检查“myservice”的成功名称解析。如果nslookup能够解析“myservice”,则服务将启动。使用一个成功的退出代码,init容器终止,让位于应用程序容器开始。否则,容器将在重试之前休眠两秒钟,从而延迟应用程序容器的启动。

为了完整起见,这是myservice的定义文件:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376

写在最后

  • Init模式是设计需要启动逻辑的应用程序时必须遵循的重要实践。
  • Kubernetes提供init容器作为将应用程序逻辑与其启动过程分离的一种方法。
  • 将应用程序初始化逻辑放在init容器中有许多优点:
    • 我们将实施关注点分离原则。应用程序可以有自己的工程师团队,而其初始化逻辑由另一个团队编写。
    • 在授权和访问控制方面,拥有一个独立的团队来处理应用程序的初始化步骤,可以给公司带来更大的灵活性。例如,如果启动应用程序需要使用需要安全许可的资源(例如,修改防火墙规则),则可以由具有适当凭据的人员来完成。应用程序团队不参与操作。
    • 如果涉及太多的初始化步骤,可以将它们分解为多个init容器,然后依次执行。如果一个步骤失败,init容将报告一个错误,这将使我们更好地了解逻辑的哪一部分不成功。
  • 在使用init容器时,应该考虑以下几点:
    • 初始化容器在失败时重新启动。因此,它们的代码必须是幂等的。
    • Init容器的请求和限制会先被调度程序用于调度判断。错误的值可能会对调度器决定将整个Pod(包括应用程序容器)放置在哪里产生负面影响。

Kubernetes中的InitContainer资源是一个有趣且非常有用的资源。在许多情况下,我们会看到它曾用于在Pod部署时,创建容器之前在卷中预填充数据,因此在业务容器启动时,卷数据已被初始化。

就我而言,我有一个带有单个静态页面的简单Web前端,它使用标准的nginx基础镜像:

1
2
3
4
FROM nginx

COPY index.html /usr/share/nginx/html/index.html
COPY smartos.ipxe /usr/share/nginx/html/smartos.ipxe

该镜像的构建和下载速度非常快,这非常棒,但是部分原因是它是无状态的。例如,smartos.ipxe文件中需要一些数据,这些数据在启动应用程序时需要可用,否则这些引用将无法按预期工作(抽象为404 HTTP响应):

1
2
3
4
5
6
#!ipxe
dhcp
set base-url http://sdc-ipxe.east.gourmet.yoga
kernel ${base-url}/smartos/smartos/platform/i86pc/kernel/amd64/unix -B smartos=true,console=ttyb,ttyb-mode="115200,8,n,1,-"
module ${base-url}/smartos/smartos/platform/i86pc/amd64/boot_archive type=rootfs name=ramdisk
boot

但是,这些文件不是应用程序的一部分,因为它们经常更新。因此,每次推出新版本时,我们都希望该卷中包含最新版本,并且由于我们不需要维护镜像中的这些文件,否则在我们的Registry中存储起来会非常大且昂贵,我们可以在Pod中的容器上挂载一个Volume来提供它们。

因此,基本上,我们需要一种方法来预填充要装入到/usr/share/nginx/html/smartos的卷。

使用InitContainer资源,我们可以指定要运行的命令,并且像Pod中的任何其他容器一样,我们可以分配要挂载的卷,因此让我们从这样的Kubernetes清单开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: apps/v1
kind: Deployment
metadata:
name: sdc-ipxe-deployment
labels:
app: sdc-ipxe
spec:
replicas: 2
selector:
matchLabels:
app: sdc-ipxe
template:
metadata:
labels:
app: sdc-ipxe
spec:
initContainers:
- name: config-data
image: ubuntu:xenial
command: ["/bin/sh","-c"]
args: ["apt update; apt install -y wget tar; wget https://us-east.manta.joyent.com/Joyent_Dev/public/SmartOS/platform-latest.tgz; tar xvf platform-latest.tgz -C /data; mkdir /data/smartos; mv /data/platform* /data/smartos/platform"]
volumeMounts:
- mountPath: /data
name: sdc-data
volumes:
- name: sdc-data
hostPath:
path: /mnt/kube-data/sdc-ipxe/
type: DirectoryOrCreate

因此,在这一点上,我们正在准备卷sdc数据,将其挂载到initContainer的/data目录上并运行:

1
apt update; apt install -y wget tar; wget https://us-east.manta.joyent.com/Joyent_Dev/public/SmartOS/platform-latest.tgz; tar xvf platform-latest.tgz -C /data; mkdir /data/smartos; mv /data/platform* /data/smartos/platform

上述命令下载数据并将其提取到卷中。现在,我们向yaml中添加一个container,然后再次附加该Volume,将可以使用预填充的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: apps/v1
kind: Deployment
metadata:
name: sdc-ipxe-deployment
labels:
app: sdc-ipxe
...
containers:
- name: sdc-ipxe
image: coolregistryusa.bix/jmarhee/sdc-ipxe:latest
imagePullPolicy: Always
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html/smartos
name: sdc-data
...

在业务容器中配置相同名称的卷,则业务容器就可以通过/usr/share/nginx/html/smartos目录获取sdc数据。

如果我们的应用程序依赖于具有可变需求的配置,则这种模式是有用的。可能是我们需要获得令牌,或者地址是动态的,并且需要通过磁盘上的文件而不是环境(比如负载平衡器,Web服务器或具有配置文件的数据库客户端,不容易通过它处理)传递文件(因为它们更改的频率不同)(Secret或ConfigMap),这种方法提供了一个易于编程的界面,用于预先填充或完成传递给容器的数据的模板化。

由于Docker在Kubernetes v1.20中已弃用,最近几天在Twitter上发生了很多讨论。

问题背景

Kubernetes v1.20的废弃说明:

如果想了解更多,强烈建议查看此Twitter

考虑到最近部署了一个Raspberry Pi Kubernetes集群,因此想就地进行更新,以使用Containerd代替Docker作为容器运行时。

免责声明–不要在生产集群中这样做。对于这些集群,只需删除现有节点,然后滚动引入新节点。这个博客只是关于Raspberry Pi集群的一个有趣的话题,看看是否可以在无需重建节点的情况下就地完成更新。

因此,要做的第一件事是drain需要更新的节点(我的节点称为k8s-node-1)并且cordon它:

1
kubectl drain k8s-node-1 --ignore-daemonsets

然后ssh进入节点并停止kubelet:

1
systemctl stop kubelet

然后删除Docker:

1
apt-get remove docker.io

删除旧的依赖项:

1
apt-get autoremove

现在unmask现有的containerd服务(Docker使用containerd,这就是为什么它已经存在的原因):

1
systemctl unmask containerd

安装所需的依赖项:

1
apt-get install unzip make golang-go libseccomp2 libseccomp-dev btrfs-progs libbtrfs-dev

完成以上步骤后,现在我们按照官方说明开始安装containerd。

无论如何,以root身份进行所有操作,获取containerd的源代码:

1
go get -d github.com/containerd/containerd

获取protoc 并安装:

1
2
wget -c https://github.com/google/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
sudo unzip protoc-3.11.4-linux-x86_64.zip -d /usr/local

获取runc 的源代码:

1
go get -d github.com/opencontainers/runc

进入到下载的包目录(检查$ GOPATH变量),使用make进行构建和安装runc和containerd:

1
2
3
4
5
6
cd ~/go/src/github.com/opencontainers/runc
make
make install
cd ~/go/src/github.com/containerd/containerd
make
make install

现在,将containerd.service文件复制到systemd以创建containerd的服务、启动服务并查看启动状态:

1
2
3
4
5
6
cp containerd.service /etc/systemd/system/
chmod 644 /etc/systemd/system/containerd.service
systemctl daemon-reload
systemctl start containerd
systemctl enable containerd
systemctl status containerd

差不多完成了,现在我们需要更新kubelet,将默认使用的docker改为containerd(参考flag设置)。我们可以通过运行:

1
sed -i 's/3.2/3.2 --container-runtime=remote --container-runtime-endpoint=unix:\/\/\/run\/containerd\/containerd.sock/g' /var/lib/kubelet/kubeadm-flags.env

如果上面的命令不起作用,请直接修改kubeadm-flags.env文件。

重启kubelet并查看服务状态:

1
2
systemctl start kubelet
systemctl status kubelet

最后,uncordon节点,并查看节点信息中的Runtime已变为containerd:

1
2
kubectl uncordon k8s-node-1
kubectl get nodes -o wide

问题引出

Helm是将应用程序部署到Kubernetes的绝佳工具。我们可以打包所有deployment和service等yaml文件,并使用一个简单的命令将它们部署到集群中。

但是Helm的另一个非常酷的功能是能够轻松升级和回滚版本(在集群中运行的Helm Chart实例的术语)的功能。

现在,我们可以使用kubectl进行此操作。如果我们使用kubectl apply升级deployment资源,则可以使用kubectl rollout undo来回滚该升级。这很棒!这是Kubernetes的最佳功能之一。

升级deployment时,将为该deployment创建一个新的replicaset,该replicaset将在一组新的Pod中运行升级后的应用程序。

如果使用kubectl rollout undo进行回滚,会删除最新replicaset中的容器,并回滚到旧replicaset的容器。

但是这里有一个潜在的问题。如果删除旧的replicaset会怎样?如果发生这种情况,我们将无法回滚升级。好吧,我们无法使用kubectl rollout undo将其回滚,但是如果我们使用Helm,会发生什么?

让我们来看一个演示。

Helm环境准备

创建一个称为testchart的Chart:

1
helm create testchart	

删除模板目录中所有不必要的文件:

1
rm -rf ./testchart/templates/*

创建一个deployment yaml文件:

1
2
3
4
kubectl create deployment nginx \
--image=nginx:1.17 \
--dry-run=client \
--output=yaml > ./testchart/templates/deployment.yaml

这将创建以下yaml并将其另存为templates目录中的deployment.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: nginx:1.17
name: nginx
resources: {}
status: {}

创建deployment:

1
kubectl create deployment nginx --image=nginx:1.17 

为service生成yaml:

1
2
3
4
5
kubectl expose deployment nginx \
--type=LoadBalancer \
--port=80 \
--dry-run=client \
--output=yaml > ./testchart/templates/service.yaml

这将为我们提供以下yaml并将其另存为模板目录中的service.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
type: LoadBalancer
status:
loadBalancer: {}

删除deployment,模板化values.yaml 和deployment.yaml文件:

1
2
3
4
kubectl delete deployment nginx
rm ./testchart/values.yaml
echo "containerImage: nginx:1.17" > ./testchart/values.yaml
sed -i 's/nginx:1.17/{{ .Values.containerImage }}/g' ./testchart/templates/deployment.yaml

最终,deployment.yaml文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: {{ .Values.containerImage }}
name: nginx
resources: {}
status: {}

改造后的yaml中容器镜像不再是硬编码的。它将从values.yaml文件中获取nginx:1.17的值,或者我们可以使用set标志来覆盖它(我们将在一分钟内完成)。

Helm部署示例

首先,将Chart部署到Kubernetes集群中:

1
helm install testchart ./testchart

helm-install-1

该应用程序版本是Chart.yaml文件中设置的默认版本(尚未更新)

检查部署中运行的镜像版本:

1
kubectl get deployment -o jsonpath='{ .items[*].spec.template.spec.containers[*].image }{"\n"}'

get-container-image-1

查看到的容器镜像就是Chart中values.yaml文件中定义的镜像版本。

现在升级Release,将默认的容器镜像值替换为set标志指定的值:

1
helm upgrade testchart ./testchart --set containerImage=nginx:1.18

确认版本已升级(检查版本号):

1
helm list

helm-upgrade-1

另外,请确认Release历史:

1
helm history testchart

helm-history-updated

这样我们就可以看到该Release的初始部署,然后是升级。应用版本保持不变,因为我没有更改Chart.yaml文件中的值。但是,镜像版本已更改,我们可以通过以下方式看到:

1
kubectl get deployment -o jsonpath='{ .items[*].spec.template.spec.containers[*].image }{"\n"}'

get-container-image-2

因此,我们已经升级了在deployment中容器运行的镜像版本。

让我们看一下deployment的replicasets:

1
kubectl get replicasets

get-replicasets-1

因此,我们为Helm版本创建的deployment有两个replicasets。最初的一个运行nginx v1.17,最新的一个运行nginx v1.18。

如果我们想使用kubectl回退升级,则可以使用(不要运行此代码!):

1
kubectl rollout undo deployment nginx

kubectl-rollout-undo

这里将发生的是,删除最新replicasets下的Pod,并创建旧replicasets下的Pod,将nginx回滚到v1.17。

但是我们不会那样做,因为我们正在使用Helm。

问题复现

继续在当前环境中获取最旧的replicasets名称,并删除它::

1
2
REPLICA_SET=$(kubectl get replicasets -o jsonpath='{.items[0].metadata.name }' --sort-by=.metadata.creationTimestamp)
kubectl delete replicasets $REPLICA_SET

因此,我们现在只有一个replicasets:

1
kubectl get replicasets

get-replicasets-2

现在尝试使用kubectl rollout undo命令进行回滚:

1
kubectl rollout undo deployment nginx

kubectl-rollout-undo-2

失败的原因是我们删除了旧的replicasets,因此该deployment没有历史记录,可以通过以下方式查看:

1
kubectl rollout history deployment nginx

kubectl-rollout-history

使用Helm回滚

虽然旧的replicasets被删除了,但是Helm的实现机制决定了使用Helm部署的Release会保留历史:

1
helm history testchart

helm-history-2

所以,我们可以使用Helm回滚:

1
helm rollback testchart 1

helm-rollback

查看Release状态:

1
helm list

helm-list-rollback

查看Release历史:

1
helm history testchart

helm-rollback-history

查看replicasets:

1
kubectl get replicasets

get-replicasets-3

旧的replicasets又回来了!怎么样?

原理探究

让我们看一下集群中的secrets:

1
kubectl get secrets

kubectl-get-secrets

可以看出,这些secrets中会存储Helm发布所有历史记录!初始版本(v1),升级(v2)和回滚(v3)。

让我们仔细看看v1版本:

1
kubectl get secret sh.helm.release.v1.testchart.v1 -o json

kubectl-get-secrets-2

嗯,这个Release内容看起来很有趣。我们可以做的是对base64进行解码,然后通过http://www.txtwizard.net/compression进行解压缩,得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
{
"name":"testchart",
"info":
{
"first_deployed":"2020-08-09T11:21:20.4665817+01:00",
"last_deployed":"2020-08-09T11:21:20.4665817+01:00",
"deleted":"",
"description":"Install complete",
"status":"superseded"},
"chart":{"metadata":
{
"name":"testchart",
"version":"0.1.0",
"description":"A Helm chart for Kubernetes",
"apiVersion":"v2",
"appVersion":"1.16.0",
"type":"application"},
"lock":null,
"templates":[
{
"name":
"templates/deployment.yaml",
"data":"YXBpVmVyc2lvbjogYXBwcy92MQpraW5kOiBEZXBsb3ltZW50Cm1ldGFkYXRhOgogIGNyZWF0aW9uVGltZXN0YW1wOiBudWxsCiAgbGFiZWxzOgogICAgYXBwOiBuZ2lueAogIG5hbWU6IG5naW54CnNwZWM6CiAgcmVwbGljYXM6IDEKICBzZWxlY3RvcjoKICAgIG1hdGNoTGFiZWxzOgogICAgICBhcHA6IG5naW54CiAgc3RyYXRlZ3k6IHt9CiAgdGVtcGxhdGU6CiAgICBtZXRhZGF0YToKICAgICAgY3JlYXRpb25UaW1lc3RhbXA6IG51bGwKICAgICAgbGFiZWxzOgogICAgICAgIGFwcDogbmdpbngKICAgIHNwZWM6CiAgICAgIGNvbnRhaW5lcnM6CiAgICAgIC0gaW1hZ2U6IHt7IC5WYWx1ZXMuY29udGFpbmVySW1hZ2UgfX0KICAgICAgICBuYW1lOiBuZ2lueAogICAgICAgIHJlc291cmNlczoge30Kc3RhdHVzOiB7fQo="},{"name":"templates/service.yaml","data":"YXBpVmVyc2lvbjogdjEKa2luZDogU2VydmljZQptZXRhZGF0YToKICBjcmVhdGlvblRpbWVzdGFtcDogbnVsbAogIGxhYmVsczoKICAgIGFwcDogbmdpbngKICBuYW1lOiBuZ2lueApzcGVjOgogIHBvcnRzOgogIC0gcG9ydDogODAKICAgIHByb3RvY29sOiBUQ1AKICAgIHRhcmdldFBvcnQ6IDgwCiAgc2VsZWN0b3I6CiAgICBhcHA6IG5naW54CiAgdHlwZTogTG9hZEJhbGFuY2VyCnN0YXR1czoKICBsb2FkQmFsYW5jZXI6IHt9Cg=="}],"values":{"containerImage":"nginx:1.17"},"schema":null,"files":[{"name":".helmignore","data":"IyBQYXR0ZXJucyB0byBpZ25vcmUgd2hlbiBidWlsZGluZyBwYWNrYWdlcy4KIyBUaGlzIHN1cHBvcnRzIHNoZWxsIGdsb2IgbWF0Y2hpbmcsIHJlbGF0aXZlIHBhdGggbWF0Y2hpbmcsIGFuZAojIG5lZ2F0aW9uIChwcmVmaXhlZCB3aXRoICEpLiBPbmx5IG9uZSBwYXR0ZXJuIHBlciBsaW5lLgouRFNfU3RvcmUKIyBDb21tb24gVkNTIGRpcnMKLmdpdC8KLmdpdGlnbm9yZQouYnpyLwouYnpyaWdub3JlCi5oZy8KLmhnaWdub3JlCi5zdm4vCiMgQ29tbW9uIGJhY2t1cCBmaWxlcwoqLnN3cAoqLmJhawoqLnRtcAoqLm9yaWcKKn4KIyBWYXJpb3VzIElERXMKLnByb2plY3QKLmlkZWEvCioudG1wcm9qCi52c2NvZGUvCg=="}]},
"manifest":"---\n#
Source: testchart/templates/service.yaml\n
apiVersion: v1\n
kind: Service\nmetadata:\n
creationTimestamp: null\n
labels:\n
app: nginx\n
name: nginx\n
spec:\n
ports:\n
- port: 80\n
protocol: TCP\n
targetPort: 80\n
selector:\n
app: nginx\n
type: LoadBalancer\n
status:\n loadBalancer: {}\n---\n#

Source: testchart/templates/deployment.yaml\n
apiVersion: apps/v1\n
kind: Deployment\n
metadata:\n
creationTimestamp: null\n
labels:\n
app: nginx\n
name: nginx\nspec:\n
replicas: 1\n
selector:\n
matchLabels:\n
app: nginx\n
strategy: {}\n
template:\n
metadata:\n
creationTimestamp: null\n
labels:\n
app: nginx\n
spec:\n
containers:\n
- image: nginx:1.17\n
name: nginx\n
resources: {}\n
status: {}\n",
"version":1,
"namespace":"default"
}

BOOM!看起来就像我们的deployment和service清单!我们可以看到最初的Helm版本中包含的所有信息(确认容器镜像为nginx:1.17)!

因此,通过将这些信息作为secrets存储在目标Kubernetes集群中,即使已删除了旧的replicasets,Helm也可以回滚升级!太酷了!

不过结果还不是很清晰,查看data字段……看起来像是加密信息。

让我们解密吧!这次在命令行上:

1
kubectl get secret sh.helm.release.v1.testchart.v1 -o jsonpath="{ .data.release }" | base64 -d | gunzip -c | jq '.chart.templates[].data' | tr -d '"' | base64 -d

decode-helm-secret

哈!这里有deployment和service的yaml文件!

通过使用Helm,即使已删除deployment的旧replicasets,我们也可以回滚,因为Helm将Release历史记录在secrets并存储在目标Kubernetes集群中。通过使用上面的代码,我们可以解密这些secrets并查看其中包含的信息。

如果我们在集群中安装了Helm Chart,可能会想知道Release的存储位置。

背景知识

让我们从一些背景开始。安装一个简单的Nginx Helm Chart:

1
$ helm install --name my-release stable/nginx-ingress

现在,要获取已安装Helm的详细信息,可以使用四个命令。

helm ls

1
2
3
$ helm ls 
NAME REVISION UPDATED STATUS
my-release 1 Wed Sep 12 07:41:48 2018 DEPLOYED

通常,我们要运行的第一个命令是helm ls。执行此操作是为了了解我们的集群中当前安装了哪些Helm Chart。无论它们是否失败,STATUS会展示出部署结果是成功还是失败。

helm get

一旦获得安装Chart的名称。下一步通常是尝试更详细地了解安装了什么。helm get命令可以为我们提供帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ helm get my-release
REVISION: 1
RELEASED: Thu Mar 23 15:59:14 2017
CHART: nginx-1.0
USER-SUPPLIED VALUES:
foo: bar

COMPUTED VALUES:
foo: bar
image: nginx
imagePullPolicy: IfNotPresent
ingress:
# **....**

helm status

如果我们遇到任何问题,并且希望获得Chart开发人员写下的一些说明。helm status可以通过呈现NOTES.txt文件来帮助我们。

1
2
3
4
5
6
$ helm status my-release
The nginx-ingress controller has been installed.
Get the application URL by running these commands:
export NODE_IP=$(kubectl --namespace {{ .Release.Namespace }} get nodes -o jsonpath="{.items[0].status.addresses[1].address}")
echo "Visit http://10.10.10.10:80 to access your application via HTTP."
echo "Visit https://10.10.10.10:443 to access your application via HTTPS."

上面的Helm状态可以通过values.yaml或–set修改。这是从NOTES.txt呈现的帮助者文本。

helm history

最后,我们还可以获得Chart部署的修订历史记录。当运行helm upgrade命令时会更新版本。假设我们要使用override.yaml覆盖某些值。

1
2
3
4
5
$ helm upgrade --install my-release --values override.yaml --set foo=notbar nginx 
$ helm history my-release
REVISION UPDATED STATUS CHART DESCRIPTION
1 Thu Mar 23 15:57:40 2020 SUPERSEDED nginx-0.4.3 Install complete
2 Thu Mar 23 15:59:14 2020 DEPLOYED nginx-0.4.3 Upgrade complete

所有这些信息都存储在哪里?

  • Helm v2版本,默认位置在configmap中:

    1
    2
    3
    4
    $ kubectl get configmap -n kube-system -l "OWNER=TILLER"
    NAME DATA AGE
    my-release.v1 1 7m
    my-release.v2 1 6m
  • Helm v3版本,默认位置在secrets中。强烈建议这样做,因为这些数据包含许多有关我们部署的信息:

    1
    2
    3
    4
    5
    $ kubectl get secrets -n kube-system
    NAME DATA AGE
    my-release.v1 1 7m
    my-release.v2 1 6m
    default-token-43hfuds 1 1d

解析Configmap内容

步骤1. 获取Configmap数据:

1
$ kubectl get configmap -n kube-system my-release.v1 -o=jsonpath='{.data.release}' > release-encoded

步骤2. 确保编码后的Release包含如下字符串:

1
2
H4sIAAAAAAAC/+w6TY8cS.....
# you should see a long block of string like above

步骤3. 解析数据:

1
cat release-encoded | base64 -d | gzip -cd > release-decoded

步骤4. 查看数据:

1
2
3
4
cat release-decoded
# you should see a whole bunch of data for the chart similar to above when you did helm get.
# but also this data contains a lot more like. the actual template. Value rendered.. etc...
# try it :) i already gave you the commands 🤠⽕😁🏃🏼‍

将Chart存储在configmaps中的问题在于,一旦黑客进入我们的集群,它就会成为黑客的金钥匙。将其存储为secrets可以提供某种保护(假设我们对机密信息进行了加密)。☸️

解析Secrets内容

步骤1. 解析指定版本的Release所有内容(Template内容依然是编码格式):

1
kubectl get secret sh.helm.release.v1.my-release.v1 -o jsonpath="{ .data.release }" | base64 -d | gunzip -c | jq .

步骤2. 解析指定版本的Release中的Template内容:

1
kubectl get secret sh.helm.release.v1.my-release.v1 -o jsonpath="{ .data.release }" | base64 -d | gunzip -c | jq '.chart.templates[].data' | tr -d '"' | base64 -d

注:该方法同样适用于解析Configmap内容。

一些建议

还有一些保护tiller的方法,例如使用https连接。但是,按照设计,tiller仍然需要大量特权才能在我们的集群中运行。并且仍然违反最小特权原则我的建议是尽快移至helm3

Helm3完全删除了tiller,而是依靠本地计算机的身份验证在群集中工作。默认情况下,它还将Chart数据作为secrets存储在群集中。Helm2将在2020年12月停止提供安全修复程序

本文提供了在Kubernetes上部署安全,可伸缩和弹性服务的最佳实践。内容开源在github仓库。如果有缺少或不足之处,欢迎提issue。

Part1 应用开发

健康检测

  • 为容器配置Readiness探针
    • 如果未设置readiness探针,则kubelet会假定该应用程序已准备就绪,可以在容器启动后立即接收流量。
    • 如果容器需要2分钟才能启动,则这2分钟内对容器的所有请求将失败。
  • 发生致命错误时允许容器崩溃
    • 如果应用程序遇到不可恢复的错误,则应使其崩溃。

    • 此类不可恢复的错误的示例是:

      1. 未捕获的异常
      2. 代码中的错字(动态语言)
      3. 无法加载标头或依赖项
    • 上述错误不应发信号通知Liveness探针失败。相反,应该立即退出该进程,并让kubelet重新启动容器。

  • 配置被动的Liveness探针
    • Liveness探针旨在容器卡住时重新启动容器。
    • 考虑以下情形:如果应用程序正在处理无限循环,则无法退出。当该进程消耗100%的CPU时,将没有时间回复(其他)Readiness探针检查,并且最终将其从服务中删除。但是,该Pod仍被注册为当前Deployment的活动副本。如果没有Liveness探针,它将保持运行状态,但与服务分离。换句话说,该进程不仅不处理任何请求,而且也在消耗资源。
    • 请注意,不应该使用Liveness探针来处理应用程序中的致命错误,并要求Kubernetes重新启动应用程序。相反,应该让应用程序崩溃。仅在过程无响应的情况下,才应将“liveness”探针用作恢复机制。
  • 两个探针的值不同
    • 当“liveness”和“readiness”探针指向相同的端点时,探针的作用会合并在一起。当应用程序发出信号表明尚未准备就绪或尚待运行时,kubelet会将容器与服务分离并同时将其删除。这时可能会注意到连接断开,因为容器没有足够的时间耗尽当前连接或处理传入的连接。
    • 更多信息参考:handling-client-requests-properly-with-kubernetes/

请注意,readiness和liveness没有默认值。

应用独立

  • Readiness探针是独立的

    • Readiness不包括对服务的依赖性,例如:数据库、数据库的迁移、API、第三方服务(反例
  • 应用重试连接到依赖服务

    • 应用启动时,它不应该因为数据库等依赖项尚未就绪而崩溃。相反,应用程序应继续尝试重新连接数据库,直到成功为止。
    • Kubernetes希望可以以任何顺序启动应用程序。当确保应用程序可以重新连接到诸如数据库之类的依赖项时,便知道可以提供更强大,更灵活的服务。

友好关闭

  • 应用程序未通过SIGTERM关闭,但可以正常终止连接
    • 可能需要一些时间才能感知到诸如kube-proxy或Ingress控制器之类的组件endpoint更改。因此,尽管标记为已终止,流量仍可能流向Pod。
    • 应用程序应停止在所有剩余连接上接受新请求,并在耗尽传出队列后将其关闭。
    • 如果想回顾endpoint在群集中的传播方式,请参考:handling-client-requests-properly-with-kubernetes/
  • 应用程序仍在宽限期内处理传入的请求
    • 可能要考虑使用容器生命周期事件(例如preStop处理程序)来自定义Pod删除之前发生的情况。
  • Dockerfile中的CMD将SIGTERM转发到进程
  • 关闭所有空闲的keep-alive套接字
    • 如果应用程序调用未关闭TCP连接(例如使用TCP保持活动状态或连接池),它将连接到一个Pod,而不使用该服务中的其他Pod。
    • 不应该突然终止长期存在的连接。相反,应该在关闭应用程序之前终止它们。
    • 更多信息参考:gracefully-shutting-down-a-nodejs-http-server

失败容忍

  • 为Deployment部署运行多个副本
    • 切勿单独运行一个Pod类型的资源,而是考虑将Pod作为Deployment,DaemonSet,ReplicaSet或StatefulSet的一部分进行部署。
    • 示例参考:Node-Management-In-GKE
  • 避免将Pod放置在单个节点中
    • 即使运行Pod的多个副本,也无法保证丢失节点不会影响服务。
    • 应该将反关联性规则应用于部署,以便Pod分布在群集的所有节点中。
    • 更多信息参考:inter-pod-affinity-and-anti-affinit
  • 设定Pod中断预算
    • drain节点后,该节点上的所有Pod都将被删除并重新安排。
    • 为了保护Deployment免受可能同时摧毁多个Pod的意外事件的影响,可以定义Pod中断预算。
    • 更多信息参考:pod-disruptions

资源使用

  • 为所有容器设置内存限制和请求
    • 资源限制用于限制容器可以使用多少CPU和内存,并使用containerSpec的resources属性设置。
    • 调度程序将这些用作度量标准之一,以确定哪个节点最适合当前Pod。
    • 根据调度程序,没有内存限制的容器的内存利用率为零。
    • 如果可调度在任何节点上的Pod数量不受限制,则会导致资源超负荷使用并可能导致节点(和kubelet)崩溃。
    • 如果容器进程超出内存限制,则该进程将终止。由于CPU是可压缩的资源,因此如果容器超出限制,则将限制该过程。即使它可以使用当时可用的某些CPU。
    • 更多信息参考:understanding-resource-limits-in-kubernetes-memoryunderstanding-resource-limits-in-kubernetes-cpu
  • 将CPU请求设置为1个CPU或以下
    • 除非有计算密集型作业,否则建议将请求设置为1个CPU或更低.
    • 更多信息参考:YouTube视频
  • 禁用CPU限制—除非有很好的用例
    • CPU 资源以 CPU 单位度量。
    • cpu:1表示每秒1个CPU单位。如果有1个线程,则每秒消耗的CPU时间不能超过1秒。如果有2个线程,则可以在0.5秒内消耗1个CPU单位。8个线程可以在0.125秒内消耗1个CPU单位。此后,请求将受到限制。
    • 如果不确定最佳应用设置,最好不要设置CPU限制。
    • 更多信息参考:understanding-resource-limits-in-kubernetes-cpu
  • 命名空间具有LimitRange
    • 如果我们认为可能忘记设置内存和CPU限制,则应考虑使用LimitRange对象为当前名称空间中部署的容器定义标准大小。
    • 设置方法参考:limit-range
  • 为Pod设置适当的服务质量(QoS)
    • 当节点进入过量使用状态(即使用过多资源)时,Kubernetes会尝试驱逐该节点中的某些Pod。
    • Kubernetes根据定义明确的逻辑对Pod进行排名和逐出。
    • 设置方法参考:quality-service-pod

请注意,如果不确定如何配置正确的CPU或内存限制,则可以使用Kubernetes中的Vertical Pod Autoscaler。自动缩放器会分析应用并给出建议的值。

标签资源

  • 定义技术标签
  • 定义业务标签
  • 定义安全标签

日志配置

  • 将应用程序日志记录到stdout和stderr
    • 有两种日志记录策略:被动和主动。使用被动日志记录的应用程序不了解日志记录基础结构,而是将消息记录到标准输出中。
    • 在主动日志记录中,该应用程序与中间聚合器建立网络连接,将数据发送到第三方日志记录服务,或直接写入数据库或索引。主动日志记录被视为反模式,应避免使用它。
    • 最佳实践参考:logs
  • 避免使用sidecar记录日志(如果可以的话)
    • 如果希望将日志转换应用于具有非标准日志事件模型的应用程序,则可能需要使用sidecar容器。
    • 使用Sidecar容器,可以在将日志条目运送到其他地方之前对其进行规范化。例如,先将Apache日志转换为Logstash JSON格式,然后再将其发送到日志基础结构。但是,如果可以控制应用程序,则可以从一开始就输出正确的格式。这样可以节省为集群中的每个Pod运行额外的容器的时间。

Pod扩缩容

  • 容器在其本地文件系统中不存储任何状态
    • 容器可以访问本地文件系统,用户可能会想使用它来持久化数据。
    • 但是,将持久性数据存储在容器的本地文件系统中会阻止Pod进行水平缩放(即通过添加或删除Pod的副本)。
    • 这是因为,通过使用本地文件系统,每个容器都维护自己的“状态”,这意味着Pod副本的状态可能会随时间而变化。从用户的角度来看,这会导致行为不一致(例如,当请求命中一个Pod时,一条特定的用户信息可用,但当请求命中另一个Pod时,则不可用)。
    • 相反,任何持久性信息都应保存在Pod外部的集中位置。例如,在集群中的PersistentVolume中,或者在集群外部的某些存储服务中甚至更好。
  • 对具有可变使用模式的应用程序使用HPA
    • HPA是内置的Kubernetes功能,可监视应用程序并根据当前使用情况自动添加或删除Pod副本。
    • 配置HPA可使应用在任何流量情况下(包括意外的高峰)保持可用并响应。
    • 配置HPA时必须创建一个HorizontalPodAutoscaler资源,该资源定义要监视的应用程序的度量。
    • HPA可以监视内置资源指标(Pod的CPU和内存使用情况)或自定义指标。对于自定义指标,还负责收集和公开这些指标,例如,可以使用Prometheus和Prometheus Adapter进行此操作。
  • Vertical Pod Autoscaler仍处于Beta版,请勿使用
    • 类似于HPA,还有VPA。
    • VPA可以自动调整Pod的资源请求和限制,以便当Pod需要更多资源时可以获取它们(增加/减少单个Pod的资源称为垂直缩放,与水平缩放相对)。
    • 这对于缩放无法水平缩放的应用程序很有用。
    • 但是,HPA当前处于beta版本,它具有一些已知的局限性(例如,通过更改其资源要求来扩展Pod,要求终止Pod并重新启动它)。
    • 考虑到这些限制以及Kubernetes上大多数应用程序都可以水平扩展的事实,建议不要在生产环境中使用VPA(至少要等到稳定的版本才能使用)。
  • 如果工作负载差异很大,请使用群集自动伸缩放器
    • 群集自动缩放器是“自动缩放器”的另一种类型(HAP和VPA除外)。
    • 群集自动缩放器可以通过添加或删除工作节点来自动缩放群集的大小。
    • 当由于现有工作节点上的资源不足而无法调度Pod时,会进行放大操作。在这种情况下,Cluster Autoscaler将创建一个新的工作节点,以便可以调度Pod。同样,当现有工作节点的利用率较低时,群集自动伸缩程序可以通过从一个工作节点中逐出所有工作负载并将其删除来进行缩减。
    • 对于高度可变的工作负载,例如当Pods的数量可能在短时间内成倍增长然后返回到先前的值时,使用Cluster Autoscaler是有意义的。在这种情况下,群集自动伸缩器可以满足需求高峰,而不会通过过度配置工作节点来浪费资源。
    • 但是,如果工作负载变化不大,则可能不值得设置Cluster Autoscaler,因为它可能永远不会触发。如果工作负载缓慢且单调地增长,则足以监视现有工作节点的利用率并在达到临界值时手动添加其他工作节点。

配置原则

  • 外部化所有配置
    • 配置应在应用程序代码之外进行维护。
    • 这有几个好处。首先,更改配置不需要重新编译应用程序。其次,可以在应用程序运行时更新配置。第三,相同的代码可以在不同的环境中使用。
    • 在Kubernetes中,可以将配置保存在ConfigMaps中,然后可以在将卷作为环境变量传入时将其安装到容器中。
    • 在ConfigMap中仅保存非敏感配置。对于敏感信息(例如凭据),请使用Secret资源。
  • 将Secrets作为卷而不是环境变量安装
    • Secret资源的内容应作为卷装入容器中,而不应作为环境变量传递。
    • 这是为了防止秘密值出现在用于启动容器的命令中,该命令可能由不应该访问秘密值的人员看到。

Part2 集群管理

命名空间限制

  • 命名空间具有LimitRange
    • 没有限制的容器可能导致与其他容器的资源争用以及计算资源的消耗。
    • Kubernetes具有两个限制资源利用的功能:ResourceQuota和LimitRange。
    • 使用LimitRange对象,可以定义资源请求的默认值以及名称空间内单个容器的限制。
    • 在该命名空间内创建的,未明确指定请求和限制值的任何容器都将分配为默认值。
    • 更多信息参考:resource-quotas
  • 命名空间具有ResourceQuotas
    • 使用ResourceQuotas,可以限制命名空间内所有容器的总资源消耗。
    • 定义命名空间的资源配额会限制属于该名称空间的所有容器可以消耗的CPU,内存或存储资源的总量。
    • 还可以为其他Kubernetes对象设置配额,例如当前名称空间中的Pod数量。
    • 如果存在他人使用群集并创建20000 ConfigMap,则可以使用LimitRange来防止这种情况。

Pod安全策略

  • 启用Pod安全策略

    • 例如,可以使用Kubernetes Pod安全策略来限制:
      1. 访问主机进程或网络名称空间;
      2. 运行特权容器容器;
      3. 运行的用户;
      4. 访问主机文件系统;
      5. Linux功能,Seccomp或SELinux配置文件
    • 选择正确的策略取决于集群的性质。
    • 更多信息参考:kubernetes-pod-security-policy
  • 禁用特权容器

    • 在Pod中,容器可以以“特权”模式运行,并且对主机系统上的资源的访问几乎不受限制。
    • 尽管在某些特定的用例中,必须具有这种级别的访问权限,但总的来说,让容器执行此操作存在安全风险。
    • 特权Pod的有效使用案例包括在节点上使用硬件,例如GPU。
    • 更多信息参考:security-context
  • 在容器中使用只读文件系统

    • 在容器中运行只读文件系统会强制容器不可变。
    • 这不仅减轻了一些旧的(且有风险的)做法(例如热修补),而且还帮助防止了恶意进程在容器内存储或操作数据的风险。
    • 使用只读文件系统运行容器听起来可能很简单,但是可能会带来一些复杂性。
    • 如果需要写日志或将文件存储在临时文件夹中怎么办?
    • 更多信息参考:running-docker-containers-securely-in-production
  • 防止容器以root身份运行

    • 在容器中运行的进程与主机上的任何其他进程没有什么不同,只不过它有一小部分元数据声明它在容器中。
    • 因此,容器中的根与主机上的根(uid 0)相同。
    • 如果用户设法脱离了以root用户身份在容器中运行的应用程序,则他们可能能够使用同一root用户获得对主机的访问权限。
    • 配置容器以使用非特权用户是防止特权升级攻击的最佳方法。
    • 更多信息参考:processes-in-containers-should-not-run-as-root
  • 限制capabilities

    • Linux capabilities使进程能够执行许多特权操作,其中只有root用户默认可以执行。

    • 例如,CAP_CHOWN允许进程“对文件UID和GID进行任意更改”。

    • 即使进程不是以root身份运行,进程也有可能通过提升特权来使用那些类似root的功能。

    • 换句话说,如果不想受到损害,则应仅启用所需的功能。

    • 但是应该启用什么功能?为什么?以下两篇文章探讨了有关Linux内核功能的理论和最佳实践:

      Linux Capabilities: Why They Exist and How They Work

      Linux Capabilities In Practice

  • 防止特权升级

    • 应该在关闭特权升级的情况下运行容器,以防止使用setuid或setgid二进制文件提升特权。

网络策略

  • 启用网络策略
    • Kubernetes网络策略指定Pod组的访问权限,就像云中的安全组用于控制对VM实例的访问一样。
    • 换句话说,它在Kubernetes集群上运行的Pod之间创建了防火墙。
    • 更多信息参考:Securing Kubernetes Cluster Networking
  • 每个命名空间中都有一个保守的NetworkPolicy

RBAC策略

  • 禁用默认服务帐户的自动挂载RBAC策略
  • 设置为所需的最少特权
    • 寻找有关如何设置RBAC规则的好的建议是一项挑战。
    • Kubernetes RBAC的3种现实方法中,可以找到三种实用场景和有关如何入门的实用建议。
  • RBAC策略是精细的,不能共享
    • Zalando有一个简洁的策略来定义角色和ServiceAccounts。
    • 首先,他们描述他们的要求:
      1. 用户应该能够部署,但不应允许他们查看如“secret”这类资源
      2. 管理员应拥有对所有资源的完全访问权限
      3. 默认情况下,应用程序不应获得对Kubernetes API的写访问权限
      4. 对于某些用途,可以有Kubernetes API写权限。
    • 四个要求转化为五个单独的角色:
      1. ReadOnly
      2. PowerUser
      3. Operator
      4. Controller
      5. Admin
    • 更多信息参考:access-control-roles-and-service-accounts

自定义策略

  • 只允许从已知registry部署容器
    • 可能要考虑的最常见的自定义策略之一是限制可以在群集中部署的镜像。
    • 参考文档说明了如何使用开放策略代理来限制未批准的镜像。
  • 强制Ingress主机名唯一
    • 用户创建Ingress清单时,可以使用其中的任何主机名。
    • 但是,可能希望阻止用户多次使用相同的主机名并互相覆盖。
    • Open Policy Agent的官方文档包含有关如何在validation Webhook中检查Ingress资源的教程。
  • 仅在Ingress主机名中使用批准的域名
    • 用户创建Ingress清单时,可以使用其中的任何主机名。
    • 但是,可能希望阻止用户使用无效的主机名。
    • Open Policy Agent的官方文档包含有关如何在validation Webhook中检查Ingress资源的教程。

Part3 集群配置

该部分还在进行中。如果对这部分内容有意见,欢迎提issue。

集群要求

  • 集群通过CIS基准测试

    • 互联网安全中心提供了一些准则和基准测试,以确保代码安全的最佳做法
    • 他们还维护了Kubernetes的基准,可以从官方网站上下载该基准。
    • 虽然可以阅读冗长的指南并手动检查集群是否符合要求,但更简单的方法是下载并执行kube-bench
    • kube-bench是一个工具,用于自动执行CIS Kubernetes基准测试并报告集群中的错误配置。

    请注意,无法使用kube-bench检查托管集群(例如GKE,EKS和AKS)的主节点。主节点由云提供商控制。

  • 禁用云提供商的元数据API

    • 云平台(AWS,Azure,GCE等)通常将本地元数据服务公开给实例。
    • 默认情况下,实例上运行的Pod可以访问这些API,并且可以包含该节点的云凭据或诸如kubelet凭据之类的置备数据。
    • 这些凭据可用于在群集内升级或升级到同一帐户下的其他云服务。
  • 限制对Alpha或Beta功能的访问

    • Alpha和Beta Kubernetes功能正在积极开发中,可能会存在限制或错误,从而导致安全漏洞。
    • 始终评估Alpha或Beta功能可能提供的价值,以防对安全状况造成潜在风险。
    • 如有疑问,请禁用不使用的功能。

身份认证

  • 使用OpenID(OIDC)令牌作为用户身份验证策略

    • Kubernetes支持各种身份验证方法,包括OpenID Connect(OIDC)。
    • OpenID Connect允许单点登录(SSO)(例如Google身份)连接到Kubernetes集群和其他开发工具。
    • 无需单独记住或管理凭据。
    • 可能有多个群集连接到同一OpenID提供程序。
    • 更多信息参考:kubernetes-single-sign-one-less-identity
  • ServiceAccount令牌仅适用于应用程序和控制器

    • ServiceAccount不应用于尝试与Kubernetes群集进行交互的最终用户,但对于在Kubernetes上运行的应用程序和工作负载,它们是首选的身份验证策略。

日志设置

  • 有一个日志保留和归档策略
    • 应该保留30-45天的历史日志。
  • 从节点,控制平面,审计中收集日志
    • 从哪些地方收集日志:
      1. 节点 (kubelet, container runtime)
      2. 控制平面 (API server, scheduler, controller manager)
      3. Kubernetes审计 (all requests to the API server)
    • 应该收集什么:
      1. 应用名称。从元数据标签中检索。
      2. 应用程序实例。从元数据标签中检索。
      3. 应用程序版本。从元数据标签中检索。
      4. 集群ID。从Kubernetes集群检索。
      5. 容器名称。从Kubernetes API检索。
      6. 运行此容器的群集节点。从Kubernetes集群检索。
      7. 运行容器的Pod名称。从Kubernetes集群检索。
      8. 命名空间。从Kubernetes集群检索。
  • 在每个节点上最好有一个守护程序来收集日志,而不是sidecar
    • 应用程序日志应输出到标准输出,而不是文件。
    • 每个节点上的守护程序可以从容器运行时收集日志(如果记录到文件,则可能需要每个pod的sidecar容器)。
  • 提供日志聚合工具
    • 使用日志聚合工具,例如EFK技术栈(Elasticsearch,Fluentd,Kibana),DataDog,Sumo Logic,Sysdig,GCP Stackdriver,Azure Monitor,AWS CloudWatch。

创建Tag保留规则

一个repository可以快速积累大量镜像tag,在给定时间之后或一旦它们被后续镜像构建取代后,可能不需要许多镜像tag。这些多余的tag显然会消耗大量的存储容量。作为Harbor系统管理员,可以定义规则来管理给定repository中要保留多少个tag,或将某些tag保留多长时间。

Tag保留规则如何工作

在repositories上而不是projects上定义tag保留规则(repository属于project内的概念)。在定义保留规则时,这可以提供更大的粒度。顾名思义,当我们为repositories定义保留规则时,也即在定义要保留的tag。我们没有定义规则来显式删除tag,而是当设置规则时,repositories中任何未标识为可保留的标记都将被丢弃。

tag保留规则具有3个按顺序应用的过滤器,如下表所述。

Order Filter Description
First 一个或多个repository 标识要在其上应用规则的一个或多个repository。可以标识具有特定名称或名称片段的repository,或者不具有该名称或名称片段的repository。允许使用通配符(例如* repo,repo *和**)。首先使用repository filter以标记要对其应用保留规则的repository。根据标签标准,将识别出的repository指定用于进一步匹配。在此阶段,对没有指定的repository不采取任何措施。
Second 保留数量 通过指定最大数量的标签或指定最大保留标签的时间来设置要保留的标签。
Third 要保留的标签 标识要应用规则的一个或多个标签。可以标识具有特定名称或名称片段的标签,或者不具有该名称或名称片段的标签。允许使用通配符(例如* tag,tag *和**)。

有关如何应用**通配符的信息,请参见https://github.com/bmatcuk/doublestar#patterns。

Example 1

  • 在一个project中有5个repositories,repositories A到E。
    • repository A具有100个镜像tag,所有这些镜像tag均已在上周有pull操作。
    • repository B到E每个都有6个镜像,上个月都没有pull操作。
  • 将repositories过滤器设置为**,这意味着包括了project中的所有repositories。
  • 设置保留策略,以在每个repositories中保留最近提取的10个映像。
  • 将标签过滤器设置为**,这意味着包括repositories中的所有标签。

在此示例中,规则在repository A中保留了10个最近有pull操作的镜像,并且在repository B至E中的每一个中都保留了所有6个镜像。因此,project中总共保留了34个镜像tag。换句话说,该规则不会将repository A到E中的所有镜像都视为单个池,然后从中选择10个最新镜像。因此,即使repository A中的第11至第100个标签比repository B至E中的任何标签相比都有pull操作,也将保留repository B至E中的所有标签,因为每个repository中的标签少于10个。

Example 2

本示例使用与示例1相同的project和repositories,但设置了保留策略以保留每个repository中最近7天内有pull操作的镜像。

在这种情况下,保留了repository A中的所有镜像,因为它们是最近7天内有pull操作的。repository B到E中的任何镜像都不会保留,因为它们在上周都没有发生pull操作。在此示例中,保留了100个镜像,而示例1中则保留了34个镜像。

Tag保留规则和原生Docker Tag删除问题

警告:由于本机Docker tag删除行为,当前的保留策略实施存在问题。如果有多个tag引用相同的SHA摘要,并且如果这些tag的子集被配置的保留策略标记为要删除,则所有其余tag也将被删除。这违反了保留策略,因此在这种情况下,所有tag都将保留。在以后的更新版本中将解决此问题,以便tag保留策略可以删除tag而不删除摘要和其他共享tag。

例如,我们有以下tag,这些tag根据其推送时间列出,并且它们都引用相同的SHA摘要:

  • harbor-1.8, pushed 8/14/2019 12:00am
  • harbor-release, pushed 8/14/2019 03:00am
  • harbor-nightly, pushed 8/14/2019 06:00am
  • harbor-latest, pushed 8/14/2019 09:00am

如果配置了保留策略,以保留与Harbor- *匹配的两个最新标记,以便删除Harbor-rc和Harbor-latest。但是,由于所有tag都引用相同的SHA摘要,因此此策略还将删除标签Harbor-1.8和Harbor-release,因此将保留所有标签。(时间和删除的tag有点对不上)

在一个Repository上合并规则

每个project最多可以定义15条规则。我们可以将多个规则应用于一个repository或一组repositories。在将多个规则应用于repository 时,它们将使用OR逻辑而不是AND逻辑来应用。这样,就不会在给定的repository 上优先应用规则。规则在后台同时运行,每个规则的结果集在运行结束时合并。

Example 3

本示例使用与示例1和2相同的project和repositories,但是设置了两个规则:

  • 规则1:保留每个repository中最近7天内pull操作的所有镜像。
  • 规则2:每个repository中最多保留10个镜像。

对于repository A,规则1保留所有镜像,因为它们都是在上周发生pull操作的。规则2保留最近发生pull操作的10个镜像。因此,由于这两个规则是通过OR关系应用的,所有100个镜像都保留在repository A中。

对于repositories B-E,规则1将保留0个镜像,因为上周未发生pull操作。规则2将保留所有6个镜像,因为6 <10。因此,由于这两个规则以OR关系应用,对于repositories B-E,每个repository将保留所有6个图像。

在此示例中,所有镜像均被保留。

Example 4

本示例使用与先前示例不同的repository。

  • 包含12个tag的repository:

    Production Release Candidate Release
    2.1-your_repo-prod 2.1-your_repo-rc 2.1-your_repo-release
    2.2-your_repo-prod 2.2-your_repo-rc 2.2-your_repo-release
    3.1-your_repo-prod 3.1-your_repo-rc 3.1-your_repo-release
    4.4-your_repo-prod 4.4-your_repo-rc 4.4-your_repo-release
  • 在此repository上定义了3个tag保留规则:

    • 保留以2开头的10个最近发生pull操作的镜像tag。

    • 保留以-prod结尾的10个最近发生pull操作的镜像tag。

    • 保留所有不包含2.1-your_repo-prod的tag。

在此示例中,规则将应用于以下7个tag(与预期不符,官方收到反馈后已修改):

  • 2.1-your_repo-rc
  • 2.1-your_repo-release
  • 2.2-your_repo-prod
  • 2.2-your_repo-rc
  • 2.2-your_repo-release
  • 3.1-your_repo-prod
  • 4.4-your_repo-prod

Tag保留规则如何与项目配额搭配使用

Harbor系统管理员可以设置一个project可以包含的tag数量及其可以使用的存储量的最大值。有关project配额的信息,请参阅配置project配额

如果在project上设置配额,则不能超过该配额。即使设置的保留规则超过配额,配额也将应用于project。换句话说,不能使用保留规则来绕过配额。

配置Tag保留规则实例

  1. 使用至少具有项目管理员特权的帐户登录到Harbor界面。

  2. 转到”project”,选择一个project,然后选择tag保留。

    tag-retention

  3. 单击”添加规则”以添加规则。

  4. 在“repositories”下拉菜单中,选择匹配或排除。

    tag-retention

  5. 在“repositories”文本框中,标识要在其上应用规则的repository。

    可以通过输入以下信息来定义要在哪些repositories上应用规则:

  • repository名称,例如my_repo_1。

  • 以逗号分隔的repository名称列表,例如my_repo_1,my_repo_2,your_repo_3。

  • 带通配符的部分repository名称,例如my _ _ 3或*repo*。

  • **将规则应用于project中的所有repositories。

    如果选择匹配,则规则将应用于标识的repository。如果选择排除,则该规则将应用于project中除已标识的repositories之外的所有repositories。

  1. 在“按镜像或天数计数”下拉菜单中,定义要保留的tag数量或保留的时间。

    tag-retention

Option Description
retain the most recently pushed # images 输入要保留的最大镜像个数,保留最近发生push操作的镜像。不考虑镜像的时间。
retain the most recently pulled # images 输入要保留的最大镜像个数,保留最近发生pull操作的镜像。不考虑镜像的时间。
retain the images pushed within the last # days 输入保留镜像的天数,仅保留在此期间发生push操作的镜像。不考虑镜像数量。
retain the images pulled within the last # days 输入保留镜像的天数,仅保留在此期间发生pull操作的镜像。不考虑镜像数量。
retain always 始终保留此规则标识的镜像。
  1. 在”tag”下拉菜单中,选择匹配或排除。

  2. 在“标签”文本框中,标识要在其上应用规则的tag。

    可以通过输入以下信息来定义要在其上应用规则的tag:

  • tag名称,例如my_tag_1。
  • tag名称的逗号分隔列表,例如my_tag_1,my_tag_2,your_tag_3。
  • 带通配符的部分tag名称,例如my _ _ 3或* tag *。
  • **将规则应用于project中的所有tag。
  1. 单击”添加”以保存规则。

  2. (可选)单击“添加规则”以添加更多规则,每个project最多15条。

  3. (可选)在“计划”下,单击“编辑”,然后选择运行规则的频率。

    tag-retention

    如果选择“自定义”,请输入cron job命令以配置规则。

    注意:如果定义多个规则,则计划将应用于所有规则。不能配置不同的规则在不同的时间运行。

  4. 单击“模拟运行”以测试定义的一个或多个规则。

  5. 单击“立即运行”以立即运行规则。

警告:运行规则后,将无法还原它。强烈建议先执行模拟运行,然后再运行规则。

要修改现有规则,请使用规则旁边的操作下拉菜单来禁用,编辑或删除该规则。

tag-retention

Big picture

了解Calico支持的各种网络选项,以便可以根据需要选择最佳选项。

Value

Calico灵活的模块化体系结构支持广泛的部署选项,因此可以根据自己的特定环境和需求选择最佳的网络方案。这包括在BGP和非BGP的情况下,以underlying或overlay模式与各种CNI和IPAM插件以及基础网络类型一起运行的能力。

Concepts

如果想完全了解可用的网络选项,我们建议确保自己熟悉并理解以下概念。如果希望跳过学习并直接获得选择和建议,则可以跳至“网络选项”。

Kubernetes网络基础知识

Kubernetes网络模型定义了一个“扁平”网络,其中:

  • 每个Pod都有自己的IP地址。
  • 无需NAT,任何节点上的Pod均可与所有其他节点上的所有Pod通信。

这将创建一个干净的,向后兼容的模型,从端口分配,命名,服务发现,负载平衡,应用程序配置和迁移的角度来看,可以将Pod像VM或物理主机一样对待。可以使用网络策略来定义网络分段,以将流量限制在这些基本网络功能内。

在此模型中,可以灵活地支持不同的网络方案和环境。确切地如何实现网络的详细信息取决于所使用的CNI,网络和云提供商插件的组合。

CNI插件

CNI(容器网络接口)是一个标准API,允许将不同的网络实现插入Kubernetes。每当创建或销毁Pod时,Kubernetes都会调用API。CNI插件有两种类型:

  • CNI网络插件:负责向Kubernetes Pod网络中添加Pod或从Kubernetes Pod网络中删除Pod。这包括创建/删除每个Pod的网络接口,以及将其连接/断开与其他网络实现的连接。
  • CNI IPAM插件:负责在Pod创建或删除时分配和释放Pod的IP地址。根据插件的不同,这可能包括为每个节点分配一个或多个IP地址(CIDR)范围,或从底层公共云网络获取IP地址以分配给Pod。

云提供商集成

Kubernetes云提供商集成是特定于云的控制器,可以配置基础云网络以帮助提供Kuberenetes网络。根据云提供商的不同,这可能包括自动将路由编程到基础云网络中,以便它本机知道如何路由Pod流量。

Kubenet

Kubenet是Kubernetes中内置的一个非常基本的网络插件。它没有实现跨节点通信或网络策略。它通常与云提供商集成一起使用,后者在云提供商网络中设置路由以在节点之间或在单节点环境中进行通信。Kubenet与Calico不兼容。

Overlay网络

overlay网络是位于另一个网络之上的网络。在Kubernetes的上下文中,overlay网络可用于处理基础网络顶部节点之间的Pod到Pod流量,这些节点不知道Pod IP地址或哪些Pod在哪些节点上运行。overlay网络通过将基础网络不知道如何处理(例如使用Pod IP地址)的网络数据包封装在基础网络确实知道如何处理的外部数据包(例如节点IP地址)中。用于封装的两种常见网络协议是VXLAN和IP-in-IP。

使用overlay网络的主要优点是它减少了对基础网络的依赖性。例如,可以在几乎任何基础网络之上运行VXLAN,而无需与基础网络集成或对其进行任何更改。

使用overlay网络的主要缺点是:

  • 对性能有轻微影响。封装数据包的过程占用少量CPU,并且数据包中用于编码封装(VXLAN或IP-in-IP标头)所需的额外字节减少了可以发送的内部数据包的最大大小,这意味着需要为相同数量的总数据发送更多数据包。
  • Pod IP地址无法在集群外部路由。

跨子网Overlay网络

除了标准的VXLAN或IP-in-IP overlay外,Calico还支持VXLAN和IP-in-IP的“cross-subnet”模式。在这种模式下,在每个子网中,基础网络充当L2网络。在单个子网内发送的数据包不进行封装,因此可以获得非overlay网络的性能。跨子网发送的数据包像普通的overlay网络一样被封装,从而减少了对基础网络的依赖(无需与基础网络集成或对其进行任何更改)。

就像使用标准overlay网络一样,基础网络也不知道Pod IP地址,并且Pod IP地址无法在集群外部路由。

Pod IP路由到集群外部的能力

不同的Kubernetes网络实现的一个重要区别特征是Pod IP地址是否可在整个较宽网络的集群外部路由。

不可路由

如果Pod IP地址无法在集群外部路由,则当Pod尝试建立与集群外部IP地址的网络连接时,Kubernetes将使用一种称为SNAT(源网络地址转换)的技术来更改源IP从Pod的IP地址到托管Pod的节点的IP地址。连接上的所有返回数据包都会自动映射回Pod IP地址。因此,Pod不知道发生了SNAT,连接的目的地将节点视为连接的源,而底层的更广泛的网络不会看到Pod IP地址。

对于相反方向的连接,其中集群外部的某些设备需要连接到Pod,这只能通过Kubernetes service或Kubernetes ingress来完成。集群之外的任何人都无法直接连接到Pod IP地址,因为更广泛的网络不知道如何将数据包路由到Pod IP地址。

可路由

如果Pod IP地址可以在集群外部路由,则Pod可以不使用SNAT即可连接到外部世界,并且集群外部可以直接连接到Pod,而无需通过Kubernetes service或Kubernetes ingress。

Pod IP可路由到集群外部的优点是:

  • 避免将SNAT用于出站连接对于与现有更广泛的安全要求进行集成可能至关重要。它还可以简化操作日志的调试和易懂性。
  • 如果有专门的工作负载,这意味着某些Pod需要直接访问而不需要通过Kubernetes service或Kubernetes ingress,那么可路由的Pod IP在操作上可能更简单。

Pod IP地址可路由到集群外的主要缺点是,Pod IP在整个网络中必须是唯一的。因此,例如,如果运行多个群集,则需要为每个群集中的Pod使用不同的IP地址范围(CIDR)。当大规模运行时,或者如果现有其他企业对IP地址空间有大量重要需求,这又可能导致IP地址范围耗尽的挑战。

决定可路由性的因素是什么?

如果集群使用overlay网络,则Pod IP通常无法路由到集群外。

如果集群不使用overlay网络,那么Pod IP是否路由到集群外取决于所用的CNI插件,云提供商集成或与物理网络(对于本地)BGP对等连接。

BGP

BGP(边界网关协议)是用于跨网络共享路由的基于标准的网络协议。它是互联网的基本组成部分之一,具有出色的扩展特性。

Calico内置了对BGP的支持。在本地部署中,这使Calico可以与物理网络(通常连接到Top或Rack路由器)建立对等关系以交换路由,从而形成一个none-overlay网络,其中Pod IP地址可以在更广泛的网络中路由,就像附加的任何其他工作负载一样到网络。

关于Calico网络

Calico网络灵活的模块化架构包括以下内容。

Calico CNI网络插件

Calico CNI网络插件使用一对虚拟以太网设备(一对)将Pod连接到主机网络名称空间的L3路由。这种L3架构避免了许多其他Kubernetes网络解决方案中附加的L2桥不必要的复杂性和性能开销。

Calico CNI IPAM插件

Calico CNI IPAM插件为一个或多个可配置IP地址范围之外的Pod分配IP地址,并根据需要为每个节点动态分配IP块。与许多其他CNI IPAM插件(包括在许多网络解决方案中使用的主机本地IPAM插件)相比,具有更有效的IP地址空间使用。

Overlay网络模式

Calico可以提供VXLAN或IP-in-IP网络,包括cross-subnet模式。

Non-overlay网络模式

Calico可以提供在任何基础L2网络之上运行的non-overlay网络,或者是具有适当的云提供商集成的公共云网络或支持BGP的L3网络(通常是具有标准Top-of-Rack路由器)。

网络策略

Calico的网络策略执行引擎实现了Kubernetes网络策略的全部功能,以及Calico Network Policy的扩展功能。这可以与Calico的内置网络模式或任何其他Calico兼容的网络插件和云提供商集成结合使用。

与Calico兼容的CNI插件和云提供商集成

除Calico CNI插件和内置网络模式外,Calico还与许多第三方CNI插件和云提供商集成兼容。

Amazon VPC CNI

Amazon VPC CNI插件从基础AWS VPC分配Pod IP,并使用AWS弹性网络接口提供VPC本机Pod网络(可在集群外部路由的Pod IP)。它是Amazon EKS中使用的默认网络,并与Calico一起用于网络策略实施。

Azure CNI

Azure CNI插件从基础Azure VNET分配Pod IP,将Azure虚拟网络配置为提供VNET本机Pod网络(可在群集外部路由的Pod IP)。它是Microsoft AKS中使用的默认网络,可与Calico一起执行网络策略。

Azure cloud provider

Azure云提供商集成可以用作Azure CNI插件的替代方法。它使用host-local IPAM插件分配Pod IP,并使用相应的路由对基础Azure VNET子网进行编程。Pod IP仅可在VNET子网内路由(这通常意味着它们无法路由到群集外部)。

Google cloud provider

Google云提供商集成使用host-local IPAM插件分配Pod IP,并对Google Cloud网络Alias IP范围进行编程,以在Google Cloud上提供VPC本机Pod网络(可在集群外部路由的Pod IP)。它是Google Kubernetes Engine(GKE)的默认设置,并带有Calico来执行网络策略。

Host local IPAM

host-local IPAM插件是常用的IP地址管理CNI插件,它为每个节点分配固定大小的IP地址范围(CIDR),然后从该范围内分配Pod IP地址。默认地址范围大小是256个IP地址(a/24),其中两个IP地址是为特殊目的保留的,未分配给Pod。host-local IPAM插件的简单性使其易于理解,但与Calico CNI IPAM插件相比,其IP地址空间使用效率较低。

Flannel

Flannel使用从host-local IPAM插件获得的静态CIDR路由pod间的通信。Flannel提供了许多网络后端,但主要与VXLAN后端一起使用。Calico CNI和Calico网络策略可以与flannel和host-local IPAM插件结合使用,以提供具有策略实施功能的VXLAN网络。这种组合有时称为“Canal”。

注意:Calico现在内置了对VXLAN的支持,为了简化起见,我们通常建议优先使用Calico + Flannel组合。

网络选择

本地

Calico本地部署最常见的网络设置是non-overlay模式,该模式使用BGP与物理网络(通常是机架路由器的顶部)对等,以使Pod IP可在集群外部路由。(当然,可以根据需要配置其余的本地部署网络,以限制群集外的Pod IP路由的范围。)此设置提供了丰富的Calico高级功能,包括公告Kubernetes serviceIP的能力(cluster IPs or external IPs),以及在Pod,名称空间或节点级别控制IP地址管理的能力,以支持与现有企业网络和安全要求集成的各种可能性。

Policy IPAM CNI Overlay Routing
Calico Calico Calico No BGP

如果不能将BGP对等连接到物理网络,并且群集在单个L2网络中,则还可以运行non-overlay模式,而Calico只能在群集中的节点之间对等BGP。即使这不是严格的overlay网络,也无法在集群外部路由Pod IP,因为基础网络没有Pod IP的路由。

Policy IPAM CNI Overlay Routing
Calico Calico Calico No BGP

或者,可以在VXLAN或IP-in-IP模式下运行Calico,并使用cross-subnet模式来优化每个L2子网内的性能。

推荐方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

替代方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico IPIP BGP

AWS

如果希望在集群外部可路由Pod IP地址,则必须使用Amazon VPC CNI插件。这是EKS的默认网络模式,并使用Calico的网络策略。Pod IP地址是从基础VPC分配的,每个节点的Pod的最大数量取决于实例类型。

Policy IPAM CNI Overlay Routing
Calico AWS AWS No VPC/Native

如果希望避免依赖特定的云提供商,或者由于IP地址范围耗尽的挑战,或者如果Amazon VPC CNI插件每个节点支持的最大Pod数量不足以从基础VPC分配Pod IP,则存在问题。根据需求,我们建议使用Calico的overlay + cross-subnet模式。Pod IP将无法在集群外部路由,但是可以在不依赖基础云网络的情况下将集群扩展到Kubernetes的极限。

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

在这个简短的视频中,可以了解有关AWS上的Kubernetes Networking的更多信息,包括上述每个选项的工作原理。Everything you need to know about Kubernetes networking on AWS

Azure

如果希望在群集外部可以路由Pod IP地址,则必须使用Azure CNI插件。这由AKS和Calico进行网络策略支持。Pod IP地址是从基础VNET分配的。

Policy IPAM CNI Overlay Routing
Calico Azure Azure No VPC/Native

如果要使用AKS,但由于IP地址范围耗尽的问题而无法从基础VNET分配Pod IP,则可以将Calico与Azure云提供商集成一起使用。它使用host-local IPAM为每个节点分配/24地址段,并为这些/24地址段在群集的基础VNET子网中编程路由。在群集/VNET子网外部无法路由Pod IP,因此如果需要,可以在多个群集中使用相同的Pod IP地址范围(CIDR)。

注意:在某些AKS文档中将其称为kubenet + Calico,但实际上是带有Azure云提供程序的Calico CNI,并且不使用kubenet插件。

Policy IPAM CNI Overlay Routing
Calico Host Local Calico No VPC/Native

如果不使用AKS,而是希望避免依赖于特定的云提供商,或者由于IP地址范围耗尽的问题而无法从基础VNET分配Pod IP,那么我们建议使用Calico的overlay + cross-subnet模式。Pod IP将无法在集群外部路由,但是可以在不依赖基础云网络的情况下将集群扩展到Kubernetes的极限。

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

可以在此短视频中了解有关Azure上Kubernetes Networking的更多信息,包括上述每个选项的工作原理:Everything you need to know about Kubernetes networking on Azure

Google Cloud

如果想在集群外部路由Pod IP地址,则必须将Google云提供商集成与host-local IPAM CNI插件结合使用。GKE和Calico都为网络策略提供了支持。从基础VPC分配Pod IP地址,并自动将相应的Alias IP地址分配给节点。

Policy IPAM CNI Overlay Routing
Calico Host Local Calico No VPC/Native

如果希望避免依赖特定的云提供商,或者由于IP地址范围耗尽的挑战而无法从基础VPC分配Pod IP,那么我们建议使用Calico的overlay模式。由于Google云网络是纯L3网络,因此不支持cross-subnet模式。Pod IP将无法在集群外部路由,但是可以在不依赖基础云网络的情况下将集群扩展到Kubernetes的极限。

推荐方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

替代方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico IPIP BGP

可以在此短片中了解有关Google云上的Kubernetes Networking的更多信息,包括上述每个选项的工作原理:Everything you need to know about Kubernetes networking on Google cloud

IBM Cloud

如果使用的是IBM Cloud,则建议使用IKS,该产品具有内置Calico的功能,可提供cross-subnet +IPIP模式的网络模式。除了为Pod提供网络策略外,IKS还使用Calico网络策略来保护群集中的主机节点。

Policy IPAM CNI Overlay Routing
Calico Calico Calico IPIP BGP

Anywhere

上面的环境列表显然并不详尽。理解本指南中的概念和解释有助于确定适合的环境。如果仍然不确定,则可以通过Calico用户的Slack或Discourse论坛寻求建议。记住,如果要使用,而不想担心各种选项,则可以在几乎任何环境中以VXLAN + overlay模式运行Calico。

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

简介

边界网关协议(BGP)是互联网涉及的核心技术之一,它使网络能够相互通信。了解如何使用此工具来定义网络拓扑,不仅可以让我们更好地了解互联网络,还可以将这种鲁棒性应用到自己的网络。

在本教程结束时,我们将熟悉BGP的核心概念,并能够把相关的术语传达给其他人。此外,我们还将学会使用BIRD的用户界面建立对等会话并开始发布路由。

本文使用docker容器实现这一目标。为了完成本教程,需要确保已安装dockerdocker-compose

准备

首先,需要克隆bird_examples_docker项目。

1
2
3
ncatelli@ofet> git clone https://github.com/ncatelli/bird_examples_docker.git
ncatelli@ofet> cd bird_examples_docker
ncatelli@ofet> docker-compose up -d

上述命令将创建三个容器(peer1peer2peer3),所有这些容器均已安装BIRD并已建立对等会话。如果还不知道这意味着什么,请不要担心,我们将在设置好BGP并准备就绪后对其进行介绍。

首先连接到peer1并检查所有设置是否正确。

1
2
3
4
5
6
7
8
9
ncatelli@ofet> docker-compose exec peer1 bash
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:03
device1 Device master up 02:36:03
direct1 Direct master up 02:36:03
peer2 BGP master up 02:36:08 Established
peer3 BGP master up 02:36:07 Established

如果发现peer2peer3已经显示Established,则说明一切正常,我们已准备就绪。在开始使用之前,将简要介绍BGP的工作原理。

原理概述

边界网关协议(BGP)是一种外部网关协议,用于在自治系统之间交换路由信息。自治系统(AS)是路由前缀和策略的组织单位。这些AS由唯一的16位(后来扩展到32位)自治系统编号(ASN)标识。例如,Facebook的ASN为32934或通常显示为AS32934。BGP的强大之处在于其在成千上万个分散式AS之间传递路由协议和策略的能力。

互联网以及许多其他网络由相互之间进行通信的许多自治系统组成。对等会话促进了这种通信,该会话允许两个AS交换策略,路由和链接状态。所有这些信息都在两个BGP守护程序之间交换,这两个守护程序在TCP的179端口上进行侦听。

虽然BGP被认为是用于在Internet上的大型组织之间进行路由的外部网关协议,但它也可以在AS中使用,以使网络工程师能够控制其内部网络的拓扑。这是eBGP和iBGP术语的来源。iBGP将是本教程其余部分的重点。现在,我们将开始使用BIRD及其交互式命令行工具birdc尝试这些对等会话。

BIRD简介

BIRD是功能齐全的路由守护程序,它支持许多不同的路由协议,包括BGP。BIRD提供了一种简单的配置格式和命令行,用于与会话进行交互。BIRD还内置了对IPv4和IPv6的支持,以及与这两种协议一起使用的相应工具。

检查BGP会话

与我们验证是否正确配置了docker环境的方式类似,我们可以通过运行以下命令查看正在运行的会话:

1
2
3
4
5
6
7
8
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:02
device1 Device master up 02:36:02
direct1 Direct master up 02:36:02
peer2 BGP master up 02:36:07 Established
peer3 BGP master up 02:36:06 Established

这给了我们很多信息。但是,让我们关注最后两个条目,peer2peer3。我们可以看到它们都是BGP协议,并且info字段显示已经Established。这些条目中的每一个都对应于peer1peer2peer3打开的BGP会话。为了演示这些值与正在运行的会话之间的关系,让我们在peer2上停止Bird服务。在新的终端窗口中,运行以下命令来停止peer2,模拟网络故障。

1
2
ncatelli@ofet> docker-compose stop peer2
Stopping bird_examples_peer2_1 ... done
1
2
3
4
5
6
7
8
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:02
device1 Device master up 02:36:02
direct1 Direct master up 02:36:02
peer2 BGP master start 02:43:38 Connect Socket: Connection closed
peer3 BGP master up 02:36:06 Established

通过重新启动peer2,BIRD应该重新启动,并且随后应重新建立对等会话。

1
2
ncatelli@ofet> docker-compose start peer2
Starting peer2 ... done
1
2
3
4
5
6
7
8
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:02
device1 Device master up 02:36:02
direct1 Direct master up 02:36:02
peer2 BGP master up 02:46:29 Established
peer3 BGP master up 02:36:06 Established

通过停止peer2上的bird守护程序,我们使端口179上的TCP连接在peer1peer2之间关闭。这样做会将我们的对等会话从Established更改为Connect。这两个状态对应于许多BGP状态中的两个,但是出于本教程的考虑,我们将仅关注Established,并将所有其他值视为未建立。对于那些更好奇的人,可以在Wikipedia上有关BGP的文章中找到有关会话状态的更多信息。

配置BGP会话

尽管现在知道了如何检查会话是否已经建立,但了解这些会话的配置也很重要。为此,我们需要深入研究bird配置文件。让我们看一下peer1上的/etc/bird下的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@peer1:~# cat /etc/bird/bird.conf
router id 10.0.0.10;

protocol kernel {
metric 0;
import none;
learn;
export all;
}

protocol device {
}

protocol direct {
}

protocol bgp peer2 {
local as 64512;
neighbor 10.0.0.11 as 64513;
import all;
export all;
}

protocol bgp peer3 {
local as 64512;
neighbor 10.0.100.11 as 64514;
import all;
export all;

我们可以看到建立这些初始会话所需的配置非常少。为了更深入地了解这项工作的真正作用,我们将专注于一个特定块:bgp peer2

1
2
3
4
5
6
protocol bgp peer2 {
local as 64512;
neighbor 10.0.0.11 as 64513;
import all;
export none;
}

在本教程的前面,我们讨论了eBGP和iBGP之间的区别以及大型AS如何使用唯一的ASN标识自己。但是,一小部分可用的ASN已保留给专用iBGP使用。这个范围是64512-65534。知道了这一点,我们可以看到我们已经将私有范围中的ASN分配给了peer2peer1被分配了ASN 64512。

查看下一条语句,我们可以看到具有IP和附加AS的邻居语句。该IP对应于我们尝试与之建立会话的主机或BGP术语中的邻居,而64513对应于我们分配给peer2主机的AS。可以通过查看peer2上的配置文件来确认这一点。

1
2
3
4
5
6
root@peer2:/# grep -A4 peer1 /etc/bird/bird.conf
protocol bgp peer1 {
local as 64513;
neighbor 10.0.0.10 as 64512;
export none;
}

协议BGP块中的这两个指令处理会话的初始建立。

虽然建立和维护会话对于BGP的运行至关重要,但仅建立会话无法路由任何流量。在下一节中,我们将探讨配置文件中的其他一些元素,以及如何使用它们来发现和宣布节点之间的路由。在继续进行此操作之前,先回顾一下我们当前的拓扑。

当前,我们的网络中有三个节点,peer1(AS64512),peer2(AS64513)和peer3(AS64514)。这些配置在同一广播域中,但是对等的结构类似于peer3 <-> peer1 <-> peer2。这种结构允许通过我们的路由服务器peer1peer2peer3进行路由通信。在继续进行本教程的下一步,即发布路由时,请牢记此拓扑。

IP地址 节点名 AS号
10.0.0.10 peer1 64512
10.0.0.11 peer2 64513
10.0.100.11 peer3 64514

BGP发布路由

内核协议

在开始发布Bird守护程序之间的路由之前,我们应该首先了解BIRD如何在Linux内核和BIRD守护程序之间传递路由。这就是我们前面看到的内核协议块起作用的地方。

1
2
3
4
5
6
protocol kernel {
metric 0;
learn;
import none;
export all;
}

kernel块中可以指定许多选项,并且可以在此处找到关于这些选项的更多信息,但是我们要执行的大部分操作由导入/导出定义。

1
import none;

告诉BIRD不要将路由从内核路由表中读取到BIRD中。我们将通过直接协议(即配置)获取路由。

1
export all;

告诉BIRD将其他公告了解的所有路由导出到内核的路由表中。这使我们可以实际利用此主机上的任何已获取的路由。

1
metric 0;

度量标准值由内核用来确定路由的优先级,并选择优先级最低的路由。在这种情况下,我们将其设置为0或未定义,以便我们首选本地路由。

1
learn;

最后,我们将设置学习指令,该指令将允许其他守护程序从内核路由表中学习路由。

发现直接路由

现在,我们已经配置了BIRD守护程序以将路由直接推送到内核路由表,我们将需要配置对等端以发现本地直接路由。由于我们会将这些路由直接添加到我们的环回接口,因此在选择的编辑器中,将直接协议配置为仅使用lo接口。

1
2
3
4
5
6
ncatelli@ofet> grep -A2 direct conf/peer2/etc/bird/bird.conf
protocol direct {
interface "lo";
}
ncatelli@ofet> docker-compose restart peer2
Restarting bird_examples_peer2_1 ... done

由于我们的网络上也有peer3,因此在此主机上进行相同操作,以防止宣布其他任何路由。

1
2
3
4
5
6
ncatelli@ofet> grep -A2 direct conf/peer3/etc/bird/bird.conf
protocol direct {
interface "lo";
}
ncatelli@ofet> docker-compose restart peer3
Restarting bird_examples_peer3_1 ... done

此时,除了默认的10.0.0.0子网(可以使用birdc进行验证)以外,我们将没有其他路由学习和发布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@peer2:/# birdc show route all
BIRD 1.6.6 ready.
10.0.0.0/24 via 10.0.0.10 on eth0 [peer1 03:05:02] ! (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.0.10
BGP.local_pref: 100
10.0.100.0/24 via 10.0.0.10 on eth0 [peer1 03:05:02] * (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.0.10
BGP.local_pref: 100

过滤引用和导出

与内核模块类似,导出和导入可用于控制BGP对等方导入和导出的内容。首先,我们探讨过滤的概念以及如何将其用于控制将宣布或导出哪些路由。

BIRD中的过滤器基本上是在路由上执行的函数,返回接受或拒绝。这使我们能够应用一种简单的编程语言来为我们的路由策略添加逻辑。过滤器可以包含任何内容,从单个语句到非常复杂的逻辑。首先,让我们将none和all指令重新实现为过滤器,然后将它们添加到include指令上方的bird.conf文件中。

1
2
3
filter accept_all {
accept;
};
1
2
3
filter reject_all {
reject;
};

现在我们已经有了过滤器,让我们在我们的协议块之一的导入/导出指令中实现它们。在主机peer1上,让我们看一下peer2块。

1
2
3
4
5
6
protocol bgp peer2 {
local as 64512;
neighbor 10.0.0.11 as 64513;
import filter accept_all;
export filter accept_all;
}

从功能上讲,这与我们的原始配置相同,但是现在我们可以使用进一步的逻辑扩展这些设置。通过进一步研究过滤器脚本语言,可以了解这些过滤器的功能。为了扩展我们所学的知识,让我们在peer2上的bird.conf中创建一个过滤器,以控制要向peer1发布的路由。

1
2
3
4
5
6
filter export_subnets {
if net ~ [ 192.168.5.5/32 ] then {
accept;
}
reject;
}

最后,我们需要在peer2上更新peer1以使用此导出过滤器。

1
2
3
4
5
6
root@peer2:/# grep -A4 peer1 /etc/bird/bird.conf
protocol bgp peer1 {
local as 64513;
neighbor 10.0.0.10 as 64512;
export filter export_subnets;
}
1
2
3
ncatelli@ofet> docker-compose restart peer1 peer2
Restarting bird_examples_peer2_1 ... done
Restarting bird_examples_peer1_1 ... done

发布路由

现在,我们有了开始发布peer1peer2之间的路由所需的所有构造块。在此之前,让我们回顾一下我们所做的事情。首先,我们已将BIRD守护程序配置为使用我们的内核协议在其内部路由表和内核路由表之间进行通信。我们已将BIRD守护程序配置为从具有直接协议的环回接口中学习路由。我们还配置了peer1从其他对等节点导入路由并导出这些路由。最终,我们将peer2配置为仅使用export_subnets过滤器将192.168.5.5/32导出到peer1。但是,目前我们还没有从peer2通告到peer1的路由。

1
2
3
4
root@peer1:/# ip route  
default via 10.0.0.1 dev eth0
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.10
10.0.100.0/24 dev eth1 proto kernel scope link src 10.0.100.10

此时,所有设置已经完成,因此可以从环回接口中学习路由。通过将IP添加到peer2的环回接口上,我们应该能够看到路由的发布。

1
root@peer2:/# ip a add 192.168.5.5/32 dev lo

现在,如果我们同时查看peer1上的birdc和内核路由表,我们应该可以看到peer1上新IP的路由。

1
2
3
4
5
6
root@peer1:~# ip route
default viia 10.0.2.2 dev eth0
10.0.0.0/24 dev eth1 proto kernel scope link src 10.0.0.10
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15
10.0.100.0/24 dev eth2 proto kernel scope link src 10.0.100.10
192.168.5.5 via 10.0.0.11 dev eth1 proto bird
1
2
3
4
5
6
7
8
9
10
11
12
root@peer1:/# birdc show route all
BIRD 1.6.6 ready.
10.0.0.0/24 dev eth0 [direct1 03:10:33] * (240)
Type: device unicast univ
10.0.100.0/24 dev eth1 [direct1 03:10:33] * (240)
Type: device unicast univ
192.168.5.5/32 via 10.0.0.11 on eth0 [peer2 03:12:39] * (100) [AS64513i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64513
BGP.next_hop: 10.0.0.11
BGP.local_pref: 100

ping将显示我们现在可以从peer1向该主机发送流量。

1
2
3
4
5
6
7
root@peer1:/# ping -c 1 192.168.5.5
PING 192.168.5.5 (192.168.5.5) 56(84) bytes of data.
64 bytes from 192.168.5.5: icmp_seq=1 ttl=64 time=0.135 ms

--- 192.168.5.5 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.135/0.135/0.135/0.000 ms

现在可以看到这些路由已通过peer1通告到peer3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@peer3:/# birdc show route all
BIRD 1.6.6 ready.
10.0.0.0/24 via 10.0.100.10 on eth0 [peer3 03:10:37] * (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.100.10
BGP.local_pref: 100
10.0.100.0/24 via 10.0.100.10 on eth0 [peer3 03:10:37] ! (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.100.10
BGP.local_pref: 100
192.168.5.5/32 via 10.0.100.10 on eth0 [peer3 03:12:38] * (100) [AS64513i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512 64513
BGP.next_hop: 10.0.100.10
BGP.local_pref: 100
1
2
3
4
5
6
7
root@peer3:/# ping -c 1 192.168.5.5
PING 192.168.5.5 (192.168.5.5) 56(84) bytes of data.
64 bytes from 192.168.5.5: icmp_seq=1 ttl=63 time=0.082 ms

--- 192.168.5.5 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.082/0.082/0.082/0.000 ms
1
2
3
4
root@peer3:/# traceroute 192.168.5.5
traceroute to 192.168.5.5 (192.168.5.5), 64 hops max
1 10.0.100.10 0.005ms 0.003ms 0.003ms
2 192.168.5.5 0.003ms 0.003ms 0.003ms

我们也可以通过查看AS PATH来了解情况。通过查看与birdc中的路由关联的AS PATH,可以看到从64513到64512的路由在到达peer3之前发布。

因为将peer1配置为将路由导出到peer3,并且由于peer3配置为从peer1导入路由,所以我们能够将此路由获取到peer3的BIRD路由表中。然后,由于我们已将内核协议配置为在BIRD中导出路由,因此这些路由会将其放入peer3的内核路由表中。

下一步

在这个简单的教程中,我们探讨了许多概念,但是,我们几乎没有涉及bird的概念。随时使用此示例,以发布和过滤路由的方式进一步探索。在以后的教程中,我们将更深入地探讨BGP的工作方式以及用于确定路由的过程,包括如何通信和本地优先级以及BGP守护程序如何使用它们来选择通往服务器的最佳路径。我们还将探讨什么是任播IP,以及如何使用BGP配置高可用性,以及如何使用过滤策略代替直接接口策略来控制向每个节点声明哪些前缀。BGP可以为我们提供对网络拓扑的大量控制,并且了解如何使用BGP可以更好地调整网络以适应自己的需求。