2
0
mirror of https://github.com/openvswitch/ovs synced 2025-08-22 01:51:26 +00:00
ovs/ovsdb/storage.c

676 lines
20 KiB
C
Raw Normal View History

/* Copyright (c) 2009, 2010, 2011, 2016, 2017 Nicira, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this storage except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <config.h>
#include "storage.h"
#include <string.h>
#include "log.h"
#include "ovsdb-error.h"
#include "openvswitch/json.h"
#include "openvswitch/poll-loop.h"
#include "openvswitch/vlog.h"
#include "ovsdb.h"
#include "raft.h"
#include "random.h"
#include "simap.h"
#include "timeval.h"
#include "util.h"
VLOG_DEFINE_THIS_MODULE(storage);
struct ovsdb_storage {
/* There are three kinds of storage:
*
* - Standalone, backed by a disk file. 'log' is nonnull, 'raft' is
* null.
*
* - Clustered, backed by a Raft cluster. 'log' is null, 'raft' is
* nonnull.
*
* - Memory only, unbacked. 'log' and 'raft' are null. */
struct ovsdb_log *log;
struct raft *raft;
char *unbacked_name; /* Name of the unbacked storage. */
/* All kinds of storage. */
struct ovsdb_error *error; /* If nonnull, a permanent error. */
long long next_snapshot_min; /* Earliest time to take next snapshot. */
long long next_snapshot_max; /* Latest time to take next snapshot. */
/* Standalone only. */
unsigned int n_read;
unsigned int n_written;
};
static void schedule_next_snapshot(struct ovsdb_storage *, bool quick);
static struct ovsdb_error * OVS_WARN_UNUSED_RESULT
ovsdb_storage_open__(const char *filename, bool rw, bool allow_clustered,
struct ovsdb_storage **storagep)
{
*storagep = NULL;
struct ovsdb_log *log;
struct ovsdb_error *error;
error = ovsdb_log_open(filename, OVSDB_MAGIC"|"RAFT_MAGIC,
rw ? OVSDB_LOG_READ_WRITE : OVSDB_LOG_READ_ONLY,
-1, &log);
if (error) {
return error;
}
struct raft *raft = NULL;
if (!strcmp(ovsdb_log_get_magic(log), RAFT_MAGIC)) {
if (!allow_clustered) {
ovsdb_log_close(log);
return ovsdb_error(NULL, "%s: cannot apply this operation to "
"clustered database file", filename);
}
error = raft_open(log, &raft);
log = NULL;
if (error) {
return error;
}
}
struct ovsdb_storage *storage = xzalloc(sizeof *storage);
storage->log = log;
storage->raft = raft;
schedule_next_snapshot(storage, false);
*storagep = storage;
return NULL;
}
/* Opens 'filename' for use as storage. If 'rw', opens it for read/write
* access, otherwise read-only. If successful, stores the new storage in
* '*storagep' and returns NULL; on failure, stores NULL in '*storagep' and
* returns the error.
*
* The returned storage might be clustered or standalone, depending on what the
* disk file contains. */
struct ovsdb_error * OVS_WARN_UNUSED_RESULT
ovsdb_storage_open(const char *filename, bool rw,
struct ovsdb_storage **storagep)
{
return ovsdb_storage_open__(filename, rw, true, storagep);
}
struct ovsdb_storage *
ovsdb_storage_open_standalone(const char *filename, bool rw)
{
struct ovsdb_storage *storage;
struct ovsdb_error *error = ovsdb_storage_open__(filename, rw, false,
&storage);
if (error) {
ovs_fatal(0, "%s", ovsdb_error_to_string_free(error));
}
return storage;
}
/* Creates and returns new storage without any backing. Nothing will be read
* from the storage, and writes are discarded. If 'name' is nonnull, it will
* be used as a storage name. */
struct ovsdb_storage *
ovsdb_storage_create_unbacked(const char *name)
{
struct ovsdb_storage *storage = xzalloc(sizeof *storage);
schedule_next_snapshot(storage, false);
storage->unbacked_name = nullable_xstrdup(name);
return storage;
}
void
ovsdb_storage_close(struct ovsdb_storage *storage)
{
if (storage) {
ovsdb_log_close(storage->log);
raft_close(storage->raft);
ovsdb_error_destroy(storage->error);
free(storage->unbacked_name);
free(storage);
}
}
const char *
ovsdb_storage_get_model(const struct ovsdb_storage *storage)
{
return storage->raft ? "clustered" : "standalone";
}
bool
ovsdb_storage_is_clustered(const struct ovsdb_storage *storage)
{
return storage->raft != NULL;
}
bool
ovsdb_storage_is_connected(const struct ovsdb_storage *storage)
{
return !storage->raft || raft_is_connected(storage->raft);
}
bool
ovsdb_storage_is_dead(const struct ovsdb_storage *storage)
{
return storage->raft && raft_left(storage->raft);
}
bool
ovsdb_storage_is_leader(const struct ovsdb_storage *storage)
{
return !storage->raft || raft_is_leader(storage->raft);
}
const struct uuid *
ovsdb_storage_get_cid(const struct ovsdb_storage *storage)
{
return storage->raft ? raft_get_cid(storage->raft) : NULL;
}
const struct uuid *
ovsdb_storage_get_sid(const struct ovsdb_storage *storage)
{
return storage->raft ? raft_get_sid(storage->raft) : NULL;
}
uint64_t
ovsdb_storage_get_applied_index(const struct ovsdb_storage *storage)
{
return storage->raft ? raft_get_applied_index(storage->raft) : 0;
}
void
ovsdb_storage_get_memory_usage(const struct ovsdb_storage *storage,
struct simap *usage)
{
if (storage->raft) {
raft_get_memory_usage(storage->raft, usage);
}
}
char *
ovsdb_storage_get_error(const struct ovsdb_storage *storage)
{
if (storage->error) {
return ovsdb_error_to_string(storage->error);
}
return NULL;
}
void
ovsdb_storage_run(struct ovsdb_storage *storage)
{
if (storage->raft) {
raft_run(storage->raft);
}
}
void
ovsdb_storage_wait(struct ovsdb_storage *storage)
{
if (storage->raft) {
raft_wait(storage->raft);
}
}
/* Returns 'storage''s embedded name, if it has one, otherwise null.
*
* Only clustered storage has a built-in name. */
const char *
ovsdb_storage_get_name(const struct ovsdb_storage *storage)
{
return storage->unbacked_name ? storage->unbacked_name
: storage->raft ? raft_get_name(storage->raft)
: NULL;
}
/* Attempts to read a log record from 'storage'.
*
* If successful, returns NULL and stores the transaction information in
* '*schemap', '*txnp', and '*txnid'. At least one of these will be nonnull.
* The caller owns the data and must eventually free it (with json_destroy()).
*
* If 'storage' is not clustered, 'txnid' may be null.
*
* If a read error occurs, returns the error and stores NULL in '*jsonp'.
*
* If the read reaches end of file, returns NULL and stores NULL in
* '*jsonp'. */
struct ovsdb_error * OVS_WARN_UNUSED_RESULT
ovsdb_storage_read(struct ovsdb_storage *storage,
struct ovsdb_schema **schemap,
struct json **txnp,
struct uuid *txnid)
{
*schemap = NULL;
*txnp = NULL;
if (txnid) {
*txnid = UUID_ZERO;
}
const struct json *schema_json = NULL;
const struct json *txn_json = NULL;
struct json *json;
if (storage->raft) {
raft: Don't keep full json objects in memory if no longer needed. Raft log entries (and raft database snapshot) contains json objects of the data. Follower receives append requests with data that gets parsed and added to the raft log. Leader receives execution requests, parses data out of them and adds to the log. In both cases, later ovsdb-server reads the log with ovsdb_storage_read(), constructs transaction and updates the database. On followers these json objects in common case are never used again. Leader may use them to send append requests or snapshot installation requests to followers. However, all these operations (except for ovsdb_storage_read()) are just serializing the json in order to send it over the network. Json objects are significantly larger than their serialized string representation. For example, the snapshot of the database from one of the ovn-heater scale tests takes 270 MB as a string, but 1.6 GB as a json object from the total 3.8 GB consumed by ovsdb-server process. ovsdb_storage_read() for a given raft entry happens only once in a lifetime, so after this call, we can serialize the json object, store the string representation and free the actual json object that ovsdb will never need again. This can save a lot of memory and can also save serialization time, because each raft entry for append requests and snapshot installation requests serialized only once instead of doing that every time such request needs to be sent. JSON_SERIALIZED_OBJECT can be used in order to seamlessly integrate pre-serialized data into raft_header and similar json objects. One major special case is creation of a database snapshot. Snapshot installation request received over the network will be parsed and read by ovsdb-server just like any other raft log entry. However, snapshots created locally with raft_store_snapshot() will never be read back, because they reflect the current state of the database, hence already applied. For this case we can free the json object right after writing snapshot on disk. Tests performed with ovn-heater on 60 node density-light scenario, where on-disk database goes up to 97 MB, shows average memory consumption of ovsdb-server Southbound DB processes decreased by 58% (from 602 MB to 256 MB per process) and peak memory consumption decreased by 40% (from 1288 MB to 771 MB). Test with 120 nodes on density-heavy scenario with 270 MB on-disk database shows 1.5 GB memory consumption decrease as expected. Also, total CPU time consumed by the Southbound DB process reduced from 296 to 256 minutes. Number of unreasonably long poll intervals reduced from 2896 down to 1934. Acked-by: Dumitru Ceara <dceara@redhat.com> Acked-by: Han Zhou <hzhou@ovn.org> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2021-08-24 21:00:38 +02:00
json = raft_next_entry(storage->raft, txnid);
if (!json) {
return NULL;
} else if (json->type != JSON_ARRAY || json_array_size(json) != 2) {
json_destroy(json);
return ovsdb_error(NULL, "invalid commit format");
}
const struct json *e0 = json_array_at(json, 0);
const struct json *e1 = json_array_at(json, 1);
schema_json = e0->type != JSON_NULL ? e0 : NULL;
txn_json = e1->type != JSON_NULL ? e1 : NULL;
} else if (storage->log) {
struct ovsdb_error *error = ovsdb_log_read(storage->log, &json);
if (error || !json) {
return error;
}
unsigned int n = storage->n_read++;
const struct json **jsonp = !n ? &schema_json : &txn_json;
*jsonp = json;
if (n == 1) {
ovsdb_log_mark_base(storage->log);
}
} else {
/* Unbacked. Nothing to do. */
return NULL;
}
/* If we got this far then we must have at least a schema or a
* transaction. */
ovs_assert(schema_json || txn_json);
if (schema_json) {
struct ovsdb_schema *schema;
struct ovsdb_error *error = ovsdb_schema_from_json(schema_json,
&schema);
if (error) {
json_destroy(json);
return error;
}
const char *storage_name = ovsdb_storage_get_name(storage);
const char *schema_name = schema->name;
if (storage_name && strcmp(storage_name, schema_name)) {
error = ovsdb_error(NULL, "name %s in header does not match "
"name %s in schema",
storage_name, schema_name);
json_destroy(json);
ovsdb_schema_destroy(schema);
return error;
}
*schemap = schema;
}
if (txn_json) {
*txnp = json_clone(txn_json);
}
json_destroy(json);
return NULL;
}
/* Reads and returns the schema from standalone storage 'storage'. Terminates
* with an error on failure. */
struct ovsdb_schema *
ovsdb_storage_read_schema(struct ovsdb_storage *storage)
{
ovs_assert(storage->log);
struct json *txn_json;
struct ovsdb_schema *schema;
struct ovsdb_error *error = ovsdb_storage_read(storage, &schema,
&txn_json, NULL);
if (error) {
ovs_fatal(0, "%s", ovsdb_error_to_string_free(error));
}
if (!schema && !txn_json) {
ovs_fatal(0, "unexpected end of file reading schema");
}
ovs_assert(schema && !txn_json);
return schema;
}
bool
ovsdb_storage_read_wait(struct ovsdb_storage *storage)
{
return (storage->raft
? raft_has_next_entry(storage->raft)
: false);
}
void
ovsdb_storage_unread(struct ovsdb_storage *storage)
{
if (storage->error) {
return;
}
if (storage->raft) {
if (!storage->error) {
storage->error = ovsdb_error(NULL, "inconsistent data");
}
} else if (storage->log) {
ovsdb_log_unread(storage->log);
}
}
struct ovsdb_write {
struct ovsdb_error *error;
struct raft_command *command;
};
/* Not suitable for writing transactions that change the schema. */
struct ovsdb_write * OVS_WARN_UNUSED_RESULT
ovsdb_storage_write(struct ovsdb_storage *storage, const struct json *data,
const struct uuid *prereq, struct uuid *resultp,
bool durable)
{
struct ovsdb_write *w = xzalloc(sizeof *w);
struct uuid result = UUID_ZERO;
if (storage->error) {
w->error = ovsdb_error_clone(storage->error);
} else if (storage->raft) {
struct json *txn_json = json_array_create_2(json_null_create(),
json_clone(data));
w->command = raft_command_execute(storage->raft, txn_json,
prereq, &result);
json_destroy(txn_json);
} else if (storage->log) {
w->error = ovsdb_log_write(storage->log, data);
if (!w->error) {
storage->n_written++;
if (durable) {
w->error = ovsdb_log_commit_block(storage->log);
}
}
} else {
/* When 'error' and 'command' are both null, it indicates that the
* command is complete. This is fine since this unbacked storage drops
* writes. */
}
if (resultp) {
*resultp = result;
}
return w;
}
/* Not suitable for writing transactions that change the schema. */
struct ovsdb_error * OVS_WARN_UNUSED_RESULT
ovsdb_storage_write_block(struct ovsdb_storage *storage,
const struct json *data, const struct uuid *prereq,
struct uuid *resultp, bool durable)
{
struct ovsdb_write *w = ovsdb_storage_write(storage, data,
prereq, resultp, durable);
while (!ovsdb_write_is_complete(w)) {
if (storage->raft) {
raft_run(storage->raft);
}
ovsdb_write_wait(w);
if (storage->raft) {
raft_wait(storage->raft);
}
poll_block();
}
struct ovsdb_error *error = ovsdb_error_clone(ovsdb_write_get_error(w));
ovsdb_write_destroy(w);
return error;
}
bool
ovsdb_write_is_complete(const struct ovsdb_write *w)
{
return (w->error
|| !w->command
|| raft_command_get_status(w->command) != RAFT_CMD_INCOMPLETE);
}
const struct ovsdb_error *
ovsdb_write_get_error(const struct ovsdb_write *w_)
{
struct ovsdb_write *w = CONST_CAST(struct ovsdb_write *, w_);
ovs_assert(ovsdb_write_is_complete(w));
if (w->command && !w->error) {
enum raft_command_status status = raft_command_get_status(w->command);
if (status != RAFT_CMD_SUCCESS) {
w->error = ovsdb_error("cluster error", "%s",
raft_command_status_to_string(status));
}
}
return w->error;
}
uint64_t
ovsdb_write_get_commit_index(const struct ovsdb_write *w)
{
ovs_assert(ovsdb_write_is_complete(w));
return (w->command && !w->error
? raft_command_get_commit_index(w->command)
: 0);
}
void
ovsdb_write_wait(const struct ovsdb_write *w)
{
if (ovsdb_write_is_complete(w)) {
poll_immediate_wake();
}
}
void
ovsdb_write_destroy(struct ovsdb_write *w)
{
if (w) {
raft_command_unref(w->command);
ovsdb_error_destroy(w->error);
free(w);
}
}
static void
schedule_next_snapshot(struct ovsdb_storage *storage, bool quick)
{
if (storage->log || storage->raft) {
unsigned int base = 10 * 60 * 1000; /* 10 minutes */
unsigned int range = 10 * 60 * 1000; /* 10 minutes */
if (quick) {
base /= 10;
range /= 10;
}
long long int now = time_msec();
storage->next_snapshot_min = now + base + random_range(range);
ovsdb: storage: Randomize should_snapshot checks when the minimum time passed. Snapshots are scheduled for every 10-20 minutes. It's a random value in this interval for each server. Once the time is up, but the maximum time (24 hours) not reached yet, ovsdb will start checking if the log grew a lot on every iteration. Once the growth is detected, compaction is triggered. OTOH, it's very common for an OVSDB cluster to not have the log growing very fast. If the log didn't grow 2x in 20 minutes, the randomness of the initial scheduled time is gone and all the servers are checking if they need to create snapshot on every iteration. And since all of them are part of the same cluster, their logs are growing with the same speed. Once the critical mass is reached, all the servers will start creating snapshots at the same time. If the database is big enough, that might leave the cluster unresponsive for an extended period of time (e.g. 10-15 seconds for OVN_Southbound database in a larger scale OVN deployment) until the compaction completed. Fix that by re-scheduling a quick retry if the minimal time already passed. Effectively, this will work as a randomized 1-2 min delay between checks, so the servers will not synchronize. Scheduling function updated to not change the upper limit on quick reschedules to avoid delaying the snapshot creation indefinitely. Currently quick re-schedules are only used for the error cases, and there is always a 'slow' re-schedule after the successful compaction. So, the change of a scheduling function doesn't change the current behavior much. Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Han Zhou <hzhou@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-12-13 16:43:33 +01:00
if (!quick) {
long long int one_day = 60LL * 60 * 24 * 1000;
storage->next_snapshot_max = now + one_day;
}
} else {
storage->next_snapshot_min = LLONG_MAX;
storage->next_snapshot_max = LLONG_MAX;
}
}
bool
ovsdb: storage: Randomize should_snapshot checks when the minimum time passed. Snapshots are scheduled for every 10-20 minutes. It's a random value in this interval for each server. Once the time is up, but the maximum time (24 hours) not reached yet, ovsdb will start checking if the log grew a lot on every iteration. Once the growth is detected, compaction is triggered. OTOH, it's very common for an OVSDB cluster to not have the log growing very fast. If the log didn't grow 2x in 20 minutes, the randomness of the initial scheduled time is gone and all the servers are checking if they need to create snapshot on every iteration. And since all of them are part of the same cluster, their logs are growing with the same speed. Once the critical mass is reached, all the servers will start creating snapshots at the same time. If the database is big enough, that might leave the cluster unresponsive for an extended period of time (e.g. 10-15 seconds for OVN_Southbound database in a larger scale OVN deployment) until the compaction completed. Fix that by re-scheduling a quick retry if the minimal time already passed. Effectively, this will work as a randomized 1-2 min delay between checks, so the servers will not synchronize. Scheduling function updated to not change the upper limit on quick reschedules to avoid delaying the snapshot creation indefinitely. Currently quick re-schedules are only used for the error cases, and there is always a 'slow' re-schedule after the successful compaction. So, the change of a scheduling function doesn't change the current behavior much. Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Han Zhou <hzhou@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-12-13 16:43:33 +01:00
ovsdb_storage_should_snapshot(struct ovsdb_storage *storage)
{
if (storage->raft || storage->log) {
/* If we haven't reached the minimum snapshot time, don't snapshot. */
long long int now = time_msec();
if (now < storage->next_snapshot_min) {
return false;
}
uint64_t log_len = (storage->raft
? raft_get_log_length(storage->raft)
: storage->n_read + storage->n_written);
raft: Transfer leadership before creating snapshots. With a big database writing snapshot could take a lot of time, for example, on one of the systems compaction of 300MB database takes about 10 seconds to complete. For the clustered database, 40% of this time takes conversion of the database to the file transaction json format, the rest of time is formatting a string and writing to disk. Of course, this highly depends on the disc and CPU speeds. 300MB is the very possible database size for the OVN Southbound DB, and it might be even bigger than that. During compaction the database is not available and the ovsdb-server doesn't do any other tasks. If leader spends 10-15 seconds writing a snapshot, the cluster is not functional for that time period. Leader also, likely, has some monitors to serve, so the one poll interval may be 15-20 seconds long in the end. Systems with so big databases typically has very high election timers configured (16 seconds), so followers will start election only after this significant amount of time. Once leader is back to the operational state, it will re-connect and try to join the cluster back. In some cases, this might also trigger the 'connected' state flapping on the old leader triggering a re-connection of clients. This issue has been observed with large-scale OVN deployments. One of the methods to improve the situation is to transfer leadership before compacting. This allows to keep the cluster functional, while one of the servers writes a snapshot. Additionally logging the time spent for compaction if it was longer than 1 second. This adds a bit of visibility to 'unreasonably long poll interval's. Reported-at: https://bugzilla.redhat.com/1960391 Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-05-06 14:47:31 +02:00
bool snapshot_recommended = false;
if (now < storage->next_snapshot_max) {
/* Maximum snapshot time not yet reached. Take a snapshot if there
* have been at least 100 log entries and the log file size has
* grown a lot. */
bool grew_lots = (storage->raft
? raft_grew_lots(storage->raft)
: ovsdb_log_grew_lots(storage->log));
raft: Transfer leadership before creating snapshots. With a big database writing snapshot could take a lot of time, for example, on one of the systems compaction of 300MB database takes about 10 seconds to complete. For the clustered database, 40% of this time takes conversion of the database to the file transaction json format, the rest of time is formatting a string and writing to disk. Of course, this highly depends on the disc and CPU speeds. 300MB is the very possible database size for the OVN Southbound DB, and it might be even bigger than that. During compaction the database is not available and the ovsdb-server doesn't do any other tasks. If leader spends 10-15 seconds writing a snapshot, the cluster is not functional for that time period. Leader also, likely, has some monitors to serve, so the one poll interval may be 15-20 seconds long in the end. Systems with so big databases typically has very high election timers configured (16 seconds), so followers will start election only after this significant amount of time. Once leader is back to the operational state, it will re-connect and try to join the cluster back. In some cases, this might also trigger the 'connected' state flapping on the old leader triggering a re-connection of clients. This issue has been observed with large-scale OVN deployments. One of the methods to improve the situation is to transfer leadership before compacting. This allows to keep the cluster functional, while one of the servers writes a snapshot. Additionally logging the time spent for compaction if it was longer than 1 second. This adds a bit of visibility to 'unreasonably long poll interval's. Reported-at: https://bugzilla.redhat.com/1960391 Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-05-06 14:47:31 +02:00
snapshot_recommended = (log_len >= 100 && grew_lots);
} else {
/* We have reached the maximum snapshot time. Take a snapshot if
* there have been any log entries at all. */
raft: Transfer leadership before creating snapshots. With a big database writing snapshot could take a lot of time, for example, on one of the systems compaction of 300MB database takes about 10 seconds to complete. For the clustered database, 40% of this time takes conversion of the database to the file transaction json format, the rest of time is formatting a string and writing to disk. Of course, this highly depends on the disc and CPU speeds. 300MB is the very possible database size for the OVN Southbound DB, and it might be even bigger than that. During compaction the database is not available and the ovsdb-server doesn't do any other tasks. If leader spends 10-15 seconds writing a snapshot, the cluster is not functional for that time period. Leader also, likely, has some monitors to serve, so the one poll interval may be 15-20 seconds long in the end. Systems with so big databases typically has very high election timers configured (16 seconds), so followers will start election only after this significant amount of time. Once leader is back to the operational state, it will re-connect and try to join the cluster back. In some cases, this might also trigger the 'connected' state flapping on the old leader triggering a re-connection of clients. This issue has been observed with large-scale OVN deployments. One of the methods to improve the situation is to transfer leadership before compacting. This allows to keep the cluster functional, while one of the servers writes a snapshot. Additionally logging the time spent for compaction if it was longer than 1 second. This adds a bit of visibility to 'unreasonably long poll interval's. Reported-at: https://bugzilla.redhat.com/1960391 Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-05-06 14:47:31 +02:00
snapshot_recommended = (log_len > 0);
}
raft: Transfer leadership before creating snapshots. With a big database writing snapshot could take a lot of time, for example, on one of the systems compaction of 300MB database takes about 10 seconds to complete. For the clustered database, 40% of this time takes conversion of the database to the file transaction json format, the rest of time is formatting a string and writing to disk. Of course, this highly depends on the disc and CPU speeds. 300MB is the very possible database size for the OVN Southbound DB, and it might be even bigger than that. During compaction the database is not available and the ovsdb-server doesn't do any other tasks. If leader spends 10-15 seconds writing a snapshot, the cluster is not functional for that time period. Leader also, likely, has some monitors to serve, so the one poll interval may be 15-20 seconds long in the end. Systems with so big databases typically has very high election timers configured (16 seconds), so followers will start election only after this significant amount of time. Once leader is back to the operational state, it will re-connect and try to join the cluster back. In some cases, this might also trigger the 'connected' state flapping on the old leader triggering a re-connection of clients. This issue has been observed with large-scale OVN deployments. One of the methods to improve the situation is to transfer leadership before compacting. This allows to keep the cluster functional, while one of the servers writes a snapshot. Additionally logging the time spent for compaction if it was longer than 1 second. This adds a bit of visibility to 'unreasonably long poll interval's. Reported-at: https://bugzilla.redhat.com/1960391 Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-05-06 14:47:31 +02:00
if (!snapshot_recommended) {
ovsdb: storage: Randomize should_snapshot checks when the minimum time passed. Snapshots are scheduled for every 10-20 minutes. It's a random value in this interval for each server. Once the time is up, but the maximum time (24 hours) not reached yet, ovsdb will start checking if the log grew a lot on every iteration. Once the growth is detected, compaction is triggered. OTOH, it's very common for an OVSDB cluster to not have the log growing very fast. If the log didn't grow 2x in 20 minutes, the randomness of the initial scheduled time is gone and all the servers are checking if they need to create snapshot on every iteration. And since all of them are part of the same cluster, their logs are growing with the same speed. Once the critical mass is reached, all the servers will start creating snapshots at the same time. If the database is big enough, that might leave the cluster unresponsive for an extended period of time (e.g. 10-15 seconds for OVN_Southbound database in a larger scale OVN deployment) until the compaction completed. Fix that by re-scheduling a quick retry if the minimal time already passed. Effectively, this will work as a randomized 1-2 min delay between checks, so the servers will not synchronize. Scheduling function updated to not change the upper limit on quick reschedules to avoid delaying the snapshot creation indefinitely. Currently quick re-schedules are only used for the error cases, and there is always a 'slow' re-schedule after the successful compaction. So, the change of a scheduling function doesn't change the current behavior much. Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Han Zhou <hzhou@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-12-13 16:43:33 +01:00
if (storage->raft) {
/* Re-scheduling with a quick retry in order to avoid condition
* where all the raft servers passed the minimal time already,
* but the log didn't grow a lot, so they are all checking on
* every iteration. This will randomize the time of the next
* attempt, so all the servers will not start snapshotting at
* the same time when the log reaches a critical size. */
schedule_next_snapshot(storage, true);
}
raft: Transfer leadership before creating snapshots. With a big database writing snapshot could take a lot of time, for example, on one of the systems compaction of 300MB database takes about 10 seconds to complete. For the clustered database, 40% of this time takes conversion of the database to the file transaction json format, the rest of time is formatting a string and writing to disk. Of course, this highly depends on the disc and CPU speeds. 300MB is the very possible database size for the OVN Southbound DB, and it might be even bigger than that. During compaction the database is not available and the ovsdb-server doesn't do any other tasks. If leader spends 10-15 seconds writing a snapshot, the cluster is not functional for that time period. Leader also, likely, has some monitors to serve, so the one poll interval may be 15-20 seconds long in the end. Systems with so big databases typically has very high election timers configured (16 seconds), so followers will start election only after this significant amount of time. Once leader is back to the operational state, it will re-connect and try to join the cluster back. In some cases, this might also trigger the 'connected' state flapping on the old leader triggering a re-connection of clients. This issue has been observed with large-scale OVN deployments. One of the methods to improve the situation is to transfer leadership before compacting. This allows to keep the cluster functional, while one of the servers writes a snapshot. Additionally logging the time spent for compaction if it was longer than 1 second. This adds a bit of visibility to 'unreasonably long poll interval's. Reported-at: https://bugzilla.redhat.com/1960391 Signed-off-by: Ilya Maximets <i.maximets@ovn.org> Acked-by: Dumitru Ceara <dceara@redhat.com>
2021-05-06 14:47:31 +02:00
return false;
}
/* If we can't snapshot right now, don't. */
if (storage->raft && !raft_may_snapshot(storage->raft)) {
/* Notifying the storage that it needs to make a snapshot soon. */
raft_notify_snapshot_recommended(storage->raft);
return false;
}
return true;
}
return false;
}
static struct ovsdb_error * OVS_WARN_UNUSED_RESULT
ovsdb_storage_store_snapshot__(struct ovsdb_storage *storage,
const struct json *schema,
ovsdb: Prepare snapshot JSON in a separate thread. Conversion of the database data into JSON object, serialization and destruction of that object are the most heavy operations during the database compaction. If these operations are moved to a separate thread, the main thread can continue processing database requests in the meantime. With this change, the compaction is split in 3 phases: 1. Initialization: - Create a copy of the database. - Remember current database index. - Start a separate thread to convert a copy of the database into serialized JSON object. 2. Wait: - Continue normal operation until compaction thread is done. - Meanwhile, compaction thread: * Convert database copy to JSON. * Serialize resulted JSON. * Destroy original JSON object. 3. Finish: - Destroy the database copy. - Take the snapshot created by the thread. - Write on disk. The key for this schema to be fast is the ability to create a shallow copy of the database. This doesn't take too much time allowing the thread to do most of work. Database copy is created and destroyed only by the main thread, so there is no need for synchronization. Such solution allows to reduce the time main thread is blocked by compaction by 80-90%. For example, in ovn-heater tests with 120 node density-heavy scenario, where compaction normally takes 5-6 seconds at the end of a test, measured compaction times was all below 1 second with the change applied. Also, note that these measured times are the sum of phases 1 and 3, so actual poll intervals are about half a second in this case. Only implemented for raft storage for now. The implementation for standalone databases can be added later by using a file offset as a database index and copying newly added changes from the old file to a new one during ovsdb_log_replace(). Reported-at: https://bugzilla.redhat.com/2069108 Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2022-07-01 01:34:07 +02:00
const struct json *data, uint64_t index)
{
if (storage->raft) {
struct json *entries = json_array_create_empty();
if (schema) {
json_array_add(entries, json_clone(schema));
}
if (data) {
json_array_add(entries, json_clone(data));
}
struct ovsdb_error *error = raft_store_snapshot(storage->raft,
ovsdb: Prepare snapshot JSON in a separate thread. Conversion of the database data into JSON object, serialization and destruction of that object are the most heavy operations during the database compaction. If these operations are moved to a separate thread, the main thread can continue processing database requests in the meantime. With this change, the compaction is split in 3 phases: 1. Initialization: - Create a copy of the database. - Remember current database index. - Start a separate thread to convert a copy of the database into serialized JSON object. 2. Wait: - Continue normal operation until compaction thread is done. - Meanwhile, compaction thread: * Convert database copy to JSON. * Serialize resulted JSON. * Destroy original JSON object. 3. Finish: - Destroy the database copy. - Take the snapshot created by the thread. - Write on disk. The key for this schema to be fast is the ability to create a shallow copy of the database. This doesn't take too much time allowing the thread to do most of work. Database copy is created and destroyed only by the main thread, so there is no need for synchronization. Such solution allows to reduce the time main thread is blocked by compaction by 80-90%. For example, in ovn-heater tests with 120 node density-heavy scenario, where compaction normally takes 5-6 seconds at the end of a test, measured compaction times was all below 1 second with the change applied. Also, note that these measured times are the sum of phases 1 and 3, so actual poll intervals are about half a second in this case. Only implemented for raft storage for now. The implementation for standalone databases can be added later by using a file offset as a database index and copying newly added changes from the old file to a new one during ovsdb_log_replace(). Reported-at: https://bugzilla.redhat.com/2069108 Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2022-07-01 01:34:07 +02:00
entries, index);
json_destroy(entries);
return error;
} else if (storage->log) {
struct json *entries[2];
size_t n = 0;
if (schema) {
entries[n++] = CONST_CAST(struct json *, schema);
}
if (data) {
entries[n++] = CONST_CAST(struct json *, data);
}
return ovsdb_log_replace(storage->log, entries, n);
} else {
return NULL;
}
}
/* 'schema' and 'data' should faithfully represent the current schema and data,
* otherwise the two storing backing formats will yield divergent results. Use
* ovsdb_storage_write_schema_change() to change the schema. */
struct ovsdb_error * OVS_WARN_UNUSED_RESULT
ovsdb_storage_store_snapshot(struct ovsdb_storage *storage,
const struct json *schema,
ovsdb: Prepare snapshot JSON in a separate thread. Conversion of the database data into JSON object, serialization and destruction of that object are the most heavy operations during the database compaction. If these operations are moved to a separate thread, the main thread can continue processing database requests in the meantime. With this change, the compaction is split in 3 phases: 1. Initialization: - Create a copy of the database. - Remember current database index. - Start a separate thread to convert a copy of the database into serialized JSON object. 2. Wait: - Continue normal operation until compaction thread is done. - Meanwhile, compaction thread: * Convert database copy to JSON. * Serialize resulted JSON. * Destroy original JSON object. 3. Finish: - Destroy the database copy. - Take the snapshot created by the thread. - Write on disk. The key for this schema to be fast is the ability to create a shallow copy of the database. This doesn't take too much time allowing the thread to do most of work. Database copy is created and destroyed only by the main thread, so there is no need for synchronization. Such solution allows to reduce the time main thread is blocked by compaction by 80-90%. For example, in ovn-heater tests with 120 node density-heavy scenario, where compaction normally takes 5-6 seconds at the end of a test, measured compaction times was all below 1 second with the change applied. Also, note that these measured times are the sum of phases 1 and 3, so actual poll intervals are about half a second in this case. Only implemented for raft storage for now. The implementation for standalone databases can be added later by using a file offset as a database index and copying newly added changes from the old file to a new one during ovsdb_log_replace(). Reported-at: https://bugzilla.redhat.com/2069108 Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2022-07-01 01:34:07 +02:00
const struct json *data, uint64_t index)
{
struct ovsdb_error *error = ovsdb_storage_store_snapshot__(storage,
ovsdb: Prepare snapshot JSON in a separate thread. Conversion of the database data into JSON object, serialization and destruction of that object are the most heavy operations during the database compaction. If these operations are moved to a separate thread, the main thread can continue processing database requests in the meantime. With this change, the compaction is split in 3 phases: 1. Initialization: - Create a copy of the database. - Remember current database index. - Start a separate thread to convert a copy of the database into serialized JSON object. 2. Wait: - Continue normal operation until compaction thread is done. - Meanwhile, compaction thread: * Convert database copy to JSON. * Serialize resulted JSON. * Destroy original JSON object. 3. Finish: - Destroy the database copy. - Take the snapshot created by the thread. - Write on disk. The key for this schema to be fast is the ability to create a shallow copy of the database. This doesn't take too much time allowing the thread to do most of work. Database copy is created and destroyed only by the main thread, so there is no need for synchronization. Such solution allows to reduce the time main thread is blocked by compaction by 80-90%. For example, in ovn-heater tests with 120 node density-heavy scenario, where compaction normally takes 5-6 seconds at the end of a test, measured compaction times was all below 1 second with the change applied. Also, note that these measured times are the sum of phases 1 and 3, so actual poll intervals are about half a second in this case. Only implemented for raft storage for now. The implementation for standalone databases can be added later by using a file offset as a database index and copying newly added changes from the old file to a new one during ovsdb_log_replace(). Reported-at: https://bugzilla.redhat.com/2069108 Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2022-07-01 01:34:07 +02:00
schema, data,
index);
bool retry_quickly = error != NULL;
schedule_next_snapshot(storage, retry_quickly);
return error;
}
struct ovsdb_write * OVS_WARN_UNUSED_RESULT
ovsdb_storage_write_schema_change(struct ovsdb_storage *storage,
const struct ovsdb_schema *schema,
const struct json *data,
const struct uuid *prereq,
struct uuid *resultp)
{
struct ovsdb_write *w = xzalloc(sizeof *w);
struct uuid result = UUID_ZERO;
if (storage->error) {
w->error = ovsdb_error_clone(storage->error);
} else if (storage->raft) {
/* Clustered storage doesn't support ephemeral columns. */
w->error = ovsdb_schema_check_for_ephemeral_columns(schema);
if (!w->error) {
struct json *schema_json, *txn_json;
schema_json = ovsdb_schema_to_json(schema);
txn_json = json_array_create_2(schema_json, json_clone(data));
w->command = raft_command_execute(storage->raft, txn_json,
prereq, &result);
json_destroy(txn_json);
}
} else if (storage->log) {
struct json *schema_json = ovsdb_schema_to_json(schema);
w->error = ovsdb_storage_store_snapshot__(storage, schema_json,
data, 0);
json_destroy(schema_json);
} else {
/* When 'error' and 'command' are both null, it indicates that the
* command is complete. This is fine since this unbacked storage drops
* writes. */
}
if (resultp) {
*resultp = result;
}
return w;
}
ovsdb raft: Precheck prereq before proposing commit. In current OVSDB Raft design, when there are multiple transactions pending, either from same server node or different nodes in the cluster, only the first one can be successful at once, and following ones will fail at the prerequisite check on leader node, because the first one will update the expected prerequisite eid on leader node, and the prerequisite used for proposing a commit has to be committed eid, so it is not possible for a node to use the latest prerequisite expected by the leader to propose a commit until the lastest transaction is committed by the leader and updated the committed_index on the node. Current implementation proposes the commit as soon as the transaction is requested by the client, which results in continously retry which causes high CPU load and waste. Particularly, even if all clients are using leader_only to connect to only the leader, the prereq check failure still happens a lot when a batch of transactions are pending on the leader node - the leader node proposes a batch of commits using the same committed eid as prerequisite and it updates the expected prereq as soon as the first one is in progress, but it needs time to append to followers and wait until majority replies to update the committed_index, which results in continously useless retries of the following transactions proposed by the leader itself. This patch doesn't change the design but simplely pre-checks if current eid is same as prereq, before proposing the commit, to avoid waste of CPU cycles, for both leader and followers. When clients use leader_only mode, this patch completely eliminates the prereq check failures. In scale test of OVN with 1k HVs and creating and binding 10k lports, the patch resulted in 90% CPU cost reduction on leader and >80% CPU cost reduction on followers. (The test was with leader election base time set to 10000ms, because otherwise the test couldn't complete because of the frequent leader re-election.) This is just one of the related performance problems of the prereq checking mechanism dicussed at: https://mail.openvswitch.org/pipermail/ovs-discuss/2019-February/048243.html Signed-off-by: Han Zhou <hzhou8@ebay.com> Signed-off-by: Ben Pfaff <blp@ovn.org>
2019-03-01 10:56:37 -08:00
ovsdb: raft: Don't forward more than one command to the leader. Every transaction has RAFT log prerequisites. Even if transactions are not related (because RAFT doesn't actually know what data it is handling). When leader writes a new record to a RAFT storage, it is getting appended to the log right away and changes current 'eid', i.e., changes prerequisites. The leader will not try to write new records until the current one is committed, because until then the pre-check will be failing. However, that is different for the follower. Followers do not add records to the RAFT log until the leader sends an append request back. So, if there are multiple transactions pending on a follower, it will create a command for each of them and prerequisites will be set to the same values. All these commands will be sent to the leader, but only one can succeed at a time, because accepting one command immediately changes prerequisites and all other commands become non-applicable. So, out of N commands, 1 will succeed and N - 1 will fail. The cluster failure is a transient failure, so the follower will re-process all the failed transactions and send them again. 1 will succeed and N - 2 will fail. And so on, until there are no more transactions. In the end, instead of processing N transactions, the follower is performing N * (N - 1) / 2 transaction processing iterations. That is consuming a huge amount of CPU resources completely unnecessarily. Since there is no real chance for multiple transactions from the same follower to succeed, it's better to not send them in the first place. This also eliminates prerequisite mismatch messages on a leader in this particular case. In a test with 30 parallel shell threads executing 12K transactions total with separate ovsdb-client calls through the same follower there is about 60% performance improvement. The test takes ~100 seconds to complete without this change and ~40 seconds with this change applied. The new time is very close to what it takes to execute the same test through the cluster leader. The test can be found at the link below. Note: prerequisite failures on a leader are still possible, but mostly in a case of simultaneous transactions from different followers. It's a normal thing for a distributed database due to its nature. Link: https://mail.openvswitch.org/pipermail/ovs-dev/2024-June/415167.html Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2024-06-27 00:02:21 +02:00
bool
ovsdb_storage_precheck_prereq(const struct ovsdb_storage *storage,
const struct uuid *prereq)
ovsdb raft: Precheck prereq before proposing commit. In current OVSDB Raft design, when there are multiple transactions pending, either from same server node or different nodes in the cluster, only the first one can be successful at once, and following ones will fail at the prerequisite check on leader node, because the first one will update the expected prerequisite eid on leader node, and the prerequisite used for proposing a commit has to be committed eid, so it is not possible for a node to use the latest prerequisite expected by the leader to propose a commit until the lastest transaction is committed by the leader and updated the committed_index on the node. Current implementation proposes the commit as soon as the transaction is requested by the client, which results in continously retry which causes high CPU load and waste. Particularly, even if all clients are using leader_only to connect to only the leader, the prereq check failure still happens a lot when a batch of transactions are pending on the leader node - the leader node proposes a batch of commits using the same committed eid as prerequisite and it updates the expected prereq as soon as the first one is in progress, but it needs time to append to followers and wait until majority replies to update the committed_index, which results in continously useless retries of the following transactions proposed by the leader itself. This patch doesn't change the design but simplely pre-checks if current eid is same as prereq, before proposing the commit, to avoid waste of CPU cycles, for both leader and followers. When clients use leader_only mode, this patch completely eliminates the prereq check failures. In scale test of OVN with 1k HVs and creating and binding 10k lports, the patch resulted in 90% CPU cost reduction on leader and >80% CPU cost reduction on followers. (The test was with leader election base time set to 10000ms, because otherwise the test couldn't complete because of the frequent leader re-election.) This is just one of the related performance problems of the prereq checking mechanism dicussed at: https://mail.openvswitch.org/pipermail/ovs-discuss/2019-February/048243.html Signed-off-by: Han Zhou <hzhou8@ebay.com> Signed-off-by: Ben Pfaff <blp@ovn.org>
2019-03-01 10:56:37 -08:00
{
if (!storage->raft) {
ovsdb: raft: Don't forward more than one command to the leader. Every transaction has RAFT log prerequisites. Even if transactions are not related (because RAFT doesn't actually know what data it is handling). When leader writes a new record to a RAFT storage, it is getting appended to the log right away and changes current 'eid', i.e., changes prerequisites. The leader will not try to write new records until the current one is committed, because until then the pre-check will be failing. However, that is different for the follower. Followers do not add records to the RAFT log until the leader sends an append request back. So, if there are multiple transactions pending on a follower, it will create a command for each of them and prerequisites will be set to the same values. All these commands will be sent to the leader, but only one can succeed at a time, because accepting one command immediately changes prerequisites and all other commands become non-applicable. So, out of N commands, 1 will succeed and N - 1 will fail. The cluster failure is a transient failure, so the follower will re-process all the failed transactions and send them again. 1 will succeed and N - 2 will fail. And so on, until there are no more transactions. In the end, instead of processing N transactions, the follower is performing N * (N - 1) / 2 transaction processing iterations. That is consuming a huge amount of CPU resources completely unnecessarily. Since there is no real chance for multiple transactions from the same follower to succeed, it's better to not send them in the first place. This also eliminates prerequisite mismatch messages on a leader in this particular case. In a test with 30 parallel shell threads executing 12K transactions total with separate ovsdb-client calls through the same follower there is about 60% performance improvement. The test takes ~100 seconds to complete without this change and ~40 seconds with this change applied. The new time is very close to what it takes to execute the same test through the cluster leader. The test can be found at the link below. Note: prerequisite failures on a leader are still possible, but mostly in a case of simultaneous transactions from different followers. It's a normal thing for a distributed database due to its nature. Link: https://mail.openvswitch.org/pipermail/ovs-dev/2024-June/415167.html Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2024-06-27 00:02:21 +02:00
return true;
ovsdb raft: Precheck prereq before proposing commit. In current OVSDB Raft design, when there are multiple transactions pending, either from same server node or different nodes in the cluster, only the first one can be successful at once, and following ones will fail at the prerequisite check on leader node, because the first one will update the expected prerequisite eid on leader node, and the prerequisite used for proposing a commit has to be committed eid, so it is not possible for a node to use the latest prerequisite expected by the leader to propose a commit until the lastest transaction is committed by the leader and updated the committed_index on the node. Current implementation proposes the commit as soon as the transaction is requested by the client, which results in continously retry which causes high CPU load and waste. Particularly, even if all clients are using leader_only to connect to only the leader, the prereq check failure still happens a lot when a batch of transactions are pending on the leader node - the leader node proposes a batch of commits using the same committed eid as prerequisite and it updates the expected prereq as soon as the first one is in progress, but it needs time to append to followers and wait until majority replies to update the committed_index, which results in continously useless retries of the following transactions proposed by the leader itself. This patch doesn't change the design but simplely pre-checks if current eid is same as prereq, before proposing the commit, to avoid waste of CPU cycles, for both leader and followers. When clients use leader_only mode, this patch completely eliminates the prereq check failures. In scale test of OVN with 1k HVs and creating and binding 10k lports, the patch resulted in 90% CPU cost reduction on leader and >80% CPU cost reduction on followers. (The test was with leader election base time set to 10000ms, because otherwise the test couldn't complete because of the frequent leader re-election.) This is just one of the related performance problems of the prereq checking mechanism dicussed at: https://mail.openvswitch.org/pipermail/ovs-discuss/2019-February/048243.html Signed-off-by: Han Zhou <hzhou8@ebay.com> Signed-off-by: Ben Pfaff <blp@ovn.org>
2019-03-01 10:56:37 -08:00
}
ovsdb: raft: Don't forward more than one command to the leader. Every transaction has RAFT log prerequisites. Even if transactions are not related (because RAFT doesn't actually know what data it is handling). When leader writes a new record to a RAFT storage, it is getting appended to the log right away and changes current 'eid', i.e., changes prerequisites. The leader will not try to write new records until the current one is committed, because until then the pre-check will be failing. However, that is different for the follower. Followers do not add records to the RAFT log until the leader sends an append request back. So, if there are multiple transactions pending on a follower, it will create a command for each of them and prerequisites will be set to the same values. All these commands will be sent to the leader, but only one can succeed at a time, because accepting one command immediately changes prerequisites and all other commands become non-applicable. So, out of N commands, 1 will succeed and N - 1 will fail. The cluster failure is a transient failure, so the follower will re-process all the failed transactions and send them again. 1 will succeed and N - 2 will fail. And so on, until there are no more transactions. In the end, instead of processing N transactions, the follower is performing N * (N - 1) / 2 transaction processing iterations. That is consuming a huge amount of CPU resources completely unnecessarily. Since there is no real chance for multiple transactions from the same follower to succeed, it's better to not send them in the first place. This also eliminates prerequisite mismatch messages on a leader in this particular case. In a test with 30 parallel shell threads executing 12K transactions total with separate ovsdb-client calls through the same follower there is about 60% performance improvement. The test takes ~100 seconds to complete without this change and ~40 seconds with this change applied. The new time is very close to what it takes to execute the same test through the cluster leader. The test can be found at the link below. Note: prerequisite failures on a leader are still possible, but mostly in a case of simultaneous transactions from different followers. It's a normal thing for a distributed database due to its nature. Link: https://mail.openvswitch.org/pipermail/ovs-dev/2024-June/415167.html Acked-by: Dumitru Ceara <dceara@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
2024-06-27 00:02:21 +02:00
return raft_precheck_prereq(storage->raft, prereq);
ovsdb raft: Precheck prereq before proposing commit. In current OVSDB Raft design, when there are multiple transactions pending, either from same server node or different nodes in the cluster, only the first one can be successful at once, and following ones will fail at the prerequisite check on leader node, because the first one will update the expected prerequisite eid on leader node, and the prerequisite used for proposing a commit has to be committed eid, so it is not possible for a node to use the latest prerequisite expected by the leader to propose a commit until the lastest transaction is committed by the leader and updated the committed_index on the node. Current implementation proposes the commit as soon as the transaction is requested by the client, which results in continously retry which causes high CPU load and waste. Particularly, even if all clients are using leader_only to connect to only the leader, the prereq check failure still happens a lot when a batch of transactions are pending on the leader node - the leader node proposes a batch of commits using the same committed eid as prerequisite and it updates the expected prereq as soon as the first one is in progress, but it needs time to append to followers and wait until majority replies to update the committed_index, which results in continously useless retries of the following transactions proposed by the leader itself. This patch doesn't change the design but simplely pre-checks if current eid is same as prereq, before proposing the commit, to avoid waste of CPU cycles, for both leader and followers. When clients use leader_only mode, this patch completely eliminates the prereq check failures. In scale test of OVN with 1k HVs and creating and binding 10k lports, the patch resulted in 90% CPU cost reduction on leader and >80% CPU cost reduction on followers. (The test was with leader election base time set to 10000ms, because otherwise the test couldn't complete because of the frequent leader re-election.) This is just one of the related performance problems of the prereq checking mechanism dicussed at: https://mail.openvswitch.org/pipermail/ovs-discuss/2019-February/048243.html Signed-off-by: Han Zhou <hzhou8@ebay.com> Signed-off-by: Ben Pfaff <blp@ovn.org>
2019-03-01 10:56:37 -08:00
}