Перейти до вмісту

Оптимізація сервера для роботи з MediaWiki

Матеріал з K2 ERP Wiki Ukraine — База знань з автоматизації та санкцій в Україні

Мета статті: зменшити навантаження на сервер MediaWiki, стабілізувати роботу php8.4-fpm, обмежити агресивних ботів за швидкістю, але не закривати їм доступ повністю.

1. Типова схема роботи

У розглянутій конфігурації сайт працює приблизно так:

Користувач / бот
        ↓
Frontend Nginx
        ↓
Backend Nginx
        ↓
php8.4-fpm
        ↓
MediaWiki
        ↓
MySQL / MariaDB

Важливо: якщо перед MediaWiki стоїть ще один Nginx, backend-сервер повинен бачити реальні IP користувачів, а не тільки IP frontend-проксі.

2. Діагностика навантаження

2.1 Перевірка процесів

htop

Або:

ps aux | grep php-fpm | sort -k3 -nr | head -20

Якщо основне навантаження створюють процеси:

php-fpm: pool www

це означає, що навантаження йде через PHP / MediaWiki.

2.2 Топ IP за кількістю запитів

sudo tail -n 50000 /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -30

2.3 Топ User-Agent

sudo tail -n 50000 /var/log/nginx/access.log | awk -F\" '{print $6}' | sort | uniq -c | sort -nr | head -30

2.4 Топ URL

sudo tail -n 50000 /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -50

Особливо важкі запити для MediaWiki:

action=history
action=edit
action=raw
diff=
oldid=
Special:
api.php
index.php?title=

3. Коректне визначення реального IP через два Nginx

3.1 Frontend Nginx

На frontend Nginx у блоці, який прокидує запити на backend, повинно бути:

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Приклад:

location / {
    proxy_pass http://BACKEND_IP;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_connect_timeout 60s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;
    send_timeout 300s;
}

3.2 Backend Nginx

На backend Nginx у файлі:

sudo nano /etc/nginx/nginx.conf

у секції http { ... } потрібно додати:

set_real_ip_from 192.168.20.225;
real_ip_header X-Real-IP;
real_ip_recursive on;

де 192.168.20.225 — IP frontend Nginx.

Не можна робити так:

set_real_ip_from 0.0.0.0/0;

Це небезпечно, бо клієнти зможуть підробляти IP через HTTP-заголовки.

3.3 Перевірка real IP

У http { ... } можна тимчасово додати:

log_format realip_debug '$host client=$remote_addr proxy=$realip_remote_addr '
                        'x_real_ip="$http_x_real_ip" '
                        'xff="$http_x_forwarded_for" '
                        '"$request" $status "$http_user_agent"';

У потрібному server { ... }:

access_log /var/log/nginx/wiki_realip_debug.log realip_debug;

Перевірка:

sudo nginx -t
sudo systemctl reload nginx
sudo tail -f /var/log/nginx/wiki_realip_debug.log

Правильний результат:

client=92.222.104.195 proxy=192.168.20.225

Неправильний результат:

client=192.168.20.225

Якщо backend Nginx бачить тільки IP frontend-проксі, то limit_req буде рахувати всіх користувачів і ботів як одного клієнта. Це може викликати 429, затримки або навіть 504.

4. Обмеження швидкості ботів без повного блокування

Завдання — не забороняти ботам доступ через 403, а зменшити швидкість їхніх запитів.

Також важливо, щоб обмеження не зачіпали POST, бо збереження сторінок MediaWiki виконується саме через POST.

4.1 Повний блок для http { }

Цей блок потрібно вставити у:

sudo nano /etc/nginx/nginx.conf

всередину секції http { ... }, бажано до рядків:

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
##
# Real client IP from frontend nginx
##

set_real_ip_from 192.168.20.225;
real_ip_header X-Real-IP;
real_ip_recursive on;


##
# Bot detection
##

map $http_user_agent $is_heavy_bot {
    default 0;

    ~*GPTBot 1;
    ~*ChatGPT-User 1;
    ~*ClaudeBot 1;
    ~*anthropic-ai 1;
    ~*PerplexityBot 1;
    ~*Bytespider 1;
    ~*Amazonbot 1;
    ~*CCBot 1;
    ~*MJ12bot 1;
    ~*AhrefsBot 1;
    ~*SemrushBot 1;
    ~*DotBot 1;
    ~*BLEXBot 1;
    ~*PetalBot 1;
    ~*YandexBot 1;
    ~*Baiduspider 1;
    ~*meta-webindexer 1;
    ~*facebookexternalhit 1;
    ~*Applebot 1;
}


##
# Limit only bot GET/HEAD requests.
# POST is not limited, so MediaWiki save actions are not affected.
##

map "$is_heavy_bot:$request_method" $bot_limit_key {
    default "";
    "1:GET"  $binary_remote_addr;
    "1:HEAD" $binary_remote_addr;
}


##
# Heavy MediaWiki query detection
##

map $query_string $mw_heavy_query {
    default 0;

    ~*(^|&)(action=edit|action=history|action=raw|action=info|action=purge) 1;
    ~*(^|&)(diff=|oldid=|curid=) 1;
    ~*(^|&)(hidebots=|limit=|from=|target=|namespace=|offset=|dir=) 1;
}


##
# Heavy MediaWiki URI detection
##

map $request_uri $mw_heavy_uri {
    default 0;

    ~*Special: 1;
    ~*/Special: 1;
    ~*/wiki/Special: 1;
}


##
# Limit only bots + heavy GET/HEAD requests
##

map "$is_heavy_bot:$mw_heavy_query:$mw_heavy_uri:$request_method" $bot_heavy_key {
    default "";

    ~^1:1:0:GET$  $binary_remote_addr;
    ~^1:1:0:HEAD$ $binary_remote_addr;
    ~^1:0:1:GET$  $binary_remote_addr;
    ~^1:0:1:HEAD$ $binary_remote_addr;
    ~^1:1:1:GET$  $binary_remote_addr;
    ~^1:1:1:HEAD$ $binary_remote_addr;
}


##
# Rate limit zones
##

limit_req_zone $bot_limit_key zone=bot_slow:20m rate=1r/s;
limit_req_zone $bot_heavy_key zone=bot_heavy_slow:20m rate=10r/m;
limit_conn_zone $bot_limit_key zone=bot_conn:20m;

Перевага цього блоку: обмежуються тільки боти, тільки GET/HEAD-запити. POST-запити, тобто збереження сторінок, не повинні потрапляти під цей rate limit.

5. Налаштування server-блоку MediaWiki

У конфігу сайту, наприклад:

sudo nano /etc/nginx/sites-available/wiki.erp.kyiv.ua

всередині server { ... } додати:

limit_req_status 429;
limit_conn bot_conn 5;

error_page 429 = @rate_limited;

location @rate_limited {
    add_header Retry-After 30 always;
    return 429 "Too many requests. Please slow down.\n";
}

У PHP location додати:

limit_req zone=bot_slow burst=20 nodelay;
limit_req zone=bot_heavy_slow burst=5 nodelay;

Повний приклад PHP location:

location ~ \.php$ {
    limit_req zone=bot_slow burst=20 nodelay;
    limit_req zone=bot_heavy_slow burst=5 nodelay;

    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    fastcgi_connect_timeout 60s;
    fastcgi_send_timeout 300s;
    fastcgi_read_timeout 300s;
}

Увага: якщо PHP-FPM у вас слухає TCP, наприклад 127.0.0.1:9000, не змінюйте fastcgi_pass на socket. Залишайте свій робочий варіант.

6. Чому потрібен nodelay

Без nodelay:

limit_req zone=bot_slow burst=20;

Nginx може затримувати зайві запити в черзі.

З nodelay:

limit_req zone=bot_slow burst=20 nodelay;

дозволені запити проходять одразу, а зайві швидше отримують:

429 Too Many Requests

Рекомендація: для захисту живих користувачів краще використовувати nodelay, щоб Nginx не накопичував довгу чергу запитів.

7. Перевірка Nginx

Після змін:

sudo nginx -t

Якщо все добре:

sudo systemctl reload nginx

Пошук старих або неповних правил:

sudo grep -R "bot_heavy_key\|mw_heavy_uri\|bot_limit_key\|is_heavy_bot\|limit_req" /etc/nginx/

Типові помилки

Помилка:

unknown "bot_heavy_key" variable

Причина: у location використовується limit_req zone=bot_heavy_slow, але в http { } не оголошено $bot_heavy_key.

Помилка:

unknown "mw_heavy_uri" variable

Причина: у конфігу використовується $mw_heavy_uri, але не оголошено відповідний map.

8. Таймаути для уникнення 504

504 Gateway Timeout означає, що Nginx не дочекався відповіді від upstream.

8.1 Frontend Nginx

У location з proxy_pass:

proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;

8.2 Backend Nginx

У PHP location:

fastcgi_connect_timeout 60s;
fastcgi_send_timeout 300s;
fastcgi_read_timeout 300s;

Важливо: збільшення timeout не лікує причину повільної роботи, але дає довгим операціям, наприклад збереженню великої сторінки, завершитися без 504.

9. Налаштування php8.4-fpm

Файл pool-конфігурації:

sudo nano /etc/php/8.4/fpm/pool.d/www.conf

Рекомендований стартовий варіант:

pm = dynamic
pm.max_children = 60
pm.start_servers = 10
pm.min_spare_servers = 10
pm.max_spare_servers = 20
pm.max_requests = 500

request_terminate_timeout = 300s
request_slowlog_timeout = 5s
slowlog = /var/log/php8.4-fpm-slow.log

Після зміни:

sudo systemctl restart php8.4-fpm

9.1 Перевірка нестачі воркерів

sudo journalctl -u php8.4-fpm | grep -i "max_children" | tail -20

Якщо є:

server reached pm.max_children setting

потрібно збільшити pm.max_children.

Наприклад:

pm = dynamic
pm.max_children = 80
pm.start_servers = 12
pm.min_spare_servers = 12
pm.max_spare_servers = 30
pm.max_requests = 500

9.2 Як порахувати pm.max_children

Перевірити середній розмір PHP-FPM процесу:

ps aux | grep "php-fpm: pool" | awk '{sum+=$6; n++} END {if (n>0) print "avg:", sum/n/1024, "MB", "count:", n}'

Формула:

pm.max_children = RAM, доступна для PHP-FPM / середній розмір одного PHP-FPM процесу

Приклад:

12000 MB / 180 MB = 66

Тоді можна поставити:

pm.max_children = 60

Не варто віддавати всю RAM під PHP-FPM. Потрібен запас для MySQL/MariaDB, Nginx, системи, файлового кешу та службових задач MediaWiki.

10. PHP-FPM slowlog

У файлі:

sudo nano /etc/php/8.4/fpm/pool.d/www.conf

додати або перевірити:

request_slowlog_timeout = 5s
slowlog = /var/log/php8.4-fpm-slow.log

Перезапуск:

sudo systemctl restart php8.4-fpm

Перегляд:

sudo tail -f /var/log/php8.4-fpm-slow.log

Slowlog допоможе знайти реальну причину повільної роботи. Часто там видно Parser, LinksUpdate, SpecialPage, SyntaxHighlight, api.php, RecentChange або JobQueue.

11. OPcache для PHP 8.4

Файл може бути:

sudo nano /etc/php/8.4/fpm/conf.d/10-opcache.ini

або:

sudo nano /etc/php/8.4/fpm/php.ini

Рекомендовані параметри:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.save_comments=1

Перезапуск:

sudo systemctl restart php8.4-fpm

Перевірка:

php -i | grep -i opcache

12. APCu для кешу MediaWiki

Встановлення:

sudo apt install php8.4-apcu

Перевірка:

php -m | grep -i apcu

Конфігурація:

sudo nano /etc/php/8.4/fpm/conf.d/20-apcu.ini
apc.enabled=1
apc.shm_size=256M
apc.ttl=3600
apc.user_ttl=3600
apc.gc_ttl=3600

Перезапуск:

sudo systemctl restart php8.4-fpm

У LocalSettings.php:

$wgMainCacheType = CACHE_ACCEL;
$wgSessionCacheType = CACHE_ACCEL;
$wgMessageCacheType = CACHE_ACCEL;
$wgParserCacheType = CACHE_ACCEL;

13. Винесення MediaWiki Job Queue з веб-запитів

У LocalSettings.php:

$wgJobRunRate = 0;

Додати cron від користувача www-data:

sudo crontab -u www-data -e
* * * * * /usr/bin/php /var/www/wiki.erp.kyiv.ua/maintenance/runJobs.php --maxjobs 100 --maxtime 50 > /dev/null 2>&1

Це одна з найважливіших оптимізацій для збереження сторінок. MediaWiki jobs не повинні виконуватись у звичайному веб-запиті користувача.

14. File cache MediaWiki

У LocalSettings.php:

$wgUseFileCache = true;
$wgFileCacheDirectory = "$IP/cache";
$wgShowIPinHeader = false;

Створити каталог:

sudo mkdir -p /var/www/wiki.erp.kyiv.ua/cache
sudo chown -R www-data:www-data /var/www/wiki.erp.kyiv.ua/cache

15. FastCGI cache в Nginx для анонімних GET

Увага: FastCGI cache потрібно впроваджувати обережно, щоб не кешувати сторінки авторизованих користувачів.

У http { ... }:

fastcgi_cache_path /var/cache/nginx/mediawiki levels=1:2 keys_zone=MEDIAWIKI:200m inactive=60m max_size=5g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

У PHP location:

set $skip_cache 0;

if ($request_method != GET) {
    set $skip_cache 1;
}

if ($query_string != "") {
    set $skip_cache 1;
}

if ($http_cookie ~* "UserID|Token|session|mediawiki") {
    set $skip_cache 1;
}

location ~ \.php$ {
    limit_req zone=bot_slow burst=20 nodelay;
    limit_req zone=bot_heavy_slow burst=5 nodelay;

    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    fastcgi_connect_timeout 60s;
    fastcgi_send_timeout 300s;
    fastcgi_read_timeout 300s;

    fastcgi_cache MEDIAWIKI;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache_valid 200 301 302 10m;
    fastcgi_cache_valid 404 1m;

    add_header X-FastCGI-Cache $upstream_cache_status always;
}

Створити каталог:

sudo mkdir -p /var/cache/nginx/mediawiki
sudo chown -R www-data:www-data /var/cache/nginx/mediawiki

Перевірка:

sudo nginx -t
sudo systemctl reload nginx
curl -I https://wiki.example.com/wiki/Main_Page

Очікуваний заголовок:

X-FastCGI-Cache: HIT

16. robots.txt без повного блокування

Файл:

sudo nano /var/www/wiki.erp.kyiv.ua/robots.txt

Приклад:

User-agent: *
Crawl-delay: 10

Disallow: /w/index.php?title=Special:
Disallow: /*action=edit
Disallow: /*action=history
Disallow: /*action=raw
Disallow: /*diff=
Disallow: /*oldid=

User-agent: GPTBot
Crawl-delay: 30

User-agent: ClaudeBot
Crawl-delay: 30

User-agent: PerplexityBot
Crawl-delay: 30

User-agent: Bytespider
Crawl-delay: 30

User-agent: CCBot
Crawl-delay: 30

User-agent: Amazonbot
Crawl-delay: 30

User-agent: AhrefsBot
Crawl-delay: 30

User-agent: SemrushBot
Crawl-delay: 30

robots.txt не є захистом від агресивних ботів. Але коректні боти можуть враховувати Crawl-delay і не сканувати важкі URL.

17. Кешування статичних файлів

У server { ... }:

gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;

location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2)$ {
    expires 30d;
    access_log off;
    add_header Cache-Control "public";
}

18. Перевірка MySQL / MariaDB

Поточні запити:

mysqladmin processlist

Загальний статус:

mysqladmin status

Увімкнення slow query log:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

Перегляд:

sudo tail -f /var/log/mysql/mysql-slow.log

Якщо в slow query log багато запитів до таблиць page, revision, text, recentchanges, logging, потрібно окремо аналізувати індекси, розмір таблиць і проблемні сторінки.

19. SyntaxHighlight та інші важкі розширення

Якщо в htop видно процеси:

python3 ... extensions/SyntaxHighlight_GeSHi/...

це означає, що сторінки з підсвіткою коду можуть створювати велике навантаження.

Рекомендації:

  • оновити розширення SyntaxHighlight;
  • уникати дуже великих блоків <syntaxhighlight>;
  • закешувати сторінки з великими блоками коду;
  • обмежити ботам швидкість доступу до oldid, diff, history;
  • перевірити PHP-FPM slowlog.

20. Діагностика 504 Gateway Timeout

У backend Nginx:

sudo tail -n 100 /var/log/nginx/error.log

Шукати:

upstream timed out while reading response header from upstream

Перевірити кількість кодів відповідей:

sudo tail -n 5000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -nr

Подивитися конкретні URL з 504:

sudo tail -n 5000 /var/log/nginx/access.log | awk '$9 == 504 {print $1, $6, $7, $9, $12}' | tail -50

Якщо 504 виникає при збереженні сторінки, потрібно перевірити:

  • чи POST не потрапляє під rate limit;
  • чи достатній proxy_read_timeout на frontend Nginx;
  • чи достатній fastcgi_read_timeout на backend Nginx;
  • чи не впирається PHP-FPM у pm.max_children;
  • чи не виконуються MediaWiki jobs під час веб-запиту;
  • що показує PHP-FPM slowlog.

21. Рекомендований порядок впровадження

Етап 1. Безпечні термінові дії

  1. Налаштувати real_ip на backend Nginx.
  2. Додати rate limit тільки для ботів і тільки для GET/HEAD.
  3. Використати nodelay.
  4. Перевірити, що POST не лімітується.
  5. Збільшити proxy_read_timeout і fastcgi_read_timeout до 300s.

Етап 2. PHP-FPM

  1. Збільшити pm.max_children.
  2. Увімкнути slowlog.
  3. Поставити request_terminate_timeout = 300s.
  4. Перевірити повідомлення server reached pm.max_children.

Етап 3. MediaWiki

  1. Увімкнути APCu.
  2. Перевірити OPcache.
  3. Винести Job Queue в cron.
  4. Увімкнути файловий кеш.
  5. Проаналізувати важкі розширення.

Етап 4. Кешування Nginx

  1. Додати кешування статичних файлів.
  2. Обережно впровадити FastCGI cache для анонімних GET.
  3. Перевірити, що авторизовані сторінки не кешуються.

Етап 5. База даних

  1. Увімкнути slow query log.
  2. Перевірити довгі запити.
  3. Оптимізувати індекси або проблемні сторінки.

Висновок

Основна ідея оптимізації MediaWiki: не просто збільшити кількість PHP-FPM воркерів, а зменшити кількість важких запитів, які доходять до PHP.

Найважливіші зміни:

  1. Backend Nginx має бачити реальний IP клієнта.
  2. Ботів потрібно обмежувати за швидкістю, а не блокувати повністю.
  3. Rate limit має діяти тільки на GET/HEAD, не на POST.
  4. Збереження сторінок не повинно потрапляти під bot limit.
  5. PHP-FPM має мати достатньо воркерів.
  6. MediaWiki jobs краще виконувати через cron.
  7. OPcache, APCu і кешування анонімних сторінок суттєво зменшують навантаження.
  8. Slowlog PHP-FPM і slow query log MySQL потрібні для пошуку реальної причини затримок.

Після впровадження цих змін сервер має краще витримувати активність ботів, не створюючи проблем для звичайних користувачів і редакторів MediaWiki.