Статья размещена автором Бетке Сергей Сергеевич

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 доступны в сети, но они неоднозначны:

Да, многие хостеры выбора не предлагают – навязывают 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 заголовки)

Опубликовать комментарий

XHTML: Вы можете использовать следующие HTML теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Tags Связь с комментариями статьи:
RSS комментарии
Обратная ссылка