缩放播放! 数以千计的并发请求

已发表: 2022-03-11

Scala Web 开发人员经常没有考虑成千上万的用户同时访问我们的应用程序的后果。 也许是因为我们喜欢快速制作原型; 也许是因为测试这样的场景简直太难了。

无论如何,我要争辩说,忽略可伸缩性并不像听起来那么糟糕——如果您使用正确的工具集并遵循良好的开发实践。

忽略可扩展性并不像听起来那么糟糕——如果你使用合适的工具的话。

洛金哈和戏剧! 框架

前段时间,我开始了一个名为 Lojinha 的项目(在葡萄牙语中意为“小商店”),我尝试建立一个拍卖网站。 (顺便说一下,这个项目是开源的)。 我的动机如下:

  • 我真的很想卖一些我不再使用的旧东西。
  • 我不喜欢传统的拍卖网站,尤其是我们在巴西的那些。
  • 我想和 Play 一起“玩”! 框架 2(双关语)。

很明显,如上所述,我决定使用 Play! 框架。 我没有准确计算构建需要多长时间,但肯定没多久我的网站就启动并运行了在 http://lojinha.jcranky.com 上部署的简单系统。 实际上,我将至少一半的开发时间花在了使用 Twitter Bootstrap 的设计上(记住:我不是设计师……)。

上面的段落至少应该说明一件事:我并没有太担心性能,如果在创建 Lojinha 时完全担心的话。

这正是我的观点:使用正确的工具是有力量的——这些工具可以让你走在正确的轨道上,这些工具可以鼓励你通过构建来遵循最佳开发实践。

在这种情况下,这些工具就是 Play! 框架和 Scala 语言,Akka 做了一些“客串”。

让我告诉你我的意思。

不变性和缓存

人们普遍认为,最小化可变性是一种很好的做法。 简而言之,可变性使您的代码更难推理,尤其是当您尝试引入任何并行性或并发性时。

表演! Scala 框架使您在大部分时间都使用不变性,Scala 语言本身也是如此。 例如,控制器生成的结果是不可变的。 有时您可能会认为这种不变性“烦人”或“烦人”,但这些“良好做法”之所以“好”是有原因的。

在这种情况下,当我最终决定运行一些性能测试时,控制器的不变性绝对是至关重要的:我发现了一个瓶颈,为了解决它,只需缓存这个不可变的响应。

通过缓存,我的意思是保存响应对象并按原样为任何新客户端提供相同的实例。 这使服务器不必重新计算结果。 如果此结果是可变的,则不可能为多个客户端提供相同的响应。

缺点:在短时间内(缓存过期时间),客户端可能会收到过时的信息。 这仅在您绝对需要客户端访问最新数据且不能容忍延迟的情况下才会出现问题。

作为参考,这里是加载带有产品列表的起始页面的 Scala 代码,没有缓存:

 def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }

现在,添加缓存:

 def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }

很简单,不是吗? 这里, “index”是要在缓存系统中使用的键,5 是过期时间,以秒为单位。

缓存后,吞吐量上升到每秒 800 个请求。 对于不到两行代码,这是一个超过 4 倍的改进。

为了测试此更改的效果,我在本地运行了一些 JMeter 测试(包含在 GitHub 存储库中)。 在添加缓存之前,我实现了每秒大约 180 个请求的吞吐量。 缓存后,吞吐量上升到每秒 800 个请求。 对于不到两行代码,这是一个超过4 倍的改进。

这就是我使用 Play 的方式!缓存以提高我的 Scala 拍卖网站的性能。

内存消耗

正确的 Scala 工具可以产生重大影响的另一个领域是内存消耗。 在这里,再次播放! 将您推向正确(可扩展)的方向。 在 Java 世界中,对于使用 servlet API 编写的“普通”Web 应用程序(即,几乎所有的 Java 或 Scala 框架),在用户会话中放置大量垃圾是非常诱人的,因为 API 提供了易于...调用允许您这样做的方法:

 session.setAttribute("attrName", attrValue);

因为向用户会话添加信息非常容易,所以经常被滥用。 因此,无缘无故用尽过多内存的风险同样高。

随着游戏! 框架,这不是一个选项——框架根本没有服务器端会话空间。 表演! 框架用户会话保存在浏览器 cookie 中,您必须忍受它。 这意味着会话空间的大小和类型受到限制:您只能存储字符串。 如果您需要存储对象,则必须使用我们之前讨论过的缓存机制。 例如,您可能希望在会话中存储当前用户的电子邮件地址或用户名,但如果您需要存储域模型中的整个用户对象,则必须使用缓存。

玩! 让您走在正确的轨道上,迫使您仔细考虑您的内存使用情况,这会产生实际上已为集群做好准备的首次通过代码。

再一次,这乍一看似乎很痛苦,但实际上,玩! 让你走在正确的轨道上,迫使你仔细考虑你的内存使用情况,这会产生实际上是集群准备好的第一次通过代码——特别是考虑到没有必须在整个集群中传播的服务器端会话,使生命无限轻松。

异步支持

本剧的下一个! 框架回顾,我们将研究如何玩! 在 async(hronous) 支持方面也很出色。 除了其原生功能之外,Play! 允许您嵌入 Akka,这是一个强大的异步处理工具。

Altough Lojinha 尚未充分利用 Akka,它与 Play 的简单集成! 非常容易:

  1. 安排异步电子邮件服务。
  2. 同时处理各种产品的报价。

简而言之,Akka 是 Erlang 著名的 Actor 模型的实现。 如果你不熟悉 Akka Actor Model,可以把它想象成一个只通过消息进行通信的小单元。

要异步发送电子邮件,我首先创建正确的消息和参与者。 然后,我需要做的就是:

 EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)

电子邮件发送逻辑在actor内部实现,消息告诉actor我们要发送哪封电子邮件。 这是在即发即弃的方案中完成的,这意味着上面的行发送请求,然后继续执行我们之后拥有的任何内容(即,它不会阻塞)。

有关 Play! 的原生 Async 的更多信息,请查看官方文档。

结论

总结:我快速开发了一个小型应用程序 Lojinha,它能够很好地扩展和扩展。 当我遇到问题或发现瓶颈时,修复既快速又简单,由于我使用的工具(Play!、Scala、Akka 等)而备受赞誉,这促使我在效率和可扩展性。 几乎不关心性能,我能够扩展到数千个并发请求。

在开发下一个应用程序时,请仔细考虑您的工具。

相关:使用 Scala 宏和准引号减少样板代码