MidКейс29 min

API Gateway

Проектирование API Gateway: маршрутизация, аутентификация, rate limiting, трансформация запросов и middleware pipeline

API Gateway -- единая точка входа для всех клиентских запросов к микросервисам. Отвечает за маршрутизацию, аутентификацию, rate limiting, логирование и трансформацию запросов/ответов.

Шаг 1: Требования

Функциональные требования

  1. Маршрутизация запросов к backend-сервисам
  2. Аутентификация и авторизация (JWT, API Key)
  3. Rate limiting (per user, per IP, per endpoint)
  4. Трансформация запросов и ответов
  5. Агрегация ответов от нескольких сервисов
  6. Health checks для backend-сервисов
  7. Логирование и мониторинг всех запросов

Нефункциональные требования

  1. Latency overhead < 10ms
  2. Доступность 99.99%
  3. 100K+ RPS
  4. Горячее обновление конфигурации (без рестарта)

Шаг 2: High-Level архитектура

┌──────────┐    ┌──────────────────────────────────────────┐
│  Client  │───>│              API Gateway                 │
│          │    │  ┌──────────────────────────────────────┐ │
└──────────┘    │  │         Middleware Pipeline          │ │
                │  │                                      │ │
                │  │  ┌─────┐ ┌──────┐ ┌─────┐ ┌──────┐  │ │
                │  │  │CORS │→│Auth  │→│Rate │→│Trans │  │ │
                │  │  │     │ │      │ │Limit│ │form  │  │ │
                │  │  └─────┘ └──────┘ └─────┘ └──────┘  │ │
                │  └──────────────────────────────────────┘ │
                │                                          │
                │  ┌──────────────────────────────────────┐ │
                │  │           Router                     │ │
                │  └──────────┬──────────────┬────────────┘ │
                └─────────────┼──────────────┼──────────────┘
                              │              │
                    ┌─────────▼──┐    ┌──────▼──────┐
                    │  User      │    │  Order      │
                    │  Service   │    │  Service    │
                    └────────────┘    └─────────────┘

Шаг 3: Детальный дизайн

3.1 Middleware Pipeline

<?php

declare(strict_types=1);

interface Middleware
{
    public function handle(Request $request, callable $next): Response;
}

final class MiddlewarePipeline
{
    /** @var Middleware[] */
    private array $middlewares = [];

    public function pipe(Middleware $middleware): self
    {
        $this->middlewares[] = $middleware;
        return $this;
    }

    public function process(Request $request): Response
    {
        $pipeline = array_reduce(
            array_reverse($this->middlewares),
            fn (callable $next, Middleware $middleware) => fn (Request $req) => $middleware->handle($req, $next),
            fn (Request $req) => new Response(404, 'Not Found'),
        );

        return $pipeline($request);
    }
}

// Gateway setup
final class ApiGateway
{
    private MiddlewarePipeline $pipeline;

    public function __construct(
        private readonly RouteRegistry $routes,
        private readonly ServiceRegistry $services,
    ) {
        $this->pipeline = new MiddlewarePipeline();

        // Order matters: executed top to bottom
        $this->pipeline
            ->pipe(new RequestIdMiddleware())
            ->pipe(new CorsMiddleware())
            ->pipe(new RequestLoggingMiddleware())
            ->pipe(new AuthenticationMiddleware())
            ->pipe(new RateLimitMiddleware())
            ->pipe(new RequestValidationMiddleware())
            ->pipe(new CircuitBreakerMiddleware())
            ->pipe(new ProxyMiddleware($this->routes, $this->services));
    }

    public function handle(Request $request): Response
    {
        return $this->pipeline->process($request);
    }
}
### 3.2 Аутентификация (JWT)
<?php

declare(strict_types=1);

final class AuthenticationMiddleware implements Middleware
{
    private const PUBLIC_PATHS = [
        '/api/v1/auth/login',
        '/api/v1/auth/register',
        '/api/v1/health',
    ];

    public function __construct(
        private readonly JwtValidator $jwtValidator,
        private readonly ApiKeyRepository $apiKeyRepo,
    ) {}

    public function handle(Request $request, callable $next): Response
    {
        // Skip auth for public endpoints
        if ($this->isPublicPath($request->getPath())) {
            return $next($request);
        }

        // Try JWT first
        $authHeader = $request->getHeader('Authorization');
        if ($authHeader !== null && str_starts_with($authHeader, 'Bearer ')) {
            $token = substr($authHeader, 7);
            return $this->authenticateJwt($token, $request, $next);
        }

        // Try API Key
        $apiKey = $request->getHeader('X-API-Key');
        if ($apiKey !== null) {
            return $this->authenticateApiKey($apiKey, $request, $next);
        }

        return new Response(401, json_encode([
            'error' => 'Authentication required',
            'code' => 'AUTH_REQUIRED',
        ]));
    }

    private function authenticateJwt(string $token, Request $request, callable $next): Response
    {
        try {
            $claims = $this->jwtValidator->validate($token);

            // Inject user context into request
            $request = $request->withAttribute('user_id', $claims['sub']);
            $request = $request->withAttribute('roles', $claims['roles'] ?? []);

            return $next($request);
        } catch (TokenExpiredException) {
            return new Response(401, json_encode([
                'error' => 'Token expired',
                'code' => 'TOKEN_EXPIRED',
            ]));
        } catch (InvalidTokenException) {
            return new Response(401, json_encode([
                'error' => 'Invalid token',
                'code' => 'INVALID_TOKEN',
            ]));
        }
    }

    private function authenticateApiKey(string $apiKey, Request $request, callable $next): Response
    {
        $keyData = $this->apiKeyRepo->findByKey($apiKey);

        if ($keyData === null || !$keyData->isActive) {
            return new Response(401, json_encode([
                'error' => 'Invalid API key',
                'code' => 'INVALID_API_KEY',
            ]));
        }

        $request = $request->withAttribute('api_key_id', $keyData->id);
        $request = $request->withAttribute('permissions', $keyData->permissions);

        return $next($request);
    }

    private function isPublicPath(string $path): bool
    {
        foreach (self::PUBLIC_PATHS as $publicPath) {
            if (str_starts_with($path, $publicPath)) {
                return true;
            }
        }
        return false;
    }
}
### 3.3 Маршрутизация и Proxy
<?php

declare(strict_types=1);

final class RouteRegistry
{
    /** @var array<string, RouteConfig> */
    private array $routes = [];

    public function register(string $pattern, RouteConfig $config): void
    {
        $this->routes[$pattern] = $config;
    }

    public function match(string $method, string $path): ?RouteMatch
    {
        foreach ($this->routes as $pattern => $config) {
            if ($this->matchesPattern($method, $path, $pattern, $config)) {
                $params = $this->extractParams($path, $pattern);
                return new RouteMatch($config, $params);
            }
        }

        return null;
    }

    private function matchesPattern(
        string $method,
        string $path,
        string $pattern,
        RouteConfig $config,
    ): bool {
        if (!in_array($method, $config->methods, true)) {
            return false;
        }

        $regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
        return (bool) preg_match("#^{$regex}$#", $path);
    }

    private function extractParams(string $path, string $pattern): array
    {
        $regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
        preg_match("#^{$regex}$#", $path, $matches);

        return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
    }
}

final readonly class RouteConfig
{
    public function __construct(
        public string $serviceName,
        public string $targetPath,
        public array $methods = ['GET', 'POST', 'PUT', 'DELETE'],
        public bool $authRequired = true,
        public ?string $rateLimit = null,
        public int $timeoutMs = 5000,
    ) {}
}

final class ProxyMiddleware implements Middleware
{
    public function __construct(
        private readonly RouteRegistry $routes,
        private readonly ServiceRegistry $services,
        private readonly HttpClient $httpClient,
    ) {}

    public function handle(Request $request, callable $next): Response
    {
        $match = $this->routes->match($request->getMethod(), $request->getPath());

        if ($match === null) {
            return new Response(404, json_encode([
                'error' => 'Route not found',
            ]));
        }

        $service = $this->services->getHealthy($match->config->serviceName);

        if ($service === null) {
            return new Response(503, json_encode([
                'error' => 'Service unavailable',
            ]));
        }

        // Build target URL
        $targetUrl = $service->baseUrl . $match->config->targetPath;
        foreach ($match->params as $key => $value) {
            $targetUrl = str_replace("{{$key}}", $value, $targetUrl);
        }

        // Forward request
        try {
            return $this->httpClient->request(
                method: $request->getMethod(),
                url: $targetUrl,
                headers: $this->forwardHeaders($request),
                body: $request->getBody(),
                timeoutMs: $match->config->timeoutMs,
            );
        } catch (TimeoutException) {
            return new Response(504, json_encode([
                'error' => 'Gateway timeout',
            ]));
        }
    }

    private function forwardHeaders(Request $request): array
    {
        $headers = $request->getHeaders();

        // Add gateway context
        $headers['X-Request-Id'] = $request->getAttribute('request_id');
        $headers['X-User-Id'] = $request->getAttribute('user_id', '');
        $headers['X-Forwarded-For'] = $request->getClientIp();
        $headers['X-Gateway-Time'] = (string) microtime(true);

        // Remove hop-by-hop headers
        unset($headers['Connection'], $headers['Keep-Alive']);

        return $headers;
    }
}
### 3.4 Circuit Breaker
<?php

declare(strict_types=1);

final class CircuitBreakerMiddleware implements Middleware
{
    private const FAILURE_THRESHOLD = 5;
    private const RECOVERY_TIMEOUT = 30; // seconds

    public function __construct(
        private readonly \Redis $redis,
    ) {}

    public function handle(Request $request, callable $next): Response
    {
        $serviceName = $request->getAttribute('target_service');
        $state = $this->getState($serviceName);

        if ($state === 'open') {
            if ($this->shouldAttemptRecovery($serviceName)) {
                // Half-open: allow one request through
                $this->setState($serviceName, 'half-open');
            } else {
                return new Response(503, json_encode([
                    'error' => 'Service temporarily unavailable',
                    'retry_after' => $this->getRetryAfter($serviceName),
                ]));
            }
        }

        try {
            $response = $next($request);

            if ($response->getStatusCode() >= 500) {
                $this->recordFailure($serviceName);
            } else {
                $this->recordSuccess($serviceName);
            }

            return $response;
        } catch (\Throwable $e) {
            $this->recordFailure($serviceName);
            throw $e;
        }
    }

    private function recordFailure(string $service): void
    {
        $key = "circuit:{$service}:failures";
        $count = $this->redis->incr($key);
        $this->redis->expire($key, 60);

        if ($count >= self::FAILURE_THRESHOLD) {
            $this->setState($service, 'open');
            $this->redis->set(
                "circuit:{$service}:opened_at",
                time(),
                ['EX' => self::RECOVERY_TIMEOUT * 2],
            );
        }
    }

    private function recordSuccess(string $service): void
    {
        $state = $this->getState($service);

        if ($state === 'half-open') {
            $this->setState($service, 'closed');
            $this->redis->del("circuit:{$service}:failures");
        }
    }

    private function getState(string $service): string
    {
        return $this->redis->get("circuit:{$service}:state") ?: 'closed';
    }

    private function setState(string $service, string $state): void
    {
        $this->redis->set("circuit:{$service}:state", $state, ['EX' => 300]);
    }

    private function shouldAttemptRecovery(string $service): bool
    {
        $openedAt = (int) $this->redis->get("circuit:{$service}:opened_at");
        return (time() - $openedAt) >= self::RECOVERY_TIMEOUT;
    }

    private function getRetryAfter(string $service): int
    {
        $openedAt = (int) $this->redis->get("circuit:{$service}:opened_at");
        $retryAt = $openedAt + self::RECOVERY_TIMEOUT;
        return max(0, $retryAt - time());
    }
}
### 3.5 Service Registry и Health Checks
<?php

declare(strict_types=1);

final class ServiceRegistry
{
    /** @var array<string, ServiceInstance[]> */
    private array $services = [];

    public function __construct(
        private readonly \Redis $redis,
    ) {
        $this->loadFromConfig();
    }

    public function getHealthy(string $serviceName): ?ServiceInstance
    {
        $instances = $this->services[$serviceName] ?? [];
        $healthy = array_filter($instances, fn (ServiceInstance $i) => $i->isHealthy);

        if (empty($healthy)) {
            return null;
        }

        // Round-robin among healthy instances
        $index = $this->getNextIndex($serviceName, count($healthy));
        return array_values($healthy)[$index];
    }

    public function checkHealth(): void
    {
        foreach ($this->services as $name => $instances) {
            foreach ($instances as $instance) {
                $instance->isHealthy = $this->ping($instance);
            }
        }
    }

    private function ping(ServiceInstance $instance): bool
    {
        try {
            $ch = curl_init($instance->baseUrl . '/health');
            curl_setopt_array($ch, [
                CURLOPT_TIMEOUT => 2,
                CURLOPT_RETURNTRANSFER => true,
            ]);

            curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            return $httpCode === 200;
        } catch (\Throwable) {
            return false;
        }
    }

    private function getNextIndex(string $service, int $totalHealthy): int
    {
        $counter = $this->redis->incr("rr:{$service}");
        return $counter % $totalHealthy;
    }
}

final class ServiceInstance
{
    public bool $isHealthy = true;

    public function __construct(
        public readonly string $id,
        public readonly string $baseUrl,
        public readonly int $weight = 1,
    ) {}
}
## Шаг 4: Конфигурация маршрутов
<?php

declare(strict_types=1);

// routes.php -- loaded on startup, hot-reloadable
return [
    '/api/v1/users/{id}' => new RouteConfig(
        serviceName: 'user-service',
        targetPath: '/users/{id}',
        methods: ['GET', 'PUT'],
        timeoutMs: 3000,
    ),
    '/api/v1/orders' => new RouteConfig(
        serviceName: 'order-service',
        targetPath: '/orders',
        methods: ['GET', 'POST'],
        rateLimit: '100/min',
        timeoutMs: 5000,
    ),
    '/api/v1/payments' => new RouteConfig(
        serviceName: 'payment-service',
        targetPath: '/payments',
        methods: ['POST'],
        rateLimit: '10/min',
        timeoutMs: 30000,
    ),
];
## Шаг 5: Масштабирование
Компонент Стратегия
Gateway instances Horizontal scaling за Load Balancer
Config Centralized config (etcd/Consul), hot reload
Rate limit state Redis Cluster
Service discovery Consul / etcd / Kubernetes
Logging Async (Kafka -> ELK)

Возможные вопросы интервьюера

  1. API Gateway vs Service Mesh?

    • Gateway: north-south traffic (client -> services)
    • Service Mesh: east-west traffic (service -> service)
    • Можно использовать оба
  2. Как избежать Single Point of Failure?

    • Несколько экземпляров за LB
    • Active-passive или active-active
    • DNS failover
  3. Как обновлять конфигурацию без downtime?

    • Hot reload через watch на config store
    • Blue-green deployment для gateway
  4. Как агрегировать ответы от нескольких сервисов?

    • Parallel requests с timeout
    • GraphQL-like query composition
    • BFF (Backend for Frontend) pattern