目录

Bird本周翻车集锦

这星期除了写代码就是各种性能分析、调优、测试,从睁眼到半夜闭眼。众所周知,代码越多,bug越多。这次写点不一样的,细数这星期出过的哭笑不得小插曲。

1. 将 MySQL 查询从 2000 ms 优化到 20 ms

听起来很牛是不是?怎么做到的呢?

最开始数据量小的时候,查询很快,到了几十万级别后,就慢到了 2000 ms甚至更多。

一开始觉得是索引问题,特地补学了索引什么的,但看了半天还是觉得,没啥问题啊??

又仔细看了一眼 gorm 打印出来的慢查询,卧槽,好像少了什么?

select 后面没有 limit!

看了眼代码,才意识到自己太久没用 gorm 了,加上使用 copilot 自动补的原因,把 Limit() 写到了 Find() 后面,导致 limit 未生效。

2. 数据库瓶颈

解决了 limit 失效问题,当数据量到击百万的时候,又慢下来了!!!

能怎么办?调优呗!!

我写的是一个爬虫,每次从数据库中获取一个 task(可以简单理解为一个链接),然后请求网页,得到内容后,解析并开启一个协程存入数据库。

而到了后期,从几百万级别的数据量中找到一个未完成的任务,耗时很久,这就成了瓶颈。

于是我们可以把它做成一次从数据库中取出 n 个任务,然后去爬。

但这又带来了另外一个问题,后续的请求和解析操作我是支持自定义并发数的,也就是多 worker 模式,并不能用简单的循环来解决。

所以我使用了任务池的概念,使用了 Go 的 channel。假设任务池的大小是200,每次获取50个数据,同时开启50个worker消耗这些任务(爬)。

这样就实现了数据库查询和后续解析的异步,虽然一次数据库查询很慢,但是只要任务池不满,就会有一个单独的协程去从数据库中获取任务;只要任务池不空,50 个 worker 就会不断地从池子里取任务。

如何实现池子不空就自动获取任务,池子不满就自动加任务,这两者的阻塞是如何实现的。这有点像操作系统的 PV 操作。如何实现?

得益于 Go 的 channel 的设计,我不需要关心如何实现,channel 的特性就是如此。

3. 数据库还是有瓶颈

从一开始设计的就是分布式爬虫,直接二进制把文件分发出去,任何系统,不需要环境就能跑,随时开启随时关闭,支持进程内多并发,也支持一个机器同时开启多个进程,也支持不同机器间一起跑,自动分配任务。

所以原先的设计是,大概每个机器的并发为 5 就差不多了,否则会被反爬,被封掉。

很牛逼是吧?

理想很丰满,现实很骨感。写完之后,发现根本找不到这么多机器来跑爬虫啊?

还是得上代理。

用了个代理,每秒支持一百个请求。所以我就在一个机器上跑,设置并发为100。

但发现数据库还是撑不住!!!

我看了一下阿里云RDS的后台,CPU直接干到 100% 了。请教了社团里其他人,说可能是缓存什么的问题。

后面又想了一下,我这数据量已经一千万了,但是买的是最垃圾的云数据库,一核CPU ,不到瓶颈才怪!!!

哭了,感受到了硬件的重要性。

后面还是采用了取巧的方式,用实验室的服务器上的数据库,但是在内网。然后用 frp ,通过我自己的服务器穿出去。

数据库性能瓶颈一下子没了,真香!

4. 参数调优

我的爬虫最后是以二进制的形式分发,因为这样不依赖于环境,已经准备了各个操作系统和芯片架构的版本。

同时,我写的爬虫的各种属性,例如并发数,任务池大小,每批的任务数等,都是可以在命令行运行的时候设置的,非常优雅。

同时,因为并发很大,所以把每次获取的任务量和任务池的大小都调得很高,几百上千的样子,防止后面的 worker 因为数据库查询而阻塞。

但我再去看代理,看代理的监控台,发现平均请求网页的次数还是没有达到预期,一定哪里又有瓶颈了!!!

百思不得其解。

于是我把日志打地更详细,后面发现,貌似还是因为任务池经常就空了。

即,添加任务(数据库查询)的速度赶不上任务消耗(爬取页面与解析)的速度。

这不可能啊?我当时大概设置的每批获取500个任务,任务池的大小2000,但是worker的并发是100左右。也就是说,一次获取的任务够所有 worker 请求 5 次,更何况任务池足足有 2000 !

后面发现了问题,出在哪呢?

我找到了这样一段代码:

if taskBatch > 100 {
    logrus.Info("每次获取任务的数量不能大于 100,已自动设置为 100")
    taskBatch = 100
}
if taskPoolCap < taskBatch {
    logrus.Info("任务池容量不能小于每次获取任务的数量,已自动设置为每次获取任务的数量")
    taskPoolCap = taskBatch
}
if taskPoolCap > 1000 {
    logrus.Info("任务池容量不能大于 1000,已自动设置为 1000")
    taskPoolCap = 1000
}

只能说平时写代码太注重健壮性了,聪明反被聪明误。为了防止其他人拿到程序乱设置参数,为每个参数设置了基本的设定。这在一开始,每个机器被设定为较小的并发的时候,是合理的。

但到后面一个机器上百并发的时候就不合理了。可我把这回事忘了!导致哪怕设置了每批次获取的任务数为 500 ,也会被自动修正成 100 。。。

5. 神奇的 502 问题

这星期我同时还在为一个开源项目写官方 `Helm Chart。

写完之后,代码推上去,release ,用测试机安装。有问题就修,改代码、推、安装,循环往复。

但是,所有的 pod 都起来了,甚至我进去看日志,也成功启动了,但是浏览器访问始终 502 !内部的网络也都是通的,就非常非常奇怪!

排查了半天,最后发现。我写的 helm 一次起两个应用,然后我复制项目配置的时候,把其中一个项目的端口复制错了,复制成了另外一个,导致端口一直没绑定上。。。。

所以很合理,确实启动成功了,也确实外面访问不到。。。