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

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

概述


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


事情经过和原因分析


image.png

银行现场的实施同事反馈,平台页面打开都要超过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容器性能的重要参数如下:

1671160680734.jpg

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<T> {
    /**
     * 根据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<T> implements LocalCache<T>, 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<String, T> loadFunction;
    protected boolean cacheTaskSwitch;
    /**
     * 定义缓存对象
     */
    private LoadingCache<String, T> guavaCache;
    public AbstractGuavaCache() {}
    @Override
    public void afterPropertiesSet() {
        // 初始化guavaCache对象
        this.guavaCache = CacheBuilder.newBuilder().expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
                .maximumSize(maximumSize).build(new CacheLoader<String, T>() {
                    @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<SysOrg> 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<SysOrg> 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<SysOrg> findOrgsByIds(Collection<String> orgIds) {
        if(CollUtil.isEmpty(orgIds)) {
            return Lists.newArrayListWithExpectedSize(16);
        }
        List<SysOrg> orgs = orgIds.stream().map(orgId -> this.get(orgId)).filter(Objects::nonNull).collect(Collectors.toList());
        return orgs;
    }
}

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


总结


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

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

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

目录
相关文章
|
3月前
|
人工智能 监控 安全
百万并发,API 网关抗住了亚运会流量高峰
本文主要介绍作为亚运会所有核心流量的入口,阿里云推出了一款百万并发规格的 API 网关,抗住了亚运会流量高峰,为亚运会提供强大的技术支持。
|
7月前
|
消息中间件 负载均衡 Java
5分钟轻松打造应对流量洪峰的稳定商城交易系统
本实验通过SAE极速部署一个微服务电商商城,同时结合RocketMQ异步解耦、削峰填谷的能力,带大家体验面对流量洪峰仍旧稳定可靠的商城交易系统!
228 1
|
7月前
|
消息中间件 负载均衡 Serverless
「5分钟打造应对流量洪峰的商城交易系统」清理及后续
【重要】体验完成后,如果您无需使用云消息队列RocketMQ 版、SAE和SLB,请按照如下操作及时清理和释放资源。
271 0
|
7月前
|
运维 测试技术
千万级乘客排队系统重构&压测方案——总结篇
千万级乘客排队系统重构&压测方案——总结篇
122 1
|
8月前
|
消息中间件 运维 应用服务中间件
5分钟轻松打造应对流量洪峰的稳定商城交易系统实验场景
本实验通过SAE极速部署一个微服务电商商城,同时结合RocketMQ异步解耦、削峰填谷的能力,带大家体验面对流量洪峰仍旧稳定可靠的商城交易系统!
313 0
|
9月前
|
存储 开发框架 负载均衡
限流的非常规用途 - 缓解抢购压力
限流的非常规用途 - 缓解抢购压力
71 0
|
数据采集 数据可视化 物联网
压力式静力水准仪上报阿里云
为大家介绍压力式静力水准仪的采集。使用海创微联采集控制系统对液位高、偏移量和温度进行采集,然后将采集到的数据上传到阿里云物联网平台将数据实时可视化。 文章分为3部分: ● 设备介绍 压力式静力水准仪的介绍。 ● 数据采集 海创微联采集控制系统控件的部署,调试。 ● 数据上传 阿里云物联网云平台的使用,产品和设备的创建。
142 0
|
SQL 网络协议 Linux
流量分析
wireshark简单流量分析
流量分析
|
存储 算法 Java
5种限流算法,7种限流方式,挡住突发流量?(二)
5种限流算法,7种限流方式,挡住突发流量?
237 0
5种限流算法,7种限流方式,挡住突发流量?(二)
|
缓存 负载均衡 算法
5种限流算法,7种限流方式,挡住突发流量?(一)
5种限流算法,7种限流方式,挡住突发流量?
445 0
5种限流算法,7种限流方式,挡住突发流量?(一)