mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-22 21:59:59 +00:00
This will help to keep us from using deprecated Python features going forward. Signed-off-by: Tristan Partin <tristan@neon.tech>
297 lines
9.9 KiB
Python
297 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
import statistics
|
|
import threading
|
|
import time
|
|
import timeit
|
|
from collections.abc import Generator
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from fixtures.benchmark_fixture import MetricReport, NeonBenchmarker
|
|
from fixtures.common_types import Lsn
|
|
from fixtures.compare_fixtures import NeonCompare, PgCompare, VanillaCompare
|
|
from fixtures.log_helper import log
|
|
from fixtures.neon_fixtures import NeonEnvBuilder, PgBin, flush_ep_to_pageserver
|
|
|
|
from performance.test_perf_pgbench import get_durations_matrix, get_scales_matrix
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any, Callable
|
|
|
|
|
|
@pytest.fixture(params=["vanilla", "neon_off", "neon_on"])
|
|
# This fixture constructs multiple `PgCompare` interfaces using a builder pattern.
|
|
# The builder parameters are encoded in the fixture's param.
|
|
# For example, to build a `NeonCompare` interface, the corresponding fixture's param should have
|
|
# a format of `neon_{safekeepers_enable_fsync}`.
|
|
# Note that, here "_" is used to separate builder parameters.
|
|
def pg_compare(request) -> Generator[PgCompare, None, None]:
|
|
x = request.param.split("_")
|
|
|
|
if x[0] == "vanilla":
|
|
# `VanillaCompare` interface
|
|
fixture = request.getfixturevalue("vanilla_compare")
|
|
assert isinstance(fixture, VanillaCompare)
|
|
|
|
yield fixture
|
|
else:
|
|
assert (
|
|
len(x) == 2
|
|
), f"request param ({request.param}) should have a format of `neon_{{safekeepers_enable_fsync}}`"
|
|
|
|
# `NeonCompare` interface
|
|
neon_env_builder = request.getfixturevalue("neon_env_builder")
|
|
assert isinstance(neon_env_builder, NeonEnvBuilder)
|
|
|
|
zenbenchmark = request.getfixturevalue("zenbenchmark")
|
|
assert isinstance(zenbenchmark, NeonBenchmarker)
|
|
|
|
pg_bin = request.getfixturevalue("pg_bin")
|
|
assert isinstance(pg_bin, PgBin)
|
|
|
|
neon_env_builder.safekeepers_enable_fsync = x[1] == "on"
|
|
|
|
env = neon_env_builder.init_start()
|
|
|
|
cmp = NeonCompare(zenbenchmark, env, pg_bin)
|
|
|
|
yield cmp
|
|
|
|
flush_ep_to_pageserver(env, cmp._pg, cmp.tenant, cmp.timeline)
|
|
env.pageserver.http_client().timeline_checkpoint(
|
|
cmp.tenant, cmp.timeline, compact=False, wait_until_uploaded=True
|
|
)
|
|
|
|
|
|
def start_heavy_write_workload(env: PgCompare, n_tables: int, scale: int, num_iters: int):
|
|
"""Start an intensive write workload across multiple tables.
|
|
|
|
## Single table workload:
|
|
At each step, insert new `new_rows_each_update` rows.
|
|
The variable `new_rows_each_update` is equal to `scale * 100_000`.
|
|
The number of steps is determined by `num_iters` variable."""
|
|
new_rows_each_update = scale * 100_000
|
|
|
|
def start_single_table_workload(table_id: int):
|
|
for _ in range(num_iters):
|
|
with env.pg.connect(options="-cstatement_timeout=300s").cursor() as cur:
|
|
cur.execute(
|
|
f"INSERT INTO t{table_id} SELECT FROM generate_series(1,{new_rows_each_update})"
|
|
)
|
|
|
|
with env.record_duration("run_duration"):
|
|
threads = [
|
|
threading.Thread(target=start_single_table_workload, args=(i,)) for i in range(n_tables)
|
|
]
|
|
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
|
|
@pytest.mark.timeout(1000)
|
|
@pytest.mark.parametrize("n_tables", [5])
|
|
@pytest.mark.parametrize("scale", get_scales_matrix(5))
|
|
@pytest.mark.parametrize("num_iters", [10])
|
|
def test_heavy_write_workload(pg_compare: PgCompare, n_tables: int, scale: int, num_iters: int):
|
|
env = pg_compare
|
|
|
|
# Initializes test tables
|
|
with env.pg.connect().cursor() as cur:
|
|
for i in range(n_tables):
|
|
cur.execute(
|
|
f"CREATE TABLE t{i}(key serial primary key, t text default 'foooooooooooooooooooooooooooooooooooooooooooooooooooo')"
|
|
)
|
|
cur.execute(f"INSERT INTO t{i} (key) VALUES (0)")
|
|
|
|
workload_thread = threading.Thread(
|
|
target=start_heavy_write_workload, args=(env, n_tables, scale, num_iters)
|
|
)
|
|
workload_thread.start()
|
|
|
|
record_thread = threading.Thread(
|
|
target=record_lsn_write_lag, args=(env, lambda: workload_thread.is_alive())
|
|
)
|
|
record_thread.start()
|
|
|
|
record_read_latency(env, lambda: workload_thread.is_alive(), "SELECT * from t0 where key = 0")
|
|
workload_thread.join()
|
|
record_thread.join()
|
|
|
|
|
|
def start_pgbench_simple_update_workload(env: PgCompare, duration: int):
|
|
with env.record_duration("run_duration"):
|
|
env.pg_bin.run_capture(
|
|
[
|
|
"pgbench",
|
|
"-j10",
|
|
"-c10",
|
|
"-N",
|
|
f"-T{duration}",
|
|
env.pg.connstr(options="-csynchronous_commit=off"),
|
|
]
|
|
)
|
|
env.flush()
|
|
|
|
|
|
@pytest.mark.timeout(1000)
|
|
@pytest.mark.parametrize("scale", get_scales_matrix(100))
|
|
@pytest.mark.parametrize("duration", get_durations_matrix())
|
|
def test_pgbench_simple_update_workload(pg_compare: PgCompare, scale: int, duration: int):
|
|
env = pg_compare
|
|
|
|
# initialize pgbench tables
|
|
env.pg_bin.run_capture(["pgbench", f"-s{scale}", "-i", env.pg.connstr()])
|
|
env.flush()
|
|
|
|
workload_thread = threading.Thread(
|
|
target=start_pgbench_simple_update_workload, args=(env, duration)
|
|
)
|
|
workload_thread.start()
|
|
|
|
record_thread = threading.Thread(
|
|
target=record_lsn_write_lag, args=(env, lambda: workload_thread.is_alive())
|
|
)
|
|
record_thread.start()
|
|
|
|
record_read_latency(
|
|
env, lambda: workload_thread.is_alive(), "SELECT * from pgbench_accounts where aid = 1"
|
|
)
|
|
workload_thread.join()
|
|
record_thread.join()
|
|
|
|
|
|
def start_pgbench_intensive_initialization(env: PgCompare, scale: int, done_event: threading.Event):
|
|
with env.record_duration("run_duration"):
|
|
# Disable statement timeout (default: 120s) because the
|
|
# initialization step can be slow with a large scale.
|
|
env.pg_bin.run_capture(
|
|
[
|
|
"pgbench",
|
|
f"-s{scale}",
|
|
"-i",
|
|
"-Idtg",
|
|
env.pg.connstr(options="-cstatement_timeout=0"),
|
|
]
|
|
)
|
|
|
|
done_event.set()
|
|
|
|
|
|
@pytest.mark.timeout(1000)
|
|
@pytest.mark.parametrize("scale", get_scales_matrix(1000))
|
|
def test_pgbench_intensive_init_workload(pg_compare: PgCompare, scale: int):
|
|
env = pg_compare
|
|
with env.pg.connect().cursor() as cur:
|
|
cur.execute("CREATE TABLE foo as select generate_series(1,100000)")
|
|
|
|
workload_done_event = threading.Event()
|
|
|
|
workload_thread = threading.Thread(
|
|
target=start_pgbench_intensive_initialization, args=(env, scale, workload_done_event)
|
|
)
|
|
workload_thread.start()
|
|
|
|
record_thread = threading.Thread(
|
|
target=record_lsn_write_lag, args=(env, lambda: not workload_done_event.is_set())
|
|
)
|
|
record_thread.start()
|
|
|
|
record_read_latency(env, lambda: not workload_done_event.is_set(), "SELECT count(*) from foo")
|
|
workload_thread.join()
|
|
record_thread.join()
|
|
|
|
|
|
def record_lsn_write_lag(env: PgCompare, run_cond: Callable[[], bool], pool_interval: float = 1.0):
|
|
if not isinstance(env, NeonCompare):
|
|
return
|
|
|
|
lsn_write_lags: list[Any] = []
|
|
last_received_lsn = Lsn(0)
|
|
last_pg_flush_lsn = Lsn(0)
|
|
|
|
with env.pg.connect().cursor() as cur:
|
|
cur.execute("CREATE EXTENSION neon")
|
|
|
|
while run_cond():
|
|
cur.execute(
|
|
"""
|
|
select pg_wal_lsn_diff(pg_current_wal_flush_lsn(),received_lsn),
|
|
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_flush_lsn(),received_lsn)),
|
|
pg_current_wal_flush_lsn(),
|
|
received_lsn
|
|
from backpressure_lsns();
|
|
"""
|
|
)
|
|
|
|
res = cur.fetchone()
|
|
assert isinstance(res, tuple)
|
|
lsn_write_lags.append(res[0])
|
|
|
|
curr_received_lsn = Lsn(res[3])
|
|
lsn_process_speed = (curr_received_lsn - last_received_lsn) / (1024**2)
|
|
last_received_lsn = curr_received_lsn
|
|
|
|
curr_pg_flush_lsn = Lsn(res[2])
|
|
lsn_produce_speed = (curr_pg_flush_lsn - last_pg_flush_lsn) / (1024**2)
|
|
last_pg_flush_lsn = curr_pg_flush_lsn
|
|
|
|
log.info(
|
|
f"received_lsn_lag={res[1]}, pg_flush_lsn={res[2]}, received_lsn={res[3]}, lsn_process_speed={lsn_process_speed:.2f}MB/s, lsn_produce_speed={lsn_produce_speed:.2f}MB/s"
|
|
)
|
|
|
|
time.sleep(pool_interval)
|
|
|
|
env.zenbenchmark.record(
|
|
"lsn_write_lag_max",
|
|
float(max(lsn_write_lags) / (1024**2)),
|
|
"MB",
|
|
MetricReport.LOWER_IS_BETTER,
|
|
)
|
|
env.zenbenchmark.record(
|
|
"lsn_write_lag_avg",
|
|
float(statistics.mean(lsn_write_lags) / (1024**2)),
|
|
"MB",
|
|
MetricReport.LOWER_IS_BETTER,
|
|
)
|
|
env.zenbenchmark.record(
|
|
"lsn_write_lag_stdev",
|
|
float(statistics.stdev(lsn_write_lags) / (1024**2)),
|
|
"MB",
|
|
MetricReport.LOWER_IS_BETTER,
|
|
)
|
|
|
|
|
|
def record_read_latency(
|
|
env: PgCompare, run_cond: Callable[[], bool], read_query: str, read_interval: float = 1.0
|
|
):
|
|
read_latencies = []
|
|
|
|
with env.pg.connect().cursor() as cur:
|
|
while run_cond():
|
|
try:
|
|
t1 = timeit.default_timer()
|
|
cur.execute(read_query)
|
|
t2 = timeit.default_timer()
|
|
|
|
log.info(
|
|
f"Executed read query {read_query}, got {cur.fetchall()}, read time {t2-t1:.2f}s"
|
|
)
|
|
read_latencies.append(t2 - t1)
|
|
except Exception as err:
|
|
log.error(f"Got error when executing the read query: {err}")
|
|
|
|
time.sleep(read_interval)
|
|
|
|
env.zenbenchmark.record(
|
|
"read_latency_max", max(read_latencies), "s", MetricReport.LOWER_IS_BETTER
|
|
)
|
|
env.zenbenchmark.record(
|
|
"read_latency_avg", statistics.mean(read_latencies), "s", MetricReport.LOWER_IS_BETTER
|
|
)
|
|
env.zenbenchmark.record(
|
|
"read_latency_stdev", statistics.stdev(read_latencies), "s", MetricReport.LOWER_IS_BETTER
|
|
)
|