购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.1 系统分析原则
——如何从全局掌控一个大型系统

在开发大型系统时,除了根据业务需求实现相应的功能模块外,还需要从高性能和高可用等多个维度对系统进行设计。本节将从高并发、容错性和可扩展等多个方面对开发大型系统的原则进行阐述,希望能够引发读者对如何从全局掌控一个大型系统设计的思考。

2.1.1 高并发原则

高并发是每个大型项目都无法回避的问题,那么如何保证项目在高并发环境下的正常运行呢?简单地说,可以通过垂直扩展和水平扩展来实现。下面来具体讲解垂直扩展和水平扩展的知识。

垂直扩展:通过软件技术或升级硬件来提高单机的性能。就好比当一头小牛无法拉动货物时,就把小牛换成体能强壮的大牛。

水平扩展:通过增加服务器的节点个数来横向扩展系统的性能。就好比当一头牛无法拉动货物时,就可以把货物拆分,然后用多头牛去拉,即分布式;或者用多头牛一起去拉这一批货物,即集群。

显然,垂直扩展是最快的方法,只需要购买性能更强大的硬件设备就能迅速提升性能。但单机的性能是有极限的,一头牛再厉害也拉不动一座小山。因此,大型互联网系统对高并发的最终解决方案是水平扩展。

具体地讲,在技术层面,可以使用缓存减少对数据库的访问,用熔断或降级提高响应的速度,通过流量削峰等手段在项目的入口限流,先拆分项目或使用微服务技术快速构建功能模块,然后再用Spring Cloud或Dubbo等统一治理这些模块,通过中间件搭建基于读写分离的高可用数据库集群等。在系统的测试阶段,还可以使用JProfiler等工具进行性能分析,寻找性能瓶颈,从而有针对性地优化。

衡量高并发的常见指标包括响应时间、吞吐量或QPS(Query per Second,每秒查询率)、并发用户数等。

2.1.2 容错原则

如果高并发使用不当,就容易造成各种逻辑混乱的情景,因此我们就需要对各种潜在的问题做好预案,确保系统拥有一定的容错性。例如,使用Spring Boot+Redis实现分布式缓存,使用MQ实现分布式场景下的事务一致性,使用MQ、PRG模式、Token等解决重复提交问题,使用“去重表”实现操作的幂等性,使用集群或Zookeeper解决失败迁移问题……

此外,在分布式系统中,网络延迟等问题是不可避免的。一般来讲,可以在长连接的环境下通过“心跳检测机制”来处理。例如,在正常的网络环境下,当用户点击了手机上某个App的“退出”按钮后,就会调用服务端的exit()等退出方法,从而注销用户的状态。如果用户的手机信号中断、关机或处于飞行模式,该如何判断用户的状态呢?除了通过Session有效时长进行判断外,还可以采用心跳检测机制判断:客户端每隔60 s向服务端发送一次心跳,如果服务端能够接收到,就说明客户端的状态正常;如果服务端没收到,就再等待客户端下一个60 s发来的心跳;如果连续 N 次都没接收到某个客户端心跳,就可以认定此客户端已经断线。

为了进一步提高容错性,还可以预先采用“隔离”的手段。例如,对于秒杀等可预知时间的流量暴增情况,就可以提前将秒杀隔离成独立的服务,防止秒杀带来的流量问题影响到系统中的其他服务。当然,如果预计流量的增加不是特别多,也可以使用多级缓存来解决高并发问题。

2.1.3 CAP原则

分布式系统包含了多个节点,多个节点之间的数据应该如何同步?在数据同步时需要考虑哪些因素?CAP原则就给出了这些问题的答案。CAP原则是理解及设计分布式系统的基础,包含了C(Consistency,一致性)、A( Availability,可用性)、P(Partition tolerance,分区容错性)三个部分,三者的具体含义如下。

一致性C:在同一时刻,所有节点中的数据都是相同的。例如,当客户端发出读请求后,立刻能从分布式的所有节点中读取到相同的数据。

可用性A:在合理的时间范围内,系统能够提供正常的服务。换句话说,不会出现异常或超时等不可用现象。

分区容错性P:当分布式系统中的一个或多个节点发生网络故障(网络分区),从而脱离整个系统的网络环境时,系统仍然能够提供可靠的服务。也就是说,当部分节点故障时,系统还能够正常运行。

如图2—1所示,如果客户端发出的写请求成功更新了服务A,但由于网络故障没有更新服务B,那么下一次客户端的读请求应该如何处理?要么允许服务A和服务B中数据不一致,即牺牲数据的一致性;要么在设计阶段,就严格要求服务A和服务B中的数据必须一致,当向任何一个服务中写失败时,就撤销全部的写操作并提示失败,即牺牲了写操作的可用性。

图2—1 一致性与可用性的选择

因此,就有了著名的CAP原则:在任何一个分布式系统中,C、A、P三者不可兼得,最多只能同时满足两个。一般而言,分布式必然会遇到网络问题,分区容错性是最基本的要求。因此,在实际设计时,往往是在一致性和可用性之间根据具体的业务来权衡。

2.1.4 幂等性原则

分布式系统提供的各个模块是通过网络进行交互的,而网络问题会对用户请求服务的次数造成影响。如图2—2所示,在分布式系统中,如果模块A(如商品服务)已经成功调用了模块B(如支付服务),但由于网络故障等问题造成模块B在返回时出错,就可能导致用户因为无法感知模块A是否成功执行,从而多次主动执行模块A,造成模块A的重复执行。例如,当用户在购买商品时,如果在点击“支付”按钮后不能看到“支付成功”等提示,就可能再次点击“支付”按钮,就会造成用户多次支付的异常行为。然而实际上,“支付”操作在用户第一次点击时就已经成功执行了,只是在给用户返回结果时出了错。

图2—2 由于返回失败而导致用户重复支付

实际上,当我们在使用消息队列和异步调用时,都需要考虑触发的动作是否会被重复执行。为了避免这种重复执行的问题,就可以使用幂等性原则。

幂等性原则是对调用服务次数的一种限制,即无论对某个服务提供的接口调用多次或是一次,其结果都是相同的。

对于分布式或微服务系统,为了实现幂等性,可以在写操作之前先通过执行读操作来实现。例如,商品服务在调用支付服务的接口时,只需要严格按照以下步骤执行就能实现幂等性。

(1)读操作:查询支付服务中的支付状态(已支付或未支付)。

(2)写操作:若已支付,直接返回结果;若未支付,先执行支付操作,再返回支付结果。

对于分布式、微服务或单系统等各种系统,还可以使用更加通用的“去重表”方式来实现幂等性,具体操作如下。

(1)每个操作在第一次执行时,会生成一个全局唯一ID,如订单ID。

(2)在“去重表”中查询“1”中的ID是否存在。

(3)如果存在,直接返回结果;如果不存在,再执行核心操作(如支付),并将“1”中的ID存入“去重表”中,最后返回结果。

除了以上介绍的两种方法外,还可以通过CAS算法、分布式锁、悲观锁等方式实现幂等性。

特殊的是,查询和删除操作是不会出现幂等性问题的,查询一次或多次,删除一次或多次的结果都是一样的。

除了幂等性外,还需要注意表单重复提交的问题。二者的主要区别是用户的操作意图不同:幂等性是由于网络等故障,用户不知道第一次操作是否成功,因此发送了多次重复操作,意图在于确保第一次的操作成功;而表单重复提交是指用户已经看到了第一次操作成功的结果,但是由于误操作或其他原因再次点击了“刷新页面”等功能按钮,导致多次发送相同的请求。可以通过Token令牌机制、PRG模式、数据库唯一约束等方法避免表单的重复提交问题。

2.1.5 可扩展原则

项目的规模会随着用户数量的增加而增大,因此大型系统务必要在设计时就考虑到项目扩展的解决方案。可扩展原则要从项目架构、数据库设计、技术选型和编码规范等多方面考量。例如,可以使用面向接口、前后端分离及模块化的编程风格,采用无状态化服务,使用高内聚低耦合的编程规范等,力争在设计时预留一些后期扩展时可能会使用到的接口,或者提前设计好项目扩展的解决方案。

以下是实现可扩展原则的一些具体措施,供读者参考。

定义项目插件的统一顶级接口,在扩展功能时使用方便扩展的集成自定义插件(如MyBatis插件开发的流程)。

使用无状态化的应用服务,避免开发后期遇到Session共享等数据同步问题。

使用HDFS等分布式文件系统,在存储容量不足时迅速通过增加设备来扩容。

合理地设计了数据库的分库分表策略及数据异构方式,就能快速进行数据库扩容。

使用分布式或微服务架构,快速开发并增加新的功能模块。

如果在系统设计时就考虑了可扩展的各种手段,就能在系统遇到瓶颈或业务需求改变时快速做出更改,从而大幅提高开发效率。

再通过横向扩展MapReduce的方法,感受一下良好的扩展性对系统扩展带来的便捷。图2—3所示是MapReduce的组织架构,如果需要通过增加节点个数来提升MapReduce的并行计算性能,就只需要在新的节点上配置好MapReduce相关环境,然后设置一些配置文件即可(通常情况,这些配置文件可以直接从其他Slaver节点复制而来),完全不需要额外的编码工作就能达到性能扩展的目的。

图2—3 通过增加新节点横向扩展MapReduce运算性能

2.1.6 可维护原则与可监控原则

一个设计优良的项目不仅能够加速项目的研发,而且能在项目竣工后提供良好的可维护性与可监控性。因此在设计阶段,要考虑项目的可维护原则与可监控原则。

可维护原则是指系统在开发完毕后,维护人员能够方便地改进或修复系统中存在的问题。可维护原则包含了可理解性、可修改性和可移植性等多方面因素,在上一小节中介绍的可扩展原则也可以归纳为可维护原则中的一个细分领域。通常可以从以下5个方面来实现项目的可维护原则。

(1)项目的日志记录功能完善,易于追溯问题、统计操作情况。

(2)有BugFree等Bug管理工具。

(3)有丰富的项目文档和注释。

(4)统一的开发规范。例如,变量命名要尽量做到见名知意、少用缩写,代码缩进规范,面向接口编程,编码与配置分离,适当地使用设计模式等。

(5)使用模块化的编程模式。

可监控原则是指对系统中的流量、性能、服务、异常等情况进行实时监控。理想的状态是,既有仪表盘形状的图形化全局监控数据,又有基线型用于显示各个时间段的历史轨迹,还有一些关键业务的变动对比图(对比业务变更前后,用户流量的变化情况等)。

此外,还需要对项目中的一些关键技术做性能的监控,确保新技术的引入的确能带来性能的提升。例如,从理论上讲,引入缓存应该能够带来访问速度的提升,但如果缓存对象选取不当、缓存块大小等参数设置得不合适,也许会造成性能不升反降的情形。因此,不能盲目地相信一些技术的广告词(尤其是在第一次使用时),要结合自己的实践、测试和监控情况进行具体的分析。 lt3eMUkpXG9+92+ZoEIjyiyB0G/xM36Ijiie70hqOzX8AEIpeWP+v0KOq2Sb2nli

点击中间区域
呼出菜单
上一章
目录
下一章
×