3.11 针对非业务的通过框架开发,如何做需求分析和设计?
3.11.1 需求分析
对于非业务通用框架的开发,做需求分析的时候,除了功能性需求分析之外,还需要考虑框架的非功能性需求。
- 易用性
- 性能
- 扩展性
- 容错性
- 通用性
3.11.1.1 项目实例
设计开发一个小的框架,能够获取接口调用的各种统计信息,比如,响应时间的最大值(max)、最小值(min)、平均值(avg)、百分位值(percentile)、接口调用次数(count)、频率(tps) 等,并且支持将统计结果以各种显示格式(比如:JSON 格式、网页格式、自定义显示格式等)输出到各种终端(Console 命令行、HTTP 网页、Email、日志文件、自定义输出终端等),以方便查看。
-
功能性需求
-
接口统计信息:包括接口响应时间的统计信息,以及接口调用次数的统计信息等。
-
统计信息的类型:max、min、avg、percentile、count、tps 等。
-
统计信息显示格式:Json、Html、自定义显示格式。
-
统计信息显示终端:Console、Email、HTTP 网页、日志、自定义显示终端。
-
线框图,原型设计
-
-
隐藏性需求
- 统计触发方式:包括主动和被动两种。主动表示以一定的频率定时统计数据,并主动推送到显示终端,比如邮件推送。被动表示用户触发统计,比如用户在网页中选择要统计的时间区间,触发统计,并将结果显示给用户。
- 统计时间区间:框架需要支持自定义统计时间区间,比如统计最近 10 分钟的某接口的 tps、访问次数,或者统计 12 月 11 日 00 点到 12 月 12 日 00 点之间某接口响应时间的最大值、最小值、平均值等。
- 统计时间间隔:对于主动触发统计,我们还要支持指定统计时间间隔,也就是多久触发一次统计显示。比如,每间隔 10s 统计一次接口信息并显示到命令行中,每间隔 24 小时发送一封统计信息邮件。
-
非功能性需求分析
- 易用性:易集成、易插拔、跟业务代码是否松耦合、提供的接口是否够灵活等等。
- 性能:低延迟,统计代码不影响或很少影响接口本身的响应时间;希望框架本身对内存的消耗不能太大。
- 扩展性:在不修改或尽量少修改代码的情况下添加新的功能。
- 容错性:对框架可能存在的各种异常情况都考虑全面,对外暴露的接口抛出的所有运行时、非运行时异常都进行捕获处理。
- 通用性:尽可能通用。除了接口统计这样一个需求,还可以适用到其他哪些场景中,比如是否还可以处理其他事件的统计信息,比如 SQL 请求时间的统计信息、业务统计信息(比如支付成功率)等。
3.11.2 设计
对于复杂框架的设计,几个小技巧,其中包括:
- 画产品线框图
- 聚焦简单应用场景
- 设计实现最小原型
- 画系统设计图等
- 这些方法的目的都是为了让问题简化、具体、明确,提供一个迭代设计开发的基础,逐步推进。
3.11.2.1 最小原型
- 对于性能计数器这个框架的开发来说,可以先聚焦于一个非常具体、简单的应用场景,比如统计用户注册、登录这两个接口的响应时间的最大值和平均值、接口调用次数,并且将统计结果以 JSON 的格式输出到命令行中。
//应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数
public class UserController {
public void register(UserVo user) {
//...
}
public UserVo login(String telephone, String password) {
//...
}
}
-
要输出接口的响应时间的最大值、平均值和接口调用次数,我们首先要采集每次接口请求的响应时间,并且存储起来,然后按照某个时间间隔做聚合统计,最后才是将结果输出。
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) {//省略代码实现} private double avg(List<Double> dataset) {//省略代码实现} }
-
统计注册、登录接口的响应时间和访问次数
//应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数 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); } }
3.11.2.2 系统设计图
- 数据采集:负责打点采集原始数据,包括记录每次接口请求的响应时间和请求时间。数据采集过程要高度容错,不能影响到接口本身的可用性。除此之外,因为这部分功能是暴露给框架的使用者的,所以在设计数据采集 API 的时候,我们也要尽量考虑其易用性。
- 存储:负责将采集的原始数据保存下来,以便后面做聚合统计。数据的存储方式有多种,比如:Redis、MySQL、HBase、日志、文件、内存等。数据存储比较耗时,为了尽量地减少对接口性能(比如响应时间)的影响,采集和存储的过程异步完成。
- 聚合统计:负责将原始数据聚合为统计数据,比如:max、min、avg、pencentile、count、tps 等。为了支持更多的聚合统计规则,代码希望尽可能灵活、可扩展。
- 显示:负责将统计数据以某种格式显示到终端,比如:输出到命令行、邮件、网页、自定义显示终端等。