大型网站技术架构-读书笔记

大型网站技术架构-读书笔记

本文是《李智慧. 大型网站技术架构:核心原理与案例分析 . 电子工业出版社. 》一书的读书笔记。

大型网站的特点:

  • 高并发,大流量的访问
  • 高可用的服务
  • 海量数据
  • 用户分布广,网络环境复杂
  • 安全环境恶劣
  • 需要快速变更,发布频繁
  • 渐进式发展,大型网站都是从一个小网站开始,渐进的演化。

架构演化历程

大型网站的技术挑战主要来自于庞大的用户,高并发的访问和海量的数据,任何简单的业务一旦需要处理数以P计的数据和面对数以亿计的用户,问题就会变得很棘手。大型网站架构主要就是解决这类问题。

小型网站阶段

大型网站都是从小型网站发展而来。小型网站最开始没有什么人访问,只要一台服务器就可以。应用程序,数据库,文件等所有资源都在一台服务器上。

应用服务和数据服务分离

随着业务的发展,一台服务器满足不了需求:

  • 过多的用户访问导致性能越来越差
  • 过多的数据导致存储空间不足。

这时候就要将应用和数据分离。应用和数据分离后网站使用3台服务器:应用服务器,文件服务器和数据服务器。

这3台服务器对硬件资源的要求各不相同,应用服务器需要处理大量的业务逻辑,需要更快的cpu;数据库服务器需要快速磁盘检索和数据缓存,需要更大的内存和更快的磁盘;文件服务器需要存储大量文件,需要更大的硬盘。

应用和数据分离后,不同特性的服务器承担不同的服务角色,网站的并发处理能力和数据存储空间得到了很大的改善。但是随着用户的增多,数据库压力太大导致访问延迟。

使用缓存改善网站性能

网站的访问特点遵循着二八定律:大部分业务访问集中在一小部分数据上,那么如果把这部分数据缓存在内存中,会减少数据库的访问压力。

网站的缓存分为两种:

  • 缓存在应用服务器上的本地缓存。
  • 缓存在专门的分布式缓存服务器上的远程缓存。

本地缓存的访问速度更快一些,但是受应用服务器内存限制,其缓存数据量有限,而且会出现和应用程序争用内存的情况。远程分布式缓存可以使用集群的方式,部署大内存的服务器作为专门的缓存服务器,可以在理论上做到不受内存容量限制的缓存服务。

使用缓存后,数据访问压力得到缓解,但是单一应用服务器能处理的请求连接有限,应用服务器成了网站的瓶颈。

使用应用服务器集群改善网站的并发能力

使用集群是网站解决高并发,海量数据问题的常用手段。当一台服务器的处理能力,存储空间不足时,考虑通过水平扩容的方式来分担服务器的访问和存储压力。

通过负载均衡调度服务器,可以将用户访问请求分发到应用服务器集群的任何一台服务器上,如果有更多用户就在集群中加入更多的应用服务器。

数据库读写分离

网站使用缓存后,大多数读操作都可以不通过数据库完成,但是仍有部分读操作(缓存不命中,缓存过期,缓存淘汰)和全部写操作需要访问数据库,因此当网站的用户达到一定规模后,数据库成了瓶颈问题。

目前大部分的主流数据库都支持主从热备功能。通过设置两台数据库的主从关系,可以将一台数据库服务的数据更新同步到另一台服务器上。将写请求落在主库,读请求落在从库,实现数据库的读写分离,从而改善数据库负载压力。

应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库,这样当应用服务器读数据的时候,就可以通过从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离对应用透明。

反向代理和CDN加速网站响应

为了提供更好的用户体验,留住用户,网站需要加速网站访问速度。主要手段有使用CDN和反向代理。CDN和反向代理的基本原理都是缓存,区别在于CDN部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;而反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。使用CDN和反向代理的目的都是尽早返回数据给用户,一方面加快用户访问速度,另一方面也减轻后端服务器的负载压力。

分布式文件系统和分布式数据库系统

任何强大的单一服务器都满足不了大型网站持续增长的业务需求。数据库经过读写分离后,从一台服务器拆分成两台服务器,但是随着网站业务的发展依然不能满足需求,这时需要使用分布式数据库。文件系统也是一样,需要使用分布式文件系统。

分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。不到不得已时,网站更常用的数据库拆分手段是业务分库,将不同业务的数据库部署在不同的物理服务器上。

使用NoSQL和搜索引擎

随着业务的复杂,对数据存储和检索的需求也越来越复杂,需要采用一些非关系数据库技术如NoSQL和非数据库查询技术如搜索引擎。

NoSQL和搜索引擎对可伸缩的分布式有更好的支持。应用服务器则通过一个统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

业务分拆

大型网站为了应对复杂的业务场景,通过使用分而治之的手段将网站业务分为不同的产品线。如大型购物交易网站就会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。

具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署维护。应用之间可以通过一个超链接建立关系(在首页上的导航链接每个都指向不同的应用地址),也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。

分布式服务

随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致存数据库接资源不足,拒绝服务。

既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。

架构模式

为了解决大型网站面临的高并发访问、海量数据处理、高可靠运行等一系列问题与挑战,大型互联网公司在实践中提出了许多解决方案,以实现网站高性能、高可用、易伸缩、可扩展、安全等各种技术架构目标。这些解决方案又被更多网站重复使用,从而逐渐形成大型网站架构模式。

分层

分层是一种最常见的架构模式,将系统在横向维度上切分成几个部分,每个部分负责一部分相对比较单一的职责。然后通过上层对下层的依赖和调用组成一个完整的系统。分层架构思想在计算机世界中无处不在,例如网络的7层通信协议可以看做是一种分层架构;计算机硬件,操作系统和应用软件也可以看做是一种分层结构。在大型网站架构中也采用分层结构,将网站软件系统分为应用层,服务层和数据层。

  • 应用层: 负责具体业务和视图展示。如网站首页及搜索输入和结果展示。
  • 服务层: 为应用层提供服务支持。如用户管理服务,购物车服务等。
  • 数据层: 提供数据存储访问服务。如数据库,缓存,文件,搜索引擎等。

通过分层可以更好地将一个庞大的软件系统切分成不同的部分,便于分工合作开发和维护;各层间有一定的独立性,只要维持调用接口不变,各层可以根据具体问题独立演化发展。

但是分层架构也有一些挑战,就是必须合理规划层次边界和接口,在开发过程中,严格遵循分层架构的约束,禁止跨层次的调用以及逆向调用。在实践中大的分层还可以再细分

分层架构是逻辑上的,在物理部署上,三层结构可以部署在同一个物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,即三层结构分别部署在不同的服务器上,使网站拥有更多的计算资源以应对越来越多的用户访问。

虽然分层架构模式最初的目的是规划软件清晰的逻辑结构便于开发维护,但在网站的发展过程中,分层结构对网站支持高并发向分布式方向发展至关重要。因此在网站规模还很小的时候就应该采用分层的架构,这样将来网站做大时才能有更好地应对。

分割

如果说分层是将软件在横向方面进行切分,那么分割就是在纵向方面对软件进行切分。网站越大,功能越复杂,服务和数据处理的种类也越多,将这些不同的功能和服务分割开来,包装成高内聚低耦合的模块单元,一方面有助于软件的开发和维护;另一方面,便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。

大型网站分割的粒度可能会很小。比如在应用层,将不同业务进行分割,例如将购物、论坛、搜索、广告分割成不同的应用,由独立的团队负责,部署在不同的服务器上;在同一个应用内部,如果规模庞大业务复杂,会继续进行分割,比如购物业务,可以进一步分割成机票酒店业务、3C业务,小商品业务等更细小的粒度。而即使在这个粒度上,还是可以继续分割成首页、搜索列表、商品详情等模块,这些模块不管在逻辑上还是物理部署上,都可以是独立的。同样在服务层也可以根据需要将服务分割成合适的模块。

分布式

分层和分割的一个主要目的是为了切分后的模块便于分布式部署:即不同的模块部署在不同的服务器上,通过远程调用协同工作。但是分布式在解决高并发问题的同时也带来了其他问题。

  • 分布式意味着服务调用必须通过网络,这可能对性能造成比较严重的影响;
  • 服务器越多,服务器宕机的概率也越大,一台服务器宕机造成的服务不可用可能会导致很多应用不可访问;
  • 数据在分布式环境保持数据一致性也非常困难,分布式事务也很难保证;
  • 分布式还导致网站依赖错综复杂,开发管理维护困难;

在网站应用中,常见的分布式方案有以下几种:

  • 分布式应用和服务: 将分层和分割后的应用和服务模块分布式部署,可以改善网站性能和并发性、加快开发和发布速度、减少数据库的连接资源消耗,复用共同的服务,便于业务功能扩展。
  • 分布式静态资源: 网站的静态资源如JS,CSS,Logo图片等资源独立分布式部署,并采用独立的域名,即动静分离。静态资源分布式部署可以减轻应用服务器的负载压力;通过使用独立域名加快浏览器并发加载速度
  • 分布式数据和存储:大型网站需要处理PB级别的海量数据,单台机器无法提供如此大的存储空间,需要分布式存储。除了对传统的关系数据库进行分布式部署外,为网站应用而生的各种NoSQL产品几乎都是分布式的。
  • 分布式计算:严格说来,应用、服务、实时数据处理都是计算,网站除了要处理这些在线业务,还有很大一部分用户没有直观感受的后台业务要处理,包括搜索引擎的索引构建、数据仓库的数据分析统计等。这些业务的计算规模非常庞大,目前网站普遍使用MR,Spark,Flink分布式计算框架进行计算,将计算程序分发到各个节点进行分布式计算。

还有可以支持网站线上服务器配置实时更新的分布式配置;分布式环境下实现并发和协同的分布式锁;支持云存储的分布式文件系统等。

集群

分布式虽然已经将分层和分割后的模块独立部署,但是对于用户访问集中的模块(比如网站的首页),还需要将独立部署的服务器集群化,即多台服务器部署相同应用构成一个集群,通过负载均衡设备共同对外提供服务。

因为服务器集群有更多服务器提供相同的服务,因此可以提供更好的并发特性,当有更高并发要求时,只需要向集群中加入新的机器。同时,因为一个应用由多台服务器提供,当某台服务器发生故障时,负载均衡设备或者系统的失效转移机制会将请求转发到集群中的其他服务器上,使服务器故障不影响用户使用。所以在网站应用中,访问量很小的服务也会至少部署两台服务器来构成集群,以提高可用性。

缓存

缓存就是将数据存放在距离计算最近的位置以加快处理速度。缓存是改善软件性能的第一手段,现代CPU越来越快的一个重要因素就是使用了更多的缓存,在复杂的软件设计中,缓存几乎无处不在。大型网站架构设计在很多方面都使用了缓存设计。

  • CDN: 即内容分发网络,部署在距离终端用户最近的网络服务商,用户的网络请求总是先到达他的网络服务商那里,在这里缓存网站的一些静态资源(较少变化的数据),可以就近以最快速度返回给用户,如视频网站和门户网站会将用户访问量大的热点内容缓存在CDN。
  • 反向代理:反向代理属于网站前端架构的一部分,部署在网站的前端,当用户请求到达网站的数据中心时,最先访问到的就是反向代理服务器,这里缓存网站的静态资源,无需将请求继续转发给应用服务器就能返回给用户。
  • 本地缓存:在应用服务器本地缓存着热点数据,应用程序可以在本机内存中直接访问数据,而无需访问数据库。
  • 分布式缓存:大型网站的数据量非常庞大,即使只缓存一小部分,需要的内存空间也不是单机能承受的,所以除了本地缓存,还需要分布式缓存,将数据缓存在一个专门的分布式缓存集群中,应用程序通过网络通信访问缓存数据。

使用缓存有两个前提条件,一是数据访问热点不均衡,某些数据会被更频繁的访问,这些数据应该放在缓存中;二是数据在某个时间段内有效,不会很快过期,否则缓存的数据就会因已经失效而产生脏读,影响结果的正确性。网站应用中,缓存除了可以加快数据访问速度,还可以减轻后端应用和数据存储的负载压力,这一点对网站数据库架构至关重要,网站数据库几乎都是按照有缓存的前提进行负载能力设计的。

异步

计算机软件发展的一个重要目标和驱动力是降低软件耦合性。事物之间直接关系越少,就越少被彼此影响,越可以独立发展。大型网站架构中,系统解耦合的手段除了前面提到的分层、分割、分布等,还有一个重要手段是异步,业务之间的消息传递不是同步调用,而是将一个业务操作分成多个阶段,每个阶段之间通过共享数据的方式异步执行进行协作。

在单一服务器内部可以通过多线程共享内存队列的方式实现异步,处在业务操作前面的线程将输出写入到队列,后面的线程从队列中读取数据进行处理;在分布式系统中,多个服务器集群通过分布式消息队列实现异步,分布式消息队列可以看作内存队列的分布式部署。

异步架构是典型的生产者消费者模式,两者不存在直接调用,只要保持数据结构不变,彼此功能实现可以随意变化而不互相影响,这对网站扩展新功能非常便利。

  • 提高系统可用性。消费者服务器发生故障,数据会在消息队列服务器中存储堆积,生产者服务器可以继续处理业务请求,系统整体表现无故障。消费者服务器恢复正常后,继续处理消息队列中的数据。
  • 加快网站响应速度。处在业务处理前端的生产者服务器在处理完业务请求后,将数据写入消息队列,不需要等待消费者服务器处理就可以返回,响应延迟减少。
  • 消除并发访问高峰。用户访问网站是随机的,存在访问高峰和低谷,即使网站按照一般访问高峰进行规划和部署,也依然会出现突发事件,比如购物网站的促销活动,微博上的热点事件,都会造成网站并发访问突然增大,这可能会造成整个网站负载过重,响应延迟,严重时甚至会出现服务宕机的情况。使用消息队列将突然增加的访问请求数据放入消息队列中,等待消费者服务器依次处理,就不会对整个网站负载造成太大压力。

但需要注意的是,使用异步方式处理业务可能会对用户体验、业务流程造成影响,需要网站产品设计方面的支持。

冗余

网站需要7*24小时连续运行,但是服务器随时可能出现故障,特别是服务器规模比较大时,出现某台服务器宕机是必然事件。要想保证在服务器宕机的情况下网站依然可以继续服务,不丢失数据,就需要一定程度的服务器冗余运行,数据冗余备份,这样当某台服务器宕机时,可以将其上的服务和数据访问转移到其他机器上。

访问和负载很小的服务也必须部署至少两台服务器构成一个集群,其目的就是通过冗余实现服务高可用。数据库除了定期备份,存档保存,实现冷备份外,为了保证在线业务高可用,还需要对数据库进行主从分离,实时同步实现热备份。

为了抵御地震、海啸等不可抗力导致的网站完全瘫痪,某些大型网站会对整个数据中心进行备份,全球范围内部署灾备数据中心。网站程序和数据实时同步到多个灾备数据中心。

自动化

在无人值守的情况下网站可以正常运行,一切都可以自动化是网站的理想状态。目前大型网站的自动化架构设计主要集中在发布运维方面。

通过减少人为干预,使发布过程自动化可有效减少故障。发布过程包括诸多环节。自动化代码管理,代码版本控制、代码分支创建合并等过程自动化,开发工程师只要提交自己参与开发的产品代号,系统就会自动为其创建开发分支,后期会自动进行代码合并;自动化测试,代码开发完成,提交测试后,系统自动将代码部署到测试环境,启动自动化测试用例进行测试,向相关人员发送测试报告,向系统反馈测试结果;自动化安全检测,安全检测工具通过对代码进行静态安全扫描及部署到安全测试环境进行安全攻击测试,评估其安全性;最后进行自动化部署,将工程代码自动部署到线上生产环境。

此外,网站在运行过程中可能会遇到各种问题:服务器宕机、程序Bug、存储空间不足、突然爆发的访问高峰。网站需要对线上生产环境进行自动化监控,对服务器进行心跳检测,并监控其各项性能指标和应用程序的关键数据指标。如果发现异常、超出预设的阈值,就进行自动化报警,向相关人员发送报警信息,警告故障可能会发生。在检测到故障发生后,系统会进行自动化失效转移,将失效的服务器从集群中隔离出去,不再处理系统中的应用请求。待故障消除后,系统进行自动化失效恢复,重新启动服务,同步数据保证数据的一致性。在网站遇到访问高峰,超出网站最大处理能力时,为了保证整个网站的安全可用,还会进行自动化降级,通过拒绝部分请求及关闭部分不重要的服务将系统负载降至一个安全的水平,必要时,还需要自动化分配资源,将空闲资源分配给重要的服务,扩大其部署规模。

安全

互联网的开放特性使得其从诞生起就面对巨大的安全挑战,网站在安全架构方面也积累了许多模式:通过密码和手机校验码进行身份认证;登录、交易等操作需要对网络通信进行加密,网站服务器上存储的敏感数据如用户信息等也进行加密处理;为了防止机器人程序滥用网络资源攻击网站,网站使用验证码进行识别;对于常见的用于攻击网站的XSS攻击、SQL注入、进行编码转换等相应处理;对于垃圾信息、敏感信息进行过滤;对交易转账等重要操作根据交易模式和交易信息进行风险控制。

架构要素

对于软件架构,维基百科的定义是有关软件整体结构与组件的抽象描述,用于指导大型软件系统各个方面的设计 。系统的各个重要组成部分及其关系构成了系统的架构,这些组成部分可以是具体的功能模块,也可以是非功能的设计与决策,他们相互关系组成一个整体,共同构成了软件系统的架构。

一般说来,除了当前的系统功能需求外,软件架构还需要关注性能、可用性、伸缩性、扩展性和安全性这5个架构要素,架构设计过程中需要平衡这5个要素之间的关系以实现需求和架构目标,也可以通过考察这些架构要素来衡量一个软件架构设计的优劣,判断其是否满足期望。

性能

性能是网站的一个重要指标。很多时候网站性能问题是网站架构升级优化的触发器。可以说性能是网站架构设计的一个重要方面,任何软件架构设计方案都必须考虑可能会带来的性能问题。

优化网站性能的手段非常多,几乎在每个环节都有其优化方案:

  1. 浏览器端:
    1. 可以通过浏览器缓存、使用页面压缩、合理布局页面、减少Cookie传输等手段改善性能。
    2. 使用CDN,将网站静态内容分发至离用户最近的网络服务商机房,使用户通过最短访问路径获取数据。
    3. 可以在网站机房部署反向代理服务器,缓存热点文件,加快请求响应速度,减轻应用服务器负载压力。
    4. … …
  2. 应用服务器端:
    1. 可以使用服务器本地缓存和分布式缓存,通过缓存在内存中的热点数据处理用户请求,加快请求处理过程,减轻数据库负载压力。
    2. 也可以通过异步操作将用户请求发送至消息队列等待后续任务处理,而当前请求直接返回响应给用户。
    3. 在网站有很多用户高并发请求的情况下,可以将多台应用服务器组成一个集群共同对外服务,提高整体处理能力,改善性能。
    4. 在代码层面,也可以通过使用多线程、改善内存管理等手段优化性能。

衡量网站性能有一系列指标,重要的有响应时间、TPS、系统性能计数器等 ,通过测试这些指标以确定系统设计是否达到目标。这些指标也是网站监控的重要参数,通过监控这些指标可以分析系统瓶颈,预测网站容量,并对异常指标进行报警,保障系统可用性。

性能符合预期仅仅是必要条件,因为无法预知网站可能会面临的访问压力,所以必须要考察系统在高并发访问情况下,超出负载设计能力的情况下可能会出现的性能问题。网站需要长时间持续运行,还必须保证系统在持续运行且访问压力不均匀的情况下保持稳定的性能特性。

可用性

网站使用的服务器硬件通常是普通的商用服务器,这些服务器的设计目标本身并不保证高可用,也就是说,很有可能会出现服务器硬件故障,也就是俗称的服务器宕机。因此网站高可用架构设计的前提是必然会出现服务器宕机,而高可用设计的目标就是当服务器宕机的时候,服务或者应用依然可用

网站高可用的主要手段是冗余:

  1. 应用部署在多台服务器上同时提供访问,
  2. 数据存储在多台服务器上互相备份,任何一台服务器宕机都不会影响应用的整体可用,也不会导致数据丢失。

对于应用服务器而言,多台应用服务器通过负载均衡设备组成一个集群共同对外提供服务,任何一台服务器宕机,只需把请求切换到其他服务器就可实现应用的高可用,但是一个前提条件是应用服务器上不能保存请求的会话信息。否则服务器宕机,会话丢失,即使将用户请求转发到其他服务器上也无法完成业务处理

对于存储服务器,由于其上存储着数据,需要对数据进行实时备份(同步副本),当服务器宕机时需要将数据访问转移到可用的服务器上,并进行数据恢复以保证继续有服务器宕机的时候数据依然可用。

除了运行环境,网站的高可用还需要软件开发过程的质量保证。通过预发布验证、自动化测试、自动化发布、灰度发布等手段,减少将故障引入线上环境的可能,避免故障范围扩大。

伸缩性

伸缩性是指通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储需求

衡量架构伸缩性的主要标准:

  1. 是否可以用多台服务器构建集群
  2. 是否容易向集群中添加新的服务器。
  3. 加入新的服务器后是否可以提供和原来的服务器无差别的服务。
  4. 集群中可容纳的总的服务器数量是否有限制。

对于应用服务器集群,只要服务器上不保存数据,所有服务器都是对等的,通过使用合适的负载均衡设备就可以向集群中不断加入服务器。

对于缓存服务器集群,加入新的服务器可能会导致缓存路由失效,进而导致集群中大部分缓存数据都无法访问。虽然缓存的数据可以通过数据库重新加载,但是如果应用已经严重依赖缓存,可能会导致整个网站崩溃。需要改进缓存路由算法保证缓存数据的可访问性。

关系数据库虽然支持数据复制,主从热备等机制,但是很难做到大规模集群的可伸缩性,因此关系数据库的集群伸缩性方案必须在数据库之外实现,通过路由分区等手段将部署有多个数据库的服务器组成一个集群。

至于NoSQL数据库产品,由于其先天就是为海量数据而生,因此其对伸缩性的支持通常都非常好,可以做到在较少运维参与的情况下实现集群规模的线性伸缩。

扩展性

不同于其他架构要素主要关注非功能性需求,网站的扩展性架构直接关注网站的功能需求。网站快速发展,功能不断扩展,如何设计网站的架构使其能够快速响应需求变化,是网站可扩展架构主要的目的。

衡量网站架构扩展性好坏的主要标准就是在网站增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或者很少改动既有业务功能就可以上线新产品。不同产品之间是否很少耦合,一个产品改动对其他产品无影响,其他产品和功能不需要受牵连进行改动。

网站可伸缩架构的主要手段是事件驱动架构和分布式服务。

事件驱动架构在网站通常利用消息队列实现,将用户请求和其他业务事件构造成消息发布到消息队列,消息的处理者作为消费者从消息队列中获取消息进行处理。通过这种方式将消息产生和消息处理分离开来,可以透明地增加新的消息生产者任务或者新的消息消费者任务。

分布式服务则是将业务和可复用服务分离开来,通过分布式服务框架调用。新增产品可以通过调用可复用的服务实现自身的业务逻辑,而对现有产品没有任何影响。可复用服务升级变更的时候,也可以通过提供多版本服务对应用实现透明升级,不需要强制应用同步变更。

安全性

互联网是开放的,任何人在任何地方都可以访问网站。网站的安全架构就是保护网站不受恶意访问和攻击,保护网站的重要数据不被窃取。衡量网站安全架构的标准就是针对现存和潜在的各种攻击与窃密手段,是否有可靠的应对策略。

网站性能优化

网站性能测试

性能测试是性能优化的前提和基础,也是性能优化结果的检查和度量标准。不同视角下的网站性能有不同的标准,也有不同的优化手段。

不同视角下的网站性能:

  1. 用户视角的网站性能.从用户角度,网站性能就是用户在浏览器上直观感受到的网站响应速度快还是慢。
  2. 开发人员视角的网站性能.开发人员关注的主要是应用程序本身及其相关子系统的性能,包括响应延迟、系统吞吐量、并发处理能力、系统稳定性等技术指标。主要的优化手段有
    1. 使用缓存加速数据读取
    2. 使用集群提高吞吐能力
    3. 使用异步消息加快请求响应及实现削峰
    4. 使用代码优化手段改善程序性能
  3. 运维人员视角的网站性能运维人员更关注基础设施性能和资源利用率,如网络运营商的带宽能力、服务器硬件的配置、数据中心网络架构、服务器和网络带宽的资源利用率等。主要优化手段有
    1. 建设优化骨干网
    2. 使用高性价比定制服务器
    3. 利用虚拟化技术优化资源利用等

网站性能测试的主要指标有响应时间、并发数、吞吐量、性能计数器等。

响应时间(RT)

响应时间指应用执行一个操作需要的时间,包括从发出请求开始到收到最后响应数据所需要的时间。响应时间是系统最重要的性能指标,直观地反映了系统的“快慢”。

操作描述响应时间
L1 cache reference一级缓存引用0.5 ns
Branch mispredict分支错误预测5 ns
L2 cache reference二级缓存引用7 ns
Mutex lock互斥锁定100 ns
Main memory reference主存引用100 ns
Compress 1K bytes with Zippy用Zippy压缩1K字节10 μs
Send 2K bytes over 1 Gbps network通过1Gbps网络发送2K字节20 μs
SSD random readSSD随机读150 μs
Read 1 MB sequentially from memory从内存顺序读取1MB250 μs
Round trip within same datacenter同一个数据中心往返0.5 ms
Read 1 MB sequentially from SSD从SSD顺序读取1MB1 ms
Disk seek磁盘寻道10 ms
Read 1 MB sequentially from network从网络连续读取1MB10ms
Read 1 MB sequentially from disk从磁盘顺序读1MB20 ms
Send packet CA->Netherlands->CA在CA向荷兰发包再返回150 ms

测试程序通过模拟应用程序,记录收到响应和发出请求之间的时间差来计算系统响应时间。但是记录及获取系统时间这个操作也需要花费一定的时间,如果测试目标操作本身需要花费的时间极少,比如几微秒,那么测试程序就无法测试得到系统的响应时间。实践中通常采用的办法是重复请求,比如一个请求操作重复执行一万次,测试一万次执行需要的总响应时间之和,然后除以一万,得到单次请求的响应时间。

并发数

并发数指系统能够同时处理请求的数目,这个数字也反映了系统的负载特性。对于网站而言,并发数即网站并发用户数,指同时提交请求的用户数目。与网站并发用户数相对应的还有网站在线用户数(当前登录网站的用户总数)和网站系统用户数(可能访问系统的总用户数,对多数网站而言就是注册用户数)。其数量比较关系为:网站系统用户数>>网站在线用户数>>网站并发用户数

在网站产品设计初期,产品经理和运营人员就需要规划不同发展阶段的网站系统用户数,并以此为基础,根据产品特性和运营手段,推算在线用户数和并发用户数。这些指标将成为系统非功能设计的重要依据

测试程序通过多线程模拟并发用户的办法来测试系统的并发处理能力,为了真实模拟用户行为,测试程序并不是启动多线程然后不停地发送请求,而是在两次请求之间加入一个随机等待时间,这个时间被称作思考时间。

吞吐量

指单位时间内系统处理的请求数量,体现系统的整体处理能力。对于网站,可以用“请求数/秒”或是“页面数/秒”来衡量,也可以用“访问人数/天”或是“处理的业务数/小时”等来衡量。TPS(每秒事务数)是吞吐量的一个常用量化指标,此外还有HPS(每秒HTTP请求数)、QPS(每秒查询数)、EPS(每秒事件处理数)等。

在系统并发数由小逐渐增大的过程中(这个过程也伴随着服务器系统资源消耗逐渐增大),系统吞吐量先是逐渐增加,达到一个极限后,随着并发数的增加反而下降,达到系统崩溃点后,系统资源耗尽,吞吐量为零。『实际上这是个危险的现象,现实中即便是分布式部署也可能会引起服务雪崩(当分布式集群整体压力就很大的时候)』

网站性能优化的目的,除了改善用户体验的响应时间,还要尽量提高系统吞吐量,最大限度利用服务器资源。

性能计数器

性能计数器是描述服务器或操作系统性能的一些数据指标。包括SystemLoad、对象与线程数、内存使用、CPU使用、磁盘与网络I/O等指标。这些指标也是系统监控的重要参数,对这些指标设置报警阈值,当监控系统发现性能计数器超过阈值时,就向运维和开发人员报警,及时发现处理系统异常。

性能测试方法

性能测试是一个总称,具体可细分为性能测试、负载测试、压力测试、稳定性测试。

  1. 性能测试以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统在资源可接受范围内,是否能达到性能预期。
  2. 负载测试对系统不断地增加并发请求以增加系统压力,直到系统的某项或多项性能指标达到安全临界值,如某种资源已经呈饱和状态,这时继续对系统施加压力,系统的处理能力不但不能提高,反而会下降。
  3. 压力测试超过安全负载的情况下,对系统继续施加压力,直到系统崩溃或不能再处理任何请求,以此获得系统最大压力承受能力。
  4. 稳定性测试被测试系统在特定硬件、软件、网络环境条件下,给系统加载一定业务压力,使系统运行一段较长时间,以此检测系统是否稳定。在不同生产环境、不同时间点的请求压力是不均匀的,呈波浪特性,因此为了更好地模拟生产环境,稳定性测试也应不均匀地对系统施加压力。

性能测试是一个不断对系统增加访问压力,以获得系统性能指标、最大负载能力、最大压力承受能力的过程。所谓的增加访问压力,在系统测试环境中,就是不断增加测试程序的并发请求数,一般说来,性能测试遵循如下图所示的抛物线规律。

图中的横坐标表示消耗的系统资源,纵坐标表示系统处理能力(吞吐量)。

  1. 在开始阶段,随着并发请求数目的增加,系统使用较少的资源就达到较好的处理能力(a~b段),
  2. 这一段是网站的日常运行区间,网站的绝大部分访问负载压力都集中在这一段区间,被称作性能测试,测试目标是评估系统性能是否符合需求及设计目标;
  3. 随着压力的持续增加,系统处理能力增加变缓,直到达到一个最大值(c点),这是系统的最大负载点,这一段被称作负载测试。测试目标是评估当系统因为突发事件超出日常访问压力的情况下,保证系统正常运行情况下能够承受的最大访问负载压力;
  4. 超过这个点后,再增加压力,系统的处理能力反而下降,而资源消耗却更多,直到资源消耗达到极限(d点),这个点可以看作是系统的崩溃点,超过这个点继续加大并发请求数目,系统不能再处理任何请求。这一段被称作压力测试,测试目标是评估可能导致系统崩溃的最大访问负载压力。

性能测试反应的是系统在实际生产环境中使用时,随着用户并发访问数量的增加,系统的处理能力。与性能曲线相对应的是用户访问的等待时间(系统响应时间),如下图所示。

在日常运行区间,可以获得最好的用户响应时间,随着并发用户数的增加,响应延迟越来越大,直到系统崩溃,用户失去响应。

性能优化策略

如果性能测试结果不能满足设计或业务需求,那么就需要寻找系统瓶颈,分而治之,逐步优化。

大型网站结构复杂,用户从浏览器发出请求直到数据库完成操作事务,中间需要经过很多环节,如果测试或者用户报告网站响应缓慢,存在性能问题,必须对请求经历的各个环节进行分析,排查可能出现性能瓶颈的地方,定位问题。排查一个网站的性能瓶颈和排查一个程序的性能瓶颈的手法基本相同:

  1. 检查请求处理的各个环节的日志,分析哪个环节响应时间不合理、超过预期;
  2. 然后检查监控数据,分析影响性能的主要因素是内存、磁盘、网络、还是CPU,是代码问题还是架构设计不合理,或者系统资源确实不足。

定位产生性能问题的具体原因后,就需要进行性能优化,根据网站分层架构,可分为Web前端性能优化、应用服务器性能优化、存储服务器性能优化3大类。

web 前端优化

一般说来Web前端指网站业务逻辑之前的部分,包括浏览器加载、网站视图模型、图片服务、CDN服务等,主要优化手段有优化浏览器访问、使用反向代理、CDN等。

浏览器访问优化

  1. 减少http请求,HTTP协议是无状态的应用层协议,意味着每次HTTP请求都需要建立通信链路、进行数据传输,而在服务器端,每个HTTP都需要启动独立的线程去处理。这些通信和服务的开销都很昂贵,减少HTTP请求的数目可有效提高访问性能。减少HTTP的主要手段是合并CSS、合并JavaScript、合并图片。将浏览器一次访问需要的JavaScript、CSS合并成一个文件,这样浏览器就只需要一次请求。图片也可以合并,多张图片合并成一张,如果每张图片都有不同的超链接,可通过CSS偏移响应鼠标点击操作,构造不同的URL。
  2. 使用浏览器缓存对一个网站而言,CSS、JavaScript、Logo、图标这些静态资源文件更新的频率都比较低,而这些文件又几乎是每次HTTP请求都需要的,如果将这些文件缓存在浏览器中,可以极好地改善性能。通过设置HTTP头中CacheControl和Expires的属性,可设定浏览器缓存,缓存时间可以是数天,甚至是几个月。在某些时候,静态资源文件变化需要及时应用到客户端浏览器,这种情况,可通过改变文件名实现,即更新JavaScript文件并不是更新JavaScript文件内容,而是生成一个新的JS文件并更新HTML文件中的引用。使用浏览器缓存策略的网站在更新静态资源时,应采用批量缓步更新的方法,比如需要更新10个图标文件,不宜把10个文件一次全部更新,而是应一个文件一个文件逐步更新,并有一定的间隔时间,以免用户浏览器突然大量缓存失效,集中更新缓存,造成服务器负载骤增、网络堵塞的情况。
  3. 启用压缩在服务器端对文件进行压缩,在浏览器端对文件解压缩,可有效减少通信传输的数据量。文本文件的压缩效率可达80%以上,因此HTML、CSS、JavaScript文件启用GZip压缩可达到较好的效果。但是压缩对服务器和浏览器产生一定的压力,在通信带宽良好,而服务器资源不足的情况下要权衡考虑。
  4. CSS放在页面最上面、JavaScript放在页面最下面浏览器会在下载完全部CSS之后才对整个页面进行渲染,因此最好的做法是将CSS放在页面最上面,让浏览器尽快下载CSS。JavaScript则相反,浏览器在加载JavaScript后立即执行,有可能会阻塞整个页面,造成页面显示缓慢,因此JavaScript最好放在页面最下面。但如果页面解析时就需要用到JavaScript,这时放在底部就不合适了。
  5. 减少Cookie传输一方面,Cookie包含在每次请求和响应中,太大的Cookie会严重影响数据传输,因此哪些数据需要写入Cookie需要慎重考虑,尽量减少Cookie中传输的数据量。另一方面,对于某些静态资源的访问,如CSS、Script等,发送Cookie没有意义,可以考虑静态资源使用独立域名访问,减少Cookie传输的次数。

CDN加速

CDN(ContentDistributeNetwork,内容分发网络)的本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户以最快速度获取数据,即所谓网络访问第一跳,如下图所示。

由于CDN部署在网络运营商的机房,这些运营商又是终端用户的网络服务提供商,因此用户请求路由的第一跳就到达了CDN服务器,当CDN中存在浏览器请求的资源时,从CDN直接返回给浏览器,最短路径返回响应,加快用户访问速度,减少数据中心负载压力。

反向代理

传统代理服务器位于浏览器一侧,代理浏览器将HTTP请求发送到互联网上,而反向代理服务器位于网站机房一侧,代理网站Web服务器接收HTTP请求。如下图所示。

和传统代理服务器可以保护浏览器安全一样,反向代理服务器也具有保护网站安全的作用,来自互联网的访问请求必须经过代理服务器,相当于在Web服务器和可能的网络攻击之间建立了一个屏障。除了安全功能,代理服务器也可以通过配置缓存功能加速Web请求。当用户第一次访问静态内容的时候,静态内容就被缓存在反向代理服务器上,这样当其他用户访问该静态内容的时候,就可以直接从反向代理服务器返回,加速Web请求响应速度,减轻Web服务器负载压力。

有些网站会把动态内容也缓存在代理服务器上,当这些动态内容有变化时,通过内部通知机制通知反向代理缓存失效,反向代理会重新加载最新的动态内容再次缓存起来。此外,反向代理也可以实现负载均衡的功能,而通过负载均衡构建的应用集群可以提高系统总体处理能力,进而改善网站高并发情况下的性能。

应用服务器性能优化

应用服务器就是处理网站业务的服务器,网站的业务代码都部署在这里,是网站开发最复杂,变化最多的地方,优化手段主要有缓存、集群、异步等。

分布式缓存

【读后感】优先考虑使用缓存优化性能。

缓存的基本原理缓存指将数据存储在相对较高访问速度的存储介质中,以供系统处理。一方面缓存访问速度快,可以减少数据访问的时间,另一方面如果缓存的数据是经过计算处理得到的,那么被缓存的数据无需重复计算即可直接使用,因此缓存还起到减少计算时间的作用。

缓存的本质是一个内存Hash表,网站应用中,数据缓存以一对Key、Value的形式存储在内存Hash表中。Hash表数据读写的时间复杂度为 O(1)O(1) 。缓存主要用来存放那些读写比很高、很少变化的数据,如商品的类目信息,热门词的搜索列表信息,热门商品信息等。应用程序读取数据时,先到缓存中读取,如果读取不到或数据已失效,再访问数据库,并将数据写入缓存。

网站数据访问通常遵循二八定律,即80%的访问落在20%的数据上,因此利用Hash表和内存的高速访问特性,将这20%的数据缓存起来,可很好地改善系统性能,提高数据读取速度,降低存储访问压力。

使用缓存对提高系统性能有很多好处,但是不合理使用缓存非但不能提高系统的性能,还会成为系统的累赘,甚至风险。实践中,缓存滥用的情景屡见不鲜——过分依赖低可用的缓存系统、不恰当地使用缓存的数据访问特性等。

  1. 频繁修改的数据如果缓存中保存的是频繁修改的数据,就会出现数据写入缓存后,应用还来不及读取缓存,数据就已失效的情形,徒增系统负担。一般说来,数据的读写比在2:1以上,即写入一次缓存,在数据更新前至少读取两次,缓存才有意义。实践中,这个读写比通常非常高,比如新浪微博的热门微博,缓存以后可能会被读取数百万次。
  2. 没有热点的访问缓存使用内存作为存储,内存资源宝贵而有限,不可能将所有数据都缓存起来,只能将最新访问的数据缓存起来,而将历史数据清理出缓存。如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。
  3. 数据不一致与脏读一般会对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中重新加载。因此应用要容忍一定时间的数据不一致,如卖家已经编辑了商品属性,但是需要过一段时间才能被买家看到。在互联网应用中,这种延迟通常是可以接受的,但是具体应用仍需慎重对待。还有一种策略是数据更新时立即更新缓存,不过这也会带来更多系统开销和事务一致性的问题。
缓存可用性

缓存是为提高数据读取性能的,缓存数据丢失或者缓存不可用不会影响到应用程序的处理——它可以从数据库直接获取数据。但是随着业务的发展,缓存会承担大部分数据访问的压力,数据库已经习惯了有缓存的日子,所以当缓存服务崩溃时,数据库会因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用。这种情况被称作缓存雪崩,发生这种故障,甚至不能简单地重启缓存服务器和数据库服务器来恢复网站访问。『如果直接重启,数据库还是没法承受查询压力。』

缓存雪崩问题该怎么处理?

  1. 事前准备,保证缓存层服务高可用性(分布式缓存,多集群部署、同步)
  2. 发生时,对缓存访问进行资源隔离(熔断)、服务降级,防止缓存层或存储层压力过大导致崩溃。
  3. 事后,Redis数据备份和恢复,以及对缓存做快速预热。

通过分布式缓存服务器集群,将缓存数据分布到集群多台服务器上可在一定程度上改善缓存的可用性。当一台缓存服务器宕机的时候,只有部分缓存数据丢失,重新从数据库加载这部分数据不会对数据库产生很大影响。

缓存预热

缓存中存放的是热点数据,热点数据又是缓存系统利用LRU(最近最久未用算法)对不断访问的数据筛选淘汰出来的,这个过程需要花费较长的时间。新启动的缓存系统如果没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,那么最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫作缓存预热(warmup)。对于一些元数据如城市地名列表、类目信息,可以在启动时加载数据库中全部数据到缓存进行预热。

缓存穿透

如果因为不恰当的业务、或者恶意攻击持续高并发地请求某个不存在的数据,由于缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大压力,甚至崩溃。一个简单的对策是将不存在的数据也缓存起来(其value值为null)。

分布式缓存架构

分布式缓存指缓存部署在多个服务器组成的集群中,以集群方式提供缓存服务,其架构方式有两种,一种是以JBossCache为代表的需要更新同步的分布式缓存,一种是以Memcached为代表的不互相通信的分布式缓存。

JBossCache的分布式缓存在集群中所有服务器中保存相同的缓存数据,当某台服务器有缓存数据更新的时候,会通知集群中其他机器更新缓存数据或清除缓存数据,,但是这种方式带来的问题是缓存数据的数量受限于单一服务器的内存空间,而且当集群规模较大的时候,缓存更新信息需要同步到集群所有机器,其代价惊人。因而这种方案更多见于企业应用系统中,而很少在大型网站使用。

大型网站需要缓存的数据量一般都很庞大,可能会需要数TB的内存做缓存,这时候就需要另一种分布式缓存。Memcached采用一种集中式的缓存集群管理,也被称作互不通信的分布式架构方式。缓存与应用分离部署,缓存系统部署在一组专门的服务器上,应用程序通过一致性Hash等路由算法选择缓存服务器远程访问缓存数据,缓存服务器之间不通信,缓存集群的规模可以很容易地实现扩容,具有良好的可伸缩性。

异步操作

使用消息队列将调用异步化,可改善网站的扩展性。事实上,使用消息队列还可改善网站系统的性能,如下图所示。

sequenceDiagram
    用户->>应用服务器: 发送请求
    应用服务器->>数据库: 保存数据
    应用服务器->>用户: 接收响应
sequenceDiagram
    用户->>应用服务器: 发送请求
    应用服务器->>消息队列: 发送消息
    消息队列->>数据库: 保存数据
    应用服务器->>用户: 接收响应

在不使用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下,会对数据库造成巨大的压力,同时也使得响应延迟加剧。在使用消息队列后,用户请求的数据发送给消息队列后立即返回,再由消息队列的消费者进程(通常情况下,该进程通常独立部署在专门的服务器集群上)从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度远快于数据库(消息队列服务器也比数据库具有更好的伸缩性),因此用户的响应延迟可得到有效改善。

消息队列具有很好的削峰作用——即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。在电子商务网站促销活动中,合理使用消息队列,可有效抵御促销活动刚开始大量涌入的订单对系统造成的冲击。

需要注意的是,由于数据写入消息队列后立即返回给用户,数据在后续的业务校验、写数据库等操作可能失败,因此在使用消息队列进行业务异步处理后,需要适当修改业务流程进行配合,如订单提交后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单,甚至商品出库后,再通过电子邮件或SMS消息通知用户订单成功,以免交易纠纷。

任何可以晚点做的事情都应该晚点再做。

使用集群

在网站高并发访问的场景下,使用负载均衡技术为一个应用构建一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性,如下图所示。

代码优化

网站的业务逻辑实现代码主要部署在应用服务器上,需要处理复杂的并发事务。合理优化业务代码,可以很好地改善网站性能。不同编程语言的代码优化手段有很多,这里我们概要地关注比较重要的几个方面。

多线程

从资源利用的角度看,使用多线程的原因主要有两个:IO阻塞与多CPU。当前线程进行IO处理的时候,会被阻塞释放CPU以等待IO操作完成,由于IO操作(不管是磁盘IO还是网络IO)通常都需要较长的时间,这时CPU可以调度其他的线程进行处理。前面我们提到,理想的系统Load是既没有进程(线程)等待也没有CPU空闲,利用多线程IO阻塞与执行交替进行,可最大限度地利用CPU资源。使用多线程的另一个原因是服务器有多个CPU,在这个连手机都有四核CPU的时代,除了最低配置的虚拟机,一般数据中心的服务器至少16核CPU,要想最大限度地使用这些CPU,必须启动多线程。

服务器上启动多少线程合适呢?假设服务器上执行的都是相同类型任务,针对该类任务启动的线程数有个简化的估算公式可供参考:启动线程数=[任务执行时间/(任务执行时间-IO等待时间)]×CPU内核数。最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。如果任务都是CPU计算型任务,那么线程数最多不超过CPU内核数,因为启动再多线程,CPU也来不及调度;相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能。

多线程编程一个需要注意的问题是线程安全问题,即多线程并发对某个资源进行修改,导致数据混乱。而线程安全Bug又难以测试和重现。

编程上,解决线程安全的主要手段有如下几点。

  1. 将对象设计为无状态对象:所谓无状态对象是指对象本身不存储状态信息(对象无成员变量,或者成员变量也是无状态对象),这样多线程并发访问的时候就不会出现状态不一致,JavaWeb开发中常用的Servlet对象就设计为无状态对象,可以被应用服务器多线程并发调用处理用户请求。而Web开发中常用的贫血模型对象都是些无状态对象。不过从面向对象设计的角度看,无状态对象是一种不良设计。
  2. 使用局部对象:即在方法内部创建对象,这些对象会被每个进入该方法的线程创建,除非程序有意识地将这些对象传递给其他线程,否则不会出现对象被多线程并发访问的情形。『感觉更好的解释是线程独占对象,即常说的ThreadLocal对象』
  3. 并发访问资源时使用锁:即多线程访问资源的时候,通过锁的方式使多线程并发操作转化为顺序操作,从而避免资源被并发修改。随着操作系统和编程语言的进步,出现各种轻量级锁,使得运行期线程获取锁和释放锁的代价都变得更小,但是锁导致线程同步顺序执行,仍可能会对系统性能产生严重影响。
资源复用

系统运行时,要尽量减少那些开销很大的系统资源的创建和销毁,比如数据库连接、网络通信连接、线程、复杂对象等。从编程角度,资源复用主要有两种模式:单例(Singleton)和对象池(ObjectPool)。

单例虽然是GoF经典设计模式中较多被诟病的一个模式,但由于目前Web开发中主要使用贫血模式,从Service到Dao都是些无状态对象,无需重复创建,使用单例模式也就自然而然了。

对象池模式通过复用对象实例,减少对象创建和资源消耗。对于数据库连接对象,每次创建连接,数据库服务端都需要创建专门的资源以应对,因此频繁创建关闭数据库连接,对数据库服务器而言是灾难性的,同时频繁创建关闭连接也需要花费较长的时间。因此在实践中,应用程序的数据库连接基本都使用连接池(ConnectionPool)的方式。数据库连接对象创建好以后,将连接对象放入对象池容器中,应用程序要连接的时候,就从对象池中获取一个空闲的连接使用,使用完毕再将该对象归还到对象池中即可,不需要创建新的连接。

数据结构

早期关于程序的一个定义是,程序就是数据结构+算法,数据结构对于编程的重要性不言而喻。在不同场景中合理使用恰当的数据结构,灵活组合各种数据结构改善数据读写和计算特性可极大优化程序的性能。

以hash算法为例。比较好的字符串Hash散列算法有Time33算法,即对字符串逐字符迭代乘以33,求得Hash值,算法原型为:hash(i)=hash(i1)*33+str[i].Time33虽然可以较好地解决冲突,但是有可能相似字符串的HashCode也比较接近,如字符串“AA”的HashCode是2210,字符串“AB”的HashCode是2211。这在某些应用场景是不能接受的,这种情况下,一个可行的方案是对字符串取信息指纹,再对信息指纹求HashCode,由于字符串微小的变化就可以引起信息指纹的巨大不同,因此可以获得较好的随机散列.

垃圾回收

如果Web应用运行在JVM等具有垃圾回收功能的环境中,那么垃圾回收可能会对系统的性能特性产生巨大影响。理解垃圾回收机制有助于程序优化和参数调优,以及编写内存安全的代码

以JVM为例,其内存主要可划分为堆(heap)和堆栈(stack)。堆栈用于存储线程上下文信息,如方法参数、局部变量等。堆则是存储对象的内存空间,对象的创建和释放、垃圾回收就在这里进行。通过对对象生命周期的观察,发现大部分对象的生命周期都极其短暂,这部分对象产生的垃圾应该被更快地收集,以释放内存,这就是JVM分代垃圾回收。

在JVM分代垃圾回收机制中,将应用程序可用的堆空间分为年轻代(YoungGeneration)和年老代(OldGeneration),又将年轻代分为Eden区(EdenSpace)、From区和To区,新建对象总是在Eden区中被创建,当Eden区空间已满,就触发一次YoungGC(GarbageCollection,垃圾回收),将还被使用的对象复制到From区,这样整个Eden区都是未被使用的空间,可供继续创建对象,当Eden区再次用完,再触发一次YoungGC,将Eden区和From区还在被使用的对象复制到To区,下一次YoungGC则是将Eden区和To区还被使用的对象复制到From区。因此,经过多次YoungGC,某些对象会在From区和To区多次复制,如果超过某个阈值对象还未被释放,则将该对象复制到OldGeneration。如果OldGeneration空间也已用完,那么就会触发FullGC,即所谓的全量回收,全量回收会对系统性能产生较大影响,因此应根据系统业务特点和对象生命周期,合理设置YoungGeneration和OldGeneration大小,尽量减少FullGC。事实上,某些Web应用在整个运行期间可以做到从不进行FullGC。

存储优化

在网站应用中,海量的数据读写对磁盘访问造成巨大压力,虽然可以通过Cache解决一部分数据读压力,但是很多时候,磁盘仍然是系统最严重的瓶颈。而且磁盘中存储的数据是网站最重要的资产,磁盘的可用性和容错性也至关重要。

机械盘 vs 固态盘

机械硬盘是目前最常用的一种硬盘,通过马达驱动磁头臂,带动磁头到指定的磁盘位置访问数据,由于每次访问数据都需要移动磁头臂,因此机械硬盘在数据连续访问(要访问的数据存储在连续的磁盘空间上)和随机访问(要访问的数据存储在不连续的磁盘空间)时,由于移动磁头臂的次数相差巨大,性能表现差别也非常大。

固态硬盘又称作SSD或Flash硬盘,这种硬盘没有机械装置,数据存储在可持久记忆的硅晶体上,因此可以像内存一样快速随机访问。而且SSD具有更小的功耗和更少的磁盘震动与噪声。

在网站应用中,大部分应用访问数据都是随机的,这种情况下SSD具有更好的性能表现。

B+树 vs LSM树

由于传统的机械磁盘具有快速顺序读写、慢速随机读写的访问特性,这个特性对磁盘存储结构和算法的选择影响甚大。为了改善数据访问特性,文件系统或数据库系统通常会对数据排序后存储,加快数据检索速度,这就需要保证数据在不断更新、插入、删除后依然有序,传统关系数据库的做法是使用B+树。

B+树是一种专门针对磁盘存储而优化的N叉排序树,以树节点为单位存储在磁盘中,从根开始查找所需数据所在的节点编号和磁盘位置,将其加载到内存中然后继续查找,直到找到所需的数据。

目前数据库多采用两级索引的B+树,树的层次最多三层。因此可能需要5次磁盘访问才能更新一条记录(三次磁盘访问获得数据索引及行ID,然后再进行一次数据文件读操作及一次数据文件写操作)。但是由于每次磁盘访问都是随机的,而传统机械硬盘在数据随机访问时性能较差,每次数据访问都需要多次访问磁盘影响数据访问性能。

目前许多NoSQL产品采用LSM树作为主要数据结构。

LSM树可以看作是一个N阶合并树。数据写操作(包括插入、修改、删除)都在内存中进行,并且都会创建一个新记录(修改会记录新的数据值,而删除会记录一个删除标志),这些数据在内存中仍然还是一棵排序树,当数据量超过设定的内存阈值后,会将这棵排序树和磁盘上最新的排序树合并。当这棵排序树的数据量也超过设定阈值后,和磁盘上下一级的排序树合并。合并过程中,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。

在需要进行读操作时,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘上的排序树顺序查找。在LSM树上进行一次数据更新不需要磁盘访问,在内存即可完成,速度远快于B+树。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用LSM树可以极大程度地减少磁盘的访问次数,加快访问速度。

RAID vs HDFS

RAID(廉价磁盘冗余阵列)技术主要是为了改善磁盘的访问延迟,增强磁盘的可用性和容错能力。目前服务器级别的计算机都支持插入多块磁盘(8块或者更多),通过使用RAID技术,实现数据在多块磁盘上的并发读写和数据备份。

常用RAID技术有以下几种:

假设服务器有N块磁盘。

  • RAID0: 数据在从内存缓冲区写入磁盘时,根据磁盘数量将数据分成N份,这些数据同时并发写入N块磁盘,使得数据整体写入速度是一块磁盘的N倍。读取时也一样,因此RAID0具有极快的数据读写速度,但是RAID0不做数据备份,N块磁盘中只要有一块损坏,数据完整性就被破坏,所有磁盘的数据都会损坏。
  • RAID1: 数据在写入磁盘时,将一份数据同时写入两块磁盘,这样任何一块磁盘损坏都不会导致数据丢失,插入一块新磁盘就可以通过复制数据的方式自动修复,具有极高的可靠性。
  • RAID10: 结合RAID0和RAID1两种方案,将所有磁盘平均分成两份,数据同时在两份磁盘写入,相当于RAID1,但是在每一份磁盘里面的N/2块磁盘上,利用RAID0技术并发读写,既提高可靠性又改善性能,不过RAID10的磁盘利用率较低,有一半的磁盘用来写备份数据。
  • RAID3: 一般情况下,一台服务器上不会出现同时损坏两块磁盘的情况,在只损坏一块磁盘的情况下,如果能利用其他磁盘的数据恢复损坏磁盘的数据,这样在保证可靠性和性能的同时,磁盘利用率也得到大幅提升。在数据写入磁盘的时候,将数据分成N1份,并发写入N1块磁盘,并在第N块磁盘记录校验数据,任何一块磁盘损坏(包括校验数据磁盘),都可以利用其他N1块磁盘的数据修复。但是在数据修改较多的场景中,修改任何磁盘数据都会导致第N块磁盘重写校验数据,频繁写入的后果是第N块磁盘比其他磁盘容易损坏,需要频繁更换,所以RAID3很少在实践中使用。
  • RAID5: 相比RAID3,方案RAID5被更多地使用。RAID5和RAID3很相似,但是校验数据不是写入第N块磁盘,而是螺旋式地写入所有磁盘中。这样校验数据的修改也被平均到所有磁盘上,避免RAID3频繁写坏一块磁盘的情况。
  • RAID6:如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下(或者运维管理水平比较落后,坏了一块磁盘但是迟迟没有更换,导致又坏了一块磁盘),仍然需要修复数据,这时候可以使用RAID6。RAID6和RAID5类似,但是数据只写入N2块磁盘,并螺旋式地在两块磁盘中写入校验信息(使用不同算法生成)。

在相同磁盘数目(N)的情况下,各种RAID技术的比较如表:

RAID类型访问速度数据可靠性磁盘利用率
RAID0很快很低100%
RAID1很慢很高50%
RAID10中等很高50%
RAID5较快较高(N-1)/N
RAID6较快较高(N-2)/N

RAID技术可以通过硬件实现,比如专用的RAID卡或者主板直接支持,也可以通过软件实现。RAID技术在传统关系数据库及文件系统中应用比较广泛,但是在大型网站比较喜欢使用的NoSQL,以及分布式文件系统中,RAID技术却遭到冷落。例如在HDFS(Hadoop分布式文件系统)中,系统在整个存储集群的多台服务器上进行数据并发读写和备份,可以看作在服务器集群规模上实现了类似RAID的功能,因此不需要磁盘RAID。

HDFS以块(Block)为单位管理文件内容,一个文件被分割成若干个Block,当应用程序写文件时,每写完一个Block,HDFS就将其自动复制到另外两台机器上,保证每个Block有三个副本,即使有两台服务器宕机,数据依然可以访问,相当于实现了RAID1的数据复制功能。当对文件进行处理计算时,通过MapReduce并发计算任务框架,可以启动多个计算子任务(MapReduceTask),同时读取文件的多个Block,并发处理,相当于实现了RAID0的并发访问功能。

HDFS架构如图所示:

在HDFS中有两种重要的服务器角色:NameNode(名字服务节点)和DataNode(数据存储节点)。NameNode在整个HDFS中只部署一个实例,提供元数据服务,相当于操作系统中的文件分配表(FAT),管理文件名Block的分配,维护整个文件系统的目录树结构。DataNode则部署在HDFS集群中其他所有服务器上,提供真正的数据存储服务。

和操作系统一样,HDFS对数据存储空间的管理以数据块(Block)为单位,只是比操作系统中的数据块(512字节)要大得多,默认为64MB。HDFS将DataNode上的磁盘空间分成N个这样的块,供应用程序使用。

应用程序(Client)需要写文件时,首先访问NameNode,请求分配数据块,NameNode根据管理的DataNode服务器的磁盘空间,按照一定的负载均衡策略,分配若干数据块供Client使用。当Client写完一个数据块时,HDFS会将这个数据块再复制两份存储在其他DataNode服务器上,HDFS默认同一份数据有三个副本,保证数据可靠性。

因此在HDFS中,即使DataNode服务器有多块磁盘,也不需要使用RAID进行数据备份,而是在整个集群上进行数据复制,而且系统一旦发现某台服务器宕机,会自动利用其他机器上的数据将这台服务器上存储的数据块自动再备份一份,从而获得更高的数据可靠性。HDFS配合MapReduce等并行计算框架进行大数据处理时,可以在整个集群上并发读写访问所有的磁盘,无需RAID支持。

网站性能对最终用户而言是一种主观感受,性能优化的最终目的就是改善用户的体验。离开这个目的,追求技术上的所谓高性能,是舍本逐末。而用户体验的快或是慢,可以通过技术手段改善,也可以通过优化交互体验改善。
即使在技术层面,性能优化也需要全面考虑,综合权衡。归根结底,技术是为业务服务的,技术选型和架构决策依赖业务规划乃至企业战略规划,离开业务发展的支撑和驱动,技术走不远,甚至还会迷路。

网站高可用架构

网站不可用也被称作网站故障,业界通常用多少个9来衡量网站的可用性,如QQ的可用性是4个9,即QQ服务99.99%可用,这意味着QQ服务要保证其在所有运行时间中,只有0.01%的时间不可用,也就是一年中大约最多53分钟不可用。

  • 网站不可用时间(故障时间)=故障修复时间点故障发现(报告)时间点
  • 网站年度可用性指标=(1网站不可用时间/年度总时间)*100%

故障分是指对网站故障进行分类加权计算故障责任的方法,例如:

分类描述权重
事故级故障严重故障,网站整体不可用100
A类故障网站访问不畅或核心功能不可用20
B类故障非核心功能不可用,或核心功能少数用户不可用5
C类故障以上之外的故障1

故障分的计算公式为:故障分=故障时间(分钟)*故障权重

高可用的完整架构

硬件故障是常态,网站的高可用架构设计的主要目的就是保证服务器硬件故障时服务依然可用、数据依然保存并能够被访问。实现上述高可用架构的主要手段是数据和服务的冗余备份及失效转移,一旦某些服务器宕机,就将服务切换到其他可用的服务器上,如果磁盘损坏,则从备份的磁盘读取数据。

典型的网站分层模型会把架构分为3层:

  1. 应用层: 负责具体业务逻辑处理
  2. 服务层:提供可复用的服务
  3. 数据层: 负责数据的存储和访问

中小网站在实际部署时,常将应用层和服务层部署在一起。

位于应用层的服务器通常为了应对高并发的访问请求,会通过负载均衡设备将一组服务器组成一个集群共同对外提供服务,当负载均衡设备通过心跳检测等手段监控到某台应用服务器不可用时,就将其从集群列表中剔除,并将请求分发到集群中其他可用的服务器上,使整个集群保持可用,从而实现应用高可用。

位于服务层的服务器情况和应用层的服务器类似,也是通过集群方式实现高可用,只是这些服务器被应用层通过分布式服务调用框架访问,分布式服务调用框架会在应用层客户端程序中实现软件负载均衡,并通过服务注册中心对提供服务的服务器进行心跳检测,发现有服务不可用,立即通知客户端程序修改服务访问列表,剔除不可用的服务器。

位于数据层的服务器情况比较特殊,数据服务器上存储着数据,为了保证服务器宕机时数据不丢失,数据访问服务不中断,需要在数据写入时进行数据同步复制,将数据写入多台服务器上,实现数据冗余备份。当数据服务器宕机时,应用程序将访问切换到有备份数据的服务器上。

网站升级的频率一般都非常高,大型网站一周发布一次,中小型网站一天发布几次。每次网站发布都需要关闭服务,重新部署系统,整个过程相当于服务器宕机。因此网站的可用性架构设计不但要考虑实际的硬件故障引起的宕机,还要考虑网站升级发布引起的宕机,而后者更加频繁。

高可用的应用

应用层主要处理网站应用的业务逻辑,因此有时也称作业务逻辑层,应用的一个显著特点是应用的无状态性。所谓无状态的应用是指应用服务器不保存业务的上下文信息,而仅根据每次请求提交的数据进行相应的业务逻辑处理,多个服务实例(服务器)之间完全对等,请求提交到任意服务器,处理结果都是完全一样的。

通过负载均衡进行无状态服务的失效转移

不保存状态的应用给高可用的架构设计带来了巨大便利,既然服务器不保存请求的状态,那么所有的服务器完全对等。当任意一台或多台服务器宕机,请求提交给集群中其他任意一台可用机器处理,这样对终端用户而言,请求总是能够成功的,整个系统依然可用。对于应用服务器集群,实现这种服务器可用状态实时监测、自动转移失败任务的机制是负载均衡

应用服务器集群的Session管理

应用服务器的高可用架构设计主要基于服务无状态这一特性,但是事实上,业务总是有状态的,在交易类的电子商务网站,需要有购物车记录用户的购买信息,用户每次购买请求都是向购物车中增加商品;在社交类的网站中,需要记录用户的当前登录状态、最新发布的消息及好友状态等,用户每次刷新页面都需要更新这些信息。Web应用中将这些多次请求修改使用的上下文对象称作会话(Session)。

集群环境下,Session管理主要有以下几种手段:

  1. Session 复制: 在集群中的几台服务器之间同步Session对象,使得每台服务器上都保存所有用户的Session信息,这样任何一台机器宕机都不会导致Session数据的丢失。当集群规模较大时,集群服务器间需要大量的通信进行Session复制,占用服务器和网络的大量资源,系统不堪负担。而且由于所有用户的Session信息在每台服务器上都有备份,在大量用户访问的情况下,甚至会出现服务器内存不够Session使用的情况。而大型网站的核心应用集群就是数千台服务器,同时在线用户可达千万,因此并不适用这种方案。
  2. Session绑定: Session绑定可以利用负载均衡的源地址Hash算法实现,负载均衡服务器总是将来源于同一IP的请求分发到同一台服务器上。但是Session绑定的方案显然不符合我们对系统高可用的需求,因为一旦某台服务器宕机,那么该机器上的Session也就不复存在了,用户请求切换到其他机器后因为没有Session而无法完成业务处理。
  3. 利用Cookie记录Session: 利用Cookie记录Session也有一些缺点,比如受Cookie大小限制,能记录的信息有限;每次请求响应都需要传输Cookie,影响性能;如果用户关闭Cookie,访问就会不正常。但是由于Cookie的简单易用,可用性高,支持应用服务器的线性伸缩,而大部分应用需要记录的Session信息又比较小。因此事实上,许多网站都或多或少地使用Cookie记录Session。
  4. Session服务器:用独立部署的Session服务器(集群)统一管理Session,应用服务器每次读写Session时,都访问Session服务器。这种解决方案事实上是将应用服务器的状态分离,分为无状态的应用服务器和有状态的Session服务器,然后针对这两种服务器的不同特性分别设计其架构。对于有状态的Session服务器,一种比较简单的方法是利用分布式缓存、数据库等,在这些产品的基础上进行包装,使其符合Session的存储和访问要求。

高可用的服务

可复用的服务模块为业务产品提供基础公共服务,大型网站中这些服务通常都独立分布式部署,被具体应用远程调用。可复用的服务和应用一样,也是无状态的服务,因此可以使用类似负载均衡的失效转移策略实现高可用的服务。

除此之外,具体实践中,还有以下几点高可用的服务策略。

  1. 分级管理: 运维上将服务器进行分级管理,核心应用和服务优先使用更好的硬件,在运维响应速度上也格外迅速。同时在服务部署上也进行必要的隔离,避免故障的连锁反应。低优先级的服务通过启动不同的线程或者部署在不同的虚拟机上进行隔离,而高优先级的服务则需要部署在不同的物理机上,核心服务和数据甚至需要部署在不同地域的数据中心。
  2. 超时设置: 由于服务端宕机、线程死锁等原因,可能导致应用程序对服务端的调用失去响应,进而导致用户请求长时间得不到响应,同时还占用应用程序的资源,不利于及时将访问请求转移到正常的服务器上。在应用程序中设置服务调用的超时时间,一旦超时,通信框架就抛出异常,应用程序根据服务调度策略,可选择继续重试或将请求转移到提供相同服务的其他服务器上。
  3. 异步调用: 应用对服务的调用通过消息队列等异步方式完成,避免一个服务失败导致整个应用请求失败的情况。当然不是所有服务调用都可以异步调用,对于获取用户信息这类调用,采用异步方式会延长响应时间,得不偿失。对于那些必须确认服务调用成功才能继续下一步操作的应用也不合适使用异步调用。
  4. 服务降级: 在访问高峰期,服务可能由于大量的并发调用而性能下降,严重时导致服务宕机。为了保证核心应用和功能的正常运行,需要对服务降级。降级有两种手段: 拒绝服务和关闭服务。拒接低优先级的应用调用,保证高优先级的应用,或者随机拒绝部分请求,让另一部分请求成功。关闭不重要的功能,或是功能中不必要的部分。
  5. 幂等设计: 应用调用服务失败后,会将调用请求重新发送到其他服务器。但是这个失败可能是虚假的失败。比如服务已经处理成功,但是因为网络故障应用没有收到响应,这时候重新提交就会导致服务重复调用。现实中,服务的重复调用是无法避免的,应用层只要没有收到调用成功的响应,就可以认为是调用失败,并重试服务调用。因此,必须在服务层保证服务的调用幂等性。

高可用的数据

不同于高可用的应用和服务,由于数据存储服务器上保存的数据不同,当某台服务器宕机时,数据访问请求不能随意切换到集群中的其他机器上。保证数据存储高可用的手段是数据备份和失效转移机制。数据备份是保证数据有多个副本,任意副本的失效都不会导致数据永久丢失,从而实现数据的完全持久化。而失效转移机制是保证当一个数据副本不可用时,可以快速切换到访问数据的其他副本上。

对于缓存服务的高可用,有两种观点:

  1. 缓存已经是数据服务的重要组成部分,缓存失效可能导致数据库负载过高,甚至宕机,因此,缓存服务需要实现高可用。
  2. 缓存服务不是数据服务,缓存服务器宕机引起的缓存数据丢失导致负载过高应该通过其他手段解决,而不是提高缓存的高可用。如果服务器集群规模过大,单机宕机的数据丢失比例和数据库负载压力变化都较小。

【读后感】实际上,上述两种观点在大型网站中都在应用,例如redis的哨兵机制和集群。

CAP 原理

高可用的数据有一下几个含义:

  1. 数据持久性: 保证数据可持久存储,在各种情况都不会出现数据丢失的问题。为了保证持久性,不但在写入数据时需要写入持久性存储,还需要将数据备份一个或多个副本,存放在不同的物理存储设备上。
  2. 数据可访问性:多数据副本分别存在不同存储设备的情况下,如果一个存储设备损坏,就需要将数据访问切换到另一个数据存储设备上,如果这个过程不能很快完成(或是过程中需要停止终端用户访问数据),那么这段时间数据是不可访问的
  3. 数据一致性: 在数据有多份副本的情况下,如果网络,服务器或程序出现故障,会导致部分副本写入成功,部分副本写入失败,这就会导致副本间数据不一致,数据冲突。

CAP原理认为,一个提供数据服务的存储系统,无法同时满足一致性,可用性和分区耐受性这个三个条件。

在大型网站应用中,数据规模快速扩张。数据的分区耐受性必不可少。在规模变大后,机器数量也会变大,这时网络和服务器故障会频繁出现,要想保证应用可用,就必须保证高可用性。因此在大型网站中,通常会选择强化分布式存储系统的可用性和分区耐受性,而在某种程度上放弃一致性,。一般来说,数据不一致通常出现在高并发写或集群状态不稳的情况下,应用系统需要对数据不一致性有所了解,并进行补偿和纠错。

数据一致性可以分为以下几点:

  1. 数据强一致: 各个副本的数据在物理存储中总是一致的,数据更新操作的结果和响应总是一致的
  2. 数据用户一致:即数据在物理存储中各个副本可以是不一致的,但是用户访问时,通过纠错和校验机制,确定一个一致且正确的数据返回用户。
  3. 数据最终一致:这是数据一致性中较弱的一种,即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复,数据会达到一致。

因为难以满足数据强一致性,网站通常会综合成本、技术、业务场景等条件,结合应用服务和其他的数据监控与纠错功能,是存储系统达到用户一致。

【读后感】在大型网站领域,一般是牺牲一致性,换取高可用。在大数据领域,更多的是牺牲高可用,来保证一致性。例如,在大数据领域常用的组件zookeeper和基于zookeeper构建的大数据中间件。

数据备份

早期的数据备份手段主要是数据冷备,即定期将数据复制到某种存储介质上并存档保管。冷备的优点是简单廉价,成本和难度都比较低,缺点是不能保证数据的最终一致性(检查点保存,会丢失检查点之后的数据)。同时,一般也不能保证数据的可用性,从冷备中恢复数据一般需要较长的时间。

因此,实际业务中还需要进行数据热备,以提供更好的数据可用性。数据热备可以分为两种: 异步热备和同步热备。

异步方式是指多份数据副本的写入操作异步完成,应用程序收到数据服务系统的写操作成功响应时,只写成功了一份,存储系统会异步地写入其他副本(这个过程可能会失败)。

在异步写入方式下,存储服务器分为主存储服务器(Master)和从存储服务器(Slave),应用程序正常情况下只能连接主存储服务器,数据写入时,由主存储服务器的写操作代理模块将数据写入本机存储系统后立刻返回写操作成功响应,然后通过异步线程将写操作数据同步到从服务器。

传统的关系型数据库系统几乎都提供了数据实时同步备份的机制。常见的mysql 数据库热备机制就是主从同步(异步写入)。

同步方式是指多份数据副本写入操作同步完成,即应用程序收到数据服务系统的写操作响应时,多份数据已经写操作完成。但是当应用程序收到数据写操作失败的响应时,可能有部分副本或者全部副本都已经写成功了。(因为网络或者系统故障,无法返回操作成功的响应)。

同步热备具体实现的时候,为了提高性能,在应用程序客户端并发向多个存储服务器同时写入数据,然后等待所有存储服务器都返回操作成功后,再通知应用程序写入成功。在这种情况下,存储服务器没有主从之分,完全对等,更便于管理和维护。存储服务客户端在写多份数据的时候,并发操作,这意味着多份数据的总写操作延迟是响应最慢的那台存储服务器的响应延迟,而不是多台存储服务响应延迟之和。所以其性能和异步热备方式差不多。

【读后感】数据同步不仅仅解决了数据备份功能,还能改善数据服务的性能,

异步写入的方式,常采用读写分离的方式去访问主从数据库,写操作只访问Master数据库,读操作只访问Slave数据库。
同步写入的方式,可以把读请求分散到各个服务器。

失效转移

如果数据服务器集群中任何一台服务器宕机,那么应用程序针对这台服务器的所有读写操作都需要重新路由到其他服务器,保证数据访问不会失败,这个过程叫失效转移。失效转移的操作由3个部分组成:

  1. 失效确认,系统确认一台服务器是否宕机的手段有两种: 心跳检测和应用程序访问失败报告。对于宕机判断,需要经过多次检测进行确认,(如果是程序访问失败,还要再发送一次心跳检测进行确认),以免错误判断服务器宕机,因为一旦失败转移,就意味着数据副本不一致,需要进行一系列的复杂补偿。
  2. 访问转移,确认某台服务器宕机后,就需要将数据读写访问重新路由到其他服务器上。如果存储是对等的(几台服务器数据都一样)那么可随机切换到一台,如果数据是不对等的,需要重新计算路由,选择对应的服务器。
  3. 数据恢复,因为某台服务器宕机所以数据存储的副本会减少,必须将副本的数量恢复到设定值,否则再有服务器宕机时,就会出现无法访问转移,数据丢失的情况。

网站软件质量

网站发布都需要在服务器上关闭原有应用,然后重新部署启动新的应用,整个过程需要不影响用户的使用。网站发布的过程实际上和服务器宕机效果相当。其对系统可用性的影响也和服务器宕机相似。

【读后感】 在发布过程中,每次关闭的服务器都应当是集群中的一小部分。并在发布完可以立刻访问。所以集群中某个功能会出现同时存在两个版本的情况,此时要注意兼容的问题。对于改动较大的业务代码,可以在代码中冗余版本,然后通过开关来控制(即发布完成后,通过配置中心的开发控制对外逻辑版本)。

代码在发布到线上前要经过严格的测试,此外还需要对这个网站功能进行全面的回归测试。

【读后感】单元测试,自动化测试。

即使经过严格的测试,部署到线上服务器后还是会有各种问题,主要是测试环境和线上环境并不相同,因为,在网站发布时,先发布到预发布机器上验证,确认无误后在发布到线上,预发部机器和线上机器的环境应该没有区别,唯一的不同是没有配置在负载均衡服务上,即接收不到外部流量。不过预发也有可能引入问题,因为预发连接的是真实的生产环境,所以某些操作可能会引起不可预期的问题,例如商品售价修改后,忘记复原。

在网站应用中一个处理错误的理念是fast failed,即在系统启动时发现问题,就立刻抛出异常,停止启动,人工介入。

网站应用开发主要是分支开发,主干发布的模式。对于发布,应该尽量实现自动化发布流程,减少人工错误问题。在发布成功后,仍可能发现因软件引入的故障,这时候就要做发布回滚,卸载刚发布的软件,将上一版本的软件包重新发布。由于大型软件集群规模很大,为了应付回滚时间过长问题,会采用灰度发布模式,每天只发布部分服务器,观察运行稳定没有故障,第二天继续发布一部分服务器。

灰度发布也常用于用户测试,监控用户对两个版本的操作,收集用户体验,以确定最终发布版本。

网站运行监控

广义上的网站监控涵盖所有非直接业务行为的数据采集和管理,包括网站用户行为日志,业务运行数据和系统性能数据。

  1. 用户行为数据包括用户的所有操作,操作环境,例如操作系统,页面路径,停留时间等。主要有两种方式收集,服务端日志收集,和客户端日志收集。
  2. 服务器性能监控,主要收集服务器性能指标,如,系统load,内存占用,磁盘IO,网络IO等。
  3. 运行数据,如缓存命中率,平均响应时长,待处理任务数等。

监控数据采集后,除了用于系统性能评价,集群规模伸缩预测等,还可以根据实时监控数据进行风险预警,并对服务器进行失效转移,自动负载调整,最大化利用集群资源。

  1. 系统预警,当服务器某些指标超过某个阈值,意味着系统将可能出现故障,这时候就要对相关人员报警,及时处理。
  2. 失效转移,除了应用程序访问失败时进行失效转移,监控系统还可以在发现故障时,主动通知应用,进行失效转移。
  3. 优雅降级,监控系统实时监控所有服务器的运行状况,如果发现部分应用负载过高,部分应用负载过低,就会适当卸载低负载应用部分服务器,重新安装部分高负载应用,使得整体负载均衡。如果所有应用负载都很高,且负载压力还在增加,就会自动关闭部分非重要功能。

网站伸缩性架构

网站的伸缩性设计可以分为两类,根据功能进行物理分离实现伸缩,和单一功能通过集群实现伸缩。

物理分离可以分为两种情况:

  1. 纵向分离: 将业务处理流程上的不同部分分离部署,实现系统伸缩性。(分层架构的思想)
  2. 横向分离: 将不同业务模块分离部署,实现系统伸缩 (微服务思想)

集群伸缩方式也可以分为两种,应用服务器伸缩和数据服务器伸缩,区别在于对数据状态的管理不同。

应用服务器伸缩

由于应用服务器的无状态特性,伸缩特性的设计主要在请求分发上,即负载均衡器

http重定向负载均衡

http重定向服务器,根据用户的http请求,计算出一台web 服务器地址,并将web服务器地址,写入http重定向响应中返回用户浏览器,浏览器然后重定向到真实地址。

这种负载均衡方式比较简单,但是性能较差,需要两次请求服务器。重定向服务器会成为集群瓶颈。实践中,该方案使用较少。

DNS域名负载均衡

在DNS服务器中配置多个A记录,每次域名解析都会根据负载均衡算法计算一个不同的IP地址返回,这样A记录配置的多个服务器就构成了一个集群,并可以实现负载均衡。

DNS负载均衡的优点时将工作转交给了DNS,同时许多DNS还支持基于地理位置的域名解析,将域名解析成距离用户最近的一个地址。但是DNS域名解析也有缺点,目前DNS是多级解析,每级DNS都可能缓存A记录。当下线某台服务器后,生效时间会很长,这期间会出现用户访问失败。

事实上,大型网站总是部分使用DNS,将DNS作为第一级的负载均衡手段,即域名解析到的服务器实际上同样是提供负载均衡的内部服务器,这组内部服务器会再次进行负载均衡将请求分发。

反向代理负载均衡

反向代理服务器管理一组web服务器,将请求根据负载均衡算法转发到不同的web服务器上。web服务器的响应也通过反向代理服务器返回。由于web服务器不直接对外提供访问,web服务器不需要使用外部IP地址。而反向代理服务器需要配置双网卡和内外两套IP地址。

由于反向代理转发请求在http协议层,也叫应用层负载均衡。其优点是部署简单,缺点是反向代理服务器是中转站,性能可能是瓶颈。

【读后感】通常来说,反向代理服务器只具备负载均衡、转发基本功能,若要需要其他功能,需要增加扩展或提供脚本来实现。API Gateway可以看作特殊的反向代理,是对反向代理服务器功能的扩充,同时API Gateway仅局限于服务API层面,对API做进一步的管理,保护。API Gateway不仅提供了负载均衡,转发功能,还提供了灰度发布,统一认证,熔断,消息转换,访问日志等丰富的功能。

IP 负载均衡

用户请求到达负载均衡服务器后,负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到真实Web服务器的地址,然后将数据包的目标IP地址修改为真实服务器地址,不需要用户进程处理,真实web应用服务器处理完后,响应数据包回到负载均衡服务器。负载均衡服务器再将数据包源地址修改为自身IP地址发送给用户浏览器。

这里的关键在于真实web服务器响应数据包如何返回负载均衡服务器。一种方案是负载均衡服务器修改目标IP的同时,修改源地址将数据包源地址设为自身IP(即源地址转换SNAT),另一种方案是将负载均衡服务器同时作为真实物理服务器集群的网关服务器,这样所有响应数据会到达负载均衡服务器。

IP负载均衡在内核进程完成数据分发,较反向代理有更好的处理性能。但是同样的集群瓶颈在负载均衡服务器的网卡带宽。

数据链路层负载均衡

数据链路负载均衡指在通信协议的数据链路层修改Mac地址进行负载均衡。这种方式又叫三角传输模式。

负载均衡服务器再分发中不修改IP地址,只修改目标Mac 地址,通过配置真实物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致。从而达到不修改数据包源和目的地址就进行分发的目的(直接路由方式)。

在linux平台上的LVS支持链路层负载均衡。

负载均衡算法

具体的负载均衡算法有以下几种:

  1. 轮询: 请求被依次分发到每台服务器上。
  2. 加权轮询:根据应用服务器硬件性能的情况,在轮询的基础上按照权重将请求分发到每个服务器上。
  3. 随机: 请求被随机分配到各个应用服务器。即使机器性能不一致,也可以使用加权随机算法。
  4. 最少连接:记录每个应用服务器正在处理的连接数,将请求分发到最少连接的服务器上。同样,最少连接也可以实现加权最少连接。
  5. 源地址hash: 根据请求IP来源进行hash计算,得到应用服务器。这样同一Ip的请求总是在同一服务器上处理,请求的上下文信息可以存储在这台服务器上。

数据服务器伸缩

由于数据服务器要支持数据存储功能,所以请求必须落在有目标数据的服务器。对于数据服务器,不能使用简单的负载均衡手段,要引入路由算法。

分布式缓存

对于KV缓存,最简单的路由算法就是。余数hash:用服务器数目除以key的hash值,余数就是目标服务器地址。

但是考虑到分布式集群扩容时,如果使用简单的余数hash,会发现已有数据有很大规模不能命中。很显然,这是不能被接受的,当大部分缓存失效时,很容易引起缓存雪崩。对于上述问题,比较流行的算法是一致性hash算法(见一致性Hash算法)。

数据存储服务器集群

和缓存服务器不一样,数据存储服务器集群对数据的持久性和可用性有更高的要求。缓存的目的是加速读取的速度并减轻数据存储服务器的压力。因此部分缓存数据的丢失并不影响业务的正常处理(这部分数据还可以从数据存储服务上获取)。

而数据存储服务器必须保证数据的可靠存储,任何情况下都必须保证数据的可用性和正确性。因此缓存服务器集群的伸缩性架构方案不能直接适用于数据存储服务。存储服务器集群的伸缩性设计相对更复杂一些。具体来说分为关系型数据库和NoSql数据库。

关系数据库集群的伸缩性设计

在这种架构中,多台服务器部署Mysql实例,但是它们的角色有主从之分,数据库的写操作都在主服务器上,由主服务器将数据同步到集群中的其它从服务器。数据读操作即数据分销等离线操作在从服务器上进行。

除了数据库的主从读写分离,前面提到的水平分割和垂直分割也可以在数据库上。这种方式常用于处理大数据量的表,其带来的制约条件是跨库的表不能用于join。

对于上面的分布式关系数据库的访问,业内一般有两种设计思路:

  1. 在客户端中配置好本地表和分布式表的路由关系,由客户端将对分布式表的SQL操作,重新解析,改写,并基于路由找到对应的本地表去执行(重点在于查询时要带上路由key,否则是全路由),然后将结果合并,是轻量级的框架,可以理解为JDBC的增强。
  2. 实现数据库上层的代理服务,适配了数据库的二进制协议,向应用完全透明,直接当数据库使用。在服务内部将sql请求解析,并重写,然后将sql 提交到代理执行模块,当代理执行模块的数据都返回后,将结果合并,并返回给应用程序。

上图就是分布式关系型数据库代理服务的架构。显然代理层的服务器可以看做是无状态的应用服务器,因此,集群伸缩可以简单通过负载均衡来实现。而当mysql 集群需要扩缩容时,要想保证集群扩容后的数据一致性负载均衡,就必须做数据迁移(路由算法结果不一样了),将原来机器中的数据迁移到新的机器上。

具体迁移时,判断哪些数据要迁移可以利用一致性hash算法,在实践中,可以将schema作为数据迁移的单位,而不是每行数据。这样在迁移时可以使用mysql的同步机制。当schema同步完成,且数据一致检查成功时,修改服务器的路由配置,并将原机器上的schema删除。

有一些分布式关系型数据库代理服务支持跨库join操作,但很显然,这样需要在服务器间传递大量数据,对代理服务器的压力较大(有部分代理服务会将不同分布式表路由key相同的schema,放在同一个机器上,这样可以提供基于路由key的join,但是很显然,代理服务器还是要对大数据量的结果做合并)。

NoSQL 数据库的伸缩性设计

NoSQL主要只非关系的,分布式的数据库设计模式。一般而言,NoSQL都放弃了关系型数据库的两大重要基础:以关系代数为基础的结构化查询语言-sql(这个好像后面的又都实现了方言)和事务一致性保证-ACID。以Hbase为例:

HBase是为海量数据存储设计的,HBase的伸缩性主要依赖可分裂的HRegion和可伸缩的分布式文件系统HDFS实现。HBase中数据以HRegion为单位管理(每个HRegion负责存储一段key值的区间),要想访问数据必须找到对应的HRegion,然后将数据读写操作提交给HRegion,由HRegion完成存储层面的数据操作。HRegionServer 是物理服务器,每个HRegionServer上可以启动多个HRegion实例。当HRegion中数据过多,达到阈值时,会分裂成两个HRegion,并将HRegion在集群中迁移,以使HRegionServer负载均衡。

所有HRegion的信息(存储key的值区间,所在HRegionServer地址,端口等信息)都记录在HMaster服务器上,为了保证高可用,HBase启动多个HMaster,并通过Zookeeper选主,应用程序通过Zookeeper获取主Master地址,输入Key值获得Key所在HRegionServer地址,然后请求HRegionServer上的HRegion,获得需要的数据。

数据写入的过程也是一样,HRegion将数据存储在若干个HFile格式的文件中,这些文件被存储在HDFS文件系统上。当HRegion中数据太多时,HRegion(包括HFile)会分裂成两个HRegion,并在集群中迁移,以实现负载均衡。

网站的可扩展架构

扩展性是指在对现有系统影响最小的情况下,系统功能可持续扩展或提升的能力。即系统继承建设稳定,不需要经常变更,应用之间较少依赖和耦合,对需求变更可以敏捷响应。当新增功能时,不需要对现有系统的结构进行修改。而伸缩性更多地指系统能够通过增加减少自身资源规模方式增强自己处理事务的能力。

设计网站可扩展性架构的核心思想是模块化,并在此基础上,降低模块间的耦合性,提高复用性。分层和分割是模块化设计的重要手段,常用于将软件分为若干组件模块,组件模块间以消息传递和依赖调用的方式聚合成系统。主要的聚合方式有分布式消息队列和分布式服务。

分布式消息队列

事件驱动架构: 通过在低耦合的模块间传输事件消息,以保持模块的松散耦合,并借助消息完成模块间合作。消息队列的发布订阅模式,使得消息发送者和消息接受者之间没有直接耦合。

  1. 消息发送者在发送完后,可以直接返回,系统有更好的响应延迟。
  2. 在网站访问高峰,消息可以暂存在消息队列中,等待消费者慢慢消费,减少后端存储的负载压力。

消息生产者应用程序通过远程接口将消息推送给消息队列服务器,消息队列服务器将消息写入本地内存队列后立刻返回成功响应给消息生产者。消息队列服务器根据订阅列表查找订阅该消息的消费者,将消息队列中的消息按照先进先出的顺序通过远程通信接口发送给消费者。

在伸缩性方面,由于消息队列服务器上的数据可以看作是被即时处理,因此类似无状态的服务器。将新服务器加入分布式消息队列集群中,通知生产者服务器修改消息队列服务器列表饥即可。(实际上MQ的本地队列还是会落盘的「例如kafka」,当消费队列扩容时,为了消费队列服务器的负载均衡,还是会进行分区的重分配。又或者MQ的存储抽象出存储层「例如pulsar」来实现伸缩,那么对于计算层就是无状态的,可以简单动态扩容)

分布式服务

分布式服务使用接口分解系统耦合性,不同子系统通过相同的接口描述进行服务调用。随着网站功能的日益复杂,网站应用系统会变成一个巨无霸:

  1. 编译部署困难。
  2. 代码分支管理困难。
  3. 数据库连接耗尽。
  4. 新增业务困难。

解决方案就是拆分模块,独立部署。其中拆分可以分为纵向和横向拆分。

  1. 纵向拆分:大应用拆分为小应用。如果业务独立,可以设计成一个独立应用(分割)。
  2. 横向拆分:将复用的业务拆分为服务,独立部署为分布式服务(分层)。

分布式服务的特点:

  1. 服务注册发现: 分布式服务需要向注册中心提供自身服务接口的属性。请求者从注册中心检索到服务信息后通过对应的协议和服务提供者通信。
  2. 负载均衡: 对于热门服务,分布式服务框架要支持服务使用可配置的负载均衡算法,实现服务提供集群的负载均衡(通常是服务消费者支持负载均衡,即服务元数据中要有足够的信息,也有用网关,nginx,lvs等来实现集中式负载均衡的)
  3. 失效转移。
  4. 高效的远程通信。
  5. 整合异构系统,
  6. 对应于最少侵入。
  7. 版本管理: 升级的窗口期,应当同时提供两个版本的服务供请求者调用。当请求者调用接口升级后才能关闭旧版服务。(当然最好的是前后版本在接口层可以直接兼容)
  8. 实时监控.

分布式服务框架Dubbo:

可扩展的字段

传统的关系型数据库通常使用冗余字段方式来支持字段扩展(还有字段存复合对象,或是列转行等)。

而NoSQL数据库的列簇就是一个解决方案(elasticsearch的对象结构也是个很好的设计,大数据还是要KV化存储)。

开发平台

  • API:API接口是开发平台暴露给开发者使用的一组API,其形式可以是RESTful,WebService,Rpc等。
  • 协议转换:将各种API输入形式转换为内部服务可以识别的形式,并将内部服务的返回封装为API的格式。
  • 安全:除了一般应用需要的身份识别,权限控制等安全手段,开发平台还需要分级的访问带宽限制,保证平台资源被合理使用的同时,保护内部服务不被外部应用拖垮。
  • 审计:此外,还需要记录第三方应用的访问情况,进行监控,计费。
  • 路由:将开发平台的各种访问路由映射到具体的内部服务。
  • 流程:将一组离散的服务组织成一个上下文相关的新服务,隐藏服务细节,提供统一接口给开发者使用。

网站安全架构

常见web 攻击手段:

  • XSS攻击:跨站脚本攻击,通过在正常网页中嵌入恶意脚本来实现。
  • 注入攻击:SQL注入攻击和OS注入攻击。在请求中注入恶意SQL命令。
  • CSRF攻击:跨站点请求伪造。通过跨站请求,以用户身份进行非法操作。
  • ErrorCode: 错误回显,根据网站异常堆栈信息制造非法输入
  • HTML注释:开发人员的HTML注释泄露相关信息
  • 文件上传:上传恶意文件
  • 路径遍历:在请求使用相对路径,遍历系统未开放目录。

如何应对:

  1. web应用防火墙可以探测攻击,并保护web应用程序。
  2. 网站漏洞扫描工具可以用于检测网站漏洞。

信息加密

信息加密技术分为3类:

  1. 单项散列加密:指对数据信息进行散列计算,得到固定长度的输出。这个计算过程时单项的,不可解密。常用于密码保存,即不保存密码明文,而是单向散列加密后存入数据库。为了加强单向散列计算的安全性,还可以给算法加盐,防止彩虹表碰撞。单向散列算法还有个特点,即微小的变化都会导致输出完全不同(常见算法有MD5,SHA)
  2. 对称加密:指加密和解密使用同一个密钥。对称加密的算法简单,加解密效率高,缺点是使用同一个密码,远程通信下如何安全交换是个难题。
  3. 非对称加密:加密和解密使用的密钥不是同一个。其中一个对外公开,称为公钥,另一个只有所有者知道,称为私钥。用公钥加密的信息,必须用私钥解开,用私钥加密的信息只有公钥才能解开。常用于信息安全传输和数字签名。

密钥管理:

  1. 将密钥和算法放在一个独立服务器上。对外提供加解密服务。
  2. 将解密算法放在应用系统中,密钥放在独立服务器中,为了提供安全性,存储时,密钥被分为数片,分别存在不同存储中。

信息过滤

广告和垃圾信息泛滥成灾。常见的信息过滤和反垃圾手段:

  • 文本比配:trie算法,按照状态机过滤。
  • 分类算法
  • 黑名单:Hash过滤,或是布隆过滤器。

风险控制

风控分为机器自动和人工。其中机器自动的技术手段主要有规则引擎和统计模型:

  1. 规则引擎: 规则引擎根据运营人员的规则文件处理输入的数据(历史数据+交易请求)。
  2. 统计模型:由于规则会越来越多,且相互间存在冲突的现象。还可以根据历史数据训练分类算法(如用户+订单+交易+产品+登录等数据加工出不同维度),计算出风险分值。

职场艺术:

  • 发现问题,寻求突破。先融入团队,深入了解,而不是上来就干。
  • 提出问题,寻找支持:
  1. 我们的问题,而不是我的问题
  2. 向上司提封闭式问题(带解决方案),给下属提开放式问题(元芳怎么看)
  3. 解决问题,而不是批评人
  4. 赞同的方式提出问题。
  • 解决问题。
  1. 先解决别人的问题,再解决问题(谈判的艺术)
  2. 适当逃避部分问题(搁置冷处理)

参考

  • [1] 李智慧. 大型网站技术架构:核心原理与案例分析 (Chinese Edition) (p. 67). 电子工业出版社…