小天才电话手表网站,古镇营销型网站建设,上海有多少家公司和企业,wordpress微信防红插件ip黑名单设计
对于IPV4而言#xff0c;理论上有256^4个#xff0c;也就是约42亿个。我想了好久#xff0c;也查了挺多资料#xff0c;但是#xff0c;确实没有通用现成的解决方案。
PS#xff1a;以下方案的讨论#xff0c;适用于对于IP管理不那么严苛的情况。当然也可…ip黑名单设计
对于IPV4而言理论上有256^4个也就是约42亿个。我想了好久也查了挺多资料但是确实没有通用现成的解决方案。
PS以下方案的讨论适用于对于IP管理不那么严苛的情况。当然也可以改进为严苛的方案无非是加锁加内存。
非常欢迎有更好方案的大佬指点一下因为我想了好久实在没有更好的思路了比如有大型业务系统的ip访问管理经验的分享一下心得。
考虑过的方案
只管理大陆的ip对于其余ip直接不给通过 当初我想过只包含国内的ip也有渠道获取国内的ip列表好像也有上亿。上亿的量不算大。可以用数组来存储。麻烦的是做映射和更新。所以放弃用一维数组结合位运算存储 可以算一下假设以int64的数组来存储一个int64可以存储64个ip256^4/64*8/1024/1024512MB也就是只需要这么多内存。这也是网上能查到资料给到的可考虑方案之一。 但是这种方案肯呢个做最基本的黑名单记录某个位上标记为1就表示它被拉黑了。为0就没被拉黑但是如果我需要记录ip访问的次数封禁的时长这些呢这个方案做不到。你说再用一个结构体来存储这些需要的信息那不是得不偿失redis来存储不说内存占用多大redis 是有网络开销的性能就不达标了对应IPv4 的四段关系采用四层引用来关系来存储当前方案。存储结构如[256]*[256]*[256]*[4]int64
第4方案优缺点
存储IP的方法
例如我要存储ip127.0.0.1那么我就会存储成[127]*[0]*[0]*[4]int64上最后这个1存在哪里就是用位运算去处理这里不再细说
缺点
当ip存储量很大时占用的空间多余直接一维数组的方式因为指针和空值也是要占用空间的而且不少和第二种方案一样同样无法记录更多的信息
优点
链式索引结构查找性能和一维数组的形式无明显差异性能非常好当ip量不大时我不需要像一维数组那样初始化就搞个那么大的数组这个方案最初只需要初始化[256]个空值占用空间4096字节64位机器上公网上很多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。而不是一个结构体因为这样可以节省很多空间。
简化的方案固然能表达的信息就少很多了
指针索引为nil或[256]int8所在位置上的值为0表示该ip未记录[256]int8上记录的值大于0表示该IP的访问次数。由于int8正数最大127所以最多记录127次访问[256]int8上记录的值小于0表示该IP被禁用时长单位分钟。由于int8负数最小负128且-128保留作为永封的标记所以最多表示的封禁时长是127分钟。如上所说-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