应用服务和数据分离体现在三个模块:应用服务器、文件服务器和数据服务器。
应用服务器需要处理大量业务逻辑,因此重点关注CPU使用;
数据服务器需要快速磁盘检索和数据缓存能力,重点在更快的磁盘和更大的内存;
文件服务器需要存储大量用户上传的文件,因此关注点在更大的硬盘上。
根据二八定律,80%的业务访问集中在20%的数据上,那么这一部分热点数据还存在内存中,可显著减少DB的访问压力。
缓存可以分为两类:应用服务器的本地缓存和存放在分布式服务器上的远程缓存。
目前主流的数据库支持主从热备能力,通常配置两台数据库主从关系,可以将一台数据库服务器中的数据同步更新到另一台服务器上,利用这一功能实现数据的读写分离,以改善数据库压力。具体为应用服务器在写入数据时访问主数据库,主数据库通过主从复制功能将数据同步更新至从数据库,当应用服务器读数据时,可以通过从数据库读取数据。
CDN和反向代理的基本原理都是缓存,区别在于CDN部署在网络提供方的机房,使用者在访问网站时先从距离自己最近的网络提供方的机房获取数据。反向代理则是部署在网站的中心机房中,当用户请求到达中心机房后首先访问的服务器是反向代理服务器,如果反向代理服务器中还存有用户请求的资源,就将其直接返回给用户。
如果数据库在经过读写分析、多台服务器扩展等方式仍不能满足需要,那么就需要使用分布式数据库。分布式数据库是网站拆分最后的手段,当单表数据量非常庞大时,可以考虑将业务分库,将不同业务的数据部署在不同的物理服务器上。
当应用需要对数据存储和检索的要求越来越高时,可以考虑非关系型数据库技术如NoSQL和非数据库查询技术如搜索引擎。应用服务器则通过统一的数据访问模块访问各类数据。
分层是业务系统中最常见的架构模式,将系统横向切分成多个部分,每个部分负责一部分相对比较单一的职责,然后通过上下层之间的依赖和调用组成一个完整的系统。如最简单的分层架构为:应用层、服务层、数据层的划分。
纵向分割是指将不同的功能和服务分割开来,包装成高内聚低耦合的模块单元,一是便于维护和开发;二是方便进行分布式部署,提高并发处理能力和功能扩展能力。
通常来讲大型网站分割力度可以很细致,例如应用层可以将不同业务进行分割,例如将购物、论坛、搜索、广告等等分割成不同的服务,由单独的团队来负责。
横向分层和纵向分割的一个主要目的就是为了切分后的模块便于分布式部署,将不同的模块部署在不同的服务器上,通过远程调用框架协同工作。
需要注意的是,分布式部署在解决高并发的同时也会带来其他问题。首先远程调用之间的网络通信问题;其次服务越多,服务器宕机的可能性也越大;另外,还存在数据在分布式环境中保持数据一致性的挑战分布式事务的管理也难以保证;最后,分布式还会导致网站复杂程度上升,开发和管理维护难度增加。
除了常规的分布式应用服务、分布式静态资源、分布式数据和存储等等,还包括支持服务器配置实时更新的分布式配置系统;分布式环境下支持并发和协同的分布式锁;支持云存储的分布式文件系统等。
为保证应用的可用性,即使访问量很小的分布式应用系统,也应该至少部署两台及以上的服务器构成一个集群,提高系统的可用性。
缓存设计主要包括CDN、反向代理、本地缓存、分布式缓存等部分。
CDN:即内容分发网络、部署在网络服务提供商的机房内,这里常用来存储一些静态资源(即较小变化的数据),如视频网站和门户网站会将用户访问量大的热点内容缓存在CDN。
反向代理:反向代理属于网站前端架构的一部分,部署在网站前端。这里存储网站的静态资源,无需将请求数据转发到应用服务器就能返回给用户。
本地缓存:应用服务器本地缓存的热点数据,可以直接在本机内存中访问无需访问数据库。
分布式缓存:将热点数据专门存放在分布式缓存集群中,应用程序通过网络通信访问缓存数据。
使用缓存的两个前提条件:一是数据访问热点不均衡,即频繁访问的数据应该放在缓存中;二是数据在某个时间段内都是有效的,不会很快过期,否则容易出现因缓存数据失效造成的脏读问题。
自动化包括自动话的代码管理机制、自动化测试、自动化安全检测、自动化部署、自动化监控、自动化报警等模块。甚至还包括自动化失效转移、自动化失效恢复、自动化降级、自动化分配资源等等。
首先是高性能,然后包括可用性和稳定性、伸缩性、扩展性、安全性等等方面。
实践中,前端架构的优化手段包括优化页面HTML样式、利用浏览器的并发和异步特性、调整浏览器缓存策略、使用CDN服务、反向代理等手段。
应用程序模块的优化则主要包含缓存使用、使用集群提高吞吐能力、使用异步消息处理方式加快请求响应以及实现削峰、代码优化等等方式。
1)减少http请求
HTTP协议是无状态协议,也就意味着每次HTTP请求都需要与服务器建立连接进行数据传输,服务器也需要对http请求启动独立线程去处理。减小http请求的主要手段包括合并CSS、JavaScript、图像等,这样减小浏览器请求次数。
2)使用浏览器缓存
通过设置http头的Cache-Control和Expires属性,可以设置浏览器缓存。在某些时候静态资源文件的变化需要客户端浏览器及时更新,这种情况下可通过更改文件名实现,即可以更新javaScript文件而不是更新JavaScript文件内容,生成一个新的JS文件并更新http文件中的引用。
3)启动压缩
对HTTP、CSS、JavaScript等文件启用Gzip压缩可以达到比较好的压缩效果,能有效减小通信传输的数据量。
4)CSS放在页面最上面、JavaScript放在页面最下端
浏览器下载完全部CSS才会对页面进行渲染,所以尽量将CSS放在页面上端。当浏览器加载完JavaScript后就会立刻执行,有时候可能会阻塞整个页面,造成显示过于缓慢,因此JavaScript最好放在页面最下端。
5)减小cookie的传输
cookie包含在每次http请求和响应中,当cookie数据太大会影响网络数据传输。例如当访问静态资源,如CSS、JavaScript时不需要传输cookie,可以考虑使用独立域名访问,避免请求静态资源时发送cookie。
CDN常用来缓存的静态资源包括图像、文件、CSS、JavaScript脚本等等。
反向代理服务器也可以配置缓存功能来加速Web请求,有时候一些动态热门数据也可以放在反向代理服务器上,例如门户网站的热门词条、帖子、博客等数据。当热点动态内容有变化时,通知反向代理缓存失效,重新加载新的热点动态内容。
此外,反向代理也能实现负载均衡的功能,改善系统在高并发环境下的处理能力。
1)缓存使用注意事项
缓存主要用来存放读写比例很高、极少变化的数据。一般来说数据的读写比例在2:1以上,这样缓存内数据才有意义。从另一个角度来讲,计算机内存资源宝贵,因此不能将所有数据都缓存起来,那么只有存放遵循二八定律的热点数据,才能最大效率发挥缓存的作用。
一般来讲,会对缓存数据设置失效时间,一旦超过失效时间,需要从数据库重新加载数据。因此应用需要容忍一定时间的数据不一致情形,或者是当数据库数据更新后立即要更新缓存。
缓存服务器的可用性也要有保证,当缓存服务器出问题后,所有请求都会涌入数据库服务器访问,会造成数据库压力过大甚至崩溃。实践中,有的网站通过缓存热备的手段提高缓存服务器的可用性。当某台缓存服务器宕机时,将缓存访问切换到备用服务器上。
另外一种方式是使用分布式缓存服务集群,将缓存数据分布到集群的多台机器上,当某台机器出故障时,只有部分缓存数据不可用,其他机器依然能保持运行。重新从数据库加载这部分数据也不会对数据库有很大的影响。(如何确定哪些数据在失效的缓存机器上?)
缓存预热:缓存中存放的热点数据一般需要缓存系统通过LRU(最近最久未用算法)筛选出,此过程会花费较长时间,对数据库压力比较大。当新的缓存系统加载时,最好把热点数据加载好,即称为缓存预热。对于一些元数据如城市列表、地名等等,可以在启动时加载数据库中的全部数据到缓存。
缓存穿透:如果持续高并发请求缓存不存在的数据,那么所有请求都将落到数据库上,加大数据库压力。一个最简单的对策是将不存在的数据也缓存起来(比如设置value值为NULL)。
2)分布式缓存架构
分布式缓存是指缓存部署在多台服务器组成的集群中,其架构有两种方式:一种是以JBoss Cache为代表的需要同步更新的分布式缓存;一种是以Memcached为代表的互不通信的分布式缓存。
JBossCache的分布式缓存集群中所有服务器都保存相同的缓存数据,当某台服务器缓存数据有变动时,会通知集群中其他机器也更新缓存。这种方式当集群规模较大时各机器间的数据同步消耗会惊人。
Memcached采用一种互不通信的分布式架构方式,缓存与应用服务器分离部署,缓存系统部署在一组专门的服务器上,应用程序通过一致性hash等路由算法选择具体的缓存服务器远程访问缓存数据。缓存服务器之间互相不通信,缓存集群的规模可以很方便的扩展,具有较好的伸缩性。
这一过程主要是指使用消息队列将调用异步化,用于改善应用系统的处理性能。另外,消息队列还具有较好的削峰功能。在高并发时期将来不及处理的请求暂存在消息队列中,从而实现对高并发量时期峰值的削平功能。
1)多线程优化
一是合理确定线程数量,如果是CPU计算型任务,那么县城数不宜超过CPU内核数,因为再启动多的线程,CPU也来不及调度。如果是I/O任务需要等待磁盘、网络响应,那么可以考虑启动更多的线程提高任务的并发度,提升系统的吞吐能力。
多线程还需注意的问题在于线程安全问题。解决方式主要有:一是将对象设计为无状态对象,使得对象本身不存储状态信息。二是使用局部对象,在线程内部创建对象,这样不会被其他线程访问。三是并发访问共享资源时使用同步机制,避免多个线程同时修改共享数据。
2)资源复用
资源复用的两种方式:单例和对象池。例如目前常见的spring容器默认构造的对象就是单例对象(要注意spring的单例是指spring容器管理的单例,不是用单例模式构造对象)。
对象池通过复用对象实例,减小对象创建和销毁的资源消耗,从而提高系统性能。例如常见的数据库连接池和缓存服务器连接池。对于web请求来说,web应用服务器都需要创建一个独立的线程去处理,这里应用服务器也是采用线程池的方式来实现的。
高可用架构设计的主要目的是保证服务器硬件出现故障时服务依然可用,数据仍然能被访问到,那么实现该功能的主要方式就是数据和服务的冗余备份和失效转移。
位于应用层的服务器通常为了应对高并发环境,会通过负载均衡设备将一组服务器组成集群共同对外提供服务。当负载均衡设备监控到某台应用服务器不可用时将其从集群列表中剔除,并将请求发送至其他服务器列表中,从而实现整个集群的高可用设计。
位于数据层的服务器比较特殊,为保证数据服务器宕机时数据不丢失且数据访问不中断,需要在数据写入时进行数据同步复制操作,将数据备份至多台机器上,实现数据冗余。
应用层主要处理业务逻辑,最显著的一个特点应该是应用的无状态性,即它不应该保存业务的上下文信息,而根据每次请求的数据进行业务逻辑处理。多个服务器之间完全对等,请求到任意一个服务器的处理结果应该都是一样的。
负载均衡主要是在高并发环境下,将请求均衡分发到一个集群组成的多台服务器上,提高整体的处理能力。当集群中的服务是无状态对等时,负载均衡可以起到高可用的作用。当某台机器失效无法正常工作时,负载均衡服务器通过心跳发现该服务器失去响应,就会将其从服务器列表删除,并将请求分发到其他服务器上,这样不会影响到请求的处理。
应用服务器通常被设计为无状态的,但是业务请求总是有状态的。例如用户的登录状态、最新发布的消息及好友状态等等。Web应用中将这些多次请求修改使用的上下午你对象成作为会话(session)。单机时session可以由服务器上的web容器管理,但是在分布式系统中,由于请求会随机发到任何一台机器上,故而要保证每次请求都能获得正确的sessio信息。
集群环境下,常见的session管理主要有以下几种方式:
1)session复制。这是一种早期使用较多的管理机制,主要原理是在集群中的几台服务器中同步session对象,每台机器中都保存有用户的session信息当集群规模非常大时,集群间的session复制会占用较高i/o或网络资源,系统压力较大。
2)session绑定。session绑定可以利用负载均衡的源地址hash算法实现。负载均衡通过将来源于同意IP的请求都发到相同的服务器上,这样整个会话期内,用户请求都在同意台服务器上完成,session也绑定在该机器上。此种方法不符合对系统高可用的要求,也不满足应用服务器无状态的特点。当某台服务器出现故障,用户请求切换到其他服务器的时候会因为没有session而无法处理业务。
3)利用cookie记录session。这种方式是将session记录在客户端。每次请求的时候将session放在请求中发送给服务器,服务器处理完请求后再将修改过的session响应返回给客户端。利用cookie存储session也存在一些问题,例如受cookie大小的限制,存放的信息有限;每次请求都需要传输cookie,影响性能;用户关闭cookie,那么就无法正常访问等等。
但是由于cookie的简单易用,支持应用服务器的线性伸缩,且大部分应用需要记录的session信息又比较小。因此,许多网站或多或少都在使用cookie来记录session。
4)session服务器。利用独立部署的session服务器集群统一管理session,可实现高可用、高伸缩性、对信息大小没有限制的session管理。其主要思想是将应用服务器的状态分离,分成无状态的应用服务器和有状态的session服务器两部分。
对于有状态的session服务器,一种比较简单的方法是利用分布式缓存、数据库等,在其基础上进行包装以实现session的存储和访问要求。如果业务场景对session要求较高,比如利用session服务集成单点登陆(SSO)、用户服务等等,则需要开发专门的session管理平台。
可复用的服务一般是为业务产品提供基础公共服务的模块,大型系统中这些模块一般独立部署,业务系统通过远程调用的方式访问这些基础服务。可复用的服务也是无状态的,因此可以利用负载均衡的失效转移机制来实现高可用。
其他常见的高可用服务策略主要有:分级管理、超时设置、异步调用服务降级、幂等性设计等等。
不同于高可用的应用和服务,实现数据的高可用手段主要是数据备份和失效转移机制。因为数据服务器之间保存的数据各不相同,当某台机器出现故障时,数据请求访问是不能任意切换到其他机器上的。
数据备份保证网站数据有多个副本,任意副本的数据丢失都不会导致数据的永久丢失,从而实现数据的完全持久化。失效转移机制则保证当一个数据副本失效时,可以快速切换到其他副本,保证系统正常可用。
对于缓存服务集群的单机故障,如果集群数量较大,那么当某台机器出现故障时缓存数据丢失的比例比较小,数据库增加的压力变化幅度也是可控的,对整体系统影响也比较小。扩大缓存服务器集群规模一个最简单的方式是所有服务共享一个缓存集群,单独的应用和服务不必自己维护缓存服务器,只需向共享缓存集群申请资源即可。
利用分布是架构保证数据高可用的同时也会引入一大挑战,即数据一致性问题。
CAP原理认为,一个提供数据服务的存储系统无法同时满足数据一致性(Consistency),数据可用性(Availibility),分区耐受性(Partition Tolerance,系统具有跨网络分区额伸缩性)这三个条件。
数据一致性又可划分为:数据强一致、数据用户一致、数据最终一致。
数据冷备份:定期将数据复制到存储介质中,如果数据系统出故障便从存储设备中回复数据。该方法优点是简单和低廉,但由于存储设备中数据比数据系统陈旧,容易出现数据丢失情况,不能保证数据最终一致性。而且在数据恢复的过程中也无法正常使用系统,不能保证系统的可用性。
数据热备份可分为异步热备和同步热备两种方式。异步热备是指多个副本的写入操作异步完成,应用程序收到数据写入请求后只操作主数据库,存储系统自己异步实现写入其他副本的功能。同步热备是指在应用程序客户端并发向多个存储服务器同时写入数据,等所有存储服务器返回成功响应后,再通知应用服务器写入成功。
关系数据库的热备机制就是通常说的Master-Slave同步机制。Master-Slave机制不仅解决了数据备份的问题,还可以改善数据库系统性能。实际中,通常使用读写分离的策略来访问Slave和Master数据库。写操作只访问Master数据库,读操作则访问Slave数据库。
失效转移包括三部分构成:失效确认,访问转移,数据恢复等。
监控数据采集包括业务运行数据和用户行为日志、以及系统运行数据等等。
1)用户行为日志搜集:包含用户操作系统和应用版本信息、IP地址、页面访问路径、页面停留时间等等;主要分为服务端日志搜集和客户端日志搜集两部分。
服务端日志搜集:一般利用服务器的日志记录功能来搜集和整理;
客户端日志搜集:利用页面嵌入的javaScript脚本搜集用户真实操作。
2)服务器性能监控:这部分内容包括CPU占用率、内存占用率、磁盘IO、网络IO等数据,用于判断服务器性能是否正常。
3)运行数据报告:主要记录与业务场景相关的技术和业务指标,例如缓存命中率、平均响应延时、每秒处理请求数、平均等待时间等等。
监控管理包括系统报警、失效转移、自动优雅降级等方式。
应用服务器应该设计成无状态的,即应用服务器不存储请求上下文信息,如此可实现应用服务器的可伸缩性设计。
负载均衡服务器则是应用服务器伸缩性设计的必须环节,它可以及时感知或者配置集群的服务器数量,可以及时发现集群中新上线或下线的服务器。
常见的负载均衡策略有:
1)HTTP重定向负载均衡:HTTP重定向服务器是根据用户的HTTP请求计算出一台真实的Web服务器地址,并将Web服务器的地址写入HTTP重定向响应(响应码302)中返回给用户。然后用户重新访问真实的Web地址。
该方法实现比较简单,但是浏览器需要请求两次才能完成一次访问,效率较低;另外,重定向服务器自身可能形成性能上的瓶颈,使得系统处理能力无法继续提高,因此实际中使用较少。
2)DNS域名解析:DNS服务器对每个域名请求会根据负载均衡策略计算出一个真实IP地址给用户,然后用户再请求到真是的Web服务器上。
3)反向代理服务器:实际中,反向代理服务器部署在Web应用前面,用户直接访问反向代理服务器的地址,反向代理服务器耿军负载均衡算法计算出一个真实的服务地址。并将请求转发到这个地址上。应用服务器处理完后返回信息给反向代理服务器,然后再将处理结果原路返回给用户。
4)IP负载均衡:用户请求数据到达负载均衡服务器后,在操作系统内核进程获取网络数据包,并将请求目的IP修改为真是的Web服务器地址。应用服务器处理完成后,数据包返回负载,修改数据包IP地址为负载地址后返回给用户
5)数据链路层负载:这种数据传播方式为三角传播,请求从用户服务器到负载服务器,再到应用服务器;应用服务器处理完后直接返回给用户。
分布式缓存服务器集群中不同节点上缓存数据均不同,缓存访问请求必须先找到存储有需要数据的服务器地址,然后才能访问。当分布式缓存集群需要扩容,新上线缓存服务器的时候,必须要对整个分布式缓存集群影响最小,使得之前已经缓存的数据能尽可能被访问到。
目前最常见的方式是使用一致性hash算法来处理新加入的服务器不影响之前服务器的路由算法。算法过程为先构造出一个长度为2e32的整数环(一致性Hash环),根据节点名称的Hash值将缓存服务器节点防止在这个Hash环上。然后计算需要缓存的数据的key的Hash值,从hash环上顺时针查找距离这个key的Hash值最近的缓存服务器节点。
当新增一个缓存服务器节点时,由于是顺时针查找距离最近的服务器节点,所以只会影响到一部分的数据。但是一致性Hash算法容易带来负载不均衡的问题,原因是各节点在Hash环上的距离可能不一致。通常使用虚拟层的方式来解决这个问题。将每台物理缓存服务器先虚拟成一组虚拟缓存服务器,将虚拟服务器的Hash值防止在hash环上,Key先找到虚拟服务器,再得到物理服务器的地址。
可扩展性与伸缩性有相似的地方。扩展性着重于在对现有系统影响最小的情况下,保持系统的可持续扩展或提升。伸缩性则指系统能够通过增加(或减小)自身资源规模的方式增强(或减小)自己的资源使用量。在架构设计中通常利用集群的方式增加服务器数量来提升系统扩展能力。
消息队列利用发布——订阅的模式使得各个模块不存在直接调用,对各模块解耦。消息生产者通过远程访问将消息推给消息队列服务器,消息队列服务器将消息写入本地内存队列后返回成功响应给消息生产者。消息队列服务器根据消息订阅列表查找订阅该消息类型的消息消费应用程序,并将消息通过远程通信方式发给消息消费程序。
服务提供方向注册中心(servicebroker)描述自身提供的服务接口属性,注册中心发布服务提供者提供的服务,服务请求者从注册中心检索到服务信息后,与服务提供者通信,调用相关服务。
阿里dubbo分布式开源框架其架构如下:
服务消费者通过服务接口来调用服务,而服务接口则通过接口访问代理来加载具体服务(本地代码模块、远程服务等)。应用程序只需要调用服务接口,服务框架根据配置自动调用需要的服务。
服务框架客户端通过服务注册中心来加载服务提供者列表,服务提供者启动后自动向注册中心注册自己可提供外部调用的服务接口列表。服务框架客户端查找需要的服务接口、并根据配置的负载均衡策略将服务调用请求发送到具体某个服务提供者。如果调用失败,那么服务框架客户端会自动从服务提供列表中重新选择一个可提供同样服务的服务器发送调用请求。
XSS攻击即跨站点脚本攻击,是指黑客通过篡改网页注入恶意脚本,在用户浏览网页时,控制用户浏览器进行恶意操作的一种攻击方式。
常见XSS攻击有两种:一种是反射型,攻击者诱导使用者点击一个嵌有恶意脚本的链接以达到攻击目的。另一种是持久型攻击,黑客提交含有恶意脚本的请求,保存在被攻击的Web站点的数据库中,用户浏览网页时,正常页面中会包含恶意脚本,从而达到攻击目的。
常见预防手段:一是消毒,避免在请求中嵌入恶意脚本。通常做法是对某些html危险字符进行转义,如>、<等。二是对敏感的Cookie添加HttpOnly属性,防止XSS攻击者窃取Cookie。
常见防止SQL注入攻击的方式是通过正则匹配过滤掉数据中可能注入的SQL。另外就是使用预编译手段和绑定参数,避免SQL注入。
1)写日志:生产环境访问数据量大,日志级别开启不合理或导致过多日志输出,最好磁盘被写满。
2)高并发锁引起的问题:某逻辑执行时需要批量写数据库,同时也是使用锁控制同步,当DB执行时间较长时,锁被占用。其他线程等待超时。
3)缓存故障:缓存机器故障导致请求全部访问数据库,DB支撑不住报警。
4)大文件读写引起内存溢出:HTTP请求传输文件二进制流,并发请求较高时排队处理,内存中存放过多大对象,最后无法被回收,报内存溢出。