Archive for the 'Architecture Design' Category

基于文件系统的生产者和消费者问题

周末的时候和team讨论了下如何用最简单的方式,提高数据文件的单位时间传输吞吐量。下面是一个简单的应用场景:

一个目录(DIR1),有很多Producer向这个目录里面放文件,同时有很多的Consumer负责从这个目录里面消费这些文件,插入数据库或者做其他的操作,然后删除或者移走这些文件。

假设条件:

  • 一个文件中转目录DIR1,这个目录位于一个网络存储上
  • 一个生产者,每一秒钟向DIR1里面放一个文件
  • 若干个消费者,假设有8个,其实是8个不同的服务器,都可以访问DIR1,多台服务器可以起负载均衡的作用,任何一台或者几台出问题,整个数据流不会中断
  • 解析一个文件大约需要2-14秒
  • 最后一点:位于网络存储上的目录DIR1,我们认为它是不会出问题的,它不是这里的问题核心

这个场景很普遍,很多公司大概都会用到,尤其是那么比较老的系统(Legacy System),下面是两种方案:

方案一

Consumer循环扫描DIR1,一旦发现有文件,循环解析这些文件,这里有8台服务器,也就是说有8个Consumer一起这样做。代码如下:

	public void run() {
		System.out.println("Created consumer:" + threadName);

		while (true) {
			File file = new File(Constant.STAGING_FOLDER);
			File files[] = file.listFiles();
			for (int i = 0; i < files.length; ++i) {
				File f = files[i];
				parse(Constant.STAGING_FOLDER + "/" + f.getName());
			}

			Commons.sleep();
		}
	}

看起来很简单,可是上面的代码效率非常的差,多个Consumer有很大的几率拿到相同的文件,当某个Consumer尝试去解析一个文件时,却发现这个文件已经被别的Consumer解析过了,并且文件也都删除或者移走了。这样浪费的很多的CPU时间。

可以用下面的方案来替代:

方案二

	public void run() {
		System.out.println("Created consumer:" + threadName);

		while (true) {
			File file = new File(Constant.STAGING_FOLDER);
			File files[] = file.listFiles();

			int nCapacity = files.length > Constant.CAPACITY ? Constant.CAPACITY
					: files.length;
			System.out.println(this.threadName + " found " + nCapacity
					+ " files");

			for (int i = 0; i < nCapacity; ++i) {
				File f = files[i];
				f.renameTo(new File(Constant.TMP_FOLDER + "/" + f.getName()));
			}

			for (int i = 0; i < nCapacity; ++i) {
				parse(Constant.TMP_FOLDER + "/" + files[i].getName());
			}

			Commons.sleep();
		}
	}

它和方案一的不同之处在于:它每次扫描完目录后,最多只取前若干个文件,这里是10个。并且,它不急于去处理文件,而是把文件马上移动到一个临时工作目录,其他的的操作都是相同的。

对于这个方案,有个附加条件:这个临时工作目录tmp,一定要和staging目录在同一个文件系统(filesystem),这样的话,mv操作就只是修改一下inode,几乎瞬间完成。

比较(Benchmarking)

为了测试两中方案的效率差别,我写了一个模拟程序(http://googlestop.com/download/SimConsumer.7z),它有7个class:

  1. App.java - 程序入口
  2. Commons.java - 共享的函数
  3. Constant.java – 配置参数
  4. Producer.java - 生产者,每隔一秒向目录staging里丢一个文件
  5. AbstractConsumer.java – 抽象消费者,定义消费者的一些基本属性和行为
  6. Consumer1.java - 具体消费者,实现方案一
  7. Consumer2.java - 具体消费者,实现方案二

在App.java中,你可以指定调用Consumer1还是Consumer2。

对于前者(Consumer1),staging目录下的文件数目不停的增长,并且如log显示,有很多冲突:一个Consumer准备处理的文件已经被其他的Consumer处理完了,造成了很多无效的操作,由于消费速度更不上生产速度,DIR1被撑爆只是时间的问题。

对于后者(Consumer2),staging目录下的文件几乎马上就会被移动到tmp目录下,大部分时间,文件数都为0。而tmp目录下,在程序稳定后大概保存在20多个文件左右,保持一个动态的平衡。用这种方式,你也会看到很多冲突,但是只会发生在程序刚开始,原因是,刚开始的时候,8个线程几乎是同时去访问staging目录,势必拿到很多相同的文件,待到稳定后,就很少有冲突发生了。

这两种方案都是最基本的,没有借助于第三方工具完成的,成本是最低的,其实还有一些其他的方案,可能会借助一些服务来实现,比如消息分发、数据库等。有时间的话,我继续补充。

Google Gears Launch

Google Reader于今日发布了”Google Gears”,相比较Google其他的一些实验室产品,Google Gears是一个革命性的开发工具/模式。它主要解决了一个问题,而这个问题也是影响WEB体验一个重要的问题,就是:用户处于离线状态,网络应用该如何继续。通常情况下,网络断后,我们只能等待网络接通后继续,有了Google Gears,一切将改变。使用GG开发的Web应用程序,会在网络连通的时候下载一些必要的数据,缓存到本地,当网络处于离线状态时,用户的数据将保存在本地,等到网络再次可以访问时,用户在离线时期所作的更改将和服务器进行同步,这样离线与否对于用户体验并没有多大的影响,这在以前是不可想像的。我们可以设想一下有了Google Gears将会对那些原有产品产生影响,Google Document就是其中一个典型,原先有很多人批评Google Doc只能在线使用,一旦离线,所有的一切都是空谈。有了GG,用户所编辑的文档都可以缓存在本地计算机,即使网络断了,只要等网络恢复后,这些数据将会和线上的数据同步,以保证数据的完整性。

但是它也有其不完美的地方,比如:某个数据被多个用户共同访问,如果某个用户断线了,那么该用户在离线模式下所作的修改该如何和其他用户的数据同步。等等。

Google用 LocalServer来管理对离线应用程序的访问,而 LocalServer使用 Database来管理用户数据,需要提一下:Google这次选择了SQLite,这是个开源的数据库系统,非常小巧,但功能丝毫不差。如果有大量的数据需要同步,WorkerPool则可以解决这个问题,有兴趣的朋友可以去Google Gears的官方站点看看:http://code.google.com/apis/gears/index.html

下图是我今天登录Google Reader的时候,Google Gears 插件同步数据的图示:

Google Gears

Update an app in a matter of time & Solution (draft)

类似MSN, QQ等, 如果要在短时间内更新大量的客户端程序, 这个过程对于服务器端的压力是非常的大的, 在没有更多服务器的情况下, 下载策略尤为重要.

暂时拟定一个方案:

Auto Update

Single Sign-On

单点登录,即Single Sign-On使得用户只需在一个Entry Point登录便可以在多个应用之间来回的切换,而无须再次登录,这样大大的增强的用户体验,最为著名的SSO的例子就是MS的Passport,想必大家已经体会到了SSO的便捷功能。

如果你Google SSO,得到的大都是WEB站点和WEB站点之间的SSO技术,对于Windows应用程序和WEB站点之间共享验证信息的技术几乎没有,目前IM软件中通常采用这样的技术,比如MS的Windows Live Messenger以及Tencent的QQ等。在这样软件中,用户只要在IM中登录,通过程序界面打开浏览器浏览WEB站点,就不需要再次登录了。

下面是我的一种解决方案:

比如用户要访问:http://www.googlestop.com/user/cp,这里假设用户名为:charry,密码为:123456,该密码MD5后为:e10adc3949ba59abbe56e057f20f883e。如果不考虑安全问题,我们可以把所有的信息都追加在URL中,比如:

http://www.googlestop.com/usre/cp/?uid=charry&passwd=e10adc3949ba59abbe56e057f20f883e

这样服务器端只要验证该用户名和密码是否都正确就可以判断来访者是否是个有效用户。但是这样做有很多缺点,比如:将用户名和密码的Hash值都以明文的方式呈现出来是个安全隐患,如果Cracker收集了这些信息将导致安全问题,还有,用户完全可以把这个URL复制出来,发给另一个用户,当另一个用户访问该URL时,他也被认为是一个合法用户。并且这个URL可以被保存下来,任何时候都可以再次使用,这么一来,Sign-In模块就形同虚设了。

如果我们把用户名和密码再次加密一次,是不是就不会导致安全性的问题了呢?回答是肯定的。针对上述的URL,我们可以把

http://www.googlestop.com/usre/cp/?uid=charry&passwd=e10adc3949ba59abbe56e057f20f883e

加密后再放到URL中。这里一定要保证的是此次加密是可逆的,否则发给服务器后,将毫无用处。注意,这里的加密算法是可逆的,我们假定选择AES算法。首先使用一个KEY将上述的URL加密,假设得到的密文是:

#$DKJFSDFSDFK(不可能这么短,只是假设)

这样我们把加密后URL,提交给 http://www.googlestop.com/redir/?q=#$DKJFSDFSDFK。服务器端通过把#$DKJFSDFSDFK解密后,就得到了

http://www.googlestop.com/usre/cp/?uid=charry&passwd=e10adc3949ba59abbe56e057f20f883e

这样它就知道用户要访问的URL、用户的ID、用户的密码等信息。

问题是,那个用于加密和解密的KEY该如何生成并告知服务器端?我们可以让程序在用户打开浏览器前,随机生成一个KEY,比如”sdfk7879#@”,然后用这个KEY加密上述的URL,然后马上在程序内部使用HTTPS协议把这个KEY发给服务器端,并保存到表中。当服务器端接到一个请求时,它首先去表中查询,是不是在短时间内(比如30秒)有一个KEY被发过来,如果有,就取得该KEY,并用KEY解密URL中得密文。接下来得事情就好办了。

上面的解决方法中存在一个隐患,如果在一个短时间内有大量的这样的请求,将导致大量的KEY被发到服务器的表中,那么服务器端如何分辨每个用户呢?解决起来也容易,在每次用户请求前,除了随机生成一个KEY外,我们再多生成一个GUID,我们把KEY和GUID一起发给服务器(HTTPS方式),作为两个字段,GUID为主键,同时明文的URL中,我们也加上这个GUID(假设为:19270efa-a213-4733-b117-bdeb4b4528a2)

http://www.googlestop.com/redir/?q=#$DKJFSDFSDFK&GUID=19270efa-a213-4733-b117-bdeb4b4528a2

这样服务器端就可以通过GUID来查找KEY了。

上面使用的是GET的方法提交,有可能会超过GET的长度限制,我们也可以用POST的方法,只是需要借助一个临时html文件来中转,好像QQ就是这么做的。

To be continued…