Why is there such a delay in processing of incoming requests by the main server goroutine and how can this delay be avoided ?
Simple code with NO DELAYS
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", root)
http.ListenAndServe(":8090", nil)
}
//---------------------------------------------------------------------------
// http handlers
//---------------------------------------------------------------------------
func root(w http.ResponseWriter, r *http.Request) {
log.Printf("[root] `%v`\n", r.URL.Path)
w.Write([]byte("What the hell"))
}
Result testing the loading
╰─➤ wrk -d20s -t5 -c100 http://localhost:8090
Running 20s test # http://localhost:8090
5 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 17.07ms 46.63ms 368.77ms 91.73%
Req/Sec 10.99k 4.87k 24.48k 62.49%
1038912 requests in 20.10s, 128.80MB read
Requests/sec: 51684.63
Transfer/sec: 6.41MB
Adding goroutines
package main
import (
"log"
"net/http"
)
func main() {
_ = NewTesterGo(100)
http.HandleFunc("/", root)
http.ListenAndServe(":8090", nil)
}
//---------------------------------------------------------------------------
// http handlers
//---------------------------------------------------------------------------
func root(w http.ResponseWriter, r *http.Request) {
log.Printf("[root] `%v`\n", r.URL.Path)
w.Write([]byte("What the fuck"))
}
//---------------------------------------------------------------------------
// tester segment
//---------------------------------------------------------------------------
type (
TesterGo struct {
Work chan string
}
)
func NewTesterGo(count int) *TesterGo {
t:=&TesterGo{
Work:make(chan string,100),
}
for ; count > 0 ; count -- {
go t.Worker()
}
return t
}
func (t *TesterGo) Worker() {
log.Printf("[testergo][worker][work] стартовал....\n")
for {
select {
case work := <-t.Work:
log.Printf("[testerGo][Worker] %v\n", work)
default:
}
}
}
Result with loading
╰─➤ wrk -d20s -t5 -c100 http://localhost:8090
Running 20s test # http://localhost:8090
5 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 464.71ms 305.44ms 1.90s 77.90%
Req/Sec 54.62 43.74 200.00 67.50%
3672 requests in 20.05s, 466.17KB read
Socket errors: connect 0, read 0, write 0, **timeout 97**
Requests/sec: **183.11**
Transfer/sec: 23.25KB
your goroutines use default, causing them to spin immediately if there is nothing in the channel (and there is nothing in your example). This probably makes Go's scheduler do way more context switching than needed, and probably consume a lot of CPU for nothing.
Is there a reason to default in a loop? If not try one of the following:
Either no default, the goroutines would simply "sleep" until there's work.
for {
select {
case work := <-t.Work:
log.Printf("[testerGo][Worker] %v\n", work)
}
}
This BTW makes the select completely redundant, so just get rid of it:
for { //you can also use a range on the channel
work := <- t.Work
log.Printf("[testerGo][Worker] %v\n", work)
}
Second option - a timeout that will make them wait before continuing in the loop:
for {
select {
case work := <-t.Work:
log.Printf("[testerGo][Worker] %v\n", work)
case <- time.After(100*time.Millisecond): //or whatever you prefer
}
}
Related
Broadly, I'm trying to answer the question "which of two approaches to handling incoming requests is more efficient". I'm measuring efficiency by:
lower max RAM use
lower percentage of CPU usage
These metrics will hopefully signal which approach is more suited to running on a resource-constrained server. It's gets a lot of volume, so the most important thing is that we can process requests faster than they come in. We've had "too many open files" problems when our queue filled up and the server had too many open connections.
So, tach of the two programs aims to respond to the initial request as quickly as possible and then queue up the actual work. In real life, it's an outgoing HTTP request; in my tests, it's a sleep for a variable amount of time, to try to simulate inconsistent conditions.
Here's the first program:
// approach A: spin up a few goroutines at the start
// and send messages into a long buffered channel
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
_ "net/http/pprof"
"time"
)
const (
minResponseTimeSec = 0.5
maxResponeTimeSec = 2.5
)
var messageChan = make(chan int, 1024)
func randResponseTime() float64 {
return minResponseTimeSec + rand.Float64()*(maxResponeTimeSec-minResponseTimeSec)
}
func main() {
rand.Seed(time.Now().UnixNano())
for i := 1; i <= 8; i++ {
go worker()
}
http.HandleFunc("/message", handler)
log.Fatal(http.ListenAndServe(":1234", nil))
}
func handler(writer http.ResponseWriter, request *http.Request) {
messageChan <- 3 // would normally send POST body
fmt.Fprint(writer, "ok\n")
}
func worker() {
for range messageChan {
time.Sleep(time.Second * time.Duration(randResponseTime()))
}
}
and here's the second:
// approach B: set a maximum number of concurrent
// goroutines and spin them up as needed
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
_ "net/http/pprof"
"time"
)
const (
minResponseTimeSec = 0.5
maxResponeTimeSec = 2.5
)
var (
semaphoreChan chan struct{}
)
func randResponseTime() float64 {
return minResponseTimeSec + rand.Float64()*(maxResponeTimeSec-minResponseTimeSec)
}
func main() {
rand.Seed(time.Now().UnixNano())
semaphoreChan = make(chan struct{}, 1024)
http.HandleFunc("/message", handler)
log.Fatal(http.ListenAndServe(":1234", nil))
}
func handler(writer http.ResponseWriter, request *http.Request) {
semaphoreChan <- struct{}{}
go fanout() // would normally send POST body
fmt.Fprint(writer, "ok\n")
}
func fanout() {
defer func() { <-semaphoreChan }()
time.Sleep(time.Second * time.Duration(randResponseTime()))
}
With each program running (separately) I used ab to send lots of requests at each in turn. But, I'm having trouble interpreting the data available at /profile/pprof. I was focusing on the stats at the bottom of /debug/pprof/heap?debug=1, especially TotalAlloc and HeapAlloc. But, those numbers all seem to grow indefinitely (as I refresh the page), while I'd expect them to stay flat before / after the benchmarking is done, which leads me to think I'm looking at the wrong numbers.
Why does Go log package slow my http APIs so bad? Is it that slow?
Here is my router example using httprouter without logging:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
func main() {
handler := httprouter.New()
handler.GET("/hello", f)
http.ListenAndServe(fmt.Sprintf(":%d", 8080), handler)
}
func f(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "world")
}
Using wrk to benchmark that endpoint I got this:
$ wrk -t1 -d1s -c100 http://localhost:8080/hello
Running 1s test # http://localhost:8080/hello
1 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.15ms 197.55us 2.84ms 80.02%
Req/Sec 84.58k 6.15k 99.01k 80.00%
83904 requests in 1.01s, 9.68MB read
Requests/sec: 83380.37
Transfer/sec: 9.62MB
When I added middleware for logging:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
func main() {
handler := httprouter.New()
handler.GET("/hello", logger(f))
fmt.Println("httprouter")
http.ListenAndServe(fmt.Sprintf(":%d", 8080), handler)
}
func logger(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
start := time.Now()
next(w, r, ps)
elapsed := time.Since(start)
log.Printf("%s | %s | %s | %d\n", time.Now().Format(time.RFC3339), r.Method, r.URL.Path, elapsed)
}
}
func f(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "world")
}
It got slowed down to the factor of 4x:
$ wrk -t1 -d1s -c100 http://localhost:8080/hello
Running 1s test # http://localhost:8080/hello
1 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.25ms 4.34ms 26.47ms 60.23%
Req/Sec 20.51k 2.19k 24.28k 70.00%
20449 requests in 1.01s, 2.36MB read
Requests/sec: 20330.66
Transfer/sec: 2.35MB
I tested it locally on:
MacBook Pro 13inches
2 GHz Quad-Core Intel Core i5
Memory 16GB
I use default Go max proc without modifying anything after installed.
Is log package that slow? Any suggestion how to improve this?
This answer summarizes the commentary on the question.
used buffered io
write from a goroutine to reduce blocking from other the logging goroutines.
Here's the code:
type writer chan []byte
func (w writer) Write(p []byte) (int, error) {
w <- append(([]byte)(nil), p...)
return len(p), nil
}
func writePump(w writer) {
bw := bufio.NewWriter(os.Stderr)
for p := range w {
bw.Write(p)
// Slurp up buffered messages in flush. This ensures
// timely output.
n := len(w)
for i := 0; i < n; i++ {
bw.Write(<-w)
}
bw.Flush()
}
}
Set it up as follows:
w := make(writer, 16) // adjust capacity to meet your needs
go writePump(w)
log.SetOutput(w)
given are the following 2 functions.
func main() {
index := int(0)
for {
Loop(index)
index = (index + 1) % 86400 // Max interval: 1 day
time.Sleep(1 * time.Second)
}
}
func Loop(index int) {
if index%10 == 0 {
go doSomething...
}
}
I want to execute something every 10/60/3600 seconds. So I thought an incrementing index with modulo should do this.
But what I noticed (especially on high traffic servers) that it appears to skip some of that loops.
I looked in my logs and sometimes there is something every 10 seconds but sometimes there is a gap up to 1 minute.
Does anybody know why this is happening?
I'd recommend using a time.Ticker to perform some action every N seconds. That way, you use built-in timers and only wake the CPU when something needs to be done. Even if the CPU is not heavily used, time.Sleep and a for loop is not the most reliable way to schedule tasks. For example (from the link above):
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
time.Sleep(10 * time.Second)
done <- true
}()
for {
select {
case <-done:
fmt.Println("Done!")
return
case t := <-ticker.C:
fmt.Println("Current time: ", t)
}
}
}
package main
import (
"fmt"
"log"
"net/http"
"time"
)
var ch chan bool
func testTimer1() {
go func() {
log.Println("test timer 1")
ch <- true
}()
}
func timer1() {
timer1 := time.NewTicker(2 * time.Second)
select {
case <-timer1.C:
testTimer1()
}
}
func myhandler(w http.ResponseWriter, r *http.Request) {
for {
go timer1()
a := <-ch
log.Println("get a: ", a)
fmt.Fprintf(w, "hello world!!!!", a)
}
log.Println("test for break")
}
func main() {
ch = make(chan bool)
http.HandleFunc("/", myhandler)
http.ListenAndServe(":8080", nil)
}
I wrote the above code, put a channel into "myhandler", channel will be
given a bool data when the
timer task executed.
then I get the data from channel and write "hello world" into http writer
but I found the client couldn't receive the "hello world", the writer has been blocked!!!!!
Any one knows about this?
looks the running pic on my cmd:
enter image description here
enter image description here
The for loop is an infinite loop so printing to the ResponseWriter is not "scheduled" to happen. If you want a comet-like approack (or long-polling URL) you may want to try this method.
There's also a leak of tickers in timer1(). According to the Go Docs:
Stop the ticker to release associated resources.
You're always creating a new ticker every time you call go timer1() and the ticker is never closed so every new ticker just adds-up.
Short answer
Avoid buffering
Client Side
Use curl with --no-buffer set
curl http://localhost:8080 --no-buffer
Server Side
Flush after every fmt.Fprint
w.(http.Flusher).Flush()
Long Answer
The biggest problem when implementing HTTP streaming is understanding the effect of buffering. Buffering is the practice of accumulating reads or writes into a temporarily fixed memory space. The advantages of buffering include reducing read or write call overhead. For example, instead of writing 1KB 4096 times, you can just write 4096KB at once. This means your program can create a write buffer holding 4096KB of temporary data (which can be aligned to the disk block size), and once the space limit is reached, the buffer is flushed to disk.
Here the above mentioned HTTP component include two components Server(go server) and Client(Curl).Each one of these components can possess adjustable and varied buffering styles and limits.
An unrelated issue, n the program given it has one more problem ie, not stopping timer always stop the ticker to release associated resources.
Here is an implementation with some corrections
Code
package main
import (
"fmt"
"log"
"net/http"
"time"
)
var ch chan bool
func testTimer1() {
go func() {
log.Println("test timer 1")
ch <- true
}()
}
func timer1() {
timer1 := time.NewTicker(2 * time.Second)
defer timer1.Stop()
<-timer1.C
testTimer1()
}
func myhandler(w http.ResponseWriter, r *http.Request) {
for {
go timer1()
a := <-ch
log.Println("get a: ", a)
fmt.Fprintf(w, "hello world!!!! - %v", a)
w.(http.Flusher).Flush()
}
}
func main() {
ch = make(chan bool)
http.HandleFunc("/", myhandler)
http.ListenAndServe(":8080", nil)
}
Curl
curl http://localhost:8080 --no-buffer
I am trying to measure execution time of funcWithUnpredictiveExecutionTime function.
func measureTime(expectedMs float64) (ok bool) {
t1 := time.Now()
funcWithUnpredictiveExecutionTime()
t2 := time.Now()
diff := t2.Sub(t1)
The measuring is fine when funcWithUnpredictiveExecutionTime works faster than I expected. But if it works slower than expectedMs the measuring will not stop right after expected amount of milliseconds passed.
Is it possible to stop time measuring when funcWithUnpredictiveExecutionTime works longer than expectedMs without waiting funcWithUnpredictiveExecutionTime to finish?
In other words, measureTime(200) should return in 200 ms anyway with a good or bad result.
I guess I should use channels and then somehow cancel waiting for a channel. But how to do it exactly?
Full code:
package main
import (
"fmt"
"math/rand"
"time"
)
// random number between min and max
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}
// sleeps for a random milliseconds amount between 200 and 1000
func funcWithUnpredictiveExecutionTime() {
millisToSleep := random(200, 1000)
fmt.Println(fmt.Sprintf("Sleeping for %d milliseconds", millisToSleep))
time.Sleep(time.Millisecond * time.Duration(millisToSleep))
}
// measures execution time of a function funcWithUnpredictiveExecutionTime
// if expectedMs < actual execution time, it's ok.
// if expectedMs milliseconds passed and funcWithUnpredictiveExecutionTime
// still did not finish execution it should return
// without waiting for funcWithUnpredictiveExecutionTime
func measureTime(expectedMs float64) (ok bool) {
t1 := time.Now()
funcWithUnpredictiveExecutionTime()
t2 := time.Now()
diff := t2.Sub(t1)
actualMs := diff.Seconds() * 1000
ok = actualMs < expectedMs
fmt.Println(actualMs)
return
}
// prints results: Ok or too late
func printTimeResults(ok bool) {
if ok {
fmt.Println("Ok")
} else {
fmt.Println("Too late")
}
}
func main() {
printTimeResults(measureTime(200)) // expect it to finish in 200 ms anyway
printTimeResults(measureTime(1000)) // expect it to finish in 1000 ms anyway
}
Output:
Sleeping for 422 milliseconds
424.11895200000004
Too late
Sleeping for 422 milliseconds
425.27274900000003
Ok
Playground
You can't cancel a goroutine, unless you design it to be canceled. You can short circuit your timing function, by using a channel to signal the completion of the function being timed:
func measureTime(expectedMs float64) (ok bool) {
done := make(chan struct{})
t1 := time.Now()
go func() {
funcWithUnpredictiveExecutionTime()
close(done)
}()
select {
case <-done:
ok = true
case <-time.After(time.Duration(expectedMs) * time.Millisecond):
}
fmt.Println(time.Since(t1))
return ok
}
Extending JimB's example a little with the design I've personally followed for async background workers. I would say in most cases it's unacceptable to launch a go routine without passing an abort channel... All of your async methods should accept one as an argument or have one defined on their receiving type so you can actually control execution. fyi there are libraries for this, here's a simple one an old colleague of mine made; https://github.com/lytics/squaredance
If your program does not have an abort path for every goroutine you're probably going to face significant quality issues sooner or later. Also, for applications that are doing any heavy lifting in a goroutine, you will likely not be able to gracefully stop and start your application.
func measureTime(expectedMs float64) (ok bool) {
done := make(chan struct{})
abort := make(chan struct{})
t1 := time.Now()
go func() {
funcWithUnpredictiveExecutionTime(abort)
close(done)
}()
select {
case <-done:
ok = true
case <-time.After(time.Duration(expectedMs) * time.Millisecond):
// after signals here when the duration is reached so I close abort
close(abort)
}
fmt.Println(time.Since(t1))
return ok
}
funcWithUnpredictiveExecutionTime(abort) {
for {
select {
// doing work in here
case abort:
// except here, we clean up and return
}
}
}