基于Harbor和CephFS搭建高可用Private Registry

我们有给客户搭建私有容器仓库的需求。开源的私有容器registry可供选择的不多,除了docker官方的distribution之外,比较知名的是VMware China出品的Harbor,我们选择了harbor。

harbor在docker distribution的基础上增加了一些安全、访问控制、管理的功能以满足企业对于镜像仓库的需求。harbor以docker-compose的规范形式组织各个组件,并通过docker-compose工具进行启停。

不过,harbor默认的安装配置是针对single node的,要想做得可靠性高一些,我们需要自己探索一些可行的方案。本文将结合harbor和CephFS搭建一个满足企业高可用性需求的private registry。

一、实验环境

这里用两台阿里云ECS作为harbor的工作节点:

node1:  10.47.217.91
node2:  10.28.61.30

两台主机运行的都是Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64),使用root用户。

docker版本与docker-compose的版本如下:

# docker version
Client:
 Version:      1.12.5
 API version:  1.24
 Go version:   go1.6.4
 Git commit:   7392c3b
 Built:        Fri Dec 16 02:42:17 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.5
 API version:  1.24
 Go version:   go1.6.4
 Git commit:   7392c3b
 Built:        Fri Dec 16 02:42:17 2016
 OS/Arch:      linux/amd64

# docker-compose -v
docker-compose version 1.12.0, build b31ff33

ceph版本如下:

# ceph -v
ceph version 10.2.7

ceph的安装和配置可参考这里

二、方案思路

首先,从部署上说,我们需要的Private Registry是独立于k8s cluster存在的,即在k8s cluster外部,其存储和管理的镜像供k8s cluster 组件以及运行于k8s cluster上的应用使用。

其次,企业对registry有高可用需求,但我们也要有折中,我们的目标并不是理想的完全高可用,那样投入成本可能有些高。一般企业环境下更注重数据安全。因此首要保证harbor的数据安全,这样即便harbor实例宕掉,保证数据依然不会丢失即可。并且生产环境下registry的使用很难称得上高频,对镜像仓库的性能要求也没那么高。这种情况下,harbor的高可用至少有两种方案:

  • 多harbor实例共享后端存储
  • 多harbor实例相互数据同步(通过配置两个harbor相互复制镜像数据)

harbor原生支持双实例的镜像数据同步。不过这里我们采用第一种方案:即多harbor实例共享后端存储,因为我们有现成的cephfs供harbor使用。理想的方案示意图如下:

img{512x368}

  • 每个安放harbor实例的node都mount cephfs;
  • 每个node上的harbor实例(包含组件:ui、db、registry等)都volume mount node上的cephfs mount路径;
  • 通过Load Balance将request流量负载到各个harbor实例上。

但这样做可行么?如果这么做,Harbor实例里的mysql container就会“抱怨”:

May 17 22:45:45 172.19.0.1 mysql[12110]: 2017-05-17 14:45:45 1 [ERROR] InnoDB: Unable to lock ./ibdata1, error: 11
May 17 22:45:45 172.19.0.1 mysql[12110]: 2017-05-17 14:45:45 1 [Note] InnoDB: Check that you do not already have another mysqld process using the same InnoDB data or log files.

MySQL多个实例无法共享一份mysql数据文件。

那么,我们会考虑将harbor连接的mysql放到外面来,使用external database;同时考虑到session共享,我们还需要增加一个存储session信息的redis cluster,这样一来,方案示意图变更如下:

img{512x368}

图中的mysql、redis你即可以用cluster,也可以用单点,还是看你的需求和投入。如果你具备现成的mysql cluster和redis cluster,那么直接用就好了。但是如果你没有,并且你还不想投入这么多(尤其是搞mysql cluster),那么用单点就好了。考虑到数据安全,可以将单点mysql的数据存储在cephfs上,如果你已经有了现成的cephfs。

三、在一个node上安装Harbor

1、初装步骤

以一个node上的Harbor安装为例,harbor提供了详细的安装步骤文档,我们按照步骤逐步进行即可(这里我使用的是1.1.0版本,截至目前为止的最新稳定版本为1.1.1版本):

~/harbor-install# wget -c https://github.com/vmware/harbor/releases/download/v1.1.0/harbor-offline-installer-v1.1.0.tgz

~/harbor-install# tar zxvf harbor-offline-installer-v1.1.0.tgz

~/harbor-install/harbor# ls -F
common/  docker-compose.notary.yml  docker-compose.yml  harbor.cfg  harbor.v1.1.0.tar.gz  install.sh*  LICENSE  NOTICE  prepare*

~/harbor-install/harbor./install.sh

[Step 0]: checking installation environment ...

Note: docker version: 1.12.5
Note: docker-compose version: 1.12.0
[Step 1]: loading Harbor images ...
... ...
[Step 2]: preparing environment ...
Generated and saved secret to file: /data/secretkey
Generated configuration file: ./common/config/nginx/nginx.conf
Generated configuration file: ./common/config/adminserver/env
Generated configuration file: ./common/config/ui/env
Generated configuration file: ./common/config/registry/config.yml
Generated configuration file: ./common/config/db/env
Generated configuration file: ./common/config/jobservice/env
Generated configuration file: ./common/config/jobservice/app.conf
Generated configuration file: ./common/config/ui/app.conf
Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.

[Step 3]: checking existing instance of Harbor ...
[Step 4]: starting Harbor ...

Creating network "harbor_harbor" with the default driver
Creating harbor-log
Creating harbor-db
Creating registry
Creating harbor-adminserver
Creating harbor-ui
Creating nginx
Creating harbor-jobservice

ERROR: for proxy  Cannot start service proxy: driver failed programming external connectivity on endpoint nginx (fdeb3e538d5f8d714ea5c79a9f3f127f05f7ba5d519e09c4c30ef81f40b2fe77): Error starting userland proxy: listen tcp 0.0.0.0:80: bind: address already in use

harbor实例默认的监听端口是80,但一般node上的80口都会被占用,因此我们需要修改一个端口号。注意:此时harbor仅启动成功了一些container而已,尚无法正常工作。

2、修改harbor proxy组件的listen端口

harbor的proxy组件就是一个nginx,通过nginx这个反向代理,将不同的服务请求分发到内部其他组件中去。nginx默认监听node的80端口,我们用8060端口替代80端口需要进行两处配置修改:

1、harbor.cfg

hostname = node_public_ip:8060

2、docker-compose.yml

proxy:
    image: vmware/nginx:1.11.5-patched
    container_name: nginx
    restart: always
    volumes:
      - ./common/config/nginx:/etc/nginx:z
    networks:
      - harbor
    ports:
      - 8060:80   <--- 修改端口映射
      - 443:443
      - 4443:4443

由于我们修改了harbor.cfg文件,我们需要重新prepare一下,执行下面命令:

# docker-compose down -v
Stopping harbor-jobservice ... done
Stopping nginx ... done
Stopping harbor-ui ... done
Stopping harbor-db ... done
Stopping registry ... done
Stopping harbor-adminserver ... done
Stopping harbor-log ... done
Removing harbor-jobservice ... done
Removing nginx ... done
Removing harbor-ui ... done
Removing harbor-db ... done
Removing registry ... done
Removing harbor-adminserver ... done
Removing harbor-log ... done
Removing network harbor_harbor

# ./prepare
Clearing the configuration file: ./common/config/nginx/nginx.conf
Clearing the configuration file: ./common/config/ui/env
Clearing the configuration file: ./common/config/ui/app.conf
Clearing the configuration file: ./common/config/ui/private_key.pem
Clearing the configuration file: ./common/config/adminserver/env
Clearing the configuration file: ./common/config/jobservice/env
Clearing the configuration file: ./common/config/jobservice/app.conf
Clearing the configuration file: ./common/config/db/env
Clearing the configuration file: ./common/config/registry/config.yml
Clearing the configuration file: ./common/config/registry/root.crt
loaded secret from file: /mnt/cephfs/harbor/data/secretkey
Generated configuration file: ./common/config/nginx/nginx.conf
Generated configuration file: ./common/config/adminserver/env
Generated configuration file: ./common/config/ui/env
Generated configuration file: ./common/config/registry/config.yml
Generated configuration file: ./common/config/db/env
Generated configuration file: ./common/config/jobservice/env
Generated configuration file: ./common/config/jobservice/app.conf
Generated configuration file: ./common/config/ui/app.conf
Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.

# docker-compose up -d

Creating network "harbor_harbor" with the default driver
Creating harbor-log
Creating harbor-adminserver
Creating registry
Creating harbor-db
Creating harbor-ui
Creating harbor-jobservice
Creating nginx

我们可以通过docker-compose ps命令查看harbor组件的状态:

# docker-compose ps
       Name                     Command               State                                 Ports
--------------------------------------------------------------------------------------------------------------------------------
harbor-adminserver   /harbor/harbor_adminserver       Up
harbor-db            docker-entrypoint.sh mysqld      Up      3306/tcp
harbor-jobservice    /harbor/harbor_jobservice        Up
harbor-log           /bin/sh -c crond && rm -f  ...   Up      127.0.0.1:1514->514/tcp
harbor-ui            /harbor/harbor_ui                Up
nginx                nginx -g daemon off;             Up      0.0.0.0:443->443/tcp, 0.0.0.0:4443->4443/tcp, 0.0.0.0:8060->80/tcp
registry             /entrypoint.sh serve /etc/ ...   Up      5000/tcp

如果安全组将8060端口打开,通过访问:http://node_public_ip:8060,你将看到如下harbor的web页面:

img{512x368}

我们可以通过harbor内置的默认用户名和密码admin/Harbor12345登录harbor ui。当然,我们更重要的是通过cmdline访问harbor,push和pull image。如果这时你直接尝试docker login harbor_url,你可能会得到如下错误日志:

# docker login -u admin -p Harbor12345 node_public_ip:8060
Error response from daemon: Get https://node_public_ip:8060/v1/users/: http: server gave HTTP response to HTTPS client

这是因为docker默认采用https访问registry,因此我们需要在docker engine的配置中,添加–insecure-registry option。关于ubuntu 16.04下docker配置的问题,请参考这里

DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --registry-mirror=https://xxxxx.mirror.aliyuncs.com --insecure-registry=node_public_ip:8060"

重启docker engine后尝试再次登录harbor:

docker login -u admin -p Harbor12345 node_public_ip:8060
Login Succeeded

一旦docker client login ok,我们就可以通过docker client对harbor中的相关repository进行操作了。

四、挂载路径修改

默认情况下,harbor将数据volume挂载到主机的/data路径下面。但由于我们采用ceph共享存储保证数据的高可用,需要修改harbor组件内容器的挂载路径,将其mount到共享存储挂载node上的路径:/mnt/cephfs/harbor/data/。对比两个路径,可以看出前缀由”/”变为了”/mnt/cephfs/harbor/”,我们需要修改docker-compose.yml和harbor.cfg两个文件。

由于docker-compose.yml文件较长,这里将原始文件改名为docker-compose.yml.orig,并将其与修改后的docker-compose.yml做对比:

# diff  docker-compose.yml.orig docker-compose.yml
8c8
<       - /var/log/harbor/:/var/log/docker/:z
---
>       - /mnt/cephfs/harbor/log/:/var/log/docker/:z
20c20
<       - /data/registry:/storage:z
---
>       - /mnt/cephfs/harbor/data/registry:/storage:z
40c40
<       - /data/database:/var/lib/mysql:z
---
>       - /mnt/cephfs/harbor/data/database:/var/lib/mysql:z
59,61c59,61
<       - /data/config/:/etc/adminserver/config/:z
<       - /data/secretkey:/etc/adminserver/key:z
<       - /data/:/data/:z
---
>       - /mnt/cephfs/harbor/data/config/:/etc/adminserver/config/:z
>       - /mnt/cephfs/harbor/data/secretkey:/etc/adminserver/key:z
>       - /mnt/cephfs/harbor/data/:/data/:z
80,81c80,81
<       - /data/secretkey:/etc/ui/key:z
<       - /data/ca_download/:/etc/ui/ca/:z
---
>       - /mnt/cephfs/harbor/data/secretkey:/etc/ui/key:z
>       - /mnt/cephfs/harbor/data/ca_download/:/etc/ui/ca/:z
100c100
<       - /data/job_logs:/var/log/jobs:z
---
>       - /mnt/cephfs/harbor/data/job_logs:/var/log/jobs:z
102c102
<       - /data/secretkey:/etc/jobservice/key:z
---
>       - /mnt/cephfs/harbor/data/secretkey:/etc/jobservice/key:z

harbor.cfg文件需要修改的地方不多:

// harbor.cfg

#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /mnt/cephfs/harbor/data/cert/server.crt
ssl_cert_key = /mnt/cephfs/harbor/data/cert/server.key

#The path of secretkey storage
secretkey_path = /mnt/cephfs/harbor/data

配置修改完毕后,执行如下命令:

# docker-compose down -v
# prepare
# docker-compose up -d

新的harbor实例就启动起来了。注意:这一步我们用cephfs替换了本地存储,主要的存储变动针对log、database和registry三个输出数据的组件。你也许会感受到cephfs给harbor ui页面加载带来的影响,实感要比之前的加载慢一些。

五、使用外部数据库(external database)

前面提到了挂载ceph后,多个node上harbor实例中的db组件将出现竞争问题,导致只有一个node上的harbor db组件可以工作。因此,我们要使用外部数据库(或db集群)来解决这个问题。但是harbor官方针对如何配置使用外部DB很是“讳莫如深”,我们只能自己探索。

假设我们已经有了一个external database,并且建立了harbor这个user,并做了相应的授权。由于harbor习惯了独享database,在测试环境下可以考虑

GRANT ALL ON *.* TO 'harbor'@'%';

1、迁移数据

如果此时镜像库中已经有了数据,我们需要做一些迁移工作。

attach到harbor db组件的container中,将registry这张表dump到registry.dump文件中:

#docker exec -i -t  6e1e4b576315  bash

在db container中:
# mysqldump -u root -p --databases registry > registry.dump

回到node,将dump文件从container中copy出来:

#docker cp 6e1e4b576315:/root/registry.dump ./

再mysql login到external Database,将registry.dump文件导入:

# mysql -h external_db_ip -P 3306 -u harbor -p
# mysql> source ./registry.dump;

2、修改harbor配置,使得ui、jobservice组件连接external db

根据当前harbor architecture图所示:

img{512x368}

与database“有染”的组件包括ui和jobservice,如何通过配置修改来让这两个组件放弃老db,访问新的external db呢?这要从挖掘配置开始。harbor的组件配置都在common/config下:

~/harbor-install/harbor# tree -L 3 common
common
├── config
│   ├── adminserver
│   │   └── env
│   ├── db
│   │   └── env
│   ├── jobservice
│   │   ├── app.conf
│   │   └── env
│   ├── nginx
│   │   └── nginx.conf
│   ├── registry
│   │   ├── config.yml
│   │   └── root.crt
│   └── ui
│       ├── app.conf
│       ├── env
│       └── private_key.pem
└── templates
 ... ...

在修改config之前,我们先docker-compose down掉harbor。接下来,我们看到ui和jobservice下都有env文件,这里想必就是可以注入新db的相关访问信息的地方,我们来试试!

// common/config/ui/env
LOG_LEVEL=debug
CONFIG_PATH=/etc/ui/app.conf
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
GODEBUG=netdns=cgo
MYSQL_HOST=new_db_ip
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password

// common/config/jobservice/env
LOG_LEVEL=debug
CONFIG_PATH=/etc/jobservice/app.conf
UI_SECRET=$ui_secret
JOBSERVICE_SECRET=$jobservice_secret
GODEBUG=netdns=cgo
MYSQL_HOST=new_db_ip
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password

同时,由于不再需要harbor_db组件,因此切记:要将其从docker-compose.yml中剔除!。docker-compose up -d重新创建harbor各组件容器并启动!Harbor的日志可以在挂载的ceph路径: /mnt/cephfs/harbor/log下查找到:

/mnt/cephfs/harbor/log# tree 2017-06-09
2017-06-09
├── adminserver.log
├── anacron.log
├── CROND.log
├── jobservice.log
├── mysql.log
├── proxy.log
├── registry.log
├── run-parts.log
└── ui.log

我们以ui.log为例,我们发现harbor启动后,ui.log输出如下错误日志(jobservice.log也是相同):

Jun  9 11:00:17 172.19.0.1 ui[16039]: 2017-06-09T03:00:17Z [INFO] initializing database: type-MySQL host-mysql port-3306 user-root database-registry
Jun  9 11:00:18 172.19.0.1 ui[16039]: 2017-06-09T03:00:18Z [ERROR] [utils.go:94]: failed to connect to tcp://mysql:3306, retry after 2 seconds :dial tcp: lookup mysql: no such host

我们明明注入了新的db env,为何ui还是要访问“tcp://mysql:3306”呢?我们docker inspect一下ui的container,看看env是否包含我们添加的那些:

# docker inspect e91ab20e1dcb
... ...
            "Env": [
                "DATABASE_TYPE=mysql",
                "MYSQL_HOST=database_ip",
                "MYSQL_PORT=3306",
                "MYSQL_PWD=harbor_password",
                "MYSQL_USR=harbor",
                "MYSQL_DATABASE=registry",
            ],
.... ...

env已经注入,那么为何ui、jobservice无法连接到external database呢?要想搞清楚这点,我们只能去“啃代码”了。还好harbor代码并非很难啃。我们发现基于beego实现的ui、jobservice两个组件并未直接通过os.Getenv去获取这些env变量,而是调用了adminserver组件的服务。adminserver在初始化时,在RESET环境变量为true的情况下,读取了common/config/adminserver/env下的所有环境变量。

搞清楚原理后,我们知道了要修改的是common/config/adminserver/env,而不是common/config/ui/env和common/config/jobservice/env。我们将后两个文件还原。修改common/config/adminserver/env文件:

//common/config/adminserver/env
... ...
MYSQL_HOST=new_db_ip
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password
... ...
RESET=true    <--- 改为true,非常关键

重新up harbor服务后,我们发现ui, jobservice与新database的连接成功了!打开harbor web页面,登录进去,我们看到了之前已经添加的用户、项目和镜像文件。

3、一劳永逸

如果你重新执行prepare,那么上面对config目录下的配置修改将被重新覆盖。如果要一劳永逸,那么需要修改的是common/templates下面的同位置同名配置文件。

六、安装其他节点上的harbor实例

前面,我们只搭建了一个节点,为的是验证方案的可行性。要实现高可用,我们还需要在其他节点上安装harbor实例。由于多个节点上harbor实例共同挂载ceph的同一目录,因此考虑到log的分离,在部署其他节点上的harbor时,最好对docker-compose.yml下log组件的volumes映射路径进行调整,以在多个节点间做隔离,便于日志查看,比如:

volumes:
      - /mnt/cephfs/harbor/log1/:/var/log/docker/:z

除此之外,各个节点上的harbor配置与上述配置完全一致。

七、共享session设置

到harbor的请求被负载均衡分发到多个node上的harbor实例上,这样就有了session共享的需求。Harbor对此已经给予了支持。在ui组件的代码中,我们发现ui在初始化时使用Getenv获取”_REDIS_URL”这个环境变量的值,因此我们只需要将_REDIS_URL这个环境变量配置到各个节点harbor ui组件的env文件中即可:

// common/config/adminserver/env

LOG_LEVEL=debug
CONFIG_PATH=/etc/ui/app.conf
UI_SECRET=LuAwkKUtYjF4l0mQ
JOBSERVICE_SECRET=SmsO1kVo4SrmgOIp
GODEBUG=netdns=cgo
_REDIS_URL=redis_ip:6379,100,redis_password,0

重新up harbor后,session共享生效。

不过光有一个外部redis存储共享session还不够,请求在多个harbor实例中的registry组件中进行鉴权需要harbor各个实例share相同的key和certificate。好在,我们的多harbor实例通过ceph共享存储,key和cert本就是共享的,都存放在目录:/mnt/cephfs/harbor/data/cert/的下边,因此也就不需要在各个harbor实例间同步key和cert了。

八、更换为域名访问

我们有通过域名访问docker registry的需求,那么直接通过域名访问harbor ui和registry是否可行呢?这要看harbor nginx的配置:

# docker ps |grep nginx
fa92765e8871        vmware/nginx:1.11.5-patched   "nginx -g 'daemon off"   3 hours ago
Up 3 hours          0.0.0.0:443->443/tcp, 0.0.0.0:4443->4443/tcp, 0.0.0.0:8060->80/tcp               nginx

# docker exec fa92765e8871 cat /etc/nginx/nginx.conf

... ...
http {
   server {
    listen 80;
   ... ...

}

nginx在http server block并未对域名或ip进行匹配,因此直接将域名A地址设置为反向代理的地址或直接解析为Harbor暴露的公网ip地址都是可以正常访问harbor服务的,当然也包括image push和pull服务。

注意:如果使用域名访问harbor服务,那么就将harbor.cfg中的hostname赋值为你的”域名+端口”,并重新prepare。否则你可能会发现通过harbor域名上传的image无法pull,因为其pull的地址为由ip组成的地址,以docker push hub.tonybai.com:8989/myrepo/foo:latest为例,push成功后,docker pull hub.tonybai.com:8989/myrepo/foo:latest可能提示你找不到该image,因为harbor中该imag
e的地址可能是my_ip_address:8989/myrepo/foo:latest。

九、统一registry的证书和token service的私钥

这是在本篇文章发表之后发现的问题,针对该问题,我专门写了一篇文章:《解决登录Harbor Registry时鉴权失败的问题》,请移步这篇文章,完成HA Harbor的搭建。

十、参考资料


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

初窥dep

Go语言程序组织和构建的基本单元是Package,但Go语言官方却没有提供一款“像样的”Package Management Tool(包管理工具)。随着Go语言在全球范围内应用的愈加广泛,缺少官方包管理工具这一问题变得日益突出。

2016年GopherCon大会后,在Go官方的组织下,一个旨在改善Go包管理的commitee成立了,共同应对Go在package management上遇到的各种问题。经过各种脑洞和讨论后,该commitee在若干月后发布了“Package Management Proposal”,并启动了最有可能被接纳为官方包管理工具的项目dep的设计和开发。2017年年初,dep项目正式对外开放。截至目前,dep发布了v0.1.0版本,并处于alpha测试阶段。

可以说,dep的进展还是蛮快的。按照dep官方说法,dep目前的manifest和lock文件格式已经stable,并保证向后兼容。同时,dep实现了“自举”,即dep使用自己作为自己的包管理工具。由于dep的“特殊身份”,虽然dep离成熟尚远,但dep的进展也吸引了诸多gopher的目光,很多组织已经开始将package management tool迁移为dep,为dep进行早期测试。

这里,我也打算“尝尝鲜”,在本篇文章中和大家一起窥探和试用一下dep。

一、Go包管理的演进历史

1、go get

在管窥dep之前,我们先来简单看看Go语言包管理的演进历史。首当其冲的就是go get。

Go语言新手在初次接触Go语言时会感觉到Go语言的package获取真的是很方便:只需一行go get xxx,github.com上的大量go package就可以随你取用。 但随着对Go语言使用的深入,人们会发现go get给我们带来方便的同时,也带来了不少的麻烦。go get本质上是githg等这些vcs工具的高级wrapper。对于使用git的go package来说,go get的实质就是将package git clone到本地的特定目录下($GOPATH/src),同时go get可以自动解析包的依赖,并自动下载相关依赖包。

go get机制的设计很大程度上源于Google公司内部的单一root的代码仓库的开发模式,并且似乎google内部各个project/repository的master分支上的代码都是被认为stable的,因此go get仅仅支持获取master branch上的latest代码,没有指定version、branch或revision的能力。而在Google公司以外的世界里,这样的做法会给gopher带来不便:依赖的第三方包总是在变。一旦第三方包提交了无法正常build或接口不兼容的代码,依赖方立即就会受到影响。

而gopher们又恰恰希望自己项目所依赖的第三方包能受到自己的控制,而不是随意变化。这样,godepgbglide等一批第三方包管理工具出现了。

以应用最为广泛的godep为例。为了能让第三方依赖包“稳定下来”,实现项目的reproduceble build,godep将项目当前依赖包的版本信息记录在Godeps/Godeps.json中,并将依赖包的相关版本存放在Godeps/_workspace中。在编译时(godep go build)godep通过临时修改GOPATH环境变量的方法让go编译器使用缓存在Godeps/_workspace下的项目依赖的特定版本的第三方包,这样保证了项目不再受制于依赖的第三方包的master branch上的latest代码的变动了。

不过,godep的“版本管理”本质上是通过缓存第三方库的某个revision的快照实现的,这种方式依然让人感觉难于管理。同时,通过对GOPATH的“偷梁换柱”的方式实现使用Godeps/_workspace中的第三方库的快照进行编译也无法兼容Go原生编译器,必须使用godep go xxx来进行。

为此,Go进一步引入vendor机制减少gopher在包管理问题上的心智负担

2、vendor机制

Go team也一直在关注Go语言包依赖的问题,尤其是在Go 1.5实现自举的情况下,官方同样在1.5版本中推出了vendor机制。vendor机制是Russ CoxGo 1.5发布前期以一个experiment feature身份紧急加入到go中的(go 1.6脱离experiment身份)。vendor标准化了项目依赖的第三方库的存放位置(不再需要Godeps/_workspace了),同时也无需对GOPATH环境变量进行“偷梁换柱”了,go compiler原生优先感知和使用vendor下缓存的第三方包。

不过即便有了vendor的支持,vendor内第三方依赖包的代码的管理依旧是不规范的,要么是手动的,要么是借助godep这样的第三方包管理工具。目前自举后的Go代码本身也引入了vendor,不过go项目自身对vendor中代码的管理方式也是手动更新,Go自身并未使用任何第三方的包管理工具。

题外话:作为一门语言的标准库,应该是使用这门语言的开发者所使用的所有lib依赖的根依赖。但在go中,go标准库居然还要依赖golang.org/x/目录下的包,既然能被std lib依赖,那么说明其已经成熟,那为何不把x内的stable的库挪到std lib中呢?这点着实让人有些不解。

~/.bin/go18/src/vendor/golang_org/x]$ls
crypto/    net/    text/

从Go官方角度出发,官方go包依赖的解决方案的下一步就应该是解决对vendor下的第三方包如何进行管理的问题:依赖包的分析、记录和获取等,进而实现项目的reproducible build。dep就是用来做这事儿的。

二、dep简介

go package management commitee的牵头人物是微服务框架go-kit作者Peter Bourgon,但当前主导dep开发的是sam boyer,sam也是dep底层包依赖分析引擎-gps的作者。

和其他一些第三方Go包管理工具有所不同,dep在进行active dev前是经过commitee深思熟虑的,包括:featuresuser story等都在事前做了初步设计。如果你拜读这些文档,你可能会觉得解决包依赖问题,还是蛮复杂的。不过,对于这些工具的使用者来说,我们面对的是一些十分简化的交互接口。

1、安装dep

dep是标准的go cli程序,执行一条命令即完成安装:

# go get -u github.com/golang/dep/cmd/dep

# dep help
dep is a tool for managing dependencies for Go projects

Usage: dep <command>

Commands:

  init    Initialize a new project with manifest and lock files
  status  Report the status of the project's dependencies
  ensure  Ensure a dependency is safely vendored in the project
  prune   Prune the vendor tree of unused packages

Examples:
  dep init                          set up a new project
  dep ensure                        install the project's dependencies
  dep ensure -update                update the locked versions of all dependencies
  dep ensure github.com/pkg/errors  add a dependency to the project

Use "dep help [command]" for more information about a command.

在我的测试环境中,go的版本为1.8;dep的版本为commit d31c621c3381b9bebc7c10b1ac7849a96c21f2c3。

注意:由于dep还在active dev过程中且处于alpha测试阶段,因此本文中执行的dep命令、命令行为以及输出结果在后续dep版本中很可能会有变动,甚至是很大变动。

2、dep一般工作流

安装好dep后,我们就来看看使用dep的一般工作流。我们首先准备一个demo程序:

//depdemo/main.go
package main

import (
    "net/http"

    "go.uber.org/zap"

    "github.com/beego/mux"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    sugar := logger.Sugar()

    mx := mux.New()
    mx.Handler("GET", "/", http.FileServer(http.Dir(".")))
    sugar.Fatal(http.ListenAndServe("127.0.0.1:8080", mx))
}

a) dep init

如果一个项目要使用dep进行包管理,那么首先需要在这个项目的根下执行dep init。在这里,我们对depdemo进行dep改造。

在depdemo目录下,执行dep init:

# dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep github.com/beego/mux
  Locking in master (626af65) for direct dep github.com/beego/mux
Following dependencies were not found in GOPATH. Dep will use the most recent versions of these projects.
  go.uber.org/zap
Root project is "github.com/bigwhite/experiments/depdemo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
(0)   ✓ select (root)
(1)    ? attempt github.com/beego/mux with 1 pkgs; at least 1 versions to try
(1)        try github.com/beego/mux@master
(1)    ✓ select github.com/beego/mux@master w/1 pkgs
(2)    ? attempt go.uber.org/zap with 1 pkgs; 12 versions to try
(2)        try go.uber.org/zap@v1.4.0
(2)    ✓ select go.uber.org/zap@v1.4.0 w/7 pkgs
(3)    ? attempt go.uber.org/atomic with 1 pkgs; 6 versions to try
(3)        try go.uber.org/atomic@v1.2.0
(3)    ✓ select go.uber.org/atomic@v1.2.0 w/1 pkgs
  ✓ found solution with 9 packages from 3 projects

Solver wall times by segment:
     b-source-exists: 1.090607387s
  b-deduce-proj-root: 288.126482ms
         b-list-pkgs: 131.059753ms
              b-gmal: 114.716587ms
         select-atom:    337.787µs
             satisfy:    298.743µs
         select-root:    292.889µs
            new-atom:    257.256µs
     b-list-versions:     42.408µs
               other:     22.307µs

  TOTAL: 1.625761599s

当前阶段,dep init命令的执行效率的确不高,因此需要你耐心的等待一会儿。如果你的project依赖的外部包很多,那么等待的时间可能会很长。并且由于dep会下载依赖包,对于国内的朋友来说,一旦下载qiang外的包,那么dep可能会“阻塞”在那里!

dep init大致会做这么几件事:

  • 利用gps分析当前代码包中的包依赖关系;
  • 将分析出的项目包的直接依赖(即main.go显式import的第三方包,direct dependency)约束(constraint)写入项目根目录下的Gopkg.toml文件中;
  • 将项目依赖的所有第三方包(包括直接依赖和传递依赖transitive dependency)在满足Gopkg.toml中约束范围内的最新version/branch/revision信息写入Gopkg.lock文件中;
  • 创建root vendor目录,并且以Gopkg.lock为输入,将其中的包(精确checkout 到revision)下载到项目root vendor下面。

执行完dep init后,dep会在当前目录下生成若干文件:

├── Gopkg.lock
├── Gopkg.toml
├── main.go
└── vendor/

我们逐一来看一下:

Gopkg.toml:

[[constraint]]
  branch = "master"
  name = "github.com/beego/mux"

[[constraint]]
  name = "go.uber.org/zap"
  version = "1.4.0"

Gopkg.toml记录了depdemo/main.go的两个direct dependency:mux和zap。通过gps的分析(可以参见上面init执行时输出的详细分析过程日志),dep确定的依赖版本约束为:mux的master分支、zap的1.4.0 version。

生成的Gopkg.lock中则记录了depdemo/main.go在上述约束下的所有依赖的可用的最新版本:

Gopkg.lock:

[[projects]]
  branch = "master"
  name = "github.com/beego/mux"
  packages = ["."]
  revision = "626af652714cc0092f492644e298e5f3ac7db31a"

[[projects]]
  name = "go.uber.org/atomic"
  packages = ["."]
  revision = "4e336646b2ef9fc6e47be8e21594178f98e5ebcf"
  version = "v1.2.0"

[[projects]]
  name = "go.uber.org/zap"
  packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","internal/multierror","zapcore"]
  revision = "fab453050a7a08c35f31fc5fff6f2dbd962285ab"
  version = "v1.4.0"

[solve-meta]
  analyzer-name = "dep"
  analyzer-version = 1
  inputs-digest = "77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f"
  solver-name = "gps-cdcl"
  solver-version = 1

vendor目录下,则是lock文件中各个依赖包的本地clone:

# tree -L 2 vendor
vendor
├── github.com
│   └── beego
└── go.uber.org
    ├── atomic
    └── zap

至此,dep init完毕,相关依赖包也已经被vendor,你可以使用go build/install进行程序构建了。

b)、提交Gopkg.toml和Gopkg.lock

如果你对dep自动分析出来的各种约束和依赖的版本没有异议,那么这里就可以将Gopkg.toml和Gopkg.lock作为项目源码的一部分提交到代码库中了。这样其他人在下载了你的代码后,可以通过dep直接下载lock文件中的第三方包版本,并存在vendor里。这样就使得无论在何处,项目构建的依赖库理论上都是一致的,实现reproduceable build。

是否需要提交vendor下的依赖包代码到代码仓库?这取决于你。提交vendor的好处是即便没有dep,也可以实现真正的reproduceable build。但vendor的提交会让你的代码库变得异常庞大,且更新vendor时,大量的diff会影响到你对代码的review。下面的内容我们以不提交vendor为前提。

c)、dep ensure

现在我们的depdemo已经加入了Gopkg.toml和Gopkg.lock。这时,如果你将depdemo clone到你的本地,你还无法进行reproduceable build,因为这时vendor还不存在。这时我们需要执行下面命令来根据Gopkg.toml和Gopkg.lock中的数据构建vendor目录和同步里面的包:

# dep ensure

# ls -F
Gopkg.lock  Gopkg.toml  main.go  vendor/

ensure成功后,你就可以进行reproduceable build了。

我们可以通过dep status查看当前的依赖情况(包括direct and transitive dependency):

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              v1.2.0         4e33664   4e33664  1
go.uber.org/zap       ^1.4.0         v1.4.0         fab4530   fab4530  7

d) 指定约束

dep init生成的Gopkg.toml中的约束是否是我们预期的呢?这个还真不一定。比如:我们将对zap的约束手工改为1.3.0:

//Gopkg.toml
... ...

[[constraint]]
  name = "go.uber.org/zap"
  version = "<=1.3.0"

执行dep ensure后,查看status:

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              v1.2.0         4e33664   4e33664  1
go.uber.org/zap       <=1.3.0         v1.4.0         fab4530   fab4530  7

不过,此时Gopkg.lock中的zap version依旧是v1.4.0,并没有修改。要想更新lock和vendor下的数据,我们需要给ensure加上一个-update参数:

# dep ensure -update

# git diff Gopkg.lock
diff --git a/depdemo/Gopkg.lock b/depdemo/Gopkg.lock
index fce53dc..7fe3640 100644
--- a/depdemo/Gopkg.lock
+++ b/depdemo/Gopkg.lock
@@ -16,12 +16,12 @@
 [[projects]]
   name = "go.uber.org/zap"
   packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","internal/multierror","zapcore"]
-  revision = "fab453050a7a08c35f31fc5fff6f2dbd962285ab"
-  version = "v1.4.0"
+  revision = "6a4e056f2cc954cfec3581729e758909604b3f76"
+  version = "v1.3.0"

 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f"
+  inputs-digest = "b09c1497771f6fe7cdfcf61ab1a026ccc909f4801c08f2c25f186f93f14526b0"
   solver-name = "gps-cdcl"
   solver-version = 1

-update让dep ensure尝试去保证并同步Gopkg.lock和vendor目录下的数据,将Gopkg.lock下的zap的version改为Gopkg.toml下约束的最大值,即v1.3.0,同时更新vendor下的zap代码。

e) 指定依赖

我们也可以直接更新dependency,这将影响Gopkg.lock和vendor下的数据,但Gopkg.toml不会被修改:

# dep ensure 'go.uber.org/zap@<1.4.0'

# git diff
diff --git a/depdemo/Gopkg.lock b/depdemo/Gopkg.lock
index fce53dc..3b17b9b 100644
--- a/depdemo/Gopkg.lock
+++ b/depdemo/Gopkg.lock
@@ -16,12 +16,12 @@
 [[projects]]
   name = "go.uber.org/zap"
   packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","internal/multierror","zapcore"]

-  revision = "fab453050a7a08c35f31fc5fff6f2dbd962285ab"
-  version = "v1.4.0"
+  revision = "6a4e056f2cc954cfec3581729e758909604b3f76"
+  version = "v1.3.0"

 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f"
+  inputs-digest = "3307cd7d5942d333c4263fddda66549ac802743402fe350c0403eb3657b33b0b"
   solver-name = "gps-cdcl"
   solver-version = 1

这种情况下会出现Gopkg.lock中的version不满足Gopkg.toml中约束的情况。这里也让我比较困惑!

三、dep探索

上面的dep使用基本工作流完全可以满足日常包管理的需求了。但对于喜欢求甚解的我来说,必要要探索一下dep背后的行为和原理。

1、dep init的两种不同结果

我们回到depdemo的初始状态,即起点:尚未生成dep metadata file的时刻。我们在两种情况下,分别执行dep init:

  • $GOPATH/src下没有go.uber.org/zap
# dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep github.com/beego/mux
  Locking in master (626af65) for direct dep github.com/beego/mux
Following dependencies were not found in GOPATH. Dep will use the most recent versions of these projects.
  go.uber.org/zap
Root project is "github.com/bigwhite/experiments/depdemo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
... ...

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              v1.2.0         4e33664   4e33664  1
go.uber.org/zap       ^1.4.0         v1.4.0         fab4530   fab4530  7

  • $GOPATH/src下存在go.uber.org/zap
# dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep github.com/beego/mux
  Locking in master (626af65) for direct dep github.com/beego/mux
  Using master as constraint for direct dep go.uber.org/zap
  Locking in master (b33459c) for direct dep go.uber.org/zap
  Locking in master (908889c) for transitive dep go.uber.org/atomic
Root project is "github.com/bigwhite/experiments/depdemo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
... ...

# dep status
PROJECT               CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beego/mux  branch master  branch master  626af65   626af65  1
go.uber.org/atomic    *              branch master  908889c   4e33664  1
go.uber.org/zap       branch master  branch master  b33459c   b33459c  7

不知道大家发现两种情况下生成的结果的异同与否。我们只看两个dep status输出中的zap一行:

go.uber.org/zap       ^1.4.0         v1.4.0         fab4530   fab4530  7

vs.

go.uber.org/zap       branch master  branch master  b33459c   b33459c  7

dep自动分析后得到截然不同的两个结果。

第一种情况,我们称之为dep init的network mode,即dep发现本地GOPATH下面没有zap,于是dep init通过network到upstream上查找zap,并“Dep will use the most recent versions of these projects”,即v1.4.0版本。

第二种情况,我们称之为dep init的GOPATH mode, 即dep发现本地GOPATH下面存在zap,于是dep init认定“Using master as constraint for direct dep go.uber.org/zap”,即master branch。

至于为何GOPATH mode下,dep init会选择master,我个人猜测是因为dep觉得既然你本地有zap,那很大可能zap master的稳定性是被你所接受了的。在“dep: updated command spec”中,似乎dep init打算通过增加一个-gopath的flag来区分两种工作模式,并将network mode作为默认工作mode。但目前我所使用的dep版本还没有实现这个功能,其默认工作方式依旧是先GOPATH mode,如果没有找到依赖包的存在,则针对该包实施network mode。

从这里也可以看得出来,对于dep init 输出的约束,你最好还是检视一下,看是否能接受,否则就通过上面提到的“指定约束”来更正dep的输出。

2、dep对项目的依赖包的cache

在进行上面的试验中,我们发现:在本地GOPATH/src下面没有zap的情况下,dep似乎是直接将zap get到本地vendor目录的,而不是先get到GOPATH/src下,在copy到vendor中。事实是什么样的呢?dep的确没有操作GOPATH/src目录,因为那是共享的。dep在$GOPATH/pkg/dep/sources下留了一块“自留地”,用于cache所有从network上下载的依赖包:

# ls -F $GOPATH/pkg/dep/sources/
https---github.com-beego-mux/  https---github.com-uber--go-atomic/  https---github.com-uber--go-zap/

# ls -aF /root/go/pkg/dep/sources/https---github.com-uber--go-zap
./             buffer/            config_test.go   field.go       .gitignore      http_handler.go       LICENSE.txt           options.go          sugar.go       writer.go
../            CHANGELOG.md       CONTRIBUTING.md  field_test.go  glide.lock      http_handler_test.go  logger_bench_test.go  README.md           sugar_test.go  writer_test.go
array.go       check_license.sh*  doc.go           flag.go        glide.yaml      internal/             logger.go             .readme.tmpl        time.go        zapcore/
array_test.go  common_test.go     encoder.go       flag_test.go   global.go       level.go              logger_test.go        stacktrace.go       time_test.go   zapgrpc/
benchmarks/    config.go          encoder_test.go  .git/          global_test.go  level_test.go         Makefile              stacktrace_test.go  .travis.yml    zaptest/

dep对于依赖包的所以git请求均在这个缓存目录下进行。

3、 vendor flatten平坦化

go在1.5加入vendor机制时,是考虑到“钻石形依赖”中存在同一个依赖包的不同版本的。我们来看看dep是否支持这一点。我们设计了一个试验:

img{512x368}

我们建立一个这样的“钻石形”试验环境,foo依赖ab两个包,而a、b两个包分别依赖f的不同版本(通过在a、b中的Gopkg.toml声明这种约束,见图中标注)。

下面是foo项目下面的main.go:

// foo/main.go

package main

import "bitbucket.org/bigwhite/b"
import "bitbucket.org/bigwhite/a"

func main() {
    a.CallA()
    b.CallB()
}

未引入dep前,我们来运行一下该代码:

$go run main.go
call A: master branch
   --> call F:
    call F: v1.1.0
   --> call F end
call B: master branch
   --> call F:
    call F: v2.0.1
   --> call F end

可以看到同样是f包的输出,由于a、b分别依赖f的不同版本,因此输出不同。

我们对foo进行一个dep 分析,看看dep给了我们什么结果:

$dep init -v
Searching GOPATH for projects...
  Using master as constraint for direct dep bitbucket.org/bigwhite/a
  Locking in master (9122a5d) for direct dep bitbucket.org/bigwhite/a
  Using master as constraint for direct dep bitbucket.org/bigwhite/b
  Locking in master (2415845) for direct dep bitbucket.org/bigwhite/b
  Locking in master (971460c) for transitive dep bitbucket.org/bigwhite/f
Root project is "Foo"
 1 transitively valid internal packages
 2 external packages imported from 2 projects
 ... ...

No versions of bitbucket.org/bigwhite/b met constraints:
    master: Could not introduce bitbucket.org/bigwhite/b@master, as it has a dependency on bitbucket.org/bigwhite/f with constraint ^2.0.0, which has no overlap with existing constraint ^1.1.0 from bitbucket.org/bigwhite/a@master
    v2.0.0: Could not introduce bitbucket.org/bigwhite/b@v2.0.0, as it is not allowed by constraint master from project Foo.
    v1.0.0: Could not introduce bitbucket.org/bigwhite/b@v1.0.0, as it is not allowed by constraint master from project Foo.
    master: Could not introduce bitbucket.org/bigwhite/b@master, as it has a dependency on bitbucket.org/bigwhite/f with constraint ^2.0.0, which has no overlap with existing constraint ^1.1.0 from bitbucket.org/bigwhite/a@master

dep init运行失败。由于a依赖的f@^1.1.0和b依赖的f@^2.0.0两个约束之间没有交集,无法调和,dep无法solve这个依赖,于是init failed!

但失败背后还有一层原因,那就是dep的设计要求flatten vendor,即使用dep的项目只能有一个root vendor,所以直接依赖或传递依赖的包中包含vendor的,vendor目录也都会被strip掉。这样一旦依赖包中存在带有冲突的约束,那么dep init必将失败。

四、小结

dep一个重要feature就是支持semver 2.0规范,不过semver的规则好多,不是这里能说清楚的,大家可以到semver官方站细读规则,或者在npm semver calculator这个站点直观感受semver规则带来的变化。

dep试验告一段落。从目前来看,dep已经进入可用阶段,建议有条件的童鞋能积极的使用dep,并为dep进行前期测试,发现问题提issue,为dep的快速完善出出力。

depdemo的代码在这里;a, b,f包的代码在这里这里这里

五、参考资料


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats