作者:万金源链接:https://www.zhihu.com/question/421237964/answer/1626541405来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

高并发系统设计学习(基础篇)

01. 高并发系统:它的通用设计方法是什么
高并发 实际技能 - 图1
02. 架构分层:我们为什么一定要这么做?
高并发 实际技能 - 图2
03. 系统设计目标(一):如何提升系统性能?
高并发 实际技能 - 图3
04. 系统设计目标(二):系统怎样做到高可用?
高并发 实际技能 - 图4
05. 系统设计目标(三):如何让系统易于扩展?
高并发 实际技能 - 图5

高并发系统设计学习(数据库篇)

07. 池化技术:如何减少频繁创建数据库连接的性能损耗?
高并发 实际技能 - 图6
08. 数据库优化方案(一):查询请求增加时,如何做主从分离?
高并发 实际技能 - 图7
09. 数据库优化方案(二):写入数据量增加时,如何实现分库分表?
高并发 实际技能 - 图8
10. 发号器:如何保证分库分表后ID的全局唯一性?
高并发 实际技能 - 图9
11. NoSQL:在高并发场景下,数据库和NoSQL如何做到互补?
高并发 实际技能 - 图10

高并发系统设计学习(缓存篇)

12. 缓存:数据库成为瓶颈后,动态数据的查询要如何加速?
13. 缓存的使用姿势(一):如何选择缓存的读写策略?
14. 缓存的使用姿势(二):缓存如何做到高可用?
15. 缓存的使用姿势(三):缓存穿透了怎么办?
16. CDN:静态资源如何加速?
高并发 实际技能 - 图11

高并发系统设计学习(消息队列篇)

17. 消息队列:秒杀时如何处理每秒上万次的下单请求?
18. 消息投递:如何保证消息仅仅被消费一次?
19. 消息队列:如何降低消息队列系统中消息的延迟?
高并发 实际技能 - 图12

高并发系统设计学习(分布式服务篇)

21. 系统架构:每秒1万次请求的系统要做服务化拆分吗?
22. 微服务架构:微服务化后,系统架构要如何改造?
23. RPC框架:10万QPS下如何实现毫秒级的服务调用?
24. 注册中心:分布式系统如何寻址?
25. 分布式Trace:横跨几十个分布式组件的慢请求要如何排查?
26. 负载均衡:怎样提升系统的横向扩展能力?
27. API网关:系统的门面要如何做呢?
28. 多机房部署:跨地域的分布式系统如何做?
29. Service Mesh:如何屏蔽服务化系统的服务治理细节?
高并发 实际技能 - 图13

高并发系统设计学习(维护篇)

30. 给系统加上眼睛:服务端监控要怎么做?
31. 应用性能管理:用户的使用体验应该如何监控?
32. 压力测试:怎样设计全链路压力测试平台?
33. 配置管理:成千上万的配置项要如何管理?
34. 降级熔断:如何屏蔽非核心系统故障的影响?
35. 流量控制:高并发系统中我们如何操纵流量?
高并发 实际技能 - 图14

高并发系统设计学习(实战篇)

37. 计数系统设计(一):面对海量数据的计数器要如何做?
38. 计数系统设计(二):50万QPS下如何设计未读数系统?
39. 信息流设计(一):通用信息流系统的推模式要如何做?
40. 信息流设计(二):通用信息流系统的拉模式要如何做?
高并发 实际技能 - 图15

作者:编程指北链接:https://www.zhihu.com/question/421237964/answer/1810636619来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这题我必须回答下!总结了十几条后台高并发、高性能的常见解决方式,当时面试BAT,都被面试官说答得很好,思路非常正确。
先上本文思维导图:
高并发 实际技能 - 图16
另外说下,如果是BAT这种核心部门,比如微信、支付宝核销交易链路、红包这些问你的话,还可以好好回答一下,实在不会也可以给面试官说没有场景,进去后再学习。
反而是一些小公司,平时QPS就个位数,面试就拿红包、秒杀来唬人的,你直接反杀一波,别被他带着走,不然两个都不懂高并发的互相在这吹逼也没啥意思。
技术面试上,除了算法和网络、操作系统这种基础之外,还有一类系统设计和优化的问题。这类问题需要你有一个全局的技术视野,以及熟悉一些常用的系统优化方法论,也就是工程上的一些 Best Practice,而不至于自己临时拍脑袋瞎设计。
在互联网公司,经常面临一个“三高”问题:

  • 高并发
  • 高性能
  • 高可用

这篇文章将总结一下后台服务器开发中有哪些常用的解决“三高”问题的方法和思想。
希望这些知识,能够给你一丝启发和帮助,助力你收割 各大公司 Offer~
先上本文思维导图:
高并发 实际技能 - 图17
话不多说,直接上干货:(记得双击点个赞哟~
本文来源: 公众号「编程指北」,分享编程、CS、面试干货,强烈推荐关注!原文:掌握了这些知识,他拿到了 BAT Offer

一、缓存

什么是缓存?看看维基百科怎么说:
In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.
在计算机中,缓存是存储数据的硬件或软件组件,以便可以更快地满足将来对该数据的请求。 存储在缓存中的数据可能是之前计算结果,也可能是存储在其他位置的数据副本
缓存本质来说是使用空间换时间的思想,它在计算机世界中无处不在, 比如 CPU 就自带 L1、L2、L3 Cache,这个一般应用开发可能关注较少。但是在一些实时系统、大规模计算模拟、图像处理等追求极致性能的领域,就特别注重编写缓存友好的代码。
什么是缓存友好?简单来说,就是代码在访问数据的时候,尽量使用缓存命中率高的方式。这个后面可以单独写一篇 CPU 缓存系统以及如何编写缓存友好代码的文章。

1.1 缓存为什么有效?

缓存之所以能够大幅提高系统的性能,关键在于数据的访问具有局部性,也就是二八定律:「百分之八十的数据访问是集中在 20% 的数据上」。这部分数据也被叫做热点数据。
缓存一般使用内存作为存储,内存读写速度快于磁盘,但容量有限,十分宝贵,不可能将所有数据都缓存起来。
如果应用访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。每次访问都会回源到数据库查询,那么反而会降低数据访问效率。
//在公众号【编程指北】后台回复【pdf】会有面试资料合集,各种面经汇总 博客 刷题笔记

1.2 缓存分类

  • 1. 本地缓存: 使用进程内成员变量或者静态变量,适合简单的场景,不需要考虑缓存一致性、过期时间、清空策略等问题。 可以直接使用语言标准库内的容器来做存储。例如:

高并发 实际技能 - 图18

  • 2. 分布式缓存: 当缓存的数据量增大以后,单机不足以承载缓存服务时,就要考虑对缓存服务做水平扩展,引入缓存集群。 将数据分片后分散存储在不同机器中,如何决定每个数据分片存放在哪台机器呢?一般是采用一致性 Hash 算法,它能够保证在缓存集群动态调整,不断增加或者减少机器后,客户端访问时依然能够根据 key 访问到数据。 一致性 Hash 算法也是值得用一篇文章来讲的,如果暂时还不懂的话可以去搜一下。 常用的组件有 MemcacheRedis Cluster 等,第二个是在高性能内存存储 Redis 的基础上,提供分布式存储的解决方案。

    1.3 缓存使用指南

    1. 适合缓存的场景:

  • 读多写少: 比如电商里的商品详情页面,访问频率很高,但是一般写入只在店家上架商品和修改信息的时候发生。如果把热点商品的信息缓存起来,这将拦截掉很多对数据库的访问,提高系统整体的吞吐量。 因为一般数据库的 QPS 由于有「ACID」约束、并且数据是持久化在硬盘的,所以比 Redis 这类基于内存的 NoSQL 存储低不少。常常是一个系统的瓶颈,如果我们把大部分的查询都在 Redis 缓存中命中了,那么系统整体的 QPS 也就上去了。

  • 计算耗时大,且实时性不高: 比如王者荣耀里的全区排行榜,一般一周更新一次,并且计算的数据量也比较大,所以计算后缓存起来,请求排行榜直接从缓存中取出,就不用实时计算了。

2. 不适合缓存的场景

  • 写多读少,频繁更新。
  • 对数据一致性要求严格: 因为缓存会有更新策略,所以很难做到和数据库实时同步。
  • 数据访问完全随机: 因为这样会导致缓存的命中率极低。

    1.4 缓存更新的策略

    如何更新缓存其实已经有总结得非常好的「最佳实践」,我们按照套路来,大概率不会犯错。
    主要分为两类 Cache-AsideCache-As-SoR。 SoR 即「System Of Record,记录系统」,表示数据源,一般就是指数据库。

    1、Cache-Aside:

    高并发 实际技能 - 图19
    这应该是最容易想到的模式了,获取数据时先从缓存读,如果 cache hit 则直接返回,没命中就从数据源获取,然后更新缓存。
    写数据的时候则先更新数据源,然后设置缓存失效,下一次获取数据的时候必然 cache miss,然后触发回源
    直接看伪代码:
    高并发 实际技能 - 图20
    可以看到这种方式对于缓存的使用者是不透明的,需要使用者手动维护缓存。

    2、Cache-As-SoR:

    高并发 实际技能 - 图21
    从字面上来看,就是把 Cache 当作 SoR,也就是数据源,所以一切读写操作都是针对 Cache 的,由 Cache 内部自己维护和数据源的一致性。
    这样对于使用者来说就和直接操作 SoR 没有区别了,完全感知不到 Cache 的存在。
    CPU 内部的 L1、L2、L3 Cache 就是这种方式,作为数据的使用方应用程序,是完全感知不到在内存和我们之间还存在几层的 Cache,但是我们之前又提到编写 “缓存友好”的代码,不是透明的吗?这是不是冲突呢?
    其实不然,缓存友好是指我们通过学习了解缓存内部实现、更新策略之后,通过调整数据访问顺序提高缓存的命中率。
    Cache-As-SoR 又分为以下三种方式:

  • Read Through:这种方式和 Cache-Aside 非常相似,都是在查询时发生 cache miss 去更新缓存,但是区别在于 Cache-Aside 需要调用方手动更新缓存,而 Cache-As-SoR 则是由缓存内部实现自己负责,对应用层透明。

  • Write Through: 直写式,就是在将数据写入缓存的同时,缓存也去更新后面的数据源,并且必须等到数据源被更新成功后才可返回。这样保证了缓存和数据库里的数据一致性
  • Write Back:回写式,数据写入缓存即可返回,缓存内部会异步的去更新数据源,这样好处是写操作特别快,因为只需要更新缓存。并且缓存内部可以合并对相同数据项的多次更新,但是带来的问题就是数据不一致,可能发生写丢失。

    二、预处理和延后处理

    预先延后,这其实是一个事物的两面,不管是预先还是延后核心思想都是将本来该在实时链路上处理的事情剥离,要么提前要么延后处理。降低实时链路的路径长度, 这样能有效提高系统性能。

    2.1 预处理

    举个我们团队实际中遇到的问题:
    前两个月支付宝联合杭州市政府发放消费劵,但是要求只有杭州市常驻居民才能领取,那么需要在抢卷请求进入后台的时候就判断一下用户是否是杭州常驻居民。
    而判断用户是否是常驻居民这个是另外一个微服务接口,如果直接实时的去调用那个接口,短时的高并发很有可能把这个服务也拖挂,最终导致整个系统不可用,并且 RPC 本身也是比较耗时的,所以就考虑在这里进行优化。
    那么该怎么做呢?很简单的一个思路,提前将杭州所有常驻居民的 user_id 存到缓存中, 比如可以直接存到 Redis。大概就是千万量级,这样,当请求到来的时候我们直接通过缓存可以快速判断是否来自杭州常驻居民。如果不是则直接在这里返回前端。
    这里通过预先处理减少了实时链路上的 RPC 调用,既减少了系统的外部依赖,也极大的提高了系统的吞吐量。
    预处理在 CPU 和操作系统中也广泛使用,比如 CPU 基于历史访存信息,将内存中的指令和数据预取到 Cache 中,这样可以大大提高Cache 命中率。 还比如在 Linux 文件系统中,预读算法会预测即将访问的 page,然后批量加载比当前读请求更多的数据缓存在 page cache 中,这样当下次读请求到来时可以直接从 cache 中返回,大大减少了访问磁盘的时间。

    2.2 延后处理

    还是支付宝,上栗子:
    高并发 实际技能 - 图22
    这是支付宝春节集五福活动开奖当晚,不过,作为非酋的我一般是不屑于参与这种活动的。
    大家发现没有,这类活动中奖奖金一般会显示 「稍后到账」,为什么呢?那当然是到账这个操作不简单!
    到账即转账,A 账户给 B 账户转钱,A 减钱, B 就必须要同时加上钱,也就是说不能 A 减了钱但 B 没有加上,这就会导致资金损失。资金安全是支付业务的生命线,这可不行。
    这两个动作必须一起成功或是一起都不成功,不能只成功一半,这是保证数据一致性。 保证两个操作同时成功或者失败就需要用到事务
    如果去实时的做到账,那么大概率数据库的 TPS(每秒处理的事务数) 会是瓶颈。通过产品提示,将到账操作延后处理,解决了数据库 TPS 瓶颈。
    延后处理还有一个非常著名的例子,COW(Copy On Write,写时复制)。 Linux 创建进程的系统调用 fork,fork 产生的子进程只会创建虚拟地址空间,而不会分配真正的物理内存,子进程共享父进程的物理空间,只有当某个进程需要写入的时候,才会真正分配物理页,拷贝该物理页,通过 COW 减少了很多不必要的数据拷贝。

    三、池化

    后台开发过程中你一定离不开各种 「池子」: 内存池、连接池、线程池、对象池……
    内存、连接、线程这些都是资源,创建线程、分配内存、数据库连接这些操作都有一个特征, 那就是创建和销毁过程都会涉及到很多系统调用或者网络 IO。 每次都在请求中去申请创建这些资源,就会增加请求处理耗时,但是如果我们用一个 容器(池) 把它们保存起来,下次需要的时候,直接拿出来使用,避免重复创建和销毁浪费的时间。

    3.1 内存池

    在 C/C++ 中,经常使用 malloc、new 等 API 动态申请内存。由于申请的内存块大小不一,如果频繁的申请、释放会导致大量的内存碎片,并且这些 API 底层依赖系统调用,会有额外的开销。
    内存池就是在使用内存前,先向系统申请一块空间留做备用,使用者需要内池时向内存池申请,用完后还回来。
    内存池的思想非常简单,实现却不简单,难点在于以下几点:

  • 如何快速分配内存

  • 降低内存碎片率
  • 维护内存池所需的额外空间尽量少

如果不考虑效率,我们完全可以将内存分为不同大小的块,然后用链表连接起来,分配的时候找到大小最合适的返回,释放的时候直接添加进链表。如:
高并发 实际技能 - 图23
当然这只是玩具级别的实现,业界有性能非常好的实现了,我们可以直接拿来学习和使用。
比如 Google 的 「tcmalloc」 和 Facebook 的 「jemalloc」。
限于篇幅我们不在这里详细讲解它们的实现原理,如果感兴趣可以搜来看看,也推荐去看看被誉为神书的 CSAPP(《深入理解计算机系统》)第 10 章,那里也讲到了动态内存分配算法。

3.2 线程池

线程是干嘛的?线程就是我们程序执行的实体。在服务器开发领域,我们经常会为每个请求分配一个线程去处理,但是线程的创建销毁、调度都会带来额外的开销,线程太多也会导致系统整体性能下降。在这种场景下,我们通常会提前创建若干个线程,通过线程池来进行管理。当请求到来时,只需从线程池选一个线程去执行处理任务即可。
线程池常常和队列一起使用来实现任务调度,主线程收到请求后将创建对应的任务,然后放到队列里,线程池中的工作线程等待队列里的任务。
线程池实现上一般有四个核心组成部分:

  • 管理器(Manager): 用于创建并管理线程池。
  • 工作线程(Worker): 执行任务的线程。
  • 任务接口(Task): 每个具体的任务必须实现任务接口,工作线程将调用该接口来完成具体的任务。
  • 任务队列(TaskQueue): 存放还未执行的任务。

高并发 实际技能 - 图24
线程池在 C、C++ 中没有具体的实现,需要应用开发者手动实现上诉几个部分。
在 Java 中 「ThreadPoolExecutor」 类就是线程池的实现。后续我也会写文章分析 C++ 如何写一个简单的线程池以及 Java 中线程池是如何实现的。

3.3 连接池

顾名思义,连接池是创建和管理连接的。
大家最熟悉的莫过于数据库连接池,这里我们简单分析下如果不用数据库连接池,一次 SQL 查询请求会经过哪些步骤:

  1. 和 MySQL server 建立 TCP 连接:
  • 三次握手
  1. MySQL 权限认证:
  • Server 向 Client 发送 密钥
  • Client 使用密钥加密用户名、密码等信息,将加密后的报文发送给 Server
  • Server 根据 Client 请求包,验证是否是合法用户,然后给 Client 发送认证结果
  1. Client 发送 SQL 语句
  2. Server 返回语句执行结果
  3. MySQL 关闭
  4. TCP 连接断开
  • 四次挥手

可以看出不使用连接池的话,为了执行一条 SQL,会花很多时间在安全认证、网络IO上。
如果使用连接池,执行一条 SQL 就省去了建立连接和断开连接所需的额外开销。
还能想起哪里用到了连接池的思想吗?我认为 HTTP 长链接也算一个变相的链接池,虽然它本质上只有一个连接,但是思想却和连接池不谋而合,都是为了复用同一个连接发送多个 HTTP 请求,避免建立和断开连接的开销。
池化实际上是预处理和延后处理的一种应用场景,通过池子将各类资源的创建提前和销毁延后。

四、同步变异步

对于处理耗时的任务,如果采用同步的方式,那么会增加任务耗时,降低系统并发度。
可以通过将同步任务变为异步进行优化。
举个例子,比如我们去 KFC 点餐,遇到排队的人很多,当点完餐后,大多情况下我们会隔几分钟就去问好了没,反复去问了好几次才拿到,在这期间我们也没法干活了,这时候我们是这样的:
高并发 实际技能 - 图25
这个就叫同步轮训, 这样效率显然太低了。
服务员被问烦了,就在点完餐后给我们一个号码牌,每次准备好了就会在服务台叫号,这样我们就可以在被叫到的时候再去取餐,中途可以继续干自己的事。
这就叫异步,在很多编程语言中有异步编程的库,比如 C++ std::future、Python asyncio 等,但是异步编程往往需要回调函数(Callback function),如果回调函数的层级太深,这就是回调地狱(Callback hell)。回调地狱如何优化又是一个庞大的话题。。。。
这个例子相当于函数调用的异步化,还有的是情况是处理流程异步化,这个会在接下来消息队列中讲到。

五、消息队列

高并发 实际技能 - 图26
这是一个非常简化的消息队列模型,上游生产者将消息通过队列发送给下游消费者。在这之间,消息队列可以发挥很多作用,比如:

5.1 服务解耦

有些服务被其它很多服务依赖,比如一个论坛网站,当用户成功发布一条帖子有一系列的流程要做,有积分服务计算积分,推送服务向发布者的粉丝推送一条消息….. 对于这类需求,常见的实现方式是直接调用:
高并发 实际技能 - 图27
这样如果需要新增一个数据分析的服务,那么又得改动发布服务,这违背了依赖倒置原则即上层服务不应该依赖下层服务,那么怎么办呢?
高并发 实际技能 - 图28
引入消息队列作为中间层,当帖子发布完成后,发送一个事件到消息队列里,而关心帖子发布成功这件事的下游服务就可以订阅这个事件,这样即使后续继续增加新的下游服务,只需要订阅该事件即可,完全不用改动发布服务,完成系统解耦。

5.2 异步处理

有些业务涉及到的处理流程非常多,但是很多步骤并不要求实时性。那么我们就可以通过消息队列异步处理。比如淘宝下单,一般包括了风控、锁库存、生成订单、短信/邮件通知等步骤。但是核心的就风控和锁库存, 只要风控和扣减库存成功,那么就可以返回结果通知用户成功下单了。后续的生成订单,短信通知都可以通过消息队列发送给下游服务异步处理。大大提高了系统响应速度。
这就是处理流程异步化。

5.3 流量削峰

一般像秒杀、抽奖、抢卷这种活动都伴随着短时间海量的请求, 一般超过后端的处理能力,那么我们就可以在接入层将请求放到消息队列里,后端根据自己的处理能力不断从队列里取出请求进行业务处理。
就像最近长江汛期,上游短时间大量的洪水汇聚直奔下游,但是通过三峡大坝将这些水缓存起来,然后匀速的向下游释放,起到了很好的削峰作用。
起到了平均流量的作用。

5.4 总结

消息队列的核心思想就是把同步的操作变成异步处理,异步处理会带来相应的好处,比如:

  • 服务解耦
  • 提高系统的并发度,将非核心操作异步处理,不会阻塞住主流程

但是软件开发没有银弹,所有的方案选择都是一种 trade-off。 同样,异步处理也不全是好处,也会导致一些问题:

  • 降低了数据一致性,从强一致性变为最终一致性
  • 有消息丢失的风险,比如宕机,需要有容灾机制

    六、批量处理

    在涉及到网络连接、IO等情况时,将操作批量进行处理能够有效提高系统的传输速率和吞吐量。
    在前后端通信中,通过合并一些频繁请求的小资源可以获得更快的加载速度。
    比如我们后台 RPC 框架,经常有更新数据的需求,而有的数据更新的接口往往只接受一项,这个时候我们往往会优化下更新接口,
    使其能够接受批量更新的请求,这样可以将批量的数据一次性发送,大大缩短网络 RPC 调用耗时。

    七、数据库

    我们常把后台开发调侃为「CRUD」,数据库在整个应用开发过程中的重要性不言而喻。
    而且很多时候系统的瓶颈也往往处在数据库这里,慢的原因也有很多,比如可能是没用索引、没用对索引、读写锁冲突等等。
    那么如何使用数据才能又快又好呢?下面这几点需要重点关注:

    7.1 索引

    索引可能是我们平时在使用数据库过程中接触得最多的优化方式。索引好比图书馆里的书籍索引号,想象一下,如果我让你去一个没有书籍索引号的图书馆找《人生》这本书,你是什么样的感受?当然是怀疑人生,同理,你应该可以理解当你查询数据,却不用索引的时候数据库该有多崩溃了吧。
    数据库表的索引就像图书馆里的书籍索引号一样,可以提高我们检索数据的效率。索引能提高查找效率,可是你有没有想过为什么呢?这是因为索引一般而言是一个排序列表,排序意味着可以基于二分思想进行查找,将查询时间复杂度做到 O(log(N)),快速的支持等值查询和范围查询。
    二叉搜索树查询效率无疑是最高的,因为平均来说每次比较都能缩小一半的搜索范围,但是一般在数据库索引的实现上却会选择 B 树或 B+ 树而不用二叉搜索树,为什么呢?
    这就涉及到数据库的存储介质了,数据库的数据和索引都是存放在磁盘,并且是 InnoDB 引擎是以页为基本单位管理磁盘的,一页一般为 16 KB。AVL 或红黑树搜索效率虽然非常高,但是同样数据项,它也会比 B、B+ 树更高,高就意味着平均来说会访问更多的节点,即磁盘IO次数!
    根据 Google 工程师 Jeff Dean 的统计,访问内存数据耗时大概在 100 ns,访问磁盘则是 10,000,000 ns。
    所以表面上来看我们使用 B、B+ 树没有 二叉查找树效率高,但是实际上由于 B、B+ 树降低了树高,减少了磁盘 IO 次数,反而大大提升了速度。
    这也告诉我们,没有绝对的快和慢,系统分析要抓主要矛盾,先分析出决定系统瓶颈的到底是什么,然后才是针对瓶颈的优化。
    其实关于索引想写的也还有很多,但还是受限于篇幅,以后再单独写。
    先把我认为索引必知必会的知识列出来,大家可以查漏补缺:

  • 主键索引和普通索引,以及它们之间的区别

  • 最左前缀匹配原则
  • 索引下推
  • 覆盖索引、联合索引

    7.2 读写分离

    一般业务刚上线的时候,直接使用单机数据库就够了,但是随着用户量上来之后,系统就面临着大量的写操作和读操作,单机数据库处理能力有限,容易成为系统瓶颈。
    由于存在读写锁冲突,并且很多大型互联网业务往往读多写少,读操作会首先成为数据库瓶颈,我们希望消除读写锁冲突从而提升数据库整体的读写能力。
    那么就需要采用读写分离的数据库集群方式,一主多从,主库会同步数据到从库。写操作都到主库,读操作都去从库。
    高并发 实际技能 - 图29
    读写分离到之后就避免了读写锁争用,这里解释一下,什么叫读写锁争用:
    MySQL 中有两种锁:

  • 排它锁( X 锁): 事务 T 对数据 A 加上 X 锁时,只允许事务 T 读取和修改数据 A。

  • 共享锁( S 锁): 事务 T 对数据 A 加上 S 锁时,其他事务只能再对数据 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。

读写分离解决问题的同时也会带来新问题,比如主库和从库数据不一致
MySQL 的主从同步依赖于 binlog,binlog(二进制日志)是 MySQL Server 层维护的一种二进制日志,是独立于具体的存储引擎。它主要存储对数据库更新(insert、delete、update)的 SQL 语句,由于记录了完整的 SQL 更新信息,所以 binlog 是可以用来数据恢复和主从同步复制的。
从库从主库拉取 binlog 然后依次执行其中的 SQL 即可达到复制主库的目的,由于从库拉取 binlog 存在网络延迟等,所以主从数据存在延迟问题。
那么这里就要看业务是否允许短时间内的数据不一致,如果不能容忍,那么可以通过如果读从库没获取到数据就去主库读一次来解决。

7.3 分库分表

如果用户越来越多,写请求暴涨,对于上面的单 Master 节点肯定扛不住,那么该怎么办呢?多加几个 Master?不行,这样会带来更多的数据不一致的问题,增加系统的复杂度。那该怎么办?就只能对库表进行拆分了。
常见的拆分类型有垂直拆分和水平拆分。
考虑拼夕夕电商系统,一般有 订单表、用户表、支付表、商品表、商家表等, 最初这些表都在一个数据库里。 后来随着砍一刀带来的海量用户,拼夕夕后台扛不住了! 于是紧急从阿狸粑粑那里挖来了几个 P8、P9 大佬对系统进行重构。

  1. P9 大佬第一步先对数据库进行垂直分库, 根据业务关联性强弱,将它们分到不同的数据库, 比如订单库,商家库、支付库、用户库。
  2. 第二步是对一些大表进行垂直分表,将一个表按照字段分成多表,每个表存储其中一部分字段。 比如商品详情表可能最初包含了几十个字段,但是往往最多访问的是商品名称、价格、产地、图片、介绍等信息,所以我们将不常访问的字段单独拆成一个表。
  • 由于垂直分库已经按照业务关联切分到了最小粒度,数据量任然非常大,P9 大佬开始水平分库,比如可以把订单库分为订单1库、订单2库、订单3库…… 那么如何决定某个订单放在哪个订单库呢?可以考虑对主键通过哈希算法计算放在哪个库。
  • 分完库,单表数据量任然很大,查询起来非常慢,P9 大佬决定按日或者按月将订单分表,叫做日表、月表。

分库分表同时会带来一些问题,比如平时单库单表使用的主键自增特性将作废,因为某个分区库表生成的主键无法保证全局唯一,这就需要引入全局 UUID 服务了。
经过一番大刀阔斧的重构,拼夕夕恢复了往日的活力,大家又可以愉快的在上面互相砍一刀了。
(分库分表会引入很多问题,并没有一一介绍,这里只是为了讲解什么是分库分表)

八、具体技法

8.1 零拷贝

高性能的服务器应当避免不必要数据复制,特别是在用户空间和内核空间之间的数据复制。 比如 HTTP 静态服务器发送静态文件的时候,一般我们会这样写:
高并发 实际技能 - 图30
如果了解 Linux IO 的话就知道这个过程包含了内核空间和用户空间之间的多次拷贝:
高并发 实际技能 - 图31
内核空间和用户空间之间数据拷贝需要 CPU 亲自完成,但是对于这类数据不需要在用户空间进行处理的程序来说,这样的两次拷贝显然是浪费。什么叫 「不需要在用户空间进行处理」?
比如 FTP 或者 HTTP 静态服务器,它们的作用只是将文件从磁盘发送到网络,不需要在中途对数据进行编解码之类的计算操作。
如果能够直接将数据在内核缓存之间移动,那么除了减少拷贝次数以外,还能避免内核态和用户态之间的上下文切换。
而这正是零拷贝(Zero copy)干的事,主要就是利用各种零拷贝技术,减少不必要的数据拷贝,将 CPU 从数据拷贝这样简单的任务解脱出来,让 CPU 专注于别的任务。
常用的零拷贝技术:

  1. mmap mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。

高并发 实际技能 - 图32

  1. sendfile sendfile 是 Linux2.1 版本提供的,数据不经过用户态,直接从页缓存拷贝到 socket 缓存,同时由于和用户态完全无关,就减少了一次上下文切换。 在 Linux 2.4 版本,对 sendfile 进行了优化,直接通过 DMA 将磁盘文件数据读取到 socket 缓存,真正实现了 ”0” 拷贝。前面 mmap 和 2.1 版本的 sendfile 实际上只是消除了用户空间和内核空间之间拷贝,而页缓存和 socket 缓存之间的拷贝依然存在。

    8.2 无锁化

    在多线程环境下,为了避免 竞态条件(race condition), 我们通常会采用加锁来进行并发控制,锁的代价也是比较高的,锁会导致上线文切换,甚至被挂起直到锁被释放。
    基于硬件提供的原子操作 CAS(Compare And Swap) 实现一些高性能无锁的数据结构,比如无锁队列,可以在保证并发安全的情况下,提供更高的性能。
    首先需要理解什么是 CAS,CAS 有三个操作数,内存里当前值M,预期值 E,修改的新值 N,CAS 的语义就是:
    如果当前值等于预期值,则将内存修改为新值,否则不做任何操作
    用 C 语言来表达就是:
    高并发 实际技能 - 图33
    注意,上面 CAS 函数实际上是一条原子指令,那么是如何用的呢?
    假设我需要实现这样一个功能:
    对一个全局变量 global 在两个不同线程分别对它加 100 次,这里多线程访问一个全局变量存在 race condition,所以我们需要采用线程同步操作,下面我分别用锁和CAS的方法来实现这个功能。
    高并发 实际技能 - 图34
    通过使用原子操作大大降低了锁冲突的可能性,提高了程序的性能。
    除了 CAS,还有一些硬件原子指令:
  • Fetch-And-Add,对变量原子性 + 1
  • Test-And-Set,这是各种锁算法的核心,在 AT&T/GNU 汇编语法下,叫 xchg 指令,我会单独写一篇如何使用 xchg 实现各种锁。

    8.3 序列化与反序列化

    先看看维基百科怎么定义的序列化:
    In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, across a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. For many complex objects, such as those that make extensive use of references, this process is not straightforward. Serialization of object-oriented objects does not include any of their associated methods with which they were previously linked.
    我相信你大概率没有看完上面的英文描述,其实我也不爱看英文资料,总觉得很慢,但是计算机领域一手的学习资料都是美帝那边的,所以没办法,必须逼自己去试着读一些英文的资料。
    实际上也没有那么难,熟悉常用的几百个专业名词,句子都是非常简单的一些从句。没看的话,再倒回去看看?
    这里我就不做翻译了,主要是水平太低,估计做到「信达雅」的信都很难。
    扯远了,还是回到序列化来。
    所有的编程一定是围绕数据展开的,而数据呈现形式往往是结构化的,比如结构体(Struct)、类(Class)。 但是当我们 通过网络、磁盘等传输、存储数据的时候却要求是二进制流。 比如 TCP 连接,它提供给上层应用的是面向连接的可靠字节流服务。那么如何将这些结构体和类转化为可存储和可传输的字节流呢?这就是序列化要干的事情,反之,从字节流如何恢复为结构化的数据就是反序列化。
    序列化解决了对象持久化和跨网络数据交换的问题。
    序列化一般按照序列化后的结果是否可读,可分为以下两类:

  • 文本类型: 如 JSON、XML,这些类型可读性非常好,是自解释的。也常常用在前后端数据交互上,因为接口调试,可读性高非常方便。但是缺点就是信息密度低,序列化后占用空间大。

  • 二进制类型 如 Protocol Buffer、Thrift等,这些类型采用二进制编码,数据组织得更加紧凑,信息密度高,占用空间小,但是带来的问题就是基本不可读。

还有 Java 、Go 这类语言内置了序列化方式,比如在 Java 里实现了 Serializable 接口即表示该对象可序列化。
说到这让我想起了大一写的的两个程序,一个是用刚 C 语言写的公交管理系统,当时需要将公交线路、站点信息持久化保存,当时的方案就是每个公交线路写在一行,用 “|”分割信息,比如:
5|6:00-22:00|大学城|南山站|北京站
123|6:30-23:00|南湖大道|茶山刘|世界
第一列就是线路编号、第二项是发车时间、后面就是途径的站点。是不是非常原始?实际上这也是一种序列化方式,只是效率很低,也不通用。而且存在一个问题就是如果信息中包含 “|”怎么办?当然是用转义。
第二个程序是用 Java 写的网络五子棋,当时需要通过网络传输表示棋子位置的对象,查了一圈最后发现只需要实现 Serializable 接口,自己什么都不用干,就能自己完成对象的序列化,然后通过网络传输后反序列化。当时哪懂得这就叫序列化,只觉得牛逼、神奇!
最后完成了一个可以网络五子棋,拉着隔壁室友一起玩。。。真的是成就感满满哈哈哈。
说来在编程方面,已经很久没有这样的成就感了。

总结

这篇文章主要是粗浅的介绍了一些系统设计、系统优化的套路和最佳实践。
不知道你发现没有,从缓存到消息队列、CAS……,很多看起来很牛逼的架构设计其实都来源于操作系统、体系结构。
所以我非常热衷学习一些底层的基础知识,这些看似古老的技术是经过时间洗礼留下来的好东西。现在很多的新技术、框架看似非常厉害,实则不少都是新瓶装旧酒,每几年又会被淘汰一批。

3

作者:不才陈某链接:https://www.zhihu.com/question/421237964/answer/1665962152来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
调优步骤:衡量系统现状、设定调优目标、寻找性能瓶颈、性能调优、衡量是否到达目标(如果未到达目标,需重新寻找性能瓶颈)、性能调优结束。

寻找性能瓶颈

性能瓶颈的表象:资源消耗过多、外部处理系统的性能不足、资源消耗不多但程序的响应速度却仍达不到要求。

资源消耗

CPU、文件IO、网络IO、内存
外部处理系统的性能不足:所调用的其他系统提供的功能或数据库操作的响应速度不够。
资源消耗不多但程序的响应速度却仍达不到要求:程序代码运行效率不够高、未充分使用资源、程序结构不合理。

CPU消耗分析

CPU主要用于中断、内核、用户进程的任务处理,优先级为中断>内核>用户进程。

上下文切换

每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或高优先级线程要执行时,将切换执行的线程。在切换时要存储目前线程的执行状态,并恢复要执行的线程的状态。
对于Java应用,典型的是在进行文件IO操作、网络IO操作、锁等待、线程Sleep时,当前线程会进入阻塞或休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU的使用。

运行队列

每个CPU核都维护一个可运行的线程队列。系统的load主要由CPU的运行队列来决定。
运行队列值越大,就意味着线程会要消耗越长的时间才能执行完成。

利用率

CPU在用户进程、内核、中断处理、IO等待、空闲,这五个部分使用百分比。

文件IO消耗分析

Linux在操作文件时,将数据放入文件缓存区,直到内存不够或系统要释放内存给用户进程使用。所以通常情况下只有写文件和第一次读取文件时会产生真正的文件IO。
对于Java应用,造成文件IO消耗高主要是多个线程需要进行大量内容写入(例如频繁的日志写入)的动作、磁盘设备本身的处理速度慢、文件系统慢、操作的文件本身已经很大。

网络IO消耗分析

对于分布式Java应用,网卡中断是不是均衡分配到各CPU(cat/proc/interrupts查看)。

内存消耗分析

-Xms和-Xmx设为相同的值,避免运行期JVM堆内存要不断申请内存
对于Java应用,内存的消耗主要在Java堆内存上,只有创建线程和使用Direct ByteBuffer才会操作JVM堆外的内存。
JVM内存消耗过多会导致GC执行频繁,CPU消耗增加,应用线程的执行速度严重下降,甚至造成OutOfMemoryError,最终导致Java进程退出。

JVM堆外的内存

swap的消耗、物理内存的消耗、JVM内存的消耗。

程序执行慢原因分析

锁竞争激烈:很多线程竞争互斥资源,但资源有限, 造成其他线程都处于等待状态。
未充分使用硬件资源:线程操作被串行化。
数据量增长:单表数据量太大(如1个亿)造成数据库读写速度大幅下降(操作此表)。

调优

JVM调优

最关键参数为:-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold

代大小调优

避免新生代大小设置过小、避免新生代大小设置过大、避免Survivor设置过小或过大、合理设置新生代存活周期。
-Xmn 调整新生代大小,新生代越大通常也意味着更多对象会在minor GC阶段被回收,但可能有可能造成旧生代大小,造成频繁触发Full GC,甚至是OutOfMemoryError。
-XX:SurvivorRatio调整Eden区与Survivor区的大小,Eden 区越大通常也意味着minor GC发生频率越低,但可能有可能造成Survivor区太小,导致对象minor GC后就直接进入旧生代,从而更频繁触发Full GC。

GC策略的调优

CMS GC多数动作是和应用并发进行的,确实可以减小GC动作给应用造成的暂停时间。对于Web应用非常需要一个对应用造成暂停时间短的GC,再加上Web应用 的瓶颈都不在CPU上,在G1还不够成熟的情况下,CMS GC是不错的选择。
(如果系统不是CPU密集型,且从新生代进入旧生代的大部分对象是可以回收的,那么采用CMS GC可以更好地在旧生代满之前完成对象的回收,更大程度降低Full GC发生的可能)
在调整了内存管理方面的参数后应通过-XX:PrintGCDetails、-XX:+PrintGCTimeStamps、 -XX:+PrintGCApplicationStoppedTime以及jstat或visualvm等方式观察调整后的GC状况。
出内存管理以外的其他方面的调优参数:-XX:CompileThreshold、-XX:+UseFastAccessorMethods、 -XX:+UseBaiasedLocking。

程序调优

CPU消耗严重的解决方法

CPU us高的解决方法

CPU us 高的原因主要是执行线程不需要任何挂起动作,且一直执行,导致CPU 没有机会去调度执行其他的线程。
调优方案: 增加Thread.sleep,以释放CPU 的执行权,降低CPU 的消耗。以损失单次执行性能为代价的,但由于其降低了CPU 的消耗,对于多线程的应用而言,反而提高了总体的平均性能。
(在实际的Java应用中类似场景, 对于这种场景最佳方式是改为采用wait/notify机制)
对于其他类似循环次数过多、正则、计算等造成CPU us过高的状况, 则需要结合业务调优。
对于GC频繁,则需要通过JVM调优或程序调优,降低GC的执行次数。

CPU sy高的解决方法

CPU sy 高的原因主要是线程的运行状态要经常切换,对于这种情况,常见的一种优化方法是减少线程数。
调优方案: 将线程数降低
这种调优过后有可能会造成CPU us过高,所以合理设置线程数非常关键。
对于Java分布式应用,还有一种典型现象是应用中有较多的网络IO操作和确实需要一些锁竞争机制(如数据库连接池),但为了能够支撑搞得并发量,可采用协程(Coroutine)来支撑更高的并发量,避免并发量上涨后造成CPU sy消耗严重、系统load迅速上涨和系统性能下降。
在Java中实现协程的框架有Kilim,Kilim执行一项任务创建Task,使用Task的暂停机制,而不是Thread,Kilim承担了线程调度以及上下切换动作,Task相对于原生Thread而言就轻量级多了,且能更好利用CPU。Kilim带来的是线程使用率的提升,但同时由于要在JVM堆中保存Task上下文信息,因此在采用Kilim的情况下要消耗更多的内存。(目前JDK 7中也有一个支持协程方式的实现,另外基于JVM的Scala的Actor也可用于在Java使用协程)

文件IO消耗严重的解决方法

从程序的角度而言,造成文件IO消耗严重的原因主要是多个线程在写进行大量的数据到同一文件,导致文件很快变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁。
常用调优方法:

  1. 异步写文件
  2. 批量读写
  3. 限流
  4. 限制文件大小

    网络IO消耗严重的解决方法

    从程序的角度而言,造成网络IO消耗严重的原因主要是同时需要发送或接收的包太多。
    常用调优方法:

  5. 限流,限流通常是限制发送packet的频率,从而在网络IO消耗可接受的情况下来发送packget。

    内存消耗严重的解决方法

    释放不必要的引用:代码持有了不需要的对象引用,造成这些对象无法被GC,从而占据了JVM堆内存。(使用ThreadLocal:注意在线程内动作执行完毕时,需执行ThreadLocal.set把对象清除,避免持有不必要的对象引用)
    使用对象缓存池:创建对象要消耗一定的CPU以及内存,使用对象缓存池一定程度上可降低JVM堆内存的使用。
    采用合理的缓存失效算法:如果放入太多对象在缓存池中,反而会造成内存的严重消耗, 同时由于缓存池一直对这些对象持有引用,从而造成Full GC增多,对于这种状况要合理控制缓存池的大小,避免缓存池的对象数量无限上涨。(经典的缓存失效算法来清除缓存池中的对象:FIFO、LRU、LFU等)
    合理使用SoftReference和WeekReference:SoftReference的对象会在内存不够用的时候回收,WeekReference的对象会在Full GC的时候回收。

    资源消耗不多但程序执行慢的情况的解决方法

    降低锁竞争: 多线多了,锁竞争的状况会比较明显,这时候线程很容易处于等待锁的状况,从而导致性能下降以及CPU sy上升。
    使用并发包中的类:大多数采用了lock-free、nonblocking算法。
    使用Treiber算法:基于CAS以及AtomicReference。
    使用Michael-Scott非阻塞队列算法:基于CAS以及AtomicReference,典型ConcurrentLindkedQueue。
    (基于CAS和AtomicReference来实现无阻塞是不错的选择,但值得注意的是,lock-free算法需不断的循环比较来保证资源的一致性的,对于冲突较多的应用场景而言,会带来更高的CPU消耗,因此不一定采用CAS实现无阻塞的就一定比采用lock方式的性能好。 还有一些无阻塞算法的改进:MCAS、WSTM等)
    尽可能少用锁:尽可能只对需要控制的资源做加锁操作(通常没有必要对整个方法加锁,尽可能让锁最小化,只对互斥及原子操作的地方加锁,加锁时尽可能以保护资源的最小化粒度为单位—如只对需要保护的资源加锁而不是this)。
    拆分锁:独占锁拆分为多把锁(读写锁拆分、类似ConcurrentHashMap中默认拆分为16把锁),很多程度上能提高读写的性能,但需要注意在采用拆分锁后,全局性质的操作会变得比较复杂(如ConcurrentHashMap中size操作)。(拆分锁太多也会造成副作用,如CPU消耗明显增加)
    去除读写操作的互斥:在修改时加锁,并复制对象进行修改,修改完毕后切换对象的引用,从而读取时则不加锁。这种称为CopyOnWrite,CopyOnWriteArrayList是典型实现,好处是可以明显提升读的性能,适合读多写少的场景, 但由于写操作每次都要复制一份对象,会消耗更多的内存。

    充分利用硬件资源(CPU和内存)

    充分利用CPU

    在能并行处理的场景中未使用足够的线程(线程增加:CPU资源消耗可接受且不会带来激烈竞争锁的场景下), 例如单线程的计算,可以拆分为多个线程分别计算,最后将结果合并,JDK 7中的fork-join框架。
    Amdahl定律公式:1/(F+(1-F)/N)。

    充分利用内存

    数据的缓存、耗时资源的缓存(数据库连接创建、网络连接的创建等)、页面片段的缓存。
    毕竟内存的读取肯定远快于硬盘、网络的读取, 在内存消耗可接受、GC频率、以及系统结构(例如集群环境可能会带来缓存的同步)可接受情况下,应充分利用内存来缓存数据,提升系统的性能。

    总结

    好的调优策略是收益比(调优后提升的效果/调优改动所需付出的代价)最高的,通常来说简单的系统调优比较好做,因此尽量保持单机上应用的纯粹性, 这是大型系统的基本架构原则。
    调优的三大有效原则:充分而不过分使用硬件资源、合理调整JVM、合理使用JDK包。
    “ 文章已经收录GitHub:https://github.com/JavaFamily

    4

    作者:HaC是个程序员链接:https://www.zhihu.com/question/421237964/answer/1764173058来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    目录

    1、对高并发的理解
    2、高并发的解决方案
    1、scale-up 纵向扩展
    2、scale-out 横向扩展
    纵向层面:
    1、堆机器
    2、机器调优
    横向层面:
    1、前端优化
    2、系统拆分
    3、负载均衡:
    4、缓存
    5、异步
    6.数据库读写分离
    业务层面:
    1、服务降级
    2、数据核对
    监控层面:
    1、监控报警
    2、灰度发布
    3、扩容
    高并发总会出现各种各样的问题,带来的就是各种各样的技术解决方案。
    但是目前大多公司都是面向业务开发,有过相关经历的小伙伴就知道了,无非就是简单的调接口。
    所以并不是每个程序员都能切身体会到高并发的场景,更多的是停留在理论上面。
    我现在说的也是理论~
    可能面试官也不一定接触过高并发业务。而在面试中,偏偏喜欢问你的项目有多少流量、有没有高并发、以及高并发解决方案。
    先说两个坑吧:
    第一是直接回答没有高并发经验。
    这种没有经验,但我相信你可以尝试去回答,有无经验不影响你做出你的看法,你可以尝试这样说:
    1、公司的业务简单,没有接触过,但我可以谈谈我的思路
    第二是直接说加机器,升级设备。
    如果你真的没接触过高并发,而且没有理论知识(毕竟大部分公司都没有高并发的业务,不是为了面试谁会看这个),你可以用jmeter压测试一下,开个10w的请求去测试自己的服务,如何保证业务的一致性、如何负载均衡、如何响应,最后查看一下jmeter的response,并尝试去优化。

    1、对高并发的理解

    常见的高并发场景:12306抢票、吃瓜事件微博宕机、淘宝双十一。
    什么样的流量才算是高并发呢?
    技术围绕着业务。高并发不能只看QPS、TPS、吞吐量,因为不同的业务场景不一样,执行复杂度也不一样,单看并发量没有意义。
    我所理解的高并发是机器在有限的资源内承受最大的压力。
    遇到高并发并不是加机器,而是从系统设计、代码、甚至是业务场景中下手。
    那如何衡量一个系统是否扛得住高并发的流量呢?
    通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数)
    举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。
    那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):
    20*500/0.1 = 100000 (10万QPS)
    咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。
    另外,从用户体验角度来看,200毫秒被认为是第一个分界点,用户感觉不到延迟,1秒是第二个分界点,用户能感受到延迟,但是可以接受。

    2、高并发的解决方案

    1、scale-up 纵向扩展

    scale-up 纵向扩展,基本上是从硬件层面出发,常见的是:
    1、通过增加内存、CPU核数、升级磁盘、带宽等堆硬件的方式来提升。
    2、其次是服务器的性能,比如缓存减少IO次数、Linux网络内核调优等。

    2、scale-out 横向扩展

    上面说到升级机器的硬件,但这是在单机层面考虑的。
    单机的性能存在极限,这时候多机集群就是很好的解决方案了,但复杂度也随之增加。
    虽然加了机器,做了集群,但还是要做好每一台机器的分层。
    现在的解决套路无非就是:
    前端优化—>系统拆分—>负载均衡—>缓存—>异步—>分库
    下面是简单的实践:

    纵向层面:

    1、堆机器

    1台8核8G的服务器很可能干不过4台2核2G的机器。 而且1台1万块钱的机器也干不过10台1千块钱的机器。

    2、机器调优

    JVM
    JVM层面也有,比如说堆内存分配空间大小,堆内存不足,线程创建失败,频繁GC,影响业务。
    CPU、内存
    CPU占用率、内存占用率,可以使用free、top 观察一下。
    还有Linux调优的参数诸如:
    限制并发连接的最大数量fs.file-max、端口状态保留时间TIME_WAIT、每个套接字允许的最大辅助缓冲区大小net.core.optmem_max、Socket接收/发送缓存最大值net.core.rmem_max
    TIME_WAIT状态保留一定的时间,当并发请求过多的时候,就会产生大量的TIME_WAIT状态的连接,无法及时断开的话,会占用大量的端口资源和服务器资源。
    可以使用以下命令看一下TIME_WAIT端口:
    netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’
    还有一些常用的优化:
    # 指示进程(例如工作进程)可同时打开的最大句柄数,直接限制并发连接的最大数量。
    # 默认值:fs.file-max = 141079
    fs.file-max = 655350

启用keepalive时,TCP发送keepalive消息的频率。默认值为2小时。将其调低一点以更快地删除无用的连接
# 默认值:net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_time = 1200

当服务器主动关闭链接时,套接字保持FN-WAIT-2状态的最大时间
# 默认值:net.ipv4.tcp_fin_timeout = 60
net.ipv4.tcp_fin_timeout = 30

该参数决定了,网络设备接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。
# 默认值:net.core.netdev_max_backlog = 1000
net.core.netdev_max_backlog = 8192

每个套接字允许的最大辅助缓冲区大小。辅助数据是带有附加数据的结构cmsghdr结构的序列。
# 默认值:net.core.optmem_max = 20480
net.core.optmem_max = 81920

指定了接收套接字缓冲区大小的最大值(以字节为单位)。
# 默认值:net.core.rmem_default = 212992
net.core.rmem_default = 262144

允许最大数量的TIME-WAIT套接字。超过几位数,TIME-WAIT套接字将立即清除,并显示警告消息。默认值为8192,太多的TIME-WAIT套接字会减慢Web服务器的速度
# 默认值:net.ipv4.tcp_max_tw_buckets = 8192
net.ipv4.tcp_max_tw_buckets = 5000

TCP接收/发送缓存最小值,默认值,最大值
# 默认值:net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_rmem = 4096 32768 262142
# 默认值:net.ipv4.tcp_wmem = 4096 16384 4194304
net.ipv4.tcp_wmem = 4096 32768 262142

Socket接收/发送缓存最大值
# 默认值均为:212992
net.core.rmem_max = 4194304
net.core.wmem_max = 4194304

启用syncookies有助于防御dos攻击,会少量增加CPU使用率,默认启用
net.ipv4.tcp_syncookies = 1

接受SYN同步包的最大客户端数量,即半连接上限,128M内存情况下是缺省值1024
默认值:net.ipv4.tcp_max_syn_backlog = 1024
net.ipv4.tcp_max_syn_backlog = 8192

服务端所能accept即处理数据的最大客户端数量,即完成连接上限
默认值:net.core.somaxconn = 1024
net.core.somaxconn = 10240

横向层面:

1、前端优化

CDN加速
启用浏览器缓存和文件压缩、添加异步请求
禁止外部盗链
外部网站的图片或者文件盗链往往会带来大量的负载压力,因此应该严格限制外部对于自身的图片或者文件盗链。

2、系统拆分

系统拆分可以理解为分布式集群,假如目前有30个并发量,你单台机器最大的处理并发量是20,此时你可以加入另一台机器,理论上能处理的最大请求量就是40。
像目前的微服务springBoot、dubbo,其中还有一个重要的通信方式就是RPC调用,传统的服务通信都是HTTP的方式,多个服务之间使用RPC性能要好一点,而且可以进行链路追踪。
而且微服务还有Ribbon和Fegin这些负载均衡组件。
而在代码层面,可以假如接口超时机制、幂等性校验等等。

3、负载均衡:

还可以通过Nginx负载均衡,分发请求,让两个机器平均处理请求量。
Nginx负载均衡:内置策略:IP Hash,加权轮询;扩展策略:fair策略,通用hash,一致性hash

4、缓存

Redis、Memcache、布隆过滤器等等。
但要注意缓存穿透、击穿、雪崩。
还可以使用缓存预热,先把一部分的数据加载到缓存,不至于让数据库压力过大影响请求。

5、异步

引入MQ(还能解耦、削峰),把请求放到MQ,稍后处理,同步转异步。

6.数据库读写分离

分库分表,读写分离。
基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作,降低数据库的压力。

业务层面:

1、服务降级

当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作,解决方案目前有流行的Spring Cloud Hystrix
比如说 直接返回“当前人数较多,请稍后重试”、限流处理、随机丢弃请求(好像淘宝的高并发业务就是这样的,要看业务),必要时进行熔断,触发失败rollback事件,随后慢慢进行数据核对。

2、数据核对

分布式系统需要考虑分布式事务,相比传统的单机服务,分布式事务的数据更难核对一致性,所以有必要建立数据核对平台。

监控层面:

这里说一下我们公司常见的监控手段:

1、监控报警

比如说链路追踪,跟踪调用链的时间和长度,快速追踪到哪个环节最耗时。
服务器CPU、内存、IO、网络,以及JVM、数据库(慢SQL)等等进行监控,在高并发请求业务下,快速定位。

2、灰度发布

模拟并发量。小流量部署,然后监控。

3、扩容

K8s可以动态扩容,在监测到服务压力激增的情况下,可以自动申请资源。
如果没有动态扩容机制,是否有备用机、灾备是否可以马上响应。
还有就是一个经验:二八原则:80%的高并发流量都在20%的时间产生。
不如好好优化一下业务。


end