第17章 应用程序调优
本章将主要讲述应用程序调优的一些方法和步骤,应用程序调优的领域很广,本章主要关注的是涉及数据库方面的调优。
在进行性能分析之前,我们先要熟悉应用的角色,它是什么版本的,做什么的,它是什么类型的应用,它是如何配置的,是否有相关的官方和社区支持,比如 Bug库、邮件组。
我们了解的信息越全面,就越有助于我们进行诊断和调优。
17.1 程序访问调优
如果能够满足以下几个方面的要求,那么程序的访问调优会更顺利。
17.1.1 好的架构和程序逻辑
最好是能够通过架构层面尽量避免性能问题的发生。
如果你的物理部署无法满足预期的负载要求,或者应用软件的功能架构无法充分利用计算资源,那么,你无论怎么“调优”都无法带来理想的性能提升和扩展性。
生产实践中的性能问题更多地归根于系统的架构设计和应用程序的程序逻辑。
运行较长时间之后,MySQL经过了高度优化,性能往往已经很好了,
由于数据库的查询只占据了总体响应时间的很小一部分,优化数据库对于整体用户体验的改善并无太大用处,而更改业务逻辑往往是最直接、最有效的。
一个应用的功能模块图对于我们的调优将会很有帮助,通过查看应用的模块图和物理部署图,从数据流、数据交互的角度去理解和分析问题,往往能够发现架 构中存在的问题。
1.缓存
互联网应用往往有多级缓存,比如用户访问网站,可能要经过浏览器缓存、应用程序缓存、Web服务器缓存、Memcached之类的缓存产品、数据库缓存等。
缓存可以加速我们从更慢的存储设备中获取数据,可以改善用户体验,提高系统吞吐。
对于多级缓存来说,越是靠近用户,越是靠近应用,就越有效,也就是说,缓存要靠近数据的使用者,靠近工作被完成的环节,
显然,在浏览器的缓存中读取图片会比到Web服务器中读取图片高效得多。
到Squid缓存服务器中获取数据比实际去后端的Web服务器中获取数据要高效得多。
缓存的存在应该限定为保护不容易水平扩展的资源,如数据库的大量读取,或者提升用户体验,或者在靠近用户的城市部署图片缓存节点。
如果资源易被水平扩展,那么添加缓存层可能不是一个好主意。
缓存的设计目标是,用更快的存储介质存储更慢的存储介质上的数据,以加速响应。比如把磁盘的数据存放在内存中。
我们这里仅仅关注被放置在数据库前端的缓存产品,比如Redis、Memcached等产品。
我们所使用的缓存产品主要是为了扩充读能力,对于写入,则并没有多少帮助。
MySQL有InnoDB缓冲区,但是其更多地只是属于数据库的一个组件,
它的功能是把热点数据缓存在内存中,如果要访问数据,则存在解析开销,也可能需要从磁盘中去获取数据,因为缓存中的内容会因为不常被使用而被剔除出缓存。
由于InnoDB缓冲的最小单元是页,而不是基于记录,因此要缓存一条记录,可能要同时缓存许多不相干的记录,这样就会导致内存缓存的利用率比较差。
而对于Memcached之类的记录级的缓存来说,因为应用程序有目的性地缓存了自己所需要的数据,所示其效率一般来说是要高于基于页的InnoDB缓存。
而且分布式的Memcached集群可以配置成一个超大的缓存,相对于单个实例内的InnoDB缓存,其扩展性也无疑好得多了。
现实中,由于Memcached的引入可能会导致开发的复杂度上升,所以在项目初期,往往并没有引入Memcached等缓存产品,一般的单机MySQL实例也可以扛得住所有流量,
当项目规模扩大之后,读请求的处理可能会成为瓶颈,这个时候可以选择增加从库,或者使用Memcached等缓存产品来突破读的瓶颈。
缓存的指标有缓存命中率、缓存失效速率等。
缓存命中率指命中次数与总的访问次数的比值。
缓存失效速率指每秒的缓存未命中次数。
缓存命中率和性能的关系如图17-1所示。 图17-1 缓存命中率和性能的关系
由图17-1可以得知缓存命中率和性能的关系,98%~99%命中率的性能提升远远大于10%~11%命中率的性能提升。
这种非线性的图形,主要是因为缓存命中(cache hit)和缓存未命中(cache miss)所访问的存储的速度差异比较大而形成的,比如内存和磁盘。
由于这样一个非线性的图形,我们在模拟缓存故障的情况下要留意缓存的命中率,
如果缓存的命中率不高,那么即使缓存挂了,对后端数据库的冲击也不会很大,但是如果缓存命中率很高,那么如果缓存挂了,可能就会对后端的数据库造成很大冲击。
缓存失效率(cache miss rate),即每秒的非命中次数,由于没有命中缓存,此时需要从更慢的存储上去获取数据。
这个指标比较直观,有利于我们分析当应用没有命中缓存时,我们的存储系统能否承受冲击。
如果缓存命中率显得很高,但是每秒缓存未命中的次数也很高,那么性能一样会很差。
当我们使用缓存产品,我们要清楚以下几点。
缓存的内容。
缓存的数据量。
设定合适的过期策略。
设置合适的缓存粒度,建议对单个记录、单个元素进行缓存而不是对一个大集合进行缓存,如果要将整个集合对象数据进行缓存的话,获得其中某个具体元素的性能将会受到严重的影响。
为了提高吞吐率,减少网络回返,建议一次获取多条记录,如Memcached的mget方法。
稳定的性能和快速的性能往往一样重要。我们在设计缓存的时候,要考虑到未命中的时候,生成结果的代价,如果会导致偶尔访问的用户响应慢,那么请不要牺牲这部分很小比例的用户。
一般来说,Memcached属于被动缓存,我们也可以采取主动缓存的策略,预先生成一些访问最多的,生成代价最昂贵的内容到Memcached中。
由于序列化和反序列化需要一定的资源开销,当处于高并发高负载的情况下,可能要消耗大量的CPU资源,
对于一些序列化的操作一定要慎重,尤其是在处理复杂数据类型时,可能序列化的开销会成为整个系统的瓶颈。
注意事项具体如下:
如果缓存挂了怎么办?或者因为调整缓存,清空缓存,对数据库产生了冲击怎么办?
假设你的业务逻辑是,如果缓存挂了,就去后端的数据库中获取数据。那么很可能短时间内的流量远远超过了数据库的处理能力,导致数据库不能提供服务。
所以就需要考虑对于后端数据库的保护,你可能需要对你的应用服务降级使用,即关闭掉不重要的模块,以确保核心功能,或者对数据库进行限流。
更友好的方式是,在应用程序里通过锁的机制控制对数据库的并发访问。
限流指的是应用对请求有排队机制,如果队列超过了一定的长度就会触发限流,就会随机抛弃掉一些请求。
2.非结构化数据的存储
不要在数据库里存储非结构化的数据,如视频、音乐、图片等,可以考虑把这些文件存储在分布式文件系统上,数据库中存储地址即可。
3.隔离大任务
批量事务一般应该和实时事务相分离,因为MySQL不太擅长同时处理这两类任务。
有时我们会运行一些定时任务,这些任务很耗费资源,我们需要注意调度,减少对生产环境的影响,
比如更新Sphinx索引,需要定期去数据库中扫描大量记录, 可能短时间内会造成数据库负荷过高的问题。
对数据库进行的一些大操作,我们可以通过小批量操作的方式减少操作对生产系统的影响,比如下面的这个删除大量数据的例子。
delete * from table_name where ctime < '2014-12-12'。
执行SQL会删除千万级别的记录,由于删除的记录过多,可能会导致执行计划变为全表扫描,从而导致不能写入数据,影响生产环境。
优化方案具体步骤如下。
1)程序每次获取1万条符合条件的记录,SQL为“SELECT id FROMtable_name WHERE ctime<'2014-12-12'limit 0,10000”。
2)根据主键id删除记录。每批100条,然后线程休眠100毫秒,直到删除完步骤1)中查询到的所有id。
3)重复步骤1)和步骤2),直到执行“SELECT id FROMtable_name WHERE ctime<'2014-12-12'limit 0,10000”返回空结果集为止。
休眠100ms是为了限制删除的速率,减少操作对生产环境的影响。
有时这些大操作无法完全消除,但又占据了大量的资源,这时我们就可以通过系统资源控制的方式对应用进行限制。
4.应用程序相关数据库优化注意事项
以下将列举一些应用程序相关的数据库优化注意事项。
检查应用程序是否需要获取那么多的数据,是否必须扫描大量的记录,是否做了多余的操作。
评估某些操作是应该放在数据库中实现还是在应用中实现?不要在数据库中进行复杂的运算操作,比如应用就更适合做正则的匹配。
应用程序中是否有复杂的查询?有时将复杂的查询分解为多个小查询,效率会更高,可以得到更高的吞吐率。
有时框架中会使用许多无效的操作,比如检测数据库连接是否可用。应该设置相关参数减少这类查询。
17.1.2 好的监控系统和可视化工具
解决性能问题如果是临时的、紧急的,特别是在生产繁忙的时候,你往往会难以下手,因为在压力之下,可能会遗漏一些问题,或者没有得到最好的解决方案。
系统应该能够及时预警,在性能问题爆发之前就能够发现问题。
所以,你应该有一个好的监控系统。能够监控到数据流各个环节的延时响应。
应用服务需要考虑维护性,可以进行性能统计,了解哪些操作占据了最多资源,通过可视化工具检查性能,可以让我们能够观察应用正在做什么事情,如何优化应用以减少不需要的工作。
17.1.3 良好的灰度发布和降级功能
系统越来越复杂,各个组件之间互相调用,可能某个模块会导致整个系统不能提供服务。
我们有必要区分核心的业务和非核心的业务,理清各种模块之间的关系,
如果能够有针对性地停止或上线功能/模块/应用,屏蔽掉性能有问题的模块,保证核心基础功能的正常运行,
那么我们的整体系统将会更加稳健,可以更快地从故障中恢复。
所以,你最好有良好的灰度发布和降级功能,这点对大系统尤为重要。
17.1.4 合理地拆分代码
进行架构调整常用的一个技术是垂直拆分,这里不会严格区分拆分代码、垂直拆分和拆库。
对于复杂的业务,如果出现了因代码而导致的性能问题,那么可以先从物理上隔离服务再考虑代码优化,如部署更多独立的Web服务器或数据库副本以提供服务。
将数据库数据拆分到独立的实例(垂直拆分)或增加读库。
垂直拆分需要慎重,因为跨表的连接会变得很难。所以还是要看业务逻辑,如果表之间的关联很多很紧密,那么可能拆分数据库就不是一个好的方案。
拆分业务之后,需要把用户引导到新的程序或服务上,有诸多方式可以使用,
如更改域名、应用前端分发、302跳转,或者有些客户端有更多的功能,可以接受云端的指令,修改访问不同功能的域名。
拆分代码需要慎重,因为分离的多套代码,可能会需要更多的接口调用,需要更多的交互,增加了复杂性,应该视后续发展而定。
如果代码之间的划分并不是很清晰,一个需求来了,要互相提供接口,更改多套代码,那样往往就增加了复杂性,会影响开发效率。
所以具体拆分代码还是要看看后续的项目发展,想清楚应该如何拆分。
许多时候,是业务优先的,因此要先保证业务,增加更多的资源用于突发的负荷。
对于数据库中数据的拆分,可能也不是一步到位的,需要兼顾业务的正常运行,因为对数据的拆分,往往需要先进行代码/模块的拆分。
可以考虑的一种措施是,克隆一份数据的副本,先用拆分出来的模块读写副本,等代码稳定后,再删除不需要的数据。
模块降级是需要考虑的,模块的各自监控也是需要的。这样在发生故障的时候可以及时关闭一些出问题的模块,保证核心的业务服务。
升级的时候,也可以进行灰度升级,逐步打开一些功能开关。
17.2 应用服务器调优
数据库的前端一般是应用服务器,如果应用服务器得到了优化,也可以减少数据库的压力,使得整个系统的性能更好、可扩展性更好。
应用服务器的调优是一个很大的主题,本章节只是介绍一些基本的指引。
在调优之前,必须要清楚应用服务器的响应过程,细分为各个阶段,哪个阶段耗费的时间最多,就首先从那个部分着手进行优化。
每个应用服务器都有许多参数配置,我们在进行调优的时候,应尽量逐个对参数加以调整优化,这样可以更好地衡量调整的效果,如果参数优化没有效果,那就恢复原来的配置。
修改参数的数值,可以逐步调整参数的值,如果一次性调整得过多,那么可能你得不到一个最优的配置,或者推翻你自己的判断,逐步调整参数,你有更大的可能性最终得到一个性能良好的系统。
在应用程序所在的软硬件环境发生变动的时候,你应该重新审视以前所做的优化配置,看它们是否依然能够工作。
你应该清楚应用服务器的一些参数会如何影响到数据库的负荷,清楚哪些参数会导致数据库的连接数增加,
由于一些连接池或框架的行为,大量的连接会导致过多地检测数据库的命令,因此也需要加以留意。
小结:
应用程序调优往往是最见效的方式,通过减少不必要的工作或让工作执行得更快,我们可以大幅度地提升性能。
应用程序访问调优更多的是软件架构的范畴。
如果读者有兴趣,建议多阅读一些软件架构方面的著作。