PHP Architecture Design and Thinking (Part 2)
Building on the earlier architecture design, here’s a more complete, ready-to-ship plan:
1. Enhanced service registry implementation (Consul + PHP)
Health checks and automatic deregistration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // Add a health check endpoint when registering the service $consul->get(AgentInterface::class)->registerService([ 'ID' => 'payment-service-'.uniqid(), 'Name' => 'payment-service', 'Tags' => ['primary', 'v2'], 'Address' => get_current_ip(), 'Port' => 8000, 'Check' => [ 'HTTP' => 'http://'.get_current_ip().':8000/health', 'Interval' => '5s', 'Timeout' => '2s', 'DeregisterCriticalServiceAfter' => '30s' ] ]); // The PHP health check endpoint $app->get('/health', function() { return json_encode([ 'status' => 'UP', 'db_connected' => DB::connection()->getPdo() ? true : false, 'redis_connected' => Redis::ping() ? true : false, 'load' => sys_getloadavg()[0] ]); });
Service discovery load balancing
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 class ServiceLocator { private static $services = []; public static function getInstance($serviceName) { if (!isset(self::$services[$serviceName]) || count(self::$services[$serviceName]) == 0) { $instances = $consul->getCatalog()->service($serviceName)->json(); self::$services[$serviceName] = array_map( fn($s) => new Instance($s['ServiceAddress'], $s['ServicePort']), $instances ); } // Pick an instance using a weighted random algorithm $totalWeight = array_sum(array_map(fn($i) => $i->weight, self::$services[$serviceName])); $rand = mt_rand(1, $totalWeight); foreach (self::$services[$serviceName] as $instance) { $rand -= $instance->weight; if ($rand <= 0) { return $instance; } } } // Refresh the service list on a timer public static function refresh() { swoole_timer_tick(30000, function() { foreach (array_keys(self::$services) as $name) { $instances = $consul->getCatalog()->service($name)->json(); self::$services[$name] = $instances; } }); } }
2. Deep API gateway integration (Kong + OpenResty)
Dynamic route configuration
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 #lua -- kong/plugins/dynamic-router/handler.lua local DynamicRouterHandler = { PRIORITY = 1000, VERSION = "1.0" } function DynamicRouterHandler:access(conf) -- Fetch the latest service instances from Consul local res, err = kong.request.make({ method = "GET", url = "http://consul:8500/v1/catalog/service/" .. kong.request.get_header("X-Service-Name") }) if not res then kong.log.err("Consul request failed: ", err) return kong.response.exit(503) end local instances = cjson.decode(res.body) if #instances == 0 then return kong.response.exit(404, { message = "Service unavailable" }) end -- Update the upstream target local ok, err = kong.admin_api.post("/upstreams/service-"..conf.service_name.."/targets", { target = instances[1].ServiceAddress..":"..instances[1].ServicePort, weight = 100 }) end return DynamicRouterHandler
PHP plugin development example (JWT validation)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // kong/plugins/jwt-validator/validate.php <?php $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (!preg_match('/Bearer\s+(.*)$/i', $token, $matches)) { header('HTTP/1.1 401 Unauthorized'); exit; } $jwt = $matches[1]; $userInfo = AuthService::validateJWT($jwt); if (!$userInfo) { header('HTTP/1.1 403 Forbidden'); exit; } // Pass the user info on to the upstream service header('X-User-Id: ' . $userInfo['id']); header('X-User-Roles: ' . implode(',', $userInfo['roles']));
3. Complete config center solution (Apollo + PHP)
Config watching via long polling
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 class ApolloWatcher { private $client; private $callbacks = []; public function __construct() { $this->client = new \ApolloClient\Client([...]); $this->startWatch(); } public function watch($namespace, callable $callback) { $this->callbacks[$namespace][] = $callback; } private function startWatch() { swoole_timer_tick(1000, function() { foreach ($this->callbacks as $namespace => $cbs) { $notifications = $this->client->getNotifications([$namespace]); if ($notifications[$namespace] > $this->versions[$namespace] ?? 0) { $config = $this->client->getConfig($namespace); foreach ($cbs as $cb) { $cb($config); } $this->versions[$namespace] = $notifications[$namespace]; } } }); } } // Usage example $watcher = new ApolloWatcher(); $watcher->watch('application', function($config) { Redis::set('app_config', json_encode($config)); });
Encrypted config storage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class SecureConfig { private $key; public function __construct() { $this->key = file_get_contents('/etc/apollo/key.pem'); } public function get($key) { $encrypted = Apollo::get($key); return openssl_decrypt( base64_decode($encrypted), 'aes-256-cbc', $this->key, 0, substr($this->key, 0, 16) ); } }
4. Reliable messaging for the service bus (RabbitMQ)
Message transactions and acknowledgements
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 // Producer side $channel->tx_select(); try { $msg = new AMQPMessage($data, [ 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, 'message_id' => uniqid() ]); $channel->basic_publish($msg, 'orders'); DB::table('outbox')->insert(['message_id' => $msg->get('message_id')]); $channel->tx_commit(); } catch (Exception $e) { $channel->tx_rollback(); Metrics::counter('publish_failed')->inc(); } // Consumer side $channel->basic_consume('orders', '', false, false, false, false, function($msg) { try { processOrder($msg->body); $msg->ack(); // Idempotent handling if (!DB::table('processed_messages')->where('msg_id', $msg->get('message_id'))->exists()) { DB::table('processed_messages')->insert([ 'msg_id' => $msg->get('message_id'), 'processed_at' => now() ]); } } catch (Exception $e) { $msg->nack(true); // requeue } } );
Dead letter queue configuration
1 2 3 4 5 6 7 8 9 10 11 // Declare the dead letter exchange $channel->exchange_declare('dlx', 'direct', false, true, false); $channel->queue_declare('dlq', false, true, false, false); $channel->queue_bind('dlq', 'dlx', 'dead'); // Main queue configuration $channel->queue_declare('orders', false, true, false, false, false, [ 'x-dead-letter-exchange' => 'dlx', 'x-dead-letter-routing-key' => 'dead', 'x-message-ttl' => 600000 // lands in the DLQ after 10 minutes ]);
5. Complete service degradation strategy
Multi-level degradation plan
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 class OrderService { const DEGRADE_LEVELS = [ 'full' => 0, // normal service 'readonly' => 1, // read-only mode 'cache' => 2, // return cached data 'static' => 3 // return a static page ]; public function getOrder($id) { $level = CircuitBreaker::getStatus('order-service'); switch ($level) { case self::DEGRADE_LEVELS['full']: return DB::table('orders')->find($id); case self::DEGRADE_LEVELS['readonly']: if (request()->isMethod('GET')) { return DB::table('orders')->find($id); } throw new DegradeException('只读模式'); case self::DEGRADE_LEVELS['cache']: return Cache::remember("order:$id", 60, function() use ($id) { return DB::table('orders')->find($id) ?? []; }); case self::DEGRADE_LEVELS['static']: return [ 'id' => $id, 'status' => 'degraded', 'message' => '系统繁忙,请稍后查看订单状态' ]; } } }
Automatic degradation rule configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # degradation-rules.yaml services: order-service: failure_threshold: 50% # failure rate threshold slow_call_threshold: 2000ms # slow call threshold minimum_calls: 20 # minimum number of calls sliding_window: 60s # statistics window wait_duration: 300s # how long to wait in the half-open state fallbacks: - condition: "error instanceof TimeoutException" action: "return cachedData()" - condition: "error instanceof DBException" action: "switchToReadonlyMode()"
6. Deployment architecture optimization
Kubernetes deployment template
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 # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 3 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: spec: containers: - name: php image: my-registry/order-service:v1.2 ports: - containerPort: 8000 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8000 initialDelaySeconds: 5 periodSeconds: 5 envFrom: - configMapRef: name: apollo-config --- # service.yaml apiVersion: v1 kind: Service metadata: name: order-service annotations: consul.hashicorp.com/service-name: order-service spec: ports: - port: 8000 targetPort: 8000 selector: app: order-service
Monitoring and alerting configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # prometheus-rules.yml groups: - name: php-services rules: - alert: HighErrorRate expr: sum(rate(http_requests_total{status=~"5.."}[1m])) by (service) / sum(rate(http_requests_total[1m])) by (service) > 0.05 for: 5m labels: severity: critical annotations: summary: "High error rate on {{ $labels.service }}" description: "Error rate is {{ $value }}" - alert: ServiceDown expr: up{job="php-services"} == 0 for: 1m labels: severity: critical annotations: summary: "{{ $labels.instance }} is down"
Development environment toolchain
Local debugging setup:
1 2 3 4 5 6 7 8 9 # Start the dependent services with docker-compose docker-compose -f dev-env.yml up consul rabbitmq apollo # Remote debugging with Xdebug PHP_IDE_CONFIG="serverName=my-service" php -dxdebug.mode=debug \ -dxdebug.client_host=host.docker.internal \ -dxdebug.client_port=9003 \ -dxdebug.start_with_request=yes \ artisan serve
API documentation generation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * @OA\Post( * path="/orders", * tags={"Orders"}, * @OA\RequestBody( * @OA\JsonContent(ref="#/components/schemas/OrderRequest") * ), * @OA\Response( * response=201, * description="Order created", * @OA\JsonContent(ref="#/components/schemas/Order") * ), * @OA\Response( * response=503, * description="Service degraded", * @OA\JsonContent(ref="#/components/schemas/Error") * ) * ) */ public function createOrder(Request $request) { // ... }
Performance testing tools:
1 2 3 4 5 6 # Load testing with wrk wrk -t4 -c100 -d60s --latency http://localhost:8000/api/orders # Generate a flame graph php -dxdebug.profiler_enable=1 -dxdebug.profiler_output_dir=/tmp \ -dxdebug.profiler_output_name=cachegrind.out.%p artisan serve
Summary of key optimization points
Service discovery: a Consul cluster + health checks + client-side caching for service state updates within seconds
Reliable messaging: transactional messages + dead letter queues + idempotent handling to make sure messages aren’t lost
Config management: Apollo long polling + encrypted storage + version rollback for a secure config center
Degradation strategy: multi-level degradation (full functionality → read-only → cache → static) to protect the critical path
Observability: Prometheus metrics + Jaeger tracing + ELK logs as a three-part monitoring setup