当前位置: 首页> 就业园地

Web项目的性能优化指南(三)

2019-06-18 14:47:59更新

关键字:Java mysql 数据库 SQL javaee JavaScript js vue oracle redis springboot springcloud spring mybatis Hibernate JPA

 

一般性方法

缓存

没有什么性能问题是缓存解决不了的,如果有,那就再加一级缓存

缓存的本质是加速访问,访问的数据要么是其他数据的副本 -- 让数据离用户更近;要么是之前的计算结果 -- 避免重复计算.

缓存需要用空间换时间,在缓存空间有限的情况下,需要优秀的置换换算来保证缓存有较高的命中率。

数据的缓存

这是我们最常见的缓存形式,将数据缓存在离使用者更近的地方。比如操作系统中的CPU cache、disk cache。对于一个web应用,前端会有浏览器缓存,有CDN,有反向代理提供的静态内容缓存;后端则有本地缓存、分布式缓存。

数据的缓存,很多时候是设计层面的考虑。

对于数据缓存,需要考虑的是缓存一致性问题。对于分布式系统中有强一致性要求的场景,可行的解决办法有lease,版本号。

计算结果的缓存

对于消耗较大的计算,可以将计算结果缓存起来,下次直接使用。

我们知道,对递归代码的一个有效优化手段就是缓存中间结果,lookup table,避免了重复计算。python中的method cache就是这种思想。

对于可能重复创建、销毁,且创建销毁代价很大的对象,比如进程、线程,也可以缓存,对应的缓存形式如单例、资源池(连接池、线程池)。

对于计算结果的缓存,也需要考虑缓存失效的情况,对于pure function,固定的输入有固定的输出,缓存是不会失效的。但如果计算受到中间状态、环境变量的影响,那么缓存的结果就可能失效,比如这篇文章讲到的:

并发

一个人干不完的活,那就找两个人干。并发既增加了系统的吞吐,又减少了用户的平均等待时间。

这里的并发是指广义的并发,粒度包括多机器(集群)、多进程、多线程。

对于无状态(状态是指需要维护的上下文环境,用户请求依赖于这些上下文环境)的服务,采用集群就能很好的伸缩,增加系统的吞吐,比如挂载nginx之后的web server

对于有状态的服务,也有两种形式,每个节点提供同样的数据,如mysql的读写分离;每个节点只提供部分数据,如mongodb中的sharding

分布式存储系统中,partition(sharding)和replication(backup)都有助于并发。

绝大多数web server,要么使用多进程,要么使用多线程来处理用户的请求,以充分利用多核CPU,再有IO阻塞的地方,也是适合使用多线程的。比较新的协程(Python greenle、goroutine)也是一种并发。

惰性

将计算推迟到必需的时刻,这样很可能避免了多余的计算,甚至根本不用计算,参见:

CopyOnWrite这个思想真牛逼!

批量,合并

在有IO(网络IO,磁盘IO)的时候,合并操作、批量操作往往能提升吞吐,提高性能。

我们最常见的是批量读:每次读取数据的时候多读取一些,以备不时之需。如GFS client会从GFS master多读取一些chunk信息;如分布式系统中,如果集中式节点复杂全局ID生成,俺么应用就可以一次请求一批id。

特别是系统中有单点存在的时候,缓存和批量本质上来说减少了与单点的交互,是减轻单点压力的经济有效的方法

在前端开发中,经常会有资源的压缩和合并,也是这种思想。

当涉及到网络请求的时候,网络传输的时间可能远大于请求的处理时间,因此合并网络请求就很有必要,比如mognodb的bulk operation,redis 的pipeline。写文件的时候也可以批量写,以减少IO开销,GFS中就是这么干的

更高效的实现

同一个算法,肯定会有不同的实现,那么就会有不同的性能;有的实现可能是时间换空间,有的实现可能是空间换时间,那么就需要根据自己的实际情况权衡。

程序员都喜欢造轮子,用于练手无可厚非,但在项目中,使用成熟的、经过验证的轮子往往比自己造的轮子性能更好。当然不管使用别人的轮子,还是自己的工具,当出现性能的问题的时候,要么优化它,要么替换掉他。

比如,我们有一个场景,有大量复杂的嵌套对象的序列化、反序列化,开始的时候是使用python(Cpython)自带的json模块,即使发现有性能问题也没法优化,网上一查,替换成了ujson,性能好了不少。

上面这个例子是无损的,但一些更高效的实现也可能是有损的,比如对于python,如果发现性能有问题,那么很可能会考虑C扩展,但也会带来维护性与灵活性的丧失,面临crash的风险。

缩小解空间

缩小解空间的意思是说,在一个更小的数据范围内进行计算,而不是遍历全部数据。最常见的就是索引,通过索引,能够很快定位数据,对数据库的优化绝大多数时候都是对索引的优化。

如果有本地缓存,那么使用索引也会大大加快访问速度。不过,索引比较适合读多写少的情况,毕竟索引的构建也是需有消耗的。

另外在游戏服务端,使用的分线和AOI(格子算法)也都是缩小解空间的方法。

性能优化与代码质量

很多时候,好的代码也是高效的代码,衡量代码质量的标准是可读性、可维护性、可扩展性,但性能优化有可能会违背这些特性,比如为了屏蔽实现细节与使用方式,我们会可能会加入接口层(虚拟层),这样可读性、可维护性、可扩展性会好很多,但是额外增加了一层函数调用,如果这个地方调用频繁,那么也是一笔开销;又如前面提到的C扩展,也是会降低可维护性、

这种有损代码质量的优化,应该放到最后,不得已而为之,同时写清楚注释与文档。

为了追求可扩展性,我们经常会引入一些设计模式,如状态模式、策略模式、模板方法、装饰器模式等,但这些模式不一定是性能友好的。所以,为了性能,我们可能写出一些反模式的、定制化的、不那么优雅的代码,这些代码其实是脆弱的,需求的一点点变动,对代码逻辑可能有至关重要的影响,所以还是回到前面所说,不要过早优化,不要过度优化。

 

首页 课程设置 师资力量 学习园地 就业园地 关于我们

地址:沈阳市和平区三好街54号辽宁物产科贸大厦7层

咨询热线:024-28667511

版权所有 © 2008-2019 沈阳爱尚教育科技w优德88 com学校

辽ICP备17004151号-1