郑昀编著,文字资料来自于张帆、白俊华、刘飞宇以及网络资料 创建于2015/10/21 最后更新于2015/10/29
关键词: Docker,容器,持续集成,持续发布,CI,私有云
提纲:
其次,一开始注定只有一少部分工程迁移到容器私有云上,既然还有大多数应用服务还在虚拟机或物理机上,那么它们之间如何通讯就成了一个必须解决的问题。
那么,我们在构建基于容器的私有云以及相应的持续发布时,遇到并解决了哪些问题呢?
先抛出问题,下面这个选择题你怎么选:
一艘鲸鱼大船,载着无数集装箱。操作系统就是这艘货轮,每一个容器就是一个集装箱,交付运行环境如同海运。大家知道吗,集装箱的英文单词就是 container!
集装箱有什么好处?
那么,上面的选择题如何回答呢?
触控科技运维负责人萧田国认为,把代码放在宿主机上,让容器通过卷组映射来读取,不建议采用这种方案,原因是,将代码拆分出容器,这违背了 Docker 的集装箱原则:
或者说,容器时代,一切版本化,抛弃过去文件分发的思想,才是正途,这样也才能实现真正意义上的容器级迁移。
我们是怎么考虑的呢?
假定是代码和 Image 分开的场景,主要有两种实现方式:
在容器云之前,我们采用的是基于 OpenStack 的虚拟机管理方案。在线下环境用 OpenStack 时,我们将所有的虚拟机放置到 ceph(注:Linux PB 级分布式文件系统) 上,但是由于线下 ceph 节点太少,单节点出问题时影响较大。每次 ceph 出现问题,所有的虚拟机都特别慢。
来到了容器时代,如果还把代码放置到 ceph 上,对 ceph 的依赖太高了,ceph 一旦有问题,有可能使用了 ceph 的所有容器都会出问题 。线上业务主要考虑的是性能(容器本地运行代码 vs 通过网络获取代码)和可靠性(每个 slave 都本地运行容器 vs 所有容器依赖共享文件系统),结论不言而喻。所以一开始决策时,没敢把代码放在 ceph 上,而是放在镜像里。 所以我们选的是方案2,代码在镜像里。
在我们的场景里,镜像会被下载到 mesos slave 物理机的本地磁盘上,所以启动容器时读的是本地磁盘。
多说一句,Qunar 等公司在2014年选择的是卷挂载方案,业界也有人认为,这事儿得分环境具体问题具体分析,开发集成环境和生产环境可能就不一样。
Docker 现有的网络模型主要是通过使用 Network namespace、Linux Bridge、iptables、veth pair 等技术实现的。( 出处8 )
用来设置网络接口的 docker run --net 命令,它的可用参数有四个:
在 docker 默认的网络环境下,单台主机上的容器可以通过 docker0 网桥直接通信,如下图(图作者冯明振)所示:
而不同主机上的容器之间只能通过在主机上做端口映射进行通信。这种端口映射方式对集群应用来说极不方便。( 出处8 )我们重点要解决这个问题。
先看一下其他学生怎么做这道选择题的:
公司 | 网络方案 | 备注 |
网易(2014) | tinc+quagga+pipework | Pipework 是对 Docker Bridge 的扩展,它由 200 多行 shell 脚本实现。通过使用 ip、brctl、ovs-vsctl 等命令来为 Docker 容器配置自定义的网桥、网卡、路由等。 |
CoreOS(2014) | flannel | flannel 属于隧道方案,UDP 广播,VxLan。 |
大众点评网(2015) | Bridge Networking 工作在 level 2 的模式,使公共 IP 得以暴露出来,这部分是做了定制的 | |
汽车之家(2015) | Bridge Networking | |
去哪儿(2015) | Host Networking | 『大吞吐量平台下,bridge 模式性能测试都偏低,于是选择了 host 模式』——20150915,徐磊 |
芒果TV(2015) | Macvlan | 属于路由方案。『从逻辑和 Kernel 层来看隔离性和性能最优的方案,基于二层隔离,所以需要二层路由器支持,大多数云服务商不支持,所以混合云上比较难以实现』——20150505,彭哲夫 |
新浪微博(2015) | Host Networking |
先说一下 host 模式。
容器的网络直接和外部网络打通,解决了不同 slave 物理机之间容器互联互通的问题,减少了 NAT 转换的损耗,但需要自行来维护容器的 IP 地址和端口,否则会冲突,这增加了管理上的复杂度。请考虑这个场景:在同一个 slave 物理机上运行两个 80 端口 Nginx 容器的情况,除非分配两个不同的 IP 地址,这种方案我们之前在 shipyard 方案中使用过。
新浪微博在《 大规模Docker集群助力微博迎接春晚峰值挑战 》一文中曾经介绍过:
Docker 部署被诟病最多就是网络,平台目前采用的是 host 模式,为什么没有采用 NAT 或者 Bridge 呢?由于涉及的技术细节比较繁冗,这里仅分享一些踩过的坑。例如 NAT 使用 iptables 底层流量转发依靠内核 netfilter 模块,其默认仅保持 65536 个链接,在服务有大量链接的场景下,会出现大量拒绝链接的现象。再如 Bridge 的 MAC 地址默认是选择其子接口中最先的一个,这样就会导致一个宿主机下多个容器启停时出现网络瞬断。还有很多问题不一一列举,平台未来计划采用 vlanif 的方案来解决容器网络部署难题。
再看 bridge 模式,它是 docker 默认的网络模式,为了解决容器间互通问题,通常有两种解决方案:
我们认为,bridge 模式加 NAT 方式,是当前 mesos+marathon 支持的最好的模式(采用的是 container port,host port,service port 的对应关系)。我们的容器云主要构建于 mesos+marathon 之上,封装了我们的业务逻辑,采用一些方式解决了容器与现存非容器架构之间的互通问题, 如 consul 的服务注册和发现机制,dubbo 的应用注册和发现机制等。
最终我们选择了 Bridge Networking 。
Libnetwork 是 Docker 官方 2015年初推出的项目,旨在将 Docker 的网络功能从 Docker 核心代码中分离出去,形成一个单独的库。 Libnetwork 以插件的形式为 Docker 提供网络功能,使得用户可以根据自己的需求实现自己的 Driver 来提供不同的网络功能。 Libnetwork 所要实现的网络模型基本是这样的: 用户可以创建一个或多个网络(一个网络就是一个网桥或者一个 VLAN ),一个容器可以加入一个或多个网络。 同一个网络中容器可以通信,不同网络中的容器隔离。 Docker 网络的发展以后就都在这个项目上了。
默认情况下,当 docker 启动时,它会在宿主机器上创建一个名为 docker0 的虚拟网络接口。它会从 RFC 1918 定义的私有地址中随机选择一个主机不用的地址和子网掩码,并将它分配给 docker0。( 出处9 )所以, 容器的 IP 是动态变化的 。
于是乎大家一开始接触 Docker 就纷纷提出给容器分配静态 IP 的需求。看一下 各大公司容器云技术栈对比 ,几家历史包袱较重的公司都选择不让上层应用感知到底层是 VM 还是容器,如360/点评/汽车之家,都改了docker 代码,固定了容器 IP。
docker 官方对此需求的态度是,Docker maintainers prefers a more abstract way to separate user intent from operational intent. Based on this feedback and various other discussions on a flexible ip address management, we feel that having a pluggable IPAM will help a great deal.
If you’re using named Docker instances, then adding the IP address 10.40.33.21 to a Docker instance bind is as simple as:
pipework br0 bind 10.40.33.21/24
If you want to route out of 10.40.33.1, change it to:
pipework br0 bind 10.40.33.21/24@10.40.33.1
(出处:https://opsbot.com/advanced-docker-networking-pipework/)
这样的话,需要在容器启动后在宿主机上运行 pipework 来设置容器的 ip,这样就增加了自动化的难度。
我们的考虑是,第一,容器应该是无状态的,分配固定 IP 违背了这个理念,第二,Docker 刚出来一两年,还在飞速发展中,现在你改了内核代码,将来它大版本升级,你怎么跟随?第三,使用固定 IP 很大程度上是为了让技术人员能像以前一样直接登录虚拟机操作,但我们的持续集成管理平台里使用 webconsole 也能登到容器里面操作。
我们的实践是:首先,我们内部调用都走内部域名,有专门的 DNS Server,其次,我们很早以前就引入了服务治理框架 Dubbo,基本做到了服务的注册和发现。再次,我们在容器云里引入了 consul 的注册和发现服务,配合 slave 节点上的 registrator 容器,以及 haproxy 和 consul-template 组件,实现了完整的容器服务的注册和发现架构。
第一,dubbo 能帮我们解决什么容器问题:
进驻容器云后 java 工程之间的调用:直接将物理机的 ip 和容器 java 工程对外提供的随机端口,注册到 dubbo 里。例如, 容器 javaA 要调用容器 javaB,容器 javaB 已经将它对外提供的 ip 和随机端口注册到了 dubbo 里,容器 javaA 可以从 dubbo 里找到容器 javaB 相关的信息直接调用。
第二,dubbo 不能解决的问题有:
基于以上两个需求、以及同一个工程的容器也要做负载均衡 ,我们引入了 consul + consul-template + registrator + haproxy,来做服务注册和发现。
实施详细流程如下:
第一步,每一台 slave 节点上都会启动一个 registrator 的容器,该容器检测 docker 引擎的 unix socket 地址:/var/run/docker.sock,从中获得该 slave 上所有容器的启动、停止以及其他运行时相关的信息;
第二步,registrator 容器同时会把它获取的其他容器的信息(IP、端口等)注册到 consul 集群中,服务信息如下面的输出:
curl 172.28.128.3:8500/v1/catalog/service/python-micro-service
"Node":"host-1",
"Address":"172.28.128.3",
"ServiceID":"registrator:service1:5000",
"ServiceName":"python-micro-service",
"ServiceTags":null,
"ServicePort":49154
第三步,consul-template 组件从 consul 中获得所有容器注册的服务信息,应用相应的规则(如,区分为镜像环境和生产环境)后,将容器提供的服务信息写入 haproxy 中,并 reload haproxy 以生效配置。
第四步,client 端通过 DNS 中注册的域名指向 haproxy 的地址,访问到具体提供服务的容器。
容器启动后,宿主机 IP 会被写入容器的环境变量里。
容器内的程序可以从环境变量里读取。
——未完待续——
附录A:参考资源
1,知乎Docker话题, http://www.zhihu.com/topic/19950993 ;
2,2015, Docker终极指南(原作者German Jaber) ;
3,2015,Docker持续部署图文详解;
4,2015, 使用Docker搭建Java Web运行环境 ;
5,2015, dockone上对代码放里面还是外面的讨论 ;
6,2013, dockerfile最佳实践 ;
7,2015, 集群规模下日志处理和网络方案 ;
8,2015, Docker网络详解及Libnetwork前瞻 ;
9, docker官网讲网络配置 , 中文译稿 ;
附录B:术语简单介绍
Registry/Repository/Image/Tag: Registry 存储镜像数据,并且提供拉取和上传镜像的功能。Registry 中镜像是通过 Repository 来组织的,而每个 Repository 又包含了若干个 Image。
比如,你在本地机器上运行 docker image 命令,可能会得到这样的输出结果:
我们常说的“ubuntu”镜像其实不是一个镜像名称,而是代表了一个名为 ubuntu 的 Repository,同时在这个 Repository 下面有一系列打了 tag 的 Image,Image 的标记是一个 GUID,为了方便也可以通过 Repository:tag 来引用。 ——《玩转Docker镜像,2014,孙宏亮》 |
Network settings: docker run --net 的四个参数进一步解释如下: None: 将网络模式设置为 none 时,这个 container 将不允许访问任何外部 router。这个 container 内部只会有一个 loopback 接口,而且不存在任何可以访问外部网络的 router。 Bridge: 默认为 bridge 模式。此时在 host 上面将存在一个 docker0 的网络接口,同时会针对 container 创建一对 veth 接口。其中一个 veth 接口是在 host 充当网卡桥接作用,另一个 veth 接口存在于 container 的命名空间中,并且指向 container 的 loopback。docker 会自动给这个 container 分配一个 IP,并且将 container 内的数据通过桥接转发到外部。 如下图(图作者冯明振)所示:
容器 eth0 网卡从 docker0 网桥所在的 IP 网段中选取一个未使用的 IP,容器的 IP 在容器重启的时候会改变。docker0 的 IP 为所有容器的默认网关。容器与外界通信为 NAT。 Host: 此时,这个 container 将完全共享 host 的网络堆栈。host 所有的网络接口将完全对 container 开放。container 的主机名也会存在于 host 的 hostname 中。这时,container 所有对外暴露的 port 和对其它 container 的 link,将完全失效。 Container: 此时,这个 container 将完全复用另一个 container 的网络堆栈。 |
附录C:
目前,我们容器管理集群的技术栈包括以下内容:
窝窝持续集成管理平台在这些技术的基础上,实现了我们的 集群管理、容器管理、应用管理 等业务流程。
欢迎订阅我的微信订阅号『老兵笔记』,请扫描二维码关注:
-EOF-