【问题标题】:Async work after response响应后的异步工作
【发布时间】:2018-10-17 01:29:39
【问题描述】:

我正在尝试实现 http 服务器:

  • 使用一些逻辑计算更远的重定向
  • 重定向用户
  • 记录用户数据

目标是实现最大吞吐量(至少 15k rps)。为了做到这一点,我想异步保存日志。我使用 kafka 作为日志系统,并将代码的日志块分离到单独的 goroutine 中。当前实现的总体示例:

package main

import (
    "github.com/confluentinc/confluent-kafka-go/kafka"
    "net/http"
    "time"
    "encoding/json"
)

type log struct {
    RuntimeParam  string `json:"runtime_param"`
    AsyncParam    string `json:"async_param"`
    RemoteAddress string `json:"remote_address"`
}

var (
    producer, _ = kafka.NewProducer(&kafka.ConfigMap{
        "bootstrap.servers": "localhost:9092,localhost:9093",
        "queue.buffering.max.ms": 1 * 1000,
        "go.delivery.reports": false,
        "client.id": 1,
    })
    topicName = "log"
)

func main() {
    siteMux := http.NewServeMux()
    siteMux.HandleFunc("/", httpHandler)
    srv := &http.Server{
        Addr: ":8080",
        Handler: siteMux,
        ReadTimeout:  2 * time.Second,
        WriteTimeout: 5 * time.Second,
        IdleTimeout:  10 * time.Second,
    }
    if err := srv.ListenAndServe(); err != nil {
        panic(err)
    }
}

func httpHandler(w http.ResponseWriter, r *http.Request) {
    handlerLog := new(log)
    handlerLog.RuntimeParam = "runtimeDataString"
    http.Redirect(w, r, "http://google.com", 301)
    go func(goroutineLog *log, request *http.Request) {
        goroutineLog.AsyncParam = "asyncDataString"
        goroutineLog.RemoteAddress = r.RemoteAddr
        jsonLog, err := json.Marshal(goroutineLog)
        if err == nil {
            producer.ProduceChannel() <- &kafka.Message{
                TopicPartition: kafka.TopicPartition{Topic: &topicName, Partition: kafka.PartitionAny},
                Value:          jsonLog,
            }
        }
    }(handlerLog, r)
}

问题是:

  1. 使用单独的 goroutine 来实现异步日志记录是否正确/有效,或者我应该使用不同的方法? (例如工人和渠道)
  2. 也许有一种方法可以进一步提高服务器的性能,但我错过了?

【问题讨论】:

  • 使用 goroutine 记录并没有本质上的错误——这实际上是对 goroutine 的一种很好的使用。你只需要小心数据竞争——确保你的logr 变量不会同时更新。从您显示的代码中无法判断这里是否存在问题。您应该使用竞赛检测器运行测试以帮助发现此类问题。
  • 不过,在进行设计时请考虑this
  • 感谢您的回复。这两个变量都只在它们的 gouroutines 中使用(从r 读取,写入log)并且它们没有外部变化。如果我理解正确 - 不应该有比赛条件? -race 标志显示没有错误。
  • 好吧,r 绝对不仅仅在那个 goroutine 中使用。该变量是从 http 服务器传入的。但除非您使用中间件或其他处理程序来延长该请求并对其进行修改,否则它应该是安全的。
  • -race 显示没有错误是好的。但这并不是万无一失的。并不是所有的比赛都会在测试条件下触发,尤其是取决于你的测试是如何编写的。将-race 视为至关重要的第一步,但不是最终决定。

标签: http asynchronous go apache-kafka goroutine


【解决方案1】:
  1. 是的,这是对 goroutine 的正确和高效使用(正如 cmets 中的 Flimzy 所指出的那样)。我完全同意,这是一个好方法。

问题是处理程序可能在 goroutine 开始处理所有内容之前完成执行,并且请求(它是一个指针)可能已经消失,或者您可能会在中间件堆栈中出现一些竞争。我读了你的 cmets,这不是你的情况,但一般来说,你不应该将请求传递给 goroutine。正如我从您的代码中看到的那样,您实际上只使用了请求中的RemoteAddr,为什么不立即重定向并将日志记录放在 defer 语句中?所以,我会稍微重写你的处理程序:

func httpHandler(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r, "http://google.com", 301)
    defer func(runtimeDataString, RemoteAddr string) {
            handlerLog := new(log)
            handlerLog.RuntimeParam = runtimeDataString
            handlerLog.AsyncParam = "asyncDataString"
            handlerLog.RemoteAddress = RemoteAddr
            jsonLog, err := json.Marshal(handlerLog)
            if err == nil {
                producer.ProduceChannel() <- &kafka.Message{
                    TopicPartition: kafka.TopicPartition{Topic: &topicName, Partition: kafka.PartitionAny},
                    Value:          jsonLog,
                }
            }
        }("runtimeDataString", r.RemoteAddr)
}  
  1. goroutines 不太可能提高服务器的性能,因为您只是提前发送响应,而这些 kafka 连接可能会在后台堆积并减慢整个服务器的速度。如果您发现这是瓶颈,您可以考虑在本地保存日志并将它们发送到服务器之外的另一个进程(或工作人员池)中的 kafka。这可能会随着时间的推移分散工作负载(例如,当您有更多请求时发送更少的日志,反之亦然)。

【讨论】:

  • 关于指针的有效点,谢谢。但是,我使用 yandex-tank 进行了一些负载测试,具有两种方法 - 使用和不使用 goroutine 进行异步日志记录。令我惊讶的是,“使用 goroutine”的变化并没有带来延迟优势。两种变体几乎相同,只是“with goroutine”产生了更多的 goroutine。指标是使用普罗米修斯完成的。我知道有很多因素,但是可能是因为golang的原生异步,所以任何阻塞操作都将控制权返回给调度程序,并且不需要单独的goroutine?
  • 我认为,这是http重定向的本质,服务器立即发送标头,没有延迟。事实上,我编辑了我的版本以使重定向成为第一件事,也许它可能会减少一些毫秒。
  • 经过测试的代码在记录后称为重定向。对您的评论的进一步调查表明,重定向呼叫的位置不会影响延迟。我还尝试在重定向后添加睡眠,并在设置超时后从服务器获取响应。我做错了吗?
  • 我明白你的意思,确实是直到返回才发送标题。我将其更改为延迟,似乎重定向现在立即发生。
  • 直接使用你的代码永远不会执行 defer 部分,因为 return 已经发生了。即使 return 放置在 - defer 之后仍然会在函数返回之前执行,如果我理解正确的话。一些即兴的test
猜你喜欢
  • 2016-04-13
  • 1970-01-01
  • 1970-01-01
  • 2016-08-18
  • 1970-01-01
  • 1970-01-01
  • 2020-08-07
  • 2020-07-19
  • 1970-01-01
相关资源
最近更新 更多