Skip to main content

ntex_server/
balance.rs

1use std::hash::{BuildHasher, RandomState};
2use std::time::{Duration, Instant};
3
4use ntex_rt::Id;
5use ntex_util::HashMap;
6
7use crate::Worker;
8
9/// How the server distributes items between workers.
10#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum LoadBalancing {
13    /// Items are sent to the workers in turn.
14    ///
15    /// Workers whose runtime is blocked by a task are skipped while another
16    /// worker is available.
17    RoundRobin,
18    /// Each item is sent to the less loaded of two randomly chosen workers.
19    ///
20    /// A worker's load is its runtime utilization, averaged over the last
21    /// ~250 ms. Workers whose utilization differs by less than 10% are compared
22    /// by their open connections plus queued items. Workers whose runtime
23    /// is blocked by a task are skipped while another worker is available.
24    LeastLoaded,
25    /// Each connection is sent to the worker placed on the CPU that receives
26    /// its network packets, unless that worker is busier than other workers.
27    ///
28    /// The worker on the incoming CPU, or on an SMT sibling of it if no worker
29    /// is placed on that CPU, is used if its utilization exceeds the
30    /// [`LeastLoaded`](Self::LeastLoaded) choice by at most 20% and its load by
31    /// at most half plus 16. Otherwise, and for items without a known
32    /// incoming CPU, it works like [`LeastLoaded`](Self::LeastLoaded).
33    ///
34    /// The incoming CPU is the receive-side scaling (RSS) CPU of the
35    /// connection on Windows, known for connections over network adapters
36    /// with RSS, and the CPU that processed the connection's last packet
37    /// (`SO_INCOMING_CPU`) on Linux and Android. Requires worker affinity, see
38    /// [`Affinity`](crate::Affinity).
39    ///
40    /// This is the default.
41    #[default]
42    IncomingCpu,
43}
44
45/// Minimum interval between worker load samples.
46const REFRESH: Duration = Duration::from_millis(10);
47/// Time constant of the utilization average.
48const TAU: Duration = Duration::from_millis(250);
49/// A runtime turn longer than this marks the worker as blocked.
50const BLOCKED: Duration = Duration::from_millis(100);
51/// Utilization difference below which workers are compared by load.
52const UTIL_SLACK: f64 = 0.1;
53/// Utilization by which the worker on the incoming CPU may exceed the
54/// least loaded choice.
55const LOCAL_UTIL_SLACK: f64 = 0.2;
56/// Load by which the worker on the incoming CPU may exceed the least loaded
57/// choice, on top of half of that choice's load.
58const LOCAL_LOAD_SLACK: usize = 16;
59
60#[derive(Copy, Clone, Debug, Default)]
61/// Worker runtime sample.
62pub(crate) struct Sample {
63    /// Total time the runtime has been busy.
64    busy: Option<Duration>,
65    /// The runtime is blocked by a task.
66    blocked: bool,
67}
68
69impl Sample {
70    fn of<T>(wrk: &Worker<T>, now: Instant) -> Sample {
71        let arb = wrk.arbiter();
72        let hnd = arb.handle();
73        let blocked = hnd.current_turn().is_some_and(|t| t >= BLOCKED)
74            || arb.latency().is_some_and(|l| {
75                l.stalled
76                    || l.last_sample
77                        .is_some_and(|t| now.saturating_duration_since(t) >= l.interval + BLOCKED)
78            });
79        Sample {
80            busy: hnd.busy_duration(),
81            blocked,
82        }
83    }
84}
85
86#[derive(Debug, Default)]
87struct State {
88    busy: Option<Duration>,
89    at: Option<Instant>,
90    util: f64,
91    blocked: bool,
92}
93
94impl State {
95    fn update(&mut self, sample: Sample, now: Instant) {
96        if let (Some(busy), Some(prev), Some(at)) = (sample.busy, self.busy, self.at) {
97            let dt = now.saturating_duration_since(at);
98            if !dt.is_zero() {
99                let used = busy.saturating_sub(prev).as_secs_f64() / dt.as_secs_f64();
100                let alpha = dt.as_secs_f64() / (TAU + dt).as_secs_f64();
101                self.util += alpha * (used.min(1.0) - self.util);
102            }
103        }
104        self.busy = sample.busy;
105        self.at = Some(now);
106        self.blocked = sample.blocked;
107    }
108}
109
110#[derive(Debug)]
111/// Selects the worker for the next item.
112///
113/// Keeps a load state per worker, in the order of the manager's worker list.
114pub(crate) struct Balancer {
115    mode: LoadBalancing,
116    next: usize,
117    states: Vec<State>,
118    /// Indexes of the workers that are not blocked.
119    ready: Vec<usize>,
120    refreshed: Option<Instant>,
121    rng: u64,
122    /// Indexes of the workers placed on each CPU, or on an SMT sibling of a
123    /// CPU without its own worker.
124    local: HashMap<usize, Vec<usize>>,
125    /// `local` must be rebuilt.
126    local_stale: bool,
127    /// Load state of unavailable workers by name, with their arbiter.
128    parked: HashMap<String, (Id, State)>,
129}
130
131impl Balancer {
132    pub(crate) fn new(mode: LoadBalancing) -> Self {
133        Self {
134            mode,
135            next: 0,
136            states: Vec::new(),
137            ready: Vec::new(),
138            refreshed: None,
139            rng: RandomState::new().hash_one(0u64) | 1,
140            local: HashMap::default(),
141            local_stale: true,
142            parked: HashMap::default(),
143        }
144    }
145
146    #[cfg(test)]
147    /// A worker is inserted at `idx`.
148    pub(crate) fn insert(&mut self, idx: usize) {
149        self.insert_state(idx, State::default());
150    }
151
152    /// A worker that became available again is inserted at `idx`.
153    ///
154    /// Its load state is kept from when it became unavailable, unless the
155    /// worker was restarted on a new arbiter since then.
156    pub(crate) fn unpark(&mut self, idx: usize, name: &str, arbiter: Id) {
157        let state = match self.parked.remove(name) {
158            Some((id, state)) if id == arbiter => state,
159            _ => State::default(),
160        };
161        self.insert_state(idx, state);
162    }
163
164    fn insert_state(&mut self, idx: usize, state: State) {
165        self.states.insert(idx, state);
166        self.refreshed = None;
167        self.local_stale = true;
168    }
169
170    /// The worker at `idx` is replaced.
171    pub(crate) fn reset(&mut self, idx: usize) {
172        self.states[idx] = State::default();
173        self.refreshed = None;
174        self.local_stale = true;
175    }
176
177    /// The worker at `idx` is removed.
178    pub(crate) fn remove(&mut self, idx: usize) {
179        self.states.remove(idx);
180        self.refreshed = None;
181        self.local_stale = true;
182    }
183
184    /// The worker at `idx` became unavailable and is removed, its load state
185    /// is kept for [`unpark`](Self::unpark).
186    pub(crate) fn park(&mut self, idx: usize, name: &str, arbiter: Id) {
187        let state = self.states.remove(idx);
188        self.parked.insert(name.to_owned(), (arbiter, state));
189        self.refreshed = None;
190        self.local_stale = true;
191    }
192
193    /// Returns `true` if [`select`](Self::select) uses the incoming CPU of
194    /// items, i.e. the mode is [`LoadBalancing::IncomingCpu`] and some
195    /// workers are placed on CPUs.
196    pub(crate) fn uses_incoming_cpu<T>(&mut self, workers: &[Worker<T>]) -> bool {
197        if self.mode != LoadBalancing::IncomingCpu {
198            return false;
199        }
200        if self.local_stale {
201            self.local_stale = false;
202            self.map_cpus(workers.iter().map(Worker::cpus));
203        }
204        !self.local.is_empty()
205    }
206
207    /// Returns the index of the worker for the next item, and whether it was
208    /// chosen for the incoming CPU of the item if that CPU is set.
209    ///
210    /// `workers` must not be empty.
211    pub(crate) fn select<T>(
212        &mut self,
213        workers: &[Worker<T>],
214        cpu: Option<usize>,
215    ) -> (usize, Option<bool>) {
216        match self.mode {
217            LoadBalancing::RoundRobin => (self.round_robin(workers.len()), None),
218            LoadBalancing::LeastLoaded | LoadBalancing::IncomingCpu => {
219                let now = Instant::now();
220                self.refresh(now, |idx| Sample::of(&workers[idx], now));
221                let load = |idx: usize| workers[idx].load();
222                let idx = self.least_loaded(load);
223                match cpu {
224                    Some(cpu) if self.uses_incoming_cpu(workers) => {
225                        let local = self.local_worker(cpu, idx, load);
226                        (local.unwrap_or(idx), Some(local.is_some()))
227                    }
228                    _ => (idx, None),
229                }
230            }
231        }
232    }
233
234    /// Next worker in turn that is not blocked, `len` workers.
235    fn round_robin(&mut self, len: usize) -> usize {
236        if self.next >= len {
237            self.next = len - 1;
238        }
239        // blocked flags are refreshed by `sample`
240        (0..len)
241            .map(|i| (self.next + i) % len)
242            .find(|&i| !self.is_blocked(i))
243            .unwrap_or(self.next)
244    }
245
246    /// Refreshes the load state of the workers.
247    ///
248    /// Returns `true` if some workers are blocked and others are not.
249    pub(crate) fn sample<T>(&mut self, workers: &[Worker<T>]) -> bool {
250        let now = Instant::now();
251        self.refresh(now, |idx| Sample::of(&workers[idx], now));
252        !self.ready.is_empty() && self.ready.len() < self.states.len()
253    }
254
255    /// Returns `true` if the worker at `idx` was blocked when last sampled.
256    pub(crate) fn is_blocked(&self, idx: usize) -> bool {
257        self.states.get(idx).is_some_and(|st| st.blocked)
258    }
259
260    /// The item was sent to the worker at `idx`.
261    pub(crate) fn sent(&mut self, idx: usize, len: usize) {
262        self.next = (idx + 1) % len;
263    }
264
265    fn refresh(&mut self, now: Instant, sample: impl Fn(usize) -> Sample) {
266        if self
267            .refreshed
268            .is_some_and(|t| now.saturating_duration_since(t) < REFRESH)
269        {
270            return;
271        }
272        self.refreshed = Some(now);
273        self.ready.clear();
274        for (idx, st) in self.states.iter_mut().enumerate() {
275            st.update(sample(idx), now);
276            if !st.blocked {
277                self.ready.push(idx);
278            }
279        }
280    }
281
282    /// Power of two choices among the workers that are not blocked.
283    fn least_loaded(&mut self, load: impl Fn(usize) -> usize) -> usize {
284        let all = self.ready.is_empty();
285        let len = if all { self.states.len() } else { self.ready.len() };
286        if len < 2 {
287            return if all { 0 } else { self.ready[0] };
288        }
289
290        let a = self.random(len);
291        let mut b = self.random(len - 1);
292        if b >= a {
293            b += 1;
294        }
295        let (a, b) = if all {
296            (a, b)
297        } else {
298            (self.ready[a], self.ready[b])
299        };
300
301        let (ua, ub) = (self.states[a].util, self.states[b].util);
302        if (ua - ub).abs() > UTIL_SLACK {
303            if ua < ub { a } else { b }
304        } else if load(b) < load(a) {
305            b
306        } else {
307            a
308        }
309    }
310
311    /// Maps CPUs to the workers placed on them, `cpus` yields the CPU of
312    /// each worker followed by its SMT siblings.
313    fn map_cpus<'a>(&mut self, cpus: impl Iterator<Item = &'a [usize]>) {
314        let cpus: Vec<_> = cpus.collect();
315        self.local.clear();
316        for (idx, cpus) in cpus.iter().enumerate() {
317            if let Some(&cpu) = cpus.first() {
318                self.local.entry(cpu).or_default().push(idx);
319            }
320        }
321        let mut siblings = HashMap::<usize, Vec<usize>>::default();
322        for (idx, cpus) in cpus.iter().enumerate() {
323            for cpu in cpus.iter().skip(1) {
324                if !self.local.contains_key(cpu) {
325                    siblings.entry(*cpu).or_default().push(idx);
326                }
327            }
328        }
329        self.local.extend(siblings);
330    }
331
332    /// Returns the least loaded worker for the incoming `cpu`, unless it is
333    /// busier than `other`, the least loaded choice.
334    fn local_worker(
335        &self,
336        cpu: usize,
337        other: usize,
338        load: impl Fn(usize) -> usize,
339    ) -> Option<usize> {
340        let util = |idx: usize| self.states[idx].util;
341        let best = self
342            .local
343            .get(&cpu)?
344            .iter()
345            .copied()
346            .filter(|&idx| !self.states[idx].blocked)
347            .min_by(|&a, &b| {
348                util(a)
349                    .total_cmp(&util(b))
350                    .then_with(|| load(a).cmp(&load(b)))
351            })?;
352        let max_load = load(other) + load(other) / 2 + LOCAL_LOAD_SLACK;
353        (best == other || (util(best) <= util(other) + LOCAL_UTIL_SLACK && load(best) <= max_load))
354            .then_some(best)
355    }
356
357    #[allow(clippy::cast_possible_truncation)]
358    /// Returns a random number in `0..n`.
359    fn random(&mut self, n: usize) -> usize {
360        // xorshift64
361        let mut x = self.rng;
362        x ^= x << 13;
363        x ^= x >> 7;
364        x ^= x << 17;
365        self.rng = x;
366        (x % n as u64) as usize
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    const MS: Duration = Duration::from_millis(1);
375
376    fn balancer(n: usize) -> Balancer {
377        let mut b = Balancer::new(LoadBalancing::LeastLoaded);
378        for i in 0..n {
379            b.insert(i);
380        }
381        b
382    }
383
384    fn sample(busy_ms: u32, blocked: bool) -> Sample {
385        Sample {
386            busy: Some(MS * busy_ms),
387            blocked,
388        }
389    }
390
391    #[test]
392    fn utilization_average() {
393        let t = Instant::now();
394        let mut st = State::default();
395        st.update(sample(0, false), t);
396        assert!(st.util.abs() < f64::EPSILON);
397
398        // fully busy for one time constant
399        st.update(sample(250, false), t + MS * 250);
400        assert!((st.util - 0.5).abs() < 1e-9, "{}", st.util);
401
402        // idle
403        st.update(sample(250, false), t + MS * 500);
404        assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
405
406        // busy time is not available, or there is no time between samples
407        st.update(Sample::default(), t + MS * 600);
408        assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
409        st.update(sample(0, false), t + MS * 700);
410        st.update(sample(0, false), t + MS * 700);
411        assert!((st.util - 0.25).abs() < 1e-9, "{}", st.util);
412    }
413
414    #[test]
415    fn refresh_interval() {
416        let t = Instant::now();
417        let mut b = balancer(2);
418        b.refresh(t, |_| sample(0, false));
419        assert_eq!(b.ready, [0, 1]);
420
421        // samples are taken at most every `REFRESH`
422        b.refresh(t + MS, |_| sample(0, true));
423        assert_eq!(b.ready, [0, 1]);
424        b.refresh(t + REFRESH, |i| sample(0, i == 0));
425        assert_eq!(b.ready, [1]);
426
427        // worker list changes force a refresh
428        b.insert(2);
429        b.refresh(t + REFRESH, |_| sample(0, false));
430        assert_eq!(b.ready, [0, 1, 2]);
431    }
432
433    #[test]
434    fn skips_blocked_workers() {
435        let t = Instant::now();
436        let mut b = balancer(3);
437        b.refresh(t, |i| sample(0, i != 2));
438        for _ in 0..100 {
439            assert_eq!(b.least_loaded(|_| 0), 2);
440        }
441
442        // all workers are blocked
443        b.refresh(t + REFRESH, |_| sample(0, true));
444        let mut seen = [false; 3];
445        for _ in 0..100 {
446            seen[b.least_loaded(|_| 0)] = true;
447        }
448        assert_eq!(seen, [true; 3]);
449    }
450
451    #[test]
452    fn round_robin_skips_blocked_workers() {
453        let t = Instant::now();
454        let mut b = balancer(3);
455        b.refresh(t, |i| sample(0, i == 1));
456        let mut seen = Vec::new();
457        for _ in 0..4 {
458            let idx = b.round_robin(3);
459            b.sent(idx, 3);
460            seen.push(idx);
461        }
462        assert_eq!(seen, [0, 2, 0, 2]);
463
464        // all workers are blocked
465        b.refresh(t + REFRESH, |_| sample(0, true));
466        assert_eq!(b.round_robin(3), 0);
467    }
468
469    #[test]
470    fn prefers_less_utilized_workers() {
471        let t = Instant::now();
472        let mut b = balancer(2);
473        b.refresh(t, |_| sample(0, false));
474        b.refresh(t + MS * 100, |i| {
475            sample(if i == 0 { 100 } else { 10 }, false)
476        });
477        assert!(b.states[0].util > b.states[1].util + UTIL_SLACK);
478        for _ in 0..100 {
479            // utilization wins over load
480            assert_eq!(b.least_loaded(|i| if i == 0 { 0 } else { 100 }), 1);
481        }
482    }
483
484    #[test]
485    fn compares_load_of_similar_workers() {
486        let t = Instant::now();
487        let mut b = balancer(4);
488        b.refresh(t, |_| sample(0, false));
489        b.refresh(t + MS * 100, |i| sample([10, 11, 12, 13][i], false));
490        let mut seen = [0; 4];
491        for _ in 0..1000 {
492            seen[b.least_loaded(|i| [5, 1, 2, 3][i])] += 1;
493        }
494        // the most loaded worker never wins a comparison
495        assert_eq!(seen[0], 0);
496        assert!(
497            seen[1] > seen[2] && seen[2] > seen[3] && seen[3] > 0,
498            "{seen:?}"
499        );
500    }
501
502    #[test]
503    fn single_worker() {
504        let mut b = balancer(1);
505        b.refresh(Instant::now(), |_| sample(0, true));
506        assert_eq!(b.least_loaded(|_| 0), 0);
507    }
508
509    #[ntex::test]
510    async fn keeps_state_of_unavailable_workers() {
511        let (a1, a2) = (ntex_rt::Arbiter::new(), ntex_rt::Arbiter::new());
512        let t = Instant::now();
513        let mut b = balancer(2);
514        b.refresh(t, |_| sample(0, false));
515        b.refresh(t + MS * 100, |i| {
516            sample(if i == 0 { 100 } else { 10 }, false)
517        });
518        let util = b.states[0].util;
519        assert!(util > b.states[1].util + UTIL_SLACK);
520
521        // the busy worker becomes unavailable and available again
522        b.park(0, "w0", a1.id());
523        b.unpark(0, "w0", a1.id());
524        assert!((b.states[0].util - util).abs() < f64::EPSILON);
525        for _ in 0..100 {
526            assert_eq!(b.least_loaded(|i| [1, 100][i]), 1);
527        }
528
529        // a restarted worker starts over
530        b.park(0, "w0", a1.id());
531        b.unpark(0, "w0", a2.id());
532        assert!(b.states[0].util.abs() < f64::EPSILON);
533        assert!(b.parked.is_empty());
534        a1.stop();
535        a2.stop();
536    }
537
538    #[test]
539    fn map_cpus() {
540        let mut b = balancer(5);
541        // workers 0 and 1 on the two CPUs of core (0, 1), workers 2 and 3
542        // on CPU 2 and 4 of cores (2, 3) and (4, 5), worker 4 not placed
543        b.map_cpus([&[0, 1][..], &[1, 0], &[2, 3], &[4, 5], &[]].into_iter());
544        let mut local: Vec<_> = b.local.iter().map(|(c, w)| (*c, w.clone())).collect();
545        local.sort();
546        assert_eq!(
547            local,
548            [
549                (0, vec![0]),
550                (1, vec![1]),
551                (2, vec![2]),
552                (3, vec![2]),
553                (4, vec![3]),
554                (5, vec![3])
555            ]
556        );
557
558        // several workers on one CPU
559        b.map_cpus([&[0][..], &[0]].into_iter());
560        assert_eq!(b.local.get(&0), Some(&vec![0, 1]));
561        assert_eq!(b.local.len(), 1);
562    }
563
564    #[test]
565    fn local_worker() {
566        let t = Instant::now();
567        let mut b = balancer(4);
568        b.map_cpus([&[0][..], &[1], &[2], &[2]].into_iter());
569        b.refresh(t, |_| sample(0, false));
570        b.refresh(t + MS * 100, |i| sample([10, 0, 100, 90][i], false));
571        let load = |_| 10;
572
573        // the worker on the incoming CPU is preferred
574        assert_eq!(b.local_worker(0, 1, load), Some(0));
575        // the least utilized of the workers on the CPU
576        assert_eq!(b.local_worker(2, 2, load), Some(3));
577        // no worker on the CPU
578        assert_eq!(b.local_worker(7, 1, load), None);
579        // utilization exceeds the other choice by more than the slack
580        assert!(b.states[3].util > b.states[1].util + LOCAL_UTIL_SLACK);
581        assert_eq!(b.local_worker(2, 1, load), None);
582        b.map_cpus([&[0][..], &[1], &[2], &[3]].into_iter());
583        assert_eq!(b.local_worker(2, 1, load), None);
584        assert_eq!(b.local_worker(2, 2, load), Some(2));
585
586        // load exceeds the other choice by more than half plus the slack
587        let load = |i: usize| [100, 10, 0, 0][i];
588        assert_eq!(b.local_worker(0, 1, load), None);
589        let load = |i: usize| [100, 56, 0, 0][i];
590        assert_eq!(b.local_worker(0, 1, load), Some(0));
591
592        // blocked worker
593        b.refresh(t + MS * 200, |i| sample([10, 0, 100, 90][i], i == 0));
594        assert_eq!(b.local_worker(0, 1, |_| 0), None);
595    }
596
597    #[test]
598    fn random_range() {
599        let mut b = balancer(0);
600        let mut seen = [false; 5];
601        for _ in 0..1000 {
602            seen[b.random(5)] = true;
603        }
604        assert_eq!(seen, [true; 5]);
605    }
606}