这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

策略

本系列文档将介绍 FGW 高级策略的使用。

Flomesh Gateway (FGW) 提供了一系列先进的负载均衡策略,以确保流量在后端服务之间有效、安全地分发。这些策略包括熔断、限流、超时、重试、重定向、路径重写、HTTP 头修改、会话保持和主动健康检查等。

本文档将深入解释这些策略的功能以及如何配置和使用。

这里介绍的部分策略与端点 Endpoints 一样,都是服务维度的配置。

有些策略则是路由或者路由+服务的粒度:

当然还有粒度更加灵活的策略,如 限流故障注入,可以作用于域名和路由的粒度。

示例

{
  "RouteRules": {
    "PORT": {
      "HOST": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {},
            "BackendService": {
              "serviceName": {
                "Weight": 100,
                "Filters": [{
                  "Type": ""
                }]
              }
            },
            "RateLimit": {},
            "Filters": [
              {
                "Type": "RequestHeaderModifier"
              },
              {
                "Type": "ResponseHeaderModifier"
              },
              {
                "Type": "RequestMirror"
              },
              {
                "Type": "RequestRedirect"
              },
              {
                "Type": "HTTPURLRewriteFilter"
              }
            ]
          }
        ],
        "RateLimit": {}
      }
    }
  },
  "Services": {
    "backendService": {
      "Endpoints": {},
      "Filters": {},
      "CircuitBreaking": {},
      "MaxRequestsPerConnection": 1,
      "MaxPendingRequests": 1,
      "RetryPolicy": {},
      "HealthCheck": {},
      "StickyCookieName": "COOKIE_NAME",
      "StickyCookieExpires": 3600
    }
  }
}

1 - 熔断

熔断策略的引入,使得系统具备更强的容错能力和更优化的资源利用。本文档将介绍如何使用 FGW 的熔断功能。

介绍

熔断是一种防止系统过载并保证服务可用性的机制。当一个服务连续失败达到一定次数时,Flomesh Gateway(FGW)将“熔断”该服务,暂时停止向其发送请求。这可以防止故障的蔓延,并给出故障服务恢复所需的时间。

熔断策略的引入,使得系统具备更强的容错能力和更优化的资源利用。

前置条件

这里后端服务建议使用工具 Fortio。Fortio 作为服务器运行时,可以在请求时指定延迟、响应状态,及其分布,非常方便测试熔断场景。同时 Forito 还可以作为负载生成器来使用。

启动服务器可以使用命令 fortio server -http-port 8082

配置说明

在 FGW 中熔断是 服务 Service 维度的配置,可以针对服务的慢访问、错误进行熔断,并支持配置熔断后的响应。这两种熔断的触发条件很容易理解:

  • 慢访问触发熔断:当系统中某个服务的响应时间超过预定阈值时,熔断器会开启,以阻止对该服务的进一步访问。
  • 错误触发熔断:当系统中某个服务的错误率超过预定阈值时,熔断器会开启,以阻止对该服务的进一步访问,比如服务返回大量的 5xx 错误。

熔断配置项

  • MinRequestAmount: 触发熔断必须达到的访问次数。为了避免基于小样本的误判,这一字段确保在短时间内的极少数错误或慢访问不会导致过早的熔断。只有在统计时间窗口内的请求达到此设定值,系统才会考虑熔断策略。必须字段。
  • StatTimeWindow: 熔断判定统计窗口(单位:秒)。这定义了评估慢访问和错误请求的时间范围。在这个窗口内,系统会收集相关数据,以判断是否需要熔断。必须字段。
  • SlowTimeThreshold: 慢访问时间阈值(单位:秒)。它定义了什么样的响应时间应该被认为是“慢”的。如果一个请求的响应时间超过了这个阈值,它就被标记为慢访问。非必须字段。
  • SlowAmountThreshold: 在 StatTimeWindow 时间统计窗口内,触发熔断的慢访问次数。达到或超过这个阈值的慢访问次数会触发熔断。非必须字段。
  • SlowRatioThreshold: 触发熔断的慢访问占比(0.00~1.00)。这与上面的次数阈值不同。如果慢访问的占比超过这个设置的阈值,则会触发熔断。非必须字段。
  • ErrorAmountThreshold: 在 StatTimeWindow 时间统计窗口内,触发熔断的失败访问次数。错误的访问次数达到或超过此阈值时,会触发熔断。非必须字段。
  • ErrorRatioThreshold: 触发熔断的失败访问占比(0.00~1.00)。与次数阈值不同,如果失败的请求占比超过这个阈值,则会触发熔断。非必须字段。
  • DegradedTimeWindow: 熔断时间窗口(单位:秒)。当熔断被触发后,服务将进入一个“降级”状态,只返回错误或预设响应,不再处理实际的请求,持续这个时间窗口长度。之后,服务会尝试自我恢复,再次接受并处理请求。必须字段。
  • DegradedStatusCode: 当服务被熔断时,返回给客户端的 HTTP 状态码。这是通知客户端因为某种原因服务当前不可用的标准方式。必须字段。
  • DegradedResponseContent: 当发生熔断时,返回给客户端的提示信息。这可以为客户端提供更具体的错误信息或建议的后续操作。非必须字段。

示例

{
  "CircuitBreaking": {
    "MinRequestAmount": 10,
    "StatTimeWindow": 60,
    "SlowTimeThreshold": 1.5,
    "SlowAmountThreshold": 5,
    "SlowRatioThreshold": 0.1,
    "ErrorAmountThreshold": 3,
    "ErrorRatioThreshold": 0.05,
    "DegradedTimeWindow": 300,
    "DegradedStatusCode": 503,
    "DegradedResponseContent": "Service is temporarily unavailable."
  }
}

配置

负载均衡器配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8082;然后根据 HTTP 插件链配置,将 http/circuit-breaker.js 插件配置在相应的配置。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/circuit-breaker.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

前面提到熔断的配置是服务维度的,接下来我们开始熔断的配置。

慢访问数量熔断

在配置熔断之前,检查下负载均衡器对慢访问的反应。将服务端的延迟设置为 600ms,概率为 30。在统计延迟分布的时候,特地设置了 71 这个百分比。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?delay\=600ms:30

# target 50% 0.0058
# target 71% 0.601213
# target 90% 0.608895
# target 95% 0.610917
# target 99% 0.612534

IP addresses distribution:
127.0.0.1:8080: 10
Code 200 : 100 (100.0 %)

设置服务 backendService1 在 10 秒的时间统计窗口内、慢访问(延迟大于 500ms)的数量达到 20 时进行熔断。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "CircuitBreaking": {
        "MinRequestAmount": 10,
        "StatTimeWindow": "60",
        "ErrorAmountThreshold": "20",
        "DegradedTimeWindow": 10,
        "DegradedStatusCode": 503,
        "DegradedResponseContent": "Service is temporarily unavailable."
      }
    }
  }
}

在配置慢访问熔断后,再进行测试就会发现有 30% 的响应状态码为 503,熔断生效。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?delay\=600ms:30

# target 50% 0.0117143
# target 71% 0.0175385
# target 90% 0.610336
# target 95% 0.614643
# target 99% 0.618088

IP addresses distribution:
127.0.0.1:8080: 30
Code 200 : 70 (70.0 %)
Code 503 : 30 (30.0 %)

慢访问占比熔断

同样,假如我们将慢访问熔断从数量触发熔断改成百分比触发, 将 SlowAmountThreshold=20 改为 SlowRatioThreshold=0.2,即慢访问的占比达到 20% 触发熔断。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "CircuitBreaking": {
        "MinRequestAmount": 10,
        "StatTimeWindow": "60",
        "SlowTimeThreshold": "0.5",
        "SlowRatioThreshold": 0.2,
        "DegradedTimeWindow": 10,
        "DegradedStatusCode": 503,
        "DegradedResponseContent": "Service is temporarily unavailable."
      }
    }
  }
}

还是用同样的方式进行测试,可以发现有 80% 的请求被熔断。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?delay\=600ms:30

# target 50% 0.00353846
# target 71% 0.00433333
# target 90% 0.00733333
# target 95% 0.601387
# target 99% 0.606937

IP addresses distribution:
127.0.0.1:8080: 80
Code 200 : 20 (20.0 %)
Code 503 : 80 (80.0 %)

假如设置 15% 的响应延迟 600ms 的话,并不会触发熔断。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?delay\=600ms:15

# target 50% 0.00286111
# target 71% 0.00469231
# target 90% 0.603466
# target 95% 0.606932
# target 99% 0.609705

IP addresses distribution:
127.0.0.1:8080: 10
Code 200 : 100 (100.0 %)

错误响应数触发熔断

我们先看下没有配置熔断的情况下,服务端 30% 响应返回 503 的情况(实际数据会有一定的浮动 +- 5%)。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?status\=500:30

IP addresses distribution:
127.0.0.1:8080: 37
Code 200 : 68 (68.0 %)
Code 500 : 32 (32.0 %)

我们设置触发熔断的错误数:ErrorAmountThreshold=20

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "CircuitBreaking": {
        "MinRequestAmount": 10,
        "StatTimeWindow": "60",
        "ErrorAmountThreshold": "20",
        "DegradedTimeWindow": 10,
        "DegradedStatusCode": 503,
        "DegradedResponseContent": "Service is temporarily unavailable."
      }
    }
  }
}

还是保持服务端的 30% 错误率,再次测试可以看到 20 个 500 错误影响之后触发了熔断,返回了预设的 503 错误。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?status\=500:30

IP addresses distribution:
127.0.0.1:8080: 60
Code 200 : 40 (40.0 %)
Code 500 : 20 (20.0 %)
Code 503 : 40 (40.0 %)

错误响应占比触发熔断

接下来我们测试下错误响应占比的触发策略,将 ErrorAmountThreshold=20,修改为 ErrorRatioThreshold=0.25

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "CircuitBreaking": {
        "MinRequestAmount": 10,
        "StatTimeWindow": "60",
        "ErrorRatioThreshold": "0.25",
        "DegradedTimeWindow": 10,
        "DegradedStatusCode": 503,
        "DegradedResponseContent": "Service is temporarily unavailable."
      }
    }
  }
}

再次测试,可以看到 30 个请求中,有 6 个请求返回了 500 的响应,达到了 25% 的熔断阈值,触发了熔断。

fortio load -quiet -c 10 -n 100 -p 50,71,90,95,99 http://localhost:8080/echo\?status\=500:30

IP addresses distribution:
127.0.0.1:8080: 85
Code 200 : 24 (24.0 %)
Code 500 : 6 (6.0 %)
Code 503 : 70 (70.0 %)

2 - 限流

限流可以确保服务在高流量下仍然稳定,预防系统雪崩。本文档将介绍如何使用 FGW 的限流功能。

介绍

限流(Rate Limiting)是一个非常重要的功能,尤其在现代的分布式应用程序中。

限流是控制数据请求速率的方法,以确保在任何给定的时间内,传入的请求数量不会超过系统预定义的限额。通过这种方式,限流可以保护系统避免因过多的请求而过载,从而维护服务的质量和响应速度。

FGW 中限流的粒度与 熔断 等策略的服务维度少有不同,可以作用在域名和路由两个维度上,提供更加精细化的限流配置。

前置条件

这里后端服务建议使用工具 Fortio。Fortio 作为服务器运行时,可以在请求时指定延迟、响应状态,及其分布,非常方便测试熔断场景。同时 Forito 还可以作为负载生成器来使用。

启动服务器可以使用命令 fortio server -http-port 8082

配置说明

限流配置

下面列出了限流配置的字段,也可以参考文档 Ratelimit 配置

  • Backlog: 积压值是指在达到限流阈值时,系统允许排队的请求数量。这是一个重要的字段,尤其是在系统突然遭受大量请求时,这些请求可能会超过设置的限流阈值。积压值为系统提供了一定的缓冲,可以处理超出限流阈值的请求,但在积压值上限内。一旦达到积压上限,任何新的请求都会立即被拒绝,无需等待。可选字段。
  • Requests: 请求值是指在限流时间窗口内允许的访问次数。这是限流策略的核心参数,它确定了在特定的时间窗口内可以接受多少请求。设置此值的目的是为了确保在给定的时间窗口内,后端系统不会受到超过它能处理的请求。必须字段。
  • Burst: 爆发值表示在短时间内允许的最大请求次数。这是一个可选字段,它主要用于处理短时间的请求高峰。爆发值通常大于请求值,允许在短时间内接受的请求数量超过平均速率。非必须字段。
  • StatTimeWindow: 限流时间窗口(单位:秒)定义了统计请求数量的时间段。限流策略通常基于滑动窗口或固定窗口来实现。StatTimeWindow 定义了这个窗口的大小。比如,如果 StatTimeWindow 设置为 60 秒,并且 Requests 为 100,则意味着每 60 秒内最多只能有 100 个请求。必须字段。
  • ResponseStatusCode: 发生限流时,返回给客户端的 HTTP 状态码。这个状态码告诉客户端请求被拒绝的原因是因为达到了限流阈值。常见的状态码是 429(Too Many Requests),但可以根据需要自定义。必须字段。
  • ResponseHeadersToAdd: 发生限流时,要添加到响应中的 HTTP 头部信息。这可以用来通知客户端有关限流策略的更多信息。例如,可以添加一个 Retry-After 头来告诉客户端多久后可以重试。还可以提供关于当前限流策略或如何联系系统管理员的其他有用信息。可选字段。

示例

{
  "RateLimit": {
    "Local": {
      "Backlog": 1,
      "Requests": 100,
      "Burst": 200,
      "StatTimeWindow": 60,
      "ResponseStatusCode": 429,
      "ResponseHeadersToAdd": [
        {
          "Name": "RateLimit-Limit",
          "Value": "100"
        }
      ]
    }
  }
}

限流的维度

基于 Host 的限流(参考 域名配置文档),可以所有对某个 Host 的请求进行统计限流。

{
  "example.com": {
    "RouteType": "HTTP",
    "Matches": [],
    "RateLimit": {}
  }
}

基于路由的限流(参考路由 配置文档),可以对匹配某个路由的请求进行统计限流。

{
  "example.com": {
    "RouteType": "HTTP",
    "Matches": [
      {
        "Path": {},
        "BackendService": {},
        "RateLimit": {}
      }
    ]
  }
}

限流的作用范围

限流是为了控制到达系统或组件的请求速率。根据其作用范围和实施地点,限流策略可以分为本地限流全局限流

注:当前版本 v1.0.0 仅支持本地限流。

1. 本地限流 (Local Rate Limiting)

  • 定义:本地限流通常在单个实例或节点上实施,独立于其他节点。每个节点都根据自己的限流策略和规则来处理流入的请求。
  • 作用范围:只对单一实例或节点有效。
  • 优势
    • 延迟较低:由于限流判断在本地完成,无需与其他系统或节点通信,因此延迟很小。
    • 简单:通常很容易实施和管理。
  • 劣势
    • 非均匀限流:由于每个节点都是独立的,因此在高流量下,某些节点可能会被快速饱和,而其他节点可能还有未使用的容量。
    • 缺乏集中的控制和可见性:各个节点可能无法获知整体系统的状态。

2. 全局限流 (Global Rate Limiting)

  • 定义:全局限流通常在系统或服务的所有实例或节点之间共享。所有的请求,不论它们到达哪个节点,都会计入全局的限流计数器。
  • 作用范围:涵盖了整个系统或服务的所有实例。
  • 优势
    • 均匀限流:确保整个系统都遵循相同的限流规则,避免了单个节点的过载。
    • 集中的控制和可见性:提供一个集中的位置来配置、管理和监视限流,使得运维更为简便。
  • 劣势
    • 潜在的延迟:为了同步全局计数器,节点间可能需要进行通信,这可能会增加延迟。
    • 实施复杂性:全局限流策略需要更多的协调和同步机制,可能需要额外的组件(如共享的数据存储或分布式计数器)。

应用场景

  • 对于小型或低流量的系统,本地限流可能就足够了,因为它简单且开销小。
  • 对于大型、分布式或高流量的系统,全局限流可能更为适合,因为它可以更有效地控制和分配请求,尤其是在多个节点或实例间。

最终,选择本地限流还是全局限流取决于系统的规模、需求以及预期的流量模式。

配置

负载均衡器配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8082;然后根据 HTTP 插件链配置,将 http/throttle-domain.jshttp/throttle-route.js 插件配置在相应的配置。

顾名思义,这两个插件将分别处理基于 Host 的限流和基于路由的限流。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/throttle-domain.js",
      "http/throttle-route.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

基于域名的限流配置

在上面的负载均衡器配置中,使用了通配符 * 来匹配所有的域名,我们也可以将基于域名的限流赋给它。

下面增加限流的配置,时间窗口为 60s,最大请求数为 100,爆发值设置为 200。同时在被限流的响应头部加上限流值的提醒。

{
  "*": {
    "RouteType": "HTTP",
    "Matches": [
      {
        "Path": {
          "Type": "Prefix",
          "Path": "/"
        },
        "BackendService": {
          "backendService1": {
            "Weight": 100
          }
        }
      }
    ],
    "RateLimit": {
      "Local": {
        "Backlog": 1,
        "Requests": 100,
        "Burst": 200,
        "StatTimeWindow": 60,
        "ResponseStatusCode": 429,
        "ResponseHeadersToAdd": [
          {
            "Name": "RateLimit-Limit",
            "Value": "100"
          }
        ]
      }
    }
  }
}

使用 Fortio 作为负载生成器:以 10 个并发、200 的 QPS 发送 1000 个请求。

fortio load -quiet -c 10 -n 1000 -qps 200 http://localhost:8080/echo
# target 50% 0.00204893
# target 75% 0.00281346
# target 90% 0.00371774
# target 99% 0.00742857
# target 99.9% 3.00293

IP addresses distribution:
127.0.0.1:8080: 802
Code 200 : 200 (20.0 %)
Code 429 : 800 (80.0 %)

上面将限流的统计时间窗口设置为了 1 分钟,因此在执行完 fortio load 命令后,仍然会处在限流状态。此时,我们使用 curl 来请求,可以查看响

curl -i http://localhost:8080/echo
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
content-length: 0
connection: keep-alive

上面是基于 Host 的限流配置,如果将这段配置移到路由上也可以得到同样的结果。

基于路由的限流

我们在原有的路由基础上,添加一条到 /echo 的路由,并将原来域名限流的配置移到这条路由上。

{
  "*": {
    "RouteType": "HTTP",
    "Matches": [
      {
        "Path": {
          "Type": "Prefix",
          "Path": "/echo"
        },
        "BackendService": {
          "backendService1": {
            "Weight": 100
          }
        },
        "RateLimit": {
          "Local": {
            "Backlog": 1,
            "Requests": 100,
            "Burst": 200,
            "StatTimeWindow": 60,
            "ResponseStatusCode": 429,
            "ResponseHeadersToAdd": [
              {
                "Name": "RateLimit-Limit",
                "Value": "100"
              }
            ]
          }
        }
      },
      {
        "Path": {
          "Type": "Prefix",
          "Path": "/"
        },
        "BackendService": {
          "backendService1": {
                "Weight": 100
          }
        }
      }
    ]
  }
}

还是以同样条件进行负载测试,可以得到同样的测试结果。但是我们马上分别请求 /echo/,可以看到两种不同的结果。

curl -i http://localhost:8080/echo
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
content-length: 0
connection: keep-alive

curl -i http://localhost:8080/
HTTP/1.1 200 OK
date: Sun, 13 Aug 2023 11:34:36 GMT
content-length: 0
connection: keep-alive

只有访问设置了限流的路由的请求才会被限流,其他路由并不会收到影响。

3 - 重试

重试功能为架构提供了额外的稳定性和韧性。本文档将介绍 FGW 的重试功能

介绍

在复杂的网络环境中,服务间的通信可能会因为各种原因失败。为了提高系统的可用性和韧性,FGW 提供了重试功能,允许在满足特定条件时自动重新发送失败的请求。

FGW 的重试功能为架构提供了额外的稳定性和韧性。通过合理地配置重试策略,可以有效地提高服务的可用性,确保在面对网络波动和服务暂时性问题时,请求仍有机会得到正确的处理。

重试策略的考虑因素

在配置重试策略时,应考虑以下几点:

  1. 服务的敏感性:对于某些服务,如支付、订单创建等,应谨慎配置重试策略,避免重复请求导致的数据不一致。
  2. 服务的容量:过度的重试可能会导致目标服务的资源耗尽。确保目标服务有足够的容量来处理重试请求。
  3. 重试间隔:为了防止因频繁的重试导致的资源耗尽或服务雪崩,建议设置一个合理的重试间隔。

前置条件

这里后端服务建议使用工具 Fortio。Fortio 作为服务器运行时,可以在请求时指定延迟、响应状态,及其分布,非常方便测试熔断场景。同时 Forito 还可以作为负载生成器来使用。

启动服务器可以使用命令 fortio server -http-port 8082

配置说明

在 FGW 中重试是 服务 Service 维度的配置

  • RetryOn:重试条件字段,定义了触发重试机制的条件。此字段用于确定在哪些特定条件下应该执行重试。例如,可以设置为特定的 HTTP 状态码,如“5xx”,这意味着当后端服务返回任何 5xx 错误时,都将触发重试。必须字段。
  • PerTryTimeout:每次尝试的超时时间,定义了单次重试请求等待响应的最长时间。如果在这个时间内没有得到后端服务的响应,该次重试会被认为是失败的,进而触发下一次的重试(如果还有剩余的重试次数)。此字段在网络不稳定或后端服务响应延迟的场景中特别有用,确保不会因为一个长时间未响应的请求而浪费整个重试机会。可选字段。如果未设置,每次重试可能会采用全局或默认的超时时间。可选字段。
  • NumRetries:重试次数,表示在放弃前尝试重新发送请求的次数。此字段对于控制可能的重试风暴非常有用。设置适当的值可以确保不会对后端服务产生过多的负担,同时也能给请求一个重新成功的机会。可选字段,如果未设置,可能需要默认值或固定重试次数。
  • RetryBackoffBaseInterval:重试间隔,定义了两次重试之间的等待时间。此字段帮助确保在连续的重试之间有适当的冷却时间,从而避免在短时间内对后端服务产生大量请求。它可以基于固定时间间隔,或者根据特定算法(如指数退避)动态调整。可选字段。如果未设置,重试可能会立即进行,或使用默认的间隔。

示例

{
  "RetryPolicy": {
    "RetryOn": "5xx",
    "PerTryTimeout": 5,
    "NumRetries": 5,
    "RetryBackoffBaseInterval": 1
  }
}

配置

负载均衡器配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8082;然后根据 HTTP 插件链配置,重试的功能在 http/forward.js" 插件中实现。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

使用 Fortio 负载生成工具:1 个并发使用 qps 10 发送 100 个请求;同时设置服务端 30% 的情况下返回 503 错误,实际测试在 30% 上下 5% 范围内浮动。

fortio load -quiet -c 1 -n 100 -qps 10 http://localhost:8080/echo\?status\=503:30

IP addresses distribution:
127.0.0.1:8080: 33
Code 200 : 68 (68.0 %)
Code 503 : 32 (32.0 %)
All done 100 calls (plus 0 warmup) 3.524 ms avg, 10.0 qps

配置重试策略

设置服务 backendService1 的重试策略:针对 503 错误进行重试、重试 5 次。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "RetryPolicy": {
        "RetryOn": "503",
        "PerTryTimeout": 5,
        "NumRetries": 5,
        "RetryBackoffBaseInterval": 1
      }
    }
  }
}

设置完重试策略后,使用同样的负载进行测试。此时可以看到所有的请求均成功,但总耗时增长非常明显,实际的 qps 只有 2.1。因此重试多次,对客户端来说单个请求的耗时增加了。所以,需要在设置策略的时候充分考虑服务的容量,避免错误的设置导致重试风暴压垮后端服务。

fortio load -quiet -c 1 -n 100 -qps 10 http://localhost:8080/echo\?status\=503:30

IP addresses distribution:
127.0.0.1:8080: 10
Code 200 : 100 (100.0 %)
All done 100 calls (plus 0 warmup) 482.113 ms avg, 2.1 qps

假如我们修改一下生成负载的设置,使服务端同样概率下返回 500 的错误,而重试策略只针对 503 错误。

fortio load -quiet -c 1 -n 100 -qps 10 http://localhost:8080/echo\?status\=500:30

IP addresses distribution:
127.0.0.1:8080: 28
Code 200 : 72 (72.0 %)
Code 500 : 28 (28.0 %)
All done 100 calls (plus 0 warmup) 3.397 ms avg, 10.0 qps

4 - 健康检查

FGW 的健康检查功能可用于提升系统的高可用性。本文档将介绍如何使用 FGW 的健康检查功能。

介绍

FGW 的健康检查功能允许自动监控和验证后端服务的健康状况。这样,FGW 可以确保流量只被转发到健康的、能够正常处理请求的服务。此功能尤其在微服务或分布式系统中至关重要,它确保了高可用性和弹性。

前置条件

用于健康检查的两个服务,可以使用 Pipy 快速模拟。

#healthy
pipy -e "pipy().listen(8081).serveHTTP(new Message({status: 200},'Hello, world'))"
#unhealthy
pipy -e "pipy().listen(8082).serveHTTP(new Message({status: 500},''))"

配置说明

参考 健康检查配置文档

  • Interval:健康检查间隔,表示系统对后端服务进行健康检查的时间间隔。此字段决定了多久检查服务一次,以确保它们的健康状况。例如,如果设置为 10 秒,系统将每 10 秒检查一次上游服务的健康状态。这有助于在服务变得不可用时快速检测并作出反应。不设置此字段意味着不进行主动健康检查。可选字段。
  • MaxFails:最大失败次数,定义了在将上游服务标记为不可用之前,可以连续失败的健康检查次数。这是一个关键参数,因为它决定了系统在决定服务不健康之前的容忍度。例如,如果设置为 3,那么在第三次连续的健康检查失败后,该服务将被标记为不可用。必须字段。
  • FailTimeout:失败超时,定义了一个上游服务在被标记为不健康后,将被暂时禁用的时间长度。这意味着,即使服务再次变得可用,它也会在这段时间内被代理视为不可用。此字段在未设置主动健康检查的情况下尤为重要,因为它决定了一个服务在多长时间内被认为是不健康的。如果已设置主动健康检查,此参数会被忽略。可选字段。
  • Uri:健康检查 URI,用于 HTTP 健康检查的路径。当使用 HTTP 方法进行健康检查时,系统会向此 URI 发送请求以确定服务的健康状态。如果此字段未设置,则健康检查将使用 TCP 方法检查端口的健康状况,而不是使用 HTTP 方法。可选字段。
  • Matches:匹配条件,用于确定 HTTP 健康检查的成功或失败。此字段可以包含多个条件,例如期望的 HTTP 状态码、响应体内容等。当健康检查的响应满足这些条件时,服务将被认为是健康的。否则,它将被认为是不健康的。这允许更细粒度的控制,确保服务不仅是可达的,而且是正常运行的。可选字段,但如果使用 HTTP 健康检查,建议设置。
    • Type:匹配类型。这个字段定义了你想在健康检查响应中匹配的具体部分。它有三个有效的值:
      • status:匹配 HTTP 响应的状态码列表,如 [200,201,204]
      • body:匹配 HTTP 响应的主体内容。
      • headers:匹配 HTTP 响应的头部信息。此字段是必须的。
    • Value:期望的数据。此字段与 Type 字段一起使用,定义了预期的匹配值。例如,如果 Type 设置为 statusValue 可能会设置为 200,表示期望 HTTP 响应的状态码为 200。此字段是必须的。
    • Name:当 Typeheaders 时使用。它定义了你想在 HTTP 响应头部中匹配的具体字段名。例如,如果你想检查 Content-Type 头部的值,你会设置 NameContent-Type。此字段只在 Type 设置为 headers 时有效,其他情况下不应该设置。此字段是可选的。

示例

{
  "HealthCheck": {
    "Interval": 10,
    "MaxFails": 3,
    "FailTimeout": 30,
    "Uri": "/",
    "Matches": [
      {
        "Type": "status",
        "Value": [
          200,
          201
        ]
      }
    ]
  }
}

配置

负载均衡器配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081127.0.0.1:8082;重试功能实现在 common/health-check.js 中,该插件是自动引入,无需再插件链中显式定义。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

在配置健康检查,我们可以看一下负载均衡的结果,一半的请求下返回了 500。

curl --head http://localhost:8080/
HTTP/1.1 200 OK
content-length: 12
connection: keep-alive

curl --head http://localhost:8080/
HTTP/1.1 500 Internal Server Error
content-length: 0
connection: keep-alive

健康检查配置

接下来为服务 backendService1 添加健康检查的配置。为了便于快速查看效果,我们讲检查的间隔时间设置为 2 秒,失败次数 2 次之后就被认定为不健康的端点。同时,设置检查的依据为 200 响应码。

实际场景中,过于频繁的健康检查虽然可以快速隔离不健康的端点,但也势必会给后端服务造成压力。同样较低的最大失败数判定,也会快速隔离不健康的端点提升服务的可用性,但也会因为偶尔的网络抖动、服务突发负载过高等原因导致大量端点“被”下线,导致服务的不可用。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "HealthCheck": {
        "Interval": 2,
        "MaxFails": 2,
        "FailTimeout": 30,
        "Uri": "/",
        "Matches": [
          {
            "Type": "status",
            "Value": [
              200,
              201
            ]
          }
        ]
      }
    }
  }
}

配置生效后,再次进行测试可以看到不健康的端点已经被隔离。

curl --head http://localhost:8080/
HTTP/1.1 200 OK
content-length: 12
connection: keep-alive

curl --head http://localhost:8080/
HTTP/1.1 200 OK
content-length: 12
connection: keep-alive

5 - HTTP 头部控制

在 FGW 的配置中,HTTP 头部控制功能允许你微调传入和传出的请求和响应头部。本文档将介绍 FGW 的 HTTP 头部控制功能。

介绍

在日常的网络交互中,HTTP 头部扮演了非常重要的角色。它们可以传递关于请求或响应的各种信息,如身份验证、缓存控制、内容类型等。FGW 提供了 HTTP 头部控制功能,允许用户对传入和传出的请求和响应头部进行精确的控制,以满足各种安全、性能和业务需求。

这里的后端服务,可以使用 Pipy 来模拟,执行下面的命令即可。

pipy -e "pipy().listen(8081).serveHTTP(msg => new Message({ 'status': 200, headers: { 'content-type': 'application/json', 'server': 'Pipy', 'token': 'CONFIDENTIAL'} }, JSON.encode({ headers: msg.head })))"

模拟的服务会返回如下信息,在消息体中返回原始请求的所有头部信息。

curl -i http://localhost:8081/
HTTP/1.1 200 OK
content-type: application/json
server: Pipy
content-length: 200
connection: keep-alive

{"headers":{"protocol":"HTTP/1.1","headers":{"host":"localhost:8080","user-agent":"Apache-HttpClient/x.y.z","accept":"*/*","client-id":"client-1","connection":"keep-alive"},"method":"GET","path":"/"}}

配置说明

FGW 的 HTTP 头部控制功能分为 请求头部控制 RequestHeaderModifier 和响应头部控制 ResponseHeaderModifier,使用二者都可以对 HTTP 头部进行增加、删除、修改三种操作。

  • Set: 设置 HTTP header。此字段用于设定或修改 HTTP 头部的值。如果指定的头部已存在,则其值将被替换为新值;如果不存在,则会添加此头部。它是一个可选字段,其值是一个对象列表,其中每个对象都包含NameValue两个键。
  • Add: 增加 HTTP header。此字段用于添加新的 HTTP 头部,而不会影响到原有的头部。如果指定的头部已存在,新的头部将被添加,而不会替换已有的头部。它是一个可选字段,其值格式与 Set 字段相同。
  • Remove: 删除 HTTP header。此字段用于从 HTTP 请求或响应中删除指定的头部。这对于确保某些敏感或不必要的头部不被暴露给客户端或后端服务是很有用的。它是一个可选字段,其值是一个字符串列表,代表要删除的头部名称。

示例

{
  "Filters": [
    {
      "Type": "RequestHeaderModifier",
      "RequestHeaderModifier": {
        "Set": [
          {
            "Name": "host",
            "Value": "set-bar"
          }
        ],
        "Add": [
          {
            "Name": "accept",
            "Value": "xxx"
          }
        ],
        "Remove": [
          "user-agent",
          "my-header4"
        ]
      }
    },
    {
      "Type": "ResponseHeaderModifier",
      "ResponseHeaderModifier": {
        "Set": [
          {
            "Name": "dummy1",
            "Value": "set-bar"
          }
        ],
        "Add": [
          {
            "Name": "dummy2",
            "Value": "add,baz"
          }
        ],
        "Remove": [
          "dummy3",
          "my-header8"
        ]
      }
    }
  ]
}

配置

负载均衡配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081;然后根据 HTTP 插件链配置,将 filter/header-modifier.js 插件配置在相应的配置。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "filter/header-modifier.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

通过负载均衡访问后端服务。

curl -i http://localhost:8080
HTTP/1.1 200 OK
content-type: application/json
server: Pipy
token: CONFIDENTIAL
content-length: 164
connection: keep-alive

{"headers":{"protocol":"HTTP/1.1","headers":{"host":"localhost:8080","user-agent":"curl/8.1.2","accept":"*/*","connection":"keep-alive"},"method":"GET","path":"/"}}

HTTP 头部控制配置

看到上面的结果后,我们希望对后端服务隐藏客户端的信息,比如将请求中的 user-agent 替换为 Apache-HttpClient/x.y.z;增加 client-id 信息 client-1;同时为了安全期间,避免服务端的 token 泄露,并模拟服务端的 server 信息为 Tomcat Server

{
  "Matches": [
    {
      "Path": {},
      "BackendService": {},
      "Filters": [
        {
          "Type": "RequestHeaderModifier",
          "RequestHeaderModifier": {
            "Set": [
              {
                "Name": "user-agent",
                "Value": "Apache-HttpClient/x.y.z"
              }
            ],
            "Add": [
              {
                "Name": "client-id",
                "Value": "client-1"
              }
            ]
          }
        },
        {
          "Type": "ResponseHeaderModifier",
          "ResponseHeaderModifier": {
            "Set": [
              {
                "Name": "server",
                "Value": "Tomcat Server"
              }
            ],
            "Remove": [
              "token"
            ]
          }
        }
      ]
    }
  ]
}

再次通过负载均衡访问后端服务,对应的头部信息已经做了修改。

curl -i http://localhost:8080/
HTTP/1.1 200 OK
content-type: application/json
server: Tomcat Server
content-length: 200
connection: keep-alive

{"headers":{"protocol":"HTTP/1.1","headers":{"host":"localhost:8080","user-agent":"Apache-HttpClient/x.y.z","accept":"*/*","client-id":"client-1","connection":"keep-alive"},"method":"GET","path":"/"}}

6 - 负载均衡算法

本文档将介绍如何为服务指定负载均衡算法

介绍

在微服务和 API 网关架构中,负载均衡是至关重要的,它确保每个服务实例都能平均地处理请求,同时也为高可用性和故障恢复提供了机制。FGW 提供了多种负载均衡算法,让可以根据业务需求和流量模式选择最适合的方法。

FGW 支持多种负载均衡算法,方便高效地分配流量,最大化资源利用率,提高服务的响应时间:

  • RoundRobinLoadBalancer:这是最常见的负载均衡算法,请求将按顺序分配给每个服务实例。如果不特别指定,FGW 默认使用此算法。
  • HashingLoadBalancer:根据请求的某些属性(如来源 IP 或请求头)计算哈希值,然后根据该哈希值将请求路由到特定的服务实例。这确保了相同的请求者或相同类型的请求总是被路由到同一服务实例。
  • LeastConnectionLoadBalancer:这种算法会考虑每个服务实例的当前工作负载(连接数),并将新的请求分配给当前负载最小的实例,从而确保更均匀的资源利用。

前置条件

用于会话保持的两个服务,可以使用 Pipy 快速模拟。

#svr-1
pipy -e "pipy().listen(8081).serveHTTP(new Message({status: 200},'Hello, world'))"
#svr-2
pipy -e "pipy().listen(8082).serveHTTP(new Message({status: 200},'Hi, world'))"

配置说明

负载均衡算法(Algorithm)是服务级别(Service)的配置,可以参考 服务配置文档

配置方法很简单,只需在服务的 Algorithm 字段指定所需的算法即可。

示例

{
  "Services": {
    "backendService1": {
      "Algorithm": "LeastWorkLoadBalancer",
      "Endpoints": {}
    }
  }
}

配置

负载均衡配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081127.0.0.1:8082;然后根据 HTTP 插件链配置,重试的功能在 http/forward.js" 插件中实现。

为了便于测试,我们将两个端点的权重分别设为 10010

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 10
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

当不指定任何负载均衡算法时,默认使用 RoundRobinLoadBalancer。使用 fortio load -quiet -c 55 -t 60s -qps 20 http://localhost:8080 生成负载,并查看到两个后端的连接数。

可以看到在加权轮训算法下,代理会按照权重选择后端的示例建立连接。50:5 正好是两个后端实例的权重比 100:10

netstat -lant | grep EST | awk '{print $5}' | grep 127.0.0.1 | grep -e "8081\|8082" | sort | uniq -c
  50 127.0.0.1.8081
   5 127.0.0.1.8082

配置哈希一致性均衡算法

在服务配置上通过 Algorithm 指定使用 HashingLoadBalancer 哈希一致性负载均衡算法。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 10
        }
      },
      "Algorithm": "HashingLoadBalancer"
    }
  }
}

待配置生效后,多次请求都可以获得相同的结果。

curl http://localhost:8080
Hello, world
curl http://localhost:8080
Hello, world
curl http://localhost:8080
Hello, world

同样在使用 fortio load -quiet -c 55 -t 60s -qps 20 http://localhost:8080 使用 55 个连接发送请求时,所有连接都连到了同一个后端实例。

netstat -lant | grep EST | awk '{print $5}' | grep 127.0.0.1 | grep -e "8081\|8082" | sort | uniq -c
  55 127.0.0.1.8082

配置最小连接数均衡算法

将均衡算法改为 LeastWorkLoadBalancer

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 10
        }
      },
      "Algorithm": "LeastConnectionLoadBalancer"
    }
  }
}

同样在使用 fortio load -quiet -c 55 -t 60s -qps 20 http://localhost:8080 使用 55 个连接发送请求时,到后端实例的连接数接近均衡。

netstat -lant | grep EST | awk '{print $5}' | grep 127.0.0.1 | grep -e "8081\|8082" | sort | uniq -c
  28 127.0.0.1.8081
  27 127.0.0.1.8082

7 - 重定向

请求重定向是一种使客户端将其请求发送到另一个位置的方法。本文档将介绍 FGW 的请求重定向功能。

介绍

请求重定向是一种常见的网络应用功能,它使得服务器能够告诉客户端:“你请求的资源已经移动到了另一个位置,请到新的位置去获取”。

随着互联网应用的复杂性增加,请求重定向变得越来越常见。FGW 的请求重定向功能提供了一个灵活而高效的方式来控制和管理这些重定向,确保流量能够正确并高效地流向正确的目的地。

前置条件

用于重定向的两个服务,可以使用 Pipy 快速模拟。

#origin
pipy -e "pipy().listen(8081).serveHTTP(new Message({status: 200},'Hello, world'))"
#target
pipy -e "pipy().listen(8082).serveHTTP(new Message({status: 200},'Hello, there'))"

两个后端服务返回不同的结果。

curl http://localhost:8081
Hello, world
curl http://localhost:8082
Hello, there

配置说明

下面根据 请求重定向的参考文档,对各个字段就行说明。

  • scheme:协议描述,此字段定义了重定向目标的协议类型。参考值:httphttps。例如,若选择了 https,那么请求会被重定向到一个 HTTPS 协议的地址。
  • hostname:重定向到的域名,此字段指定了重定向目标的域名或主机名。例如,设置为 example.com,则请求会被重定向到 example.com
  • path:重定向到的路径,此字段是必须的,它定义了重定向目标的特定路径。例如,若设定路径为 /new-path,请求将被重定向到指定的 /new-path
  • statusCode:重定向返回的状态码,此字段是必须的,它定义了服务器返回的 HTTP 状态码,通常用来告知客户端重定向的类型。

示例

{
  "Filters": [
    {
      "Type": "RequestRedirect",
      "RequestRedirect": {
        "Scheme": "https",
        "Hostname": "",
        "Path": "/abc",
        "Port": 8443,
        "StatusCode": 301
      }
    }
  ]
}

配置

负载均衡器配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081 ,重定向功能实现在 filter/request-redirect.js 中,需要将其添加到插件链中。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "filter/request-redirect.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

在服务的端点列表中,我们配置了 127.0.0.1:8081,通过负载均衡访问会返回 Hello, world

curl http://localhost:8080
Hello, world

配置请求重定向

设置将请求重定向到 8082 的后端服务,将 statusCode 设置为 301

{
  "Matches": [
    {
      "Path": {},
      "BackendService": {},
      "Filters": [
        {
          "Type": "RequestRedirect",
          "RequestRedirect": {
            "Scheme": "http",
            "Hostname": "127.0.0.1",
            "Path": "/",
            "Port": 8082,
            "StatusCode": 301
          }
        }
      ]
    }
  ]
}

配置生效后,再次请求。

curl -i http://localhost:8080
HTTP/1.1 301 Moved Permanently
Location: http://localhost:8082/
content-length: 0
connection: keep-alive

借助 -L 参数,实现自动重定向。

curl -L http://localhost:8080
Hello, there

8 - URL 重写

URL 重写功能为系统提供了更大的灵活性来适应后端服务的不断变化。本文档将介绍如何使用 FGW 的 URL 重写功能。

介绍

URL 重写功能为 FGW 用户提供了一种在流量进入目标服务之前修改请求 URL 的方法。这不仅提供了更大的灵活性来适应后端服务的变化,而且确保了应用的流畅迁移和 URL 的规范化。

URL 重写常被用在以下场景:

  • 版本迁移:当应用或网站进行版本迁移时,可以使用前缀匹配来将旧版本的路径前缀替换为新版本的路径前缀。
  • 目录结构更改:若应用或网站的目录结构发生变化,但仍需保持旧的 URL 可访问,可利用全路径匹配重写功能。
  • URL 规范化:保证 URL 符合特定规范或风格。

前置条件

后端服务可以使用 Pipy 来模拟一个返回正在请求的路径的服务。

pipy -e "pipy().listen(8081).serveHTTP(msg => new Message('You are requesting ' + msg.head.headers.host + msg.head.path))"

它会返回我们正在请求的路径。

curl http://localhost:8081/api/hi
You are requesting localhost:8081/api/hi
curl http://localhost:8081/user
You are requesting localhost:8081/user

配置说明

下面参考 URL重写文档 对配置字段进行说明。

  • Hostname:重写后请求的目标域名或主机名。此字段可用于改变请求的目标地址,将流量引导至一个完全不同的域名或子域名。值为任何有效的域名,如 example.comsub.example.com。可选字段,不配置时会继续使用原请求中 host

  • Path:对 path 路径的重写规则。

    • Type:URL 重写匹配规则,此字段是必须的,它定义了路径重写时所采用的匹配方式。参考值有:ReplacePrefixMatch 表示前缀匹配,和 ReplaceFullPath 表示全路径匹配。选择合适的匹配规则是重写功能的基础。
    • ReplacePrefixMatch:前缀匹配时的 path,当 Type 字段设置为 ReplacePrefixMatch 时,此字段必须配置。它定义了需要被替换的 URL 前缀。例如,如果设置为 /old-prefix,那么所有以 /old-prefix 开始的路径都会被替换。
    • ReplacePrefix:前缀匹配时替换成这个 path,当 Type 字段设置为 ReplacePrefixMatch 时,此字段可以配置,但不是必须的。它定义了用来替换的新前缀。默认值为 /。例如,如果 ReplacePrefixMatch 设置为 /old-prefix 并且 ReplacePrefix 设置为 /new-prefix,那么 /old-prefix/example 会被重写为 /new-prefix/example
    • ReplaceFullPath:全路径匹配时的 path,当 Type 字段设置为 ReplaceFullPath 时,此字段必须配置。它定义了需要完全匹配并替换的路径。例如,如果设置为 /old-path/example, 那么只有完全匹配到这个路径的请求会被重写。

示例

{
  "Filters": [
    {
      "Type": "HTTPURLRewriteFilter",
      "UrlRewrite": {
        "Hostname": "",
        "Path": {
          "Type": "ReplacePrefixMatch",
          "ReplacePrefixMatch": "/path-prefix"
        }
      }
    },
    {
      "Type": "HTTPURLRewriteFilter",
      "UrlRewrite": {
        "Hostname": "",
        "Path": {
          "Type": "ReplacePrefixMatch",
          "ReplacePrefixMatch": "/path-prefix",
          "ReplacePrefix": "/new-path-prefix"
        }
      }
    },
    {
      "Type": "HTTPURLRewriteFilter",
      "UrlRewrite": {
        "Hostname": "",
        "Path": {
          "Type": "ReplaceFullPath",
          "ReplaceFullPath": "/path-prefix"
        }
      }
    }
  ]
}

配置

负载均衡配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081;同时将该后端服务的路由路径设置为 /sample;最后根据 HTTP 插件链配置,将 filter/url-rewrite.js 插件配置在相应的配置。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/sample"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "filter/url-rewrite.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

测试服务的访问,/sample 的前缀会被带到后端服务。

curl http://localhost:8080/sample/api/hi
You are requesting localhost:8080/sample/api/hi

前缀重写配置

假如前缀 /sample 并不是后端服务的合法路径,负载均衡器在转发请求的时候需要将该前缀去掉。我们使用 ReplacePrefixMatch 类型的 URL 重写规则,将 /sample 作为前缀匹配请求,然后将该前缀替换为 /release

{
  "Matches": [
    {
      "Path": {},
      "BackendService": {},
      "Filters": [
        {
          "Type": "URLRewrite",
          "UrlRewrite": {
            "Path": {
              "Type": "ReplacePrefixMatch",
              "ReplacePrefixMatch": "/release"
            }
          }
        }
      ]
    }
  ]
}

还是使用之前的地址发送请求,这次后端服务接收到的请求路径为 /release/api/hi,前缀被替换。

curl http://localhost:8080/sample/api/hi
You are requesting localhost:8080/release/api/hi

如果想将其重写为 /api/hi,也就是去掉前缀场景,可以将 ReplacePrefixMatch 设置为 ""(空字符串)。

全路径重写配置

有些情况下如果服务路径已经不再使用,但出于安全和合规的原因,不希望外部用户访问它,可以使用全路径重写将其重定向到一个通用的 404 页面或其他安全页面。

添加 HTTPURLRewriteFilter 全路径重写规则,将访问服务的所有请求路径都修改为目标路径 /404

{
  "Matches": [
    {
      "Path": {},
      "BackendService": {},
      "Filters": [
        {
          "Type": "URLRewrite",
          "UrlRewrite": {
            "Path": {
              "Type": "ReplaceFullPath",
              "ReplaceFullPath": "/404"
            }
          }
        }
      ]
    }
  ]
}

此时再请求,就会去到 /404 地址。

curl http://localhost:8080/sample/api/hi
You are requesting localhost:8080/404

重写主机名

在上面的例子中,后端服务打印的主机名都是 localhost:8080 也就是代理的地址。有时候,我们希望对上游服务隐藏代理的地址,这时候我们就需要重写主机名。

这里就不需要重写路径了,而是需要 Hostname 字段。在下面的代码片段中,我们将主机名改写为 example.com

{
  "Matches": [
    {
      "Path": {},
      "BackendService": {},
      "Filters": [
        {
          "Type": "URLRewrite",
          "UrlRewrite": {
            "Hostname": "example.com"
          }
        }
      ]
    }
  ]
}

应用配置后,我们还是发送原来的请求,可以看到后端服务拿到的主机名为 example.com

curl http://localhost:8080/sample/api/hi
You are requesting example.com/sample/api/hi

9 - 流量镜像

本文档将介绍如何在不影响生产流量的前提下,将网络流量的副本发送到另一个服务。

介绍

FGW 的流量镜像功能主要用于在不影响生产流量的前提下,将网络流量的副本发送到另一个服务。这项功能常用于故障排查、性能监控、数据分析和安全审计等场景。通过流量镜像,可以实现实时数据捕获和分析,而不会对现有的业务流程造成任何干扰。

前置条件

使用 Pipy 模拟两个后端服务,这两个服务分别监听 80818082 端口。

pipy -e "pipy().listen(8081).serveHTTP(msg=>(console.log(msg.head),msg))"
pipy -e "pipy().listen(8082).serveHTTP(msg=>(console.log(msg.head),msg))"

当请求后端服务时,其会将请求的头部信息打印出来。

#request
curl localhost:8081/query
#log
2023-11-29 15:42:58.803 [INF] { protocol: "HTTP/1.1", headers: { "host": "localhost:8081", "user-agent": "curl/8.1.2", "accept": "*/*" }, method: "GET", scheme: undefined, authority: undefined, path: "/query" }

配置说明

如同在 策略概览 中提到的,流量镜像策略可以应用在服务/路由粒度。它的配置只有一个字段:

  • BackendService:指定请求应该镜像到哪个后端服务。

示例

{
  "Filters": [
    {
      "Type": "RequestMirror",
      "BackendService": "backendService2"
    }
  ]
}

配置

基础配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081。然后根据 HTTP 插件链配置,将 filter/request-mirror.js 插件配置在相应的配置。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "filter/request-mirror.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

通过 FGW 访问后端服务 curl localhost:8080/query,可以看到 8081 的后端服务会打印请求的头部信息。

2023-11-29 16:15:15.306 [INF] { protocol: "HTTP/1.1", headers: { "host": "127.0.0.1:8081", "user-agent": "curl/8.1.2", "accept": "*/*", "x-forwarded-for": "127.0.0.1" }, method: "GET", scheme: undefined, authority: undefined, path: "/query" }

路由粒度的流量镜像

修改路由的配置,在路由上添加 RequestMirror 配置,将流量镜像到后端服务 backendService2

{
  "Matches": [
    {
      "Path": {
        "Type": "Prefix",
        "Path": "/"
      },
      "BackendService": {
        "backendService1": {
          "Weight": 100
        }
      },
      "Filters": [
        {
          "Type": "RequestMirror",
          "BackendService": "backendService2"
        }
      ]
    }
  ]
}

同时,配置后端服务 backendService2 的端点。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    },
    "backendService2": {
      "Endpoints": {
        "127.0.0.1:8082": {
          "Weight": 100
        }
      }
    }
  }
}

此时再次发送请求,会发现后端服务 8082 也会同样打印出请求的信息。

2023-11-29 16:48:28.153 [INF] { protocol: "HTTP/1.1", headers: { "host": "127.0.0.1:8082", "user-agent": "curl/8.1.2", "accept": "*/*", "x-forwarded-for": "127.0.0.1" }, method: "GET", scheme: undefined, authority: undefined, path: "/query" }

服务粒度的流量镜像

在前面的配置基础上调整:将路由负载均衡到后端服务 80818082,但是在选择 8081 作为后端服务时,将请求镜像到 8082

{
  "Matches": [
    {
      "Path": {
        "Type": "Prefix",
        "Path": "/"
      },
      "BackendService": {
        "backendService1": {
          "Weight": 100,
          "Filters": [
            {
              "Type": "RequestMirror",
              "BackendService": "backendService2"
            }
          ]
        },
        "backendService2": {
          "Weight": 100
        }
      }
    }
  ]
}

在测试的时候,还是同样的请求会被均衡到两个后端服务,但 `8081` 的请求会被镜像到 `8082`,也就是说 `8082` 将会收到所有的请求。

10 - 故障注入

本篇文档将介绍如何在网关层面注入特定的故障来测试系统的行为和稳定性。

介绍

网关的故障注入功能允许在网络层面引入特定故障,如延迟和中断,以测试和评估应用在不同故障情况下的行为和稳定性。这个功能特别适合进行弹性和容错能力的测试。

这个功能特别适合进行弹性和容错能力的测试。

功能特点

  • 延迟注入:模拟网络延迟,观察应用对延迟的响应。
  • 错误注入:模拟服务错误响应,测试系统的恢复和容错机制。
  • 可配置的故障比例:可以设定故障注入的覆盖范围,例如只对一定比例的请求注入故障。

前置条件

后端服务可以使用 Pipy 来模拟一个返回正在请求的路径的服务。

pipy -e "pipy().listen(8081).serveHTTP(msg => new Message('You are requesting ' + msg.head.headers.host + msg.head.path))"

它会返回我们正在请求的路径。

curl http://localhost:8081/user
You are requesting localhost:8081/user

配置说明

FGW 提供了三种粒度的故障注入:网关全局域名路由

Fault: 用于配置故障注入策略。

  • Delay: 配置响应延时的参数。
    • Percent: 对请求注入延时的百分比,决定了有多少比例的请求会被注入延时。
    • Fixed: 指定固定的延时时间(如果设置),单位由 Unit 参数确定。
    • Range: 提供延时时间的随机范围,如 “0-100”,表示延时时间在0到100毫秒之间随机变动。
    • Unit: 延时时间的单位,支持 “ms”、“s”、“m” ,默认为毫秒(ms)。
  • Abort: 配置故障响应码的参数。
    • Percent: 注入特定错误响应的请求比例。
    • Status: 指定要注入的HTTP或GRPC错误状态码,如503表示服务不可用。
    • Message: 可选,提供错误响应的提示信息。

示例

在这个配置示例中,30% 的请求将遇到 50 至 300 毫秒之间的随机延迟,5% 的请求将收到带有 “Error injected” 消息的 503 错误响应。

{
  "Fault": {
    "Delay": {
      "Percent": 30,
      "Fixed": 2000,
      "Range": "50-300",
      "Unit": "ms"
    },
    "Abort": {
      "Percent": 5,
      "Status": 503,
      "Message": "Error injected"
    }
  }
}

配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081。然后根据 HTTP 插件链配置,将 http/fault-injection.js 插件配置在相应的配置。

{
  "Configs": {
    "DefaultPassthroughUpstreamPort": 443,
    "EnableDebug": true
  },
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/fault-injection.js",
      "http/service.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

路由粒度的故障注入

/user 路径增加一条路由,并在该路由上设置故障注入:50% 的概率下返回 503 的状态码,以及 Error injected 响应。

{
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/user"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            },
            "Fault": {
              "Abort": {
                "Percent": 50,
                "Status": 503,
                "Message": "Error injected"
              }
            }
          },
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  }
}

多次请求 /user 50% 的情况下会遇到 503 的错误,而请求其他路径正常返回。

curl -i localhost:8080/user

HTTP/1.1 503 Service Unavailable
content-length: 14
connection: keep-alive

Error injected

curl -i localhost:8080/user

HTTP/1.1 200 OK
content-length: 38
connection: keep-alive

You are requesting 127.0.0.1:8081/user

域名粒度的故障注入

删除路由上的故障注入配置。

接下来,我们在域名 * 上配置故障注入:100% 的情况下加入 2000ms 的延迟。

{
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ],
        "Fault": {
          "Delay": {
            "Percent": 100,
            "Fixed": 2000,
            "Unit": "ms"
          }
        }
      }
    }
  }
}

此时测试可以发现响应的耗时都在 2s 以上。

time curl localhost:8080/user
You are requesting 127.0.0.1:8081/usercurl localhost:8080/user  0.00s user 0.01s system 0% cpu 2.032 total

网关粒度的全局配置

删除域名上的故障注入配置。

添加全局配置,设置 100% 的情况下加入 1000ms 的延迟以及返回 401 Unauthorized! 响应。

{
  "Configs": {
    "DefaultPassthroughUpstreamPort": 443,
    "EnableDebug": true,
    "Fault": {
      "Delay": {
        "Percent": 100,
        "Fixed": 1000,
        "Unit": "ms"
      },
      "Abort": {
        "Percent": 100,
        "Status": 401,
        "Message": "Unauthorized!\n"
      }
    }
  }
}

测试。

time curl -i localhost:8080/user
HTTP/1.1 401 Unauthorized
content-length: 14
connection: keep-alive

Unauthorized!
curl -i localhost:8080/user  0.01s user 0.01s system 1% cpu 1.028 total

11 - 会话保持

会话保持功能允许用户的连续请求在一段时间内被定向到同一台后端服务器。本文档将介绍如何使用 FGW 的会话保持功能。

介绍

会话保持功能允许用户的连续请求在一段时间内被定向到同一台后端服务器。它通常在需要连续交互或状态跟踪的场景中被使用,例如在线购物车、用户登录状态或多步骤的事务处理。

前置条件

用于会话保持的两个服务,可以使用 Pipy 快速模拟。

#svr-1
pipy -e "pipy().listen(8081).serveHTTP(new Message({status: 200},'Hello, world'))"
#svr-2
pipy -e "pipy().listen(8082).serveHTTP(new Message({status: 503},'Service unavailable'))"

两个后端服务返回不同的状态码,用于区分会话保持是否生效。

curl -i http://localhost:8081
HTTP/1.1 200 OK
content-length: 12
connection: keep-alive

Hello, world

curl -i http://localhost:8082
HTTP/1.1 503 Service Unavailable
content-length: 19
connection: keep-alive

Service unavailable

配置说明

会话保持属于服务粒度的策略配置,可以参考 服务配置文档

  • StickyCookieName:使用 cookie 保持会话的负载均衡时的 cookie 名称。这个字段是非必须的,但当启用基于 cookie 的会话保持时,它定义了存储后端服务器信息的 cookie 的名称,例如 _srv_id。这意味着当用户首次访问应用时,一个名为 _srv_id 的 cookie 会被设置,其值通常对应于某个后端服务器。当该用户再次访问时,此 cookie 会确保他们的请求被路由到与之前相同的服务器。
  • StickyCookieExpires:使用 cookie 保持会话时,cookie 的有效期。这定义了 cookie 的存活时间,即多长时间内用户的连续请求会被定向到同一台后端服务器。参考值为 3600,意味着该 cookie 在一小时后过期。在此期间,所有来自同一用户的请求都会被路由到他们首次访问时的那台服务器。一旦 cookie 过期,下一次的请求可能会被路由到任何后端服务器,同时会收到一个新的 cookie。

示例

{
  "Services": {
    "backendService1": {
      "StickyCookieName": "_srv_id",
      "StickyCookieExpires": 3600,
      "Endpoints": {}
    }
  }
}

配置

负载均衡配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081127.0.0.1:8082;然后根据 HTTP 插件链配置,重试的功能在 http/forward.js" 插件中实现。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

使用同一个 cookie 多次请求会被负载均衡到不同的后端服务。

curl -b "_srv_id=client-1" localhost:8080
Service unavailable
curl -b "_srv_id=client-1" localhost:8080
Hello, world
curl -b "_srv_id=client-1" localhost:8080
Service unavailable

会话保持配置

添加会话保持的配置,使用 _srv_id 作为 cookie 名。

{
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        },
        "127.0.0.1:8082": {
          "Weight": 100
        }
      },
      "StickyCookieName": "_srv_id",
      "StickyCookieExpires":  3600
    }
  }
}

配置生效后,通过负载均衡器访问后端服务,负载均衡器会在响应的头部添加 cookie。

curl -i localhost:8080
HTTP/1.1 200 OK
set-cookie: _srv_id=7376698948304402; path=/; expires=Mon, 14 Aug 2023 16:14:33 GMT; max-age=3600
content-length: 12
connection: keep-alive

Hello, world

再次访问,但这次带上前面返回的 cookie。可以看到,每次都能获取到同样的结果,说明同一个会话(cookie 值相同)下,所有的请求都由相同的后端端点处理。

curl -b "_srv_id=7376698948304402" localhost:8080
Hello, world
curl -b "_srv_id=7376698948304402" localhost:8080
Hello, world
curl -b "_srv_id=7376698948304402" localhost:8080
Hello, world

12 - 黑白名单

本文档介绍如何使用 FGW 的黑白名单功能对访问来源进行控制

介绍

在微服务和 API 网关中,黑白名单功能是一种常用的安全机制,它可以根据指定的 IP 地址或 IP 地址范围来控制对服务或路由的访问。通过 FGW 的黑白名单功能,可以轻松地对 IP 进行访问控制,提高服务的安全性。

使用黑白名单,可以精确地控制哪些 IP 可以访问的服务,以及哪些 IP 应被禁止。这种功能在对外公开的 API 或面向特定客户或合作伙伴的服务中特别有用。

  • 黑名单:一旦 IP 地址被列入黑名单,来自这些 IP 地址的请求将被 FGW 拒绝。这通常用于封锁可疑的流量或已知的恶意 IP 地址。
  • 白名单:仅允许列在白名单中的 IP 地址访问。不在白名单上的所有 IP 都将被 FGW 拒绝。这通常用于确保只有特定的合作伙伴或内部 IP 可以访问特定的服务或路由。

黑白名单功能支持多种粒度的控制:端口、域名(服务)以及路由,通过在不同级别配置黑白名单来可以完成细粒度地灵活控制,可根据使用场景灵活选择。

  • 端口级别的控制在建立连接时就会进行检查,不被允许的 IP 会直接拒绝连接,效率会更好。
  • 服务和路由级别的控制粒度更小更灵活,但是需要在 HTTP 协议解码之后才可以进行检查,相比端口级别的黑白名单效率相对较低。

前置条件

这里的后端服务,可以使用 Docker 运行一个 httpbin 服务。

docker run --rm -d --name httpbin -p 8081:80 kennethreitz/httpbin

这个服务提供多个端点,比如 /ip/headers

curl -i http://localhost:8081/status/200
HTTP/1.1 200 OK
Server: gunicorn/19.9.0
Date: Thu, 31 Aug 2023 03:22:37 GMT
Connection: keep-alive
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Length: 0

curl http://localhost:8081/headers
{
  "headers": {
    "Accept": "*/*",
    "Host": "localhost:8081",
    "User-Agent": "curl/8.1.2"
  }
}

配置说明

访问控制列表(AccessControlLists)的配置字段:

  • blacklist:列出要阻止访问的 IP 地址或子网。例如, ["1.1.1.1", "2.2.2.0/24"]。如果指定了该配置,FGW 将拒绝列出的 IP 地址或范围内的所有请求。
  • whitelist:列出允许访问的 IP 地址或子网。例如, ["1.1.1.1", "2.2.2.0/24"]。如果指定了该配置,FGW 仅允许列出的 IP 地址或范围内的请求访问。
  • enableXFF:是否检查请求头部 x-forwarded-for 中的 IP 地址(逗号分隔),默认不检查。
  • status:拒绝访问后返回的响应状态码,不指定时默认为 403,可以用于定制响应。
  • message:拒绝访问后返回的响应内容,不指定时默认为空字符,可以用于定制响应。

示例配置

{
  "AccessControlLists": {
    "enableXFF": true,
    "status": "403",
    "message": "IP Forbidden",
    "blacklist": [
      "1.1.1.1",
      "2.2.2.0/24"
    ],
    "whitelist": [
      "3.3.3.3",
      "4.4.4.0/24"
    ]
  }
}

配置

负载均衡配置

我们借助在文档 HTTP 负载均衡 中的负载均衡器配置,修改服务的端点列表为我们的后端服务地址 127.0.0.1:8081;然后根据 HTTP 插件链配置,将 common/access-control.jshttp/access-control-domain.jshttp/access-control-route.js 插件配置在相应的配置。

这里我们会多加入一个监听端口 8079,与端口 8080 使用同样的负载均衡配置。当我们做端口级别的黑白名单时,方便进行进行对比验证。

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8079
    },
    {
      "Protocol": "HTTP",
      "Port": 8080
    }
  ],
  "RouteRules": {
    "8079,8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/ip"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          },
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  },
  "Services": {
    "backendService1": {
      "Endpoints": {
        "127.0.0.1:8081": {
          "Weight": 100
        }
      }
    }
  },
  "Chains": {
    "HTTPRoute": [
      "common/access-control.js",
      "common/consumer.js",
      "http/codec.js",
      "http/route.js",
      "http/service.js",
      "http/access-control-domain.js",
      "http/access-control-route.js",
      "http/forward.js",
      "http/default.js"
    ]
  }
}

通过负载均衡访问后端服务。

curl http://localhost:8080/ip
{
  "origin": "192.168.215.1"
}

curl http://localhost:8080/headers
{
  "headers": {
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Host": "localhost:8080",
    "User-Agent": "curl/8.1.2"
  }
}

curl http://localhost:8079/headers
{
  "headers": {
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Host": "localhost:8079",
    "User-Agent": "curl/8.1.2"
  }
}

端口级别黑白名单配置

我们在监听端口 8080 上配置黑名单,因为请求发起方和代理都位于同一机器上,IP 地址使用 127.0.0.1

{
  "Listeners": [
    {
      "Protocol": "HTTP",
      "Port": 8080,
      "AccessControlLists": {
        "blacklist": [
          "127.0.0.1"
        ]
      }
    }
  ]
}

我们发起请求验证是否生效。

curl http://localhost:8080/headers
curl: (56) Recv failure: Connection reset by peer

curl http://localhost:8079/headers
{
  "headers": {
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Host": "localhost:8079",
    "User-Agent": "curl/8.1.2"
  }
}

域名级别黑白名单设置

记得移除上面配置的端口级别黑白名单配置。

接下来,我们将同样的配置挂在域名 * 下。

{
  "RouteRules": {
    "8079,8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/ip"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          },
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ],
        "AccessControlLists": {
          "blacklist": [
            "127.0.0.1"
          ]
        }
      }
    }
  }
}

此时,访问 80808079 都会收到 403 的响应。

curl -i http://localhost:8080/headers
HTTP/1.1 403 Forbidden
content-length: 0
connection: keep-alive

curl -i http://localhost:8079/headers
HTTP/1.1 403 Forbidden
content-length: 0
connection: keep-alive

路由级别黑白名单设置

记得移除上面配置的端口级别黑白名单配置。

有些时候,我们希望更细粒度的控制,比如限制某些 IP 对某个路由的访问。我们将同样的配置挂在 /ip 路由下。

{
  "RouteRules": {
    "8079,8080": {
      "*": {
        "RouteType": "HTTP",
        "Matches": [
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/ip"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            },
            "AccessControlLists": {
              "blacklist": [
                "127.0.0.1"
              ]
            }
          },
          {
            "Path": {
              "Type": "Prefix",
              "Path": "/"
            },
            "BackendService": {
              "backendService1": {
                "Weight": 100
              }
            }
          }
        ]
      }
    }
  }
}

此时,我们访问 /ip 路由被限制,而其他的路由如 /headers 则无限制。

curl http://localhost:8080/headers
{
  "headers": {
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Host": "localhost:8080",
    "User-Agent": "curl/8.1.2"
  }
}

curl -i http://localhost:8080/ip
HTTP/1.1 403 Forbidden
content-length: 0
connection: keep-alive

curl -i http://localhost:8079/ip
HTTP/1.1 403 Forbidden
content-length: 0
connection: keep-alive