在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、网易、有赞、希音、百度、网易、滴滴的面试资格,遇到一几个很重要的面试题:
- 1000W并发,需部署多少个节点?
- 如何觉得部署多少个节点,是怎么预估以及部署的?
尼恩提示,部署架构、节点规划 相关的问题,是架构的核心知识,又是线上的重点难题。
所以,这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V102版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取
大家先思考一个问题,这也是在面试过程中经常遇到的问题。
如果你们公司现产品是卖口罩的,平时能够支持 10W 用户访问,
遇到突发的情况,如疫情来了,
预计在 1 个月后用户量会达到 1000W,如果这个任务交给你,你应该怎么做?
对于如何支持 1000 万用户的问题,实际上是一个相当抽象的问题。
对于技术开发者来说,需要量化。
什么是量化?就是需要一个明确的性能指标数据,以便在执行关键业务时进行参考。
例如,在高峰时段,系统的事务响应时间、并发用户数量、每秒查询率(QPS)、成功率等。
量化的基本要求,就是各项指标,必须清晰明了。
只有这样,才能有效地指导整个架构的改进和优化。
因此,如果你面临这样的问题,首先需要找到问题的核心,也就是了解一些可以量化的数据指标。
在估算关键指标如响应时间、并发用户数量、每秒查询率(QPS)、成功率的同时,你也需要关注具体的业务功能需求。
每个业务功能都有其独特的特点。例如:
因此,学会平衡这些指标之间的关系是必要的。
在大多数情况下,最好为这些指标设定一个优先级顺序,并尽可能只关注几个高优先级的指标要求。
SLA:Service-Level Agreement 的缩写,意思是服务等级协议。
服务的 SLA 是服务提供者对服务消费者的正式承诺,是衡量服务能力等级的关键项。
服务 SLA 中定义的项必须是可测量的,有明确的测量方法。
SLA项 | 含义 | 测量方法 | 示例 | 服务级别 | 接口级别 |
---|---|---|---|---|---|
请求成功率 | 测量周期内服务成功应答的请求占总请求数的百分比 | (成功应答请求数/总请求)*100 | >99% | 是 | 否 |
可用性 | 测量周期内,服务可用时间所占百分比,可用性分三个等级。 1.99.999%-99.9999%,这个是可用性最高的服务,一年累计不可用时间为5.256分钟-31.536秒,这类服务不可用会影响到用户使用,比如登录 2.99.99%%-99.999%,一年累计不可用时间为52.56分钟-5.256分钟,出现不可用时会影响用户的操作,间接面向用户的服务 3. 99.9%-99.99%,一年累计不可用时间为8.76小时-52.56分钟,出现服务不可用时不会影响用户的使用。 |
(服务在线时间/统计周期总时间)*100 | Level 1 | 是 | 否 |
数据一致性 | 服务消费者调用服务接口写入数据后马上调用服务接口读取,是否可以读到写入的数据内容,包含三个等级 1.强一致 2.弱一致 3.最终一致 |
调用资源创建接口,调用资源查询接口获取创建的数据 | 最终一致 | 是 | 否 |
吞吐量 | 每秒钟处理的请求数,对于服务集群建议给出总体吞吐量的计算方式,比如集群吞吐量=吞吐量*服务实例数,如果难以给出,则至少要给出典型的集群实例数情况总体吞吐量 | 统计服务每秒处理的请求数量 | 200 | 是 | 可选 |
TP50请求延迟 | 服务运行周期内50%的请求延时地域定义的值 | 使用百分位计算方式 | 100ms | 是 | 可选 |
TP99.9请求延迟 | 服务运行周期内99.9%的请求延迟地域定义的值 | 使用百分位计算方式 | 200ms | 是 | 可选 |
在深入探讨上述问题之前,我想先向大家介绍一下与系统相关的一些关键评估指标:
这些关键概念,尼恩写过专门的文章介绍过, 具体请参见下面的文章:
你们系统qps多少,怎么部署的?假设每天有几千万请求,该如何部署?
让我们回归最初的问题:1000W并发,需部署多少个节点?
假设我们没有历史数据可以参考,我们可以采用二八定律来进行估算。
在大致估算了后端服务器需要承受的最高并发峰值之后,我们需要从整个系统架构的角度进行压力测试,然后合理配置服务器数量和架构。
首先,我们需要了解一台服务器能承受多大的并发量,那么该如何进行分析呢?
由于我们的应用部署在 Tomcat 上,因此我们需要从 Tomcat 的性能入手。
以下是一个描述 Tomcat 工作原理的图表,图表说明如下:
从这个图中我们可以得知,影响 Tomcat 请求数量的因素主要有四个方面。
我想可能大家遇到过类似“Socket/File:Can’t open so many files”的异常,这就是 Linux 系统中文件句柄限制的表示。
在 Linux 操作系统中,每一个 TCP 连接都会占用一个文件描述符(fd),当文件描述符超过 Linux 系统当前的限制时,就会弹出这个错误提示。
我们可以通过以下命令来查看一个进程能够打开的文件数量上限。
ulimit -a 或者 ulimit -n
open files (-n) 1024 是 linux 操作系统对一个进程打开的文件句柄数量的限制(也包含打开的套接字数量)
这里只是对用户级别的限制,其实还有个是对系统的总限制,查看系统总线制:
cat /proc/sys/fs/file-max
file-max 是设定系统所有进程总共可以打开的文件数量。
同时,部分程序可以通过setrlimit调用,设置每个进程的限制。如果收到大量文件句柄使用完毕的错误信息,那么我们应该考虑增加这个数值。
当遇到上述错误时,我们可以通过以下方式进行修改(针对单个进程的文件打开数量限制)
vi /etc/security/limits.conf
root soft nofile 65535
root hard nofile 65535
* soft nofile 65535
* hard nofile 65535
*
代表所有用户、root
表示 root 用户。另外,还需要确保针对进程级别的文件打开数量限制是小于或等于系统的总限制,如果不是,那么我们需要修改系统的总限制。
vi /proc/sys/fs/file-max
TCP 连接对于系统资源最大的开销在于内存。
由于 TCP 连接需要双方进行数据接收和发送,因此需要设置读取缓冲区和写入缓冲区。
在 Linux 系统中,这两个缓冲区的最小大小为 4096 字节,可以通过查看/proc/sys/net/ipv4/tcp_rmem 和/proc/sys/net/ipv4/tcp_wmem 来获取相关信息。
因此,一个 tcp 连接最小占用内存为 4096+4096 = 8k,那么对于一个 8G 内存的机器,如果不考虑其他限制,其最大并发数约为:8 * 1024 * 1024/8 约等于 100 万。
这个数字是理论上的最大值,在实际应用中,受到 Linux 内核对部分资源的限制以及程序业务处理的影响,8GB 内存很难达到 100 万连接。
当然,我们可以通过增加内存来提高并发数。
我们都知道,Tomcat 是一个 Java 程序,运行在 JVM 上,
因此,对 JVM 进行优化也是提高 Tomcat 性能的关键。下面简单介绍一下 JVM 的基本情况,如下图所示。
在 JVM 里,内存被划分为堆、程序计数器、本地方法栈、方法区(元空间)和虚拟机栈。
堆内存是 JVM 内存中最大的一个区域,绝大多数的对象和数组都会被分配在此,它供所有线程共享。堆空间被划分为新生代和老年代,新生代进一步被划分为 Eden 和 Survivor 区,如下图所示。
新生代和老年代的比例为 1:2,也就是说新生代占堆空间的 1/3,而老年代占 2/3。
另外,在新生代中,空间分配比例为 Eden:Survivor0:Survivor1=8:1:1。
举例来说,如果 Eden 区的内存大小是 40M,那么两个 Survivor 区的内存分别占 5M,新生代的总内存就是 50M,进而计算出老年代的内存大小为 100M,也就是说堆空间的总内存大小是 150M。
可以通过 java -XX:PrintFlagsFinal -version 查看默认参数
uintx InitialSurvivorRatio = 8 uintx NewRatio = 2
InitialSurvivorRatio: 新生代 Eden/Survivor 空间的初始比例
NewRatio : Old 区/Young 区的内存比例
堆内存的具体工作机制如下:
GC 标记-清除算法 在执行过程中暂停其他线程??
程序计数器用于记录各个线程执行的字节码地址等信息,在线程发生上下文切换时,依赖它来记录当前执行位置,以便在下次恢复执行时能够从上次执行位置继续执行。
方法区是一个逻辑概念,在 HotSpot 虚拟机的 1.8 版本中,它的具体实现就是元空间。
方法区主要用来存储已经被虚拟机加载的类相关信息,包括类元信息、运行时常量池、字符串常量池,类信息又包括类的版本、字段、方法、接口和父类信息等。
方法区和堆空间相似,它是一个共享内存区域,因此方法区是线程共享的。
本地方发栈和虚拟机栈
Java 虚拟机栈是线程私有的内存空间,当创建一个线程时,会在虚拟机中分配一个线程栈,用于存储方法的局部变量、操作数栈、动态链接方法等信息。每次调用一个方法,都会伴随着栈帧的入栈操作,当方法返回后,就是栈帧的出栈操作。
本地方法栈与虚拟机栈类似,本地方法栈用于管理本地方法的调用,也就是 native 方法。
JVM 内存设置方法
在了解上述基本知识后,我们来探讨一下 JVM 内存应该如何设置,以及有哪些参数可以用来设置。
在 JVM 中,需要配置的核心参数包括:
-Xms
,Java 堆内存大小-Xmx
,Java 最大堆内存大小-Xmn
,Java 堆内存中的新生代大小,扣除新生代剩下的就是老年代内存-XX:MetaspaceSize
,元空间大小, 128M-XX:MaxMetaspaceSize
,最大云空间大小 (如果没有指定这两个参数,元空间会在运行时根据需要动态调整。) 256M-Xss
,线程栈内存大小,这个基本上不需要预估,设置 512KB 到 1M 就行,因为值越小,能够分配的线程数越多。JVM 内存的大小受到服务器配置的影响,例如,一台拥有 2 个核心和 4G 内存的服务器,分配给 JVM 进程的内存大约为 2G。
这是因为服务器本身也需要内存,并且还需要为其他进程预留内存。这 2G 内存还需要分配给栈内存、堆内存和元空间,因此,堆内存可用的大约为 1G。
然后,堆内存还需要划分为新生代和老年代。
tomcat核心配置如下:
Apache Tomcat 8 Configuration Reference (8.0.53) - The HTTP Connector
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as
-1
to make clear that it is not used.
server:
tomcat:
uri-encoding: UTF-8
#最大工作线程数,默认200, 4核8g内存,线程数经验值800
#操作系统做线程之间的切换调度是有系统开销的,所以不是越多越好。
max-threads: 1000
# 等待队列长度,默认100,
accept-count: 1000
max-connections: 20000
# 最小工作空闲线程数,默认10, 适当增大一些,以便应对突然增长的访问量
min-spare-threads: 100
在我们之前的分析中,我们了解到当 NIOEndPoint 接收到客户端的请求连接后,会生成一个 SocketProcessor 任务并将其提交给线程池处理。
SocketProcessor 中的 run 方法会调用 HttpProcessor 组件来解析应用层的协议,并生成 Request 对象。
最后,调用 Adapter 的 Service 方法将请求传递到容器中。
容器主要负责处理内部的请求,即当前置的连接器通过 Socket 获取到信息后,将获得一个 Servlet 请求,而容器则负责处理这个 Servlet 请求。
Tomcat 使用 Mapper 组件将用户请求的 URL 定位到一个具体的 Serlvet,然后 Spring 中的 DispatcherServlet 拦截到该 Servlet 请求后,基于 Spring 自身的 Mapper 映射定位到我们具体的 Controller 中。
当请求到达 Controller 后,对于我们的业务来说,才算是请求的真正开始。
Controller 调用 Service、Service 调用 dao,完成数据库操作后将请求原路返回给客户端,完成一次整体的会话。
因此,Controller 中的业务逻辑处理时间,会对整个容器的并发性能产生影响。
简单的数学计算一下:
假设一个 Tomcat 节点的 QPS 为 500,如果要支持高峰时期的 QPS 为 18000,那么需要 40 台服务器。
这 40 台服务器需要通过 Nginx 软件负载均衡进行请求分发。
Nginx 的性能很好,官方说明其处理静态文件的并发能力可达 5W/s。
由于 Nginx 不能单点,我们可以采用 LVS 对 Nginx 进行负载均衡,LVS(Linux VirtualServer)采用 IP 负载均衡技术实现负载均衡。
通过这样的一组架构,我们当前服务端是能够同时承接 QPS=18000,但还不够。我们回到之前提到的两个公式。
假设我们的 RT 为 3s,那么服务器端的并发数=18000 * 3=54000,即同时有 54000 个连接打到服务器端。因此,服务端需要同时支持的连接数为 54000。
如果 RT 越大,意味着积压的连接越多,这些连接会占用内存资源/CPU 资源等,容易造成系统崩溃。
同时,当连接数超过阈值时,后续的请求无法进入,用户会得到一个请求超时的结果,这不是我们希望看到的。因此,我们必须缩短 RT 的值。
继续看上面这个图,一个请求需要等待 Tomcat 容器中的应用执行完成后才能返回。
在执行过程中,请求会进行哪些操作呢?
这些操作都会消耗时间,客户端请求需要等待这些操作完成后才能返回。
因此,降低响应时间的方法就是优化业务逻辑处理。
当 18000 个请求进入服务端并被接收后,开始执行业务逻辑处理,必然会涉及到数据库查询。
每个请求至少执行一次数据库查询操作,多的需要查询 3~5 次以上。
假设按照 3 次计算,那么每秒会对数据库形成 54000 个请求。
假设一台数据库服务器每秒支持 10000 个请求(影响数据库请求数量的因素有很多,如数据库表的数据量、数据库服务器的系统性能、查询语句的复杂度),那么需要 6 台数据库服务器才能支持每秒 10000 个请求。
除此之外,数据库层面还有其他优化方案。
将 MySQL 数据库中的数据放入 Redis 缓存中可以提升性能的原因如下:
- Redis 存储的是 Key-Value 格式的数据,其查找时间复杂度为 O(1)(常数阶),而 MySQL 引擎底层实现是 B+Tree,时间复杂度为 O(logn)(对数阶)。因此,Redis 相较于 MySQL 具有更快的查询速度。
- MySQL 数据存储在表中,查找数据时需要对表进行全局扫描或根据索引查找,这涉及到磁盘查找。而 Redis 则无需这么复杂,因为它直接根据数据在内存中的位置进行查找。
- Redis 是单线程的多路复用 IO,避免了线程切换的开销和 IO 等待的开销,从而在多核处理器下提高了处理器的使用效率。
对于磁盘操作,主要包括读取和写入。例如,在交易系统场景中,通常需要对账文件进行解析和写入。针对磁盘操作的优化方法有:
充分利用内存缓存,将经常访问的数据和对象保存在内存中,以避免重复加载或减少数据库访问带来的性能损耗。
远程服务调用会影响到 I/O 性能,主要包括:
在微服务中,针对处理时间长、逻辑复杂的情况,高并发时可能导致服务线程耗尽,无法创建新线程处理请求。
针对这种情况,除了在程序层面优化(如数据库调优、算法调优、缓存等),还可以考虑在架构上进行调整,如先返回结果给客户端,让用户可以继续使用客户端的其他操作,然后将服务端的复杂逻辑处理模块进行异步化处理。
这种异步化处理方式适用于客户端对处理结果不敏感、不要求实时的场景,如群发邮件、群发消息等。
异步化设计的解决方案有:
除了上述手段外,将业务系统拆分为微服务也十分必要,原因包括:
最重要的是,单个应用在性能上的瓶颈难以突破。
例如,要支持 18000 QPS,单个服务节点肯定无法支撑。因此,服务拆分的好处在于可以利用多台计算机组成一个大规模的分布式计算网络,通过网络通信完成整个业务逻辑。
关于如何拆分服务,虽然看起来简单,但实际操作时会遇到一些边界问题。
例如,有些数据模型既适用于 A 模块,也适用于 B 模块,如何划分界限呢?此外,服务拆分的粒度应该如何确定呢?
通常,服务拆分是按照业务进行的,并根据领域驱动设计(DDD)来指导微服务的边界划分。
领域驱动设计是一套方法论,通过定义领域模型,从而确定业务边界和应用边界,以保证业务模型和代码模型的一致性。
无论是 DDD 还是微服务,都需要遵循软件设计的基本原则:高内聚低耦合。
服务内部应具有高内聚性,服务之间应具有低耦合性。
实际上,一个领域服务对应了一个功能集合,这些功能具有一定共性。
例如,订单服务包括创建订单、修改订单、查询订单列表等功能,领域边界越清晰,功能内聚性越强,服务之间的耦合性就越低。
服务拆分还需要根据当前技术团队和公司状况来进行。
对于初创团队,不应过分追求微服务,以免导致业务逻辑过于分散,技术架构过于复杂,再加上基础设施尚不完善,可能导致交付时间延长,对公司发展产生较大影响。因此,在进行服务拆分时,还需要考虑以下因素:
对于旧系统改造,可能涉及的风险和问题更多。在开始改造之前,需要考虑以下几个步骤:拆分前准备阶段、设计拆分改造方案、实施拆分计划。
另外,每个阶段需要集中精力在一到两个具体的目标上,如果目标过多,反而可能会一事无成。例如,某个系统的微服务分解,制定了如下几个目标:
当然,不要期望一次性完成所有目标,每一个阶段可以选择一两个优先级高的目标进行执行。
微服务架构首先表现为一种分布式架构,其次,我们需要展现和提供业务服务能力,接着,我们要考虑与这些业务能力相关的各种非功能性能力。这些分散在不同位置的服务需要被统一管理,同时对服务的调用方保持透明,这样就产生了服务注册和发现的功能需求。
同样地,每个服务可能会部署在多台机器上的多个实例,因此,我们需要具备路由和寻址的能力,实现负载均衡,以提高系统的扩展性。面对这么多对外提供的服务接口,我们需要一种机制来统一接入控制,并将一些非业务策略应用到这个接入层,例如权限相关的策略,这就是服务网关的作用。同时,我们发现随着业务的发展和特定运营活动(如秒杀、大促等)的进行,流量可能会激增十倍以上,这时候我们就需要考虑系统容量、服务间的强弱依赖关系,实施服务降级、熔断和系统过载保护等措施。
以上由于微服务带来了这些复杂性,应用配置和业务配置都被分散到各个地方,因此,分布式配置中心的需求也随之产生。
最后,系统在分散部署后,所有的调用都涉及到跨进程,我们还需要一套能够在线进行链路跟踪和性能监控的技术,以便随时了解系统内部的状态和指标,使我们能够随时对系统进行分析和干预。
通过从微观到宏观的全面分析,我们可以基本上构建出一个完整的架构图。
原子服务为整个架构提供可复用的能力,
例如,评论服务作为一项原子服务,在B站的视频、文章、社区都需要,那么为了提高复用性,评论服务就可以独立为原子服务,不能与特定需求紧密耦合。
在这种情况下, 评论服务,需要供一种可以适应不同场景的复用能力。
类似的,文件存储、数据存储、推送服务、身份验证服务等功能,都会沉淀为原子服务,业务开发人员,在原子服务基础上,进行编排、配置、组合,可以快速构建业务应用。
3高到底如何量化,如何度量?
高并发没有一个确切的定义,它主要描述的是在短时间内面临大量流量的情况。
当你在面试或者工作中,你的领导或者面试官询问你如何设计一个能承受千万级别流量的系统时,你可以按照我提供的步骤进行分析。
一个能满足高并发需求的系统,并不是单纯地追求性能,而是需要至少满足三个宏观目标:
性能指标
通过性能指标,我们可以衡量当前的性能问题,并作为优化性能的评估依据。通常,我们会把一段时间内的接口响应时间作为衡量标准。
可用性指标
高可用性是指系统具有较高的无故障运行能力,可用性 = 平均故障时间 / 系统总运行时间,通常我们用几个 9 来描述系统的可用性。
对于高并发系统,最低要求是保证 3 个 9 或者 4 个 9。原因很直观,如果你只能做到 2 个 9,意味着有 1% 的故障时间,对于一些大公司每年千亿级别的 GMV 或收入,1% 的故障时间将导致十亿级别的业务影响。
可扩展性指标
在面对突发流量时,我们不能临时改造架构,所以增加机器以线性提高系统的处理能力是最快的方式。
对于业务集群或基础组件,扩展性 = 性能提升比例 / 机器增加比例,理想的扩展能力是:资源增加几倍,性能提升几倍。通常来说,扩展能力要保持在 70% 以上。
然而,从高并发系统的整体架构角度看,扩展的目标不仅仅是把服务设计成无状态,因为当流量增加 10 倍,业务服务可以快速扩容 10 倍,但数据库可能会成为新的瓶颈。
像 MySQL 这样的有状态存储服务通常是扩展的技术难点,如果架构上没有提前规划(垂直和水平拆分),就可能涉及到大量数据的迁移。
因此,高扩展性需要考虑:服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等,当并发达到某一个量级后,上述每个因素都可能成为扩展的瓶颈点。
通用设计方法
纵向扩展(scale-up)
它的目标是提升单机的处理能力,方案又包括:
横向扩展(scale-out)
由于单机性能总有极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,包括以下两个方向:
高可用方案主要从冗余、取舍、系统运维三个方向考虑,同时需有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
部署架构、节点规划相关面试题,是非常常见的面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
学习过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
https://zhuanlan.zhihu.com/p/422165687
《网易一面:单节点2000Wtps,Kafka怎么做的?》
《字节一面:事务补偿和事务重试,关系是什么?》
《网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?》
《亿级短视频,如何架构?》
《炸裂,靠“吹牛”过京东一面,月薪40K》
《太猛了,靠“吹牛”过顺丰一面,月薪30K》
《炸裂了…京东一面索命40问,过了就50W+》
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓