中文
Sentinel 结合 eBPF 的探索
Sentinel 是一款面向微服务的高可用流控防护组件;eBPF 程序,以 XDP 为例,可以尽早地对网络包进行丢弃,减少进入协议栈的包数。
本文将探索 Sentinel-Rust 与 eBPF 的结合,在这个例子中,我们将端口抽象成 Sentinel 中的资源进行统一管理,根据对应资源(端口) Sentinel 的创建情况在 XDP 中丢弃包。eBPF 程序的编写基于 redbpf 库。
内核 XDP 程序
在 内核程序 中创建两个 eBPF map,用来做用户态 Sentinel 创建程序和内核中的 XDP 程序的通讯。port_events
记录的是某个端口接收到了包这一事件,而 port_blocked
则是一个数组,它的下标对应端口号。
rust
#[map]
static mut port_events: PerfMap<PortEvent> = PerfMap::with_max_entries(1024);
#[map]
static mut port_blocked: Array<bool> = Array::with_max_entries(1 << 16);
接下来编写一个简单的 XDP 程序,我们只检测接收到的包的目的端口号,触发一个事件提交到 port_events
中,该事件会在用户态程序中被捕获到;XDP程序会检测 port_blocked
中 Sentinel 是否创建失败了,如果 Sentinel 创建失败,那么可能是由于该端口的 QPS 过高,因此可以直接丢弃掉该包。
rust
#[xdp]
pub fn block_port(ctx: XdpContext) -> XdpResult {
if let Ok(transport) = ctx.transport() {
let port = transport.dest();
let event = MapData::new(PortEvent { port });
unsafe { port_events.insert(&ctx, &event) };
// the mmapped memory port_blocked not sync between kernel and userspace
let blocked = unsafe { port_blocked.get(port as u32) };
if let Some(&blocked) = blocked {
if blocked {
return Ok(XdpAction::Drop);
}
}
}
Ok(XdpAction::Pass)
}
用户态 Sentinel 程序
在 用户态,我们首先完成 Sentinel 的初始化程序,之后加载 XDP 程序并将它注入到某个网卡上(示例中选择了 lo
)。之后我们加载 Sentinel 的流控规则。这里我们设置名为 port:8000
的资源的 QPS 的阈值为 1.0,即每秒仅能有一个该资源被创建。
rust
flow::load_rules(vec![Arc::new(flow::Rule {
resource: "port:8000".into(),
threshold: 1.0,
calculate_strategy: flow::CalculateStrategy::Direct,
control_strategy: flow::ControlStrategy::Reject,
..Default::default()
})]);
完成上述初始化后,我们监听 MPSC 的 event 队列。当检测到 port_events
中的事件时,我们使用 port:{}
的命名格式去构建 Sentinel,当构建成功/失败时,更改 port_blocked
的状态以便指导 XDP 程序。
rust
while let Some((map_name, events)) = loaded.events.next().await {
let port_blocked_map = loaded.map("port_blocked").unwrap();
let port_blocked =
Array::<bool>::new(port_blocked_map).unwrap();
for event in events {
match map_name.as_str() {
"port_events" => {
let event = unsafe { std::ptr::read(event.as_ptr() as *const PortEvent) };
let entry_builder = EntryBuilder::new(format!("port:{}", event.port))
.with_traffic_type(base::TrafficType::Inbound);
if let Ok(entry) = entry_builder.build() {
port_blocked
.set(event.port as u32, false)
.unwrap();
entry.exit()
} else {
port_blocked
.set(event.port as u32, true)
.unwrap();
}
}
_ => panic!("unexpected event"),
}
}
}
思考
当然这里有一个问题:用户态的 Sentinel 创建程序和内核中的 XDP 程序对 port_blocked
这个 ebpf map 的读写是不同步的,这在初始化时尤为明显。例如将 Sentinel 的规则设置为禁止 8000
端口的所有流量,即 threshold
设置为 0,仍然可以完成第一次请求。
是否可以去做同步呢?一般来讲,eBPF 一定是非阻塞的程序,也可以说是原子的。LWN 的 一篇文章 介绍了 BPF_PROG_TYPE_LSM
和 BPF_PROG_TYPE_LSM
两类 eBPF 程序中的标志 BPF_F_SLEEPABLE
。即使是我们有某种同步手段,阻塞 XDP 的执行似乎仍然不是一个明智的选择。