MySQL之针对应用层的优化


  如果在提高MySQL的性能上花费太多时间,容易使视野局限于MySQL本身,而忽略了用户体验。回过头来看,也许可以意识到,或许MySQL已经足够优化,对于用户看到的响应时间而言,其所占的比重已经非常之小,此时应该关注下其他部分了。这是个很不错的观点,尤其是对DBA而言,这是很值得去做的正确的事。但如果不是MySQL,那又是什么导致了问题呢?通过测量可以快速而准确地给出答案。如果能顺着应用的逻辑过程从头到尾来剖析,那么找到问题的源头一般来说并不困难。有时,尽管问题在MySQL上,也很容易在系统的另一部分得到解决。

  无论问题出在哪里,都至少可以找到一个靠谱的工具来帮助进行分析,而且通常是免费的。例如,如果有JavaScript或者页面道染的问题,可以使用包括Firefox浏览器的Firebug插件在内的调优工具,或者使用Yahoo!的YSlow工具。一些工具甚至可以剖析整个堆栈:New Relic是一个很好的例子,它可以剖析Web应用的前端、应用以及后端。

 

1.常见问题

  我们在应用中反复看到一些相同的问题,经常是因为人们使用了缺乏设计的现成系统或者简单开发的流行框架。虽然有时候可以通过这些框架更快更简单地构建系统,但是如果不清楚这些框架背后做了什么操作,反而会增加系统的风险。

  下面是我们经常会碰到的问题清单,通过这些过程可以激发你的思维。

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

  长连接和连接池的区别可能使人困惑。长连接可能跟连接池有同样的副作用,因为重用的连接在这两种情况下都是有状态的。

  然而,连接池通常不会导致服务器连接过多,因为它们会在进程间排队和共享连接。另一方面,长连接是在每个进程基础上创建,不会在进程间共享。

  连接池也比共享连接的方式对连接策略有更强的控制力。连接池可以配置为自动扩展,但是通常的实践经验是,当遇到连接池完全占满时,应该将连接请求进行排队而不是扩展连接池。这样做可以在应用服务器上进行排队等待,而不是将压力传递到MySQL数据库服务器上导致连接数太多而过载。

  有很多方法可以使得査询和连接更快,但是一般的规则是,如果能够直接避免进行査询和连接,肯定比努力提升査询和连接的性能能获得更好的优化结果。

 

2.Web服务器问题

  Apache是最流行的Web应用服务器软件。它在许多情况下都运行良好,但如果使用不当也会消耗大量的资源。最常见的问题是保持它的进程的存活(alive)时间过长,或者在各种不同的用途下混合使用,而不是分别对不同类型的工作进行优化。

  Apache通常是通过prefork配置来使用mod_php、mod_perl和mod_python模块的。prefork模式会为每个请求预分配进程。因为PHP、Perl和Python脚本是可以定制化的,每个进程使用50MB或100MB内存的情况并不少见。当一个请求完成后,会释放大部分内存给操作系统,但并不是全部。Apache会保持进程处于打开状态以备后来的请求重用。这意味着,如果下一个请求是请求静态文件,比如一个CSS文件或者一张图片,就会出现用一个占用内存很多的进程来为一个很小的请求服务的情况。这就是使用Apache作为通用Web服务器很危险的原因。它的确是为通用目的而设计的,但如果能够有针对性地使用其长处,会获得更好的性能。

  另一个主要的问题是,如果开启了Keep-Alive设置,进程可能很长时间处于繁忙状态。当然,即使没有开启Keep-Alive,某些进程也可能存活很久,“填鸭式”地将内容传给客户端可能导致获取数据很慢。

  人们常犯的另外一个错误,就是保持那些Apache默认开启的模块不动。

  最好能够精简Apache的模块,移除掉那些不需要的。这很简单:只需要检査Apache的配置文件,注释掉不想要的模块,然后重启Apache就行。也可以在文件中删除不使用的PHP模块。

  最差情况是,如果用一个通用目的的Apache配置直接用于Web服务,最后很可能产生很多重量级的Apache进程。这将浪费Web服务器的资源。它们还可能保持大量MySQL连接,浪费MySQL的资源。下面是一些可以降低服务器负载的方法

  不要使用Apache来做静态内容服务,或者至少和动态服务使用不同的Apache实例。流行的替代品有Nginx。

  • 使用缓存代理服务器,比如Squid或者Varnish,防止所有的请求都到达Web服务器。这个层面即使不能缓存所有页面,也可以缓存大部分页面,并且使用像ESI这样的技术来将部分页面中的小块的动态内容嵌入到静态缓存部分。
  • 对动态和静态资源都设置过期策略。可以使用Squid这样的缓存代理显式地使内容过期。维基百科就使用了这个技术来清理缓存中变更过的文章。

  有时也许还需要修改应用程序,以便得到更长的过期时间。例如,如果你告诉浏览器永久缓存CSS和JavaScript文件,然后对站点的HTML做了一个修改,这个页面渲染将会出问题。这种情况可以为文件的每个版本设定唯一的文件名。例如,你可以定制网站的发布脚本,复制CSS文件到/css/123_frontpage.css,这里的123就是版本管理器中的版本号。对图片文件的文件名也可以这么做——永不重用文件名,这样页面就不会在升级时出问题,浏览器缓存多久的文件都没问题。

  • 不要让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进程存活时间太长的问题。这种问题通常是无解的。

 

2.1寻找最优并发度

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

  对于大流量的网站,Web服务器同一时刻处理上千个连接是很常见的。然而,只有一小部分连接需要进程实时处理。其他的可能是读请求,处理文件上传,填鸭式服务内容,或者只是等待客户端的下一步请求。

  随着并发的增加,服务器会逐渐到达它的最大吞吐量。在这之后,吞吐量通常开始降低。更重要的是,响应时间(延迟)也会因为排队而开始增加。

  为什么会这样呢?试想,如果服务器只有一个CPU,同时接收到了 100个请求,会发生什么事情呢?假设CPU每秒能够处理一个请求。即便理想情况下操作系统没有调度的开销,也没有上下文切换的成本,那100个请求也需要CPU花费整整100s才能完成。

  处理请求的最好方法是什么?可以将其一个个地排到队列中,也可以并行地执行并在不同请求之间切换,每次切换都给每个请求相同的服务时间。在这两种情况下,吞吐量都是每秒处理一个请求。然而,如果使用队列(并发=1),平均延时是50s,如果是并发执行(并发=100)则是100s。在实践中,并发执行会使平均延时更高,主要是因为上下文切换的代价。

  对于CPU密集型工作负载,最佳并发度等于CPU数量(或者CPU核数)。然而,进程并不总是处于可运行状态的,因为会有一些阻塞式请求,例如I/0、数据库査询,以及网络请求。因此,最佳并发度通常会比CPU数量髙一些。

  可以预测最优并发度,但是这需要精确的分析。尝试不同的并发值,看看在不增加响应时间的情况下的最大吞吐量是多少,或者测量真正的工作负载并且进行分析,这通常更容易。Percona Toolkit的pt-tcp-model工具可以帮助从TCP转储中测量和建模分析系统的可扩展性和性能特性。

 

3.缓存

  缓存对高负载应用来说是至关重要的。一个典型的Web应用程序会提供大量的内容,直接生成这些内容的成本比采用缓存要高得多(包含检査和缓存超时的开销),所以采用缓存通常可以获得数量级的性能提升。诀窍是找到正确的粒度和缓存过期策略组合。另外也需要决定哪些内容适合缓存,缓存在哪里。

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

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

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

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

 

3.1 应用层以下的缓存

  MySQL服务器有自己的内部缓存,但也可以构建你自己的缓存和汇总表。可以对缓存表量身定制,使它们最有效地过滤、排序、与其他表关联、计数,或者用于其他用途。缓存表也比许多应用层缓存更持久,因为在服务器重启后它们还存在。

 

  讨论:缓存并不总是有用

  必须确认缓存真的可以提升性能,因为有时缓存可能没有任何帮助。例如,在实践中发现从Nginx的内存中获取内容比从缓存代理中获取要快。如果代理的缓存在磁盘上则尤其如此。

  原因简单:缓存自身也有一些开销。比如检查缓存是否存在,如果命中则直接从缓存中返回数据。另外将缓存对象失效或者写入新的缓存对象都会有开销。缓存只在这些开销比没有缓存的情况下生成和提供数据的开销少时才有用。

  如果知道所有这些操作的开销,就可以计算出缓存能提供多少帮助。没有缓存时的开销就是为每不请求生成数据的开销。有缓存时的开销是检查缓存的开销加上缓存不命中的概率乘以生成数据的开销,再加上缓存命中的概率乘以缓存提供数据的开销。

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

 

3.2 应用层缓存

  应用层缓存通常在同一台机器的内存中存储数据,或者通过网络存在另一台机器的内存中。

  因为应用可以缓存部分计算结果,所以应用层缓存可能比更低层次的缓存更有效。因此,应用层缓存可以节省两方面的工作:获取数据以及基于这些数据进行计算。一个很好的例子是HTML文本块。应用程序可以生成例如头条新闻的标题这样的HTML片段,并且做好缓存。后续的页面视图就可以简单地插入这个缓存过的文本。一般来说,在缓存数据前对数据做的处理越多,缓冲命中节省的工作就越多。

  但应用层缓存也有缺点,那就是缓存命中率可能更低,并且可能使用较多的内存。假设需要50个不同版本的头条新闻标题,以使不同地区生活的用户看到不同的内容,那就需要足够的内存去存储全部50个版本,任何给定版本的标题命中次数都会更少,并且失效策略也会更加复杂。

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

  本地缓存

这种缓存通常很小,只在进程处理请求期间存在于进程内存中。本地缓存可以有效地避免对某些资源的重复请求。这种类型的缓存技术并不复杂:通常只是应用代码中的一个变量或者哈希表。例如,假设需要显示一个用户名,而且已经知道其ID,就可以创建一个get_name_from_id()函数并且在其中增加缓存,像下面这样。

<?php
function get_name_from_id($user_id) {
   static $name; // static makes the variable persist
   if ( !$name ) {
      // Fetch name from database
   }
   return $name;
}
?>

如果使用的是Perl,那么Memoize模块是函数调用结果标准的缓存方式。

use Memoize qw(memoize);
memoize 'get_name_from_id';
sub get_name_from_id {
   my ( $user_id ) = @_;
   my $name = # get name from database
   return $name;
}

这些技巧都很简单,但却可以为应用程序节省很多工作。

  本地共享内存缓存

这种缓存一般是中等大小(几个GB),快速,难以在多台机器间同步。它们对小型的半静态位数据比较合适。例如每个州的城市列表,分片数据存储的分区函数(映射表),或者使用存活时间(TTL)策略进行失效的数据等。共享内存最大的好处是访问非常快——通常比其他任何远程缓存访问都要快不少。

  分布式内存缓存

最常见的分布式内存缓存的例子是memcached。分布式缓存比本地共享内存缓存要大得多,增长也容易。缓存中创建的数据的每一个比特都只有一份副本,这样既不会浪费内存,也不会因为相同的数据存在不同的地方而引入一致性问题。分布式内存非常适合存储共享对象,例如用户资料、评论,以及HTML片段。

分布式缓存比本地共享缓存的延时要高得多,所以最髙效的使用方法是批量进行多个获取操作(例如,在一次循环中获取多个对象)。分布式缓存还需要考虑怎么增加更多的节点,以及某个节点崩溃了怎么处理。对于这两个场景,应用程序必须决定在节点间怎么分布或重分布缓存对象。当缓存集群增加或减少一台服务器时,一致性缓存对避免性能问题而言是非常重要的。在下面这个网站有一个为memcache做的一致性缓存库:http://www.audioscrobbler.net/development/ketama/。

  磁盘上的缓存

磁盘是很慢的,所以缓存在磁盘上的最好是持久化对象,很难全部装进内存的对象,或者静态内容(例如预处理的自定义图片)。

对于磁盘上的缓存和Web服务器,一个非常有用的技巧是使用404错误处理机制来捕捉缓存未命中的情况。假设Web应用要在头部展示一张基于用户名(“欢迎回来,John!”)的自定义图片,并且通过的/images/welcomeback/john.jpg这样的路径引用此图片。如果图片不存在,将会导致一个404错误,并且触发上述错误处理。这个错误处理可以生成图片,在磁盘上存储它,然后发出一个重定向或者将该图片传回浏览器。后续的请求只需要从文件中直接返回图片。

有很多类型的内容可以使用这种技巧。例如,不用再将最近的标题作为HTML部分进行缓存,可以在JavaScript文件中存储这些东西,然后在网页头中引用这个文件:latest_headlines.js。

缓存失效很简单:删除文件即可。可以通过执行一个删除N分钟前所创建的文件的定时任务,来实现TTL失效。如果想要限制缓存大小,也可以通过按最近访问时间排序来删除文件,从而实现最近最少使用(LRU)失效算法。

如果失效策略是基于最近访问时间,则必须在文件系统挂载参数中打开访问时间记录。(忽略noatime选项即可。)如果这么做,应该使用内存文件系统来避免大量磁盘操作。

 

3.3 缓存控制策略

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

  TTL(time to live,存活时间)

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

  显式失效

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

  读时失效

在更改旧数据时,为了避免要同时失效派生出来的脏数据,可以在缓存中保存一些信息,当从缓存中读数据时可以利用这些信息判断数据是否已经失效。和显式失效策略相比,这样做有很大的优势:成本固定且可以分散在不同时间内。假设要失效一个有一百万缓存对象依赖的对象,如果采用写时失效,需要一次在缓存中失效一百万个对象,即使有髙效的方法来找到这些对象,也可能需要很长的时间才能完成。如果采用读时失效,写操作可以立即完成,但后续这一百万对象的读操作可能会有略微的延迟。这样就把失效一百万对象的开销分散了,并且可以帮助避免出现负载冲髙和延迟增大的峰值。

  一种最简单的读时失效的办法是采用对象版本控制。使用这种方法,在缓存中存储一个对象时,也可以存储对象所依赖的数据的当前版本号或者时间戳。例如,假设要缓存用户博客日志的统计信息,包括用户发表的博客数。当缓存blog_stats对象时,也可以同时存储用户的当前版本号,因为该统计信息是依赖于用户的。

  不管什么时候更新依赖于用户的数据,都需要更新用户的版本号,假设用户的版本号初始为0,并且由你来生成和缓存统计信息。当用户发表了一篇博客,就增加用户的版本号到1 (当然也要同时存储这篇博客,尽管在这个例子并没有用到博客数据)。然后当需要显示统计数据的时候,可以对缓存中blog_stats对象的版本与缓存的用户版本进行比较。因为用户的版本比对象的版本高,所以可以知道缓存的统计信息已经过期了,需要重新计算。

  这是一个非常粗糙的内容失效方式,因为它假设依赖于用户的每一个比特的数据与所有其他数据都有交互。但这个假设并不总是成立的。举个例子,如果一个用户对一篇博客做了编辑,你也需要增加用户的版本号,这就会导致存储的统计信息失效,而实际上统计信息(发表的博客数)并没真的改变。这个取舍是很简单的。一个简单的缓存失效策略不只是更容易创建,也可能更加高效。

  对象版本控制是一种简单的标记缓存方法,它可以处理更复杂的依赖关系。一个标记的缓存可以识别不同类型的依赖,并且分别跟踪每个依赖的版本。

 

3.4 缓存对象分层

  分层缓存对象对检索、失效和内存利用都有帮助。相对于只缓存对象,也可以缓存对象的ID、对象的ID组等通常需要一起检索的数据。

  电子商务网站的搜索结果是这种技术很好的例子。一次搜索可能返回一个匹配产品的列表,包括名称、描述、缩略图,以及价格。缓存整个列表的效率很低:其他的搜索也可能会包含一些相同的产品,这就会导致数据重复,并且浪费内存。这种策略也使得当一个产品的价格变动时,找出并失效搜索结果变得很困难,因为你必须查看每个列表,找到哪些列表包含了更新过的产品。

  可以缓存关于搜索的最小信息,而不必缓存整个列表,例如返回结果的数量以及列表中的产品ID。然后可以再单独缓存每个产品。这样做可解决两个问题:不会重复存放任何结果数据,也更容易在失效产品的粒度上去失效缓存。

  缺点则是,相对于一次性获得整个捜索结果,必须在缓存中检索多个对象。然而不管怎么说,为搜索结果缓存产品ID的列表都是更有效的做法。先在一个缓存命中返回ID的列表,再使用这些ID去请求缓存获得产品信息。如果缓存允许在一次调用里返回多个结果,第二次请求就可以返回多个产品通过(memcached通过mget()调用来支持)。

  如果使用不当,这种方法可能会导致奇怪的结果。假设使用TTL策略来失效搜索结果,并且当产品变更时显式地去失效单个产品。现在想象一下,一个产品的描述发生了变化,不再包含搜索中匹配的关键字,但是搜索结果的缓存还没有过期失效。此时用户就会看到错误的捜索结果,因为缓存的搜索结果将会引用这个变化了的产品,即使它不再包含匹配捜索的关键字。

  对于大多数应用程序来说,这不是问题。如果应用程序不能容忍这种情况,可以使用基于版本的缓存,并在执行捜索时在结果中存储产品的版本号。当发现捜索结果在缓存中时,可以将当前搜索结果的版本号和搜索结果中每个产品的版本号做比较。如果发现任何一个产品的版本数据不一致,可以重新搜索并且重新缓存结果。

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

 

3.5预生成内容

  除了在应用程序级别缓存位数据,也可以在后台预先请求一些页面,并且将结果存为静态页面。如果页面是动态的,也可以预先生成页面的部分内容,然后使用像服务端包含(SSI)这样的技术创建最终页面。这有助于减小预生成内容的大小和开销,否则可能在将不同部分拼装到最终页面的时候,由于微小的变化产生大量的重复内容。几乎可以对任何类型的缓存使用预生成策略,包括memcached。

  预生成内容有几个重要的好处。

  • 应用代码没有复杂的命中和未命中处理路径。
  • 当未命中的处理路径慢得不可接受时,这种方案可以很好地工作,因为它保证了未命中的情况永远不会发生。实际上,在任何时候设计任何类型的缓存系统,总是应该考虑未命中的路径有多慢。如果平均性能提升很大,但是因为要预生成缓存内容,偶尔有一些请求变得非常缓慢,这时可能比不用缓存还糟糕。性能的持续稳定通常跟高性能一样重要。
  • 预生成内容可以避免在缓存未命中时导致的雪崩效应。

  缓存预生成好的内容可能占用大量空间,并且并不总能预生成所有东西。无论是哪种形式的缓存,需要预生成的内容中最重要的部分是那些最经常被请求,或者生成的成本最高的,所以可以通过本章前面提到的404错误处理机制来按需生成。

  预生成的内容有时候也可以从内存文件系统中获益,因为可以避免磁盘I/O。

 

3.6 作为基础组件的缓存

  缓存有可能成为基础设施的重要组成部分。也很容易陷入一个陷阱,认为缓存虽然很好用,但并不是重要到非有不可的东西。你也许会辩驳,如果缓存服务器宕机或者缓存被清空,请求也可以直接落在数据库上,系统依然可以正常运行。如果是刚刚将缓存加入应用系统,这也许是对的,但是缓存的加入可以使得在应用压力显著增长时不需要对系统的某些部分同比增加资源投入——通常是数据库部分。因此,系统可能慢慢地变得对缓存非常依赖,却没有被发觉。

  例如,如果高速缓存命中率是90%,当由于某种原因失去缓存,数据库上的负载将增加到原来的10倍。这很可能导致压力超过数据库服务器的性能极限。

  为了避免像这样的意外,应该设计一些髙可用性缓存(包括数据和服务)的解决方案, 或者至少是评估好禁用缓存或丢失缓存时的性能影响。比如说可以设计应用在遇到这样的情况时能够进行降级处理。

 

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。MySQL5.6的版本有一个插件提供了这个接口。 

  两种方法都有一些限制一一特别是的方法,这种方法对很多访问数据的方法都不支持。为什么会希望采用SQL以外的什么办法访向数据呢?除了速度之外,最大的原因可能是简单。这样做最大的好处是可以摆脱缓存,以及所有的失效逻辑,还有为它们服务的额外的基础设施。

 

4.拓展MySQL

  如果MySQL不能做你需要的事,一种可能是拓展其功能。在这里我们不会展示如何做到这一点,但会提供一些可能的方向。如果你对进一步探索有兴趣,那么有很多很好的在线资源,以及许多关于这些内容的书籍可以参考。

  当我们说“MySQL不能做你需要的事”,我们指的是两件事情:MySQL根本做不到这一点,或者MySQL可以做到,但是只能通过缓慢或笨拙的方法,总之做得不够好。无论哪个都是需要对MySQL拓展的原因。好消息是,MySQL已经越来越模块化和通用。

  存储引擎是拓展MySQL的一个很好的方式。Brian Aker已经写了一个存储引擎的框架,还有一系列介绍有关如何幵始编写自己的存储引擎的文章。这是目前几个主要的第三方存储引擎的基础。许多公司都编写了它们自己的内部存储引擎。例如,一些社交网络公司使用了特殊的为社交图形操作设计的存储引擎,我们还知道有个公司定制了一个用于模糊捜索的引擎。写一个简单的自定义存储引擎并不难。

  还可以使用存储引擎作为另一个软件的接口。Sphinx引擎就是一个很好的例子,该引擎是Sphinx全文检索软件的接口。

 

5.MySQL的替代品

  MySQL并不是适合每一个场景的解决方案。有些工作通常在MySQL以外来做会更好,即使MySQL理论上也可以做到。 

  最明显的一个例子是在传统的文件系统中存储文件,而不是在表中。图像文件是经典案例:虽然可以把它们放到一个BLOB列,但这通常不是个好办法。一般的做法是,在文件系统中存储图片或其他大型二进制文件,而在MySQL中只存储文件名;然后应用程序在MySQL之外存取文件。对于Web应用程序,可以把文件名放在元素的src属性中,这样就可以实现对文件的存取。 

  全文检索是另一个最好放在MySQL之外处理的例子——MySQL在全文搜索方面明显不如Lucene和Sphinx。

  NDB API也可能对某些任务有用。例如,尽管MySQL的NDB集群存储引擎(目前还)不适合存储一个高性能Web应用程序的全部数据,但用NDB API直接存储网站会话数据或用户注册信息还是可能的。在如下网站可以了解到更多关于NDB API的内容:http://dev.mysql.com/doc/ndbapi/en/index.html。还有供Apache使用的NDB模块,mod_ndb,可以在http://code.google.com/p/mode_ndb/下载。

  最后,对于某些操作——如图形关系和树遍历——关系型数据库并不总是正确的典范。MySQL并不擅长分布式数据处理,因为它缺乏并行执行査询的能力。出于这些目的情况还是建议使用其他工具(可能与MySQL结合)。现在想到的例子包括:

  • 对于简单的键一值存储,在复制严重落后的非常高速的访问场景中,我们建议用Redis替换MySQL。即使MySQL主库可以承担这样的压力,备库的延迟也是非常让人头疼的。Redis也常用来做队列,因为它对队列操作支持得很好。
  • Hadoop是房间中的大象,一语双关。混合MySQL/Hadoop的部署在处理大型或半结构化数据时非常常见。

 

6.总结

  优化并不只是数据库的事。最髙形式的优化既包含业务上的,也包含用户层的。全方位的优化才是好的优化。

  一般来说,首先要做的事是测量。认真剖析每一层的问题。哪一层导致了大部分的响应 时间?对这一层就要重点关注。如果用户的经验是大部分的时间消耗在浏览器的DOM渲染上面,MySQL只贡献总响应时间的一小部分,那么进一步优化査询语句绝对不可能明显地改善用户体验。在测量完成后,通常很容易理解应该在哪里投入精力。我们建议阅读 Steve Souders 的两本书(High Performance Web Sites和Even Faster Web Sites),并且建议使用New Relic工具。

  在Web服务器的配置和缓存中经常可以发现大问题,而这些问题往往很容易解决。还有 一个固有的观念,“总是数据库的问题”,但这其实是不正确的。应用程序中的其他层也同样重要,它们很可能被错误配置,尽管有时不太明显。特别是缓存,能承受比只使用MySQL要低得多的成本传递大量内容。虽然Apache依然是世界上最流行的Web服务器软件,但它并不总是最合适的工具,因此考虑像Nginx这样的替代方案也是非常有意义的。