zoukankan      html  css  js  c++  java
  • 可恶的爬虫直接把生产6台机器爬挂了!

    引言

    • 正在午睡,突然收到线上疯狂报警的邮件,查看这个邮件发现这个报警的应用最近半个月都没有发布,应该不至于会有报警,但是还是打开邮件通过监控发现是由于某个接口某个接口流量暴增,CPU暴涨。为了先解决问题只能先暂时扩容机器了,把机器扩容了一倍,问题得到暂时的解决。最后复盘为什么流量暴增?由于最近新上线了一个商品列表查询接口,主要用来查询商品信息,展示给到用户。业务逻辑也比较简单,直接调用底层一个soa接口,然后把数据进行整合过滤,排序推荐啥的,然后吐给前端。这个接口平时流量都很平稳。线上只部署了6台机器,面对这骤增的流量,只能进行疯狂的扩容来解决这个问题。扩容机器后一下问题得到暂时的解决。后来经过请求分析原来大批的请求都是无效的,都是爬虫过来爬取信息的。这个接口当时上线的时候是裸着上的也没有考虑到会有爬虫过来。

    解决办法

    • 既然是爬虫那就只能通过反爬来解决了。自己写一套反爬虫系统,根据用户的习惯,请求特征啥的,浏览器cookie、同一个请求频率、用户ID、以及用户注册时间等来实现一个反爬系统。
    • 直接接入公司现有的反爬系统,需要按照它提供的文档来提供指定的格式请求日志让它来分析。
      既然能够直接用现成的,又何必自己重新造轮子呢。最后决定还是采用接入反爬系统的爬虫组件。爬虫系统提供了两种方案如下:

    方案1:

    • 爬虫系统提供批量获取黑名单IP的接口(getBlackIpList)和移除黑名单IP接口(removeBlackIp)。
      业务项目启动的时候,调用getBlackIpList接口把所有IP黑名单全部存入到本地的一个容器里面(Map、List),中间会有一个定时任务去调用getBlackIpList接口全量拉取黑名单(黑名单会实时更新,可能新增,也可能减少)来更新这个容器。
    • 每次来一个请求先经过这个本地的黑名单IP池子,IP是否在这个池子里面,如果在这个池子直接返回爬虫错误码,然后让前端弹出一个复杂的图形验证码,如果用户输入验证码成功(爬虫基本不会去输入验证码),然后把IP从本地容器移除,同时发起一个异步请求调用移除黑名单IP接口(removeBlackIp),以防下次批量拉取黑单的时候又拉入进来了。然后在发送一个activemq消息告诉其他机器这个IP是被误杀的黑名单,其他机器接受到了这个消息也就会把自己容器里面这个IP移除掉。(其实同步通知其他机器也可以通过把这个IP存入redis里面,如果在命中容器里面是黑名单的时候,再去redis里面判断这个ip是否存在redis里面,如果存在则说明这个ip是被误杀的,应该是正常请求,下次通过定时任务批量拉取黑名单的时候,拉取完之后把这个redis里面的数据全部删除,或者让它自然过期。
      这种方案:性能较好,基本都是操作本地内存。但是实现有点麻烦,要维护一份IP黑名单放在业务系统中。
      在这里插入图片描述

    方案2:

    • 爬虫系统提供单个判断IP是否黑名单接口checkIpIsBlack(但是接口耗时有点长5s)和移除黑名单IP接口(removeBlackIp)。每一个请求过来都去调用爬虫系统提供的接口(判断IP是否在黑名单里面)这里有一个网络请求会有点耗时。如果爬虫系统返回是黑名单,就返回一个特殊的错误码给到前端,然后前端弹出一个图形验证码,如果输入的验证码正确,则调用爬虫系统提供的移除IP黑名单接口,把IP移除。
      这种方案:对于业务系统使用起来比较简单,直接调用接口就好,没有业务逻辑,但是这个接口耗时是没法忍受的,严重影响用户的体验
      最终综合考虑下来最后决定采用方案1.毕竟系统对响应时间是有要求的尽量不要增加不必要的耗时。

    方案1 实现

    方案1伪代码实现 我们上文《看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap》有提到过对于读多写少的线程安全的容器我们可以选择CopyOnWrite容器。

    static CopyOnWriteArraySet blackIpCopyOnWriteArraySet = null;
        /**
         * 初始化
         */
        @PostConstruct
        public void init() {
            // 调用反爬系统接口 拉取批量黑名单
            List<String> blackIpList = getBlackIpList();
            // 初始化
            blackIpCopyOnWriteArraySet = new CopyOnWriteArraySet(blackIpList);
        }
    
        /**
         * 判断IP 是否黑名单
         * @param ip
         * @return
         */
        public boolean checkIpIsBlack(String ip) {
          boolean checkIpIsBlack =  blackIpCopyOnWriteArraySet.contains(ip);
           if (!checkIpIsBlack ) 
           		return false;
           // 不在redis白名单里面
           if (!RedisUtils.exist(String.format("whiteIp_%", ip)){
          	 	return false;
    		} 
           return  true;
        }
    

    上线后经过一段时间让爬虫系统消费我们的请求日志,经过一定模型特征的训练,效果还是很明显的。由于大部分都是爬虫很多请求直接就被拦截了,所以线上的机器可以直接缩容掉一部分了又回到了6台。但是好景不长,突然发现GC次数频繁告警不断。为了暂时解决问题,赶紧把生产机器进行重启(生产出问题之后,除了重启和回退还有什么解决办法吗),并且保留了一台机器把它拉出集群,重启之后发现过又是一样的还是没啥效果。通过dump线上的一台机器,通过MemoryAnalyzer分析发现一个大对象就是我们存放IP的大对象,存放了大量的的IP数量。这个IP存放的黑名单是放在一个全局的静态CopyOnWriteArraySet,所以每次gc 它都不会被回收掉。只能临时把线上的机器配置都进行升级,由原来的8核16g直接变为16核32g,新机器上线后效果很显著。
    为啥测试环境没有复现?
    测试环境本来就没有什么其他请求,都是内网IP,几个黑名单IP还是开发手动构造的。

    解决方案

    业务系统不再维护IP黑名单池子了,由于黑名单来自反爬系统,爬虫黑名单的数量不确定。所以最后决定采取方案2和方案1结合优化。

    • 1.项目启动的时候把所有的IP黑名单全部初始化到一个全局的布隆过滤器
    • 2.一个请求过来先经过布隆过滤器,判断是否在布隆过滤器里面,如果在的话我们再去看看是否在redis白名单里面(误杀用户需要进行洗白)我们再去请求反爬系统判断IP是否是黑名单接口,如果接口返回是IP黑名单直接返回错误码给到前端,如果不是直接放行(布隆过滤器有一定的误判,但是误判率是非常小的,所以即使被误判了,最后再去实际请求接口,这样的话就不会存在真正的误判真实用户)。如果不存在布隆器直接放行。
    • 3.如果是被误杀的用户,用户进行了IP洗白,布隆过滤器的数据是不支持删除(布谷鸟布隆器可以删除(可能误删)),把用户进行正确洗白后的IP存入redis里面。(或者一个本地全局容器,mq消息同步其他机器)
      下面我们先来了解下什么是布隆过滤器把。
    什么是布隆过滤器

    布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

    上述出自百度百科。
    说白了布隆过滤器主要用来判断一个元素是否在一个集合中,它可以使用一个位数组简洁的表示一个数组。它的空间效率和查询时间远远超过一般的算法,不过它存在一定的误判的概率,适用于容忍误判的场景。如果布隆过滤器判断元素存在于一个集合中,那么大概率是存在在集合中,如果它判断元素不存在一个集合中,那么一定不存在于集合中。

    实现原理

     布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点,把它们置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说“可能”是误差的存在)。底层是采用一个bit数组和几个哈希函数来实现。
    在这里插入图片描述
    在这里插入图片描述
    下面我们以一个 bloom filter 插入"java" 和"PHP"为例,每次插入一个元素都进行了三次hash函数
    java第一次hash函数得到下标是2,所以把数组下标是2给置为1
    java第二次Hash函数得到下标是3,所以把数组下标是3给置为1
    java第三次Hash函数得到下标是5,所以把数组下标是5给置为1
    PHP 第一次Hash函数得到下标是5,所以把数组下标是5给置为1
    ...
    查找的时候,当我们去查找C++的时候发现第三次hash位置为0,所以C++一定是不在不隆过滤器里面。但是我们去查找“java”这个元素三次hash出来对应的点都是1。只能说这个元素是可能存在集合里面。

    • 布隆过滤器添加元素
    1. 将要添加的元素给k个哈希函数
    2. 得到对应于位数组上的k个位置
    3. 将这k个位置设为1
    • 布隆过滤器查询元素
    1. 将要查询的元素给k个哈希函数
    2. 得到对应于位数组上的k个位置
    3. 如果k个位置有一个为0,则肯定不在集合中
    4. 如果k个位置全部为1,则可能在集合中

    使用BloomFilter

    引入pom

     <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>23.0</version>
      </dependency> 
    
        public static int count = 1000000;
        private static BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")), count,0.009);
        public static void main(String[] args) {
            int missCount = 0;
            for (int i = 0; i < count; i++) {
                bf.put(i+"");
            }
            for (int i = count; i < count+1000000; i++) {
                boolean b = bf.mightContain(i +"");
                if (b) {
                    missCount++;
                }
            }
            System.out.println(new BigDecimal(missCount).divide(new BigDecimal(count)));
        }
    

    解决问题

    布隆过滤器介绍完了,我们再回到上述的问题,我们把上述问题通过伪代码来实现下;

       /**
         * 初始化
         */
        @PostConstruct
        public void init() {
            // 这个可以通过配置中心来读取
            double fpp = 0.001;
            // 调用反爬系统接口 拉取批量黑名单
            List<String> blackIpList = getBlackIpList();
            // 初始化 不隆过滤器
            blackIpBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")), blackIpList.size(), fpp);
            for (String ip: blackIpList) {
                blackIpBloomFilter.put(ip);
            }
        }
        /**
         * 判断是否是爬虫
    	 */
        public boolean checkIpIsBlack(String ip) {
            boolean contain = blackIpBloomFilter.mightContain(ip);
            if (!contain) {
                return false;
            }
             // 不在redis白名单里面
           if (!RedisUtils.exist(String.format("whiteIp_%", ip)){
          	 	return false;
    		} 
            // 调用反爬系统接口 判断IP是否在黑名单里面
        }
    

    总结

    上述只是列举了通过IP来反爬虫,这种反爬的话只能应对比较低级的爬虫,如果稍微高级一点的爬虫也可以通过代理IP来继续爬你的网站,这样的话成本可能就会加大了一点。爬虫虽然好,但是还是不要乱爬,“爬虫爬的好,牢饭吃到饱

    结束

    • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
    • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
    • 感谢您的阅读,十分欢迎并感谢您的关注。
  • 相关阅读:
    线段树专辑—— pku 1436 Horizontally Visible Segments
    线段树专辑——pku 3667 Hotel
    线段树专辑——hdu 1540 Tunnel Warfare
    线段树专辑—— hdu 1828 Picture
    线段树专辑—— hdu 1542 Atlantis
    线段树专辑 —— pku 2482 Stars in Your Window
    线段树专辑 —— pku 3225 Help with Intervals
    线段树专辑—— hdu 1255 覆盖的面积
    线段树专辑—— hdu 3016 Man Down
    Ajax跨域访问
  • 原文地址:https://www.cnblogs.com/root429/p/14299024.html
Copyright © 2011-2022 走看看