<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Envoy on Wilson Wu</title><link>https://wilsonwu.me/zh/tags/envoy/</link><description>Recent content in Envoy on Wilson Wu</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Mon, 17 Aug 2026 00:00:00 -0400</lastBuildDate><atom:link href="https://wilsonwu.me/zh/tags/envoy/index.xml" rel="self" type="application/rss+xml"/><item><title>使用 Envoy Ratelimit 构建支持 RPM/TPM 的分布式 AI 限流服务器</title><link>https://wilsonwu.me/zh/blog/2026/envoy-ai-ratelimit/</link><pubDate>Mon, 17 Aug 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/envoy-ai-ratelimit/</guid><description>&lt;p&gt;&lt;img alt="概览" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/envoy-ai-ratelimit/1-overview.png"&gt;&lt;/p&gt;
&lt;p&gt;在普通 HTTP API 中，我们通常只需要限制“每分钟请求数”。但对于大模型接口，仅限制请求数量远远不够：一次请求可能只消耗几十个 Token，也可能消耗数万个 Token。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="概览" loading="lazy" src="/zh/blog/2026/envoy-ai-ratelimit/1-overview.png"></p>
<p>在普通 HTTP API 中，我们通常只需要限制“每分钟请求数”。但对于大模型接口，仅限制请求数量远远不够：一次请求可能只消耗几十个 Token，也可能消耗数万个 Token。</p>
<p>因此，一个实用的 AI 网关通常需要同时支持：</p>
<ul>
<li><strong>RPM（Requests Per Minute）</strong>：每分钟请求数。</li>
<li><strong>TPM（Tokens Per Minute）</strong>：每分钟 Token 数。</li>
<li>按租户、API Key、模型或套餐分别计费。</li>
<li>多个 Envoy 实例共享同一份额度。</li>
<li>在高并发条件下避免各实例独立计数造成额度放大。</li>
<li>返回统一的 <code>429 Too Many Requests</code> 和剩余额度信息。</li>
</ul>
<p>本文介绍如何使用 <a href="https://github.com/envoyproxy/ratelimit">envoyproxy/ratelimit</a>、Redis 和 Envoy 构建一套分布式 AI 限流系统。</p>
<h2 id="为什么不能只使用-envoy-local-rate-limit">为什么不能只使用 Envoy Local Rate Limit</h2>
<p>Envoy 提供了 Local Rate Limit Filter，但它的计数器默认位于当前 Envoy 进程中。</p>
<p>假设我们有 4 个 Envoy 实例，并为某个租户配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>RPM = 100
</span></span></code></pre></div><p>如果每个 Envoy 都独立维护 100 RPM，那么整个集群实际上可能放行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>4 × 100 = 400 RPM
</span></span></code></pre></div><p>这显然不是我们想要的结果。</p>
<p>Envoy 的 Global Rate Limit Filter 可以把限流请求发送给外部 Rate Limit Service。<code>envoyproxy/ratelimit</code> 正是 Envoy 官方生态中常用的全局限流服务实现，它将计数器存放在 Redis 中。</p>
<p>整体架构如下：</p>
<p><img alt="架构" loading="lazy" src="/zh/blog/2026/envoy-ai-ratelimit/2-architect.png"></p>
<p>这里增加了一个很薄的 <strong>AI Quota Adapter</strong>，主要用于解决两个问题：</p>
<ol>
<li>从请求体中解析模型、租户和 Token 数量。</li>
<li>分别使用不同的 <code>hits_addend</code> 检查 RPM 和 TPM。</li>
</ol>
<h2 id="为什么-ai-tpm-需要额外的适配层">为什么 AI TPM 需要额外的适配层</h2>
<p>Envoy 的普通限流规则通常把一次 HTTP 请求视为一次命中：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>hits_addend = 1
</span></span></code></pre></div><p>这适合 RPM，但不适合 TPM。</p>
<p>例如下面两个请求：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>请求 A：消耗 200 Token
</span></span><span style="display:flex;"><span>请求 B：消耗 20,000 Token
</span></span></code></pre></div><p>如果两者都只增加一次计数，TPM 就没有意义。TPM 必须让一次请求按照 Token 数增加计数器：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>hits_addend = token_cost
</span></span></code></pre></div><p>例如：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>RPM 请求：hits_addend = 1
</span></span><span style="display:flex;"><span>TPM 请求：hits_addend = 3200
</span></span></code></pre></div><p>Rate Limit Service 的 <code>hits_addend</code> 通常作用于整个 <code>RateLimitRequest</code>，因此 RPM 和 TPM 需要使用两次限流调用，或者由自定义适配服务组合处理。</p>
<p>推荐的处理顺序是：</p>
<ol>
<li>检查并增加 RPM，<code>hits_addend=1</code>。</li>
<li>RPM 允许后，再检查并增加 TPM，<code>hits_addend=token_cost</code>。</li>
<li>任意一个返回 <code>OVER_LIMIT</code>，适配层向 Envoy 返回 429。</li>
</ol>
<p>这种模式下，每个维度都由 Redis 原子计数，多个 Envoy 实例不会各自放大额度。</p>
<p>需要注意：两次调用并不是跨计数器事务。RPM 已增加后，TPM 可能拒绝请求。因此要提前明确计数语义：</p>
<ul>
<li>RPM 统计所有进入限流流程的请求尝试；或者</li>
<li>自行扩展服务，使用一段 Redis Lua 脚本同时检查 RPM 和 TPM。</li>
</ul>
<p>多数系统接受第一种语义，因为它更简单，也能避免恶意用户通过大量超大请求免费消耗网关资源。</p>
<h2 id="设计限流-descriptor">设计限流 Descriptor</h2>
<p>限流维度通常包括：</p>
<ul>
<li><code>tenant</code>：租户、组织或 API Key。</li>
<li><code>model</code>：模型名称。</li>
<li><code>metric</code>：<code>rpm</code> 或 <code>tpm</code>。</li>
</ul>
<p>最终生成的 Descriptor 可以表示为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tenant = tenant-a
</span></span><span style="display:flex;"><span>model  = gpt-4.1
</span></span><span style="display:flex;"><span>metric = rpm
</span></span></code></pre></div><p>以及：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tenant = tenant-a
</span></span><span style="display:flex;"><span>model  = gpt-4.1
</span></span><span style="display:flex;"><span>metric = tpm
</span></span></code></pre></div><p>这样可以实现：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tenant-a + gpt-4.1 -&gt; 60 RPM
</span></span><span style="display:flex;"><span>tenant-a + gpt-4.1 -&gt; 100,000 TPM
</span></span></code></pre></div><p>如果不希望按模型拆分额度，也可以只保留：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tenant + metric
</span></span></code></pre></div><p>例如所有模型共享同一份租户额度。</p>
<h2 id="配置-envoyproxyratelimit">配置 envoyproxy/ratelimit</h2>
<p>创建 <code>config/config.yaml</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">domain</span>: <span style="color:#ae81ff">ai-gateway</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">tenant</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">model</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">metric</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">value</span>: <span style="color:#ae81ff">rpm</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">rate_limit</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">requests_per_unit</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">metric</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">value</span>: <span style="color:#ae81ff">tpm</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">rate_limit</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">requests_per_unit</span>: <span style="color:#ae81ff">100000</span>
</span></span></code></pre></div><p>这里 <code>tenant</code> 和 <code>model</code> 没有指定固定值，可用于根据请求中的 Descriptor 值进行分桶。实际部署前，应使用所选 ratelimit 版本提供的配置校验工具检查配置。</p>
<p>适配层发送 RPM Descriptor：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;tenant&#34;</span>, <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;tenant-a&#34;</span>},
</span></span><span style="display:flex;"><span>  {<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;model&#34;</span>, <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;gpt-4.1&#34;</span>},
</span></span><span style="display:flex;"><span>  {<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;metric&#34;</span>, <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;rpm&#34;</span>}
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>发送 TPM Descriptor：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;tenant&#34;</span>, <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;tenant-a&#34;</span>},
</span></span><span style="display:flex;"><span>  {<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;model&#34;</span>, <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;gpt-4.1&#34;</span>},
</span></span><span style="display:flex;"><span>  {<span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;metric&#34;</span>, <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;tpm&#34;</span>}
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>Descriptor 的顺序必须与配置中的嵌套路径一致。</p>
<h3 id="不同套餐额度">不同套餐额度</h3>
<p>如果不同租户拥有不同额度，可以增加套餐维度：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tenant -&gt; plan -&gt; model -&gt; metric
</span></span></code></pre></div><p>例如：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">domain</span>: <span style="color:#ae81ff">ai-gateway</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">tenant</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">plan</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">standard</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">model</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">metric</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">value</span>: <span style="color:#ae81ff">rpm</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">rate_limit</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">requests_per_unit</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">metric</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">value</span>: <span style="color:#ae81ff">tpm</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">rate_limit</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">requests_per_unit</span>: <span style="color:#ae81ff">100000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">plan</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">enterprise</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">model</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">descriptors</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">metric</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">value</span>: <span style="color:#ae81ff">rpm</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">rate_limit</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">requests_per_unit</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">metric</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">value</span>: <span style="color:#ae81ff">tpm</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">rate_limit</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">requests_per_unit</span>: <span style="color:#ae81ff">2000000</span>
</span></span></code></pre></div><p>请求中的 <code>tenant</code> 用于隔离计数器，<code>plan</code> 用于选择额度。</p>
<p>不要只使用 <code>plan</code> 作为计数维度，否则所有 Standard 用户可能共享同一个计数器。</p>
<h2 id="五使用-docker-compose-部署">五、使用 Docker Compose 部署</h2>
<p>下面是一个简化示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">redis</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">redis:7-alpine</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">redis-server</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">appendonly</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;yes&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">redis-data:/data</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;6379:6379&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ratelimit</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">envoyproxy/ratelimit:&lt;固定版本号&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>: <span style="color:#ae81ff">/bin/ratelimit</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">LOG_LEVEL</span>: <span style="color:#ae81ff">info</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">REDIS_SOCKET_TYPE</span>: <span style="color:#ae81ff">tcp</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">REDIS_URL</span>: <span style="color:#ae81ff">redis:6379</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">RUNTIME_ROOT</span>: <span style="color:#ae81ff">/data</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">RUNTIME_SUBDIRECTORY</span>: <span style="color:#ae81ff">ratelimit</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">RUNTIME_WATCH_ROOT</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">USE_STATSD</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./config:/data/ratelimit</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8081:8081&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;6070:6070&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">redis</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">redis-data</span>:
</span></span></code></pre></div><p>生产环境不要直接使用 <code>master</code> 或浮动标签，应固定到经过验证的 release 版本。</p>
<p>不同版本的镜像端口和环境变量可能存在差异，因此部署时应以对应版本仓库中的示例配置为准。</p>
<p>启动：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div><p>查看日志：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose logs -f ratelimit
</span></span></code></pre></div><p>生产环境可以运行多个 ratelimit 副本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ratelimit-1 ─┐
</span></span><span style="display:flex;"><span>ratelimit-2 ─┼── Redis
</span></span><span style="display:flex;"><span>ratelimit-3 ─┘
</span></span></code></pre></div><p>ratelimit 服务本身可以保持无状态，只要所有副本：</p>
<ul>
<li>使用相同的配置。</li>
<li>指向同一个 Redis 逻辑集群。</li>
<li>使用一致的 Redis Key 前缀或命名空间。</li>
</ul>
<h2 id="ai-quota-adapter-的核心逻辑">AI Quota Adapter 的核心逻辑</h2>
<p>适配层可以实现为 Envoy <code>ext_authz</code> 服务。它接收 Envoy 转发过来的请求信息，完成身份识别、Token 计算和额度检查。</p>
<p>下面是简化后的 Go 伪代码：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CheckAIQuota</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tenant</span> <span style="color:#66d9ef">string</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">model</span> <span style="color:#66d9ef">string</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tokenCost</span> <span style="color:#66d9ef">uint32</span>,
</span></span><span style="display:flex;"><span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 第一次调用：RPM</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">rpmResp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rateLimitClient</span>.<span style="color:#a6e22e">ShouldRateLimit</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ctx</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitRequest</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Domain</span>: <span style="color:#e6db74">&#34;ai-gateway&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Descriptors</span>: []<span style="color:#f92672">*</span><span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitDescriptor</span>{
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Entries</span>: []<span style="color:#f92672">*</span><span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitDescriptor_Entry</span>{
</span></span><span style="display:flex;"><span>                        {<span style="color:#a6e22e">Key</span>: <span style="color:#e6db74">&#34;tenant&#34;</span>, <span style="color:#a6e22e">Value</span>: <span style="color:#a6e22e">tenant</span>},
</span></span><span style="display:flex;"><span>                        {<span style="color:#a6e22e">Key</span>: <span style="color:#e6db74">&#34;model&#34;</span>, <span style="color:#a6e22e">Value</span>: <span style="color:#a6e22e">model</span>},
</span></span><span style="display:flex;"><span>                        {<span style="color:#a6e22e">Key</span>: <span style="color:#e6db74">&#34;metric&#34;</span>, <span style="color:#a6e22e">Value</span>: <span style="color:#e6db74">&#34;rpm&#34;</span>},
</span></span><span style="display:flex;"><span>                    },
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">HitsAddend</span>: <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">rpmResp</span>.<span style="color:#a6e22e">OverallCode</span> <span style="color:#f92672">==</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitResponse_OVER_LIMIT</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ErrRPMExceeded</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 第二次调用：TPM</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tpmResp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">rateLimitClient</span>.<span style="color:#a6e22e">ShouldRateLimit</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ctx</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitRequest</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Domain</span>: <span style="color:#e6db74">&#34;ai-gateway&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Descriptors</span>: []<span style="color:#f92672">*</span><span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitDescriptor</span>{
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Entries</span>: []<span style="color:#f92672">*</span><span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitDescriptor_Entry</span>{
</span></span><span style="display:flex;"><span>                        {<span style="color:#a6e22e">Key</span>: <span style="color:#e6db74">&#34;tenant&#34;</span>, <span style="color:#a6e22e">Value</span>: <span style="color:#a6e22e">tenant</span>},
</span></span><span style="display:flex;"><span>                        {<span style="color:#a6e22e">Key</span>: <span style="color:#e6db74">&#34;model&#34;</span>, <span style="color:#a6e22e">Value</span>: <span style="color:#a6e22e">model</span>},
</span></span><span style="display:flex;"><span>                        {<span style="color:#a6e22e">Key</span>: <span style="color:#e6db74">&#34;metric&#34;</span>, <span style="color:#a6e22e">Value</span>: <span style="color:#e6db74">&#34;tpm&#34;</span>},
</span></span><span style="display:flex;"><span>                    },
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">HitsAddend</span>: <span style="color:#a6e22e">tokenCost</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">tpmResp</span>.<span style="color:#a6e22e">OverallCode</span> <span style="color:#f92672">==</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ratelimitv3</span>.<span style="color:#a6e22e">RateLimitResponse_OVER_LIMIT</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ErrTPMExceeded</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>实际代码中的 protobuf 包名和字段类型应以项目锁定的 Envoy API 版本为准。</p>
<p>适配层的完整处理流程如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>1. 从 Authorization/API Key 中识别 tenant
</span></span><span style="display:flex;"><span>2. 从 JSON 请求体中读取 model
</span></span><span style="display:flex;"><span>3. 使用模型对应的 tokenizer 计算 input_tokens
</span></span><span style="display:flex;"><span>4. 根据策略计算 token_cost
</span></span><span style="display:flex;"><span>5. 调用 RLS 检查 RPM
</span></span><span style="display:flex;"><span>6. 调用 RLS 检查 TPM
</span></span><span style="display:flex;"><span>7. 都允许：返回 OK
</span></span><span style="display:flex;"><span>8. 任意超限：返回 429
</span></span></code></pre></div><h2 id="在-envoy-中接入适配层">在 Envoy 中接入适配层</h2>
<p>如果适配层实现 Envoy 的 <code>ext_authz</code> gRPC 协议，可以在 Envoy 中配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">http_filters</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">envoy.filters.http.ext_authz</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">typed_config</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;@type&#34;: </span><span style="color:#ae81ff">type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">transport_api_version</span>: <span style="color:#ae81ff">V3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">grpc_service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">envoy_grpc</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cluster_name</span>: <span style="color:#ae81ff">ai_quota_adapter</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">0.</span><span style="color:#ae81ff">5s</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">with_request_body</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">max_request_bytes</span>: <span style="color:#ae81ff">1048576</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">allow_partial_message</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pack_as_bytes</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">failure_mode_allow</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">envoy.filters.http.router</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">typed_config</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;@type&#34;: </span><span style="color:#ae81ff">type.googleapis.com/envoy.extensions.filters.http.router.v3.Router</span>
</span></span></code></pre></div><p>对应的 Cluster：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">clusters</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ai_quota_adapter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">STRICT_DNS</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">connect_timeout</span>: <span style="color:#ae81ff">0.</span><span style="color:#ae81ff">25s</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">http2_protocol_options</span>: {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">load_assignment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cluster_name</span>: <span style="color:#ae81ff">ai_quota_adapter</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">endpoints</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">lb_endpoints</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">endpoint</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">address</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">socket_address</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">address</span>: <span style="color:#ae81ff">ai-quota-adapter</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">port_value</span>: <span style="color:#ae81ff">9000</span>
</span></span></code></pre></div><p>因为 Token 计算需要读取 JSON 请求体，所以需要配置 <code>with_request_body</code>。</p>
<p>注意限制最大请求体，避免攻击者提交超大 Prompt 导致 Envoy 或适配层内存压力。</p>
<h2 id="tpm-应该如何计算">TPM 应该如何计算</h2>
<p>RPM 的定义比较简单，一次请求增加 1。</p>
<p>TPM 更复杂，因为请求发出时通常只知道输入 Token，尚不知道模型最终会产生多少输出 Token。</p>
<h3 id="方案一只计算输入-token">方案一：只计算输入 Token</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>token_cost = input_tokens
</span></span></code></pre></div><p>优点：</p>
<ul>
<li>请求开始前就能精确计算。</li>
<li>实现最简单。</li>
<li>非流式和流式接口都适用。</li>
</ul>
<p>缺点：</p>
<ul>
<li>无法限制输出 Token。</li>
<li>用户可能通过很短的 Prompt 请求大量输出。</li>
</ul>
<p>适合将 TPM 定义为“输入 Token 配额”的系统。</p>
<h3 id="方案二预留最大输出-token">方案二：预留最大输出 Token</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>token_cost = input_tokens + requested_max_output_tokens
</span></span></code></pre></div><p>例如：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>input_tokens = 1200
</span></span><span style="display:flex;"><span>max_tokens   = 4096
</span></span><span style="display:flex;"><span>token_cost   = 5296
</span></span></code></pre></div><p>优点：</p>
<ul>
<li>在调用模型前就能做严格准入控制。</li>
<li>不会因为输出 Token 未知而突破额度。</li>
<li>适合流式响应。</li>
</ul>
<p>缺点：</p>
<ul>
<li>实际只输出 300 Token 时仍预留了 4096 Token。</li>
<li>如果不实现退款，额度利用率会偏低。</li>
</ul>
<p>这是最容易实现“严格不超额”的方案。</p>
<p>网关必须对 <code>max_tokens</code> 设置服务端上限，不能完全信任客户端：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>effective_max_tokens = min(client_max_tokens, server_model_limit)
</span></span></code></pre></div><p>否则客户端可以提交异常大的 <code>max_tokens</code>，让自己或整个租户迅速耗尽配额。</p>
<h3 id="方案三先预留再按实际用量结算">方案三：先预留，再按实际用量结算</h3>
<p>完整思路是：</p>
<ol>
<li>请求开始时预留：</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>input_tokens + max_output_tokens
</span></span></code></pre></div><ol start="2">
<li>响应结束后得到实际输出 Token。</li>
<li>退还未使用部分：</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>refund = max_output_tokens - actual_output_tokens
</span></span></code></pre></div><p>但 <code>envoyproxy/ratelimit</code> 的标准限流调用主要面向正向增加计数，并不天然提供完整的“预留—提交—退款”事务模型。</p>
<p>如果业务必须支持精确退款，通常需要：</p>
<ul>
<li>在适配层实现额外的 Redis Lua 脚本。</li>
<li>使用独立的预留记录和过期时间。</li>
<li>请求结束后进行 commit/refund。</li>
<li>使用请求 ID 保证幂等。</li>
</ul>
<p>此时 <code>envoyproxy/ratelimit</code> 仍可负责 RPM 或基础 TPM，但结算逻辑已经属于定制配额服务。</p>
<h3 id="方案四流式增量扣费">方案四：流式增量扣费</h3>
<p>对于流式输出，可以：</p>
<ol>
<li>请求开始时扣除输入 Token。</li>
<li>每生成一批输出 Token，再增加一次 TPM 计数。</li>
<li>一旦超过额度，终止上游流。</li>
</ol>
<p>例如每 32 或 128 个 Token 扣一次。</p>
<p>这种方式需要能观察响应流，因此仅靠请求阶段的 <code>ext_authz</code> 不够，需要：</p>
<ul>
<li>Envoy <code>ext_proc</code>；</li>
<li>Wasm Filter；</li>
<li>自定义 HTTP Filter；</li>
<li>或由 AI Gateway 自身代理并读取流式响应。</li>
</ul>
<p>增量批次越小，控制越精确，但 gRPC 和 Redis 调用次数也越多。</p>
<h2 id="多-envoy-实例如何保证共享额度">多 Envoy 实例如何保证共享额度</h2>
<p>假设一个租户拥有：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>RPM = 100
</span></span><span style="display:flex;"><span>TPM = 200,000
</span></span></code></pre></div><p>请求可能随机落到不同 Envoy：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Request 1 -&gt; Envoy A
</span></span><span style="display:flex;"><span>Request 2 -&gt; Envoy B
</span></span><span style="display:flex;"><span>Request 3 -&gt; Envoy C
</span></span></code></pre></div><p>但所有限流请求最终都会访问同一套 Redis 计数器：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ai-gateway / tenant-a / gpt-4.1 / rpm
</span></span><span style="display:flex;"><span>ai-gateway / tenant-a / gpt-4.1 / tpm
</span></span></code></pre></div><p>因此额度是整个集群共享的，而不是每个 Envoy 各有一份。</p>
<p>可以横向扩展的部分包括：</p>
<ul>
<li>Envoy。</li>
<li>AI Quota Adapter。</li>
<li>envoyproxy/ratelimit。</li>
</ul>
<p>不能随意拆分的是同一个配额 Key 的计数状态。如果使用多个完全独立的 Redis，则必须确保相同租户、模型和指标始终路由到同一个分片，否则额度又会被放大。</p>
<h2 id="返回标准限流响应">返回标准限流响应</h2>
<p>发生超限时，适配层应向 Envoy 返回 429：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-http" data-lang="http"><span style="display:flex;"><span><span style="color:#66d9ef">HTTP</span><span style="color:#f92672">/</span><span style="color:#ae81ff">1.1</span> <span style="color:#ae81ff">429</span> <span style="color:#a6e22e">Too Many Requests</span>
</span></span><span style="display:flex;"><span>Content-Type<span style="color:#f92672">:</span> <span style="color:#ae81ff">application/json</span>
</span></span><span style="display:flex;"><span>Retry-After<span style="color:#f92672">:</span> <span style="color:#ae81ff">12</span>
</span></span></code></pre></div><p>响应体可以采用 OpenAI 风格：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Rate limit exceeded for tenant tenant-a&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;rate_limit_error&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;code&#34;</span>: <span style="color:#e6db74">&#34;tpm_limit_exceeded&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>建议分别暴露 RPM 和 TPM 信息：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-http" data-lang="http"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Limit-Requests: 60
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Remaining-Requests: 12
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Reset-Requests: 18
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Limit-Tokens: 100000
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Remaining-Tokens: 24580
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Reset-Tokens: 18
</span></span></span></code></pre></div><p>为了避免客户端形成同步重试风暴，SDK 应使用带随机抖动的指数退避：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>sleep = random(0, min(cap, base × 2^retry_count))
</span></span></code></pre></div><h2 id="精准限流需要明确的边界">“精准限流”需要明确的边界</h2>
<p>使用 Redis 共享计数器可以解决多实例额度放大的问题，但“精准”仍然需要明确具体含义。</p>
<h3 id="1-它通常是固定时间窗">1. 它通常是固定时间窗</h3>
<p>配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">unit</span>: <span style="color:#ae81ff">minute</span>
</span></span></code></pre></div><p>通常表示分钟级固定时间窗，不等同于严格的滑动窗口。</p>
<p>用户可能在一个窗口结束前发送 100 次请求，并在新窗口开始后立刻再发送 100 次。从任意连续 60 秒观察，流量可能接近配置值的两倍。</p>
<p>因此，该方案能够保证：</p>

<blockquote>
  <p>所有 Envoy 实例共享同一个固定时间窗计数器。</p>

</blockquote>
<p>但它不自动等于：</p>

<blockquote>
  <p>任意连续 60 秒都绝不超过限制。</p>

</blockquote>
<p>如果业务要求严格滑动窗口，需要使用 Redis Sorted Set、滑动窗口计数器或专门的配额算法。</p>
<h3 id="2-输出-token-在请求开始时未知">2. 输出 Token 在请求开始时未知</h3>
<p>如果 TPM 包含输出 Token，则无法在生成之前知道最终消耗量。</p>
<p>所谓严格控制通常意味着：</p>
<ul>
<li>使用 <code>max_tokens</code> 预留；或者</li>
<li>在流式生成过程中增量扣费。</li>
</ul>
<p>单纯在响应结束后上报实际 Token，只能用于计费和统计，不能阻止当前请求突破 TPM。</p>
<h3 id="3-rpm-和-tpm-不是一个原子事务">3. RPM 和 TPM 不是一个原子事务</h3>
<p>标准 RLS 请求中的 <code>hits_addend</code> 是统一的，而 RPM 和 TPM 需要不同增量，因此通常需要两次调用。</p>
<p>如果必须保证“RPM 和 TPM 同时检查、同时增加、任意失败都不修改”，需要定制服务并使用单个 Redis Lua 脚本完成事务。</p>
<h2 id="生产环境中的关键问题">生产环境中的关键问题</h2>
<h3 id="1-请求重试和重复扣费">1. 请求重试和重复扣费</h3>
<p>Rate Limit Service 的计数调用通常不是业务级幂等操作。</p>
<p>如果适配层因为超时重试同一个请求：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>第一次调用已经写入 Redis，但响应丢失
</span></span><span style="display:flex;"><span>第二次调用再次增加计数
</span></span></code></pre></div><p>就会产生重复扣费。</p>
<p>建议：</p>
<ul>
<li>限流调用不要盲目自动重试。</li>
<li>使用 Envoy 的 <code>x-request-id</code> 或业务请求 ID。</li>
<li>对严格计费场景，在适配层中实现请求幂等记录。</li>
<li>幂等记录过期时间至少覆盖限流窗口和最大请求时长。</li>
</ul>
<h3 id="2-故障时-fail-open-还是-fail-closed">2. 故障时 Fail Open 还是 Fail Closed</h3>
<p>当 Redis 或 ratelimit 服务不可用时，可以选择：</p>
<p><strong>Fail Open：</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>限流系统不可用时继续放行
</span></span></code></pre></div><p>优点是可用性高，缺点是可能导致成本失控。</p>
<p><strong>Fail Closed：</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>限流系统不可用时拒绝请求
</span></span></code></pre></div><p>优点是保护成本，缺点是限流组件故障会影响业务。</p>
<p>AI 推理通常成本较高，面向外部用户的付费 API 更适合 Fail Closed。内部低风险环境可以考虑短时间 Fail Open，并配合熔断和告警。</p>
<h3 id="3-tokenizer-必须与模型匹配">3. Tokenizer 必须与模型匹配</h3>
<p>不同模型的 Tokenizer 可能不同。不能使用简单的字符数除以固定比例来实现严格 TPM。</p>
<p>应该维护：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>model -&gt; tokenizer
</span></span></code></pre></div><p>例如：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>gpt-family       -&gt; cl100k/o200k 类 tokenizer
</span></span><span style="display:flex;"><span>llama-family     -&gt; 对应 SentencePiece/BPE tokenizer
</span></span><span style="display:flex;"><span>自定义模型        -&gt; 模型实际 tokenizer.json
</span></span></code></pre></div><p>如果 tokenizer 不可用，可以做保守估算，但必须预留误差：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>estimated_tokens = ceil(raw_estimate × safety_factor)
</span></span></code></pre></div><h3 id="4-规范化模型名称">4. 规范化模型名称</h3>
<p>客户端可能使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>gpt-4.1
</span></span><span style="display:flex;"><span>gpt-4.1-2025-04-14
</span></span><span style="display:flex;"><span>my-gpt-alias
</span></span></code></pre></div><p>限流前应将它们映射到统一的配额名称：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>my-gpt-alias -&gt; gpt-4.1
</span></span></code></pre></div><p>否则用户可能通过切换别名绕过同一模型的额度。</p>
<h3 id="5-不要信任客户端提供的-token-数">5. 不要信任客户端提供的 Token 数</h3>
<p>不能让客户端直接提交：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-http" data-lang="http"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-Token-Cost: 1
</span></span></span></code></pre></div><p>然后按该值扣费。</p>
<p>Token 数必须由可信组件计算，或者由上游模型返回后进行校验。客户端声明只能作为参考，不能作为计费依据。</p>
<h3 id="6-配置发布必须一致">6. 配置发布必须一致</h3>
<p>如果多个 ratelimit 副本加载了不同配置，可能出现：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>副本 A：RPM 100
</span></span><span style="display:flex;"><span>副本 B：RPM 200
</span></span></code></pre></div><p>即使 Redis 相同，结果也会变得不可预测。</p>
<p>生产环境应：</p>
<ul>
<li>使用固定版本配置。</li>
<li>进行启动前校验。</li>
<li>滚动发布时保持向后兼容。</li>
<li>为配置添加版本号和监控指标。</li>
</ul>
<h2 id="监控指标">监控指标</h2>
<p>至少应监控：</p>
<h3 id="envoy">Envoy</h3>
<ul>
<li>ext_authz 调用次数。</li>
<li>ext_authz 超时和失败次数。</li>
<li>HTTP 429 数量。</li>
<li>上游请求数和并发数。</li>
<li>请求体缓冲失败次数。</li>
</ul>
<h3 id="ai-quota-adapter">AI Quota Adapter</h3>
<ul>
<li>RPM 允许和拒绝次数。</li>
<li>TPM 允许和拒绝次数。</li>
<li>Token 计算耗时。</li>
<li>RLS gRPC 延迟。</li>
<li>Tokenizer 加载失败次数。</li>
<li>按模型和租户聚合的 Token 消耗。</li>
<li>重复请求或幂等命中次数。</li>
</ul>
<h3 id="ratelimit">Ratelimit</h3>
<ul>
<li><code>OK</code>、<code>OVER_LIMIT</code>、错误数量。</li>
<li>Redis 请求延迟。</li>
<li>Redis 连接池使用率。</li>
<li>配置加载状态。</li>
<li>各限流规则命中次数。</li>
</ul>
<h3 id="redis">Redis</h3>
<ul>
<li>CPU 和内存。</li>
<li>命令延迟。</li>
<li>连接数。</li>
<li>Key 数量。</li>
<li>Key 过期速率。</li>
<li>主从复制延迟。</li>
<li>Failover 次数。</li>
</ul>
<p>高基数租户 ID 不适合直接作为 Prometheus Label。否则租户数量较多时会产生严重的指标基数问题。按租户的详细用量更适合写入日志、ClickHouse 或专门的计量系统。</p>
<h2 id="推荐的最终实现">推荐的最终实现</h2>
<p>对于大多数 AI API 网关，可以采用下面的组合：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Envoy
</span></span><span style="display:flex;"><span>  └── ext_authz
</span></span><span style="display:flex;"><span>        └── AI Quota Adapter
</span></span><span style="display:flex;"><span>              ├── 解析租户和模型
</span></span><span style="display:flex;"><span>              ├── 使用可信 tokenizer 计算 input_tokens
</span></span><span style="display:flex;"><span>              ├── 规范化并限制 max_tokens
</span></span><span style="display:flex;"><span>              ├── RPM：RLS hits_addend = 1
</span></span><span style="display:flex;"><span>              └── TPM：RLS hits_addend =
</span></span><span style="display:flex;"><span>                       input_tokens + max_output_tokens
</span></span><span style="display:flex;"><span>                        └── envoyproxy/ratelimit
</span></span><span style="display:flex;"><span>                              └── Redis
</span></span></code></pre></div><p>这一方案具备以下特征：</p>
<ul>
<li>多 Envoy 实例共享统一额度。</li>
<li>RPM 和 TPM 分别计数。</li>
<li>支持按租户、模型、套餐隔离。</li>
<li>在请求进入模型服务之前完成准入控制。</li>
<li>通过预留最大输出 Token，保证不会因为流式输出突破 TPM。</li>
<li>Envoy、适配层和 ratelimit 服务都可以横向扩展。</li>
</ul>
<p>如果还需要按照实际输出 Token 精确结算，则在此基础上增加：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>预留记录 + 响应用量采集 + commit/refund + 请求幂等
</span></span></code></pre></div><p>此时系统已经从“限流服务”演进为一套完整的“分布式 AI 配额与计量服务”。</p>
<h2 id="总结">总结</h2>
<p><code>envoyproxy/ratelimit</code> 非常适合解决多 Envoy 实例之间的全局共享限流问题。它通过 Redis 集中维护计数器，避免每个网关实例独立计算额度。</p>
<p>对于 AI 场景，需要在标准 HTTP 限流之上增加 Token 感知能力：</p>
<ul>
<li>RPM 使用 <code>hits_addend=1</code>。</li>
<li>TPM 使用 <code>hits_addend=token_cost</code>。</li>
<li>使用租户、模型和指标构建 Descriptor。</li>
<li>请求阶段通过输入 Token或最大输出 Token预留完成准入。</li>
<li>多 Envoy、多个适配层和多个 RLS 副本共享同一个 Redis 计数空间。</li>
</ul>
<p>需要特别注意的是，分布式共享计数、固定时间窗、RPM/TPM 组合事务以及实际输出 Token 结算是四个不同问题。理解这些边界，才能构建一套既能保护模型成本、又能在高并发环境中稳定运行的 AI 限流系统。</p>
]]></content:encoded></item><item><title>Kubernetes Ingress NGINX 退役：全面迁移到 Gateway API 的方案与实践指南</title><link>https://wilsonwu.me/zh/blog/2025/ingress-to-gateway-api/</link><pubDate>Mon, 01 Dec 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/ingress-to-gateway-api/</guid><description>&lt;p&gt;Kubernetes 官方在 &lt;strong&gt;2025 年 11 月 11 日&lt;/strong&gt;发布博客，正式宣布 &lt;strong&gt;Ingress NGINX 项目进入退役（Retirement）阶段&lt;/strong&gt;，并将于 &lt;strong&gt;2026 年 3 月&lt;/strong&gt;彻底停止维护。&lt;/p&gt;
&lt;p&gt;这一举措标志着 Kubernetes 在集群入口与流量管理方面正式进入 &lt;strong&gt;Gateway API 时代&lt;/strong&gt;。对于正在使用 Ingress NGINX 的团队，这不仅是一次技术升级，更是一项需要尽快规划的风险管理工作。&lt;/p&gt;</description><content:encoded><![CDATA[<p>Kubernetes 官方在 <strong>2025 年 11 月 11 日</strong>发布博客，正式宣布 <strong>Ingress NGINX 项目进入退役（Retirement）阶段</strong>，并将于 <strong>2026 年 3 月</strong>彻底停止维护。</p>
<p>这一举措标志着 Kubernetes 在集群入口与流量管理方面正式进入 <strong>Gateway API 时代</strong>。对于正在使用 Ingress NGINX 的团队，这不仅是一次技术升级，更是一项需要尽快规划的风险管理工作。</p>
<p>本文将基于 Kubernetes 官方退役公告、Gateway API 最新生态与社区最佳实践，系统介绍：</p>
<ul>
<li>为什么 Ingress NGINX 被退休？</li>
<li>Ingress 模型的根本局限与 Gateway API 的优势</li>
<li>新的推荐替代方案：Envoy Gateway（基于 Gateway API）</li>
<li>如何从 Ingress 迁移到 Gateway API（完整迁移路径）</li>
<li>关键注意事项与生产实践建议</li>
</ul>
<h2 id="ingress-nginx-为什么从-kubernetes-正式退役">Ingress NGINX 为什么从 Kubernetes 正式退役？</h2>
<p>根据官方博客（2025–11–11），Ingress NGINX 的退役原因包括：</p>
<h3 id="-1-项目长期缺乏维护者">● 1. 项目长期缺乏维护者</h3>
<p>虽然 Ingress NGINX 使用极广，但维护压力巨大，而核心维护者数量越来越少，社区贡献下滑。项目经过数月讨论，确定无法持续投入。</p>
<h3 id="-2-无法跟上-kubernetes-网络模型的发展">● 2. 无法跟上 Kubernetes 网络模型的发展</h3>
<p>Ingress 诞生于 Kubernetes 早期，仅支持 HTTP(s) 基础路由；而随着 Service Mesh、多协议服务、网关统一治理等场景爆发，Ingress 模型难以满足现代需求。</p>
<h3 id="-3-功能严重依赖-controller-私有扩展">● 3. 功能严重依赖 Controller 私有扩展</h3>
<p>如 NGINX Ingress 的大量 Annotation、Rewrite、Header 管理等，使得 Ingress 配置在不同控制器之间高度不兼容，难以形成云原生网络的统一抽象。</p>
<h3 id="-4-gateway-api-已经成熟能够全面替代">● 4. Gateway API 已经成熟，能够全面替代</h3>
<p>Gateway API 于 2023–2024 进入 GA，解决了 Ingress 的所有痛点，是 Kubernetes SIG-Network 未来 10 年的官方战略方向。</p>

<blockquote>
  <p>官方公告明确：</p>
<ul>
<li><strong>从 2026 年 3 月起将不再提供任何补丁，包括安全补丁。</strong></li>
<li>项目代码、容器镜像、Helm Chart 虽然仍可访问，但不会再更新。</li>
<li>官方建议用户“尽快迁移”。</li>
</ul>

</blockquote>
<h2 id="ingress-的局限性为什么它必须退出历史舞台">Ingress 的局限性：为什么它必须退出历史舞台？</h2>
<p>Ingress 的设计非常简单，只能做最基础的 L7 HTTP(s) 路由：</p>
<table>
	<thead>
			<tr>
					<th>能力</th>
					<th>Ingress</th>
					<th>Gateway API</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Host / Path 路由</td>
					<td>✔</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>Header 匹配</td>
					<td>❌</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>Query 匹配</td>
					<td>❌</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>HTTP Method 匹配</td>
					<td>❌</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>流量拆分 / 灰度发布</td>
					<td>❌</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>多监听端口</td>
					<td>❌</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>TCP / UDP / gRPC 多协议</td>
					<td>❌</td>
					<td>✔</td>
			</tr>
			<tr>
					<td>多租户 / RBAC 边界</td>
					<td>弱</td>
					<td>强</td>
			</tr>
			<tr>
					<td>可扩展性</td>
					<td>Annotation 混乱且不兼容</td>
					<td>CRD 标准扩展</td>
			</tr>
	</tbody>
</table>
<p>Ingress 的能力已无法支撑现代云原生系统的需求，而且控制器之间的行为极不一致。Gateway API 的出现正是为了解决上述所有问题。</p>
<p><img alt="架构对比" loading="lazy" src="/zh/blog/2025/ingress-to-gateway-api/1-ingress-vs-gateway-api.png"></p>
<h2 id="推荐替代方案envoy-gateway--gateway-api">推荐替代方案：Envoy Gateway + Gateway API</h2>
<p>目前官方推荐的方向是：<strong>转向 Gateway API，并选择任意兼容的 Gateway 实现</strong>。</p>
<p>在众多实现中，我们推荐 <strong>Envoy Gateway</strong>，原因包括：</p>
<h3 id="1-对-gateway-api-支持最完整">1. 对 Gateway API 支持最完整</h3>
<p>完美支持 HTTPRoute / TCPRoute / GRPCRoute 这是 Ingress 无法做到的。</p>
<h3 id="2-基于-envoy-proxy性能与可扩展性行业领先">2. 基于 Envoy Proxy，性能与可扩展性行业领先</h3>
<p>Envoy 是 CNCF 毕业项目，得到大规模生产验证。</p>
<h3 id="3-社区活跃长期维护无风险">3. 社区活跃，长期维护无风险</h3>
<p>与 Ingress NGINX 的“维护者不足”相比，Envoy Gateway 是明确的长期支持项目。</p>
<h3 id="4-与-service-mesh如-istio原生兼容">4. 与 Service Mesh（如 Istio）原生兼容</h3>
<p>Gateway API 未来将成为 Service Mesh 入口的新标准。</p>
<p>换句话说：<strong>Envoy Gateway 是最稳妥的迁移目标</strong>。</p>
<p><img alt="Envoy Gateway 架构" loading="lazy" src="/zh/blog/2025/ingress-to-gateway-api/2-envoy-gateway-architecture.png"></p>
<h2 id="迁移路径从-ingress-nginx--gateway-apienvoy-gateway">迁移路径：从 Ingress NGINX → Gateway API（Envoy Gateway）</h2>
<p>大多数企业推荐采用 <strong>双栈迁移</strong>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Ingress（旧） 和 Gateway API（新） 并存 → 服务逐个迁移 → 完全切流 → 卸载 Ingress NGINX
</span></span></code></pre></div><p>以下是完整步骤。</p>
<h2 id="步骤-1安装-gateway-api-和-envoy-gateway">步骤 1：安装 Gateway API 和 Envoy Gateway</h2>
<p>安装标准 Gateway API CRD：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/latest/download/standard-install.yaml
</span></span></code></pre></div><p>然后安装 Envoy Gateway 控制器（官方提供 Helm / YAML）。</p>
<h2 id="步骤-2创建-gateway替代-ingress-controller--loadbalancer">步骤 2：创建 Gateway（替代 Ingress Controller + LoadBalancer）</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">gateway.networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Gateway</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">main-gateway</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">infra</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">gatewayClassName</span>: <span style="color:#ae81ff">envoy</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listeners</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">http</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">port</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">HTTP</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">https</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">port</span>: <span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">HTTPS</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tls</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">Terminate</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">certificateRefs</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo-cert</span>
</span></span></code></pre></div><p>这相当于原来的：</p>
<ul>
<li>Ingress Controller</li>
<li>
<ul>
<li>LoadBalancer Service</li>
</ul>
</li>
<li>
<ul>
<li>TLS 配置</li>
</ul>
</li>
</ul>
<h2 id="步骤-3把-ingress-转换为-gateway-api-路由">步骤 3：把 Ingress 转换为 Gateway API 路由</h2>
<p>原 Ingress：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">host</span>: <span style="color:#ae81ff">demo.example.com</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">http</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pathType</span>: <span style="color:#ae81ff">Prefix</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">backend</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo-svc</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">port</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">number</span>: <span style="color:#ae81ff">80</span>
</span></span></code></pre></div><p>新 HTTPRoute：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">gateway.networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">HTTPRoute</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parentRefs</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">main-gateway</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">infra</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hostnames</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#e6db74">&#34;demo.example.com&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">matches</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">path</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#ae81ff">PathPrefix</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">backendRefs</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo-svc</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">port</span>: <span style="color:#ae81ff">80</span>
</span></span></code></pre></div><h2 id="步骤-4灰度切流可选但强烈推荐">步骤 4：灰度切流（可选但强烈推荐）</h2>
<p>Gateway API 支持权重：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">backendRefs</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo-svc-v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">port</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">demo-svc-v2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">port</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">10</span>
</span></span></code></pre></div><p>用于：</p>
<ul>
<li>灰度发布</li>
<li>蓝绿切换</li>
<li>测试新网关</li>
<li>逐步迁移流量</li>
</ul>
<p>这是 Ingress 以前必须靠 Nginx snippet 才能做到的。</p>
<h2 id="步骤-5服务逐个迁移--最终替换所有-ingress">步骤 5：服务逐个迁移 → 最终替换所有 Ingress</h2>
<p>建议迁移顺序：</p>
<ol>
<li>无状态服务</li>
<li>内部服务（非公网）</li>
<li>低风险流量</li>
<li>高风险 / 主业务</li>
<li>全局入口流量</li>
<li>完成后卸载 Ingress NGINX</li>
</ol>
<h2 id="迁移注意事项生产环境必读">迁移注意事项（生产环境必读）</h2>
<h3 id="1-annotation-不会自动迁移">1. Annotation 不会自动迁移</h3>
<p>Ingress 的 Annotation（rewrite、headers、timeouts 等）要重写为 Gateway API 的 Policy / Filter。</p>
<h3 id="2-tls-配置方式不同">2. TLS 配置方式不同</h3>
<p>Gateway API 是 Listener 层统一处理 TLS。</p>
<h3 id="3-流量切换建议逐步进行">3. 流量切换建议逐步进行</h3>
<p>不要一次性替换大量入口。</p>
<h3 id="4-建议提前半年开始迁移">4. 建议提前半年开始迁移</h3>
<p>要赶在 2026 年 3 月停止维护前完成迁移。</p>
<h2 id="总结gateway-api-是-kubernetes-入口的未来">总结：Gateway API 是 Kubernetes 入口的未来</h2>
<p>Ingress NGINX 的退役并不是坏事，而是 Kubernetes 网络体系完成演进的标志。</p>
<p><strong>Ingress → Gateway API</strong>
是必然趋势，也是一次全面升级：</p>
<ul>
<li>更强的路由能力</li>
<li>更清晰的多租户设计</li>
<li>更丰富的现代云原生场景支持</li>
<li>更长期的社区维护保障</li>
</ul>
<p>对于已经依赖 Ingress NGINX 的团队，<strong>现在就应该开始迁移规划</strong>。</p>
<p>Gateway API + Envoy Gateway 是当前生态最稳、最现代、最推荐的方案。</p>
]]></content:encoded></item></channel></rss>