在我以前的文章中,我展示了将Scala与Java进行基准测试是没有意义的,并得出结论,说到性能,您应该问的问题是:当服务器因意外负载而崩溃时,Scala将如何帮助我? ' 在这篇文章中,我将试图回答这一问题,并表明确实Scala是一种用于构建可伸缩系统的语言,远胜于Java。
但是,不要期望我们的旅程会很轻松。 首先,虽然做微基准测试非常容易,但是尝试显示现实应用程序如何处理或不处理施加在它们上面的负载却非常困难,因为创建一个足够小以进行演示的应用程序非常困难并在同一篇博客文章中进行解释,该博客文章的大小足够大,可以实际显示真实世界的应用程序在负载下的行为,而且很难模拟真实世界的负载。 因此,我将在现实世界中可能会出错的地方做一小部分,并展示Scala为您提供帮助的一种方法,而Java则不会。 然后,我将解释这仅仅是冰山一角,还有更多的情况和Scala的许多功能可以在现实世界中为您提供帮助。
网上商店
为此,我建立了一个在线商店。 下图显示了该商店的体系结构:
如您所见,商店有与之交谈的支付服务和搜索服务,商店处理三种类型的请求,一种是不需要任何其他服务的索引页面,一种用于支付使用支付服务,另一个用于搜索使用搜索服务的商店产品列表。 在线商店是我将要进行基准测试的系统的一部分,我将用Java实现一个版本,并在Scala中实现另一个版本,并进行比较。 搜索和付款服务不会改变。 它们的实际实现将是简单的JSON API,这些API返回硬编码的值,但是它们每个都将模拟20ms的处理时间。
对于商店的Java实现,我将使它尽可能简单,使用直接的servlet来处理请求,使用Apache Commons HTTP客户端进行请求,并使用Jackson进行JSON解析和格式化。 我将应用程序部署到Tomcat,并使用NIO连接器配置Tomcat,使用默认连接限制10000和线程池大小200。
对于Scala实现,我将使用Play框架2.1,使用由Ning HTTP客户端支持的Play WS API发出请求,并使用由Jackson支持的JSON解析和格式化的Play JSON API。 Play Framework是使用Netty构建的,没有连接限制,并使用Akka进行线程池,我将其配置为使用默认的线程池大小,即每个CPU一个线程,并且我的计算机有4个。
我将要执行的基准测试将使用JMeter。 对于每种请求类型(索引,付款和搜索),我将有300个线程在一个循环中旋转,从而发出请求,每个请求之间有500-1500ms的随机间隔。 这样,每种请求类型的平均最大吞吐量为每秒300个请求,或每秒总计900个请求。
因此,让我们看一下Java基准测试的结果:
在此图上,我为每种请求类型绘制了3个指标。 中位数是请求时间的中位数。 对于索引页面,这几乎是零,对于搜索和付款请求,这大约是77毫秒。 我还绘制了90%线,这是Web应用程序中的常见指标,它显示了90%的请求不足,因此可以很好地了解慢速请求的状态。 对于索引页面,这几乎没有显示任何内容,而对于搜索和付款请求,则显示了116ms。 最终指标是吞吐量,它显示每秒处理的请求数。 我们与理论最大值相差不远,该索引显示每秒290个请求,而搜索和付款请求大约每秒270个请求。 这些结果很好,我们的Java服务可以轻松处理正在处理的负载。
现在让我们看一下Scala基准测试:
如您所见,它与Java结果相同。 这不足为奇,因为在线商店的Java和Scala实现都在工作代码方面做到了极少的工作,因此大部分处理时间都花在了对远程服务的请求上。
出问题了
因此,我们已经在Scala和Java中看到了两个相同的东西的快乐实现,从而减轻了我给他们带来的负担。 但是,当事情不太顺利和花哨时,会发生什么? 如果他们正在与之交谈的服务之一出现故障,该怎么办? 假设搜索服务开始需要30秒钟才能做出响应,然后返回错误。 这不是异常的故障情况,尤其是当您通过代理进行负载平衡时,代理会尝试连接到服务,并在30秒后失败,从而给您网关错误。 让我们看看我们的应用程序如何处理我现在向它们施加的负载。 我们希望搜索请求至少需要30秒才能响应,但是其他请求呢? 这是Java结果:
好吧,我们不再有一个快乐的应用程序了。 搜索请求自然会花费很长时间,但是付款服务现在平均需要9秒钟才能做出响应,而90%的时间是20秒钟。 不仅如此,索引页也会受到类似的影响–如果用户浏览了您的网站以显示主页,则用户将不必等待那么长时间。 而且,每个服务器的吞吐量已降至每秒30个请求。 这不好,因为您的搜索服务中断了,整个站点现在几乎无法使用,并且您很快就会失去客户和金钱。
那么我们的Scala应用如何公平? 让我们找出:
现在,在我再说什么之前,我要指出的是,我将响应时间限制为160毫秒-搜索请求实际上需要大约30秒才能响应,但是在图表上,与其他值相邻的是30秒对齐高像素的线。 因此,我们在这里可以看到的是,尽管搜索无法使用,但我们的付款和索引请求响应时间以及吞吐量都保持不变 。 显然,客户对无法进行搜索并不会感到满意,但至少他们仍然可以使用网站的其他部分,查看带有特价的主页,甚至仍然可以为商品付款。 嘿,Google还不错,他们可以随时使用Google搜索您的网站。 因此,您可能会失去一些业务,但影响有限。
因此,在此基准测试中,我们可以看到Scala胜出。 当问题开始出现问题时,Scala应用程序将全力以赴,为您提供最好的解决方案,而Java应用程序可能会崩溃。
但是我可以用Java做到这一点
现在从一点开始,我对人们会对此基准提出的许多预期的批评进行反驳。 第一个也是最明显的一个例子是,在我的Scala解决方案中,我使用了异步IO,而在我的Java解决方案中,我没有使用,因此无法进行比较。 的确,我可以用Java实现异步解决方案,在这种情况下,Java结果将与Scala结果相同。 但是,尽管我可以做到这一点,但是Java开发人员却没有这样做。 不是因为他们做不到,而是因为他们做不到。 我用Java编写了许多Web应用程序,这些Web应用程序可以调用其他系统,而且很少,而且只有在非常特殊的情况下,我才使用异步IO。 让我告诉你为什么。
假设您必须对一系列远程服务进行一系列调用,每个调用都取决于前一个返回的数据。 这是Java中很好的老式同步解决方案:
User user = getUserById(id);
List<Order> orders = getOrdersForUser(user.email);
List<Product> products = getProductsForOrders(orders);
List<Stock> stock = getStockForProducts(products);
上面的代码简单,易于阅读,并且对于Java开发人员来说,编写代码是完全自然的。 为了完整起见,让我们看一下Scala中的同一件事:
val user = getUserById(id)
val orders = getOrdersForUser(user.email)
val products = getProductsForOrders(orders)
val stock = getStockForProducts(products)
现在,让我们看一下相同的代码,但是这次假设我们正在进行异步调用并以promise返回结果。 在Java中看起来像什么?
Promise<User> user = getUserById(id);
Promise<List<Order>> orders = user.flatMap(new Function<User, List<Order>>() {
public Promise<List<Order>> apply(User user) {
return getOrdersForUser(user.email);
}
}
Promise<List<Product>> products = orders.flatMap(new Function<List<Order>, List<Product>>() {
public Promise<List<Product>> apply(List<Order> orders) {
return getProductsForOrders(orders);
}
}
Promise<List<Stock>> stock = products.flatMap(new Function<List<Product>, List<Stock>>() {
public Promise<List<Stock>> apply(List<Product> products) {
return getStockForProducts(products);
}
}
因此,首先,上面的代码不可读,实际上很难遵循,执行代码的实际代码的噪声水平很高,因此很容易出错和遗漏东西。 其次,编写代码很繁琐,没有开发人员愿意编写看起来像这样的代码,我讨厌这样做。 任何想要像这样编写整个应用程序的开发人员都是疯狂的。 最后,这只是感觉不自然,不是您在Java中做事的方式,不是惯用语言,在Java生态系统的其余部分不能很好地发挥作用,第三方库不能与这种风格很好地集成。 如前所述,Java开发人员可以编写实现此目的的代码,但是他们不能这样做 ,并且正如您所看到的,他们没有充分的理由。
因此,让我们看一下Scala中的异步解决方案:
for {
user <- getUserById(id)
orders <- getOrdersForUser(user.email)
products <- getProductsForOrders(orders)
stock <- getStockForProducts(products)
} yield stock
与Java异步解决方案相比,此解决方案完全可读,就像Scala和Java同步解决方案一样可读。 这不仅仅是大多数Scala开发人员从未接触过的一些怪异的Scala功能,这就是典型的Scala开发人员每天编写代码的方式。 Scala库旨在使用这些惯用语来工作,感觉很自然,该语言正在与您一起工作 。 在Scala中编写这样的代码很有趣!
这篇文章不是关于如何使用一种语言编写性能经过高度调整的应用程序,而该应用程序要比使用另一种针对性能进行高度调整的语言编写的应用程序要快。 这篇文章介绍了Scala如何使用自然,可读和惯用的代码帮助您编写默认情况下可扩展的应用程序。 就像草地上的球有偏差一样,Scala也有偏差来帮助您编写可扩展的应用程序,而Java使您可以向上游游泳。
但是扩展的意义远不止于此
我提供的Scala扩展很好的示例(Java并非很好)是一个非常具体的示例,但是在高负载下您的应用程序失败的情况不是什么呢? 让我给出其他一些示例,其中Scala更好的异步IO支持可帮助您编写可伸缩代码:
- 使用Akka,您可以轻松地为不同类型的请求定义参与者,并为他们分配不同的资源限制。 因此,如果单个应用程序的某些部分开始出现问题或承受意外负载,则这些部分可能会停止响应,但应用程序的其余部分可以保持正常运行。
- Scala,Play和Akka使使用并行运行的多个线程来处理单个请求的操作非常简单,这使您可以在很短的时间内处理很多事情。 Klout写了一篇很棒的文章,介绍了他们如何在API中做到这一点。
- 由于异步IO非常简单,因此可以在不占用第一台计算机上线程的情况下安全地将处理卸载到其他计算机上。
Java 8将使Java中的异步IO变得简单
Java 8可能会包含对某种类型的闭包的支持,这对Java世界来说是个好消息,特别是如果您要进行异步IO。 但是,该语法仍然无法像我上面显示的Scala代码那样可读。 Java 8何时发布? Java 7于去年发布,花了5年时间才发布。 Java 8计划于2013年夏季发布,但即使按时发布,该生态系统要花多长时间才能赶上? Java开发人员从同步思维方式转换到异步思维方式需要多长时间? 我认为Java 8太少了,太晚了。
这就是异步IO的全部内容吗?
到目前为止,我所讨论和展示的都是Scala如何轻松实现异步IO,以及如何帮助您扩展。 但这并不止于此。 让我选择Scala的另一个功能,即不变性。
当您开始使用多个线程来处理单个请求时,便开始在这些线程之间共享状态。 这就是事情变得非常混乱的地方,因为计算机系统中共享状态的世界是一个疯狂的世界,发生不可能的事情。 这是一个死锁世界,一个线程正在更新内存,但是另一个线程却没有看到这种变化,一个竞争条件世界,一个性能瓶颈世界,因为您过度地将某些方法标记为同步。
但是,这还不算太糟,因为有一个非常简单的解决方案,可以使您的所有状态保持不变。 如果您的所有状态都是不可变的,那么以上所有问题均不会发生。 这也是Scala可以帮助您节省大量时间的地方,因为在Scala中,默认情况下事物是不可变的。 集合API是不可变的,您必须明确要求可变集合才能获取可变集合。
现在,使用Java,您可以使事情变得不可变。 有一些库可以帮助您(尽管很笨拙)处理不可变的集合。 但是,很容易意外忘记使某些东西变得易变。 Java API和语言本身使使用不可变结构变得不容易,并且,如果您使用的是第三方库,则很可能没有使用不可变结构,并且经常需要使用可变结构,例如JPA需要这个。
让我们看一些代码。 这是Scala中的一个不变的类:
case class User(id: Long, name: String, email: String)
这种结构是不可变的。 此外,它会自动为属性生成访问器。 让我们看一下相应的Java:
public class User {
private final long id;
private final String name;
private final String email;
public User(long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email
}
}
那是大量的代码! 如果我添加新属性该怎么办? 我必须在构造函数中添加一个新参数,这将破坏现有代码,或者我必须定义第二个构造函数。 在Scala中,我可以这样做:
case class User(id: Long, name: String, email: String, company: Option[Company] = None)
我所有调用该构造函数的现有代码仍然可以使用。 而且,当该对象在构造函数中增长为具有10个项目时,如何构造它成为一场噩梦! Java中对此的一种解决方案是使用构建器模式,该模式将为对象编写的代码量增加一倍以上。 在Scala中,您可以命名参数,因此很容易看到哪个参数是哪个参数,并且不必按正确的顺序排列。 但是也许我可能只想修改一个属性。 这可以在Scala中完成,如下所示:
case class User(id: Long, name: String, email: String, company: Option[Company] = None) {
def copy(id: Long = id, name: String = name, email: String = email, company: Option[Company] = company) = User(id, name, email, company)
}
val james = User(1, 'James', 'james@jazzy.id.au')
val jamesWithCompany = james.copy(company = Some(Company('Typesafe')))
上面的代码是自然的,简单的,易读的,这是Scala开发人员每天编写代码的方式,并且是不可变的。 它非常适合于并发代码,并允许您安全地编写可扩展的系统。 在Java中可以完成相同的操作,但是这很繁琐,并且编写起来一点也不高兴。 我是Java不变代码的大力提倡者,并且我已经用Java编写了许多不变类,这很痛苦,但这要少两点。 在Scala中,使用可变对象比使用不可变对象需要更多的代码。 同样,Scala倾向于帮助您扩展规模。
结论
我无法深入探讨Scala可以帮助您扩展Java无法做到的所有方式。 但是我希望我能带您了解为什么Scala在编写可扩展系统时会站在您这一边。 我已经展示了一些具体的指标,已经比较了Java和Scala解决方案来编写可伸缩代码,并且展示了,并不是Scala系统的扩展性总是优于Java系统,而是Scala是您所使用的语言。编写可伸缩系统时的问题。 它偏向于扩展,它鼓励可以帮助您扩展的实践。 相比之下,Java使您难以实施这些实践,它对您不利。
如果您对我的在线商店代码感兴趣,可以在GitHub存储库中找到它。 我的性能测试中的数字可以在此电子表格中找到。
参考: James and Beth Roper的博客博客中的JCG合作伙伴 James Roper的Scaling Scala vs Java 。
翻译自: https://www.javacodegeeks.com/2012/11/scaling-scala-vs-java.html