vlambda博客
学习文章列表

读书笔记《building-a-restful-web-service-with-spring》性能

Chapter 6. Performance

为了在商业环境中部署 RESTful Web 服务,必须满足许多条件。这些标准之一是性能。除了产生正确的结果外,RESTful 端点还必须及时这样做。本章讨论如何在现实世界的 Web 服务中解决这些问题。性能优化技术可以应用于 Web 应用程序的不同方面。但是,在本章中,我们将重点关注 RESTful(Web)层。 第 10 章扩展 RESTful Web 服务,探索适用于 Web 应用程序其他方面的技术。以下主题将在接下来的几页中介绍:

  • 使用 HTTP 压缩

  • 使用 HTTP Cache-Control 指令

  • 使用 HTTP ETag 标头

  • 使用 HTTP Last-Modified/If-Modified-Since 标头

为了说明这些技术,我们将构建示例物业管理系统 Web 服务的房间可用性组件

HTTP compression


在与远程服务通信 时,不可避免地会花费大量时间通过网络发送和接收数据。从应用程序的角度来看,为了减少网络延迟,服务设计人员可以确保将往返次数保持在最低限度。这是本章其余部分的主题。然而,现在让我们看一下另一种可用于减少通过线路发送的数据量的技术。 HTTP 规范定义了一种机制,用于在响应传输到客户端之前对响应应用压缩算法。

Content negotiation

HTTP 压缩围绕两方(服务器和客户端)之间的内容协商展开。 客户端必须通知服务器它支持的压缩算法。通常,这些将是 deflate gzip。客户端通过将以下标头添加到请求中来做到这一点:

"Accept-Encoding": "gzip, deflate"

如果服务器支持这些压缩方案之一,它可以将该方案应用于传出数据。如果数据被压缩,服务器应在响应中添加以下标头:

"Content-Encoding": "gzip"

有了这些信息,客户端就能够适当地处理响应数据。

gzip or deflate?

其他压缩方案可以与 HTTP 一起使用,但 gzip 和 deflate 是最常见的。那么,问题是服务设计师应该更喜欢哪一个?不幸的是,HTTP 规范中的命名存在一些混淆。 Deflate 和 gzip 实际上使用相同的 压缩算法。 gzip 是一种利用 deflate(算法)进行压缩的数据格式。在 HTTP 的上下文 中,deflate 指的是 Zlib,它是另一种使用 deflate 的数据格式。在技​​术方面,deflate 方案提供了更好的性能,但不如 gzip 得到广泛支持。因此,gzip 是更普遍的 HTTP 压缩选择。

gzip compression in RESTful web services

既然我们 知道 gzip 是 HTTP 压缩更普遍的选择,我们希望支持 gzip 压缩 在我们的 RESTful Web 服务中。让我们来看看如何实现这一点。

一般来说,处理压缩是 servlet 容器(例如 Tomcat、Jetty、JBoss 等)的职责。有关启用压缩的详细信息,您应该参考这些容器的文档。

Spring Boot

如果 Web 服务使用 Spring Boot 并在 Tomcat 或 Jetty 上运行,启用 gzip 压缩就像 application.properties:

server.compression.enabled=true
server.compression.mime-types=application/json

前一个属性打开压缩,而后者确保将压缩应用于 JSON 内容。

Tip

gzip 压缩在应用于未压缩内容时效果最佳。实际上,将 gzip 压缩应用于已经压缩的内容(例如图像)不太可能减少任何数据大小,同时会浪费 CPU 周期。

HTTP caching


当提到 性能优化的话题时,缓存往往是首先想到的技术。这种技术可以应用于 Web 服务的不同层。在本节中,我们将专注于利用 HTTP 缓存来提高性能。 第 10 章扩展 RESTful Web 服务,讨论其他形式的缓存。

Cache-Control

HTTP 支持缓存控制指令,以防止客户端和服务器之间不必要的往返。毕竟,减少请求延迟的最佳方法是不必联系服务器来获取响应。这些 指令定义了谁可以缓存响应、在什么条件下以及缓存多长时间。

Private/Public caching

例如,如果一个资源 可以安全地在客户端缓存一段时间,服务设计者可以选择设置 Cache-Control 标头来指示这样的行为。

服务设计者必须选择缓存是私有的还是公共的。他们可以通过在 Cache-Control 标头中设置适当的值来做到这一点:

"cache-control": "public"

Public 表示响应可以被任何中间缓存缓存。这也意味着可以缓存通常不可缓存的内容,例如具有 HTTP 身份验证的响应。

另一方面,private 响应可以被 Web 浏览器缓存,但它们通常只与单个用户相关。在这种模式下,中间体不缓存内容。

要指定缓存内容的 长度,可以指定 max-age 指令:

"cache-control": "private, max-age=300"

包含此标头的响应可以由用户的 Web 浏览器缓存长达 5 分钟(300 秒)。

No caching

对于动态资源,缓存 可能不合适。在这种情况下,响应应包含以下标头:

"cache-control": "no-cache"

这将指示浏览器/客户端在每次发出请求时与服务器进行检查。该标头通常与 ETag 标头结合使用,本章稍后将对此进行讨论。

在必须完全禁用缓存的情况下,应该使用 no-store 值:

"cache-control": "no-store"

此标头通知代理和客户端不要保留响应缓存,并在发出新请求时始终返回服务器。

Tip

Cache-Control 标头是 HTTP/1.1 规范的一部分。较旧的浏览器和代理可能不完全支持它。 A 常见的解决方法是使用 Pragma 标头(HTTP 的一部分/1.0 规范),而不是缓存控制。

虽然在某些情况下禁用 HTTP 缓存是相关的,例如,当响应中包含敏感数据时,应在适用的情况下启用它。除了使用 max-age 指令外,还可以使用 ETag 标头创建更复杂的解决方案。下一节将讨论此标头的使用。

ETags

ETag 提供了一种验证缓存响应的机制。这使缓存更加高效,并最终提高了服务 响应时间。发出响应后,服务器会生成一个封装资源状态的 令牌:

"ETag": "xyz123"

当客户端需要为缓存响应重新发出新请求时,它们可以以 If-None-Match 标头的形式包含 ETag:

"If-None-Match": "xyz123"

然后,服务器可以将此标头与资源的当前状态进行比较。如果资源已更改,则服务器可以使用新资源发出响应。否则,服务器可以返回 304 Not Modified 响应。

Last-Modified/If-Modified-Since headers

这些标头提供 一种类似于 ETag 的机制,允许客户端验证缓存响应的状态。不是生成资源的哈希,而是使用时间戳并进行比较来确定缓存的响应是否有效。

现在我们已经介绍了 HTTP 缓存背后的技术概念,让我们在示例 RESTful Web 服务的可用性组件中将它们付诸实践。

Room availability


在前面的章节中,我们讨论了我们的物业管理服务的库存组件。我们将实现的下一个组件是可用性服务。

An overview of implementation

这个 组件的目的是为最终用户和第三方系统提供一种查询房间在给定时间段内是否可用的方法。

我们将通过查找示例属性中的所有房间并覆盖给定时间段内的现有预订来实现此功能,以计算可用性。在大型酒店使用的实际系统中,使用更复杂的算法来优化房间分配。然而,在我们的例子中,更简单的方法就足够了。因此,让我们考虑以下服务接口:

public interface AvailabilityService {

  /**
  * Answers the availability status for the given query.
  *
  * @param query the availability query
  *
  * @return the availability status for each day in the requested period.
  */
  public List<AvailabilityStatus> getAvailableRooms(AvailabilityQuery query);
}

该服务接受可用性查询并返回给定时间段内可用的房间。用户将 能够查询时间段和可选房间类别的可用性。

Note

该服务和相关类的实现可以从 Packt Publishing 网站下载。

The REST resource

现在让我们考虑我们需要创建的端点以通过 RESTful API 公开此功能。在继续实施此端点之前,要回答的一个基本问题是,该资源将在哪个 URL 下可用?一种可能的方法是向我们现有的房间资源添加一个新端点(参见 第 3 章 第一个端点)。但是,由于我们不会只返回房间列表,因此它不是最合乎逻辑的 解决方案。此外,出于可扩展性的目的,我们可能希望将此组件部署在与 Inventory Service 不同的架构上(这些问题将在 第 10 章扩展 RESTful Web 服务)。因此,我们将使用在新 URL 下公开的新资源:

@RestController
@RequestMapping("/availability")
public class AvailabilityResource {

  @RequestMapping(method = RequestMethod.GET)
  public ApiResponse getAvailability(
    @RequestParam("from") String from,
    @RequestParam("until") String until,
    @RequestParam(value = "roomCategoryId", required = false) String categoryId) {
    // omitted
  }
}

使用这个新的 Spring RestController,可以使用诸如 http://localhost:8080/availability?from=2016-12 之类的 URL 检索房间可用性-01&until=2016-12-01

此 URL 将返回 2016 年 12 月 1 日的可用房间列表。

Note

由开发人员决定以哪种格式表示日期。例如,日期可以接受 UNIX 时间(https://en.wikipedia.org/wiki/Unix_time)。在我们的例子中,我们选择以 ISO 8601 格式(https://en.wikipedia.org/wiki/ISO_8601),因为它更易于阅读。

此外,我们还定义了一个名为 roomCategoryId 的查询参数,它是可选的。使用 Spring,我们可以使用以下注解来做到这一点:

@RequestParam(value = "roomCategoryId", required = false)

正如我们在 第 5 章 中看到的,REST 中的 CRUD 操作< /span>,这个注解指示 Spring 将查询参数 roomCategoryId 映射到我们的 Java 方法参数(如果存在)。

Tip

在不声明此请求参数为不需要的情况下,如果 URL 中不存在该参数,Spring 将生成带有 HTTP 状态 400 的错误响应。

请求给定日期的可用性将返回如下数据:

{
  "status": "OK",
  "data": [{
    "date": "2016-12-01",
    "rooms": [{
      "id": 1,
      "name": "Room 1",
      "roomCategoryId": 1,
      "description": "Nice, spacious double bed room with usual amenities"
    }]
  }]
}

根据我们的简报,此回复列出了请求时间段内每个日期的可用房间。在这种情况下,ID 为 1 的 房间在 2016 年 12 月 1 日可用。

定义 RESTful 端点后,让我们看看如何添加对 HTTP 缓存的支持。

Adding HTTP caching

正如本章开头所讨论的,HTTP 提供了几种缓存方法。在本节中,我们将使用 Last-Modified/If-Modified-Since 标头来防止通过网络发送不必要的数据.为此,我们需要访问 HTTP 响应。让我们修改我们的端点:

@RequestMapping(method = RequestMethod.GET)
public ApiResponse getAvailability(
  @RequestParam("from") String from,
  @RequestParam("until") String until,
  @RequestParam(value = "roomCategoryId", required = false) String categoryId, WebRequest request) {
  // omitted
}

Spring 会将新属性映射到表示请求的 org.springframework.web.context.request.WebRequest 对象。

下一步是能够计算出捕获响应当前状态的日期。在我们的例子中,我们可以使用在请求期间最近更新的预订的更新日期。

然后,在我们端点的方法体中,我们可以利用 Spring 对缓存的支持,如下所示:

AvailabilityQuery query = new AvailabilityQuery(dateRange, categoryId);
// we use the last updated booking date as our Last Modified value
Date lastUpdatedBooking = getLastModified(query);
if (request.checkNotModified(lastUpdatedBooking.getTime())) {
    return null;
}
// perform the query and return the availability status

通过使用 WebRequest.checkNotModified(),我们将 If-Modified-Since 请求标头值与我们计算的值进行比较.如果值匹配,我们不需要处理请求,Spring 将返回 304 响应。否则,我们可以继续处理可用性请求并包含我们生成的 Last-Modified 标头。

Note

调用 checkNotModified() 将确保在响应中设置相关的 HTTP 标头。因此,如果数据没有发生变化,则不需要进一步处理,开发者可以安全地返回 null,而不是手动构造 304 响应。

例如,如果一个 新用户第一次请求可用性,服务器将响应以下标头:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Sun, 14 Jun 2015 22:00:00 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 15 Jun 2015 12:12:32 GMT

重新发出相同的请求后,用户的浏览器将包含以下标头:

GET /availability?from=2016-12-01&until=2016-12-01 HTTP/1.1
// omitted
If-Modified-Since: Sun, 14 Jun 2015 22:00:00 GMT

然后,服务器将能够将请求中提供的时间戳与我们上次修改的日期进行比较,并发出 304 响应:

HTTP/1.1 304 Not Modified
Server: Apache-Coyote/1.1
Date: Mon, 15 Jun 2015 12:16:03 GMT

这种方法减少了服务器上所需的处理量(假设生成最后修改日期不像处理请求那样昂贵)和数据量 发送到服务器,从而提高性能。

Caching with ETags

当生成最后修改日期不合适时,可以使用ETags代替。 Spring 以可以添加到 REST servlet 的过滤器 (org.springframework.web.filter.ShallowEtagHeaderFilter) 的形式对 ETag 提供透明支持。此过滤器会自动生成响应的 MD5 哈希。

可以在 Web 应用程序描述符 (web.xml) 中添加过滤器:

<filter>
  <filter-name>etagFilter</filter-name>
  <filter-class>
    org.springframework.web.filter.ShallowEtagHeaderFilter
  </filter-class>
</filter>
<filter-mapping>
  <filter-name>etagFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

或者,在使用 Spring Boot 时,可以使用配置类添加过滤器:

@Configuration
@EnableWebMvc
@ComponentScan
public class WebApplicationConfiguration extends WebMvcAutoConfiguration {

  @Bean
  public Filter etagFilter() {
    return new ShallowEtagHeaderFilter();
  }
}

当客户端发出第一个可用性请求时,服务器会返回以下标头:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: "09f4ab0b7ca5d8280fbb890a6a5e1c220"
Content-Type: application/json;charset=UTF-8
Content-Length: 463
Date: Mon, 15 Jun 2015 12:40:31 GMT

重新发出相同的请求后,Web 浏览器将包含以下标头:

GET /availability?from=2016-12-01&until=2016-12-01 HTTP/1.1
// omitted
If-None-Match: "09f4ab0b7ca5d8280fbb890a6a5e1c220"

The server will issue the following response:

HTTP/1.1 304 Not Modified
Server: Apache-Coyote/1.1
ETag: "09f4ab0b7ca5d8280fbb890a6a5e1c220"
Date: Mon, 15 Jun 2015 12:50:00 GMT

实际的响应正文 未发送,并指示浏览器使用其本地缓存副本。虽然这种方法提供了一种透明机制来缓存未更改的数据,但它无助于提高服务器端的性能。

Summary


本章让我们有机会了解如何利用 HTTP 优化方法来提高 RESTful Web 服务的性能。在减少客户端和服务器之间的往返行程以及通过网络发送的数据量时,服务设计人员可以确保将请求延迟保持在最低限度。 HTTP 优化的其他好处包括由于带宽减少和服务消费者的功耗降低而降低了运营费用。当消费者是电池寿命有限的移动设备时,最后一点非常重要。

除了与 HTTP 相关的优化之外,第 10 章扩展 RESTful Web 中讨论的其他技术服务,可用于进一步管理网络服务性能。创建 RESTful 服务时的下一个重要主题是安全性。在下一章中,我们将看看如何使用 Spring 处理安全性。