基于kubernetes的Docker Registry的高可用部署

写在前面

在kubernetes集群中运维生成环境的服务已经长达半年多时间,我们遇到了很多问题,也踩到了很多坑,其中因为 Docker Registry 的故障而导致的不可用事件还是挺多的,这些问题常常被用户埋怨。

Docker Registry 作为镜像仓库、数据中心,在整个服务发布流程中是异常关键的一环。由于之前初期我们搭建的 Docker Registry 是通过 docker run 跑在单机的方式,这种情况下不仅有单点问题,还面临着磁盘损坏和镜像丢失的危险性。

后来为了提高平台的稳定性和可靠性,也为了我的毕业论文,特地的花时间来调研 Docker Registry 的部署和设计方案,这个方案不仅采纳了开源社区和其他公司的部署策略,同时也结合了本公司内部的基础上而设计的一套高可用的 Docker Registry 方案。

调研初期

在2017年3月份时候有幸参加了 ebay 公司举办了亿贝TechDay活动,在这次公开技术分享中,我不仅见识了由DaoCloud技术合伙人孙宏亮大神所分享的Docker安全思考,也同时领略了其他公司关于kubernetes、关于docker、关于registry的思考和尝试。

携程、京东的 Docker Registry 的部署过程中,都用到了一个harbor的开源镜像管理软件,这是我第一次接触这个项目。后面我们的 Docker Registry 部署方案的设计中也同时采用了 harbor 这个开源项目。

Docker Registry 的后端存储

在 harbor 这个开源项目中,默认的 registry 是以 replication controller 的形式部署在 kubernetes 上的,采用 persistent volume 作为 registry 的后端存储。存在的问题是:即使增大 registry 的副本数,这些 registry 后端存储的仍是同一个本地的文件路径 “host path”,这种形式下仍旧存在单点问题,造成镜像文件的丢失。

所以目前存在的问题是多个 Docker Registry 如何共享后端存储?网上的实现方式有很多,比如 NFS、GlusterFS、CephFS 等。相对于使用这些分布式系统,我们内部有现成的一套 HDFS 分布式文件系统,但有个问题是 kubernetes 本身是不支持 HDFS 文件系统作为它内部的 volume 的,这就造成了如何在 kubernetes 部署 Docker Registry 集群造成一定的困难。

变通与使用 HDFS

我们的办法是使用 kubernetes 内部支持的 host path 的形式作为每个 registry 的后端存储,最关键的一步是将这些 host path 全部挂载到 HDFS 的同一个目录,这样就可以实现多个 registry 共享同一套后端存储,既解决了 registry 的单点问题,又保证了镜像仓库的可靠性。

挂载 HDFS 的目录

我们先选好三台机器作为 Docker Registry 的部署机器,分别打上标签

1
2
3
kubectl label node node1.X.com registry=true
kubectl label node node3.X.com registry=true
kubectl label node node4.X.com registry=true

其次分别登录到这些机器上,建立一个目录作为 registry 的存储位置

1
mkdir -p /home/work/hdfs/docker/images

然后把这个目录挂载到预先建立好的同一个HDFS目录上,挂载方式是通过执行 hadoop client 里的挂载脚本,过程如下

1
2
3
4
5
6
local_dir=/home/work/hdfs/docker/images
remote_dir=/user/X/docker/images
hostname=nj02-rp-nlpc-zk00.nj02.X.com
port=54310

sh ./hadoop-client/hadoop-vfs/bin/mount.sh $hostname:$port:$remote_dir $local_dir

最后的效果就是这三个节点的 /home/work/hdfs/docker/images 路径全部挂载到了 HDFS 的 /user/X/docker/images 目录上。

镜像迁移

由于我们集群上仍旧运行着很多服务,为了不影响服务的变迁过程,我们在搭建新的 registry 集群的时候,需要将原有的仓库迁移过去。迁移办法很简单,通过脚本方式完成,缺点是比较耗时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import os
import json
import urllib2
import subprocess

images_list = []
# 源仓库地址
source_registry = 'registry.sofacloud.baidu.com'
# 目的仓库地址
target_registry = '10.254.60.41:5000'

try:
images_list = json.load(urllib2.urlopen('http://' + source_registry + '/v2/_catalog'))['repositories']
except Exception as e:
print 'get images list from source registry: ' + source_registry + ' Error: ' + str(e)
exit(0)

def get_image_tag(registry, image):
#url = 'http://registry.sofacloud.baidu.com/v2/' + image + '/tags/list'
url = 'http://' + registry + '/v2/' + image + '/tags/list'
tags = []
try:
#tagInfo = urllib2.urlopen(url, timeout = 3).read()
tags = json.load(urllib2.urlopen(url, timeout = 3))['tags']
except Exception as e:
print 'get images ' + image + ' tags error. Error : ' + str(e)
return tags

# get images info
target_images_info = {}
source_images_info = {}
for image in images_list:
print 'get image : ' + image + ' tags info'
target_images_info[image] = get_image_tag(target_registry, image)
source_images_info[image] = get_image_tag(source_registry, image)

# make a diff
diff_images = []
for image in source_images_info:
if image not in target_images_info:
print 'image : ' + image + ' not found in target image repo'
continue
target_tags = target_images_info[image]
source_tags = source_images_info[image]

for tag in source_tags:
if tag not in target_tags:
diff_images.append(image + ':' + tag)
print 'image : 10.254.60.41:5000/' + image + ':' + tag + ' is diff'

# begin to sync
for image in diff_images:
target_image = source_registry + '/' + image
print 'begin to pull image ' + target_image
try:
retcode = subprocess.call(['docker', 'pull', target_image])
if retcode != 0:
print 'image ' + target_image + ' pull failed'
continue

# tag new image
new_image = target_registry + '/' + image
print new_image
retcode = subprocess.call(['docker', 'tag', target_image, new_image])
if retcode != 0:
print 'image ' + target_image + ' tag failed'
continue

# push new image
retcode = subprocess.call(['docker', 'push', new_image])
if retcode != 0:
print 'image ' + target_image + ' tag failed'

except Exception as e:
print "image : " + image + " sync failed. Error: " + str(e)

注意 target_registry = '10.254.60.41:5000' 是 kubernetes 临时搭建的镜像仓库地址,不过后端存储使用的是 HDFS 上的镜像仓库目录 /user/X/docker/images

kubernetes 上部署

在 kubernetes 上拉起三个 registry 集群的配置文件: Config Map 的配置文件 registry.cm.yaml 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: v1
kind: ConfigMap
metadata:
name: harbor-registry-config
namespace: harbor
data:
config: |
version: 0.1
log:
level: debug
fields:
service: registry
storage:
filesystem:
rootdirectory: /storage
cache:
layerinfo: inmemory
maintenance:
uploadpurging:
enabled: false
delete:
enabled: true
http:
addr: :5000
secret: placeholder
debug:
addr: localhost:5001
notifications:
endpoints:
- name: harbor
disabled: false
url: http://ui/service/notifications
timeout: 3000ms
threshold: 5
backoff: 1s

cert: |

Replication Controller 配置文件 registry.rc.yaml 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: v1
kind: ReplicationController
metadata:
name: registry-rc
namespace: harbor
labels:
name: registry-rc
spec:
replicas: 2
selector:
name: registry-apps
template:
metadata:
labels:
name: registry-apps
spec:
restartPolicy: Always
nodeSelector:
registry: "true"
containers:
- name: registry-app
image: registry.sofacloud.X.com/public/registry:2.6
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
hostPort: 8988
- containerPort: 5001
hostPort: 8989
volumeMounts:
- name: config
mountPath: /etc/docker/registry
- name: storage
mountPath: /storage
volumes:
- name: config
configMap:
name: harbor-registry-config
items:
- key: config
path: config.yml
- key: cert
path: root.crt
- name: storage
hostPath:
path: /home/work/hdfs/docker/images

然后创建 registry 集群

1
2
# kubectl create -f registry.cm.yaml
# kubectl create -f registry.rc.yaml

Registry 镜像仓库的负载均衡

kubernetes 上我们已经搭建了三个 registry 实例,它们共享同一套后端存储,统一把镜像文件存储到 HDFS 集群上,这样保障了镜像仓库的可靠性。为了用户更好的使用这些 registry,还需要为这三个 registry 增加一个负载均衡器。

本文三个 registry 部署的情况如下:

1
2
3
hostA:8989
hostB:8989
hostC:8989

可以选用 nginx + keepalived 实现高可用的负载均衡器,不过为了安全起见,我们是在公司内部申请了一个虚拟的 VIP 来映射三个 registry 的部署端口,从而实现负载均衡的功能。

最后

经过这次成本很低的简易改造,大大的提高了 registry 镜像仓库的可靠性和稳定性。