本篇第一部分为我阅读《从零开始学架构》的读书笔记。如觉得有帮助,请购买正版图书学习。
第二部分为平时看到的有关架构的一些资料整理。
高性能存储
关系型数据库
数据库范式
- 每个属性都不可再分。即每个列不可再分。1NF 是所有关系型数据库的最基本要求。
- 表中的每列都和主键相关。一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
- 每一列数据都和主键直接相关,而不能间接相关。
读写分离
- 主从模式(一主多从,多主多从),数据写如主库,从库复制主库数据,分担读压力。
- 问题
- 延迟:主从复制需要时间,可能造成延迟。解决方法:
- 写操作后的读,发送给主库
- 从库读取不到时,读取主库(二次读取)
- 关键业务(延迟要求低业务)全部从主库读取,非关键业务从库读取
- 数据还是单机存储,当数据越来越多时,读写性能下降,备份话的时间更长,极端情况下丢失风险越大
- MySQL 是通过 binglog(保持在磁盘上的二进制日志)实现的,主库异步的将 binglog 传给从库,从库收到后解析成 SQL 在自己执行
业务分库
- 按照业务模块将数据分散到不同的数据库
- 建议:如果没有性能上的瓶颈就暂时不做分库;如果做一次做到位
- 问题:
- join:原本可以通过 join 得到的结果,需要更复杂的实现(先查询 id,再根据 id 查询内容)
- 事务:不同数据库中的表,可能在同一个操作中被修改(MySQL 分布式事务解决方案:XA)
- 成本:需要更多机器支持分库,因此是否分库需要正确评估
分表
- 垂直分表:将原本一张表的内容写入多张表中,加快单表读写性能,读取完整数据可能更复杂
- 水平分表:将一张表(千万级别)的数据存储于多张表中,问题:
- 路由:水平分表后,需要确定一条数据属于哪个子表,需增加路由算法。常见的路由有:范围,hash,配置
- join:需要在业务代码或数据库中间件中进行多次 join 查询,然后合并结果
- count:多次 count,或使用计数表(可能造成数据不一致,增加写压力)
- group by:需要在业务代码或数据库中间件中进行排序
- 无分区键字段查询:维护额外的分区键与查询字段的关系表
唯一主键生成
- Snowflake:
- 特点:调递增,包含机器 ID、序列号、时间戳。
- 使用:可以嵌入代码中,也可以部署单独的 ID 服务器。
- 缺点:依赖系统的时间戳,如果时间不准会产生重复 ID;并发不高时,可能造成 ID 分布不均,可以用秒做结束,或者随机一个开始序号
NoSQL
K-V 存储(Redis 为例)
支持string, hash, list, set, zset, map
缺点:不完全支持 ACID
- 原子性:事务支持不完善
- 一致性:事务支持不完善
- 独立性:单线程,事务过大会阻塞
- 持久性:RDB 和 AOF
一致性 HASH 算法
Redis 集群部署时考虑容错性和可用性,采用一致性 Hash 算法计算 key 的存储节点。一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数 H 的值空间为 0-2^32-1。使用一致性 Hash 算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。为了解决这种数据倾斜问题,一致性 Hash 算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
文档数据库
为了解决 schema 带来的问题,最大的特点是灵活无 schema,大部分使用 json 存储数据。适合与电商游戏等场景。可作为关系型数据库的补充。
优势:
- 新增字段简单
- 历史数据不会出错
- 可以很容易存储复杂数据
缺点:
列式数据库
一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景通常在几个列中,且不需更新删除数据。
优势:
- 在只需要几个值的情况下,可直接读出,节省 IO
- 压缩比例高
缺点:
- 当同时需要多个列时,效率较低。因为要更新需要将存储的数据解压后更新,再压缩,最后写入磁盘
全文搜索引擎
传统关系型数据库可能无法满足全文搜索的需求场景,因为全文搜索的条件通常是任意组合的,如果建索引需要建很多;like 是整表扫描,效率非常底
全文搜索通常使用倒排索引实现,原理是建立单词到文档的索引。
缓存
问题:
- 缓存穿透:指缓存没有发生作用,虽然去查询了缓存,但缓存中没有数据,再次访问存储系统
- 原因:1)数据不存在;2)生产缓存需要大量的运算
- 解决:解决 1)没有查询到的值设置默认值;解决 2)监控
- 缓存雪崩:当缓存失效后,引起的系统大面积的性能下降
- 解决:更新锁,对缓存更新进行加锁保护;后台更新,由特定的后台线程完成缓存定时更新操作,此时也可能由于缓存占满导致缓存失效,此时可使用定时读取或消息队列通知的形式通知后台线程更新缓存
- 缓存热点:当大多数查询同时命中缓存时可能造成缓存服务器压力较大
CDN 缓存静态资源
CDN 就是将静态的资源分发到位于多个地理位置机房中的服务器上,因此它能很好地解决数据就近访问的问题,也就加快了静态资源的访问速度。
以 www.baidu.com 为例给你简单的域名解析过程:
- 域名解析请求先会检查本机的 hosts 文件,查看是否有 www.baidu.com 对应的 IP;
- 如果没有的话,就请求 Local DNS 是否有域名解析结果的缓存,如果有就返回标识是从非权威 DNS 返回的结果;
- 如果没有就开始 DNS 的迭代查询。先请求根 DNS,根 DNS 返回顶级 DNS(.com)的地址;再请求.com 顶级 DNS 得到 baidu.com 的域名服务器地址;再从 baidu.com 的域名服务器中查询到 www.baidu.com 对应的 IP 地址,返回这个 IP 地址的同时标记这个结果是来自于权威 DNS 的结果,同时写入 Local DNS 的解析结果缓存,这样下一次的解析同一个域名就不需要做 DNS 的迭代查询了。
消息队列
作用
- 削峰填谷,将峰值流量缓存在消息队列中,如秒杀场景
- 异步处理,处理非主要流程,如消费后发放优惠卷
- 解耦,不直接依赖接口,依赖消息队列降低系统间的耦合型
消息丢失
- 生产者发送消息时网络错误,以为发送给了 MQ,实际 MQ 没有接收到。这种情况设置正确的消息重传次数和失败处理,另外也可以设置消息同步到多个副本才算成功。
- 设置副本数大于 1
- 设置活跃 follower 大于 1
- 设置 leader 需要把消息同步到最小活跃同步 follower 才算写入成功
- 重试次数和处理设置
- MQ 仅接收到消息,还未来得及写入磁盘保存;只写入到 leader 没有同步到 follower 时,leader 挂了
- 设置副本数大于 1
- 设置活跃 follower 大于 1
- 设置 leader 需要把消息同步到最小活跃同步 follower 才算写入成功
- 重试次数和处理设置
- 消费者已经提交了确认,但消费途中没有处理完就 down 了 –> 关闭自动提交确认,手动保证处理完成后提交
消息重复消费
避免重复消费,需要保证消息的生产、消费过程的幂等性:
- 生产幂等性有多种 MQ 都支持,MQ 做法是给每一个生产者一个唯一的 ID,并且为生产的每一条消息赋予一个唯一 ID,消息队列的服务端会存储 < 生产者 ID,最后一条消息 ID> 的映射。当某一个生产者产生新的消息时,消息队列服务端会比对消息 ID 是否与存储的最后一条 ID 一致,如果一致就认为是重复的消息,服务端会自动丢弃
- 消费者通过鉴定唯一 ID 是否已经消费过保证。可以用数据库保证,需要整个处理过程在同一个事务中。也可以用乐观锁保证,比如给数据添加版本号。
消息延迟
- 监控:
- 使用消息队列提供的工具,通过监控消息的堆积来完成,如
kafka-consumer-groups.sh
、JMX
- 生成监控消息的方式来监控消息的延迟情况,具体是生成一个时间戳消息,消费者消费到此消息时判断时间是否大于阈值,发送报警
- 减少的方法:
- 优化消费性能
- 增加消费者数量(Kafka 消费者的数量和分区数一样,需要同时增加分区数和消费者数)
Kafka 设计优点
- Kafka 使用硬盘存储,但做了很多优化的设计使存储性能很高
- 顺序存储,所有的消息在文件后面追加
- 通过维护一个 offset 来实现顺序访问
- IO 采用 0 拷贝技术,减少了从内核空间读取到用户缓存,再从用户缓存输出到网络流的时间。直接从页缓存写入网络流
- 网络带宽上的设计考虑,会对消息做压缩,减少带宽消耗。也可以设置消息批量发送,减少网络请求次数
- 分布式存储设计,有备份和主分区,保证消息不丢失。用户通过 offset 也能从宕机事故中快速恢复
计算高性能
高性能计算有两个角度:1)提高单机计算能力;2)若单机无法支撑,设计服务集群方案
单机高性能
单机高性能与服务器采用的网络编程模型有关:服务器如何管理连接;服务器如何处理请求。
PPC(Process per Connection)
每次有新的连接就建立一个线程去处理。适合服务器的连接数没那么多的情况。
缺点:
- 进程建立代价高
- 父子进程通信复杂
- 进程数量增多后系统压力打
pre-fork
提前创建进程,节约创建进程的时间,但与 PPC 没有太大区别,有同样问题
TPC(Thread per Connection)
每次请求新建进程处理,线程更轻量极,通信也更简单
缺点:引入了一些新问题
- 线程也有代价
- 线程间通信问题
- 某个线程异常会导致进程退出,没那么稳定
pre-thread
预先创建线程,节约时间。
Reactor(Dispatcher)
资源复用,不再为每个连接创建进程,而是创建进程池。引入 I/O 多路复用概念,增加吞吐量。
多路复用技术要点:
- 当多条连接共用一个阻塞对象时,进程只需在一个阻塞对象上等待,无需轮寻
- 当某个连接有数据可以处理时,唤醒进程,开始处理
实现方式:
- 单 Reactor 单进程/线程(Redis)
- 单 Reactor 多线程(Java NIO)
- 多 Reactor 多进程/线程(Nginx,Memcache,Netty)
Proactor
异步网络模型(AIO)
集群高性能
负载均衡
分类:
- DNS:解析同一个域名可以返回不同的 IP(按地区访问不同的服务)
- 优点:便宜简单;就近访问
- 缺点:更新不及时,缓存;拓展性能差;分配策略简单
- 硬件负载均衡:基础网络设备,F5,A10
- 优点:功能性能强大;稳定性高;支持安全防护
- 缺点:贵,拓展差
- 软件负载均衡:Nginx(7 层),LVS(4 层)
- 优点:简单,便宜,灵活
- 缺点:性能一般;不具有安全功能
以上三种互相结合使用,DNS 转发区域请求,F5 负责区域集群请求,Nginx 用于机器级别
算法:轮寻;加权轮寻;负载低优先;性能最优;hash
CAP
一个分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个。
解释:
- Consistence:某个节点能读到最新的写结果,所有节点在任意时间被访问返回的数据完全一致。服务端视角,更新如何同步到整个系统,以保证数据的一致性。客户端视角,并发访问时,更新过的数据如何获取。
- Availability:非故障节点能在合理时间内返回合理结果。服务断视角,正常工作的服务节点总能响应应用请求,不会吞噬、阻塞请求。客户端视角,发出请求总有响应,不会出现整个服务无法连接、超时、无响应
- Partition Tolerance: 当网络分区后,系统能继续履行职责。服务端视角,分区节点故障、网络异常,服务集群仍然可以对外提供服务。客户端视角,服务端的各种故障对自己透明
应用
真实情况是 P 必须被选择,因为网络不稳定。所以可选择 AP 或者 CP。
在设计时需要注意:
- 粒度是数据,而不是系统。系统中的某些数据可能需要 AP,某些可能需要 CP
- CAP 忽略了网络延迟。但在数据同步中可能因网络延迟造成不一致,所以强一致性要求的话,需要保证 CA,放弃 P,即单点写入,其他节点备份。这时可使用如用户分区之类的方法,减小 P 缺失带来的影响。
- 正常情况下,可以同时满足 CA
- 放弃并不意味着什么都不做,需要为恢复做准备。如可用分区日志进行数据恢复,也可以一定规则恢复数据。
ACID 和 BASE
ACID:数据库事物:原子性,一致性,隔离性,持久性
BASE(AP 方案的衍生):基本可用,软状态,最终一致性
以下是高并发系统设计学习笔记。
在微博中,明星动辄拥有几千万甚至上亿的粉丝,你要怎么保证明星发布的内容让粉丝实时地看到呢?
淘宝双十一,当你和上万人一起抢购一件性价比超高的衣服时,怎么保证衣服不会超卖?
春运时我们都会去 12306 订购火车票,以前在抢票时经常遇到页面打不开的情况,那么如果你来设计 12306 系统,要如何保证在千万人访问的同时也能支持正常抢票呢?
基础篇
API 设计
好的 API 设计需要满足两个主要的目的:
- 平台独立性。 任何客户端都能消费 API,而不需要关注系统内部实现。API 应该使用标准的协议和消息格式对外部提供服务。传输协议和传输格式不应该侵入到业务逻辑中,也就是系统应该具备随时支持不同传输协议和消息格式的能力。
- 系统可靠性。 在 API 已经被发布和非 API 版本改变的情况下,API 应该对契约负责,不应该导致数据格式发生破坏性的修改。在 API 需要重大更新时,使用版本升级的方式修改,并对旧版本预留下线时间窗口。
设计注意点:
- 避免简单封装。API 应该是有业务含义的,不要沦为数据库操作的接口。
- 关注点分离。接口的功能要单一,不要提供大而全的接口,应该根据业务做 API 功能的区分。
- 完全穷尽,彼此独立。不应该提供相互功能叠加的 API。要合理的抽象可操作的对象,彼此直接不要有交叉。
- 版本化。
- 合理命名。
- 尽量和业务名词保证一致
- 尽量不要用简写,如果要用保证全局一致
- 尽可能使用不需要编码的字符
- 安全
- 内部接口:健壮性,如验证、错误信息
- 外部接口:错误的调用方式、接口滥用、非法访问
API 设计评审清单:
- URI 命名是否通过聚合根和实体统一
- URI 命名是否采用名词复数和连接线
- URI 命名是否都是单词小写
- URI 是否暴露了不必要的信息,例如/cgi-bin
- URI 规则是否统一
- 资源提供的能力是否彼此独立
- URI 是否存在需要编码的字符
- 请求和返回的参数是否不多不少
- 资源的 ID 参数是否通过 PATH 参数传递
- 认证和授权信息是否暴露到 query 参数中
- 参数是否使用奇怪的缩写
- 参数和响应数据中的字段命名统一
- 是否存在无意义的对象包装 例如{“data”:{}’}
- 出错时是否破坏约定的数据结构
- 是否使用合适的状态码
- 是否使用合适的媒体类型
- 响应数据的单复是否和数据内容一致
- 响应头中是否有缓存信息
- 是否进行了版本管理
- 版本信息是否作为 URI 的前缀存在
- 是否提供 API 服务期限
- 是否提供了 API 返回所有 API 的索引
- 是否进行了认证和授权
- 是否采用 HTTPS
- 是否检查了非法参数
- 是否增加安全性的头部
- 是否有限流策略
- 是否支持 CORS
- 响应中的时间格式是否采用 ISO 8601 标准
- 是否存在越权访问
通用设计方法
- Scale-out(横向拓展):突破单机计算瓶颈
- 缓存:降低响应时间的中间存储
- 异步:削峰填谷,减小服务器压力,收发更多请求
系统演进思路:
- 最简单的系统设计满足业务需求和流量现状,选择最熟悉的技术体系
- 随着流量的增加和业务的变化修正架构中存在问题的点,如单点问题、横向扩展问题、性能无法满足需求的组件。在这个过程中,选择社区成熟的、团队熟悉的组件帮助我们解决问题,在社区没有合适解决方案的前提下才会自己造轮子
- 当对架构的小修小补无法满足需求时,考虑重构、重写等大的调整方式以解决现有的问题
架构分层
分层的好处:
- 简化系统设计,让不同的人专注做某层的事情
- 可以提高复用性,不同的上层项目可以依赖相同的中间层或基础层
- 可以方便横向拓展,只用拓展某一层
分层的坏处:
- 引入复杂性
- 性能损耗
如何分层:
三层架构模型:表现层、业务逻辑层、数据访问层
细化三层架构:终端显示层、开放接口层(提供给外部的开放接口)、web 层(访问控制进行转发,校验、不复用的业务简单处理)、业务逻辑层、通用业务层(原子化、通用服务)、数据访问层、外部接口或第三方平台
DDD(领域驱动设计)
系统设计目标
高性能
原则:
- 不能盲目,问题导向
- 抓住主要矛盾,优先优化主要的性能瓶颈
- 性能优化要有数据支持,要时刻了解你的优化让响应减少了多少,提升了多少吞吐量
- 持续优化
指标:
- 平均值:均值可以在一定程度上反应这段时间的性能,但它敏感度比较差,如果这段时间有少量慢请求时,在平均值上并不能如实地反应。平均值对于度量性能来说只能作为一个参考。
- 最大值:这段时间内所有请求响应时间最长的值,但它的问题又在于过于敏感了。
- 分位值:分位值有很多种,比如 90 分位、95 分位、75 分位。一共有 100 个请求,那么排在第 90 位的响应时间就是 90 分位值。分位值排除了偶发极慢请求对于数据的影响,能够很好地反应这段时间的性能情况,分位值越大,对于慢请求的影响就越敏感。
- 吞吐量和响应时间:度量性能时都会同时兼顾吞吐量和响应时间,比如设立性能优化的目标时通常会这样表述:在每秒 1 万次的请求量下,响应时间 99 分位值在 10ms 以下。
优化:
- 硬件优化(CPU、内存),有拐点,压测可以找到拐点
- CPU 密集型,优化算法
- IO 密集型,找到系统瓶颈,逐个击破
高可用(系统具备较高的无故障运行的能力)
度量:
- MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高
- MTTR(Mean Time To Repair)表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。
- Availability = MTBF / (MTBF + MTTR)。可用性可以几个九来描述系统的可用性。四个九要建立完善的值班体系、故障处理流程和业务变更流程;五个九要建立自动的恢复机制和容灾能力。
高可用设计思路:
- 系统设计:高可用系统设计的第一原则是“Design for failure”,故障在庞大集群中总是存在的。具体方法有:故障转移、超时控制、降级、限流
- 系统运维:灰度发布、故障演练
高可扩展
提高可扩展性的方式的重要思路是拆分。我们可以按照业务区域对系统进行拆分,使不同业务直接更加独立,可以单独扩展。
分布式
分布式改造的原因
- 系统中使用的资源出现拓展性问题,尤其是数据库
- 大团队共同维护一套代码,效率降低、成本提升
- 系统部署成本增加
服务拆分
- 单一服务内部功能的高内聚和低耦合
- 关注服务拆分的粒度,先粗略拆分再逐渐细化。拆分初期可以把服务粒度拆得粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化
- 拆分的过程,要尽量避免影响产品的日常功能迭代。可以逐步拆分,先拆分独立、边缘服务;存在依赖关系的先拆分被依赖的服务
- 服务接口定义要具备可扩展性,可以封装参数是类,或者采用版本管理,逐步替换方式
问题和解决
- 服务治理,服务的部署信息统一管理 –> 注册中心
- 一个服务可能被多个其他服务使用,一旦发生性能问题,造成整个系统问题 –> 融断、降级、限流、超时控制
- 调用链变长,问题难以排查 –> 链路追踪
注册中心
功能:提供服务地址的存储;当一个服务端的地址发生变化后通知其他客户端
服务状态管理实现:
- 主动探测,注册中心定时调用接口判断服务是否可用。这种方式服务数量上升后轮寻调用成本增高。
- 心跳模式,节点定时发送心跳包给注册中心,如果长时间未收到心跳包,表示节点不可用。
问题:
- 节点过度摘除:注册中心可以添加一些保护措施,比如当移除的节点超过所有节点的 40%停止移除,发送报警,此时考虑是网络问题或服务中心本身问题。
- 通知风暴:当管理集群过大时,节点状态的同步消息就需要发送给更多节点。可以采用扩展注册中心;注意注册中心流量,控制管理集群规模;保护机制
链路追踪
问题排查、性能瓶颈查找
可以通过在日志中加入请求 idrequestId
,下一跳 idspanId
来跟踪请求,每次请求将它们作为参数传递。日志可以采用采样打印方式防止过度性能消耗,日志可以先发送给 MQ 提高写入效率。
客户端负载均衡
在客户端选择要调用的服务端,通常会结合注册中心一起使用。
常用策略有:静态策略:轮寻、带权重轮寻;动态策略:优先选择活跃连接数最少服务;加响应时间权重分配
API 网关
API 网关(API Gateway)不是一个开源组件,而是一种架构模式,它是将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。你可以把它看作系统的边界,它可以对出入系统的流量做统一的管控。
网关的实现重点在于性能和拓展性,可以用多路 I/O 复用模型和线程池并发处理,来提升系能,使用责任链模式来提升网关的拓展性。为防止某服务处理时间过长或者故障,占有所有网关线程资源,可以考虑给不同的服务专属线程池,或者限制服务占用线程池的大小。
- 入口网关:
- 提供给客户端统一的接入地址,将请求动态路由到不同业务的服务,并且可以做一些转换工作
- 植入一些服务治理的策略,如服务的熔断、降级、流量控制和分流
- 客户端认证和授权
- 黑白名单,比如针对设备 ID、用户 IP、用户 ID 等维度的黑白名单
- 日志记录,链路追踪
- 出口网关
- 系统依赖第三方服务,可以用出口网关提供统一的介入方式
维护
监控
服务端监控
- 指标
- 四个黄金信号(Four Golden Signals):延迟、通信量(吞吐量)、错误和饱和度(服务或者资源的利用率)
- RED 指标体系:R 代表请求量(Request rate)、E 代表错误(Error)、D 代表响应时间(Duration)
- 指标采集
- agent。如 java 可通过 JMX
- 埋点。一般会在埋点时先做一些汇总。
- 日志。日志采集工具:Apache Flume、Fluentd 和 Filebeat
- 报表:访问趋势报表、性能报表、资源报表
客户端监控
- APM 系统:在客户端上植入 SDK,由 SDK 负责采集信息,并且经过采样之后,通过一个固定的接口定期发送给服务端。这个固定接口和服务端,我们可以称为 APM 通道服务
- 监控信息:网络、CDN、DNS、客户端卡顿、垃圾收集
压测
- 误区:
- 不使用线上环境和数据
- 使用模拟请求,而不是线上请求
- 从一台服务器发起流量
- 全链路压测平台:
- 流量构造:流量拷贝,压测浏览染色。工具
GoReplay
- 压测数据隔离
- 系统健康检查和压测流量干预
稳定性监控
融断
- 定义:服务治理中的熔断机制指的是在发起服务调用的时候,如果返回错误或者超时的次数超过一定阈值,则后续的请求不再发向远程服务而是暂时返回错误。
- 融断三种状态:关闭(服务正常)失败一定次数变为打开状态,打开状态下定时探测或定时打开一部分流量此时为半打开状态,半打开状态成功一定次数变为关闭状态,半打开状态出现失败返回打开状态。
降级
- 定义:站在整体系统负载的角度上,放弃部分非核心功能或者服务,保证整体的可用性的方法,是一种有损的系统容错方式。熔断也是降级的一种,除此之外还有限流降级、开关降级。
- 开关降级指的是在代码中预先埋设一些“开关”,用来控制服务调用的返回值,常见策略有:
- 针对读取数据的场景,我们一般采用的策略是直接返回降级数据,可直接返回缓存,也可返回固定结果
- 对于一些轮询查询数据的场景,比如每隔 30 秒轮询获取未读数,可以降低获取数据的频率(将获取频率下降到 10 分钟一次)
- 而对于写数据的场景,一般会考虑把同步写转换成异步写,这样可以牺牲一些数据一致性保证系统的可用性
流量控制
- 定义:限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性。限流策略一般部署在服务的入口层,比如 API 网关中,这样可以对系统整体流量做塑形。而在微服务架构中,你也可以在 RPC 客户端中引入限流的策略,来保证单个服务不会被过大的流量压垮。
- 实操:限流策略是微服务治理中的标配策略,只是你很难在实际中确认限流的阈值是多少,设置的小了容易误伤正常的请求,设置的大了则达不到限流的目的。所以,一般在实际项目中,我们会把阈值放置在配置中心中方便动态调整;同时,我们可以通过定期的压力测试得到整体系统以及每个微服务的实际承载能力,然后再依据这个压测出来的值设置合适的阈值。
- 方法
- 滑动窗口:将时间的窗口划分为多个小窗口,每个小窗口中都有单独的请求计数。滑动窗口算法解决了窗口边界的大流量的问题,但是它和固定窗口算法一样,还是无法限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑
- 令牌桶:如果我们需要在一秒内限制访问次数为 N 次,那么就每隔 1/N 的时间,往桶内放入一个令牌;在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题。
- 漏斗:流量产生端和接收端之间增加一个漏桶,流量会进入和暂存到漏桶里面,而漏桶的出口处会按照一个固定的速率将流量漏出到接收端。一般会使用消息队列作为漏桶的实现,流量首先被放入到消息队列中排队,由固定的几个队列处理程序来消费流量,如果消息队列中的流量溢出,那么后续的流量就会被拒绝。
真实场景
秒杀
问题:
- 高并发:时间极短、瞬间用户量大,数据库、缓存压力
- 超卖
- 链接暴露,恶意请求:机器模拟用户请求,参与秒杀
前端优化:
- 资源静态化:可以提前将图片、视频等资源放在 cdn 服务器,减少秒杀时服务器压力
- 秒杀链接加盐:URL 动态化,通过前端代码获取加密串作为请求链接,后端校验
- 限流:防止并发点击
后端优化:
- 单一职责:秒杀服务器隔离
- Redis 集群:集群部署缓存服务 Redis
- 库存预热:提前把商品的库存加载到 Redis 中去,让整个流程都在 Redis 里面去做,然后等秒杀结束了,再异步的去修改库存就好了
- 限流、降级、融断、隔离
- 消息队列:利用消息队列缓存请求,服务端消费消息,削峰填谷
海量数据计数器
微博的评论数、点赞数、转发数、浏览数、表态数等等;
用户的粉丝数、关注数、发布微博数、私信数等等
问题:
- 数据量巨大
- 访问量打,对性能要求高,要求要毫秒级别返回结果
- 对于可用性、数字的准确性要求高
comments powered by