本篇是读《亿级流量网站架构核心技术》的一些总结;可以作为在实际项目搭建过程中架构核心点实施的扩展发散或是作为一个项目架构的参考
限流
限流算法
令牌桶
-
固定速率生成令牌
-
桶满时新加的令牌丢弃
-
批量获取的时候,如果令牌数不够,丢弃请求或缓冲区等待
-
-可以应对请求量突发增加,Guava 提供RateLimiter实现
漏桶
-
漏桶为容量,常量流出请求,容量可以任意速率填满,如果溢出则丢弃
-
-可以使流量平滑过度
计数器
-
超出设定的并发阈值,则进行限流
-
总访问数限流(秒杀)
-
AtomicLong
-
Semaphore
-
-
时间段限流
- 对时间进行hash,做一个缓存计数,设置超时
-
分布式限流方案
- Redis + Lua 或 Nginx + Lua
超时、重试机制
读服务
天然适合重试
写服务
需要保证服务的幂等性
网络连接超时
代理层超时与重试
-
Haproxy、Nginx、Twemproxy(Redis分片代理)
-
Nginx
-
代理超时设置
-
客户端超时
-
DNS解析超时
-
Web容器超时
- Tomcat、Jetty
中间件客户端超时与重试
- Dubbo、MQ、HttpclientHttpclient
数据库客户端超时
- MySql、Oracle
业务超时
-
订单取消、限时活动
-
Future#get
异步并发
异步是针对CPU和IO的,是指当IO没有就绪时要让出CPU来处理其他任务。Java中真正实现异步化是非常困难的,大多数场景并不是真正的异步化
异步并发并不能使响应变得更快,更多是为了提升吞吐量、对请求更细粒度的控制
同步阻塞调用
- 即串行调用,响应时间为所有依赖服务的响应时间总和
异步Future
- 并发发出N个请求,等待最慢的一个返回
异步Callback
- 通过回调机制实现,提升吞量
异步编排CompletableFuture
- JDK8或以上,内部使用ForkJoinPool实现异步处理
Hystrix
熔断、降级
- HystrixCircuitBreaker#allowRequest实现
采样统计(内存中存储)
计数统计
- BucketedCounterStream,时间滚转采样分组
最大并发统计
- RollingConcurrencyStream
延时百分比统计
- RollingDistributionStream、HdrHistogram
Turbine + Hystrix-Dashboard实现可视化统计数据
请求合并
- 支持将多个单请求转换为一个批量请求;调用过程中使用#queue。批量口查询结果会回调返回到相应的单个请求口
压测与预案
系统压测
线下压测
JMeter、Apache ab
- 仿真度不高,数据只能作为参考
全链路压测
线上压测
数据仿真度
-
仿真压测
- 程序模拟请求
-
引流压测
- 可以通过TCPCopy复制线上流量进行压测
读、写
-
读压测
- 商品的详情,价格
-
写压测
- 下单;尽量回滚或删除写入数据
-
混合压测
业务服务
-
隔离集群压测
- 可以先将需要压测的集群从线上摘除,再将线上流量引流到该集群
-
线上集群压测
- 直接压测线上集群,风险高
连接池、线程池
数据库连接池
C3P0
DBCP
Druid
HikariCP
切记要设置destroy-method=“close”,否则多次重启tomcat后旧的数据库连接池的连接不会释放
建议需要有熔断和快速失败机制
HttpClient连接池
-
3.x、4.x、5.x API完全不兼容
-
只有在开启长连接时才是真正的连接池,如果是短连接,只作为一个信号量来限制总请求数
-
HttpClient是线程安全的,不要每次使用创建一个
-
使用连接池时,要尽快消费响应体并释入连接到连接池
连接复用条件还很苛刻,使用的时候要格外注意
JVM设置线程栈大小
- -Xss128k
线程池大小配置
-
利特尔法则
-
实际业务压测
java实现
-
ThreadPoolExecutor
- 标准线程池
-
ScheduledThreadPoolExecutor
- 支持延迟任务的线程池
ForkJoinPool
- 使用work-stealing算法,使得空闲线程可以窃取其它队列的任务去处理
队列术
异步处理
系统解耦
- 不需要实时处理、不需要强一致
数据同步
流量削峰
- 缓存+队列将数据流量流削峰;秒杀系统下单服务的应用场景
缓冲队列
- 使用缓冲队列应对突发流量时,并不能使处理速度变快,而是使处理速度变平滑
任务队列
- 不需要与主线程同步执行的任务扔到任务队列进行异步处理
消息队列
订阅模式
-
点对点
-
发布订阅
双写模式
- DB写入和MQ发送在同一个线程处理;保证数据最终一致
单写DB
- 可以订阅数据库日志机制进行业务处理;不存在数据不一致情况
通过消息队列可以实现异步处理、系统解耦和数据异构
请求队列
- 可以实现流量控制、请求分级、请求隔离;提高系统的可用性,一般用于前端接入层
数据总线队列
- 使用场景只是数据维度的同步,阿里的otter,全量离线数据同步 kettle
混合队列
- 应用层按照不同维度发送MQ,下游应用接收到该消息后会将其放入Redis中,使用RedisList来存储这些任务,消息被消费后再次发送MQ出去;使用Redis队列的主要原因是想提升消息堆积能力和并发处理能力
分布式服务隔离机制
线程
- 项目折分服务化
进程
- 负载均衡
集群
- 对服务化分组
读、写隔离
动、静隔离
- 路由服务接口,静态js/css路由CDN
爬虫隔离
- user-agent路由方案
热点隔离
- 秒杀、抢购系统单独部署
资源隔离
- 硬件资源
构建需求响应式
单品页技术架构发展
1.0
- DB+memcached
2.0
- 静态化HTML
3.0
- 多维度数据异构
详情页架构设计原则
数据闭环
- 数据都在自己的系统里维护、自我管理,不依赖于任何其他系统。包括:数据异构、数据原子化、数据聚合、数据存储
数据维度化
- 按照商品自身维度和作用进行维度化,进行更有效地存储和使用
worker无状态化+任务化
- 数据异构和数据同步worker无状态设计;任务的多队列化;任务副本队列用来执行修正逻辑回放
异步化+并发化
多级缓存化
动态化
-
数据获取动态化
-
模板渲染实时化
-
重启应用秒级化
弹性化
- 自动扩容
降级开关
扩容
单体应用垂直扩容
- 通过硬件来扩容
单体应用水平扩容
- 部署更多的镜像
应用拆分
- 单体应用垂直和水平扩容都不能解决问题的时候,需要采取应用拆分
数据拆分
单库查询要改为跨库查询
- 全局表、ES搜索
读、写分离
水平拆分;分库分表
-
策略
-
取模
-
可以按照主键哈希取模进行分库分表
-
扩容:成倍增量,每一个旧节点与之对应一个或一组哈希后的新节点,只需要把对应分组的数据复制迁移就可以了
-
优点:数据热点分散
-
缺点:按照非主键维度进行查询时需要跨库/跨表查询
-
-
分区
-
时间段分区
- 例:一个月一张表,一年一个库
-
数据量分区
- 每2000万记录一个表
-
优点:易于水平扩展
-
缺点:存在热点问题
-
-
路由表
-
-
注意点
-
应用层支持,还是通过中间件层
-
分库分表的算法是什么
-
join是否支持,排序分页是否支持,事务是否支持
-
中间件层的实现有奇虎360的Atlas、阿里的Cobar、Mycat;对oracle支持低
-
应用层的实现有当当的sharding-jdbc,阿里的cobar-client;对oracle支持低
-
数据异构
-
查询维度异构
-
聚合据异构
垂直拆分;宽表拆子表
缓存的应用
-
数据最终一致
-
缓存的同步写
- 性能差,数据一致性好
-
缓存的异步写
- 性能好,数据一致性有延时,可以把用户指到同一集群,避免多次刷新看到的数据不一致
任务系统扩容
简单任务
- 可以使用Thread死循环;周期性的任务可以使用Timer
分布式任务
- 可以选择Quartz集群版、tbschedule、elastic-job
回滚机制
事务回滚
分布式事务
-
最终一致性
-
事务表
-
消息队列
-
补偿机制
-
TCC模式
-
Sagas模式
-
-
-记录事务日志
-
-批处理任务核验
代码库回滚
SVN
- 集中版本控制系统
GIT
- 分布式版本控制系统
部署版本回滚
部署版本化
- 留存历史发布包,以备回全量主机回滚
小版本增量发布
- 部分主机部署验证后,全量主机发布
大版本灰度发布
- 新、老版本共存
架构升级并发发布
- 新、老版本共存
例:新、老共存迁移部署比例1%->10%->50%->100%
静态资源版本回滚
数据版本回滚
全量回滚
- 保存的数据多,回滚方便
增量回滚
- 保存数据少,需要逐级回溯
应用级缓存
五分钟法则
- 数据的访问周期在5分钟以内则存放在内存中,否则应该存放在硬盘中。具体由来可以参考:https://blog.csdn.net/pennyliang/article/details/5903181
局布性原理
- 时间局布性,空间局布性。具体可参考:http://www.cnblogs.com/jqctop1/p/4714116.html
缓存命中率
- 可以做为评定缓存优劣的标准
回收策略
基于空间
- 达到设定空间上限时,指定策略删除
基于容量
- 达到设定记录条数上限时,指定策略删除
基于时间
-
TTL(Time To Live)
- 存活期,超过设定时间段内的数据全部视为过期
-
TTI(Time To Idle)
- 空闲时期,数据在设定时间内没有被访问过视为过期
基于Java对象引用
- JVM回收机制
回收算法
FIFO(First In First Out)
- 先进先出算法
LRU(Least Recently Used)
- 最近最少使用算法,使用时间距离当前时间最长的数据将被移除;Guava Cache、Ehcache
LFU(Least Frequently Used)
- 最不常用算法,约定时间段内使用频率最少的数据将被移除
java缓存类型
堆缓存
-
优点:不需要序列化
-
缺点:GC暂停时间变长
-
重启需要重新加截
-
实现:Guava Cache、Ehcache 3.x、MapDB
堆外缓存
-
优点:可以有更大的缓存空间,GC时间短
-
缺点:需要序列化,读、写相对堆缓存慢很多
-
重启需要重新加载
-
实现:Ehcache 3.x、MapDB
磁盘缓存
- 重启不需要重新加载
分布式缓存
- 解决了单机容量问题。但需要注意数据一致性问题
单机方案:最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据到磁盘缓
集群方案:存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据到分布式缓存
Cache-Aside方式
-
理解:业务代码围绕着Cache写,由业务代码直接维护缓存
-
并发更新问题
-
用户维度的缓存发生并发概率很小,可以不考虑
-
如商品数据更新,可以使用canal订阅binlog来进行增量更新分布式缓存
-
Cache-As-SoR方式
-
概念:所有操作都只对Cache进行操作,Cache看作为SoR,然后Cache再委托给SoR进行真实的读、写。
-
read-through
-
cache不命中需要回源SoR
-
Guava Cache、Ehcache 3.x支持,需要配置一个CacheLoader
-
-
write-through
-
穿透写模式,由cache直接写入SoR
-
Ehcache支持,需要配置CacheLoaderWriter
-
-
write-behind
-
回写模式,不同于write-through的同步写,转为异步写
-
Ehcache支付,需要配置CacheLoaderWriter
-
Guava Cache、Ehcache中的堆缓存都是基于引用,在存储到SoR时应该复制对象
性能测试
- 可以使用JMH
HTTP缓存
基于浏览器的last-modified
-
spring MVC 请求响应设置last-modified,expires,cache-control,http-status = 304
-
nginx 也提拱了expires、etag、if-modified-since指令来实现缓存控制
HttpClient客户端缓存
多级缓存
搭建层级
接入Nginx将请求负载均衡到应用Nginx
-
轮询算法
- 使用服务器的请求更加均衡
-
一致性哈希
- 提升应用的缓存命中率
应用Nginx读取本地缓存
-
提升吞吐量,降低后端压力
-
解决热点问题
读取分布式缓存
-
如果应用Nginx没有命中本地缓存
-
如果是读写分离的集群,可以再尝试读取一次主集群
-
减少回源访问率
回源Tomcat集群
-
如果分存式缓存也没有命中
-
可以使用轮询、一致性哈希算法回源Tomcat
Tomcat读取本地缓存
- 添加一层本地缓存,目的在于预防缓存崩溃,和快速修复缓存
回源DB读取数据
- 如果所有缓存都未命中,则回源数据源读取数据
数据缓存
时限
-
设定过期
- 常见用法,读取缓存未命中,回源数据源,异步存入缓存,设定过期时间;需要考虑数据一至性问题
-
设定不过期
- 长尾数据,比如用户、分类、商品、价格、订单等;可以考虑使用LRU机制驱逐缓存数据
维度化缓存与增量缓存
- 比如一个商品会由多维度拼装而成,如果全量更新成本会很高,这时可以建立多维度缓存,增量更新某一维度
大Value缓存
- 首先要尽量避免大Value来缓存数据;如果当前场景必须使用,则可以考虑memcached的多线程来实现,或者对Value进行压缩,或者还可以拆分多个小Value,由业务端来聚合
热点缓存
-
可以在客户端做一份本地缓存,避免拉取远程数量访问量太大导致远程缓存请求过多、负载过高或带宽过高等问题
-
实现架构
-
单机全量缓存+主从
- 所有缓存都存储在应用本机,如果缓存未命中,回源数据更新到主缓存服务器,再同步在从缓存服务器中;回源更新可以采用懒加载或订阅消息
-
分布式缓存+应用本地热点
-
-
更新缓存与原子性
-
使用时间戳或者版本对比,如果使用的是Redis,则可以利用其单线程机制进行原子化更新
-
使用如canal订阅数据库binlog
-
缓存的分步式
- 将数据分散在多个实例或多台服务器中;如果使用redis可以考虑redis-cluster
缓存崩溃与快速修复
-
主从机制,做好冗余
-
部分用户降级,慢慢减少降级量;通过worker预热缓存数据
写缓存操作不要放在事务中,防止写操作异常引起整个事务回滚
网友评论