Политика
PoolPolicy — неизменяемый объект-значение. У каждой настройки есть умолчание;
задавайте именованными аргументами только то, что меняете.
use Flytachi\Winter\CPool\PoolPolicy;
new PoolPolicy(maximumPoolSize: 20, maxLifetime: 600.0);
PoolPolicy::default(); // все значения по умолчаниюЁмкость
| Параметр | По умолчанию | Что делает |
|---|---|---|
maximumPoolSize |
10 |
Жёсткий потолок числа открытых соединений |
connectionTimeout |
15.0 |
Дедлайн всей выдачи |
Это одно решение, а не два. Потолок превращает «слишком много соединений» из отказа на стороне базы в очередь на стороне приложения; таймаут решает, насколько длинной этой очереди позволено вырасти, прежде чем запрос сдастся.
connectionTimeout ограничивает выдачу целиком, а не каждое ожидание внутри неё.
Ожидание свободного соединения, выбрасывание мёртвых и открытие замены идут из одного
бюджета, и когда он исчерпан, выдача падает: unusable() — если время ушло на мёртвые
соединения, exhausted() — если все были просто заняты.
Как прикидывать размер: потолок — свойство базы, делённое на число воркеров,
которые с ней говорят. Четыре воркера Swoole против PostgreSQL с max_connections = 100, с запасом на миграции и psql, — это скорее maximumPoolSize: 15, чем 100.
Пул на потолке — не всегда мал
Прежде чем поднимать лимит, поищите borrow() без парного release(): каждая такая
утечка навсегда уменьшает рабочий размер пула на единицу, и симптом выглядит
одинаково.
Срок жизни
| Параметр | По умолчанию | Что делает |
|---|---|---|
maxLifetime |
1800.0 |
Соединение старше этого выселяется при выдаче |
maxLifetimeJitter |
0.1 |
Доля maxLifetime, на которую разбрасывается срок |
Зачем вообще выселять исправное соединение: прокси, балансировщики и failover-адреса переезжают под долгоживущими сокетами. Соединение, открытое час назад, может исправно отвечать, указывая при этом на сервер, который выводят из ротации. Переоткрытие — способ следовать за инфраструктурой, которой пул не видит.
Джиттер здесь не украшение. Десять соединений, созданных при старте с одинаковым сроком, истекут в одну секунду — и приложение встанет, пока все десять переподключаются. Десять процентов от получаса растягивают это на три минуты.
Проверка живости
| Параметр | По умолчанию | Что делает |
|---|---|---|
aliveBypassWindow |
0.5 |
Пропустить проверку для соединения, использованного недавно |
Поставьте 0.0 — проверка пойдёт на каждой выдаче: корректно и удваивает round-trip’ы
нагруженного сервиса. Увеличьте — сломанный сокет проживёт чуть дольше до обнаружения.
Умолчание исходит из того, что соединение, ответившее полсекунды назад, живо; если база
упала именно в этом окне, запрос упадёт, и вызывающий выселит соединение сам.
Фоновая уборка
| Параметр | По умолчанию | Что делает |
|---|---|---|
housekeepingInterval |
30.0 |
Как часто проходит подметание |
keepaliveTime |
120.0 |
Пинговать соединения, простоявшие дольше (0 — выключено) |
idleTimeout |
600.0 |
Закрывать простоявшие дольше (0 — выключено) |
minimumIdle |
0 |
Не опускаться ниже этого числа |
Уборка включена по умолчанию, и причин тому две — это одна и та же причина с двух
сторон. Пул, который никто не подметает, держит свои сокеты вечно: maxLifetime
проверяется при выдаче, а простаивающее приложение ничего не выдаёт, и пул, выросший на
всплеске в полночь, к утру всё ещё держит каждый сокет. А когда сервер или файрвол за
это время их рвут, узнаёт об этом первый же вернувшийся запрос — ему и разбирать трупы,
и переоткрывать. keepaliveTime не даёт им умирать, idleTimeout возвращает их базе, и
ни за то ни за другое запрос не платит.
Таймер заводится первой выдачей, а не созданием пула, и только под Swoole. Приложение, которое ни разу не открыло соединение, по-прежнему не стоит ничего. То, которое открыло, платит тик раз в тридцать секунд на пул на воркер плюс пинг на каждое простаивающее соединение раз в две минуты.
Пул после первой выдачи держит таймер
Живой повторяющийся таймер не даёт реактору Swoole завершиться, поэтому пул обязательно
закрывать: под фреймворком это уже сделано (workerExit закрывает пулы), а в своём
скрипте или тесте вызывайте close() сами — иначе Swoole\Coroutine\run() не вернётся.
Проход пингует соединения по одному и возвращает каждое в канал прежде, чем взять следующее, — заёмщики всегда находят остальные на месте, вне канала только то, что сейчас проверяется. Один проход стоит round-trip на каждое соединение, которому пора: пять соединений против сервера с задержкой 50 мс — это ~250 мс фонового времени и ноль времени любого запроса.
keepaliveTime выключают (0.0), когда соединения дёшевы, а сервер рядом: пинг раз в
две минуты не бесплатен, если пул большой, а канал — нет. idleTimeout выключают,
когда пул должен оставаться тёплым между всплесками, — и тогда же задают minimumIdle,
чтобы следующий всплеск не начинался с нуля.
Правило порядка
keepaliveTime < idleTimeout < maxLifetime < то, что рвёт соединения выше по стеку
Все три — сроки, отмеренные одному и тому же соединению, и каждый имеет смысл, только пока соединение ещё есть, чтобы его получить:
keepaliveTime ≥ maxLifetime— соединение переоткроют раньше, чем до него дойдёт первый пинг. Пингов не будет вовсе.keepaliveTime ≥ idleTimeout(приminimumIdle: 0) — соединение закроют раньше первого пинга. Итог тот же. С тёплым полом это нормально: соединения на полу переживаютidleTimeout, и работа для keepalive остаётся.maxLifetime ≥ собственного таймаута сервера— побеждает ровно то, от чего всё это защищает:idle_session_timeoutу Postgres,timeoutу Redis, NAT, забывший поток. Пул обязан переоткрывать раньше, чем они рвут, и эту границу он знать не может.
Первые две пул применяет сам: недостижимый keepaliveTime обнуляется при создании
политики, так что PoolPolicy::$keepaliveTime всегда читается как то, что реально будет
делать уборщик. Настройка, которая молча ничего не делает, хуже честно выключенной:
оператор уверен, что простаивающие соединения пингуются, а они тихо умирают. HikariCP
поступает так же, отключая keepaliveTime, дотянувшийся до maxLifetime.
Для ориентира: умолчания самого HikariCP лежат внутри этого правила — keepaliveTime
2 минуты, idleTimeout 10 минут, maxLifetime 30 минут.
Открытие после отказа
Это не настройка — пул решает сам. Когда открыть соединение не удалось или оно открылось и не смогло ответить, открытие запирается на 10 мс; каждый следующий отказ подряд удваивает паузу до 5 секунд, а первое открывшееся и ответившее соединение снимает её.
Не вынесено в настройку намеренно: у «долбить падающий сервер сильнее» нет полезного значения. Числа — хикарины, их фоновый создатель соединений душит отказы точно так же. Зачем это нужно, видно на сервере, который принимает сокеты, но не обслуживает их, — база ещё поднимается, кэш ещё грузит дамп: без паузы одна выдача успевала открыть 10 038 сокетов за пять миллисекунд, и столько же делал каждый параллельный запрос.
Дальше
- Справочник API — где каждая настройка применяется
- Свой адаптер —
validate()под окном пропуска