一线大厂问题详解(二)

一、从 B 站崩溃的故障排查和恢复过程中学到什么?

2021 年 7 月 13 日晚上 22:52 ,B 站崩了。整个事件的罪魁祸首竟然只是,这么短短 的几行代码;

回到 B 崩溃那天 ,仅不到半个小时 ,这个消息就冲上了微博的热搜头条。

B 站出来的用户甚至带崩了 A 站、豆瓣、知乎等多个网站。

网友调侃说,  B 站服务器一蹦 ,紧张加班的除了 B 站的程序员 ,同时让 A 站乎、豆瓣、微博的程序员默默打开了电脑。

B 站崩溃的背后 ,背后的根本原因到底是什么呢?

1、了解 B 栈的公网架构

时隔一年以后,  B 站对外公布了导致这个问题的底层原因 ,实际上这个问题并不复杂,但是为了让大家彻底搞懂这个问题 ,我们先来了解一下 B 站的公网架构  (如图)  。     

    1、CDN 是内容分发网络的一个简称 ,它提供了地域的就近访问功能 ,为用户获取服器信息提供了加速的功能。

    2、lvs 是一个四层负载均衡器,也就是基于 ip+端口的负载,为 OpenResty 提供高可集群。

    3、OpenResty ,是一个基于 Nginx+Lua 的高性能 Web 平台 ,简单来说,就是我们可以直接使用 Lua 脚本来扩展 Nginx 的能力 ,从而构建动态的 Web 应用。 

    4、最后 ,使用了多机房部署实现异地多活的架构保证高可用性。

当用发起请求后 ,经过 CDN 分发到业务主机房 ,然后通过 LVS 四层负载路由 OpenResty 服务器上,最后再转发到对应的应用服务器实例获取相关数据并返回。

2、故障解决过程分析

针对这样的一种架构,  (如图) 在 B 站崩溃的时候,运维人员采取了一些常规的措施去 定位和解决问题。

  •     1、梳理整个请求链路,查看存在异常的服务器节点,SLB 运维人员发现七层负载服务的 CPU 达到 100%,于是采取了重新加载以及冷重启 SLB 的方式都没有解决这个问题。

    2、接着发现多活机房在 CPU 使用率正常的情况下 ,SLB 存在大量超时请求 ,于是重启多活机房 SLB,恢复了部分业务的使用。

大家发现没有 ,遇到生产事故的时候 ,在不清楚具体问题的情况下,优先重启相关服务节点来达到故障止损是效率最高的手段,因此 B 站的运维人员通过重启 SLB 以及多活节点后。

使得多活机房恢复正常后部分业务可以正常访问。

但是业主机房还没有恢复 ,这个时候 ,就需要慢慢去排查了。

于是运维人员使用了 Linux 提供的 Perf 系统分析工具,定位到主机房 SLB 服务器的 CPU 热点集中在一个 Lua 函数上。

于是 ,也采取了常规的解决思路- 版本回滚。

一般一个问题的出现 ,在宏观层面没有太多变化的情况下,很有可能是因为最近发布了新版本导致的。

从 B 站的问题复盘来看 ,回滚并没有解决问题 ,最后是通过重新构建新的 SLB 集群解 决的

个解决过程耗时 3 小时 ,对于互联网公司来说 ,这是一个非常大的故障。

虽然问题解决了,但是这个问题的根源以及让谁来背这个锅,还没搞清楚,所以需要继续分析原因。

过一段时间的排查发现,CPU 跑满的原因是 OpenResty 里面的一个 lua 函数导致的(如图)  

这个函数的作用是从注册中心同步服务注册地址以及该服务节点的访问权重保存到Nginx 的享内存里面。

然后使用 lua-resty-balancer 模块对服务地址进行动态路由。

其中 ,在进行目标服务器的动态选择的时候 ,用到了加权轮询算法,并使用了下面  (如图)  这个方法计算所有实例权重的最大公约数。

这个方法本身没有问题 ,但是当节点的权重 weight=“0”的时候,_gcd 函数收到的 入参 b 可能是字符串“0”。

Lua 又是弱类型语言 ,允许传入字符串“0”,这个时候 if 条件字符串 0 不等于数字 0 ,会执行_gcd 递归调用 ,其中在执行 a%b 的候 ,用字符串和数字取模,得到一个 NaN 的结果。

于是再次执行的时候 ,就变成了_gcd(NaN,  NaN)的递归调用导致死循环问题。

可能大家会有疑惑 ,为什么回滚代码没有解决这个问题。

官方的声明说了,权重这个功能是 2 个月之前上线的,也就是这个问题的潜在风险存在  2 个月。

在某种发布模式下,应用实例的权重会短暂调整为 0,导致注册中心返回给 SLB 的权重 一个字符串的“0”。

个问题之所以一直没暴露出来 ,是因为这种发布模式的使用频率极低。

一个影响生产环境 3 个小时,造成巨大影响和损失的生成事故,竟然是一个数据类型导 致的当得到这个答案的时候,大家可能会觉得难以接受,这就是千里之堤毁于蚁穴的实写 啊。

在这个事故发生后 ,公司内部必然要做的几件事

    1、出一份详细的事故报告 ,明确事故的责任人和事故的级别

    2、针对事故进行复盘

    3、从技术层面、以及管理层面提出优化和改进的措施 ,避免后续再出现类似问题。 总的来说 ,越是偏向底层的开发 ,对于技术能力的要求以及工作的严谨性就越高。

通过 B 站的这次案例,屏幕前的你们也可以学习一些经验,从容应对未来具有挑战的工作。

二、limit 1000000,10 加载很慢该怎么优化

关于这个问题 ,有多种解决方案 ,大家可以在回答的时候尽可能的考虑全面一点。

1.   如果 id 是连续的 ,可以直接使用这样的方式。

select * from order where id > 1000000 limit 10

这种方式其实就是先对数据做过滤 ,然后再 limit ,可以有效提升查询效率

2.   通过 order by+索引来解决

select * from order order by id limit 1000000,10

注意 id 是索引列 ,通过索引排序后再 limit ,同样减少了计算次数

3.   从业务层面来考虑,限制页数,一般情况下用户去翻 100W 页来找数据,如果让你 们老板去翻 100w 页,估计第二天就把你开除了。我们通常会通过搜索来优化过程

以上就是这个问题的回答思路,面试的时候不一定要完全陷入到面试官的逻辑中,也可以跳出来思考。

三、会员批量过期的方案怎么实现

“有一张 200W 数据量的会员表 ,每个会员会有长短不一的到期时间 ,现在想在快到 期之前发送邮件通知提醒续费”该怎么实现。

题解析

对于这类的场景问题,我建议大家不要急着去回答,而是先冷静下来,想清楚面试官希 望通过这问题去了解哪方面的能力 ,以及这个问题里面有哪些陷阱。

注意一个点 ,一定要理解清楚面试官的问题 ,如果没听懂 ,可以确认一遍。

很显然 这个问题里面有几个关键词:

    1、200W 数据意味着数据量比较大

    2、每个会员都有过期时间 ,需要能够筛选出快过期的会员

很显然 ,如果直接去通过 select 语句做筛选 ,就掉入坑里了 ,因为这里会存在性能问  ,那接下来看一下一些相对比较合理的回答。

题答案

第一种

系统不主动轮询 ,而是等用户登录到系统以后 ,触发一次检查。如果发现会员的过期时间小于设定的阈值 ,就触发一次弹窗和邮件提醒。这种方式规避了轮询问题 ,不会对数据库和后端应用程序造成任何压力。缺点是,如果用户一直不登陆,就一直无法实现会员过期,并且也无法提前去根据运营略发送续期的提醒消息。

第二种

我们可以使用搜索引擎 ,比如 Solr、或者 Elasticsearch。把会员表里面的会员 id 和会员到期时间存储一份到搜索引擎中。搜索引擎的优势在于大数据量的快速检索,并且具有高可扩展性和高可靠性,非常适合大规模数据的处理。

第三

可以使用 Redis 来实现。用户开通会员以后 ,在 Redis 里面存储这个会员id ,以及设置这个 id 的过期时间。 然后可以使用 redis 的过期提醒功能,把配置项 notify-keyspace-events 改为 notify-keyspace-events "Ex",当 Redis 里面的 key 过期以后,会触发一个 key 过期事件 ,我们可以在应用程序中监 听这个事件来处理。

可以直接使用 MQ 提供的延迟队列 ,当用户开通会员以后 ,直接计算这个会 过期时间 ,然后发送一个延迟消息到 MQ 上 ,一旦消息达到过期时间 ,消费者就 可以消费这个消息来触发会员过期的提醒。

四、什么是幂等?如何解决幂等性问题?

幂等,其实它是一个数学上的概念,在计算机编程领域中,幂等是指一个方法被多 次重复执行的时候产生的影响和第一次执行的影响相同。

之所以要考虑到幂等性问题,是因为在网络通信中,存在两种行为可能会导致接口被重 复执行

1.   用户的重复提交或者用户的恶意攻击 ,导致这个请求会被多次重复执行。

2.   在分布式架构中,为了避免网络通信导致的数据丢失,在服务之间进行通信的时候

都会设计超时重试的机制 ,而这种机制有可能导致服务端接口被重复调用

所以在程序设计中 ,对于数据变更类操作的接口 ,需要保证接口的幂等性。

而幂等性的核心思想,其实就是保证这个接口的执行结果只影响一次,后续即便再次调 ,也不能对数据产生影响 ,所以基于这样一个诉求 ,常见的解决方法有很多。

3.   使用数据库的唯一约束实现幂等,比如对于数据插入类的场景 ,比如创建订单 ,因 为订单号肯定是唯一的,所以如果是多次调用就会触发数据库的唯一约束异常,从 而避免一个请求创建多个订单的问题。

4.   使用 redis 里面提供的 setNX 指令 ,比如对于 MQ 消费的场景 ,为了避免 MQ 重 复消费导致数据多次被修改的问题,可以在接受到 MQ 的消息时,把这个消息通过 setNx 写入到 redis 里面 ,一旦这个消息被消费过 ,就不会再次消费。

5.   使用状态机来实现幂等 ,所谓的状态机是指一条数据的完整运行状态的转换流程, 如订单状态 ,因为它的状态只会向前变更,所以多次修改同一条数据的时候,一 旦状态发生变 ,那么对这条数据修改造成的影响只会发生一次。

 ,除了这些方法以外 ,还可以基于 token 机制、去重表等方法来实现 ,但是不管是什么方法 ,无非就是两种:

     要么就是接口只允许调用一次 ,比如唯一约束、基于 redis 的锁机制。

    要么就是对数据的影响只会触发一次 ,比如幂等性、乐观锁。

 五、常的限流算法有哪些? 

限流算法是一种系统保护策略,主要是避免在流量高峰导致系统被压垮,造成系统不可用的问题。

1.     (如图)  计数器限流,一般用在单一维度的访问频率限制上 ,比如短信验证码每隔 60s 只能发送一次 ,或者接口调用次数等它的实现方法很简单 ,每调用一次就加 1 ,处理结束以后减一

2.     (如图)  滑动窗口限流,本质上也是一种计数器,只是通过以时间为维度的可滑动 窗口设计 ,来减了临界值带来的并发超过阈值的问题。

每次进行数据统计的时候 ,只需要统计这个窗口内每个时间刻度的访问量就以了。   Spring Cloud 里面的熔断框架 Hystrix  ,以及 Spring Cloud Alibaba 里面的 Sentinel 采用了滑动窗口来做数据统计。

3.     (如图)漏桶算法 ,它是一种恒定速率的限流算法,不管请求量是多少 ,服务端的处理效是恒定的。基于MQ 来实现的生产者消费者模型,其实算是一种漏桶限流算法。

4.     (如图)  令牌桶算法 ,相对漏桶算法来说 ,它可以处理突发流量的问题。

它的核心思想是,令牌桶以恒定速率去生成令牌保存到令牌桶里面,桶的大小是固定的, 牌桶满了以后就不再生成令牌。

每个客端请求进来的时候 ,必须要从令牌桶获得一个令牌才能访问 ,否则排队等待。 在流量低峰的时候,令牌桶会出现堆积,因此当出现瞬时高峰的时候,有足够多的令牌 可以获取 ,因此令牌桶能够允许瞬时流量的处理

网关层面的流、或者接口调用的限流,都可以使用令牌桶算法,像 Google 的 Guava, Redisson 的流 ,都用到了令牌桶算法。

限流的本质是实现系统保护,最终选择什么样的算法,一方面取决于统计的 精准度 ,另一方面考虑限流维度和场景的需求。

   

猜你喜欢

转载自blog.csdn.net/gnwu1111/article/details/132690986