高性能MySQL读书摘要(十四)- 应用层优化(缓存)

14.1常见问题

下面是我们经常会碰到的问题清单,通过这些过程可以激发你的思维。
• 什么东西在消耗系统中每台主机的CPU、磁盘、网络,以及内存资源?这些值是否合理?如果不合理,对应用程序做基本的检査,看什么占用了资源。配置文件通常是解决问题最简单的方式。例如,如果Apache因为创建1000个需要50MB内存的工作进程而导致内存溢出,就可以配置应用程序少使用一些Apache工作进程。也可以配置每个进程少使用一些内存。
• 应用真的需要所有获取到的数据吗?获取1000行数据但只显示10行,而丢弃剩下的990行,这是常见的错误。(如果应用程序缓存了另外的990行备用,这也许是有意的优化。)
• 应用在处理本应由数据库处理的事情吗,或者反过来?这里有两个例子,从表中获取所有的行在应用中进行统计计数,或者在数据库中执行复杂的字符串操作。数据库擅长统计计数,而应用擅长正则表达式。要善于使用正确的工具来完成任务。
• 应用执行了太多的査询? ORM宣称的把程序员从写SQL中解放出来的语句接口通常是罪魁祸首。数据库服务器为从多个表匹配数据做了很多优化,因此应用程序完全可以删掉多余的嵌套循环,而使用数据库的关联来代替。
• 应用执行的査询太少了?好吧,上面只说了执行太多SQL可能成为问题。但是,有时候让应用来做“手工关联”以及类似的操作也可能是个好主意。因为它们允许更细的粒度控制和更有效的使用缓存,以及更少的锁争用,甚至有时应用代码里模拟的哈希关联会更快(MySQL的嵌套循环的关联方法并不总是高效的)。
• 应用创建了没必要的MySQL连接吗?如果可以从缓存中获得数据,就不要再连接数据库。
• 应用对一个MySQL实例创建连接的次数太多了吗(也许因为应用的不同部分打开了它们自己的连接)?通常来说更好的办法是重用相同的连接。
• 应用做了太多的“垃圾”査询? 一个常见的例子是发送査询前先发送一个ping命令看数据库是否存活,或者每次执行SQL前选择需要的数据库。总是连接到一个特定的数据库并使用完整的表名也许是更好的方法。(这也使得从日志或者通过SHOW
PROCESSLIST看SQL更容易了,因为执行日志中的SQL语句的时候不用再切换到特定的数据库,数据库名已经包含在SQL语句中了。)“预备(Preparing)”连接是另一个常见问题。Java驱动在预备期间会做大量的操作,其中大部分可以禁用。另一个常见的垃圾査询是SET NAMES UTF8,这是一个错误的方法(它不会改变客户端库的字符集,只会影响服务器的设置)。如果应用在大部分情况使用特定的字符集工作,可以修改配置文件把特定字符集设为默认值,而不需要在每次执行时去做修改。
• 应用使用了连接池吗?这既可能是好事,也可能是坏事。连接池可以帮助限制总的连接数,有大量SQL执行的时候效果不错(Ajax应用是一个典型的例子)。然而,连接池也可能有一些副作用,比如说应用的事务、临时表、连接相关的配置项,以
及用户自定义变量之间相互干扰等。
• 应用是否使用长连接?这可能导致太多连接。通常来说长连接不是个好主意,除非网络环境很慢导致创建连接的开销很大,或者连接只被一或两个很快的SQL使用,或者连接频率很高导致客户端本地端口不够用。如果MySQL的配置正确,也许就不需要长连接了。比如使用skip-name-resolve来避免DNS反向査询,确保 thread_cache足够大,并且增加back_log。可以参考第8章和第9章得到更多的细节。
• 应用是否在不使用的时候还保持连接打开?如果是这样,尤其是连接到很多服务器时,可能会过多地消耗其他进程所需要的连接。例如,假设你连接到10个MySQL服务器。从一个Apache进程中获取10个连接不是问题,但是任意时刻其中只有1个在真正工作。其他9个大部分时间都处于Sleep状态。如果其中一台服务器变慢了,或者有一个很长的网络请求,其他的服务器就可能因为连接数过多受到影响。解决方案是控制应用怎么使用连接。例如,可以将操作批量地依次发送到每个MySQL实例,并且在下一次执行SQL前关闭每个连接。如果执行的是比较消耗时间的操作,例如调用Web服务接口,甚至可以先关闭MySQL连接,执行耗时的工作,再重新打开MySQL连接继续在数据库上工作。

14.2 Web服务器问题

当一个请求完成后,会释放大部分内存给操作系统,但并不是全部。Apache会保持进程处于打开状态以备后来的请求重用。这意味着,如果下一个请求是请求静态文件,比如一个CSS文件或者一张图片,就会出现一个占用内存很多的进程来为一个很小的请求服务的情况。这就是使用Apache作为通用Web服务器很危险的原因。它的确是为通用目的而设计的,但如果能够有针对性地使用其长处,会获得更好的性能。

不要使用Apache来做静态内容服务,或者至少和动态服务使用不同的Apache实例。流行的替代品有 Nginx(http://www.nginx.com)和 lighttpd (http://www.lighttpd.net)。
• 使用缓存代理服务器,比如Squid或者Varnish,防止所有的请求都到达Web服务器。这个层面即使不能缓存所有页面,也可以缓存大部分页面,并且使用像ESI (Edge Side Includes,参见加切:〃www.esf.org)这样的技术来将部分页面中的小块的动态内容嵌入到静态缓存部分。
• 对动态和静态资源都设置过期策略。可以使用Squid这样的缓存代理显式地使内容过期。维基百科就使用了这个技术来清理缓存中变更过的文章。

有时也许还需要修改应用程序,以便得到更长的过期时间。例如,如果你告诉浏览器永久缓存CSS和JavaScript文件,然后对站点的HTML做了一个修改,这个页面渲染将会出问题。这种情况可以为文件的毎个版本设定唯一的文件名。例如,你可以定制网站的发布脚本,复制CSS文件到/css/123_frontpage.css,这里的Z23就是版本管理器中的版本号。对图片文件的文件名也可以这么做——永不重用文件名,这样页面就不会在升级时出问题,浏览器缓存多久的文件都没问题。
• 不要让Apache填鸭式地服务客户端,这不仅仅会导致慢,也会导致DDoS攻击变得简单。硬件负载均衡器通常可以做缓冲,所以Apache可以快速地完成,让负载均衡器通过缓存响应客户端的请求,也可以在应用服务器前端使用Nginx、Squid或
者事件驱动模式下的Apache。
• 打开gzip压缩。对于现在的CPU而言这样做的代价很小,但是可以节省大部分流量。如果想节省CPU周期,可以使用缓存,或者诸如Nginx这样的轻量级服务器保存压缩过的页面版本。
• 不要为用于长距离连接的Apache配置启用Keep-Alive选项,因为这会使得重量级的Apache进程存活很长时间。可以用服务器端的代理来处理保持连接的工作,从而防止Apache被客户端拖垮。配置Apache到代理之间的连接使用Keep-Alive是可以的,因为代理只会使用很少的Apache连接去获取数据。图14-1展示了这个区别。推荐使用此方式。
在这里插入图片描述
这些策略可以使Apache进程存活时间变得很短,所以会有比实际需求更多的进程。无论如何,有些操作依然可能导致Apache进程存活时间太长,并且占用大量资源。举个例子,一个请求査询延时非常大的外部资源,例如远程的Web服务,就会出现Apache进程存活时间太长的问题。这种问题通常是无解的。

14.2.1 寻找最优并发度

每个Web服务器都有一个最佳并发度—就是说,让进程处理请求尽可能快,并且不超过系统负载的最优的并发连接数。这就是我们在第11章说的最大系统容量。进行一个简单的测量和建模,或者只是反复试验,就可以找到这个“神奇的数”,为此花一些时间是值得的。

14.3 缓存

采用缓存通常可以获得数量级的性能提升。诀窍是找到正确的粒度和缓存过期策略组合。另外也需要决定哪些内容适合缓存,缓存在哪里。

典型的高负载应用汇有很多层缓存(多级缓存)。缓存并不仅仅发生在服务器上,而是在每一个环节,甚至包括用户的Web浏览器(这就是内容过期头的用处)。通常,缓存越接近客户端,就越节省资源并且效率更高。从浏览器缓存提供一张图片比从Web服务器的内存获取快得多,而从服务器的内存读取又比从服务器的磁盘上读取好得多。每种类型的缓存有其不一样的特点,例如容量和延时;在后面的章节我们会解释其中的一部分。

可以把缓存分成两大类:被动缓存和主动缓存。被动缓存除了存储和返回数据外不做任何事情。当从被动缓存请求一些内容时,要么可以得到结果,要么得到“结果不存在”。被动缓存的一个典型例子是memcached。

相比之下,主动缓存会在访问未命中时做一些额外的工作。通常会将请求转发送给应用的其他部分来生成请求结果,然后存储该结果并返回给应用。Squid缓存代理服务器就是一个主动缓存。

设计应用程序时,通常希望缓存是主动的(也可以叫做透明的),因为它们对应用隐藏了检査一生成一存储这个逻辑过程。也可以在被动缓存的前面构建一个主动缓存。

14.3.1应用层以下缓存

缓存并不总是有用
必须确认缓存真的可以提升性能,因为有时缓存可能没有任何帮助。例如,在实践中发现从Nginx的内存中获取内容比从缓存代理中获取要快。如果代理的缓存在磁盘上则尤其如此。
原因很简单:缓存自身也有一些开销。比如检查缓存是否存在,如果命中则直接从缓存中返回数据。另外将缓存对象失效或者写入新的缓存对象都会有开销。缓存只在这些开销比没有缓存的情况下生成和提供数据的开销少时才有用。
如果知道所有这些操作的开销,就可以计算出缓存能提供多少帮助。没有缓存时的开销就是为每冷■请求生成数据的开销。有缓存时的开销是检查缓存的开销加上缓存不命中的概率乘以生成数据的开销,再加上缓存命中的概率乘以缓存提供数据的开销。

如果有煖存时的开销比没有时要低,则说明缓存可能有用,但依然不能保证。还要记住,就像从Nginx的内存中获取数据比从代理在磁盘中的缓存获取要好一样,有些绕存的开销比另外一些要低。

14.3.2应用层缓存

应用层缓存通常在同一台机器的内存中存赭数据,或者通过网络存在另一台机器的内存中。
因为应用可以缓存部分计算结果,所以应用层缓存可能比更低层次的缓存更有效。因此,应用层缓存可以节省两方面的工作:获取数据以及基于这些数据进行计算。一个很好的例子是HTML文本块。应用程序可以生成例如头条新闻的标题这样的HTML片段,并且做好缓存。后续的页面视图就可以简单地插入这个缓存过的文本。一般来说,在缓存数据前对数据做的处理越多,缓冲命中节省的工作就越多。但应用层缓存也有缺点,,那就是缓存命中率可能更低,并且可能使用较多的内存。假设
需要50个不同版本的头条新闻标题,以使不同地区生活的用户看到不同的内容,那就需要足够的内存去存储全部50个版本,任何给定版本的标题命中次数都会更少,并且失效策略也会更加复杂。

应用缓存有许多种,下面是其中的一小部分。

本地缓存
这种缓存通常很小,只在进程处理请求期间存在于进程内存中。本地缓存可以有效地避免对某些资源的重复请求。这种类型的缓存技术并不复杂:通常只是应用代码中的一个变量或者哈希表。
本地共享内存缓存
这种缓存一般是中等大小(几个GB),快速,难以在多台机器间同步。它们对小型的半静态位数据比较合适。例如每个州的城市列表,分片数据存储的分区函数(映射表),或者使用存活时间(TTL)策略进行失效的数据等。共享内存最大的好处是访问非常快—通常比其他任何远程缓存访问都要快不少。
分布式内存缓存

最常见的分布戎内存缓存的例子是memcached。分布式缓存比本地共享内存缓存要大得多,增长也容易。缓存中创建的数据的每一个比特都只有一份副本,这样既不会浪费内存,也不会因为相同的数据存在不同的地方而引入一致性问题。分布式内
存非常适合存储共享对象,例如用户资料、评论,以及HTML片段。
分布式缓存比本地共享缓存的延时要高得多,所以最高效的使用方法是批量进行多个获取操作(例如,在一次循环中获取多个对象)。分布式缓存还需要考虑怎么增加更多的节点,以及某个节点崩溃了怎么处理。对于这两个场景,应用程序必须决定在节点间怎么分布或重分布缓存对象。当缓存集群增加或减少一台服务器时,一致性缓存对避免性能问题而言是非常重要的。在下面这个网站有一个为memcached做的一致性缓存库:http://www.audioscrobbler.net/development/ketama/B
磁盘上的缓存
磁盘是很慢的,所以缓存在磁盘上的最好是持久化对象,很难全部装进内存的对象,或者静态内容(例如预处理的自定义图片)。

14.3.3缓存控制策略

缓存也有像反范式化数据库设计一样的问题:重复数据,也就是说有多个地方需要更新数据,所以需要想办法避免读到脏数据。下面是一些最常见的缓存控制策略。

TTL (time to live,存活时间)
缓存对象存储时设置一个过期时间;可以通过清理进程在达到过期时间后删掉对象,或者先留着直到下次访问时再清理(清理后需要使用新的版本替换)。对于数据很少变更或者没有新数据的情况,这是最好的失效策略。

显式失效
如果不能接受脏数据,那么进程在更新原始数据时需要同时使缓存失效。这种策略有两个变种:写一失效和写一更新。写一失效策略很简单:只需要标记缓存数据已经过期(是否清理缓存数据是可选的)。写一更新策略需要多做一些工作,因为在更新数据时就需要替换掉缓存项。无论如何,这都是非常有益的,特别是当生成缓存数据代价很昂贵时(写线程也许已经做了)。如果更新缓存数据,后续的请求将不再需要等待应用来生成。如果在后台做失效处理,例如基于TTL的失效,就可以在一个从用户请求完全分离出来的进程中生成失效数据的新版本。

读时失效
在更改旧数据时,为了避免要同时失效派生出来的脏数据,可以在缓存中保存一些信息,当从缓存中读数据时可以利用这些信息判断数据是否已经失效。和显式失效策略相比,这样做有很大的优势:成本固定且可以分散在不同时间内。假设要失效一个有一百万缓存对象依赖的对象,如果采用写时失效,需要一次在缓存中失效一百万个对象,即使有高效的方法来找到这些对象,也可能需要很长的时间才能完成。如果采用读时失效,写操作可以立即完成,但后续这一百万对象的读操作可能会有略微的延迟。这样就把失效一百万对象的开销分散了,并且可以帮助避免出现负载冲高和延迟增大的峰值。
一种最简单的读时失效的办法是采用对象版本控制。使用这种方法,在缓存中存储一个对象时,也可以存储对象所依赖的数据的当前版本号或者时间戳。例如,假设要缓存用户博客日志的统计信息,包括用户发表的博客数。当缓存blog_stats对象时,也可以同时存储用户的当前版本号,因为该统计信息是依赖于用户的。

不管什么时候更新依赖于用户的数据,都需要更新用户的版本号,假设用户的版本号初始为0,井且由你来生成和缓存统计信息。当用户发表了一篇博客,就增加用户的版本号到1 (当然也要同时存储这篇博客,尽管在这个例子并没有用到博客数据)。然后当需要显示统计数据的时候,可以对缓存中blog_stats对象的版本与缓存的用户版本进行比较。因为用户的版本比对象的版本高,所以可以知道缓存的统计信息已经过期了,需要重新计算。
这是一个非常粗糙的内容失效方式,因为它假设依赖于用户的每一个比特的数据与所有其他数据都有交互。但这个假设并不总是成立的。举个例子,如果一个用户对一篇博客做了编辑,你也需要增加用户的版本号,这就会导致存储的统计信息失效,而实际上统计信息(发表的博客数)并没真的改变。这个取舍是很简单的。一个简单的缓存失效策略不只是更容易创建,也可能更加高效。
对象版本控制是一种简单的标记缓存方法,它可以处理更复杂的依赖关系。一个标记的缓存可以识别不同类型的依赖,并且分别跟踪每个依赖的版本。回到第11章中图书俱乐部的例子,你可以通过下面的版本号标记评论,使缓存的评论依赖于用户的版本和书的版本:user_ver=1234和book_ver=5678。任一版本号变了,都应该刷新缓存的评论。

14.3.4缓存对象分层

这对理解远程缓存访问的花销是多么昂贵非常重要。虽然缓存很快,也可以避免很多工作,但在LAN环境下网络往返缓存服务器通常也需要0.3ms左右。我们见过很多案例,复杂的网页需要一千次左右的缓存访问来组合页面结果,这将会耗费3s左右的网络延时,意味着你的页面可能慢得不可接受,即使它甚至不需要访问数据库!因此,在这种情况下对缓存使用批量获取调用是非常重要的。对缓存进行分层,采用小一些的本地缓存,也可能获得很大的收益。

14.3.5预生成內容

预生成内容有几个重要的好处。
• 应用代码没有复杂的命中和未命中处理路径。
• 当未命中的处理路径慢得不可接受时,这种方案可以很好地工作,因为它保证了未命中的情况永远不会发生。实际上,在任何时候设计任何类型的缓存系统,总是应该考虑未命中的路径有多慢。如果平均性能提升很大,但是因为要预生成缓存内容,偶尔有一些请求变得非常缓慢,这时可能比不用缓存还糟糕。性能的持续稳定通常跟高性能一样重要。
• 预生成内容可以避免在缓存未命中时导致的雪崩效应。
缓存预生成好的内容可能占用大量空间,并且并不总能预生成所有东西。无论是哪种形式的缓存,需要预生成的内容中最重要的部分是那些最经常被请求,或者生成的成本最高的,所以可以通过本章前面提到的404错误处理机制来按需生成。
预生成的内容有时候也可以从内存文件系统中获益,因为可以避免磁盘I/O.

14.3.6作为基础组件的缓存

缓存有可能成为基础设施的重要组成部分。也很容易陷入一个陷阱,认为缓存虽然很好用,但并不是重要到非有不可的东西。你也许会辩驳,如果缓存服务器宕机或者缓存被清空,请求也可以直接落在数据库上,系统依然可以正常运行。如果是刚刚将缓存加入应用系统,这也许是对的,但是缓存的加入可以使得在应用压力显著增长时不需要对系
统的某些部分同比增加资源投入——通常是数据库部分。因此,系统可能慢慢地变得对缓存非常依赖,却没有被发觉。
例如,如果高速缓存命中率是90%,当由于某种原因失去缓存,数据库上的负载将增加到原来的10倍。这很可能导致压力超过数据库服务器的性能极限。
为了避免像这样的意外,应该设计一些高可用性缓存(包括数据和服务)的解决方案,或者至少是评估好禁用缓存或丢失缓存时的性能影响。比如说可以设计应用在遇到这样的情况时能够进行降级处理。

14.3.7 使用 HandlerSocket 和 memcached

相对于数据存储在MySQL中而缓存在MySQL外部的缓存方案,另外有一种替代方法是为MySQL创建一个更快的访问路径,直接绕过使用缓存。对于小而简单的査询语句,很大一部分开销来自解析SQL,检査权限,生成执行计划,等等。如果这种开销可以避免,MySQL在处理简单査询时将非常快。
目前有两个解决方案可以用所谓的NoSQL方式访问MySQL.第一种是一个后台进程插件,称为HandlerSocket,由DeNA开发,这是日本最大的社交网站。HandlerSocket允许通过一个简单的协议访问InnoDB Handler对象。实际上,也就是绕过了上层的服务器层,通过网络直接连接到了 InnoDB引擎层。有报告称HandlerSocket毎秒可以执行超过750 000条査询。Percona Server分支中自带了 HandlerSocket插件引擎层。
第二个方案是通过memcached协议访问InnoDB. MySQL 5.6的实验室版本有一个插件提供了这个接口。

14.4 MySQL的替代品

最后,对于某些操作——如图形关系和树遍历一关系型数据库并不总是正确的典范。MySQL并不擅长分布式数据处理,因为它.缺乏并行执行査询的能力。出于这些目的情况还是建议使用其他工具(可能与MySQL结合)。现在想到的例子包括:
• 对于简单的键一值存储,在复制严重落后的非常高速的访问场景中,我们建议用Redis替换MySQL.即使MySQL主库可以承担这样的压力,备库的延迟也是非常让人头疼的。Redis也常用来做队列,因为它对队列操作支持得很好。
• Hadoop是房间中的大象,一语双关。混合MySQL/Hadoop的部署在处理大型或半结构化数据时非常常见。

©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页