Monthly Archives: March 2012

听了一场跟Flash存储有关的演讲

没有全部听懂,但有段关于latency的内容记住了:

  1. 内存的latency: 纳秒级

  2. 普通硬盘:    毫秒级

  3. Flash存储:      微秒级

也就是说,Flash存储的性能虽比不上内存,但比普通硬盘强太多了; 有的I/O密集型应用如数据库,如果对某些数据使用Flash存储,对性能的提升会非常明显。 

jmockit中的诡异陷阱

以前用过一些Mock框架;使用jmockit时,我以为mock框架都差不多,jmockit也不例外。吃了一些苦头才发现,jmockit是多么的与众不同(中性词).

在我看来,jmockit作者的思维方式是扭曲的,jmockit的用法也是诡异的。要想充分利用jmockit的强大功能,你必须容忍它与众不同的地方。本文给出一些最近收集到的诡异点:

Mock行为中的诡异点

看起来只Mock了一个对象,实际上Mock了整个类

 @Mocked //看起来只Mock了bean这个对象
 private SimpleBean bean;

 @Test
 public void test_MockedInstance() throws Exception {
  bean.throwException(); // 不会报异常,因为bean已经被mock了. 这个好理解.
 }

 @Test
 public void test_AnotherInstance() throws Exception {
  SimpleBean bean2 = new SimpleBean();
  bean2.throwException(); // 也不会报异常,因为SimpleBean的任何一个实例都被Mock了。想不到吧?
 }

看起来只Mock了本类,实际上这个类的父类及所有祖先类都被Mock了

 @Mocked
 private SubSimpleBean bean; //看起来只Mock了SubSimpleBean类

 @Test
 public void test_AnotherInstance() throws Exception {
  SimpleBean bean2 = new SimpleBean(); //SubSimpleBean的父类
  bean2.throwException(); // 不会报异常,因为SubSimpleBean的父类也被Mock了 (所有祖先类都被Mock, 但java.lang.Object除外)  
 }

要Mock一个类,可以把这个类的实例作为测试方法的形式参数,也算是有创意了

 @Test
 public void putTest(final Cache mockedCache) { //被mock对象作为方法的参数

  new Expectations() {
   {
    mockedCache.put(bean); 
   }
  };
 
 ....

 }

Expectation中的诡异点

Expectation中不但做了mocking,也隐式地做了verifying

  
new Expectations() {
   {
    /**
     * 下面的代码做了两件事:
     * 1. mock dao的行为: dao.getNumOfBeans()如果被调用,则返回值是100<br/>
     * 2. 同时,期望dao.getNumOfBeans()在测试时被调用,如果没调用,意味测试未通过
     */
    dao.getNumOfBeans(); 
    result = 100; 
    
   }
  };

Expectioan里的隐式verification只跟被Mock的方法有关

 
@Test
 public void test_Not_Mocked_Object(){
  Service service = new Service();
 ...  
  new Expectations(){{
   nonMockedClass.someMethod();   //不会出错。虽然service.saveAndCache(bean)时不会调用NonMockedClass的方法,但由于NonMockedClass不是被mock类,jmockit不会验证跟它有关的语句  
  }}; 
 
  service.saveAndCache(bean);
 }

Expectations录制了方法未执行,会报错;未录制但执行了,却不会报错

 
@Test
 public void test_Not_Invoked_Method(){
  Service service = new Service();
  service.setCache(cache);
  service.setDao(dao);
  final SimpleBean bean = new SimpleBean();
  
  new Expectations(){{
   mockedList.someMethod();  //会出错,因为service.saveAndCache(bean)时不需要调任何MockedList的方法  
  }}; 
 
  service.saveAndCache(bean);
 }
 

 @Test
 public void test_NoCoverage(){
  Service service = new Service();
  service.setCache(cache);
  service.setDao(dao);
  
  new Expectations(){{   
   //测试可以通过,虽然这里没有录制任何被mock类的方法   
  }}; 
  service.saveAndCache(new SimpleBean());
 }
 
 @Test
 public void test_CoverOnlyOne(){
  Service service = new Service();
  service.setCache(cache);
  service.setDao(dao);
  final SimpleBean bean = new SimpleBean();
  
  new Expectations(){{
   //测试可以通过,虽然这里只录制了部分被mock类的方法
   dao.save(bean); 
   
  }}; 
 
  service.saveAndCache(bean);
 }

根据Module的特性决定单元测试的覆盖率要求

业务应用的单元测试覆盖率可以低一些,框架和工具箱的覆盖率必须要很高

 

单元测试的覆盖率高当然很好,但高的覆盖率意味着要写很多测试;而写测试本身是件费脑费时的事情,代价不低。 所以要在收益和代价之间取得平衡。

 

可以着眼现在,放眼未来,具体考量四个因素:

    1. 对代码正确性的要求。 要求越高,覆盖率就应该越高。

    2. 能否人肉测试。 如果用人肉可以做测试,则自动化运行的单元测试代码可以少一些

    3. 代码逻辑的易变性。    代码逻辑越易变,写的测试被浪费的概率越大,对低覆盖率的容忍度越大

    4. 发现bug时的易改性。 出现bug的代码越容易改,对低质量的容忍度越大,对低覆盖率的容忍度也越大

   

 

以互联网为例,在业务应用层面,可以为了迅速推出新产品而容忍一定的bug,可以用鼠标进行人肉测试,产品易变代码逻辑也易变,发现bug时也容易改,所以这一块的测试覆盖率可以低一些

 

而框架、工具箱等Module的代码正确性必须非常高,否则会影响很多上层模块和系统;没有界面因此不能人肉测试; 代码逻辑不怎么变,单元测试被浪费的概率不高;一旦出现bug,受牵连的上层模块和系统可能会非常多,因此不能轻易让bug出现。 所以,框架、工具箱等Module的测试覆盖率必须非常高。

《构建高性能Web站点》笔记:11 性能监控

性能监控

 

1.要监控什么

  a. linux服务器的性能指标:如cpu,内存,I/O等,可以使用nmon, snmp等工具或手段

  b. 各种服务的性能:
    i.apache: mod_status模块 + 模块提供的http请求接口
    ii.mysql: Cacti, mysqlreport等
    iii.Memcached: Nagios等

  c. web页面的响应时间
  通过工具模拟浏览器从外网访问你的一些页面,如www.jiankongbao.com就提供了这样的服务

 

 

2.在哪里查看监控结果?

 

 

  a.服务器上直接查看
    可以用Nmon在本地实时监控,实时刷新各种指标,包括内核状态、NFS等;也可以定期采集数据,并通过Nmon Analyser生成报表

 

 

  b.远程监控:

    i.服务器上运行监控代理,在其他机器上另建监控中心与代理交互

   ii.可以让代理主动采集数据,定期上报给监控中心

  iii.也可以让代理被动响应数监控中心的请求

   iv.代理采集数据的方式有:
      1. 调用shell命令,获得输出,基于正则表达式进行解析
      2. 可以把SNMP服务器直接用作监控代理
      3. 有些服务端软件自己提供了监控接口,如Nginx就提供了一个http请求接口

    v.监控中心可以用开源产品Cacti来搭建,它的绘图能力很强

《构建高性能Web站点》笔记:10 异步计算与并行计算

要解决的问题:并发能力没问题,但处理单个请求比较耗时,比如,获得一个统计值需要查多次数据库导致很长的响应时间

 

解决办法
      1.同步改异步,给用户的反馈也改成异步
         前端程序把任务丢到后台消息队列后,立即告诉用户:“正在为你统计,请稍候刷新页面”
         消息队列的消费者,即worker,在算出统计值后,再把结果丢进数据库
         用户刷新页面时,前端程序去数据库查询到结果,再反馈给用户

        这就叫异步计算

 

      2.串行变并行,减少总体响应时间,用户可以同步等待反馈
         把任务拆分成多个小任务,后台多个worker各领一个小任务并同时处理,响应时间立刻缩短
         并行处理完后,再将结果合并起来,返回给用户
         如果总体时间够短,对用户的反馈机制就没必要做成异步;用户可同步等等反馈

         这叫做并行计算

 

具体技术
      1. 同步改异步:用消息队列,如MemcacheQ
      2. 并行计算:采用Map/Reduce
         a. Map代表拆分,Reduce代表汇总
         b. 兼做监控、容错、可用性、负载均衡等事情
         c. Map/Reduce框架本身不是开放的(Google的内部产品),但思想是开放的;你可以用Hadoop来做Map/Reduce, 也可以自己编码实现

《构建高性能Web站点》笔记:9 数据库的扩展

数据库的扩展问题可以拆为两个子问题
  1. 高并发时,单台数据库服务器会不堪重负
      2. 大数据量时,针对一个大数据集进行数据读写,响应时间会比较长

 

前端和后端


出于分而治之的理念,可以想到一种理想的方案:把DBMS拆分为数据服务系统和数据存储系统,前者作为用户请求的入口,后者则存储数据,两者的关系类似于web服务器和文件服务器;分离之后,再分别采用针对性的扩展方案,比如说数据服务系统可以像web服务器一样简单地集群,数据存储系统则可以采用类似于分布式文件系统的方案,然后再让“前端”调用“后端”,整体上像一个分布式系统

 

遗憾的是,传统的DBMS内部似乎并不支持这种分布(TBD)。一个DBMS进程中,一般都既要响应请求,又要存取数据;这种感觉就像,文件只能存放在web服务器里,如果要复制web服务器,就一定要考虑文件怎么处理

接受“拆不开”这个事实后,再来看目前流行的各种方案。

首先,可以看看如何提高并发能力

1.搞N个一模一样的数据库服务器,每个服务器分摊一部分用户请求

  每台服务器都提供服务,每台服务器上都包含全量数据,这是最直接的方案

      问题在于,实时同步各台服务器上的数据本身比较耗费性能:一台服务器上来了新数据,就立即广播出去,然后大家都从它那里复制数据;还没复制完,又有一台服务器广播了一点更新消息。。。如果大家总是忙于跟它人搞双向复制,就没有余力处理用户的请求了

2.服务器之间单向复制数据,并使用单个数据更新源
   单向复制比双向复制快的多。一台服务器更新数据后顺便更新一下自己的日志(主服务器),其余服务器只根据这个日志更新自己的数据(从服务器),不再接收用户的数据更新请求。那“从服务器”对高并发又起什么作用呢?他们可以为用户的数据查询请求提供服务。 这就是读写分库,把读和写分摊掉了。

    这种方案的问题在于,如果写请求很多,主服务器忙不过来,加再多从服务器也没有用。

3.分摊写请求

    主服务器本身可以根据数据更新的目的地的不同,而分拆成不同的结点。 如果一个服务器中有两个库,可以把其中一个库挪到另一台服务器中去(前提是A库和B库基本不join),这样一来,对不同库的数据更新请求就落到了不同的服务器上(当然,读的请求也分摊了)。你也可以把库中不同的表放到不同的服务器上。

这种分法叫垂直分区

但垂直分区仍有个极限:如果某张表只有一个字段,而且对这张表的请求非常多,你怎么办?

4.再次分摊写请求

    可以把1万行的表,拆成10张1000行的子表,放到十台服务器中,这样可以再次分摊请求; 而且理论上说,这种分摊没有极限,数据每增多一点,服务器就多加一台。
这种分法叫做水平分区。其实水平分区不仅可以分摊请求,同时也可以对付大数据集的问题。夸张地说,以前表里有1万行数据,按索引查数据都很慢;现在表里只有1000行数据,全表扫描都无所谓了

大数据量的问题
    上面提到的水平分区,已经可以用来对付大数据量的问题。这里就不再提了

最后罗列一下上述各种方案中可以运用的技术或工具  
1. 实时、双向同步数据: mysql clustering(TBD)

2. 读写分离: mysql主从复制可以实现读写分离,mysql proxy则可以前置于主从服务器前面,自动决定读、写的归属,为应用程序提供透明的服务

3. 水平分区: 

   如果要根据user.user_id对user及其相关表分区,对于一个user_id,应该放到哪个区呢?
   a. 取模:  5,10,15…放A区, 6, 11, 16…放B区 …
   b. 按区间: [1-1000]放A, [1001-2000]放B ….
   c. 另建一张全表,记录每个user_id所处的区
     

 
   

 

《构建高性能Web站点》笔记:8 共享式/分布式文件系统

共享文件系统

  
主流实现:NFS

         a.直接基于RPC协议,client/server通信模型

b.client再把服务端的ip映射为本地磁盘

c.RPC默认使用UDP,在应用层即RPC协议里做差错重传等操作

d.服务端采用多进程模型

e.NFS的性能天花板非常低,在高并发环境下(比如前置10台web服务器,每个web服务器100个并发数)非常容易成为瓶颈

f.唯一的好处就是简单

g.支持异步写。server接到异步写的请求后会立即返回"OK",然后再在后台把文件写入磁盘

文件系统中影响性能的因素:

    1.磁盘吞吐率

    2.服务器的并发处理能力

    3.网络带宽

==========================

共享文件系统很容易成为瓶颈;
替代方案是要让web服务器上也有文件,同时又保证服务器中各个节点上都有相同文件的副本。那怎么创建这些副本呢?  

1. 可以主动地将文件传给别的服务器

  a. 可以通过高级语言里的SSH Client模块 (具体的SSH应用是SCP和SFTP),将文件复制出去

  b. 也可以走Http WebDAV协议(subversion走的就是WebDAV)

2. 也可以被动地轮循别的服务器,下载这段时间内更新过的文件; linux下的rsync就是这样一个工具。

3.另外,文件的复制可以用多级的方式实现。如中心服务器把文件复制到各省服务器,各省再复制到各市等等

==========================

分布式文件系统

   这里的文件的概念不是操作系统中的文件的概念,目录也不是操作系统里的目录(MogileFS甚至没有目录的概念);分布式文件系统自己定义了一套文件的概念,它运行在用户态。

分布式文件系统的作用:

   1.在使用者眼里,一个分布式文件系统是一个单一的整体

   2.但实际上,一个分布式文件系统确可以跨越多台服务器,并在服务器之间进行自动的文件复制

原理:

   1.文件存储在Storage Node中

   2.Storage Node之间可以根据规则自动复制文件

   3.Tracker放在Nodes前端,作为客户端的入口

     a.负载均衡:工作机制类似于dns负载均衡

   b.故障转移

     c.文件寻址

一个应用为什么要拆成多个jar包,并且包要有版本?

都说应用大了就要拆,有人建议拆成多个应用并构成分布式系统,有人建议维持一个应用但拆成多个包(指jar包,不是package);本文讨论的是后一种分拆法。 

 

下面从多个方面来讲述分包的好处。我将主要使用“反证法”,论述大应用如果不分包可能导致的后果,反过来证明分包的合法性。 本文的观点和论据大部分来自于Robert C. Martin的《敏捷软件开发》,有兴趣的朋友可以细阅这本书的第20章-“包的设计原则”

 

 

从软件开发的多个方面看待分包问题:

 

 

系统的可理解性

 

   分包可以提供模块化的视图,帮助了解系统的组织结构

   如果一个大公司没有分部门,你是不是很难说出这家公司哪些人有哪些职能? 如果一个系统的java代码都堆在一起,你是不是也很难说清这个系统大概有哪几块? 如果系统的结构不清晰,对大家理解系统、系统架构的未来规划都有很大的坏处。

   而系统分包就是系统模块化的过程;结合包依赖图,可以帮助了解系统的组成部分以及模块之间的依赖关系

 

编译

 

   a. 分包可以避免一次编译所有代码,减少编译时间

 

   如果代码不分包,则意味着每次编译都要编译整个应用;对于一个大的应用,这个编译时间会很长;如果应用是c/c++写的,这个编译时间可能会达到无可容忍的地步。

   分包之后,大部分代码已经编译掉、作为jar文件引入了,此时你要编译的代码只是你正在改的那一小部分。这种情况下,编译时间可大大缩短。

 

   b. 分包后,服务提供者的接口变更不会立即导致消费者代码编译失败

 

   如果代码不分包,且大家都在同一份代码库里开发,会有这样一种场景:A写完FooService准备提交时,发现它依赖的BarService已经被删掉了! 这时A就没办法提交代码了。如果A是一个冒进的人,他在提交之前不会更新代码库,而是直接提交FooService,然后下班;比他下班晚的人如果签出最新代码,就会发现代码无法编译。

 

   如果大家各搞一个私有分支,可以避免这种问题;大家在各自的分支里进行开发,最后再合到一起。但最后合并的工作量可能很多,因为要解决的代码冲突可能会很多或者很难搞,上面说的BarService被删掉的问题就很难搞;应用越大,这种冲突的概率就越高。

 

   把应用拆成带版本的jar包可以解决这个问题:把BarService放到bar-1.0.jar里,FooService依赖bar-1.0.jar. 如果BarService被删掉,删它的人会打出一个bar-1.1.jar; FooService则仍然使用bar-1.0.jar,不会出现编译问题。

 

3.代码的正确性

 

   分包后,服务消费者不必使用服务提供者的最新版本,避免出现未经验证的代码组合

 

   如果大家都在同一份代码库里开发,A写完FooService自测并提交,然后B把它依赖的BarService改掉,FooService和新的BarService共同运行时可能就会导致错误的结果,因为对于这个Foo+Bar组合,还没人测过。

 

   把应用拆成带版本的jar包可以解决这个问题:把BarService放到bar-1.0.jar里,FooService依赖bar-1.0.jar. 如果BarService被修改,修改它的人会打出一个bar-1.1.jar; FooService依赖的仍然是bar-1.0.jar,不会出错。

 

4.代码的安全性 

 

  分包后,每个包独占一个代码库,方便用作权限控制的单位

  有些代码只准某些人修改,这种控制一般要依托版本控制系统(VCS)实现。 如果整个应用的代码都放一起,想对某部分代码做权限控制,意味着要对代码库里的某些子目录进行权限控制,VCS在这方面的支持往往不好,或者配置起来不那么简单。把应用拆成多个jar包后,每个jar包的代码库对VCS来说都是一个独立的代码库,这时再做权限控制会容易的多。

 

5.架构

 

  分包体现了防卫性设计,可以保证层次之间及模块之间的单向依赖

 

  如果整个应用的代码都放在一起,难保不会有人用DAO调Service, Service调Servlet; 也难保不会有人让一个基础应用调用一个易变应用,比如UserService里调用GoVocationBiz

 

  把代码分成包后,通过maven/ivy等包际依赖管理工具,可以明确定义好包之间的依赖关系,避免上述事情发生; 如果开发者试图让user-service.jar依赖了vocation-biz.jar,他会在review时被阻止,因为这种级别的代码变动往往需要团队里的资深成员介入review

 

6.项目进度

 

  分包后,服务提供者的升级项目和消费者的升级项目可以错开进行

  整个应用的代码放在一起时,如果BarService的接口改了,依赖它的FooService也得跟着改;当BarService发布时,FooService也得跟着发布,即使FooService团队正在忙于其它的项目,也不得不抽出时间内重构FooService并盯着它和BarService一起发布; 如若不然,则BarService只好搁置自己的升级计划。

 

  有的团队会把BarService打成包(出于防卫性设计考虑),却不使用版本机制;在这种情况下,FooService总是使用BarService的唯一版本,也就是BarService的最新版本,此时,进度捆绑问题仍会发生。

 

  如果把代码分包并引入版本机制,当BarService升级后,FooService仍可以使用老的BarService版本继续运行;等FooService团队有空了,再另开项目使FooService适应BarService的新版本。 BarService的升级项目和FooService的升级可以分开做