Skip to main content

Persistent Services

PHP-FPM vs Swoole

Đây là điểm khác biệt cốt lõi nhất giữa framework này và Laravel/Symfony truyền thống.

PHP-FPM (truyền thống)

Request 1 ──> PHP process khởi động ──> load classes ──> boot providers ──> xử lý ──> Process kết thúc
Request 2 ──> PHP process khởi động ──> load classes ──> boot providers ──> xử lý ──> Process kết thúc
Request 3 ──> PHP process khởi động ──> load classes ──> boot providers ──> xử lý ──> Process kết thúc

Mỗi request: autoload toàn bộ classes, khởi tạo lại mọi service, kết nối lại database...

Swoole (framework này)

Server khởi động ──> load classes ──> boot providers ──> kết nối DB, Redis...

┌──────────┼──────────┐
▼ ▼ ▼
Request 1 Request 2 Request 3 ← Dùng chung mọi thứ đã boot
│ │ │
Cleanup Cleanup Cleanup ← Chỉ dọn dẹp dữ liệu per-request

Kết quả: server boot một lần, phục vụ hàng trăm nghìn request mà không cần khởi tạo lại.

Hai loại dữ liệu

Framework phân chia rõ ràng dữ liệu thành hai loại:

1. Singleton - Tồn tại suốt vòng đời server

Được tạo khi boot và giữ trong memory mãi mãi. Tất cả requests dùng chung instance này.

Server memory:
┌─────────────────────────────────────────────┐
│ Singletons (tồn tại mãi) │
│ ├── App container │
│ ├── Config (đã load từ files) │
│ ├── Route definitions │
│ ├── DB connection pool │
│ ├── Redis connection pool │
│ ├── Logger │
│ └── ... (33 service providers) │
└─────────────────────────────────────────────┘

2. Scoped - Tạo mới cho mỗi request, xóa sau khi xong

Request A coroutine:          Request B coroutine:
┌──────────────────┐ ┌──────────────────┐
│ Request object │ │ Request object │
│ Route dispatcher │ │ Route dispatcher │
│ Context data │ │ Context data │
│ Auth user │ │ Auth user │
└──────────────────┘ └──────────────────┘
Xóa sau request A Xóa sau request B

Xem trong code

Đăng ký singleton

Trong Service Provider, dùng singleton():

class LoggerServiceProvider extends ServiceProvider
{
public function register(): void
{
// Chỉ tạo 1 lần, giữ mãi
$this->app->singleton('logger', function () {
return new Logger(config('logging'));
});
}
}

Đăng ký scoped (per-request)

Dùng scoped() - mỗi request tạo mới, tự động xóa khi request kết thúc:

class RoutingServiceProvider extends ServiceProvider
{
public function register(): void
{
// Tạo mới cho mỗi request
$this->app->scoped('request', function () {
return new Request();
});

$this->app->scoped('route.dispatcher', function ($app) {
return new RouteDispatcher($app);
});
}
}

Cleanup sau request

// core/Http/Server.php
public function terminateRequest(): void
{
// Xóa tất cả scoped instances
$this->app->forgetScopedInstances();

// Xóa context data của coroutine này
Context::delete();
}

Ứng dụng thực tế: Connection Pooling

Vì server không restart, framework có thể duy trì pool các connection sẵn sàng:

Boot lần đầu:
DB Pool: [conn1] [conn2] [conn3] [conn4] [conn5] ← tạo sẵn

Request 1 arrives: DB Pool: [ ] [conn2] [conn3] [conn4] [conn5]
↑ conn1 đang dùng

Request 2 arrives: DB Pool: [ ] [ ] [conn3] [conn4] [conn5]
↑ conn2 đang dùng

Request 1 ends: DB Pool: [conn1] [ ] [conn3] [conn4] [conn5]
↑ trả lại pool

So với PHP-FPM: mỗi request phải tạo kết nối mới (TCP handshake, auth...) → chậm hơn rất nhiều.

Những gì bạn cần lưu ý

✅ An toàn: Readonly sau boot

Services chỉ đọc config/routes/định nghĩa → không thay đổi giữa các request:

// ✅ Config được load một lần, chỉ đọc
$timeout = config('database.timeout'); // nhanh, không I/O

// ✅ Routes được đăng ký một lần

❌ Nguy hiểm: State bị rò rỉ giữa các request

class OrderService
{
// ❌ BUG: $currentUser là singleton, bị ghi đè bởi request khác!
private static ?User $currentUser = null;

public static function setUser(User $user): void
{
self::$currentUser = $user; // request A ghi user A vào đây
// request B cũng ghi vào đây → ghi đè!
}
}

Giải pháp: dùng Context để lưu dữ liệu per-request:

class OrderService
{
// ✅ Mỗi coroutine (request) có context riêng
public static function setUser(User $user): void
{
Context::set('current_user', $user);
}

public static function getUser(): ?User
{
return Context::get('current_user');
}
}

❌ Nguy hiểm: Không đóng resource trong coroutine

// ❌ File handle không được đóng → memory leak dần dần
Coroutine::create(function () {
$file = fopen('/tmp/data.txt', 'r');
$data = fread($file, 1024);
// Quên fclose($file) → rò rỉ file descriptor
});

// ✅ Dùng defer để đảm bảo cleanup
Coroutine::create(function () {
$file = fopen('/tmp/data.txt', 'r');
Coroutine::defer(fn() => fclose($file)); // luôn được gọi

$data = fread($file, 1024);
});

Khởi tạo service với trạng thái per-request

Đôi khi service cần biết thông tin từ request hiện tại (ví dụ: ngôn ngữ của user). Dùng scoped binding:

class TranslationServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->scoped('translator', function ($app) {
$request = $app->get('request');
$locale = $request->header('Accept-Language', 'vi');

return new Translator($locale);
// Tạo mới mỗi request với đúng locale của request đó
});
}
}

Reload config/routes mà không restart server

Vì services tồn tại mãi, cần có cơ chế reload khi deploy:

# Reload worker processes (không interrupt request đang xử lý)
php vietiso server:reload

# Hoặc send signal
kill -USR1 $(cat storage/server.pid)

Swoole sẽ graceful reload: tạo worker mới → chuyển traffic → đóng worker cũ sau khi request hiện tại xong.

Tổng quan so sánh

Đặc điểmPHP-FPMFramework này (Swoole)
BootMỗi requestMột lần khi khởi động
MemoryGiải phóng sau mỗi requestGiữ singletons mãi
ConnectionTạo mới mỗi requestPool, tái sử dụng
Biến staticReset sau mỗi requestTồn tại giữa các requests
Hiệu năngChậm hơn (overhead boot)Nhanh hơn đáng kể
Rủi roÍt bug statePhải cẩn thận với shared state
Quy tắc vàng
  • Config, routes, service definitions → Singleton (tạo 1 lần)
  • Dữ liệu của từng request (user hiện tại, request object, session) → Context hoặc scoped
  • Biến static chứa dữ liệu per-request → ❌ Không bao giờ làm vậy