← 中文资源

简体中文版本

轮询选择可用后端

用原子游标按轮询顺序选择健康后端,跳过故障节点,全部不可用时返回 None。

阅读英文原文

功能说明

轮询(round-robin)是负载均衡中最简单的策略:请求依次分配给后端列表中的节点,循环往复。这段代码在轮询基础上增加健康检查,跳过 healthy == false 的节点,并在所有节点都不可用时返回 None,让调用方决定降级或报错。

它适合多副本只读服务、本地连接池选择或测试环境中的简单分流。原子游标让 next 可以并发调用,不需要为每次选择加互斥锁。

关键代码

LoadBalancer 保存后端列表和一个 AtomicUsize 游标。next 先检查列表是否为空,然后最多尝试 count 次:每次用 fetch_add(1, Relaxed) 取当前下标并递增,对列表长度取模,命中健康节点就返回引用。

最多尝试 count 次保证不会因为连续遇到不健康节点而无限循环。set_healthy 需要 &mut self,可以在启动或运维操作时更新状态。

使用注意

Relaxed 只保证原子性,不提供跨线程的顺序保证;这里游标只用于计数,足够使用。set_healthy 不能在并发读取时调用,否则需要 Mutex 或原子布尔字段。

轮询假设各后端容量相同,不适合权重不同的场景。健康状态需要外部探活更新,代码本身不检测超时。游标在节点增删后会跳过或重复部分节点,变更列表时应重建负载均衡器。返回的是引用,调用期间不能修改后端列表。

验证方式

示例中 a、c 健康,b 故障,连续取四次得到 ["a", "c", "a", "c"],证明故障节点被跳过且轮询继续。此时游标已经推进到位置 6,把 b 标记为健康后再取三次,得到 ["a", "b", "c"],可以看出轮询从上次位置继续而不是重置。空列表返回 None。可以加入全部故障的场景,确认返回 None 而不是死循环。

代码示例

use std::sync::atomic::{AtomicUsize, Ordering};

#[derive(Debug, Clone, PartialEq, Eq)]
struct Backend {
    name: &'static str,
    healthy: bool,
}

struct LoadBalancer {
    backends: Vec<Backend>,
    cursor: AtomicUsize,
}

impl LoadBalancer {
    fn new(backends: Vec<Backend>) -> Self {
        Self {
            backends,
            cursor: AtomicUsize::new(0),
        }
    }

    fn next(&self) -> Option<&Backend> {
        let count = self.backends.len();
        if count == 0 {
            return None;
        }
        for _ in 0..count {
            let index = self.cursor.fetch_add(1, Ordering::Relaxed) % count;
            let backend = &self.backends[index];
            if backend.healthy {
                return Some(backend);
            }
        }
        None
    }

    fn set_healthy(&mut self, name: &str, healthy: bool) {
        if let Some(backend) = self.backends.iter_mut().find(|item| item.name == name) {
            backend.healthy = healthy;
        }
    }
}

fn main() {
    let mut balancer = LoadBalancer::new(vec![
        Backend { name: "a", healthy: true },
        Backend { name: "b", healthy: false },
        Backend { name: "c", healthy: true },
    ]);

    let picks: Vec<&str> = (0..4)
        .filter_map(|_| balancer.next().map(|backend| backend.name))
        .collect();
    println!("{picks:?}");
    assert_eq!(picks, vec!["a", "c", "a", "c"]);

    balancer.set_healthy("b", true);
    let names: Vec<&str> = (0..3)
        .filter_map(|_| balancer.next().map(|backend| backend.name))
        .collect();
    assert_eq!(names, vec!["a", "b", "c"]);
    assert_eq!(LoadBalancer::new(Vec::new()).next(), None);
}