星期一, 十二月 11, 2006

数据结构与配置设想

1. 编程语言与数据结构
我开始学习编程和数据结构都是从 C 语言开始的,现在看来这并不是一个太好的选择(如果有得选择的话)。因为 C 语言的结构性还是不够,通过它来理解数据结构则难免会造成一些理解的混乱,也不利于拓宽思路。

在学习了 python 之后,我发现 python 的数据类型的设定就要聪明得多(当然 python 本身是用 C 编写的)。当然它是"动态强类型",但这不是我要说重点的,重点是对于数据结构的分类。

在 C 语言中,我们的分类是很混乱的,我们有字符串、数组,然后我们有链表,但是链表有可以引申为循环链表、双向链表等等,接着是堆栈和队列,但是堆栈和队列既可以用数组来表示也可以用链表来表示,以后的二叉树、树和图也是如此,并且我们还引申出诸如邻接列表这样的东西......

究其原因,我觉得这是因为有一个最基本的概念没有弄清楚,那就是数据的逻辑结构和它的"物理"实现之间的区别。

我认为,所谓数据的逻辑结构,其实就是数据分布在人头脑中的静态印象。这样,在我看来,真正的逻辑结构就这么几种:sequnce/list, map/dictionary, table 和 tree 以及 graph(应该很少用到)。至于 stack 和 queue 以及 string 等其实都是 sequnce 的特例,它们之所以成为 stack 或 queue,只是人们决定要这样使用它,所以属于动态的图象。

一种特定的编程语言的数据结构实现与逻辑数据结构之间一定会有差异。有时侯是因为实现的难度,例如用 C 语言的数组来实现各种结构,有时侯则是为了特定的操作更方便,例如几乎所有语言中都存在的 string。

象邻接列表这种结构,不就是一种表(table)吗?而在 python 中也没有对于 table 的直接实现,只能使用二维的 sequnce 来表达。

所以越高级的语言,它的数据类型中就有越多的类型与逻辑数据结构一致,以使得相应的操作更简单。但总的来说,数据的逻辑结构与它的实现之间是分离的。

2. 配置与数据逻辑结构设计
既然数据的逻辑结构是一种静态印象,并且和它的编程实现分离,那么当然应该可以将其置入文件中,同时考虑到“数据驱动编程“是一种比较好的设计方法,通常代码决定了程序能做什么,而配置数据决定了程序要做什么,那么问题就变成了如何在配置文件中使用更据有逻辑性的数据结构来决定程序的运行。

显然,应用程序必须足够灵活以适应不同的情况,相应的配置文件也就同样必须足够灵活。因此只能选择最复杂的数据结构 tree。

所以基本的配置文件的结构应该是树型结构,而目前也有好几种树型类型的文本格式,最著名的当然就是 XML 了,另外还有 sysctl.conf,是使用平面形式进行表达的(postfix 的 main.cf 其实类似,不过它使用了"_"作为不同节点之间的分隔符)。

然而现实的情况是,配置文件的格式五花八门,即使使用树型结构的配置方式也并不统一,比如上面的情况。导致的结果必然是大量的知识和代码的重复,每一个应用程序都必须编写自己的配置读取和写入模块。

因此我觉得需要有一个统一的标准或编程接口,来实现对基本树型配置的读取和写入。对于应用程序来说,它只知道这样一个 API,通过向它传递"config.level1.level2.[...]key = value"这样的树型串来操作指定的配置(不一定是文件),至于底层的实现,则交给这个编程接口的层面去完成。

所以底层的格式可以多样化以适应不同的环境,比如说类 sysctl.conf 格式(plain tree)或 XML 乃至 LDAP 数据库,当然各种格式之间也可以互相转化。

就我个人而言,我比较喜欢 plain tree 这种格式,它就是"config.top.level1.level2.[...]key = value"这种形式,与 XML 相比,它具有更好的可读性,而且可以非常方便的使用传统的文本工具来处理。在我看来,XML 更适合于编写文档的角色──当然,在设计中,它必须是可选的。

当然,除了表达最基本的树型结构之外,还需要一些其他的功能,比如变量的设置和替换、跨文件/数据库的读取、对编程语言的数据类型的更好的支持...等等,所以最终我们将得到一个 mini language 来专门进行树型配置的表达。

但树型并不能解决所有的数据驱动的问题。一般来说,对于情况变化比较多,但数据总量相对来说比较小的配置,适宜使用树型结构,但对于数据量相对比较大(不宜太大,配置的内容不宜太多,否则如果不能使用树型层次结构,则可读性太差),特别是大多是重复性结构的情况,则比较适合使用 sequnce 和 table,所以需要使用相应的文件或关系型数据库,但其设计的原则应该与树型配置一样,即提供统一的 API。

而整体的结构则应该是这样的:以树型结构作为中心和基础,在必要的情况下,它延伸出来的支脉节点指向具体的序列和表。(当然也可以指向其他的树)。所以树始终是一个核心。

例如,我有一个配置文件的格式如下:
fs_backup.format = "bid, btime, archive, type, list, host";
# or fs_backup.format = python:"['bid', 'btime', 'archive', 'type', 'list', 'host']";
fs_backup.type.default = "full"; # comment
fs_backup.type = ${fs_backup.type.default};
# fs_backup.type = "incr";
fs_backup.datadir = "/var/task/fs_backup";
fs_backup.tagfile = ctable:"$datadir/.tag";
# Or use absolute path:
# fs_backup.tagfile = ctable:"${fs_backup.datadir}/.tag";
fs_backup.time = bash:"`date '+%Y%m%d %H:%M:%S'`";
fs_backup.timestamp = bash:"`date -d "$_time" +%Y%m%d%H%M.%S`";

fs_backup.log_of_files = clist:"__files__";
fs_backup.log_of_dirs = clist:"__dirs__";
fs_backup.info = ctree:"__info__";
fs_backup.name = "$hostname.$type.$timestamp";
fs_backup.archive = "$datadir/$name.tgz";

# Multiline
test.long.value = "The value of the options
also support
multilines mode";

fs_backup.others1 = ctree:/etc/others1.ctree
fs_backup.others2 = ldap:......
fs_backup.others3 = xml:/etc/others2.xml
这里就是一个很简单的树型配置(当然只是一种设想,实际最终的情况应该会有很大变化),基本上只有两层结构,当然可以根据需要增加更多的层次,这样整个系统使用统一的标准,通过变量、跨文件/数据库读取、编程语言类型支持来减少很多重复配置。从上面的设置可以看到,我可以将整个系统的各种配置树按照不同的需要进行组合(ctree),同时我还可以指向表和序列配置,比如上面的 ctable 和 clist。

在这个例子中,clist 就是一个文件和目录的列表,而 ctable 则是一个 ${fs_backup.format}:
bid, btime, archive, type, list, host
格式的文本(具体的设计有待深入考虑)。当然可以考虑使用关系型数据库。(bid--backup-id, 我希望将需要备份的文件分成若干情况,有的文件如 $HOME/.* 可以直接迁移,但有些如 /etc/X11/xorg.conf 等与硬件相关,这就需要区分,在恢复时,fs_rstore 寻找指定 archive--例如一个增量备份的归档--中的 __info__, __files__, __dirs__,从 __info__ 中获取诸如 bid 这样的信息,自动找到所有其他需要的完全和增量备份,执行自动恢复,并根据参数比较 __files__, __dirs__ 删除那些实际上删除了的文件--tar 默认不会进行文件删除。)

所以,问题转变。现在我只需要有几个专门的模块:ctree, ctable, clist 来进行相应的处理,其他程序只需要调用相应的 API,传递诸如"fs_backup.*"这样的参数,则它需要的配置都可以获得。

而且,有了这样一个统一的标准,我们甚至可以设计一个交互是的方式,象操作文件系统的目录一样来操作和调整整个系统、乃至整个部门整个企业的所有的应用的配置设置。当然,我们已经有 LDAP,也有 SQL,但我觉得不够,我们需要整体的联结、需要统一的接口、需要文本形式的良好的可读性和简单性、需要对配置的版本控制...。配置是越集中越好,因为便于管理、避免重复,但运行则是越分布越好,因为有更好的冗余和性能,能否找到一种办法兼顾两者之长?

在这个基础上,针对企业系统架构,就可以作出更多的设计来了。

3. 澄清概念:配置和数据
我觉得有必要说明一下配置的数据和一般程序处理的数据之间的不同。配置的数据是元数据,它决定了程序要怎样运行。而程序处理的数据,是程序处理过程中的输入输出结果,它从一端进入,从另一端释出,本身发生了变化,但对程序本身的行为没有实质影响。

另外,配置一方面固然是来决定程序运行的,但同时也必须给人看,以作为人机交互的一种方式。而程序处理的数据则不一定需要这么强的可读性。基于这一点,文本形式是必须的,虽然也需要考虑 LDAP, SQL 等数据库,但只能作为管理方便和性能考虑等问题的一种辅助手段,文本始终是基础!

所以,代码/文档是静止的数据,配置是相对比较静态的数据(否则不利于人类的理解),而处理数据是流动的。

这样,配置看上去这有点象 Windows 的注册表。实际上注册表确实给了我一点启发。但我觉得注册表本身是一个很糟糕的设计,就因为它不是文本化的,那些晦涩的东西几乎不具有可读性。

4. Enterprise Architecture
我关于设计统一树型配置的设想实际上是从工作中遇到的问题和需求来的。当时,不同部门的、不同子系统的、不同应用之间的配置方式和格式之间的不一致所导致的重复几乎使我抓狂。那时候我开始考虑有没有一种办法能够统一这些配置。

我查了不少资料,脑子里塞满了概念,接着我发现 Linux 系统本身的各种应用程序的配置格式都是不同的,而且也同样存在着重复设置的问题,例如:gpm 的 /dev/input/mice 和 Xorg 的 /dev/input/mice。关于这一点,应该说,前面已经讨论过了,所以撇开这个问题,考虑一下在网络、集群或分布式环境下怎么办。

在一个网络环境中,先来考虑一下处在不同主机的配置和程序怎样连接?前面已经说过,配置是集中的好,所以理想的设计是有一个配置中心,存放着所有的配置元数据──当然,是针对这个企业的网络上的应用,各个子系统和子应用之间必须共享的配置。所以,如果从配置的数据流向来看,应该会是这样的一个视图:
/text/
[service]
{API}
(directory)
%program%
<=manual= $DB$ /<---------%convert%--------->/ctree/ <=modify= | | [LDAP]------|<--%convert%-->/xml/<=modify= | | | | %replicate% | | | \--->[SVN]<---/ V | [LDAP] | | v | /---(sandbox)---\ | | | | V V | /xml/ /ctree/~~~~point~~~>$MySQL$
| | | |
| V V |
| {xmlconf} {plainconf} |
| | | |
| \--->{ctree}<---/ | | | | | V | \--------------------------> %application% <-------------------/

再看看多主机的视图:

[meta-conf]
|
V
[subversion]
|
(sandbox)
|
/--------/--------/ \------\
| | | |
V V V V
{API} {API} {API} {API}---\
| | | |
V V V V
%task1% %task2% %task3% %cluster%
|
.......
讨论一下这样做的好处。

前面已经说过,配置是集中的好。通过 meta-conf 这个配置元数据,可以作到这一点。它向一个版本控制器添加文本配置的改动(当然,在版本控制器的前后,应该施加认证和授权机制),而其他的子系统任务从版本控制器签出需要的配置,执行相关的操作。版本控制器的压力不会太大,因为配置是相对静态的数据,而且必然是在人的干预下进行变动(这也是配置的目的所在──施加人的控制)。

如果配置中心或者版本控制器当机,整个系统仍然能够正常运转,因为每个子任务都有它所需要的配置的副本;同时你不需要分别设置每个子任务,通过自动的批处理可以一次性完成整个系统的更新。版本控制器记录了每一个管理员所进行的更改,如果有问题,可以迅速恢复到正确的配置版本。

没有评论: