关于大型asp.net应用系统的架构—如何做到高性能高可伸缩性
简介
前面一篇<<关于大型asp.net应用系统的架构-架构的选择>>写完之后,有一些同仁热心回复,有的是提问题,同时希望能举一些例子来说明;有的是提建议,希望下一篇写得更详细点;还有的同仁提出不同的观点。感谢大家的参与。会继续努力的。本文将针对Layer(层)和Tier(排)的区别做个辨析。并详细介绍3 Tier / N Tier架构中各Tier的开发。各Tier的分布式方式。以及为了达到高性能,低延迟,高可伸缩性,需要采取哪些方法和手段。
关于“大型asp.net应用系统 ”的概念
意指能支持同时在线用户数目很多的asp.net应用系统。同时在线用户数目要达到多少才算大型。其实也没有一个可以作为共识的定义,个人认为如果一个应用系统能做到7x24小时同时在线用户数不少于5000的,应该可以称为大型应用系统。例如:微软的官网www.microsoft.com,7x24小时都有来自全球的人访问,有查阅MSDN的,有访问微软博客的,有看微软产品信息的,有逛微软论坛的,等等等等。同时访问微软官网的人太多了,远多于5000。还有Myspace。 它有总数为几千万的用户,它的同时在线用户数也是相当惊人的。它之所以能服务众多的用户,是因其背后有一个庞大的系统来支撑。
Layer和Tier的辨析
这里针对上篇的评论,对Layer和Tier做个辨析。上篇提到了Layered(分层)的架构只能部署在同一台服务上,有同仁在评论里提出不同意见,说Layered的架构也可以部署到多台服务器上的。Layer是指应用程序各功能在逻辑上的分组,而Tier表示了应用程序各功能是物理分部在多台计算机上。Layer很好理解,就是相同功能的类被逻辑上分到了一组,如:数据存取的类都放到了一块,在同一个名称空间下,在同一个程序集里,商务逻辑的类也是一样进行分组,各组之间有统一的调用形式。如商务逻辑的类引用数据存取的类,调用其方法,取得返回结果。同时UI层可调用商务逻辑层的类。商务逻辑层的类既有服务UI层的功能,也有调用数据访问层的功能。是个承上启下的Layer。这些Layer都是按照功能来划分的。Layer是一种逻辑上的划分。Tier是特指物理的划分,应用程序的各功能,分别被放在了不同的服务器上,如UI功能单独占用一些服务器,商务逻辑功能占用另外的一些服务器。这两种功能部件之间有服务器的边界,那么就有专门负责分布式调用的功能部件。如果单从功能逻辑上看,Tier中也是有Layer的,只是比传统Layer的划分多了一些用于分布式调用的Layer。Tier是各Layer物理分离后,再加入一些负责分布式调用的Layer才形成的。Tier和Layer是有着联系的。从这个意义上说,Tier是Layer物理分离时的特例。有Layer物理分离的情况下,可以称之为Layered的架构,但是实际上这并不准确,因为Tier是专门为这个场景定义的。有物理分离,就叫Tier更准确些。Layer只要一做物理分离,就转化成了Tier。
从部署角度试图来区别Layered的架构和3 Tier / N Tier的架构。因为物理分离的场景已经被定义成Tier,那么剩下的就只能是物理不分离的场景了。所以Layered架构就特指部署在同一台服务上的场景(即物理不分离),3 Tier / N Tier架构就特指各Layer物理分离的场景。Layered的架构部署到多台服务器上,理论上是可以的,但是光靠原有的Layer是不够的,有了服务器的边界之后,原来在同一个进程里面的方法调用就不再可行,必须新加一些Layer来做分布式的调用,才能让原来的各Layer运行起来。等做完这一切,发现这个架构再叫Layered的架构就不合适了,必须得叫3 Tier / Tier架构才合适。
Layer和Tier之间有联系,Layered的架构和3 Tier / N Tier架构可以互相转化。
整体映象
从前面的描述中可以得知应用系统的每一Tier都是由许多服务器来完成的。比如UI Tier,可以是几十个服务器,几百个服务器,甚至是几千个服务器。具体每一个Tier所需服务器的数目根据实际的需要来配置。所谓实际的需要就是看这一Tier服务器的硬件资源利用率。比如CPU, 内存,磁盘读写等情况,如果相当高,就必须加入新的服务器部署该Tier同样的应用到新服务器上。让新的服务器也能分担些压力。其实这就是要让应用程序能支持高可伸缩性。在每一个Tier之间有硬件负载均衡,再其后就是下一个Tier的服务接口了。在其服务接口之后才是该Tier的服务。
除了高伸缩性之外,还有如何保证高性能。即应用程序必须是良好设计的。在每一个Tier的内部,可以采取一些措施让应用程序的执行效率达到最高。让硬件的资源得到充分的利用。这有一些策略,如缓存。减少访问数据库的次数,等等。以下是一个可伸缩的asp.net应用系统的整体映象图:
一个在互联网上的用户的请求的处理过程是这样的:
1. 首先经硬件负载均衡处理,选定一个Web服务器来响应这个请求,然后将该请求交给该服务器。
2. 此Web服务器执行所请求的页面,该页面的后端代码先查询缓存服务器,即调用缓存服务接口查询是否已经有缓存,如果有,就直接返回缓存的结果。
3. 如果缓存里没有就调用商务逻辑服务接口,进而调用商务逻辑服务。商务逻辑服务执行时,如果需要访问数据库,会先检查缓存中是否有缓存的数据库内容,如果有,就会用缓存的数据库内容来进行商务逻辑的计算。如果没有缓存,就会调用数据访问接口以存取数据。
4. 类似地,数据访问服务也会查看缓存,然后根据所要求的数据内容去访问相应的数据库,如果是只读的请求,数据访问服务可以将数据库访问请求发给做日志复制的数据库服务器。如果是写的请求,可以发给主数据库服务器。
5. 数据库服务器执行应用的Sql请求,返回结果。再由数据服务返回给商务逻辑服务。
6. 商务逻辑服务再返回给Web服务器,由Web服务器生成页面内容返回给互联网上的用户。
以上过程与Layered的架构类似,只是比Layered的架构多经过了几个服务接口。如果没有这些服务接口,因为UI Tier,商务逻辑Tier,数据访问Tier是在不同的服务器上的,它们根本就不能直接对话。因为它们是在不同的.net VM中的。它们必须得借助与这些服务接口才能互相之间进行调用。这些服务接口具体的组成技术可以是WCF,也可以是.net remoting,等。应该说目前最好的选择是WCF。
UI Tier
关于SessionState的技术方案
为了让应用程序具有可伸缩性,必须让每一Tier都有负载均衡的特性,也就是要做到用户的请求由任何一个同一Tier中的服务器来处理都不会有任何问题。关于用户Session的处理就必须有一个妥善的解决方案。有不少人不赞同采用SessionState,觉得SessionState对ASP.NET应用的性能影响比较大。还有人写文章说同一个SessionID的AcquireRequestState会在页面代码前获得对Session对象的锁,因此容易有较大的延迟,对性能影响不小。另外的人认为Session占用服务器的内存比较多,同时需要一些CPU资源来将Session中的对象序列化和反序列化。所以一种比较普遍的观点是不采用ASP.NET本身提供的Session机制。其实采用SessionState和不采用SessionState都各有特点。了解其特点后再做权衡取舍才比较合适。
完全不采用SesstionState
完全不采用SesstionState是在Web.config中写上<sessionState mode=”Off”/> 或者 <Pages enableSessionState=”Off”/>来禁止SessionState。那整个应用的所有页面都不会用SessionState。其实这不全面,http请求处理周期里还有一个系统默认的httpmodule在处理SessionState。还须在Web.config加一句:
<httpModules>
<remove name="Session" />
</httpModules>
应用程序里完全不采用ASP.NET本身提供的SessionState机制,但是应用的需求是要求应用程序有类似于Session的机制的。比如购物车的概念。记住用户选择了哪些商品,在用户点了买单时才处理用户选择了的商品。如果不用ASP.NET本身提供的SessionState机制,就必须自己实现一个Session机制。比如可以在数据库中有一张表来记录自定义的Session数据。如果用户浏览器支持cookie,可以用该cookie存储自定义的Session ID值。这个Session ID值用于到数据库中去查询存储的Session数据。如果用户浏览器不支持cookie,那么就可以在页面中放置隐藏的字段(hidden field)。此隐藏字段用于存储自定义的Session ID。还可以用URL中参数放一个Session参数的办法。这样获得的Session机制是自己管理的Session机制。需要将Session的创建,过时失效,查询Session数据,删除旧Session等都管理起来。
这样的自定义的Session机制将Session数据存储到了数据库。那么就可以不依赖与某一台具体的服务器。从而获得的可伸缩的特性。
采用SessionState
采用SessionState是ASP.NET默认的机制。ASP.NET的SessionState有几种模式。InProc,StateServer,SqlServer模式和自定义模式。InProc不支持负载均衡的场景。只有StateServer和SqlServer模式才支持。自定义模式是指我们自己实现Session数据的持久化,比如将Session数据放到Oracle数据库或者MySql数据库中,自定义模式也可以支持负载均衡。在StateServer和SqlServer模式时,放入Session中的数据都必须是能序列化的。建议采用SqlServer模式的Session机制。配置是这样的:
<system.web>
<sessionState mode=" Off | InProc | StateServer | SQLServer "
cookieless=" true | false "
timeout=" number of minutes "
stateConnectionString=" tcpip=server:port "
sqlConnectionString=" sql connection string "
stateNetworkTimeout=" number of seconds " />
</system.web>
Session采用了SqlServer模式之后,所有数据都会经序列化,并存储到SqlServer数据库中。采用这种模式的Session机制,其Session可以由任何一个UI Tier的服务器来处理,因为Session数据是存储在专门的数据库中的。如果是采用这种模式的Session机制,那么最好有专门的数据库服务器供存储Session数据。通过上述安排,ASP.NET应用就获得了负载均衡,可伸缩的能力。
采用了ASP.NET的SessionState的之后,同一个Session ID下的不同页面请求会有一定的制约。注意这里说的同一个Session ID下的不同页面。这就象数据库的锁机制一样。默认的ASP页面设置都是能对Session对象进行读和写。那么如果同一个Session ID的两个不同请求访问两个不同的页面,就会因为都去锁住Session对象,而造成有一个请求被阻塞较长时间,因为要等另一个请求处理完毕。有同仁可能觉得奇怪,怎么会有同一个Session ID请求两个不同的页面。其实这与页面中的iframe,frameset和AJAX技术有关。包含iframe, frameset的页面已经要存取Session了,iframe或者frameset里面的页面也要存取Session,就有可能造成一先一后,都是同一个Session ID,后面的页面被前面的页面锁住,直到前面的页面都处理完,释放对Session的锁,才能处理后面的页面。AJAX也类似。也存在这个问题。这个默认的机制所带来的延迟在小型的ASP.NET应用中可以不用理睬。但是在大型的ASP.NET应用中是必须解决的问题。要解决这个问题,只能从应用的角度尽力减少需要写Session的范围,即明确确定哪些页面需要读且写Session数据。还需要确定哪些页面是只需要读Session数据。另外还需要确定哪些页面不需要参与读或者写Session数据,即与Session数据无关的页面。通过这样的工作,就确定了Session的范围。对于需要读且写Session的页面,可以显示地在页面中写上< % @Page enableSessionState=”On”% >。对于只需要读Session的页面,可以写上< % @Page enableSessionState=”ReadOnly”% >。对于不需要Session的页面,可以写上< % @Page enableSessionState=”Off”% >。在一个iframe相关的所有页面中,不要所有的页面都去读写Session,这样就可以避免Session争锁所带来的延迟。AJAX所涉及的页面也是如此,尽可能地减少读写Session,发生这种Session争锁的延迟就会少一些。锁越少,整个UI Tier的处理能力就会越大。
关于ViewState的技术方案
ViewState使服务器控件可以在往返行程中重新填充它们的属性值,而程序员不需要编写任何代码。这些属性值包括可见的属性,也包括不可见的。可见的属性如Text属性,不可见的是某些控件的ControlState。ControlState是比较特殊的内容,它总是存储在ViewState字段中。即使用EnableViewState="false"禁止了ViewState,ViewState字段还是有一些内容,这些内容就是ControlState。
曾经听到不少人抱怨说ViewState大,有时光ViewState就几百K。一个页面的HTML,很大的部分是ViewState占用了。微软的文章也在说不需要ViewState的地方就禁止ViewState。所以合理决定应用程序哪些地方需要ViewState。毕竟ViewSate也一定程度上带给程序员一些方便。禁止ViewState是可以在整个应用的级别,页面的级别,和控件的级别来禁止。整个应用的级别禁止ViewState: <pages enableViewState="false" enableViewStateMac="false" enableEventValidation="false"></pages>,页面的级别如:< % @ Page EnableViewState="false" % >,控件的级别如:<asp:datagrid EnableViewState="false" datasource="..." runat="server"/>。禁止了ViewState之后,页面中的__ViewState字段已经大大减小了,但是还是存在。上面已经提到了,__ViewState字段里剩下的内容就是ControlState的。如果想让__ViewState字段没有内容,可以改写Page类的此两方法:
protected override void SavePageStateToPersistenceMedium(object viewState)
{
}
protected override object LoadPageStateFromPersistenceMedium()
{
return null;
}
这样__ViewState字段就完全没有内容了。当然我们可以在此两方法里面设计出自己的持久化ViewState内容的方案。比如将ViewState持久化到缓存中去,或者持久化到SqlServer中去。那么ViewState的内容就不再需要发送的到用户浏览器中了。上面介绍了一些在某些地方禁用ViewState的方法。下面就由开发者和用户来决定哪些页面或者控件需要ViewState,还是完全不要ViewState。ViewState机制具有两面性,一方面方便了程序员,另一方面可能对性能造成影响。所以要小心对待。
减少与服务器的交互次数和不必要的服务器端处理
Page.IsPostBack
Page.IsPostBack可以判断是否有Form提交。第一次访问时的处理和有Form提交的处理是不一样的。这样可以避免不必要的服务器端处理。
AutoPostBack属性
许多服务器端控件都有AutoPostBack,能禁止的都禁止了。
多做客户端的数据验证
用户在浏览器里面的输入,尽量先用客户端JavaScript验证处理,等通过了再提交给服务器。这样减少向服务器提交请求的次数。
AJAX的请求量进行控制
AJAX带来了很炫的效果,但是能适当地减少调用AJAX调用次数,比如能否合并AJAX的调用。
用Server.Transfer不用Response.Redirect
Server.Transfer发生在服务器端,而Response.Redirect发生在用户浏览器中。会多一次HTTP请求。
去除不必要的默认httpModule
如不要SessionState,不要WindowsAuthentication,不要PassportAuthentication等等:
<httpModules>
<remove name="Session" />
<remove name="WindowsAuthentication" />
<remove name="PassportAuthentication" />
<remove name="AnonymousIdentification" />
<remove name="UrlAuthorization" />
<remove name="FileAuthorization" />
</httpModules>
设置processModel
手动设置processModel参数中的MaxWorkerThreads 和 MaxIOThreads 属性,通过观察效果带调整参数。如果机器资源允许,可以稍微多点。
设置Web garden
只要服务器资源允许,就可以建立Web garden,在同一个服务器上多开几个工作者进程。32位Windows上一个进程通常只能占用2G-3G内存(因为高地址的2G或者1G是Windows本身用来装配系统文件用的)。64位Windows上一个进程能占用的内存相对32位大一点,但是服务器有比如100多G的内存,可以适当多开几个工作者进程。这可以增加单台服务器的处理能力。要设置Web garden可以先在IIS管理器里面找到对应的应用程序池,在查看该应用程序池的高级属性,再找到最大工作者进程参数,见图。
缓存
ASP.NET中可用的缓存主要有:页面级的缓存,控件级,System.Web.Caching.Cache,以及分布式缓存如Velocity和memcahced。页面级的缓存可以在ASPX页面用< % @ OutputCache Duration="10" VaryByParam="none" % >,在用户控件中可以用< % @ OutputCache Duration="10" VaryByParam="none" VaryByControl=””% >,与页面级的cache相比,多了VaryByControl参数。必须得指出这些页面级的和控件级的缓存是存储在特定的Web服务器上的。除非在负载均衡的硬件上做特殊的设置,否则这些页面级和控件级的缓存都意义不大。因为这些页面级的和控件级的缓存是存储在特定的Web服务器上的,第一次用户的请求是由此服务器处理的,然后有了页面缓存,如果负载均衡硬件将第一次以后的请求交由其他服务器处理,那么这个处理第一次请求所做的页面和控件级缓存都失去了意义。只有进行了特殊设置后,负载均衡的硬件才能知道刚才这个请求是哪个服务器处理的,就继续向该服务器转发HTTP请求。那么保存的页面等缓存才会起到相应的作用。System.Web.Caching.Cache是个很好的缓存机制,可以给程序员利用来缓存一些内容。可惜它不是分布式的。它的存储限定在特定的服务器上。所以它对负载均衡是不支持的。要支持负载均衡,需要使用分布式的缓存如Velocity或memcached,在UI Tier缓存的内容可以是数据库查询结果。如果是自己管理的Session机制,可以将分布式缓存作为Session的存储,所有Session中的对象,可以存储在分布式缓存中。还有ViewState,如果希望客户浏览器不下载ViewState但是又要用ViewState,可以重载Page类的SavePageStateToPersistenceMedium和LoadPageStateFromPersistenceMedium方法,并在此方法中将ViewState存储到分布式缓存。
考虑预编译
将所有ASP.NET页面都预先编译。可以减少第一次访问时由于ASP.NET编译页面所带来的延迟。
在生产环境禁用调试模式
生产环境使用Release模式的编译,会使程序运行稍微快一点。
尽量避免异常
异常是非正常的程序控制流。发生异常多对性能的影响比较大。所以在程序中多对可能的情况进行检测,比如判断某对象是否为空。此同样适用于其他Tier。
尽量避免锁住资源
在多线程的场景下,尽可能地去避免锁住资源。尽量各线程都用私有的资源。此同样适用于其他Tier。
压缩页面和相关文件
比如可以打开IIS的gzip,还有用一个自制的HTTP module压缩页面的HTML, .js文件。去掉不显示的回车和空格。进行尽可能多的压缩。
商务逻辑Tier
商务逻辑服务接口
前面已经提到,服务接口可以考虑用WCF, Remoting等技术。目前最好的是采用WCF。原因是WCF支持事务,支持多种通信方式。商务逻辑服务有时是必须在互联网上公开。所以WCF可以选用基于Web service的通信方式,这样支持的外部系统比较多。如果商务逻辑服务只是在内部使用,可以选用TCP/IP socket的通信方式。这个商务逻辑服务接口其实就是后面的商务逻辑服务的包装。商务逻辑服务提供哪些方法,就用相应的接口进行对应。
商务逻辑
事务的控制
商务逻辑这里应该对事务进行控制。这与WCF接口支持事务想匹配的。
预取与缓存
比如翻页,可以在用户取第一页时,取出5页,缓存起来,用户往后翻几页时就可以不再查询数据库。减少对数据库的查询次数。有些查询特别多的数据,直接都在分布式缓存里面存着。只有缓存里没有的时候,才去查询数据库。
对数据库的访问也是可以分布式的调用
大家看到了上面的图,对数据库的访问也是需要通过分布式的调用才能完成。数据库查询的结果通过自定义的对象集合来传递。
采用自定义的对象作为商务逻辑的处理对象
这些自定义的对象其实就是一个数据库中数据的在内存中的反映。商务逻辑的处理对象最好用自定义的对象。不要用DataSet。
商务逻辑Tier最好是无状态的
该Tier最好是状态无关的。与商务有关的数据都存储到分布式缓存里面。服务器内存里面不长时间存储商务有关的数据。这样,一个对商务逻辑的请求就可以由任何一台商务逻辑Tier的服务器来处理,这样就做到了负载均衡。
长时间计算型的任务最好交给其他系统来在后台处理
有些计算密集的任务,最好交给其他系统在后台运行。与计算密集的系统交互就只通过数据文件进行交互。
数据访问Tier
数据访问服务接口
类似于商务逻辑服务接口,数据访问服务接口可以考虑用WCF, Remoting等技术。目前最好的是采用WCF。原因是WCF支持事务,支持多种通信方式。可以选用基于Web service的通信方式,也可以选用TCP/IP socket的通信方式。这个数据访问服务接口其实就是后面的数据访问服务的包装。
数据访问
对事务的支持
如前所述,商务逻辑控制着事务,数据访问Tier只是作为商务逻辑控制的事务的一部分。数据访问Tier中有许多数据库的操作,如,查询,更新等。建议所有的数据库操作都用存储过程来实现。这些数据库操作都作为商务逻辑控制的事务的一部分。不要在存储过程中实现商务逻辑。这些数据库操作都只是替商务逻辑服务完成数据库查询或者存储数据到数据库的任务。所以不要在存储过程或者数据访问Tier实现任何商务逻辑的内容。
数据库读写分离的支持
如前图所示,数据库有只读模式的。可以将部分读的请求分流到只读模式的数据库服务器上。只有写的请求才流到主数据库服务器上。这就要求分别支持不同的连接。
连接池的管理
每台数据库服务器所允许的连接数是一定的。需要管理好个数据访问服务的数据库连接。管理好每台数据访问服务服务器连接池。
在读的时候用SqlDataReader
读取数据的时候,可用SqlDataReader来读取快速只进的数据流。
缓存
将数据库访问获得的内容缓存到分布式缓存服务器上。
数据库的设计和安排
读写分离
主数据库服务器是集群的数据库服务器。SqlServer 2008 R2 / Windows Server 2008 最多支持16台服务器的集群。可以架设一些只读模式数据库服务器,采用日志复制方式,将主数据库的所有日志复制到只读模式的数据库服务器上。那么只读模式数据库服务器内容就可以保持和主数据库服务器一致。这些只读数据库服务器就可以用于分担读的压力。
库表的分离
从应用的角度将某一些数据分到多个数据库来存储。比如Myspace有7000多万用户,它把每一百万用户存放于一个数据库。这样每个数据库都小了很多。查询起来相对快一些,但是程序就会设计得复杂一点。分开的数据库可以放在不同的服务器,也可在同一服务器。请根据实际情况来决定。
表的设计
3NF, BCNF是肯定要达到的。这不多说了。主要想说说聚集索引。表的聚集索引是很关键的一个索引。需要从应用角度考虑,最多的查询是什么样的,然后按照使用最频繁的查询来设计聚集索引。一般来说聚集索引需要选用短的,基本数据类型的字段。比如整数, 固定长度的文本,日期之类的字段作为聚集索引的字段。而且具有单向递增的特性,比如日期,自增的字段。良好的聚集索引的设计,对最频繁的查询的性能改进是很有帮助的,同时对插入,更新都有较大的帮助。插入时是在物理的表记录末尾加入新记录,引起的磁盘IO较小;更新时也可按照索引来很快查找到记录并更新。同时也得考虑删除时的效率。如果可能的话尽量不要删除记录,只将需要删除的记录置成删除状态。
除了聚集索引,还有普通索引,合适的普通索引对查询的性能也是有帮助的。还是分析应用可能的查询,可以将次优先的那些查询分析一下,这些查询主要用到哪些字段作为搜索条件。然后可以适当地建立普通索引。这些聚集索引和普通索引对查询的性能是有帮助的。
创建表分区
将表的记录按一定规则来分到不同的数据文件上存储。可以分区的字段也是基本的类型。比如日期,文本等。创建分区的表的IO可以由多个线程同时来读写不同的数据文件。在IO上可以有所改进。
合理使用视图
创建一定数量的试图,可以对查询性能起到帮助。
分布式调用越少越好?
前面一篇文章<<关于大型asp.net应用系统的架构-架构的选择>>有同仁提出分布式调用越少越好的观点。这里可以说一下。如果只有一台服务器的时候,单纯比较用分布式调用和非分布式调用,分布式调用肯定比非分布式调用慢,因为分布式调用要多一些中间接口的处理。但是非分布式调用能同时支持那么多人同时访问吗?非分布式调用能将用户的请求交由任何一个服务器来处理而不出现问题吗?万一一台服务器出现了问题,那么这台服务器上的用户就丢失他/她的会话和数据吗?大家看吧。
当然也有这种可能,就是整个系统中某些地方采用分布式调用,另外一些地方采用非分布式调用。例如:商务逻辑服务和数据访问服务之间就不用分布式调用了。那么整个系统的图就成了这样:
这样做不是不可以,就是有其优缺点,优点是商务逻辑调用数据访问可以比全部分布式的更快,缺点就有可能是,商务逻辑服务器多到一定程度,就会发现,数据库连接却不能再往上增加了,而要统一调度数据库连接也是很困难的。商务逻辑与数据访问的耦合度是否有点高?
结束语
对于大型的ASP.NET来说,首先要保证负载均衡和可伸缩性,再来做到每一台服务器的性能最大化。要使整个系统的服务能力最大化,需要使用软件硬件的所有手段。这里谈到的只是一些方面,不够全面。
posted on 2010-11-30 17:33 mikelij 阅读(16567) 评论(80) 编辑 收藏 举报