Laravel nâng cao

Laravel Octane Production: Những bài học từ 6 tháng vận hành

Kinh nghiệm thực tế deploy và maintain Laravel Octane trong production: memory leaks, monitoring, scaling và những cạm bẫy cần tránh.

newspaper

Phạm Hoàng Long

5 tháng 1, 2026 schedule 6 phút đọc
Laravel Octane Production: Những bài học từ 6 tháng vận hành
Featured Image

6 tháng trước, tôi migrate API của công ty từ PHP-FPM sang Laravel Octane.

Performance tăng 4 lần. Response time giảm từ 450ms xuống 120ms. Mọi thứ hoàn hảo… cho đến tuần thứ 2.

Lúc 3 giờ sáng, tôi nhận được alert: “API down. Memory usage 100%”.

Đó là bài học đầu tiên về Octane production. Đây là những gì tôi học được sau 6 tháng vận hành.

Lesson 1: Memory Leak là kẻ thù số 1

Vấn đề

Sau 2-3 ngày chạy, memory usage của workers tăng từ 50MB lên 800MB. Server crash.

Root Cause

Bug 1: Static cache trong Service

class ProductService
{
    private static $categoryCache = [];
    
    public function getProductsByCategory($categoryId)
    {
        if (!isset(self::$categoryCache[$categoryId])) {
            self::$categoryCache[$categoryId] = Product::where('category_id', $categoryId)->get();
        }
        return self::$categoryCache[$categoryId];
    }
}

Vấn đề: Cache không bao giờ clear. Sau 10,000 requests, cache chứa hàng ngàn categories.

Fix:

class ProductService
{
    public function getProductsByCategory($categoryId)
    {
        // Dùng Laravel Cache với TTL
        return Cache::remember("products.category.{$categoryId}", 3600, function () use ($categoryId) {
            return Product::where('category_id', $categoryId)->get();
        });
    }
}

Bug 2: Event Listeners tích lũy

// AppServiceProvider
public function boot()
{
    Event::listen('order.created', function ($order) {
        // Process order
    });
}

Vấn đề: Mỗi lần worker restart (nhưng không exit process), listener được register lại. Sau 100 lần, có 100 listeners cho cùng event.

Fix:

public function boot()
{
    // Chỉ register một lần
    if (!app()->bound('order.listener.registered')) {
        Event::listen('order.created', OrderCreatedListener::class);
        app()->instance('order.listener.registered', true);
    }
}

Hoặc dùng Event Listener classes thay vì closures.

Solution: Auto-restart workers

// config/octane.php
return [
    'swoole' => [
        'options' => [
            'max_request' => 5000, // Restart sau 5000 requests
            'max_conn' => 1000,
        ],
    ],
];

Trade-off: Performance giảm nhẹ (restart overhead), nhưng stability tăng đáng kể.

Lesson 2: Monitoring là bắt buộc

Setup monitoring cho Octane

1. Memory monitoring

// app/Console/Commands/MonitorOctane.php
class MonitorOctane extends Command
{
    public function handle()
    {
        $workers = $this->getWorkerStats();
        
        foreach ($workers as $worker) {
            if ($worker['memory_mb'] > 500) {
                // Alert: Worker memory too high
                Log::critical("Octane worker {$worker['pid']} using {$worker['memory_mb']}MB");
                
                // Auto-restart worker
                $this->restartWorker($worker['pid']);
            }
        }
    }
    
    private function getWorkerStats()
    {
        $output = shell_exec('ps aux | grep "octane:start"');
        // Parse output và return stats
    }
}

Schedule command mỗi phút:

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    $schedule->command('octane:monitor')->everyMinute();
}

2. Request metrics

// app/Http/Middleware/OctaneMetrics.php
class OctaneMetrics
{
    public function handle($request, $next)
    {
        $start = microtime(true);
        $startMemory = memory_get_usage();
        
        $response = $next($request);
        
        $duration = (microtime(true) - $start) * 1000;
        $memoryUsed = (memory_get_usage() - $startMemory) / 1024 / 1024;
        
        Log::info('Request metrics', [
            'url' => $request->url(),
            'duration_ms' => $duration,
            'memory_mb' => $memoryUsed,
            'worker_pid' => getmypid(),
        ]);
        
        return $response;
    }
}

3. Grafana Dashboard

Export metrics sang Prometheus:

Route::get('/metrics', function () {
    $workers = app('octane')->workers();
    
    return response([
        'octane_workers_total' => count($workers),
        'octane_memory_usage_bytes' => memory_get_usage(),
        'octane_requests_total' => Cache::get('octane.requests.total', 0),
    ])->header('Content-Type', 'text/plain');
});

Lesson 3: Database Connection Pool

Vấn đề

Sau khi scale lên 4 servers, database báo: “Too many connections”.

Root Cause

Mỗi Octane worker giữ 1 persistent connection.

  • 4 servers × 8 workers/server = 32 connections
  • MySQL max_connections = 50
  • Chỉ còn 18 connections cho background jobs, admin panel, etc.

Solution 1: Tăng max_connections

-- MySQL
SET GLOBAL max_connections = 200;

Trade-off: Tốn nhiều RAM hơn.

Solution 2: Connection pooling

// config/database.php
'mysql' => [
    'driver' => 'mysql',
    'pool' => [
        'min_connections' => 1,
        'max_connections' => 10,
        'wait_timeout' => 3.0,
    ],
],

Lưu ý: Chỉ Swoole hỗ trợ connection pooling. RoadRunner không có.

Solution 3: Giảm số workers

// config/octane.php
'swoole' => [
    'options' => [
        'worker_num' => 4, // Giảm từ 8 xuống 4
    ],
],

Trade-off: Throughput giảm, nhưng ổn định hơn.

Lesson 4: Session & Cache phải dùng Redis

Vấn đề

User login ở server A, request tiếp theo đến server B → Chưa login.

Root Cause

Session driver = file. Mỗi server có file system riêng.

Solution

SESSION_DRIVER=redis
CACHE_DRIVER=redis
REDIS_HOST=redis-cluster.example.com

Bonus: Redis cũng nhanh hơn file driver.

Lesson 5: Graceful Shutdown

Vấn đề

Deploy code mới → Octane restart → Requests đang xử lý bị drop → Users thấy 502 error.

Solution: Zero-downtime deployment

Bước 1: Reload thay vì restart

# Bad: Hard restart
php artisan octane:restart

# Good: Graceful reload
php artisan octane:reload

reload đợi workers xử lý xong requests hiện tại trước khi restart.

Bước 2: Blue-Green Deployment

# Deploy code mới lên server "green"
# Chuyển load balancer từ "blue" sang "green"
# Đợi "blue" xử lý xong requests
# Shutdown "blue"

Bước 3: Health check

Route::get('/health', function () {
    return response()->json([
        'status' => 'ok',
        'octane' => true,
        'memory_mb' => memory_get_usage(true) / 1024 / 1024,
    ]);
});

Load balancer chỉ route traffic đến servers có health check pass.

Lesson 6: File Uploads cần special handling

Vấn đề

User upload file lên server A. Request tiếp theo (download file) đến server B → File not found.

Solution: Shared storage

Option 1: S3

// config/filesystems.php
'default' => 's3',

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
],

Option 2: NFS/EFS

Mount shared volume trên tất cả servers:

# /etc/fstab
nfs-server:/exports/uploads /var/www/storage/app/public nfs defaults 0 0

Lesson 7: Logging cần centralized

Vấn đề

Bug xảy ra → Phải SSH vào 4 servers để đọc log → Mất thời gian.

Solution: Centralized logging

Option 1: Laravel Log to Database

// config/logging.php
'channels' => [
    'database' => [
        'driver' => 'custom',
        'via' => App\Logging\DatabaseLogger::class,
    ],
],

Option 2: ELK Stack (Elasticsearch, Logstash, Kibana)

// config/logging.php
'channels' => [
    'logstash' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\SocketHandler::class,
        'handler_with' => [
            'connectionString' => 'tcp://logstash:5000',
        ],
    ],
],

Option 3: Cloud logging (Papertrail, Logtail)

'channels' => [
    'papertrail' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\SyslogUdpHandler::class,
        'handler_with' => [
            'host' => env('PAPERTRAIL_URL'),
            'port' => env('PAPERTRAIL_PORT'),
        ],
    ],
],

Production Checklist

Trước khi deploy Octane production:

  • Test memory leaks với load test (10,000+ requests)
  • Set max_request để auto-restart workers
  • Monitor memory usage của workers
  • Dùng Redis cho session và cache
  • Setup connection pooling cho database
  • Implement health check endpoint
  • Centralized logging
  • Shared storage cho file uploads
  • Graceful shutdown trong deployment
  • Rollback plan (có thể quay lại PHP-FPM nhanh chóng)

Kết luận

Laravel Octane trong production không phải “cài xong là xong”.

Những điều tôi ước mình biết trước:

  • Memory leak sẽ xảy ra, cần monitoring chặt chẽ
  • Database connections cần quản lý cẩn thận
  • Session/Cache phải dùng Redis
  • File uploads cần shared storage
  • Logging phải centralized

Có đáng không?

Sau 6 tháng, câu trả lời của tôi là: .

  • API response time giảm 70%
  • Server cost giảm 40% (cần ít servers hơn)
  • User experience tốt hơn đáng kể

Nhưng bạn phải sẵn sàng đầu tư thời gian vào monitoring và maintenance.

Lời khuyên cuối:

  • Bắt đầu với staging environment
  • Monitor kỹ 2-4 tuần trước khi scale
  • Luôn có rollback plan
  • Document mọi issue và fix

Octane không phải cho mọi project. Nhưng nếu project của bạn cần performance, và team sẵn sàng học, nó là một game changer.

quizQuick Quiz
Câu 1/3

Cách tốt nhất để detect memory leak trong Laravel Octane production?

history_edu Góc học tập & giải trí

Thử Thách Kiến Thức Lịch Sử?

Khám phá hàng trăm câu hỏi trắc nghiệm lịch sử thú vị tại HistoQuiz. Vừa học vừa chơi, nâng cao kiến thức ngay hôm nay!

Chơi Ngay arrow_forward