在上一篇文章中,详细分析了设计一个千万级并发架构所需要思考的问题,以及解决方案。
在这一片文章中,我们主要分析如何在职场足够用户数量的情况下,同步提升架构的性能降低平均响应时间。

如何降低RT的值

继续看上面这个图,一个请求只有等到tomcat容器中的应用执行完成才能返回,而请求在执行过程中会做什么事情呢?

  • 查询数据库
  • 访问磁盘数据
  • 进行内存运算
  • 调用远程服务

这些操作每一个步骤都会消耗时间,当前客户端的请求只有等到这些操作都完成之后才能返回,所以降低RT的方法,就是优化业务逻辑的处理。

数据库瓶颈的优化

当18000个请求进入到服务端并且被接收后,开始执行业务逻辑处理,那么必然会查询数据库。

每个请求至少都有一次查询数据库的操作,多的需要查询3~5次以上,我们假设按照3次来计算,那么每秒会对数据库形成54000个请求,假设一台数据库服务器每秒支撑10000个请求(影响数据库的请求数量有很多因素,比如数据库表的数据量、数据库服务器本身的系统性能、查询语句的复杂度),那么需要6台数据库服务器才能支撑每秒10000个请求。

除此之外,数据库层面还有涉及到其他的优化方案。

  • 首先是Mysql的最大连接数设置,大家可能遇到过MySQL: ERROR 1040: Too many connections这样的问题,原因就是访问量过高,连接数耗尽了。

    show variables like '%max_connections%';

    如果服务器的并发连接请求量比较大,建议调高此值,以增加并行连接数量,当然这建立在机器能支撑的情况下,因为如果连接数越多,介于MySQL会为每个连接提供连接缓冲区,就会开销越多的内存,所以要适当调整该值,不能盲目提高设值。

  • 数据表数据量过大,比如达到几千万甚至上亿,这种情况下sql的优化已经毫无意义了,因为这么大的数据量查询必然会涉及到运算。

    • 可以缓存来解决读请求并发过高的问题,一般来说对于数据库的读写请求也都遵循2/8法则,在每秒54000个请求中,大概有43200左右是读请求,这些读请求中基本上90%都是可以通过缓存来解决。

    • 分库分表,减少单表数据量,单表数据量少了,那么查询性能就自然得到了有效的提升

    • 读写分离,避免事务操作对查询操作带来的性能影响

      • 写操作本身耗费资源

        数据库写操作为IO写入,写入过程中通常会涉及唯一性校验、建索引、索引排序等操作,对资源消耗比较大。一次写操作的响应时间往往是读操作的几倍甚至几十倍。

      • 锁争用

        写操作很多时候需要加锁,包括表级锁、行级锁等,这类锁都是排他锁,一个会话占据排它锁之后,其他会话是不能读取数据的,这会会极大影响数据读取性能。

        所以MYSQL部署往往会采用读写分离方式,主库用来写入数据及部分时效性要求很高的读操作,从库用来承接大部分读操作,这样数据库整体性能能够得到大幅提升。

  • 不同类型的数据采用不同的存储库,

    • MongoDB nosql 文档化存储
    • Redis nosql key-value存储
    • HBase nosql, 列式存储,其实本质上有点类似于key-value数据库。
    • cassandra,Cassandra 是一个来自 Apache 的分布式数据库,具有高度可扩展性,可用于管理大量的结构化数据
    • TIDB,是PingCAP公司自主设计、研发的开源分布式关系型数据库,是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and Analytical Processing, HTAP) 的融合型分布式数据库产品

为什么把mysql数据库中的数据放redis缓存中能提升性能?

  1. Redis存储的是k-v格式的数据。时间复杂度是O(1),常数阶,而mysql引擎的底层实现是B+TREE,时间复杂度是O(logn)是对数阶的。Redis会比Mysql快一点点。
  2. Mysql数据存储是存储在表中,查找数据时要先对表进行全局扫描或根据索引查找,这涉及到磁盘的查找,磁盘查找如果是单点查找可能会快点,但是顺序查找就比较慢。而redis不用这么麻烦,本身就是存储在内存中,会根据数据在内存的位置直接取出。
  3. Redis是单线程的多路复用IO,单线程避免了线程切换的开销,而多路复用IO避免了IO等待的开销,在多核处理器下提高处理器的使用效率可以对数据进行分区,然后每个处理器处理不同的数据。
  • 池化技术,减少频繁创建数据库连接的性能损耗。

    每次进行数据库操作之前,先建立连接然后再进行数据库操作,最后释放连接。这个过程涉及到网络通信的延时,频繁创建连接对象和销毁对象的性能开销等,当请求量较大时,这块带来的性能影响非常大。

数据存储

磁盘数据访问优化

对于磁盘的操作,无非就是读和写。

比如对于做交易系统的场景来说,一般会设计到对账文件的解析和写入。而对于磁盘的操作,优化方式无非就是

  • 磁盘的页缓存,可以借助缓存 I/O ,充分利用系统缓存,降低实际 I/O 的次数。

  • 顺序读写,可以用追加写代替随机写,减少寻址开销,加快 I/O 写的速度。

  • SSD代替HDD,固态硬盘的I/O效率远远高于机械硬盘。

  • 在需要频繁读写同一块磁盘空间时,可以用 mmap (内存映射,)代替 read/write,减少内存的拷贝次数

  • 在需要同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘,即可以用 fsync() 取代 O_SYNC

合理利用内存

充分利用内存缓存,把一些经常访问的数据和对象保存在内存中,这样可以避免重复加载或者避免数据库访问带来的性能损耗。

调用远程服务

远程服务调用,影响到IO性能的因素有。

  • 远程调用等待返回结果的阻塞
    • 异步通信
  • 网络通信的耗时
    • 内网通信
    • 增加网络带宽
  • 远程服务通信的稳定性

异步化架构

微服务中的逻辑复杂处理时间长的情况,在高并发量下,导致服务线程消耗尽,不能再创建线程处理请求。对这种情况的优化,除了在程序上不断调优(数据库调优,算法调优,缓存等等),可以考虑在架构上做些调整,先返回结果给客户端,让用户可以继续使用客户端的其他操作,再把服务端的复杂逻辑处理模块做异步化处理。这种异步化处理的方式适合于客户端对处理结果不敏感不要求实时的情况,比如群发邮件、群发消息等。

异步化设计的解决方案: 多线程、MQ。

应用服务的拆分

除了上述的手段之外,业务系统往微服务化拆分也非常有必要,原因是:

  • 随着业务的发展,应用程序本身的复杂度会不断增加,同样会产生熵增现象。
  • 业务系统的功能越来越多,参与开发迭代的人员也越多,多个人维护一个非常庞大的项目,很容易出现问题。
  • 单个应用系统很难实现横向扩容,并且由于服务器资源有限,导致所有的请求都集中请求到某个服务器节点,造成资源消耗过大,使得系统不稳定
  • 测试、部署成本越来越高
  • …..

其实,最终要的是,单个应用在性能上的瓶颈很难突破,也就是说如果我们要支持18000QPS,单个服务节点肯定无法支撑,所以服务拆分的好处,就是可以利用多个计算机阶段组成一个大规模的分布式计算网络,通过网络通信的方式完成一整套业务逻辑。

img

如何拆分服务

如何拆分服务,这个问题看起来简单,很多同学会说,直接按照业务拆分啊。

但是实际在实施的时候,会发现拆分存在一些边界性问题,比如有些数据模型可以存在A模块,也可以存在B模块,这个时候怎么划分呢?另外,服务拆分的粒度应该怎么划分?

一般来说,服务的拆分是按照业务来实现的,然后基于DDD来指导微服务的边界划分。领域驱动就是一套方法论,通过领域驱动设计方法论来定义领域模型,从而确定业务边界和应用边界,保证业务模型和代码模型的一致性。不管是DDD还是微服务,都要遵循软件设计的基本原则:高内聚低耦合。服务内部高内聚,服务之间低耦合,实际上一个领域服务对应了一个功能集合,这些功能一定是有一些共性的。比如,订单服务,那么创建订单、修改订单、查询订单列表,领域的边界越清晰,功能也就越内聚,服务之间的耦合性也就越低。

服务拆分还需要根据当前技术团队和公司所处的状态来进行。

如果是初创团队,不需要过分的追求微服务,否则会导致业务逻辑过于分散,技术架构太过负载,再加上团队的基础设施还不够完善,导致整个交付的时间拉长,对公司的发展来说会造成较大的影响。所以在做服务拆分的时候还需要考虑几个因素。

  • 当前公司业务所处领域的市场性质,如果是市场较为敏感的项目,前期应该是先出来东西,然后再去迭代和优化。
  • 开发团队的成熟度,团队技术能否能够承接。
  • 基础能力是否足够,比如Devops、运维、测试自动化等基础能力。 团队是否有能力来支撑大量服务实例运行带来的运维复杂度,是否可以做好服务的监控。
  • 测试团队的执行效率,如果测试团队不能支持自动化测试、自动回归、压力测试等手段来提高测试效率,那必然会带来测试工作量的大幅度提升从而导致项目上线周期延期

如果是针对一个老的系统进行改造,那可能涉及到的风险和问题更多,所以要开始着手改动之前,需要考虑几个步骤:拆分前准备阶段,设计拆分改造方案,实施拆分计划

  • 拆分之前,先梳理好当前的整个架构,以及各个模块的依赖关系,还有接口

    准备阶段主要是梳理清楚了依赖关系和接口,就可以思考如何来拆,第一刀切在哪儿里,即能达到快速把一个复杂单体系统变成两个更小系统的目标,又能对系统的现有业务影响最小。要尽量避免构建出一个分布式的单体应用,一个包含了一大堆互相之间紧耦合的服务,却又必须部署在一起的所谓分布式系统。没分析清楚就强行拆,可能就一不小心剪断了大动脉,立马搞出来一个 A 类大故障,后患无穷。

  • 不同阶段拆分要点不同,每个阶段的关注点要聚焦

    拆分本身可以分成三个阶段,核心业务和非业务部分的拆分、核心业务的调整设计、核心业务内部的拆分。

    • 第一阶段将核心业务瘦身,把非核心的部分切开,减少需要处理的系统大小;

    • 第二阶段。重新按照微服务设计核心业务部分;

    • 第三阶段把核心业务部分重构设计落地。

    拆分的方式也有三个:代码拆分、部署拆分、数据拆分。

另外,每个阶段需要聚焦到一两个具体的目标,否则目标太多反而很难把一件事儿做通透。例如某个系统的微服务拆分,制定了如下的几个目标:

  1. 性能指标(吞吐和延迟):核心交易吞吐提升一倍以上(TPS:1000->10000),A 业务延迟降低一半(Latency:250ms->125ms),B 业务延迟降低一半(Latency:70ms->35ms)。
  2. 稳定性指标(可用性,故障恢复时间):可用性>=99.99%,A 类故障恢复时间<=15 分钟,季度次数<=1 次。
  3. 质量指标:编写完善的产品需求文档、设计文档、部署运维文档,核心交易部分代码 90%以上单测覆盖率和 100%的自动化测试用例和场景覆盖,实现可持续的性能测试基准环境和长期持续性能优化机制。
  4. 扩展性指标:完成代码、部署、运行时和数据多个维度的合理拆分,对于核心系统重构后的各块业务和交易模块、以及对应的各个数据存储,都可以随时通过增加机器资源实现伸缩扩展。
  5. 可维护性指标:建立全面完善的监控指标、特别是全链路的实时性能指标数据,覆盖所有关键业务和状态,缩短监控报警响应处置时间,配合运维团队实现容量规划和管理,出现问题时可以在一分钟内拉起系统或者回滚到上一个可用版本(启动时间<=1 分钟)。
  6. 易用性指标,通过重构实现新的 API 接口既合理又简单,极大的满足各个层面用户的使用和需要,客户满意度持续上升。
  7. 业务支持指标:对于新的业务需求功能开发,在保障质量的前提下,开发效率提升一倍,开发资源和周期降低一半。

当然,不要期望一次性完成所有目标,每一个阶段可以选择一个两个优先级高的目标进行执行。

img

微服务化架构带来的问题

微服务架构首先是一个分布式的架构,其次我们要暴露和提供业务服务能力,然后我们需要考虑围绕这些业务能力的各种非功能性的能力。这些分散在各处的服务本身需要被管理起来,并且对服务的调用方透明,这样就有了服务的注册发现的功能需求。

同样地,每个服务可能部署了多台机器多个实例,所以,我们需要有路由和寻址的能力,做负载均衡,提升系统的扩展能力。有了这么多对外提供的不同服务接口,我们一样需要有一种机制对他们进行统一的接入控制,并把一些非业务的策略做到这个接入层,比如权限相关的,这就是服务网关。同时我们发现随着业务的发展和一些特定的运营活动,比如秒杀大促,流量会出现十倍以上的激增,这时候我们就需要考虑系统容量,服务间的强弱依赖关系,做服务降级、熔断,系统过载保护等措施。

以上这些由于微服务带来的复杂性,导致了应用配置、业务配置,都被散落到各处,所以分布式配置中心的需求也出现了。最后,系统分散部署以后,所有的调用都跨了进程,我们还需要有能在线上做链路跟踪,性能监控的一套技术,来协助我们时刻了解系统内部的状态和指标,让我们能够随时对系统进行分析和干预。

image-20210624133950124

整体架构图

基于上述从微观到宏观的整体分析,我们基本上能够设计出一个整体的架构图。

  • 接入层,外部请求到内部系统之间的关口,所有请求都必须经过api 网关。

  • 应用层,也叫聚合层,为相关业务提供聚合接口,它会调用中台服务进行组装。

  • 中台服务,也是业务服务层,以业务为纬度提供业务相关的接口。中台的本质是为整个架构提供复用的能力,比如评论系统,在咕泡云课堂和Gper社区都需要,那么这个时候评论系统为了设计得更加可复用性,就不能耦合云课堂或者Gper社区定制化的需求,那么作为设计评论中台的人,就不需要做非常深度的思考,如何提供一种针对不同场景都能复用的能力。

    你会发现,当这个服务做到机制的时候,就变成了一个baas服务。

    服务商客户(开发者)提供整合云后端的服务,如提供文件存储、数据存储、推送服务、身份验证服务等功能,以帮助开发者快速开发应用。

image-20210624152616146

了解什么是高并发

总结一下什么是高并发。

高并发并没有一个具体的定义,高并发主要是形容突发流量较高的场景。

如果面试的过程中,或者在实际工作中,你们领导或者面试官问你一个如何设计承接千万级流量的系统时,你应该要按照我说的方法去进行逐一分析。

  • 一定要形成可以量化的数据指标,比如QPS、DAU、总用户数、TPS、访问峰值
  • 针对这些数据情况,开始去设计整个架构方案
  • 接着落地执行

高并发中的宏观指标

一个满足高并发系统,不是一味追求高性能,至少需要满足三个宏观层面的目标:

  • 高性能,性能体现了系统的并行处理能力,在有限的硬件投入下,提高性能意味着节省成本。同时,性能也反映了用户体验,响应时间分别是 100 毫秒和 1 秒,给用户的感受是完全不同的。
  • 高可用,表示系统可以正常服务的时间。一个全年不停机、无故障;另一个隔三差五出现上事故、宕机,用户肯定选择前者。另外,如果系统只能做到 90%可用,也会大大拖累业务。
  • 高扩展,表示系统的扩展能力,流量高峰时能否在短时间内完成扩容,更平稳地承接峰值流量,比如双 11 活动、明星离婚等热点事件。

image-20210624211728937

微观指标

性能指标

通过性能指标可以度量目前存在的性能问题,同时作为性能优化的评估依据。一般来说,会采用一段时间内的接口响应时间作为指标。

1、平均响应时间:最常用,但是缺陷很明显,对于慢请求不敏感。比如 1 万次请求,其中 9900 次是 1ms,100 次是 100ms,则平均响应时间为 1.99ms,虽然平均耗时仅增加了 0.99ms,但是 1%请求的响应时间已经增加了 100 倍。

2、TP90、TP99 等分位值:将响应时间按照从小到大排序,TP90 表示排在第 90 分位的响应时间, 分位值越大,对慢请求越敏感。

img

可用性指标

高可用性是指系统具有较高的无故障运行能力,可用性 = 平均故障时间 / 系统总运行时间,一般使用几个 9 来描述系统的可用性。

对于高并发系统来说,最基本的要求是:保证 3 个 9 或者 4 个 9。原因很简单,如果你只能做到 2 个 9,意味着有 1%的故障时间,像一些大公司每年动辄千亿以上的 GMV 或者收入,1%就是 10 亿级别的业务影响。

可扩展性指标

面对突发流量,不可能临时改造架构,最快的方式就是增加机器来线性提高系统的处理能力。

对于业务集群或者基础组件来说,扩展性 = 性能提升比例 / 机器增加比例,理想的扩展能力是:资源增加几倍,性能提升几倍。通常来说,扩展能力要维持在 70%以上。

但是从高并发系统的整体架构角度来看,扩展的目标不仅仅是把服务设计成无状态就行了,因为当流量增加 10 倍,业务服务可以快速扩容 10 倍,但是数据库可能就成为了新的瓶颈。

像 MySQL 这种有状态的存储服务通常是扩展的技术难点,如果架构上没提前做好规划(垂直和水平拆分),就会涉及到大量数据的迁移。

因此,高扩展性需要考虑:服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等,当并发达到某一个量级后,上述每个因素都可能成为扩展的瓶颈点。

实践方案

通用设计方法

纵向扩展(scale-up)

它的目标是提升单机的处理能力,方案又包括:

1、提升单机的硬件性能:通过增加内存、CPU 核数、存储容量、或者将磁盘升级成 SSD 等堆硬件的方式来提升。

2、提升单机的软件性能:使用缓存减少 IO 次数,使用并发或者异步的方式增加吞吐量。

横向扩展(scale-out)

因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下 2 个方向:

1、做好分层架构:这是横向扩展的提前,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。

2、各层进行水平扩展:无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。

高性能实践方案

1、集群部署,通过负载均衡减轻单机压力。

2、多级缓存,包括静态数据使用 CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点 key、缓存穿透、缓存并发、数据一致性等问题的处理。

3、分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。

4、考虑 NoSQL 数据库的使用,比如 HBase、TiDB 等,但是团队必须熟悉这些组件,且有较强的运维能力。

5、异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。

6、限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx 接入层的限流、服务端的限流。

7、对流量进行削峰填谷,通过 MQ 承接流量。

8、并发处理,通过多线程将串行逻辑并行化。

9、预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。

10、缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。

11、减少 IO 次数,比如数据库和缓存的批量读写、RPC 的批量接口支持、或者通过冗余数据的方式干掉 RPC 调用。

12、减少 IO 时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存 key 的大小、压缩缓存 value 等。

13、程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For 循环的计算逻辑优化,或者采用更高效的算法。

14、各种池化技术的使用和池大小的设置,包括 HTTP 请求池、线程池(考虑 CPU 密集型还是 IO 密集型设置核心参数)、数据库和 Redis 连接池等。

15、JVM 优化,包括新生代和老年代的大小、GC 算法的选择等,尽可能减少 GC 频率和耗时。

16、锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。

高可用实践方案

1、对等节点的故障转移,Nginx 和服务治理框架均支持一个节点失败后访问另一个节点。

2、非对等节点的故障转移,通过心跳检测并实施主备切换(比如 redis 的哨兵模式或者集群模式、MySQL 的主从切换等)。

3、接口层面的超时设置、重试策略和幂等设计。

4、降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。

5、限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。

6、MQ 场景的消息可靠性保证,包括 producer 端的重试机制、broker 侧的持久化、consumer 端的 ack 机制等。

7、灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。

8、监控报警:全方位的监控体系,包括最基础的 CPU、内存、磁盘、网络的监控,以及 Web 服务器、JVM、数据库、各类中间件的监控和业务指标的监控。

9、灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。

高可用的方案主要从冗余、取舍、系统运维 3 个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。

高扩展的实践方案

1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。

2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。

3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求去拆(比如 To C 和 To B,APP 和 H5)。