系统性能优化的十大策略(强烈推荐,建议收藏)

点击关注公众号,实用技术文章及时了解 e546bafe96d1b310b2f02e1e8a41c6dd.png

10ms之后。

b2cea099a8c0c276038bf87067ebbfc1.png

缓存优化性能的原理和索引一样,是拿额外的存储空间换取查询时间。缓存无处不在,设想一下我们在浏览器打开这篇文章,会有多少层缓存呢/p>

  • 首先解析DNS时,浏览器一层DNS缓存、操作系统一层DNS缓存、DNS服务器链上层层缓存;

  • 发送一个GET请求这篇文章,服务端很可能早已将其缓存在KV存储组件中了;

  • 即使没有击中缓存,数据库服务器内存中也缓存了最近查询的数据;

  • 即使没有击中数据库服务器的缓存,数据库从索引文件中读取,操作系统已经把热点文件的内容放置在Page Cache中了;

  • 即使没有击中操作系统的文件缓存,直接读取文件,大部分固态硬盘或者磁盘本身也自带缓存;

  • 数据取到之后服务器用模板引擎渲染出HTML,模板引擎早已解析好缓存在服务端内存中了;

  • 历经数十毫秒之后,终于服务器返回了一个渲染后的HTML,浏览器端解析DOM树,发送请求来加载静态资源;

  • 需要加载的静态资源可能因Cache-Control在浏览器本地磁盘和内存中已经缓存了;

  • 即使本地缓存到期,也可能因Etag没变服务器告诉浏览器304 Not Modified继续缓存;

  • 即使Etag变了,静态资源服务器也因其他用户访问过早已将文件缓存在内存中了;

  • 加载的JS文件会丢到JS引擎执行,其中可能涉及的种种缓存就不再展开了;

  • 整个过程中链条上涉及的所有的计算机和网络设备,执行的热点代码和数据很可能会载入CPU的多级高速缓存。

这里列举的仅仅是一部分常见的缓存,就有多种多样的形式:从廉价的磁盘到昂贵的CPU高速缓存,最终目的都是用来换取宝贵的时间。

既然缓存那么好,那么问题就来了:缓存是“银弹”吗/p>

不,Phil Karlton 曾说过:

There are only two hard things in Computer Science: cache invalidation and naming things.

计算机科学中只有两件困难的事情:缓存失效和命名规范。

缓存的使用除了带来额外的复杂度以外,还面临如何处理缓存失效的问题。

  • 多线程并发编程需要用各种手段(比如Java中的synchronized volatile)防止并发更新数据,一部分原因就是防止线程本地缓存的不一致;

  • 缓存失效衍生的问题还有:缓存穿透、缓存击穿、缓存雪崩。解决用不存在的Key来穿透攻击,需要用空值缓存或布隆过滤器;解决单个缓存过期后,瞬间被大量恶意查询击穿的问题需要做查询互斥;解决某个时间点大量缓存同时过期的雪崩问题需要添加随机TTL;

  • 热点数据如果是多级缓存,在发生修改时需要清除或修改各级缓存,这些操作往往不是原子操作,又会涉及各种不一致问题。

除了通常意义上的缓存外,对象重用的池化技术,也可以看作是一种缓存的变体。

常见的诸如JVM,V8这类运行时的常量池、数据库连接池、HTTP连接池、线程池、Golang的sync.Pool对象池等等。

在需要某个资源时从现有的池子里直接拿一个,稍作修改或直接用于另外的用途,池化重用也是性能优化常见手段。

压缩术

edef2f1b6e0a7539d6bbca9684d4bbe9.png

那么,有损压缩有哪些应用呢/p>

  • 预览和缩略图,低速网络下视频降帧、降清晰度,都是对信息的有损压缩;

  • 音视频等多媒体数据的采样和编码大多是有损的,比如MP3是利用傅里叶变换,有损地存储音频文件;jpeg等图片编码也是有损的。虽然有像WAV/PCM这类无损的音频编码方式,但多媒体数据的采样本身就是有损的,相当于只截取了真实世界的极小一部分数据;

  • 散列化,比如K-V存储时Key过长,先对Key执行一次“傻”系列(SHA-1、SHA-256)哈希算法变成固定长度的短Key。另外,散列化在文件和数据验证(MD5、CRC、HMAC)场景用的也非常多,无需耗费大量算力对比完整的数据。

除了有损/无损压缩,但还有一个办法,就是压缩的极端——从根本上减少数据或彻底删除。

能减少的就减少:

  • JS打包过程“摇树”,去掉没有使用的文件、函数、变量;

  • 开启HTTP/2和高版本的TLS,减少了Round Trip,节省了TCP连接,自带大量性能优化;

  • 减少不必要的信息,比如Cookie的数量,去掉不必要的HTTP请求头;

  • 更新采用增量更新,比如HTTP的PATCH,只传输变化的属性而不是整条数据;

  • 缩短单行日志的长度、缩短URL、在具有可读性情况下用短的属性名等等;

  • 使用位图和位操作,用风骚的位操作最小化存取的数据。典型的例子有:用Redis的位图来记录统计海量用户登录状态;布隆过滤器用位图排除不可能存在的数据;大量开关型的设置的存储等等。

能删除的就删除:

  • 删掉不用的数据;

  • 删掉不用的索引;

  • 删掉不该打的日志;

  • 删掉不必要的通信代码,不去发不必要的HTTP、RPC请求或调用,轮询改发布订阅;

终极方案:砍掉整个功能。

毕竟有位叫做 Kelsey Hightower 的大佬曾经说过:

No code is the best way to write secure and reliable applications. Write nothing; deploy nowhere

不写代码,是编写安全可靠的应用程序的最佳方式。什么都不写;哪里都不部署。

预取术

预取通常搭配缓存一起用,其原理是在缓存空间换时间基础上更进一步,再加上一次“时间换时间”,也就是:用事先预取的耗时,换取第一次加载的时间。

当可以猜测出以后的某个时间很有可能会用到某种数据时,把数据预先取到需要用的地方,能大幅度提升用户体验或服务端响应速度。

9723c7b54b6dbf78a3f0df0ff74010e9.png

削峰填谷的原理也是“时间换时间”,谷时换峰时。

削峰填谷与预取是反过来的:预取是事先花时间做,削峰填谷是事后花时间做。就像三峡大坝可以抗住短期巨量洪水,事后雨停再慢慢开闸防水。软件世界的“削峰填谷”是类似的,只是不是用三峡大坝实现,而是用消息队列、异步化等方式。

常见的有这几类问题,我们分别来看每种对应的解决方案:

  • 针对前端、客户端的启动优化或首屏优化:代码和数据等资源的延时加载、分批加载、后台异步加载、或按需懒加载等等。

  • 背压控制 – 限流、节流、去抖等等。一夫当关,万夫莫开,从入口处削峰,防止一些恶意重复请求以及请求过于频繁的爬虫,甚至是一些DDoS攻击。简单做法有网关层根据单个IP或用户用漏桶控制请求速率和上限;前端做按钮的节流去抖防止重复点击;网络层开启TCP SYN Cookie防止恶意的SYN洪水攻击等等。彻底杜绝爬虫、黑客手段的恶意洪水攻击是很难的,DDoS这类属于网络安全范畴了。

  • 针对正常的业务请求洪峰,用消息队列暂存再异步化处理:常见的后端消息队列Kafka、RocketMQ甚至Redis等等都可以做缓冲层,第一层业务处理直接校验后丢到消息队列中,在洪峰过去后慢慢消费消息队列中的消息,执行具体的业务。另外执行过程中的耗时和耗计算资源的操作,也可以丢到消息队列或数据库中,等到谷时处理。

  • 捋平毛刺:有时候洪峰不一定来自外界,如果系统内部大量定时任务在同一时间执行,或与业务高峰期重合,很容易在监控中看到“毛刺”——短时间负载极高。一般解决方案就是错峰执行定时任务,或者分配到其他非核心业务系统中,把“毛刺”摊平。比如很多数据分析型任务都放在业务低谷期去执行,大量定时任务在创建时尽量加一些随机性来分散执行时间。

  • 避免错误风暴带来的次生洪峰:有时候网络抖动或短暂宕机,业务会出现各种异常或错误。这时处理不好很容易带来次生灾害,比如:很多代码都会做错误重试,不加控制的大量重试甚至会导致网络抖动恢复后的瞬间,积压的大量请求再次冲垮整个系统;还有一些代码没有做超时、降级等处理,可能导致大量的等待耗尽TCP连接,进而导致整个系统被冲垮。解决之道就是做限定次数、间隔指数级增长的Back-Off重试,设定超时、降级策略。

批量处理术

c8638c0355b759b3b94f2d88b966d714.png 7f9d72ed84933fd949790b6d81c47ee4.png

因此,即使现代的硬件水平已经强悍到如此境地,性能优化仍然是有必要的。

软件日益复杂,抽象层级越来越高,就越需要底层基础设施被充分优化。

对于大部分开发者而言,高层代码逐步走向低代码化、可视化,“一行代码”能产生的影响也越来越大,写出低效代码则会吃掉更多的硬件资源。

下篇

引言

本篇也是本系列最硬核的一篇,本人技术水平有限,可能存在疏漏或错误之处,望斧正。仍然选取了《火影忍者》的配图和命名方式帮助理解:

  • 八门遁甲 —— 榨干计算资源

  • 影分身术 —— 水平扩容

  • 奥义 —— 分片术

  • 秘术 —— 无锁术

(注:这些“中二”的前缀仅是用《火影》中的一些术语,形象地描述技术方案)

八门遁甲 —— 榨干计算资源

45b6c0dde1f8c9df2959a4f721388184.png
  • 多副本

  • 水平扩容的前提是无状态

  • 读>>写, 多个读实例副本 (CDN)

  • 自动扩缩容,根据常用的或自定义的metrics,判定扩缩容的条件,或根据CRON

  • 负载均衡策略的选择

奥义 —— 分片术

水平扩容针对无状态组件,分片针对有状态组件。二者原理都是提升并行度,但分片的难度更大。

负载均衡也不再是简单的加权轮询了,而是进化成了各个分片的协调器

19994fc584bb1025a8c11ccf604de3b5.png

有些业务场景,比如库存业务,按照正常的逻辑去实现,水平扩容带来的提升非常有限,因为需要锁住库存,扣减,再解锁库存。

票务系统也类似,为了避免超卖,需要有一把锁禁锢了横向扩展的能力。

不管是单机还是分布式微服务,锁都是制约并行度的一大因素。比如上篇提到的秒杀场景,库存就那么多,系统超卖了可能导致非常大的经济损失,但用分布式锁会导致即使服务扩容了成千上万个实例,最终无数请求仍然阻塞在分布式锁这个串行组件上了,再多水平扩展的实例也无用武之地。

避免竞争Race Condition 是最完美的解决办法。

上篇说的应对秒杀场景,预取库存就是减轻竞态条件的例子,虽然取到服务器内存之后仍然有多线程的锁,但锁的粒度更细了,并发度也就提高了。

  • 线程同步锁

  • 分布式锁

  • 数据库锁 update select子句

  • 事务锁

  • 顺序与乱序

  • 乐观锁/无锁 CAS Java 1.8之后的ConcurrentHashMap

  • pipeline技术 – CPU流水线 Redis Pipeline 大数据分析 并行计算

  • TCP的缓冲区排头阻塞 QUIC HTTP3.0

总结

以ROI的视角看软件开发,初期人力成本的投入,后期的维护成本,计算资源的费用等等,选一个合适的方案而不是一个性能最高的方案。

本篇结合个人经验总结了常见的性能优化手段,这些手段只是冰山一角。在初期就设计实现出一个完美的高性能系统是不可能的,随着软件的迭代和体量的增大,利用压测,各种工具(profiling,vmstat,iostat,netstat),以及监控手段,逐步找到系统的瓶颈,因地制宜地选择优化手段才是正道。

有利必有弊,得到一些必然会失去一些,有一些手段要慎用。Linux性能优化大师Brendan Gregg一再强调的就是:切忌过早优化、过度优化。

持续观测,做80%高投入产出比的优化。

除了这些设计和实现时可能用到的手段,在技术选型时选择高性能的框架和组件也非常重要。

另外,部署基础设施的硬件性能也同样,合适的服务器和网络等基础设施往往会事半功倍,比如云服务厂商提供的各种字母开头的instance,网络设备带宽的速度和稳定性,磁盘的I/O能力等等。

多数时候我们应当使用更高性能的方案,但有时候甚至要故意去违背它们。最后,以《Effective Java》第一章的一句话结束本文吧。

首先要学会基本的规则,然后才能知道什么时候可以打破规则。

来源:code2life.top/2020/08/15/0055-performance

推荐

Java面试题宝典

技术内卷群,一起来学习!!

295f1866f641f17b50cd5d62285c85f9.gif

文章知识点与官方知识档案匹配,可进一步学习相关知识算法技能树首页概览34202 人正在系统学习中

来源:Java知音_

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年10月18日
下一篇 2022年10月18日

相关推荐