0%

之前看了重构的书,最近在公司内部自学网站学习重构的课程,简单记录一些核心要点。

阅读全文 »

JMeter超大入参返回

入参太大,StackOverFlow

如果入参太大,比如几千行的json,http sample直接无法执行,查看日志显示StackOverFlow. 从hashcode方法开始抛出的,怀疑是放入本地变量了。
windows解决方案,新增文件%JMETER_HOME%\bin\setenv.bat,可以容纳近10m的入参。

1
set HEAP=-Xss10m

返回太大,Heap OOM

如果返回的内容太大,除了要禁用查看结果树、开启gzip压缩等,还可以调大堆区,这里同样给出windows解决方案。
windows解决方案,新增文件%JMETER_HOME%\bin\setenv.bat,亲测大约5m以下json返回,GUI不会崩。

1
set HEAP=-Xms4g -Xmx5g -XX:MaxMetaspaceSize=512m

高可用之接口限流

限流方案

  1. 请求存消息队列,实例根据自己处理能力,拉取消息消费
  2. 线程池控制,排满线程池队就直接失败
  3. 请求拦截器,维护一个信号量控制同时并发处理数,超出排队值(require排队)就返回429状态码

信号量方案

Spring管理唯一bean,内含唯一的限流拦截器。以下是关键变量
Semaphore current_model_semaphore 所有请求都共用一个信号量;
int semaphoreSize 信号量有多少permits
int semaphoreMaxQueueSize 设定多少请求可以排队,超了就抛异常了
List apiFilterList 哪些URL需要拦截
ConcurrentMap<String, AtomicInteger> concurrents 接口对应有多少个当前处理数
Boolean returnTooManyRequestError 是否启用429返回

处理逻辑

  1. 判断当前请求的url是否匹配apiFilterList其中一个,不匹配返回filter chain后续处理
  2. 如果未启动429返回,则阻塞地获取信号量。获取成功则继续处理,超时会触发 InterruptedException ,捕捉后抛业务异常
  3. 如果请求头没有标识本次请求为最后一次重试,调用tryAcquire()获取信号量的permits,如果没有现成可用的permits,立即返回,接着就返回429状态码,结束请求
  4. 如果已达最大重试次数,那么本次请求应该更加“倔强”地等。如果排队获取信号量的线程还没达到设定的最长semaphoreMaxQueueSize,那么就acquire等待。如果队伍已满,只能放弃等待直接返回429.
  5. 所有acquire()后,在finally块做信号量的release()
graph TB
    1{判断当前请求的url是否匹配apiFilterList其中一个}
    2[拦截器返回交给filter chian后续处理]
    3{是否启用429返回}
    4{acquire阻塞地获取信号量}
    5[捕获InterruptedException后抛业务异常]
    6[正常走业务逻辑]
    7{是否达到最大重试次数}
    8{tryAcquire有无现成的permits}
    9[快速失败返回429]
    10{是否达到最大排队长度}


    1--无需限流-->2
    1--需要限流-->3
    3--未启用429-->4
    4--超时-->5
    4--成功获取-->6
    3--启用429-->7
    7--否-->8
    8--无-->9
    8--有-->6
    7--是-->10
    10--是-->9
    10--否-->4

在菊厂中做着卑微的螺丝钉,日常经常使用很多很多IT的系统、框架、公共能力,接触的微服务架构。每天都只是简单地使用,了解特性,总是对其中的实现和原理抱有很大的好奇。画瓢系列第一章就是Java web常用Springboot骨架的尝试。直接抄袭公司的东西是不对的,直接用开源的组件模拟一下大概的意思应该是可以的。

阅读全文 »

在菊厂中做着卑微的螺丝钉,日常经常使用很多很多IT的系统、框架、公共能力,接触的微服务架构。每天都只是简单地使用,了解特性,总是对其中的实现和原理抱有很大的好奇。画瓢系列第一章就是Java web常用Springboot骨架的尝试。直接抄袭公司的东西是不对的,直接用开源的组件模拟一下大概的意思应该是可以的。

阅读全文 »

Jmeter测试时间不准?开发拿Postman结果说事

术语解释

Jmeter术语

Latency:请求发出到接收到第一个返回的字节的时间
Response time (= Sample time = Load time = Elapsed time):请求发出到接收完最后一个字节的时间
可以看到响应时间总是比Latency大

Postman术语

Socket Initialization: 打开socket时间,一般常数时间
DNS Lookup:将请求域名换成ip的时间,一般也很短,太慢也存在优化空间
TCP Handshake:TCP握手时间,受网络状态影响
Transfer Start:请求发出到接收到第一个返回的字节的时间。等于Jmeter的Latency,等于Chrome的TTFB(Time To First Byte)
Download:整个返回体的下载(接收)时间
Process:基本是常数时间,接收后Postman做请求头分离等时间

场景复现

同一个请求,返回体Postman显示900k,耗时大约300ms,Jmeter要500+ms。定位问题使用Dynatrace,发现服务器处理时间260+,接近Postman。此时开发就在嘟囔,测试的时间不知道怎么搞的。这时候作为测试负责人,心里窝火,但是事实摆在眼前。只能认真研究为啥。
首先,我们看下具体展开的时间

工具 总时间 TTFB Download
Postman 194 162 26
Jmeter 537 172 365

TTFB/Latency时间差不多,在Download时间上差距很大,为什么Postman这么快呢?经前辈指点说Jmeter要手动添加gzip。
立马查看Jmeter请求的头和Postman请求的头,原来Postman的request header自动加上了Accept-Encoding: gzip, deflate,而JMeter没有。
打开JMeter的Debug级别日志可以看见返回体已经不是明文,实测一下:

工具 总时间 TTFB Download
Postman 194 162 26
Jmeter with gzip 177 171 6

扩展: Accept-Encoding & Content-Encoding

HTTP 请求头 Accept-Encoding 会将客户端能够理解的内容编码方式——通常是某种压缩算法——进行通知(给服务端)。通过内容协商的方式,服务端会选择一个客户端提议的方式,使用并在响应头 Content-Encoding 中通知客户端该选择。

即使客户端和服务器都支持相同的压缩算法,在 identity 指令可以被接受的情况下,服务器也可以选择对响应主体不进行压缩。导致这种情况出现的两种常见的情形是:

要发送的数据已经经过压缩,再次进行压缩不会导致被传输的数据量更小。一些图像格式的文件会存在这种情况;
服务器超载,无法承受压缩需求导致的计算开销。通常,如果服务器使用超过80%的计算能力,微软建议不要压缩。

建议&讨论

  1. 请求体可以常规地加上 Accept-Encoding: gzip, deflate
  2. 如果Sample Time在开启压缩后还是比Latency大很多,说明这个接口返回很大数据。此时我们要考虑服务器的内存使用量,因为大数据量返回的接口常规优化方式就是改大数据库一次的Fetch Size,此时服务器的内存占用量会大幅上升,严重时可以导致OOM。我们在回归性能问题单的同时,要观察服务器的内存使用量。
  3. 大数据量的接口从业务系统同步接口返回可能不是一个最佳的方案,会扰动业务系统的性能。应该通过别的途径,以非实时批处理的方式取大量数据更加符合分析统计的场景。
  4. 开发在优化时往往对Download Time没有特别好的优化方法,我们在确定返回体已经开启压缩后,可以用Latency作为指标。但是聚合报告聚合的是Sample Time,此时我们可以在查看结果树用表格查看结果 中配置保存到csv文件,后期用Excel分析平均值或90%值。

连接池

数据源是各种数据库,甚至你可以认为excel都是一个数据源。连接池是为了复用连接数据源的链接的管理池。
连接池的监控可以反应一部分的业务高峰,形成告警。持续的监控指标也能作为应用崩溃的复盘数据。

监控原理

公司平台使用普罗米修斯采集Java暴露的JMX数据,作为产品研发团队,只需要在程序启动后,将每个数据源包装一个Listener,多个数据源打包成map,交给MBeanExporter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MBeanExporter exporter = new MBeanExporter();
// 探测模式 0不自动 1当前IOC容器查找组件 2 根据的策略进行探测
exporter.setAutodetectMode(0);
Map<String, AtomikosDataSourceBean> dataSources = null; // 自行获取
Map<String, Object> map = new HashMap<String, Object>();
for (String key : dataSources.keySet()) {
AtomikosDataSourceBean dataSrouce = dataSources.get(key);
DataSourceListener listener = new DataSourceListener(dataSrouce);
map.put("Catalina:type=DatasourceConnectionPool,name=" + dataSrouce.getUniqueResourceName(), listener);
}

exporter.setBeans(map);
exporter.afterPropertiesSet();
exporter.afterSingletonsInstantiated();

指标设置

样例代码中,指标的计算是这样的:
最大上限: maxSize
当前创建: poolSize(池中总连接)
空闲数: available(池中可用连接)
已使用: poolSize - available
利用率: (poolSize - available) / poolSize

一开始我是觉得不太合理,监控的时候,不太好监控。仔细思考一下,也可以组合一下用当前暴露的指标做监控:

  1. 当前创建>90 (硬指标,不方便)
  2. 利用率在多个采集周期都>90% (持续高涨,当前创建数应该会增加)

其实如果不是非常学院派,常人理解的指标应该是这样计算:
最大上限: maxSize
当前创建: poolSize
空闲数: maxSize - poolSize
已使用: poolSize - available
利用率: poolSize / maxSize