3分钟,手摸手教你用OpenResty搭建高性能隧道代理(附完整配置!)

简介: 在爬虫开发中,代理 IP 是常用手段,但管理代理池繁琐且易出错。本文介绍了如何使用隧道代理简化代理 IP 管理,通过 OpenResty 实现高效的动态代理切换,提升爬虫稳定性与维护效率。

经常写爬虫的小伙伴们对代理 IP 应该不会很陌生了吧?

通常,我们为了让爬虫更加稳定,一般我们都会去购买一些代理 IP 用在我们的爬虫服务上。常规的做法,我们一般会去某个代理网站上面购买服务,然后我们会得到一个获取代理 IP 的请求地址,之后我们再写一个请求去获取这些代理 IP。

一般来说,这些代理 IP 的有效期都不会太长,当然和你购买的套餐有一定的关系,常规来说,一般每个代理 IP 的有效期就只有 1-5分钟。我们还需要在爬虫应用程序中去维护这些代理 IP,可能我们的代码就会这样去写

package main

import (
    "crypto/tls"
    "io"
    "net/http"
    "net/url"
    "time"
)

func main() {
   
   // 通过请求代理IP服务获得一些可用的代理 IP
   // ips := []string{"192.168.0.1:8080", "192.168.0.1:8081", "192.168.0.1:8082"}
   ips := fetchProxyIPs()
   proxyIP := ips[1]

    proxyUrl, err := url.Parse("http://"+proxyIP)
    if err != nil {
   
        panic(err)
    }
    tr := &http.Transport{
   
        Proxy:           http.ProxyURL(proxyUrl),
        TLSClientConfig: &tls.Config{
   InsecureSkipVerify: true},
    }
    client := &http.Client{
   
        Transport: tr,
        Timeout:   15 * time.Second,
    }
    resp, err := client.Get("https://httpbin.org/ip")
    if err != nil {
   
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
   
        panic("Failed to get a valid response")
    }
    content, err := io.ReadAll(resp.Body)
    if err != nil {
   
        panic(err)
    }
    println("Response:", string(content))

}

如果我们的爬虫程序只有一个,那么上面的代码完全没有啥问题。但是,如果我们的爬虫程序不止一个呢?是不是 fetchProxyIPs() 的代码逻辑就得复制粘贴多次? 如果哪天我想更换代理服务商岂不是还得一个一个的去改代码?

那么,有没有一种方式,可以在我设置代理 IP 的时候,就设置一个固定的 IP,然后这个固定的 IP 再帮我“自动”去使用代理 IP 呢?

是的,隧道代理就是干这事儿的。

在软件开发中,没有什么是不能通过加一层中间件来解决问题的,如果有,那么就再加一层……

可能,我们最终需要写的代码,就类似这样:

package main

import (
    "crypto/tls"
    "io"
    "net/http"
    "net/url"
    "time"
)

func main() {
   
    // 只需要配置隧道代理地址,无需管理代理池
    proxyUrl, err := url.Parse("http://127.0.0.1:9527")
    if err != nil {
   
        panic(err)
    }
    tr := &http.Transport{
   
        Proxy:           http.ProxyURL(proxyUrl),
        TLSClientConfig: &tls.Config{
   InsecureSkipVerify: true},
    }
    client := &http.Client{
   
        Transport: tr,
        Timeout:   15 * time.Second,
    }
    resp, err := client.Get("https://httpbin.org/ip")
    if err != nil {
   
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
   
        panic("Failed to get a valid response")
    }
    content, err := io.ReadAll(resp.Body)
    if err != nil {
   
        panic(err)
    }
    println("Response:", string(content))

}

http://127.0.0.1:9527 服务就是我们设定的隧道代理,当我们通过 http://127.0.0.1:9527 去设置代理时,http://127.0.0.1:9527 会自动帮我们切换代理 IP。

现在有很多代理 IP 服务商都有提供隧道代理服务的,但是,价格一般都不会太便宜。感兴趣的小伙伴们可以去了解了解。

其实,自己动手搭建一个隧道代理服务也不会太复杂,用 go 写一个代理转发程序也是可以的,但是,在这个应用场景下,还有更好的选择:OpenResty

OpenResty 其实是 Nginx + Lua JIT。Nginx 本身就擅长处理 TCP 连接,性能高,稳定成熟。

有小伙伴这时候就说了,不太会 Lua 脚本怎么办?

没关系,这里我将整个配置都贴出来,以供各位参考:

worker_processes  16;

error_log  /usr/local/openresty/nginx/logs/error.log debug;

events {
   
    worker_connections  1024;
}


stream {
   

    # 自定义 TCP 日志格式定义
    # 包含连接的 IP、时间、协议、状态、流量、会话时长、上游地址及流量等
    log_format tcp_proxy '$remote_addr [$time_local] '
                         '$protocol $status $bytes_sent $bytes_received '
                         '$session_time "$upstream_addr" '
                         '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';
    # 启用日志记录到指定文件,并使用自定义格式
    access_log /usr/local/openresty/nginx/logs/tcp-access.log tcp_proxy;
    open_log_file_cache off;

    # TCP 代理配置
    # upstream 块中定义一个占位 server
    # 注意:0.0.0.0:1101 实际不会使用,真正地址会被 balancer_by_lua_block 动态覆盖
    upstream real_server {
   
        server 0.0.0.0:1101;

        # 使用 balancer_by_lua_block 动态设置后端目标主机和端口
        balancer_by_lua_block {
   
            -- 检查 preread 阶段是否已经设置了 proxy_host 和 proxy_port
            -- 从 ngx.ctx 中获取代理服务器地址
            if not ngx.ctx.proxy_host or not ngx.ctx.proxy_port then
                ngx.log(ngx.ERR, "====>proxy_host or proxy_port is not set in ngx.ctx<====")
                return
            end

            -- 初始化 balancer
            local balancer = require "ngx.balancer"
            local host = ""
            local port = 0

            -- 从上下文中提取目标 IP 和端口
            host = ngx.ctx.proxy_host
            port = ngx.ctx.proxy_port
            -- 设置代理服务器地址
            local ok, err = balancer.set_current_peer(host, port)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to set current peer: " .. tostring(err) .. "<====")
                return
            end
        }
    }

    # 定义 TCP server 模块(stream)监听端口和代理逻辑
    server {
   
        # preread_by_lua_block 在客户端连接建立时就会触发,用于预处理逻辑
        preread_by_lua_block {
   
            -- https://github.com/openresty/lua-resty-redis
            local redis = require "resty.redis"
            local redis_instance = redis:new()

            -- 设置 Redis 操作超时时间(毫秒)
            redis_instance:set_timeout(5000)

            -- 一些 redis 连接配置
            local rdb_host = "192.168.1.208"
            local rdb_port = 6379
            local rdb_pwd = ""
            local rdb_db = 1
            -- 存放代理服务器地址的 zset 表名称
            local zset_table_name = "tunnel_proxy_pool"

            -- 连接到 Redis
            local ok, err = redis_instance:connect(rdb_host, rdb_port)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to connect to Redis: [" .. tostring(ok) .. "] err msg ==> " .. tostring(err) .. "<====")
                return
            end

            -- 选择数据库
            local ok, err = redis_instance:select(rdb_db)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to select Redis DB: [" .. tostring(ok) .. "] err msg ==> " .. tostring(err) .. "<====")
                return
            end

            -- 如果设置了密码,则进行认证
            if rdb_pwd and rdb_pwd ~= "" then
                local ok, err = redis_instance:auth(rdb_pwd)
                if not ok then
                    ngx.log(ngx.ERR, "====>failed to auth Redis: [" .. tostring(ok) .. "] err msg ==> " .. tostring(err) .. "<====")
                    return
                end
            end

            -- 先检查 zset 表是否存在或者是否有数据
            local hosts_count, err = redis_instance:zcard(zset_table_name)
            if not hosts_count or hosts_count <= 0 then
                ngx.log(ngx.ERR, "====>no available proxy servers in Redis zset table: " .. tostring(zset_table_name) .. " ==> " .. tostring(err) .. "<====")
                return
            end
            -- 获取分数最低的前 1 个代理服务器地址
            local res, err = redis_instance:zrange(zset_table_name, 0, 0, "WITHSCORES")
            if not res or #res == 0 then
                ngx.log(ngx.ERR, "====>failed to get proxy server from Redis zset table: " .. tostring(zset_table_name) .. "<====")
                return
            end
            -- 解析结果(假设之前存入 zset 的元素类似 127.0.0.1:8080127.0.0.1:8181 分数为使用次数)
            local proxy_ip, proxy_port = res[1]:match("([^:]+):(%d+)")
            if not proxy_ip or not proxy_port then
                ngx.log(ngx.ERR, "====>failed to parse proxy server address ==> " .. tostring(res[1]) .. "<====")
                return
            end
            -- 获取了当前代理服务器地址后,给其分数加 1,表示当前已经使用过一次
            local ok, err = redis_instance:zincrby(zset_table_name, 1, res[1])
            if not ok then
                ngx.log(ngx.ERR, "====>failed to increment proxy server score in Redis zset table: " .. tostring(zset_table_name) .. " ==> " .. tostring(err) .. "<====")
                return
            end

            -- 将获取到的代理服务器地址存入 ngx.ctx 中,供 balancer_by_lua_block 使用
            ngx.ctx.proxy_host = proxy_ip
            ngx.ctx.proxy_port = tonumber(proxy_port)
            ngx.log(ngx.INFO, "====>using proxy server ==> " .. tostring(proxy_ip) .. ":" .. tostring(proxy_port) .. "<====")

            -- 释放 Redis 连接,否则连接池将保留不完整的连接状态
            ok, err = redis_instance:set_keepalive(10000, 100)
            if not ok then
                ngx.log(ngx.ERR, "====>failed to set Redis keepalive: " .. tostring(err) .. "<====")
            end
        }

        # 对外暴露的监听端口
        listen 0.0.0.0:9527;
        # 设置代理的目标 upstream 名称
        proxy_pass real_server;
        proxy_connect_timeout 5s;
        proxy_timeout 15s;
    }

}

以上,其实我们就是借用 OpenResty 做了一层代理转发,你可以结合流程图来看看

流程图

那么,如何部署 OpenResty 呢?

可以直接使用下面的 docker-compose.yaml 文件:

services:
  openresty:
    container_name: openresty_server
    image: openresty/openresty:1.25.3.2-5-centos7
    ports:
      - "9527:9527"
    volumes:
      - ./conf/tunnel_proxy_redis.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
      - ./logs:/usr/local/openresty/nginx/logs

文件写好之后,直接在和 docker-compose.yaml 文件同级目录下执行 docker-compose up 即可启动 OpenResty 服务。

另外,还忘记说了一点:你需要自己写一个脚本,定时将可用的代理 IP 同步到 redis 中,上面的 Lua 脚本只是会从 redis 中取出可用的代理 IP 进行转发。

自动脚本干的活儿类似写入这样的数据

zadd tunnel_proxy_pool 0 127.0.0.1:9001 0 4127.0.0.1:9002 0 127.0.0.1:9003

大家感兴趣的,可以通过访问 https://github.com/pudongping/tunnel-proxy 获得源码。

相关文章
|
数据采集 SQL 分布式计算
数据处理 、大数据、数据抽取 ETL 工具 DataX 、Kettle、Sqoop
数据处理 、大数据、数据抽取 ETL 工具 DataX 、Kettle、Sqoop
2112 0
|
Shell 网络安全 开发工具
Tabby终端工具的配置和使用
Tabby终端工具的配置和使用
8268 0
|
3月前
|
人工智能 自然语言处理 机器人
盘点集成DeepSeek大模型的智能语音机器人,看看哪款更适合你
对话式AI将降低高达25%的客服座席离职率,集成DeepSeek等大模型的智能语音机器人正成企业标配。其核心是实现7x24小时高效服务、优化成本并提供人性化交互。选型需聚焦AI模型能力、业务场景匹配度与数据安全。合力亿捷、阿里云等是市场主流选择,选对智能语音机器人是企业提升沟通效率、构筑核心竞争力的战略投资。
125 0
|
10月前
|
机器学习/深度学习 API
重磅!阿里云百炼上线Qwen百万长文本模型
重磅!阿里云百炼上线Qwen百万长文本模型
397 11
重磅!阿里云百炼上线Qwen百万长文本模型
|
12月前
|
存储 Kubernetes Cloud Native
部署Kubernetes客户端和Docker私有仓库的步骤
这个指南涵盖了部署Kubernetes客户端和配置Docker私有仓库的基本步骤,是基于最新的实践和工具。根据具体的需求和环境,还可能需要额外的配置和调整。
256 1
|
编解码 应用服务中间件 开发工具
如何在RTMP推送端和RTMP播放端支持Enhanced RTMP H.265(HEVC)
时隔多年,在Enhancing RTMP, FLV With Additional Video Codecs And HDR Support(2023年7月31号正式发布)官方规范出来之前,如果RTMP要支持H.265,大家约定俗成的做法是扩展flv协议,CDN厂商携手给出的解决方案是给flv的videotag CodecID增加一个新类型(12)来表示h265(hevc),和h264不同的地方是要解析HEVCDecoderConfigurationRecord,从HEVCDecoderConfigurationRecord中解析出vps, sps, pps. 有了vps, sps, pps,
390 6
|
9月前
|
SQL 关系型数据库 MySQL
数据库灾难应对:MySQL误删除数据的救赎之道,技巧get起来!之binlog
《数据库灾难应对:MySQL误删除数据的救赎之道,技巧get起来!之binlog》介绍了如何利用MySQL的二进制日志(Binlog)恢复误删除的数据。主要内容包括: 1. **启用二进制日志**:在`my.cnf`中配置`log-bin`并重启MySQL服务。 2. **查看二进制日志文件**:使用`SHOW VARIABLES LIKE &#39;log_%&#39;;`和`SHOW MASTER STATUS;`命令获取当前日志文件及位置。 3. **创建数据备份**:确保在恢复前已有备份,以防意外。 4. **导出二进制日志为SQL语句**:使用`mysqlbinlog`
499 2
|
10月前
|
Java
SpringBoot 内部方法调用,事务不起作用的原因及解决办法
在做业务开发时,遇到了一个事务不起作用的问题。大概流程是这样的,方法内部的定时任务调用了一个带事务的方法,失败后事务没有回滚。查阅资料后,问题得到解决,记录下来分享给大家。
458 4
|
12月前
|
Java Spring
spring boot 启动项目参数的设定
spring boot 启动项目参数的设定
230 0
|
12月前
|
应用服务中间件 nginx 数据安全/隐私保护
使用Harbor搭建Docker私有仓库
Harbor是一款开源的企业级Docker仓库管理工具,分为私有与公有仓库两种类型,其中私有仓库被广泛应用于运维场景。Harbor提供图形化界面,便于直观操作,并且其核心组件均由容器构建而成,因此安装时需预先配置Docker及docker-compose。Harbor支持基于项目的用户与仓库管理,实现细粒度的权限控制;具备镜像复制、日志收集等功能,并可通过UI直接管理镜像,支持审计追踪。部署Harbor涉及配置文件调整、登录认证等步骤,并可通过客户端进行镜像的上传、拉取等操作。系统内置多种角色,包括受限访客、访客、开发者、维护人员及管理员,以满足不同场景下的使用需求。
429 0