Интеграция веб-сервера с базами данных: Nginx, PHP и PostgreSQL
Обзор связки Nginx, PHP и PostgreSQL
Как настроить Nginx для обработки PHP-скриптов, взаимодействующих с PostgreSQL?
Наиболее эффективное решение базируется на трёх компонентах: Nginx выступает в роли фронтального веб-сервера, PHP-FPM обрабатывает PHP-скрипты, а PostgreSQL обеспечивает хранение данных. Для взаимодействия PHP с PostgreSQL используются встроенное расширение pgsql или PDO_PGSQL. Ниже приведена базовая конфигурация.
Пример конфигурации Nginx (файл /etc/nginx/sites-available/default):
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
Nginx php mariadb (nginx, php и mariadb)
Параметр fastcgi_pass указывает на сокет PHP-FPM. Для PostgreSQL в PHP используется следующий код:
<?php
$conn = pg_connect("host=127.0.0.1 port=5432 dbname=mydb user=myuser password=mypass");
if (!$conn) {
error_log("Ошибка подключения к PostgreSQL: " . pg_last_error());
http_response_code(500);
exit(1);
}
$result = pg_query($conn, "SELECT now()");
$row = pg_fetch_row($result);
echo "Текущее время сервера: $row[0]";
pg_close($conn);
?>
Nginx php postgresql (nginx, php и postgresql)
Типичные проблемы:
- 502 Bad Gateway – PHP-FPM не запущен или не совпадает версия сокета. Решение: проверить статус systemctl status php8.1-fpm, перезапустить службу.
- pg_connect(): Unable to connect to PostgreSQL server – неверные параметры подключения или PostgreSQL не принимает соединения. Решение: проверить файл pg_hba.conf, разрешить подключения с localhost.
- Медленные запросы – отсутствие индексов в базе. Анализ с помощью EXPLAIN ANALYZE.
Вариант 1. Постоянные соединения с PostgreSQL (pg_pconnect)
Когда использовать постоянные соединения с PostgreSQL?
Функция pg_pconnect создаёт постоянное соединение, которое не закрывается после завершения скрипта. Это уменьшает накладные расходы на установку соединения при каждом HTTP-запросе. Целесообразно применять на сайтах с высокой нагрузкой, где время жизни PHP-FPM процессов регулируется параметрами pm.max_requests. Однако при использовании в пуле PHP-FPM может возникнуть исчерпание соединений, если pm.max_children превышает лимит max_connections в PostgreSQL.
<?php
// Пример использования постоянного соединения
$conn = pg_pconnect("host=127.0.0.1 dbname=mydb user=myuser password=mypass");
if (!$conn) {
error_log("Не удалось установить постоянное соединение");
exit;
}
// Работа с базой
$result = pg_query($conn, "SELECT COUNT(*) FROM users");
$count = pg_fetch_result($result, 0);
echo "Всего пользователей: $count";
// pg_close($conn) не вызывается, соединение будет переиспользовано
?>
Возможные ошибки:
- pg_pconnect(): Unable to connect to PostgreSQL server – аналогично pg_connect.
- Переполнение пула соединений – если pg_pconnect используется без ограничения, каждый PHP-FPM процесс создаёт новое постоянное соединение, что может превысить max_connections (по умолчанию 100). Решение: в PostgreSQL установить max_connections с запасом (например, 200), либо применять пулер соединений PgBouncer.
Вариант 2. Использование пула соединений PgBouncer
Как организовать пул соединений к PostgreSQL при высокой нагрузке?
PgBouncer – лёгкий менеджер пула соединений для PostgreSQL. Он поддерживает три режима: session, transaction и statement. Наиболее эффективен режим transaction, когда соединение возвращается в пул после завершения транзакции. Это позволяет обслуживать тысячи PHP-запросов малым числом реальных подключений к PostgreSQL.
# Конфигурация PgBouncer (pgbouncer.ini)
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 25
max_client_conn = 200
В PHP подключение осуществляется на порт 6432:
<?php
$conn = pg_connect("host=127.0.0.1 port=6432 dbname=mydb user=myuser password=mypass");
// далее стандартная работа
?>
Типичные ошибки:
- Ошибка аутентификации: auth failed – неверный пароль в userlist.txt или несовпадение метода хэширования. Решение: сгенерировать правильный MD5-хэш командой psql -c "SELECT concat('\"', usename, '\" \"', passwd, '\"') FROM pg_shadow WHERE usename='myuser'" mydb.
- Достигнут лимит max_client_conn – увеличить значение в конфигурации.
Вариант 3. Кеширование результатов запросов в Nginx
Как кешировать ответы PHP, содержащие данные из PostgreSQL, с помощью Nginx?
Nginx может кешировать ответы PHP-скриптов, если они помечены соответствующими заголовками. Для этого используется модуль proxy_cache (работает в паре с PHP-FPM, если PHP-FPM слушает TCP-сокет). Ответы, которые редко меняются (например, список городов или настройки), можно кешировать на стороне Nginx, снижая нагрузку на базу.
# В секции http nginx.conf
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=phpcache:10m max_size=1g inactive=60m use_temp_path=off;
# В серверном блоке
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_cache phpcache;
fastcgi_cache_valid 200 302 60m;
fastcgi_cache_key "$request_method$request_uri";
fastcgi_ignore_headers Cache-Control Expires;
# Для пропуска кеша по кукам или другим параметрам
fastcgi_no_cache $http_cookie;
}
PHP-скрипт должен отправлять заголовок Cache-Control: public для кешируемых ответов:
<?php
header("Cache-Control: public, max-age=3600");
$result = pg_query($conn, "SELECT id, name FROM regions");
while ($row = pg_fetch_assoc($result)) {
echo "{$row['id']}: {$row['name']}\n";
}
?>
Проблемы:
- Кеширование динамического контента – Nginx может отдавать устаревшие данные. Решение: использовать fastcgi_cache_use_stale для выдачи устаревшего кеша при ошибках, либо настроить инвалидацию через proxy_cache_purge.
Вариант 4. Балансировка нагрузки между несколькими серверами PHP-FPM (upstream)
Как распределить нагрузку между несколькими серверами PHP-FPM?
Если одного сервера PHP-FPM недостаточно, Nginx может балансировать запросы на несколько upstream-серверов. Каждый сервер может иметь свой пул PHP-FPM, работающий на отдельном сокете или порту.
# Определение upstream
upstream php_backend {
least_conn;
server unix:/run/php/php8.1-fpm1.sock weight=3;
server unix:/run/php/php8.1-fpm2.sock weight=2;
server 192.168.1.100:9000 max_fails=3 fail_timeout=30s;
}
server {
...
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass php_backend;
}
}
Балансировка с помощью upstream позволяет масштабировать обработку PHP-запросов горизонтально. Каждый PHP-FPM должен иметь собственное подключение к PostgreSQL (или использовать общий PgBouncer).
Ошибки:
- 502 Bad Gateway для части upstream – один из серверов недоступен. Решение: настроить fail_timeout и max_fails для своевременного исключения сбойного сервера.
Расширенные примеры конфигурации и кода
Пример 1. Настройка PgBouncer в режиме transaction pooling с детальной конфигурацией
Конфигурация PgBouncer с ограничением времени простоя и логированием:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb pool_size=50 reserve_pool=10
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
admin_users = admin
pool_mode = transaction
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 5.0
max_client_conn = 200
max_db_connections = 100
idle_transaction_timeout = 300
disable_pqexec = 0
Команда для генерации userlist.txt из PostgreSQL:
# Получение MD5-паролей
sudo -u postgres psql -c "SELECT concat('\"', usename, '\" \"', passwd, '\"') FROM pg_shadow WHERE usename NOT LIKE 'pg_%';" > /etc/pgbouncer/userlist.txt
Результат (userlist.txt):
"myuser" "md5abcdef1234567890" "app_user" "md5fedcba0987654321"
Пример 2. Кеширование в Nginx с использованием ключа на основе URI и переменной
Расширенная конфигурация с исключением кеша для административной панели:
http {
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=fastcache:10m max_size=1g inactive=60m use_temp_path=off;
server {
location /api/ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_cache fastcache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 302 1h;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
# Не кешировать для авторизованных пользователей
if ($http_cookie ~* "PHPSESSID") {
set $no_cache 1;
}
fastcgi_no_cache $no_cache;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
add_header X-Nginx-Cache $upstream_cache_status;
}
}
}
Проверка кеша - ответ содержит заголовок X-Nginx-Cache: HIT или MISS.
Пример 3. Использование PDO с PostgreSQL и обработка ошибок
<?php
$dsn = 'pgsql:host=127.0.0.1;port=5432;dbname=mydb;user=myuser;password=mypass';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, null, null, $options);
$stmt = $pdo->prepare("SELECT id, username, email FROM users WHERE active = :active");
$stmt->execute([':active' => true]);
echo "<table>";
while ($row = $stmt->fetch()) {
echo "<tr><td>{$row['id']}</td><td>{$row['username']}</td><td>{$row['email']}</td></tr>";
}
echo "</table>";
} catch (PDOException $e) {
error_log("PDO Error: " . $e->getMessage());
http_response_code(500);
echo "Внутренняя ошибка сервера";
}
?>
Результат - HTML-таблица с данными из PostgreSQL. Если возникает ошибка подключения, в лог записывается детальное сообщение, а пользователю возвращается 500.
Пример 4. Тонкая настройка пула PHP-FPM для работы с PostgreSQL
Оптимальные значения параметров в файле /etc/php/8.1/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.process_idle_timeout = 10s
pm.max_requests = 500
Пояснение параметров:
- pm.max_children - максимальное количество дочерних процессов PHP-FPM. Должно быть меньше либо равно max_connections в PostgreSQL (с учётом других потребителей).
- pm.max_requests - число запросов, после которого процесс завершается, чтобы избежать утечек памяти.
- Если используется постоянные соединения pg_pconnect, то pm.max_children не должно превышать max_connections, иначе часть процессов не сможет подключиться.
Пример 5. Использование несколько upstream для PHP-FPM с разными сокетами
upstream php_pool {
least_conn;
server unix:/run/php/php8.1-fpm.sock;
server unix:/run/php/php8.2-fpm.sock;
}
server {
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass php_pool;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Результат - запросы равномерно распределяются между двумя пулами PHP разных версий. Однако приложения должны быть совместимы.