Sử dụng lại các kết nối http trong Golang


81

Tôi hiện đang đấu tranh để tìm cách sử dụng lại các kết nối khi tạo các bài đăng HTTP trong Golang.

Tôi đã tạo một phương tiện vận tải và khách hàng như vậy:

// Create a new transport and HTTP client
tr := &http.Transport{}
client := &http.Client{Transport: tr}

Sau đó tôi chuyển con trỏ khách hàng này vào một quy trình tạo nhiều bài đăng cho cùng một điểm cuối như sau:

r, err := client.Post(url, "application/json", post)

Nhìn vào netstat, điều này dường như dẫn đến một kết nối mới cho mỗi bài đăng, dẫn đến một số lượng lớn các kết nối đồng thời được mở.

Cách chính xác để sử dụng lại các kết nối trong trường hợp này là gì?


2
Câu trả lời đúng cho câu hỏi này được niêm yết tại trùng lặp này: Go chương trình khách hàng tạo ra rất nhiều một ổ cắm trong trạng thái TIME_WAIT
Brent Bradburn

Câu trả lời:


94

Đảm bảo rằng bạn đọc cho đến khi hoàn tất phản hồi VÀ cuộc gọi Close().

ví dụ

res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()

Một lần nữa ... Để đảm bảo http.Clientsử dụng lại kết nối, hãy đảm bảo:

  • Đọc cho đến khi Phản hồi hoàn tất (tức là ioutil.ReadAll(resp.Body))
  • Gọi Body.Close()

1
Tôi đang gửi bài cho cùng một máy chủ. Tuy nhiên, sự hiểu biết của tôi là MaxIdleConnsPerHost sẽ dẫn đến việc đóng các kết nối nhàn rỗi. đây không phải là trường hợp à?
sicr

5
+1, bởi vì tôi đã gọi defer res.Body.Close()trong một chương trình tương tự, nhưng cuối cùng thỉnh thoảng quay lại từ hàm trước khi phần đó được thực thi (nếu resp.StatusCode != 200chẳng hạn), điều này khiến nhiều bộ mô tả tệp đang mở không hoạt động và cuối cùng đã giết chương trình của tôi. Việc đánh chuỗi này khiến tôi phải tự mình truy cập lại phần mã đó và facepalm. cảm ơn.
sa125

3
một lưu ý thú vị là bước đọc có vẻ là cần thiết và đủ. Chỉ riêng bước đọc sẽ trả lại kết nối đến nhóm, nhưng riêng bước đóng thì không; kết nối sẽ kết thúc bằng TCP_WAIT. Cũng gặp rắc rối vì tôi đang sử dụng json.NewDecoder () để đọc phản hồi.Body, không đọc đầy đủ. Đảm bảo bao gồm io.Copy (ioutil.Discard, res.Body) nếu bạn không chắc chắn.
Sam Russell

2
Có cách nào để kiểm tra xem nội dung đã được đọc hoàn toàn chưa? Có ioutil.ReadAll()được đảm bảo là đủ hay tôi vẫn cần io.Copy()gọi điện thoại khắp nơi, đề phòng?
Patrik Iselind

4
Tôi đã xem mã nguồn và có vẻ như phần thân phản hồi Close () đã xử lý việc rút phần thân: github.com/golang/go/blob/…
dr.scre

44

Nếu ai đó vẫn đang tìm câu trả lời về cách thực hiện, đây là cách tôi đang làm.

package main

import (
    "bytes"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

var httpClient *http.Client

const (
    MaxIdleConnections int = 20
    RequestTimeout     int = 5
)

func init() {
    httpClient = createHTTPClient()
}

// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxIdleConnsPerHost: MaxIdleConnections,
        },
        Timeout: time.Duration(RequestTimeout) * time.Second,
    }

    return client
}

func main() {
    endPoint := "https://localhost:8080/doSomething"

    req, err := http.NewRequest("POST", endPoint, bytes.NewBuffer([]byte("Post this data")))
    if err != nil {
        log.Fatalf("Error Occured. %+v", err)
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    response, err := httpClient.Do(req)
    if err != nil && response == nil {
        log.Fatalf("Error sending request to API endpoint. %+v", err)
    }

    // Close the connection to reuse it
    defer response.Body.Close()

    // Let's check if the work actually is done
    // We have seen inconsistencies even when we get 200 OK response
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatalf("Couldn't parse response body. %+v", err)
    }

    log.Println("Response Body:", string(body))    
}

Đi đến sân chơi: http://play.golang.org/p/oliqHLmzSX

Tóm lại, tôi đang tạo một phương thức khác để tạo một máy khách HTTP và gán nó cho biến toàn cục, sau đó sử dụng nó để đưa ra yêu cầu. Lưu ý

defer response.Body.Close() 

Thao tác này sẽ đóng kết nối và đặt nó sẵn sàng để sử dụng lại.

Hy vọng điều này sẽ giúp một ai đó.


1
Việc sử dụng http.Client làm biến toàn cục có an toàn khỏi các điều kiện chủng tộc không nếu có nhiều goroutines gọi một hàm sử dụng biến đó?
Bart Silverstrim

3
@ bn00d là defer response.Body.Close()đúng? tôi yêu cầu bởi vì bằng cách bảo vệ đóng, chúng tôi sẽ không thực sự đóng conn để sử dụng lại cho đến khi chức năng chính thoát ra, do đó người ta chỉ cần gọi .Close()trực tiếp sau đó .ReadAll(). điều này có vẻ không phải là một vấn đề trong ví dụ của bạn b / c, nó không thực sự chứng minh việc thực hiện nhiều yêu cầu, nó chỉ thực hiện một yêu cầu và sau đó thoát ra nhưng nếu chúng tôi thực hiện một số yêu cầu trở lại, có vẻ như kể từ khi chỉnh sửa defer, .Close()sẽ không được gọi là cho đến khi thoát khỏi func. hoặc ... tôi đang thiếu một cái gì đó? cảm ơn.
mad.meesh

1
@ mad.meesh nếu bạn thực hiện nhiều cuộc gọi (ví dụ: bên trong một vòng lặp), chỉ cần bọc lệnh gọi đến Body.Close () bên trong một bao đóng, theo cách này, nó sẽ bị đóng ngay sau khi bạn xử lý xong dữ liệu.
Antoine Cotten

Làm cách nào tôi có thể đặt các proxy khác nhau cho mọi yêu cầu theo cách này? Nó có khả thi không?
Amir Khoshhal

@ bn00d Ví dụ của bạn dường như không hoạt động. Sau khi thêm vòng lặp, mỗi yêu cầu vẫn dẫn đến một kết nối mới. play.golang.org/p/9Ah_lyfYxgV
Lewis Chan

37

Chỉnh sửa: Đây là một lưu ý nhiều hơn cho những người xây dựng Vận chuyển và Khách hàng cho mọi yêu cầu.

Edit2: Đã thay đổi liên kết thành godoc.

Transportlà cấu trúc chứa các kết nối để sử dụng lại; xem https://godoc.org/net/http#Transport ("Theo mặc định, Truyền tải lưu trữ các kết nối để sử dụng lại trong tương lai.")

Vì vậy, nếu bạn tạo một Giao thông vận tải mới cho mỗi yêu cầu, nó sẽ tạo các kết nối mới mỗi lần. Trong trường hợp này, giải pháp là chia sẻ một cá thể Vận chuyển giữa các máy khách.


Vui lòng cung cấp các liên kết bằng cách sử dụng cam kết cụ thể. Liên kết của bạn không còn đúng nữa.
Inanc Gumus

play.golang.org/p/9Ah_lyfYxgV ví dụ này chỉ hiển thị một phương tiện truyền tải, nhưng nó vẫn tạo ra một kết nối cho mỗi yêu cầu. Tại sao vậy ?
Lewis Chan

12

IIRC, khách hàng mặc định không kết nối tái sử dụng. Bạn có đang đóng câu trả lời không?

Người gọi nên đóng lại ứng dụng khi đọc xong. Nếu resp.Body không được đóng, RoundTripper cơ bản của Client (thường là Transport) có thể không sử dụng lại được kết nối TCP liên tục với máy chủ cho một yêu cầu "duy trì hoạt động" tiếp theo.


Hi, nhờ các phản ứng. Vâng, xin lỗi tôi cũng nên bao gồm điều đó. Tôi đang đóng kết nối với r.Body.Close ().
sicr

@sicr, bạn có khẳng định là máy chủ không thực sự tự đóng các kết nối không? Ý tôi là, những kết nối xuất sắc có thể là một trong các *_WAITquốc gia hoặc một cái gì đó như thế này
kostix

1
@kostix Tôi thấy một số lượng lớn kết nối với trạng thái ĐÃ ĐƯỢC LẬP khi nhìn vào netstat. Có vẻ như một kết nối mới đang được tạo ra trên mọi yêu cầu POST trái ngược với kết nối tương tự đang được sử dụng lại.
sicr

@sicr, bạn đã tìm ra giải pháp về việc tái sử dụng kết nối chưa? cảm ơn rất nhiều, Daniele
Daniele B

3

về Body

// It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.

Vì vậy, nếu bạn muốn sử dụng lại các kết nối TCP, bạn phải đóng Body mỗi lần sau khi đọc xong. Một hàm ReadBody (io.ReadCloser) được đề xuất như thế này.

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "time"
)

func main() {
    req, err := http.NewRequest(http.MethodGet, "https://github.com", nil)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    client := &http.Client{}
    i := 0
    for {
        resp, err := client.Do(req)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        _, _ = readBody(resp.Body)
        fmt.Println("done ", i)
        time.Sleep(5 * time.Second)
    }
}

func readBody(readCloser io.ReadCloser) ([]byte, error) {
    defer readCloser.Close()
    body, err := ioutil.ReadAll(readCloser)
    if err != nil {
        return nil, err
    }
    return body, nil
}

2

Một cách tiếp cận khác init()là sử dụng phương thức singleton để lấy ứng dụng khách http. Bằng cách sử dụng đồng bộ hóa. Khi bạn có thể chắc chắn rằng chỉ một phiên bản sẽ được sử dụng cho tất cả các yêu cầu của bạn.

var (
    once              sync.Once
    netClient         *http.Client
)

func newNetClient() *http.Client {
    once.Do(func() {
        var netTransport = &http.Transport{
            Dial: (&net.Dialer{
                Timeout: 2 * time.Second,
            }).Dial,
            TLSHandshakeTimeout: 2 * time.Second,
        }
        netClient = &http.Client{
            Timeout:   time.Second * 2,
            Transport: netTransport,
        }
    })

    return netClient
}

func yourFunc(){
    URL := "local.dev"
    req, err := http.NewRequest("POST", URL, nil)
    response, err := newNetClient().Do(req)
    // ...
}


Điều này hoạt động hoàn hảo để tôi xử lý 100 yêu cầu HTTP mỗi giây
philip mudenyo

0

Điểm còn thiếu ở đây là thứ "goroutine". Truyền tải có nhóm kết nối riêng, theo mặc định, mỗi kết nối trong nhóm đó được sử dụng lại (nếu nội dung được đọc và đóng hoàn toàn) nhưng nếu một số goroutines đang gửi yêu cầu, các kết nối mới sẽ được tạo (nhóm có tất cả các kết nối bận và sẽ tạo các kết nối mới ). Để giải quyết vấn đề đó, bạn sẽ cần giới hạn số lượng kết nối tối đa trên mỗi máy chủ: Transport.MaxConnsPerHost( https://golang.org/src/net/http/transport.go#L205 ).

Có lẽ bạn cũng muốn thiết lập IdleConnTimeoutvà / hoặc ResponseHeaderTimeout.


0

https://golang.org/src/net/http/transport.go#L196

bạn nên đặt MaxConnsPerHostrõ ràng cho của bạn http.Client. Transportkhông sử dụng lại kết nối TCP, nhưng bạn nên giới hạn MaxConnsPerHost(mặc định 0 nghĩa là không có giới hạn).

func init() {
    // singleton http.Client
    httpClient = createHTTPClient()
}

// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxConnsPerHost:     1,
            // other option field
        },
        Timeout: time.Duration(RequestTimeout) * time.Second,
    }

    return client
}

-3

Có hai cách:

  1. Sử dụng một thư viện sử dụng lại nội bộ và quản lý các bộ mô tả tệp, được liên kết với từng yêu cầu. Http Client thực hiện điều tương tự trong nội bộ, nhưng sau đó bạn sẽ có quyền kiểm soát số lượng kết nối đồng thời để mở và cách quản lý tài nguyên của bạn. Nếu bạn quan tâm, hãy xem triển khai netpoll, sử dụng nội bộ epoll / kqueue để quản lý chúng.

  2. Cách đơn giản là, thay vì gộp các kết nối mạng, hãy tạo một nhóm công nhân, cho các quy trình của bạn. Điều này sẽ dễ dàng và giải pháp tốt hơn, sẽ không cản trở cơ sở mã hiện tại của bạn và sẽ yêu cầu những thay đổi nhỏ.

Giả sử bạn cần thực hiện n yêu cầu ĐĂNG sau khi nhận được yêu cầu.

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Bạn có thể sử dụng các kênh để thực hiện điều này.

Hoặc, đơn giản là bạn có thể sử dụng thư viện của bên thứ ba.
Như: https://github.com/ivpusic/grpool

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.