You need to enable JavaScript to run this app. Elixir下如何拦截HTTP请求结合Hammer实现URL维度限流
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.11 18:36:06