K8s Network Policy Controller之Kube-router功能介绍

Author: Jimmy Zhang (张浩)

Network Policy

Network Policy是k8s提供的一种资源,用于定义基于pod的网络隔离策略。它描述了一组pod是否可以与其它组pod,以及其它network endpoints进行通信。

Kube-router

kube-router项目的三大功能:

  • Pod Networking
  • IPVS/LVS based service proxy
  • Network Policy Controller

在腾讯云TKE上,Pod Networking功能由基于IAAS层VPC的高性能容器网络实现,service proxy功能由kube-proxy所支持的ipvs/iptables两种模式实现。建议在TKE上,只使用kube-router的Network Policy功能。

在TKE上部署kube-router

腾讯云提供的kube-router版本

腾讯云PAAS团队提供的镜像”ccr.ccs.tencentyun.com/library/kube-router:v1”基于官方的最新版本:v0.2.1

在该项目的开发过程中,腾讯云PAAS团队积极参与社区,持续贡献了一些feature support和bug fix, 列表如下(均已被社区合并):

我们会继续贡献社区,并提供腾讯云镜像的版本升级。

部署kube-router

Daemonset yaml文件:

#kube-router-firewall-daemonset.yaml.zip#

能访问公网,也能访问TKE集群apiserver的机器上,执行以下命令即可完成kube-router部署。

如果集群节点开通了公网IP,则可以直接在集群节点上执行以下命令。

如果集群节点没有开通公网IP, 则可以手动下载和粘贴yaml文件内容到节点, 保存为kube-router-firewall-daemonset.yaml,再执行最后的kubectl create命令。

1
2
3
wget https://ask.qcloudimg.com/draft/982360/9wn7eu0bek.zip
unzip 9wn7eu0bek.zip
kuebectl create -f kube-router-firewall-daemonset.yaml

yaml文件内容和参数说明

kube-router-firewall-daemonset.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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
apiVersion: v1
kind: ConfigMap
metadata:
name: kube-router-cfg
namespace: kube-system
labels:
tier: node
k8s-app: kube-router
data:
cni-conf.json: |
{
"name":"kubernetes",
"type":"bridge",
"bridge":"kube-bridge",
"isDefaultGateway":true,
"ipam": {
"type":"host-local"
}
}
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: kube-router
namespace: kube-system
labels:
k8s-app: kube-router
spec:
template:
metadata:
labels:
k8s-app: kube-router
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
containers:
- name: kube-router
image: ccr.ccs.tencentyun.com/library/kube-router:v1
args: ["--run-router=false", "--run-firewall=true", "--run-service-proxy=false", "--kubeconfig=/var/lib/kube-router/kubeconfig", "--iptables-sync-period=5m", "--cache-sync-timeout=3m"]
securityContext:
privileged: true
imagePullPolicy: Always
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
livenessProbe:
httpGet:
path: /healthz
port: 20244
initialDelaySeconds: 10
periodSeconds: 3
volumeMounts:
- name: lib-modules
mountPath: /lib/modules
readOnly: true
- name: cni-conf-dir
mountPath: /etc/cni/net.d
- name: kubeconfig
mountPath: /var/lib/kube-router/kubeconfig
readOnly: true
initContainers:
- name: install-cni
image: busybox
imagePullPolicy: Always
command:
- /bin/sh
- -c
- set -e -x;
if [ ! -f /etc/cni/net.d/10-kuberouter.conf ]; then
TMP=/etc/cni/net.d/.tmp-kuberouter-cfg;
cp /etc/kube-router/cni-conf.json ${TMP};
mv ${TMP} /etc/cni/net.d/10-kuberouter.conf;
fi
volumeMounts:
- name: cni-conf-dir
mountPath: /etc/cni/net.d
- name: kube-router-cfg
mountPath: /etc/kube-router
hostNetwork: true
tolerations:
- key: CriticalAddonsOnly
operator: Exists
- effect: NoSchedule
key: node-role.kubernetes.io/master
operator: Exists
volumes:
- name: lib-modules
hostPath:
path: /lib/modules
- name: cni-conf-dir
hostPath:
path: /etc/cni/net.d
- name: kube-router-cfg
configMap:
name: kube-router-cfg
- name: kubeconfig
hostPath:
path: /root/.kube/config

args说明:

  1. “–run-router=false”, “–run-firewall=true”, “–run-service-proxy=false”:只加载firewall模块;
  2. kubeconfig:用于指定master信息,映射到主机上的kubectl配置目录/root/.kube/config;
  3. –iptables-sync-period=5m:指定定期同步iptables规则的间隔时间,根据准确性的要求设置,默认5m;
  4. –cache-sync-timeout=3m:指定启动时将k8s资源做缓存的超时时间,默认1m;

NetworkPolicy配置示例

1.nsa namespace下的pod可互相访问,而不能被其它任何pod访问

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: npa
namespace: nsa
spec:
ingress:
- from:
- podSelector: {}
podSelector: {}
policyTypes:
- Ingress

2.nsa namespace下的pod不能被任何pod访问

1
2
3
4
5
6
7
8
9
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: npa
namespace: nsa
spec:
podSelector: {}
policyTypes:
- Ingress

3.nsa namespace下的pod只在6379/TCP端口可以被带有标签app: nsb的namespace下的pod访问,而不能被其它任何pod访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: npa
namespace: nsa
spec:
ingress:
- from:
- namespaceSelector:
matchLabels:
app: nsb
ports:
- protocol: TCP
port: 6379
podSelector: {}
policyTypes:
- Ingress

4.nsa namespace下的pod可以访问CIDR为14.215.0.0/16的network endpoint的5978/TCP端口,而不能访问其它任何network endpoints(此方式可以用来为集群内的服务开访问外部network endpoints的白名单)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: npa
namespace: nsa
spec:
egress:
- to:
- ipBlock:
cidr: 14.215.0.0/16
ports:
- protocol: TCP
port: 5978
podSelector: {}
policyTypes:
- Egress

5.default namespace下的pod只在80/TCP端口可以被CIDR为14.215.0.0/16的network endpoint访问,而不能被其它任何network endpoints访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: npd
namespace: default
spec:
ingress:
- from:
- ipBlock:
cidr: 14.215.0.0/16
ports:
- protocol: TCP
port: 80
podSelector: {}
policyTypes:
- Ingress

附: 测试情况

用例名称 测试结果
不同namespace的pod互相隔离,同一namespace的pod互通 通过
不同namespace的pod互相隔离,同一namespace的pod隔离 通过
不同namespace的pod互相隔离,白名单指定B可以访问A 通过
允许某个namespace访问集群外某个CIDR,其他外部IP全部隔离 通过
不同namespace的pod互相隔离,白名单指定B可以访问A中对应的pod以及端口 通过
以上用例,当source pod 和 destination pod在一个node上时,隔离是否生效 通过

功能测试用例

#kube-router测试用例.xlsx.zip#

kubernetes集群中夺命的5秒DNS延迟

作者: 洪志国

超时问题

客户反馈从pod中访问服务时,总是有些请求的响应时延会达到5秒。正常的响应只需要毫秒级别的时延。

DNS 5秒延时

在pod中(通过nsenter -n tcpdump)抓包,发现是有的DNS请求没有收到响应,超时5秒后,再次发送DNS请求才成功收到响应。

在kube-dns pod抓包,发现是有DNS请求没有到达kube-dns pod, 在中途被丢弃了。

为什么是5秒? man resolv.conf可以看到glibc的resolver的缺省超时时间是5s。

丢包原因

经过搜索发现这是一个普遍问题。
根本原因是内核conntrack模块的bug。

Weave works的工程师Martynas Pumputis对这个问题做了很详细的分析:
https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts

相关结论:

  • 只有多个线程或进程,并发从同一个socket发送相同五元组的UDP报文时,才有一定概率会发生
  • glibc, musl(alpine linux的libc库)都使用”parallel query”, 就是并发发出多个查询请求,因此很容易碰到这样的冲突,造成查询请求被丢弃
  • 由于ipvs也使用了conntrack, 使用kube-proxy的ipvs模式,并不能避免这个问题

问题的根本解决

Martynas向内核提交了两个patch来fix这个问题,不过他说如果集群中有多个DNS server的情况下,问题并没有完全解决。

其中一个patch已经在2018-7-18被合并到linux内核主线中: netfilter: nf_conntrack: resolve clash for matching conntracks

目前只有4.19.rc 版本包含这个patch。

规避办法

规避方案一:使用TCP发送DNS请求

由于TCP没有这个问题,有人提出可以在容器的resolv.conf中增加options use-vc, 强制glibc使用TCP协议发送DNS query。下面是这个man resolv.conf中关于这个选项的说明:

1
2
3
use-vc (since glibc 2.14)
Sets RES_USEVC in _res.options. This option forces the
use of TCP for DNS resolutions.

笔者使用镜像”busybox:1.29.3-glibc” (libc 2.24) 做了试验,并没有见到这样的效果,容器仍然是通过UDP发送DNS请求。

规避方案二:避免相同五元组DNS请求的并发

resolv.conf还有另外两个相关的参数:

  • single-request-reopen (since glibc 2.9)
  • single-request (since glibc 2.10)

man resolv.conf中解释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
single-request-reopen (since glibc 2.9)
Sets RES_SNGLKUPREOP in _res.options. The resolver
uses the same socket for the A and AAAA requests. Some
hardware mistakenly sends back only one reply. When
that happens the client system will sit and wait for
the second reply. Turning this option on changes this
behavior so that if two requests from the same port are
not handled correctly it will close the socket and open
a new one before sending the second request.

single-request (since glibc 2.10)
Sets RES_SNGLKUP in _res.options. By default, glibc
performs IPv4 and IPv6 lookups in parallel since
version 2.9. Some appliance DNS servers cannot handle
these queries properly and make the requests time out.
This option disables the behavior and makes glibc
perform the IPv6 and IPv4 requests sequentially (at the
cost of some slowdown of the resolving process).

笔者做了试验,发现效果是这样的:

  • single-request-reopen
    发送A类型请求和AAAA类型请求使用不同的源端口。这样两个请求在conntrack表中不占用同一个表项,从而避免冲突。
  • single-request
    避免并发,改为串行发送A类型和AAAA类型请求。没有了并发,从而也避免了冲突。

要给容器的resolv.conf加上options参数,有几个办法:

1) 在容器的”ENTRYPOINT”或者”CMD”脚本中,执行/bin/echo 'options single-request-reopen' >> /etc/resolv.conf
2) 在pod的postStart hook中:
1
2
3
4
5
6
7
lifecycle:
postStart:
exec:
command:
- /bin/sh
- -c
- "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"
3) 使用template.spec.dnsConfig (k8s v1.9 及以上才支持):
1
2
3
4
5
template:
spec:
dnsConfig:
options:
- name: single-request-reopen
4) 使用ConfigMap覆盖POD里面的/etc/resolv.conf

configmap:

1
2
3
4
5
6
7
8
9
apiVersion: v1
data:
resolv.conf: |
nameserver 1.2.3.4
search default.svc.cluster.local svc.cluster.local cluster.local ec2.internal
options ndots:5 single-request-reopen timeout:1
kind: ConfigMap
metadata:
name: resolvconf

POD spec:

1
2
3
4
5
6
7
8
9
10
11
12
13
        volumeMounts:
- name: resolv-conf
mountPath: /etc/resolv.conf
subPath: resolv.conf
...

volumes:
- name: resolv-conf
configMap:
name: resolvconf
items:
- key: resolv.conf
path: resolv.conf

5) 使用MutatingAdmissionWebhook

MutatingAdmissionWebhook 是1.9引入的Controller,用于对一个指定的Resource的操作之前,对这个resource进行变更。
istio的自动sidecar注入就是用这个功能来实现的。 我们也可以通过MutatingAdmissionWebhook,来自动给所有POD,注入以上3)或者4)所需要的相关内容。


以上方法中, 1)和2)都需要修改镜像, 3)和4)则只需要修改POD的spec, 能适用于所有镜像。不过还是有不方便的地方:

  • 每个工作负载的yaml都要做修改,比较麻烦
  • 对于通过helm创建的工作负载,需要修改helm charts

方法5)对集群使用者最省事,照常提交工作负载即可。不过初期需要一定的开发工作量。

规避方案三:使用本地DNS缓存

容器的DNS请求都发往本地的DNS缓存服务(dnsmasq, nscd等),不需要走DNAT,也不会发生conntrack冲突。另外还有个好处,就是避免DNS服务成为性能瓶颈。

使用本地DNS缓存有两种方式:

  • 每个容器自带一个DNS缓存服务
  • 每个节点运行一个DNS缓存服务,所有容器都把本节点的DNS缓存作为自己的nameserver

从资源效率的角度来考虑的话,推荐后一种方式。

实施办法

条条大路通罗马,不管怎么做,最终到达上面描述的效果即可。

POD中要访问节点上的DNS缓存服务,可以使用节点的IP。 如果节点上的容器都连在一个虚拟bridge上, 也可以使用这个bridge的三层接口的IP(在TKE中,这个三层接口叫cbr0)。 要确保DNS缓存服务监听这个地址。

如何把POD的/etc/resolv.conf中的nameserver设置为节点IP呢?

一个办法,是设置POD.spec.dnsPolicy为”Default”, 意思是POD里面的/etc/resolv.conf, 使用节点上的文件。缺省使用节点上的/etc/resolv.conf(如果kubelet通过参数–resolv-conf指定了其他文件,则使用–resolv-conf所指定的文件)。

另一个办法,是给每个节点的kubelet指定不同的–cluster-dns参数,设置为节点的IP,POD.spec.dnsPolicy仍然使用缺省值”ClusterFirst”。 kops项目甚至有个issue在讨论如何在部署集群时设置好–cluster-dns指向节点IP: https://github.com/kubernetes/kops/issues/5584