您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
大型网站架构改进历程:存储的瓶颈(下)
 
作者 夏天的森林 来源:博客园  火龙果软件 发布于 2015-1-30
  1327  次浏览      15
 

在高并发的情况整个网站系统首先暴露出问题的是数据库,如果我们把整个网站系统比作一个盛水的木桶,那么木桶最短的那个板就是数据库了,一般而言网站的服务应用出问题都会是解决存储问题之后才会出现。

503错误

在上篇,我讲到某些网站在高并发下会报出503错误,503错误的含义是指网站服务端暂时无法提供服务,503还表达了网站服务端现在有问题,但是以后可能会提供正常的服务,对http协议熟悉的人都知道,5开头的响应码表达了服务端出现了问题,在我们开发测试时候最为常见的是500错误,500代表的含义是服务端程序出现了错误导致网站无法正常提供服务,500通常是服务端异常和错误所致,如果生产系统里发现了500错误,那么只能说明网站存在逻辑性的错误,这往往是系统上线前的测试做的不到位所致。回到503错误,我上文解释为拒绝访问,其实更加准确的回答应该是服务不可用,那么为什么我会说503错误在高并发的情况下90%的原因是数据库所致呢?

上文我做出了详细的解释,但是今天我回味了一下,发现那个解释还不是太突出重点,问题的重点是在高并发的情况整个网站系统首先暴露出问题的是数据库,如果我们把整个网站系统比作一个盛水的木桶,那么木桶最短的那个板就是数据库了,一般而言网站的服务应用出问题都会是解决存储问题之后才会出现

数据库出现了瓶颈并不是程序存在逻辑性错误,数据库瓶颈的表现就是数据库因为承受了太多的访问后,数据库无法迅速的做出响应,严重时候数据库会拒绝进一步操作死锁在哪里不能做出任何反应。数据库犹如一把巨型的大锁,很多人争抢这个锁时候会导致这个大锁完全被锁死,最终请求的处理就停留在这个大锁上,最终导致网站提示出503错误,503错误最终会传递到所有的客户端上,最终的现象就是全站不可用了。

session共享的一个方案是将session数据存储在外部一个独立的缓存服务器里,我开始说用一台服务器做缓存服务器,后面提到如果觉得一台服务器做缓存不安全,那么采用分布式缓存服务器例如memcached,那么这里就有一个问题了,为了保证Web服务的可用性,我们会把Web服务分开部署到不同的服务器上,这些服务器都是对等关系,其中一台服务器不能正常提供服务不会影响到整个网站的稳定性,那么我们采取memcached集群是不是可以达到同样的效果了?

即缓存服务器集群中一台服务器挂掉,不会影响到用户对网站的使用了?问题的答案是令人失望了,假如我们使用两台服务器做缓存服务器来存储session信息,那么如果其中一台服务器挂掉了,那么网站将会有一半的用户将不能正常使用网站,原因是他们的session信息丢失了,网站无法正常的跟踪用户的会话状态。

我之所以提到这个问题是想告诉大家以memcached为代表的分布式缓存和我们传统理解的分布式系统是有区别的,传统的分布式系统都会包含一个容灾维护系统稳定性的功能,但实际的分布式技术是多种多样的,例如memcached的分布式技术并不是为了解决容灾维护系统稳定性的模式设计,换个说法就是memcached集群的设计是没有过分考虑冗余的问题,而只有适当的冗余才能保证系统的健壮性问题。分布式技术的实现是千差万别的,每个优秀的分布式系统都有自身独有的特点。

memcached技术

全面的讲述memcached技术并非本文的主题,而且这个主题也不是一两句话能说清楚的,这里我简单的介绍下memcached实现的原理,当网站使用缓存集群时候,缓存数据是通过一定的算法将缓存数据尽量均匀分布到不同服务器上,如果用户A的缓存在服务器A上,那么服务器B上是没有该用户的缓存数据,早期的memcache数据分布式的算法是根据缓存数据的key即键值计算出一个hash值,这个hash值再除以缓存服务器的个数,得到的余数会对应某一台服务器,例如1对应服务器A,2对应服务器B,那么余数是1的key值缓存就会存储在服务器A上,这样的算法会导致某一台服务器挂掉,那么网站损失的缓存数据的占比就会比较高,为了解决这个问题,memcached引入了一致性hash算法。

一致性hash可以服务器宕机时候这台服务器对整个缓存数据的影响最小。

读/写分离方案

上文里我讲到了读写分离的设计方案,而读写分离方案主要是应用于网站读写比例严重失衡的网站,而互联网上绝大部分网站都是读操作的比例远远大于写操作,这是网站的主流,如果一个网站读写比例比较均衡,那么这个网站一般都是提供专业服务的网站,这种网站对于个人而言是一个提供生活便利的工具,它们和企业软件类似。大部分关注大型网站架构技术关心的重点应该是那种对于读写比例失衡的网站,因为它们做起来更加有挑战性。

将数据库进行读写分离是网站解决存储瓶颈的第一步,为什么说是第一步呢?因为读写分离从业务角度而言它是一种粗粒度的数据拆分,因此它所包含的业务复杂度比较低,容易操作和被掌控,从技术而言,实现手段也相对简单,因此读写分离是一种低成本解决存储瓶颈的一种手段,这种方案是一种改良方案而不是革命性的的方案,不管是从难度,还是影响范围或者是经济成本角度考虑都是很容易让相关方接受的。

那么我们仅仅将数据库做读写分离为何能产生好的效率了?我们首先要了解下硬盘的机制,硬盘的物理机制就有一个大圆盘飞速旋转,然后有个磁头不断扫描这个大圆盘,这样的物理机制就会导致硬盘数据的顺序操作比随机操作效率更高,这点对于硬盘的读和写还算公平,但是写操作在高并发情况下会有点复杂,写操作有个特性就是我们要保证写操作的准确性,但是高并发下可能会出现多个用户同时修改某一条数据,为了保证数据能被准确的修改,那么我们通常要把并行的操作转变为串行操作,这个时候就会出现一个锁机制,锁机制的实现是很复杂的,它会消耗很多系统性能,如果写操作掺杂了读操作情况就更复杂,效率会更加低效,相对于写操作读操作就单纯多了,如果我们的数据只有读操作,那么读的性能也就是硬盘顺序读能力和随机读能力的体现,即使掺杂了并发也不会对其有很大的影响,因此如果把读操作和写操作分离,效率自然会得到很大提升。

为什么要引入缓存系统和搜索技术?

既然读写分离可以提升存储系统的效率,那么为什么我们又要引入缓存系统和搜索技术了?缓存将数据存在内存中,内存效率是硬盘的几万倍,这样的好处不言而喻,而选择搜索技术背后的原理就不同了,数据库存储的数据称之为结构化数据。

结构化数据的限制很多,当结构化数据遇到了千变万化的随机访问时候,其效率会变得异常低效,但是如果一个网站不能提供灵活、高效的随机访问能力,那么这个网站就会变得单板没有活力。例如我们在淘宝里查找我们想要的商品,但是时常我们并不清楚自己到底想买啥,如果是在实体店里店员会引导我们的消费,但是网站又如何引导我们的消费,那么我们必须要赋予网站通过人们简单意向随机找到各种不同的商品,这个对于数据库就是一个like操作的,但是数据里数据量达到了一定规模以后like的低效是无法让人忍受的,这时候搜索技术在随机访问的能力正好可以弥补数据库这块的不足。

拆分数据

业务再接着的增长下去,数据量也会随之越来越大了,这样发展下去总有一天主库也会产生瓶颈了,那么接下来我们又该如何解决主库的瓶颈了?

方法很简单就是我们要拆分主库的数据了,那么我该以什么维度拆分数据了?一个数据库里有很多张表,不同的表都针对不同的业务,网站的不同业务所带来的数据量也不是不同的,这个时候系统的短板就是那些数据量最大的表,所以我们要把那些会让数据库产生瓶颈的表拆出来,例如电商系统里商品表和交易表往往数据量非常大,那么我们可以把这两种表建立在单独的两个数据库里,这样就拆分了数据库的压力,这种做法叫做数据垂直拆分,不过垂直拆分会给原有的数据库查询,特别是有事务的相关操作产生影响,这些问题我们必须要进行改造,关于这个问题,我将在下篇里进行讨论。

当我们的系统做完了读写分离,数据垂直拆分后,我们的网站还在迅猛发展,最终一定又会达到新的数据库瓶颈,当然这些瓶颈首先还是出现在那些数据量大的表里,这些表数据的处理已经超出了单台服务器的能力,这个时候我们就得对这个单库单表的数据进行更进一步的拆分,也就是将一张表分布到两台不同的数据库里,这个做法就是叫做数据的水平拆分了

这两篇文章我们可以理出一个解决大型网站数据瓶颈的一个脉络了,具体如下:

 单库数据库-->数据库读写分离-->缓存技术-->搜索技术-->数据的垂直拆分-->数据的水平拆分

以上的每个技术细节在具体实现中可能存在很大的不同,但是问题的缘由大致是一致的,我们理清这个脉络就是想告诉大家我们如果碰到这样的问题应该按何种思路进行思考和设计解决方案。

存储的瓶颈写到现在就要进入到深水区了,如果我们所做的网站已经到了做数据库垂直拆分和水平拆分的阶段,那么此时我们所面临的技术难度的挑战也会大大增强。

这里我们先回顾下数据库的垂直拆分和水平拆分的定义:

垂直拆分:把一个数据库中不同业务单元的数据分到不同的数据库里。

水平拆分:是根据一定的规则把同一业务单元的数据拆分到多个数据库里。

垂直拆分是一个粗粒度的拆分数据,它主要是将原来在一个数据库下的表拆分到不同的数据库里,水平拆分粒度比垂直拆分要更细点,它是将一张表拆到不同数据库里,粒度的粗细也会导致实现技术的难度的也不一样,很明显水平拆分的技术难度要远大于垂直拆分的技术难度。难度意味着投入的成本的增加以及我们需要承担的风险的加大,我们做系统开发一定要有个清晰的认识:能用简单的方案解决问题,就一定要毫不犹豫的舍弃复杂的方案,当系统需要使用高难度技术的时候,我们一定要让自己感受到这是迫不得已的

我是以java工程师应聘进了我现在的公司,所以在我转到专职前端前,我也做过不少java的应用开发,当时我在公司的前辈告诉我,我们公司的数据库建模很简单,怎么个简单法了,数据库的表之间都没有外键,数据库不准写触发器,可以写写存储过程,但是存储过程决不能用于处理生产业务逻辑,而只能是一些辅助工作,例如导入导出写数据啊,后面听说就算是数据库做到了读写分离,数据之间同步也最好是用java程序做,也不要使用存储过程,除非迫不得已。

开始我还不太理解这些做法,这种不理解不是指我质疑了公司的做法,而是我在想如果一个数据库我们就用了这么一点功能,那还不如让数据库公司为咋们定制个阉割版算了,不过在我学习了hadoop之后我有点理解这个背后的深意了,其实作为存储数据的数据库,它和我们开发出的程序的本质是一样的那就是:存储和计算,那么当数据库作为一个业务系统的存储介质时候,那么它的存储对业务系统的重要性要远远大于它所能承担的计算功能,当数据库作为互联网系统的存储介质时候,如果这个互联网系统成长迅速,那么这个时候我们对数据库存储的要求就会越来越高,最后估计我们都想把数据库的计算特性给阉割掉,当然数据库基本的增删改查我们是不能舍弃的,因为它们是数据库和外界沟通的入口,我们如果接触过具有海量数据的数据库,我们会发现让数据库运行的单个sql语句都会变得异常简洁和简单,因为这个时候我们知道数据库已经在存储这块承担了太多的负担,那么我们能帮助数据库的手段只能是尽量降低它运算的压力

回到关于数据库垂直拆分和水平拆分的问题,假如我们的数据库设计按照我们公司业务数据库为蓝本的话,那么数据库进行了水平拆分我们会碰到什么样的问题了?为了回答这个问题我就要比较下拆分前和拆分后会给调用数据库的程序带来怎样的不同,不同主要是两点:

第一点:被拆出的表和原库的其他表有关联查询即使用join查询的操作需要进行改变;

第二点:某些增删改(注意:一般业务库设计很少使用物理删除,因为这个操作十分危险,这里的删往往是逻辑删除,一般做法就是更新下记录的状态,本质是一个更新操作)牵涉到拆分的表和原库其他表共同完成,那么该操作的事务性就会被打破,如果处理不好,假如碰到操作失败,业务无法做到回滚,这会对业务操作的安全性带来极大的风险。

关于解决第一点的问题还是相对比较简单的,方式方法也很多,下面我来讲讲我所知道的一些方法,具体如下:

方法一:在垂直拆表时候,我们先梳理下使用到join操作sql查询,梳理的维度是以被拆分出的表为原点,如果是弱依赖的join表我们改写下sql查询语句,如果是强依赖的join表则随拆分表一起拆分,这个方法很简单也很可控,但是这个技术方案存在一个问题,就是让拆分粒度变大,拆分的业务规则被干扰,这么拆分很容易犯一个问题就是一个数据库里总会存在这样一些表,就是很多数据库都会和它关联,我们很难拆解这些关联关系,当我们无法理清时候就会把该表做冗余,即不同数据库存在雷同表,随着业务增长,这种表的数据同步就成为了数据库的一个软肋,最终它会演变为整个数据库系统的短板甚至是全系统的短板。

方法二:我们拆表的准则还是按业务按需求在数据库层面进行,等数据库拆好后,再改写原来受到影响的join查询语句,这里我要说明的是查询语句修改的成本很低,因为查询操作是个只读操作,它不会改变任何底层的东西,如果数据表跨库,我们可以把join查询拆分为多次查询,最后将查询结果在内存中归纳和合并,其实我们如果主动拆库,绝不会把换个不同的数据库产品建立新库,肯定是使用相同数据库,同类型的数据库基本都支持跨库查询,不过跨库查询听说效率不咋地,我们可以有选择的使用。这种方案也有个致命的缺点,我们做数据库垂直拆分绝不可能一次到位,一般都是多次迭代,而该方案的影响面很大,关联方过多,每次拆表几乎要检查所有相关的sql语句,这会导致系统不断累积不可预知的风险。

以下三段内容是方法三:

不管是方法一还是方法二,都有一个很根本的缺陷就是数据库和上层业务操作耦合度很高,每次数据库的变迁都导致业务开发跟随做大量的同步工作,这样的后果就是资源浪费,做服务的人不能天天被数据库牵着鼻子走,这样业务系统的日常维护和业务扩展会很存问题,那么我们一定要有一个服务和数据库解耦方案,那么这里我们就得借鉴ORM技术了。(这里我要说明下,方法一和方法二我都是以修改sql阐述的,在现实开发里很多系统会使用ORM技术,互联网一般用ibatis和mybatis这种半ORM的产品,因为它们可以直接写sql和数据库最为亲近,如果使用hibernate则就不同了,但是hibernate虽然大部分不是直接写sql,但是它只不过是对数据库操作做了一层映射,本质手段是一致,所以上文的sql可以算是一种指代,它也包括ORM里的映射技术)

传统的ORM技术例如hibernate还有mybatis都是针对单库进行的,并不能帮我们解决垂直拆分的问题,因此我们必须自己开发一套解决跨库操作的ORM系统,这里我只针对查询的ORM谈谈自己的看法(讲到这里是不是有些人会有种似成相识的感觉,这个不是和分布式系统很像吗)。

其实具体怎么重构有问题的sql不是我想讨论的问题,因为这是个技术手段或者说是一个技术上的技巧问题,我这里重点讲讲这个ORM与服务层接口的交互,对于服务层而言,服务层最怕的就是被数据库牵着鼻子走,因为当数据库要进行重大改变时候,服务层总是想方设法让自己不要发生变化,对于数据库层而言服务层的建议都应该是合理,数据库层要把服务层当做自己的需求方,这样双方才能齐心协力完成这件重要的工作,那么服务层一般是怎样和数据库层交互的呢?

从传统的ORM技术我们可以找到答案,具体的方式有两种:

第一种:以hibernate为代表的,hibernate框架有一套自己的查询语言就是hql,它类似于sql,自定义一套查询语言看起来很酷,也非常灵活,但是实现难度非常之高,因为这种做法相当于我们要自己编写一套新的编程语言,如果这个语言设计不好,使用者又理解不深入,最后往往会事与愿违,就像hibernate的hql,我们经常令可直接使用sql也不愿意使用hql,这其中的缘由用过的人一定很好理解的。

第二种:就是数据层给服务层提供调用方法,每个方法对应一个具体的数据库操作,就算底层数据库发生重大变迁,只要提供给服务端的方法定义不变,那么数据库的变迁对服务层影响度也会最低。

前面我提到技术难度是我们选择技术的一个重要指标,相比之下第二种方案将会是我们的首选。

垂直拆分数据库还会带来另一个问题就是对事务的影响,垂直拆分数据库会导致原来的事务机制变成了分布式事务,解决分布式事务问题是非常难的,特别是如果我们想使用业界推出的解决分布式事务方案,那么要自己实现个分布式事务就更难了,不过这里我要说明一下,我这里说的更难是和我写本文有关,我本篇文章之所以现在才写是因为我想先研究下业界推出的分布式解决方案,但是这些方案的原理看得我很沮丧,我就想如果我们直接用方案的接口实现了它,因为还是不懂他的很多原理,那么这些方案其实就是不可控方案,说不定使用过多就会给系统埋下定时炸弹,因此这里我就只提提这些方案,有兴趣的童鞋可以去研究下:

一、X/OPEN组织推出的分布式事务规范XA,其中还包括该组织定义的分布式事务处理模型X/OPEN

二、大型网站一致性理论CAP/BASE

三、 PAXOS协议。

这里特别要提的是PAXOS协议,我以前写过好几篇关于zookeeper的文章,zookeeper框架有一个特性就是它本身是一个分布式文件系统,当我们往zookeeper写数据时候,zookeeper集群能保证我们的写操作的可靠性,这个可靠性和我们使用线程安全来控制写数据一样,绝对不会让写操作出错,之所以zookeeper能做到这点,是因为zookeeper内部有一个类似PAXOS协议的协议,这个协议类似一个选举方案,它能保证写入操作的原子性。

其实事务也是和线程安全技术类似,只不过事务是要保证一个业务操作的原子性问题,当然事务还要有个特点就是回滚机制即业务操作失败,事务可以保证系统恢复到业务操作前的状态,回滚机制的本质其实是维护业务操作的状态性,具体点我这里列举个例子:当系统将要执行一个业务操作时候,我们首先为业务系统定义一个初始状态,业务执行操作时候我们可以定义一个执行状态,操作成功就是一个成功状态,操作失败就是一个操作失败状态,如果业务操作是失败状态,我们可以让业务回滚到初始状态,更进一步如果执行状态超时也可以将整个业务状态回退到初始状态,其实所有事务回滚机制的本质基本都是如此

记得不久前,在群里有个群友就问大家如何实现分布式事务,他想要知道的分布式事务是有没有一种技术能像我们操作数据库或者是jdbc那样一个commit,一个rollback就搞定,但是现实中的分布式事务比commit和rollback复杂的多,不可能简单的让我们写几个标记就能实现分布式事务,当然业界是有方案的,就是我上面提到的,如果有人真想知道可以自己研究下,不过我本人现在还是不太懂上面这些技术的原理和思想。

其实当时我马上给那位群友一个解答,我说我们开发时候是经常碰到分布式事务,但是我们解决分布式事务大多数从业务角度来解决的,而没去选择纯技术手段,因为技术手段太复杂难以控制。这个答案可能不会令提问者满意,但是我现在还是坚持这个观点,这个观点符合我提到的原则,当技术方案难度过高,我们就不要轻易选择使用它,因为这么做是很危险的,今天我就举个例子吧,这样可能更有说服力。

我现在做的系统很多业务操作经常要和其他系统共同完成,其他系统有我们公司自己的系统,也有其他企业的系统,这里我还是把业务操作比作一辆在高速公路的汽车,那么每个系统就是高速公路上的一个收费站,业务每到一个收费站,该系统的数据库就会在对应的数据库的某张表里某条记录上记录一个状态,当汽车跑完全程,各个收费站就会相互通知,告诉大家任务完成,最终将所有的状态置为已完成,如果失败,就废掉这辆汽车,收费站之间也会相互通知,让所有的记录状态回归到初始状态,就当从来没有这辆汽车来过。

这个做法的原理就是使用了事务回滚的本质,状态的变迁和回退,这个做法在业务系统开发里也有个专有术语就是工作流。其实大多数问如何实现分布式事务如何实现的问题的本质就是想解决事务的回滚问题,我们其实不要被这个分布式事务的名字给吓住了,其实有很多不起眼的技术手段和业务手段都能达到相同的目的。

晚上11点了,看来本文今天写不完了,今天就到此为止,最后我要总结下本文的内容,具体如下:

1. 大型网站解决存储瓶颈的问题,我们要找准存储这个关键点,因为数据库其实是存储和运算的组合体,但是在我们这个场景下,存储是第一位的,当存储是瓶颈时候我们要狠下心来尽量多的抛弃数据的计算特点,所以上文中我提出我们数据库就不要滥用计算功能了例如触发器、存储过程等等。

2. 数据库剥离计算功能不代表不要数据的计算功能,因为没有数据的计算功能数据库也就没价值了,那么我们要将数据库的计算功能进行迁移,迁移到程序里面,一般大型系统程序和数据库都是分开部署到不同服务器上,因此程序里处理数据计算就不会影响到数据库所在服务器的性能,就可以让安装数据库的服务器专心服务于存储。

3. 我们要尽一切可能的把数据库的变化对服务层的影响降到最低,最好是数据库做拆分后,现有业务不要任何的更改,那么我们就得设计一个全新的数据访问层,这个数据访问层将数据库和服务层进行解耦,任何数据库的变化都由数据访问层消化,数据访问层对外接口要高度统一,不要轻易改变。

4. 如果我们设计了数据访问层来解决数据库拆分的问题,数据访问层加上数据库其实就组合出了一个分布式数据库的解决方案,由此可见拆分数据库的难度是很高的,因为数据库将拥有分布式的特性,而分布式开发就意味开发难度的增加。

5. 对于分布式事务的处理,我们尽量要从具体问题具体分析,不要一感觉这个事务操作本质是分布式事务就去寻找通用的分布式事务技术手段,这样的想法其实是回避困难的思想,结果可能会是把问题搞得更加复杂。

   
1327 次浏览       15
相关文章

企业架构、TOGAF与ArchiMate概览
架构师之路-如何做好业务建模?
大型网站电商网站架构案例和技术架构的示例
完整的Archimate视点指南(包括示例)
相关文档

数据中台技术架构方法论与实践
适用ArchiMate、EA 和 iSpace进行企业架构建模
Zachman企业架构框架简介
企业架构让SOA落地
相关课程

云平台与微服务架构设计
中台战略、中台建设与数字商业
亿级用户高并发、高可用系统架构
高可用分布式架构设计与实践
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...