尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MiroFish:自建Docker Registry的镜像清理与同步管理利器

MiroFish:自建Docker Registry的镜像清理与同步管理利器 用过自建Docker Registry的朋友应该都有这种体会镜像仓库这东西刚搭好的时候岁月静好跑上两三个月就开始暴露脾气。磁盘告警、垃圾镜像堆积、测试环境的临时镜像和正式环境的稳定镜像混在一起、CI那边三天两头因为仓库爆满而推送失败。你打开Registry容器所在的那台机器df -h一看好家伙几百个G说没就没。MiroFish就是冲着这些问题来的。它是一个面向自建镜像仓库的命令行管理工具专门解决Docker Registry使用过程中的镜像浏览、批量清理、磁盘分析、跨仓库同步这几类高频需求。你可以把它理解成用命令行操作镜像仓库的瑞士军刀——不需要给Registry装任何插件不依赖额外的Web服务直接在任意一台能访问仓库的机器上跑命令就行。这篇文章我会把MiroFish从设计思路到实际使用完整拆开讲一遍包括我自己的部署踩坑记录、清理策略的参数计算过程以及几个你在官方文档里大概率找不到的细节问题。无论你是刚搭好仓库的小白还是被镜像撑爆磁盘的老手这篇都值得你花十分钟看完。1. 为什么需要MiroFish自建镜像仓库的三类痛点1.1 仓库管理为什么这么容易失控Docker Registry本身的设计哲学是“存储不管事”。它只负责把镜像的层layer和元数据写进磁盘至于这些镜像有没有用、要不要删、哪个仓库占了多大空间它几乎不提供任何直观的管理能力。官方V2 API虽然暴露了_catalog、/tags/list这些端点但用起来相当原始。一套典型的自建仓库使用轨迹通常是这样的先建一个library仓库存基础镜像然后开发环境变多了开始出现project-a/dev-api、project-b/test-web这种带环境后缀的命名空间。再往后CI开始介入每次提交都打一个带短commit号的tag。几个月下来仓库里同一个镜像的tag可能攒了几百个其中绝大多数永远不会再被拉取。而Docker Registry的垃圾回收GC机制又比较保守——不主动触发不清理很多删除操作之后磁盘空间根本没有变化。我见过不止一次这样的情况团队花了半小时排查为什么Registry服务越来越慢最后发现根因就是某台构建机上遗落的一个循环push脚本把snapshot-YYYYMMDD这样的tag刷了几千个仓库索引文件膨胀到了几百MB。1.2 已有的管理工具到底差在哪那么问题来了市面上的开源工具其实不少比如reg、skopeo、crane甚至一些人用Python脚本直接调API。为什么还需要MiroFish我自己用下来的体会是现有工具大多解决的是单点问题比如skopeo擅长镜像复制crane擅长把远程仓库的镜像拷到本地tar包但它们都不太关心“这个仓库整体健不健康”。你想做一个“列出所有大于1GB的镜像并按项目分组”的操作或者“删除项目X下所有除latest和v开头的正式版本之外的tag”这些工具需要你自己写一堆组合逻辑而且很容易在分页、认证刷新、tag排序这些细节上翻车。MiroFish的设计思路恰好是冲着这个盲区去的。它把所有高频管理动作收敛成一组子命令内部统一处理Registry V2 API的token认证、分页遍历、tag排序和并发控制。你不需要懂HTTP API细节也不需要为每一个运维场景临时写脚本。1.3 MiroFish的核心设计目标在动手用之前先把它的设计原则说清楚这样你后续看命令的时候会更容易理解为什么某些选项要这样设计无侵入只通过Registry HTTP API通信不往Registry容器里注入任何Agent或插件不改变仓库存储层结构。可预测所有删除、同步类操作默认带--dry-run模式先看报告再动手降低误操作风险。可按脚本调用支持JSON输出方便接入CI流水线、告警系统或定时任务。多仓库管理在一份配置文件里维护多套Registry的连接信息切换到不同环境不用改命令只换context。2. 整体设计与工作流程解析2.1 直连Registry V2 API为什么不需要装AgentMiroFish的底层通信完全基于Docker Registry HTTP API V2。这个选择的核心逻辑很简单绝大多数Harbor、Nexus、GitLab Container Registry以及裸的registry:2容器都原生兼容这套接口而且API本身不要求你在服务端额外部署任何东西。只要你的Registry暴露了5000端口或者通过Nginx做了TLS终结MiroFish就可以像docker pull一样正常访问。这一点在跨网络环境的时候尤其有用——比如你在一台跳板机上部署MiroFish它的工作方式决定了你不需要给生产环境的Registry开SSH权限也不需要进到容器内部去操作文件安全性高不少。2.2 Token认证与自动刷新机制用Docker Registry认证过的朋友都知道现在的私有仓库基本都是Bearer Token认证流程客户端先向/v2/发请求如果返回401再从响应头里的WWW-Authenticate字段拿到token服务地址然后用账号密码换取一个短时效的访问令牌。MiroFish把整个流程封装成了内部组件并且在令牌过期前做了主动刷新处理。实际效果就是你只要在配置文件里维护好用户名密码或者Token它就可以长时间后台运行不会跑着跑着突然报unauthorized。2.3 两条核心数据链路清理链路与同步链路从功能架构上看MiroFish的所有操作都围绕两条链路展开清理链路list发现仓库/标签 →analyze分析大小与引用情况 →clean删除目标Tag和Manifest → 触发garbage-collect回收存储层。同步链路sync plan计算待同步清单 → 按策略并发复制Blob和Manifest → 完成后生成差异报告。这两条链路在实现上共用了同一个“仓库遍历器”——它负责处理API分页、排序和并发拉取所以不管你是做全量清理还是一次跨环境同步看到的输出格式和日志风格都是统一的用起来非常顺手。2.4 配置文件组织方式MiroFish没有用一堆命令行Flag来传递连接参数而是采用一个YAML配置文件集中管理。好处不用多讲——当你同时维护三五套环境的时候把连接信息落在文件里比每次敲一长串--registry、--username参数要可靠得多也方便交到同事手里。3. 部署安装与初始化配置3.1 三种安装方式实测MiroFish支持三种安装方式我按推荐程度排列二进制直接下载从发布页下载对应平台的压缩包解压后把mirofish放到/usr/local/bin适合Linux服务器快速部署。这个方式最稳不依赖任何运行时。Homebrew安装macOS开发机上推荐一条brew install mirofish就搞定升级也方便。源码编译需要Go 1.21环境适合你想修改内部逻辑或者贡献代码的情况。安装完成后验证一下版本mirofish version正常情况下会输出版本号和构建时间。如果提示command not found检查一下二进制文件是否有执行权限以及所在目录是否在PATH里。3.2 生成配置文件与最小可用配置第一次使用可以用mirofish init生成默认配置模板。我建议在不熟悉参数之前不要手搓文件直接基于模板改最省事。生成的配置文件长这样contexts: production: registry: registry.example.com:5000 username: admin password: ${REGISTRY_PASSWORD} insecure: false default: true lab: registry: 192.168.1.100:5000 username: dev password: dev-pass-123 insecure: true这里有几个细节要解释insecure只在你用HTTP访问仓库时设为true生产环境千万别开。密码支持环境变量引用写在文件里的是占位符而不是明文。这是我从同事那里学来的习惯否则配置文件一旦被提交到Git历史里密码就全裸奔了。default: true表示默认使用这个context。准备就绪后先用mirofish list看能不能正常连通mirofish list --context production如果能看到仓库列表说明认证和数据链路都没问题。3.3 多Registry管理与命名空间设计我这边实际管理的场景是一台Harbor生产、一台裸Registry测试用、还有GitLab自带的Container Registry。用MiroFish维护三个context之后日常操作基本就变成了mirofish repo list --context production mirofish repo list --context lab不用记IP、不用记端口一个名字切来切去。如果你管理的镜像命名空间本身就有规范比如team-a/backend、team-b/frontendMiroFish在列仓库时也支持按前缀筛选后面讲清理的时候会展开。3.4 自检命令不要跳过这一步配置完建议先跑一次mirofish doctor。这个命令会检查是否能正确连接Registry认证是否有效API路径是否兼容配置文件格式是否有问题我遇到过一种情况某台Nginx没有正确转发/v2/前缀路径docker login是正常的但MiroFish和skopeo统统报404。跑doctor一眼就能看出是API路径探测失败。4. 镜像清理实操从规则制定到恢复磁盘空间4.1 清理规则的核心字段与优先级镜像清理是MiroFish最有价值的使用场景绝大多数人用这个工具就是冲着清理来的。它的清理规则由几个字段组合而成--repo-pattern仓库名匹配模式支持通配符比如project-a/*、*-dev/*。--tag-patternTag匹配模式。--keep-last每个仓库保留最近的N个Tag按创建时间排序。--keep-tags必须保留的Tag列表比如latest、v1.0.0、production。--min-size只处理Size超过指定大小的镜像单位MB。--created-before只处理创建时间早于指定时间的Tag。这些规则是组合而不是单选。举一个我自己常用的清理方案mirofish clean \ --repo-pattern project-a/* \ --tag-pattern * \ --keep-last 30 \ --keep-tags latest,production,v* \ --created-before 720h \ --dry-run这个命令表达的意思是在project-a/下所有仓库中保留每个仓库最近30个Tag但latest、production和任何以v开头的版本Tag无条件保留同时只删除创建时间早于30天720小时的镜像。先预演一遍看会删掉哪些再决定是否真正执行。4.2 保留策略的参数计算不要拍脑袋定数字很多人在--keep-last后面随便填个50或者100这其实不太科学。我讲一个我自己推导保留数量的方法供你参考。假设你的CI每天构建3次每次都会往project-a/dev-api推送一个带短commit号的tag。一周就是21个一个月就是约90个。如果你希望本地能回溯最近两周的构建产物那--keep-last至少得设置为2 * 21 42再留一点冗余设50比较合适。那如果团队每天构建20次呢按同样的思路两周就是280个Tag。这时候你就要想想真的需要回溯两周的构建产物吗还是只保留最近7天就够了回溯窗口越长需要保留的Tag就越多磁盘占用自然指数级上升。我个人的做法是事后再用MiroFish的analyze命令看实际效果如果保留50个Tag占用了200GB而团队真正会回滚的窗口只有两三天那就把保留数量往下降用数据倒推策略。这是磁盘和便利性之间的动态平衡没有一劳永逸的配置。4.3 一次完整的清理演练清理是最容易出事儿的操作所以我强烈建议你把--dry-run作为一个强制习惯固化下来。我自己的流程是第一步跑一次dry-runmirofish clean \ --repo-pattern project-a/* \ --tag-pattern * \ --keep-last 30 \ --keep-tags latest,production \ --created-before 720h \ --dry-run输出会列出将会删除的每个Tag、所属仓库、大小、创建时间。我一般会重点扫一眼有没有意外命中的正式版本Tag确认没有v1.2.0这种被误杀的情况。第二步如果dry-run报告没问题去掉--dry-run执行mirofish clean \ --repo-pattern project-a/* \ --tag-pattern * \ --keep-last 30 \ --keep-tags latest,production \ --created-before 720h这里有个细节MiroFish删除的不是Registry的存储文件本身而是通过API删除对应的Manifest。所以清理完成后镜像的元数据已经移除但底层Blob文件可能还残留在磁盘上需要触发Registry自带的垃圾回收才能真正释放空间。第三步触发GC这一步在裸registry:2上尤其重要docker exec -it registry-container /bin/registry garbage-collect /etc/docker/registry/config.yml如果用的是Harbor则需要在Harbor界面上触发GC或者使用Harbor API。总之清理完Blob的引用之后必须GC否则你会发现线上磁盘空间一点没变这是新手最容易产生的误解。4.4 GC触发时机与踩坑记录GC虽然能帮你释放空间但它不是一个可以随时乱跑的操作。在Docker Registry的实现中GC会尝试删除未引用的Blob与正在进行的推送或拉取操作有潜在的锁冲突风险。我在生产环境踩过一次坑某天早上业务高峰期跑了一次GC恰好当时CI正在批量推送新镜像结果部分正在上传的层被误判为“未引用”导致推送失败CI报表红了一整页。从那以后我给自己定了一个操作窗口所有GC都安排在凌晨低峰期跑并通过定时任务串在清理流程后面。我的定时任务命令基本是这样的30 2 * * * mirofish clean --config /etc/mirofish/config.yaml --repo-pattern */dev-* --keep-last 20 --dry-run /var/log/mirofish-clean.log 21如果dry-run日志里没有异常再执行实际清理和GC。这种半自动的节奏既保留了人对高危操作的控制力又不至于每次都要半夜起来手动敲命令。5. 镜像同步与跨环境迁移5.1 同步的核心场景第二个高频场景是镜像同步。最常见的需求来自网络隔离环境开发环境在一个内网生产环境在另一个内网两边物理隔离但生产环境的镜像更新必须及时跟进。MiroFish的sync命令可以直接把源Registry上的镜像同步到目标Registry。另外一个场景是灾备和迁移。比如你准备把一台Harbor从旧服务器迁到新服务器或者从裸Registry迁到HarborMiroFish可以扮演一次性批量复制工具的角色。5.2 同步命令与过滤规则单仓库同步的基本命令mirofish sync \ --source production \ --target lab \ --repo-pattern project-a/backend \ --tag-pattern v*它做的事情是列出源production中project-a/backend下所有以v开头的Tag然后把对应的Manifest和Blob复制到目标lab。与clean一样sync也支持丰富的过滤条件我只复制正式版本mirofish sync \ --source production \ --target lab \ --repo-pattern project-a/* \ --tag-pattern v[0-9]\.[0-9]\.[0-9]这里用正则表达式可以精准避开所有SNAPSHOT、dev、test等非正式Tag。5.3 同步中的去重、并行度与断点续传同步的本质是跨网络的Blob和Manifest复制。Docker镜像的许多Blob在不同镜像之间是共享的比如的基础层、公共依赖层如果同步时不做去重会把同一个大层重复传输好几遍浪费带宽也拖慢速度。MiroFish内部的策略是先计算源端所有的Blob指纹Digest再查询目标端已存在的Blob列表自动跳过那些两边都有的层。实际效果是当你同步第二批镜像时如果它们和第一批共享了一部分基础层那么这部分层的耗时几乎可以忽略不计。并发度通过--concurrency控制默认值我记忆中好像是4在普通千兆内网环境下效果足够好。如果你在跨公网环境同步建议把并发调低到2甚至1避免把有限的带宽塞满影响同一链路上的其他业务流量。有一点比较遗憾MiroFish目前的同步是单次命令触发的全量同步不支持常驻的增量监听模式。如果需要持续同步要么依赖外部cron定期调度要么配合CI在推送后自动触发一次同步。5.4 同步过程的网络与存储权衡同步之前先算一笔账一个500MB的镜像假设内网带宽是1Gbps理论最快也要4秒但实际跑起来受磁盘IO、Registry处理能力和网络延迟的影响通常至少要10到20秒。如果仓库里有上百个这样规模的镜像一次全量同步可能要跑半小时甚至更久。所以我的建议是同步任务尽量安排在业务低峰期执行并且先把--tag-pattern收窄到当前需要的版本范围。不要一上来就同步所有Tag否则你会把大量的时间浪费在从来不会被使用的历史版本上。6. 磁盘分析与容量规划6.1 仓库占用排行一句话定位磁盘杀手清理之前先分析通过mirofish analyze可以按仓库、按Tag维度查看镜像占用空间。这个命令的价值在于它帮你在动手删除之前先看清“哪些镜像真正占据了磁盘”而不是凭感觉去删。我习惯的输出方式是mirofish analyze --repo-pattern * --sort-size desc --format table输出会按Size从大到小列出所有仓库。你会发现一个有意思的现象磁盘空间的Top10清单里往往躺着一堆“一次性构建缓存”和“临时测试镜像”而真正重要的正式版本反而排不上号。这正好反向印证了清理策略的调整方向。6.2 识别“僵尸镜像”和“垃圾层”这里说的“僵尸镜像”是指那些没有任何活跃Tag引用、但仍然被底层Blob占据空间的Manifest。它们是怎么产生的最常见的情况是你通过docker push覆盖了一个Tag旧Manifest虽然已经不在Tag列表里了但GC没有触发它仍然残留在存储层。另一个更隐蔽的来源是Registry自身的_catalog索引里出现了重复或损坏的条目。我遇到过一次某机器人账号误操作导致上传被中断结果仓库里出现了一个缺了大部分Blob的残缺Manifest它既不能被正常拉取也不在常规的Tag列表里。靠肉眼和docker命令很难发现这种东西而analyze配合异常扫描就能快速识别。MiroFish的analyze --orphans会专门列出这类未被任何Tag引用的Manifest并且可以一键生成清理报告。我建议每季度跑一次这个检查把仓库里的“隐藏垃圾”清理掉。6.3 基于分析结果调整保留策略分析命令的最终价值是帮你校准规则。比如说你原本设置的保留策略是“每个仓库保留最近30个Tag”但通过analyze发现占空间最大的是某个tensorflow仓库共有200个Tag每个都是数百MB的深度学习镜像。这时候你可能需要针对这个仓库单独收紧策略比如改成--keep-last 5而不是全仓库一刀切。MiroFish允许在配置文件中针对特定仓库定义独立的清理策略这点用起来非常灵活。如果你有这类“大镜像专用仓库”的场景我非常推荐研究一下这个配置项。7. 常见问题与排查技巧实录7.1 删除镜像后空间没有变小这是问得最多的问题我在前面也提过这里再强调一遍Docker Registry清理数据是两阶段的。clean只是删除了Manifest引用底层Blob需要靠GC回收才能从磁盘上消失。如果你执行完清理后发现空间没变化先确认是否已经触发GC以及GC日志里是否确实标记了释放的Blob数量。另外有些NAS或云盘的文件系统在删除文件后不会立即释放空间给操作系统而是需要等待快照周期或者空间回收任务。这种情况可以通过du核对Registry数据目录的磁盘占用如果du显示减少了而df没变说明是文件系统层级的回收延迟耐心等待即可。7.2 认证失败或请求401排查步骤可以按这个顺序来先确认docker login到同一仓库是否正常如果docker login正常MiroFish报401大概率是MiroFish配置里的username/password有误或者配置文件的密码引用了单个不存在的环境变量导致实际发送的密码是空字符串。如果docker login也不正常那么问题出在Registry服务端或前置Nginx上检查一下Nginx的proxy_pass路径是否正确、TLS证书是否过期、WWW-Authenticate响应头是否被代理层吃掉。7.3 大镜像同步超时或中断跨公网同步大镜像时连接被重置、传输超时是比较常见的事。遇到这种情况我会优先检查目标Registry是否有上传大小限制比如某些Nginx默认的client_max_body_size只开了1MB这会导致任何大镜像的Blob上传都被拒之门外。MiroFish同步时如果意外中断已经传输完成的Blob不会重新传输因为Digest匹配机制会自动跳过已知Blob。所以实际做法就是把同步失败的命令原样再跑一遍它会继续从断点处续传直到完成。7.4 高并发把Registry打挂MiroFish的并发清理或同步会同时发起大量HTTP请求在某些配置较弱的Registry上可能造成CPU和内存飙升甚至崩溃。我踩过一次一台1核2G的裸Registry实例上跑同步我没控制并发结果Registry进程OOM导致当时正在拉镜像的业务服务全部报错。解决方案是把并发调小走温和路线mirofish sync --source production --target lab --concurrency 2 --repo-pattern project-a/*另外同步时间窗口错开业务高峰也是成本最低的规避手段。常见问题直接原因解决办法清理后磁盘未释放未触发GC手动执行garbage-collect或Harbor GC同步时大量Blob重复传输源端和目标端Blob不共享调整过滤条件首次同步先传输公共基础层401认证失败密码配置错误或token过期验证docker login检查配置文件大镜像push失败Nginx大小限制设置client_max_body_size 0Registry进程崩溃并发过高降低--concurrency错峰执行8. 关于定时任务的一点个人建议如果你打算把MiroFish接入到日常运维体系里我建议先别急着全自动化。我的做法是分两步走前两周保持人工确认每天看一次--dry-run报告观察它的判断逻辑是否符合团队的预期确认无误后再把清理执行和GC接到定时任务里但依然保留日报。这个过渡期很值得花因为每个团队的镜像命名习惯、Tag策略都不一样工具再聪明也需要一个“学习期”。另外有一个我最近才养成的小习惯把MiroFish的analyze输出接到运维群里的机器人Webhook上每周自动推送一次仓库磁盘占用Top10。有了这个数据大家会自然形成镜像瘦身的意识比管理者单独盯要有效得多。工具能解决技术问题但使用习惯还是要靠流程来引导。
返回列表