13

可恶的爬虫直接把生产6台机器爬挂了!

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIyMjQwMTgyNA%3D%3D&%3Bmid=2247485131&%3Bidx=1&%3Bsn=08f23605ae311689e40458f007d5ad9f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

点击上方“ Java金融 ”,选择“ 设为星标”

后台回复"888 "获取bat面试题集

引言

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

解决办法

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

方案1:

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

方案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 黑名单全部初始化到一个全局的布隆过滤器
  • redis
    IP
    IP
    
  • IP
    IP
    redis
    mq
    

什么是布隆过滤器

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

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

实现原理

布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点,把它们置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说“可能”是误差的存在)。底层是采用一个bit数组和几个哈希函数来实现。 ArmyMfa.png!mobilej2EBZzm.png!mobile 下面我们以一个 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 来继续爬你的网站,这样的话成本可能就会加大了一点。爬虫虽然好,但是还是不要乱爬,“ 爬虫爬的好,牢饭吃到饱

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

NfyMVz2.png!mobile

最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构、等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK