部分更新

今天和同事讨论这样的一个问题,如何让后台的daemon进程在不重启的情况下,更新掉部分代码和配置文件?

我认为这主要是架构的问题而不是技术限制的问题。想当然的明白要替换掉一个普通的文本文件是多么的容易,就算是要替换掉程序的代码,那么也有dlopen这样的函数可以用,java也可以动态的load/unload class。拿java为例,在不改变类的对外接口(函数的签名、添加或减少非私有变量)的前提下,改变某个函数的实现,是很容易的事情。难点在于如何设计一种合理的架构。

假设这个daemon进程的是由消息驱动的。消息可能来自于网络也可能来自于程序内部的其它模块。设想这样一种模型,每个模块维护这样一个map,其中key是消息的类型id,对应的value是一个object,这个object有一个void process(message msg)方法。每来一个消息就去调用这个方法。而这个对象有两种方式被存储在硬盘上,class文件,或者一个脚本文件。如果是C/C++,那么就是从一个so中用dlsym获得函数指针而已。无论如何,当需要更新的时候,就是从硬盘文件反序列化得到一个对象(或函数指针)插入到现在的map中。现有的那个对象在没有message继续引用它时自动删除。

这套逻辑看起来是没有问题的,可是,如果这个object包含状态(如成员变量,网络连接)怎么办?结论是:这些只能被丢弃,否则问题会急剧复杂。重要的数据应该都在数据库里,或者对象析构的时候写入到数据库里。如果是后者,那么还要求在旧对象清理结束之后再注册新的对象。

上面所说的对象,也可以看成是一个微型模块。模块是什么?就像so文件一样,有明确的init和fini,并对外以函数指针的方式提供服务。具体到java之中,可以把一堆代码放在一个package下,然后以一个object实例对外提供公开函数,其它的所有变量和函数都是私有或包隐藏的。如果模块之间需要交互,那么应该是在模块的构造函数那里把其它模块的对象实例传进去。如果一个模块不被其它任何模块所依赖,那么可以轻易的把它的所有消息处理钩子注销掉,unload然后重新load。

这样就没问题了吗?不是。假设我在数据库里存储着账户的货币数。分金银两种货币,老的实现是1金=100银,新的实现是1金=10银。然后消息处理函数在这两个实现之间来回切换,不会出问题吗?或者某个业务逻辑,原来有3个状态,更新后只有2个状态。然后导致某些业务卡在中间状态?

或者换个角度想,配置文件也是数据库的数据,只不过读的频率远远大于写。当我们在写业务逻辑处理代码的时候,假定配置文件是在多大程度上一致的?这个答案决定了动态更新的配置文件的难度。假设我们是在开一个网店,早上卖了3件物品,每个10块钱。然后更新了配置文件,下午卖了5件物品,每件7块钱。然后晚上关店,然后统计收入。那么配置文件就绝对不能简单的是Map<商品id,商品价格>这么简单。就事论事,我认为应该划定为在单个消息的处理过程中是一致的。那么最简单的做法就是,把每个配置文件当作一个对象,在消息处理的过程中如果要读某个配置文件,这个对象的引用计数就加1,然后在消息处理完毕的时候释放所有引用。如果一个配置文件的引用为0,那么就可以安全的从内存删除。如果这个对象不在内存中,就按预先约定好的位置去硬盘读取。按照上述的逻辑,reload(Conf[])函数很好写。哦,或者,直接用Java的rwlock。w的优先级优于r,即如果有线程在等写锁,那么其它的请求读锁的请求都会被阻塞。

顺便,今天看dlopen的man page的时候发现glibc的RTLD_DEEPBIND这个flag非常好!

此博客中的热门博文

少写代码,多读别人写的代码

在windows下使用llvm+clang

tensorflow distributed runtime初窥