ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Madeira构建企业级镜像仓库:K8s原生部署与运维实践

基于Madeira构建企业级镜像仓库:K8s原生部署与运维实践 1. 项目整体设计与思路拆解做容器化落地这两年镜像仓库一直是个容易被低估但又极其关键的基础设施。团队规模过了十几人业务模块拆成微服务架构之后镜像的产出频率、版本数量、并发推送量都会出现指数级增长公共仓库和简单的私有Registry很难撑住。这时候需要一个真正能承载完整交付链路、具备企业级能力的镜像仓库系统而我最近在一个中型项目里落地的方案就是基于Madeira这套体系来构建的。先交代一下背景。我们团队当时的情况是开发环境基于KubernetesCI流水线每天要构建40到60个镜像测试环境、预发环境、生产环境分开部署每个环境需要对应的镜像Tag和版本策略。之前用的方案是Harbor的社区版本功能上没问题但在资源占用、高可用部署、增量同步这几个方面开始逐渐暴露出一些让人头疼的问题尤其是当镜像数量突破一万、磁盘和数据库压力上来之后GC垃圾回收策略、缓存命中率、跨机房复制这些细节变得格外敏感。所以当我们评估新一代镜像仓库方案时Madeira进入了视野。可能很多人对Madeira这个名字还有点陌生。它其实是Harbor生态里的一个重要分支或者说增强版实现核心目标很明确在保持Harbor原有仓库管理、复制、RBAC权限这些成熟能力的基础上针对大规模镜像存储、高并发推送场景、异构存储后端做了大量优化。简单说它本质上是一个更轻、更快、更适合在Kubernetes原生环境里跑起来的镜像仓库方案。我当时的判断标准有三个第一是否原生于K8s环境运维是否省心第二对象存储对接能力是否够灵活能不能直接复用已有的MinIO或S3集群第三高可用和迁移成本是否可控。Madeira在这三个维度上都比老方案更贴合我们的实际情况所以最终选定它为镜像仓库的核心底座。选型环节里还有一个很重要的思路。过去我们容易把镜像仓库当成一个“能存就行”的工具实际跑过之后才会理解它的性能瓶颈通常不在磁盘本身而在于元数据管理、并发锁竞争、存储驱动与底层文件系统的适配性。举个很直观的例子传统Registry使用本地文件系统存储时镜像层文件一多目录遍历的开销就会显著拉高尤其是大镜像比如Java应用动辄1GB以上推送和拉取时IO延迟和锁等待很容易让流水线卡住。Madeira在设计上把存储抽象层和元数据索引做了彻底解耦既可以跑在本地磁盘也可以无缝切到对象存储这个“存储无关”的架构思路是我们决定投入它去做技术改造的关键理由。选型不是在真空中做的还要考虑团队的技术栈现状。我们那边的K8s集群已经比较成熟Prometheus监控、EFK日志、GitLab CI这些基础设施都已就位。所以理想的镜像仓库方案应该能天然融入这套体系而不是再引入一大坨新的组件。Madeira的部署架构恰好是“一条Helm命令拉起依赖组件可选、可替换”这个对我们在已有集群里落地特别友好。后面部署章节我会把具体参数和步骤写清楚。这一整轮下来我的核心感受是选镜像仓库方案本质上不是选一个工具而是选一种和现有CI/CD流程、存储体系、团队运维能力匹配的“交付基础设施”。Madeira能跑通这条链路核心原因是它在架构层就已经把扩展性和可运维性放在了一个很高的优先级上。2. 快速部署与核心配置要点2.1 部署前的环境评估与组件规划部署任何一套基础设施第一步都不是敲命令而是想清楚拓扑。我这次做了一个单集群内的生产级部署用的是Kubernetes Helm的方式整体环境如下Kubernetes 版本 1.24三个Master节点、五个Worker节点之前已经跑着Ingress-Nginx和Cert-Manager存储端一套独立的MinIO集群4节点纠删码模式专门给镜像仓库做对象存储后端数据库高可用的PostgreSQL云厂商托管版本14缓存Redis 6.x单节点部署用于缓存访问令牌和部分元数据网络集群内网带宽万兆外部访问通过Ingress TLS终结这里我建议第一次做技术评估的同学画一张“组件依赖表”把每个组件的用途、故障影响、替代方案都写清楚。我当时的表格长这样组件用途不可用影响替代方案PostgreSQL存储镜像元数据、项目、用户、RBAC策略仓库不可读写平台功能基本瘫痪内置的轻量数据库仅测试环境用Redis会话缓存、令牌缓存、并发锁登录失效、性能下降但不至于丢数据可暂时禁用但高并发下会明显变慢MinIO/S3镜像Blob存储拉取和推送全部失败本地文件系统但容量和扩展性受限Ingress外部流量入口、TLS终结仓库地址无法访问NodePort直连仅临时方案组件规划的本质是把“不确定因素”提前暴露出来。比如如果你决定用内置的数据库可能短平快很舒服但等镜像数量到了几千上万元数据查询就是明显瓶颈尤其是镜像列表、Tag列表、清理任务这些高频操作会越来越慢。所以生产环境我强烈建议外部数据库和对象存储一步到位别想着后面再迁移迁移永远比重来一遍更痛苦。2.2 Helm部署的完整过程与参数解释我用的是官方Chart仓库当时拿到的版本是3.x具体步骤记录如下helm repo add madeira-repo https://example.com/madeira-charts helm repo update kubectl create namespace madeira接下来是values.yaml的编写环节这也是整个部署过程中最需要静下心做的地方。一个最简可用的配置是expose: type: ingress tls: enabled: true certSource: secret secret: secretName: madeira-cert ingress: hosts: core: harbor.madeira.example.com className: nginx externalURL: https://harbor.madeira.example.com persistence: enabled: true persistentVolumeClaim: registry: existingClaim: storageClass: local-path database: existingClaim: storageClass: local-path redis: existingClaim: storageClass: local-path database: type: external external: host: postgres.example.com port: 5432 username: harbor password: your-db-password coreDatabase: harbor_core # 其余数据库标识按需填写 redis: type: external external: addr: redis.example.com:6379 password: your-redis-password storageService: type: object s3: region: us-east-1 bucket: harbor-images accesskey: your-minio-access-key secretkey: your-minio-secret-key endpoint: http://minio.example.com:9000 secure: false # 注意MinIO默认走HTTP如果启用了TLSsecure改为true这里有几个参数我特意想展开聊一下因为踩过坑之后记忆格外深。首先是persistence.persistentVolumeClaim这一段。很多人会想既然存储都用了对象存储为什么还需要PVC答案是Registry内部还有一层相对小的本地缓存和临时空间用于暂存上传的分片数据、缓存签名信息等。这层存储不需要很大但需要稳定、低延迟的块存储。我这里用了local-path这个StorageClass性能很好但需要注意local-path的Pod漂移问题——如果Registry的Pod被调度到别的节点本地数据可能会丢。好在这层数据不是持久业务数据丢了可以自动恢复影响可控。如果你的环境有成熟的NFS或块存储类StorageClass优先用那个能省掉很多排查精力。然后是externalURL。这个参数必须和Ingress的实际访问地址完全一致否则在推送镜像时会报HTTPS证书错误或者401认证失败。我之前看到有人在values文件里写的还是默认的http://localhost结果客户端docker login一直报错查了半天才发现是这个低级失误。改完之后记得让Helm重新渲染。执行安装一条命令helm install madeira madeira-repo/madeira -n madeira -f values.yaml安装完成后检查状态kubectl get pods -n madeira正常情况下Core、Registry、Portal、JobService这几个核心组件都应该处于Running状态。有个细节首次启动时Portal的Pod可能比Core先就绪但页面访问偶尔会报503等JobService真正起来了之后再访问就正常了。这时候不要急着去排查等一两分钟做一次全组件健康检查再动手。2.3 存储后端对接与性能实测存储后端我这里用的是MinIO按官方S3协议对接。这里有两个容易踩坑的点第一endpoint的写法。如果你的MinIO服务是minio.example.com:9000在s3.endpoint里要写完整的主机和端口但不要在末尾加路径也不要加http://之外的东西。我见过有人把bucket路径也拼进去了结果Registry启动时报“Invalid Bucket Name”之类的错误。第二MinIO的bucket需要提前创建。Madeira启动时不会自动创建bucket如果bucket不存在Registry会一直报错但错误信息并不直观。我建议部署前先用mc客户端先把bucket建好并且做好访问Key和Secret的权限隔离只给这个bucket的读写权限不要用全局管理员Key。这既是安全习惯也能避免误删其他桶的数据。存储对接完成后我做了两轮基础性能验证推送测试一个约800MB的镜像压缩后约300MB从构建机推送到仓库耗时约8秒万兆内网这个数据在可接受范围。拉取测试模拟K8s节点拉取同一镜像冷启动状态下耗时约5秒热缓存情况下几乎瞬时。实测下来对象存储后端对大批量小文件的并发处理能力明显优于本地文件系统。主要原因在于对象存储的写入路径是HTTP调用没有POSIX文件系统和inode锁的竞争问题尤其适合镜像仓库这种读多写多、小文件密集的场景。如果你还在用本地磁盘跑Registry并发推送的镜像一多能明显感觉到延迟波动那就是文件系统层级在打架。2.4 Ingress暴露与TLS证书配置对外访问我用的是Ingress-Nginx Cert-Manager自动签发证书的模式。在values.yaml中指定的TLS Secret是madeira-cert所以需要先创建一个自签证书或者提前从Lets Encrypt申请好。因为仓库要应对的是内网和公网的混合访问场景我建议在Ingress上加两个注解一个是nginx.ingress.kubernetes.io/proxy-body-size设成0不限制请求体大小否则Ingress默认的1MB限制会把大镜像推送直接卡死另一个是nginx.ingress.kubernetes.io/proxy-read-timeout设成600因为大镜像推送到一半可能会触发读取超时。这两条注解是我个人认为最高性价比的调优项。镜像仓库的Ingress和普通Web应用的Ingress在流量特征上有本质区别——请求体动辄几百MB甚至上GB连接持续时间长如果直接用通用Ingress配置大概率会在第一个大镜像推送时就暴露问题。3. 日常运维与实用操作经验3.1 项目管理与镜像清理策略镜像仓库跑起来之后最常被忽略但又影响最深远的就是镜像空间的增长曲线。我们项目跑了两个月之后镜像总计到达了1.5TB里面包含大量过期的测试构建产物。这时候我才认真把镜像清理和GC策略纳入日常运维范畴。Madeira继承了Harbor的清理机制核心是“规则清除”和“GC”两步。规则清除是逻辑删除它会把符合条件的Tag标记为待清理这些Tag不再出现在UI列表和API接口中但底层Blob数据还占着磁盘或对象存储空间。真正释放空间需要触发GCGC过程会扫描所有未被引用的Blob并删除。我设了两条规则保留每个项目最近30天的镜像保留每个项目最近50个版本的镜像这个策略对周期性发版的项目非常适用。如果你的项目每天构建十几个版本30天加上最近50条的兜底规则基本能做到既不丢失有效历史版本又能把存储增长控制在一个合理区间。这里需要提醒一点规则设置时要考虑“至少保留”和“最多保留”两个维度不要只设时间维度。因为有些历史镜像虽然时间很久了但可能还是某个环境正在用的版本只按时间清理会误删。清理的执行入口可以用UI操作也可以在JobService里定义定时的清理任务。我这边是直接写了一个CronJob每周日凌晨跑一次规则清除和GCapiVersion: batch/v1 kind: CronJob metadata: name: harbor-gc-cron namespace: madeira spec: schedule: 0 3 * * 0 jobTemplate: spec: template: spec: containers: - name: harbor-gc image: your-registry/gc-runner:1.0 restartPolicy: OnFailureGC运行期间可以关注JobService的日志里面会输出类似deleting blob: sha256:...的记录可以据此判断清理是否正常推进。GC过程中Registry不会完全停摆但大规模清理时IO和CPU负载会明显上升如果是单节点部署建议还是放在业务低峰期跑。3.2 安全加固与访问控制安全这块我们做了三层第一层是TLS全链路加密。外部访问走Ingress的HTTPS内部Pod访问Registry走集群内网的HTTP跳转Ingress层把明文的HTTP请求全部重定向到HTTPS。做法是在Ingress注解里加上nginx.ingress.kubernetes.io/ssl-redirect: true第二层是RBAC权限管理。Madeira默认提供项目级别的用户体系我们按团队拆分项目每个项目配置一个管理员和若干开发人员。开发人员的权限只到“推送和拉取”管理员才能修改项目配置和触发垃圾回收。第三层是拉取凭证的自动轮换。K8s节点拉取镜像时不能把用户名密码长期写在docker config里。我们使用了imagePullSecrets和K8s的Secret自动同步机制每30天轮换一次凭证。这个做法能有效降低因为密钥泄漏导致仓库被拉空的风险。访问控制的逻辑里有一件事特别值得提不要所有团队共用一个机器人账号。每个项目组建一个独立的机器人账号既能做权限隔离也能在审计日志中精确定位到操作来源。Madeira的审计日志很全什么时间、哪个用户、对哪个镜像做了什么操作都记录在案出了问题追查起来非常方便。这在多人协作的团队里特别有价值。3.3 监控告警与容量规划仓库不是装完就能撒手的它需要有指标监控和告警。我们用的是Prometheus Grafana的常见组合Madeira本身暴露了/metrics端点直接把指标接过来即可。我重点关注以下几个指标Registry的请求QPS和错误率特别是5xx比例存储桶的容量和增长率GC任务执行时长和失败次数Core服务的响应延迟P99告警阈值我的设置经验是存储用量超过桶容量的80%立即告警这个容量是需要提前扩容的预警线5xx错误率连续5分钟超过1%立即告警GC执行失败连续2次告警并检查JobService状态容量规划是整个运维过程中最考验“大局观”的部分。我一般按“周增量”去做推演先看过去两周的存储增长曲线算出一周平均增量然后乘以一个1.5到2的缓冲系数得出未来一两个月的大致需求。比如每周涨80GB那两个月大约需要640GB到1.28TB的增量空间。提前和存储团队打好招呼别等到桶满了才想起扩容。4. 技术架构解析与性能调优4.1 Madeira的核心组件与协作流程Madeira整体上沿用了Harbor的分层架构但内部几个关键组件做了轻量化重构这也是它能比传统Harbor更适配K8s环境的原因。核心组件包括Core负责API接口、认证鉴权、项目管理和用户策略Registry处理镜像上传下载的实际IO操作核心是Docker Registry的增强版JobService异步任务执行器负责复制、GC、清理扫描等后台任务PortalWeb用户界面方便日常管理和排查Trivy扫描器可选对镜像做漏洞扫描从一条镜像推送的完整链路来看客户端调用Core的API进行认证认证通过后获取一个上传/下载的令牌然后用这个令牌和Registry通信。Registry收到请求后把Blob数据写入存储后端对象存储或本地文件系统同时把元数据信息同步到Core由Core写入数据库。整个链路有几个非常关键的并发控制点如果处理不好高并发下很容易出现数据不一致或者超时。从日常使用的角度我不会去改动它的内部代码逻辑但理解这个通信链路对排查问题非常有帮助。比如你发现“拉取镜像时报unauthorized”但登录是成功的那问题多半出在令牌过期或Project内的角色权限配置上而不会是Registry的存储问题。理解了这一层排查方向就不会跑偏。4.2 Registry深入调优从缓存到存储如果说Core是大脑Registry就是镜像仓库的心脏。它的IO路径直接影响用户体感所以我把Registry的调优单独拎出来说。Registry的配置在K8s Secret里核心参数包括registry: storage: cache: layerinfo: redis delete: enabled: true middleware: registry: name: harborlayerinfo: redis这行很重要它表示把镜像层信息的缓存放到Redis减轻数据库的查询压力。当你拉取一个多人共享的基础镜像时大量Pod同时请求同一个Layer如果没有缓存层数据库和对象存储都会被高频打到。加了Redis缓存之后缓存命中的情况下拉取请求可以直接走缓存返回元数据速度提升非常明显。还有一个容易被忽略的参数是delete.enabled: true。如果不开启这个选项前台的Tag清理操作不会真正释放Blob存储空间。很多人的镜像仓库容量只增不减大概率就是忘了开这个开关。我建议部署完成后第一件事就是去Registry的配置文件里确认这个值。另外关于并发调优Registry内部有一个上传下载的并发控制默认值比较保守。如果你的网络条件很好万兆内网可以适当调高。我这边把并发下载数从默认值调到了20并发上传也做了对应的调整实测大批量Pod同时拉取镜像时整体吞吐有明显改善。调整Registry配置需要重启Registry的Pod会影响正在进行的推送拉取操作所以务必选择业务低峰期操作。另外Registry是Deployment部署修改配置后建议滚动更新而不是直接删除Pod。4.3 CPU与内存资源分配心得在K8s里部署资源请求和限制的设置直接关系到稳定性。我这里给出一个经验值范围大家可以根据自己的业务负载调整组件CPU Limit内存 Limit说明Core2核4GB高并发时对内存的需求明显太小的Limit容易OOMRegistry4核8GB大镜像推送和GC任务期间内存占用会显著上升JobService1核2GB任务量不大但不要设成0Portal0.5核512MB管理界面资源占用很小这里要特别提醒Registry的内存。大镜像推送过程中Registry会先在本地做分片数据的暂存和校验内存占用会随镜像大小波动。如果内存Limit设得太小可能出现推送中途Pod被OOMKilled的情况而且这个问题只会在推大镜像时暴露平时小镜像一切正常很难排查。我一开始给Registry设了4GB的Limit推一个1.5GB的镜像就崩了一次后来调到8GB才算稳定。还要注意和HPA水平自动伸缩配合的问题。Registry是无状态服务可以配HPA。但注意GC任务在JobService里执行不会因为HPA自动扩展而提升效率所以JobService的规模还是要靠人工评估。GC的瓶颈通常在数据库查询和存储IO不是Pod副本数。5. 常见故障与排查思路速查5.1 推送失败与认证报错推送失败是最常见的问题场景和原因多种多样。我把典型的几类问题整理成了一张速查表问题现象可能原因排查思路推送时报 401 Unauthorized机器人账号密钥过期、项目角色权限不足检查Secret是否轮换查看Core日志定位认证失败原因推送时报 413 Request Entity Too LargeIngress的proxy-body-size限制确认Ingress注解中proxy-body-size已设为0推送大镜像时报 502 Bad GatewayRegistry内存不足被OOMKilled或nginx超时查看Registry Pod状态和重启记录适当调高内存Limit推送时报 503 Service Unavailable数据库连接满或JobService异常检查数据库连接池和JobService Pod状态遇到过最隐蔽的一个问题是内网DNS解析。我们有多个环境的K8s集群每个集群内的Registry地址都是同一个域名但DNS解析指向的IP是在不同VPC里的偶尔会把跨环境的请求路由到错误的目标上。排查了很久才发现是Core配置中存储了某个固定IP的地址导致跨网络访问时出现间歇性失败。后来统一规范了所有访问地址都走域名不用IP这个问题彻底消失。5.2 GC任务卡住与磁盘空间不释放GC卡住是运维中被问得最多的问题之一。现象是清理规则执行成功Tag在界面上消失了但对象存储的容量一直没降下来或者GC任务状态一直是Running但几个小时都不结束。排查思路分步骤先看JobService的日志确认GC任务是否还在扫描Blob。如果日志停留在某个Blob长时间不动可能是该Blob对应的对象存储访问异常。检查对象存储的访问密钥是否过期或权限被修改。GC任务删除Blob需要存储桶的DeleteObject权限如果密钥只给了读写而没有删除权限删除操作就会静默失败。检查数据库的GC记录表看看实际执行进度。另一个特别容易踩的坑是多个项目共享同一个存储桶时GC会扫描整个桶的Blob但只会删除被标记为“孤儿”的Blob。如果一个Blob被多个项目引用但它已经有一个项目做了清理标记GC可能不会马上释放空间需要等所有引用关系都解除了才会被真正删除。这个机制本身是合理的但理解它之后你就不会对着“清理完但空间没变化”的现象干着急了。5.3 存储后端故障与容灾演练对象存储后端也不是100%可靠。MinIO出过一次节点故障表现为部分新推送的镜像可以写成功但拉取时极不稳定一会儿能拉一会儿报404。当时排查下来是MinIO集群的纠删码模式下某个节点磁盘挂了读操作需要从多个分片恢复数据而恢复过程会超时。这种场景下的应对思路是准备一个备用的对象存储桶当主桶访问异常时通过修改Registry配置切换桶定期做容灾演练检验切换桶后数据的完整性和可用性不要把所有鸡蛋放在同一个存储集群里如果条件允许至少做两个不同的存储后端镜像仓库的高可用设计一定要提前做不能等到故障发生时才临阵磨枪。哪怕只是每季度做一次模拟故障演练也能在真正出问题时减少很多慌乱。6. 用户体验与管理细节优化6.1 镜像命名规范与版本策略有一件事我觉得值得所有团队重视镜像命名和Tag策略是仓库管理中最容易被忽视但受益最大的环节。我们这边定的规范是项目名/应用名:环境标识-版本号-commit短哈希。这样一个镜像拉取下来不需要额外的文档就能追溯它是哪个环境、哪个版本、哪个代码提交构建出来的。比如order-service/order-api:prod-2.3.1-a1b2c3d一下子就能看懂。Tag策略的执行需要配合CI/CD流水线。GitLab CI里每个Job对应一个环境构建完成后用规范好的Tag推送到仓库。这个规范执行了一个多月之后开发人员再互相协作时拉取镜像再也不用反复确认是哪个版本效率提升非常明显。6.2 团队培训与使用规范镜像仓库用得好不好不光靠技术还靠团队的使用习惯。我在落地过程中给团队做过两次内部培训核心内容就三条不要随意推送没有明确Tag的latest镜像latest应该只由CI自动打不要把生产环境的镜像Tag覆盖掉所有Tag都应该是不可变的定期清理自己不再使用的开发分支镜像配合界面权限控制普通开发人员默认不能删除镜像只有项目管理员能执行删除操作。这样即使有人误操作也有管理员兜底不会导致整个项目镜像被清空。另外在Portal的界面管理上可以开启项目的“自动清理”策略针对那些明确只用于测试的项目设一个比较短的保留时间比如3天或7天。开发环境中产生的大量临时镜像在规则的约束下会自动清理不需要人工干预。7. 半个月使用下来的体会最后分享一点个人经验。这套Madeira体系的镜像仓库跑了大约一个半月之后我们整个CI/CD链路的稳定性有了肉眼可见的提升。一个明显的例子是以前高峰期并发推送10个镜像偶发出现推送超时和重试现在同样的并发量几乎没有再出现因为仓库原因导致的构建失败。我在实际使用中特别满意的一点是它的“存储与计算分离”架构。Kubernetes节点挂了镜像仓库不受影响MinIO的一个节点挂了也不影响镜像的推送和拉取。存储和计算拆分之后每一个环节都可以独立扩展和容灾这对做基础设施的工程师来说意味着“你不用半夜被叫起来赶工”的概率大大提升。最后再分享一个小技巧。如果你和团队也在评估镜像仓库方案我建议在部署完成之后立刻做一次全链路的故障演练手动停掉一个Registry副本模拟节点故障然后再把对象存储的访问密钥改错观察系统的恢复行为。这个过程会让你对整套架构的薄弱环节有一个非常直观的感知比看一百篇架构文档都管用。基础设施只有在故障面前才能真正验证它的价值。
返回列表