背景
在大规模舆情数据采集和市场情报挖掘场景中,爬虫能够正常请求的频率和规模常常受到目标网站的限制。代理IP作为突破访问限制的利器,在实际应用中却面临着诸多挑战:
- 代理IP质量参差不齐,可用性不稳定
- 不同供应商的API接口和数据格式不统一
- 流量使用不均衡,资源利用率低下
- 缺乏有效的监控和调度机制
针对这些问题,我实现了一套 “爬虫代理IP资源智能调度系统”。通过可用性评估模型与隧道代理服务的结合,实现代理IP的全生命周期管理与动态调度。
方案目标与核心思路
本系统旨在通过自研隧道代理服务,对代理IP资源进行统一管理、实时监控、智能分配。
它能够从多个维度评估IP质量(频次、流量、区域、连接成功率等),计算出综合“可用分数”,并基于分数进行动态分配与回收。
本方案解决的核心问题:
- 代理IP统一管理:支持多供应商、不同格式的代理源接入与标准化处理;
- 多维度数据采集:通过频次、流量、成功率、区域等多维度数据计算综合得分;
- 智能调度引擎:根据分数动态调整分配策略,保证资源高效利用;
- 可视化监控与告警:通过看板实时查看IP使用情况与可用率,及时发现异常。
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 代理IP资源池 │───▶ │ 多维度数据采集 │───▶│ 智能调度引擎 │
│ │ │ │ │ │
│ • 供应商代理IP │ │ • 使用频次 │ │ • 权重计算 │
│ • 自建VPS │ │ • 流量统计 │ │ • 动态分配 │
│ • VPN转发 │ │ • 区域信息 │ │ • 阈值监控 │
└─────────────────┘ │ • 连接成功率 │ └─────────────────┘
└──────────────────┘ │
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ 数据存储层 │ │ 租户应用 │
│ │ │ │
│ • MySQL │ │ • 代理提取 │
│ • ClickHouse │ │ • 使用监控 │
│ • Redis │ │ • 看板展示 │
└────────────────┘ └─── ────────────┘
核心功能模块详解
1. 代理IP资源统一管理
资源接入多样化
- 支持多供应商代理IP接入
- 兼容自建VPS和VPN网络转发
- 统一的认证和信息管理接口
数据存储优化 采用MySQL持久化存储 + Redis缓存查询,确保高性能读写。
-- 外部代理列表信息表:存储所有可用的外部代理基础信息
CREATE TABLE `t_tunnel_proxy_external_list` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`protocol` varchar(10) DEFAULT 'HTTP' COMMENT '代理协议 HTTP, SOCKS5',
`proxy_code` varchar(100) NOT NULL COMMENT '代理唯一编码',
`proxy_category` varchar(100) DEFAULT NULL COMMENT '代理类别',
`proxy_ip` varchar(100) DEFAULT NULL COMMENT '代理IP地址',
`proxy_port` int(11) DEFAULT NULL COMMENT '代理端口',
`need_auth` tinyint unsigned DEFAULT NULL COMMENT '是否需要认证',
`auth_user` varchar(100) DEFAULT NULL COMMENT '认证用户名',
`auth_pwd` varchar(100) DEFAULT NULL COMMENT '认证密码',
`country` varchar(50) DEFAULT NULL COMMENT '所在国家',
`province` varchar(100) DEFAULT NULL COMMENT '所在省份',
`city` varchar(200) DEFAULT NULL COMMENT '所在城市',
`data_source` varchar(100) DEFAULT NULL COMMENT '数据来源',
`provider` varchar(100) DEFAULT NULL COMMENT '服务提供商',
`status` varchar(10) DEFAULT 'ENABLE' COMMENT '状态 ENABLE使用中; DISABLE禁止使用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `status_index` (`status`) USING BTREE,
KEY `data_source_index` (`data_source`) USING BTREE,
KEY `provider_index` (`provider`) USING BTREE,
KEY `proxy_code_index` (`proxy_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='外部代理列表信息表:存储所有可用的外部代理基础信息';
-- 隧道代理客户端信息表:存储使用代理服务的客户端(租户)基本信息
CREATE TABLE `t_tunnel_proxy_clients` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`parent_client_id` varchar(100) default '' COMMENT '父客户端ID',
`client_id` varchar(100) NOT NULL COMMENT '客户端唯一标识',
`client_key` varchar(100) not null comment '客户端请求密钥',
`client_secret` varchar(100) not null comment '客户端访问密钥',
`client_role` int(11) unsigned default '1' not null comment '客户端角色 1 管理员; 0 子账号',
`client_name` varchar(255) NOT NULL COMMENT '客户端名称',
`creator` varchar(100) NOT NULL COMMENT '创建人',
`applicant` varchar(100) NOT NULL COMMENT '申请人',
`dept` varchar(100) NOT NULL COMMENT '所属部门',
`description` varchar(255) DEFAULT NULL COMMENT '描述信息',
`client_status` varchar(10) DEFAULT 'ACTIVATE' COMMENT '状态 ACTIVATE使用中; INACTIVATE失效',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_client_id` (`client_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='隧道代理客户端信息表:存储使用代理服务的客户端(租户)基本信息';
-- 隧道代理客户端限制配置表:存储客户端的代理使用限制参数
CREATE TABLE `t_tunnel_proxy_client_limits` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`parent_client_id` varchar(100) default '' COMMENT '父客户端ID',
`client_id` varchar(100) NOT NULL COMMENT '客户端唯一标识',
`max_proxy_count` int(11) unsigned DEFAULT NULL COMMENT '最大分配代理数',
`allowed_providers` varchar(200) DEFAULT NULL COMMENT '允许使用的服务提供商, 多个用英文逗号隔开',
`max_connections` int(11) unsigned DEFAULT NULL COMMENT '最大连接数限制',
`bandwidth_limit` bigint(20) unsigned DEFAULT NULL COMMENT '带宽限制(单位:Bytes)单次连接的最大带宽流量',
`traffic_quota` bigint(20) unsigned DEFAULT NULL COMMENT '流量配额(单位:Bytes)一旦用尽则账号失效',
`white_ips` varchar(100) NOT NULL COMMENT 'IP白名单限制 当不为空则仅该IP可以调用提取',
`white_domains` varchar(100) NOT NULL COMMENT '域名白名单限制 当不为空则仅有该网站放行',
`black_domains` varchar(100) NOT NULL COMMENT '域名黑名单限制 当不为空则仅有该网站禁止',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
CONSTRAINT `fk_client_id` FOREIGN KEY (`client_id`) REFERENCES `t_tunnel_proxy_clients` (`client_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隧道代理客户端限制配置表:存储客户端的代理使用限制参数';
-- 隧道代理客户端映射表:记录客户端与代理的绑定关系
CREATE TABLE `t_tunnel_proxy_client_mapping` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`client_id` varchar(100) NOT NULL COMMENT '客户端唯一标识',
`mapping_id` varchar(100) NOT NULL COMMENT '映射唯一标识, 规则是 client_id + 序号',
`proxy_code` varchar(100) NOT NULL COMMENT '代理唯一编码',
`mapping_status` varchar(10) DEFAULT 'ENABLE' COMMENT '映射状态 ENABLE使用中; DISABLE禁止使用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_mapping` (`client_id`, `proxy_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='隧道代理客户端映射表:记录客户端与代理的绑定关系';
2. 多维度数据采集体系
静态属性采集
通过集成第三方IP信息服务(如scamalytics.com、ipinfo.io),定时更新代理IP的:
- 地理位置信息(国家、城市、经纬度)
- 网络属性(ISP、组织关联)
- 风险评分
动态使用监控
基于隧道代理服务实现实时数据上报:
| 监控指标 | 采集方式 | 存储目标 |
|---|---|---|
| 使用流量 | 实时上报 | ClickHouse |
| 请求频次 | 计数统计 | ClickHouse |
| 状态码分布 | 日志分析 | ClickHouse |
| 响应时间 | 时间戳计算 | ClickHouse |
3. 智能权重分配算法
核心计算公式:
def calculate_availability_score(ip_stats):
"""
计算代理IP可用性分数
分数越高表示IP质量越好,优先分配
"""
# 归一化处理(防零值和平滑处理)
frequency_norm = log10(max(ip_stats.frequency, 1)) / log10(max_frequency)
traffic_norm = log10(max(ip_stats.traffic, 1)) / log10(max_traffic)
# 权重配置(可动态调整)
# 取经验值
weights = {
'frequency': 0.3, # 使用频次权重
'traffic': 0.3, # 流量使用权重
'region': 0.2, # 区域权重
'success': 1.0 # 成功率基础权重
}
# 区域系数映射(根据业务需求配置)
region_coefficient = get_region_coefficient(ip_stats.region)
# 综合分数计算
score = (
weights['frequency'] * (1 - frequency_norm) +
weights['traffic'] * (1 - traffic_norm) +
weights['region'] * region_coefficient +
ip_stats.success_rate
) / (weights['frequency'] + weights['traffic'] + weights['region'] + 1)
return score
算法特点:
- 📊 对数归一化:压缩极端值影响,提高数据稳定性
- ⚖️ 可配置权重:根据不同业务场景调整权重参数
- 🔄 动态更新:定时重新计算,反映最新IP状态
- 🎯 业务导向:使用频次越低、流量越小、成功率越高的IP分数越高
4. 动态分配策略
初次分配流程
用户申请 → 填写需求信息 → 计算可用IP分数 → 按分数排序 → 分配高分数IP → 建立租户关联
定时监控与再分配
# 定时监控脚本核心逻辑
def monitor_and_reallocate():
tenants = get_all_tenants()
for tenant in tenants:
allocated_ips = get_tenant_ips(tenant.id)
available_count = count_available_ips(allocated_ips)
if available_count < tenant.min_threshold:
# 触发再分配
additional_ips = allocate_additional_ips(
tenant,
tenant.min_threshold - available_count
)
if len(additional_ips) == 0:
# 资源不足告警
send_alert(tenant, "IP资源不足")
核心交互细节
隧道代理的分配和校验流程
- 代理分配系统:
tunnel-proxy分配系统负责用户账号的创建及代理资源的动态分配。 - 账号校验与代理提取服务:此流程用于校验隧道代理服务的认证合法性,并处理代理提取操作。
租户创建流程以及代理分配流程时序图
核心数据转发代码
package service
import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
uuid "github.com/satori/go.uuid"
"github.com/panjf2000/ants/v2"
"github.com/sirupsen/logrus"
"io"
"net"
"net/http"
"net/url"
"runtime/debug"
"strings"
"sync/atomic"
"time"
"tproxy/internal/client"
"tproxy/internal/hook"
"tproxy/internal/models"
"tproxy/internal/protocol"
)
// ============================
// 核心结构定义
// ============================
type TproxyHTTPAgent struct {
Host string
Port int
done int32
ConnectPoolSize int32
Ants *ants.Pool
}
type tproxyHTTPContext struct {
F logrus.Fields
Proxyer *client.TproxyClient
ProxyReq *protocol.ProxyRequest
Mtr *models.TrafficMonitorData
}
// ============================
// 启动监听入口
// ============================
func (t *TproxyHTTPAgent) Listen() {
if t.Host == "" {
t.Host = "127.0.0.1"
}
if t.Port == 0 {
t.Port = 50100
}
cert := &Certificate{ValidFor: 365 * 24 * time.Hour, KeySize: 2048}
genCert, err := cert.GenCertificate()
if err != nil {
logrus.WithField("action", "Tproxy").Panicf("创建证书失败: %v", err)
}
options := ants.Options{PreAlloc: true, Nonblocking: false}
pool, err := ants.NewPool(int(t.ConnectPoolSize), ants.WithOptions(options))
if err != nil {
logrus.Errorf("协程池创建失败: %v", err)
return
}
t.Ants = pool
addr := fmt.Sprintf("%s:%d", t.Host, t.Port)
logrus.Infof("Tproxy 启动监听: %s", addr)
server := http.Server{
Addr: addr,
Handler: t,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{genCert}},
}
server.ListenAndServe()
}
// ============================
// ServeHTTP 分发
// ============================
func (t *TproxyHTTPAgent) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/health/tproxy_service":
t.handleMonitor(w, r)
default:
t.handleTunnel(w, r)
}
}
func (t *TproxyHTTPAgent) handleMonitor(w http.ResponseWriter, r *http.Request) {
logrus.Infof("[Monitor] method=%s path=%s from=%s", r.Method, r.URL.Path, r.RemoteAddr)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":200}`))
}
// ============================
// 隧道主逻辑
// ============================
func (t *TproxyHTTPAgent) handleTunnel(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
logrus.Errorf("[Panic] %v\n%s", err, debug.Stack())
}
atomic.AddInt32(&t.done, -1)
}()
ctx := &tproxyHTTPContext{
Proxyer: &client.TproxyClient{},
ProxyReq: &protocol.ProxyRequest{Proto: r.Proto},
Mtr: models.NewTrafficMonitorData(),
}
ctx.F = logrus.Fields{"seq": ctx.Mtr.RndSeqID}
ctx.Mtr.ConnectingHost = r.Host
ctx.Mtr.DestinationAddr = r.Host
ctx.Mtr.SourceAddr = getRealIP(r)
ctx.Mtr.Method = r.Method
ctx.Mtr.Protocol = r.Proto
if atomic.AddInt32(&t.done, 1) > t.ConnectPoolSize {
ctx.Mtr.Message = "连接过多"
ctx.Mtr.ErrorOrReport()
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
logrus.WithFields(ctx.F).Info("开始认证代理凭证")
authURL := t.authenticate(w, r, ctx)
if authURL == nil {
logrus.WithFields(ctx.F).Warn("认证失败,终止请求")
return
}
if r.Method == http.MethodConnect {
ctx.Mtr.TrafficType = "HTTPS"
t.handleHTTPS(w, r, ctx, authURL)
} else {
ctx.Mtr.TrafficType = "HTTP"
t.handleHTTP(w, r, ctx, authURL)
}
}
// ============================
// 认证逻辑
// ============================
func (t *TproxyHTTPAgent) authenticate(w http.ResponseWriter, r *http.Request, ctx *tproxyHTTPContext) *url.URL {
var (
flag int
err error
)
authHeader := ""
if vals, ok := r.Header["Proxy-Authorization"]; ok && len(vals) > 0 {
authHeader = vals[0]
}
flag, err = ctx.Proxyer.Auth(ctx.Mtr, authHeader)
if flag != client.PROXY_AUTH_SUCC {
HttpError(w, fmt.Sprintf("[%d] PROXY_AUTH_FAIL", flag), http.StatusProxyAuthRequired)
ctx.Mtr.Message = "认证失败: " + err.Error()
ctx.Mtr.ErrorOrReport()
return nil
}
mapInfo, err := ctx.Proxyer.ResolveMapping(ctx.Mtr)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
ctx.Mtr.Message = "提取失败: " + err.Error()
ctx.Mtr.ErrorOrReport()
return nil
}
var proxyURL string
if mapInfo.HasProxyAuth == 0 {
proxyURL = fmt.Sprintf("%s://%s:%d", mapInfo.Protocol, mapInfo.ProxyHost, mapInfo.ProxyPort)
} else {
proxyURL = fmt.Sprintf("%s://%s:%s@%s:%d", mapInfo.Protocol,
mapInfo.ProxyUser, mapInfo.ProxyPwd, mapInfo.ProxyHost, mapInfo.ProxyPort)
}
uri, err := url.Parse(proxyURL)
if err != nil {
HttpError(w, err.Error(), http.StatusProxyAuthRequired)
ctx.Mtr.Message = "代理URL无效: " + err.Error()
ctx.Mtr.ErrorOrReport()
return nil
}
ctx.Mtr.Message = "认证成功"
ctx.Mtr.InfoOrReport()
return uri
}
// ============================
// HTTP转发逻辑
// ============================
func (t *TproxyHTTPAgent) handleHTTP(w http.ResponseWriter, r *http.Request, ctx *tproxyHTTPContext, proxy *url.URL) {
ctx.Mtr.EventStage = models.ConnectStage
transport := &http.Transport{
Proxy: http.ProxyURL(proxy),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
r.Header.Del("Proxy-Authorization")
r.Body = &hook.RequestBodyLogger{O: r.Body, M: ctx.Mtr}
defer r.Body.Close()
resp, err := transport.RoundTrip(r)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
ctx.Mtr.Message = "连接失败: " + err.Error()
ctx.Mtr.ErrorOrReport()
return
}
defer resp.Body.Close()
resp.Header.Add("X-Tproxy-Id", uuid.NewV4().String())
copyHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
writer := hook.LogLimitReaderWriter(hook.WriterNopCloser{w}, 0, true, ctx.Mtr)
reader := hook.LogLimitReader(resp.Body, 0, true, ctx.Mtr)
defer reader.Close()
ctx.Mtr.EventStage = models.TransferStage
ctx.Mtr.InfoOrReport()
t.transfer(writer, reader)
}
// ============================
// HTTPS隧道转发逻辑
// ============================
func (t *TproxyHTTPAgent) handleHTTPS(w http.ResponseWriter, r *http.Request, ctx *tproxyHTTPContext, proxy *url.URL) {
ctx.Mtr.EventStage = models.ConnectStage
destConn, err := net.DialTimeout("tcp", proxy.Host, 60*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
ctx.Mtr.Message = "连接失败: " + err.Error()
ctx.Mtr.ErrorOrReport()
return
}
defer destConn.Close()
proxyHeader := buildProxyHeader(r, proxy)
_, err = destConn.Write([]byte(proxyHeader))
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
ctx.Mtr.Message = "发送握手失败: " + err.Error()
ctx.Mtr.ErrorOrReport()
return
}
buf := make([]byte, 2048)
n, err := destConn.Read(buf)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
if !strings.Contains(string(buf[:n]), "200") {
http.Error(w, "Proxy authentication required", http.StatusProxyAuthRequired)
ctx.Mtr.Message = "代理供应商需要验证"
ctx.Mtr.ErrorOrReport()
return
}
w.WriteHeader(http.StatusOK)
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacker not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hj.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
writer := hook.LogLimitReaderWriter(clientConn, 0, true, ctx.Mtr)
ctx.Mtr.EventStage = models.TransferStage
ctx.Mtr.InfoOrReport()
t.Ants.Submit(func() {
defer destConn.Close()
defer writer.Close()
go t.transfer(destConn, writer)
t.transfer(writer, destConn)
})
}
// ============================
// 公共工具函数
// ============================
func HttpError(w http.ResponseWriter, msg string, code int) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Proxy-Authenticate", `Basic realm="tproxy"`)
w.WriteHeader(code)
fmt.Fprintln(w, msg)
}
func buildProxyHeader(r *http.Request, proxy *url.URL) string {
builder := strings.Builder{}
builder.WriteString(fmt.Sprintf("%s %s %s\r\n", r.Method, r.Host, r.Proto))
for k, v := range r.Header {
if strings.EqualFold(k, "Proxy-Authorization") {
continue
}
builder.WriteString(fmt.Sprintf("%s: %s\r\n", k, v[0]))
}
if proxy.User != nil {
pwd, _ := proxy.User.Password()
auth := base64.StdEncoding.EncodeToString([]byte(proxy.User.Username() + ":" + pwd))
builder.WriteString("Proxy-Authorization: Basic " + auth + "\r\n")
}
builder.WriteString("\r\n")
return builder.String()
}
func (t *TproxyHTTPAgent) transfer(dst io.WriteCloser, src io.ReadCloser) {
defer func() {
if e := recover(); e != nil {
logrus.Errorf("transfer panic: %v\n%s", e, debug.Stack())
}
dst.Close()
src.Close()
}()
io.Copy(dst, src)
}
数据流转流程
- IP信息更新:XXL-JOB定时任务 → 第三方API → 更新IP维度数据
- 使用监控:隧道代理 → Pulsar → ClickHouse日志表
- 分数计算:XXL-JOB定时任务 → 多维度数据 → 权重公式 → 更新分数
- 资源分配:监控触发 → 分数排序 → IP分配 → 租户关联更新
实践效果与价值
- 代理IP可用率:从~60%提升至85%+
- 资源利用率:通过动态分配提高30%+
- 响应时间:智能调度减少失败重试,平均降低40%
- 智能告警:资源不足自动预警