#rest #cache #reverse-proxy #api-response #proxy #http-cache #response-headers

app bloom-server

HTTP REST API缓存中间件,用于负载均衡器和REST API工作进程之间

32个稳定版本

1.35.2 2023年9月6日
1.34.0 2023年8月13日
1.32.0 2023年6月1日
1.31.0 2022年8月17日
1.15.0 2017年11月23日

#288缓存

Download history 104/week @ 2024-03-31 60/week @ 2024-06-30

每月60次下载

MPL-2.0 许可证

130KB
2.5K SLoC

Bloom

Test and Build Build and Release dependency status Buy Me A Coffee

Bloom是一个REST API缓存中间件,充当负载均衡器和REST API工作进程之间的反向代理。

它对您的API实现完全无感知,并且只需对现有API代码进行最小更改即可使用。

Bloom依赖于 redis,配置为缓存存储缓存数据。它用Rust编写,专注于稳定性、性能和低资源使用。

重要:如果您要实现的API遵循REST约定,则Bloom工作得很好。您的API需要使用HTTP读取方法,即 GETHEADOPTIONS 仅作为读取方法(不要使用HTTP GET参数作为更新数据的方式)。

在Rust版本 rustc 1.71.1 (eb26296b5 2023-08-03) 上进行了测试

🇫🇷 在法国布勒斯特制作。

📰 Bloom项目最初在 我的个人日志中的一篇文章 中宣布。

Bloom

谁在使用它?

Crisp

👋 您使用Bloom并希望被列在这里? 联系我

功能

  • 同一个Bloom服务器可以同时用于不同的API工作进程,使用HTTP头 Bloom-Request-Shard(例如,主API使用分片 0,搜索API使用分片 1)。
  • 在桶上存储缓存,使用HTTP头 Bloom-Response-Buckets 在您的REST API响应中指定。
  • 按认证令牌分组的缓存,不可能出现跨用户的缓存泄漏,使用标准的 Authorization HTTP头。
  • 可以直接从您的REST API工作程序中使缓存过期,通过控制通道。
  • 可按请求配置缓存策略,使用请求中转发到Bloom的负载均衡器的 Bloom-Request-* HTTP头。
    • 使用 Bloom-Request-Shard 为API系统指定缓存碎片(默认碎片是 0,最大值是 15)。
  • 可按响应配置缓存策略,使用您的API响应中的 Bloom-Response-* HTTP头。
    • 使用 Bloom-Response-Ignore(值为 1)禁用API路由的所有缓存。
    • 使用 Bloom-Response-Buckets 为API路由指定缓存桶(如果多个桶,则逗号分隔)。
    • 使用 Bloom-Response-TTL 为API路由指定缓存TTL(秒)(除默认TTL外,为秒数)。
  • 向非修改路由内容提供 304 Not Modified,降低带宽使用并加快用户请求的速度。

Bloom方法

Bloom可以热插拔到位于现有负载均衡器(例如NGINX)和API工作程序(例如NodeJS)之间的位置。它最初是为了减少API流量激增或DOS/DDoS攻击时的工作负载和显著降低CPU使用而构建的。

可能有一个更简单的缓存方法,就是在负载均衡器级别为HTTP读取方法(GETHEADOPTIONS)启用缓存。虽然作为一种解决方案很简单,但它不会与REST API一起工作。REST API本质上是动态内容,并且严重依赖于授权头。此外,如果由于某些数据库中的数据更新,缓存中的内容变得过时,任何缓存都需要在某个时候被清除。

你说NGINX Lua脚本可以很好地完成这项工作!但是,我坚信负载均衡器应该是简单的,并且只基于配置,而不需要脚本。由于负载均衡器是所有HTTP/WebSocket服务的入口点,你希望避免频繁的部署和自定义代码,并将缓存复杂性交给专门的中间件组件。

它是如何工作的?

Bloom安装在您的每个API工作程序的同一服务器上。从您的负载均衡器看,每个API工作程序都有一个Bloom实例。这样,您的负载均衡设置(例如,带有健康检查的轮询)不会被破坏。每个Bloom实例都可以设置为从其自己的LAN IP可见,然后这些Bloom实例可以指向本地回环上的API工作程序监听器。

Bloom充当自己的反向代理,并缓存读取HTTP方法(GETHEADOPTIONS),而直接代理HTTP写入方法(POSTPATCHPUT和其他方法)。所有Bloom实例共享LAN上可用的公共 redis 实例上的相同缓存存储。

Bloom是用Rust编写的,以确保内存安全、代码优雅,尤其是性能。Bloom可以编译成适用于您的服务器架构的本地代码。

布隆(Bloom)具有最小的静态配置,并依赖于您的API工作者提供的HTTP响应头来按响应配置缓存。这些HTTP头被布隆拦截,不会发送到您的负载均衡器响应。这些头部的格式为Bloom-Response-*。在向您的负载均衡器提供响应时,布隆设置一个缓存状态头,即Bloom-Status,可以在HTTP响应中公开看到(值可以是HITMISSDIRECT——它有助于调试您的缓存配置)。

Bloom Schema

如何使用它?

安装

布隆是用Rust编写的。要安装它,可以从布隆发布页面下载版本,使用cargo install或从master拉取源代码。

👉 每个发布二进制文件都包含一个.asc签名文件,可以使用@valeriansaliou GPG公钥进行验证:🔑valeriansaliou.gpg.pub.asc

从源代码安装

如果您已从Git拉取源代码,可以使用cargo构建它。

cargo build --release

构建的二进制文件可以在./target/release目录中找到。

从Cargo安装

您可以直接使用cargo install安装布隆。

cargo install bloom-server

请确保您的$PATH已正确配置为源Crate二进制文件,然后使用bloom命令运行布隆。

从软件包安装

Debian和Ubuntu软件包也可用。请参阅如何在Debian & Ubuntu上安装?部分。

从Docker Hub安装

您可能发现通过Docker运行布隆更方便。您可以在Docker Hub上找到预构建的布隆镜像,名为valeriansaliou/bloom

首先,拉取valeriansaliou/bloom镜像

docker pull valeriansaliou/bloom:v1.35.2

然后,提供一个配置文件并运行它(将/path/to/your/bloom/config.cfg替换为您配置文件的路径)

docker run -p 8080:8080 -p 8811:8811 -v /path/to/your/bloom/config.cfg:/etc/bloom.cfg valeriansaliou/bloom:v1.35.2

在配置文件中,请确保

  • server.inet设置为0.0.0.0:8080(这允许布隆从容器外部访问)
  • control.inet设置为0.0.0.0:8811(这允许布隆控制从容器外部访问)

布隆可以通过https://127.0.0.1:8080访问,布隆控制可以通过tcp://localhost:8811访问。

配置

使用示例配置文件config.cfg并调整到您自己的环境。

请确保正确配置 [proxy] 部分,以便 Bloom 指向您的 API 工作主机和端口。

可用选项

以下是对可用配置选项的注释,包括允许的值

[server]

  • log_level (类型:字符串,允许:debuginfowarnerror,默认:error) — 日志的详细程度,在生产环境中设置为 error
  • inet (类型:字符串,允许:IPv4 / IPv6 + 端口,默认:[::1]:8080) — Bloom 服务器应监听的宿主机和 TCP 端口

[control]

  • inet (类型:字符串,允许:IPv4 / IPv6 + 端口,默认:[::1]:8811) — Bloom 控制应监听的宿主机和 TCP 端口
  • tcp_timeout (类型:整数,允许:秒,默认:300) — 空闲/死客户端连接到 Bloom 控制的超时时间

[proxy]

  • shard_default (类型:整数,允许:015,默认:0) — 在代理 HTTP 请求中没有指定分片时使用的默认分片索引

[proxy.shard]

  • shard (类型:整数,允许:015,默认:0) — 分片索引(在请求 Bloom 时通过 Bloom-Request-Shard 路由)
  • host (类型:字符串,允许:主机名,IPv4,IPv6,默认:localhost) — 要代理到该分片的目标宿主机(即 API 监听的位置)
  • port (类型:整数,允许:TCP 端口,默认:3000) — 要代理到该分片的目标 TCP 端口(即 API 监听的位置)

[cache]

  • ttl_default (类型:整数,允许:秒,默认:600) — 默认缓存 TTL(秒),当没有提供 Bloom-Response-TTL
  • executor_pool (类型:整数,允许:0(2^16)-1,默认:16) — 缓存执行器池大小(可以同时执行多少个缓存请求)
  • disable_read (类型:布尔值,允许:truefalse,默认:false) — 是否禁用缓存读取(对测试很有用)
  • disable_write (类型: 布尔型, 允许值: true, false, 默认值: false) — 是否禁用缓存写入(适用于测试)
  • compress_body (类型: 布尔型, 允许值: true, false, 默认值: true) — 在存储时是否压缩正文(使用 Brotli;通常可以减少正文大小40%)

[redis]

  • host (类型: 字符串, 允许值: 主机名,IPv4,IPv6,默认值: localhost) — 目标 Redis 主机
  • port (类型: 整数, 允许值: TCP 端口,默认值: 6379) — 目标 Redis TCP 端口
  • password (类型: 字符串, 允许值: 密码值,默认值: 无) — Redis 密码(如果没有密码,不要设置此键)
  • database (类型: 整数, 允许值: 0255,默认值: 0) — 目标 Redis 数据库
  • pool_size (类型: 整数, 允许值: 0(2^32)-1,默认值: 80) — Redis 连接池大小(应略高于 cache.executor_pool,因为它同时被 Bloom 代理和 Bloom 控制使用)
  • max_lifetime_seconds (类型: 整数, 允许值: 秒,默认值: 60) — Redis 连接的最大存活时间(你希望它小于5分钟,因为这会影响连接断开后的重连延迟)
  • idle_timeout_seconds (类型: 整数, 允许值: 秒,默认值: 600) — 空闲/死亡连接池连接到 Redis 的超时时间
  • connection_timeout_seconds (类型: 整数, 允许值: 秒,默认值: 1) — 超时时间,在秒内认为 Redis 已死,并发出一个不使用缓存而直接连接到 API 的 DIRECT 连接(保持这个值较低,因为当 Redis 不可用时,它决定了在忽略 Redis 响应并直接代理之前需要等待多长时间)
  • max_key_size (类型: 整数, 允许值: 字节,默认值: 256000) — 在 Redis 中存储键的最大数据大小(字节,防止缓存非常大的响应)
  • max_key_expiration (类型: 整数, 允许值: 秒,默认值: 2592000) — 在 Redis 中缓存的键的最大 TTL(防止错误的 Bloom-Response-TTL 值)

环境变量

你可以在配置文件中使用 环境变量

这是一个使用环境变量的示例配置

[cache]
compress_body = "${BLOOM_COMPRESS_BODY}"

[redis]
host = "${BLOOM_REDIS_HOST}"

然后,你可以运行 Bloom,提供所有已声明的环境变量

BLOOM_REDIS_HOST=localhost BLOOM_COMPRESS_BODY=false \
  ./bloom -c /path/to/config.cfg

注意:它只能用于字符串和布尔值

运行 Bloom

Bloom 可以这样运行

./bloom -c/path/to/config.cfg

重要:确保为你的基础设施上运行的每个 API 工作进程启动一个 Bloom 实例。Bloom 本身不管理负载均衡逻辑,因此你应该为每个 API 工作进程实例有一个 Bloom 实例,并且仍然依赖于例如 NGINX 进行负载均衡。

配置负载均衡器

一旦Bloom启动并指向您的API,您就可以配置您的负载均衡器指向Bloom IP和端口(而不是之前指向的API IP和端口)。

NGINX说明

➡️ 配置现有的代理规则集

Bloom需要您的负载均衡器在代理客户端请求到Bloom时设置Bloom-Request-Shard HTTP头。此头告诉Bloom使用哪个缓存分片来存储数据(这样,您可以有一个用于不同API子系统的单个Bloom实例,它们在同一服务器上监听)。

# Your existing ruleset goes here
proxy_pass http://(...)

# Adds the 'Bloom-Request-Shard' header for Bloom
proxy_set_header Bloom-Request-Shard 0;

➡️ 调整现有的CORS规则(如果使用的话)

如果您的API运行在专用的主机名上(例如,https://api.crisp.chatCrisp),请不要忘记相应地调整您的CORS规则,以便API Web客户端(即浏览器)可以利用Bloom添加的ETag头。这将有助于在较慢的网络中加快API读取请求。 如果您没有现有的CORS规则,您可能不需要它们,所以忽略此条。

# Merge those headers with your existing CORS rules
add_header 'Access-Control-Allow-Headers' 'If-Match, If-None-Match' always;
add_header 'Access-Control-Expose-Headers' 'Vary, ETag' always;

请注意,分片号是0到15的整数(8位无符号数,限制为16个分片)。

Bloom添加的响应头包括

  • ETag:返回数据的唯一标识符(启用浏览器缓存);查看MDN
  • Vary:告诉其他缓存层(例如代理)ETag字段可能在每个请求中变化,因此它们需要重新验证它;查看MDN

浏览器添加的请求头,这是由于Bloom添加了上述请求头的结果

  • If-Match:客户端用于匹配给定的服务器ETag字段(在写入请求中);查看MDN
  • If-None-Match:客户端用于匹配给定的服务器ETag字段(在读取请求中);查看MDN

请注意,您需要将新的请求和响应头都添加到您的CORS规则中。如果您忘记了任何一个,对您的API的请求可能会在某些浏览器(例如,使用PATCH请求的Chrome)上开始失败。

配置您的API

现在,Bloom在您的API前面运行并在其 behalf 上提供服务;您的API可以指导Bloom如何根据每个响应的行为。

您的API可以向Bloom发送私有HTTP头,这些头由Bloom使用并在发送给请求客户端的响应中删除(Bloom-Response-* HTTP头)。

请注意,您的API不应以压缩格式提供响应。请禁用应用程序服务器上的任何Gzip或Brotli中间件,因为Bloom无法解码压缩的响应体。动态内容的压缩应由负载均衡器本身处理。

➡️ 不要缓存响应

要告诉Bloom不要缓存响应,将以下HTTP头作为API响应的一部分发送

Bloom-响应-忽略: 1

默认情况下,Bloom保留所有安全缓存的响应,只要它们匹配以下两个条件

1. 可缓存方法

  • GET
  • HEAD
  • OPTIONS

2. 可缓存状态

  • OK
  • Non-Authoritative Information
  • No Content
  • Reset Content
  • Partial Content
  • Multi-Status
  • Already Reported
  • Multiple Choices
  • Moved Permanently
  • Found
  • See Other
  • Permanent Redirect
  • Unauthorized
  • Payment Required
  • Forbidden
  • Not Found
  • Method Not Allowed
  • Gone
  • URIToo Long
  • Unsupported Media Type
  • 范围不满足
  • 期望失败
  • I‘m一壶茶
  • 锁定
  • 失败依赖
  • 需要前置条件
  • 请求头字段过大
  • 未实现
  • 未扩展

如需查找匹配的状态码,请参阅维基百科上的状态码列表

➡️ 在响应缓存上设置过期时间

要告诉Bloom在响应缓存上使用特定的过期时间(缓存失效后的时间,客户端请求时将获取新的响应),请将以下HTTP头作为API响应的一部分发送(此处为60秒的TTL)

Bloom-响应-TTL: 60

默认情况下,Bloom设置TTL为600秒(10分钟),但可以通过config.cfg进行配置。

➡️ 标记缓存响应(用于Bloom控制缓存清除)

如果您想使用Bloom控制来程序化清除缓存响应(见缓存能否程序化过期?),则需要在这些响应被缓存时进行标记。您可以告诉Bloom在1个或多个桶中对缓存响应进行标记,如下所示

Bloom-响应-:user_id:10012,heavy_route:1203

然后,当您需要清除标识为10012的用户标记的响应时,可以在user_id:10012桶上调用Bloom控制缓存清除。对于heavy_route:1203桶的操作流程类似。

默认情况下,缓存响应没有标签,因此无法通过Bloom控制直接清除。

如何在Debian & Ubuntu上安装它?

Bloom为基于Debian的系统(Debian、Ubuntu等)提供了预构建包

重要提示:Bloom目前只提供针对Debian 10、11 & 12的64位包(代号:busterbullseye & bookworm)。您仍然可以在其他Debian版本以及Ubuntu上使用它们。

1️⃣ 添加Bloom APT仓库(例如,对于Debian bookworm

echo "deb [signed-by=/usr/share/keyrings/valeriansaliou_bloom.gpg] https://packagecloud.io/valeriansaliou/bloom/debian/ bookworm main" > /etc/apt/sources.list.d/valeriansaliou_bloom.list
curl -fsSL https://packagecloud.io/valeriansaliou/bloom/gpgkey | gpg --dearmor -o /usr/share/keyrings/valeriansaliou_bloom.gpg
apt-get update

2️⃣ 安装Bloom包

apt-get install bloom

3️⃣ 编辑预填充的Bloom配置文件

nano /etc/bloom.cfg

4️⃣ 重新启动Bloom

service bloom restart

它有多快、多轻量?

Bloom是用Rust编写的,可以编译为您的架构的本地代码。与Golang等相比,Rust不携带GC(垃圾收集器),这对于高吞吐量/高负载的生产系统通常是个坏消息(因为GC会暂停所有程序指令执行一段时间,这取决于内存中保留的引用数量)。

请注意,Bloom在内存管理方面做出了一些妥协。为了简化,大量使用了堆分配对象。即您的API工作者的响应在发送给客户端之前完全缓存在内存中;这有好处,即可以从您的API工作者尽可能快地清除数据,即使请求客户端的带宽非常慢。

Crisp的生产环境中,我们运行了多个Bloom实例(每个API工作者一个)。每个实例处理约250个HTTP RPS(每秒请求数),以及约500个Bloom控制RPS(例如,缓存清除)。每个Bloom实例运行在单个2016 Xeon vCPU上,配以512MB RAM。Bloom处理的HTTP请求在读取(GETHEADOPTIONS)和写入(POSTPATCHPUT以及其他)之间平衡。

在如此负载下运行Bloom的服务器获得了以下htop反馈

htop

如您所见,Bloom 只消耗CPU时间的很小一部分(不到5%)和很小的RAM占用(大约5%,即大约25MB)。在这样的小型服务器上,我们可以预测Bloom可以扩展到更高的速率(例如,10k RPS),而不会对系统造成太大压力(底层NodeJS API工作进程会首先过热,因为它比Bloom重得多)。

如果您希望Bloom处理非常高的RPS,请确保将cache.executor_poolredis.pool_size选项调整到更高的值(如果您的Redis链接有少量延迟,这可能会限制您的RPS - 因为Redis连接是阻塞的)。

它是如何处理认证路由的?

认证路由通常由REST API用于返回请求者用户的私有数据。由于Bloom是一个缓存系统,防止从认证路由中发生缓存泄漏至关重要。Bloom通过为发送HTTP Authorization头部的请求隔离缓存来轻松解决这个问题。这是默认的安全行为。

如果请求的路由没有HTTP Authorization头(即请求是匿名/公开的),无论HTTP响应代码如何,Bloom都会将该响应缓存起来。

由于您的HTTP Authorization头包含敏感的认证数据(例如,用户名和密码),Bloom将这些值以散列形式存储在redis中(使用加密散列函数)。这样,即使您的redis数据库泄露,攻击者也无法恢复认证密钥对。

缓存可以被程序性地过期吗?

是的。由于您的现有API工作进程已经在他们的端执行数据库更新,因此它们已经非常清楚哪些数据 - 可能会被Bloom缓存 - 已过时。因此,Bloom提供了一种有效的方法来告诉它过期给定桶的缓存。这个系统被称为Bloom Control

Bloom可以配置为监听TCP套接字以公开缓存控制接口。默认TCP端口是8811。Bloom实现了一个基本的命令-确认协议。

这样,您的API工作进程(或您的基础设施中的任何其他工作进程)都可以告诉Bloom

  • 过期给定桶的缓存。请注意,由于给定的桶可能包含不同HTTP Authorization头部的缓存变体,当您清除桶的缓存时,会同时清除所有身份验证令牌的桶缓存。
  • 过期给定HTTP Authorization头部的缓存。如果用户注销并撤销他们的身份验证令牌,这很有用。

➡️ 可用命令

  • FLUSHB <namespace>:清除给定桶命名空间的缓存
  • FLUSHA <authorization>:清除给定授权的缓存
  • SHARD <shard>:选择要使用的连接分片
  • PING:ping服务器
  • QUIT:停止连接

⬇️ 控制流示例

telnet bloom.local 8811
Trying ::1...
Connected to bloom.local.
Escape character is '^]'.
CONNECTED <bloom v1.0.0>
HASHREQ hxHw4AXWSS
HASHRES 753a5309
STARTED
SHARD 1
OK
FLUSHB 2eb6c00c
OK
FLUSHA b44c6f8e
OK
PING
PONG
QUIT
ENDED quit
Connection closed by foreign host.

注意:在发出任何命令之前,Bloom要求客户端验证其哈希函数与Bloom内部哈希函数(使用HASHREQHASHRES交换)的一致性。使用FarmHash对键进行哈希,使用FarmHash.fingerprint32()计算结果,可能在不同的架构上有所不同。这样,可以预先防止大多数奇怪的Bloom Control问题。

📦 Bloom Control库

👉 找不到您编程语言的库?自己构建一个并在此处引用!(《联系我》)

🔥 报告漏洞

如果您在Bloom中发现了漏洞,欢迎您直接向 @valeriansaliou 报告,通过发送加密邮件至 [email protected]。请不要在公共GitHub问题中报告漏洞,因为它们可能会被恶意人士利用来攻击运行未修补Bloom实例的生产服务器。

⚠️ 您必须使用 @valeriansaliou GPG公钥加密您的邮件:[🔑valeriansaliou.gpg.pub.asc](https://valeriansaliou.name/files/keys/valeriansaliou.gpg.pub.asc)。

依赖项

~32MB
~622K SLoC