设计模式简记-实战二:如何实现一个支持各种统计规则的性能计数器?

Posted 杨海星

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式简记-实战二:如何实现一个支持各种统计规则的性能计数器?相关的知识,希望对你有一定的参考价值。

3.12 实战二:如何实现一个支持各种统计规则的性能计数器?

3.12.1 划分职责进而识别出有哪些类

根据需求描述,先大致识别出下面几个接口或类。这一步不难,完全就是翻译需求。

  • MetricsCollector 类负责提供 API,来采集接口请求的原始数据。我们可以为 MetricsCollector 抽象出一个接口,但这并不是必须的,因为暂时我们只能想到一个 MetricsCollector 的实现方式。
  • MetricsStorage 接口负责原始数据存储,RedisMetricsStorage 类实现 MetricsStorage 接口。这样做是为了今后灵活地扩展新的存储方法,比如用 HBase 来存储。
  • Aggregator 类负责根据原始数据计算统计数据。
  • ConsoleReporter 类、EmailReporter 类分别负责以一定频率统计并发送统计数据到命令行和邮件。至于 ConsoleReporter 和 EmailReporter 是否可以抽象出可复用的抽象类,或者抽象出一个公共的接口,我们暂时还不能确定。

3.12.2 定义类及类与类之间的关系

  • 识别出几个核心的类之后,先在 IDE 中创建好这几个类,然后开始试着定义它们的属性和方法。在设计类、类与类之间交互的时候,不断地用之前学过的设计原则和思想来审视设计是否合理

    比如,是否满足单一职责原则、开闭原则、依赖注入、KISS 原则、DRY 原则、迪米特法则,是否符合基于接口而非实现编程思想,代码是否高内聚、低耦合,是否可以抽象出可复用代码等等。

  • 数据采集类

    public class MetricsCollector {
      private MetricsStorage metricsStorage;//基于接口而非实现编程
    
      //依赖注入
      public MetricsCollector(MetricsStorage metricsStorage) {
        this.metricsStorage = metricsStorage;
      }
    
      //用一个函数代替了最小原型中的两个函数
      public void recordRequest(RequestInfo requestInfo) {
        if (requestInfo == null || StringUtils.isBlank(requestInfo.getApiName())) {
          return;
        }
        metricsStorage.saveRequestInfo(requestInfo);
      }
    }
    
    public class RequestInfo {
      private String apiName;
      private double responseTime;
      private long timestamp;
      //...省略constructor/getter/setter方法...
    }
    
  • MetricsStorage 类和 RedisMetricsStorage 类的属性和方法也比较明确。具体的代码实现如下所示。注意,一次性取太长时间区间的数据,可能会导致拉取太多的数据到内存中,有可能会撑爆内存(OOM, FULL GC)。

    public interface MetricsStorage {
      void saveRequestInfo(RequestInfo requestInfo);
    
      List<RequestInfo> getRequestInfos(String apiName, long startTimeInMillis, long endTimeInMillis);
    
      Map<String, List<RequestInfo>> getRequestInfos(long startTimeInMillis, long endTimeInMillis);
    }
    
    public class RedisMetricsStorage implements MetricsStorage {
      //...省略属性和构造函数等...
      @Override
      public void saveRequestInfo(RequestInfo requestInfo) {
        //...
      }
    
      @Override
      public List<RequestInfo> getRequestInfos(String apiName, long startTimestamp, long endTimestamp) {
        //...
      }
    
      @Override
      public Map<String, List<RequestInfo>> getRequestInfos(long startTimestamp, long endTimestamp) {
        //...
      }
    }
    
  • 统计和显示所要完成的功能逻辑细分:

    • 根据给定的时间区间,从数据库中拉取数据;
    • 根据原始数据,计算得到统计数据;
    • 将统计数据显示到终端(命令行或邮件);
    • 定时触发以上 3 个过程的执行。

    选择把第 1、3、4 逻辑放到 ConsoleReporter 或 EmailReporter 类中,把第 2 个逻辑放到 Aggregator 类中。其中,Aggregator 类负责的逻辑比较简单把它设计成只包含静态方法的工具类。具体的代码实现如下所示:

    public class Aggregator {
      public static RequestStat aggregate(List<RequestInfo> requestInfos, long durationInMillis) {
        double maxRespTime = Double.MIN_VALUE;
        double minRespTime = Double.MAX_VALUE;
        double avgRespTime = -1;
        double p999RespTime = -1;
        double p99RespTime = -1;
        double sumRespTime = 0;
        long count = 0;
        for (RequestInfo requestInfo : requestInfos) {
          ++count;
          double respTime = requestInfo.getResponseTime();
          if (maxRespTime < respTime) {
            maxRespTime = respTime;
          }
          if (minRespTime > respTime) {
            minRespTime = respTime;
          }
          sumRespTime += respTime;
        }
        if (count != 0) {
          avgRespTime = sumRespTime / count;
        }
        long tps = (long)(count / durationInMillis * 1000);
        Collections.sort(requestInfos, new Comparator<RequestInfo>() {
          @Override
          public int compare(RequestInfo o1, RequestInfo o2) {
            double diff = o1.getResponseTime() - o2.getResponseTime();
            if (diff < 0.0) {
              return -1;
            } else if (diff > 0.0) {
              return 1;
            } else {
              return 0;
            }
          }
        });
        int idx999 = (int)(count * 0.999);
        int idx99 = (int)(count * 0.99);
        if (count != 0) {
          p999RespTime = requestInfos.get(idx999).getResponseTime();
          p99RespTime = requestInfos.get(idx99).getResponseTime();
        }
        RequestStat requestStat = new RequestStat();
        requestStat.setMaxResponseTime(maxRespTime);
        requestStat.setMinResponseTime(minRespTime);
        requestStat.setAvgResponseTime(avgRespTime);
        requestStat.setP999ResponseTime(p999RespTime);
        requestStat.setP99ResponseTime(p99RespTime);
        requestStat.setCount(count);
        requestStat.setTps(tps);
        return requestStat;
      }
    }
    
    public class RequestStat {
      private double maxResponseTime;
      private double minResponseTime;
      private double avgResponseTime;
      private double p999ResponseTime;
      private double p99ResponseTime;
      private long count;
      private long tps;
      //...省略getter/setter方法...
    }
    
    • ConsoleReporter 类相当于一个上帝类,定时根据给定的时间区间,从数据库中取出数据,借助 Aggregator 类完成统计工作,并将统计结果输出到命令行。具体的代码实现如下所示:
    public class ConsoleReporter {
      private MetricsStorage metricsStorage;
      private ScheduledExecutorService executor;
    
      public ConsoleReporter(MetricsStorage metricsStorage) {
        this.metricsStorage = metricsStorage;
        this.executor = Executors.newSingleThreadScheduledExecutor();
      }
      
      // 第4个代码逻辑:定时触发第1、2、3代码逻辑的执行;
      public void startRepeatedReport(long periodInSeconds, long durationInSeconds) {
        executor.scheduleAtFixedRate(new Runnable() {
          @Override
          public void run() {
            // 第1个代码逻辑:根据给定的时间区间,从数据库中拉取数据;
            long durationInMillis = durationInSeconds * 1000;
            long endTimeInMillis = System.currentTimeMillis();
            long startTimeInMillis = endTimeInMillis - durationInMillis;
            Map<String, List<RequestInfo>> requestInfos =
                    metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis);
            Map<String, RequestStat> stats = new HashMap<>();
            for (Map.Entry<String, List<RequestInfo>> entry : requestInfos.entrySet()) {
              String apiName = entry.getKey();
              List<RequestInfo> requestInfosPerApi = entry.getValue();
              // 第2个代码逻辑:根据原始数据,计算得到统计数据;
              RequestStat requestStat = Aggregator.aggregate(requestInfosPerApi, durationInMillis);
              stats.put(apiName, requestStat);
            }
            // 第3个代码逻辑:将统计数据显示到终端(命令行或邮件);
            System.out.println("Time Span: [" + startTimeInMillis + ", " + endTimeInMillis + "]");
            Gson gson = new Gson();
            System.out.println(gson.toJson(stats));
          }
        }, 0, periodInSeconds, TimeUnit.SECONDS);
      }
    }
    
    public class EmailReporter {
      private static final Long DAY_HOURS_IN_SECONDS = 86400L;
    
      private MetricsStorage metricsStorage;
      private EmailSender emailSender;
      private List<String> toAddresses = new ArrayList<>();
    
      public EmailReporter(MetricsStorage metricsStorage) {
        this(metricsStorage, new EmailSender(/*省略参数*/));
      }
    
      public EmailReporter(MetricsStorage metricsStorage, EmailSender emailSender) {
        this.metricsStorage = metricsStorage;
        this.emailSender = emailSender;
      }
    
      public void addToAddress(String address) {
        toAddresses.add(address);
      }
    
      public void startDailyReport() {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        Date firstTime = calendar.getTime();
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
          @Override
          public void run() {
            long durationInMillis = DAY_HOURS_IN_SECONDS * 1000;
            long endTimeInMillis = System.currentTimeMillis();
            long startTimeInMillis = endTimeInMillis - durationInMillis;
            Map<String, List<RequestInfo>> requestInfos =
                    metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis);
            Map<String, RequestStat> stats = new HashMap<>();
            for (Map.Entry<String, List<RequestInfo>> entry : requestInfos.entrySet()) {
              String apiName = entry.getKey();
              List<RequestInfo> requestInfosPerApi = entry.getValue();
              RequestStat requestStat = Aggregator.aggregate(requestInfosPerApi, durationInMillis);
              stats.put(apiName, requestStat);
            }
            // TODO: 格式化为html格式,并且发送邮件
          }
        }, firstTime, DAY_HOURS_IN_SECONDS * 1000);
      }
    }
    

3.12.3 将类组装起来并提供执行入口

两个执行入口:一个是 MetricsCollector 类,提供了一组 API 来采集原始数据;另一个是 ConsoleReporter 类和 EmailReporter 类,用来触发统计显示。框架具体的使用方式如下所示:

public class Demo {
  public static void main(String[] args) {
    MetricsStorage storage = new RedisMetricsStorage();
    ConsoleReporter consoleReporter = new ConsoleReporter(storage);
    consoleReporter.startRepeatedReport(60, 60);

    EmailReporter emailReporter = new EmailReporter(storage);
    emailReporter.addToAddress("wangzheng@xzg.com");
    emailReporter.startDailyReport();

    MetricsCollector collector = new MetricsCollector(storage);
    collector.recordRequest(new RequestInfo("register", 123, 10234));
    collector.recordRequest(new RequestInfo("register", 223, 11234));
    collector.recordRequest(new RequestInfo("register", 323, 12334));
    collector.recordRequest(new RequestInfo("login", 23, 12434));
    collector.recordRequest(new RequestInfo("login", 1223, 14234));

    try {
      Thread.sleep(100000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

以上是关于设计模式简记-实战二:如何实现一个支持各种统计规则的性能计数器?的主要内容,如果未能解决你的问题,请参考以下文章

设计模式简记-实战二:针对非业务的通过框架开发,如何做需求分析设计

设计模式简记-设计符合设计原则的业务系统之需求分析

设计模式简记-通过重构增强代码可测试性实战

设计模式简记-通过重构增强代码可测试性实战

组件库实战 | 教你如何设计Web世界中的表单验证

组件库实战 | 教你如何设计Web世界中的表单验证