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)

  1. 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]
]);
});
  1. 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)

  1. 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
  1. 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)

  1. 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));
});
  1. 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)

  1. 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
}
}
);
  1. 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

  1. 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' => '系统繁忙,请稍后查看订单状态'
    ];
    }
    }
    }
  2. 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

  1. 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
  2. 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"
  1. Development environment toolchain

  2. 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
  1. 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) {
// ...
}
  1. 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

  1. 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