24 December 2015

数据采集变慢,不一定是 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。





blog comments powered by Disqus
Fork me on GitHub