Сегодняшняя история програмистская. Кому компьютеры скучны, можете смело пропускать.Как все знают, у меня есть
отдельный блог на boku.ru. Записи туда по большей части копируются отсюда - в виде исключения я пару раз выложил там скучные рассказы, чтобы включить их в архив, но не афишировать.
Блог сделан на Wordpress. Копирование записей устроено так:
специальный скрипт генерирует RSS из дневника на Diary, а плагин
FeedWordPress забирает RSS и импортирует в Wordpress.
Категории при этом сохраняются, внутренние ссылки мой собственный плагин заменяет на местные, а если пост на дайри изменился - изменяется и пост на boku.ru, так что всё устроено достаточно удобно.
Но есть проблема.
(далее)Код FeedWordPress выглядит примерно так:
function WhatToDo(post)
localPost = FindLocalCopy(post);
if localPost==null then
//New post!
AddPostMeta(localPost, 'syndication_item_hash', post.Hash);
return doCreateNewPost;
else
if not FindMeta(localPost, 'syndication_item_hash', post.Hash) then
//Post changed!
AddPostMeta(localPost, 'syndication_item_hash', post.Hash);
return doUpdatePost;
else
//No changes.
return doNothing;
Если пост на дайри не менялся, FeedWordPress не будет заново его импортировать и не создаст новой ревизии местного поста. Это хорошо. Но перед тем, как записать пост в базу, Wordpress прогоняет его через ряд плагинов, которые меняют его содержание:
Пост на дайри (из RSS) --> (замена ссылок на местные) ----> (исправление форматирования) --> Пост на boku.ru
Бывает, что какой-то из плагинов сбоит, и преобразует пост неправильно. Тогда я начинаю искать, в чём дело. Чтобы разобраться, мне нужно импортировать пост снова и снова, пока я не найду ошибку.
Но как это сделать? Ведь пост уже импортирован, и с точки зрения FeedWordPress, его содержимое не менялось (на дайри он остался тем же).
Для этой цели я влез в файлы FeedWordPress, и временно покромсал описанную выше процедуру. Она стала выглядеть так:
function WhatToDo(post)
localPost = FindLocalCopy(post);
if localPost==null then
//New post!
AddPostMeta(localPost, 'syndication_item_hash', post.Hash);
return doCreateNewPost;
else
if not FindMeta(localPost, 'syndication_item_hash', post.Hash) then
//Hack: Post is always changed!
AddPostMeta(localPost, 'syndication_item_hash', post.Hash);
return doUpdatePost;
//TODO: Restore normal version.
Менялся пост или нет, мы всегда импортируем его заново. Конечно, при этом создаётся новая ревизия и захламляется база, но подумаешь, мне же ненадолго... А старые ревизии поста легко удалить.
Поправив таким образом FeedWordPress, я залил новые файлы на сервер и стал искать баг в своих плагинах. И нашёл. Исправил. Убедился, что теперь посты преобразуются правильно. Всё сохранил, применил, закрыл... а отключить хак забыл.
И ушёл.
Вторая половина этой истории началась через месяц, когда я зашёл на блог на boku.ru. Все страницы с последними постами не работали.
Вместо них отображался белый экран. Не работала даже консоль админа, из которой посты можно удалить. В логах сервера появлялась ошибка:
php error: maximum memory allocation exceeded
Какой-то из скриптов жрёт память? Но почему? Что я менял?
И тут я вспомнил, что забыл отключить хак.
Но постойте, а что такого? Проверки раз в полчаса - это 48 проверок в день, жалкие полторы тысячи ревизий за месяц. Wordpress может обслуживать десятки тысяч постов, для MySQL лишние несколько тысяч ревизий - пустяк.
Если я напишу ещё полторы тысячи постов - вордпресс даже не поперхнётся. А полторы тысячи ревизий вывели его из строя?
Да ну! Не так он написан.
Тогда почему любая страница, которая обращается к последним постам - вылетает с переполнением памяти? База данных находится на диске - что вообще вордпресс грузит в память?
Метадату.
Каждый раз, загружая очередной пост для печати, вордпресс делает примерно следующее:
rows = exec_sql('SELECT * FROM post_metadata WHERE post_id=id');
while rows.MoveNext() do
metadata[rows['name']]=rows['value']
То есть, читает из базы все относящиеся к посту записи метадаты и загружает их для быстрого доступа в память.
Обычно таких записей 8-10, иногда до 15 - мелочи, в общем.
У последних записей в моём блоге их было по 60 000.
Ничего удивительного, что обращаясь к этим записям, вордпресс падал. Он не рассчитан на 60 000 записей метадаты у поста. Удивительно, откуда эти записи взялись.
Я открыл таблицу phpMyAdmin-ом, и увидел, что все они - это копии параметра syndication_permalink. Тогда всё стало ясно.
Описанная выше функция WhatToDo решает, что делать с постом из RSS - добавить новый, обновить существующий или пропустить. При этом она регистрирует syndication_permalink, чтобы второй раз не обновлять одно и то же.
Да, импорт RSS происходит лишь раз в полчаса, 48 раз в сутки, и каждый пост импортируется лишь однажды - но функция проверки WhatToDo вызывается десятки раз за процедуру одного импорта. Только однажды её результат имеет значение, поэтому ревизий в базе действительно создано лишь полторы тысячи - но при каждом вызове она добавляет syndication_permalink, и этих пермалинков, совершенно одинаковых, у одного поста набираются десятки тысяч.
Ирония: вордпресс мог бы вынести десятки тысяч постов - но не десятки тысяч свойств поста.
Как всё это чинить?
Итак, испорчена таблица post_metadata: в ней для некоторых постов некоторые записи продублированы десятки тысяч раз. Нужно удалить дубли, но оставить по одной копии каждой записи.
После некоторой возни и гуглинга сотворился следующий манёвр:
CREATE TABLE `keep_ids` AS (
SELECT MIN(`rowid`) AS `rowid` FROM `post_metadata` GROUP BY `postid`, `name`, `value`
)
Этим запросом мы находим все цепочки дублей (записей с одинаковыми данными в полях postid, name и value), и в каждой выбираем наименьший номер записи. Таким образом, мы получаем по одной копии каждой уникальной записи. Эти копии надо сохранить, а всё остальное удалить.
ALTER TABLE `keep_ids` ADD UNIQUE INDEX `rowid` (`rowid`)
Это чтобы операции с новой таблицей были быстрыми - сейчас понадобится.
DELETE FROM `post_metadata` WHERE `rowid` NOT IN (SELECT `rowid` FROM `keep_ids`)
Удаляем все записи из исходной таблицы, которые не вошли в наш "список на сохранение". Если б в `keep_ids` не было индексов, мы бы тут завязли на несколько минут, а так - только секунд.
Ну и, наконец, удаляем временную таблицу:
DROP TABLE `keep_ids`
Победа! Число записей в post_metadata резко падает с сотен тысяч до 13 000 и блог снова работает нормально.
Названия таблиц и полей в примерах условны, и не соответствуют настоящим названиям в базе вордпресс. Код написан на условном языке, а код SQL может быть не совсем правильным, но передаёт общую мысль.