pageserver: handle WAL gaps on sharded tenants (#6788)

## Problem

In the test for https://github.com/neondatabase/neon/pull/6776, a test
cases uses tiny layer sizes and tiny stripe sizes. This hits a scenario
where a shard's checkpoint interval spans a region where none of the
content in the WAL is ingested by this shard. Since there is no layer to
flush, we do not advance disk_consistent_lsn, and this causes the test
to fail while waiting for LSN to advance.

## Summary of changes

- Pass an LSN through `layer_flush_start_tx`. This is the LSN to which
we have frozen at the time we ask the flush to flush layers frozen up to
this point.
- In the layer flush task, if the layers we flush do not reach
`frozen_to_lsn`, then advance disk_consistent_lsn up to this point.
- In `maybe_freeze_ephemeral_layer`, handle the case where
last_record_lsn has advanced without writing a layer file: this ensures
that disk_consistent_lsn and remote_consistent_lsn advance anyway.

The net effect is that the disk_consistent_lsn is allowed to advance
past regions in the WAL where a shard ingests no data, and that we
uphold our guarantee that remote_consistent_lsn always eventually
reaches the tip of the WAL.

The case of no layer at all is hard to test at present due to >0 shards
being polluted with SLRU writes, but I have tested it locally with a
branch that disables SLRU writes on shards >0. We can tighten up the
testing on this in future as/when we refine shard filtering (currently
shards >0 need the SLRU because they use it to figure out cutoff in GC
using timestamp-to-lsn).
This commit is contained in:
John Spray
2024-04-04 17:54:38 +01:00
committed by GitHub
parent 862a6b7018
commit ac7fc6110b
4 changed files with 225 additions and 31 deletions
+5
View File
@@ -85,6 +85,11 @@ class Workload:
if self._endpoint is not None:
self._endpoint.stop()
def stop(self):
if self._endpoint is not None:
self._endpoint.stop()
self._endpoint = None
def init(self, pageserver_id: Optional[int] = None):
endpoint = self.endpoint(pageserver_id)
+97 -5
View File
@@ -11,7 +11,9 @@ from fixtures.neon_fixtures import (
NeonEnv,
NeonEnvBuilder,
StorageControllerApiException,
last_flush_lsn_upload,
tenant_get_shards,
wait_for_last_flush_lsn,
)
from fixtures.remote_storage import s3_storage
from fixtures.types import Lsn, TenantId, TenantShardId, TimelineId
@@ -466,13 +468,11 @@ def test_sharding_split_stripe_size(
os.getenv("BUILD_TYPE") == "debug",
reason="Avoid running bulkier ingest tests in debug mode",
)
def test_sharding_ingest(
def test_sharding_ingest_layer_sizes(
neon_env_builder: NeonEnvBuilder,
):
"""
Check behaviors related to ingest:
- That we generate properly sized layers
- TODO: that updates to remote_consistent_lsn are made correctly via safekeepers
Check that when ingesting data to a sharded tenant, we properly respect layer size limts.
"""
# Set a small stripe size and checkpoint distance, so that we can exercise rolling logic
@@ -503,6 +503,7 @@ def test_sharding_ingest(
workload.write_rows(4096, upload=False)
workload.write_rows(4096, upload=False)
workload.write_rows(4096, upload=False)
workload.validate()
small_layer_count = 0
@@ -515,7 +516,9 @@ def test_sharding_ingest(
shard_id = shard["shard_id"]
layer_map = pageserver.http_client().layer_map_info(shard_id, timeline_id)
for layer in layer_map.historic_layers:
historic_layers = sorted(layer_map.historic_layers, key=lambda layer: layer.lsn_start)
for layer in historic_layers:
assert layer.layer_file_size is not None
if layer.layer_file_size < expect_layer_size // 2:
classification = "Small"
@@ -552,6 +555,93 @@ def test_sharding_ingest(
assert huge_layer_count <= shard_count
def test_sharding_ingest_gaps(
neon_env_builder: NeonEnvBuilder,
):
"""
Check ingest behavior when the incoming data results in some shards having gaps where
no data is ingested: they should advance their disk_consistent_lsn and remote_consistent_lsn
even if they aren't writing out layers.
"""
# Set a small stripe size and checkpoint distance, so that we can exercise rolling logic
# without writing a lot of data.
expect_layer_size = 131072
checkpoint_interval_secs = 5
TENANT_CONF = {
# small checkpointing and compaction targets to ensure we generate many upload operations
"checkpoint_distance": f"{expect_layer_size}",
"compaction_target_size": f"{expect_layer_size}",
# Set a short checkpoint interval as we will wait for uploads to happen
"checkpoint_timeout": f"{checkpoint_interval_secs}s",
# Background checkpointing is done from compaction loop, so set that interval short too
"compaction_period": "1s",
}
shard_count = 4
neon_env_builder.num_pageservers = shard_count
env = neon_env_builder.init_start(
initial_tenant_conf=TENANT_CONF,
initial_tenant_shard_count=shard_count,
initial_tenant_shard_stripe_size=128,
)
tenant_id = env.initial_tenant
timeline_id = env.initial_timeline
# Just a few writes: we aim to produce a situation where some shards are skipping
# ingesting some records and thereby won't have layer files that advance their
# consistent LSNs, to exercise the code paths that explicitly handle this case by
# advancing consistent LSNs in the background if there is no open layer.
workload = Workload(env, tenant_id, timeline_id)
workload.init()
workload.write_rows(128, upload=False)
workload.churn_rows(128, upload=False)
# Checkpoint, so that we won't get a background checkpoint happening during the next step
workload.endpoint().safe_psql("checkpoint")
# Freeze + flush, so that subsequent writes will start from a position of no open layers
last_flush_lsn_upload(env, workload.endpoint(), tenant_id, timeline_id)
# This write is tiny: at least some of the shards should find they don't have any
# data to ingest. This will exercise how they handle that.
workload.churn_rows(1, upload=False)
# The LSN that has reached pageservers, but may not have been flushed to historic layers yet
expect_lsn = wait_for_last_flush_lsn(env, workload.endpoint(), tenant_id, timeline_id)
# Don't leave the endpoint running, we don't want it writing in the background
workload.stop()
log.info(f"Waiting for shards' consistent LSNs to reach {expect_lsn}")
shards = tenant_get_shards(env, tenant_id, None)
def assert_all_disk_consistent():
"""
Assert that all the shards' disk_consistent_lsns have reached expect_lsn
"""
for tenant_shard_id, pageserver in shards:
timeline_detail = pageserver.http_client().timeline_detail(tenant_shard_id, timeline_id)
log.info(f"{tenant_shard_id} (ps {pageserver.id}) detail: {timeline_detail}")
assert Lsn(timeline_detail["disk_consistent_lsn"]) >= expect_lsn
# We set a short checkpoint timeout: expect things to get frozen+flushed within that
wait_until(checkpoint_interval_secs * 3, 1, assert_all_disk_consistent)
def assert_all_remote_consistent():
"""
Assert that all the shards' remote_consistent_lsns have reached expect_lsn
"""
for tenant_shard_id, pageserver in shards:
timeline_detail = pageserver.http_client().timeline_detail(tenant_shard_id, timeline_id)
log.info(f"{tenant_shard_id} (ps {pageserver.id}) detail: {timeline_detail}")
assert Lsn(timeline_detail["remote_consistent_lsn"]) >= expect_lsn
# We set a short checkpoint timeout: expect things to get frozen+flushed within that
wait_until(checkpoint_interval_secs * 3, 1, assert_all_remote_consistent)
workload.validate()
class Failure:
pageserver_id: Optional[int]
@@ -795,6 +885,8 @@ def test_sharding_split_failures(
".*Reconcile error: receive body: error sending request for url.*",
# Node offline cases will fail inside reconciler when detaching secondaries
".*Reconcile error on shard.*: receive body: error sending request for url.*",
# While parent shard's client is stopped during split, flush loop updating LSNs will emit this warning
".*Failed to schedule metadata upload after updating disk_consistent_lsn.*",
]
)