Time taken by 10000 API calls in too long

106 Views Asked by At

I am learning and trying the WaitGroup functionality in Golang. Here is my code:

package main

import (
    "atomic"
    "http"
    "log"
    "sync"
    "time"
)

func makeRequest(n int) {
    wg := sync.WaitGroup{}
    count := atomic.Int32{}
    wg.Add(n)
    s := time.Now()
    for j := 0; j < n; j++ {
        go myGoRoutine(&wg, &count)
    }
    wg.Wait()
    log.Printf("Time Elapsed: %v for %d iterations", time.Since(s), n)
}

func myGoRoutine(wg *sync.WaitGroup, count *atomic.Int32) {
    defer wg.Done()
    _, err := http.Get("https://jsonplaceholder.typicode.com/todos/1")
    if err != nil {
        log.Printf("Error: %v", err.Error())
    }
    count.Add(1)
}
func main() {
    cases := []int{1, 10, 100, 1000, 10000}
    for _, c := range cases {
        makeRequest(c)
    }

}

This is the output:

2023/12/28 07:31:34 Time Elapsed: 936.169917ms for 1 iterations
2023/12/28 07:31:34 Time Elapsed: 16.452041ms for 10 iterations
2023/12/28 07:31:34 Time Elapsed: 37.336667ms for 100 iterations
2023/12/28 07:31:36 Time Elapsed: 2.037294792s for 1000 iterations
2023/12/28 07:32:23 Time Elapsed: 47.1717935s for 10000 iterations

Since all API calls are happening concurrently, I expect all 5 cases to take similar if not the same amount of time. This is indeed true for the first 3 cases but then it takes 2s for 1000 and over 45s for 10000 API calls.

My understanding of WaitGroup in Golang is barely a couple of days old. Its likely that I am making a mistake in the code or my fundamental understanding is incorrect. Either way, I would appreciate any help.

1

There are 1 best solutions below

3
nimdrak On

Summary

  • We need to check the overhead of Goroutine by using Benchmark test if needed.
  • Goroutine itself is efficient when doing multiple high load tasks in parallel. But if the load is too tiny, then it can be better to do them in serial.
  • Your code is not proper to test Goroutine performance because Calling API multiple times can make queueing at the backend server.

In detail

Go routine test with low load task

  • You can see go routine has worse-performance with low load tasks than just calling a function
func BenchmarkTest_makeRequest(b *testing.B) {
    tests := []struct {
        makeRequest func(n int)
    }{
        {
            makeRequest: makeRequestWithGoRoutine,
        },

        {
            makeRequest: makeRequestWithoutGoRoutine,
        },

        {
            makeRequest: makeRequestWithoutFunctionCall,
        },
    }
    for _, tt := range tests {
        b.Run("test", func(b *testing.B) {
            tt.makeRequest(b.N)
        })
    }
}

BenchmarkTest_makeRequest/test-12                5522967               206.9 ns/op
BenchmarkTest_makeRequest/test#01-12            79312916                15.42 ns/op
BenchmarkTest_makeRequest/test#02-12            163376266                6.478 ns/op

Go routine test with high load task

  • You can see go routine has higher-performance with high load tasks than just calling a function
  • For simulating the high load, I just added time.Sleep(time.Millisecond * 10).
func BenchmarkTest_makeRequestWithHighLoad(b *testing.B) {
    tests := []struct {
        makeRequest func(n int)
    }{
        {
            makeRequest: makeRequestWithGoRoutine,
        },

        {
            makeRequest: makeRequestWithoutGoRoutine,
        },
    }
    for _, tt := range tests {
        b.Run("test", func(b *testing.B) {
            tt.makeRequest(b.N)
        })
    }
}
BenchmarkTest_makeRequestWithHighLoad/test-12            2819386               409.7 ns/op
BenchmarkTest_makeRequestWithHighLoad/test#01-12             100          10787388 ns/op

What overhead are in Goroutine and how can we handle it?

The rest of the codes

func myGoRoutine(wg *sync.WaitGroup, count *int32) {
    defer wg.Done()
    atomic.AddInt32(count, 1)
}
func myGoRoutineWithHighLoad(wg *sync.WaitGroup, count *int32) {
    defer wg.Done()
    time.Sleep(time.Millisecond * 10)
    atomic.AddInt32(count, 1)
}

func makeRequestWithGoRoutine(n int) {
    wg := sync.WaitGroup{}
    var count int32
    wg.Add(n)
    for j := 0; j < n; j++ {
//      go myGoRoutine(&wg, &count)
        go myGoRoutineWithHighLoad(&wg, &count)
    }
    wg.Wait()
}

func makeRequestWithoutGoRoutine(n int) {
    wg := sync.WaitGroup{}
    var count int32
    wg.Add(n)
    for j := 0; j < n; j++ {
//      myGoRoutine(&wg, &count)
        myGoRoutineWithHighLoad(&wg, &count)
    }
    wg.Wait()
}

func makeRequestWithoutFunctionCall(n int) {
    wg := sync.WaitGroup{}
    var count int32
    wg.Add(n)
    for j := 0; j < n; j++ {
        count++
        wg.Done()
    }
    wg.Wait()
}