PaaS 平台的网络需求

在使用 Docker 构建 PaaS 平台的过程中,我们首先遇到的问题是需要选择一个满足需求的网络模型:

  • 让每个容器拥有自己的网络栈,特别是独立的 IP 地址
  • 能够进行跨服务器的容器间通讯,同时不依赖特定的网络设备
  • 有访问控制机制,不同应用之间互相隔离,有调用关系的能够通讯

调研了几个主流的网络模型:

  • Docker 原生的 Bridge 模型:NAT 机制导致无法使用容器 IP 进行跨服务器通讯(后来发现自定义网桥可以解决通讯问题,但是觉得方案比较复杂)
  • Docker 原生的 Host 模型:大家都使用和服务器相同的 IP,端口冲突问题很麻烦
  • Weave OVS 等基于隧道的模型:由于是基于隧道的技术,在用户态进行封包解包,性能折损比较大,同时出现问题时网络抓包调试会很蛋疼

在对上述模型都不怎么满意的情况下,发现了一个还不怎么被大家关注的新项目:Project Calico 。

Project Calico 是纯三层的 SDN 实现,它基于 BPG 协议和 Linux 自己的路由转发机制,不依赖特殊硬件,没有使用 NAT 或 Tunnel 等技术。能够方便的部署在物理服务器,虚拟机(如 OpenStack)或者容器环境下。同时它自带的基于 Iptables 的 ACL 管理组件非常灵活,能够满足比较复杂的安全隔离需求。

使用 Calico 来实现 Docker 的跨服务器通讯

 

环境准备

  • 两个 Linux 环境 node1|2(物理机,VM 均可),假定 IP 为:192.168.78.21|22
  • 为了简单,请将 node1|2 上的 Iptables INPUT 策略设为 ACCEPT,同时安装 Docker
  • 一个可访问的 Etcd 集群(192.168.78.21:2379),Calico 使用其进行数据存放和节点发现

启动 Calico

在 node1|2 上面下载控制脚本:

# wget https://github.com/projectcalico/calico-docker/releases/download/v0.4.9/calicoctl

启动

# export ETCD_AUTHORITY=192.168.78.21:2379
# ./calicoctl node --ip=192.168.78.21|22

docker ps 能看到:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
74cc20b90b0f calico/node:v0.4.9 "/sbin/my_init" 24 seconds ago Up 23 seconds calico-node 

部署测试实例

在 Calico 中,有一个 Profile 的概念(类似 AWS 的 Security Group),位于同一个 Profile 中的实例才能互相通讯,所以我们先创建一个名为 db 的 Profile:

在 node1 上执行:

[node1]# ./calicoctl profile add db

然后启动测试实例:

[node1]# export DOCKER_HOST=localhost:2377
[node1]# docker run -n container1 -e CALICO_IP=auto -e CALICO_PROFILE=db -td ubuntu

这里大家注意,我们注入了两个环境变量:CALICO_IP 和 CALICO_PROFILE 。

前者告诉 CALICO 自动进行 IP 分配,后者将此容器加入到 Profile db 中。

那么 Calico 是怎么做到在容器启动的时候分配 IP 的呢?

大家注意我们在 run 一个容器前,先执行了一个 export,这里其实就是将 Docker API 的入口劫持到了 Calico 那里。Calico 内部是一个 twistd 实现的 Python Daemon,转发所有 Docker 的 API 请求给真正的 Docker 服务,如果发现是start 则插入自己的逻辑创建容器的网络栈。

容器启动后我们查看 container1 获取的 IP 地址:

[container1]# ip addr
...
8: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 1e:48:3e:ec:71:52 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.1/32 scope global eth1
valid_lft forever preferred_lft forever

我们会看到 eth1 这个网络接口被设置了 IP 192.168.0.1

同样在 node2 上面部署 container2。

默认设置下 IP 会在 192.168.0.0/16 中按顺序分配,所以 container2 会是192.168.0.2

然后我们就会发现 container1|2 能够互相 ping 通了!

路由实现

接下来让我们看一下在上面的 demo 中,Calico 是如何让不在一个节点上的两个容器互相通讯的:

  • Calico 节点启动后会查询 Etcd,和其他 Calico 节点使用 BGP 协议建立连接
[node1]# netstat -anpt | grep 179
tcp 0 0 0.0.0.0:179 0.0.0.0:* LISTEN 21887/bird
tcp 0 0 192.168.78.21:46427 192.168.78.22:179 ESTABLISHED 21887/bird
  • 容器启动时,劫持相关 Docker API,进行网络初始化
    • 如果没有指定 IP,则查询 Etcd 自动分配一个可用 IP
    • 创建一对 veth 接口用于容器和主机间通讯,设置好容器内的 IP 后,打开 IP 转发
    • 在主机路由表添加指向此接口的路由
主机上:
[node1]# ip link show
...
7: cali2466cece7bc: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 96:c4:86:4d:d7:2c brd ff:ff:ff:ff:ff:ff
容器内:
[container1]# ip addr
...
8: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 1e:48:3e:ec:71:52 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.1/32 scope global eth1
valid_lft forever preferred_lft forever
主机路由表:
[node1]# ip route
...
192.168.0.1 dev cali2466cece7bc scope link
  • 然后将此路由通过 BGP 协议广播给其他所有节点,在两个节点上的路由表最终是这样的:
[node1]# ip route
...
192.168.0.1 dev cali2466cece7bc scope link
192.168.0.2 via 192.168.78.22 dev enp0s8 proto bird
[node2]# ip route
...
192.168.0.1 via 192.168.78.21 dev enp0s8 proto bird
192.168.0.2 dev caliea3aaf5a7be scope link

大家看这个路由,node2 上面的 container2 要访问 container1(192.168.0.1),通过查路由表得知需要将包转给 192.168.78.21,也就是 node1。形象的展示数据流向是这样的:

container2[eth1] -> node2[caliea3aaf5a7be] -> route -> node1[cali2466cece7bc] -> container1[eth1]

至此,跨节点通讯打通,整个流程没有任何 NAT,Tunnel 封包。所以只要三层可达的环境,就可以应用 Calico。

利用 Profile 实现 ACL

在之前的 demo 中我们提到了 Profile,Calico 每个 Profile 都自带一个规则集,用于对 ACL 进行精细控制,如刚刚的db 的默认规则集是:

[node1]# ./calicoctl profile db rule json
{
"id": "db",
"inbound_rules": [
{
"action": "allow",
"src_tag": "db"
}
],
"outbound_rules": [
{
"action": "allow"
}
]
}

这个规则集表示入连接只允许来自 Profile 名字是 db 的实例,出连接不限制,最后隐含了一条默认策略是不匹配的全部 drop,所以同时位于不同 Profile 的实例互相是不能通讯的,这就解决了隔离的需求。

下面是一个更复杂的例子:

在常见的网站架构中,一般是前端 WebServer 将请求反向代理给后端的 APP 服务,服务调用后端的 DB:

WEB -> APP -> DB

所以我们要实现:

  • WEB 暴露 80 和 443 端口
  • APP 允许 WEB 访问
  • DB 允许 APP 访问 3306 端口
  • 除此之外,禁止所有跨服务访问

那么我们就可以如此构建 json:

对于 WEB

[node1]# cat web-rule.json
{
"id": "web",
"inbound_rules": [
{
"action": "allow",
"src_tag": "web"
},
{
"action": "allow",
"protocol": "tcp",
"dst_ports": [
80,
443
]
}
],
"outbound_rules": [
{
"action": "allow"
}
]
}
[node1]# ./calicoctl profile web rule update < web-rule.json

入站规则我们增加了一条允许 80 443

对于 APP

[node1]# cat app-rule.json
{
"id": "app",
"inbound_rules": [
{
"action": "allow",
"src_tag": "app"
},
{
"action": "allow",
"src_tag": "web"
}
],
"outbound_rules": [
{
"action": "allow"
}
]
}
[node1]# ./calicoctl profile app rule update < app-rule.json

对于后端服务,我们只允许来自 web 的连接。

对于 DB,我们在只允许 APP 访问的基础上还限制了只能连接 3306。

[node1]# cat db-rule.json
{
"id": "db",
"inbound_rules": [
{
"action": "allow",
"src_tag": "db"
},
{
"action": "allow",
"src_tag": "APP",
"protocol": "tcp",
"dst_ports": [
3306
]
}
],
"outbound_rules": [
{
"action": "allow"
}
]
}
[node1]# ./calicoctl profile db rule update < db-rule.json

很简单的几条规则,我们就实现了上述需求。

Profile 高级特性:Tag

有同学可能说,在现实环境中,会有多组不同的 APP 都需要访问 DB,如果每个 APP 都在 db 中增加一条规则也很麻烦同时还容易出错。

这里我们可以利用Profile 的高级特性 Tag 来简化操作:

  • 每个 Profile 默认拥有一个和 Profile 名字相同的 Tag
  • 每个 Profile 可以有多个 Tag,以 List 形式保存

利用 Tag 我们可以将一条规则适配到指定的一组 Profile 上。

参照上面的例子,我们给所有需要访问 DB 的 APP 的 Profile 都加上 db-users这个 Tag:

[node1]# ./calicoctl profile app1 tag add db-users
[node1]# ./calicoctl profile app2 tag add db-users
[node1]# ./calicoctl profile app3 tag add db-users
...

然后修改 db-rule.json 为:

{
"id": "db",
"inbound_rules": [
{
"action": "allow",
"src_tag": "db"
},
{
"action": "allow",
"src_tag": "db-users",
"protocol": "tcp",
"dst_ports": [
3306
]
}
],
"outbound_rules": [
{
"action": "allow"
}
]
}

将之前的 src_tag: app 替换为 src_tag: db-users。这样所有打了 db-user 这个 Tag 的实例就都能访问数据库了。

Profile 的实现

Profile 的实现基于 Iptables 和 IPSet。我们以刚刚的 db 规则集中 inbound 部分为例:

Calico 在启动后会在 Iptables 中新建一些 Chain,数据包会在不同的 Chain 之间跳转,下面我截取了一些关键的规则列表:

[node1]# iptables -n -L -v
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
target prot in out source destination
felix-FORWARD all * * 0.0.0.0/0 0.0.0.0/0
Chain felix-FORWARD (1 references)
target prot in out source destination
felix-TO-ENDPOINT all * cali+ 0.0.0.0/0 0.0.0.0/0
Chain felix-TO-ENDPOINT (1 references)
target prot in out source destination
felix-to-2466cece7bc all * cali2466cece7bc 0.0.0.0/0 0.0.0.0/0 [goto]
Chain felix-to-2466cece7bc (1 references)
target prot in out source destination
felix-p-db-i all * * 0.0.0.0/0 0.0.0.0/0
Chain felix-p-db-i (2 references)
target prot in out source destination
RETURN all * * 0.0.0.0/0 0.0.0.0/0 match-set felix-v4-db src
RETURN tcp * * 0.0.0.0/0 0.0.0.0/0 match-set felix-v4-db-users src multiport dports 3306

这个略复杂,我们慢慢看。基本上数据包是从上到下一步步跳转的。

当发给 container1 的数据包到达 node1 后,由于目标 IP 192.168.0.1和 node1 自身 IP 不同,会被放入FORWARD 链,然后跳转到felix-FORWARD,通过查询路由表:

192.168.0.1 dev cali2466cece7bc scope link

得知下一跳接口为 cali2466cece7bc,于是先跳转到felix-TO-ENDPOINT,再跳转到 felix-to-2466cece7bc

在这里,定义了具体的 ACL 列表,felix-p-db-i,这个 db 是不是很眼熟?

对,就是这个 container 所属 Profile 的名字,而 felix-p-db-i 中就是 Profiledb 的 inbound 规则集。而 felix-p-db-i 的内容:

match-set felix-v4-db src
match-set felix-v4-db-users src multiport dports 3306

felix-v4-db 和 felix-v4-db-users 是不是也很熟悉?

在 db 规则集中的两个 Tag 在这里加了个前缀变成了 IPSet,它包括了所有打了这个 Tag 的 IP 列表:

[node1]# ipset list
...
Name: felix-v4-db
Type: hash:ip
Revision: 1
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 16576
References: 1
Members:
192.168.0.1
192.168.0.2

至此,ACL 部分分析完毕。可见 Calico 灵活运用了 Iptables 的种种高级特性。

性能测试

来自官方测试结果

测试环境

  • 8 core CPU
  • 64GiB RAM
  • 10Gb 网卡直连
  • Ubuntu 14.04.2 with 3.13 Kernel(3.10 版本的 Kernel 修复了一些 veth 性能的问题)
  • 没有额外的内核参数调优

测试了四种场景:

  • 物理服务器(基准)
  • 部署了 Calico 的容器之间
  • 部署了 Calico 的 OpenStack VM 之间
  • 部署了 OVS with VxLAN 的 OpenStack VM 之间

测试了两种数据大小:

  • 20000 byte
  • 500 byte

吞吐量 & CPU 使用率测试

吞吐量极限

同一时刻 CPU 的使用率

把它们合并成一张图就是每 Gb/s 的 CPU 使用率

可以看到,部署了 Calico 的两个场景都非常贴近物理服务器的性能。

延迟测试

测试方法是:节点间交换 1 byte 数据包

这个结果显示,Calico 容器非常接近物理服务器,而 OpenStack 场景由于网络虚拟化的缘故延迟稍大。

结论

测试结果表明,Calico 的性能非常接近物理服务器,比基于隧道的 OVS 性能好很多。

Calico 的发展

Calico 和 Docker 一样是很年轻的项目,但是坑比后者少多了,我遇到了一些,如docker inspect 没有显示 Calico 分配的 IP,BGP 客户端重启姿势不正确导致路由周期性消失重建等等。但是他们的开发进度非常快,一个 issue 提出来到修复可能就一两天时间(再次鄙视 Docker)。

目前唯一一个比较麻烦的问题是,Calico 这种劫持 Docker API 的方式,容器的网络栈是在容器启动后才进行初始化,所以在头几秒其实是没有网络可用的,这会导致那些启动就要访问网络的容器挂掉。解决方案有两个:

  • 升级 Docker 到支持 libnetwork 的版本,Calico 在新版本(>0.5)中支持了 libnetwork,理论上能够解决这个问题。但是代价要踩新版本 Docker 带来的更多的坑。
  • 自定义容器的 CMD,实现一个 entry 脚本,待网络可用后再 exec 载入真正的进程。

Q&A

Q:自定义容器的 CMD,实现一个 entry 脚本,待网络可用后再 exec 载入真正的进程。 有没有具体的?

主要实现方式就是 Dockerfile 中的 CMD 可以这个样子: /entry.sh your-cmd 。这个 entry.sh 中判断 IP 是否已经分配好,如果没有就 sleep 重试。分配好后再用 exec 载入后面的 your-cmd 。

Q:是否每增加一个容器宿主机就需要增加一条路由?如果容器数量很多会有问题吗?

是的。关于这个问题我咨询过开发团队,他们表示压测过单机 10 万条路由,没有问题。同时将来会推出路由段的广播机制,即:每台服务器使用一小段,之间只需要广播此段即可。

Q:如果要做容器间通讯限速,Calico能做吗?

由于每个由 Calico 管理的容器在宿主机上面都有一个唯一的网络接口(veth 的一端),通过限制此接口流量即可进行限速。Calico 官方没有提供这个功能,我们可以用常规的其他手段解决。同时这个接口还有一个好处就是非常容易做容器的流量监控,只要看接口计数器即可。

Q:Calico 和 Kubernetes 的整合你们尝试过吗?Calico只是接管了容器间的通信,和 Kubernetes 的 Service Cluster IP 没有关系吧?

我们没有使用 Kubernetes 的解决方案,而是自行开发的调度编排等组件。Cailco 官方是支持的并且有相关文档可以参考。具体请参考:https://github.com/projectcalico/calico-docker/blob/master/docs/kubernetes/README.md

原文链接

posted on 2016-05-25 12:14  生活费  阅读(1106)  评论(0编辑  收藏  举报

导航