Сегодняшняя история програмистская. Кому компьютеры скучны, можете смело пропускать.
Как все знают, у меня есть отдельный блог на boku.ru. Записи туда по большей части копируются отсюда – в виде исключения я пару раз выложил там скучные рассказы, чтобы включить их в архив, но не афишировать.
Блог сделан на WordPress. Копирование записей устроено так: специальный скрипт генерирует RSS из дневника на Diary, а плагин FeedWordPress забирает RSS и импортирует в WordPress.
Категории при этом сохраняются, внутренние ссылки мой собственный плагин заменяет на местные, а если пост на дайри изменился – изменяется и пост на boku.ru, так что всё устроено достаточно удобно.
Но есть проблема. (далее)
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 может быть не совсем правильным, но передаёт общую мысль.
Один комментарий
От оно как было… Сводки с полей сражений.