[转]大量小文件的实时同步方案

作者:davies

来源:http://blog.daviesliu.net/2008/04/24/sync/

传统的文件同步方案有rsync(单向) 和 unison(双向)等,它们需要扫描所有文件后进行比对,差量传输。如果文件数量达到了百万甚至千万量级,扫描所有文件将非常耗时。而且正在发生变化的往往是其中很少的一部分,这是非常低效的方式。

之前看了Amazon的Dynamo的设计文档,它们每个节点的数据是通过Hash Tree来实现同步,既有通过日志来同步的软实时特点(msyql, bdb等),也可以保证最终数据的一致性(rsync, unison等)。Hash Tree的大体思路是将所有数据存储成树状结构,每个节点的Hash是其所有子节点的Hash的Hash,叶子节点的Hash是其内容的Hash。这样一旦某个节点发生变化,其Hash的变化会迅速传播到根节点。需要同步的系统只需要不断查询跟节点的hash,一旦有变化,顺着树状结构就能够在logN级别的时间找到发生变化的内容,马上同步。

文件系统天然的是树状结构,尽管不是平衡的数。如果文件的修改时间是可靠的,可以表征文件的变化,那就可以用它作为文件的Hash值。另一方面,文件的修改通常是按顺序执行的,后修改的文件比早修改的文件具有更大的修改时间,这样就可以把一个目录内的最大修改时间作为它的修改时间,以实现Hash Tree。这样,一旦某个文件被修改,修改时间的信息就会迅速传播到根目录。

一般的文件系统都不是这样做的,目录的修改时间表示的是目录结构最后发生变化的时间,不包括子目录,否则会不堪重负。因为我们需要自己实现这个功能,利用Linux 2.6内核的新特性inotify获得某个目录内文件发生变化的信息,并把其修改时间传播到它的上级目录(以及再上级目录)。Python 有 pyinotify,watch.py的代码如下:

  1. #!/usr/bin/python
  2. frompyinotifyimport*
  3. importos,os.path
  4. flags=IN_CLOSE_WRITE|IN_CREATE|IN_Q_OVERFLOW
  5. dirs={}
  6. base='/log/lighttpd/cache/images/icon/u241'
  7. base='tmp'
  8. classUpdateParentDir(ProcessEvent):
  9. defprocess_IN_CLOSE_WRITE(self,event):
  10. print'modify',event.pathname
  11. mtime=os.path.getmtime(event.pathname)
  12. p=event.path
  13. whilep.startswith(base):
  14. m=os.path.getmtime(p)
  15. ifm<mtime:
  16. print'update',p
  17. os.utime(p,(mtime,mtime))
  18. elifm>mtime:
  19. mtime=m
  20. p=os.path.dirname(p)
  21. process_IN_MODIFY=process_IN_CLOSE_WRITE
  22. defprocess_IN_Q_OVERFLOW(self,event):
  23. print'overflow'
  24. max_queued_events.value*=2
  25. defprocess_default(self,event):
  26. pass
  27. wm=WatchManager()
  28. notifier=Notifier(wm,UpdateParentDir())
  29. dirs.update(wm.add_watch(base,flags,rec=True,auto_add=True))
  30. notifier.loop()

在已经有Hash Tree的时候,同步就比较简单了,不停地获取根目录的修改时间并顺着目录结构往下找即可。需要注意的是,在更新完文件后,需要设置修改时间为原文件的修改时间,目录也是,保证Hash Tree的一致性,否则没法同步。mirror.py的代码如下

  1. #!/usr/bin/python
  2. importsys,time,re,urllib
  3. importos,os.path
  4. fromos.pathimportexists,isdir,getmtime
  5. src=sys.argv[1]
  6. dst=sys.argv[2]
  7. deflocal_mirror(src,dst):
  8. ifexists(dst)andmtime==getmtime(dst):
  9. return
  10. ifnotisdir(src):
  11. print'update:',dst
  12. open(dst,'wb').write(open(src).read())
  13. else:
  14. ifnotexists(dst):
  15. os.makedirs(dst)
  16. forfilenameinos.listdir(src):
  17. local_mirror(os.path.join(src,filename),os.path.join(dst,filename))
  18. os.utime(dst,(mtime,mtime))
  19. defget_info(path):
  20. f=urllib.urlopen(path)
  21. mtime=f.headers.get('Last-Modified')
  22. ifmtime:
  23. mtime=time.mktime(time.strptime(mtime,'%a,%d%b%Y%H:%M:%S%Z'))
  24. content=f.read()
  25. f.close()
  26. returnint(mtime),content
  27. p=re.compile(r'([\d.]+?)+([\w/]+)')
  28. defremote_mirror(src,dst):
  29. mtime,content=get_info(src)
  30. ifexists(dst)andmtime==int(getmtime(dst)):
  31. return
  32. print'update:',dst,src
  33. ifnotsrc.endswith('/'):
  34. open(dst,'wb').write(content)
  35. else:
  36. ifnotexists(dst):
  37. os.makedirs(dst)
  38. formt,filenameinp.findall(content):
  39. mt=int(float(mt))
  40. lpath=dst+filename
  41. ifnotexists(lpath)orint(getmtime(lpath))!=mt:
  42. remote_mirror(src+filename,lpath)
  43. os.utime(dst,(mtime,mtime))
  44. ifsrc.startswith('http://'):
  45. mirror=remote_mirror
  46. else:
  47. mirror=local_mirror
  48. whileTrue:
  49. mirror(src,dst)
  50. time.sleep(1)

如果源文件不在同一台机器上,可以通过NFS等共享过来。或者可以通过支持列目录的HTTP服务器来访问远程目录,mirror.py 已经支持这种访问方式。server.py 是用webpy做的一个简单的只是列目录的文件服务器。由于瓶颈在IO上,它的性能不是关键。server.py的代码如下:

  1. #!/usr/bin/python
  2. importos,os.path
  3. importweb
  4. importtime
  5. root='tmp'
  6. HTTP_HEADER_TIME='%a,%d%b%Y%H:%M:%S%Z'
  7. classFileServer:
  8. defGET(self,path):
  9. path=root+path
  10. ifnotos.path.exists(path):
  11. return404
  12. mtime=time.localtime(os.path.getmtime(path))
  13. web.header('Last-Modified',time.strftime(HTTP_HEADER_TIME,mtime))
  14. ifos.path.isdir(path):
  15. forfileinos.listdir(path):
  16. iffile.startswith('.'):continue
  17. p=os.path.join(path,file)
  18. m=os.path.getmtime(p)
  19. ifos.path.isdir(p):
  20. file+='/'
  21. printm,file
  22. else:
  23. printopen(path,'rb').read()
  24. urls=(
  25. "(/.*)","FileServer",
  26. )
  27. if__name__=='__main__':
  28. web.run(urls,globals())

为了获得更好性能,以达到更好的实时性,Hash Tree最好是平衡的,比如BTree。如果一个文件发生变化,同步它需要进行的IO操作为N*M,其中N为数的层数,M为每层的文件数目。现在我们N为2,M最大为10000,适当减少它可以获得更好的性能,比如N为4,M为100。在以后创建目录结构时,最好能够考虑这方面的因素。

之前hongqn推荐过一个利用inotify的文件同步方案,同步方式类似于mysql和bdb等,由于过于复杂导致不可靠而没有采用。上面这个方案只用了一百多行Python代码就基本解决问题了,是不是很帅?:-)

你可能感兴趣的:(数据结构,Web,python,OS,lighttpd)