PHP中的API Gateway实现:使用Swoole/RoadRunner实现高性能的请求路由与限流

PHP API Gateway 实现:Swoole/RoadRunner 高性能请求路由与限流

各位同学,大家好!今天我们来聊聊如何使用 PHP,特别是结合 Swoole 或 RoadRunner 这两个高性能框架,构建一个强大的 API Gateway,实现请求路由和限流功能。

API Gateway 在微服务架构中扮演着至关重要的角色,它是所有客户端请求的入口,负责请求的路由、认证、授权、限流、监控等功能。一个好的 API Gateway 可以有效地解耦客户端和后端服务,提高系统的可维护性和可扩展性,并提供更好的安全性和性能。

1. API Gateway 的核心功能

在深入代码之前,我们先明确 API Gateway 的核心功能:

  • 请求路由 (Request Routing): 根据请求的 URI、Header 或其他信息,将请求转发到不同的后端服务。
  • 认证与授权 (Authentication & Authorization): 验证客户端的身份,并判断其是否有权限访问特定的 API。
  • 限流 (Rate Limiting): 限制客户端在一定时间内可以发送的请求数量,防止恶意攻击和过载。
  • 监控 (Monitoring): 收集 API 的调用数据,例如请求数量、响应时间、错误率等,用于性能分析和故障排查。
  • 缓存 (Caching): 缓存 API 的响应数据,减少后端服务的负载,提高响应速度。
  • 请求转换与聚合 (Request Transformation & Aggregation): 对请求进行转换,例如修改请求头、添加参数等,或者将多个请求聚合为一个请求,减少客户端的请求数量。

2. 技术选型:Swoole vs RoadRunner

在 PHP 中构建高性能的 API Gateway,Swoole 和 RoadRunner 是两个非常流行的选择。它们都提供了协程、异步 IO 等特性,可以显著提高 PHP 应用的性能。

特性 Swoole RoadRunner
运行模式 常驻内存的服务器模式,基于事件循环 应用服务器模式,基于进程管理
开发难度 较高,需要深入理解协程和异步 IO 相对较低,更贴近传统的 PHP 开发模式
资源消耗 较低,因为是单进程多协程 较高,因为是多进程
适用场景 对性能要求极高,需要处理大量并发请求的应用 对性能有要求,但希望快速开发和部署的应用

Swoole 是一个基于 C 语言扩展的 PHP 异步、并发、高性能网络通信引擎。它提供了协程、TCP/UDP 服务器、HTTP 服务器、WebSocket 服务器等功能,可以用来构建各种高性能的 PHP 应用。

RoadRunner 是一个高性能的 PHP 应用服务器、负载均衡器和进程管理器。它使用 Go 语言编写,通过 RPC 与 PHP 进程通信,实现了零停机部署、热重启、进程监控等功能。

在今天的示例中,我们将重点介绍使用 Swoole 构建 API Gateway,因为它可以提供更高的性能。RoadRunner 的实现思路类似,只是在代码结构和部署方式上有所不同。

3. 使用 Swoole 构建 API Gateway 的基本框架

首先,我们需要创建一个 Swoole HTTP 服务器:

<?php

use SwooleHttpRequest;
use SwooleHttpResponse;
use SwooleHttpServer;

$server = new Server("0.0.0.0", 9501);

$server->on("start", function (Server $server) {
    echo "Swoole HTTP server is started at http://0.0.0.0:9501n";
});

$server->on("request", function (Request $request, Response $response) {
    // 在这里处理请求
    $path = $request->server['request_uri'];

    // 路由逻辑
    $backendService = routeRequest($path);

    if ($backendService) {
        // 发起后端服务请求
        $responseData = forwardRequest($backendService, $request);

        $response->header("Content-Type", "application/json");
        $response->end($responseData);
    } else {
        $response->status(404);
        $response->header("Content-Type", "text/plain");
        $response->end("404 Not Found");
    }
});

$server->start();

function routeRequest(string $path): ?string
{
    //  路由规则可以存储在配置文件或数据库中
    $routes = [
        '/users' => 'http://localhost:8001/users',
        '/products' => 'http://localhost:8002/products',
    ];

    if (isset($routes[$path])) {
        return $routes[$path];
    }

    return null;
}

function forwardRequest(string $backendService, Request $request): string
{
    // 使用 SwooleCoroutineHttpClient 发起异步请求
    $cli = new SwooleCoroutineHttpClient(parse_url($backendService, PHP_URL_HOST), parse_url($backendService, PHP_URL_PORT) ?? 80);

    // 传递请求方法
    $method = strtoupper($request->server['request_method']);
    $cli->setMethod($method);

    // 传递请求头
    foreach ($request->header as $key => $value) {
        $cli->setHeader($key, $value);
    }

    //传递cookie
    if(isset($request->cookie)){
        $cli->setCookies($request->cookie);
    }

    // 传递请求体
    if ($method === 'POST' || $method === 'PUT' || $method === 'PATCH') {
        $cli->setData($request->rawContent());
    }

    $cli->execute(parse_url($backendService, PHP_URL_PATH));

    $statusCode = $cli->getStatusCode();
    if($statusCode >= 200 && $statusCode < 300){
        $response = $cli->getBody();
    }else{
        $response = json_encode(['error'=>"backend service error, status code: $statusCode"]);
    }

    $cli->close();

    return $response;
}

这段代码创建了一个简单的 Swoole HTTP 服务器,它监听 9501 端口。当收到请求时,routeRequest 函数会根据请求的 URI 查找对应的后端服务地址,然后 forwardRequest 函数会使用 Swoole 协程 HTTP 客户端将请求转发到后端服务,并将后端服务的响应返回给客户端。

4. 实现限流功能

限流是 API Gateway 的一个重要功能,可以防止恶意攻击和过载。常见的限流算法有:

  • 计数器 (Counter): 记录单位时间内请求的数量,超过阈值则拒绝请求。
  • 滑动窗口 (Sliding Window): 将时间窗口划分为多个小窗口,记录每个小窗口内的请求数量,超过阈值则拒绝请求。
  • 漏桶 (Leaky Bucket): 将请求放入一个固定容量的桶中,桶以固定的速率漏水,如果桶满了则拒绝请求。
  • 令牌桶 (Token Bucket): 以固定的速率向桶中添加令牌,每个请求需要消耗一个令牌,如果桶中没有令牌则拒绝请求。

在这里,我们使用 令牌桶 算法来实现限流功能。

<?php

use SwooleHttpRequest;
use SwooleHttpResponse;
use SwooleHttpServer;

class TokenBucket
{
    private int $capacity;
    private float $rate;
    private float $tokens;
    private float $lastRefillTimestamp;

    public function __construct(int $capacity, float $rate)
    {
        $this->capacity = $capacity;
        $this->rate = $rate;
        $this->tokens = $capacity;
        $this->lastRefillTimestamp = microtime(true);
    }

    public function consume(int $tokens = 1): bool
    {
        $now = microtime(true);
        $this->refill($now);

        if ($this->tokens >= $tokens) {
            $this->tokens -= $tokens;
            return true;
        }

        return false;
    }

    private function refill(float $now): void
    {
        $elapsedTime = $now - $this->lastRefillTimestamp;
        $newTokens = $elapsedTime * $this->rate;
        $this->tokens = min($this->capacity, $this->tokens + $newTokens);
        $this->lastRefillTimestamp = $now;
    }
}

// 初始化令牌桶
$rateLimiters = [
    '/users' => new TokenBucket(100, 10), // 100 容量,每秒 10 个令牌
    '/products' => new TokenBucket(50, 5), // 50 容量,每秒 5 个令牌
];

$server = new Server("0.0.0.0", 9501);

$server->on("start", function (Server $server) {
    echo "Swoole HTTP server is started at http://0.0.0.0:9501n";
});

$server->on("request", function (Request $request, Response $response) {
    $path = $request->server['request_uri'];

    // 限流
    if (isset($rateLimiters[$path])) {
        if (!$rateLimiters[$path]->consume()) {
            $response->status(429);
            $response->header("Content-Type", "text/plain");
            $response->end("Too Many Requests");
            return;
        }
    }

    // 路由逻辑
    $backendService = routeRequest($path);

    if ($backendService) {
        // 发起后端服务请求
        $responseData = forwardRequest($backendService, $request);

        $response->header("Content-Type", "application/json");
        $response->end($responseData);
    } else {
        $response->status(404);
        $response->header("Content-Type", "text/plain");
        $response->end("404 Not Found");
    }
});

$server->start();

function routeRequest(string $path): ?string
{
    //  路由规则可以存储在配置文件或数据库中
    $routes = [
        '/users' => 'http://localhost:8001/users',
        '/products' => 'http://localhost:8002/products',
    ];

    if (isset($routes[$path])) {
        return $routes[$path];
    }

    return null;
}

function forwardRequest(string $backendService, Request $request): string
{
    // 使用 SwooleCoroutineHttpClient 发起异步请求
    $cli = new SwooleCoroutineHttpClient(parse_url($backendService, PHP_URL_HOST), parse_url($backendService, PHP_URL_PORT) ?? 80);

    // 传递请求方法
    $method = strtoupper($request->server['request_method']);
    $cli->setMethod($method);

    // 传递请求头
    foreach ($request->header as $key => $value) {
        $cli->setHeader($key, $value);
    }

    //传递cookie
    if(isset($request->cookie)){
        $cli->setCookies($request->cookie);
    }

    // 传递请求体
    if ($method === 'POST' || $method === 'PUT' || $method === 'PATCH') {
        $cli->setData($request->rawContent());
    }

    $cli->execute(parse_url($backendService, PHP_URL_PATH));

    $statusCode = $cli->getStatusCode();
    if($statusCode >= 200 && $statusCode < 300){
        $response = $cli->getBody();
    }else{
        $response = json_encode(['error'=>"backend service error, status code: $statusCode"]);
    }

    $cli->close();

    return $response;
}

这段代码实现了一个 TokenBucket 类,它维护了一个令牌桶。在 request 事件处理函数中,我们首先检查请求的 URI 是否需要限流,如果需要,则调用 TokenBucket::consume() 方法尝试消耗一个令牌。如果令牌桶中没有足够的令牌,则返回 429 Too Many Requests 错误。

5. 认证与授权

认证和授权是 API Gateway 的另一个重要功能,可以保护后端服务免受未经授权的访问。常见的认证方式有:

  • Basic Authentication: 客户端在请求头中携带用户名和密码。
  • API Key: 客户端在请求头或查询参数中携带 API Key。
  • OAuth 2.0: 客户端使用 OAuth 2.0 协议获取访问令牌,然后在请求头中携带访问令牌。
  • JWT (JSON Web Token): 客户端使用 JWT 令牌进行身份验证。

在这里,我们使用 JWT 认证方式来实现认证和授权功能。

<?php

use FirebaseJWTJWT;
use FirebaseJWTKey;
use SwooleHttpRequest;
use SwooleHttpResponse;
use SwooleHttpServer;

// 秘钥,应该保存在安全的地方
$secretKey = 'your_secret_key';

// 模拟用户数据
$users = [
    'admin' => ['password' => 'password', 'roles' => ['admin']],
    'user' => ['password' => 'password', 'roles' => ['user']],
];

function generateJWT(string $username, string $secretKey): string
{
    global $users;
    $payload = [
        'iss' => 'api-gateway',
        'aud' => 'backend-service',
        'iat' => time(),
        'nbf' => time(),
        'exp' => time() + 3600, // 1小时过期
        'username' => $username,
        'roles' => $users[$username]['roles'],
    ];

    return JWT::encode($payload, $secretKey, 'HS256');
}

function authenticate(Request $request, string $secretKey): ?array
{
    $authHeader = $request->header['authorization'] ?? '';
    if (strpos($authHeader, 'Bearer ') === 0) {
        $token = substr($authHeader, 7);
        try {
            $decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
            return (array) $decoded;
        } catch (Exception $e) {
            // Token验证失败
            return null;
        }
    }
    return null;
}

function authorize(array $decodedToken, string $requiredRole): bool
{
    if (isset($decodedToken['roles']) && in_array($requiredRole, $decodedToken['roles'])) {
        return true;
    }
    return false;
}

$server = new Server("0.0.0.0", 9501);

$server->on("start", function (Server $server) {
    echo "Swoole HTTP server is started at http://0.0.0.0:9501n";
});

$server->on("request", function (Request $request, Response $response) {

    $path = $request->server['request_uri'];

    // 登录接口
    if ($path === '/login') {
        $username = $request->post['username'] ?? '';
        $password = $request->post['password'] ?? '';

        global $users;
        if (isset($users[$username]) && $users[$username]['password'] === $password) {
            $jwt = generateJWT($username, $secretKey);
            $response->header("Content-Type", "application/json");
            $response->end(json_encode(['token' => $jwt]));
            return;
        } else {
            $response->status(401);
            $response->header("Content-Type", "application/json");
            $response->end(json_encode(['error' => 'Invalid credentials']));
            return;
        }
    }

    // 身份验证
    $decodedToken = authenticate($request, $secretKey);
    if (!$decodedToken) {
        $response->status(401);
        $response->header("Content-Type", "application/json");
        $response->end(json_encode(['error' => 'Unauthorized']));
        return;
    }

    // 授权
    if ($path === '/admin' && !authorize($decodedToken, 'admin')) {
        $response->status(403);
        $response->header("Content-Type", "application/json");
        $response->end(json_encode(['error' => 'Forbidden']));
        return;
    }

    // 路由逻辑
    $backendService = routeRequest($path);

    if ($backendService) {
        // 发起后端服务请求
        $responseData = forwardRequest($backendService, $request, $decodedToken);

        $response->header("Content-Type", "application/json");
        $response->end($responseData);
    } else {
        $response->status(404);
        $response->header("Content-Type", "text/plain");
        $response->end("404 Not Found");
    }
});

$server->start();

function routeRequest(string $path): ?string
{
    //  路由规则可以存储在配置文件或数据库中
    $routes = [
        '/users' => 'http://localhost:8001/users',
        '/products' => 'http://localhost:8002/products',
        '/admin' => 'http://localhost:8003/admin', // 需要 admin 权限
    ];

    if (isset($routes[$path])) {
        return $routes[$path];
    }

    return null;
}

function forwardRequest(string $backendService, Request $request, array $decodedToken): string
{
    // 使用 SwooleCoroutineHttpClient 发起异步请求
    $cli = new SwooleCoroutineHttpClient(parse_url($backendService, PHP_URL_HOST), parse_url($backendService, PHP_URL_PORT) ?? 80);

    // 传递请求方法
    $method = strtoupper($request->server['request_method']);
    $cli->setMethod($method);

    // 传递请求头
    foreach ($request->header as $key => $value) {
        $cli->setHeader($key, $value);
    }

    //传递cookie
    if(isset($request->cookie)){
        $cli->setCookies($request->cookie);
    }

    // 将用户信息传递给后端服务
    $cli->setHeader('X-User-Id', $decodedToken['username'] ?? ''); // 传递用户名,可以传递更多信息
    $cli->setHeader('X-User-Roles', implode(',', $decodedToken['roles'] ?? []));

    // 传递请求体
    if ($method === 'POST' || $method === 'PUT' || $method === 'PATCH') {
        $cli->setData($request->rawContent());
    }

    $cli->execute(parse_url($backendService, PHP_URL_PATH));

    $statusCode = $cli->getStatusCode();
    if($statusCode >= 200 && $statusCode < 300){
        $response = $cli->getBody();
    }else{
        $response = json_encode(['error'=>"backend service error, status code: $statusCode"]);
    }

    $cli->close();

    return $response;
}

这段代码使用了 firebase/php-jwt 库来生成和验证 JWT 令牌。它实现了以下功能:

  • /login 接口:用于生成 JWT 令牌。
  • authenticate 函数:用于验证 JWT 令牌。
  • authorize 函数:用于检查用户是否具有访问特定 API 的权限。

request 事件处理函数中,我们首先验证 JWT 令牌,如果令牌无效则返回 401 Unauthorized 错误。然后,我们检查用户是否具有访问特定 API 的权限,如果没有则返回 403 Forbidden 错误。

6. 其他功能

除了请求路由、限流、认证和授权之外,API Gateway 还可以实现其他功能,例如:

  • 监控: 使用 Swoole 的 stats 功能或第三方监控工具,收集 API 的调用数据。
  • 缓存: 使用 Redis 或 Memcached 等缓存系统,缓存 API 的响应数据。
  • 请求转换与聚合: 使用 PHP 的字符串处理函数或第三方库,对请求进行转换,或者将多个请求聚合为一个请求。
  • 日志记录: 使用 Swoole 的 log 功能或第三方日志库,记录 API 的调用日志。

7. 总结:构建高性能、安全的 API 网关

通过以上示例,我们了解了如何使用 Swoole 构建一个高性能的 API Gateway,实现请求路由、限流和认证授权功能。 结合具体业务场景选择合适的限流算法和认证方式,确保API的安全性和稳定性。 可以根据实际需求添加更多功能,例如监控、缓存、请求转换等,打造一个功能完善的 API Gateway。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注