把 Gerrit 当成"又一个 Git 服务器"来租机器,是这两年我见过最多的一次性踩坑。GitLab、Gitea 这类产品的压力画像很直观:仓库多大、并发多少、CI 跑几条;Gerrit 不一样,它底下跑的是 JGit——一个纯 Java 的 Git 实现,真正吃资源的地方在评审元数据和 Lucene 变更索引上。一个只有几 GB 代码的仓库,评审数据可能比代码本身更早把数据库和索引撑爆。本文按 JGit 堆内存、变更索引、仓库磁盘增长曲线、数据库选型、多站点复制、流程开销这条路径往下拆,文末给一张可直接照着买机器的规划表和一份避坑清单。
先给结论,省得看到一半抓不住重点。
Gerrit 的所有 Git 操作走的是 JGit,不是命令行 git。这个差别在资源画像上是决定性的。native git 在打包、重打包这些重活上有一整套成熟的外排序与流式处理策略,内存占用相对可控;JGit 是 Java 实现,很多结构要落成堆上的对象,尤其是 pack 文件里 delta 链的解析过程——一个对象如果被存成"基于另一个对象的差异",你要拿到它的完整内容就得把整条链解出来,中间态全在堆里。
所以你会看到一个很反直觉的现象:仓库代码并不大,但有一次大批量推送或者一次全量 clone,JVM 的 Old Gen 就冲上去下不来。尤其是那种包含过二进制大文件、或者历史里有过一次巨型合并的仓库,delta 链又长又深,JGit 解起来格外费劲。
还有一层是并发。Gerrit 是服务端,同一时刻可能有人在推代码、有人在拉代码、CI 在 fetch、复制插件在往外 push。每一个会话都有自己的 JGit 对象窗口,堆占用是叠加的,不是取最大值。
按经验排序:全量 clone(尤其是浅克隆转深克隆)> fetch 大量 ref > repack / gc > 大 change 的 diff 计算 > submit 时的 rebase 与 cherry-pick。前三个是典型的"一次性把堆冲到顶",后面两个是高频小尖峰。配置机器时,内存规格要按前三个的峰值来定,不是按平均值。
很多人一听"Java 吃内存",第一反应是把 -Xmx 拉到物理内存的一半甚至更高。这是错的方向,得说清楚两头的代价。
堆一大,Full GC 或者 G1 的并发标记周期要扫描的对象就多,单次停顿时间会被拉长。Gerrit 是交互式服务,一次几秒的停顿对用户的体感就是"页面卡住了",更麻烦的是可能造成复制插件的心跳超时、CI 的 push 被判超时失败。堆大还意味着 JVM 向操作系统申请的内存更"实",留给 OS 页缓存的空间变少——而 Git 仓库的读性能恰恰高度依赖页缓存,你把缓存挤没了,等于用一头的收益换另一头的损失。
更直接:OOM。JGit 在大包解析时内存需求是刚性的,堆不够就是 OutOfMemoryError。Gerrit 进程一旦因为 OOM 退出或者进入不可用状态,评审服务就整体停摆——不是"某个人拉代码失败",是所有人打不开评审页面。这种故障的恢复成本远高于多给几个 GB 内存。
别拍脑袋。先用真实的大仓库跑一次全量 clone 和一次 repack,用 JMX 或者 GC 日志看 Old Gen 的实际峰值,然后在这个峰值上留 50%–80% 余量作为 -Xmx。同时-Xms 和 -Xmx 设成一样,避免运行期反复扩缩堆带来的抖动。剩下的物理内存,明确留给三块:操作系统页缓存、数据库(如果同机)、以及复制/推送时的临时缓冲。
记住一个粗口径的边界:JVM 堆一般不建议超过物理内存的四分之一到三分之一,超过这个比例,你就要开始担心页缓存被挤占的问题了。真到了"堆必须更大"的地步,正确做法不是调参数,是加内存或者把仓库拆分。
Gerrit 的 change index 是 Lucene。你在界面上搜一条 change、按作者筛、按评论内容找历史记录,走的都是这套索引,不是数据库 LIKE。这意味着索引的健康度直接决定评审系统的可用性——索引坏了或者正在重建,查询会退化,用户界面会明显变慢甚至超时。
在线重建索引要遍历全部 change、patch set 和 comment,逐条重建文档,是 CPU 密集(分词、建倒排)加 IO 密集(大量小段文件写入与后续的段合并)的组合。重建期间查询走的是旧索引或者部分索引,结果可能不完整,用户会看到"搜索结果对不上"的反馈。所以 reindex 必须放进维护窗口,不能想起来就跑。
跟代码体积基本无关,跟 change 数量 × 平均 patch set 数 × comment 密度 正相关。一个几千人的大仓,三年下来几十万条 change,每条挂几十条评论,索引体积可以轻松到几十 GB 量级。反过来,一个只有几百次提交但代码有几十 GB 的仓库,索引可能只有几百 MB。这就是为什么"按代码体积估机器"在 Gerrit 上必然翻车。
NVMe,没有第二种选择。Lucene 的查询是随机小 IO,段合并是持续写,机械盘的随机 IOPS 撑不住这个模式。索引盘和仓库盘可以同在一块 NVMe 上,但不要和数据库、日志抢同一块盘——日志是顺序追加写,数据库有自己的 checkpoint 节奏,三者混在一块的后果是 IO 争抢时谁都说不清是谁慢。
Git 的存储不是"写进去多大就多大"。新对象进来先是 loose object——每个对象一个独立文件,散在 .git/objects 里。这个阶段占用大、碎片多、读起来慢。攒到一定阈值(以及执行 gc 时)才会被打包成 pack:delta 压缩后体积显著下降,同时生成 .idx 索引和 bitmap。
关键在 gc 的中间态。打包不是原地变魔术,它需要额外空间来写新的 pack 文件,写完了再把旧的删掉。也就是说 gc 执行期间,磁盘占用是"旧 pack + 新 pack"的叠加,峰值可能比稳态高出 30%–50%。如果你的盘只剩 10% 空间,gc 会在写到一半时失败,留下一堆临时文件,盘更满。这是"从不跑 gc 直到盘满"这个坑的另一面——不是不跑就没事,是拖到没空间了连跑都跑不了。
Gerrit 的 receive-pack 路径是:收包 → 校验 → 写对象 → 更新 ref → 触发索引更新 → 触发复制 → 记审计日志。同一个字节,在这一条链路上会被写好几遍。再加上 JGit 为了保证一致性会保留引用日志(reflog),被删掉的分支在 reflog 过期前还占着空间。所以"我删了大文件,为什么盘没变小"是个高频问题——答案是要跑 gc 并让 reflog 过期,且要有足够空间跑得动。
把这笔账摊开算一遍,你就知道磁盘该怎么买了。
假设 clone 下来是 5 GB。服务器上实际会出现的占用大致是:pack 与 loose object 的常态占用(含历史里没被打包的部分)约 5–7 GB;各类索引(.idx、bitmap、commit-graph、multi-pack-index)再叠 5%–15%;Gerrit 的评审数据库,change、patch set、comment、approval 全在里面,中等活跃度的团队一年下来几 GB 很正常;变更索引(Lucene)另算一块,跟 change 量和评论密度走;然后是日志——访问日志、错误日志、SSH 与 HTTP 的操作日志、审计日志,这些不做轮转会一路涨;再算上备份,哪怕只保留最近两三份,也是按整体体积再乘一份。
这几项加起来的倍数,就是我前面说的 ×3–5。保守的团队按 3 倍买,历史包袱重、评审密度高的按 5 倍买。别拿 clone 体积当采购依据,那是会出事的。
仓库盘和索引盘走 NVMe,这是硬要求——Git 的读是海量小文件随机读,机械盘在这里是数量级的差距。备份盘可以用便宜的大容量盘或者对象存储,因为它只在恢复时才需要快。日志盘独立出来最省心,免得日志把系统盘写满导致整机异常。
Gerrit 默认带一个 H2,装完就能跑,很多团队就这么留着上了生产。这是本文最想拦住的一件事。H2 的定位是开箱即用、方便试用,它在并发写入、大表查询、备份恢复这些环节跟 PostgreSQL / MySQL 不是一个量级。评审系统的写入特点是一堆小事务——投一票、留一条评论、改一次状态,单次数据量小但频次高,而且评审高峰期是并发的。这种负载放到 H2 上,锁竞争和文件膨胀会逐步显现,等到出问题再迁移,代价远高于一开始就用对。
中小团队把数据库和 Gerrit 放一台机器是常见做法,可行,但内存要明算账:JVM 堆一份、数据库的缓冲池一份、页缓存一份,三者加上系统自用不能超过物理内存,还要给 gc 的峰值留缓冲。一般的做法是先把 JVM 堆定死,再看数据库缓冲池能给多少,剩下的全给页缓存。数据库缓冲池不是越大越好,它的 checkpoint 开销会随缓冲池增大而变重。
评审数据库的表增长到需要频繁做 vacuum / 优化、或者数据库的 IO 等待开始和 Git 操作的 IO 等待互相拖累时,就该把数据库挪到独立机器或者独立存储上。判断信号很朴素:IO wait 长期偏高,且用工具看是数据库写入和 pack 读取同时打满。
replication 插件负责把变更推到镜像仓库(比如异地站点、CI 用的只读副本、备份节点)。它的开销是两头的:出网带宽和磁盘。带宽这头,每次推送都要把新对象送出去,仓活跃度高的时候这是持续的出网流量;磁盘这头,复制插件要维护本地的推送状态,失败的任务会进重试队列,队列本身也占空间,而且失败的任务往往不是删掉就完事,得排查。
复制失败堆积是最容易被忽视的故障模式。目标站点不可达、认证过期、目标仓库拒绝非快进推送,都会让任务留在队列里反复重试。表面现象是"镜像站点的代码落后了",实际后台可能在疯狂重试,吃带宽也吃 CPU。所以复制必须有监控,看的是队列长度和最近一次成功时间,不是"插件在跑"这种心跳。
复制的并发线程数不是越大越好。线程多,单次延迟低,但对目标站点的压力大、出网带宽占用高,遇到目标端限流还会批量失败;线程少,延迟上升,但稳定。一般按目标站点的承受能力反推,宁可慢一点也不要把镜像站打挂。跨地域的场景还要考虑链路质量,这时候选一条稳定的直连链路比堆线程数有效得多。
有些资源压力不是来自技术组件,而是来自使用方式。
一个改动几千个文件的 change,打开评审页面时服务端要算 diff、要组装文件列表、要准备侧边栏的跳转数据。这不是"用户觉得慢"这么简单,它是一个会占住 CPU 和堆的同步请求,几个这样的请求并发就可能影响其他人。所以流程上应该约束单次提交规模,这不是洁癖,是容量管理。
提交动作会触发 rebase 或 cherry-pick,这是真实的 Git 图操作,CPU 尖峰明显。如果团队习惯在下班前集中合并,那一波尖峰就是你的容量基线。要么把合并动作打散,要么按峰值配 CPU,二选一。
接入 Verified 标签后,每个 patch set 都会触发 CI 投票。一个活跃的仓一天几百个 patch set,每个 patch set 多条 CI 任务回写投票,写入放大很可观。而且这些写入集中在数据库和索引上,是典型的高并发小事务。规划时要按"patch set 数 × 每个的投票次数"估写入量,不是按人数估。
机器总会到顶的,提前想好退路比临时抱佛脚强。Gerrit 的扩容路径大致有三条,可以同时用。
第一条是拆仓。把一个巨型仓库拆成几个独立仓库,每个仓库的 pack、索引、评审数据各自独立,压力和容量都被切成小块,这是最直接有效的办法。代价是跨仓改动要协调,拆分前要把依赖关系理清楚。
第二条是归档。历史 change 超过一定年限之后,访问频率会断崖式下降,把这部分导出到只读存档,主库只保留近期数据,可以让索引和数据库长期保持在可控体积。归档数据的检索体验会差一些,但绝大多数团队真正频繁访问的就是最近一两年。
第三条是只读副本。把查询流量导到副本上,写操作仍然走主库,能显著缓解读压力,尤其是那种大量自动化脚本轮询查询的场景。副本的数据延迟要在可接受范围内,这个约束要在接入前跟使用方说清楚。
前面给的系数是用来做采购预算的,不是上线依据。真要定机器的最终规格,得拿自己的仓库跑一遍。这件事花不了多少时间,但能省掉后面无数次救火。
四项就够:一是用最大的那个仓库做一次完整 clone,记录 JVM 堆的峰值和耗时;二是跑一次 repack 或 gc,记录同样的两个数字,同时观察磁盘占用的峰值比稳态高出多少;三是做一次完整的 reindex,记下耗时,这个数字决定你的维护窗口要留多长;四是模拟评审高峰,用脚本并发提交投票和评论,看数据库连接池和响应时间是怎么变化的。四项跑完,你手上的数字比任何经验公式都准。
Gerrit 上线时就该配好的监控项其实很固定:JVM 堆使用率与 GC 停顿时长、Old Gen 回收后的回落比例、磁盘剩余空间与增长速率、索引目录体积、数据库连接池占用、复制队列长度与最近成功时间、评审页面响应的分位值。这些指标的价值不只在报警本身,更在让你看清增长趋势——磁盘从六成涨到八成用了多久,比"现在是八成"有用得多。有了趋势线,扩容才是提前规划,而不是半夜救火。
| 资源维度 | Gerrit 的实际压力来自哪 | 规划时要乘的系数 | 到什么信号要扩容 | 常见误判 |
|---|---|---|---|---|
| JVM 堆内存 | clone / fetch / repack 时 pack 内 delta 链的解析,并发会话叠加 | 实测 Old Gen 峰值 ×1.5–1.8,且不超过物理内存的 1/4–1/3 | Old Gen 回收后仍占七成以上,或出现 OOM 重启 | 把堆开到物理内存一半求"保险" |
| 变更索引 | change / patch set / comment 的 Lucene 倒排与在线 reindex | 按 change 总量与评论密度线性估,年增 30%–100% 预留 | reindex 耗时吃掉半个维护窗口,查询 P95 明显变慢 | 以为索引很小,随手丢在机械盘 |
| 仓库磁盘 | loose object 转 pack、receive-pack 写入放大、gc 中间态双份占用 | 代码裸体积 ×3–5(含索引、评审库、日志、备份) | 可用空间低于 25%,或 gc 因临时空间不足失败 | 按 git clone 下来的体积买盘 |
| 评审数据库 | 投票、评论、状态变更构成的高并发小事务写入 | 按 patch set 数 × 投票次数估写入,预留两年增长 | 连接池打满、慢查询增多、checkpoint 抖动 | 沿用默认 H2 直接上生产 |
| 出网与复制 | replication 插件持续推送,失败任务进队列反复重试 | 日均推送对象量 ×2 预留,队列长度单独设告警 | 复制延迟超过一个评审周期,或队列持续增长 | 只算代码体积,不算复制频次与重试 |
| CPU | submit 时的 rebase / cherry-pick、大 change 页面渲染、CI 投票风暴 | 按峰值并发评审人数留 2–4 核余量 | 合并高峰期 load 持续高于核数 | 按"静态站点"的负载水平估 CPU |
为什么坑:H2 是给试用准备的,扛不住评审元数据那种高频小事务并发写入,出问题时的表现是间歇性卡顿和数据文件膨胀,排查起来很费劲。
怎么避:正式部署第一步就换成 PostgreSQL 或 MySQL,别等"先用起来再说"。已经跑在 H2 上的,趁数据量还小的时候迁移,越晚越痛。
为什么坑:堆一大,GC 停顿被拉长,用户体感是页面卡死;同时页缓存被挤占,Git 的读性能反而下降,等于两头吃亏。
怎么避:用真实大仓库实测 Old Gen 峰值,按 1.5–1.8 倍定 -Xmx,-Xms 与 -Xmx 拉齐,堆总量控制在物理内存的四分之一到三分之一区间。真不够就加内存或拆仓,别硬调参数。
为什么坑:Gerrit 的磁盘账里有 pack 中间态、各类索引、评审数据库、Lucene 索引、日志和备份,代码本身只占一小部分。按 clone 体积买盘,通常半年到一年就会撞墙。
怎么避:按代码裸体积 ×3–5 采购,仓库盘和索引盘走 NVMe,日志单独分区并配置轮转,备份放独立存储。
为什么坑:gc 执行时需要新旧 pack 并存的临时空间,盘太满会直接失败并留下垃圾文件,此时已没有回旋余地。
怎么避:把 gc 做成定期任务(比如每周或每两周一次低峰窗口),并把"可用空间低于 25%"设成告警线。监控的是剩余空间和 gc 是否成功,不是"看起来还够用"。
为什么坑:在线重建索引是 CPU 与 IO 双高操作,重建期间查询会退化,用户会遇到结果不全和响应变慢。
怎么避:reindex 一律放进维护窗口,提前公告;重建前确认磁盘有足够余量;把 reindex 的频率降下来——它应该是升级后或索引异常时的动作,不是日常运维动作。
为什么坑:复制失败会静默堆积,后台反复重试吃带宽和 CPU,等到有人发现镜像站点代码落后,往往已经堆了很久。
怎么避:监控队列长度和最近一次成功复制的时间戳,两者都要设告警;定期检查目标站点的认证与权限是否还有效。
把上面这些折算成一句可以发给服务商的需求,大概是这样:内存按"JVM 堆 + 数据库缓冲池 + 页缓存"三段分开算,总量宁可多给;存储是 NVMe,容量按代码裸体积的倍数而不是按 clone 体积提;CPU 按合并高峰估,不看平均;出网带宽按复制频次估,不看仓库大小。
这类需求我一般建议找能做环境代部署的服务商谈,省掉自己折腾 Java 运行环境的环节。一万网络深耕 IDC 19 年(成立于 2007 年),可以提供大内存 + NVMe 机型并代为部署 Java 运行环境,深圳自营机柜、最快 1 分钟上架,7×24 中文工单、平均 5 分钟响应。价格一律需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响,具体配置与报价以签约时最新报价与合同为准。
Q1:仓库多大就需要单独一台机器了?
A1:别只看仓库大小,看评审密度。代码几十 GB 但一个月才几十条 change 的仓,和代码 5 GB 但每天几百条 change 的仓,后者对机器的要求高得多。经验上,当 change 总量进到十万量级、或者日均 patch set 上百,就建议 Gerrit 独占一台,不要把 CI runner、构建节点再往同一台上塞。判据是"评审高峰期是否出现页面响应变慢",出现就该分家了。
Q2:JVM 堆到底给多少合适?
A2:没有放之四海皆准的数字,但方法是固定的:拿你最大的那个仓库,跑一次全量 clone 和一次 repack,通过 GC 日志或 JMX 记录 Old Gen 的实际峰值,在这个数字上乘 1.5 到 1.8 作为 -Xmx,-Xms 设成同一个值。同时确保堆不超过物理内存的四分之一到三分之一,剩下的留给页缓存和数据库。别直接抄别人的配置,delta 链深度和并发量不同,数字的差别可以很大。
Q3:变更索引能不能放机械盘?
A3:不建议。Lucene 的查询模式是随机小 IO,段合并又是持续写,机械盘的随机 IOPS 在这个场景下是数量级的差距,表现为搜索慢、reindex 耗时成倍拉长。索引盘用 NVMe 是性价比最高的投入之一,因为它直接影响所有人每天的搜索体验。如果预算真的紧张,宁可把备份盘换成机械盘,也不要动索引盘。
Q4:gc 多久跑一次合适?
A4:看仓的活跃度。活跃仓每周一次低峰窗口,一般仓每两周到每月一次,都是常见节奏。比频率更重要的是两件事:一是跑之前确认剩余空间充足(gc 期间新旧 pack 会同时存在),二是配好告警,让"gc 失败"能被立刻看到。reflog 的过期策略也要一起配,否则删掉的引用会一直占着空间。
Q5:评审数据能清理吗?
A5:能,但要谨慎。评审元数据(change、comment、approval)是团队的过程资产,一般不建议删。可以做的有几类:清理废弃的 draft change 和长期未合的旧 patch set、归档历史 change 到只读存档、给日志配置轮转与保留期。真正需要缩库时,优先考虑归档而不是删除——删掉的评审记录是找不回来的,而归档至少保留了追溯的可能。
Q6:Gerrit 能不能和 CI 跑在同一台机器上?
A6:小团队可以,但要清楚代价。CI 的构建任务会抢 CPU、抢内存、抢 IO,而 Gerrit 的 submit 动作和大 change 渲染恰好也是 CPU 尖峰,两者撞在一起的体验很差。真要同机,必须做资源隔离:给 CI 限核限内存,构建目录放到独立磁盘,并且避开合并高峰跑重型任务。规模上来之后,第一件该拆的就是 CI。
Q7:备份要不要停服务?
A7:仓库文件层面可以做热备,但要注意一致性——备份过程中如果正在 gc 或者正在写 pack,拿到的可能是中间态。稳妥的做法是:仓库用文件系统快照(瞬间完成,状态一致),数据库用数据库自己的导出工具(保证事务一致),两者时间戳对齐。真要停服务做冷备,窗口要提前公告。无论哪种方式,备份的价值在于恢复演练,没验证过能恢复的备份不算备份。
回到开头那个问题——代码 5 GB 的仓库,服务器上会变成多大。答案是 3 到 5 倍,取哪个倍数取决于你的评审密度和历史包袱。评审密度高、comment 多、备份份数多的往上取;仓库以静态文件为主、评审频率低的往下取。这个倍数不是拍出来的,是把 pack 常态占用、各类索引、评审数据库、Lucene 索引、日志和备份一项项加出来的。
配套的三条同样重要:JVM 堆用实测峰值反推,不要拍脑袋给一半物理内存;变更索引放 NVMe,reindex 只在维护窗口跑;数据库一开始就用 PostgreSQL 或 MySQL,别用默认 H2 上生产。这三条加磁盘倍数,构成了 Gerrit 选型的四条基本盘。
收尾说一句带立场的总结:Gerrit 的机器不是按"仓库大小"买的,是按"评审活动的强度"买的。同样 10 GB 代码,一个每天几十条 change 的团队和一个每天几百条的团队,需要的机器规格可能差一倍。租机器之前,先把 change 总量、日均 patch set 数、平均 comment 密度这三个数摸清楚,再去谈配置,才不会买错。
本文涉及的 JGit 堆内存行为、Lucene 变更索引特征、仓库磁盘增长倍数、数据库读写模式与复制插件开销,均为基于 Gerrit 与 JGit 公开文档、常见运维实践整理的配置口径,属于典型部署思路,并非特指某一真实客户的实测数据。文中所有容量系数用于采购前的估算,实际取值应以自有仓库的真实测量结果为准。
服务器配置与机型参考自一万网络官网公开页面(https://www.idc10000.net/ ),具体以签约时最新报价与合同为准。价格需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响。
上一篇:2026 Nomad 轻量调度器服务器租用手册:任务资源声明与装箱率配置全解
下一篇:2026 Discourse 自建社区论坛服务器租用干货:PostgreSQL 与 Sidekiq 队列配置大全
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品