运用学过的设计原则和思想完善之前讲的性能计数器项目
回顾之前的实现
- 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; } } }); if (count != 0) { int idx999 = (int)(count * 0.999); int idx99 = (int)(count * 0.99); 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 和 EmailReporter两个类部分代码重复(违反DRY),可以抽离;职责也不够单一(违反SRP),不仅包含了reporting的逻辑,还包含了一些其他的数据构造;另外包含静态函数和多线程,可测性不好。
public class ConsoleReporter { private MetricsStorage metricsStorage; private ScheduledExecutorService executor; public ConsoleReporter(MetricsStorage metricsStorage) { this.metricsStorage = metricsStorage; this.executor = Executors.newSingleThreadScheduledExecutor(); } public void startRepeatedReport(long periodInSeconds, long durationInSeconds) { executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { 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(); RequestStat requestStat = Aggregator.aggregate(requestInfosPerApi, durationInMillis); stats.put(apiName, requestStat); } 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); } }
重构之前的实现
版本1中,MetricsCollector收集数据并调用MetricsStorage存储;Aggregator负责计算;XXXReportor不仅是作为reporting的function类,也同时作为了上帝类串联其他类的执行。
重构的思路是,引入一个更简洁的上帝类只做串联工作,不去执行其他的逻辑。让reportor只负责他们自己的reportor的事情。这样代码层级也很清晰,reportor的工作也比较单一,符合SRP。
public interface StatViewer { void output(Map<String, RequestStat> requestStats, long startTimeInMillis, long endTimeInMills); } public class ConsoleViewer implements StatViewer { public void output( Map<String, RequestStat> requestStats, long startTimeInMillis, long endTimeInMills) { System.out.println("Time Span: [" + startTimeInMillis + ", " + endTimeInMills + "]"); Gson gson = new Gson(); System.out.println(gson.toJson(requestStats)); } } public class EmailViewer implements StatViewer { private EmailSender emailSender; private List<String> toAddresses = new ArrayList<>(); public EmailViewer() { this.emailSender = new EmailSender(/*省略参数*/); } public EmailViewer(EmailSender emailSender) { this.emailSender = emailSender; } public void addToAddress(String address) { toAddresses.add(address); } public void output( Map<String, RequestStat> requestStats, long startTimeInMillis, long endTimeInMills) { // format the requestStats to HTML style. // send it to email toAddresses. } }
另一个点就是重构aggregator,将其中的各种function抽出来更小的函数,让aggregate方法更清晰;另外就是在aggregator中放入一个聚合计算逻辑,执行Metrics的计算。
public class Aggregator { public Map<String, RequestStat> aggregate( Map<String, List<RequestInfo>> requestInfos, long durationInMillis) { Map<String, RequestStat> requestStats = new HashMap<>(); for (Map.Entry<String, List<RequestInfo>> entry : requestInfos.entrySet()) { String apiName = entry.getKey(); List<RequestInfo> requestInfosPerApi = entry.getValue(); RequestStat requestStat = doAggregate(requestInfosPerApi, durationInMillis); requestStats.put(apiName, requestStat); } return requestStats; } private RequestStat doAggregate(List<RequestInfo> requestInfos, long durationInMillis) { List<Double> respTimes = new ArrayList<>(); for (RequestInfo requestInfo : requestInfos) { double respTime = requestInfo.getResponseTime(); respTimes.add(respTime); } RequestStat requestStat = new RequestStat(); requestStat.setMaxResponseTime(max(respTimes)); requestStat.setMinResponseTime(min(respTimes)); requestStat.setAvgResponseTime(avg(respTimes)); requestStat.setP999ResponseTime(percentile999(respTimes)); requestStat.setP99ResponseTime(percentile99(respTimes)); requestStat.setCount(respTimes.size()); requestStat.setTps((long) tps(respTimes.size(), durationInMillis/1000)); return requestStat; } // 以下的函数的代码实现均省略... private double max(List<Double> dataset) {} private double min(List<Double> dataset) {} private double avg(List<Double> dataset) {} private double tps(int count, double duration) {} private double percentile999(List<Double> dataset) {} private double percentile99(List<Double> dataset) {} private double percentile(List<Double> dataset, double ratio) {} }
最后就是组装这些类,并定时触发。(将定时触发的逻辑放在上帝类,提升function类的可测性)。
public class ConsoleReporter { private MetricsStorage metricsStorage; private Aggregator aggregator; private StatViewer viewer; private ScheduledExecutorService executor; public ConsoleReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.aggregator = aggregator; this.viewer = viewer; this.executor = Executors.newSingleThreadScheduledExecutor(); } public void startRepeatedReport(long periodInSeconds, long durationInSeconds) { executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { long durationInMillis = durationInSeconds * 1000; long endTimeInMillis = System.currentTimeMillis(); long startTimeInMillis = endTimeInMillis - durationInMillis; Map<String, List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis); Map<String, RequestStat> requestStats = aggregator.aggregate(requestInfos, durationInMillis); viewer.output(requestStats, startTimeInMillis, endTimeInMillis); } }, 0L, periodInSeconds, TimeUnit.SECONDS); } } public class EmailReporter { private static final Long DAY_HOURS_IN_SECONDS = 86400L; private MetricsStorage metricsStorage; private Aggregator aggregator; private StatViewer viewer; public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.aggregator = aggregator; this.viewer = viewer; } 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 = aggregator.aggregate(requestInfos, durationInMillis); viewer.output(stats, startTimeInMillis, endTimeInMillis); } }, firstTime, DAY_HOURS_IN_SECONDS * 1000); } }
具体使用:
我们需要在应用启动的时候,创建好 ConsoleReporter 对象,并且调用它的 startRepeatedReport() 函数,来启动定时统计并输出数据到终端。同理,我们还需要创建好 EmailReporter 对象,并且调用它的 startDailyReport() 函数,来启动每日统计并输出数据到制定邮件地址。我们通过 MetricsCollector 类来收集接口的访问情况,这部分收集代码会跟业务逻辑代码耦合在一起,或者统一放到类似 Spring AOP 的切面中完成。
Review新的代码
reporter易用性还有待提高,而且他们也存在一定的重复:可以提取出来到父类:
public abstract class ScheduledReporter { protected MetricsStorage metricsStorage; protected Aggregator aggregator; protected StatViewer viewer; public ScheduledReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.aggregator = aggregator; this.viewer = viewer; } protected void doStatAndReport(long startTimeInMillis, long endTimeInMillis) { long durationInMillis = endTimeInMillis - startTimeInMillis; Map<String, List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis); Map<String, RequestStat> requestStats = aggregator.aggregate(requestInfos, durationInMillis); viewer.output(requestStats, startTimeInMillis, endTimeInMillis); } }
To Be Reviewed 25,26,39,40