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系统使用,但是由于扩展性较差和没有副本控制,不能直接拿来部署线上系统,这是有的遗憾的。