🧙 Садовник: уточнил шаги, добавил проверки и обоснования. Примите, если полезно.

#1
open+265proposed by devops · Aug 16, 2026 · based on v2
Proposed changes · v2 → suggestion
+744
1¶ # Исходная
2
3 Реальная ситуация с боевого сервера:
4
5 - запросы к базе — от 3 до 8 секунд;
6 - PostgreSQL занимает 20 МБ из 3.8 ГБ доступной памяти;
7 - `shared_buffers` — 128 МБ, то есть значение по умолчанию;
8 - кэш есть, но с коротким временем жизни;
9 - пула подключений нет.
10
11 Составленный тогда план выглядел так: поднять время жизни кэша, потом настройки PostgreSQL, потом пул подключений. Обещанный итог — улучшение на 99%.
12
13 План рабочий, но в нём пропущен первый шаг и есть одна правка, которая не работает вообще. Разберём по порядку.
141## Сначала измерить
1521. Найти, какой именно запрос медленный
163 Перед любой правкой надо знать конкретный запрос. Включи лог медленных запросов:
174
185 ```sql
196 -- логировать всё, что дольше секунды
207 ALTER SYSTEM SET log_min_duration_statement = 1000;
218 SELECT pg_reload_conf();
229 ```
2310
2411 Либо ставь расширение `pg_stat_statements` и смотри топ по суммарному времени:
2512
2613 ```sql
2714 SELECT calls, mean_exec_time, total_exec_time, query
2815 FROM pg_stat_statements
2916 ORDER BY total_exec_time DESC
3017 LIMIT 10;
3118 ```
3219
3320 Колонка `total_exec_time` важнее, чем `mean_exec_time`: запрос на 50 мс, выполняемый десять тысяч раз, съедает больше, чем один на четыре секунды.
3421 $ SELECT calls, mean_exec_time, query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
3522 why: Оптимизация без измерения — это угадывание. За общим «база медленная» обычно стоит один-два конкретных запроса, а остальное работает нормально.
3623 - [ ] Известен конкретный текст медленного запроса
3724 - [ ] Ты смотрел на суммарное время, а не только на среднее
3825 → pg_stat_statements — статистика по запросам — https://www.postgresql.org/docs/current/pgstatstatements.html
3926 → Чеклист по медленным запросам — https://wiki.postgresql.org/wiki/Slow_Query_Questions
40272. Посмотреть план выполнения
4128 ```sql
4229 EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
4330 ```
4431
4532 Что искать в выводе:
4633
4734 | Строка в плане | Что значит |
4835 |---|---|
4936 | `Seq Scan` по большой таблице | нет нужного индекса |
5037 | `rows=1` в плане, `actual rows=50000` | статистика устарела, нужен `ANALYZE` |
5138 | тот же узел с `loops=200` | классическая проблема N+1 |
5239 | `Sort Method: external merge Disk` | не хватает `work_mem` |
5340
5441 Вывод удобно разбирать в визуализаторе — ссылка ниже.
5542 $ EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
5643 why: Этого шага в исходном плане не было вообще — и это главная его слабость. Запрос на четыре секунды при небольшом объёме данных почти никогда не лечится памятью. Обычно это отсутствующий индекс или N+1 — и тогда одна строка даёт выигрыш в сотни раз, а не на 20%.
5744 - [ ] Ты знаешь, есть ли Seq Scan по большой таблице
5845 - [ ] Проверил, не N+1 ли это
5946 → Как читать EXPLAIN — https://www.postgresql.org/docs/current/using-explain.html
6047 → Визуализатор планов запросов — https://explain.dalibo.com/
6148 → Индексы в PostgreSQL — https://www.postgresql.org/docs/current/indexes.html
62¶ ## Почему порядок важен
63
64 Исходный план ранжировал работы по соотношению эффекта к затраченному времени — это правильный подход:
65
66 | Шаг | Время | Заявленный эффект |
67 |---|---|---|
68 | Поднять время жизни кэша | 5 мин | минус 99% нагрузки на базу |
69 | Настройки PostgreSQL | 15 мин | 20–30% |
70 | Пул подключений | 30 мин | 10–20% |
71
72 Но с двумя оговорками.
73
74 **Первая.** «Минус 99% нагрузки» верно только для повторных чтений редко меняющихся данных: настройки сайта, контакты, баннеры. На корзине, остатках товара или личном кабинете такой цифры не будет никогда. Кэш убирает повторные чтения, а не медленный запрос.
75
76 **Вторая, важнее.** Первый запрос после истечения кэша всё равно идёт четыре секунды. Кэш не починил базу — он **спрятал проблему** и отложил её до дня, когда кэш упадёт, и все запросы разом уйдут в медленную базу.
77
78 Поэтому правильный порядок: **сначала EXPLAIN и индексы, потом кэш.** Кэш ставят поверх быстрого запроса, а не вместо него.
7949## Правки
80503. Настройки PostgreSQL — и главная ловушка
8151 Значения по умолчанию не зависят от объёма памяти машины: `shared_buffers` всегда 128 МБ, `work_mem` — 4 МБ. На сервере с 4 ГБ это мало.
8252
8353 **Вот как НЕ работает** — именно так было написано в исходном плане:
8454
8555 ```yaml
8656 environment:
8757 POSTGRES_SHARED_BUFFERS: 1GB # такой переменной не существует
8858 POSTGRES_WORK_MEM: 16MB # и такой тоже
8959 ```
9060
9161 Официальный образ postgres знает только `POSTGRES_PASSWORD`, `POSTGRES_USER`, `POSTGRES_DB`, `POSTGRES_INITDB_ARGS`, `POSTGRES_HOST_AUTH_METHOD` и `PGDATA`. Всё остальное он просто игнорирует — **без единого предупреждения**.
9262
9363 **Вот как работает:**
9464
9565 ```yaml
9666 postgres:
9767 image: postgres:16-alpine
9868 command: >
9969 postgres
10070 -c shared_buffers=1GB
10171 -c effective_cache_size=3GB
10272 -c work_mem=16MB
10373 -c maintenance_work_mem=256MB
10474 ```
10575
10676 Отправная точка для расчёта: `shared_buffers` около 25% от памяти, `effective_cache_size` — 50–75%.
10777 $ docker exec -it <контейнер> psql -U postgres -c "SHOW shared_buffers;"
10878 why: Это самый опасный вид ошибки: правка внесена, контейнер поднялся, ошибок нет — и ничего не изменилось. Задача закрыта, галочка стоит, проблема на месте.
10979 - [ ] Настройки заданы через command с -c или файлом конфига
11080 - [ ] SHOW shared_buffers показывает новое значение, а не 128MB
11181 → Какие переменные понимает образ postgres — https://hub.docker.com/_/postgres
11282 → Настройки памяти PostgreSQL — https://www.postgresql.org/docs/current/runtime-config-resource.html
113834. Кэш: поднять время жизни и сразу же продумать сброс
11484 Для редко меняющихся данных время жизни в 10 минут действительно мало:
11585
11686 ```python
11787 # было везде 600 секунд
11888 SEO_TTL = 3600 # час
11989 ANALYTICS_TTL = 7200 # два часа
12090 CONTACTS_TTL = 1800 # полчаса
12191 ```
12292
12393 **Но вместе с этим обязательно сброс при записи:**
12494
12595 ```python
12696 async def update_contacts(self, data):
12797 result = await self.repo.update(data)
12898 await self.cache.delete("site_content:contacts") # без этой строки
12999 return result # админ будет
130100 # в ярости
131101 ```
132102
133103 Без сброса время жизни в два часа означает буквально следующее: администратор меняет телефон на сайте, обновляет страницу и видит старый. Два часа подряд.
134104
135105 Проверить, что кэш работает:
136106
137107 ```bash
138108 docker exec -it <redis> redis-cli
139109 > KEYS site_content:*
140110 > TTL site_content:contacts
141111 ```
142112 $ docker exec -it <redis> redis-cli TTL site_content:contacts
143113 why: Кэш — это размен: скорость в обмен на актуальность. Чем больше время жизни, тем больше платишь. Поднять время жизни без сброса при записи — значит перенести жалобы с медленного сайта на «я всё поменял, а ничего не поменялось».
144114 - [ ] Время жизни поднято только там, где данные редко меняются
145115 - [ ] Каждый метод записи сбрасывает свой ключ
146116 - [ ] Проверено вручную: поменял через админку — сразу видно на сайте
1471175. Пул подключений — когда он действительно нужен [optional]
148118 Каждое подключение к PostgreSQL — это отдельный процесс на сервере базы. Сотня подключений — сотня процессов.
149119
150120 Но сначала проверь, есть ли проблема:
151121
152122 ```sql
153123 SELECT count(*), state FROM pg_stat_activity GROUP BY state;
154124 SHOW max_connections;
155125 ```
156126
157127 Если подключений десяток из ста возможных — пул не нужен, это не узкое место.
158128
159129 Если нужен — есть два уровня:
160130
161131 1. **Пул в самом приложении.** У SQLAlchemy он включён по умолчанию — проверь `pool_size` и `max_overflow` прежде, чем ставить что-то ещё.
162132 2. **Внешний пул (PgBouncer).** Нужен, когда к базе ходят несколько разных приложений или много копий одного.
163133
164134 У PgBouncer есть важное ограничение: в режиме `transaction` не работают подготовленные выражения и сеансовые настройки. Для asyncpg это значит, что нужно явно отключать кэш выражений, иначе получишь плавающие ошибки под нагрузкой.
165135 $ SELECT count(*), state FROM pg_stat_activity GROUP BY state;
166136 why: Пул — самый дорогой из трёх шагов и самый слабый по эффекту. Ставить его первым — верный способ потратить день и получить новый источник странных ошибок.
167137 - [ ] Проверено, что подключения действительно заканчиваются
168138 - [ ] Сначала посмотрели пул самого приложения
169139 → PgBouncer — документация — https://www.pgbouncer.org/
170## Правильный порядок целиком
171
172 1. **Измерить.** Какой запрос, сколько раз, сколько времени суммарно.
173 2. **EXPLAIN ANALYZE.** Индекс, N+1 или устаревшая статистика самый дешёвый и самый большой выигрыш.
174 3. **Настройки базы.** Через `command: postgres -c ...`, с проверкой через `SHOW`.
175 4. **Кэш.** Поверх уже быстрого запроса, со сбросом при записи.
176 5. **Пул подключений.** Только если измерения показали, что он нужен.
177
178 **После каждого шага — измерить снова.** Иначе через неделю ты не сможешь сказать, какая из пяти правок сработала, а какая просто добавила сложности.
179
180 И отдельно про цифры в планах. Когда видишь «улучшение на 99.9%» — спроси, на каком сценарии. Обычно это сто одинаковых запросов к одному эндпоинту подряд — то есть идеальный для кэша и не очень похожий на живой трафик.
181
182
183
140+## Проверка
141+6. Проверить актуальные версии инструментов
142+ Убедитесь, что используете актуальные версии PostgreSQL и Redis, чтобы избежать проблем с совместимостью и использовать последние улучшения производительности.
143+ why: Актуальные версии содержат исправления и улучшения, которые могут значительно повлиять на производительность.
144+7. Проверить настройки конфигурации
145+ Убедитесь, что все изменения в конфигурации сохранены и применены. Проверьте, что настройки, такие как `shared_buffers` и `work_mem`, установлены правильно.
146+ why: Неправильные настройки могут привести к неэффективному использованию ресурсов и медленной работе базы данных.
Review