PHP 怎么实现服务发现

wen PHP项目 2

本文目录导读:

PHP 怎么实现服务发现

  1. 基于 DNS 的服务发现(推荐)
  2. 使用 Etcd + Watch 机制
  3. 使用 Consul PHP 客户端
  4. Redis 服务发现(轻量方案)
  5. 最佳实践建议

在 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']}";
    }
}
?>

最佳实践建议

  1. 选择适合的方案

    • 小型项目:DNS 或 Redis
    • 中型项目:Consul 或 Etcd
    • 中大型微服务:Kubernetes 自带服务发现
  2. 服务健康检查

    • 实现心跳机制
    • 设置合理的 TTL
  3. 负载均衡

    • 集成随机、轮询、最少连接数等策略
    • 考虑故障转移
  4. 监听机制

    • 实现服务变更的实时监听
    • 更新本地缓存
  5. 附加考虑

    • 缓存发现结果以减少请求
    • 实现优雅停机和重连机制
    • 添加监控和日志记录

根据具体场景选择合适的实现方案,高可用生产环境建议使用 Consul 或 Etcd,配合健康检查和负载均衡策略使用。

抱歉,评论功能暂时关闭!