【重磅!进 BAT 必读,大厂对分布式线上问题真实拷问连环炮!】

大厂要求

  • 技术广度
  • 项目经验
  • 生产经验
  • 技术深度
  • 系统设计

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 高可扩展性

  1. 核心组件全部接口化,组件和组件之间的调用,必须全部依托于接口,去动态找配置的实现类,如果没有配置就用他自己默认的
  2. 提供一种自己实现的组件的配置的方式,比如自己实现了某个组件,配置一下,运行的时候直接找你配置的组件,作为实现类 ,不用自己默认组件

设计 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


  1. TM 请求 TC 要开启一个新的分布式事务,TC 生成一个 XID
  2. XID 通过服务调用传递下去
  3. RM 注册本地事务作为一个此次分布式事务的一个分支事务到 TC 上
  4. 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 抗高并发,做好一致性方案。

你可能感兴趣的:(分布式,java)