Bootstrap

架构设计面试题 (史上最全、持续更新、吐血推荐)

文章很长,建议收藏起来,慢慢读! 高并发学习社群 - 疯狂创客圈奉献给大家:


文章目录


价值连城:2021春招月薪过5万 面试题 系列

搞定下面这些面试题,2021春招月薪过5万(猛!)阿里、京东、美团、头条… 随意挑、横着走!!!
Java基础
1: JVM面试题(史上最强、持续更新、吐血推荐)https://www.cnblogs.com/crazymakercircle/p/14365820.html
2:Java基础面试题(史上最全、持续更新、吐血推荐)https://www.cnblogs.com/crazymakercircle/p/14366081.html
3:死锁面试题(史上最强、持续更新)[https://www.cnblogs.com/crazymakercircle/p/14323919.html]
4:设计模式面试题 (史上最全、持续更新、吐血推荐)https://www.cnblogs.com/crazymakercircle/p/14367101.html
5:架构设计面试题 (史上最全、持续更新、吐血推荐)https://www.cnblogs.com/crazymakercircle/p/14367907.html
还有 10 几篇价值连城 的面试题具体… 请参见【 疯狂创客圈 高并发 总目录

万字长文: 疯狂创客圈 springCloud 高并发系列

springCloud 高质量 博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
springcloud + webflux 高并发实战 Webflux(史上最全)
SpringCloud gateway (史上最全)
还有 10 几篇 万字长文 的高质量 博文具体… 请参见【 疯狂创客圈 高并发 总目录

2021春招月薪过5万(猛!惊!)面试题之:架构设计篇

场景题

整理了一些常见的架构设计面试题,主要记录关键点,具体细节就不详细叙述了,案例慢慢补充。目前想起以下问题:

  • 秒杀系统
  • 短链接生成
  • 高并发的红包系统
  • 分布式ID生成
  • 分布式限流
  • 分布式定时任务
  • 新浪微博怎么推送微博
  • 大文件有限内存排序

秒杀系统

秒杀系统基本面试被问烂了,网上资料也很多,基本整理了内容如下:

设计难点:并发量大,应用、数据库都承受不了。另外难控制超卖。

设计要点:

  • 将请求尽量拦截在系统上游,html尽量静态化,部署到cdn上面。按钮及时设置为不可用,禁止用户重复提交请求。
  • 设置页面缓存,针对同一个页面和uid一段时间内返回缓存页面。
  • 数据用缓存抗,不直接落到数据库。
  • 读数据的时候不做强一致性教研,写数据的时候再做。
  • 在每台物理机上也缓存商品信息等等变动不大的相关的数据
  • 像商品中的标题和描述这些本身不变的会在秒杀开始之前全量推送到秒杀机器上并一直缓存直到秒杀结束。
  • 像库存这种动态数据会采用被动失效的方式缓存一定时间(一般是数秒),失效后再去Tair缓存拉取最新的数据。
  • 如果允许的话,用异步的模式,等缓存都落库之后再返回结果。
  • 如果允许的话,增加答题教研等验证措施。

其他业务和技术保障措施:

  • 业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就是已知热点,当真正开始时我们可以提前做好预热。
  • 系统隔离。系统隔离更多是运行时的隔离,可以通过分组部署的方式和另外 99% 分开。秒杀还申请了单独的域名,目的也是让请求落到不同的集群中。
  • 数据隔离。秒杀所调用的数据大部分都是热数据,比如会启用单独 cache 集群或 MySQL 数据库来放热点数据,目前也是不想0.01%的数据影响另外99.99%。

另外需要复习缓存穿透、雪崩等等问题,主要的流量都落在了缓存数据库上,需要针对缓存数据库的高可用作保障。

短链接生成

这个应该是比较公认的方案了:

  1. 分布式ID生成器产生ID
  2. ID转62进制字符串
  3. 记录数据库,根据业务要求确定过期时间,可以保留部分永久链接

主要难点在于分布式ID生成。鉴于短链一般没有严格递增的需求,可以使用预先分发一个号段,然后生成的方式。

看了下新浪微博的短链接,8位,理论上可以保存超过200万亿对关系,具体怎么存储的还有待研究。

红包系统

红包系统其实很像秒杀系统,只不过同一个秒杀的总量不大,但是全局的并发量非常大,比如春晚可能几百万人同时抢红包。

主要技术难点也类似,主要在数据库,减库存的时候会抢锁。另外由于业务需求不同,没办法异步,也不能超卖,事务更加严格。

不能采用的方式:

  • 乐观锁:手慢会失败,DB 面临更大压力,所以不能采用。
  • 直接用缓存顶,涉及到钱,一旦缓存挂掉就完了。

建议的方式:

  • 接入层垂直切分,根据红包ID,发红包、抢红包、拆红包、查详情详情等等都在同一台机器上处理,互不影响,分而治之。
  • 请求进行排队,到数据库的时候是串行的,就不涉及抢锁的问题了。
  • 为了防止队列太长过载导致队列被降级,直接打到数据库上,所以数据库前面再加上一个缓存,用CAS自增控制并发,太高的并发直接返回失败。
  • 红包冷热数据分离,按时间分表。

分布式ID

分布式ID生成大概也算老生常谈的问题了,主要关键在于是否需要严格递增,严格递增的话效率必然大降。

不需要递增的话比较简单:

  • 一种方式是预先分片,比如十台机器,每台先分一千个ID,一号机从0开始,二号从1000开始等等。缺点是大致上可以被人看出来业务量。
  • 另一种方式是类似雪花算法,每个机器有个id,然后基于时间算一个id,再加上一个递增id。比如如下美团的方案。缺点是机器的时间戳不能回拨,回拨的话会出现问题。

img

如果要求严格递增,我没找到现成的很好的方案,大概只能单机生成,不能分布式了,然后都去单机上取号。效率的话,类似Redis的数据库大概能到每秒十几二十几万的速度。

分布式限流

常见的限流方法:

  • 固定窗口计数器:按照时间段划分窗口,有一次请求就+1,最为简单的算法,但这个算法有时会让通过请求量允许为限制的两倍。
  • 滑动窗口计数器:通过将窗口再细分,并且按照时间“滑动”来解决突破限制的问题,但是时间区间的精度越高,算法所需的空间容量就越大。
  • 漏桶:请求类似水滴,先放到桶里,服务的提供方则按照固定的速率从桶里面取出请求并执行。缺陷也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。
  • 令牌桶:往桶里面发放令牌,每个请求过来之后拿走一个令牌,然后只处理有令牌的请求。令牌桶满了则多余的令牌会直接丢弃。令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。

Google 的开源项目 guava 提供了 RateLimiter 类,实现了单点的令牌桶限流。

分布式环境下,可以考虑用 Redis+Lua 脚本实现令牌桶。

如果请求量太大了,Redis 也撑不住怎么办?我觉得可以类似于分布式 ID 的处理方式,Redis 前面在增加预处理,比如每台及其预先申请一部分令牌,只有令牌用完之后才去 Redis。如果还是太大,是否可以垂直切分?按照流量的来源,比如地理位置、IP 之类的再拆开。

分布式定时任务

任务轮询或任务轮询+抢占排队方案

  • 每个服务器首次启动时加入队列;
  • 每次任务运行首先判断自己是否是当前可运行任务,如果是便运行;
  • 如果不是当前运行的任务,检查自己是否在队列中,如果在,便退出,如果不在队列中,进入队列。

微博推送

主要难点:关系复杂,数据量大。一个人可以关注非常多的用户,一个大 V 也有可能有几千万的粉丝。

先介绍最基本的方案:

  1. 推模式:推模式就是,用户A关注了用户 B,用户 B 每发送一个动态,后台遍历用户B的粉丝,往他们粉丝的 feed 里面推送一条动态。
  2. 拉模式:推模式相反,拉模式则是,用户每次刷新 feed 第一页,都去遍历关注的人,把最新的动态拉取回来。

一般采用推拉结合的方式,用户发送状态之后,先推送给粉丝里面在线的用户,然后不在线的那部分等到上线的时候再来拉取。

另外冷热数据分离,用户关系在缓存里面可以设置一个过期时间,比如七天。七天没上线的可能就很少用这个 APP。

大文件排序

对于远高于内存的文件排序。

外归并排序:

  • 对文件分割,然后分别排序
  • 排好序的文件依次读取一个缓冲区的大小,然后进行排序,输出到输出缓冲区,然后保存到结果文件。

如果是数字,可以用位图排序,但是要求比较苛刻:

  • 数字不重复
  • 知道最大值
  • 相对密集,因为没出现的数字也会占用空间

比较适合电话号之类的。

其他场景题

1、情景题:如果一个外卖配送单子要发布,现在有200个骑手都想要接这一单,如何保证只有一个骑手接到单子?

这个可以用redis的lpush rpop来实现。

2、场景题:美团首页每天会从10000个商家里面推荐50个商家置顶,每个商家有一个权值,你如何来推荐?第二天怎么更新推荐的商家?

这个可以用堆排,第二天更新推荐的可以在要更新之前的时候在redis做计算操作,然后放数据库做个同步就行了。

3、场景题:微信抢红包问题
悲观锁,乐观锁,存储过程放在mysql数据库中。
4、场景题:1000个任务,分给10个人做,你怎么分配,先在纸上写个最简单的版本,然后优化。
全局队列,把1000任务放在一个队列里面,然后每个人都是取,完成任务。
分为10个队列,每个人分别到自己对应的队列中去取务。
5、场景题:保证发送消息的有序性,消息处理的有序性。

给消息加一个header,识别一下header的syn就行

6、如何把一个文件快速下发到100w个服务器

边下发,边复制

7、给每个组分配不同的IP段,怎么设计一种结构使的快速得知IP是哪个组的?

8、10亿个数,找出最大的10个。
建议一个大小为10的小根堆。
9、有几台机器存储着几亿淘宝搜索日志,你只有一台2g的电脑,怎么选出搜索热度最高的十个搜索关键词?

分bucket,每个bucket找出频次最高的10个,总体用分治算法做。

10、分布式集群中如何保证线程安全?

分布式锁,意在让分布式多线程的环境针对一个共享资源一次性只会有一个线程获取到锁里的资源。分布式锁的实现一般就是redis和zk居多。redis设置一下expire time就行。

11、10万个数,输出从小到大?
先划分成多个小文件,送进内存排序,然后再采用多路归并排序。
12、有十万个单词,找出重复次数最高十个?

map<String,Integer> 字符串,频次,然后堆排。

场景题答题小建议:

架构设计题目远不止这些,我觉得主要从以下几个方面准备:

  • 先了解常用算法,针对解决各种问题能用哪些算法,比如大文件排序用外排序,大量数据中的命中判断用位图/布隆过滤器等等。
  • 注意扩展性、多考虑极端情况,多问自己几个为什么。比如说起单机的限流算法想想分布式的怎么做。
  • 实在不知道怎么弄的叙述自己的思考过程,着重展示自己考虑周全、思维缜密。

比如之前一个同事面阿里被问整个机房网络挂了/断电了之类的极端问题,相信很多人不可能真的遇到这种情况,而且可用性一般也到不了这个级别。如果是我的话,我会着重考虑数据一致性、数据恢复、脏数据处理之类的问题,是否有其他机房提供服务,如果有的话涉及到网络分区,是不是可以引申谈谈 zab、raft 算法,另外原本两个或者三个机房提供服务,现在瞬间少了一个,其他机房的负载瞬间上升,怎么做削峰、降级,这就有回到我们会的问题上了。另外等到网络恢复了,怎么恢复服务?之前处理到一半的数据怎么处理?这些引申开来都有太多可以聊。

分布式微服务架构

问1.什么是分布式系统

分布式系统的目标与要素

分布式系统的目标是提升系统的整体性能和吞吐量另外还要尽量保证分布式系统的容错性(假如增加10台服务器才达到单机运行效果2倍左右的性能,那么这个分布式系统就根本没有存在的意义)。

即使采用了分布式系统,我们也要尽力运用并发编程、高性能网络框架等等手段提升单机上的程序性能。

分布式系统设计两大思路:中心化和去中心化

分布式系统设计两大思路:中心化和去中心化

中心化设计

  • 两个角色: 中心化的设计思想很简单,分布式集群中的节点机器按照角色分工,大体上分为两种角色: “领导”“干活的”
  • 角色职责: “领导”通常负责分发任务并监督“干活的”,发现谁太闲了,就想发设法地给其安排新任务,确保没有一个“干活的”能够偷懒,如果“领导”发现某个“干活的”因为劳累过度而病倒了,则是不会考虑先尝试“医治”他的,而是一脚踢出去,然后把他的任务分给其他人。其中微服务架构 Kubernetes 就恰好采用了这一设计思路。
  • 中心化设计的问题
    1. 中心化的设计存在的最大问题是“领导”的安危问题,如果“领导”出了问题,则群龙无首,整个集群就奔溃了。但我们难以同时安排两个“领导”以避免单点问题。
    2. 中心化设计还存在另外一个潜在的问题,既“领导”的能力问题:可以领导10个人高效工作并不意味着可以领导100个人高效工作,所以如果系统设计和实现得不好,问题就会卡在“领导”身上。
  • 领导安危问题的解决办法: 大多数中心化系统都采用了主备两个“领导”的设计方案,可以是热备或者冷备,也可以是自动切换或者手动切换,而且越来越多的新系统都开始具备自动选举切换“领导”的能力,以提升系统的可用性。

去中心化设计

  • 众生地位平等: 在去中心化的设计里,通常没有“领导”和“干活的”这两种角色的区分,大家的角色都是一样的,地位是平等的,全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备宕机,都只会影响很小范围的功能。
  • “去中心化”不是不要中心,而是由节点来自由选择中心。 (集群的成员会自发的举行“会议”选举新的“领导”主持工作。最典型的案例就是ZooKeeper及Go语言实现的Etcd)
  • 去中心化设计的问题: 去中心化设计里最难解决的一个问题是 “脑裂”问题 ,这种情况的发生概率很低,但影响很大。脑裂指一个集群由于网络的故障,被分为至少两个彼此无法通信的单独集群,此时如果两个集群都各自工作,则可能会产生严重的数据冲突和错误。一般的设计思路是,当集群判断发生了脑裂问题时,规模较小的集群就“自杀”或者拒绝服务。
分布式与集群的区别是什么?
  • 分布式: 一个业务分拆多个子业务,部署在不同的服务器上
  • 集群: 同一个业务,部署在多个服务器上。比如之前做电商网站搭的redis集群以及solr集群都是属于将redis服务器提供的缓存服务以及solr服务器提供的搜索服务部署在多个服务器上以提高系统性能、并发量解决海量存储问题。

问2. 如何防止HA集群的脑裂

1. 引言

脑裂(split-brain),指在一个高可用(HA)系统中,当联系着的两个节点断开联系时,本来为一个整体的系统,分裂为两个独立节点,这时两个节点开始争抢共享资源,结果会导致系统混乱,数据损坏。

对于无状态服务的HA,无所谓脑裂不脑裂;但对有状态服务(比如MySQL)的HA,必须要严格防止脑裂。(但有些生产环境下的系统按照无状态服务HA的那一套去配置有状态服务,结果可想而知…)

2如何防止HA集群脑裂

一般采用2个方法

  1. 仲裁
    当两个节点出现分歧时,由第3方的仲裁者决定听谁的。这个仲裁者,可能是一个锁服务,一个共享盘或者其它什么东西。
  2. fencing
    当不能确定某个节点的状态时,通过fencing把对方干掉,确保共享资源被完全释放,前提是必须要有可靠的fence设备。

理想的情况下,以上两者一个都不能少。但是,如果节点没有使用共享资源,比如基于主从复制的数据库HA,我们也可以安全的省掉fence设备,只保留仲裁。而且很多时候我们的环境里也没有可用的fence设备,比如在云主机里。

所以,单纯的双节点,无论如何也防止不了脑裂。

3. 没有fence设备是否安全

以PostgreSQL或MySQL的数据复制为例来说明这个问题。

客户端路由有几种方式,基于VIP,基于Proxy,基于DNS或者干脆客户端维护一个服务端地址列表自己判断主从。不管采用哪种方式,主从切换的时候都要更新路由。

基于VIP的路由有一些变数,如果本该死掉的节点没有摘掉自己身上的VIP,那么它随时可能出来捣乱(即使新主已经通过arping更新了所有主机上的arp缓存,如果某个主机的arp过期,发一个arp查询,那么就会发生ip冲突)。所以可以认为VIP也是一种特殊的共享资源,必需把它从故障节点上摘掉。至于怎么摘,最简单的办法就是故障节点发现自己失联后自己摘,如果它还活着的话(如果它死了,也就不用摘了)。如果负责摘vip的进程无法工作怎么办?这时候就可以用上本来不太靠谱的软fence设备了(比如ssh)。
也要考虑Proxy的高可用
至于基于服务端地址列表的方法,客户端需要通过后台服务判断主从(比如PostgreSQL/MySQL会话是否处于只读模式)。这时,如果出现2个主,客户端就会错乱。为防止这个问题,原主节点发现自己失联后要自己把服务停掉,这和前面摘vip的道理是一样的。

4. 主从切换后数据能否保证不丢

主从切换后数据会不会丢和脑裂可以认为是2个不同的问题。还以PostgreSQL或MySQL的数据复制为例来说明。
对PostgreSQL,如果配置成同步流复制,可以做到不管路由是否正确,都不会丢数据。因为路由到错误节点的客户端根本写不进任何数据,它会一直等待从节点的反馈,而它以为的从节点,现在已经是主子了,当然不会理它。当然如果老是这样也不好,但它给集群监视软件纠正路由错误提供了充足的时间。
如果本来就是配置的异步复制,那就是说已经做好丢数据的准备了。这时候,主从切换时丢点数据也没啥大不了,但要控制自动切换的次数。比如控制已经被failover掉的原主不允许自动上线,否则如果因为网络抖动导致故障切换,那么主从就会不停的来回切,不停的丢数据,破坏数据的一致性。

5. 如何实现上面的策略

你可以自己完全从头开始实现一套符合上述逻辑的脚本。但我更倾向于基于成熟的集群软件去搭建,比如Pacemaker+Corosync+合适的资源Agent。Keepalived我是极不推荐的,它就不适合用于有状态服务的HA,即使你把仲裁,fence那些东西都加到方案里,总觉得别扭。

使用Pacemaker+Corosync的方案也有一些注意事项
1)了解资源Agent的功能和原理
了解资源Agent的功能和原理,才能知道它适用的场景。比如pgsql的资源Agent是比较完善的,支持同步和异步流复制,并且可以在两者之前自动切换,并且可以保证同步复制下数据不会丢失。但目前MySQL的资源Agent就很弱了,没有使用GTID又没有日志补偿,很容易丢数据,还是不要用的好,继续用MHA吧(但是,部署MHA时务必要防范脑裂)。

2)确保法定票数(quorum)
quorum可以认为是Pacemkaer自带的仲裁机制,集群的所有节点中的多数选出一个协调者,集群的所有指令都由这个协调者发出,可以完美的杜绝脑裂问题。为了使这套机制有效运转,集群中至少有3个节点,并且把no-quorum-policy设置成stop,这也是默认值。(很多教程为了方便演示,都把no-quorum-policy设置成ignore,生产环境如果也这么搞,又没有其它仲裁机制,是很危险的!)
但是,如果只有2个节点该怎么办?
一是拉一个机子借用一下凑足3个节点,再设置location限制,不让资源分配到那个节点上。
二是把多个不满足quorum小集群拉到一起,组成一个大的集群,同样适用location限制控制资源的分配的位置。

但是如果你有很多双节点集群,找不到那么多用于凑数的节点,又不想把这些双节点集群拉到一起凑成一个大的集群(比如觉得不方便管理)。那么可以考虑第三种方法。
第三种方法是配置一个抢占资源,以及服务和这个抢占资源的colocation约束,谁抢到抢占资源谁提供服务。这个抢占资源可以是某个锁服务,比如基于zookeeper包装一个,或者干脆自己从头做一个,就像下面这个例子。http://my.oschina.net/hanhanztj/blog/515065(这个例子是基于http协议的短连接,更细致的做法是使用长连接心跳检测,这样服务端可以及时检出连接断开而释放锁)但是,一定要同时确保这个抢占资源的高可用,可以把提供抢占资源的服务做成lingyig高可用的,也可以简单点,部署3个服务,双节点上个部署一个,第三个部署在另外一个专门的仲裁节点上,至少获取3个锁中的2个才视为取得了锁。这个仲裁节点可以为很多集群提供仲裁服务(因为一个机器只能部署一个Pacemaker实例,否则可以用部署了N个Pacemaker实例的仲裁节点做同样的事情。)。但是,如非迫不得已,尽量还是采用前面的方法,即满足Pacemaker法定票数,这种方法更简单,可靠

问3. 微服务架构基本理念和原则,为什么会在团队中使用微服务架构,实行微服务架构过程中碰到的问题及其解决方案。

关键思路

对于微服务架构而言,服务建模、服务拆分和服务集成是基本的设计理念,而 RPC、RESTful、API 网关、分布式配置中心等基础组件以及服务可用性和数据最终一致性等关键要素也是重点内容。至于引入微服务架构的原因和碰到的困难,团队业务发展特点和组织架构、微服务粒度和边界等都是回答该问题的切入点。为了实现微服务架构,我们可以引入 Spring Cloud 等相对完善的技术实现体系,也可以使用 Dubbo 作为我们的基础微服务架构。

参考答案:

一体化架构的问题

或者说是微服务架构所解决的问题。

1.难以扩展

一体化架构应用只能通过在负载均衡器后面放置整个应用程序的多个实例来进行水平扩展。如果应用中的特定服务需要扩展,则没有简单的选项。我们需要完整地扩展应用程序,这显然会造成不必要的资源浪费。

相比之下,基于微服务的应用程序允许我们根据需要独立扩展单个服务。在上图中,如果需要缩放服务B,则可以有10个实例,同时保持其他实例,并可以根据需要随时更改。

2.交付时间长

一体化架构在单个应用的任何部分/层中进行的任何更改都需要构建和部署整个应用程序。个人开发人员还需要下载整个应用程序代码来修复和测试,而不仅仅是受影响的模块,这就影响到了持续部署的效率。

而在微服务架构中,如果仅在一百个微服务中的一个中需要改变,则仅构建和部署改变的微服务,没有必要部署一切。我们甚至可以在短时间内多次部署。

3.应用复杂性

过去,随着应用规模的增长(功能、功能等),团队也会相应扩张,应用很快就就会变得复杂和交织在一起。随着不同的团队不断修改代码,维护模块化结构慢慢变得越来越困难,并慢慢导致像意大利面一样交织的代码。这不仅会影响代码质量,还会影响整个组织。

在基于微服务的应用中,每个团队都在单独的微服务上工作,代码会有序很多。

4.没有明确的所有权

在一体化应用中,看起来独立的团队实际上并不是独立的。它们同时在相同的代码库上工作,严重依赖于彼此。

在基于微服务的应用中,独立团队处理单独的微服务。一个团队将拥有一个完整的微服务。工作的明确所有权明确控制服务的一切,包括开发、部署和监控。

5.故障级联

如果没有正确设计,一体化交媾应用的一部分失败可能会级联并导致整个系统崩溃。

在基于微服务的架构的情况下,我们可以使用断路器来避免这种故障。

6.Dev和Ops之间的墙

开发团队通常会进行开发、测试,一旦部署,就会将维护和支持的所有权交给运维团队,应用此时与开发团队无关了,而运维团队需要努力在生产环境中支持一体化架构应用。

在基于微服务的应用中,团队的组织理解为“构建它、运行它”,开发团队继续在生产中拥有该应用。

7.陷入某种技术/语言

使用一体化架构,意味着被某种已实现的技术/语言锁定。如果需要更改技术/语言,则必须重写整个应用程序。

使用微服务,每个服务可以根据需求和业务以不同的技术或语言实现。任何改变服务技术/语言的决定都只需要重写该特定服务,因为所有微服务都是相互独立的。

8.支持微服务的正确工具/技术的可用性

几年前,我们还没有适当的工具和技术来支持微服务。但自从Docker容器和云基础设施(特别是PaaS)向大众提供服务以来,微服务正在大规模采用,因为它们提供了我们所需的“自由”,而无需进行传统的配置程序。

小结

简单来说,使用微服务架构会获得以下好处:

  • 独立开发部署服务
  • 速度和敏捷性
  • 更高的代码质量
  • 获得围绕业务功能创建/组织的代码
  • 提高生产力
  • 更容易扩展
  • 自由(在某种程度上)选择实施技术/语言

问3. RPC 的概念及其包含的核心组件和主流实现技术,如何实现一个自定义的 RPC 框架。

关键思路

该面试题相对容易回答,RPC 是分布式系统的基础,从思路上我们应该理解它是由网络通信、序列化、传输协议、服务调用等组件所构成。同时,对业界主流 RPC 的实现技术也要有足够的了解,如 Alibaba Dubbo、Google gRPC、Facebook Thrift 等。

RPC的由来

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。

  • 单一应用架构
  • 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
  • 此时,用于简化增删改查工作量的 数据访问框架(ORM) 是关键。
  • 垂直应用架构
  • 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。
  • 此时,用于加速前端页面开发的 Web框架(MVC) 是关键。
  • 分布式服务架构
  • 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
  • 此时,用于提高业务复用及整合的 分布式服务框架(RPC),提供统一的服务是关键。

例如:各个团队的服务提供方就不要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。

所以,统一RPC框架来解决提供统一的服务。

以下我将分别从如下四个方面详解RPC。

RPC框架的实现原理,及RPC架构组件详解

RPC的实现原理

RPC框架的实现原理,及RPC架构组件详解

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

比如说,A服务器想调用B服务器上的一个方法:

Employee getEmployeeByName(String fullName)

整个调用过程,主要经历如下几个步骤:

1、建立通信

首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。

主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。

2、服务寻址

要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。

通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。

可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用redis或者zookeeper来注册服务等等。

RPC框架的实现原理,及RPC架构组件详解

  1. 从服务提供者的角度看:当提供者服务启动时,需要自动向注册中心注册服务;
  2. 当提供者服务停止时,需要向注册中心注销服务;
  3. 提供者需要定时向注册中心发送心跳,一段时间未收到来自提供者的心跳后,认为提供者已经停止服务,从注册中心上摘取掉对应的服务。
  4. 从调用者的角度看:调用者启动时订阅注册中心的消息并从注册中心获取提供者的地址;
  5. 当有提供者上线或者下线时,注册中心会告知到调用者;
  6. 调用者下线时,取消订阅。

3、网络传输

3.1、序列化

当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。

3.2、反序列化

当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用,
通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。

4、服务调用

B机器进行本地调用(通过代理Proxy)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。

通常,经过以上四个步骤之后,一次完整的RPC调用算是完成了。

PRC架构组件

一个基本的RPC架构里面应该至少包含以下4个组件:

1、客户端(Client):服务调用方(服务消费者)

2、客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端

3、服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理

4、服务端(Server):服务的真正提供者

RPC调用过程

RPC框架的实现原理,及RPC架构组件详解

1、服务消费者(client客户端)通过本地调用的方式调用服务

2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体

3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端

4、服务端存根(server stub)收到消息后进行解码(反序列化操作)

5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理

6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub)

7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方

8、客户端存根(client stub)接收到消息,并进行解码(反序列化)

9、服务消费方得到最终结果

问4 说说springcloud rpc 的执行流程和原理? 如何对springcloud 微服务进行高并发的性能优化?

请参考 《springcloud nginx 高并发核心编程》一书

分布式事务

问1:什么是分布式事务?

指事务的每个操作步骤都位于不同的节点上,需要保证事务的 AICD 特性。

  1. 产生原因

数据库分库分表;

SOA 架构,比如一个电商网站将订单业务和库存业务分离出来放到不同的节点上。

  1. 应用场景

下单:减少库存同时更新订单状态。库存和订单不在不同一个数据库,因此涉及分布式事务。

支付:买家账户扣款同时卖家账户入账。买家和卖家账户信息不在同一个数据库,因此涉及分布式事务。

  1. 解决方案

3.1 两阶段提交协议

两阶段提交协议可以很好得解决分布式事务问题,它可以使用 XA 来实现,XA 它包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口,而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。

3.2 消息中间件

消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转如余额宝操作,支付宝系统执行减少余额操作之后向消息系统发一个消息,余额宝系统订阅这条消息然后进行增加账户金额操作。

3.2.1 消息处理模型

点对点

img

.

发布/订阅

img

.

3.2.2 消息的可靠性

消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统。

消息的接收端的可靠性:接收端仅且能够从消息中间件成功消费一次消息。

发送端的可靠性

在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。

接收端的可靠性

保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。

保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。

CAP定理

CAP定理

在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:

选项描述
Consistency(一致性)指数据在多个副本之间能够保持一致的特性(严格的一致性)
Availability(可用性)指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)
Partition tolerance(分区容错性)分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障

Spring Cloud在CAP法则上主要满足的是A和P法则,Dubbo和Zookeeper在CAP法则主要满足的是C和P法则

CAP仅适用于原子读写的NOSQL场景中,并不适合数据库系统。现在的分布式系统具有更多特性比如扩展性、可用性等等,在进行系统设计和开发时,我们不应该仅仅局限在CAP问题上。

注意:不是所谓的3选2(不要被网上大多数文章误导了)

现实生活中,大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在CAP理论诞生12年之后,CAP之父也在2012年重写了之前的论文。

当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能2选1。也就是说当网络分区之后P是前提,决定了P之后才有C和A的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。

CAP定理的证明

关于CAP这三个特性我们就介绍完了,接下来我们试着证明一下为什么CAP不能同时满足

img

为了简化证明的过程,我们假设整个集群里只有两个N1和N2两个节点,如下图:

N1和N2当中各自有一个应用程序AB和数据库,当系统满足一致性的时候,我们认为N1和N2数据库中的数据保持一致。在满足可用性的时候,我们认为无论用户访问N1还是N2,都可以获得正确的结果,在满足分区容错性的时候,我们认为无论N1还是N2宕机或者是两者的通信中断,都不影响系统的运行。

我们假设一种极端情况,假设某个时刻N1和N2之间的网络通信突然中断了。如果系统满足分区容错性,那么显然可以支持这种异常。问题是在此前提下,一致性和可用性是否可以做到不受影响呢?

我们做个假象实验,如下图,突然某一时刻N1和N2之间的关联断开:

img

有用户向N1发送了请求更改了数据,将数据库从V0更新成了V1。由于网络断开,所以N2数据库依然是V0,如果这个时候有一个请求发给了N2,但是N2并没有办法可以直接给出最新的结果V1,这个时候该怎么办呢?

这个时候无法两种方法,一种是将错就错,将错误的V0数据返回给用户。第二种是阻塞等待,等待网络通信恢复,N2中的数据更新之后再返回给用户。显然前者牺牲了一致性,后者牺牲了可用性。

这个例子虽然简单,但是说明的内容却很重要。在分布式系统当中,CAP三个特性我们是无法同时满足的,必然要舍弃一个。三者舍弃一个,显然排列组合一共有三种可能。

BASE理论

BASE理论由eBay架构师Dan Pritchett提出,在2008年上被分表为论文,并且eBay给出了他们在实践中总结的基于BASE理论的一套新的分布式事务解决方案。

BASEBasically Available(基本可用)Soft-state(软状态)Eventually Consistent(最终一致性) 三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,它大大降低了我们对系统的要求。

BASE理论的核心思想

即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。

针对数据库领域,BASE思想的主要实现是对业务数据进行拆分,让不同的数据分布在不同的机器上,以提升系统的可用性,当前主要有以下两种做法:

  • 按功能划分数据库
  • 分片(如开源的Mycat、Amoeba等)。

由于拆分后会涉及分布式事务问题,所以eBay在该BASE论文中提到了如何用最终一致性的思路来实现高性能的分布式事务。

BASE理论三要素

BASE理论三要素

1. 基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。

比如:

  • 响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
  • 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
2. 软状态

软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

3. 最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

负载均衡的算法与实现

1.轮询(Round Robin)

轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。

img

.

该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载。下图中,服务器 2 的性能比服务器 1 差,那么服务器 2 可能无法承担多大的负载。

img

.

1. 加权轮询(Weighted Round Robbin)

加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。

img

.

1.3 最少连接(least Connections)

由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数多大,而另一台服务器的连接多小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,它们的连接都还没有断开,继续运行时,服务器 2 会承担多大的负载。

img

.

最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么请求 6 就会被发送到服务器 1 上。

img

.

1.4 加权最小连接(Weighted Least Connection)

在最小连接的基础上,根据服务器的性能为每台服务器分配权重,然后根据权重计算出每台服务器能处理的连接数。

img

.

1.5 随机算法(Random)

把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。

img

.

  1. 实现

2.1 DNS 解析

使用 DNS 作为负载均衡器,会根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式最为第一级负载均衡手段,然后在内部在第二级负载均衡。

img

.

2.2 修改 MAC 地址

使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。

img

.

2.3 修改 IP 地址

在网络层修改请求的目的 IP 地址。

img

.

2.4 HTTP 重定向

HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后再次发送请求。

img

.

2.5 反向代理

正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。

反向代理:发生在服务器端,用户不知道发生了代理。

img

.

分布式锁

Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock,当你的应用是单机或者说单进程应用时,可以使用 synchronized 或 Lock 来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。

  1. 使用场景

例如一个应用有手机 APP 端和 Web 端,如果在两个客户端同时进行一项操作时,那么就会导致这项操作重复进行。

  1. 实现方式

数据库分布式锁

基于 MySQL 锁表

该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。这种方式存在以下几个问题:

锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。

只能是非阻塞锁,插入失败直接就报错了,无法重试。

不可重入,同一线程在没有释放锁之前无法再获得锁。

采用乐观锁增加版本号

根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。

Redis 分布式锁

基于 SETNX、EXPIRE

使用 SETNX(set if not exist)命令插入一个键值对时,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。因此客户端在尝试获得锁时,先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。

EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。

RedLock 算法

ReadLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。

尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。

计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。

如果锁获取失败,会到每个实例上释放锁。

Zookeeper 分布式锁

Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。

抽象模型

Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。

img

.

节点类型

永久节点:不会因为会话结束或者超时而消失;

临时节点:如果会话结束或者超时就会消失;

有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。

监听器

为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。

分布式锁实现

创建一个锁目录 /lock。

在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推。

客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;

执行业务代码,完成后,删除对应的子节点。

会话超时

如果一个已经获得锁的会话超时了,因为创建的是临时节点,因此该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。

羊群效应

在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知,而我们只希望它的下一个子节点收到通知。

分布式 Session

如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在 A、B 两台服务器,用户在第一次访问网站时,Nginx 通过其负载均衡机制将用户请求转发到 A 服务器,这时 A 服务器就会给用户创建一个 Session。当用户第二次发送请求时,Nginx 将其负载均衡到 B 服务器,而这时候 B 服务器并不存在 Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。

1 粘性 Session

原理

粘性 Session 是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 Session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 Session 机制。

优点

简单,不需要对 Session 做任何处理。

缺点

缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 Session 信息都将失效。

适用场景

发生故障对客户产生的影响较小;

服务器发生故障是低概率事件。

2 服务器 Session 复制

原理

任何一个服务器上的 Session 发生改变,该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 Session,以此来保证 Session 同步。

优点

可容错,各个服务器间 Session 能够实时响应。

缺点

会对网络负荷造成一定压力,如果 Session 量大的话可能会造成网络堵塞,拖慢服务器性能。

实现方式

设置 Tomcat 的 server.xml 开启 tomcat 集群功能。

在应用里增加信息:通知应用当前处于集群环境中,支持分布式,即在 web.xml 中添加 选项。

3 Session 共享机制

使用分布式缓存方案比如 Memcached、Redis,但是要求 Memcached 或 Redis 必须是集群。

使用 Session 共享也分两种机制,两种情况如下:

3.1 粘性 Session 共享机制

和粘性 Session 一样,一个用户的 Session 会绑定到一个 Tomcat 上。Memcached 只是起到备份作用。

img

.

3.2 非粘性 Session 共享机制

原理

Tomcat 本身不存储 Session,而是存入 Memcached 中。Memcached 集群构建主从复制架构。

img

.

优点

可容错,Session 实时响应。

实现方式

用开源的 msm 插件解决 Tomcat 之间的 Session 共享:Memcached_Session_Manager(MSM)

4 Session 持久化到数据库

原理

拿出一个数据库,专门用来存储 Session 信息。保证 Session 的持久化。

优点

服务器出现问题,Session 不会丢失

缺点

如果网站的访问量很大,把 Session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

5 Terracotta 实现 Session 复制

原理

Terracotta 的基本原理是对于集群间共享的数据,当在一个节点发生变化的时候,Terracotta 只把变化的部分发送给 Terracotta 服务器,然后由服务器把它转发给真正需要这个数据的节点。它是服务器 Session 复制的优化。

img

.

优点

这样对网络的压力就非常小,各个节点也不必浪费 CPU 时间和内存进行大量的序列化操作。把这种集群间数据共享的机制应用在 Session 同步上,既避免了对数据库的依赖,又能达到负载均衡和灾难恢复的效果。

分库与分表

分库与分表带来的分布式困境与应对之策

img

.

  1. 事务问题

使用分布式事务。

  1. 查询问题

使用汇总表。

  1. ID 唯一性

使用全局唯一 ID:GUID;

为每个分片指定一个 ID 范围

;