这篇文章在medium上很火,作者以实际案例来分析,讲得很好。
我们经常听说使用Go的goroutine和channel很容易实现高并发,那是不是全部代码都放在goroutine中运行就可以实现高并发程序了呢?很显然并不是。这篇文章将教大家如何一步一步写出一个简单的, 高并发的Go程序。
正文
我在几家不同的公司从事反垃圾邮件,防病毒和反恶意软件的工作超过15年,现在我知道这些系统最终会因为我们要每天处理大量数据而变得越来越复杂。
目前,我是smsjunk.com的CEO和 KnowBe4的首席架构师,他们都是网络安全行业的公司。
有趣的是,在过去的10年里,作为一名软件工程师,我参与过的所有Web后端开发大部分都是使用RubyonRails完成的。不要误会我的意思,我喜欢 RubyonRails,我相信这是一个了不起的生态,但是过了一段时间,你开始以 Ruby的方式思考和设计系统,忘了如何高效和原本可以利用多线程、并行、快速执行和小的内存消耗来简化软件架构。多年来,我是一名C/C++,Delphi和 C#开发人员,而且我刚开始意识到如何正确的使用工具进行工作可能会有多复杂。
我对互联网中那些语言和框架战争并不太感兴趣,比如哪门语言更好,哪个框架更快。 我始终相信效率,生产力和代码可维护性主要取决于如何简单的构建解决方案。
问题
在处理我们的匿名监测和分析系统时,我们的目标是能够处理来自数百万端点的大量POST请求。Web处理程序将收到一个JSON文档,该文档可能包含需要写入 AmazonS3的多个有效内容的集合,以便我们的 map-reduce系统稍后对这些数据进行操作。
传统上,我们会考虑创建一个工作层架构,利用诸如以下的技术栈:
- Sidekiq
- Resque
- DelayedJob
- ElasticbeanstalkWorkerTier
- RabbitMQ
- ...
并搭建2个不同的集群,一个用于web前端,一个用于worker,因此我们可以随意扩容机器来处理即将到来的请求。
从一开始,我们的团队就知道我们可以在Go中这样做,因为在讨论阶段我们看到这可能是一个非常大流量的系统。我一直在使用Go,大约快2年时间了,而且我们也使用Go开发了一些系统,但是没有一个系统的流量能够达到这个数量级。我们首先创建了几个struct来定义我们通过POST调用接收到的Web请求,并将其上传到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{})
- }
Naive的做法-硬核使用Goroutine
最初,我们对POST处理程序进行了非常简单粗暴的实现,将每个请求直接放到新的goroutine中运行:
- 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)
- }
对于一般的并发量,这其实是可行的,但这很快就证明不能适用于高并发场景。我们可能有更多的请求,当我们将第一个版本部署到生产环境时,我们开始看到的数量级并不是如此,我们低估了并发量。
上述的方法有几个问题。没有办法控制正在工作的goroutine的数量。而且,由于我们每分钟有100万个POST请求,所以系统很快就崩溃了。
重来
(编辑:ASP站长网)
|