香港特惠服务器
三网CN2带宽,提供30M至100M大带宽,保障CN2/CMIN2/CU/PCCW四大运营商线路稳定接入

| 类别 | 规格/版本 | 说明 |
|---|---|---|
| 机房 | 香港将军澳机柜 x2 | 双上联,ToR 聚合 |
| 服务器 | 2× Intel Xeon Silver 4210R / 128GB RAM | 10G SFP+ 双口 |
| 网卡 | Intel X710-DA2(ixgbe/ice 家族) | 单机 CN2 专用口 eth1 |
| 操作系统 | RHEL 9.2(内核 5.14.x) | 默认支持 BPF & BTF |
| 上联 | CN2 GIA 1 Gbps 物理口 | 目标口:eth1(或 bond1.392 等 VLAN 子接口) |
| 编译工具 | clang/llvm、bpftool、libbpf | 通过 dnf 安装 |
| 监控 | Prometheus + Grafana | 10s 抓取/1s 采样 |
| 预警 | Slack Webhook | 双阈值策略 |
# 1) 基础包
sudo dnf -y install clang llvm bpftool libbpf libbpf-devel \
kernel-headers kernel-devel elfutils-libelf-devel make git \
iproute-tc ethtool golang
# 2) 挂载 bpf 和 tracefs(通常已挂载)
sudo mount -t bpf bpf /sys/fs/bpf || true
sudo mount -t tracefs nodev /sys/kernel/tracing || true
# 3) 确认 BPF JIT(可选)
cat /proc/sys/net/core/bpf_jit_enable
# 若为 1/2 均可;不是 1 就:
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_enable
# 4) 找到 CN2 接口 ifindex
cat /sys/class/net/eth1/ifindex
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
char LICENSE[] SEC("license") = "GPL";
struct key {
__u32 ifindex;
__u32 dir; // 0=ingress, 1=egress
};
// per-CPU hash,value 是累计字节数
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 1024);
__type(key, struct key);
__type(value, __u64);
} bytes SEC(".maps");
// 统一的累加函数
static __always_inline int count_bytes(struct __sk_buff *skb, __u32 dir)
{
struct key k = {
.ifindex = skb->ifindex,
.dir = dir,
};
__u64 initv = 0, *val = bpf_map_lookup_elem(&bytes, &k);
__u64 len = (__u64)skb->len;
if (!val) {
// 第一次出现该 key,插入 0 后再取
bpf_map_update_elem(&bytes, &k, &initv, BPF_NOEXIST);
val = bpf_map_lookup_elem(&bytes, &k);
if (!val) return BPF_OK;
}
// 累加字节
__sync_fetch_and_add(val, len);
return BPF_OK;
}
SEC("tc")
int tc_ingress(struct __sk_buff *skb)
{
return count_bytes(skb, 0);
}
SEC("tc")
int tc_egress(struct __sk_buff *skb)
{
return count_bytes(skb, 1);
}
DEV=eth1 # 你的 CN2 接口
# 1) 添加 clsact(幂等处理)
sudo tc qdisc del dev $DEV clsact 2>/dev/null || true
sudo tc qdisc add dev $DEV clsact
# 2) 挂 ingress/egress 的 eBPF 程序
sudo tc filter add dev $DEV ingress bpf direct-action obj cn2_bw.bpf.o sec tc_ingress
sudo tc filter add dev $DEV egress bpf direct-action obj cn2_bw.bpf.o sec tc_egress
# 3) 把 map pin 到 bpffs,便于用户态读取
sudo bpftool prog show | grep cn2_bw | awk '{print $1,$2,$3,$4,$5,$6,$7,$8}'
# 找到 bytes map 的 id
MAP_ID=$(sudo bpftool map show | awk '/bytes/ {print $1}' | sed 's/id//')
sudo mkdir -p /sys/fs/bpf/cn2bw
sudo bpftool map pin id $MAP_ID /sys/fs/bpf/cn2bw/bytes
# 验证(查看当前 map)
sudo bpftool map dump pinned /sys/fs/bpf/cn2bw/bytes | head
瞬时告警:过去 30s 内,任意 20s 有 util > 80%。
listen_addr: ":9109"
interfaces:
- name: "eth1" # CN2 口
ifindex: 0 # 0 表示自动从 /sys/class/net/name/ifindex 读取
port_capacity_bps: 1000000000 # 1 Gbps
tso_correction: 1.03 # egress 校准系数(TSO/GSO 情况下适当>1)
alerting:
webhook: "https://hooks.slack.com/services/XXXX/XXXX/XXXX"
instant_util_threshold: 0.80
instant_window_sec: 30
instant_required_high_secs: 20
sustained_util_p95_threshold: 0.70
sustained_window_sec: 600
bpf:
pinned_map: "/sys/fs/bpf/cn2bw/bytes"
sample_interval_sec: 1
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/cilium/ebpf"
"gopkg.in/yaml.v3"
)
type IfCfg struct {
Name string `yaml:"name"`
Ifindex int `yaml:"ifindex"`
PortCapacityBps uint64 `yaml:"port_capacity_bps"`
TsoCorrection float64 `yaml:"tso_correction"`
}
type AlertCfg struct {
Webhook string `yaml:"webhook"`
InstantUtilThreshold float64 `yaml:"instant_util_threshold"`
InstantWindowSec int `yaml:"instant_window_sec"`
InstantRequiredHighSecs int `yaml:"instant_required_high_secs"`
SustainedUtilP95Threshold float64 `yaml:"sustained_util_p95_threshold"`
SustainedWindowSec int `yaml:"sustained_window_sec"`
}
type Cfg struct {
ListenAddr string `yaml:"listen_addr"`
Interfaces []IfCfg `yaml:"interfaces"`
BPF struct {
PinnedMap string `yaml:"pinned_map"`
SampleIntervalSec int `yaml:"sample_interval_sec"`
} `yaml:"bpf"`
Alerting AlertCfg `yaml:"alerting"`
}
type key struct {
Ifindex uint32
Dir uint32 // 0 ingress, 1 egress
}
type perIfStats struct {
mu sync.Mutex
// 累计字节
totalIn, totalOut uint64
// 上一次读到的累计字节
lastIn, lastOut uint64
// 当前速率(bps)
bpsIn, bpsOut float64
// 最近窗口的利用率缓存
utilHist []float64
}
func mustIfindex(name string) int {
if idx, err := os.ReadFile("/sys/class/net/" + name + "/ifindex"); err == nil {
i, _ := strconv.Atoi(strings.TrimSpace(string(idx)))
return i
}
log.Fatalf("cannot read ifindex for %s", name)
return 0
}
func p95(xs []float64) float64 {
if len(xs) == 0 {
return 0
}
cp := append([]float64(nil), xs...)
// 简单选择算法
n := int(math.Ceil(0.95 * float64(len(cp))))
for i := 0; i < n; i++ {
minIdx := i
for j := i + 1; j < len(cp); j++ {
if cp[j] < cp[minIdx] {
minIdx = j
}
}
cp[i], cp[minIdx] = cp[minIdx], cp[i]
}
return cp[n-1]
}
func postWebhook(url, text string) {
if url == "" { return }
body := `{"text":` + strconv.Quote(text) + `}`
http.Post(url, "application/json", bytes.NewBufferString(body))
}
func main() {
// 读取配置
cfgData, err := os.ReadFile("/etc/cn2-bw/config.yaml")
if err != nil { log.Fatal(err) }
var cfg Cfg
if err := yaml.Unmarshal(cfgData, &cfg); err != nil { log.Fatal(err) }
// 打开 pinned map
m, err := ebpf.LoadPinnedMap(cfg.BPF.PinnedMap, nil)
if err != nil { log.Fatalf("open pinned map: %v", err) }
defer m.Close()
// 接口状态结构
stats := map[int]*perIfStats{}
for i := range cfg.Interfaces {
if cfg.Interfaces[i].Ifindex == 0 {
cfg.Interfaces[i].Ifindex = mustIfindex(cfg.Interfaces[i].Name)
}
stats[cfg.Interfaces[i].Ifindex] = &perIfStats{
utilHist: make([]float64, 0, cfg.Alerting.SustainedWindowSec),
}
}
// 抓取协程
go func() {
ticker := time.NewTicker(time.Duration(cfg.BPF.SampleIntervalSec) * time.Second)
defer ticker.Stop()
for range ticker.C {
// 遍历 map
itr := m.Iterate()
var rawK, rawV []byte
for {
if ok := itr.Next(&rawK, &rawV); !ok {
if err := itr.Err(); err != nil && err != io.EOF {
log.Printf("iterate err: %v", err)
}
break
}
if len(rawK) < 8 || len(rawV) < 8 { continue }
var k key
var v uint64
binary.Read(bytes.NewReader(rawK[:8]), binary.LittleEndian, &k)
binary.Read(bytes.NewReader(rawV[:8]), binary.LittleEndian, &v)
if st, ok := stats[int(k.Ifindex)]; ok {
st.mu.Lock()
if k.Dir == 0 {
st.totalIn = v
} else {
st.totalOut = v
}
st.mu.Unlock()
}
}
// 计算 bps 和利用率
for _, ic := range cfg.Interfaces {
st := stats[ic.Ifindex]
st.mu.Lock()
dIn := st.totalIn - st.lastIn
dOut := st.totalOut - st.lastOut
st.lastIn = st.totalIn
st.lastOut = st.totalOut
interval := float64(cfg.BPF.SampleIntervalSec)
st.bpsIn = float64(dIn)*8.0/interval
st.bpsOut = float64(dOut)*8.0/interval * ic.TsoCorrection
util := math.Max(st.bpsIn, st.bpsOut) / float64(ic.PortCapacityBps)
if len(st.utilHist) >= cfg.Alerting.SustainedWindowSec {
st.utilHist = st.utilHist[1:]
}
st.utilHist = append(st.utilHist, util)
st.mu.Unlock()
}
}
}()
// 告警协程
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
for _, ic := range cfg.Interfaces {
st := stats[ic.Ifindex]
st.mu.Lock()
h := append([]float64(nil), st.utilHist...)
st.mu.Unlock()
// 即时告警(过去 N 秒有 M 秒超过阈值)
if len(h) >= cfg.Alerting.InstantWindowSec {
win := h[len(h)-cfg.Alerting.InstantWindowSec:]
countHigh := 0
for _, u := range win {
if u >= cfg.Alerting.InstantUtilThreshold { countHigh++ }
}
if countHigh >= cfg.Alerting.InstantRequiredHighSecs {
postWebhook(cfg.Alerting.Webhook,
fmt.Sprintf("[CN2拥塞-瞬时] iface=%s util>=%.0f%% %d/%ds",
ic.Name, cfg.Alerting.InstantUtilThreshold*100,
countHigh, cfg.Alerting.InstantWindowSec))
}
}
// 持续拥塞(p95 超阈)
if len(h) >= cfg.Alerting.SustainedWindowSec {
p := p95(h[len(h)-cfg.Alerting.SustainedWindowSec:])
if p >= cfg.Alerting.SustainedUtilP95Threshold {
postWebhook(cfg.Alerting.Webhook,
fmt.Sprintf("[CN2拥塞-持续] iface=%s p95(util)=%.1f%% in last %ds",
ic.Name, p*100, cfg.Alerting.SustainedWindowSec))
}
}
}
}
}()
// Prometheus /metrics
http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
var b strings.Builder
b.WriteString("# HELP cn2_bps_in Ingress bits per second\n")
b.WriteString("# TYPE cn2_bps_in gauge\n")
b.WriteString("# HELP cn2_bps_out Egress bits per second\n")
b.WriteString("# TYPE cn2_bps_out gauge\n")
b.WriteString("# HELP cn2_utilization Link utilization (0-1)\n")
b.WriteString("# TYPE cn2_utilization gauge\n")
for _, ic := range cfg.Interfaces {
st := stats[ic.Ifindex]
st.mu.Lock()
b.WriteString(fmt.Sprintf("cn2_bps_in{iface=%q} %f\n", ic.Name, st.bpsIn))
b.WriteString(fmt.Sprintf("cn2_bps_out{iface=%q} %f\n", ic.Name, st.bpsOut))
util := math.Max(st.bpsIn, st.bpsOut) / float64(ic.PortCapacityBps)
b.WriteString(fmt.Sprintf("cn2_utilization{iface=%q} %f\n", ic.Name, util))
st.mu.Unlock()
}
io.WriteString(w, b.String())
})
log.Printf("listening on %s", cfg.ListenAddr)
log.Fatal(http.ListenAndServe(cfg.ListenAddr, nil))
}
cd /opt && sudo mkdir -p cn2-bw && cd cn2-bw
sudo mkdir -p cmd/cn2-bw-exporter
# 将 main.go 放到 cmd/cn2-bw-exporter/ 下
go mod init cn2-bw
go get github.com/cilium/ebpf gopkg.in/yaml.v3
go build -o /usr/local/bin/cn2-bw-exporter ./cmd/cn2-bw-exporter
# 配置文件
sudo mkdir -p /etc/cn2-bw
sudo tee /etc/cn2-bw/config.yaml >/dev/null <<'YAML'
# (粘贴上面的 YAML)
YAML
# systemd 服务
sudo tee /etc/systemd/system/cn2-bw-exporter.service >/dev/null <<'UNIT'
[Unit]
Description=CN2 bandwidth exporter (eBPF)
After=network-online.target
[Service]
ExecStart=/usr/local/bin/cn2-bw-exporter
Restart=always
RestartSec=2
AmbientCapabilities=CAP_BPF CAP_NET_ADMIN CAP_PERFMON
CapabilityBoundingSet=CAP_BPF CAP_NET_ADMIN CAP_PERFMON
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now cn2-bw-exporter
# server 端(对端)
iperf3 -s
# 本机:上行 800Mbps 持续 120s
iperf3 -c <server-ip> -u -b 800M -l 1400 -t 120
| 时间(壁钟) | Ingress bps | Egress bps | Util(Max(in,out)/1Gbps) |
|---|---|---|---|
| 02:31:00 | 120,531,000 | 225,004,000 | 0.225 |
| 02:31:05 | 118,220,000 | 802,114,000 | 0.802 |
| 02:31:10 | 115,908,000 | 840,321,000 | 0.840 |
| 02:31:15 | 121,777,000 | 876,992,000 | 0.877 |
| 02:31:20 | 119,304,000 | 901,210,000 | 0.901 |
| 02:31:25 | 117,998,000 | 915,433,000 | 0.915 |
| 02:31:30 | 118,412,000 | 934,551,000 | 0.935 |
| 02:31:35 | 120,083,000 | 951,002,000 | 0.951 |
| 02:31:40 | 121,775,000 | 968,331,000 | 0.968 |
| 02:31:45 | 119,990,000 | 983,114,000 | 0.983 |
| 02:31:50 | 118,207,000 | 995,442,000 | 0.995 |
| 02:31:55 | 118,331,000 | 999,882,000 | 1.000 |
- job_name: 'cn2-bw'
scrape_interval: 10s
static_configs:
- targets: ['10.0.0.12:9109']
DEV=eth1
sudo tc filter del dev $DEV ingress
sudo tc filter del dev $DEV egress
sudo tc qdisc del dev $DEV clsact
sudo rm -f /sys/fs/bpf/cn2bw/bytes
sudo systemctl disable --now cn2-bw-exporter