API 网关与服务发现
动态路由、聚合并定位服务
API 网关与服务发现 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
网关与服务发现
一旦拥有十几个服务,就会出现两个问题。客户端不应该需要知道每个服务的地址,也不应该为了渲染一个页面而调用其中五个服务——这正是 API 网关 的职责。会动态扩缩容并更换 IP 的服务需要一种彼此查找的方式——这就是 服务发现。
本课将分别讲解这两个主题,并使用 PHP 实现网关边缘层。
API 网关的作用
API 网关是位于各项服务前方的统一入口。典型职责包括:
- 将请求路由到正确的后端。
- 横切关注点:身份验证、速率限制、CORS、TLS 终止。
- 聚合:将多个后端调用组合成一次客户端响应。
- 协议转换:外部 REST 请求进入,内部 gRPC 请求发出。
它让客户端保持简单,并集中管理原本需要在每个服务中重复的策略。
边缘路由
从核心上看,网关会将传入路径映射到上游服务。生产环境中的网关(Kong、Traefik、Nginx、AWS API Gateway)通常以声明式方式完成这项工作,但这段逻辑足够简单,可以用 PHP 加以演示。
<?php
$routes = [
'#^/api/orders#' => 'http://orders-svc',
'#^/api/customers#' => 'http://customers-svc',
'#^/api/catalog#' => 'http://catalog-svc',
];
function resolveUpstream(string $path, array $routes): ?string {
foreach ($routes as $pattern => $upstream) {
if (preg_match($pattern, $path)) {
return $upstream . $path;
}
}
return null; // 404 at the gateway
}
echo resolveUpstream('/api/orders/42', $routes), "\n";集中处理身份验证
在网关处验证调用方一次,然后向下游转发可信身份,这样每个服务就不必重新验证原始令牌。网关会检查 JWT 的签名和过期时间,并将 X-User-Id 等标头注入内部请求中(通过受信任的网络)。
<?php
function authenticate(string $authHeader): ?array {
if (!str_starts_with($authHeader, 'Bearer ')) return null;
$jwt = substr($authHeader, 7);
$claims = verifyJwt($jwt); // signature + exp check
if ($claims === null) return null;
// Forward minimal trusted identity to internal services
return ['X-User-Id' => $claims['sub'], 'X-Scopes' => implode(',', $claims['scopes'])];
}
function verifyJwt(string $j): ?array { return ['sub' => 'u-7', 'scopes' => ['orders:read']]; }
print_r(authenticate('Bearer abc.def.ghi'));响应聚合
移动端页面可能需要订单、客户和目录数据。与其让客户端发起三次调用,不如由网关向多个服务发起请求、等待结果并合并响应。为了保持速度,应并发发起上游调用(使用 Guzzle 异步请求 / curl_multi),而不是串行调用。
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Promise\Utils;
$http = new Client(['timeout' => 2.0]);
$promises = [
'order' => $http->getAsync('http://orders-svc/orders/42'),
'customer' => $http->getAsync('http://customers-svc/customers/7'),
'catalog' => $http->getAsync('http://catalog-svc/items?order=42'),
];
$results = Utils::settle($promises)->wait(); // run in parallel
// merge the fulfilled bodies into one response for the client前端专用后端
一个通用网关通常无法同样出色地服务网页应用、移动应用和合作伙伴——它们各自需要不同的聚合方式和数据负载结构。前端专用后端(BFF)模式为每种客户端类型提供一个专属的轻量网关,针对其需求进行定制,同时保持共享服务的通用性。
这样可以避免网关变成臃肿的全能网关,也让各个客户端团队能够独立推进。
服务发现问题
在动态环境中,实例会不断出现和消失,其 IP 也会发生变化。将 http://10.0.3.14:8080 硬编码进去十分脆弱。服务发现会维护一份实时注册表,记录“当前服务 X 存在哪些健康实例”,这样调用方就能在调用时将逻辑名称解析为真实地址。
客户端发现与服务端发现
有两种模式:
- 客户端发现——调用方查询注册表(Consul、etcd),自行选择实例并进行负载均衡。
- 服务端发现——调用方访问一个稳定的虚拟地址(负载均衡器 / Kubernetes 服务),由该地址为其完成解析和负载均衡。
在 Kubernetes 中,您通常可以免费获得服务端发现:调用 http://customers-svc,集群 DNS 和服务对象会处理其余工作。在 k8s 之外,Consul 风格的注册表很常见。
查询注册表
使用客户端发现时,PHP 调用方会向注册表请求健康实例并选择其中一个。注册表只会返回通过健康检查的实例,因此故障节点会被自动排除。
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
function discover(Client $http, string $service): string {
// Consul: only passing-health instances
$res = $http->get("http://consul:8500/v1/health/service/$service?passing=true");
$nodes = json_decode((string) $res->getBody(), true);
if (!$nodes) throw new \RuntimeException("No healthy $service");
$pick = $nodes[array_rand($nodes)]['Service']; // simple LB
return "http://{$pick['Address']}:{$pick['Port']}";
}健康检查与注册
服务发现的可靠程度取决于其健康状态数据。每个服务都会提供一个 /health 端点,用于检查真实依赖项(数据库、缓存),并在启动时自行注册(或由平台代为注册)。注册表会探测该端点,并移除检查失败的实例。
请让健康检查真正有意义:数据库已宕机时仍返回 200,比毫无用处更糟,因为这样会把流量路由到已损坏的节点。
<?php
// GET /health
function health(PDO $db, Redis $cache): array {
$checks = [
'db' => safe(fn() => $db->query('SELECT 1') !== false),
'cache' => safe(fn() => $cache->ping() === '+PONG'),
];
$ok = !in_array(false, $checks, true);
http_response_code($ok ? 200 : 503);
return ['status' => $ok ? 'pass' : 'fail', 'checks' => $checks];
}
function safe(callable $c): bool { try { return (bool) $c(); } catch (\Throwable) { return false; } }存活性与就绪性
只有一个健康端点还不够——您需要区分两个问题:
- 存活性:“进程是否存活?”如果检查失败,编排器会重启容器。请让它成本低且不依赖外部依赖,否则不稳定的数据库会触发毫无意义的重启。
- 就绪性:“它现在能处理流量吗?”如果检查失败,流量会被暂不转发,但进程会继续运行(例如正在预热缓存,或数据库暂时无法访问)。
混淆这两个概念会导致重启循环,或将流量路由到尚未就绪的节点。
<?php
// GET /livez - is the process itself healthy? (no external deps)
function livez(): void { http_response_code(200); echo 'alive'; }
// GET /readyz - should we receive traffic? (checks dependencies)
function readyz(PDO $db): void {
try { $db->query('SELECT 1'); http_response_code(200); echo 'ready'; }
catch (\Throwable) { http_response_code(503); echo 'not ready'; }
}快速检查
减少客户端往返次数。
回顾
路由和定位服务:
- API 网关集中处理路由、身份验证、速率限制、TLS 和聚合。
- 应并发聚合;当客户端需求出现差异时,使用BFF。
- 服务发现将逻辑名称解析为实时且健康的实例。
- 服务发现分为客户端发现(查询注册表)和服务端发现(稳定的 LB / k8s 服务)。
- 有实际意义的健康检查可以让流量避开故障节点。
下一步:当部分服务不可避免地发生故障时,如何让所有这些调用保持可靠。
常见问题解答
「API 网关与服务发现」课时是免费的吗?
是的 — 「API 网关与服务发现」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「API 网关与服务发现」这节课中我会学到什么?
动态路由、聚合并定位服务 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「API 网关与服务发现」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 从单体架构到微服务
- 服务通信:REST 和 gRPC
- API 网关与服务发现
- 韧性:熔断器与重试