Skip to content

Commit 50d1023

Browse files
dgoffredoMongoDB Bot
authored andcommitted
SERVER-104413: admission rate limiter metrics (#36574)
GitOrigin-RevId: 13fd645d22cda2d4854f22e6697f60efdd6d0f5b
1 parent 1bbedc9 commit 50d1023

16 files changed

+510
-157
lines changed

jstests/noPassthrough/connection_establishment_rate_limiting_client_disconnect.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ const testKillOnClientDisconnect = (conn) => {
4343
if (isLinux()) {
4444
assert.soon(() => checkLog.checkContainsOnceJson(
4545
conn, 20883)); // Interrupted operation as its client disconnected
46-
assert.soon(
47-
() => (1 ==
48-
getConnectionStats(
49-
conn)["establishmentRateLimit"]["totalInterruptedDueToClientDisconnect"]));
46+
assert.soon(() =>
47+
(1 ==
48+
getConnectionStats(
49+
conn)["establishmentRateLimit"]["interruptedDueToClientDisconnect"]));
5050
}
5151
};
5252

jstests/noPassthrough/connection_establishment_rate_limiting_exemptions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ const testExemptIPsFromRateLimit = (conn) => {
4040
return false;
4141
});
4242

43-
assert.soon(() => 1 == getConnectionStats(conn)["establishmentRateLimit"]["totalRejected"]);
43+
assert.soon(() => 1 == getConnectionStats(conn)["establishmentRateLimit"]["rejected"]);
4444

4545
// Connections over an exempted ip will succeed.
4646
assert(new Mongo(`mongodb://${exemptIP}:${conn.port}`));
47-
assert.soon(() => getConnectionStats(conn)["establishmentRateLimit"]["totalExempted"] >= 1);
47+
assert.soon(() => getConnectionStats(conn)["establishmentRateLimit"]["exempted"] >= 1);
4848
};
4949

5050
const testExemptIPsFromRateLimitOpts = {

jstests/noPassthrough/connection_establishment_rate_limiting_proxy_protocol.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ rs.initiate();
5858
assert.neq(null, conn, 'Client was unable to connect');
5959
}
6060

61-
assert.soon(
62-
() => getConnectionStats(rs.getPrimary())["establishmentRateLimit"]["totalExempted"] >=
63-
numConnections);
61+
assert.soon(() => getConnectionStats(rs.getPrimary())["establishmentRateLimit"]["exempted"] >=
62+
numConnections);
6463

6564
proxy_server.stop();
6665
}
@@ -100,8 +99,8 @@ rs.getPrimary().adminCommand({
10099
return false;
101100
});
102101

103-
assert.soon(() => 1 ==
104-
getConnectionStats(rs.getPrimary())["establishmentRateLimit"]["totalRejected"]);
102+
assert.soon(() =>
103+
1 == getConnectionStats(rs.getPrimary())["establishmentRateLimit"]["rejected"]);
105104

106105
proxy_server.stop();
107106
}

jstests/noPassthrough/connection_establishment_rate_limiting_stats.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,28 @@
1010
import {configureFailPoint} from "jstests/libs/fail_point_util.js";
1111
import {Thread} from "jstests/libs/parallelTester.js";
1212
import {
13-
getConnectionStats,
13+
getLimiterStats,
1414
runTestReplSet,
1515
runTestShardedCluster,
1616
runTestStandaloneParamsSetAtRuntime,
1717
runTestStandaloneParamsSetAtStartup
1818
} from "jstests/noPassthrough/libs/conn_establishment_rate_limiter_helpers.js";
1919

20+
// Return whether the left Number or NumberLong is equal to the right Number or NumberLong.
21+
const equal = (left, right) => {
22+
if (left instanceof NumberLong && right instanceof NumberLong) {
23+
return left.compare(right) === 0;
24+
}
25+
return left == right;
26+
};
27+
2028
const maxQueueSize = 3;
2129
// Hardcoded so that we can assert on "available" connections in serverStatus.
2230
const maxIncomingConnections = 1000;
2331

2432
const testRateLimiterStats = (conn) => {
25-
// Start maxQueueDepth + 3 threads that will all try to connect to the server. The rate limiter
26-
// should allow maxQueueDepth connections to be queued, and the rest should be rejected.
33+
// Start maxQueueSize + 3 threads that will all try to connect to the server. The rate limiter
34+
// should allow maxQueueSize connections to be queued, and the rest should be rejected.
2735
let connDelayFailPoint = configureFailPoint(conn, 'hangInRateLimiter');
2836
const extraConns = 3;
2937
const threads = [];
@@ -41,15 +49,39 @@ const testRateLimiterStats = (conn) => {
4149
}
4250

4351
assert.soon(() => {
44-
const stats = getConnectionStats(conn);
45-
// Queued connections should be counted in "queued", "totalCreated", and "available" stats.
46-
return (stats["queued"] == maxQueueSize && stats["totalCreated"] >= maxQueueSize &&
47-
stats["available"] <= maxIncomingConnections - maxQueueSize) &&
52+
const {connections: cstats, ingressSessionEstablishmentQueues: qstats} =
53+
getLimiterStats(conn, {log: false});
54+
55+
jsTestLog("stats: " + tojson({cstats, qstats}));
56+
57+
const checks = [
58+
// Queued connections should be counted in "queuedForEstablishment", "totalCreated", and
59+
// "available" stats.
60+
() => equal(cstats["queuedForEstablishment"], maxQueueSize),
61+
() => cstats["totalCreated"] >= maxQueueSize,
62+
() => cstats["available"] <= maxIncomingConnections - maxQueueSize,
4863
// "extraConns" connections should be rejected because we create more than the rate
4964
// limit can handle-- they should be counted as rejected in the establishmentRateLimit
5065
// subsection and the overall "rejected" count.
51-
(stats["rejected"] == extraConns &&
52-
stats["establishmentRateLimit"]["totalRejected"] == extraConns);
66+
() => equal(cstats["rejected"], extraConns),
67+
() => equal(cstats["establishmentRateLimit"]["rejected"], extraConns),
68+
// There is a correspondence between the connections stats and the session
69+
// establishment queue stats, because they use the same underlying rate limiter.
70+
() => equal(cstats["establishmentRateLimit"]["rejected"], qstats["rejectedAdmissions"]),
71+
() => equal(cstats["queuedForEstablishment"],
72+
qstats["addedToQueue"] - qstats["removedFromQueue"]),
73+
// Somebody either waited or was admitted immediately, so there is an average wait
74+
// time.
75+
() => qstats["averageTimeQueuedMicros"] >= 0
76+
];
77+
78+
// results :: {<body of check>: <boolean result of check>}
79+
const results = Object.fromEntries(checks.map(check => [check.toString(), check()]));
80+
81+
jsTestLog("checks: " + tojson(results));
82+
83+
// "all of the checks passed"
84+
return Object.values(results).every(result => result);
5385
});
5486

5587
connDelayFailPoint.off();

jstests/noPassthrough/libs/conn_establishment_rate_limiter_helpers.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
export const getLimiterStats = (conn, {log = false}) => {
2+
const serverStatus = conn.adminCommand({serverStatus: 1});
3+
assert(serverStatus, "Failed to get server status");
4+
const result = {
5+
connections: serverStatus.connections,
6+
ingressSessionEstablishmentQueues: serverStatus.queues.ingressSessionEstablishment
7+
};
8+
if (log) {
9+
jsTestLog("Limiter stats: " + tojson(result));
10+
}
11+
return result;
12+
};
13+
114
export const getConnectionStats = (conn) => {
2-
const connStats = conn.adminCommand({serverStatus: 1})["connections"];
3-
assert.neq(null, connStats, "Failed to get connection stats");
4-
jsTestLog("Connection stats: " + tojson(connStats));
5-
return connStats;
15+
const {connections} = getLimiterStats(conn, {log: true});
16+
assert(connections, "Failed to get connection stats");
17+
return connections;
618
};
719

820
export const runTestStandaloneParamsSetAtStartup = (setParams, testCase) => {

src/mongo/db/admission/SConscript

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ env.Library(
117117
'$BUILD_DIR/mongo/db/commands/server_status_core',
118118
'$BUILD_DIR/mongo/db/server_feature_flags',
119119
'$BUILD_DIR/mongo/db/service_context',
120+
'$BUILD_DIR/mongo/transport/transport_layer_common',
120121
'ingress_admission_control',
121122
'ticketholder_manager',
122123
],

src/mongo/db/admission/queues_server_status_section.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
#include "mongo/db/admission/ingress_admission_control_gen.h"
3131
#include "mongo/db/admission/ingress_admission_controller.h"
3232
#include "mongo/db/admission/ticketholder_manager.h"
33-
#include "mongo/db/server_feature_flags_gen.h"
34-
3533
#include "mongo/db/commands/server_status.h"
34+
#include "mongo/transport/session_establishment_rate_limiter.h"
35+
#include "mongo/transport/transport_layer.h"
3636

3737
namespace mongo {
3838
namespace admission {
@@ -48,27 +48,37 @@ class AdmissionServerStatusSection : public ServerStatusSection {
4848

4949
BSONObj generateSection(OperationContext* opCtx,
5050
const BSONElement& configElement) const override {
51+
const ClusterRole role = opCtx->getService()->role();
52+
5153
BSONObjBuilder admissionBuilder;
5254
auto ticketHolderManager = TicketHolderManager::get(opCtx->getServiceContext());
53-
if (ticketHolderManager) {
55+
if (ticketHolderManager && role.has(ClusterRole::ShardServer)) {
5456
BSONObjBuilder executionBuilder(admissionBuilder.subobjStart("execution"));
5557
ticketHolderManager->appendStats(executionBuilder);
5658
executionBuilder.done();
5759
}
5860

59-
if (gIngressAdmissionControlEnabled.load()) {
61+
if (gIngressAdmissionControlEnabled.load() && role.has(ClusterRole::ShardServer)) {
6062
BSONObjBuilder ingressBuilder(admissionBuilder.subobjStart("ingress"));
6163
auto& controller = IngressAdmissionController::get(opCtx);
6264
controller.appendStats(ingressBuilder);
6365
ingressBuilder.done();
6466
}
6567

68+
if (auto* limiter =
69+
transport::SessionEstablishmentRateLimiter::get(*opCtx->getServiceContext())) {
70+
BSONObjBuilder ingressSessionEstablishmentBuilder(
71+
admissionBuilder.subobjStart("ingressSessionEstablishment"));
72+
limiter->appendStatsQueues(&ingressSessionEstablishmentBuilder);
73+
ingressSessionEstablishmentBuilder.done();
74+
}
75+
6676
return admissionBuilder.obj();
6777
}
6878
};
6979

7080
auto& admissionSection =
71-
*ServerStatusSectionBuilder<AdmissionServerStatusSection>("queues").forShard();
81+
*ServerStatusSectionBuilder<AdmissionServerStatusSection>("queues").forShard().forRouter();
7282

7383
} // namespace
7484
} // namespace admission

src/mongo/db/admission/rate_limiter.cpp

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
#include <folly/TokenBucket.h>
3333

3434
#include "mongo/logv2/log.h"
35+
#include "mongo/util/duration.h"
36+
#include "mongo/util/scopeguard.h"
3537

3638
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kDefault
3739

@@ -46,37 +48,42 @@ Milliseconds doubleToMillis(double t) {
4648
} // namespace
4749

4850
struct RateLimiter::RateLimiterPrivate {
49-
RateLimiterPrivate(
50-
double r, double b, int64_t m, std::string n, std::unique_ptr<RateLimiter::Stats> s)
51-
: tokenBucket{r, b}, maxQueueDepth(m), name(std::move(n)), stats(std::move(s)) {}
51+
RateLimiterPrivate(double r, double b, int64_t m, std::string n)
52+
// Initialize the token bucket with one "burst" of tokens. The third parameter to
53+
// tokenBucket's constructor ("zeroTime") is interpreted as a number of seconds from the
54+
// epoch of the clock used by the token bucket. The clock is
55+
// `std::chrono::steady_clock`, whose epoch is unspecified but is usually the boot time
56+
// of the machine. Rather than have an initial accumulation of tokens based on some
57+
// unknown point in the past, set the zero time to a known time in the past: enough time
58+
// for burst size (b) tokens to have accumulated.
59+
: tokenBucket{r, b, folly::TokenBucket::defaultClockNow() - b / r},
60+
maxQueueDepth(m),
61+
queued(0),
62+
name(std::move(n)) {}
5263

5364
WriteRarelyRWMutex rwMutex;
5465
folly::TokenBucket tokenBucket;
5566

67+
Stats stats;
68+
5669
Atomic<int64_t> maxQueueDepth;
57-
Atomic<int64_t> numWaiters;
70+
Atomic<int64_t> queued;
5871

5972
std::string name;
6073

61-
/**
62-
* Users may inherit from the RateLimiter::Stats class and define their own stats to track.
63-
*/
64-
std::unique_ptr<Stats> stats;
65-
66-
Status rejectIfOverQueueLimit(double nWaiters) {
67-
auto maxDepth = maxQueueDepth.loadRelaxed();
68-
if (MONGO_unlikely(nWaiters >= maxDepth)) {
69-
{
70-
auto lk = rwMutex.readLock();
71-
tokenBucket.returnTokens(1.0);
74+
Status enqueue() {
75+
const auto maxDepth = maxQueueDepth.loadRelaxed();
76+
int64_t expected = queued.load();
77+
do {
78+
if (expected >= maxDepth) {
79+
return Status(ErrorCodes::TemporarilyUnavailable,
80+
fmt::format("RateLimiter queue depth has exceeded the maxQueueDepth. "
81+
"numWaiters={}; maxQueueDepth={}; rateLimiterName={}",
82+
expected,
83+
maxDepth,
84+
name));
7285
}
73-
return Status(ErrorCodes::TemporarilyUnavailable,
74-
fmt::format("RateLimiter queue depth has exceeded the maxQueueDepth. "
75-
"numWaiters={}; maxQueueDepth={}; rateLimiterName={}",
76-
nWaiters,
77-
maxDepth,
78-
name));
79-
}
86+
} while (!queued.compareAndSwap(&expected, expected + 1));
8087

8188
return Status::OK();
8289
}
@@ -85,20 +92,21 @@ struct RateLimiter::RateLimiterPrivate {
8592
RateLimiter::RateLimiter(double refreshRatePerSec,
8693
double burstSize,
8794
int64_t maxQueueDepth,
88-
std::string name,
89-
std::unique_ptr<Stats> stats) {
95+
std::string name) {
9096
uassert(ErrorCodes::InvalidOptions,
9197
fmt::format("burstSize cannot be less than 1.0. burstSize={}; rateLimiterName={}",
9298
burstSize,
9399
name),
94100
burstSize >= 1.0);
95101
_impl = std::make_unique<RateLimiterPrivate>(
96-
refreshRatePerSec, burstSize, maxQueueDepth, std::move(name), std::move(stats));
102+
refreshRatePerSec, burstSize, maxQueueDepth, std::move(name));
97103
}
98104

99105
RateLimiter::~RateLimiter() = default;
100106

101107
Status RateLimiter::acquireToken(OperationContext* opCtx) {
108+
_impl->stats.attemptedAdmissions.incrementRelaxed();
109+
102110
// The consumeWithBorrowNonBlocking API consumes a token (possibly leading to a negative
103111
// bucket balance), and returns how long the consumer should nap until their token
104112
// reservation becomes valid.
@@ -111,12 +119,15 @@ Status RateLimiter::acquireToken(OperationContext* opCtx) {
111119
}
112120

113121
if (auto napTime = doubleToMillis(waitForTokenSecs); napTime > Milliseconds{0}) {
114-
auto nWaiters = _impl->numWaiters.fetchAndAdd(1);
115-
ON_BLOCK_EXIT([&] { _impl->numWaiters.fetchAndSubtract(1); });
116-
if (auto s = _impl->rejectIfOverQueueLimit(nWaiters); !s.isOK()) {
117-
return s;
122+
if (auto status = _impl->enqueue(); !status.isOK()) {
123+
_impl->stats.rejectedAdmissions.incrementRelaxed();
124+
return status;
118125
}
119-
126+
_impl->stats.addedToQueue.incrementRelaxed();
127+
ON_BLOCK_EXIT([&] {
128+
_impl->stats.removedFromQueue.incrementRelaxed();
129+
_impl->queued.fetchAndSubtract(1);
130+
});
120131
try {
121132
LOGV2_DEBUG(10550200,
122133
4,
@@ -125,6 +136,7 @@ Status RateLimiter::acquireToken(OperationContext* opCtx) {
125136
"napTimeMillis"_attr = napTime.toString());
126137
opCtx->sleepFor(napTime);
127138
} catch (const DBException& e) {
139+
_impl->stats.interruptedInQueue.incrementRelaxed();
128140
LOGV2_DEBUG(10440800,
129141
4,
130142
"Interrupted while waiting in rate limiter queue",
@@ -139,15 +151,14 @@ Status RateLimiter::acquireToken(OperationContext* opCtx) {
139151
_impl->name));
140152
}
141153
}
142-
return Status::OK();
143-
}
144154

145-
RateLimiter::Stats* RateLimiter::getRateLimiterStats() {
146-
return _impl->stats.get();
155+
_impl->stats.successfulAdmissions.incrementRelaxed();
156+
_impl->stats.averageTimeQueuedMicros.addSample(waitForTokenSecs * 1'000'000);
157+
return Status::OK();
147158
}
148159

149-
int64_t RateLimiter::getNumWaiters() {
150-
return _impl->numWaiters.load();
160+
void RateLimiter::recordExemption() {
161+
_impl->stats.exemptedAdmissions.incrementRelaxed();
151162
}
152163

153164
void RateLimiter::setRefreshRatePerSec(double refreshRatePerSec) {
@@ -169,4 +180,33 @@ void RateLimiter::setBurstSize(double burstSize) {
169180
void RateLimiter::setMaxQueueDepth(int64_t maxQueueDepth) {
170181
_impl->maxQueueDepth.storeRelaxed(maxQueueDepth);
171182
}
183+
184+
const RateLimiter::Stats& RateLimiter::stats() const {
185+
return _impl->stats;
186+
}
187+
188+
void RateLimiter::appendStats(BSONObjBuilder* bob) const {
189+
invariant(bob);
190+
bob->append("addedToQueue", stats().addedToQueue.get());
191+
bob->append("removedFromQueue", stats().removedFromQueue.get());
192+
bob->append("interruptedInQueue", stats().interruptedInQueue.get());
193+
bob->append("rejectedAdmissions", stats().rejectedAdmissions.get());
194+
bob->append("exemptedAdmissions", stats().exemptedAdmissions.get());
195+
bob->append("successfulAdmissions", stats().successfulAdmissions.get());
196+
bob->append("attemptedAdmissions", stats().attemptedAdmissions.get());
197+
if (const auto avg = stats().averageTimeQueuedMicros.get()) {
198+
bob->append("averageTimeQueuedMicros", *avg);
199+
}
200+
bob->append("totalAvailableTokens", tokensAvailable());
201+
}
202+
203+
double RateLimiter::tokensAvailable() const {
204+
auto lk = _impl->rwMutex.readLock();
205+
return _impl->tokenBucket.available();
206+
}
207+
208+
int64_t RateLimiter::queued() const {
209+
return _impl->queued.load();
210+
}
211+
172212
} // namespace mongo::admission

0 commit comments

Comments
 (0)