赞
踩
前不久公司请来了位互联网界的技术大牛跟我们做了一次大型网站架构的培训,两天12个小时信息量非常大,知识的广度和难度也非常大,培训完后我很难完整理出全部听到的知识,今天我换了个思路是回味这次培训,这个思路就是通过本人目前的经验和技术水平来思考下大型网站技术演进的过程。
首先我们要思考一个问题,什么样的网站才是大型网站,从网站的技术指标角度考虑这个问题人们很容易犯一个毛病就是认为网站的访问量是衡量的指标,懂点行的人也许会认为是网站在单位时间里的并发量的大小来作为指标,如果按这些标准那么像hao123这样的网站就是大型网站了,如下图所示:
其实这种网站访问量非常大,并发数也非常高,但是它却能用最为简单的web技术来实现:我们只要保持网站的充分的静态化,多部署几台服务器,那么就算地球上所有人都用它,网站也能正常运行。
我觉得大型网站是技术和业务的结合,一个满足某些用户需求的网站只要技术和业务二者有一方难度很大,必然会让企业投入更多的、更优秀的人力成本实现它,那么这样的网站就是所谓的大型网站了。
一个初建的网站往往用户群都是很小的,最简单的网站架构就能解决实际的用户需求,当然为了保证网站的稳定性和安全性,我们会把网站的应用部署到至少两台机器上,后台的存储使用数据库,如果经济实力允许,数据库使用单台服务器部署,由于数据是网站的生命线,因此我们常常会把部署数据库的服务器使用的好点,这个网站结构如下所示:
这个结构非常简单,其实大部分初建网站开发里往往业务逻辑没有企业级系统那么复杂,所以只要有个好的idea,建设一个新网站的成本是非常低的,所使用的技术手段也是非常的基本和简单,不过该图我们要准备三台服务器,而且还要租个机房放置我们的服务器,这些成本对于草根和屌丝还是非常高的,幸运的是当下很多大公司和机构提供了云平台,我们可以花费很少的钱将自己的应用部署到云平台上,这种做法我们甚至不用去考虑把应用、数据库分开部署的问题,更加进一步的降低了网站开发和运维的成本,但是这种做法也有一个问题,就是网站的小命被这个云平台捏住了,如果云平台挂了,俺们的网站服务也就跟着挂了。
这里我先讲讲自己独立使用服务器部署网站的问题,如果我们要把网站服务应用使用多台服务器部署,这么做的目的一般有两个:
不过要做到以上两点,并不是我们简单将网站分开部署就可以满足的,因为大多数网站在用户使用时候都是要保持用户的状态,具体点就是网站要记住请求是归属到那一个客户端,而这个状态在网站开发里就是通过会话session来体现的。分开部署的web应用服务要解决的一个首要问题就是要保持不同物理部署服务器之间的session同步问题,从而达到当用户第一次请求访问到服务器A,第二个请求访问到服务器B,网站任然知道这两个请求是同一个人,解决方案很直接:服务器A和服务器B上的session信息要时刻保持同步,那么如何保证两台服务器之间session信息的同步呢?
为了回答上面的问题,我们首先要理解下session的机制,session信息在web容器里都是存储在内存里的,web容器会给每个连接它的客户端生成一个sessionid值,这个sessionid值会被web容器置于http协议里的cookie域下,当响应被客户端处理后,客户端本地会存储这个sessionid值,用户以后的每个请求都会让这个sessionid值随cookie一起传递到服务器,服务器通过sessionid找到内存中存储的该用户的session内容,session在内存的数据结构是一个map的格式。那么为了保证不同服务器之间的session共享,那么最直接的方案就是让服务器之间session不断的传递和复制,例如java开发里常用的tomcat容器就采用这种方案,以前我测试过tomcat这种session同步的性能,我发现当需要同步的web容器越多,web应用所能承载的并发数并没有因为服务器的增加而线性提升,当服务器数量达到一个临界值后,整个web应用的并发数甚至还会下降,为什么会这样了?
原因很简单,不同服务器之间session的传递和复制会消耗服务器本身的系统资源,当服务器数量越大,消耗的资源越多,当用户请求越频繁,系统消耗资源也会越来越大。如果我们多部署服务器的目的只是想保证系统的稳定性,采用这种方案还是不错的,不过web应用最好部署少点,这样才不会影响到web应用的性能问题,如果我们还想提升网站的并发量那么就得采取其他的方案了。
时下使用的比较多的方案就是使用独立的缓存服务器,也就是将session的数据存储在一台独立的服务器上,如果觉得存在一台服务器不安全,那么可以使用memcached这样的分布式缓存服务器进行存储,这样既可以满足了网站稳定性问题也提升了网站的并发能力。
不过早期的淘宝在这个问题解决更加巧妙,他们将session的信息直接存储到浏览器的cookie里,每次请求cookie信息都会随着http一起传递到web服务器,这样就避免了web服务器之间session信息同步的问题,这种方案会让很多人诟病,诟病的原因是cookie的不安全性是总所周知的,如果有人恶意截取cookie信息那么网站不就不安全了吗?这个答案还真不好说,但是我觉得我们仅仅是跟踪用户的状态,把session存在cookie里其实也没什么大不了的。
其实如此专业的淘宝这么做其实还是很有深意的,还记得本文开篇提到的hao123网站,它是可以承载高并发的网站,它之所以可以做到这一点,原因很简单它是个静态网站,静态网站的特点就是不需要记录用户的状态,静态网站的服务器不需要使用宝贵的系统资源来存储大量的session会话信息,这样它就有更多系统资源来处理请求,而早期淘宝将cookie存在客户端也是为了达到这样的目的,所以这个方案在淘宝网站架构里还是使用了很长时间的。
在我的公司里客户端的请求到达web服务器之前,会先到F5,F5是一个用来做负载均衡的硬件设备,它的作用是将用户请求均匀的分发到后台的服务器集群,F5是硬件的负载均衡解决方案,如果我们没那么多钱买这样的设备,也有软件的负载均衡解决方案,这个方案就是大名鼎鼎的LVS了,这些负载均衡设备除了可以分发请求外它们还有个能力,这个能力是根据http协议的特点设计的,一个http请求从客户端到达最终的存储服务器之前可能会经过很多不同的设备,如果我们把一个请求比作高速公路上的一辆汽车,这些设备也可以叫做这些节点就是高速路上的收费站,这些收费站都能根据自己的需求改变http报文的内容,所以负载均衡设备可以记住每个sessionid值对应的后台服务器,当一个带有sessionid值的请求通过负载均衡设备时候,负载均衡设备会根据该sessionid值直接找到指定的web服务器,这种做法有个专有名词就是session粘滞,这种做法也比那种session信息在不同服务器之间拷贝复制要高效,不过该做法还是比存cookie的效率低下,而且对于网站的稳定性也有一定影响即如果某台服务器挂掉了,那么连接到该服务器的用户的会话都会失效。
解决session的问题的本质也就是解决session的存储问题,其本质也就是解决网站的存储问题,一个初建的网站在早期的运营期需要解决的问题基本都是由存储导致的。上文里我提到时下很多新建的web应用会将服务器部署后云平台里,好的云平台里或许会帮助我们解决负载均衡和session同步的问题,但是云平台里有个问题很难解决那就是数据库的存储问题,如果我们使用的云平台发生了重大事故,导致云平台存储的数据丢失,这种会不会导致我们在云平台里数据库的信息也会丢失了,虽然这个事情的概率不高,但是发生这种事情的几率还是有的,虽然很多云平台都声称自己多么可靠,但是真实可靠性有多高不是局中人还真不清楚哦,因此使用云平台我们首要考虑的就是要做好数据备份,假如真发生了数据丢失,对于一个快速成长的网站而言可能非常致命。
写到这里一个婴儿般的网站就这样被我们创造出来了,我们希望网站能健康快速的成长,如果网站真的按我们预期成长了,那么一定会有一天我们制造的宝宝屋已经满足不了现实的需求,这个时候我们应该如何抉择了?换掉,全部换掉,使用新的架构例如我们以前长提的SOA架构,分布式技术,这个方法不错,但是SOA和分布式技术是很难的,成本是很高的,如果这时候我们通过添加几台服务器就能解决问题的话,我们绝对不要去选择什么分布式技术,因为这个成本太高了。上面我讲到几种session共享的方案,这个方案解决了应用的水平扩展问题,那么当我们网站出现瓶颈时候就多加几台服务器不就行了吗?那么这里就有个问题了,当网站成长很快,网站首先碰到的瓶颈到底是哪个方面的问题?
本人是做金融网站的,我们所做的网站有个特点就是当用户访问到我们所做的网站时候,目的都很明确就是为了付钱,用户到了我们所做的网站时候都希望能快点,再快点完成本网站的操作,很多用户在使用我们做的网站时候不太去关心网站的其他内容,因此我们所做的网站相对于数据库而言就是读写比例其实非常的均匀,甚至很多场景写比读要高,这个特点是很多专业服务网站的特点,其实这样的网站和企业开发的特点很类似:业务操作的重要度超过了业务展示的重要度,因此专业性网站吸纳企业系统开发的特点比较多。但是大部分我们日常常用的网站,我们逗留时间很长的网站按数据库角度而言往往是读远远大于写,例如大众点评网站它的读写比率往往是9比1。
12306或许是中国最著名的网站之一,我记得12306早期经常出现一个问题就是用户登录老是登不上,甚至在高峰期整个网站挂掉,页面显示503网站拒绝访问的问题,这个现象很好理解就是网站并发高了,大量人去登录网站,购票,系统挂掉了,最后所有的人都不能使用网站了。当网站出现503拒绝访问时候,那么这个网站就出现了最致命的问题,解决大用户访问的确是个超级难题,但是当高并发无法避免时候,整个网站都不能使用这个只能说网站设计上发生了致命错误,一个好的网站设计在应对超出自己能力的并发时候我们首先应该是不让他挂掉,因为这种结果是谁都不能使用,我们希望那些在可接受的请求下,让在可接受请求范围内的请求还是可以正常使用,超出的请求可以被拒绝,但是它们绝对不能影响到全网站的稳定性,现在我们看到了12306网站的峰值从未减少过,而且是越变越多,但是12306出现全站挂掉的问题是越来越少了。通过12036网站改变我们更进一步思考下网站的瓶颈问题。
排除一些不可控的因素,网站在高并发下挂掉的原因90%都是因为数据库不堪重负所致,而应用的瓶颈往往只有在解决了存储瓶颈后才会暴露,那么我们要升级网站能力的第一步工作就是提升数据库的承载能力,对于读远大于写的网站我们采取的方式就是将数据库从读写这个角度拆分,具体操作就是将数据库读写分离,如下图所示:
我们这时要设计两个数据库,一个数据库主要负责写操作我们称之为主库,一个数据库专门负责读操作我们称之为副库,副库的数据都是从主库导入的,数据库的读写分离可以有效的保证关键数据的安全性,但是有个缺点就是当用户浏览数据时候,读的数据都会有点延时,这种延时比起全站不可用那肯定是可以接受的。不过针对12306的场景,仅仅读写分离还是远远不够的,特别是负责读操作的副库,在高访问下也是很容易达到性能的瓶颈的,那么我们就得使用新的解决方案:使用分布式缓存,不过缓存的缺点就是不能有效的实时更新,因此我们使用缓存前首先要对读操作的数据进行分类,对于那些经常不发生变化的数据可以事先存放到缓存里,缓存的访问效率很高,这样会让读更加高效,同时也减轻了数据库的访问压力。至于用于写操作的主库,因为大部分网站读写的比例是严重失衡,所以让主库达到瓶颈还是比较难的,不过主库也有一个读的压力就是主库和副库的数据同步问题,不过同步时候数据都是批量操作,而不是像请求那样进行少量数据读取操作,读取操作特别多,因此想达到瓶颈还是有一定的难度的。听人说,美国牛逼的facebook对数据的任何操作都是事先合并为批量操作,从而达到减轻数据库压力的目的。
上面的方案我们可以保证在高并发下网站的稳定性,但是针对于读,如果数据量太大了,就算网站不挂掉了,用户能很快的在海量数据里检索到所需要的信息又成为了网站的一个瓶颈,如果用户需要很长时间才能获得自己想要的数据,很多用户会失去耐心从而放弃对网站的使用,那么这个问题又该如何解决了?
解决方案就是我们经常使用的百度,谷歌哪里得来,对于海量数据的读我们可以采用搜索技术,我们可以将数据库的数据导出到文件里,对文件建立索引,使用倒排索引技术来检索信息,我们看到了百度,谷歌有整个互联网的信息我们任然能很快的检索到数据,搜索技术是解决快速读取数据的一个有效方案,不过这个读取还是和数据库的读取有所区别的,如果用户查询的数据是通过数据库的主键字段,或者是通过很明确的建立了索引的字段来检索,那么数据库的查询效率是很高的,但是使用网站的人跟喜欢使用一些模糊查询来查找自己的信息,那么这个操作在数据库里就是个like操作,like操作在数据库里效率是很低的,这个时候使用搜索技术的优势就非常明显了,搜索技术非常适合于模糊查询操作。
OK,很晚了,关于存储的问题今天就写在这里,下一篇我将接着这个主题讲解,解决存储问题是很复杂的,下篇我尽量讲仔细点。
上篇里我讲到某些网站在高并发下会报出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实现的原理,当网站使用缓存集群时候,缓存数据是通过一定的算法将缓存数据尽量均匀分不到不同服务器上,如果用户A的缓存在服务器A上,那么服务器B上是没有该用户的缓存数据,早期的memcache数据分布式的算法是根据缓存数据的key即键值计算出一个hash值,这个hash值再除以缓存服务器的个数,得到的余数会对应某一台服务器,例如1对应服务器A,2对应服务器B,那么余数是1的key值缓存就会存储在服务器A上,这样的算法会导致某一台服务器挂掉,那么网站损失的缓存数据的占比就会比较高,为了解决这个问题,memcached引入了一致性hash算法。关于一致性hash网上有很多资料,这里我就贴出一个链接,本文就不做过多论述了。链接地址如下:
http://blog.csdn.net/kongqz/article/details/6695417
一致性hash可以服务器宕机时候这台服务器对整个缓存数据的影响最小。
上文里我讲到了读写分离的设计方案,而读写分离方案主要是应用于网站读写比例严重失衡的网站,而互联网上绝大部分网站都是读操作的比例远远大于写操作,这是网站的主流,如果一个网站读写比例比较均衡,那么这个网站一般都是提供专业服务的网站,这种网站对于个人而言是一个提供生活便利的工具,它们和企业软件类似。大部分关注大型网站架构技术关心的重点应该是那种对于读写比例失衡的网站,因为它们做起来更加有挑战性。
将数据库进行读写分离是网站解决存储瓶颈的第一步,为什么说是第一步呢?因为读写分离从业务角度而言它是一种粗粒度的数据拆分,因此它所包含的业务复杂度比较低,容易操作和被掌控,从技术而言,实现手段也相对简单,因此读写分离是一种低成本解决存储瓶颈的一种手段,这种方案是一种改良方案而不是革命性的的方案,不管是从难度,还是影响范围或者是经济成本角度考虑都是很容易让相关方接受的。
那么我们仅仅将数据库做读写分离为何能产生好的效率了?回答这个问题我们首先要了解下硬盘的机制,硬盘的物理机制就有一个大圆盘飞速旋转,然后有个磁头不断扫描这个大圆盘,这样的物理机制就会导致硬盘数据的顺序操作比随机操作效率更高,这点对于硬盘的读和写还算公平,但是写操作在高并发情况下会有点复杂,写操作有个特性就是我们要保证写操作的准确性,但是高并发下可能会出现多个用户同时修改某一条数据,为了保证数据能被准确的修改,那么我们通常要把并行的操作转变为串行操作,这个时候就会出现一个锁机制,锁机制的实现是很复杂的,它会消耗很多系统性能,如果写操作掺杂了读操作情况就更复杂,效率会更加低效,相对于写操作读操作就单纯多了,如果我们的数据只有读操作,那么读的性能也就是硬盘顺序读能力和随机读能力的体现,即使掺杂了并发也不会对其有很大的影响,因此如果把读操作和写操作分离,效率自然会得到很大提升。
既然读写分离可以提升存储系统的效率,那么为什么我们又要引入缓存系统和搜索技术了?缓存将数据存在内存中,内存效率是硬盘的几万倍,这样的好处不言而喻,而选择搜索技术的背后的原理就不同了,数据库存储的数据称之为结构化数据,结构化数据的限制很多,当结构化数据遇到了千变万化的随机访问时候,其效率会变得异常低效,但是如果一个网站不能提供灵活、高效的随机访问能力,那么这个网站就会变得单板没有活力,例如我们在淘宝里查找我们想要的商品,但是时常我们并不清楚自己到底想买啥,如果是在实体店里店员会引导我们的消费,但是网站又如何引导我们的消费,那么我们必须要赋予网站通过人们简单意向随机找到各种不同的商品,这个对于数据库就是一个like操作的,但是数据里数据量达到了一定规模以后like的低效是无法让人忍受的,这时候搜索技术在随机访问的能力正好可以弥补数据库这块的不足。
业务再接着的增长下去,数据量也会随之越来越大了,这样发展下去总有一天主库也会产生瓶颈了,那么接下来我们又该如何解决主库的瓶颈了?方法很简单就是我们要拆分主库的数据了,那么我该以什么维度拆分数据了?一个数据库里有很多张表,不同的表都针对不同的业务,网站的不同业务所带来的数据量也不是不同的,这个时候系统的短板就是那些数据量最大的表,所以我们要把那些会让数据库产生瓶颈的表拆出来,例如电商系统里商品表和交易表往往数据量非常大,那么我们可以把这两种表建立在单独的两个数据库里,这样就拆分了数据库的压力,这种做法叫做数据垂直拆分,不过垂直拆分会给原有的数据库查询,特别是有事务的相关操作产生影响,这些问题我们必须要进行改造,关于这个问题,我将在下篇里进行讨论。
当我们的系统做完了读写分离,数据垂直拆分后,我们的网站还在迅猛发展,最终一定又会达到新的数据库瓶颈,当然这些瓶颈首先还是出现在那些数据量大的表里,这些表数据的处理已经超出了单台服务器的能力,这个时候我们就得对这个单库单表的数据进行更进一步的拆分,也就是将一张表分布到两台不同的数据库里,这个做法就是叫做数据的水平拆分了。
Ok,今天内容就讲到这里了,有这两篇文章我们可以理出一个解决大型网站数据瓶颈的一个脉络了,具体如下:
单库数据库-->数据库读写分离-->缓存技术-->搜索技术-->数据的垂直拆分-->数据的水平拆分
以上的每个技术细节在具体实现中可能存在很大的不同,但是问题的缘由大致是一致的,我们理清这个脉络就是想告诉大家我们如果碰到这样的问题应该按何种思路进行思考和设计解决方案,好了,今天就写到这里了,晚安。
存储的瓶颈写到现在就要进入到深水区了,如果我们所做的网站已经到了做数据库垂直拆分和水平拆分的阶段,那么此时我们所面临的技术难度的挑战也会大大增强。
这里我们先回顾下数据库的垂直拆分和水平拆分的定义:
垂直拆分:把一个数据库中不同业务单元的数据分到不同的数据库里。
水平拆分:是根据一定的规则把同一业务单元的数据拆分到多个数据库里。
垂直拆分是一个粗粒度的拆分数据,它主要是将原来在一个数据库下的表拆分到不同的数据库里,水平拆分粒度比垂直拆分要更细点,它是将一张表拆到不同数据库里,粒度的粗细也会导致实现技术的难度的也不一样,很明显水平拆分的技术难度要远大于垂直拆分的技术难度。难度意味着投入的成本的增加以及我们需要承担的风险的加大,我们做系统开发一定要有个清晰的认识:能用简单的方案解决问题,就一定要毫不犹豫的舍弃复杂的方案,当系统需要使用高难度技术的时候,我们一定要让自己感受到这是迫不得已的。
我是以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. 对于分布式事务的处理,我们尽量要从具体问题具体分析,不要一感觉这个事务操作本质是分布式事务就去寻找通用的分布式事务技术手段,这样的想法其实是回避困难的思想,结果可能会是把问题搞得更加复杂。
好了,今天就写到这里吧,祝大家晚安,生活愉快!
如果数据库需要进行水平拆分,这其实是一件很开心的事情,因为它代表公司的业务正在迅猛的增长,对于开发人员而言那就是有不尽的项目可以做,虽然会感觉很忙,但是人过的充实,心里也踏实。
数据库水平拆分简单说来就是先将原数据库里的一张表在做垂直拆分出来放置在单独的数据库和单独的表里后更进一步的把本来是一个整体的表进一步拆分成多张表,每一张表都用独立的数据库进行存储。当表被水平拆分后,原数据表成为了一个逻辑的概念,而这个逻辑表的业务含义需要多张物理表协同完成,因此数据库的表被水平拆分后,那么我们对这张表的操作已经超出了数据库本身提供给我们现有的手段,换句话说我们对表的操作会超出数据库本身所拥有的处理能力,这个时候我就需要设计相关的方案来弥补数据库缺失的能力,这就是数据库水平拆分最大的技术难点所在。
数据库的水平拆分是数据库垂直拆分的升级版,它和垂直拆分更像继承机制里的父子关系,因此水平拆分后,垂直拆分所遇到的join查询的问题以及分布式事务的问题任然存在,由于表被物理拆解增加了逻辑表的维度,这也给垂直拆分里碰到的两个难题增加了更多的维度,因此水平拆分里join查询的问题和分布式事务会变得更加复杂。水平拆分除了垂直拆分两个难题外,它还会产生新的技术难题,这些难题具体如下:
难题一:数据库的表被水平拆分后,该表的主键设计会变得十分困难;
难题二:原来单表的查询逻辑会面临挑战。
在准备本篇文章时候,我看到一些资料里还提到了一些难题,这些难题是:
难题三:水平拆分表后,外键的设计也会变得十分困难;
难题四:这个难题是针对数据的新增操作的,大致的意思是,我们到底按什么规则把需要存储的数据存储在拆分出的那个具体的物理数据表里。
难题三的问题,我在上篇已经给出了解答,这里我进行一定的补充,其实外键问题在垂直拆分就已经存在,不过在讲垂直拆分时候我们没有讲到这个问题,这主要是我设定了一个前提,就是数据表在最原始的数据建模阶段就要抛弃所有外键的设计,并将外键的逻辑抛给服务层去完成,我们要尽全力减轻数据库承担的运算压力,其实除了减轻数据库运算压力外,我们还要将作为存储原子的表保持相对的独立性,互不关联,那么要做到这点最直接的办法就是去掉表与表之间关联的象征:外键,这样我们就可以从根基上为将来数据库做垂直拆分和水平拆分打下坚实的基础。
至于难题四,其实问题的本质是分库分表后具体的数据在哪里落地的问题,而数据存储在表里的关键障碍其实就是主键,试想一下,我们设计张表,所有字段我们都准许可以为空,但是表里有个字段是绝对不能为空的,那就是主键,主键是数据在数据库里身份的象征,因此我们在主键设计上是可以体现出该数据的落地规则,那么难题四也会随之解决。因此下文我会重点讲解前两个水平拆分的难题。
首先是水平拆分里的主键设计问题,抛开所有主键所能代表的业务含义,数据库里标的主键本质是表达表里的某一条记录的唯一性,在设计数据库的时候我们可以由一个绝对不可重复的字段表示主键,也可以使用多个字段组合起来表达这种唯一性,使用一个字段表示主键,这已经是很原子级的操作,没法做进一步的修改,但是如果使用多个字段表示一个主键对于水平拆分而言就会碰到问题了,这个问题主要是体现在数据到底落地于哪个数据库,关于主键对数据落地的影响我会在把相关知识讲解完毕后再着重阐述,这里要提的是当碰到联合主键时候我们可以设定一个没有任何业务含义的字段来替代,不过这个要看场景了,我倾向于将联合主键各个字段里的值合并为一个字段来表示主键,如果有的朋友认为这样会导致数据冗余,那么可以干脆去掉原来做联合主键的相关字段就是用一个字段表示,只不过归并字段时候使用一个分隔符,这样方便服务层进行业务上的拆分。
由上所述,这里我给出水平拆分主键设计的第一个原则:被水平拆分的表的主键设计最好使用一个字段表示。
如果我们的主键只是表达记录唯一性的话,那么水平拆分时候相对要简单的多,例如在Oracle数据库里有一个sequence机制,这其实就是一个自增数的算法,自增机制几乎所有关系数据库都有,也是我们平时最喜欢使用的主键字段设计方案,如果我们要拆分的表,使用了自增字段,同时这个自增字段只是用来表达记录唯一性,那么水平拆分时候处理起来就简单多了,我这里给出两个经典方案,方案如下:
方案一:自增列都有设定步长的特性,假如我们打算把一张表只拆分为两个物理表,那么我们可以在其中一张表里把主键的自增列的步长设计为2,起始值为1,那么它的自增规律就是1,3,5,7依次类推,另外一张物理表的步长我们也可以设置为2,如果起始值为2,那么自增规律就是2,4,6,8以此类推,这样两张表的主键就绝对不会重复了,而且我们也不用另外做两张物理表相应的逻辑关联了。这种方案还有个潜在的好处,那就是步长的大小和水平数据拆分的粒度关联,也是我们为水平拆分的扩容留有余量,例如我们把步长设计为9,那么理论上水平拆分的物理表可以扩容到9个。
方案二:拆分出的物理表我们允许它最多存储多少数据,我们其实事先通过一定业务技术规则大致估算出来,假如我们估算一张表我们最多让它存储2亿条,那么我们可以这么设定自增列的规律,第一张物理表自增列从1开始,步长就设为1,第二种物理表的自增列则从2亿开始,步长也设为1,自增列都做最大值的限制,其他的依次类推。
那么如果表的主键不是使用自增列,而是业务设计的唯一字段,那么我们又如何处理主键分布问题了?这种场景很典型,例如交易网站里一定会有订单表,流水表这样的设计,订单表里有订单号,流水表里有流水号,这些编号都是按一定业务规则定义并且保证它的唯一性,那么前面的自增列的解决方案就没法完成它们做水平拆分的主键问题,那么碰到这个情况我们又该如何解决了?我们仔细回味下数据库的水平拆分,它其实和分布式缓存何其的类似,数据库的主键就相当于分布式缓存里的键值,那么我们可以按照分布式缓存的方案来设计主键的模型,方案如下:
方案一:使用整数哈希求余的算法,字符串如果进行哈希运算会得出一个值,这个值是该字符串的唯一标志,如果我们稍微改变下字符串的内容,计算的哈希值肯定是不同,两个不同的哈希值对应两个不同字符串,一个哈希值有且只对应唯一一个字符串,加密算法里的MD5,SHA都是使用哈希算法的原理计算出一个唯一标示的哈希值,通过哈希值的匹配可以判断数据是否被篡改过。不过大多数哈希算法最后得出的值都是一个字符加数字的组合,这里我使用整数哈希算法,这样计算出的哈希值就是一个整数。接下来我们就要统计下我们用于做水平拆分的服务器的数量,假如服务器的数量是3个,那么接着我们将计算的整数哈希值除以服务器的数量即取模计算,通过得到的余数来选择服务器,该算法的原理图如下所示:
方案二:就是方案一的升级版一致性哈希,一致性哈希最大的作用是保证当我们要扩展物理数据表的数量时候以及物理表集群中某台服务器失效时候才会体现,这个问题我后续文章会详细讨论物理数据库扩容的问题,因此这里先不展开讨论了。
由上所述,我们发现在数据库进行水平拆分时候,我们设定的算法都是通过主键唯一性进行的,根据主键唯一性设计的特点,最终数据落地于哪个物理数据库也是由主键的设计原则所决定的,回到上文里我提到的如果原库的数据表使用联合字段设计主键,那么我们就必须首先合并联合主键字段,然后通过上面的算法来确定数据的落地规则,虽然不合并一个字段看起来也不是太麻烦,但是在我多年开发里,把唯一性的字段分割成多个字段,就等于给主键增加了维度,字段越多,维度也就越大,到了具体的业务计算了我们不得不时刻留心这些维度,结果就很容易出错,我个人认为如果数据库已经到了水平拆分阶段了,那么就说明数据库的存储的重要性大大增强,为了让数据库的存储特性变得纯粹干净,我们就得尽力避免增加数据库设计的复杂性,例如去掉外键,还有这里的合并联合字段为一个字段,其实为了降低难度,哪怕做点必要的冗余也是值得。
解决数据库表的水平拆分后的主键唯一性问题有一个更加直接的方案,这也是很多人碰到此类问题很自然想到的方法,那就是把主键生成规则做成一个主键生成系统,放置在单独一台服务器上统一生成,每次新增数据主键都从这个服务器里获取,主键生成的算法其实很简单,很多语言都有计算UUID的功能,UUID是根据所在服务器的相关的硬件信息计算出的全球唯一的标示,但是这里我并没有首先拿出这个方案,因为它相比如我前面的方案缺点太多了,下面我要细数下它的缺点,具体如下:
缺点一:把主键生成放到外部服务器进行,这样我们就不得不通过网络通信完成主键值的传递,而网络是计算机体系里效率最低效的方式,因此它会影响数据新增的效率,特别是数据量很大时候,新增操作很频繁时候,该缺点会被放大很多;
缺点二:如果我们使用UUID算法做主键生成的算法,因为UUID是依赖单台服务器进行,那么整个水平拆分的物理数据库集群,主键生成器就变成整个体系的短板,而且是关键短板,主键生成服务器如果失效,整个系统都会无法使用,而一张表需要被水平拆分,而且拆分的表是业务表的时候,那么这张表在整个系统里的重要度自然很高,它如果做了水平拆分后出现单点故障,这对于整个系统都是致命的。当然有人肯定说,既然有单点故障,那么我们就做个集群系统,问题不是解决了吗?这个想法的确可以解决我上面阐述的问题,但是我前文讲到过,现实的软件系统开发里我们要坚守一个原则那就是有简单方案尽量选择简单的方案解决问题,引入集群就是引入了分布式系统,这样就为系统开发增加了开发难度和运维风险,如果我们上文的方案就能解决我们的问题,我们何必自讨苦吃做这么复杂的方案呢?
缺点三:使用外部系统生成主键使得我们的水平拆分数据库的方案增加了状态性,而我上面提到的方案都是无状态的,有状态的系统会相互影响,例如使用外部系统生成主键,那么当数据操作增大时候,必然会造成在主键系统上资源竞争的事情发生,如果我们对主键系统上的竞争状态处理不好,很有可能造成主键系统被死锁,这也就会产生我前文里说到的503错误,而无状态的系统是不存在资源竞争和死锁的问题,这洋就提升了系统的健壮性,无状态系统另一个优势就是水平扩展很方便。
这里我列出单独主键生成系统的缺点不是想说明我觉得这种解决方案完全不可取,这个要看具体的业务场景,根据作者我的经验还没有找到一个很合适使用单独主键生成器的场景。
上文里我提出的方案还有个特点就是能保证数据在不同的物理表里均匀的分布,均匀分布能保证不同物理表的负载均衡,这样就不会产生系统热点,也不会让某台服务器比其他服务器做的事情少而闲置资源,均匀分配资源可以有效的利用资源,降低生产的成本提高生产的效率,但是均匀分布式数据往往会给我们业务运算带来很多麻烦。
水平拆分数据库后我们还要考虑水平扩展问题,例如如果我们事先使用了3台服务器完成了水平拆分,如果系统运行到一定阶段,该表又遇到存储瓶颈了,我们就得水平扩容数据库,那么如果我们的水平拆分方案开始设计的不好,那么扩容时候就会碰到很多的麻烦。
以上问题将是我下篇文章里进行讨论的,今天就写到这里,祝大家生活愉快。
上文里我遗留了两个问题,一个问题是数据库做了水平拆分以后,如果我们对主键的设计采取一种均匀分布的策略,那么它对于被水平拆分出的表后续的查询操作将有何种影响,第二个问题就是水平拆分的扩容问题。这两个问题在深入下去,本系列就越来越技术化了,可能最终很多朋友读完后还是没有找到解决实际问题的启迪,而且我觉得这些问题都是像BAT这样巨型互联网公司才会认真思考的,因此本篇我打算换个角度来阐述本文的后续内容。
这里我们首先要明确一个问题,到底是什么因素促使我们去做数据库的垂直拆分和水平拆分的呢?答案很简单就是业务发展的需求,前文里的水平拆分技术方案基本都是抛弃千变万化的业务规则的限制,尽量将水平拆分的问题归为一个简单的技术实现方案,而纯技术手段时常是看起来很美,但是到了面对现实问题时候,常常会变得那么苍白和无力。
水平拆分的难题里我还有个难题没有讲述,就是水平拆分后对查询操作的影响,特别是对单表查询的影响,这点估计也是大伙最为关心的问题,今天我不在延着水平拆分的技术手段演进是阐述上文的遗留问题,而是我要把前面提到的技术手段和一些典型场景结合起来探讨如何解决网站存储的瓶颈问题。
前文中我总结过一个解决存储瓶颈的脉络,具体如下:
单库数据库-->数据库读写分离-->缓存技术-->搜索技术-->数据的垂直拆分-->数据的水平拆分
这个脉络给一些朋友产生了误解,就是认为这个过程应该是个串行的过程,其实在实际的场景下这个过程往往是并行的,但是里面有一个元素应该是串行的或者说思考时候有个先后问题,那就是对数据库层的操作,具体如下:
单库数据库-->数据库读写分离-->数据的垂直拆分-->数据的水平拆分
而缓存技术和搜索技术在数据库的任意阶段里都可以根据实际的业务需求随时切入其中帮助数据库减轻不必要的压力。例如,当网站的后台数据库还是单库的时候,数据库渐渐出现了瓶颈问题,而这个瓶颈又没有达到需要采取大张旗鼓做读写分离方案的程度,那么我这个时候可以考虑引入缓存机制。不过要合理的使用缓存我们首先要明确缓存本身的特点,这些特点如下所示:
特点一:缓存主要是适用于读操作,并且缓存的读操作的效率要远远高于从数据库以及硬盘读取数据的效率。
特点二:缓存的数据是存储在内存当中,因此当系统重启,宕机等等异常场景下,缓存数据就会不可逆的丢失,且无法恢复,因此缓存不能作为可靠存储设备,这就导致一个问题,缓存里的数据必须首先从数据库里同步到内存中,而使用缓存的目的就是为了解决数据库的读操作效率低下的问题,数据库的数据同步到缓存的操作会因为数据库的效率低下而在性能上大打折扣,所以缓存适合的场景是那些固定不变的数据以及业务对实时性变化要求不高的数据。
根据缓存的上述两个特点,我们可以把数据库里和上述描述类似操作的相关数据迁移到缓存里,那样我们就从数据库上剥离了那些对数据库价值不高的操作,让数据库专心做有价值的操作,这样也是减轻数据库压力的一种手段。
不过这个手段局限性很强,局限性主要是一台计算机了用于存储缓存的内存的大小都是远远要低于硬盘,并且内存的价格要远贵于硬盘,如果我们将大规模的数据从硬盘往内存迁移,从资源成本和利用率角度考虑性价比还是很低的,因此缓存往往都是用于转存那些不会经常变化的数据字典,以及经常会被读,而修改较少的数据,但是这些数据的规模也是有一定限度的,因此当单库数据库出现了瓶颈时候马上就着手进行读写分离方案的设计性价比还是很高的。
前文我讲到我们之所以选择数据库读写分离是主要原因是因为数据库的读写比例严重失衡所致,但是做了读写分离必然有个问题不可避免,写库向读库同步数据一定会存在一定的时间差,如果我们想减小读库和写库数据的时间差,那么任然会导致读库因为写的粒度过细而发生部分性能的损失,但是时间差过大,或许又会无法满足实际的业务需求,因此这个时间差的设计一定要基于实际的业务需求合理的设计。
同步的时间差的问题还是个小问题,也比较好解决,但是如何根据实际的业务需求做读写分离这其实还是非常有挑战性的,这里我举个很常见的例子来说明读写分离的难度问题,我们这里以淘宝为例,淘宝是个C2C的电商网站,它是互联网公司提供一个平台,商家自助接入这个平台,在这个平台上卖东西,这个和线下很多大卖场的模式类似。淘宝是个大平台,它的交易表里一定是要记下所有商户的交易数据,但是针对单个商家他们只会关心自己的网店的销售数据,这就有一个问题了,如果某一个商家要查询自己的交易信息,淘宝就要从成千上万的交易信息里检索出该商家的交易信息,那么如果我们把所有交易信息放在一个交易表里,肯定有商家会有这样的疑问,我的网店每天交易额不大,为什么我查询交易数据的速度和那些大商家一样慢了?那么我们到底该如何是解决这样的场景了?
碰到这样的情况,当网站的交易规模变大后就算我们把交易表做了读写分离估计也是没法解决实际的问题,就算我们做的彻底点把交易表垂直拆分出来估计还是解决不了问题,因为一个业务数据库拥有很多张表,但是真正压力大的表毕竟是少数,这个符合28原则,而数据库大部分的关键问题又都是在那些数据压力大的表里,就算我们把这些表单独做读写分离甚至做垂直拆分,其实只是把数据库最大的问题迁移出原来数据库,而不是在解决该表的实际问题。
如果我们要解决交易表的问题我们首先要对交易表做业务级的拆分,那么我们要为交易表增加一个业务维度:实时交易和历史交易,一般而言实时交易以当天及当天24小时为界,历史交易则是除去当天交易外的所有历史交易数据。实时交易数据和历史交易数据有着很大不同,实时交易数据读与写是比较均衡的,很多时候估计写的频率会远高于读的频率,但是历史交易表这点上和实时交易就完全不同了,历史交易表的读操作频率会远大于写操作频率,如果我们将交易表做了实时交易和历史交易的拆分后,那么读写分离方案适合的场景是历史交易查询而非实时交易查询,不过历史交易表的数据是从实时交易表里同步过来的,根据这两张表的业务特性,我们可以按如下方案设计,具体如下:
我们可以把实时交易表设计成两张表,把它们分别叫做a表和b表,a表和b表按天交替进行使用,例如今天我们用a表记录实时交易,明天我们就用b表记录实时交易,当然我们事先可以用个配置表记录今天到底使用那张表进行实时交易记录,为什么要如此麻烦的设计实时交易表了?这么做的目的是为了实时交易数据同步到历史数据时候提供便利,一般我们会在凌晨0点切换实时交易表,过期的实时交易表数据会同步到历史交易表里,这个时候需要数据迁移的实时交易表是全表数据迁移,效率是非常低下,假如实时交易表的数据量很大的时候,这种导入同步操作会变得十分耗时,所以我们设计两张实时交易表进行切换来把数据同步的风险降到最低。由此可见,历史交易表每天基本都只做一次写操作,除非同步出了问题,才会重复进行写操作,但是写的次数肯定是很低的,所以历史交易表的读写比例失衡是非常严重的。不过实时交易表的切换也是有技术和业务风险的,为了保证实时交易表的高效性,我们一般在数据同步操作成功后会清空实时交易表的数据,但是我们很难保证这个同步会不会有问题,因此同步时候我们最好做下备份,此外,两个表切换的时候肯定会碰到这样的场景,就是有人在凌晨0点前做了交易,但是这个交易是在零点后做完,假如实时交易表会记录交易状态的演变过程,那么在切换时候就有可能两个实时表的数据没有做好接力,因此我们同步到历史交易表的数据一定要保持一个原则就是已经完成交易的数据,没有完成的交易数据两张实时交易还要完成一个业务上的接力,这就是业界常说的数据库日切的问题。
历史交易表本身就是为读使用的,所以我们从业务角度将交易表拆分成实时交易表和历史交易表本身就是在为交易表做读写分离,居然了设计了历史交易表我们就做的干脆点,把历史交易表做垂直拆分,将它从原数据库里拆分出来独立建表,随着历史交易的增大,上文里所说的某个商户想快速检索出自己的数据的难题并没有得到根本的改善,为了解决这个难题我们就要分析下难题的根源在那里。这个根源很简单就是我们把所有商户的数据不加区别的放进了一张表里,不管是交易量大的商户还是交易量小的商户,想要查询出自己的数据都要进行全表检索,而关系数据库单表记录达到一定数据量后全表检索就会变的异常低效,例如DB2当数据量超过了1亿多,mysql单表超过了100万条后那么全表查询这些表的记录都会存在很大的效率问题,那么我们就得对历史交易表进一步拆分,因为问题根源是单表数据量太大了,那我们就可以对单表的数据进行拆分,把单表分成多表,这个场景就和前面说的水平拆分里把原表变成逻辑表,原表的数据分散到各个独立的逻辑表里的方式一致,不过这里我们没有一开始做水平拆分,那是会把问题变麻烦,我们只要在一个数据库下对单表进行拆分即可,这样也能满足我们的要求,并且避免了水平拆分下的跨库写作的难题。接下来我们又有一个问题了那就是我们按什么维度拆分这张单表呢?
我们按照前文讲到的水平拆分里主键设计方案执行吗?当然不行哦,因为那些方案明显提升不了商户检索数据的效率问题,所以我们要首先分析下商户检索数据的方式,商户一般会按这几个维度检索数据,这些维度分别是:商户号、交易时间、交易类型,当然还有其他的维度,我这里就以这三个维度为例阐述下面的内容,商户查询数据效率低下的根本原因是全表检索,其实商户查询至少有一个维度那就是商户号来进行查询,如果我们把该商户的数据存入到一张单独的表里,自然查询的效率会有很大的提升,但是在实际系统开发里我们很少通过商户号进行拆分表,这是为什么呢?因为一个电商平台的商户是个动态的指标,会经常发生变化,其次,商户号的粒度很细,如果使用商户号拆分表的必然会有这样的后果那就是我们可能要频繁的建表,随着商户的增加表的数量也会增加,造成数据的碎片化,同时不同的商户交易量是不一样的,按商户建表会造成数据存储的严重不平衡。如果使用交易类型来拆分表,虽然维度的粒度比商户号小,但是会造成数据的分散化,也就是说我们查询一个商户的全部交易数据会存在很大问题。由此可见拆表时候如何有效的控制维度的粒度以及数据的聚集度是拆分的关键所在,因为使用交易时间这个维度就会让拆分更加合理,不过时间的维度的设计也是很有学问的,下面我们看看腾讯分析的维度,如下所示:
腾讯分析的维度是今天这个其实相当于实时交易查询,除此之外都是对历史数据查询,它们分为昨天、最近7天和最近30天,我们如果要对历史交易表进行拆分也是可以参照腾讯分析的维度进行,不过不管我们选择什么维度拆分数据,那么都是牺牲该维度成全了其他维度,例如我们按腾讯分析的维度拆分数据,那么我们想灵活使用时间查询数据将会受到限制。
我们把历史交易数据通过交易时间维度进行了拆分,虽然得到了效率提升,但是历史交易数据表是个累积表,随着时间推移,首先是月表,接下来是周表都会因为数据累积产生查询效率低下的问题,这个时候我们又该如何解决了?这个时候我们需要再引进一个维度,那么这个时候我们可以选择商户号这个维度,但是商户号作为拆分维度是有一定问题的,因为会造成数据分布不均衡,那么我们就得将维度的粒度由小变粗,其实一个电商平台上往往少数商户是完成了大部分电商平台的交易,因此我们可以根据一定指标把重要商户拆分出来,单独建表,这样就可以平衡了数据的分布问题。
我们总结下上面的案例,我们会得到很多的启迪,我将这些启迪总结如下:
启迪一:数据库的读写分离不是简单的把主库数据导入到读库里就能解决问题,读数据库和写数据的分离的目的是为了让读和写操作不能相互影响效率。
启迪二:解决读的瓶颈问题的本质是减少数据的检索范围,数据检索的范围越小,读的效率也就越高;
启迪三:数据库的垂直拆分和水平拆分首先不应该从技术角度进行,而是通过业务角度进行,如果数据库进行业务角度的水平拆分,那么拆分的维度往往是要根据该表的某个字段进行的,这个字段选择要有一定原则,这个原则主要是该字段的维度的粒度不能过细,该字段的维度范围不能经常的动态发生变化,最后就是该维度不能让数据分布严重失衡。
回到现实的开发里,对于一个数据库做拆表,分表的工作其实是一件很让人恼火的工作,这主要是有以下原因所造成的,具体如下所述:
原因一:一个数据库其实容纳多少张表是有一定限制的,就算没有超过这个限制,如果原库本来有30张表,我们拆分后变成了60张,接着是120张,那么数据库本身管理这么多表也会消耗很多性能,因此公司的DBA往往会控制那些过多分表的行为。
原因二:每次拆表后,都会牵涉到历史数据的迁移问题,这个迁移风险很大,迁移方案如果设计的不完善可能会导致数据丢失或者损坏,如果关键数据发生了丢失和损坏,结果可能非常致命。因此在设计数据库分表分库方案时候我们要尽量让受影响的数据范围变得最小。
原因三:每次拆表和分表都会让系统的相关方绷紧神经,方案执行后,会有很长时间的监控和观察期,所以拆数据库时常是一件令人讨厌的事情。
原因四:为了保证新方案执行后确保系统没有问题,我们常常会让新旧系统并行运行一段时间,这样可以保证如果新方案出现问题,问题的影响面最低,但是这种做法也有一个恶果就是会导致数据迁移方案要进行动态调整,从而增加迁移数据的风险
因此当公司不得不做这件事情时候,公司都会很自然去考虑第三种解决方案,第三种解决方案是指尽量不改变原数据库的功能,而是另起炉灶,使用新技术来解决我们的问题,例如前文所说的搜索技术解决数据库like的低效问题就是其中方案之一,该方案只要我们将数据库的表按一定时间导入到文件系统,然后对文件建立倒排索引,让like查询效率更好,这样就不用改变原数据库的功能,又能减轻数据库的压力。
现在常用的第三种解决方案就是使用NoSql数据库,NoSql数据库大多都是针对文件进行的,因此我们可以和使用搜索引擎那样把数据导入到文件里就行了,NoSql基本都采用Key/Value这种简单的数据结构,这种数据结构和关系数据库比起来更加的灵活,对原始数据的约束最少,所以在NoSql数据库里建表我们可以很灵活的把列和行的特性交叉起来用,这句话可能很多人不太理解,下面我举个例子解释下,例如hadoop技术体系里的hbase,hbase是一个基于列族的数据库,使用hbase时候我们就可以通过列来灵活的拆分数据,比如我们可以把中国的省份作为一个列,将该省份的数据都放入到这个列下面,在省这个维度下我们可以接着在定义一个列的维度,例如软件行业,属于软件行业的数据放在这个列下面,最终提供用户查询时候我们就可以减少数据检索的范围,最终达到提升查询效率的目的。由此可见当我们用惯了关系数据库后,学习像hbase这样的Nosql数据库我们会非常的不适应,因为关系数据库的表有固定模式,也就是我们常说的结构化数据,当表的定义好了后,就算里面没有数据,那么这个结构也就固定了,我们使用表的时候都是按这个模型下面,我们几乎感觉不到它,但是到了hbase的使用就不同了,hbase使用时候我们都在不停的为数据增加结构化模型,而且这个维度是以列为维度的,而关系数据库里列确定后我们使用时候是无法改变的,这就是学习hbase的最大困难之一。Hbase之所以这么麻烦的设计这样的计算模型,终极目的就是为了让海量数据按不同维度存储起来,使用时候尽全力检索数据检索的数量,从而达到海量数据快速读取的目的。
好了,今天就写到这里,祝大家生活愉快。
在讲数据库水平拆分时候,我列出了水平拆分数据库需要解决的两个难题,它们分别是主键的设计问题和单表查询的问题,主键问题前文已经做了比较详细的讲述了,但是第二个问题我没有讲述,今天我将会讲讲如何解决数据表被垂直拆分后的单表查询问题。
要解决数据表被水平拆分后的单表查询问题,我们首先要回到问题的源头,我们为什么需要将数据库的表进行水平拆分。下面我们来推导下我们最终下定决心做水平拆分表的演进过程,具体如下:
第一个演进过程:进行了读写分离的表在数据增长后需要进行水平拆分吗?回答这个疑问我们首先要想想进行读写分离操作的表真的是因为数据量大吗?答案其实是否定的。最基本的读写分离的目的是为了解决数据库的某张表读写比率严重失衡的问题,举个例子,有一张表每天会增加1万条数据,也就是说我们的系统每天会向这张表做1万次写的操作,当然也有可能我们还会更新或者删除这张表的某些已有的记录,这些操作我们把它归并到写操作,那么这张表一天我们随意定义个估值吧2万5千次写操作,其实这种表的数据量并不大,一年下来也就新增的几百万条数据,一个大型的商业级别的关系数据库,当我们为表建立好索引和分区后,查询几百万条数据它的效率并不低,这么说来查询的效率问题还不一定是读写分离的源头。其实啊,这张表除了写操作每天还承受的读操作可能会是10万,20万甚至更高,这个时候问题来了,像oracle和mysql这样鼎鼎大名的关系数据库默认的最大连接数是100,一般上了生产环境我们可能会设置为150或者200,这些连接数已经到了这些关系数据库的最大极限了,如果再加以提升,数据库性能会严重下降,最终很有可能导致数据库由于压力过大而变成了一个巨锁,最终导致系统发生503的错误,如是我们就会想到采用读写分离方案,将数据库的读操作迁移到专门的读库里,如果系统的负载指标和我列举的例子相仿,那么迁移的读库甚至不用做什么垂直拆分就能满足实际的业务需求,因为我们的目的只是为了减轻数据库的连接压力。
第二个演进过程:随着公司业务的不断增长,系统的运行的压力也越来越大了,我们已经了解了系统的第一个瓶颈是从存储开始了,如是我们开始谈论方案如何解决存储的问题,这时我们发现我们已经做了读写分离,也使用了缓存,甚至连搜索技术也用上了,那么下个阶段就是垂直拆分了,垂直拆分很简单就是把表从数据库里拆出来,单独建库建表,但是这种直截了当的方案想想就能感到这样的做法似乎没有打中系统的痛点,那么系统的痛点到底是什么呢?根据数据库本身的特性,我们会发现痛点主要是三个方面组成:
第一个方面:数据库的连接数的限制。原库的某些表可能承担数据库80%的连接,极端下甚至可以超过90%的连接,而且这些表的业务操作十分的频繁,当其他小众业务的表需要进行操作时候,搞不好因为连接数被全部占用而不得不排队等待空闲连接的出现,那么这个时候我们就会考虑把这张表做垂直拆分,这样就减轻了原数据库连接的压力,使得数据库连接负载变得比较均衡。
第二个方面是数据库的读操作,第三个方面是数据库的写操作,虽然把读和写分成两个方面,但是这两个方面在我们做垂直拆分时候要结合起来考虑。首先我们要分析下数据库的写操作,单独的写操作效率都是很高的,不管我们的写是单条记录的写操作,还是批量的写操作,这些写操作的数据量就是我们要去写的数据的大小,因此控制写的数据量的大小是一件很容易很天然的操作,所以这些操作不会造成数据库太大负担,详细点的话,对于数据库而言,新增操作无非是在原来数据后面追加些记录,而修改操作或者删除操作一般都是通过建立了高效索引的字段来定位数据后再进行的操作,因此它的性能也是非常高的。而读操作看起来比写操作简单(例如:读操作不存在像事务这些乌七八糟因素的干扰),但是当读操作面对海量数据时候就严重挑战着数据库和硬盘的极限能力,因此读操作很容易产生瓶颈问题,而且这个瓶颈不管问题表是否读写失衡都会面临的。前文里我详细列举了一个交易表设计的案例,其中我们可以看到数据库垂直拆分在实际应用里的运用,在例子里我们首先根据业务特点将交易表分成了实时交易表和历史交易表,这个做法其实就是将原交易表的读和写进行分离,但是这种分离和纯粹的读写分离相比会更加有深意,这个深意就是拆分实时和历史交易表也就是在分拆原表的读写操作的关联性,换句话说,如果我们不这么做的话,那么交易表的每次写和每次读几乎等价,这样我们没法单独解决读的性能问题,分出了历史交易表后我们再对历史交易表来做读的优化,那么这也不会影响到写操作,这样把问题的复杂度给降低了。在案例里我们对历史交易表进行了业务级别的水平拆分,但是这个拆分是以如何提升读的效率进行的,因此前文讲到的水平拆分里主键设计方案基本上派不上用场,因为这两种水平拆分的出发点是不同的,那么使用的手段和达到效果也将不一样。
由上所述,我们可以把数据库的水平拆分重新定义下,我在这几篇文章里一直讲述的水平拆分本质是从数据库技术来定义的,我把它们称为狭义的水平拆分,与狭义相对的就是广义的水平拆分,例如上文例子里把交易表根据业务特性分为实时交易表和历史交易表,这种行为也是一种水平拆分,但是这个拆分不会遵守我前面讲到主键设计方案,但是它的确达到水平拆分的目的,所以这样的水平拆分就属于广义的水平拆分了。
第三个演进过程:到了三个演进过程我们就会考虑到真正的水平拆分了,也就是上面提到的狭义的水平拆分了,狭义的水平拆分执行的理由有两个,一个那就是数据量太大了,另一个是数据表的读写的关联性很难进行拆分了,这点和垂直拆分有所不同,做垂直拆分的考虑不一定是因为数据量过大,例如某种表数据量不大,但是负载过重,很容易让数据库达到连接的极限值,我们也会采取垂直拆分手段来解决问题,此外,我们想减轻写操作和读操作的关联性,从而能单独对有瓶颈的写操作或读操作做优化设计,那么我们也会考虑到垂直拆分,当然数据量实在是太大的表我们想优化,首先也会考虑到垂直拆分,因为垂直拆分是针对海量数据优化的起始手段,但是垂直拆分可不一定能解决海量数据的问题。
狭义水平拆分的使用的前提是因为数据量太大,到底多大了,我们举个例子来说明下,假如某个电商平台一天的交易笔数有2亿笔,我们用来存储数据的关系数据库单表记录到了5千万条后,查询性能就会严重下降,那么如果我们把这两亿条数据全部存进这个数据库,那么随着数据的累积,实时交易查询基本已经没法正常完成了,这个时候我们就得考虑把实时交易表进行狭义的水平拆分,狭义的水平拆分首先碰到的难点就是主键设计的问题,主键设计问题也就说明狭义水平拆分其实解决的是海量数据写的问题,如果这张表读操作很少,或者基本没有,这个水平拆分是很好设计的,但是一张表只写不读,对于作为业务系统的后台数据库那基本是非常罕见的,。
前文讲到的主键设计方案其实基本没有什么业务上的意义,它解决的主要问题是让写入的数据分布均匀,从而能合理使用存储资源,但是这个合理分布式存储资源却会给查询操作带来极大的问题,甚至有时可以说狭义水平拆分后数据查询变得困难就是由这种看起来合理的主键设计方案所致。
我们还是以实时交易表的实例来说明问题,一个电商平台下会接入很多不同的商户,但是不同的商户每天产生的交易量是不同,也就是说商户的维度会让我们使交易数据变得严重的不均衡,可能电商平台下不到5%的商户完成了全天交易量的80%,而其他95%的商户仅仅完成20%的交易量,但是作为业务系统的数据表,进行读操作首先被限制和约束的条件就是商户号,如果要为我们设计的实时交易表进行狭义的水平拆分,做拆分前我们要明确这个拆分是由交易量大的少量商户所致,而不是全部的商户所致的。如果按照均匀分布主键的设计方案,不加商户区分的分布数据,那么就会发生产生少量交易数据的商户的查询行为也要承受交易量大的商户数据的影响,而能产生大量交易数据的商户也没有因为自己的贡献度而得到应有的高级服务,碰到这个问题其实非常好解决,就是在做狭义水平拆分前,我们先做一次广义的水平拆分,把交易量大的商户交易和交易量小的商户交易拆分出来,交易量小的商户用一张表记录,这样交易量小的商户也会很happy的查询出需要的数据,心里也是美滋滋的。接下来我们就要对交易量大的商户的交易表开始做狭义的水平拆分了,为这些重点商户做专门的定制化服务。
做狭义水平拆分前,我们有个问题需要过一下,在狭义水平拆分前我们需要先做一下广义的水平拆分吗?这个我这里不好说,具体要看实际的业务场景,但是针对我列举的实时交易的例子而言,我觉得没那个必要,因此拆分出的重点商户交易量本来就很大,每个都在挑战数据库读能力的极限,更重要的是实时交易数据的时间粒度已经很小了,再去做广义水平拆分难度很大,而且很难做好,所以这个时候我们还是直接使用狭义的水平拆分。拆分完毕后我们就要解决查询问题了。
做实时查询的标准做法就是分页查询了,在讲述如何解决分页查询前,我们看看我们在淘宝里搜索【衣服】这个条件的分页情况,如下图所示:
我们看到一共才100页,淘宝上衣服的商品最多了,居然搜索出来的总页数只有100页,这是不是在挑战我们的常识啊,淘宝的这个做法也给我们在实现水平拆分后如何做分页查询一种启迪。要说明这个启迪前我们首先要看看传统的分页是如何做的,传统分页的做法是首先使用select count(1) form table这样的语句查询出需要查询数据的总数,然后再根据每页显示的记录条数,查询出需要显示的记录,然后页面根据记录总数,每页的条数,和查询的结果来完成分页查询。回到我们的交易表实例里,有一个重要商户在做实时交易查询,可是这个时候该商户已经产生了1千万笔交易了,假如每页显示10条,记录那么我们就要分成100万页,这要是真显示在页面上,绝对能让我们这些开发人员像哥伦布发现新大陆那样惊奇,反正我见过的最多分页也就是200多页,还是在百度搜索发现的。其实当数据库一张表的数据量非常大的时候,select的count查询效率就非常低下,这个查询有时也会近似个全表检索,所以count查询还没结束我们就会失去等待结果的耐心了,更不要是说等把数据查询出来了,所以这个时候我们可以学习下淘宝的做法,当商户第一次查询我们准许他查询有限的数据。我自己所做的一个项目的做法就是这样的,当某个商户的交易量实在是很大时候我们其实不会计算数据的总笔数,而是一次性查询出1000条数据,这1000条数据查询出来后存入到缓存里,页面则只分100页,当用户一定要查询100页后的数据,我们再去追加查询,不过实践下来,商户基本很少会查询100页后的数据,常常看了5,6页就会停止查询了。不过商户也时常会有查询全部数据的需求,但是商户有这种需求的目的也不是想在分页查询里看的,一般都是为了比对数据使用的,这个时候我们一般是提供一个发起下载查询全部交易的功能页面,商户根据自己的条件先发起这样的需求,然后我们系统会在后台单独起个线程查询出全部数据,生成一个固定格式的文件,最后通过一些有效手段通知商户数据生成好了,让商户下载文件即可。
对于进行了狭义水平拆分的表做分页查询我们通常都不会是全表查询,而是抽取全局的数据的一部分结果呈现给用户,这个做法其实和很多市场调查的方式类似,市场调查我们通常是找一些样本采集相关数据,通过分析这些样本数据推导出全局的一个发展趋势,那么这些样本选择的合理性就和最终的结论有很大关系,回到狭义水平拆分的表做分页查询,我们为了及时满足用户需求,我们只是取出了全部数据中的一部分,但是这一部分数据是否满足用户的需求,这个问题是很有学问的,如果是交易表,我们往往是按时间先后顺序查询部分数据,所以这里其实使用到了一个时间的维度,其他业务的表可能这个维度会不一样,但肯定是有个维度约束我们到底返回那些部分的数据。这个维度可以用一个专有的名词指代那就是排序,具体点就是要那个字段进行升序还是降序查询,看到这里肯定有人会有异议,那就是这种抽样式的查询,肯定会导致查询的命中率的问题,即查出来的数据不一定全部都是我们要的,其实要想让数据排序正确,最好就是做全量排序,但是一到全量排序那就是全表查询,做海量数据的全表排序查询对于分页这种场景是无法完成的。回到淘宝的例子,我们相信淘宝肯定没有返回全部数据,而是抽取了部分数据分页,也就是淘宝查询时候加入了维度,每个淘宝的店家都希望自己的商户放在搜索的前列,那么淘宝就可以让商家掏钱,付了钱以后淘宝改变下商家在这个维度里的权重,那么商家的商品就可以排名靠前了。
狭义水平拆分的本身对排序也有很大的影响,水平拆分后我们一个分页查询可能要从不同数据库不同的物理表里去取数据,单表下我们可以先通过数据库的排序算法得到一定的数据,但是局部的排序到了全局可能就不正确了,这个又该怎么办了?其实由上面内容我们可以知道要满足对海量数据的所有查询限制是非常难的,时常是根本就无法满足,我们只能做到尽量多满足些查询限制,也就是海量查询只能做到尽量接近查询限制的条件,而很难完全满足,这个时候我前面提到的主键分布方案就能起到作用了,我们在设计狭义水平拆分表主键分布时候是尽量保持数据分布均衡,那么如果我们查询要从多张不同物理表里取的时候,例如我们要查1000条数据,而狭义水平拆分出了两个物理数据库,那么我们就可以每个数据库查询500条,然后在服务层归并成1000条数据,在服务层排序,这种场景下如果我们的主键设计时候还包含点业务意义,那么这个排序的精确度就会得到很大提升。假如用户对排序不敏感,那就更好做了,分页时候如果每页规定显示10条,我们可以把10条数据平均分配给两个数据库,也就是显示10条A库的数据,再显示5条B库的数据。
看到这里有些细心的朋友可能还会有疑问,那就是居然排序是分页查询的痛点,那么我们可以不用数据库查询,而使用搜索技术啊,NoSql数据库啊,的确这些技术可以更好的解决分页问题,但是关系数据库过渡到搜索引擎和NoSql数据库首先需要我们转化数据,而狭义的水平拆分的数据表本身数据量很大,这个转化过程我们是没法快速完成的,如果我们对延时容忍度那么高,其实我们就没必要去做数据库的狭义水平拆分了。这个问题反过来说明了使用狭义拆分数据表的业务场景,那就是:针对数据量很大的表同时该表的读写的关联性是没法有效拆分的。
最后我要讲的是,如果系统到了狭义水平拆分都没法解决时候,我们就要抛弃传统的关系数据方案了,将该业务全部使用NoSql数据库解决或者像很多大型互联网公司那样,改写开源的mysql数据库。文章写道这里,我还是想说一个观点,如果一个系统有很强烈需求去做狭义的水平拆分,那么这个公司的某个业务那肯定是非常的大了,所以啊,这个方案以公司为单位应该有点小众了。
好了,今天写到这里,祝大家晚安,生活愉快。
本文开篇提个问题给大家,关系数据库的瓶颈有哪些?我想有些朋友看到这个问题肯定会说出自己平时开发中碰到了一个跟数据库有关的什么什么问题,然后如何解决的等等,这样的答案没问题,但是却没有代表性,如果出现了一个新的存储瓶颈问题,你在那个场景的处理经验可以套用在这个新问题上吗?这个真的很难说。
其实不管什么样的问题场景最后解决它都要落实到数据库的话,那么这个问题场景一定是击中了数据库的某个痛点,那么我前面的六篇文章里那些手段到底是在解决数据库的那些痛点,下面我总结下,具体如下:
痛点一:数据库的连接数不够用了。换句话说就是在同一个时间内,要求和数据库建立连接的请求超出了数据库所允许的最大连接数,如果我们对超出的连接数没有进行有效的控制让它们直接落到了数据库上,那么就有可能会让数据库不堪重负,那么我们就得要分散这些连接,或者让请求排队。
痛点二:对于数据库表的操作无非两种一种是写操作,一种是读操作,在现实场景下很难出现读写都成问题的事情,往往是其中一种表的操作出现了瓶颈问题所引起的,由于读和写都是操作同一个介质,这就导致如果我们不对介质进行拆分去单独解决读的问题或者写的问题会让问题变的复杂化,最后很难从根本上解决问题。
痛点三:实时计算和海量数据的矛盾。本系列讲存储瓶颈问题其实有一个范畴的,那就是本系列讲到的手段都是在使用关系数据库来完成实时计算的业务场景,而现实中,数据库里表的数据都会随着时间推移而不断增长,当表的数据超出了一定规模后,受制于计算机硬盘、内存以及CPU本身的能力,我们很难完成对这些数据的实时处理,因此我们就必须要采取新的手段解决这些问题。
我今天之所以总结下这三个痛点,主要是为了告诉大家当我们面对存储瓶颈问题时候,我们要把问题最终落实到这个问题到底是因为触碰到了数据库的那些痛点,这样回过头来再看我前面说到的技术手段,我就会知道该用什么手段来解决问题了。
好了,多余的话就说到这里,下面开始本篇的主要内容了。首先给大伙看一张有趣的漫画,如下图所示:
身为程序员的我看到这个漫画感到很沮丧,因为我们被机器打败了。但是这个漫画同时提醒了做软件的程序员,软件的性能其实和硬件有着不可分割的关系,也许我们碰到的存储问题不一定是由我们的程序产生的,而是因为好的炮弹装进了一个老旧过时的大炮里,最后当然我们会感到炮弹的威力没有达到我们的预期。除此之外了,也有可能我们的程序设计本身没有有效的利用好已有的资源,所以在前文里我提到如果我们知道存储的瓶颈问题将会是网站首先发生问题的地方,那么在数据库建模时候我们要尽量减轻数据库的计算功能,只保留数据库最基本的计算功能,而复杂的计算功能交由数据访问层完成,这其实是为解决瓶颈问题打下了一个良好的基础。最后我想强调一点,作为软件工程师经常会不自觉地忽视硬件对程序性能的影响,因此在设计方案时候考察下硬件和问题场景的关系或许能开拓我们解决问题的思路。
上面的问题按本篇开篇的痛点总结的思路总结下的话,那么就是如下:
痛点四:当数据库所在服务器的硬件有很大提升时候,我们可以优先考虑是否可以通过提升硬件性能的手段来提升数据库的性能。
在本系列的第一篇里,我讲到根据http无状态的特点,我们可以通过剥离web服务器的状态性主要是session的功能,那么当网站负载增大我们可以通过增加web服务器的方式扩容网站的并发能力。其实不管是读写分离方案,垂直拆分方案还是水平拆分方案细细体会下,它们也跟水平扩展web服务的方式有类似之处,这个类似之处也就是通过增加新的服务来扩展整个存储的性能,那么新的问题来了,前面的三种解决存储瓶颈的方案也能做到像web服务那样的水平扩展吗?换句话说,当方案执行一段时间后,又出现了瓶颈问题,我们可以通过增加服务器就能解决新的问题吗?
要回答清楚这个问题,我们首先要详细分析下web服务的水平扩展原理,web服务的水平扩展是基于http协议的无状态,http的无状态是指不同的http请求之间不存在任何关联关系,因此如果后台有多个web服务处理http请求,每个web服务器都部署相同的web服务,那么不管那个web服务处理http请求,结果都是等价的。这个原理如果平移到数据库,那么就是每个数据库操作落到任意一台数据库服务器都是等价的,那么这个等价就要求每个不同的物理数据库都得存储相同的数据,这么一来就没法解决读写失衡,解决海量数据的问题了,当然这样做看起来似乎可以解决连接数的问题,但是面对写操作就麻烦了,因为写数据时候我们必须保证两个数据库的数据同步问题,这就把问题变复杂了,所以web服务的水平扩展是不适用于数据库的。这也变相说明,分库分表的数据库本身就拥有很强的状态性。
不过web服务的水平扩展还代表一个思想,那就是当业务操作超出了单机服务器的处理能力,那么我们可以通过增加服务器的方式水平拓展整个web服务器的处理能力,这个思想放到数据库而言,肯定是适用的。那么我们就可以定义下数据库的水平扩展,具体如下:
数据库的水平扩展是指通过增加服务器的方式提升整个存储层的性能。
数据库的读写分离方案,垂直拆分方案还有水平拆分方案其实都是以表为单位进行的,假如我们把数据库的表作为一个操作原子,读写分离方案和垂直拆分方案都没有打破表的原子性,并且都是以表为着力点进行,因此如果我们增加服务器来扩容这些方案的性能,肯定会触碰表原子性的红线,那么这个方案也就演变成了水平拆分方案了,由此我们可以得出一个结论:
数据库的水平扩展基本都是基于水平拆分进行的,也就是说数据库的水平扩展是在数据库水平拆分后再进行一次水平拆分,水平扩展的次数也就代表的水平拆分迭代的次数。因此要谈好数据库的水平扩展问题,我们首先要更加细致的分析下水平拆分的方案,当然这里所说的水平拆分方案指的是狭义的水平拆分。
数据库的水平扩展其实就是让被水平拆分的表的数据跟进一步的分散,而数据的离散规则是由水平拆分的主键设计方案所决定的,在前文里我推崇了一个使用sequence及自增列的方案,当时我给出了两种实现手段,一种是通过设置不同的起始数和相同的步长,这样来拆分数据的分布,另一种是通过估算每台服务器的存储承载能力,通过设定自增的起始值和最大值来拆分数据,我当时说到方案一我们可以通过设置不同步长的间隔,这样我们为我们之后的水平扩展带来便利,方案二起始也可以设定新的起始值也来完成水平扩展,但是不管哪个方案进行水平扩展后,有个新问题我们不得不去面对,那就是数据分配的不均衡,因为原有的服务器会有历史数据的负担问题。而在我谈到狭义水平拆分时候,数据分配的均匀问题曾被我作为水平技术拆分的优点,但是到了扩展就出现了数据分配的不均衡了,数据的不均衡会造成系统计算资源利用率混乱,更要命的是它还会影响到上层的计算操作,例如海量数据的排序查询,因为数据分配不均衡,那么局部排序的偏差会变得更大。解决这个问题的手段只有一个,那就是对数据根据平均原则重新分布,这就得进行大规模的数据迁移了,由此可见,除非我们觉得数据是否分布均匀对业务影响不大,不需要调整数据分布,那么这个水平扩展还是很有效果,但是如果业务系统不能容忍数据分布的不均衡,那么我们的水平扩展就相当于重新做了一遍水平拆分,那是相当的麻烦。其实这些还不是最要命的,如果一个系统后台数据库要做水平扩展,水平扩展后又要做数据迁移,这个扩展的表还是一个核心业务表,那么方案上线时候必然导致数据库停止服务一段时间。
数据库的水平扩展本质上就是水平拆分的迭代操作,换句话说水平扩展就是在已经进行了水平拆分后再拆分一次,扩展的主要问题就是新的水平拆分是否能继承前一次的水平拆分,从而实现只做少量的修改就能达到我们的业务需求,那么我们如果想解决这个问题就得回到问题的源头,我们的前一次水平拆分是否能良好的支持后续的水平拆分,那么为了做到这点我们到底要注意哪些问题呢?我个人认为应该主要注意两个问题,它们分别是:水平扩展和数据迁移的关系问题以及排序的问题。
问题一:水平扩展和数据迁移的关系问题。在我上边的例子里,我们所做的水平拆分的主键设计方案都是基于一个平均的原则进行的,如果新的服务器加入后就会破坏数据平均分配的原则,为了保证数据分布的均匀我们就不能不将数据做相应的迁移。这个问题推而广之,就算我们水平拆分没有过分强调平均原则,或者使用其他维度来分割数据,如果这个维度在水平扩展时候和原库原表有关联关系,那么结果都有可能导致数据的迁移问题,因为水平扩展是很容易产生数据迁移问题。
对于一个实时系统而言,核心的业务表发生数据迁移是一件风险很大成本很高的事情,抛开迁移的操作危险,数据迁移会导致系统停机,这点是所有系统相关方很难接受的。那么如何解决水平扩展的数据迁移问题了,那么这个时候一致性哈希就派上用场了,一致性哈希是固定哈希算法的衍生,下面我们就来简单介绍下一致性哈希的原理,首先我看看下面这张图:
一致性哈希使用时候首先要计算出用来做水平拆分服务器的数字哈希值,并将这些哈希值配置到0~232的圆上,接着计算出被存储数据主键的数字哈希值,并把它们映射到这个圆上,然后从数据映射到的位置开始顺时针查找,并将数据保存在找到的第一个服务器上,如果主键的哈希值超过了232,那么该记录就会保存在第一台服务器上。这些如上图的第一张图。
那么有一天我们要添加新的服务器了,也就是要做水平扩展了,如上图的第二张图,新节点(图上node5)只会影响到的原节点node4,即顺时针方向的第一个节点,因此一致性哈希能最大限度的抑制数据的重新分布。
上面的例图里我们只使用了4个节点,添加一个新节点影响到了25%左右的数据,这个影响度还是有点大,那有没有办法还能降低点影响了,那么我们可以在一致性哈希算法的基础上进行改进,一致性哈希上的分布节点越多,那么添加和删除一个节点对于总体影响最小,但是现实里我们不一定真的是用那么多节点,那么我们可以增加大量的虚拟节点来进一步抑制数据分布不均衡。
前文里我将水平拆分的主键设计方案类比分布式缓存技术memcached,其实水平拆分在数据库技术里也有一个专属的概念代表他,那就是数据的分区,只不过水平拆分的这个分区粒度更大,操作的动静也更大,笔者这里之所以提这个主要是因为写存储瓶颈一定会受到我自己经验和知识的限制,如果有朋友因为看了本文而对存储问题发生了兴趣,那么我这里也可以指明一个学习的方向,这样就能避免一些价值不高的探索过程,让学习的效率会更高点。
问题二:水平扩展的排序问题。当我们要做水平扩展时候肯定有个这样的因素在作怪:数据量太大了。前文里我说道过海量数据会对读操作带来严重挑战,对于实时系统而言,要对海量数据做实时查询几乎是件无法完成的工作,但是现实中我们还是需要这样的操作,可是当碰到如此操作我们一般采取抽取部分结果数据的方式来满足查询的实时性,要想让这些少量的数据能让用户满意,而不会产生太大的业务偏差,那么排序就变变得十分重要了。
不过这里的排序一定要加上一个范畴,首先我们要明确一点啊,对海量数据进行全排序,而这个全排序还要以实时的要求进行,这个是根本无法完成的,为什么说无法完成,因为这些都是在挑战硬盘读写速度,内存读写速度以及CPU的运算能力,假如1Tb的数据上面这三个要素不包括排序操作,读取操作能在10毫秒内完成,也许海量数据的实时排序才有可能,但是目前计算机是绝对没有这个能力的。
那么现实场景下我们是如何解决海量数据的实时排序问题的呢?为了解决这个问题我们就必须有点逆向思维的意识了,另辟蹊径的处理排序难题。第一种方式就是缩小需要排序的数据大小,那么数据库的分区技术是一个很好的手段,除了分区手段外,其实还有一个手段,前面我讲到使用搜索技术可以解决数据库读慢的难题,搜索库本身可以当做一个读库,那么搜索技术是怎么来解决快速读取海量数据的难题了,它的手段是使用索引,索引好比一本书的目录,我们想从书里检索我们想要的信息,我们最有效率的方式就是先查询目录,找到自己想要看的标题,然后对应页码,把书直接翻到那一页,存储系统索引的本质和书的目录一样,只不过计算机领域的索引技术更加的复杂。其实为数据建立索引,本身就是一个缩小数据范围和大小的一种手段,这点它和分区是类似的。我们其实可以把索引当做一张数据库的映射表,一般存储系统为了让索引高效以及为了扩展索引查找数据的精确度,存储系统在建立索引的时候还会跟索引建立好排序,那么当用户做实时查询时候,他根据索引字段查找数据,因为索引本身就有良好的排序,那么在查询的过程里就可以免去排序的操作,最终我们就可以高效的获取一个已经排好序的结果集。
现在我们回到水平拆分海量数据排序的场景,前文里我提到了海量数据做分页实时查询可以采用一种抽样的方式进行,虽然用户的意图是想进行海量数据查询,但是人不可能一下子消化掉全部海量数据的特点,因此我们可以只对海量数据的部分进行操作,可是由于用户的本意是全量数据,我们给出的抽样数据如何能更加精确点,那么就和我们在分布数据时候分布原则有关系,具体落实的就是主键设计方案了,碰到这样的场景就得要求我们的主键具有排序的特点,那么我们就不得不探讨下水平拆分里主键的排序问题了。
在前文里我提到一种使用固定哈希算法来设计主键的方案,当时提到的限制条件就是主键本身没有排序特性,只有唯一性,因此哈希出来的值是唯一的,这种哈希方式其实不能保证数据分布时候每台服务器上落地数据有一个先后的时间顺序,它只能保证在海量数据存储分布式时候各个服务器近似均匀,因此这样的主键设计方案碰到分页查询有排序要求时候其实是起不到任何作用的,因此如果我们想让主键有个先后顺序最好使用递增的数字来表示,但是递增数字的设计方案如果按照我前面的起始数,步长方式就会有一个问题,那就是单库单表的顺序性可以保障,跨库跨表之间的顺序是很难保证的,这也说明我们对于水平拆分的主键字段对于逻辑表进行全排序也是一件无法完成的任务。
那么我们到底该如何解决这个问题了,那么我们只得使用单独的主键生成服务器了,前文里我曾经批评了主键生成服务器方案,文章发表后有个朋友找到我谈论了下这个问题,他说出了他们计划的一个做法,他们自己研发了一个主键生成服务器,因为害怕这个服务器单点故障,他们把它做成了分布式,他们自己设计了一套简单的UUID算法,使得这个算法适合集群的特点,他们打算用zookeeper保证这个集群的可靠性,好了,他们做法里最关键的一点来了,如何保证主键获取的高效性,他说他们没有让每次生成主键的操作都是直接访问集群,而是在集群和主键使用者之间做了个代理层,集群也不是频繁生成主键的,而是每次生成一大批主键,这一大批主键值按队列的方式缓存在代理层了,每次主键使用者获取主键时候,队列就消耗一个主键,当然他们的系统还会检查主键使用的比率,当比率到达阀值时候集群就会收到通知,马上开始生成新的一批主键值,然后将这些值追加到代理层队列里,为了保证主键生成的可靠性以及主键生成的连续性,这个主键队列只要收到一次主键请求操作就消费掉这个主键,也不关心这个主键到底是否真的被正常使用过,当时我还提出了一个自己的疑问,要是代理挂掉了呢?那么集群该如何再生成主键值了,他说他们的系统没有单点系统,就算是代理层也是分布式的,所以非常可靠,就算全部服务器全挂了,那么这个时候主键生成服务器集群也不会再重复生成已经生成过的主键值,当然每次生成完主键值后,为了安全起见,主键生成服务会把生成的最大主键值持久化保存。
其实这位朋友的主键设计方案其实核心设计起点就是为了解决主键的排序问题,这也为实际使用单独主键设计方案找到了一个很现实的场景。如果能做到保证主键的顺序性,同时数据落地时候根据这个顺序依次进行的,那么在单库做排序查询的精确度就会很高,查询时候我们把查询的条数均匀分布到各个服务器的表上,最后汇总的排序结果也是近似精确的。
自从和这位朋友聊到了主键生成服务的设计问题后以及我今天讲到的一致性哈希的问题,我现在有点摒弃前文里说到的固定哈希算法的主键设计方案了,这个摒弃也是有条件限制的,主键生成服务的方案其实是让固定哈希方案更加完善,但是如果主键本身没有排序性,只有唯一性,那么这个做法对于排序查询起不到什么作用,到了水平扩展,固定哈希排序的扩展会导致大量数据迁移,风险和成本太高,而一致性哈希是固定哈希的进化版,因此当我们想使用哈希来分布数据时候,还不如一开始就使用一致性哈希,这样就为后续的系统升级和维护带来很大的便利。
有网友在留言里还提到了哈希算法分布数据的一个问题,那就是硬件的性能对数据平均分配的影响,如果水平拆分所使用的服务器性能存在差异,那么平均分配是会造成热点问题的出现,如果我们不去改变硬件的差异性,那么就不得不在分配原则上加入权重的算法来动态调整数据的分布,这样就制造了人为的数据分布不均衡,那么到了上层的计算操作时候某些场景我们也会不自觉的加入权重的维度。但是作为笔者的我对这个做法是有异议的,这些异议具体如下:
异议一:我个人认为不管什么系统引入权重都是把问题复杂化的操作,权重往往都是权益之计,如果随着时间推移还要进一步扩展权重算法,那么问题就变得越加复杂了,而且我个人认为权重是很难进行合理处理的,权重如果还要演进会变得异常复杂,这个复杂度可能会远远超出分布式系统,数据拆分本身的难度,因此除非迫不得已我们还是尽量不去使用什么权重,就算有权重也不要轻易使用,看有没有方式可以消除权重的根本问题。
异议二:如果我们的系统后台数据库都是使用独立服务器,那么一般都会让最好的服务器服务于数据库,这个做法本身就说明了数据库的重要性,而且我们对数据库的任何分库分表的解决方案都会很麻烦,很繁琐甚至很危险,因此本篇开始提出了如果我们解决瓶颈问题前先考虑下硬件的问题,如果硬件可以解决掉问题,优先采取硬件方案,这就说明我们合理对待存储问题的前提就是让数据库的硬件跟上时代的要求,那么如果有些硬件出现了性能瓶颈,是不是我们忽视了硬件的重要性了?
异议三:均匀分布数据不仅仅可以合理利用计算资源,它还会给业务操作带来好处,那么我们扩展数据库时候就让各个服务器本身能力均衡,这个其实不难的,如果老的服务器实在太老了,用新服务器替换掉,虽然会有全库迁移的问题,但是这么粗粒度的数据平移,那可是比任何拆分方案的数据迁移难度低的多的。
好了,本篇就写到这里,祝大家工作生活愉快!
在开始本篇主要内容前,我们一起看看下面的几张截图,首先是第一张图,如下图所示:
这是一家电商网站的首页,当我们第一次打开这个首页,网站会弹出一个强制性的对话框,让用户选择货物配送的地址,如果是淘宝和京东的话,那么这个选择配货地址的选项是在商品里,如下图是淘宝的选择配送地点:
下图是京东选择配货地点:
那么图一跟京东和淘宝有什么区别呢?图一的电商强制用户选择地区后,那么我们在查询这个商品时候会因为地区不同,显示的查询结果会不一样,这个就和网站做国际化有点像,不过网站国际化是切语言和语言相关的静态资源,但是电商这个地域的选择是和业务相关的,不同的地域查询结果是不相同的,这个选择地域的弹出框很像一个路由器。相比之下,淘宝和京东把商品的配送和商品相关,那么我们在这些网站里查询商品时候,其实是按照全国查询的,全国不同的地方查询同一个条件所获得到的结果是一致的。从业务角度而言,这说明第一个电商的业务没有全国铺开,就算是铺开了,地域的差异也影响到物流的问题,而淘宝和京东则正是一个全国意义的大型电商网站了。
回到技术的角度,这两种不同的做法有没有可能还和技术问题有关了?今天我就来探讨下这个问题。
不管网站大与小,一个网站肯定可以分为客户端、服务端和存储端,勾连这不同的组成部分是网络,网络是一种通讯设施,距离的远近会直接影响到网络传输的效率问题,如是乎就出现了像CDN这样的技术,很多大型互联网公司还会在不同的城市建立机房,这些手段的目的就是在解决距离对网络传输效率的影响,但是当这种就近解决问题的方案落到存储层的时候,问题就来了。上篇里我说道web服务的水平扩展问题,这种水平扩展是基于一种无状态性的原理设计的,但是到了存储层我们不管怎么拆分它,它都很难消除状态的问题,也就是存储层有状态性是它的天然属性。特别是碰到一个竞争性的存储资源时候,这种状态性会变得非常顽固,例如商品的库存问题,如果我们把库存数据对等的平移到不同地域的数据中心,那么如何保证不同地方的库存信息总是准确的,这就成为了难题。这种问题放在一个小国家不是什么问题,但是放到地大物博的中国那就很成问题了。所以存储是这种就近方案的短板了。
我曾了解到中国一家大型信息企业在设计它们第一代系统时候,就考虑到了这种地域性差异对系统设计的影响,它们的第一代系统在存储层这块就设计成了一个双核系统,什么叫做存储层的双核系统了?它们的做法是在北京和上海分别建立两个数据中心,系统的存储层分别部署在北京的数据中心和上海的数据中心,两个数据中心是等价的,那么中国北部的交易就走北京数据中心,中国南部的交易就走上海的数据中心。但是系统上线后,发现这种双核设计方案成为了整个系统的梦魇了,这个梦魇的最核心的问题就是数据的同步问题,因为该企业是一个全国性业务的企业,因此有大量交易需要南北数据中心同步完数据后才能正常完成,但是想从北京和上海同步数据的效率是异常的低效,我曾经看过一份资料,里面说有机构做了一个测试,当两个数据中心的距离超过了80公里,那么网络的延迟性基本是无法忍受的,当然不差钱的企业可以专门铺设专线来连接两个数据中心,这种专线的成本高的吓人,我曾听人说就在上海,如果铺专线从浦东到浦西,那么这条专线基本是用人民币铺就的,更何况是从北京到上海铺专线,就算企业不差这些钱延迟性也严重影响了企业业务的发展。除了延迟性外,通过网络大规模传输数据,数据的可靠性是很难保证的,也就是网络传输时候经常没有道理的丢包,这就造成了很多重复性传输,使得同步数据的效率更加的低效。
因为存储层这种双核设计缺陷,该企业马上从事了二代系统的设计和开发,而这个二代系统核心业务就是解决这个存储层的双核问题。那到底该怎么解决了?把双核变成单核,既然两个数据中心这么麻烦,那我们就搞一个数据中心算了,既省钱有没那么多麻烦事情,这个肯定不是解决问题的正确思路了,双核设计的出发点是非常有现实意义和价值的,最后该公司使用了一个新的方案替代双核,这个方案称之为主备方案,存储层任然部署到两个数据中心,到了业务运行阶段,一个数据中心为主,一个数据中心为辅,不过这个主备方案绝不是通常意义的数据备份方案,他其实是吸收了单核和双核方案的优点,同时尽量避免单核和双核的缺点,那么这点上这个主备方案是如何做到的呢?
首先我们还是要把系统业务交易分下类,系统有些交易对于实时性啊,数据的正确性啊要求非常高,那么这样的业务场景使用单核存储系统比较合适,一个业务系统不可能全是这样的实时性交易,也有一些交易对实时性要求比较差,当然我们还是得要考察下这种交易对于延时容忍度,具体就是一般延时多久用户是可以接受的,这点非常重要,因为就算是主备方案,那么数据还是会有同步的操作,只不过这个同步的时间粒度上会更粗些,我们可以以系统和业务角度合理设置一个同步时间间隔,如果延时性交易的延时时间超过了这个间隔时间的话,那么这样的业务场景其实是可以就近处理的,没有必要将这些请求都发送到主数据中心,这样可以减轻主数据中心的运行压力。该企业的二代信息系统还有个要求就是过了每天的零点,前一天的数据必须在两个数据中心完成同步,换句话说,两个数据中心数据的差异性最大容忍度是天,为什么要这样做了?有的朋友看到了一定认为这是为了备份数据,的确这是目的之一,但这个做法还有更大的深意,双核设计除了解决距离对网络效率的影响外,还有个重要的目的就是容灾,我记得几年前,有个朋友告诉我他们公司网站挂了6个小时,我当时很奇怪,我就问你们系统难道不是分布式吗?他说他们线上系统没有单点,那为什么网站还会整个挂掉了?答案真的让人不敢相信,因为他们的机房漏雨了,机房的线路短路了,那个朋友告诉我这件事情以后,他们公司又在附近租了个新机房做容灾,防止此类事情再发生了。这种情况真的可以称之为天灾了,不过这样的事情概率很低,可是一旦发生就会非常致命,记得日本爆发九级大地震的时候,我看到一个新网报道,报道里面有好多大型计算机倒掉了,而这个机房的机器的作用几乎关系到亚洲互联网系统的命脉,大家都知道每个网站都有自己的域名,域名是一个网站的入口,而日本那个机房放置的服务器就是全球赫赫有名的13台服务器之一,专门用来解析域名的DNS服务器,如果这些机器挂掉了,可能发生一整个国家都不能正常使用互联网。但是天灾毕竟是局部的,因此全国甚至全球设立不同的数据中心用来容灾是很多大型互联网公司必须走的道路,回到本文的主备方案,为了保证数据中心的容灾性,那么我们再设计主备方案同时还要保证主备数据中心可以迅速切换,当一个数据中心出现问题时候可以马上把辅助的数据中心转化为主数据中心。为了保证这种切换的可靠性,该企业经常在晚上交易量小的时候,把主备来回切换跑跑。
回到开篇提到的那三张截图,那个一开始弹出地域选择框的电商网站,当我们选择不同的地域时候,查询同样的商品最后显示的商品列表是不同,而京东虽然也有地域选择,但是我们切换地域后查询商品后结果基本没有变化,至于淘宝和天猫压根就没有让我们选择地域的选项,配送都是在商品这边进行选择的。可能淘宝和天猫没有自营业务,因此天猫很难控制里面商家的地域区别,京东和前面哪家电商网站因为大部分是直营业务,因此配送地址和他们仓储所在地是有关系的,其实这个做法衍生下的话,地域其实还可以做到数据中心的划分,例如江沪浙用一个数据中心,中部地区用一个数据中心,那么这种方式就可以帮助我们解决存储层的就近问题,从这里我们似乎也可以看出B2C和C2C的业务场景的一些区别。
由此我可以做一个总结,首先存储层做到对等多核的体系基本是不可能的,主备的方案可以解决单核和多核的缺点,同时可以发扬单核和多核的优点,距离的远近也能产生业务的差异性,我们可以通过这种差异性把数据中心变成分散式,这样还可以解决数据访问的就近原则。
美国的互联网公司规模很大,他们从一开始就是全球化的,那么对于美国的大型互联网公司将数据中心分散化和本地化就变的非常重要,所以好的存储层的分布设计方案是完成网站全球布局任务的基础。但是对于很多中小企业,或者是刚刚创业的公司能在不同地域建立数据中心,或者不差钱但是能快速的建立不同地域的数据中心其实是非常难的事情,那么这个时候我们找一家全球性的云平台例如亚马逊的云平台,或者我们的业务就局限在中国,使用个本土优秀的云平台也是一种不错的选择,云计算的推广使得创业者的成本越来越低了。
好了,本系列的文章到此为止,本系列都是在讲数据库的问题,我曾经说过任何程序或软件都是计算和存储的结合体,本系列着重讲到的是存储,时下很多大型互联网公司在存储这块已经发生了很大的变化,在关系数据库这块都已经做到了去商业关系数据库,而使用开源的关系数据库,并将这些开源的关系数据进行了大规模的改造,这个做法应该算是互联网领域关系数据库发展的前沿了,同时将关系数据库很难做到的事情用Nosql数据库来替代也是一种大趋势。
本系列讲述时候设置了一个很大的前提,那就是尽量保持关系数据库存储的本性,因此我将很多计算建议迁移到应用层,这个观点我有很多理由说明它的好处,但是现实中是否是最好的方法,这个就要具体看了,因此我不想去苛求这么做的合理性,但是逻辑上合理的方案总是会有很多借鉴意义的,这就是我想表达的,至于关于存储层的计算我倾向于在数据访问层里做,因此按照我的思路,最终这个关系数据库存储层就会变成一个分布式数据库,数据访问层当然也是使用分布式系统原理来做,讲解分布式系统也是本文章后续想讨论,如果我有时间接着写这个大系列博客我会在分布式系统这块继续讲解数据访问层的设计问题。
好了,文章写完了,祝大家生活愉快。
在存储瓶颈的开篇我提到像hao123这样的导航网站只要它部署的web服务器数量足够,它可以承载超大规模的并发访问量,如果是一个动态的网站,特别是使用到了数据库的网站是很难做到通过增加web服务器数量的方式来有效的增加网站并发访问能力的。但是现实情况是像淘宝、京东这样的大型动态网站在承担高并发的情况下任然能保证快速的响应,这其中有什么样的技术手段可以达到动态网站支撑高并发的场景了,这也许是每个做web开发的朋友都很感兴趣的问题,今天我将写一个新的系列来探讨下这个问题,希望我的经验和研究能给大多数人以启迪。这里要说明下,本系列的写法和存储的瓶颈的写法有所不同,本系列开始部分主要是讲解原理,后面部分会针对原理讲解具体的实现手段,如果有朋友感觉这种写法不适应,还请谅解。
我个人总结下来,这些大型动态网站之所以可以做到能快速响应高并发,它们都是尽量让自己的网站静态化,当然这种静态化绝不是把网站就做成静态网站,而是在充分理解了静态网站在提升网站响应速度的基础上对动态网站进行改良,所以我这里首先要讨论下静态网站那些特点可以用于我们提升网站的响应速度。
静态网站非常简单,它就是通过一个url访问web服务器上的一个网页,web服务器接收到请求后在网络上使用http协议将网页返回给浏览器,浏览器通过解析http协议最终将页面展示在浏览器里,有时这个网页会比较复杂点,里面包含了一些额外的资源例如:图片、外部的css文件、外部的js文件以及一些flash之类的多媒体资源,这些资源会单独使用http协议把信息返回给浏览器,浏览器从页面里的src,href、Object这样的标签将这些资源和页面组合在一起,最终在浏览器里展示页面。但是不管什么类型的资源,这些资源如果我们不是手动的改变它们,那么我们每次请求获得结果都是一样的。这就说明静态网页的一个特点:静态网页的资源基本是不会发生变化的。因此我们第一次访问一个静态网页和我们以后访问这个静态网页都是一个重复的请求,这种网站加载的速度基本都是由网络传输的速度,以及每个资源请求的大小所决定,既然访问的资源基本不会发生变化,那么我们重复请求这些资源,自己在那里空等不是很浪费时间吗?如是乎,浏览器出现了缓存技术,我们开发时候可以对那些不变的资源在http协议上编写相应指令,这些指令会让浏览器第一次访问到静态资源后缓存起这些静态资源,用户第二次访问这个网页时候就不再需要重复请求了,因为请求资源本地缓存,那么获取它的效率就变得异常高效。
由于静态网站的请求资源是不会经常发生变化的,那么这种资源其实很容易被迁移,我们都知道网络传输的效率是和距离长短有关系的,既然静态资源很容易被迁移那么我们就可以把静态资源服务器按地域分布在多个服务节点上,当用户请求网站时候根据一个路由算法将请求落地在离用户最近的节点上,这样就可以减少网络传输的距离从而提升访问的效率,这就是我们长提的大名鼎鼎的CDN技术,内容分发网络技术。
网络传输效率还和我们传输资源的大小有关,因此我们在资源传输前将其压缩,减小资源的大小从而达到提升传输效率的目的;另外,每个http请求其实都是一个tcp的请求,这些请求在建立连接和释放连接都会消耗很多系统资源,这些性能的消耗时常会比传输内容本身还要大,因此我们会尽力减少http请求的个数来达到提升传输效率的目的或者使用http长连接来消除建立连接和释放连接的开销(长连接的使用要看具体场景,这个我会在后面文章讲到)。
其实雅虎提出的网站优化的14条建议大部分都是基于以上原理得出的,关于雅虎的14条件建议,本系列后面内容将做详细的讨论,这里就不展开了。
我常常认为最佳的性能优化手段就是使用缓存了,但是缓存的数据一般都是那些不会经常变化的数据,上文里说到的浏览器缓存,CDN其实都是可以当做缓存手段来理解,它们也是提升网站性能最为有效的方式之一,但是这些缓存技术到了动态网站却变得异常不好实施,这到底是怎么回事了?
首先动态网站和静态网站有何不同呢?我觉得动态网站和静态网站的区别就是动态网站网页虽然也有一个url,但是我们如果传输参数不同那么这个url请求的页面并不是完全一样,也就是说动态网站网页的内容根据条件不同是会发生改变的,但是这些变化的内容却是同一个url,url在静态网站里就是一个资源的地址,那么在动态网站里一个地址指向的资源其实是不同的。因为这种不同所以我们没法把动态的网页进行有效的缓存,而且不恰当的使用缓存还会引发错误,所以在动态网页里我们会在meta设定页面不会被浏览器缓存。
如果每次访问动态的网页该网页的内容都是完全不同的,也许我们就没有必要写网站静态化的主题了,现实中的动态网页往往只是其中一部分会发生变化,例如电商网站的菜单、页面头部、页面尾部这些其实都不会经常发生变化,如果我们只是因为网页一小部分经常变化让用户每次请求都要重复访问这些重复的资源,这其实是非常消耗计算资源了,我们来做个计算吧,假如一个动态页面这些不变的内容有10k,该网页一天有1000万次的访问量,那么每天将消耗掉1亿kb的网络资源,这个其实很不划算的,而且这些重复消耗的宽带资源并没有为网站的用户体验带来好处,相反还拖慢了网页加载的效率。那么我们就得考虑拆分网页了,把网页做一个动静分离,让静态的部分当做不变的静态资源进行处理,动态的内容还是动态处理,然后在合适的地方将动静内容合并在一起。
这里有个关键点就是动静合并的位置,这个位置的选择会直接导致我们整个web前端的架构设计。我们这里以java的web开发为例,来谈谈这个问题。
java的web开发里我们一般使用jsp来编写页面,当然也可以使用先进点的模板引擎开发页面例如velocity,freemark等,不管我们页面使用的是jsp还是模板引擎,这些类似html的文件其实并不是真正的html,例如jsp本质其实是个servlet也就是一个java程序,所以它们的本质是服务端语言和html的一个整合技术,在实际运行中web容器会根据服务端的返回数据将jsp或模板引擎解析成浏览器能解析的html,然后传输这个html到浏览器进行解析。由此可见服务端语言提供的开发页面的技术其实是动静无法分离的源头,但是这些技术可以很好的完成动静资源中的动的内容,因此我们想做动静分离那么首先就要把静的资源从jsp或者模板语言里抽取出来,抽取出来的静态资源当然就要交给静态的web服务器来处理,我们常用的静态资源服务器一般是apache或ngnix,所以这些静态资源应该放置在这样的服务器上,那么我们是否可以在这些静态web服务器上做动静结合呢?答案是还真行,例如apache服务器有个模块就可以将它自身存储的静态资源和服务端传输的资源整合在一起,这种技术叫做ESI,这个时候我们可以把不变的静态内容制作成模板放置在静态服务器上,动态内容达到静态资源服务器时候,使用ESI或者CSI的标签,把动静内容结合在一起,这就完成了一个动静结合操作。这里就有一个问题了,我前面提到过CDN,CDN其实也是一组静态的web服务器,那么我们是否可以把这些事情放到CDN做了?理论上是可以做到,但是现实却是不太好做,因为除了一些超有钱的互联网公司,大部分公司使用的CDN都是第三方提供的,第三方的CDN往往是一个通用方案,再加上人家毕竟不是自己人,而且CDN的主要目的也不是为了做动静分离,因此大部分情况下在CDN上完成这类操作并不是那么顺利,因此我们常常会在服务端的web容器前加上一个静态web服务器,这个静态服务器起到一个反向代理的作用,它可以做很多事情,其中一件事情就是可以完成这个动静结合的问题。
那么我们把这个动静结合点再往前推,推到浏览器,浏览器能做到这件事情吗?如果浏览器可以,那么静态资源也就可以缓存在客户端了,这比缓存在CDN效率还要高,其实浏览器还真的可以做到这点,特别是ajax技术出现后,浏览器来整合这个动静资源也就变得更加容易了。不过一般而言,我们使用ajax做动静分离都是都是从服务端请求一个html片段,到了浏览器后,使用dom技术将这个片段整合到页面里,虽然这个已经比全页面返回高效很多,但是他还是有问题的,服务端处理完请求最终返回结果其实都是很纯粹的数据,可是这些数据我们不得不转化为页面片段返回给浏览器,这本质是为纯粹的数据上加入了很多与服务端无用的结构,之所以说无用是因为浏览器自身也可以完成这些结构,为什么我们一定要让服务端做这个事情了?如是乎javascript的模板技术出现了,这些模板技术和jsp,velocity类似,只不过它们是通过javascript设计的模板语言,有了javascript模板语言,服务端可以完全不用考虑对页面的处理,它只需要将有效的数据返回到页面就行了,使用了javascript模板技术,可以让我们动静资源分离做的更加彻底,基本上所有的浏览器相关的东西都被静态化了,服务端只需要把最原始的数据传输到浏览器即可。讲到这里我们就说到了web前端最前沿的技术了:javascriptMVC架构了。
好了今天就写到这里,本篇文章是网站静态化处理理论的总述,后面的文章我将会一点一滴的讲述实现网站静态化的各种技术实现细节。
上篇文章我简要的介绍了下网站静态化的演进过程,有朋友可能认为这些知识有点过于稀松平常了,而且网站静态化的技术基点也不是那么高深和难以理解,因此它和时下日新月异的web前端技术相比,就显得不伦不类了。其实当我打算写本系列的之前我个人觉得web前端有一个点是很多人都知道重要,但是有常常低估它作用的,那就是web前端和web服务端如何融合的这个点上,这个点再加上我们要做出一个规模庞大,高并发,快速响应的网站时候它对于web前端的架构技术的演进起到了一个不可忽视的作用。
网站的web前端要实现高效,第一个要解决的短板就是网络的延迟性对网站的加载效率的影响,当然很多人会说网速快不快这是网络运营商的问题,不是网站的问题,但是大家肯定也见过就算我们用上了千兆宽带也会有些网站加载速度慢的让人无法忍受,网站本身的确是没法控制网络速度的能力,但是如果我们不降低网络对页面加载效率的影响,其他任何优化网站的手段也就无从谈起,原因就是网络效率对于网页加载效率的影响是起到大头作用的,只有这个大头被解决了,那么解决其他的小头才能发挥作用。
回到上文讲到的网站静态化的关键点动静分离,解决这个关键点的本质就是为了降低网速对网站加载效率的影响,但是我们在处理动静分离问题时候采取的策略不同会对我们整个网站架构产生重大影响,特别是将网页做好动静拆分后,静态的资源尽力向浏览器端推移,这就导致了前端架构出现了以前服务端才有的MVC模式,这就导致web前端架构产生了质的变化,如是一些原来适用于flash这样的重客户端的技术也被传统的web前端所采用,MVC模式在web前端进一步演进由此而出现了MVP(Model-View-Presenter)模式,MVVM(Model-View-ViewModel)模式。也许上篇文章里有人对讲述动静分离的原理有点异议,但是当今日新月异的web前端技术就是这些常见技术不断演化而来,这就是我上篇想表达的内容,我觉得这个系列的特点应该是细节,这是和上个系列存储的瓶颈注重思想是有所不同的。
动态网站最难以动静分离的就是页面了,其他的静态资源例如:图片、外部脚本文件等等这些和静态网站的手法基本一致,其实业界很早就关注了动态网站的动静分离问题,并且为不同的动静分离方案都进行了总结,今天我就介绍下这些技术。本人web服务端的工作语言是java,因此下面服务端的例子是使用java的web技术阐述的,其他语言例如php都有与之对应的技术,所以请那些不是使用java作为服务端工作语言的朋友可以类比学习。
在java的web开发里,页面技术jsp本身就包含了将页面动静分离的手段,例如下面的代码:
- <%@ include file=”header.jsp” %>
-
- <body>
-
- ……….
-
- <%@ include file=”footer.jsp” %>
但是这个做法有一个问题,问题就是这种动静分离其实都是作用于单个页面的,也就是说每个页面都要手动的重复这个动静分离的操作,大多数情况这种做法都不会有什么问题,但是对于一个大型网站而言这种做法就有可能会制造不必要的麻烦,这里我截取了一张京东的首页,如下图所示:
讲述前我要事先声明下,京东网站可能不存在我要讲述的问题,我这里只是使用京东网站的首页做例子来说明,看图里的首页和食品两个条目,有些公司做这样的网站时候这些导航进入的页面会是一个独立的工程,每个工程都是由独立的项目组开发维护的,虽然项目组不同但是他们页面的整体结构会是一致的,如果按照上面的动静分离手段,那么每个项目组都要独立维护一份相同的头部尾部资源,这个时候麻烦来了,如果该公司要新增个新的条目,那么每个项目组都要更新自己不变的资源,如果该企业一共分了5个项目组,现在又做了一个新的条目,那么其他与之无关的项目组都得折腾一次更改统一引用文件的工作,要是做的不仔细就有可能出现页面展示不一致的问题,为了解决这个问题,java的web开发里就会考虑使用模板语言替代jsp页面技术,例如模板语言velocity,这些模板语言都包含一个布局的功能,例如velocity就有这样的功能,我们看看velocity的布局模板实例,如下所示:
- <html>
-
- <head>
-
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-
- <title>#springMessage("page_upop_title")</title>
-
- <meta http-equiv="X-UA-Compatible" content="requiresActiveX=true"/>
-
- <meta name="keywords" content='#springMessage("page_upop_keywords")'/>
-
- <meta content='#springMessage("page_upop_description")' name="description"/>
-
- </head>
-
- <body οncοntextmenu="return false" onselectstart="return false">
-
- #if($pageHead)
-
- #parse($pageHead)
-
- #end
-
- $screen_content
-
- #parse($page_footer)
-
- </body>
-
- </html>
- <property name="layoutUrl" value="layout/default.vm"/><!--指定layout文件-->
-
这是布局文件的引用方式,我们可以把布局文件放置在网络上,项目里应用这个文件所在地址即可,这样我们就把项目里不变的静态资源抽取在同一个地方,如果在碰到布局要做修改,那么我们只需要改一个地方即可。
不管服务端采取何种动静分离,动静资源的整合都是有服务端完成,按照上文提到网站静态化的思想,这些做法不会给网站性能提升带来任何好处,它们只是给开发,运维提供了便利而已,按前文的思路,我们要把动静分离往前移,服务端往前移碰到的第一个点就是静态的web服务器例如apache或ngnix。
在讲解静态的web服务器动静分离前我要先讲一下为什么我们要在服务端前面加个静态web服务器的道理。我个人觉得在每个服务端之前都布置一个静态web服务器,该服务器起到一个反向代理的作用,而且我觉得不管我们是否使用CDN,最好都这么做,这么做有如下好处:
好处一:方便日志的记录。
好处二:在服务端之前设立了一个安全屏障,即静态web服务器可以在必要时候过滤有害的请求。
好处三:可以控制流入到服务端的请求个数,当并发很高时候,可以利用静态web服务器能承担更高并发的能力来缓冲服务端的压力,这里我补充一些实践技巧,以java里常用的web容器tomcat为例,一般官方给出它的最大并发数应该不会超过200,如果我们在tomcat前放置了一个apache服务器,那么我们可以把tomcat的最大并发数设置为无效大,把并发数的控制放置在apache这边控制,这么做会给我们系统运维带来很大的好处,tomcat虽然有一个建议最大并发数,但是实际运行里java的web容器到底能承受多大并发其实要看具体场景了,因此我们如果可以动态控制apache的并发数,这个操作很方便的,那么我们就可以动态的调整tomcat这样容器的承载能力。
好处四:可以便于我们做动静分离。
这里我们以apache为例子讲解将动静分离前移到apache的一些做法,apache有一个功能叫做SSI,英文全称是Server Side Include,页面上我们一般这样使用SSI,SSI有一种标签,例如:
- <!--#include file="info.htm"-->
-
Apache这样的静态资源服务器还支持一种动静整合的技术,这个技术就是ESi,它的英文全称叫做Edge Side Includes,它和SSI功能类似,它的用法如下所示:
- <esi:include src="test.vm.esi?id=100" max-age="45"/>
-
它和SSI区别,使用esi标签获取的资源来自于缓存服务器,它和SSI相比有明显的性能优势,其实网页特别是一个复杂的网页我们做了动静分离后静态的资源本身还可以拆分,有的部分缓存的时间会长点,有点会短点,其实网页里某些动态内容本身在一定时间里有些资源也是不会发生变化的,那么这些内容我们可以将其存入到缓存服务器上,这些缓存服务器可以根据页esi传来的命令将各个不同的缓存内容整合在一起,由此我们可以发现使用esi我们会享受如下优点:
优点一:静态资源会存放在缓存里,那么获取静态资源的效率会更高。
优点二:根据静态资源的时效性,我们可以对不同的静态资源设置不同的缓存策略,这就增加了动静分离方案的灵活性。
优点三:缓存的文件的合并交由缓存服务器完成,这样就减少了web服务器本身抓取文件的开销,从而达到提升web服务器的并发处理能力,从而达到提升网站访问效率的目的。
(友情提示:ESI这块我还了解的不太深入,听说它其实可以直接使用在jboss上,相关知识我还要继续收集资料学习)
SSI和ESI是静态web服务器处理动静资源整合的手段,那么我们再把动静整合操作往前移,这个时候就到了浏览器端了。浏览器端的动静整合的技术称之为CSI,英文全称叫做Client Side Includes,这个技术就是时下javascriptMVC、MVVM以及MVP技术采取的手段,实现CSI一般是采用异步请求的方式进行,在ajax技术还没出现的年代我们一般采取iframe的方式,不过使用CSI技术页面加载就会被人为分成两次,一次是加载静态资源,等静态资源加载完毕,启动异步请求加载动态资源,这么一做的确会发生有朋友提到的一种加载延迟的问题,这个延迟我们可以使用适当的策略来解决的,关于CSI的使用是本系列的重点,我会在后面文章里重点讲解。
好了,今天就写到这里,祝大家生活愉快,晚安。
下面我们要讨论下动静分离的策略了,一个页面什么内容是动态的,什么内容是静态的,这个我们到底该如何来区分了?这个问题学问非常大,我们的标准不同,最后拆分出来的动静资源就会存在很大的不同。在本系列开篇里,我提到了什么样的页面是静态页面,什么样的页面是动态页面,我是以一个url的角度定义的,每个独立的页面都会有一个url,这个url就好比这个页面的门牌号,我们每次访问这个url时候如果得到的响应页面都是一样的那么我们就认为该页面是静态页面,如果访问某个url,我们访问的时间不同,最后展示的页面也不一样那么这个页面就是动态页面,动态页面就是我们要进行动静分离的载体了,我们可以看到我的定义其实是和时间相关的,也就是说访问时间不同,得到的结果会不一致,所以我们可以根据时间这个维度分析页面里那些内容是静态的,那些是动态,但是这个划分在实际情况里就会变得非常复杂,下面我就讲讲这个复杂度。
场景一:假如我们是一个商户,我们查询自己网店的交易数据,一般这个交易数据我们会放置在页面的右下部分,这个部分我们很自然把它当做动态资源,就算我们的网店交易量很小,我们也不敢把这个部分当做静态资源处理。
场景二:我们网站为了给用户一个友好的体验,会在用户登录网站后在页面某个地方显示欢迎语,例如:上午好,夏天的森林,欢迎使用我们的网站!,到了下午,这个欢迎语可能就变成了下午好,夏天的森林,欢迎使用我们的网站!,那么这块内容我们应该是当做静态内容还是动态内容呢?这个就需要思考了。
场景三:网站页面里会有很多图片,有些图片的确是很久很久都不会发生变化,例如网站的图标,但是有的图片却不同了,例如有一个星期我们要为某个商户做营销活动,那么营销图片这块更新后就会有一个星期的有效期,复杂点的话,我们可能会在营销活动期间在页面的某一块专设给这个商户营销活动的内容区,这个内容区使用一个html片段,但是当营销活动结束了,这个营销的图片可能就要发生变化,营销的内容区可能会被去掉,那么这些东西我们是当做静态内容还是动态内容处理了?
由上面的场景我们可以知道,这个动静分离是要讲究策略的,如果策略设计的不好,可能我们把网站静态化处理后,效果并没有达到我们的预期。其实,我认为动静分离除了以时间维度考虑外,还应该有个维度,就是被拆分的资源是否需要服务端应用加以配合,例如像交易查询这样的动态内容,我们其实需要服务端程序按照一定的业务逻辑处理请求后从存储层获取数据,那么这种动态资源是没法做静态化处理的,还有一部分资源例如场景三里的图片以及营销的html片段,这些资源写好后在有效时间内是不会发生变化的,那么这块内容虽然时效性可能会有差异,但是它却是可以在这段时间做静态化处理的,还有种情形就是场景二了,这个场景虽然使用数据需要服务端参入计算,但是计算结果在一定时间范围内是不变的,也就是说结果是可以被缓存的,那么这块的资源也是可以当做静态化资源进行处理的,为什么说拆分策略要考虑服务端应用的因素了?因为上面这些场景都是由服务端应用参入的形式所决定,在有效时间里服务端应用不需要参入,或者参入一次后,可以长期保存结果,那么我们可以把这些资源当做静态资源处理。
除此之外,服务端应用和结果的密切度也是要当做考虑的因素的。在web开发里,除了需要浏览器处理的,其他技术都可以当做服务端来理解,如果我们网站使用到了CDN,使用到了静态web服务器例如apache,以及服务端的web容器例如jboss,那么按请求的行进路径,我们结果处理越早那么网站响应效率也就越高,所以当请求在CDN返回了,那么肯定比在apache返回效率高,在apache就返回了肯定比jboss返回的效率高,再则服务端的web容器本身因为服务端程序运行要消耗部分系统资源,所以它在处理请求的效率会比CDN和apache差很多,所以当我们按照动静分离策略拆分出了静态资源后,这个资源能不放在最底层的服务端的web容器处理就不要放在服务端的web容器里处理。
由上所述,我们再回过头来看看静态web服务器的SSI技术,这个技术使用起来和我们在服务端使用include类似,但是在SSI使用include一定会比在服务端效率高,因为服务端在整合动静资源时候还会掺杂很多服务端程序处理,因此动静资源的效率就会大打折扣。我们再看看SSI的include的用法,如下所示:
<!--#include file="info.htm"-->
这个写法是使用页面的注释标签,当静态web容器处理请求时候,它会扫描里面的SSI标签,接着就会处理这个标签的内容,如果找到了资源那么web容器会将资源插入到页面里,如果web没有处理这个SSI标签,那么等结果到了浏览器,这个也就是一个注释而已,不会影响页面的展示,而且SSI标签处理的资源也是非常丰富的,不管这个资源是静态的,还是动态的,只要获取时候是个完整的资源都能被正常加入到页面里,所以像前面的场景二这种动态的内容也是可以正常处理的。因此场景二,场景三这样的情况都可以使用SSI来解决。SSI的作用当然不仅仅只是可以做include操作,它的标签也可以做一些逻辑上的操作,讲述如何使用SSI不是本文的重点,有兴趣的朋友可以去研究下。
不过SSI也有自己的局限性,它的第一个局限就是SSI解析是静态web容器来完成,因此它会消耗web容器的性能,如果SSI使用时候还有一定的逻辑,那么这种性能消耗就会更大,其实我觉得更加重要的是如果静态web容器过渡使用SSI,那么就会把自己变成了一个服务端的web容器,除了会影响到请求处理的效率,它还会降低自身的并发处理能力,所以我们希望资源整合策略交给外部服务处理效果会更好些,如是有些大型互联网公司使用ESI技术,ESI技术和缓存关系密切,这个内容我就放到下篇讨论了。
本篇最后我要再讲讲CDN的问题,上篇我讲到静态web容器整合动静资源的好处,由此我说如果CDN可以做动静整合,那么就能做到就近处理,这样效果会更高,今天我对这个做法做了一些考证,觉得该说法有点不妥,至少我现在的公司没有使用到这样的技术,CDN技术应该由三个步骤组成,首先是解析DNS,找到离用户最近的CDN服务器,接下来CDN要做一下负载均衡,根据负载均衡策略将请求落地到最合适的一个服务器上,如果CDN服务器上就有用户所需要的静态资源,那么这个资源就会直接返回给浏览器,如果没有CDN服务器会请求远端的服务器,拉取资源再把资源返回给浏览器,如此同时拉取的资源也被缓存在CDN服务器上,下次访问就不需要在请求远端的服务器了,CDN存储资源的方式使用的是缓存,这个缓存的载体是和apache,nginx类似的服务器,我们一般称之为http加速器,之所以成为http加速器是为了和传统静态web服务器区别开来,传统的静态资源服务器一般都是从持久化设备上读取文件,而http加速器则是从内存里读取,不过具体存储的计算模型会根据硬件特点做优化使得资源读取的效率更高,常见的http加速器有varnish,squid。Ngnix加上缓存模块也是可以当做http加速器使用的,不管使用什么技术CDN的服务器基本都是做一个就近的缓存操作,这也就是说CDN是否可以完成SSI操作是值得商榷的,所以前文的说法还是有点问题的。
好了,今天就写到这里,祝大家生活愉快。
由此可见SSI在互联网的应用还是非常广泛的。其实互联网很多网页如果我们按照动静分离策略拆分,绝大部分都是可以当做静态资源处理,例如新闻网站,文学网站,这些网页生成后,大部分的资源都是不变的,说白了这些网页本质就是一个静态页面,我们开发他们时候也不需要服务端的参入,每一个网站都有自己固定的板式,假如每个新网页都要完完整整的开发,重复性的工作实在太多了,出错的概率也非常高,在本系列第二篇里我曾经详细介绍了velocity的布局模板技术,其实SSI也可以制作出一套固定的模板,开发时候我们只需要在定义好的模板里添加或者修改我们需要更改的内容就可以完成一个页面的制作,可见SSI技术为了我们开发网页提供了很大的便利。
与SSI相对应的还有ESI,这个技术不是太常用,在网上能收集到的资料也不是太多,网上有限的资料也基本都是和淘宝相关的,不过仔细研究下淘宝对ESI的运用,对于理解网站静态化处理是非常有借鉴意义的,下面我将重点讲讲ESI的知识。首先看个场景啊。
我们登录支付宝网站,到了个人首页我们发现在支付宝下面有一个条目,如下图所示:
这是支付宝默认给我们显示的【生活好助手】,右边有个箭头,我们点开它,如下图所示:
我们看到这里有添加的按钮,通过添加按钮,我们可以添加其他常用的组件图标。(注意:我这里只是以支付宝这个功能为例,支付宝是否按照我说的设计,这个我就不清楚了)
如果我们按照自己个性化添加了自己的组件,不同的人添加的常用组件也会是不尽相同的,如果我们自己开发的网站也有这样的功能,那么我们该如何设计了?我们一般都会很直观的把这个新增的组件信息存储在数据库里,用户每次登录时候该信息也会随之从数据库里读取,但是这个场景对于像支付宝这种用户量极大,日均访问量极高的大型网站,这种个性化又非核心的功能都从数据库里读取,那么它对数据库造成的压力一定是十分巨大的,在存储的瓶颈里我们讲了那么多优化数据库的手段,其核心手段有一个就是减少对数据库价值不高的操作,而这种个性化配置跟支付宝的支付操作相比起来,价值度实在太低,因此我们最好的选择是避免数据库承担过多此类的操作。
不过像上面这个场景里的功能,它所使用的数据又不是那种可有可无的,假如数据存储的不可靠造成数据丢失还是会造成不必要的麻烦,所以我们还是会把这些信息做持久化存储。此外像上面的【生活好助手】条目还是页面的一个重要组成部分,因此像SSI那种使用html注释指令,当指令无法正常解析,就直接返回到浏览器,因为是注释,所以页面也不会显示它,SSI的这种做法用在上面场景肯定是不太合适的。这样的场景在电商网站里是十分常见的,例如一个商品页面,页面里会有商品的图片,还有商品的详细介绍,这些内容其实都是会用持久化系统进行存储,同时它们本身也是网页的重要做成部分,如果碰到问题就忽略最终会造成页面显示错误。
结合上面的场景我们来讨论下ESI技术了,ESI技术和SSI技术类似,其实也和jsp里的include指令类似,它也是在页面里使用一个指令标签web容器解析这个标签后将获取的数据替换掉这个标签。我们来看看ESI的使用方法,我们可以在velocity里自定义一个esi标签,velocity里的使用如下所示:
esiTool.setTemplate('test.vm').addQueryData('id', 100)
velocity引擎解析vm模板,最终会把vm解析成html页面,这个时候该页面里使用esi标签的地方就被转化为:
<esi:include src="test.vm.esi?id=100" />
当页面到了服务端web容器之前的静态web容器(该web容器要安装好解析esi的模块),静态web容器就会解析这个esi标签,静态web容器会以test.vm.esi?id=100 作为key,到缓存系统里查找信息,如果查到了信息,缓存服务器就直接返回,用返回内容替换掉esi标签,如果缓存里没有找到则会直接请求持久化系统,持久化系统返回信息后,缓存系统将信息缓存起来,同时也将信息返回至静态web容器,那么下次用户再访问同样内容就会直接从缓存里读取了。
ESI技术和SSI很像,只不过ESI技术是和缓存技术配合起来的,同时ESI标签也不像SSI标签那样使用注释的形式,因此ESI标签是一定要被解析的,如果仅仅是缓存,ESI和SSI比较起来也没显得那么有优势和特别,但是对于电商这种场景而言ESI的现实意义非常大,电商网站也是一个由用户产生内容的网站,每一个商家的店铺虽然我们都知道它是属于淘宝或天猫的,但是单独一个商家的店铺都是个性化很强的,与其他店铺差异很大,为了买卖商品,商家会上传自己商品的图片,还会使用图片和文字描述自己的商品,单个商品页面我们做动静分离分析,很容易分辨出动态内容和静态内容,但是如果一个电商平台拥有超乎想象数量的商家,那么每个页面的图片,文字和商家页面的关系就会变得有点微妙了。由于电商网站的图片特别多,那么电商网站系统一般都会设计一套管理这些小图片的分布式存储系统,例如淘宝的TFS文件系统,它是专门针对图片使用的分布式文件系统,这些文件系统里存储的图片会和商家紧密关联,这就让图片本身拥有了一定的动态属性,但是对于每个商家页面而言,商家自己的图片资源都是可以静态化的,也就是说图片的读取是要通过商家信息进行计算的,计算出的结果对于商家而言又是静态的,可以被缓存的。但是这个静态资源的处理时候就变得复杂了,而这些是SSI无法完成的。
首先我们直接从文件系统读取图片,效率是非常低效,因此我们还是会把它们缓存在内存里,但是由于不同图片和不同商户是相关联,那么对于缓存查找时候是需要一定的条件,不同商户对自己页面的设计方案也会有所不同,一般商户这些资源,存储系统肯定会按照设计模板的维度存储,不同商家由于商品和文化的不一样,那么使用的模板也不一样,因此资源返回静态web容器前还需要一个整合过程,这样场景下的静态资源获取其实是需要一定逻辑计算的,那么这个计算一般都会在开发时候由代码完成,所以从上面ESI使用的例子看到,开发人员会使用velocity的esi标签,这个标签开发人员可以设置参数,velocity引擎最终会将这个标签解析成静态web容器可以解析的esi标签,标签里有这样的代码test.vm.esi?id=100,文件后面会带上参数,那么这个参数其实是动态的,那么这个参数也就是缓存系统获取正确信息的规则了。这样我们就完成了静态资源获取的逻辑计算,计算完毕后这些资源会在一段时间里长期有效,因此它就演变为静态资源,可以被缓存了。ESI比SSI强大多了,同时ESI也可以完成SSI的功能,所以使用了ESI也就没必要用SSI了。
像我们平时做web开发时候可能没有太留心一个问题,一般的web开发里使用的静态资源例如图片,css文件,js文件我们都会放置在一个resource包里,如果是企业开发,这个web应用上线时候也就直接打包在web工程里,一些互联网网站也只不过会将这些资源放置在单独的静态服务器上,我平时开发时常听到有人说,项目里图片太多了,应该合并下,css文件和js文件也太多了也要合并下,这个多到底多多少了,几十个文件,几百个文件,这个要和社交网站,电商网站这种用户可以产生图片的网站比起来那就是小巫见大巫了,因为用户能产生内容的网站静态资源会随着时间推移文件规模变得异乎寻常的大,所以此类网站的静态资源已经没法放置在项目下,它就要求我们需要有新的手段管理这些静态资源,并且有新的手段使用这些静态资源,那么像TFS文件存储系统出现了,缓存技术出现了,最后我们在应用里使用ESI技术把它们整合到我们网页里,通过这个分析我们就能明白ESI适用的业务场景了。
网站静态化处理我们首先要按规则拆分动静资源,拆分出来的静态资源该如何处理就是网站静态化处理的关键所在,把静态资源处理从服务端的web应用里剥离出来,不让服务端的web应用参入过多的静态资源解析,这样就可以为服务端的web应用减少不必要的处理操作,从而达到提升服务端web应用的运行效率,接着我们就把拆分出来的静态资源处理操作往前推移到静态web服务器,前两篇文章和今天的文章我着重讲解了静态web服务器处理静态资源的手段,那么这里有个问题了,这些处理可以再往前推到浏览器来完成吗?答案当然是否定的,首先浏览器的缓存是非常不可靠的,如果用户把浏览器设置为不缓存任何数据的模式,那么浏览器就没法缓存数据了,而用户的行为那是根本没法控制的,其次浏览器缓存的数据量是有限的,如果我们要在浏览器进行缓存也是缓存最有价值缓存的数据,更重要的一点,为了做好网站静态化处理我们对网页的动静资源做了拆分,但是拆分出的静态资源也并不是完全不需要进行任何逻辑处理就能使用的,例如前面讲到的ESI适用的场景我们就发现,有些静态资源的获取还是要很多条件的参入,而这些条件是由动态数据产生的,那么这样的静态数据浏览器是没法做缓存的,这点也说明了拆分出来的静态化资源绝大部分还是要停留在服务端的,居然只能停留在服务端,那么最为高效的处理这些静态资源的地方就是CDN和静态web容器了。所以在本系列的第一篇里我讲到网站生产部署时候最好是在服务端web应用之前放置一个静态web容器,如果有了这样的静态web容器做反向代理,那么我们就可以让它来完成静态资源的相关操作,而且静态web容器还能辅助完成一些逻辑上的处理,从而弥补了CDN的不足之处。当然这么做的好处不仅仅只有这些,第二篇文章里我曾经讨论了反向代理的好处,可能大家印象还不是很深刻,我将会在后面文章里对反向代理做更加深入的分析。
讲完了ESI的妙用后,我下面将讲讲本篇的主题缓存了。其实单独讲缓存真的没啥太多内容,不过在网站静态化处理里的缓存还是和存储里讲到的分布式缓存有所不同,分布式缓存的数据都是存储在内存里,而网站静态处理的缓存既有存储在内存里还有存储在硬盘上,当然存储在内存里读取速度会更快,但是网站的读取效率还和资源距离用户的远近有关系,例如浏览器的缓存其实是把静态内容缓存在硬盘上,但是因为不需要通过网络获取资源,因此它的读取效率就显得特别高了,除了这个因素外还有个因素,我们前面做动静拆分的目的就是想拆分出静态资源,让这些静态资源远离服务端的web应用,这样可以减少服务端不必要的压力,从而达到提升服务端web应用处理能力,而把静态资源放置在静态web容器处理,它的处理静态资源效率又会高于在服务端web应用的处理,从而也达到提升静态资源读取的效率。不过如果静态资源最终只能放置在服务端,那么这个时候我们把静态资源存入到缓存里即内存里效率肯定比在硬盘上高,所以CDN的服务节点一般都是采取将静态资源存储在内存里,就算是服务端web应用前的静态web容器,如果我们让静态资源缓存在内存里,效率肯定也是比在硬盘上高。
好了,今天就写到这里,最后祝大家生活愉快,新年快乐。
CSI技术其实是在页面进行动静分离后,将页面加载分为两个步骤完成,第一步是加载静态资源,静态资源加载完毕后进行第二步骤加载动态资源。不过这个定义还是表述的不全面,不全面的地方就是我们要强调动静分离的目的,我们把页面里的动静资源拆分出来是为了将静态资源做有效的缓存,这个静态资源可能是在静态web容器上,也有可能是在CDN上,也有可能是在浏览器上,不管静态资源是如何缓存的,我们的目的都是为了让静态资源加载的速度更快,如果我们没有让静态资源加载变得高效,就算我们使用了CSI的形式来设计页面,其实也没有发挥CSI的优点,反倒还会一不小心引入CSI的缺点。那什么是CSI的缺点呢?具体如下:
CSI的缺点一:CSI不利于页面的SEO即搜索引擎优化。搜索引擎的网络爬虫一般是根据url访问页面,获取页面的内容后去掉没用的信息例如:css样式,js脚本,然后分析剩下的文本内容,因此假如页面的一部分内容需要进行异步加载,那么这个加载控制肯定是由javascript代码来完成的,因此网络爬虫爬下来的页面里异步加载的操作是没法执行的(听说有些高级的爬虫可以执行异步的操作,抓取异步的内容,即便有这个技术,大部分主流的爬虫还是会忽略掉javascript代码的也会忽略异步加载的内容的),这就会导致爬虫爬的页面里有部分信息丢失了,所以说CSI对SEO不太友好。不过这个缺点我们仔细分析下,可能并不会是那么严重,前面我们谈论了很多静态分离的策略,如果我们动静分离策略做的好,那么动态资源基本都是不能被缓存的内容,经常发生变化的内容,这些变化的内容本来就不需要被网络爬虫爬到,就算真的被爬到,搜索引擎有个查询结果指向了这个页面,我们点开这个页面结果也是在页面找不到被搜索的关键字,这种情形我相信很多朋友在使用搜索引擎时候都会碰到过。不过我想如果开发人员没有正确使用CSI,那么这块他们可能也不会处理的特别好,因此这个缺点还是很容易被引入的。
CSI的缺点二:我们那么费时费力想让自己的网站静态化,目的就是想让页面加载更快点,我们简简单单把页面加载分成了两个步骤进行,那么这么做就真的快吗?这可不一定啊,其实动静分离的做法和我上一个系列里讲到的数据库读写分离有类似之处,数据库读写分离我们是通过拆分原表的读写之间的关联关系,从而达到解决读的瓶颈问题,而网页的动静分离是因为静态资源很容易被优化,所以我们要拆分动静资源。所以当我们对资源进行了动静分离,但是又没有优化静态资源,这个一看就知道我们缺少一个加速页面加载速度的操作,那么真的能让页面加载快点,还真的很难说了,而且异步加载需要执行javascript代码才行,但是静态资源加载时候很容易造成javascript脚本被阻塞,如果阻塞的脚本正好是异步加载的部分,结果只会是比以前加载的更慢了。
由此可见,我在前面讲到的SSI和ESI技术对于我们在浏览器端发挥CSI技术优点是非常有必要的,SSI和ESI做好了能让动静分离出的静态资源加载的更加高效,这也就让CSI操作的第一个步骤变得高效,第一个步骤处理好了我们只要在页面控制好脚本阻塞对异步加载的影响,那么我们就可以达到提升整个页面加载效率的目的了。此外我觉得CSI对SEO有重大影响是个伪命题,假如使用CSI造成了SEO效果不佳,那么肯定是我们CSI方案设计的不到位。
有人认为CSI还会有个缺点,不过笔者我并不认为这是一个缺点,这其实是一个设计问题,好与坏是根据个人的操作习惯所决定的。这个别人认为的缺点是什么呢?它就是使用CSI技术时候,虽然页面很快的被加载出来了,但是动态内容那部分可能会显示一个正在加载的提示,那么这就导致页面用户友好性降低,其实这种同步和异步加载混搭操作实在太常见了,几乎所有大型门户网站,电商网站还有一大堆数不尽的网站都是采用同步和异步混搭的加载方式,假如这些网站不这么做,我相信这些网站例如首页加载一定会慢的让人吐血,因为它们很多网页里面内容实在太多,图片也都有点爆棚了,所以它们不得不使用同步和异步混搭的加载方式,甚至很多静态资源例如图片,flash这些东西也会采取异步加载方式。说到这里,估计有人还是觉得不服气,他就是不喜欢页面加载时候还要出现个正在加载提示,但是网页里又非常需要CSI带来的好处,那么我们该如何解决这个问题呢?这个问题很好解决,首先愿意使用CSI技术也就说明用户还是很愿意使用异步的加载技术的,不喜欢则是正在加载的提示,这说明用户想要在做同步加载操作时候不要掺杂异步操作,虽然现在ajax技术大行其道,但是ajax技术有个同步加载是没有办法解决的,那就是我们在浏览器地址栏里输入网站url请求页面 ,所以面对上面的需求我们只要保证这种同步操作只是一个纯粹的同步操作而不要掺杂异步加载即可,这个方案还是很好实施的,这里我就不再累述了。
动静分离后我们会把静态资源进行缓存,前面文章里讲了一大堆都是在讲服务端的静态资源缓存,现在讲到了CSI已经到了浏览器端,那么我们就得谈谈浏览器的缓存操作。页面的缓存操作就是使用http的expires和cache-control,我们首先看看下面的写法:
<meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0">
这是我现在做的java的web项目里,jsp和vm文件都会使用的meta配置,它的目的就是让页面不要被浏览器缓存,但是如果使用CSI技术,同时动静分离做的很好,那么在页面头部其实我们可以不再这么写了,我们可以让页面在合理的时间范围内被浏览器缓存,如果该页面做了缓存操作,那么以后我们再访问该页面,网页的加载效率就会变得更高了。
这里还有个问题,在雅虎优化网站的建议里,为了充分利用网页并行加载的特点,我们往往会把图片,外部的js和css文件放置在单独的静态web容器或CDN上,那么这些文件往往也是可以被浏览器缓存,这个我们又如何设置才能让浏览器知道要缓存它们呢?这里我们以apache为例,为了让静态资源被浏览器缓存,apache需要使用mod_expires模块,然后在apache的配置文件里添加如下配置:
<FilesMatch "\.(gif|jpg|png|js|css">ExpiresDefault "access plus 10 years"</FilesMatch>
那么浏览器访问此apache上的静态资源后,浏览器就会把图片和该服务器上的js和css文件缓存在浏览器里。
我们看看被缓存的静态资源是如何被使用的,如下图所示:
当http的响应码是304的时候,那么浏览器就会从缓存里读取资源了,这里有的朋友可能会感到奇怪为什么缓存的资源还要发送个http请求了?理解这个我们就要了解下缓存的机制,缓存的含义是临时保存某些东西,既然是临时保存,那么就应该有个保存的有效期,我们定义缓存的方式是通过http完成的,那么按道理检查缓存是否过期也应该是http来决定的,因此每次使用缓存时候我们要发个请求到服务端,服务端会检查下资源是否过期了,如果没有过期,服务端返回个304的响应码,304的返回响应是没有http报文体的,所以这个http请求的返回数据是非常小的,因此这个http效率还是很高的,如果服务端发现资源过期了那么服务端就会把新资源返回给浏览器了,其实这个检测资源是否过期的请求有个专有名词叫做条件Get请求。至于服务端是如何完成检查操作,本系列在讲web前端优化时候会详细阐述,这里就不深入了。看到这里估计有朋友又有疑问了,为什么缓存是否过期不能在浏览器端来做了?这主要是浏览器做这个检查非常不准,因为用户的电脑时钟不一定准确,或者用户电脑时钟和服务端不一致,如果再加上时区那么就更加麻烦了,所以缓存失效最好是在服务端进行,这样缓存的有效期的准确性才能得到保证。html5的出现,浏览器缓存的能力大大增强了,不过使用html5技术进行缓存我还没有深入研究过,所以这里也不讲述了,有兴趣的朋友可以自己研究下。
好了,CSI主题内容讲完了,讲到CSI技术和浏览器我们就可以开始本系列另一个重要内容前后端分离了,这将是我下篇的主题,我在自己博客里多次讲到前后端分离,马上又要再次讲了,这次讲是我这么长时间做前后端分离研究的大总结了。
最后,祝大家新年快乐,新的一年喜气洋洋,开开心心哦。
前后端分离技术我个人认为是web前端被专业化以后的必由之路,而nodejs的出现是前后端分离技术的一个强兴的催化剂,原因是nodejs的出现削平了前端技术和服务端技术之间的鸿沟,使得前后端两套不同技术体系进行真正意义的解耦提供了无限的可能性。但是如果我们把nodejs技术的使用认为就是实现了前后端分离,这种理解又实在太肤浅了,下面我将讲讲我研究过的前后端分离技术方案,以及这些技术方案隐藏在背后思考,希望这些思考能给大家以一个新的思路来理解前后端分离技术。
我们要深刻理解前后端分离技术有一个重要的前提,那就是要把前后端分离技术认为是传统的web应用里的MVC设计模式的进一步演进。那么我们首先来看看MVC的定义,下面的内容摘录于维基百科的解释,具体如下:
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。 MVC模式最早由Trygve Reenskaug在1978年提出[1] ,是施乐帕罗奥多研究中心(Xerox PARC)在20世纪80年代为程序语言Smalltalk发明的一种软件设计模式。MVC模式的目的是实现一种动态的程式设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组: (控制器 Controller)- 负责转发请求,对请求进行处理。 (视图 View) - 界面设计人员进行图形界面设计。 (模型 Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
各类用于Web应用开发的语言里都有属于自己的MVC框架,例如本人最熟悉的服务端语言java里就有大名鼎鼎的struts2,springMVC的MVC应用框架,我早期从事java的web开发时候认为这些MVC框架都是非常的博大精深,用途广泛,但是当我逐渐转向了web前端技术开发以后又觉得这些框架的很多功能显得那么的多余和累赘,因此我曾写过一篇文章专门讨论过这些问题,该文章的名字叫做《为什么做java的web开发我们会使用struts2,springMVC和spring这样的框架?》。
其实这篇文章被写的源头就是在于我认为像struts2和springMVC这样的框架做了太多浏览器本身就可以完成的工作,例如:页面的渲染操作,因为服务端抢了浏览器端的部分工作,这其实也就等于限制了web前端技术的深入运用,像很多前端的优化技术以及很多提升用户体验的技术就很难派上用场,之所以产生这些问题,我认为传统的MVC框架本质其实是一个服务端的MVC框架,虽然MVC设计模式里的V即View视图层是想把界面开发工作专业化,让界面设计人员能专心于界面开发,但是传统的MVC框架下的View层的本质却是一个不折不扣的服务端技术。
我们以java的web开发里jsp为例,JSP全名为Java Server Pages,中文名叫java服务器页面,其根本是一个简化的Servlet设计,它是java里动态网页的技术标准,这就说明jsp虽然看起来像html,其实它并不是真正的html,它需要被java的web容器进行解析转化为浏览器可以解析的html页面,然后通过网络传输到浏览器后,浏览器才能正确的展示这个jsp页面,其他web开发语言里都有类似的动态网页技术标准,但是不管什么语言的动态网页技术标准,我们使用它时候就是让web前端技术被服务端技术所绑架,这也就是为什么每个招聘web前端工程师的岗位都要问你是否会java,php语言的源头。但是随着互联网的大发展,对web前端的要求是越来越专业化,web前端本身所包含的技术难度已经不亚于任何一个服务端语言开发难度,因此我们需要web前端更高的专业化,而不希望web前端工程师被服务端技术束缚的更多而限制了自身能力的发展,这就导致前后端分离技术的出现。
不过前后端分离技术的第一阶段倒不是从改变view层即视图层开始的,而是从连接客户端和服务端的C层即控制层开始的,控制层既要作用于客户端又要作用于服务端,如果一个功能页面是一个程序员从浏览器端一直写到模型层,控制层也就不是什么问题了,但是如果当我们想按MVC的设计思想,让界面开发人员专注于页面开发,服务端开发人员专注于服务端开发,那么这个时候控制层的归属问题就显的非常重要了。在传统的MVC框架里,因为M层和C层是使用同样的语言体系,因此我们很自然会把M层和C层的开发工作都交由服务端开发人员完成,这个决定无可厚非,但是传统的MVC框架里V层和C层其本质也是同一个技术体系下的(例如java的web开发里的jsp本质就是个servlet),因此V层和C层也是紧耦合的,因此界面开发人员开发页面时候如何没有C层支撑,那么这个页面其实是根本跑不起来的,如果前端开发人员这时候跑去写写C层即控制层的代码,这就打破了原有的横向分工,这个时候控制层的编码工作就会变得混乱而难以控制,看到这里有人一定会说既然控制层是属于服务端的,那么前端技术人员就等等服务端的开发进度,再不行就自己写个mock模拟下服务端的控制层,听到这种建议,我相信不管是前端的还是服务端的技术人员都会头脑发麻,第一反应就是这不是自找麻烦啊,还不如一个人全部搞定算了。由此第一阶段的前后端分离技术方案出现了,这个方案需要解决的问题就是如何能让web前端技术人员和web服务端技术人员协同起来工作,合理的分工,换句话说就是按web前端和web服务端角度如何能横向的分解web的开发工作。
前后端分离的第一阶段需要解决问题的核心就是控制层的归属问题,从技术角度而言就是控制层到底是应该和视图层解耦比较合理还是跟模型层解耦比较合理的问题。那么我们这里先回顾下MVC设计模式里对控制层的定义,维基百科里的定义是:
(控制器 Controller)- 负责转发请求,对请求进行处理。
不过这个解释我认为并不全面,以java的web开发里的控制层设计为例,我们发现控制层以沟通视图层和模型层的角度而言,控制层其实主要完成三项具体的工作,它们分别是:
工作一:控制层起到一个路由的作用。客户端请求到达控制层后,控制层根据请求内容将请求路由到服务端某个模型层进行处理,模型层将请求处理完毕后,会把响应结果返回给控制层,控制层在根据响应信息路由到特定的页面。
工作二:控制层起到一个报文信息格式转化的作用。这里以java的web开发为例,浏览器的数据都是以http报文形式发送给服务端,而控制层就是将http报文信息解析成java的对象,当然也可以是java的基本数据类型,然后控制层把解析好的信息传递给模型层进行处理。
工作三:传统的MVC框架里,控制层其实深入参入到了页面渲染的操作。在java的web开发里的控制层不管如何被包装,其本质就是一个servlet,而jsp页面本质也是个serlvet,因此我们可以这么理解jsp,jsp就是以页面开发的方式写java,而servlet就是以java的方式写页面,所以我们可以在servlet里以文件流的方式输出页面,也可以让servlet跳转到jsp页面。
由上面的论述里我们发现,其实传统MVC框架里控制层和模型层的联系方式相对很简单的,它们的联系主要是路由和报文格式的转化上,而控制层与视图层的联系除此之外还多了一个页面渲染,而页面渲染本身应该是属于浏览器的技术范畴,是浏览器技术不可分割的一部分,也是我上面内容里诟病传统MVC框架问题所在,如果控制层承担了页面渲染工作,那么控制层和视图层的耦合度就变得非常高,要想将其解耦是十分困难,一般只有我们打破了现有MVC框架的技术体系才能完成,相比之下,控制层与模型层的解耦就显得容易多了。那么控制层与模型层如何解耦呢?具体如下:
首先我们来解决下报文格式转化的问题,这个技术方案很简单就是借鉴http统一报文格式的特点,我们为控制层和模型层定义一套统一的报文格式,例如我们定义控制层和模型层都以map的数据类型进行数据传递,这个map里有个专门的字段用来定义被路由到的模型接口信息,有个字段专门存储需要传递的数据,具体的设计方案可以根据实际的业务需要来设计。
接下来就是路由的问题了,在解决报文格式转化问题的论述里我讲到要在统一报文格式里专门定义一个字段用来存储该数据到底路由到哪个模型进行处理,不过这个字段并不能完全解决路由问题,因此我们需要模型层对控制层提供一个统一的接口,任何控制层与模型层的沟通都通过这个统一接口来完成,只不过不同请求报文组装的内容不一样而已,而这个接口还有个重要职责就是解析报文里的路由信息,让请求能被正确的路由到对应的模型接口所处理。当然这个接口的返回值最好也是一个统一的报文格式,这样控制层解析模型层的返回数据也会便利的多了。
由上所述,我们发现第一阶段的前后端分离工作控制层应该归属于web前端,这么做更加合理,也更加容易实现,其实之后进化版的前后端分离方案,控制层也都是属于web前端,只不过形式不同而已,这个我在下一篇文章里继续讨论。
第一阶段前后端分离方案解决的核心就是让控制层和模型层解耦,这个方案进一步演化一下,我们可以把控制层和视图层独立成一个web应用,模型层也独立成一个web应用,两个web应用之间通过远程调用方式进行沟通,这个方案我在以前文章里写过,这篇文章的名字叫做《我设计的网站的分布式架构》。
这个进化版的方案增加了系统开发的难度,因为我们需要增加网络通信的编程以及远程调用的实现,更麻烦的是我们还需要进行复杂的多线程编程,既然增加了开发的难度为什么我还要这么做呢?首先我们通过应用分层,可以动态的调节web前端和web服务端的负载压力,还可以在模型层之前提供一道安全屏障,不过被服务端绑架的web前端在提升整个web应用负载能力这块还是很有限的,其实这种做法的最大好处就是利于SOA框架的设计,也就是说这种架构我们可以为服务端的SOA化提供有力的保障,因为控制层和模型层的解耦,可以让模型层真正做到专注于业务,而不会再发生那种把业务逻辑写到控制层的问题了从而降低代码的健壮性。
好了,今天就写到这里,最好祝大家新年快乐,晚安了。
首先要提的就是javascriptMVC,下面我摘抄的是维基百科里对javascriptMVC的解释,具体如下:
首先是简介:
JavaScriptMVC 是一套开放源代码的多样化互联网应用程序框架,以 jQuery 与 OpenAJAX 为基础。JavaScriptMVC 利用 MVC 架构与工具扩展这些函式库,以便开发与测试。由于 JavaScriptMVC 不需要任何服务器端的配合,因此它可以和任何的网站服务接口与编程语言整合,如 ASP.NET、Java、Perl、PHP、Python 或 Ruby。
接下来是历史:
JavaScriptMVC 的第一个版本是在2008年5月释出。稳定版的 JavaScriptMVC 2.0 在2009年6月释出,并以 jQuery 为基础。主要开发目标为维持程式码的简短和专注在它独特的功能上。3.0版本在2010年12月释出。而从 JavaScriptMVC 中所独立出来的 MVC 架构“CanJS”则在2012年4月释出。
从维基百科里的解释我们会发现如下启示,它们分别如下:
启示一:javascriptMVC是一个应用框架的名字,这和jQuery的命名是一样的,所以这里我要声明一下,本系列里的javascriptMVC不是指代这个框架,而是指代的是使用javascript语言实现出的一类的web前端的MVC框架,本系列后面的javascriptMVC和前端MVC的含义是一致的。
启示二:从javascriptMVC历史里我们可以看到第一版的javascriptMVC产生于2008年,这个历史要远早于nodejs出现的时间,这说明了前端的MVC并不是因为nodejs的出现而产生的,应该是nodejs推动了前端的MVC框架的应用和普及。
启示三:维基百科里有一段解释:
由于 JavaScriptMVC 不需要任何服务器端的配合,因此它可以和任何的网站服务接口与编程语言整合,如 ASP.NET、Java、Perl、PHP、Python 或 Ruby。
这段话说明了前端MVC的一个很重要的特点就是前端MVC可以摆脱服务端语言的束缚做到真正的独立,同时前端MVC又可以和任何服务端语言进行整合,大家可以试想下如果我们开发的web应用前端达到了前端MVC的程度,那么公司在招聘web前端工程师的时候就不在会问你“你会java吗?”或者“你会php吗?”假如这个前端工程师所会的服务端语言能力和公司不匹配,面试官也不会再犹豫和摇头了。
启示三同时还隐含了一个问题,为什么好的前端MVC框架可以做到和任何服务端语言配合呢?这个解决手段之一我在前文中的第一阶段前后端分离方案里就提到了,那就是解决报文格式的统一和交互接口的统一的技术手段,只有这样前端MVC和服务端的灵活对接就不会再是问题了。但是仅仅这个手段还是远远不够的,我们要达到这个需求还需要解决一个问题,这个问题就是要把服务端MVC霸占web前端的工作也要抢回来。那如何抢呢?
上篇文章里我分析过服务端MVC的视图层的问题,服务端MVC的视图层技术例如java里的jsp技术,这个技术是将html和java代码整合的技术,java的web容器把jsp解析完毕后最终生成为html文件发送给浏览器,浏览器在解析这个html将最终效果展示给用户。那么我们要抢回服务端霸占的web前端的工作我们就得分析下这些动态页面技术到底做了哪些事情特别是侵占web前端的事情。
这里首先我们要谈谈服务端在动态页面里的作用,其实服务端为动态页面作用很单一就是提供了网站需要展示的数据而已,服务端是不会创造一个新页面的。服务端提供的数据的类型也是很统一,要不就是服务端语言提供的基本数据类型例如:字符、数字、日期等等,要不就是复杂点的数据类型例如数组、列表、键值对等等,不过归属服务端的动态页面还需要服务端语言帮助做一件事情,那就是把服务端提供的数据整合到页面里,最终产生一个浏览器可以解析的html网页,这个操作无非就是使用服务端语言可以构造文件的能力构建一个符合要求的html文件而已。不过一个页面里需要动态变化的往往只是其中一部分,所以做服务端的动态页面开发时候我们可以直接写html代码,这些html代码就等于在构造页面展示的模板而已,而模板的空白处则是使用服务端数据填充,因此在java的web开发里视图层技术延生出了velocity,freemark这样的技术,我们将其称之为模板语言的由来。
由此可见,服务端MVC框架里抢夺的web前端的工作就是抢占了构建html模板的工作,那么我们在设计web前端的MVC框架时候对于和服务端对接这块只需要让服务端保持提供数据的特性即可。从这些论述里我们发现了,其实前端MVC框架要解决的核心问题应该有这两个,它们分别是:
核心问题一:让模板技术交由浏览器来做,让服务端只提供单纯的数据服务。
核心问题二:模板技术交由浏览器来承担,那么页面的动态性体现也就是根据不同的服务端数据进行页面部分刷新来完成的。
而这两个核心问题解决办法那就是使用ajax技术,ajax技术天生就符合解决这些问题的技术手段了。
要让web前端承担模板技术,就得使用javascript的模板技术,时下javascript的模板技术可谓是百花齐放,百家争鸣,很多朋友曾为这些技术称奇,其实探求它的本源无非就是用javascript为基础实现了个jsp,velocity而已,如果有朋友还没接触过javascript模板技术,可以在百度里搜索下【javascript模板引擎】,本文这里就不展开谈论了。
前端的MVC讨论到这里又出现了一个新的疑问,我上面讲到解决前端MVC两大核心问题的手段是ajax技术,ajax是异步请求,那么这是不是就是说让网站全部使用异步请求我们就可以实现前端MVC,并且解决网站所有的问题呢?
这个问题的回答当然是不可能的。一个网站是永远没法摆脱与异步请求相对的同步请求,就算有个网站把异步做到了极致,但是它也无法摆脱用户第一次访问要在浏览器地址栏填写网站入口页面url地址的同步请求问题,网站把异步操作做到极致也无非就是把网站做成了一个纯粹的单页面形式而已。
纯粹单页面的网站很多人一听到就觉得好牛逼啊,很前卫,很厉害,对前端有所了解的人还会想到单页面也就意味要运用更多的javascript编程和DOM编程,前端代码难度也会大大增强,好的单页面应用如果这个应用还包含复杂的业务逻辑,那么单页面前端开发里很可能还会使用到如今很火爆的javascript模块技术例如requirejs或者seajs技术,单页面听起来实在太完美了,但是我们冷静下来思考下,单页面真的完美吗?下面我要为单页面泼泼凉水了,具体如下:
泼凉水一:单页面其实指的是网站只有一个入口,但是并不代表用户看到的网页就是一个样子的,单页面里也会有很多页面切换,但是不管页面里的模样如何变化,浏览器地址栏的地址都不会变化,能做到这点就得归功ajax的超强能力了,单页面不同模样的展示都是在javascript代码里实现的,那么问题来了,单页面对于搜索引擎的网络爬虫就非常不友好了,因为网络爬虫是根据url抓取页面,抓取完毕后会忽略javascript代码,那么单页面的设计方案就会导致SEO优化只能作用于首页,而网站其他页面将无非有效的被SEO技术进行优化。
泼凉水二:一个网站做成单页面以后那么网站不同的展示都在一个url下面,但是如果有些用户只是对网站的某一部分功能很感兴趣,而这部分功能又不是被单页面的唯一同步请求所展示的首页里的内容,那么结果就是这些用户每次登陆网站时候都要手动操作一下才能进入自己想要的功能页面里,假如首页进入功能页面的操作步骤比较繁琐,那么这个必然会导致网站用户体验的下降。
那么上面的问题该如何来破呢?
这里我首先来讲讲第二个问题的解决方案,第二个泼凉水的问题的核心就是要记录单页面的状态问题,这个状态可以帮助首页能快速切换到具体的功能页面,要让客户端网页有状态最常用的手段就是cookie了,如果浏览器支持html5,那么保存状态的手段就更多,能力也更强了。但是这种手段是和客户端紧耦合的,那么如果碰到这种情况,该手段就会出现问题了,例如如果有个人发现单页面网站里一个很有趣的功能,这时候他正好和朋友QQ聊天,他告诉了他的朋友,他的朋友也该兴趣,让他把链接发过来,那么这个朋友就不得不在从首页在重复操作一遍,由此可见,cookie的手段并没有全面解决这个问题,那我们还有其他手段嘛?
答案是还真有,那就是使用html的锚链接,锚链接的形式如下所示:
http://www.baidu.com/#sharpxiajun
下面是我摘抄下百度百科对锚链接的解释:
锚链接实际上就是链接文本,又叫锚文本。可以理解为:带有文本的超链接,就叫锚链接。锚文本可以作为文本链接所在的页面的内容的评估。
一般的来讲,网站页面中增加的锚链接都和页面本身的内容有一定的必然联系。网站建设的行业网站上会增加一些同行网站的链接或者一些做网站建设的知名设计网站的链接;
另一方面,锚文本能作为对所指向页面的评估。锚文本能精确的描述所指向页面的内容,个人网站上增加Google的链接,锚文本为 “搜索引擎”。这样通过锚文本本身就能知道,Google是搜索引擎。
那么在单页面里的功能切换时候我们改变一下url上的锚文字,反过来说使用锚文字做路由器,让其可以路由到对应的功能页面那么上面的问题不就可以解决了。关于锚链接我这里要补充一些知识,首先锚链接的形式是url#文字,锚的起始标记是#号,这个#号的内容其实是属于浏览器端的,也就是说#包括#号后面的内容是不会被发送到服务端的,那么我们想改变锚链接只能在客户端进行,但是传统的锚链接的变化是很难被javascript语言监控到的,直到html5的出现才从根本上解决了这个问题,html5提供了hashchange事件,该事件可以监控锚链接的变化,因为javascript语言可以监控锚链接的变化,那么使用锚链接路由功能页面就成为了可能,那么低版本的浏览器该怎么办了?这个主要是ie的问题了,其实ie8包括ie8都支持hashchange事件,再低就不行了,不过jQuery有个插件可以让低版本的ie支持hashchange事件,有兴趣的童鞋可以百度下啊。
看来泼凉水二问题是有解的,那么泼凉水一怎么解决了?我的回答是基本无解,这个问题的关键在网络爬虫这边,如果我们被动解决这个问题,那只能是抛弃javascript了,这个玩笑就开大了,所以我们只好祈求各大搜索引擎能不能智能化再厉害点了。这里加个题外话,我最近几天突然意识到一个问题,那就是讲到web前端技术我一定要加强对SEO的思考,因为绝大多数网站都会把搜素引擎当做入口的生命线,这是一个很难回避的问题,不管我们网站做的如何优秀,假如用户很难找到它,那一切都将会是百搭,而在前端设计里要加入SEO的思考,这必然会导致整个架构的重大变化。这个问题我会在以网站静态化角度审视前后端分离方案时候重点讲下。
前端MVC讨论到这里我们会发现我们的谈论里缺了一环那就是MVC的M层模型层,web前端要侵入到模型层了,这不就等于web前端要造反了,它不仅仅想改变从属服务端的悲惨命运,还要抢夺服务端的部分功能,让服务端成为浏览器对应的存储系统,这不是无异于虎口夺食,在时下服务端如此强势的大环境下,这种想法简直就是活得不耐烦了,哈哈,当然这是戏言了,做技术做工程还是要讲求个合理性和逻辑性的,技术和工程都是实在的东西很讲道理的,只要道理站得住脚怎么个做法都是其次,回到问题本身,我个人觉得在PC端讨论web前端做模型层其实往往利大于弊,就安全而言,模型层意味有大量业务逻辑推移到web前端,那么安全的保障难度会加大,就技术难度而言,web前端做模型层会让javascript编程巨复杂,所以要做这个抉择时候一定要结合业务做仔细的权衡,其实我现在接触的一些说包含模型层能力的前端框架在实际运用里模型层的功能还是使用太少,不过这个问题如果放到移动端,或者是PC和移动端融合可能就会有些不同,这个问题我将在本系列的终篇里再谈谈,这里也不累述了。
说到这里需要总结下了,前端的MVC不应该等于单页面开发,前端MVC也不是把ajajx用到极致,根据实际业务场景,我们需要适当的把同步请求和异步请求结合起来。如果前端MVC里包含了更多同步请求,那么对于MVC里的C层即控制层就会有更高的要求。前后端分离主题还有个下篇,下篇里我还会提到一种前后端分离方案那就是nodejs的运用,而nodejs的运用就是和控制层有密切的关系,上篇里我提到nodejs是前后端分离方案的催化剂,其实我个人认为nodejs参入的前后端分离方案才是更加完美些的前后端分离方案,这个完美的评价原因之一就是从前端承担控制层作用角度思考的,所以前端控制层这个内容我将放在下篇讨论。
好了,本篇写完了,从本篇我们可以看到前端MVC的历史很早,它的出现早于nodejs,这就说明前端MVC其实并不是什么新技术,只不过是现在才被大家重视起来,完善它的人也越来越多。从本篇我们还发现前端MVC其实并不完美,问题很多,最致命的就是对网络爬虫的不友好,所以我们需要考虑到SEO技术参入其中的前后端分离方案。
最后,祝大家晚安了。
为什么我当时会认为javascript作为服务端语言的前景堪忧呢?我当时有如下的思考,这些思考放到时下nodejs已经非常火爆的背景下,我相信对很多朋友任然有参考意义,下面是我当时的思考,具体如下:
质疑nodejs思考一:2010年之前我还不是敢自称自己是一名专业web前端的工程师,因此对于javascript的认识和掌握程度也不能和现在相比,但是对于javascript的难学,难深入却是有着切肤之痛,因此我想javascript作为服务端语言就是让会其他服务端语言的工程师更加深入的学习常被服务端工程师诟病的javascript,这么做的结果无异于逼迫服务端工程师转向成web前端工程师嘛,这个想想就让人觉得不现实。
质疑nodejs思考二:我对web应用开发的技术选型认识比较肤浅。技术的选型是个很宽泛的问题,回到我对nodejs的质疑思考主要是体现在web应用服务端语言选择上,在中国用作web服务端开发的语言非常多,但是主流的无非就是java、php、C#以及C语言系列,当然web服务端技术发展到现在Python、ruby也是有一定市场,作为一名具体干活的软件工程师对于项目选择何种技术是没啥发言权的,因此我常常觉得技术选型就是项目经理或者是技术经理以及架构师的问题,而大多时候我们去询问为什么用这个服务端语言得到的答案都是非技术性的回答,例如:公司主要是使用php啊,java比较流行人好找啊,C#开发快啊能很快的完成工作,很少有人会这么告诉你我们的项目是个什么样的项目,这个项目使用A语言比使用其他的B语言、X语言有何种好处和优势,其实中国很多软件企业做项目在技术选型这块都很粗,说的难听点其实就是很多能控制项目的人技术水平很难被恭维,当然大部分项目其实使用什么技术实现并不是太重要的问题,但是这个到了技术架构异常复杂的大型网站技术选型问题就显得尤为重要,这个认识主要是来自于我阅读《淘宝技术这十年》所感受到的,淘宝网站的技术选型随着业务的发展变化的如此之大,颠覆性如此之高,这个在我待过的很多项目组都是难以令人想象的。
Web应用发展这么多年,那些占据了天时、地利和人和的现有技术基本都是处于一个垄断的地位,新的同类型语言想突破重围必然有着自己独有的技术优势,这就好比在中国做互联网如果有家新型互联网公司可以突破BAT的围追堵截,那么这家公司一定是有着自己得天独厚的优势,所以nodejs一定是获得一种得天独厚的优势,那么nodejs优势在哪里了?不过在讲述nodejs的优势之前我们先来讲讲上篇文章里遗留下来的问题。
其实上篇里我讲到前端MVC,文章里只是着重讲到了V层即视图层和M层即模型层的问题,而唯独没有专门讲解C层即控制层的问题。在前后端分离文章第一篇里,我谈到如果把MVC框架里的C层以作为连接web前端和web服务端的角度来理解,C层主要承担了三个方面的工作,它们分别是:路由、报文格式转化和页面渲染的工作。前端MVC在处理报文格式转化和页面渲染这两个方面还是比较容易做到,但是在做路由这块存在一定问题,前端MVC框架对于获取服务端数据这块以及异步请求处理方面其实和传统MVC框架的处理的手段本质上是类似,只是实现载体有所不同而已,但是控制层还有一个路由功能,其中用于页面切换的路由存在一定的问题,不过这个切换也要限定一下范围,页面通过ajax技术让页面部分刷新,假如这种部分刷新让页面展示效果发生很大变化,对于用户而言也是页面发生了切换,但是这种切换是不会让地址栏的url产生任何改变,这就是问题的根本所在了,我在上篇里已经讨论过这些问题,通过这些问题我们发现如果页面转化时候地址栏的地址随之也发生改变是会给用户体验、网站的友好度以及SEO优化带来好处的,如是乎我提供了一种手段,那就是使用锚链接来帮助我们实现url的变化,因为锚链接只是作用于浏览器,因此这种手段是对前端MVC的C层实现页面路由功能的一种很好的支持,但是因为这种方式需要在javascript里完成,那么对于SEO优化就产生了问题,最后我提出了页面切换我们最好使用同步请求的方式。
这个时候问题来了,如果要使用同步请求,那么这个同步操作自然是要让服务端来控制,这么做的结果就是让服务端再去回收部分控制层的功能,这样下来一个使用前端MVC架构的网站就有点不太纯粹了,具体点就不是一个单页面的网站,这里我们的讨论又回归到了单页面的问题了,前文讲前端MVC框架很多热心网友对我的论述发表了有价值的评论,但是我发现我的想法有些朋友可能没有真正理解(这也许是我的表述的问题吧),我前端MVC讲述的一个思路是以批判前端MVC的角度进行的,我早些时候和一些网友探讨过前端MVC的设计问题,有些朋友在没有做具体web前端MVC架构前总是想实现纯粹的前端MVC框架,延着这些朋友的思路我们就会把所有的C层和M层的东西都移到web前端,我常想如果真的这么实现了,结果自然就是单页面网站了,或者就是在前端引入了复杂的模型层设计,不过探讨毕竟是探讨真的实现时候很多朋友就会知道了难度所在了,所以说理想和现实是有差距的,这话又一次灵验了。
这种理想和现实的差距,其实就告诉我们一定有个地方出问题了,那么问题在哪里了?下面我将我对这个问题的思考,总结如下:
问题思考一:让前端承担大部分MVC的工作,那么前端本身的技术能力是否能达到所有的要求吗?这个回答似乎是肯定的,例如单页面的出现就代表了这种可能性,javascript也是拥有强大的面向对象的编程能力,因此再写复杂的业务模型层也是没问题,但是前端这么做了以后其实并不能满足所有人的需求,例如:SEO的要求,SEO很多技术都是以同步网站请求技术为根基,这个和前端MVC框架以ajax技术为根基产生了冲突,这就让前端技术产生了局限性。使用javascript面向对象技术来实现业务模型,这个也是有问题,javascript的面向对象的学习成本和精通难度超出了传统的面向对象的语言例如像java这样的语言,而且javascript要设计和写出更加容易维护的代码是非常不容易的,这么做不符合我在存储系列里讲到的要用最简单的方式实现的原则,这其实也是说明前端技术能力不足的问题。
问题思考二:其实不管什么形式的前后端分离方案它最根本的思想就是让前后端进行解耦,让不同技术语言体系下的人能做到工作的隔离,最后协同起来各自发挥出自己的最大价值,但是如果我们只是按前端,后端的角度来做分离,是不是有点粒度过粗,考虑是不是过于片面了?特别是这个片面的问题,web应用的问题并不是一个纯技术问题,而是一个技术和业务结合的问题,因此任何应用于生产的技术方案都会受到业务的影响,例如上面当我们要考虑SEO的问题,考虑开发难度的问题,那么纯粹的前端MVC的框架就会显示出自己的局限性。前端技术无法改变浏览器地址栏的url,这个从很多角度思考是个合理的设计,但是到了前端MVC里对C层的设计而言则变成了一种技术手段的局限性了。因为这种局限性就让我们不得不回到问题的原点状态,例如页面的同步请求,而同步请求最合理的控制地点就是服务端了。
问题思考三:本思考是一个延生性的思考,我从事这么多年的web开发,我其实一直困惑于web应用开发和MVC的关系,为什么我们做web应用开发时候都要那么强调和重视MVC设计思想呢?难道web应用开发的世界没有MVC就不能活了吗?回首下web应用发展的历史,在web应用开发的忙慌年代,的确是看不到MVC的影子,那个时代的确很自由,自由到许多web应用混乱不堪,质量和健壮性差的不能再差了,这个时候一个英雄出现了那就是MVC,MVC代表了一种次序,一种基本的法则,这就好比人类社会建立的根本原则一样,这些原则让人类和野兽有了区别,人类也因为这些原则而成为万物之灵长,相比之下MVC就是web开发世界里的游戏规则和行为准则,因此只有当我们从MVC角度思考web应用的建设,才会让web应用更加的优秀,这也就是在讲述前后端分离技术时候我都是以MVC思想作为准则进行思考的。思考回到具体的场景,MVC思想的运用就是让我们把web应用开发里可以归为一类的场景聚集在一个范畴之下,不同范畴使用一种双发都可以接受的统一准则进行沟通,这么一来我们就把需要解决的问题简单化了,各个独立的范畴因为减少许多不必要的干扰,因此能让它们发挥出更大的潜力,更重要的MVC还让web应用伸缩性,健壮性,可维护性大大增强。例如在很多传统web应用开发里在控制层这块前后端的矛盾就是属于MVC规则使用不完善所致。
单页面的应用存在很多问题,因此需要同步请求的介入,这就导致了服务端再度回收了失去的控制层的功能,这么做也无可厚非,但是我很担心这个改进的引入会不会导致传统MVC框架里控制层的混乱问题,根据我的经验,这种混乱的程度已经降低了很多很多,基本我们可以忽视原来C层的问题了。
不过很多有追求的web前端工程师对于这种不纯粹的前端MVC的异议还是很多的,大部分异议还是源自浏览器能力的局限性,当服务端很多方面被弱化后,也许可以解决我们以前在前端被服务端束缚的很多问题,但是同时又产生了新的问题,这些新问题我总结如下:
新问题一:在传统的网站动静划分里,我们常把浏览器端的技术html、css和javascript归结于静态技术的范畴,如果网站使用Web前端MVC那么前端就会接过很多动态网站的功能,这个时候传统的静态技术就被人为的演变为动态技术。回顾网站的发展历程,基本是从静态到动态的转变,这个结论用在时下其实已经有点不太对了,随着网站越来越庞大越来越复杂,网站技术发展逐渐开始逆向进行了,网站从动态化向静态化转变的需求变得越来越强烈了,这也是时下前沿的前端技术正在解决的问题,例如本系列的主题网站静态化技术就是顺应这个发展趋势而来的,所以前端MVC框架在这点上有点逆历史潮流的问题了。
新问题二:前端MVC让web前端的技术难度和架构难度成指数级上升,而javascript语言天生有着自己设计的缺陷,这个缺陷在写大规模复杂应用时候就显得尤为突出,例如:javascript没有模块化管理,javascript面向对象的实现难度,所以前端MVC的应用可能会变相的提升企业的技术成本和开发成本,当然很多新的技术手段能解决javascript固有的缺陷,对这些新技术有个更大的问题就是“你会吗“,不会的话首先要解决会的问题,这也是个成本问题。
新问题三:当前端真的越来越独立于服务端后,这会导致服务端一些可以优化web前端的重要技术就很难实现了,例如网站静态化系列里讲到了缓存运用,CDN的运用就很难达到预期效果,或者根本没法使用,因为这些技术的根基都是认为网站动态性是由服务端发生的,而客户端霸占了动态性,那么这些技术的作用就被限制住了。
由此我可以下个结论:如果前后端分离方案是以浏览器和服务器角度来划分并不是最好的前端分离方案,那么前后端分离方案还有没有新的解决思路了?这个真的有,那就是nodejs参入的前后端分离方案。
其实前后端分离的驱动永远都是前端强于服务端,而前后端分离的重要目的也是要给web前端创造一个更加干净的开发环境,那么写的代码是否是在浏览器上跑还是在服务端上跑这个并不是太重要,所以引入nodejs,就是让服务端也能跑javascript代码并不会是让人无法接受的事情,回到前后端分离方案里以服务端驱动的前后端分离方案,我曾说过这个方案能获得服务端开发人员更多的掌声,我相信这个掌声不会是服务端为前端的喝彩,而是服务端终于从web前端解脱出来了,这样服务端运用更加高级的SOA技术就成为了可能,那么我们把web前端的控制层使用nodejs替代,这么一来我们既可以继承所有传统MVC框架的优点,同时也达到以前后端分离的根本的问题就是为web前端创造一个很干净的开发环境问题,那么我们在前端MVC框架使用时候遇到的问题都会很好的被解决。
Nodejs的运用让动态网站的动态性再度停留在了服务端,那么我前面讲的那么多网站静态化技术就可以和前后端分离方案很好的融合了,因此本篇先不具体讨论nodejs做前后端分离的实现手段了,在下篇讲从网站静态化角度重新审视前后端分离方案时候一起讲解,这么做会更加符合本系列的主题。
现在我们可以解答为什么nodejs技术可以突破传统服务端技术的包围,因为nodejs可以让前后端达到更高程度的分离,从而让前后端各自发挥自己的优势,很有意思的是,虽然nodejs技术属于服务端范畴,但是它却是前端工程师驱动来普及的,这绝对是web前端逆袭啊。
好了,本文就讲到这里,最后祝大家工作和生活都愉快。
网站静态化技术相对于前后端分离技术的关注度要低的多,如果业界的一些公司因为看了本人的文章能对网站静态化技术有一种新的认识,从而考虑在自己网站上使用网站静态化技术,同时也想实现前后端分离技术,那么新的问题出现了,这两种技术同时使用会发生矛盾吗?如果有矛盾,我们到底将如何解决这些矛盾?解决这些矛盾的时候我们是不是可以做好两者的兼顾,而不会发生其中一方妥协于另一方,最终导致其中一方没有充分的发挥自己的能力。要解答上面的一系列问题,我首先要探求的就是网站静态化技术和前后端分离方案里那些方面会产生矛盾。
从我前面对网站静态化技术的阐述,我们知道网站静态化的技术最佳作用位置应该是服务端而非是浏览器端,之所以会这样是因为网站静态化技术的技术基础是动静分离和缓存,这两个方面如果落到浏览器端会碰到很多难以解决的问题,那么我们要分析下这些难以解决的问题,具体如下:
浏览器之缓存问题:浏览器也有缓存,不过浏览器端的缓存那就不是指内存里的缓存,而是持久化的缓存,实际上浏览器端的缓存非常不可靠,会被很多非技术的因素所限制,例如我们手动删除缓存或者使用无痕模式上网,那么这些持久化的缓存就会失效,用户再度访问网站时候都将是第一次访问这个网站,这就使得很多优秀的缓存策略方案在浏览器端实施效果大打折扣。
浏览器之动静分离问题:网站静态化技术里一个重要的手段就是如何设计动静分离策略,纯粹的静态内容这个没啥好说的,但是动态的内容在一定的条件(例如:时间,一些业务属性例如商户属性)下是可以转化为静态内容,这些内容如果能被有效缓存,对网站性能提升是不可估量的,而且这种动静转化的策略也可以减少业务服务器上处理不必要的请求,从而减轻业务服务器的压力,达到提升后台核心业务服务端的负载压力。但是如果我们使用前端MVC框架,一股脑子把很多服务端功能往前端迁移,那么这种动静处理手段就很难做,而且很多场景基本上是无法应用了。
因此我认为前后端分离方案使用nodejs价值更高,因为使用nodejs我们就可以根据网站静态化技术将需要保留在服务端的功能可以继续保留在服务端,这样就能达到二者兼顾的目的。但是如果我们认为把nodejs引入后,nodejs的目的就是用来做网站整体MVC架构下的C层即控制层,这个思路到底合理不合理呢?这个问题还是很值得玩味的,因此我们需要分析下网站整体MVC架构下的C层即控制层的作用。
在前面文章里我曾总结过C层即控制层在MVC框架里的作用,这个作用分别是:路由、报文格式转化以及页面渲染,但是这个作用的总结我是有个前提条件的,那就是以C层即控制层作为前后端沟通介质的前提下。如果前后端分离方案引入后把控制层归为前端的组成部分,那么控制层跟前端的结合问题都是人民内部的矛盾,都是比较好解决,但是控制层就仅仅是用来连接前后端一个作用吗?对于网站架构里的控制层,有一个不可避免的功能那就是作为后端服务端的安全入口的作用,也就是说控制层是做请求安全检查和安全监控的地方,而且很多安全校验还会和业务相关,例如检查报文是否被篡改啊,防钓鱼的功能,如果这些功能被前端来承担,首先不谈前端技术人员会不会做这些,但是至少一点问题是会发生的,前端工程师在关心页面开始同时还要写服务端的业务逻辑了,不管怎么说,这些功能迁移到前端总不是太合适。当网站演变为超大型网站后,大型网站往往是很多小中型网站项目的集合体,为了减少网站整体的异构性,我们常常把不同的模块网站的入口整合在一个大型控制层项目下面,这个大型控制层项目一般称为网关项目,它的作用和网络里的网关非常相似。除此之外,还有些网站的控制层非常特别,例如一些做第三方支付的网站,那么这样网站项目本身就是个大网关,而且这个网关很特别,它后台的服务就是其他银行的系统,它的路由工作就会变得异常复杂,例如:根据用户使用银行的不同,控制层要组装不同的报文信息,而这些功能都是属于控制层,这样的场景无疑大幅度提升了控制层再和模型层对接的技术难度,而增加的难度问题又和模型层耦合度很高,由此可见,web应用整体的MVC的控制层比我们想象中要复杂的多。
回到用nodejs替代控制层这个主题,我们来看看实际的场景吧,假如我们的网站控制层相对比较简单,好了,这时候我们跟领导或老板说“现在很流行前后端分离,我们项目也使用下前后端分离技术”,领导或老板一听可能会为之一振,那么就会问你”那么该怎么做了”,你这时对他说“首先把控制层用nodejs重写下”,领导或老板听到这个回答他会同意你这么干嘛?一个不会给网站增加任何新功能,同时不能很直接有效的提升网站的性能,而且执行它还会有很大风险的方案,头儿们会同意吗?好了,假如你终于找到合理理由说服头儿们,那么如果我们的网站规模已经很大,控制层已经演变成了网关项目,控制层本身已经巨复杂了,你敢用nodejs重写一遍网关项目吗?所以说吧nodejs直接当做控制层,其实实践起来困难重重,而且nodejs完全承担控制层,它的性能,它能否很好的运用于集群开发这都是很难把控的问题。分析到这里,我们似乎又进入了死胡同了,那如何来破这个局呢?
上面的问题只是反映出整个网站MVC里的控制层其实还有部分功能是和服务端的模型层紧耦合的,因此要解决这个问题就是把传统的控制层再细分一下,属于前端的部分划分给web前端作为web前端的控制层,属于服务端的部分任然留给服务端,这么拆分后,当我们引入了以nodejs为基础的前后端分离方案,服务端的控制层改造无非就是去掉页面路由,页面渲染,再修改下返回数据格式即可,因为不用修改服务端的业务代码,其代价是很低的,头儿们也很容易接受这样的方案,并支持我们大胆去尝试新技术。
服务端网站静态化技术SSI和ESI,主要是根据动静分离策略把网页不会经常变化的模板进行缓存,然后在静态资源服务器位置整合动静资源,如果我们使用nodejs只是简单替换原来的控制层,那么这些策略其实还是有问题的,那么怎样做可以让nodejs兼容SSI和ESI了?这里我列举个实际的案例,nodejs有一个模板语言叫做jade,nodejs里还有个技术叫做handlebarsjs,其中handlebarsjs和struts的标签类似,它可以处理一些简单的业务逻辑,我们开发时候使用jade编写页面的模板,使用handlebarsjs让动态数据和模板进行整合,项目发布时候,使用像grunt这样的项目管理工具编译项目,jade文件变成html文件,而handlebarsjs则会转化为javascript代码,这样我们就可以把生成的html文件在服务端进行有效缓存,而handlebars生成的javascript文件负责整合动静数据,这样nodejs就可以达到兼容SSI和ESI的作用了。
不过引入nodejs会让网站处理请求的过程里增加一个环节,这样可能会导致部分性能的损失,但是我上面的实例却能有另外的方式规避这个问题,因为nodejs的代码是用javascript语言编写的,那么这个代码是可以运行在浏览器上的,那么这就会产生了一个处理手法,那就是我们在生产部署时候其实不需要部署nodejs的,我们把静态模板就缓存在服务端或者推送到CDN上,然后handlebarsjs生成的js代码就让它传送到浏览器端,因为这个js代码生成后基本不会变化,浏览器可以缓存它,当然CDN或静态资源服务器也可以缓存它,其实它在浏览器运行时候变化无非就是获取一次服务端数据而已。这么一来,生产上的web前端又转变成了前端MVC的形式,还把动静整合的事情交由了浏览器来完成,这不仅是兼顾的网站静态化要求,还让动静整合推到了更加靠前的浏览器端,这不是达到了一个双赢的效果了嘛。
好了,本篇就写到这里,最后祝大家晚安,生活愉快
其实不管是正向代理还是反向代理,这两个概念的定义都是以浏览器侧为基准进行的,正向代理是代理浏览器来访问互联网,反向代理是指代理不再是代理浏览器侧了,而是反过来代理浏览器需要访问的应用服务器。那为什么我们要使用正向代理服务器了?答案当然不是为了FQ了,下面我来列举些实例来说明这个问题了。
例如公司里使用代理服务器主要是为了安全的考虑,很多公司内部都有自己的局域网,一般我们称之为内网,内网里有公司的各种资源,如果公司员工的电脑随意连接到互联网,假如碰到那些别有用心的黑客,通过攻击员工的工作电脑截取了公司重要的文件资料,那样就会造成公司的重大损失,正向代理除了能防范外部的黑客攻击外还能监控和控制公司内部员工将公司重要文件通过互联网传递给不恰当的人,因此公司让员工使用代理上网基本都是出于安全的角度来考虑的。
正向代理的合理使用还能帮助一些企业提升自己产品的核心竞争力,例如在移动端有一款非常流行的浏览器,它之所以非常受用户的欢迎,是因为使用该浏览器上网速度比其他浏览器明显的快多了,那么这款浏览器是如何做到这点的呢?奥秘就是这家公司为自己的浏览器对应建立一个十分强大的代理服务器集群,用户使用该浏览器访问网站时候用户首先访问的是该公司的代理服务器,而这些代理服务器使用缓存技术缓存了海量的网站信息,再加上使用一些web加速的技术例如CDN技术,这就让该浏览器访问网站的效率明显快于其他浏览器。
反向代理和正向代理从技术角度上基本上是一致的,区别主要是代理的内容不一样了,反向代理代理的是应用服务器。反向代理技术也基本上是互联网公司的一个标配技术,但是反向代理能否正确使用,能否更进一步的发挥它的实用价值,我觉得并不是所有公司都能做好的,下面我来总结一下反向代理的使用目的吧,具体如下:
使用目的一:反向代理可以隐藏真实的应用服务器。该目的属于安全的范畴,反向代理隐藏真实的应用服务器,那么就可以让别有用心的黑客很难掌握正确的应用服务器,从而增加黑客的攻击难度。
使用目的二:反向代理可以实现负载均衡的功能,例如在java的web开发里有一种很简单的实现集群的手段,这个手段就是使用apache加上tomcat的组合,用户请求先到达前置的apache服务器,apache再使用负载均衡策略将请求分配给后台不同的tomcat服务器上。
使用目的三:反向代理可以起到动态调节应用服务器并发数的目的,一般用作反向代理的服务器都是静态资源服务器,这样的服务器在并发处理能力上要远强于后台的web应用服务器,那么可以通过控制web应用服务器前置的反向代理服务器,这样就可以动态调节后台服务的负载的大小,这个做法的好处可能很多朋友都不太了解,这里我列举个例子,一个网站最需要稳定性的部分是哪个部分呢?很多朋友会说是数据库,的确数据库是最重要的,因为数据库做分布式很难,很容易形成单点故障,要是数据库挂了基本一切都没法玩了,那么除了数据库之外还有别的吗?当然有,那就是用于处理业务的应用服务器了,应用服务器如果做了集群,集群中其中一台服务器挂了其影响面会比数据库挂掉低多了,但是一个网站的做业务处理的应用服务器挂掉,对公司的损失还是很大的,而web应用服务器前面的用作反向代理的静态资源服务器挂掉问题就会小多了,至少不会产生公司业务无法正常完成的事情了,因此当网站负载过高,让过载的请求被反向代理拦截或者阻止,这对应用服务器的稳定性提升有莫大的好处。当然反向代理调节应用服务器的负载水平的用途不仅仅这些,有兴趣的朋友可以在网络上找找相关的介绍。
使用目的四:反向代理可以缓存静态数据,一般用作反向代理的服务器都是使用像apache或者是ngnix这样的静态资源服务器,因此我们可以把web应用里的静态资源缓存在反向代理服务器上,从而达到提升请求处理的速度问题。反向代理的这个功能就和本系列的主题网站静态化处理切合了。
分析完反向代理的使用目的后,我们现在将反向代理应用到项目里,这里应用的一个前置限定就是将反向代理应用到网站静态化的处理之上,首先是第一个应用方式,如下图所示:
第一种反向代理应用方式就是让反向代理和应用服务器一一对应,也就是每台应用服务的部署服务器上都对应部署一台反向代理服务器,这么做有怎样的好处呢?首先我们来讲第一个好处,如果我们将网页做了动静分离,那么反向代理服务器就可以负责对请求中的静态资源访问进行处理,同时反向代理还可以承担动静资源整合的目的。这里要特别说明下,前文里我说道动静资源会因为我们使用的动静策略而发生转化,那么有些动态内容在一定条件被转化为静态资源后,我们可以将这些做了转化的静态资源在服务器上缓存起来,这个时候上图展示的架构模型就会发生变化,如下图所示:
我们看到反向代理服务器和应用服务器之间会形成一个cache层,反向代理访问cache层的效率会比直接访问应用服务器要高的多,这等于是给应用服务器做了一个加速操作,同时通过缓存我们可以减少应用服务器的运算压力,从而达到提升应用服务器性能的目的。以前有朋友问我这么做会不会增加应用服务器的压力,因为一台服务器上部署了两台可以处理web请求的服务器,那么它们之间一定会有发生冲突的时候,不过我想产生冲突肯定是我们没有很好的处理二者关系所致,所以我们要理清在同一台服务器上部署反向代理和应用服务器后,它们之间的关系到底是怎样的?
其实反向代理和应用服务器从物理形态角度上它们是两个不同的东西,但是二者在逻辑上其实是一个整体,它们共同完成一个逻辑性的应用服务器的功能,只不过二者因为应用场景不同而形成了一种分工合作的关系,反向代理服务器主要完成对静态资源请求的处理,而应用服务器则是负责业务逻辑的处理,它们最终形成一个强大的合力使得整体的逻辑性应用服务器的性能得到显著的提升。
除此之外,这个反向代理还可以发挥动态调节应用服务器的并发数的目的,但是上面的技术方案却没有发挥反向代理的负载均衡以及安全性这两个方面的作用。为了让反向代理四个使用目的得到充分的发挥,那么我们该如何来做了?
方法很简单就是把反向代理的部署地点从应用服务器所在的物理服务器上迁移出来,放到一台独立的物理服务器上,但是这个做法会有性能上的损失,同时还会增加整个技术架构的复杂性。为什么性能会损失呢?因为原来的反向代理服务器和应用服务器部署在同一个物理服务器上,那么它们之间的通讯都是以内存共享的方式进行的,这样的通讯效率是非常高,现在换成了通过网络通讯进行沟通,而网络通讯是IO设备里效率最差,可靠性最差的,因此单独部署反向代理服务器或多或少都会造成一定性能的损失。
为什么说单独部署反向代理会增加整个网站技术架构的复杂性了?我们把反向代理服务器单独部署,那么单独部署时候我们还会是使用一一对应的策略吗?先不谈这么做,从技术和业务角度的好处和坏处,但从成本这个考虑就是会让很多公司望而却步,因为这个做法就会导致用于部署应用服务器的成本翻倍的增加,而增加的服务器用于反向代理,这样的做法怎么体会都不是觉得物有所值,再说用于反向代理的静态资源服务器本身处理请求的并发能力是普通应用服务器的数百倍,一一对应本身也没有完全发挥反向代理服务器的潜力,因此最好的解决方法就是把反向代理服务器做成一个反向代理服务器集群,做成集群问题又来,集群里每台反向代理缓存的数据是不是要保持一个同步了?这就好比处理应用服务器的session同步问题,如果真的这么做会不会导致反向代理服务器上缓存大量使用率不高的数据从而导致缓存的利用率很差,同时同步操作本身也会影响到反向代理集群的性能,所以要设计一个好的反向代理集群是一件十分复杂的事情,其实合理的反向代理集群的做法就是在集群里在进行分组,每个分组应该是和后端的SOA服务相匹配,这个时候反向代理集群的效率才能得到最大的发挥,同时资源利用率也会更加的合理。其实使用反向代理集群方式,也会给生产部署造成麻烦,如果网站进行了静态化处理,那么反向代理需要承担对静态资源的处理操作,这个时候反向代理和对应的应用服务器结合起来才能形成一个完整的应用服务器,但是现在我们将一个完整的逻辑应用服务器分开部署了,那么当我们发布新应用的时候就得面临更加复杂的情况,这就增加了部署和运维的风险和难度。
我如此批评单独部署反向代理的问题,但是我并不是说这种做法完全不可取,而是想告诉大家这种做法其实是一种高级的做法,但是也是一个复杂的做法,要做好这个集群是很麻烦的一件事情的,我觉得只有当我们的网站业务量和请求量很大的时候,同时原有方案出现了瓶颈时候可以认真考虑反向代理集群方案的实现,不过将反向代理形成集群会给网站的安全性带来莫大的好处,反向代理可以隐藏后台的应用服务器,这种隐藏就是客户端只需要访问代理服务器即可,应用服务器对外都是以反向代理来展示的,但是如果反向代理和应用服务器一一对应,那么恶意黑客找准了某台反向代理服务器后,对这个反向代理服务器进行反复的攻击,那么这个攻击也就等于攻击与之对应的应用服务器,这就导致反向代理隐藏真实应用服务器的作用就没有得到有效的发挥,而集群这块就可以很好的处理这个问题,不过我们如果觉得使用集群代价太高,我们也有变通的方法,那就是在所有逻辑应用服务器前面再放置一个反向代理服务器,这个反向代理服务器不再承担缓存的功能,而只是用来做负载均衡和安全处理,这样一一对应的策略安全性也可以得到保证,不过如果公司技术能力好可以考虑使用LVS这种软件化的负载均衡技术方案,假如公司还很有钱还可以考虑使用更加高级的硬件负载均衡设备例如F5设备。
如果我们网站除了使用网站静态化技术还使用了前后端分离技术,当然这个前后端分离技术应该是使用nodejs的前后端分离技术,那么nodejs应该放置在生产部署的什么位置上了?上篇文章里我曾列举了一个nodejs的应用实践场景,在这个实践场景里我曾经提到如果在原有的网站生产架构下引入nodejs会增加一个请求处理环节,而nodejs使用主要是为了满足前后端分离而非增加网站性能,因此增加的环节可能会让请求处理的性能下降,因此我最后提出一种变通手法,就是nodejs项目发布时候编译源代码,然后将编译出的javascript和html文件干脆推移到浏览器端处理,这样就变相形成了前端MVC框架,这个做法总是有点不伦不类的意味,假如我们真的想把nodejs引入到应用生产的网络架构里,我们不希望无端的增加请求处理环节,那么最好是让nodejs服务器替换某个部分。按照这个思路思考,那么我觉得nodejs在生产的引入最好是和反向代理相关,最简单的方式就是让nodejs和反向代理一一对应,这样就可以很好的降低引入nodejs带来的问题,当然复杂点的就是反向代理集群对应的应用服务器应该是nodejs的应用服务器,而不是用来做业务处理的业务级别的应用服务器。
不管怎么说,我认为在网站静态化方案里我们一定要考虑反向代理的运用,如果静态化技术方案里没有反向代理的身影,那么这个网站静态化处理可能很难达到我们预期的效果。
好了,今天就写到这里,最后祝大家晚安。
好了,我们现在回到web前端优化这个主题吧。Web前端优化技术的普及还是要归功于互联网两大巨头雅虎和谷歌的贡献,他们通过多年的积累和总结,将这些web前端优化的经验无偿的公布给全世界,从而推动了web前端的发展,这些技术都不是什么秘密,我在网上找到一篇讲解这些技巧的文章,文章就是《Web前端优化最佳实践及工具集锦》。
web前端优化技术和网站静态化技术使用目的是一致的,就是让网站变得更快,用户体验更好,我个人认为网站静态化技术其实就是web前端优化的一部分,只不过网站静态化技术是通过服务端的大规模技术改造来实现web前端技术优化,而服务端的这种改造的目的就是让整个网站的后台技术架构更加切合web前端的要求,从而能更好的实现web前端优化。我这里之所以能如此评价网站静态化技术,其实说明网站静态化技术和web前端优化技术一定存在某种强烈的切合点,我个人认为这个切合点就是它们背后使用的理论基点是一致的。那么它们之间这个切合的理论基点到底是什么呢?
优秀的网站应该是用户体验好的网站,当人们使用这个网站感觉爽,好评不断,那么这个网站就是一个用户体验优秀的网站,但是用户体验好的网站就是网站布局精美,图片很炫,人性化设计到位这么简单吗?这些要素都是网站使用者的感受,但是对于网站设计和开发人员而言,再好的网站一定要解决一个根本问题,那就是网站加载的速度要快,如果网站加载速度不快,你就算把网站设计的再漂亮,估计也会搞的无人问津,说到这里,是不是有较真的朋友不信我的结论呢?我把前面引用文章里的一张图再给大家瞧瞧,如下图所示:
其实当我们开发网站如果只考虑如何把网站做的漂亮而忽视网站的性能,我们就会发现漂亮的网站和网站的性能其实是矛与盾的关系,例如精美的图片往往需要高质量的图片格式,而高质量的图片格式就意味图片会变得很大,那么在图片通过网络加载时候就需要花费更多的时间,所以我们在设计和开发优秀网站时候,漂亮和效率是需要我们认真权衡的,认真思考的,最终要找到一个最好的方式实现二者的平衡,同时更加充分的发挥双方的潜在价值。而直观的用户体验好这其实更多的是一个设计问题,而解决用户体验好的根基:速度问题,这就是一个技术问题了。
要解决网站的速度和效率问题,那么我们就得思考网站的载体计算机到底哪些因素会影响网站的速度和效率。其实计算机的本质很简单,那就是计算和存储,计算主要是CPU来完成,而计算机用于存储的介质就多了,它们主要是内存、硬盘,如果是网站应用还有个很关键的存储介质需要考虑那就是网络了。那么计算机用于计算和存储的这些介质的效率是怎样的一个情况呢?这个问题我在以前一篇文章里有过阐述,这篇文章就是《关于如何提高Web服务端并发效率的异步编程技术》
这篇文章的其他内容太多了,我把关键部分在本文摘抄一遍,内容如下:
对于一个网络请求的处理,是由两个不同类型的操作共同完成,这两个操作是CPU的计算操作和IO操作,如果我们以处理效率角度来评判这两个操作,CPU操作效率是光速的,而IO操作就不尽然了,计算机里的IO操作就是对存储数据介质的操作,计算机里有如下几个介质可以存储数据,它们分别是:CPU的一级缓存、二级缓存、内存、硬盘和网络,一级缓存存储和读取数据的能力接近光速,它比二级缓存快个5倍到6倍,但是不管是一级缓存还是二级缓存,它们存储数据量太少了,做不了什么大事情,下面就是内存了,以一级缓存的效率做参照,一级缓存比内存速度快100多倍,到了硬盘存储和读取数据效率就更慢了,一级缓存比硬盘要快1000多万倍,到了网络就慢的更不像话了,一级缓存比网络要快一亿多倍,可见一个请求处理的效率瓶颈都是由IO引起的。
由此可见网站的速度和效率问题似乎都是由存储即IO造成的。不过我们不能因为感觉发现问题根源在于存储,而就忽视对CPU的思考,所以我先讲讲CPU和网站性能的关系吧。CPU是计算机用于做计算的设备,现在的电脑能看电影,能听歌,可以和朋友聊天,还能用于工作,这些令人称奇的功能其实到了CPU这里也就是通过加减乘除这类基本的数学运算完成的,说到这个真是难以让人想象,读书时候学数学总是觉得那么枯燥乏味,没想到如此强大的人类神器居然就是通过数学运算得来的,难怪有国外科学家说宇宙都是通过数学运算得来的,这还是有道理的。不过网站背后的数学运算却有着自己的特点,虽然CPU计算能力很强,但是在实际场景下很多业务的计算其实很消耗时间的,如果网站某些请求响应背后的运算是需要消耗太多的时间,那么这个时候CPU也就会成为网站性能的瓶颈所在,网站应用有个重要的特点,这个特点有个专有名词描述那就是网站的实时性,根据网站实时性的特点,那么就要求我们网站每个请求所包含的计算都要简单和快捷,简单快捷的计算也就让每个请求背后所包含的业务性运算要更加简单,这也就是为什么很多人会说互联网的网站和企业的web应用相比,互联网的业务逻辑比较简单的道理,但是随着网站的规模扩大,业务模式越来越丰富,这个时候网站在某些业务环节不可避免的变得复杂,假如这些复杂的业务又需要实时的反应给用户,那么CPU不能快速完成业务计算就是网站的效率问题的根源了,例如我在存储系列里说到的海量数据的计算操作,就是这样的场景之一,那么这个时候我们该如何来做了?
碰到这个问题,我们首先要明确一个问题,计算出现了瓶颈,那么最直接的手段就是增加计算机的计算能力,比如使用运算更快的CPU,但是更快的CPU面对快速增长的业务而言,增加的效率是非常有限的,所以在CPU这块出现了多核技术,我们可以把一个计算任务拆分成诺干个子运算,这些子运算在不同CPU上计算,最终把结果汇总起来,但是这个手段和用更快的CPU手段一样,面对快速增长的业务很快就会达到性能瓶颈,最终我们发现我们的业务计算任务其实已经超出了单机计算机的能力,如是乎分布式技术出现了,我们这回不再是在CPU上做文章了,而是使用多台计算机联合计算,但是分布式计算系统是需要网络进行互联的,而网络是计算和存储里最大的短板,再加以现在互联网的所使用的计算资源规模达到了超乎想象的程度,我们发现想通过扩展计算机的计算能力来解决网站快速响应的问题基本是一件无法完成的任务,那么这个时候我们又该怎么办呢?
这个时候我们就要转化思路了,因为当网站的计算瓶颈问题已经到了这个地步了,我们再去更加深入挖掘计算机的计算能力这对最终的结果影响已经意义不大了,因此我们只能从计算的相关方哪里寻找问题的解决方案。那什么是相关方呢?仔细分析计算相关方的确太多了,但是有一个最根本的相关方就是用户的实际业务需求了,用户可能认为自己的业务需求都是很明确的,例如电商里的用户想查询自己的交易数据,但是这个业务问题转移到网站的开发人员和业务人员,面对这么多用户的交易查询那就是一个超级复杂的计算问题,如是网站的业务和开发人员就会根据自己系统本身的特点和问题,进一步思考用户业务计算问题的本质,谈论业务计算本质这个问题如果展开细化是非常复杂的,因为现实的业务场景实在是太多太复杂了,但是放到网站实时计算这个角度,其实有一个很简单的解决思路,我们回顾下我们前面讨论的计算瓶颈问题,其实这个问题的本质不是计算能否成功完成的问题,而是计算是否能及时完成的问题,如果用户的请求计算的确是没法很快完成,那么我们就不要让用户觉得这个计算是能很快的完成,这个做法也有一个专有名词那就是异步计算,但是如果我们把难以快速完成的计算都这么来处理,虽然让用户感觉网站已经很坦诚的告诉自己能力有限啊,但是苛刻的用户可不一定会买这个账,因此当有同类型网站使用新的技术手段解决了快速实时计算问题后,假如我们的网站还是驻步不前,那么后果就会很严重了,那么这个时候我们又该如何突破了?
那么我们就得进一步思考计算本身到底哪里出现了影响速度的问题,计算本身包含三个方面,首先是用于计算的计算资源,再就是做运算的工具即CPU,最后是计算的最终结果,如果业务计算慢的原因是因为数据量太大了,CPU很难快速完成,那么这个时候我们有一些手段可以解决这个问题,我们可以把海量数据做一个分类,例如存储系列里说的历史交易数据和当日交易数据的分类,当日数据因为数据量有限在一定条件下可以快速计算出来,面对历史数据,如果我们的计算结果最终是很简单的而且在一定时间范围里是不会变化的,那么我们可不可以这么考虑,让这些结果提前计算出来,然后将结果存储在效率更高的存储设备里例如内存,当用户请求操作这个业务计算时候我们只需要直接读取缓存里的计算结果就行了,这样就避免了计算,同时计算结果存储在效率高效的缓存里,用户获得响应的速度也会快多了,这个其实就是网站静态化技术里ESI技术背后的深意了。
当然当我们要解决网站性能问题,不太可能单独从计算或者存储一个维度来思考,一般都是把双方放在一起思考,按照我前面提到计算和存储介质的效率问题,我们发现存储其实是最容易影响网站效率的痛点,实际情况也是如此,当网站发生计算瓶颈问题之前,更多的效率问题还是由存储所导致的,而且复杂计算过程也是需要存储参入才能正常完成,例如计算过程里的中间结果当超出CPU缓存大小后我们就不得不将中间结果放到内存里,当内存也不够的时候我们就得放到硬盘里,所以解决计算效率问题也受到存储性能很大的影响。假如我们还是按照木桶理论来理解这个问题,我们发现不管是单纯的存储问题还是计算和存储混合的问题,最终的短板都是其中效率最差的哪一方,而计算和存储里效率最差的一方就是网络了,不过有些马虎的朋友可能说现在宽带好快了,我在网上下载一部几个G的电影也就几十秒,甚至有时比我硬盘拷贝还快,像你说网络是最大的短板其实不准确的,这位朋友的想法的确有他的道理,但是不是每个人使用的网络都是你那么快呢,而且现在移动互联网已经普了及,移动互联网速度比普通宽带就差多了,而且你在移动设备上使用网络流量越大,成本也就越高,如果你认为我说的这些问题都不算啥,网络还和地域的距离有关,你宽带很快,你想访问大洋彼岸美国的网站(这个网站在中国没有任何缓存处理),访问速度肯定还是快不起来,而且互联网的连通路径本身也很复杂,例如你感觉自己访问的是一个上海本土的网站,但是这个网站说不定好多重要服务器是放置在北京,这么复杂的网络环境,这么多不可控的因素还会影响网络的传输效率,网络谈何能说自己性能比硬盘强呢?
由此我们就可以发现谷歌和雅虎总结的web前端优化技巧以及我这里谈的网站静态化技术大部分都是围绕如何解决网络传输效率来进行了,因为它是整个木桶最大的短板,我们只有首先解决了这个短板,那么再去解决其他因素的效率问题,才能发挥其作用。这里的这个解释也可以解答前不久一个网友问我,为什么我讲网站优化很少讲解如何编写高效的代码,而都是从一些和代码无关的角度来阐述的了,其实你想通过代码优化提升网站性能,你首先要解决好对网站效率影响更大更关键的要素例如网络通讯问题,否则你代码优化的再好,对最终效果影响都是有限的。
看来本文今天写不完了,关于存储和web前端优化的内容我将在下一篇文章进一步讨论。最后祝大家晚安,生活和工作愉快。
网站的通讯技术是构建在http协议上,http协议底层通讯手段使用的是tcp/ip协议,但是tcp通讯协议在建立连接和断开连接这两个动作上是非常消耗通讯性能的,这主要是因为tcp/ip协议在连接建立时候的三次握手机制和断开连接时候的四次挥手机制所致,我们来看看下面的图形:
图中中间被红色标记的方块就是tcp/ip协议在建立连接时候需要发送三次报文才能确认连接是否建立成功,中间四个蓝色的方框就是说明tcp/ip协议在断开连接时候要发四次报文才能确定连接最终被断开,而一个具体的http请求和响应也就发送两次报文,这也就说明如果浏览器每次和服务端的交互都要新建和关闭一个tcp/ip连接,那么浏览器和服务器之间就要往返9次报文通讯,而真正用来处理用户请求的报文确只有其中的两次,换句话说这样的一个请求大概会有80%左右的性能都不是用来处理业务需求,等于是损失了80%左右的性能,当然这个比率是9次报文交互的数据大小一致情况下得出的,如果用户业务请求和响应的数据量比较大,那么建立连接和断开连接的性能损失占比会降低,不过就算占比降低了那也是在请求处理本身的时间变的更慢的基础上的降低,要是浏览器和服务器之间的距离特别大,那么多出来的7次报文交换的效率问题就更加严重了,不管怎样,tcp/ip的三次握手机制和四次挥手机制只要发生都会对网络请求效率产生重大影响。
为了解决这个报文交互次数过多的问题,http协议本身也发生了改变,那就是http开始采用了长连接,使用长连接后网站只需要开启一个长连接,在用户关闭浏览器关闭之前浏览器里的网页都会复用这个长连接。不过http协议的1.0版本默认是不启用长连接的,所以在使用http协议1.0版本时候我就得手动的打开长连接,这个方法就是在http头里设置Connection: Keep-Alive,而http1.1版本里长连接是默认打开的,所以不需要我们手动的设置,而且时下的浏览器几乎都支持http1.1协议,因此大多时候情况下我们是没有必要手动去打开长连接的。
虽然http协议采用长连接后可以减少网站通讯时候三次握手和四次挥手的次数,但是长连接建立起来后需要浏览器和服务器长时间维护,这本身会消耗浏览器和服务器的性能,特别是服务器端长时间维护长连接本身还会损坏服务器处理并发的能力,所以早期浏览器会限制http1.1开启连接的数量,例如ie7这个古董浏览器,它准许http1.1最多开启2个长连接,而http1.0因为默认使用短连接它默认可以开启4个,下面有张图可以说明,如下所示:
提升浏览器加载效率的手段除了提升每个连接的传输效率外,其实还有一种方式,这个方式就是使用多个连接进行并行加载,这个等于几个人联合起来一起完成一个任务,那么效率肯定就比一个人高,而页面加载时候很符合使用并发加载的场景,例如我们让页面里的图片并行加载肯定会比一个个加载图片的效率要高多了。回到浏览器支持的连接数的问题,由于早期浏览器在http1.0和http1.1连接数的差异,某些网站例如维基百科这样的网站,它的静态资源特别多,为了充分发挥并发的优势,它将存放这些静态资源的服务器采用http1.0协议,这样就能并行加载更多的静态资源,因为这个并行加载的总体效率提升相比tcp/ip握手和挥手的损失要高的多,不过现在这个手法已经起不到什么作用了,因为新版的浏览器已经把两种版本的http协议支持的连接数调整一致了,因为长连接可以复用链路,因此使用长连接的效率会比非长连接更好。
上面连接数也是有一个限制的,这个限制就是必须是在同一个域名下,如果一个页面某些静态资源放在不同域名下面,那么这个做法就可以增加页面里的并发数量,例如我们把一些不是经常变化的静态资源例如图片、外部的css文件以及javascript文件单独放置在一个静态资源服务器上,静态资源服务器对外的url地址和页面本身的url地址不在同一个域名下,那么页面本身的并发加载连接数就会增加一倍,不过这也就意味着浏览器端要维护的长连接数会变得更多,雅虎工程师曾经总结过一个页面里合理的域名数量,那就是两个,这个结论的提出已经过去了好多年了,现在的浏览器和服务器的性能已经今非昔比了,这个跨域数量应该可以增加点,不过我个人认为一个页面的里包含的域名数量还是不要太多,其实如果我们web前端优化手段使用得当,两个不同域名就足够用了,多了价值不大,除非你网站情况是在特殊,例如你看看现在浏览器本身支持的连接数量已经很高了,大部分都是6,ie9甚至还达到了10,翻个倍就有12和20个连接数,我们在翻个倍就是24和40个,这个数字看起来就很恐怖了,一个计算机支持这么多并发,假如你在浏览器还打开个网站也是这么干的,那么浏览器的并发数多的实在太吓人了,我估计到时计算机本身就跑不动了,所以10多个连接数很够用了,你合理发挥下这些连接数网站的性能就能有很大提升,再说了一个网站并发连接数太多那本身就说明了你在减少http个数这个手段没有运用好。
回到web前端优化的手段,我们如果把这些手段再仔细分析下就会发现很多手段使用都是在同步请求这个场景下进行了,当然这些手段在合适情况下也能作用于异步加载场景,但是异步加载场景发生并发加载之前需要一个单线程的异步加载,这个单线程的异步加载就和分布式系统里的单点故障有点像了,它很有可能是整个流程的软肋所在,所以合理使用同步请求还能让异步操作性能更加优秀做好准备。上面我讲到浏览器在同一个域名下最多可以开启多少个连接数,但是从事web前端开发的人都能感觉到,我们做页面开发时候其实是没法控制这个连接数的,那么问题来了,这么多连接到底是在什么条件下被开启的呢?这个问题非常有意思的,我们来看下面的瀑布图:
从上面的瀑布图我们发现,并行下载的是图片,这个推而广之要是我们看见某些网站的网页做过并发优化处理的设计,我们就会发现并发的资源都是纯静态的资源,那么这个并发连接数跟我们页面的设计存在一个怎样的关系呢?首先我们总结一下页面里的静态资源,在页面里静态资源有html,如果html里面有内联的css代码和javascript代码,那么这些代码也会归属于html,除了html外还有外部的css文件、外部的javascript文件和页面里使用到的图片,那么这些要素怎样会促发页面的并行加载了,换个说法这些要素又是如何促使浏览器同时打开更多连接呢?
首先我们要明确一个问题,浏览器之所以可以打开更多连接数,让这么多连接并行执行是有个前提的,这个前提就是这些资源是不是被并行加载的,例如像外部css文件,图片这样的资源,这些资源下载完毕后马上就可以使用,因为它们下载完毕后没有逻辑性问题要处理因此下载完毕后就可以直接拿来使用,因此它们并行加载不会影响到页面的展示问题,这个情况如果碰到javascript就有点麻烦了,外部javascript代码是包含逻辑在里面,而且有些逻辑很有可能会影响页面的展示,所以javascript下载完毕后,浏览器就得马上执行,所以我们就会看到这样的瀑布图,如下图所示:
上面的空白区就是浏览器在执行javascript代码所要花费的时间。浏览器开启多少个连接是浏览器自发的行为,这个自发行为主要出于提升浏览器并发下载效率的角度出发的。由于现在浏览器的连接基本都是采取的是http1.1协议,也就是使用的长连接,那么连接建立后这个连接就会长期维护,如果这个长连接是单独的静态资源服务器上的长连接,这个问题倒没什么,如果这个长连接放在主域名下面,问题就来了,主域名在页面初始化加载时候会用来下载html,如果我们为提高并发下载效率,让这个主域名下还放置其他的静态资源,那么可能会导致浏览器和主域名的服务器下维护更多的长连接,而页面后续操作基本是使用ajax来操作的,而ajax往往只会复用其中一个长连接,那么其他多余的长连接等于要空转了,这个空转还需要消耗浏览器和服务器的系统资源,所以我们发现主域名下的请求资源类型一定要认真加以控制,能迁移到单独的静态资源服务器上的一定要进行迁移,尽量让主域名下处理的请求都是包含业务逻辑的请求,这样就可以有效提升系统资源的使用率。这个问题进一步思考下去,我们就会发现如果服务端的业务应用服务器之前放置一个反向代理,反向代理都是使用静态资源服务器,而静态资源服务器对并发的承载能力是远超业务应用服务器,如果主域名下我们不小心放置了太多静态资源,要是后台使用了反向代理,那么反向代理也可以减轻这种长连接所造成的计算资源损失。
上面这些场景都是在浏览器同步请求下进行了,那么换到异步请求这个并行加载静态资源的手段还有效吗?回答这个问题前,我们首先要想想异步加载会导致新的静态资源被加载吗?这个当然可能,特别是在前端MVC的场景下,我们会把模板技术放到浏览器端完成,这个时候有些html模板一开始可能会包含在javascript代码里,作为一个变量存储下来,而这个模板里很有可能包含好多新的图片被使用,当ajax从服务端获取数据后,解析了这个模板,然后我们把构造好的模板加入到页面的DOM结构里,浏览器重新渲染页面时候看到很多新图片需要加载,就有可能会开启多个连接进行并行加载来提升资源加载效率,如果碰到通过ajax技术动态加载外部CSS文件,那么这个并行加载情况就会更加突出了,因为css文件里很有可能包含大量的图片资源,如果我们把不变的静态资源都放置在了单独的静态资源服务器,那么这个并行加载就不会在主域名下打开更多长连接,由此可见,将静态资源使用单独的域名的静态资源服务器处理的好处非常之多。
现在http2.0协议还在起草之中,http2.0如果落地将会给web前端优化技术产生重大影响,http2.0打算在一个页面里只使用一个tcp/Ip连接,不过http2.0会在这个连接上进行链路复用,也就是让一个连接上也能做到并行操作,让连接的利用率更高,如果http2.0落地后,web前端里那些用于减少http连接数的手段都会失去市场了,因为协议本身就能处理好并发的问题了,到时像外部css文件,外部javascript文件,css sprite技术说不定就要成为历史了。
看来本主题又写不完了,下篇接着写吧,今天是元宵节,这里我祝大家节日快乐。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。