如何使用滑动窗口限流优化网站性能 —— 安企CMS中的实践

news2024/9/21 17:53:04

如何优雅地处理高频访问?

今天早上,我收到了一条客户反馈,说网站打开很卡。我立刻打开服务器进行监控,发现服务器的负载异常高。经过一番排查,我发现在极短的时间内,某个IP以非常规律的频率访问着网站的多个页面。几乎一眼就能看出,网站被恶意的采集工具盯上了。这个IP通过不断请求页面,极大地消耗了服务器资源,导致正常用户无法访问。

面对此类高频请求问题,如果不采取有效的限流措施,网站不仅会出现性能问题,还可能在遭受持续攻击的情况下直接崩溃。于是,我决定着手处理这个问题,设计一套高效的请求限流方案。

传统限流方式面临的挑战

很多现有的限流方案都基于固定时间窗口逐次记录请求时间戳。这些方法虽然能解决部分问题,但在实际项目中有几个明显的缺点:

  1. 固定时间窗口:它按固定时段(如1分钟、5分钟)计数,当时间窗口切换时,所有请求记录清零。这样很容易导致“突发请求”问题:刚刚进入新窗口时,计数器归零,瞬间允许大量请求通过。

  2. 时间戳记录:逐次记录每次请求的时间戳虽然精确,但它要求对每个IP记录大量请求数据,内存占用较大,特别是在高流量网站上,容易出现性能瓶颈。

显然,这些传统方法难以应对我的需求。于是,我开始寻找一种更高效且灵活的方案。

如何选择最优解?

在进行方案对比时,我考虑了以下几种解决方案:

  • 漏桶算法(Leaky Bucket):将请求当成水滴,滴入“桶”中,按照固定速率“漏”出。这个方法虽然能平滑处理请求流量,但对于高频突发的请求依然存在难以控制的情况。

  • 令牌桶算法(Token Bucket):类似于漏桶算法,但它允许在短时间内处理请求的“突发”,只要有足够的“令牌”。然而,令牌桶算法相对复杂,且需要不断生成令牌,管理难度较大。

  • 滑动窗口计数法(Sliding Window):通过动态滑动的时间窗口统计请求,不仅能够灵活应对突发流量,还能保证整个窗口期内的请求量精确计算,避免传统固定时间窗口的缺点。

经过对比,我最终选择了滑动窗口计数法。这种方法既能有效限制请求频率,又不会像记录时间戳那样占用大量内存。

滑动窗口 + 时间桶

为了优化滑动窗口的内存使用,我设计了一个基于时间桶的滑动窗口算法。该方案不需要逐次记录每个请求的时间戳,而是将整个窗口期分成多个“时间桶”,每个桶记录1分钟内的请求总数。通过动态滑动这些桶,我们可以精准控制5分钟内的请求总量。

核心思路:

  1. 滑动窗口:将时间窗口分成5个1分钟的桶,每当新的一分钟开始时,移除最早的1分钟数据,动态计算最新的5分钟请求总量。
  2. 时间桶:每个桶存储该分钟内的请求数量,而不是每个请求的时间戳。这极大降低了内存占用。
  3. IP白名单:同时,我还引入了IP白名单,内网和本地IP无需受到限流控制,确保正常流量不受影响。
  4. UA白名单:同样,我引入了UA白名单,对特定UA的请求不做限流控制,避免了搜索引擎蜘蛛被误伤。

实现代码:

定义数据结构
type VisitInfo struct {
    Buckets     [5]int    // 每分钟一个桶,共5个桶
    LastVisit   int64     // 上次请求的时间戳
    CurrentIdx  int       // 当前时间对应的桶索引
    TotalCount  int       // 当前窗口期内的请求总数
}

var ipVisits = make(map[string]*VisitInfo)
var blockedIPs = make(map[string]time.Time)
var mu sync.Mutex

const WindowSize = 5 * time.Minute  // 窗口大小为5分钟
const MaxRequests = 100             // 5分钟内最大请求次数
const BlockDuration = 1 * time.Hour // 封禁时长为1小时

var whiteListIPs = []string{"127.0.0.1", "192.168.0.0/16"} // 内网和本地IP白名单

func isWhitelisted(ip string) bool {
    for _, cidr := range whiteListIPs {
        _, subnet, _ := net.ParseCIDR(cidr)
        if subnet.Contains(net.ParseIP(ip)) {
            return true
        }
    }
    return false
}
记录请求并清理过期记录
func recordIPVisit(ip string) bool {
    mu.Lock()
    defer mu.Unlock()

    now := time.Now().Unix() // 当前的Unix时间(秒)
    currentMinute := now / 60 % 5 // 当前在5个桶中的索引

    // 检查是否已有该IP的访问记录
    visitInfo, exists := ipVisits[ip]
    if !exists {
        visitInfo = &VisitInfo{
            Buckets:    [5]int{},
            LastVisit:  now,
            CurrentIdx: int(currentMinute),
        }
        ipVisits[ip] = visitInfo
    }

    // 计算时间差,更新桶的状态
    elapsedMinutes := int(now/60 - visitInfo.LastVisit/60)
    
    // 如果时间超过了窗口大小,重置所有桶
    if elapsedMinutes >= 5 {
        visitInfo.Buckets = [5]int{}
        visitInfo.TotalCount = 0
    } else {
        // 依次清理过期的桶
        for i := 1; i <= elapsedMinutes; i++ {
            idx := (visitInfo.CurrentIdx + i) % 5
            visitInfo.TotalCount -= visitInfo.Buckets[idx]
            visitInfo.Buckets[idx] = 0
        }
    }

    // 更新当前桶的索引和计数
    visitInfo.CurrentIdx = int(currentMinute)
    visitInfo.Buckets[visitInfo.CurrentIdx]++
    visitInfo.TotalCount++

    // 更新最后访问时间
    visitInfo.LastVisit = now

    // 检查是否超过最大请求次数
    if visitInfo.TotalCount > MaxRequests {
        return false // 超过最大请求次数,应该封禁
    }

    return true
}
处理请求逻辑
func handleRequest(w http.ResponseWriter, r *http.Request) {
    ip := r.RemoteAddr

    // 检查并跳过白名单和搜索引擎
    if isWhitelisted(ip) || !isUAWhitelisted(r.UserAgent()) {
      ...
    }

    // 检查IP是否已被封禁
    if isIPBlocked(ip) {
        http.Error(w, "Your IP is blocked.", http.StatusForbidden)
        return
    }

    // 记录IP访问,并检查是否超出阈值
    if !recordIPVisit(ip) {
        blockIP(ip)
        http.Error(w, "Too many requests from this IP.", http.StatusTooManyRequests)
        return
    }

    // 正常处理请求
    ...
}
定时清理封禁的IP
func cleanupExpiredRecords() {
    mu.Lock()
    defer mu.Unlock()

    now := time.Now()

    // 清理过期的封禁记录
    for ip, unblockTime := range blockedIPs {
        if now.After(unblockTime) {
            delete(blockedIPs, ip)
        }
    }

    // 清理过期的IP访问记录,这里只回收最后一次访问超过5分钟的记录
    for ip, visitInfo := range ipVisits {
        if now.After(time.Unix(visitInfo.LastVisit, 0).Add(WindowSize)) {
            delete(ipVisits, ip)
        }
    }
}

func startCleanupTask() {
    ticker := time.NewTicker(1 * time.Minute)
    go func() {
        for range ticker.C {
            cleanupExpiredRecords()
        }
    }()
}

当请求到来时,系统首先检查该IP是否在白名单中。如果是白名单IP,直接放行;如果不是,则使用滑动窗口算法动态统计请求数量。

判断UA,如果是搜索引擎蜘蛛,则也同样跳过后续的检查,直接放行。

将滑动窗口集成到安企CMS中

将滑动窗口限流方案集成到安企CMS时,我主要关注以下几点:

  1. 高效性:确保限流逻辑在高并发情况下依然能够快速处理,不影响正常请求。
  2. 灵活性:通过调节时间桶数量和每个桶的大小,适应不同的流量场景。例如,系统默认5分钟内允许100次请求,但可以根据业务需求灵活调整。
  3. 稳定性:对封禁的IP进行1小时的封禁处理,并定期清理过期的封禁记录,确保系统长时间稳定运行。

总结:滑动窗口限流在安企CMS中的应用

通过滑动窗口和时间桶相结合的方法,我成功解决了安企CMS中的恶意请求问题。该方案不仅显著降低了内存开销,还使得系统在高流量下表现稳定。特别是在集成了IP白名单功能后,内网和本地IP用户可以免受限流影响,保证了系统对内部流量的友好性。

优点:

  • 高效:相比逐次记录请求时间戳的传统方法,内存占用和计算量大幅减少。
  • 灵活:可以根据业务需求灵活调整限流策略和封禁时长。
  • 安全:封禁机制有效防止恶意用户对系统发起过多请求,提升整体安全性。

这次滑动窗口限流方案的实践,不仅提升了安企CMS的性能和稳定性,也为其他开发者提供了一个简单易用的高效限流方案。如果你也在开发过程中遇到类似的高频请求问题,希望这篇文章能为你提供一些参考。

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

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

相关文章

docker技术(上)

一、docker简介 Docker 是一个开源的应用容器引擎&#xff0c;于 2013 年由 Solomon Hykes 推出并开源。它基于 Go 语言开发&#xff0c;遵从 Apache2.0 协议。Docker 可以让开发者将应用及其依赖包打包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 或 Windows…

文件外发控制怎么做?公司文件外发管控的方法(这五种方法你一定要学会!)

还在担心重要文件发出去就"人间蒸发"&#xff1f; 或者每次发送公司机密都提心吊胆&#xff1f; 其实&#xff0c;文件外发就像放风筝&#xff0c;你需要时刻握住“线头”&#xff0c;确保它不会飞得太远&#xff01; 今天我们来揭秘五种公司文件外发的神级管控方法…

基于SpringBoot的医院挂号就诊系统【附源码】

基于SpringBoot的高校社团管理系统&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1界面设计原则 4.2功能结构设计 4.3.2 数据库物理设计 第5章 系统实现 5.1用户信息管理 5.2 医生信息管理 5.3公告类型管理 5.1公告信息管理 4…

C++进阶(2):多态

多态的概念 多态分为编译时多态(静态多态)和运行时多态(动态多态)。**编译时多态&#xff1a;**主要就是我们前面讲的函数重载和函数模版。之所以叫编译时多态&#xff0c;是因为实参传给形参的参数匹配是发生在编译时完成的&#xff08;ps&#xff1a;通常把编译时一般归为静…

常见项目场景题1(数据量很大时如何去重,实现超时处理)

数据很多&#xff0c;限制内存&#xff0c;如何去重 对于大数据量去重的场景&#xff0c;我们可以考虑使用位图(Bitmap) Bitmap 是使用二进制来表示某个元素是否存在的数组。用0和1来表示存在与不存在 使用Bitmap的话&#xff0c;一个数字占用1bit&#xff0c;大大减少内存消耗…

JVM 调优篇8 调优案例5- 逃逸分析

一 逃逸分析 1.1 概念 逃逸分析的基本行为就是分析对象动态作用域&#xff1a;当一个对象在方法中被定义后&#xff0c;对象只在方法内部使用&#xff0c;则认为没有发生逃逸。当一个对象在方法中被定义后&#xff0c;它被外部方法所引用&#xff0c;则认为发生逃逸。例如作为…

打造未来企业:业务能力建模的实践应用与数字化转型的落地策略

在当今数字化迅速发展的时代&#xff0c;企业的转型迫在眉睫。通过数字技术提升运营效率、增强客户体验、优化资源配置成为了企业竞争的核心战略。《业务能力指南》为企业提供了清晰的业务能力建模框架&#xff0c;并指导企业如何将其应用于实际操作中&#xff0c;帮助企业在数…

(三)代码实现:Boustrophedon Cellular Decomposition Path Planning用珊格地图生成每个cell的覆盖路径

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言算法原理方法一&#xff1a;全地图进行牛耕覆盖步骤方法二&#xff1a;区域分解地图进行牛耕覆盖步骤凸多边形基于栅格地图的…

Windows系统文件夹中的文件名排序

一天张三、李四的同事周五接到王哥的一个任务需求&#xff0c;有一个文件夹&#xff0c;里面有许多图片文件&#xff0c;网页访问某个分类展示文件的时候&#xff0c;王哥希望文件名的展示顺序可以按照Windows资源管理器中文件名升序排序的方式展示。 网站图片目录中有如下图片…

程序遇到问题错误bug时的13种解决方法途径总结以及之前的一些具体例子

目录 1 信心--没有解决不了的bug 2 耐心、不要着急、静下心来、用脑思考 2.1 开始解决问题前不要着急&#xff0c;先思考 2.2 在解决问题的过程中也不要着急&#xff0c;要冷静思考 3 网络搜索 4 大模型问答&#xff1a;必应、kimi、通义千问、文心一言 5 看芯片手册、S…

0921VGG网络实现

深度学习之VGG网络搭建 1.VGG动机2.VGG架构3.代码4.结论1.VGG动机 随着卷积网络在计算机视觉领域的快速发展,越来越多的研究人员开始通过改变模型的网络结构在提高在图像识别任务中的精度,例如使用更小的卷积核和步长[2]。基于类似的想法,论文作者提出可以尝试通过改变卷积…

【设计模式】创建型模式(三):单例模式

创建型模式&#xff08;三&#xff09;&#xff1a;单例模式 1.概念2.案例3.实现方式3.1 懒汉式&#xff0c;线程不安全3.2 懒汉式&#xff0c;线程安全3.3 饿汉式3.4 双检锁/双重校验锁&#xff08;DCL&#xff0c;Double-Checked Locking&#xff09;3.5 登记式/静态内部类3.…

俄罗斯OZON新生儿产品好不好卖,OZON新生儿产品

Top1 遥控水球坦克 Танк на радиоуправлении стреляющий орбизами PANAWEALTH 商品id&#xff1a;1384249985 月销量&#xff1a;692 欢迎各位OZON卖家朋友点击这里选品&#xff1a; &#x1f449; D。DDqbt。COm/74rD 遥控射击水…

【项目管理进阶】风险问题

前言 各位盆友&#xff0c;你们期待的项目管理进阶系列有新的消息&#xff0c;请注意查收&#xff0c;并反馈哦~ 在参加项目的过程中&#xff0c;你是否面临或参加过类似如下的场面&#xff1a; 为了立项&#xff0c;先调研市场、技术、社会、组织内部的现状为了科学的管理项目…

如何使用Claude进行Android App开发 —— 基于Jetpack和Compose的电影App实例

如何使用Claude进行Android App开发 —— 基于Jetpack和Compose的电影App实例 近年来&#xff0c;人工智能&#xff08;AI&#xff09;在软件开发中的应用越来越广泛&#xff0c;帮助开发者在设计、编码、测试和优化中提高生产效率。Claude是Anthropic开发的一款强大的AI助手&…

Redis的三种持久化方法详解

Redis持久化机制详解 | JavaGuide Redis 不同于 Memcached 的很重要一点就是&#xff0c;Redis 支持持久化&#xff0c;而且支持 3 种持久化方式: 快照&#xff08;snapshotting&#xff0c;RDB&#xff09;只追加文件&#xff08;append-only file, AOF&#xff09;RDB 和 A…

Vue使用axios二次封装、解决跨域问题

1、什么是 axios 在实际开发过程中&#xff0c;浏览器通常需要和服务器端进行数据交互。而 Vue.js 并未提供与服务器端通信的接口。从 Vue.js 2.0 版本之后&#xff0c;官方推荐使用 axios 来实现 Ajax 请求。axios 是一个基于 promise 的 HTTP 客户端。 关于 promise 的详细介…

C++:类和对象OJ题

目录 一、求123...n 二、计算日期到天数的转换 三、日期差值 四、打印日期 一、求123...n 这里先把题目链接放在这里求123.....n 描述&#xff1a; 求123...n&#xff0c;要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句&#xff08;A?B:C…

鸿萌数据恢复服务: 修复 Windows, Mac, 手机中 “SD 卡无法读取”错误

天津鸿萌科贸发展有限公司从事数据安全服务二十余年&#xff0c;致力于为各领域客户提供专业的数据恢复、数据备份解决方案与服务&#xff0c;并针对企业面临的数据安全风险&#xff0c;提供专业的相关数据安全培训。 公司是多款国际主流数据恢复软件的授权代理商&#xff0c;为…

老年人养生之道:岁月静好,健康常伴

老年人养生之道&#xff1a;岁月静好&#xff0c;健康常伴 随着年岁的增长&#xff0c;老年人更需注重养生&#xff0c;以维持身心的和谐与健康&#xff0c;享受幸福晚年。养生不仅是一种生活态度&#xff0c;更是一种智慧的选择&#xff0c;它涵盖了饮食、运动、心理、社交等…