Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Vosmaer <jacob@gitlab.com>2020-02-21 14:15:46 +0300
committerJacob Vosmaer <jacob@gitlab.com>2020-02-21 14:15:46 +0300
commit443e3578520f7fb92f42a96d3fc77b0ad1381820 (patch)
tree85a0841a8ae31532ef9fe4e0500854effa5ba711
parent1f4e674faf4e3c2b727c09ce6c9cea7e01962746 (diff)
parent54a0120deb0336377c3c56d8995bdbe41861aa96 (diff)
Merge branch 'sh-gitaly-ha-consistency-docs' into 'master'
Add documentation about Gitaly HA consistency See merge request gitlab-org/gitaly!1830
-rw-r--r--doc/design_ha.md214
1 files changed, 214 insertions, 0 deletions
diff --git a/doc/design_ha.md b/doc/design_ha.md
index 477225dcb..46ec5bb43 100644
--- a/doc/design_ha.md
+++ b/doc/design_ha.md
@@ -125,6 +125,220 @@ to perform leader election.
When the beta nears completion further stages will be defined.
+## Eventual consistency
+
+The beta implemention above describes an eventually consistent system:
+when a repository is modified, the secondaries asynchronously fetch the
+changes. This is similar to how Geo works today:
+
+1. A new change is detected.
+1. An asynchronous process is queued.
+1. A `git fetch` is run to synchronize the latest changes.
+
+The main advantages of this approach:
+
+1. Writes are not delayed by a slow replica
+1. This can be implemented today without too much refactoring. This is
+ less complex than supporting strong consistency, as we will discuss below.
+
+However, the main disadvantage of this approach is that there are no
+consistency guarantees: a replica may be out of sync for seconds,
+minutes, or even hours. For busy repositories, this makes it difficult
+to offload read queries to the replica, which is especially important
+for projects that rely on continuous integration to test the latest
+changes.
+
+## Compared to Geo
+
+Despite the similarities above, there are significant differences
+between Gitaly HA and Geo:
+
+1. High-availability vs. disaster recovery: These are fundamentally
+ different goals. Gitaly HA enables GitLab to function even if a
+ single Gitaly node goes down by transparently failing over to a
+ secondary Gitaly node.
+
+ From operational perspective Geo is a disaster recovery solution. If
+ the primary datacenter goes down, any data stored in GitLab will be
+ preserved in another location. Some data loss is acceptable, since
+ having a significant amount of data--even if it stale--is better than
+ having no data at all. In addition, the Geo-repliaced instance can
+ take over for the primary with some manual coordination. However, Geo
+ does not handle failure of a single Gitaly node.
+
+1. Unlike Geo, strong consistency is most likely a requirement for
+ Gitaly HA. Gitaly HA has to be able to fail over to replicas without
+ human interaction.
+
+1. Gitaly HA only replicates Git repository data and omits other GitLab
+ artifacts (e.g. upload attachments, Git LFS files, CI artifacts, Docker
+ containers, etc.).
+
+1. Under the hood, the manner in which Geo and Gitaly HA detect
+repository changes is subtlely different but important. For example,
+when a user pushes to a Geo primary instance, the Git post-receive
+handler generates emits an event (in the form of a database row in
+PostgreSQL) that tells secondaries that the repository has changed. In
+Gitaly HA, Praefect directly handles the RPC that will mutate the
+repository. This architecture makes it possible for Praefect to support
+strong consistency.
+
+### Strong consistency
+
+Strong consistency is desirable for a number of reasons:
+
+1. Minimizes data loss: Strong consistency guarantees that a replica
+will never be out of sync with the primary Gitaly node. If a primary
+fails with no up-to-date secondaries available, then the latest commits
+will be gone. Merge requests may fail to load, CI builds will fail to
+fetch references, and users will be upset.
+
+1. Distributed reads: Strong consistency also makes it more likely that
+reads can be directed to secondaries. If secondaries take minutes to
+sync with the primary, then all reads will have to be directed to the
+primary until the secondaries catch up.
+
+1. Faster replication: When implemented properly, it should be possible
+to optimize the amount of time and data needed to update secondaries. In
+the eventual consistent implementation described above, a `git fetch` is
+used to synchronize the secondaries. However, this `git fetch` is a bit
+of a blunt tool: the secondaries have to negotiate with the primary what
+data it doesn't have, and the primary has to calculate the delta and send
+the difference.
+
+However, there are downsides:
+
+1. It will require a significant amount of work to refactor each Gitaly
+RPC to support consistent replication.
+1. It may be harder to prove correctness.
+
+An eventually consistent model may be "good enough" if replication delay
+can be minimized and handled gracefully.
+
+#### Example: Replicating Git pushes
+
+If we assume that all replicas are all in sync with the primary, a push
+to the primary can be replicated in parallel to many nodes. A consistent
+replication approach might look like:
+
+```mermaid
+sequenceDiagram
+ User->>Praefect: Push to repository X
+ Praefect->>Node A: Stream receive-pack data (ReceivePack)
+ Praefect->>Node B: Stream receive-pack data (ReceivePack)
+ Node A->>Praefect: Ready to commit
+ Node B->>Praefect: Ready to commit
+ Praefect->>Node A: Commit refs
+ Praefect->>Node B: Commit refs
+ Node A->>Praefect: Success!
+ Node B->>Praefect: Success!
+```
+
+This diagram starts with a user pushing data to GitLab, which is handled
+by the Gitaly `ReceivePack` RPC. Let's suppose the user attempts to push
+two branches `debug` and `master`. The Git protocol data might look like
+the following:
+
+```
+ C: 00677d1665144a3a975c05f1f43902ddaf084e784dbe 74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/debug\n
+ C: 006874730d410fcb6603ace96f1dc55ea6196122532d 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a refs/heads/master\n
+ C: 0000
+ C: [PACKDATA]
+```
+
+If we assume nodes A and B are in sync, the `ReceivePack` RPC should
+first do the following:
+
+1. Check that the `before` SHAs match in both `debug` and `master`. For example,
+ `00677d1665144a3a975c05f1f43902ddaf084e784dbe` should be the tip of `debug`.
+1. Execute git receive-pack, which then:
+ a. Decodes the packfile
+ b. Writes the new objects into the Git repository
+
+##### Three-phase commit
+
+To ensure consistency, we can only update the refs for `debug` and
+`master` when we are sure that all nodes can perform that operation.
+That is why in the diagram above, we need a three-phase commit protocol:
+the nodes should only update Git references when Praefect receives
+acknowledgement that all nodes are able to update. This update also has
+to be done atomically; we either want both `debug` and `master` to be
+updated or not at all. This can be done using [Git's `--atomic`
+option](https://github.com/git/git/blob/ff66981f4593aec0f3b3eeace0eacb7dbe44fd8c/Documentation/git-push.txt#L210-L213).
+
+How might we make the push wait for Praefect to confirm? One way may be
+to use the Git pre-receive hook. Normally, Gitaly uses the pre-receive
+hook to make an internal API call to check whether the push should be
+allowed go through. We still need to do that for the primary case, but
+we can skip this for the secondaries and simply make a separate Gitaly
+RPC to indicate the current transaction is ready to go:
+
+```mermaid
+sequenceDiagram
+ Praefect->>Node B: Stream receive-pack data (ReceivePack)
+ Node B->>Node B: git receive-pack --atomic
+ Node B->>Node B: pre-receive <changes>
+ Node B->>Praefect: Ready to commit
+ Praefect->>Node B: Commit refs
+ Node B->>Node B: <exits pre-receive with exit code 0>
+ Node B->>Node B: git update-ref <changes>
+ Node B->>Praefect: Success!
+```
+
+This diagram shows in more detail how Node B performs the three-phase
+commit. `git receive-pack` will call the `pre-receive` hook once the Git
+data has been received. If Node B is in a good state (i.e. branches are
+at the right SHAs), Node B will notify Praefect `Ready to commit`. Once
+`Praefect` responds with `Commit refs`, the pre-receive handler will
+exit with success code 0, and the reference updates will commence. When
+the update finishes with success, the `receive-pack` process should exit
+successfully, and `ReceivePack` will respond with a success message.
+
+Note that if the primary node does not receive authorization to commit
+the changes, the transaction is aborted. Alternatively, we can check for
+authorization before deciding to send the data to all replicas, but this
+might slow the push down.
+
+Using Git's `receive-pack` on the nodes with this pre-receive hook
+should ensure that the objects sent via the packfile should be stored in
+a temporary, quarantined directory until the transaction completes. This
+should avoid causing lots of stale, uncommitted objects from being
+written to a secondary.
+
+##### Handling failures
+
+What happens if any of the secondaries fail during the replication
+attempt? There are several options:
+
+1. Fail the push entirely
+1. Commit the transaction if there is a quorum (e.g. 2 of 3 ready to commit)
+
+When a node is out of sync, it needs to be taken out of rotation, and
+Praefect has to initiate a repair operation (e.g. `git fetch`, reclone,
+delete out-of-sync branches).
+
+#### Example: Replicating merges (`UserMergeBranch` RPC)
+
+As seen in the push case, atomic transactions are crucial for
+maintaining consistency. However, like `SSHReceivePack`, there are many
+other RPCs that create objects and update references. Suppose a user
+merges `develop` into `master`. The `UserMergeBranch` RPC handles the
+merge [in the following
+way](https://gitlab.com/gitlab-org/gitaly/blob/bb6fc2885c4acfb0bcff11e3b3ea0ce991d191ce/ruby/lib/gitlab/git/repository.rb#L279-284):
+
+1. Create a merge commit that merges `develop` and `master`
+2. Update `master` to point to this merge commit
+
+For strong consistency to work, we need to write this merge commit to
+all nodes and wait for acknowledgement before step 2 can proceed. Every
+RPC that mutates the repository therefore has to be modified to ensure
+that a three-phase commit will synchronize changes properly.
+
+One wrinkle here is that some Gitaly RPCs are implemented in Go, and
+some are implemented in Ruby via gitaly-ruby. We either have to
+implement three-phase commit in both languages or port the remaining
+RPCs to Go.
+
## Notes
* Existing discussions
* Requirements: https://gitlab.com/gitlab-org/gitaly/issues/1332