首先给出 原文链接.
网上有很多该文章的翻译版本,但是笔者认为机翻痕迹严重,并不利于新手阅读理解
笔者觉得此文写的通俗易懂,言简意赅,于是打算翻译出来供go服务器新手学习参考,好了废话不说,开始正文
在Malwarebytes的工作让我经历的惊人的成长,一年前,在我加入这家公司之前,我在硅谷的主要工作内容就是定制解决方案,以应对快速增长的公司每天数百万的使用频率。我已经在不同的公司从事反病毒和反恶意软件行业工作了12年,我知道这些系统的复杂程度取主要决于我们每天要处理多大量数据。
有趣的是,在过去的9年当中,我从事的所有网站开发都是在Ruby on Rails的方案下实施的。不要误会我的意思,我非常热爱 Ruby on Rails,我认为这是一个令人惊奇的开发方案。但是在一段时间以后,当你开始用Ruby on Rails的思路去思考和设计系统,你就会忘记那些可以利用的多线程、并行,快速执行和小的内存开销的解决方案,而这些将会使你开发的软件高效和简洁。多年来,我一直是一个C / C++、Delphi和C #开发者,我开始意识到,当你使用适合的工具时,事情会是多么的简洁。
作为一个架构师,我不是很在意那些网站之间的关于语言和框架之间孰优孰劣的纷争。我相信,效率,生产力和代码的可维护性主要依赖于如何简单的构建你的解决方案。
一个难题
当我们在进行一个匿名的测试和分析系统时,我们的目标是能够处理来自数百万端点的大量POST请求。web处理程序将接收一个包含众多数据集合的JSON文档,它们将被写入Amazon S3数据库,以供我们的大数据系统进行后续操作
面对这样的需求,通常我们会考虑诸如:
Sidekiq
Resque
DelayedJob
Elasticbeanstalk Worker Tier
RabbitMQ
等等框架和方案...
并且,我们会设置2个不同的集群,一个用于Web前端,另一个用于后台服务,这样我们就可以通过增加减少后台服务器的数量来控制我们能够处理的请求数。但是,从一开始我们的团队就决定采用Go语言作为开发方案,因为经过讨论,我们发现这将会是一个非常庞大的系统。我们已经从事Go语言开发两年,在工作中设计架构了一些系统,但是还没有任何一个系统有如此庞大的数据量。
我们首先创建了一些结构体来定义POST的request中内容,以及一个上传到S3库的方法。
小试牛刀
最初我们采取了一个非常简单的POST请求处理实现方案,尝试简单并发goroutine去处理任务
对于中等的负载量,这个方案可以满足大多数人的需求。但是当数据量增大的时候,它开始显得不那么好用了。在第一版投入生产环节中,我们预估了一下request的数量,事实上我们完全低估了这个庞大的数据量。
上面的方案在有很多弊端,首先我们无法控制开启的goroutine数量,然后,当请求达到每分钟一百万次的数量级,很快这段代码就崩溃了。
再次尝试
我们需要一个新方案,在一开始的讨论中,我们明确了几点,首先要保证request handler 的生命周期足够短,其次要做到在后台进行异步并发处理。显然如果采用Ruby on Rails的方案,这些都是必要的事情,不然就会阻塞整个网络。那么,我们将不得不采取一些常见的方案来解决这个事情比如使用 Resque, Sidekiq, SQS等等。
所以,第二次迭代的任务就是创建一个用于缓存列队的通道,用来缓存请求,并将它们逐一存入S3服务器。因为我们可以控制队列通道内的最大容量,并且我们有足够大的内存来缓存队列,我们认为这将会一个是极好的方案。
然后,为了将任务列队并依次处理,我们用了如下面这样的代码
老实的讲,我并不知道我们在想些什么。这将会是一个充满红牛的不眠之夜。这个方案并没有给我们带来任何改进。我们用一个缓存列队代替了有缺陷的并发方案,这仅仅是推迟了问题的产生时间而已。我们的同步处理器每次只能上传一分数据到S3,而队列中传入请求的速度远大于处理器上传数据到S3的数据,很快的我们的队列就达到了极限,从而阻塞了后续的请求添加到队列。我们仅仅简单的去回避问题,这仅仅是开启了一个系统崩溃死亡的倒计时。
更好的方案
当我们使用Go的通道时,我们决定利用一个公共模式来创建一个双层的通道系统。一个用来队列任务,而另一个则是控制当前处理队列任务的线程数量。
这个想法是要采用一个合理的可持续的速率并行的上传数据到S3服务器,既不会阻塞服务器的性能,也不会从S3服务器获取上传失败的错误回调。因此,我们构建一个job/worker模型,这看起来有些像Java, C#,等等,而我们则是考虑通过Golang的方式,利用通道来代替它们,去实现一个处理器线程池。
我们已经修改了我们的网路请求handler来创建一个包含载荷数据的任务实例,我们将它发送到任务队列通道中去,供处理线程们去处理。
当我们的网络服务器在初始化的过程中,我们创建了一个名叫Run()的调度器来创建worker线程池,并且开始监听发送到任务队列中的任务。
下面是我们调度器的代码实现
值得注意的是,我们提供了最大处理线程的并发数量,用来实例化任务处理线程并且将他们添加到我们的任务处理线程池当中。我们使用亚马逊的Elasticbeanstalk服务并且采用了docker化的Go 运行环境,而且我们总是尝试遵循 12要素的方法论(译者注:应该是一种广泛认可的架构设计思路,虽然我没听说过)来配置我们在生产环境中的系统,我们通过环境变量来读取这些值。这样我们就可以控制处理线程数量和任务队列通道的所能承载的最大容量,因此,我们可以快速的改变这些配置而不用重新部署服务器集群
最直观的结果
在我们部署完它之后,我们立即发现所有的延迟率都降到了微不足道的数量,我们处理请求的能力激增。
在经历了几分钟弹性的负载均衡热身之后,我们看到,我们的ElasticBeanstalk应用每分钟接近响应了一百万的请求。
在我们部署了新代码之后,服务器的数量大幅下降,从100台服务器降到20台服务器。
结论
在我的故事中,极简主义总是获胜的一方。我们可以设计一个复杂的系统,具有许多队列,异步的后台处理,复杂的调度,但是我们决定利用Elasticbeanstalk的自动缩放的高效的简洁的能力去实现Golang提供给我们的并发效果。
你并不是每天需要部署一个4服务器的集群,来给亚马逊的S3服务器每分钟写入100万次。
总会有一个合适的工具来解决问题,有时,当你的Ruby Rails系统需要一个功能强大的Web处理程序时,可以稍微考虑一下Ruby生态系统之外的更简单、更强大的替代解决方案。