logo  

Java编程实用经验

Java编程实用经验
作者: 陈安廉

摘要:软件开发进阶系列


如何实现高可用的网站架构


2021-09-14 14:15:09

大型网站的分布式部署,使得位于不同层次的服务器具有不同的可用性设计:


应用层 - 为了应对高并发的访问请求,通常会使用负载均衡设备把一组的服务器组合为一个集群,共同对外提供服务。当负载均衡设备监控到某台服务器不可用时,就将其从集群中剔除,并把请求转发到集群中的其他可用的服务器。

服务层 - 应用层通过分布式服务框架来调用这一层提供的服务。分布式服务框架会在应用层的客户端程序中实现软件级的负载均衡,并通过注册中心监控这些提供服务的服务器。一旦发现某个服务器不可用,会立即通知客户端程序修改服务访问列表,剔除不可用的服务器。

数据层 - 写入数据的同时同步复制数据,把数据写入多台服务器,实现数据的冗余备份。当数据服务器宕机时,会把针对数据的访问切换到拥有备份数据的服务器上。


实现高可用的应用层

应用层是处理业务逻辑,这一层的特点是无状态性。

无状态性指的是:多个服务器之间是完全对等的,即把请求提交给任意一台服务器,处理的结果都是一样的。

1 通过负载均衡实现失效转移

在业务量和数据量都较高的情况下,通过负载均衡,可以将流量和数据分摊到一个集群所组成的多台服务器上,以提高整体的处理能力。目前的负载均衡软、硬件都提供了失效转移功能。所以,如果集群的服务是无状态的对等服务时,负载均衡技术可以保证系统的高可用:

当 Web 服务器集群中的服务器都可用时,负载均衡服务器会把访问请求分发给任意一台服务器进行处理。负载均衡服务器通过心跳检测机制,如果发现某台服务器(比如图示中的 10.0.0.1)失去响应,就会把它从服务器列表中剔除。


为了保证高可用,即使某个应用访问量非常少,也必须至少部署两台服务器,通过负载均衡技术来构建一个小型集群。


2 服务器集群中的 Session 管理

业务总是有状态的,比如交易类网站,需要有购物车记录用户购买的商品。在 web 应用中,这些状态就称为 Session。在单机的情况下,Session 可以由部署在服务器上的 Web 容器(如 Tomcat)进行统一管理。但在使用负载均衡的集群环境中,要保证每次请求都能够获得正确的 Session 就很复杂。

利用独立部署的 Session 服务器或集群,来统一管理 Session。应用服务器每次读写 Session 时,都需要访问 Session 服务器。

这样的方案是把应用服务器,分为无状态的应用服务器与有状态的 Session 服务器。

对于有状态的 Session 服务器,可以这样简单实现:利用分布式缓存、数据库等组件的基础上进行封装,使其符合 Session 的存储与访问的要求。

实现高可用的服务层

可复用的服务模块为产品提供了基础公共服务。这些服务通常都是独立分布式部署,由具体应用进行远程调用。它们也是无状态的服务,因此可以使用类似负载均衡的失效转移策略实现高可用。

还有这些高可用的策略:

1 分级管理

运维层面对服务器进行分级管理,核心应用与服务优先使用更好的硬件。


在服务部署上进行必要的隔离,避免故障的连锁反应:


低优先级的服务通过启动不同的线程或部署在不同的虚拟机上进行隔离。

高优先级的服务需要部署在不同的物理机上,核心服务和数据最好是部署在不同地域的数据中心上。

2 超时设置

在应用中设置调用服务的超时时间,一旦超时,通信框架就抛出异常。应用根据服务调度策略,可以选择继续重试或者将请求转移到提供相同服务的其他服务器上。

3 异步调用

应用对服务的调用,通过消息队列的异步方式实现。应用程序会将用户注册信息发送给消息队列服务器后,立即返回用户注册成功的响应。而多个消费者任务(记录用户信息到数据库、发送注册成功邮件、开通权限)会从消息队列中获取用户注册信息后异步执行。


注意:不是所有的的服务调用都可以使用异步调用模式。比如 “获取用户信息的服务” 或 “必须确认服务调用成功才能继续下一步操作的服务” 等,都不适合使用异步调用模式。

4 服务降级

网站访问的高峰期,服务可能因为大量的并发调用而导致性能下降,严重时甚至会导致服务宕机。所以为了保证核心应用与服务的正常运行,我们必须对某些服务进行降级。降级有两种手段:


拒绝服务 - 拒绝低优先级应用的调用,减少并发数,以确保核心应用;或随机拒绝部分请求,以节约资源。

关闭功能 - 关闭部分不重要的服务,或关闭服务内不重要的功能,以节约资源。比如淘宝在 “双十一” 促销期间就使用了这种方法:它在系统最繁忙的阶段关闭了 “评价”、“确认收货” 等非核心服务,以保证核心的交易服务。

5 冥等性设计

应用调用服务失败后,会把请求发给其他的服务器,但这个失败可能不是真的!比如服务实际已处理成功,但因为网络故障,所以应用没有收到响应,那么这时应用所重新提交的请求,就是在重复调用服务!如果这是一个转账服务,那么后果就很严重啦。


重复调用服务是不可避免的,所以服务层必须保证重复调用服务与调用一次服务的结果是相同的,即服务具有冥等性。


比如转账交易的操作,就需要通过交易编号对调用的服务进行有效性校验,保证调用是有效的才能继续执行。


实现高可用的数据层


对网站而言,最高贵的资产是多年运营积累下来的数据(用户数据、交易数据、商品数据等)。所以保护了数据就是保护了企业的命脉。


保证数据高可用的手段是数据备份和失效转移:


数据备份 - 保证数据有多个副本,任意副本的失效不会导致数据的永久丢失。

失效转移 - 当一个数据副本不可访问时,可以快速地切换到其他副本。

注意:缓存服务不是数据存储服务,缓存服务器的宕机所引起的缓存数据丢失而导致的服务器负载过高的情况,应该通过其他手段进行解决。可以让整个网站共享一个分布式缓存集群,应用只需要向共享缓存集群申请缓存资源即可。这样任何一台缓存服务器宕机所引起的缓存失效,都只会影响到缓存数据的一小部分,而不会对应用的性能和数据库负载造成太大的影响。


1 CAP 原理

为了保证数据的高可用,网站通常会牺牲数据的一致性。


高可用的数据有这几层含义:


数据持久性 - 保证数据可持久存储,即把数据写入可持久性存储的硬件的同时,还需要把数据备份成一个或多个副本。

数据可访问性 -把多个数据副本存储在不同的存储设备下,如果一个存储设备损坏,就需要把对数据的访问切换到另一个数据存储设备上。这个过程要尽量快,让客户不可感知。

数据一致性 - 在数据有多个副本的情况下,如果网络、服务器或软件出现故障,会导致部分副本写入成功,部分写入失败。这样就会造成各个副本之间存在数据不一致的情况。


CPA 原理认为:一个提供数据服务的存储系统无法同时满足数据一致性(Consistency)、数据可用性(Availability )、分区耐受性(Patition Tolerance)这三个条件。


大型网站中,数据规模会快速扩张,因此可伸缩性(分区耐受性)必不可少;规模变大后,机器数量也会变得庞大,这时网络和服务器故障就会频繁出现,所以网站使用了分布式系统保证应用高可用。一般情况下,大型网站大都会强化存储系统的可用性(A)和伸缩性(P),在某种程度上放弃一致性(C)。


CPA 原理告诉我们:在系统设计开发的过程中,如果不恰当地迎合各种需求,就会使设计进入两难境地,难以为继。


数据的一致性有这几个级别:


数据强一致 - 各个副本的数据在物理存储中总是一致的。

数据用户一致 - 各个副本的数据可能是不一致的,但当用户访问时,通过纠错和校验机制,可以确定一个一致的、正确的数据返回给用户。

数据最终一致 - 这是数据一致性中最弱的一种。系统经过一段时间(比较短的时间)的自我修复,数据会达到最终的一致。

数据强一致很难达到,所以网站会综合考虑成本、技术、业务场景等因素,使用数据监控和纠错功能,达到“数据用户一致”。


2 备份数据

早期的数据备份主要是冷备份,即定期地将数据复制到存储介质中(磁带、光盘等)并物理存档保存。


冷备份的优点是简单、廉价。缺点是不能保证数据最终一致。因为数据是定期复制,所以备份设备中的数据比系统的数据来的旧,如果系统数据发生丢失,那么从上一个备份点开始后,更新的数据就会永久丢失,不能恢复。


冷备份作为一个传统的数据保护手段,依然在网站日常运维中使用。但还需要进行数据热备份,以提供更好的数据可用性。


数据热备份有两种:异步热备和同步热备。

2.1 异步热备

异步热备指的是,使用异步方式写入多个数据副本。应用收到存储系统的写操作成功响应时,实际上只成功写入了一份,其他副本由存储系统异步写入(可能会失败):

存储服务器分为主服务器(Master )和从服务器(Slave )。应用只连接到主服务器。写入数据时,由主服务器上的写操作代理模块,把数据写入本机后立即返回写操作成功响应,然后通过异步线程把写操作的数据同步到存储服务器。


关系型数据库的热备机制就是 Master-Slave 同步机制。这样不但实现了数据备份,还改善了性能。因为在实践中,通常采用读写分离的方法来访问数据库。写操作只访问 Master 数据库,读操作只访问 Slave 数据库。


2.2 同步热备

同步热备指的是,同步写入多个数据副本,都写入成功后,应用才会收到写成功响应。如果应用收到写失败响应时(网络或系统故障),可能有部分或全部副本都已写入成功,这时就要进行纠错咯。

为了提高性能,应用的客户端会并发地向多个存储服务器同时写入数据,然后等待所有的存储服务器都返回操作成功的响应后,才会通知应用写操作成功。

这里的存储服务器是完全对等,这样有利于管理与维护。总的写操作延迟是那个响应最慢的存储服务器。

不管是关系型数据库还是 NoSQL 数据库,都提供了数据实时备份功能。


3 失效转移

失效转移指的是,服务器集群中,如果任何一台服务器宕机,那么应用所针对这台服务器的所有读写操作,都需要重新路由到其他服务器,保证服务依然是可用的。

失效转移有这些部分:

1.失效确认

系统通过心跳检测和应用访问失败的报告来确认某台服务器是否宕机:

对于应用程序的访问失败报告,控制中心还需要再发送一次心跳检测对服务器进行确认,以免应用程序判断错误。因为一旦进行失效转移,就会出现存储的多份数据副本不一致,那么后续就要对这种情况进行一系列复杂的纠错操作。

2. 访问转移


确认服务器宕机后,就要把数据的读写访问重新路由到其他的服务器上。这又分为两种情况:


存储完全对等:直接切换到对等服务器上。

存储不对等:重新计算路由,选择健康的服务器。

3. 数据恢复


某台服务器宕机后,数据存储的副本就会减少,这时系统会从健康的服务器上复制数据,把副本的数目恢复到系统设定的值。



高可用质量保证


1 发布网站

网站的发布实际上与服务器的宕机效果相当,因为需要关闭服务器上原有的应用,然后重新部署启动新的应用。
但网站的发布毕竟是一次提前预知的“服务器宕机”,因为过程可以更柔和,对用户的影响可以更小。我们通常使用脚本来完成发布。

因为每次关闭的服务器只是集群中的一小部分,而且在发布完成后可以立即访问,所以整个发布过程并不影响用户的使用。


2 自动化测试

代码在发布到服务器之前需要进行严格的测试。即使发布的新功能都是在原有系统的功能上进行小幅的增加,也需要对整个网站功能进行全面的回归测试。此外,还要针对各种浏览器进行兼容性测试。


大部分网站都是采用 Web 自动化测试技术。比较流行的是 Selenium。它运行于浏览器,可以模拟用户的操作进行测试,因此 Selenium 可以同时完成 Web 功能测试和浏览器兼容性测试。


大型网站会开发自有的自动化测试工具,实现一键部署、生成测试数据、执行测试和生成测试报告等测试过程。测试工程师的编码能力毫不逊于软件工程师哦O(∩_∩)O~


3 预发布过程

即使经过了严格测试,部署到现网后还是有可能出现问题。因为测试环境与现网环境不同,特别是应用可能会依赖的其他服务,诸如数据库、缓存以及第三方的服务(短信网关、网银接口等)。


因此在发布时,可以先把包发布到 “预发布服务器” 上,这样开发与测试工程师就可以在预发布服务器进行验证咯,一般会测试一些典型的业务流程,确认系统没有问题后才会正式发布。


预发布服务器与正式服务器的不同之处是:外部用户无法访问(没有配置在负载均衡服务器上)。

预发布服务器与线上的正式服务器部署在相同的物理环境上(同一个数据中心或同一个机架,如果使用虚拟机,甚至可以部署在同一个物理服务器上),使用相同的线上配置,依赖同样的外部服务。工程师在开发机上配置 hosts 文件绑定域名,这样就可以访问到预发布服务器咯。如果预发布服务器执行的验证通过的话,基本上可以确保部署到线上的正式服务器没有问题。


但有可能会因为预发布验证而引入新的问题。因为预发布服务器连接的是真实的生产环境,所以,所有的预发布验证操作的都是真实有效的数据,这些操作会引起某些不可预期的问题。比如上架了一个商品,可能真的会有用户过来购买,如果不能发货,那就惨了哦。


注意:在应用中应该强调 “快速失败” 的错误理念。即如果在系统启动时发现问题就立刻抛出异常,让工程师可以尽快介入解决问题。


4 代码控制

代码控制的核心问题是如何进行代码管理,即能保证代码发布的版本稳定正确,又能保证不同团队的开发互不影响。


目前大部分网站的源代码版本控制工具是 SVN 与 Git。


SVN 的代码控制发布的方式有两种:


1. 主干开发、分支发布


修改都在主干(trunk)上进行,在需要发布的时候,从主干上拉出一个分支(branch)发布,这个分支就是一个发布版本,如果这个版本发现了 bug,就继续在该分支上修改发布,并将修改合并(merge)回主干,直到下一次发布。


2. 分支开发、主干发布


任何修改都不得在主干上进行。需要开发新功能或修复 bug 时,就从主干上拉下一个分支进行开发,开发完成并测试通过后,合并回主干,然后从主干上发布,主干上的代码永远是最新发布的版本。


“主干开发、分支发布” 的方式,主干代码反映的是目前应用的状态,便于管理和控制,也有利于持续集成。

“ 分支开发、主干发布” 的方式,各个分支独立进行,互不干扰,因此可以同时进行不同发布周期的执行过程。


目前网站应用主要使用的是 “ 分支开发、主干发布” 的方式:



同一个应用,有可能 A 项目发布时, B 项目还在开发中,那么“ 分支开发、主干发布” 的方式,只需要将 A 项目的分支合并回主干即可,这样可以不受 B 项目发布时间的影响。


Git 目前正逐步取代 SVN。虽然 Git 学习成本较高,但它对分布式开发和分支开发等有更好的支持,而且可以更容易在各个开发分支上及时反映出主干上的最新更新(代码变得更容易合并啦)。相信 Git 迟早会成为网站标准版本的控制工具。

5 自动化发布

很多网站选择周四作为发布日,这样一周的前三天可以做好发布准备,后面一天可以挽回错误。如果选择的是周五,那么如果发现了问题,就必须周末来加班咯。


使用 “火车发布模型” 可以有效地控制发布故障、减少发布日的加班问题。


火车发布模型是这样的:可以把应用的发布过程看做是一次火车旅行。旅行路线上有若干个站点,每一站都要进行例行检查。不通过的项目就下车,剩下的项目继续旅行,直到达到终点(应用发布成功)。



火车发布模型是基于规则驱动的流程,所以可以自动化。因为人工干预的越少,自动化程度就会越高,引入故障的可能性也就越小。


6 灰度发布

应用发布成功后,仍可能因为软件问题而引入故障。这时候就要做发布回滚,即卸载刚刚发布的软件,将上一个版本的软件包重新发布,复原系统。


大型网站的服务器集群规模非常大,有的甚至超过一万台。一旦发生故障,回滚也需要花费很长的时间才能完成。所以大型网站一般使用灰度发布模式:即把集群服务器分为若干部分,每天只发布一部分服务器,观察运行稳定没有出现故障,那么第二天再继续发布另一部分的服务器,这样持续几天才会把整个集群都发布完毕。期间如果发现问题,就只需要回滚那些已发布的那部分服务器就好啦O(∩_∩)O~



灰度发布模型也常用于用户测试,即在部分服务器上发布新版本,其余服务器保持旧版本,然后监控用户的操作行为,通过用户的体验报告,就可以比较出用户对新旧两个版本的满意度,以确定最终的发布版本。这也被称为 AB 测试。


运行监控

没有被监控的系统不能上线! 因为运行监控对于网站运维与架构设计优化至关重要。


1 采集监控数据

收集用户行为日志

用户行为日志指的是用户在浏览器所做的所有操作以及用户所在的操作环境(操作系统与浏览器的版本、IP 地址、页面访问路径、页面停留时间等)。


有两种收集对象:


服务端日志 - 开启 web 服务器的日志记录即可。缺点是可能出现信息失真,比如 IP 地址可能是代理服务器的地址。

客户端浏览器日志 - 嵌入专门的 JavaScript 脚本就可以收集用户真实的操作行为。缺点是麻烦,因为需要在页面中嵌入特定的脚本。

大型网站的用户日志数据量惊人,数据存储与计算的压力都很大,所以许多网站都逐步开发了基于实时计算框架 Apache Storm 的日志统计与分析工具。


2.监控服务器性能


运维人员在初始化系统时统一部署,收集服务器性能指标。根据监控到的数据,运维工程师可以合理安排服务器的集群规模,架构师可以及时改善系统的性能和调整伸缩性策略。


目前使用广泛的性能监控工具是 Ganglia,它支持大规模的服务器集群,并支持以图形的方式在浏览器中展示实时的性能曲线。


运行数据报告

应用需要在代码中加入采集运行数据的逻辑,汇总后统一显示。


2 监控管理

系统报警

如果监控的某项指标超过了阈值,那就意味着系统可能将要出现故障,这时就要对相关人员进行报警。


监控管理系统可以配置报警的阈值和值守人员的联络方式(通过邮件、即时通信工具、手机短信等方式),及时发出预警。


失效转移

监控系统在发现故障时应该主动通知应用,及时进行失效转移。


自动优雅降级

优雅降级指的是,网站为了应付突发的访问高峰,会主动关闭一部分功能,释放系统资源。


自动优雅降级的监控系统是一种柔性架构的理想状态:监控系统会实时监控所有的服务器,如果发现某部分的应用负载过高,那么就会适当卸载低负载应用的一部分服务器,重新安装启动高负载的应用,使得应用负载总体均衡。如果所有应用的负载都很高,那么就会自动关闭一部分非重要的功能,保证核心功能的正常运行。

















系统拆分

将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。

缓存

缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发

MQ

MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok 的,这个之前还特意说过。

分库分表

分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 sql 跑的性能。

读写分离

读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库

ElasticSearch

Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。

上面的 6 点,基本就是高并发系统肯定要干的一些事儿,大家可以仔细结合之前讲过的知识考虑一下,到时候你可以系统的把这块阐述一下,然后每个部分要注意哪些问题,之前都讲过了,你都可以阐述阐述,表明你对这块是有点积累的。













如何设计一个高并发系统?


可以分为以下 6 点:

  • 系统拆分

  • 缓存

  • MQ

  • 分库分表

  • 读写分离

  • ElasticSearch