tio-boot官网 原文链接

基于客户端 IP 的流量数据

概述

本文介绍了如何使用 Tio-boot 通过 MyIpStatListener 来监控基于客户端 IP 的网络流量数据。通过这种方式,可以获取有关网络通信的详细数据,如连接请求次数、发送和接收的字节数、处理消息包的时间等。

实现步骤

IpStatDuration

定义 IP 统计时段,以 5 分钟为例:

import com.litongjava.tio.utils.time.Time;

public interface IpStatDuration {
  public static final Long DURATION_1 = Time.MINUTE_1 * 5;
  public static final Long[] IPSTAT_DURATIONS = new Long[] { DURATION_1 };
}

MyIpStatListener 实现

MyIpStatListener 通过多个接口监听网络通信的各个阶段,记录相关数据:

package com.litongjava.tio.web.hello.stat;

import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.TioConfig;
import com.litongjava.tio.core.intf.Packet;
import com.litongjava.tio.core.stat.IpStat;
import com.litongjava.tio.core.stat.IpStatListener;
import com.litongjava.tio.utils.json.JsonUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyIpStatListener implements IpStatListener {
  public static final MyIpStatListener me = new MyIpStatListener();

  private MyIpStatListener() {}

  @Override
  public void onExpired(TioConfig tioConfig, IpStat ipStat) {
    // 当统计数据过期时,可以将数据入库或记录日志
    if (log.isInfoEnabled()) {
      log.info("统计数据过期,数据如下:\n{}", JsonUtils.toJson(ipStat));
    }
  }

  @Override
  public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect, IpStat ipStat) throws Exception {
    if (log.isInfoEnabled()) {
      log.info("连接成功:\n{}", JsonUtils.toJson(ipStat));
    }
  }

  @Override
  public void onDecodeError(ChannelContext channelContext, IpStat ipStat) {
    if (log.isInfoEnabled()) {
      log.info("解码错误:\n{}", JsonUtils.toJson(ipStat));
    }
  }

  @Override
  public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess, IpStat ipStat) throws Exception {
    if (log.isInfoEnabled()) {
      log.info("消息发送完成:\n{}\n{}", packet.logstr(), JsonUtils.toJson(ipStat));
    }
  }

  @Override
  public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize, IpStat ipStat) throws Exception {
    if (log.isInfoEnabled()) {
      log.info("消息解码完成:\n{}\n{}", packet.logstr(), JsonUtils.toJson(ipStat));
    }
  }

  @Override
  public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes, IpStat ipStat) throws Exception {
    if (log.isInfoEnabled()) {
      log.info("接收字节数:\n{}", JsonUtils.toJson(ipStat));
    }
  }

  @Override
  public void onAfterHandled(ChannelContext channelContext, Packet packet, IpStat ipStat, long cost) throws Exception {
    if (log.isInfoEnabled()) {
      log.info("消息处理完成:\n{}\n{}", packet.logstr(), JsonUtils.toJson(ipStat));
    }
  }
}

配置 TioBootServerConfig

配置 TioBootServerConfig,启用 IP 统计监听器,并设定统计时段:

package com.litongjava.tio.web.hello.config;

import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.server.ServerTioConfig;
import com.litongjava.tio.web.hello.stat.IpStatDuration;
import com.litongjava.tio.web.hello.stat.MyIpStatListener;

@AConfiguration
public class TioBootServerConfig {

  @AInitialization
  public void config() {
    TioBootServer server = TioBootServer.me();
    ServerTioConfig serverTioConfig = server.getServerTioConfig();
    serverTioConfig.debug = true;

    // 设置 IP 统计监听器
    serverTioConfig.setIpStatListener(MyIpStatListener.me);
    // 设置 IP 统计时段
    serverTioConfig.ipStats.addDurations(IpStatDuration.IPSTAT_DURATIONS);
  }
}

实现原理

MyIpStatListener

MyIpStatListener 提供了多种数据获取的时机,可以监控网络通信的各个阶段:

  1. onExpired: 当 IP 统计数据过期时被触发,适合将数据存储到数据库或记录日志。
  2. onAfterConnected: 客户端与服务器成功建立连接后触发,提供有关该连接的统计信息。
  3. onDecodeError: 当发生解码错误时触发,可以记录解码失败的次数。
  4. onAfterSent: 消息发送完成后触发,统计有关发送的消息包和字节数。
  5. onAfterDecoded: 消息解码完成后触发,记录已解码的消息包数量和字节数。
  6. onAfterReceivedBytes: 接收字节数后触发,统计接收到的数据量。
  7. onAfterHandled: 消息处理完成后触发,记录处理消息的时间和资源消耗。

IP 统计时段的作用

IP 统计时段 是指服务器对每个 IP 进行统计的时间窗口。在配置时通过 IpStatDuration 类中的 DURATION_1 设置为 5 分钟(300 秒)。每 5 分钟为一个时间窗口,在这段时间内收集统计信息,比如 IP 发送的字节数、接收的字节数、错误次数等。当时间窗口结束时,onExpired 方法会被调用,清除过期的数据并进行处理。

IpStat

IpStat 类用于监控基于客户端 IP 的网络流量数据,帮助识别恶意行为(如攻击),或者记录某个 IP 的通信状况。以下是 IpStat 类中各个字段的详细解释:

  1. start (Date start)
    记录了统计数据的开始时间。该时间戳用于确定统计数据的起点。
  2. duration (long duration)
    记录了当前统计持续了多长时间,单位是毫秒。这个值通常在 getDuration() 方法中动态计算,表示自 start 开始到当前的时间差。
  3. durationType (Long durationType)
    记录统计时段的长度,单位是秒。常见的时段有 60 秒、3600 秒等,这个字段主要用于设定 IP 监控的时间窗口。
  4. ip (String ip)
    客户端的 IP 地址,表示当前监控的是哪个 IP 的数据。
  5. decodeErrorCount (AtomicInteger decodeErrorCount)
    解码错误的次数。如果某个 IP 在解码请求数据时频繁出错,表明该 IP 存在潜在的攻击行为或其他异常。
  6. requestCount (AtomicInteger requestCount)
    该 IP 发起的连接请求次数,表明该 IP 多次与服务器建立连接。
  7. sentBytes (AtomicLong sentBytes)
    服务器向该 IP 发送的字节数总量。这个字段帮助监控服务器发送给客户端的数据流量。
  8. sentPackets (AtomicLong sentPackets)
    服务器向该 IP 发送的数据包数量。这与 sentBytes 配合使用,可以更好地分析发送的数据包大小和频率。
  9. handledBytes (AtomicLong handledBytes)
    服务器处理的数据字节数总量(针对该 IP)。这个字段监控的是服务器处理来自该 IP 的流量。
  10. handledPackets (AtomicLong handledPackets)
    服务器处理的数据包数量(针对该 IP)。配合 handledBytes,可以反映服务器为该 IP 处理的数据包频率和数据量。
  11. handledPacketCosts (AtomicLong handledPacketCosts)
    处理消息包的总耗时,单位为毫秒。这个字段用于监控服务器处理某个 IP 的请求时花费的总时间,衡量服务器的响应性能。
  12. receivedBytes (AtomicLong receivedBytes)
    服务器从该 IP 接收到的字节总量。此字段监控该 IP 向服务器发送的数据量,用于监控流量和发现异常行为。
  13. receivedTcps (AtomicLong receivedTcps)
    服务器从该 IP 接收到的 TCP 数据包总数。此字段用于监控网络层传输,判断 TCP 层的通信量。
  14. receivedPackets (AtomicLong receivedPackets)
    服务器从该 IP 接收到的应用层数据包总数。与 receivedTcps 相对应,此字段用于监控具体的业务数据包。
  15. getBytesPerTcpReceive()
    计算每个 TCP 连接接收到的平均字节数。可以用于检测慢速攻击(Slow Attack),如果每个 TCP 包接收到的字节数特别少,表明潜在的慢速攻击。
  16. getPacketsPerTcpReceive()
    计算每个 TCP 连接接收到的业务包的平均数量。与慢速攻击检测相关,如果该值较低,表明攻击行为。
  17. getHandledCostsPerPacket()
    计算每个数据包的平均处理耗时。该值用于衡量服务器的性能以及对该 IP 请求的处理效率。
小结
  • 流量数据统计IpStat 可以帮助收集某个 IP 的发送、接收字节数和数据包数,以及服务器处理这些数据包的时长。
  • 异常行为检测:通过统计错误解码次数、每个 TCP 包的字节数等,可以用于检测慢速攻击、异常连接等问题。
  • 性能监控:通过统计数据包处理时间,可以用于监控服务器的性能瓶颈,分析某些 IP 是否消耗了过多资源。

这套统计机制可以帮助系统实时监控 IP 流量,发现潜在的攻击,优化服务器资源分配。

IpStatListener 详解

在 Tio-boot 环境下,MyIpStatListener 提供了一系列接口用于监控和记录基于客户端 IP 的流量数据。这些接口在网络通信的不同阶段被触发,帮助开发者了解和分析通信过程中的各类事件。以下将详细解释这些接口的调用顺序,并结合提供的日志示例解析日志中各字段的数据含义。

日志

2024-09-08 12:41:30.019 [tio-group-2] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterConnected:22 - 连接成功:{"duration":"-1","handledPacketCosts":0,"handledPackets":0,"handledCostsPerPacket":0.0,"bytesPerTcpReceive":0.0,"receivedPackets":0,"packetsPerTcpReceive":0.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":0,"handledBytes":0,"receivedTcps":0,"sentBytes":0,"requestCount":1,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"0毫秒"}
2024-09-08 12:41:30.022 [tio-group-3] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterReceivedBytes:29 - 接收字节数:{"duration":"-1","handledPacketCosts":0,"handledPackets":0,"handledCostsPerPacket":0.0,"bytesPerTcpReceive":1469.0,"receivedPackets":0,"packetsPerTcpReceive":0.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":1469,"handledBytes":0,"receivedTcps":1,"sentBytes":0,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"0毫秒"}
2024-09-08 12:41:30.029 [tio-group-3] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterDecoded:36 - 消息解码完成:request ID_1
 {"duration":"15","handledPacketCosts":0,"handledPackets":0,"handledCostsPerPacket":0.0,"bytesPerTcpReceive":1469.0,"receivedPackets":1,"packetsPerTcpReceive":1.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":1469,"handledBytes":0,"receivedTcps":1,"sentBytes":0,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"15毫秒"}
2024-09-08 12:41:30.216 [tio-group-3] INFO  c.l.t.b.h.h.DefaultHttpRequestHandler.handler:317 -
-----------action report---------------------
request:GET / HTTP/1.1
method:public com.litongjava.tio.utils.resp.RespVo com.litongjava.tio.web.hello.controller.IndexController.index()

2024-09-08 12:41:30.231 [tio-group-3] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterHandled:50 - 消息处理完成:request ID_1
 {"duration":"216","handledPacketCosts":201,"handledPackets":1,"handledCostsPerPacket":201.0,"bytesPerTcpReceive":1469.0,"receivedPackets":1,"packetsPerTcpReceive":1.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":1469,"handledBytes":1469,"receivedTcps":1,"sentBytes":0,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"216毫秒"}
2024-09-08 12:41:30.235 [tio-group-5] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterSent:57 - 消息发送完成:reponse: requestID_1  /
 {"duration":"216","handledPacketCosts":201,"handledPackets":1,"handledCostsPerPacket":201.0,"bytesPerTcpReceive":1469.0,"receivedPackets":1,"packetsPerTcpReceive":1.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":2,"receivedBytes":1469,"handledBytes":1469,"receivedTcps":1,"sentBytes":274,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"216毫秒"}

1、接口调用顺序

根据提供的日志示例,以下是各接口的典型调用顺序:

  1. onAfterConnected

    • 触发时机:当客户端与服务器成功建立连接后立即触发。
    • 作用:记录连接建立的统计信息,如连接时间、客户端 IP 等。
  2. onAfterReceivedBytes

    • 触发时机:服务器接收到来自客户端的字节数据后触发。
    • 作用:统计接收到的字节数、TCP 数据包数等信息。
  3. onAfterDecoded

    • 触发时机:服务器成功解码接收到的数据包后触发。
    • 作用:记录解码完成的消息包数量和字节数。
  4. 业务逻辑处理

    • 触发时机:服务器处理解码后的业务请求,此阶段涉及控制器处理请求。
    • 作用:执行业务逻辑,如处理 HTTP 请求、生成响应等。
  5. onAfterHandled

    • 触发时机:服务器完成业务逻辑处理后触发。
    • 作用:记录处理完成的消息包数量、处理耗时等信息。
  6. onAfterSent

    • 触发时机:服务器完成向客户端发送响应后触发。
    • 作用:统计发送的消息包和字节数,以及发送是否成功。
  7. onExpired

    • 触发时机:IP 统计数据超过设定的统计时段(如 5 分钟)后触发。
    • 作用:处理过期的统计数据,如将数据存储到数据库或记录日志。
注意onDecodeError 仅在发生数据解码错误时触发。在提供的日志示例中未出现解码错误,因此未调用该接口。

2、日志数据含义解析

以下是提供的日志示例及其解析:

1. onAfterConnected
2024-09-08 12:41:30.019 [tio-group-2] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterConnected:22 - 连接成功:{"duration":"-1","handledPacketCosts":0,"handledPackets":0,"handledCostsPerPacket":0.0,"bytesPerTcpReceive":0.0,"receivedPackets":0,"packetsPerTcpReceive":0.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":0,"handledBytes":0,"receivedTcps":0,"sentBytes":0,"requestCount":1,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"0毫秒"}
  • 触发时机: 当客户端与服务器成功建立连接后。
  • 关键字段:

    • ip: 客户端 IP 地址,0:0:0:0:0:0:0:1 表示本地回环地址。
    • requestCount: 请求次数,1 表示首次请求。
    • duration: 持续时间,初始值为 -1 表示还没有数据传输。
  • 含义: 客户端与服务器成功建立了连接,初始化了 IP 统计信息。
2. onAfterReceivedBytes
2024-09-08 12:41:30.022 [tio-group-3] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterReceivedBytes:29 - 接收字节数:{"duration":"-1","handledPacketCosts":0,"handledPackets":0,"handledCostsPerPacket":0.0,"bytesPerTcpReceive":1469.0,"receivedPackets":0,"packetsPerTcpReceive":0.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":1469,"handledBytes":0,"receivedTcps":1,"sentBytes":0,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"0毫秒"}
  • 触发时机: 当服务器从客户端接收到字节数据后。
  • 关键字段:

    • receivedBytes: 收到的字节数,1469 字节。
    • bytesPerTcpReceive: 每个 TCP 接收的字节数为 1469.0
  • 含义: 服务器从客户端接收了 1469 字节的数据。
3. onAfterDecoded
2024-09-08 12:41:30.029 [tio-group-3] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterDecoded:36 - 消息解码完成:request ID_1
 {"duration":"15","handledPacketCosts":0,"handledPackets":0,"handledCostsPerPacket":0.0,"bytesPerTcpReceive":1469.0,"receivedPackets":1,"packetsPerTcpReceive":1.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":1469,"handledBytes":0,"receivedTcps":1,"sentBytes":0,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"15毫秒"}
  • 触发时机: 服务器成功解码收到的数据包后。
  • 关键字段:

    • receivedPackets: 收到的业务包数量为 1
    • formatedDuration: 持续时间为 15 毫秒。
  • 含义: 服务器成功解码了从客户端接收到的一个业务数据包。
4. 业务逻辑处理
2024-09-08 12:41:30.216 [tio-group-3] INFO  c.l.t.b.h.h.DefaultHttpRequestHandler.handler:317 -
-----------action report---------------------
request:GET / HTTP/1.1
method:public com.litongjava.tio.utils.resp.RespVo com.litongjava.tio.web.hello.controller.IndexController.index()
  • 触发时机: 业务逻辑处理阶段。
  • 内容: 处理 GET / 请求,由 IndexController.index() 方法处理。
  • 含义: 服务器处理了来自客户端的 HTTP 请求。
5. onAfterHandled
2024-09-08 12:41:30.231 [tio-group-3] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterHandled:50 - 消息处理完成:request ID_1
 {"duration":"216","handledPacketCosts":201,"handledPackets":1,"handledCostsPerPacket":201.0,"bytesPerTcpReceive":1469.0,"receivedPackets":1,"packetsPerTcpReceive":1.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":0,"receivedBytes":1469,"handledBytes":1469,"receivedTcps":1,"sentBytes":0,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"216毫秒"}
  • 触发时机: 业务逻辑处理完成后。
  • 关键字段:

    • handledPackets: 处理的包数为 1
    • handledPacketCosts: 消息包处理耗时为 201 毫秒。
  • 含义: 服务器成功处理了一个消息包,耗时 201 毫秒。
6. onAfterSent
2024-09-08 12:41:30.235 [tio-group-5] INFO  c.l.t.w.h.s.MyIpStatListener.onAfterSent:57 - 消息发送完成:reponse: requestID_1  /
 {"duration":"216","handledPacketCosts":201,"handledPackets":1,"handledCostsPerPacket":201.0,"bytesPerTcpReceive":1469.0,"receivedPackets":1,"packetsPerTcpReceive":1.0,"ip":"0:0:0:0:0:0:0:1","sentPackets":2,"receivedBytes":1469,"handledBytes":1469,"receivedTcps":1,"sentBytes":274,"requestCount":2,"start":"2024-09-08 12:41:30","durationType":"300","decodeErrorCount":0,"formatedDuration":"216毫秒"}
  • 触发时机: 服务器向客户端发送响应后。
  • 关键字段:

    • sentBytes: 发送的字节数为 274
    • sentPackets: 发送的包数量为 2
  • 含义: 服务器成功向客户端发送了 2 个包,合计 274 字节。

3、日志字段详细解释

以下是日志中 IpStat 对象各字段的详细解释,结合上述日志示例中的数据:

  1. duration

    • 含义:当前统计的持续时间,单位为毫秒。
    • 示例

      • -9: 是初始值或计算错误。
      • 6: 6 毫秒。
      • 209: 209 毫秒。
  2. bytesPerTcpReceive

    • 含义:每个 TCP 接收的平均字节数。
    • 示例1495.0 表示每个 TCP 数据包平均接收 1495 字节。
  3. packetsPerTcpReceive

    • 含义:每个 TCP 接收的业务包数量。
    • 示例

      • 初始为 0.0
      • 更新为 1.0,表示每个 TCP 数据包对应一个业务包。
  4. handledPackets

    • 含义:服务器处理的消息包数量。
    • 示例0 表示尚未处理,1 表示已处理一个消息包。
  5. receivedPackets

    • 含义:服务器接收到的业务包数量。
    • 示例0 表示尚未接收,1 表示已接收一个业务包。
  6. handledPacketCosts

    • 含义:处理消息包的总耗时,单位为毫秒。
    • 示例203 表示处理一个消息包耗时 203 毫秒。
  7. handledCostsPerPacket

    • 含义:每个消息包的平均处理耗时,单位为毫秒。
    • 示例203.0 毫秒。
  8. ip

    • 含义:客户端的 IP 地址。
    • 示例0:0:0:0:0:0:0:1 表示本地回环地址(IPv6)。
  9. handledBytes

    • 含义:服务器处理的总字节数,针对该 IP。
    • 示例0 表示尚未处理,1495 表示已处理 1495 字节。
  10. receivedBytes

    • 含义:服务器从该 IP 接收到的总字节数。
    • 示例0 表示尚未接收,1495 表示已接收 1495 字节。
  11. sentPackets

    • 含义:服务器发送给该 IP 的消息包数量。
    • 示例0 表示尚未发送,日志中未更新。
  12. sentBytes

    • 含义:服务器发送给该 IP 的总字节数。
    • 示例0 表示尚未发送,日志中未更新。
  13. receivedTcps

    • 含义:服务器从该 IP 接收到的 TCP 数据包数量。
    • 示例

      • 初始为 0
      • 更新为 1,表示接收到一个 TCP 数据包。
  14. requestCount

    • 含义:来自该 IP 的连接请求次数。
    • 示例1 表示一次连接请求。
  15. start

    • 含义:统计开始时间。
    • 示例2024-09-08 11:59:08
  16. decodeErrorCount

    • 含义:解码错误的次数。
    • 示例0 表示无解码错误。
  17. formatedDuration

    • 含义:格式化的统计持续时间,便于阅读。
    • 示例

      • "0毫秒"
      • "6毫秒"
      • "209毫秒"
  18. durationType

    • 含义:统计时段的长度,单位为秒。
    • 示例"300" 表示 5 分钟(300 秒)的统计窗口。

4、总结

通过上述日志示例和字段解释,可以清晰地了解在一次完整的客户端请求过程中,MyIpStatListener 各接口的调用顺序及其所记录的数据含义。具体流程如下:

  1. 连接建立onAfterConnected 被调用,记录连接建立的信息。
  2. 数据接收onAfterReceivedBytes 被调用,统计接收到的数据量和 TCP 数据包数。
  3. 数据解码onAfterDecoded 被调用,记录解码完成的消息包数量。
  4. 业务处理:服务器通过控制器处理业务逻辑,相关日志记录处理过程。
  5. 处理完成onAfterHandled 被调用,记录处理完成的消息包数量和耗时。
  6. 响应发送onAfterSent 被调用,记录响应发送的信息。
  7. 数据过期:若统计数据超过设定的时间窗口(如 5 分钟),onExpired 被触发,处理过期的数据。

这种详细的日志记录机制,有助于开发者实时监控网络通信的各个环节,分析性能瓶颈,发现潜在的异常行为,并优化系统的整体性能和安全性。

总结

通过 MyIpStatListener 和 IpStatDuration,我们可以实现基于客户端 IP 的详细流量监控,捕获网络通信中的各类事件。通过这些数据,可以优化服务器资源分配,检测异常行为,并提升系统的整体性能和安全性。

这种实现适用于需要对网络流量进行实时监控和分析的场景,特别是对高流量系统的监控非常有帮助。


李通
215 声望0 粉丝