微服务之流量控制

news2024/12/22 14:17:01

Informal Essay By English

I have been thinking about a question recently, what is the end of coding?
在这里插入图片描述

参考书籍:
“凤凰架构”

流量控制

任何一个系统的运算、存储、网络资源都不是无限的,当系统资源不足以支撑外部超过预期的突发流量时,便应该要有取舍,建立面对超额流量自我保护的机制,这个机制就是微服务中常说的“限流”

大家应该都经历过早期的 12306 系统在春节期间全国人民都上去抢票的结果是全国人民谁都买不上票的情况,出现这种情况的原因很大部分原因就是早期的12306系统并没有做好突发流量的控制导致的。为了避免这种状况出现,一个健壮的系统需要做到恰当的流量控制,更具体地说,需要妥善解决以下三个问题:

  • 依据什么限流?:要不要控制流量,要控制哪些流量,控制力度要有多大,等等这些操作都没法在系统设计阶段静态地给出确定的结论,必须根据系统此前一段时间的运行状况,甚至未来一段时间的预测情况来动态决定。
  • 具体如何限流?:解决系统具体是如何做到允许一部分请求能够通行,而另外一部分流量实行受控制的失败降级,这必须了解掌握常用的服务限流算法和设计模式。
  • 超额流量如何处理?:超额流量可以有不同的处理策略,也许会直接返回失败(如 429 Too Many Requests),或者被迫使它们进入降级逻辑,这种被称为否决式限流。也可能让请求排队等待,暂时阻塞一段时间后继续处理,这种被称为阻塞式限流

🔥了解更多

依据什么限流?

这个问题,其实是比较好理解的,举个生活中常见的例子:

洗澡用的热水器一个小时能加热0.5吨的水,如果只是你一个人使用,这个热水器是可以满足日常使用热水的需求的,但是如果突然有一天家里来了几个朋友,那么只靠一个热水器可能就不能满足所有人的需求(不考虑一起洗的情况🌝),假设你洗澡会使用0.2吨的水,朋友A洗澡需要使用0.2吨的水,朋友B日常洗澡需要0.3吨的水,显然光靠加热一个小时的热水器中的水是不足以满足所有的人洗澡的需求的,在保证大家的洗澡用量的前提下,需要根据热水器的容量和加热时间来合理的安排大家的洗澡顺序和时间来满足每个人对热水的需求(热水器有人在洗澡的时候是可以不断加热水的)

在编程世界里面,热水器可以类比是系统,人看作是流量。这里会有一个疑问,为什么会知道三个人需要合理安排洗澡时间和洗澡用水量才能满足各自的用水需求,理由当然的回答说热水器每小时加热水的容量无法满足各自洗澡的需求,那么在编程世界里面我们是何如来衡量一个系统的是否能满足突然大用户量的需求呢?一般根据以下三个指标的数值来决定(引用自《凤凰架构》,):

  • 每秒事务数(Transactions per Second,TPS):TPS 是衡量信息系统吞吐量的最终标准。“事务”可以理解为一个逻辑上具备原子性的业务操作。譬如你在 Fenix’s Bookstore(凤凰书店) 买了一本书,将要进行支付,“支付”就是一笔业务操作,支付无论成功还是不成功,这个操作在逻辑上是原子的,即逻辑上不可能让你买本书还成功支付了前面 200 页,又失败了后面 300 页。
  • 每秒请求数(Hits per Second,HPS):HPS 是指每秒从客户端发向服务端的请求数(请将 Hits 理解为 Requests 而不是 Clicks,国内某些翻译把它理解为“每秒点击数”多少有点望文生义的嫌疑)。如果只要一个请求就能完成一笔业务,那 HPS 与 TPS 是等价的,但在一些场景(尤其常见于网页中)里,一笔业务可能需要多次请求才能完成。譬如你在 Fenix’s Bookstore 买了一本书要进行支付,尽管逻辑上它是原子的,但技术实现上,除非你是直接在银行开的商城中购物能够直接扣款,否则这个操作就很难在一次请求里完成,总要经过显示支付二维码、扫码付款、校验支付是否成功等过程,中间不可避免地会发生多次请求。
  • 每秒查询数(Queries per Second,QPS):QPS 是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那 QPS 和 HPS 是等价的,但在分布式系统中,一个请求的响应往往要由后台多个服务节点共同协作来完成。譬如你在 Fenix’s Bookstore 买了一本书要进行支付,尽管扫描支付二维码时客户端只发送了一个请求,但这背后服务端很可能需要向仓储服务确认库存信息避免超卖、向支付服务发送指令划转货款、向用户服务修改用户的购物积分,等等,这里面每次内部访问都要消耗掉一次或多次查询数

目前,主流系统大多倾向使用 HPS 作为首选的限流指标,它是相对容易观察统计的,而且能够在一定程度上反应系统当前以及接下来一段时间的压力。但限流指标并不存在任何必须遵循的权威法则,根据系统的实际需要,哪怕完全不选择基于调用计数的指标都是有可能的。譬如下载、视频、直播等 I/O 密集型系统,往往会把每次请求和响应报文的大小,而不是调用次数作为限流指标,譬如只允许单位时间通过 100MB 的流量。又譬如网络游戏等基于长连接的应用,可能会把登陆用户数作为限流指标,热门的网游往往超过一定用户数就会让你在登陆前排队等候。

具体如何限流?

这个问题也比较好理解,还是拿上面洗澡的案例说明,在这个案例中,我们给每个人分配洗澡时间和用水量其实就是一种限流的手段。那么编程世界中我们大体有以下几种限流设计模式可以参考使用。

1.流量计数器模式

根据当前时刻的流量计数结果是否超过阈值来决定是否限流,比如:我们计算得出了某系统能承受的最大持续流量是 80 TPS,那就控制任何一秒内,发现超过 80 次业务请求就直接拒绝掉超额部分,代码也比较好实现,下面就是计数器模式的实现案例:

public class CountLimiter {
    private int requestCount;
    private long lastRefillTime;
    private long limitTime;
    private int requestLimit;

    public CountLimiter(int requestCount, int requestLimit, long lastRefillTime, long limitTime) {
        this.requestCount = requestCount;
        this.requestLimit = requestLimit;
        this.lastRefillTime = lastRefillTime;
        this.limitTime = limitTime;
    }

    public boolean isAllowed() {
        synchronized (this) {
            long currTime = System.currentTimeMillis();
            long elapsedTime = currTime - lastRefillTime;
            if (elapsedTime > limitTime) {
                reset(currTime);
            }
            if (requestCount > requestLimit){
                return false;
            }
            requestCount++;
            return true;
        }
    }

    private void reset(long currTime) {
        this.lastRefillTime = currTime;
        this.requestCount = 0;
    }
}

这种做法很直观,也确实有些简单的限流就是这么实现的,但它并不严谨,以下两个结论就很可能出乎对限流算法没有了解的同学意料之外:

  • 即使每一秒的统计流量都没有超过 80 TPS,也不能说明系统没有遇到过大于 80 TPS 的流量压力。你可以想像如下场景,如果系统连续两秒都收到 60 TPS 的访问请求,但这两个 60 TPS 请求分别是前 1 秒里面的后 0.5 秒,以及后 1 秒中的前面 0.5 秒所发生的。这样虽然每个周期的流量都不超过 80 TPS 请求的阈值,但是系统确实曾经在 1 秒内实在在发生了超过阈值的 120 TPS 请求
  • 即使连续若干秒的统计流量都超过了 80 TPS,也不能说明流量压力就一定超过了系统的承受能力。你可以想像如下场景,如果 10 秒的时间片段中,前 3 秒 TPS 平均值到了 100,而后 7 秒的平均值是 30 左右,此时系统是否能够处理完这些请求而不产生超时失败?答案是可以的,因为条件中给出的超时时间是 10 秒,而最慢的请求也能在 8 秒左右处理完毕。如果只基于固定时间周期来控制请求阈值为 80 TPS,反而会误杀一部分请求,造成部分请求出现原本不必要的失败。

流量计数器的缺陷根源在于它只是针对时间点进行离散的统计,为了弥补该缺陷,一种名为“滑动时间窗”的限流模式被设计出来,它可以实现平滑的基于时间片段统计。

2.滑动时间窗口模式

这种模式的实现基于滑动时间窗口算法,本文不对这种算法做过多的介绍(默认大家都会🤺),直接贴上实现代码:

@Component
@ConditionalOnMissingBean(StringRedisTemplate.class)
public class TimeSlidingRedisLimiter {
    @Autowired
    private StringRedisTemplate redisTemplate;


    public Boolean isActionAllowed(String value, String actionKey, int period, int maxCount) {
        String key = String.format(LimiterConstant.LIMIT_KEY + "%s:%s", value, actionKey);
        long nowTime = System.currentTimeMillis();
        redisTemplate.opsForZSet().add(key, nowTime + "", nowTime);
        redisTemplate.opsForZSet().removeRangeByScore(key, 0, nowTime - period * 1000);
        List<Object> result = redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
                RedisTemplate<String, String> redisTemplate = (RedisTemplate<String, String>) redisOperations;
                redisTemplate.opsForZSet().zCard(key);
                return null;
            }
        }, this.redisTemplate.getValueSerializer());
        Long count = (Long) result.get(0);
        redisTemplate.expire(key, period + 1, TimeUnit.MILLISECONDS);
        return count <= maxCount;
    }

}

3.漏桶模式与令牌桶模式

在计算机网络中,专门有一个术语流量整形(Traffic Shaping)用来描述如何限制网络设备的流量突变,使得网络报文以比较均匀的速度向外发送。 流量整形通常都需要用到缓冲区来实现,当报文的发送速度过快时,首先在缓冲区中暂存,然后再在控制算法的调节下均匀地发送这些被缓冲的报文。常用的控制算法有漏桶算法(Leaky Bucket Algorithm)和令牌桶算法(Token Bucket Algorithm)两种,这两种算法的思路截然相反,但达到的效果又是相似的。

所谓漏桶,就是大家小学做应用题时一定遇到过的“一个水池,每秒以 X 升速度注水,同时又以 Y 升速度出水,问水池啥时候装满”的那个奇怪的水池。你把请求当作水,水来了都先放进池子里,水池同时又以额定的速度出水,让请求进入系统中。这样,如果一段时间内注水过快的话,水池还能充当缓冲区,让出水口的速度不至于过快。不过,由于请求总是有超时时间的,所以缓冲区大小也必须是有限度的,当注水速度持续超过出水速度一段时间以后,水池终究会被灌满,此时,从网络的流量整形的角度看是体现为部分数据包被丢弃,而在信息系统的角度看就体现为有部分请求会遭遇失败和降级。而令牌桶它与漏桶一样都是基于缓冲区的限流算法,只是方向刚好相反,漏桶是从水池里往系统出水,令牌桶则是系统往排队机中放入令牌。

实现代码如下:

/**
 * 漏桶模式
 *
 * @author disaster
 * @version 1.0
 */
public class FunnelRateLimiter {
    private final int capacity;
    private final double rate;
    private int leftQuota;
    private long lastRefillTime;

    public FunnelRateLimiter(int capacity, double rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.leftQuota = capacity;
        this.lastRefillTime = System.currentTimeMillis();
    }

    public boolean isAllowed(int quota) {
        refillQuota();
        if (this.leftQuota >= quota) {
            this.leftQuota -= quota;
            return true;
        }
        return false;
    }

    private void refillQuota() {
        long currentTimeMillis = System.currentTimeMillis();
        long seconds = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis - this.lastRefillTime);
        System.out.println("seconds = " + seconds);
        double quotaRefilled = Math.ceil(seconds * rate);
        System.out.println("quotaRefilled = " + quotaRefilled);
        this.lastRefillTime = currentTimeMillis;
        this.leftQuota = (int) Math.min(leftQuota + quotaRefilled, capacity);
        System.out.println("quotaRefilled = " + quotaRefilled);
    }

    public static void main(String[] args) throws InterruptedException {
        FunnelRateLimiter funnelRateLimiter = new FunnelRateLimiter(5, 0.9d);
        for (int i = 0; i < 10; i++) {
            boolean allowed = funnelRateLimiter.isAllowed(1);
            if (allowed) {
                System.out.println("i = " + i + " is allowed");
            } else {
                System.out.println("i = " + i + " is not allowed");
            }
            TimeUnit.MILLISECONDS.sleep(1000);
        }
    }
}
/**
 * 令牌桶限流
 *
 * @author disaster
 * @version 1.0
 */
public class TokenBucketRateLimiter {
    private final int capacity;
    private final double rate;
    private int tokens;
    private long lastRefillTime;

    public TokenBucketRateLimiter(int capacity, double rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = 0;
        this.lastRefillTime = System.currentTimeMillis();
    }

    public boolean isAllowed(int quota) {
        refillTokens();
        if (tokens >= quota) {
            tokens -= quota;
            return true;
        }
        return false;
    }

    private void refillTokens() {
        long currentTime = System.currentTimeMillis();
        long elapsedTime = currentTime - lastRefillTime;
        int tokensToRefill = (int) ((elapsedTime - lastRefillTime) * rate / 1000);
        lastRefillTime = currentTime;
        Math.min(tokensToRefill + tokens, tokens);
    }
}

上面实现过的那些限流算法,除了滑动时间窗口的实现是可以直接使用到分布式系统中外,其他的几种实现方式只能在单体架构中使用,至于为什么,这里留给大家思考一下🤓

超额流量如何处理?

同样还是用开头的例子来解释这个问题,按照上面的几种处理方案,三个人通过合理的安排使用热水器大家都可以在合理的时间内满足各自的洗澡需求,现在我们假设你人缘特好,有那么一天有20个朋友想来你家做客,这个时候你该如何是好?按照上面的处理,再怎么安排有几个朋友洗完澡估计都得早上去了,这是你没办法忍受的。这里早上洗澡的几个朋友对应的就是热水器的超额流量,如果你是没办法拒绝别人的性格,那么这个时候你最好的选择就是这次邀请一部分朋友来你家玩,下次再邀请一部分来你家玩。

在编码世界也是如此,这种超额流量怎么处理也是需要根据不同的系统来决定的!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/580036.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字信号处理8:利用Python进行数字信号处理基础

我前两天买了本MATLAB信号处理&#xff0c;但是很无语&#xff0c;感觉自己对MATLAB的语法很陌生&#xff0c;看了半天也觉得自己写不出来&#xff0c;所以就对着MATLAB自己去写用Python进行的数字信号处理基础&#xff0c;我写了两天左右&#xff0c;基本上把matlab书上的代码…

【数据结构】轻松掌握二叉树的基本操作及查找技巧

二叉树的基本操作 ​ 在学习二叉树的基本操作前&#xff0c;需先要创建一棵二叉树&#xff0c;然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入&#xff0c;为了降低学习成本&#xff0c;此处手动快速创建一棵简单的二叉树&#xff0c;快速进入二叉树操…

【自然语言处理】不同策略的主题建模方法比较

不同策略的主题建模方法比较 本文将介绍利用 LSA、pLSA、LDA、NMF、BERTopic、Top2Vec 这六种策略进行主题建模之间的比较。 1.简介 在自然语言处理&#xff08;NLP&#xff09;中&#xff0c;主题建模一词包含了一系列的统计和深度学习技术&#xff0c;用于寻找文档集中的隐…

【刷题之路】LeetCode 2073. 买票需要的时间

【刷题之路】LeetCode 2073. 买票需要的时间 一、题目描述二、解题1、方法1——记录每个人需要的时间1.1、思路分析1.2、代码实现 2、方法2——队列记录下标2.1、思路分析2.2、先将队列实现一下2.3、代码实现 一、题目描述 原题连接&#xff1a; 2073. 买票需要的时间 题目描述…

Linux---用户组命令(groupadd、groupdel、groupmod、newgrp、getent)

1. groupadd命令 [rootlocalhost ~]# groupadd [选项] 组名 [rootlocalhost ~]# groupadd group1 --向系统中增加了一个新组group1&#xff0c;新组的组标识号是在当前已有的最大组标识号的基础上加1。 [rootlocalhost ~]# groupadd -g 101 group2 --向系统中增加了一个新组gr…

MySQL5.7递归查询与CTE递归查询

文章目录 一、8.0版本的递归1、CTE递归2、举例3、递归CTE的限制 二、5.7版本的递归1、find_in_set 函数2、concat函数3、自定义函数实现递归查询4、向上递归5、可能遇到的问题 一、8.0版本的递归 1、CTE递归 先看8.0版本的递归查询CET。语法规则&#xff1a; WITH RECURSIVE…

深入浅出解析Stable Diffusion完整核心基础知识 | 【算法兵器谱】

Rocky Ding 公众号&#xff1a;WeThinkIn 写在前面 【算法兵器谱】栏目专注分享AI行业中的前沿/经典/必备的模型&论文&#xff0c;并对具备划时代意义的模型&论文进行全方位系统的解析&#xff0c;比如Rocky之前出品的爆款文章Make YOLO Great Again系列。也欢迎大家提…

笔试强训错题总结(一)

笔试强训错题总结 文章目录 笔试强训错题总结选择题编程题连续最大和不要二最近公共祖先最大连续的bit数幸运的袋子手套 选择题 以下程序的运行结果是&#xff08;&#xff09; #include <stdio.h> int main(void) {printf("%s , %5.3s\n", "computer&q…

<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动

&#xff1c;Linux开发&#xff1e;驱动开发 -之-基于pinctrl/gpio子系统的beep驱动 交叉编译环境搭建&#xff1a; &#xff1c;Linux开发&#xff1e; linux开发工具-之-交叉编译环境搭建 uboot移植可参考以下&#xff1a; &#xff1c;Linux开发&#xff1e; -之-系统移植…

如何在华为OD机试中获得满分?Java实现【人民币转换】一文详解!

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: Java华为OD机试真题&#xff08;2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述…

认识Servlet---1

hi ,大家好,今天为大家带来Servlet相关的知识,并且实现第一个程序 &#x1f389;1.什么是Servlet &#x1f389;2.使用Servlet写一个hello程序 &#x1f33b;&#x1f33b;&#x1f33b;1.创建项目 &#x1f33b;&#x1f33b;&#x1f33b;2.引入依赖 &#x1f33b;&…

GitHub基本概念

创建日期: 2018-09-22 09:50:06 Git & GitHub Git是一个版本控制软件&#xff1a; 读作[gɪt] ,拼音读作gē y te。 Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed an…

STM32之温湿度LCD显示并上传服务器

目录 项目需求 项目框图 硬件清单 LCD1602介绍及实战 硬件接线 引脚封装 代码实现 DHT11介绍及实战 硬件接线 引脚封装 代码实现 项目设计及实现 项目设计 项目实现 项目需求 使用温湿度传感器模块&#xff08; DHT11 &#xff09;获取温度及湿度&#xff0c…

推荐计算机领域的几本入门书籍

人工智能入门&#xff1a; 人工智能&#xff1a;现代方法&#xff08;第4版&#xff09;揭示AI与chatgpt的奥秘&#xff0c;详解人工智能的发展与未来&#xff01; 推荐理由&#xff1a;系统性总结人工智能的方方面面&#xff0c;国际人工智能领域专家斯图尔特罗素撰写人工智能…

YOLO-NAS对象检测算法再一次颠覆YOLO系列算法——已超越YOLOv8

对象检测彻底改变了机器感知和解释人类世界的方式。这是计算机视觉中一项特别关键的任务,使机器能够识别和定位图像或视频中的物体。如自动驾驶汽车、面部识别系统等。推动对象检测进步的一个关键因素是发明了神经网络架构。强大的神经网络推动了对象检测的进步,增强了计算机…

Meta Learning

Meta Learning&#xff08;元学习&#xff09;是一种机器学习技术&#xff0c;它的核心思想是学习如何学习。 Meta Learning的目标是从以前的学习经验中学习到通用的学习策略和模式&#xff0c;以便在新的任务上快速适应和学习。 Meta Learning的核心思想是将学习任务视为元任…

Vivado下阻塞赋值和非阻塞赋值的对比

Verilog 基础知识 中已经介绍过了阻塞赋值和非阻塞赋值的区别&#xff0c;下面通过一个在Vivado中的简单例子来直观的反映两者的不同。 首先给出设计源代码如下。 module block(a,b,c,clk,x);input x;input clk;output reg a,b,c;always(posedge clk) begina x; //阻塞赋值…

零钱兑换,凑零钱问题,从暴力递归到动态规划(java)

凑零钱问题&#xff0c;从暴力递归到动态规划 leetcode 322 题 零钱兑换暴力递归&#xff08;这个会超时&#xff0c;leetcode 跑不过去&#xff09;递归缓存 leetcode 322 题 零钱兑换 322 零钱兑换 - 可以打开链接测试 给你一个整数数组 coins &#xff0c;表示不同面额的硬…

[MAUI]模仿Chrome下拉标签页的交互实现

文章目录 创建粘滞效果的圆控件贝塞尔曲线绘制圆创建控件创建形变可控形变形变边界形变动画 创建手势控件创建页面布局更新拖拽物位置其它细节 项目地址 今天来说说怎样在 .NET MAUI 中制作一个灵动的类标签页控件&#xff0c;这类控件常用于页面中多个子页面的导航功能。 比如…

《数据库应用系统实践》------ 公园游客日流量管理系统

系列文章 《数据库应用系统实践》------ 公园游客日流量管理系统 文章目录 系列文章一、需求分析1、系统背景2、 系统功能结构&#xff08;需包含功能结构框图和模块说明&#xff09;3&#xff0e;系统功能简介 二、概念模型设计1&#xff0e;基本要素&#xff08;符号介绍说明…