Skip to content

Commit ec8e44c

Browse files
committed
lsm/io: read cloud SSTs in chunks
The cloud cache LSM persistence was previously reading SSTs whole. At scale, this meant that when the LSM grew to include some L3 or L4 objects (O(GiBs) each), readers could be hit with minutes of latency. Worse yet, often times the reads only need to read some few MiBs (footers, bloom filters, indexes) to actually serve the incoming metastore request. This adds the new chunked file reader to the cloud_cache_persistence, though it still attempts to read the full file from the cache if it exists (e.g. if the object was there from previous versions of Redpanda, or if it was written to the cache on the SST upload path). The default chunk size is 24MiB, 1.5x the default SST size of L0, so that by default L0 files are read whole (empirically they didn't seem problematic being downloaded whole).
1 parent 357ce17 commit ec8e44c

9 files changed

Lines changed: 110 additions & 81 deletions

File tree

src/v/cloud_topics/level_one/domain/tests/db_domain_manager_test.cc

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,22 @@ void flush_as_manifest(
232232
term_state_update_t new_terms) {
233233
auto domain_prefix = cloud_storage_clients::object_key{
234234
domain_cloud_prefix(uuid)};
235-
auto cloud_db = lsm::database::open(
236-
{.database_epoch = db_epoch},
237-
lsm::io::persistence{
238-
.data = lsm::io::open_cloud_cache_data_persistence(
239-
cache, remote, bucket, domain_prefix)
240-
.get(),
241-
.metadata = lsm::io::open_cloud_metadata_persistence(
242-
remote, bucket, domain_prefix)
243-
.get()})
244-
.get();
235+
auto cloud_db
236+
= lsm::database::open(
237+
{.database_epoch = db_epoch},
238+
lsm::io::persistence{
239+
.data = lsm::io::open_cloud_cache_data_persistence(
240+
cache,
241+
remote,
242+
bucket,
243+
domain_prefix,
244+
config::shard_local_cfg()
245+
.cloud_topics_metastore_sst_chunk_size.bind())
246+
.get(),
247+
.metadata = lsm::io::open_cloud_metadata_persistence(
248+
remote, bucket, domain_prefix)
249+
.get()})
250+
.get();
245251

246252
// Pre-register the object IDs before building the add_objects rows.
247253
preregister_objects_db_update prereg_update;

src/v/cloud_topics/level_one/metastore/lsm/replicated_db.cc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,12 @@ replicated_database::open(
115115

116116
auto data_persist_fut = co_await ss::coroutine::as_future(
117117
lsm::io::open_cloud_cache_data_persistence(
118-
cache, remote, bucket, domain_prefix, cloud_io::group_id::metastore));
118+
cache,
119+
remote,
120+
bucket,
121+
domain_prefix,
122+
config::shard_local_cfg().cloud_topics_metastore_sst_chunk_size.bind(),
123+
cloud_io::group_id::metastore));
119124
if (data_persist_fut.failed()) {
120125
co_return std::unexpected(wrap_failed_future(
121126
data_persist_fut.get_exception(), "Failed to open data persistence"));

src/v/cloud_topics/level_one/metastore/lsm/tests/replicated_db_test.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ class ReplicatedDatabaseTest
252252
&test_cache.local(),
253253
&sr->remote.local(),
254254
bucket_name,
255-
domain_prefix)
255+
domain_prefix,
256+
config::shard_local_cfg()
257+
.cloud_topics_metastore_sst_chunk_size.bind())
256258
.get(),
257259
.metadata = lsm::io::open_cloud_metadata_persistence(
258260
&sr->remote.local(), bucket_name, domain_prefix)

src/v/cloud_topics/read_replica/snapshot_manager.cc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,12 @@ ss::future<> database_refresher::open_or_refresh() {
138138
cloud_storage_clients::object_key domain_prefix{
139139
domain_cloud_prefix(domain_uuid_)};
140140
auto data_persist = co_await lsm::io::open_cloud_cache_data_persistence(
141-
cache_, remote_, bucket_, domain_prefix, cloud_io::group_id::metastore);
141+
cache_,
142+
remote_,
143+
bucket_,
144+
domain_prefix,
145+
config::shard_local_cfg().cloud_topics_metastore_sst_chunk_size.bind(),
146+
cloud_io::group_id::metastore);
142147
auto meta_persist = co_await lsm::io::open_cloud_metadata_persistence(
143148
remote_, bucket_, domain_prefix, cloud_io::group_id::metastore);
144149
lsm::io::persistence io{

src/v/cloud_topics/read_replica/tests/db_utils.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "cloud_topics/level_one/metastore/lsm/state_reader.h"
1818
#include "cloud_topics/level_one/metastore/lsm/state_update.h"
1919
#include "cloud_topics/level_one/metastore/lsm/write_batch_row.h"
20+
#include "config/configuration.h"
2021
#include "container/chunked_hash_map.h"
2122
#include "lsm/io/cloud_cache_persistence.h"
2223
#include "lsm/lsm.h"
@@ -49,7 +50,11 @@ inline ss::future<lsm::database*> get_or_create_writer_db(
4950
cloud_storage_clients::object_key domain_prefix{cloud_prefix};
5051

5152
auto data_persist = co_await lsm::io::open_cloud_cache_data_persistence(
52-
cache, remote, bucket, domain_prefix);
53+
cache,
54+
remote,
55+
bucket,
56+
domain_prefix,
57+
config::shard_local_cfg().cloud_topics_metastore_sst_chunk_size.bind());
5358
auto meta_persist = co_await lsm::io::open_cloud_metadata_persistence(
5459
remote, bucket, domain_prefix);
5560

src/v/lsm/io/BUILD

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ redpanda_cc_library(
5252
srcs = ["cloud_cache_persistence.cc"],
5353
hdrs = ["cloud_cache_persistence.h"],
5454
implementation_deps = [
55+
":chunked_remote_file_reader",
5556
":file_io",
5657
"//src/v/cloud_io:io_result",
57-
"//src/v/config",
5858
"//src/v/lsm/core:exceptions",
5959
"//src/v/lsm/core/internal:files",
6060
"//src/v/ssx:future_util",
@@ -66,6 +66,7 @@ redpanda_cc_library(
6666
"//src/v/cloud_io:cache",
6767
"//src/v/cloud_io:remote",
6868
"//src/v/cloud_storage_clients",
69+
"//src/v/config",
6970
"@seastar",
7071
],
7172
)

src/v/lsm/io/cloud_cache_persistence.cc

Lines changed: 54 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "config/configuration.h"
1616
#include "lsm/core/exceptions.h"
1717
#include "lsm/core/internal/files.h"
18+
#include "lsm/io/chunked_remote_file_reader.h"
1819
#include "lsm/io/file_io.h"
1920
#include "ssx/future-util.h"
2021
#include "utils/retry_chain_node.h"
@@ -33,11 +34,14 @@ namespace {
3334

3435
static constexpr auto reservation_timeout = std::chrono::seconds(30);
3536

37+
static constexpr auto cloud_op_timeout = std::chrono::seconds(10);
38+
39+
std::chrono::milliseconds cloud_op_backoff() {
40+
return config::shard_local_cfg().cloud_storage_initial_backoff_ms.value();
41+
}
42+
3643
retry_chain_node make_cloud_rtc(ss::abort_source& as) {
37-
constexpr auto timeout = std::chrono::seconds(10);
38-
auto backoff
39-
= config::shard_local_cfg().cloud_storage_initial_backoff_ms.value();
40-
return retry_chain_node{as, timeout, backoff};
44+
return retry_chain_node{as, cloud_op_timeout, cloud_op_backoff()};
4145
}
4246

4347
bool check_cloud_result(cloud_io::download_result result) {
@@ -239,53 +243,50 @@ class cloud_cache_data_persistence : public data_persistence {
239243
cloud_io::remote* remote,
240244
cloud_storage_clients::bucket_name bucket,
241245
cloud_storage_clients::object_key prefix,
246+
config::binding<size_t> chunk_size,
242247
cloud_io::group_id gid)
243248
: _cache(cache)
244249
, _remote(remote)
245250
, _bucket(std::move(bucket))
246251
, _prefix(std::move(prefix))
252+
, _chunk_size(std::move(chunk_size))
247253
, _gid(gid) {}
248254

249255
ss::future<optional_pointer<random_access_file_reader>>
250256
open_random_access_reader(
251-
internal::file_handle h, uint64_t /*file_size*/) override {
257+
internal::file_handle h, uint64_t file_size) override {
252258
_as.check();
253259
auto _ = _gate.hold();
254260
auto filename = internal::sst_file_name(h);
255-
auto key = cache_key(filename);
256261

257-
auto root = make_cloud_rtc(_as);
258-
while (true) {
259-
auto reader = co_await open_cached_reader(key);
260-
if (reader) {
261-
co_return reader;
262-
}
262+
auto cached = co_await open_cached_reader(cache_key(filename));
263+
if (cached) {
264+
// The whole object is already local -- e.g. just written through
265+
// by the cache-staged writer. Serve it directly rather than
266+
// re-fetching it from the cloud in chunks.
267+
co_return cached;
268+
}
263269

264-
auto dl_fut = co_await ss::coroutine::as_future(
265-
_remote->download_stream(
266-
{
267-
.bucket = _bucket,
268-
.key = cloud_key(filename),
269-
.parent_rtc = root,
270-
},
271-
[this,
272-
&key](uint64_t content_length, ss::input_stream<char> stream) {
273-
return save_to_cache(
274-
content_length, std::move(stream), key);
275-
},
276-
"SST file download",
277-
/*acquire_hydration_units=*/true,
278-
/*byte_range=*/std::nullopt,
279-
/*throttle_metric_ms_cb=*/{},
280-
_gid));
281-
if (dl_fut.failed()) {
282-
throw_as_lsm_ex(
283-
dl_fut.get_exception(), "error downloading file");
284-
}
285-
if (!check_cloud_result(dl_fut.get())) {
286-
co_return std::nullopt;
287-
}
270+
// Hydrate chunks on demand. Chunk entries live in a sibling
271+
// subdirectory so they can never collide with the whole-file key
272+
// probed above.
273+
auto reader = co_await chunked_remote_file_reader::open(
274+
_cache,
275+
_remote,
276+
_bucket,
277+
cloud_key(filename),
278+
chunk_cache_prefix(filename),
279+
file_size,
280+
_chunk_size(),
281+
config::shard_local_cfg().cloud_storage_hydration_timeout_ms(),
282+
cloud_op_backoff(),
283+
_as,
284+
_gid);
285+
if (!reader) {
286+
co_return std::nullopt;
288287
}
288+
std::unique_ptr<random_access_file_reader> ptr = std::move(*reader);
289+
co_return ptr;
289290
}
290291

291292
ss::future<std::unique_ptr<sequential_file_writer>>
@@ -403,9 +404,9 @@ class cloud_cache_data_persistence : public data_persistence {
403404
co_return std::nullopt;
404405
}
405406
auto local_path = _cache->get_local_path(key);
406-
std::unique_ptr<random_access_file_reader> ptr;
407-
ptr = std::make_unique<disk_file_reader>(
408-
std::move(local_path), std::move(item->body));
407+
std::unique_ptr<random_access_file_reader> ptr
408+
= std::make_unique<disk_file_reader>(
409+
std::move(local_path), std::move(item->body));
409410
co_return ptr;
410411
} catch (const std::system_error& e) {
411412
if (e.code() == std::errc::no_such_file_or_directory) {
@@ -423,29 +424,16 @@ class cloud_cache_data_persistence : public data_persistence {
423424
}
424425
}
425426

426-
ss::future<uint64_t> save_to_cache(
427-
uint64_t content_length,
428-
ss::input_stream<char> input_stream,
429-
const std::filesystem::path& key) {
430-
std::exception_ptr ex;
431-
try {
432-
auto reservation = co_await _cache->reserve_space(
433-
content_length, 1);
434-
co_await _cache->put(key, input_stream, reservation);
435-
} catch (...) {
436-
ex = std::current_exception();
437-
}
438-
co_await input_stream.close();
439-
if (ex) {
440-
std::rethrow_exception(ex);
441-
}
442-
co_return content_length;
443-
}
444-
445427
std::filesystem::path cache_key(std::string_view name) {
446428
return std::filesystem::path("lsm") / _prefix() / name;
447429
}
448430

431+
// Where the chunked reader caches its chunks for `name`: a sibling of the
432+
// whole-file key so the two never share a path.
433+
std::filesystem::path chunk_cache_prefix(std::string_view name) {
434+
return cache_key(fmt::format("{}.chunks", name));
435+
}
436+
449437
cloud_storage_clients::object_key cloud_key(std::string_view name) {
450438
return join_cloud_key(_prefix, name);
451439
}
@@ -454,6 +442,7 @@ class cloud_cache_data_persistence : public data_persistence {
454442
cloud_io::remote* _remote;
455443
cloud_storage_clients::bucket_name _bucket;
456444
cloud_storage_clients::object_key _prefix;
445+
config::binding<size_t> _chunk_size;
457446
cloud_io::group_id _gid;
458447
ss::abort_source _as;
459448
ss::gate _gate;
@@ -600,9 +589,15 @@ ss::future<std::unique_ptr<data_persistence>> open_cloud_cache_data_persistence(
600589
cloud_io::remote* remote,
601590
cloud_storage_clients::bucket_name bucket,
602591
cloud_storage_clients::object_key prefix,
592+
config::binding<size_t> sst_chunk_size,
603593
cloud_io::group_id gid) {
604594
co_return std::make_unique<cloud_cache_data_persistence>(
605-
cache, remote, std::move(bucket), std::move(prefix), gid);
595+
cache,
596+
remote,
597+
std::move(bucket),
598+
std::move(prefix),
599+
std::move(sst_chunk_size),
600+
gid);
606601
}
607602

608603
ss::future<std::unique_ptr<metadata_persistence>>

src/v/lsm/io/cloud_cache_persistence.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,22 @@
1313
#include "cloud_io/cache_service.h"
1414
#include "cloud_io/remote.h"
1515
#include "cloud_storage_clients/types.h"
16+
#include "config/property.h"
1617
#include "lsm/io/persistence.h"
1718

1819
namespace lsm::io {
1920

2021
/// Open a data persistence backed by the cloud cache and cloud storage.
21-
/// `gid` tags every cloud operation this persistence issues for the
22-
/// cloud_io scheduler; defaults to default_group.
22+
/// SST files are read from object storage in chunks of `sst_chunk_size`
23+
/// bytes, hydrated into the cache on demand; the binding is read per open so
24+
/// the size can change at runtime. `gid` tags every cloud operation this
25+
/// persistence issues for the cloud_io scheduler; defaults to default_group.
2326
ss::future<std::unique_ptr<data_persistence>> open_cloud_cache_data_persistence(
2427
cloud_io::cache* cache,
2528
cloud_io::remote* remote,
2629
cloud_storage_clients::bucket_name bucket,
2730
cloud_storage_clients::object_key prefix,
31+
config::binding<size_t> sst_chunk_size,
2832
cloud_io::group_id gid = cloud_io::group_id::default_group);
2933

3034
/// Open a metadata persistence backed by cloud storage. `gid` tags every

src/v/lsm/io/tests/persistence_test.cc

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ TEST_P(PersistenceTest, CanWriteAndReadAFile) {
6868
w->append(iobuf::from("world")).get();
6969
}
7070
{
71-
auto maybe_r = persistence->open_random_access_reader({}, 0).get();
71+
auto maybe_r
72+
= persistence->open_random_access_reader({}, /*file_size=*/10).get();
7273
ASSERT_TRUE(bool(maybe_r));
7374
auto r = std::move(*maybe_r);
7475
auto _ = ss::defer([&r] { r->close().get(); });
@@ -114,7 +115,8 @@ TEST_P(PersistenceTest, DuplicateWritePreservesOriginal) {
114115
} catch (...) {
115116
// Expected for backends that use O_EXCL.
116117
}
117-
auto maybe_r = persistence->open_random_access_reader({}, 0).get();
118+
auto maybe_r
119+
= persistence->open_random_access_reader({}, /*file_size=*/13).get();
118120
ASSERT_TRUE(bool(maybe_r));
119121
auto r = std::move(*maybe_r);
120122
auto _ = ss::defer([&r] { r->close().get(); });
@@ -124,7 +126,10 @@ TEST_P(PersistenceTest, DuplicateWritePreservesOriginal) {
124126
}
125127

126128
TEST_P(PersistenceTest, ReadNonExisting) {
127-
auto maybe_r = persistence->open_random_access_reader({}, 0).get();
129+
// A nonzero size is required by backends that probe the object on open;
130+
// the file does not exist, so the result is still nullopt.
131+
auto maybe_r
132+
= persistence->open_random_access_reader({}, /*file_size=*/16).get();
128133
EXPECT_FALSE(bool(maybe_r));
129134
}
130135

@@ -156,7 +161,7 @@ TEST_P(PersistenceTest, RandomAccessReaderComprehensive) {
156161
}
157162

158163
// Open reader for all tests
159-
auto maybe_r = persistence->open_random_access_reader({}, 0).get();
164+
auto maybe_r = persistence->open_random_access_reader({}, file_size).get();
160165
ASSERT_TRUE(bool(maybe_r));
161166
auto r = std::move(*maybe_r);
162167

@@ -359,7 +364,8 @@ INSTANTIATE_TEST_SUITE_P(
359364
&cache->local(),
360365
&sr->remote.local(),
361366
fixture->bucket_name,
362-
cloud_storage_clients::object_key("test-prefix"));
367+
cloud_storage_clients::object_key("test-prefix"),
368+
config::mock_binding<size_t>(1_MiB));
363369

364370
co_return std::make_unique<mock_cloud_cache_data_persistence>(
365371
std::move(fixture),

0 commit comments

Comments
 (0)