【2020-面试实战】-分布式微服务

文章目录

      • 延1 说说 Zookeeper 都有哪些使用场景?
      • 延2 分布式锁是啥? 该如何进行选择?
        • 延2.1 实现分布式锁的方式有哪几种?该如何进行选择?
        • 延2.2 讲下 Redlock
        • 延2.3 讲下 Curator
      • 延3 说说你们的分布式 session 方案是啥? 怎么做的 ?
      • 延4 了解分布式事务方案吗? 你们都怎么做的? 有什么坑?
      • 延5 分布式锁和分布式事务的区别
      • 1 为什么要把系统拆分成分布式的?为什么要用Dubbo?
      • 2 RPC 的原理
      • 3 Dubbo的工作原理是啥? 注册中心挂了会怎样?
        • 3.1 Dubbo 单注册表中心会怎样?注册中心挂了还可以继续通信吗?
        • 3.2 Dubbo监控中心Monitor的监控原理
      • 4 Dubbo 如何区分测试和生产环境?
      • 5 Dubbo都支持哪些通信协议以及序列化协议?
      • 6 Dubbo都支持哪些负载均衡,高可用以及动态代理的策略?
      • 7 SPI 是什么思想? Dubbo的 SPI 机制是怎么玩的?
      • 8 基于Dubbo如何做服务治理,服务降级以及重试?
      • 9 分布式系统中接口的幂等性该如何保证? 举例说明(如重复扣款的问题)
      • 10 分布式系统中的接口如何保证顺序性?
      • 11 如何设计一个类似 Dubbo 的 RPC框架? 架构上该如何考虑?
      • 12 说一下Spring Cloud的核心架构原理?
      • 12.1 Ribbon 的负载均衡策略
      • 13 服务注册中心的对比,选择
        • 13.1 注册中心集群如何保证高可用
        • 13.2 你们系统遇到过服务发现过慢的问题吗?
      • 14 服务网关的技术对比,选型
        • 14.1 生产环境下,怎么实现的网关对服务的动态路由?
        • 14.2 如果网关要抗每秒10万级的高并发, 如何做优化?
        • 14.3 你们是如何基于网关做灰度发布的?
      • 15 说一下服务熔断和降级
      • 16 你们公司的各个服务在生产环境是如何部署的?机器配置如何?
      • 17 你们系统每天有多大访问量?每个服务高峰多少QPS?压测过服务最大QPS吗?
      • 18 你们系统是如何配置超时和重试参数的?
      • 19 对于核心接口的防重幂等性你们是如何设计的?
      • 20 从底层原理的角度,对比下Dubbo和Spring Cloud的优劣
      • 21 淘宝和京东的库存是怎么实现的?

分布式锁和分布式事务

延1 说说 Zookeeper 都有哪些使用场景?

(1)分布式协调:这个其实是zk很经典的一个用法,简单来说,就好比,你A系统发送个请求到mq,然后B消息消费之后处理了。那A系统如何知道B系统的处理结果?用zk就可以实现分布式系统之间的协调工作。A系统发送请求之后可以在zk上对某个节点的值注册个监听器,一旦B系统处理完了就修改zk那个节点的值,A立马就可以收到通知,完美解决。

(2)分布式锁:对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行另外一个机器再执行。那么此时就可以使用zk分布式锁,一个机器接收到了请求之后先获取zk上的一把分布式锁,就是可以去创建一个znode,接着执行操作;然后另外一个机器也尝试去创建那个znode,结果发现自己创建不了,因为被别人创建了。。。。那只能等着,等第一个机器执行完了自己再执行。

(3)元数据/配置信息管理:zk可以用作很多系统的配置信息的管理,比如kafka、storm等等很多分布式系统都会选用zk来做一些元数据、配置信息的管理,包括dubbo注册中心不也支持zk么

(4)HA高可用性:这个应该是很常见的,比如hadoop、hdfs、yarn等很多大数据系统,都选择基于zk来开发HA高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过zk感知到切换到备用进程

延2 分布式锁是啥? 该如何进行选择?

首先Java提供的原生锁机制(syncronized,lock)无法保证在分布式系统中,高并发场景对共享数据的修改问题(如超卖问题),此时我们需要使用分布式锁;
其解决的就是不同机器上的多线程对共享资源的正确访问;

延2.1 实现分布式锁的方式有哪几种?该如何进行选择?

分布式锁有三种实现方式:

  • 1 基于数据库实现分布式锁;

    1 悲观锁
    利用select...where...for update排他锁
    但是需要注意,where collection = lock,这个collection字段必须要走索引,否则会锁表. 有些情况下,比如表不大, MySQL优化器会不走这个索引, 导致锁表问题;

    2 乐观锁
    实现方式有二: CAS+乐观锁 -> CAS会有ABA的问题,加上乐观锁的话,就可以解决(友情参考AtomicReference和AtomicStampeReference,后者可以解决前者CAS计算中的ABA问题)

    缺点:

    • db操作性能较差, 并且有锁表的风险;
    • 非阻塞操作失败后, 需要轮询, 占用CPU资源;
    • 长时间不commit或者长时间轮询, 可能会占用较多连接资源;
  • 2 基于Redis实现分布式锁;

    使用命令
    (1) SETNX
    SETNX key val: 当且仅当key不存在时, set一个key为val的字符串,若key不存在进行保存,返回1; 若key存在则什么都不做, 返回0;
    (2) expire
    expire key timeout:为key设置一个超时时间,若超过这个时间,锁会自动释放, 避免死锁;
    (3) delete
    delete key: 删除key

    实现思想
    (1) 获取锁的时候,使用sentnx加锁, 并使用expire命令为锁添加一个超时时间, 超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,用来在释放锁的时候进行判断;
    (2) 获取锁的时候还设置一个获取的超时时间, 若超过这个时间则放弃获取锁;
    (3) 释放锁的时候,通过UUID判断是不是该锁, 若是该锁, 则执行delete进行锁释放;

    Redis有自己专门实现的分布式锁:Redlock

    缺点:

    • 锁删除失败, 过期时间不好控制;
    • 非阻塞操作失败后,需要伦旭占用cpu资源;
    • 主从结构中存在锁安全失效的问题(如客户端A从master获取到锁,在master将该信息同步到slave之前,master宕掉了.此时某slave晋级为master节点,客户端B取得了客户端A已经取到的锁,安全失效.)
  • 3 基于Zookeeper实现分布式锁;
    作为一个为分布式应用提供一致性服务的开源组件, 其实现分布式锁的原理如下:

    1. 在ZK中创建一个持久节点ParentLock. 当第一个客户端想要获得锁时, 需要在ParentLock这个节点下面创建一个临时顺序节点 如 Lock1, 之后 Client1查找ParentLock下面所有的临时顺序节点并排序, 判断自己所创建的节点Lock1是不是最靠前的一个. 如果是第一个节点,则成功获得锁;
    2. 此时,如果另一个客户端Client2 前来获取锁, 则在ParentLock下 再创建一个临时顺序节点Lock2; Client2 查找 ParentLock下面所有的临时顺序节点并排序, 判断自己所创建的节点,Lock2是不是顺序最靠前的一个, 结果发现并不是, 于是Client2 向紧比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在; 这意味着Client2 抢锁失败, 进入了等待状态;
    3. 此时如果有Client3前来获取锁, 其原理同上, 如果Client1仍然持有着锁,那么 Client3 和 Client2 一样,先创建自己的节点Lock3并注册一个Watcher用于监听仅比自己靠前的节点Lock2节点是否存在.
    4. 如果 Client1 使用完了锁, 其会调用删除节点Lock1的指令; 如果 Client1 在任务执行过程中, 出现了异常导致崩溃,则自动断开与ZK服务端的链接. 因此临时节点 Lock1 就会自动被删除; 此时Client2 的Watcher监听到Lock1被删除, 它会再次查询ParentLock下的所有节点,确认自己的节点Lock2是不是目前最小的,如果是,则获得锁;

    方案:
    可使用ZK第三方库Curator客户端,其直接封装了一个可重入的所服务

    Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

    缺点:

    • 性能上可能没有缓存服务那么高. 因为在每次创建锁和释放锁的过程中,都要动态创建, 销毁瞬时节点来实现锁功能; ZK中创建和删除节点只能听过Leader服务器来执行,然后将数据同步到所有的Follower机器上
分布式锁 优点 缺点
Zookeeper 有等待锁的队列,大大提升抢锁效率 添加和删除节点性能低
Redis Set和Del指令的性能较高 没有等待锁的队列,只能在客户端自旋来等锁,效率低下

三种方案的比较:
从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

延2.2 讲下 Redlock

延2.3 讲下 Curator

延3 说说你们的分布式 session 方案是啥? 怎么做的 ?

spring session + redis

<dependency>
  <groupId>org.springframework.sessiongroupId>
  <artifactId>spring-session-data-redisartifactId>
  <version>1.2.1.RELEASEversion>
dependency>
<dependency>
  <groupId>redis.clientsgroupId>
  <artifactId>jedisartifactId>
  <version>2.8.1version>
dependency>

# spring配置文件
<bean id="redisHttpSessionConfiguration"
     class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
    <property name="maxInactiveIntervalInSeconds" value="600"/>
bean>

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxTotal" value="100" />
    <property name="maxIdle" value="10" />
bean>

<bean id="jedisConnectionFactory"
      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
    <property name="hostName" value="${redis_hostname}"/>
    <property name="port" value="${redis_port}"/>
    <property name="password" value="${redis_pwd}" />
    <property name="timeout" value="3000"/>
    <property name="usePool" value="true"/>
    <property name="poolConfig" ref="jedisPoolConfig"/>
bean>

# web.xml
<filter>
    <filter-name>springSessionRepositoryFilterfilter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilterfilter-name>
    <url-pattern>/*url-pattern>
filter-mapping>
@Controller
@RequestMapping("/test")
public class TestController {

@RequestMapping("/putIntoSession")
@ResponseBody
    public String putIntoSession(HttpServletRequest request, String username){
        request.getSession().setAttribute("name",  “leo”);

        return "ok";
    }

@RequestMapping("/getFromSession")
@ResponseBody
    public String getFromSession(HttpServletRequest request, Model model){
        String name = request.getSession().getAttribute("name");
        return name;
    }
}

给sping session配置基于redis来存储session数据,然后配置了一个spring session的过滤器,这样的话,session相关操作都会交给spring session来管了。接着在代码中,就用原生的session操作,就是直接基于spring sesion从redis中获取数据了。

延4 了解分布式事务方案吗? 你们都怎么做的? 有什么坑?

事务解决的是ACID问题,分布式事务就是在集群环境下,如何使数据从一个一致性状态到另一个一致性的状态。

分布式事务方案

方案1: XA方案

XA方案:即两阶段提交方案

两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库准备好了吗? 如果每个数据库都恢复 OK, 那么就正式提交事务,在各个数据库上执行操作, 如果任何一个数据库回答了 NO, 那么就回滚事务;

这种分布式事务,比较适合单块应用里, 跨多个库的分布式事务, 而且因为严重依赖于数据库层面来搞定复杂的事务, 效率很低, 不适合高并发的场景;

方案2: TCC方案

TCC 的全程是: Try Confirm Cancel

该方案其实是用到了补偿的概念,分为三个阶段:

  1. Try阶段: 对各个服务的资源做检测及对资源进行锁定或者预留
  2. Confirm阶段: 在各个服务中执行实际的操作
  3. Cancel阶段: 如果任何一个服务的业务方法执行出错, 那么这里就需要进行补偿, 就是执行已经执行成功的业务逻辑的回滚操作;

举例:跨行转账

/**
1)Try阶段:先把两个银行账户中的资金给它冻结住就不让操作了
2)Confirm阶段:执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加
3)Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,就是比如A银行账户如果已经扣减了,
但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去
*/

这种方案说实话几乎很少用人使用,我们用的也比较少,因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。
但是也有使用的场景:一致性要求极高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿(回滚代码)

方案3: 本地消息表

国外 ebay提出并使用的该套思想

大致思想:

  1. A系统在自己本地执行一个事务的同时,插入一条数据到消息表(此处指的是数据库或者redis中的消息表);
  2. 接着A系统事务执行成功之后,会将这个消息发送到MQ中去;
  3. B系统接收到消息之后, 在一个事务里,往自己本地消息表中插入该条数据,同时执行其它的业务操作; 如果这个消息已经被处理过了,那么此时事务回滚,这样保证不会重复处理消息;
  4. B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表中该消息的状态;
  5. 如果B系统处理失败了, 那么就不会更新本地消息表的状态以及A系统消息表中的状态;

该方案保证了最终一致性,哪怕事务B失败了,但是A会不断重发消息,直到B那边成功为止
但是,其最大的问题在于严重依赖于数据库的消息表来管理事务. 一般使用也很少

方案4: 可靠消息最终一致性方案

该方案放弃了使用本地消息表,而是直接基于MQ实现事务;比如阿里的RocketMQ就支持消息事务;

大致思想:

  1. A系统先发送一个prepared消息到MQ, 如果这个prepared消息发送失败那么就直接取消操作不执行
  2. 如果消息发送成功,那么接着执行本地事务,如果事务执行成功就告诉MQ: 发送确认消息,如果执行失败就告诉MQ: 回滚消息
  3. 如果发送了确认消息, 那么此时B系统会接受到确认消息, 然后执行本地的事务;
  4. MQ会自动定时轮询所有prepared消息回调你的接口,询问这个消息是不是本地事务处理失败了, 所以没发送确认消息? 是继续重试还是回滚?(一般来说这里你需要查一下数据库看之前本地事务是否执行,如果回滚了,那就回复MQ执行回滚),这个就避免可能本地事务执行成功了,但是确认消息发送失败的情况;
  5. 如果系统B的事务失败了, B会自动不断的重试直到成功, 如果实在是不行, 要么是针对重要的资金类业务进行回滚(如:B系统本地回滚后,想办法通知系统A也回滚). 要么是发送报警由人工来手工回滚和补偿;

该方案是我们最常用到的方案

你们公司是如何做的选择?

我们在像放款这种特别严格的场景,用的是TCC来保证强一致性;
然后其他的一些场景基于了阿里的RocketMQ来实现了分布式事务。

但是分布式事务,在绝大多数情况下是不需要使用的,因为无论我们选择哪个,都会导致你那块儿代码会复杂10倍。很多情况下,系统A调用系统B、系统C、系统D,我们可能根本就不做分布式事务。如果调用报错会打印异常日志
因此99%的分布式接口调用,不要做分布式事务,直接就是监控(发邮件、发短信)、记录日志(一旦出错,完整的日志)、事后快速的定位、排查和出解决方案、修复数据。


延5 分布式锁和分布式事务的区别

  1. 锁和事务解决的是两个问题。锁解决的是多线程对共享资源的访问,分布式锁就是说在集群环境下,如何控制不同机器上的多线程对共享资源的访问。事务解决的是ACID问题,如何使数据从一个一致性状态到另一个一致性的状态,分布式事务就是在集群环境下,如果使在不同机器上的数据执行操作使数据从一个一致性状态到另一个一致性状态。
  2. 通常说的分布式锁,我们可以基于数据库for update实现、缓存redis实现、zookeeper实现;分布式事务通常通过2pc、3pc、基于消息的事务来实现。

Dubbo

1 为什么要把系统拆分成分布式的?为什么要用Dubbo?

为什么拆分?
如果不拆分系统, 开发效率极其低下,问题很多. 但是拆分系统之后, 每个人负责自己的小部分,可以大幅度提升复杂系统大型团队的开发效率.

你们系统如何拆分的?

为什么要用Dubbo? 不用可以吗?
如何不用Dubbo(或者Cloud), 也是可以的,只不过就是各个系统之间直接基于SpringMVC,纯Http接口相互通信. 但是你要考虑多台系统弄之间的超时重试,负载均衡等各种问题,维护成本太高;

而Dubbo,它是一种 RPC 架构, 也就是本地执行接口调用的时候Dubbo会代理这个调用请求, 跟远程机器网络通信, 帮你处理负载均衡, 服务实例上下线自动感知, 超时重试等问题.

2 RPC 的原理

Remote Procedure Call 即远程过程调用, 允许一台计算机调用另一台计算机上的程序得到结果,而代码中不需要做额外的编程,就像在本地调用一样;
RPC框架中主要有三个角色:provider,Consumer和Registry;分别是暴露服务的服务提供方,调用远程服务的服务消费方和服务注册与发现的注册中心
服务提供方将服务注册到注册中心,服务调用方像注册中心发起订阅请求得到响应,服务调用方调用服务提供方提供的服务;

3 Dubbo的工作原理是啥? 注册中心挂了会怎样?

Dubbo工作原理:共10层

  1. service层, 接口层, 给服务提供者和消费者来实现
  2. config层, 配置层, 主要是对dubbo进行各种配置的
  3. proxy层, 服务代理层, 透明生成客户端的stub和服务端的 skeleton
  4. registry层, 服务注册层, 负责服务的注册与发现
  5. cluster层, 集群层, 封装多个服务提供者的路由以及负载均衡, 将多个实例组合成一个服务;
  6. monitor层, 监控层, 对 RPC 接口的调用次数和调用时间进行监控
  7. protocol层, 远程调用层, 封装 RPC 调用;
  8. exchange层, 信息交换层, 封装请求响应模式, 同步转异步
  9. transport层, 网络传输层, 抽象 mina 和 netty 为统一接口
  10. serialize层, 数据序列层

Dubbo工作流程
【2020-面试实战】-分布式微服务_第1张图片
【2020-面试实战】-分布式微服务_第2张图片

3.1 Dubbo 单注册表中心会怎样?注册中心挂了还可以继续通信吗?

首先我们要知道,注册中心最好做集群,这样任意一台宕机后,将自动切换到另一台.如果单注册表中心,就不能保证宕机切换的问题. 但是注册中心全部宕掉之后,服务提供者和服务消费者扔能通过本地缓存通讯;

3.2 Dubbo监控中心Monitor的监控原理

Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。

1、MonitorFilter 向 DubboMonitor 发送数据

2、DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference

3、SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000)

4、SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写)

5、SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表

4 Dubbo 如何区分测试和生产环境?

注册中心分组实现测试和生产环境的隔离

5 Dubbo都支持哪些通信协议以及序列化协议?

1)dubbo协议
dubbo://192.168.0.1:20188
默认就是走dubbo协议的,单一长连接,NIO异步通信,基于hessian作为序列化协议
适用的场景就是:传输数据量很小(每次请求在100kb以内),但是并发量很高
为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就100个连接。然后后面直接基于长连接NIO异步通信,可以支撑高并发请求。
否则如果上亿次请求每次都是短连接的话,服务提供者会扛不住。
而且因为走的是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑`高并发访问。

2)rmi协议
java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用

3)hessian协议
hessian序列化协议,多个短连接,适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用

4)http协议
json序列化

5)webservice
SOAP文本序列化

所以dubbo实际基于不同的通信协议,支持hessian、java二进制序列化、json、SOAP文本序列化多种序列化协议。但是hessian是其默认的序列化协议。

6 Dubbo都支持哪些负载均衡,高可用以及动态代理的策略?

负载均衡策略
1)random loadbalance
默认情况下,dubbo是random load balance随机调用实现负载均衡,可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高。

2)roundrobin loadbalance
还有roundrobin loadbalance,这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。

3)leastactive loadbalance
这个就是自动感知一下,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器更少的请求

4)consistanthash loadbalance
一致性Hash算法,相同参数的请求一定分发到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。

集群容错策略
1)failover cluster模式
失败自动切换,自动重试其他机器,默认就是这个,常见于读操作

2)failfast cluster模式
一次调用失败就立即失败,常见于写操作

3)failsafe cluster模式
出现异常时忽略掉,常用于不重要的接口调用,比如记录日志

4)failback cluster模式
失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种

5)forking cluster
并行调用多个provider,只要一个成功就立即返回

6)broadcacst cluster
逐个调用所有的provider

动态代理策略
默认使用 javassist 动态字节码生成,创建代理类
但是可以通过spi扩展机制配置自己的动态代理策略

7 SPI 是什么思想? Dubbo的 SPI 机制是怎么玩的?

SPI思想
SPI:Service Provider Interface
微内核,可插拔,大量的组件,Protocol负责rpc调用的东西,你可以实现自己的rpc调用组件,实现Protocol接口,给自己的一个实现类即可。

Dubbo系统在运行的会后,会判断应该选择Protocol接口的哪个实现类来实例化对象.它会去找一个你配置的Protocol,将你配置的Protocol实现类加载到JVM中来, 然后实例化对象,如果你没有特殊的实现和配置,就走默认的实现;

SPI机制是怎么玩的
Dubbo的Protocol接口

@SPI("dubbo")  
public interface Protocol {  
    int getDefaultPort();  
    @Adaptive  
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  
    @Adaptive  
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  
    void destroy();  
}  

在dubbo自己的jar里,在 /META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol 文件中:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

所以说,这就看到了dubbo的spi机制默认是怎么玩儿的了,其实就是Protocol接口,@SPI(“dubbo”)说的是,通过SPI机制来提供实现类,实现类是通过dubbo作为默认key去配置文件里找到的,配置文件名称与接口全限定名一样的,通过dubbo作为key可以找到默认的实现了就是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol。

dubbo的默认网络通信协议,就是dubbo协议,用的DubboProtocol

如果想要动态替换掉默认的实现类,需要使用@Adaptive接口,Protocol接口中,有两个方法加了@Adaptive注解,就是说那俩接口会被代理实现。

8 基于Dubbo如何做服务治理,服务降级以及重试?

服务治理思想
(1) 调用链路自动生成
基于Dubbo做的分布式系统中, 对各个服务之间的调用自动记录下来, 然后自动将各个服务之间的依赖关系和调用链路生成出来,做成一张图,专门页面显示出来;(系统中workflow执行流程,就是通过日志回写的形式,将服务调用之间的关系的展示)

(2) 服务访问压力以及时长统计
需要自动统计各个接口和服务之间的调用次数以及访问延时,而且要分成两个级别。一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50,TP90,TP99,三个档次的请求延时分别是多少;第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的TP50,TP90,TP99,分别是多少。

TP指标: 指在一个时间段内,统计该方法每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序,并取出结果为:总次数 * 指标数 = 对应TP指标的值, 在取出排序好的时间。
TP50:指在一个时间段内(如5分钟),统计该方法每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序,取第50%的那个值作为TP50 值;配置此监控指标对应的报警阀值后,需要保证在这个时间段内该方法所有调用的消耗时间至少有50%的值要小于此阀值,否则系统将会报警。

服务降级
Dubbo的服务降级思路:

  1. 在Dubbo配置中,将mock配置为true
  2. 在你需要实现的接口的同一个路径下实现一个Mock类,命名规则就是接口名称加Mock后缀,然后在Mock中实现自己的降级逻辑;(比如返回固定的retCode和retMessage)

失败重试和超时重试
所谓失败重试,就是consumer调用provider要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。

<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>

只需要在Dubbo的配置中,将retries和timeout配置出来,比如重试3次,超时时间2000毫秒;
如果重试三次都失败的话,就会走降级策略

9 分布式系统中接口的幂等性该如何保证? 举例说明(如重复扣款的问题)

所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款,不能多插入一条数据,不能将统计值多加了1。这就是幂等性;

其实保证幂等性主要是三点:
(1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次;
(2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysql中记录个状态(版本号),比如支付之前记录一条这个订单的支付流水,而且支付流水采;
(3)每次接收请求需要进行判断之前是否处理过的逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。

//避免重复支付的例子
/**
比如说用redis用orderId作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
要求是支付一个订单,必须插入一条支付流水,order_id建一个唯一键,unique key
所以你在支付一个订单之前,先插入一条支付流水,order_id就已经进去了
你就可以写一个标识到redis里面去,set order_id payed,下一次重复请求过来了,先查redis的order_id对应的value,
如果是payed就说明已经支付过了,你就别重复支付了
然后呢,你再重复支付这个订单的时候,你写尝试插入一条支付流水,数据库给你报错了,说unique key冲突了,整个事务回滚就可以了
来保存一个是否处理过的标识也可以,服务的不同实例可以一起操作redis。
*/

10 分布式系统中的接口如何保证顺序性?

首先,一般来说,我个人给你的建议是,你们从业务逻辑上最好设计的这个系统不需要这种顺序性的保证,因为一旦引入顺序性保障,会导致系统复杂度上升,而且会带来效率低下,热点数据压力过大,等问题;

方案1
使用dubbo的一致性hash负载均衡策略,将比如某一个订单id对应的请求都给分发到某个机器上去,接着就是在那个机器上因为可能还是多线程并发执行的, 因此需要立即将某个订单id对应的请求扔一个内存队列里去,强制排队,这样来确保他们的顺序性。

方案2
使用分布式锁 保证100%的顺序性;但是分布式锁会存在多次获取释放的过程,比较消耗性能;

  1. 用户的多个操作请求在发送到系统的时候需要携带一个顺序标识, 如seq=1,seq=2,seq=3
  2. 获取分布式锁之后,通过标识做判断,是否可以执行该请求,如果不可以就释放锁;

11 如何设计一个类似 Dubbo 的 RPC框架? 架构上该如何考虑?

简单思路:
(1)上来你的服务就得去注册中心注册吧,你是不是得有个注册中心,保留各个服务的信心,可以用zookeeper来做,对吧
(2)然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上
(3)接着你就该发起一次请求了,咋发起?蒙圈了是吧。当然是基于动态代理了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址
(4)然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是
(5)接着找到一台机器,就可以跟他发送请求了,第一个问题咋发送?你可以说用netty了,nio方式;第二个问题发送啥格式数据?你可以说用hessian序列化协议了,或者是别的,对吧。然后请求过去了。。
(6)服务器那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口了,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码。


SpringCloud

12 说一下Spring Cloud的核心架构原理?

Spring Cloud 核心包含: Eureka, Ribbon, Feign

Eureka,Ribbon,Feign,Zuul
Eureka服务注册中心: 它是Spring Cloud最关键的模块
其核心有2大功能,一个是服务注册与发现 ;一个是心跳与故障

剩下的Ribbon(负载均衡),Feign(服务调用)都是在Eureka的基础上进行使用的

其工作原理如下:

  1. 首先Eureka本身维护着 服务注册表,ReadWrite缓存, ReadOnly缓存 以及服务检查线程心跳检查线程;

    Eureka之所以维护一个读写缓存和一个只读缓存,是为了解决 并发冲突的问题, 即我们所说的读写需要互斥的问题

  2. 当某服务A启动后,作为服务提供者,向Eureka进行服务注册,将自身接口信息注册到Eureka服务注册表中,服务注册表会将该信息立马同步到ReadWrite缓存里面;
  3. 服务检查线程会每隔30秒(可配置)检查 ReadWrite缓存中是否有新的数据,如果有会将该数据同步到ReadOnly缓存中;
  4. 其它作为使用者的服务,也会定时进行一次服务发现,定时将ReadOnly缓存中的数据更新到本地服务注册表中,进行服务调用
  5. 作为服务提供者的A在启动之后会定时向Eureka的服务注册表中发送心跳,表明自己仍然存活
  6. Eureka中的心跳检查线程也会定时检查服务注册表中各个服务的心跳发送情况,如果超过一定的时间(如60秒),服务A的心跳状态仍然未更新,此时会将该服务标记为下线,并立马将ReadWrite缓存中的数据清空
  7. 服务检查线程也会将ReadOnly缓存中的数据进行清空
  8. 最终各个服务消费者通过服务发现,将本地维护的服务注册表清空
  9. 直到有新的服务提供者向Eureka的服务注册表中进行服务注册,会开始第1步的操作

接上面Eureka服务注册于发现完成之后,开始进行服务调用:
在进行服务调用的时候,Feign,它是通过对一个接口添加了@FeignClient注解,所有被@FeignClient标注的接口会生成动态代理,然后针对动态代理去调用接口中方法的时候,Feign会在底层生成 HTTP 协议格式的请求
.
底层的话,使用HTTP通信的框架组件,HttpClient, 先得使用 Ribbon去本地服务中维护的Eureka注册表的缓存中获取对方机器的列表,然后进行负载均衡,选择出一台机器出来, 接着针对那台机器发送 HTTP 请求过去即可;

12.1 Ribbon 的负载均衡策略

  • RandomRule 随机选择
  • RoundRobinRule 轮询策略
  • BestAvailableRule 最大可用策略
  • WeightedResponseTimeRule 加权的轮询策略
  • AvailablityFilteringRule 可用过滤策略
  • ZoneAvoidanceRule 区域感知策略

13 服务注册中心的对比,选择

(1) 两个服务注册中心的原理

  • Zookeeper
  • Eureka

Eureka的服务注册原理在上面已经说过了

Eureka集群架构原理

Eureka集群中,其每一个机器的地位都是对等的,各个服务可以向任何一个 Eureka 实例进行服务注册和服务发现, 集群里任何一个 Eureka 实例接收到写请求之后, 会自动同步给其它所有的Eureka实例

Zookeeper集群原理

Zookeeper服务注册和发现, 其集群中存在 Leader 和 Follower 两种角色,只有Leader可以负责写,也就是服务注册,它会把数据同步给Follower, 读的时候Leader/Follower都可以读

(2) 两个服务注册中心的一致性保证
CP or AP

CAP: C 一致性, A 可用性, P 分区容错性

Zookeeper 采取的是 CP原则

Zookeeper是有一个 Leader 节点会接收数据, 然后同步写其他Follower节点, 一旦 Leader 挂了,要重新选举 Leader, 这个过程为了保证 C, 就牺牲了 A, 不可用一段时间, 但是一个 Leader 选举好了, 那么就可以继续写数据了, 保证强一致性;
【2020-面试实战】-分布式微服务_第3张图片

Eureka 采取的是 AP原则

Eureka 是 peer 模式, 可能某一个 Eureka 还没同步数据到其它 Eureka,结果自己挂掉, 此时还是可以继续从别的Eureka上拉取注册表, 但是看到的就不是最新的数据了, 但是保证了可用性和最终一致性;

(3) 两个服务注册中心的时效性

  • Zookeeper

    zk 的时效性特别好, 进行服务注册或者有服务挂掉,一般都是秒级的感知

  • Eureka

    Eureka的默认配置非常糟糕, 服务发现感知要到几十秒甚至分钟级别

(4) 两个服务注册中心的容量
Zookeeper, 不适合大规模的服务实例, 因为服务上下线的时候, 需要瞬间推送数据通知到所有的其他服务实例, 所以一旦服务规模太大(几千的服务实例), 就会导致网络宽带被大量占用

Eureka, 也很难支撑大规模的服务实例, 因为每个 Eureka 实例都要接收所有的请求,实例多了压力太大, 扛不住,也很难到几千服务实例

13.1 注册中心集群如何保证高可用

Eureka注册中心集群中,如果有任何一台Eureka挂掉, 我们所有的服务注册和服务发现都会自动转到去其它Eureka实例

Zookeeper注册中心集群, 由于ZK保证的是CP,而放弃了A即可用性,其在高可用上很难达到

13.2 你们系统遇到过服务发现过慢的问题吗?

Zookeeper做注册中心的时候,基本没有遇到过

但是Eureka 我们在使用的时候必须优化其默认的参数

#eureka server刷新readCacheMap的时间,注意,client读取的是readCacheMap,
#readCacheMap即ReadOnly缓存
#这个时间决定了多久会把readWriteCacheMap的缓存更新到readCacheMap上
#默认30s
eureka.server.responseCacheUpdateIntervalMs = 30000

#eureka client刷新本地缓存时间
#默认30s
eureka.client.registryFetchchintervalSeconds = 30000

#表示eureka client发送心跳给server端的频率
eureka.client.leaseRenewalIntervalInSeconds = 30

#清理间隔
eureka.server.evictionIntervalTimerInMs = 60000

#租期到期时间
eureka.instance.lease.ExpirationDurationInSeconds = 90
#关闭自我保护
eureka.server.enable-self-preservation=false

14 服务网关的技术对比,选型

首先,网关的核心功能:

  • 动态路由: 新开发某个服务,动态把请求路径和服务的映射关系热加载到网关里去,服务增减机器,网关自动热感知
  • 灰度发布
  • 授权认证
  • 性能监控: 每个 API 接口的耗时, 成功率, QPS
  • 系统日志
  • 数据缓存
  • 限流熔断

其次,网关技术类型:

  • Kong: Nginx里面的一个基于 lua 写的模块, 实现了网关的功能
  • Zuul : Spring Cloud 中使用的网关技术
  • Nginx+lua(OpenResty)
  • 自研网关(基于Servlet Netty来做网关)

大厂一般自研, 中小公司主要是使用Zuul, 也可以不用网关,直接通过Nginx反向代理,做负载均衡

14.1 生产环境下,怎么实现的网关对服务的动态路由?

可通过Apollo配置中心进行操作

14.2 如果网关要抗每秒10万级的高并发, 如何做优化?

首先我们需要知道,所有的请求到后台,最合理的配置应该是先到LVS,通过LVS做负载均衡均匀分发到Nginx上,然后再通过Nginx均匀分发到Zuul集群中

LVS+Nginx是运维工程师需要关注的

在这里插入图片描述

14.3 你们是如何基于网关做灰度发布的?

Dubbo
通过自定义负载均衡实现灰度发布;

实现LoadBalance接口或者继承AbstractLoadBalance重写策略
根据dubbo SPI发现机制,在resources下添加META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance
逻辑:目标服务的端口和灰度服务端口的一致,并且请求方法的第一个参数类型是Long(userId)并且是灰度用户,则判断为灰度服务,否则按照默认随机调用其余非灰度服务

Spring Cloud
Zuul+数据库配置实现灰度发布

准备一个数据库和一个表(也可以用Apollo配置中心,redis,zk)放一个灰度发布启用表
在Zuul里面开启ZuulFilter(即继承,复写其run方法,在run方法中写具体的灰度发布逻辑)

15 说一下服务熔断和降级

服务熔断
当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
服务熔断其实是框架级的处理,在Cloud中我们使用的熔断框架是Hystrix

//滑动窗口的大小,默认为20
circuitBreaker.requestVolumeThreshold 
//过多长时间,熔断器再次检测是否开启,默认为5000,即5s钟
circuitBreaker.sleepWindowInMilliseconds 
//错误率,默认50%
circuitBreaker.errorThresholdPercentage

**以上Hystrix配置表示,每当20个请求中,有50%失败时,熔断器就会打开,此时再调用此服务,将会直接返回失败,不再调远程服务。直到5s钟之后,重新检测该触发条件,判断是否把熔断器关闭,或者继续打开。
这些属于框架层级的实现,我们只要实现对应接口就好!

服务降级

  • 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度!
  • 当下游的服务因为某种原因不可用,上游主动调用本地的一些降级逻辑,避免卡顿,迅速返回给用户!

降级方式:

  • 开关降级(业务降级), 限流降级, 熔断降级

服务降级大多是属于一种业务级别的处理,我们在开发中最常用的降级方式就是开关降级

做法很简单,做个开关,然后将开关放配置中心(数据字典也是可以的)!在配置中心更改开关,决定哪些服务进行降级。

在应用程序中部下开关的这个过程,业内也有一个名词,称为埋点,哪些业务需要埋点?

  • 系统中的非核心业务流程可以埋点
  • 微服务下 次要的服务
  • 核心业务流程同步改异步

16 你们公司的各个服务在生产环境是如何部署的?机器配置如何?

服务注册中心
【2020-面试实战】-分布式微服务_第4张图片
在这里插入图片描述
网关
在这里插入图片描述
数据库(MySQL)
在这里插入图片描述

17 你们系统每天有多大访问量?每个服务高峰多少QPS?压测过服务最大QPS吗?

【2020-面试实战】-分布式微服务_第5张图片

18 你们系统是如何配置超时和重试参数的?

Spring Cloud 生产优化:
有一个普遍的现象,就是Cloud系统在第一次启动的时候, 人家调用你的应用经常会出现 timeout

这是因为,每个服务每次请求的时候,他会去初始化一个Ribbon的组件, 初始化这些组件需要耗费一定的时间,所以很容易会导致 调用timeout的问题; 我们只需要让每个服务启动的时候就直接初始化 Ribbon 相关的组件,避免第一次请求的时候初始化

ribbon:
	eager-load:
		enabled: true

zuul:
	ribbon:
		eager-load:
			enabled: true		

【2020-面试实战】-分布式微服务_第6张图片

19 对于核心接口的防重幂等性你们是如何设计的?

方式大致有2种:

  • 基于数据库唯一索引

    对于插入类操作, 一般都是建议在数据库表中设计一些唯一索引
    举例订单发货操作:
    我们可以将order_id设置为唯一索引,插入发货单的时候,同一个order_id就只能创建一个发货单

  • 基于Redis实现一套幂等性防重框架

    对于像扣减库存,累加积分这种更新操作, 我们可以基于 Redis 实现一套接口的防重框架
    首先需要在拦截器中,拦截所有的请求,对所有请求的参数进行处理
    将参数+接口名称 拼接在一起,作为key去redis中判断一下,是否存在这个key, 之前附加这些参数的请求是否发起过,如果没有就可以把这些参数+接口名称 作为一个key 存储到redis中
    然后把请求放行,去执行这个请求(如果请求失败可以将redis中的key删除)
    如果说调用方重试再次发起这个请求,此时就可以判断出参数组成的key在redis中已经存在了, 我们就任务是重复调用

20 从底层原理的角度,对比下Dubbo和Spring Cloud的优劣

Dubbo 的定位始终是一款 RPC 框架,而 Spring Cloud 的目标是微服务架构下的一站式解决方案。

因此我们可以将Dubbo看做是Spring Cloud的一个子集

除此之外 Spring Cloud 还提供了配置、消息、安全、调用链跟踪等分布式问题解决方案。这都是Dubbo所不具备的
【2020-面试实战】-分布式微服务_第7张图片

21 淘宝和京东的库存是怎么实现的?

【2020-面试实战】-分布式微服务_第8张图片

你可能感兴趣的:(面试,笔记)