使用Golang构建一万+每秒处理请求的高性能系统


使用Golang构建一万+每秒处理请求的高性能系统

文章插图
背景一谈到golang,大家的第一感觉就是高并发,高性能 。但是语言本身的优势是不是,就让程序员觉得编写高性能的处理系统变得轻而易举,水到渠成呢 。下面这篇文章给大家的提醒便是,我们只有在充分理解语言本身的特性,并巧妙加以利用的前提下,才能写出高性能、高并发的处理程序,才能为企业节省成本,为客户提供好的服务 。
每分钟处理百万请求?Malwarebytes的首席架构师Marcio Castilho分享了他在公司高速发展过程中,开发高性能数据处理系统的经历 。整个过程向我们详细展示了如何不断的优化与提升系统性能的过程,值得我们思考与学习 。大佬也不是一下子就给出最优方案的 。
首先作者的目标是能够处理来自数百万个端点的大量POST请求,然后将接收到的JSON 请求体,写入Amazon S3,以便map-reduce稍后对这些数据进行操作 。这个场景和我们现在的很多互联网系统的场景是一样的 。传统的处理方式是,使用队列等中间件,做缓冲,消峰,然后后端一堆worker来异步处理 。因为作者也做了两年GO开发了,经过讨论他们决定使用GO来完成这项工作 。
第一版代码下面是Marcio给出的本能第一反应的解决方案,和大家的思路是不是一致的 。首先他给出了负载(Payload)还有负载集合(PayloadCollection)的定义,然后他写了一个处理web请求的Handler(payloadHandler) 。在payloadHandler里面,由于把负载上传S3比较耗时,所以针对每个负载,启动GO的协程来异步上传 。具体的实现,大家可以看下面48-50行贴出的代码 。
type PayloadCollection struct {windowsVersionstring`json:"version"`Tokenstring`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 namestorage_path := fmt.Sprintf("%v/%v", p.storageFolder, time.Now().UnixNano())bucket := S3Bucketb := 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.Privatevar contentType = "Application/octet-stream"return bucket.PutReader(storage_path, b, int64(b.Len()), contentType, acl, s3.Options{})}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 decodingvar 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 S3for _, payload := range content.Payloads {go payload.UploadToS3()// <----- DON'T DO THIS}w.WriteHeader(http.StatusOK)}那结果怎么样呢?Marcio和他的同事们低估了请求的量级,而且上面的实现方法,又无法控制GO协程的生成数量,这个版本部署到生产后,很快就崩溃了 。Marcio毕竟是牛逼架构师,他很快根据问题给出了新的解决方案 。
第二版代码第一个版本的假设是,请求的生命周期都是很短的,不会有长时间的阻塞操作耗费资源 。在这个前提下,我们可以根据请求不停的生成GO协程来处理请求 。但是事实并非如此,Marcio转变思路,引入队列的思想 。创建了Buffered Channel,把请求缓冲起来,然后再通过一个同步处理器从Channel里面把请求取出,上传S3.这是典型的生产者-消费者模型 。
使用Golang构建一万+每秒处理请求的高性能系统

文章插图
处理流程
这个版本的问题是,首先同步处理器的处理能力有限,他的处理能力比不上请求到达的速度 。很快Buffered Channel就会满了,然后后续的客户请求都会被阻塞 。在Marcio他们部署这个有缺陷的版本几分钟后,延迟率会以固定的速率增加 。
使用Golang构建一万+每秒处理请求的高性能系统

文章插图
系统部署后的延迟
第三版代码Marcio引入了2层Channel,一个Channel用于缓存请求,是一个全局Channel,本文中就是下面的JobQueue,一个Channel用于控制每个请求队列并发多少个worker.从下面的代码可以看到,每个Worker都有两个关键属性,一个是WorkerPool(这个也是一个全局的变量,即所有的worker的这个属性都指向同一个,worker在创建后,会把自身的JobChannel写入WorkerPool完成注册),一个是JobChannel(用于缓存分配需要本worker处理的请求作业) 。web处理请求payloadHandler,会把接收到的请求放到JobQueue后,就结束并返回 。


推荐阅读