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 地址,过不了校验会被扔掉
信息 |
---|
思考,请结合前面的 iptables 教程和本小结的知识,自行配置让两端容器互通。 |
结论
路由这种互通就是利用宿主机转发,也就是 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 |