Sessions: FilesystemStore中的竞争条件

创建于 2013-06-11  ·  23评论  ·  资料来源: gorilla/sessions

我打算修复FilesystemStore中的一个竞争条件,但是在继续之前,请您输入一下。 基本上,问题是,如果您有来自同一用户的并发请求(同一会话),则可能发生以下情况:

  1. 请求1打开会话以执行半长操作
  2. 请求2打开会话
  3. 请求2删除会话数据以执行“注销”或类似操作
  4. 请求2保存
  5. 请求1保存,就好像从未注销会话一样

我在cless / session @ f84abeda17de0b4fcd72d277412f3d3192f206f2中添加了一个针对此缺陷的

解决此问题的最直接的方法是在文件系统级别引入锁。 但是,golang没有跨平台的方法来进行文件锁定。 它确实在syscall中公开了flock ,但是只有在操作系统支持的情况下,它才起作用。 我相信在不同的Unix上,flock的行为也可能有所不同,尽管我不确定情况是否如此。 flock的另一个问题是它可能不适用于NFS。

完全不同的解决方案是在FilesystemStore对象本身中保留锁映射。 这还有另一个缺点:不能有多个进程访问同一文件系统会话,也不能在单个应用程序中为同一文件系统会话创建多个存储。 但是,这两个事情在不引起问题的情况下已经不可能完成。

最后,我认为最好的解决方案是在存储对象中保留锁映射,因为可以正确记录该情况下的所有缺点,并且您可以在不同系统上的行为相同时进行答复。

基于FilesystemStore的其他存储后端可能会复制此缺陷(在查看我的boj / redistore#2项目的Redistore代码时,我注意到此问题)

bug stale

所有23条评论

而不是锁定,基于事务的系统呢? 会话对象可以携带lastModified字段。 当您尝试将会话保存回会话存储时,它将返回错误,表明自上次读取以来已对其进行了修改。 然后,程序员可以选择通过再次获取Session来重试。

那可能行得通,它的优点是不需要清除锁,也没有死锁的机会。 我不确定开发人员是否总是有可能根据请求已完成的内容进行有意义的重试。 我认为锁定绝对是更安全的选择,但是如果API清楚地记录了失败的可能性并且开发人员努力地处理了这些错误,则事务肯定可以工作。

就是说,即使FilesystemStore使用事务,我认为API也应该为_do_使用锁定的存储后端做好准备,但这将意味着不允许多次调用session.Save() _引入新的session.Release() 。 您想要哪个,还是不希望存储后端使用锁定?

跟随此问题,因为它影响RediStore。 感谢您提供信息@cless

除了从长远来看做正确的事情之外,目前我没有任何其他偏好:)当然,我也想保留现有的API。 此外,到处添加锁会带来很多额外的复杂性和开销,这也很容易避免。

您是否有其他解决此问题的会话框架的示例? 我很想知道他们在做什么。

我知道默认的php会话处理程序使用基于文件系统的锁定(在php 5.4 tarball中,我刚刚检查过这是在文件ext/session/mod_files.c 。它使用ext/standard/flock_compat.c提供的flock)。

考虑到它的声誉,我不确定php是否是一个很好的例子,但是恐怕它是我目前所知道的唯一一个。 我将尝试环顾四周,看看是否可以找到其他框架,并看看它们如何解决该问题。

我很想知道Flask,Pyramid或Django如何处理这些事情。 如果有时间,我会看一看。 如果有人熟悉该代码库,Rails也会很有趣。

PyramidFlask似乎都在使用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已经具有一个粗粒度的互斥体以防止会话存储损坏。 我不确定应该在库中添加多少额外的保护,因为这会使每个请求增加很多开销,只是使某些请求的内容更加一致。 当然,这里的方案是可行的,但我认为从一般意义上讲,它可能太复杂了。 我可以看到在很多情况下现在发生的一切都很好,而在某些应用程序中这可能是个问题。 我怀疑大多数情况下,无论如何,对于给定的会话,您只会收到一个机上请求。

确实,这是任何Web资源中都存在的问题,如果您在基于该信息的GET之后加上PUT,则同样的事情也会发生,同时情况可能会有所变化。

无论如何,这些是我目前的想法,但我很高兴进一步讨论。

从长远来看,这似乎更多是一个应用程序领域的问题。

给定场景,如果请求1预期运行足够长的时间以至于请求2可以并行发生(例如,用Angular.js编写的单页应用程序,或者使用某些API的异步Node.js应用程序),长时间运行的请求应从会话中分离出来,并在此时应将其视为独立的工作程序。

当然,这些方法都不能直接解决这个问题,但是正如kisielk所提到的,对于似乎只是边缘情况的情况,进行任何形式的锁定都会增加大量开销。

这是一些真实的示例,说明会话竞赛条件的影响。 这些bug难以调试,似乎随机出现,并且对开发人员和用户都是烦人的。 在充分利用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偶尔会强制一次重新生成会话ID,所以他们的问题变得很复杂,那里的大多数讨论都围绕着这个事实。 。

我们了解您不愿意以不兼容的方式更改会话,也不愿意在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之类的数据库中,并且在您获得该锁定后数据库连接断开,那么您将永远无法释放它,并且会话实际上被死锁了。 锁应该以某种方式到期,但是我不完全确定应该如何向开发人员公开。

抱歉,发布我的照片时,我错过了Boj的回复:
重要的是要记住,长时间运行的请求不是触发竞争条件的必要条件。 我的测试用例中有500 ms的睡眠,只是为了确保触发竞争条件。 在现实世界中,这些争用条件也会发生在“短”请求上,发生频率降低了,这正是使它们难以调试的原因。

您还必须记住,在高负载下,即使是简短的请求也可能需要一些时间来执行。

@cless好点。

我喜欢您建议的界面更改。

您的锁定到期对于RediStore而言相对容易实现,Redis具有EXPIRE命令,该命令允许为密钥设置TTL。

对于许多类型的会话存储来说,这种细粒度的锁定仍然不是很有效吗? 到期也是一个问题。

问题的一部分似乎是,即使会话存在更长的时间,Save也可以成功,如果不是这样的话,它对解决问题是否有很大帮助?

@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可能无法实现。 尽管考虑到极少数情况可能会导致比赛条件不理想,但至少当前的大猩猩/赛段设计非常简单,并且遵循了至少令人惊讶的理念。

按照您的建议,实现大猩猩/会话的接口部分可能非常简单,但是,您会创建一个方案,在这种方案中,您可能会或可能不会轻易地换出后端,除非它们碰巧同样实现了锁定/释放。高效的方式。 关于您的建议,似乎有太多假设,最重要的是“后端是否可以在不依靠编程的情况下实现此目的?”,其次是“我试图从FileSystemStore交换到FooBarStore,但它没有似乎实现了“锁定/释放”功能,并且我的程序运行不正常。”

这里有一篇很好的文章介绍了每次会话锁定的细微差别:

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

正如我们已经在这里得出的结论一样,如果出现问题,锁的超时是实现这种方案的主要问题之一。 这需要更多的思考,并且非常依赖于后端

至于实现Lock / Release,我们可以通过具有单独的接口使每个会话的锁实现成为可选的。 然后可以针对该接口声明后端,如果不实现该接口,我们可以使用内存中锁提供默认实现。

当然,另一个问题是该库的用户将需要更新其代码以使用“锁定和释放”,但我想如果没有它们,他们将具有与现在相同的行为。

“我们可以提供使用内存锁的默认实现”

在请求负载均衡的多服务器环境中,这将毫无意义。

我喜欢为锁提供完全不同的接口的想法,因为它使您可以更自由地正确实现它,而无需更改使用会话的现有代码的行为。

正如@boj指出的那样,您实际上无法以任何有意义的方式在数据库存储上提供内存锁定。 提供锁定功能的负担可能应该由商店开发人员承担。 如果缺少锁接口,则会话尝试使用它们时仍可以返回错误。 这真的不是问题,如果默认存储提供了锁定,那么其他开发人员将效仿,并且对用户的影响很小,甚至根本不会带来麻烦。

内存锁在某些商店中仍然很有意义,我认为FilesystemStore是它们的不错选择。 实际上,您不会在负载平衡的情况下使用文件系统存储(尽管理论上可以使用网络文件系统)。 文件系统锁似乎更可取,但是它们似乎很难在跨平台中实现跨平台,此外,也不保证它们也可以与NFS一起使用。

@boj :同意,但是正如@cless指出的那样,这是基于您拥有的商店的类型。 FilesystemStore已经依赖RWMutex来防止同时修改会话存储。 我希望大多数后端实际上至少最终会实现锁定接口,但是拥有默认值将允许开发人员同时发布适合单服务器环境的版本。 在Go库中的跨平台锁定情况变得更好之前,FilesystemStore将是其中之一。

我完全同意@cless,并想强调会话锁定的重要性,而成熟的平台(例如PHP)在其会话存储中实现它。 在文件上,使用“ flock”系统调用,在内存缓存中,使用“ add”操作的返回值以及带有“ .lock”后缀和到期集的额外键。 我也同意@kisielk具有新接口的方法(具有“锁定”和“释放”功能很有意义。请问我们可以吗?Go应该是实现高性能和可靠性的语言。我是愿意贡献一些实现(例如FileSystem,Redis和Memcache),请注意,我们确实需要一些新的配置选项:

  • spinLockWait:两次尝试获取锁定之间的睡眠时间(默认值:150)
  • lockMaxWait:等待锁定的秒数(默认值:30)
  • lockMaxAge:在自动释放锁之前,可以保留该锁的秒数(默认值:lockMaxWait)

由于该问题尚未发现最新更新,因此已自动标记为陈旧。 几天后它将自动关闭。

此页面是否有帮助?
0 / 5 - 0 等级