庄周梦蝶

生活、程序、未来
   :: 首页 ::  ::  :: 聚合  :: 管理

    在初步确定CMS参数后,系统运行了几天,今天尝试在线上打开了GC日志,按阿宝同学的说法是gc日志的开销比之jstat还小,打开之后发现确实影响很小。打开GC日志之后又发现几个隐藏的问题比较有价值,这里记录下。

   首先是系统在启动的时候有一次System.gc()调用引起的full gc,日志输出类似这样:
1.201: [Full GC (System) 1.201: [CMS: 0K->797K(1310720K), 0.1090540 secs] 29499K->797K(1546688K), [CMS Perm : 5550K->5547K(65536K)], 0.1091860 secs] [Times: user=0.05 sys=0.06, real=0.11 secs]
   可以确认的是我们系统里的代码绝对没有调用System.gc()方法,但是不保证第三方代码有调用,通过搜索代码引用,后来定位到了mina的ByteBuffer创建上面。Mina 1.1封装的ByteBuffer的allocate()方法默认创建的是Direct ByteBuffer,而DirectByteBuffer的构造函数里调用了
Bits.reserveMemory(cap);

这个方法强制调用了System.gc():
static void reserveMemory(long size) {

    
synchronized (Bits.class) {
        
if (!memoryLimitSet && VM.isBooted()) {
        maxMemory 
= VM.maxDirectMemory();
        memoryLimitSet 
= true;
        }
        
if (size <= maxMemory - reservedMemory) {
        reservedMemory 
+= size;
        
return;
        }
    }

    System.gc();
    
try {
        Thread.sleep(
100);
    } 
catch (InterruptedException x) {
        
// Restore interrupt status
        Thread.currentThread().interrupt();
    }
    
synchronized (Bits.class) {
        
if (reservedMemory + size > maxMemory)
        
throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory 
+= size;
    }

    }
    调用这个方法是为了用户对Direct ByteBuffer的内存可控。而在我们系统中使用的通讯层初始化Decoder的时候通过Mina 1.1创建了一个Direct ByteBuffer,导致了这一次强制的full gc。这个Buffer因为是长期持有的,因此创建Direct类型也还可以接受。

    但是在这次GC后,又发现每隔一个小时就有一次System.gc()引起的full gc,这就太难以忍受了,日志大概是这样,注意间隔时间都是3600秒左右:
10570.672: [Full GC (System) 10570.672: [CMS: 779199K->107679K(1310720K), 1.2957430 secs] 872163K->107679K(1546688K), [CMS Perm : 23993K->15595K(65536K)], 1.2959630 secs] [Times: user=1.27 sys=0.02, real=1.30 secs] 
14171.971: [Full GC (System) 14171.971: [CMS: 680799K->83681K(1310720K), 1.0171580 secs] 836740K->83681K(1546688K), [CMS Perm : 20215K->15599K(65536K)], 1.0173850 secs] [Times: user=0.97 sys=0.01, real=1.02 secs] 
17774.020: [Full GC (System) 17774.020: [CMS: 676201K->79331K(1310720K), 0.9652670 secs] 817596K->79331K(1546688K), [CMS Perm : 22808K->15619K(65536K)], 0.9655150 secs] [Times: user=0.93 sys=0.02, real=0.97 secs] 
21374.989: [Full GC (System) 21374.989: [CMS: 677818K->78590K(1310720K), 0.9297080 secs] 822317K->78590K(1546688K), [CMS Perm : 16435K->15593K(65536K)], 0.9299620 secs] [Times: user=0.89 sys=0.01, real=0.93 secs] 
24976.948: [Full GC (System) 24976.948: [CMS: 659511K->77608K(1310720K), 0.9255360 secs] 794004K->77608K(1546688K), [CMS Perm : 22359K->15594K(65536K)], 0.9257760 secs] [Times: user=0.88 sys=0.02, real=0.93 secs] 
28578.892: [Full GC (System) 28578.892: [CMS: 562058K->77572K(1310720K), 0.8365500 secs] 735072K->77572K(1546688K), [CMS Perm : 15840K->15610K(65536K)], 0.8367990 secs] [Times: user=0.82 sys=0.00, real=0.84 secs] 
32179.731: [Full GC (System) 32179.732: [CMS: 549874K->77224K(1310720K), 0.7864400 secs] 561803K->77224K(1546688K), [CMS Perm : 16016K->15597K(65536K)], 0.7866540 secs] [Times: user=0.75 sys=0.01, real=0.79 secs]

    搜遍了源码和依赖库,没有再发现显式的gc调用,问题只能出在运行时上,突然想起我们的系统使用RMI暴露JMX给监控程序,监控程序通过RMI连接JMX监控系统和告警等,会不会是RMI的分布式垃圾收集导致的?果然,一查资料,RMI的分布式收集会强制调用System.gc()来进行分布式GC,server端的间隔恰好是一个小时,这个参数可以通过:
-Dsun.rmi.dgc.server.gcInterval=3600000
来调整。调长时间是一个解决办法,但是我们更希望能不出现显式的GC调用,禁止显式GC调用通过-XX:+DisableExplicitGC是一个办法,但是禁止了分布式GC会导致什么问题却是心理没底,毕竟我们的JMX调用还是很频繁的,幸运的是JDK6还提供了另一个选项-XX:+ExplicitGCInvokesConcurrent,允许System.gc()也并发运行,调整DGC时间间隔加上这个选项双管齐下彻底解决了full gc的隐患。

    打开GC日志后发现的另一个问题是remark的时间过长,已经启用了并行remark,但是时间还是经常超过200毫秒,这个可能的原因有两个:我们的年老代太大或者触发CMS的阀值太高了,CMS进行的时候年老代里的对象已经太多。初步的计划是调小-XX:SurvivorRatio增大救助空间并且降低-XX:CMSInitiatingOccupancyFraction这个阀值。此外,还找到另一个可选参数-XX:+CMSScavengeBeforeRemark,启用这个选项后,强制remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次相对较长时间minor gc,如果你的minor gc很快的话可以考虑下这个选项,暂未实验。


posted @ 2009-09-22 20:58 dennis 阅读(3488) | 评论 (0)编辑 收藏

    首先感谢阿宝同学的帮助,我才对这个gc算法的调整有了一定的认识,而不是停留在过去仅仅了解的阶段。在读过sun的文档和跟阿宝讨论之后,做个小小的总结。
    CMS,全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。在我们的应用中,因为有缓存的存在,并且对于响应时间也有比较高的要求,因此希望能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便获得更短的垃圾回收的暂停时间,提高程序的响应性。
    CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:
    初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)。
    其中的1,3两个步骤需要暂停所有的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后,暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且remark这个阶段可以并行标记。

    而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或者多个垃圾回收线程和应用程序线程并发地运行,垃圾回收线程不会暂停应用程序的执行,如果你有多于一个处理器,那么并发收集线程将与应用线程在不同的处理器上运行,显然,这样的开销就是会降低应用的吞吐量。Remark阶段的并行,是指暂停了所有应用程序后,启动一定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的。

    CMS的young generation的回收采用的仍然是并行复制收集器,这个跟Paralle gc算法是一致的。

    下面是参数介绍和遇到的问题总结,

1、启用CMS:-XX:+UseConcMarkSweepGC。 咳咳,这里犯过一个低级错误,竟然将+号写成了-号

2。CMS默认启动的回收线程数目是  (ParallelGCThreads + 3)/4) ,如果你需要明确设定,可以通过-XX:ParallelCMSThreads=20来设定,其中ParallelGCThreads是年轻代的并行收集线程数

3、CMS是不会整理堆碎片的,因此为了防止堆碎片引起full gc,通过会开启CMS阶段进行合并碎片选项:-XX:+UseCMSCompactAtFullCollection,开启这个选项一定程度上会影响性能,阿宝的blog里说也许可以通过配置适当的CMSFullGCsBeforeCompaction来调整性能,未实践。

4.为了减少第二次暂停的时间,开启并行remark: -XX:+CMSParallelRemarkEnabled,如果remark还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次minor gc。

5.为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:

+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled


6.默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值:
-XX:CMSInitiatingOccupancyFraction=80

这里修改成80%沾满的时候才开始CMS回收。

7.年轻代的并行收集线程数默认是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),如果你希望设定这个线程数,可以通过-XX:ParallelGCThreads= N 来调整。

8.进入重点,在初步设置了一些参数后,例如:
-server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=64m
-XX:MaxPermSize=64m -XX:-UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSParallelRemarkEnabled
-XX:SoftRefLRUPolicyMSPerMB=0

需要在生产环境或者压测环境中测量这些参数下系统的表现,这时候需要打开GC日志查看具体的信息,因此加上参数:

-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log

在运行相当长一段时间内查看CMS的表现情况,CMS的日志输出类似这样:
4391.322: [GC [1 CMS-initial-mark: 655374K(1310720K)] 662197K(1546688K), 0.0303050 secs] [Times: user=0.02 sys=0.02, real=0.03 secs]
4391.352: [CMS-concurrent-mark-start]
4391.779: [CMS-concurrent-mark: 0.427/0.427 secs] [Times: user=1.24 sys=0.31, real=0.42 secs]
4391.779: [CMS-concurrent-preclean-start]
4391.821: [CMS-concurrent-preclean: 0.040/0.042 secs] [Times: user=0.13 sys=0.03, real=0.05 secs]
4391.821: [CMS-concurrent-abortable-preclean-start]
4392.511: [CMS-concurrent-abortable-preclean: 0.349/0.690 secs] [Times: user=2.02 sys=0.51, real=0.69 secs]
4392.516: [GC[YG occupancy: 111001 K (235968 K)]4392.516: [Rescan (parallel) , 0.0309960 secs]4392.547: [weak refs processing, 0.0417710 secs] [1 CMS-remark: 655734K(1310720K)] 766736K(1546688K), 0.0932010 secs] [Times: user=0.17 sys=0.00, real=0.09 secs]
4392.609: [CMS-concurrent-sweep-start]
4394.310: [CMS-concurrent-sweep: 1.595/1.701 secs] [Times: user=4.78 sys=1.05, real=1.70 secs]
4394.310: [CMS-concurrent-reset-start]
4394.364: [CMS-concurrent-reset: 0.054/0.054 secs] [Times: user=0.14 sys=0.06, real=0.06 secs]

其中可以看到CMS-initial-mark阶段暂停了0.0303050秒,而CMS-remark阶段暂停了0.0932010秒,因此两次暂停的总共时间是0.123506秒,也就是123毫秒左右。两次短暂停的时间之和在200以下可以称为正常现象。

但是你很可能遇到两种fail引起full gc:Prommotion failed和Concurrent mode failed。

Prommotion failed的日志输出大概是这样:
 [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]42576.951: [CMS: 1139969K->1120688K(
2166784K), 
9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]

这个问题的产生是由于救助空间不够,从而向年老代转移对象,年老代没有足够的空间来容纳这些对象,导致一次full gc的产生。解决这个问题的办法有两种完全相反的倾向:增大救助空间、增大年老代或者去掉救助空间。增大救助空间就是调整-XX:SurvivorRatio参数,这个参数是Eden区和Survivor区的大小比值,默认是32,也就是说Eden区是Survivor区的32倍大小,要注意Survivo是有两个区的,因此Surivivor其实占整个young genertation的1/34。调小这个参数将增大survivor区,让对象尽量在survitor区呆长一点,减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代,加快年老代的回收频率,减少年老代暴涨的可能性,这个是通过将-XX:SurvivorRatio 设置成比较大的值(比如65536)来做到。在我们的应用中,将young generation设置成256M,这个值相对来说比较大了,而救助空间设置成默认大小(1/34),从压测情况来看,没有出现prommotion failed的现象,年轻代比较大,从GC日志来看,minor gc的时间也在5-20毫秒内,还可以接受,因此暂不调整。

Concurrent mode failed的产生是由于CMS回收年老代的速度太慢,导致年老代在CMS完成前就被沾满,引起full gc,避免这个现象的产生就是调小-XX:CMSInitiatingOccupancyFraction参数的值,让CMS更早更频繁的触发,降低年老代被沾满的可能。我们的应用暂时负载比较低,在生产环境上年老代的增长非常缓慢,因此暂时设置此参数为80。在压测环境下,这个参数的表现还可以,没有出现过Concurrent mode failed。


参考资料:
JDK5.0垃圾收集优化之--Don't Pause》 by 江南白衣
《记一次Java GC调整经历》1,2 by Arbow
Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
Tuning Garbage Collection with the 5.0 JavaTM Virtual Machine

  


posted @ 2009-09-22 02:10 dennis 阅读(18249) | 评论 (1)编辑 收藏

    按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

   Java nio 2.0的主要改进就是引入了异步IO(包括文件和网络),这里主要介绍下异步网络IO API的使用以及框架的设计,以TCP服务端为例。首先看下为了支持AIO引入的新的类和接口:

 java.nio.channels.AsynchronousChannel
       标记一个channel支持异步IO操作。

 java.nio.channels.AsynchronousServerSocketChannel
       ServerSocket的aio版本,创建TCP服务端,绑定地址,监听端口等。

 java.nio.channels.AsynchronousSocketChannel
       面向流的异步socket channel,表示一个连接。

 java.nio.channels.AsynchronousChannelGroup
       异步channel的分组管理,目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池,这个线程池执行两个任务:处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个AsynchronousChannelGroup,那么通过AsynchronousServerSocketChannel创建的AsynchronousSocketChannel将同属于一个组,共享资源

 java.nio.channels.CompletionHandler
       异步IO操作结果的回调接口,用于定义在IO操作完成后所作的回调工作。AIO的API允许两种方式来处理异步操作的结果:返回的Future模式或者注册CompletionHandler,我更推荐用CompletionHandler的方式,这些handler的调用是由AsynchronousChannelGroup的线程池派发的。显然,线程池的大小是性能的关键因素。AsynchronousChannelGroup允许绑定不同的线程池,通过三个静态方法来创建:
 public static AsynchronousChannelGroup withFixedThreadPool(int nThreads,
                                                               ThreadFactory threadFactory)
        
throws IOException

 
public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,
                                                                
int initialSize)

 
public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)
        
throws IOException

     需要根据具体应用相应调整,从框架角度出发,需要暴露这样的配置选项给用户。

     在介绍完了aio引入的TCP的主要接口和类之后,我们来设想下一个aio框架应该怎么设计。参考非阻塞nio框架的设计,一般都是采用Reactor模式,Reacot负责事件的注册、select、事件的派发;相应地,异步IO有个Proactor模式,Proactor负责CompletionHandler的派发,查看一个典型的IO写操作的流程来看两者的区别:

     Reactor:  send(msg) -> 消息队列是否为空,如果为空  -> 向Reactor注册OP_WRITE,然后返回 -> Reactor select -> 触发Writable,通知用户线程去处理 ->先注销Writable(很多人遇到的cpu 100%的问题就在于没有注销),处理Writeable,如果没有完全写入,继续注册OP_WRITE。注意到,写入的工作还是用户线程在处理。
     Proactor: send(msg) -> 消息队列是否为空,如果为空,发起read异步调用,并注册CompletionHandler,然后返回。 -> 操作系统负责将你的消息写入,并返回结果(写入的字节数)给Proactor -> Proactor派发CompletionHandler。可见,写入的工作是操作系统在处理,无需用户线程参与。事实上在aio的API中,AsynchronousChannelGroup就扮演了Proactor的角色。

    CompletionHandler有三个方法,分别对应于处理成功、失败、被取消(通过返回的Future)情况下的回调处理:

public interface CompletionHandler<V,A> {

     
void completed(V result, A attachment);

    
void failed(Throwable exc, A attachment);

   
    
void cancelled(A attachment);
}

    其中的泛型参数V表示IO调用的结果,而A是发起调用时传入的attchment。

    在初步介绍完aio引入的类和接口后,我们看看一个典型的tcp服务端是怎么启动的,怎么接受连接并处理读和写,这里引用的代码都是yanf4j 的aio分支中的代码,可以从svn checkout,svn地址: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio

    第一步,创建一个AsynchronousServerSocketChannel,创建之前先创建一个AsynchronousChannelGroup,上文提到AsynchronousServerSocketChannel可以绑定一个AsynchronousChannelGroup,那么通过这个AsynchronousServerSocketChannel建立的连接都将同属于一个AsynchronousChannelGroup并共享资源:
this.asynchronousChannelGroup = AsynchronousChannelGroup
                    .withCachedThreadPool(Executors.newCachedThreadPool(),
                            
this.threadPoolSize);

    然后初始化一个AsynchronousServerSocketChannel,通过open方法:
this.serverSocketChannel = AsynchronousServerSocketChannel
                .open(
this.asynchronousChannelGroup);

    通过nio 2.0引入的SocketOption类设置一些TCP选项:
this.serverSocketChannel
                    .setOption(
                            StandardSocketOption.SO_REUSEADDR,
true);
this.serverSocketChannel
                    .setOption(
                            StandardSocketOption.SO_RCVBUF,
16*1024);

    绑定本地地址:

this.serverSocketChannel
                    .bind(
new InetSocketAddress("localhost",8080), 100);
   
    其中的100用于指定等待连接的队列大小(backlog)。完了吗?还没有,最重要的监听工作还没开始,监听端口是为了等待连接上来以便accept产生一个AsynchronousSocketChannel来表示一个新建立的连接,因此需要发起一个accept调用,调用是异步的,操作系统将在连接建立后,将最后的结果——AsynchronousSocketChannel返回给你:

public void pendingAccept() {
        
if (this.started && this.serverSocketChannel.isOpen()) {
            
this.acceptFuture = this.serverSocketChannel.accept(null,
                    
new AcceptCompletionHandler());

        } 
else {
            
throw new IllegalStateException("Controller has been closed");
        }
    }
   注意,重复的accept调用将会抛出PendingAcceptException,后文提到的read和write也是如此。accept方法的第一个参数是你想传给CompletionHandler的attchment,第二个参数就是注册的用于回调的CompletionHandler,最后返回结果Future<AsynchronousSocketChannel>。你可以对future做处理,这里采用更推荐的方式就是注册一个CompletionHandler。那么accept的CompletionHandler中做些什么工作呢?显然一个赤裸裸的AsynchronousSocketChannel是不够的,我们需要将它封装成session,一个session表示一个连接(mina里就叫IoSession了),里面带了一个缓冲的消息队列以及一些其他资源等。在连接建立后,除非你的服务器只准备接受一个连接,不然你需要在后面继续调用pendingAccept来发起另一个accept请求
private final class AcceptCompletionHandler implements
            CompletionHandler
<AsynchronousSocketChannel, Object> {

        @Override
        
public void cancelled(Object attachment) {
            logger.warn(
"Accept operation was canceled");
        }

        @Override
        
public void completed(AsynchronousSocketChannel socketChannel,
                Object attachment) {
            
try {
                logger.debug(
"Accept connection from "
                        
+ socketChannel.getRemoteAddress());
                configureChannel(socketChannel);
                AioSessionConfig sessionConfig 
= buildSessionConfig(socketChannel);
                Session session 
= new AioTCPSession(sessionConfig,
                        AioTCPController.
this.configuration
                                .getSessionReadBufferSize(),
                        AioTCPController.
this.sessionTimeout);
                session.start();
                registerSession(session);
            } 
catch (Exception e) {
                e.printStackTrace();
                logger.error(
"Accept error", e);
                notifyException(e);
            } 
finally {
                pendingAccept();
            }
        }

        @Override
        
public void failed(Throwable exc, Object attachment) {
            logger.error(
"Accept error", exc);
            
try {
                notifyException(exc);
            } 
finally {
                pendingAccept();
            }
        }
    }
   
    注意到了吧,我们在failed和completed方法中在最后都调用了pendingAccept来继续发起accept调用,等待新的连接上来。有的同学可能要说了,这样搞是不是递归调用,会不会堆栈溢出?实际上不会,因为发起accept调用的线程与CompletionHandler回调的线程并非同一个,不是一个上下文中,两者之间没有耦合关系。要注意到,CompletionHandler的回调共用的是AsynchronousChannelGroup绑定的线程池,因此千万别在回调方法中调用阻塞或者长时间的操作,例如sleep,回调方法最好能支持超时,防止线程池耗尽。

    连接建立后,怎么读和写呢?回忆下在nonblocking nio框架中,连接建立后的第一件事是干什么?注册OP_READ事件等待socket可读。异步IO也同样如此,连接建立后马上发起一个异步read调用,等待socket可读,这个是Session.start方法中所做的事情:

public class AioTCPSession {
    
protected void start0() {
        pendingRead();
    }

    
protected final void pendingRead() {
        
if (!isClosed() && this.asynchronousSocketChannel.isOpen()) {
            
if (!this.readBuffer.hasRemaining()) {
                
this.readBuffer = ByteBufferUtils
                        .increaseBufferCapatity(
this.readBuffer);
            }
            
this.readFuture = this.asynchronousSocketChannel.read(
                    
this.readBuffer, thisthis.readCompletionHandler);
        } 
else {
            
throw new IllegalStateException(
                    
"Session Or Channel has been closed");
        }
    }
   
}

     AsynchronousSocketChannel的read调用与AsynchronousServerSocketChannel的accept调用类似,同样是非阻塞的,返回结果也是一个Future,但是写的结果是整数,表示写入了多少字节,因此read调用返回的是Future<Integer>,方法的第一个参数是读的缓冲区,操作系统将IO读到数据拷贝到这个缓冲区,第二个参数是传递给CompletionHandler的attchment,第三个参数就是注册的用于回调的CompletionHandler。这里保存了read的结果Future,这是为了在关闭连接的时候能够主动取消调用,accept也是如此。现在可以看看read的CompletionHandler的实现:
public final class ReadCompletionHandler implements
        CompletionHandler
<Integer, AbstractAioSession> {

    
private static final Logger log = LoggerFactory
            .getLogger(ReadCompletionHandler.
class);
    
protected final AioTCPController controller;

    
public ReadCompletionHandler(AioTCPController controller) {
        
this.controller = controller;
    }

    @Override
    
public void cancelled(AbstractAioSession session) {
        log.warn(
"Session(" + session.getRemoteSocketAddress()
                
+ ") read operation was canceled");
    }

    @Override
    
public void completed(Integer result, AbstractAioSession session) {
        
if (log.isDebugEnabled())
            log.debug(
"Session(" + session.getRemoteSocketAddress()
                    
+ ") read +" + result + " bytes");
        
if (result < 0) {
            session.close();
            
return;
        }
        
try {
            
if (result > 0) {
                session.updateTimeStamp();
                session.getReadBuffer().flip();
                session.decode();
                session.getReadBuffer().compact();
            }
        } 
finally {
            
try {
                session.pendingRead();
            } 
catch (IOException e) {
                session.onException(e);
                session.close();
            }
        }
        controller.checkSessionTimeout();
    }

    @Override
    
public void failed(Throwable exc, AbstractAioSession session) {
        log.error(
"Session read error", exc);
        session.onException(exc);
        session.close();
    }

}

   如果IO读失败,会返回失败产生的异常,这种情况下我们就主动关闭连接,通过session.close()方法,这个方法干了两件事情:关闭channel和取消read调用:
if (null != this.readFuture) {
            
this.readFuture.cancel(true);
        }
this.asynchronousSocketChannel.close();
   在读成功的情况下,我们还需要判断结果result是否小于0,如果小于0就表示对端关闭了,这种情况下我们也主动关闭连接并返回。如果读到一定字节,也就是result大于0的情况下,我们就尝试从读缓冲区中decode出消息,并派发给业务处理器的回调方法,最终通过pendingRead继续发起read调用等待socket的下一次可读。可见,我们并不需要自己去调用channel来进行IO读,而是操作系统帮你直接读到了缓冲区,然后给你一个结果表示读入了多少字节,你处理这个结果即可。而nonblocking IO框架中,是reactor通知用户线程socket可读了,然后用户线程自己去调用read进行实际读操作。这里还有个需要注意的地方,就是decode出来的消息的派发给业务处理器工作最好交给一个线程池来处理,避免阻塞group绑定的线程池。
  
   IO写的操作与此类似,不过通常写的话我们会在session中关联一个缓冲队列来处理,没有完全写入或者等待写入的消息都存放在队列中,队列为空的情况下发起write调用:


    
protected void write0(WriteMessage message) {
        
boolean needWrite = false;
        
synchronized (this.writeQueue) {
            needWrite 
= this.writeQueue.isEmpty();
            
this.writeQueue.offer(message);
        }
        
if (needWrite) {
            pendingWrite(message);
        }
    }

    
protected final void pendingWrite(WriteMessage message) {
        message 
= preprocessWriteMessage(message);
        
if (!isClosed() && this.asynchronousSocketChannel.isOpen()) {
            
this.asynchronousSocketChannel.write(message.getWriteBuffer(),
                    
thisthis.writeCompletionHandler);
        } 
else {
            
throw new IllegalStateException(
                    
"Session Or Channel has been closed");
        }
    }


    write调用返回的结果与read一样是一个Future<Integer>,而write的CompletionHandler处理的核心逻辑大概是这样:
@Override
    
public void completed(Integer result, AbstractAioSession session) {
        
if (log.isDebugEnabled())
            log.debug(
"Session(" + session.getRemoteSocketAddress()
                    
+ ") writen " + result + " bytes");
                
        WriteMessage writeMessage;
        Queue
<WriteMessage> writeQueue = session.getWriteQueue();
        
synchronized (writeQueue) {
            writeMessage 
= writeQueue.peek();
            
if (writeMessage.getWriteBuffer() == null
                    
|| !writeMessage.getWriteBuffer().hasRemaining()) {
                writeQueue.remove();
                
if (writeMessage.getWriteFuture() != null) {
                    writeMessage.getWriteFuture().setResult(Boolean.TRUE);
                }
                
try {
                    session.getHandler().onMessageSent(session,
                            writeMessage.getMessage());
                } 
catch (Exception e) {
                    session.onException(e);
                }
                writeMessage 
= writeQueue.peek();
            }
        }
        
if (writeMessage != null) {
            
try {
                session.pendingWrite(writeMessage);
            } 
catch (IOException e) {
                session.onException(e);
                session.close();
            }
        }
    }

   compete方法中的result就是实际写入的字节数,然后我们判断消息的缓冲区是否还有剩余,如果没有就将消息从队列中移除,如果队列中还有消息,那么继续发起write调用。

   重复一下,这里引用的代码都是yanf4j aio分支中的源码,感兴趣的朋友可以直接check out出来看看: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio。
   在引入了aio之后,java对于网络层的支持已经非常完善,该有的都有了,java也已经成为服务器开发的首选语言之一。java的弱项在于对内存的管理上,由于这一切都交给了GC,因此在高性能的网络服务器上还是Cpp的天下。java这种单一堆模型比之erlang的进程内堆模型还是有差距,很难做到高效的垃圾回收和细粒度的内存管理。

   这里仅仅是介绍了aio开发的核心流程,对于一个网络框架来说,还需要考虑超时的处理、缓冲buffer的处理、业务层和网络层的切分、可扩展性、性能的可调性以及一定的通用性要求。

    

posted @ 2009-09-20 14:02 dennis 阅读(11805) | 评论 (10)编辑 收藏

    MQ在分布式系统中扮演着重要角色,异步的消息通信全要靠它,而异步通信正是提高系统伸缩性的不二良方。说说我认为的一个优秀的MQ产品需要具备的特征。

    首先显然是高可用性,我们当然希望MQ能支撑7x24小时应用,而不是三天两头当机,我们要追求的是99.9%的可靠服务时间。要做到高可用性,显然我们需要做MQ的集群,一台当了,不影响整个集群的服务能力,这里涉及到告警、流控、消息的负载均衡、数据库的使用、测试的完备程度等等。

    其次是消息存储的高可靠性。我们要保证100%不丢消息。要做到消息存储的高可靠性,不仅仅是MQ的责任,更涉及到硬件、操作系统、语言平台和数据库的一整套方案。许多号称可靠存储的MQ产品其实都不可靠,要知道,硬件错误是常态,如果在硬件错误的情况下还能保证消息的可靠存储这才是难题。这里可能需要用到特殊的存储硬件,特殊的数据库,分布式的数据存储,数据库的分库分表和同步等等。你要考虑消息存储在哪里,是文件系统,还是数据库,是本地文件,还是分布式文件,是搞主辅备份呢还是多主机写入等等。
   
    第三是高可扩展性,MQ集群能很好地支持水平扩展,这就要求我们的节点之间最好不要有通信和数据同步。

    第四是性能,性能是实现高可用性的前提,很难想象单机性能极差的MQ组成的集群能在高负载下幸免于难。性能因素跟采用的平台、语言、操作系统、代码质量、数据库、网络息息相关。MQ产品的核心其实是消息的存储,在保证存储安全的前提下如何保证和提高消息入队的效率是性能的关键因素。这里需要开发人员建立起性能观念,不需要你对一行行代码斤斤计较,但是你需要知道这样做会造成什么后果,有没有更好更快的方式,你怎么证明它更好更快。软件实现的性能是一方面,另一方面就是平台相关了,因为MQ本质上是IO密集型的系统,瓶颈在IO,如何优化网络IO、文件IO这需要专门的知识。性能另一个相关因素是消息的调度上,引入消息顺序和消息优先级,允许消息的延迟发送,都将增大消息发送调度的复杂性,如何保证高负载下的调度也是要特别注意的地方。

  
   第五,高可配置性和监控工具的完整,这是一个MQ产品容易忽略的地方。异步通信造成了查找问题的难度,不像同步调用那样有相对明确的时序关系。因此查找异步通信的异常是很困难的,这就需要MQ提供方便的DEBUG工具,查找分析日志的工具,查看消息生命周期的工具,查看系统间依赖关系的工具等等。可定制也是MQ产品非常重要的一方面,可方便地配置各类参数并在集群中同步,并且可动态调整各类参数,这将大大降低维护难度。

   一些不成熟的想法,瞎侃。


   


posted @ 2009-09-18 00:09 dennis 阅读(2851) | 评论 (1)编辑 收藏


    XMemcached 1.2.0-RC2 released,main highlights:

1、支持Kestrel。Kestrel是一个scala编写的简单高效的MQ,它是Twitter发布的开源产品,支持memcached协议,但并不完全兼容。更多信息看这里。Xmemcached提供了一个KestrelCommandFactory,用于对kestrel特性的支持。

2、新增了基于Election Hash的SessionLocator。Election Hash的详细解释看这里。简单来说就是每次查找key对应的节点的时候,都计算节点ip+key的MD5值,然后进行排序,取最大者为目标节点。这个算法解决的问题与Consistent Hash类似,但是因为每次都要计算,因此开销会比较大,适合节点数比较少的情况,避免了consistent hash为了节点比较均匀需要引入虚拟节点的问题。测试表明,Election Hash的结果也是比较均匀的,并且在节点增删的情况下能保持与一致性哈希相近的命中率。要使用election hash,请使用ElectionMemcachedSessionLocator

3、从RC1版本以来的Bug fixed.

   欢迎试用并反馈任何意见和BUG。


posted @ 2009-09-17 23:24 dennis 阅读(1436) | 评论 (0)编辑 收藏

    Kestrel是一个scala写的twitter开源的消息中间件,特点是高性能、小巧(2K行代码)、持久存储(记录日志到journal)并且可靠(支持可靠获取)。Kestrel的前身是Ruby写的Starling项目,后来twitter的开发人员尝试用scala重新实现。它的代码非常简洁并且优雅,推荐一读。

    Kestrel采用的协议是memcached的文本协议,但是并不完全支持所有memcached协议,也不是完全兼容现有协议。标准的协议它仅支持GET、SET、FLUSH_ALL、STATS,扩展的协议有:
           
              SHUTDOWN       关闭kestrel server     
              RELOAD         动态重新加载配置文件
              DUMP_CONFIG    dump配置文件
              FLUSH queueName   flush某个队列

    每个key对应都是一个队列。标准memcached文本协议的支持上也没有完全兼容,SET不支持flag,因此现有大多数基于flag做序列化的memcached client都无法存储任意java类型到kestrel;FLUSH_ALL返回"Flushed all queues.\r\n"而不是"OK\r\n"。

    GET协议支持阻塞获取和可靠获取,都是在key上作文章,例如你要获取queue1的消息,并且在没有消息的时候等待一秒钟,如果有消息马上返回,超时时间后还没有就返回空,kestrel允许你通过发送
                     "GET queue1/t=1000\r\n"

来阻塞获取。本来的key应该queue1,这里变成了"queue1/t=1000",因此如果你使用的client有对返回的key和发送的key做校验,那么可能就认为kestrel返回错误。
    什么是可靠获取呢?默认的GET是从队列中获取消息后,server端就将该消息从队列中移除,客户端需要自己保证不把这个消息丢失掉,也就是说这里是类似JMS规范中的自动应答(auto-acknowledge),如果客户端在处理这个消息的时候异常崩溃或者在接收消息数据的时候连接断开,那么可能导致这个消息永久丢失。Kestrel的可靠获取就是类似JMS规范中的CLIENT_ACK mode,客户端获取消息后,server将这个消息从队列移除并正常发送给客户端,如果这时候客户端崩溃或者连接断开,那么server将不会确认该消息被消费并且"un-get"这个消息,重新放到队列头部,那么当client重新连接上来的时候还可以获取这个消息;只有当server收到客户端的明确确认消息成功的时候,才将消息移除。这个功能也是通过key做手脚,

"GET queue1/open\r\n"   开始一次可靠获取
"GET queue1/close\r\n"  确认消费成功


你要关闭前一次可靠获取开启新的一次,还可以这样调用
"GET queue1/close/open\r\n"


    要注意的是每个连接的client只能有一个正在执行的可靠获取,关闭一个没有开启的reliable fetch或者在执行一次reliable fetch再次open一个新的获取都将直接返回空。

    从kestrel的协议方面,我们可以学习到的一点就是在做一份协议的时候,如果有多种不同语言的client的话,应该尽量用通用协议,通用协议通常都已经有很多成熟的client可以使用,避免了为私有协议开发不同语言的client;并且我们可以在通用协议上作扩展,例如kestrel在key上面做的花样,通过给key附加不同的属性即可实现一些特殊功能

    XMemcachedClient默认是无法支持kestrel对memcached的协议的扩展,也就是说无法支持阻塞获取、可靠获取和flush_all,这是因为xmemcached会对返回的key和发送的key做校验,如果不相等就认为解码错误;并且由于kestrel不支持flag,因此无法存储java序列化类型;另外一个问题是,xmemcached(spymemcached)都会将连续的GET协议合并成一个bulk get协议,而kestrel也并不支持bulk get,所以需要关闭这个优化,这个可以通过下列代码关闭:
memcachedClient.setOptimizeGet(false);

Spymemcached似乎不提供这个选项。为了解决序列化问题,我添加了一个新的KestrelCommandFactory,使用这个CommandFactory后,将默认关闭get优化,并且不对GET返回的key做校验从而支持阻塞获取和可靠获取,并且将在存储的数据之前加上4个字节的flag(整型),因此可以支持存储任意可序列化类型。但是有一些应用只需要存储字符串类型和原生类型,这是为了在不同语言的client之间保持可移植(如存储json数据),那么就不希望在数据之前加上这个flag,关闭这个功能可以通过
memcachedClient.setPrimitiveAsString(true);

方法来设置,所有的原生类型都将调用toString转成字符串来存储,字符串前不再自动附加flag。

    KestrelCommandFactory已经提交到svn trunk,预计在xmemcached 1.2.0-RC2的时候发布。

    使用KestrelCommandFactory对kestrel做的性能测试,server和client都跑在linux上,jdk6,单线程单client连续push消息

               消息个数       消息长度       是否启用journal   时间       TPS(/s)
                500000          256             否              123.0s     4065
                500000          1024            否              126.3s     3959
                500000          4096            否              120.6s     4145
                500000          4096            是              122.1s     4095
                500000          8192            是              121.2s     4125

从数据上来看比官方数据好很多,可能机器配置不同。是否启用journal带来的影响似乎很小,写文件都是append,还是比较高效的。

kestrel的项目主页  http://github.com/robey/kestrel
kestrel的wiki页    http://wiki.github.com/robey/kestrel
xmemcached项目主页  http://code.google.com/p/xmemcached/


posted @ 2009-09-15 11:34 dennis 阅读(15353) | 评论 (9)编辑 收藏

  XMemcached是一个基于java nio的Memcached Client,正式发布1.2.0-RC1版本。此版本又是一个里程碑版本,开始支持memcached的二进制协议,并添加了几个更有价值的功能。此版本的主要改进如下:

1、支持完整的memcached binary协议。XMemcached现在已经支持memcached的所有文本协议和二进制协议,成为一个比较完整的java client。Memcached的二进制协议带来更好的性能以及更好的可扩展性。在XMemcached中使用二进制协议,你只要添加一行代码:
 XMemcachedClientBuilder builder=.
  builder.setCommandFactory(
new BinaryCommandFactory());//此行

或者在Spring配置中增加一行配置:
<bean name="memcachedClient2"
        class
="net.rubyeye.xmemcached.utils.XMemcachedClientFactoryBean" destroy-method="shutdown">
   
   
<!--采用binary command -->
   
<property name="commandFactory">
           
<bean class="net.rubyeye.xmemcached.command.BinaryCommandFactory"></bean>

</bean>

2.支持与hibernate-memcached的集成Hibernate-memcached是可以将memcached作为hibernate二级缓存的开源项目,它默认采用的是Spymemcached,XMemcached 1.2.0开始提供对它的集成,具体的配置信息参考这里

3.兼容JDK5。XMemcached的1.x版本都仅能在jdk6上使用,从1.2.0-RC1开始,XMemcached开始兼容jdk5。当时考虑只支持jdk6是由于nio的Epoll Selector实现是在jdk6上成为默认,而jdk5需要设置环境变量。不过XMemcached 1.2.0-RC1将自动帮你判断是否是linux平台,并且判断是否可以启用epoll,如果可以,那么将在linux平台采用EPollSelectorProvider,这一切对用户来说是透明的。(注意,jdk5的低版本在linux平台仍然是没有epoll实现的)。

4.日志从common-logging迁移到slf4j。XMemcached现在必须的两个依赖包分别是slf4jyanf4j(1.0-SNAPSHOT).

5.另一个关键性的改进是允许设置连接池。众所周知,nio的client默认一般都是一个连接,传统的阻塞io采用连接池的方式提高效率。但是在典型的高并发场景下,nio的单连接也将遇到瓶颈,此时允许设置连接池将是一个可选的调优手段。XMemcached 1.2.0-RC1支持设置连接池,允许对同一个memcached节点建立多个连接,启用的代码如下:
MemcachedClient mc =.
mc.setConnectionPoolSize(2);

 默认的pool size是1。设置这一数值不一定能提高性能,请依据你的项目的测试结果为准。初步的测试表明只有在大并发下才有提升。设置连接池的一个不良后果就是,同一个memcached的连接之间的数据更新并非同步的,因此你的应用需要自己保证数据更新的原子性(采用CAS或者数据之间毫无关联)。

6、简化构建,移除ant构建,简化maven构建,现在只采用maven构建了。借助于wagon-svn这个扩展,可以将svn作为maven仓库,因此xmemcached的构建现在变的非常方便,下载源码后敲入mvn package即可。

7.升级yanf4j到1.0-SNAPSHOT版本,此版本引入了SocketOption类,方便设置socket选项,并为引入aio做了重构。

8、从1.1.3和1.2.0-beta以来的bug fixed.


欢迎使用和建议。

下载地址:
http://code.google.com/p/xmemcached/downloads/list


posted @ 2009-09-09 09:50 dennis 阅读(1732) | 评论 (2)编辑 收藏

    XMemcached的结构方面的文档比较少,可能对有兴趣了解它的基本结构,或者想读源码的朋友入手比较困难。画了两张UML图,一张是主要的类图,描述了主要的类和接口之间的关系和结构。一张是序列图,一次典型的get操作需要经过什么步骤。

    首先看类图,没有什么需要特别说明的。


再看一下get操作的序列图,需要注意的是等待响应的过程是异步的。



posted @ 2009-08-26 18:04 dennis 阅读(2266) | 评论 (2)编辑 收藏

    推迟了半个月之后,发布xmemcached-1.2.0的beta测试版本,此版本又是一个里程碑版本,主要亮点如下:

1、支持全部的二进制协议,包括noreply的二进制协议。memcached 1.4.0正式推出memcached的二进制协议,相比于文本协议,二进制协议更复杂,但是也更容易解析和编码,并且可扩展性也比较强,比如原来文本协议只允许key为String类型,二进制协议允许key是任意类型,并且长度可以达到2^16-1,大大超过原有的255的限制。另一方面,文本协议的可读性更好,在不同上平台上实现也比较容易,而二进制协议就可能需要考虑可移植性的问题。 
   xmemcached支持全部二进制协议后才算是一个比较完整的memcached的java客户端了。在实现上可能还有一些隐藏的BUG和问题,欢迎试用并反馈,注意,如果使用二进制协议,你的memcached版本是必须是最新的1.4.0。
   如果要使用二进制协议,你只需要添加一行代码:
       
             MemcachedClientBuilder builder = new XMemcachedClientBuilder(
                    AddrUtil.getAddresses(servers));
            
//添加下面这行,采用BinaryCommandFactory即可使用二进制协议          
            builder.setCommandFactory(new BinaryCommandFactory());
            MemcachedClient mc 
= builder.build();

2、支持hibernate-memcached,在某用户的要求下添加了此特性。hibernate-memcached允许你使用memcached作为hibernate的二级缓存,但是它默认使用的是Spymemcached,想替换成Xmemcached就需要做一些扩展,在1.2.0提供了这一支持。你需要做的是将memcacheClientFactory属性设置为Xmemcached的即可:

hibernate.memcached.memcacheClientFactory=net.rubyeye.xmemcached.utils.hibernate.XmemcachedClientFactory

更多设置参考wiki page.

3、1.1.3以来的一些bug fixed.

项目主页: http://code.google.com/p/xmemcached/
下载地址:  http://code.google.com/p/xmemcached/downloads/list

posted @ 2009-08-26 09:21 dennis 阅读(2731) | 评论 (4)编辑 收藏

    在这里要推荐下《观止-微软创建NT和未来的夺命狂奔》,非常精彩,讲述了windows NT开发过程中的人和事。这不仅仅是故事书,也可以看做一本项目管理方面的指南,可以看看这么巨大的项目(几百万行代码)所遭遇到的难题和痛楚。我更愿意将这本书当做《人月神话》的故事版,同样是创建划时代的OS,同样是管理众多人参与的大型的项目,也同样遭遇了种种困扰和痛苦,从这个角度也可以看出,人类的痛苦的相通的:)

   单纯从软件构建的角度去看这本书,可以说说我看到的东西,这些是我今天早上走在上班路上的时候想的,咳咳。

1、开发OS是烧钱的事情,NT开发接近5年,每年的花费据说在5000万美刀,那可是在90年代初期,换算成现在更是天文数字。从另一个侧面也说明了linux系统的伟大。开发一个这么烧钱的玩意,如果没有管理层的强力支持,那么不是被砍掉,就是遭遇流产的命运,幸运的是NT团队得到了盖茨的鼎力支持,大概也只有他能这么烧钱了。Dave Culter从DEC辞职的原因也是因为管理层砍掉了他的团队。盖茨另一个做法是不干涉NT团队的开发工作,他只提出目标和期望,然后就偶尔过来看看,不对不知道的东西指手画脚,这点可不容易。

2、每日构建非常重要,NT团队的构建实验室一开始是每周构建,后来做到了每日构建。只有每日构建,持续集成,才能帮你掌控产品质量,及时发现潜在的问题。我们现在的项目使用了hudson,比CC容易配置一点,效果还不错。

3、测试极其重要,专业的测试团队对于大型项目来说尤其重要。除了测试人员之外,开发人员需要做自测,需要对自己check-in的代码负责,如果你签入的代码导致构建失败,那么Dave culter可能冲破墙壁进来,拍着桌子冲你咆哮。对check in必须做严格控制和跟踪,如果在项目的最后冲击阶段,除了showstopper级别的修正代码允许签入之外,其他的修改都不被接受。开发者和测试人员很容易存在对立,检讨自己,我对测试人员也存在偏见和某种程度上的轻视和厌烦,如果从就事论事和都是为一个目标努力的角度来说,测试和开发并不对立,两者是相辅相成,甚至于测试人员更为至关重要。

4、在一个长期而复杂的项目中,如何保持团队成员的士气也是个难事儿。软件开发归根到底是的因素是人,而非工具或者其他,关注人,其实就是在关注你的软件。鼓励士气的常见做法就是设定里程碑,在这个里程碑上发布一个重要版本,让大家看到希望,但是对于OS这样的巨型项目来说,里程碑不是那么容易设定,这从书中项目的不断延期可以看到。另外就是宽松的工作环境和假期,微软的工作环境有目共睹,能做到每个员工独立一个办公室的国内企业还没有吧。国外的开发者似乎很会玩,赛车、滑雪、空手道,其实不是我们不会玩,是我们玩不起,国内的待遇和生活压力让你想玩也玩不起。
   可是就算是再好的物质待遇,其实也换不来美好生活,书中充斥着开发者对家庭和婚姻的困惑和痛苦,为了NT,他们也失去了很多,对工作过度投入的后果就是失去平衡的家庭生活,再次验证上帝是公平的,有得必有失,就看你看重的是什么。

5、开发者的效率差异是惊人的,在《人月神话》里已经说明了这一点,开发者之间的效率差异可以达到惊人的10倍,在NT这样的团队里也再次验证了这一结论。

6、投入越多的人力,并不能带来效率的提升,当NTFS文件系统的进度拖慢的时候,微软的经理们考虑添加人手,但是经过慎重的考虑还是没有加人,因为文件系统是技术活,新人很难马上投入开发,而需要老手的带领和培训,引入了更多的沟通成本和培训成本。

7、优秀的代码无法通过行数来衡量,软件某种程度上还真是魔法的产物。

8、NT的一个教训是,应该及早设定你的性能目标,并在适当时候开始关注并优化系统。NT团队后期的很大部分工作都是在优化系统性能,并缩小尺寸。

9、设定Deadline常常是不靠谱的事情,对软件开发的时间估计也常常是不靠谱的事情,这一点从NT的一次又一次的延期可以看出。延期失望的不仅仅是客户,也会打击你的团队成员,遥遥无期的开发过程容易让人崩溃。

10、NT的开发贯穿了对市场的需求的考虑,有个牛X的产品经理还是相当重要的。当然,没有开发者喜欢添加新功能,特别是在已经完成一个新功能的情况下,以至发展到NT的开发者看到产品经理就不由得拿起球棒击墙的地步:)

   这本书花了我两个晚上看完,还是看故事有趣呀,上面所说只是我的印象,书中还有许多八卦故事老少咸宜,如果有出入,请看原著:) 有空还得重读下。




posted @ 2009-08-13 12:44 dennis 阅读(991) | 评论 (2)编辑 收藏

仅列出标题
共56页: First 上一页 14 15 16 17 18 19 20 21 22 下一页 Last