大厂要求
- 技术广度
- 项目经验
- 生产经验
- 技术深度
- 系统设计
Dubbo vs Spring Cloud
Eureka: 服务注册
Feign:服务调用
Ribbon:负载均衡
Zuul/Spring Cloud Gateway: 网关,灰度发布、统一限流(每秒 1W 个请求)、统一熔断、统一降级、统一缓存、统一授权认证
Hystrix:链路追踪
Stream:
Dubbo 底层运行原理
消费者
动态代理:Proxy
负载均衡: Cluster,负载均衡,故障转移
注册中心:Registry
通信协议:Protocol,http,rmi,dubbo
信息交换:Exchange,Request 和 Response
网络通信: Transport ,netty, mina
序列化:封装好的请求如何序列化成二进制数组,通过 netty /mina 发送出去
提供者
网络通信 : Transport 基于 Netty /Mina 实现的 Server
信息交换: Exchange, Response
通信协议:Protocol
动态代理:Proxy
Dubbo 底层网络通信
Dubbo 高可扩展性
- 核心组件全部接口化,组件和组件之间的调用,必须全部依托于接口,去动态找配置的实现类,如果没有配置就用他自己默认的
- 提供一种自己实现的组件的配置的方式,比如自己实现了某个组件,配置一下,运行的时候直接找你配置的组件,作为实现类 ,不用自己默认组件
设计 RPC 框架
核心就是Dubbo里的动态代理里面的代码逻辑,网络通信、代理机制、负载均衡
Spring Cloud 底层架构原理
- Eureka
作用:服务注册与发现、心跳与故障
二级缓存,优化并发冲突,读多写少
- Feign
对接口打了一个注解,针对这个注解标注的接口生成动态代理,然后针对Feign的动态代理去调用他的方法的时候,会在底层生成 http协议格式的请求
先使用 Ribbon 去本地的 Eureka 注册表的缓存里获取出来对方机器的列表 ,进行负载均衡,选择一台机器出来 ,然后使用HttpClient 框架组件对那台机器发送 HTTP请求
- Zuul
配置一下不同的请求和服务的对应关系,你请求到了网关,他直接查找匹配的服务,然后直接把请求转发给那个服务的某台机器,Ribbon 从 Eureka 本地的缓存列表里获取一台机器,负载均衡,把请求直接用 HTTP 通信框架发送到指定机器上去。
Dubbo VS Spring Cloud
- Dubbo ,RPC性能比 HTTP性能更好,并发能力更强。Dubbo一次请求 10ms,Spring Cloud 耗费 20ms
- Dubbox 以前定位是单纯的服务框架,Spring Cloud是全家桶概念、提供分布式配置中心、授权认证、服务调用链路追踪、资源隔离、熔断降级、请求 QPS 监控
契约测试、消息中间件封装、Zk封装
- Spring Cloud Alibaba, 技术相互融合
注册中心调研
- Eureka
- ZooKeeper
- Consul
- Nacos
Eureka 集群架构原理
peer-to-peer,部署一个集群,但是集群里每个机器的地方是对等的,各个服务可以向任何一个 Eureka实例服务注册和服务发现,集群里任何一个 Eureka 实例接收到写请求之后,会自动同步给其他所有的 Eureka 实例
ZooKeeper 集群架构原理
Leader + Follower 2 种角色,只有Leader 可以负责写(服务注册),他可以把数据同步给 Follower ,读的时候 Leader /Follower 都可以读
CAP
C 一致性,A 可用性,P 分区容错性
ZooKeeper 采用 CP,选举期间服务不可用
Eureka 采用 AP和最终一致性
服务注册发现时效性
- ZooKeeper 时效性更好,注册或者挂了,一般秒级就能感知到
- Eureka 默认配置非常糟糕,服务发现感知要几十秒,甚至分钟级别,上线一个新的服务实例,到其他人可以发现他,极端情况下要 1 分钟的时间,Ribbon 同时也需要间隔时间才会更新它自己的缓存。
服务故障,间隔 60 秒才去检查心跳,发现这个服务上一次心跳是 60 秒之前,隔 60 秒取检查心跳,超过 90 秒认为没有心跳,才会认为他死了,已经 2 分钟过去了,30 秒才会更新缓存,30 秒,其他服务才会来拉最新的注册表
容量
- zk 不适合大规模的服务实例,因为服务上下线的时候,需要瞬间推送数据通知到所有的其他服务实例,所以一旦服务规模太大,到了几千个服务实例的时候,会导致网络带宽会大量占用
- eureka也很难支撑大规模的服务实例,因为每个 eureka实例都要接受所有的请求,实例多了压力太大,扛不住,也很难到几千服务实例
注册中心高可用
eureka 集群部署
服务发现过慢
- eureka 里配置 ReadWrite 同步到 ReadOnly 缓存的间隔时间
eureka.server.responseCacheUpdateIntervalMs = 3000
- 服务里配置拉取注册中心配置的间隔时间
eureka.client.registryFetchIntervalSeconds = 3000
- 服务里配置心跳间隔的时间
eureka.client.leaseRenewalIntervalInSeconds: 30
- eureka里线程检查心跳的间隔时间
eureka.server.evictionIntervalTimerInMs = 6000
- eureka里配置心跳过期时间,这条记录就会从注册表中删掉,然后ReadWrite缓存就会淘汰掉
eureka.instance.leaseExpirationDurationInSeconds: 6
优化成12 秒内可以感受到节点上线、下线
- 关闭 eureka 的自我保护 (当突然大面积服务都没心跳了,eureka 会保护注册表不下线这些服务,源码层很多bug)
eureka.server.enableSelfPreservation: false
生产环境中的注册中心
- eureka 配置 8核 16G,16 核 32G,每台机器每秒钟的请求支撑几千绝对没问题,可以支撑上千个服务
- 时效性优化
网关
- 动态路由:新开发某个服务,动态把请求路径和服务的映射关系热加载到网关里去,服务增减机器,网关自动热感应
- 灰度发布:
- 鉴权认证
- 性能监控:每个API 接口的耗时、成功率、QPS
- 系统日志
- 数据缓存
- 限流熔断:控制 1 秒钟 1000 个请求
网关调研
- Kong
Nginx里面的一个基于 lua 写的模块,实现了网关的功能
- Zuul
Spring Cloud
- OpenResty
Nginx + lua
- 自研网关
Servlet + Netty来做网关
中小型公司:
SpringCloud体系用Zuul,如果是Dubbo,有的采用Kong;或者 nginx+负载均衡
大厂:
自研
Zuul(Servlet,Java)高并发能力不强,基于Tomcat部署把网关跑起来,Java语言开发,可以把控源码做二次开发
Nginx(Kong,Nginx+Lua)抗高并发能力很强,精通Nginx源码很难,很难从Nginx内核层面去做一些二次开发和源码定制
网关对服务动态路由
读取数据库(Redis ,ZooKeeper, 阿波罗)路由配置以及定时任务刷新
网关优化
每秒 10W 请求 ,一个 zuul 在 8核 16 上可以抗住 几千+ 请求,几十台 zuul 可以抗住 10W+
16 核 32G 的,一台抗住小几万,几台 zuul 就可以抗住 10W+
自研服务注册中心
eureka :peer to peer 每台机器都是高并发请求,有瓶颈
zookeeper: 上下线全量通知其他服务,网络带宽被打满,有瓶颈
分布式注册中心,分片存储服务注册表,横向扩容, 每天机器均摊高并发请求,各个服务按需主动拉取 ,避免反向通知网卡被打满,Master-Slave 高可用性,Master写到Slave后才算写成功,强一致性
网关灰度发布
eureka.instance.meta-data-map.version: new
RibbonFilterContextHolder
生产环境部署
注册中心
中小型公司 20~30 个服务,注册中心 2 ~3 台机器(4 核 8G),每秒抗上千请求
注册中心优化服务注册和发现配置
注册表多级缓存同步 1 秒,注册表拉取频率 1 秒
服务心跳 1 秒上报一次,服务故障发现 1 秒,发现 2 秒内服务没上报心跳,就故障
服务
每秒并发在1000 以内,每个服务部署 2 台机器,每台机器 4 核 8G,每台机器抗几百请求一点问题都没
大部分系统高峰期每秒几百请求,低峰期每秒几十请求,几个请求
网关系统
4 核 8G,一台机器抗每秒几百请求,部署 3~4 台
数据库
16 核 32G,物理机,高峰期每秒几千(三四千的时候,网络负载比较高,CPU 使用率比较高,I/O 负载比较高)请求问题不大,
QPS
metrics 机制,利用 AtomicLong 算出核心接口每分钟调用多少次(除以 60 就是高峰期每秒访问次数)以及每天被调用总次数
TP99 = 100ms , 99%的接口耗时在 100ms以内,1%的接口耗时在100ms以上
平均响应延时 = (每次调用的耗时 + 历史总耗时 )/ 当前请求的总次数
最大 QPS:
假设最大 QPS 为 800 ,当压测工具每秒发起 1000 个请求的时候,只有 800个可以同时被处理,200 个在排队被阻塞住
系统访问量增加 10 倍
- 网关多部署 10 倍机器
- 服务多加机器
- Eureka 换成 8 核 16G (抗住上千请求很轻松)
- 数据库换成32 核 128G (每秒抗住几千请求问题不大)
超时和重试
- 第一次请求的时候,会初始化 Ribbon 组件,需要耗费一定的时间 ,所以很容易导致超时。(改进:让每个服务启动的时候直接初始化 Ribbon 组件 )
ribbon.eager-load.enabled: true , zuul.ribbon.eager-load.enabled: true
- 关闭 hystrix (性能)
feign.hystrix.enabled:false
- 设置超时时间
connectTimeout:1000
ReadTimeout:1000
OKToRetryOnAllOperations:true
MaxAutoRetries:1
MaxAutoRetriesNextServer:1
防重幂等性
- 插入操作幂等性
数据库建立唯一索引
- 更新操作幂等性
在少数核心接口内,根据业务逻辑做幂等性,业务执行成功后,生成一个唯一的key(order_id_stock_deduct)到redis里,下一次进来的时候,如果发现重复key,则执行反向操作
分布式事务
XA、TCC、可靠消息最终一致性方案、最大努力通知方案、Sega
TCC
Try-Confirm-Cancel 每个人先走 try ,有人失败了就会让大家都走 Cancel, 如果Try都成功了,会让大家调用Confirm
库存接口拆分成 1. 扣减库存接口 2.回滚扣减库存接口 由分布式事务框架通知已经执行成功的服务的回滚接口,保证回滚掉。数据要么全部成功、要么全部失败。
技术选型
- TCC 框架有 ByteTCC ,Himly (个人),seata (阿里开源)
- 可靠消息最终一致性方案 基于ActiveMq封装可靠消息服务、基于RabbitMQ自己开发一个可靠消息服务,RocketMQ提供分布式事务支持,实现了可靠消息服务功能
TCC 核心架构
- TC TM RM
- TM 请求 TC 要开启一个新的分布式事务,TC 生成一个 XID
- XID 通过服务调用传递下去
- RM 注册本地事务作为一个此次分布式事务的一个分支事务到 TC 上
- TM 发现全部执行成功,则通知 TC 进行全局提交,此时 TC 通知每个分支事务,可以提交了
如果 TM 发现有错误(某个分支事务未执行成功,把异常一层一层抛到最外层),则通知 TC 进行回滚,TC 会通知所有分支事务进行回滚
TCC 高并发场景
- 每个服务都需要跟 TC 这个角色进行频繁的网络通信,带来开销,不引入分布式事务可能 100ms,引入了可能200ms
- 上报分支事务状态给 TC,其中seata-server 基于文件存储,会更耗费网络请求
- TC ,seata-server 也要支持扩容,部署多台机器 ,TC 背后的数据库要分库分表
RocketMQ对事务的支持
MQ:抗高并发 、削峰、解耦
自己实现 RocketMQ 对事务的支持
分布式锁
解决库存为-5 的问题
分布式锁解决库存问题
Redis 分布式锁
Redisson 框架
redisson.lock("product_1_stock")
product_1_stock:{
"87ff-dsfsfs-121212-sfsfsfs :1":1
}
生存时间:30s
watchdog,redisson 框架后台执行一段逻辑,每隔10s去检查一下这个锁是否还被当前客户端持有,如果是的话,重新刷新一下 key的生存时间为 30s
其他客户端尝试加锁,这个时候发现“product_1_stock” 这个 key已经存在了,里面显示被别的客户端加锁了,此时他就会陷入一个无限循环,阻塞住自己,不能干任何事情,必须在这里等待。
第一个客户端加锁成功,此时有 2 种情况,1)这个客户端操作完毕之后,主动释放锁(将 UUID 线程 ID 对应的值 -1 ,当加锁次数未 0 的时候,删除 key ,否则刷新 key的生存周期 30s) 2)如果这个客户端宕机了,那么和这个客户端的 redisson框架之前启动的后台 watchdog线程就没了,此时最多30s,这个key-value就消失了,自动释放了宕机客户端之前持有的锁
集群故障会导致锁失效?
会,除非是修改 redis和 redisson框架源码,二次开发,加锁必须是 master 和 slave 同时写成功了,才算是加锁成功。
ZooKeeper 分布式锁
curator,基于 zk 实现了一整套高级功能
ZooKeeper 羊群效应
如果几十个客户端同时争抢一个锁,此时会导致任何一个客户端释放锁的时候 ,zk要反向通知几十个客户端,几十个客户端又要发送请求到 zk 去尝试创建锁,几十个人要加锁,大家都是乱糟糟的,无序的。造成很多没必要的请求和网络开销。其实只需要下一个顺序节点收到通知去加锁
ZooKeeper 脑裂
因为ZooKeeper与客户端 A 之间网络发生问题,ZooKeeper 误认为客户端 A 挂掉了,此时删掉客户端 A 创建的临时节点,此时 A 还在运行,B 客户端监听到了 A 客户端的节点被删掉,以为获取到锁,加锁成功,此时分布式锁就失效了。
分布式系统,主控节点有一个 Master,此时因为网络故障,导致其他人以为这个 Master 不可用了,其他节点出现了别的Master,导致集群里有 2 个Master同时在运行。
解决方案:修改curator框架源码,加一些复杂协调机制
Redis VS ZooKeeper 分布式锁
从分布式系统协调语义而言,ZooKeeper 做分布式锁(选举、监听)更好一些,因为Redis 其实是缓存,但是Redis能抗高并发,高并发场景更好一些。
ZooKeeper 本身不适合部署大规模集群,他本身适用的场景就是部署三五台机器,不是承载高并发请求的,仅仅用作分布式系统协调的。
分布式锁高并发优化
Redis 单机抗住几万(1,2W)并发轻松
优化策略:分段加锁
比如:苹果库存有 10000 个,此时在数据库中创建 10 个库存字段,stock_01,stock_02..stock_10 ,每个库存字段里放 1000 个库存,此时这个库存的分布式锁,对应 10 个 key, product_1_stock_01, product_1_stock_02,product_1_stock_03 ,product_1_stock_10,请求过来之后,从 10个 key随机选择一个key,去加锁。每秒 1W 个请求过来,此时他们会对 10 个库存分段 key 加锁,每个 key就 1000 个请求,每台服务器也就 1000 个请求而已。
万一说每个库存分段仅仅剩余 10 个库存了,此时我下订单买 20 个苹果,合并扣减库存。对 product_1_stock_5 加锁了,此时库存只有 10 个,不够买 20 个苹果,可以尝试去 product_1_stock_1,再查询他的库存可能 有 30 个,此时下单就可以,锁定库存的时候,对 product_1_stock_5锁定 10 个库存,对product_1_stock_1锁定 10 个库存,一共锁定 20 个库存。
淘宝京东库存扣减方案
大公司一般有分布式 K-V 存储,tair,redis,mongodb,高并发,每秒几万几十万都没问题,甚至每秒百万。
实时库存数据放 K-V 存储里去,你在操作库存的时 候,直接扣减(不用先查库再扣减),如果你发现扣减之后是负数的话,此时就认为库存超卖了,回滚刚才的扣减,返回提示给用户,库存不足,不能下订单了。对 K-V 库存每次修改写到 MQ 里,异步同步到数据库,相当于异步双写,用分布式 K-V 抗高并发,做好一致性方案。