Docker registry工作机制简介

1 Registry简介

Docker registry是存储docker image的仓库,它在docke生态环境中的位置如下图所示,运行docker pushdocker pulldocker search时,实际上是通过docker daemon与docker registry通信。

2 Registry源码结构分析

Registry主要由以下两部分组成:(源码地址:https://github.com/docker/docker-registry)

  1. “前端”:registry基于Flask框架,为daemon提供REST API接口。代码主要在./docker-registry目录下。了解Flask框架对理解整个代码框架有很大的帮助。
  2. “后台”:storage驱动,为respositories和images提供存储的地方,并为“前端”提供一些接口。代码主要在./depends/docker-registry-core/docker_registry/drivers目录下。

Storage驱动需要提供的接口有:get_contentput_contentstream_readstream_writelist_directoryexistsremoveget_size,其它的在driver.Base里(如果为一个新的存储(如:HDFS)写驱动,可以从继承driver.Base类开始,编写驱动时需要注意:”Drivers are expected to receive bytes and to return bytes. Don’t try to decode or encode content”)。put_contentstream_write都是写文件,区别在于前者适合写小文件,后者适合写大文件(stream_write的实现使用了关键字yield),get_contentstream_read分别适合读取小文件和大文件。

有兴趣的同学可以看看file.py里的实现,当然这只是基于本地文件系统最简单的实现——普通的文件操作,例:get_content的定义如下:

@lru.get
 def get_content(self, path):
      path = self._init_path(path)
      try:
           with open(path, mode='rb') as f:
           d = f.read()
      except Exception:
           raise exceptions.FileNotFoundError('%s is not there' % path)

      return d

值得一提的是get_content前的装饰器@lru.get。它会以path为key从配置好的Redis中去取值,如果取到则直接返回,否则从文件去读。当然,put_content前会有一个@lru.set,会以(path,content)的形式写到Redis中。@lru.get@lru.set@lru.remove./depends/docker-registry-core/docker_registry/core/lru.py提供。(注:需要将Redis配置为LRU模式:http://redis.io/topics/lru-cache)。

还有重要的一点是:registry的“前端”是无状态的,状态(数据)都存储在storage和Redis上。

3 运行 registry

可以直接通过官方的registry image运行:

$ docker run -p 5000:5000  STORAGE_PATH=/tmp/registry -v /home/tdwadmin/registry:/tmp/registry registry`

以上命令的作用是把registry server运行在docker里,通过docker ps可以看到。STORAGE_PATH指定image存放的路径,但这个目录存在于container里,“-v /home/tdwadmin/registry:/tmp/registry”的作用是将该container里的目录/tmp/registry映射到本地目录/home/tdwadmin/registry(请确保该目录存在,最好是空目录,方便观察)。通过配置文件或命令行选项可以调整该命令的行为,关于配置文件的内容可见https://github.com/docker/docker-registry/blob/master/README.md。

打开http://127.0.0.1:5000/就可以看到界面了,不过,界面上什么也没有 :)

通过其打印的日志,我们能看出它做了哪些操作,比如:如果打印 /GET /v1/images/…/ancestry,我们就能知道它调用了相应的函数get_image_ancestry

4 Respositories 在 storage 上是怎么存储的

在分析docker pull的机制之前,我们需要了解:docker respository由哪些部分组成,在storage上是怎么存储的(以下内容以file.py的实现举例说明)。

我们刚才启动的registry里的storage是空的,我们可以向里面push一些image

// 从hub.docker.io上pull ubuntu
$ docker pull ubuntu:14.04                 

// 打上我们的标签,重要的是加上标识127.0.0.1:5000
$ docker tag ubuntu:14.04 127.0.0.1:5000/hello   

// daemon通过"127.0.0.1: 5000/hello"中的'.'和':'识别出这是一个发往127.0.0.1:5000的请求,因此会将该push请求发送给我们启动的registry
$ docker push 127.0.0.1:5000/hello           
The push refers to a repository [127.0.0.1:5000/hello] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/hello (1 tags)
511136ea3c5a: Image successfully pushed
d497ad3926c8: Image successfully pushed
ccb62158e970: Image successfully pushed
e791be0477f2: Image successfully pushed
3680052c0f5c: Image successfully pushed
22093c35d77b: Image successfully pushed
5506de2b643b: Image successfully pushed
Pushing tag for rev [5506de2b643b] on {http://127.0.0.1:5000/v1/repositories/hello/tags/latest}

我们只push了一个image,但是显示的是有多个image被push成功了?下面会解释。

现在我们可以看看/home/tdwadmin/registry上是怎么存储它的。

[tdwadmin@c27 registry]$ tree -L 2
.
| ---- images
|   |---- 22093c35d77bb609b9257ffb2640845ec05018e3d96cb939f68d0e19127f1723
|   |---- 3680052c0f5cf8ecb86ddf4d6ed331c89cdb691554572a80ec04724cf6ee9436
|   |---- 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
|   |---- 5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5
|   |---- ccb62158e97068cc05b2f0927a8acde14c64d0d363cc448238357fe221a39699
|   |---- d497ad3926c8997e1e0de74cdd5285489bb2c4acd6db15292e04bbab07047cd0
|   `---- e791be0477f28fd52f7609aed81733427d4cc0da620962d072e18ebcb32720a4
`---- repositories
`---- library

[tdwadmin@c27 registry]$ tree repositories/library/
repositories/library/
`---- hello
|---- _index_images
|---- json
|---- tag_latest
`---- taglatest_json

我们可以看到hello在respoitories/library下,由_index_images、json等组成。

_index_images记录了hello(hello所有的tag)由哪些image组成的,记录的是这些image的id。一个hello会有多个image,是因为docker image用的层级(layer)的概念,如下图所示,除了Base Image,每个image都是基于其他image而制成,这就是Dockerfile里最开始那条语句如“From ubuntu”的作用,告诉docker它将基于ubuntu而制成(事实上ubuntu也是基于其它image制成的)。引入层级的概念有什么好处呢?假设image ‘foo’是基于image A、image B、image C的,而image ‘bar’是基于image A、image B、image D的,这样在pull image ‘foo’后(此时会将image A、B、C都pull到本地),如果再pull image ‘bar’,此时就只需要pull image D了。而存储时也会有相同的好处。

respository hello上可能有多个tag,默认是hello:latest,我们可以再向registry push一个带tag的image hello:test(docker push 127.0.0.1:5000/hello:test),然后我们可以看到hello/目录下多了一个文件tag_test,它记录的是hello:test对应的image id,而tag_latest对应的是hello(也就是hello:latest)的image id。

每个image id都对应着一个image,它们在images/目录下有相应的目录。以22093c35d77bb609b9257ffb2640845ec05018e3d96cb939f68d0e19127f1723为例,它有以下内容:

[tdwadmin@c27 images]$ ls 22093c35d77bb609b9257ffb2640845ec05018e3d96cb939f68d0e19127f1723/
ancestry _checksum json layer

ancestry:记录该image依赖的image,如果该文件里记录的内容只有它自己的id,说明它不依赖于别的image

_checksum: image的sha256的检验值

json: image的一些描述信息

layer:实实在在起作用的数据、程序。它们是压缩后的。

现在,我们可以想想怎么根据一个respository来获取image了:(以下hello:test为例说明)

  1. repositories/library/hello/tag_test,取出它对应的image id: 5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5
  2. 获取image 5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5的内容,包括layer和ancestry,并根据ancestry得到它依赖的image,采取广度或深度优先的方法遍历这些image以及它们依赖的image(当然还有它们的layer)

5 Docker pull的工作机制

我们将以docker pull为例说明registry的作用,docker pushdocker search的工作机制类似。

docker pull 127.0.0.1:5000/hello时,daemon会首先搜索本地是否有相应的image,如果没有则会向registry发送REST请求(daemon通过”127.0.0.1: 5000/hello”中的’.’和’:’识别出这是一个发往127.0.0.1:5000的请求)。

1) Get /v1/repositories/library/hello/images

获取hello的_index_images文件的内容。

相应的处理函数如下所示:

@app.route(‘/v1/repositories//images’, methods=[‘GET’])

@toolkit.parse_repository_name

@toolkit.requires_auth

@mirroring.source_lookup(index_route=True)

def get_repository_images(namespace, repository)

2) Get /v1/repositories/library/hello/tags

获取hello下的所有tag和其相应的image id(通过读取文件tag_tagname得到),对于image hello,返回的是 {latest: 5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5, test: 5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5}。

相应的处理函数如下所示:

@app.route(‘/v1/repositories//tags’, methods=[‘GET’])

@toolkit.parse_repository_name

@toolkit.requires_auth

@mirroring.source_lookup_tag

def _get_tags(namespace, repository)

3) Get

/v1/images/5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5/layer

获取layer的内容了store.stream_read(path, bytes_range)(byte_range是由docker daemon指定的,读取指定范围的内容),registry提供了其他两种方式供我们选择:提供了nginx_x_accel_redirect(返回一个重定向到ngix的url,然后从那获取内容)和storage_redirect(返回一个重定向的url,从那获取内容,“storage_redirect: Redirect resource requested if storage engine supports this, e.g. S3 will redirect signed URLs, this can be used to offload the server.”)。

相应的处理函数如下所示:

@app.route(‘/v1/images//layer’, methods=[‘GET’])

@toolkit.requires_auth

@require_completion

@set_cache_headers

@mirroring.source_lookup(cache=True, stream=True)

def get_image_layer(image_id, headers):

4) Get

/v1/images/5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5/ancestry

获取依赖的image id。根据这些id再去遍历它们所依赖的image,并获取layer。

相应的处理函数如下所示:

@app.route(‘/v1/images//ancestry’, methods=[‘GET’])

@toolkit.requires_auth

@require_completion

@set_cache_headers

@mirroring.source_lookup(cache=True, stream=False)

def get_image_ancestry(image_id, headers)

注:为了简便,以上省略了一些过程,如:如果daemon发现该image已在本地则不用pull了。

6 总结

本文主要介绍了docker registry的源码结构及工作机制,对于读者了解registry有一定的帮助。后面将继续介绍我们对registry做的一些改进和优化。

7 参考资料

Registry源码:https://github.com/docker/docker-registry

Flask快速入门:http://flask.pocoo.org/docs/0.10/quickstart/

Docker源码分析:http://www.infoq.com/cn/articles/docker-source-code-analysis-part1

Where are docker images stored: http://blog.thoward37.me/articles/where-are-docker-images-stored/ges-stored/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

龚军的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏aoho求索

Redis Cluster深入与实践

1. redis介绍 www.redis.io redis是一个基于内存的K-V存储数据库。支持存储的类型有string,list,set,zset(sorte...

466120
来自专栏FreeBuf

通过浏览器缓存来bypass nonce script CSP

最近看了去年google团队写的文章CSP Is Dead, Long Live CSP!,对csp有了新的认识,在文章中,google团队提出了nonce...

281100
来自专栏java技术学习之道

10大必备的Intellij插件,大幅提高你的工作效率

40430
来自专栏维C果糖

详述 IntelliJ IDEA 创建 Maven 项目及设置 java 源目录的方法

Maven 是一个优秀的项目管理工具,它为我们提供了一个构建完整的生命周期框架。现在,就让我们一起看看如何利用 IntelliJ IDEA 快速的创建 Mave...

95190
来自专栏木头编程 - moTzxx

Laravel 文件上传功能实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011415782/article/de...

2.3K40
来自专栏北京马哥教育

这8种命令都不会,还算什么Linux运维!

01 查看系统内核版本 显示了系统名称(CentOS)和内核版本(release 6.5) The file /etc/issue is a text file...

36870
来自专栏云计算

Kubernetes的服务网格(第2部分):Pod是最基本的操作单元,但不是最好的部署单元

在本系列文章的上一篇中,细心的读者注意到,linkerd是使用DaemonSet而不是作为挎斗(SideCar)进程安装的(关于SideCar的概念及翻译引用自...

25090
来自专栏Java工程师日常干货

深入浅出Nginx前言反向代理服务器?Nginx的Master-Worker模式我们的主战场:nginx.conf

Nginx是一款轻量级的Web服务器、反向代理服务器,由于它的内存占用少,启动极快,高并发能力强,在互联网项目中广泛应用。

16230
来自专栏企鹅号快讯

如何通过Smem命令行检查Ubuntu上的内存使用情况

如何检查Ubuntu Linux上的内存使用情况,我们可以安装并使用Smem内存报告工具来显示Ubutnu Linux系统上的内存使用情况。 Smem是一个命令...

25780
来自专栏纯洁的微笑

Java程序员必备的Intellij插件

支持lombok的各种注解,从此不用写getter setter这些 可以把注解还原为原本的java代码 非常方便

14520

扫码关注云+社区

领取腾讯云代金券