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 条评论
登录 后参与评论

相关文章

来自专栏张戈的专栏

Linux运维工程师:30道面试题整理

前段时间,我在准备面试的时搜到的一套 Linux 运维工程师面试题,感觉比较全面,一直保存在草稿,刚在整理后台时翻了出来,干脆就发出来好了,以备不时之需。 1....

7575
来自专栏SeanCheney的专栏

Python模拟登陆 —— 征服验证码 9 微博weibo.com

登录界面 抓包分析可以使用Http Analyzer,Filders,但是看起来很复杂,还是使用火狐好(chrome远远没有火狐好用)。 首先,在输入用户名后,...

26710
来自专栏PHP在线

PHP实现文件下载断点续传

如果我们的网站提供文件下载的服务,那么通常我们都希望下载可以断点续传(Resumable Download),也就是说用户可以暂停下载,并在未来的某个时间从暂停...

1467
来自专栏小灰灰

Quick-Task 动态脚本支持框架之任务动态加载

前面几篇博文分别介绍了整个项目的基本架构,使用说明,以及整体框架的设计与实现初稿,接下来则进入更细节的实现篇,将整个工程中核心实现捞出来,从为什么这么设计到最终...

1032
来自专栏一个会写诗的程序员的博客

RequireJS极简入门教程RequireJS核心功能:HOW TOmain.js使用 shim

随着网站功能逐渐丰富,网页中的js也变得越来越复杂和臃肿,原有通过script标签来导入一个个的js文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作...

893
来自专栏Dawnzhang的开发者手册

IDEA 代码规范插件

这时候就必须得有一些代码规范,来统一团队代码;IEDA中,有一个插件(Alibaba Java Coding Guidelines)帮我们很好的解决了这一问题;

1352
来自专栏python学习路

三、Requests库的使用

requests 的底层实现其实就是 urllib3  Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。 学过关于urll...

36710
来自专栏PhpZendo

PHP 文件系统完全指南

今天我们将开启一个新的探索旅程,深入到 PHP 文件系统中,系统的学习和掌握 PHP 文件系统的基本使用。

702
来自专栏ionic3+

【技巧】Ionic3多文件上传

文件上传,我们一般需要和本地文件打交道,先安装file插件(全称cordova-plugin-file),

994
来自专栏魏艾斯博客www.vpsss.net

FTP 软件使用教程

1885

扫码关注云+社区