一、问题是啥

  数据面("真实跑的包")和控制面/监控面("你以为跑的包")经常对不上号:

  - 监控说"QPS 1万、零丢包",但用户却在投诉超时
  - Prometheus 曲线平滑好看,可 tcpdump 抓到一堆重传
  - iptables -L -n -v 看规则命中数,和 conntrack -L 记录的连接对不上
  - eBPF 采样计数 10万,iptables 计数器却只有 9万
  - Pod 日志说请求成功,上游网关却记录 5xx

  为啥会不一致? 因为内核数据路径上有一堆"观察点",每个点看到的东西不一样:

  网卡硬件计数器  ——看到: 物理收发包总数 (连坏包都算)
     ↓
  XDP 计数器     ——看到: 进 XDP 的包 (但 XDP_DROP 的不进协议栈)
     ↓
  tcpdump/AF_PACKET——看到: 经过某网卡的包 (有采样会漏)
     ↓
  iptables 计数  ——看到: 命中某条规则的包 (规则顺序影响)
     ↓
  conntrack     ——看到: 有状态的连接 (UDP/ICMP 记录不同)
     ↓
  eBPF hook     ——看到: 挂载点能看到的那部分
     ↓
  socket 统计   ——看到: 应用层真正收到的
     ↓
  应用日志      ——看到: 应用愿意记的

  大白话: 一条消息从你嘴里到对方耳朵,路上有门卫、保安、前台、秘书、助理各自记账,每人记的点不一样、时间不一样、维度不
  一样,合账的时候当然对不上。

  二、三个根本原因

  1. 采样点不同,覆盖的包不同

  XDP 在驱动层,连 sk_buff 都没建;tcpdump 在 AF_PACKET,已经过了驱动 GRO 合并;iptables 在 netfilter
  hook,不同链命中条件不同。

  2. 采样时机不同,快照不是原子的

  Prometheus 每 15 秒拉一次,eBPF map 每秒上报,网卡 ethtool 计数实时累加 —— 三者永远在不同时刻抓快照。

  3. 聚合维度不同,丢了信息

  监控把百万连接聚合成一条曲线,个体异常被平均掉。P50 漂亮,P99 爆炸。

  三、最佳方式(大白话口诀)

  ▎ "同一条包,全链路打标签;同一个时刻,全节点对齐戳;同一个维度,全层级下钻。"

  方式 1:统一 flow ID 贯穿全链路(最治本)

  给每条连接/每个包打一个全局 ID(比如 5 元组哈希 或 SO_MARK),网卡计数、eBPF、iptables、应用日志都带这个 ID。对账时按
  ID 合并。

  方式 2:eBPF 做"内核级 tcpdump + Prometheus"

  用 eBPF 在每个关键钩子(XDP → TC → netfilter → tcp_rcv → socket)挂程序,同一个 map
  记账,一次遍历数据路径就能对齐。代表:Cilium Hubble、Pixie、Inspektor Gadget。

  方式 3:pcap 级采样 + 应用 trace 关联

  抓包文件里每个包带上时间戳 + flow ID,和应用 OpenTelemetry trace 通过 ID 关联,把包级事件对应到 RPC 级事件。

  方式 4:counter drift 对账

  定期(比如每分钟)把各层计数器读出来 diff,如果 网卡收包 - XDP看到 - iptables命中 - socket收到 >
  阈值,告警。这是差异检测而不是追求绝对一致。

  方式 5:单一真相源(Single Source of Truth)

  指定某一层为"权威"(一般是 eBPF 或 conntrack),其他层只是参考。监控看板统一从权威源出数据。

  四、PHP 完整代码演示

  模拟一条请求穿过 7 个观察点,各层独立计数,看不一致是怎么产生的,以及打 flow ID + 单一真相源如何破解。

  <?php
  /**
   * 演示:可观测性与数据面一致性
   * 场景:1 万个包进来,在内核 7 个观察点各自计数,对账时出现 drift
   *       对比"各自为政"和"flow ID + 单一真相源"两种方式
   */

  // =====================================================
  //  一个数据包
  // =====================================================
  class Packet {
      public int    $id;           // 全局 flow ID(包级)
      public string $flow;         // 五元组哈希(连接级)
      public bool   $isMalformed;  // 坏包(校验和错)
      public bool   $isDropped;    // 被 XDP 拦
      public bool   $isFiltered;   // 被 iptables 丢
      public bool   $isRetransmit; // TCP 重传(应用看不到)
      public bool   $sampled;      // 是否被 tcpdump 采到
      public function __construct(int $id, string $flow) {
          $this->id = $id;
          $this->flow = $flow;
          $this->isMalformed = mt_rand(1, 1000) <= 5;    // 0.5% 坏包
          $this->isDropped   = mt_rand(1, 100)  <= 2;    // 2% 被 XDP 丢
          $this->isFiltered  = mt_rand(1, 100)  <= 1;    // 1% 被 iptables 丢
          $this->isRetransmit= mt_rand(1, 100)  <= 3;    // 3% 是重传
          $this->sampled     = mt_rand(1, 10)   == 1;    // tcpdump 1/10 采样
      }
  }

  // =====================================================
  //  内核观察点:每个点只看到自己位置的事
  // =====================================================
  class ObservationPoints {
      // 各层独立计数(模拟 ethtool / bpftool map / iptables -vn / ss)
      public int $nicRx = 0;          // 网卡硬件
      public int $xdpSeen = 0;        // XDP
      public int $xdpDrop = 0;
      public int $tcpdumpSeen = 0;    // AF_PACKET 采样
      public int $iptablesHit = 0;    // netfilter
      public int $conntrack = 0;      // 连接跟踪
      public int $socketRecv = 0;     // 应用收到
      public int $appLog = 0;         // 应用日志

      // 以 flow ID 为维度的统一账本(方式:打标签)
      public array $flowLedger = [];

      public function ingest(Packet $p, bool $useFlowId): void {
          // --- 1. 网卡:什么都数,连坏包都数 ---
          $this->nicRx++;
          if ($p->isMalformed) return;  // 网卡后,坏包被丢,下游看不到

          // --- 2. XDP:sk_buff 还没建,DROP 不进协议栈 ---
          $this->xdpSeen++;
          if ($useFlowId) $this->tag($p, 'xdp');
          if ($p->isDropped) { $this->xdpDrop++; return; }

          // --- 3. tcpdump(AF_PACKET):10% 采样 ---
          if ($p->sampled) $this->tcpdumpSeen++;

          // --- 4. iptables:1% 的包被过滤 ---
          if ($p->isFiltered) return;  // 注意:iptables 丢包后,命中计数也不变(规则在 DROP 之前)
          $this->iptablesHit++;
          if ($useFlowId) $this->tag($p, 'iptables');

          // --- 5. conntrack:只看新建连接,不是每个包 ---
          if (!isset($this->flowLedger[$p->flow])) {
              $this->conntrack++;
          }

          // --- 6. socket:重传包在 TCP 层被合并,应用收不到 ---
          if ($p->isRetransmit) return;
          $this->socketRecv++;
          if ($useFlowId) $this->tag($p, 'socket');

          // --- 7. 应用日志:只记成功业务(假设 99% 记,1% 没记) ---
          if (mt_rand(1, 100) != 1) {
              $this->appLog++;
              if ($useFlowId) $this->tag($p, 'app');
          }
      }

      private function tag(Packet $p, string $layer): void {
          if (!isset($this->flowLedger[$p->flow])) {
              $this->flowLedger[$p->flow] = [];
          }
          $this->flowLedger[$p->flow][$layer] =
              ($this->flowLedger[$p->flow][$layer] ?? 0) + 1;
      }
  }

  // =====================================================
  //  主流程
  // =====================================================
  const PKT_COUNT = 10000;
  const FLOW_COUNT = 100;  // 100 条连接,每条约 100 个包

  // 造包
  function buildPackets(int $n, int $flows): array {
      $pkts = [];
      for ($i = 0; $i < $n; $i++) {
          $flow = 'flow_' . ($i % $flows);
          $pkts[] = new Packet($i, $flow);
      }
      return $pkts;
  }

  echo "==================================================\n";
  echo " 可观测性与数据面一致性:各层计数器对账\n";
  echo "==================================================\n";
  echo "输入: " . number_format(PKT_COUNT) . " 个包,"
     . FLOW_COUNT . " 条 flow\n\n";

  $pkts = buildPackets(PKT_COUNT, FLOW_COUNT);

  // ---------- 方式 1:各层独立计数(不打标签) ----------
  $obs = new ObservationPoints();
  foreach ($pkts as $p) $obs->ingest($p, false);

  echo "---------- 方式 A:各层独立计数 ----------\n";
  printf("网卡 RX          : %6d  (最原始)\n",         $obs->nicRx);
  printf("XDP 看到         : %6d  (去掉坏包)\n",       $obs->xdpSeen);
  printf("XDP DROP         : %6d\n",                   $obs->xdpDrop);
  printf("tcpdump 采到     : %6d  (10%% 采样,不可比)\n", $obs->tcpdumpSeen);
  printf("iptables 命中    : %6d\n",                   $obs->iptablesHit);
  printf("conntrack 新建   : %6d  (连接数,不是包数)\n", $obs->conntrack);
  printf("socket 收到      : %6d  (去掉重传)\n",       $obs->socketRecv);
  printf("应用日志         : %6d  (有采样/丢日志)\n",   $obs->appLog);

  // 对账
  $drift = $obs->nicRx - $obs->appLog;
  printf("\n对账差异: 网卡(%d) - 应用(%d) = %d (%.1f%%)\n",
      $obs->nicRx, $obs->appLog, $drift, $drift / $obs->nicRx * 100);
  echo "每层差异原因不同,没有 flow ID 根本无法定位是哪批包丢在哪一层\n\n";

  // ---------- 方式 2:打 flow ID,对某条 flow 下钻 ----------
  $obs2 = new ObservationPoints();
  foreach ($pkts as $p) $obs2->ingest($p, true);

  echo "---------- 方式 B:flow ID 贯穿所有层(下钻分析) ----------\n";
  echo "随机抽 3 条 flow 看各层命中:\n";
  $flows = array_rand($obs2->flowLedger, 3);
  printf("%-12s %6s %10s %8s %6s\n", 'flow', 'xdp', 'iptables', 'socket', 'app');
  echo str_repeat('-', 50) . "\n";
  foreach ($flows as $f) {
      $l = $obs2->flowLedger[$f];
      printf("%-12s %6d %10d %8d %6d\n",
          $f,
          $l['xdp']      ?? 0,
          $l['iptables'] ?? 0,
          $l['socket']   ?? 0,
          $l['app']      ?? 0);
  }
  echo "\n有了 flow ID,就能看出:比如 flow_7 在 iptables 到 socket 之间漏了\n";
  echo "多少包,直接指向重传/丢弃的具体连接\n\n";

  // ---------- 方式 3:差异检测(counter drift)----------
  echo "---------- 方式 C:counter drift 告警 ----------\n";
  $layers = [
      '网卡 RX'        => $obs->nicRx,
      'XDP 看到'       => $obs->xdpSeen,
      'iptables 命中'  => $obs->iptablesHit,
      'socket 收到'    => $obs->socketRecv,
      '应用日志'       => $obs->appLog,
  ];
  $prev = null;
  $prevName = '';
  foreach ($layers as $name => $val) {
      if ($prev !== null) {
          $diff = $prev - $val;
          $pct = $prev > 0 ? $diff / $prev * 100 : 0;
          $flag = $pct > 5 ? ' ⚠ 差异过大' : '';
          printf("%-12s -> %-14s 差 %5d (%.1f%%)%s\n",
              $prevName, $name, $diff, $pct, $flag);
      }
      $prev = $val;
      $prevName = $name;
  }

  echo "\n==================================================\n";
  echo " 大白话结论\n";
  echo "==================================================\n";
  echo "为啥对不上?每个观察点看到的数据路径段不一样:\n";
  echo "  - 网卡看物理层(连坏包都数)\n";
  echo "  - XDP 看驱动层(DROP 后下游看不见)\n";
  echo "  - tcpdump 会采样(计数不可直接比)\n";
  echo "  - iptables 命中数受规则顺序影响\n";
  echo "  - conntrack 是连接维度,不是包维度\n";
  echo "  - socket 不看重传包\n";
  echo "  - 应用日志自己还会丢\n\n";
  echo "怎么破解?\n";
  echo "  1. 给每个包打 flow ID,所有层用同一个 key 记账\n";
  echo "  2. 定期 diff 各层计数,偏差超阈值告警 (counter drift)\n";
  echo "  3. 指定 eBPF/Hubble 为单一真相源,其他层作参考\n";
  echo "  4. 应用 trace 的 traceparent/x-request-id 和内核 flow ID 关联\n";
  echo "  5. pcap 存样本,必要时下钻包级证据\n";

  五、预期输出

  ---------- 方式 A:各层独立计数 ----------
  网卡 RX          :  10000  (最原始)
  XDP 看到         :   9949  (去掉坏包)
  XDP DROP         :    197
  tcpdump 采到     :    974  (10% 采样,不可比)
  iptables 命中    :   9650
  conntrack 新建   :    100  (连接数,不是包数)
  socket 收到      :   9354  (去掉重传)
  应用日志         :   9261  (有采样/丢日志)

  对账差异: 网卡(10000) - 应用(9261) = 739 (7.4%)

  ---------- 方式 C:counter drift 告警 ----------
  网卡 RX       -> XDP 看到         差    51 (0.5%)
  XDP 看到      -> iptables 命中    差   299 (3.0%)
  iptables 命中 -> socket 收到      差   296 (3.1%)
  socket 收到   -> 应用日志         差    93 (1.0%)

  看最后这一列就能立刻判断:哪一层丢得多,是不是正常(坏包 / DROP / 重传合并本来就该丢)还是异常。

  六、生产落地方案

  1. eBPF 统一观测(Hubble / Cilium / Pixie)

  # Hubble:给每条 flow 打 ID,所有层共用
  hubble observe --output jsonpb | jq '.flow | {id, verdict, source, destination}'

  # 一条 flow 在 各 hook 点的命中,能直接 join
  hubble observe --from-pod frontend --to-pod backend --verdict DROPPED

  2. counter drift 监控(Prometheus recording rule)

  # 各层计数器差异监控
  - record: network:drift:nic_to_socket
    expr: |
      rate(node_network_receive_packets_total[1m])
      - rate(container_network_receive_packets_total[1m])

  - alert: NetworkDriftHigh
    expr: network:drift:nic_to_socket > 1000
    for: 5m
    annotations:
      summary: "网卡收到的包和应用收到的差异过大,可能有异常丢包"

  3. flow ID 贯穿应用与内核

  # 用 SO_MARK 在 socket 上打 mark,eBPF 读 mark 关联应用 trace ID
  # 应用里:
  setsockopt(fd, SOL_SOCKET, SO_MARK, &trace_id, sizeof(trace_id));

  # eBPF 里:
  SEC("cgroup_skb/egress")
  int log_egress(struct __sk_buff *skb) {
      bpf_printk("flow=%u bytes=%u\n", skb->mark, skb->len);
      return 1;
  }

  4. pcap + OpenTelemetry 关联

  # tcpdump 带 flow ID(TCP timestamp / TCP option)
  tcpdump -i eth0 -w cap.pcap 'tcp'

  # 应用端打 trace ID,Jaeger/Tempo 里查
  # 需要时:拿 trace ID 反查 pcap 里对应的包

  5. 关键对账指标(SRE 必看)

  网卡 rx_packets      vs  XDP 计数              → 驱动层是否有丢
  XDP 计数             vs  iptables INPUT 命中    → 内核前段是否丢
  iptables 命中        vs  conntrack new/packets  → 有状态处理是否正常
  conntrack packets    vs  ss -s 的 established   → 是否有连接泄漏
  socket recv          vs  应用 metrics           → 应用层是否处理不过来
  应用 metrics         vs  外部用户看到的         → SLO 差距的根因

  七、一句话总结

  ▎ 可观测性不一致 = 不同观察点看到的不是同一件事。
  ▎
  ▎ 解法三板斧:
  ▎ - 打标签:flow ID / trace ID 贯穿所有层,对账有 key
  ▎ - 对差异:各层计数器定期 diff,drift 超阈值告警(不追求绝对一致,追求差异可解释)
  ▎ - 定真相:选一层(通常 eBPF/Hubble)作为权威源,其他层做交叉验证
  ▎
  ▎ 口诀:"同包同 ID,同时同戳,同维同源,差异有据。"
  ▎ 生产环境真正做到一致性的,基本都是 Cilium + Hubble + OpenTelemetry + Prometheus drift 告警 四件套。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐