Golang HTTP请求工作池

时间:2017-10-18 22:53:19

标签: multithreading api go goroutine

我正在尝试构建一个系统,工作池/作业队列,以便在每个API端点上处理尽可能多的http requests。我调查了这个example,并且工作得很好,除了我偶然发现了我不明白如何将pool / jobqueue扩展到不同端点的问题。

为了方案起见,让我们草绘一个Golang http服务器,该服务器在不同的端点和请求类型GET& POST ETC。

我如何扩展这个概念?我应该为每个端点创建不同的工作池和作业。或者我可以创建不同的作业并将它们输入到同一队列中并使用相同的池处理这些作业吗?

我想保持简洁,如果我创建一个新的API端点,我不必创建新的工作池,所以我可以专注于api。但是表现也非常考虑。

我尝试构建的代码取自之前链接的示例,here是github' gist'这个代码的其他人。

3 个答案:

答案 0 :(得分:2)

预先做好准备:如果您正在运行HTTP服务器(无论如何都是Go的标准服务器),您无法在不停止和重新启动服务器的情况下控制goroutine的数量。每个请求至少开始一个goroutine,你无能为力。好消息是,这通常不是问题,因为goroutines非常轻巧。但是,你想要控制正在努力工作的goroutine的数量是完全合理的。

您可以将任何值添加到频道中,包括功能。因此,如果目标是只需要在http处理程序中编写代码,那就让工作关闭 - 工作人员不知道(或关心)他们正在做什么。

package main

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
)

var largePool chan func()
var smallPool chan func()

func main() {
    // Start two different sized worker pools (e.g., for different workloads).
    // Cancelation and graceful shutdown omited for brevity.

    largePool = make(chan func(), 100)
    smallPool = make(chan func(), 10)

    for i := 0; i < 100; i++ {
            go func() {
                    for f := range largePool {
                            f()
                    }
            }()
    }

    for i := 0; i < 10; i++ {
            go func() {
                    for f := range smallPool {
                            f()
                    }
            }()
    }

    http.HandleFunc("/endpoint-1", handler1)
    http.HandleFunc("/endpoint-2", handler2) // naming things is hard, okay?

    http.ListenAndServe(":8080", nil)
}

func handler1(w http.ResponseWriter, r *http.Request) {
    // Imagine a JSON body containing a URL that we are expected to fetch.
    // Light work that doesn't consume many of *our* resources and can be done
    // in bulk, so we put in in the large pool.
    var job struct{ URL string }

    if err := json.NewDecoder(r.Body).Decode(&job); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
    }

    go func() {
            largePool <- func() {
                    http.Get(job.URL)
                    // Do something with the response
            }
    }()

    w.WriteHeader(http.StatusAccepted)
}

func handler2(w http.ResponseWriter, r *http.Request) {
    // The request body is an image that we want to do some fancy processing
    // on. That's hard work; we don't want to do too many of them at once, so
    // so we put those jobs in the small pool.

    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
    }

    go func() {
            smallPool <- func() {
                    processImage(b)
            }
    }()
    w.WriteHeader(http.StatusAccepted)
}

func processImage(b []byte) {}

这是一个非常简单的例子来说明问题。设置工作池的方式并不重要。你只需要一个聪明的工作定义。在上面的示例中,它是一个闭包,但您也可以定义一个Job接口,例如。

type Job interface {
    Do()
}

var largePool chan Job
var smallPool chan Job

现在,我不打电话给整个工人池方法&#34;简单&#34;。你说你的目标是限制goroutines(正在做的工作)的数量。这根本不需要工人;它只需要一个限制器。这是与上面相同的示例,但使用通道作为信号量来限制并发。

package main

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
)

var largePool chan struct{}
var smallPool chan struct{}

func main() {
    largePool = make(chan struct{}, 100)
    smallPool = make(chan struct{}, 10)

    http.HandleFunc("/endpoint-1", handler1)
    http.HandleFunc("/endpoint-2", handler2)

    http.ListenAndServe(":8080", nil)
}

func handler1(w http.ResponseWriter, r *http.Request) {
    var job struct{ URL string }

    if err := json.NewDecoder(r.Body).Decode(&job); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
    }

    go func() {
            // Block until there are fewer than cap(largePool) light-work
            // goroutines running.
            largePool <- struct{}{}
            defer func() { <-largePool }() // Let everyone that we are done

            http.Get(job.URL)
    }()

    w.WriteHeader(http.StatusAccepted)
}

func handler2(w http.ResponseWriter, r *http.Request) {
    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
    }

    go func() {
            // Block until there are fewer than cap(smallPool) hard-work
            // goroutines running.
            smallPool <- struct{}{}
            defer func() { <-smallPool }() // Let everyone that we are done

            processImage(b)
    }()

    w.WriteHeader(http.StatusAccepted)
}

func processImage(b []byte) {}

答案 1 :(得分:0)

目前尚不清楚为什么你需要工作人员池?不是goroutines就足够了吗?

如果您受资源限制,可以考虑实施rates limiting。如果不是为什么根本不根据需要跨越惯例?

最好的学习方法是研究别人如何做好事。

查看https://github.com/valyala/fasthttp

  

Go的快速HTTP包。调整为高性能。热路径中的零内存分配。比notifyItemChanged(position)快10倍。

他们声称:

  

从每个物理服务器超过1.5M并发保持活动连接提供高达200K rps

这令人印象深刻,我怀疑你可以用net/http做得更好。

答案 2 :(得分:0)

如前所述,在您的服务器中,每个请求处理程序将至少在一个goroutine中运行。

但是如果需要,您仍然可以使用工作池来执行后端并行任务。例如,假设您的某些Http Handler函数触发对其他外部api的调用并将其结果“聚合”在一起,因此在这种情况下调用顺序无关紧要,这是一种可以利用工作池并分发您的工作是为了让它们并行运行,将每个任务分配给一个工人goroutine:

示例代码段:

df1$CEMETERY <- sub("(.*(M|m)edieval).*", "\\1", df1$CEMETERY)
df1$CEMETERY
#[1] "Medieval"      "Medieval"      "Medieval"      "Medieval"     
#[5] "Medieval"      "Medieval"      "Medieval"      "Medieval"     
#[9] "Medieval"      "Medieval"      "Post-Medieval" "Post-Medieval"
#[13] "Post-Medieval" "Post-Medieval" "Post-Medieval"

。工作池异步运行“通用”任务的完整示例:https://github.com/guilhebl/go-offer/blob/master/offer/repo.go

。使用的工作池库:https://github.com/guilhebl/go-worker-pool