เรื่องน่ารู้เกี่ยวกับ HTTP Client ใน Golang
สมมุติว่าเราต้องการเขียน HTTP Client เพื่อส่งข้อมูล และ รับข้อมูลไปสักที่ โดยไม่ใช้ External Library ใดๆเลย และใช้ค่า Default ที่ทาง GO ได้เตรียมไว้ให้ ตามตัวอย่างโค๊ดชุดนี้
การทำงานของโค๊ดเป็นการดึงข้อมูลแบบธรรมดา สามารถทำงานได้ปกติ แต่ถ้าเราใช้โค๊ดแบบนี้บน Production จะเกิดอะไรขึ้น?
Default value = 0 หรือ เท่ากับไม่มี Timeout
// Timeout specifies a time limit for requests made by this
// Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
//
// A Timeout of zero means no timeout.
//
// The Client cancels requests to the underlying Transport
// as if the Request's Context ended.https://golang.org/pkg/net/http/#Client
ตามเอกสารได้ออกแบบไว้ว่า http.Client
นั้นได้ถูกกำหนดไว้ ให้รองรับการทำงานแบบ Long running connection ดังนั้น ผู้ใช้ต้องปรับแต่งตามความเหมาะสม ตามลักษณะงานนั้นๆ ด้วยนะครับ
กำหนด Timeout ให้กับ &http.Client{}
ใช้การกำหนด Context
อีกทางเลือกนึงในการ Cancel request เมื่อถึง timeout ที่กำหนด เราสามารถใช้ Context ได้ เช่นกัน
วิเคราะห์ปัญหา TIME_WAIT
เมื่อเกิดการ Request ตัว TCP State ในส่วนของ TIME_WAIT
คือ ระยะการรอให้ Connection เก่าทำงานให้เสร็จก่อน และมีการ ACK ตอบกลับ ถึงจะ Close Connection แบบสมบูรณ์ได้ มีการเกิดจังหวะการรอ เคลีย์ของเก่าให้หมดก่อนนะ ถึงจะเริ่มสร้างของใหม่
ตัวอย่างโค๊ด ที่อาจเกิดปัญหา TIME_WAIT
เพื่อแก้ปัญหา TCP TIME_WAIT
เราเลยขอใช้ ioutil.ReadAll()
มาทดสอบ
และผลลัพธ์ที่ได้ TIME_WAIT
จะเท่ากับ 0 เพราะ เราอ่านค่าใน Stream เรียบร้อยแล้ว และ Connection นั้นจะบอก TCP State ว่าทำงานเสร็จเรียบร้อยแล้ว
Clients และ Transport สร้างครั้งเดียว และสามารถ Re-use ได้
tr := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
http.Transport มีอะไรให้เล่นบ้าง?
ถ้าเรา initiate struct &http.Client{}
ขึ้นมา สิ่งที่แถมมาด้วย คือ http.Transport
type Client struct {
// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
Transport RoundTripper
}
ซึ่งถ้าเข้าไปดู DefaultTransport จะมีค่า ดังนี้
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
Configuration ที่น่าสนใจ
- Proxy กำหนด function ที่ return http scheme ถ้าไม่ระบุเลย by default คือ
http
- TLSHandshakeTimeout กำหนด TLS Handshake ว่ามี timeout เท่าไหร่
- DisableKeepAlives กำหนดว่าจะให้ คง HTTP Connect Pool เอาไว้ เพื่อ Re-use HTTP Connection หรือไม่
- DisableCompression ต้องการ Compression Data ใน Transport Layer ไหม
- MaxIdleConns จำนวน HTTP Connection ที่เก็บไว้ใน Pool
- MaxIdleConnsPerHost จำนวน HTTP Connection ภายใต้ Host เดียว
- IdleConnTimeout TTL ของ idle connection ถ้าภายใน 90 วินาที ไม่มี HTTP Request เลย มันก็จะ terminate ตัวเองไป
สามารถ Config ค่าเพิ่มเติมโดยดูจาก Struct Transport ได้ ที่นี่ https://golang.org/src/net/http/transport.go
ถ้ามี HTTP Request เข้ามาที่ Host จำนวนมาก จัดการอย่างไรดี
การที่เปิด ปิด HTTP Connection บ่อย มันมี overhead อยู่พอสมควร ดังนั้น ต้องมีการเทส เพื่อหาสถิติการใช้งาน ทางออกของปัญหา ที่สามารถควบคุมได้ คือ การใช้ HTTP Connection pool ด้วยการเซท MaxIdleConns และ IdleConnTimeout ให้เหมาะสม อย่าลืมว่า Memory มีจำกัด
และสุดท้ายบทความนี้ต้องการนำเสนอข้อมูลพื้นฐาน เพื่อใช้ประกอบการตัดสินใจ ในการออกแบบ http.Client
ให้เหมาะสมกับโปรเจคที่ท่านผู้อ่านทำงานด้วยอยู่ ขอขอบคุณสำหรับการติดตามนะครับ หากผิดพลาดประการใด ขอคำแนะนำด้วยครับผม
Reference