速率限制

Frappe 框架开箱即用地支持对 HTTP 请求进行速率限制。

Frappe 框架基于请求所消耗的时间实现固定窗口速率限制。该限制针对在配置窗口内所有 HTTP 请求所花费时间之和进行强制约束。每经过 window 秒后周期会重置,例如,将 window 设置为 3600 秒,将根据站点所在时区在每个小时开始时将使用计数器重置为 0。

注意:超过限制的请求不会被处理,并会返回 HTTP 429(请求过多)响应。

你可以通过在 site_config.json 中添加类似以下配置来为你的站点启用速率限制:

{
 "rate_limit": {
 "limit": 600,
 "window": 3600
 }
}
键 描述
limit 在速率限制窗口内允许使用的最大时间量(以秒为单位)。
window 速率限制窗口的大小(以秒为单位)。

每个 HTTP 请求返回的 HTTP 头都会显示当前的速率限制状态,例如

curl -i https://frappe.io/docs
HTTP/1.1 200 OK
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 518060453
X-RateLimit-Reset: 3513
X-RateLimit-Used: 100560

如果在配置的限制耗尽后仍有请求发出,则会返回 HTTP 429 响应以及速率限制状态:

curl -i https://frappe.io/docs
HTTP/1.1 429 TOO MANY REQUESTS
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1242
Retry-After: 1242
头 描述
Retry-After 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Limit 在一个速率限制窗口内允许使用的时间(以微秒为单位)。
X-RateLimit-Remaining 当前速率限制窗口中剩余(可使用)的时间(以微秒为单位)。
X-RateLimit-Reset 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Used 用于处理当前请求所使用的时间(以微秒为单位)。

特定端点的速率限制

站点范围的配置限制的是请求的总处理时间,而开发者可以使用位于 frappe.rate_limiter 中的 @rate_limit 装饰器,对特定的白名单 Python 方法应用严格的请求次数限制。

装饰器参数:

参数 类型 默认值 描述
key str None 来自 frappe.form_dict 的特定键,用于唯一标识该请求。
limit int 或 Callable 5 在时间窗口内允许的最大请求数。
seconds int 86400 时间窗口,以秒为单位(默认是 24 小时)。
methods str 或 list "ALL" 要进行速率限制的 HTTP 方法(例如 "POST" 或 ["GET", "POST"])。
ip_based bool True 按请求者的 IP 地址进行限制跟踪(网络感知)。
endpoint str None (在 v17 中添加)自定义计数器名称。如果可调用对象没有稳定的点分路径(例如 functools.partial),则此项为必填。
user_based bool False (在 v17 中添加)按已认证用户的会话 ID 进行限制跟踪(身份感知)。

跟踪行为与互斥性 (身份感知跟踪和安全访客回退从版本 17 开始可用。在 v15 或 v16 中使用 user_based 或 endpoint 会引发 TypeError)。

速率限制器旨在按网络身份或用户身份来跟踪请求,而不会同时按两者跟踪。

  • 身份感知(user_based=True):限制严格应用于已认证的 frappe.session.user。请求者的 IP 地址会被完全忽略,从而允许共享企业网络(NAT)中的用户避免无意中互相阻塞。
  • 网络感知(ip_based=True, user_based=False):限制严格应用于 request_ip。用户的会话 ID 会被忽略。这是默认的旧版行为。
  • 访客回退:如果某个端点配置了 user_based=True,未认证的“访客”流量将安全地回退为按其 IP 地址进行跟踪。这可以防止单个恶意访客耗尽全局访客配额,从而导致所有其他匿名用户遭遇拒绝服务。

示例实现:

from frappe.rate_limiter import rate_limit

# Limits authenticated users to 10 requests per minute per account.
# Guests hitting this mixed-auth endpoint are limited to 10 requests per minute per IP.
@frappe.whitelist()
@rate_limit(limit=10, seconds=60, user_based=True)
def generate_heavy_report():
    pass

速率限制

Frappe 框架开箱即用地支持对 HTTP 请求进行速率限制。

Frappe 框架基于请求所消耗的时间实现了固定窗口速率限制。该限制针对在配置窗口内所有 HTTP 请求所耗时间之和进行约束。每经过 window 秒后周期会重置,例如,将 window 设置为 3600 秒,就会根据站点时区在每个小时开始时将使用计数器重置为 0。

注意:超过限制的请求不会被处理,并会返回 HTTP 429(请求过多)响应。

你可以通过在 site_config.json 中添加类似以下配置来为你的站点启用速率限制:

{
 "rate_limit": {
 "limit": 600,
 "window": 3600
 }
}
键 描述
limit 在速率限制窗口内允许使用的最大时间量(以秒为单位)。
window 速率限制窗口的大小(以秒为单位)。

每个 HTTP 请求返回的 HTTP 头都会显示当前的速率限制状态,例如:

curl -i https://frappe.io/docs
HTTP/1.1 200 OK
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 518060453
X-RateLimit-Reset: 3513
X-RateLimit-Used: 100560

如果在配置的限制耗尽后仍有请求,则会返回 HTTP 429 响应以及速率限制状态:

curl -i https://frappe.io/docs
HTTP/1.1 429 TOO MANY REQUESTS
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1242
Retry-After: 1242
头部 描述
Retry-After 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Limit 在一个速率限制窗口内允许使用的时间(以微秒为单位)。
X-RateLimit-Remaining 当前速率限制窗口中剩余(可使用)的时间(以微秒为单位)。
X-RateLimit-Reset 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Used 处理当前请求所使用的时间(以微秒为单位)。

零停机迁移

零*停机迁移让站点在升级过程中以只读模式运行。这意味着你的静态站点、只需要读取数据的动态页面都可以继续正常工作,用户不会察觉到任何差异。任何需要写入数据库的操作都会被禁用,直到升级完成后才会恢复。

这是如何实现的

零停机迁移通过以下两种方式之一来启用只读模式:

  1. 使用只读副本。
  2. 使用数据库提供的“只读事务”。

无论采用哪种方式,在维护模式解除之前,数据库的写入操作都会被禁用。

在更新期间,网页端和 Desk 都会显示持续的只读警告。所有表单也会变为只读,因此你无法修改任何数据。

需要注意的事项

  • 并非所有应用都自动支持此功能。如果它们没有针对只读模式进行设计,可能会出现异常行为。
  • 在数据库架构更新期间,客户端应用也会被更新,因此你偶尔会遇到一些小问题。这些无法完全避免;此功能的目的仅仅是减少停机带来的影响。

在升级期间启用只读模式

  1. [可选] – 按照复制指南设置一个只读副本
  2. 编辑你的站点配置以启用此功能。 bench --site develop set-config allow_reads_during_maintenance 1

在你的应用中支持只读模式

如果你是一名应用开发者,并且想在你的应用中添加对此功能的支持,那么你需要修改你的应用

服务端更改

你可以使用 frappe.flags.read_only 来在只读模式下禁用或修改行为。

示例:记录页面访问

@frappe.whitelist()
def log_visit():
    if frappe.flags.read_only:
        return

    doc = frappe.new_doc("Doc Visit")
    doc.insert()

Jinja / 客户端页面更改

理想情况下,你应该更新 UI 以表明你的应用处于只读模式,当前无法执行写入操作。

网站上下文默认包含 read_only_mode 键;你可以利用它在页面上有条件地添加警告或切换功能。示例:

<!-- some html page -->
{% if read_only_mode %}
    <div class="alert">Site is running in read only mode. Full functionality will be restored soon.</div>
{% endif %}
<!-- other content -->

生产设置

Bench 是用于管理基于 Frappe 框架的站点部署的命令行工具。以下是在生产环境中部署 Frappe 站点的步骤。

安装 Bench

部署 Frappe 站点与在本地系统上搭建并没有太大区别。如果你的服务器是受支持的 Linux 发行版(Debian、Ubuntu、CentOS),可以使用 Easy Install 脚本安装 Bench。请确保向脚本传递 --production 标志。

sudo python install.py --production --user [frappe-user]

设置站点和应用

# change directory to frappe-bench
cd frappe-bench

# create a new site
bench new-site example.com

# download frappe apps or your custom-apps
bench get-app erpnext
bench get-app https://github.com/yourremote/yourapp.git

# install apps onto your site
bench --site example.com install-app erpnext yourapp

检查 supervisor

如果一切设置正确,你应该能在 supervisor 状态输出中看到 Frappe 进程。

$ sudo supervisorctl status
frappe-bench-redis:frappe-bench-redis-cache RUNNING pid 6467, uptime 10 days, 8:12:09
frappe-bench-redis:frappe-bench-redis-queue RUNNING pid 6466, uptime 10 days, 8:12:09
frappe-bench-redis:frappe-bench-redis-socketio RUNNING pid 6468, uptime 10 days, 8:12:09
frappe-bench-web:frappe-bench-frappe-web RUNNING pid 8856, uptime 10 days, 4:32:18
frappe-bench-web:frappe-bench-node-socketio RUNNING pid 8858, uptime 10 days, 4:32:18
frappe-bench-workers:frappe-bench-frappe-default-worker-0 RUNNING pid 8823, uptime 10 days, 4:32:19
frappe-bench-workers:frappe-bench-frappe-long-worker-0 RUNNING pid 8824, uptime 10 days, 4:32:19
frappe-bench-workers:frappe-bench-frappe-schedule RUNNING pid 8822, uptime 10 days, 4:32:19
frappe-bench-workers:frappe-bench-frappe-short-worker-0 RUNNING pid 8825, uptime 10 days, 4:32:19

如果你拥有 example.com 并且它已映射到服务器的 IP 地址,你的站点应该可以通过 example.com 访问。

更新

要更新你的站点,只需运行以下命令。它会更新你所有的应用(git pull),在所有站点上运行补丁,构建 JS 和 CSS 资源,并重启 supervisor。

# update everything
bench update

# update apps
bench update --pull

# run patches only
bench update --patch

# build assets only
bench update --build

# update bench (the cli)
bench update --bench

# update python packages and node_modules
bench update --requirements

Redis 命名空间

命名空间有助于保护用户数据。多个 Bench 可以共用同一个 Redis 实例,而无需担心命名空间冲突。你可以按照以下设置步骤在 Frappe 中启用 Redis 命名空间(目前 Frappe 在 Bench 级别支持 Redis 队列的命名空间)。

你可以使用 bench create-rq-users 命令行工具创建 Redis 用户,并使用凭据配置 Frappe。

bench create-rq-users

# Optional flags
# --use-rq-auth Enable Redis authentication for all bench sites
# --set-admin-password sets default user admin password

上述命令会在 Bench 配置目录中生成一个 ACL 文件。请确保 Redis_queue.conf 文件已配置为使用 ACL 文件(如果尚未配置的话)(或者,使用最新版本的 Bench CLI 运行 bench setup redis)。

aclfile [frappe-bench-absolute-path]/config/redis_queue.acl

数据库迁移

项目在开发过程中经常会遇到与数据库架构相关的变更。有时还需要对现有数据进行修补。Frappe 提供了迁移和补丁系统工具来处理这些情况。

当你的应用中有架构变更时,要将现有站点的数据库迁移到新架构,你应该运行以下命令。

bench --site [sitename] migrate

架构变更

你可以编辑 DocType 来添加、删除或修改字段。保存 DocType 时,包含 DocType 数据的 JSON 文件会被添加到应用的源代码树中。当你将应用添加到站点时,会使用这个 JSON 文件来安装 DocTypes(创建数据库表)。

要进行架构变更,你必须启用开发者模式。

运行 migrate 时,所有应用都会迁移到其当前版本。首先,我们运行 “before_migrate” 钩子来同步用户权限。然后运行实际的补丁,迁移各个应用。之后,我们同步以下组件:

  • 数据库架构
  • 后台任务
  • Fixtures(了解更多)
  • 仪表板、桌面图标和网页
  • 更新翻译
  • 为所有路由重建搜索索引

特别是对于 DocTypes,我们会将每个 DocType JSON 的 MD5 哈希值与存储在 DocType 数据库表中的哈希值进行比较。如果哈希值不匹配,我们会重新加载该 DocType。这种技术称为校验和比较,你可以在这里了解更多。最后,我们运行 “after_migrate” 钩子来完成迁移。你可以在 bench-site-commands 中找到各个进程运行的命令。

注意:在 v13 之前,DocTypes 是基于 JSON 文件中的修改时间戳来同步的。这种方法容易出错,所以我们改用了更可靠的哈希比较方法。你可以在这里了解更多。

当你删除或重命名 DocType 中的字段时,对应的数据库列不会从数据库表中删除,但它们不会在表单视图中显示。这样做是为了避免潜在的数据丢失情况,并允许你编写可能需要旧字段值的数据迁移(补丁)。

Frappe 不支持反向架构迁移。

数据迁移

在引入与数据相关的变更时,你可能需要运行一次性脚本来将现有数据更改为符合新代码预期的状态。在 Frappe 中,我们将这些脚本称为补丁。

编写补丁

要编写补丁,你必须在 Python 脚本中编写一个 execute 方法,并将其添加到应用的 patches.txt 中。

建议在文件路径中包含补丁编号和名称,并将其添加到应用的 patches 包(目录)中。然后你可以将补丁模块的点分路径添加到 patches.txt 中。

Frappe 中遵循的目录结构如下

frappe
└── patches
 └── v12_0
  └── my_awesome_patch.py

然后可以通过点分路径将补丁添加到 patches.txt 中。

frappe.patches.v12_0.my_awesome_patch

补丁执行期间的架构

execute 函数中可用的 DocType meta 将按照旧的 JSON 来提供。这样你就可以在编写迁移代码时假设仍然拥有旧字段。补丁运行完成后,新架构才会应用到 DocType。

如果你希望在补丁执行期间使用新架构,请使用 reload_doc 方法。

import frappe

def execute():
 frappe.reload_doc(module_name, "doctype", doctype_name)

 # your patch code here

模型同步后补丁 [仅限 v14 / develop 分支]

通常你的补丁可能不需要在 DocType 模型与数据库同步之前访问数据库架构。在这种情况下,最好将补丁放在 patches.txt 的 [post_model_sync] 部分中。

patches.txt 支持类似 INI 的文件格式,其中两个部分指定了补丁应该在何时运行——在 doctype 架构迁移之前或之后。模型同步后补丁不需要重新加载任何 doctypes,因为所有 doctypes 在执行它们之前都会被重新加载。以下是这种 patches.txt 文件的示例:

[pre_model_sync]
app.module.patch1
app.module.patch2

[post_model_sync]
app.module.patch3
app.module.patch4

一次性 Python 语句

你也可以使用以下语法在 patches.txt 中添加一次性 Python 语句,

frappe.patches.v12_0.my_awesome_patch
execute:frappe.delete_doc('Page', 'applications', ignore_missing=True)

补丁执行顺序

补丁按照定义的顺序运行。patches.txt 中的所有行必须是唯一的。如果某个补丁之前已经运行过,它就不会再次运行。如果你想再次运行某个补丁,可以添加一条注释,使该行看起来是新的。

例如,

frappe.patches.v12_0.my_awesome_patch #2019-09-08