一人血书,想让why哥讲一下这道面试题。

2020-11-02 14:13:27 浏览数 (1)

这是why哥的第 71 篇原创文章

一道面试题

兄弟们,怎么说?

我觉得如果你工作了两年左右的时间,或者是突击准备了面试,这题回答个八成上来,应该是手到擒来的事情。这题中规中矩,考点清晰,可以说的东西不是很多。

但是这都上血书了,那不得分析一波?

先把这个面试题拿出来一下:

1000 多个并发线程,10 台机器,每台机器 4 核,设计线程池大小。

这题给的信息非常的简陋,但是简陋的好处就是想象空间足够大。

第一眼看到这题的时候,我直观的感受到了两个考点:

  1. 线程池设计。
  2. 负载均衡策略。

我就开门见山的给你说了,这两个考点,刚好都在我之前的文章的射程范围之内:

《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答》

《吐血输出:2万字长文带你细细盘点五种负载均衡策略》

下面我会针对我感受到的这两个考点去进行分析。

线程池设计

我们先想简单一点:1000 个并发线程交给 10 台机器去处理,那么 1 台机器就是承担 100 个并发请求。

100 个并发请求而已,确实不多。

而且他也没有说是每 1 秒都有 1000 个并发线程过来,还是偶尔会有一次 1000 个并发线程过来。

先从线程池设计的角度去回答这个题。

要回答好这个题目,你必须有两个最基本的知识贮备:

  1. 自定义线程池的 7 个参数。
  2. JDK 线程池的执行流程。

先说第一个,自定义线程池的 7 个参数。

java.util.concurrent.ThreadPoolExecutor#ThreadPoolExecutor

害,这 7 个参数我真的都不想说了,你去翻翻历史文章,我都写过多少次了。你要是再说不出个头头是道的,你都对不起我写的这些文章。

而且这个类上的 javadoc 已经写的非常的明白了。这个 javadoc 是 Doug Lea 老爷子亲自写的,你都不拜读拜读?

为了防止你偷懒,我把老爷子写的粘下来,我们一句句的看。

关于这几个参数,我通过这篇文章再说最后一次。

如果以后的文章我要是再讲这几个参数,我就不叫 why 哥,以后你们就叫我小王吧。

写着写着,怎么还有一种生气的感觉呢。似乎突然明白了当年在讲台上越讲越生气的数学老师说的:这题我都讲了多少遍了!还有人错?

好了,不生气了,说参数:

  1. corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set (核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。)
  2. maximumPoolSize:the maximum number of threads to allow in the pool。(最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)
  3. keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。(存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)
  4. unit:the time unit for the {@code keepAliveTime} argument (keepAliveTime 的时间单位。)
  5. workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。(存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了,好吗?不要自己给自己挖坑。)
  6. threadFactory:the factory to use when the executor creates a new thread。(线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。)
  7. handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)

第一个知识贮备就讲完了,你先别开始背,这玩意你背下来有啥用,你得结合着执行流程去理解。

接下来我们看第二个:JDK 线程池的执行流程。

一图胜千言:

关于 JDK 线程池的 7 个参数和执行流程。

虽然我很久没有参加面试了,但是我觉得这题属于必考题吧。

所以如果你真的还不会,麻烦你写个 Demo ,换几个参数调试一下。把它给掌握了。

而且还得多注意由这些知识点引申出来的面试题。

比如从图片也可以看出来,JDK 线程池中如果核心线程数已经满了的话,那么后面再来的请求都是放到阻塞队列里面去,阻塞队列再满了,才会启用最大线程数。

但是你得知道,假如我们是 web 服务,请求是通过 Tomcat 进来的话,那么 Tomcat 线程池的执行流程可不是这样的。

Tomcat 里面的线程池的运行过程是:如果核心线程数用完了,接着用最大线程数,最后才提交任务到队列里面去的。这样是为了保证响应时间优先。

所以,Tomcat 的执行流程是这样的:

其技术细节就是自己重写了队列的 offer 方法。在这篇文章里面说的很清楚了,大家可以看看:

《每天都在用,但你知道 Tomcat 的线程池有多努力吗?》

好的,前面两个知识点铺垫完成了。

这个题,从线程池设计的角度,我会这样去回答:

前面我们说了,10 个机器,1000 个请求并发,平均每个服务承担 100 个请求。服务器是 4 核的配置。

那么如果是 CPU 密集型的任务,我们应该尽量的减少上下文切换,所以核心线程数可以设置为 5,队列的长度可以设置为 100,最大线程数保持和核心线程数一致。

如果是 IO 密集型的任务,我们可以适当的多分配一点核心线程数,更好的利用 CPU,所以核心线程数可以设置为 8,队列长度还是 100,最大线程池设置为 10。

当然,上面都是理论上的值。

我们也可以从核心线程数等于 5 开始进行系统压测,通过压测结果的对比,从而确定最合适的设置。

同时,我觉得线程池的参数应该是随着系统流量的变化而变化的。

所以,对于核心服务中的线程池,我们应该是通过线程池监控,做到提前预警。同时可以通过手段对线程池响应参数,比如核心线程数、队列长度进行动态修改。

上面的回答总结起来就是四点:

  1. CPU密集型的情况。
  2. IO密集型的情况。
  3. 通过压测得到合理的参数配置。
  4. 线程池动态调整。

前两个是教科书上的回答,记下来就行,面试官想听到这两个答案。

后两个是更具有实际意义的回答,让面试官眼前一亮。

基于这道面试题有限的信息,设计出来的线程池队列长度其实只要大于 100 就可以。

甚至还可以设置的极限一点,比如核心线程数和最大线程数都是 4,队列长度为 96,刚好可以承担这 100 个请求,多一个都不行了。

所以这题我觉得从这个角度来说,并不是要让你给出一个完美的解决方案,而是考察你对于线程池参数的理解和技术的运用。

面试的时候我觉得这个题答到这里就差不多了。

接下来,我们再发散一下。

比如面试官问:如果我们的系统里面没有运用线程池,那么会是怎么样的呢?

首先假设我们开发的系统是一个运行在 Tomcat 容器里面的,对外提供 http 接口的 web 服务。

系统中没有运用线程池相关技术。那么我们可以直接抗住这 100 个并发请求吗?

答案是可以的。

Tomcat 里面有一个线程池。其 maxThreads 默认值是 200(假定 BIO 模式):

maxThreads 用完了之后,进队列。队列长度(acceptCount)默认是 100:

在 BIO 的模式下,Tomcat 的默认配置,最多可以接受到 300 (200 100)个请求。再多就是连接拒绝,connection refused。

所以,你要说处理这 100 个并发请求,那不是绰绰有余吗?

但是,如果是每秒 100 个并发请求,源源不断的过来,那就肯定是吃不消了。

这里就涉及到两个层面的修改:

  1. Tomcat 参数配置的调优。
  2. 系统代码的优化。

针对 Tomcat 参数配置的调优,我们可以适当调大其 maxThreads 等参数的值。

针对系统代码的优化,我们就可以引入线程池技术,或者引入消息队列。总之其目的是增加系统吞吐量。

同理,假设我们是一个 Dubbo 服务,对外提供的是 RPC 接口。

默认情况下,服务端使用的是 fixed 线程池,核心线程池数和最大线程数都是 200。队列长度默认为 0:

那么处理这个 100 个并发请求也是绰绰有余的。

同样,如果是每秒 100 个并发请求源源不断的过来,那么很快就会抛出线程池满的异常:

解决套路其实是和 Tomcat 的情况差不多的,调参数,改系统,加异步。

这个情况下的并发,大多数系统还是抗住的。

面试官还可以接着追问:如果这时由于搞促销活动,系统流量翻了好倍,那你说这种情况下最先出现性能瓶颈的地方是什么?

最先出问题的地方肯定是数据库嘛,对吧。

那么怎么办?

分散压力。分库分表、读写分离这些东西往上套就完事了。

然后在系统入口的地方削峰填谷,引入缓存,如果可以,把绝大部分流量拦截在入口处。

对于拦不住的大批流量,关键服务节点还需要支持服务熔断、服务降级。

实在不行,加钱,堆机器。没有问题是不能通过堆机器解决的,如果有,那么就是你堆的机器不够多。

面试反正也就是这样的套路。看似一个发散性的题目,其实都是有套路可寻的。

好了,第一个角度我觉得我能想到的就是这么多了。

首先正面回答了面试官线程池设计的问题。

然后分情况聊了一下如果我们项目中没有用线程池,能不能直接抗住这 1000 的并发。

最后简单讲了一下突发流量的情况。

接下来,我们聊聊负载均衡。

负载均衡策略

我觉得这个考点虽然稍微隐藏了一下,但还是很容易就挖掘到的。

毕竟题目中已经说了:10 台机器。

而且我们也假设了平均 1 台处理 100 个情况。

这个假设的背后其实就是一个负载均衡策略:轮询负载均衡。

如果负载均衡策略不是轮询的话,那么我们前面的线程池队列长度设计也是有可能不成立的。

还是前面的场景,如果我们是运行在 Tomcat 容器中,假设前面是 nginx,那么 nginx 的负载均衡策略有如下几种:

  1. (加权)轮询负载均衡
  2. 随机负载均衡
  3. 最少连接数负载均衡
  4. 最小响应时间负载均衡
  5. ip_hash负载均衡
  6. url_hash负载均衡

如果是 RPC 服务,以 Dubbo 为例,有下面几种负载均衡策略:

  1. (加权)轮询负载均衡
  2. 随机负载均衡
  3. 最少活跃数负载均衡
  4. 最小响应时间负载均衡
  5. 一致性哈希负载均衡

哦,对了。记得之前还有一个小伙伴问我,在 Dubbo zookeeper 的场景下,负载均衡是 Dubbo 做的还是 zk 做的?

肯定是 Dubbo 啊,朋友。源码都写在 Dubbo 里面的,zk 只是一个注册中心,关心的是自己管理着几个服务,和这几个服务的上下线。

你要用的时候,我把所有能用的都给你,至于你到底要用那个服务,也就是所谓的负载均衡策略,这不是 zk 关心的事情。

不扯远了,说回来。

假设我们用的是随机负载均衡,我们就不能保证每台机器各自承担 100 个请求了。

这时候我们前面给出的线程池设置就是不合理的。

常见的负载均衡策略对应的优缺点、适用场景可以看这个表格:

关于负载均衡策略,我的《吐血输出:2万字长文带你细细盘点五种负载均衡策略》这篇文章,写了 2 万多字,算是写的很清楚了,这里就不赘述了。

说起负载均衡,我还想起了之前阿里举办的一个程序设计大赛。赛题是《自适应负载均衡的设计实现》。

赛题的背景是这样的:

负载均衡是大规模计算机系统中的一个基础问题。灵活的负载均衡算法可以将请求合理地分配到负载较少的服务器上。

理想状态下,一个负载均衡算法应该能够最小化服务响应时间(RTT),使系统吞吐量最高,保持高性能服务能力。

自适应负载均衡是指无论处在空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,更好的进行流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。

具体题目和获奖团队答辩可以看这里:

代码语言:javascript复制
题目:https://tianchi.aliyun.com/competition/entrance/231714/information?spm=a2c22.12849246.1359729.1.6b0d372cO8oYGK

答辩:https://tianchi.aliyun.com/course/video?spm=5176.12586971.1001.1.32de8188ivjLZj&liveId=41090

推荐大家有兴趣的去看一下,还是很有意思的,可以学到很多的东西。

扩展阅读

这一小节,我截取自《分布式系统架构》这本书里面,我觉得这个示例写的还不错,分享给大家:

这是一个购物商场的例子:

系统部署在一台 4C/8G 的应用服务器上、数据在一台 8C/16G 的数据库上,都是虚拟机。

假设系统总用户量是 20 万,日均活跃用户根据不同系统场景稍有区别,此处取 20%,就是 4 万。

按照系统划分二八法则,系统每天高峰算 4 小时,高峰期活跃用户占比 80%,高峰 4 小时内有 3.2 万活跃用户。

每个用户对系统发送请求,如每个用户发送 30 次,高峰期间 3.2 万用户发起的请求是 96 万次,QPS=960 000/(4x60x60)≈67 次请求,每秒处理 67 次请求,处理流程如下图有所示:

一次应用操作数据库增删改查(CRUD)次数平均是操作应用的三倍,具体频率根据系统的操作算平均值即可。一台应用、数据库能处理多少请求呢?

具体分析如下。

  1. 首先应用、数据库都分别部署在服务器,所以和服务器的性能有直接关系,如 CPU、内存、磁盘存储等。
  2. 应用需要部署在容器里面,如 Tomcat、Jetty、JBoss 等,所以和容器有关系,容器的系统参数、配置能增加或减少处理请求的数目。
  3. Tomcat 部署应用。Tomcat 里面需要分配内存,服务器共 8GB 内存,服务器主要用来部署应用,无其他用途,所以设计 Tomcat 的可用内存为8/2=4GB (物理内存的1/2),同时设置一个线程需要 128KB 的内存。由于应用服务器默认的最大线程数是 1000(可以参考系统配置文件),考虑到系统自身处理能力,调整 Tomcat 的默认线程数至 600,达到系统的最大处理线程能力。到此一台应用最大可以处理 1000 次请求,当超过 1000 次请求时,暂存到队列中,等待线程完成后进行处理。
  4. 数据库用 MySQL。MySQL 中有连接数这个概念,默认是 100 个,1 个请求连接一次数据库就占用 1 个连接,如果 100 个请求同时连接数据库,数据库的连接数将被占满,后续的连接需要等待,等待之前的连接释放掉。根据数据库的配置及性能,可适当调整默认的连接数,本次调整到 500,即可以处理 500 次请求。

显然当前的用户数以及请求量达不到高并发的条件,如果活跃用户从 3.2 万扩大到 32 万,每秒处理 670 次请求,已经超过默认最大的 600 ,此时会出现高并发的情况,高并发分为高并发读操作和高并发写操作。

好了,书上分享的案例就是这样的。

对了,如果你最近有在当当上面买书打算的话,可以用我优惠码:ZKGAPA

用 160 块就可以买到原价 400 元的书,4 折优惠!

那么这次的文章就到这里啦。关于这道面试题我个人的看法就是这样的。当然,不知道合不合面试官的口味。

你要是不喜欢下面的荒腔走板环节的话,也请记得拉到文章的最后。留言、点赞、在看、转发、赞赏,随便来一个就行。你要是都安排上,我也不介意。

荒腔走板

上周五晚上去看了《金刚川》。

据说拍摄周期只有 2 个月,但是电影整体来说还是挺好看的。前半段比较的平缓,但是后半段高潮迭起。对我而言,是有好几个泪点的。

第一个泪点是“喀秋莎”火箭炮出来的时候,像烟花一样,战士们说:这就是我们的喀秋莎吧?

这一波“喀秋莎”就是对我方战士的一针强心剂。这比多少不切实际的鼓励的话,来的实际的多。

第二个泪点是张译扮演的张飞,一个人对抗美军侦察机,在高炮上高呼:“千古流芳莽撞人”的时候。

用老张的话说:不要以为这是神剧套路,历史现场比这还要惨烈。

张译的演技,没的说。一个人撑起了这个片段的一半。影帝预定一个。

第三个泪点就是燃烧弹落到江面,然后随之响起的《我的祖国》 BGM 了:

一条大河波浪宽

风吹稻花香两岸

我家就在岸上住

听惯了艄公的号子

看惯了船上的白帆

这是美丽的祖国

是我生长的地方

配合着整个修桥的故事,联想到同是抗美援朝时期的上甘岭战役,简直就是泪点暴击。

实话实话,《金刚川》这个电影不是特别的完美。但是我还是力荐大家去电影院支持这部电影。

因为这部电影,拍在抗美援朝 70 周年纪念的这个特殊节点。能让有幸生活在和平时代的我们知道,现在的和平长安,国富民强不是从天上掉下来的,是 70 年前,那一群只有十七八岁的“最可爱的人”用生命打下来的。

之前看《人民日报》里面的一个短视频,一位老兵说的话特别的感动,他说:

什么是祖国?当我们跨过鸭绿江,看到战火的时候,我身后就是祖国。

和平来之不易,吾辈自当珍惜。

向最可爱的人致敬。

最后说一句(求关注)

好了,看到了这里安排个“一键三连”(转发、在看、点赞)吧,周更很累的,不要白嫖我,需要一点正反馈。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

0 人点赞