高可用高并发系统架构的核心技术要点

最近在看《亿级流量网站架构核心技术》这本书,写的非常不错。本文就其核心要点,做个记录,方便回忆。

拥有亿级流量的网站,肯定会存在高并发场景,同时还要保证高可用,本书针对这两个方面进行了总结概括。

先总结下此类型系统的一些设计原则:

一.设计原则

高并发原则

1.无状态
无状态,实际上是为了方便水平扩展,一台机器扛不住,那么就扩展为多台;为了保证每台服务是无差别的,需要做到服务无状态;可以把一些公共的配置或者状态信息,放在第三方配置中心。

2.拆分
拆分,侠义上可以简单理解为就是垂直扩展。可以按照按照功能维度,将一个系统,拆分多个系统,相互之间进行RPC调用。当然拆分广义上说的不止这些,还包括读写拆分,代码结构分层拆分,分库分表等。

3.消息队列
消息队列(中间件)可以实现服务的解耦,异步处理,流量削峰/缓冲等。
但同时要注意不要让消息队列成为了并发瓶颈,这个时候可以考虑对消息队列进行多个镜像复制。
在使用消息队列时,还要注意消息的生产失败,重复接收等问题。

4.数据异构
对于一些业务聚合的场景,往往需要查询多个表种的数据,或者依赖多个服务接口的数据,这个时候,可以考虑对聚合后的数据进行保存,存入数据库或这缓存。如果有数据更新,可以考虑使用消息队列异步通知更新。

5.缓存
缓存包括浏览器缓存,客户端缓存,CDN(内容分发网络)缓存,Nginx等接入层缓存,应用层缓存,分布式缓存等。合理使用缓存,可以提高服务响应速度,减轻底层服务压力。

6.并发化
在聚合业务场景,服务A依赖B,C,D服务的数据,如果依赖的数据彼此没有相互依赖关系,那么A可以并行请求BCD服务的数据,然后聚合,而不是串行请求。

高可用原则

1.降级
依赖服务不可用时,可执行降级代码,比如缓存种的数据。或者人工进行开关控制,问题发生时,关闭对应模块或者执行老版本代码。

2.限流
限流的目的是放之恶意请求流量,恶意攻击,或者流量超出系统可承受峰值。

3.切流量
要求可以控制流量在多机房,或者多应用实例之间切换,其中一部分服务挂掉后,可以将流量全部切到其他可用服务。

4.可回滚
新上线一版本代码后,如果发现线上出现问题,能做到及时回滚到最近上个版本的代码状态。

上面总结的是设计原则,下面是一些具体的相关技术:

二.高可用

1.负载均衡与反向代理
单机部署的服务在出现问题时,会导致整个业务功能不可用,这个时候可以进行集群部署,使用Nginx等进行负载均衡和反向代理

2.隔离
隔离是将系统或者资源分隔开,目的就是一方出现问题时,不会影响到其他部分。隔离有很多层面,如下:

  • 线程隔离:不同的任务交给不同的线程池处理,比如Hystrix支持线程池隔离;
  • 进程隔离:类似垂直拆分,不同的功能部署在不同的服务上,比如商品和订单两个模块,可以拆分为两个独立的服务,分开部署,避免相互影响;
  • 集群隔离:比如有商品服务集群,有普通交易和秒杀交易,那么就可以为秒杀交易用到的商品服务单独搞一个服务集群,避免秒杀影响其他普通交易;
  • 机房隔离:同一个机房的服务应该只访问当前机房的其他服务,不能访问其他机房;
  • 读写隔离:比如数据库读写分离,查询只访问读库,写入访问写库,增加查询性能;
  • 动静隔离:js/css等静态资源存放在CDN上,节省带宽;
  • 爬虫隔离:识别爬虫,并将其路由到单独的服务集群;
  • 热点隔离:秒杀和抢购,这种热点商品,可以做成单独的系统使用;
  • 资源隔离:硬盘,cpu,网络等资源,可以根据具体业务的负载,进行特定的配置。

3.限流
避免过高并发导致服务宕机,可使用限流措施保护服务;常用的限流算法有令牌桶算法,漏桶算法;具体的可以有应用别限流和分布式限流。

4.降级
在依赖服务不可用时,要使用响应的降级方法,来保证响应,哪怕内容是不准确的;常见的,在调用某个接口时,如果发现接口报错或者超时等不可用时,此时可以降级去访问之前缓存的数据。具体的实现有Hystrix框架。

5.超时重试机制
类似降级过程中,如果发现某次调用接口报错或者超时等不可用时,可以进行多次尝试,因为某次超时可能是网络抖动导致,再请求一次可能就会成功。
注意,读操作重试没有啥问题,写操作重试要注意幂等性问题。
具体的技术有:

  • 代理层超时配置(Nginx和Twemproxy);
  • Web容器超时配置(Tomcat);
  • 一些中间件的超时,比如消息队列的调用超时配置;
  • 数据库连接池的超时配置;包括mysql,redis等;
  • 服务调用超时;
  • 前端Ajax超时

6.回滚机制

  • 事务回滚
  • 代码库回滚
  • 部署版本回滚
  • 数据库数据版本回滚
  • js/css等静态资源的回滚;

7.压测与预案

  • 系统线上线下压测;
  • 系统优化和容灾;
  • 应急预案;

三.高并发

1.缓存
缓存,让数据更接近使用者,提升访问速度,工作机制是先从缓存中读取数据,缓存中没有的,在从慢设备设备上读取数据并同步到缓存中去。

回收算法:

  • FIFO(First In First Out):先进先出算法,先放入缓存的先被移除;
  • LRU(Least Recently Used):最近最少使用算法,使用时间距离现在最久的那个被移除;
  • LFU(Least Frequently Used):最不常用算法,一段时间内使用次数最少的那个被移除。

java缓存:

  • 堆内缓存
  • 堆外缓存
  • 磁盘缓存
  • 分布式缓存

HTTP缓存: 浏览器缓存,节省带宽以提升性能;

HttpClient客户端缓存: 引入httpclent-cache 包;

Nginx HTTP 缓存: Nginx提供了expires,etag,if-modifyed-since指令来实现浏览器缓存控制;

  • 浏览器发起请求,首先到nginx,nginx根据URL在Nginx本地查找是否有代理层本地缓存;
  • 如果nginx没有找到本地缓存,则访问后端获取最新文档,并放入Nginx本地缓存,返回200状态码和最新的文档给浏览器;
  • 如果Nginx找到了本地缓存,首先验证文档是否过期,如果过期,则执行步骤2;如果没过期,则返回特殊状态码如304给浏览器。

多级缓存: Nginx本地缓存+Redis分布式缓存+tomcat堆缓存;

2.连接池

数据库链接池: 有很多实现,C3P0,DBCP,Druid;

HttpClient连接池: 进行HTTP服务访问,HttpClient3.x,HttpClient4.x,HttpClient5.x;

java线程池: 3种实现:

  • ThreadPoolExecutor:标准线程池;
  • ScheduledThreadPoolExecutor:支持延迟任务的线程池;
  • ForkJoinPool:类是ThreadPoolExecutor,但是使用work-stealing的模式,为每个线程创建一个队列。

Tomcat线程池: 参数说明:

  • acceptCount:请求等待队列大小,当没有空闲的线程处理请求时,新来的链接请求将放入等待队列,默认为100;队列满后,新的链接请求将被拒绝。
  • maxConnection:Tomcat能处理的最大并发链接数,当超过后,还是会接收链接并放入等待队列。BIO默认是maxThreads的数量,NIO和NIO2默认是10000,ARP默认是8192.
  • minSpareThreads:线程池最小线程数,默认是10 。
  • maxThreads:线程池最大链接数,默认为200 。

3.异步并发

  • 异步Future:线程池配合Future可以实现并发请求,串行变为并行,主线程阻塞等待。
  • 异步Callback:如HttpAsyncClient使用基于nio的异步I/O模型实现,它实现了Reactor模式。
  • 异步编排CompletableFuture:JDK8提供,可以对多个异步处理进行编排,实现更为复杂的异步处理,内部使用ForkJionPool实现。

4.可扩容

单体应用垂直扩容:增加CPU核数,增加内存硬盘等;

单体应用水平扩容:提供多个镜像 ,使用负载均衡;

应用拆分:按照业务模块把一个大的系统拆分为多个自系统;

数据库拆分
按照业务维度垂直拆分:比如将商品,订单,用户信息放在多个数据库中,需要解决跨库join问题,还要解决分布式事务等问题。

读写分离:解决读多写少时数据库的瓶颈问题。

水平拆分:使用分库分表,按照如ID,用户,时间等维度进行数据拆分;拆分算法可以使用取模,哈希,或者使用数据库路由表。
分库分表可以在应用层或者中间层实现:

  • 中间层:好处是对应用透明,开源的中间件有基于MySql-Proxy开发的360的Atlas,阿里的Cobar,基于Cobar开发的MyCat等;坏处是需要增加人力维护。
  • 应用层:可以直接在JDBC驱动层,DAO框架层,如IBatis/Mybatis/Hibernate/JPA上完成。比如当当的Sharding-jdbc就是在JDBC驱动层实现的。阿里的cobar-client是在iBatis上实现。

分库分表策略:

  • 取模:比如按照主键取模;为了减少扩容时带来的麻烦,可以在初期规划时冗余足够的分库分区表,比如一年规划只需分2个库4个表,可以冗余设计4个库8个表,编号0和1在1号机器,2和3在2号机器,如果遇到性能问题,何以把1号和3号库迁移到新的机器上。
  • 分区:可按照时间分区,比如一月一个表,一年一个库;范围分区,比如0-2000万一个表。分区易于水平扩展,避免数据迁移,缺点是存在热点。
  • 取模+分区:两个组合使用;

数据异构
分库分表带来跨库join,分页排序等问题,可以使用数据异构来解决。

  • 查询维度异构:比如订单库,当对其分库分表后,如果想按照商家维度或者用户维度查询,比较困难,此时可以异构出商家维度和用户维度的新库表,然后查询。
  • 聚合数据异构:比如商品详情页包括商品基本信息,商品属性,商品图片等信息,都是分开存储的,需要查询多个表聚合;此时可以把聚合后的数据存储到Redis中保存,后续查询redis即可。对于聚合后的数据更新,可以使用定时任务或者订阅消息解决。具体的如阿里开源的Canal,基于Mysql的binlog增量订阅消费组件,类似数据的触发器。

任务系统
需要在特定时间执行一些任务。

  • 单机版本:Thread,Timer,ScheduleExecutor,Quartz单机版;
  • 分布式任务:Elastic-Job,xxl-Job。

5.使用队列

应用场景:

  • 异步处理
  • 系统解耦
  • 数据同步
  • 流量削峰

队列类型:

  • 缓冲队列:如log4j的日志缓冲区的实现;
  • 任务队列:java的LinkedBlockingQueue等;
  • 消息队列:ActiveMQ,kafa,RabbitMQ,RoketMQ,Redis;
  • 请求队列:指类似在Web环境下对用户请求排队,从而进行一些特殊的控制:流量控制,请求分级,请求隔离;
  • 数据总线队列:例如数据库变更后需要同步到缓存,或者一个机房数据同步到另外一个机房,此时应该使用总线队列,如阿里的Canal,LinkedIn的databus;
  • 混合队列:MQ+Redis的List;
发布了62 篇原创文章 · 获赞 29 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/csdn_20150804/article/details/104258736