
01 Zun 服务简介
Zun 是 OpenStack 的容器服务(Containers as Service),类似于 AWS 的 ECS 服务,但实现原理不太一样,ECS 是把容器启动在 EC2 虚拟机实例上,而 Zun 会把容器直接运行在 compute 节点上。
和 OpenStack 另一个容器相关的 Magnum 项目不一样的是:Magnum 提供的是容器编排服务,能够提供弹性 Kubernetes、Swarm、Mesos 等容器基础设施服务,管理的单元是 Kubernetes、Swarm、Mesos 集群,而 Zun 提供的是原生容器服务,支持不同的 runtime 如 Docker、Clear Container 等,管理的单元是 container。
Zun 服务的架构如图:

Zun 服务和 Nova 服务的功能和结构非常相似,只是前者提供容器服务,后者提供虚拟机服务,二者都是主流的计算服务交付模式。功能类似体现在如下几点:
通过 Neutron 提供网络服务。
通过 Cinder 实现数据的持久化存储。
都支持使用 Glance 存储镜像。
其他如 quota、安全组等功能。
组件结构结构相似则表现在:
二者都是由 API、调度、计算三大组件模块构成,Nova 由 nova-api、nova-scheduler、nova-compute 三大核心组件构成,而 Zun 由 zun-api、zun-compute 两大核心组件构成,之所以没有 zun-scheduler 是因为 scheduler 集成到 zun-api 中了。
nova-compute 调用 compute driver 创建虚拟机,如 Libvirt。zun-compute 调用 container driver 创建容器,如 Docker。
Nova 通过一系列的 proxy 代理实现 VNC(nova-novncproxy)、Splice(nova-spiceproxy)等虚拟终端访问,Zun 也是通过 proxy 代理容器的 websocket 实现远程 attach 容器功能。
02 Zun 服务部署
Zun 服务部署和 Nova、Cinder 部署模式类似,控制节点创建数据库、Keystone 创建 service 以及注册 endpoints 等,最后安装相关包以及初始化配置。计算节点除了安装 zun-compute 服务,还需要安装要使用的容器,比如 Docker。详细的安装过程可以参考官方文档,如果仅仅是想进行 POC 测试,可以通过 DevStack 自动化快速部署一个 AllInOne 环境,供参考的 local.conf 配置文件如下:

如上配置会自动通过 DevStack 安装 Zun 相关组件、Kuryr 组件以及 Docker。
03 Zun 服务入门
3.1 Dashboard
安装 Zun 服务之后,可以通过 zun 命令行以及 Dashboard 创建和管理容器。
有一个非常赞的功能是如果安装了 Zun,Dashboard 能够支持 Cloud Shell,用户能够在 DashBoard 中进行交互式输入 OpenStack 命令行。

原理的话就是通过 Zun 启动了一个 gbraad/openstack-client:alpine 容器。
通过 Dashboard 创建容器和创建虚拟机的过程非常相似,都是通过 panel 依次选择镜像(image)、选择规格(Spec)、选择或者创建卷(volume)、选择网络(network/port)、选择安全组(SecuiryGroup)以及 scheduler hint,如图:

其中 Miscellaneous 杂项中则为针对容器的特殊配置,比如设置环境变量(Environment)、工作目录(Working Directory)等。
3.2 命令行操作
通过命令行创建容器也非常类似,使用过 nova 以及 docker 命令行的基本不会有困难,下面以创建一个 mysql 容器为例:

如上通过–mount 参数指定了 volume 大小,由于没有指定 volume_id,因此 Zun 会新创建一个 volume。需要注意的是,Zun 创建的 volume 在容器删除后,volume 也会自动删除(auto remove),如果需要持久化 volume 卷,则应该先通过 Cinder 创建一个 volume,然后通过 source 选项指定 volume_id,此时当容器删除时不会删除已有的 volume 卷。
和虚拟机不一样,虚拟机通过 flavor 配置规格,容器则直接指定 cpu、memory、disk。
如上没有指定–image-driver 参数,则默认从 dockerhub 下载镜像,如果指定 glance,则会往 glance 下载镜像。
另外 mysql 容器初始化时数据卷必须为空目录,挂载的 volume 新卷格式化时会自动创建 lost+found 目录,因此需要手动删除,否则 mysql 容器会初始化失败:

创建完成后可以通过 zun list 命令查看容器列表:

可以看到 mysql 的容器 fixed IP 为 192.168.233.80,和虚拟机一样,租户 IP 默认与外面不通,需要绑定一个浮动 IP(floating ip),

zun 命令行目前还无法查看 floating ip,只能通过 neutron 命令查看,获取到 floatingip 并且安全组入访允许 3306 端口后就可以远程连接 mysql 服务了:

当然在同一租户的虚拟机也可以直接通过 fixed ip 访问 mysql 服务:

可见,通过容器启动 mysql 服务和在虚拟机里面部署 mysql 服务,用户访问上没有什么区别,在同一个环境中,虚拟机和容器可共存,彼此可相互通信,在应用层上可以完全把虚拟机和容器透明化使用,底层通过应用场景选择虚拟机或者容器。
3.3 关于 capsule
Zun 除了管理容器 container 外,还引入了 capsule 的概念,capsule 类似 Kubernetes 的 pod,一个 capsule 可包含多个 container,这些 container 共享 network、ipc、pid namespace 等。
通过 capsule 启动一个 mysql 服务,声明 yaml 文件如下:

创建 mysql capsule:

可见 capsule 的 init container 用的就是 kubernetes 的 pause 镜像。
3.4 总结
OpenStack 的容器服务本来是在 Nova 中实现的,实现了 Nova ComputeDriver,因此 Zun 的其他的功能如容器生命周期管理、image 管理、service 管理、action 管理等和 Nova 虚拟机非常类似,可以查看官方文档,这里不再赘述。
04 Zun 实现原理
4.1 调用容器接口实现容器生命周期管理
前面提到过 Zun 主要由 zun-api 和 zun-compute 服务组成,zun-api 主要负责接收用户请求、参数校验、资源准备等工作,而 zun-compute 则真正负责容器的管理,Nova 的后端通过 compute_driver 配置,而 Zun 的后端则通过 container_driver 配置,目前只实现了 DockerDriver。因此调用 Zun 创建容器,最终就是 zun-compute 调用 docker 创建容器。
下面以创建一个 container 为例,简述其过程。
4.1.1 zun-api
首先入口为 zun-api,主要代码实现在 zun/api/controllers/v1/containers.py 以及 zun/compute/api.py,创建容器的方法入口为 post()方法,其调用过程如下:
zun/api/controllers/v1/containers.py
policy enforce: 检查 policy,验证用户是否具有创建 container 权限的 API 调用。
check security group: 检查安全组是否存在,根据传递的名称返回安全组的 ID。
check container quotas: 检查 quota 配额。
build requested network: 检查网络配置,比如 port 是否存在、network id 是否合法,最后构建内部的 network 对象模型字典。注意,这一步只检查并没有创建 port。
create container object:根据传递的参数,构造 container 对象模型。
build requeted volumes: 检查 volume 配置,如果传递的是 volume id,则检查该 volume 是否存在,如果没有传递 volume id 只指定了 size,则调用 Cinder API 创建新的 volume。
zun/compute/api.py
schedule container: 使用 FilterScheduler 调度 container,返回宿主机的 host 对象。这个和 nova-scheduler 非常类似,只是 Zun 集成到 zun-api 中了。目前支持的 filters 如 CPUFilter、RamFilter、LabelFilter、ComputeFilter、RuntimeFilter 等。
image validation: 检查镜像是否存在,这里会远程调用 zun-compute 的 image_search 方法,其实就是调用 docker search。这里主要为了实现快速失败,避免到了 compute 节点才发现 image 不合法。
record action: 和 Nova 的 record action 一样,记录 container 的操作日志。
rpc cast container_create: 远程异步调用 zun-compute 的 container_create()方法,zun-api 任务结束。
4.1.2 zun-compute
zun-compute 负责 container 创建,代码位于 zun/compute/manager.py,过程如下:
wait for volumes avaiable: 等待 volume 创建完成,状态变为 avaiable。
attach volumes:挂载 volumes,挂载过程后面再介绍。
checksupportdisk_quota: 如果使用本地盘,检查本地的 quota 配额。
pull or load image: 调用 Docker 拉取或者加载镜像。
创建 docker network、创建 neutron port,这个步骤下面详细介绍。
create container: 调用 Docker 创建容器。
container start: 调用 Docker 启动容器。
以上调用 Dokcer 拉取镜像、创建容器、启动容器的代码位于 zun/container/docker/driver.py,该模块基本就是对社区 Docker SDK for Python 的封装。

Zun 的其他操作比如 start、stop、kill 等实现原理也类似,这里不再赘述。
4.2 通过 websocket 实现远程容器访问
我们知道虚拟机可以通过 VNC 远程登录,物理服务器可以通过 SOL(IPMI Serial Over LAN)实现远程访问,容器则可以通过 websocket 接口实现远程交互访问。
Docker 原生支持 websocket 连接,参考 APIAttach to a container via a websocket,websocket 地址为/containers/{id}/attach/ws,不过只能在计算节点访问,那如何通过 API 访问呢?
和 Nova、Ironic 实现完全一样,也是通过 proxy 代理转发实现的,负责 container 的 websocket 转发的进程为 zun-wsproxy。
当调用 zun-compute 的 container_attach()方法时,zun-compute 会把 container 的 websocket_url 以及 websocket_token 保存到数据库中.

zun-wsproxy 则可读取 container 的 websocket_url 作为目标端进行转发:

通过 Dashboard 可以远程访问 container 的 shell:

当然通过命令行 zun attach 也可以 attach container。
4.3 使用 Cinder 实现容器持久化存储
前面介绍过 Zun 通过 Cinder 实现 container 的持久化存储,之前我的另一篇文章介绍了 Docker 使用 OpenStack Cinder 持久化 volume 原理分析及实践,介绍了 john griffith 开发的 docker-cinder-driver 以及 OpenStack Fuxi 项目,这两个项目都实现了 Cinder volume 挂载到 Docker 容器中。另外 cinderclient 的扩展模块 python-brick-cinderclient-ext 实现了 Cinder volume 的 local attach,即把 Cinder volume 挂载到物理机中。
Zun 没有复用以上的代码模块,而是重新实现了 volume attach 的功能,不过实现原理和上面的方法完全一样,主要包含如下过程:
connect volume: connect volume 就是把 volume attach(映射)到 container 所在的宿主机上,建立连接的的协议通过 initialize_connection 信息获取,如果是 LVM 类型则一般通过 iscsi,如果是 Ceph rbd 则直接使用 rbd map。
ensure mountpoit tree: 检查挂载点路径是否存在,如果不存在则调用 mkdir 创建目录。
make filesystem:如果是新的 volume,挂载时由于没有文件系统因此会失败,此时会创建文件系统。
do mount: 一切准备就绪,调用 OS 的 mount 接口挂载 volume 到指定的目录点上。
Cinder Driver 的代码位于`zun/volume/driver.py 的 Cinder 类中,方法如下:

其中 cinder.attach_volume()实现如上的第 1 步,而_mount_device()实现了如上的 2-4 步。
4.4 集成 Neutron 网络实现容器网络多租户
4.4.1 关于容器网络
前面我们通过 Zun 创建容器,使用的就是 Neutron 网络,意味着容器和虚拟机完全等同的共享 Neutron 网络服务,虚拟机网络具有的功能,容器也能实现,比如多租户隔离、floating ip、安全组、防火墙等。
Docker 如何与 Neutron 网络集成呢?根据官方 Docker network plugin API 介绍,插件位于如下目录:
/run/docker/plugins
/etc/docker/plugins
/usr/lib/docker/plugins

由此可见 Docker 使用的是 kuryr 网络插件。
Kuryr 也是 OpenStack 中一个较新的项目,其目标是“Bridge between container framework networking and storage models to OpenStack networking and storage abstractions.”,即实现容器与 OpenStack 的网络与存储集成,当然目前只实现了网络部分的集成。
而我们知道目前容器网络主要有两个主流实现模型:
CNM:Docker 公司提出,Docker 原生使用的该方案,通过 HTTP 请求调用,模型设计可参考 The Container Network Model Design,network 插件可实现两个 Driver,其中一个为 IPAM Driver,用于实现 IP 地址管理,另一个为 Docker Remote Drivers,实现网络相关的配置。
CNI:CoreOS 公司提出,Kubernetes 选择了该方案,通过本地方法或者命令行调用。
因此 Kuryr 也分成两个子项目,kuryr-network 实现 CNM 接口,主要为支持原生的 Docker,而 kury-kubernetes 则实现的是 CNI 接口,主要为支持 Kubernetes,Kubernetes service 还集成了 Neutron LBaaS,下次再单独介绍这个项目。
由于 Zun 使用的是原生的 Docker,因此使用的是 kuryr-network 项目,实现的是 CNM 接口,通过 remote driver 的形式注册到 Docker libnetwork 中,Docker 会自动向插件指定的 socket 地址发送 HTTP 请求进行网络操作,我们的环境是http://127.0.0.1:23750,即 kuryr-libnetwork.service 监听的地址,Remote API 接口可以参考 Docker Remote Drivers。
4.4.2 kuryr 实现原理
前面 4.1 节介绍到 zun-compute 会调用 docker driver 的 create()方法创建容器,其实这个方法不仅仅是调用 python docker sdk 的 create_container()方法,还做了很多工作,其中就包括网络相关的配置。
首先检查 Docker 的 network 是否存在,不存在就创建,network name 为 Neutron network 的 UUID,

然后会调用 Neutron 创建 port,从这里可以得出结论,容器的 port 不是 Docker libnetwork 也不是 Kuryr 创建的,而是 Zun 创建的。
回到前面的 Remote Driver,Docker libnetwork 会首先 POST 调用 kuryr 的/IpamDriver.RequestAddressAPI 请求分配 IP,但显然前面 Zun 已经创建好了 port,port 已经分配好了 IP,因此这个方法其实就是走走过场。如果直接调用 docker 命令指定 kuryr 网络创建容器,则会调用该方法从 Neutron 中创建一个 port。
接下来会 POST 调用 kuryr 的/NetworkDriver.CreateEndpoint 方法,这个方法最重要的步骤就是 binding,即把 port attach 到宿主机中,binding 操作单独分离出来为 kuryr.lib 库,这里我们使用的是 veth driver,因此由 kuryr/lib/binding/drivers/veth.py 模块的 port_bind()方法实现,该方法创建一个 veth 对,其中一个为 tap-xxxx,xxxx 为 port ID 前缀,放在宿主机的 namespace,另一个为 t_cxxxx 放到容器的 namespace,t_cxxxx 会配置上 IP,而 tap-xxxx 则调用 shell 脚本(脚本位于/usr/local/libexec/kuryr/)把 tap 设备添加到 ovs br-int 桥上,如果使用 HYBRID_PLUG,即安全组通过 Linux Bridge 实现而不是 OVS,则会创建 qbr-xxx,并创建一个 veth 对关联到 ovs br-int 上。
从这里可以看出,Neutron port 绑定到虚拟机和容器基本没有什么区别,如下所示:

唯一不同的就是虚拟机是把 tap 设备直接映射到虚拟机的虚拟设备中,而容器则通过 veth 对,把另一个 tap 放到容器的 namespace 中。
有人会说,br-int 的流表在哪里更新了?这其实是和虚拟机是完全一样的,当调用 port update 操作时,neutron server 会发送 RPC 到 L2 agent 中(如 neutron-openvswitch-agent),agent 会根据 port 的状态更新对应的 tap 设备以及流表。
因此其实 kuryr 只干了一件事,那就是把 Zun 申请的 port 绑定到容器中。
05 总结
OpenStack Zun 项目非常完美地实现了容器与 Neutron、Cinder 的集成,加上 Ironic 裸机服务,OpenStack 实现了容器、虚拟机、裸机共享网络与存储。未来我觉得很长一段时间内裸机、虚拟机和容器将在数据中心混合存在,OpenStack 实现了容器和虚拟机、裸机的完全平等、资源共享以及功能对齐,应用可以根据自己的需求选择容器、虚拟机或者裸机,使用上没有什么区别,用户只需要关心业务针对性能的需求以及对硬件的特殊访问,对负载(workload)是完全透明的。
参考文献
docker python sdk: https://docker-py.readthedocs.io/en/stable/
Zun’s documentation: https://docs.openstack.org/zun/latest/
https://docs.docker.com/engine/api/v1.39/#operation/ContainerAttachWebsocket
http://int32bit.me/2017/10/04/Docker使用OpenStack-Cinder持久化volume原理分析及实践/
https://specs.openstack.org/openstack/cinder-specs/specs/mitaka/use-cinder-without-nova.html
https://github.com/docker/libnetwork/blob/master/docs/design.md
https://github.com/docker/libnetwork/blob/master/docs/ipam.md
https://github.com/docker/libnetwork/blob/master/docs/remote.md
https://www.nuagenetworks.net/blog/container-networking-standards/
http://blog.kubernetes.io/2016/01/why-Kubernetes-doesnt-use-libnetwork.html
本文转载自公众号 int32bit(ID:int32bit)。
原文链接:
https://mp.weixin.qq.com/s/aJsTSaCjoO2Fq_nqCkz0ZQ
评论