Go webserver performance drastically drops as number of requests increases - go

I'm benchmarking a simple webserver written in Go using wrk. The server is running on a machine with 4GB RAM. At the beginning of the test, the performance is very good with the code serving up to 2000 requests/second. But as time goes on, memory utilized by the process creeps up and once it reaches 85% (I'm checking this using top), the throughput drops to ~100 requests/second. The throughput again increases to optimal amounts once I restart the server.
Is the degradation of performance due to memory issues? Why isn't Go releasing this memory? My Go server looks like this:
func main() {
defer func() {
// Wait for all messages to drain out before closing the producer
p.Flush(1000)
p.Close()
}()
http.HandleFunc("/endpoint", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
In the handler, I convert the incoming Protobuf message to a Json and write it to Kafka using confluent Kafka Go library.
var p, err = kafka.NewProducer(&kafka.ConfigMap{
"bootstrap.servers": "abc-0.com:6667,abc-1.com:6667",
"message.timeout.ms": "30000",
"sasl.kerberos.keytab": "/opt/certs/TEST.KEYTAB",
"sasl.kerberos.principal": "TEST#TEST.ABC.COM",
"sasl.kerberos.service.name": "kafka",
"security.protocol": "SASL_PLAINTEXT",
})
var topic = "test"
func handler(w http.ResponseWriter, r *http.Request) {
body, _ := ioutil.ReadAll(r.Body)
// Deserialize byte[] to Protobuf message
protoMessage := &tutorial.REALTIMEGPS{}
_ := proto.Unmarshal(body, protoMessage)
// Convert Protobuf to Json
realTimeJson, _ := convertProtoToJson(protoMessage)
_, err := fmt.Fprintf(w, "")
if err != nil {
log.Fatal(responseErr)
}
// Send to Kafka
produceMessage([]byte(realTimeJson))
}
func produceMessage(message []byte) {
// Delivery report
go func() {
for e := range p.Events() {
switch ev := e.(type) {
case *kafka.Message:
if ev.TopicPartition.Error != nil {
log.Println("Delivery failed: ", ev.TopicPartition)
} else {
log.Println("Delivered message to ", ev.TopicPartition)
}
}
}
}()
// Send message
_ := p.Produce(&kafka.Message{
TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
Value: message,
}, nil)
}
func convertProtoToJson(pb proto.Message) (string, error) {
marshaler := jsonpb.Marshaler{}
json, err := marshaler.MarshalToString(pb)
return json, err
}

The problem is that at the end of each of your request you call produceMessage(), which sends a message to kafka, and launches a goroutine to receive events for checking errors.
Your code launches goroutines non-stop as incoming requests come in, and they don't end until something goes wrong with your kafka client. This requires more and more memory, and likely more and more CPU as more goroutines are to be scheduled.
Don't do this. A single goroutine is sufficient for delivery report purposes. Launch a single goroutine when your p variable is set, and you're good to go.
For example:
var p *kafka.Producer
func init() {
var err error
p, err = kafka.NewProducer(&kafka.ConfigMap{
// ...
}
if err != nil {
// Handle error
}
// Delivery report
go func() {
for e := range p.Events() {
switch ev := e.(type) {
case *kafka.Message:
if ev.TopicPartition.Error != nil {
log.Println("Delivery failed: ", ev.TopicPartition)
} else {
log.Println("Delivered message to ", ev.TopicPartition)
}
}
}
}()
}

Related

Golang - instability with Gob over TCP

I am having an issue with instability when using gob for TCP communication.
The main problem is that if i transfer data "to fast" i either get errors where the server terminates the connection or the package simply doesn't arrive at the server. Now, if i add a delay of 20ms between packages, everything runs as expected.
Sadly i cannot link this to playground as i am running it in three different libraries, but i have inserted the essential/culprit code.
My guess is that if i don't have a delay timer i am overwriting the stream. Any ideas?
Update: By swapping out the receiver to bufio.NewReader(c.socket) i am actually able to get an error: "gob: unknown type id or corrupted data" which somewhat like the same issue as https://github.com/golang/go/issues/1238#event-242248991
//client -> server, running as a goroutine
func (c *Client) transmitter() {
d := 20 * time.Millisecond
timer := time.NewTimer(d) //silly hack
for {
select {
case gd := <- c.broadcast:
<- timer.C
Write(c.socket, gd.Data, gd.NetworkDataType)
timer.Reset(d) //refreshing timer
}
}
}
//server <- client, running as a goroutine
func (c *client) receiver (s *Server){
for {
in, err := Read(c.socket)
if err != nil {
log.println(err)
}
//doing stuff
}
}
func Read(conn net.Conn) (GeneralData, error) {
in := GeneralData{}
if err := gob.NewDecoder(conn).Decode(&in); err != nil {
return in, err
}
return in, nil
}
func Write(conn net.Conn, data interface{}, ndt NetworkDataType) {
err := gob.NewEncoder(conn).Encode(GeneralData{ndt,data})
if err != nil {
log.Println("error in write: ", err)
}
}

socket: too many open files Error for goroutines in indefinite loop

I have a requirement in my program to send metrics to datadog indefinitely (for continuous app monitoring in datadog). The program runs for a while and exits with the error "dial udp 127.0.0.1:18125: socket: too many open files".
func sendData(name []string, channel chan []string) {
c, err := statsd.New("127.0.0.1:18125")
if err != nil {
log.Fatal(err)
}
v := versionDetails()
tag := "tag:" + v
final_tag := []string{dd_tags}
appEpochTimeList := epochTime()
rate := float64(1)
for i, app := range name {
e := c.Gauge(app, float64(appEpochTimeList[i]), final_tag , rate)
if e != nil {
log.Println(e)
channel <- name
}
channel <- name
log.Printf("Metrics Sent !!")
}
}
The app names are read from a config.toml file
The problem is your sendData() function. This function is called in your for loop and has the following line:
c, err := statsd.New("127.0.0.1:18125")
This line will create a new DataDog client, which uses a Unix socket. This explains your error message.
With every iteration of your loop, a new socket is "allocated". After a sufficient amount of loops no sockets can be opened, resulting in:
socket: too many open files
To fix this you should create the client only once and pass it to your method as parameter.
func sendData(client *statsd.Client, name []string, channel chan []string) {
// do something with client...
}
func main() {
client, err := statsd.New("127.0.0.1:18125")
if err != nil {
log.Fatal(err)
}
// do something else ...
for res := range channel {
go func(client *statsd.Client, appName []string) {
time.Sleep(5 * time.Second)
go sendData(client, appName, channel)
}(client, res)
}
}

GCP Pub/sub: using goroutines to make multiple subscriber running in one application

I have found a strange behaviour when receiving message from GCP Pub/Sub.
Following codes are how I register the subscriptions using pubsub client
gcp.go
package gcp
import (
"context"
"path"
"runtime"
"google.golang.org/api/option"
"cloud.google.com/go/pubsub"
)
// PubsubClient is the GCP pubsub service client.
var PubsubClient *pubsub.Client
// Initialize initializes GCP client service using the environment.
func Initialize(env, projectName string) error {
var err error
ctx := context.Background()
credentialOpt := option.WithCredentialsFile(getFilePathByEnv(env))
PubsubClient, err = pubsub.NewClient(ctx, projectName, credentialOpt)
return err
}
// GetTopic returns the specified topic in GCP pub/sub service and create it if it not exist.
func GetTopic(topicName string) (*pubsub.Topic, error) {
topic := PubsubClient.Topic(topicName)
ctx := context.Background()
isTopicExist, err := topic.Exists(ctx)
if err != nil {
return topic, err
}
if !isTopicExist {
ctx = context.Background()
topic, err = PubsubClient.CreateTopic(ctx, topicName)
}
return topic, err
}
// GetSubscription returns the specified subscription in GCP pub/sub service and creates it if it not exist.
func GetSubscription(subName string, topic *pubsub.Topic) (*pubsub.Subscription, error) {
sub := PubsubClient.Subscription(subName)
ctx := context.Background()
isSubExist, err := sub.Exists(ctx)
if err != nil {
return sub, err
}
if !isSubExist {
ctx = context.Background()
sub, err = PubsubClient.CreateSubscription(ctx, subName, pubsub.SubscriptionConfig{Topic: topic})
}
return sub, err
}
func getFilePathByEnv(env string) string {
_, filename, _, _ := runtime.Caller(1)
switch env {
case "local":
return path.Join(path.Dir(filename), "local.json")
case "development":
return path.Join(path.Dir(filename), "development.json")
case "staging":
return path.Join(path.Dir(filename), "staging.json")
case "production":
return path.Join(path.Dir(filename), "production.json")
default:
return path.Join(path.Dir(filename), "local.json")
}
}
main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"runtime"
"runtime/debug"
"runtime/pprof"
"time"
"rpriambudi/pubsub-receiver/gcp"
"cloud.google.com/go/pubsub"
"github.com/go-chi/chi"
)
func main() {
log.Fatal(http.ListenAndServe(":4001", Route()))
}
func Route() *chi.Mux {
InitializeSubscription()
chiRoute := chi.NewRouter()
chiRoute.Route("/api", func(r chi.Router) {
r.Get("/_count", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Number of goroutines: %v", runtime.NumGoroutine())
})
r.Get("/_stack", getStackTraceHandler)
})
return chiRoute
}
func InitializeSubscription() {
gcp.Initialize("local", "fifth-bonbon-277102")
go pubsubHandler("test-topic-1", "test-topic-1-subs")
go pubsubHandler("test-topic-2", "test-topic-2-subs")
go pubsubHandler("test-topic-3", "test-topic-3-subs")
// ....
return
}
func getStackTraceHandler(w http.ResponseWriter, r *http.Request) {
stack := debug.Stack()
w.Write(stack)
pprof.Lookup("goroutine").WriteTo(w, 2)
}
func pubsubHandler(topicID string, subscriptionID string) {
topic, err := gcp.GetTopic(topicID)
fmt.Println("topic: ", topic)
if err != nil {
fmt.Println("Failed get topic: ", err)
return
}
sub, err := gcp.GetSubscription(subscriptionID, topic)
fmt.Println("subscription: ", sub)
if err != nil {
fmt.Println("Get subscription err: ", err)
return
}
err = sub.Receive(context.Background(), func(ctx context.Context, msg *pubsub.Message) {
messageHandler(subscriptionID, ctx, msg)
})
if err != nil {
fmt.Println("receive error: ", err)
}
}
func messageHandler(subscriptionID string, ctx context.Context, msg *pubsub.Message) {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered from panic.")
msg.Ack()
}
}()
fmt.Println("message of subscription: ", subscriptionID)
fmt.Println("Message ID: ", string(msg.ID))
fmt.Println("Message received: ", string(msg.Data))
msg.Ack()
time.Sleep(10 * time.Second)
}
It works great when i just have a few of pubsubHandler inside the InitializeSubscription. But when I adding more pubsubHandler inside the initialize function (approx 10 or more handler), things starting got interesting. The ack never reach the pubsub server, making the message is simply not ack-ed (I have checked the AcknowledgeRequest in metrics explorer, and no ack request coming). Thus, the message is keep coming back to the subscriber. Also, when i restart the application, sometimes it won't receive any message, neither new or an un-acked ones.
I seems to find a workaround by set the NumGoroutines to 1 for each subscription object in the pubsubHandler function.
func pubsubHandler(topicID string, subscriptionID string) {
....
sub, err := gcp.GetSubscription(subscriptionID, topic)
....
sub.ReceiverSettings.NumGoroutines = 1
err = sub.Receive(context.Background(), func(ctx context.Context, msg *pubsub.Message) {
messageHandler(subscriptionID, ctx, msg)
})
....
}
My question is, is this an intended behaviour? What is the root cause that may lead to those unexpected behaviours? Or my implementations is simply wrong, to achieve the intended results? (multiple subscription inside one application). Or is there any best practices to follow when creating a subscription handler?
In my understanding, the Receive function from pubsub.Subscription is a blocking code natively. Hence, when I tried to run it inside a goroutines, it may lead to an unexpected side effects, especially if we're not limiting the number of goroutines that may handle the messages. Is my reasoning a valid one?
Thank you for your answers, and have a good day!
Edit 1: Updating the example to a full code, since the pubsub client is not directly imported in the main.go before.
I believe the issue might be the rate at which you are handling messages (currently 10 seconds per message). If you receive too many messages at once, your client might be overwhelmed, which will lead to a buildup of a backlog of messages.
I recommend playing around with flow control settings and increasing ReceiveSettings.NumGoroutines to something higher than the default of 10. If your publish rate is high, you could also increase MaxOutstandingMessages, or completely disable the limit by setting it to -1. This tells the client to hold onto more messages at once, a limit that is shared per Receive call.

What is the right way to use this code (Paho MQTT) as GoRoutine and pass messages via channel to publish via websockets

As standard code I am using to publish message for testing purpose:
func main() {
opts := MQTT.NewClientOptions().AddBroker("tcp://127.0.0.1:1883")
opts.SetClientID("myclientid_")
opts.SetDefaultPublishHandler(f)
opts.SetConnectionLostHandler(connLostHandler)
opts.OnConnect = func(c MQTT.Client) {
fmt.Printf("Client connected, subscribing to: test/topic\n")
if token := c.Subscribe("logs", 0, nil); token.Wait() && token.Error() != nil {
fmt.Println(token.Error())
os.Exit(1)
}
}
c := MQTT.NewClient(opts)
if token := c.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}
for i := 0; i < 5; i++ {
text := fmt.Sprintf("this is msg #%d!", i)
token := c.Publish("logs", 0, false, text)
token.Wait()
}
time.Sleep(3 * time.Second)
if token := c.Unsubscribe("logs"); token.Wait() && token.Error() != nil {
fmt.Println(token.Error())
os.Exit(1)
}
c.Disconnect(250)
}
This works well ! but passing messages in mass while doing high latency tasks, performance of my program will be low, so I have to use goroutine and channel.
So, I was looking for a way to make a Worker inside goroutine for PUBLISHING messages to the browser using Paho MQTT library for GOlang, I had a hard time to find a better solution that feet my need, but after some searches, I found this code:
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"strings"
"time"
MQTT "git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.golang.git"
"linksmart.eu/lc/core/catalog"
"linksmart.eu/lc/core/catalog/service"
)
// MQTTConnector provides MQTT protocol connectivity
type MQTTConnector struct {
config *MqttProtocol
clientID string
client *MQTT.Client
pubCh chan AgentResponse
subCh chan<- DataRequest
pubTopics map[string]string
subTopicsRvsd map[string]string // store SUB topics "reversed" to optimize lookup in messageHandler
}
const defaultQoS = 1
func (c *MQTTConnector) start() {
logger.Println("MQTTConnector.start()")
if c.config.Discover && c.config.URL == "" {
err := c.discoverBrokerEndpoint()
if err != nil {
logger.Println("MQTTConnector.start() failed to start publisher:", err.Error())
return
}
}
// configure the mqtt client
c.configureMqttConnection()
// start the connection routine
logger.Printf("MQTTConnector.start() Will connect to the broker %v\n", c.config.URL)
go c.connect(0)
// start the publisher routine
go c.publisher()
}
// reads outgoing messages from the pubCh und publishes them to the broker
func (c *MQTTConnector) publisher() {
for resp := range c.pubCh {
if !c.client.IsConnected() {
logger.Println("MQTTConnector.publisher() got data while not connected to the broker. **discarded**")
continue
}
if resp.IsError {
logger.Println("MQTTConnector.publisher() data ERROR from agent manager:", string(resp.Payload))
continue
}
topic := c.pubTopics[resp.ResourceId]
c.client.Publish(topic, byte(defaultQoS), false, resp.Payload)
// We dont' wait for confirmation from broker (avoid blocking here!)
//<-r
logger.Println("MQTTConnector.publisher() published to", topic)
}
}
func (c *MQTTConnector) stop() {
logger.Println("MQTTConnector.stop()")
if c.client != nil && c.client.IsConnected() {
c.client.Disconnect(500)
}
}
func (c *MQTTConnector) connect(backOff int) {
if c.client == nil {
logger.Printf("MQTTConnector.connect() client is not configured")
return
}
for {
logger.Printf("MQTTConnector.connect() connecting to the broker %v, backOff: %v sec\n", c.config.URL, backOff)
time.Sleep(time.Duration(backOff) * time.Second)
if c.client.IsConnected() {
break
}
token := c.client.Connect()
token.Wait()
if token.Error() == nil {
break
}
logger.Printf("MQTTConnector.connect() failed to connect: %v\n", token.Error().Error())
if backOff == 0 {
backOff = 10
} else if backOff <= 600 {
backOff *= 2
}
}
logger.Printf("MQTTConnector.connect() connected to the broker %v", c.config.URL)
return
}
func (c *MQTTConnector) onConnected(client *MQTT.Client) {
// subscribe if there is at least one resource with SUB in MQTT protocol is configured
if len(c.subTopicsRvsd) > 0 {
logger.Println("MQTTPulbisher.onConnected() will (re-)subscribe to all configured SUB topics")
topicFilters := make(map[string]byte)
for topic, _ := range c.subTopicsRvsd {
logger.Printf("MQTTPulbisher.onConnected() will subscribe to topic %s", topic)
topicFilters[topic] = defaultQoS
}
client.SubscribeMultiple(topicFilters, c.messageHandler)
} else {
logger.Println("MQTTPulbisher.onConnected() no resources with SUB configured")
}
}
func (c *MQTTConnector) onConnectionLost(client *MQTT.Client, reason error) {
logger.Println("MQTTPulbisher.onConnectionLost() lost connection to the broker: ", reason.Error())
// Initialize a new client and reconnect
c.configureMqttConnection()
go c.connect(0)
}
func (c *MQTTConnector) configureMqttConnection() {
connOpts := MQTT.NewClientOptions().
AddBroker(c.config.URL).
SetClientID(c.clientID).
SetCleanSession(true).
SetConnectionLostHandler(c.onConnectionLost).
SetOnConnectHandler(c.onConnected).
SetAutoReconnect(false) // we take care of re-connect ourselves
// Username/password authentication
if c.config.Username != "" && c.config.Password != "" {
connOpts.SetUsername(c.config.Username)
connOpts.SetPassword(c.config.Password)
}
// SSL/TLS
if strings.HasPrefix(c.config.URL, "ssl") {
tlsConfig := &tls.Config{}
// Custom CA to auth broker with a self-signed certificate
if c.config.CaFile != "" {
caFile, err := ioutil.ReadFile(c.config.CaFile)
if err != nil {
logger.Printf("MQTTConnector.configureMqttConnection() ERROR: failed to read CA file %s:%s\n", c.config.CaFile, err.Error())
} else {
tlsConfig.RootCAs = x509.NewCertPool()
ok := tlsConfig.RootCAs.AppendCertsFromPEM(caFile)
if !ok {
logger.Printf("MQTTConnector.configureMqttConnection() ERROR: failed to parse CA certificate %s\n", c.config.CaFile)
}
}
}
// Certificate-based client authentication
if c.config.CertFile != "" && c.config.KeyFile != "" {
cert, err := tls.LoadX509KeyPair(c.config.CertFile, c.config.KeyFile)
if err != nil {
logger.Printf("MQTTConnector.configureMqttConnection() ERROR: failed to load client TLS credentials: %s\n",
err.Error())
} else {
tlsConfig.Certificates = []tls.Certificate{cert}
}
}
connOpts.SetTLSConfig(tlsConfig)
}
c.client = MQTT.NewClient(connOpts)
}
This code do exactly what I am looking for !
But as noob in Golang, I can't figure out how to run START() function inside my main function and what argument to pass !
And espacially, how I will process to pass messages to the worker (Publisher) using channel ?!
Your help will be appreciated !
I posted the answer below on the github repo but as you have asked the same question here thought it was worth cross posting (with a bit more info).
When you say "passing messages in mass while doing high latency tasks" I assume that you mean that you want to send the messages asynchronously (so the message is handled by a different go-routine than your main code is running on).
If that is the case then a very simple change to your initial example will give you that:
for i := 0; i < 5; i++ {
text := fmt.Sprintf("this is msg #%d!", i)
token := c.Publish("logs", 0, false, text)
// comment out... token.Wait()
}
Note: Your example code may exit before the messages are actually sent; adding time.Sleep(10 &ast; time.Second) would give it time for these to go out; see the code below for another way of handling this
The only reason that your initial code stopped until the message was sent was that you called token.Wait(). If you don't care about errors (and you are not checking for them so I assume you dont care) then there is little point in calling token.Wait() (it just waits until the message is sent; the message will go out whether you call token.Wait() or not).
If you want to log any errors you could use something like:
for i := 0; i < 5; i++ {
text := fmt.Sprintf("this is msg #%d!", i)
token := c.Publish("logs", 0, false, text)
go func(){
token.Wait()
err := token.Error()
if err != nil {
fmt.Printf("Error: %s\n", err.Error()) // or whatever you want to do with your error
}
}()
}
Note that there are a few more things that you need to do if message delivery is critical (but as you are not checking for errors I'm assuming its not).
In terms of the code you found; I suspect that this would add complexity you dont need (and more info would be required to work this out; for example the MqttProtocol struct is not defined within the bit you pasted).
Extra bit... In your comments you mentioned "Published messages must be ordered". If that is essential (so you want to wait until each message has been delivered before sending another) then you need something like:
msgChan := make(chan string, 200) // Allow a queue of up to 200 messages
var wg sync.WaitGroup
wg.Add(1)
go func(){ // go routine to send messages from channel
for msg := range msgChan {
token := c.Publish("logs", 2, false, msg) // Use QOS2 is order is vital
token.Wait()
// should check for errors here
}
wg.Done()
}()
for i := 0; i < 5; i++ {
text := fmt.Sprintf("this is msg #%d!", i)
msgChan <- text
}
close(msgChan) // this will stop the goroutine (when all messages processed)
wg.Wait() // Wait for all messages to be sent before exiting (may wait for ever is mqtt broker down!)
Note: This is similar to the solution from Ilya Kaznacheev (if you set workerPoolSize to 1 and make the channel buffered)
As your comments indicate that the wait group makes this difficult to understand here is another way of waiting that might be clearer (waitgroups are generally used when you are waiting for multiple things to finnish; in this example we are only waiting for one thing so a simpler approach can be used)
msgChan := make(chan string, 200) // Allow a queue of up to 200 messages
done := make(chan struct{}) // channel used to indicate when go routine has finnished
go func(){ // go routine to send messages from channel
for msg := range msgChan {
token := c.Publish("logs", 2, false, msg) // Use QOS2 is order is vital
token.Wait()
// should check for errors here
}
close(done) // let main routine know we have finnished
}()
for i := 0; i < 5; i++ {
text := fmt.Sprintf("this is msg #%d!", i)
msgChan <- text
}
close(msgChan) // this will stop the goroutine (when all messages processed)
<-done // wait for publish go routine to complete
Why don't you just split message sending into a bunch of workers?
Something like this:
...
const workerPoolSize = 10 // the number of workers you want to have
wg := &sync.WaitGroup{}
wCh := make(chan string)
wg.Add(workerPoolSize) // you want to wait for 10 workers to finish the job
// run workers in goroutines
for i := 0; i < workerPoolSize; i++ {
go func(wch <-chan string) {
// get the data from the channel
for text := range wch {
c.Publish("logs", 0, false, text)
token.Wait()
}
wg.Done() // worker says that he finishes the job
}(wCh)
}
for i := 0; i < 5; i++ {
// put the data to the channel
wCh <- fmt.Sprintf("this is msg #%d!", i)
}
close(wCh)
wg.Wait() // wait for all workers to finish
...

Multiple docker container logs

I'm trying to get the logs from multiple docker containers at once (order doesn't matter). This works as expected if types.ContainerLogsOption.Follow is set to false.
If types.ContainerLogsOption.Follow is set to true sometimes the log output get stuck after a few logs and no follow up logs are printed to stdout.
If the output doesn't get stuck it works as expected.
Additionally if I restart one or all of the containers the command doesn't exit like docker logs -f containerName does.
func (w *Whatever) Logs(options LogOptions) {
readers := []io.Reader{}
for _, container := range options.Containers {
responseBody, err := w.Docker.Client.ContainerLogs(context.Background(), container, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: options.Follow,
})
defer responseBody.Close()
if err != nil {
log.Fatal(err)
}
readers = append(readers, responseBody)
}
// concatenate all readers to one
multiReader := io.MultiReader(readers...)
_, err := stdcopy.StdCopy(os.Stdout, os.Stderr, multiReader)
if err != nil && err != io.EOF {
log.Fatal(err)
}
}
Basically there is no great difference in my implementation from that of docker logs https://github.com/docker/docker/blob/master/cli/command/container/logs.go, hence I'm wondering what causes this issues.
As JimB commented, that method won't work due to the operation of io.MultiReader. What you need to do is read from each from each response individually and combine the output. Since you're dealing with logs, it would make sense to break up the reads on newlines. bufio.Scanner does this for a single io.Reader. So one option would be to create a new type that scans multiple readers concurrently.
You could use it like this:
scanner := NewConcurrentScanner(readers...)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatalln(err)
}
Example implementation of a concurrent scanner:
// ConcurrentScanner works like io.Scanner, but with multiple io.Readers
type ConcurrentScanner struct {
scans chan []byte // Scanned data from readers
errors chan error // Errors from readers
done chan struct{} // Signal that all readers have completed
cancel func() // Cancel all readers (stop on first error)
data []byte // Last scanned value
err error
}
// NewConcurrentScanner starts scanning each reader in a separate goroutine
// and returns a *ConcurrentScanner.
func NewConcurrentScanner(readers ...io.Reader) *ConcurrentScanner {
ctx, cancel := context.WithCancel(context.Background())
s := &ConcurrentScanner{
scans: make(chan []byte),
errors: make(chan error),
done: make(chan struct{}),
cancel: cancel,
}
var wg sync.WaitGroup
wg.Add(len(readers))
for _, reader := range readers {
// Start a scanner for each reader in it's own goroutine.
go func(reader io.Reader) {
defer wg.Done()
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
select {
case s.scans <- scanner.Bytes():
// While there is data, send it to s.scans,
// this will block until Scan() is called.
case <-ctx.Done():
// This fires when context is cancelled,
// indicating that we should exit now.
return
}
}
if err := scanner.Err(); err != nil {
select {
case s.errors <- err:
// Reprort we got an error
case <-ctx.Done():
// Exit now if context was cancelled, otherwise sending
// the error and this goroutine will never exit.
return
}
}
}(reader)
}
go func() {
// Signal that all scanners have completed
wg.Wait()
close(s.done)
}()
return s
}
func (s *ConcurrentScanner) Scan() bool {
select {
case s.data = <-s.scans:
// Got data from a scanner
return true
case <-s.done:
// All scanners are done, nothing to do.
case s.err = <-s.errors:
// One of the scanners error'd, were done.
}
s.cancel() // Cancel context regardless of how we exited.
return false
}
func (s *ConcurrentScanner) Bytes() []byte {
return s.data
}
func (s *ConcurrentScanner) Text() string {
return string(s.data)
}
func (s *ConcurrentScanner) Err() error {
return s.err
}
Here's an example of it working in the Go Playground: https://play.golang.org/p/EUB0K2V7iT
You can see that the concurrent scanner output is interleaved. Rather than reading all of one reader, then moving on to the next, as is seen with io.MultiReader.

Resources