extract the l0stack generator into a reusable python module

This commit is contained in:
Christian Schwarz
2025-01-16 13:24:34 +01:00
parent 5b77a6d3ce
commit 2ff0a4ae82
6 changed files with 149 additions and 97 deletions
+28 -90
View File
@@ -15,33 +15,28 @@ Measure impact of that.
Some commands from shell history used in combination with this script:
NEON_PAGESERVER_VALUE_RECONSTRUCT_IO_CONCURRENCY=futures-unordered ./target/release/neon_local pageserver restart
NEON_PAGESERVER_VALUE_RECONSTRUCT_IO_CONCURRENCY=serial ./target/release/neon_local pageserver restart
NEON_PAGESERVER_VALUE_RECONSTRUCT_IO_CONCURRENCY=parallel ./target/release/neon_local pageserver restart
cat >> .neon/pageserver_1/pageserver.toml <<"EOF"
# customizations
virtual_file_io_mode = "direct"
page_service_pipelining={mode="pipelined", max_batch_size=32, execution="concurrent-futures"}
```
./target/release/neon_local stop
rm -rf .neon
./target/release/neon_local init
cat >> .neon/pageserver_1/pageserver.toml <<"EOF"
# customizations
virtual_file_io_mode = "direct"
page_service_pipelining={mode="pipelined", max_batch_size=32, execution="concurrent-futures"}
EOF
./target/release/neon_local start
starting from scrach:
psql 'postgresql://localhost:1235/storage_controller' -c 'DELETE FROM tenant_shards'
NEON_PAGESERVER_VALUE_RECONSTRUCT_IO_CONCURRENCY=... ./target/release/neon_local pageserver restart
./target/debug/neon_local endpoint stop foo
rm -rf .neon/endpoints/foo
./target/debug/neon_local endpoint create foo
echo 'full_page_writes=off' >> .neon/endpoints/foo/postgresql.conf
./target/debug/neon_local endpoint start foo
non-package-mode-py3.10christian@neon-hetzner-dev-christian:[~/src/neon/test_runner]: poetry run python3 deep_layers_with_delta.py
Need 6400 pages, 38400 rows
0
modified rows 6400
1
modified rows 6400
...
19
modified rows 6400
psql 'postgresql://localhost:1235/storage_controller' -c 'DELETE FROM tenant_shards'
NEON_PAGESERVER_VALUE_RECONSTRUCT_IO_CONCURRENCY=futures-unordered ./target/release/neon_local pageserver restart
sleep 2
./target/release/neon_local tenant create --set-default
./target/debug/neon_local endpoint stop foo
rm -rf .neon/endpoints/foo
./target/debug/neon_local endpoint create foo
echo 'full_page_writes=off' >> .neon/endpoints/foo/postgresql.conf
./target/debug/neon_local endpoint start foo
pushd test_runner; poetry run python3 deep_layers_with_delta.py; popd
```
Layer files created:
@@ -77,82 +72,25 @@ Now use pagebench to measure perf, e.g.
"""
from pathlib import Path
import time
from fixtures.common_types import Lsn, TenantId, TenantShardId, TimelineId
import psycopg2
from fixtures.common_types import TenantShardId, TimelineId
from fixtures.pageserver.http import PageserverHttpClient
from fixtures.pageserver.makelayers.l0stack import L0StackShape, make_l0_stack_standalone
ps_http = PageserverHttpClient(port=9898, is_testing_enabled_or_skip=lambda: None)
vps_http = PageserverHttpClient(port=1234, is_testing_enabled_or_skip=lambda: None)
tenants = ps_http.tenant_list()
assert len(tenants) == 1
tenant_id = tenants[0]["id"]
tenant_shard_id = TenantShardId.parse(tenants[0]["id"])
timlines = ps_http.timeline_list(tenant_id)
timlines = ps_http.timeline_list(tenant_shard_id)
assert len(timlines) == 1
timeline_id = timlines[0]["timeline_id"]
config = {
"gc_period": "0s", # disable periodic gc
"checkpoint_timeout": "10 years",
"compaction_period": "1h", # doesn't matter, but 0 value will kill walredo every 10s
"compaction_threshold": 100000, # we just want L0s
"compaction_target_size": 134217728,
"checkpoint_distance": 268435456,
"image_creation_threshold": 100000, # we just want L0s
}
vps_http.set_tenant_config(tenant_id, config)
timeline_id = TimelineId(timlines[0]["timeline_id"])
connstr = "postgresql://cloud_admin@localhost:55432/postgres"
import psycopg2
def last_record_lsn(
pageserver_http_client: PageserverHttpClient,
tenant: TenantId | TenantShardId,
timeline: TimelineId,
) -> Lsn:
detail = pageserver_http_client.timeline_detail(tenant, timeline)
lsn_str = detail["last_record_lsn"]
assert isinstance(lsn_str, str)
return Lsn(lsn_str)
conn = psycopg2.connect(connstr)
conn.autocommit = True
cur = conn.cursor()
# each tuple is 23 (header) + 100 bytes = 123 bytes
# page header si 24 bytes
# 8k page size
# (8k-24bytes) / 123 bytes = 63 tuples per page
# set fillfactor to 10 to have 6 tuples per page
cur.execute("DROP TABLE IF EXISTS data")
cur.execute("CREATE TABLE data(id bigint, row char(92)) with (fillfactor=10)")
desired_size = 50 * 1024 * 1024 # 50MiB
need_pages = desired_size // 8192
need_rows = need_pages * 6
print(f"Need {need_pages} pages, {need_rows} rows")
cur.execute(f"INSERT INTO data SELECT i,'row'||i FROM generate_series(1, {need_rows}) as i")
# every iteration updates one tuple in each page
delta_stack_height = 20
for i in range( 0, delta_stack_height):
print(i)
cur.execute(f"UPDATE data set row = row||',u' where id % 6 = {i%6}")
print("modified rows", cur.rowcount)
cur.execute("SELECT pg_current_wal_flush_lsn()")
flush_lsn = Lsn(cur.fetchall()[0][0])
while True:
last_record = last_record_lsn(ps_http, tenant_id, timeline_id)
if last_record >= flush_lsn:
break
time.sleep(0.1)
ps_http.timeline_checkpoint(tenant_id, timeline_id)
ps_http.timeline_compact(tenant_id, timeline_id)
shape = L0StackShape(logical_table_size_mib=50, delta_stack_height=20)
make_l0_stack_standalone(vps_http, ps_http, tenant_shard_id, timeline_id, conn, shape)
+4
View File
@@ -208,6 +208,10 @@ class ShardIndex:
shard_count=int(input[2:4], 16),
)
@property
def is_sharded(self) -> bool:
return self.shard_count != 0
class TenantShardId:
def __init__(self, tenant_id: TenantId, shard_number: int, shard_count: int):
+4
View File
@@ -313,6 +313,10 @@ class PgProtocol:
"""
return self.safe_psql(query, log_query=log_query)[0][0]
def show_timeline_id(self) -> TimelineId:
"""SHOW neon.timeline_id"""
return TimelineId(cast("str", self.safe_psql("show neon.timeline_id")[0][0]))
class PageserverWalReceiverProtocol(StrEnum):
VANILLA = "vanilla"
@@ -0,0 +1,104 @@
from dataclasses import dataclass
from psycopg2.extensions import connection as PgConnection
from fixtures.common_types import Lsn, ShardIndex, TenantShardId, TimelineId
from fixtures.log_helper import log
from fixtures.neon_fixtures import Endpoint
from fixtures.pageserver.http import PageserverHttpClient
from fixtures.pageserver.utils import wait_for_last_record_lsn
@dataclass
class L0StackShape:
logical_table_size_mib: int = 50
delta_stack_height: int = 20
def make_l0_stack(endpoint: Endpoint, shape: L0StackShape):
env = endpoint.env
# TDOO: wait for storcon to finish any reonciles before jumping to action here?
shards = env.storage_controller.tenant_describe(endpoint.tenant_id)
shard_idx = ShardIndex.parse(shards[0]["shard_id"])
assert shard_idx.shard_count == 1, "see above"
tenant_shard_id = TenantShardId(
endpoint.tenant_id, shard_idx.shard_number, shard_idx.shard_count
)
ps = env.get_pageserver(shards[0]["node_id"])
timeline_id = endpoint.show_timeline_id()
vps_http = env.storage_controller.pageserver_api()
ps_http = ps.http_client()
endpoint_conn = endpoint.connect()
make_l0_stack_standalone(vps_http, ps_http, tenant_shard_id, timeline_id, endpoint_conn, shape)
def make_l0_stack_standalone(
vps_http: PageserverHttpClient,
ps_http: PageserverHttpClient,
tenant_shard_id: TenantShardId,
timeline_id: TimelineId,
endpoint_conn: PgConnection,
shape: L0StackShape,
):
"""
Creates stack of L0 deltas each of which should have 1 Value::Delta per page in `data`.
Usable from scripts (ad-hoc manual testing in neon_local) and pytest code alike.
"""
assert (
not tenant_shard_id.shard_index.is_sharded
), "the current implementation only supports unsharded tenants"
tenant_id = tenant_shard_id.tenant_id
conn = endpoint_conn
desired_size = shape.logical_table_size_mib * 1024 * 1024
config = {
"gc_period": "0s", # disable periodic gc
"checkpoint_timeout": "10 years",
"compaction_period": "1h", # doesn't matter, but 0 value will kill walredo every 10s
"compaction_threshold": 100000, # we just want L0s
"compaction_target_size": 134217728,
"checkpoint_distance": 268435456,
"image_creation_threshold": 100000, # we just want L0s
}
vps_http.set_tenant_config(tenant_id, config)
conn.autocommit = True
cur = conn.cursor()
# each tuple is 23 (header) + 100 bytes = 123 bytes
# page header si 24 bytes
# 8k page size
# (8k-24bytes) / 123 bytes = 63 tuples per page
# set fillfactor to 10 to have 6 tuples per page
cur.execute("DROP TABLE IF EXISTS data")
cur.execute("CREATE TABLE data(id bigint, row char(92)) with (fillfactor=10)")
need_pages = desired_size // 8192
need_rows = need_pages * 6
log.info(f"Need {need_pages} pages, {need_rows} rows")
cur.execute(f"INSERT INTO data SELECT i,'row'||i FROM generate_series(1, {need_rows}) as i")
def settle_and_flush():
cur.execute("SELECT pg_current_wal_flush_lsn()")
flush_lsn = Lsn(cur.fetchall()[0][0])
wait_for_last_record_lsn(ps_http, tenant_shard_id, timeline_id, flush_lsn)
ps_http.timeline_checkpoint(tenant_id, timeline_id)
ps_http.timeline_compact(tenant_id, timeline_id)
# create an L0 for the initial data we just inserted
settle_and_flush()
# every iteration updates one tuple in each page
delta_stack_height = shape.delta_stack_height
for i in range(0, delta_stack_height):
log.info(i)
cur.execute(f"UPDATE data set row = row||',u' where id % 6 = {i%6}")
log.info("modified rows", cur.rowcount)
settle_and_flush()
@@ -1,11 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass
import dataclasses
import json
from pathlib import Path
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
@@ -238,7 +238,6 @@ def setup_tenant_template(env: NeonEnv, pg_bin: PgBin, scale: int):
return (template_tenant, template_timeline, config)
def run_pagebench_benchmark(
env: NeonEnv, pg_bin: PgBin, record, duration_secs: int, n_clients: int
):
@@ -254,7 +253,7 @@ def run_pagebench_benchmark(
pageserver_cpu_seconds_total: float
pageserver_layers_visited_per_vectored_read_global_buckets: dict[int, int]
def __sub__(self, other: "Metrics") -> "Metrics":
def __sub__(self, other: Metrics) -> Metrics:
return Metrics(
time=self.time - other.time,
pageserver_cpu_seconds_total=self.pageserver_cpu_seconds_total
@@ -265,7 +264,7 @@ def run_pagebench_benchmark(
},
)
def normalize(self, by) -> "Metrics":
def normalize(self, by) -> Metrics:
return Metrics(
time=self.time / by,
pageserver_cpu_seconds_total=self.pageserver_cpu_seconds_total / by,
@@ -277,6 +276,7 @@ def run_pagebench_benchmark(
def get_metrics() -> Metrics:
pageserver_metrics = ps_http.get_metrics()
def parse_le_label(s):
try:
return int(s)
@@ -285,6 +285,7 @@ def run_pagebench_benchmark(
return sys.maxsize
else:
raise
return Metrics(
time=time.time(),
pageserver_cpu_seconds_total=pageserver_metrics.query_one(
@@ -328,7 +329,6 @@ def run_pagebench_benchmark(
metrics = metrics_after - metrics_before
#
# Record the results
#
@@ -368,4 +368,6 @@ def run_pagebench_benchmark(
report=MetricReport.TEST_PARAM,
)
else:
record(f"counters.{metric}", metric_value=value, unit="", report=MetricReport.TEST_PARAM)
record(
f"counters.{metric}", metric_value=value, unit="", report=MetricReport.TEST_PARAM
)