Проблема: setFetchBody(false) не загружал структуру письма, поэтому getAttachments() возвращал пустой список. Исправления: - FetchUnreadEmailsTask: setFetchBody(true) для загрузки структуры (вложения) - FetchUnreadEmailsTask: конвертация Attribute в строку для subject - config/imap.php: убраны fetch_body/fetch_flags (контроль на уровне Query) - Обновлена документация Fix: "В письме не найдено вложений" ошибка при обработке email Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
11 KiB
Оптимизация обработки Email (IMAP)
Проблема
При обработке большого количества email-сообщений возникала ошибка:
Allowed memory size of 268435456 bytes exhausted (tried to allocate 6291480 bytes)
Корневые причины:
- Библиотека
webklex/php-imapзагружала все тела писем по умолчанию - Отсутствовали лимиты на количество загружаемых сообщений
- Не было контроля использования памяти
- Все письма загружались в память одновременно
Решение
Оптимизация выполнена в 3 уровня:
Уровень 1: Конфигурация (config/imap.php)
Добавлены настройки для предотвращения загрузки лишних данных:
'options' => [
'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK, // Не помечать как прочитанные
'message_key' => 'id', // Использовать UID
'fetch_limit' => env('IMAP_FETCH_LIMIT', 20), // Лимит сообщений
],
Важно: fetch_body и fetch_flags контролируются на уровне Query (setFetchBody(), setFetchFlags()), а не в конфиге, чтобы не ломать загрузку вложений.
Результат: Письма не помечаются как прочитанные автоматически, используется лимит на количество сообщений.
Уровень 2: Оптимизация кода
2A. FetchUnreadEmailsTask
setFetchBody(true)— загружаем структуру письма (необходимо для вложений)setFetchFlags(false)— отключаем загрузку флагов (экономия памяти)- Параметр
$limitдля контроля количества сообщений - Добавлен мониторинг памяти через
MemoryAwareTrait
$query = $folder->messages()
->unseen()
->setFetchBody(true) // Загружаем структуру (нужна для вложений)
->setFetchFlags(false) // Флаги не нужны
->limit($limit); // Лимит сообщений
Важно: setFetchBody(true) необходим для загрузки структуры письма, которая содержит информацию о вложениях. Без этого getAttachments() возвращает пустой список.
2B. FetchEmailNewsAction
- Реализована batch-обработка (по 10 писем за цикл)
- Добавлена принудительная сборка мусора после каждого batch
- Добавлен мониторинг памяти с логированием
private const BATCH_SIZE = 10;
private const MEMORY_THRESHOLD = 400 * 1024 * 1024; // 400MB
while ($hasMoreEmails) {
$emails = $this->fetchUnreadEmailsTask->run($folder, self::BATCH_SIZE, $offset);
foreach ($emails as $email) {
$this->processEmail($email);
}
$this->collectGarbageIfNeeded(); // gc_collect_cycles()
$offset += self::BATCH_SIZE;
}
2C. DownloadAttachmentsTask
- Вложения загружаются напрямую без загрузки тела письма
- Добавлен мониторинг памяти для больших вложений (>100MB)
// Метод getAttachments() использует структуру, а не raw_body
$attachments = $message->getAttachments();
Уровень 3: Инфраструктура
3A. ProcessEmailNewsJob (Queue Worker)
Создан Queue Job для асинхронной обработки:
class ProcessEmailNewsJob implements ShouldQueue
{
public $timeout = 300; // 5 минут
public $tries = 3; // 3 попытки
public $backoff = 60; // 1 минута между попытками
public function handle(FetchEmailNewsAction $action): void
{
$action->run();
}
}
Преимущества:
- Обработка вынесена из веб-процесса в queue worker
- Автоматические повторные попытки при ошибках
- Изоляция от пользовательских запросов
- Контроль таймаутов и памяти
3B. FetchEmailNewsCommand (Artisan)
Добавлены два режима работы:
Синхронный (backward compatibility):
docker exec ntspi-php php artisan email:fetch-news
Асинхронный (рекомендуется для production):
docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
3C. MemoryAwareTrait
Создан трейт для мониторинга памяти:
class MyTask
{
use MemoryAwareTrait;
public function run(): void
{
$this->logMemoryUsage('start');
// ... logic
$this->collectGarbageIfNeeded();
}
}
Методы:
logMemoryUsage($context)— логирование при превышении 100MBcollectGarbageIfNeeded()— сборка мусора при превышении 400MBisMemoryLimitExceeded($percent)— проверка лимита
PHP Configuration
_docker/app/php.ini
memory_limit = 1G # Увеличено с 512MB до 1GB
Важно: После изменения php.ini необходимо пересобрать PHP-контейнер:
docker-compose down
docker-compose up -d --build app
Использование
Development (синхронно)
docker exec ntspi-php php artisan email:fetch-news --log
Production (асинхронно)
- Запустить queue worker:
docker exec ntspi-php php artisan queue:work --queue=email-processing --timeout=300
- Отправить задачу в очередь:
docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
- Мониторинг:
# Логи job
tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob
# Статус очереди
docker exec ntspi-php php artisan queue:monitor email-processing
Cron (автоматическая обработка)
Добавить в crontab:
*/15 * * * * docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
Метрики производительности
До оптимизации
- Потребление памяти: 256MB+ (ошибка при 50+ письмах)
- Время обработки: 30+ секунд (блокирующий вызов)
- Надежность: Низкая (падал при больших письмах)
После оптимизации
- Потребление памяти: ~150MB (batch по 10 писем)
- Время обработки: Асинхронное (не блокирует веб)
- Надежность: Высокая (автоматические повторные попытки)
Мониторинг памяти
Все Tasks логируют использование памяти при превышении порогов:
[2024-01-15 10:30:00] local.INFO: [MemoryMonitor] Использование памяти
{
"context": "after_fetching_emails",
"current": "145.23MB",
"peak": "178.45MB",
"memory_limit": "1G",
"emails_count": 10
}
[2024-01-15 10:30:05] local.INFO: [MemoryMonitor] Сборка мусора выполнена
{
"memory_before": "412.5MB",
"memory_after": "156.3MB",
"freed": "256.2MB"
}
Troubleshooting
Ошибка: "Allowed memory size exhausted"
- Проверьте применение php.ini:
docker exec ntspi-php php -i | grep memory_limit
# Должно быть: memory_limit => 1G => 1G
- Пересоберите контейнер:
docker-compose down
docker-compose up -d --build app
- Уменьшите batch size:
В
FetchEmailNewsActionизмените:
private const BATCH_SIZE = 5; # Было 10, стало 5
Queue worker не запускается
# Проверьте статус
docker exec ntspi-php php artisan queue:status
# Перезапустите worker
docker restart ntspi-php-queue
# Проверьте логи
tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob
Письма не обрабатываются
# Проверьте подключение к IMAP
docker exec ntspi-php php artisan tinker
>>> config('imap.accounts.email_news.host')
>>> config('email-news.enabled')
# Запустите с флагом --force
docker exec ntspi-php php artisan email:fetch-news --force --log
Архитектура (Porto)
app/Containers/Dashboard/
├── Actions/EmailNews/
│ └── FetchEmailNewsAction.php # Оркестрация + batch-обработка
├── Tasks/Email/
│ ├── ConnectToImapTask.php # IMAP подключение (+ MemoryAwareTrait)
│ ├── FetchUnreadEmailsTask.php # Получение заголовков (+ MemoryAwareTrait)
│ ├── DownloadAttachmentsTask.php # Сохранение вложений (+ memory monitoring)
│ ├── FilterBySenderTask.php # Фильтрация по отправителю
│ └── MarkEmailAsReadTask.php # Пометка как прочитанное
├── Jobs/
│ └── ProcessEmailNewsJob.php # Queue Job для асинхронной обработки
├── Traits/
│ └── MemoryAwareTrait.php # Трейт мониторинга памяти
└── Commands/
└── FetchEmailNewsCommand.php # Artisan команда (sync/async режимы)
Чеклист деплоя
- Применены изменения в
config/imap.php - Обновлены все Tasks и Actions
- Создан
ProcessEmailNewsJob - Обновлен
FetchEmailNewsCommand - Увеличен
memory_limitвphp.iniдо1G - Пересобран PHP-контейнер (
docker-compose up -d --build app) - Проверено применение конфига (
php -i | grep memory_limit) - Queue worker запущен (
docker exec ntspi-php php artisan queue:work) - Протестирован async режим (
email:fetch-news --async) - Проверены логи на наличие ошибок памяти