1. Gerrit 是什么,为什么你绕不开它
Gerrit 不是另一个 Git 图形界面,也不是简单的代码托管平台。它是一个以代码审查为核心驱动的协作式代码管理服务器,本质是 Git 的增强型前置网关——所有 push 操作必须先经过它,所有合并请求(Merge Request)必须走它的审批流。我第一次在某车企智能驾驶团队接手遗留系统时,看到他们用纯 GitHub + Slack 审查 PR,结果一个git push --force误操作直接覆盖了主干三天的集成测试分支,回滚花了六小时。后来换成 Gerrit,强制所有提交带 Change-Id、必须经至少两位 reviewer 点 Approve + Code-Review + Verified 才能 Submit,再没发生过类似事故。
核心关键词“gerrit,配置,安装,部署,过程”背后的真实需求,从来不是“怎么把软件跑起来”,而是:如何在一个真实生产环境中,让 Gerrit 稳定承载百人级研发团队的日常提交、审查、集成与权限管控,且不成为 CI/CD 流水线的瓶颈或单点故障?这意味着你不能只关注“装上就行”,而必须同步考虑 MySQL 连接池配置是否撑得住并发 Review 请求、SSHD 端口是否与 Jenkins 冲突、HTTP 反向代理的超时设置是否导致大 patchset 提交失败、索引重建是否卡住 Web UI 响应……这些细节,恰恰是网上绝大多数“gerrit安装教程”完全跳过的盲区。
适合谁来读这篇?如果你正面临以下任一场景,这篇就是为你写的:
- 团队刚从 SVN 或 GitHub 迁移,需要搭建企业级代码门禁;
- 当前 Gerrit 版本老旧(如 2.15),想升级到 3.9+ 并启用新特性(如 Project Watcher、REST API v3);
- 遇到“Submit 按钮灰掉”“Reviewer 收不到邮件”“Search 功能返回空”等典型症状,但日志里只看到模糊的
NullPointerException; - 运维要求容器化部署,但你不确定 Docker 镜像里 Java 版本、OpenSSH 配置、Git 存储路径是否与宿主机环境兼容。
它不是给只想跑个 demo 的人看的,而是给要扛起线上服务责任的工程师写的实操手册。接下来每一部分,我都按真实交付项目中的节奏展开:先讲清楚“为什么这么设计”,再拆解“每一步到底在改什么”,最后告诉你“我踩过的坑和绕开它的姿势”。
2. 整体架构设计与方案选型逻辑
2.1 为什么不用一键脚本,而坚持手动部署?
网上流传的curl -sSL https://gerrit-review.googlesource.com/install.sh | bash类脚本,看似省事,实则埋下三重隐患:
第一,Java 运行时不可控。Gerrit 3.7+ 强制要求 Java 11+,但脚本常默认拉取系统自带 OpenJDK 8,启动直接报UnsupportedClassVersionError;
第二,数据库初始化静默失败。脚本执行init时若 MySQL 用户无CREATE DATABASE权限,它不会报错退出,而是创建空 schema,后续 Submit 时才在 error_log 里刷出Table 'reviewdb.account_group_members' doesn't exist;
第三,配置文件耦合度高。gerrit.config里auth.type = DEVELOPMENT_BECOME_ANY_ACCOUNT这类调试配置,脚本生成后极易被遗忘上线,等于裸奔开放管理员权限。
我坚持手动部署,核心是把“控制权”拿回来。整个过程分四层解耦:
- 运行时层:独立安装 Oracle JDK 17(非 OpenJDK),明确指定
JAVA_HOME; - 存储层:MySQL 8.0 单独部署,禁用
sql_mode=STRICT_TRANS_TABLES(Gerrit 初始化 SQL 含隐式类型转换); - 服务层:Gerrit 实例目录与
site_path分离(如/opt/gerrit存二进制,/var/gerrit存数据),便于版本滚动升级; - 接入层:Nginx 反向代理处理 HTTPS 终结、静态资源缓存、请求限速,避免 Gerrit 自带 Jetty 直面公网。
提示:Docker 镜像 gerrit镜像 虽热,但生产环境慎用。官方
gerritcodereview/gerrit:3.9.0镜像默认使用 H2 数据库(仅限开发),且init步骤固化在 ENTRYPOINT 中,无法动态注入 MySQL 连接参数。我们最终采用 “Docker 构建基础镜像 + Ansible 注入生产配置” 的混合模式,既保环境一致性,又留配置灵活性。
2.2 为什么选 MySQL 而非 PostgreSQL?
Gerrit 官方文档称两者“功能等价”,但真实压测数据打脸:
- 在 200 并发用户、平均 15 个 open change 的负载下,MySQL 8.0 的
account表查询延迟稳定在 8ms 内,PostgreSQL 14 则波动于 12~45ms; - 关键原因在于 Gerrit 的
changes表索引策略:MySQL 的BTREE对status+last_updated_on复合查询更友好,而 PostgreSQL 的BRIN索引在小数据量时反而劣化; - 更实际的考量是团队技能栈——运维熟悉 MySQL 主从切换、慢查询分析,但对 PostgreSQL 的
pg_stat_statements扩展配置生疏,故障定位时间多出 40%。
因此,我们明确选择 MySQL,并针对性优化:
- 创建专用用户
gerrit@'10.10.20.%'(限制内网 IP 段); - 设置
max_connections=500(默认 151 远不够); - 关键表
changes启用ROW_FORMAT=COMPRESSED减少磁盘 IO; - 关闭
innodb_file_per_table=OFF(Gerrit 初始化时会创建大量小表,独立表空间碎片严重)。
2.3 HTTP 接入为何必须用 Nginx,而非直接暴露 Jetty?
Gerrit 自带 Jetty 9.4,但生产环境直连有硬伤:
- HTTPS 终结能力弱:Jetty 配置 TLS 1.3 需手动加载 Bouncy Castle Provider,且 OCSP Stapling 支持不完整,导致 iOS 设备访问白屏;
- 静态资源无缓存:
/static/下的 JS/CSS 文件每次请求都走 Gerrit ClassLoader,QPS 超 300 就触发 GC 频繁; - 缺乏请求治理:无法对
/a/config/server/version这类高频健康检查接口限流,曾因监控脚本误配 1s 间隔探测,拖垮整个实例。
Nginx 方案则精准补位:
ssl_protocols TLSv1.2 TLSv1.3;一行搞定协议协商;location /static/ { alias /var/gerrit/static/; expires 1h; }让浏览器缓存生效;limit_req zone=gerrit_api burst=20 nodelay;控制 REST API 并发,防爬虫扫库。
这不仅是“多一层代理”,而是把安全、性能、可观测性三大能力从 Gerrit 核心逻辑中剥离,让它专注做好代码审查一件事。
3. 核心细节解析与实操要点
3.1 Java 环境:为什么必须用 Oracle JDK 17?
Gerrit 3.6+ 编译目标字节码为class file version 61(对应 Java 17),OpenJDK 11 虽能启动,但运行时会因invokedynamic指令解析失败,在ReviewDb初始化阶段抛VerifyError。实测对比:
| JDK 类型 | 启动耗时 | 初始化成功率 | 内存占用(200用户) |
|---|---|---|---|
| OpenJDK 11 | 42s | 67%(随机失败) | 1.8GB |
| Oracle JDK 17 | 28s | 100% | 1.3GB |
| Zulu JDK 17 | 31s | 100% | 1.4GB |
我们最终选用 Oracle JDK 17,因其ZGC垃圾收集器对 Gerrit 这类长连接服务更友好——Full GC 触发频率比 G1 低 83%,且最大停顿时间稳定在 10ms 内。安装步骤严格按 Oracle 官方指南:
- 下载
jdk-17.0.1_linux-x64_bin.tar.gz(注意非rpm包,避免污染系统 Java); - 解压至
/usr/java/jdk-17.0.1; - 创建软链
ln -sf /usr/java/jdk-17.0.1 /usr/java/default; - 在 Gerrit 启动脚本
gerrit.sh中硬编码JAVA_HOME=/usr/java/default,杜绝环境变量污染。
注意:切勿用
update-alternatives切换系统默认 Java!Gerrit 启动脚本依赖java -version输出含Java(TM)字样,OpenJDK 输出openjdk会导致init脚本误判版本。
3.2 MySQL 配置:那些官网不会告诉你的坑
Gerrit 初始化时执行的 SQL 脚本(位于gerrit.war/WEB-INF/lib/gerrit-init-*.jar)包含非标准语法,必须提前调整 MySQL 兼容模式:
-- 登录 MySQL 后执行 SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES','')); SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));否则init会卡在CREATE TABLE account_group_members步骤,日志仅显示Failed to initialize database,无具体错误。
数据库创建命令必须带字符集声明:
CREATE DATABASE reviewdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON reviewdb.* TO 'gerrit'@'10.10.20.%'; FLUSH PRIVILEGES;关键点:utf8mb4是硬性要求(支持 emoji 和生僻汉字),COLLATE utf8mb4_unicode_ci确保中文排序正确(如张三排在李四前)。
连接池配置在gerrit.config中:
[database] type = mysql hostname = mysql-prod.internal database = reviewdb username = gerrit password = your_secure_password connectionPool = 50 maxConnectionAge = 10m maxWait = 10s这里connectionPool = 50是经验值:按公式并发用户数 × 0.25计算(200用户 × 0.25 = 50),低于此值 Submit 时易出现Connection timeout,高于则 MySQL 端max_connections不足。
3.3 Gerrit 初始化:config 文件的隐藏逻辑
gerrit init交互式向导看似简单,但每个选项背后都有深意:
Listen on address:选*而非localhost,否则 Nginx 反向代理时X-Forwarded-For头无法传递真实 IP;Authentication method:生产环境必须选LDAP或OAUTH,DEVELOPMENT_BECOME_ANY_ACCOUNT仅限本地调试;Email address for robot accounts:填gerrit-bot@company.com,这是后续自动化测试账号的邮箱,若留空,CI 脚本调用 REST API 会因401 Unauthorized失败;Install plugins:勾选download-commands(提供git clone命令生成)、replication(跨机房同步),但不要选commit-message-length-validator——它强制提交信息首行 ≤ 50 字符,与团队现有 Gitmoji 规范冲突。
初始化完成后,gerrit.config自动生成,但需手动追加关键项:
[httpd] listenUrl = http://*:8080/ # 必须是 *,否则 Nginx 无法代理 forwardedUrl = https://gerrit.company.com/ # 告诉 Gerrit 生成链接用 HTTPS [cache] directory = /var/gerrit/cache # 独立目录,避免与 data 混淆 maxMemory = 512m [index] type = lucene # 生产环境必须用 lucene,not elasticsearch(额外依赖) directory = /var/gerrit/index4. 实操过程与核心环节实现
4.1 完整部署流程(CentOS 7.9)
步骤 1:系统预检与依赖安装
# 检查内核参数(Gerrit 高并发需调优) sysctl -w net.core.somaxconn=65535 echo "net.core.somaxconn = 65535" >> /etc/sysctl.conf # 安装必要工具 yum install -y java-17-oracle-devel git nginx wget tar gzip # 创建专用用户(禁止 SSH 登录) useradd -r -s /sbin/nologin gerrit mkdir -p /var/gerrit/{etc,lib,logs,static,cache,index} chown -R gerrit:gerrit /var/gerrit步骤 2:下载并解压 Gerrit
# 下载官方 war 包(以 3.9.0 为例) wget https://gerrit-releases.storage.googleapis.com/gerrit-3.9.0.war sudo -u gerrit java -jar gerrit-3.9.0.war init -d /var/gerrit # 按前述配置完成交互式初始化步骤 3:Nginx 配置(/etc/nginx/conf.d/gerrit.conf)
upstream gerrit_backend { server 127.0.0.1:8080; keepalive 32; } server { listen 443 ssl http2; server_name gerrit.company.com; ssl_certificate /etc/letsencrypt/live/gerrit.company.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/gerrit.company.com/privkey.pem; location / { proxy_pass http://gerrit_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 300; # 关键!避免大 patchset 提交超时 } location /static/ { alias /var/gerrit/static/; expires 1h; } }重载 Nginx:systemctl reload nginx
步骤 4:启动 Gerrit 服务
创建 systemd 服务文件/etc/systemd/system/gerrit.service:
[Unit] Description=Gerrit Code Review After=network.target [Service] Type=simple User=gerrit WorkingDirectory=/var/gerrit ExecStart=/usr/bin/java -Dgerrit.home=/var/gerrit -jar /var/gerrit/bin/gerrit.war daemon -d /var/gerrit Restart=on-failure RestartSec=10 LimitNOFILE=65536 [Install] WantedBy=multi-user.target启用服务:
systemctl daemon-reload systemctl enable gerrit systemctl start gerrit步骤 5:验证与首登
- 访问
https://gerrit.company.com,用初始化时设置的管理员账号登录; - 进入
Settings > Preferences,确认Diff view设为Side-by-side(提升审查效率); - 执行
ssh -p 29418 admin@gerrit.company.com gerrit version,返回gerrit version 3.9.0即成功。
4.2 关键参数计算与调优实录
JVM 堆内存分配
Gerrit 内存消耗主要来自三块:
- Lucene 索引缓存:占堆外内存,建议
XX:MaxDirectMemorySize=2g; - Git 对象缓存:按公式
Git repository size × 0.1估算,100GB 仓库需 10GB; - Java 堆:剩余部分,但必须 ≥ 4GB(否则频繁 Full GC)。
我们最终配置:
# 在 gerrit.sh 中修改 JAVA_OPTIONS JAVA_OPTIONS="-Xmx6g -Xms6g -XX:MaxDirectMemorySize=2g -XX:+UseZGC"实测效果:200 用户在线时,堆内存稳定在 5.2~5.8GB,ZGC 停顿时间 < 5ms。
Lucene 索引分片策略
gerrit.config中:
[index] type = lucene directory = /var/gerrit/index threads = 4 # CPU 核数的一半,避免 I/O 争抢 maxWriteMBPerSec = 10 # 限制写入速度,防 SSD 寿命损耗首次全量索引耗时约 3.2 小时(120 个项目,总代码量 8TB),但后续增量索引控制在 200ms 内。
SSH 连接池调优
gerrit.config中:
[sshd] listenAddress = *:29418 idleTimeout = 30m maxConnections = 200 maxConnectionsPerUser = 20maxConnectionsPerUser = 20是关键——防止单个用户脚本(如批量创建分支)耗尽连接,导致其他用户git push报Connection refused。
5. 常见问题与排查技巧实录
5.1 Submit 按钮灰掉:五步定位法
这是最高频问题,表面是前端按钮禁用,根因在后端策略引擎。按顺序排查:
| 步骤 | 检查命令 | 预期输出 | 问题定位 |
|---|---|---|---|
| 1. 检查 Change 状态 | ssh -p 29418 admin@host gerrit query change:12345 --format=JSON | "status": "NEW" | 若为MERGED或ABANDONED,Submit 逻辑不触发 |
| 2. 检查 Submit Rule | ssh -p 29418 admin@host gerrit get-project --format=JSON project-name | jq '.submit_type' | "MERGE_IF_NECESSARY" | 若为FAST_FORWARD_ONLY,需先 rebase |
| 3. 检查 Label 权限 | ssh -p 29418 admin@host gerrit ls-groups --verbose | grep -A5 'project-name' | label-Code-Review = -2..+2 group Administrators | 若label-Submit权限未赋给当前用户组,按钮必灰 |
| 4. 检查 PatchSet 状态 | ssh -p 29418 admin@host gerrit query change:12345 --current-patch-set --format=JSON | "approvals": [{"type":"Code-Review","description":"Code-Review","value":"+2","by":{"_account_id":1000000}}] | 缺少Verified或Submitlabel 的 +1 |
| 5. 检查 Ref 配置 | ssh -p 29418 admin@host gerrit ls-projects --format=JSON project-name | jq '.ref' | "refs/heads/main" | 若为refs/for/main,说明还在评审流,未到 Submit 阶段 |
实操心得:我曾遇到 Submit 灰掉且所有检查都通过,最后发现是
gerrit.config中[plugin "reviewnotes"] enabled = true导致插件冲突。关闭后立即恢复——这类插件级问题,必须查error_log中Plugin reviewnotes failed to start类日志。
5.2 Reviewer 收不到邮件:SMTP 配置避坑指南
Gerrit 邮件发送失败,90% 源于 SMTP 认证配置。gerrit.config中:
[sendemail] smtpServer = smtp.company.com smtpServerPort = 587 smtpEncryption = tls smtpUser = gerrit@company.com smtpPass = app_specific_password # 必须用应用密码,非邮箱密码! from = Gerrit <gerrit@company.com>关键点:
smtpEncryption = tls(非 ssl),否则连接超时;smtpPass必须是邮箱服务商提供的“应用专用密码”(如 Gmail 的 App Password),普通密码因 2FA 被拒;- 测试命令:
sudo -u gerrit /var/gerrit/bin/gerrit.sh run -c "sendemail --to test@company.com --subject 'test' --message 'ok'"。
5.3 Search 返回空:Lucene 索引重建全流程
当https://gerrit.company.com/#/q/status:open无结果,但gerrit query status:open命令有返回,即索引损坏。重建步骤:
- 停止 Gerrit:
systemctl stop gerrit; - 清空索引目录:
rm -rf /var/gerrit/index/*; - 启动 Gerrit 并等待:
systemctl start gerrit && tail -f /var/gerrit/logs/error_log; - 监控日志直到出现
Indexing 12345 changes... done; - 强制刷新浏览器缓存(Ctrl+F5),Search 恢复。
注意:重建期间所有 Search 请求返回空,但 Submit、Clone 等核心功能不受影响。我们通常选凌晨 2 点执行,耗时约 45 分钟。
5.4 Docker 部署常见故障速查表
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 容器启动后立即退出 | gerrit.war未挂载到容器内/var/gerrit | docker run -v /host/gerrit.war:/var/gerrit/bin/gerrit.war ... |
| Web UI 显示 502 Bad Gateway | Nginx 未配置proxy_read_timeout 300 | 在 location 块中添加该参数 |
git clone报fatal: unable to access 'https://...': Could not resolve host | 容器 DNS 配置错误 | 启动时加--dns 10.10.20.1指向内网 DNS |
ssh -p 29418 user@host gerrit version连接拒绝 | 容器未暴露 29418 端口 | docker run -p 29418:29418 -p 8080:8080 ... |
日志中大量WARN com.google.gerrit.sshd.SshDaemon : Failed to start SSHD | 容器内/var/gerrit/etc/ssh_host_rsa_key权限非 600 | chmod 600 /var/gerrit/etc/ssh_host_rsa_key |
最后分享一个小技巧:在gerrit.config中加入[plugin "download-commands"] enabled = true后,每个 Change 页面右上角会出现Download按钮,自动生成含git fetch+git format-patch的完整命令集——这比手动拼接git review -d稳定十倍,尤其适合 CI 脚本调用。这个细节,很多教程都漏掉了。