Блог Конфуция
Управление яваскриптовой памятью в энжинксе 03.12.2009
Пишем сценарий

UPD 2010-06-08: Внимание! С тех пор модуль оченно уложнился и точек входа в яваскрипт стало гораздо больше. Теперь каждая обертка укоренняется и связывается с нативным запросом сразу же после оборачивания.

UPD 2011-04-08: А теперь еще и устройство SpiderMonkey поменялось целиком, и поди разберись, как оно там теперь работает ;)

Когда только задумался, а не встроить ли яваскрипт в энжинкс, все казалось легко. Достаточно обернуть нативный энжинксовский запрос в объект и передать в яваскриптовый движок. А что там с ним дальше будет — не моя забота, ведь, в яваскрипте есть автоматическая сборка мусора. Оказалось все не так уж и просто. Есть как минимум три разных сценария обработки запроса, каждый из которых требует вдумчивого анализа.

Первый, и самый простой, тип. Пришел запрос, обернули его в яваскриптовый объект, передали назначенному в конфиге обработчику (обычная яваскриптовая функция, то есть замыкание) и получили ответ. Собственно все. Дальше нам не нужна ни структура запроса в памяти энжинкса, ни объект-обертка в памяти cпайдерманки. Надо только передать текст ответа дальше энжинксу и можно идти спать.

Второй сценарий посложнее. Начало такое же как и у первого. Запрос → обертка → обработчик. Дальше в развитие событий врывается самая клевая штука во всем этом яваскриптовом встраивании — сохранение запроса в глобальном объекте, чтобы его не съел уборщик. После того, как яваскрипт вернет нам управление, мы уже не сможем просто забыть об объекте-оберке. Это потому, что энжинкс и слыхом не слыхивал о сборке мусора в яваскрипте: он просто освободит память из пула запроса и отдаст ее другому запросу, а то и хуже — под данные чьих-нибудь буферов :) Следовательно, почти сразу после сохранения, объект-обертка начинает указывать в пустоту. А это значит что энжинкс упадет тоже сразу (или хуже того — не сразу) после того, как кто-то в яваскрипте попробует обратиться к сохраненному запросу. Например, кто-то решит собрать статистику по запросам и станет складывать их в какой-нибудь массив, чтобы потом за один раз посчитать к каким локациям обращались чаще всего. Тут-то все и рухнет, так как ни одного их этих запросов уже нет, а память совсем не похожа на то, что там было в момент сохранения объекта-обертки в массиве. Да, вот так закручено, люблю этот мир ;)

Третий тип запросов еще хитрее, если такое вообще возможно. Начало опять простое: запрос к энжинксу → обертка → вызов яваскрипта. И тут в развитие событий врывается вторая суперудобная и обалденная штука из мира динамических языков — замыкания. Кто-то в яваскрипте решает выполнить подзапрос или поставить таймер. При таких делах яваскрипт сохранит объект-обертку так же, как и во втором ходе событий, и нам придется решать все те же проблемы. Но само замыкание для нас никто не сохранит, а это значит, что когда подзапрос выполнится или таймер сработает, уже не будет ни объекта-обертки, ни замыкания — их всех удалит сборщик мусора — останутся просто указатели в пустоту. То есть этот тип развития событий подразумевает взаимные ссылки: яваскрипта на энжинкс, а энжинкса на яваскрипт.

О скорости

Третий тип развития событий самый полный и включает в себя решение всех задач по встраиванию скриптового движка в материнское приложение. Если так, то тогда зачем мы тут себе голову забиваем этими примитивными путями. А вот зачем.

Энжинкс славится своей скоростью и малым потреблением ресурсов. Ну, собственно одно из другого следует. И раз он так хорош, то не стоит его совсем уж портить этим медленный SpiderMonkey (в сравнении с Google v8 и SquirrelFish Extreme — да, ключевики всё же должны встречаться в теле страницы). Так вот, если все делать по третьему варианту, то для каждого, даже, малюсенького запросика мы должны проделать кучу работы по управлению памятью. И если научиться определять, по какому пути пошел процесс, то можно изрядно сэкономить. Один момент. Второй вариант — это когда яваскрипт куда-то складывает запросы — совершенно неопределим для спайдерманки (или v8) без запуска полного цикла сборки мусора. А это просто сказочно медленно. Таким образом, мы избавляемся от одной из веток развития событий. Итак, их осталось две: простая, когда можно забить на мусоросборщик почти совсем; и сложная, когда надо везде все запомнить и всячески перестраховаться.

Запросов первого типа будет подавляющее большинство. Это могут быть запросы к кешу, к статусу какого-нибудь процесса, или запросы типа «запланируй вот это, а я пока погуляю». Поэтому, стоит научиться определять их быстро. В энжинксе, начиная с версии 0.8 (не помню точно с какой подверсии) появился счетчик ссылок на запрос. Да, как в перле — reference counting. ИМХО, просто, понятно и относительно надежно. Нам, скриптерам (а я далеко не один такой — чего только в энжинкс не страивают), этот счетчик вообще незаменим, и не только для решения этой задачи. Так вот, его-то мы и используем для разделения задачи на две, обозначенные выше. Если r->main->count больше единицы, то у нас ситуация, когда запрос будет жить дольше, чем выполняется наш обработчик. Если же равен единице, то можно на все забить и все выкинуть (ну, почти). Если меньше единицы, значит мы где-то накосячили, и стоит ждать сегфолта. Научившись быстро определять, какие задачи надо решить в конкретном случае, пора уже начать решать эти задачи.

Решаем проблемы

В первом варианте задач никаких и нет. Вновь созданный объект-обертка сразу же передается функции внутри яваскрипта в виде параметра (или инвоканта), что защищает его от уничтожения, пока эта эта функция не завершится. А завершится она как раз тогда, когда этот объект становится нам больше не нужен. Надо только не забыть выставить указатель на нативный запрос в ноль. Здесь могут быть мелкие неприятности, типа того, что спайдерманки решит начать уборку как раз между созданием объекта и передачей его в функцию. Это возможно в многопоточных приложениях и для борьбы с такими неприятностями есть функции входа и выхода в «неубираемую» область (JS_EnterLocalRootScope() и  JS_LeaveLocalRootScope()). А еще, из неприятностей, могут взять и отменить защиту от сборки для параметров, переданных извне. Но это все мелочи и они решаются легко и быстро.

И вот, наконец-то, решение задач второго варианта (он же третий по первоначальному табелю). Повторим коротко условия:

  1. Нельзя ссылаться на отработавший свое запрос энжинкса.
  2. Нельзя дать уборщику уничтожить объект-обертку, который еще может понадобиться энжи.
  3. Необходимо сохранить от уборщика все яваскриптовые функции, которые еще понадобятся.
  4. Надо не забыть разрешить уничтожение объекта-обертки, когда он больше не нужен.

Итак по порядку.

1. Инвалидация указателя

Избавиться от ссылки на запрос энжинкса (это такая большущая многоярусная структура) вообще-то нельзя. То есть пока обертка плавает где-то в памяти яваскрипта, она будет ссылаться куда-то. То есть, через час после запроса мы все-таки можем вызвать r.sendHeaders(200, "Content-type: plain: encoding=utf-8"). Важно как-то понять, что этого делать нельзя и сообщить об этом скрипту. Тут все очень просто: достаточно сказать объекту-обертке, что теперь он указывает на NULL, а при каждом обращении к свойствам или методам проверять, не равен ли NULL, случайно, наш обернутый запрос. В том случае, если уже нал (нулл, нил, нуль, ноль, зеро), то выбросить исключение и вернуть пользователю красивую ошибку 500. Это, прямо скажем, очень здорово, если сравнивать с падением всего рабочего процесса, обслуживающего в этот момент тысячу—другую невинных клиентов.

Как же выставить указатель на оборачиваемый объект в NULL? Спасибо Игорю, это очень просто. Достаточно подписаться на событие уничтожения запроса, и в обработчике этого события выставить указатель в ноль. Просто, быстро и надежно, как сам нжинкс.

2. Защита от уничтожения — корни

Защитить объект от уборки (на рабочем столе и по дому, a еще разрешить ему посуду не мыть) можно как угодно легко. Можно присвоить его глобальной переменной или свойству другого защищенного объекта (global. requests. push(r)). Можно записать его в слот защищенного объекта (JS_SetReservedSlot()), а можно сделать его самого защищенным, назначив корнем (JS_AddRoot()). Сборщик мусора начинает помечать нужные еще объекты с таких корней. Глобальный объект, например, является именно таким корнем. В данном случае, мы создадим в структуре запроса энжинкса специальный указатель (jsval), который попросим назначать корнем всякий объект, на который он указывает. И, да, именно, запишем туда над объект-обертку. Все, его не уничтожат до тех пор, пока мы не уничтожим корень. А корень мы уничтожим там же, где выставляли NULL — в обработчике события уничтожения запроса. Но об этом далее.

3. Защита от уничтожения — слоты в корнях

Защитить функции (они же замыкания, или, почти, как функторы) можно теми же методами, что и сам объект-обертку. Собственно, год назад я так и поступил: добавлял для обработчика тела новый корень, а потом его удалял. Теперь же добавились таймеры и подзапросы. Клепать корни для всех этих товарищей в глобальном пространстве не хочется, так как запросов будут тысячи. Нам хватит нагрузки от создания корней для самих этих тысяч запросов. К тому же создание корня (как сейчас видно из исходников спайдерманки) — это добавление ключа в хеш по значению указателя. И чем больше мы туда запихнем, тем хуже для нас. Кстати, как раз для того, чтобы разгрузить этот хеш мы тут и затеяли все эти выкрутасы с разделение запросов на два типа, из чего теперь получился целый здоровенный пост (вы читать не устали?).

Так вот, вместо корней лучше используем слоты. Они тоже не быстрые (судя по исходникам спайдерманки там нет ничего быстрого), но это хотя бы не поиск в таблице из тысяч записей. А еще слоты хороши тем, что их можно потом не стирать. То есть объект-обертка когда-нибудь будет убран а вместе с ним и все эти функции со своими локальными и замкнутыми переменными. Для v8 это вообще подарок с его поколениями переменных. Этот подход позволяет обработчику запроса энжинкса отцепить объект-обертку и одним махом избавиться ото всех его паразитов.

4. Освобождение памяти

Правильно отцепить объект-обертку — дело не хитрое. Отцепим его там же, где помечали его пустым, в обработчике уничтожения запроса. Ах, как же я был рад, когда нашел его в исходниках энжинкса! Тут достаточно вызвать JS_RemoveRoot() и все в порядке.

После подробного разбора всех накладных расходов по управлению памятью, становится понятно, зачем мы тут всю эту кашу заварили.

Послесловие

При чем тут v8? А при том, что завязываться на один браузер, тьфу, то есть движок яваскрипта, мы, клиентские программисты, не приучены. Сегодня (точнее уже вчера) хорош и стабилен спайдерманки, а завтра (точнее уже сегодня) становится хорош v8. А потом станет хорош еще кто-то, или даже совсем не яваскрипт. А ведь корни, слоты и указатели на нативные объекты есть везде, где убирают мусор автоматом.

Где исходники? Они — на гитхабе. Скоро поставлю красивые гитхабовские ссылочки на строчечки (обожаю гитхаб!).

Ого, а хотел в пару абзацев уложиться.

Теги:
  • сервер
  • C
  • embedding javascript
  • nginx
  • ngx_http_javascript_module
  • ngx_http_js_module
  • serverside javascript
  • spidermonkey
Очень жду ваших комментариев на почту или на гитхаб.