mirror of
https://github.com/neondatabase/neon.git
synced 2026-05-26 01:20:38 +00:00
Add WAL offloading to s3 on safekeepers.
Separate task is launched for each timeline and stopped when timeline doesn't need offloading. Decision who offloads is done through etcd leader election; currently there is no pre condition for participating, that's a TODO. neon_local and tests infrastructure for remote storage in safekeepers added, along with the test itself. ref #1009 Co-authored-by: Anton Shyrabokau <ahtoxa@Antons-MacBook-Pro.local>
This commit is contained in:
@@ -12,7 +12,7 @@ from contextlib import closing
|
||||
from dataclasses import dataclass, field
|
||||
from multiprocessing import Process, Value
|
||||
from pathlib import Path
|
||||
from fixtures.zenith_fixtures import PgBin, Etcd, Postgres, Safekeeper, ZenithEnv, ZenithEnvBuilder, PortDistributor, SafekeeperPort, zenith_binpath, PgProtocol
|
||||
from fixtures.zenith_fixtures import PgBin, Etcd, Postgres, RemoteStorageUsers, Safekeeper, ZenithEnv, ZenithEnvBuilder, PortDistributor, SafekeeperPort, zenith_binpath, PgProtocol
|
||||
from fixtures.utils import get_dir_size, lsn_to_hex, mkdir_if_needed, lsn_from_hex
|
||||
from fixtures.log_helper import log
|
||||
from typing import List, Optional, Any
|
||||
@@ -401,7 +401,7 @@ def test_wal_removal(zenith_env_builder: ZenithEnvBuilder):
|
||||
|
||||
http_cli = env.safekeepers[0].http_client()
|
||||
# Pretend WAL is offloaded to s3.
|
||||
http_cli.record_safekeeper_info(tenant_id, timeline_id, {'s3_wal_lsn': 'FFFFFFFF/FEFFFFFF'})
|
||||
http_cli.record_safekeeper_info(tenant_id, timeline_id, {'backup_lsn': 'FFFFFFFF/FEFFFFFF'})
|
||||
|
||||
# wait till first segment is removed on all safekeepers
|
||||
started_at = time.time()
|
||||
@@ -414,6 +414,56 @@ def test_wal_removal(zenith_env_builder: ZenithEnvBuilder):
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('storage_type', ['mock_s3', 'local_fs'])
|
||||
def test_wal_backup(zenith_env_builder: ZenithEnvBuilder, storage_type: str):
|
||||
zenith_env_builder.num_safekeepers = 3
|
||||
if storage_type == 'local_fs':
|
||||
zenith_env_builder.enable_local_fs_remote_storage()
|
||||
elif storage_type == 'mock_s3':
|
||||
zenith_env_builder.enable_s3_mock_remote_storage('test_safekeepers_wal_backup')
|
||||
else:
|
||||
raise RuntimeError(f'Unknown storage type: {storage_type}')
|
||||
zenith_env_builder.remote_storage_users = RemoteStorageUsers.SAFEKEEPER
|
||||
|
||||
env = zenith_env_builder.init_start()
|
||||
|
||||
env.zenith_cli.create_branch('test_safekeepers_wal_backup')
|
||||
pg = env.postgres.create_start('test_safekeepers_wal_backup')
|
||||
|
||||
# learn zenith timeline from compute
|
||||
tenant_id = pg.safe_psql("show zenith.zenith_tenant")[0][0]
|
||||
timeline_id = pg.safe_psql("show zenith.zenith_timeline")[0][0]
|
||||
|
||||
pg_conn = pg.connect()
|
||||
cur = pg_conn.cursor()
|
||||
cur.execute('create table t(key int, value text)')
|
||||
|
||||
# Shut down subsequently each of safekeepers and fill a segment while sk is
|
||||
# down; ensure segment gets offloaded by others.
|
||||
offloaded_seg_end = ['0/2000000', '0/3000000', '0/4000000']
|
||||
for victim, seg_end in zip(env.safekeepers, offloaded_seg_end):
|
||||
victim.stop()
|
||||
# roughly fills one segment
|
||||
cur.execute("insert into t select generate_series(1,250000), 'payload'")
|
||||
live_sk = [sk for sk in env.safekeepers if sk != victim][0]
|
||||
http_cli = live_sk.http_client()
|
||||
|
||||
started_at = time.time()
|
||||
while True:
|
||||
tli_status = http_cli.timeline_status(tenant_id, timeline_id)
|
||||
log.info(f"live sk status is {tli_status}")
|
||||
|
||||
if lsn_from_hex(tli_status.backup_lsn) >= lsn_from_hex(seg_end):
|
||||
break
|
||||
elapsed = time.time() - started_at
|
||||
if elapsed > 20:
|
||||
raise RuntimeError(
|
||||
f"timed out waiting {elapsed:.0f}s segment ending at {seg_end} get offloaded")
|
||||
time.sleep(0.5)
|
||||
|
||||
victim.start()
|
||||
|
||||
|
||||
class ProposerPostgres(PgProtocol):
|
||||
"""Object for running postgres without ZenithEnv"""
|
||||
def __init__(self,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import field
|
||||
from enum import Flag, auto
|
||||
import textwrap
|
||||
from cached_property import cached_property
|
||||
import asyncpg
|
||||
@@ -421,10 +422,51 @@ class MockS3Server:
|
||||
def secret_key(self) -> str:
|
||||
return 'test'
|
||||
|
||||
def access_env_vars(self) -> Dict[Any, Any]:
|
||||
return {
|
||||
'AWS_ACCESS_KEY_ID': self.access_key(),
|
||||
'AWS_SECRET_ACCESS_KEY': self.secret_key(),
|
||||
}
|
||||
|
||||
def kill(self):
|
||||
self.subprocess.kill()
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalFsStorage:
|
||||
local_path: Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class S3Storage:
|
||||
bucket_name: str
|
||||
bucket_region: str
|
||||
endpoint: Optional[str]
|
||||
|
||||
|
||||
RemoteStorage = Union[LocalFsStorage, S3Storage]
|
||||
|
||||
|
||||
# serialize as toml inline table
|
||||
def remote_storage_to_toml_inline_table(remote_storage):
|
||||
if isinstance(remote_storage, LocalFsStorage):
|
||||
res = f"local_path='{remote_storage.local_path}'"
|
||||
elif isinstance(remote_storage, S3Storage):
|
||||
res = f"bucket_name='{remote_storage.bucket_name}', bucket_region='{remote_storage.bucket_region}'"
|
||||
if remote_storage.endpoint is not None:
|
||||
res += f", endpoint='{remote_storage.endpoint}'"
|
||||
else:
|
||||
raise Exception(f'Unknown storage configuration {remote_storage}')
|
||||
else:
|
||||
raise Exception("invalid remote storage type")
|
||||
return f"{{{res}}}"
|
||||
|
||||
|
||||
class RemoteStorageUsers(Flag):
|
||||
PAGESERVER = auto()
|
||||
SAFEKEEPER = auto()
|
||||
|
||||
|
||||
class ZenithEnvBuilder:
|
||||
"""
|
||||
Builder object to create a Zenith runtime environment
|
||||
@@ -440,6 +482,7 @@ class ZenithEnvBuilder:
|
||||
broker: Etcd,
|
||||
mock_s3_server: MockS3Server,
|
||||
remote_storage: Optional[RemoteStorage] = None,
|
||||
remote_storage_users: RemoteStorageUsers = RemoteStorageUsers.PAGESERVER,
|
||||
pageserver_config_override: Optional[str] = None,
|
||||
num_safekeepers: int = 1,
|
||||
pageserver_auth_enabled: bool = False,
|
||||
@@ -449,6 +492,7 @@ class ZenithEnvBuilder:
|
||||
self.rust_log_override = rust_log_override
|
||||
self.port_distributor = port_distributor
|
||||
self.remote_storage = remote_storage
|
||||
self.remote_storage_users = remote_storage_users
|
||||
self.broker = broker
|
||||
self.mock_s3_server = mock_s3_server
|
||||
self.pageserver_config_override = pageserver_config_override
|
||||
@@ -497,9 +541,9 @@ class ZenithEnvBuilder:
|
||||
aws_access_key_id=self.mock_s3_server.access_key(),
|
||||
aws_secret_access_key=self.mock_s3_server.secret_key(),
|
||||
).create_bucket(Bucket=bucket_name)
|
||||
self.remote_storage = S3Storage(bucket=bucket_name,
|
||||
self.remote_storage = S3Storage(bucket_name=bucket_name,
|
||||
endpoint=mock_endpoint,
|
||||
region=mock_region)
|
||||
bucket_region=mock_region)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -557,6 +601,7 @@ class ZenithEnv:
|
||||
self.safekeepers: List[Safekeeper] = []
|
||||
self.broker = config.broker
|
||||
self.remote_storage = config.remote_storage
|
||||
self.remote_storage_users = config.remote_storage_users
|
||||
|
||||
# generate initial tenant ID here instead of letting 'zenith init' generate it,
|
||||
# so that we don't need to dig it out of the config file afterwards.
|
||||
@@ -605,8 +650,12 @@ class ZenithEnv:
|
||||
id = {id}
|
||||
pg_port = {port.pg}
|
||||
http_port = {port.http}
|
||||
sync = false # Disable fsyncs to make the tests go faster
|
||||
""")
|
||||
sync = false # Disable fsyncs to make the tests go faster""")
|
||||
if bool(self.remote_storage_users
|
||||
& RemoteStorageUsers.SAFEKEEPER) and self.remote_storage is not None:
|
||||
toml += textwrap.dedent(f"""
|
||||
remote_storage = "{remote_storage_to_toml_inline_table(self.remote_storage)}"
|
||||
""")
|
||||
safekeeper = Safekeeper(env=self, id=id, port=port)
|
||||
self.safekeepers.append(safekeeper)
|
||||
|
||||
@@ -638,7 +687,7 @@ def _shared_simple_env(request: Any,
|
||||
mock_s3_server: MockS3Server,
|
||||
default_broker: Etcd) -> Iterator[ZenithEnv]:
|
||||
"""
|
||||
Internal fixture backing the `zenith_simple_env` fixture. If TEST_SHARED_FIXTURES
|
||||
# Internal fixture backing the `zenith_simple_env` fixture. If TEST_SHARED_FIXTURES
|
||||
is set, this is shared by all tests using `zenith_simple_env`.
|
||||
"""
|
||||
|
||||
@@ -822,20 +871,6 @@ class PageserverPort:
|
||||
http: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalFsStorage:
|
||||
root: Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class S3Storage:
|
||||
bucket: str
|
||||
region: str
|
||||
endpoint: Optional[str]
|
||||
|
||||
|
||||
RemoteStorage = Union[LocalFsStorage, S3Storage]
|
||||
|
||||
CREATE_TIMELINE_ID_EXTRACTOR = re.compile(r"^Created timeline '(?P<timeline_id>[^']+)'",
|
||||
re.MULTILINE)
|
||||
CREATE_TIMELINE_ID_EXTRACTOR = re.compile(r"^Created timeline '(?P<timeline_id>[^']+)'",
|
||||
@@ -998,6 +1033,7 @@ class ZenithCli:
|
||||
append_pageserver_param_overrides(
|
||||
params_to_update=cmd,
|
||||
remote_storage=self.env.remote_storage,
|
||||
remote_storage_users=self.env.remote_storage_users,
|
||||
pageserver_config_override=self.env.pageserver.config_override)
|
||||
|
||||
res = self.raw_cli(cmd)
|
||||
@@ -1022,14 +1058,10 @@ class ZenithCli:
|
||||
append_pageserver_param_overrides(
|
||||
params_to_update=start_args,
|
||||
remote_storage=self.env.remote_storage,
|
||||
remote_storage_users=self.env.remote_storage_users,
|
||||
pageserver_config_override=self.env.pageserver.config_override)
|
||||
|
||||
s3_env_vars = None
|
||||
if self.env.s3_mock_server:
|
||||
s3_env_vars = {
|
||||
'AWS_ACCESS_KEY_ID': self.env.s3_mock_server.access_key(),
|
||||
'AWS_SECRET_ACCESS_KEY': self.env.s3_mock_server.secret_key(),
|
||||
}
|
||||
s3_env_vars = self.env.s3_mock_server.access_env_vars() if self.env.s3_mock_server else None
|
||||
return self.raw_cli(start_args, extra_env_vars=s3_env_vars)
|
||||
|
||||
def pageserver_stop(self, immediate=False) -> 'subprocess.CompletedProcess[str]':
|
||||
@@ -1041,7 +1073,8 @@ class ZenithCli:
|
||||
return self.raw_cli(cmd)
|
||||
|
||||
def safekeeper_start(self, id: int) -> 'subprocess.CompletedProcess[str]':
|
||||
return self.raw_cli(['safekeeper', 'start', str(id)])
|
||||
s3_env_vars = self.env.s3_mock_server.access_env_vars() if self.env.s3_mock_server else None
|
||||
return self.raw_cli(['safekeeper', 'start', str(id)], extra_env_vars=s3_env_vars)
|
||||
|
||||
def safekeeper_stop(self,
|
||||
id: Optional[int] = None,
|
||||
@@ -1237,22 +1270,13 @@ class ZenithPageserver(PgProtocol):
|
||||
def append_pageserver_param_overrides(
|
||||
params_to_update: List[str],
|
||||
remote_storage: Optional[RemoteStorage],
|
||||
remote_storage_users: RemoteStorageUsers,
|
||||
pageserver_config_override: Optional[str] = None,
|
||||
):
|
||||
if remote_storage is not None:
|
||||
if isinstance(remote_storage, LocalFsStorage):
|
||||
pageserver_storage_override = f"local_path='{remote_storage.root}'"
|
||||
elif isinstance(remote_storage, S3Storage):
|
||||
pageserver_storage_override = f"bucket_name='{remote_storage.bucket}',\
|
||||
bucket_region='{remote_storage.region}'"
|
||||
|
||||
if remote_storage.endpoint is not None:
|
||||
pageserver_storage_override += f",endpoint='{remote_storage.endpoint}'"
|
||||
|
||||
else:
|
||||
raise Exception(f'Unknown storage configuration {remote_storage}')
|
||||
if bool(remote_storage_users & RemoteStorageUsers.PAGESERVER) and remote_storage is not None:
|
||||
remote_storage_toml_table = remote_storage_to_toml_inline_table(remote_storage)
|
||||
params_to_update.append(
|
||||
f'--pageserver-config-override=remote_storage={{{pageserver_storage_override}}}')
|
||||
f'--pageserver-config-override=remote_storage={remote_storage_toml_table}')
|
||||
|
||||
env_overrides = os.getenv('ZENITH_PAGESERVER_OVERRIDES')
|
||||
if env_overrides is not None:
|
||||
@@ -1786,8 +1810,9 @@ class Safekeeper:
|
||||
class SafekeeperTimelineStatus:
|
||||
acceptor_epoch: int
|
||||
flush_lsn: str
|
||||
remote_consistent_lsn: str
|
||||
timeline_start_lsn: str
|
||||
backup_lsn: str
|
||||
remote_consistent_lsn: str
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1812,8 +1837,9 @@ class SafekeeperHttpClient(requests.Session):
|
||||
resj = res.json()
|
||||
return SafekeeperTimelineStatus(acceptor_epoch=resj['acceptor_state']['epoch'],
|
||||
flush_lsn=resj['flush_lsn'],
|
||||
remote_consistent_lsn=resj['remote_consistent_lsn'],
|
||||
timeline_start_lsn=resj['timeline_start_lsn'])
|
||||
timeline_start_lsn=resj['timeline_start_lsn'],
|
||||
backup_lsn=resj['backup_lsn'],
|
||||
remote_consistent_lsn=resj['remote_consistent_lsn'])
|
||||
|
||||
def record_safekeeper_info(self, tenant_id: str, timeline_id: str, body):
|
||||
res = self.post(
|
||||
|
||||
Reference in New Issue
Block a user