2026/1/10 9:08:14
网站建设
项目流程
个人网站设计实验原理,微信客户联系方式,山西省最新干部调整,做游戏用什么电脑系统下载网站好1000W并发如何部署#xff1f;部署多少节点#xff1f;量化标准是什么#xff1f;
对于如何支持 1000 万用户的问题#xff0c;实际上是一个相当抽象的问题。
对于技术开发者来说#xff0c;需要量化。
什么是量化#xff1f;就是需要一个明确的性能指标数据#xff0c;…1000W并发如何部署部署多少节点量化标准是什么对于如何支持 1000 万用户的问题实际上是一个相当抽象的问题。对于技术开发者来说需要量化。什么是量化就是需要一个明确的性能指标数据以便在执行关键业务时进行参考。例如在高峰时段系统的事务响应时间、并发用户数量、每秒查询率QPS、成功率等。量化的基本要求就是各项指标必须清晰明了。只有这样才能有效地指导整个架构的改进和优化。因此如果你面临这样的问题首先需要找到问题的核心也就是了解一些可以量化的数据指标。如果你有相关的历史业务交易数据那么你应该尽可能地参考这些数据并处理这些收集来的原始数据日志以分析出高峰时段和该时段的交易行为、规模等信息以便更清楚地了解需求细节。如果你没有相关的数据指标可以参考那么你就需要依靠经验来进行分析。例如你可以参考一些类似行业的成熟业务交易模型如银行业的日常交易活动或交通行业的售检票交易活动或者直接采用“2/8”原则和“2/5/8”原则来开始实践。当用户能在 2 秒内得到系统的响应时他们会觉得系统响应迅速在 2-5 秒内得到响应时他们会觉得系统响应速度尚可在 5-8 秒内得到响应时他们会觉得系统响应速度较慢但仍能接受然而当用户在超过 8 秒后仍未得到响应时他们会感到系统性能极差或者认为系统已经无法响应从而选择离开网站或者发起第二次请求。在估算关键指标如响应时间、并发用户数量、每秒查询率QPS、成功率的同时你也需要关注具体的业务功能需求。每个业务功能都有其独特的特点。例如在某些场景下可以不需要同步返回明确的执行结果而在某些业务场景下可以接受返回“系统忙请等待”这样的提示信息以避免过大的处理流量导致系统大规模瘫痪。因此学会平衡这些指标之间的关系是必要的。服务等级协议在大多数情况下最好为这些指标设定一个优先级顺序并尽可能只关注几个高优先级的指标要求。SLAService-Level Agreement 的缩写意思是服务等级协议。服务的 SLA 是服务提供者对服务消费者的正式承诺是衡量服务能力等级的关键项。服务 SLA 中定义的项必须是可测量的有明确的测量方法。1、并发中相关概念的解释在深入探讨上述问题之前我想先向大家介绍一下与系统相关的一些关键评估指标:qpstpsdaupvuv2、按照二八法则来推算 1000w 用户的访问量让我们回归最初的问题1000W并发需部署多少个节点假设我们没有历史数据可以参考我们可以采用二八定律来进行估算。假设有 1000W 用户每天访问网站的用户占比为 20%那么每天大约有 200W 用户访问。假设每个用户平均点击 50 次那么总的页面浏览量 PV1 亿。一天有 24 小时根据二八定律每天大部分用户活跃的时间点集中在(24 * 0.2) 约等于 5 个小时以内而大部分用户指的是1 亿点击 * 80%约等于 8000WPV 意味着在 5 个小时以内大概会有 8000W 点击进来也就是每秒大约有 4500(8000W/5 小时)个请求。4500 只是一个平均数值。在这 5 小时内请求量并不一定均匀可能会出现大量用户集中访问的情况比如像淘宝这样的网站日访问量高峰时间段集中在下午 14:00 和晚上 21:00其中 21:00 是一天中访问量的最高峰。通常情况下访问量高峰时段的请求量是平均请求量的 3 到 4 倍这是一个经验值我们按照 4 倍来计算。那么在这 5 小时内可能会出现每秒 18000 个请求的情况。因此问题由原本的支撑 1000W 用户变成了一个具体的问题就是服务器端需要能够支撑每秒 18000 个请求QPS180003、服务器压力预估在大致估算了后端服务器需要承受的最高并发峰值之后我们需要从整个系统架构的角度进行压力测试然后合理配置服务器数量和架构。首先我们需要了解一台服务器能承受多大的并发量那么该如何进行分析呢由于我们的应用部署在 Tomcat 上因此我们需要从 Tomcat 的性能入手。以下是一个描述 Tomcat 工作原理的图表图表说明如下LimitLatch 是连接控制器它负责控制 Tomcat 能同时处理的最大连接数。在 NIO/NIO2 模式下默认值为 10000而在 APR/native 模式下默认值为 8192。Acceptor 是一个独立线程它在 run 方法中的 while 循环里调用 socket.accept 方法接收客户端的连接请求。每当有新的请求到来时accept 会返回一个 Channel 对象然后将该 Channel 对象交给 Poller 处理。Poller 本质上是一个 Selector它也实现了线程。Poller 在内部维护一个 Channel 数组并在一个死循环中不断检测 Channel 的数据就绪状态。一旦有 Channel 可读它将生成一个 SocketProcessor 任务对象并交给 Executor 处理。SocketProcessor 实现了 Runnable 接口。当线程池执行 SocketProcessor 任务时它会通过 Http11Processor 处理当前请求。Http11Processor 读取 Channel 的数据以生成 ServletRequest 对象。Executor 是线程池负责运行 SocketProcessor 任务。SocketProcessor 的 run 方法会调用 Http11Processor 读取和解析请求数据。我们知道Http11Processor 是应用层协议的封装它会调用容器获取响应然后将响应通过 Channel 写出。从这个图中我们可以得知影响 Tomcat 请求数量的因素主要有四个方面。3.1 Tomcat 影响因素1当前服务器系统资源我想可能大家遇到过类似“Socket/FileCan’t open so many files”的异常这就是 Linux 系统中文件句柄限制的表示。在 Linux 操作系统中每一个 TCP 连接都会占用一个文件描述符fd当文件描述符超过 Linux 系统当前的限制时就会弹出这个错误提示。我们可以通过以下命令来查看一个进程能够打开的文件数量上限。ulimit-a 或者 ulimit-nopenfiles-n1024是 linux 操作系统对一个进程打开的文件句柄数量的限制也包含打开的套接字数量这里只是对用户级别的限制其实还有个是对系统的总限制查看系统总线制cat/proc/sys/fs/file-maxfile-max 是设定系统所有进程总共可以打开的文件数量。同时部分程序可以通过setrlimit调用设置每个进程的限制。如果收到大量文件句柄使用完毕的错误信息那么我们应该考虑增加这个数值。当遇到上述错误时我们可以通过以下方式进行修改针对单个进程的文件打开数量限制vi/etc/security/limits.conf root soft nofile65535root hard nofile65535*soft nofile65535*hard nofile65535*代表所有用户、root表示 root 用户。noproc 表示最大进程数量nofile 代表最大文件打开数量。soft/hard前者当达到阈值时制作警告后者会报错。另外还需要确保针对进程级别的文件打开数量限制是小于或等于系统的总限制如果不是那么我们需要修改系统的总限制。vi/proc/sys/fs/file-maxTCP 连接对于系统资源最大的开销在于内存。由于 TCP 连接需要双方进行数据接收和发送因此需要设置读取缓冲区和写入缓冲区。在 Linux 系统中这两个缓冲区的最小大小为 4096 字节可以通过查看/proc/sys/net/ipv4/tcp_rmem 和/proc/sys/net/ipv4/tcp_wmem 来获取相关信息。因此一个 tcp 连接最小占用内存为409640968k那么对于一个8G 内存的机器如果不考虑其他限制其最大并发数约为8*1024*1024/8约等于100万。这个数字是理论上的最大值在实际应用中受到 Linux 内核对部分资源的限制以及程序业务处理的影响8GB 内存很难达到 100 万连接。当然我们可以通过增加内存来提高并发数。3.2 Tomcat 影响因素2Tomcat 依赖的 JVM 的配置我们都知道Tomcat 是一个 Java 程序运行在 JVM 上因此对 JVM 进行优化也是提高 Tomcat 性能的关键。下面简单介绍一下 JVM 的基本情况如下图所示。在 JVM 里内存被划分为堆、程序计数器、本地方法栈、方法区元空间和虚拟机栈。1.3.2.1 堆空间说明堆内存是 JVM 内存中最大的一个区域绝大多数的对象和数组都会被分配在此它供所有线程共享。堆空间被划分为新生代和老年代新生代进一步被划分为 Eden 和 Survivor 区如下图所示。新生代和老年代的比例为 1:2也就是说新生代占堆空间的 1/3而老年代占 2/3。另外在新生代中空间分配比例为 Eden:Survivor0:Survivor18:1:1。举例来说如果 Eden 区的内存大小是 40M那么两个 Survivor 区的内存分别占 5M新生代的总内存就是 50M进而计算出老年代的内存大小为 100M也就是说堆空间的总内存大小是 150M。可以通过 java -XX:PrintFlagsFinal -version 查看默认参数uintxInitialSurvivorRatio8uintxNewRatio2InitialSurvivorRatio:新生代Eden/Survivor空间的初始比例NewRatio Old 区/Young 区的内存比例堆内存的具体工作机制如下绝大多数对象在创建后会被放置在 Eden 区当 Eden 区满时会触发 YGCYoung GC大部分对象会被回收仍有存活的对象会被复制到 Survivor0此时 Eden 区被清空。如果再次触发 YGC存活的对象会从 EdenSurvivor0 区复制到 Survivor1 区此时 Eden 和 Survivor0 区被清空。再次触发 YGCEdenSurvivor1 中的对象会被复制到 Survivor0 区如此循环直到对象的年龄达到阈值则会被移至老年代。这样的设计是因为 Eden 区的大多数对象会被回收。Survivor 区无法容纳的对象会直接进入老年代。当老年代满时会触发 Full GC。GC 标记-清除算法 在执行过程中暂停其他线程1.3.2.2 程序计数器程序计数器用于记录各个线程执行的字节码地址等信息在线程发生上下文切换时依赖它来记录当前执行位置以便在下次恢复执行时能够从上次执行位置继续执行。1.3.2.3 方法区方法区是一个逻辑概念在 HotSpot 虚拟机的 1.8 版本中它的具体实现就是元空间。方法区主要用来存储已经被虚拟机加载的类相关信息包括类元信息、运行时常量池、字符串常量池类信息又包括类的版本、字段、方法、接口和父类信息等。方法区和堆空间相似它是一个共享内存区域因此方法区是线程共享的。本地方发栈和虚拟机栈Java 虚拟机栈是线程私有的内存空间当创建一个线程时会在虚拟机中分配一个线程栈用于存储方法的局部变量、操作数栈、动态链接方法等信息。每次调用一个方法都会伴随着栈帧的入栈操作当方法返回后就是栈帧的出栈操作。本地方法栈与虚拟机栈类似本地方法栈用于管理本地方法的调用也就是 native 方法。JVM 内存设置方法在了解上述基本知识后我们来探讨一下 JVM 内存应该如何设置以及有哪些参数可以用来设置。在 JVM 中需要配置的核心参数包括-XmsJava 堆内存大小-XmxJava 最大堆内存大小-XmnJava 堆内存中的新生代大小扣除新生代剩下的就是老年代内存新生代内存设置过小会频繁触发 Minor GC频繁触发 GC 会影响系统的稳定性-XX:MetaspaceSize元空间大小 128M-XX:MaxMetaspaceSize最大云空间大小 如果没有指定这两个参数元空间会在运行时根据需要动态调整。 256M一个新系统的元空间基本上没办法有一个测算的方法一般设置几百兆就够用因为这里面主要存放一些类信息。-Xss线程栈内存大小这个基本上不需要预估设置 512KB 到 1M 就行因为值越小能够分配的线程数越多。JVM 内存的大小受到服务器配置的影响例如一台拥有 2 个核心和 4G 内存的服务器分配给 JVM 进程的内存大约为 2G。这是因为服务器本身也需要内存并且还需要为其他进程预留内存。这 2G 内存还需要分配给栈内存、堆内存和元空间因此堆内存可用的大约为 1G。然后堆内存还需要划分为新生代和老年代。3.3 Tomcat 影响因素3Tomcat 本身的配置tomcat核心配置如下[ApacheTomcat8ConfigurationReference(8.0.53)-TheHTTPConnector]http://tomcat.apache.org/tomcat-8.0-doc/config/http.htmlThemaximum number of request processing threadstobecreated bythisConnector,which therefore determines the maximum number of simultaneous requests that can behandled.Ifnot specified,thisattribute is setto200.Ifan executor is associatedwiththisconnector,thisattribute is ignored as the connector will execute tasks using the executor rather than an internal threadpool.Notethatifan executor is configured any value setforthisattribute will be recorded correctly but it will be reported(e.g.via JMX)as-1tomakeclear that it is not used.server:tomcat:uri-encoding:UTF-8#最大工作线程数默认200,4核8g内存线程数经验值800#操作系统做线程之间的切换调度是有系统开销的所以不是越多越好。 max-threads:1000# 等待队列长度默认100 accept-count:1000max-connections:20000# 最小工作空闲线程数默认10,适当增大一些以便应对突然增长的访问量 min-spare-threads:100accept-count这是最大等待数当 HTTP 请求数量达到 Tomcat 的最大线程数时如果有新的 HTTP 请求到达Tomcat 会将该请求放入等待队列中。这个 acceptCount 就是指能够接受的最大等待数默认值为 100。如果等待队列也被填满那么新的请求将会被 Tomcat 拒绝connection refused。maxThreads这是最大线程数每当一个 HTTP 请求到达 Web 服务时Tomcat 都会创建一个线程来处理该请求。maxThreads 决定了 Web 服务容器能同时处理多少个请求。maxThreads 默认值为 200建议增加。然而增加线程会有成本更多的线程不仅会带来更多的线程上下文切换成本还会消耗更多的内存。JVM 默认在创建新线程时会分配大小为 1M 的线程栈因此更多的线程意味着需要更多的内存。线程数的经验值为1 核 2g 内存为 200线程数经验值 2004 核 8g 内存线程数经验值 800。maxConnections这是最大连接数这个参数指定了在同一时间内Tomcat 能够接受的最大连接数。对于 Java 的阻塞式 BIO默认值是 maxthreads 的值如果在 BIO 模式下使用自定义的 Executor 执行器默认值将是执行器中 maxthreads 的值。对于 Java 新的 NIO 模式maxConnections 默认值是 10000。对于 Windows 上的 APR/native IO 模式maxConnections 默认值为 8192。如果设置为-1则禁用 maxconnections 功能表示不限制 tomcat 容器的连接数。maxConnections 和 accept-count 的关系为当连接数达到最大值 maxConnections 后系统会继续接收连接但不会超过 acceptCount 的值。3.4 Tomcat 影响因素4应用带来的压力在我们之前的分析中我们了解到当 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 中的业务逻辑处理时间会对整个容器的并发性能产生影响。4、服务器数量评估简单的数学计算一下假设一个 Tomcat 节点的 QPS 为 500如果要支持高峰时期的 QPS 为 18000那么需要 40 台服务器。这 40 台服务器需要通过 Nginx 软件负载均衡进行请求分发。Nginx 的性能很好官方说明其处理静态文件的并发能力可达 5W/s。由于 Nginx 不能单点我们可以采用 LVS 对 Nginx 进行负载均衡LVSLinux VirtualServer采用 IP 负载均衡技术实现负载均衡。通过这样的一组架构我们当前服务端是能够同时承接 QPS18000但还不够。我们回到之前提到的两个公式。QPS并发量/平均响应时间并发量QPS * 平均响应时间假设我们的 RT 为 3s那么服务器端的并发数18000 * 354000即同时有 54000 个连接打到服务器端。因此服务端需要同时支持的连接数为 54000。如果 RT 越大意味着积压的连接越多这些连接会占用内存资源/CPU 资源等容易造成系统崩溃。同时当连接数超过阈值时后续的请求无法进入用户会得到一个请求超时的结果这不是我们希望看到的。因此我们必须缩短 RT 的值。5、如何降低RT的值继续看上面这个图一个请求需要等待 Tomcat 容器中的应用执行完成后才能返回。在执行过程中请求会进行哪些操作呢查询数据库访问磁盘数据进行内存运算调用远程服务这些操作都会消耗时间客户端请求需要等待这些操作完成后才能返回。因此降低响应时间的方法就是优化业务逻辑处理。5.1 数据库的优化当 18000 个请求进入服务端并被接收后开始执行业务逻辑处理必然会涉及到数据库查询。每个请求至少执行一次数据库查询操作多的需要查询 3~5 次以上。假设按照 3 次计算那么每秒会对数据库形成 54000 个请求。假设一台数据库服务器每秒支持 10000 个请求影响数据库请求数量的因素有很多如数据库表的数据量、数据库服务器的系统性能、查询语句的复杂度那么需要 6 台数据库服务器才能支持每秒 10000 个请求。除此之外数据库层面还有其他优化方案。首先是 MySQL 的最大连接数设置。当访问量过高时可能会遇到 MySQL: ERROR 1040: Too many connections 的问题原因就是连接数耗尽。如果服务器的并发连接请求量较大建议调高此值以增加并行连接数量。但需要考虑到机器的承载能力因为连接数越多每个连接提供的连接缓冲区会占用越多的内存所以要适当调整该值不能盲目提高设值。引入缓存组件。数据表数据量过大例如达到几千万甚至上亿。这种情况下SQL 优化已经意义不大因为这么大的数据量查询必然会涉及到计算。可以通过缓存解决读请求并发过高的问题。一般来说数据库的读写请求遵循二八法则。在每秒 54000 个请求中大约有 43200 个是读请求这些读请求中大约 90% 都可以通过缓存解决。将 MySQL 数据库中的数据放入 Redis 缓存中可以提升性能的原因如下1.Redis 存储的是 Key-Value 格式的数据其查找时间复杂度为 O(1)常数阶而 MySQL 引擎底层实现是 BTree时间复杂度为 O(logn)对数阶。因此Redis 相较于 MySQL 具有更快的查询速度。2.MySQL 数据存储在表中查找数据时需要对表进行全局扫描或根据索引查找这涉及到磁盘查找。而 Redis 则无需这么复杂因为它直接根据数据在内存中的位置进行查找。3.Redis 是单线程的多路复用 IO避免了线程切换的开销和 IO 等待的开销从而在多核处理器下提高了处理器的使用效率。分库分表减少单表数据量单表数据量少了查询性能自然得到有效提升。读写分离避免事务操作对查询操作带来的性能影响。写操作本身耗费资源数据库写操作为 IO 写入写入过程中通常会涉及唯一性校验、建索引、索引排序等操作对资源消耗较大。一次写操作的响应时间往往是读操作的几倍甚至几十倍。锁争用写操作很多时候需要加锁包括表级锁、行级锁等。这类锁都是排他锁一个会话占据排它锁后其他会话不能读取数据这会极大影响数据读取性能。因此MySQL 部署通常采用读写分离方式主库用来写入数据及部分时效性要求很高的读操作从库用来承接大部分读操作这样数据库整体性能能够得到大幅提升。sqlnosql异构存储。针对不同特性的数据采用不同的存储库例如 MongoDBNoSQL 文档化存储、RedisNoSQL Key-Value 存储、HBaseNoSQL 列式存储这些数据库在某种程度上与 Key-Value 数据库相似。nosql具有很高的扩展性适合管理大量非结构化数据。客户端池化技术减少频繁创建数据库连接的性能损耗。在每次进行数据库操作之前先建立连接然后进行数据库操作最后释放连接。这个过程涉及到网络通信的延时以及频繁创建和销毁连接对象的性能开销。当请求量较大时这种性能损耗会变得非常明显。通过使用连接池技术可以重用已创建的连接降低这种性能损耗。5.2 磁盘数据访问优化对于磁盘操作主要包括读取和写入。例如在交易系统场景中通常需要对账文件进行解析和写入。针对磁盘操作的优化方法有利用磁盘缓存通过缓存 I/O充分利用系统缓存以降低实际 I/O 的次数。采用顺序读写用追加写代替随机写减少寻址开销提高 I/O 写入速度。使用 SSD 代替 HDD固态硬盘的 I/O 效率远高于机械硬盘。在频繁读写相同磁盘空间时可以使用 mmap内存映射代替 read/write减少内存拷贝次数。在需要同步写入的场景中尽量合并写请求而不是让每个请求都同步写入磁盘可以使用 fsync() 代替 O_SYNC。5.3 合理利用内存充分利用内存缓存将经常访问的数据和对象保存在内存中以避免重复加载或减少数据库访问带来的性能损耗。5.4 调用远程服务远程服务调用会影响到 I/O 性能主要包括远程调用等待返回结果的阻塞异步通信网络通信的耗时内网通信增加网络带宽远程服务通信的稳定性5.5 异步化架构在微服务中针对处理时间长、逻辑复杂的情况高并发时可能导致服务线程耗尽无法创建新线程处理请求。针对这种情况除了在程序层面优化如数据库调优、算法调优、缓存等还可以考虑在架构上进行调整如先返回结果给客户端让用户可以继续使用客户端的其他操作然后将服务端的复杂逻辑处理模块进行异步化处理。这种异步化处理方式适用于客户端对处理结果不敏感、不要求实时的场景如群发邮件、群发消息等。异步化设计的解决方案有多线程消息队列MQ6、应用服务的拆分除了上述手段外将业务系统拆分为微服务也十分必要原因包括业务发展导致应用程序复杂度增加产生熵增现象。业务系统功能越来越多参与开发迭代的人员也越来越多维护一个庞大的项目容易出现问题。单个应用系统难以实现横向扩容服务器资源有限可能导致所有请求集中请求到某个服务器节点造成资源消耗过大系统不稳定。测试、部署成本逐渐增加。…最重要的是单个应用在性能上的瓶颈难以突破。例如要支持 18000 QPS单个服务节点肯定无法支撑。因此服务拆分的好处在于可以利用多台计算机组成一个大规模的分布式计算网络通过网络通信完成整个业务逻辑。6.1 如何拆分服务关于如何拆分服务虽然看起来简单但实际操作时会遇到一些边界问题。例如有些数据模型既适用于 A 模块也适用于 B 模块如何划分界限呢此外服务拆分的粒度应该如何确定呢通常服务拆分是按照业务进行的并根据领域驱动设计DDD来指导微服务的边界划分。领域驱动设计是一套方法论通过定义领域模型从而确定业务边界和应用边界以保证业务模型和代码模型的一致性。无论是 DDD 还是微服务都需要遵循软件设计的基本原则高内聚低耦合。服务内部应具有高内聚性服务之间应具有低耦合性。实际上一个领域服务对应了一个功能集合这些功能具有一定共性。例如订单服务包括创建订单、修改订单、查询订单列表等功能领域边界越清晰功能内聚性越强服务之间的耦合性就越低。服务拆分还需要根据当前技术团队和公司状况来进行。对于初创团队不应过分追求微服务以免导致业务逻辑过于分散技术架构过于复杂再加上基础设施尚不完善可能导致交付时间延长对公司发展产生较大影响。因此在进行服务拆分时还需要考虑以下因素公司业务所处领域的市场性质如果是市场敏感项目应先推出产品然后再进行迭代和优化。开发团队的成熟度团队技术能否承接。基础能力是否足够如 DevOps、运维、测试自动化等基础能力。团队是否有能力支持大量服务实例运行带来的运维复杂度是否可以做好服务监控。测试团队的执行效率如果测试团队不能支持自动化测试、自动回归、压力测试等手段来提高测试效率那必然会导致测试工作量显著增加从而导致项目上线周期延期。对于旧系统改造可能涉及的风险和问题更多。在开始改造之前需要考虑以下几个步骤拆分前准备阶段、设计拆分改造方案、实施拆分计划。在开始分解之前需要先对当前的整体架构以及各个模块之间的依赖关系有一个清晰的理解同时在准备阶段主要需要弄明白依赖关系和接口。这样可以在分解时知道如何操作比如应该在哪里进行第一次切割以便将一个复杂的单体系统迅速变为两个较小的系统同时也要尽量减少对系统现有业务的影响。要避免构建出一个分布式的单体应用这种应用包含了许多互相之间紧密耦合的服务却又必须一起部署这被称为所谓的分布式系统。如果没有进行深入的分析就强行分解可能会不小心切断重要的依赖导致出现 A 类大故障后果不堪设想。在不同的阶段分解的重点是不同的每个阶段都有其需要关注的核心问题。分解本身可以分为三个阶段核心业务和非业务部分的分解、核心业务的调整设计、核心业务内部的分解。在第一个阶段需要将核心业务精简将非核心部分剥离以减小需要处理的系统规模在第二个阶段需要按照微服务的设计理念重新构建核心业务部分在第三个阶段需要将核心业务部分的重构设计实施。分解的方式也有三种代码分解、部署分解、数据分解。另外每个阶段需要集中精力在一到两个具体的目标上如果目标过多反而可能会一事无成。例如某个系统的微服务分解制定了如下几个目标1.性能指标吞吐量和延迟核心交易的吞吐量提升一倍以上TPS1000-10000A 业务的延迟减少一半Latency250ms-125msB 业务的延迟减少一半Latency70ms-35ms。2.稳定性指标可用性故障恢复时间可用性99.99%A 类故障恢复时间15 分钟一个季度内的故障次数1 次。3.质量指标编写完整的产品需求文档、设计文档、部署运维文档核心交易部分代码 90% 以上单测覆盖率和 100% 的自动化测试用例和场景覆盖实现可持续的性能测试基准环境和长期持续性能优化机制。4.扩展性指标完成代码、部署、运行时和数据多个维度的合理分解对于核心系统重构后的各块业务和交易模块、以及对应的各个数据存储都可以随时通过增加机器资源实现伸缩扩展。5.可维护性指标建立全面完善的监控指标、特别是全链路的实时性能指标数据覆盖所有关键业务和状态缩短监控报警响应处置时间配合运维团队实现容量规划和管理出现问题时可以在一分钟内拉起系统或者回滚到上一个可用版本启动时间1 分钟。6.易用性指标通过重构实现新的 API 接口既合理又简单极大地满足各个层面用户的使用需求客户满意度持续上升。7.业务支持指标对于新的业务需求功能开发在保障质量的前提下开发效率提升一倍开发资源和周期降低一半。当然不要期望一次性完成所有目标每一个阶段可以选择一两个优先级高的目标进行执行。6.2 微服务化之后如何进行服务治理微服务架构首先表现为一种分布式架构其次我们需要展现和提供业务服务能力接着我们要考虑与这些业务能力相关的各种非功能性能力。这些分散在不同位置的服务需要被统一管理同时对服务的调用方保持透明这样就产生了服务注册和发现的功能需求。同样地每个服务可能会部署在多台机器上的多个实例因此我们需要具备路由和寻址的能力实现负载均衡以提高系统的扩展性。面对这么多对外提供的服务接口我们需要一种机制来统一接入控制并将一些非业务策略应用到这个接入层例如权限相关的策略这就是服务网关的作用。同时我们发现随着业务的发展和特定运营活动如秒杀、大促等的进行流量可能会激增十倍以上这时候我们就需要考虑系统容量、服务间的强弱依赖关系实施服务降级、熔断和系统过载保护等措施。以上由于微服务带来了这些复杂性应用配置和业务配置都被分散到各个地方因此分布式配置中心的需求也随之产生。最后系统在分散部署后所有的调用都涉及到跨进程我们还需要一套能够在线进行链路跟踪和性能监控的技术以便随时了解系统内部的状态和指标使我们能够随时对系统进行分析和干预。6.3 整体架构图通过从微观到宏观的全面分析我们可以基本上构建出一个完整的架构图。接入层这是外部请求进入内部系统的门户所有的请求都必须通过 API 网关。应用层也被称为聚合层它为相关业务提供聚合接口并调用中台服务进行组合。原子服务包括就是原子技术服务原子业务服务根据业务需求提供相关的接口。原子服务为整个架构提供可复用的能力例如评论服务作为一项原子服务在B站的视频、文章、社区都需要那么为了提高复用性评论服务就可以独立为原子服务不能与特定需求紧密耦合。在这种情况下, 评论服务需要供一种可以适应不同场景的复用能力。类似的文件存储、数据存储、推送服务、身份验证服务等功能都会沉淀为原子服务业务开发人员在原子服务基础上进行编排、配置、组合可以快速构建业务应用。7、3高到底如何量化如何度量3高到底如何量化如何度量高并发没有一个确切的定义它主要描述的是在短时间内面临大量流量的情况。当你在面试或者工作中你的领导或者面试官询问你如何设计一个能承受千万级别流量的系统时你可以按照我提供的步骤进行分析。首先你需要确立一些可以量化的数据指标例如每秒查询率QPS、每日活跃用户数DAU、总用户数、每秒事务数TPS以及访问峰值。然后根据这些数据你开始设计系统的架构方案。接着落地执行7.1 高并发中的宏观指标一个能满足高并发需求的系统并不是单纯地追求性能而是需要至少满足三个宏观目标高性能这是系统并行处理能力的体现。在有限的硬件投入下提高性能就意味着节约成本。同时性能也关乎用户体验响应时间是 100 毫秒还是 1 秒用户的感受是完全不同的。高可用这是系统能正常提供服务的时间。一个全年无故障、不停机的系统和一个经常出故障、宕机的系统用户肯定会选择前者。另外如果系统的可用性只能达到 90%也会对业务造成重大影响。高扩展这是系统的扩展能力即在流量高峰期是否能在短时间内完成扩容以更稳定地承受峰值流量例如双 11 活动、明星离婚等热点事件。7.2 微观指标性能指标通过性能指标我们可以衡量当前的性能问题并作为优化性能的评估依据。通常我们会把一段时间内的接口响应时间作为衡量标准。1.平均响应时间这是最常用的衡量方式但它的缺点是对于慢请求不敏感。例如1 万次请求中有 9900 次是 1 毫秒有 100 次是 100 毫秒那么平均响应时间就是 1.99 毫秒。尽管平均耗时仅增加了 0.99 毫秒但 1% 的请求的响应时间已经增加了 100 倍。2.TP90、TP99 等分位值这是将响应时间按照从小到大排序后的指标TP90 表示排在第 90 分位的响应时间分位值越大对慢请求越敏感。可用性指标高可用性是指系统具有较高的无故障运行能力可用性 平均故障时间 / 系统总运行时间通常我们用几个 9 来描述系统的可用性。对于高并发系统最低要求是保证 3 个 9 或者 4 个 9。原因很直观如果你只能做到 2 个 9意味着有 1% 的故障时间对于一些大公司每年千亿级别的 GMV 或收入1% 的故障时间将导致十亿级别的业务影响。可扩展性指标在面对突发流量时我们不能临时改造架构所以增加机器以线性提高系统的处理能力是最快的方式。对于业务集群或基础组件扩展性 性能提升比例 / 机器增加比例理想的扩展能力是资源增加几倍性能提升几倍。通常来说扩展能力要保持在 70% 以上。然而从高并发系统的整体架构角度看扩展的目标不仅仅是把服务设计成无状态因为当流量增加 10 倍业务服务可以快速扩容 10 倍但数据库可能会成为新的瓶颈。像 MySQL 这样的有状态存储服务通常是扩展的技术难点如果架构上没有提前规划垂直和水平拆分就可能涉及到大量数据的迁移。因此高扩展性需要考虑服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等当并发达到某一个量级后上述每个因素都可能成为扩展的瓶颈点。7.3 实践方案通用设计方法纵向扩展scale-up它的目标是提升单机的处理能力方案又包括1.提升单机的硬件性能通过增加内存、CPU 核数、存储容量、或将磁盘升级成 SSD 等方式来提升。2.提升单机的软件性能使用缓存减少 IO 次数使用并发或异步的方式增加吞吐量。横向扩展scale-out由于单机性能总有极限所以最终还需要引入横向扩展通过集群部署以进一步提高并发处理能力包括以下两个方向1.做好分层架构这是横向扩展的基础因为高并发系统通常业务复杂通过分层处理可以简化复杂问题更容易做到横向扩展。2.各层进行水平扩展无状态水平扩容有状态做分片路由。业务集群通常能设计成无状态的而数据库和缓存往往是有状态的因此需要设计分区键做好存储分片当然也可以通过主从同步、读写分离的方案提升读性能。7.3.1 高性能实践方案1.分布式部署通过负载均衡分担单机压力。2.多层次缓存包括静态数据使用 CDN、本地缓存、分布式缓存等以及处理缓存场景中的热点 key、缓存穿透、缓存并发、数据一致性等问题。3.数据库和索引优化以及利用搜索引擎解决复杂查询问题。4.考虑使用 NoSQL 数据库如 HBase、TiDB 等但团队需熟悉这些组件并具备强大的运维能力。5.异步处理将次要流程通过多线程、消息队列、甚至延时任务进行异步处理。6.流量控制考虑业务是否允许限流如秒杀场景包括前端限流、Nginx 接入层限流、服务端限流。7.流量削峰填谷通过消息队列接收流量。8.并发处理通过多线程将串行逻辑并行化。9.预计算如抢红包场景可提前计算红包金额并缓存发红包时直接使用。10.缓存预热通过异步任务提前将数据预热到本地缓存或分布式缓存中。11.减少 IO 次数如数据库和缓存的批量读写、RPC 的批量接口支持、或通过冗余数据减少 RPC 调用。12.减少 IO 时的数据包大小包括采用轻量级通信协议、合适的数据结构、去除接口中多余字段、减少缓存 key 大小、压缩缓存 value 等。13.优化程序逻辑如将高概率阻断执行流程的判断逻辑前置、For 循环计算逻辑优化或采用更高效算法。14.使用各种池化技术如 HTTP 请求池、线程池考虑 CPU 密集型或 IO 密集型设置核心参数、数据库和 Redis 连接池等。15.JVM 优化包括新生代和老年代的大小、GC 算法选择等以减少 GC 频率和耗时。16.锁策略选择读多写少场景使用乐观锁或考虑通过分段锁减少锁冲突。7.3.2 高可用实践方案1.节点故障转移Nginx 和服务治理框架支持故障节点切换到另一个节点。2.非对等节点的故障转移通过心跳检测并实施主备切换如 Redis 哨兵模式或集群模式、MySQL 主从切换等。3.设置接口层的超时、重试策略和幂等设计。4.降级处理保证核心服务牺牲非核心服务必要时进行熔断或核心链路出问题时有备选链路。5.流量控制对超过系统处理能力的请求直接拒绝或返回错误码。6.消息队列的可靠性保证包括生产者端的重试机制、消息代理的持久化、消费者端的 ack 机制等。7.灰度发布支持按机器维度进行小流量部署观察系统日志和业务指标运行稳定后再推全量。8.监控报警包括基础 CPU、内存、磁盘、网络 监控以及 Web 服务器、JVM、数据库、各类中间件监控和业务指标监控。9.灾备演练类似当前的“混沌工程”对系统进行破坏性手段观察局部故障是否会引起可用性问题。高可用方案主要从冗余、取舍、系统运维三个方向考虑同时需有配套的值班机制和故障处理流程当出现线上问题时可及时跟进处理。7.3.3 高扩展的实践方案1.合理的分层架构例如互联网最常见的分层架构还可以进一步按照数据访问层、业务逻辑层对微服务进行更细粒度的分层但需评估性能会存在网络多一跳的情况。2.存储层拆分按照业务维度进行垂直拆分、按照数据特征维度进行水平拆分分库分表。3.业务层拆分最常见的是按照业务维度拆如电商场景的商品服务、订单服务等也可以按照核心接口和非核心接口拆还可以按照请求去拆如 To C 和 To BAPP 和 H5。