Sessions: Состояние гонки в FilesystemStore

Созданный на 11 июн. 2013  ·  23Комментарии  ·  Источник: gorilla/sessions

В FilesystemStore есть состояние гонки, которое я собираюсь исправить, но я хотел бы получить ваше мнение, прежде чем я продолжу и сделаю это. В основном проблема в том, что если у вас есть одновременные запросы от одного и того же пользователя (тот же сеанс), возможно следующее:

  1. Запрос 1 открывает сеанс для выполнения полудлинной операции
  2. Запрос 2 открывает сеанс
  3. Запрос 2 Удаляет данные сеанса для выполнения «выхода» или аналогичного.
  4. Запросить 2 сохранения
  5. Запрос 1 сохраняет, что делает его так, как будто сеанс никогда не выходил из системы

Я добавил тестовый пример для этого недостатка в cless / sessions @ f84abeda17de0b4fcd72d277412f3d3192f206f2

Самый простой способ исправить это - ввести блокировки на уровне файловой системы. Однако у golang нет кроссплатформенного способа блокировки файлов. Он показывает flock в syscall но это работает только в том случае, если ОС поддерживает его. Я считаю, что поведение flock также может отличаться в разных unix, хотя я не уверен, что это так. Другая проблема с flock заключается в том, что он может не работать в NFS.

Совершенно иное решение - сохранить карту блокировок в самом объекте FilesystemStore . У этого есть еще один набор недостатков: вы не можете иметь несколько процессов, обращающихся к одним и тем же сеансам файловой системы, и вы не можете создать несколько хранилищ для одного и того же сеанса файловой системы в одном приложении. Однако обе эти вещи уже невозможно сделать, не вызывая проблем.

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

Другие серверные части хранилища, основанные на FilesystemStore, могут скопировать этот недостаток (я заметил эту проблему при просмотре кода Redistore для моего проекта boj / redistore # 2)

bug stale

Все 23 Комментарий

А как насчет системы на основе транзакций вместо блокировки? Объект Session может содержать поле lastModified. Когда вы пытаетесь сохранить сеанс обратно в хранилище сеансов, он вернет ошибку, указывающую, что он был изменен с момента последнего чтения. Затем программист мог выбрать повторную попытку, снова получив сеанс.

Это может сработать, его преимущество состоит в том, что нет блокировок, которые нужно очищать, и нет шансов на взаимоблокировки. Я не уверен, что разработчики всегда могут выполнить осмысленную повторную попытку в зависимости от того, что уже было сделано в запросе. Я думаю, что блокировка, безусловно, более безопасный вариант, но транзакции, безусловно, могут работать, если API четко документирует вероятность сбоя и если разработчики старательно обрабатывают эти ошибки.

Тем не менее, даже если FilesystemStore использует транзакции, я думаю, что API должен быть подготовлен для бэкэндов хранилища, которые _do_ используют блокировку, но это будет означать либо запрет на вызов session.Save() более одного раза _или_ введение нового session.Release() . Что бы вы предпочли или бы вы предпочли, чтобы никакие серверные части хранилища не использовали блокировку?

После этой проблемы, поскольку она влияет на RediStore. Спасибо за информацию @cless

На данный момент у меня нет никаких предпочтений, кроме как поступать правильно в долгосрочной перспективе :) Конечно, я также хотел бы сохранить существующий API. Кроме того, повсеместное добавление блокировок создает дополнительную сложность и накладные расходы, которых также неплохо избежать.

Есть ли у вас какие-либо примеры других фреймворков сессий, которые решают эту проблему? Было бы интересно посмотреть, что они делают.

Я знаю, что обработчик сеанса php по умолчанию использует блокировку на основе файловой системы (в tarball php 5.4, который я только что проверил, это находится в файле ext/session/mod_files.c . Он использует flock, который предоставляется ext/standard/flock_compat.c ).

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

Мне было бы любопытно посмотреть, как Flask, Pyramid или Django справляются с этим. Я посмотрю на них, если будет время. Rails также был бы интересен, если кто-нибудь знаком с этой кодовой базой.

И Pyramid, и Flask, похоже, используют cookie для хранения данных, я подозреваю, что ни один из них не обрабатывает условия гонки. Я лишь бегло прочитал документацию, так что вполне могу ошибаться.

Django поддерживает данные сеанса на стороне сервера, поэтому я немного рассмотрю этот код.

Бэкэнд сеанса Django по умолчанию - это база данных, и здесь используется уровень абстракции базы данных Django, с которым я не знаком, поэтому мне трудно интерпретировать. Однако я нашел этот комментарий в бэкэнде файловой системы:

        # Write the session file without interfering with other threads
        # or processes.  By writing to an atomically generated temporary
        # file and then using the atomic os.rename() to make the complete
        # file visible, we avoid having to lock the session file, while
        # still maintaining its integrity.
        #
        # Note: Locking the session file was explored, but rejected in part
        # because in order to be atomic and cross-platform, it required a
        # long-lived lock file for each session, doubling the number of
        # files in the session storage directory at any given time.  This
        # rename solution is cleaner and avoids any additional overhead
        # when reading the session data, which is the more common case
        # unless SESSION_SAVE_EVERY_REQUEST = True.
        #
        # See ticket #8616.

Кажется, это предполагает, что они следят за тем, чтобы содержимое файла сеанса никогда не искажалось, но не о том, что не может возникнуть никаких условий гонки. Если у кого-то есть опыт работы с Django, было бы неплохо увидеть тестовый пример, например cless / sessions @ f84abed
Stackoverflow также, похоже, указывает на то, что Django может иметь условия гонки в сеансах: http://stackoverflow.com/search?q=django+session+race+condition

Хорошо, что ж, FileSystemStore уже имеет крупнозернистый мьютекс, чтобы предотвратить повреждение хранилища сеансов. Я не уверен, сколько дополнительной защиты стоит добавить в библиотеку, потому что это добавит много накладных расходов для каждого запроса, просто чтобы сделать вещи более согласованными для некоторых запросов. Конечно, этот сценарий возможен, но я думаю, что он, вероятно, слишком сложен, чтобы иметь с ним дело в общем смысле. Я вижу, что во многих случаях то, что происходит сейчас, совершенно нормально, тогда как в некоторых приложениях это может быть проблемой. Я подозреваю, что в большинстве случаев у вас будет только один запрос в полете для данного сеанса.

На самом деле это проблема, которая присутствует в любом веб-ресурсе. То же самое произошло бы, если бы у вас был GET, за которым следует PUT на основе информации, за это время все могло измениться.

Во всяком случае, это мои мысли на данный момент, но я рад обсудить их дальше.

Похоже, что в конечном итоге это скорее проблема прикладной области.

Учитывая опубликованный сценарий cless, если ожидается, что запрос 1 будет выполняться достаточно долго, чтобы запрос 2 мог выполняться параллельно (например, одностраничное приложение, написанное на Angular.js, или асинхронное приложение Node.js, обращающееся к некоторому API), кажется, что долго выполняющийся запрос должен быть отделен от сеанса и в этот момент считаться независимым рабочим процессом.

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

вот несколько реальных примеров влияния условий гонки сеанса. Ошибки трудно отлаживать, они появляются случайно и раздражают как разработчиков, так и пользователей. Учитывая достаточно большое приложение, которое широко использует ajax, эти условия гонки рано или поздно проявятся.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g@

Я прочитал всю ветку на EllisLab / CodeIgniter # 1746, в которой рассматривается аналогичная проблема, но их проблема осложняется тем фактом, что CodeIgniter принудительно регенерирует идентификатор сеанса время от времени, и большая часть обсуждения там вращается вокруг этого факта. .

Я понимаю ваше нежелание изменять сеансы несовместимым образом или вносить тонкие различия в API, которые могут нарушить работу существующих приложений. Тем не менее, я все еще думаю, что должна быть задействована дополнительная блокировка. А как насчет добавления двух функций в API магазина, например:

type Store interface {
    Get(r *http.Request, name string) (*Session, error)
    New(r *http.Request, name string) (*Session, error)
    Save(r *http.Request, w http.ResponseWriter, s *Session) error
    Lock(r *http.Request, name string) error
    Release(r *http.Request, name string) error
}

Использование этих функций было бы совершенно необязательным (хотя я лично рекомендовал бы блокировку каждого запроса, который будет записывать в сеанс) и не окажет влияния на существующие приложения.

Существует проблема с предлагаемым интерфейсом блокировки: если блокировка удерживается в базе данных, такой как MySQL, и ваше соединение с базой данных разрывается после того, как вы установили блокировку, вы никогда не сможете ее снять, и ваш сеанс фактически заблокирован. Блокировки должны каким-то образом истечь, но я не совсем уверен, как это должно быть раскрыто разработчику.

Извините, я пропустил ответ Боя, когда разместил свой:
Важно помнить, что длительные запросы не являются обязательным требованием для запуска состояния гонки. Спящий режим в 500 мс в моем тестовом примере нужен только для того, чтобы убедиться, что условие гонки запущено. В реальном мире эти условия гонки будут возникать и для «коротких» запросов, только реже, и именно это затрудняет их отладку.

Вы также должны иметь в виду, что при высокой нагрузке выполнение даже коротких запросов может занять некоторое время.

@cless Хорошие моменты.

Мне нравятся предложенные вами изменения интерфейса.

Срок действия вашей блокировки будет относительно легко реализовать для RediStore, в Redis есть команда EXPIRE, которая позволяет установить TTL для ключа.

Разве такая мелкозернистая блокировка по-прежнему не будет достаточно неэффективной для многих типов хранилищ сеансов? И истечение срока также является проблемой.

Кажется, часть проблемы заключается в том, что сохранение может быть успешным, даже если сеанс дольше существует, разве это не поможет решить проблему, если это не так?

@kisielk , вы можете привести примеры неэффективных магазинов? Большинство баз данных типа "ключ-значение" имеют некоторую форму атомарных операций, которые позволяют создавать блокировки. Базы данных SQL обычно имеют доступ к блокировке на основе строк (хотя я должен сказать, что не знаком с подробностями здесь).
Метод блокировки хранилищ ключей и значений, таких как redis, требует многократных обращений к базе данных, возможно, вы это имели в виду?
Существует проблема с хранилищами файловой системы, потому что go не предоставляет кроссплатформенный способ доступа к блокировкам файлов. Я полагаю, cgo позволит вам создать систему, поддерживающую основные платформы, но, вероятно, я бы избегал этого решения.

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

@boj , проблема в том, что программисту нужно каким-то образом проверить, что блокировка еще не истекла, прежде чем он попытается сохранить. Каким должно быть время истечения срока действия блокировки?

Это один из возможных способов сделать это, но я не уверен, что он мне очень нравится. Вы зависите от того, что часы не прыгают внезапно вперед или назад, и это небезопасное предположение:

func Lock(expiration time.Duration) time.Time {
    // Block here until the lock becomes your. Set the lock to expire after
    // dur+50ms. This extra 50 ms ensures that you never assume you own an
    // expired lock
    return time.Now().Add(expiration)
}

func main() {
    end := Lock(1000 * time.Millisecond)

    // Do your thing here ...
    // time.Sleep(1100 * time.Millisecond)

    if time.Now().After(end) {
        fmt.Println("Lock expired, throw an error")
    } else {
        fmt.Println("Good to go, save the session")
    }
}

@cless Весь ваш комментарий как бы суммирует то, что, кажется, беспокоит @kisielk . X делает это, Y делает то, Z может быть невозможным, если не задействованы некоторые крайности. Хотя условия гонки нежелательны, учитывая редкие случаи, когда они могут произойти, по крайней мере, текущий дизайн гориллы / сессий очень прост и следует элементу философии наименьшего удивления.

Было бы очень просто реализовать интерфейсную часть gorilla / sessions, как вы предложили, однако вы создаете сценарий, в котором вы можете или не сможете легко поменять бэкенды, если они не реализуют Lock / Release столь же легко и эффективный способ. Кажется, слишком много «что, если» в отношении того, что вы предлагаете, главным из которых является «может ли серверная часть даже реализовать это, не прибегая к хакерскому программированию?», За которым следует «Я пытался переключиться с FileSystemStore на FooBarStore, но это не так. похоже, реализует Lock / Release, и моя программа работает не так, как ожидалось ".

Здесь есть хорошая статья о нюансах блокировки сеанса:

http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

Как мы уже сделали здесь, тайм-аут блокировок, если что-то пойдет не так, является одной из основных проблем при реализации такой схемы. Это требует дополнительных размышлений и очень зависит от серверной части.

Что касается реализации Lock / Release, мы могли бы сделать реализацию блокировки для каждого сеанса необязательной, имея отдельный интерфейс. Затем бэкэнд может быть применен к этому интерфейсу, и если он не реализует его, мы могли бы предоставить реализацию по умолчанию с использованием блокировок в памяти.

Другой проблемой, конечно же, является то, что пользователям библиотеки нужно будет обновить свой код, чтобы использовать Lock and Release, но я предполагаю, что без этого они будут вести себя так же, как сейчас.

"мы могли бы предоставить реализацию по умолчанию, используя блокировки в памяти"

Это было бы бессмысленно в многосерверной среде, где запросы сбалансированы по нагрузке.

Мне нравится идея предоставить совершенно другой интерфейс для блокировки, потому что он дает больше свободы для его правильной реализации без изменения поведения существующего кода, использующего сеансы.

Как заметил @boj, вы не можете каким-либо значимым образом обеспечить блокировку памяти в хранилище базы данных. Бремя обеспечения функции блокировки, вероятно, должно быть на разработчике магазина. Если интерфейс блокировок отсутствует, вы все равно можете возвращать ошибки, когда сеанс пытается их использовать. Это действительно не должно быть проблемой, если хранилища по умолчанию обеспечивают блокировку, тогда другие разработчики последуют их примеру, и пользователи будут минимально, если вообще, неудобства.

Блокировки памяти все еще могут иметь смысл в некоторых магазинах, и я думаю, что FilesystemStore - хороший кандидат для них. Реально вы не будете использовать хранилище файловой системы в ситуации с балансировкой нагрузки (хотя теоретически это возможно с сетевыми файловыми системами). Блокировки файловой системы кажутся предпочтительными, но их сложно реализовать на ходу на кросс-платформенной основе, и, кроме того, они не гарантируют работу с NFS.

@boj : согласен, но, как отмечает @cless , это зависит от типа вашего магазина. FilesystemStore уже полагается на RWMutex для предотвращения одновременных изменений хранилища сеансов. Я ожидаю, что большинство бэкэндов на самом деле будут реализовывать интерфейс блокировки, по крайней мере, в конечном итоге, но наличие значения по умолчанию позволит разработчику в то же время выпустить версию, подходящую для среды с одним сервером. FilesystemStore будет одним из них, пока ситуация с межплатформенной блокировкой в ​​библиотеках Go не улучшится.

Я полностью согласен с @cless и хочу подчеркнуть важность блокировки сеанса, и зрелые платформы (такие как PHP) реализуют его в своих хранилищах сеансов. В файлах используется системный вызов 'flock', а в кэше памяти используется возвращаемое значение операции 'add' и дополнительный ключ с суффиксом '.lock' и установленным сроком действия. Я также согласен с подходом @kisielk к новому интерфейсу (наличие функций «Блокировка» и «Освобождение» имеет смысл. Можно нам это, пожалуйста? Go должен быть языком, на котором нужно перейти к высокой производительности и надежности. Я готовы внести свой вклад в некоторые реализации (например, FileSystem, Redis и Memcache). Обратите внимание, что нам действительно нужны несколько новых параметров конфигурации:

  • spinLockWait: время ожидания в миллисекундах между попытками получения блокировки (по умолчанию: 150)
  • lockMaxWait: секунды ожидания блокировки (по умолчанию: 30)
  • lockMaxAge: секунды, в которых блокировка может быть сохранена, прежде чем она будет снята автоматически (по умолчанию: lockMaxWait)

Эта проблема была автоматически помечена как устаревшая, поскольку для нее не было недавних обновлений. Он будет автоматически закрыт через несколько дней.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги