Scale-out storage: PostgreSQL and MySQL read replicas (ST-5 to ST-15)
A data store with readReplicas becomes a replicated store. Writes, operator-written SQL and everything outside a read scope go to the primary. JMAP reads before a request's first write, IMAP LIST, STATUS, SEARCH, SORT and FETCH, POP3 RETR and TOP, DAV GET, PROPFIND and REPORT, and blob downloads run in a read scope. Only account data (properties, indexes, change logs, counters, ACLs, blobs, the search index) is read from a replica; the registry, in-memory values, the task queue and the rest stay on the primary. In a scope, the first read picks a replica round-robin among those up and under the lag limit, and only if it has every change this node has written or heard of for the scope's accounts: marks come from write results, the cluster's state-change broadcasts, a sinceState the client presents, and, with more than one node, Redis. A write inside the scope sends the rest of it to the primary. A miss on a replica is looked up on the primary, and a replica error retries the read there and marks the replica down. Each node samples lag every second (WAL positions on PostgreSQL; GTID sets or Seconds_Behind_Source on MySQL), stops reading from a replica over 5 s and starts again under 2.5 s, and probes a down replica every 10 s. At startup a replica is left out if it's the primary, isn't read-only, applies out of commit order, or doesn't show a marker written to the primary within six tries. replica_tests (postgres, STORE=PostgreSqlReplicated) runs a primary and a streaming hot standby in containers: tests 9, 10, 12, 13, 14 and 15 pass.
This commit is contained in:
@@ -201,6 +201,25 @@ impl SearchStore {
|
||||
filters: &[SearchFilter],
|
||||
sort: &[SearchComparator],
|
||||
) -> trc::Result<Vec<u32>> {
|
||||
// inbuxa: ST-6, ST-12: a replica answers search queries in a read scope
|
||||
if let SearchStore::Store(Store::Replicated(store)) = self {
|
||||
return match store.read_target(crate::SUBSPACE_SEARCH_INDEX).await {
|
||||
Some(replica) => match crate::sql_backend!(
|
||||
&store.replicas[replica].store,
|
||||
db => db.query(index, filters, sort).await
|
||||
) {
|
||||
Ok(ids) => {
|
||||
store.served(replica);
|
||||
Ok(ids)
|
||||
}
|
||||
Err(err) => {
|
||||
store.failed(replica, err);
|
||||
crate::sql_backend!(&store.primary, db => db.query(index, filters, sort).await)
|
||||
}
|
||||
},
|
||||
None => crate::sql_backend!(&store.primary, db => db.query(index, filters, sort).await),
|
||||
};
|
||||
}
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
@@ -215,6 +234,13 @@ impl SearchStore {
|
||||
}
|
||||
|
||||
pub async fn query_global(&self, query: SearchQuery) -> trc::Result<Vec<u64>> {
|
||||
// inbuxa: ST-5: global queries are maintenance, on the primary
|
||||
if let SearchStore::Store(Store::Replicated(store)) = self {
|
||||
return crate::sql_backend!(
|
||||
&store.primary,
|
||||
db => db.query(query.index, &query.filters, &query.comparators).await
|
||||
);
|
||||
}
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
@@ -245,6 +271,9 @@ impl SearchStore {
|
||||
}
|
||||
|
||||
pub async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
if let SearchStore::Store(Store::Replicated(store)) = self {
|
||||
return crate::sql_backend!(&store.primary, db => db.index(documents).await);
|
||||
}
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
@@ -259,6 +288,9 @@ impl SearchStore {
|
||||
}
|
||||
|
||||
pub async fn unindex(&self, query: SearchQuery) -> trc::Result<u64> {
|
||||
if let SearchStore::Store(Store::Replicated(store)) = self {
|
||||
return crate::sql_backend!(&store.primary, db => db.unindex(query).await);
|
||||
}
|
||||
match self {
|
||||
SearchStore::Store(store) => match store {
|
||||
#[cfg(feature = "postgres")]
|
||||
@@ -279,6 +311,8 @@ impl SearchStore {
|
||||
Store::PostgreSQL(_) => None,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(_) => None,
|
||||
// inbuxa: ST-3: as its primary
|
||||
Store::Replicated(replicated) if replicated.primary.is_pg_or_mysql() => None,
|
||||
store => Some(store),
|
||||
},
|
||||
_ => None,
|
||||
@@ -289,6 +323,10 @@ impl SearchStore {
|
||||
match self {
|
||||
#[cfg(feature = "mysql")]
|
||||
SearchStore::Store(Store::MySQL(_)) => true,
|
||||
#[cfg(feature = "mysql")]
|
||||
SearchStore::Store(Store::Replicated(store)) => {
|
||||
matches!(store.primary, Store::MySQL(_))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -297,6 +335,10 @@ impl SearchStore {
|
||||
match self {
|
||||
#[cfg(feature = "postgres")]
|
||||
SearchStore::Store(Store::PostgreSQL(_)) => true,
|
||||
#[cfg(feature = "postgres")]
|
||||
SearchStore::Store(Store::Replicated(store)) => {
|
||||
matches!(store.primary, Store::PostgreSQL(_))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -316,6 +358,9 @@ impl SearchStore {
|
||||
Store::PostgreSQL(store) => store.create_search_tables().await,
|
||||
#[cfg(feature = "mysql")]
|
||||
Store::MySQL(store) => store.create_search_tables().await,
|
||||
Store::Replicated(store) => {
|
||||
crate::sql_backend!(&store.primary, db => db.create_search_tables().await)
|
||||
}
|
||||
_ => Ok(()),
|
||||
},
|
||||
SearchStore::ElasticSearch(store) => store.create_indexes().await,
|
||||
|
||||
Reference in New Issue
Block a user