极客时间《架构师训练营》第七周课后作业

第一题

性能压测的时候,随着并发压力的增加,系统响应时间和吞吐量如何变化,为什么?
贴一张经典的的性能测试曲线图:
极客时间《架构师训练营》第七周课后作业
文章图片
性能测试曲线 并发量与响应时间和吞吐量的关系,通俗来说可以分为三个阶段:
  1. 轻负载阶段
    这个阶段负载远未达到系统软硬件瓶颈,资源随时待命,请求被以最快的速度计算返回。响应时间保持平稳,几乎为最短消耗时间;吞吐量与负载也呈线性增长关系。
  2. 重负载阶段
    该阶段系统无法再实现一次性处理所有响应了,受某些资源的限制,一些请求被阻塞在队列内,但软硬件依旧可以承受这种负载;响应时间开始单调递增,吞吐量保持相对稳定。
  3. 压垮阶段
    这个阶段软硬件已无法承受这么大的负载了,系统资源消耗殆尽;响应时间垂直上涨,吞吐量呈断崖式下降。
第二题
用你熟悉的编程语言写一个 web 性能压测工具,输入参数:URL、请求总次数、并发数。输出参数:平均响应时间、95%响应时间。用这个测试工具以 10 并发、100 次请求压测百度(或其他网站)
我是前端开发,用 Typescript(Javascript 超集)写并发有天然的语言优势,哈哈。
预热
【极客时间《架构师训练营》第七周课后作业】先准备两个方法,即响应时间的平均数和 95% 数:
// utils.ts export function response_avg(arr: number[]): number { return arr.reduce((p, c) => p + c, 0) / arr.length; }export function response_95(arr: number[]): number { arr.sort((a, b) => a - b); const idx: number = (arr.length * 0.95) | 0; return arr[idx]; }

fetch 方法
请求函数我用到了axios库,但是原生的 axios 请求不能计算响应时间,所以魔改了一下:
  1. 给它的拦截器加了两个中间件:为 request 添加发起时间,为 response 计算响应时间
  2. 封装了 axios,export fetch 方法并返回本次请求的响应时间
// fetch.ts import axios from "axios"; axios.interceptors.request.use( (config: Config) => { config.meta = { requestStartedAt: new Date().getTime() } return config; })axios.interceptors.response.use((response: Response) => { response.responseTime = new Date().getTime() - response.config.meta.requestStartedAt; return response; }); export function fetch(url: string): Promise { return axios(url).then((response: Response) => response.responseTime); }

并发池
并发设计的思路很简单,就是建一个并发池;假设并发数为 10,就在这个并发池里放 10 个执行器——executor。TS 有个好处就是不用开多线程,天然的异步语言;直接 Promise.all 就可以并发执行池子里所有的 executor 了。
const asyncPool: Promise[] = []; // concurrency = 10; while(concurrency--){ asyncPool.push( executor() ); }await Promise.all(asyncPool)

执行器
Executor 的设计,我用到了 Promise-then 可以串行执行异步函数的功能。通过递归调用,并发池里的 executor 就会不断地消费请求,直到完成目标请求数。
type Request = () => Promise; function executor(requests: Request [], rts: number[] = []) {const req: Request= requests.pop(); if(req === undefined) return Promise.resolve(rts)return req().then((rt) => executor(requests, [...rts, rt])); }

  • 参数 requests 是所有请求的集合——函数数组;Request 是一个别名类型,代表一个返回 Promise 的函数。所有的 executor 都会竞争执行这个数组里的请求,直至为 0。
  • 另一个参数 rts 就是 Response Times 的缩写,目的是保存每个请求的响应时间。
测试代码
把上述代码组合起来,就得到了一个统计输出函数了:
function runConcurrencyTest( args: {url: string, concurrency: number, times: number} ) {const requests: Request[] = [...Array(args.times)].fill(() => fetch(args.url)); const asyncPool: Promise[] = []; let limit: number = args.concurrency; while( limit-- ) { asyncPool.push( executor(requests) ) }return Promise.all(asyncPool) .then((rts) => {const responseTimes: number[] = rts.flat()return { avg: response_avg(responseTimes), res_95: response_95(responseTimes), }; }); }

附上 github 源码:main.ts
最后,我试了一下百度的响应结果:{ avg: 2511.72, res_95: 2854 }, 平均要 2 秒多;感觉挺慢的,看了一下浏览器加载时间也差不多,应该是百度需要加载的资源太多了吧。我又测了一下自家的网站:{ avg: 473.87, res_95: 657 },竟然比百度要快,总算我平日里没白忙活。

    推荐阅读