现状

我们使用 Harbor v2.2.0 做 Docker 的镜像仓库。后端使用 Swift+Ceph。

因为后端 Ceph 不能做两 IDC 的高可用方案。所以就搞了两套 Harbor,IDC A 和 IDC B 各一个。使用 Harbor 自己带的 Replication 功能做镜像的复制。

这种方案有个问题:应用层面的数据不一致,用户的信息,项目的配置,包括 ID,Webhook 配置,用户权限等,都不能同步。

之前的想法

决定改变

因为 Ceph 里面的数据是不一致的(不实时一致),所以 DB 这层也没法使用同一个。所以这个应用层的数据不一致的问题就不好解决。

后面有一次和同事聊天,说能不能改一下代码,DB 全用同一个,到存储(Ceph)拉数据的时候如果失败了,就去另外一个存储去拉。
我想了想,决定试一下。

使用 Nginx 助力

我印象中,Nginx 有这样的功能, Upstream 里面的一组 Server,如果一个失败了,就继续尝试接下来的 Server。

这样的话,我们都不用改代码,直接复用 Nginx 配置一下就好了。

歪路

其实也不能叫“歪路”,只是最后我们放弃了,因为 Nginx 目前的一个特性,导致这条路流产了。如果 Nginx 能支持的话(或者自己修改一下再编译),在一些场景下(比如本地存储)应该还是一个不错的方案。

记录一下这种路上的尝试吧,也遇到过好多问题,一一解决了,到 404 这个问题时,跨不过去了。

沿着之前提到的思路,我们需要解决两个问题:

  1. GET/HEAD 请求两个 IDC ;POST/PUT/PATCH 请求只能到本 IDC,因为这些请求是上传镜像数据的。
  2. 两 IDC 的 GET 请求,需要配置做失败重试,到本 IDC 的请求失败了,会继续尝试另外一个 IDC 的。

相应的,我们在 Nginx 和 Harbor 上做一些配置修改。

Nginx:

upstream registry_local {
 server https://hub-1.corp.com/;
}

upstream registry_both {
 server https://hub-1.corp.com/;
 server https://hub-2.corp.com/;
}
...

location /registry/ {
  proxy_pass http://registry_local/;

  if ($request_method = GET) {
    rewrite ^(/registry/)/(.*)$ /registry_both/$1 break;
  }

  if ($request_method = HEAD) {
    rewrite ^(/registry/)/(.*)$ /registry_both/$1 break;
  }
}

common/config/core/env 里面配置 REGISTRY_URL=http://nginx:8080/registry

docker login 成功了。但是 docker push 的时候,报错 401。在这里查了很久,才搞明白了原因。记录在最后面吧。

然后配置错误重试

upstream registry_both {
 server https://hub-1.corp.com/ max_fails=1;
 server https://hub-2.corp.com/ max_fails=1;
}

docker pull 测试失败了。然后查 Nginx 文档。

http_upstream 这里提到,有什么情况被认为是失败是可以配置的,具体在 http_proxy 这里配置。

不幸的是:

The cases of http_403 and http_404 are never considered unsuccessful attempts.

Nginx 有这种限制当然很合理。但我们的路也被堵死了。

结合现状再想一想

前面提到,我们目前的现状是使用 Harbor 自己的 Replication 来做镜像的复制,从 Hub-01 复制到 Hub-02 会有约一分钟延时。在这一分钟里面,如果从 Hub-02 拉镜像会失败。但这个是次要问题

我们要解决的主要问题是应用层面的数据不一致。

只是解决主要问题的话,其实 upstream 里面并不需要配置多个 server。再进一步,我们并不需要在 Nginx 里面配置一个 registry upstream。

把前面提到的配置恢复原样就好。只是保留数据库使用同一个。后面的存储(Ceph)各用各的。

对比

和现在的 HR 方案比有啥不一样呢?

优点很明显,这样做解决了主要矛盾,应用层面的那些不一致都被解决了。

缺点是,Hub-02 的应用层面数据和存储层面的数据有一些不一致了。
但这不是大问题,因为我想使用 Replication 来做 DR 的同学,都会把主域名指向 Hub-01 这个主 Harbor。在 Hub-01 出问题的时候才会切换指向 Hub-02。
所以这种不一致带来的问题,应该是极少会触发到。

后记

404

如果 Nginx 可以把 404 也配置成允许重试。那我们甚至可以直接使用本地磁盘来做 分布式 存储了。写的时候会随机写到一个机器上,读的时候会轮询到一个成功的。

Replication 配置

因为应用层面一样的,使用一个 DB。所以 Replicate To Hub-01 Hub-02 会同时出现在两个 Harbor 里面。
不管往哪一个 Harbor Push 镜像都会触发两次 Replicate。其中一个 Replicate 又会再触发两次。一共是 4 次。

那这里建议把5000端口打开,使用 Docker Registry 类型的 Registry 来做 Replication。会减少两次。

Redis 配置

Redis 必须每个 Harbor 使用自己独立的。

Push 401

我觉得 Harbor 这块处理的不是特别合理。下面详细说一下。

Docker Push 的时候,所有请求都是经过 Core 组件代理到 Registry,这点大家应该都知道了。

在 2.2.0 版本里面,Registry 是配置的 Basic Auth 认证。(v1.x 里面是使用的 Bearer 认证)

用户的请求(我们称之为原始请求,后面会用到)在 Core 组件里面会添加上 Basic Auth 的用户密码信息。但这里出现问题了:Harbor 代码并没有写死是添加 Basic Auth 认证信息,还是 Bearer 认证信息。Core 会先请求一下 /v2/ ,看一下返回里面是需要 Basic 还是 Bearer,然后再添加对应的认证信息。

Harbor 默认配置的配置下,这个 /v2/ 请求到了 Registry 组件这里,返回了 Basic 认证需求。

但注意了,Core 请求 /v2/ 的时候,并没有使用配置的 REGISTRY_URL,而是使用 req.Host + “/v2” 这样拼凑起来的。 拼出来一个 nginx:8080/v2,这个请求到了 core 组件这里,并返回了 Bearer 认证需求。

这是第一个我觉得不合理的地方,因为这块的代码是 Client.go 里面的,这个 Client 只是请求 Registry 使用的 Client,所以我觉得使用 REGISTRY_URL 更合理吧。

第二个我觉得不合理的地方,Harbor 代码里面还会验证,这个原始请求的路径是不是 /v2/ 打头的,再次不幸,我们的请求是 /registry/ 打头的,验证失败。所以 Harbor 不会继续添加认证信息。不管你返回了 Basic 还是 Bearer 都不会添加。
Harbor 这里写死了,是和 /v2/ 做比较,而不是和 REGISTRY_URL 做比较,这一点应该也不合理。

第三点是应该先验证,再去请求 /v2/ ,否则浪费了。

第四点是,既然 Registry 组件的认证方式是配置好的,Harbor 应该允许这个验证也可以配置。直接省去上面这些所有问题了。

2021-09-18 补充:http://hub.corp.com/v2//tags/list , 不管请求哪一边的 harbor,都会返回所有 tag,不管数据在不在。 所以需要使用 registry:5000 去请求。原因后面查。

Written with StackEdit.