掘金 后端 ( ) • 2024-05-08 18:01

前言:对于一个新手来说,当接手一个大型C端系统的时候,常常难以评判出这个系统的性能的好与差。直接看代码的话效率较低而且容易陷入细节难以掌握大局,在这篇文章中我将根据我亲身的经验来介绍如何做系统性能评估,以及如何解决常见的性能问题。

当然,关于性能评估并不是几句话能讲清楚的,如果讲的很宽泛则将毫无意义,因为遇到了问题还是没法解决。就好比很多人背了很多八股文写起代码来还是一坨shit一样,毫无抽象可言处处都是性能问题。所以我可能会分几部分来讲。

当客户端进行了一次http调用的时候,服务端响应如果耗时很长,比如只是查询一下用户最近发文就用了2秒。那么这个接口的性能一定是不行的。影响因素可能非常多,但是大多逃不过IO操作,比如使用MQ发送消息,进行RPC和HTTP进行远程调用,操作Redis,调用MySQL等。

在我举的例子中,MQ发送消息和redis调用一般来说性能极高。所以RPC、HTTP和MySQL对性能的影响会更显著,无数的事故和bug都可以验证这一点。

而归根结底,关键影响因素主要还是MySQL,因为RPC和HTTP最终也可能是调用MySQL导致性能不行。大多时候瓶颈也都再此。所以要做系统评估的时候我们优先从最底层,也就是数据库开始。

关于慢查询

不是说慢查询就一定会导致系统出问题,比如我本地连接了一下数据库,帮运营查了个数据耗时2S,这是没啥影响的。

主要影响因素是这是sql量一旦大了慢查询就是灾难性的了,比如占用了大量连接,导致其他客户端拿不到链接就会一直超时了。

是否好奇过,有的时候我们明明都没动过代码,甚至很久没上线了,但是突然系统变慢了,发生慢查询告警了。这是因为可能一开始查询的表数据量不大,但是大量逐渐增大时,查询耗时会逐渐上涨最终达到了系统设置的慢查询阈值,最终造成事故。

所以我们一定要对SQL进行监控。在sql发展成慢查询SQL前进行解决。

SQL监控

在中大厂里面都有专门的DBA对数据库做了监控和告警,比如CPU、线程数、连接数、传输数据量等。但是这只是数据库系统层面的监控,这一般不会有问题,有问题DBA都找你了。

我们要特别关心SQL监控,每日进行巡检,通过SQL监控来进行针对性优化。

既然叫SQL监控,那么自然是监控SQL,比如调用量,qps,耗时等。下图是一张每分钟调用量的监控图

关于SQL监控有的公司做了监控有的则没有,比如快手就有、小米就没有。

image.png 如果没有的话自己做也不难,比如你用的mybatis,那么加层拦截器即可,可以参考下面的demo

image.png


    @Intercepts({
            @Signature(
                    type = Executor.class,
                    method = "query",
                    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
            ),
            @Signature(
                    type = Executor.class,
                    method = "query",
                    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
            )
    })
    @Component
    public class SQLExecuteCostInterceptor implements Interceptor {
        private static final Logger LOGGER = 	 LoggerFactory.getLogger(SQLExecuteCostInterceptor.class);
    
        @Autowired
        private PerfHelper perfHelper;
    
        @Override
        public Object intercept(Invocation invocation) throws Exception {
            Object[] args = invocation.getArgs();
            MappedStatement statement = (MappedStatement) args[0];
            Object parameter = args[1];
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = statement.getBoundSql(parameter);
            } else {
                boundSql = (BoundSql) args[5];
            }
            StopWatch stopWatch = StopWatch.createStarted();
            Object result = invocation.proceed();
            // 监控打点
            perfHelper.perf("database-name", boundSql.getSql())
                    .micros(stopWatch.getNanoTime())
                    .logstash();
    
            LOGGER.info("执行 {}, cost : {}", boundSql.getSql(), stopWatch.getNanoTime());
            return result;
        }
    }

如何看SQL监控

关注qps

我们首先要关注QPS,因为我们的数据库能承载的qps和tps是非常有限的,比如可能qps最多不能超高5000。所以当你发现某些sql的调用qps非常高时可能要特别关注了。

qps激增

这比如某个时间点突然上涨,可以看看是否是接口流量激增,或者发生了缓存失效(这种尖刺会比较明显)

是否有必要入库查询

一、很多时候的查询是无效查询,业务层面就能判断处理掉。

拿之前直播活动遇到的问题来举例,用户需要完成直播活动的任务才能拿到现金奖励,这些任务和拿到的奖励都会在页面展示。所以当查询用户任务的时候,发现用户任务状态是未完成就不必去入库查询结算表了。从下图能看到效果是极其明显的。

776b9c97e7a5b9af56aefab0ca880e1.jpg

二、是否可以使用缓存、布隆过滤器等来解决

这里说的缓存可以是分布式缓存redis,memcached,也可以是本地缓存。尤其对于热key的访问绝对是避免直接访问数据库的。别觉得理所应当,我现在所在的”小厂“很多人都考虑不到这点,我刚接手的时候所有的查询都是走的数据库,因为是新项目没啥人用自然没问题。

对于权限系统甚至可以使用布隆过滤器来判存,比如我们的某些功能系统只对少量用户开放,系统抗流量能力不高。完全可以使用布隆过滤器作为上层判断,如果userId命中再调用我们的系统接口来判断该用户是否拥有权限。

关于这部分可以参考我的高并发系列文章。

避免轮训数据库

单库单表

如果你要批量查询用户相关信息,比如查询用户奖励信息,应该使用批量查询接口而不是去轮训

反例:

while(long userId : userIds) {
	rewardService.getByUserId(userId);
}

正例:

// userIds量大就partition
rewardService.batchGetByUserIds(userIds);
散表

如果散表了,避免单表过热则是依次查询0-N(表数量-1),当然这必须是离线查询,因为时间非常长。

如果是要实时查询,那么就将用户奖励信息写到es,然后查询es。

减少操作数据库次数

比如我给用户下发任务,要下发1000W个用户。然后创建1000W个用户任务,我们最好就是下发用户任务的时候,也就是发送消息到消息队列。发送的消息每次包含1000个用户id和任务系统,我们最好就是根据用户任务表的分库分表规则,比如我们有1000张表,userId%1000确定落在哪张表,所有发送消息的时候根据这个规则保证1000个userId落在一张表,下游处理的时候,先创建1000个UserTask,然后一次性插入到用户任务表即可。否则最差情况就会有1000次io操作。

关注耗时

慢查询对业务的影响是最致命的,但是数据量不大的时候查询还是很快,比如全表扫描扫描100条数据和1000W条数据的速度是完全不一样的。最终可能导致整个数据库不可用,整个业务崩溃。这种情况做过大型业务的人肯定遇到过,我遇到不下3次。

通过SQL监控能尽早的帮我们发现问题,比如当我们发现某条查询量大的sql平均耗时200ms时我们就肯定要去优化解决了,等发展到500-600可能就拖慢了整个业务,当达到2-5秒那系统就得告警甚至瘫痪了。

关于SQL优化

关于SQL优化的问题网上很多,我这里只提一些常见的。

所有查询都必须使用到索引

在做C端开发的时候,我们基本的原则是保证用户SQL都能使用到索引。索引是印象耗时的最关键因素,所有慢查询问题几乎无一不是索引问题,写代码前先使用explain去判断吧。

分页查询的排序字段要加索引

这个组里遇到的所有人写的分页查询都没用到索引,你的order by字段,比如order by create_time,如果create_time不在你的联合索引里面,数据量一大就filesort了。给个之前公司的效果图吧,就是这个问题。

优化前SQL耗时:

image.png

优化后SQL耗时:

image.png

最左匹配原则

这个没啥好说的,用得最多的就是联合索引了,查询的时候要满足最左匹配原则即可。