Docker Swarm是docker公司2014年出品的基于docker的集群管理调度工具,官方文档地址:https://docs.docker.com/swarm/overview/。Swarm能够把多台主机构建成一个docker的集群,用户可以只和swarm api操作来管理多个主机上的docker,再结合上Overlay的网络实现简单的容器调度和互访。
Docker Swarm在设计上遵从了可插拔的设计思想,安装集群只需要启动几个docker就可以完成,安装过程可以参考这里:http://www.tuicool.com/articles/UJJJFjU。
总结下Swarm的特性: 1.工作节点的注册和发现 2.管理节点收集集群中所有的信息 3.管理节点支持HA 4.管理节点能感知集群的变化,在节点挂掉后重新调度上面的container 5.提供filter和scheduler的调度策略调度集群里的容器
下面,本文会从源码层面解密Swarm是如何实现上面的特性的。
首先上一张整体的架构图。 来自daocloud的架构图。 http://blog.daocloud.io/wp-content/uploads/2015/01/swarmarchitecture.jpg
在工作节点启动时会在后端的kvstore上注册一个节点,路径是etcd://ip:2376/docker/swarm/nodeip,Worker会把当前集群的eth0 的ip注册上etcd,然后设置上一个ttl时间,比如3秒。然后启一个for循环每隔2秒(配置heartbeat)注册一次,这样,如果etcd上这个节点没了就说明这个worker已经挂了。
for { log.WithFields(log.Fields{"addr": addr, "discovery": dflag}).Infof("Registering on the discovery service every %s...", hb) if err := d.Register(addr); err != nil { log.Error(err) } time.Sleep(hb) }Manager的leader会启动一个go router watch后端的kvstore上注册上来的ip,如果是新节点注册上来就把节点加入到manager的内存中,开始收集数据,如果是节点挂了就删除
discoveryCh, errCh := cluster.discovery.Watch(nil) go cluster.monitorDiscovery(discoveryCh, errCh) go cluster.monitorPendingEngines() for { select { case entries := <-ch: added, removed := currentEntries.Diff(entries) currentEntries = entries // Remove engines first. `addEngine` will refuse to add an engine // if there's already an engine with the same ID. If an engine // changes address, we have to first remove it then add it back. for _, entry := range removed { c.removeEngine(entry.String()) } for _, entry := range added { c.addEngine(entry.String()) } case err := <-errCh: log.Errorf("Discovery error: %v", err) } }管理节点会收集集群中所有主机的信息放到内存中。当一个主机加入到Swarm中时,首先会对节点上所有的信息都收集一把到内存中,然后会建立一个docker client长链接,通过event API获取这个主机上的更新。
加入主机时的代码,首先做主机的全同步,然后启动eventMonitor,监控主机上的event:
e.eventsMonitor = NewEventsMonitor(e.apiClient, e.handler) // Fetch the engine labels. if err := e.updateSpecs(); err != nil { return err } e.StartMonitorEvents() // Force a state update before returning. if err := e.RefreshContainers(true); err != nil { return err } if err := e.RefreshImages(); err != nil { return err } // Do not check error as older daemon doesn't support this call. e.RefreshVolumes() e.RefreshNetworks()Event的Handler,会根据event的类别更新对应类型的数据。这里由于考虑docker event的兼容性有点长,我就只贴一段:
switch msg.Type { case "network": e.refreshNetwork(msg.Actor.ID) case "volume": e.refreshVolume(msg.Actor.ID) case "image": e.RefreshImages() case "container": action := msg.Action // healthcheck events are like 'health_status: unhealthy' if strings.HasPrefix(action, "health_status") { action = "health_status" } switch action { case "commit": // commit a container will generate a new image e.RefreshImages() case "die", "kill", "oom", "pause", "start", "restart", "stop", "unpause", "rename", "update", "health_status": e.refreshContainer(msg.ID, true) case "top", "resize", "export", "exec_create", "exec_start", "exec_detach", "attach", "detach", "extract-to-dir", "copy", "archive-path": // no action needed default: e.refreshContainer(msg.ID, false)同其他很多的分布式的项目一样,Docker Swarm也是利用了raft里选举算法做的HA,我们来看下它的实现。
首先创建好candidata和follower,顺便说下leader election的path是docker/swarm/leader
client := kvDiscovery.Store() p := path.Join(kvDiscovery.Prefix(), leaderElectionPath) candidate := leadership.NewCandidate(client, p, addr, leaderTTL) follower := leadership.NewFollower(client, p)然后启两个协程,一个进行选举,如果成功了则成为leader,一个监听选举成功的消息,如果监听到别的manager成为leader则把自己设置成candidate,如果API请求到candidate会proxy到真正的manager。
primary := api.NewPrimary(cluster, tlsConfig, &statusHandler{cluster, candidate, follower}, c.GlobalBool("debug"), c.Bool("cors")) replica := api.NewReplica(primary, tlsConfig) go func() { for { run(cluster, candidate, server, primary, replica) time.Sleep(defaultRecoverTime) } }() go func() { for { follow(follower, replica, addr) time.Sleep(defaultRecoverTime) } }() server.SetHandler(primary)由于worker会loop往kvstore上发送消息,因此如果节点挂掉时manager能立刻感知到变化,并触发removeEngine的动作,把container重新调度到其他节点上就很容易做到了。
其实有了所有集群里的所有节点的信息,调度容器就变得比较简单了。Swarm提供了Filter和scheduler来让用户定义调度的策略。
调度本质上是让用户可以定义Container分配到集群中的策略。 Filter指定了如果满足这样的条件的节点不会(会)被分配上。 Scheduler指定了满足Filter后的节点按照怎样的优先级排序,排在前面的会被有限分配上Container。
Filter和Scheduler的种类我就不赘述了,可以参考官方文档:https://docs.docker.com/swarm/scheduler/rescheduling/#rescheduling-policies (貌似最近又有了新的策略 rescheduler)
调度的代码如下:
accepted, err := filter.ApplyFilters(s.filters, config, nodes, soft) if err != nil { return nil, err } if len(accepted) == 0 { return nil, errNoNodeAvailable } return s.strategy.RankAndSort(config, accepted)在使用Docker Swarm的时候大家其实可以发现,Swarm的设计还是有一些缺陷的,这会导致Swarm的一些局限性,比如:
1.worker的行为过于简单。只是往kvstore上同步状态,就启动一个Container,不做任何实际的工作,把所有活都交给Manager干,颇为浪费。 2.由于worker”啥也不干“,Manager必须保持所有节点的tcp长链接,扩展性很差。 3.没有加入副本控制。
总结下,Swarm作为一代的Docker调度工具提供了基本的调度能力,可以满足一些内部的CI/CD系统使用,但是由于扩展性较差和没有副本控制,不能直接拿来部署线上系统,这是有的遗憾的。