侧边栏壁纸
博主头像
汪洋

即使慢,驰而不息,纵会落后,纵会失败,但一定可以达到他所向的目标。 - 鲁迅

  • 累计撰写 192 篇文章
  • 累计创建 74 个标签
  • 累计收到 120 条评论

Kubernetes - 数据包的生命周期

汪洋
2021-09-22 / 2 评论 / 1 点赞 / 623 阅读 / 45,627 字

一、Linux 命名空间

Linux 命名空间包含了现代容器中的一些基础技术。从高层来看,这一技术允许把系统资源在进程之间进行隔离。例如 PID 命名空间会会把进程 ID 空间进行隔离,这样同一个主机之中的两个进程就能隔离了。这个级别的隔离对容器世界来说是很重要的。没有命名空间的话,A 容器中的进程可能会卸载 B 容器中的文件系统,或者修改 C 容器的主机名,又或删除 D 容器的网卡。将这些资源纳入命名空间进行管理,A 容器甚至无法感知 B、C、D 容器的存在。

  1. Mount:隔离文件系统加载点;
  2. UTS:隔离主机名和域名;
  3. IPC:隔离跨进程通信(IPC)资源;
  4. PID:隔离 PID 空间;
  5. 网络:隔离网络接口;
  6. 用户:隔离 UID/GID 空间;
  7. Cgroup:隔离 cgroup 根目录。

绝大多数容器会使用上述命名空间在容器进程之间进行隔离。要注意 cgroup 命名空间出现较晚,相对其它命名空间来说,用的比较少。

二、容器网络(网络命名空间)

在进入 CNI 和 Docker 之前,首先看看容器网络的核心技术。Linux 内核有不少多租户方面的功能。命名空间对不同种类的资源进行了隔离,网络命名空间隔离的自然就是网络。在主流 Linux 操作系统中都可以简单地用 ip 命令创建网络命名空间。接下来创建两个分别用于服务器和客户端的网络命名空间。

$ ip netns add client
$ ip netns add server
$ ip netns list
  server
  client

创建一对 veth 将命名空间进行连接,可以把 veth 想象为连接两端的网线。

$ ip link add veth-client type veth peer name veth-server
$ ip link list | grep veth
  4: veth-server@veth-client: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
  5: veth-client@veth-server: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

这一对 veth 是存在于主机的网络命名空间的,接下来我们把两端分别置入各自的命名空间:

$ ip link set veth-client netns client
$ ip link set veth-server netns server
$ ip link list | grep veth # doesn’t exist on the host network namespace now

client 命名空间检查一下命名空间中的 veth 状况:

$ ip netns exec client ip link
  1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  5: veth-client@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether ca:e8:30:2e:f9:d2 brd ff:ff:ff:ff:ff:ff link-netnsid 1

然后是 server 命名空间:

$ ip netns exec server ip link
  1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  4: veth-server@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 42:96:f0:ae:f0:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0

接下来给这些网络接口分配 IP 地址并启用:

$ ip netns exec client ip address add 10.0.0.11/24 dev veth-client
$ ip netns exec client ip link set veth-client up
$ ip netns exec server ip address add 10.0.0.12/24 dev veth-server
$ ip netns exec server ip link set veth-server up
$
$ ip netns exec client ip addr
  1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
      link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  5: veth-client@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
      link/ether ca:e8:30:2e:f9:d2 brd ff:ff:ff:ff:ff:ff link-netnsid 1
      inet 10.0.0.11/24 scope global veth-client
         valid_lft forever preferred_lft forever
      inet6 fe80::c8e8:30ff:fe2e:f9d2/64 scope link
         valid_lft forever preferred_lft forever
$
$ ip netns exec server ip addr
  1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
      link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  4: veth-server@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
      link/ether 42:96:f0:ae:f0:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
      inet 10.0.0.12/24 scope global veth-server
         valid_lft forever preferred_lft forever
      inet6 fe80::4096:f0ff:feae:f0c5/64 scope link
         valid_lft forever preferred_lft forever

client 命名空间中使用 ping 命令检查一下两个网络命名空间的连接状况:

$ ip netns exec client ping 10.0.0.12
  PING 10.0.0.12 (10.0.0.12) 56(84) bytes of data.
  64 bytes from 10.0.0.12: icmp_seq=1 ttl=64 time=0.101 ms
  64 bytes from 10.0.0.12: icmp_seq=2 ttl=64 time=0.072 ms
  64 bytes from 10.0.0.12: icmp_seq=3 ttl=64 time=0.084 ms
  64 bytes from 10.0.0.12: icmp_seq=4 ttl=64 time=0.077 ms
  64 bytes from 10.0.0.12: icmp_seq=5 ttl=64 time=0.079 ms

如果要创建更网络命名空间并互相连接,用 veth 对将这些网络命名空间进行两两连接就很麻烦了。可以创建创建一个 Linux 网桥来连接这些网络命名空间。Docker 就是这样为同一主机内的容器进行连接的。下面就创建网络命名空间并用网桥连接起来:

# All in one
# ip link add <p1-name> netns <p1-ns> type veth peer <p2-name> netns <p2-ns>
BR=bridge1
HOST_IP=172.17.0.33
# 新创建一对类型为veth peer的网卡
ip link add client1-veth type veth peer name client1-veth-br
ip link add server1-veth type veth peer name server1-veth-br
ip link add $BR type bridge
ip netns add client1
ip netns add server1
ip link set client1-veth netns client1
ip link set server1-veth netns server1
ip link set client1-veth-br master $BR
ip link set server1-veth-br master $BR
ip link set $BR up
ip link set client1-veth-br up
ip link set server1-veth-br up
ip netns exec client1 ip link set client1-veth up
ip netns exec server1 ip link set server1-veth up
ip netns exec client1 ip addr add 172.30.0.11/24 dev client1-veth
ip netns exec server1 ip addr add 172.30.0.12/24 dev server1-veth
ip netns exec client1 ping 172.30.0.12 -c 5
ip addr add 172.30.0.1/24 dev $BR
ip netns exec client1 ping 172.30.0.12 -c 5
ip netns exec client1 ping 172.30.0.1 -c 5

还是用 ping 命令检查两个网络命名空间的连接性:

$ ip netns exec client1 ping 172.30.0.12 -c 5
  PING 172.30.0.12 (172.30.0.12) 56(84) bytes of data.
  64 bytes from 172.30.0.12: icmp_seq=1 ttl=64 time=0.138 ms
  64 bytes from 172.30.0.12: icmp_seq=2 ttl=64 time=0.091 ms
  64 bytes from 172.30.0.12: icmp_seq=3 ttl=64 time=0.073 ms
  64 bytes from 172.30.0.12: icmp_seq=4 ttl=64 time=0.070 ms
  64 bytes from 172.30.0.12: icmp_seq=5 ttl=64 time=0.107 ms

从命名空间中 ping 一下主机 IP:

$ ip netns exec client1 ping $HOST_IP -c 2
  connect: Network is unreachable

Network is unreachable 的原因是路由不通,加入一条缺省路由:

$ ip netns exec client1 ip route add default via 172.30.0.1
$ ip netns exec server1 ip route add default via 172.30.0.1
$ ip netns exec client1 ping $HOST_IP -c 5
  PING 172.17.0.23 (172.17.0.23) 56(84) bytes of data.
  64 bytes from 172.17.0.23: icmp_seq=1 ttl=64 time=0.053 ms
  64 bytes from 172.17.0.23: icmp_seq=2 ttl=64 time=0.121 ms
  64 bytes from 172.17.0.23: icmp_seq=3 ttl=64 time=0.078 ms
  64 bytes from 172.17.0.23: icmp_seq=4 ttl=64 time=0.129 ms
  64 bytes from 172.17.0.23: icmp_seq=5 ttl=64 time=0.119 ms
  --- 172.17.0.23 ping statistics ---
  5 packets transmitted, 5 received, 0% packet loss, time 3999ms
  rtt min/avg/max/mdev = 0.053/0.100/0.129/0.029 ms

default 路由打通了网桥的通信,这样这个命名空间就能和外部网络进行通信了:

$ ping 8.8.8.8 -c 2
  PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
  64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=3.40 ms
  64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=3.81 ms
  --- 8.8.8.8 ping statistics ---
  2 packets transmitted, 2 received, 0% packet loss, time 1001ms
  rtt min/avg/max/mdev = 3.403/3.610/3.817/0.207 ms

三、从外部服务器连接内网

如你所见,这里演示用的机器已经安装了 Docker,也就是说已经创建了 docker0 网桥。测试场景需要所有网络命名空间的协同,进行 Web Server 的测试有些复杂,因此这里就借用一下 docker0

docker0   Link encap:Ethernet  HWaddr 02:42:e2:44:07:39
          inet addr:172.18.0.1  Bcast:172.18.0.255  Mask:255.255.255.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

运行一个 nginx 容器并进行观察:

$ docker run -d --name web --rm nginx
	efff2d2c98f94671f69cddc5cc88bb7a0a5a2ea15dc3c98d911e39bf2764a556
$ WEB_IP=`docker inspect -f "{{ .NetworkSettings.IPAddress }}" web`
$ docker inspect web --format '{{ .NetworkSettings.SandboxKey }}'
	/var/run/docker/netns/c009f2a4be71

Docker 创建的 netns 没有保存在缺省位置,所以 ip netns list 是看不到这个网络命名空间的。我们可以在缺省位置创建一个符号链接:

$ container_id=web
$ container_netns=$(docker inspect ${container_id} --format '{{ .NetworkSettings.SandboxKey }}')
$ mkdir -p /var/run/netns
$ rm -f /var/run/netns/${container_id}
$ ln -sv ${container_netns} /var/run/netns/${container_id}
	'/var/run/netns/web' -> '/var/run/docker/netns/c009f2a4be71'
$ ip netns list
  web (id: 3)
  server1 (id: 1)
  client1 (id: 0)

看看 web 命名空间的 IP 地址:

$ ip netns exec web ip addr
  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
  11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
      link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
      inet 172.18.0.3/24 brd 172.18.0.255 scope global eth0
         valid_lft forever preferred_lft forever

然后看看容器里的 IP 地址:

$ WEB_IP=`docker inspect -f "{{ .NetworkSettings.IPAddress }}" web`
$ echo $WEB_IP
  172.18.0.3

从主机访问一下 web 命名空间的服务:

$ curl $WEB_IP
  <!DOCTYPE html>
  <html>
  <head>
  <title>Welcome to nginx!</title>
  ...

加入端口转发规则,其它主机就能访问这个 nginx 了:

$ iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination $WEB_IP:80
$ echo $HOST_IP
  172.17.0.23

使用主机 IP 访问 Nginx:

$ curl 172.17.0.23
  <!DOCTYPE html>
  <html>
  <head>
  <title>Welcome to nginx!</title>
  <style>
      body {
          width: 35em;
          margin: 0 auto;
          font-family: Tahoma, Verdana, Arial, sans-serif;
      }
  </style>
  </head>
  <body>
  <h1>Welcome to nginx!</h1>
  <p>If you see this page, the nginx web server is successfully installed and
  working. Further configuration is required.</p>
  <p>For online documentation and support please refer to
  <a href="http://nginx.org/">nginx.org</a>.<br/>
  Commercial support is available at
  <a href="http://nginx.com/">nginx.com</a>.</p>
  <p><em>Thank you for using nginx.</em></p>
  </body>
  </html>

CNI 插件会执行上面的过程(不完全相同,但是类似)来设置 loopbacketh0,并给容器分配 IP。容器运行时调用 CNI 设置 Pod 网络,接下来讨论一下 CNI。

四、CNI 是什么

CNI 插件负责在容器网络命名空间中插入一个网络接口(也就是 veth 对中的一端)并在主机侧进行必要的变更(把 veth 对中的另一侧接入网桥)。然后给网络接口分配 IP,并调用 IPAM 插件来设置相应的路由。

看起来很眼熟吧?是的,我们在前面的容器网络部分已经说了这些内容。CNI 是一个 CNCF 项目,其中包含了在 Linux 容器进行网络配置的规范和库。CNI 的主要工作就是容器网络的连接能力,并在容器销毁时移除相应的已分配资源。这种专注性使得 CNI 易于实现,因此被广泛接受。

此处所说的运行时可能是 Kubernetes、Podman 、cloud Foundry等等。

CNI 规范

https://github.com/containernetworking/cni/blob/master/SPEC.md

在我首次阅读时,注意到了一些点:

  • 因为 Docker 等运行时会为每个容器新建一个网络命名空间,所以规范把容器定义为 Linux 网络命名空间;
  • CNI 的网络定义用 JSON 格式存储;
  • 网络定义通过 STDIN 发送给插件;换句话说主机上并没有网络配置文件;
  • 其他参数通过环境变量进行传递;
  • CNI 插件是可执行文件;
  • CNI 插件负责容器的网络;换句话说,它需要完成所有容器接入网络所需的工作。在 Docker 中会包含把容器网络命名空间连回主机的工作;
  • CNI 插件负责 IPAM 工作,其中包括 IP 地址分配和路由设置。

接下来尝试脱离 Kubernetes 模拟创建 Pod,并使用 CNI 插件而非 CLI 命令进行 IP 分配。完成 Demo 就会更好地理解 Kubernetes 中 Pod 的本质。

第一步:下载 CNI 插件:

$ mkdir cni
$ cd cni
$ curl -O -L https://github.com/containernetworking/cni/releases/download/v0.4.0/cni-amd64-v0.4.0.tgz
    % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                   Dload  Upload   Total   Spent    Left  Speed
  100   644  100   644    0     0   1934      0 --:--:-- --:--:-- --:--:--  1933
  100 15.3M  100 15.3M    0     0   233k      0  0:01:07  0:01:07 --:--:--  104k
$ tar -xvf cni-amd64-v0.4.0.tgz
  ./
  ./macvlan
  ./dhcp
  ./loopback
  ./ptp
  ./ipvlan
  ./bridge
  ./tuning
  ./noop
  ./host-local
  ./cnitool
  ./flannel

第二步,创建一个 JSON 格式的 CNI 配置(00-demo.conf):

{
    "cniVersion": "0.2.0",
    "name": "demo_br",
    "type": "bridge",
    "bridge": "cni_net0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "subnet": "10.0.10.0/24",
        "routes": [
            { "dst": "0.0.0.0/0" },
            { "dst": "1.1.1.1/32", "gw":"10.0.10.1"}
        ]    
    }
}

CNI 配置参数:

-:CNI generic parameters:-
cniVersion: The version of the CNI spec in which the definition works with
name: The network name
type: The name of the plugin you wish to use.  In this case, the actual name of the plugin executable
args: Optional additional parameters
ipMasq: Configure outbound masquerade (source NAT) for this network
ipam:
    type: The name of the IPAM plugin executable
    subnet: The subnet to allocate out of (this is actually part of the IPAM plugin)
    routes:
        dst: The subnet you wish to reach
        gw: The IP address of the next hop to reach the dst.  If not specified the default gateway for the subnet is assumed
dns:
    nameservers: A list of nameservers you wish to use with this network
    domain: The search domain to use for DNS requests
    search: A list of search domains
    options: A list of options to be passed to the receiver

第三步:创建一个网络为 none 的容器,这个容器没有网络地址。可以用任意的镜像创建该容器,这里我用 pause 来模拟 Kubernetes:

$ docker run --name pause_demo -d --rm --network none kubernetes/pause
  Unable to find image 'kubernetes/pause:latest' locally
  latest: Pulling from kubernetes/pause
  4f4fb700ef54: Pull complete
  b9c8ec465f6b: Pull complete
  Digest: sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
  Status: Downloaded newer image for kubernetes/pause:latest
  763d3ef7d3e943907a1f01f01e13c7cb6c389b1a16857141e7eac0ac10a6fe82
$ container_id=pause_demo
$ container_netns=$(docker inspect ${container_id} --format '{{ .NetworkSettings.SandboxKey }}')
$ mkdir -p /var/run/netns
$ rm -f /var/run/netns/${container_id}
$ ln -sv ${container_netns} /var/run/netns/${container_id}
  '/var/run/netns/pause_demo' -> '/var/run/docker/netns/0297681f79b5'
$ ip netns list
  pause_demo
$ ip netns exec $container_id ifconfig
  lo        Link encap:Local Loopback
            inet addr:127.0.0.1  Mask:255.0.0.0
            UP LOOPBACK RUNNING  MTU:65536  Metric:1
            RX packets:0 errors:0 dropped:0 overruns:0 frame:0
            TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:1
            RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

第四步:用前面的配置来调用 CNI 插件:

$ CNI_CONTAINERID=$container_id CNI_IFNAME=eth10 CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/$container_id CNI_PATH=`pwd` ./bridge </tmp/00-demo.conf
  2020/10/17 17:32:37 Error retriving last reserved ip: Failed to retrieve last reserved ip: open /var/lib/cni/networks/demo_br/last_reserved_ip: no such file or directory
  {
      "ip4": {
          "ip": "10.0.10.2/24",
          "gateway": "10.0.10.1",
          "routes": [
              {
                  "dst": "0.0.0.0/0"
              },
              {
                  "dst": "1.1.1.1/32",
                  "gw": "10.0.10.1"
              }
          ]
      },
      "dns": {}
  • CNI_COMMAND=ADD

动作,可选范围包括 ADDDELCHECK

  • CNI_CONTAINER=pause_demo

通知 CNI 对 pause_demo 网络命名空间进行操作;

  • CNI_NETNS=/var/run/netns/pause_demo

命名空间所在路径;

  • CNI_IFNAME=eth10

在容器端创建的网络接口名称;

  • CNI_PATH=pwd

CNI 插件的可执行文件的位置,在本例中我们的当前目录已经是 cni 目录,因此这个环境变量设置为 pwd即可.

强烈建议阅读 CNI 规范以获知更多 CNI 插件及其功能的信息。在同一个 JSON 文件中可以使用多个插件形成调用链,可以用于建立防火墙规则等类似操作。

第五步,运行上面的命令会返回一些内容。

首先是因为 IPAM 驱动在本地找不到保存 IP 信息的文件而报错。但是因为第一次运行插件时会创建这个文件,所以在其他命名空间再次运行这个命令就不会出现这个问题了。

其次是得到一个说明插件已经完成相应 IP 配置的 JSON 信息。在本例中,网桥的 IP 地址应该是 10.0.10.1/24,命名空间网络接口的地址则是 10.0.10.2/24。另外还会根据我们的 JSON 配置文件,加入缺省路由以及 1.1.1.1/32 路由。检查一下:

$ ip netns exec pause_demo ifconfig
  eth10     Link encap:Ethernet  HWaddr 0a:58:0a:00:0a:02
            inet addr:10.0.10.2  Bcast:0.0.0.0  Mask:255.255.255.0
            UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
            RX packets:18 errors:0 dropped:0 overruns:0 frame:0
            TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:0
            RX bytes:1476 (1.4 KB)  TX bytes:0 (0.0 B)
  lo        Link encap:Local Loopback
            inet addr:127.0.0.1  Mask:255.0.0.0
            UP LOOPBACK RUNNING  MTU:65536  Metric:1
            RX packets:0 errors:0 dropped:0 overruns:0 frame:0
            TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:1
            RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
$ ip netns exec pause_demo ip route
  default via 10.0.10.1 dev eth10
  1.1.1.1 via 10.0.10.1 dev eth10
  10.0.10.0/24 dev eth10  proto kernel  scope link  src 10.0.10.2

CNI 创建了网桥并根据 JSON 信息进行了相应配置:

$ ifconfig
  cni_net0  Link encap:Ethernet  HWaddr 0a:58:0a:00:0a:01
            inet addr:10.0.10.1  Bcast:0.0.0.0  Mask:255.255.255.0
            inet6 addr: fe80::c4a4:2dff:fe4b:aa1b/64 Scope:Link
            UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
            RX packets:7 errors:0 dropped:0 overruns:0 frame:0
            TX packets:20 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:1000
            RX bytes:1174 (1.1 KB)  TX bytes:1545 (1.5 KB)

第六步,启动 Web Server 并共享 pause 容器命名空间:

  $ docker run --name web_demo -d --rm --network container:$container_id nginx8fadcf2925b779de6781b4215534b32231685b8515f998b2a66a3c7e38333e30

第七步,使用 pause 容器的 IP 地址访问 Web Server:

$ curl `cat /var/lib/cni/networks/demo_br/last_reserved_ip`
  <!DOCTYPE html>
  <html>
  <head>
  <title>Welcome to ngi,nx!</title>
  <style>
      body {
          width: 35em;
          margin: 0 auto;
          font-family: Tahoma, Verdana, Arial, sans-serif;
      }
  </style>
  </head>
  ...

五、Pod 网络命名空间

接触 Kubernetes 最应该知道的一个问题就是,Pod 不等于容器,而是一组容器。这一组容器会共享同一个网络栈。每个 Pod 都会包含有 pause 容器,Kubernetes 通过这个容器来管理 Pod 的网络。所有其他容器都会附着在 pause 容器的网络命名空间中,而 pause 除了网络之外,再无其他作用。因此同一个 Pod 中的不同容器,可以通过 localhost 进行互访:

如前所述,CNI 插件是 Kubernetes 网络的重要组件。目前有很多第三方 CNI 插件,Calico[1] 就是其中之一,因为它的易用性和网络能力,得到很多工程师的青睐。它支持很多不同的平台,例如 Kubernetes、OpenShift、Docker EE、OpenStack[2] 以及裸金属服务。Calico Node 组件以 Docker 容器的形式运行在 Kubernetes 的所有 Master 和 Node 节点上。Calico-CNI 插件会直接集成到 Kubernetes 每个节点的 Kubelet 进程中,一旦发现了新建的 Pod,就会将其加入 Calico 网络。

下面的内容会涉及安装、Calico 模块(Felix、BIRD 以及 Confd)和路由模式,但是不会包含网络策略方面的内容。

CNI 的任务

  1. 创建 veth 对,并移入容器
  2. 鉴别正确的 POD CIDR
  3. 创建 CNI 配置文件
  4. IP 地址的分配和管理
  5. 在容器中加入缺省路由
  6. 把路由广播给所有 Peer 节点(不适用于 VxLan)
  7. 在主机上加入路由
  8. 实施网络策略

其实还有很多别的需求,但是上面几个点是最基础的。看看 Master 和 Worker 节点上的路由表。每个节点都有一个容器,容器有一个 IP 地址和缺省的容器路由。

上面的路由表说明,Pod 能够通过 3 层网络进行互通。什么模块负责添加路由,如何获取远端路由呢?为什么这里缺省网关是 169.254.1.1 呢?我们接下来会讨论这些问题。

Calico 的核心包括 Bird、Felix、ConfD、ETCD 以及 Kubernetes API Server。Calico 需要保存一些配置信息,例如 IP 池、端点信息、网络策略等,数据存储位置是可以配置的,本例中我们使用 Kubernetes 进行存储。

BIRD(BGP)

Bird 是一个 BGP 守护进程,运行在每个节点上,负责相互交换路由信息。通常的拓扑关系是节点之间构成的网格:

然而集群规模较大的时候,就会很麻烦了。可以使用 Route Reflector(部分 BGP 节点能够配置为 Route Reflector)来完成路由的传播工作,从而降低 BGP 连接数量。路由广播会发送给 Route Reflector,再由 Route Reflector 进行传播,更多信息可以参考 RFC4456。

BIRD 实例负责向其它 BIRD 实例传递路由信息。缺省配置方式就是 BGP Mesh,适用于小规模部署。在大规模集群中,建议使用 Route Reflector 来克服这个缺点。可以使用多个 RR 来达成高可用目的,另外还可以使用外部 RR 来替代 BIRD。

ConfD

ConfD 是一个简单的配置管理工具,运行在 Calico Node 容器中。它会从 ETCD 中读取数据(Calico 的 BIRD 配置),并写入磁盘文件。它会循环读取网络和子网,并应用配置数据(CIDR 键),组装为 BIRD 能够使用的配置。这样不管网络如何变化,BIRD 都能够得到通知并在节点之间广播路由。

Felix

Calico Felix 守护进程在 Calico Node 容器中运行,完成如下功能:

  • 从 Kubernetes ETCD 中读取信息
  • 构建路由表
  • 配置 iptables 或者 IPVS

看看集群中所有的 Calico 模块:

是不是有点不同?veth 的一端是“悬空”的,没有连接。

数据包如何被路由到 Peer 节点的?

  1. Master 上的 Pod 尝试 Ping 10.0.2.11
  2. Pod 向网关发送一个 ARP 请求
  3. 从 ARP 响应中得到 MAC 地址
  4. 但是谁响应的 ARP 请求?

容器是怎样路由到一个不存在的 IP 的?容器的缺省路由指向了 169.254.1.1。容器的 eth0 需要访问这个地址,因此在使用缺省路由的时候会对这个 IP 进行 ARP 查询。

如果能捕获 ARP 响应信息,会发现 veth 另外一侧的(cali123) MAC 地址。所以到底是怎样响应一个没有 IP 接口的 ARP 请求的呢?答案是 proxy-arp,如果我们检查一下主机侧的 veth 接口,会看到启用了 proxy-arp

$ cat /proc/sys/net/ipv4/conf/cali123/proxy_arp
1

Proxy ARP 技术能用特定网络上的代理设备来响应针对本网络不存在的 IP 地址的 ARP 查询。这个代理知道流量的目标,会以自己的 MAC 地址进行响应。如此一来,流量就转给 Proxy,通常会被 Proxy 使用其它网络接口或者隧道路由到原定目标。这种以自己 MAC 地址响应其他 IP 地址的 ARP 请求,完成代理任务的行为有时也被称为发布。

仔细看看 Worker 节点:


数据包进入内核之后,会根据路由表进行路由。

入栈流量:首先进入Worker 节点内核。内核把数据包发给 cali123

路由模式

Calico 支持三种路由模式,本节中会对几种模式的优劣和适用场景进行讨论。

  • IP-in-IP

    缺省,有封装行为;

  • Direct/NoEncapMode

    无封包(推荐);

  • VxLan

    有封包(无 BGP)

IP-in-IP

这是一种简单的对 IP 包进行再封包的方式。传输中的数据包带有一个外层头部,其中描述了源主机和目的 IP,还有一个内层头部,包含源 Pod 和目标 IP。目前 Azure 还不支持 IP-IP,因此这种环境中无法使用该模式,建议关掉 IP-IP 以提高性能。

NoEncapMode

这种模式下数据包是用 Pod 发出时的原始格式发出来的。因为没有封包和解包的开销,这种模式比较有性能优势。

AWS 中要使用这种模式需要关闭源 IP 校验。

VXLAN

Calico 3.7 以后的版本才支持 VXLAN 路由。

VXLAN 是 Virtual Extensible LAN 的缩写。VXLAN 是一种封包技术,二层数据帧被封装为 UDP 数据包。VXLAN 是一种网络虚拟化技术。当设备在软件定义的数据中心里进行通信时,会在这些设备之间建立 VXLAN 隧道。这些隧道能建立在物理或虚拟交换机之上。这些交换端口被称为 VXLAN Tunnel Endpoints(VTEPs),负责 VXLAN 的封包和解包工作。不支持 VXLAN 的设备可以连接到 VTEP,由 VTEP 提供 VXLAN 的出入转换工作。

VXLAN 对于不支持 IP-in-IP 的网络非常有用,例如 Azure 或者其它不支持 BGP 的数据中心。

演示—— IPIP 和 UnEncapMode

在没安装 Calico 之前检查一下集群:

$ kubectl get nodes
  NAME           STATUS     ROLES    AGE   VERSION
  controlplane   NotReady   master   40s   v1.18.0
  node01         NotReady   <none>   9s    v1.18.0

$ kubectl get pods --all-namespaces
  NAMESPACE     NAME                                   READY   STATUS    RESTARTS   AGE
  kube-system   coredns-66bff467f8-52tkd               0/1     Pending   0          32s
  kube-system   coredns-66bff467f8-g5gjb               0/1     Pending   0          32s
  kube-system   etcd-controlplane                      1/1     Running   0          34s
  kube-system   kube-apiserver-controlplane            1/1     Running   0          34s
  kube-system   kube-controller-manager-controlplane   1/1     Running   0          34s
  kube-system   kube-proxy-b2j4x                       1/1     Running   0          13s
  kube-system   kube-proxy-s46lv                       1/1     Running   0          32s
  kube-system   kube-scheduler-controlplane            1/1     Running   0          33s

检查 CNI 的二进制文件和目录。其中没有任何配置文件或者 Calico 二进制,Calico 安装过程会用加载卷来填充其中的内容:

$ cd /etc/cni
  -bash: cd: /etc/cni: No such file or directory
$ cd /opt/cni/bin
$ ls
  bridge  dhcp  flannel  host-device  host-local  ipvlan  loopback  macvlan  portmap  ptp  sample  tuning  vlan

在 Master/Worker 节点上检查 ip route

$ ip route
  default via 172.17.0.1 dev ens3
  172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.32
  172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown

在集群环境中下载并提交 calico.yaml

$ curl https://docs.projectcalico.org/manifests/calico.yaml -O
$ kubectl apply -f calico.yaml

看看其中的配置参数:

cni_network_config: |-
    {
      "name": "k8s-pod-network",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "calico", >>> Calico's CNI plugin
          "log_level": "info",
          "log_file_path": "/var/log/calico/cni/cni.log",
          "datastore_type": "kubernetes",
          "nodename": "__KUBERNETES_NODE_NAME__",
          "mtu": __CNI_MTU__,
          "ipam": {
              "type": "calico-ipam" >>> Calico's IPAM instaed of default IPAM
          },
          "policy": {
              "type": "k8s"
          },
          "kubernetes": {
              "kubeconfig": "__KUBECONFIG_FILEPATH__"
          }
        },
        {
          "type": "portmap",
          "snat": true,
          "capabilities": {"portMappings": true}
        },
        {
          "type": "bandwidth",
          "capabilities": {"bandwidth": true}
        }
      ]
    }
# Enable IPIP
- name: CALICO_IPV4POOL_IPIP
    value: "Always" >> Set this to 'Never' to disable IP-IP
# Enable or Disable VXLAN on the default IP pool.
- name: CALICO_IPV4POOL_VXLAN
    value: "Never"

安装完毕之后,检查 Pod 和节点状态。

$ kubectl get pods --all-namespaces
  NAMESPACE     NAME                                       READY   STATUS              RESTARTS   AGE
  kube-system   calico-kube-controllers-799fb94867-6qj77   0/1     ContainerCreating   0          21s
  kube-system   calico-node-bzttq                          0/1     PodInitializing     0          21s
  kube-system   calico-node-r6bwj                          0/1     PodInitializing     0          21s
  kube-system   coredns-66bff467f8-52tkd                   0/1     Pending             0          7m5s
  kube-system   coredns-66bff467f8-g5gjb                   0/1     ContainerCreating   0          7m5s
  kube-system   etcd-controlplane                          1/1     Running             0          7m7s
  kube-system   kube-apiserver-controlplane                1/1     Running             0          7m7s
  kube-system   kube-controller-manager-controlplane       1/1     Running             0          7m7s
  kube-system   kube-proxy-b2j4x                           1/1     Running             0          6m46s
  kube-system   kube-proxy-s46lv                           1/1     Running             0          7m5s
  kube-system   kube-scheduler-controlplane                1/1     Running             0          7m6s
$ kubectl get nodes
  NAME           STATUS   ROLES    AGE     VERSION
  controlplane   Ready    master   7m30s   v1.18.0
  node01         Ready    <none>   6m59s   v1.18.0

Kubelet 需要 CNI 的配置文件来设置网络:

$ cd /etc/cni/net.d/
$ ls
10-calico.conflist  calico-kubeconfig
$
$
$ cat 10-calico.conflist
{
  "name": "k8s-pod-network",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "calico",
      "log_level": "info",
      "log_file_path": "/var/log/calico/cni/cni.log",
      "datastore_type": "kubernetes",
      "nodename": "controlplane",
      "mtu": 1440,
      "ipam": {
          "type": "calico-ipam"
      },
      "policy": {
          "type": "k8s"
      },
      "kubernetes": {
          "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
      }
    },
    {
      "type": "portmap",
      "snat": true,
      "capabilities": {"portMappings": true}
    },
    {
      "type": "bandwidth",
      "capabilities": {"bandwidth": true}
    }
  ]
}

检查 CNI 的二进制文件:

$ ls
bandwidth  bridge  calico  calico-ipam dhcp  flannel  host-device  host-local  install  ipvlan  loopback  macvlan  portmap  ptp  sample  tuning  vlan

安装 calicoctl 来获取 Calico 的更多信息并能修改 Calico 配置:

$ cd /usr/local/bin/
$ curl -O -L  https://github.com/projectcalico/calicoctl/releases/download/v3.16.3/calicoctl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   633  100   633    0     0   3087      0 --:--:-- --:--:-- --:--:--  3087
100 38.4M  100 38.4M    0     0  5072k      0  0:00:07  0:00:07 --:--:-- 4325k
$ chmod +x calicoctl
$ export DATASTORE_TYPE=kubernetes
$ export KUBECONFIG=~/.kube/config
# Check endpoints - it will be empty as we have't deployed any POD
$ calicoctl get workloadendpoints
WORKLOAD   NODE   NETWORKS   INTERFACE

检查 BGP Peer 的状态,会看到 Worker 节点是一个 Peer。

$ calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS |     PEER TYPE     | STATE |  SINCE   |    INFO     |
+--------------+-------------------+-------+----------+-------------+
| 172.17.0.40  | node-to-node mesh | up    | 00:24:04 | Established |
+--------------+-------------------+-------+----------+-------------+

创建一个两副本 Pod,并设置 tolerations,使之可以运行在 Master 节点:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox-deployment
spec:
  selector:
    matchLabels:
      app: busybox
  replicas: 2
  template:
    metadata:
      labels:
        app: busybox
    spec:
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - name: busybox
        image: busybox
        command: ["sleep"]
        args: ["10000"]

获取 Pod 和端点状态:

$ kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE           NOMINATED NODE   READINESS GATES
busybox-deployment-8c7dc8548-btnkv   1/1     Running   0          6s    192.168.196.131   node01         <none>           <none>
busybox-deployment-8c7dc8548-x6ljh   1/1     Running   0          6s    192.168.49.66     controlplane   <none>           <none>
$ calicoctl get workloadendpoints
WORKLOAD                             NODE           NETWORKS             INTERFACE
busybox-deployment-8c7dc8548-btnkv   node01         192.168.196.131/32   calib673e730d42
busybox-deployment-8c7dc8548-x6ljh   controlplane   192.168.49.66/32     cali9861acf9f07

获取 Pod 所在主机上的 VETH 信息:

$ ifconfig cali9861acf9f07
cali9861acf9f07: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1440
        inet6 fe80::ecee:eeff:feee:eeee  prefixlen 64  scopeid 0x20<link>
        ether ee:ee:ee:ee:ee:ee  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5  bytes 446 (446.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

获取 Pod 网络界面的信息:

$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ifconfig
eth0      Link encap:Ethernet  HWaddr 92:7E:C4:15:B9:82
          inet addr:192.168.49.66  Bcast:192.168.49.66  Mask:255.255.255.255
          UP BROADCAST RUNNING MULTICAST  MTU:1440  Metric:1
          RX packets:5 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:446 (446.0 B)  TX bytes:0 (0.0 B)
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- arp

获取主节点路由:

$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.32
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
blackhole 192.168.49.64/26 proto bird
192.168.49.65 dev calic22dbe57533 scope link
192.168.49.66 dev cali9861acf9f07 scope link
192.168.196.128/26 via 172.17.0.40 dev tunl0 proto bird onlink

尝试 Ping Worker 节点来触发 ARP:

$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ping 192.168.196.131 -c 1
PING 192.168.196.131 (192.168.196.131): 56 data bytes
64 bytes from 192.168.196.131: seq=0 ttl=62 time=0.823 ms
$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- arp
? (169.254.1.1) at ee:ee:ee:ee:ee:ee [ether]  on eth0

注意上面的 MAC 地址。发出流量时,内核根据 IP 路由将数据包写入 tunl0,Proxy ARP 的配置:

$ cat /proc/sys/net/ipv4/conf/cali9861acf9f07/proxy_arp
1

六、目标节点如何处理数据包

node01 $ ip route
  default via 172.17.0.1 dev ens3
  172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.40
  172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
  192.168.49.64/26 via 172.17.0.32 dev tunl0 proto bird onlink
  blackhole 192.168.196.128/26 proto bird
  192.168.196.129 dev calid4f00d97cb5 scope link
  192.168.196.130 dev cali257578b48b6 scope link
  192.168.196.131 dev calib673e730d42 scope link

接收到数据包之后,内核会根据路由表将数据包发给对应的 veth

如果抓包的话会看出 IP-IP 协议。据我所知,Azure 不支持 IP-IP,也就是说我们无法在这种环境里使用 IP-IP。关闭 IP-IP 能获得更高性能,下面一节尝试一下。

禁用 IP-IP

更新 ippool.yaml 设置 IPIP 为 Never,然后用 calicoctl 应用配置:

$ calicoctl get ippool default-ipv4-ippool -o yaml > ippool.yaml
$ vi ippool.yaml
...
$ calicoctl apply -f ippool.yaml
Successfully applied 1 'IPPool' resource(s)

再次检查 ip route

$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.32
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
blackhole 192.168.49.64/26 proto bird
192.168.49.65 dev calic22dbe57533 scope link
192.168.49.66 dev cali9861acf9f07 scope link
192.168.196.128/26 via 172.17.0.40 dev ens3 proto bird

设备不再是 tunl0,而是变成 Master 节点的管理界面(ens3)。

Ping 一下 Worker 节点,验证工作情况,此时不再使用 IPIP 协议:

$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ping 192.168.196.131 -c 1
PING 192.168.196.131 (192.168.196.131): 56 data bytes
64 bytes from 192.168.196.131: seq=0 ttl=62 time=0.653 ms
--- 192.168.196.131 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.653/0.653/0.653 ms

注意在 AWS 环境中使用这种模式需要禁用源 IP 检查。

演示 VXLAN

重新进行集群初始化,并下载 calico.yaml 文件,进行如下变更:

livenessProbereadinessProbe 中删除 bird

livenessProbe:
            exec:
              command:
              - /bin/calico-node
              - -felix-live
              - -bird-live >> Remove this
            periodSeconds: 10
            initialDelaySeconds: 10
            failureThreshold: 6
          readinessProbe:
            exec:
              command:
              - /bin/calico-node
              - -felix-ready
              - -bird-ready >> Remove this

calico_backend 修改为 vxlan,不再需要 BGP:

kind: ConfigMap
apiVersion: v1
metadata:
  name: calico-config
  namespace: kube-system
data:
  # Typha is disabled.
  typha_service_name: "none"
  # Configure the backend to use.
  calico_backend: "vxlan"

禁用 IPIP:

# Enable IPIP
- name: CALICO_IPV4POOL_IPIP
    value: "Never" >> Set this to 'Never' to disable IP-IP
# Enable or Disable VXLAN on the default IP pool.
- name: CALICO_IPV4POOL_VXLAN
    value: "Never"

应用这个 YAML:

$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.15
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
192.168.49.65 dev calif5cc38277c7 scope link
192.168.49.66 dev cali840c047460a scope link
192.168.196.128/26 via 192.168.196.128 dev vxlan.calico onlink
vxlan.calico: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1440
        inet 192.168.196.128  netmask 255.255.255.255  broadcast 192.168.196.128
        inet6 fe80::64aa:99ff:fe2f:dc24  prefixlen 64  scopeid 0x20<link>
        ether 66:aa:99:2f:dc:24  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 11 overruns 0  carrier 0  collisions 0

获取 Pod 状态:

$ kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE           NOMINATED NODE   READINESS GATES
busybox-deployment-8c7dc8548-8bxnw   1/1     Running   0          11s   192.168.49.67     controlplane   <none>           <none>
busybox-deployment-8c7dc8548-kmxst   1/1     Running   0          11s   192.168.196.130   node01         <none>           <none>

查看 ip route

$ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link

执行 Ping,触发 ARP 查询:

$ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- arp
master $ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=116 time=3.786 ms
^C
$ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- arp
? (169.254.1.1) at ee:ee:ee:ee:ee:ee [ether]  on eth0

概念和前一种模式相似,区别在于数据包抵达 vxland 的时候,会把节点 IP 以及 MAC 地址封装并发送。另外 vxland 的 UDP 端口是 4789。这里会从 etcd 获取可用节点以及节点支持的 IP 范围,从而让 vxlan-calico 据此构建数据包。

VxLan 模式需要更多系统开销

接下来我们会讨论一下 Kubernetes 的 kube-proxy 是如何使用 iptables 控制流量的。注意,kube-proxy + iptables 的组合并非完成该任务的唯一选择。

我们会从 Kubernetes 的多种通信模型和实现开始,如果读者已经了解了 Service、ClusterIP 以及 NodePort 的概念,可以直接跳到 kube-proxy/iptables 一节。

Pod 到 Pod

CNI 会配置节点和 Pod 的路由,kube-proxy 不会介入 Pod 到 Pod 之间的通信过程。所有的容器都无需 NAT 就能互相通信;节点和容器之间的通信也是无需 NAT 的。

Pod 的 IP 地址是不固定的(也有办法做成静态 IP,但是缺省配置是不提供这种保障的)。在 Pod 重启时 CNI 会给他分配新的 IP 地址,CNI 不负责维护 IP 地址和 Pod 的映射。Pod 名称在 Deployment 之中也是不固定的。

Deployment 中的 Pod 是无状态的,一个应用可能会有多个 Pod 副本,因此需要一个负载均衡之类的东西来负责对外开放服务,Kubernetes 中的 Service 对象负责完成这个任务。

Pod 到外部

Kubernetes 会使用 SNAT 完成从 Pod 向外发出的访问。SNAT 会将 Pod 的内部 IP:Port 替换为主机的 IP:Port。返回数据包到达节点时,IP:Port 又会换回 Pod。这个过程对于原始 Pod 是透明无感知的。

Pod 到 Service

Cluster IP

Kubernetes 有一个叫做 Service 的对象,是一个通向 Pod 的 4 层负载均衡。Service 对象有很多类型,最基本的类型叫做 ClusterIP,这种类型的 Service 有一个唯一的 VIP 地址,其路由范围仅在集群内部有效。

Kubernetes 集群中,Pod 可能发生移动、重启、升级或者扩缩容,因此向应用 Pod 发送流量是有困难的,另外应用通常有多个副本,我们需要一些方法来进行负载均衡。

Kubernetes 使用 Service 对象来解决这个问题。Service 是一个 API 对象,它用一个虚拟 IP 映射到一组 Pod。另外 Kubernetes 为每个 Service 的名称及其虚拟 IP 建立了 DNS 记录,因此可以轻松地根据名称进行寻址。

虚拟 IP 到 Pod IP 的转换是通过每个节点上的 kube-proxy 完成的。在 Pod 向外发起通信时,这个进程会通过 iptables 或者 IPVS 自动把 VIP 转为 Pod IP,每个连接都有跟踪,所以数据包返回时候,地址还能够被正确地转回原样。IPVS 和 iptables 在 VIP 和 Pod IP 之间承担着负载均衡的角色,IPVS 能够提供更多的负载均衡算法。虚拟 IP 并不存在于网络接口上,而是在 iptable 中:

FrontEnd Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

Backend Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth
  labels:
    app: auth
spec:
  replicas: 2
  selector:
    matchLabels:
      app: auth
  template:
    metadata:
      labels:
        app: auth
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

Service:

---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: frontend
spec:
  ports:
  - port: 80
    protocol: TCP
  type: ClusterIP
  selector:
    app: webapp
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  labels:
    app: backend
spec:
  ports:
  - port: 80
    protocol: TCP
  type: ClusterIP
  selector:
    app: auth
...

现在 FrontEnd Pod 能够通过 ClusterIP 或者 DNS 名称来访问 Backend 了。CoreDNS 这样的 DNS 服务器具备 Kubernetes 集群感知的能力,他们会对 Kubernetes API 进行监控,一旦新建了 Service,就会新建对应的 DNS 记录。如果集群中启用的 DNS,所有 Pod 都能够自动的根据 DNS 名称来解析到 Service。

NodePort(外部到 Pod)

在集群内部可以用 DNS 访问 Service。然而 Service 的 IP 是私有的和虚拟的,所以集群外是无法访问的。

试试看从外部访问 frontEnd 的 Pod(此时还没有给 frontEnd 创建 Service):

Pod IP 是私有的,无法路由。

接下来创建一个 NodePort 类型的 Service 把 FrontEnd 服务开放给外部世界。如果把 type 字段设置为 NodePort,Kubernetes 控制面使用 --service-node-port-range 参数为 NodePort 服务分配了一个端口范围。每个节点都会会把这个端口映射给特定的服务。Service 使用 .spec.ports[*].nodePort 字段来指定该端口:

---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  selector:
    app: webapp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 31380
...

这样就可以在集群外使用任意节点的 nodePort 来访问服务了。还可以给 nodePort 赋值以指定特定开放端口。这种情况下,为了防止端口冲突,需要自行管理端口,并且指定端口也必须在参数中声明的端口范围之内。

ExternalTrafficPolicy

ExternalTrafficPolicy 字段表明所属 Service 对象会把来自外部的流量路由给本节点还是集群范围内的端点。如果赋值为 Local,会保留客户端源 IP 同时避免 NodePort 类型服务的多余一跳,但是有流量分配不均匀的隐患;如果设置为 Cluster,会抹掉客户端的源 IP,并导致到其它节点的一跳,但会获得相对较好的均衡效果。

Cluster

这是 Kubernetes Service 的缺省 ExternalTrafficPolicy。这个选项会把流量平均分配给该 Service 的所有 Pod 上。

这种策略的一个弱点是会存在不必要的节点间网络跳转。例如在一个节点的 NodePort 上接收到流量时,即使本节点上存在可用 Pod,流量还是可能会随机地把流量路由到另外一个节点上的 Pod,造成不必要的跳转。

Cluster 策略下,数据包的流向:

  • 客户端把数据包发送给 node2:31380
  • node2 替换源 IP 地址(SNAT)为自己的 IP 地址;
  • node2 将目标地址替换为 Pod IP;
  • 数据包被路由到 node1 或者 node3,然后到达 Pod;
  • Pod 的响应返回到 node2
  • Pod 的响应返回到客户端。

Local

这种策略中,kube-proxy 只会在存在目标 Pod 的节点上加入 NodePort 的代理规则。API Server 要求只有使用 LoadBalancer 或者 NodePort 类型的 Service 才能够使用这种策略。这是因为 Local 策略只跟外部访问相关。

如果使用了 Local 策略,kube-proxy 只会代理到本地 endpoint 的流量,不会向其它节点转发。如果本地没有相应端点,发送到该节点的流量就会被丢弃,所以数据包中会保留正确的源 IP,可以放心的在数据包处理规则中使用。

---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: webapp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 31380
...

Local 策略下的数据包:

  • 客户端发送数据包到 node1:31380,这个端点上存在目标 Pod;
  • node1 把数据包路由到端点,其中带有正确的源 IP;
  • 因为策略限制,node1 不会把数据包发给 node3
  • 客户端发送数据包给 node2:31380,该节点上不存在目标 Pod;
  • 数据包被丢弃。

LoadBalancer Service 类型中的 Local 策略

如果在 Google GKE 上使用 Local 策略,由于健康检查的原因,会把不运行对应 Pod 的节点从负载均衡池中剔除,所以不会发生丢弃流量的问题。这种模型对于需要处理大量外部入栈流量,需要避免跨节点跳转从而降低延迟的应用非常有帮助。另外因为不需要进行 SNAT,从而让源 IP 得以保存。然而官方文档声明,这种策略存在不够均衡的短板。

Kube-Proxy(iptable)

Kubernetes 中负责 Service 对象的组件就是 kube-proxy。它在每个节点上运行,为 Pod 和 Service 生成复杂的 iptables 规则,完成所有的过滤和 NAT 工作。如果登录到 Kubernetes 节点上,运行 iptables-save,会看到 Kubernetes 或者其它组件生成的规则。最重要的是 KUBE-SERVICEKUBE-SVC-* 以及 KUBE-SEP-*

  • KUBE-SERVICEService 包的入口。

    它负责匹配 IP:Port,并把数据包发给对应的 KUBE-SVC-*

  • KUBE-SVC-* 担任负载均衡的角色,会平均分配数据包到 KUBE-SEP-*

    每个 KUBE-SVC-* 都有和 Endpoint 同样数量的 KUBE-SEP-*

  • KUBE-SEP-* 代表的是 ServiceEndPoint,它负责的是 DNAT,会把 Service 的 IP:Port 替换为 Pod 的 IP:Port。

Conntrack 会介入 DNAT 过程,使用状态机来跟踪连接状态。为了记住目标地址的变更,并在回包时候进行恢复,这些状态是必须保存的。iptables 还可以根据 conntrack 状态(ctstate)来决定数据包的目标。下面四个 conntrack 状态尤其重要:

  • NEW

    conntrack 对该数据包一无所知,该状态出现在收到 SYN 的时候。

  • ESTABLISHED

    conntrack 知道该数据包属于一个已发布连接,该状态出现于握手完成之后。

  • RELATED

    这个数据包不属于任何连接,但是他是隶属于其它连接的,在 FTP 之类的协议里常用。

  • INVALID

    有问题的数据包,conntrack 不知道如何处理。

    这种状态是 Kubernetes 问题的常客。

Service 和 Pod 之间的 TCP 连接过程如下:

  • 左侧的客户端 Pod 发送数据包到一个 Service:

    2.2.2.10:80

  • 数据包经过客户端节点的 iptables 规则,目标改为 1.1.1.20:80

  • 服务端 Pod 处理数据包,发送一个响应包到 1.1.1.10

  • 数据包回到客户端节点,conntrack 认出这个数据包,把源地址改回 2.2.2.10:80

  • 客户端 Pod 收到响应包。

iptables

在 Linux 操作系统中使用 netfilter 处理防火墙工作。这是一个内核模块,决定是否放行数据包。iptables 是 netfilter 的前端。二者经常被混为一谈。

每条链负责一种特定任务。

  • PREROUTING

    决定数据包刚刚进入网络端口时的对策。

    有几种不同的选择,例如修改数据包(NAT),丢弃数据包或者什么都不做使其通过;

  • INPUT

    其中经常包含一些用于防止恶意行为的严格规则,防止系统遭到入侵。

    开放或者屏蔽端口的行为就是在这里进行的;

  • FORWARD

    顾名思义,负责数据包的转发。

    在将服务器作为路由器的时候,就需要在这里完成任务。

  • OUTPUT

    这里负责所有的网络浏览的行为。

    这里可以限制所有数据包的发送。

  • POSTROUTING

    发生在数据包离开服务器之前,数据包最后的可跟踪位置。

FORWARD 仅在 ip_forward 启用时才有效。所以下面的命令在 Kubernetes 中很重要:

$ sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
$ cat /proc/sys/net/ipv4/ip_forward
1

上面的变更是暂时性的,要持久化这个变更,需要在 /etc/sysctl.conf 中写入 net.ipv4.ip_forward = 1

接下来会讨论 NAT 表,除此之外还有几个:

  • Filter

    缺省表,这里决定是否允许数据包出入本机,因此可以在这里进行屏蔽等操作;

  • Nat

    是网络地址转换的缩写。

    下面会有例子说明;

  • Mangle

    仅对特定包有用。

    它的功能是在包出入之前修改包中的内容;

  • RAW

    用于处理原始数据包,主要用在跟踪连接状态,下面有一个放行 SSH 连接的例子。

  • Security

    负责在 Filter 之后保障安全。

Kubernetes 中的 iptables 配置

部署一个 2 副本 Nginx 应用,导出 iptables 规则。

服务类型 NodePort

$ kubectl get svc webapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp NodePort 10.103.46.104 <none> 80:31380/TCP 3d13h
$ kubectl get ep webapp 
NAME ENDPOINTS AGE
webapp 10.244.120.102:80,10.244.120.103:80 3d13h

ClusterIP 是一个存在于 iptables 中的虚拟 IP,Kubernetes 会把这个地址存在 CoreDNS 中。

$ kubectl exec -i -t dnsutils -- nslookup webapp.default
Server:  10.96.0.10
Address: 10.96.0.10#53
Name: webapp.default.svc.cluster.local
Address: 10.103.46.104

为了能够进行包过滤和 NAT,Kubernetes 会创建一个 KUBE-SERVICES 链,把所有 PREROUTINGOUTPUT 流量转发给 KUBE-SERVICES

sudo iptables -t nat -L PREROUTING | column -t
Chain            PREROUTING  (policy  ACCEPT)                                                                    
target           prot        opt      source    destination                                                      
cali-PREROUTING  all         --       anywhere  anywhere     /*        cali:6gwbT8clXdHdC1b1  */                 
KUBE-SERVICES    all         --       anywhere  anywhere     /*        kubernetes             service   portals  */
DOCKER           all         --       anywhere  anywhere     ADDRTYPE  match                  dst-type  LOCAL

使用 KUBE-SERVICES 介入包过滤和 NAT 之后,Kubernetes 会监控通向 Service 的流量,并进行 SNAT/DNAT 的处理。在 KUBE-SERVICES 链尾部,会写入另一个链 KUBE-SERVICES,用于处理 NodePort 类型的 Service。

KUBE-SVC-2IRACUALRELARSND 链会处理针对 ClusterIP 的流量,否则的话就会进入 KUBE-NODEPORTS

$ sudo iptables -t nat -L KUBE-SERVICES | column -t
Chain                      KUBE-SERVICES  (2   references)                                                                                                                                                                             
target                     prot           opt  source          destination                                                                                                                                                             
KUBE-MARK-MASQ             tcp            --   !10.244.0.0/16  10.103.46.104   /*  default/webapp                   cluster  IP          */     tcp   dpt:www                                                                          
KUBE-SVC-2IRACUALRELARSND  tcp            --   anywhere        10.103.46.104   /*  default/webapp                   cluster  IP          */     tcp   dpt:www                                                                                                                                             
KUBE-NODEPORTS             all            --   anywhere        anywhere        /*  kubernetes                       service  nodeports;  NOTE:  this  must        be  the  last  rule  in  this  chain  */  ADDRTYPE  match  dst-type  LOCAL

看看 KUBE-NODEPORTS 的内容:

$ sudo iptables -t nat -L KUBE-NODEPORTS | column -t
Chain                      KUBE-NODEPORTS  (1   references)                                            
target                     prot            opt  source       destination                               
KUBE-MARK-MASQ             tcp             --   anywhere     anywhere     /*  default/webapp  */  tcp  dpt:31380
KUBE-SVC-2IRACUALRELARSND  tcp             --   anywhere     anywhere     /*  default/webapp  */  tcp  dpt:31380

看起来 ClusterIPNodePort 处理过程是一样的,那么看看下面的处理流程:

# statistic  mode  random -> Random load-balancing between endpoints.
$ sudo iptables -t nat -L KUBE-SVC-2IRACUALRELARSND | column -t
Chain                      KUBE-SVC-2IRACUALRELARSND  (2   references)                                                                             
target                     prot                       opt  source       destination                                                                
KUBE-SEP-AO6KYGU752IZFEZ4  all                        --   anywhere     anywhere     /*  default/webapp  */  statistic  mode  random  probability  0.50000000000
KUBE-SEP-PJFBSHHDX4VZAOXM  all                        --   anywhere     anywhere     /*  default/webapp  */

$ sudo iptables -t nat -L KUBE-SEP-AO6KYGU752IZFEZ4 | column -t
Chain           KUBE-SEP-AO6KYGU752IZFEZ4  (1   references)                                               
target          prot                       opt  source          destination                               
KUBE-MARK-MASQ  all                        --   10.244.120.102  anywhere     /*  default/webapp  */       
DNAT            tcp                        --   anywhere        anywhere     /*  default/webapp  */  tcp  to:10.244.120.102:80

$ sudo iptables -t nat -L KUBE-SEP-PJFBSHHDX4VZAOXM | column -t
Chain           KUBE-SEP-PJFBSHHDX4VZAOXM  (1   references)                                               
target          prot                       opt  source          destination                               
KUBE-MARK-MASQ  all                        --   10.244.120.103  anywhere     /*  default/webapp  */       
DNAT            tcp                        --   anywhere        anywhere     /*  default/webapp  */  tcp  to:10.244.120.103:80

$ sudo iptables -t nat -L KUBE-MARK-MASQ | column -t
Chain   KUBE-MARK-MASQ  (24  references)                         
target  prot            opt  source       destination            
MARK    all             --   anywhere     anywhere     MARK  or  0x4000

注意:输出内容已经被精简。

  • ClusterIP:KUBE-SERVICESKUBE-SVC-XXXKUBE-SEP-XXX
  • NodePort:KUBE-SERVICESKUBE-NODEPORTSKUBE-SVC-XXXKUBE-SEP-XXX

NodePort 服务会有一个 ClusterIP 用于处理内外部通信。

上述规则的可视化表达:

ExtrenalTrafficPolicy: Local

如前文所述,使用 ExtrenalTrafficPolicy: Local 会保留源 IP,并在到达节点上没有 Endpoint 的时候丢弃流量。没有本地 Endpoint 的节点上,iptables 的规则会怎样?

使用 ExtrenalTrafficPolicy: Local 部署 Nginx 服务:

$ kubectl get svc webapp -o wide -o jsonpath={.spec.externalTrafficPolicy}
Local

$ kubectl get svc webapp -o wide
NAME     TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE   SELECTOR
webapp   NodePort   10.111.243.62   <none>        80:30080/TCP   29m   app=webserver

检查一下没有本地 Endpoint 的节点上的 iptables 规则:

$ sudo iptables -t nat -L KUBE-NODEPORTS
Chain KUBE-NODEPORTS (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp — 127.0.0.0/8 anywhere /* default/webapp */ tcp dpt:30080
KUBE-XLB-2IRACUALRELARSND tcp — anywhere anywhere /* default/webapp */ tcp dpt:30080

再看一下 KUBE-XLB-2IRACUALRELARSND

$ iptables -t nat -L KUBE-XLB-2IRACUALRELARSND
Chain KUBE-XLB-2IRACUALRELARSND (1 references)
target prot opt source destination
KUBE-SVC-2IRACUALRELARSND all — 10.244.0.0/16 anywhere /* Redirect pods trying to reach external loadbalancer VIP to clusterIP */
KUBE-MARK-MASQ all — anywhere anywhere /* masquerade LOCAL traffic for default/webapp LB IP */ ADDRTYPE match src-type LOCAL
KUBE-SVC-2IRACUALRELARSND all — anywhere anywhere /* route LOCAL traffic for default/webapp LB IP to service chain */ ADDRTYPE match src-type LOCAL
KUBE-MARK-DROP all — anywhere anywhere /* default/webapp has no local endpoints */

这里就会看到,集群级别的流量没什么问题,但是 NodePort 流量会被丢弃。

Headless Service

有的应用并不需要负载均衡和服务 IP。在这种情况下就可以使用 headless Service,只要设置 .spec.clusterIPNone 即可。

可以借助这种服务类型和其他服务发现机制协作,无需和 Kubernetes 绑定。kube-proxy 不对这种没有 IP 的服务提供支持,也就没有什么负载均衡和代理之类的能力了。DNS 的配置要根据 Selector 来确定。

有 Selector

定义了 Selector 的 Headless Service,Endpoint 控制器会创建 Endpoint 记录,并修改 DNS 记录来直接返回 Service 后端的 Pod 地址。

$ kubectl get svc webapp-hs
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
webapp-hs   ClusterIP   None         <none>        80/TCP    24s
$ kubectl get ep webapp-hs
NAME        ENDPOINTS                             AGE
webapp-hs   10.244.120.109:80,10.244.120.110:80   31s
无 Selector

没有定义 Selector 的 Headless Service,也就没有 Endpoint 记录。然而 DNS 系统会尝试配置:

  • ExternalName 类型的服务,会产生 CNAME 记录;
  • 其他类型则是所有 Endpoint 共享服务名称。

如果外部 IP 被路由到集群节点上,Kubernetes Service 可以用 externalIPs 开放出来。通过 externalIP 进入集群的流量,会被路由到 Service Endpoint 上。externalIPs 不是 Kubernetes 管理的,需要集群管理员自行维护。

七、网络策略

阅读至此,Kubernetes 网络策略的实现方法已经呼之欲出了——是的,就是 iptables。目前是 CNI 而非 kube-proxy 负责实现网络策略。

我们创建三个服务:frontend、backend 和 db。缺省情况下,Pod 没有任何隔离,会接受任何来源的通信。

想要制定规则,禁止 frontend 访问 db:

这里推荐阅读 Guide to Kubernetes Ingress Network Policies 了解网络策略配置方面的更多内容。本节内容关注的是 Kubernetes 中策略的实现方式,而非配置知识。

创建一个策略把 db 和 frontend 隔离开,这样一来 frontend 和 db 之间的流量就会被阻断。

上图中为了简单起见,写的是 Service 而非 Pod,安全策略的控制对象实际上是 Pod。

策略实施之后会产生如下效果,frontend 的 Pod 能访问 backend 但是无法访问 db。backend 的 Pod 可以访问 db。

$ kubectl exec -it frontend-8b474f47-zdqdv -- /bin/sh
$ curl backend
backend-867fd6dff-mjf92
$ curl db
curl: (7) Failed to connect to db port 80: Connection timed out

$ kubectl exec -it backend-867fd6dff-mjf92 -- /bin/sh
$ curl db
db-8d66ff5f7-bp6kf

看看这里用到的网络策略:只允许 ‘allow-db-access 标签设置为 true 的 Pod 访问 db。

Calico 会把 Kubernetes 网络策略翻译成 Calico 格式:

$ calicoctl get networkPolicy --output yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
  kind: NetworkPolicy
  metadata:
    creationTimestamp: "2020-11-05T05:26:27Z"
    name: knp.default.allow-db-access
    namespace: default
    resourceVersion: /53872
    uid: 1b3eb093-b1a8-4429-a77d-a9a054a6ae90
  spec:
    ingress:
    - action: Allow
      destination: {}
      source:
        selector: projectcalico.org/orchestrator == 'k8s' && networking/allow-db-access
          == 'true'
    order: 1000
    selector: projectcalico.org/orchestrator == 'k8s' && app == 'db'
    types:
    - Ingress
kind: NetworkPolicyList
metadata:
  resourceVersion: 56821/56821

iptables 的 filter 表在网络策略的实现中起了很重要的作用。Calico 中用到了 ipsec 等高级概念,难于进行反向工程。在这个规则中可以看到,只有来自 backend 的流量才被允许发给 db。

使用 calicoctl 获取 endpoint 详情:

$ calicoctl get workloadEndpoint
WORKLOAD                         NODE       NETWORKS        INTERFACE         
backend-867fd6dff-mjf92          minikube   10.88.0.27/32   cali2b1490aa46a   
db-8d66ff5f7-bp6kf               minikube   10.88.0.26/32   cali95aa86cbb2a   
frontend-8b474f47-zdqdv          minikube   10.88.0.24/32   cali505cfbeac50

cali95aa86cbb2a 就是 db Pod veth 的主机侧。

看看跟这个网络接口有关的 iptables 规则:

$ sudo iptables-save | grep cali95aa86cbb2a
:cali-fw-cali95aa86cbb2a - [0:0]
:cali-tw-cali95aa86cbb2a - [0:0]
...
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:pm-LK-c1ra31tRwz" -m mark --mark 0x0/0x20000 -j cali-pi-_tTE-E7yY40ogArNVgKt
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:q_zG8dAujKUIBe0Q" -m comment --comment "Return if policy accepted" -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:FUDVBYh1Yr6tVRgq" -m comment --comment "Drop if no policies passed packet" -m mark --mark 0x0/0x20000 -j DROP
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:X19Z-Pa0qidaNsMH" -j cali-pri-kns.default
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:Ljj0xNidsduxDGUb" -m comment --comment "Return if profile accepted" -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:0z9RRvvZI9Gud0Wv" -j cali-pri-ksa.default.default
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:pNCpK-SOYelSULC1" -m comment --comment "Return if profile accepted" -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment "cali:sMkvrxvxj13WlTMK" -m comment --comment "Drop if no profiles matched" -j DROP
$ sudo iptables-save -t filter | grep cali-pi-_tTE-E7yY40ogArNVgKt
:cali-pi-_tTE-E7yY40ogArNVgKt - [0:0]
-A cali-pi-_tTE-E7yY40ogArNVgKt -m comment --comment "cali:M4Und37HGrw6jUk8" -m set --match-set cali40s:LrVD8vMIGQDyv8Y7sPFB1Ge src -j MARK --set-xmark 0x10000/0x10000
-A cali-pi-_tTE-E7yY40ogArNVgKt -m comment --comment "cali:sEnlfZagUFRSPRoe" -m mark --mark 0x10000/0x10000 -j RETURN

检查一下 ipset,会看到只有来自 backend pod 的 10.88.0.27 才能访问 db。

最后我们会跟进 Kubernetes 的 Ingress 和 Ingress 控制器。Ingress 控制器会关注 API Server 中 Ingress 对象的更新,并据此配置 Ingress 的负载均衡。

Nginx控制器和负载均衡/代理服务器

Ingress控制器[5]一般是会以Pod形式运行在 Kubernetes 集群中的应用,它会根据集群中的 Ingress 对象的变化对负载均衡器进行配置。这里说的负载均衡器可以是一个集群内运行的软件也可以是一个硬件还可以是集群外部运行的云基础设施不同的负载均衡器需要不同的Ingress控制器

Ingress 的基本目标是提供一个相对高级的流量(尤其是 [http(s)])管理能力。使用Ingress可以在无需创建多个负载均衡或者对外开放多个 Service 的条件下,为服务流量进行路由。可以给服务配置外部 URL、进行负载均衡、终结 SSL 以及根据主机名或者内容进行路由等

配置选项

在把 Ingress 对象转换为负载均衡配置之前,Kubernetes Ingress控制器会用 Ingress Class 对 Kubernetes 的 Ingress 对象进行过滤。这样就允许多个 Ingress 控制器共存,各司其职。

基于前缀
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: prefix-based
  annotations:
    kubernetes.io/ingress.class: "nginx-ingress-inst-1"
spec:
  rules:
  - http:
      paths:
      - path: /video
        pathType: Prefix
        backend:
          service:
            name: video
            port:
              number: 80
      - path: /store
        pathType: Prefix
        backend:
          service:
            name: store
            port:
              number: 80
基于主机
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: host-based
  annotations:
    kubernetes.io/ingress.class: "nginx-ingress-inst-1"
spec:
  rules:
  - host: "video.example.com"
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: video
            port:
              number: 80
  - host: "store.example.com"
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: store
            port:
              number: 80
主机加前缀
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: host-prefix-based
  annotations:
    kubernetes.io/ingress.class: "nginx-ingress-inst-1"
spec:
  rules:
  - host: foo.com
    http:
      paths:
      - backend:
          serviceName: foovideo
          servicePort: 80
        path: /video
      - backend:
          serviceName: foostore
          servicePort: 80
        path: /store
  - host: bar.com
    http:
      paths:
      - backend:
          serviceName: barvideo
          servicePort: 80
        path: /video
      - backend:
          serviceName: barstore
          servicePort: 80
        path: /store

Ingress 是一个内置 API 对象,但是 Kubernetes 并没有内置任何 Ingress 控制器,需要另行安装控制器才能真正地使用 Ingress。Ingress 功能是由 API 对象控制器协同完成的。Ingress 对象负责描述集群中 Service 对象的开放需求。而控制器则负责真正的实现 Ingress API,根据 Ingress 对象的定义内容来完成实际工作。市面上有很多不同的 Ingress 控制器,需要根据实际用例谨慎地进行选择使用。

同一集群里可以有多个 Ingress 控制器,并为每个 Ingress 直接指派具体的控制器,在同一个集群中可以根据不同需要为不同服务配置不同的 Ingress。例如某服务用于一个 Ingress 处理来自集群外的 SSL 流量,另外一个用于处理集群内的明文通信。

部署选项

Contour[6] + Envoy[7]

Contour Ingress 控制器由两部分组成:

  • Envoy 提供了高性能的反向代理服务;
  • Contour 负责对 Envoy 进行管理,为其下发配置。

这些容器是各自部署的,Contour 是一个 Deployment,而 Envoy 则是一个 Daemonset,当然也可以用其他模式进行部署。Contour 是 Kubernetes API 的客户端,会跟踪 Ingress、HTTPProxy、Secret、Service 以及 Endpoint 对象,并承担管理 Envoy 的职责,它会把它的对象缓存转换为 Envoy 的 JSON 报文,Service 转换为 CDS、Ingress 转为 RDS、Endpoint 转换为 EDS 等。

下面的例子展示了启用 Host Network 的 EnvoyProxy:

Nginx

Nginx Ingress 控制器[8]的主要能力之一就是生成配置文件(nginx.conf)。这个实现还有个需要就是在配置发生变化之后重载 Nginx。在只有 upstream 发生变化时(例如部署调整时产生的 Endpoint 变化)不会进行重载,而是通过 lua-nginx-module[9] 完成任务。

每次 Endpoint 发生变动,控制器会从所有服务中拉取 Endpoint,生成对应的后端对象。这些对象会被发送给 Nginx 中运行的 Lua 处理器。Lua 代码会把这些对象保存到共享内存区域。每次 balancer_by_lua 都会检查一下 upstream 中的有效节点,以此为目标按照预配置的算法进行负载均衡。如果在一个较大的集群中有比较频繁的发布行为,这种避免重载的方式能够大幅减少重载次数,从而更好地保障了响应的延迟时间,达成较高的负载均衡水平。

Nginx+ Keepalived[10] —— 高可用部署

Keepalived 守护进程可以监控服务或者系统,如果发现问题,能够进行自动的切换。配置一个能在节点之间转移的浮动 IP。如果节点宕机,浮动 IP 会自动漂移到其它节点,Nginx 可以绑定到新的 IP 地址。

MetalLB[11] — 面向具备少量公有 IP 池的私有集群的负载均衡服务

部署到 Kubernetes 中的 MetalLB 为集群提供了一个负载均衡的实现。简单说来,MetalLB 能够在非公有云 Kubernetes 环境中对 LoadBalancer 类型的 Service 提供支持。在托管 Kubernetes 环境中,申请一个负载均衡之后,云平台会给这个新的负载均衡分配 IP;MetalLB 可以负责这个分配过程。MetalLB 给 Service 分配外部 IP 之后,需要声明该 IP 属于本集群,它使用标准路由协议来完成这一任务:ARP、NDP 或 BGP。

在 2 层模式中,集群的一个节点获取这个 Service 的所有权,然后使用标准的地址发现协议(IPv4 使用 ARP、IPv6 使用 NDP)在本地网中让次 IP 可达。从局域网的角度来看,这个节点只是多了一个 IP 地址。

在 BGP 模式中,集群中的所有节点都会对附近的路由器发起 BGP 对等会话,告知路由器如何将流量转发给这些服务。BGP 的策略机制有细粒度的流量控制能力,能真正地在多个节点之间进行负载均衡。

MetalLB 的 Pod:

  • Controller(Deployment)是集群级的 MetalLB 控制器,负责 IP 分配。
  • Speaker(Daemonset)在每个节点上运行,使用多种发布策略公告服务和外部 IP 的对应关系。

MetalLB 能够用在集群里的任何 LoadBalancer 类型的 Service 中,但是 MetalLB 为大型 IP 地址池工作就不太现实了。

0

评论区