高并发场景下中间件实践总结

1 中间件实践总结

为什么要性能调优?

1)高并发场景下会遇到各种问题

2)充分利用服务器资源,降低成本

1.1 tomcat调优

Tomcat 的关键指标有吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存。前三个指标是我们最关心的业务指标,Tomcat 作为服务器,就是要能够又快有好地处理请求,因此吞吐量要大、响应时间要短,并且错误数要少。后面三个指标是跟系统资源有关的,当某个资源出现瓶颈就会影响前面的业务指标,比如线程池中的线程数量不足会影响吞吐量和响应时间;但是线程数太多会耗费大量 CPU,也会影响吞吐量;当内存不足时会触发频繁地 GC,耗费 CPU,最后也会反映到业务指标上来

主要参数

maxThreads: 最大线程数,默认设置 200,一般建议在 500 800,根据硬件设施和业务来判断

minSpareThreads: 核心线程数,默认设置 25

maxQueueSize: 最大的等待队列数,超过则拒绝请求 ,默认 Integer.MAX_VALUE,  我们也不一定要求请求一定等待,可以设置最大等待队列大小,如果超过就不等待了。这样虽然有些请 求是失败的,但是请求时间会变短

maxIdleTime: 线程空闲时间,超过该时间,线程会被销毁,单位毫秒, 默认值600001分钟)

1.2 jvm调优

G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式,一般而言JVM调优对于机器性能提升有限,效果远不如优化代码收益高。

-Xms:堆内存的初始大小,默认为物理内存的1/64,推荐设置为容器内存的50%,不能超过容器内存的80%

-Xmx:最大堆内存大小,通常是物理内存1/2

-Xmn:年轻代大小,full gc频繁,调整此参数,通常设置为堆内存1/2

-XX:+UseG1GC:使用G1收集器,推荐6G以上内存

-XX:MaxGCPauseMillis:目标暂停时间(默认200ms), 通常设置100-300ms比较合适

-XX:MetaspaceSize: 堆外内存,用来保存类、方法、数据结构等运行时信息和元信息的,分配给类元数据空间(以字节计)的初始大小。如果程序动态的创建了很多类,或出现过

    java.lang.OutOfMemoryError:Metaspace 默认为20.8M, 推荐256M

-XX:MaxMetaspaceSize: 分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小, 推荐256M

-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

-XX:G1MaxNewSizePercent:新生代内存最大空间(默认整堆60%)

-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合

收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能

就要触发MixedGC了

线上分析工具

线程堆栈:Universal JVM GC analyzer - Java Garbage collection log analysis made easy  Smart Java thread dump analyzer - thread dump analysis in seconds  arthas

堆内存分析:MAT(Memory Analyzer Tool) 

1.3 mysql

可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。

1、无索引行锁会升级为表锁,锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁

     update account set balance = 800 where name = 'lilei';

     其他对该表任一行操作都会阻塞住

     解决方法:加索引或者锁定某一行用for update(排它锁),select * from test_innodb_lock where name = 'lilei' for update; 这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交

2、间隙锁,在某些情况下可以解决幻读问题。

     例如:id: 1 2 3 10 20, 间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,update account set name = 'zs' where id > 8 and id <18; 则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。

     解决方法:通过id更新

锁优化建议:尽可能减少检索条件范围,避免间隙锁,尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行。

3、分布式事务

   1)保留主要数据:当调用只有一个重要接口时,可调整程序顺序,保留主要数据。

   2)阻塞式重试:当请求一个微服务的 API 失败后,发起三次重试。如果三次还是失败,就打印日志,继续执行或向上层抛出错误,接口幂等、调用失败做定时任务补偿,适用于业务对一致性要求不敏感的场景, 这种场景最多

   3)异步队列:在当前服务将数据写入 DB 后,推送一条消息给 MQ,由独立的服务去消费 MQ 处理业务逻辑

   4)TCC 补偿事务:seata,Himly

   5)本地消息表:具体做法是在本地事务中插入业务数据时,也插入一条消息数据。然后在做后续操作,如果其他操作成功,则删除该消息;如果失败则不删除,异步监听这个消息,不断重试。

  分布式事务的 6 种解决方案,写得非常好!-腾讯云开发者社区-腾讯云

1.4 redis

缓存穿透与缓存击穿

public String getClickId(ClickIdDTO clickIdDTO) {
    final Integer channelCode = clickIdDTO.getChannelCode();
    final String requestRefer = clickIdDTO.getRequestRefer();
 
    //是否在布隆过滤器存在
    if (!redisBloomFilter.exist(PutinBloomFilterEnum.BACK_REPORT_CHANNEL, channelCode)) {
        log.info("channel not exist, channel:{}", channelCode);
        return null;
    }
 
    //查询缓存
    PlatformCallbackHandlerEntity platformCallbackHandlerEntity = cacheService.get(PutinCacheEnum.BACK_REPORT_CHANNEL, channelCode.toString(), PlatformCallbackHandlerEntity.class);
    if (platformCallbackHandlerEntity != null) {
        return getClickIdByRefer(platformCallbackHandlerEntity, requestRefer);
    }
 
    return LockSimpleTemplate.executeWithTryLock(RedisLockKeyBackEnum.REPORT_CHANNEL_LOCK,
            () -> {
                //查询缓存
                PlatformCallbackHandlerEntity handlerEntity = cacheService.get(PutinCacheEnum.BACK_REPORT_CHANNEL, channelCode.toString(), PlatformCallbackHandlerEntity.class);
                if (handlerEntity != null) {
                    return getClickIdByRefer(handlerEntity, requestRefer);
                }
 
                //查询数据库
                PlatformCallbackHandlerEntity strategy = platformCallbackHandlerService.getByReportChannel(channelCode);
                if (strategy != null) {
                    return getClickIdByRefer(strategy, requestRefer);
                }
                return null;
            }, channelCode);
}
 
 
@Component
@Slf4j
public class RedisBloomFilter {
 
    @Autowired(required = false)
    private RedissonClient redissonClient;
 
    private static final ConcurrentHashMap<String, RBloomFilter<Object>> filterMap = new ConcurrentHashMap<>();
 
    public <T> void add(PutinBloomFilterEnum bloomFilterEnum, T value) {
        getRBloomFilter(bloomFilterEnum).add(value);
    }
 
    public <T> void add(PutinBloomFilterEnum bloomFilterEnum, T... values) {
        RBloomFilter<T> rBloomFilter = getRBloomFilter(bloomFilterEnum);
        for (T v : values) {
            rBloomFilter.add(v);
        }
    }
 
    public <T> boolean exist(PutinBloomFilterEnum bloomFilterEnum, T value) {
        try {
            return getRBloomFilter(bloomFilterEnum).contains(value);
        } catch (Exception e) {
            log.error("error", e);
        }
        return true;
    }
 
    /**
     * 只要有一个包含,就算包含
     * @param bloomFilterEnum
     * @param values
     * @return
     */
    public <T> boolean existAnyOne(PutinBloomFilterEnum bloomFilterEnum, T... values) {
        try {
            RBloomFilter<T> rBloomFilter = getRBloomFilter(bloomFilterEnum);
            for (T v : values) {
                if (rBloomFilter.contains(v)) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            log.error("error", e);
        }
        return false;
    }
 
    /**
     * 统计存在数量
     *
     * @param bloomFilterEnum
     * @return
     */
    public long countNum(PutinBloomFilterEnum bloomFilterEnum) {
        return getRBloomFilter(bloomFilterEnum).count();
    }
 
    private <T> RBloomFilter<T> getRBloomFilter(PutinBloomFilterEnum bloomFilterEnum) {
        String boomFilterKey = getBoolFilterKey(bloomFilterEnum.getCode());
        if(filterMap.containsKey(boomFilterKey)) {
            return (RBloomFilter<T>) filterMap.get(boomFilterKey);
        }
        RBloomFilter<Object> rBloomFilter = redissonClient.getBloomFilter(boomFilterKey);
        PutinBloomFilterEnum putinBloomFilterEnum = PutinBloomFilterEnum.getEnum(bloomFilterEnum.getCode());
        if(putinBloomFilterEnum != null) {
            // 预计统计数量设置比实际要大一些, 降低误判率, 误差率越小,精度越高
            rBloomFilter.tryInit(putinBloomFilterEnum.getExpectedInsertions(), putinBloomFilterEnum.getFalseProbability());
        }
        filterMap.put(boomFilterKey, rBloomFilter);
        return (RBloomFilter<T>) rBloomFilter;
    }
 
    private String getBoolFilterKey(String key) {
        return EwpThreadLocal.getPartnerCode() + ":" + key;
    }
}

1.5 xxl-job

数据逻辑分片、物理分片、高性能处理

/**
 * 服务参数
 */
@Data
public class TaskServerParam implements Serializable {
    private static final long serialVersionUID = -8075352114642218846L;
 
    /**
     * 获取数据条数
     */
    private int fetchCount = 200;
    /**
     * 单次执行多少条
     */
    private int executeCount = 20;
    /**
     * 线程数
     */
    private int threadCount = 10;
 
 
    private Integer partnerCode;
 
    /**
     * 业务数据
     */
    private String bizContent;
}
 
 
 
@Slf4j
@Component
public class CommonTestJob extends AbstractScheduleTaskProcess<OrderJobDTO> {
    @EwpMethodJob("common_testJob")
    public void commonTestJob() {
        try {
            execute();
        } catch (Exception exception) {
            log.error("#commonTestJob error:", exception);
        }
    }
 
    @Override
    protected List<OrderJobDTO> selectTasks(TaskServerParam taskServerParam, int curServer) {
        // TODO: 2023-04-02 获取任务数据
        // 获取数据条数 taskServerParam.getFetchCount();
        // 1)物理分片:
        //    where run_status = #{runStatus}
        //          and run_time < 6
        //          and next_run_time < now()
        //          and mod(id, #{shardTotal}) = #{shardIndex}
        //          limit #{limit}
        // 2)逻辑分片:
        //   数据所有数据:id % shardTotal == shardIndex;
        // 3) 高性能场景:
        //   任务表存储计算好 shardIndex, 直接获取
        return new ArrayList<>();
    }
 
    @Override
    protected void executeTasks(List<OrderJobDTO> tasks) {
        // TODO: 2023-04-02 任务处理
    }
}
 
 
/**
 * 任务抽象类
 * @param <T>
 */
@Slf4j
public abstract class AbstractScheduleTaskProcess<T> {
 
    /**
     * 任务线程
     */
    private ThreadPoolExecutor executor;
 
 
    private volatile int lastThreadCount = 0;
 
    /**
     * 执行任务
     * 获取业务条数,然后根据每次执行条数设置线程数量
     * @return
     */
    protected void execute() {
        String jobParam = EwpJobUtils.getJobParam();
        if(StringUtils.isEmpty(jobParam)) {
            throw new EwpException("job参数信息为空!");
        }
        TaskServerParam serverParam = JSON.parseObject(jobParam, TaskServerParam.class);
        EwpThreadLocal.setPartnerCode(serverParam.getPartnerCode());
        PutInTraceIdHelper.initTraceId();
 
        int curServer = EwpJobUtils.getShardIndex();
        if (log.isInfoEnabled()) {
            log.info("开始执行任务[" + this.getClass().getName() + "][" + curServer + "]....");
        }
 
        //获取任务
        List<T> tasks = this.selectTasks(serverParam, curServer);
        if (log.isInfoEnabled()) {
            log.info("获取任务[" + this.getClass().getName() + "]共" + (tasks == null ? 0 : tasks.size()) + "条");
        }
 
        //执行任务
        if (!CollectionUtils.isEmpty(tasks)) {
            this.executeTasksInner(serverParam, tasks);
        }
    }
 
    private void executeTasksInner(TaskServerParam param, List<T> tasks) {
        int threadCount = param.getThreadCount();
        synchronized(this) {
            if (this.executor == null) {
                this.executor = (ThreadPoolExecutor) CoreUtil.createCustomExecutorService(threadCount, "dts.executeTasks");
                this.lastThreadCount = threadCount;
            } else if (threadCount > this.lastThreadCount) {
                this.executor.setMaximumPoolSize(threadCount);
                this.executor.setCorePoolSize(threadCount);
                this.lastThreadCount = threadCount;
            } else if (threadCount < this.lastThreadCount) {
                this.executor.setCorePoolSize(threadCount);
                this.executor.setMaximumPoolSize(threadCount);
                this.lastThreadCount = threadCount;
            }
        }
 
        //数据分隔
        List<List<T>> lists = CoreUtil.splitList(tasks, param.getExecuteCount());
        final CountDownLatch latch = new CountDownLatch(lists.size());
        Iterator iterator = lists.iterator();
        while(iterator.hasNext()) {
            final List<T> list = (List)iterator.next();
            this.executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        AbstractScheduleTaskProcess.this.executeTasks(list);
                    } catch (Exception exception) {
                        throw exception;
                    } finally {
                        latch.countDown();
                    }
                }
            });
        }
 
        try {
            latch.await();
        } catch (InterruptedException exception) {
            throw new RuntimeException("interrupted when processing data access request in concurrency", exception);
        }
    }
 
    protected abstract List<T> selectTasks(TaskServerParam taskServerParam, int curServer);
 
    protected abstract void executeTasks(List<T> tasks);
}

1.6 dubbo

provider端:

     threads:业务线程池, 默认:200, 可是适当调大

     iothreads:io线程池大小,CPU+1

     queues: 线程池队列大小,当线程池满时,排队等待执行的队列大小,建议不要设置,当线程程池时应立即失败,重试其它服务提供机器,而不是排队,除非有特殊需求

consumer端:

    timeout: 超时时间

    retries: 重试次数

接口总结:

1、接口幂等性设计,比如:消费方加业务bizId,服务提供方根据bizId去重
2、能缓存的数据要缓存
3、接口入参个数不能超过3个,入参,出参必须实现Serializable接口,入参出参中禁止出现Object对象,必须是其子类,入参出参字段不能使用枚举值,出参不建议使用Map或者JSON这种对象
4、批量查询接口必须要分页,每页不超过100条,能调批量的接口不要调单个的
5、对外接口需要配置限流策略
6、sdk的pom文件,不能引用第三方包,若引用必须使用provided或者optional属性标记
7、接口必须添加注释,清晰描述接口的功能和参数的含义,注释包含字段是否必传
8、当方法入参大于1个且有基本类型(如String,Long)时,封装成对象进行传递
9、一般不要重试,写数类的接口禁止重试
10、在sdk外层目录添加代码升级的changelog
11、对外接口含错误码的,禁止错误码不区别业务,统一返回500系统异常的吞异常行为
12、所有对外提供接口,必须返回包装对象,并且约定错误码,不建议抛出异常
13、对外接口需要添加权限校验

业务需求:李运华5W1H8C1D

异步调用:提高并发能力,如一个接口中调用多个接口
服务降级:比如在电商大促期间,不重要接口降级
本地存根:比如缓存返回结果数据

1.7 mq

1、消息幂等:业务主键+版本号或者处理状态去重、唯一索引

2、消息积压:

浅谈如何解决RocketMQ消息堆积的问题_rocketmq消息堆积解决方案_梦之救赎的博客-CSDN博客

1.8 elasticsearch

     当数据写入到ES分片时,会首先写入到内存中,然后通过内存的buffer生成一个segment,并刷到文件系统缓存中(os cache),数据可以被检索(注意不是直接刷到磁盘)ES中默认1秒,refresh一次

默认每隔30分钟会将文件系统缓存的数据刷入到磁盘

1、setting示例

{

  "index": {

    "lifecycle": {

      "name""policy_wx_chat_msg",

      "rollover_alias""alias_wx_chat_msg_w"

    },

    "refresh_interval""10s",

    "number_of_shards""5",

    "sort": {

      "field": [

        "msgTime",

        "msgSeq"

      ],

      "order": [

        "desc",

        "desc"

      ]

    },

    "store": {

      "type""niofs"

    },

    "analysis": {

      "analyzer": {

        "default": {

          "type""ik_max_word"

        }

      }

    },

    "number_of_replicas""1"

  }

}

写数据:计算路由到指定index,

读数据:根据别名读取

数据删除:指定rollup任务或者手写定时任务删除

2、MySQL 数据同步到 ES 中,大致总结可以分为两种方案

1:监听 MySQL 的 Binlog,分析 Binlog 将数据同步到 ES 集群中,延迟性高

2:直接通过 ES API 将数据写入到 ES 集群中,延迟性低,每次业务操作只更新一次 ES,如果发生错误或者异常,在数据库中插入一条补救任务,有 Worker 任务会实时地扫这些数据,以数据库订单数据为基准来再次更新 ES 数据,实时性要求高的查询走 DB

3、优化

ES 性能调优,这可能是全网最详细的 Elasticsearch 性能调优指南_es调优_Elastic开源社区的博客-CSDN博客

https://www.cnblogs.com/jajian/p/10465519.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值