Dùng CI:GithubAction để phát hiện toàn bộ câu truy vấn N+1 trong Laravel
Nếu bạn không chắc hệ thống của mình đang tồn tại bao nhiêu câu truy vấn có vấn đề về N+1 Query thì bài viết này dành cho bạn. Đặc biệt dành cho các bạn quản lý dự án, team leader.
Đã sử dụng Laravel thì có lẽ mọi người cũng biết đến laravel-query-detector, một thư viện nho nhỏ để phát hiện N+1 Query. Tuy nhiên, vì nhiều lý do thì trong quá trình phát triển, chúng ta chưa chú ý giải quyết vấn đề “N+1 Query” này. Đến một lúc nào đó, khi lượng truy vấn nhiều lên (mức sử dụng database) thì vấn đề N+1 Query lại trở thành chủ đề cần giải quyết càng sớm càng tốt.
Nội dung chính
Câu truy vấn N+1 (N+1 Query) là gì
Nói ngắn gọn là Laravel sinh ra nhiều câu query hơn mức cần thiết. Chính xác hơn là 1+N, tức là khi ta truy vấn 1 query, mà query đó có quan hệ 1-nhiều (1-n), nhiều-nhiều (n-n) sẽ làm nảy sinh thêm n (nhiều) câu query khác. Lý do là Laravel cho phép chúng ta viết code cho model dễ đọc, mà không cần phải hiểu cách hoạt động đằng sau của chúng.
Tất nhiên, vấn đề này nảy sinh không phải chỉ do Eloquent hay chỉ đối với mỗi mình Laravel, mà trong cả ngành công nghiệp lập trình khi mà chúng ta sử dụng các framework vì sự tiện dụng của chúng.
Trong nội dung bài viết này, sẽ không đề cập đến cách giải quyết vấn đề này trong Laravel, bạn có thể tìm kiếm trên các bài viết khác nhé.
Phát hiện câu truy vấn có vấn đề N+1 Query
Khi sử dụng laravel-query-detector, bạn sẽ phát hiện N+1 Query và dựa theo cấu hình tương ứng để nắm thông tin N+1 Query.
Trong từng API (dùng clockwork):
Trong từng màn hình (dùng cấu hình Alert):
Trong từng màn hình (sử dụng debugbar, kết hợp cấu hình Log):
Danh sách các thiết lập thì bạn có thể xem thêm tại đây: https://beyondco.de/docs/laravel-query-detector/usage
/*
* Define the output format that you want to use. Multiple classes are supported.
* Available options are:
*
* Alert:
* Displays an alert on the website
* \BeyondCode\QueryDetector\Outputs\Alert::class
*
* Console:
* Writes the N+1 queries into your browsers console log
* \BeyondCode\QueryDetector\Outputs\Console::class
*
* Clockwork: (make sure you have the itsgoingd/clockwork package installed)
* Writes the N+1 queries warnings to Clockwork log
* \BeyondCode\QueryDetector\Outputs\Clockwork::class
*
* Debugbar: (make sure you have the barryvdh/laravel-debugbar package installed)
* Writes the N+1 queries into a custom messages collector of Debugbar
* \BeyondCode\QueryDetector\Outputs\Debugbar::class
*
* JSON:
* Writes the N+1 queries into the response body of your JSON responses
* \BeyondCode\QueryDetector\Outputs\Json::class
*
* Log:
* Writes the N+1 queries into the Laravel.log file
* \BeyondCode\QueryDetector\Outputs\Log::class
*/
'output' => [
\BeyondCode\QueryDetector\Outputs\Log::class,
\BeyondCode\QueryDetector\Outputs\Alert::class,
]
Hệ thống của bạn có bao nhiêu câu truy vấn có vấn đề N+1 Query?
Vì nhiều lý do, trong 1 dự án có nhiều người với peer-review yếu, hoặc chưa quan tâm lắm đến vấn đề N+1 Query, thì khả năng bỏ qua vấn đề N+1 Query khá là cao. Vì vậy đến một lúc nào đó khi bạn trở thành người quản lý dự án đó, chịu trách nhiệm nâng cao hiệu suất sử dụng database bạn sẽ rất vất vả để tìm ra các điểm nghẽn đó.
Ý tưởng cũng khá đơn giản: viết toàn bộ testcase cho tất cả router, khi chạy test thì sử dụng laravel-query-detector ghi log các câu truy vấn có vấn đề N+1 Query ra 1 file riêng, dùng các lệnh về xử lý file để tìm kiếm và thống kê.
Viết toàn bộ testcase cho tất cả router
Nếu bạn có viết testcase rồi thì tốt, không có thì cũng chỉ cần viết cái testcase đơn giản là gọi đến toàn bộ route rồi cho skip cũng được 😑. Làm sao đảm bảo là gọi hết đến tất cả các route thì mới phát hiện được hết được. Không có thời gian thì viết cho mấy cái route quan trọng cũng được 🥲
Cấu hình laravel-query-detector
php artisan vendor:publish --provider="BeyondCode\QueryDetector\QueryDetectorServiceProvider"
Ta sẽ có được file config/querydetector.php
...
/* Chú ý dòng này, nếu bạn có file phpunit.xml, cần thiết lập QUERY_DETECTOR_ENABLED=true */
'enabled' => env('QUERY_DETECTOR_ENABLED', null),
...
/* Tùy theo tính chất khắt khe mà đặt chốt chặn phù hợp */
'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1),
...
'log_channel' => env('QUERY_DETECTOR_LOG_CHANNEL', 'querydetector'),
...
'output' => [
...
\BeyondCode\QueryDetector\Outputs\Log::class,
...
]
Chúng ta sẽ ghi log vào channel ‘querydetector’, vậy nên phải cấu hình channel trong file config/logging.php
'querydetector' => [
'driver' => 'single',
'path' => storage_path('logs/querydetector.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
]
Như vậy toàn bộ kết quả của laravel-query-detector sẽ được ghi vào file storage/logs/querydetector.log
Thống kê
grep "Detected N+1 Query" storage/logs/querydetector.log | wc -l
Ok, như vậy là ta sẽ ra được 1 con số, được hiểu là số vấn đề N+1 Query đang tồn tại trong hệ thống. Con số này có thể không chính xác vì nhiều lý do:
- Một route được viết testcase nhiều lần – nói 1 cách khác là được gọi nhiều lần.
- Trong một route, số vấn đề N+1 Query là số nhiều
Nhưng nếu kết quả là số 0 thì chúc mừng bạn, hệ thống của bạn không có vấn đề (mặc dù còn phụ thuộc số lượng testcase)
Sử dụng CI: GithubAction để thống kê
Nếu bạn có sử dụng CI với GithubAction, thì với kết quả của phpUnit, bạn làm thêm 1 vài thao tác nữa sẽ được kết quả như thế này:
316 – con số thật khủng khiếp phải không nào :D, điều tuyệt vời là bạn đã biết được con số đó, và mỗi khi giải quyết N+1 Query được push lên, GithubAction sẽ comment con số cuối cùng.
File cấu hình ci.yml sẽ được chỉnh sửa thêm vào như sau:
//ci.yml
on: [ pull_request ]
jobs:
phpUnit:
- name: chạy phpunit
.... "./vendor/bin/phpunit --stop-on-failure"
- name: N+1 Query Report #Đọc kết quả querydetector, thống kê và lưu vào file n-1-query-summary.log
run: |
grep "Detected N+1 Query" storage/logs/querydetector.log | wc -l > storage/logs/n-1-query-summary.log
working-directory: ${{env.working-directory}}
- name: Read coverage summary #Đọc nội dung file n-1-query-summary.log
id: n-1-query-summary
uses: juliangruber/read-file-action@v1
with:
path: ./app/laravel/storage/logs/n-1-query-summary.log
- name: Comment N+1 Query Summary # Comment kết quả vào PR
uses: marocchino/sticky-pull-request-comment@v2
with:
recreate: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
header: n-1-query
message: |
## N+1 Query Summary
Number of N+1 queries: ${{ steps.n-1-query-summary.outputs.content }}
- name: Archive N+1 query detector results # Lưu log của querydetector vào Artifact của github
uses: actions/upload-artifact@v2
with:
name: n+1-query-report
path: ./app/laravel/storage/logs/querydetector.log
File querydetector.log sẽ được upload lên artifacts của Github
Như vậy ở góc độ quản lý dự án, bạn không cần phải chạy test trên máy của mình mà vẫn đảm bảo chất lượng dự án ngày càng cải thiện, ít nhất là phải biết hiện trạng của dự án.
Fullstack Station Tips
- Khi bạn đã biết số vấn đề còn tồn tại , có thể đưa vào CI (ci.yml chẳng hạn) để đảm bảo con số đó không tăng lên khi phát triển thêm chức năng.
- Có thể dựa vào danh sách file có vấn đề N+1 Query trong querydetector.log để so sánh với danh sách file được thêm/sửa. Nếu nằm trong querydetector.log thì không cho pass CI 🤣