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ểm | PHP-FPM | Framework này (Swoole) |
|---|---|---|
| Boot | Mỗi request | Một lần khi khởi động |
| Memory | Giải phóng sau mỗi request | Giữ singletons mãi |
| Connection | Tạo mới mỗi request | Pool, tái sử dụng |
| Biến static | Reset sau mỗi request | Tồn tại giữa các requests |
| Hiệu năng | Chậm hơn (overhead boot) | Nhanh hơn đáng kể |
| Rủi ro | Ít bug state | Phải cẩn thận với shared state |
- 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) →
Contexthoặcscoped - Biến
staticchứa dữ liệu per-request → ❌ Không bao giờ làm vậy