Web сервер для WordPress на Windows 2012 R2: настраиваем и оптимизируем
Давно перенёс свой блог на свой IIS7 и свой домен соответственно. Однако, установка “по умолчанию” позволяет построить достаточно непроизводительный сервер. Более чем непроизводительный. Да, сам wordpress и не отличается производительностью, но обеспечить достойную реакцию для связки WordPress+IIS7+Windows Server+MySQL+PHP можно. Об этом постараюсь и написать, чтобы просто не забыть.
Рекомендую замечательную видео-инструкцию от MS по установке wordpress через Web Platform Installer! Просто халява! Итак, приступим.
IIS7 уже имеем на базе Windows Web Server 2012 R2. Качаю Web Platform Installer. Далее по видео-инструкции всё устанавливаю – работает! Да, автоматизация просто на высоте, благодарности Microsoft.
Для Web сервера оптимально, безусловно, использовать виртуальный сервер (благо имею лицензию на MS Windows Server 2012 R2 Datacenter, посему могу себе позволить произвольное количество виртуальных машин). Но при конфигурировании сервера следует задумываться над последствиями каждого шага.
MySQL Server
MySQL Server data диск
И первый шаг, над которым следует хорошо подумать, – виртуальные диски. Не стоит размещать на системном диске и базы данных MySQL, и каталоги приложений IIS. Настойчиво рекомендую Вам подключить отдельный виртуальный диск, отформатированный в NTFS, для размещения баз MySQL. Объяснения простые – MySQL при закрытии транзакций сбрасывает кеш диска, на котором размещены его базы (что можно изменить параметром innodb_flush_log_at_trx_commit, но делать это небезопасно с точки зрения сохранения целостности базы данных). Чтобы выбор значения данного параметра не влиял на производительность ОС – перенесите каталог баз данных MySQL на отдельный (от системного) диск (в случае виртуальной машины – виртуальный диск). Ниже – цитата из my.ini:
[mysqld] datadir = I:\ProgramData\MySQL\MySQL Server 5.6\data
Останавливаем службу MySQL сервера, вносим изменения в my.ini, переносим указанный выше каталог с системного диска на новый диск, запускаем службу.
Управление сервером MySQL
Да, всё можно сделать через консоль mysql, однако куда нагляднее и удобнее использовать для целей администрирования MySQL Workbench. Загружаем его с сайта, устанавливаем, запускаем.
Технологии подключения: TCP/IP vs Named Pipe
Собственно MySQL Server предлагает нам три “протокола” связи:
- shared memory (только в пределах одной рабочей станции);
- named pipe
- tcp/ip
“Родной” клиент mysql так же поддерживает все три протокола. Безусловно очевидно, что в среде windows протоколы приведены выше в порядке убывания производительности, и наиболее оптимальным для взаимодействия с локальный MySQL сервером был бы протокол shared memory. Однако php плагины на сегодняшний день не могут использовать shared memory для работы с MySQL. А жаль.
Вторым по производительности будет использование именованных каналов – named pipes. Доказывать смысла не вижу, в интернете можно найти массу обзоров. Основной аргумент – при использовании именованных каналов на одной станции ОС самостоятельно выбирает транспорт отличный от сетевого транспорта – собственно shared memory и выбирает. Посему производительность именованных каналов в случае локально расположенного MySQL сервера будет выше, и при удалённо расположенном – не ниже. (P.S. Возможно по этой причине разработчики PHP и не предложили явного использования протокола shared memory, предоставив при этом возможность использования named pipes). Стоит почитать и иные мнения: при сетевом взаимодействии tcp/ip будет быстрее (канал не передаёт данные, пока приёмник их не запросит, а в tcp/ip несколько иначе – приёмника никто не спрашивает, посылают и ожидают подтверждения). Итого, моё мнение: при локальной установке MySQL Server по отношению к IIS/PHP – выбирайте named pipes (не даром PHP по умолчанию пытается использовать named pipes), при удалённой установке – явно настраивайте TCP/IP подключение.
Опять цитата my.ini
:
[mysqld] shared-memory shared-memory-base-name=MySQL enable-named-pipe socket = MySQL skip-networking
Я явно запретил использование сетевых протоколов моему экземпляру MySQL Server’а. В php.ini параметры подключения к БД выглядят следующим образом:
[MySQL] mysql.allow_persistent = On mysql.max_persistent = -1 mysql.max_links = -1 mysql.cache_size = 2000 mysql.default_socket = mysql.default_host = ".:MySQL" mysql.default_user = mysql.default_password = mysql.connect_timeout = 60 mysql.trace_mode = Off [MySQLi] mysqli.allow_persistent = On mysqli.max_persistent = -1 mysqli.max_links = -1 mysqli.cache_size = 2000 mysqli.default_socket = mysqli.default_host = ".:MySQL" mysqli.default_user = mysqli.default_pw = mysqli.reconnect = Off [Pdo_mysql] pdo_mysql.cache_size = 2000 pdo_mysql.default_socket = [mysqlnd] mysqlnd.collect_statistics = Off mysqlnd.collect_memory_statistics = Off mysqlnd.net_cmd_buffer_size = 2048 mysqlnd.net_read_buffer_size = 32768
Использование "."
вместо localhost
существенно!
P.S. Установка параметра default_socket
в MySQL
(значение по умолчанию) приводит к невозможности подключения, поэтому единственно правильно указывать default_host
, как указано выше.
Ниже цитирую wp_config.php
в части настроек подключения к БД:
<?php // ** MySQL settings - You can get this info from your web host ** // /** MySQL hostname */ define( 'DB_HOST', ini_get( "mysql.default_host" ) ); /** The name of the database for WordPress */ define( 'DB_NAME', 'blogdb' ); /** MySQL database username */ define( 'DB_USER', 'blogdbuser' ); /** MySQL database password */ define( 'DB_PASSWORD', 'blogdbuserpassword' ); $table_prefix = 'wp_'; ?>
Как видно, DB_HOST
читаю из php.ini
с расчётом на то, что MySQL сервер для всех php сайтов на данном веб-сервере один. Такой вариант существенно гибче в случае эксплуатации нескольких сайтов на одном сервере – настройки подключения правим в php.ini
, и они действуют на все сайты.
Кратко итоги: на моём тестовом wordpress блоге время отдачи главной страницы сократилось с 410 мс в среднем до 390 мс при переходе с TCP/IP на named pipes (при локальном расположении сервера). Безусловно – при прочих равных условиях и при отсутствии промежуточных кэширующих серверов.
Charset и Collation
Естественно, целесообразно указать и кодовую таблицу для символьных данных, и правила их сравнения / сортировки. Достаточно подробное описание для MySQL нашёл на просторах сети — http://gahcep.github.io/blog/2013/01/05/mysql-utf8/. Итак, цитаты файлов конфигурации:
- Цитата
wp_config.php
:<?php /** Database Charset to use in creating database tables. */ define( 'DB_CHARSET', 'utf8' ); /** The Database Collate type. Don't change this if in doubt. */ define( 'DB_COLLATE', 'utf8_unicode_ci' ); ?>
- Цитата из
my.ini
:[mysqld] init_connect="SET collation_connection = utf8_unicode_ci" character-set-server = utf8 collation-server = utf8_unicode_ci lc-messages = ru_RU [client] default-character-set = utf8
- В
php.ini
параметры подключения к БД выглядят следующим образом:default_charset = "UTF-8"
Рекомендую использовать utf8_unicode_ci
вместо utf8_general_ci
, хотя он и более медленный, зато сюрпризов не будет, все правила сравнения и сортировки будут работать так, как ожидается.
P.S. lc-messages
к этому разделу имеет далёкое отношение, но привести решил его здесь. Всё-таки приятнее видеть ошибки от MySQL на русском языке, нежели на каком-либо чужом.
MyISAM vs InnoDB
Общеизвестно, что MySQL “из коробки” поддерживает два движка БД – InnoDB и MyISAM. Начиная с версии 5.5 движком по умолчанию является InnoDB. Обзоры и обоснование выбора того или иного движка для WordPress доступны в сети, но они неоднозначны:
- http://www.uran1980.com/myfolio/2009/05/18/myisam-vs-innodb/
- Can WordPress Developers survive without InnoDB? (with MyISAM vs InnoDB benchmarks)
- WordPress Database Slow — should I switch to InnoDB?
- Enhancing Performance in WordPress by Moving from MyISAM to Innodb
Да, многие хостеры выбора не предлагают – навязывают MyISAM вследствие невысокой потребности в памяти. Однако, для меня ключевым моментом стала поддержка кэширования данных со стороны InnoDB (MyISAM кэширует только индексы). Да, InnoDB поддерживает транзакции и блокировки на уровне записей (а не таблиц в целом), но для меня эти факты не являются значимыми – wordpress 99% запросов на чтение (если не больше), на запись – только мои запросы из консоли администратора, а я могу и подождать. А вот кэширование данных, что крайне положительно сказывается на выполнении банальных выборок, более чем полезно. Поэтому я выбираю InnoDB (ниже цитата из my.ini):
[mysqld] default-storage-engine=InnoDB
Но этого недостаточно!
Кроме того, что первые версии блога функционировали на более ранних версиях MySQL, поэтому таблицы базы созданы с движком MyISAM. Посему необходимо на живой базе изменить движок для таблиц. Для конвертации воспользуемся SQL сценарием, который я запущу из MySQL Workbench, но его с тем же успехом можно запустить и из mysql
:
delimiter $$ CREATE PROCEDURE ChangeEngine( db varchar(20) , newEngine varchar(20) ) BEGIN DECLARE done BOOL DEFAULT FALSE; DECLARE dbTable varchar(255); DECLARE sqlAlterTable varchar(255); DECLARE dbTables CURSOR FOR SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE ( TABLE_SCHEMA = db ) AND ( ENGINE <> newEngine ) AND ( TABLE_TYPE = 'BASE TABLE' ) ; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN dbTables; FETCH dbTables INTO dbTable; WHILE NOT( done ) DO SET @sqlAlterTable = CONCAT( 'ALTER TABLE ', db, '.', dbTable, ' ENGINE ', newEngine, ';' ); PREPARE stmtAlterTable FROM @sqlAlterTable; EXECUTE stmtAlterTable; DEALLOCATE PREPARE stmtAlterTable; FETCH dbTables INTO dbTable; END WHILE; CLOSE dbTables; END$$ delimiter ; CALL ChangeEngine( 'nicetournovrutest', 'InnoDB' ); DROP PROCEDURE ChangeEngine;
Итак, SQL код, приведённый выше, успешно меняет движок всех таблиц базы на InnoDB. При использовании MyISAM среднее время формирования главной страницы блога составило 390 мс. Однако, и после перехода на InnoDB (с настройками по умолчанию) время ответа то же — 390 мс. Следует разобраться с параметрами кэширования данных в InnoDB.
Некоторые рекомендации по реконфигурированию MySQL при переходе на InnoDB приведены на официальном сайте. Уменьшаем буфер для MyISAM (key_buffer_size
) и увеличиваем буфер для InnoDB (innodb_buffer_pool_size
) с учётом того, что InnoDB кэширует не только индексы, но и данные. В итоге, в my.ini
внёс следующие изменения:
[mysqld] query_cache_size=256M tmp_table_size=16M thread_cache_size=38 default-storage-engine=InnoDB innodb_buffer_pool_size=512M innodb_additional_mem_pool_size=12M innodb_log_buffer_size=6M innodb_log_file_size=10M innodb_random_read_ahead=ON innodb_read_ahead_threshold=64 innodb_flush_log_at_trx_commit=2 innodb_thread_concurrency=10 key_buffer_size=8M
На этом с выбором и настройкой собственно движка пока закончим. Посмотрим, что нам сообщает сам MySQL в своих журналах.
Отлавливаем неоптимизированные медленные запросы wordpress
В каталоге данных (в моём случае – “I:\ProgramData\MySQL\MySQL Server 5.6\data”) MySQL сохраняет крайне полезный файл — %COMPUTERNAME%-slow.log
. Останавливаем MySQL, удаляем / переименовываем имеющийся файл, запускаем MySQL и пытаемся воспользоваться нашим тестовым блогом. В результате вижу в этом файле:
C:\Program Files\MySQL\MySQL Server 5.6\bin\mysqld.exe, Version: 5.6.10-log (MySQL Community Server (GPL)). started with: TCP Port: 0, Named Pipe: MySQL Time Id Command Argument # Time: 140731 1:37:50 # User@Host: *******[*********] @ localhost [] Id: 3 # Query_time: 0.078339 Lock_time: 0.015618 Rows_sent: 592 Rows_examined: 688 use nicetournovru; SET timestamp=1406756270; SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'; # User@Host: *******[*********] @ localhost [] Id: 3 # Query_time: 0.000000 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 2 SET timestamp=1406756270; SELECT * FROM wp_wpo_campaign WHERE 1 = 1 AND active = 1 AND (frequency + UNIX_TIMESTAMP(lastactive)) < 1406741870 ORDER BY created_on DESC; # User@Host: *******[*********] @ localhost [] Id: 3 # Query_time: 0.015632 Lock_time: 0.015632 Rows_sent: 0 Rows_examined: 0 SET timestamp=1406756270; SELECT DISTINCT widget_id FROM wp_dynamic_widgets WHERE maintype LIKE 'page%' OR maintype IN ('date', 'role', 'browser', 'tpl', 'wpml', 'qt'); # Time: 140731 1:37:51 # User@Host: *******[*********] @ localhost [] Id: 3 # Query_time: 0.031256 Lock_time: 0.015631 Rows_sent: 4 Rows_examined: 13 SET timestamp=1406756271; SELECT * FROM wp_links INNER JOIN wp_term_relationships AS tr ON (wp_links.link_id = tr.object_id) INNER JOIN wp_term_taxonomy as tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE 1=1 AND link_visible = 'Y' AND ( tt.term_id = 3 ) AND taxonomy = 'link_category' ORDER BY link_name ASC;
В этот журнал, как уже понятно из его названия, MySQL записывает “медленные” запросы. В частности – запросы с отбором / сортировкой / объединениями без индексов. Как видно, в моём случае таких запросов оказалось 4. P.S. Блог “вырос” ещё с wordpress 3.0, возможно – проблемы со структурой таблиц связаны именно с этим, но решать то их всё равно нужно!
Итак, исправим таблицы (добавим индексы). В случае с wp_options
индексов будет мало. Не могу объяснить решение разработчиков wordpress по применению типа varchar(10)
для поля, по которому осуществляется отбор, и которое на самом деле имеет только два значения - ’yes
и ’no’
. Даже применение индекса в этом случае приведёт к приличному числу сравнений unicode строк по достаточно сложным правилам! Посему решил изменить типа поля autoload
на ENUM( ‘yes’, ‘no’ )
. Подобная замена типа пройдёт совершенно прозрачно для wordpress (запросы будут выполняться, как и раньше), но при этом глубина индекса – 1!
Таблица wp_wpo_campaign
– наследство плагина WP-o-Matic. Там записей в моём случае всего 2!, но запрос, естественно, выполняется без индексов! Однозначно напрашивается поле nextactive
вместо выражения в фильтре. Вероятнее всего, этот плагин я просто отключу. В любом случае при каждом обращении к сайту этот запрос мне просто не нужен!
Таблица wp_dynamic_widgets
– наследство плагина Dynamic Widgets. Отказываюсь от его использования. Таблица явно не оптимизирована под фильтры, и оптимизация приведёт к изменению самих запросов.
Также целесообразно отказаться от плагина Redirection в пользу URL Rewrite на IIS или .htaccess на Apache. Данный плагин генерирует запросы при каждом обращении, причём, несмотря на наличие всех необходимых индексов, план выполнения запроса приводит к операциям без использования индексов (join трёх таблиц в одном запросе). Я пока не отказался от него, послежу ещё за его запросами.
Сценарий для оптимизации БД в моём случае выглядит так:
ALTER TABLE wp_options CHANGE COLUMN `autoload` `autoload` ENUM('yes','no') NOT NULL DEFAULT 'yes' , ADD INDEX `autoload` (`autoload` ASC) ; ALTER TABLE wp_links ADD INDEX `link_name` (`link_name` ASC) , ADD INDEX `link_owner` (`link_owner`); ALTER TABLE wp_term_relationships ADD INDEX `term_order` (`term_order` ASC) ; ALTER TABLE wp_comments CHANGE COLUMN `comment_approved` `comment_approved` ENUM( '0', '1', 'spam' ) NOT NULL DEFAULT '1' , ADD INDEX `comment_approved` (`comment_approved` ASC) , CHANGE COLUMN `comment_type` `comment_type` ENUM( '', 'pingback' ) NOT NULL DEFAULT '' , ADD INDEX `comment_type` (`comment_type` ASC) ; ALTER TABLE wp_posts CHANGE COLUMN `post_status` `post_status` ENUM( 'publish', 'draft', 'auto-draft', 'private', 'future', 'pending', 'inherit', 'trash' ) NOT NULL DEFAULT 'publish' , ADD INDEX `post_status` (`post_status` ASC) ; ALTER TABLE wp_users ADD INDEX `display_name` (`display_name`);
После его исполнения в журнале медленных запросов MySQL появляется только первый запрос (по wp_options
), и только при при первом запросе к БД. Итого — проблема медленных запросов решена. В итоге указанных выше небольших оптимизаций время генерации главной страницы сократилось с 390 до 330 мс!
Прочие мелочи
Приведу пару полезных ссылок при работе с MySQL:
MySQL и альтернативы: а можно ли быстрее?
Существуют альтернативные серверы БД, построенные на базе открытого кода MySQL. На Windows доступна только одна альтернатива — MariaDB. Качаю дистрибутив под Windows (есть msi пакет, что удобно). Скачал дистрибутив, запустил установку. Выбираем тип установки – обновление установленного экземпляра сервера БД. И обновление пройдёт полностью в автоматизированном режиме. При этом обновится и существующая служба MySQL Server. Замену произвёл ради более производительного движка вместо InnoDB – XtraDB.
В общем – рекомендую использовать MariaDB вместо MySQL с движком XtraDB. Однако, существенной разницы для wordpress я не заметил – порядка 30 мс отыграли.
А можно ли ещё быстрее? Кэшируем PHP код
Windows Cache Extension for PHP
P.S> Этот раздел можно сразу пропустить и перейти к следующему – альтернативному решению.
С увеличением объёма PHP кода (в том числе – и за счёт многочисленных плагинов) неизбежно возникают затраты на собственно интерпретацию PHP при каждом запросе. Но и здесь возможно кэширование. Для этих целей сообщество предлагает нам Windows Cache Extension 1.3 for PHP. Качаем его и устанавливаем. Настоятельно рекомендую Вам установить данное расширение к PHP.
Я поступил следующим образом:
- скопировал
php_wincache.dll
вC:\Program Files (x86)\PHP\v5.5\ext
; - в
php.ini
дописал:[PHP] extension=php_wincache.dll [Session] session.save_handler = wincache [wincache] wincache.fcenabled = 1 wincache.fcachesize = 85 wincache.fcndetect = 1 wincache.internedsize = 4 wincache.maxfilesize = 2048 wincache.ocenabled = 1 wincache.ocachesize = 255 wincache.chkinterval = 300 wincache.ttlmax = 7200 wincache.enablecli = 1 wincache.ignorelist = wincache.ucenabled = 1 wincache.ucachesize = 5 wincache.rerouteini = "C:\Program Files (x86)\IIS\Windows Cache for PHP\reroute.ini"
- и добавил файл
reroute.ini
дописал:[FunctionRerouteList] file_exists=wincache_file_exists file_get_contents:2=wincache_file_get_contents filesize=wincache_filesize readfile:2=wincache_readfile is_readable=wincache_is_readable is_writable=wincache_is_writable is_writeable=wincache_is_writable is_file=wincache_is_file is_dir=wincache_is_dir realpath=wincache_realpath
При включенном wincache.ocenabled
минимум 100 мс мы выигрываем даже для PHP 5.5.
Родной OpCache
Но! В PHP 5.4+ есть встроенный оптимизатор (кэш) кода, но по умолчанию он выключен. И для него так же есть масса параметров, мой вариант для php.ini:
[PHP] ; extension=php_wincache.dll zend_extension=php_opcache.dll [opcache] opcache.enable = 1 opcache.enable_cli = 0 opcache.memory_consumption = 256 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 4000 ;opcache.max_wasted_percentage=5 opcache.use_cwd = 1 opcache.validate_timestamps = 0 ;opcache.revalidate_freq=2 opcache.revalidate_path = 0 opcache.save_comments = 0 opcache.load_comments = 0 opcache.fast_shutdown = 1 opcache.enable_file_override = 1 opcache.optimization_level = 0xffffffff ;opcache.blacklist_filename= opcache.max_file_size = 0 opcache.consistency_checks = 0 opcache.force_restart_timeout = 3600 opcache.error_log = opcache.log_verbosity_level = 2 ;opcache.preferred_memory_model= opcache.protect_memory=0
На этом с оптимизацией среды исполнения заканчиваю. И пора заняться кэшированием (http cache-control заголовки)
RSS комментарии
Обратная ссылка