返回

线上接口流量突增,扛不住了

发布时间:2022-10-27 07:30:26 320
# spring# 数据库# sql# 数据# 信息

概述

本篇文章主要记录下公司的系统部署到银行内部内uat环境后,流量突增后,导致整个系统所有请求耗时十分之久,最后崩溃,无奈连夜赶往现场,解决处理。

事情经过和原因分析

线上接口流量突增,扛不住了_缓存

银行现场的实施同事反馈,平台页面打开都要超过5分钟,任何接口都十分卡顿。我一听就不对劲了,怎么可能呢?

到现场才发现,现场只部署了了一个节点,在UAT阶段流量接入以后,平台直接就扛不住了,其中有个接口高峰期的qps是1000,但是通过浏览器发现这个接口的平均相应时间是15s,而tomcat本身默认的线程数是200个,所以肯定导致其他所有的请求阻塞了,因为没有连接资源了,造成服务的不可用。

所以除了扩充节点解决问题以外,还需要提高单节点接口的性能。

优化手段

排查连接池大小

其实大部分的请求都是会访问数据库,而数据库严重依赖连接池数量,如果一个项目连接池数量设置过小,那势必会导致性能下降。

Hikarip连接池配置说明如下:

#最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
spring.datasource.hikari.minimum-idle=5
#最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值
spring.datasource.hikari.maximum-pool-size=100
#自动提交从池中返回的连接,默认值为true
spring.datasource.hikari.auto-commit=true
#空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
#只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
spring.datasource.hikari.idle-timeout=30000
#连接池名称,默认HikariPool-1
spring.datasource.hikari.pool-name=Hikari
#连接最大存活时间.不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短;单位ms
spring.datasource.hikari.max-lifetime=55000
#连接超时时间:毫秒,小于250毫秒,会被重置为默认值30秒

后来发现项目中设置的最大连接数​maximum-pool-size​是50,有点小,后面我改成了500。

tomcat性能优化

我们项目采用的spingboot项目,内置的tomcat容器,影响tomcat容器性能的重要参数如下:

线上接口流量突增,扛不住了_重置_02

maxThreads

我们知道 maxThreads 指的是请求处理线程的最大数量,在 Tomcat7 和 Tomcat8 中都是默认 200 个。

对于这个参数的设置,需要根据任务的执行内容去调整,一般来说计算公式为:最大线程数 = ((IO时间 + CPU时间)/CPU时间) * CPU 核数。这个公式的思路其实很简单,就是最大化利用 CPU 的资源。一个任务的耗时分为 IO 耗时和 CPU 耗时,基本上 IO 耗时是最多的,这时候 CPU 是没事干的。

maxConnections

maxConnections 指的是当线程池的线程达到最大值,并且都在忙的时候,Connector 中的队列最多能容纳多少个连接。一般来说,我们都要设置一个合理的数值,不能让其无限制堆积。因为 Tomcat 的处理能力肯定是有限的,到达一定程度肯定就处理不过来了,因此你堆积太多了也没啥用,反而会造成内存堆积,最终导致内存溢出 OOM 的发生。一般来说,一个经验值是可以设置成为 maxThreads 同样的大小。

acceptCount

acceptCount 指的是当 Container 线程池达到最大数量且没有空闲线程,同时 Connector 队列达到最大数量时,操作系统最多能接受的连接数。 当队列中的个数达到最大值后,进来的请求一律被拒绝,默认值是 100。这可以理解成是操作系统的一种自我保护机制吧,堆积太多无法处理,那就直接拒绝掉,保护自身资源。

在项目中,由于时间有限,我没有用jemeter等进行性能压测,直接预估了下, 设置了maxThreads、maxConnections、acceptCount都为800,如果时间允许的情况下,建议还是通过jemter压测出一个以最优值。

接口代码优化

完成上面的系统级别的优化后,就要针对具体的代码进行分析优化了,首先推荐一个神器arthas, 可以查看接口中的方法耗时情况,执行trace命令,可以看到如下例图:

优化方法无非是如下几种情况:

  1. 尽量避免for循环中查询数据库或者访问外部接口
  2. sql调优
  3. 缓存
  4. ....

排查了项目的情况,发现是在调用远程服务时,返回延迟比较大,考虑到该远程服务变化可能很小,于是做了一个本地缓存处理,同时定时同步处理,大致代码如下:

  1. 缓存接口定义
public interface LocalCache {

/**
* 根据key获取缓存信息
* @param key 缓存key
* @return 缓存对象
*/
T get(String key);

/**
* 保存缓存信息, 存在了会覆盖
* @param key 缓存key
* @param cacheItem 缓存对象
*/
void save(String key, T cacheItem);

/**
* 根据缓存key删除缓存信息
*
* @param key 缓存对象
*/
void delete(String key);
}
  1. 定义抽象父类
public abstract class AbstractGuavaCache implements LocalCache, InitializingBean {

private static final ScheduledExecutorService SCHEDULED_CACHE =
new ScheduledThreadPoolExecutor(2, new ThreadFactoryBuilder().setNameFormat("guava cache-%d").build());

protected long expireSeconds;

protected long maximumSize;

protected long initDelay;

protected long delay;

protected Function loadFunction;

protected boolean cacheTaskSwitch;

/**
* 定义缓存对象
*/
private LoadingCache guavaCache;

public AbstractGuavaCache(){}

@Override
public void afterPropertiesSet(){
// 初始化guavaCache对象
this.guavaCache = CacheBuilder.newBuilder().expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.maximumSize(maximumSize).build(new CacheLoader() {
@Override
public T load(String key){
return loadFunction != null ? loadFunction.apply(key) : null;
}
});
// 定时任务
if(cacheTaskSwitch) {
SCHEDULED_CACHE.scheduleWithFixedDelay(() -> {
try {
this.reloadAllToCache();
} catch (Exception e) {
log.error("cache error", e);
}
}, initDelay, delay, TimeUnit.SECONDS);
}
}

@SneakyThrows
@Override
public T get(String key){
return guavaCache.get(key);
}

@Override
public void save(String key, T cacheItem){
guavaCache.put(key, cacheItem);
}

@Override
public void delete(String key){
guavaCache.invalidate(key);
}

/**
* 更新缓存操作
*/
protected void reloadAllToCache(){

}

}
  1. 定义具体实现
@Component("orgCache")
@Slf4j
public class SysOrgCacheManager extends AbstractGuavaCache implements OrgCacheService {

public SysOrgCacheManager(EventCacheProperties eventCacheProperties){
this.expireSeconds = eventCacheProperties.getExpireSeconds();
this.maximumSize = eventCacheProperties.getMaximumSize();
this.cacheTaskSwitch = eventCacheProperties.isTaskEnabled();
this.initDelay = eventCacheProperties.getInitDelay();
this.delay = eventCacheProperties.getDelay();
this.loadFunction = key -> loadByKey(key);
}

private SysOrg loadByKey(String orgId){
log.warn("缓存未命中,直接查询数据库, orgId: [{}]", orgId);
SysOrg org = OrgApi.getOrgById(orgId);
SysOrg cacheOrg = transformCachedOrg(org);
log.warn("缓存未命中,加载后的数据, data: [{}]", JSON.toJSONString(cacheOrg));
return cacheOrg;
}

private SysOrg transformCachedOrg(SysOrg org){
if(org == null || StrUtil.isEmpty(org.getId())) {
return null;
}
return new SysOrg().setId(org.getId()).setName(org.getName()).setFullPath(org.getFullPath());
}

@Override
protected void reloadAllToCache(){
log.info("-------cache org 【缓存机构】 开始 -------");
TimeInterval timeInterval = new TimeInterval();
// 查询全量的机构数据
List remoteOrgs = OrgApi.selectAll();
remoteOrgs.forEach(org -> {
SysOrg cacheOrg = this.transformCachedOrg(org);
this.save(org.getId(), cacheOrg);
});
log.info("-------cache org【缓存机构】 结束, cost: [{}] -------", timeInterval.intervalSecond());
}

@Override
public List findOrgsByIds(Collection orgIds){
if(CollUtil.isEmpty(orgIds)) {
return Lists.newArrayListWithExpectedSize(16);
}

List orgs = orgIds.stream().map(orgId -> this.get(orgId)).filter(Objects::nonNull).collect(Collectors.toList());
return orgs;
}
}

这里是通过guava cache实现的,通过配置可以修改缓存全量刷新的时间、缓存的失效时间、缓存最多存储的数据量。

总结

后面复盘分析了下,导致该问题主要的原因如下:

  1. 公司管理松散混乱,没有流程,包括本次部署架构和方案都没有,实施水平参差不齐,员工流动性大,招进来就用,完全没有什么培训机制。
  2. 公司标准研发流程存在问题,蒙眼狂奔,一大堆新需求,砍工时,完全没有排非功能性测试、性能测试的时间。
  3. 开发人员也要不断提高自己

目前虽然基本达到客户需求,但是感觉还是有很多不足,大家有没有一些其他的优化思路和方案呢?

特别声明:以上内容(图片及文字)均为互联网收集或者用户上传发布,本站仅提供信息存储服务!如有侵权或有涉及法律问题请联系我们。
举报
评论区(0)
按点赞数排序
用户头像
精选文章
thumb 中国研究员首次曝光美国国安局顶级后门—“方程式组织”
thumb 俄乌线上战争,网络攻击弥漫着数字硝烟
thumb 从网络安全角度了解俄罗斯入侵乌克兰的相关事件时间线
下一篇
SpringCloud 之初识 GateWay 2022-10-27 07:14:23