hello云胜

技术与生活

0%

有状态应用

为什么要有StatefulSet,使用deployment+pv可以不可以?

单单就存储数据而言使可以的。但是有状态应用还有更复杂的要求,

多个实例的依赖关系、启动顺序,必须主备关系

外界的客户端也可能要使用固定的网络标识来访问实例,而且这些信息还必须要保证在 Pod 重启后不变。

所以才有了StatefulSet

和 DaemonSet 类似,StatefulSet 也可以看做是 Deployment 的一个特例,它也不能直接用 kubectl create 创建样板文件,但它的对象描述和 Deployment 差不多,你同样可以把 Deployment 适当修改一下,就变成了 StatefulSet 对象。

特点

  • StatefulSet 所管理的 Pod 不再是随机的名字了,而是有了顺序编号,从 0 开始分别被命名

  • 域名:Service 发现这些 Pod 不是一般的应用,而是有状态应用,需要有稳定的网络标识,所以就会为 Pod 再多创建出一个新的域名,格式是“Pod 名. 服务名. 名字空间.svc.cluster.local”。当然,这个域名也可以简写成“Pod 名. 服务名”。这样在 StatefulSet 里的这两个 Pod 都有了各自的域名,也就是稳定的网络标识

    Service 自己会有一个域名,格式是“对象名. 名字空间”,每个 Pod 也会有一个域名,形式是“IP 地址. 名字空间”。但因为 IP 地址不稳定,所以 Pod 的域名并不实用,一般我们会使用稳定的 Service 域名。

    域名必须通过Service对象才能实现

  • Headless Service:Service 原本的目的是负载均衡,应该由它在 Pod 前面来转发流量,但是对 StatefulSet 来说,这项功能反而是不必要的,因为 Pod 已经有了稳定的域名,外界访问服务就不应该再通过 Service 这一层了。所以,从安全和节约系统资源的角度考虑,我们可以在 Service 里添加一个字段 clusterIP: None ,告诉 Kubernetes 不必再为这个对象分配 IP 地址

存储

pv,pvc, storageClass

PV 实际上就是一些存储设备、文件系统

PV 属于集群的系统资源,是和 Node 平级的一种对象,Pod 对它没有管理权,只有使用权。

但是不同存储差别太大,让 Pod 直接去选择 PV 也很不灵活

于是 Kubernetes 就又增加了两个新对象,PersistentVolumeClaimStorageClass,用的还是“中间层”的思想,把存储卷的分配管理过程再次细化。

PersistentVolumeClaim,简称 PVC,从名字上看比较好理解,就是用来向 Kubernetes 申请存储资源的。PVC 是给 Pod 使用的对象,它相当于是 Pod 的代理,代表 Pod 向系统申请 PV。一旦资源申请成功,Kubernetes 就会把 PV 和 PVC 关联在一起,这个动作叫做“绑定”(bind)。

但是,系统里的存储资源非常多,如果要 PVC 去直接遍历查找合适的 PV 也很麻烦,所以就要用到 StorageClass。

StorageClass 的作用有点像 IngressClass,它抽象了特定类型的存储系统(比如 Ceph、NFS),在 PVC 和 PV 之间充当“协调人”的角色,帮助 PVC 找到合适的 PV。也就是说它可以简化 Pod 挂载“虚拟盘”的过程,让 Pod 看不到 PV 的实现细节

到目前为止,PV 还是需要人工管理。为了使pv的创建自动化,可以用 StorageClass 绑定一个 Provisioner 对象,而这个 Provisioner 就是一个能够自动管理存储、创建 PV 的应用。 Kubernetes 里就是“动态存储卷”的概念。

Kubernetes 里每类存储设备都有相应的 Provisioner 对象。比如nfs,NFS Provisioner 也是以 Pod 的形式运行在 Kubernetes 里的,在 GitHub 的 deploy 目录里是部署它所需的 YAML 文件,一共有三个,分别是 rbac.yaml、class.yaml 和 deployment.yaml。

因为有了 Provisioner,我们就不再需要手工定义 PV 对象了,只需要在 PVC 里指定 StorageClass 对象,它再关联到 Provisioner。我们不需要直接定义 PV 对象,但由于有 NFS Provisioner,它就自动创建一个 PV。

pvc–> storageClass –> provisioner –> pv

开发自己的helm chart

helm chart项目结构

创建一个新的helm chart项目

1
helm create hello-helm

看一下创建出的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@paas-m-k8s-master-1 hello-helm]# tree hello-helm/
hello-helm/
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── serviceaccount.yaml
│   ├── service.yaml
│   └── tests
│   └── test-connection.yaml
└── values.yaml

3 directories, 10 files

各文件目录的作用:

Chart.yaml

Chart.yaml ,声明了当前 Chart 的名称、版本等基本信息,这些信息会在该 Chart 被放入仓库后,供用户使用时查看

文件的字段也比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v2  // 不要改
name: hello-helm // chart应用名,一般建好了也不用改
description: 描述

# 类型,可以是 'application' 或者 'library'.
# application类型说明这个chart是个应用,可以部署
# library类型的chart不能直接部署,是作为其他chart的依赖库使用
type: application

# chart的版本,每次发布要递增
version: 0.1.0

# 内部应用的版本,比如我这个chart是部署redis的,这里应该写redis的版本
appVersion: "5.0.8"

values.yaml

我们在部署时修改的配置参数抽象出来,存放在values.yaml文件里。

当部署chart时,可以根据自己的需求就行修改。

values.yaml中的配置参数会被使用到templates 目录下的部署文件中。

templates 目录

templates 文件夹内放的是应用部署所需要使用的 YAML 文件,比如 Deployment 和 Service的yaml文件。

我们打开自动生成的一个deployment.yaml文件

![image-20230627111524840](D:\github\docs\云原生\k8s\开发自己的helm chart.assets\image-20230627111524840.png)

这里会用到很多helm template的语法。

helm在部署应用时,会解析template下的文件,对文件进行“编译”,然后交给K8s执行。

具体内容可以查看官方文档Helm | Getting Started

这里只简单说说重要的

  1. 模板命令放在{{ }}之间

  2. 使用values.yaml中的配置,就用{{ .Values.参数名 }}

    比如imagePullPolicy: {{ .Values.image.pullPolicy }}

  3. 可以使用Chart.yaml中的数据

比如image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"

​ 用到了chart的AppVersion

  1. 使用本次install的版本信息,.Release

  2. 当前执行的template相关信息,.Template

  3. 函数

    template里可以使用一些内置函数。比如default函数,设置一个默认值。如果前面忘记设置这个默认值会生效。

    还有截图中的nindent函数控制缩进

    Helm | Template Function List

  4. 管道符 | 和linux下的用法一样

  5. 条件控制语句

    • if/else, 用来创建条件语句
    • with, 用来限定作用域。
    • range, 提供”for each”类型的循环
  6. {{-`的含义。表示向左删除空白, 而` -}}表示右边的空格应该被去掉。 一定注意空格就是换行

  7. include 可以引入定义的模板,关于定义模板查看官方文档。上面截图中include语句后面的.,是传给模板的作用域。

charts目录

charts目录下可以包含其他chart。

不必须,入门的时候先不管这个。

其他文件

NOTES.txt:显示给用户的帮助文档。用户执行helm install时看到的

_helpers.tpl:模板文件,可以复用。入门阶段不要管这个。

.helmignore:来指定你不想包含在你的helm chart中的文件

好了,现在把各个文件大体看一下即可。然后我们把templates目录下的文件都删除。开始创建自己的helm chart。

helm Chart初体验

之前我们部署过一个单节点的redis

K8S上部署一个redis (qq.com)

在部署过程中,我们依次创建了configmap,pvc,deployment,svc等资源。

现在我们把这个部署创建成自己的helm chart

依次建好几个文件

入门起见,这里也很简单,你一看就明白。

Chart.yaml

1
2
3
4
5
6
7
8
9
apiVersion: v2
name: my-redis
description: simple redis demo

type: application

version: 1.0.0

appVersion: "5.0.5"

values.yaml

1
2
3
4
5
6
7
8
9
10
name: my-redis

image: redis:5.0.5-alpine
replicas: 1
storageClassName: nfs-client
storageSize: 1Gi

redis:
dir: /data/data
requirepass: 123456

templates/NOTES.txt

1
2
3
4
5
6
7
8
9
10
Thank you for installing {{ .Chart.Name }}.

Your release is named {{ .Release.Name }}.

redis 密码为: {{ .Values.redis.requirepass }}

To learn more about the release, try:

$ helm -n {{ .Release.Namespace }} status {{ .Release.Name }}
$ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }}

templates/几个部署yaml

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.name }}-config
data:
redis-config: |
dir {{ .Values.redis.dir }}
requirepass {{ .Values.redis.requirepass }}
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Values.name }}-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.storageSize }}
storageClassName: {{ .Values.storageClassName }}
volumeMode: Filesystem

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.name }}-deploy
labels:
app: redis
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: {{ .Values.name }}-pvc
- name: config
configMap:
name: {{ .Values.name }}-config
items:
- key: redis-config
path: redis.conf
containers:
- name: redis
image: {{ .Values.image }}
command:
- "sh"
- "-c"
- "redis-server /data/conf/redis.conf"
ports:
- containerPort: 6379
protocol: TCP
resources:
limits:
cpu: "1"
volumeMounts:
- name: redis-data
mountPath: {{ .Values.redis.dir }}
- name: config
mountPath: /data/conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.name }}-svc
labels:
app: redis
spec:
type: NodePort
ports:
- name: redis-port
port: 6379
targetPort: 6379
selector:
app: redis

部署

1
2
3
4
5
6
kubectl create ns my-redis
// 安装
helm -n my-redis install my-redis .

// 调试修改代码后升级安装
helm -n my-redis upgrade my-redis .

![image-20230627173840903](D:\github\docs\云原生\k8s\开发自己的helm chart.assets\image-20230627173840903.png)

好了,效果还可以。

总体感受是,helm chart写起来不难,它内置的函数很多,可以编写比较复杂的流程。

将变量提取到values文件中,避免了每次部署需要去多个文件修改变量的问题,减少了出错的可能。

开发自己的helm chart

helm chart项目结构

创建一个新的helm chart项目

1
helm create hello-helm

看一下创建出的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@paas-m-k8s-master-1 hello-helm]# tree hello-helm/
hello-helm/
├── charts
├── Chart.yaml
├── templates
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── serviceaccount.yaml
│   ├── service.yaml
│   └── tests
│   └── test-connection.yaml
└── values.yaml

3 directories, 10 files

各文件目录的作用:

Chart.yaml

Chart.yaml ,声明了当前 Chart 的名称、版本等基本信息,这些信息会在该 Chart 被放入仓库后,供用户使用时查看

文件的字段也比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v2  // 不要改
name: hello-helm // chart应用名,一般建好了也不用改
description: 描述

# 类型,可以是 'application' 或者 'library'.
# application类型说明这个chart是个应用,可以部署
# library类型的chart不能直接部署,是作为其他chart的依赖库使用
type: application

# chart的版本,每次发布要递增
version: 0.1.0

# 内部应用的版本,比如我这个chart是部署redis的,这里应该写redis的版本
appVersion: "5.0.8"

values.yaml

我们在部署时修改的配置参数抽象出来,存放在values.yaml文件里。

当部署chart时,可以根据自己的需求就行修改。

values.yaml中的配置参数会被使用到templates 目录下的部署文件中。

templates 目录

templates 文件夹内放的是应用部署所需要使用的 YAML 文件,比如 Deployment 和 Service的yaml文件。

我们打开自动生成的一个deployment.yaml文件

![image-20230627111524840](D:\github\docs\云原生\k8s\开发自己的helm chart.assets\image-20230627111524840.png)

这里会用到很多helm template的语法。

helm在部署应用时,会解析template下的文件,对文件进行“编译”,然后交给K8s执行。

具体内容可以查看官方文档Helm | Getting Started

这里只简单说说重要的

  1. 模板命令放在{{ }}之间

  2. 可以使用Chart.yaml中的数据

​ 用到了chart的AppVersion

  1. 使用本次install的版本信息,.Release

  2. 当前执行的template相关信息,.Template

  3. 函数

    template里可以使用一些内置函数。比如default函数,设置一个默认值。如果前面忘记设置这个默认值会生效。

    还有截图中的nindent函数控制缩进

    Helm | Template Function List

  4. 管道符 | 和linux下的用法一样

  5. 条件控制语句

    • if/else, 用来创建条件语句
    • with, 用来限定作用域。
    • range, 提供”for each”类型的循环
  6. include 可以引入定义的模板,关于定义模板查看官方文档。上面截图中include语句后面的.,是传给模板的作用域。

charts目录

charts目录下可以包含其他chart。

不必须,入门的时候先不管这个。

其他文件

NOTES.txt:显示给用户的帮助文档。用户执行helm install时看到的

_helpers.tpl:模板文件,可以复用。入门阶段不要管这个。

.helmignore:来指定你不想包含在你的helm chart中的文件

好了,现在把各个文件大体看一下即可。然后我们把templates目录下的文件都删除。开始创建自己的helm chart。

helm Chart初体验

之前我们部署过一个单节点的redis

K8S上部署一个redis (qq.com)

在部署过程中,我们依次创建了configmap,pvc,deployment,svc等资源。

现在我们把这个部署创建成自己的helm chart

依次建好几个文件

入门起见,这里也很简单,你一看就明白。

Chart.yaml

1
2
3
4
5
6
7
8
9
apiVersion: v2
name: my-redis
description: simple redis demo

type: application

version: 1.0.0

appVersion: "5.0.5"

values.yaml

1
2
3
4
5
6
7
8
9
10
name: my-redis

image: redis:5.0.5-alpine
replicas: 1
storageClassName: nfs-client
storageSize: 1Gi

redis:
dir: /data/data
requirepass: 123456

templates/NOTES.txt

1
2
3
4
5
6
7
8
9
10
Thank you for installing {{ .Chart.Name }}.

Your release is named {{ .Release.Name }}.

redis 密码为: {{ .Values.redis.requirepass }}

To learn more about the release, try:

$ helm -n {{ .Release.Namespace }} status {{ .Release.Name }}
$ helm -n {{ .Release.Namespace }} get all {{ .Release.Name }}

templates/几个部署yaml

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.name }}-config
data:
redis-config: |
dir {{ .Values.redis.dir }}
requirepass {{ .Values.redis.requirepass }}
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Values.name }}-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.storageSize }}
storageClassName: {{ .Values.storageClassName }}
volumeMode: Filesystem

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.name }}-deploy
labels:
app: redis
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: {{ .Values.name }}-pvc
- name: config
configMap:
name: {{ .Values.name }}-config
items:
- key: redis-config
path: redis.conf
containers:
- name: redis
image: {{ .Values.image }}
command:
- "sh"
- "-c"
- "redis-server /data/conf/redis.conf"
ports:
- containerPort: 6379
protocol: TCP
resources:
limits:
cpu: "1"
volumeMounts:
- name: redis-data
mountPath: {{ .Values.redis.dir }}
- name: config
mountPath: /data/conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.name }}-svc
labels:
app: redis
spec:
type: NodePort
ports:
- name: redis-port
port: 6379
targetPort: 6379
selector:
app: redis

部署

1
2
3
4
5
6
kubectl create ns my-redis
// 安装
helm -n my-redis install my-redis .

// 调试修改代码后升级安装
helm -n my-redis upgrade my-redis .

![image-20230627173840903](D:\github\docs\云原生\k8s\开发自己的helm chart.assets\image-20230627173840903.png)

好了,效果还可以。

总体感受是,helm chart写起来不难,它内置的函数很多,可以编写比较复杂的流程。

将变量提取到values文件中,避免了每次部署需要去多个文件修改变量的问题,减少了出错的可能。

veth

理论

veth peer 是啥?

顾名思义,首先根据veth推测是一种虚拟的以太网卡。peer说明是成对的。

是的,veth peer就是一种成对出现的虚拟网卡设备,它就像水管的两个口,报文从veth peer的一端进去就会由另一端收到。

所以,根据这一个特性,veth peer常用于两个ns直接的通信。

image-20231008104534662

就像把一根水管查到两个水缸中一样。

我们可以使用命令将veth放到任一的ns中,但是真实的网络设备只能在系统主机的ns中,并且一个网络设备只能放在一个ns中。

实践

创建一对veth peer

1
ip link add veth100 type veth peer name veth101

这对veth peer的名字分别是veth100和veth101

查看一下

1
2
3
4
5
6
# ip link list
...
7: veth101@veth100: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 36:61:58:ca:2f:cc brd ff:ff:ff:ff:ff:ff
8: veth100@veth101: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether ae:b4:78:33:73:be brd ff:ff:ff:ff:ff:ff

可以看到新创建的veth peer设备的默认mtu是1500,设备初始状态是DOWN

注意现在这两个veth设备都在主机的network ns中。

将veth101移动到一个mynet ns中

把veth101移进上一节创建的mynet namespace中

1
ip link set veth101 netns mynet

进入ns查看

image-20231008110656799

绑定ip并将设备状态置为up

在上一节我们使用ip link set dev命令设置设备的状态

现在用另一个命令ifconfig。

因为现在两个网卡都没有ip,所以需要配置ip

1
2
ifconfig veth100 10.10.10.100/24 up
ip netns exec mynet ifconfig veth101 10.10.10.101/24 up

查看一下

image-20231008111225006

image-20231008111247516

这样我们的一对veth,一个在主机的网络空间下,一个在mynet网络空间中,

互相ping一下

image-20231008111329631

ok,可以互通

查看下ns里的路由

为什么能通呢?看一下mynet里的路由就明白了

1
2
3
4
# ip netns exec mynet route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
10.10.10.0 0.0.0.0 255.255.255.0 U 0 0 0 veth101

发给10.10.10.100的包会通过veth101出去,因为是veth peer。所以包就到了veth100

通过这个路由我们也能看出,现在在mynet里只能通10.10.10.0/24网段。其他的都通不了。

一个题外话

因为我们现在是直到veth100和101是一对。那如果我们不知道呢?怎么找到一对veth

image-20231008112422789

可以用ip link list。然后根据编号来找

vxlan

简单理解下vxlan,因为不是专业搞网络的,没必要搞得太透彻,只说下vxlan是怎么回事。

理解vxlan之前,先理解两个名词overlay和underlay。

underlay就是指底层的物理网络。

overlay就是在物理网络之上的虚拟覆盖网络。

vxlan就是一种overlay技术。通过三层的网络搭建虚拟的二层网络。

简单来说,就是在underlay网络上使用隧道技术,使用udp协议构建起overlay逻辑网络。

好处是什么?

对原有的网络架构影响小,不需要对原网络做任何改动(因为使用隧道打通),就可在原网络的基础上架设一层新的网络。

vxlan技术是对vlan技术的升级,VLAN技术的缺陷是VLAN Header预留的长度只有12比特,所以最多只能支持划分4096个子网。无法满足现在云计算场景的需要。

云计算场景下,虚拟机/容器会随时大规模迁移,因此网络需要保证一直可用,这就是大二层的概念。但是在保证大二层的同时又需要控制二层的广播域不能太大。

vxlan是建立在三层网络上的,因此要求网络必须三层可达。在三层网络上面搭建一层虚拟的二层网络。

我们提了很多次隧道这个词。要注意隧道tunnel只是一个逻辑上的概念,并没有一个真实的物理实体与其对应。

只是vxlan的通信双方以为互相之间是直接通信,是vxlan为通信双方建立了一个单独的通信通道。

缺点:

因为vxlan是一个二层网络,所以他的报文时mac in udp。

image-20231010095203859

vxlan的封包格式。

ethernet header 就是mac地址头

可以看出,VXLAN报文比原始报文多出了50个字节。这样的话就对小包的传输就不太合算。

而且每次都需要进行vxlan的数据包封包和解包会存在一定的性能损耗

在k8s网络中的应用

在k8s的网络插件flannel和calico中都支持vxlan模式

flannel默认和推荐的方法是使用VxLAN

veth pair对互相之间无法ping通

我们创建一对veth,然后配置ip

1
2
3
[root@t-middle01 ~]# ip link add veth0 type veth peer name veth1
[root@t-middle01 ~]# ifconfig veth0 1.2.3.100/24 up
[root@t-middle01 ~]# ifconfig veth1 1.2.3.200/24 up

![image-20231009093950110](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009093950110.png)

提两个问题?

  1. ping -I 1.2.3.100 1.2.3.200 能不能通?
  2. ping -I veth0 1.2.3.200能不能通?

我的测试结果

  1. ip ping能通
  2. veth0 不通

很困惑。看TCP dump抓包情况。对veth0、veth1、lo回环设备进行监听

ip ping的场景

![image-20231009102248195](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009102248195.png)

可以ping通

![image-20231009101851749](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009101851749-16968179323972.png)

veth0网卡没有任何流量

![image-20231009101932609](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009101932609.png)

veth1网卡也没有任何流量

![image-20231009102012730](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009102012730.png)

发现icmp的请求和响应包都是从lo回环设备上走的。

veth ping的情况

![image-20231009102226169](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009102226169.png)

ping不通

![image-20231009102321044](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009102321044-16968182017864.png)

veth0发起arp请求

因为veth0不知道1.2.3.200的MAC地址,于是,veth1广播了arp request

![image-20231009102404295](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009102404295.png)

veth1也发起arp请求,找1.2.3.200

这个有点奇怪。自己就是1.2.3.200。为什么不响应呢?

![image-20231009102436476](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009102436476.png)

lo回环设备返回1.2.3.200无法到达。

什么原因?为什么veth1不响应arp?

经多方查找,发现是系统内核中一些 ARP 相关的默认配置限制所导致的

当前的配置

![image-20231009111321587](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009111321587.png)

修改为

1
2
3
4
5
6
7
8
9
echo 1 > /proc/sys/net/ipv4/conf/veth1/accept_local

echo 1 > /proc/sys/net/ipv4/conf/veth0/accept_local

echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter

echo 0 > /proc/sys/net/ipv4/conf/veth0/rp_filter

echo 0 > /proc/sys/net/ipv4/conf/veth1/rp_filter

再测试,还是不通

再次抓包。现在情况有些不同了。

![image-20231009112532003](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009112532003.png)

veth0的情况,看到发出了arp request。并且收到了arp reply。

![image-20231009113332433](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009113332433.png)

veth1收到了veth0的icmp request。但是没有发出icmp replay

![image-20231009112625377](D:\github\docs\云原生\k8s\基础原理\veth pair对互相之间无法ping通.assets\image-20231009112625377.png)

lo没收到icmp request,但是发出了icmp replay

network namespace

理论

我们都知道容器化技术的两大柱石是linux的namespace和cgroup两大基础能力。

namespace提供资源隔离能力,cgroup提供资源限制的能力。

linux提供6种不同的ns类型,对不同的资源进行隔离。分别是

  • Mount: 隔离文件系统挂载点
  • UTS: 隔离主机名和域名信息
  • IPC: 隔离进程间通信
  • PID: 隔离进程的ID
  • Network: 隔离网络资源
  • User: 隔离用户和用户组的ID

现在我们重点关注network namespace。这是整个k8s网络的基础的基础。

因为有了network的隔离,所以我们进到容器里,感觉就是每个容器好像都有自己的网络设备(虚拟的),每个容器都可以随意监听自己的端口而不用担心端口冲突。

实践

network namespace相关操作命令是在ip netns 命令下。

可以先看一下help

1
2
3
4
5
6
7
8
9
10
11
# ip netns help
Usage: ip netns list
ip netns add NAME
ip netns set NAME NETNSID
ip [-all] netns delete [NAME]
ip netns identify [PID]
ip netns pids NAME
ip [-all] netns exec [NAME] cmd ...
ip netns monitor
ip netns list-id

如果你不能执行ip netns。报这个粗

1
Object "netns" is unknown, try "ip help".

是因为你的linux内核版本不支持。我一开始用的centos6.5不行。然后换了个centos 7的就可以了。

CentOS6.5以前,内核不支持网络namespace,需要升级内核和iproute。升级到6.7以后的都可以。

创建一个network namespace

1
ip netns add mynet

查看一下

1
2
# ip netns list
mynet

在network ns创建后,会在/var/run/netns/目录下创建一个同名的挂载点

1
2
3
]# ls /var/run/netns/ -l
总用量 0
-r--r--r-- 1 root root 0 10月 8 09:56 mynet

使用ip netns exec进入network ns执行操作

查看网卡配置

1
2
3
# ip netns exec mynet ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

目前没有任何配置,只有一个lo 回环设备。

状态还是DOWN的。因此现在ping 本地也是不通的。

1
2
# ip netns exec mynet ping 127.0.0.1
connect: Network is unreachable

开启网络设备

进入ns,使用ip link set dev 命令将lo设备状态置为UP

1
ip netns exec mynet ip link set dev lo up

image-20231008102110586

现在就可以ping通了。

现在我们还是只能ping通本地,还无法和外界通信。

要实现两个ns间互通的目标,需要借助veth peer

关于veth peer我们下节再讲,因为内容会比较多。然后继续我们的实验。

docker网络

端口映射

在启动一个docker容器时,经常指定-p绑定端口。

1
docker run -p 8000:80 --name nginx8000 -d nginx

将主机的8000端口和容器的80端口进行映射

实现的原理就是iptable的nat表中添加相应的规则,将对本机8000端口的访问进行一次dnat操作。转换成对容器ip:容器端口的访问

查看一下本机的iptables

1
iptables -t nat -nL

image-20231011140525875

我们知道docker会创建自定义链DOCKER

然后创建容器如果需要端口映射时,就在里面定义一个DNAT规则

这个DOCKER链被PREROUTING链和OUTPUT链引用。就是在进来和出去的时候都会做dnat

访问外网

默认情况下容器就是可以访问外网的。因为容器的默认网络接口是在docker0网桥上的接口,也就是主机上的本地接口。

只要主机能访问的地址,容器默认就能访问。

其原理是通过Linux系统的转发功能实现的(把主机当交换机)。

如果你的容器不能访问。看看ip_forward是否为1

可以做个实验验证

1
docker exec -it nginx8000 ping 一个ip

发现报错了,因为没有ping命令。

这是我们使用容器经常遇到的一个问题。那怎么办?去容器理安装吗?

其实有个更好的办法

我们之前学过network的4种网络模式,有一个container模式。可以将新容器加到已有容器的网路空间中。

现在我们就可以利用这个特性

1
docker run -it --network container:nginx8000 alpine

启动一个alpine容器,加到nginx8000容器的网络空间下

image-20231011152405931

就可以测试了。是不是很方便

自定义网络cidr

我们说过,docker默认的网段是172.17.0.0/16。可以在启动docker时指定cidr来自定义网段。

注意修改cidr需要保证现在没有运行中的docker。所有最好是启动docker daemon前就做这个。

现在我已经有启动的容器,先全部停掉

然后修改/etc/docker/daemon.json

1
2
3
{
"bip":"172.88.88.1/24"
}

注意bip应该是个有效ip地址。

1
2
3
4
# systemctl stop docker
# ip link set dev docker0 down
# brctl delbr docker0
# systemctl start docker

再启动一个容器看看

1
2
3
4
# docker run -p 8100:80 --name nginx8100 -d nginx
# docker inspect -f "{{ .NetworkSettings.IPAddress }}" nginx8100
172.88.88.2

现在就是我们自定义的cidr段了

新增网络

如果已经启动了很多容器,就不能修改默认的网络了。可以新增一个自己的网络

1
2
# docker network create -d bridge --subnet 166.66.66.0/24 mynet
9d56da8bfe3d233317fff8f623c03a4b74ffaa90b89c7b70821e1f6153385aa7

返回的是网络id

我们创建了一个名为mynet的网络,网段是166.66.66.0/24。使用的网络策略是bridge模式

1
2
3
4
# brctl show
bridge name bridge id STP enabled interfaces
br-9d56da8bfe3d 8000.02424dbc03f4 no
docker0 8000.02426a04ad73 no veth474c3aa

会创建出一个新的网桥br-9d56da8bfe3d

image-20231011171427742

网桥的ip就是166.66.66.1

image-20231011171657885

我们看docker的网络也多了我们的这个

使用

只需要在创建容器时,指定下使用mynet

1
2
3
# docker run -p 8200:80 --name nginx8200 --net mynet -d nginx
# # docker inspect -f "{{ .NetworkSettings.Networks.mynet.IPAddress }}" nginx8200
166.66.66.2

ipip

ipip的全称是IPv4 in IPv4,也就是说在IPv4报文的外面再用一个IPv4报文包起来。

ipip是一种隧道技术。

要使用ipip隧道,首先需要内核模块ipip.ko的支持

首先查看内核是否已经加载ipip模块

1
2
3
4
]# lsmod | grep ipip
ipip 13465 0
tunnel4 13252 1 ipip
ip_tunnel 25163 1 ipip

像这样就是已经支持ipip了。

实践

我们来通过ipip隧道实现两个ns间的互通

  1. 创建两个ns

    1