高性能设计会涉及到几个名词:IO多路复用、零拷贝、线程池、冗余等,其本质上是一个系统性的问题,可以从计算机体系结构的底层原来去思考,系统优化离不开CPU和IO两个维度,具体如下:如何设计高性能计算(CPU):减少计算成本,合理使用同步异步、限流减少请求次数等;让更多的核参与计算:多线程代替单线程、集群代替单机等等;如何提升系统IO:加快IO速度,顺序读写代替随机读写、硬件上SSD提升等;减少IO次数,索引分布式计算代替全表扫描、零拷贝减少IO复制次数、DB批量读写、分库分表增加连接数等;减少IO存储,数据过期策略、合理使用内存、缓存、DB等中间件,做好消息压缩等。方案一:计算性能优化 1、减少程序计算复杂度循环遍历请求的requests,判断如果是A业务且A业务未达到终态返回false,否则返回truefor(Requetrequest:requests){1。queryDB获取TestDOStringidrequest。getId();TestDOtestDOqueryDOById(id);2。如果是A业务且testDO未到达中态记录为falseif(StringUtils。equals(A,request。getBizType())){check是否到达终态if(!StringUtils。equals(FINISHED,testDO。getStatus)){}}} 代码中存在很明显的几个问题: 1。每次请求过来在第6行都去查询DB,但是在第8行对请求做了判断和筛选,导致第6行的代码计算资源浪费,而且第6行访问DAO数据,是一个比较耗时的操作,可以先判断业务是否属于A再去查询DB; 2。当前的需求是只要有一个A业务未到达终态即可返回false,11行可以在拿到false之后,直接break,减少计算次数; 优化后的代码:循环遍历请求的requests,判断如果是A业务且A业务未达到终态返回false,否则返回truefor(Requetrequest:requests){1。不是A业务的不走查询DB的逻辑if(!StringUtils。equals(A,request。getBizType())){}2。queryDB获取TestDOStringidrequest。getId();TestDOtestDOqueryDOById(id);check是否到达终态if(!StringUtils。equals(FINISHED,testDO。getStatus)){}} 优化之后的计算耗时从平均270。75ms40。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(Exceptionexception){LogUtil。error(exception,logger,dSystemClient。updateResultfailed!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任务并发执行futuresexecutor。invokeAll(tasks,timeout,timeUnit);遍历读取结果for(FutureResfuture:futures){try{获取结果RessingleResultfuture。get();if(singleResult!null){result。add(singleResult);}}catch(Exceptione){LogUtil。error(e,logger,并发执行发生异常!,poolName{0}。,threadPoolName);}} 5、集群计算代替单机 这里可以使用三层分发,将计算任务分片后执行,MapReduce思想,减少单机的计算压力。方案二:系统IO性能优化 1、常见的FullGC解决 系统常见的FullGC问题有很多,先讲一下JVM的垃圾回收机制:Heap区在设计上是分代设计的,划分为了Eden、Survivor和TenuredOld,其中Eden区、Survivor(存活)属于年轻代,TenuredOld区属于老年代或者持久代。一般我们将年轻代发生的GC称为MinorGC,对老年代进行GC称为MajorGC,FullGC是对整个堆来说。 内存分配策略:1。对象优先在Eden区分配2。大对象直接进入老年代3。长期存活的对象将进入老年代4。动态对象年龄判定(虚拟机并不会永远地要求对象的年龄都必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄的所有对象的大小总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代)5。只要老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minorGC,否则会进行fullGC。 系统常见触发FullGC的case: (1)查询大对象:业务上历史巡检数据需要定期清理,删除策略是每天删除上个月之前的数据(业务上打上软删除标记),等数据库定时清理任务彻底回收; 某一天修改了删除策略,从删除上个月之前的数据改成了删除上周之前的数据,因此删除的数据从1000条膨胀到了15万条,数据对象占用了80以上的内存,直接导致系统的FullGC,其他任务都有影响; 很多系统代码对于查询数据没有数量限制,随着业务的不断增长,系统容量在不升级的情况下,经常会查询出来很多大的对象List,出现大对象频繁GC的情况。 (2)设置了用不回收的static方法 A系统设置了static的List对象,本身是用来做DRM配置读取的,但是有个逻辑对配置信息做了查询之后,还进行了Put操作,导致随着业务的增长,static对象越来越大且属于类对象,无法回收,最终使得系统频繁GC。 本身用Object做Map的Key有一定的不合理性,同时key中的对象是不可回收的,导致出现了GC。 当执行FullGC后空间仍然不足,则抛出如下错误【java。lang。OutOfMemoryError:Javaheapspace】,而为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在MinorGC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 2、顺序读写代替随机读写 对于普通的机械硬盘而言,随机写入的性能会很差,时间久了还会出现碎片,顺序的写入会极大节省磁盘寻址及磁盘盘片旋转的时间,极大提升性能;这层其实本身中间件帮我们实现了,比如Kafka的日志文件存储消息,就是通过有序写入消息和不可变性,消息追加到文件的末尾,来保证高性能读写。 3、DB索引设计 设计表结构时,我们要考虑后期对表数据的查询操作,设计合理的索引结构,一旦表索引建立好了之后,也要注意后续的查询操作,避免索引失效。 (1)尽量不选择键值较少的列即区分度不明显,重复数据很少的做索引;比如我们用isdelete这种列做了索引,查询10万条数据,whereisdelete0,有9万条数据块,加上访问索引块带来的开销,不如全表扫描全部的数据块了; (2)避免使用前导like以及like,因为前面的匹配是模糊的,很难利用索引的顺序去访问数据块,导致全表扫描;但是使用likeA不影响,因为遇到B开头的数据就可以停止查找列,我们在做根据用户信息模糊查询数据时,遇到了索引失效的情况; (3)其他可能的场景比如,or查询,多列索引不使用第一部分查询,查询条件中有计算操作,或者全表扫描比索引查询更快的情况下也会出现索引失效; 目前AntMonitor以及Tars等工具已经帮我们扫描出来耗时和耗CPU很大的SQL,可以根据执行计划调整查询逻辑,频繁的少量数据查询利用好索引,当然建立过多的索引也有存储开销,对于插入和删除很频繁的业务,也要考虑减少不必要的索引设计。 4、分库分表设计 随着业务的增长,如果集群中的节点数量过多,最终会达到数据库的连接限制,导致集群中的节点数量受限于数据库连接数,集群节点无法持续增加和扩容,无法应对业务流量的持续增长;这也是蚂蚁做LDC架构的其中原因之一,在业务层做水平拆分和扩展,使得每个单元的节点只访问当前节点对应的数据库。 5、避免大量的表JOIN 阿里编码规约中超过三个表禁止JOIN,因为三个表进行笛卡尔积计算会出现操作复杂度呈几何数增长,多个表JOIN时要确保被关联的字段有索引。 如果为了业务上某些数据的级联,可以适当根据主键在内存中做嵌套的查询和计算,操作非常频繁的流水表建议对部分字段做冗余,以空间复杂度换取时间复杂度。 6、减少业务流水表大量耗时计算 业务记录有时候会做一些count操作,如果对时效性要求不高的统计和计算,建议定时任务在业务低峰期做好计算,然后将计算结果保存在缓存。 涉及到多个表JOIN的建议采用离线表进行MapReduce计算,然后再将计算结果回流到线上表进行展示。 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避免的方法:反转,加盐和哈希反转:比如用户ID2088这种前缀,以及BBCRL开头的这种相同前缀,都可以适当的反转往后移动。加盐:RowKey的前面增加一些前缀,比如时间戳Hash,加盐的前缀种类越多,才会根据随机生成的前缀分散到各个region中,避免了热点现象,但是也要考虑scan方便哈希:为了在业务上能够完整地重构RowKey,前缀不可以是随机的。所以一般会拿原RowKey或其一部分计算Hash值,然后再对Hash值做运算作为前缀。 总之Rowkey在设计的过程中,尽量保证长度原则、唯一原则、排序原则、散列原则。