Пакет · cpool

Политика

PoolPolicy — неизменяемый объект-значение. У каждой настройки есть умолчание; задавайте именованными аргументами только то, что меняете.

php
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 сокетов за пять миллисекунд, и столько же делал каждый параллельный запрос.

Дальше