软件系统性能解决方案
高性能设计会涉及到几个名词:IO多路复用、零拷贝、线程池、冗余等,其本质上是一个系统性的问题,可以从计算机体系结构的底层原来去思考,系统优化离不开CPU和IO两个维度,具体如下: 如何设计高性能计算(CPU):减少计算成本,合理使用同步/异步、限流减少请求次数等;让更多的核参与计算: 多线程代替单线程、集群代替单机等等; 如何提升系统IO:加快IO速度,顺序读写代替随机读写、硬件上SSD提升等;减少IO次数,索引/分布式计算代替全表扫描、零拷贝减少IO复制次数、DB批量读写、分库分表增加连接数等;减少IO存储,数据过期策略、合理使用内存、缓存、DB等中间件,做好消息压缩等。 方案一:计算性能优化
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避免的方法: 反转,加盐和哈希 反转:比如用户ID2088这种前缀,以及BBCRL开头的这种相同前缀,都可以适当的反转往后移动。 加盐: RowKey 的前面增加一些前缀,比如时间戳Hash,加盐的前缀种类越多,才会根据随机生成的前缀分散到各个 region 中,避免了热点现象,但是也要考虑scan方便 哈希:为了在业务上能够完整地重构 RowKey,前缀不可以是随机的。 所以一般会拿原 RowKey 或其一部分计算 Hash 值,然后再对 Hash 值做运算作为前缀。
总之Rowkey在设计的过程中,尽量保证长度原则、唯一原则、排序原则、散列原则。
喝白酒为何要配一瓶矿泉水?新手只用来解渴,酒桌老手用处大在山东河南等白酒大省,无论是吃宵夜大排档,还是火锅中餐店,酒桌上喝白酒的人,手边千篇一律会配一瓶矿泉水,当然也有可能是纯净水,这种习惯已经逐渐流传到了全国各地。水的作用,肯定是用来
4碗入秋养生汤,赶走肺燥脾虚和积食燥为秋之主气,而初秋的燥邪还带有夏暑的余热,燥与温热结合则为温燥。但这时的暑湿和热都未完全消散,因此入秋后饮食要注意清热润肺以及顾护脾胃。玉米笋排骨汤预防秋燥,滋润美肤材料玉米笋,
秋冬润燥养颜汤,比燕窝还养人红枣桂圆酒酿食材红枣桂圆酒酿红糖姜丝枸杞鸡蛋做法1。锅中放入生姜红枣桂圆枸杞,加入适量清水。2。小火煮10分钟,加入红糖后接着煮10分钟。打入两个鸡蛋。4。最后加入一大勺酒酿再煮两
ARTISTICampampampCO。携IPRIMO联合打造VIP奢享沙龙9月24日,日本高端美容仪品牌ARTISTICCO。携手日本轻奢婚戒定制品牌IPRIMO及贵金属品牌Pt铂金,在上海凯宾斯基大酒店联合举办了一场别开生面的婚礼季主题VIP奢享沙龙,
诚邀大家来看牛仔萌妹Lisa的少女感牛仔套装,要带印花哦!随着BLACKPINK的回归,好久没举办的签售会也开始了,谁又能不被穿着碎花的Lisa可爱到呢?这件来自GANNI的牛仔外套真的长在了我的审美
冬季职场男生通勤穿搭相比日常穿的随性,职场搭配还是不能太马虎,体面是成年人该有的姿态,典雅与气场并存,为你增加不少男性魅力。18套冬季通勤搭配每一套都很合适上班族们穿。颜色参考职场穿搭配一般我们都会选
和平精英辅助瞄准小白究竟要不要开?这三种情况建议开启辅助瞄准这样一个功能在之前是主要是出现在手柄上,为了帮助玩家在开枪瞄准时降低准心在一定接近范围内的移动速度。现在,不少射击手游像和平精英使命召唤等都做了辅助瞄准这个功能设置。和平精
日常生活中,普通人该如何防肾病?医生建议做到这三大关键肾脏每日24小时默默地为人体排出多余的代谢废物和水分,产生各种激素维持人体的正常生理需要。插图来源于网络关键一合理的生活方式(1)戒烟限酒。(2)坚持每周有氧锻炼150分钟。建议养
自从学会猪肉这样做,我家米饭都不够吃了,营养下饭又解馋,真香三餐美味,四季幸福,大家好!随着现在物质生活的丰富,人们对食物的要求也越来越高,尤其是现在的孩子也越来越挑食,家长绞尽脑汁做美食。今天,我要要教大家一个在家制作肉卷的方法,做出来的
ISV的想法,用友全都懂不妨大胆猜想,若只留下三五个数字平台,只留下一个商业创新平台,独立软件开发商(ISV)会如何选择。用友BIP3是里程碑式产品。王文京如此评价并非拉票,在BIP3完成脱胎换骨的技术迭
为什么不要购买512GB手机?内行人3个忠告,不要买了才后悔为什么不要购买512GB手机?内行人3个忠告,不要买了才后悔现在的手机功能越来越多,可是也正是因为这个,大家的手机容量也越换越大了,可是不用多久手机内存就出现警告了。iPhone1