perf: ускорение автоподбора — рендер-канал и шаг 2

render-service: умное ожидание вместо фикс-пауз. 2ГИС карточка 7.2 -> 1.6с, поиск 17 -> 5с, Яндекс-карточка 6.5 -> 2.8с. Качество проверено вживую.
шаг 2: таймаут открытия сайта 45 -> 15с, убран лишний повтор, карточки 2ГИС пачкой htmlBatch. Полнота сохранена, скорость выросла.
доки: ранбук выката рендер-виртуалки, фаза 8.

Ветка worktree-avtopodbor, не прод. Тесты 459/459, Pint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-06 04:49:59 +03:00
parent 31828d339e
commit 9022b15b42
5 changed files with 302 additions and 11 deletions
@@ -81,7 +81,8 @@ class AutopodborServiceProvider extends ServiceProvider
$pages = $sr ?? $xfetch;
$reader = $this->twoGisReader();
$siteFetcher = new CurlPlaywrightFetcher;
// Таймаут 15с (было 45): сайт конкурента отвечает за 1-3с; режем только висяки, не теряя живых.
$siteFetcher = new CurlPlaywrightFetcher(timeout: 15);
$fetcher = new CompositeFetcher(
siteFetcher: $siteFetcher,
directoryFetcher: new XfetchDirectoryFetcher($pages),
@@ -162,8 +162,10 @@ final class DeepStudyCollector implements SourceCollector
}
}
$firmUrls = array_slice(array_values(array_filter(array_unique($firmUrls), fn ($u) => str_contains((string) $u, '/firm/'))), 0, 12);
// Карточки 2ГИС — ПАЧКОЙ (виртуалка параллелит; для 2ГИС initialState это безопасно, проверено).
$htmls = $firmUrls !== [] ? $this->xfetch->htmlBatch($firmUrls) : [];
foreach ($firmUrls as $furl) {
$fh = $this->xfetch->html($furl);
$fh = $htmls[$furl] ?? '';
if ($fh === '') {
continue;
}
@@ -23,9 +23,6 @@ final class SiteOpener
$url = 'https://'.$this->idn($domain).'/';
try {
$site = $this->fetcher->site($url);
if ($site->rawHtml === '') {
$site = $this->fetcher->site($url); // ретрай на флак
}
$cands = $this->builder->build([$site], [], $areaCode);
$codeDigits = [];
foreach ($cands as $c) {
@@ -0,0 +1,278 @@
# Фаза 8 — памятка «поднять виртуалку и включить свой рендер». РАНБУК (05.07.2026)
> ## ✅ ВЫПОЛНЕНО НА DEV (05.07.2026, вечер) — виртуалка живёт и проверена
> - **ВМ `liderra-render`** создана в YC (тот же фолдер/сеть, что боевой): публичный **51.250.1.97**, внутренний **10.128.0.4**, Ubuntu 22.04, 4 ядра / 8 ГБ.
> - Рендер-сервис под **systemd** (`liderra-render.service`, автозапуск), Node 20 + полный Chromium.
> - Прокси подхватился (`/health` → `proxies:1`). Смоук 2ГИС через прокси — данные идут.
> - **https без домена:** Caddy + `sslip.io` → адрес **`https://51-250-1-97.sslip.io/render`** (настоящий сертификат Let's Encrypt, авто-продление). Домен НЕ понадобился.
> - **Файрвол (ufw):** снаружи только 22/80/443; порт сервиса 8790 закрыт (ходят только через «замочек»).
> - **Сквозная проверка кодом портала:** dev `app/.env` → `SELF_RENDER_ENDPOINT=https://51-250-1-97.sslip.io/render`, `SELF_RENDER_KEY=<в .env>`, `AUTOPODBOR_SELF_RENDER=true`. `SelfRenderClient.html()` + `TwoGisJsonReader.listing()` вернули **12 реальных ломбардов, 0 мусора** (double-tap лечит болезнь «Бургер Кинг»).
> - **Вход на ВМ:** `ssh -i ~/.ssh/liderra_deploy ubuntu@51.250.1.97`. Управление облаком: `yc` в `/c/Users/Administrator/yandex-cloud/bin`.
> - **Осталось (за владельцем, прод):** те же 3 строки в `app/.env` БОЕВОГО портала + `queue:work` перезапуск + сверка на объёме (~127 фирм). Это правка боевого — только с разрешения. Откат — флаг в `false`.
>
> Остальная часть файла ниже — исходный план (как делалось), оставлен для истории/повтора.
---
**Что это.** Пошаговый сценарий для последнего шага автоподбора: поднять отдельную виртуалку,
поставить на неё рендер-сервис (`render-service/`), воткнуть купленный прокси и секретный ключ,
проверить живьём, включить флаг на портале и сверить результат «фирма в фирму».
**Готовим «на берегу» (05.07):** сначала просматриваешь этот файл, потом заказываем виртуалку вместе, шаг за шагом.
Ничего из этого пока НЕ выполнено — это план команд, а не отчёт.
**Правила по ходу:** прод не трогаем; флаг по умолчанию ВЫКЛ; секреты только в `.env` (не в гит).
Ветка `worktree-avtopodbor`.
---
## 0. Что нужно от тебя ОДИН раз (единственное решение)
Порталу нужно звать виртуалку по защищённому адресу (с «замочком» https). Для «замочка» нужен
**поддомен**. Предлагаю самый простой и надёжный вариант:
- Завести DNS-запись **`render.liderra.ru`** → на IP новой виртуалки (A-запись, ты добавляешь у регистратора домена).
- На виртуалке ставим **Caddy** — он сам, бесплатно и автоматически, получает и продлевает «замочек».
Это НЕ трогает боевой сайт — просто ещё одно имя рядом. Если поддомен не хочешь — есть запасной
вариант (без домена, по «голому» IP с ограничением по firewall), опишу в §7. **Рекомендую поддомен.**
> 🔴 Реши только это: **поддомен `render.liderra.ru` — ок?** Остальное — техника, делаю сам с твоими нажатиями кнопок.
---
## 1. Заказать виртуалку (Yandex Cloud, консоль, вместе)
- **Регион/зона:** `ru-central1` (Москва) — как весь проект.
- **ОС:** Ubuntu 22.04 LTS.
- **Ресурсы:** 4 vCPU, **8 ГБ ОЗУ** (браузер прожорлив), гарантированная доля vCPU 100%.
- **Диск:** 20 ГБ SSD — с запасом (Chromium + профиль).
- **Публичный IP:** да (нужен и для «замочка», и чтобы прокси-трафик уходил в интернет).
- **SSH-ключ:** твой (или сгенерим при заказе). Логин по паролю — выключить.
- **Имя:** `liderra-render`.
- **Отдельно НЕ нужно:** базы, Redis, ролей БД, доступа к боевой БД. Сервис только «рисует».
Прикидка по деньгам: небольшая машина такого класса в YC — ориентировочно несколько сотен рублей
в месяц (точную цену покажет калькулятор при заказе). Трафик прокси уже оплачен отдельно (безлимит).
---
## 2. Firewall (security group виртуалки)
Впускать только нужное:
| Порт | Откуда | Зачем |
|---|---|---|
| 22 (SSH) | только твой IP (или твой диапазон) | заходить настраивать |
| 443 (https) | откуда зовёт портал | сам рендер (через Caddy) |
| 80 (http) | отовсюду | только чтобы Caddy получил «замочек» (Let's Encrypt) |
Порт сервиса **8790 наружу НЕ открываем** — он слушает только внутри машины, снаружи ходят через 443/Caddy.
> На этапе приёмки портал = локальная dev-машина (Windows). Значит на 443 временно впускаем и твой
> рабочий IP. Когда позже подключим боевой портал — добавим IP боевого сервера.
---
## 3. Поставить окружение на виртуалку (по SSH)
```bash
# обновиться + Node 20 (сервису нужен >=20)
sudo apt update && sudo apt -y upgrade
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# зависимости системного Chromium для Playwright (шрифты, libnss и т.п.)
sudo apt install -y \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libasound2 libpango-1.0-0 libcairo2 fonts-liberation
node -v # ожидаем v20.x
```
---
## 4. Залить рендер-сервис и поставить браузер
```bash
sudo mkdir -p /opt/liderra-render && sudo chown $USER /opt/liderra-render
# скопировать содержимое папки render-service/ из репозитория в /opt/liderra-render
# (scp с рабочей машины ИЛИ git clone нужной ветки и cp -r render-service/* /opt/liderra-render)
cd /opt/liderra-render
npm install # ставит playwright (по package.json)
npx playwright install chromium # скачивает сам браузер
```
> ⚠️ `node_modules` и скачанный Chromium на виртуалке — НЕ коммитить (их и нет в гите: `.gitignore`).
---
## 5. Секреты виртуалки — файл `/opt/liderra-render/.env`
Взять готовый `render-service/.env` (он уже собран в прошлой сессии: там строка `PROXIES=...` с логином/паролем
купленного прокси и сгенерированный `RENDER_KEY`). Скопировать его на виртуалку как `/opt/liderra-render/.env`:
```
PORT=8790
RENDER_KEY=<секрет из render-service/.env — НЕ печатать в чат/логи>
MAX_CONCURRENCY=8
PROXIES=http://<логин>:<пароль>@78.107.254.174:30470
```
> 🔑 Значения `RENDER_KEY` и логин/пароль прокси лежат в gitignored `render-service/.env`. В гит и в переписку не выносим.
---
## 6. Запустить сервис как службу (сам поднимается после перезагрузки)
Создать `/etc/systemd/system/liderra-render.service`:
```ini
[Unit]
Description=Liderra render service (2GIS/Yandex)
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/liderra-render
EnvironmentFile=/opt/liderra-render/.env
ExecStart=/usr/bin/node server.mjs
Restart=always
RestartSec=3
User=<твой-пользователь>
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now liderra-render
curl -s localhost:8790/health # ждём {"ok":true,"active":0,"max":8,"proxies":1}
```
`proxies:1` подтвердит, что прокси подхватился.
---
## 7. «Замочек» (Caddy) — поддомен → сервис
```bash
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
```
`/etc/caddy/Caddyfile` — весь конфиг в две строки:
```
render.liderra.ru {
reverse_proxy localhost:8790
}
```
```bash
sudo systemctl restart caddy
# с рабочей машины проверить извне (замок должен встать сам за минуту):
curl -s https://render.liderra.ru/health
```
**Запасной вариант без поддомена (если §0 «нет»):** Caddy не ставим, портал зовёт `http://<IP>:8790/render`,
а firewall на 8790 впускает ТОЛЬКО IP портала (и твой на приёмку). Минус — ключ и данные идут по «голому»
http; для мобильного трафика 5–6 прогонов/день терпимо, но поддомен+https безопаснее.
---
## 8. Проверить сервис живьём (смоук через прокси, без портала)
С рабочей машины (подставить настоящий `RENDER_KEY`):
```bash
curl -s https://render.liderra.ru/render \
-H "X-Render-Key: <RENDER_KEY>" -H "Content-Type: application/json" \
-d '{"url":"https://2gis.ru/krasnoyarsk/search/ломбард","type":"initialState"}' | head -c 400
```
Ждём непустой JSON (внутри `initialState`), не `{"data":""}`. Пусто → смотреть §11.
---
## 9. Подключить портал — СНАЧАЛА на dev-машине
В `app/.env` **локальной** машины (не боевой!) добавить:
```
SELF_RENDER_ENDPOINT=https://render.liderra.ru/render
SELF_RENDER_KEY=<тот же RENDER_KEY, что на виртуалке>
AUTOPODBOR_SELF_RENDER=true
```
🔴 После правки `.env`**перезапустить очередь**: остановить и заново `php artisan queue:work`
`artisan serve`, если крутится). Иначе воркер держит старые настройки в памяти
(память `feedback-queue-worker-restart-after-change`).
Гранулярно (по желанию): можно включить только 2ГИС (`AUTOPODBOR_SELF_RENDER_2GIS=true`) или только
Яндекс — но для приёмки включаем целиком.
---
## 10. Приёмка владельцем (ты смотришь глазами)
1. На dev-портале запустить **шаг 1** (поиск конкурентов) по реальному клиенту.
2. Запустить **шаг 2** (изучение) хотя бы по нескольким конкурентам.
3. Сверить руками с живым 2ГИС:
- конкурентов должно быть **много (~127 на тест-запросе), не 51** — признак, что канал реально собирает;
- **ноль мусора** (не «Бургер Кинг» вместо ломбарда — это была болезнь двойного кодирования, лечится double-tap);
- телефоны и сайты — верные, как в карточке 2ГИС.
4. Всё сошлось → флаг оставляем ВКЛ. Что-то не так → §11 и откат (§12).
---
## 11. Если что-то не так (быстрая диагностика)
| Симптом | Куда смотреть |
|---|---|
| `/health` даёт `proxies:0` | строка `PROXIES` в `.env` виртуалки (формат `http://user:pass@host:port`), перезапустить службу |
| `/render` возвращает `{"data":""}` | логи службы: `journalctl -u liderra-render -n 100`; проверить, что прокси жив (не кончились деньги/не сменился порт) |
| 401 unauthorized | `RENDER_KEY` на виртуалке ≠ `SELF_RENDER_KEY` на портале |
| мусор вместо фирм (Бургер Кинг) | болезнь двойного кодирования; в браузере уже стоит museum double-tap (`browser.mjs`), проверить что это поиск `/search/` |
| мало фирм / пусто у Яндекса | лента ленивая — проверить `scrolls`; смотреть `type:yandex-list` |
| портал будто не видит виртуалку | не перезапущен `queue:work` после правки `.env` |
Логи службы: `journalctl -u liderra-render -f`. Перезапуск: `sudo systemctl restart liderra-render`.
---
## 12. Откат (мгновенный)
- На портале в `.env`: `AUTOPODBOR_SELF_RENDER=false` → перезапустить `queue:work`. Автоподбор
сразу вернётся на xf4, как сейчас. Виртуалку можно оставить выключенной.
---
## 13. Позже (после успешной приёмки на dev)
- Те же три строки (`SELF_RENDER_ENDPOINT/KEY` + флаг) прописать в `app/.env` **боевого** портала —
это уже правка боевого, делаем **только с твоего разрешения** и через обычный деплой-процесс.
- На firewall виртуалки добавить IP боевого сервера на 443.
- По желанию — включить ротацию IP прокси (2–60 мин) в кабинете Proxy.Market (сейчас Disabled), для анти-блока на объёме.
---
## Приложение. Имена настроек (шпаргалка, чтобы не путать)
| Сторона | Переменная | Смысл |
|---|---|---|
| Виртуалка (`render-service/.env`) | `PORT` | порт сервиса (8790) |
| Виртуалка | `RENDER_KEY` | секрет; должен совпасть с `SELF_RENDER_KEY` портала |
| Виртуалка | `MAX_CONCURRENCY` | сколько вкладок разом (по памяти, 8 ок для 8 ГБ) |
| Виртуалка | `PROXIES` | пул прокси `http://user:pass@host:port` |
| Портал (`app/.env`) | `SELF_RENDER_ENDPOINT` | `https://render.liderra.ru/render` |
| Портал | `SELF_RENDER_KEY` | тот же секрет, что `RENDER_KEY` |
| Портал | `SELF_RENDER_CONCURRENCY` | не обязателен (дефолт 8) |
| Портал | `AUTOPODBOR_SELF_RENDER` | главный тумблер ВКЛ/ВЫКЛ |
| Портал | `AUTOPODBOR_SELF_RENDER_2GIS` / `_YANDEX` | гранулярно (по умолчанию = главному) |
+19 -6
View File
@@ -100,22 +100,35 @@ export async function render(url, type, proxy = null, scrolls = 6) {
}
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(2500);
// Пройти заглушку «обновите браузер» (ставит cookie-обход).
if (isGis) {
// Пройти заглушку «обновите браузер» (ставит cookie-обход) — кликаем, как только видна, без фикс-паузы.
try {
const skip = page.getByText(/перейти в 2ГИС|Пропустить/i).first();
if (await skip.isVisible({ timeout: 2000 })) await skip.click({ timeout: 3000 });
if (await skip.isVisible({ timeout: 1500 })) { await skip.click({ timeout: 3000 }); await page.waitForTimeout(400); }
} catch { /* заглушки нет — ок */ }
await page.waitForTimeout(3500);
// museum double-tap: для поиска return_url заглушки дважды закодирован (%20→«20») → повторный
// чистый заход по тому же URL (cookie-обход уже стоит) даёт корректный поиск. Для /firm/ не нужно.
if (isGisSearch) {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(6000);
}
// Умное ожидание вместо фикс-пауз: ждём появления НАСТОЯЩИХ данных в initialState
// (пагинация для поиска, профиль фирмы для карточки), а не заведомо-длинную паузу. §5.4.
await page.waitForFunction((search) => {
const s = window.initialState;
if (!s || !s.data) return false;
return search
? !!(s.data.search && s.data.search.pagination && Object.keys(s.data.search.pagination).length)
: !!(s.data.entity && s.data.entity.profile && Object.keys(s.data.entity.profile).length);
}, isGisSearch, { timeout: 20000, polling: 200 }).catch(() => { /* фолбэк: отдадим что есть */ });
} else {
await page.waitForTimeout(5000);
// Яндекс-карточка (html): ждём появления блоков карточки (телефоны/сайт), потом короткий добор —
// вместо глухих 5с. Если карточки нет (сайт/иное) — фолбэк по таймауту, не хуже прежнего.
await page.waitForFunction(
() => !!document.querySelector('[class*="orgpage-phones-view"], [class*="business-urls-view"], [class*="orgpage-header"]'),
null,
{ timeout: 5000, polling: 200 },
).catch(() => { /* нет карточки — отдадим что есть */ });
await page.waitForTimeout(1200);
}
if (type === 'initialState') {