mrhhsg commented on code in PR #68032:
URL: https://github.com/apache/doris/pull/68032#discussion_r4279089206
##########
be/src/exec/spill/spill_file_reader.cpp:
##########
@@ -62,77 +68,177 @@ SpillFileReader::SpillFileReader(RuntimeState* state,
RuntimeProfile* profile,
_read_file_size = get_counter(profile::SPILL_READ_FILE_BYTES);
_read_rows_count = get_counter(profile::SPILL_READ_ROWS);
_read_file_count = get_counter(profile::SPILL_READ_FILE_COUNT);
+ // Optional: older profiles may not register it.
+ _remote_read_requests =
custom_profile->get_counter(profile::SPILL_REMOTE_READ_REQUESTS);
+ if (_is_remote) {
+ _coalesce_bytes = static_cast<size_t>(std::max<int64_t>(
+ 0,
+ std::min(config::spill_s3_read_coalesce_bytes,
state->spill_buffer_size_bytes())));
+ }
+}
+
+void SpillFileReader::_record_read(size_t bytes_read) {
+ COUNTER_UPDATE(_read_file_size, bytes_read);
+
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_read_bytes(bytes_read);
+ if (_is_remote) {
+ // One successful read_at() is one logical GET. Retries inside the
object storage client
+ // and reads that finally failed are not counted (see
RemoteWriteStats).
+ if (_remote_read_requests != nullptr) {
+ COUNTER_UPDATE(_remote_read_requests, 1);
+ }
+ if (_resource_ctx) {
+
_resource_ctx->io_context()->update_spill_read_bytes_from_remote_storage(bytes_read);
+ _resource_ctx->io_context()->update_spill_remote_read_requests(1);
+ }
+
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_remote_read(bytes_read,
1);
+ } else if (_resource_ctx) {
+
_resource_ctx->io_context()->update_spill_read_bytes_from_local_storage(bytes_read);
+ }
}
Status SpillFileReader::open() {
if (_is_open || _part_count == 0) {
return Status::OK();
}
- RETURN_IF_ERROR(_open_part(0));
+ RETURN_IF_ERROR(_open_part(0, true));
_is_open = true;
return Status::OK();
}
-Status SpillFileReader::_open_part(size_t part_index) {
+Status SpillFileReader::_open_part(size_t part_index, bool fetch_small_part) {
_close_current_part();
_current_part_index = part_index;
_part_opened = true;
std::string part_path = _spill_dir + "/" + std::to_string(part_index);
- SCOPED_TIMER(_read_file_timer);
COUNTER_UPDATE(_read_file_count, 1);
- RETURN_IF_ERROR(io::global_local_filesystem()->open_file(part_path,
&_file_reader));
-
- size_t file_size = _file_reader->size();
- DCHECK(file_size >= 16); // max_sub_block_size + block count
-
- Slice result((char*)&_part_block_count, sizeof(size_t));
+ auto fs = _data_dir != nullptr ? _data_dir->fs() :
io::global_local_filesystem();
+ if (fs == nullptr) {
+ return Status::InternalError("spill store {} is not ready",
_data_dir->path());
+ }
+ io::FileReaderOptions opts;
+ opts.cache_type = io::FileCachePolicy::NO_CACHE;
+ // The writer recorded the part size; on object storage this saves a HEAD
request.
+ opts.file_size = _part_sizes[part_index];
+ {
+ SCOPED_TIMER(_read_file_timer);
+ RETURN_IF_ERROR(fs->open_file(part_path, &_file_reader, &opts));
+ }
+ RETURN_IF_ERROR(_read_footer(_file_reader->size(), fetch_small_part));
+ _part_read_block_index = 0;
+ return Status::OK();
+}
- // read block count
- size_t bytes_read = 0;
- RETURN_IF_ERROR(_file_reader->read_at(file_size - sizeof(size_t), result,
&bytes_read));
- DCHECK(bytes_read == 8);
-
- // read max sub block size
- bytes_read = 0;
- result.data = (char*)&_part_max_sub_block_size;
- RETURN_IF_ERROR(_file_reader->read_at(file_size - sizeof(size_t) * 2,
result, &bytes_read));
- DCHECK(bytes_read == 8);
-
- // The buffer is used for two purposes:
- // 1. Reading the block start offsets array (needs _part_block_count *
sizeof(size_t) bytes)
- // 2. Reading a single block's serialized data (needs up to
_part_max_sub_block_size bytes)
- // We must ensure the buffer is large enough for either case, so take the
maximum.
- size_t buff_size = std::max(_part_block_count * sizeof(size_t),
_part_max_sub_block_size);
- if (buff_size > _read_buff.size()) {
- _read_buff.reserve(buff_size);
- }
-
- // Read the block start offsets array from the end of the file.
- // The file layout (from end backwards) is:
+Status SpillFileReader::_read_footer(size_t file_size, bool fetch_small_part) {
+ // The part layout (from the end backwards) is:
// [block count (size_t)]
// [max sub block size (size_t)]
// [block start offsets array (_part_block_count * size_t)]
- // So the offsets array starts at:
- // file_size - (_part_block_count + 2) * sizeof(size_t)
- size_t read_offset = file_size - (_part_block_count + 2) * sizeof(size_t);
- result.data = _read_buff.data();
- result.size = _part_block_count * sizeof(size_t);
+ // [serialized blocks]
+ constexpr size_t kFooterTailBytes = 2 * sizeof(size_t);
+ // Enough for the offsets of a few thousand blocks, so the footer usually
takes one GET.
+ constexpr size_t kRemoteFooterProbeBytes = 64 * 1024;
+ if (file_size < kFooterTailBytes) {
+ return Status::InternalError("spill part {} is too small: {} bytes",
+ _file_reader->path().native(), file_size);
+ }
- RETURN_IF_ERROR(_file_reader->read_at(read_offset, result, &bytes_read));
- DCHECK(bytes_read == _part_block_count * sizeof(size_t));
+ // Without coalescing (local disk) the footer is read exactly. Otherwise a
larger tail is
+ // read in one request, or the whole part when it fits in one coalesced
read.
+ size_t probe_size = kFooterTailBytes;
+ if (_coalesce_bytes > 0) {
+ probe_size = fetch_small_part && file_size <= _coalesce_bytes
Review Comment:
Resolving: the fix from 559acdf1aa0 is in the current head. `_read_footer`
bounds the probe by `std::max(_coalesce_bytes, kFooterTailBytes)` (and by the
part size), and fetches the rest of a larger offset array with one more read.
##########
be/src/exec/spill/spill_file_manager.cpp:
##########
@@ -153,14 +199,16 @@ void SpillFileManager::stop() {
// The GC thread may observe the stop latch before processing a recently
queued failed deletion.
// Retry the pending directories after the thread exits; later failures
get one final retry in
// the destructor.
- _retry_pending_query_spill_directories();
+ _retry_pending_spill_directories();
}
Status SpillFileManager::init() {
LOG(INFO) << "init spill stream manager";
RETURN_IF_ERROR(_init_spill_store_map());
+ _remote_upload_budget =
+
std::make_shared<SpillRemoteUploadBudget>(config::spill_s3_max_inflight_upload_bytes);
- for (const auto& [path, store] : _spill_store_map) {
+ for (auto* store : _local_stores) {
Review Comment:
Resolving as a documented limitation (see "Known limitations" in the PR
description, which links this thread). Rediscovering residual objects would
need a listing of this BE's prefix at startup; the startup sweep and the
recycler sweep were removed on reviewer request, and the lifetime of objects
left by an abrupt exit is the bucket lifecycle rule's. The reported value is
documented as best-effort and not a billing source.
##########
be/src/exec/spill/spill_file_writer.cpp:
##########
@@ -90,42 +146,208 @@ Status SpillFileWriter::_close_current_part(const
std::shared_ptr<SpillFile>& sp
_part_meta.append((const char*)&_part_max_sub_block_size,
sizeof(_part_max_sub_block_size));
_part_meta.append((const char*)&_part_written_blocks,
sizeof(_part_written_blocks));
- {
+ int64_t meta_size = _part_meta.size();
+ // The footer must always be written so that the part can be closed;
account it
+ // without checking the capacity limit.
+ Status status = _data_dir->try_reserve(meta_size, /*force=*/true);
+ if (status.ok()) {
SCOPED_TIMER(_write_file_timer);
- RETURN_IF_ERROR(_file_writer->append(_part_meta));
+ status = _append_to_part(Slice(_part_meta));
+ if (!status.ok()) {
+ _data_dir->release(meta_size);
+ }
}
- int64_t meta_size = _part_meta.size();
- _part_written_bytes += meta_size;
- COUNTER_UPDATE(_write_file_total_size, meta_size);
+ if (status.ok()) {
+ _part_written_bytes += meta_size;
+ COUNTER_UPDATE(_write_file_total_size, meta_size);
+ if (_resource_ctx) {
+ if (_data_dir->is_remote()) {
+
_resource_ctx->io_context()->update_spill_write_bytes_to_remote_storage(meta_size);
+ } else {
+
_resource_ctx->io_context()->update_spill_write_bytes_to_local_storage(meta_size);
+ }
+ }
+ if (_write_file_current_size) {
+ COUNTER_UPDATE(_write_file_current_size, meta_size);
+ }
+
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_write_bytes(meta_size);
+ // Incrementally update SpillFile's accounting so gc() can always
+ // decrement the correct amount, even if close() is never called.
+ if (spill_file) {
+ spill_file->update_written_bytes(meta_size);
+ }
+ }
+
+ // Close synchronously. Spilling already runs on a thread that may block
on every append
+ // (the pipeline execution thread running the revoke task, or the async
result-writer
+ // thread for Iceberg), and with the default part size the wait at a part
boundary (the
+ // tail of the uploads plus one CompleteMultipartUpload) is negligible
against the part
+ // itself, so overlapping it with the next part is not worth an async
close pipeline.
+ std::unique_ptr<io::FileWriter> writer = std::move(_file_writer);
+ MultipartUploadId upload = _multipart_upload_id(writer.get());
+ if (status.ok()) {
+ MonotonicStopWatch close_watch;
+ close_watch.start();
+ status = writer->close();
Review Comment:
Resolving, unchanged. `be/src/io/fs/s3_file_writer.{h,cpp}` are identical to
master in this PR (`git diff origin/master -- be/src/io/fs/s3_file_writer.cpp`
is empty), so `check_after_upload()` and its `DCHECK`s run for every S3 upload
on master already (loads, compaction, segment writes); spill does not add a new
assertion. In release builds the `IOError` is returned: the part close fails,
the multipart upload is aborted and the query fails with that error. Changing
the assertion policy of `S3FileWriter` for all its callers is a separate change.
##########
fe/fe-core/src/main/java/org/apache/doris/master/ReportHandler.java:
##########
@@ -185,6 +185,10 @@ public TMasterResult handleReport(TReportRequest request)
throws TException {
reportType = ReportType.DISK;
}
+ if (request.isSetRemoteSpillBytes()) {
+ backend.setRemoteSpillBytes(request.getRemoteSpillBytes());
Review Comment:
Resolving, unchanged, as explained above: the stale value lasts at most one
disk report interval (`report_disk_state_interval_seconds`, 30 s by default)
until the first report of the new process overwrites it, the disk capacity in
the same request has the same ordering, and the value is documented as
best-effort.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]