高并发系统架构设计需要考虑哪些

本文探讨了高并发系统设计的关键要素,包括分层架构(如MVC、三层结构)的重要性,性能优化原则、分层设计中的关键因素、系统扩展策略、数据库优化和数据库访问中间件。还强调了高可用性、性能度量和故障演练在系统设计中的应用。
摘要由CSDN通过智能技术生成

高并发系统架构设计需要考虑哪些

转载于https://www.toudo.cn/article/21
我们在应对高并发大流量时应对方法,归纳起来共有三种方案。

Scale-out(横向扩展): 分而治之是一种常见的高并发系统设计方法,采用分布式部署的方式把流量分流开,让每个服务器都承担一部分并发和流量。

缓存:使用缓存来提高系统的性能,就好比用“拓宽河道”的方式来抵御高并发大流量的冲击。

异步:在某些场景下,未处理完之前,我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求。

架构分层

什么是分层架构

软件架构分层在软件工程中是一种常见的设计模式,他是将系统拆分成N个层次、每个层次有独立的职责,多个层次协同提供完整的功能。

MVC架构:他将整体的系统分成了Model模型、View视图和Controller控制器三个层次,也就是将用户视图和业务处理分隔开,并且通过控制器连接起来,很好的实现了表现和逻辑的解耦,是一种标准的软件分层架构。
MVC
另一种常见的分层方式就是将整体架构分为表现层、逻辑层和数据访问层:
比如controller、service、dao,他们分别对应了表现层、逻辑层和数据访问层。

MVC
还比如OSI网络模型,它把真个网络分成了七层,自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

工作中常用的TCP/IP协议,它把网络简化成了四层,链路层、网络层、传输层和应用层。

分层有什么好处

分层的好处可以简化系统设计,让不同的人专注做某一层的事情。
再有分层后可以做到很高的复用。
分层架构可以让我们更容易横向扩展,如果系统没有分层,当流量增加时我们需要针对整体系统来做扩展。但是如果我们按照上面提到的三层架构将系统分层后,那么我们就可以针对具体的问题来进行扩展了。

如何来做系统分层

说了这么多分层的好处,那当我们要做分层设计的时候,需要考虑哪些关键因素呢?
最主要的一点就是你需要理清楚每个层次的边界是什么。

当业务逻辑简单时,层次之间的边界的确清晰,开发新的功能时也知道哪些代码要往
哪儿写。但是当业务逻辑变得越来越复杂时,边界就会变得越来越模糊。

参照阿里发布的《阿里巴巴 Java 开发手册 v1.4.0(详尽版)》,我们
可以将原先的三层架构细化成下面的样子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GSWpChlq-1633934483081)(https://www.toudo.cn/upload/2021/10/7eea5lgsimgmgocisvge1e2kid.png)]
终端显示层:各端模板渲染并执行显示的层。当前主要是 Velocity 渲染,JS 渲染, JSP
渲染,移动端展示等。

开放接口层:将 Service 层方法封装成开放接口,同时进行网关安全控制和流量控制等。

Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理
等。

Service 层:业务逻辑层。

Manager 层:通用业务处理层。这一层主要有两个作用,其一,你可以将原先 Service
层的一些通用能力下沉到这一层,比如与缓存和存储交互策略,中间件的接入;其二,你
也可以在这一层封装对第三方接口的调用,比如调用支付服务,调用审核服务等。

DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。

在这个分层架构中主要增加了 Manager 层,它与 Service 层的关系是:Manager 层提供
原子的服务接口,Service 层负责依据业务逻辑来编排原子接口。

我们还是以三层架构为例,数据从表示层进入之后一定要流转到逻辑层,做业务逻辑处理,
然后流转到数据访问层来和数据库交互。那么你可能会问:“如果业务逻辑很简单的话可不
可以从表示层直接到数据访问层,甚至直接读数据库呢?”

其实从功能上是可以的,但是从长远的架构设计考虑,这样会造成层级调用的混乱,比方说
如果表示层或者业务层可以直接操作数据库,那么一旦数据库地址发生变更,你就需要在多
个层次做更改,这样就失去了分层的意义,并且对于后面的维护或者重构都会是灾难性的。

分层架构的不足

任何事物都不可能是尽善尽美的,分层架构虽有优势也会有缺陷,它最主要的一个缺陷就是
增加了代码的复杂度。

这是显而易见的嘛,明明可以在接收到请求后就可以直接查询数据库获得结果,却偏偏要在
中间插入多个层次,并且有可能每个层次只是简单地做数据的传递。有时增加一个小小的需
求也需要更改所有层次上的代码,看起来增加了开发的成本,并且从调试上来看也增加了复
杂度,原本如果直接访问数据库我只需要调试一个方法,现在我却要调试多个层次的多个方
法。

另外一个可能的缺陷是,如果我们把每个层次独立部署,层次间通过网络来交互,那么多层
的架构在性能上会有损耗。这也是为什么服务化架构性能要比单体架构略差的原因,也就是
所谓的“多一跳”问题。

那我们是否要选择分层的架构呢?答案当然是肯定的。

你要知道,任何的方案架构都是有优势有缺陷的,天地尚且不全何况我们的架构呢?分层架
构固然会增加系统复杂度,也可能会有性能的损耗,但是相比于它能带给我们的好处来说,
这些都是可以接受的,或者可以通过其它的方案解决的。我们在做决策的时候切不可以偏概
全,因噎废食。

如何提升系统性能

高并发系统设计的三大目标:高并发、高可用、可扩展。

高并发,是指运用设计手段让系统能够处理更多的用户并发请求,也就是承担更大的流量。
它是一切架构设计的背景和前提,脱离了它去谈性能和可用性是没有意义的。很显然嘛,你
在每秒一次请求和每秒一万次请求,两种不同的场景下,分别做到毫秒级响应时间和五个九
(99.999%)的可用性,无论是设计难度还是方案的复杂度,都不是一个级别的。

而性能和可用性,是我们实现高并发系统设计必须考虑的因素。

性能反应了系统的使用体验,想象一下,同样承担每秒一万次请求的两个系统,一个响应时
间是毫秒级,一个响应时间在秒级别,它们带给用户的体验肯定是不同的。

另一个耳熟能详的名词叫**“可扩展性”**,它同样是高并发系统设计需要考虑的因素。为什么
呢?
流量分为平时流量和峰值流量两种,峰值流量可能会是平时流量的几倍甚至几十倍,在应对
峰值流量的时候,我们通常需要在架构和方案上做更多的准备。这就是淘宝会花费大半年的
时间准备双十一,也是在面对“明星离婚”等热点事件时,看起来无懈可击的微博系统还是
会出现服务不可用的原因。而易于扩展的系统能在短时间内迅速完成扩容,更加平稳地承担
峰值流量。

性能优化原则

**首先,性能优化一定不能盲目,一定是问题导向的。**脱离了问题,盲目地提早优化会增加系
统的复杂度,浪费开发人员的时间,也因为某些优化可能会对业务上有些折中的考虑,所以
也会损伤业务。

**其次,性能优化也遵循“八二原则”,即你可以用 20% 的精力解决 80% 的性能问题。**所
以我们在优化过程中一定要抓住主要矛盾,优先优化主要的性能瓶颈点。

**再次,性能优化也要有数据支撑。**在优化过程中,你要时刻了解你的优化让响应时间减少了
多少,提升了多少的吞吐量。

**最后,性能优化的过程是持续的。**高并发的系统通常是业务逻辑相对复杂的系统,那么在这
类系统中出现的性能问题通常也会有多方面的原因。因此,我们在做性能优化的时候要明确
目标,比方说,支撑每秒 1 万次请求的吞吐量下响应时间在 10ms,那么我们就需要持续
不断地寻找性能瓶颈,制定优化方案,直到达到目标为止。

性能度量指标

性能优化的第三点原则中提到,对于性能我们需要有度量的标准,有了数据才能明确目前存
在的性能问题,也能够用数据来评估性能优化的效果。所以明确性能的度量指标十分重要。

一般来说,度量性能的指标是系统接口的响应时间,但是单次的响应时间是没有意义的,你
需要知道一段时间的性能情况是什么样的。所以,我们需要收集这段时间的响应时间数据,
然后依据一些统计方法计算出特征值,这些特征值就能够代表这段时间的性能情况。我们常
见的特征值有以下几类。

平均值:
举个例子,假设我们在 30s 内有 10000 次请求,每次请求的响应时间都是 1ms,那么这
段时间响应时间平均值也是 1ms。这时,当其中 100 次请求的响应时间变成了 100ms,
那么整体的响应时间是 (100 * 100 + 9900 * 1) / 10000 = 1.99ms。你看,虽然从平均值
上来看仅仅增加了不到 1ms,但是实际情况是有 1% 的请求(100/10000)的响应时间已
经增加了 100 倍。所以,平均值对于度量性能来说只能作为一个参考。

最大值:
还拿上面的例子来说,如果 10000 次请求中只有一次请求的响应时间达到 100ms,那么这
段时间请求的响应耗时的最大值就是 100ms,性能损耗为原先的百分之一,这种说法明显
是不准确的。

分位值:
分位值有很多种,比如 90 分位、95 分位、75 分位。以 90 分位为例,我们把这段时间请
求的响应时间从小到大排序,假如一共有 100 个请求,那么排在第 90 位的响应时间就是
90 分位值。分位值排除了偶发极慢请求对于数据的影响,能够很好地反应这段时间的性能
情况,分位值越大,对于慢请求的影响就越敏感。

在我来看,分位值是最适合作为时间段内,响应时间统计值来使用的,在实际工作中也应用
最多。除此之外,平均值也可以作为一个参考值来使用。

从用户使用体验的角度来看,200ms 是第一个分界点:接口的响应时间在 200ms 之内,
用户是感觉不到延迟的,就像是瞬时发生的一样。而 1s 是另外一个分界点:接口的响应时
间在 1s 之内时,虽然用户可以感受到一些延迟,但却是可以接受的,超过 1s 之后用户就
会有明显等待的感觉,等待时间越长,用户的使用体验就越差。所以,健康系统的 99 分位
值的响应时间通常需要控制在 200ms 之内,而不超过 1s 的请求占比要在 99.99% 以上。

高并发下的性能优化

假如说,你现在有一个系统,这个系统中处理核心只有一个,执行的任务的响应时间都在
10ms,它的吞吐量是在每秒 100 次。那么我们如何来优化性能从而提高系统的并发能力
呢?主要有两种思路:一种是提高系统的处理核心数,另一种是减少单次任务的响应时间。

  • 提高系统的处理核心数
  • 减少单次任务响应时间

系统怎样做到高可用

可用性的度量

可用性是一个抽象的概念,你需要知道要如何来度量它,与之相关的概念是:MTBF 和
MTTR。

**MTBF(Mean Time Between Failure)**是平均故障间隔的意思,代表两次故障的间隔时
间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。

**MTTR(Mean Time To Repair)**表示故障的平均恢复时间,也可以理解为平均故障时
间。这个值越小,故障对于用户的影响越小。

可用性与 MTBF 和 MTTR 的值息息相关,我们可以用下面的公式表示它们之间的关系:
Availability = MTBF / (MTBF + MTTR)

这个公式计算出的结果是一个比例,而这个比例代表着系统的可用性。一般来说,我们会使
用几个九来描述系统的可用性。
可用性

高可用系统设计的思路
  • 系统设计
    “Design for failure”是我们做高可用系统设计时秉持的第一原则。在承担百万 QPS 的高
    并发系统中,集群中机器的数量成百上千台,单机的故障是常态,几乎每一天都有发生故障
    的可能。
    未雨绸缪才能决胜千里。我们在做系统设计的时候,要把发生故障作为一个重要的考虑点,
    预先考虑如何自动化地发现故障,发生故障之后要如何解决。当然了,除了要有未雨绸缪的
    思维之外,我们还需要掌握一些具体的优化方法,比如failover(故障转移)、超时控制以
    及降级和限流。

一般来说,发生 failover 的节点可能有两种情况:
1. 是在完全对等的节点之间做 failover。
2. 是在不对等的节点之间,即系统中存在主节点也存在备节点。

在对等节点之间做 failover 相对来说简单些。在这类系统中所有节点都承担读写流量,并
且节点中不保存状态,每个节点都可以作为另一个节点的镜像。在这种情况下,如果访问某
一个节点失败,那么简单地随机访问另一个节点就好了。

举个例子,Nginx 可以配置当某一个 Tomcat 出现大于 500 的请求的时候,重试请求另一
个 Tomcat 节点,就像下面这样:

针对不对等节点的 failover 机制会复杂很多。比方说我们有一个主节点,有多台备用节
点,这些备用节点可以是热备(同样在线提供服务的备用节点),也可以是冷备(只作为备
份使用),那么我们就需要在代码中控制如何检测主备机器是否故障,以及如何做主备切
换。

使用最广泛的故障检测机制是“心跳”。你可以在客户端上定期地向主节点发送心跳包,也
可以从备份节点上定期发送心跳包。当一段时间内未收到心跳包,就可以认为主节点已经发
生故障,可以触发选主的操作。
选主的结果需要在多个备份节点上达成一致,所以会使用某一种分布式一致性算法,比方说
Paxos,Raft。

除了故障转移以外,对于系统间调用超时的控制也是高可用系统设计的一个重要考虑方面。

复杂的高并发系统通常会有很多的系统模块组成,同时也会依赖很多的组件和服务,比如说
缓存组件,队列服务等等。它们之间的调用最怕的就是延迟而非失败,因为失败通常是瞬时
的,可以通过重试的方式解决。而一旦调用某一个模块或者服务发生比较大的延迟,调用方
就会阻塞在这次调用上,它已经占用的资源得不到释放。当存在大量这种阻塞请求时,调用
方就会因为用尽资源而挂掉。

在系统开发的初期,超时控制通常不被重视,或者是没有方式来确定正确的超时时间。

既然要做超时控制,那么我们怎么来确定超时时间呢?这是一个比较困难的问题。
超时时间短了,会造成大量的超时错误,对用户体验产生影响;超时时间长了,又起不到作
用。我建议你通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后
依据这个时间来指定超时时间。如果没有调用的日志,那么你只能按照经验值来指定超时时
间。不过,无论你使用哪种方式,超时时间都不是一成不变的,需要在后面的系统维护过程
中不断地修改。

超时控制实际上就是不让请求一直保持,而是在经过一定时间之后让请求失败,释放资源给
接下来的请求使用。这对于用户来说是有损的,但是却是必要的,因为它牺牲了少量的请求
却保证了整体系统的可用性。而我们还有另外两种有损的方案能保证系统的高可用,它们就
是降级和限流。

降级是为了保证核心服务的稳定而牺牲非核心服务的做法。比方说我们发一条微博会先经过
反垃圾服务检测,检测内容是否是广告,通过后才会完成诸如写数据库等逻辑。

限流完全是另外一种思路,它通过对并发的请求进行限速来保护系统。比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误
给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短
暂的行为,因此是可以接受的。

  • 系统运维
    在系统设计阶段为了保证系统的可用性可以采取上面的几种方法,那在系统运维的层面又能
    做哪些事情呢?其实,我们可以从灰度发布、故障演练两个方面来考虑如何提升系统的可用
    性。

你应该知道,在业务平稳运行过程中,系统是很少发生故障的,90% 的故障是发生在上线
变更阶段的。比方说,你上了一个新的功能,由于设计方案的问题,数据库的慢请求数翻了
一倍,导致系统请求被拖慢而产生故障。

如果没有变更,数据库怎么会无缘无故地产生那么多的慢请求呢?因此,为了提升系统的可
用性,重视变更管理尤为重要。而除了提供必要回滚方案,以便在出现问题时快速回滚恢复
之外,另一个主要的手段就是灰度发布。

灰度发布指的是系统的变更不是一次性地推到线上的,而是按照一定比例逐步推进的。一般
情况下,灰度发布是以机器维度进行的。比方说,我们先在 10% 的机器上进行变更,同时
观察 Dashboard 上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较
平稳并且没有出现大量的错误日志,那么再推动全量变更。

灰度发布是在系统正常运行条件下,保证系统高可用的运维手段,那么我们如何知道发生故
障时系统的表现呢?这里就要依靠另外一个手段:故障演练。

故障演练指的是对系统进行一些破坏性的手段,观察在出现局部故障时,整体的系统表现是
怎样的,从而发现系统中存在的,潜在的可用性问题。

一个复杂的高并发系统依赖了太多的组件,比方说磁盘,数据库,网卡等,这些组件随时随
地都可能会发生故障,而一旦它们发生故障,会不会如蝴蝶效应一般造成整体服务不可用
呢?我们并不知道,因此,故障演练尤为重要。

在我来看,故障演练和时下比较流行的“混沌工程”的思路如出一辙,作为混沌工程的鼻
祖,Netfix 在 2010 年推出的“Chaos Monkey”工具就是故障演练绝佳的工具。它通过
在线上系统上随机地关闭线上节点来模拟故障,让工程师可以了解,在出现此类故障时会有
什么样的影响。
当然,这一切是以你的系统可以抵御一些异常情况为前提的。如果你的系统还没有做到这一
点,那么我建议你另外搭建一套和线上部署结构一模一样的线下系统,然后在这套系统上做
故障演练,从而避免对生产系统造成影响。

如何让系统易于扩展

高可扩展性的设计思路
拆分是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的
模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些。将复杂的问题简
单化,这就是我们的思路。

存储层的扩展性

无论是存储的数据量,还是并发访问量,不同的业务模块之间的量级相差很大,比如说成熟
社区中,关系的数据量是远远大于用户数据量的,但是用户数据的访问量却远比关系数据要
大。所以假如存储目前的瓶颈点是容量,那么我们只需要针对关系模块的数据做拆分就好
了,而不需要拆分用户模块的数据。所以存储拆分首先考虑的维度是业务维度。

按照业务拆分,在一定程度上提升了系统的扩展性,但系统运行时间长了之后,单一的业务
数据库在容量和并发请求量上仍然会超过单机的限制。这时,我们就需要针对数据库做第二
次拆分。

这次拆分是按照数据特征做水平的拆分,比如说我们可以给用户库增加两个节点,然后按照
某些算法将用户的数据拆分到这三个库里面,具体的算法我会在后面讲述数据库分库分表时
和你细说。
水平拆分之后,我们就可以让数据库突破单机的限制了。但这里要注意,我们不能随意地增
加节点,因为一旦增加节点就需要手动地迁移数据,成本还是很高的。所以基于长远的考
虑,我们最好一次性增加足够的节点以避免频繁地扩容。

当数据库按照业务和数据维度拆分之后,我们尽量不要使用事务。因为当一个事务中同时更
新不同的数据库时,需要使用二阶段提交,来协调所有数据库要么全部更新成功,要么全部
更新失败。这个协调的成本会随着资源的扩展不断升高,最终达到无法承受的程度。

业务层的扩展性

我们一般会从三个维度考虑业务层的拆分方案,它们分别是:业务纬度,重要性纬度和请求
来源纬度。

每个业务依赖独自的数据库资源,不会依赖其它业务的数据库资源。这样当某一个业务的接
口成为瓶颈时,我们只需要扩展业务的池子,以及确认上下游的依赖方就可以了,这样就大
大减少了扩容的复杂度。

除此之外,我们还可以根据业务接口的重要程度,把业务分为核心池和非核心池。打个比
方,就关系池而言,关注、取消关注接口相对重要一些,可以放在核心池里面;拉黑和取消
拉黑的操作就相对不那么重要,可以放在非核心池里面。这样,我们可以优先保证核心池的
性能,当整体流量上升时优先扩容核心池,降级部分非核心池的接口,从而保证整体系统的
稳定性。

如何减少频繁创建数据库连接的性能损耗?

那么为什么频繁创建连接会造成响应时间慢呢?来看一个实际的测试。

我用"tcpdump -i bond0 -nn -tttt port 4490"命令抓取了线上 MySQL 建立连接的网络包
来做分析,从抓包结果来看,整个 MySQL 的连接过程可以分为两部分:

**第一部分是前三个数据包。**第一个数据包是客户端向服务端发送的一个“SYN”包,第二个
包是服务端回给客户端的“ACK”包以及一个“SYN”包,第三个包是客户端回给服务端
的“ACK”包,熟悉 TCP 协议的同学可以看出这是一个 TCP 的三次握手过程。

**第二部分是 MySQL 服务端校验客户端密码的过程。**其中第一个包是服务端发给客户端要
求认证的报文,第二和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是
服务端回给客户端认证 OK 的报文。从图中,你可以看到整个连接过程大概消耗了
4ms(969012-964904)。
连接
那么单条 SQL 执行时间是多少呢?我们统计了一段时间的 SQL 执行时间,发现 SQL 的平
均执行时间大概是 1ms,也就是说相比于 SQL 的执行,MySQL 建立连接的过程是比较耗
时的。这在请求量小的时候其实影响不大,因为无论是建立连接还是执行 SQL,耗时都是
毫秒级别的。可是请求量上来之后,如果按照原来的方式建立一次连接只执行一条 SQL 的
话,1s 只能执行 200 次数据库的查询,而数据库建立连接的时间占了其中 4/5。

那这时你要怎么做呢?

一番谷歌搜索之后,你发现解决方案也很简单,只要使用连接池将数据库连接预先建立好,
这样在使用的时候就不需要频繁地创建连接了。调整之后,你发现 1s 就可以执行 1000 次
的数据库查询,查询性能大大的提升了。

用连接池预先建立数据库连接

其实,在开发过程中我们会用到很多的连接池,像是数据库连接池、HTTP 连接池、Redis
连接池等等。而连接池的管理是连接池设计的核心,我就以数据库连接池为例,来说明一下
连接池管理的关键点。

数据库连接池有两个最重要的配置:最小连接数和最大连接数,它们控制着从连接池中获取
连接的流程:

  • 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
  • 如果连接池中有空闲连接则复用空闲连接;
  • 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
  • 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配
  • 置是 checkoutTimeout)等待旧的连接可用;
  • 如果等待超过了这个设定时间则向用户抛出错误。
  1. 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命
    令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关
    闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用,也是我比较推荐的方式。

  2. 在获取到连接之后,先校验连接是否可用,如果可用才会执行 SQL 语句。比如 DBCP 连
    接池的 testOnBorrow 配置项,就是控制是否开启这个验证。这种方式在获取连接时会引
    入多余的开销,在线上系统中还是尽量不要开启,在测试服务上可以使用。

用线程池预先创建线程

果不其然,JDK 1.5 中引入的 ThreadPoolExecutor 就是一种线程池的实现,它有两个重要
的参数:coreThreadCount 和 maxThreadCount,这两个参数控制着线程池的执行过程。

如果线程池中的线程数少于 coreThreadCount 时,处理新的任务时会创建新的线程;
如果线程数大于 coreThreadCount 则把任务丢到一个队列里面,由当前空闲的线程执
行;
当队列中的任务堆积满了的时候,则继续创建线程,直到达到 maxThreadCount;
当线程数达到 maxTheadCount 时还有新的任务提交,那么我们就不得不将它们丢弃
了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMBfm46w-1633934483100)(https://www.toudo.cn/upload/2021/10/thrfcuuoeaj7mqhuhjfpnelsv3.png)]
这个任务处理流程看似简单,实际上有很多坑,你在使用的时候一定要注意。

首先, JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它
比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务。这是为什么
呢?因为执行 CPU 密集型的任务时 CPU 比较繁忙,因此只需要创建和 CPU 核数相当的线
程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当当前线程数超过核
心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来。

但是,我们平时开发的 Web 系统通常都有大量的 IO 操作,比方说查询数据库、查询缓存
等等。任务在执行 IO 操作的时候 CPU 就空闲了下来,这时如果增加执行任务的线程数而
不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞
吐量。所以你看 Tomcat 使用的线程池就不是 JDK 原生的线程池,而是做了一些改造,当
线程数超过 coreThreadCount 之后会优先创建线程,直到线程数到达
maxThreadCount,这样就比较适合于 Web 系统大量 IO 操作的场景了,你在实际运用过
程中也可以参考借鉴。

其次,线程池中使用的队列的堆积量也是我们需要监控的重要指标,对于实时性要求比较高
的任务来说,这个指标尤为关键。

**我在实际项目中就曾经遇到过任务被丢给线程池之后,长时间都没有被执行的诡异问题。**最
初,我认为这是代码的 Bug 导致的,后来经过排查发现,是因为线程池的
coreThreadCount 和 maxThreadCount 设置的比较小,导致任务在线程池里面大量的堆
积,在调大了这两个参数之后问题就解决了。跳出这个坑之后,我就把重要线程池的队列任
务堆积量,作为一个重要的监控指标放到了系统监控大屏上。

最后,如果你使用线程池请一定记住不要使用无界队列(即没有设置固定大小的队列)。也
许你会觉得使用了无界队列后,任务就永远不会被丢弃,只要任务对实时性要求不高,反正
早晚有消费完的一天。但是,大量的任务堆积会占用大量的内存空间,一旦内存空间被占满
就会频繁地触发 Full GC,造成服务不可用,我之前排查过的一次 GC 引起的宕机,起因就
是系统中的一个线程池使用了无界队列。

**这是一种常见的软件设计思想,叫做池化技术,**它的核心思想是空间换时间,期望使用预先
创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了
对象的使用的成本,总之是好处多多。

数据库优化方案

数据库交互
此时,你的数据库还是单机部署,依据一些云厂商的 Benchmark 的结果,在 4 核 8G 的
机器上运 MySQL 5.7 时,大概可以支撑 500 的 TPS 和 10000 的 QPS。

主从读写分离

主从读写的两个技术关键点

  1. 一个是数据的拷贝,我们称为主从复制;
  2. 在主从分离的情况下,我们如何屏蔽主从分离带来的访问数据库方式的变化,让开发同
    学像是在使用单一数据库一样。
主从复制

我先以 MySQL 为例介绍一下主从复制。

MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所有变化并以二进制形
式保存在磁盘上二进制日志文件。主从复制就是将 binlog 中的数据从主库传输到从库上,
一般这个过程是异步的,即主库上的操作不会等待 binlog 同步的完成。

主从复制的过程是这样的:首先从库在连接到主节点时会创建一个 IO 线程,用以请求主库
更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件中,而主
库也会创建一个 log dump 线程来发送 binlog 给从库;同时,从库还会创建一个 SQL 线
程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较
常见的主从复制方式。

在这个方案中,使用独立的 log dump 线程是一种异步的方式,可以避免对主库的主体更
新流程产生影响,而从库在接收到信息后并不是写入从库的存储中,是写入一个 relay
log,是避免写入从库实际存储会比较耗时,最终造成从库和主库延迟变长。
主从复制
你会发现,基于性能的考虑,主库的写入流程并没有等待主从同步完成就会返回结果,那么
在极端的情况下,比如说主库上 binlog 还没有来得及刷新到磁盘上就出现了磁盘损坏或者
机器掉电,就会导致 binlog 的丢失,最终造成主从数据的不一致。不过,这种情况出现的
概率很低,对于互联网的项目来说是可以容忍的。

做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求
会锁表或者锁记录,也不会影响到读请求的执行。同时呢,在读流量比较大的情况下,我们
可以部署多个从库共同承担读流量,这就是所说的“一主多从”部署方式,在你的垂直电商
项目中就可以通过这种方式来抵御较高的并发读流量。另外,从库也可以当成一个备库来使
用,以避免主库故障导致数据丢失。

那么你可能会说,是不是我无限制地增加从库的数量就可以抵抗大量的并发呢?实际上并不
是的。因为随着从库数量增加,从库连接上来的 IO 线程比较多,主库也需要创建同样多的
log dump 线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带
宽,所以在实际使用中,一般一个主库最多挂 3~5 个从库。

**当然,主从复制也有一些缺陷,**除了带来了部署上的复杂度,还有就是会带来一定的主从同
步的延迟,这种延迟有时候会对业务产生一定的影响,我举个例子你就明白了。

在发微博的过程中会有些同步的操作,像是更新数据库的操作,也有一些异步的操作,比如
说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的 ID 写入消息队
列,再由队列处理机依据 ID 在从库中获取微博信息再发送给审核系统。此时如果主从数据
库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。
主从
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,纯粹以上面的例子来
说,我就有三种解决方案:

**第一种方案是数据的冗余。**你可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处
理机需要的所有微博信息,借此避免从数据库中重新查询数据。

**第二种方案是使用缓存。**我可以在同步写数据库的同时,也把微博的数据写入到
Memcached 缓存里面,这样队列处理机在获取微博信息的时候会优先查询缓存,这样也
可以保证数据的一致性。

**最后一种方案是查询主库。**我可以在队列处理机中不查询从库而改为查询主库。不过,这种
方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对
主库造成比较大的压力。

如何访问数据库

我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库读写的分离,这
时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要
使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,如果结合下一节课
中要讲解的内容“分库分表”,复杂度会提升更多。为了降低实现的复杂度,业界涌现了很
多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。

第一类以淘宝的 TDDL( Taobao Distributed Data Layer)为代表,以代码形式内嵌运行
在应用程序内部。你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个
数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将
SQL 语句发给某一个指定的数据源来处理,然后将处理结果返回。

这一类中间件的优点是简单易用,没有多余的部署成本,因为它是植入到应用程序内部,与
应用程序一同运行的,所以比较适合运维能力较弱的小团队使用;缺点是缺乏多语言的支
持,目前业界这一类的主流方案除了 TDDL,还有早期的网易 DDB,它们都是 Java 语言开
发的,无法支持其他的语言。另外,版本升级也依赖使用方更新,比较困难。

另一类是单独部署的代理层方案,这一类方案代表比较多,如早期阿里巴巴开源的
Cobar,基于 Cobar 开发出来的 Mycat,360 开源的 Atlas,美团开源的基于 Atlas 开发
的 DBProxy 等等。

这一类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上
它内部管理着很多的数据源,当有数据库请求时,它会对 SQL 语句做必要的改写,然后发
往指定的数据源。
它一般使用标准的 MySQL 通信协议,所以可以很好地支持多语言。由于它是独立部署
的,所以也比较方便进行维护升级,比较适合有一定运维能力的大中型团队使用。它的缺陷
是所有的 SQL 语句都需要跨两次网络:从应用到代理层和从代理层到数据源,所以在性能
上会有一些损耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值