第二部分:实战二

第二部分:实战二

实战二(上)

项目背景

  • 文中举例,设计开发一个小的框架,能够获取接口调用的各种统计信息,并且支持将统计结果以各种显示格式输出到各种终端,以方便查看。

需求分析

  • 性能计数器作为一个跟业务无关的功能,我们完全可以把它开发成一个独立的框架或者类库,集成到很多业务系统中。
  • 作为可被复用的框架,除了功能性需求之外,非功能性需求也非常重要。

功能性需求分析

  • 接口统计信息:包括接口响应时间的统计信息,以及接口调用次数的统计信息等。
  • 统计信息的类型:max、min、avg、percentile、count、tps 等。
  • 统计信息显示格式:Json、Html、自定义显示格式。
  • 统计信息显示终端:Console、Email、HTTP 网页、日志、自定义显示终端。
  • 统计触发方式:包括主动和被动两种。
  • 统计时间区间:框架需要支持自定义统计时间区间。
  • 统计时间间隔:对于主动触发统计,我们还要支持指定统计时间间隔,也就是多久触发一次统计显示。

image

非功能性需求分析

  • 易用性:
    • 框架是否易集成、易插拔、跟业务代码是否松耦合、提供的接口是否够灵活等等,都是我们应该花心思去思考和设计的。
    • 有的时候,文档写得好坏甚至都有可能决定一个框架是否受欢迎。
  • 性能:
    • 对于需要集成到业务系统的框架来说,我们不希望框架本身的代码执行效率,对业务系统有太多性能上的影响。
  • 扩展性:
    • 在不修改或尽量少修改代码的情况下添加新的功能。
    • 使用者可以在不修改框架源码,甚至不拿到框架源码的情况下,为框架扩展新的功能。这就有点类似给框架开发插件。
    • 比如,在不修改框架源码的情况下,以继承框架中方法的方式来达到修改框架源码中方法的目的。
  • 容错性:
    • 不能因为框架本身的异常导致接口请求出错。
    • 对外暴露的接口抛出的所有运行时、非运行时异常都进行捕获处理。
  • 通用性:
    • 为了提高框架的复用性,能够灵活应用到各种场景中。框架在设计的时候,要尽可能通用。

框架设计

  • 数据采集:
    • 负责打点采集原始数据,包括记录每次接口请求的响应时间和请求时间。
    • 数据采集过程要高度容错,不能影响到接口本身的可用性。
    • 暴露给框架的使用者,也要尽量考虑其易用性。
  • 存储:
    • 负责将采集的原始数据保存下来,以便后面做聚合统计。
    • 数据存储比较耗时,为了尽量地减少对接口性能(比如响应时间)的影响,采集和存储的过程异步完成。
  • 聚合统计:
    • 负责将原始数据聚合为统计数据。
    • 为了支持更多的聚合统计规则,代码希望尽可能灵活、可扩展。
  • 显示:
    • 负责将统计数据以某种格式显示到终端。

image

解决一个简单应用场景的性能计数器:统计用户注册、登录这两个接口的响应时间的最大值和平均值、接口调用次数,并且将统计结果以 JSON 的格式输出到命令行中。

应用场景的代码,具体如下所示:

// 应用场景:统计下面两个接口 (注册和登录)的响应时间和访问次数
public class UserController {
	public void register(UserVo user) {
		//...
	}
	public UserVo login(String telephone, String password) {
		//...
	}
}

最小原型实现如下所示:recordResponseTime() 和 recordTimestamp() 两个函数分别用来记录接口请求的响应时间和访问时间,startRepeatedReport() 函数以指定的频率统计数据并输出结果

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.google.gson.Gson;

public class Metrics {
        // Map 的 key 是接口名称,value 对应接口请求的响应时间或时间戳;
        private Map<String, List<Double>> responseTimes = new HashMap<>();
        private Map<String, List<Double>> timestamps = new HashMap<>();
        private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

        public void recordResponseTime(String apiName, double responseTime) {
            responseTimes.putIfAbsent(apiName, new ArrayList<>());
            responseTimes.get(apiName).add(responseTime);
        }

        public void recordTimestamp(String apiName, double timestamp) {
            timestamps.putIfAbsent(apiName, new ArrayList<>());
            timestamps.get(apiName).add(timestamp);
        }

        public void startRepeatedReport(long period, TimeUnit unit){
            executor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    Gson gson = new Gson();
                    Map<String, Map<String, Double>> stats = new HashMap<>();
                    for (Map.Entry<String, List<Double>> entry : responseTimes.entrySet()) {
                        String apiName = entry.getKey();
                        List<Double> apiRespTimes = entry.getValue();
                        stats.putIfAbsent(apiName, new HashMap<>());
                        stats.get(apiName).put("max", max(apiRespTimes));
                        stats.get(apiName).put("avg", avg(apiRespTimes));
                    }
                    for (Map.Entry<String, List<Double>> entry : timestamps.entrySet()) {
                        String apiName = entry.getKey();
                        List<Double> apiTimestamps = entry.getValue();
                        stats.putIfAbsent(apiName, new HashMap<>());
                        stats.get(apiName).put("count", (double)apiTimestamps.size());
                    }
                    System.out.println(gson.toJson(stats));

                }
            }, 0, period, unit);
        }

    private double max(List<Double> dataset) {
        // 省略代码实现
        return (Double)null;
        
    }
    private double avg(List<Double> dataset) {
        // 省略代码实现
        return (Double)null;
    }

}

如何用它来统计注册、登录接口的响应时间和访问次数,具体的代码如下所示:

// 应用场景:统计下面两个接口 (注册和登录)的响应时间和访问次数
import java.util.concurrent.TimeUnit;

public class UserController {
    private Metrics metrics = new Metrics();

    public UserController() {
        metrics.startRepeatedReport(60, TimeUnit.SECONDS);
    }

    public void register(UserVo user) {
        long startTimestamp = System.currentTimeMillis();
        metrics.recordTimestamp("regsiter", startTimestamp);
        //...
        long respTime = System.currentTimeMillis() - startTimestamp;
        metrics.recordResponseTime("register", respTime);
    }

    public UserVo login(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        metrics.recordTimestamp("login", startTimestamp);
        //...
        long respTime = System.currentTimeMillis() - startTimestamp;
        metrics.recordResponseTime("login", respTime);
        return (UserVo) null;
    }
}

实战二(下)

小步快跑、逐步迭代

  • 数据采集:负责打点采集原始数据,包括记录每次接口请求的响应时间。
  • 存储:负责将采集的原始数据保存下来,以便之后做聚合统计。数据的存储方式有很多种,我们暂时只支持 Redis 这一种存储方式,并且,采集与存储两个过程同步执行。
  • 聚合统计:负责将原始数据聚合为统计数据,包括响应时间的最大值、最小值、平均值、99.9 百分位值、99 百分位值,以及接口请求的次数和 tps。
  • 显示:负责将统计数据以某种格式显示到终端,暂时只支持主动推送给命令行和邮件。命令行间隔 n 秒统计显示上 m 秒的数据(比如,间隔 60s 统计上 60s 的数据)。邮件每日统计上日的数据。

面向对象设计与实现

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

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

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

  • 接下来就是定义类及属性和方法,定义类与类之间的关系。这两步没法分得很开。
  • MetricsStorage 接口定义存取数据相关的属性和方法。RedisMetricsStorage 类实现 MetricsStorage 接口,填充具体的方法和属性。
  • MetricsCollector 类在构造函数中,以依赖注入的方式引入 MetricsStorage 接口,并在类内部的方法中得以调用数据存取的方法。
  • 统计显示所要完成的功能逻辑细分位下面 4 点:
    1. 根据给定的时间区间,从数据库中拉取数据
    2. 根据原始数据,计算得到统计数据
    3. 将统计数据显示到终端(命令行或邮件)
    4. 定时触发以上 3 个过程的执行
  • 面向对象设计和实现要做的事情,就是把合适的代码放到合适的类中。让代码尽量地满足低耦合、高内聚、单一职责、对扩展开放对修改关闭等之前讲到的各种设计原则和思想,尽量地让设计满足代码易复用、易读、易扩展、易维护。
  • 我们暂时选择把第 1、3、4 逻辑放到 ConsoleReporter 或 EmailReporter 类中,把第 2 个逻辑放到 Aggregator 类中。
  • Aggregator 类负责的逻辑比较简单,我们把它设计成只包含静态方法的工具类。静态方法中有统计方式,比如加和、取最大最小等,使用RequestStat 类中的 set 方法赋值给 RequestStat 类定义的这些统计属性。
  • ConsoleReporter 类相当于一个上帝类,定时根据给定的时间区间,从数据库中取出数据,借助 Aggregator 类完成统计工作,并将统计结果输出到命令行。也就是在 4 中定时触发 1、2、3 代码的执行。

MetricsCollector 代码实现:

import org.apache.commons.lang3.StringUtils;

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);
    }
}

RequestInfo 代码实现:

public class RequestInfo {
    private String apiName;
    private double responseTime;
    private long timestamp;

    public RequestInfo(String apiName, int responseTime, int timestamp) {
        this.apiName = apiName;
        this.responseTime = responseTime;
        this.timestamp = timestamp;
    }
    //... 省略 constructor/getter/setter 方法...

    public String getApiName() {
        return apiName;
    }

    public void setApiName(String apiName) {
        this.apiName = apiName;
    }

    public double getResponseTime() {
        return responseTime;
    }

    public void setResponseTime(double responseTime) {
        this.responseTime = responseTime;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }
}

MetricsStorage 代码实现:

import java.util.List;
import java.util.Map;

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);
}

RedisMetricsStorage 代码实现:

import java.util.List;
import java.util.Map;

public class RedisMetricsStorage implements MetricsStorage{

    @Override
    public void saveRequestInfo(RequestInfo requestInfo) {

    }

    @Override
    public List<RequestInfo> getRequestInfos(String apiName, long startTimeInMillis, long endTimeInMillis) {
        return null;
    }

    @Override
    public Map<String, List<RequestInfo>> getRequestInfos(long startTimeInMillis, long endTimeInMillis) {
        return null;
    }
}

Aggregator 代码实现:

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

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;
    }

}

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 方法...

    public double getMaxResponseTime() {
        return maxResponseTime;
    }

    public void setMaxResponseTime(double maxResponseTime) {
        this.maxResponseTime = maxResponseTime;
    }

    public double getMinResponseTime() {
        return minResponseTime;
    }

    public void setMinResponseTime(double minResponseTime) {
        this.minResponseTime = minResponseTime;
    }

    public double getAvgResponseTime() {
        return avgResponseTime;
    }

    public void setAvgResponseTime(double avgResponseTime) {
        this.avgResponseTime = avgResponseTime;
    }

    public double getP999ResponseTime() {
        return p999ResponseTime;
    }

    public void setP999ResponseTime(double p999ResponseTime) {
        this.p999ResponseTime = p999ResponseTime;
    }

    public double getP99ResponseTime() {
        return p99ResponseTime;
    }

    public void setP99ResponseTime(double p99ResponseTime) {
        this.p99ResponseTime = p99ResponseTime;
    }

    public long getCount() {
        return count;
    }

    public void setCount(long count) {
        this.count = count;
    }

    public long getTps() {
        return tps;
    }

    public void setTps(long tps) {
        this.tps = tps;
    }


}

ConsoleReporter 代码实现:

import com.google.gson.Gson;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

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);
    }
}

EmailReporter 代码实现:

import java.util.*;

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);
    }
}

Demo 类实现:

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();
        }
    }

}

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

  • 一个是 MetricsCollector 类,提供了一组 API 来采集原始数据。
  • 另一个是 ConsoleReporter 类和 EmailReporter 类,用来触发统计显示。

Review 设计与实现

  • MetricsCollector 负责采集和存储数据,职责相对来说还算比较单一。它基于接口而非实现编程,通过依赖注入的方式来传递 MetricsStorage 对象,可以在不需要修改代码的情况下,灵活地替换不同的存储方式,满足开闭原则。

  • RedisMetricsStorageMetricsStorage 的设计比较简单。当我们需要实现新的存储方式的时候,只需要实现 MetricsStorage 接口即可。其他接口函数调用的地方都不需要改动,满足开闭原则。

  • Aggregator 类是一个工具类,里面只有一个静态函数,有 50 行左右的代码量,负责各种统计数据的计算。一旦越来越多的统计功能添加进来之后,这个函数的代码量会持续增加,可读性、可维护性就变差了。这个类的设计可能存在职责不够单一、不易扩展等问题,需要在之后的版本中,对其结构做优化。

  • ConsoleReporter和EmailReporter中存在问题较多:

    • 从数据库中取数据、做统计的逻辑都是相同的,可以抽取出来复用,违反了 DRY 原则。
    • 整个类负责的事情比较多,职责不单一。特别是显示部分的代码,可能会比较复杂(比如 Email 的展示方式),最好是将显示部分的代码逻辑拆分成独立的类。
    • 因为代码中涉及线程操作,并且调用了 Aggregator 的静态函数,所以代码的可测试性不好。
posted @ 2021-10-04 19:55  起床睡觉  阅读(36)  评论(0编辑  收藏  举报