多个目录的原子复制

多个目录的原子复制

该问题与 Web 应用程序的部署有关。

简介(可以跳过)

我正在使用 django,而我的托管服务提供商设置其 django 支持的方式最终导致 web 应用程序分散在至少三个位置:

  • 实际应用代码/something/<my_apps>/
  • 网站设置/网址/模板/something/<my_site>/
  • css、javascript 和其他“媒体”/something_else/media/

因此,当我部署/升级网站时,我需要一次更新几个目录。

实际问题:

有没有办法实现原子文件复制?我绝不是 Linux 系统的“专家”,所以请原谅我的无知。

复制操作涉及几个目录树,基本上是两个或三个:

copy _tree1 to tree1
copy _tree2 to tree2

原子的意思是:

  • 要么全部复制,要么完全不复制。它绝不应该处于部分复制但部分失败的状态。
  • 它会在尽可能短的时间内完成。理想情况下,系统不应该在复制过程中看到任何时间点,它要么看到文件的旧版本,要么看到新版本;在任何时间点,它都不应该看到文件 A 的旧版本,而应该看到文件 B 的新版本。如果这不可能,那么它应该不会超过几毫秒。

我的想法是拥有类似双缓冲的东西:我在一个暂存区准备好所有内容,例如,_tree_x然后将其复制移动到tree_x应该是一个原子操作,只需更改磁盘上的指针即可。

我认为在 Linux 中单个这样的复制移动操作是原子的(不是吗?),但我需要几个这样的操作也是原子的;我希望它们被视为单个移动操作。

答案1

我认为您在暂存区域方面的努力是正确的。我不知道有任何原子命令,但如果您暂存文件,然后使用脚本删除第一个目录并移动(而不是复制)第二个目录,并对这三个目录执行此操作,那么应该会非常快。

或者,您可能希望使用符号链接。这样,您可以大致获得:

/version/22/<my-apps>
/version/22/<my-site>
/version/22/<my-media>

并部署一个/version/23具有相同子目录的目录。然后,在实际文件将要去的地方(同样,为了提高速度,您需要一个脚本),您可以使用符号链接,这样当任何人访问最新页面时,他们都会获得最新版本(这一切都是透明的,他们不知道)。这样做的好处是,您的旧作品仍然存在,直到您决定删除它。[尽管,当然,版本控制系统是保存旧作品的更好选择。]

您必须检查 1)您是否可以运行脚本(并且以网络用户无法运行的方式!),以及 2)您是否可以使用符号链接(因为某些网络服务器配置为不遵循它们。)

答案2

也许我还没有完全想清楚,但为什么不将复制操作执行到新目录呢?完成后,将旧目录“mv”为另一个名称,将新目录“mv”为所需的名称。

从技术上来说这不是原子的,因为在一段时间内旧目录会被移动而新目录尚未到位,但这可能已经足够好了。

答案3

对于大型网站,可以通过使用多台服务器处理请求来执行网站更新。然后,您可以让一台服务器下线,对其进行更新,然后将其重新上线,对集群中的其他服务器重复此操作。

对于单一托管站点,通过将站点关闭的页面放入根文件夹中的 index.html,然后进行更改来关闭网站可能是有意义的。

如果您确实需要尽可能长时间地保持网站正常运行,我建议您执行以下操作:

  1. 原子副本不存在,但是重命名单个文件夹确实会以原子方式发生。通过将重命名放入脚本并运行该脚本,您可以快速地连续进行一系列重命名。您需要两倍于站点的磁盘空间才能在服务器上同时拥有前后文件夹。

  2. 这并不能解决您的问题 - 只是减少了曝光度。之前和之后的版本可能需要不同的数据库数据字段,因此 SQL 查询也需要运行。有人可能会在您更新的同时进行页面加载。网页加载的开始可能会加载更改之前的页面,而页面加载的最后部分可能会使用复制之后的文件。

答案4

对于阅读这个老问题的人来说,这是可能的,但它需要许多符号链接。

我的回答基于 Clinton Blackmore 的回答(目前接受的答案)。

多个目录(或多个文件)无法原子地更改。所以我们不能直接使用目录。可以使用 rename() 系统调用原子地更新单个文件(用新文件替换旧文件)。符号链接可以以相同的方式更新,使用mv -Tletmv不取消引用目标。

因此,如果我们有这些目录:

/srv/dirA/
/srv/dirB/
/srv/dirC/

我们可以让它们都成为指向其他三个目录的符号链接:

/srv/dirA -> /version/current/dirA
/srv/dirB -> /version/current/dirB
/srv/dirC -> /version/current/dirC

/version/current反过来,它只是指向当前版本目录的符号链接:

/version/current -> /version/22/

然后可以使用两个简单的命令更新整个 Web 应用程序,其中最后一个命令一次“替换”所有三个目录(它实际上并没有替换目录,它只替换这些目录指向的位置):

$ ln -s /version/23/ /version/next
$ mv -T /version/next /version/current

我还没有实际测试过,但它应该可以工作。该-T标志可能是一个非标准标志。作为替代方案,python -c "import os; os.rename('/version/next', '/version/current')"可以改用(也未经测试)。

我不知道性能影响会有多大,但如果你已经在运行 Django,我怀疑影响不会很大。我认为如果你以尽可能低的延迟提供大量静态文件(如 CDN),这可能会很重要,即使这样,也可能只会对性能产生很小的影响。简而言之,你不必担心性能。

请注意,有几个问题:Django 是一个服务器,不会在同一时间重新启动。为了真正实现原子性,Django 必须以一种完全跳过“当前版本”概念的方式进行设置。相反,您将从当前版本启动 Django 以供生产使用。对于更新,将启动下一个版本,然后重新启动服务器(我假设大多数 Web 服务器都有某种无需离线即可重新启动的方法),整个过程应该是原子的。但我不是这个领域的专家。

另一个陷阱(正如 Ptolemy 提到的)是在繁忙的服务器上,有些人看到的页面部分来自一个版本,部分来自另一个版本,这是由于缓存和页面加载期间在不同时间请求多个资源(资源加载之间可能间隔几秒钟)造成的。我认为这两者中,缓存将是最重要的,但也是最容易解决的。不过,我怀疑这在实践中不会成为什么大问题。

相关内容