Tối Ưu Hóa Truyền Tải Dữ Liệu Lớn với Laravel Streaming Responses

Tối Ưu Hóa Truyền Tải Dữ Liệu Lớn với Laravel Streaming Responses

Nguồn gốc: Laravel News – Optimizing Large Data Delivery with Laravel Streaming Responses
Tác giả: Harris Raftopoulos
Ngày xuất bản: 7 tháng 1, 2025


Giới thiệu

Tính năng Streaming Response trong Laravel cho phép xử lý hiệu quả các tập dữ liệu lớn bằng cách gửi dữ liệu theo từng phần nhỏ ngay khi chúng được tạo ra, thay vì phải chờ toàn bộ dữ liệu sẵn sàng mới gửi đi. Phương pháp này giúp:

  • Giảm mức sử dụng bộ nhớ (RAM) trên server
  • Cải thiện thời gian phản hồi – người dùng nhận được dữ liệu ngay lập tức
  • Tránh timeout khi xử lý tác vụ nặng, tốn thời gian
  • Nâng cao trải nghiệm người dùng khi xuất file hoặc tải dữ liệu lớn

Cơ bản: Cách hoạt động của Streaming

Laravel cung cấp helper response()->stream() để trả về dữ liệu dạng luồng. Cấu trúc cơ bản như sau:

Route::get('/stream', function () {
    return response()->stream(function () {
        foreach (range(1, 100) as $number) {
            echo "Line {$number}\n";
            ob_flush(); // Xả bộ đệm output của PHP
            flush();    // Gửi dữ liệu xuống client ngay lập tức
        }
    }, 200, ['Content-Type' => 'text/plain']);
});

Giải thích chi tiết:

Thành phần Mục đích
response()->stream(callback, status, headers) Tạo một streaming response
echo Ghi dữ liệu vào output buffer
ob_flush() Xả output buffer của PHP
flush() Buộc PHP gửi dữ liệu xuống web server và client

Lưu ý quan trọng: Cần gọi cả ob_flush()flush() để đảm bảo dữ liệu được gửi đi ngay lập tức, không bị giữ lại trong buffer.


Ví dụ thực tế: Xuất CSV dữ liệu đơn hàng lớn

Đây là trường hợp điển hình nhất của streaming – xuất hàng trăm nghìn bản ghi ra file CSV mà không làm quá tải bộ nhớ server.

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Support\Facades\DB;

class ExportController extends Controller
{
    public function exportOrders()
    {
        return response()->stream(function () {
            // Gửi header của file CSV
            echo "Order ID,Customer,Total,Status,Date\n";
            ob_flush();
            flush();

            // Xử lý đơn hàng theo từng đợt (chunk) để tiết kiệm bộ nhớ
            Order::query()
                ->with('customer')
                ->orderBy('created_at', 'desc')
                ->chunk(500, function ($orders) {
                    foreach ($orders as $order) {
                        echo sprintf(
                            "%s,%s,%.2f,%s,%s\n",
                            $order->id,
                            str_replace(',', ' ', $order->customer->name), // Tránh lỗi CSV do dấu phẩy
                            $order->total,
                            $order->status,
                            $order->created_at->format('Y-m-d H:i:s')
                        );
                        ob_flush();
                        flush();
                    }
                });

        }, 200, [
            'Content-Type'        => 'text/csv',
            'Content-Disposition' => 'attachment; filename="orders.csv"',
            'X-Accel-Buffering'   => 'no', // Tắt buffering của Nginx
        ]);
    }
}

Phân tích các kỹ thuật được sử dụng:

1. Chunking – Phân trang dữ liệu từ database

->chunk(500, function ($orders) { ... })

Thay vì load toàn bộ hàng triệu bản ghi vào RAM cùng lúc, chunk() chỉ lấy 500 bản ghi mỗi lần truy vấn. Sau khi xử lý xong một batch, bộ nhớ sẽ được giải phóng trước khi lấy batch tiếp theo.

2. Eager Loading – Tránh N+1 Query

->with('customer')

Nạp sẵn dữ liệu quan hệ customer trong cùng một truy vấn, tránh tình trạng mỗi đơn hàng lại thực hiện thêm một câu query để lấy tên khách hàng.

3. Headers quan trọng

Header Giá trị Mục đích
Content-Type text/csv Xác định định dạng file
Content-Disposition attachment; filename="orders.csv" Kích hoạt dialog tải file
X-Accel-Buffering no Tắt buffering của Nginx để dữ liệu được truyền ngay lập tức

Các trường hợp sử dụng phổ biến khác

1. Streaming JSON Lines (JSONL)

Phù hợp khi client cần xử lý từng bản ghi ngay khi nhận được:

Route::get('/stream-products', function () {
    return response()->stream(function () {
        Product::chunk(200, function ($products) {
            foreach ($products as $product) {
                echo json_encode($product) . "\n";
                ob_flush();
                flush();
            }
        });
    }, 200, ['Content-Type' => 'application/x-ndjson']);
});

2. Server-Sent Events (SSE) – Cập nhật thời gian thực

SSE cho phép server đẩy dữ liệu xuống client theo thời gian thực (ví dụ: tiến trình xử lý, thông báo):

Route::get('/progress', function () {
    return response()->stream(function () {
        $steps = ['Đang chuẩn bị dữ liệu...', 'Đang xử lý...', 'Hoàn tất!'];

        foreach ($steps as $i => $message) {
            $progress = (int)(($i + 1) / count($steps) * 100);

            echo "data: " . json_encode([
                'message'  => $message,
                'progress' => $progress,
            ]) . "\n\n";

            ob_flush();
            flush();
            sleep(1); // Giả lập tác vụ mất thời gian
        }
    }, 200, [
        'Content-Type'  => 'text/event-stream',
        'Cache-Control' => 'no-cache',
        'X-Accel-Buffering' => 'no',
    ]);
});

3. Streaming nội dung từ AI (LLM)

Kết hợp streaming với các API AI như OpenAI để hiển thị văn bản từng từ như ChatGPT:

Route::post('/ai-response', function (Request $request) {
    return response()->stream(function () use ($request) {
        $client = OpenAI::client(config('services.openai.key'));

        $stream = $client->chat()->createStreamed([
            'model'    => 'gpt-4o',
            'messages' => [['role' => 'user', 'content' => $request->input('prompt')]],
        ]);

        foreach ($stream as $response) {
            $text = $response->choices[0]->delta->content ?? '';
            echo "data: " . json_encode(['text' => $text]) . "\n\n";
            ob_flush();
            flush();
        }
    }, 200, [
        'Content-Type'      => 'text/event-stream',
        'Cache-Control'     => 'no-cache',
        'X-Accel-Buffering' => 'no',
    ]);
});

So sánh: Streaming vs Response thông thường

Tiêu chí Response thông thường Streaming Response
Bộ nhớ sử dụng Cao (load toàn bộ dữ liệu) Thấp (xử lý từng phần)
Thời gian đến byte đầu tiên (TTFB) Chậm Nhanh
Nguy cơ timeout Cao với dữ liệu lớn Thấp
Độ phức tạp triển khai Đơn giản Trung bình
Phù hợp với Dữ liệu nhỏ Dữ liệu lớn, tác vụ dài

Lưu ý khi triển khai lên môi trường production

Nginx

Cần thêm header X-Accel-Buffering: no hoặc cấu hình trong Nginx:

location /stream {
    proxy_buffering off;
    proxy_cache off;
}

PHP-FPM

Đảm bảo output_buffering được tắt trong php.ini hoặc dùng ini_set:

ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', 'off');

Apache

Thêm vào .htaccess:

SetEnv no-gzip 1
SetEnv dont-vary 1

Kết luận

Laravel Streaming Responses là một công cụ mạnh mẽ và không thể thiếu khi xây dựng các tính năng như:

  • Xuất file lớn (CSV, Excel với hàng triệu dòng)
  • Xử lý dữ liệu thời gian thực (SSE, live progress)
  • Tích hợp AI với phản hồi streaming từ LLM
  • API trả về dữ liệu lớn mà không lo bộ nhớ

Bằng cách kết hợp response()->stream() với kỹ thuật chunk() để phân trang dữ liệu từ database, bạn có thể xử lý hàng triệu bản ghi một cách hiệu quả trong khi vẫn duy trì trải nghiệm người dùng tốt và hệ thống ổn định.