ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Harbor镜像仓库高可用:HAProxy主备切换与故障演练

Harbor镜像仓库高可用:HAProxy主备切换与故障演练

Harbor 装起来确实快,一条install.sh跑完,几十分钟一个私有镜像仓库就活了。但部署快不代表能高枕无忧,我在实际维护中吃过亏:镜像仓库挂在 CI/CD 链路的咽喉位置,前面连着代码构建,后面接着应用发布,一旦它出问题,整个开发团队都会停下来等。最开始我只在一台机器上跑 Harbor,结果有一次镜像数据把磁盘写满,服务直接假死,线上发布窗口被卡了整整一个下午。从那以后,我就不再接受"单点 Harbor"这个选项。这篇文章就把我用 HAProxy 给 Harbor 做高可用的完整落地过程写一遍,包括架构设计、共享存储、外部数据库、双节点部署、负载均衡配置和故障演练,都是可以直接抄作业的内容。

1. 为什么镜像仓库必须做高可用

1.1 单点 Harbor 的隐患

很多团队觉得镜像仓库只是个"文件服务器",重要性比不上核心业务应用。这个认知在数据量小、发布频率低的时候没毛病,但一旦团队规模上来,镜像仓库就是事实上的单点故障源。

单点 Harbor 的风险主要集中在三个方面。第一是资源耗尽,Harbor 运行时会不断产生日志、临时文件、漏洞扫描数据,磁盘一旦写满,registry 组件会直接报 500,看起来像是应用假死,实际上所有构建和部署都在等它恢复。第二是容器状态丢失,Docker 容器本身是无状态的,Harbor 容器一重启,如果本地数据没有做好持久化,镜像说没就没,这对生产环境是毁灭性的。第三是升级和修复窗口困难,单机跑 Harbor,你想做版本升级、调配置、优化参数,都必须停服操作,而停服就意味着发布链路中断。

所以高可用这个事,不是我建议你做,而是只要你的团队每天依赖这个仓库拉镜像、推镜像,就应该把它当成生产基础设施来对待。

1.2 高可用方案怎么选

Harbor 高可用的核心思路就一句话:把有状态的东西抽出去,让应用层变成可以水平扩展的无状态节点。

有状态的东西有三个:镜像数据、数据库元数据、会话缓存。镜像数据要放到共享存储上,比如 NFS、NAS、云盘或者对象存储;数据库要换成外部 PostgreSQL;缓存要换成共享 Redis。做完这三件事,Harbor 节点本身就是"无状态"的,节点挂了随时可以替换。

应用层的部署形态有两条路线:双活和主备。双活是两个 Harbor 节点同时接收读写流量,由 HAProxy 做负载均衡。听起来资源利用率更高,但对底层存储的一致性要求很苛刻。镜像推送到一半的时候,registry 会在本地生成临时文件和锁状态,如果两个节点同时处理同一个 blob 的上传,共享存储上很容易出现写冲突。很多团队跑了双活之后,push 大镜像时偶发 manifest 冲突,排查起来很痛苦。

主备模式就简单直接:同一个时刻只有主节点接收流量,备节点处于 ready 状态但不接收业务流量,主节点挂了之后由 HAProxy 自动切换。这种模式对共享存储和数据库的压力小得多,故障切换时间在几秒到十几秒之间,对绝大多数发布场景完全够用。我目前在生产环境里给客户落地这套方案,用的就是主备模式,稳得很。

2. 高可用架构设计与组件准备

2.1 架构拓扑与流量走向

整套架构的流量走向非常清晰:客户端把harbor.example.com解析到 HAProxy 所在服务器的地址,所有 Docker 命令请求先打到 HAProxy,HAProxy 根据健康检查结果把请求转发给当前可用的 Harbor 节点。Harbor 节点之间通过共享存储和外部数据库保证数据一致,任一节点故障都不影响整体服务。

具体角色划分如下:

  • HAProxy 节点:对外暴露 443 端口,负责 TLS 终结、流量转发、健康检查和故障切换。
  • Harbor 节点 1:主节点,正常状态下承接所有读写流量。
  • Harbor 节点 2:备节点,和主节点使用同一套共享存储与外部数据库,平时不接流量,主节点宕机时自动接管。
  • NFS 存储:存放 Harbor 的data_volume数据,包括镜像层文件、配置、临时文件等。
  • PostgreSQL:存放 Harbor 的元数据,包括项目、镜像仓库、标签、用户权限等。
  • Redis:存放会话和缓存数据,避免切换节点后用户登录态丢失。

这里补充一点:HAProxy 本身也是单点,如果连 HAProxy 这台机器都挂了,整个链路照样断。所以更完整的方案会给 HAProxy 配 Keepalived 做 VIP 漂移,两台 HAProxy 一主一备。篇幅有限,本文把重点放在 Harbor 节点的高可用上,HAProxy 自身的双机热备我会在文末提一句,有需要可以自己扩展。

2.2 组件选型与版本搭配

我落地这套方案用的组件版本如下,都是经过生产验证的稳定组合:

组件版本作用
Harbor2.8 系列企业级镜像仓库,支持外部数据库与外部 Redis
HAProxy2.6 系列四七层负载均衡,健康检查与故障切换能力完善
PostgreSQL14 系列元数据存储,推荐独立部署或使用云数据库
Redis6.2 系列会话与缓存存储,使用独立实例
NFS4.1 协议共享镜像数据,也可替换为 NAS 或云盘

Harbor 的版本选择很关键。老版本对高可用的支持较弱,我之前用 1.x 版本做外部数据库时踩过依赖组件不全的坑。2.x 之后官方已经明确支持外部 PostgreSQL 和外部 Redis,并且组件之间通过 Redis 做分布式锁,多节点协同更可靠。HAProxy 用 2.x 是因为它的http-check send语法更灵活,健康检查可以带完整请求头,比老版本只写一个 URL 要精确得多。

2.3 端口规划与安全边界

规划端口时我给每个服务都做了清晰的分配,避免端口冲突和排查混乱:

服务部署位置端口说明
HAProxy 对外入口HAProxy 节点443Docker 客户端访问的统一地址
Harbor 节点 HTTP节点 1 / 节点 28080HAProxy 后端连接使用的内部端口
Harbor 节点 HTTPS节点 1 / 节点 28443直连测试用的 HTTPS 端口
PostgreSQL数据库主机5432只允许 Harbor 节点网段访问
Redis数据库主机6379只允许 Harbor 节点网段访问
NFS存储主机2049只允许 Harbor 节点网段访问

这里有个细节要注意:Harbor 默认会占用宿主机的 80 和 443 端口,我把它改成 8080 和 8443,是为了避免和 HAProxy 的 443 端口在客户端访问路径上产生歧义。你也可以不改,只要 HAProxy 后端连接用对应的端口就行,但改成非标准端口之后,直连某个节点测试时会更容易区分流量走的是谁。

3. 底层依赖先行:共享存储、外部数据库与缓存

3.1 NFS 共享存储配置

Harbor 的镜像数据都在data_volume目录下,两个 Harbor 节点必须把这一目录挂载到同一个共享存储上。我用的是 NFS,配置过程比较简单。

存储节点上先创建导出的根目录:

mkdir -p /data/nfs/harbor

编辑/etc/exports,把目录导出给内网网段:

/data/nfs/harbor 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)

注意no_root_squash这个参数不能少。Harbor 容器内部进程往存储目录写入时,用的是容器内的 root 或指定用户,如果 NFS 默认启用 root_squash,容器内 root 会被映射成匿名用户,写入直接 permission denied。加上这个参数可以避免大量莫名其妙的权限报错。

导出之后重启 NFS 服务:

exportfs -ra systemctl restart nfs-server

两个 Harbor 节点上执行挂载:

mkdir -p /data mount.nfs4 192.168.1.20:/data/nfs/harbor /data

为了重启后自动挂载,写入/etc/fstab:

192.168.1.20:/data/nfs/harbor /data nfs4 rw,hard,intr,timeo=600,retrans=2,noatime 0 0

把/data权限设置为 777 最省事,因为 Harbor 会在这个目录下创建很多子目录,某些子目录属主是容器内用户,直接给满权限一劳永逸。

3.2 外部 PostgreSQL 部署

PostgreSQL 我用独立主机部署,版本用的 14。如果团队有云数据库,直接用云厂商的实例也可以,关键是给 Harbor 建好专用的库和账号。

安装完成后执行:

sudo -u postgres psql

创建 Harbor 专用的账号和数据库:

CREATE USER harbor WITH PASSWORD 'HarborPGpass'; CREATE DATABASE harbor OWNER harbor;

Harbor 2.8 之后只需要一个harbor库就够用了。早期版本连 Trivy 还需要单独的harbor_clair库,很多朋友照着老教程抄,结果发现 2.8 执行install.sh时根本不会去碰那个库,白白浪费时间。

远程连接要允许 Harbor 节点网段访问。编辑pg_hba.conf,在文件末尾加上:

host all harbor 192.168.1.0/24 md5

然后修改postgresql.conf里的listen_addresses,重启服务:

alter system set listen_addresses = '192.168.1.30'; systemctl restart postgresql

这里有一个容易忽略的点:Harbor 连接数据库时会创建连接池,连接数取决于max_open_conns。如果 PostgreSQL 默认的max_connections是 100,而你把max_open_conns也设成 100,再算上其他连接,数据库很容易被打满。建议 Harbor 配置里max_open_conns不要超过 50,给数据库预留一些余量。

3.3 Redis 共享缓存配置

Redis 的作用经常被低估。Harbor 的 Web 会话、registry 的某些缓存状态、分布式锁都依赖 Redis。两个 Harbor 节点必须使用同一个 Redis 实例,否则用户登录状态和跨节点的数据一致性都会出问题。

安装 Redis 后做三件事:

bind 192.168.1.30 requirepass HarborRedispass

重启服务后,在两个 Harbor 节点的harbor.yml里配置同一个 Redis 地址和密码。Redis 不需要开持久化,因为这里存的是缓存和会话数据,丢了最多让人重新登录一次,不会丢核心数据。但 Redis 所在的机器最好有充足内存,别和 PostgreSQL 挤在同一台低配机器上,否则镜像并发推送时容易出现延迟。

4. Harbor 双节点部署实操

4.1 harbor.yml 核心配置

从官网下载 Harbor 离线安装包后,进入目录复制模板:

tar xzvf harbor-offline-installer-v2.8.4.tgz cd harbor cp harbor.yml.tmpl harbor.yml

编辑harbor.yml,核心配置如下:

hostname: harbor.example.com http: port: 8080 https: port: 8443 certificate: /data/cert/harbor.crt private_key: /data/cert/harbor.key harbor_admin_password: HarborAdmin123 data_volume: /data database: type: external external: host: 192.168.1.30 port: 5432 db_name: harbor username: harbor password: HarborPGpass sslmode: disable max_idle_conns: 5 max_open_conns: 50 redis: type: external external: host: 192.168.1.30 port: 6379 password: HarborRedispass db: 0

注意hostname两个节点必须完全一致,都填同一个对外域名。Harbor 会根据这个值生成 registry 的访问地址,如果节点 2 的 hostname 填成节点 2 自己的 IP,Docker 客户端从 HAProxy 转发过来之后,某些 API 返回的地址会错乱,导致 push 失败。

证书文件要提前放到/data/cert下,Harbor 的 HTTPS 端口需要用到。如果你打算让 HAProxy 终结 TLS,Harbor 节点的证书可以直接用同一张,也可以临时生成一张自签的,后面统一替换。

4.2 部署首个节点并初始化数据库

配置好harbor.yml后,执行安装脚本:

sudo ./install.sh --with-trivy

首次安装会做数据库初始化、迁移、创建容器组件。这个过程在节点 1 上必须完整跑完,让数据库 schema 迁到最新版本。跑完之后看下所有容器状态:

sudo docker-compose ps

正常情况下所有组件状态都是 healthy。如果某个组件一直处于 restarting 状态,大概率是配置问题,先看日志排查:

sudo docker-compose logs -f core

节点 1 初始化完成后,外部数据库里已经存在 Harbor 的表结构和基础数据。这是整个部署过程中最关键的一步,因为后面节点 2 启动时会复用这套 schema,绝不能在节点 2 上再执行一次数据库初始化。

4.3 部署第二个节点

节点 2 的部署方式几乎和节点 1 一模一样,但有几个细节要格外小心。

把节点 1 那份已经验证过的harbor.yml直接复制到节点 2,不要重新从模板生成,否则容易漏掉外部数据库和外部 Redis 的配置。然后执行同样的安装命令:

sudo ./install.sh --with-trivy

节点 2 启动时,Harbor 会检测外部数据库已有数据,自动跳过初始化迁移,直接拉起组件。如果日志里出现 migration 失败,不要硬着头皮重试,先检查数据库版本是否和 Harbor 版本匹配。我遇到过一次节点 2 用的离线包和节点 1 不是同一个微版本,结果数据库迁移直接报了版本冲突。

两个节点都起来之后,确认节点 2 不会尝试重置管理员密码。因为管理员密码存在外部数据库中,节点 2 的harbor_admin_password即使和节点 1 不同,也不会覆盖数据库里的密码。实际登录时用的还是节点 1 部署时设置的那个密码。

4.4 双节点功能自检

直接访问节点 IP 做健康检查,确认核心 API 正常:

curl -k https://192.168.1.10:8443/api/v2.0/ping curl -k https://192.168.1.11:8443/api/v2.0/ping

返回 200 说明核心服务正常。接着验证登录和 push 流程,可以先用 docker 客户端登录:

echo 'HarborAdmin123' | docker login harbor.example.com -u admin --password-stdin

两个节点都自检通过后,就可以切到 HAProxy 配置了。

5. HAProxy 负载均衡配置核心细节

5.1 安装与证书准备

HAProxy 我单独放在一台低配虚拟机上,2C4G 就够跑,主要吃带宽和连接数。安装:

apt install haproxy

TLS 证书直接复用 Harbor 的那张证书。HAProxy 需要的是 PEM 格式,把证书和私钥合并成一个文件:

mkdir -p /etc/haproxy/certs cat /data/cert/harbor.crt /data/cert/harbor.key > /etc/haproxy/certs/harbor-example.pem chmod 600 /etc/haproxy/certs/harbor-example.pem

这里建议直接用一张包含harbor.example.com、节点 1 IP、节点 2 IP 的 SAN 证书,这样无论客户端最终连到哪个节点,证书校验都能通过。如果证书和域名不匹配,客户端的 docker daemon 会拒绝连接,终端会报 unknown certificate 错误。

5.2 主备模式的 HAProxy 配置

编辑/etc/haproxy/haproxy.cfg,完整配置如下:

global log /dev/log local0 info maxconn 50000 user haproxy group haproxy daemon stats socket /run/haproxy/admin.sock mode 660 level admin spread-checks 5 defaults mode http log global option httplog option dontlognull option redispatch timeout connect 10s timeout client 900s timeout server 900s timeout check 10s frontend harbor_https bind *:443 ssl crt /etc/haproxy/certs/harbor-example.pem http-request set-header X-Forwarded-Proto https option forwardfor default_backend harbor_nodes backend harbor_nodes balance leastconn option httpchk http-check send meth GET uri /api/v2.0/ping hdr Host harbor.example.com http-check expect status 200 server harbor01 192.168.1.10:8080 check inter 3s fall 2 rise 2 server harbor02 192.168.1.11:8080 check inter 3s fall 2 rise 2 backup listen stats bind *:9101 mode http stats enable stats uri /stats stats refresh 5s stats auth monitor:Haproxy@123

关键点是harbor02后面的backup关键字。HAProxy 的调度策略是只要存在可用的非 backup 服务器,就绝不把新连接发给 backup 服务器。也就是说,正常情况流量全部走harbor01,harbor01宕机后harbor02自动接管。这正好实现了我前面说的主备模式。

5.3 健康检查参数拆解

很多人配置健康检查只写一个check就完事,但这样只是 TCP 层的端口探测,Harbor 进程没死透但业务已经不可用时,端口照样能连上,流量还是会打过来。

所以这里用option httpchk加上http-check send,向/api/v2.0/ping发送请求并要求返回 200。这个接口是整个 Harbor 对外服务的心跳口,只要核心 API 组件活着,这个接口就正常;只要这个接口出问题,说明 Harbor 已经无法正常服务业务,健康检查就会立刻标记节点 DOWN。

参数解释:

  • inter 3s:每 3 秒发送一次健康检查。
  • fall 2:连续两次失败才判定节点 DOWN,避免网络抖动误切换。
  • rise 2:连续两次成功才判定节点 UP,避免刚恢复就立刻接流量又被冲垮。
  • spread-checks 5:给健康检查增加 5% 的随机时间偏置,避免两个节点的检查请求同时打过来形成节奏性冲击。

按照这个配置,主节点从故障到切换完成的时间大约在 3 到 6 秒之间。如果你希望切换更快,可以把inter缩短到 2 秒、fall降为 1,但会增加误判风险,我建议保持这个平衡。

5.4 统计监控页面与日志

配置里listen stats段启动了一个统计服务,访问http://HAProxy节点IP:9101/stats,输入配置的账号密码就能看到两个后端节点的实时状态。页面里会清楚显示主节点状态为 UP、备节点状态为 UP (backup),一旦主节点宕机,它的状态变成 DOWN,流量全跑到备节点上。这个页面也是我做故障演练时的第一观察入口。

HAProxy 日志默认输出到系统日志,可以在/var/log/haproxy.log里看到转发记录。我把option httplog打开之后,每条请求都能看到源 IP、目标节点、状态码。故障排查时最常用的操作就是:

tailf /var/log/haproxy.log

重点看有没有 503 状态码,出现 503 说明当前没有可用后端,要么两个节点都挂了,要么健康检查配置有问题。

5.5 针对大镜像场景的超时调优

默认的timeout client和timeout server经常不够用。Docker 客户端 push 一个几个 GB 的大镜像时,整个数据传输过程可能持续几分钟,而 HAProxy 默认的 server 超时只有几十秒。一旦超过时限,HAProxy 会主动切断连接,客户端就会报 unexpected EOF 或者连接重置。

我把两个超时都设成 900 秒,基本可以覆盖绝大部分生产场景。如果你的团队经常推送超大模型镜像或者离线安装包,可以再往上调。这个参数改完之后的效果立竿见影,之前偶尔出现的"大镜像 push 到一半断了"的诡异问题瞬间消失。

6. 故障切换演练与高频问题排查

6.1 手动模拟主节点宕机

方案落地后一定要做一次故障演练,别等真出事了再验证配置有没有用。我的演练流程如下。

先在测试机器上推送一个镜像到 Harbor,确认当前后端节点是主节点harbor01。然后到主节点上直接停掉 Harbor 服务:

cd /data/harbor && sudo docker-compose down

此时立即观察 HAProxy 统计页面,大约 6 秒后主节点状态从 UP 变成 DOWN,同时备节点状态从 UP (backup) 变成 UP 并从右侧流量计数里看到请求开始进来。

再执行一次 docker 登录和 push:

echo 'HarborAdmin123' | docker login harbor.example.com -u admin --password-stdin docker push harbor.example.com/test/demo:v1

一切正常,说明故障切换生效。

最后恢复主节点:

cd /data/harbor && sudo docker-compose up -d

等待容器全部 healthy,再观察 HAProxy 统计页面,主节点恢复 UP 状态并自动重新接管流量,备节点退回 backup 状态。整个过程不需要人工干预,Docker 客户端也无感知。

6.2 高频问题速查表

现象可能原因解决办法
健康检查失败,HAProxy 返回 503Harbor 容器未启动或 NFS 挂载断开查看docker-compose ps,检查mount状态,重新挂载 /data
ping 接口返回 200 但 docker login 报证书错误客户端不信任 Harbor 证书将 Harbor 证书加入 Docker 客户端信任列表或配置 insecure-registries 后重启 docker
主备切换后 push 报 manifest 冲突旧连接残留在已停止节点上等待旧连接自然断开,或在客户端重新登录再 push
NFS 目录写入 permission deniedroot_squash 生效或目录属主不匹配exports 配置 no_root_squash,并将 /data 权限设为 777
PostgreSQL 连接数打满max_open_conns 配得过大将 max_open_conns 调整为 30-50,并把 PG max_connections 调大
双节点部署后 UI 访问偶发 401Redis 未共用或 session 不一致确认两个节点的 redis.external 配置指向同一个实例
大镜像 push 到一半断开HAProxy 超时配置过小将 timeout client 和 timeout server 调大到 900s

6.3 生产环境避坑心得

整套方案跑起来不难,真正难的是那些文档里不写的细节。第一个坑是双节点不要同时跑install.sh。我有一次图省事,两个节点同时执行安装脚本,结果外部数据库的迁移锁被两个进程同时抢占,Harbor 任务表的数据序列直接错乱,最后只能删掉数据库重建。正确做法是老老实实先跑节点 1,确认数据迁移完成再跑节点 2。

第二个坑是 NFS 性能。镜像仓库是 IO 密集型场景,尤其多人同时 push 时,底层存储压力非常大。千兆网卡在并发超过几十个连接时就会成为瓶颈,表现为 push 速度很慢、健康检查偶发超时。建议至少上万兆内网,或者直接用云盘、NAS。性能测试时别只测单线程 push,要模拟多团队并发推镜像的真实场景。

第三个坑是证书和域名的一致性。HAProxy、Harbor 节点、客户端三方的域名和证书必须统一。最省心的方法是把同一个 PEM 文件复制到所有节点,并确保证书 SAN 包含对外域名。客户端侧在/etc/docker/daemon.json里把harbor.example.com加进insecure-registries或者配置系统 CA,这样就不会出现"测试环境能登录,生产环境一直报错"的尴尬。

第四个坑是高可用不等于备份。即使做了双节点主备,也不代表数据绝对安全,因为两个节点用的是同一份 NFS 和同一个数据库,一旦存储设备整体损坏,两个节点一起完蛋。所以镜像数据的外部备份仍然是必须的,定期将 NFS 数据复制到异地或者推到云对象存储,才是真正的最后一道防线。

我个人在实际操作中的体会是:高可用方案的价值不在技术含量,而在稳定性和可维护性。主备模式确实不上双活那些华丽的概念,但它够简单、够可靠,切换逻辑一眼就能看懂,出了问题也好排查。这套架构我前后在好几个环境跑了一年多,故障切换时间稳定在十秒内,平时几乎感觉不到它的存在,但每次演练都在提醒我,这份"存在感低"的配置,正是整个发布链路最关键的保险绳。最后再给一个实用建议:方案全部落地后,把故障演练排进运维计划,每季度至少做一次,不要等到镜像仓库真的挂了才验证配置是否有效。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进