distribution源码分析(三):registry pull操作详细流程

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)

这里以blobDispatcher为例讲一下,该函数的实现位于registry/handlers/blob.go中,代码如下:

// blobDispatcher uses the request context to build a blobHandler.
func blobDispatcher(ctx *Context, r *http.Request) http.Handler {
    dgst, err := getDigest(ctx)
    if err != nil {

        if err == errDigestNotAvailable {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
            })
        }

        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
        })
    }

    blobHandler := &blobHandler{
        Context: ctx,
        Digest:  dgst,
    }

    return handlers.MethodHandler{
        "GET":    http.HandlerFunc(blobHandler.GetBlob),
        "HEAD":   http.HandlerFunc(blobHandler.GetBlob),
        "DELETE": http.HandlerFunc(blobHandler.DeleteBlob),
    }
}

由以上代码可知,返回的是一个MethodHandler的map,其中不仅包含http.HandlerFunc,还包含一个查找该Handler的string,这主要是因为提供服务会调用接口Handler的ServeHTTP函数提供服务,distribution对ServeHTTP也做了针对于两个参数的更改。该函数的实现位于gorilla/handlers/handlers.go中,其中最重要的代码为:

if handler, ok := h[req.Method]; ok {
        handler.ServeHTTP(w, req)
}

可见,在此将两个参数化为针对于特定于GET、HEAD或是DELETE的处理函数,最终调用的还是http.HandlerFunc,比如当为GET时,调用的是http.HandlerFunc(blobHandler.GetBlob),具体提供服务的就是blobHandler.GetBlob函数,到此已经涉及到了取数据以及之后的传数据,在第六节展开。

5. manifest传输

在第四节中是以blobDispatcher为例展开叙述的,所以到最后是HEAD或者data的传输,如果以imageManifestDispatcher为例展开叙述,那么最后传输的就是镜像的manifest,具体流程相似,可以参照上一小节。
HandlerFunc中最后注入的函数为GetImagemanifest,实现位于registry/handlers/images.go中,它从后端存储中得到manifest数据并且写入http中返回给请求端,整体流程比较清晰,取了数据后直接放到http.ResponseWriter中返回了,所以在此不再赘述,下一节data传输相对来说流程相似,但更加复杂,将在那部分详细介绍整个流程。

// GetImageManifest fetches the image manifest from the storage backend, if it exists.
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
    ctxu.GetLogger(imh).Debug("GetImageManifest")
    manifests, err := imh.Repository.Manifests(imh)
    if err != nil {
        imh.Errors = append(imh.Errors, err)
        return
    }
    var sm *schema1.SignedManifest
    if imh.Tag != "" {
        sm, err = manifests.GetByTag(imh.Tag)
    } else {
        if etagMatch(r, imh.Digest.String()) {
            w.WriteHeader(http.StatusNotModified)
            return
        }
        sm, err = manifests.Get(imh.Digest)
    }
    if err != nil {
        imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
        return
    }
    // Get the digest, if we don't already have it.
    if imh.Digest == "" {
        dgst, err := digestManifest(imh, sm)
        if err != nil {
            imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
            return
        }
        if etagMatch(r, dgst.String()) {
            w.WriteHeader(http.StatusNotModified)
            return
        }
        imh.Digest = dgst
    }
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.Header().Set("Content-Length", fmt.Sprint(len(sm.Raw)))
    w.Header().Set("Docker-Content-Digest", imh.Digest.String())
    w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
    w.Write(sm.Raw)
}

6. data传输

第四节最后已经说到,HandlerFunc里最后注入的函数为blobHandler.GetBlob,该函数位于registry/handlers/blob.go中,如下所示。
其中,desc, err := blobs.Stat(bh, bh.Digest)实现位于registry/storage/blobstore.go中,返回的是一个distribution.Descriptor,该结构包括MediaType、Size和Digest,可以用来fetch、store和target任何blob。最开始的代码
path, err := pathFor(blobDataPathSpec{digest: dgst,})根据digest返回blob的路径,该路径是从/docker/registry/v2开始的,不包括在yaml文件中配置的那部分前缀。这个函数并没有直接和磁盘交互,因为distribution将manifest、tag、blob等内容存入特定的目录,所以在这里只是根据用户要提取的内容组合出相应的目录返回。但是在pathFor之后的代码确实通过driver和磁盘交互了,返回了文件的大小以及建立时间等信息,针对于本地文件系统,函数的实现位于registry/storage/driver/filesystem/driver.go中,该函数组建了blob的全路径并通过读磁盘确立了路径的有效性。

// GetBlob fetches the binary data from backend storage returns it in the response.
func (bh *blobHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
    context.GetLogger(bh).Debug("GetBlob")
    blobs := bh.Repository.Blobs(bh)
    desc, err := blobs.Stat(bh, bh.Digest)
    if err != nil {
        if err == distribution.ErrBlobUnknown {
            bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown.WithDetail(bh.Digest))
        } else {
            bh.Errors = append(bh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
        }
        return
    }
    if err := blobs.ServeBlob(bh, w, r, desc.Digest); err != nil {
        context.GetLogger(bh).Debugf("unexpected error getting blob HTTP handler: %v", err)
        bh.Errors = append(bh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
        return
    }
}

再继续向下讲之前先介绍一个结构体fileReader,提供了一个存储在storagedriver中的文件的read seeker接口。可见该结构体记录了后端使用的存储方式、文件路径以及大小等。

type fileReader struct {
    driver storagedriver.StorageDriver
    ctx context.Context
    // identifying fields
    path string
    size int64 // size is the total size, must be set.
    // mutable fields
    rc     io.ReadCloser // remote read closer
    brd    *bufio.Reader // internal buffered io
    offset int64         // offset is the current read offset
    err    error         // terminal error, if set, reader is closed
}

回到GetBlob函数中,最后的blobs.ServeBlob的实现位于registry/storage/blobserver.go中,下面的代码列出比较重要的部分,newFileReader返回一个记录了存储方式、文件路径以及大小等组成的fileReader结构,后面就可以根据这个调用针对于特定文件系统的read函数了。
得到fileReader后就是设置http.ResponseWriter的头部信息了,最后一步用的是http.ServeContent函数,将具体blob的内容返回给docker端。
golang
// Fallback to serving the content directly.
br, err := newFileReader(ctx, bs.driver, path, desc.Size)
if err != nil {
return err
}
defer br.Close()
w.Header().Set("ETag", fmt.Sprintf(`"%s"`, desc.Digest)) // If-None-Match handled by ServeContent
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.f", blobCacheControlMaxAge.Seconds()))
if w.Header().Get("Docker-Content-Digest") == "" {
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
}
if w.Header().Get("Content-Type") == "" {
// Set the content type if not already set.
w.Header().Set("Content-Type", desc.MediaType)
}
if w.Header().Get("Content-Length") == "" {
// Set the content length if not already set.
w.Header().Set("Content-Length", fmt.Sprint(desc.Size))
}
http.ServeContent(w, r, desc.Digest.String(), time.Time{}, br)

7. 总结

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

8. 作者介绍

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

9. 参考文献

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值