HTTP (HyperText Transfer Protocol) 的设计可以支持将数据从服务器传输到客户端的无状态、请求-响应模式。协议的第一版 1.0 只能支持 1:1 的请求与连接比例(即每个连接支持一个请求-响应对)。
1.1 版本将这一比例扩大为 N:1,即每个连接可以支持许多请求。此举是为了应对 Web 页面日益增加的复杂性,包括许多需要从服务器传输到客户端的对象和元素。
随着 2.0 的采用,HTTP 还继续支持每个连接可对应许多请求的模式,但涉及一些最根本的变化:交换标头和从基于文本的传输转向二进制。
在某一阶段,HTTP 不仅仅是将文本和图像从服务器传输到客户端的简单机制,还成为了一个应用平台。浏览器具有普遍性、跨平台性,无需为支持多种操作系统和环境而付出高额成本就能轻松部署应用,这些特点无疑极具吸引力。不过遗憾的是,HTTP 的设计使其不能用作应用传输协议,但可以传输文档。即使是 HTTP 的现代用途(如 API 的用途)也是假定一个类似于文档的有效负载。JSON(一种以文本形式传输的关键值对数据格式)就是最好的示例。虽然文档和应用协议一般都是基于文本,但相似之处也只有这点而已。
传统的应用需要通过某种方式来维持其状态,而文档则不需要。应用是建立在逻辑流和过程的基础之上,这两者都要求应用要获取用户当时的位置,这就需要状态。尽管 HTTP 本身具有无状态的特性,但就实际而言,它已经成为 Web 中的应用传输协议。纵观技术历史上受到最广泛认可且有效的黑客攻击,HTTP 均为追踪应用整个使用过程中的状态提供了途径。“入侵点”便是会话和 Cookie 发挥作用的位置。
会话是 Web 和应用服务器维持状态的方式。这些简单的内存块与各个进行 Web 或应用服务器的 TCP 连接相关联,并在基于 HTTP 的应用中作为信息的内存存储。
当用户第一次连接到服务器时,会创建一个会话并与该连接相关联。然后,开发人员使用该会话作为存储与应用相关的数据位的地方。这些数据从重要的信息(如客户 ID)到不太重要的数据(如倾向于哪种网站首页的显示方式)均有。
购物车便是会话用处的最佳示例,因为我们几乎所有人都有过网上购物的经历。购物车中的物品会在一个“会话”过程中保持不变,原因在于购物车中的每个物品都会以某种方式在服务器的会话中进行表示。另一个形象的示例是向导式的产品配置或自定义应用。这些“迷你”应用可支持浏览一组选项,并进行选择;最后,您通常会对预估的成本感到诧异,因为自己居然添加了如此多的额外功能。当单击各个“选项屏幕”时,所选择的其他选项会存储在会话中,因此可以很容易地对其进行检索、添加或删除。
现代应用的设计是无状态的,但其架构可能不符合这一原则。现代的扩展方法通常依赖于 Sharding 这样的架构模式,这需要基于一些可索引数据(如用户名或帐号)来路由请求。这就需要一种有状态的方法,其中每个请求都会携带可索引数据,确保正确的路由和应用行为。在这方面,现代“无状态”应用和 API 往往需要与旧版有状态版本类似的处理和支持。
问题在于会话与连接是绑定状态,连接闲置时间过长就会失效。另外,对于连接“过长”的定义与其应用于会话时会有很大不同。例如,一些 Web 服务器的默认配置是:一旦连接空闲(也就是说,没有更多的请求被发出)15 秒,就会关闭该连接。相反,在这些 Web 服务器中,默认情况下,一个会话将在内存中保留 300 秒(或 5 分钟)。显然,这两者相互矛盾,因为如果会话与连接相关联,那么一旦连接超时,作用何在?
您可能会想到只要增加连接超时值使之与会话匹配便可以解决这种差异。增加超时意味着有可能要消耗内存来维持一个不一定会用到的连接。这可能会降低服务器的总并发用户容量,最终会阻碍其性能;而且您也肯定不会希望借由减少会话超时来匹配连接超时,因为大多数人需要不止五分钟的时间来选购或定制自己的新应用。
重要提示:虽然 HTTP/2 解决了其中的一些问题,但也引入了其他与维护状态有关的问题。虽然规范中并未作出要求,但主流浏览器只支持 HTTP/2 over TLS/SSL。两个协议都需要持久化以避免重新协商的性能成本,这又需要会话感知,也就是有状态的行为。
因此,最后的结果便是即使在会话关联的连接因不活动而被终止后,该会话仍然可作为内存留在服务器,而这些会话会占用宝贵的资源,并可能因应用无法运行而导致用户不满。
好在此问题可以通过使用 Cookie 得到解决。
Cookie 是浏览器存储在客户端的数据。Cookie 可以(也确实会)存储关于您、您的应用和您所访问网站的各种信息。术语“Cookie”来源于魔术曲奇(magic cookie),这是 UNIX 计算中一个著名的概念,想法和名称都源自于此。Cookie 是通过 HTTP 标头(Cookie)在浏览器和服务器之间完成创建和共享。
Cookie: JSESSIONID=9597856473431 Cache-Control: no-cache Host: 127.0.0.2:8080 Connection: Keep-Alive
浏览器会自动将 HTTP 标头中的 Cookie 存储于电脑文件中,并且会以每个域为基础,对 Cookie 进行追踪。任何给定域的 Cookie 始终由浏览器在 HTTP 标头中传递给服务器,因此 Web 应用开发人员只需在应用的服务器端询问就可以检索这些值。
会话/连接长度问题可以通过 Cookie 解决。几乎所有的现代 Web 应用都会生成“会话 ID”,并将其作为 Cookie 传递给用户。这使得应用能够在服务器上找到会话,即使在创建会话的连接关闭后也是如此。通过这种会话 ID 的交换,即使是像 HTTP 这样的无状态协议,也能维持状态。但是,当 Web 应用的使用超出了单个 Web 或应用服务器的能力时,又会如何?通常会引入负载平衡器,或者在如今的架构中引入应用交付控制器 (ADC) 对应用实施扩展,从而在可用性和性能方面令用户感到满意。
在现代应用中仍然可以使用 Cookie,但其他 HTTP 标头的重要性也会随之凸显。用于身份验证和授权的 API 密钥通常通过 HTTP 标头,还有其他自定义标头来传输,这些标头会携带路由和适当规模的后端服务所需数据。无论是使用传统的“Cookie”还是使用其他 HTTP 标头来携带这些数据,意识到这点对于整体架构的重要性才是更为关键的问题。
上述问题在于负载均衡算法一般只关注在服务器之间分配请求。负载均衡技术是基于工业标准算法,如轮询、最小连接数或最快响应时间。这些算法均不是有状态的算法,同一个用户向应用发出的每个请求都有可能被分配到不同的服务器。这使得所有为 HTTP 实施状态的工作都变得毫无用处,因为存储在一个服务器会话中的数据很少与“池”中的其他服务器共享。
这也是持久化概念诞生的来源。
持久化(也就是所谓的粘性)是 ADC 实施的一种技术,目的是确保来自单个用户的请求总是被分发到其来源的服务器。一些负载均衡产品和服务将这种技术描述为“粘性会话”,形容得非常贴切。
长期以来,持久化一直用于由负载平衡 SSL/TLS 提供支持的网站,因为一旦协商过程(计算密集型的过程)已经完成并交换了密钥,重启这个过程将大幅降低性能。因此,ADC 实施了 SSL 会话持续性,以确保总是将用户引导至他们首次连接的相同服务器。
多年来,浏览器实施已经催生一种需求,即开发一种可以避免在这些会话上耗时耗力的重新协商。这种技术被称为基于 Cookie 的持久化。
负载平衡器不会依赖 SSL/TLS 会话ID,而是会在客户端第一次访问网站时插入 Cookie 来进行唯一会话识别,然后在后续请求中引用该 Cookie 来坚持连接到适当的服务器。
此后,基于 Cookie 的持久化概念会应用于应用会话,使用 Web 和应用服务器生成的会话 ID 信息,以确保用户的请求在同一会话期间总是指向同一服务器。如果没有这种能力,需要负载均衡的应用将需要找到其他方式来共享会话信息,或者在不得已的情况下,增加会话和连接超时,以至于支持其用户群所需的服务器数量将迅速增长到无法管控的程度。
虽然最常见的持久化形式是使用 HTTP 标头中传递的会话 ID ,但如今 ADC 也可以对其他数据进行持久化。任何可以存储在 Cookie 中或从 IP、TCP 或 HTTP 标头中派生的数据都可以用来持久化会话。事实上,智能 ADC 可以使用应用消息中唯一标识用户的任何数据来持久化浏览器和服务器之间的连接。
HTTP 可能是一个无状态的协议,但我们已经成功地将状态强加到了这个使用性广泛的协议中。使用持久化和应用交付控制器,可以构建高可用性、高性能的 Web 应用,而不会破坏维护状态所需的 Cookie 和会话的集成。
这些功能的作用是赋予 HTTP 状态,尽管其实施和执行仍然是无状态。如果没有 Cookie、会话和持久化,我们肯定会找到一个有状态的协议来构建应用。相反,在应用交付控制器中发现的特性和功能在浏览器(客户端)和服务器之间进行调解以提供这种功能,将 HTTP 的有用性从静态 Web 页面和传统应用扩展到现代基于微服务的架构和数字经济的关键部分 API。