当前位置: 首页 > 科技观察

Go语言如何做到每分钟处理100万次请求

时间:2023-03-12 07:59:08 科技观察

摘要:笔者结合自己的工作经验,以一个项目为案例,尝试了多个Go语言程序实例,来讲解Go语言是如何做到每分钟处理100万次请求的。分钟请求,这里是翻译。我在几家不同的公司从事反垃圾邮件、反病毒和反恶意软件工作超过15年,现在我知道这些系统的复杂性可能是由于我们每天处理的数据量巨大。目前,我是smsjunk.com的首席执行官和KnowBe4的首席架构师,这两家公司活跃于网络安全行业。有趣的是,在过去10年左右的时间里,作为一名软件工程师,我所参与的web后端开发绝大部分都是在RubyonRails(Rails是一个用Ruby语言编写的web程序开发框架,目的为开发者开发提供通用组件)。不要误会我的意思,我喜欢RubyonRails,我相信它是一个迷人的开发环境,但过了一段时间,你开始以Ruby的方式思考和设计系统,忘记了如何高效和利用多线程,并行性、快速执行和小内存消耗简化了软件架构。多年来,我一直是C/C++、Delphi和C#开发人员,我刚刚意识到拥有适合工作的工具可以降低事情的复杂性。我不太喜欢网站一直在争夺的语言和框架战争。我相信效率、生产力和代码可维护性主要取决于构建解决方案的难易程度。问题当我们在匿名遥测和分析系统上工作时,我们的目标是能够处理来自数百万端点的大量POST请求。Web处理程序将收到一个JSON文档,其中可能包含需要写入AmazonS3的许多有效负载的集合,以便map-reduce系统稍后处理此数据。传统上,我们会考虑创建第一层工作架构,利用诸如:SidekiqResqueDelayedJobElasticbeanstalkWorkerTierRabbitMQ等...设置2个不同的集群,一个用于Web前端,另一个用于工作人员,将扩展可以处理的后台作业数。但是从一开始,我们的团队就知道应该这样做,因为在讨论阶段,我们就预见到这可能是一个非常高流量的系统。我使用Go语言大约2年左右,我们开发了一些正在使用的系统,但没有一个能承受如此大的负载。首先创建一些结构,定义通过POST调用接收的Web请求负载,以及将请求负载上传到S3存储桶的函数。Go程序的朴素方法最初我们对POST处理采用了一种非常朴素的方法,只是试图将任务并行化为一个简单的goroutine:对于中等负载,这可能适用于大多数人,但很快就证明对于大负载,它不是很好用。我们预计会收到很多请求,但在将第一个版本部署到生产环境时并没有看到如此大规模的请求。我们完全低估了流量。上面的方法在几个方面是不好的,没有办法控制我们正在量产的Go程序会产生多少例程。由于我们每分钟收到100万个POST请求,当然,这段代码很快就会崩溃。要再试一次,我们需要换个角度看。从一开始,我们就讨论了如何使请求处理程序的生命周期非常短,并在后台生成处理。当然,这必须在RubyonRails世界中完成,否则这将限制所有可用的web处理器,无论你使用puma、unicorn、passenger中的哪一个(请不要加入JRuby讨论)。那么我们就需要使用常见的解决方案来做这件事,比如Resque、Sidekiq、SQS等等。这个列表可以继续下去,因为有很多方法可以做到这一点。所以第二个版本是创建一个缓存通道,我们可以在其中排队一些作业并上传到S3,因为我们可以控制队列中的项目数量,在内存中我们有足够的RAM来处理任务对于排队,我们认为它是可以只在通道队列中缓存作业。然后为了实际出列和处理作业,我们使用了一个类似的函数:老实说,我不知道我们在想什么。这一定是一个充满红牛的深夜。这种方法对我们没有任何好处,我们用缓冲队列换取有缺陷的并发性,只会延迟问题的发生。我们的同步处理器一次只能将一个有效载荷上传到S3,并且由于传入请求的速率远远大于单个处理器上传到S3的能力,缓冲通道很快达到其极限,从而限制了请求处理程序的能力排队更多的项目。我们只是回避了这个问题,最终导致系统死亡。在我们部署了这个有问题的版本之后,我们的延迟继续以恒定的速度增加。更好的解决方案在使用Go通道时,我们决定利用通用模式来创建一个2层通道系统,一个用于作业排队,另一个用于控制同时在JobQueue上操作的工人数。这个想法是以某种可持续的速度并行上传到S3,既不会削弱机器性能,也不会从S3开始产生连接错误。所以我们选择创建作业/操作符模式。对于熟悉java、C#等语言的,可以考虑使用Go语言实现channel方法,而不是算子线程池方法。我们修改了web请求处理程序以创建一个带有负载的jobstruct实例并将其发送到JobQueue通道,以便操作员可以将其拾取。在Web服务器初始化期间,我们创建一个Dispatcher,调用Run()来创建一个工作池,并开始侦听出现在JobQueue上的作业。dispatcher:=NewDispatcher(MaxWorker)dispatcher.Run()以下是调度程序执行的代码:请注意,我们将提供实例化并添加到工作池中的最新工作人员数量。由于我们具有dockerizedGo环境的项目使用AmazonElasticbeanstalk,因此我们始终尝试遵循12因素方法在生产中配置系统,从环境变量中读取这些值。这允许控制worker和作业队列的最大数量,因此我们可以快速调整这些值而无需重新部署集群。var(MaxWorker=os.Getenv(“MAX_WORKERS”)MaxQueue=os.Getenv(“MAX_QUEUE”))部署之后,我们发现所有的延迟率都降到了微不足道的数字,系统处理请求的能力急剧下降.弹性负载均衡器完全预热几分钟后,我们看到ElasticBeanstalk应用程序服务接近每分钟100万个请求。通常在早上,流量达到每分钟超过100万个请求的峰值。部署新代码后,服务器数量从100台减少到大约20台。在正确配置集群和自动缩放设置后,我们能够将其减少到仅4xEC2c4。如果CPU连续5分钟超过90%,大型实例和弹性自动缩放设置会生成一个新实例。结论在我的字典里,简单永远是赢家。我们可以设计一个具有多个队列、后台工作者和复杂部署的复杂系统。但是我们决定利用Elasticbeanstalk的自动缩放和高效简单的并发方式,而Go语言很好地提供了这些功能。这不像每天都有一个只有四台机器的集群来处理每分钟写入AmazonS3存储桶的100万个POST请求,这可能比我最好的MacBookPro强大得多。总有适合工作的工具。有时,当您的RubyonRails系统需要一个非常强大的Web处理程序时,值得在Ruby生态系统之外寻找更简单、更强大的替代解决方案。