将 P2P 系统从原型推向生产环境,需要面对连接管理、安全防护、可观测性等一系列工程挑战。本文从实战角度,覆盖连接资源控制、消息序列化、安全策略、监控体系、容器化部署和故障排查六大主题,并提供可复用的代码片段。
连接管理与资源限制
P2P 节点需要同时维持大量连接,不加限制的资源消耗会导致节点 OOM 或文件描述符耗尽。生产环境下必须对以下三个维度进行严格控制。
三层资源管控模型
flowchart LR
subgraph 传输层限制
A1["最大入站连接<br/>MaxInbound: 1024"] --> B1["连接队列"]
A2["最大出站连接<br/>MaxOutbound: 512"] --> B1
end
subgraph 流层限制
C1["入站并发流<br/>MaxInboundStreams: 128"] --> D1["流调度器"]
C2["出站并发流<br/>MaxOutboundStreams: 256"] --> D1
end
subgraph 空闲管理
E1["连接空闲超时<br/>IdleTimeout: 30s"] --> F1{"健康检查"}
E2["流空闲超时<br/>StreamTimeout: 60s"] --> F1
F1 -->|"通过"| G1["更新活跃时间戳"]
F1 -->|"失败"| G2["关闭连接/流<br/>释放 FD 与内存"]
end
B1 --> C1
B1 --> C2
D1 --> E1
D1 --> E2
style A1 fill:#4CAF50,color:#fff
style A2 fill:#4CAF50,color:#fff
style C1 fill:#2196F3,color:#fff
style C2 fill:#2196F3,color:#fff
style E1 fill:#FF9800,color:#fff
style E2 fill:#FF9800,color:#fff
Rust 连接管理配置
Rust 的 SwarmBuilder 提供了链式 API 配置连接参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
| use std::time::Duration;
use libp2p::swarm::{SwarmBuilder, SwarmConfig};
let swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, peer_id)
// 最大协商中的入站流数量,超过则拒绝
.max_negotiating_inbound_streams(128)
// 连接空闲超时:30 秒内无任何协议活动则自动关闭
.connection_idle_timeout(Duration::from_secs(30))
// 订阅连接状态变化通知
.notify_handler_buffer_size(64)
// 连接背压缓冲区
.connection_event_buffer_size(64)
.build();
|
空闲超时机制详解:connection_idle_timeout 是防止僵尸连接的关键参数。当连接上没有活跃的流(stream)且没有待处理的协议协商时,计时器启动。每次有新流创建或协议完成时重置计时器。生产环境推荐值 30-60 秒——太短会导致频繁重建连接,太长会浪费资源。
Go 资源管理器配置
Go 的 ResourceManager 提供了更细粒度的内存和连接控制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| import (
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/p2p/host/resource-manager"
)
func setupResourceManager() (libp2p.Option, error) {
// 使用默认限流器并调整限制
limiter := rcmgr.NewDefaultLimiter()
// 调整全局限制
limiter.IncreaseLimit(rcmgr.BaseLimit{
Streams: 1024,
StreamsInbound: 256,
StreamsOutbound: 768,
Connections: 400,
ConnectionsInbound: 200,
ConnectionsOutbound: 200,
Memory: 256 << 20, // 256 MB
FD: 512,
})
rm, err := rcmgr.NewResourceManager(limiter)
if err != nil {
return nil, err
}
return libp2p.ResourceManager(rm), nil
}
func main() {
rmOpt, _ := setupResourceManager()
h, err := libp2p.New(
libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"),
rmOpt,
libp2p.ConnectionGater(NewCustomGater()), // 自定义连接门控
)
if err != nil {
panic(err)
}
defer h.Close()
}
|
建议为 ConnectionGater 实现白名单/黑名单逻辑,在建立连接前拦截恶意地址。
消息序列化与压缩
P2P 网络中消息体积直接占用带宽,序列化效率影响节点吞吐量。合理选择序列化方案可减少 60%-80% 的网络传输量。
序列化方案对比
| 方案 | 编码体积 | 解析速度 | Schema 约束 | 版本兼容 | 适用场景 |
|---|
| Protobuf | 小(~30% of JSON) | 快 | 严格 .proto | 支持向后兼容 | 通用 RPC、结构化数据 |
| FlatBuffers | 中(~40% of JSON) | 极快(零拷贝) | 严格 .fbs | 支持 | 高频低延迟消息 |
| Cap’n Proto | 小(~35% of JSON) | 极快(零拷贝) | 严格 | 支持 | 嵌入式、高性能 |
| MessagePack | 中(~50% of JSON) | 中 | 无 | 无 | 兼容 JSON 的轻量场景 |
| JSON | 大(基准) | 慢 | 弱(需手动校验) | 天然兼容 | 调试、日志、简单 API |
Protobuf 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| syntax = "proto3";
package p2p.message;
message Block {
bytes hash = 1; // SHA-256 内容哈希
bytes data = 2; // 原始数据
uint64 timestamp = 3; // Unix 时间戳(毫秒)
uint32 ttl = 4; // 生存时间(秒)
uint32 version = 5; // 消息格式版本
bytes signature = 6; // 发送者签名
}
message PeerInfo {
bytes peer_id = 1;
repeated bytes multiaddrs = 2; // 多地址列表
map<string, string> metadata = 3; // 自定义元数据
}
|
压缩策略
| 算法 | 压缩比 | 速度(编码) | 速度(解码) | 推荐场景 |
|---|
| Snappy | 1.5-2.5x | 极快(~400MB/s) | 极快(~800MB/s) | 延迟敏感的实时消息 |
| Gzip (Level 3) | 3-5x | 中(~50MB/s) | 中(~100MB/s) | 批量同步、大块数据传输 |
| Zstandard (Level 1) | 2.5-4x | 快(~300MB/s) | 快(~500MB/s) | 兼顾压缩比和速度的通用场景 |
经验法则:消息体 < 1 KB 不必压缩,压缩开销反而超过收益。超过 10 KB 的块始终使用 Snappy,批量传输使用 Gzip 或 Zstd。
1
2
3
4
5
6
7
8
9
10
11
12
| // Snappy 压缩示例(Rust)
use snap::raw::{Encoder, Decoder};
fn compress_block(data: &[u8]) -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.compress(data).expect("snappy compress failed")
}
fn decompress_block(compressed: &[u8]) -> Vec<u8> {
let mut decoder = Decoder::new();
decoder.decompress(compressed).expect("snappy decompress failed")
}
|
安全考量
P2P 网络不受信任边界保护,每个节点都可能成为攻击入口。安全必须在协议层内置而非事后补丁。
mindmap
root((P2P 安全))
传输安全
Noise 协议
TLS 1.3
Perfect Forward Secrecy
消息验证
签名验证
防重放攻击
SeqNo 单调递增
资源保护
速率限制
连接数限制
内存配额
数据完整性
SHA-256 校验
增量验证
Merkle 树
节点管理
黑名单机制
信誉系统
行为评分
传输加密
始终启用 Noise 或 TLS 1.3,确保所有传输数据使用 Perfect Forward Secrecy(PFS)加密。libp2p 默认使用 Noise_XK 握手模式:
1
2
3
4
5
6
7
8
9
| use libp2p::noise::{Config as NoiseConfig, AuthenticKeypair, Keypair};
// 生成 Noise 身份密钥
let id_keys = identity::Keypair::generate_ed25519();
let noise_keys = AuthenticKeypair::new(id_keys).unwrap();
// 配置加密传输
let transport = libp2p::tokio_development_transport(id_keys.clone())?;
// Noise 由 tokio_development_transport 内部启用
|
Token Bucket 速率限制
Token Bucket 是控制消息频率的标准算法,实现简单且支持突发流量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::Instant;
pub struct TokenBucket {
capacity: u64, // 桶容量(最大突发量)
tokens: AtomicU64, // 当前令牌数
fill_rate: u64, // 每秒填充速率
last_refill: Mutex<Instant>,
}
impl TokenBucket {
pub fn new(capacity: u64, fill_rate: u64) -> Self {
Self {
capacity,
tokens: AtomicU64::new(capacity),
fill_rate,
last_refill: Mutex::new(Instant::now()),
}
}
pub fn allow(&self) -> bool {
// 1. 原子性补充令牌
if let Ok(mut last) = self.last_refill.lock() {
let now = Instant::now();
let elapsed = now.duration_since(*last).as_secs();
if elapsed > 0 {
let current = self.tokens.load(Ordering::Relaxed);
let new = current.saturating_add(elapsed * self.fill_rate).min(self.capacity);
self.tokens.store(new, Ordering::Release);
*last = now;
}
}
// 2. CAS 消耗一个令牌
loop {
let current = self.tokens.load(Ordering::Acquire);
if current == 0 {
return false; // 令牌耗尽,拒绝
}
if self.tokens.compare_exchange_weak(
current, current - 1,
Ordering::Release, Ordering::Relaxed,
).is_ok() {
return true;
}
}
}
}
|
使用策略:对每个 Peer 维护一个独立的 TokenBucket,根据节点信誉动态调整容量。可信节点分配更大的 fill_rate。
黑名单实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| use std::collections::HashSet;
use std::sync::RwLock;
use libp2p::PeerId;
use std::time::{Duration, Instant};
struct BlacklistEntry {
reason: String,
expires_at: Instant,
}
pub struct PeerBlacklist {
permanent: RwLock<HashSet<PeerId>>,
temporary: RwLock<std::collections::HashMap<PeerId, BlacklistEntry>>,
}
impl PeerBlacklist {
pub fn new() -> Self {
Self {
permanent: RwLock::new(HashSet::new()),
temporary: RwLock::new(std::collections::HashMap::new()),
}
}
pub fn is_blocked(&self, peer: &PeerId) -> bool {
if self.permanent.read().unwrap().contains(peer) {
return true;
}
if let Some(entry) = self.temporary.read().unwrap().get(peer) {
if Instant::now() < entry.expires_at {
return true;
}
// 过期自动清理
}
false
}
pub fn ban_permanent(&self, peer: PeerId, reason: &str) {
self.permanent.write().unwrap().insert(peer);
}
pub fn ban_temporary(&self, peer: PeerId, duration: Duration, reason: &str) {
let mut map = self.temporary.write().unwrap();
map.insert(peer, BlacklistEntry {
reason: reason.to_string(),
expires_at: Instant::now() + duration,
});
}
}
|
消息签名与完整性校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| use libp2p::identity::Keypair;
use sha2::{Sha256, Digest};
#[derive(prost::Message)]
struct SignedMessage {
#[prost(bytes, tag = 1)]
payload: Vec<u8>,
#[prost(bytes, tag = 2)]
signature: Vec<u8>,
#[prost(uint64, tag = 3)]
seq_no: u64, // 单调递增序列号,防重放
}
fn sign_message(keypair: &Keypair, payload: &[u8], seq_no: u64) -> SignedMessage {
let mut data = payload.to_vec();
data.extend_from_slice(&seq_no.to_be_bytes());
let signature = keypair.sign(&data).expect("sign failed");
SignedMessage { payload: payload.to_vec(), signature, seq_no }
}
fn verify_message(public_key: &identity::PublicKey, msg: &SignedMessage) -> bool {
let mut data = msg.payload.clone();
data.extend_from_slice(&msg.seq_no.to_be_bytes());
public_key.verify(&data, &msg.signature).is_ok()
}
fn hash_block(data: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().into()
}
|
监控与可观测性
P2P 网络的分布式特性使得监控比传统客户端-服务器架构更具挑战。完善的指标体系是快速定位问题的前提。
Prometheus 指标采集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| use libp2p::metrics::Metrics;
use prometheus::{Registry, Encoder, TextEncoder};
use lazy_static::lazy_static;
lazy_static! {
static ref REGISTRY: Registry = Registry::new();
}
fn setup_metrics() -> Metrics {
let metrics = Metrics::new(®ISTRY);
// 注册自定义指标
let msg_throughput = prometheus::CounterVec::new(
prometheus::opts!("p2p_messages_total", "Total P2P messages"),
&["direction", "protocol"],
).unwrap();
REGISTRY.register(Box::new(msg_throughput)).unwrap();
metrics
}
fn expose_metrics() -> String {
let encoder = TextEncoder::new();
let metric_families = REGISTRY.gather();
let mut buffer = vec![];
encoder.encode(&metric_families, &mut buffer).unwrap();
String::from_utf8(buffer).unwrap_or_default()
}
// HTTP 端点暴露(通过 /metrics 路由)
// warp::path!("metrics").map(|| warp::reply::body(metrics::expose_metrics()))
|
关键指标与告警阈值
| 指标 | 类型 | 说明 | 告警阈值 | 严重级别 |
|---|
libp2p_connections_opened_total | Counter | 累计建立连接数 | 速率突降 >50% | Warning |
libp2p_connections_active | Gauge | 当前活跃连接数 | > 上限的 80% | Warning |
libp2p_dht_query_duration_seconds | Histogram | DHT 查找延迟 | P99 > 5s | Critical |
libp2p_gossipsub_messages_received_total | Counter | Gossipsub 接收消息总数 | 速率异常突增 >300% | Warning |
libp2p_gossipsub_peers | Gauge | Gossipsub mesh 中对等节点数 | < 4 或 > 20 | Warning |
libp2p_relay_connections_active | Gauge | 活跃中继连接数 | > 上限的 80% | Warning |
process_resident_memory_bytes | Gauge | 节点 RSS 内存 | > 80% 配置限制 | Critical |
p2p_message_processing_seconds | Histogram | 消息处理延迟 | P99 > 1s | Warning |
p2p_peer_dial_failures_total | Counter | 拨号失败累积数 | 速率 > 10/min | Warning |
p2p_blacklist_total | Counter | 加入黑名单节点数 | 一分钟内 > 20 | Info |
监控仪表盘架构
flowchart TD
subgraph 节点层
P1["P2P 节点 1<br/>/metrics 端点"]
P2["P2P 节点 2<br/>/metrics 端点"]
P3["P2P 节点 N<br/>/metrics 端点"]
end
subgraph 采集层
PM["Prometheus Server<br/>pull 模式抓取"]
SH["Service Discovery<br/>DNS / 文件 / Consul"]
end
subgraph 存储层
VC["VictoriaMetrics<br/>长期存储"]
AL["AlertManager<br/>告警路由"]
end
subgraph 可视化层
GF["Grafana<br/>统一仪表盘"]
NT["通知渠道<br/>钉钉 / Slack / PagerDuty"]
end
P1 --> PM
P2 --> PM
P3 --> PM
SH --> PM
PM --> VC
VC --> GF
PM --> AL
AL --> NT
style P1 fill:#4CAF50,color:#fff
style P2 fill:#4CAF50,color:#fff
style P3 fill:#4CAF50,color:#fff
style PM fill:#2196F3,color:#fff
style VC fill:#FF9800,color:#fff
style AL fill:#f44336,color:#fff
style GF fill:#9C27B0,color:#fff
日志结构化
生产环境建议使用结构化日志,便于日志系统(Loki / ELK)索引:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| use tracing::{info, warn, error, Span};
use serde::Serialize;
#[derive(Serialize)]
struct P2PEvent {
peer_id: String,
event_type: String,
duration_ms: u64,
protocol: String,
error: Option<String>,
}
fn log_connection_event(peer: &PeerId) {
info!(
target: "p2p_events",
peer_id = %peer,
event_type = "connection_opened",
protocol = "libp2p",
);
}
|
部署考量
P2P 节点的容器化部署与传统 Web 服务有所不同,需要特别关注网络配置和状态管理。
Docker 容器化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # 多阶段构建
FROM rust:1.78-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
# 运行阶段
FROM gcr.io/distroless/cc-debian12
WORKDIR /app
COPY --from=builder /app/target/release/p2p-node /app/p2p-node
# P2P 端口
EXPOSE 4001/tcp # libp2p 传输
EXPOSE 4001/udp # QUIC 传输
EXPOSE 9100/tcp # Prometheus metrics
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/app/p2p-node", "health"]
ENTRYPOINT ["/app/p2p-node"]
|
推荐使用 distroless 基础镜像减少攻击面。P2P 端口需要在 Docker 运行时映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # docker-compose.yml
version: "3.8"
services:
p2p-bootstrap:
image: p2p-node:latest
ports:
- "4001:4001/tcp"
- "4001:4001/udp" # QUIC
- "9100:9100"
environment:
- P2P_BOOTSTRAP=true
- P2P_LISTEN_ADDRS=/ip4/0.0.0.0/tcp/4001,/ip4/0.0.0.0/udp/4001/quic-v1
- P2P_EXTERNAL_ADDRS=/ip4/${PUBLIC_IP}/tcp/4001
- P2P_MAX_CONNECTIONS=500
- RUST_LOG=info
- RUST_BACKTRACE=1
restart: always
volumes:
- p2p-data:/app/data
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
volumes:
p2p-data:
|
环境变量配置
所有运行时参数通过环境变量注入,保持镜像的环境无关性:
| 环境变量 | 默认值 | 说明 |
|---|
P2P_LISTEN_ADDRS | /ip4/0.0.0.0/tcp/4001 | 监听地址列表(逗号分隔) |
P2P_EXTERNAL_ADDRS | - | 外部可访问地址(NAT 场景必填) |
P2P_BOOTSTRAP_PEERS | - | 引导节点 multiaddr 列表 |
P2P_BOOTSTRAP | false | 是否作为引导节点 |
P2P_MAX_CONNECTIONS | 400 | 最大连接数 |
P2P_METRICS_PORT | 9100 | Prometheus 指标端口 |
P2P_DATA_DIR | /app/data | 持久化数据目录 |
RUST_LOG | info | 日志级别 |
优雅关闭
节点关闭时需要通知对等节点、完成进行中的 DHT 操作、持久化路由表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| use tokio::signal;
use tracing::info;
async fn graceful_shutdown(swarm: &mut Swarm<MyBehaviour>) {
// 等待 SIGINT 或 SIGTERM
signal::ctrl_c().await.expect("failed to listen for signal");
info!("Shutdown signal received, starting graceful shutdown");
// 1. 停止接受新连接
swarm.stop_listening(swarm.listeners().next().unwrap()).unwrap();
// 2. 等待进行中的操作完成(最多 10 秒)
tokio::time::timeout(Duration::from_secs(10), async {
loop {
tokio::select! {
event = swarm.select_next_some() => {
// 处理剩余事件,忽略新的行为请求
info!("Draining event: {:?}", std::mem::discriminant(&event));
}
_ = tokio::time::sleep(Duration::from_secs(1)) => break,
}
}
}).await.ok();
// 3. 持久化路由表到磁盘
if let Err(e) = save_routing_table(swarm).await {
error!("Failed to persist routing table: {e}");
}
info!("Graceful shutdown complete");
}
async fn save_routing_table(swarm: &Swarm<MyBehaviour>) -> anyhow::Result<()> {
let peers = swarm.behaviour().kademlia.kbuckets()
.flat_map(|bucket| bucket.iter().map(|entry| *entry.node.key.preimage()))
.collect::<Vec<_>>();
let data = serde_json::to_string(&peers)?;
tokio::fs::write("/app/data/routing_table.json", data).await?;
info!("Persisted {} peers to routing table", peers.len());
Ok(())
}
|
健康检查端点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| use warp::Filter;
fn health_routes() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
let liveness = warp::path!("health" / "live")
.map(|| warp::reply::json(&serde_json::json!({"status": "ok"})));
let readiness = warp::path!("health" / "ready")
.and(warp::any().map(move || get_node_status()))
.map(|status| warp::reply::json(&status));
liveness.or(readiness)
}
#[derive(Serialize)]
struct NodeStatus {
status: String,
connected_peers: usize,
routing_table_size: usize,
uptime_seconds: u64,
}
fn get_node_status() -> NodeStatus {
NodeStatus {
status: "ok".to_string(),
connected_peers: count_connected_peers(),
routing_table_size: get_routing_table_size(),
uptime_seconds: get_uptime(),
}
}
|
常见问题排查
| 问题 | 可能原因 | 排查步骤 | 解决方案 |
|---|
| 节点无法连接 | NAT 为对称型 | 检查 libp2p_nat_traversal 指标 | 启用中继节点(Relay)+ DCUtR 打洞 |
| DHT 查询慢 | 引导节点不可达或太少 | curl localhost:9100/metrics 查看 libp2p_dht_query_duration_seconds | 添加 3-5 个稳定引导节点,配置备用列表 |
| 消息丢失 | Gossipsub mesh 不稳定 | 检查 libp2p_gossipsub_peers 是否 < 6 | 调整 D 参数至 6-8,增大 heartbeat_interval |
| 内存泄漏 | 连接未正确关闭 | pprof 采集堆快照,检查连接生命周期 | 设置 connection_idle_timeout,实现 Drop 清理 |
| CPU 过高 | 心跳太频繁 | perf top 或 flamegraph 定位热点 | 调整心跳间隔至 1s+,减少不必要协议 |
| 消息延迟突增 | 网络拥塞或对端过载 | 查看 p2p_message_processing_seconds P99 | 启用消息优先级队列,实现 backpressure |
| 磁盘使用暴涨 | 日志或 DHT 记录失控 | 检查 du -sh /app/data | 配置日志轮转,限制 DHT 存储大小 |
| 文件描述符耗尽 | 连接数超限 | `lsof -p | wc -l` |
快速诊断脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| #!/bin/bash
# P2P 节点健康检查脚本
NODE=${1:-"http://localhost:9100"}
echo "=== P2P Node Diagnostics ==="
echo "Node: $NODE"
echo ""
# 基础可达性
echo "1. Connectivity:"
if curl -sf "$NODE/metrics" > /dev/null 2>&1; then
echo " ✅ Metrics endpoint reachable"
else
echo " ❌ Cannot reach metrics endpoint"
fi
# 连接数
CONN=$(curl -sf "$NODE/metrics" 2>/dev/null | grep "^libp2p_connections_active" | awk '{print $2}')
echo "2. Active Connections: ${CONN:-N/A}"
# DHT 延迟 P99
DHT_P99=$(curl -sf "$NODE/metrics" 2>/dev/null | grep "libp2p_dht_query_duration_seconds" | grep "quantile=\"0.99\"" | awk '{print $2}')
echo "3. DHT Query P99: ${DHT_P99:-N/A}s"
# 内存使用
MEM=$(curl -sf "$NODE/metrics" 2>/dev/null | grep "^process_resident_memory_bytes" | awk '{print $2}')
if [ -n "$MEM" ]; then
MEM_MB=$((MEM / 1024 / 1024))
echo "4. Memory: ${MEM_MB}MB"
else
echo "4. Memory: N/A"
fi
# 错误率
ERR=$(curl -sf "$NODE/metrics" 2>/dev/null | grep "p2p_peer_dial_failures_total" | awk '{print $2}')
echo "5. Dial Failures Total: ${ERR:-N/A}"
|
关键优化检查清单
参考资料