CPU
暴漲。爲了先解決問題只能先暫時擴容機器了,把機器擴容了一倍,問題獲得暫時的解決。最後覆盤爲何流量暴增?因爲最近新上線了一個商品列表查詢接口,主要用來查詢商品信息,展現給到用戶。業務邏輯也比較簡單,直接調用底層一個soa
接口,而後把數據進行整合過濾,排序推薦啥的,而後吐給前端。這個接口平時流量都很平穩。線上只部署了6臺機器,面對這驟增的流量,只能進行瘋狂的擴容來解決這個問題。擴容機器後一下問題獲得暫時的解決。後來通過請求分析原來大批的請求都是無效的,都是爬蟲過來爬取信息的。這個接口當時上線的時候是裸着上的也沒有考慮到會有爬蟲過來。 cookie
、同一個請求頻率、用戶ID
、以及用戶註冊時間等來實現一個反爬系統。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是否黑名單接口checkIpIsBlack
(可是接口耗時有點長5s)和移除黑名單IP
接口(removeBlackIp
)。每個請求過來都去調用爬蟲系統提供的接口(判斷IP
是否在黑名單裏面)這裏有一個網絡請求會有點耗時。若是爬蟲系統返回是黑名單,就返回一個特殊的錯誤碼給到前端,而後前端彈出一個圖形驗證碼,若是輸入的驗證碼正確,則調用爬蟲系統提供的移除IP
黑名單接口,把IP
移除。
這種方案:對於業務系統使用起來比較簡單,直接調用接口就好,沒有業務邏輯,可是這個接口耗時是無法忍受的,嚴重影響用戶的體驗
最終綜合考慮下來最後決定採用方案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結合優化。算法
IP
黑名單所有初始化到一個全局的布隆過濾器redis
白名單裏面(誤殺用戶須要進行洗白)咱們再去請求反爬系統判斷IP
是不是黑名單接口,若是接口返回是IP
黑名單直接返回錯誤碼給到前端,若是不是直接放行(布隆過濾器有必定的誤判,可是誤判率是很是小的,因此即便被誤判了,最後再去實際請求接口,這樣的話就不會存在真正的誤判真實用戶)。若是不存在布隆器直接放行。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。只能說這個元素是可能存在集合裏面。安全
引入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
來繼續爬你的網站,這樣的話成本可能就會加大了一點。爬蟲雖然好,可是仍是不要亂爬,「爬蟲爬的好,牢飯吃到飽」異步