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

第一题

性能压测的时候,随着并发压力的增加,系统响应时间和吞吐量如何变化,为什么?

贴一张经典的的性能测试曲线图:

极客时间《架构师训练营》第七周课后作业_第1张图片
性能测试曲线

并发量与响应时间和吞吐量的关系,通俗来说可以分为三个阶段:

  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 },竟然比百度要快,总算我平日里没白忙活。

你可能感兴趣的:(极客时间《架构师训练营》第七周课后作业)