-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Added documentation for Active-Active #3753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vladvildanov
wants to merge
12
commits into
feat/active-active
Choose a base branch
from
vv-AA-docs
base: feat/active-active
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ccc22eb
Added Active-Active documentation page
vladvildanov 0c664bd
Merge branch 'feat/active-active' of github.com:redis/redis-py into v…
vladvildanov 42ab5d5
Merge branch 'feat/active-active' of github.com:redis/redis-py into v…
vladvildanov 8e49c9a
Added documentation for Active-Active
vladvildanov 6bcb611
Merge branch 'feat/active-active' into vv-AA-docs
vladvildanov d83a1ea
Refactored docs
vladvildanov 166fb3a
Merge branch 'vv-AA-docs' of github.com:redis/redis-py into vv-AA-docs
vladvildanov 2ab08b2
Refactored pipeline and transaction section
vladvildanov 4cd426f
Merge branch 'feat/active-active' into vv-AA-docs
vladvildanov 8815800
Updated docs
vladvildanov 4cb18ef
Extended list of words
vladvildanov 8c909e5
Merge branch 'feat/active-active' into vv-AA-docs
vladvildanov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,383 @@ | ||
Active-Active | ||
============= | ||
|
||
MultiDBClient explanation | ||
-------------------------- | ||
|
||
Starting from redis-py 6.5.0 we introduce a new type of client to communicate | ||
with databases in Active-Active setup. `MultiDBClient` is a wrapper around multiple | ||
Redis or Redis Cluster clients, each of them has 1:1 relation to specific | ||
database. `MultiDBClient` in most of the cases provides the same API as any other | ||
client for the best user experience. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The core feature of `MultiDBClient` is automaticaly triggered failover depends on the | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
database healthiness. The pre-condition is that each database that is configured | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
to be used by MultiDBClient are eventually consistent, so client could choose | ||
any database in any point of time for communication. `MultiDBClient` always communicates | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
with single database, so there's 1 active and N passive databases that acting as a | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
stand-by replica. By default, active database is choosed based on the weights that | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
has to be assigned for each database. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
We have two mechanisms to verify database healthiness: `Healthcheck` and | ||
`Failure Detector`. | ||
|
||
To be able to use `MultiDBClient` you need to install a `pybreaker` package: | ||
|
||
.. code:: python | ||
|
||
pip install pybreaker>=1.4.0 | ||
|
||
The very basic configuration you need to setup a `MultiDBClient`: | ||
|
||
.. code:: python | ||
|
||
// Expected active database (highest weight) | ||
database1_config = DatabaseConfig( | ||
weight=1.0, | ||
from_url="redis://host1:port1", | ||
client_kwargs={ | ||
'username': "username", | ||
'password': "password", | ||
} | ||
) | ||
|
||
// Passive database (stand-by replica) | ||
database2_config = DatabaseConfig( | ||
weight=0.9, | ||
from_url="redis://host2:port2", | ||
client_kwargs={ | ||
'username': "username", | ||
'password': "password", | ||
} | ||
) | ||
|
||
config = MultiDbConfig( | ||
databases_config=[database1_config, database2_config], | ||
) | ||
|
||
client = MultiDBClient(config) | ||
|
||
|
||
Healthcheck | ||
----------- | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
By default, we're using healthcheck based on `ECHO` command to verify that database is | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
reachable and ready to serve requests (`PING` guarantees first, but not the second). | ||
Additionaly, you can add your own healthcheck implementation and extend a list of | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
healthecks | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
All healthchecks are running in the background with given interval and configuration | ||
defined in `MultiDBConfig` class. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Lag-Aware Healthcheck | ||
~~~~~~~~~~~~~~~~~~~~~ | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
This is a special type of healthcheck available for Redis Software and Redis Cloud | ||
that utilizes REST API endpoint to obtain an information about synchronisation lag | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
between given database and all other databases in Active-Active setup. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
To be able to use this type of healthcheck, first you need to adjust your | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`DatabaseConfig` to expose `health_check_url` used by your deployment. | ||
By default, your Cluster FQDN should be used as URL, unless you have | ||
some kind of reverse proxy behind an actual REST API endpoint. | ||
|
||
.. code:: python | ||
|
||
database1_config = DatabaseConfig( | ||
weight=1.0, | ||
from_url="redis://host1:port1", | ||
health_check_url="https://c1.deployment-name-000000.cto.redislabs.com" | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
client_kwargs={ | ||
'username': "username", | ||
'password': "password", | ||
} | ||
) | ||
|
||
Since, Lag-Aware Healthcheck only available for Redis Software and Redis Cloud | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
it's not in the list of the default healthchecks for `MultiDBClient`. You have | ||
to provide it manually during client configuration or in runtime. | ||
|
||
.. code:: python | ||
|
||
// Configuration option | ||
config = MultiDbConfig( | ||
databases_config=[database1_config, database2_config], | ||
health_checks=[ | ||
LagAwareHealthCheck(auth_basic=('username','password'), verify_tls=False) | ||
] | ||
) | ||
|
||
client = MultiDBClient(config) | ||
|
||
.. code:: python | ||
|
||
// In runtime | ||
client = MultiDBClient(config) | ||
client.add_health_check( | ||
LagAwareHealthCheck(auth_basic=('username','password'), verify_tls=False) | ||
) | ||
|
||
As mentioned we utilise REST API endpoint for Lag-Aware healthchecks, so it accepts | ||
different type of HTTP-related configuration: authentication credentials, request | ||
timeout, TLS related configuration, etc. (check `LagAwareHealthCheck` class). | ||
|
||
You can also specify `lag_aware_tolerance` parameter to specify the tolerance in MS | ||
of lag between databases that your application could tolerate. | ||
|
||
.. code:: python | ||
|
||
LagAwareHealthCheck( | ||
rest_api_port=9443, | ||
auth_basic=('username','password'), | ||
lag_aware_tolerance=150, | ||
verify_tls=True, | ||
ca_file="path/to/file" | ||
) | ||
|
||
|
||
Failure Detector | ||
---------------- | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Unlike healthcheck, `Failure Detector` verifies database healthiness based on organic | ||
trafic, so the default one reacts to any command failures within a sliding window of | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
seconds and mark database as unhealthy if threshold has been exceeded. You can extend | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
a list of failure detectors providing your own implementation, configuration defined | ||
in `MultiDBConfig` class. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
Databases configuration | ||
----------------------- | ||
|
||
You have to provide a configuration for each database in setup separately, using | ||
`DatabaseConfig` class per database. As mentioned, there's an undelying instance | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
of `Redis` or `RedisCluster` client for each database, so you can pass all the | ||
arguments related to them via `client_kwargs` argument. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.. code:: python | ||
|
||
database_config = DatabaseConfig( | ||
weight=1.0, | ||
client_kwargs={ | ||
'host': 'localhost', | ||
'port': 6379, | ||
'username': "username", | ||
'password': "password", | ||
} | ||
) | ||
|
||
It also supports `from_url` or `from_pool` capabilites to setup a client using | ||
Redis URL or custom `ConnectionPool` object. | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.. code:: python | ||
|
||
database_config1 = DatabaseConfig( | ||
weight=1.0, | ||
from_url="redis://host1:port1", | ||
client_kwargs={ | ||
'username': "username", | ||
'password': "password", | ||
} | ||
) | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
database_config2 = DatabaseConfig( | ||
weight=0.9, | ||
from_pool=connection_pool, | ||
) | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The only exception from `client_kwargs` is the retry configuration. We do not allow | ||
to pass underlying `Retry` object to avoid nesting retries. All the retries are | ||
controlled by top-level `Retry` object that you can setup via `command_retry` | ||
argument (check `MultiDBConfig`) | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
Pipeline | ||
-------- | ||
|
||
`MultiDBClient` supports pipeline mode with guaranteed pipeline retry in case | ||
of failover. Unlike, the `Redis` and `RedisCluster` clients you cannot | ||
execute transactions via pipeline mode, only via `transaction` method | ||
on `MultiDBClient`. This was done for better retries handling in case | ||
of failover. | ||
|
||
The overall interface for pipeline execution is the same, you can | ||
pipeline commands using chaining calls or context manager. | ||
|
||
.. code:: python | ||
|
||
// Chaining | ||
client = MultiDBClient(config) | ||
pipe = client.pipeline() | ||
pipe.set('key1', 'value1') | ||
pipe.get('key1') | ||
pipe.execute() // ['OK', 'value1'] | ||
|
||
// Context manager | ||
client = MultiDBClient(config) | ||
with client.pipeline() as pipe: | ||
pipe.set('key1', 'value1') | ||
pipe.get('key1') | ||
pipe.execute() // ['OK', 'value1'] | ||
|
||
|
||
Transaction | ||
----------- | ||
|
||
`MultiDBClient` supports transaction execution via `transaction()` method | ||
with guaranteed transaction retry in case of failover. Like any other | ||
client it accepts a callback with underlying `Pipeline` object to build | ||
your transaction for atomic execution | ||
|
||
CAS behaviour supported as well, so you can provide a list of keys to track. | ||
|
||
.. code:: python | ||
|
||
client = MultiDBClient(config) | ||
|
||
def callback(pipe: Pipeline): | ||
pipe.set('key1', 'value1') | ||
pipe.get('key1') | ||
|
||
client.transaction(callback, 'key1') // ['OK1', 'value1'] | ||
|
||
|
||
Pub/Sub | ||
------- | ||
|
||
`MultiDBClient` supports Pub/Sub mode with guaranteed re-subscription | ||
to the same channels in case of failover. So the expectation is that | ||
both publisher and subscriber are using `MultiDBClient` instance to | ||
provide seamless experience in terms of failover. | ||
|
||
1. Subscriber failover to another database and re-subscribe to the same | ||
channels. | ||
|
||
2. Publisher failover to another database and starts publishing | ||
messages to the same channels. | ||
|
||
However, it's still possible to lose messages if order of failover | ||
will be reversed. | ||
|
||
Like the other clients, there's two main methods to consume messages: | ||
in the main thread and in the separate thread | ||
|
||
.. code:: python | ||
|
||
client = MultiDBClient(config) | ||
p = client.pubsub() | ||
|
||
// In the main thread | ||
while True: | ||
message = p.get_message() | ||
if message: | ||
// do something with the message | ||
time.sleep(0.001) | ||
|
||
|
||
.. code:: python | ||
|
||
// In separate thread | ||
client = MultiDBClient(config) | ||
p = client.pubsub() | ||
messages_count = 0 | ||
data = json.dumps({'message': 'test'}) | ||
|
||
def handler(message): | ||
nonlocal messages_count | ||
messages_count += 1 | ||
|
||
// Assign a handler and run in a separate thread. | ||
p.subscribe(**{'test-channel': handler}) | ||
pubsub_thread = pubsub.run_in_thread(sleep_time=0.1, daemon=True) | ||
|
||
for _ in range(10): | ||
client.publish('test-channel', data) | ||
sleep(0.1) | ||
|
||
|
||
OSS Cluster API support | ||
----------------------- | ||
|
||
As mentioned `MultiDBClient` also supports integration with OSS Cluster API | ||
databases. If you're instantiating client using Redis URL, the only change | ||
you need comparing to standalone client is the `client_class` argument. | ||
DNS server will resolve given URL and will point you to one of the node that | ||
vladvildanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
could be used to discover overall cluster topology. | ||
|
||
.. code:: python | ||
|
||
config = MultiDbConfig( | ||
client_class=RedisCluster, | ||
databases_config=[database1_config, database2_config], | ||
) | ||
|
||
If you would like to specify the exact node to use for topology | ||
discovery, you can specify it the same way `RedisCluster` does | ||
|
||
.. code:: python | ||
|
||
// Expected active database (highest weight) | ||
database1_config = DatabaseConfig( | ||
weight=1.0, | ||
client_kwargs={ | ||
'username': "username", | ||
'password': "password", | ||
'startup_nodes': [ClusterNode('host1', 'port1')], | ||
} | ||
) | ||
|
||
// Passive database (stand-by replica) | ||
database2_config = DatabaseConfig( | ||
weight=0.9, | ||
client_kwargs={ | ||
'username': "username", | ||
'password': "password", | ||
'startup_nodes': [ClusterNode('host2', 'port2')], | ||
} | ||
) | ||
|
||
config = MultiDbConfig( | ||
client_class=RedisCluster, | ||
databases_config=[database1_config, database2_config], | ||
) | ||
|
||
Sharded Pub/Sub | ||
~~~~~~~~~~~~~~~ | ||
|
||
If you would like to use a Sharded Pub/Sub capabilities make sure to use | ||
correct Pub/Sub configuration. | ||
|
||
.. code:: python | ||
|
||
client = MultiDBClient(config) | ||
p = client.pubsub() | ||
|
||
// In the main thread | ||
while True: | ||
// Reads messaage from sharded channels. | ||
message = p.get_sharded_message() | ||
if message: | ||
// do something with the message | ||
time.sleep(0.001) | ||
|
||
|
||
.. code:: python | ||
|
||
// In separate thread | ||
client = MultiDBClient(config) | ||
p = client.pubsub() | ||
messages_count = 0 | ||
data = json.dumps({'message': 'test'}) | ||
|
||
def handler(message): | ||
nonlocal messages_count | ||
messages_count += 1 | ||
|
||
// Assign a handler and run in a separate thread. | ||
p.ssubscribe(**{'test-channel': handler}) | ||
|
||
// Proactively executes get_sharded_pubsub() method | ||
pubsub_thread = pubsub.run_in_thread(sleep_time=0.1, daemon=True, sharded_pubsub=True) | ||
|
||
for _ in range(10): | ||
client.spublish('test-channel', data) | ||
sleep(0.1) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.