微服务治理服务网络健康检查配置ThinkPHP8与Consul API的完美融合:轻松实现服务发现与配置
28.7的博客PHP与Consul API的完美融合
优化后的文档在结构逻辑、代码规范性和可读性上进行了提升,按“概述→准备工作→核心实现→API参考”的流程重组,同时修复代码语法问题、统一格式规范。
1. 概述
在分布式系统中,服务发现与配置管理是保障系统稳定性的核心环节。Consul 作为功能全面的中间件,可提供服务注册/发现、健康检查、KV配置存储等能力;PHP 则是 Web 开发领域的主流语言,广泛应用于业务系统开发。
本文聚焦 PHP 与 Consul API 的实际融合方案,通过依赖引入、客户端配置、核心功能编码,完整实现服务查询、健康检查等关键场景,为分布式 PHP 系统提供基础支撑。
2. 准备工作
2.1 安装 Consul PHP 依赖
使用 Composer 引入成熟的 Consul API 客户端库,避免重复开发 HTTP 请求逻辑。
1
| composer require dcarbone/php-consul-api
|
2.2 配置 Consul 客户端
统一管理 Consul 连接参数,避免硬编码,支持后续环境切换(如开发/生产环境)。
1 2 3 4 5 6 7 8 9 10
| <?php
return [ 'base_uri' => "http://127.0.0.1:8500", 'token' => "07c5745c-d0d1-3f02-d441-8252661ce925", 'get_single_api' => "/v1/health/service/", 'get_all_api' => "/v1/catalog/services", 'get_single_old_api'=> "/v1/catalog/service/", 'get_all_health_api'=> "/v1/agent/checks" ];
|
2.3 配置日志通道
为 Consul 相关操作单独配置日志,便于问题排查(以 ThinkPHP 日志配置为例)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| return [ 'channels' => [ 'consul' => [ 'type' => 'File', 'path' => runtime_path() . 'log/consul/', 'single' => false, 'apart_level' => ['error'], 'max_files' => 30, 'level' => ['info', 'error', 'debug'], 'json' => false, 'realtime_write' => true, 'format' => '[%s][%s] %s' ], ] ];
|
3. 核心功能实现
3.1 查询指定服务健康状态
API 端点:/v1/health/service/<service-name>
功能:根据服务名查询实例,支持过滤仅健康实例,返回服务ID、地址、健康检查结果等关键信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| <?php
namespace app\service;
use think\Config; use think\Log;
class ConsulService { private $BaseURI;
public function __construct() { $this->BaseURI = Config::get('consul.base_uri'); }
public function queryServices(string $serviceName, bool $onlyHealthy = true, bool $sslpem = false) { $apiPath = Config::get('consul.get_single_api'); $token = Config::get('consul.token', ''); $passingParam = $onlyHealthy ? 'true' : 'false'; $requestUrl = "{$this->BaseURI}{$apiPath}{$serviceName}?passing={$passingParam}"; if (!empty($token)) { $requestUrl .= "&token={$token}"; }
Log::channel("consul")->info("Consul 服务健康查询请求URL: {$requestUrl}");
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $requestUrl, CURLOPT_RETURNTRANSFER => true, // 结果返回为字符串 CURLOPT_TIMEOUT => 5, // 超时时间5秒(避免阻塞业务) CURLOPT_SSL_VERIFYPEER => $sslpem, // 是否验证SSL证书 CURLOPT_SSL_VERIFYHOST => $sslpem ? 2 : false, // SSL主机验证(2=严格验证) CURLOPT_HTTPHEADER => ['Accept-Charset: utf-8'] // 指定UTF-8编码 ]);
$response = curl_exec($ch); $curlError = curl_errno($ch) ? curl_error($ch) : ''; curl_close($ch);
if ($curlError) { $errMsg = "Consul 服务查询CURL失败: {$curlError}"; Log::channel("consul")->error($errMsg); return ['code' => -1, 'msg' => $errMsg, 'data' => []]; }
$serviceList = json_decode($response, true); $jsonError = json_last_error(); if ($jsonError !== JSON_ERROR_NONE) { $errMsg = $this->getJsonErrorMsg($jsonError); Log::channel("consul")->error("Consul 响应JSON解析失败: {$errMsg} | 原始响应: {$response}"); return ['code' => -2, 'msg' => $errMsg, 'raw_response' => $response, 'data' => []]; }
$formattedData = []; foreach ($serviceList as $item) { $service = $item['Service'] ?? []; $healthCheck = $item['Checks'][1] ?? []; $formattedData[] = [ 'service_id' => $service['ID'] ?? '', 'service_name' => $service['Service'] ?? '', 'tags' => $service['Tags'] ?? [], 'address' => $service['Address'] ?? '', 'port' => $service['Port'] ?? 0, 'health_output' => isset($healthCheck['Output']) ? substr($healthCheck['Output'], 0, 50) : '' ]; }
return [ 'code' => 0, 'msg' => '服务健康查询成功', 'count' => count($formattedData), 'data' => $formattedData ]; }
private function getJsonErrorMsg(int $errorCode): string { $errorMap = [ JSON_ERROR_DEPTH => 'JSON深度超过限制', JSON_ERROR_SYNTAX => 'JSON语法错误(如括号不闭合、多余逗号)', JSON_ERROR_UTF8 => 'UTF-8编码错误(如乱码)', JSON_ERROR_CTRL_CHAR => '非法控制字符', JSON_ERROR_STATE_MISMATCH => 'JSON状态不匹配', JSON_ERROR_RECURSION => 'JSON包含递归引用', JSON_ERROR_INF_OR_NAN => 'JSON包含INF/NAN值', JSON_ERROR_UNSUPPORTED_TYPE => '包含不支持的数据类型(如资源)' ]; return $errorMap[$errorCode] ?? "未知JSON错误(错误码: {$errorCode})"; } }
|

3.2 查询所有服务列表
API 端点:/v1/catalog/services
功能:获取Consul集群中所有已注册的服务名及标签,用于快速遍历服务全集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?php
public function getAllServiceName(bool $sslpem = false) { $apiPath = Config::get('consul.get_all_api'); $requestUrl = "{$this->BaseURI}{$apiPath}"; $token = Config::get('consul.token', '');
Log::channel("consul")->info("Consul 所有服务列表请求URL: {$requestUrl}");
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $requestUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => $sslpem, CURLOPT_SSL_VERIFYHOST => $sslpem ? 2 : 0, CURLOPT_HTTPHEADER => [ 'Accept-Charset: utf-8', 'X-Consul-Token: ' . $token // ACL Token通过Header传递(更规范) ] ]);
$response = curl_exec($ch); $curlError = curl_errno($ch) ? curl_error($ch) : ''; $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $requestTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME); curl_close($ch);
Log::channel("consul")->info("Consul 所有服务列表请求结果", [ 'url' => $requestUrl, 'http_code' => $httpCode, 'curl_error' => $curlError, 'request_time_ms' => round($requestTime * 1000, 2), // 转毫秒并保留2位小数 'success' => $httpCode == 200 && empty($curlError) ]);
if ($curlError || $httpCode != 200) { $errMsg = $curlError ?: "API返回非200状态码({$httpCode})"; return ['code' => -1, 'msg' => $errMsg, 'data' => []]; }
$serviceList = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { $errMsg = "JSON解析失败: " . json_last_error_msg(); Log::channel("consul")->error($errMsg . " | 原始响应: {$response}"); return ['code' => -2, 'msg' => $errMsg, 'data' => []]; }
return [ 'code' => 0, 'msg' => '所有服务列表查询成功', 'data' => [ 'count' => count($serviceList), 'services' => $serviceList ] ]; }
|

3.3 查询服务基础注册信息
API 端点:/v1/catalog/service/<service-name>
功能:获取服务的基础注册信息(如节点关联、服务地址等),不含健康检查结果,适用于轻量查询场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| <?php
public function queryServiceBasicInfo(string $serviceName, bool $sslpem = false) { $apiPath = Config::get('consul.get_single_old_api'); $requestUrl = "{$this->BaseURI}{$apiPath}{$serviceName}"; $token = Config::get('consul.token', '');
Log::channel("consul")->info("Consul 服务基础信息请求URL: {$requestUrl}");
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $requestUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => $sslpem, CURLOPT_SSL_VERIFYHOST => $sslpem ? 2 : 0, CURLOPT_HTTPHEADER => [ 'Accept-Charset: utf-8', 'X-Consul-Token: ' . $token ] ]);
$response = curl_exec($ch); $curlError = curl_errno($ch) ? curl_error($ch) : ''; $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $requestTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME); curl_close($ch);
Log::channel("consul")->info("Consul 服务基础信息请求结果", [ 'url' => $requestUrl, 'http_code' => $httpCode, 'curl_error' => $curlError, 'request_time_ms' => round($requestTime * 1000, 2), 'success' => $httpCode == 200 && empty($curlError) ]);
if ($curlError || $httpCode != 200) { $errMsg = $curlError ?: "API返回非200状态码({$httpCode})"; return ['code' => -1, 'msg' => $errMsg, 'data' => []]; }
$serviceInfo = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { $errMsg = "JSON解析失败: " . json_last_error_msg(); Log::channel("consul")->error($errMsg . " | 原始响应: {$response}"); return ['code' => -2, 'msg' => $errMsg, 'data' => []]; }
$formattedInfo = []; foreach ($serviceInfo as $item) { $formattedInfo[] = [ 'service_id' => $item['ServiceID'] ?? '', 'service_name' => $item['ServiceName'] ?? '', 'node_name' => $item['Node'] ?? '', 'address' => $item['Address'] ?? '', 'port' => $item['ServicePort'] ?? 0, 'tags' => $item['ServiceTags'] ?? [], 'datacenter' => $item['Datacenter'] ?? '' ]; }
return [ 'code' => 0, 'msg' => '服务基础信息查询成功', 'data' => [ 'count' => count($formattedInfo), 'service_info' => $formattedInfo ] ]; }
|

3.4 查询所有服务健康检查状态
API 端点:/v1/agent/checks
功能:获取当前Consul Agent上所有服务的健康检查结果(含节点级检查),适用于全局健康监控场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?php
public function getAllServiceHealth(bool $sslpem = false) { $apiPath = Config::get('consul.get_all_health_api'); $requestUrl = "{$this->BaseURI}{$apiPath}"; $token = Config::get('consul.token', '');
Log::channel("consul")->info("Consul 所有服务健康检查请求URL: {$requestUrl}");
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $requestUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => $sslpem, CURLOPT_SSL_VERIFYHOST => $sslpem ? 2 : 0, CURLOPT_HTTPHEADER => [ 'Accept-Charset: utf-8', 'X-Consul-Token: ' . $token ] ]);
$response = curl_exec($ch); $curlError = curl_errno($ch) ? curl_error($ch) : ''; $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $requestTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME); curl_close($ch);
Log::channel("consul")->info("Consul 所有服务健康检查请求结果", [ 'url' => $requestUrl, 'http_code' => $httpCode, 'curl_error' => $curlError, 'request_time_ms' => round($requestTime * 1000, 2), 'success' => $httpCode == 200 && empty($curlError) ]);
if ($curlError || $httpCode != 200) { $errMsg = $curlError ?: "API返回非200状态码({$httpCode})"; return ['code' => -1, 'msg' => $errMsg, 'data' => []]; }
$healthList = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { $errMsg = "JSON解析失败: " . json_last_error_msg(); Log::channel("consul")->error($errMsg . " | 原始响应: {$response}"); return ['code' => -2, 'msg' => $errMsg, 'data' => []]; }
return [ 'code' => 0, 'msg' => '所有服务健康检查查询成功', 'data' => [ 'count' => count($healthList), 'health_checks' => $healthList ] ]; }
|

4. Consul API 核心端点列表
整理高频使用的 Consul API 端点,按功能分类,便于开发时快速查阅。
| 功能分类 |
请求方法 |
API 路径 |
描述 |
关键参数/示例 |
| 服务注册与发现 |
PUT |
/v1/agent/service/register |
注册服务(可关联健康检查) |
请求体:{"ID":"user-svc-01","Name":"user-service","Port":8080,"Check":{"HTTP":"http://xxx/health","Interval":"10s"}} |
|
GET |
/v1/catalog/services |
获取集群所有服务(服务名+标签) |
响应:{"user-service":["api"],"order-service":["rpc"]} |
|
GET |
/v1/catalog/service/<service-name> |
获取指定服务基础信息(不含健康状态) |
路径示例:/v1/catalog/service/user-service |
|
PUT |
/v1/agent/service/deregister/<service-id> |
注销服务(需服务ID) |
路径示例:/v1/agent/service/deregister/user-svc-01 |
| 健康检查 |
PUT |
/v1/agent/check/register |
注册独立健康检查(节点/服务级) |
请求体:`{“ID”:”disk-check”,”Name”:”Disk Usage”,”Script”:”df -h |
|
GET |
/v1/agent/checks |
获取当前Agent所有健康检查 |
响应含检查ID、状态(passing/warning/critical)、关联服务 |
|
GET |
/v1/health/service/<service-name>?passing=true |
获取指定服务的健康实例 |
路径示例:/v1/health/service/user-service?passing=true |
| KV 配置存储 |
PUT |
/v1/kv/<key> |
写入/更新KV键值对(值通过请求体传递) |
路径:/v1/kv/config/db/url,请求体:jdbc:mysql://10.0.0.10:3306/user_db |
|
GET |
/v1/kv/<key>?raw |
读取KV键原始值(不加?raw返回Base64编码) |
路径示例:/v1/kv/config/db/url?raw |
|
GET |
/v1/kv/<parent-key>?recurse |
递归读取父键下所有子键 |
路径示例:/v1/kv/config/?recurse(读取所有配置) |
| 集群与权限 |
GET |
/v1/catalog/nodes |
获取集群所有节点信息 |
响应含节点ID、IP、数据中心、标签 |
|
PUT |
/v1/acl/create |
创建ACL Token(需启用ACL) |
请求体:{"Name":"svc-token","Policy":"service:write,kv:read"} |