Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more visibility to IP address resolution when using DNS #556

Merged
merged 26 commits into from May 5, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e6cd0b1
Add log to show the ip address DNS resolve to
wuhaoyujerry Apr 22, 2022
15b92fc
Add GetIpAddress method HTTPClient
wuhaoyujerry Apr 27, 2022
0bfe637
Fast client get ip address report
wuhaoyujerry Apr 28, 2022
243d759
get ip address for std client
wuhaoyujerry Apr 28, 2022
ff6d3b9
Merge branch 'fortio:master' into master
wuhaoyujerry Apr 28, 2022
a96fb27
remove todo and add error handling
wuhaoyujerry Apr 28, 2022
2ffe6bb
resolve code climate comments
wuhaoyujerry Apr 28, 2022
bc18497
fix the data race issue
wuhaoyujerry Apr 28, 2022
df56f48
run go lint
wuhaoyujerry Apr 28, 2022
f976f40
fix typo
wuhaoyujerry Apr 28, 2022
2cef96c
change fmt.Print to log.Info
wuhaoyujerry Apr 28, 2022
53a5138
sort ip address by its usage count
wuhaoyujerry Apr 28, 2022
b5c6807
fix go lint
wuhaoyujerry Apr 28, 2022
85f467b
update the log to show ip address per thread
wuhaoyujerry Apr 28, 2022
c606fcf
revert the go lint change
wuhaoyujerry Apr 28, 2022
26b433d
remove go routine to get ip address when using stdclient
wuhaoyujerry Apr 28, 2022
e58dfb5
add socket count for stdclient and add unit test
wuhaoyujerry Apr 28, 2022
fad9e7d
Add a todo to move the IPCountMap field
wuhaoyujerry Apr 28, 2022
aff79ab
Add unit test for ip distribution
wuhaoyujerry Apr 29, 2022
ee34952
revert diff
wuhaoyujerry May 3, 2022
626f6d5
change the sort ip distribution list logic
wuhaoyujerry May 4, 2022
433bacf
fix lint
wuhaoyujerry May 4, 2022
1ce5b9b
move the getIPUsageCount to httprunner_test.go
wuhaoyujerry May 4, 2022
031b469
change the client init order to avoid using pointer for socket count
wuhaoyujerry May 4, 2022
f3c54aa
Fix the issue that can cause nil pointer error and add unit test to c…
wuhaoyujerry May 5, 2022
db3d86e
Fix lint
wuhaoyujerry May 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 26 additions & 5 deletions fhttp/http_client.go
Expand Up @@ -46,6 +46,8 @@ type Fetcher interface {
// Close() cleans up connections and state - must be paired with NewClient calls.
// returns how many sockets have been used (Fastclient only)
Close() int
// GetIPAddress() get the ip address that DNS resolves to
GetIPAddress() string
}

const (
Expand Down Expand Up @@ -308,6 +310,7 @@ type Client struct {
bodyContainsUUID bool // if body contains the "{uuid}" pattern (lowercase)
logErrors bool
id int
socketCount *int
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
}

// Close cleans up any resources used by NewStdClient.
Expand All @@ -324,7 +327,7 @@ func (c *Client) Close() int {
if c.transport != nil {
c.transport.CloseIdleConnections()
}
return 0 // TODO: find a way to track std client socket usage.
return *c.socketCount
}

// ChangeURL only for standard client, allows fetching a different URL.
Expand Down Expand Up @@ -396,6 +399,11 @@ func (c *Client) Fetch() (int, []byte, int) {
return code, data, 0
}

// GetIPAddress get the ip address that DNS resolves to when using stdClient.
func (c *Client) GetIPAddress() string {
wuhaoyujerry marked this conversation as resolved.
Show resolved Hide resolved
return c.req.RemoteAddr
}

// NewClient creates either a standard or fast client (depending on
// the DisableFastClient flag).
func NewClient(o *HTTPOptions) (Fetcher, error) {
Expand All @@ -415,6 +423,8 @@ func NewStdClient(o *HTTPOptions) (*Client, error) {
if req == nil {
return nil, err
}

var socketCount int
tr := http.Transport{
MaxIdleConns: o.NumConnections,
MaxIdleConnsPerHost: o.NumConnections,
Expand All @@ -426,9 +436,14 @@ func NewStdClient(o *HTTPOptions) (*Client, error) {
if o.Resolve != "" {
addr = o.Resolve + addr[strings.LastIndex(addr, ":"):]
}
return (&net.Dialer{
conn, err := (&net.Dialer{
Timeout: o.HTTPReqTimeOut,
}).DialContext(ctx, network, addr)

req.RemoteAddr = conn.RemoteAddr().String()
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
socketCount++

return conn, err
},
TLSHandshakeTimeout: o.HTTPReqTimeOut,
}
Expand All @@ -451,9 +466,10 @@ func NewStdClient(o *HTTPOptions) (*Client, error) {
Timeout: o.HTTPReqTimeOut,
Transport: &tr,
},
transport: &tr,
id: o.ID,
logErrors: o.LogErrors,
transport: &tr,
id: o.ID,
logErrors: o.LogErrors,
socketCount: &socketCount,
}
if !o.FollowRedirects {
// Lets us see the raw response instead of auto following redirects.
Expand Down Expand Up @@ -511,6 +527,11 @@ type FastClient struct {
tlsConfig *tls.Config
}

// GetIPAddress get the ip address that DNS resolves to when using fast client.
func (c *FastClient) GetIPAddress() string {
wuhaoyujerry marked this conversation as resolved.
Show resolved Hide resolved
return c.dest.String()
}

// Close cleans up any resources used by FastClient.
func (c *FastClient) Close() int {
log.Debugf("Closing %p %s socket count %d", c, c.url, c.socketCount)
Expand Down
8 changes: 8 additions & 0 deletions fhttp/http_utils.go
Expand Up @@ -627,3 +627,11 @@ func (r *SyncReader) Read(p []byte) (n int, err error) {

return r.reader.Read(p)
}

func getIPUsageCount(ipCountMap map[string]int) (count int) {
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
for _, v := range ipCountMap {
count += v
}

return count
}
38 changes: 32 additions & 6 deletions fhttp/httprunner.go
Expand Up @@ -35,8 +35,9 @@ import (
// Also is the internal type used per thread/goroutine.
type HTTPRunnerResults struct {
periodic.RunnerResults
client Fetcher
RetCodes map[int]int64
client Fetcher
RetCodes map[int]int64
IPCountMap map[string]int // TODO: Move it to a shared results struct where all runner should have this field
// internal type/data
sizes *stats.Histogram
headerSizes *stats.Histogram
Expand All @@ -50,6 +51,12 @@ type HTTPRunnerResults struct {
aborter *periodic.Aborter
}

// IPCountPair stores the ip address and its corresponding usage count.
type IPCountPair struct {
ip string
count int
}

// Run tests http request fetching. Main call being run at the target QPS.
// To be set as the Function in RunnerOptions.
func (httpstate *HTTPRunnerResults) Run(t int) (bool, string) {
Expand Down Expand Up @@ -98,6 +105,7 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
total := HTTPRunnerResults{
HTTPOptions: o.HTTPOptions,
RetCodes: make(map[int]int64),
IPCountMap: make(map[string]int),
sizes: stats.NewHistogram(0, 100),
headerSizes: stats.NewHistogram(0, 5),
AbortOn: o.AbortOn,
Expand Down Expand Up @@ -182,6 +190,11 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
// unused ones. We also must cleanup all the created clients.
keys := []int{}
for i := 0; i < numThreads; i++ {
// Get the report on the IP address each thread use to send traffic
ip := httpstate[i].client.GetIPAddress()
log.Infof("[%d] %s resolve to IP address: %s\n", i, o.URL, ip)
total.IPCountMap[ip]++

total.SocketCount += httpstate[i].client.Close()
// Q: is there some copying each time stats[i] is used?
for k := range httpstate[i].RetCodes {
Expand All @@ -193,14 +206,27 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
total.sizes.Transfer(httpstate[i].sizes)
total.headerSizes.Transfer(httpstate[i].headerSizes)
}

// Sort the ip address form largest to smallest based on its usage count
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
ipCountList := make([]IPCountPair, 0, len(total.IPCountMap))
for k, v := range total.IPCountMap {
ipCountList = append(ipCountList, IPCountPair{k, v})
}

sort.Slice(ipCountList, func(i, j int) bool {
return ipCountList[i].count > ipCountList[j].count
})

// Cleanup state:
r.Options().ReleaseRunners()
sort.Ints(keys)
totalCount := float64(total.DurationHistogram.Count)
if !o.DisableFastClient {
_, _ = fmt.Fprintf(out, "Sockets used: %d (for perfect keepalive, would be %d)\n", total.SocketCount, r.Options().NumThreads)
}
_, _ = fmt.Fprintf(out, "Sockets used: %d (for perfect keepalive, would be %d)\n", total.SocketCount, r.Options().NumThreads)
_, _ = fmt.Fprintf(out, "Uniform: %t, Jitter: %t\n", total.Uniform, total.Jitter)
_, _ = fmt.Fprintf(out, "IP addresses distribution:\n")
for _, v := range ipCountList {
_, _ = fmt.Fprintf(out, "%s: %d\n", v.ip, v.count)
}
for _, k := range keys {
_, _ = fmt.Fprintf(out, "Code %3d : %d (%.1f %%)\n", k, total.RetCodes[k], 100.*float64(total.RetCodes[k])/totalCount)
}
Expand All @@ -216,7 +242,7 @@ func RunHTTPTest(o *HTTPRunnerOptions) (*HTTPRunnerResults, error) {
return &total, nil
}

// A errgroup is a collection of goroutines working on subtasks that are part of
// An errgroup is a collection of goroutines working on subtasks that are part of
// the same overall task.
type errgroup struct {
wg sync.WaitGroup
Expand Down
32 changes: 20 additions & 12 deletions fhttp/httprunner_test.go
Expand Up @@ -60,6 +60,10 @@ func TestHTTPRunner(t *testing.T) {
if res.SocketCount != res.RunnerResults.NumThreads {
t.Errorf("%d socket used, expected same as thread# %d", res.SocketCount, res.RunnerResults.NumThreads)
}
count := getIPUsageCount(res.IPCountMap)
if count != res.RunnerResults.NumThreads {
t.Errorf("Total IP usage count %d, expected same as thread %d", count, res.RunnerResults.NumThreads)
}
// Test raw client, should get warning about non init timeout:
rawOpts := HTTPOptions{
URL: opts.URL,
Expand Down Expand Up @@ -121,11 +125,8 @@ func testHTTPNotLeaking(t *testing.T, opts *HTTPRunnerOptions) {
if ngAfter > ngBefore2+8 {
t.Errorf("Goroutines after test %d, expected it to stay near %d", ngAfter, ngBefore2)
}
if !opts.DisableFastClient {
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
// only fast client so far has a socket count
if res.SocketCount != res.RunnerResults.NumThreads {
t.Errorf("%d socket used, expected same as thread# %d", res.SocketCount, res.RunnerResults.NumThreads)
}
if res.SocketCount != res.RunnerResults.NumThreads {
t.Errorf("%d socket used, expected same as thread# %d", res.SocketCount, res.RunnerResults.NumThreads)
}
}

Expand Down Expand Up @@ -190,17 +191,16 @@ func TestHTTPRunnerClientRace(t *testing.T) {
}
}

func TestClosingAndSocketCount(t *testing.T) {
func testClosingAndSocketCount(t *testing.T, o *HTTPRunnerOptions) {
mux, addr := DynamicHTTPServer(false)
mux.HandleFunc("/echo42/", EchoHandler)
URL := fmt.Sprintf("http://localhost:%d/echo42/?close=true", addr.Port)
opts := HTTPRunnerOptions{}
opts.Init(URL)
opts.QPS = 10
o.Init(URL)
o.QPS = 10
numReq := int64(50) // can't do too many without running out of fds on mac
opts.Exactly = numReq
opts.NumThreads = 5
res, err := RunHTTPTest(&opts)
o.Exactly = numReq
o.NumThreads = 5
res, err := RunHTTPTest(o)
if err != nil {
t.Fatal(err)
}
Expand All @@ -217,6 +217,14 @@ func TestClosingAndSocketCount(t *testing.T) {
}
}

func TestClosingAndSocketCountFastClient(t *testing.T) {
testClosingAndSocketCount(t, &HTTPRunnerOptions{})
}

func TestClosingAndSocketCountStdClient(t *testing.T) {
testClosingAndSocketCount(t, &HTTPRunnerOptions{HTTPOptions: HTTPOptions{DisableFastClient: true}})
wuhaoyujerry marked this conversation as resolved.
Show resolved Hide resolved
}

func TestHTTPRunnerBadServer(t *testing.T) {
// Using http to an https server (or the current 'close all' dummy https server)
// should fail:
Expand Down
1 change: 0 additions & 1 deletion stats/stats_test.go
Expand Up @@ -726,7 +726,6 @@ func TestBucketLookUp(t *testing.T) {
input float64 // input
start float64 // start
end float64 // end

}{
{input: 11, start: 10, end: 11},
{input: 171, start: 160, end: 180},
Expand Down
3 changes: 2 additions & 1 deletion ui/restHandler.go
Expand Up @@ -236,7 +236,8 @@ func RESTRunHandler(w http.ResponseWriter, r *http.Request) { // nolint: funlen

// Run executes the run (can be called async or not, writer is nil for async mode).
func Run(w http.ResponseWriter, r *http.Request, jd map[string]interface{},
runner, url string, ro periodic.RunnerOptions, httpopts *fhttp.HTTPOptions) {
runner, url string, ro periodic.RunnerOptions, httpopts *fhttp.HTTPOptions,
) {
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
// go func() {
var res periodic.HasRunnerResult
var err error
Expand Down