关于java:建设高并发系统的一些经验总结

41次阅读

共计 12800 个字符,预计需要花费 32 分钟才能阅读完成。

对于如何建设高并发零碎的一些经验总结,仅供参考,欢送交换。

  • 前言
  • 基础设施
  • 数据库
  • 架构
  • 利用
  • 标准
  • 总结

前言

来到饿了么有一段时间了,在饿了么期间从 2017 年开始接手运单零碎的开发和保护工作,从最早的日均百万单,到来到时的日均千万单,业务的疾速倒退再加上外卖业务的特点是业务量集中在午顶峰和晚顶峰两个高峰期,所以高峰期并发申请量也是水涨船高,每天都要面对高并发的挑战。拿运单零碎来举例,日常午顶峰外围查问服务的 QPS 在 20 万以上,Redis 集群的 QPS 更是在百万级,数据库 QPS 也在 10 万级以上,TPS 在 2 万以上。

在这么大的流量下,次要的工作也是以围绕如何建设零碎的稳定性和晋升容量开展,上面次要从基础设施、数据库、架构、利用、标准这几方面谈谈如何建设高并发的零碎。以下都是我集体这几年的一些经验总结,架构没有银弹,因而也称不上是最佳实际,仅供参考

基础设施

在分层架构中,最底层的就是基础设施。根底设置一般来说蕴含了物理服务器、IDC、部署形式等等。就像一个金字塔,基础设施就是金字塔的底座,只有底座稳固了,下层能力稳固。

异地多活

饿了么在 2017 年的时候做了异地多活,整个计划是由公司基础架构部门主导的,业务方须要配合多活的计划做相应的革新,也是通过这个我的项目也是学习到了对于多活架构的一些常识。

多活能够分为同城多活、异地多活等等,实现形式也有多种,比方阿里应用的单元化计划,饿了么应用的是多核心的计划,对于饿了么多活的实现能够参考 https://zhuanlan.zhihu.com/p/…。过后做多活的次要出发点是保证系统的高可用性,防止单 IDC 的单点故障问题,同时因为每个机房的流量都变成了总流量的 1 /N,也变相晋升了零碎容量,在高并发的场景下能够抗更多的流量。下图是饿了么多活的整体架构,来源于下面饿了么多活实现的分享文章中。

数据库

数据库是整个零碎最重要的组成部分之一,在高并发的场景下很大一部分工作是围绕数据库开展的,次要须要解决的问题是如何晋升数据库容量。

读写拆散

互联网的大部分业务特点是读多写少,因而应用读写拆散架构能够无效升高数据库的负载,晋升零碎容量和稳定性。外围思路是由主库承当写流量,从库承当读流量,且在读写拆散架构中个别都是 1 主多从的配置,通过多个从库来分担高并发的查问流量。比方当初有 1 万 QPS 的以及 1K 的 TPS,假如在 1 主 5 从的配置下,主库只承当 1K 的 TPS,每个从库承当 2K 的 QPS,这种量级对 DB 来说是齐全可承受的,相比读写拆散革新前,DB 的压力显著小了许多。

这种模式的益处是简略,简直没有代码革新老本或只有大量的代码革新老本,只须要配置数据库主从即可。毛病也是同样显著的:

主从提早

MySQL 默认的主从复制是异步的,如果在主库插入数据后马上去从库查问,可能会产生查不到的状况。失常状况下主从复制会存在毫秒级的提早,在 DB 负载较高的状况下可能存在秒级提早甚至更久,但即便是毫秒级的提早,对于实时性要求较高的业务来说也是不可漠视的。所以在一些要害的查问场景,咱们会将查问申请绑定到主库来防止主从提早的问题。对于主从提早的优化网上也有不少的文章分享,这里就不再赘述。

从库的数量是无限的

一个主库能挂载的从库数量是很无限的,没方法做到有限的程度扩大。从库越多,尽管实践上能接受的 QPS 就越高,然而从库过多会导致主库主从复制 IO 压力更大,造成更高的提早,从而影响业务,所以一般来说只会在主库后挂载无限的几个从库。

无奈解决 TPS 高的问题

从库尽管能解决 QPS 高的问题,但没方法解决 TPS 高的问题,所有的写申请只有主库能解决,一旦 TPS 过高,DB 仍然有宕机的危险。

分库分表

当读写拆散不能满足业务须要时,就须要思考应用分库分表模式了。当确定要对数据库做优化时,应该优先思考应用读写拆散的模式,只有在读写拆散的模式曾经没方法接受业务的流量时,咱们才思考分库分表的模式。分库分表模式的最终成果是把单库单表变成多库多表,如下图。

首先来说下分表,分表能够分为垂直拆分和程度拆分。垂直拆分就是按业务维度拆,假如原来有张订单表有 100 个字段,能够按不同的业务纬度拆成多张表,比方用户信息一张表,领取信息一张表等等,这样每张表的字段相对来说都不会特地多。

程度拆分是把一张表拆分成 N 张表,比方把 1 张订单表,拆成 512 张订单子表。

在实践中能够只做程度拆分或垂直拆分,也能够同时做程度及垂直拆分。

说完了分表,那分库是什么呢?分库就是把原来都在一个 DB 实例中的表,按肯定的规定拆分到 N 个 DB 实例中,每个 DB 实例都会有一个 master,相当于是多 mater 的架构,同时为了保障高可用性,每个 master 至多要有 1 个 slave,来保障 master 宕机时 slave 能及时顶上,同时也能保证数据不失落。拆分完后每个 DB 实例中只会有局部表。

因为是多 master 的架构,分库分表除了蕴含读写拆散模式的所有长处外,还能够解决读写拆散架构中无奈解决的 TPS 过高的问题,同时分库分表实践上是能够有限横向扩大的,也解决了读写拆散架构下从库数量无限的问题。当然在理论的工程实际中个别须要提前预估好容量,因为数据库是有状态的,如果发现容量有余再扩容是十分麻烦的,应该尽量避免。

在分库分表的模式下能够通过不启用查问从库的形式来防止主从提早的问题,也就是说读写都在主库,因为在分库后,每个 master 上的流量只占总流量的 1 /N,大部分状况下能扛住业务的流量,从库只作为 master 的备份,在主库宕机时执行主从切换顶替 master 提供服务应用。
说完了益处,再来说说分库分表会带来的问题,次要有以下几点:

革新老本高

分库分表个别须要中间件的反对,常见的模式有两种:客户端模式和代理模式。客户端模式会通过在服务上援用 client 包的形式来实现分库分表的逻辑,比拟有代表的是开源的 sharding JDBC。
代理模式是指所有的服务不是间接连贯 MySQL,而是通过连贯代理,代理再连贯到 MySQL 的形式,代理须要实现 MySQL 相干的协定。

两种模式各有优劣势,代理模式相对来说会更简单,然而因为多了一层代理,在代理这层能做更多的事件,也比拟不便降级,而且通过代理连贯数据库,也能保障数据库的连接数稳固。应用客户端模式益处是相对来说实现比较简单,无两头代理,实践上性能也会更好,然而在降级的时候须要业务方革新代码,因而降级会比代理模式更艰难。在饿了么应用的是代理模式,饿了么有对立的数据库拜访中间件——DAL,负责代理所有的数据库,实现分库分表逻辑,对业务放弃通明。

事务问题

在业务中咱们会应用事务来解决多个数据库操作,通过事务的 4 个个性——一致性、原子性、持久性、隔离性来保障业务流程的正确性。在分库分表后,会将一张表拆分成 N 张子表,这 N 张子表可能又在不同的 DB 实例中,因而尽管逻辑上看起来还是一张表,但其实曾经不在一个 DB 实例中了,这就造成了无奈应用事务的问题。

最常见的就是在批量操作中,在分库分表前咱们能够同时把对多个订单的操作放在一个事务中,但在分库分表后就不能这么干了,因为不同的订单可能属于不同用户,假如咱们按用户来分库分表,那么不同用户的订单表位于不同的 DB 实例中,多个 DB 实例显然没方法应用一个事务来解决,这就须要借助一些其余的伎俩来解决这个问题。在分库分表后应该要尽量避免这种跨 DB 实例的操作,如果肯定要这么应用,优先思考应用弥补等形式保证数据最终一致性,如果肯定要强一致性,罕用的计划是通过分布式事务的形式。

无奈反对多维度查问

分库分表个别只能按 1 - 2 个纬度来分,这个维度就是所谓的sharding key。罕用的维度有用户、商户等维度,如果按用户的维度来分表,最简略的实现形式就是按用户 ID 来取模定位到在哪个分库哪个分表,这也就意味着之后所有的读写申请都必须带上用户 ID,但在理论业务中不可避免的会存在多个维度的查问,不肯定所有的查问都会有用户 ID,这就须要咱们对系统进行革新。

为了能在分库分表后也反对多维度查问,罕用的解决方案有两种,第一种是引入一张索引表,这张索引表是没有分库分表的,还是以按用户 ID 分库分表为例,索引表上记录各种维度与用户 ID 之间的映射关系,申请须要先通过其余维度查问索引表失去用户 ID,再通过用户 ID 查问分库分表后的表。这样,一来须要多一次 IO,二来索引表因为是没有分库分表的,很容易成为零碎瓶颈。

第二种计划是通过引入 NoSQL 的形式,比拟常见的组合是 ES+MySQL,或者HBase+MySQL 的组合等,这种计划实质上还是通过 NoSQL 来充当第一种计划中的索引表的角色,然而绝对于间接应用索引表来说,NoSQL具备更好的程度扩展性和伸缩性,只有设计切当,个别不容易成为零碎的瓶颈。

数据迁徙

分库分表个别是须要进行数据迁徙的,通过数据迁徙将原有的单表数据迁徙到分库分表后的库表中。数据迁徙的计划常见的有两种,第一种是停机迁徙,顾名思义,这种形式简略粗犷,益处是能一步到位,迁徙周期短,且能保证数据一致性,害处是对业务有损,某些要害业务可能无奈承受几分钟或更久的停机迁徙带来的业务损失。

另外一种计划是双写,这次要是针对新增的增量数据,存量数据能够间接进行数据同步,对于如何进行双写迁徙网上曾经有很多分享了,这里也就不赘述,核心思想是同时写老库和新库。双写的益处是对业务的影响小,但也更简单,迁徙周期更长,容易呈现数据不统一问题,须要有残缺的数据一致性保障计划反对。

小结

读写拆散模式和分库分表模式举荐优先应用读写拆散模式,只有在不满业务需要的状况才才思考应用分库分表模式。起因是分库分表模式尽管能显著晋升数据库的容量,但会减少零碎复杂性,而且因为只能反对多数的几个维度读写,从某种意义上来说对业务零碎也是一种限度,因而在设计分库分表计划的时候须要联合具体业务场景,更全面的思考。

架构

在高并发零碎建设中,架构同样也是十分重要的,这里分享缓存、音讯队列、资源隔离等等模式的一些教训。

缓存

在高并发的零碎架构中缓存是最无效的利器,能够说没有之一。缓存的最大作用是能够晋升零碎性能,爱护后端存储不被大流量打垮,减少零碎的伸缩性。缓存的概念最早来源于 CPU 中,为了进步 CPU 的处理速度,引入了 L1、L2、L3 三级高速缓存来减速拜访,当初零碎中应用的缓存也是借鉴了 CPU 中缓存的做法。

缓存是个十分大的话题,能够独自写一本书也毫不夸大,在这里总结一下我集体在运单零碎设计和实现缓存的时候遇到的一些问题和解决方案。缓存次要分为本地缓存和分布式缓存,本地缓存如 Guava CacheEHCache 等,分布式缓存如 RedisMemcached 等,在运单零碎中应用的次要以分布式缓存为主。

如何保障缓存与数据库的数据一致性

首先是如何保障缓存与数据库的数据一致性问题,根本在应用缓存的时候都会遇到这个问题,同时这也是个高频的面试题。在我负责的运单零碎中应用缓存这个问题就更突出了,首先运单是会频繁更新的,并且运单系统对数据一致性的要求是十分高的,根本不太能承受数据不统一,所以不能简略的通过设置一个过期工夫的形式来生效缓存。

对于缓存读写的模式举荐浏览耗子叔的文章:https://coolshell.cn/articles…,外面总结了几种罕用的读写缓存的套路,我在运单零碎中的缓存读写模式也是参考了文章中的 Write through 模式,通过伪代码的形式大略是这样的:

lock(运单 ID) {
    //...
      
    // 删除缓存
      deleteCache();
    // 更新 DB
      updateDB();
    // 重建缓存
      reloadCache()}

既然是 Write through 模式,那对缓存的更新就是在写申请中进行的。首先为了避免并发问题,写申请都须要加分布式锁,锁的粒度是以运单 ID 为 key,在执行完业务逻辑后,先删除缓存,再更新 DB,最初再重建缓存,这些操作都是同步进行的,在读申请中先查问缓存,如果缓存命中则间接返回,如果缓存不命中则查问 DB,而后间接返回,也就是说在读申请中不会操作缓存,这种形式把缓存操作都收敛在写申请中,且写申请是加锁的,无效避免了读写并发导致的写入脏缓存数据的问题。

缓存数据构造的设计

缓存要防止大 key 和热 key 的问题。举个例子,如果应用 redis 中的 hash 数据结构,那就比一般字符串类型的 key 更容易有大 key 和热 key 问题,所以如果不是非要应用 hash 的某些特定操作,能够思考把 hash 拆散成一个一个独自的 key/value 对,应用一般的 string 类型的 key 存储,这样能够避免 hash 元素过多造成的大 key 问题,同时也能够防止单 hash key 过热的问题。

读写性能

对于读写性能次要有两点须要思考,首先是写性能,影响写性能的次要因素是 key/value 的数据大小,比较简单的场景能够应用 JSON 的序列化形式存储,然而在高并发场景下应用 JSON 不能很好的满足性能要求,而且也比拟占存储空间,比拟常见的代替计划有 protobufthrift 等等,对于这些序列化 / 反序列化计划网上也有一些性能比照,参考 https://code.google.com/p/thr…。

读性能的次要影响因素是每次读取的数据包的大小。在实践中举荐应用 redis pipeline+ 批量操作的形式,比如说如果是字符串类型的 key,那就是pipeline+mget 的形式,假如一次 mget10 个 key,100 个mget 为一批 pipeline,那一次网络 IO 就能够查问 1000 个缓存 key,当然这里具体一批的数量要看缓存 key 的数据包大小,没有对立的值。

适当冗余

适当冗余的意思是说咱们在设计对外的业务查问接口的时候,能够适当的做一些冗余。这个教训是来自于过后咱们在设计运单零碎对外查问接口的时候,为了谋求通用性,将接口的返回值设计成一个大对象,把运单上的所有字段都放在了这个大对象外面间接对外裸露了,这样的益处是不须要针对不同的查问方开发不同的接口了,反正字段就在接口里了,要什么就本人取。

这么做一开始是没问题的,但到咱们须要对查问接口减少缓存的时候发现,因为所有业务方都通过这一个接口查问运单数据,咱们没方法晓得他们的业务场景,也就不晓得他们对接口数据一致性的要求是怎么样的,比方是否承受短暂的数据一致性,而且咱们也不晓得他们具体应用了接口中的哪些字段,接口中有些字段是不会变的,有些字段是会频繁变更的,针对不同的更新频率其实能够采纳不同的缓存设计方案,但很惋惜,因为咱们设计接口的时候过于谋求通用性,在做缓存优化的时候就十分麻烦,只能按最坏的状况打算,也就是所有业务方都对数据一致性要求很高来设计方案,导致最初的计划在数据一致性这块花了大量的精力。

如果咱们一开始设计对外查问接口的时候能做一些适当的冗余,辨别不同的业务场景,尽管这样势必会造成有些接口的性能是相似的,但在加缓存的时候就能对症下药,针对不同的业务场景设计不同的计划,比方要害的流程要重视数据一种的保障,而非关键场景则容许数据短暂的不统一来升高缓存实现的老本。同时在接口中最好也能将会更新的字段和不会更新的字段做肯定的辨别,这样在设计缓存计划的时候,针对不会更新的字段,能够设置一个较长的过期工夫,而会更新的字段,则只能设置较短的过期工夫,并且须要做好缓存更新的方案设计来保证数据一致性。

音讯队列

在高并发零碎的架构中,音讯队列(MQ)是必不可少的,当大流量来长期,咱们通过音讯队列的异步解决和削峰填谷的个性来减少零碎的伸缩性,避免大流量打垮零碎,此外,应用音讯队列还能使零碎间达到充沛解耦的目标。

音讯队列的外围模型由生产者(Producer)、消费者(Consumer)和消息中间件(Broker)组成。目前业界罕用的开源解决方案有 ActiveMQRabbitMQKafkaRocketMQ 和近年比拟火的Pulsar,对于各种消息中间件的比照能够参考文章:https://zhuanlan.zhihu.com/p/…。

应用音讯队列后,能够将本来同步解决的申请,改为通过生产 MQ 音讯异步生产,这样能够缩小零碎解决的压力,减少零碎吞吐量,对于如何应用音讯队列有许多的分享的文章,这里我的教训是在思考应用音讯队列时要联合具体的业务场景来决定是否引入音讯队列,因为应用音讯队列后其实是减少了零碎的复杂性的,原来通过一个同步申请就能搞定的事件,须要引入额定的依赖,并且生产音讯是异步的,异步天生要比同步更简单,还须要额定思考音讯乱序、提早、失落等问题,如何解决这些问题又是一个很大话题,天下没有收费的午餐,做任何架构设计是一个取舍的过程,须要认真思考得失后再做决定。

服务治理

服务治理是个很大的话题,能够独自拿出来说,在这里我也把它归到架构中。服务治理的定义是

个别指独立于业务逻辑之外,给零碎提供一些牢靠运行的零碎保障措施。

常见的保障措施包含服务的注册发现、可观测性(监控)、限流、超时、熔断等等,在微服务架构中个别通过服务治理框架来实现服务治理,开源的解决方案包含 Spring CloudDubbo 等。

在高并发的零碎中,服务治理是十分重要的一块内容,相比于缓存、数据库这些大块的内容,服务治理更多的是细节,比方对接口的超时设置到底是 1 秒还是 3 秒,怎么样做监控等等,有句话叫细节决定成败,有时候就是因为一个接口的超时设置不合理而导致大面积故障的事件,我已经也是见识过的,特地是在高并发的零碎中,肯定要留神这些细节。

超时

对于超时的准则是:所有皆有超时。不论是 RPC 调用、Redis 操作、生产音讯 / 发送音讯、DB 操作等等,都要有超时。之前在饿了么就遇到过依赖了内部组件,然而没有设置正当的超时,当内部依赖呈现故障时,把服务所有的线程全副阻塞导致资源耗尽,无奈响应内部申请,从而引发故障,这些都是“血”的教训。

除了要设置超时,还要设置正当的超时也同样重要,像下面提到的故障即便设置了超时,然而超时太久的话仍然会因为内部依赖故障而把服务拖垮。
如何设置一个正当的超时是很有考究的,能够从是否要害业务场景、是否强依赖等方面去思考,没有什么通用的规定,须要联合具体的业务场景来看。比方在一些 C 端展现接口中,设置 1 秒的超时仿佛没什么问题,但在一些对性能十分敏感的场景下 1 秒可能就太久了,总之,须要联合具体的业务场景去设置,但无论怎么样,准则还是那句话:所有皆有超时。

监控

监控就是零碎的眼睛,没有监控的零碎就像一个黑盒,从内部齐全不晓得外面的运行状况,咱们就无奈治理和运维这个零碎。所以,监控零碎是十分重要的。
零碎的可观测性次要蕴含三个局部——loggingtracingmetrics。之前在饿了么次要是应用的自研的监控零碎,不得不说真的是十分的好用,具体的介绍能够参考:https://mp.weixin.qq.com/s/1V…。
在建设高并发零碎时,咱们肯定要有欠缺的监控体系,包含零碎层面的监控(CPU、内存、网络等)、利用层面的监控(JVM、性能等)、业务层面的监控(各种业务曲线等)等,除了监控还要有欠缺的报警,因为不可能有人 24 小时盯着监控,一旦有什么危险肯定要报警进去,及时染指,防备危险于未然。

熔断

在微服务框架中个别都会内置熔断的个性,熔断的目标是为了在上游服务出故障时爱护本身服务。熔断的实现个别会有一个断路器(Crit Breaker),断路器会依据接口成功率 / 次数等规定来判断是否触发熔断,断路器会管制熔断的状态在敞开 - 关上 - 半关上中流转。熔断的复原会通过工夫窗口的机制,先经验半关上状态,如果成功率达到阈值则敞开熔断状态。

如果没有什么非凡需要的话在业务零碎中个别是不须要针对熔断做什么的,框架会主动关上和敞开熔断开关。可能须要留神的点是要防止 有效的熔断,什么是有效的熔断呢?在以前碰到过一个故障,是服务的提供方在一些失常的业务校验中抛出了不合理的异样(比方零碎异样),导致接口熔断影响失常业务。所以咱们在接口中抛出异样或者返回异样码的时候肯定要辨别业务和零碎异样,一般来说业务异样是不须要熔断的,如果是业务异样而抛出了零碎异样,会导致被熔断,失常的业务流程就会受到影响。

降级

降级不是一种具体的技术,更像是一种架构设计的方法论,是一种丢卒保帅的策略,核心思想就是在异样的状况下限度本身的一些能力,来保障外围性能的可用性。降级的实现形式有许多,比方通过配置、开关、限流等等形式。降级分为被动降级和被动降级。

在电商零碎大促的时候咱们会把一些非核心的性能临时敞开,来保障外围性能的稳定性,或者当上游服务呈现故障且短时间内无奈复原时,为了保障本身服务的稳定性而把上游服务降级,这些都是被动降级。

被动降级指的是,比方调用了上游一个接口,然而接口超时了,这个时候咱们为了让业务流程能继续执行上来,个别会抉择在代码中 catch 异样,打印一条谬误日志,而后继续执行业务逻辑,这种降级是被动的。

在高并发的零碎中做好降级是十分重要的。举个例子来说,当申请量很大的时候不免有超时,如果每次超时业务流程都中断了,那么会大大影响失常业务,正当的做法是咱们应该认真辨别强弱依赖,对于弱依赖采纳被动降级的降级形式,而对于强依赖是不能进行降级的。降级与熔断相似,也是对本身服务的爱护,防止当内部依赖故障时拖垮本身服务,所以,咱们要做好充沛的降级预案。

限流

对于限流的文章和介绍网上也有许多,具体的技术实现能够参考网上文章。对于限流我集体的教训是在设置限流前肯定要通过压测等形式充沛做好零碎容量的预估,不要拍脑袋,限流一般来说是有损用户体验的,应该作为一种兜底伎俩,而不是惯例伎俩。

资源隔离

资源隔离有各种类型,物理层面的服务器资源、中间件资源,代码层面的线程池、连接池,这些都能够做隔离。这里介绍的资源隔离次要是利用部署层面的,比方 Set 化 等等。上文提到的异地多活也算是 Set 化的一种。

我在饿了么负责运单零碎的期间也做过一些相似的资源隔离上的优化。背景是过后出遇到过一个线上故障,起因是某服务部署的服务器都在一个集群,没有按流量划分各自独自的集群,导致要害业务和非关键业务流量相互影响而导致的故障。因而,在这个故障后我也是决定对服务器做按集群隔离部署,隔离的维度次要是按业务场景辨别,分为要害集群、次要害集群和非关键集群三类,这样能防止要害和非关键业务相互影响。

小结

在架构方面,我集体也不是业余的架构师,也是始终在学习相干技术和方法论,下面介绍的很多技术和架构设计模式都是在工作中边学习边实际。如果说非要总结一点教训心得的话,我感觉是重视细节。集体认为架构不止高大上的方法论,技术细节也是同样重要的,正所谓细节决定成败,有时候遗记设置一个小小的超时,可能导致整个零碎的解体。

利用

在高并发的零碎中,在利用层面能做的优化也是十分多的,这部分次要分享对于弥补、幂等、异步化、预热等这几方面的优化。

弥补

在微服务架构下,会按各业务畛域拆分不同的服务,服务与服务之前通过 RPC 申请或 MQ 音讯的形式来交互,在分布式环境下必然会存在调用失败的状况,特地是在高并发的零碎中,因为服务器负载更高,产生失败的概率会更大,因而弥补就更为重要。罕用的弥补模式有两种:定时工作模式 或者 音讯队列模式

定时工作模式

定时工作弥补的模式个别是须要配合数据库的,弥补时会起一个定时工作,定时工作执行的时候会扫描数据库中是否有须要弥补的数据,如果有则执行弥补逻辑,这种计划的益处是因为数据都长久化在数据库中了,相对来说比较稳定,不容易出问题,有余的中央是因为依赖了数据库,在数据量较大的时候,会对数据库造成肯定的压力,而且定时工作是周期性执行的,因而个别弥补会有肯定的提早。

音讯队列模式

音讯队列弥补的模式个别会应用音讯队列中提早音讯的个性。如果解决失败,则发送一个提早音讯,提早 N 分钟 / 秒 / 小时后再重试,这种计划的益处是比拟轻量级,除了 MQ 外没有内部依赖,实现也比较简单,相对来说也更实时,有余的中央是因为没有长久化到数据库中,有失落数据的危险,不够稳固。
因而,我集体的教训是在要害链路的弥补中应用定时工作的模式,非关键链路中的弥补能够应用音讯队列的模式。除此之外,在弥补的时候还有一个特地重要的点就是 幂等性 设计。

幂等

幂等操作的特点是 其任意屡次执行所产生的影响均与一次执行的影响雷同 ,体现在业务上就是用户对于同一操作发动的一次申请或者屡次申请的后果是统一的,不会因为发动屡次而产生副作用。
在分布式系统中产生零碎谬误是在劫难逃的,当产生谬误时,会应用重试、弥补等伎俩来进步容错性,在高并发的零碎中产生零碎谬误的概率就更高了,所以这时候接口幂等就十分重要了,能够避免屡次申请而引起的副作用。

幂等的实现须要通过一个惟一的业务 ID 或者 Token 来实现,个别的流程是先在 DB 或者缓存中查问惟一的业务 ID 或者 token 是否存在,且状态是否为已解决,如果是则示意是反复申请,那么咱们须要幂等解决,即不做任何操作,间接返回即可。

在做幂等性设计的时候须要留神的是并不是所有的场景都要做幂等,比方用户反复转账、提现等等,因为幂等会让内部零碎的感知是调用胜利了,并没有阻塞后续流程,但其实咱们零碎外部是没有做任何操作的,相似下面提到的场景,会让用户误以为操作已胜利。所以说要认真辨别须要幂等的业务场景和不能幂等的业务场景,对于不能幂等的业务场景还是须要抛出业务异样或者返回特定的异样码来阻塞后续流程,避免引发业务问题。

异步化

上文提到的音讯队列也是一种异步化,除了依赖内部中间件,在利用内咱们也能够通过线程池、协程的形式做异步化。

对于线程池的实现原理,拿 Java 中线程池的模型来举例,外围是通过工作队列和复用线程的形式相配合来实现的,网上对于这些分享的文章也很多。在应用线程池或者协程等相似技术的时候,我集体的教训是有以下两点是须要特地留神的:

要害业务场景须要配合弥补

咱们都晓得,不论是线程池也好,协程也好,都是基于内存的,如果服务器意外宕机或者重启,内存中的数据是会失落的,而且线程池在资源有余的时候也会回绝工作,所以在一些要害的业务场景中如果应用了线程池等相似的技术,须要配合弥补一块应用,防止内存中数据失落造成的业务影响。在我保护的运单零碎中有一个要害的业务场景是入单,简略来说就是接管上游申请,在零碎中生成运单,这是整个物流履约流量的入口,是特地要害的一个业务场景。

因为生成运单的整个流程比拟长,依赖内部接口有 10 几个,所以过后为了谋求高性能和吞吐率,设计成了异步的模式,也就是在线程池中解决,同时为了避免数据失落,也做了欠缺的弥补措施,在饿了么这几年工夫入单这块根本没有出过问题,并且因为采纳了异步的设计,性能十分好,那咱们具体是怎么做的呢。

总的流程是在接管到上游的申请后,第一步是将所有的申请参数落库,这一步是十分要害的,如果这一步失败,那整个申请就失败了。在胜利落库后,封装一个 Task 提交到线程池中,而后间接对上游返回胜利。后续的所有解决都是在线程池中进行的,此外,还有一个定时工作会定时弥补,弥补的数据源就是在第一步中落库的数据,每一条落库的记录会有一个 flag 字段来示意解决状态,如果发现是未解决或者解决失败,则通过定时工作再触发弥补逻辑,弥补胜利后再将 flag 字段更新为解决胜利。

做好监控

在微服务中像 RPC 接口调用、MQ 音讯生产,包含中间件、基础设施等的监控,这些根本都会针对性的做欠缺的监控,然而相似像线程池个别是没有现成监控的,须要应用方自行实现上报打点监控,这点很容易被脱漏。咱们晓得线程池的实现是会有内存队列的,而咱们也个别会对内存队列设置一个最大值,如果超出了最大值可能会抛弃工作,这时候如果没有监控是发现不了相似的问题的,所以,应用线程池肯定要做好监控。
那么线程池有哪些能够监控的指标呢,按我的教训来说,个别会上报线程池的 沉闷线程数 以及 工作队列的工作个数,这两个指标我认为是最重要的,其余的指标就见仁见智了,能够联合具体业务场景来选择性上报。

预热

Warm Up。当零碎长期处于低水位的状况下,流量忽然减少时,间接把零碎拉升到高水位可能霎时把零碎压垮。通过 ” 冷启动 ”,让通过的流量迟缓减少,在肯定工夫内逐步减少到阈值下限,给冷零碎一个预热的工夫,防止冷零碎被压垮。

参考网上的定义,说白了,就是如果服务始终在低水位,这时候忽然来一波高并发的流量,可能会一下子把零碎打垮。零碎的预热个别有 JVM 预热、缓存预热、DB 预热等,通过预热的形式让零碎先“热”起来,为高并发流量的到来做好筹备。
预热理论利用的场景有很多,比方在电商的大促到来前,咱们能够把一些热点的商品提前加载到缓存中,避免大流量冲击 DB,再比方 Java 服务因为 JVM 的动静类加载机制,能够在启动后对服务做一波压测,把类提前加载到内存中,同时还有能够提前触发 JIT 编译、Code cache 等等益处。

还有一种预热的思路是利用业务的个性做一些 预加载,比方咱们在保护运单零碎的时候做过这样一个优化,在一个失常的外卖业务流程中是用户下单后到用户交易系统生成订单,而后经验领取 -> 商家接单 -> 申请配送这样一个流程,所以说从用户下单到申请配送这之间有秒级到分钟级的时间差,咱们能够通过感知用户下单的动作,利用这时间差来提前加载一些数据。

这样在理论申请到来的时候只须要到缓存中获取即可,这对于一些比拟耗时的操作晋升是十分大的,之前咱们利用这种形式能晋升接口性能 50% 以上。当然有个点须要留神的就是如果对于一些可能会变更的数据,可能就不适宜预热,因为预热后数据存在缓存中,前面就不会再去申请接口了,这样会导致数据不统一,这是须要特地留神的。

小结

在做高并发零碎设计的时候咱们总是会特地关注架构、基础设施等等,这些确实十分重要,但其实在利用层面能做的优化也是十分多的,而且老本会比架构、基础设施的架构优化低很多。很多时候在利用层面做的优化须要联合具体的业务场景,利用特定的业务场景去做出正当的设计,比方缓存、异步化,咱们就须要思考哪些业务场景能缓存,能异步化,哪些就是须要同步或者查问 DB,肯定要联合业务能力做出更好的设计和优化。

标准

这是对于建设高并发零碎教训分享的最初一个局部了,但我认为标准的重要性一点都不比基础设施、架构、数据库、利用低,可能还比这些都更重要。依据二八定律,在软件的整个生命周期中,咱们花了 20% 工夫发明了零碎,但要花 80% 的工夫来保护零碎,这也让我想起来一句话,有人说代码次要是给人读的,顺便给机器运行,其实都是体现了可维护性的重要性。

在咱们应用了高大上的架构、做了各种优化之后,零碎的确有了一个比拟好的设计,但问题是怎么在后续的保护过程中避免架构腐化呢,这时候就须要标准了。

标准包含代码标准、变更标准、设计规范等等,当然这里我不会介绍如何去设计这些标准,我更想说的是咱们肯定要器重标准,只有在有了标准之后,零碎的可维护性能力有保障。依据破窗实践,通过各种标准咱们尽量不让零碎有第一扇破窗产生。

总结

说了这么多对于设计、优化的办法,最初想再分享两点。

第一点就是有句驰名的话——“过早优化是万恶之源”,集体十分认同,我做的所有这些设计和优化,都是在零碎遇到理论的问题或瓶颈的时候才做的,切忌不要脱离实际场景过早优化,不然很可能做无用功甚至得失相当。

第二点是在设计的时候要遵循KISS 准则,也就是 Keep it simple, stupid。简略意味着维护性更高,更不容易出问题,正所谓大道至简,或者就是这个情理。

以上这些都是我在饿了么工作期间保护高并发零碎的一些经验总结,鉴于篇幅和集体技术水平起因,可能有些局部没有介绍的特地具体和深刻,算是抛砖引玉吧。如果有什么说的不对的中央也欢送指出,同时也欢送交换和探讨。

正文完
 0