读书笔记:《分布式JAVA应用 基础与实践》 第六章 构建高可用的系统

阅读更多

对于互联网或企业中的大型应用而言,多数要求做到 7*24 小时不间断运行。实际上要完全做到不太可能,但可尽量接近,各大网站或大型应用在总结一年的运行状况时,通常会有当年的可用性为 99.9% 这样的内容。

为实现类似的高可用,要避免系统中出现单点。

6.1 避免系统中出现单点

单点是指系统部署在单台机器上,一旦这台机器出现问题 ( 硬件损坏,网络不通等 ) ,系统就不可用。解决单点最常见的方法是采用集群。采用集群则要系统支持水平伸缩。如何做到水平伸缩,第 7 单会详细阐述。除水平伸缩外,还需做到以下两点:

1. 保证均衡使用集群内的机器

2. 保证当机器出现问题时,用户不会访问到问题机器

6.1.1 负载均衡技术

       为实现上述两点,通常采用负载均衡技术,其中又分为硬件负载均衡和软件负载均衡。前者由硬件芯片完成功能,后者则由软件来实现。

       无论是硬件还是软件负载均衡,都在系统中增加了负载均衡机器,负载均衡机器为避免自己成为单点,通常由两台购成,一台提供服务,一台 standby, 一旦提供服务的机器出现问题, syandby 这台自动接管。

选择实际业务处理机器的方式

负载均衡机器选择实际业务处理机器主要有如下几种方式

1.       随机 (Random) 选择

实现最简单,性能最高。可基本保证均衡。适用于处理各种请求时所需消耗的资源相差不是特别大的情况。

2.       Hash 选择

对应用层的请求信息做 hash ,再分发到相应的机器上,典型的应用场景是图片的加载。对请求的图片的 url hash ,基本可保证每次请求同一台机器,命中缓存,提升性能。不过,由于它要读应用层的信息,也要做 hash ,故需要消耗更多的 CPU 并导致些许的性能下降

3.       (Round-Robin) 选择

即根据地址列表按顺序选择。比随机多了一个同步操作,但这个同步操作非常短,性能损失较少。这种方式应用场景和随机方式差不多,目前的硬件和软件负载都支持,实际使用较多。

4.       按权重 (weight) 选择

根据机器配置不同分配不同权重,以更好地利用机器性能。

5.       按负载 (Load) 选择

即根据实际业务处理机器的负载来选择。适合于业务处理机器在处理多种请求时所需资源相差较大的情况。这种方式要求负载均衡机器定时采集业务处理机器的负载情况,给负载均衡机器增加了负担,且万一采集过程中出现较大延时或采集不到时,很可能造成严重不均衡,实际使用较少。

6.       按连接 (Connection) 选择

根据业务处理机器中连接数的多少来选择,应用于连接数不均衡的场景。这种方式要注意的是,一旦其中一台业务机器重启,且重启瞬间请求量巨大,如 10000 个,那么,这 10000 个请求会同时发到新启动的机器上,导致机器宕掉,因此这种方式实际应用较少

 

除以上的选址方式外,通常还会支持按 cookie 信息绑定访问相同的机器的方式,这样可把用户相关信息绑定到指定服务器上,避免引入分布式缓存等复杂技术,但带来的问题是一旦机器出现故障,在该机器上登录过的用户就要重新登陆了。

此外,实际场景中还有一个典型问题: web server 接到负载均衡机器分派过来的请求后,如可以处理,则分派线程来处理,如已达到最大线程数,则放入队列等待。如果前面的请求处理缓慢,后面分派过来的请求就有可能被丢弃或超时。 Twitter 在此时采用 unicorn 来解决。 Unicorn 可理解为到银行办事时的统一叫号,多窗口办事的方式。 Lighthttpd 也有一个支持这种方式的均衡算法 : Shortest Queue First.

响应返回方式

通常支持以下两种方式将响应返回求用户

1.       通过负载均衡机器返回

最常见的简易方式。这种方式,随着请求量上涨,负载均衡机器所受到的压力会迅速上升,特别是对于请求包小、响应包大的 Web 应用。

2.       直接返回至请求发起方

响应直接返回至请求发起方,以分散负载均衡压力,支持更大请求量。要达到这种效果,须要采用 IP Tunneling DR Direct Routing, 硬件负载均衡中又简写为 DSR:Direct Service Routing )方式。 IP Tunneling 方式要求负载均衡机器和实际业务处理机器都支持 IP Tunneling Direct Routing 方式要求负载均衡机器和实际的业务处理机器在同一个物理网段中,并且不响应 ARP

 

目前大多数系统 都支持 IP Tunniling Direct Routing 对环境要求较高,故通常 IP Tunniling 更适合。

 

除以上通用实现外,硬件负载和软件负载在实现时有一些细节的不同。

硬件负载均衡

硬件负载设备中最为知名的是 F5 Netscalar ,他们实现自动接管的方式是采用心跳线的方式。

硬件负载设备大部分操作都在硬件芯片上直接做(例如 HASH 计算),所以能支撑的量非常高,运行非常稳定,不过有以下两点需要注意

1.       负载设备的流量

一旦负载设备的流量达到上限,就会出现大量访问出错等异常问题,一此要注意监测,一旦接近上限,就必须立即扩充带宽或增加负载设备

2.       长连接

长连接方式的应用采用硬件负载设备很容易出现问题,当某台实际业务服务器重启同时又有大量请求进来时,所有的请求都发到健康的服务器上,并建立起长连接,导致重启后的机器只有很少请求或没有请求,造成严重的负载不均衡。可采用手工断开负载设备上某 VIP 所有连接的方法来避免,或只允许每个长连接执行一定次数的请求,当达阀值后即关闭此连接。

软件负载方案

最常用的为 LVS Linux Virtual Server ),多数情况下采用 LVS+Keepalived 来避免负载均衡机器的单点,实现负载均衡机器的自动接管。也可采用类似硬件负载设备的方式来实现,即采用心跳线 + 高可用软件来实现,其中应用最广的高可用软件为 heartbeat,       默认情况下 heartbeat 通过 UDP 方式来监测。

LVS 外,还用 HAProxy 应用较广。

去中心化实现负载均衡

无论是硬件还是软件负载均衡,都需要在请求的过程中引入了一个中间点,这会对性能、可用性造成一定影响,且由于负载设备在做水平伸缩时,要修改调用方应用,比较麻烦,于是业界出现了基于 Gossip 实现无中间点的软件负载。 Facebook 开源的 Cassandra 就是一个基于 Gossip 实现的无中心的 NOSQL 数据库。

去掉中间点后,请求 / 响应可直接进行,对于性能、可用性都会带来提升。同时,无中间点后,还可以将路由策略放在访问端,从而在不影响性能的情况下实现复杂的路由策略。

以上都是实现本地机器的负载均衡,当系统规模扩大到一定程度后,会要在不同地域建设机房,在不同地域的集群实现负载均衡的技术通常称为全局负载均衡 (Global Load Balance) 。各负载设备的硬件厂商通常提供了一种称为 GSLB(Global Server Load Balance) 的设备来实现,可避免由于一个地方的机房出现故障导致系统不可用,同时 GSLB 还可根据地理位置来选择最近的机器,这也在一定程度上提高了性能。

6.1.2          热备

集群要求系统本身支持水平伸缩,对于系统而言成本偏高,可采用热备作为替代方案。热备的情况下对外服务的机器只有一台,其它机器处理 standby 状态, standby 机器通过心跳机制检查对外服务机器的健康情况,当出现问题时, standby 机器即进行接管。

  除单机故障外,还须考虑整个机房出现不可用的情况,故引入多机房方案

使用多机房

       多机房对技术上的要求较高,难点主要为跨机房的状态同步。包括持久数据的同步、内存数据的同步和文件的同步。当机房不在同一个地方时,面临的最大问题是网络延时和各种异常情况,对实时要求高的应用是一个很大的挑战。

数据库数据的同步通常采用单 master slave 或多 master 方案。多 mashter 方案有多个写入点,实现较为复杂,通常采取两阶段提交、三阶段提交或基于 Paxos 的方式来保持多 master 数据的一致性。此外,还可选择 PNUTS 方式来实现,对多机房方案感兴趣的读者可进一步参考 google 工程师介绍 GAE 后端数据服务的 PPT

文件的同步和内存数据的同步方案和数据库数据同步的方案基本相同。

 

6.2 提高应用自身的可用性

应用通常要满足多种多样的功能要求,尤其是互联网应用在不断添加新功能的同时还必须保持高速发展,应用中难免出现 bug 。在这里介绍一下保障应用自身高可用的常用方法

 

6.2.1 尽可能地避免故障

       要做到这点,一方面要深入理解 JAVA 类库和框架,另一方面则是经验。以下是一些常见故障点形成的可用性设计原则,这些原则如下。

明确使用场景

一种是设计时过多地考虑复用,没仔细分析使用场景的不同而导致出现故障。另一种是设计时没有从场景去考虑,更多的是从纯技术角度去考虑,设计了很多不必要的功能,如不必要的系统扩展、系统功能等,将系统复杂化,容易造成故障。因此设计时应贴近使用场景,保持系统的简单。对复杂的功能,应分解为多个阶段来完成,保持每个阶段的简单。

设计可容错的系统

       要使系统具备高度的容错能力,主要从两方面进行掌控,一是 Fail Fast 原则,二是保证接口和对象设计的严谨。

       Fail Fast 原则是当主流程的任何一步出现问题时,都应快速结束整个流程,而不是等到后续才来处理。如系统启动阶段要从本地加载一些数据放入缓存,如加载失败,则直接退出 JVM ,避免启动后不可用;又如代码中判断传入参数非法时直接抛错。

       接口和对象设计的严谨最容易被忽略。通常我们在对外提供接口或对象时,总是假定一定会按照要求去使用,不采取任何的保护措施,这种情况下非常容易产生问题

设计具备自我保护能力的系统

对所有第三方依赖 ( 如对数据库的依赖、存储设备的依赖、其它系统提供的功能的依赖等 ) 的地方都应保持怀疑的态度,对这些地方做一些保护措施,使第三方出现问题时,对应用本身不产生太大的影响,如只是某功能暂时不可用。

限制使用资源

1.       限制内存的使用

主要是注意对 JVM 内存的使用限制,避免出现频繁 Full GC 或内存溢出的现象。一是注意避免集合过大,二要注意释放已经不用的对象引用,如使用线程池的同时使用了 ThreadLocal, 在线程使用结束后,要进行 threadLocal.set(null) 操作,否则 ThreadLocal 里的对象就会一直等待线程退出后才能回收。

2.       限制文件的使用

典型的是日志文件的使用,一是要控制日志文件的大小,二是要控制写日志的频率。

3.       限制网络的使用

对分布式 JAVA 应用而言,网络的使用是其中重要的一环,对网络资源的限制使用也是非常重要的,具体反映在以下两方面

连接资源

客户端基本都会采用连接池方式避免对服务端创建过多连接,服务端自身也必须控制避免过多连接,以避免出现资源不足导致服务不可用。

操作系统 sendBuffer 资源

在向服务器发送流时,都是先放入操作系统的 sendBuffer 区,而 sendBuffer 区是有限的,因此要适当控制往操作系统 sendBuffer 区写入的流数量,避免出现问题。

另一方面,由于在发送数据时都要先序列化流,流在发送到操作系统 sendBuffer 区前会在 jvm 中存活,因此要注意不要有太多这样的流存在,避免 JVM 内存耗光。

通常采用流控的方法来避免 sendBuffer 区资源和 JVM 内存消耗过多,通常做法有当到达某阀值时直接拒绝发送,或延迟发送。

4.       限制线程的使用

通常使用线程池避免无限制的使用线程。

从其它角度避免故障

除了以上从设计角度的考虑外,编码过程中,还须确保对所用到的框架及 JAVA 类库的实现都有较深的掌握,同时,还要不时地组织代码 review ,结合知识库及团队的智慧尽可能地避免代码中的潜在 bug

另外,测试阶段也要注意功能测试和压力测试,保障系统的基本可用性及高压力下的可用性。

部署阶段,也要注意不要对系统的可用性产生影响,这个说易行难,如一个典型的部署步骤为先停集群中一半的机器,更新应用包,然后重启。如果更新或启动的过程太长,另一半提供服务的机器可能就无法支撑全部的流量,最终导致宕机,因此要注意尽量平滑部署。另外,尽量做到自动化部署,虽然这点比较困难。

 

6.2.2 及时发现故障

无论系统怎样完善,要完全避免故障基本不太可能,因此及时发现故障就至关重要。因此,需要对系统的日志的记录和分析来做报警,包括单机状况的报警、集群状况的报警及关键数据的报警。

单机状况的报警用于报告产生了致使影响的点,如 CPU 使用率过高,某功能点失败率过高,依赖的第三方系统连接出现问题等。

集群状况的报警,通常是在集群的访问状况、响应状况等指标和同期或基线指标对比出现大偏差时发现。

关键数据的报警通常针对系统中的关键功能,如交易类的系统,其最关键的数据是实时交易额,因此当交易额数据和基线指标对比出现大偏差时,就需要报警。

即时发现故障在各大互联网公司中都非常重视,从 Twitter Facebook 、盛大、 51.com 等公开的 PPT 来看,可以看到它们都拥有一套强大的监测系统,通常具备以下特征:

1.       有系统依赖关系的分析,便于找到故障的 root cause

2.       有系统全局状态图显示,既便于找到故障的 root cause ,也能根据故障点找到目前所影响的功能点

3.       能根据报警规则、级别进行报警,对单机直接处理,避免单机扩散到全局

4.       根据报警信息的记录,跟进记录分析报警的原因及后续的处理步骤,方便为将来再次出现时更快速的处理。

 

6.2.3 及时处理故障

       常见的故障快速处理措施有:

1.       自动修复

通过学习总结发生过的各类故障处理措施,将来再发生时自动智能化处理。难度较大,可先只让系统只提出建议,再逐步完善。

2.       执行风险点应对措施

手工修复是最保险的方式,但机器数量太多的情况下比较麻烦,可通过一个集中点发送命令给所有机器,快速修复故障。

3.       全局资源调整

当某集群的压力过大时,可通过调整资源来重新平衡全局的资源,保障系统的可用性。更先进的是根据系统 QoS Quality of Service )来自动平衡全局资源,所谓 QoS 通常是指每秒需要支撑多少的请求量,但 QoS 也可能是随时段不同是变化的指标。

4.       功能降级

在无法快速修改的情况下,先把系统的某些功能关闭,以保证核心系统的可用。

5.       降低对资源的使用量

如正常情况下数据库连接池最大是 10 个,出故障时调整为 5 个,同时缩短数据库连接的等待时间,又或者是在资源紧张的情况下关闭消耗资的操作

 

处理故障后,应注重分析和总结故障发生的根本原因,放入知识库,一方面是为了再碰到类似故障时能自动处理,另一方面是为了积累经验,形成更多的保障可用性的设计原则, review 原则等,尽可能地避免故障再次发生。

 

6.2.4 访问量及数据量不断上涨的应对策略

对于访问量不断上涨的情况,通常应对的措施是拆分系统和水平伸缩,拆分系统后简化了各个系统的功能,并且使其拥有更多的资源,从而提升其所能支撑的用户访问量。通常系统依照功能进行拆分,例如 eBey 将系统拆分为交易、商品、评价等。

随着数据量的不断上涨,通常采取的是拆分数据库、拆分表及读写分离。这些技术将在下一章中详细介绍。

 

你可能感兴趣的:(Java,读书,网络应用,企业应用,应用服务器)