Sessions: Condição de corrida em FilesystemStore

Criado em 11 jun. 2013  ·  23Comentários  ·  Fonte: gorilla/sessions

Há uma condição de corrida em FilesystemStore que pretendo corrigir, mas gostaria de sua opinião antes de prosseguir e fazê-lo. Basicamente, o problema é que, se você tiver solicitações simultâneas do mesmo usuário (mesma sessão), o seguinte é possível:

  1. A solicitação 1 abre a sessão para realizar uma operação semi-longa
  2. O pedido 2 abre a sessão
  3. Solicitação 2 Remove os dados da sessão para realizar o "logout" ou similar
  4. Solicitar 2 salvamentos
  5. A solicitação 1 salva, o que faz com que a sessão nunca tenha sido desconectada

Eu adicionei um caso de teste para esta falha em cless / sessions @ f84abeda17de0b4fcd72d277412f3d3192f206f2

A maneira mais direta de corrigir isso seria introduzir bloqueios no nível do sistema de arquivos. No entanto, o golang não tem uma forma de plataforma cruzada para bloquear arquivos. Ele expõe flock em syscall mas isso só funciona se o sistema operacional suportar. Eu acredito que o comportamento do flock também pode ser diferente em diferentes unixes, embora eu não tenha certeza de que seja esse o caso. Outro problema com o flock é que ele pode não funcionar no NFS.

Uma solução totalmente diferente seria manter um mapa de bloqueios no próprio objeto FilesystemStore . Isso tem outro conjunto de desvantagens: você não pode ter vários processos acessando as mesmas sessões do sistema de arquivos e não pode criar vários armazenamentos para a mesma sessão do sistema de arquivos em um único aplicativo. No entanto, essas duas coisas já são impossíveis de fazer sem causar problemas.

No final, acho que a melhor solução é manter um mapa de bloqueios no objeto da loja, porque todas as desvantagens nesse cenário podem ser devidamente documentadas e você pode responder sobre o comportamento sendo o mesmo em diferentes sistemas.

Outros back-ends de armazenamento baseados em FilesystemStore podem copiar essa falha (percebi esse problema ao revisar o código do Redistore para um projeto meu boj / redistore # 2)

bug stale

Todos 23 comentários

Em vez de bloqueio, que tal um sistema baseado em transações? O objeto Session pode conter um campo lastModified. Quando você tenta salvar a sessão de volta no armazenamento de sessão, ele retorna um erro indicando que ela foi modificada desde a última leitura. O programador pode então escolher tentar novamente obtendo a Sessão novamente.

Isso poderia funcionar, tem a vantagem de não haver bloqueios que precisem ser limpos e não haver chance de bloqueios. Não tenho certeza se é sempre possível para os desenvolvedores fazer uma nova tentativa significativa, dependendo do que a solicitação já fez. Acho que o bloqueio é definitivamente a opção mais segura, mas as transações certamente poderiam funcionar se a API documentasse claramente a chance de falha e se os desenvolvedores tratassem esses erros diligentemente.

Dito isso, mesmo se FilesystemStore usar transações, acho que a API deve ser preparada para back-ends de armazenamento que _do_ usam bloqueio, mas isso significaria proibir a chamada de session.Save() mais de uma vez _ou_ introduzir um novo session.Release() . Qual você prefere ou prefere que nenhum back-end de armazenamento use bloqueio?

Seguindo este problema, pois impacta o RediStore. Obrigado pela informação @cless

Eu realmente não tenho nenhuma preferência no momento a não ser fazer a coisa certa a longo prazo :) Claro, eu também gostaria de manter a API existente. Além disso, adicionar bloqueios em todo o lugar introduz muita complexidade adicional e sobrecarga, o que também seria bom evitar.

Você tem algum exemplo de outras estruturas de sessão que resolvam esse problema? Eu ficaria curioso para ver o que eles fazem.

Eu sei que o manipulador de sessão php padrão usa bloqueio baseado em sistema de arquivos (no tarball php 5.4 que acabei de verificar, está no arquivo ext/session/mod_files.c . Ele usa flock que é fornecido por ext/standard/flock_compat.c ).

Não tenho certeza se php é um bom exemplo devido à sua reputação, mas infelizmente é o único que conheço no momento. Vou tentar dar uma olhada para ver se consigo encontrar outras estruturas e ver como elas resolvem o problema.

Eu ficaria curioso para ver como Flask, Pyramid ou Django lidam com essas coisas. Vou dar uma olhada neles se tiver tempo. Rails também seria interessante se alguém estiver familiarizado com essa base de código.

Ambos Pyramid e Flask parecem estar usando um cookie para armazenar os dados, eu suspeito que nenhum deles lida com condições de corrida. Eu li apenas brevemente a documentação, então posso muito bem estar errado.

O Django oferece suporte a dados de sessão do lado do servidor, portanto, examinarei esse código um pouco.

O backend de sessão padrão do Django é o banco de dados e este usa a camada de abstração do banco de dados Django, com a qual não estou familiarizado, por isso é difícil interpretar. No entanto, encontrei este comentário no back-end do sistema de arquivos:

        # 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.

Isso parece sugerir que eles se certificam de que o conteúdo do arquivo de sessão nunca seja mutilado, mas não que nenhuma condição de corrida possa acontecer. Se alguém tiver experiência em Django seria bom ver um caso de teste como cless / sessions @ f84abed
Stackoverflow também parece indicar que o Django pode ter condições de corrida nas sessões: http://stackoverflow.com/search?q=django+session+race+condition

Tudo bem, o FileSystemStore já tem um mutex de granulação grossa para evitar a corrupção do armazenamento de sessão. Não tenho certeza de quanta proteção adicional vale a pena colocar na biblioteca, porque adicionaria muito overhead para cada solicitação, apenas para tornar as coisas mais consistentes para algumas solicitações. Certamente, o cenário aqui é viável, mas acho que provavelmente é muito complicado de lidar em um sentido geral. Posso ver que há muitos casos em que o que acontece agora é totalmente normal, enquanto em algumas aplicações pode ser um problema. Suspeito que, na maioria das vezes, você só teria uma solicitação durante o voo para uma determinada sessão.

Realmente é um problema que está presente em qualquer recurso da web, o mesmo aconteceria se você tivesse um GET seguido de um PUT baseado na informação, as coisas podem ter mudado nesse meio tempo.

De qualquer forma, esses são meus pensamentos no momento, mas estou feliz em discutir mais.

Parece que, a longo prazo, isso é mais um problema de domínio de aplicativo.

Dado o cenário postado, se a solicitação 1 deve ser executada por tempo suficiente para que a solicitação 2 possa acontecer em paralelo (digamos, um aplicativo de página única escrito em Angular.js, ou um aplicativo Node.js assíncrono atingindo alguma API), parece que a solicitação de longa execução deve ser desacoplada da sessão e considerada um processo de trabalho independente nesse ponto.

Nada disso aborda diretamente o problema, é claro, mas como Kisielk mencionou, fazer qualquer tipo de bloqueio adiciona muita sobrecarga para o que parece ser um caso marginal.

aqui estão alguns exemplos do mundo real do impacto das condições de corrida de sessão. Os bugs são difíceis de depurar, aparecem aparentemente ao acaso e são um incômodo para desenvolvedores e usuários. Dado um aplicativo suficientemente grande que faz uso extensivo do ajax, essas condições de corrida aparecerão mais cedo ou mais tarde.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g@

Eu li todo o tópico no EllisLab / CodeIgniter # 1746 que lida com um problema semelhante, mas o problema é complicado pelo fato de que o CodeIgniter forçosamente regenera o id da sessão de vez em quando e a maior parte da discussão gira em torno deste fato .

Eu entendo sua relutância em alterar as sessões de maneira incompatível ou em introduzir diferenças sutis na API que podem quebrar os aplicativos existentes. No entanto, ainda acho que deve haver algum bloqueio opcional envolvido. Que tal adicionar duas funções à API da loja como esta:

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
}

O uso dessas funções seria totalmente opcional (embora eu pessoalmente encoraje o bloqueio de todas as solicitações que serão gravadas na sessão) e não tem impacto sobre os aplicativos existentes.

Há um problema com a interface de bloqueio proposta: se o bloqueio for mantido em um banco de dados como o MySQL e sua conexão com o banco de dados cair após você adquirir o bloqueio, você nunca poderá liberá-lo e sua sessão será efetivamente travada. Os bloqueios devem expirar de alguma forma, mas não tenho certeza de como isso deve ser exposto ao desenvolvedor.

Desculpe ter perdido a resposta de Boj quando postei a minha:
É importante ter em mente que solicitações de longa duração não são um requisito para acionar uma condição de corrida. O sono de 500 ms em meu caso de teste existe apenas para garantir que a condição de corrida seja acionada. No mundo real, essas condições de corrida ocorrerão para solicitações "curtas" também, apenas com menos frequência, e é exatamente isso que as torna difíceis de depurar.

Você também deve ter em mente que sob carga alta, mesmo solicitações curtas podem levar algum tempo para serem executadas.

@cless Bons pontos.

Eu gosto de suas alterações de interface propostas.

A expiração de seu bloqueio seria relativamente fácil de implementar para RediStore, Redis tem um comando EXPIRE que permite que um TTL seja definido para uma chave.

Este tipo de bloqueio refinado ainda não seria bastante ineficiente para muitos tipos de armazenamentos de sessão? E a expiração também é um problema.

Parece que parte do problema é que Salvar pode ser bem-sucedido mesmo se a sessão durar mais; não seria um longo caminho para resolver o problema se esse não fosse o caso?

@kisielk , você pode dar exemplos de lojas que seriam ineficientes? A maioria dos bancos de dados de valor-chave tem alguma forma de operações atômicas que permitem a criação de bloqueios. Os bancos de dados SQL geralmente têm acesso ao bloqueio baseado em linha (embora eu deva dizer que não estou familiarizado com os detalhes aqui).
O método de bloqueio de armazenamentos de valores-chave como redis requer várias viagens de ida e volta ao banco de dados, talvez seja isso o que você quis dizer?
Há um problema com os armazenamentos de sistema de arquivos porque go não fornece uma maneira de plataforma cruzada para acessar bloqueios de arquivo. Suponho que o cgo permitiria que você criasse um compatível com as plataformas principais, mas essa é provavelmente uma solução que eu evitaria.

Não acho que salvar quando a sessão não existe mais é um problema real, a menos que você esteja falando sobre remover a sessão atual e substituí-la por uma nova sessão para evitar a fixação da sessão, mas para ser honesto, esse é um problema totalmente diferente.

@boj , o problema é que o programador precisa de alguma forma para verificar se o bloqueio ainda não expirou antes de tentar salvar. Qual deve ser o tempo de expiração de uma fechadura?

Essa é uma maneira possível de fazer isso, mas não tenho certeza se gosto muito. Você está dependendo de o relógio não saltar para a frente ou para trás de repente e essa não é uma suposição segura a se fazer:

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 Todo o seu comentário resume o que parece ser as preocupações de @kisielk . X faz isso, Y faz aquilo, Z pode não ser possível, a menos que alguns extremos estejam envolvidos. Embora as condições de corrida não sejam desejáveis, dados os raros casos em que podem acontecer, pelo menos o design atual do gorila / sessão é muito simples e segue o elemento da filosofia menos surpresa.

Poderia muito bem ser simples implementar a parte da interface do gorila / sessões como você propôs, no entanto, você cria um cenário onde pode ou não ser capaz de trocar facilmente os back-ends, a menos que implementem Lock / Release de uma forma igualmente fácil e de maneira eficiente. Parece haver muitos questionamentos em relação ao que você propõe, sendo o principal "o back-end pode até mesmo implementar isso sem recorrer à programação hackeada?", Seguido por "Eu tentei trocar de FileSystemStore para FooBarStore, mas não parecem implementar Lock / Release e meu programa não se comporta como esperado. "

Há um bom artigo sobre as nuances do bloqueio por sessão aqui:

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

Como já concluímos aqui, o tempo limite para bloqueios se algo der errado é um dos principais problemas na implementação desse tipo de esquema. Isso requer mais reflexão e é muito dependente do backend

Quanto à implementação de Bloqueio / Liberação, poderíamos tornar a implementação de bloqueio por sessão opcional, tendo uma interface separada. O back-end pode então ser declarado em relação a essa interface e, se ele não o implementar, podemos fornecer uma implementação padrão usando bloqueios na memória.

Outro problema, é claro, é que os usuários da biblioteca precisarão atualizar seu código para usar o Lock and Release, mas acho que sem isso eles teriam o mesmo comportamento que têm agora.

"poderíamos fornecer uma implementação padrão usando bloqueios na memória"

Isso não faria sentido em um ambiente com vários servidores, onde as solicitações têm balanceamento de carga.

Gosto da ideia de fornecer uma interface totalmente diferente para bloqueio, porque permite mais liberdade para implementá-la corretamente sem alterar o comportamento do código existente que usa sessões.

Como @boj observou, você não pode realmente fornecer bloqueios de memória em um armazenamento de banco de dados de nenhuma maneira significativa. A responsabilidade de fornecer a funcionalidade de bloqueio provavelmente deve ser do desenvolvedor da loja. Se a interface de bloqueios estiver ausente, você ainda poderá retornar erros quando a sessão tentar usá-los. Isso realmente não deve ser um problema, se os armazenamentos padrão fornecem bloqueio, então outros desenvolvedores seguirão o exemplo e os usuários serão minimamente, ou nada, inconvenientes.

Os bloqueios de memória ainda podem fazer sentido em algumas lojas, e acho que FilesystemStore é um bom candidato para eles. Realisticamente, você não usará o armazenamento do sistema de arquivos em uma situação de balanceamento de carga (embora teoricamente possível com sistemas de arquivos de rede). Os bloqueios do sistema de arquivos parecem preferíveis, mas parecem ser difíceis de implementar entre plataformas em go e, além disso, também não têm garantia de funcionamento com NFS.

@boj : concordou, mas como @cless aponta, isso é baseado no tipo de loja que você tem. FilesystemStore já conta com um RWMutex para evitar modificações simultâneas do armazenamento de sessão. Espero que a maioria dos back-ends implementem a interface de bloqueio pelo menos eventualmente, mas ter um padrão permitiria a um desenvolvedor lançar o que é adequado para um ambiente de servidor único nesse meio tempo. FilesystemStore seria um desses até que a situação de bloqueio entre plataformas nas bibliotecas Go melhore.

Eu concordo totalmente com @cless e quero enfatizar a importância do bloqueio de sessão e plataformas maduras (como PHP) implementá-lo em seus armazenamentos de sessão. Em arquivos, usando a escala 'flock' e no memcache usando o valor de retorno da operação 'add' e uma chave extra com um sufixo '.lock' e um conjunto expirar. Também concordo com a abordagem de @kisielk de ter uma nova interface (ter as funções 'Bloquear' e 'Liberar' faz sentido. Podemos fazer isso, por favor? Go deve ser a linguagem para obter alto desempenho e confiabilidade. Eu sou dispostos a contribuir com algumas implementações (FileSystem, Redis e Memcache), por exemplo. Observe que precisamos de algumas novas opções de configuração:

  • spinLockWait: milissegundos para hibernar entre as tentativas de adquirir o bloqueio (padrão: 150)
  • lockMaxWait: segundos para esperar por um bloqueio (padrão: 30)
  • lockMaxAge: segundos em que um bloqueio pode ser mantido antes de ser liberado automaticamente (padrão: lockMaxWait)

Este problema foi marcado automaticamente como obsoleto porque não teve uma atualização recente. Ele será fechado automaticamente em alguns dias.

Esta página foi útil?
0 / 5 - 0 avaliações