Elixir下如何拦截HTTP请求结合Hammer实现URL维度限流
Elixir前置层限流与请求拦截实现方案
你遇到的问题本质是对Hammer的bucket ID参数存在理解偏差——Hammer的bucket ID没有任何强制格式约束,它本质就是一个用来区分不同限流分组的任意字符串,你完全可以把请求路径、客户端IP、查询参数等任何你需要的限流维度拼接成符合格式的字符串,直接作为入参传入check_rate函数,不存在"无法用请求URL作为限流维度"的限制。
要实现「Elixir前置层拦截请求→限流校验→放行转发到非Elixir业务服务」的流程,核心依赖三个能力的组合:
- 基于Plug构建请求处理管道,实现请求的全链路拦截、流程中断、响应返回
- 集成Hammer完成限流计数、规则判定的核心逻辑,不用重复造轮子实现滑动窗口、固定窗口的计数能力
- 基于轻量HTTP客户端实现请求透传,把通过校验的请求转发到后端实际业务服务
第一步:配置基础依赖
在项目的mix.exs文件中引入需要的依赖:
defp deps do [ # 限流核心库 {:hammer, "~> 6.1"}, # 构建HTTP服务的Plug基础组件 {:plug_cowboy, "~> 2.6"}, # 轻量HTTP客户端,用于请求转发 {:finch, "~> 0.18"}, # JSON序列化组件 {:jason, "~> 1.4"} ] end
在项目配置文件中初始化Hammer的运行参数,开发阶段可直接使用内存存储的ETS后端,生产环境可无缝切换为Redis后端:
# config/config.exs config :hammer, backend: {Hammer.Backend.ETS, [ expiry_ms: 60_000 * 60 * 2, sweep_interval_ms: 60_000 ]} # 配置Finch客户端 config :your_app, YourApp.Finch, adapter: Finch.SimpleAdapter
第二步:实现自定义限流拦截Plug
这部分是核心逻辑,重点演示如何把请求维度拼接成Hammer可用的bucket ID:
defmodule YourApp.RateLimiterPlug do import Plug.Conn @behaviour Plug @impl true def init(opts) do # 从初始化参数读取限流规则:时间窗口、窗口内最大请求数 limit = Keyword.get(opts, :max_requests, 100) window_ms = Keyword.get(opts, :window_ms, :timer.minute(1)) {limit, window_ms} end @impl true def call(conn, {limit, window_ms}) do # 从请求中提取需要参与限流计算的维度 client_ip = conn.remote_ip |> :inet.ntoa() |> to_string() request_method = conn.method request_path = conn.request_path # 如果需要按全量URL(含查询参数)限流,可额外提取query_string query_string = conn.query_string # 自由拼接成唯一标识限流分组的bucket ID,无格式强制要求 # 示例规则:按 客户端IP+请求方法+请求路径 做限流分组 bucket_id = "rate_group:#{client_ip}:#{request_method}:#{request_path}" # 调用Hammer的限流检查接口 case Hammer.check_rate(bucket_id, window_ms, limit) do {:allow, _current_count} -> # 未触发限流,直接放行到管道下一个环节(转发逻辑) conn {:deny, _max_limit} -> # 触发限流规则,直接返回429响应,中断后续流程 conn |> put_resp_content_type("application/json") |> send_resp(429, Jason.encode!(%{ error: "请求频率超出限制,请稍后再试" })) |> halt() end end end
这里需要明确:bucket_id的拼接规则完全可以根据业务需求调整,没有任何限制:
- 如果需要按全量URL限流,只要把
query_string拼到bucket_id字符串里即可 - 如果需要按路径前缀做聚合限流(比如所有
/api/user/开头的路径共用一个限流额度),只要提取路径的前N段做拼接即可 - 如果后续需要结合用户ID、租户ID做限流,只要从请求头或者鉴权结果里提取对应字段拼进bucket_id就行
第三步:实现请求转发逻辑
对通过限流校验的请求,直接透传到后端非Elixir业务服务即可,示例实现:
defmodule YourApp.ReverseProxyPlug do import Plug.Conn @behaviour Plug # 替换为实际业务服务的根地址 @backend_base_url "http://your-non-elixir-service.internal" @impl true def init(opts), do: opts @impl true def call(conn, _opts) do # 构造转发的完整目标地址 target_url = @backend_base_url <> conn.request_path <> "?" <> conn.query_string # 透传原请求的请求头 forward_headers = Enum.map(conn.req_headers, fn {key, value} -> {String.downcase(key), value} end) # 透传原请求体 request_body = conn.assigns[:raw_body] || "" # 发起请求到后端业务服务 {:ok, backend_resp} = Finch.build( conn.method, target_url, forward_headers, request_body ) |> Finch.request(YourApp.Finch) # 将后端服务的响应原样返回给客户端 conn |> put_resp_content_type(backend_resp.headers["content-type"]) |> send_resp(backend_resp.status, backend_resp.body) end end
第四步:组装请求处理管道
在项目的Endpoint配置中,按顺序把两个Plug串起来,保证请求先过限流校验,再进入转发逻辑:
defmodule YourApp.Endpoint do use Phoenix.Endpoint, otp_app: :your_app # 解析请求体,提前把原始body存入assigns供转发使用 plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Jason, body_reader: [length: 10_000_000] # 先执行限流拦截 plug YourApp.RateLimiterPlug, max_requests: 100, window_ms: :timer.minute(1) # 限流通过后执行转发逻辑 plug YourApp.ReverseProxyPlug end
内容的提问来源于stack exchange,提问作者S_rockz
相关产品推荐
相关产品推荐

