本文目录导读:

在 PHP 中实现服务发现,主要有以下几种方案,我按照使用场景和复杂度来介绍:
基于 DNS 的服务发现(推荐)
这是最简单且最轻量的方案,利用 DNS SRV 记录:
配置示例:
<?php
// etcd/consul 等注册服务时写入 SRV 记录
// _service._tcp.service.consul. 86400 IN SRV 0 5 8080 node1.service.consul.
// _service._tcp.service.consul. 86400 IN SRV 0 5 8080 node2.service.consul.
$dnsResult = dns_get_record('_service._tcp.consul.', DNS_SRV);
foreach ($dnsResult as $record) {
echo "Target: {$record['target']}:{$record['port']}\n";
// 根据 target 再做 A 记录解析
}
?>
使用场景:
- 使用 Consul DNS 服务
- 使用 Kubernetes DNS(CoreDNS)
- 使用独立的 DNS 服务器
使用 Etcd + Watch 机制
这是动态服务发现的常见方案:
<?php
use Etcd\Client;
class EtcdServiceDiscovery
{
private $client;
public function __construct()
{
$this->client = new Client([
'host' => 'etcd-server:2379'
]);
}
// 注册服务
public function registerService($serviceName, $instanceId, $address, $port)
{
$key = "/services/{$serviceName}/{$instanceId}";
$value = json_encode(['address' => $address, 'port' => $port]);
$this->client->put($key, $value);
// 设置租约(TTL)实现心跳
$lease = $this->client->grant(10); // 10秒租约
$this->client->put($key, $value, ['lease' => $lease->ID]);
// 维持心跳
$this->heartbeat($lease->ID);
}
// 发现服务
public function discoverService($serviceName)
{
$services = [];
$key = "/services/{$serviceName}";
$result = $this->client->getPrefix($key);
foreach ($result as $kv) {
$data = json_decode($kv['value'], true);
$services[] = [
'address' => $data['address'],
'port' => $data['port']
];
}
return $services;
}
// 监听服务变化
public function watchService($serviceName, callable $callback)
{
$key = "/services/{$serviceName}";
$watch = $this->client->watch($key, null, true);
while ($event = $watch->next()) {
$callback($event);
}
}
private function heartbeat($leaseId)
{
// 使用异步任务定期续租
pcntl_async_signals(true);
pcntl_signal(SIGALRM, function() use ($leaseId) {
$this->client->keepAliveOnce($leaseId);
pcntl_alarm(5); // 每5秒续租
});
pcntl_alarm(5);
}
}
?>
使用 Consul PHP 客户端
环境准备:
composer require sensiolabs/consul-php-sdk
实现代码:
<?php
use SensioLabs\Consul\ServiceFactory;
use SensioLabs\Consul\Services\Agent;
use SensioLabs\Consul\Services\Health;
class ConsulServiceDiscovery
{
private $serviceFactory;
private $agentService;
private $healthService;
public function __construct($consulHost = '127.0.0.1:8500')
{
$this->serviceFactory = new ServiceFactory(['base_uri' => "http://{$consulHost}"]);
$this->agentService = $this->serviceFactory->get(Agent::class);
$this->healthService = $this->serviceFactory->get(Health::class);
}
// 注册服务
public function register($serviceName, $serviceId, $port, $address = null)
{
$service = [
'Name' => $serviceName,
'ID' => $serviceId,
'Port' => $port,
'Address' => $address ?: gethostbyname(gethostname()),
'Tags' => ['php', 'api'],
'Check' => [
'TCP' => "{$address}:{$port}",
'Interval' => '10s',
'Timeout' => '1s'
]
];
return $this->agentService->registerService($service);
}
// 发现健康服务
public function discoverHealthyServices($serviceName)
{
$services = [];
$response = $this->healthService->service($serviceName);
foreach ($response->json() as $service) {
if ($this->isHealthy($service)) {
$services[] = [
'id' => $service['Service']['ID'],
'address' => $service['Service']['Address'],
'port' => $service['Service']['Port']
];
}
}
return $services;
}
// 负载均衡(简单轮询)
public function getServiceRuntimeUrl($serviceName)
{
$services = $this->discoverHealthyServices($serviceName);
if (empty($services)) {
throw new \Exception("No healthy services found for: {$serviceName}");
}
$index = mt_rand(0, count($services) - 1); // 随机选择
$service = $services[$index];
return "http://{$service['address']}:{$service['port']}";
}
private function isHealthy($service)
{
if (empty($service['Checks'])) return true;
foreach ($service['Checks'] as $check) {
if ($check['Status'] === 'critical') {
return false;
}
}
return true;
}
public function deregister($serviceId)
{
return $this->agentService->deregisterService($serviceId);
}
}
使用示例:
<?php
$discovery = new ConsulServiceDiscovery();
// 注册服务
$serviceId = 'user-service-node1';
$discovery->register('user-service', $serviceId, 8080);
// 获取服务地址
$url = $discovery->getServiceRuntimeUrl('user-service');
echo "服务地址: " . $url . "\n";
// 调用服务
$response = file_get_contents($url . '/api/users');
?>
Redis 服务发现(轻量方案)
<?php
class RedisServiceDiscovery
{
private $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
// 注册服务(设置 TTL 实现自动过期)
public function register($serviceName, $instanceId, $data, $ttl = 10)
{
$key = "service:{$serviceName}:{$instanceId}";
$this->redis->setex($key, $ttl, json_encode($data));
// 维护服务列表
$this->redis->sadd("services:{$serviceName}", $instanceId);
}
// 心跳续期
public function heartbeat($serviceName, $instanceId, $ttl = 10)
{
$key = "service:{$serviceName}:{$instanceId}";
$this->redis->expire($key, $ttl);
}
// 获取服务实例
public function getServices($serviceName)
{
$services = [];
$instances = $this->redis->smembers("services:{$serviceName}");
foreach ($instances as $instanceId) {
$key = "service:{$serviceName}:{$instanceId}";
$data = $this->redis->get($key);
if ($data) {
$services[$instanceId] = json_decode($data, true);
} else {
// 清理过期实例
$this->redis->srem("services:{$serviceName}", $instanceId);
}
}
return $services;
}
// 负载均衡策略
public function getServiceEndpoint($serviceName, $strategy = 'random')
{
$services = $this->getServices($serviceName);
if (empty($services)) {
throw new \Exception("Service not available: {$serviceName}");
}
switch ($strategy) {
case 'random':
$instance = $services[array_rand($services)];
break;
case 'round-robin':
$this->redis->incr("service:rr:".$serviceName);
$index = $this->redis->get("service:rr:".$serviceName) % count($services);
$instance = array_values($services)[$index];
break;
default:
$instance = $services[array_rand($services)];
}
return "{$instance['host']}:{$instance['port']}";
}
}
?>
最佳实践建议
-
选择适合的方案:
- 小型项目:DNS 或 Redis
- 中型项目:Consul 或 Etcd
- 中大型微服务:Kubernetes 自带服务发现
-
服务健康检查:
- 实现心跳机制
- 设置合理的 TTL
-
负载均衡:
- 集成随机、轮询、最少连接数等策略
- 考虑故障转移
-
监听机制:
- 实现服务变更的实时监听
- 更新本地缓存
-
附加考虑:
- 缓存发现结果以减少请求
- 实现优雅停机和重连机制
- 添加监控和日志记录
根据具体场景选择合适的实现方案,高可用生产环境建议使用 Consul 或 Etcd,配合健康检查和负载均衡策略使用。