本文主要是介绍一些在技术范畴内,经常看到的一些关键字,一些话术

关键字

架构

量毛刺

写扩散

把写路径延长一些,读路径缩短一些 , 目的提`升读性能` 、反规范化

写扩散的缺陷治理

  • 实时性差

写扩散虽然对于读取更为有利,但是写的性能也不能太差,所以冗余数据的写入常常是异步的。这就导致写者写完后,读者无法立马读到。这在分布式系统中叫做 读自己写(read-own-write) 问题。
不过这对于大多数应用来说,这都不是什么问题,可以通过下面手段解决:
首先用户写的数据大多不是给自己看,比如推文是给粉丝看的,用户很难发觉其中的延迟;
其次,即使写入的数据是给自己看的,也可以在用户提交完数据后,给用户一个完成页面,一定要用户手动点击退出,才能看到自己写入的数据。比如每次在淘宝提交订单后,都会弹出一个 “订单已完成” 页面,要先点击退出,才能看到订单列表;

  • 数据一致性

基本上所有大规模应用都会碰到的问题,但是大家又不得不都做一遍这些老生常谈的保障:

  1. 数据对账
  2. 定期全量刷新,纠正增量链路中可能存在的无法核对的错误
  3. 冗余数据无法写入时(比如数据源故障),记录错误日志,并实时同步到 odps,等到数据源恢复后根据日志再重新同步。
  • 无效数据过多
    上面的案例中,写扩散总是从用户维度切入,给每个用户保存一份数据。不禁让人困惑,这数据量会不会太大了?而且大多数用户根本就不会进入这个页面,所以写入的大多数数据都是无效的。
    优化这两个问题的方法常被称做 “读写结合”,本质就是在部分场景采用读扩散减少数据冗余,举几个例子:
    1. 对于钉钉审批首页案例,我们可以做好用户分层。对于审批单模板数量较小的企业,还是采用读扩散,只有在审批单数据达到一定规模后,才触发写扩散的方案。”写扩散” 就变成了一个针对大客户的 “高端服务方案”,甚至可以引导用户付费购买超额的模板数量。
    2. 钉钉视频号中的大 V 拥有几十万的粉丝,大 V 一旦发一条视频,系统就需要在所有粉丝收件箱中推送这条记录,造成了巨大的延迟和系统压力。一个优化方案是,只给高活用户收件箱进行推送(写扩散),普通用户等在下一次访问时,才即时构建收件箱(读扩散),这样就能大大减少写入的无效数据。

读扩展

把读路径延长一点,写路径缩短一些 , 目的提`升写性能` 、规范化

从读写扩散看业务发展的三阶段

img.png
第一阶段:业务刚刚启动,还处于探索与试错期。应用倾向于使用读扩散方案快速迭代试错。

第二阶段:业务已经确定可行,很快就进入了快速规模增长期。之前快速迭代留下的坑,都随着规模增长一一暴露。之前读扩散的方案已经很难满足业务要求,架构治理迫在眉睫,开发者们就会使用各种写扩散的技术优化性能,以支持快速增长的规模。

第三阶段:业务的规模已经达到天花板,进入 业务瓶颈期。之前因为规模和营收还在快速增长,所以写扩散带来的存储成本,看起来没什么。但是到了业务瓶颈期,写扩散带来的成本,已经没法带来相应的规模增长了。此时很多开发者们的 OKR 都会变成 “降成本”, 读扩散的方案因为存储成本低,在很多场景又会被重新提出,最终变成 “读写混合” 的方案。

稳定性设计

  • 数据对账

网络

  • 背压策略

写在最后

后续有新的关键字会陆陆续续补充

架构的核心是你的思维、模型思维,重点不是画那一堆框图,图只是一种表达方式。图画的再漂亮,不理解基层逻辑,面对的情况一旦变化,还是一抹黑。

架构的关键在于实践,对于各种架构类培训,要擦亮眼睛,正确评估,再出血。很多好的培训是不适合刚从事这个岗位的人去参加的。在实践中多思考,解决一个车间的问题,要从一个工厂的角度去统筹,真正遇上难题了,也一般不是培训能解决的,就需要专业的咨询服务,只有共同研究,共同创新才能解决难题。

  1. 首先我们需要明确的一点,就是你的业务架构是站在那个层级的,比如,如果是公司级的,面向客户的业务场景,那么我们就比较清楚,企业与客户之间的一个完整业务交互生命周期场景应该如何去定义了;
  2. 其次,一定要非常清楚知道业务架构包含几层含义:一般情况下,我们理解企业的业务架构,其实包含三个互相垂直的领域:执行层-管理层-治理层;执行层就是具体的完成实际的业务目的需要实施的各种业务中间状态,比如:完成销售场景,就是从客户察看商品信息,选择商品内容,配置其他附属商品,选择优惠方式,配送方式,支付方式,收获方式,订单生成,备货,打包,出仓,运输,收货等等一系列过程;而管理层更多是需要其中如何决策,如何处理非标需求等;治理则是解决相关的问题而实施的处理过程,比如:新的营销,新的流程场景等;我们一般在定义业务架构的时候,重点解决的是执行层的业务架构;
  3. 理解业务架构的逻辑含义:一般来说,相对实际的企业形态,我们把业务架构从逻辑上划分为2层:就是在你要确定的主流程场景下,有那些需要使用的基础能力,就像上面的例子一样;其次就是核心的主流程,这个是我们真正的诉求;
  4. 业务架构的层次理解:这个应该很多人没有仔细想过,业务架构层次应该包含几层,从最基本的解读出发,业务架构的层次包含最小三层:
    • 第一层:站在客户的角度,为了完成一次确定的交易,会经历一个完整的生命周期,这个之前大家都探讨过的:MTL-LTO-OTC-CTD-DTS等,也就是需要经历这样一个完整的过程,才能叫做达成一次交易;
    • 第二层:基于上面的完整过程,其中的每一个阶段都需要客户和企业之间进行一个或者多个流程交互,每个流程都是互不相同,并且需要基于不同的基础能力单元来完成;
    • 第三层:对于第二层的每一个流程,其中都有很多环节,每个环节涉及到具体的能力单元进行处理的时候,实际上也是一系列不同的流程,这个流程是二阶流程环节与能力单元之间的交互;
    • 有了这三个层次,基本上业务架构关系就比较清楚了(当然也只限于业务在生产状态的结构关系);
  5. 还要考虑的就是要把治理的生命周期纳入进来,就是说,我们上面实际的业务体系,站在企业的角度,也是有一个完整的生命周期来规划,设计,实现,上线,退服等一系列操作;这个也一样都是一系列的流程;
  6. 在具体的业务主场景架构内,站在企业的角度,我们每次上线一个线的业务实例,同样都需要经历规划,计划,方案,落地,准备,上线,运营,下线的各个阶段,才能让具体的业务对象实现与客户的交易;
  7. 上面阐述的业务架构,其实是一个完整的体系,站在企业的角度,就是如何把一个具体的业务真正从发起,到最终下线的全过程和内容都确定了一遍,因此,在画业务架构图的时候,最好是采用总分,阶段,层次的方式用多张图来表达,每张图阐述一个业务结论,最好不要用一张图试图讲所有的东西,这个做不到。

介绍

架构模式是对给定上下文的软件架构中常见问题的一种通用的可复用的解决方案。一种模式就是特定上下文的问题的一种解决方案

架构模式

  • 分层架构
  • 多层架构
  • 管道 - 过滤器架构
  • 客户端 - 服务器架构
  • 模型 - 视图 - 控制器架构
  • 事件驱动架构
  • 微服务架构

分层架构

最常见的架构模式就是分层架构或者称为 n 层架构。大部分软件架构师、设计师和开发者都对这个架构模式非常熟悉。尽管对于层的数量和类型没有具体限制,但大部分分层架构主要由四层组成:展现层、业务层、持久层和数据库层,如下图所示。

img.png

模式解析

上下文 所有复杂的系统都会经历独立地发展和衍化系统各个部分的需要。出于这个原因,系统开发者需要对关注点进行清晰且条理分明的分离,以便系统的各个模块可以独立地开发和维护。

问题 软件需要以这样一种方式分割:各个模块可以独自开发和衍化,各自部分之间的交互非常少,支持可移植性、可修改性和复用性。

方案 为了实现关注点分离,分层模式将软件分割成各个单元(称为“层”)。每一层都是一组模块,提供了一组高内聚的服务。其使用必须是单向的。层将一组软件作为一个完整的分区,每个分区暴露一个公开接口。

  • 第一个概念是,每一层都有特定的角色和职责。例如,展现层负责处理所有的用户界面。分层架构的这种关注点分离,让构建高效的角色和职责非常简单。
  • 第二个概念是,分层架构模式是一个技术性的分区架构,而非一个领域性的分区架构。它们是由组件组成的,而不是领域。
  • 最后一个概念是,分层架构中的每一层都被标记为封闭或者开放。封闭层意味着请求从一层移到另一层,它必须通过它正下面的这一层才能达到下面这一层的再下一层。请求不能跳过任何层。
    封闭层和请求访问

弱点 分层会导致性能下降。这种模式不适合高性能应用程序,因为经过架构中的多层来实现一个业务请求的效率是不高的。分层还会增加系统的前期成本和复杂性。

用途 我们应该将这种方式应用于小型简单的应用程序或网站。对于预算和时间非常紧张的场景,这是一个不错的选择。

管道-过滤器架构

软件架构中反复出现的一种模式是管道 - 过滤器(pipe-filter)模式

img_1.png

模式解析

上下文

许多系统需要转换从输入到输出的离散数据流。许多类型转换在实践中重复出现,因此将其创建成独立的可复用的部分,这是比较理想的。

问题

这些系统需要被分割成可复用的松耦合的组件,组件之间拥有简单通用的交互机制。这样它们就可以灵活地相互结合。这些通用松耦合的组件就很容易复用。那些独立的组件可以并行执行。

方案

这种架构中的管道构成了过滤器之间的通信通道。第一个概念是,由于性能原因,每个管道都是非定向的和点对点的,接受来自一个源的输入并经常直接输出到另外一个源。在这种模式中,有如下四种过滤器。

producer(source):一个过程的起点。
transformer (map):对一些或所有数据进行转换。
tester (reduce):测试一个或多个条件。
consumer (sink):终点。

弱点

不太适合交互性的系统,因为它们的转换特性。过多的解析和反解析会导致性能损失,也会增加编写过滤器本身的复杂性。

用途

管道 - 过滤器架构用于各种应用程序,特别是简化单项处理的任务,例如 EDI、ETL 工具。编译器:连续的过滤器执行词法分析、语法分析、语义分析和代码生成。

客户端-过滤器架构

img_2.png

模式解析

上下文

有许多共享资源和服务是大量分布式的客户端希望访问的,我们希望控制访问或服务质量。

问题

通过管理一组共享资源和服务,我们可以通过分解公共服务并在单个位置或少数位置进行修改来提高可修改性和复用性。我们想要通过在将资源本身分布在多个物理服务器上的同时集中控制这些资源和服务,来提高可伸缩性和可用性。

方案

在客户端 - 服务器模式中,组件和连接器具有特定的行为。称为“客户端”的组件将请求发送到称为“服务器”的组件,然后等待回复。服务器组件接收到客户端的请求并向其发送回复。

弱点

服务器会成为性能瓶颈和单点故障位置。在系统建成后,关于功能位置(在客户端还是在服务器)的决策通常是复杂的而且变动成本很大。

用途

对于有许多组件(客户端)发送请求到另外一些提供服务的组件(服务器)的系统,我们可以使用客户端 - 服务器模式来建模这个系统的一部分:在线应用程序,例如电子邮件、共享文档或银行服务。

模型-视图-控制器架构(MVC)

img_3.png

模式解析

上下文

用户界面通常是一个交互性应用程序的最频繁被修改的部分。用户通常希望从不同的视角查看数据,例如柱状图或者饼图。这些表示形式都应该反映数据当前的状态。

问题

用户界面功能如何独立于应用程序功能,同时还还对用户输入或底层应用程序数据的更改做出响应?当底层应用程序数据更改时,如何创建、维护和协调用户界面的多个视图?

方案

模型 - 视图 - 控制器(model-view-controller,即 MVC)模式将应用程序功能分为以下三种类型的组件:

模型,包含应用程序的数据。
视图,显示部分底层数据并与用户交互。
控制器,在模型和视图之间进行中介并管理状态更改的通知。

弱点

对于简单的用户界面,其复杂性并不值得这么做。模型、视图和控制器抽象可能不适用于某些用户界面工具包。

用途

MVC 是网站或移动应用程序开发用户界面常用的一种架构模式。

事件驱动架构

img_4.png

模式解析

上下文

需要提供计算和信息资源来处理传入的应用程序生成的独立异步事件,这种方式可以随着需求的增加而扩展。

问题

构建分布式系统,这个系统可以服务异步到达的事件相关信息,并且能从简单小型扩展到复杂大型。

方案

为事件处理部署独立的事件进程或处理器。到达的事件进入队列。调度程序根据调度策略从队列中拉取事件并将它们分配到合适的事件处理器。

弱点

性能和错误恢复可能是问题。

用途

使用这个方案的电商应用程序将工作如下:Order Service 创建一个 Order,这个订单处于待定状态,然后发布一个OrderCreated事件。

  • Customer Service 接收到这个事件并尝试为这个 Order 扣除信用。然后发布一个 Credit Reserved 事件或者CreditLimitExceeded(超出信用限额)事件。
  • Order Service 接收到 Customer Service 发送的事件并将订单状态更改为已核准或已取消。

微服务架构

img_5.png

模式解析

上下文

部署基于服务器的企业应用程序,支持各种浏览器和原生移动客户端。应用程序通过执行业务逻辑、访问数据库、与其它系统交换信息并返回响应来处理客户端请求。这个应用程序可能会暴露一个第三方 API。

问题

一体化应用程序会变得过于庞大和复杂,无法得到有效支持和部署来实现最优的分布式资源利用,例如在云环境中。

方案

将应用程序构建成服务套件。每个服务都是独立部署和可扩展的,拥有自己的 API 边界。不同的服务可以用不同的编程语言编写,管理它们自己的数据库,由不同的团队开发。

弱点

系统设计必须能容忍服务失败,需要更多的系统监控。服务编排和事件协作开销比较大。当然,我们还需要更多钱。

用途

许多使用场景都可以应用微服务架构,特别是那些涉及大量数据管道的场景。例如,一个微服务系统对关于一个公司的零售店销售的报表系统会比较理想。数据展现过程的每一步都会被一个微服务处理:数据收集、清理、规范化、浓缩、聚合、报告等。

本文介绍的实现方式属于应用级限制,应用级限流方式只是单应用内的请求限流,不能进行全局限流。要保证系统的抗压能力,限流是一个必不可少的环节,虽然可能会造成某些用户的请求被丢弃,但相比于突发流量造成的系统宕机来说,这些损失一般都在可以接受的范围之内。

限流

为什么要进行限流?

1.瞬时流量过高,服务被压垮?

2.恶意用户高频光顾,导致服务器宕机?

3.消息消费过快,导致数据库压力过大,性能下降甚至崩溃?

什么是限流?

限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。

在高并发系统中,出于系统保护角度考虑,通常会对流量进行限流。

在分布式系统中,高并发场景下,为了防止系统因突然的流量激增而导致的崩溃,同时保证服务的高可用性和稳定性,限流是最常用的手段。

有哪些限流算法?

常见的四种限流算法,分别是:固定窗口算法、滑动窗口算法、漏桶算法、令牌桶算法。

固定窗口

固定窗口又称固定窗口(又称计数器算法,Fixed Window)限流算法,是最简单的限流算法。

实现原理:

在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。如图所示,我们要求3秒内的请求不要超过150次:

img_1.png

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class FixedWindowRateLimiter {
Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class);
//时间窗口大小,单位毫秒
long windowSize;
//允许通过的请求数
int maxRequestCount;
//当前窗口通过的请求数
AtomicInteger counter = new AtomicInteger(0);
//窗口右边界
long windowBorder;
public FixedWindowRateLimiter(long windowSize, int maxRequestCount) {
this.windowSize = windowSize;
this.maxRequestCount = maxRequestCount;
this.windowBorder = System.currentTimeMillis() + windowSize;
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if (windowBorder < currentTime) {
logger.info("window reset");
do {
windowBorder += windowSize;
} while (windowBorder < currentTime);
counter = new AtomicInteger(0);
}

if (counter.intValue() < maxRequestCount) {
counter.incrementAndGet();
logger.info("tryAcquire success");
return true;
} else {
logger.info("tryAcquire fail");
return false;
}
}
}

优缺点

优点:实现简单,容易理解

缺点:

  • 限流不够平滑。例如:限流是每秒3个,在第一毫秒发送了3个请求,达到限流,窗口剩余时间的请求都将会被拒绝,体验不好。

  • 无法处理窗口边界问题。因为是在某个时间窗口内进行流量控制,所以可能会出现窗口边界效应,即在时间窗口的边界处可能会有大量的请求被允许通过,从而导致突发流量。即:如果第2到3秒内产生了150次请求,而第3到4秒内产生了150次请求,那么其实在第2秒到第4秒这两秒内,就已经发生了300次请求了,远远大于我们要求的3秒内的请求不要超过150次这个限制,如下图所示:

img_2.png

滑动窗口

滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求。在滑动窗口算法中,窗口的起止时间是动态的,窗口的大小固定。这种算法能够较好地处理窗口边界问题,但是实现相对复杂,需要记录每个请求的时间戳。

实现原理

滑动窗口在固定窗口的基础上,将时间窗口进行了更精细的分片,将一个窗口分为若干个等份的小窗口,每次仅滑动一小块的时间。每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阈值。其中,Sentinel就是采用滑动窗口算法来实现限流的。如图所示:
img_4.png

核心步骤:

  • 把3秒钟划分为3个小窗,每个小窗限制请求不能超过50秒。
  • 比如我们设置,3秒内不能超过150个请求,那么这个窗口就可以容纳3个小窗,并且随着时间推移,往前滑动。每次请求过来后,都要统计滑动窗口内所有小窗的请求总量。



代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class SlidingWindowRateLimiter {
Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class);
//时间窗口大小,单位毫秒
long windowSize;
//分片窗口数
int shardNum;
//允许通过的请求数
int maxRequestCount;
//各个窗口内请求计数
int[] shardRequestCount;
//请求总数
int totalCount;
//当前窗口下标
int shardId;
//每个小窗口大小,毫秒
long tinyWindowSize;
//窗口右边界
long windowBorder;

public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) {
this.windowSize = windowSize;
this.shardNum = shardNum;
this.maxRequestCount = maxRequestCount;
this.shardRequestCount = new int[shardNum];
this.tinyWindowSize = windowSize / shardNum;
this.windowBorder = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if (windowBorder < currentTime) {
logger.info("window reset");
do {
shardId = (++shardId) % shardNum;
totalCount -= shardRequestCount[shardId];
shardRequestCount[shardId] = 0;
windowBorder += tinyWindowSize;
} while (windowBorder < currentTime);
}

if (totalCount < maxRequestCount) {
logger.info("tryAcquire success:{}", shardId);
shardRequestCount[shardId]++;
totalCount++;
return true;
} else {
logger.info("tryAcquire fail");
return false;
}
}
}

优缺点

优点:解决了固定窗口算法的窗口边界问题,避免突发流量压垮服务器。

缺点:还是存在限流不够平滑的问题。例如:限流是每秒3个,在第一毫秒发送了3个请求,达到限流,剩余窗口时间的请求都将会被拒绝,体验不好。

漏桶算法

实现原理

漏桶限流算法是一种常用的流量整形(Traffic Shaping)和流量控制(Traffic Policing)的算法,它可以有效地控制数据的传输速率以及防止网络拥塞。

主要的作用:

a.控制数据注入网络的速度。

b.平滑网络上的突发流量
实现原理:
漏桶是一个很形象的比喻,外部请求就像是水一样不断注入水桶中,而水桶已经设置好了最大出水速率,漏桶会以这个速率匀速放行请求,而当水超过桶的最大容量后则被丢弃。不管上面的水流速度有多块,漏桶水滴的流出速度始终保持不变。消息中间件就采用的漏桶限流的思想。如图所示:

img_5.png

核心步骤:

a.一个固定容量的漏桶,按照固定速率出水(处理请求);

b.当流入水(请求数量)的速度过大会直接溢出(请求数量超过限制则直接拒绝)。

c.桶里的水(请求)不够则无法出水(桶内没有请求则不处理)。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class LeakyBucketRateLimiter {
Logger logger = LoggerFactory.getLogger(LeakyBucketRateLimiter.class);
//桶的容量
int capacity;
//桶中现存水量
AtomicInteger water = new AtomicInteger();
//开始漏水时间
long leakTimestamp;
//水流出的速率,即每秒允许通过的请求数
int leakRate;

public LeakyBucketRateLimiter(int capacity, int leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
}

public synchronized boolean tryAcquire() {
//桶中没有水, 重新开始计算
if (water.get() == 0) {
logger.info("start leaking");
leakTimestamp = System.currentTimeMillis();
water.incrementAndGet();
return water.get() < capacity;
}
//先漏水,计算剩余水量
long currentTime = System.currentTimeMillis();
int leakedWater = (int) ((currentTime - leakTimestamp) / 1000 * leakRate);
logger.info("lastTime:{}, currentTime:{}. LeakedWater:{}", leakTimestamp, currentTime, leakedWater);
//可能时间不足,则先不漏水
if (leakedWater != 0) {
int leftWater = water.get() - leakedWater;
//可能水已漏光。设为0
water.set(Math.max(0, leftWater));
leakTimestamp = System.currentTimeMillis();
}
logger.info("剩余容量:{}", capacity - water.get());
if (water.get() < capacity) {
logger.info("tryAcquire sucess");
water.incrementAndGet();
return true;
} else {
logger.info("tryAcquire fail");
return false;
}
}
}

优缺点

优点:

  • 平滑流量。由于漏桶算法以固定的速率处理请求,可以有效地平滑和整形流量,避免流量的突发和波动(类似于消息队列的削峰填谷的作用)。

  • 防止过载。当流入的请求超过桶的容量时,可以直接丢弃请求,防止系统过载。

缺点:

  • 无法处理突发流量:由于漏桶的出口速度是固定的,无法处理突发流量。例如,即使在流量较小的时候,也无法以更快的速度处理请求。

  • 可能会丢失数据:如果入口流量过大,超过了桶的容量,那么就需要丢弃部分请求。在一些不能接受丢失请求的场景中,这可能是一个问题。

  • 不适合速率变化大的场景:如果速率变化大,或者需要动态调整速率,那么漏桶算法就无法满足需求。

  • 资源利用率:不管当前系统的负载压力如何,所有请求都得进行排队,即使此时服务器的负载处于相对空闲的状态,这样会造成系统资源的浪费。
    由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少。



令牌算法

实现原理

令牌桶算法是基于漏桶算法的一种改进,主要在于令牌桶算法能够在限制服务调用的平均速率的同时,还能够允许一定程度内的突发调用。

实现原理:

  • 系统以固定的速率向桶中添加令牌;

  • 当有请求到来时,会尝试从桶中移除一个令牌,如果桶中有足够的令牌,则请求可以被处理或数据包可以被发送;

  • 如果桶中没有令牌,那么请求将被拒绝;

  • 桶中的令牌数不能超过桶的容量,如果新生成的令牌超过了桶的容量,那么新的令牌会被丢弃。

  • 令牌桶算法的一个重要特性是,它能够应对突发流量。当桶中有足够的令牌时,可以一次性处理多个请求,这对于需要处理突发流量的应用场景非常有用。但是又不会无限制的增加处理速率导致压垮服务器,因为桶内令牌数量是有限制的。
    如图所示:

img_6.png

代码实现

Guava中的RateLimiter就是基于令牌桶实现的,可以直接拿来使用。

优缺点

优点:

  • 可以处理突发流量:令牌桶算法可以处理突发流量。当桶满时,能够以最大速度处理请求。这对于需要处理突发流量的应用场景非常有用。

  • 限制平均速率:在长期运行中,数据的传输率会被限制在预定义的平均速率(即生成令牌的速率)。

  • 灵活性:与漏桶算法相比,令牌桶算法提供了更大的灵活性。例如,可以动态地调整生成令牌的速率。

缺点:

  • 可能导致过载:如果令牌产生的速度过快,可能会导致大量的突发流量,这可能会使网络或服务过载。

  • 需要存储空间:令牌桶需要一定的存储空间来保存令牌,可能会导致内存资源的浪费。

  • 实现稍复杂:相比于计数器算法,令牌桶算法的实现稍微复杂一些。

应用实践

Guava中的RateLimiter就是基于令牌桶实现的,可以直接拿来使用。所有整个实践是基于Guava的应用。

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>

API直接使用

固定产生令牌

1
2
3
4
5
6
7
8
9
@Test
public void acquireTest() {
//每秒固定生成5个令牌
RateLimiter rateLimiter = RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double time = rateLimiter.acquire();
logger.info("等待时间:{}s", time);
}
}

结果:

img_7.png

可以看到,每200ms左右产生一个令牌并放行请求,也就是1秒放行5个请求,使用RateLimiter能够很好的实现单机的限流。

同时产生多个令牌

那么再回到我们前面提到的突发流量情况,令牌桶是怎么解决的呢?RateLimiter中引入了一个预消费的概念。

申请令牌的数量不同不会影响这个申请令牌这个动作本身的响应时间,acquire(1)和acquire(1000)这两个请求会消耗同样的时间返回结果,但是会影响下一个请求的响应时间。

如果一个消耗大量令牌的任务到达空闲的RateLimiter,会被立即批准执行,但是当下一个请求进来时,将会额外等待一段时间,用来支付前一个请求的时间成本。

至于为什么要这么做,通过举例来引申一下。当一个系统处于空闲状态时,突然来了1个需要消耗100个令牌的任务,那么白白等待100秒是毫无意义的浪费资源行为,那么可以先允许它执行,并对后续请求进行限流时间上的延长,以此来达到一个应对突发流量的效果。

1
2
3
4
5
6
7
8
9
@Test
public void acquireSmoothly() {
RateLimiter rateLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS);
long startTimeStamp = System.currentTimeMillis();
for (int i = 0; i < 15; i++) {
double time = rateLimiter.acquire();
logger.info("等待时间:{}s, 总时间:{}ms", time, System.currentTimeMillis() - startTimeStamp);
}
}

结果:

img_8.png

可以看到,令牌发放时间从最开始的500ms多逐渐缩短,在3秒后达到了200ms左右的匀速发放。

总的来说,基于令牌桶实现的RateLimiter功能还是非常强大的,在限流的基础上还可以把请求平均分散在各个时间段内,因此在单机情况下它是使用比较广泛的限流组件。

AOP切面

第一步:创建注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
// 资源主键
String key() default "";
//最多访问次数,代表请求总数量
double permitsPerSeconds();
// 时间:即timeout时间内,只允许有permitsPerSeconds个请求总数量访问,超过的将被限制不能访问
long timeout();
//时间类型
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
//提示信息
String msg() default "系统繁忙,请稍后重试";
}

第二步:AOP切面实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Aspect
@Component
public class LimitAspect {
Logger logger = LoggerFactory.getLogger(LimitAspect.class);
private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();

@Around("@annotation(com.alibaba.xxx.xxx.annotation.Limit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿limit的注解
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
// key作用:不同的接口,不同的流量控制
String key = limit.key();
RateLimiter rateLimiter;
//验证缓存是否有命中key
if (!limitMap.containsKey(key)) {
//创建令牌桶
rateLimiter = RateLimiter.create(limit.permitsPerSeconds());
limitMap.put(key, rateLimiter);
logger.info("新建了令牌桶={},容量={}", key, limit.permitsPerSeconds());
}
rateLimiter = limitMap.get(key);
//拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeUnit());
//拿不到令牌,直接返回异常信息
if (!acquire) {
logger.debug("令牌桶={},获取令牌失败", key);
throw new RuntimeException(limit.msg());
}
}
return joinPoint.proceed();
}
}

第三步:应用

1
@Limit(key = "query",permitsPerSeconds = 1,timeout = 1,msg = "触发接口限流,请重试")

第四步:使用位置详解
若是放在http的mapping接口上,返回如下

1
2
3
4
5
6
7
{
"timestamp": "2023-12-07 11:21:47",
"status": 500,
"error": "Internal Server Error",
"path": "/table/query"
}

若是放在service服务的接口上,返回如下

1
2
3
4
5
{
"code": -1,
"message": "触发接口限流,请重试",
"data": "fail"
}

总结

本文介绍的实现方式属于应用级限制,应用级限流方式只是单应用内的请求限流,不能进行全局限流。假设将应用部署到多台机器,我们需要分布式限流和接入层限流来解决这个问题。

总的来说,要保证系统的抗压能力,限流是一个必不可少的环节,虽然可能会造成某些用户的请求被丢弃,但相比于突发流量造成的系统宕机来说,这些损失一般都在可以接受的范围之内。前面也说过,限流可以结合熔断、降级一起使用,多管齐下,保证服务的可用性与健壮性。

什么是规则引擎?

在企业项目中,关键或核心部分总是业务逻辑或业务规则,也就是 CRUD,这些系统都有一个共同的特征是,某个模块中的一些或许多规则或策略总会发生变化,例如购物网站的顾客折扣、物流企业的运价计算等。随着这些变化而来的是大量的重复工作,如果系统没有足够的抽象,那么每当增加一种规则时,开发者需要在规则、回归测试、性能测试等方面的变化中编写代码。

对规则引擎的要求

  • 可视化编辑
  • 可倒推数量
  • 复原计算逻辑
  • 学习成本低,文档丰富

可视化的重要性

优点

可视化编辑可以看成一张思维导图,每个节点都能有下一个节点,并且节点上都有条件公式,这样就成了一个完整规则。普通人都看懂各个节点的关系。还可以导出图片,与业务人员进行沟通讨论。

倒推生产数量

比如 在制造业内,如纸箱制造,纸很容易损坏,要生产1000只,就要倒推工序,每个工序需要多少材料。而市面上的规则引擎都不能满足。

可视化复原计算逻辑过程

当价格计算特别复杂,客服过来让开发人员来解释这件商品为何这么低的价格时,作为开发人员,研究的是代码,又不研究价格,那怎么办?只得进行调试,一步步调试,查到原因来跟客服沟通。如果这种情况过多,开发就完全成为客服的工具人了。
有些项目会使用计算引擎,这种情况下,调试复原计算逻辑中,老难了。让人头痛的是断点。价格计算逻辑特别复杂时,通常公式套另一个公式,或者一个公式套着三四个公式,又因加了缓存数据加快运行速度这个逻辑,我们查了一个公式,用了一个断点,结果跑到另一个公式的条件检测上去了。当我们又想查另一个公式时,结果发现值已缓存,跳不进去了,被迫得进行第二次调试。
所以可视化复原很重要!!!


- 快速判断规则节点运行是否正确:
- 方便查询下一公式计算过程,以及可以看到哪个节点设置了这个公式
- 能看到节点的判断条件,及不符合条件。

总结

规则引擎核心组件有图形编辑器、代码编辑器、计算引擎。
现在开源的有很多种,但是有可视化界面的不多, 可以基于开源的进行二次开发, 增加人性化的规则配置页面。
如果只是单纯的运行表达式,直接用开源的就可以, 如果说是要给业务人员使用,需要有一个更贴合业务的配置页面,以及运行过程的展示等。

词法分析 -> 语法分析 -> 语义分析 -> 生成中间代码 -> 优化代码(消除二义性) -> 生成目标代码

词法规则

大小写不敏感 : 可以将词法规则中的每个字母都附上大小写

STRICT : [Ss][Tt][Rr][Ii][Cc][Tt] ;
GRAPH : [Gg][Rr][Aa][Pp][Hh] ;

examples/R.g4

| ‘{‘ exprlist’}’ // compound statement

| ‘if’ ‘(‘ expr ‘)’ expr

| ‘if’ ‘(‘ expr ‘)’ expr ‘else’ expr

| ‘for’ ‘(‘ ID ‘in’ expr ‘)’ expr

| ‘while’ ‘(‘ expr ‘)’ expr

| ‘repeat’ expr

| ‘?’ expr // get help on expr, usually string or ID

| ‘next’

| ‘break’

具体规则含义

() : 产生式组合
? : 产生式出现0或1次

  • : 0或多次
  • : 1或多次
    . : 任意一个字符
    ~ : 不出现后面的字符
    .. : 字符范围

参考资料

解析树vs语法树

大部分的资料中,都把Antlr4生成的树状结构,称为解析树或者是语法树,但是,如果我们细究的话,可能说成是解析树更加准确,因为Antlr4的结果,只是简单的文法解析,不能称之为语法树(语法树应该是能够体现出来语法特性的信息),如上面的那些问题,就很难在Antlr4生成的解析树上获取到。

所以,现在很多工具,基于Antlr4进行封装,然后进行了更进一步地处理,从而获取到了更加丰富的语法树,例如CheckStyle。因此,如果通过Antlr4解析语言简单使用,可以直接基于Antlr4的结果开发,但是如果要进行更加深入的处理,就需要对Antlr4的结果进行更进一步的处理,以更符合我们的使用习惯(例如,Java Parser格式的Java的AST,Clang格式的C/C++的AST),然后才能更好地在上面进行开发。

语法规则

// 1.注释
/**

  • 注释有 单行,多行,Javadoc注释 三种
    */
    // 语法声明,关键字 grammar
    // 不带前缀的语法声明是混合语法,可以同时包含词法规则和文法规则
    // 若要创建一份只允许文法规则出现的语法,使用声明 parser grammar Name; 纯词法的语法声明,使用 lexer grammar Name;

grammar ZC;
/*
多行注释
*/

// 2.标识符
// 词法符号名 和 词法规则名 以 大写字母开头
// 文法规则 以 小写字母开头
// 首字母之后的字符可以是大小写字符、数字和下划线

// 文法规则名
// expr 是 文法规则名
// : 表示后面是具体的文法规则
// 这个文法由 ID 词法 或 STRING 词法组成
expr : ID | STRING | ;

// 3 文法规则
// 3.1 语法分析器由一系列文法规则组成,这些规则既可以位于文法语法中,也可以位于混合语法中
// 规则中可以包含由|分隔的备选分支。
stat : retstat | ‘break’ ‘;’ | ‘continue’ ‘;’;
retstat : ID;

// 3.2 备选分支是一组可以为空的规则元素列表。例如,下列规则中的空备选分支使得整条规则成为了可选的
stat2 : retstat | ;

// 3.3 备选分支的标签
// 可以使用#给最外层的备选分支添加标签,以获得更加精确的语法分析器监听器事件
// 一条规则中的备选分支要么全部带上标签,要么全部不带标签
// ANTLR为每个标签生成一个规则上下文类
stat3 : ‘return’ e ‘;’ # Return
| ‘break’ ‘;’ # Break
;
e : e ‘*’ e # Mult
| e ‘+’ e # Add
| INT # Int
;

// 4.规则元素
// 规则元素指明了语法分析器在特定的时间需要完成的任务
// 规则元素可以是一条规则、一个词法符号 或者 一个字符串常量
// T 匹配词法符号T,词法符号以大写字母开头
// ‘str’ 匹配字符串常量
// r 匹配规则r,像函数一样调用该规则,文法规则以小写字母开头
// r[<>] 匹配r,并像函数调用一样传入一组参数
// {<>} 在备选元素之后,后一个备选元素之前执行的代码动作.
// {<

>}? 执行语义判定<

>.
// . 匹配任意除文件结束符之外的语法符号.
// ~(INT|ID)匹配除INT或ID之外的任意词法符号

// 5.子规则
// 一条规则可以包含称为子规则的备选分支块
// 子规则和规则相似,只是缺少名字并被包裹在圆括号内。在子规则的括号内,可以包含一个或者多个备选分支
// 存在四种类型的子规则(其中x、y、z代表语法元素)
// (x|y|z) 匹配该子规则内的任意备选分支 一次
// (x|y|z)? 匹配该子规则内的任意备选分支 一次 或 不匹配任何东西
// (x|y|z)* 匹配该子规则内的任意备选分支 0多次
// (x|y|z)+ 匹配该子规则内的任意备选分支 1
多次
// 在子规则仅包含一个备选分支时,可以忽略子规则两侧的括号

// 6.捕获异常
// 当在一条规则中发生语法错误时,ANTLR会捕获该异常,报告错误,并试图从中恢复(可能通过消费更多的词法符号来完成此过程),然后从规则中返回。
// 每条规则都包裹在一个try/catch/finally语句中

// 7.规则属性定义
// 与规则和动作相关的语法元素。规则可以像编程语言中的函数一样,包含参数、返回值以及局部变量
// ANTLR会将你定义的所有变量收集起来并存储到规则上下文对象中

// 8.起始规则和文件结束符

// 词法符号 和 词法规则名
// 词法的语法 由 词法规则组成,并且可被分解为多个模式
// 词法规则的定义方式和文法规则非常相似,除了一些例外:词法规则不能包含参数、返回值或者局部变量。
// 词法规则名必须以大写字母开头,以和文法规则名区分开。

// ID 词法 表示: 只要不是 逗号 \n \t 中的一个字符就行
// 1.词法模式
// 词法模式 允许你将词法规则按照上下文分组
// 2.词法规则元素
// 词法规则元素总结
// ‘str’ 匹配指定的字符或字符序列,如’while’或’=’
// [char set]匹配字符集中的一个字符.如 [x-y]从x到y的字符集合(包含x和y). \n,\r,\b,\t,,-, Unicode字符.
// 3.递归词法规则
// ANTLR词法规则可以是递归的

ID : [,\n\r”]+ ;
// STRING词法
STRING : ‘“‘ (‘“”‘|
‘“‘)* ‘“‘ ;
INT: ‘1’;

// antlr关键字: import、fragment、lexer、parser、grammar、returns、locals、throws、catch、finally、mode、options、tokens

// 1.语法导入
// 语法导入允许你将语法分解成可复用的逻辑单元
// 一个语法会从其导入的语法中继承所有的规则、词法符号声明和具名的动作
// 位于“主语法”中的规则将会覆盖其导入的语法中的规则,以此来实现继承机制
// 可以将import看作是一种智能的、不会引入本文件中已经定义过的规则的引入语句(include statement)
// 在处理一份主语法的过程中,ANTLR工具将所有被导入的语法加载到一起,然后将其中的规则、词法符号类型以及具名动作合并到主语法中

// 2.词法符号声明
// tokens区域存在的意义在于,它定义了一份语法所需,但却未在本语法中列出对应规则的词法符号

// 文法规则
// 语法分析器由一系列文法规则组成,这些规则既可以位于文法语法中,也可以位于混合语法中
// Java程序通过调用ANTLR自动生成的、与预期的起始规则相对应的函数来启动语法分析器
// 规则最基本的形式是规则名后面紧接着一个备选分支,然后是一个分号
expr2 : ID | STRING | ;

什么是事件驱动架构

事件驱动架构是一种松耦合、分布式的驱动架构,收集到某应用产生的事件后实时对事件采取必要的处理后路由至下游系统,无需等待系统响应。使用事件总线EventBridge可以构建各种简单或复杂的事件驱动架构,以标准化的CloudEvents 1.0协议连接云产品和应用、应用和应用等。
事件驱动架构体系结构具备以下三个能力:

事件收集:负责收集各种应用发生的事件,如新建订单,退换货订单等其他状态变更。
事件处理:对事件进行脱敏处理,并对事件进行初步的过滤和筛选。
事件路由:分析事件内容并将事件路由分发至下游产品。

优势

事件驱动架构具有以下优势:

降低耦合

降低事件生产者和订阅者的耦合性。事件生产者只需关注事件的发生,无需关注事件如何处理以及被分发给哪些订阅者。任何一个环节出现故障,不会影响其他业务正常运行。
异步执行

事件驱动架构适用于异步场景,即便是需求高峰期,收集各种来源的事件后保留在事件总线中,然后逐步分发传递事件,不会造成系统拥塞或资源过剩的情况。
可扩展性

事件驱动架构中路由和过滤能力支持划分服务,便于扩展和路由分发。
敏捷性

事件驱动架构支持与各种阿里云产品和应用集成,支持事件路由至任何系统服务,提供各种敏捷高效的部署方案。

事件驱动架构图

下图是人力资源服务系统的事件驱动架构示例,事件总线EventBridge收集人力资源服务系统产生的新员工入职事件,并对此事件进行路由和转发。这种体系结构可以提高站点的可扩展性,同时能更轻便的应对企业架构升级和系统拓展。

什么时候使用事件驱动架构

如果盲目使用事件驱动设计架构,就有可能要承担中断业务逻辑的风险,因为这些业务逻辑具有概念上的高度内聚,却采用了解耦机制将它们联系在一起。换句话说,就是将原本需要组织在一起的代码强行分离,并且这样难于定位处理流程,还有数据一致性保证等问题。为了防止我们的代码变成一堆复杂的逻辑,我们应当在某些明确场景下使用事件驱动架构。以经验来讲,以下三 种场景可以使用事件驱动开发:

  • 组件的解耦
  • 执行异步任务(可以拆解, 分多个小事务完成, 非强一致性)
  • 跟踪状态的变化 (事件溯源)Event Sourcing Pattern

使用事件驱动的优点

  • 高内聚,低耦合
  • 架构更健壮。如果加入队列的事件能够在源组件中执行,但在其它组件中由于 bug 导致其无法执行(由于将其加入到队列任务中,它们可以在 bug 修复后再执行)。
  • 业务处理减少延迟。当用户无需等待所有的逻辑都执行完成时,可以将这类工作加入到事件队列。
  • 便于系统扩展,能够让组件的研发团队独立开发,加快项目进度、降低功能难度、减少问题发生并且更有组织性。
  • 将信息封装在“事件”里,便于系统内传播。

在享受便利的同时,会带来一些缺点

  • 数据一致性问题。由于流程依赖于最终的一致性,因此通常不支持ACID事务,
  • 因此重复或乱序事件的处理会使服务代码更加复杂,并且难以测试和调试所有情况。
  • 缺点和优点相对应,正是因为它提供了很好的解耦能力,我们会比较难通过阅读代码去得到整个系统和流程的全貌。因为这些逻辑之间的关系不再是之前的依赖关系。这将会是一个挑战。
    如果处理器占用时间较长,那会阻塞应用程序的响应。

事件承载状态转移

我们在使用事件通知时,事件里面往往不会包含下游系统处理这个事件需要的所有信息。比如当内容发生下架变更时,内容平台会生成一个“内容下架“的事件,但当下游系统处理这个事件时,往往还需要知道,该内容上个状态是什么,是谁触发下架等信息,才能完成后续处理。所以不可避免地,下游系统在处理这个事件时,往往还需要通过平台服务来获取这些额外信息。

为了解决这个问题,我们引入一个种新的模式,叫做“事件承载状态转移”。简单来说,就是让事件的消费方自己保留一份在业务处理过程中需要用到的上游系统的数据。比如让下游系统保留一份在处理内容状态变更事件时所需要用到的内容变更前的状态,避免回头去平台查询。

优点

架构更健壮。减少事件消费方对生产方的额外依赖(获取事件处理所需数据);

业务处理减少延迟。增加事件消费方系统的响应速度,因为不再需要调用平台API以获取事件处理所需数据;

无需担心被查询组件的负载(尤其是远程组件)。

缺点

尽管现在数据存储已经不再是问题根源,依然会保存多个只读的数据副本,一致性进一步被破坏;

增加数据处理的复杂度,即使处理逻辑符合规范,它也需要额外处理和维护外部数据的本地副本业务逻辑。

五、总结

GUI框架
I/O框架

主流场景下,传统面向服务(或以数据驱动)的平台存在系统性不足,需要增强以下能力:

在传统数据集成基础上需要进一步提升业务集成能力。

需要提高集成平台的业务敏捷性和反应能力。

需要进一步实现业务系统间的解耦和高可靠性。

需要进一步提升管控平台的实时响应能力。

”事件驱动架构“天然地满足了这些能力要求。事件驱动架构”天生“的优点,比如,封装、高内聚和低耦合,还可以提升代码的可维护性、性能和业务增长的需求,通过事件溯源模式,还能提高系统数据的可靠性。

不过,事件驱动同样存在弊端,因为无论是概念上的复杂度还是技术上的复杂度都增加了,当它被滥用时将导致灾难性的后果

https://blog.csdn.net/vivo_tech/article/details/122661076?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-4-122661076-blog-118662876.pc_relevant_aa&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-4-122661076-blog-118662876.pc_relevant_aa&utm_relevant_index=5

下一篇可能会写一篇java中使用内存屏障实现的语义

cache aside (旁路缓存)

  • 缓存数据只存在,添加 和 删除 , 不做更新
    容易出现缓存不一致情况 , 可以通过延时双删, 或者, 给缓存增加过期时间, 减少不一致的时间窗口

更新频繁的场景下会导致缓存频繁的被删除,降低了缓存的作用 写穿透 Write through

核心策略:以缓存为操作为主,数据存先存在于缓存,缓存的数据是不会过期的.

写穿透 Write through

用于读操作较多.实现简单.

Write Through:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,,果缓存中数据不存在,我们把这种情况叫做Write Miss(写失效),图中以Write allocate方式.

Read Through:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据.

前言

很久之前对Docker进行了简单的了解,在后续的工作中,公司也用到了Docker,本篇文章主要记录在生产实际使用中遇到的一些问题和解决方案

生产场景-问题列表

coreDns解析阿里云REDIS/hbase服务的域名时,总是出现解析超时或者会把redis/hbase的整体域名作为一个服务名

错误日志:

img_1.png
img_2.png

原因

是因为COREDNS在解析域名的时候比较复杂,网络抖动,可能会偶发超时问题, 下面是COREDNS解析日志如下
img.png
解析过程会尝试作为k8sservice处理

解决方案:

  • 在域名后面增加. 作为全限定域名,这样coredns在解析的时候,就不会把这个当做k8s service处理了 ,不会去组合各种解析场景,最后才作为域名去解析,这样可以提高coredns解析的效率, 解析效率高了,出现问题的机率会减小 但是对于网络抖动超时,无能为力
  • 咨询过阿里云,建议安装localDNS, 需要重启pod

避免两个非常重要的应用出现在同一个节点上

可以调整pod的亲和力配置

最佳实践

  • K8S节点配置推荐 cpu核数:内存 =1:4, 生产推荐 8C32G
  • pipeline
  • 阿里云环境使用容器服务,节点池节点,最佳实践,CPU与内存配置比例是1:8
    [1]: http://pic.albk.tech/docker-1-0.png

##前言

怎么配置tomcat,才能使得自己的服务效率更高呢?

  • 首先,这和tomcat的使用的IO模式有关
  • 其次,也和tomcat的配置参数有关

参数详解 maxConnections、maxThreads、acceptCount

一、accept-count:最大等待数

官方文档的说明为:当所有的请求处理线程都在使用时,所能接收的连接请求的队列的最大长度。当队列已满时,任何的连接请求都将被拒绝。accept-count的默认值为100。
详细的来说:当调用HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中,这个acceptCount就是指能够接受的最大等待数,默认100。如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)。

二、maxConnections:最大连接数

官方文档的说明为:

这个参数是指在同一时间,tomcat能够接受的最大连接数。对于Java的阻塞式BIO,默认值是maxthreads的值;如果在BIO模式使用定制的Executor执行器,默认值将是执行器中maxthreads的值。对于Java 新的NIO模式,maxConnections 默认值是10000。
对于windows上APR/native IO模式,maxConnections默认值为8192,这是出于性能原因,如果配置的值不是1024的倍数,maxConnections 的实际值将减少到1024的最大倍数。
如果设置为-1,则禁用maxconnections功能,表示不限制tomcat容器的连接数。
maxConnections和accept-count的关系为:当连接数达到最大值maxConnections后,系统会继续接收连接,但不会超过acceptCount的值。

三、maxThreads:最大线程数

每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,那么最大线程数决定了Web服务容器可以同时处理多少个请求。maxThreads默认200,肯定建议增加。但是,增加线程是有成本的,更多的线程,不仅仅会带来更多的线程上下文切换成本,而且意味着带来更多的内存消耗。JVM中默认情况下在创建新线程时会分配大小为1M的线程栈,所以,更多的线程异味着需要更多的内存。线程数的经验值为:1核2g内存为200,线程数经验值200;4核8g内存,线程数经验值800。

1.4.1 Tomcat的高效配置
明白了 Tomcat的maxConnections、maxThreads、acceptCount三大配置之后,可以通过application.yml配置文件来改变这个三个值,一个标准的示例如下:

1
2
3
4
5
6
7
8
9
10
11
server:
tomcat:
uri-encoding: UTF-8
#最大工作线程数,默认200, 4核8g内存,线程数经验值800
#操作系统做线程之间的切换调度是有系统开销的,所以不是越多越好。
max-threads: 1000
# 等待队列长度,默认100
accept-count: 1000
max-connections: 20000
# 最小工作空闲线程数,默认10, 适当增大一些,以便应对突然增长的访问量
min-spare-threads: 100

1.4.2 图解:maxConnections、maxThreads、acceptCount关系
maxConnections、maxThreads、acceptCount关系之间,具体的关系如何呢? 在疯狂创客圈社群中,有不少的同学对于这个问题是云里雾里的,并且多次进行求助。这里用一个形象的比喻,通俗易懂的解释一下tomcat的最大线程数(maxThreads)、最大等待数(acceptCount)和最大连接数(maxConnections)三者之间的关系。

我们可以把tomcat比做一个火锅店,流程是取号、入座、叫服务员,可以做一下三个形象的类比:

  • (1)acceptCount 最大等待数
    可以类比为火锅店的排号处能够容纳排号的最大数量;排号的数量不是无限制的,火锅店的排号到了一定数据量之后,服务往往会说:已经客满。
  • (2)maxConnections 最大连接数
    可以类比为火锅店的大堂的餐桌数量,也就是可以就餐的桌数。如果所有的桌子都已经坐满,则表示餐厅已满,已经达到了服务的数量上线,不能再有顾客进入餐厅了。
  • (3)maxThreads:最大线程数
    可以类比为厨师的个数。每一个厨师,在同一时刻,只能给一张餐桌炒菜,就像极了JVM中的一条线程。

整个就餐的流程,大致如下:

  • (1)取号:如果maxConnections连接数没有满,就不需要取号,因为还有空余的餐桌,直接被大堂服务员领上餐桌,点菜就餐即可。如果 maxConnections 连接数满了,但是取号人数没有达到 acceptCount,则取号成功。如果取号人数已达到acceptCount,则拿号失败,会得到Tomcat的Connection refused connect 的回复信息。
  • (2)上桌:如果有餐桌空出来了,表示maxConnections连接数没有满,排队的人,可以进入大堂上桌就餐。
  • (3)就餐:就餐需要厨师炒菜。厨师的数量,比顾客的数量,肯定会少一些。一个厨师一定需要给多张餐桌炒菜,如果就餐的人越多,厨师也会忙不过来。这时候就可以增加厨师,一增加到上限maxThreads的值,如果还是不够,只能是拖慢每一张餐桌的上菜速度,这种情况,就是大家常见的“上一道菜吃光了,下一道菜还没有上”尴尬场景。

img.png

作者:BK
https://www.jianshu.com/u/a5230c4f0b7a
鉴于本人才疏学浅,不足之处还望斧正,也欢迎关注我,无特殊说明的都是自己一字一句码出来的,尊重原创,如果转载请说明出处!

0%