Some Gerrit releases bring new index schema version(s). In that case a reindexing is needed in order to populate the new schema version. This process is also called index schema upgrade.
Reindexing can be done offline or online. For zero-downtime Gerrit installations, offline reindexing is often not acceptable as it may require a significant downtime. Online reindexing provides index schema upgrade while Gerrit server is running and serving user traffic.
While online reindexing helps to reduce or, in a multi-primary setup, eliminate downtime it also has its own set of issues:
Online index schema upgrade is performed as a background reindexing task. In addition, when a change gets created/updated during reindexing, the Gerrit server will index it in both the old and new index version. Index queries must use the old index version until the index schema upgrade is completed.
The background reindexing task will index each change in the new index version. Note that the set of changes that this task will traverse may not include those changes that will be created/updated during the lifetime of the reindexing task. For example, reindexing all changes of a project P may already be finished and a new change may get created afterwards. This is why, in addition to the background reindexing task, we also need the concept of the write indexes set and the search index.
Search index (S) is the index version which is used to serve queries. Write indexes set (W) is the set of index versions that is updated when an entity gets reindexed.
During normal operation, when reindexing is not running, the write indexes set will consist of only one index version, the latest index version (Vn). The search index would also be the latest index version:
Normal operation: W = {Vn}, S = Vn
However, during an index schema upgrade the write indexes set consists of two index versions, the old (Vn) and the new index version (Vn+1). The search index is the old index version.
Online reindexing: W = {Vn, Vn+1}, S = Vn
After online reindexing is completed Gerrit stops writing to the old index version and uses the new index version only:
Normal operation: W = {Vn+1}, S = Vn+1
A major goal in optimizing online reindexing is to minimize the time that a Gerrit primary server spends in the online reindexing mode. This can be achieved by preparing the new index version outside of the Gerrit primary server process. The design of the solution depends on whether a local Lucene index or a shared index is used.
A technical limitation of the Lucene engine is that one index can only be open from one JVM process. This limitation means that we cannot externalize the background reindexing task into a separate JVM process because this would require write access to the old and new index versions from both Gerrit primary's JVM process and the JVM process running the background reindexing task. However, we could prepare a copy of the new index version by running the reindexing task in a separate JVM process. When the reindexing task is completed the created new index version is made available to the Gerrit primary server. The new index version is not completely up-to-date and the Gerrit primary needs to run full online reindexing. However, this operation will be fast if the Gerrit primary only reindexes stale entries or, in other words, reuses the existing index documents which are up-to-date.
This change introduced the feature of reusing existing documents when reindexing. This feature is a building block for implementing optimized online reindexing:
To reuse existing index documents in the new index version it is necessary to prepare the new index version. This can be done outside of the Gerrit primary process. During this phase the primary Gerrit still runs on the old index version. When the new index is ready it is copied to the Gerrit primary site and the primary Gerrit is upgraded and the online index schema upgrade is performed with the option to reuse existing documents. Gerrit primary will skip reindexing of all up-to-date index documents and will only reindex those entities whose entries in the index are stale. Based on our measurements, even for very large Gerrit sites with more than 6 million of changes, this usually finishes in 1-2 hours. Without the optimization the online index schema upgrade can take days to finish.
Preparation phase:
The online reindex phase uses already prepared Vn+1 index:
Online index schema upgrade completed:
The previously described optimized upgrade process, if performed manually, is significantly more complex than the default upgrade process. It is also error prone as it depends on the human operator to execute the steps in the correct order.
With the k8s-gerrit we have many possibilities to completely automate the optimized index schema upgrade process.
This task can be implemented as a k8s job which runs on demand. The task performs full reindexing and stops. The reindexing can be performed as offline, the reindex program, or online, whatever is better suited for our needs. Note that this job operates on its own index copy. When this job finishes, the created indexes are copied to a shared volume that can be accessed from the primary Gerrit servers.
The gerrit-init container copies the latest prepared new index version from a shared storage to the index directory of the primary Gerrit container. This will only happen if this index version is not yet present in the index directory of this Gerrit primary. Thus, when the primary Gerrit starts it may already have a new index version prepared. If an online index schema upgrade is needed it will be performed with the option to reuse existing documents.
Note that the preparation and the usage of the new index version are fully decoupled. The primary Gerrit server will make use of any prepared new index version but it will also be able to perform index schema upgrades if the new index version is not prepared.
Preparation:
gerrit-init container run:
In order to prepare the new index version, the Gerrit indexer job needs to use a container with the new Gerrit version. However, Gerrit primary needs to continue using the old index version during the index preparation phase. While this can be achieved by using a container with the old Gerrit version for running Gerrit primary, it may be undesirable to use different Gerrit versions in different containers. Ideally, all containers shall use the same Gerrit version. In order to run Gerrit primary with the new Gerrit version we need to ensure that during the new index version preparation it:
When using a shared index, for example based on OpenSearch or ElasticSearch, then the limitation of using one index version from one Gerrit process is gone. In this scenario, optimization of the online index schema upgrade is more straightforward than in the case of using local Lucene indexes.
In this case, the default upgrade index schema upgrade process could be applied with only one optimization:
In this scenario the background reindexing task is moved to run into a separate container outside of the primary Gerrit process:
When the reindexing job finishes, Gerrit primary servers atomically switch to using the new index version as the search index and continue writing into the new index version only.
In case of performance or any other issues after an index schema upgrade sometimes the only option may be to rollback to the previous Gerrit version. This requires keeping the old index up-to-date for some time after switching to the new index version. Technically, the write indexes set has to include the old index version during that period. After a period of time where the new Gerrit version runs smoothly, the old index version can be finally removed from the write set. With that action, rollback to the Gerrit version that requires the old index version is not anymore possible without recreating the old index version.
Normal operation, after online reindexing is completed, rollback possible:
W = {Vn, Vn+1}, S = Vn+1
Normal operation, after online reindexing is completed, rollback impossible:
W = { Vn+1}, S = Vn+1
A challenge when scaling up Gerrit primary servers is to provide an up-to-date, or nearly up-to-date, index for the new primary server(s) in a fully automated way. A possible solution is to convert the “Gerrit indexer” job (see Usage of the prepared new index version) into a k8s cron job. In that solution, “Gerrit indexer” would run on a cron schedule, for example every two hours. Alternatively, index snapshots could be created from already running primary Gerrits and provided to the new Gerrit instance. This means that a nearly up-to-date index is available at any time on a shared volume. When a new Gerrit primary server needs to be created, the gerrit-init container would copy the index from the shared volume. The new Gerrit primary server then needs to run a full reindexing of the latest index version with the option to reuse existing index documents. The new Gerrit primary should be declared ready to k8s until the reindexing is finished but it should join the HA cluster and start receiving and processing HA messages immediately.
The Gerrit Operator provides the GerritIndexer
CustomResource to prepare a new index version. To make use of it, first the volumes containing the data have to be prepared.
Since the Volume containing the Gerrit site, including the indexes and caches, are running in ReadWriteOnce
mode, it can't be mounted into the GerritIndexer pod, while the primary Gerrit is still running. To solve this, create a VolumeSnapshot of one of the site volumes, e.g.:
apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: gerrit-site-gerrit-primary-0-snapshot namespace: gerrit spec: source: persistentVolumeClaimName: gerrit-site-gerrit-primary-0 # Name of the PVC containing a Gerrit site volumeSnapshotClassName: default
To be able to use the VolumeSnapshot as a Volume, create a PersistentVolumeClaim like this:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gerrit-site-gerrit-primary-0-snapshot namespace: gerrit spec: dataSource: apiGroup: snapshot.storage.k8s.io kind: VolumeSnapshot name: gerrit-site-gerrit-primary-0-snapshot # Name of the VolumeSnapshot created above resources: requests: storage: 5Gi # Has to be the size configured for the primary Gerrit's site in the GerritCluster volumeMode: Filesystem accessModes: - ReadWriteOnce
In k8s-gerrit, when using the high-availability setup, the repositories are located on a shared volume. There are two options on how to mount this data into the GerritIndexer pod:
Since the Volume used to store repositories is required to use the ReadWriteMany
mode, it can directly be mounted into the GerritIndexer pod. The advantage is that this requires less preparation and is cheaper. However, the GerritIndexer might affect performance of the client-serving Gerrit instances by competing for the resources of the NFS server.
To avoid that, a VolumeSnapshot can be used as for the other site components. Note, that this will only be possible if a CSI driver for the filesystem being used is available.
Before updating Gerrit to a version bringing a new version of an index, especially the changes index, disable the online upgrade of the indexes (set index.onlineUpgrade
to false
). Then update the version of the container images to the version containing the new Gerrit version and apply the updated GerritCluster resource. This will update Gerrit, but Gerrit will continue to use the previous index versions. Note, that the groups and accounts indexes will always be updated offline during startup. This of course only works, if the new Gerrit version still supports the previous index version.
Create a GerritIndexer CustomResource like this example (see reference):
apiVersion: "gerritoperator.google.com/v1beta9" kind: GerritIndexer metadata: name: gerrit-indexer namespace: gerrit spec: # This has to match the name of the GerritCluster serving the data to be # indexed cluster: gerrit-cluster resources: requests: cpu: 2 memory: 5Gi limits: cpu: 3 memory: 6Gi # As for the client-serving Gerrit instances, the operator will enforce some # configuration configFiles: gerrit.config: |- [gerrit] serverId = gerrit-1 disableReverseDnsLookup = true [index] type = LUCENE [auth] type = DEVELOPMENT_BECOME_ANY_ACCOUNT [httpd] requestLog = true gracefulStopTimeout = 1m [transfer] timeout = 120 s [user] name = Gerrit Code Review email = gerrit@example.com anonymousCoward = Unnamed User [container] javaOptions = -Xms200m javaOptions = -Xmx4g storage: site: # The name of the PVC containing the Gerrit site persistentVolumeClaim: gerrit-site-gerrit-0-snapshot repositories: # The name of the PVC containing the repositories persistentVolumeClaim: shared-pvc # The path in the mounted volume containing the repositories subPath: git output: # The name of the PVC to use for storing the new indexes persistentVolumeClaim: shared-pvc # The path in the mounted volume where to store the indexes subPath: shared/indexes
It is highly recommended to use the shared volume used by the GerritCluster with a subPath under shared
to store the new index versions, since this way the indexes will be available right away in the Gerrit pods. This is the case by default.
When the GerritIndexer is applied to the cluster, the Gerrit Operator will create a Job that will run offline reindexing on the repositories. It will use the caches available in the mounted site. After the indexes were created, they will be copied to the output volume.
This step is currently not automated and requires manual steps.
Go into each primary Gerrit pod and copy the new indexes to the index directory in the Gerrit site.
Now, set index.onlineUpgrade
and index.reuseExistingDocuments
to true
. When applying the updated Gerrit config (by applying the updated GerritCluster resource), the Gerrit pods will restart and start online reindexing. Since most entries will already be available in the index, only changes updated in the meantime will be reindexed.
Since Kubernetes Jobs are immutable after running, it is recommended to delete the GerritIndexer CustomResource after the index upgrade has been completed.