软件系统性能解决方案

高性能设计会涉及到几个名词:IO多路复用、零拷贝、线程池、冗余等,其本质上是一个系统性的问题,可以从计算机体系结构的底层原来去思考,系统优化离不开CPU和IO两个维度,具体如下:

方案一:计算性能优化

1、减少程序计算复杂度

boolean result = true;
// 循环遍历请求的requests, 判断如果是A业务且A业务未达到终态返回false, 否则返回true
for(Requet request: requests){
     // 1. query DB 获取TestDO
     String id = request.getId();
     TestDO testDO = queryDOById(id);
     // 2. 如果是A业务且testDO未到达中态记录为false
     if(StringUtils.equals("A", request.getBizType())){
         // check是否到达终态
         if(!StringUtils.equals("FINISHED", testDO.getStatus)){
             result = result && false;
         }
     }
}
return result;

代码中存在很明显的几个问题:

1.每次请求过来在第6行都去查询DB,但是在第8行对请求做了判断和筛选,导致第6行的代码计算资源浪费,而且第6行访问DAO数据,是一个比较耗时的操作,可以先判断业务是否属于A再去查询DB;

2.当前的需求是只要有一个A业务未到达终态即可返回false, 11行可以在拿到false之后,直接break,减少计算次数;

优化后的代码:

boolean result = true;
// 循环遍历请求的requests, 判断如果是A业务且A业务未达到终态返回false, 否则返回true
for(Requet request: requests){
// 1. 不是A业务的不走查询DB的逻辑
if(!StringUtils.equals("A", request.getBizType())){
continue;
     }
     // 2. query DB 获取TestDO
     String id = request.getId();
     TestDO testDO = queryDOById(id);
     // check是否到达终态
     if(!StringUtils.equals("FINISHED", testDO.getStatus)){
         result = false;
         break;
     }
}
return result;

优化之后的计算耗时从平均270.75ms-->40.5ms

日常优化代码可以用ARTHAS工具分析下程序的调用耗时,耗时大的任务尽可能做好过滤,减少不必要的系统调用。

2、合理使用同步异步

分析业务链路中,哪些需要同步等待结果,哪些不需要,核心依赖的调度可以同步,非核心依赖尽量异步。

场景:从链路上看A系统调用B系统,B系统调用C系统完成计算再把结论返回给A,A系统超时时间400ms,通常A系统调用B系统300ms,B系统调用C系统200ms。


现在C系统需要将调用结论返回给D系统,耗时150ms

此时A系统- B系统- C系统已有的调用链路可能会超时失败,因为引入D系统之后,耗时增加了150ms,整个过程是同步调用的,因此需要C系统将调用D系统更新结论的非强依赖改成异步调用。

// C系统调用D系统更新结果
featureThreadPool.execute(()->{
   try{
      dSystemClient.updateResult(resultDTO);
   }catch (Exception exception){
      LogUtil.error(exception, logger, "dSystemClient.updateResult failed! resultDTO = {0}", JSON.toJSONString(resultDTO));
   }
});

3、做好限流保护

故障场景:A系统调用B系统查询异常数据,日常10TPS左右甚至更少,某一天A系统改了定时任务触发逻辑,加上代码bug,调用频率达到了500TPS,并且由于ID传错,绕过了缓存直接查询了DB和Hbase, 造成了Hbase读热点,拖垮集群,存储和查询都受到了影响。

后续对A系统做了查询限流,保证并发量在15TPS以内,核心业务服务需要做好查询限流保护,同时也要做好缓存设计。

4、多线程代替单线程

场景:应急定位场景下,A系统调用B系统获取诊断结论,TR超时时间是500ms,对于一个异常ID事件,需要执行多个诊断项服务,并记录诊断流水;每个诊断的耗时大概在100ms以内,随着业务的增长,超过5个诊断项,计算耗时累加到500ms+,这时候服务会出现高峰期短暂不可用。

将这段代码改成异步执行,这样执行诊断的时间是耗时最大的诊断服务

// 提交future任务并发执行
futures = executor.invokeAll(tasks, timeout, timeUnit);
// 遍历读取结果
for (Future future : futures) {
    try {
        // 获取结果
        Res singleResult = future.get();
        if (singleResult != null) {
            result.add(singleResult);
        }
    } catch (Exception e) {
        LogUtil.error(e, logger, "并发执行发生异常!,poolName={0}.", threadPoolName);
    }
}

5、集群计算代替单机

这里可以使用三层分发,将计算任务分片后执行,Map-Reduce思想,减少单机的计算压力。

方案二:系统IO性能优化

1、常见的FullGC解决

系统常见的FullGC问题有很多,先讲一下JVM的垃圾回收机制: Heap区在设计上是分代设计的, 划分为了Eden、Survivor 和 Tenured/Old ,其中Eden区、Survivor(存活)属于年轻代,Tenured/Old区属于老年代或者持久代。一般我们将年轻代发生的GC称为Minor GC,对老年代进行GC称为Major GC,FullGC是对整个堆来说。

内存分配策略:1. 对象优先在Eden区分配 2. 大对象直接进入老年代 3. 长期存活的对象将进入老年代4. 动态对象年龄判定(虚拟机并不会永远地要求对象的年龄都必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄的所有对象的大小总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代)5. 只要老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minor GC,否则会进行full GC。

系统常见触发FullGC的case:

(1)查询大对象:业务上历史巡检数据需要定期清理,删除策略是每天删除上个月之前的数据(业务上打上软删除标记),等数据库定时清理任务彻底回收;

某一天修改了删除策略,从“删除上个月之前的数据”改成了“删除上周之前的数据”,因此删除的数据从1000条膨胀到了15万条,数据对象占用了80%以上的内存,直接导致系统的FullGC, 其他任务都有影响;

很多系统代码对于查询数据没有数量限制,随着业务的不断增长,系统容量在不升级的情况下,经常会查询出来很多大的对象List,出现大对象频繁GC的情况。

(2)设置了用不回收的static方法

A系统设置了static的List对象,本身是用来做DRM配置读取的,但是有个逻辑对配置信息做了查询之后,还进行了Put操作,导致随着业务的增长,static对象越来越大且属于类对象,无法回收,最终使得系统频繁GC 。

本身用Object做Map的Key有一定的不合理性,同时key中的对象是不可回收的,导致出现了GC。

当执行Full GC后空间仍然不足,则抛出如下错误【java.lang.OutOfMemoryError: Java heap space】,而为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

2、顺序读写代替随机读写

对于普通的机械硬盘而言,随机写入的性能会很差,时间久了还会出现碎片,顺序的写入会极大节省磁盘寻址及磁盘盘片旋转的时间,极大提升性能;这层其实本身中间件帮我们实现了,比如Kafka的日志文件存储消息,就是通过有序写入消息和不可变性,消息追加到文件的末尾,来保证高性能读写。

3、DB索引设计

设计表结构时,我们要考虑后期对表数据的查询操作,设计合理的索引结构,一旦表索引建立好了之后,也要注意后续的查询操作,避免索引失效。


(1)尽量不选择键值较少的列即区分度不明显,重复数据很少的做索引;比如我们用is_delete这种列做了索引,查询10万条数据,where is_delete=0,有9万条数据块,加上访问索引块带来的开销,不如全表扫描全部的数据块了;

(2)避免使用前导like "%***"以及like "%***%", 因为前面的匹配是模糊的,很难利用索引的顺序去访问数据块,导致全表扫描;但是使用like "A**%"不影响,因为遇到"B"开头的数据就可以停止查找列,我们在做根据用户信息模糊查询数据时,遇到了索引失效的情况;

(3) 其他可能的场景比如,or查询,多列索引不使用第一部分查询,查询条件中有计算操作,或者全表扫描比索引查询更快的情况下也会出现索引失效;

目前AntMonitor以及Tars等工具已经帮我们扫描出来耗时和耗CPU很大的SQL,可以根据执行计划调整查询逻辑,频繁的少量数据查询利用好索引,当然建立过多的索引也有存储开销,对于插入和删除很频繁的业务,也要考虑减少不必要的索引设计。

4、分库分表设计


随着业务的增长,如果集群中的节点数量过多,最终会达到数据库的连接限制,导致集群中的节点数量受限于数据库连接数,集群节点无法持续增加和扩容,无法应对业务流量的持续增长;这也是蚂蚁做LDC架构的其中原因之一,在业务层做水平拆分和扩展,使得每个单元的节点只访问当前节点对应的数据库。

5、避免大量的表JOIN

阿里编码规约中超过三个表禁止JOIN,因为三个表进行笛卡尔积计算会出现操作复杂度呈几何数增长,多个表JOIN时要确保被关联的字段有索引。

如果为了业务上某些数据的级联,可以适当根据主键在内存中做嵌套的查询和计算,操作非常频繁的流水表建议对部分字段做冗余,以空间复杂度换取时间复杂度。

6、减少业务流水表大量耗时计算

业务记录有时候会做一些count操作,如果对时效性要求不高的统计和计算,建议定时任务在业务低峰期做好计算,然后将计算结果保存在缓存。

涉及到多个表JOIN的建议采用离线表进行Map-Reduce计算,然后再将计算结果回流到线上表进行展示。

7、数据过期策略

一张表的数据量太大的情况下,如果不按照索引和日期进行部分扫描而出现全表扫描的情况,对DB的查询性能是非常有影响的,建议合理的设计数据过期策略,历史数据定期放入history表,或者备份到离线表中,减少线上大量数据的存储。

8、合理使用内存

众所周知,关系型数据库DB查询底层是磁盘存储,计算速度低于内存缓存,缓存DB与业务系统连接有一定的调用耗时,速度低于本地内存;但是从存储量来看,内存存储数据容量低于缓存,长期持久化的数据建议放DB存在磁盘中,设计过程中考虑好成本和查询性能的平衡。

说到内存,就会有数据一致性问题,DB数据和内存数据如何保证一致性,是强一致性还是弱一致性,数据存储顺序和事务如何控制都需要去考虑,尽量做到用户无感知。

9、做好数据压缩

很多中间件对数据的存储和传输采用了压缩和解压操作,减少数据传输中的带宽成本,这里对数据压缩不再做过多的介绍,想提的一点是高并发的运行态业务,要合理的控制日志的打印,不能够为了便于排查,打印过多的JSON.toJSONString(Object),磁盘很容易被打满,按照日志的容量过期策略也很容易被回收,更不方便排查问题;因此建议合理的使用日志,错误码仅可能精简,核心业务逻辑打印好摘要日志,结构化的数据也便于后续做监控和数据分析。

打印日志的时候思考几个问题:这个日志有没有可能会有人看,看了这个日志能做什么,每个字段都是必须打印的吗,出现问题能不能提高排查效率。

10、Hbase热点key问题

Habse的存储结构如下:Table在行的方向上分割为多个HRegion,HRegion是HBase中分布式存储和负载均衡的最小单元,即不同的HRegion可以分别在不同的HRegionServer上,但同一个HRegion是不会拆分到多个HRegionServer上的。HRegion按大小分割,每个表一般只有一个HRegion,随着数据不断插入表,HRegion不断增大,当HRegion的某个列簇达到一个阈值(默认256M)时就会分成两个新的HRegion。

HBase 中的行是按照 Rowkey 的字典顺序排序的,这种设计优化了 scan 操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。Rowkey这种固有的设计是热点故障的源头。热点的热是指发生在大量的 client 直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。

大量访问会使热点 Region 所在的单个机器超出自身承受能力,引起性能下降甚至 Region 不可用,这也会影响同一个 RegionServer 上的其他 Region,由于主机无法服务其他 Region 的请求,这样就造成数据热点(数据倾斜)现象。

所以我们在向 HBase 中插入数据的时候,应优化 RowKey 的设计,使数据被写入集群的多个 region,而不是一个,尽量均衡地把记录分散到不同的 Region 中去,平衡每个 Region 的压力。

常见的热点Key避免的方法: 反转,加盐和哈希

总之Rowkey在设计的过程中,尽量保证长度原则、唯一原则、排序原则、散列原则。

展开阅读全文

页面更新:2024-03-02

标签:热点   前缀   集群   索引   对象   内存   解决方案   性能   操作   业务   数据   系统

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top