第七十章 【重磅加餐】一线大厂如何产生海量请求压测
第1集 Jmeter单接口压测问题点和流量漏斗模型介绍
简介: Jmeter单接口压测问题点和流量漏斗模型介绍
-
压测工具选择
- Jmeter
- LoadLunner
- Apache AB
-
问题点
-
常用的Jmeter压测工具,进行单接口压测是没问题的,可以得出基于某个机器配置下接口的吞吐量
-
但是实际线上业务,用户不可能只访问某个接口
-
更多的是多个接口业务联动,有可能某个接口出现瓶颈导致其他接口出现问题
-
例子
1、轮播图列表接口【4核8G,QPS 3万】 2、分类列表接口【4核8G,QPS 5万】 3、视频详情页接口【4核8G,QPS 4万】 4、加入购物车接口【4核8G,QPS 2万】 5、创建订单接口【4核8G,QPS 8千】 6、注册接口【4核8G,QPS 1万】 7、用户信息查询接口【4核8G,QPS 1万】 8、推荐视频拉链接口【4核8G,QPS 1K】
- 某天你的网站突然火了上了热门,每天几百万的用户访问
-
-
最高的时候每秒有3千用户访问,但后台却挂了。。。。。
明明单机QPS都很高足够支撑这个访问量级了,却还出现问题
分析
- 单个接口支撑是没问题的,但是忽略了某些低QPS的接口,如【推荐视频】接口
- 由于这些接口性能不足,导致系统CPU、内存、链接被耗尽了
- 从而连锁反应导致其他接口RT响应超时、GC频繁、线程池耗尽
什么是流量漏斗模型(别名:流量模型)
- 谁会用:公司产品经理、运营、公司CTO、CEO
- 那我们研发工程师为啥要用?
- 举个例子大家就明白
- 互联网产品其本身就是一个虚拟的漏斗,用户的行为路径有很多,举个淘宝、JD这电商例子
- 首页->查看商品->添加购物车->注册->登录->下单->支付->确认收货
-
最终怎么办
-
需要推测常规用户的访问流量模型 和 随着流量增大 和服务器性能关联指标变化情况
-
比如100万个用户进来
- 有多少是新用户会进行注册
- 有多少是新用户会进入详情页
- 有多少用户是会加入购物车
- 有多少用户是会支付订单
- …
-
知道解决方式后,又带来新的问题,怎么记录用户访问链路【流量模型和增加大流量】?
-
第2集 带你走进流量模型-流量记录重放技术
简介: 带你走进流量模型-流量记录重放技术
- 需要解决的问题
- 记录用户访问链路【流量模型和增加大流量】后各个指标的变化情况,CPU、RT、GC 、内存、带宽等
- 混合链路压测的要点
- 需要真实的用户访问分布数据(流量模型)
- 支持成倍的扩大缩放相关流量数据
- 敏感数据脱敏处理,压测脏数据隔离
- 隔离数据包括不限,日志、Redis、MQ、数据库等
- 敏感信息脱敏:手机号、session信息、联系方式等
-
工具
-
Nginx访问日志(基于HTTP协议)
- 需要二次开发程序读取协议或者使用三方工具
- 成本高,复用性相对弱
-
TCP Copy(基于TCP协议)
- 记录最原始的流量,支持多种协议
- 学习成本高,使用相对复杂
-
GoReplay(基于HTTP协议)
- 仅支持HTTP协议
- 成本低,上手快
-
第3集 流量重放GoReplay介绍和依赖环境讲解
简介: 流量重放GoReplay介绍和依赖环境讲解
- 什么是 流量重放GoReplay
- 地址
- 官网:https://goreplay.org/
- github:https://github.com/buger/goreplay
- GO语言编写的http流量复制工具
- 使用流程简单,支持多个系统,mac、linux、win
- GoReplay 不是代理,而是在后台侦听网络接口上的流量
- 无需更改生产基础架构,只需在与服务相同的机器上运行 GoReplay 守护程序
- 使用者:腾讯、京东、阿里等一线大厂
- 地址
-
流量录制重放特点
- 捕获网络指定端口流量,输出到控制台
- 捕获网络指定端口流量,将原始流量实时重放到其他环境中
- 捕获网络指定端口流量,并保存到文件中
- 捕获网络指定端口流量,请求过滤指定路径流量,并保存到文件中
-
机器和环境选择
- 机器:Nginx所在机器,入口流量
- 安装Go环境
- Go语言是Google开发的具有良好并发能力的编程语言
- Go语言别名Golang
第4集 阿里云Linux服务器安装Go环境和GoReplay实战
简介: 阿里云Linux服务器安装Go环境和GoReplay实战
-
阿里云Nginx机器:112.74.55.160
-
Golang环境安装
- 安装包在本章本集资料里面
- 解压到指定目录
tar -C /usr/local -zxvf go1.5.3.linux-amd64.tar.gz
- 添加PATH环境变量
# 1.打开文件 vim /etc/profile # 2.添加环境变量 export GOROOT=/usr/local/go export PATH=$PATH:$GOROOT/bin # 3.编译生效 source /etc/profile
- 测试
输入 go version 出现版本号即为成功。
GoReplay安装
- 安装包在本章本集资料里面
- 解压工具
tar xvzf gor_1.3.1_x64.tar.gz
- 解压完压缩包后,可以从当前目录进行Gor,也可以将Gor文件复制到的PATH文件下
- 测试
./gor
第5集 GoReplay流量录制和重放功能讲解
简介: GoReplay流量录制和重放功能讲解
-
重放机器准备
- 阿里云Nginx机器:112.74.55.160
- Docker安装Nginx,8080端口作为映射
docker run --name class_nginx -p 8080:80 -d nginx:1.21.6
-
参数
-
输入
- –input-raw : 用于捕获 HTTP 流量时,应指定 IP 地址或界面以及应用程序端口
- –input-file :接收以前使用过的文件记录
- –input-tcp :如果决定将多个转发器Gor实例转发流量到它,Gor聚合实例使用
-
可用输出:
–output-http :重播HTTP流量到给定的端点
–output-file :记录传入到文件的流量
–output-tcp :将传入的数据转发到另一个Gor实例
–output-stdout :用于调试,输出所有数据。
-
-
案例测试
#1.捕获网络流量,表示监听80端口发生的所有网络活动记录到stdout
gor --input-raw :80 --output-stdout
#2.重放,将原始流量重放到其他环境中,同一个服务器但是端口不同,多个亦可
gor --input-raw :80 --output-http="http://127.0.0.1:8080"
#3.捕抓流量请求并保存到文件中,实际会保存为requests.gor文件名
gor --input-raw :80 --output-file requests.gor
#4.从保存下来的流量文件中提取流量向某个端口输出
gor --input-file requests_0.gor --output-http="http://127.0.0.1:8080"
#5.请求过滤指定路径流量,使用该机制,只记录/account-server/api路径下的请求
gor --input-raw :80 --output-http "http://127.0.0.1:8080" --http-allow-url /account-server/api
第6集 GoReplay多倍速度-循环重放流量实战
简介: GoReplay多倍循环重放流量实战
- 多倍重放录制流量
#流量录制的监听命令
gor --input-raw :80 --output-file requests.gor
gor --input-file requests_0.gor --output-http "http://127.0.0.1:8080"
# --input-file 从文件中获取请求数据,重放的时候5倍速度重放; 比如10秒20条请求,扩大2倍后就是5秒就跑完20条请求
# --input-file-loop 无限循环,而不是读完这个文件就停止
# --output-http 发送请求到 http://xdclass.net
# --stats --output-http-stats 每 5 秒输出一次 TPS 数据
gor --input-file "requests_0.gor|200%" --input-file-loop --output-http "http://127.0.0.1:8080" --stats --output-http-stats
gor --input-file "requests_0.gor|2000%" --output-http "http://127.0.0.1:8080"
gor --input-file "requests_0.gor|5000%" --input-file-loop --output-http "http://127.0.0.1:8080" --stats --output-http-stats
gor --input-file "requests_0.gor|6000%" --input-file-loop --output-http "http://127.0.0.1:80" --stats --output-http-stats
第7集 基于Gor录制短链平台基础流量模型数据
简介: 基于Gor录制短链平台基础流量模型数据
-
开启录制
gor --input-raw :80 --output-file requests.gor
-
注意事情
-
各个微服务可以单节点进行部署,方便压测观察数据
-
并非大规模全链路压测,环境和数据没做隔离
-
-
基础数据访问
- 因为我们还没上线,所以直接PostMan访问用于模拟正常用户的链路
- 公司
- 没上线,灰度发布,录制部分流量(产品、运营经理会进行推广的)
- 已经上线版本迭代,直接录制线上流量模型即可的
- 增删改查接口
- 有些新增、删除 是具备唯一性,则会新增错误
- 主要是查询为主,多数业务都 二八原则
- 流量角度:少数接口承接了80%的流量,多数接口承接了20%的流量
- 接口类型:查询类型接口承接了80%的流量,新增/更新/删除 承接了20%的流量
第8集 多倍扩大Gor回放流量模型-压测短链平台
简介: 多倍扩大Gor回放流量模型-压测短链平台
- 流量回放
- 10倍回放
- 50倍回放
- N倍回放(取决机器配置)
gor --input-file "requests_0.gor|300000%" --input-file-loop --output-http "http://127.0.0.1:80" --stats --output-http-stats
-
观察结果
- 分钟调用次数:RT响应、CPU、内存、带宽占用、GC次数、接口成功率等指标
第七十一章 短链平台基础接口补充开发
第1集 账号服务个人信息查询基础接口开发
简介: 账号服务个人信息查询基础接口开发
-
账号微服务接口补充开发
- 查看个人信息模块
/** * 查看个人信息 * @return */ @GetMapping("detail") public JsonData detail(){ JsonData jsonData = accountService.detail(); return jsonData; } @Override public AccountDO detail(Long accountNo) { return accountMapper.selectOne(new QueryWrapper<AccountDO>().eq("account_no", accountNo)); } @Data public class AccountVO { private Long accountNo; /** * 头像 */ private String headImg; /** * 手机号 */ private String phone; /** * 邮箱 */ private String mail; /** * 用户名 */ private String username; /** * 认证级别,DEFAULT,REALNAME,ENTERPRISE,访问次数不一样 */ private String auth; @JsonProperty("createTime") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date gmtCreate; }
第2集 惰性更新流量包前端展示和短链过期时间
简介: 惰性更新流量包前端展示和短链过期时间
-
问题点
- 流量包是用的时候才去检查更新,那如果用户昨天用了但今天没用
- 查看的总的流量包时候,还是旧的流量包
-
解决方式
- 利用当前时间和流量包更新时间进行判断
-
流量包惰性更新涉及的接口
- 分页查看流量包列表
- 查看流量包详情
-
编码实现
private TrafficVO beanProcess(TrafficDO trafficDO) {
TrafficVO trafficVO = new TrafficVO();
BeanUtils.copyProperties(trafficDO, trafficVO);
//惰性更新,前端显示问题,根据更新时间进行判断是否是最新的
//今天日期
String todayStr = TimeUtil.format(new Date(), "yyyy-MM-dd");
String trafficUpdateDate = TimeUtil.format(trafficDO.getGmtModified(), "yyyy-MM-dd");
//日期不一样,则未更新,dayUsed需要为0
if (!todayStr.equalsIgnoreCase(trafficUpdateDate)) {
trafficVO.setDayUsed(0);
}
return trafficVO;
}
- 短链服务expired过期时间处理 前端入参 expired字段
@Data
public class ShortLinkAddRequest {
/**
* 组
*/
private Long groupId;
/**
* 短链标题
*/
private String title;
/**
* 原生url
*/
private String originalUrl;
/**
* 域名id
*/
private Long domainId;
/**
* 域名类型
*/
private String domainType;
/**
* 过期时间
*/
@JsonFormat(locale = "zh",timezone = "GMT+8",pattern="yyyy-MM-dd HH:mm:ss")
private Date expired;
}
- 访问
低于1980的则是永久有效,毫秒
1980-01-01 00:00:00 -> 315504000000L
第3集 数据库 Too many connection异常解决
简介: 数据库 Too many connection异常解决
-
异常信息
- Mysql数据库报错,too many connection
- 结果就是导致无法连接数据库,微服务或者数据库客户端连接超时
-
原因分析
- MySQL默认的连接为100个,系统自带的连接数太小,连接的线程超过系统配置导致出现错误
- 可以通过部署多几个MysqlServer实例,物理分库
#查看当前连接数 show full processlist; #查看最大连接数 show variables like "max_connections"; set global max_connections=1000
- mysql的连接数保持时间-默认 28800(8个小时)
#查看连接睡眠时间,默认是 28800,相对较少调整这个,不能太短,也不能过长; show global variables like 'wait_timeout' wait_timeout解释: 当一个客户端连接到MySQL数据库后,如果客户端不自己断开,也不做任何操作,MySQL数据库会将这个连接保留"wait_timeout"这么长时间(单位是s,默认是28800s,也就是8小时),超过时间之后,MySQL数据库为了节省资源,就会断开这个连接
- 程序没及时关闭连接,产生过多sleep进程
- MySQL默认的连接为100个,系统自带的连接数太小,连接的线程超过系统配置导致出现错误
-
注意
- 上述是临时修改,重启mysql会失效
- 永久修改可以通过修改mysql的配置/etc/my.cnf配置文件
- 搜索下mysql配置文件修改博文
第4集 Flink-IP解析地理位置信息-百度地图api案例
简介: Flink根据IP解析地理位置信息-百度地图api案例
-
本章内容都是跳动比较大,缝缝补补,根据大家遇到的问题补充的知识点
-
IP信息转换为地理位置信息,
- 百度地图文档 :https://lbsyun.baidu.com/index.php?title=webapi/ip-api
- 离线解决方案:纯真IP库 GeoLite2 埃文科技 ip2region
-
代码
@Slf4j public class AsyncLocationRequestFunction extends RichAsyncFunction<ShortLinkWideDO,String> { //private static final String IP_PARSE_URL = "https://restapi.amap.com/v3/ip?ip=%s&output=json&key=4f6e1b4212a5fdec6198720f261892bd"; private static final String IP_PARSE_URL = "http://api.map.baidu.com/location/ip?ak=ot37gt3nQm27omNBBqFHygbxVUafTl2V&ip=%s"; private CloseableHttpAsyncClient httpAsyncClient; @Override public void timeout(ShortLinkWideDO input, ResultFuture<String> resultFuture) throws Exception { resultFuture.complete(Collections.singleton(null)); } @Override public void open(Configuration parameters) throws Exception { this.httpAsyncClient = createAsyncHttpClient(); } @Override public void close() throws Exception { if(httpAsyncClient!=null){ httpAsyncClient.close(); } } @Override public void asyncInvoke(ShortLinkWideDO shortLinkWideDO, ResultFuture<String> resultFuture) throws Exception { String ip = shortLinkWideDO.getIp(); String url = String.format(IP_PARSE_URL,ip); HttpGet httpGet = new HttpGet(url); Future<HttpResponse> future = httpAsyncClient.execute(httpGet, null); CompletableFuture<ShortLinkWideDO> completableFuture = CompletableFuture.supplyAsync(new Supplier<ShortLinkWideDO>() { @Override public ShortLinkWideDO get() { try { HttpResponse response = future.get(); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK) { HttpEntity entity = response.getEntity(); String result = EntityUtils.toString(entity, "UTF-8"); JSONObject locationObj = JSON.parseObject(result); //String city = locationObj.getString("city"); //String province = locationObj.getString("province"); String city = locationObj.getJSONObject("content").getJSONObject("address_detail").getString("city"); String province = locationObj.getJSONObject("content").getJSONObject("address_detail").getString("province"); shortLinkWideDO.setProvince(province); shortLinkWideDO.setCity(city); return shortLinkWideDO; } } catch (InterruptedException | ExecutionException | IOException e) { log.error("ip解析错误,value={},msg={}", shortLinkWideDO, e.getMessage()); } shortLinkWideDO.setProvince("-"); shortLinkWideDO.setCity("-"); return shortLinkWideDO; } }); completableFuture.thenAccept(new Consumer<ShortLinkWideDO>() { @Override public void accept(ShortLinkWideDO shortLinkWideDO) { resultFuture.complete(Collections.singleton(JSON.toJSONString(shortLinkWideDO))); } }); // completableFuture.thenAccept( (dbResult) -> { // resultFuture.complete(Collections.singleton(JSON.toJSONString(shortLinkWideDO))); // }); } private CloseableHttpAsyncClient createAsyncHttpClient() { try { RequestConfig requestConfig = RequestConfig.custom() //返回数据的超时时间 .setSocketTimeout(20000) //连接上服务器的超时时间 .setConnectTimeout(10000) //从连接池中获取连接的超时时间 .setConnectionRequestTimeout(1000) .build(); ConnectingIOReactor ioReactor = new DefaultConnectingIOReactor(); PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor); //设置连接池最大是500个连接 connManager.setMaxTotal(500); //MaxPerRoute是对maxtotal的细分,每个主机的并发最大是300,route是指域名 connManager.setDefaultMaxPerRoute(300); CloseableHttpAsyncClient httpClient = HttpAsyncClients.custom().setConnectionManager(connManager) .setDefaultRequestConfig(requestConfig) .build(); httpClient.start(); return httpClient; } catch (IOReactorException e) { log.error("初始化 CloseableHttpAsyncClient异常:{}",e.getMessage()); return null; } } }
第七十二章 短链平台-前后端联调实战和Bug修复
第1集 短链平台-前端业务功能效果演示
简介: 短链平台-前端业务功能效果演示
- 效果和功能测试
第2集 短链平台-前端技术栈环境搭建和结构讲解
简介: 短链平台-前端页面技术栈环境搭建
-
前端代码(本章本集资料)
-
开发环境说明
-
编辑器
- Vscode( 自己下载即可,或者看HTML课程里面有安装包)
-
框架(确保大版本一致,如果之前安装了旧版node和npm,搜索博文重新升级下)
-
Node
- 版本 v16.15.0
- 【安装包】 作者有偿提供。
-
Npm版本 8.5.5
-
其他版本
Vue3 版本:3.2.13 Ant-Design-Vue 版本:3.2.2 EchartJS 版本:5.3.2
-
-
-
下载代码-安装环境,如何启动?
-
导入VSCode
-
构建启动
npm install npm run serve
-
核心目录结构讲解
- 依赖组件模块
- 请求接口地址
- 页面组件
- 后端api域名
- 前后端联调注意事项
- 前端详情启动端口不要冲突了
- 后端接口协议要和课程保持一致,不然前端代码识别不了
- 失败情况:前端 http , 后端https
- 失败情况:前端 https, 后端http
- 成功:前端http、后端http
- 成功:前端https、后端https
- 后端API地址记得修改(课程的后端api地址不一定可用,会改动,可以用自己的)
第3集 前后端联调-前端不能识别雪花算法id解决方案
简介: 前后端联调-前端不能识别雪花算法id解决
- 问题
- 雪花算法生成的id作为主键时,因为其长度为19位
- 而前端JS一般能处理16位,如果不处理的话在前端会造成精度丢失,最后两位会变成00
-
后端 解决方式
- 直接把id类型改为String就行,使用JackSon包的注解
- 对应的实体类主键属性加入注解@JsonSerialize
@JsonSerialize(using = ToStringSerializer.class) @TableId private Long id;
前端 解决方式
- 前端使用 json-bigint 模块进行处理,一般都是用axios数据请求
npm install json-bigint
#代码封装
axios.defaults.transformResponse = [
function (data) {
const json = JSONBIG({
storeAsString: true
})
const res = json.parse(data)
return res
}
]
或
axios.defaults.transformResponse = [
function (data) {
const json = JsonBigint({
storeAsString:true
})
const res = json.parse(data)
return res
}
]
axios.create({
baseURL: 'http://*******.com',
timeout: 5000,
timeoutErrorMessage: '请求时间过长,请联系后端或者优化请求',
})
第4集 短链平台-前端Vue3打包发布阿里云OSS实战
简介: 短链平台-前端Vue3打包发布阿里云OSS实战
-
前端发布有多个方式
- Nginx文件服务器
- 云厂商文件服务器+CDN:阿里云OSS
-
Vue项目发布上线
-
打包编译
npm run build
-
上传阿里云OSS
-
配置域名和默认静态页面
-
第5集 订单服务-主动关单查询支付平台业务处理
简介: 订单服务-主动关单查询支付平台业务处理
- 需求
- 用户下单支付成功了,但是回调通知失败了,导致本地订单数据库未支付成功
- 延迟队列订单超时,取消订单前需要向第三方平台查询订单是否支付完成
-
- 接收微信推送的支付成功消息后,我们需要做啥?
- 更新订单状态
- 调用账号服务发放流包
- 接收微信推送的支付成功消息后,我们需要做啥?
- 编码实战
/**
* //延迟消息的时间 需要比订单过期 时间长一点,这样就不存在查询的时候,用户还能支付成功
* <p>
* //查询订单是否存在,如果已经支付则正常结束
* //如果订单未支付,主动调用第三方支付平台查询订单状态
* //确认未支付,本地取消订单
* //如果第三方平台已经支付,主动的把订单状态改成已支付,造成该原因的情况可能是支付通道回调有问题,然后触发支付后的动作,如何触发?RPC还是?
*
* @param eventMessage
*/
@Override
public boolean closeProductOrder(EventMessage eventMessage) {
String outTradeNo = eventMessage.getBizId();
Long accountNo = eventMessage.getAccountNo();
ProductOrderDO productOrderDO = productOrderManager.findByOutTradeNoAndAccountNo(outTradeNo, accountNo);
if (productOrderDO == null) {
//订单不存在
log.warn("订单不存在");
return true;
}
if (productOrderDO.getState().equalsIgnoreCase(ProductOrderStateEnum.PAY.name())) {
//已经支付
log.info("直接确认消息,订单已经支付:{}", eventMessage);
return true;
}
//未支付,需要向第三方支付平台查询状态
if (productOrderDO.getState().equalsIgnoreCase(ProductOrderStateEnum.NEW.name())) {
//向第三方查询状态
PayInfoVO payInfoVO = new PayInfoVO();
payInfoVO.setPayType(productOrderDO.getPayType());
payInfoVO.setOutTradeNo(outTradeNo);
payInfoVO.setAccountNo(accountNo);
// 需要向第三方支付平台查询状态
String payResult = payFactory.queryPayStatus(payInfoVO);
if (StringUtils.isBlank(payResult)) {
//如果为空,则未支付成功,本地取消订单
productOrderManager.updateOrderPayState(outTradeNo, accountNo, ProductOrderStateEnum.CANCEL.name(), ProductOrderStateEnum.NEW.name());
log.info("未支付成功,本地取消订单:{}", eventMessage);
} else {
//支付成功,主动把订单状态更新成支付
log.warn("支付成功,但是微信回调通知失败,需要排查问题:{}", eventMessage);
productOrderManager.updateOrderPayState(outTradeNo, accountNo, ProductOrderStateEnum.PAY.name(), ProductOrderStateEnum.NEW.name());
//触发支付成功后的逻辑,
Map<String, Object> content = new HashMap<>(4);
content.put("outTradeNo", outTradeNo);
content.put("buyNum", productOrderDO.getBuyNum());
content.put("accountNo", accountNo);
content.put("product", productOrderDO.getProductSnapshot());
//构建消息
EventMessage payEventMessage = EventMessage.builder()
.bizId(outTradeNo)
.accountNo(accountNo)
.messageId(outTradeNo)
.content(JsonUtil.obj2Json(content))
.eventMessageType(EventMessageType.PRODUCT_ORDER_PAY.name())
.build();
//如果key不存在,则设置成功,返回true
Boolean flag = redisTemplate.opsForValue().setIfAbsent(outTradeNo, "OK", 3, TimeUnit.DAYS);
if (flag) {
rabbitTemplate.convertAndSend(rabbitMQConfig.getOrderEventExchange(),
rabbitMQConfig.getOrderUpdateTrafficRoutingKey(), payEventMessage);
return false;
}
}
}
return true;
}
第6集 缝缝补补-短链平台Bug修复
简介: 缝缝补补-短链平台Bug修复
- 修复一
- 分页某个分组的查找短链,未加排序
- 修复二
- 数据可视化服务-升序
- 修复三
- Flink服务TimeUtil时间格式化
修复四
- 修复Agent空判断
修复五
- 查看我的订单-降序