Interface performance optimization in practice: from 20s to 500ms, three tricks

foreword

The interface performance problem is an unavoidable topic for students engaged in back-end development. To optimize the performance of an interface, you need to start from many aspects.

This article will continue the topic of interface performance optimization, and talk about how I optimize a slow query interface from a practical point of view.

Last week, I optimized the online batch scoring query interface, optimizing the interface performance from the initial 20s to the current 500ms.

Generally speaking, it can be done with three tricks.

What happened?

Crime scene

Before going to work every morning, we will receive an online slow query interface summary email, which will display information such as the interface address, number of calls, maximum time consumption, average time consumption, and traceId .

I saw that there is a batch scoring query interface, the maximum time consumption reaches 20s, and the average time consumption is also 2s.

Use skywalking to view the call information of this interface, and find that in most cases, the interface responds relatively quickly. In most cases, the response can be returned within about 500ms, but there are also a small number of requests that exceed 20s.

This phenomenon is very strange.

Could it be related to data?

For example: it is very fast to check the data of a certain organization. But if you want to check the platform, that is, the root node of the organization, in this case, the amount of data to be queried is very large, and the interface response may be very slow.

But it turns out that's not the reason.

Soon a colleague gave the answer.

They requested this interface in batches on the settlement list page, but the amount of data passed by him was very large.

What's going on?

The demand mentioned at the beginning is that this interface is called for the paginated list page. The size of each page is: 10, 20, 30, 50, 100, and the user can choose.

In other words, by calling the batch evaluation query interface, a maximum of 100 records can be queried at one time.

But the reality is: the bill list page also contains a lot of orders. Basically, each settlement has multiple orders. When calling the batch evaluation query interface, it is necessary to combine the data of the settlement sheet and the order.

The result of this is: when calling the batch evaluation query interface, a lot of parameters are passed in at one time, and the input parameter list may contain hundreds or even thousands of pieces of data.

status quo

If hundreds or thousands of ids are passed in at one time, it is okay to query data in batches, and the primary key index can be used, and the query efficiency is not too bad.

But the logic of the batch scoring query interface is not simple.

The pseudocode is as follows:

public List<ScoreEntity> query(List<SearchEntity> list) {
   
       //结果    List<ScoreEntity> result = Lists.newArrayList();    //获取组织id    List<Long> orgIds = list.stream().map(SearchEntity::getOrgId).collect(Collectors.toList());    //通过regin调用远程接口获取组织信息    List<OrgEntity> orgList = feginClient.getOrgByIds(orgIds);        for(SearchEntity entity : list) {
   
           //通过组织id找组织code        String orgCode = findOrgCode(orgList, entity.getOrgId());            //通过组合条件查询评价        ScoreSearchEntity scoreSearchEntity = new ScoreSearchEntity();        scoreSearchEntity.setOrgCode(orgCode);        scoreSearchEntity.setCategoryId(entity.getCategoryId());        scoreSearchEntity.setBusinessId(entity.getBusinessId());        scoreSearchEntity.setBusinessType(entity.getBusinessType());        List<ScoreEntity> resultList = scoreMapper.queryScore(scoreSearchEntity);                if(CollectionUtils.isNotEmpty(resultList)) {
   
               ScoreEntity scoreEntity = resultList.get(0);            result.add(scoreEntity);        }    }    return result;}

In fact, in the real scene, the code is much more complicated than this, so here we simplify it for you to demonstrate.

There are two most critical points:

  • Another interface is called remotely in the interface

  • Need to query data in for loop

The first point, that is: another interface is called remotely in the interface, this code is necessary.

Because if an organization code field is redundant in the evaluation form, in case the organization code in the organization table is modified one day, we have to use some mechanism to notify us to modify the organization code of the evaluation form synchronously, otherwise there will be data inconsistencies question.

Obviously, if this adjustment is to be made, the business process will need to be changed, and the code changes will be a bit large.

So, let's keep the remote call in the interface first.

From this point of view, the place that can be optimized can only be: query data in the for loop.

first optimization

Since it needs to be in the for loop, each record needs to query the desired data according to different conditions.

Since the business system does not pass the id when calling this interface, it is not good to use id in (...) in the where condition to query data in batches.

In fact, there is a way to meet the requirements without looping the query: use or keyword splicing, for example:

(org_code='001' and category_id=123 and business_id=111 and business_type=1) or (org_code='002' and category_id=123 and business_id=112 and business_type=2) or (org_code='003' and category_id=124 and business_id=117 and business_type=1)...

In this way, the sql statement will be very long and the performance will be poor.

In fact, there is another way to write:

where (a,b) in ((1,2),(1,3)...)

However, this kind of sql, if the amount of data queried at one time is too much, the performance is not very good.

It can't be changed to batch query, so it can only optimize the execution efficiency of single query sql.

Start with the index first, because the modification cost is the lowest.

The first optimization is to optimize the index.

An ordinary index on the business_id field was established before the evaluation table, but the efficiency is not ideal from the current point of view.

Since I decisively added a joint index:

alter table user_score add index  `un_org_category_business` (`org_code`,`category_id`,`business_id`,`business_type`) USING BTREE;

The joint index consists of four fields: org_code, category_id, business_id and business_type.

After this optimization, the effect is immediate.

The maximum time-consuming of the batch evaluation query interface has been shortened from the initial 20s to about 5s.

Second optimization

Since it needs to be in the for loop, each record needs to query the desired data according to different conditions.

Only querying data in one thread is obviously too slow.

So, why can't it be changed to multi-threaded calls?

For the second optimization, the query database is changed from single thread to multi thread.

However, since this interface is to return all the data queried, it is necessary to obtain the query results.

Using multi-threaded calls and getting the return value, it is very suitable to use CompleteFuture in java8 in this scenario.

The code is adjusted to:

CompletableFuture[] futureArray = dataList.stream()     .map(data -> CompletableFuture          .supplyAsync(() -> query(data), asyncExecutor)          .whenComplete((result, th) -> {
   
          })).toArray(CompletableFuture[]::new);CompletableFuture.allOf(futureArray).join();

The essence of CompleteFuture is to create threads for execution. In order to avoid generating too many threads, it is necessary to use a thread pool.

It is recommended to use the ThreadPoolExecutor class first, we customize the thread pool.

The specific code is as follows:

ExecutorService threadPool = new ThreadPoolExecutor(    8, //corePoolSize线程池中核心线程数    10, //maximumPoolSize 线程池中最大线程数    60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收    TimeUnit.SECONDS,//时间单位    new ArrayBlockingQueue(500), //队列    new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略

You can also use the ThreadPoolTaskExecutor class to create a thread pool:

@Configurationpublic class ThreadPoolConfig {
   
   
    /**     * 核心线程数量,默认1     */    private int corePoolSize = 8;        /**     * 最大线程数量,默认Integer.MAX_VALUE;     */    private int maxPoolSize = 10;        /**     * 空闲线程存活时间     */    private int keepAliveSeconds = 60;        /**     * 线程阻塞队列容量,默认Integer.MAX_VALUE     */    private int queueCapacity = 1;        /**     * 是否允许核心线程超时     */    private boolean allowCoreThreadTimeOut = false;
    @Bean("asyncExecutor")    public Executor asyncExecutor() {
   
           ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();        executor.setCorePoolSize(corePoolSize);        executor.setMaxPoolSize(maxPoolSize);        executor.setQueueCapacity(queueCapacity);        executor.setKeepAliveSeconds(keepAliveSeconds);        executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);        // 设置拒绝策略,直接在execute方法的调用线程中运行被拒绝的任务        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());        // 执行初始化        executor.initialize();        return executor;    }}

After this optimization, the interface performance has also increased by 5 times.

From about 5s, shortened to about 1s.

But the overall effect is not ideal.

The third optimization

After the previous two optimizations, the performance of the batch query evaluation interface has been improved, but the time-consuming is still greater than 1s.

The root cause of this problem is: too much data is queried at one time.

So, why don't we limit the number of records per query?

The third optimization is to limit the number of records for one-time query. In fact, it has been restricted before, but the maximum is 2000 records, and the effect is not good from the current point of view.

The interface is limited to only 200 records at a time, and an error message will be reported if there are more than 200 records.

If the interface is directly restricted, it may cause abnormalities in the business system.

In order to avoid this from happening, it is necessary to discuss the optimization plan with the business system team.

There are two main options:

1. Front-end pagination

On the settlement list page, each settlement only displays one order by default, and there are redundant pagination queries.

In this case, if the calculation is based on the maximum of 100 records per page, the settlement statement and order can only query up to 200 records at a time.

This requires the front-end of the business system to implement paging functions, and at the same time, the back-end interface needs to be adjusted to support paging queries.

But the current status is that there is no extra development resources for the front end.

Due to lack of manpower, this program can only be put on hold for now.

2. Call the interface in batches

The backend of the business system used to call the evaluation query interface once, but now it is called in batches.

For example: before querying 500 records, the business system only calls the query interface once.

Now it is changed to a business system that only checks 100 records at a time, calls them in 5 batches, and queries a total of 500 records.

Isn't this slower?

Answer: If the 5 batches of calls to the evaluation query interface are performed in a single-threaded sequence in the for loop, the overall time-consuming may of course be slower.

However, the business system can also be changed to multi-threaded calls, and it only needs to summarize the results in the end.

At this point, some people may ask: is the server multi-threaded call of the evaluation query interface the same as multi-threaded call in other business systems?

How about increasing the maximum number of threads in the thread pool of the server for the batch evaluation query interface?

Obviously you overlooked one thing: online applications are generally not deployed as a single point. In most cases, in order to avoid a single point of failure due to server failure, at least two nodes will be deployed. In this way, even if a node is down, the entire application can be accessed normally.

Of course, this situation may also occur: if one node is hung up, the other node may be unable to bear the pressure due to too much access traffic, and may also hang up because of this.

In other words, through the multi-thread call interface in the business system, the traffic load of the access interface can be balanced to different nodes.

They also use 8 threads to divide the data into batches of 100 records, and finally summarize the results.

After this optimization, the interface performance has been doubled again.

From about 1s, shortened to less than 500ms.

Warm reminder, whether it is querying the database through the batch query evaluation interface, or calling the batch query evaluation interface in the business system, using multi-threaded calls, it is only a temporary solution and is not perfect.

The reason for this is mainly to solve the problem quickly first, because this kind of solution change is minimal.

To solve the problem fundamentally, this set of functions needs to be redesigned, the table structure needs to be modified, and the business process may even need to be modified. However, due to the involvement of multiple business lines and multiple business systems, it can only be done slowly according to schedule.

Guess you like

Origin blog.csdn.net/hebiwen95/article/details/126346235