try fix overload
This commit is contained in:
@@ -43,6 +43,11 @@ class BackendSettings:
|
|||||||
packet_tracker_retention_seconds: float
|
packet_tracker_retention_seconds: float
|
||||||
packet_tracker_min_flush_interval_seconds: float
|
packet_tracker_min_flush_interval_seconds: float
|
||||||
packet_tracker_persist_timeout_seconds: float
|
packet_tracker_persist_timeout_seconds: float
|
||||||
|
packet_tracker_persist_retry_backoff_seconds: float
|
||||||
|
packet_tracker_persist_retry_backoff_max_seconds: float
|
||||||
|
packet_tracker_error_log_interval_seconds: float
|
||||||
|
packet_tracker_flush_batch_size: int
|
||||||
|
packet_tracker_max_entries: int
|
||||||
packet_tracker_stop_join_timeout_seconds: float
|
packet_tracker_stop_join_timeout_seconds: float
|
||||||
packet_tracker_reject_correlation_window_seconds: float
|
packet_tracker_reject_correlation_window_seconds: float
|
||||||
sniffer_buffer_capacity: int
|
sniffer_buffer_capacity: int
|
||||||
@@ -52,6 +57,12 @@ class BackendSettings:
|
|||||||
sniffer_buffer_drain_interval_seconds: float
|
sniffer_buffer_drain_interval_seconds: float
|
||||||
sniffer_thread_join_timeout_seconds: float
|
sniffer_thread_join_timeout_seconds: float
|
||||||
bridge_bpf_build_dir: str
|
bridge_bpf_build_dir: str
|
||||||
|
bridge_telemetry_raw_sample_every: int
|
||||||
|
bridge_telemetry_meta_sample_every: int
|
||||||
|
bridge_telemetry_ingress_perf_pages: int
|
||||||
|
bridge_telemetry_meta_perf_pages: int
|
||||||
|
bridge_telemetry_event_queue_maxsize: int
|
||||||
|
bridge_telemetry_drop_log_interval_seconds: float
|
||||||
bridge_link_state_thread_join_timeout_seconds: float
|
bridge_link_state_thread_join_timeout_seconds: float
|
||||||
bridge_link_state_failure_holdoff_seconds: float
|
bridge_link_state_failure_holdoff_seconds: float
|
||||||
bridge_link_state_recovery_holdoff_seconds: float
|
bridge_link_state_recovery_holdoff_seconds: float
|
||||||
@@ -78,6 +89,17 @@ def load_settings() -> BackendSettings:
|
|||||||
packet_tracker_retention_seconds=_env_float("BACKEND_PACKET_TRACKER_RETENTION_SECONDS", 10.0),
|
packet_tracker_retention_seconds=_env_float("BACKEND_PACKET_TRACKER_RETENTION_SECONDS", 10.0),
|
||||||
packet_tracker_min_flush_interval_seconds=_env_float("BACKEND_PACKET_TRACKER_MIN_FLUSH_INTERVAL_SECONDS", 0.05),
|
packet_tracker_min_flush_interval_seconds=_env_float("BACKEND_PACKET_TRACKER_MIN_FLUSH_INTERVAL_SECONDS", 0.05),
|
||||||
packet_tracker_persist_timeout_seconds=_env_float("BACKEND_PACKET_TRACKER_PERSIST_TIMEOUT_SECONDS", 2.0),
|
packet_tracker_persist_timeout_seconds=_env_float("BACKEND_PACKET_TRACKER_PERSIST_TIMEOUT_SECONDS", 2.0),
|
||||||
|
packet_tracker_persist_retry_backoff_seconds=_env_float(
|
||||||
|
"BACKEND_PACKET_TRACKER_PERSIST_RETRY_BACKOFF_SECONDS",
|
||||||
|
0.25,
|
||||||
|
),
|
||||||
|
packet_tracker_persist_retry_backoff_max_seconds=_env_float(
|
||||||
|
"BACKEND_PACKET_TRACKER_PERSIST_RETRY_BACKOFF_MAX_SECONDS",
|
||||||
|
5.0,
|
||||||
|
),
|
||||||
|
packet_tracker_error_log_interval_seconds=_env_float("BACKEND_PACKET_TRACKER_ERROR_LOG_INTERVAL_SECONDS", 5.0),
|
||||||
|
packet_tracker_flush_batch_size=max(1, _env_int("BACKEND_PACKET_TRACKER_FLUSH_BATCH_SIZE", 500)),
|
||||||
|
packet_tracker_max_entries=max(1, _env_int("BACKEND_PACKET_TRACKER_MAX_ENTRIES", 50_000)),
|
||||||
packet_tracker_stop_join_timeout_seconds=_env_float("BACKEND_PACKET_TRACKER_STOP_JOIN_TIMEOUT_SECONDS", 2.0),
|
packet_tracker_stop_join_timeout_seconds=_env_float("BACKEND_PACKET_TRACKER_STOP_JOIN_TIMEOUT_SECONDS", 2.0),
|
||||||
packet_tracker_reject_correlation_window_seconds=_env_float(
|
packet_tracker_reject_correlation_window_seconds=_env_float(
|
||||||
"BACKEND_PACKET_TRACKER_REJECT_CORRELATION_WINDOW_SECONDS",
|
"BACKEND_PACKET_TRACKER_REJECT_CORRELATION_WINDOW_SECONDS",
|
||||||
@@ -90,6 +112,12 @@ def load_settings() -> BackendSettings:
|
|||||||
sniffer_buffer_drain_interval_seconds=_env_float("BACKEND_SNIFFER_BUFFER_DRAIN_INTERVAL_SECONDS", 5.0),
|
sniffer_buffer_drain_interval_seconds=_env_float("BACKEND_SNIFFER_BUFFER_DRAIN_INTERVAL_SECONDS", 5.0),
|
||||||
sniffer_thread_join_timeout_seconds=_env_float("BACKEND_SNIFFER_THREAD_JOIN_TIMEOUT_SECONDS", 2.0),
|
sniffer_thread_join_timeout_seconds=_env_float("BACKEND_SNIFFER_THREAD_JOIN_TIMEOUT_SECONDS", 2.0),
|
||||||
bridge_bpf_build_dir=_env_str("BACKEND_BRIDGE_BPF_BUILD_DIR", "/tmp/mitm-bpf"),
|
bridge_bpf_build_dir=_env_str("BACKEND_BRIDGE_BPF_BUILD_DIR", "/tmp/mitm-bpf"),
|
||||||
|
bridge_telemetry_raw_sample_every=max(0, _env_int("BACKEND_BRIDGE_TELEMETRY_RAW_SAMPLE_EVERY", 1)),
|
||||||
|
bridge_telemetry_meta_sample_every=max(0, _env_int("BACKEND_BRIDGE_TELEMETRY_META_SAMPLE_EVERY", 1)),
|
||||||
|
bridge_telemetry_ingress_perf_pages=max(1, _env_int("BACKEND_BRIDGE_TELEMETRY_INGRESS_PERF_PAGES", 256)),
|
||||||
|
bridge_telemetry_meta_perf_pages=max(1, _env_int("BACKEND_BRIDGE_TELEMETRY_META_PERF_PAGES", 128)),
|
||||||
|
bridge_telemetry_event_queue_maxsize=max(1, _env_int("BACKEND_BRIDGE_TELEMETRY_EVENT_QUEUE_MAXSIZE", 20_000)),
|
||||||
|
bridge_telemetry_drop_log_interval_seconds=_env_float("BACKEND_BRIDGE_TELEMETRY_DROP_LOG_INTERVAL_SECONDS", 5.0),
|
||||||
bridge_link_state_thread_join_timeout_seconds=_env_float(
|
bridge_link_state_thread_join_timeout_seconds=_env_float(
|
||||||
"BACKEND_BRIDGE_LINK_STATE_THREAD_JOIN_TIMEOUT_SECONDS",
|
"BACKEND_BRIDGE_LINK_STATE_THREAD_JOIN_TIMEOUT_SECONDS",
|
||||||
2.0,
|
2.0,
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import queue
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, Mapping, Optional
|
from typing import Iterable, Mapping, Optional
|
||||||
|
|
||||||
@@ -27,6 +29,18 @@ class BridgeTelemetryManager:
|
|||||||
self._session_ids_by_interface: dict[str, tuple[str, ...]] = {}
|
self._session_ids_by_interface: dict[str, tuple[str, ...]] = {}
|
||||||
self._process: Optional[subprocess.Popen[str]] = None
|
self._process: Optional[subprocess.Popen[str]] = None
|
||||||
self._reader_thread: Optional[threading.Thread] = None
|
self._reader_thread: Optional[threading.Thread] = None
|
||||||
|
self._event_queue: queue.Queue = queue.Queue(maxsize=settings.bridge_telemetry_event_queue_maxsize)
|
||||||
|
self._worker_thread = threading.Thread(
|
||||||
|
target=self._event_worker_loop,
|
||||||
|
daemon=True,
|
||||||
|
name="bridge-telemetry-worker",
|
||||||
|
)
|
||||||
|
self._worker_thread.start()
|
||||||
|
self._dropped_events = 0
|
||||||
|
self._dropped_raw_payloads = 0
|
||||||
|
self._last_drop_log_at = 0.0
|
||||||
|
self._suppressed_collector_messages = 0
|
||||||
|
self._last_collector_warning_at = 0.0
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def update_sessions(self, session_interfaces: Mapping[str, Iterable[str]]) -> None:
|
def update_sessions(self, session_interfaces: Mapping[str, Iterable[str]]) -> None:
|
||||||
@@ -80,8 +94,21 @@ class BridgeTelemetryManager:
|
|||||||
",".join(sorted(self._interfaces)),
|
",".join(sorted(self._interfaces)),
|
||||||
"--build-dir",
|
"--build-dir",
|
||||||
settings.bridge_bpf_build_dir,
|
settings.bridge_bpf_build_dir,
|
||||||
|
"--raw-sample-every",
|
||||||
|
str(settings.bridge_telemetry_raw_sample_every),
|
||||||
|
"--meta-sample-every",
|
||||||
|
str(settings.bridge_telemetry_meta_sample_every),
|
||||||
|
"--ingress-pages",
|
||||||
|
str(settings.bridge_telemetry_ingress_perf_pages),
|
||||||
|
"--meta-pages",
|
||||||
|
str(settings.bridge_telemetry_meta_perf_pages),
|
||||||
]
|
]
|
||||||
logger.info("Starting bridge telemetry collector for interfaces=%s", sorted(self._interfaces))
|
logger.info(
|
||||||
|
"Starting bridge telemetry collector for interfaces=%s raw_sample_every=%s meta_sample_every=%s",
|
||||||
|
sorted(self._interfaces),
|
||||||
|
settings.bridge_telemetry_raw_sample_every,
|
||||||
|
settings.bridge_telemetry_meta_sample_every,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self._process = subprocess.Popen(
|
self._process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
@@ -158,6 +185,90 @@ class BridgeTelemetryManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to process ingress raw packet event")
|
logger.exception("Failed to process ingress raw packet event")
|
||||||
|
|
||||||
|
def _event_worker_loop(self) -> None:
|
||||||
|
while True:
|
||||||
|
event = self._event_queue.get()
|
||||||
|
try:
|
||||||
|
if not isinstance(event, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if event.get("event_type") == "ingress":
|
||||||
|
self._handle_ingress_packet(event)
|
||||||
|
|
||||||
|
try:
|
||||||
|
packet_tracker.observe_telemetry(event)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to process telemetry event: %s", event)
|
||||||
|
finally:
|
||||||
|
self._event_queue.task_done()
|
||||||
|
|
||||||
|
def _enqueue_event(self, event: dict[str, object]) -> None:
|
||||||
|
try:
|
||||||
|
self._event_queue.put_nowait(event)
|
||||||
|
return
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raw_b64 = event.pop("raw_b64", None)
|
||||||
|
dropped_raw = isinstance(raw_b64, str) and bool(raw_b64)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._event_queue.get_nowait()
|
||||||
|
self._event_queue.task_done()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._event_queue.put_nowait(event)
|
||||||
|
except queue.Full:
|
||||||
|
with self._lock:
|
||||||
|
self._dropped_events += 1
|
||||||
|
self._log_drop_summary()
|
||||||
|
return
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._dropped_events += 1
|
||||||
|
if dropped_raw:
|
||||||
|
self._dropped_raw_payloads += 1
|
||||||
|
self._log_drop_summary()
|
||||||
|
|
||||||
|
def _log_drop_summary(self) -> None:
|
||||||
|
now_ts = time.time()
|
||||||
|
if now_ts - self._last_drop_log_at < settings.bridge_telemetry_drop_log_interval_seconds:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_drop_log_at = now_ts
|
||||||
|
with self._lock:
|
||||||
|
dropped_events = self._dropped_events
|
||||||
|
dropped_raw_payloads = self._dropped_raw_payloads
|
||||||
|
queue_size = self._event_queue.qsize()
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"Bridge telemetry is overloaded; queue=%s dropped_events=%s dropped_raw_payloads=%s",
|
||||||
|
queue_size,
|
||||||
|
dropped_events,
|
||||||
|
dropped_raw_payloads,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _log_collector_message(self, text: str) -> None:
|
||||||
|
lower_text = text.lower()
|
||||||
|
is_loss_message = "lost" in lower_text and "sample" in lower_text
|
||||||
|
if not is_loss_message:
|
||||||
|
logger.info("bridge-telemetry: %s", text)
|
||||||
|
return
|
||||||
|
|
||||||
|
now_ts = time.time()
|
||||||
|
if now_ts - self._last_collector_warning_at < settings.bridge_telemetry_drop_log_interval_seconds:
|
||||||
|
with self._lock:
|
||||||
|
self._suppressed_collector_messages += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
suppressed = self._suppressed_collector_messages
|
||||||
|
self._suppressed_collector_messages = 0
|
||||||
|
self._last_collector_warning_at = now_ts
|
||||||
|
logger.warning("bridge-telemetry: %s (suppressed similar messages=%s)", text, suppressed)
|
||||||
|
|
||||||
def _read_loop(self, process: subprocess.Popen[str]) -> None:
|
def _read_loop(self, process: subprocess.Popen[str]) -> None:
|
||||||
stdout = process.stdout
|
stdout = process.stdout
|
||||||
if stdout is None:
|
if stdout is None:
|
||||||
@@ -170,20 +281,14 @@ class BridgeTelemetryManager:
|
|||||||
try:
|
try:
|
||||||
event = json.loads(text)
|
event = json.loads(text)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.info("bridge-telemetry: %s", text)
|
self._log_collector_message(text)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if "event_type" not in event:
|
if "event_type" not in event:
|
||||||
logger.info("bridge-telemetry: %s", event)
|
logger.info("bridge-telemetry: %s", event)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if event.get("event_type") == "ingress":
|
self._enqueue_event(event)
|
||||||
self._handle_ingress_packet(event)
|
|
||||||
|
|
||||||
try:
|
|
||||||
packet_tracker.observe_telemetry(event)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to process telemetry event: %s", event)
|
|
||||||
|
|
||||||
rc = process.poll()
|
rc = process.poll()
|
||||||
if rc not in (0, None):
|
if rc not in (0, None):
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ BPF_SOURCE = r"""
|
|||||||
#define EVENT_INGRESS 1
|
#define EVENT_INGRESS 1
|
||||||
#define EVENT_EGRESS 2
|
#define EVENT_EGRESS 2
|
||||||
#define EVENT_DROP 3
|
#define EVENT_DROP 3
|
||||||
|
#define RAW_SAMPLE_EVERY __RAW_SAMPLE_EVERY__
|
||||||
|
#define META_SAMPLE_EVERY __META_SAMPLE_EVERY__
|
||||||
|
|
||||||
struct vlan_hdr_t {
|
struct vlan_hdr_t {
|
||||||
__be16 h_vlan_TCI;
|
__be16 h_vlan_TCI;
|
||||||
@@ -112,6 +114,16 @@ struct event_t {
|
|||||||
BPF_PERF_OUTPUT(ingress_events);
|
BPF_PERF_OUTPUT(ingress_events);
|
||||||
BPF_PERF_OUTPUT(meta_events);
|
BPF_PERF_OUTPUT(meta_events);
|
||||||
|
|
||||||
|
static __always_inline int should_emit_sample(__u32 packet_mark, __u32 every) {
|
||||||
|
if (every == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (every == 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return (packet_mark % every) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
static __always_inline __u32 ensure_packet_mark(struct __sk_buff *skb) {
|
static __always_inline __u32 ensure_packet_mark(struct __sk_buff *skb) {
|
||||||
__u32 next = skb->mark;
|
__u32 next = skb->mark;
|
||||||
|
|
||||||
@@ -346,7 +358,11 @@ int handle_ingress(struct __sk_buff *skb) {
|
|||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
ingress_events.perf_submit_skb(skb, skb->len, &event, sizeof(event));
|
if (should_emit_sample(event.skb_mark, RAW_SAMPLE_EVERY)) {
|
||||||
|
ingress_events.perf_submit_skb(skb, skb->len, &event, sizeof(event));
|
||||||
|
} else if (should_emit_sample(event.skb_mark, META_SAMPLE_EVERY)) {
|
||||||
|
meta_events.perf_submit(skb, &event, sizeof(event));
|
||||||
|
}
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +384,9 @@ int handle_egress(struct __sk_buff *skb) {
|
|||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
meta_events.perf_submit(skb, &event, sizeof(event));
|
if (should_emit_sample(event.skb_mark, META_SAMPLE_EVERY)) {
|
||||||
|
meta_events.perf_submit(skb, &event, sizeof(event));
|
||||||
|
}
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +415,9 @@ TRACEPOINT_PROBE(skb, kfree_skb) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
meta_events.perf_submit(args, &event, sizeof(event));
|
if (should_emit_sample(event.skb_mark, META_SAMPLE_EVERY)) {
|
||||||
|
meta_events.perf_submit(args, &event, sizeof(event));
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -539,10 +559,41 @@ def _emit_meta_event(cpu: int, data: int, size: int) -> None:
|
|||||||
print(json.dumps(payload, separators=(",", ":")), flush=True)
|
print(json.dumps(payload, separators=(",", ":")), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bpf_source(raw_sample_every: int, meta_sample_every: int) -> str:
|
||||||
|
return (
|
||||||
|
BPF_SOURCE.replace("__RAW_SAMPLE_EVERY__", str(max(0, raw_sample_every)))
|
||||||
|
.replace("__META_SAMPLE_EVERY__", str(max(0, meta_sample_every)))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
def _parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="tc/eBPF bridge telemetry collector")
|
parser = argparse.ArgumentParser(description="tc/eBPF bridge telemetry collector")
|
||||||
parser.add_argument("--ifaces", required=True, help="Comma-separated list of interfaces to instrument")
|
parser.add_argument("--ifaces", required=True, help="Comma-separated list of interfaces to instrument")
|
||||||
parser.add_argument("--build-dir", required=True, help="Directory for compiled tc BPF objects")
|
parser.add_argument("--build-dir", required=True, help="Directory for compiled tc BPF objects")
|
||||||
|
parser.add_argument(
|
||||||
|
"--raw-sample-every",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Emit full raw ingress packets every Nth marked packet. Use 0 to disable raw packet export.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--meta-sample-every",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Emit metadata events every Nth marked packet. Use 0 to disable metadata-only events.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ingress-pages",
|
||||||
|
type=int,
|
||||||
|
default=256,
|
||||||
|
help="Perf-buffer page count for raw ingress packet events.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--meta-pages",
|
||||||
|
type=int,
|
||||||
|
default=128,
|
||||||
|
help="Perf-buffer page count for metadata events.",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -621,6 +672,11 @@ def _cleanup_tc(ifaces: Iterable[str]) -> None:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = _parse_args()
|
args = _parse_args()
|
||||||
|
raw_sample_every = max(0, args.raw_sample_every)
|
||||||
|
meta_sample_every = max(0, args.meta_sample_every)
|
||||||
|
ingress_pages = max(1, args.ingress_pages)
|
||||||
|
meta_pages = max(1, args.meta_pages)
|
||||||
|
|
||||||
global TARGET_INTERFACES
|
global TARGET_INTERFACES
|
||||||
TARGET_INTERFACES = {iface.strip() for iface in args.ifaces.split(",") if iface.strip()}
|
TARGET_INTERFACES = {iface.strip() for iface in args.ifaces.split(",") if iface.strip()}
|
||||||
if not TARGET_INTERFACES:
|
if not TARGET_INTERFACES:
|
||||||
@@ -630,7 +686,7 @@ def main() -> int:
|
|||||||
signal.signal(signal.SIGTERM, _sigterm)
|
signal.signal(signal.SIGTERM, _sigterm)
|
||||||
signal.signal(signal.SIGINT, _sigterm)
|
signal.signal(signal.SIGINT, _sigterm)
|
||||||
|
|
||||||
bpf = BPF(text=BPF_SOURCE)
|
bpf = BPF(text=_build_bpf_source(raw_sample_every, meta_sample_every))
|
||||||
ingress_prog_name = ""
|
ingress_prog_name = ""
|
||||||
egress_prog_name = ""
|
egress_prog_name = ""
|
||||||
try:
|
try:
|
||||||
@@ -643,14 +699,18 @@ def main() -> int:
|
|||||||
"ingress_program": _json_safe(ingress_prog_name),
|
"ingress_program": _json_safe(ingress_prog_name),
|
||||||
"egress_program": _json_safe(egress_prog_name),
|
"egress_program": _json_safe(egress_prog_name),
|
||||||
"build_dir": str(args.build_dir),
|
"build_dir": str(args.build_dir),
|
||||||
|
"raw_sample_every": raw_sample_every,
|
||||||
|
"meta_sample_every": meta_sample_every,
|
||||||
|
"ingress_pages": ingress_pages,
|
||||||
|
"meta_pages": meta_pages,
|
||||||
},
|
},
|
||||||
separators=(",", ":"),
|
separators=(",", ":"),
|
||||||
),
|
),
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
bpf["ingress_events"].open_perf_buffer(_emit_ingress_event, page_cnt=256)
|
bpf["ingress_events"].open_perf_buffer(_emit_ingress_event, page_cnt=ingress_pages)
|
||||||
bpf["meta_events"].open_perf_buffer(_emit_meta_event, page_cnt=128)
|
bpf["meta_events"].open_perf_buffer(_emit_meta_event, page_cnt=meta_pages)
|
||||||
while True:
|
while True:
|
||||||
bpf.perf_buffer_poll()
|
bpf.perf_buffer_poll()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import concurrent.futures
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -174,7 +175,13 @@ class PacketTracker:
|
|||||||
"persisted_without_raw": 0,
|
"persisted_without_raw": 0,
|
||||||
"persisted_kernel_mark": 0,
|
"persisted_kernel_mark": 0,
|
||||||
"persisted_legacy_hash": 0,
|
"persisted_legacy_hash": 0,
|
||||||
|
"persist_failed_total": 0,
|
||||||
|
"persist_timeout_total": 0,
|
||||||
|
"persist_failure_log_suppressed": 0,
|
||||||
|
"evicted_persisted_total": 0,
|
||||||
|
"evicted_unpersisted_total": 0,
|
||||||
}
|
}
|
||||||
|
self._last_persist_error_log_at = 0.0
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self._thread = threading.Thread(target=self._run, daemon=True, name="packet-tracker")
|
self._thread = threading.Thread(target=self._run, daemon=True, name="packet-tracker")
|
||||||
@@ -233,6 +240,7 @@ class PacketTracker:
|
|||||||
self._merge_packet_info(entry, pkt_info, now_ts)
|
self._merge_packet_info(entry, pkt_info, now_ts)
|
||||||
self._maybe_promote_reject_from_reply(pkt_info, now_ts)
|
self._maybe_promote_reject_from_reply(pkt_info, now_ts)
|
||||||
self._maybe_mark_complete(entry)
|
self._maybe_mark_complete(entry)
|
||||||
|
self._enforce_entry_limit_locked()
|
||||||
return correlation_key
|
return correlation_key
|
||||||
|
|
||||||
def observe_telemetry(self, event: Dict[str, Any]) -> Optional[str]:
|
def observe_telemetry(self, event: Dict[str, Any]) -> Optional[str]:
|
||||||
@@ -315,6 +323,7 @@ class PacketTracker:
|
|||||||
entry["last_observed_at"] = now_ts
|
entry["last_observed_at"] = now_ts
|
||||||
entry["dirty"] = True
|
entry["dirty"] = True
|
||||||
self._maybe_mark_complete(entry)
|
self._maybe_mark_complete(entry)
|
||||||
|
self._enforce_entry_limit_locked()
|
||||||
return correlation_key
|
return correlation_key
|
||||||
|
|
||||||
def _new_entry(self, correlation_key: str, now_ts: float) -> Dict[str, Any]:
|
def _new_entry(self, correlation_key: str, now_ts: float) -> Dict[str, Any]:
|
||||||
@@ -344,8 +353,35 @@ class PacketTracker:
|
|||||||
"created_at": now_ts,
|
"created_at": now_ts,
|
||||||
"last_observed_at": now_ts,
|
"last_observed_at": now_ts,
|
||||||
"last_persisted_at": 0.0,
|
"last_persisted_at": 0.0,
|
||||||
|
"last_persist_attempt_at": 0.0,
|
||||||
|
"persist_failures": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _enforce_entry_limit_locked(self) -> None:
|
||||||
|
overflow = len(self._entries) - settings.packet_tracker_max_entries
|
||||||
|
if overflow <= 0:
|
||||||
|
return
|
||||||
|
eviction_chunk = max(1, min(settings.packet_tracker_flush_batch_size, settings.packet_tracker_max_entries))
|
||||||
|
evict_count = min(len(self._entries), max(overflow, eviction_chunk))
|
||||||
|
|
||||||
|
candidates = sorted(
|
||||||
|
self._entries.items(),
|
||||||
|
key=lambda item: (
|
||||||
|
0 if item[1].get("persisted") else 1,
|
||||||
|
0 if item[1].get("finalized") else 1,
|
||||||
|
float(item[1].get("last_observed_at") or 0.0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for correlation_key, entry in candidates[:evict_count]:
|
||||||
|
if entry.get("persisted"):
|
||||||
|
self._stats["evicted_persisted_total"] += 1
|
||||||
|
if entry.get("finalized") and not entry.get("stats_recorded"):
|
||||||
|
self._record_stats(entry["payload"])
|
||||||
|
entry["stats_recorded"] = True
|
||||||
|
else:
|
||||||
|
self._stats["evicted_unpersisted_total"] += 1
|
||||||
|
self._entries.pop(correlation_key, None)
|
||||||
|
|
||||||
def _ensure_correlation(self, payload: Dict[str, Any]) -> Optional[str]:
|
def _ensure_correlation(self, payload: Dict[str, Any]) -> Optional[str]:
|
||||||
skb_mark = payload.get("skb_mark")
|
skb_mark = payload.get("skb_mark")
|
||||||
if payload.get("packet_id") is None and skb_mark is not None:
|
if payload.get("packet_id") is None and skb_mark is not None:
|
||||||
@@ -635,13 +671,15 @@ class PacketTracker:
|
|||||||
or entry["finalized"]
|
or entry["finalized"]
|
||||||
or (now_ts - entry["last_persisted_at"]) >= self._min_flush_interval_seconds
|
or (now_ts - entry["last_persisted_at"]) >= self._min_flush_interval_seconds
|
||||||
)
|
)
|
||||||
if should_flush:
|
if should_flush and self._persist_backoff_elapsed(entry, now_ts):
|
||||||
due_entries.append(
|
if len(due_entries) < settings.packet_tracker_flush_batch_size:
|
||||||
{
|
due_entries.append(
|
||||||
"correlation_key": entry["correlation_key"],
|
{
|
||||||
"payload": dict(entry["payload"]),
|
"correlation_key": entry["correlation_key"],
|
||||||
}
|
"payload": dict(entry["payload"]),
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
entry["last_persist_attempt_at"] = now_ts
|
||||||
elif entry["persisted"] and age >= self._retention_seconds:
|
elif entry["persisted"] and age >= self._retention_seconds:
|
||||||
if entry["finalized"] and not entry["stats_recorded"]:
|
if entry["finalized"] and not entry["stats_recorded"]:
|
||||||
self._record_stats(entry["payload"])
|
self._record_stats(entry["payload"])
|
||||||
@@ -654,6 +692,21 @@ class PacketTracker:
|
|||||||
for entry in due_entries:
|
for entry in due_entries:
|
||||||
self._persist(entry)
|
self._persist(entry)
|
||||||
|
|
||||||
|
def _persist_backoff_elapsed(self, entry: Dict[str, Any], now_ts: float) -> bool:
|
||||||
|
failures = int(entry.get("persist_failures") or 0)
|
||||||
|
if failures <= 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
base = max(0.0, settings.packet_tracker_persist_retry_backoff_seconds)
|
||||||
|
if base <= 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
backoff = min(
|
||||||
|
settings.packet_tracker_persist_retry_backoff_max_seconds,
|
||||||
|
base * (2 ** min(failures - 1, 6)),
|
||||||
|
)
|
||||||
|
return now_ts - float(entry.get("last_persist_attempt_at") or 0.0) >= backoff
|
||||||
|
|
||||||
def _persist(self, entry: Dict[str, Any]) -> None:
|
def _persist(self, entry: Dict[str, Any]) -> None:
|
||||||
payload = dict(entry["payload"])
|
payload = dict(entry["payload"])
|
||||||
|
|
||||||
@@ -671,8 +724,34 @@ class PacketTracker:
|
|||||||
current["persisted"] = True
|
current["persisted"] = True
|
||||||
current["dirty"] = False
|
current["dirty"] = False
|
||||||
current["last_persisted_at"] = time.time()
|
current["last_persisted_at"] = time.time()
|
||||||
except Exception:
|
current["persist_failures"] = 0
|
||||||
logger.exception("Failed to persist packet %s", entry["correlation_key"])
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
fut.cancel()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
is_timeout = isinstance(exc, (TimeoutError, concurrent.futures.TimeoutError, asyncio.TimeoutError))
|
||||||
|
with self._lock:
|
||||||
|
self._stats["persist_failed_total"] += 1
|
||||||
|
if is_timeout:
|
||||||
|
self._stats["persist_timeout_total"] += 1
|
||||||
|
current = self._entries.get(entry["correlation_key"])
|
||||||
|
if current is not None:
|
||||||
|
current["dirty"] = True
|
||||||
|
current["persist_failures"] = int(current.get("persist_failures") or 0) + 1
|
||||||
|
|
||||||
|
now_ts = time.time()
|
||||||
|
if now_ts - self._last_persist_error_log_at >= settings.packet_tracker_error_log_interval_seconds:
|
||||||
|
self._last_persist_error_log_at = now_ts
|
||||||
|
logger.warning(
|
||||||
|
"Packet persistence is overloaded; failed to persist %s (%s). Further errors are rate-limited.",
|
||||||
|
entry["correlation_key"],
|
||||||
|
type(exc).__name__,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with self._lock:
|
||||||
|
self._stats["persist_failure_log_suppressed"] += 1
|
||||||
|
|
||||||
def _record_stats(self, payload: Dict[str, Any]) -> None:
|
def _record_stats(self, payload: Dict[str, Any]) -> None:
|
||||||
capture_sources = set(payload.get("capture_sources") or [])
|
capture_sources = set(payload.get("capture_sources") or [])
|
||||||
|
|||||||
Reference in New Issue
Block a user