go语言实现高性能自定义ip管理模块(ip黑名单)

ip黑名单设计

对于IPV4而言,理论上有256^4个,也就是约42亿个。我想了好久,也查了挺多资料,但是,确实没有通用现成的解决方案。

PS:以下方案的讨论,适用于对于IP管理不那么严苛的情况。当然也可以改进为严苛的方案,无非是加锁,加内存。

非常欢迎有更好方案的大佬指点一下,因为我想了好久,实在没有更好的思路了,比如有大型业务系统的ip访问管理经验的,分享一下心得。

考虑过的方案

  1. 只管理大陆的ip,对于其余ip直接不给通过
    当初我想过只包含国内的ip,也有渠道获取国内的ip列表,好像也有上亿。上亿的量不算大。可以用数组来存储。麻烦的是做映射和更新。所以放弃
  2. 用一维数组,结合位运算存储
    可以算一下,假设以int64的数组来存储,一个int64可以存储64个ip,256^4/64*8/1024/1024=512MB,也就是只需要这么多内存。这也是网上能查到资料给到的可考虑方案之一。
    但是,这种方案肯呢个做最基本的黑名单记录,某个位上标记为1,就表示它被拉黑了。为0就没被拉黑,但是,如果我需要记录ip访问的次数,封禁的时长这些呢?这个方案做不到。你说再用一个结构体来存储这些需要的信息?那不是得不偿失?
  3. redis来存储?不说内存占用多大,redis 是有网络开销的,性能就不达标了
  4. 对应IPv4 的四段关系,采用四层引用来关系来存储(当前方案)。存储结构如[256]*[256]*[256]*[4]int64

第4方案优缺点

存储IP的方法

例如我要存储ip127.0.0.1,那么,我就会存储成[127]*[0]*[0]*[4]int64,上,最后这个1存在哪里,就是用位运算去处理,这里不再细说

缺点
  1. 当ip存储量很大时,占用的空间多余直接一维数组的方式,因为指针和空值也是要占用空间的,而且不少
  2. 和第二种方案一样,同样无法记录更多的信息
优点
  1. 链式索引结构,查找性能和一维数组的形式无明显差异,性能非常好
  2. 当ip量不大时,我不需要像一维数组那样初始化就搞个那么大的数组,这个方案最初只需要初始化[256]个空值,占用空间4096字节(64位机器上)
  3. 公网上很多ip段是不能被使用的,如果我们为这些IP段去做映射,可以节省空间,但是映射的规则非常复杂,增加代码的复杂度,降低可维护性。而采取这种方案,这些不可能出现的IP段也不会占用空间,所以根本无需考虑做映射。这也是比一维数组方案的一大优势。

改进

针对上述缺点,可以考虑,对于百万级别的ip,需要多大的业务量才达得到这个量级。我想全国每天能有百万个ip访问的系统,应该都是非常知名的了,所以,完全不用考虑这么多,所以,第一条缺点,不存在,反而证明了第二条优点。

我最开始想要设计这个ip管理模块是因为想为自己的网站防御ddos攻击,虽然我不太懂ddos攻击,但是看来拉不拉黑名单对ddos没啥用,他是阻塞带宽从而使正常的访问无法进入。

但是,我已经花了好多时间去向这个方案了。虽然对ddos没用,但是对系统稳定性有用嘛

如何才能记录更多的信息

我想要做的不仅仅是黑名单,而是ip管理,也就是可以记录IP是否访问,访问次数,什么时候访问,最近一次访问,是否被封禁,封禁时长,永久封禁,如何解封等

记录信息,那么就需要有这个一个结构体,搞一堆字段,最后的结构应该是[256]*[256]*[256]*ipManage{}。这样固然好,记录什么信息都可以自由拓展。当然,你需要为他付出更多的内存空间。保存后面保存到数据库的空间。

所以,我没钱租那么大内存的服务器,所以我选择简化方案。

最终方案

最终我综合考虑自己的业务需求,设计了如何结构体:

type ipVisitS struct {IpList            [256]*[256]*[256]*[256]int8limit             int8  // 设置的每cycleSecond的访问上限cycleSecond       int32 // 每次循环检查的间隔时长,单位秒visitLimitBanTime int8  // 访问超限的封禁时长,单位分钟
}

IpList就是我拿来储存访问的IP信息的数组,我最后采用int8。而不是一个结构体,因为这样可以节省很多空间。

简化的方案固然能表达的信息就少很多了:

  1. 指针索引为nil[256]int8所在位置上的值为0,表示该ip未记录
  2. [256]int8上记录的值大于0,表示该IP的访问次数。由于int8正数最大127,所以最多记录127次访问
  3. [256]int8上记录的值小于0,表示该IP被禁用时长,单位分钟。由于int8负数最小负128,且-128保留作为永封的标记,所以最多表示的封禁时长是127分钟。
  4. 如上所说-128表达为永封,当然你可以根据实际需求把更多负数赋予特殊含义

是的,局限也很大,但是是我考虑实际得出的方案,不满足就可以换成int16,甚至是结构体。思路是一样的。
另外解释下其他属性的含义:

limit :每个ip在单位时间内(也就是cycleSecond)的访问上限,虽然int8正数最大127,但是未必就一定要让他的上限等于127,支持自定义
cycleSecond:一个周期的时长,单位是秒,关系到limit 的记录周期,以及后面一个巡检的定时器周期,后面在介绍定时器
visitLimitBanTime:访问超过limit后的封禁时长,单位是分钟

代码实现

ip管理模块实现如下接口:

type IBlacklist2 interface {/**- @description: 将一个ip 字符串添加到名单里,单线程下添加一千万ip耗时约不到4秒- @param {string} ipStr 形如127.0.0.1- @return {*}0: 输入的ip格式错误(0,128): cycleSecond时间内访问次数(-128,0):封禁时长-128:    永封*/Add(ipStr string) int8/**- @description: 增加封禁时长,如果还未被封禁,则等于封禁时长,如果已被封禁,则增加banTime- @param {string} ipStr 封禁的IP地址- @param {int8} banTime 用负数表示,数值表示增加的时长,单位分钟,-128表示永封- @return {int8} 0: 输入的ip格式错误(0,128): cycleSecond时间内访问次数(-128,0):封禁时长-128:    永封*/AddBanTime(ipStr string, banTime int8) int8 // 给一个ip增加封禁时长/*** @description: 判断一个ip是否被封禁,如果被封禁,则返回封禁时长,否则返回访问次数* @param {string} ipStr* @return {int8} 第一个返回值的含义:0:表示该ip未记录;>0:表示该ip的访问次数;<0:表示该ip的封禁时长;-128:表示该ip永久封禁{bool} 第二个返回值的含义:false:输入的ip有误,true:输入的ip正确*/IsBan(ipStr string) (int8, bool) // 判断一个ip是否被禁用GetLen() int                     //获取黑名单列表长度GetAll() []string                // 获取黑名单列表,升序排序GetSizeOf() uintptr              // 获取黑名单列表占用的内存空间
}

额外实现两个方法:

/*** @description: 初始化* @param {int8} limit 设置的每cycleSecond的访问上限* @param {int32} cycleSecond 每次循环检查的间隔时长,单位秒* @param {int8} visitLimitBanTime 访问超限的封禁时长,单位分钟* @return {*ipVisitS} 对象* @return {error} 错误*/
func InitIpVisit(limit int8, cycleSecond int32, visitLimitBanTime int8,stopChan chan bool) (*ipVisitS, error) {if !(limit >= 0 && limit <= 127) {return nil, errors.New("limit的取值范围是 [0,127]")} else if !(cycleSecond >= 1 && cycleSecond <= 3600) {return nil, errors.New("cycleSecond的取值范围是 [1,3600]秒")} else if !(visitLimitBanTime >= 1 && visitLimitBanTime <= 127) {return nil, errors.New("visitLimitBanTime的取值范围是 [1,127]分钟")}ipVisit := &ipVisitS{limit:             limit,cycleSecond:       cycleSecond,visitLimitBanTime: visitLimitBanTime,}ipVisit.CheckBlackList(stopChan) // 启动定时器return ipVisit, nil
}
/*** @description: 定时任务,每cycleSecond循环检查一次,当取值范围为[-127,0)时,加1,表示封禁时长减一分钟。当取值范围为(0,127]时,减1,表示访问次数清零。且将不再记录ip段重置为空值,防止一直占用内存* @return {*} *time.Ticker的指针*/
func (ip *ipVisitS) CheckIPList() *time.Ticker {
······省略
功能就是每个周期巡检IP列表
1.封禁时长减1
2.访问次数清零,重新记录。是的,直接清零,这也是该方案的一个缺陷,因为无依据判断访问的时间,索性直接清零
3.清理历史记录的空间,释放内存
}

性能测试

测试代码:

	ipStart := time.Now()ipList := test.CreateIp(1000000, 4) // 创建ip 一百万个,4个协程同时进行fmt.Println("创建ip耗时:", time.Since(ipStart))//初始化stopchan:=make(chan bool,1)onlyIp, _ := ipmanage.InitIpVisit(127, 60, 5,stopchan)startTime := time.Now()// 单线程运行添加ipfor i := 0; i < len(ipList); i++ {onlyIp.Add(ipList[i])}//模拟第二次访问for i := 0; i < 10000; i++ {onlyIp.Add(ipList[i])}// 模拟第三次访问for i := 0; i < 10000; i++ {onlyIp.Add(ipList[i])}onlyIp.AddBanTime("0.0.0.1", -5)	// 模拟封禁5分钟onlyIp.AddBanTime("127.0.0.1", -128) // 模拟永封fmt.Println("记录ip耗时:", time.Since(startTime))// 等待十次巡检for i := 0; i < 10; i++ {sizeStart := time.Now()fmt.Println("ip队列占用内存(字节):", onlyIp.GetSizeOf())fmt.Println("计算ip队列占用内存耗时:", time.Since(sizeStart))allStart := time.Now()fmt.Println("ip队列长度:", onlyIp.GetLen())fmt.Println("计算ip队列长度耗时:", time.Since(allStart))time.Sleep(time.Minute)}

创建ip耗时: 65.4136ms
记录ip耗时: 404.0073ms
ip队列占用内存(字节): 390746112
计算ip队列占用内存耗时: 14.0027ms
ip队列长度: 1000002
计算ip队列长度耗时: 369.1569ms

可以看出,单线程应对百万IP,也仅需300多毫秒,我觉很OK了,当然,我没有加锁。追求极致性能。因为我的业务量下, 为了那极低概率会出现的线程不安全问题,加锁简直就是浪费,对于那偶尔的计算出错,无所谓。

完整代码:https://gitee.com/lsjWeiYi/ip-manage

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

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

相关文章

代码随想录二刷|两两交换链表中的节点

两两交换链表中的节点 1、需要哪几个指针&#xff1f; cur&#xff1a;当前遍历的节点 tmp&#xff1a;节点“1” tmp1&#xff1a;节点“3” &#xff08;cur要指向当前要交换的两个节点的前一个节点&#xff09; 2、为什么不需要保存节点“2”&#xff1f; 因为就是cur->…

gitlab安装配置及应用

安装 ##安装依赖 yum install -y curl policycoreutils-python openssh-server perl#上传包 rz gitlab-jh-16.5.2-jh.0.el7.x86_64.rpm 安装 yum install gitlab-jh-16.0.3-jh.0.el7.x86_64.rpm 初始化并启动 # 以下两种方法都可以配置访问地址&#xff0c;第一种需要在yum安…

git使用记录

初始化仓库 git init 与远程仓库进行联接 git remote add origin &#xff08;仓库复制下来的地址&#xff09; 拉取远程仓库代码 #查看远程分支 git branch -r #查看本地分支 git branch #拉取远程分支&#xff0c;会产生映射关系 使用该方式会在本地新建分支x&#xff0c;并…

使用Nodejs和Express构建http响应流实现下载功能

首先创建一个文件流来读取要下载的文件&#xff0c;当然可以是动态产生的输入流 const fileStream fs.createReadStream(test.zip);然后创建响应头&#xff0c;指定响应的类型&#xff0c;同时也可以使用Content-Disposition设置浏览器下载时需要保存的文件名 const head {…

深度学习之二(前馈神经网络--Feedforward Neural Network)

概念 前馈神经网络(Feedforward Neural Network)是一种最基本的神经网络结构,也被称为多层感知器(Multilayer Perceptron,MLP)。它的特点是信息只在网络中单向传播,不会形成环路。每一层神经元的输出都作为下一层神经元的输入,没有反馈回路。 结构: 前馈神经网络通…

基于SpringBoot的花店销售网站

文章目录 项目介绍主要功能截图:部分代码展示设计总结项目获取方式🍅 作者主页:超级无敌暴龙战士塔塔开 🍅 简介:Java领域优质创作者🏆、 简历模板、学习资料、面试题库【关注我,都给你】 🍅文末获取源码联系🍅 项目介绍 基于SpringBoot的花店销售网站,java项目…

小程序中打印机纸张都支持哪些尺寸?

在小程序中添加打印机功能是一项非常实用的功能&#xff0c;它可以让用户方便地将小程序中的内容打印出来。然而&#xff0c;当用户想要打印内容时&#xff0c;他们可能会关心打印纸张支持哪些尺寸。打印机分为四种打印机&#xff1a;小票、标签、发货单和电子面单。下面具体介…

YOLO改进系列之注意力机制(GatherExcite模型介绍)

模型结构 尽管在卷积神经网络&#xff08;CNN&#xff09;中使用自底向上的局部运算符可以很好地匹配自然图像的某些统计信息&#xff0c;但它也可能阻止此类模型捕获上下文的远程特征交互。Hu等人提出了一种简单&#xff0c;轻量级的方法&#xff0c;以在CNN中更好地利用上下…

使用VC++设计程序,进行全局固定阈值分割、自适应阈值分割

图像分割 获取源工程可访问gitee可在此工程的基础上进行学习。 文章目录 图像分割实验内容一、全局固定阈值分割全局固定阈值分割的原理全局固定阈值分割的实验代码全局固定阈值分割的实验现象 二、自适应阈值分割自适应阈值分割的实验原理自适应阈值分割的实验代码自适应阈值…

解决 urllib2 中 CookiesMiddleware 的 cookie 问题

1. 问题背景 在网络爬虫开发中&#xff0c;Cookie 是一项关键的技术&#xff0c;用于跟踪用户的身份和状态。Cookie 是服务器在客户端存储的数据&#xff0c;通常用于维护用户会话和保存用户的登录信息。在爬虫应用中&#xff0c;模拟用户行为和保持 Cookie 状态是必要的&…

51单片机应用

目录 ​编辑 1. C51的数据类型 1.1 C51中的基本数据类型 1.2 特殊功能寄存器类型 2. C51的变量 2.1 存储种类 1. C51的数据类型 C51是一种基于8051架构的单片机&#xff0c;它支持以下基本数据类型&#xff1a; 位&#xff08;Bit&#xff09;&#xff1a;可以表…

超级实用的程序员接单平台,看完少走几年弯路,强推第一个!

”前途光明我看不见&#xff0c;道路曲折我走不完。“ 兜兜转转&#xff0c;心心念念&#xff0c;念念不忘&#xff0c;必有回响。终于找到了....... 网络上好多人都在推荐程序员线上接单&#xff0c;有人说赚得盆满钵满&#xff0c;有的人被坑得破口大骂&#xff0c;还有的人…

STM32踩坑:LAN8720未接网线,上电后再接网线,网络模块无法正常使用

LAN8720未接网线&#xff0c;上电后再接网线&#xff0c;网络模块无法正常使用 一、问题描述 最近因为做的项目出了BUG&#xff0c;STM32 单片机在未接网线的状态下&#xff0c;上电一段时间后&#xff0c;将网线插入网口后&#xff0c;IP地址ping不通&#xff0c;网络模块无…

正则笔记(持续更新)

1. java 正则替换 指定字符及其之前的字符 System.out.println("em_4b6add2cfb415db2".replaceFirst("\\.*._",""));//结果 -> e4b6add2cfb415db22. java 正则替换 指定字符及其之后的字符 String name "name.keyword^1.0" ; St…

怎么使用OpenFeign和配置中心

首先&#xff0c;在您的Spring Boot项目中添加OpenFeign和配置中心的依赖项。您可以通过将以下内容添加到项目的pom.xml文件中来实现&#xff1a; <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfei…

XDAG同步节点部署

系统环境要求 JDK : v17 Maven : v3.9.1-v3.9.5 MySQL : v8.0系列 1、MySQL8.0安装 1&#xff09;docker-compose安装详情 MySQL安装 2&#xff09;配置数据库账号密码及键表 # docker exec -it mysql8 /bin/bash # root0286a1fd60e6:/# mysql -uroot -p Enter password:…

ubuntu 20.04 搭建crash dump问题分析环境

ubuntu 20.04 搭建crash dump问题分析环境 1 安装依赖软件1.1 linux-dump1.2 kexec-tools1.3 安装crash工具1.4 安装gdb调试工具1.5 安装ubuntu内核调试符号1.5.1 GPG 秘钥导入1.5.2 添加仓库配置1.5.3 更新软件包1.5.4 下载和安装内核调试符号1.5.5 验证内核调试符号已经被安装…

SELinux零知识学习二十三、SELinux策略语言之类型强制(8)

接前一篇文章&#xff1a;SELinux零知识学习二十二、SELinux策略语言之类型强制&#xff08;7&#xff09; 二、SELinux策略语言之类型强制 3. 访问向量规则 AV规则就是按照对客体类别的访问许可指定具体含义的规则&#xff0c;SELinux策略语言目前支持四类AV规则&#xff1a…

SpringBoot学习笔记-创建个人中心页面(下)

笔记内容转载自 AcWing 的 SpringBoot 框架课讲义&#xff0c;课程链接&#xff1a;AcWing SpringBoot 框架课。 CONTENTS 1. 实现个人中心页面2. POJO时区修改3. 集成代码编辑器 本节实现个人中心的前端页面&#xff0c;用户能够查看自己的 Bot 信息&#xff0c;并能创建、修改…

【Echart】Echart设置label太长隐藏:

文章目录 第一种&#xff1a;竖排显示第二种&#xff1a;显示部分第三种&#xff1a;强制显示所有标签并旋转 第一种&#xff1a;竖排显示 xAxis: {type: category,data: res.data.data.sz.xAxis,axisLabel:{fontSize:12,formatter: function(value) {return value.split().joi…