一波爬蟲,直接把年終獎乾沒了

引言

  • 正在午睡,忽然收到線上瘋狂報警的郵件,查看這個郵件發現這個報警的應用最近半個月都沒有發佈,應該不至於會有報警,可是仍是打開郵件經過監控發現是因爲某個接口某個接口流量暴增,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容器。java

    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仍是開發手動構造的。redis

    解決方案

    業務系統再也不維護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

      引入pomcookie

      <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來繼續爬你的網站,這樣的話成本可能就會加大了一點。爬蟲雖然好,可是仍是不要亂爬,「爬蟲爬的好,牢飯吃到飽異步

結束

  • 因爲本身才疏學淺,不免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。
    在這裏插入圖片描述
相關文章
相關標籤/搜索