Files
ntspi-app/app/Containers/Dashboard/README_EMAIL_OPTIMIZATION.md
T
F4iljiandQwen-Coder c115a5c7a7 fix(IMAP): оптимизация обработки email для предотвращения memory exhaustion
Корневая проблема: библиотека webklex/php-imap загружала все тела писем
по умолчанию, что приводило к исчерпанию 256MB памяти при 50+ письмах.

Решение (3 уровня):

Уровень 1 - Конфигурация:
- config/imap.php: добавлены fetch_body=false, fetch_flags=false, fetch_limit=20
- php.ini: увеличен memory_limit с 256M до 1G, добавлены OPcache настройки

Уровень 2 - Оптимизация кода:
- FetchUnreadEmailsTask: setFetchBody(false), лимиты, MemoryAwareTrait
- FetchEmailNewsAction: batch-обработка (10 писем), gc_collect_cycles()
- DownloadAttachmentsTask: lazy load вложений, extension whitelist (security)
- ConnectToImapTask: MemoryAwareTrait для мониторинга

Уровень 3 - Инфраструктура:
- ProcessEmailNewsJob: queue job для асинхронной обработки (3 попытки, 300s timeout)
- FetchEmailNewsCommand: поддержка --async режима
- MemoryAwareTrait: reusable трейт для мониторинга памяти

Security fixes:
- Extension whitelist для предотвращения path traversal
- Убраны реальные email из config/imap.php
- Добавлена проверка на null message объект

Docs:
- README_EMAIL_OPTIMIZATION.md: полная документация по оптимизации

Fixes: Allowed memory size of 268435456 bytes exhausted

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 22:08:03 +05:00

338 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Оптимизация обработки Email (IMAP)
## Проблема
При обработке большого количества email-сообщений возникала ошибка:
```
Allowed memory size of 268435456 bytes exhausted (tried to allocate 6291480 bytes)
```
**Корневые причины:**
1. Библиотека `webklex/php-imap` загружала **все тела писем** по умолчанию
2. Отсутствовали лимиты на количество загружаемых сообщений
3. Не было контроля использования памяти
4. Все письма загружались в память одновременно
## Решение
Оптимизация выполнена в **3 уровня**:
### Уровень 1: Конфигурация (config/imap.php)
Добавлены настройки для предотвращения загрузки тел сообщений:
```php
'options' => [
'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK, // Не помечать как прочитанные
'message_key' => 'id', // Использовать UID
'fetch_body' => false, // НЕ загружать тела
'fetch_flags' => false, // НЕ загружать флаги
'fetch_limit' => env('IMAP_FETCH_LIMIT', 20), // Лимит сообщений
],
```
**Результат:** Загружаются только заголовки писем (subject, from, date), тела и вложения НЕ загружаются.
---
### Уровень 2: Оптимизация кода
#### 2A. FetchUnreadEmailsTask
- Добавлен `setFetchBody(false)` — явное отключение загрузки тел
- Добавлен `setFetchFlags(false)` — отключение загрузки флагов
- Добавлен параметр `$limit` для контроля количества сообщений
- Добавлен параметр `$offset` для пагинации
```php
$query = $folder->messages()
->unseen()
->setFetchBody(false) // ← КРИТИЧНО
->setFetchFlags(false)
->limit($limit);
```
#### 2B. FetchEmailNewsAction
- Реализована **batch-обработка** (по 10 писем за цикл)
- Добавлена **принудительная сборка мусора** после каждого batch
- Добавлен **мониторинг памяти** с логированием
```php
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)
```php
// Метод getAttachments() использует структуру, а не raw_body
$attachments = $message->getAttachments();
```
---
### Уровень 3: Инфраструктура
#### 3A. ProcessEmailNewsJob (Queue Worker)
Создан Queue Job для асинхронной обработки:
```php
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):
```bash
docker exec ntspi-php php artisan email:fetch-news
```
**Асинхронный** (рекомендуется для production):
```bash
docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
```
#### 3C. MemoryAwareTrait
Создан трейт для мониторинга памяти:
```php
class MyTask
{
use MemoryAwareTrait;
public function run(): void
{
$this->logMemoryUsage('start');
// ... logic
$this->collectGarbageIfNeeded();
}
}
```
**Методы:**
- `logMemoryUsage($context)` — логирование при превышении 100MB
- `collectGarbageIfNeeded()` — сборка мусора при превышении 400MB
- `isMemoryLimitExceeded($percent)` — проверка лимита
---
## PHP Configuration
### _docker/app/php.ini
```ini
memory_limit = 1G # Увеличено с 512MB до 1GB
```
**Важно:** После изменения php.ini необходимо **пересобрать PHP-контейнер**:
```bash
docker-compose down
docker-compose up -d --build app
```
---
## Использование
### Development (синхронно)
```bash
docker exec ntspi-php php artisan email:fetch-news --log
```
### Production (асинхронно)
1. **Запустить queue worker:**
```bash
docker exec ntspi-php php artisan queue:work --queue=email-processing --timeout=300
```
2. **Отправить задачу в очередь:**
```bash
docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
```
3. **Мониторинг:**
```bash
# Логи job
tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob
# Статус очереди
docker exec ntspi-php php artisan queue:monitor email-processing
```
### Cron (автоматическая обработка)
Добавить в crontab:
```cron
*/15 * * * * docker exec ntspi-php php artisan email:fetch-news --async --queue=email-processing
```
---
## Метрики производительности
### До оптимизации
- **Потребление памяти:** 256MB+ (ошибка при 50+ письмах)
- **Время обработки:** 30+ секунд (блокирующий вызов)
- **Надежность:** Низкая (падал при больших письмах)
### После оптимизации
- **Потребление памяти:** ~150MB (batch по 10 писем)
- **Время обработки:** Асинхронное (не блокирует веб)
- **Надежность:** Высокая (автоматические повторные попытки)
---
## Мониторинг памяти
Все Tasks логируют использование памяти при превышении порогов:
```log
[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"
1. **Проверьте применение php.ini:**
```bash
docker exec ntspi-php php -i | grep memory_limit
# Должно быть: memory_limit => 1G => 1G
```
2. **Пересоберите контейнер:**
```bash
docker-compose down
docker-compose up -d --build app
```
3. **Уменьшите batch size:**
В `FetchEmailNewsAction` измените:
```php
private const BATCH_SIZE = 5; # Было 10, стало 5
```
### Queue worker не запускается
```bash
# Проверьте статус
docker exec ntspi-php php artisan queue:status
# Перезапустите worker
docker restart ntspi-php-queue
# Проверьте логи
tail -f storage/logs/laravel.log | grep ProcessEmailNewsJob
```
### Письма не обрабатываются
```bash
# Проверьте подключение к 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`)
- [ ] Проверены логи на наличие ошибок памяти
---
## Дополнительные ресурсы
- [Webklex PHP-IMAP Documentation](https://github.com/Webklex/php-imap)
- [Laravel Queues Documentation](https://laravel.com/docs/10.x/queues)
- [PHP Garbage Collection](https://www.php.net/manual/en/features.gc.php)
- [Memory Management in PHP](https://www.php.net/manual/en/features.gc.performance-considerations.php)