缓存与文件系统——之一


当你觉得需要一个缓存的时候,把情况再探究得深入一些然后问自己为什么需要这个缓存。

Unix认为世界是静止不动的。

妥协并不讨好。

——《The Art of UNIX Programming》

最近一段时间的主要工作是维护和开发一个文件浏览器类型软件(类似于Mac OS X的Finder和Windows的Explorer)。这个软件与Finder和Explorer的一些重要的不同之处有,从功能上说更加偏重图片管理,从技术上说设置了一个缓存,用来存储经常遇到的图片的缩小版本。缓存会让显示文件缩略图(Thumbnail)的速度大大加快。应该说这个缓存功能对于管理大量高分辨率图像是很有用的。但是它未能避免违背《The Art of UNIX Programming》的一些告诫。尽管违背这些告诫有很好的理由,而且大部分用户还是对违背这些告诫的代价和收益表示认可,但是维护中因此遇到的越来越多的麻烦导致我开始思考是否这些设计上可以有更好的替代。

缓存是在谈及提高效率时通常被首先考虑的技术之一,但负面影响也在《The Art of UNIX Programming》里被强调——和主存的同步问题和命中率问题很难解决。这个软件的缓存比较特别,由于存储的是图片,而且是主存的缩小版本,所以该缓存可以存储所有访问过的主存图片,而不必用类似最近最少(LRU)算法去丢弃某些部分(至少不必像CPU缓存那么频繁),因此不存在传统的命中率丢失问题。但是它有另一种形式的命中率问题。如果没有这个缓存的存在,那么一个程序要显示一个图片的缩略图,只需要把图片读出,用down-sample算法缩小,然后绘制在屏幕上。有了这个缓存,除了上述的操作,程序还需要把down-sample的结果存储到缓存中(由于绘制在屏幕上的缩略图每次大小不完全相同,存储到缓存中的缩小版本的分辨率还要比屏幕缩略图需要的高,这样以后读取缓存来绘制缩略图的时候仍然需要down-sample,虽然down-sample的程度减小了)。另外,为了用批处理提高效果,缓存的生成是以文件夹为单位。这样即使一个文件夹用户只浏览了前几个图片,后端的线程也会为整个文件夹生成缓存。如果用户偶尔浏览一些以后很少用到的文件夹,对磁盘I/O和磁盘空间可能都会造成一定程度的浪费。所以在某些情况下这个缓存会拖慢而不是提高效率。这说明任何类型的缓存(甚至任何类型的优化策略)都会在某些极端情况下劣于简单的直接访问(蛮力策略)。

《The Art of UNIX Programming》提到的缓存的同步问题主要受到主存更新方式的影响。但是现代软件越来越多的采用plug-in架构和具备online live update的功能,所以缓存的同步也开始受到软件本身更新方式的影响。如果一些plug-in或者软件本身的更新改变了生成缓存图片的方式,那么如何同步已经缓存的图片就成为一个复杂的问题。比如,一个plug-in的升级可能改正了原来有色彩失真的缓存生成方式,但是原来哪些文件会造成色彩失真并不是一个可以简单的用某些轻量级算法判断的标准。所以保险的方法只能是在升级plug-in的同时清除所有已有的缓存。由于plug-in架构本身是为了屏蔽不同plug-in实现的细节,所以缓存中哪些图片是由哪些plug-in生成的也往往不容易判断,所以最后常常只能通过清除整个缓存来解决。

主存的更新是缓存同步的最主要挑战。对于这个软件来说,这个挑战的更加艰巨。因为这个软件的设计是把整个文件系统作为主存。正如《The Art of UNIX Programming》指出的,UNIX文化建议利用文件系统达到重用,但是没有过多的考虑通过并发访问文件系统来协作,UNIX假定程序的运行是短暂的,在运行期其涉及到的文件不会被其它程序访问;假定程序间的文件访问是交替的而不是并发的;假定程序对频繁修改的文件独享,而只会共享少量罕有修改的文件。把整个文件系统作为主存,通过设置缓存来提高性能,这样的设计会遇到很多问题。比如,对于正在被写入的大文件,很难判断是正在写入过程中,还是写入已经完成。如果在写入过程中被生成缓存,那么缓存文件就是错误的,但是没有机会得到改正(除非用户手动清除缓存——但是缓存策略的主旨就是避免用户经常手动清除缓存)。由于所有操作系统在文件系统方面受到UNIX很大影响,所以它们都很大程度上有同样的问题。解决的办法往往很难做到高可移植性,往往非常复杂,会引入更多的线程同步问题。

将软件设计成为一个类似Finder和Explorer的全系统浏览器,还有一个问题,那就是是否要用其本身的GUI反映一个文件系统的所有细节。显然,出于并不希望复制Finder和Explorer和突出自己特有功能的目的,该软件选择了简化某些细节的做法。比如不会显示文件的读写权限,只显示文件的锁定状态。但是,和很多复杂系统一样,文件系统的很多概念是相互联系,但是又部分正交的。很难完美的掩饰某些细节而不造成混淆。比如,文件是否可以锁定是受到文件读写权限的限制,所以文件锁定状态的改变会因为读写权限的原因而失败,最终用户还是必须了解被隐藏的概念和细节,这就抵消了最初了通过简单的UI简化用户操作的初衷。其实就连Mac OS X的Finder本身也有这样的问题——OS X的HFS拥有传统的user-group-other权限和ACL的权限,但是Finder没有区别显示而是混在一个GUI权限列表中;另外,HFS的锁定(lock)有user-lock和supervisor-lock两种,Finder也没有区别显示,这样Finder对于一些ACL比较复杂和一些拥有supervisor-lock的文件的显示结果就比较奇怪。这说明文件系统的细节的复杂程度连专门浏览器的GUI都有些难以应付。当你希望隐藏细节的时候,“妥协并不讨好。”

而且,不要忘记,所有这些被显示或者被隐藏的细节,都可能被其它的程序在任何时候修改。在决定了让你的应用可以访问整个文件系统的同时,相应的就必须承担其它应用和你的应用竞争访问的后果。(有人认为Windows那种强制文件锁(mandatory file lock)可以解决问题。其实强制锁引入的问题比解决的问题多,而且其功能与文件权限概念有重叠和混淆。)更不要忘记,在被显示的细节里,有一部分会被放到缓存中。这些问题加在一起,会把系统卷入无穷无尽的边缘情况(edge case)中。

有了这些观察和思考,我开始理解为什么类似iPhoto的软件开始采用另一种设计模式——私有数据库。iPhoto类的软件没有被设计成直接处理文件系统任意区域的软件,而是要求用户必须把待处理的图片“引入(import)”。引入操作实际是把图片置于一个受到保护的私有数据库。引入私有数据库是否意味着要重复实现很多文件系统已经提供的功能?并不是必然的,因为这个私有数据库可以普通文件夹——唯一的特殊之处是不对其它程序开放(比如位置难于访问,或者在文档中说明,或者利用其它程序的一些功能——比如Finder中显示为单个文件——从技术上完全阻止直接访问内部),对其编程访问仍然可以利用文件系统的操作,同时省去了诸多的文件并发访问问题。在私有数据库内部,文件的细节也更好控制,比如你完全可以控制数据库内的任何文件都没有ACL(甚至拥有统一的user-group-other权限),也没有supervisor-lock,因为修改这些细节的只有你自己的程序。这时一个简化的UI也极少会导致混淆(即使会也是易于修改的bug而不是上面那种由基本设计导致的问题)。在私有数据库模式中,缓存的两个固有问题不会消失,但是会得到大大的缓解。也可以说,如果为了图片管理中的性能目的而必须引入缓存,那么私有数据库模式是一个很值得考虑的选择。

现代操作系统是一个非常复杂的高度协作的系统。通过单个应用程序达到一个全系统级别的简化操作就算不是完全不可能,也是一项有极端挑战性的工作。因为这意味着你的应用程序就像一个把全系统都覆盖在后面的幕布,但是同时你又必须允许你的用户绕过幕布去操作这个系统(因为你把全系统都盖住了,但是用户又不可能只用你这一个应用)——这种矛盾的策略让你很难在这个幕布之前演出一幕没有纰漏的剧目。更为可行的办法仍然是针对自己的需求,把应用程序的操作涉及范围尽量局部化,这样,你的幕布仅仅覆盖了系统的一小部分,你更为容易说服用户永远不要绕过这个幕布。

后记:出于篇幅的考虑,有些问题的细节没能全面展开。这样可能造成一个假像,让一些喜欢思考的不易被轻易说服的读者认为文中列举的问题没有严重到必须采用私有数据库架构的地步。所以我考虑冠以“之一”来让以后的“之n”等展开说明那些被质疑的比较多的问题。

一条回应 to “缓存与文件系统——之一”

  1. zy Says:

    “Unix认为世界是静止不动的”是什么意思?愿闻其详。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 注销 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 注销 /  更改 )

Connecting to %s


%d 博主赞过: