Docker编排工具调研:Rancher

Rancher提供了一套完整的Docker编排解决方案(重点是开源的)。功能上包括网络,存储,负载均衡,安全,服务发现和资源管理等。Rancher可以管理DigitalOceanAWSOpenStack等云主机,自动创建Docker运行环境,实现跨云管理。使用上可以通过Web界面或者命令行方式进行操作。

下面介绍一下使用上的一些体会,并且分析一下一些有意思的特性的技术实现。先说下整体上的体验,Rancher在使用上非常流畅,当然是指排除“功夫王”的影响之外。学习成本也比较小,稍微有一点Docker知识的同学,都能很快上手。

运行

Rancher提供了Docker镜像,直接通过docker run -d --restart=always -p 8080:8080 rancher/server命令运行即可。除了标准的Docker镜像之外,Rancher还提供了手动、Vagrant、Puppet、Ansible等方式安装。

这里要提到一点,Rancher也是采用Master-Agent的方式,但是Rancher的Agent也是以容器方式运行的,虚机环境修改很少。

Rancher容器是采用的胖容器方式,会运行Cattle,Mysql,rancher-compose-executor,go-machine-service等几个服务,以及crontab等定时任务。其中值得一提Cattle,Cattle是Rancher实现的一套基于Docker的编排引擎,初步上来看跟Docker Swarm类似,是用Java语言实现的,初步看代码实现的功能还是比较多的。

另外需要注意,由于Rancher目前还仅支持管理墙外的云服务,所以在墙内运行的话会导致创建虚机失败率非常高。另外Rancher运行至少需要1G的内存,这也导致小编在DO上创建512MB的Droplet运行Rancher服务失败,辛辛苦苦排查了半天,如果你遇到下面类似的异常日志,那说明你得换一个大一点的Droplet:

2015-10-12 10:17:58,910 INFO    [main] [ConsoleStatus] [99/102] [43348ms] [8ms] Starting register
runtime/cgo: pthread_create failed: Resource temporarily unavailable
SIGABRT: abort
PC=0x6f8177 m=0

goroutine 0 [idle]:

goroutine 1 [running]:
runtime.systemstack_switch()
    /usr/local/go/src/runtime/asm_amd64.s:216 fp=0xc820024770 sp=0xc820024768
runtime.main()
    /usr/local/go/src/runtime/proc.go:49 +0x62 fp=0xc8200247c0 sp=0xc820024770
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:1696 +0x1 fp=0xc8200247c8 sp=0xc8200247c0

goroutine 17 [syscall, locked to thread]:
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:1696 +0x1

rax    0x0
rbx    0xc30488
rcx    0x6f8177
rdx    0x6
rdi    0x8c
rsi    0x8c
rbp    0x734b32
rsp    0x7ffe7ce05578
r8     0xa
r9     0x27ae880
r10    0x8
r11    0x202
r12    0x27b0bc0
r13    0x8ed580
r14    0x0
r15    0x8
rip    0x6f8177
rflags 0x202
cs     0x33
fs     0x0
gs     0x0
runtime/cgo: pthread_create failed: Resource temporarily unavailable
SIGABRT: abort
PC=0x6f8177 m=0

另外还有一个需要注意的,Rancher容器启动之后并不能立即对外提供服务,Rancher内部服务启动需要耗费一定的时间。从日志上看大概需要将近1分钟的启动时间,这个有待优化啊!所以如果你运行完容器之后,访问界面抛出Connection Refuse Exception不要以为是启动方式有问题,请耐心等待......

09:36:32.655 [main] INFO  ConsoleStatus - [DONE ] [50277ms] Startup Succeeded, Listening on port 8081

虚机

从Web界面上看,操作是非常简便的,例如对于DO来说直接输入APPKEY就可以完成Droplet的创建。目前Rancher还仅支持Ubuntu版本的虚机。从运行日志上看,Rancher在初始化虚机的时候做了这么几件事情:

  • 通过定制化的Docker Machine创建虚机,完成Docker安装
  • 初始化SSH证书
  • 上传rancher.tar.gz到虚机
  • 拉取rancher/agent:v0.8.2镜像

可以看出环境依赖非常少。Docker配置也比较标准,除了添加了TLS认证之外没有额外的配置。主机配置也就是一些证书文件,没有额外的配置。从这方面来看Rancher本身是比较容易支持扩展的,引入对阿里云的支持也会比较简单。但是另外一方面,由于代码架构上的原因,第三方开发支持云服务可能会比较纠结,比较可行的方式还是Rancher和各云服务提供商配合。这可能也会限制Rancher在国内的推广。

另外创建出来的虚机仅支持根据ssh key方式登陆,不支持root密码登陆。目前有两种方式可以ssh登陆到虚机,一个是通过Rancher提供的Web SSH界面,一个是下载主机配置,使用里面的id_rsa文件来登陆ssh -i id_rsa root@<IP_OF_HOST>

虚机支持按Tag属性管理,不支持直接按组管理,对于多租户场景可以通过Rancher提供的Management Environment功能来实现。

另外一个比较有意思的是Rancher提供了一个View in API功能,返回的是一个JSON格式数据,其中links字段包含了一些有关的接口地址,这个功能对二次功能的开发比较友好,可以通过接口API获取API,这样就不必学习接口调用方式、参数等等内容了。

{
"id": "1h1",
"type": "host",
"links": {
"self": "…/v1/projects/1a5/hosts/1h1",
"account": "…/v1/projects/1a5/hosts/1h1/account",
"clusters": "…/v1/projects/1a5/hosts/1h1/clusters",
"containerEvents": "…/v1/projects/1a5/hosts/1h1/containerevents",
"hostLabels": "…/v1/projects/1a5/hosts/1h1/hostlabels",
"instances": "…/v1/projects/1a5/hosts/1h1/instances",
"ipAddresses": "…/v1/projects/1a5/hosts/1h1/ipaddresses",
"loadBalancerHostMaps": "…/v1/projects/1a5/hosts/1h1/loadbalancerhostmaps",
"loadBalancers": "…/v1/projects/1a5/hosts/1h1/loadbalancers",
"physicalHost": "…/v1/projects/1a5/hosts/1h1/physicalhost",
"serviceEvents": "…/v1/projects/1a5/hosts/1h1/serviceevents",
"storagePools": "…/v1/projects/1a5/hosts/1h1/storagepools",
"stats": "…/v1/projects/1a5/hosts/1h1/stats",
"hostStats": "…/v1/projects/1a5/hosts/1h1/hoststats",
"containerStats": "…/v1/projects/1a5/hosts/1h1/containerstats",
},
"actions": {
"update": "…/v1/projects/1a5/hosts/1h1/?action=update",
"deactivate": "…/v1/projects/1a5/hosts/1h1/?action=deactivate",
},
"name": "rancher",
"state": "active",
"accountId": "1a5",
"agentState": null,
"computeTotal": 1000000,
"created": "2015-10-13T04:08:55Z",
"createdTS": 1444709335000,
"description": null,
"info": {
"osInfo": {

同时还支持基础层面的监控,而且是用WebSocket实现的,"url": "ws://104.236.151.239:8080/v1/hoststats"。看代码应该是没有用到什么第三方的监控解决方案,比如ELK、Graphite之类的,应该Cattle自行实现的。猜测实现是在io.cattle.platform.host.stats.api。不过貌似不是特别稳定,窗口切换后会导致监控数据不再刷新。像下图这种情况:

容器

容器编排本身并没有什么特别的功能,基本是对Docker命令的封装。其中有几个特性值得关注,一个是Managed网络,第二个是健康检查,第三个是调度策略。下面一个个来看:

Managed网络

Rancher在Docker本身提供的几种网络基础上提供了一种叫做Managed的网络特性,按照其官方说法是:

The Rancher network uses IPsec tunnelling and encryption for security

简单理解就是在容器之间构建了一条私有网络,只有容器与容器之间可以访问。Rancher会为每个容器分配一个10.42.*.*的私有网络地址,这个地址只在容器之间是可达的,对外不可见,有点类似一种纯软件上的VPC方式。

从上面的部署上看,Rancher并没有对网络有什么特殊的设置,它是如何实现的呢?从路由上可以看出,对于10.42网段的路由是通过docker0接口,docker0的特殊的配置了IP也包含了10.42网段。

root@rancher:~# ip route
default via 192.241.212.1 dev eth0
10.42.0.0/16 dev docker0  proto kernel  scope link  src 10.42.0.1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.42.1
192.241.212.0/24 dev eth0  proto kernel  scope link  src 192.241.212.19

root@rancher:~# ip addr
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:21:6e:92:f5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.42.1/16 scope global docker0
       valid_lft forever preferred_lft forever
    inet 10.42.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:21ff:fe6e:92f5/64 scope link
       valid_lft forever preferred_lft forever  

实现流量转发应该是靠iptables实现的,规则如下:

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
CATTLE_PREROUTING  all  --  anywhere             anywhere
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere            !loopback/8           ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
CATTLE_POSTROUTING  all  --  anywhere             anywhere
MASQUERADE  all  --  172.17.0.0/16        anywhere
MASQUERADE  udp  --  172.17.0.4           172.17.0.4           udp dpt:ipsec-nat-t
MASQUERADE  udp  --  172.17.0.4           172.17.0.4           udp dpt:isakmp

Chain CATTLE_POSTROUTING (1 references)
target     prot opt source               destination
ACCEPT     all  --  10.42.0.0/16         169.254.169.250
MASQUERADE  tcp  --  10.42.0.0/16        !10.42.0.0/16         masq ports: 1024-65535
MASQUERADE  udp  --  10.42.0.0/16        !10.42.0.0/16         masq ports: 1024-65535
MASQUERADE  all  --  10.42.0.0/16        !10.42.0.0/16
MASQUERADE  tcp  --  172.17.0.0/16        anywhere             masq ports: 1024-65535
MASQUERADE  udp  --  172.17.0.0/16        anywhere             masq ports: 1024-65535

Chain CATTLE_PREROUTING (1 references)
target     prot opt source               destination
DNAT       udp  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL udp dpt:ipsec-nat-t to:10.42.241.7:4500
DNAT       udp  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL udp dpt:isakmp to:10.42.241.7:500
DNAT       tcp  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL tcp dpt:http-alt to:10.42.75.19:8080

Chain DOCKER (2 references)
target     prot opt source               destination
DNAT       udp  --  anywhere             anywhere             udp dpt:ipsec-nat-t to:172.17.0.4:4500
DNAT       udp  --  anywhere             anywhere             udp dpt:isakmp to:172.17.0.4:500

对iptables规则本身不是特别了解,所以只能转包看看网络包是如何被转发的。构建一个两个虚机的测试环境,创建容器之后,让其中一个容器nc监听一个端口,然后在另外一台虚机的容器中nc连接这个端口。在Docker自身的网络模型中,这种方式是不能work的,因为在一个中随意open一个端口,由于没有在docker run中设置export端口,所以外界是无法访问的。但是Rancher的这种网络结构却可以实现这一点。在虚机一上tcpdump -i eth0 host 45.55.29.83,在虚机一的容器中tcpdump -i eth0,这是可以发现实际上数据包是通过IPsec封装传输到目标容器的,猜测实际网络传输是UDP协议封装的:

tcpdump -i eth0 host 45.55.29.83
12:10:44.663797 IP 192.241.212.19.1024 > 45.55.29.83.ipsec-nat-t: isakmp-nat-keep-alive
12:10:47.229600 IP 45.55.29.83.ipsec-nat-t > 192.241.212.19.ipsec-nat-t: UDP-encap: ESP(spi=0x09b0aceb,seq=0x2f), length 100
12:10:47.230028 IP 192.241.212.19.1024 > 45.55.29.83.ipsec-nat-t: UDP-encap: ESP(spi=0x0b788656,seq=0x10), length 100
12:10:47.230495 IP 45.55.29.83.ipsec-nat-t > 192.241.212.19.ipsec-nat-t: UDP-encap: ESP(spi=0x09b0aceb,seq=0x30), length 100
12:10:47.230579 IP 45.55.29.83.ipsec-nat-t > 192.241.212.19.ipsec-nat-t: UDP-encap: ESP(spi=0x09b0aceb,seq=0x31), length 100
12:10:47.230602 IP 45.55.29.83.ipsec-nat-t > 192.241.212.19.ipsec-nat-t: UDP-encap: ESP(spi=0x09b0aceb,seq=0x32), length 100
12:10:47.230665 IP 192.241.212.19.1024 > 45.55.29.83.ipsec-nat-t: UDP-encap: ESP(spi=0x0b788656,seq=0x11), length 100
12:10:47.231275 IP 192.241.212.19.1024 > 45.55.29.83.ipsec-nat-t: UDP-encap: ESP(spi=0x0b788656,seq=0x12), length 100
12:10:47.231511 IP 45.55.29.83.ipsec-nat-t > 192.241.212.19.ipsec-nat-t: UDP-encap: ESP(spi=0x09b0aceb,seq=0x33), length 100
12:10:47.511757 IP 45.55.29.83.ipsec-nat-t > 192.241.212.19.ipsec-nat-t: isakmp-nat-keep-alive
tcpdump -i eth0
16:10:47.229876 IP 10.42.116.141.36379 > 10.42.75.19.1234: Flags [S], seq 867849729, win 29200, options [mss 1460,sackOK,TS val 289931 ecr 0,nop,wscale 8], length 0
16:10:47.229941 IP 10.42.75.19.1234 > 10.42.116.141.36379: Flags [S.], seq 4135689986, ack 867849730, win 28960, options [mss 1460,sackOK,TS val 10776863 ecr 289931,nop,wscale 8], length 0
16:10:47.230535 IP 10.42.116.141.36379 > 10.42.75.19.1234: Flags [.], ack 1, win 115, options [nop,nop,TS val 289932 ecr 10776863], length 0
16:10:47.230623 IP 10.42.116.141.36379 > 10.42.75.19.1234: Flags [P.], seq 1:7, ack 1, win 115, options [nop,nop,TS val 289932 ecr 10776863], length 6
16:10:47.230638 IP 10.42.75.19.1234 > 10.42.116.141.36379: Flags [.], ack 7, win 114, options [nop,nop,TS val 10776863 ecr 289932], length 0
16:10:47.230642 IP 10.42.116.141.36379 > 10.42.75.19.1234: Flags [F.], seq 7, ack 1, win 115, options [nop,nop,TS val 289932 ecr 10776863], length 0
16:10:47.231246 IP 10.42.75.19.1234 > 10.42.116.141.36379: Flags [F.], seq 1, ack 8, win 114, options [nop,nop,TS val 10776863 ecr 289932], length 0
16:10:47.231546 IP 10.42.116.141.36379 > 10.42.75.19.1234: Flags [.], ack 2, win 115, options [nop,nop,TS val 289932 ecr 10776863], length 0

不得不说,这个特性非常赞!很多场景都迎刃而解,比如像测试环境快速构建就有这种需求。开发工程师完成编码之后进行快速验证,申请一个测试环境容器(假设是一个Ubuntu),第一步就是把代码包上传,然后改BUG后再上传,比较土的做法就是nc。但是由于Docker的网络限制,这种需求不太好支持。但是Rancher实现的这种虚拟网络能够很好的解决这类诉求。

健康检查 && 调度策略

Rancher还自带了一个Health Check功能,然而貌似并没有看到实际作用。另外比较担心如果管理规模比较大的容器集群时,健康检查所带来的主节点的负载压力会不会较大,进而影响正常功能使用?

Rancher的调度策略也比较有意思,最开始以为也是什么CPU、Memory到了多少扩容多少个容器之类的策略。实际一看发现Rancher还是比较务实的,只是实现了类似Swarm的Affinity的调度功能,通过这个功能可以完成一些有状态容器的编排,还是比较实用的,至少比根据CPU扩容靠谱多了。

当然Rancher还有很多有意思的特性,例如整合了docker compose(话说貌似docker compose就合到Rancher了),可以实现一些固定场景部署的自动化操作;还有Stack的概念,支持拓扑展示;这里就不一一介绍了,有兴趣的同学可以尝试一下。

服务发现 && 负载均衡

提到容器编排肯定是少不了服务发现和负载均衡的,Rancher的服务发现是基于DNS实现的,具体实现是在/var/lib/cattle/bin/rancher-dns,比较有意思的是所有容器的DNS服务器都指向了169.254.169.250,然后又通过路由转发,实际是转给了此容器对应的rancher/agent-instance:v0.4.1容器中的DNS服务器。

nameserver 169.254.169.250

169.254.169.250 dev eth0  scope link  src 10.42.84.2

至于为什么这么蹩脚的实现,目前能想到的合理解释就是为了实现只有link的服务才能解析到域名。例如上图中web容器link了memcached容器,这是在web容器中可以解析到memcached域名,而其他容器无法解析(虽然ip还是能够ping的通)。

Rancher的负载均衡是通过HAProxy实现的,也比较简单,可以在界面上直接创建一个负载均衡。而且负载均衡可以动态修改,容器创建也可以自动绑定到负载均衡器。这样就可以实现双向互动,比较方便。

附录

Mysql表结构定义

+-----------------------------------------------+
| Tables_in_cattle                              |
+-----------------------------------------------+
| DATABASECHANGELOG                             |
| DATABASECHANGELOGLOCK                         |
| account                                       |
| agent                                         |
| agent_group                                   |
| auth_token                                    |
| certificate                                   |
| cluster_host_map                              |
| config_item                                   |
| config_item_status                            |
| container_event                               |
| credential                                    |
| credential_instance_map                       |
| data                                          |
| environment                                   |
| external_handler                              |
| external_handler_external_handler_process_map |
| external_handler_process                      |
| generic_object                                |
| global_load_balancer                          |
| healthcheck_instance                          |
| healthcheck_instance_host_map                 |
| host                                          |
| host_ip_address_map                           |
| host_label_map                                |
| host_vnet_map                                 |
| image                                         |
| image_storage_pool_map                        |
| instance                                      |
| instance_host_map                             |
| instance_label_map                            |
| instance_link                                 |
| ip_address                                    |
| ip_address_nic_map                            |
| ip_association                                |
| ip_pool                                       |
| label                                         |
| load_balancer                                 |
| load_balancer_certificate_map                 |
| load_balancer_config                          |
| load_balancer_config_listener_map             |
| load_balancer_host_map                        |
| load_balancer_listener                        |
| load_balancer_target                          |
| mount                                         |
| network                                       |
| network_service                               |
| network_service_provider                      |
| network_service_provider_instance_map         |
| nic                                           |
| offering                                      |
| physical_host                                 |
| port                                          |
| process_execution                             |
| process_instance                              |
| project_member                                |
| resource_pool                                 |
| service                                       |
| service_consume_map                           |
| service_event                                 |
| service_expose_map                            |
| setting                                       |
| snapshot                                      |
| snapshot_storage_pool_map                     |
| storage_pool                                  |
| storage_pool_host_map                         |
| subnet                                        |
| subnet_vnet_map                               |
| task                                          |
| task_instance                                 |
| user_preference                               |
| vnet                                          |
| volume                                        |
| volume_storage_pool_map                       |
| zone                                          |
+-----------------------------------------------+
Written on October 14, 2015