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。