需求是每秒查一次 MySQL ,发现有新任务就开始爬数据,速度要求每分钟 1~2 万个网页(内容有时效性,必须尽快爬完,每个网页的响应速度差别很大),解析出需要的数据后存入 MySQL 。(需要爬取的 URL 在程序运行时动态接收并添加到 MySQL 中,超时 /失败则按 MySQL 中的该域名的指定间隔(大部分是 1 秒,也有一些是 1 小时)重试,达到 MySQL 中的指定次数则在 MySQL 中标记失败不再重试)
目前的做法是 async 异步请求(域名超时和重试次数限制用一个缓存模块自动缓存到 Redis ,失败任务如果设置的重试间隔小于一分钟则 Redis 存下次重试时间,否则存 MySQL ,MySQL 查询任务列表的时候用条件过滤没到时间的)+SemaphoreSlim 限制最大同时执行任务数量为 400 (因为有时候一分钟会添加十几万个网页,同时发这么多请求程序会卡死)。发现爬取速度比较慢,查了下 CPU 有一个核心占用 100%,剩下都不到 1%。带宽因为大部分是下行,阿里云共享不知道多大的下行带宽只用了 18Mbps ,独享 7Mbps 上行带宽只用了 2Mbps 。MySQL 用的是阿里云云数据库。Redis 在本机(指爬数据的服务器上)。
被爬网站均没有限流。
1
Juszoe 2022-05-31 20:03:06 +08:00
不懂 C#,但是 async 异步在大部分语言里都是单线程运行吧
|
3
Juszoe 2022-05-31 20:16:06 +08:00
@dfgxcvbcv #2 你的任务负载这么大,得上多线程才能利用更多的核心。将任务分解出来,每个线程内可以用 async ,一个线程带几万个协程还是太为难了
|
5
Building 2022-05-31 20:31:02 +08:00
多线程应该和 thread 有关吧,async 只是把任务放到不同的 queue ,thread 不变,当然就只有一个核心的 CPU 在跑,不知道 C# 是不是这样
|
6
ikesnowy 2022-05-31 20:34:21 +08:00
你在本地运行可以跑满多个 CPU 吗?排除服务器和部署的原因,如果本地也跑不满的话就是代码写得有问题了。
|
7
thinkershare 2022-05-31 20:37:35 +08:00
使用一个主线程去发送请求(和 MySQL 打交道, 发送异步请求), 关闭同步上下文, 然后在主线程上发送 HTTP 请求, 然后主线程就等待(用 Task.Delay()), 时间到了再次轮询数据库. 在 HTTP 的 IO 的 Task 上附加成功的后续操作, 这个操着会在线程池中获取一个空闲线程, 然后执行. 一开始配置好线程池的物理线程数量和 I/O 线程数量. 你给出来的信息太有限了, 很难给你分析. 另外真个场景我估计瓶颈不在 CPU, 如果后续操作费事很少, 则 CPU 应该一直空闲, 如果你开启了上下文同步, 则可能导致主线程假死!
|
8
thinkershare 2022-05-31 20:38:41 +08:00
@Building C#的 Task 比较复杂, 并不是简单的协程, 既可以多线程, 也可以单线程
|
9
Soar360 2022-05-31 20:43:12 +08:00
如果是 framework 的话,看看是不是限制并发了?
|
10
Buges 2022-05-31 20:43:43 +08:00 via Android
你这种情况,盲猜是阻塞了。哪里有 CPU 密集型都任务,把线程给占了。
纯异步 IO 话单线程 /多线程的 executor 没有太显著的差异。 |
11
ragnaroks 2022-05-31 20:44:00 +08:00
不知道楼主什么水平,但是并不是 public async void function1() 就是异步,没有代码不好诊断,可以先把任务压倒一个 list ,用并行去跑,简单快捷
|
12
netnr 2022-05-31 21:16:35 +08:00
C# 并行任务 Parallel 类
https://www.netnr.com/home/list/139 |
13
ration 2022-05-31 21:34:13 +08:00 via Android
异步的话所有方法都得异步,排查下是不是有些方法写成同步阻塞了。
|
14
darklights 2022-05-31 22:29:48 +08:00
async 是异步,不是并行,如果代码这样写:
while (link != null) { var content = await Fetch(link); await Save(content); link = FindNext(html); } 那跑起来跟 JS 一样,只能用一个核。 |
15
SMGdcAt4kPPQ 2022-05-31 22:56:34 +08:00
不发代码不好分析,可能就是和楼上说的一样立即 await
|
16
forgottencoast 2022-05-31 23:39:04 +08:00
上面很多人说了,异步和并行是两个概念。
默认情况下异步你用 await 的话,CPU 就是单线程的,但是 IO 方面可以并发。 简单一点,你可以不要 await ,然后任务完成以后自己处理回调。 要想优雅就用前面#12 提到的 Parallel 。 一般情况下爬虫 CPU 不应该跑满的,除非你大量分析内容,带宽要先跑满,你还是要重新分析一下自己的代码。 |
17
fanxiao 2022-06-01 08:31:51 +08:00
直接将这个 task 丢到 ThreadPool 里面运行就好了 , 主线程 await io 资源还没有返回,不会往下面执行,本质跟你写个同步语句没有区别,只是当 io 资源满足之后方便回到现场,所以还需要 task.Run 或者 ThreadPool 来支持多线程.
|
18
fanxiao 2022-06-01 08:33:19 +08:00
https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming
Task-based asynchronous programming 可以看看微软的文档 |
19
ClorisYe 2022-06-01 09:59:41 +08:00
异步是开辟一个后台线程去做 IO 的,当你 await 的时候,主线程在等待异步线程的执行回调。所以,异步本身是多线程实现的,这个你可以通过在异步方法内打印线程 ID 可以看到与主线程的是不一样的。如果你想同时开多个异步 IO ,你可以把任务存放到一个队列里面,然后遍历 await 。这样批量任务可以同时执行。
|
20
beginor 2022-06-01 12:45:31 +08:00 via Android
#12 楼上正解,parallel 可以调用全部 CPU 核心
|
21
billzhuang 2022-06-01 15:14:35 +08:00
不管是查 mysql 还是爬虫,都是 io bound ,正确使用 async/await 一个核心都嫌多。
|