数据采集变慢,不一定是 HTTP 请求本身慢。一次初始化阻塞,问题出在代理资源的分配方式:我们把可以共享的配置当成必须独占的资源,过早分配给了各个采集类。
资源没有用完,请求却拿不到
系统有 100 个代理,初始化时放进 ArrayBlockingQueue,每个采集类一次取走 10 个并长期持有。前 10 个类完成分配后,第 11 个类再取代理就会等待,而前面的类并没有归还机制。
这不是队列性能差,而是资源生命周期与设计不一致。若一个代理允许多个请求使用,就不应该在采集类初始化时永久占有;若代理确实只能服务有限并发,就应该按请求借出、用完归还,并为等待设置超时。
对可共享配置进行轮询
只需要在请求之间均匀选择代理时,可以保留一份不可变配置,用原子操作分配下标。下面的通用选择器只使用 JDK 8 API,示例中的 T 可以换成代理配置类型:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public final class RoundRobin<T> {
private final List<T> entries;
private final AtomicInteger next = new AtomicInteger();
public RoundRobin(List<T> entries) {
if (entries == null || entries.isEmpty()) {
throw new IllegalArgumentException("entries must not be empty");
}
if (entries.contains(null)) {
throw new IllegalArgumentException("entries must not contain null");
}
this.entries = Collections.unmodifiableList(new ArrayList<>(entries));
}
public T get() {
int index = next.getAndUpdate(i -> i == entries.size() - 1 ? 0 : i + 1);
return entries.get(index);
}
}
下标读取和回绕发生在同一次原子更新里,不需要先递增再由另一个操作重置,因此不会因为多个线程同时到达末尾而读出越界下标。构造时复制配置,避免外部增删列表导致长度变化;配置对象自身仍应不可变或另行保证线程安全。
轮询不等于资源池
这个选择器只负责选择,不保证代理可用,也不限制同一代理上的并发。请求耗时不同,轮询次数均匀也不代表负载均匀。
在真实采集中,还需要记录连接失败、响应耗时和目标站点限流情况。短暂失败应有有界重试与退避,不可用代理应暂时退出选择;是否恢复应通过探测确认。共享配置的选择与连接池、并发许可是不同职责,不能用一个下标计数器全部解决。
最后,初始化也应按实际需要进行。没有被调度的采集任务不必提前持有连接或其他稀缺资源。先测量等待发生在哪里,再判断该优化请求、调度还是资源分配,通常比直接增加线程更有效。
参考:AtomicInteger。