您的位置:首页 > 其它

Distribution源码分析(四):registry push操作详细流程

2016-01-17 17:13 423 查看

1. 前言

仓库的设计初衷就是为了存储镜像数据并提供上传下载镜像服务的,所以与镜像存储以及镜像数据传输是非常重要的方面。本节中将对镜像存储以及与docker端的数据传输过程做出详细解析。

2. 本文分析内容安排

建立连接

接受request并分发到handler分发以及proxy

manifest传输

data传输

3. 建立连接

建立连接前的初始化工作主要是对于Registry.App的初始化,初始化的流程如图3.1所示:



图3.1 建立连接流程

上述流程图是registry初始化然后提供给docker http服务的全过程,其中最后三步之前对应的是distribution/registry/registry.go中Cmd变量定义中的
registry, err := NewRegistry(ctx, config)
这行代码,主要是对registry本身的初始化,包括Handler、storage、endpoint等一切和镜像管理相关的结构;最后三步是根据配置好的registry调用http Listener 和 Server提供服务,对应于distribution/registry/registry.go中Cmd变量定义中的
registry.ListenAndServe()
。实际上,最早接收到docker端请求在后三步,这三步中包括了接收请求以及返回结果的接口。具体流程是Listener接收到请求后,根据之前NewRegistry配置的Handler调用相应的函数到注册的storage中读取数据,然后通过Serve接口将结果返回给docker端。可见,将Listener作为切入点研究distribution代码,便可以一步步弄清楚整个流程。

ListenAndServer函数在系列(二)中已经介绍过了,主要语句是
ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr)
监听连接,之后
registry.server.Serve(ln)
,建立持续连接并根据server中注册的Handler、storage等提供服务。Serve是net/http包中的函数,会通过route调用恰当的Handler来提供服务。至此,可以说建立连接的过程已经完成,接下来是收到request并分发到相应handler提供服务了。

4. 接受request并分发到handler分发以及proxy

注册handler并提供服务是net/http包提供的原生功能,distribution直接利用了go语言的该功能。

4.1 go语言net/http注入Handler原生特性

func ListenAndServe(addr string, handler Handler) error
该方法用于在指定的 addr 地址进行监听,然后调用服务端处理程序来处理传入的链接请求。第二个参数表示服务端处理程序,如果为空,意味着调用http.DefaultServeMux进行处理,而服务端编写的业务逻辑处理程序http.Handle()或http.HandleFunc()默认注入http.DefaultServeMux中,示例如下:

http.Handle("/foo",fooHandler)
http.HandleFunc("/bar",func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))


也可以自己重新定义http.Server,将Handler直接写入Serve中,这样非但不用再调用http.Handle或者http.HandleFunc注册而且可以更多地控制服务端的行为,distribution源码就是这么做的,在NewRegistry函数中就已经重新定义了Server,将handler注入了,并添加了很多控制行为。这里先不说distribution,而是举个例子说明下用法:

s := &http.Server{
Addr:                    ":8080",
Handler:               myHandler,
ReadTimeout:      10*time.Second,
WriteTimeout:     10*time.Second,
MaxHeaderBytes:   1<<20,
}
log.Fatal(s.ListenAndServe())


4.2 distribution中Handler注入实现

这里从后向前推,在distribution/registry/registry.go中,NewRegistry在最后返回之前的语句为

server := &http.Server{
Handler: handler,
}


可见,是对Server做了重新定义,主要是注入了Handler处理函数,处理函数为handler,定义在
handler := configureReporting(app)
,在该函数中最重要的一行代码为
var handler http.Handler = app
,因为http.Handler接口只有ServeHTTP一个函数,handlers.App实现了该函数,所以便实现了http.Handler接口。可知,app即为distribution注入的接收请求后的处理函数。具体的注册是在NewApp中的这几行

// Register the handler dispatchers.
app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
return http.HandlerFunc(apiBase)
})
app.register(v2.RouteNameManifest, imageManifestDispatcher)
app.register(v2.RouteNameCatalog, catalogDispatcher)
app.register(v2.RouteNameTags, tagsDispatcher)
app.register(v2.RouteNameBlob, blobDispatcher)
app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)


5. data传输

push镜像时docker端会一层层的从文件系统中读取数据,构建整个image的Manifest后上传该层内容,当所有层都上传完成后,整个manifest便构建成功了。其中,在执行上传data操作之前docker会先先判断是否已经上传了该层数据,如果没有的话

if !exists {
if pushDigest, err := p.pushV2Image(p.repo.Blobs(context.Background()), layer); err != nil {
return err
} else if pushDigest != dgst {
// Cache new checksum
if err := p.graph.SetLayerDigest(layer.ID, pushDigest); err != nil {
return err
}
dgst = pushDigest
}
}


在pushV2Image函数中,docker会先从底层存储中读取镜像数据,然后上传到registry,上传的过程中会计算hash返回给pushDigest,如果跟从磁盘读到的hash不同,会将磁盘上存储到hash设定为刚计算出的。同时会触发registry端的StartBlobUpload句柄开始data传输,之后的progressreader后的io.Copy操作会触发registry端的PatchBlobData句柄传递具体data数据,之后的
layerUpload.Commit(context.Background(), distribution.Descriptor{Digest: dgst})
调用registry端的PutBlobUploadComplete句柄完成上传。在registry端的操作主要是接收docker端传来的数据并存入后端存储。

6. manifest传输

完成所有镜像层的传输后docker端执行的最后的代码是上传manifest数据,代码如下:

logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())
signed, err := manifest.Sign(m, p.trustKey)
if err != nil {
return err
}

manifestDigest, manifestSize, err := digestFromManifest(signed, p.repo.Name())
if err != nil {
return err
}
if manifestDigest != "" {
out.Write(p.sf.FormatStatus("", "%s: digest: %s size: %d", tag, manifestDigest, manifestSize))
}

manSvc, err := p.repo.Manifests(context.Background())
if err != nil {
return err
}
return manSvc.Put(signed)


这段代码中,manifest.Sign将已经完成构建的manifest用特定的私钥签名,从而转变为SignedManifest;digestFromManifest计算manifest的hash值;r.repo.Manifests返回的是distribution/registry/client/repository.go中的manifests;manSvc.Put传递manifest数据并触发registry端的registry/handlers/images.go中的PutImageManifest句柄。

7. 总结

以上内容包括了监听docker请求,分配Handler从registry存储后端读取数据,读取时会根据不同的后端调用相应的storagedriver,读取数据之后将数据返回给docker端,可知,内容已经涵盖了push操作的所有部件,读懂后便对distribution的架构有所了解了。

8. 作者介绍

梁明远,国防科大并行与分布式计算国家重点实验室应届研究生,14年入学伊始便开始接触docker,准备在余下的读研时间在docker相关开源社区贡献自己的代码,毕业后准备继续从事该方面研究。邮箱:liangmingyuanneo@gmail.com

9. 参考文献

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: