Fork me on GitHub

2023年2月

实战 Docker 容器网络

我们知道,容器技术出现的初衷是对容器之间以及容器和宿主机之间的进程、用户、网络、存储等进行隔离,提供一种类似沙盒的虚拟环境,容器网络是这个虚拟环境的一部分,它能让应用从宿主机操作系统的网络环境中独立出来,形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等模块。但是网络作为一种特殊的通信机制,我们有时候又希望容器之间、容器和宿主机之间甚至容器和远程主机之间能够互相通信,既要保证容器网络的隔离性,又要实现容器网络的连通性,这使得在容器环境下,网络的问题变得异常复杂。

Docker 是目前最流行的容器技术之一,它提供了一套完整的网络解决方案,不仅可以解决单机网络问题,也可以实现跨主机容器之间的通信。

容器网络模型

在学习各种不同的容器网络解决方案之前,我们首先来了解下 CNM 的概念。CNM(Container Network Model) 是 Docker 提出并主推的一种容器网络架构,这是一套抽象的设计规范,主要包含三个主要概念:

  • Sandbox - 提供了容器的虚拟网络栈,即端口套接字、IP 路由表、防火墙、DNS 配置等内容,主要用于隔离容器网络与宿主机网络,形成了完全独立的容器网络环境,一般通过 Linux 中的 Network Namespace 或类似的技术实现。一个 Sandbox 中可以包含多个 Endpoint。
  • Endpoint - 就是虚拟网络的接口,就像普通网络接口一样,它的主要职责是创建 Sandbox 到 Network 之间的连接,一般使用 veth pair 之类的技术实现。一个已连接的 Endpoint 只能归属于一个 Sandbox 和 一个 Network。
  • Network - 提供了一个 Docker 内部的虚拟子网,一个 Network 可以包含多个 Endpoint,同一个 Network 内的 Endpoint 之间可以互相通讯,一般使用 Linux bridge 或 VLAN 来实现。

这三个概念之间的关系如下图所示:

cnm.jpeg

libnetwork 是一个 Go 语言编写的开源库,它是 CNM 规范的标准实现,Docker 就是通过 libnetwork 库来实现 CNM 规范中的三大概念,此外它还实现了本地服务发现、基于 Ingress 的容器负载均衡、以及网络控制层和管理层功能。

除了控制层和管理层,我们还需要实现网络的数据层,这部分 Docker 是通过 驱动(Driver) 来实现的。驱动负责处理网络的连通性和隔离性,通过不同的驱动,我们可以扩展 Docker 的网络栈,实现不同的网络类型。

Docker 内置如下这些驱动,通常被称作 原生驱动 或者 本地驱动

第三方也可以通过 Network plugins 实现自己的网络驱动,这些驱动也被称作 远程驱动,比如 calicoflannelweave 等。

network-drivers.png

基于 Docker 网络的这种可插拔的设计,我们通过切换不同的网络驱动,就可以实现不同的容器网络解决方案。

单机容器网络方案

让我们从最简单的单机网络方案开始。Docker 在安装时,默认会在系统上创建三个网络,可以通过 docker network ls 命令查看:

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
04eb1fccf2a8   bridge    bridge    local
189dfa3e3e64   host      host      local
1e63120a4e7a   none      null      local

这三个网络分别是 bridgehostnone,可以看到这三个网络都是 local 类型的。

Bridge 网络

Bridge 网络是目前使用最广泛的 Docker 网络方案,当我们使用 docker run 启动容器时,默认使用的就是 Bridge 网络,我们也可以通过命令行参数 --network=bridge 显式地指定使用 Bridge 网络:

$ docker run --rm -it --network=bridge busybox
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:9 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:782 (782.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)

可以看到,使用 Bridge 网络的容器里除了 lo 这个本地回环网卡外,还有一个 eth0 以太网卡,那么这个 eth0 网卡是连接到哪里呢?我们可以从 /sys/class/net/eth0 目录下的 ifindexiflink 文件中一探究竟:

/ # cat /sys/class/net/eth0/ifindex
74
/ # cat /sys/class/net/eth0/iflink
75

其中 ifindex 表示网络设备的全局唯一 ID,而 iflink 主要被隧道设备使用,用于标识隧道另一头的设备 ID。也可以直接使用 ip link 命令查看:

/ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
74: eth0@if75: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff

如果 ifindexiflink 相同,表示这是一个真实的设备,ip link 中直接显示设备名,比如上面的 lo 网卡;如果 ifindexiflink 不相同,表示这是一个隧道设备,ip link 中显示格式为 ifindex: eth0@iflink,比如上面的 eth0 网卡。

所以容器中的 eth0 是一个隧道设备,它的 ID 为 74,连接它的另一头设备 ID 为 75,那么这个 ID 为 75 的设备又在哪里呢?答案在宿主机上。

我们在宿主机上运行 ip link 命令,很快就可以找到这个 ID 为 75 的设备:

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:c1:96:99 brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
    link/ether 02:42:43:d2:12:6f brd ff:ff:ff:ff:ff:ff
75: vethe118873@if74: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
    link/ether 96:36:59:73:43:cc brd ff:ff:ff:ff:ff:ff link-netnsid 0

设备的名称为 vethe118873@if74,也暗示着另一头连接着容器里的 ID 为 74 的设备。像上面这样成对出现的设备,被称为 veth pairveth 表示虚拟以太网(Virtual Ethernet),而 veth pair 就是一个虚拟的以太网隧道(Ethernet tunnel),它可以实现隧道一头接收网络数据后,隧道另一头也能立即接收到,可以把它想象成一根网线,一头插在容器里,另一头插在宿主机上。那么它又插在宿主机的什么位置呢?答案是 docker0 网桥。

使用 brctl show 查看宿主机的网桥:

# brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.024243d2126f    no        vethe118873

可以看到一个名为 docker0 的网桥设备,并且能看到 vethe118873 接口就插在这个网桥上。这个 docker0 网桥是 Docker 在安装时默认创建的,当我们不指定 --network 参数时,创建的容器默认都挂在这个网桥上,它也是实现 Bridge 网络的核心部分。

使用下面的命令可以更直观的看出该网桥的详细信息:

# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "ea473e613c26e31fb6d51edb0fb541be0d11a73f24f81d069e3104eef97b5cfc",
        "Created": "2022-06-03T11:56:33.258310522+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "33c8609c8cbe8e99f63fd430c926bd05167c2c92b64f97e4a06cfd0892f7e74c": {
                "Name": "suspicious_ganguly",
                "EndpointID": "3bf4b3d642826ab500f50eec4d5c381d20c75393ab1ed62a8425f8094964c122",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

其中 IPAM 部分表明这个网桥的网段为 172.17.0.0/16,所以上面看容器的 IP 地址为 172.17.0.2;另外网关的地址为 172.17.0.1,这个就是 docker0 的地址:

# ip addr show type bridge
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:43:d2:12:6f brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:43ff:fed2:126f/64 scope link 
       valid_lft forever preferred_lft forever

docker0 网段地址可以在 Docker Daemon 的配置文件 /etc/docker/daemon.json 中进行修改:

$ vi /etc/docker/daemon.json
{
  "bip": "172.100.0.1/24"
}

修改之后重启 Docker Daemon:

$ systemctl restart docker

验证配置是否生效:

$ ip addr show docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:43:d2:12:6f brd ff:ff:ff:ff:ff:ff
    inet 172.100.0.1/24 brd 172.100.0.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:43ff:fed2:126f/64 scope link 
       valid_lft forever preferred_lft forever

下图是 Bridge 网络大致的网络拓扑结构:

bridge-network.png

自定义 Bridge 网络

从 Bridge 的网络拓扑结构中我们可以看到,所有的容器都连接到同一个 docker0 网桥,它们之间的网络是互通的,这存在着一定的安全风险。为了保证我们的容器和其他的容器之间是隔离的,我们可以创建自定义的网络:

$ docker network create --driver=bridge test
5266130b7e5da2e655a68d00b280bc274ff4acd9b76f46c9992bcf0cd6df7f6a

创建之后,再次查看系统中的网桥列表:

$ brctl show
bridge name    bridge id        STP enabled    interfaces
br-5266130b7e5d        8000.0242474f9085    no        
docker0        8000.024243d2126f    no    

brctl show 的运行结果中可以看出多了一个新的网桥,网桥的名字正是我们创建的网络 ID,Docker 会自动为网桥分配子网和网关,我们也可以在创建网络时通过 --subnet--gateway 参数手工设置:

$ docker network create --driver=bridge --subnet=172.200.0.0/24 --gateway=172.200.0.1 test2
18f549f753e382c5054ef36af28b6b6a2208e4a89ee710396eb2b729ee8b1281

创建好自定义网络之后,就可以在启动容器时通过 --network 指定:

$ docker run --rm -it --network=test2 busybox
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    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
80: eth0@if81: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:c8:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.200.0.2/24 brd 172.200.0.255 scope global eth0
       valid_lft forever preferred_lft forever

由于 test2 的网段为 172.200.0.0/24,所以 Docker 为我们的容器分配的 IP 地址是 172.200.0.2,这个网段和 docker0 是隔离的,我们 ping 之前的容器 IP 是 ping 不同的:

/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss

自定义 Bridge 网络的拓扑结构如下图所示:

custom-network.png

虽然通过上面的实验我们发现不同网桥之间的网络是隔离的,不过仔细思考后会发现这里有一个奇怪的地方,自定义网桥和 docker0 网桥实际上都连接着宿主机上的 eth0 网卡,如果有对应的路由规则它们之间按理应该是可以通信的,为什么会网络不通呢?这就要从 iptables 中寻找答案了。

Docker 在创建网络时会自动添加一些 iptables 规则,用于隔离不同的 Docker 网络,可以通过下面的命令查看 iptables 规则:

$ iptables-save | grep ISOLATION
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-5266130b7e5d ! -o br-5266130b7e5d -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-18f549f753e3 ! -o br-18f549f753e3 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-5266130b7e5d -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-18f549f753e3 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN

前两行以 : 开头,表示创建两条空的 规则链(Rule Chain),在 iptables 中,规则链是由一系列规则(Rule)组成的列表,每个规则可以根据指定的条件来匹配或过滤数据包,并执行特定的动作。规则链可以包含其他规则链,从而形成一个规则链树。iptables 中有五个默认的规则链,分别是:

  • INPUT:用于处理进入本地主机的数据包;
  • OUTPUT:用于处理从本地主机发出的数据包;
  • FORWARD:用于处理转发到其他主机的数据包,这个规则链一般用于 NAT;
  • PREROUTING:用于处理数据包在路由之前的处理,如 DNAT;
  • POSTROUTING:用于处理数据包在路由之后的处理,如 SNAT。

第三行 -A FORWARD -j DOCKER-ISOLATION-STAGE-1 表示将在 FORWARD 规则链中添加一个新的规则链 DOCKER-ISOLATION-STAGE-1,而这个新的规则链包含下面定义的几条规则:

-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-5266130b7e5d ! -o br-5266130b7e5d -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-18f549f753e3 ! -o br-18f549f753e3 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN

第一条规则中的 -i docker0 ! -o docker0 表示数据包想进入 docker0 接口并且不是从 docker0 接口发出的,-i 表示 输入接口(input interface)-o 表示 输出接口(output interface),后面的 -j DOCKER-ISOLATION-STAGE-2 表示当匹配该规则时,跳转到 DOCKER-ISOLATION-STAGE-2 规则链继续处理。后面的规则都非常类似,每一条对应一个 Docker 创建的网桥的名字。最后一条表示当所有的规则都不满足时,那么这个数据包将被直接返回到原始的调用链,也就是被防火墙规则调用链中的下一条规则处理。

在上面的实验中,我们的容器位于 test2 网络中(对应的网桥是 br-18f549f753e3),要 ping 的 IP 地址属于 docker0 网络中的容器,很显然,数据包是发往 docker0 接口的(输入接口),并且是来自 br-18f549f753e3 接口的(输出接口),所以数据包满足 DOCKER-ISOLATION-STAGE-1 规则链中的第一条规则,将进入 DOCKER-ISOLATION-STAGE-2 规则链:

-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-5266130b7e5d -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-18f549f753e3 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN

DOCKER-ISOLATION-STAGE-2 规则链的第三条规则是,如果数据包从 br-18f549f753e3 接口发出则被直接丢弃。这就是我们为什么在自定义网络中 ping 不通 docker0 网络的原因。

我们可以通过 iptables -R 命令,将规则链中的规则修改为 ACCEPT:

$ iptables -R DOCKER-ISOLATION-STAGE-2 1 -o docker0 -j ACCEPT
$ iptables -R DOCKER-ISOLATION-STAGE-2 2 -o br-5266130b7e5d -j ACCEPT
$ iptables -R DOCKER-ISOLATION-STAGE-2 3 -o br-18f549f753e3 -j ACCEPT

此时再测试网络的连通性发现可以正常 ping 通了:

/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=63 time=0.123 ms
64 bytes from 172.17.0.2: seq=1 ttl=63 time=0.258 ms
64 bytes from 172.17.0.2: seq=2 ttl=63 time=0.318 ms

测试完记得将规则还原回来:

$ iptables -R DOCKER-ISOLATION-STAGE-2 1 -o docker0 -j DROP
$ iptables -R DOCKER-ISOLATION-STAGE-2 2 -o br-5266130b7e5d -j DROP
$ iptables -R DOCKER-ISOLATION-STAGE-2 3 -o br-18f549f753e3 -j DROP

docker network connect

上面介绍了 Docker 如何通过 iptables 隔离不同网桥之间的通信,但是在一些现实场景中,我们可能需要它们之间互相通信,这可以通过 docker network connect 命令来实现:

$ docker network connect test2 8f358828cec2

这条命令的意思是将 8f358828cec2 容器连接到 test2 网络,相当于在容器里添加了一张网卡,并使用一根网线连接到 test2 网络上。我们在容器里可以看到多了一张新网卡 eth1

/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    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
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:c8:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/24 brd 172.17.0.255 scope global eth0
       valid_lft forever preferred_lft forever
30: eth1@if31: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:c8:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.200.0.3/24 brd 172.200.0.255 scope global eth1
       valid_lft forever preferred_lft forever

eth1@if31 这个名字可以看出,它也是一个 veth pair 隧道设备,设备的一头插在容器里的 eth1 网卡上,另一头插在自定义网桥上:

docker-network-connect.png

此时,容器之间就可以互相通信了,不过要注意的是,连接在 docker0 的容器现在有两个 IP,我们只能 ping 通 172.200.0.3172.17.0.2 这个 IP 仍然是 ping 不通的:

/ # ping 172.200.0.3
PING 172.200.0.3 (172.200.0.3): 56 data bytes
64 bytes from 172.200.0.3: seq=0 ttl=64 time=0.109 ms
64 bytes from 172.200.0.3: seq=1 ttl=64 time=0.106 ms
^C
--- 172.200.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.106/0.107/0.109 ms
/ #
/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

Container 网络

上面学习了两种容器间通信的方式,一种是通过 --network 参数在创建容器时指定自定义网络,另一种是 docker network connect 命令将已有容器加入指定网络,这两种方式都可以理解为:在容器内加一张网卡,并用网线连接到指定的网桥设备。

Docker 还提供了另一种容器间通信的方式:container 网络(也被称为 joined 网络),它可以让两个容器共享一个网络栈。我们通过 --network=container:xx 参数创建一个容器:

$ docker run --rm -it --network=container:9bd busybox

这个新容器将共享 9bd 这个容器的网络栈,容器的网络信息如下:

/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    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
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

我们进入 9bd 容器并查看该容器的网络信息:

# docker exec -it 9bd sh
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    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
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

可以发现新容器和 9bd 两个容器具有完全相同的网络信息,不仅 IP 地址一样,连 MAC 地址也一样,这两个容器之间可以通过 127.0.0.1 来通信。这种网络模式的拓扑图如下所示:

container-network.png

Container 网络一般用于多个服务的联合部署,比如有些应用需要运行多个服务,使用 Container 网络可以将它们放在同一个网络中,从而让它们可以互相访问,相比于自定义网桥的方式,这种方式通过 loopback 通信更高效。此外 Container 网络也非常适合边车模式,比如对已有的容器进行测试或监控等。

Host 网络

通过 --network=host 参数可以指定容器使用 Host 网络,使用 Host 网络的容器和宿主机共用同一个网络栈,也就是说,在容器里面,端口套接字、IP 路由表、防火墙、DNS 配置都和宿主机完全一样:

> docker run --rm -it --network=host busybox
/ # ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:07:3E:52:7E
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.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)

eth0      Link encap:Ethernet  HWaddr 0A:3E:D1:F7:58:33
          inet addr:192.168.65.4  Bcast:0.0.0.0  Mask:255.255.255.255
          inet6 addr: fe80::83e:d1ff:fef7:5833/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1280 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1271 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:2752699 (2.6 MiB)  TX bytes:107715 (105.1 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:709688 errors:0 dropped:0 overruns:0 frame:0
          TX packets:709688 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:149025485 (142.1 MiB)  TX bytes:149025485 (142.1 MiB)

veth2fbdd331 Link encap:Ethernet  HWaddr 46:B7:0B:D6:16:C2
          inet6 addr: fe80::44b7:bff:fed6:16c2/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:3876 errors:0 dropped:0 overruns:0 frame:0
          TX packets:3830 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:370360 (361.6 KiB)  TX bytes:413700 (404.0 KiB)

veth4d3538f6 Link encap:Ethernet  HWaddr 56:C0:72:ED:10:21
          inet6 addr: fe80::54c0:72ff:feed:1021/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:10934 errors:0 dropped:0 overruns:0 frame:0
          TX packets:12400 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:2197702 (2.0 MiB)  TX bytes:3897932 (3.7 MiB)

在容器里使用 ifconfig 命令,输出的结果其实是我们宿主机的网卡,容器里的 hostname 也和宿主机一样,所以我们可以在容器里使用 localhost 访问宿主机。

由于直接使用宿主机的网络,少了一层网络转发,所以 Host 网络具有非常好的性能,如果你的应用对网络传输效率有较高的要求,则可以选择使用 Host 网络。另外,使用 Host 网络还可以在容器里直接对宿主机网络进行配置,比如管理 iptables 或 DNS 配置等。

需要注意的是,Host 网络的主要优点是提高了网络性能,但代价是容器与主机共享网络命名空间,这可能会带来安全隐患。因此,需要仔细考虑使用 Host 网络的场景,并采取适当的安全措施,以确保容器和主机的安全性。

下图是 Host 网络大致的网络拓扑结构:

host-network.png

None 网络

None 网络是一种特殊类型的网络,顾名思义,它表示容器不连接任何网络。None 网络的容器是一个完全隔绝的环境,它无法访问任何其他容器或宿主机。我们在创建容器时通过 --network=none 参数使用 None 网络:

$ docker run --rm -it --network=none busybox
/ # 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:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

在使用 None 网络的容器里,除了 lo 设备之外,没有任何其他的网卡设备。这种与世隔离的环境非常适合一些安全性工作或测试工作,比如,对恶意软件进行逆向分析时,我们不希望恶意软件访问外部网络,使用 None 网络可以避免它对宿主机或其他服务造成影响;或者在单元测试时,通过 None 网络可以模拟网络隔离的效果,这样我们可以测试和验证没有网络情况下的程序表现。

下图是 None 网络大致的网络拓扑结构:

none-network.png

跨主机容器网络方案

在前一节中我们学习了 Docker 的几种网络模式,它们都是用于解决单个主机上的容器通信问题,那么如何实现不同主机上的容器通信呢?实际上,Docker 提供了几种原生的跨主机网络解决方案,包括:

这些跨主机网络方案需要具备一定的网络知识,比如 overlay 网络是基于 VxLAN (Virtual eXtensible LAN) 技术实现的,这是一种 隧道技术,将二层数据封装到 UDP 进行传输,这种网络技术我们将其称为 overlay 网络,指的是建立在其他网络上的网络;macvlan 是一种网卡虚拟化技术,它本身是 linux kernel 的一个模块,可以为同一个物理网卡配置多个 MAC 地址,每个 MAC 地址对应一个虚拟接口,由于它直接使用真实网卡通信,所以性能非常好,不过每个虚拟接口都有自己的 MAC 地址,而网络接口和交换机支持的 MAC 地址个数是有限的,MAC 地址过多时会造成严重的性能损失;ipvlan 解决了 MAC 地址过多的问题,它和 macvlan 类似,也是 linux kernel 的一个模块,但是它和 macvlan 不一样的是,macvlan 是为同一个网卡虚拟出多个 MAC 地址,而 ipvlan 是为同一个 MAC 地址虚拟多个 IP 地址;macvlan 和 ipvlan 不需要对包进行封装,这种网络技术又被称为 underlay 网络

除此之外,还有很多第三方网络解决方案,它们通过 Docker 网络的插件机制实现,包括:

参考

  1. Networking overview | Docker Documentation
  2. 每天5分钟玩转Docker容器技术
  3. Docker 网络模式详解及容器间网络通信
  4. 网络 - Docker — 从入门到实践
  5. 花了三天时间终于搞懂 Docker 网络了
  6. docker的网络-Container network interface(CNI)与Container network model(CNM)
  7. Docker容器网络互联
  8. linux 网络虚拟化: network namespace 简介
  9. Linux 虚拟网络设备 veth-pair 详解,看这一篇就够了
  10. 从宿主机直接进入docker容器的网络空间
  11. Deep dive into Linux Networking and Docker — Bridge, vETH and IPTables
  12. Container Networking: What You Should Know

更多

Kubernetes 网络

  1. 数据包在 Kubernetes 中的一生(1)
  2. 从零开始入门 K8s:理解 CNI 和 CNI 插件
  3. IPVS从入门到精通kube-proxy实现原理
  4. Kubernetes(k8s)kube-proxy、Service详解
扫描二维码,在手机上阅读!

使用 Helm 部署 Kubernetes 应用

Kubernetes 使用小记 中我们学习了如何通过 Deployment 部署一个简单的应用服务并通过 Service 来暴露它,在真实的场景中,一套完整的应用服务可能还会包含很多其他的 Kubernetes 资源,比如 DaemonSetIngressConfigMapSecret 等,当用户部署这样一套完整的服务时,他就不得不关注这些底层的 Kubernetes 概念,这对用户非常不友好。

我们知道,几乎每个操作系统都内置了一套软件包管理器,用来方便用户安装、配置、卸载或升级各种系统软件和应用软件,比如 Debian 和 Ubuntu 使用的是 DEB 包管理器 dpkgapt,CentOS 使用的是 RPM 包管理器 yum,Mac OS 有 Homebrew,Windows 有 WinGetChocolatey 等,另外很多系统还内置了图形界面的应用市场方便用户安装应用,比如 Windows 应用商店、Apple Store、安卓应用市场等,这些都可以算作是包管理器。

使用包管理器安装应用大大降低了用户的使用门槛,他只需要执行一句命令或点击一个安装按钮即可,而不用去关心这个应用需要哪些依赖和哪些配置。那么在 Kubernetes 下能不能也通过这种方式来部署应用服务呢?Helm 作为 Kubernetes 的包管理器,解决了这个问题。

Helm 简介

Helm 是 Deis 团队 于 2015 年发布的一个 Kubernetes 包管理工具,当时 Deis 团队还没有被微软收购,他们在一家名为 Engine Yard 的公司里从事 PaaS 产品 Deis Workflow 的开发,在开发过程中,他们需要将众多的微服务部署到 Kubernetes 集群中,由于在 Kubernetes 里部署服务需要开发者手动编写和维护数量繁多的 Yaml 文件,并且服务的分发和安装也比较繁琐,Matt Butcher 和另外几个同事于是在一次 Hackthon 团建中发起了 Helm 项目。

Helm 的取名非常有意思,Kubernetes 是希腊语 “舵手” 的意思,而 Helm 是舵手操作的 “船舵”,用来控制船的航行方向。

Helm 引入了 Chart 的概念,它也是 Helm 所使用的包格式,可以把它理解成一个描述 Kubernetes 相关资源的文件集合。开发者将自己的应用配置文件打包成 Helm chart 格式,然后发布到 ArtifactHub,这和你使用 docker push 将镜像推送到 DockerHub 镜像仓库一样,之后用户安装你的 Kubernetes 应用只需要一条简单的 Helm 命令就能搞定,极大程度上解决了 Kubernetes 应用维护、分发、安装等问题。

Helm 在 2018 年 6 月加入 CNCF,并在 2020 年 4 月正式毕业,目前已经是 Kubernetes 生态里面不可或缺的明星级项目。

快速开始

这一节我们将通过官方的入门示例快速掌握 Helm 的基本用法。

安装 Helm

我们首先从 Helm 的 Github Release 页面找到最新版本,然后通过 curl 将安装包下载下来:

$ curl -LO https://get.helm.sh/helm-v3.11.1-linux-amd64.tar.gz

然后解压安装包,并将 helm 安装到 /usr/local/bin 目录:

$ tar -zxvf helm-v3.11.1-linux-amd64.tar.gz
$ sudo mv linux-amd64/helm /usr/local/bin/helm

这样 Helm 就安装好了,通过 helm version 检查是否安装成功:

$ helm version
version.BuildInfo{Version:"v3.11.1", GitCommit:"293b50c65d4d56187cd4e2f390f0ada46b4c4737", GitTreeState:"clean", GoVersion:"go1.18.10"}

使用 helm help 查看 Helm 支持的其他命令和参数。

一般来说,直接下载 Helm 二进制文件就可以完成安装,不过官方也提供了一些其他方法来安装 Helm,比如通过 get_helm.sh 脚本来自动安装,或者通过 yumapt 这些操作系统的包管理器来安装,具体内容可参考官方的 安装文档

使用 Helm

Helm 安装完成之后,我们就可以使用 Helm 在 Kubernetes 中安装应用了。对于新手来说,最简单的方法是在 ArtifactHub 上搜索要安装的应用,然后按照文档中的安装步骤来操作即可。比如我们想要部署 Nginx,首先在 ArtifactHub 上进行搜索:

search-nginx.png

注意左侧的 KIND 勾选上 Helm charts,搜索出来的结果会有很多条,这些都是由不同的组织或个人发布的,可以在列表中看出发布的组织或个人名称,以及该 Charts 所在的仓库。Bitnami 是 Helm 中最常用的仓库之一,它内置了很多常用的 Kubernetes 应用,于是我们选择进入 第一条搜索结果

bitnami-nginx.png

这里可以查看关于 Nginx 应用的安装步骤、使用说明、以及支持的配置参数等信息,我们可以点击 INSTALL 按钮,会弹出一个对话框,并显示该应用的安装步骤:

nginx-install.png

我们按照它的提示,首先使用 helm repo add 将 Bitnami 仓库添加到我们的电脑:

$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

然后使用 helm install 安装 Nginx 应用:

$ helm install my-nginx bitnami/nginx --version 13.2.23
NAME: my-nginx
LAST DEPLOYED: Sat Feb 11 08:58:10 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: nginx
CHART VERSION: 13.2.23
APP VERSION: 1.23.3

** Please be patient while the chart is being deployed **
NGINX can be accessed through the following DNS name from within your cluster:

    my-nginx.default.svc.cluster.local (port 80)

To access NGINX from outside the cluster, follow the steps below:

1. Get the NGINX URL by running these commands:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w my-nginx'

    export SERVICE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].port}" services my-nginx)
    export SERVICE_IP=$(kubectl get svc --namespace default my-nginx -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    echo "http://${SERVICE_IP}:${SERVICE_PORT}"

稍等片刻,Nginx 就安装好了,我们可以使用 kubectl 来验证:

$ kubectl get deployments
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
my-nginx   1/1     1            1           12m
$ kubectl get svc
NAME             TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
kubernetes       ClusterIP      10.96.0.1        <none>        443/TCP          75d
my-nginx         LoadBalancer   10.111.151.137   localhost     80:31705/TCP     12m

访问 localhost:80 可以看到 Nginx 已成功启动:

nginx.png

卸载和安装一样也很简单,使用 helm delete 命令即可:

$ helm delete my-nginx
release "my-nginx" uninstalled

我们还可以通过 --set 选项来改写 Nginx 的一些参数,比如默认情况下创建的 Service 端口是 80,使用下面的命令将端口改为 8080:

$ helm install my-nginx bitnami/nginx --version 13.2.23 \
    --set service.ports.http=8080

更多的参数列表可以参考安装文档中的 Parameters 部分。

另外,helm 命令和它的子命令还支持一些其他选项,比如上面的 --version--set 都是 helm install 子命令的选项,我们可以使用 helm 命令的 --namespace 选项将应用部署到指定的命名空间中:

$ helm install my-nginx bitnami/nginx --version 13.2.23 \
    --set service.ports.http=8080 \
    --namespace nginx --create-namespace

常用的 Helm 命令

通过上一节的学习,我们大致了解了 Helm 中三个非常重要的概念:

  • Repository
  • Chart
  • Release

Repository 比较好理解,就是存放安装包的仓库,可以使用 helm env 查看 HELM_REPOSITORY_CACHE 环境变量的值,这就是仓库的本地地址,用于缓存仓库信息以及已下载的安装包:

$ helm env | grep HELM_REPOSITORY_CACHE
HELM_REPOSITORY_CACHE="/home/aneasystone/.cache/helm/repository"

当我们执行 helm repo add 命令时,会将仓库信息缓存到该目录;当我们执行 helm install 命令时,也会将安装包下载并缓存到该目录。查看该目录,可以看到我们已经添加的 bitnami 仓库信息,还有已下载的 nginx 安装包:

$ ls /home/aneasystone/.cache/helm/repository
bitnami-charts.txt  bitnami-index.yaml  nginx-13.2.23.tgz

这个安装包就被称为 Chart,是 Helm 特有的安装包格式,这个安装包中包含了一个 Kubernetes 应用的所有资源文件。而 Release 就是安装到 Kubernetes 中的 Chart 实例,每个 Chart 可以在集群中安装多次,每安装一次,就会产生一个 Release。

明白了这三个基本概念,我们就可以这样理解 Helm 的用途:它先从 Repository 中下载 Chart,然后将 Chart 实例化后以 Release 的形式部署到 Kubernetes 集群中,如下图所示(图片来源):

helm-overview.png

而绝大多数的 Helm 命令,都是围绕着这三大概念进行的。

Repository 相关命令

helm repo

该命令用于添加、删除或更新仓库等操作。

将远程仓库添加到本地:

$ helm repo add bitnami https://charts.bitnami.com/bitnami

查看本地已安装仓库列表:

$ helm repo list
NAME    URL
bitnami https://charts.bitnami.com/bitnami

更新本地仓库:

$ helm repo update bitnami
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈

读取某个仓库目录,根据找到的 Chart 生成索引文件:

$ helm repo index SOME_REPO_DIR
# cat SOME_REPO_DIR/index.yaml

删除本地仓库:

$ helm repo remove bitnami
"bitnami" has been removed from your repositories

helm search

该命令用于在 ArtifactHub 或本地仓库中搜索 Chart。

在 ArtifactHub 中搜索 Chart:

$ helm search hub nginx
URL                                                     CHART VERSION   APP VERSION          DESCRIPTION
https://artifacthub.io/packages/helm/cloudnativ...      3.2.0           1.16.0               Chart for the nginx server
https://artifacthub.io/packages/helm/bitnami/nginx      13.2.23         1.23.3               NGINX Open Source is a web server that can be a...

要注意搜索出来的 Chart 链接可能不完整,不能直接使用,根据 stackoverflow 这里的解答,我们可以使用 -o json 选项将结果以 JSON 格式输出:

$ helm search hub nginx -o json | jq .
[
  {
    "url": "https://artifacthub.io/packages/helm/cloudnativeapp/nginx",
    "version": "3.2.0",
    "app_version": "1.16.0",
    "description": "Chart for the nginx server",
    "repository": {
      "url": "https://cloudnativeapp.github.io/charts/curated/",
      "name": "cloudnativeapp"
    }
  },
  {
    "url": "https://artifacthub.io/packages/helm/bitnami/nginx",
    "version": "13.2.23",
    "app_version": "1.23.3",
    "description": "NGINX Open Source is a web server that can be also used as a reverse proxy, load balancer, and HTTP cache. Recommended for high-demanding sites due to its ability to provide faster content.",
    "repository": {
      "url": "https://charts.bitnami.com/bitnami",
      "name": "bitnami"
    }
  }
}

在本地配置的所有仓库中搜索 Chart:

$ helm search repo nginx
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION
bitnami/nginx                           13.2.23         1.23.3          NGINX Open Source is a web server that can be a...
bitnami/nginx-ingress-controller        9.3.28          1.6.2           NGINX Ingress Controller is an Ingress controll...
bitnami/nginx-intel                     2.1.15          0.4.9           DEPRECATED NGINX Open Source for Intel is a lig...

Chart 相关命令

helm create

该命令用于创建一个新的 Chart 目录:

$ helm create demo
Creating demo

创建的 Chart 目录是下面这样的结构:

$ tree demo
demo
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

3 directories, 10 files

helm package

将 Chart 目录(必须包含 Chart.yaml 文件)打包成 Chart 归档文件:

$ helm package nginx
Successfully packaged chart and saved it to: /home/aneasystone/helm/nginx-13.2.23.tgz

helm lint

验证 Chart 是否存在问题:

$ helm lint ./nginx
==> Linting ./nginx

1 chart(s) linted, 0 chart(s) failed

helm show

该命令用于显示 Chart 的基本信息,包括:

  • helm show chart - 显示 Chart 定义,实际上就是 Chart.yaml 文件的内容
  • helm show crds - 显示 Chart 的 CRD
  • helm show readme - 显示 Chart 的 README.md 文件中的内容
  • helm show values - 显示 Chart 的 values.yaml 文件中的内容
  • helm show all - 显示 Chart 的所有信息

helm pull

从仓库中将 Chart 安装包下载到本地:

$ helm pull bitnami/nginx
$ ls
nginx-13.2.23.tgz

helm push

将 Chart 安装包推送到远程仓库:

$ helm push [chart] [remote]

Release 相关命令

helm install

将 Chart 安装到 Kubernetes 集群:

$ helm install my-nginx bitnami/nginx --version 13.2.23

安装时可以通过 --set 选项修改配置参数:

$ helm install my-nginx bitnami/nginx --version 13.2.23 \
    --set service.ports.http=8080

其中 bitnami/nginx 是要安装的 Chart,这种写法是最常用的格式,被称为 Chart 引用,一共有六种不同的 Chart 写法:

  • 通过 Chart 引用:helm install my-nginx bitnami/nginx
  • 通过 Chart 包:helm install my-nginx ./nginx-13.2.23.tgz
  • 通过未打包的 Chart 目录:helm install my-nginx ./nginx
  • 通过 Chart URL:helm install my-nginx https://example.com/charts/nginx-13.2.23.tgz
  • 通过仓库 URL 和 Chart 引用:helm install --repo https://example.com/charts/ my-nginx nginx
  • 通过 OCI 注册中心:helm install my-nginx --version 13.2.23 oci://example.com/charts/nginx

helm list

显示某个命名空间下的所有 Release:

$ helm list --namespace default
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
my-nginx        default         1               2023-02-11 09:35:09.4058393 +0800 CST   deployed        nginx-13.2.23   1.23.3

helm status

查询某个 Release 的状态信息:

$ helm status my-nginx

helm get

获取某个 Release 的扩展信息,包括:

  • helm get hooks - 获取 Release 关联的钩子信息
  • helm get manifest - 获取 Release 的清单信息
  • helm get notes - 获取 Release 的注释
  • helm get values - 获取 Release 的 values 文件
  • helm get all - 获取 Release 的所有信息

helm upgrade

将 Release 升级到新版本的 Chart:

$ helm upgrade my-nginx ./nginx
Release "my-nginx" has been upgraded. Happy Helming!

升级时可以通过 --set 选项修改配置参数:

$ helm upgrade my-nginx ./nginx \
    --set service.ports.http=8080

helm history

查看某个 Release 的版本记录:

$ helm history my-nginx
REVISION        UPDATED                         STATUS          CHART           APP VERSION     DESCRIPTION
1               Sun Feb 12 11:14:18 2023        superseded      nginx-13.2.23   1.23.3          Install complete
2               Sun Feb 12 11:15:53 2023        deployed        nginx-13.2.23   1.23.3          Upgrade complete

helm rollback

将 Release 回滚到某个版本:

$ helm rollback my-nginx 1
Rollback was a success! Happy Helming!

再查看版本记录可以看到多了一条记录:

$ helm history my-nginx
REVISION        UPDATED                         STATUS          CHART           APP VERSION     DESCRIPTION
1               Sun Feb 12 11:14:18 2023        superseded      nginx-13.2.23   1.23.3          Install complete
2               Sun Feb 12 11:15:53 2023        superseded      nginx-13.2.23   1.23.3          Upgrade complete
3               Sun Feb 12 11:20:27 2023        deployed        nginx-13.2.23   1.23.3          Rollback to 1

helm uninstall

卸载 Release:

$ helm uninstall my-nginx
release "my-nginx" uninstalled

参考

  1. Helm | 快速入门指南
  2. Helm | 使用Helm
  3. Helm | 项目历史
  4. 微软 Deis Labs 的传奇故事
  5. 使用Helm管理kubernetes应用

更多

其他 Helm 命令

除了本文介绍的 Helm 三大概念以及围绕这三大概念的常用命令,也还有一些其他的命令:

制作自己的 Helm Chart

可视化管理工具

扫描二维码,在手机上阅读!

APISIX 使用小记

Apache APISIX 是基于 Nginx/OpenResty + Lua 方案打造的一款 动态实时高性能云原生 API 网关,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。APISIX 由国内初创公司 支流科技 于 2019 年 6 月开源,并于 7 月纳入 CNCF 全景图,10 月进入 Apache 孵化器,次年 7 月 毕业,成为国内唯一一个由初创公司贡献的项目,也是中国最快毕业的 Apache 顶级项目。

入门示例初体验

学习一门技术最好的方法就是使用它。这一节,我们将通过官方的入门示例,对 APISIX 的概念和用法有个基本了解。

首先,我们下载 apisix-docker 仓库:

git clone https://github.com/apache/apisix-docker.git

这个仓库主要是用来指导用户如何使用 Docker 部署 APISIX 的,其中有一个 example 目录,是官方提供的入门示例,我们可以直接使用 docker-compose 运行它:

$ cd apisix-docker/example
$ docker-compose up -d
[+] Running 8/8
 - Network example_apisix                Created                         0.9s
 - Container example-web2-1              Started                         5.1s
 - Container example-web1-1              Started                         4.0s
 - Container example-prometheus-1        Started                         4.4s
 - Container example-grafana-1           Started                         5.8s
 - Container example-apisix-dashboard-1  Started                         6.0s
 - Container example-etcd-1              Started                         5.1s
 - Container example-apisix-1            Started                         7.5s

可以看到创建了一个名为 example_apisix 的网络,并在这个网络里启动了 7 个容器:

  • etcd - APISIX 使用 etcd 作为配置中心,它通过监听 etcd 的变化来实时更新路由
  • apisix - APISIX 网关
  • apisix-dashboard - APISIX 管理控制台,可以在这里对 APISIX 的 Route、Upstream、Service、Consumer、Plugin、SSL 等进行管理
  • prometheus - 这个例子使用了 APISIX 的 prometheus 插件,用于暴露 APISIX 的指标,Prometheus 服务用于采集这些指标
  • grafana - Grafana 面板以图形化的方式展示 Prometheus 指标
  • web1 - 测试服务
  • web2 - 测试服务

部署之后可以使用 APISIX 的 Admin API 检查其是否启动成功:

$ curl http://127.0.0.1:9180/apisix/admin/routes \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
{"list":[],"total":0}

目前我们还没有创建任何路由,所以 /apisix/admin/routes 接口返回的结果为空。我们可以使用 Admin API 和 Dashboard 两种方式来创建路由。

使用 Admin API 创建路由

路由( Route ) 是 APISIX 中最基础和最核心的资源对象,APISIX 通过路由定义规则来匹配客户端请求,根据匹配结果加载并执行相应的插件,最后将请求转发到指定的上游服务。一条路由主要包含三部分信息:

  • 匹配规则:比如 methodsurihost 等,也可以根据需要自定义匹配规则,当请求满足匹配规则时,才会执行后续的插件,并转发到指定的上游服务;
  • 插件配置:这是可选的,但也是 APISIX 最强大的功能之一,APISIX 提供了非常丰富的插件来实现各种不同的访问策略,比如认证授权、安全、限流限速、可观测性等;
  • 上游信息:路由会根据配置的负载均衡信息,将请求按照规则转发到相应的上游。

所有的 Admin API 都采用了 Restful 风格,路由资源的请求地址为 /apisix/admin/routes/{id},我们可以通过不同的 HTTP 方法来查询、新增、编辑或删除路由资源(官方示例):

  • GET /apisix/admin/routes - 获取资源列表;
  • GET /apisix/admin/routes/{id} - 获取资源;
  • PUT /apisix/admin/routes/{id} - 根据 id 创建资源;
  • POST /apisix/admin/routes - 创建资源,id 将会自动生成;
  • DELETE /apisix/admin/routes/{id} - 删除指定资源;
  • PATCH /apisix/admin/routes/{id} - 标准 PATCH,修改指定 Route 的部分属性,其他不涉及的属性会原样保留;
  • PATCH /apisix/admin/routes/{id}/{path} - SubPath PATCH,通过 {path} 指定 Route 要更新的属性,全量更新该属性的数据,其他不涉及的属性会原样保留。

下面的例子将入门示例中的 web1 服务添加到路由中:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web1",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "web1:80": 1
        }
    }
}'

其中 X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 是 Admin API 的访问 Token,可以在 APISIX 的配置文件 apisix_conf/config.yaml 中找到:

deployment:
  admin:
    allow_admin:
      - 0.0.0.0/0
    admin_key:
      - name: "admin"
        key: edd1c9f034335f136f87ad84b625c8f1
        role: admin
      - name: "viewer"
        key: 4054f7cf07e344346cd3f287985e76a2
        role: viewer

如果路由创建成功,将返回下面的 201 Created 信息:

HTTP/1.1 201 Created
Connection: close
Transfer-Encoding: chunked
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: *
Access-Control-Max-Age: 3600
Content-Type: application/json
Date: Tue, 31 Jan 2023 00:19:03 GMT
Server: APISIX/3.1.0
X-Api-Version: v3

{"key":"\/apisix\/routes\/1","value":{"create_time":1675124057,"uri":"\/web1","status":1,"upstream":{"pass_host":"pass","scheme":"http","nodes":{"web1:80":1},"hash_on":"vars","type":"roundrobin"},"priority":0,"update_time":1675124057,"id":"1"}}

这个路由的含义是当请求的方法是 GET 且请求的路径是 /web1 时,APISIX 就将请求转发到上游服务 web1:80。我们可以通过这个路径来访问 web1 服务:

$ curl http://127.0.0.1:9080/web1
hello web1

如果上游信息需要在不同的路由中复用,我们可以先创建一个 上游(Upstream)

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/upstreams/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "type": "roundrobin",
    "nodes": {
        "web1:80": 1
    }
}'

然后在创建路由时直接使用 upstream_id 即可:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web1",
    "upstream_id": "1"
}'

另外,你可以使用下面的命令删除一条路由:

$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'

使用 Dashboard 创建路由

APISIX 提供了一套图形化 Dashboard 用来对网关的路由、插件、上游等进行管理,在入门示例中已经自带部署了 Dashboard,通过浏览器 http://localhost:9000 即可访问:

dashboard-login.png

默认的用户名和密码可以在 dashboard_conf/conf.yaml 文件中进行配置:

authentication:
  secret:
    secret     
  expire_time: 3600
  users:
    - username: admin
      password: admin
    - username: user
      password: user

登录成功后进入路由页面:

dashboard-routes.png

然后点击 “创建” 按钮创建一个路由:

dashboard-create-route.png

看上去这里的路由信息非常复杂,但是实际上我们只需要填写 名称路径HTTP 方法 即可,其他的维持默认值,当我们对 APISIX 的路由理解更深刻的时候可以再回过头来看看这些参数。

点击 “下一步” 设置上游信息:

dashboard-create-route-2.png

同样的,我们只关心目标节点的 主机名端口 两个参数即可。

然后再点击 “下一步” 进入插件配置,这里暂时先跳过,直接 “下一步” 完成路由的创建。路由创建完成后,访问 /web2 来验证路由是否生效:

$ curl http://127.0.0.1:9080/web2
hello web2

使用 APISIX 插件

通过上面的示例,我们了解了 APISIX 的基本用法,学习了如何通过 Admin API 或 Dashboard 来创建路由,实现了网关最基础的路由转发功能。APISIX 不仅具有高性能且低延迟的特性,而且它强大的插件机制为其高扩展性提供了无限可能。我们可以在 APISIX 插件中心 查看所有官方已经支持的插件,也可以 使用 lua 语言开发自己的插件,如果你对 lua 不熟悉,还可以使用其他语言 开发 External Plugin,APISIX 支持通过 Plugin Runner 以 sidecar 的形式来运行你的插件,APISIX 和 sidecar 之间通过 RPC 通信,不过这种方式对性能有一定的影响,如果你比较关注性能问题,那么可以使用你熟悉的语言开发 WebAssembly 程序,APISIX 也支持 运行 wasm 插件

external-plugin.png

这一节我们将通过几个官方插件来实现一些常见的网关需求。

在上面的学习中我们知道,一个路由是由匹配规则、插件配置和上游信息三个部分组成的,但是为了学习的递进性,我们有意地避免了插件配置部分。现在我们可以重新创建一个路由,并为其加上插件信息:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/3 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web3",
    "plugins": {
        "limit-count": {
            "count": 2,
            "time_window": 60,
            "rejected_code": 503,
            "key": "remote_addr"
        },
        "prometheus": {}
    },
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "web1:80": 1
        }
    }
}'

上面的命令创建了一个 /web3 路由,并配置了两个插件:

  • limit-count - 该插件使用 固定窗口算法(Fixed Window algorithm) 对该路由进行限流,每分钟仅允许 2 次请求,超出时返回 503 错误码;
  • prometheus - 该插件将路由请求相关的指标暴露到 Prometheus 端点;

我们连续访问 3 次 /web3 路由:

$ curl http://127.0.0.1:9080/web3
hello web1
$ curl http://127.0.0.1:9080/web3
hello web1
$ curl http://127.0.0.1:9080/web3
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>openresty</center>
<p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body>
</html>

可以看到 limit-count 插件的限流功能生效了,第 3 次请求被拒绝,返回了 503 错误码。另外,可以使用下面的命令查看 Prometheus 指标:

$ curl -i http://127.0.0.1:9091/apisix/prometheus/metrics

这个 Prometheus 指标地址可以在 apisix_conf/config.yaml 文件的 plugin_attr 中配置:

plugin_attr:
  prometheus:
    export_uri: /apisix/prometheus/metrics
    export_addr:
      ip: "0.0.0.0"
      port: 9091

APISIX 的插件可以动态的启用和禁用、自定义错误响应、自定义优先级、根据条件动态执行,具体内容可以参考 官方的 Plugin 文档。此外,如果一个插件需要在多个地方复用,我们也可以创建一个 Plugin Config

$ curl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "desc": "enable limit-count plugin",
    "plugins": {
        "limit-count": {
            "count": 2,
            "time_window": 60,
            "rejected_code": 503
        }
    }
}'

然后在创建路由时,通过 plugin_config_id 关联:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web1",
    "upstream_id": "1",
    "plugin_config_id": "1"
}'

在 APISIX 的插件中心,我们可以看到 APISIX 将插件分成了下面几个大类:

  • General - 通用功能,比如 gzip 压缩配置、重定向配置等;
  • Transformation - 这类插件会对请求做一些转换操作,比如重写请求响应、gRPC 协议转换等;
  • Authentication - 提供一些常见的认证授权相关的功能,比如 API Key 认证、JWT 认证、Basic 认证、CAS 认证、LDAP 认证等;
  • Security - 安全相关的插件,比如开启 IP 黑白名单、开启 CORS、开启 CSRF 等;
  • Traffic - 这些插件对流量进行管理,比如限流、限速、流量镜像等;
  • Observability - 可观测性插件,支持常见的日志(比如 File-Logger、Http-Logger、Kafka-Logger、Rocketmq-Logger 等)、指标(比如 Prometheus、Datadog 等)和链路跟踪(比如 Skywalking、Zipkin、Opentelemetry 等)系统;
  • Serverless - 对接常见的 Serverless 平台,实现函数计算功能,比如 AWS Lambda、Apache OpenWhisk、CNCF Function 等;
  • Other Protocols - 这些插件用于支持 Dubbo、MQTT 等其他类型的协议;

参考

  1. 快速入门指南 | Apache APISIX® -- Cloud-Native API Gateway
  2. API 网关策略的二三事
  3. 从 Apache APISIX 来看 API 网关的演进
  4. 云原生时代的中外 API 网关之争

更多

APISIX 的部署模式

APISIX 支持多种不同的 部署模式,上面的示例中使用的是最常用的一种部署模式:traditional 模式,在这个模式下 APISIX 的控制平台和数据平面在一起:

deployment-traditional.png

我们也可以将 APISIX 部署两个实例,一个作为数据平面,一个作为控制平面,这被称为 decoupled 模式,这样可以提高 APISIX 的稳定性:

deployment-decoupled.png

上面两种模式都依赖于从 etcd 中监听和获取配置信息,如果我们不想使用 etcd,我们还可以将 APISIX 部署成 standalone 模式,这个模式使用 conf/apisix.yaml 作为配置文件,并且每间隔一段时间自动检测文件内容是否有更新,如果有更新则重新加载配置。不过这个模式只能作为数据平面,无法使用 Admin API 等管理功能(这是因为 Admin API 是基于 etcd 实现的):

deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml

将 APISIX 扩展为服务网格的边车

集成服务发现注册中心

扫描二维码,在手机上阅读!