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.com9. 参考文献
相关文章推荐
- HDU 1124 Factorial&&nyoj 84 阶乘的 0【数学】
- 华为oj 挑7
- 树
- page curling(翻页 翻屏)
- JSP内置对象(包括request和response)以及解决url传中文参数出现乱码问题
- 富文本编辑器 - wangEditor 插入代码
- 16-01-17 LinearLayout 属性 及示例
- C/C++ 名正则言顺
- C/C++ 名正则言顺
- iOS学习之七牛云存储应用
- I.MX6 Linux udev porting
- struts2 纯注解 却提示There is no Action mapped for namespace
- 一致性哈希算法及其在分布式系统中的应用
- cocos进入后台
- SQL--子查询
- Delphi IOS 后台定时器
- View Programming Guide 总结
- Java Serializable(序列化)的理解和总结
- 加一段代码,让vuforia识别度更高
- 修练8年C++面向对象程序设计之体会 之杂项