上篇文章写了 Docker Registry Auth 的流程,这里研究一下 Docker Registry Storage。
Docker 镜像仓库的支持支持多种储存类型,主要分为两大类
- 本地储存
- 远程储存
本地储存为 filesystem 和 inmemory,然后远程储存就主要是一些云计算厂商的储存业务了。无论那种储存都是基于 StorageDriver 这个接口的实现。
储存结构
.
└── docker
└── registry
└── v2
├── blobs
│ └── sha256
│ ├── 07
│ │ └── 0766572b4bacfaee9a8eb6bae79e6f6dbcdfac0805c7c6ec8b6c2c0ef097317a
│ ├── 9a
│ │ └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
│ └── 9c
│ └── 9ca846b27f6e92f0739af5bba5509357b52be0ce0dd02d216f4dccdacd695a8a
└── repositories
├── alpine
│ ├── _layers
│ │ └── sha256
│ │ ├── 0766572b4bacfaee9a8eb6bae79e6f6dbcdfac0805c7c6ec8b6c2c0ef097317a
│ │ └── 9ca846b27f6e92f0739af5bba5509357b52be0ce0dd02d216f4dccdacd695a8a
│ ├── _manifests
│ │ ├── revisions
│ │ │ └── sha256
│ │ │ └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
│ │ └── tags
│ │ └── 3.4
│ │ ├── current
│ │ └── index
│ │ └── sha256
│ │ └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
│ └── _uploads
└── library
└── alpine
├── _layers
│ └── sha256
│ ├── 0766572b4bacfaee9a8eb6bae79e6f6dbcdfac0805c7c6ec8b6c2c0ef097317a
│ └── 9ca846b27f6e92f0739af5bba5509357b52be0ce0dd02d216f4dccdacd695a8a
├── _manifests
│ ├── revisions
│ │ └── sha256
│ │ └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
│ └── tags
│ └── 3.4
│ ├── current
│ └── index
│ └── sha256
│ └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
└── _uploads
这个也就是就是官方 registry 镜像的默认储存目录 /var/lib/registry
的文件结构。上述就是镜像 alpine:3.4 在镜像仓库中的储存结构,docker/registry/v2/repositories/library/alpine
表示镜像的 repository,也是我们俗称的镜像,然后在 docker/registry/v2/repositories/library/alpine/_manifests/tags/
下面是镜像的所有 tag 信息,tag 目录下存的是 tag 的索引,根据索引,再去 docker/registry/v2/blob
目录下就可以一级一级往下找到对应的镜像层了。一般情况下,我们会将镜像分组在相同的 namespace 下,也就是上述的 library
,所以其实在官方的 hub 上面,官方的镜像都是数据 library
这个 namespace 下面的,而每个用户就在自己的 namespace 下,也就是 project。
StorageDriver
type StorageDriver interface {
// Name returns the human-readable "name" of the driver, useful in error
// messages and logging. By convention, this will just be the registration
// name, but drivers may provide other information here.
Name() string
// GetContent retrieves the content stored at "path" as a []byte.
// This should primarily be used for small objects.
GetContent(ctx context.Context, path string) ([]byte, error)
// PutContent stores the []byte content at a location designated by "path".
// This should primarily be used for small objects.
PutContent(ctx context.Context, path string, content []byte) error
// Reader retrieves an io.ReadCloser for the content stored at "path"
// with a given byte offset.
// May be used to resume reading a stream by providing a nonzero offset.
Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error)
// Writer returns a FileWriter which will store the content written to it
// at the location designated by "path" after the call to Commit.
Writer(ctx context.Context, path string, append bool) (FileWriter, error)
// Stat retrieves the FileInfo for the given path, including the current
// size in bytes and the creation time.
Stat(ctx context.Context, path string) (FileInfo, error)
// List returns a list of the objects that are direct descendants of the
//given path.
List(ctx context.Context, path string) ([]string, error)
// Move moves an object stored at sourcePath to destPath, removing the
// original object.
// Note: This may be no more efficient than a copy followed by a delete for
// many implementations.
Move(ctx context.Context, sourcePath string, destPath string) error
// Delete recursively deletes all objects stored at "path" and its subpaths.
Delete(ctx context.Context, path string) error
// URLFor returns a URL which may be used to retrieve the content stored at
// the given path, possibly using the given options.
// May return an ErrUnsupportedMethod in certain StorageDriver
// implementations.
URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error)
}
如果要自己实现镜像的 storage,就得按照 StorageDriver
。
对于镜像的 push 过程,最主要的函数就是 StorageDriver.Write
,这个主要是用来上传大文件的,而 StorageDriver.PutContent
就是一些小文件的上传,比如说 tag 的 index。
StorageDriver.Write
返回一个 FileWriter
,这个也需要自己实现,当有文件上传时,会不停的调用 FileWriter.Write
,当文件上传完毕,会调用 FileWriter.Commit
。
Qiniu Storage
官方并没有提供七牛的 storage,但是因为有 StorageDriver
,所以也不妨碍我们自己撸一个使用七牛作为储存的私有镜像仓库来
初始化的 driver 的过程,参照阿里云的 OSS 过程,目测阿里云的 OSS 也是参照 AWS 来的,而且阿里云的 OSS API 接口,跟 AWS 真是神似了。
StorageDriver.GetContent
func (d *driver) GetContent(ctx dockerContext.Context, path string) ([]byte, error) {
baseURL := kodo.MakeBaseUrl(d.domain, d.qiniuPath(path))
policy := kodo.GetPolicy{}
privateURL := d.client.MakePrivateUrl(baseURL, &policy)
resp, err := http.Get(privateURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, storagedriver.PathNotFoundError{Path: path}
}
return ioutil.ReadAll(resp.Body)
}
StorageDriver.GetContent
直接使用七牛的 Golang SDK ,根据 path
生成对应的文件的下载路径,然后下载后,直接返回 []byte
就行了。
StorageDriver.Reader
func (d *driver) Reader(ctx dockerContext.Context, path string, offset int64) (io.ReadCloser, error) {
contents, err := d.GetContent(ctx, path)
if err != nil {
return nil, err
}
return ioutil.NopCloser(bytes.NewReader(contents[offset:])), nil
}
StorageDriver.Reader
应该是返回一段 offset
偏移量的文件内容,但是七牛貌似并没有提供像阿里云一样的 range
下载的方式,于是只能将整个文件下载下来,然后再手动切分文件了,这样做,要是镜像的某一层很大,会占用很多内存。
StorageDriver.PutContent
func (d *driver) PutContent(ctx dockerContext.Context, path string, contents []byte) error {
putExtra := kodo.PutExtra{}
body := bytes.NewReader(contents)
err := d.bucket.Put(context.Background(), nil, d.qiniuPath(path), body, int64(len(contents)), &putExtra)
return err
}
StorageDriver.PutContent
也是直接使用七牛的 SDK 上传小文件。
StorageDriver.Writer
func (d *driver) Writer(ctx dockerContext.Context, path string, append bool) (storagedriver.FileWriter, error) {
var w *writer
qiniuPath := d.qiniuPath(path)
if !append {
w = &writer{
key: qiniuPath,
driver: d,
buffer: make([]byte, 0, maxChunkSize),
uploadCtxList: make([]string, 0, 1),
}
d.writerPath[qiniuPath] = w
} else {
w = d.writerPath[qiniuPath].(*writer)
w.closed = false
}
return w, nil
}
由于七牛的切片上传方式与阿里云提供的切片有点不同,七牛的需要客户端自己记录上传的片段,所以不得不在 StorageDriver
这里做手脚了,这里将 FileWriter
保存在 StorageDriver
上,以便下一次使用。
StorageDriver.Move
func (d *driver) Move(ctx dockerContext.Context, sourcePath string, destPath string) error {
if err := d.bucket.Move(context.Background(), d.qiniuPath(sourcePath), d.qiniuPath(destPath)); err != nil {
return err
}
return nil
}
StorageDriver.Move
在这里的作用是因为 registry 在上传时,会先将文件放在 docker/registry/v2/repositories/library/alpine/_uploads
目录下面,然后等文件正式上传完毕后,在移动到对应的镜像目录下面。所以也需要 StorageDriver.Delete
删除上传的临时文件。
StorageDriver.Stat
func (d *driver) Stat(ctx dockerContext.Context, path string) (storagedriver.FileInfo, error) {
entries, commonPrefixes, _, err := d.bucket.List(context.Background(), d.qiniuPath(path), "/", "", 1)
if err != nil && len(entries) == 0 {
if err == io.EOF {
return nil, storagedriver.PathNotFoundError{Path: path}
}
return nil, err
}
fi := storagedriver.FileInfoFields{
Path: path,
}
if len(entries) == 1 {
if entries[0].Key != d.qiniuPath(path) {
fi.IsDir = true
} else {
fi.IsDir = false
fi.Size = entries[0].Fsize
fi.ModTime = time.Unix(0, entries[0].PutTime)
}
} else if len(commonPrefixes) == 1 {
fi.IsDir = true
} else {
return nil, storagedriver.PathNotFoundError{Path: path}
}
return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil
}
StorageDriver.Stat
可以在上传时判断文件是否已经存在。
FileWriter.Writer
func (w *writer) Write(p []byte) (int, error) {
if w.closed {
return 0, fmt.Errorf("already closed")
} else if w.committed {
return 0, fmt.Errorf("already committed")
} else if w.cancelled {
return 0, fmt.Errorf("already cancelled")
}
if err := w.flushBuffer(); err != nil {
return 0, err
}
w.buffer = append(w.buffer, p...)
if len(w.buffer) >= maxChunkSize {
if err := w.flushBuffer(); err != nil {
return 0, err
}
}
w.size += int64(len(p))
return len(p), nil
}
func (w *writer) flushBuffer() error {
for len(w.buffer) >= maxChunkSize {
if err := w.mkblk(bytes.NewReader(w.buffer[:maxChunkSize]), maxChunkSize); err != nil {
return err
}
w.buffer = w.buffer[maxChunkSize:]
}
return nil
}
由于七牛切片上传每块大小最大只能为 4MB ,而 registry 每次上传的一段的大小又是不确定的,所以此处将 registry 每段上传的内容存到 buffer
中,然后当 buffer
里的内容达到 4MB,就将 buffer
中的 4MB 上传到七牛。
FileWriter.Commit
func (w *writer) Commit() error {
defer func() {
w.driver.RemoveWriter(w.key)
}()
if w.closed {
return fmt.Errorf("already closed")
} else if w.committed {
return fmt.Errorf("already committed")
} else if w.cancelled {
return fmt.Errorf("already cancelled")
}
if err := w.flushBuffer(); err != nil {
return err
}
if len(w.buffer) > 0 {
if err := w.mkblk(bytes.NewReader(w.buffer), len(w.buffer)); err != nil {
return err
}
}
if err := w.mkfile(); err != nil {
return err
}
w.committed = true
return nil
}
最终上传完毕后,在 FileWriter.Commit
需要将driver
上的FileWriter
给删掉,不然会内存泄露了,在最后 commit
时,再次确认 buffer
中的内容已经全部上传到了七牛,然后调用七牛的 mkfile
合并块成文件,想当初去七牛面试技术支持的岗位时,还问过我这个问题,当时答得并不咋地。
最后
最终需要在 cmd/registry/main.go
的文件中初始化我们自己的 driver
。
完整的七牛 Storage 在这里,https://gist.github.com/cloverstd/813a0df6b9eacc459dd84509e902e744 。