[译]使用golang每分钟处理百万请求

在Malwarebytes,我们正在经历惊人的增长,自从我在1年前加入硅谷的这家公司以来,我的主要职责是为多个系统做架构和开发,为这家安全公司的快速发展以及百万日活产品所必需的基础设施提供支持。我曾在一些不同的公司从事反病毒和反恶意软件行业超过12年,我知道这些系统最终会因为我们每天处理的大量数据而变得十分复杂。

有趣的是,在过去9年左右的时间里,我所参与的所有Web后端的开发工作大部分都是在Rails框架的基础上用ruby实现的。不要误解我的意思,虽然我很喜欢Rails框架和Ruby,我相信这是一个会让你感到惊叹的环境,但一段时间之后,你就会用ruby的方式来进行思考和设计系统,却忘记了本来可以利用多线程,并行化,快速执行和小内存开销来使你的软件架构变得如此高效和简单。作为一个多年的C / C ++,Delphi和C#开发人员,我也同样开始意识到了如何在工作中使用正确的工具来降低事情的复杂度。

作为首席架构师,我不太重视对互联网所进行的语言和框架之争。我相信软件的效率(efficiency),生产力(productivity)和代码可维护性主要取决于你构建解决方案的简单程度。

问题

在构建我们的匿名检测和分析系统时,我们的目标是能够处理来自数百万个端点的的大量POST请求。 Web处理程序将接收一个JSON文档,该文档可能包含了需要写入Amazon S3(注:这个是亚马逊的云计算服务平台)的许多负载(payload)的集合,以便我们的map-reduce系统稍后对这些数据进行处理。

从传统上来说,我们会考虑利用以下工具(基本都是开源的)创建一个工作层架构:

然后创建2个不同的集群,一个用于Web前端,另一个用于后台工作的处理,以扩展可以处理的后台工作的数量。

但是从一开始,我们的团队就知道我们应该使用go语言进行开发,因为在讨论阶段我们就意识到了这可能是一个吞吐量非常大的系统。我使用go大概有2年左右的时间,我们也开发了一些系统,但是没有一个系统有如此大的吞吐量。

我们开始创建了一些结构体,用于定义通过POST调用获取的网络请求负载(payload),同时定义了一个方法将这些负载上传到S3。

type PayloadCollection struct {
WindowsVersion string `json:"version"`
Token string `json:"token"`
Payloads []Payload `json:"data"`
} type Payload struct {
// [redacted]
} func (p *Payload) UploadToS3() error {
// the storageFolder method ensures that there are no name collision in
// case we get same timestamp in the key name
storage_path := fmt.Sprintf("%v/%v", p.storageFolder, time.Now().UnixNano()) bucket := S3Bucket b := new(bytes.Buffer)
encodeErr := json.NewEncoder(b).Encode(payload)
if encodeErr != nil {
return encodeErr
} // Everything we post to the S3 bucket should be marked 'private'
var acl = s3.Private
var contentType = "application/octet-stream" return bucket.PutReader(storage_path, b, int64(b.Len()), contentType, acl, s3.Options{})
}

从原生方法到Go协程

一开始我们用非常原生的方法来实现POST句柄,尝试通过使用一个简单的协程来将作业的处理并行化:

func payloadHandler(w http.ResponseWriter, r *http.Request) {

    if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
} // Read the body into a string for json decoding
var content = &PayloadCollection{}
err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&content)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusBadRequest)
return
} // Go through each payload and queue items individually to be posted to S3
for _, payload := range content.Payloads {
go payload.UploadToS3() // <----- DON'T DO THIS
} w.WriteHeader(http.StatusOK)
}

对于适当的载荷,上面的方法在大多数情况下能够工作,但是在大规模载荷的情况下,上面的方法很快被证明不能够发挥很好的作用。我们想像会有很多的请求,但是当部署第一个版本到生产环境时,没有想到会有如此的量级。我们完全低估了载荷的数量。

上述方法在几个方面都很糟糕。它没有办法控制产生的go协程数量。既然我们每分钟会收到百万请求,这段代码会很快崩溃。

再试一次

我们需要寻找别的方法。开始我们就讨论了我们需要保持请求句柄的生命周期的短暂性以及请求的处理要在后台进行。当然,这是在Rails的世界中用Ruby必须要做到的,否则会阻塞所有能用的web工作处理器,不论你是使用puma,unicorn或者passenger。然后我们需要利用常见的解决方案来做到这一点,例如Resque,Sidekiq,SQS等。当然还有很多其它方法也能做到这一点,

所以第二次迭代中我们会创建一个缓冲通道(buffered channel),作业可以插入到缓冲通道中并将作业的负载上传到S3,因为我们可以控制缓冲通道中元素的数量,并且我们有大量的内存将作业插入缓冲通道,我们认为这种方法是没有任何问题的。

var Queue chan Payload

func init() {
Queue = make(chan Payload, MAX_QUEUE)
} func payloadHandler(w http.ResponseWriter, r *http.Request) {
...
// Go through each payload and queue items individually to be posted to S3
for _, payload := range content.Payloads {
Queue <- payload
}
...
}

然后从缓冲通道中取出作业进行处理,像下面这样:

func StartProcessor() {
for {
select {
case job := <-Queue:
job.payload.UploadToS3() // <-- STILL NOT GOOD
}
}
}

说实话,我不知道我们在想什么。这个夜晚注定是用红牛度过的。这种方法并没有给我们带来任何好处,我们用缓冲队列代替了有缺陷的并发,但这只是推迟了问题。我们的同步处理器一次只向S3上传一个有效载荷(payload),由于传入请求的速率远远大于单个处理器上传到S3的能力,我们的缓冲通道很快就达到了极限并阻止了请求句柄可以添加更多作业的能力。

我们只是避免了这个问题,系统的死期最终也进入了倒计时。在我们部署这个有缺陷的版本几分钟后,延迟率会以固定的速率增加。

更好的解决方案

为了创建一个2层的channel系统,我们决定使用一个通用模式,一个用来插入作业,一个用来控制作业队列上同时运行的工作协程。

我们的想法是将并行上传稳定在一个可持续的速率,这不会削弱机器的性能,也不会产生到S3的连接错误。所以我们选择创建了一个Job / Worker模式。对于熟悉Java,C#等的人来说,想像一下如何以golang的方式用channel来实现一个worker线程池。

var (
MaxWorker = os.Getenv("MAX_WORKERS")
MaxQueue = os.Getenv("MAX_QUEUE")
) // Job represents the job to be run
type Job struct {
Payload Payload
} // A buffered channel that we can send work requests on.
var JobQueue chan Job // Worker represents the worker that executes the job
type Worker struct {
WorkerPool chan chan Job
JobChannel chan Job
quit chan bool
} func NewWorker(workerPool chan chan Job) Worker {
return Worker{
WorkerPool: workerPool,
JobChannel: make(chan Job),
quit: make(chan bool)}
} // Start method starts the run loop for the worker, listening for a quit channel in
// case we need to stop it
func (w Worker) Start() {
go func() {
for {
// register the current worker into the worker queue.
w.WorkerPool <- w.JobChannel select {
case job := <-w.JobChannel:
// we have received a work request.
if err := job.Payload.UploadToS3(); err != nil {
log.Errorf("Error uploading to S3: %s", err.Error())
} case <-w.quit:
// we have received a signal to stop
return
}
}
}()
} // Stop signals the worker to stop listening for work requests.
func (w Worker) Stop() {
go func() {
w.quit <- true
}()
}

我们修改了Web请求句柄,创建一个带有负载的Job结构体实例,并将其发送到JobQueue channel中以供workers获取。

func payloadHandler(w http.ResponseWriter, r *http.Request) {

    if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
} // Read the body into a string for json decoding
var content = &PayloadCollection{}
err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(&content)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusBadRequest)
return
} // Go through each payload and queue items individually to be posted to S3
for _, payload := range content.Payloads { // let's create a job with the payload
work := Job{Payload: payload} // Push the work onto the queue.
JobQueue <- work
} w.WriteHeader(http.StatusOK)
}

在我们的Web服务器初始化期间,我们创建了一个Dispatcher并调用Run()来创建works pool并开始侦听即将出现在JobQueue中的作业。

dispatcher := NewDispatcher(MaxWorker)
dispatcher.Run()

下面是dispatcher 的实现代码:

 type Dispatcher struct {
// A pool of workers channels that are registered with the dispatcher
WorkerPool chan chan Job
} func NewDispatcher(maxWorkers int) *Dispatcher {
pool := make(chan chan Job, maxWorkers)
return &Dispatcher{WorkerPool: pool}
} func (d *Dispatcher) Run() {
// starting n number of workers
for i := 0; i < d.maxWorkers; i++ {
worker := NewWorker(d.pool)
worker.Start()
} go d.dispatch()
} func (d *Dispatcher) dispatch() {
for {
select {
case job := <-JobQueue:
// a job request has been received
go func(job Job) {
// try to obtain a worker job channel that is available.
// this will block until a worker is idle
jobChannel := <-d.WorkerPool // dispatch the job to the worker job channel
jobChannel <- job
}(job)
}
}
}

注意,我们提供了添加到works pool 中的works 最大数量。既然我们在工程中使用了带有容器化Go环境的Amazon Elasticbeanstalk,我们就会尝试一直遵循12-factor方法论来在生产环境中配置我们的系统。因此我们会从环境变量中读取这些值。这样我们就可以控制JobQueue的work数量,调整这些值后可以快速生效而无需重新部署集群。

var (
MaxWorker = os.Getenv("MAX_WORKERS")
MaxQueue = os.Getenv("MAX_QUEUE")
)

实时结果

在我们部署新代码之后,我们立即看到延迟率下降到了很小的数值,并且处理请求的能力急剧上升。

在我们的Elastic负载均衡完全预热几分钟之后,我们看到我们的ElasticBeanstalk应用程序每分钟处理了近100万个请求。并且在早上的几个小时,请求流量飙升到了每分百万之上。

服务器的使用数量从100台下降到了大概20台。

在我们正确配置了群集和自动扩展功能之后,实例数量降到了4x EC2 c4.Large(没看懂,大概是这个意思),自动缩放配置好之后,只有CPU使用率超过90%并且维持5分钟,才会产生一个新的实例。

结论

我信奉简单致胜。我们原本设计了一个使用大量队列和后台wokers并且部署复杂的系统,但我们决定使用Elasticbeanstalk的自动扩展能力以及Golang为我们提供开箱即用的高效和简单的并发方法。

你总能为你的工作找到正确的工具。有时候在你的ruby系统中需要一个强大的web处理器,请考虑一下Ruby之外的生态系统,你可以获得更简单但更强大的替代解决方案。

原文链接

[译]使用golang每分钟处理百万请求的更多相关文章

  1. 我读《通过Go来处理每分钟达百万的数据请求》

    我读<通过Go来处理每分钟达百万的数据请求> 原文 原文作者为Malwarebytes公司的首席架构师Marcio Castilho http://marcio.io/2015/07/ha ...

  2. 每秒处理3百万请求的Web集群搭建-如何生成每秒百万级别的 HTTP 请求?

    本文是构建能够每秒处理 3 百万请求的高性能 Web 集群系列文章的第一篇.它记录了我使用负载生成器工具的一些经历,希望它能帮助每一个像我一样不得不使用这些工具的人节省时间. 负载生成器是一些生成用于 ...

  3. golang常用的http请求操作

    之前用python写各种网络请求的时候写的非常顺手,但是当打算用golang写的时候才发现相对来说还是python的那种方式用的更加顺手,习惯golang的用法之后也就差别不大了,下面主要整理了常用的 ...

  4. golang中发送http请求的几种常见情况

    整理一下golang中各种http的发送方式 方式一 使用http.Newrequest 先生成http.client -> 再生成 http.request -> 之后提交请求:clie ...

  5. 每秒处理3百万请求的Web集群搭建-用 LVS 搭建一个负载均衡集群

    这篇文章是<打造3百万次请求/秒的高性能服务器集群>系列的第3部分,有关于性能测试工具以及优化WEB服务器部分的内容请参看以前的文章. 本文基于你已经优化好服务器以及网络协议栈的基础之上, ...

  6. 每秒处理3百万请求的Web集群搭建-为最佳性能调优 Nginx

    这篇文章是<打造3百万次请求/秒的高性能服务器集群>系列的第2部分,在这个部分中你可以使用任何一种 WEB 服务器,不过我决定使用 Nginx,因其轻量级.高可靠及高性能的优点. 通常来说 ...

  7. golang几种post请求方式

    get请求 get请求可以直接http.Get方法,非常简单. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func httpGet() {     resp, err := h ...

  8. golang微信公众号请求获取信息

    初次用golang在公众号中获取信息,记录一下 看了下文档,粗略的写了个demo,如下: func HttpGet(c*gin.Context) { var param GetType if er:= ...

  9. golang接收get/post请求并返回json数据

    // @router /d2 [post] func (c *MainController) D2() { // jsoninfo := c.GetString("ok") // ...

随机推荐

  1. MySQL性能分析之Explain

    目录 Explain基础 Explain进阶 Explain基础 关于explain命令相信大家并不陌生,具体用法和字段含义可以参考官网explain-output ,这里需要强调rows是核心指标, ...

  2. 常用的方法论-5W2H

  3. 走进python

    python史 1.python之父 Guido van Rossum 2.python的优缺点 优点:开发效率高,可跨平台,可嵌入,可扩展,优雅简洁 缺点:运行稍慢,代码不能加密,不能实现真正的多线 ...

  4. Nginx运行报错unknown directive ""

    使用文本编辑器把编码格式修改为UTF-8即可. 推荐文本编辑器:notepad++,自行百度搜索即可下载

  5. 使用GDAL实现DEM的地貌晕渲图(一)

    目录 1. 原理 1) 点法向量 2) 日照方向 (1) 太阳高度角和太阳方位角 (2) 计算过程 3) 晕渲强度 2. 实现 3. 参考 @ 1. 原理 以前一直以为对DEM的渲染就是简单的根据DE ...

  6. Java底层技术系列文章-hashcode深入理解

    带着问题去理解: 1. Object类HashCode方法是如何实现的,和String类有什么区别? 2.HashCode和Equals之间的关系? 一.hashCode作用 hashCode方法返回 ...

  7. 第九章 webase 分布式中间件平台快速部署

    鉴于笔者以前各大博客教程都有很多人提问,早期建立一个技术交流群,里面技术体系可能比较杂,想了解相关区块链开发,技术提问,请加QQ群:538327407 参考资料:https://webasedoc.r ...

  8. web安全测试必须注意的五个方面

    随着互联网的飞速发展,web应用在软件开发中所扮演的角色变得越来越重要,同时,web应用遭受着格外多的安全攻击,其原因在于,现在的网站以及在网站上运行的应用在某种意义上来说,它是所有公司或者组织的虚拟 ...

  9. 【素数的判定-从暴力到高效】-C++

    今天我们来谈一谈素数的判定. 对于每一个OIer来说,在漫长的练习过程中,素数不可能不在我们的眼中出现,那么判定素数也是每一个OIer应该掌握的操作,那么我们今天来分享几种从暴力到高效的判定方法. 1 ...

  10. 个人永久性免费-Excel催化剂功能第82波-复制粘贴按源区域大小自动扩展收缩目标区域

    日常工作中,复制粘贴的操作,永远是最高频的操作,没有之一,在最高频的操作上,进行优化,让过程更智能,比一天到晚鼓吹人工智能替换人的骇人听闻的新闻来得更实际.此篇带来一点点的小小的改进,让日后无数的复制 ...