数据库系统架构及实现对性能的影响

  一个 WEB 应用系统,自然离不开 Web 应用程序(WebApp)和应用程序服务器(AppServer)。AppServer 我们能控制的内容不多,大多都是使用已经久经考验的成熟产品,大家能做的也就只是通过一些简单的参数设置调整来进行调优,不做细究。而 WebApp 大部分都是各自公司根据业务需求自行开发,可控性要好很多。所以我们从Web应用程序着手分析一个应用程序架构的不同设计对整个系统性能的影响将会更合适。

  上一节中商业需求告诉了我们一个系统应该有什么不应该有什么,系统架构则则决定了我们系统的构建环境。就像修建一栋房子一样,在清楚了这栋房子的用途之后,会先有建筑设计师来画出一章基本的造型图,然后还需要结构设计师为我们设计出结构图。系统架构设计的过程就和结构工程好似设计结构图一样,需要为整个系统搭建出一个尽可能最优的框架,让整个系统能够有一个稳定高效的结构体系让我们实现各种商业需求。

  谈到应用系统架构的设计,可能有人的心里会开始嘀咕,一个DBA有什么资格谈论人家架构师(或者程序员)所设计的架构 ?其实大家完全没有必要这样去考虑,我们谈论架构只是分析各种情形下的性能消耗区别,仅仅是根据自己的专业特长来针对相应架构给出我们的建议及意见,并不是要批判架构整体的好坏,更不是为了推翻某个架构。而且我们所考虑的架构大多数时候也只是数据层面相关的架构。

  我们数据库中存放的数据都是适合在数据库中存放的吗

  对于有些开发人员来说,数据库就是一个操作最方便的万能存储中心,希望什么数据都存放在数据库中,不论是需要持久化的数据,还是临时存放的过程数据,不论是普通的纯文本格式的字符数据,还是多媒体的二进制数据,都喜欢全部塞如数据库中。因为对于应用服务器来说,数据库很多时候都是一个集中式的存储环境,不像应用服务器那样可能有很多台;而且数据库有专门的DBA去帮忙维护,而不像应用服务器很多时候还需要开发人员去做一些维护;还有一点很关键的就是数据库的操作非常简单统一,不像文件操作或者其他类型的存储方式那么复杂。

  其实我个人认为,现在的很多数据库为我们提供了太多的功能,反而让很多并不是太了解数据库的人错误的使用了数据库的很多并不是太擅长或者对性能影响很大的功能,最后却全部怪罪到数据库身上。

  实际上,以下几类数据都是不适合在数据库中存放的:

  1. 二进制多媒体数据

  将二进制多媒体数据存放在数据库中,一个问题是数据库空间资源耗用非常严重,另一个问题是这些数据的存储很消耗数据库主机的CPU资源。这种数据主要包括图片,音频、视频和其他一些相关的二进制文件。这些数据的处理本不是数据的优势,如果我们硬要将他们塞入数据库,肯定会造成数据库的处理资源消耗严重。

  2. 流水队列数据

  我们都知道,数据库为了保证事务的安全性(支持事务的存储引擎)以及可恢复性,都是需要记录所有变更的日志信息的。而流水队列数据的用途就决定了存放这种数据的表中的数据会不断的被 INSERT,UPDATE 和 DELETE,而每一个操作都会生成与之对应的日志信息。在 MySQL 中,如果是支持事务的存储引擎,这个日志的产生量更是要翻倍。而如果我们通过一些成熟的第三方队列软件来实现这个 Queue 数据的处理功能,性能将会成倍的提升。

  3. 超大文本数据

  对于 5.0.3 之前的 MySQL 版本,VARCHAR 类型的数据最长只能存放 255 个字节,如果需要存储更长的文本数据到一个字段,我们就必须使用 TEXT 类型(最大可存放64KB)的字段,甚至是更大的

  LONGTEXT 类型(最大4GB)。而 TEXT 类型数据的处理性能要远比 VARCHAR 类型数据的处理性能低下很多。从 5.0.3 版本开始,VARCHAR 类型的最大长度被调整到 64KB 了,但是当实际数据小于 255Bytes 的时候,实际存储空间和实际的数据长度一样,可一旦长度超过 255Bytes 之后,所占用的存储空间就是实际数据长度的两倍。

  所以,超大文本数据存放在数据库中不仅会带来性能低下的问题,还会带来空间占用的浪费问题。

  是否合理的利用了应用层Cache机制 ?

  对于Web应用,活跃数据的数据量总是不会特别的大,有些活跃数据更是很少变化。对于这类数据,我们是否有必要每次需要的时候都到数据库中去查询呢 ?如果我们能够将变化相对较少的部分活跃数据通过应用层的 Cache 机制 Cache 到内存中,对性能的提升肯定是成数量级的,而且由于是活跃数据,对系统整体的性能影响也会很大。

  当然,通过Cache机制成功的案例数不胜数,但是失败的案例也同样并不少见。如何合理的通过Cache技术让系统性能得到较大的提升也不是通过寥寥几笔就能说明的清楚,这里我仅根据以往的经验列举一下什么样的数据适合通过Cache技术来提高系统性能:
  1. 系统各种配置及规则数据;

  由于这些配置信息变动的频率非常低,访问概率又很高,所以非常适合存使用Cache;

  2. 活跃用户的基本信息数据;

  虽然我们经常会听到某某网站的用户量达到成百上千万,但是很少有系统的活跃用户量能够都达到这个数量级。也很少有用户每天没事干去将自己的基本信息改来改去。更为重要的一点是用户的基本信息在应用系统中的访问频率极其频繁。所以用户基本信息的Cache,很容易让整个应用系统的性能出现一个质的提升。

  3. 活跃用户的个性化定制信息数据;

  虽然用户个性化定制的数据从访问频率来看,可能并没有用户的基本信息那么的频繁,但相对于系统整体来说,也占了很大的比例,而且变更皮律一样不会太多。从 Ebay 的 PayPal 通过 MySQL 的 Memory 存储引擎实现用户个性化定制数据的成功案例我们就能看出对这部分信息进行 Cache 的价值了。虽然通过 MySQL 的Memory存储引擎并不像我们传统意义层面的 Cache 机制,但正是对 Cache 技术的合理利用和扩充造就了项目整体的成功。

  4. 准实时的统计信息数据;
  所谓准实时的统计数据,实际上就是基于时间段的统计数据。这种数据不会实时更新,也很少需要增量更新,只有当达到重新Build该统计数据的时候需要做一次全量更新操作。虽然这种数据即使通过数据库来读取效率可能也会比较高,但是执行频率很高之后,同样会消耗不少资源。既然数据库服务器的资源非常珍贵,我们为什么不能放在应用相关的内存Cache中呢 ?

  5. 其他一些访问频繁但变更较少的数据;

  除了上面这四种数据之外,在我们面对的各种系统环境中肯定还会有各种各样的变更较少但是访问很频繁的数据。只要合适,我们都可以将对他们的访问从数据库移到 Cache 中。

  我们的数据层实现都是最精简的吗

  从以往的经验来看,一个合理的数据存取实现和一个拙劣的实现相比,在性能方面的差异经常会超出一个甚至几个数量级。我们先来分析一个非常简单且经常会遇到类似情况的示例:

  在我们的示例网站系统中,现在要实现每个用户查看各自相册列表(假设每个列表显示 10 张相片)的时候,能够在相片名称后面显示该相片的留言数量。这个需求大家认为应该如何实现呢 ?我想 90% 的开发开发工程师会通过如下两步来实现该需求:
  1、通过

得到第一页的相片的相关信息;
  2、通过第 1 步结果集中的 10 个相片 id 循环运行十次

来得到每张相册的回复数量然后再瓶装展现对象。

  此外可能还有部分人想到了如下的方案:
  1、和上面完全一样的操作步骤;
  2、通过程序拼装上面得到的 10 个 photo 的 id,再通过 in 查询

一次得到10个photo的所有回复数量,再组装两个结果集得到展现对象。

  我们来对以上两个方案做一下简单的比较:

  1、从 MySQL 执行的 SQL 数量来看,第一种解决方案为 11(1+10=11)条 SQL 语句,第二种解决方案为 2 条 SQL 语句(1+1);
  2、从应用程序与数据库交互来看,第一种为 11 次,第二种为 2 次;
  3、从数据库的 IO 操作来看,简单假设每次 SQL 为 1 个 IO,第一种最少 11 次 IO,第二种小于等于 11 次 IO,而且只有当数据非常之离散的情况下才会需要 11 次;
  4、从数据库处理的查询复杂度来看,第一种为两类很简单的查询,第二种有一条 SQL 语句有 GROUP BY 操作,比第一种解决方案增加了了排序分组操作;
  5、从应用程序结果集处理来看,第一种 11 次结果集的处理,第二中 2 次结果集的处理,但是第二种解决方案中第二词结果处理数量是第一次的 10 倍;
  6、从应用程序数据处理来看,第二种比第一种多了一个拼装 photo_id 的过程。

  我们先从以上6点来做一个性能消耗的分析:

  1、由于MySQL对客户端每次提交的 SQL 不管是相同还是不同,都需要进行完全解析,这个动作主要消耗的资源是数据库主机的 CPU,那么这里第一种方案和第二种方案消耗 CPU 的比例是 11:2。SQL 语句的解析动作在整个SQL语句执行过程中的整体消耗的CPU比例是较多的;
  2、应用程序与数据库交互所消耗的资源基本上都在网络方面,同样也是 11:2;
  3、数据库 IO 操作资源消耗为小于或者等于 1:1;
  4、第二种解决方案需要比第一种多消耗内存资源进行排序分组操作,由于数据量不大,多出的消耗在语句整体消耗中占用比例会比较小,大概不会超过 20%,大家可以针对性测试;
  5、结果集处理次数也为 11:2,但是第二中解决方案第二次处理数量较大,整体来说两次的性能消耗区别不大;
  6、应用程序数据处理方面所多出的这个 photo_id 的拼装所消耗的资源是非常小的,甚至比应用程序与 MySQL 做一次简单的交互所消耗的资源还要少。综合上面的这6点比较,我们可以很容易得出结论,从整体资源消耗来看,第二中方案会远远优于第一种解决方案。而在实际开发过程中,我们的程序员却很少选用。主要原因其实有两个,一个是第二种方案在程序代码实现方面可能会比第一种方案略为复杂,尤其是在当前编程环境中面向对象思想的普及,开发工程师可能会更习惯于以对象为中心的思考方式来解决问题。还有一个原因就是我们的程序员可能对 SQL 语句的使用并不是特别的熟悉,并不一定能够想到第二条 SQL 语句所实现的功能。对于第一个原因,我们可能只能通过加强开发工程师的性能优化意识来让大家能够自觉纠正,而第二个原因的解决就正是需要我们出马的时候了。SQL 语句正是我们的专长,定期对开发工程师进行一些相应的数据库知识包括SQL 语句方面的优化培训,可能会给大家带来意想不到的收获的。

  这里我们还仅仅只是通过一个很长见的简单示例来说明数据层架构实现的区别对整体性能的影响,实际上可以简单的归结为过渡依赖嵌套循环的使用或者说是过渡弱化SQL语句的功能造成性能消耗过多的实例。后面我将进一步分析一下更多的因为架构实现差异所带来的性能消耗差异。

  过度依赖数据库SQL语句的功能造成数据库操作效率低下前面的案例是开发工程师过渡弱化 SQL 语句的功能造成的资源浪费案例,而这里我们再来分析一个完全相反的案例:在群组简介页面需要显示群名称和简介,每个群成员的 nick_name,以及群主的个人签名信息。

  需求中所需信息存放在以下四个表中:user,user_profile,groups,user_group

  我们先看看最简单的实现方法,一条SQL语句搞定所有事情:
  

  当然我们也可以通过如下稍微复杂一点的方法分两步搞定:

  首先取得所有需要展示的group的相关信息和所有群组员的nick_name信息和组员类别:
  

  然后在程序中通过上面结果集中的user_type找到群主的user_id再到user_profile表中取得群主的签名信息:

  大家应该能够看出两者的区别吧,两种解决方案最大的区别在于交互次数和SQL复杂度。而带来的实际影响是第一种解决方案对 user_profile 表有不必要的访问(非群主的 profile 信息),造成 IO 访问的直接增加在20%左右。而大家都知道,IO操作在数据库应用系统中是非常昂贵的资源。尤其是当这个功能的PV较大的时候,第一种方案造成的 IO 损失是相当大的。

  重复执行相同的SQL造成资源浪费

  这个问题其实是每个人都非常清楚也完全认同的一个问题,但是在应用系统开发过程中,仍然会常有这样的现象存在。究其原因,主要还是开发工程师思维中面向对象的概念太过深入,以及为了减少自己代码开发的逻辑和对程序接口过度依赖所造成的。

  我曾经在一个性能优化项目中遇到过一个案例,某个功能页面一侧是“分组”列表,是一列“分组”的名字。页面主要内容则是该“分组”的所有“项目”列表。每个“项目”以名称(或者图标)显示,同时还有一个SEO相关的需求就是每个“项目”名称的链接地址中是需要有“分组”的名称的。所以在“项目”列表的每个“项目”的展示内容中就需要得到该项目所属的组的名称。按照开发工程师开发思路,非常容易产生取得所有“项目”结果集并映射成相应对象之后,再从对象集中获取“项目”所属组的标识字段,然后循环到“分组”表中取得需要的”组名“。然后再将拼装成展示对象。看到这里,我想大家应该已经知道这里存在的一个最大的问题就是多次重复执行了完全相同的SQL得到完全相同的内容。同时还犯了前面第一个案例中所犯的错误。或许大家看到之后会不相信有这样的案例存在,我可以非常肯定的告诉大家,事实就是这样。同时也请大家如果有条件的话,好好Review自己所在的系统的代码,非常有可能同样存在上面类似的情形。还有部分解决方案要远优于上面的做法,那就是不循环去取了,而是通过Join一次完成,也就是解决了第一个案例所描述的性能问题。但是又误入了类似于第二个案例所描述的陷阱中了,因为实际上他只需要一次查询就可以得到所有“项目”所属的“分组”的名称(所有项目都是同一个组的)。当然,也有部分解决方案也避免了第二个案例的问题,分为两条SQL,两步完成了这个需求。这样在性能上面基本上也将近是数量级的提升了。但是这就是性能最优的解决方案了么 ?不是的,我们甚至可以连一次都不需要访问就获得所需要的“分组”名称。首先,侧栏中的“分组”列表是需要有名称的,我们为什么不能直接利用到呢 ?

  当然,可能有些系统的架构决定了侧栏和主要内容显示区来源于不同的模板(或者其他结构),那么我们也完全可以通过在进入这个功能页面的链接请求中通过参数传入我们需要的“分组”名称。这样我们就可以完全不需要根据“项目”相关信息去数据库获取所属“分组”的信息,就可以完成相应需求了。当然,是否需要通过请求参数来节省最后的这一次访问,可能会根据这个功能页面的PV来决定,如果访问并不是非常频繁,那么这个节省可能并不是很明显,而应用系统的复杂度却有所增加,而且程序看上去可能也会不够优雅,但是如果访问非常频繁的场景中,所节省的资源还是比较可观的。上面还仅仅只是列举了我们平时比较常见的一些实现差异对性能所带来的影响,除了这些实现方面所带来的问题之外,应用系统的整体架构实现设计对系统性能的影响可能会更严重。下面大概列举了一些较为常见的架构设计实现不当带来的性能问题和资源浪费情况。
  
  1、Cache 系统的不合理利用导致 Cache 命中率低下造成数据库访问量的增加,同时也浪费了 Cache 系统的硬件资源投入;
  2、过度依赖面向对象思想
  3、对可扩展性的过渡追求,促使系统设计的时候将对象拆得过于离散,造成系统中大量的复杂 Join 语句,而 MySQLServer 在各数据库系统中的主要优势在于处理简单逻辑的查询,这与其锁定的机制也有较大关系;
  4、对数据库的过渡依赖,将大量更适合存放于文件系统中的数据存入了数据库中,造成数据库资源的浪费,影响到系统的整体性能,如各种日志信息;
  5、过度理想化系统的用户体验,使大量非核心业务消耗过多的资源,如大量不需要实时更新的数据做了实时统计计算。以上仅仅是一些比较常见的症结,在各种不同的应用环境中肯定还会有很多不同的性能问题,可能需要大家通过仔细的数据分析和对系统的充分了解才能找到,但是一旦找到症结所在,通过相应的优化措施,所带来的收益也是相当可观的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注