LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
123
返回列表 发新帖
楼主: Chowroc

[原创][涅磐系列]文件系统近实时镜像工具及其高可用应用

[复制链接]
发表于 2007-11-2 10:10:45 | 显示全部楼层
不错,以后抽时间看一下。谢谢了
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-12-3 13:47:14 | 显示全部楼层

==故事二==

==故事二==
现在,一个从"pyinotify.ProcessEvent"继承的类的实例可以被用来执行与事件
相应的操作,例如,有一个方法:
``def process_IN_CLOSE_WRITE(self, event):``
这个 inotify "IN_CLOSE_WRITE" 事件将在一个循环中被这个方法执行,例如:
```
  1. while not self.Terminate:
  2.         ...
  3.         if wmNotifier.check_events(wmTimeout):
  4.                 wmNotifier.read_events()
  5.                 ...
  6.                         wmNotifier.process_events()
  7.                         ...wmNotifier.process_events()
复制代码

```
这里 wmNotifier 是 "pyinotify.Notifier" 的一个实例(如果你不太清楚,请
参阅 pyinotify 的文档),当 wmNotifier.process_events() 被调用的时候,它
运行一个从"rocessEvent"继承的类的实例的 process_* 方法,"IN_DELETE"事件
对应于"process_IN_DELETE()","IN_WRITE_CLOSE"对应于
"process_IN_WRITE_CLOSE",如前所述。

因此,要做的就是编写这样的一个实例及其 process_* 方法,我们可以将这个
继承的类命名为 "rocessor"。

并不是所有由 inotify 产生的事件都是重要的,对于文件系统镜像来说,只有
几个事件需要被考虑:
IN_CREATE (with IN_ISDIR for only directory)
IN_WRITE_CLOSE (only for regular file)
IN_DELETE
IN_MOVED_FROM
IN_MOVED_TO

__另一个也很有用的事件是"IN_ATTRIB",但是目前这个版本还没有应用__
__因为一开始设计 mirrord 的时候想法还不是特别成熟__

那么这个 Processor 应该做什么?或者说 process_* 应该是怎样的?让我们先
看看这个问题:什么需要被监控。

如果你已经读过 inotify 和 pyinotify 的文档,你知道只有那些被 inotify
监控了的目录/文件才会产生事件,而将这些目录/文件加入监控则是你的应用
程序的事情。因此你必须通过调用 inotify 的 inotify_add_watch() 或
pyinotify 的 WatchManager.add_watch() 来什么要将那些内容加入监控。

这个操作涉及到前面已经提到过的一个可管理性问题。我们需要知道的第一个
规则就是:只有目录需要被加入监视。因为任何其子目录/子文件产生的变更都会
导致 inotify 事件产生,而你可以通过 pyinotify 的"Event"实例来获得监视
目录的路径和相应的子目录/子文件的名字。实际上,pyinotify 就是只监控目录


前面提到过递归的问题,这就是说,当你将一个目录增加到 inotify 的监视中的
时候,只有在这个目录下第一层的子目录和子文件会产生 inotify 事件,例如,
如果"/var/www/html"被监控,则"/var/www/html/index.php"和
"/var/www/html/include"的变更可以被捕捉,但
"/var/www/html/include/config.php"或"/var/www/html/include/etc"却不行。

这意味着如果使用``mkdir -p /var/www/html/include/etc``创建目录而
"/var/www/html"被监控,只有"/var/www/html/include"将会导致 inotify 产生
"IN_CREATE|IN_ISDIR"事件。同样的规则适用于 Python 的``os.makedirs``,
以及任何递归的拷贝/移动操作!

所以将所有需要的目录加入到监控室这个程序(mirrord)的事情。这将导致对文件
系统的某些部分进行遍历操作,而这是比较消耗资源的,不过这个遍历只需要做
一次,而在这个名之为"boot init"的阶段之后,资源可以被释放而 mirrord 将
以 daemon 方式运行。

尽管 pyinotify.WatchManager 有一个"rec"参数来调用 add_watch(),但我不
打算使用它,因为:
+ 它没有排除目录的功能,例如,一些"cache"目录,或保护变化频繁的大文件的
目录。

+ 这个"rec"是静态的,这意味着当目录/文件已经被删除或移走后,inotify 的
watches 不会进行相应的调整。事实上,这种情况主要发生在"IN_MOVED_FROM"
事件时,你可以看看在 svn 源代码中的"ulfs/cutils/trunk"目录下的的原型
脚本"prototypes/mirrord_inotify_delete.py"。

有必要避免这些监控"尸体"留在系统中,这可能导致 mirrord 进程变得太臃肿,
同时因为当前的 pyinotify 使用内存来存储 watches,这也会导致内存的浪费。

% EOL

这里,一个``fs_info.py``模块被用来进行包含/排除操作。这可以看作是一个
文件系统标识系统,可以使用它的包含/排除配置来将文件系统切割成若干部分
,并且使用它提供的方法来得到符合配置的所有目录/文件的一个完整列表。

例如,你可以将一些目录/文件加入到一个文件系统标识中,通过如下方法:
```
sh# fs_info -a t:/var/named \
-a t:/var/www/html \
-a x:/var/www/html/cache \
-a x:/var/www/html/log website
```
这里``fs_info``是一个命令行的接口,它会根据传递的选项调用相应的
fs_info.py 模块的方法,"-a"意思是"append",而 t/x 与 tar 的 "-T/-X"选项
意思相近,或者说,"include/exclude"。

标识名是"website",这意味着文件系统标识"website"包含若干目录及其子目录/
子文件,而当你调用 mirrord 时,你可以直接将这个 identity 作为参数传递给
``mirrord.py``模块的``Mirrord``构造,这样这个实例将可以调用
FsInfo.find() 来得到所有子目录/子文件的完整列表,同时排除包含在这个标识
里面的排除目录。

fs_info.FsInfo 会建立一个"/var/fs_info/website"目录,并且记录这些保护(t)
目录到 /var/fs_info/website/.t_files,记录排除(x)目录到
/var/fs_info/website/.x_files。你可以调整位置,通过修改 fs_info 的配置:
``options.datadir = "/var/mirrord``,配置在 /etc/python/fs_info_config.py。

所以最后我们得到这样一个图:
```
  1. 使用等宽字体显示:

  2.              (1)---> [mirrord] ---(2)---> [fs_mirror] ---(3)
  3.              /                                             \
  4.         [fs_info]    /==============(4)===============> [fs_sync]
  5.            /        /                                        \
  6.     +--------------+                                  +---------------+
  7.     | original     |                                  | mirrored      |
  8.     |  file system |                                  |   file system |
  9.     +--------------+                                  +---------------+

  10. (1) tell it what to monitor for mirror
  11. (2) tell it what are modified
  12. (3) tell it what to transport
  13. (4) the actual regular files been transporting
复制代码

```

通过第(2)步,fs_mirror 知道有哪些内容发生了变化,并调用 fs_sync 来执行
相应的操作:目录创建、目录/文件删除和移动可以在客户端上直接完成,因此
不需要消耗服务器上的其他资源;但普通文件内容的变更无法镜像除非将文件
拷贝过来。这个拷贝操作可以通过任何网络传输协议和工具来完成,例如 FTP,
NFS, SMB/CIFS, SSH 或 rsync ...,因此 fs_sync 可以有多种实现。

更进一步的关于这个同步机制的说明可以在
[The Fifth Story: Monitor #ru_data_man_design_monitor]
找到。

在解释了"fs_info"和(1)之后,回到前面的问题:"rocessor"(为避免混淆,
我们现在将其称为"Monitor")应该做什么?这是"mirrord"和(2)的功能的一部分。
更详细的关于 fs_info 的细节,请阅读代码和内部文档。

从前面的图形和故事一,我们知道 mirrord 必须:
+ 收集所有的路径名到 inotify watches,这些文件名可以通过
fs_info.FsInfo.find() 方法得到,在 mirrord.Mirrord 的"boot init"阶段。

+ 维护文件系统快照(snapshot)。这是另一个有用的特性,是一个当前文件系统
所有路径名的集合(包含若干相关的其他信息)。有几个理由说明为什么要使用
snapshot,将在后面谈到。

+ 创建一个 Monitor 实例,在 inotify 产生有关事件的时候来执行相应的
process_* 操作,包括在删除/移动或递归拷贝/移动的时候调整 watches,以及
记录文件系统的变更。

+ 将这些变更记录传递给 fs_mirror (2),但是只在 fs_mirror 要求进行的时候
才传输内容,因为这种"拉"的模式更安全也更灵活(如果使用"推"的模式,则当不
能 push 的时候,mirrord 其他方面的工作会受到影响,除非 mirrord 做得更加
复杂)。所以 mirrord 应该是一个服务器端的代理(agent),而 fs_mirror 是
客户端。

通过 socket 来传输这些记录是比较合适的,因此这导致要在 agent 和 client
之间设计一个合理的协议。

+ 能够为若干客户端服务,以形成"多点镜像(multi-mirror)",即若干客户端
可以并发的镜像一个文件系统,或者更进一步(在未来某个版本中),并发同步
文件系统的不同部分。

+ 调度若干线程或进程以完成不同的事情。至少有两个线程存在:main thread
和 schedule thread。

主线程在一个无限循环中运行,通过"**Monitor**"来检查、读取和处理 inotify
事件,当没有任何事情发生的时候,阻塞周期为若干秒(默认为 4s),否则相应的
操作会立即调用。

既然 mirrord 需要通过 socket 来为 fs_mirror 服务,那么至少需要有一个
线程监听在一个端口上等待连接和请求,而主线程已经被 Monitor 占据来处理
inotify 事件,所以一个调度线程就变得有必要了(另一个解决办法是使用
select/poll)。它被称为"schedule thread",可以支持"multi-mirror",因为
当一个运行 fs_mirror 的主机请求同步的时候,一个新的"服务线程"会被启动
并服务于这个客户端。

服务线程(server thread)主要处理与协议相关的事情。

+ 在线程之间,主要是在主线程和服务线程之间,共享一些变量,例如变更记录
和文件系统快照。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-12-12 10:55:47 | 显示全部楼层

===故事三:记录日志===

===故事三:记录日志===
在故事一种,我们已经讨论了一种记录变更的机制,并且已经证明那不是一个好
办法。

在那之后,首先进入我脑子的一个想法是建立四个数组:
CREATE (IN_CREATE|IN_ISDIR only for directories),
FWRITE (only for regular files writing),
DELETE (IN_DELETE),
MOVE (IN_MOVED_FROM and IN_MOVED_TO),
并将任何变更有关的路径名不断追加到这几个相应的数组中。

所以,一开始我想设计如何在每一个服务线程中去管理这些数组,即每次一个
服务线程被启动,它就为自己创建这些数组,因为每一个客户端的状态都是不同
的。

要维护这些线程相关的数组,就要求每一个线程都实现一个自己的 inotify
Processor 实例,或者说,一个"Counter",来追加变更记录,并在客户端已经
读取之后清除这些记录。

但是这种设计仍然是有很大问题的:
- 既然已经有一个 inotify "rocessor"(在主线程中),那么为每一个服务线程
维护一个"rocessor"则造成了重复。

- 这种类型的记录缺少变更的顺序信息。

- 没有历史记录。一旦一个记录被客户端读取,它就被清除了,因此没有机会回
滚并从断点重启同步。

% EOL

如果参考一下 MySQL replication,那么就可以发现正确的解决办法了。所需要
的就是建立一个 **Log** 来记录所有类型的变更相应的路径名。让我们将其命名
为"wmLog"(Watch Modify Log)吧。

这个 wmLog 应该被主线程和服务线程共享,在主线程中的"Monitor"将最新的
变更追加到 wmLog 的尾部,同时服务线程可以读取 wmLog 并利用一个指针来
标识读取位置,这样一个服务线程就知道哪些内容已经处理过了。

那么这个 wmLog 应该象什么样子呢?既然顺序很重要,那么一个列表
(list/sequence)就相当直接,但是一个哈希表则更灵活和清楚。如果需要更改
wmLog,例如,删除过时的记录(比如一个月以前的记录)以避免 wmLog 太长了,
哈希表就会非常有用;并且利用类似哈希表的接口,可以比较方便的切换到其他
的日志机制(例如,Berkeley DB,后面将讨论)。

为了使哈希有序,其键值是序列数字。每次一个 inotify 事件发生和处理之后,
序列号就递增 1,因此 wmLog 看起来就像是 Python 中的 dictionary 结构那样

  1. ```
  2. {
  3.     1 : ('CREATE', '/var/www/html'),
  4.     2 : ('CREATE', '/var/www/html/include'),
  5.     3 : ('FWRITE', '/var/www/html/index.php'),
  6.     4 : ('DELETE', '/var/www/html.old'),
  7.     5 : ('MOVE', ('/var/www/html', '/var/www/html.new')),
  8.     ...
  9. }
  10. ```
复制代码


这样每一个服务线程所拥有的指针就是一个整数,它与主线程以及其他服务线程
的序列号都不一样,这反映出不同的客户端不同的读取进度。

这种机制使得客户端从断点重启同步成为可能,例如,当主线程已经将序列号
递增到 10234 时,一个服务线程可能仅仅督导 9867 位置,这时这个客户端可能
因为某种原因终止了,而主线程的序列号则继续递增。下次重启这个客户端的
时候,它可以告诉服务线程从 9867 这一点开始传输 wmLog,否则客户端不得不
要求一个全新的同步,那将迫使服务线程重新发送整个文件系统快照的全部内容
,而这个重发的过程是比较消耗资源的(既然你可以使用 Python 的 generator,
这个过程不会消耗太多内存,但仍然需要消耗 CPU 和网络带宽),并且接下来的
普通文件的传输将更消耗资源。

但是不像 MySQL replication,到目前为止还没有实现一种机制使 mirrord 可以
从断点重启。因为在两次 mirrord 启动之间,文件系统可能发生了没有被监控的
变化,所以最简单的方法就是重建整个快照,并重置 wmLog 为 0,这当然也将
导致客户端重做初始同步,因为必须保证源文件系统和镜像文件系统的一致性。
mirrord 通过要求客户端提供一个利用时间产生的 MD5 session 给服务线程来
实现这个目标。

未来的一个版本将会通过在启动时比较现在的文件系统和上次终止时的快照来
改进这种启动策略,以“计算”出在两次启动之间的变更,这可以使的启动更
平滑。

wmLog 是哈希表,所以 Python 的 dictionary 相当直接,但它常驻内存,而
Log 一定是会越来越大的。如果我们假设每一条记录的长度是 30 bytes,而变更
的频率是 1/s,那么每天它需要消耗"30 * 86400 / 1024 / 1024 = 2.47 MBytes"
内存,因为可以控制 wmLog 的长度,所以也可以控制内存使用的上限。如果源
文件系统不是太大,或者变化不是太频繁,将记录放在内存的 wmLog 中看上去
还是不错的。

相反的,使用一个 hash like 的数据库也很有道理,例如 Berkeley DB。仅仅
只需要创建一个简单的类 fs_wmlog.WmLog 来包裹对 Berkeley DB 的操作,因为
wmLog 只接受整数而 BDB 只接受字符串,这种方式保持了一致性。

最初我是一内存 dict,但是生产线上我必须处理超过百万的目录/文件的文件
系统,为简单起见,这个版本完全使用 BDB,但在未来我将考虑将两者都实现。

第一个实现是将它们都联合到一个统一的 API,我的意思是将有 dict 和
Berkeley DB 两个 wmLog,象这样:
  1. ```
  2. class WmLog:
  3.         def __init__(self, path):
  4.                 self.memlog =  {}
  5.                 self.dbdlog = bsddb.btopen(path, 'n')
  6. ```
复制代码

最近的记录将放在 memlog 中,随着其不断增大,更早的记录将放在 Berkeley
DB 中。后来我发现其实不用这么麻烦,因为完全依赖于 BDB 的内存管理也许来
得更方便。

到目前为止,只有与动作相关的路径名被记录和传输,但是文件的状态其实也
很有用,更进一步,利用 hostid,将有可能是两台主机互为镜像。由于最初在
设计上还存在不足导致了这些缺失,并将在后续版本中加以改进。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-12-25 15:02:43 | 显示全部楼层
把中文的文档写完了

http://www.yourlfs.org/ru_data_man_zh_CN.html
回复 支持 反对

使用道具 举报

发表于 2007-12-25 18:29:33 | 显示全部楼层
很感謝樓主的分享,但樓主這些東西太覆雜,小弟才疏學淺,至今未有看懂 :(

BTW,好奇一問,何以網站不用 unicode 編碼?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表