Skip to content

Commit 0197908

Browse files
authored
watchdog: alert on ET_NET thread stalls beyond threshold (#12524)
Run a watchdog thread to find blocking events.
1 parent a57a824 commit 0197908

File tree

8 files changed

+219
-1
lines changed

8 files changed

+219
-1
lines changed

doc/admin-guide/files/records.yaml.en.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,18 @@ Thread Variables
419419

420420
This option only has an affect when |TS| has been compiled with ``--enable-hwloc``.
421421

422+
.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.timeout_ms INT 0
423+
:units: milliseconds
424+
425+
Set the timeout for the exec thread watchdog in milliseconds. If an exec thread
426+
does not heartbeat within this time period, the watchdog will log a warning message.
427+
If this value is zero, the watchdog is disabled.
428+
429+
The default of this watchdot timeout is set to 0 (disabled) for ATS 10.2 for
430+
compatibility. We recommend that administrators set a reasonable
431+
value, such as 1000, for production configurations, in order to
432+
catch hung plugins, or server overload scenarios.
433+
422434
.. ts:cv:: CONFIG proxy.config.system.file_max_pct FLOAT 0.9
423435
424436
Set the maximum number of file handles for the traffic_server process as a percentage of the fs.file-max proc value in Linux. The default is 90%.

include/iocore/eventsystem/EThread.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "iocore/eventsystem/PriorityEventQueue.h"
3434
#include "iocore/eventsystem/ProtectedQueue.h"
3535
#include "tsutil/Histogram.h"
36+
#include "iocore/eventsystem/Watchdog.h"
3637

3738
#if TS_USE_HWLOC
3839
struct hwloc_obj;
@@ -584,6 +585,8 @@ class EThread : public Thread
584585

585586
Metrics metrics;
586587

588+
Watchdog::Heartbeat heartbeat_state;
589+
587590
private:
588591
void cons_common();
589592
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/** @file
2+
3+
A watchdog for event loops
4+
5+
Each event thread advertises its current state through a lightweight
6+
"heartbeat" struct: the thread publishes the timestamps for the most recent
7+
sleep/wake pair along with a monotonically increasing sequence number.
8+
`Watchdog::Monitor`, started from `traffic_server.cc`, runs in its own
9+
`std::thread` and periodically scans those heartbeats; if a thread has been
10+
awake longer than the configured timeout it emits a warning (timeout values
11+
come from `proxy.config.exec_thread.watchdog.timeout_ms`, where 0 disables
12+
the monitor). The monitor never touches event-system locks, keeping the
13+
runtime overhead in the hot loop confined to a handful of atomic updates.
14+
15+
@section license License
16+
17+
Licensed to the Apache Software Foundation (ASF) under one
18+
or more contributor license agreements. See the NOTICE file
19+
distributed with this work for additional information
20+
regarding copyright ownership. The ASF licenses this file
21+
to you under the Apache License, Version 2.0 (the
22+
"License"); you may not use this file except in compliance
23+
with the License. You may obtain a copy of the License at
24+
25+
http://www.apache.org/licenses/LICENSE-2.0
26+
27+
Unless required by applicable law or agreed to in writing, software
28+
distributed under the License is distributed on an "AS IS" BASIS,
29+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30+
See the License for the specific language governing permissions and
31+
limitations under the License.
32+
33+
*/
34+
35+
#pragma once
36+
37+
#include <atomic>
38+
#include <chrono>
39+
#include <vector>
40+
#include <thread>
41+
42+
class EThread;
43+
44+
namespace Watchdog
45+
{
46+
struct Heartbeat {
47+
std::atomic<std::chrono::time_point<std::chrono::steady_clock>> last_sleep{
48+
std::chrono::steady_clock::time_point::min()}; // set right before sleeping (e.g. before epoll_wait)
49+
std::atomic<std::chrono::time_point<std::chrono::steady_clock>> last_wake{
50+
std::chrono::steady_clock::time_point::min()}; // set right after waking from sleep (e.g. epoll_wait returns)
51+
std::atomic<uint64_t> seq{0}; // increment on each loop - used to deduplicate warnings
52+
std::atomic<uint64_t> warned_seq{0}; // last seq we logged a warning about
53+
};
54+
55+
class Monitor
56+
{
57+
public:
58+
explicit Monitor(EThread *threads[], size_t n_threads, std::chrono::milliseconds timeout_ms);
59+
~Monitor();
60+
Monitor() = delete;
61+
62+
private:
63+
const std::vector<EThread *> _threads;
64+
std::thread _watchdog_thread;
65+
const std::chrono::milliseconds _timeout;
66+
std::atomic<bool> _shutdown = false;
67+
void monitor_loop() const;
68+
};
69+
70+
} // namespace Watchdog

src/iocore/eventsystem/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ add_library(
3535
ConfigProcessor.cc
3636
RecRawStatsImpl.cc
3737
RecProcess.cc
38+
Watchdog.cc
3839
)
3940
add_library(ts::inkevent ALIAS inkevent)
4041

src/iocore/eventsystem/UnixEThread.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,17 @@ EThread::execute_regular()
304304
ink_hrtime post_drain = ink_get_hrtime();
305305
ink_hrtime drain_queue = post_drain - loop_start_time;
306306

307+
// watchdog kick - pre-sleep
308+
// Relaxed store because this EThread is the only writer and the watchdog only needs a coherent timestamp.
309+
this->heartbeat_state.last_sleep.store(std::chrono::steady_clock::now(), std::memory_order_relaxed);
310+
307311
tail_cb->waitForActivity(sleep_time);
308312

313+
// watchdog kick - post-wake
314+
// Relaxed store/fetch because the monitor thread is the single reader and per-field coherence is sufficient.
315+
this->heartbeat_state.last_wake.store(std::chrono::steady_clock::now(), std::memory_order_relaxed);
316+
this->heartbeat_state.seq.fetch_add(1, std::memory_order_relaxed);
317+
309318
// loop cleanup
310319
loop_finish_time = ink_get_hrtime();
311320
// @a delta can be negative due to time of day adjustments (which apparently happen quite frequently). I

src/iocore/eventsystem/Watchdog.cc

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/** @file
2+
3+
A watchdog for event loops
4+
5+
@section license License
6+
7+
Licensed to the Apache Software Foundation (ASF) under one
8+
or more contributor license agreements. See the NOTICE file
9+
distributed with this work for additional information
10+
regarding copyright ownership. The ASF licenses this file
11+
to you under the Apache License, Version 2.0 (the
12+
"License"); you may not use this file except in compliance
13+
with the License. You may obtain a copy of the License at
14+
15+
http://www.apache.org/licenses/LICENSE-2.0
16+
17+
Unless required by applicable law or agreed to in writing, software
18+
distributed under the License is distributed on an "AS IS" BASIS,
19+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+
See the License for the specific language governing permissions and
21+
limitations under the License.
22+
23+
*/
24+
25+
#include "iocore/eventsystem/Watchdog.h"
26+
#include "iocore/eventsystem/EThread.h"
27+
#include "tscore/Diags.h"
28+
#include "tscore/ink_assert.h"
29+
#include "tscore/ink_thread.h"
30+
#include "tsutil/DbgCtl.h"
31+
32+
#include <atomic>
33+
#include <chrono>
34+
#include <thread>
35+
#include <functional>
36+
37+
namespace Watchdog
38+
{
39+
40+
DbgCtl dbg_ctl_watchdog("watchdog");
41+
42+
Monitor::Monitor(EThread *threads[], size_t n_threads, std::chrono::milliseconds timeout_ms)
43+
: _threads(threads, threads + n_threads), _timeout{timeout_ms}
44+
{
45+
// Precondition: timeout_ms must be > 0. A timeout of 0 indicates the watchdog is disabled
46+
// and the caller should not instantiate the Monitor (see traffic_server.cc).
47+
ink_release_assert(timeout_ms.count() > 0);
48+
_watchdog_thread = std::thread(std::bind_front(&Monitor::monitor_loop, this));
49+
}
50+
51+
Monitor::~Monitor()
52+
{
53+
_shutdown.store(true, std::memory_order_release);
54+
_watchdog_thread.join();
55+
}
56+
57+
void
58+
Monitor::monitor_loop() const
59+
{
60+
// Divide by a floating point 2 to avoid truncation to zero.
61+
auto sleep_time = _timeout / 2.0;
62+
ink_release_assert(sleep_time.count() > 0);
63+
Dbg(dbg_ctl_watchdog, "Starting watchdog with timeout %" PRIu64 " ms on %zu threads. sleep_time = %" PRIu64 " us",
64+
_timeout.count(), _threads.size(), std::chrono::duration_cast<std::chrono::microseconds>(sleep_time).count());
65+
66+
ink_set_thread_name("[WATCHDOG]");
67+
68+
while (!_shutdown.load(std::memory_order_acquire)) {
69+
std::chrono::time_point<std::chrono::steady_clock> now = std::chrono::steady_clock::now();
70+
for (size_t i = 0; i < _threads.size(); ++i) {
71+
EThread *t = _threads[i];
72+
// Relaxed load: each heartbeat field has a single writer (its EThread) so per-object coherence suffices.
73+
std::chrono::time_point<std::chrono::steady_clock> last_sleep = t->heartbeat_state.last_sleep.load(std::memory_order_relaxed);
74+
if (last_sleep == std::chrono::steady_clock::time_point::min()) {
75+
// initial value sentinel - event loop hasn't started
76+
continue;
77+
}
78+
// Same reasoning for relaxed load on wake timestamp.
79+
std::chrono::time_point<std::chrono::steady_clock> last_wake = t->heartbeat_state.last_wake.load(std::memory_order_relaxed);
80+
81+
if (last_wake == std::chrono::steady_clock::time_point::min() || last_wake < last_sleep) {
82+
// not yet woken from last sleep
83+
continue;
84+
}
85+
86+
auto awake_duration = now - last_wake;
87+
if (awake_duration > _timeout) {
88+
// Monitor thread is the sole reader (and warned_seq writer), so relaxed accesses are race-free.
89+
uint64_t seq = t->heartbeat_state.seq.load(std::memory_order_relaxed);
90+
uint64_t warned_seq = t->heartbeat_state.warned_seq.load(std::memory_order_relaxed);
91+
if (warned_seq < seq) {
92+
// Warn once per loop iteration
93+
Warning("Watchdog: [ET_NET %zu] has been awake for %" PRIu64 " ms", i,
94+
std::chrono::duration_cast<std::chrono::milliseconds>(awake_duration).count());
95+
t->heartbeat_state.warned_seq.store(seq, std::memory_order_relaxed);
96+
}
97+
}
98+
}
99+
100+
std::this_thread::sleep_for(sleep_time);
101+
}
102+
Dbg(dbg_ctl_watchdog, "Stopping watchdog");
103+
}
104+
} // namespace Watchdog

src/records/RecordsConfig.cc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1529,7 +1529,12 @@ static constexpr RecordElement RecordsConfig[] =
15291529
{RECT_CONFIG, "proxy.config.io_uring.wq_workers_unbounded", RECD_INT, "0", RECU_NULL, RR_NULL, RECC_NULL, nullptr, RECA_NULL},
15301530
{RECT_CONFIG, "proxy.config.aio.mode", RECD_STRING, "auto", RECU_DYNAMIC, RR_NULL, RECC_STR, "(auto|io_uring|thread)", RECA_NULL},
15311531
#endif
1532-
1532+
//###########
1533+
//#
1534+
//# Thread watchdog
1535+
//#
1536+
//###########
1537+
{RECT_CONFIG, "proxy.config.exec_thread.watchdog.timeout_ms", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-10000]", RECA_NULL}
15331538
};
15341539
// clang-format on
15351540

src/traffic_server/traffic_server.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include "iocore/aio/AIO.h"
3434
#include "iocore/cache/Store.h"
35+
#include "iocore/eventsystem/Watchdog.h"
3536
#include "tscore/TSSystemState.h"
3637
#include "tscore/Version.h"
3738
#include "tscore/ink_platform.h"
@@ -214,6 +215,8 @@ int cmd_block = 0;
214215
// -1: cache is already initialized, don't delay.
215216
int delay_listen_for_cache = 0;
216217

218+
std::unique_ptr<Watchdog::Monitor> watchdog = nullptr;
219+
217220
ArgumentDescription argument_descriptions[] = {
218221
{"net_threads", 'n', "Number of Net Threads", "I", &num_of_net_threads, "PROXY_NET_THREADS", nullptr },
219222
{"udp_threads", 'U', "Number of UDP Threads", "I", &num_of_udp_threads, "PROXY_UDP_THREADS", nullptr },
@@ -267,6 +270,9 @@ struct AutoStopCont : public Continuation {
267270
int
268271
mainEvent(int /* event */, Event * /* e */)
269272
{
273+
// Stop the watchdog before shutting threads down
274+
watchdog.reset();
275+
270276
TSSystemState::stop_ssl_handshaking();
271277

272278
APIHook *hook = g_lifecycle_hooks->get(TS_LIFECYCLE_SHUTDOWN_HOOK);
@@ -2131,6 +2137,14 @@ main(int /* argc ATS_UNUSED */, const char **argv)
21312137
RecRegisterConfigUpdateCb("proxy.config.dump_mem_info_frequency", init_memory_tracker, nullptr);
21322138
init_memory_tracker(nullptr, RECD_NULL, RecData(), nullptr);
21332139

2140+
// Start the watchdog
2141+
int watchdog_timeout_ms = RecGetRecordInt("proxy.config.exec_thread.watchdog.timeout_ms").value_or(0);
2142+
if (watchdog_timeout_ms > 0) {
2143+
watchdog = std::make_unique<Watchdog::Monitor>(eventProcessor.thread_group[ET_NET]._thread,
2144+
static_cast<size_t>(eventProcessor.thread_group[ET_NET]._count),
2145+
std::chrono::milliseconds{watchdog_timeout_ms});
2146+
}
2147+
21342148
{
21352149
auto s{RecGetRecordStringAlloc("proxy.config.diags.debug.client_ip")};
21362150
if (auto p{ats_as_c_str(s)}; p) {

0 commit comments

Comments
 (0)