我希望有这样一种HTTP代理服务软件,它首先会尝试直接连接目标服务器,当连接出现问题时,自动通过另外一个上级代理进行重试。Squid是个很有名很强大的代理服务软件,所以我先想到能不能把它配置成这样。

翻看Squid的文档,一下子就看到了这样两个参数:

在squid中如果配置了上级代理,默认会优先走上级代理,再尝试直连。prefer_direct参数让squid优先选择直连。squid会通过一个叫peerSelect函数,检查所有的acl规则后,产生一个可用的上级代理列表,如果有prefer_direct参数,表示直连的DIRECT会在列表的最前面,否则在最后面。另外squid还有个never_direct参数,如果加了这个参数,这个列表里面就没有DIRECT了。

squid默认只会在连接代理出现502(bad gateway)和504(gateway timeout)时,换个代理进行重试。如果是直连没有成功,不是这两个错误,通常是503(Service Unavailable)。加上retry_on_error参数后,对于直连失败也会换代理进行重试。

貌似加上这两个参数,就可以让squid达到我的效果了。我在ubuntu自带的squid3.1.14上尝试了以后,发现这组参数对于HTTP请求工作得很好,但是对于HTTPS(通过CONNECT方法)的请求没有用。看了内部实现后,发现squid内部在处理HTTPS (CONNECT方法)时,peerSelect是共用的,prefer_direct有效。但是连接处理是完全分开的,处理http请求在forward模块中,处理https请求在一个叫tunnel的模块中,完全不理会retry_on_error参数。

继续看文档,发现SQUID3.2版本新增了这样一个新功能:

换squid3.2再试。squid3.2还是beta版,所有linux发行版中都没有,可以从官网下载源代码编译安装。装完后发现这个功能并不能正常工作。看squid的debug log发现,第一次直接连接一个IP没有连上,底层通讯模块会尝试重连这个IP地址,重连竟然显示成功,但是后续所有读写操作失败。此时上层模块并不会换个代理服务器再连,因为底层报告成功后,上层就会给客户端发送HTTP200的返回表示成功了,此时如果底层再产生通讯问题,上层就没有办法换代理重试了。

这个问题出现在src/comm/ConnOpener.cc里面的timeout函数里:

    void
    Comm::ConnOpener::timeout(const CommTimeoutCbParams &)
    {
         connect();
    }

这个函数处理连接超时的情况,连接超时后,直接调用connect,connect函数会调用更底层的comm_connect_addr进行连接,但这时是第二次调用,底层会说你以前调用过了直接返回状态OK,然后上层就以为连接成功了。一个workaround的办法(更好的办法还没想到,可以给squid报bug先)是,timeout这里不要对同一个IP进行重试了,直接返回失败。做法是这样的:

    void
    Comm::ConnOpener::timeout(const CommTimeoutCbParams &io)
    {
        debugs(5, 5, HERE << conn_ << ": * - ERR took too long to connect. (czk)");
        calls_.earlyAbort_->cancel("Comm::ConnOpener::connect timed out");
        calls_.earlyAbort_ = NULL;
        conn_->close();
        doneConnecting(COMM_ERR_CONNECT, io.xerrno);
    }

改完代码再试一次。这里发现squid的编译系统也有点问题,改完这个文件需要make clean然后再make all否则不会生效。编译安装完后进行测试,发现这样确实解决了https重试的问题。

但是发现切换到squid3.2以后,http重试不如以前稳定了。看debug log发现,squid3.2的peerSelect准备了一张很长的重试列表,也不再称作server list或者peer list了,而是叫做path list。它会和3.1一样,先准备一个服务器列表,根据我们的需求,里面只会放两个服务器,一个DIRECT,和一个上级代理。然后它做了更多的事情,它把这个列表里面的域名拿去做dns解析,然后把每个服务器解析出来的所有IP地址都放在path列表里面。然后tunnel或者forward模块会根据path列表进行重试。比如连接www.youtube.com,DNS解析返回6个IP地址,peerSelect就返回了一张这样的重试列表:

    DIRECT = local=0.0.0.0 remote=72.14.203.102:80 flags=1
    DIRECT = local=0.0.0.0 remote=72.14.203.101:80 flags=1
    DIRECT = local=0.0.0.0 remote=72.14.203.139:80 flags=1
    DIRECT = local=0.0.0.0 remote=72.14.203.100:80 flags=1
    DIRECT = local=0.0.0.0 remote=72.14.203.113:80 flags=1
    DIRECT = local=0.0.0.0 remote=72.14.203.138:80 flags=1
    cache_peer = local=0.0.0.0 remote=127.0.0.1:58118 flags=1
    cache_peer = local=0.0.0.0 remote=127.0.0.1:58118 flags=1
    cache_peer = local=0.0.0.0 remote=127.0.0.1:58118 flags=1

这个列表的总长度受一个叫forward_max_tries的参数控制,如果不幸这个域名返回的IP地址个数大于forward_max_tries,那么就不会有上级代理出现在这个列表里面了。查看代码src/peer_select.cc里面的peerSelectDnsResults函数,里面有一个循环把每一个IP加入path列表:

    for (int n = 0; n < ia->count; n++, ip++) {

简单的做法就是在这里加个限制,每个域名最多2个IP加入path列表:

    for (int n = 0; n < (ia->count>2?2:ia->count); n++, ip++) {

当然更好的做法是有一个参数能控制这个事情,可以给squid发个feature request。

改了这么多,还没有完。发现HTTPS重试的功能在twitter、facebook等网站上工作得都很好,但是对于google的网站,经常会失败。检查debug log发现,是臭名昭著的墙的随机RESET谷歌的HTTPS连接的问题。这个问题没有好的解决办法,因为连接已经建立再断开的,和前面说的那个bug产生的效果一样。解决的办法只能是把google网站的https链接手动加入never_direct。最简单配置是这样的:

    acl CONNECT method CONNECT
    acl google dstdomain .google.com
    acl google dstdomain .blogger.com
    never_direct allow CONNECT google 

加上下面一些必要的参数,就是一个最简单的可以自动重试的squid配置了:

    http_access allow all
    http_port 3128
    cache_peer 127.0.0.1 parent 8123 0 no-query default name=polipo
    forward_timeout 15 seconds
    connect_timeout 3 seconds
    nonhierarchical_direct off
    prefer_direct on
    dns_nameservers 127.0.0.1
    retry_on_error on
    forward_max_tries 5

其中有些参数需要说明一下,connect_timeout控制重试列表里面每一项的连接超时时间,forward_timeout控制整个重试列表的尝试时间,如果超过这个时间,直接就会返回失败不会继续尝试重试列表中剩余项。dns_nameservers用来指定一个与系统配置不同的域名服务器,这里用它指向一个没有被污染的安全的域名服务器。

补充:后来发现,POST方法也会遭遇CONNECT方法一样的待遇,当内容传输到一半连接被RESET时,不会重试。解决办法也只能和CONNECT一样。


CategoryBlog

将squid打造成能自动重试的代理服务 (2020-04-18 13:01:14由czk编辑)

ch3n2k.com | Copyright (c) 2004-2020 czk.