分布式系统设计 - 性能优化

打印 上一主题 下一主题

主题 1832|帖子 1832|积分 5496

什么是高性能系统

先明白一下什么是高性能设计,官方定义: 高可用(High Availability,HA)核心目标是保障业务的一连性,从用户视角来看,业务永远是正常稳固的对外提供服务,业界一样寻常用几个9来权衡系统的可用性。通常采用一系列专门的设计(冗余、去单点等),镌汰业务的歇工时间,从而保持其核心折务的高度可用性。
高并发(High Concurrency)通常是指系统可以或许同时并行处理很多哀求。一样寻常用响应时间、并发吞吐量TPS, 并发用户数等指标来权衡。
高性能是指程序处理速率非常快,所占内存少,CPU占用率低。高性能的指标常常和高并发的指标精密相关,想要提高性能,那么就要提高系统发并发本领。
本文重要对做“高性能、高并发、高可用”服务的设计进行先容和分享。
从哪几个方面做好性能提升
每次谈到高性能设计,常常会面临几个名词:IO多路复用、零拷贝、线程池、冗余等等,关于这部分的文章非常的多,实在本质上是一个系统性的问题,可以从计算机体系布局的底层原来去思索,系统优化离不开计算性能(CPU)和存储性能(IO)两个维度,总结如下方法:
如何设计高性能计算(CPU)
镌汰计算成本: 代码优化计算的时间复杂度O(N^2)->O(N),合理使用同步/异步、限流镌汰哀求次数等;
让更多的核参与计算: 多线程取代单线程、集群取代单机等等;
如何提升系统IO
加速IO速率: 顺序读写取代随机读写、硬件上SSD提升等;
镌汰IO次数: 索引/分布式计算取代全表扫描、零拷贝镌汰IO复制次数、DB批量读写、分库分表增长连接数等;
镌汰IO存储: 数据过期策略、合理使用内存、缓存、DB等中间件,做好消息压缩等;
高性能优化策略

1. 计算性能优化策略
1.1 镌汰程序计算复杂度
简朴来看这段伪代码
  1. boolean result = true;
  2. // 循环遍历请求的requests, 判断如果是A业务且A业务未达到终态返回false, 否则返回true
  3. for(Requet request: requests){
  4.      // 1. query DB 获取TestDO
  5.      String id = request.getId();
  6.      TestDO testDO = queryDOById(id);
  7.      // 2. 如果是A业务且testDO未到达中态记录为false
  8.      if(StringUtils.equals("A", request.getBizType())){
  9.          // check是否到达终态
  10.          if(!StringUtils.equals("FINISHED", testDO.getStatus)){
  11.              result = result && false;
  12.          }
  13.      }
  14. }
  15. return result;
复制代码
代码中存在很明显的几个问题:
1.每次哀求过来在第6行都去查询DB,但是在第8行对哀求做了判定和筛选,导致第6行的代码计算资源浪费,而且第6行访问DAO数据,是一个比较耗时的操作,可以先判定业务是否属于A再去查询DB;
2.当前的需求是只要有一个A业务未到达终态即可返回false, 11行可以在拿到false之后,直接break,镌汰计算次数;
优化后的代码:
  1. boolean result = true;
  2. // 循环遍历请求的requests, 判断如果是A业务且A业务未达到终态返回false, 否则返回true
  3. for(Requet request: requests){
  4. // 1. 不是A业务的不走查询DB的逻辑
  5. if(!StringUtils.equals("A", request.getBizType())){
  6. continue;
  7.      }
  8.      // 2. query DB 获取TestDO
  9.      String id = request.getId();
  10.      TestDO testDO = queryDOById(id);
  11.      // check是否到达终态
  12.      if(!StringUtils.equals("FINISHED", testDO.getStatus)){
  13.          result = false;
  14.          break;
  15.      }
  16. }
  17. return result;
复制代码
一样寻常优化代码可以用ARTHAS工具分析下程序的调用耗时,耗时大的任务尽可能做好过滤,镌汰不须要的系统调用。


1.2 合理使用同步异步
分析业务链路中,哪些必要同步等候结果,哪些不必要,核心依赖的调度可以同步,非核心依赖尽量异步。
场景:从链路上看A系统调用B系统,B系统调用C系统完成计算再把结论返回给A,A系统超时时间400ms,通常A系统调用B系统300ms,B系统调用C系统200ms。

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

此时A系统- B系统- C系统已有的调用链路可能会超时失败,因为引入D系统之后,耗时增长了150ms,整个过程是同步调用的,因此必要C系统将调用D系统更新结论的非强依赖改成异步调用。
  1. // C系统调用D系统更新结果
  2. featureThreadPool.execute(()->{
  3.    try{
  4.       dSystemClient.updateResult(resultDTO);
  5.    }catch (Exception exception){
  6.       LogUtil.error(exception, logger, "dSystemClient.updateResult failed! resultDTO = {0}", JSON.toJSONString(resultDTO));
  7.    }
  8. });
复制代码
1.3 做好限流保护
故障场景:A系统调用B系统查询异常数据,一样寻常10TPS左右甚至更少,某一天A系统改了定时任务触发逻辑,加上代码bug,调用频率到达了500TPS,并且由于ID传错,绕过了缓存直接查询了DB和Hbase, 造成了Hbase读热点,拖垮集群,存储和查询都受到了影响。

后续对A系统做了查询限流,包管并发量在15TPS以内,核心业务服务必要做好查询限流保护,同时也要做好缓存设计。
1.4 多线程取代单线程
场景:应急定位场景下,A系统调用B系统获取诊断结论,TR超时时间是500ms,对于一个异常ID事件,必要实行多个诊断项服务,并记载诊断流水;每个诊断的耗时大概在100ms以内,随着业务的增长,超过5个诊断项,计算耗时累加到500ms+,这时候服务会出现高峰期短暂不可用。

将这段代码改成异步实行,如许实行诊断的时间是耗时最大的诊断服务
  1. // 提交future任务并发执行
  2. futures = executor.invokeAll(tasks, timeout, timeUnit);
  3. // 遍历读取结果
  4. for (Future<Res> future : futures) {
  5.     try {
  6.         // 获取结果
  7.         Res singleResult = future.get();
  8.         if (singleResult != null) {
  9.             result.add(singleResult);
  10.         }
  11.     } catch (Exception e) {
  12.         LogUtil.error(e, logger, "并发执行发生异常!,poolName={0}.", threadPoolName);
  13.     }
  14. }
复制代码
1.5 集群计算取代单机

这里可以使用三层分发,将计算任务分片后实行,Map-Reduce思想,镌汰单机的计算压力。
2. 系统IO性能优化策略
2.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.2 顺序读写取代随机读写
对于平凡的机器硬盘而言,随机写入的性能会很差,时间久了还会出现碎片,顺序的写入会极大节省磁盘寻址及磁盘盘片旋转的时间,极大提升性能;这层实在本身中间件帮我们实现了,好比Kafka的日记文件存储消息,就是通过有序写入消息和不可变性,消息追加到文件的末尾,来包管高性能读写。
2.3 DB索引设计
设计表布局时,我们要思量后期对表数据的查询操作,设计合理的索引布局,一旦表索引建立好了之后,也要注意后续的查询操作,制止索引失效。

(1)尽量不选择键值较少的列即区分度不明显,重复数据很少的做索引;好比我们用is_delete这种列做了索引,查询10万条数据,where is_delete=0,有9万条数据块,加上访问索引块带来的开销,不如全表扫描全部的数据块了;
(2)制止使用前导like “%"以及like "%%”, 因为前面的匹配是模糊的,很难使用索引的顺序去访问数据块,导致全表扫描;但是使用like "A**%"不影响,因为遇到"B"开头的数据就可以停止查找列,我们在做根据用户信息模糊查询数据时,遇到了索引失效的情况;
(3) 其他可能的场景好比,or查询,多列索引不使用第一部分查询,查询条件中有计算操作,或者全表扫描比索引查询更快的情况下也会出现索引失效;
目前AntMonitor以及Tars等工具已经帮我们扫描出来耗时和耗CPU很大的SQL,可以根据实行筹划调整查询逻辑,频繁的少量数据查询使用好索引,固然建立过多的索引也有存储开销,对于插入和删除很频繁的业务,也要思量镌汰不须要的索引设计。
2.4 分库分表设计


随着业务的增长,如果集群中的节点数量过多,最终会到达数据库的连接限制,导致集群中的节点数量受限于数据库连接数,集群节点无法持续增长和扩容,无法应对业务流量的持续增长;这也是蚂蚁做LDC架构的其中缘故原由之一,在业务层做水平拆分和扩展,使得每个单位的节点只访问当前节点对应的数据库。
2.5 制止大量的表JOIN
阿里编码规约中超过三个表禁止JOIN,因为三个表进行笛卡尔积计算会出现操作复杂度呈多少数增长,多个表JOIN时要确保被关联的字段有索引。

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

2.6 镌汰业务流水表大量耗时计算
业务记载有时候会做一些count操作,如果对时效性要求不高的统计和计算,发起定时任务在业务低峰期做好计算,然后将计算结果保存在缓存。

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

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

2.8 合理使用内存
众所周知,关系型数据库DB查询底层是磁盘存储,计算速率低于内存缓存,缓存DB与业务系统连接有一定的调用耗时,速率低于本地内存;但是从存储量来看,内存存储数据容量低于缓存,长期长期化的数据发起放DB存在磁盘中,设计过程中思量好成本和查询性能的均衡。

说到内存,就会有数据划一性问题,DB数据和内存数据如何包管划一性,是强划一性还是弱划一性,数据存储顺序和事务如何控制都必要去思量,尽量做到用户无感知。
2.9 做好数据压缩
很多中间件对数据的存储和传输采用了压缩和解压操作,镌汰数据传输中的带宽成本,这里对数据压缩不再做过多的先容,想提的一点是高并发的运行态业务,要合理的控制日记的打印,不可以或许为了便于排查,打印过多的JSON.toJSONString(Object),磁盘很轻易被打满,按照日记的容量过期策略也很轻易被回收,更不方便排盘问题;因此发起合理的使用日记,错误码仅可能精简,核心业务逻辑打印好择要日记,布局化的数据也便于后续做监控和数据分析。
打印日记的时候思索几个问题:这个日记有没有可能会有人看,看了这个日记能做什么,每个字段都是必须打印的吗,出现问题能不能提高排查效率。
2.10 Hbase热点key问题
HBase是一个高可靠、高性能、面向列、可伸缩的分布式存储系统,是一种非关系数据库,Hbase存储特点如下:
1.列的可以动态增长,并且列为空就不存储数据,节省存储空间。
2.HBase自动切分数据,使得数据存储自动具有水平scalability。
3.HBase可以提供高并发读写操作的支持,分布式架构,读写锁等候的概率大大低落。
4.不能支持条件查询,只支持按照Rowkey来查询。
5.暂时不能支持Master server的故障切换,当Master宕机后,整个存储系统就会挂掉。
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在设计的过程中,尽量包管长度原则、唯一原则、排序原则、散列原则。
实战-应急链路系统设计方案

要包管整体服务的高可用,必要从全链路视角去对待高可用系统的设计,这里简朴的分享一个上游多个系统调用异常处理系统实行应急的业务场景,分析其中的性能优化改造。
以资金应急系统为例分析系统设计过程中的性能优化。如下图所示,异常处理系统涉及到多个上游App(1-N),这些App发“差异日记数据”给到消息队列, 异常处理系统订阅并消耗消息队列中的“错误日记数据”,然后对这部分数据进行分析、加工聚合等操作,完成异常的发送及应急处理。

发送阶段高可用设计
生产消息阶段:本地队列缓存异常明细数据,保卫线程定时拉取并批量发送(优化方案1中单条上报的性能问题)

消息压缩发送:异通例则复用用一份组装的模型,按照规则则Code聚合压缩上报(优化业务层数据压缩复用本领)

中间件帮你做好了消息的高效序列化机制以及发送的零拷贝技术
存储阶段
目前Kafka等中间件,采用IO多路复用+磁盘顺序写数据的机制,包管IO性能
同时采用分区分段存储机制,提升存储性能
消耗阶段
定时拉取一段数据批量处理,处理之后上报消耗位点,继续计算

内部好做数据的幂等控制,发布过程中的抖动或者单机故障包管数据的不重复计算

为了提升DB的count性能,先用Hbase对异常数量做好累加,然后定时线程获取数据批量update

为了提升DB的配置查询性能,首次查询配置放入本地内存存储20分钟,数据更新之后内存失效

对于统计类的计算采用explorer存储,对于非布局化的异常明细采用Hbase存储,对于布局化且可靠性要求高的异常数据采用OB存储

1.然后对系统的性能做好压测和容量评估,演练数据是异常数据的3-5倍做好流量隔离,对管道进行拆分,消耗链路的线程池做好隔离

2.对于单点的计算模块做好冗余和故障转移, 采取限流等措施
限流本领,上报端采用开关控制限流和熔断

故障转移本领

3.对于系统内部可以提升的地方,可以参考高可用性能优化策略去逐个突破。
高性能设计总结

1. 架构设计
1.1 冗余本领
做好集群的三副本甚至五副本的自动复制,包管全部数据冗余成功场景,任务才可以继续实行,如果对可用性要求很高,可以低落副本数以及任务的提交一实行束缚。
冗余很轻易明白,如果一个系统的可用性为90%,两台呆板的可用性为1-0.1*0.1=99%,呆板越多,可用性会更高;对于DB这种对连接数有瓶颈的,我们必要在业务上做好分库分表也是一种冗余的水平扩展本领。
1.2 故障转移本领
部分业务场景对于DB的依赖性很高,在DB不可用的情况下,能不能转移到FO库或者先中断现场,保存上下文,对当前的业务场景上下文写入延迟队列,等故障恢复后再对数据进行消耗和计算。
有些不可抗力和第三方问题,可能会严重影响整个业务的可用性,因此要做好异地多话,冗余灾备以及定期演练。
1.3 系统资源隔离性
在异常处理的case中,常常会因为上游数据的大量上报导致队列阻塞,影响时效性,因此可以做好核心业务和非核心业务资源隔离,对于秒杀类的场景甚至可以单独摆设独立的集群支持业务。
如果A系统可用性90%,B系统的可用性40%,A系统某服务强依赖B系统,那么A系统的可用性为P(A|B), 可用性大大低落。
2. 事前防御
2.1 做好监控
对系统的CPU,线程CE、IO、服务调用TPS、DB计算耗时等设置合理的监控阈值,发现问题及时应急
2.2 做好限流/熔断/降级等
上游业务流量突增的场景,必要有一定的自我保护和熔断机制,条件是制止业务的强依赖,办理单点问题,在异常消耗链路中,对上游做了DRM管控,下游也有一定的快速泄洪本领,防止因为单业务异常拖垮整个集群导致不可用。
瞬间流量问题很轻易引发故障,一定要做好压测和熔断本领,秒杀类的业务镌汰对核心系统的强依赖,提前做好预案管控,对于缓存的雪崩等也要有一定的预热和保护机制。
同时有些业务开放了不合理的接口,采用爬虫等大量哀求web接口,也要有辨认和熔断的本领
2.3 提升代码质量
核心业务在大促期间做好封网、资金安全提前摆设核对自动验证代码的可靠性,编码符合规范等等,都是制止线上问题的防御措施;
代码的FullGC, 内存走漏都会引发系统的不可用,尤其是业务低峰期可能不明显,业务流量高的情况下性能会恶化,提前做好压测和代码Review。
3. 过后防御和恢复
事前做好可监控和可灰度,过后做好任何场景下的故障可回滚。
其他关于防御本领的另有:摆设过程中如何做好代码的平滑发布,问题代码呆板如何快速地摘流量;上下游系统调用的发布,如何包管依赖顺序;发布过程中,正常的业务已经在发布过的代码中实行,逆向操作在未发布的呆板中实行,如何包管业务划一性,都要有充分的思量。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

民工心事

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表