之前,我们讨论了随着负载的增加,如何逐步在生产中的关键服务后端放弃使用Python,而将其替换为Go。 今天,我,Madmin开发团队的负责人Denis Girko,希望分享细节:在我们业务最重要的服务之一的例子中,这种情况是如何发生的以及为什么发生-计算价格时要考虑到优惠券的折扣。

使用优惠券的机制可能由至少一次在网上商店购买过商品的人代表。 在特殊页面上或直接在购物篮中,输入优惠券编号,然后根据承诺的折扣重新计算价格。 计算取决于优惠券提供的折扣类型-以百分比,固定金额的形式或使用其他一些数学方法(例如,我们另外考虑忠诚度计划的积分,商店促销,商品类型等)。 自然地,订单已经以新价格发出。
所有这些与价格打交道的机制使企业感到高兴,但我们想从稍微不同的角度来谈论服务。
如何运作
考虑到后端的所有这些困难,为了定价,我们现在提供单独的服务。 但是,他并不总是独立的。 该服务在在线商店开始后的一两年内出现,到2016年,它已成为大型Python整体的一部分,其中包括用于营销活动(Madmin)的各种组件。 当他转向微服务架构时,他作为一个独立的“街区”脱颖而出。
像通常的整体式一样,Madmin被修改并与大量开发人员部分对应。 第三方库在此处进行了集成,从而简化了开发过程,但通常不会对性能产生最佳影响。 但是,那时,我们并没有真正在意销售过程中的抗重负荷能力,因为该服务出色地完成了任务。 但是2016年改变了一切。

在美国,“黑色星期五”自上世纪60年代以来就广为人知。 在俄罗斯,它开始于2010年代推出,而行动必须从头开始-市场还没有为此做好充分的准备。 但是,组织者的努力并没有白费,并且在销售日中,每年访问我们站点的用户流量都在增加。 因此,我们与负载的冲突(对于该版本的价格计算服务而言是过多的)只是时间问题。
黑色星期五2016年。我们睡过头了
由于销售创意发挥了最大的作用,因此“黑色星期五”与一年中的其他任何一天都不同,因为午夜时分,大约每周一次的网站访问者来到商店。 这是所有服务的困难时期。 即使是那些全年运行平稳的企业,有时也会出现问题。
现在,我们正在为每个新的“黑色星期五”做准备,模仿了预期的负荷,但是在2016年,我们的行为仍然有所不同。 在重要的一天之前测试Madmin时,我们会定期使用用户行为场景测试负载阻力。 事实证明,该测试不能完全反映实际情况,因为在“黑色星期五”,很多人都拥有相同的优惠券。 结果,考虑到此折扣的价格计算服务无法应付三倍(与普通天相比)的负荷,从而使我们无法在销售的最高峰为客户提供两个小时的服务。
该服务在午夜前一个小时“进行”。 一切始于与数据库的连接中断(当时为MySQL),此后并非所有正在运行的价格计算服务副本都可以重新连接。 仍然连接的那些卡住了基本锁,无法承受负载并停止响应。
碰巧的是,那名初级人员当时仍在值班,服务中断时他正从办公室回家的路上。 他只有在到达该地点并召唤“重炮”即紧急值班人员时,才能解决问题。 但是,仅在两个小时之后,他们一起使局势正常化。
随着诉讼程序的开始,有关服务质量欠佳的细节开始公开。 例如,事实证明,要计算一张优惠券,需要对数据库进行28个查询(不足为奇的是,所有工作都在100%CPU利用率下进行)。 上面提到的使用相同黑色星期五优惠券的用户并不能简化情况,尤其是因为我们为所有优惠券都设有一个应用程序计数器-因此每次使用都通过引用该计数器来增加负载。
2016年给了我们很多思考的机会-主要是关于如何通过优惠券和测试来调整我们的工作,以免再次发生这种情况。 这张图片最能描述星期五的数字:
黑色星期五2016年的结果黑色星期五2017年。我们正在认真准备,但...
收到了很好的教训之后,我们为下一个“黑色星期五”进行了提前准备,并认真重建和优化了服务。 例如,我们最终创建了两种类型的优惠券:限制和无限制-为了避免锁定对数据库的同时访问,我们从应用流行的优惠券的脚本中删除了数据库条目。 同时,在黑色星期五之前的1-2个月,我们在服务中从MySQL切换到了PostgreSQL,并通过代码优化将数据库调用的次数从28个减少到了4-5个。这些改进使我们能够将测试服务扩展到SLA要求-回答在3秒内达到600 RPS的95%。
不知道我们的改进在多大程度上加快了该服务旧版本的生产速度,当时正准备为黑色星期五准备两个版本的Python代码-高度优化的现有版本和从头开始编写的全新代码。 在生产中,第二台产品已推出,并已在白天和晚上进行了测试。 然而,事实证明,这是在“战斗中”,但未得到充分测试。
在“紧急”的一天,随着主要客户流的到来,服务的负担开始呈指数级增长。 一些请求最多处理了两分钟。 由于某些请求的长时间处理,其他工作人员的负担增加了。
我们的主要任务是为企业提供如此宝贵的流量。 但是显而易见的是,“铸铁”并不能解决问题,忙碌的工人人数将在任何时候达到100%。 不知道当时到底要面对什么,我们决定在uWSGI中激活harakiri并简单地确定很长的请求(处理时间超过6秒)以释放正常请求的资源。 它确实有助于抵抗-工人在完全精疲力尽前几分钟就被释放了。
过了一会儿,我们弄清了情况……原来,这些请求是用非常大的篮子(从40到100种商品)提出的,并带有对范围有限制的特定优惠券。 新代码很难解决这种情况。 它显示了对数组的不正确工作,从而变成了无限递归。 奇怪的是,我们然后用一个大篮子而不是棘手的优惠券测试了一个箱子。 作为解决方案,我们只是切换到该代码的其他版本。 没错,这发生在黑色星期五结束前三个小时。 从这一刻起,所有篮子都开始被正确处理。 尽管我们当时完成了销售计划,但由于负载是通常的五倍,我们避免了奇迹般的全球性问题。
黑色星期五2018
到2018年,对于为网站提供高负载的服务,我们逐渐开始实施Go。 考虑到以前的黑色星期五的历史,折扣计算服务是最先进行处理的候选人之一。

当然,我们可以保存已经在“战斗中测试过”的Python版本,在新的“黑色星期五”之前,我们可以关闭繁重的库并丢弃非最佳代码。 但是,Golang到那时已经扎根,并且看起来更有希望。
我们在今年夏天改用了一项新服务,因此在下一次销售之前,我们设法对其进行了良好的测试,包括增加了负荷。
在测试期间,事实证明,高负载方面的弱点仍然是我们的基础。 事务处理时间太长导致我们选择了整个连接池,并且请求已排队。 因此,我们不得不重做一下应用程序的逻辑,将数据库的使用量减少到最低限度(仅在没有它的情况下才引用它),并从数据库中缓存目录和黑色星期五流行的优惠券上的数据。
没错,今年我们错了上调负载预测:我们正准备将高峰期增长6-8倍,并为如此大量的请求实现了良好的服务工作(增加了缓存,预先禁用了实验功能,简化了一些事情,部署了额外的Kubernetes节点甚至是副本的数据库服务器(最终不需要)。 实际上,用户兴趣的激增较少,因此一切照常进行。 在95%时,服务响应时间不超过50毫秒。
对我们来说,最重要的特征之一是当没有足够的资源可用于一个副本时,应用程序如何扩展。 Go可以更有效地使用硬件资源,因此在相同的负载下,您需要运行较少的副本(最终在相同的硬件资源上处理更多的请求)。 今年,在销售高峰时,有16个应用程序实例正在运行,平均每秒处理300个请求,最高每秒处理400个请求,这比通常的负载高出两倍。 请注意,去年,Python服务需要102个实例。
似乎从第一种方法开始的Go服务就满足了我们的所有需求。 但是Golang并不是“解决所有问题的一站式解决方案”。 没有一些功能就无法做到。 例如,我们必须限制该服务可以在Kubernetes多处理器节点上启动的线程数,以便在扩展时不会干扰生产中“相邻”应用程序(默认情况下,Go对将使用的处理器数量没有限制)。 为此,我们在Go的所有应用程序中设置了GOMAXPROCS。 我们将很高兴地评论这种做法的实用性-在我们的团队中,这只是关于如何处理“邻居”的退化的一种假设。
另一个“设置”是保持活动状态的连接数。 默认情况下,Go中的常规http和DB客户端仅保留两个连接,因此,如果有多个并发请求,并且您需要节省TCP连接设置的流量,则可以通过分别设置MaxIdleConnsPerHost和SetMaxIdleConns来增加此值。
但是,即使有了这些手动“扭曲”,Golang也为我们的未来销售提供了很大的性能余地。