This is an automated email from the ASF dual-hosted git repository.

gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new a42928c5caf [fix](be) Avoid repeated peer connect failures (#67464)
a42928c5caf is described below

commit a42928c5caf1e064dbc4f6b7450430d49a1549e7
Author: deardeng <[email protected]>
AuthorDate: Tue Sep 8 16:02:14 2026 +0800

    [fix](be) Avoid repeated peer connect failures (#67464)
    
    Problem:
    Peer reads create an uncached BRPC client for every request. When a peer
    is unreachable, readers across tablets repeatedly connect to the same
    address with the default 2-second connect timeout and 10 retries.
    
    Impact:
    An unavailable cache peer can repeatedly delay reads before
    remote-storage fallback and generate unnecessary connection attempts and
    warning logs.
    
    Fix:
    - Add a BE-wide, address-level circuit breaker shared by peer readers.
    - Open the circuit after a configurable number of consecutive connection
    or RPC failures (3 by default) for a configurable cooldown (30 seconds
    by default).
    - Reject reads while the circuit is open, allow one probe after
    cooldown, and clear the failure state after a successful RPC.
    - Make uncached BRPC connect timeout and retry count configurable at the
    call site, and use a 200 ms timeout with no retries for peer reads.
    
    Test:
    Stop the peer test server and verify that four reads issue only three
    RPC attempts, with the final read rejected by the circuit breaker.
---
 be/src/cloud/config.cpp                            |  4 ++
 be/src/cloud/config.h                              |  3 ++
 be/src/io/cache/peer_file_cache_reader.cpp         | 63 ++++++++++++++++++++--
 be/src/util/brpc_client_cache.h                    |  7 +--
 .../cache/cached_remote_file_reader_peer_test.cpp  | 23 ++++++++
 5 files changed, 93 insertions(+), 7 deletions(-)

diff --git a/be/src/cloud/config.cpp b/be/src/cloud/config.cpp
index 9dcf4daa627..b8475ef888d 100644
--- a/be/src/cloud/config.cpp
+++ b/be/src/cloud/config.cpp
@@ -166,6 +166,10 @@ 
DEFINE_mInt64(file_cache_warmup_download_rate_limit_bytes_per_second, "104857600
 DEFINE_mInt64(peer_candidate_cleanup_interval_s, "3600"); // cleanup interval, 
1 hour
 DEFINE_mInt64(peer_candidate_expiry_s, "3600");           // candidate expiry, 
1 hour
 DEFINE_mInt32(peer_rpc_failure_eviction_threshold, "3");  // consecutive 
failures to evict
+// Consecutive connection or RPC failures to one peer address before opening 
its circuit.
+DEFINE_mInt32(cache_peer_read_failure_threshold, "3");
+// Seconds to reject reads to a peer address before allowing one recovery 
probe.
+DEFINE_mInt32(cache_peer_read_circuit_open_seconds, "30");
 DEFINE_mInt32(peer_all_miss_cooldown_threshold,
               "5"); // consecutive all-miss races to trigger cooldown
 DEFINE_mInt64(peer_all_miss_cooldown_duration_s, "300"); // cooldown duration, 
5 minutes
diff --git a/be/src/cloud/config.h b/be/src/cloud/config.h
index 14f5cab309d..9bc1b11bdc6 100644
--- a/be/src/cloud/config.h
+++ b/be/src/cloud/config.h
@@ -209,6 +209,9 @@ 
DECLARE_mInt64(file_cache_warmup_download_rate_limit_bytes_per_second);
 DECLARE_mInt64(peer_candidate_cleanup_interval_s);
 DECLARE_mInt64(peer_candidate_expiry_s);
 DECLARE_mInt32(peer_rpc_failure_eviction_threshold);
+// Address-level circuit breaker shared by peer reads across tablets.
+DECLARE_mInt32(cache_peer_read_failure_threshold);
+DECLARE_mInt32(cache_peer_read_circuit_open_seconds);
 DECLARE_mInt32(peer_all_miss_cooldown_threshold);
 DECLARE_mInt64(peer_all_miss_cooldown_duration_s);
 
diff --git a/be/src/io/cache/peer_file_cache_reader.cpp 
b/be/src/io/cache/peer_file_cache_reader.cpp
index 76cffab8d44..e72d084304d 100644
--- a/be/src/io/cache/peer_file_cache_reader.cpp
+++ b/be/src/io/cache/peer_file_cache_reader.cpp
@@ -17,6 +17,7 @@
 #include "io/cache/peer_file_cache_reader.h"
 
 #include <brpc/controller.h>
+#include <bthread/mutex.h>
 #include <butil/iobuf.h>
 #include <bvar/latency_recorder.h>
 #include <bvar/reducer.h>
@@ -25,8 +26,12 @@
 #include <glog/logging.h>
 
 #include <algorithm>
+#include <chrono>
+#include <mutex>
+#include <unordered_map>
 #include <utility>
 
+#include "cloud/config.h"
 #include "common/compiler_util.h" // IWYU pragma: keep
 #include "common/metrics/doris_metrics.h"
 #include "runtime/exec_env.h"
@@ -45,6 +50,51 @@ namespace doris::io {
 
 namespace {
 
+struct PeerConnectionHealth {
+    int32_t consecutive_failures = 0;
+    std::chrono::steady_clock::time_point circuit_open_until;
+    bool probe_in_flight = false;
+};
+
+// Entries are removed after a successful connection. Add expiry cleanup if 
permanently
+// unavailable peer addresses accumulate.
+bthread::Mutex peer_connection_health_mutex;
+std::unordered_map<std::string, PeerConnectionHealth> peer_connection_health;
+
+// Every true result must be paired with exactly one 
finish_peer_connection_attempt() call.
+// Use Defer at the call site so every return path completes the attempt.
+bool peer_connection_circuit_allows(const std::string& address) {
+    std::unique_lock<bthread::Mutex> lock(peer_connection_health_mutex);
+    auto it = peer_connection_health.find(address);
+    if (it == peer_connection_health.end() ||
+        it->second.circuit_open_until == std::chrono::steady_clock::time_point 
{}) {
+        return true;
+    }
+    auto& health = it->second;
+    if (std::chrono::steady_clock::now() < health.circuit_open_until || 
health.probe_in_flight) {
+        return false;
+    }
+    health.probe_in_flight = true;
+    return true;
+}
+
+// Completes an attempt admitted by peer_connection_circuit_allows().
+void finish_peer_connection_attempt(const std::string& address, bool 
succeeded) {
+    std::unique_lock<bthread::Mutex> lock(peer_connection_health_mutex);
+    if (succeeded) {
+        peer_connection_health.erase(address);
+        return;
+    }
+    auto& health = peer_connection_health[address];
+    health.probe_in_flight = false;
+    ++health.consecutive_failures;
+    if (health.consecutive_failures >= std::max(1, 
config::cache_peer_read_failure_threshold)) {
+        health.circuit_open_until =
+                std::chrono::steady_clock::now() +
+                std::chrono::seconds(std::max(0, 
config::cache_peer_read_circuit_open_seconds));
+    }
+}
+
 struct ExpectedPeerFetch {
     std::vector<FileBlock::Range> expected_ranges;
     std::vector<FileBlock::Range> pending_ranges;
@@ -212,15 +262,19 @@ Status PeerFileCacheReader::fetch_blocks(const 
std::vector<FileBlockSPtr>& block
         }
     }
     std::string brpc_addr = get_host_port(realhost, port);
-    Status st = Status::OK();
+    if (!peer_connection_circuit_allows(brpc_addr)) {
+        return Status::RpcError<false>("Peer connection circuit is open for 
{}", brpc_addr);
+    }
+    bool transport_succeeded = false;
+    Defer finish_connection_attempt {
+            [&] { finish_peer_connection_attempt(brpc_addr, 
transport_succeeded); }};
     std::shared_ptr<PBackendService_Stub> brpc_stub =
             
ExecEnv::GetInstance()->brpc_internal_client_cache()->get_new_client_no_cache(
-                    brpc_addr);
+                    brpc_addr, "", "", "", 200, 0);
     if (!brpc_stub) {
         peer_cache_reader_failed_counter << 1;
         LOG(WARNING) << "failed to get brpc stub " << brpc_addr;
-        st = Status::RpcError<false>("Address {} is wrong", brpc_addr);
-        return st;
+        return Status::RpcError<false>("Address {} is wrong", brpc_addr);
     }
 
     size_t filled = 0;
@@ -246,6 +300,7 @@ Status PeerFileCacheReader::fetch_blocks(const 
std::vector<FileBlockSPtr>& block
     if (cntl.Failed()) {
         return Status::RpcError<false>(cntl.ErrorText());
     }
+    transport_succeeded = true;
     if (resp.has_status()) {
         Status st2 = Status::create<false>(resp.status());
         LOG_EVERY_N(WARNING, 1000) << "peer cache read failed, status=" << 
st2.msg();
diff --git a/be/src/util/brpc_client_cache.h b/be/src/util/brpc_client_cache.h
index 510fbb4afa7..9818b5247e5 100644
--- a/be/src/util/brpc_client_cache.h
+++ b/be/src/util/brpc_client_cache.h
@@ -253,7 +253,8 @@ public:
     std::shared_ptr<T> get_new_client_no_cache(const std::string& host_port,
                                                const std::string& protocol = 
"",
                                                const std::string& 
connection_type = "",
-                                               const std::string& 
connection_group = "") {
+                                               const std::string& 
connection_group = "",
+                                               int connect_timeout_ms = 2000, 
int max_retry = 10) {
         brpc::ChannelOptions options;
         Status status = 
doris::client::configure_brpc_channel_options(&options);
         if (!status.ok()) {
@@ -276,9 +277,9 @@ public:
         }
         // Add random connection id to connection_group to make sure use new 
socket
         options.connection_group += 
std::to_string(_connection_id.fetch_add(1));
-        options.connect_timeout_ms = 2000;
+        options.connect_timeout_ms = connect_timeout_ms;
         options.timeout_ms = 2000;
-        options.max_retry = 10;
+        options.max_retry = max_retry;
 
         std::unique_ptr<FailureDetectChannel> channel(new 
FailureDetectChannel());
         int ret_code = 0;
diff --git a/be/test/io/cache/cached_remote_file_reader_peer_test.cpp 
b/be/test/io/cache/cached_remote_file_reader_peer_test.cpp
index 0e8b79e4d1b..879f1607963 100644
--- a/be/test/io/cache/cached_remote_file_reader_peer_test.cpp
+++ b/be/test/io/cache/cached_remote_file_reader_peer_test.cpp
@@ -64,6 +64,8 @@ bool 
test_try_reject_if_queue_timed_out(std::chrono::steady_clock::time_point en
 
 namespace doris::io {
 
+extern bvar::Adder<uint64_t> peer_cache_reader_read_counter;
+
 namespace {
 
 constexpr size_t kPeerTestBlockSize = 4;
@@ -3491,4 +3493,25 @@ TEST_F(CrossCGWinnerRaceTest, 
cross_cg_peer_wins_race_records_timer_metrics) {
             << "cross_cg_peer_io_timer should be non-zero after cross-CG peer 
win";
 }
 
+TEST_F(CachedRemoteFileReaderPeerTest, 
peer_connection_failures_open_address_circuit) {
+    MockPeerCacheService service("abcd");
+    brpc::Server server;
+    const auto addr = start_peer_test_server(&server, &service);
+    stop_peer_test_server(&server);
+
+    const std::vector<FileBlockSPtr> blocks {
+            create_manual_peer_test_block("peer_connection_failure", 0, 
kPeerTestBlockSize)};
+    const uint64_t rpc_count_before = 
peer_cache_reader_read_counter.get_value();
+
+    for (int i = 0; i < 4; ++i) {
+        PeerFileCacheReader peer_reader(Path("peer_connection_failure"), true, 
"127.0.0.1",
+                                        addr.port);
+        PeerFetchResult result;
+        const auto st = peer_reader.fetch_blocks(blocks, &result, 
kPeerTestBlockSize, nullptr);
+        EXPECT_TRUE(st.is<ErrorCode::THRIFT_RPC_ERROR>()) << st;
+    }
+
+    EXPECT_EQ(peer_cache_reader_read_counter.get_value() - rpc_count_before, 
3);
+}
+
 } // namespace doris::io


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to