公司采用 Gerrit 代码版本控制软件管理 Android 代码,随着仓库的增加,研发访问量的逐步上升,Gerrit 服务器本身压力逐渐呈现。
现决定利用 Gerrit 自身的 Replication 插件搭建主从备份机制,以期缓解主 Gerrit 服务器压力。
主服务器
从服务器
我采用的是 Docker 部署(主要便于维护以及隔离),但此处有一个坑:docker 容器内的运行账号与宿主机账号的不一致(UID 与 GID 不一致),会导致 Replication 之后,从服务器的Gerrit 无法访问 git 数据。
如果没有我这种情况的,请直接跳过此步骤,直接前往 主服务器配置。
公司内提供的服务器已经预装好了 CentOS,并且有一个初始的系统管理员账号 XXX,因此它占用了 1000 这个 UID;
而 Docker 容器运行时,一般都是以 1000 这个 UID 运行,例如此处的 Gerrit ,在运行容器时,内部账号是:
uid=1001(gerrit) gid=1001(gerrit) groups=1001(gerrit)
同时 replication 插件在同步的时候是采用 ssh 协议,因此是以从服务器(此处 UID 是 1001) 的宿主机账号登录并且推送,导致同步到从服务器之后的代码文件权限也是 1001 ,与 docker 容器内的 Gerrit 账号 (UID 1000)不一致,导致无法访问。
因此重新构建了一个 Gerrit Docker 镜像,使 Gerrit 账号以 1001 运行即可。
Dockerfile 如下:
FROM gerritcodereview/gerrit:3.7.2
USER root
RUN set -xe \
&& usermod -u 1001 gerrit \
&& groupmod -g 1001 gerrit
USER gerrit
构建 Docker 镜像:
docker build -t new_gerrit-3.7.2:1.0 .
用 docker-compose 编排 容器:
version: "3"
services:
gerrit_server:
restart: always
hostname: gerrit_server
image: "new_gerrit-3.7.2:1.0"
ports:
- "29418:29418"
- "8080:8080"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- /data/gerrit/review_site/git:/var/gerrit/git
- /data/gerrit/review_site/db:/var/gerrit/db
- /data/gerrit/review_site/index:/var/gerrit/index
- /data/gerrit/review_site/cache:/var/gerrit/cache
- /data/gerrit/review_site/etc:/var/gerrit/etc
environment:
- HTTPD_LISTEN_URL=proxy-http://*:8080
- CANONICAL_WEB_URL=http://11.11.11.11:8080
deploy:
resources:
limits:
memory: 32G
docker-compose up -d gerrit_server
第一次运行容器的时候,可能会报权限错误,原因是第一次会自动创建 /var/gerrit/[git,db,index,cache,etc] 几个目录,并且是以 root 创建。
建议在运行容器前,先手动创建目录:
mkdir -p /data/gerrit/review_site/git
mkdir -p /data/gerrit/review_site/db
mkdir -p /data/gerrit/review_site/index
mkdir -p /data/gerrit/review_site/cache
mkdir -p /data/gerrit/review_site/etc
mkdir -p /data/gerrit/review_site/etc/mail
或者在失败后,手动修改目录权限:
sudo chown -R 1001:1001 /data/gerrit/
/data/gerrit 是我本地的目录,请根据自己情况修改。
主从服务器都可按照相同配置先部署并启动。
主服务器启动后,新增 replication 配置文件:
/data/gerrit/review_site/etc/replication.config
增加内容:
[gerrit]
autoReload = true
replicateOnStartup = true
defaultForceUpdate = true
[remote "gerrit_slave"]
url = ssh://[email protected]:/data/gerrit/review_site/git/${name}.git
push = +refs/*:refs/*
mirror = true
replicatePermissions = true
rescheduleDelay = 5
配置主服务器 ssh config
由于主服务器 Replication 插件采用 ssh 协议,因此需要配置主服务器到从服务器的 ssh 免密登录。
我这里采用的 docker 部署,因此需要在主服务器的 Gerrit 容器内配置。
进入容器
docker exec -it gerrit_gerrit_server_1 bash
生成 ssh key
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/var/gerrit/.ssh/id_rsa):
Created directory '/var/gerrit/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /var/gerrit/.ssh/id_rsa.
Your public key has been saved in /var/gerrit/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:4A9i0XH0wxqR04V9odXshHutgE+U6Yw696wxFLWnGYA gerrit@gerrit_server
The key's randomart image is:
+---[RSA 3072]----+
| ..++.+ooo= |
| . oE+oo=+o +|
| . o ..=*+..+.|
| o . ooo==. +|
| o o S..oo. o |
| . . oo.. . . |
| .ooo |
| oo |
| .. |
+----[SHA256]-----+
配置 /var/gerrit/.ssh/config 文件:
IdentityFile ~/.ssh/id_rsa
StrictHostKeyChecking no
PreferredAuthentications publickey
Host *
IdentityFile ~/.ssh/id_rsa
PreferredAuthentications publickey
保存退出。
配置免密登录
ssh-copy-id -i ~/.ssh/id_rsa.pub [email protected]
输入 yes ,然后输入密码后,成功!
bash-4.4$ ssh-copy-id -i ~/.ssh/id_rsa.pub [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/var/gerrit/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.
(if you think this is a mistake, you may want to use -f option)
在从服务器上只需要做一个配置:
/data/gerrit/review_site/etc/gerrit.config
在 container 配置段中增加配置项:
[container]
slave = true
docker restart gerrit_gerrit_server_1
并且配置此项后,从服务器的 web 端将不可用:
web 页面提示 “Not Found”
官方如此设计是为了防止用户在从服务器的页面修改、删除、提交等操作后,导致主从服务器的信息不同步。
并且,如果尝试往从服务器推送,会出现以下错误信息:
$ git push origin HEAD:refs/for/master
fatal: Service not enabled
fatal: remote error: Service not enabled
$ git push origin HEAD:master
fatal: Service not enabled
fatal: remote error: Service not enabled
直接推送和推送代码审核分支都不行。
但是 fetch 和 pull 不会报错。
$ git fetch origin -v
remote: Counting objects: 5, done
remote: Finding sources: 100% (3/3)
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), 388 bytes | 97.00 KiB/s, done.
From ssh://22.22.22.22:29418/test
645220d..fff98e5 master -> origin/master
需要利用 Gerrit ssh 后台管理接口加载并启动 Replication
重新加载主服务器的 Replication 插件:
11.11.11.11 是主服务器
# 重新加载插件
ssh -p 29418 [email protected] gerrit plugin reload replication
启动同步任务
ssh -p 29418 [email protected] replication start --all
查看同步任务
ssh -p 29418 [email protected] replication list --detail
至此,基于 Gerrit Replication 插件实现的主从服务器(镜像备份服务器)的搭建已完成,主服务器上的所有变更:分支、Tag、Gerrit 所有 Changes 都会自动同步备份到从服务器。
实现了 Gerrit 的热备份。
同时,按照这个方法步骤,也可以实现一主多从的方案,只需要在主服务器的 replication.config 中配置多个 [remote] 段即可。
[gerrit]
autoReload = true
replicateOnStartup = true
defaultForceUpdate = true
[remote "gerrit_slave"]
url = ssh://[email protected]:/data/gerrit/review_site/git/${name}.git
push = +refs/*:refs/*
mirror = true
replicatePermissions = true
rescheduleDelay = 5
[remote "gerrit_slave_beijing"]
url = ssh://[email protected]:/data/gerrit/review_site/git/${name}.git
push = +refs/*:refs/*
mirror = true
replicatePermissions = true
rescheduleDelay = 5
[remote "gerrit_slave_shanghai"]
url = ssh://[email protected]:/data/gerrit/review_site/git/${name}.git
push = +refs/*:refs/*
mirror = true
replicatePermissions = true
rescheduleDelay = 5
当然,其中的从服务器代码仓路径可根据实际情况修改。
更多其他 replication.config 的参数可以参考官方文档:
https://gerrit.googlesource.com/plugins/replication/+doc/master/src/main/resources/Documentation/about.md
或者本地搭建的 Gerrit 服务中的文档:
http://[本地-Gerrit-IP]/plugins/replication/Documentation/config.md
替换为自己的IP地址。
为了缓解 Gerrit 主服务器的压力,研发本地配置代码的读写分离,写操作(push)走主服务器,读操作(fetch、pull)走从服务器。
仅需要在 研发本地修改 git 配置(在 git 仓库目录下执行):
jjran 修改为自己的账号。
# 替换 push url 地址, 将 22.22.22.22 替换为 11.11.11.11
git config --add url."ssh://[email protected]:29418".pushInsteadOf ssh://[email protected]:29418
需要注意这个命令两个 ip 地址的顺序含义:在 push 操作的时候,用左边的 ip 地址替换右边的 ip 地址
执行完成后,当前 git 仓库内的 .git/config 文件内容:
忽略其他配置项
[remote "origin"]
url = ssh://[email protected]:29418/test
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[url "ssh://[email protected]:29418"]
pushInsteadOf = ssh://[email protected]:29418
我这里仅对一个 git 代码仓生效,如果要对本地所有 git 仓库生效,可以加上全局参数 --global
git config --global --add url."ssh://[email protected]:29418".pushInsteadOf ssh://[email protected]:29418
经过以上配置之后,本地的读写即可实现分离功能:
$ git push origin HEAD:refs/for/master
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 325 bytes | 325.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Processing changes: refs: 1, new: 1, done
remote:
remote: SUCCESS
remote:
remote: http://11.11.11.11:8080/c/test/+/42 ranyi add 33 [NEW]
remote:
To ssh://11.11.11.11:29418/test
* [new reference] HEAD -> refs/for/master
$ git fetch origin
remote: Counting objects: 5, done
remote: Finding sources: 100% (3/3)
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), 367 bytes | 91.00 KiB/s, done.
From ssh://22.22.22.22:29418/test
ef8f246..645220d master -> origin/master
可以看到上面在执行 push 的时候 url 是 ssh://11.11.11.11:29418
而在 fetch 的时候 url 是 ssh://22.22.22.22:29418
更简单的查看 url 方式:
$ git remote -v
origin ssh://[email protected]:29418/test (fetch)
origin ssh://[email protected]:29418/test (push)
注意:建议使用 --global 参数针对全局性配置
git config --global --add url."ssh://[email protected]:29418".pushInsteadOf ssh://[email protected]:29418
关于 git config pushInsteadOf 配置,有一个小坑需要注意:
我这里跳过了这个坑,是因为我当前初始 clone 是从 从服务器 进行的。
如果本地 git 仓库最初 clone 的时候,是直接从 主服务器 clone,
即,使用 clone 命令:
$ git clone ssh://[email protected]:29418/test
那么本地 .git/config 配置应该是:
省略其他配置项
[remote "origin"]
url = ssh://[email protected]:29418/test
fetch = +refs/heads/*:refs/remotes/origin/*
git remote -v 查看:
$ git remote -v
origin ssh://[email protected]:29418/test (fetch)
origin ssh://[email protected]:29418/test (push)
然后添加 pushInsteadOf 之后:
[remote "origin"]
url = ssh://[email protected]:29418/test
fetch = +refs/heads/*:refs/remotes/origin/*
[url "ssh://[email protected]:29418"]
pushInsteadOf = ssh://[email protected]:29418
此时 push 没有问题,但 fetch 的时候仍然是走主服务器:
$ git remote -v
origin ssh://[email protected]:29418/test (fetch)
origin ssh://[email protected]:29418/test (push)
这并不符合我们期望!
所以,还是需要将 remote url 改为从服务器地址才行,或者初始 clone 仍然需要从 从服务器 clone:
[remote "origin"]
url = ssh://[email protected]:29418/test
fetch = +refs/heads/*:refs/remotes/origin/*
[url "ssh://[email protected]:29418"]
pushInsteadOf = ssh://[email protected]:29418
尝试添加 insteadOf 选项并没有成功,发现 insteadOf 的优先级高于 pushInsteadOf
$ git config --add url."ssh://[email protected]:29418".pushInsteadOf ssh://[email protected]:29418
$ git config --add url."ssh://[email protected]:29418".insteadOf ssh://[email protected]:29418
$ git remote -v
origin ssh://[email protected]:29418/test (fetch)
origin ssh://[email protected]:29418/test (push)
并未生效,目测 insteadOf 优先级高于 pushInsteadOf !
关于读写分离的 git 配置,还有另外一种配置方法:
remote 字段中单独指定 pushurl:
.git/config 文件:
[remote "origin"]
url = ssh://[email protected]:29418/test
pushurl = ssh://[email protected]:29418/test
fetch = +refs/heads/*:refs/remotes/origin/*
执行 git remote -v 查看:
$ git remote -v
origin ssh://[email protected]:29418/test (fetch)
origin ssh://[email protected]:29418/test (push)
可以生效!
对服务器的灾备一直是所有技术公司必备的一个话题,都是为了以防万一。
这里 Gerrit 的主从搭建方案也是为了防止主服务器宕机后,无法提交下载代码。
由于我们已经搭建了主从备份,并且配置了 +refs/:refs/ 因此 git 仓库下的所有 ref 都会备份到从服务器。
主服务器已宕机,完全不可访问。
我们只需要在修改从服务器上的配置文件 etc/gerrit.config 中,注释掉或者删除 slave = true 即可:
[container]
# slave = true
然后重启从服务器:
此处我使用 docker 部署
$ docker restart gerrit_gerrit_server_1
从服务器重启后,web 页面是空的,changes 无法显示出来。
这是由于,replication 同步的是 git 仓库数据,也就是 refs/* 下的所有内容,虽然也包含了 changes (refs/changes)
但,Gerrit 页面上的显示内容是 Gerrit Index 数据,即索引数据,是需要用 Gerrit 的 reindex 重新构建索引才能在网页中显示出来。
执行 Gerrit ssh 命令:
注意此处是要执行 从服务器 上的 Gerrit 命令
$ ssh -p 29418 [email protected] gerrit index start changes --force
Reindexer started
查看从服务器的日志
不出意料的报错了。这里有个内部错误:
com.google.gerrit.server.notedb.InvalidServerIdException: invalid server id, expected 5775d2e3-0d62-4988-bcfc-edd41a70359a: actual: 7080f5c8-d3f7-4ea3-8926-82e478e1df3a
它的意思是,在 Gerrit 执行索引重建的时候发现 changes 的创建是 7080f5c8-d3f7-4ea3-8926-82e478e1df3a 这个服务器(主服务器) ID 创建的,
但当前的服务器ID与之不符,因此导致无法进行索引重建。
这是 Gerrit 内部设计,解决也容易,将从服务器的 serverId 修改为跟主服务器一致即可。
修改 Gerrit 服务器配置文件 etc/gerrit.config:
[gerrit]
basePath = git
canonicalWebUrl = http://22.22.22.22:8080
serverId = 7080f5c8-d3f7-4ea3-8926-82e478e1df3a
保存退出,然后重启 Gerrit 服务,再重新执行索引重建:
$ docker restart gerrit_gerrit_server_1
$ ssh -p 29418 [email protected] gerrit index start changes --force
Reindexer started
再次观察 Gerrit 日志:
[2023-07-26T02:37:00.976Z] [Reindex changes v79-v79] INFO com.google.gerrit.server.index.OnlineReindexer : Starting online reindex of changes from schema version 79 to 79
[2023-07-26T02:37:01.493Z] [Reindex changes v79-v79] INFO com.google.gerrit.server.index.OnlineReindexer : Reindex changes to version 79 complete
[2023-07-26T02:37:01.493Z] [Reindex changes v79-v79] INFO com.google.gerrit.server.index.OnlineReindexer : Using changes schema version 79
日志中有这样的输出,并且没有错误的话,就表示索引数据重建成功。
web 页面再次刷新:
最终完成从服务器转正,然后通知研发修改本地 ip 地址即可。
目前所有基于安卓的源码几乎都是在 1000 上下的 git 仓库量级,并且谷歌自己开发了 repo 工具来进行这种多库的管理,并且引入 manifest 配置文件。
repo 工具以及 manifest 配置文件,也是支持 git 库读写分离配置的。
只需要在 manifest 文件的 remote 标签中增加 pushurl 属性即可:
pushurl=“http://11.11.11.11:8080/” 属性单独指定 push 命令的 url 地址。这样我们在使用 git push 命令的时候,就使用该属性指定的 url。
review=“http://11.11.11.11:8080/” review 属性在使用 repo upload 命令的时候才会用到,否则可以不用指定。
修改后的结果:
$ repo forall -pvc git remote -v
project ranyi_test1/
origin http://22.22.22.22:8080/ranyi_test1 (fetch)
origin http://11.11.11.11:8080/ranyi_test1 (push)
project test/
origin http://22.22.22.22:8080/test (fetch)
origin http://11.11.11.11:8080/test (push)
提示: 如果按照前面提到的配置了 git config --global,
git config --global --add url."ssh://[email protected]:29418".pushInsteadOf ssh://[email protected]:29418
那么其实就不用再进行 manifest 的配置了。