Skip to content

Commit 8222d6e

Browse files
authored
Enable PoolQueue balancer (#2559)
This change culminates recent work to restructure the balancer to use a PoolQueue so that balancer changes may occur independently of request processing. This replaces independent discovery buffering so that the balancer task is responsible for polling discovery streams without independent buffering. Requests are buffered and processed as soon as the pool has available backends. Fail-fast circuit breaking is enforced on the balancer's queue so that requests can't get stuck in a queue indefinitely. In general, the new balancer is instrumented directly with metrics, and the relevant metric name prefix and labelset is provided by the stack. In addition to detailed queue metrics including request (in-queue) latency histograms, but also failfast states, discovery updates counts, and balancer endpoint pool sizes.
1 parent a349575 commit 8222d6e

File tree

21 files changed

+453
-828
lines changed

21 files changed

+453
-828
lines changed

linkerd/app/core/src/control.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ impl svc::Param<Addr> for ControlAddr {
4444
}
4545
}
4646

47+
impl svc::Param<svc::queue::Capacity> for ControlAddr {
48+
fn param(&self) -> svc::queue::Capacity {
49+
svc::queue::Capacity(1_000)
50+
}
51+
}
52+
53+
impl svc::Param<svc::queue::Timeout> for ControlAddr {
54+
fn param(&self) -> svc::queue::Timeout {
55+
const FAILFAST: time::Duration = time::Duration::from_secs(30);
56+
svc::queue::Timeout(FAILFAST)
57+
}
58+
}
59+
4760
impl svc::Param<http::balance::EwmaConfig> for ControlAddr {
4861
fn param(&self) -> http::balance::EwmaConfig {
4962
EWMA_CONFIG
@@ -76,6 +89,7 @@ impl Config {
7689
svc::BoxCloneSyncService<http::Request<tonic::body::BoxBody>, http::Response<RspBody>>,
7790
> {
7891
let addr = self.addr;
92+
tracing::trace!(%addr, "Building");
7993

8094
// When a DNS resolution fails, log the error and use the TTL, if there
8195
// is one, to drive re-resolution attempts.
@@ -121,11 +135,7 @@ impl Config {
121135
.lift_new()
122136
.push(self::balance::layer(registry, dns, resolve_backoff))
123137
.push(metrics.to_layer::<classify::Response, _, _>())
124-
.push(classify::NewClassify::layer_default())
125-
// This buffer allows a resolver client to be shared across stacks.
126-
// No load shed is applied here, however, so backpressure may leak
127-
// into the caller task.
128-
.push(svc::NewQueue::layer_via(self.buffer));
138+
.push(classify::NewClassify::layer_default());
129139

130140
balance
131141
.push(self::add_origin::layer())
@@ -159,7 +169,7 @@ impl From<(&self::client::Target, Error)> for EndpointError {
159169
/// Sets the request's URI from `Config`.
160170
mod add_origin {
161171
use super::ControlAddr;
162-
use linkerd_stack::{layer, NewService};
172+
use crate::svc::{layer, NewService};
163173
use std::task::{Context, Poll};
164174

165175
pub fn layer<M>() -> impl layer::Layer<M, Service = NewAddOrigin<M>> + Clone {
@@ -219,26 +229,36 @@ mod balance {
219229
use super::{client::Target, ControlAddr};
220230
use crate::{
221231
dns,
232+
metrics::prom::{self, encoding::EncodeLabelSet},
222233
proxy::{dns_resolve::DnsResolve, http, resolve::recover},
223234
svc, tls,
224235
};
225-
use linkerd_metrics::prom;
236+
use linkerd_stack::ExtractParam;
226237
use std::net::SocketAddr;
227238

228239
pub fn layer<B, R: Clone, N>(
229-
_registry: &mut prom::Registry,
240+
registry: &mut prom::Registry,
230241
dns: dns::Resolver,
231242
recover: R,
232243
) -> impl svc::Layer<
233244
N,
234-
Service = http::NewBalancePeakEwma<B, recover::Resolve<R, DnsResolve>, NewIntoTarget<N>>,
245+
Service = http::NewBalancePeakEwma<
246+
B,
247+
Params,
248+
recover::Resolve<R, DnsResolve>,
249+
NewIntoTarget<N>,
250+
>,
235251
> {
236252
let resolve = recover::Resolve::new(recover, DnsResolve::new(dns));
253+
let metrics = Params(http::balance::MetricFamilies::register(registry));
237254
svc::layer::mk(move |inner| {
238-
http::NewBalancePeakEwma::new(NewIntoTarget { inner }, resolve.clone())
255+
http::NewBalancePeakEwma::new(NewIntoTarget { inner }, resolve.clone(), metrics.clone())
239256
})
240257
}
241258

259+
#[derive(Clone, Debug)]
260+
pub struct Params(http::balance::MetricFamilies<Labels>);
261+
242262
#[derive(Clone, Debug)]
243263
pub struct NewIntoTarget<N> {
244264
inner: N,
@@ -250,6 +270,11 @@ mod balance {
250270
server_id: tls::ConditionalClientTls,
251271
}
252272

273+
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
274+
struct Labels {
275+
addr: String,
276+
}
277+
253278
// === impl NewIntoTarget ===
254279

255280
impl<N: svc::NewService<ControlAddr>> svc::NewService<ControlAddr> for NewIntoTarget<N> {
@@ -273,6 +298,16 @@ mod balance {
273298
.new_service(Target::new(addr, self.server_id.clone()))
274299
}
275300
}
301+
302+
// === impl Metrics ===
303+
304+
impl ExtractParam<http::balance::Metrics, ControlAddr> for Params {
305+
fn extract_param(&self, tgt: &ControlAddr) -> http::balance::Metrics {
306+
self.0.metrics(&Labels {
307+
addr: tgt.addr.to_string(),
308+
})
309+
}
310+
}
276311
}
277312

278313
/// Creates a client suitable for gRPC.

linkerd/app/integration/src/tests/direct.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::*;
22

3-
#[tokio::test]
3+
#[tokio::test(flavor = "current_thread")]
44
async fn h2_hinted() {
55
let _trace = trace_init();
66

@@ -44,7 +44,7 @@ async fn h2_hinted() {
4444
/// `INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION` env var.
4545
/// TODO(eliza): add a similar test where the policy on the opaque port is
4646
/// discovered from the policy controller.
47-
#[tokio::test]
47+
#[tokio::test(flavor = "current_thread")]
4848
async fn opaque_hinted() {
4949
let _trace = trace_init();
5050

linkerd/app/outbound/src/http/concrete/balance.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::Endpoint;
22
use crate::{
33
http::{self, balance, breaker},
4+
metrics::BalancerMetricsParams,
45
stack_labels, BackendRef, ParentRef,
56
};
67
use linkerd_app_core::{
@@ -57,6 +58,18 @@ impl<T> svc::Param<svc::queue::Timeout> for Balance<T> {
5758
}
5859
}
5960

61+
impl<T: svc::Param<ParentRef>> svc::Param<ParentRef> for Balance<T> {
62+
fn param(&self) -> ParentRef {
63+
self.parent.param()
64+
}
65+
}
66+
67+
impl<T: svc::Param<BackendRef>> svc::Param<BackendRef> for Balance<T> {
68+
fn param(&self) -> BackendRef {
69+
self.parent.param()
70+
}
71+
}
72+
6073
impl<T> Balance<T>
6174
where
6275
// Parent target.
@@ -68,7 +81,7 @@ where
6881
pub(super) fn layer<N, NSvc, R>(
6982
config: &crate::Config,
7083
rt: &crate::Runtime,
71-
_registry: &mut prom::Registry,
84+
registry: &mut prom::Registry,
7285
resolve: R,
7386
) -> impl svc::Layer<N, Service = svc::ArcNewCloneHttp<Self>> + Clone
7487
where
@@ -92,6 +105,8 @@ where
92105
.push_map_target(|t: Self| ConcreteAddr(t.addr))
93106
.into_inner();
94107

108+
let metrics_params = BalancerMetricsParams::register(registry);
109+
95110
svc::layer::mk(move |inner: N| {
96111
let endpoint = svc::stack(inner)
97112
.push_map_target({
@@ -140,11 +155,13 @@ where
140155
.push(svc::ArcNewService::layer());
141156

142157
endpoint
143-
.push(http::NewBalancePeakEwma::layer(resolve.clone()))
158+
.push(http::NewBalancePeakEwma::layer(
159+
resolve.clone(),
160+
metrics_params.clone(),
161+
))
144162
.push_on_service(http::BoxResponse::layer())
145163
.push_on_service(metrics.proxy.stack.layer(stack_labels("http", "balance")))
146164
.push(svc::NewMapErr::layer_from_target::<BalanceError, _>())
147-
.push(svc::NewQueue::layer())
148165
.instrument(|t: &Self| {
149166
let BackendRef(meta) = t.parent.param();
150167
info_span!(

linkerd/app/outbound/src/http/concrete/metrics.rs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use crate::{BackendRef, ParentRef};
1+
use crate::{metrics::ConcreteLabels, BackendRef, ParentRef};
22
use ahash::AHashMap;
33
use linkerd_app_core::{
44
metrics::{metrics, FmtLabels, FmtMetrics, Gauge},
55
svc::http::balance,
66
};
77
use parking_lot::Mutex;
8-
use std::{fmt::Write, sync::Arc};
8+
use std::sync::Arc;
99

1010
metrics! {
1111
outbound_http_balancer_endpoints: Gauge {
@@ -15,22 +15,19 @@ metrics! {
1515

1616
#[derive(Clone, Debug, Default)]
1717
pub struct BalancerMetrics {
18-
balancers: Arc<Mutex<AHashMap<Labels, balance::EndpointsGauges>>>,
18+
balancers: Arc<Mutex<AHashMap<ConcreteLabels, balance::EndpointsGauges>>>,
1919
}
2020

21-
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
22-
struct Labels(ParentRef, BackendRef);
23-
24-
struct Ready<'l>(&'l Labels);
25-
struct Pending<'l>(&'l Labels);
21+
struct Ready<'l>(&'l ConcreteLabels);
22+
struct Pending<'l>(&'l ConcreteLabels);
2623

2724
// === impl RouteBackendMetrics ===
2825

2926
impl BalancerMetrics {
3027
pub(super) fn http_endpoints(&self, pr: ParentRef, br: BackendRef) -> balance::EndpointsGauges {
3128
self.balancers
3229
.lock()
33-
.entry(Labels(pr, br))
30+
.entry(ConcreteLabels(pr, br))
3431
.or_default()
3532
.clone()
3633
}
@@ -58,17 +55,7 @@ impl FmtMetrics for BalancerMetrics {
5855
}
5956
}
6057

61-
impl FmtLabels for Labels {
62-
fn fmt_labels(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63-
let Labels(parent, backend) = self;
64-
65-
crate::metrics::write_service_meta_labels("parent", parent, f)?;
66-
f.write_char(',')?;
67-
crate::metrics::write_service_meta_labels("backend", backend, f)?;
68-
69-
Ok(())
70-
}
71-
}
58+
// === impl Ready ===
7259

7360
impl<'l> FmtLabels for Ready<'l> {
7461
fn fmt_labels(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -77,6 +64,8 @@ impl<'l> FmtLabels for Ready<'l> {
7764
}
7865
}
7966

67+
// === impl Pending ===
68+
8069
impl<'l> FmtLabels for Pending<'l> {
8170
fn fmt_labels(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8271
self.0.fmt_labels(f)?;

linkerd/app/outbound/src/http/concrete/tests.rs

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
use super::*;
1+
use super::{balance::*, *};
22
use crate::test_util::*;
3-
use linkerd_app_core::{
4-
svc::{http::balance::EwmaConfig, ServiceExt},
5-
svc::{NewService, Service},
6-
trace,
7-
};
3+
use linkerd_app_core::{proxy::http::balance::EwmaConfig, svc::NewService, trace};
84
use linkerd_proxy_client_policy as policy;
95
use std::{net::SocketAddr, num::NonZeroU16, sync::Arc};
106
use tokio::{task, time};
@@ -37,8 +33,8 @@ async fn gauges_endpoints() {
3733
panic!("unexpected endpoint: {:?}", ep)
3834
};
3935

40-
let mut svc = svc::stack(stk)
41-
.push(balance::Balance::layer(
36+
let _svc = svc::stack(stk)
37+
.push(Balance::layer(
4238
&outbound.config,
4339
&outbound.runtime,
4440
&mut Default::default(),
@@ -49,33 +45,15 @@ async fn gauges_endpoints() {
4945
addr,
5046
parent: Target,
5147
queue: QueueConfig {
52-
capacity: 10,
53-
failfast_timeout: time::Duration::from_secs(1),
48+
capacity: 100,
49+
failfast_timeout: time::Duration::from_secs(3),
5450
},
5551
ewma: EwmaConfig {
5652
default_rtt: time::Duration::from_millis(100),
5753
decay: time::Duration::from_secs(10),
5854
},
5955
});
6056

61-
// Run a background task that drives requests through the balancer as it is notified.
62-
//
63-
// XXX(ver) Discovery updates are only processed when the buffer is actively
64-
// processing requests, so we need to drive requests through this test to
65-
// update the gauge metrics. If the discovery processing logic changes, we
66-
// can update this to test updates without processing requests.
67-
let ready = Arc::new(tokio::sync::Notify::new());
68-
let _task = tokio::spawn({
69-
let ready = ready.clone();
70-
async move {
71-
loop {
72-
ready.notified().await;
73-
svc.ready().await.unwrap();
74-
svc.call(http::Request::default()).await.unwrap();
75-
}
76-
}
77-
});
78-
7957
let gauge = outbound
8058
.runtime
8159
.metrics
@@ -91,51 +69,42 @@ async fn gauges_endpoints() {
9169
// gauge is accurate.
9270
resolve_tx.add(vec![(ep0, Metadata::default())]).unwrap();
9371
handle0.allow(1);
94-
ready.notify_one();
9572
task::yield_now().await;
9673
assert_eq!(
9774
(gauge.pending.value(), gauge.ready.value()),
9875
(0, 1),
9976
"After adding an endpoint one should be ready"
10077
);
101-
let (_, res) = handle0.next_request().await.unwrap();
102-
res.send_response(http::Response::default());
10378

10479
// Add a second endpoint and ensure the gauge is updated.
10580
resolve_tx.add(vec![(ep1, Metadata::default())]).unwrap();
106-
handle0.allow(0);
107-
handle1.allow(1);
108-
ready.notify_one();
81+
handle1.allow(0);
10982
task::yield_now().await;
11083
assert_eq!(
11184
(gauge.pending.value(), gauge.ready.value()),
11285
(1, 1),
11386
"Added a pending endpoint"
11487
);
115-
let (_, res) = handle1.next_request().await.unwrap();
116-
res.send_response(http::Response::default());
88+
89+
handle1.allow(1);
90+
task::yield_now().await;
91+
assert_eq!(
92+
(gauge.pending.value(), gauge.ready.value()),
93+
(0, 2),
94+
"Pending endpoint became ready"
95+
);
11796

11897
// Remove the first endpoint.
11998
resolve_tx.remove(vec![ep0]).unwrap();
120-
handle1.allow(2);
121-
ready.notify_one();
122-
let (_, res) = handle1.next_request().await.unwrap();
123-
res.send_response(http::Response::default());
124-
125-
// The inner endpoint isn't actually dropped until the balancer's subsequent poll.
126-
ready.notify_one();
12799
task::yield_now().await;
128100
assert_eq!(
129101
(gauge.pending.value(), gauge.ready.value()),
130102
(0, 1),
131-
"Removed an endpoint"
103+
"Removed first endpoint"
132104
);
133-
let (_, res) = handle1.next_request().await.unwrap();
134-
res.send_response(http::Response::default());
135105

136106
// Dropping the remaining endpoint, the gauge is updated.
137107
resolve_tx.remove(vec![ep1]).unwrap();
138-
ready.notify_one();
139108
task::yield_now().await;
140109
assert_eq!(
141110
(gauge.pending.value(), gauge.ready.value()),

0 commit comments

Comments
 (0)