Интеграция веб-сервера с базами данных: Nginx, PHP и PostgreSQL

Раздел: Администрирование веб-серверов -> Интеграция Nginx с базами данных

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

Nginx, PHP и PostgreSQL - comments

En
Nginx php postgresql (php)