- 创建者: 虚拟的现实,上次更新时间:9月 06, 2024 需要 5 分钟阅读时间
container 网络
后面起的容器和指定容器共用一个 network namespace,而其他 namespace 还是各自隔离独享的,例如:
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 30d40445175c nginx:alpine "/docker-entrypoint.…" 4 hours ago Up 4 hours 0.0.0.0:80->80/tcp, :::80->80/tcp t2 $ docker run --rm -ti --net container:30d40445175c zhangguanzhang/netshoot 30d40445175c> curl -I localhost HTTP/1.1 200 OK ...
应用场景
- 某个容器内没有网络相关排查命令,例如新版本 coredns,可以这样进入容器网络后排查
- K8S 的 sandbox pause 容器,后面的容器都使用这个容器的 network namespace 组成 POD 的概念
nsenter 字面意思 namespace enter,进入到指定的 namespace 里:
$ nsenter --help Usage: nsenter [options] <program> [<argument>...] Run a program with namespaces of other processes. Options: -t, --target <pid> target process to get namespaces from -m, --mount[=<file>] enter mount namespace -u, --uts[=<file>] enter UTS namespace (hostname etc) -i, --ipc[=<file>] enter System V IPC namespace -n, --net[=<file>] enter network namespace -p, --pid[=<file>] enter pid namespace -U, --user[=<file>] enter user namespace -S, --setuid <uid> set uid in entered namespace -G, --setgid <gid> set gid in entered namespace --preserve-credentials do not touch uids or gids -r, --root[=<dir>] set the root directory -w, --wd[=<dir>] set the working directory -F, --no-fork do not fork before exec'ing <program> -Z, --follow-context set SELinux context according to --target PID -h, --help display this help and exit -V, --version output version information and exit For more details see nsenter(1).
container 网络与 nsenter 的区别
# 宿主机 DNS $ cat /etc/resolv.conf nameserver 10.236.158.114 nameserver 10.236.158.106 $ docker inspect a7abc0e4af98 | grep -m1 -i pid "Pid": 3376, $ nsenter --net --target 3376 $ netstat -nlptu Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 3376/nginx: master tcp6 0 0 :::80 :::* LISTEN 3376/nginx: master $ cat /etc/resolv.conf nameserver 10.236.158.114 nameserver 10.236.158.106 # 实际的容器内的 /etc/resolv.conf $ docker exec a7abc0e4af98 cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5
nsenter 最常见的就是 --net 选项,但是这样下涉及到文件的例如 /etc/resolv.conf 和 hosts 都是宿主机的,带上 --mount 后会进入容器的 rootfs 而没有排查命令。
简单的看 IP 、端口和网络信息可以 nsenter --net --target <pid> 后使用宿主机的命令查看,而依赖这俩文件的,还是用 --net container:xxx 起工具容器完善。
$ docker run --rm --net container:a7abc0e4af98 alpine:latest cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5
容器跨节点通信
单机 docker 的容器网络很好,但是生产里服务都部署在单机上就存在单点故障,通常是部署在多台机器上部署。要解决容器跨节点的问题。假设在没有 K8S 下,如何来实现容器跨节点通信。
跨节点容器互通
跨节点互通的前提,因为容器 IP 都是 docker0 的 CIDR 下的,所以互通机器的 docker0 的网段要配置成不同,然后再考虑实现手段。
宿主机转发
# 192.168.2.112 上 $ ping 172.27.0.2 PING 172.27.0.2 (172.27.0.2) 56(84) bytes of data. ^C --- 172.27.0.2 ping statistics --- 2 packets transmitted, 0 received, 100% packet loss, time 1005ms # 192.168.2.111 上 $ ping 172.17.0.2 PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data. ^C --- 172.17.0.2 ping statistics --- 2 packets transmitted, 0 received, 100% packet loss, time 2024ms
直接是不通的,因为会走默认路由,走到网关,匹配到网关的默认路由,最后发到后续的设备被丢弃。
根据网络知识,我们知道只要不过网关出去,也就是二层网络内 IP 不会被 NAT,所以我们可以直接添加路由让 172.27.0.0/16 发往 192.168.2.111 机器上:
# 192.168.2.112 上 $ ip route add 172.27.0.0/16 via 192.168.2.111 $ ping -c1 172.27.0.2 PING 172.27.0.2 (172.27.0.2) 56(84) bytes of data. 64 bytes from 172.27.0.1: icmp_seq=1 ttl=64 time=0.274 ms --- 172.27.0.2 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.274/0.274/0.274/0.000 ms
没问题,然后我们在 192.168.2.112 上的容器内 ping 下:
# 192.168.2.112 上 $ docker exec -ti ctr2 ping 172.27.0.2 PING 172.27.0.2 (172.27.0.2): 56 data bytes 64 bytes from 172.27.0.2: seq=0 ttl=62 time=0.341 ms 64 bytes from 172.27.0.2: seq=1 ttl=62 time=0.394 ms 64 bytes from 172.27.0.2: seq=2 ttl=62 time=0.360 ms 64 bytes from 172.27.0.2: seq=3 ttl=62 time=0.494 ms
但是会有个问题,如果你 ping 的同时去 192.168.2.111 上抓包:
$ tcpdump -nn -e -i eth0 icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 7e:bf:9c:27:f5:9e > 7e:10:5f:2f:f1:47, ethertype IPv4 (0x0800), length 98: 192.168.2.112 > 172.27.0.2: ICMP echo request, id 68, seq 42, length 64 7e:10:5f:2f:f1:47 > 7e:bf:9c:27:f5:9e, ethertype IPv4 (0x0800), length 98: 172.27.0.2 > 192.168.2.112: ICMP echo reply, id 68, seq 42, length 64
会发现来源 IP 是宿主机而并非容器的 IP,这是因为每个机器上都有 docker 配置的 SNAT。实际中,我们肯定希望一个节点上的容器访问另一个节点上的容器 IP 是不做 NAT 的,所以我们希望添加一个 iptables 规则跳过 docker 的 NAT iptables 规则,例如在 192.168.2.112 上:
- 来源 IP 172.17.0.0/16 + 目标 IP 是 172.27.0.0/16 的直接跳到走 POSTROUTING 的 ACCEPT
转换下就是在 192.168.2.112 上:
# 192.168.2.112 上 $ iptables -t nat -S POSTROUTING -P POSTROUTING ACCEPT -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE # 添加规则,跳过 nat $ iptables -w -t nat -I POSTROUTING -s 172.17.0.0/16 -d 172.27.0.0/16 -j RETURN
添加后我们在 192.168.2.112 上再 ping 目标容器 IP 会发现 ping 不通:
# 测试容器访问公网,不影响访问公网的 SNAT $ docker exec -ti ctr2 ping -c 2 223.5.5.5 PING 223.5.5.5 (223.5.5.5): 56 data bytes 64 bytes from 223.5.5.5: seq=0 ttl=112 time=18.470 ms 64 bytes from 223.5.5.5: seq=1 ttl=112 time=18.881 ms --- 223.5.5.5 ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 18.470/18.675/18.881 ms $ docker exec -ti ctr2 ping 172.27.0.2 PING 172.27.0.2 (172.27.0.2): 56 data bytes
然后我们在 2.111 上抓包,会发现:
$ tcpdump -nn -i eth0 icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes 10:51:13.421797 IP 172.17.0.2 > 172.27.0.2: ICMP echo request, id 74, seq 72, length 64 10:51:13.421947 IP 172.27.0.2 > 172.17.0.2: ICMP echo reply, id 74, seq 72, length 64
来源 IP 是对的,没有 SNAT 了,容器也回包了,但是因为 2.111 上没有添加路由,所以 27 回包给 17 会走默认路由发到网关上。
请不要在一些底层使用 openstack 虚拟化的虚机上验证本部分,因为 openstack 的组件会对从虚机网卡发出去包的 MAC 地址和 IP 校验,此处的包的 MAC 地址是网卡的,但是 IP 地址不是网卡的 IP 地址,过不了校验会被扔掉
结论
路由这种互通就是利用宿主机转发,也就是 flannel 的 host-gw 模式(自行去查看 iptables 规则,实际上是一个 16 位 CIDR 段,省去多条 iptables 规则),它没有对包做处理,比 vxlan、IPIP 做了隧道封装的效率高,缺点是只能在同一个二层网络内使用:
例如图里的大概网络 100.64.100.0/24 和机器 10.13.178.0/24 的有两台或者多台机器组了 K8S 节点,这个时候配置可不单单在 Linux 机器上添加路由就行了,而现实生活里,你更没有权限和能力在网络设备上去配置。
负载均衡
解决了 docker 容器跨节点互通,但是实际中不可能一个应用一个副本容器,也就是下面的情况
例如 ctr1 要访问 ctr2 ,但是 ctr2 有两个副本,最常见的负载均衡就是四层均衡了,不用考虑上层的应用层数据,只要保证 TCP/UDP 数据发送到即可。所以我们希望实现:
- 访问 ctr2-LB-IP:port 负载到 ctr2-1:port 和 ctr2-2:port
实现思路是两个:
- iptables 实现,目标IP ctr2-LB-IP + 目标端口 port + 轮询 dnat 到 ctr2-1:port 和 ctr2-2:port
- lvs 实现这个负载均衡
这个就是 K8S service 的 ClusterIP 的实现思想,kube-apiserver 负责分配出 LB-IP(service IP),kube-proxy watch 到 service 添加创建,会在本机上新增 iptables 或者 lvs 规则,在 POD 会发生重建调度,kube-proxy 会更新负载均衡后端的 real server 信息。
DNS
假设我们用 iptables 实现了负载均衡,LB-IP 肯定是动态分配的,ctr1 访问 ctr2 的 LB-IP 可以使用服务注册发现来适配动态 IP,但是这样负载均衡和应用耦合在一起了,如果某个应用需要 nginx 代理下,nginx 也部署多个副本,总不能魔改 nginx 加上服务注册发现 SDK。所以一个服务调用另一个服务都是用的域名(就像公有云上 RDS 实例,不用关注背后的实现),也就是 K8S 的 service name。
然后 DNS 也要多个副本,所以容器内的 dns server 会写指定的 DNS server 的 LB-IP,也就是 K8S 所有 POD 内:
$ grep nameserver /etc/resolv.conf nameserver 10.96.0.10
- 无标签