目录

Bird本周翻车集锦

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

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

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

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

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

select 后面没有 limit!

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

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

能怎么办?调优呗!!

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

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

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

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

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

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

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

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

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

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

很牛逼是吧?

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

还是得上代理。

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

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

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

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

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

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

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

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

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

同时,因为并发很大,所以把每次获取的任务量和任务池的大小都调得很高,几百上千的样子,防止后面的 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 。。。

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

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

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

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

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