Sessions: Racebedingung im FilesystemStore

Erstellt am 11. Juni 2013  ·  23Kommentare  ·  Quelle: gorilla/sessions

Es gibt eine Rennbedingung in FilesystemStore , die ich beheben möchte, aber ich möchte Ihre Eingabe, bevor ich fortfahre und es tue. Grundsätzlich besteht das Problem darin, dass bei gleichzeitigen Anforderungen desselben Benutzers (derselben Sitzung) Folgendes möglich ist:

  1. Anforderung 1 öffnet die Sitzung, um eine halblange Operation auszuführen
  2. Anfrage 2 öffnet die Sitzung
  3. Anforderung 2 Entfernt Sitzungsdaten, um "Abmelden" oder ähnliches durchzuführen
  4. Anfrage 2 speichert
  5. Anforderung 1 speichert, wodurch es so aussieht, als ob die Sitzung nie abgemeldet wurde

Ich habe einen Testfall für diesen Fehler unter cless / session @ f84abeda17de0b4fcd72d277412f3d3192f206f2 hinzugefügt

Der einfachste Weg, dies zu beheben, wäre die Einführung von Sperren auf Dateisystemebene. Golang bietet jedoch keine plattformübergreifende Möglichkeit zum Sperren von Dateien. Es macht flock in syscall verfügbar, aber das funktioniert nur, wenn das Betriebssystem dies unterstützt. Ich glaube, dass das Verhalten der Herde auch bei verschiedenen Unixen unterschiedlich sein kann, obwohl ich nicht sicher bin, ob dies der Fall ist. Ein weiteres Problem mit Flock ist, dass es unter NFS möglicherweise nicht funktioniert.

Eine völlig andere Lösung wäre, eine Karte mit Sperren im FilesystemStore -Objekt selbst zu führen. Dies hat noch weitere Nachteile: Sie können nicht mehrere Prozesse auf dieselben Dateisystemsitzungen zugreifen lassen und Sie können nicht mehrere Speicher für dieselbe Dateisystemsitzung in einer einzigen Anwendung erstellen. Beides ist jedoch bereits unmöglich, ohne Probleme zu verursachen.

Letztendlich denke ich, dass die beste Lösung darin besteht, eine Karte der Sperren im Speicherobjekt zu führen, da alle Nachteile in diesem Szenario ordnungsgemäß dokumentiert werden können und Sie darauf antworten können, dass das Verhalten auf verschiedenen Systemen gleich ist.

Andere Speicher-Backends, die auf FilesystemStore basieren, kopieren diesen Fehler möglicherweise (ich habe dieses Problem beim Überprüfen des Redistore-Codes für ein Projekt von mir, boj / redistore # 2, festgestellt).

bug stale

Alle 23 Kommentare

Was ist mit einem transaktionsbasierten System, anstatt zu sperren? Das Session-Objekt kann ein lastModified-Feld enthalten. Wenn Sie versuchen, die Sitzung wieder im Sitzungsspeicher zu speichern, wird ein Fehler zurückgegeben, der angibt, dass sie seit dem letzten Lesen geändert wurde. Der Programmierer kann dann versuchen, es erneut zu versuchen, indem er die Sitzung erneut abruft.

Das könnte funktionieren, es hat den Vorteil, dass es keine zu bereinigenden Schlösser gibt und keine Möglichkeit für Deadlocks besteht. Ich bin mir nicht sicher, ob es Entwicklern immer möglich ist, einen sinnvollen Wiederholungsversuch durchzuführen, je nachdem, was die Anfrage bereits getan hat. Ich denke, Sperren ist definitiv die sicherere Option, aber Transaktionen könnten sicherlich funktionieren, wenn die API die Fehlerwahrscheinlichkeit eindeutig dokumentiert und Entwickler diese Fehler sorgfältig behandeln.

Das heißt, selbst wenn FilesystemStore Transaktionen verwendet, sollte die API meiner Meinung nach für Speicher-Backends vorbereitet sein, die _do_ sperren, aber das würde bedeuten, dass entweder das Aufrufen von session.Save() mehr als einmal nicht zugelassen wird oder ein neues session.Release() . Welches würden Sie bevorzugen, oder würden Sie es vorziehen, dass keine Speicher-Backends Sperren verwenden?

Folgen Sie diesem Problem, da es Auswirkungen auf RediStore hat. Danke für die Information @cless

Ich habe im Moment keine andere Präferenz als auf lange Sicht das Richtige zu tun :) Natürlich möchte ich auch die vorhandene API behalten. Das Hinzufügen von Sperren überall führt zu einer Menge zusätzlicher Komplexität und Overhead, was ebenfalls zu vermeiden wäre.

Haben Sie Beispiele für andere Sitzungs-Frameworks, die dieses Problem lösen? Ich wäre gespannt, was sie tun.

Ich weiß, dass der Standard-PHP-Session-Handler Dateisystem-basiertes Sperren verwendet (im PHP 5.4-Tarball habe ich gerade überprüft, dass dies in der Datei ext/session/mod_files.c . Er verwendet Flock, der von ext/standard/flock_compat.c bereitgestellt wird).

Ich bin mir nicht sicher, ob PHP angesichts seines guten Rufs ein gutes Beispiel ist, aber ich fürchte, es ist das einzige, das mir derzeit bekannt ist. Ich werde versuchen, mich umzuschauen, um zu sehen, ob ich andere Frameworks finden und wie sie das Problem lösen.

Ich wäre gespannt, wie Flask, Pyramid oder Django mit diesen Dingen umgehen. Ich werde sie mir ansehen, wenn ich Zeit habe. Rails wäre auch interessant, wenn jemand mit dieser Codebasis vertraut wäre.

Sowohl Pyramid als auch Flask scheinen einen Cookie zum Speichern der Daten zu verwenden. Ich vermute, dass keiner der beiden die Rennbedingungen behandelt. Ich habe die Dokumentation nur kurz gelesen, damit ich mich sehr gut irren kann.

Django unterstützt serverseitige Sitzungsdaten, daher werde ich mir diesen Code gleich ansehen.

Das Standard-Django-Sitzungs-Backend ist die Datenbank. Dabei wird die Django-Datenbank-Abstraktionsschicht verwendet, mit der ich nicht vertraut bin, sodass ich sie nur schwer interpretieren kann. Ich habe diesen Kommentar jedoch im Backend des Dateisystems gefunden:

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

Dies scheint darauf hinzudeuten, dass sie sicherstellen, dass der Inhalt der Sitzungsdatei niemals entstellt wird, aber nicht, dass keine Rennbedingungen auftreten können. Wenn jemand Erfahrung mit Django hat, wäre es schön, einen Testfall wie cless / session @ f84abed zu sehen
Stackoverflow scheint auch darauf hinzudeuten, dass Django in den Sessions möglicherweise Rennbedingungen hat: http://stackoverflow.com/search?q=django+session+race+condition

Okay, FileSystemStore verfügt bereits über einen grobkörnigen Mutex, um die Beschädigung des Sitzungsspeichers zu verhindern. Ich bin mir nicht sicher, wie viel zusätzlichen Schutz es wert ist, in die Bibliothek eingefügt zu werden, da dies für jede Anforderung einen hohen Overhead bedeuten würde, nur um die Konsistenz für einige Anforderungen zu verbessern. Sicher ist das Szenario hier machbar, aber ich denke, es ist wahrscheinlich zu kompliziert, um es allgemein zu behandeln. Ich kann sehen, dass es viele Fälle gibt, in denen das, was jetzt passiert, völlig in Ordnung ist, während es in einigen Anwendungen ein Problem sein könnte. Ich vermute, dass Sie die meiste Zeit ohnehin nur eine Anfrage während des Fluges für eine bestimmte Sitzung haben würden.

Wirklich, es ist ein Problem, das in jeder Webressource vorhanden ist. Dasselbe würde passieren, wenn Sie ein GET gefolgt von einem PUT basierend auf den Informationen hätten. Möglicherweise haben sich die Dinge in der Zwischenzeit geändert.

Wie auch immer, das sind meine Gedanken im Moment, aber ich bin froh, weiter darüber zu diskutieren.

Auf lange Sicht scheint dies eher ein Problem mit der Anwendungsdomäne zu sein.

Wenn angesichts der veröffentlichten Szenario-Cless erwartet wird, dass Anforderung 1 so lange ausgeführt wird, dass Anforderung 2 parallel ausgeführt werden kann (z. B. eine in Angular.js geschriebene Einzelseitenanwendung oder eine asynchrone Node.js-App, die auf eine API trifft), scheint dies der Fall zu sein Langfristige Anforderungen sollten von der Sitzung entkoppelt und zu diesem Zeitpunkt als unabhängiger Arbeitsprozess betrachtet werden.

Nichts davon spricht das Problem natürlich direkt an, aber wie Kisielk erwähnte, bedeutet das Sperren jeglicher Art viel Aufwand für einen scheinbar marginalen Fall.

Hier sind einige Beispiele aus der Praxis für die Auswirkungen der Sitzungsbedingungen. Die Fehler sind schwer zu debuggen, treten scheinbar zufällig auf und sind sowohl für Entwickler als auch für Benutzer ärgerlich. Bei einer ausreichend großen Anwendung, die Ajax in großem Umfang nutzt, werden diese Rennbedingungen früher oder später angezeigt.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g@

Ich habe den gesamten Thread in EllisLab / CodeIgniter # 1746 gelesen, der sich mit einem ähnlichen Problem befasst, aber ihr Problem wird durch die Tatsache kompliziert, dass CodeIgniter die Sitzungs-ID von Zeit zu Zeit zwangsweise neu generiert und sich der größte Teil der Diskussion dort um diese Tatsache dreht .

Ich verstehe Ihre Zurückhaltung, Sitzungen auf inkompatible Weise zu ändern oder subtile Unterschiede in der API einzuführen, die vorhandene Anwendungen beschädigen könnten. Ich denke jedoch immer noch, dass es einige optionale Sperren geben sollte. Was ist mit dem Hinzufügen von zwei Funktionen zur Store-API wie folgt:

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
}

Die Verwendung dieser Funktionen wäre völlig optional (obwohl ich persönlich empfehlen würde, jede Anforderung, die in die Sitzung geschrieben werden soll, zu sperren) und hätte keine Auswirkungen auf vorhandene Anwendungen.

Es gibt ein Problem mit der vorgeschlagenen Sperrschnittstelle: Wenn die Sperre in einer Datenbank wie MySQL gehalten wird und Ihre Datenbankverbindung nach dem Erwerb der Sperre unterbrochen wird, können Sie sie niemals freigeben und Ihre Sitzung ist effektiv blockiert. Sperren sollten in irgendeiner Weise ablaufen, aber ich bin nicht ganz sicher, wie das für den Entwickler verfügbar sein soll.

Entschuldigung, ich habe Bojs Antwort verpasst, als ich meine gepostet habe:
Es ist wichtig zu beachten, dass lang laufende Anforderungen keine Voraussetzung sind, um eine Rennbedingung auszulösen. Der 500-ms-Schlaf in meinem Testfall dient nur dazu, sicherzustellen, dass die Rennbedingung ausgelöst wird. In der realen Welt treten diese Rennbedingungen auch für "kurze" Anfragen auf, nur seltener, und genau das macht es schwierig, sie zu debuggen.

Sie müssen auch berücksichtigen, dass die Ausführung selbst kurzer Anforderungen unter hoher Last einige Zeit in Anspruch nehmen kann.

@cless Gute Punkte.

Ich mag Ihre vorgeschlagenen Schnittstellenänderungen.

Der Ablauf Ihrer Sperre wäre für RediStore relativ einfach zu implementieren. Redis verfügt über einen EXPIRE- Befehl, mit dem eine TTL für einen Schlüssel festgelegt werden kann.

Wäre diese Art der feinkörnigen Sperrung für viele Arten von Sitzungsspeichern nicht immer noch ziemlich ineffizient? Und der Ablauf ist auch ein Problem.

Es scheint ein Teil des Problems zu sein, dass das Speichern auch dann erfolgreich sein kann, wenn die Sitzung länger besteht. Würde dies nicht einen großen Beitrag zur Lösung des Problems leisten, wenn dies nicht der Fall wäre?

@kisielk , können Sie Beispiele für Geschäfte nennen, die ineffizient wären? Die meisten Schlüsselwertdatenbanken verfügen über atomare Operationen, mit denen Sie Sperren erstellen können. SQL-Datenbanken haben normalerweise Zugriff auf zeilenbasiertes Sperren (obwohl ich sagen muss, dass ich mit den Details hier nicht vertraut bin).
Die Sperrmethode von Schlüsselwertspeichern wie redis erfordert jedoch mehrere Roundtrips zur Datenbank. Vielleicht haben Sie das so gemeint?
Es gibt ein Problem mit Dateisystemspeichern, da go uns keine plattformübergreifende Möglichkeit bietet, auf Dateisperren zuzugreifen. Ich nehme an, mit cgo können Sie eine erstellen, die die Hauptplattformen unterstützt, aber das ist wahrscheinlich eine Lösung, die ich vermeiden würde.

Ich denke nicht, dass das Speichern, wenn die Sitzung nicht mehr existiert, ein echtes Problem ist, es sei denn, Sie möchten die aktuelle Sitzung entfernen und durch eine neue Sitzung ersetzen, um eine Sitzungsfixierung zu verhindern, aber um ehrlich zu sein, ist dies ein ganz anderes Problem.

@boj , das Problem ist, dass der Programmierer eine Möglichkeit benötigt, um zu überprüfen, ob eine Sperre noch nicht abgelaufen ist, bevor er versucht zu speichern. Wie sollte die Ablaufzeit für ein Schloss sein?

Dies ist ein möglicher Weg, aber ich bin mir nicht sicher, ob es mir sehr gefällt. Sie sind darauf angewiesen, dass die Uhr nicht plötzlich vorwärts oder rückwärts springt, und das ist keine sichere Annahme:

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 Dein ganzer Kommentar fasst zusammen, was @kisielks Sorgen zu sein

Es könnte sehr einfach sein, den von Ihnen vorgeschlagenen Schnittstellenteil von Gorilla / Sessions zu implementieren. Sie erstellen jedoch ein Szenario, in dem Sie Backends möglicherweise einfach austauschen können oder nicht, es sei denn, sie implementieren Lock / Release in einer ebenso einfachen Implementierung und effiziente Weise. Es scheint zu viele Was-wäre-wenns in Bezug auf das zu geben, was Sie vorschlagen. Das wichtigste ist: "Kann das Backend dies überhaupt implementieren, ohne auf hackige Programmierung zurückzugreifen?", Gefolgt von "Ich habe versucht, von FileSystemStore zu FooBarStore zu wechseln, aber das tut es nicht." scheinen Lock / Release zu implementieren und mein Programm verhält sich nicht wie erwartet. "

Hier gibt es einen guten Artikel über die Nuancen der Sperrung pro Sitzung:

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

Wie wir hier bereits festgestellt haben, ist das Zeitlimit für Sperren, wenn etwas schief geht, eines der Hauptprobleme bei der Implementierung dieser Art von Schema. Dies erfordert mehr Überlegungen und ist sehr backendabhängig

Bei der Implementierung von Lock / Release können wir die Implementierung der Sperre pro Sitzung optional machen, indem wir eine separate Schnittstelle haben. Das Backend könnte dann gegen diese Schnittstelle aktiviert werden, und wenn es es nicht implementiert, könnten wir eine Standardimplementierung unter Verwendung speicherinterner Sperren bereitstellen.

Ein weiteres Problem ist natürlich, dass Benutzer der Bibliothek ihren Code aktualisieren müssen, um Lock and Release verwenden zu können, aber ich denke, ohne dies hätten sie das gleiche Verhalten wie jetzt.

"Wir könnten eine Standardimplementierung mithilfe von In-Memory-Sperren bereitstellen."

Dies wäre in einer Umgebung mit mehreren Servern, in der Anforderungen lastausgeglichen sind, bedeutungslos.

Ich mag die Idee, eine völlig andere Schnittstelle zum Sperren bereitzustellen, da Sie dadurch mehr Freiheit haben, sie korrekt zu implementieren, ohne das Verhalten von vorhandenem Code zu ändern, der Sitzungen verwendet.

Wie @boj feststellte, können Sie Speichersperren für einen Datenbankspeicher nicht auf sinnvolle Weise bereitstellen. Die Last der Bereitstellung der Sperrfunktionalität sollte wahrscheinlich beim Store-Entwickler liegen. Wenn die Sperrschnittstelle fehlt, können Sie dennoch Fehler zurückgeben, wenn die Sitzung versucht, sie zu verwenden. Das sollte wirklich kein Problem sein, wenn die Standardspeicher Sperren bereitstellen, werden andere Entwickler nachziehen und die Benutzer werden, wenn überhaupt, nur minimal belästigt.

Speichersperren können in einigen Geschäften immer noch Sinn machen, und ich denke, dass FilesystemStore ein guter Kandidat für sie ist. Realistisch gesehen werden Sie den Dateisystemspeicher nicht in einer Situation mit Lastenausgleich verwenden (obwohl dies theoretisch mit Netzwerkdateisystemen möglich ist). Dateisystem-Sperren scheinen vorzuziehen, aber es scheint schwierig zu sein, plattformübergreifend zu implementieren, und außerdem kann nicht garantiert werden, dass sie mit NFS funktionieren.

@boj : stimmte zu, aber wie @cless betont , hängt das von der Art des Geschäfts ab, das Sie haben. FilesystemStore verwendet bereits einen RWMutex, um gleichzeitige Änderungen des Sitzungsspeichers zu verhindern. Ich gehe davon aus, dass die meisten Backends die Sperrschnittstelle zumindest irgendwann tatsächlich implementieren würden, aber eine Standardeinstellung würde es einem Entwickler ermöglichen, eine Version freizugeben, die in der Zwischenzeit für eine Einzelserverumgebung geeignet ist. FilesystemStore wäre einer davon, bis die plattformübergreifende Sperrsituation in Go-Bibliotheken besser wird.

Ich stimme @cless voll und ganz zu und möchte betonen, wie wichtig das Sperren von Sitzungen ist und dass ausgereifte Plattformen (wie PHP) es in ihren Sitzungsspeichern implementieren. Bei Dateien mit dem Syscall 'flock' und im Memcache mit dem Rückgabewert der Operation 'add' und einem zusätzlichen Schlüssel mit dem Suffix '.lock' und einem Ablaufsatz. Ich stimme auch dem Ansatz von @kisielk zu, eine neue Benutzeroberfläche zu haben (die Funktionen 'Sperren' und 'Freigeben' sind sinnvoll. Können wir das bitte haben? Go sollte die Sprache sein, in der hohe Leistung und Zuverlässigkeit erreicht werden sollen bereit, einige Implementierungen (z. B. FileSystem, Redis und Memcache) beizutragen. Beachten Sie, dass wir einige neue Konfigurationsoptionen benötigen:

  • spinLockWait: Millisekunden zwischen den Versuchen, die Sperre zu erlangen (Stand: 150)
  • lockMaxWait: Sekunden, um auf eine Sperre zu warten (Standard: 30)
  • lockMaxAge: Sekunden, in denen eine Sperre beibehalten werden kann, bevor sie automatisch aufgehoben wird (Standard: lockMaxWait)

Dieses Problem wurde automatisch als veraltet markiert, da es kein aktuelles Update erhalten hat. Es wird in ein paar Tagen automatisch geschlossen.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen