V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
wnanbei
V2EX  ›  Go 编程语言

Go net/http Client 的某些参数不是并发安全的?

  •  
  •   wnanbei · 2019-11-25 16:16:13 +08:00 · 6919 次点击
    这是一个创建于 1823 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在官方文档中有提到 net/http client 是协程安全的,应该复用。

    The Client's Transport typically has internal state (cached TCP connections), so Clients should be reused instead of created as needed. Clients are safe for concurrent use by multiple goroutines.

    但使用 Client 发起请求时,有一部分请求的设置是以函数或字段的方式放在 Client 的参数中的。例如 Proxy 代理、重定向检查、超时设置。要设置的话必须像以下这般设置:

    func RedirectFunc(req *http.Request, via []*http.Request) error {
    	if len(via) > 5 {
    		err := &RedirectError{r}
    		return WrapErr(err, "RedirectError")
    	}
    	return nil
    }
    
    client.CheckRedirect = RedirectFunc  // 设置重定向
    

    这样的话就会造成一个问题,在并发的过程中如果要更改重定向次数的话,就会有并发安全问题,设置 Proxy 代理和超时时间也有这个问题。

    比如在这样一个假设情况中,我现在有 10000 个请求需要并发,每个请求需要设置不同的特定 Proxy 代理。那么这时候使用全局的 Client,在每个协程中更改 client.CheckRedirect 函数,然后发起请求,显然会有并发问题,发起请求时使用的并不一定是指定的那个 Proxy。

    想了想解决的办法:

    1. 每个请求新建一个 Client ?
    2. 把更改参数和请求一起加锁锁起来?

    请问这种情况有靠谱的解决方法吗?

    第 1 条附言  ·  2019-11-26 10:26:00 +08:00

    因为说了并发安全这个词,好像很多朋友没理解我的意思。

    这个问题的起因主要就以下几点:

    1. 因为 Golang net/http 发起请求的时候,是没有办法为单个 Request 请求设置重定向、代理、超时这些设置的,要设置只能设置到 Client 上。
    2. Client 底层有连接池,所以需要复用,创建多个新的 Client 来使用开销会很大。
    3. 目前需要在并发的情况下更改单个 Request 请求的重定向、代理、超时等设置,但因为 1 的原因只能改 Client。
    4. 但复用 Client 的话,Client 的值就类似一个全局变量,在这个协程中更改了,会影响另一个协程中的请求。

    情况大致是这么个情况,目前想问的就是有没有更好的解决办法。

    最终如果没有更好办法的话肯定只能创建新的 Client,每个Client一份设置不去更改。

    第 2 条附言  ·  2019-11-29 10:47:51 +08:00
    @icexin
    @winterssy
    这两天已经用 context.Context 实现了这个需求,可以给单个请求指定 Proxy 代理、重定向次数、超时时间了,有兴趣的话可以看一下。
    https://github.com/wnanbei/direwolf/blob/master/session.go#L264
    https://github.com/wnanbei/direwolf/blob/master/download.go#L18
    27 条回复    2019-11-27 10:56:20 +08:00
    tsl0922
        1
    tsl0922  
       2019-11-25 16:22:40 +08:00
    大兄弟,人家说的是让你复用 Transport,划重点 。
    wnanbei
        2
    wnanbei  
    OP
       2019-11-25 16:28:27 +08:00
    @tsl0922 说的是 `so Clients should be reused`,而且设置代理在 transport 里,一样的也是这个问题。
    tsl0922
        3
    tsl0922  
       2019-11-25 16:33:13 +08:00
    抱歉,没仔细看题。我仔细看了文档,感觉这里的复用应该不是只并发的复用,Client 应该是有状态的。GitHub 上 golang 的官方 issue 里也有人提到关于复用的另一个问题:

    https://github.com/golang/go/issues/26095
    janxin
        4
    janxin  
       2019-11-25 16:36:05 +08:00
    你对并发安全理解有问题吧,这个明显你修改的话是在请求过程中还是使用你指定的 Func,但是你每个都修改,client 只会使用你最后指定的参数,这样看上去是不是没问题了?

    如果是单纯 proxy,你有个简单方案是按照 proxy 生成对应的 client,然后使用就行了
    wnanbei
        5
    wnanbei  
    OP
       2019-11-25 16:43:13 +08:00
    @tsl0922 嗯嗯,我也知道官方文档的复用大部分意思指的是复用连接。

    但代理、超时、重定向这些设置,只有 Client 和 Transport 这种级别可以设置,在 Request 这个级别无法指定到特定的请求上。现在又需要复用 Client,但在并发的时候更改 Client 里的这些设置又不是并发安全的,就很头疼。
    wnanbei
        6
    wnanbei  
    OP
       2019-11-25 16:47:47 +08:00
    @janxin 目前就是需要把 超时、代理、重定向 这些设置到单个 Request 上,但 net/http 不提供,就只能设置在 Client 上。
    并发的时候改 Client 就会有问题,因为 Client 需要复用,是全局的。
    wnanbei
        7
    wnanbei  
    OP
       2019-11-25 16:49:37 +08:00
    @janxin 在当前协程改了 func 后,发起请求,不一定用的就是刚刚改的这个 func。因为在这个过程中这个 func 很可能又被其他协程改掉了
    tsl0922
        8
    tsl0922  
       2019-11-25 17:28:35 +08:00
    go 文档里说的复用应该是指复用 tcp 连接( keep-alive ),你这种换代理的方式很显然要重新连接。你想要的只是复用 Client 这一个结构体,还是老老实实不同的 proxy 创建不同的 Client 对象吧,应该不会多出多少开销。
    wnanbei
        9
    wnanbei  
    OP
       2019-11-25 17:54:47 +08:00
    @tsl0922 有时候其实换代理的量很大,而且还有超时,重定向等等。哎,脑阔疼
    chennqqi
        10
    chennqqi  
       2019-11-25 17:59:21 +08:00
    Client 基本可以复用,request 不能复用,是这个意思?
    gaara
        11
    gaara  
       2019-11-25 18:15:45 +08:00
    session = client
    session.Timeout=
    session.Redirect=
    chennqqi
        12
    chennqqi  
       2019-11-25 18:25:28 +08:00
    @wnanbei 我看了 github issue 原文是如果请求的 body 被正确关闭,并被读取就可以重用吧
    darrh00
        13
    darrh00  
       2019-11-25 18:34:15 +08:00
    CheckRedirect 这个属性为什么需要修改呢?

    这个函数的签名提供的两个 req *http.Request, via []*http.Request 参数还不够做业务判断吗?

    我的意思是,client 创建完成了,在生命周期内保持 CheckRedirect 不变化, 所有需要处理重定向而需要作区分对待的统一在 CheckRedirect 函数里实现不行吗?
    zhujinliang
        14
    zhujinliang  
       2019-11-25 18:44:29 +08:00 via iPhone
    同意 @darrh00,楼主用法错了,client 设置完以后就不要修改了
    wnanbei
        15
    wnanbei  
    OP
       2019-11-26 10:10:19 +08:00
    @chennqqi 不是,是 Request 里根本没有设置重定向,超时,代理这些的地方,只能去 client 里设置。并发的时候要改这些设置就只能改 client,并发的时候改 client 就有问题。
    wnanbei
        16
    wnanbei  
    OP
       2019-11-26 10:11:17 +08:00
    @darrh00 因为重定向次数是写死在这个 CheckRedirect 里的,有一些请求需要限制的重定向次数可能是不同的,所以需要改。
    wnanbei
        17
    wnanbei  
    OP
       2019-11-26 10:12:09 +08:00
    @zhujinliang 是 Request 里根本没有设置重定向,超时,代理这些的地方,只能去 client 里设置。并发的时候要改这些设置就只能改 client。不然就只有建新的 Client。
    icexin
        18
    icexin  
       2019-11-26 11:12:55 +08:00
    @wnanbei 可以试试使用 NewRequestWithContext 在 Request 里面附加上下文信息,在 CheckRedirect 或者 Proxy 函数里面从 Request.Context 拿到 Context 来切换对应的策略。
    wnanbei
        19
    wnanbei  
    OP
       2019-11-26 13:45:00 +08:00
    @icexin 好像可行,我去研究一下,非常感谢!!!
    reus
        20
    reus  
       2019-11-26 15:19:00 +08:00
    一般这些都只设一次的
    8kFT2l6aoU9566Bg
        21
    8kFT2l6aoU9566Bg  
       2019-11-26 21:46:16 +08:00
    这个问题我之前在基于 net/http 开发一个网络请求库的时候也思考过,能不能做到想 python requests 库那样为每一个 HTTP 请求单独设置证书校验逻辑、代理等控制策略,而且做到并发安全。

    可 go 的方案就是 client 只初始化 1 次就好,之后不应该再修改,要实现我想要的功能就必须动态修改 client,文档告诉我们 client 是并发安全的,即使你在多个 goroutines 修改 client 也不会出现数据竞争,但 client 在发起请求时只会使用最近修改的值,并不能达到我所期望的效果(每个请求对应不同的策略)。

    我想到可行的方法就是为每一个请求克隆一个 client,但这样势必会影响性能。最后一刀切,凡是修改 http client 的我都没提供 api 去简化,有这种需求的话创建不同的 client 就是。

    18 楼提到 context 我觉得也是不可行的,因为本质上你还是要动态修改 client,反而额外增加了类型断言的开销。
    icexin
        22
    icexin  
       2019-11-27 00:45:19 +08:00
    @winterssy 这些函数本身就接受一个 Request 对象来根据不同的请求做出不同的处理策略,函数就赋值一次,为什么说修改 client 了?
    8kFT2l6aoU9566Bg
        23
    8kFT2l6aoU9566Bg  
       2019-11-27 00:54:08 +08:00
    @icexin #22 因为 net/http 设置代理 /重定向策略都是在 client 设置的,即便你在 Request 对象上下文携带了相关参数,你要发起请求就要经过 client。
    icexin
        24
    icexin  
       2019-11-27 01:04:51 +08:00 via iPhone
    @winterssy 是一个 client,但各个请求是独立处理的
    8kFT2l6aoU9566Bg
        25
    8kFT2l6aoU9566Bg  
       2019-11-27 01:18:35 +08:00
    @icexin #24 各个请求是独立处理,但他们是共用一个 client,多个 gourotines 并发去请求(多核的话可以并行),同一个 client 同一时刻可以也有不同的参数?退一步来说,如果这可行的话不用 context 就能做到,何必多此一举。
    icexin
        26
    icexin  
       2019-11-27 10:42:10 +08:00   ❤️ 2
    8kFT2l6aoU9566Bg
        27
    8kFT2l6aoU9566Bg  
       2019-11-27 10:56:20 +08:00
    @icexin #26 感谢,这个对 Proxy,CheckRedirect 可行,因为它们跟 request 关联,对于 transport 其它跟 request 没有关联的参数就无解了,比如像 python requests 的 verify
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1284 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 17:53 · PVG 01:53 · LAX 09:53 · JFK 12:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.