Swarm源码分析(未完待续)

2015-12-31 Lingxian Kong 更多博文 » 博客 » GitHub »

原文链接 https://lingxiankong.github.io/2015-12-31-swarm-source-code-analysis.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


本文适合Go语言和Swarm的初学者

在我之前的一篇博客中,我在本地mac机器上搭建了swarm。前两天刚刚看完了Go的基础教程,所以,我就顺着swarm搭建的步骤,来尝试阅读swarm的源码。

因为我搭建swarm集群时使用的是swarm的docker镜像,所以就从swarm中的dockerfile文件开始吧。

swarm dockerfile

swarm的dockerfile很简单,只有十几行。在镜像中安装了swarm,暴漏2375端口,容器入口命令为swarm。

也就是说,我使用docker run --rm swarm create命令获取token时,实际上在swarm容器里运行的是swarm create命令。同样,执行docker run -d swarm join --addr=192.168.33.12:2375 token://a3ec81af8d0d0690fcaf2ac95042f771,等同于在容器里执行swarm join...命令,但注意,此时,容器是以deamon方式运行,容器中应该有后台服务在跑。

由此推测,swarm肯定有命令行解析程序。

swarm命令行

Go程序的入口函数是main,刚好swarm主目录下就有main.go文件,里面就有main函数。main函数很简单:

func main() {
    cli.Run()
}

swarm使用了github上的https://github.com/codegangsta/cli项目,其实就是类似于Python中的argparse模块,提供命令行解析功能。有兴趣的可以阅读该项目的readme。

swarm定义了两个命令行参数,debug和log-level,无关痛痒。同时提供了4个命令,create, list, manage, join,这四个命令我在创建swarm集群时都用到了。

swarm join

在使用docker hub提供的token机制做服务发现时(虽然没有成功),我用的第一个命令就是join,将本节点加入swarm集群,命令见上。

join支持4个参数:

{
    Name:      "join",
    ShortName: "j",
    Usage:     "join a docker cluster",
    Flags:     []cli.Flag{flJoinAdvertise, flHeartBeat, flTTL, flDiscoveryOpt},
    Action:    join,
},

参数定义在cli/flags.go中,分别表示本机docker engine的服务地址、心跳值、TTL和服务发现的参数(就是token://XXX字符串)。用这几个参数调用discovery包种的New和Register方法。

discovery register

接着找到discovery包种的New方法,发现读取了一个map类型的全局变量discoveries,其值是Discovery类型,但在discovery.go文件中却没有找到对discoveries的赋值,隐隐约约觉得,swarm官网所谓的可插拔的discovery backends,估计就是在各自的包中往discoveries中注册内容。

于是,直接到discovery/token/token.go中找init,果不其然:

func init() {
    Init()
}

// Init is exported
func Init() {
    discovery.Register("token", &Discovery{})
}

对于token机制的服务发现来说,节点的注册就是往https://discovery.hub.docker.com/v1/clusters/<token>?ttl=180的URL发送了POST请求,请求的body体是命令行中的addr值。

我之前使用token方式之所以失败,就是因为国内对discovery.hub.docker.com域名的访问受限导致。

同时,应该注意,join命令不会退出,而是在一个死循环中,每隔一段时间(心跳间隔),都会做一次注册的动作,以此保证自身对swarm manager的可见性。

当然,除了docker hub token的方式,swarm还提供了很多服务发现的plugin,在官网上有很好的介绍,其代码结构与token.go类似。

如果是node或file的方式,不需要在各个agent节点上执行join命令。相应的,它们也没有Register方法。

swarm manage

manage是swarm最为重要的管理命令。一旦swarm manage命令在Swarm节点上被触发,则说明用户开始管理Docker集群了。manage命令的参数也比较多,我就不一一细说。主要的参数是容器放置策略过滤器、支持HA的参数、安全访问TLS等。

在manage函数中,依次创建discovery、strategy、filters、scheduler(使用strategy、filter)、cluster(使用discovery, scheduler)对象,最终创建API服务,监听API请求。

其中,创建cluster对象时,同时启动了docker主机的刷新线程。一个线程定时查询集群中的增删主机,轮询时间由heartbeat参数决定,另一个线程根据查询的结果维护内存中的所有docker主机信息。cluster对象中维护了engines列表,即各个docker主机的信息

discoveryCh, errCh := cluster.discovery.Watch(nil)
go cluster.monitorDiscovery(discoveryCh, errCh)

manage命令会启动HTTP server,接收API请求。swarm目前对外提供的API定义都在api/primary.go中:

var routes = map[string]map[string]handler{
    "HEAD": {
        "/containers/{name:.*}/archive": proxyContainer,
    },
    "GET": {
        "/_ping":                          ping,
        "/events":                         getEvents,
        "/info":                           getInfo,
        "/version":                        getVersion,
        "/images/json":                    getImagesJSON,
        "/images/viz":                     notImplementedHandler,
        "/images/search":                  proxyRandom,
        "/images/get":                     getImages,
        "/images/{name:.*}/get":           proxyImageGet,
        "/images/{name:.*}/history":       proxyImage,
        "/images/{name:.*}/json":          proxyImage,
        "/containers/ps":                  getContainersJSON,
        "/containers/json":                getContainersJSON,
        "/containers/{name:.*}/archive":   proxyContainer,
        "/containers/{name:.*}/export":    proxyContainer,
        "/containers/{name:.*}/changes":   proxyContainer,
        "/containers/{name:.*}/json":      getContainerJSON,
        "/containers/{name:.*}/top":       proxyContainer,
        "/containers/{name:.*}/logs":      proxyContainer,
        "/containers/{name:.*}/stats":     proxyContainer,
        "/containers/{name:.*}/attach/ws": proxyHijack,
        "/exec/{execid:.*}/json":          proxyContainer,
        "/networks":                       getNetworks,
        "/networks/{networkid:.*}":        proxyNetwork,
        "/volumes":                        getVolumes,
        "/volumes/{volumename:.*}":        proxyVolume,
    },
    "POST": {
        "/auth":                               proxyRandom,
        "/commit":                             postCommit,
        "/build":                              postBuild,
        "/images/create":                      postImagesCreate,
        "/images/load":                        postImagesLoad,
        "/images/{name:.*}/push":              proxyImagePush,
        "/images/{name:.*}/tag":               postTagImage,
        "/containers/create":                  postContainersCreate,
        "/containers/{name:.*}/kill":          proxyContainerAndForceRefresh,
        "/containers/{name:.*}/pause":         proxyContainerAndForceRefresh,
        "/containers/{name:.*}/unpause":       proxyContainerAndForceRefresh,
        "/containers/{name:.*}/rename":        postRenameContainer,
        "/containers/{name:.*}/restart":       proxyContainerAndForceRefresh,
        "/containers/{name:.*}/start":         proxyContainerAndForceRefresh,
        "/containers/{name:.*}/stop":          proxyContainerAndForceRefresh,
        "/containers/{name:.*}/wait":          proxyContainerAndForceRefresh,
        "/containers/{name:.*}/resize":        proxyContainer,
        "/containers/{name:.*}/attach":        proxyHijack,
        "/containers/{name:.*}/copy":          proxyContainer,
        "/containers/{name:.*}/exec":          postContainersExec,
        "/exec/{execid:.*}/start":             postExecStart,
        "/exec/{execid:.*}/resize":            proxyContainer,
        "/networks/create":                    postNetworksCreate,
        "/networks/{networkid:.*}/connect":    proxyNetworkContainerOperation,
        "/networks/{networkid:.*}/disconnect": proxyNetworkContainerOperation,
        "/volumes/create":                     postVolumesCreate,
    },
    "PUT": {
        "/containers/{name:.*}/archive": proxyContainer,
    },
    "DELETE": {
        "/containers/{name:.*}":    deleteContainers,
        "/images/{name:.*}":        deleteImages,
        "/networks/{networkid:.*}": deleteNetworks,
        "/volumes/{name:.*}":       deleteVolumes,
    },
}

创建container

就跟在openstack中创建虚拟机一样,跟踪一下swarm中创建container的流程,几乎就可以了解swarm中全部的运行机制了。但读完创建容器的代码,发现相比openstack,swarm中的逻辑还是比较简单,可能还是个新项目,并且在生产环境中使用的还不多,并没有考虑openstack中早已碰到的并发效率等问题,而且调度模块也不像openstack那样是一个独立的服务。但如果真的是弄成独立的服务了,又会失去轻量的优势。

根据上面的URL路由,创建虚拟机由postContainersCreate函数处理,并最终走到cluster对象的createContainer方法处理。

容器名称唯一性判断、生成SwarmID、选取docker主机等操作都是在scheduler的锁中进行,然后调用engine对象的Create方法创建容器,最后在锁中删除cluster对象pendingContainers中对应的中间态容器。

选取docker主机时,新建了一个列表对象nodes,其实是跟engines对应,基本信息都是从engines而来,但会考虑所有正在创建中的容器(计算可用的cpu和memory)。