Design and Thinking Around PHP Architecture (Part 3)

The ultimate integrated plan for a PHP microservices architecture:

  graph TD
      A[客户端] --> B{Kong网关}
      B -->|路由| C[用户服务]
      B -->|路由| D[订单服务]
      B -->|路由| E[支付服务]

      C -->|注册| F[Consul集群]
      D -->|注册| F
      E -->|注册| F

      C -->|配置| G[Apollo]
      D -->|配置| G
      E -->|配置| G

      D -->|消息| H[RabbitMQ集群]
      H --> E
      H --> I[通知服务]

      F --> J[Prometheus]
      G --> J
      H --> J
      J --> K[Grafana]

      L[Jaeger] --> M[服务追踪]
  1. Fast infrastructure deployment
    Full-stack Docker Compose 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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    version: '3.8'

    services:
    # Registry
    consul-server:
    image: consul:1.15
    ports: ["8500:8500"]
    command: "agent -server -bootstrap-expect=1 -ui -client 0.0.0.0"

    # API gateway
    kong:
    image: kong:3.4
    depends_on: [consul-server]
    environment:
    KONG_DATABASE: "off"
    KONG_DECLARATIVE_CONFIG: "/etc/kong/kong.yml"
    KONG_PROXY_ACCESS_LOG: "/dev/stdout"
    KONG_ADMIN_ACCESS_LOG: "/dev/stdout"
    KONG_PROXY_ERROR_LOG: "/dev/stderr"
    ports: ["8000:8000", "8001:8001"]
    volumes:
    - ./kong:/etc/kong

    # Config center
    apollo:
    image: apolloconfig/apollo-portal:2.1.0
    ports: ["8070:8070"]
    environment:
    SPRING_DATASOURCE_URL: "jdbc:mysql://apollo-db:3306/ApolloPortalDB"

    # Message queue
    rabbitmq:
    image: rabbitmq:3.11-management
    ports: ["5672:5672", "15672:15672"]
    environment:
    RABBITMQ_DEFAULT_USER: "admin"
    RABBITMQ_DEFAULT_PASS: "secret"

    # Monitoring system
    prometheus:
    image: prom/prometheus:v2.47
    ports: ["9090:9090"]
    volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
  2. PHP implementation package for the core services

    Key composer.json dependencies

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "require": {
    "php": ">=8.2",
    "ext-swoole": "*",
    "ext-redis": "*",
    "ext-amqp": "*",

    "sensiolabs/consul-php-sdk": "^2.0",
    "guzzlehttp/guzzle": "^7.8",
    "php-amqplib/php-amqplib": "^3.2",
    "resilience-php/resilience-php": "^1.3",
    "apolloconfig/apollo-client": "^2.0"
    }
    }
  3. Complete service registration and discovery implementation

    Service registration (bootstrap.php)

    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
    use SensioLabs\Consul\ServiceFactory;

    $consul = new ServiceFactory(['base_uri' => 'http://consul-server:8500']);

    // Get this machine's IP automatically
    $ip = trim(shell_exec("hostname -i"));

    // Register the service
    $consul->get(AgentInterface::class)->registerService([
    'ID' => 'order-service-'.gethostname(),
    'Name' => 'order-service',
    'Address' => $ip,
    'Port' => 8000,
    'Check' => [
    'HTTP' => "http://{$ip}:8000/health",
    'Interval' => '5s',
    'Timeout' => '2s',
    'DeregisterCriticalServiceAfter' => '30s'
    ],
    'Tags' => ['v2', 'primary']
    ]);

    // Health check endpoint
    $app->get('/health', function() {
    check_database();
    check_redis();
    return json_response(['status' => 'UP']);
    });

    Service discovery and 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
    35
    36
    37
    class ServiceDiscovery {
    private static $cache = [];
    private static $ttl = 5; // cache for 5 seconds

    public static function getInstance(string $service): string {
    $now = time();

    // Return straight away while the cache is still valid
    if (isset(self::$cache[$service]) &&
    $now - self::$cache[$service]['timestamp'] < self::$ttl) {
    return self::selectInstance(self::$cache[$service]['instances']);
    }

    // Fetch fresh instances from Consul
    $instances = $consul->getCatalog()->service($service)->json();
    self::$cache[$service] = [
    'instances' => $instances,
    'timestamp' => $now
    ];

    return self::selectInstance($instances);
    }

    private static function selectInstance(array $instances): string {
    // Weighted random algorithm
    $total = array_sum(array_column($instances, 'Weight'));
    $rand = mt_rand(1, $total);
    $current = 0;

    foreach ($instances as $instance) {
    $current += $instance['Weight'];
    if ($rand <= $current) {
    return "http://{$instance['ServiceAddress']}:{$instance['ServicePort']}";
    }
    }
    }
    }
  4. Hooking up the unified config center

    Apollo config listening

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    $apollo = new \ApolloClient\Client([
    'config_server' => 'http://apollo:8070',
    'app_id' => 'order-service',
    'cluster' => 'default'
    ]);

    // Load the initial config
    $mysqlConfig = $apollo->get('mysql');
    DB::connect($mysqlConfig);

    // Dynamic listening
    $apollo->listen(['mysql', 'redis'], function($namespace, $config) {
    switch ($namespace) {
    case 'mysql':
    DB::reconnect($config);
    break;
    case 'redis':
    Redis::setConfig($config);
    break;
    }

    Logger::info("Config updated: $namespace");
    });
  5. The complete service communication solution

    Synchronous calls (HTTP)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class HttpServiceClient {
    use CircuitBreaker;

    public function call(string $service, string $endpoint, array $data) {
    return $this->protect(function() use ($service, $endpoint, $data) {
    $baseUrl = ServiceDiscovery::getInstance($service);
    $client = new GuzzleHttp\Client([
    'base_uri' => $baseUrl,
    'timeout' => 2.0
    ]);

    return $client->post($endpoint, [
    'json' => $data,
    'headers' => [
    'X-Trace-Id' => Trace::getId()
    ]
    ]);
    }, function() { // fallback handling
    return ['status' => 'degraded'];
    });
    }
    }

    Asynchronous messaging (RabbitMQ)

    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
    class EventPublisher {
    private $channel;

    public function __construct() {
    $conn = new AMQPStreamConnection('rabbitmq', 5672, 'admin', 'secret');
    $this->channel = $conn->channel();

    // Declare the dead-letter exchange
    $this->channel->exchange_declare('dlx', 'direct', false, true);
    $this->channel->queue_declare('dlq', false, true);
    $this->channel->queue_bind('dlq', 'dlx');
    }

    public function publish(string $event, array $data) {
    $this->channel->tx_select();

    try {
    $message = new AMQPMessage(json_encode($data), [
    'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
    'message_id' => Uuid::uuid4(),
    'timestamp' => time()
    ]);

    $this->channel->basic_publish($message, 'events', $event);
    DB::table('outbox')->insert(['message_id' => $message->get('message_id')]);
    $this->channel->tx_commit();
    } catch (Exception $e) {
    $this->channel->tx_rollback();
    throw $e;
    }
    }
    }
  6. Service degradation and circuit breaking strategy

    Multi-level degradation 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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    class DegradeManager {
    private static $levels = [
    'order-service' => [
    'full' => ['threshold' => 0.95, 'fallback' => 'cache'],
    'cache' => ['threshold' => 0.8, 'fallback' => 'readonly'],
    'readonly' => ['threshold' => 0.5, 'fallback' => 'static']
    ]
    ];

    public static function handle(string $service, callable $func) {
    $status = self::getServiceStatus($service);

    try {
    switch ($status) {
    case 'full':
    return $func();
    case 'cache':
    return Cache::remember("fallback:$service", 60, $func);
    case 'readonly':
    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    return $func();
    }
    throw new DegradeException('只读模式');
    case 'static':
    return ['status' => 'degraded'];
    }
    } catch (Exception $e) {
    self::recordFailure($service);
    return self::handle($service, $func); // degrade automatically
    }
    }

    private static function getServiceStatus(string $service): string {
    $failureRate = Prometheus::getFailureRate($service);

    foreach (self::$levels[$service] as $level => $config) {
    if ($failureRate <= $config['threshold']) {
    return $level;
    }
    }

    return 'static';
    }
    }
  7. Monitoring and alerting configuration

    Prometheus metrics collection

    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
    class Metrics {
    private static $counter;

    public static function init() {
    $registry = new CollectorRegistry(new InMemory());

    self::$counter = $registry->registerCounter(
    'php',
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
    );

    // Expose the metrics endpoint
    $app->get('/metrics', function() use ($registry) {
    $renderer = new RenderTextFormat();
    return $renderer->render($registry->getMetricFamilySamples());
    });
    }

    public static function countRequest($method, $path, $status) {
    self::$counter->inc([
    $method,
    preg_replace('/\d+/', '{id}', $path),
    $status
    ]);
    }
    }

    // Called from the middleware
    $app->addMiddleware(function($req, $handler) {
    $start = microtime(true);
    $response = $handler->handle($req);
    $duration = microtime(true) - $start;

    Metrics::countRequest(
    $req->getMethod(),
    $req->getUri()->getPath(),
    $response->getStatusCode()
    );

    return $response;
    });

  8. Production environment deployment recommendations

  1. Service registry:

    • Deploy a 3-node Consul cluster

    • Enable ACL and TLS encryption

    • Set up an automatic backup policy

  • API gateway:

    • Kong cluster + Nginx load balancing

    • Enable the JWT plugin and rate limiting

    • Configure WAF rules to block attacks

  • Config center:

    • Apollo multi-environment isolation (DEV/TEST/PROD)

    • Encrypted storage for sensitive configuration

    • Set up an approval flow for config changes

  • Message queue:

    • RabbitMQ mirrored queues

    • Set sensible TTL and dead-letter policies

    • Monitor queue backlog

  • Monitoring system:

    • Prometheus federated cluster

    • Unified Grafana dashboards

    • Alerts on key metrics (P99 latency > 500ms, error rate > 1%)

Example of a complete architecture call flow
  1. Client request → Kong gateway (auth + rate limiting)
  • The gateway queries Consul for an order-service instance

  • While the order service handles the request:

    • Read the current config from Apollo

    • Call the payment service over HTTP (with circuit breaking)

    • Publish a message to RabbitMQ

  • After the payment service consumes the message:

    • Update the database

    • Record Prometheus metrics

    • Push the result to the notification service

  • Report full-link tracing data to Jaeger