通过基于Python的守护进程在NFS共享上执行文件I/O时的特殊注意事项?
我有一个基于Python的后台程序,它通过HTTP提供一个类似REST的接口,可以和一些命令行工具进行交互。这个工具的主要功能是接收请求,执行一些命令行操作,把处理后的数据保存到磁盘上,并把一些数据返回给请求者。在后台程序启动时,会启动一个额外的线程,这个线程会定期查看磁盘上的数据,并根据数据的内容进行一些清理工作。
如果保存数据的磁盘是本地的Linux磁盘,这一切都运行得很好。但如果换成NFS挂载的磁盘,后台程序刚开始运行时没问题,但随着时间的推移,NFS挂载的共享“消失”了,后台程序就无法通过像os.getcwd()
这样的调用来找到磁盘的位置。你会开始看到它记录一些错误,比如:
2011-07-13 09:19:36,238 INFO Retrieved submit directory '/tech/condor_logs/submit'
2011-07-13 09:19:36,239 DEBUG CondorAgent.post_submit.do_submit(): handler.path: /condor/submit?queue=Q2%40scheduler
2011-07-13 09:19:36,239 DEBUG CondorAgent.post_submit.do_submit(): submitting from temporary submission directory '/tech/condor_logs/submit/tmpoF8YXk'
2011-07-13 09:19:36,240 ERROR Caught un-handled exception: [Errno 2] No such file or directory
2011-07-13 09:19:36,241 INFO submitter - - [13/Jul/2011 09:19:36] "POST /condor/submit?queue=Q2%40scheduler HTTP/1.1" 500 -
这个未处理的异常表明后台程序无法再看到磁盘了。此时,任何尝试用os.getcwd()
来获取后台程序当前工作目录的操作都会失败。即使试图切换到NFS挂载的根目录/tech
,也会失败。与此同时,logger.logging.*
的方法却依然在高兴地把日志和调试信息写入位于NFS挂载共享的日志文件/tech/condor_logs/logs/CondorAgentLog
中。
磁盘显然还是可用的。在这个时间段内,还有其他基于C++的后台程序在这个共享上频繁地读写数据。
我在调试这个问题时遇到了瓶颈。因为在本地磁盘上能正常工作,所以代码的整体结构应该是没问题的,对吧?但在NFS挂载的共享上,我的代码和它之间似乎有些不兼容的地方,我却不知道是什么。
在处理一个长时间运行的Python后台程序时,是否需要特别注意频繁读写NFS挂载的文件共享?
如果有人想看看代码,处理HTTP请求并将数据写入磁盘的部分可以在GitHub上这里找到。而用于定期清理磁盘上数据的子线程部分则在这里。
2 个回答
直接回答这个问题,是的,使用NFS(网络文件系统)时确实有一些需要注意的地方。例如:
NFS并不是缓存一致的,也就是说如果有多个客户端同时访问一个文件,它们可能会看到过时的数据。
特别是,你不能依赖O_APPEND这个选项来安全地向文件追加内容。
根据不同的NFS服务器,O_CREAT|O_EXCL这个选项可能无法正常工作(不过在现代Linux上通常是可以的)。
尤其是老旧的NFS服务器在锁定支持方面可能有缺陷,甚至完全不支持。即使在更现代的服务器上,服务器或客户端重启后,恢复锁定也可能会出现问题。NFSv4是一个有状态的协议,相比于旧版本,它在这方面应该更可靠。
说了这么多,听起来你的问题其实和上面提到的这些并没有太大关系。根据我的经验,Condor守护进程在某些情况下会根据配置清理已经完成的作业留下的文件。我猜这里可能是问题的关键。
我找到了问题的解决办法,跟我在NFS共享上进行文件操作没有关系。其实,问题在于如果在NFS上进行操作,问题会更快出现,而在本地磁盘上则相对慢一些。
一个关键的信息是,我的代码是通过 SocketServer.ThreadingMixIn
和 HTTPServer
类以线程的方式运行的。
我的处理程序代码大致是这样的:
base_dir = getBaseDirFromConfigFile()
current_dir = os.getcwd()
temporary_dir = tempfile.mkdtemp(dir=base_dir)
chdir(temporary_dir)
doSomething()
chdir(current_dir)
cleanUp(temporary_dir)
大致流程就是这样。
问题并不是因为在NFS上进行文件操作,而是因为 os.getcwd()
这个函数不是线程局部的,而是全局的。也就是说,当一个线程调用 chdir()
来切换到它刚创建的临时目录时,另一个线程在调用 os.getcwd()
时会得到第一个线程的 temporary_dir
,而不是HTTP服务器启动时的静态基础目录。
解决办法是去掉 chdir()
和 getcwd()
的调用。也就是说,启动时保持在一个目录中,其他所有操作都通过绝对路径来访问。
关于NFS和本地文件的事情让我感到困惑。结果发现,我的代码块:
chdir(temporary_dir)
doSomething()
chdir(current_dir)
cleanUp(temporary_dir)
在NFS文件系统上运行得慢得多,而不是在本地磁盘上。因为这增加了一个线程在执行 doSomething()
的同时,另一个线程在运行 current_dir = os.getcwd()
的几率。在本地磁盘上,线程们快速通过整个代码块,所以它们很少会交叉。但是,如果给它足够的时间(大约一周),在使用本地磁盘时问题也会出现。
所以,这让我在Python中学到了一个关于线程安全操作的重要教训!