Rails系统性能优化之路

这篇文章讲述的是我们在一个Rails on Jruby系统的性能优化之路上披荆斩棘的故事。

优化之前

在开始性能优化之前,有几点必须明确:

1. 性能优化的对象:并不是所有页面都需要优化,而且首先应该选择那些访问率最高、性能瓶颈最大的页面来进行优化。

2. 性能优化的目标:性能优化必须有一个具体的目标,即要达到的响应时间和吞吐量。有了目标,我们就知道目前离目标的距离,需要优化的力度;同时,也知道在何时停止优化。

3. 伴随优化的测量:没有测量的性能优化很可能让你缘木求鱼。

优化之路

下面,分别从多个方面看一下性能优化中的一些实践。

缓存

缓存是性能优化的一大利器。Rails本身对缓存机制有很好的支持。

客户端缓存

一般而言,对于客户端缓存的原则是:对动态html页面不作任何缓存,永久缓存任何其它类型的文件。

Rails很好地支持了这个原则。比如:

stylesheet_link_tag(”application”)

生成的页面元素是:

<link href=”/stylesheets/application.css?1232285206″ media=”screen” rel=”stylesheet” type=”text/css”/>

大家都注意到在css文件后面有个时间戳,这其实是一个小计策:当文件发生改变的时候,时间戳也发生了改变;而客户端会认为这是一个新链接,就会重新获取文件。有了这种机制的支持,我们可以很放心地在客户端对静态文件进行永久缓存,而不用担心过期问题。

服务端缓存

Rails提供了三种服务器端缓存方式:page cache,action cache和fragment cache。对于动态页面,fragment缓存使用的机会会比较多。

让我们看一个例子:一个产品页面片段,片段上面的部分信息对于所有用户都是一致的,但另一部分对于不同权限用户是不一致的。在admin登录时,能看到a链接,b按钮和c复选框;在一般用户登录时,能看到b按钮和c复选框;未登录用户只能看到c复选框。

一种很简便的缓存策略是根据用户标志进行缓存,即对用户p1缓存,对p2也进行缓存,那么我们就可以根据用户的标识以及产品的标志来定位缓存。

但是,这种缓存策略存在一个问题,即缓存数量太大:同一个产品片段存在n份不同的拷贝,n = admin用户的数量 + 一般用户的数量 + 1 (未登录用户)。而且,对于同一种权限的用户来说,他们对同一个产品的缓存是一样的。

消除这种重复的策略是根据用户的类型来对缓存进行分类,那么对于一个产品而言,它只会有3份并且不重复的缓存,3 = 1 (admin权限) + 1 (一般权限) + 1 (未登录用户)。

这 可能是一个比较简单的例子,比较容易想到。但在一些很复杂的情况下,可能就会迷惑。其实原则就是:消除重复,根据片段的根本特征差别来对缓存进行分类。但 这里引起了另外一个问题:如果根本特征的区别需要对很多数据进行大量的计算,那么缓存就失去了它的意义。所以,要把握好权衡。缓存,最重要的是为了减少数 据的重复获取,减少重复计算。

另外,缓存的清理策略是至关重要的:何时清除缓存,以及如何定位失效缓存。对于要求实时性的页面,可以使用 rails提供的sweeper机制来进行缓存清理。但sweeper有个不方便的地方是需要对相关的action都逐一进行声明。我们的系统巧妙地利用 了rails的observer:在observer检测到数据的更新时对相关缓存进行清理。

计算结果缓存

在一个request的生命周期之内,有些数据不会改变,或者我们不关心改变,则可以通过对结果缓存以避免重复计算。

def length
@length ||= end – start
end

内存

来看个例子:我们要在一个页面中显示一百个产品的信息,产品信息从一个信息搜索平台获取。下面的代码是从搜索结果数据集创建产品对象:

records.map { |record| Product.new record }

但这里有个问题,Product是一个ActiveRecord类,但页面显示结果并不需要这么“重量级”的对象。取而代之以轻量级的对象会减少内存的消耗,并提升速度。

数据库

性能的瓶颈往往不在语言级别,而在IO上。对数据库的优化是重中之重。

在数据库的优化上有一些耳熟能详的方法,比如:

  • 增加适当的索引以加快查询。有些很好的工具可以帮助我们找到性能的瓶颈,比如分析execution plan。
  • 缩小transaction的粒度以减少锁的见时间。
  • 避免多次创建transaction的开销(如下代码)
Product.transaction do
search_results.each do |search_result|
Product.create(
search_result)
end
end


大型查询

对于像报表类的大型查询,要绕过ActiveRecord,而直接使用数据库驱动。同时,对于多表连接的查询,可以考虑几种方法来优化:

  • 引入中间表,让另外一个进程定时把查询结果插入中间表。但这会牺牲一定的实时性。
  • 避免重复结果的获取,减少结果集。这往往出现在一对多表之间,连接会导致“一”这边出现多条重复结果。可以考虑把一个查询分拆成多个查询。
  • 使用存储过程或者functions。当然,这失去的是业务逻辑的透明性和灵活性。

Rails的finder

接下来,探讨一下如何正确使用Rails的finder。Rails的finder是很容易误用或者不合理使用的地方,往往有很多性能问题因此而起。

在使用finder时,要搞清楚几个问题:

  • 是否需要结果集中的所有数据?
  • 是否需要预先加载?

表的扫描是邪恶的,很吃性能。一定要减少获取的结果集,用:select指定需要的字段。配合以创建正确的数据库索引,并在索引上挂载其它需要的字段,避免表的扫描。

正确使用预先加载可以避免n+1查询:

Company.all(:include => :products:conditions => “company.kind = ‘toy’”)

产生的sql查询是:

SELECTFROM companies WHERE kind = ‘toy’

SELECTFROM products WHERE products.company_id IN (12, 423, 431…)

但错误使用预先加载是个很危险的事情,它可能不会影响结果的正确性,但会引起很严重的性能问题:

Company.all(:include => :products:conditions => “products.id IS NOT NULL AND products.weight > 10″)

其实写这个查询的人的目的是为了找出拥有products,并且products的weight大于10的company。但这个语句导致的sql查询是性能低下的:

SELECT companies.id AS t0_r0, …., products.id as t1_r0, … FROM companies
LEFT
OUTER JOIN products ON products.company_id = companies.id
WHERE products.id IS NOT NULL AND products.weight > 10

这个sql查询有两个问题:

  • 结果集中的products信息是不需要的
  • LEFT OUTER JOIN的性能劣于INNER JOIN

我们可以使用如下的语句来避免这两个问题:

Company.all(:joins=> “INNER JOIN products ON products.company_id = companies.id, :conditions => products.weight > 10)

它生成的sql是:

SELECT companies.* FROM companies
INNER
JOIN products ON products.company_id = companies.id
WHERE products.weight > 10

这个查询的效率会高很多。所以,正确使用Rails的finder是至关重要的。

页面

View的helper方法生成html元素,比如:

link_to “My Company”, company_path(@company)

# => <a href=”/companies/1″>My Company</a>

过度使用helper方法会引起性能问题。比如上面这个方法,每次都会调用link_to和company_path — 从routes中解析path。

但注意,只有在过度使用的时候,才需要考虑是否可以减少helper方法的使用来提升性能。同时,也可以用一些工具替代ERB来提升性能,比如erubis:它通过预处理避免每次调用的重复开销。

多线程

版本2.2之后,Rails终于是线程安全的了,意味着我们可以开启多线程模式,这绝对是一个巨大的进步。

非线程安全的Rails,无法有效利用共享资源。在以前的版本中,假如我们的应用部署在mongrel上,那么对于n个并发请求,就需要n份rails,n份application,n个数据库链接等等。几乎所有的东西都无法共享。

多 线程模式,对于Rails on JRuby带来的改变尤其巨大,因为jruby的线程是native thread(相对于ruby的green thread)。比如我们的系统只开启了一个应用实例来处理所有的请求(当然,当瓶颈出现在一些共享资源上时,可以考虑增加实例)。

还是用数字来说话吧:Rails on JRuby的内存使用是原先非线程安全时的1/n (n是并发请求的数量),是Rails on Ruby的线程安全模式的1/m(m是cpu的数量)。

那么,开启多线程模式要注意什么?如果你的应用中有类变量的使用,请注意它们是否会在并发下出现问题。

服务器

服务器的配置优化对性能的影响很大。我们的系统以war包的形式部署在tomcat上,主要的配置优化在于对JVM的内存分配上。可以从这几个方面看:

xms — 初始内存大小是否合适?如果你的系统在一开始的时候就需要较大的内存分配,就可以设置一个合理的xms值。
xmx — 最大内存大小是否合适?如果太小,会导致OutOfMomery,会导致持续的GC;反之,则会导致一次GC时间过长,在这段时间内,系统的性能将会受到影响(当然,这跟GC的算法有关)。

性能优化的几个原则

更近

数据库,应用服务器,web服务器以及客户端,这是信息传输的一条链(当然,有些系统的链会更长,更复杂)。那么,减少响应时间的一个原则就是:让数据离客户端更近。

一个最能体现更近原则的优化就是客户端缓存–客户端是距离用户最近的地方。

更加细节的一些例子,比如sql server的nonclustered index现在可以把非键的数据挂载在索引的叶子节点上,这样就不需要再去表上扫描获取这些数据。这也是更近原则的一个体现。

更快

更加快速的响应,需要更加快速的计算。前文中提到的加速页面元素的生成,就属于更快原则。

这方面的实践包括有算法优化,数据库索引,使用更加高效的方法,使用正则表达式匹配等等。

举个Rails中的例子:使用Model.find_by_*方法是很低效的,因为它需要调用method_missing来动态生成方法。而Model.find_by_sql方法的效率高很多。

更少

传输过多的数据,进行过多的操作,可能都会影响性能。

减少数据的传输量有很多例子:压缩静态文件,以减少服务器和客户端之间的传输量;避免在action中滥用实例变量,以减少实例变量在action和view之间的传输;正确合理使用finder,以减少从数据库中获取的数据量等等。

减少操作次数的例子有:合理运用预先加载,减少查询次数;减少transaction的不必要重复创建等等

其它还有:缩小transaction的粒度;缩小并行运算锁的粒度等等。

平衡

在追求更近、更快、更少的时候,要注意平衡。

比如给JVM分配一个合理大小的内存,而不是过大或者过小;不要滥用数据库索引,要考虑是否会影响插入数据的速度;平衡一个运算在空间和时间上的消耗;保持性能在长时间内的平稳等等。

性能优化是一个不断实践、不断调优的过程。比如前文中提到的服务器端缓存,最后并没有被采用,因为我们发现相比而言缓存命中的开销反而更大。

小结

在经过一系列的优化之后,我们的系统很好地满足了客户对性能的要求。下面是几点总结:

  1. 大多数性能问题都出在IO上,IO应是关注的重点。
  2. 性能优化是一个实践的过程,空讲理论是没有意义的。
  3. 出现性能问题,先不要怪罪于平台、语言、框架,大多数性能问题都产生于错误或者不合理的实现。
  4. 性能优化过程并不一定需要贯穿整个项目的始终,但一定要时刻保持对性能问题的关注:从刚开始的架构设计,到项目开发中的代码编写、重构等等,性能都应该是关注的一个方面。
————————————————————————————————————–
此文是三个月之前的旧文了,刚发表于2010年1月刊的《程序员》杂志。

读书时间:《软件开发沉思录》

软件开发沉思录》,参与了翻译,出版也很久了。但上个礼拜才拾起来,今天才把整本书读完。

13个来自不同职位、不同角色的ThoughtWorks员工成就了这样一本书–可以说,非常精彩。

看完这本书,第一个感觉就是公司知识和文化的传承做得充分到位。因为读很多章节,都会联想到自己在曾经或者现在项目中遇到的问题和解决方案,它们是何其的相似。第十二章-一键发布,我们在曾经的项目里面就完美地实现了。第十四章-实用主义的性能测试,我甚至很想厚着脸皮说:我们实际上早就已经超越了。

第四章-语言的盛景,让我们看到一个百花齐放的时代,这不,Google刚刚发布了它自己的语言Go。同时,随着并发的需求越来越强烈,也让我下定在今年学习一门函数式语言的决心。

公司老板在第二章里面就开宗明义的说到:敏捷过程的价值,就在于减少从“提出业务需求”直到“软件上线来满足业务需求”这两个端点之间所需的时间与成本。但这不妨碍也不冲突一个软件开发人员拥有一些艺术上的追求。在整本书中,我最钟爱的就是第六章–对象健身操。这个章节的内容并不算新,它们都可以在前人的一些书籍中找到,比如《重构》、《设计模式》等。但作者巧妙得总结出一套简单的实践和易记但深刻的经验来支持程序员写出优质的代码。如果说《重构》、《设计模式》等是基石,那么对象健身操就是一套在基石之上的简单的工具集合。

总的来说,这本书,值回票价!

Vim

It’s very important for you to manage a text editor if you are an programmer, no matter if it’s Vim or Emacs. It cannot cost you longer than one week, but you can benefit from it for a life!vim

《ThoughtWorks文集》中译本序

这本《 ThoughtWorks文集 》中译本面世之际,也正值“敏捷中国2009大会”召开在即。两者可谓相得益彰。

从 2004年进入中国,ThoughtWorks见证和参与了中国敏捷社区的发展历程:从五年前的筚路蓝缕,到如今的欣欣向荣。更令人欣慰的是,在原则、价 值观等“大问题”上,敏捷的实践者们已经基本达成共识,社区的话题更加趋于关注实践──这意味着敏捷社区正在步入成熟,将用他们的知识和技能为各自效力的 企业创造更大的价值。

我们在这个时候把《ThoughtWorks文集》翻译出版,是希望为社区的发展再尽绵薄之力。作为敏捷方法 的积极推动者,ThoughtWorks从多年、多个行业的实践中积累了丰富的经验。本书收录的13篇文章涵盖了编程技术、项目管理、持续集成、测试等方 面内容,将带领读者了解ThoughtWorks在软件生命周期各个环节所推荐的工作方式。

比较难得的是,这本《文集》不仅由 ThoughtWorks员工撰写,也由ThoughtWorks员工翻译。译者们或是与文章作者素有私交,或是在文章所论述的领域有所专擅,这也使得翻 译的质量更有保障。感谢这些译者在工作之余的辛勤翻译,才使这本《文集》如期付梓。他们是:韩锴,胡振波,金明,李剑,乔梁,熊节,徐昊,张晓庆,郑晔。

一本薄薄的《文集》当然不可能解决所有问题,我们更希望它能够收到抛砖引玉的效果。希望ThoughtWorks的经验心得能对国内的敏捷实践者们有所启发,帮助他们做出更多创新,创造更大价值。最后,希望你阅读愉快。

郭晓
总经理,ThoughtWorks中国公司

Rails之美

本文发表于《程序员》杂志2009年10月刊。可能由于编辑的工作繁忙,发表的不是此最终版本。杂志发表版本中有些不恰当表述,对此造成的困扰,深表歉意。
 
Rails之美,我总结的有这样几点:简洁 、透明、自由、开放、轻灵、丰富和优美。可能你已经感觉到,这些词汇大多展现的是感性的一面。没错,Rails开发的每一天都是那么“畅快”,畅快背后其实就是这些生动的感触。笔者希望从这些简单的感触出发,结合实际的例子,来展示Rails真实的美。

Rails之美

简洁

可能很多人在推荐别人使用Rails的时候,都会列举一个理由:简洁。的确,简洁是促使很多人开始学习和使用Rails的原因。那到底什么是简洁?简洁可能代表少,简洁可能代表没有重复,简洁当然也代表复杂的对立面。

Rails是基于ruby语言的。动态语言带来的好处之一是代码量的急剧减少。有一个鲜活的例子,有一次跟客户进行pair,把曾经用Java实现的一个900多行的类,缩减到了100行。客户很是惊讶。当然,纯粹量的减少可能并不代表什么,但至少带来了清晰和易读这两个对代码来说非常重要的特性。

因为动态语言的良好支持,Rails框架使重复的配置工作减少到了极致。比如在Java世界的大量OR Mapping配置文件,在Rails里面不再需要。虽然现在Java世界的配置量也在不断地精简,但还是占据了一定的工作量。重复工作的减少,亦即工作效率的提升。

作为Web开发领域的DSL,Rails提供的各种机制在各个层面极大地简化了开发的工作量和难度。比如ActionView提供的FormHelper,简化了页面上form的生成;比如ActiveRecord提供的Association,简化了模型之间关联的维护。

举个association的例子,来看一下Rails的简洁之处。下面这两个模型是一对多的关系:一个lightbox有很多images。

class Lightbox < ActiveRecord::Base   
end   
  
class Image < ActiveRecord::Base   
end

要删除一个lightbox,以及它的所有images,需要这样写:

@images = Image.find_by_lightbox_id(@lightbox.id)   
@images.each do |image|   
  image.destroy   
end   
  
@lightbox.destroy
 

接下来,让我们给它们声明正确的关联关系:

class Lightbox < ActiveRecord::Base   
  has_many :images, :dependent => :destroy   
end   
  
class Image < ActiveRecord::Base   
  belongs_to :lightbox
end

则删除操作就变得简单了,且在语义和逻辑上更加明确和清晰:

@lightbox.destroy

DSL的一个目的是使某个领域的开发变得更加具体、简单和清晰。Rails框架是从一个现实项目中提炼出来的,这同时也证明了一句话:好的框架都不是凭空想象出来的。

Rails还有很多其它方面可以体现它的简单。简单,就是美。

透明

项目开发过程中,让我觉得很痛快的一件事情是:基本上不需要借助任何外部的文档。

因为Rails本身是透明的,这首先是动态语言提供的好处。当需要了解任何一个方法的功能或者实现时,只需要跳到那个方法查看源代码即可。

同时,对大多数方法,Rails都提供了详尽的文档以及具体的示例。

开放的源代码,以及详尽的注释,让开发人员得以在一个“透明的环境”上进行开发。开发中可以彻底地了解所用工具的习性,这不可谓不是一件痛快的事情。

自由

对一个问题,Rails往往都提供了多种解决方案。我们可以根据问题的场景,自由地选择合适的方案。

比如对于页面中form的生成,我们可以选择使用form_tag方法。但当这个form跟对象关联的时候,更好的选择是使用form_for方法。结合text_field等helper方法,使页面上的元素跟对象的属性更加紧密地结合。

下面再举一个association的例子,来看看Rails如何表现自由的精神。声明多对多关系时,一般是这样的:

class Teacher  
  has_and_belongs_to_many :students  
end  
  
class Student  
  has_and_belongs_to_many :teachers  
end
 

但有时我们需要利用中间表,并让它映射到一个模型。那么,可以这样来声明模型之间多对多的关联:

class Teacher  
  has_many :relations  
  has_many :students, :through => :relations  
end  
  
class Relation  
  belongs_to :teacher  
  belongs_to :student  
end  
  
class Student  
  has_many :relations  
  has_many :teachers, :through => :relations  
end 

选择的多样化带来的是自由。但根据需要和场合选择正确的方案,更加重要。

开放

Rails所体现的一点极其重要的精神是:开放。因为Rails从来不限制你去做任何事情。

有个项目是建立在一个遗留数据库上,并且大多数数据库表结构和遗留数据因为有些原因不能更改。但问题是表结构并不满足Rails的约定,下面列举一些问题和解决办法来窥探一下Rails的开放精神所在。

问题1:单数表名

根据Rails的约定,表名都是复数形式的。比如一个User模型,对应的表名是users。而遗留数据库上的表名是单数形式:user。

解决方案

在environment.rb文件的配置初始化里,关闭默认的复数表名配置:

config.active_record.pluralize_table_names = false

问题2:type字段不代表单表继承

根据Rails的约定,type字段是单表继承的保留字段。当从数据库读取数据并实例化成对象时,它会根据type字段的内容来寻找相应的子类型。但这里type字段并不代表单表继承。

解决方案

在模型里面声明另一个字段代表单表继承,比如:

set_inheritance_column :clazz

问题3:type字段的值不满足单表继承的约定

又出现另一个问题,type字段用来表示单表继承,但是它的值并不满足单表继承的约定。根据Rails的约定,type的值应该是类名。比如ShoppingCart继承自ImageCollection,那么type的值应该是”ShoppingCart”。但在遗留数据库里,使用的是”shopping_cart”。

解决方案

这里涉及到两个问题,一是在存储一个对象时要设置正确的type值,二是实例化成对象时,需要根据type值找到正确的子类型。通过查看源代码,发现计算type值和子类型的分别是ActiveRecord上的sti_name和computer_type方法。大家都应该想到了解决方法,就是覆盖这两个方法。对于ShoppingCart类,解决方案可以这样:

def sti_name
  super.underscore.upcase
end

def compute_type(type_name)
  super(type_name.downcase.camelize)
end

通过上面的例子,我们已经了解了Rails的开放。Rails有很多约定,但不代表强制。而且Rails的开放不仅限于此,比如你可以打开任何一个类,往里面添加方法(当然,这是Ruby给予的权力)。举个例子,比如我们可以通过打开NilClass,来实现Null Object模式(在有些情况下这种做法比较极端):

NilClass.class_eval do
  def your_method
     …
  end
end

有些语言在天性上对程序员防备多于信任,他们总觉得赋予程序员过多的权力,会容易带来破坏。但其实防止破坏靠的应该是程序员的修炼和自我约束。语言,应该以一种更加开放的态度赋予程序员更多的权利和自由。

轻灵

很多人都觉得Rails是一个庞然大物。但其实Rails并不庞大,DHH在迷思系列里面也解释过。而且,Rails可以轻松剔除任一可选组件。比如要去除ActionController的benchmark组件,只要注释掉include ActionController::Benchmarking,并删除相应的文件即可。

在这背后,其实有一个神奇的方法,叫做alias_method_chain。这个方法非常有用,它的设计理念很好地支持了Rails轻灵的特性,因为它让Rails的各个可选组件都只是很“轻巧”地挂载在上面。继续用Benchmarking这个例子来了解一下它。

Benchmarking的功能是度量action的性能,并把结果输出到日志。这其实是对perform_action方法的增强。从Benchmarking源代码中可以看到如下的代码:

alias_method_chain :perform_action, :benchmark

以及perform_action_with_benchmark方法的实现。

其实,alias_method_chain跟下面的实现是等价的:

alias_method :perform_action_without_benchmark, :perform_action  # 为原来的方法建立别名
alias_method :perform_action, :perform_action_with_benchmark      # 重定向原来的方法名到功能增强之后的方法

这种方式其实就是AOP的工作方式:ActionController::Base声明了perform_action方法,但它对benchmarking一无所知,只要把Benchmarking模块包含进去,就获得了benchmark的功能。Rails通过这种方式实现了低耦合,我们可以轻松地选择去除非必要的所有可选组件。Rails并非庞大,它是轻灵的,但我们需要了解它。

丰富

发展到今天Ruby和Rails社区已经非常的活跃和强大。千万开源爱好者在不停地贡献着各种各样的Rails插件和Ruby库。

比如认证系统插件:restful_authentication,分页插件:will_paginate等等,这些插件的出现帮助我们节省了很多工作。并且,这些插件的实现都非常的优美,并不断地在优化和演进。

强大的社区支持,丰富的插件,让Rails开发变得更加容易。

优美

Rails的优美体现在很多地方,比如它本身就是一个REST风格的WEB架构。

但REST就代表着优美么?这不足以让人信服,还是举个例子吧。

假定有一个InvoicesController,现在需要生成一个invoice的pdf。我们可能会想到给controller添加一个叫做download_pdf的action:

def download_pdf
  invoice = Invoice.find(params[:id])
  send_data(generate_pdf(invoice), :filename =>

    “#{invoice.no}.pdf”, :type => “application/pdf”)
end

但从另一个角度看,其实pdf只是invoice这个资源的一种表现形式。而展现一个资源,更适合让show action来做。用REST风格实现invoide的pdf下载:

def show
  @invoice = Invoice.find(params[:id])
  respond_to do |format|
    format.html
    format.pdf { render :pdf => generate_pdf(@invoice) }
  end
end

用正确的方式做正确的事情,就是优美的体现。

反思

介绍了这么多Rails开发的优点,肯定有人不禁要提问:Rails开发难道就没有欠缺的地方么?

笔者也一样,在开发过程中不停地反思。比如为Rails创造最大声誉的“快速开发”,就值得谨慎看待。用Rails搭建的系统在代码量上确实少了很多,但纯粹用代码量来衡量开发效率是不准确的。有几点思考:

1. 当今程序员不再是纯文本编辑时代,强大的IDE极大地提高了程序员的开发效率。但作为一种动态语言,Ruby目前还很难享受到这种好处。
2. 对于任何语言,要写出简洁优美高效的代码,都需要精雕细琢。
3. 随着语言的进步,开发效率真正的瓶颈越来越多地体现在业务逻辑上,而不是代码的编写上,特别是对于复杂系统而言。

从上文列举的那些例子中我们可以看到,Rails很美,但只有在正确使用它时才会很美。初学者可能会因为经验的缺乏落入一个又一个陷阱。不可否认,Rails的性能问题一直是大家担心的。但系统的性能问题真的是Rails本身引起的么?看似优美的代码,背后是否做着一些“丑陋”的事情?希望能在下篇讲述性能优化故事的文章中跟大家再次分享和探讨这些问题。

无关敏捷,关乎责任

JJG在《The Elements of User Experience》特别强调,要让每一个人参与到网站设计中:高层管理人员,市场人员,销售人员,等等。不过这里,我想他忽略了一个很重要的群体,就是开发团队

The Elements of User Experience》把用户体验分为五个要素: Strategy, Scope, Structure, Skeleton, Surface。

其中最根本的是strategy,因为它是用户的需求和网站的目标

在我们的开发过程中,拿到一个story并不意味着开发的开始,而往往很多时候我们会花很多时间论证这个story的价值所在。开发团队经常会向客户提很多问题,探究这个story的起源和目的;开发团队经常和客户一起讨论甚至争论一个story的功能或者设计,因为随着开发的深入,对项目的了解,我们有义务告诉客户我们所想,帮助客户找到真正所需。

讨论的结果可能证明开发团队是错的,也可能证明客户是错的。但双方都在讨论中对story的价值越加清晰。

因为通过争论,客户会发现

  • 其实这才是我们真正想要的:经过向公司相关人员咨询,发现这果然是更好的方案。
  • 原来可以通过这种更简单的方式得到我们想要的,得到用户所需要的。
  • 应该丢弃这个功能,这样做是错的,这样的设计不仅对我们未来的业务发展没有好处,而且还可能成为一个束缚。
  • ……

开发团队:

  • 的确客户是对的,我们在实现的东西是有价值的。
  • 又一次不仅帮助了客户找到了真正的价值,也避免了让自己花很多时间做一个用户不会喜欢的功能。
  • ……

印象特别深刻的是在项目结束之后,客户的BA诚挚地对我们说:谢谢团队的每一个人,谢谢你们不停地问问题

后面的那句话,我想,是他们意外得到的。所以在感谢的时候特别地提了一下。

开发团队保证正确实现客户所要的,就够了么?不,开发团队要保证正确实现客户真正想要的

这里无关敏捷,这里关乎责任

The Elements of User Experience

发布之后

release

发布之后,系统才开始在真实的数据、环境上运行,才开始经受真实用户的考验。发布,不意味着项目的结束,却是挑战的到来。如何在发布之后,快速修复影响到 系统使用的bug;如何在发布之后,快速改进在真实环境中无法承受的性能问题;如何在发布之后,快速调整用户体验较差的界面设计或者功能实现。开发团队或 者维护团队,如果不能快速响应这些突然袭来的变化,就会给客户带来损失。

同时,从发布之后出现的问题,可以反思开发过程中某些方面的不足。以下会列出我们在发布之后遇到的一系列问题,以及对这些问题的思考。

难以重现的问题

当发现bug的时候,立即想到的就是在测试环境或者UAT上重现问题,以便快速定位问题的根源。

场景

系 统发布一个小时之后,客户一封邮件过来,告诉我们有一个重要功能在产品环境上不工作:一个公司无法为她的包月套餐付费。于是,我们立即在测试环境上试图重 现问题,但失败了;UAT上也如是。这是一个重要的功能,经过详尽的测试,却在产品环境上突然出现问题,令人匪夷所思。于是,只好到产品环境上测试,果然 重现了bug。首先排除了环境的区别,那么肯定就是数据的区别。在仔细观察数据的时候,敏感地注意到一个特征:被测公司的所有员工都没有接受网站的条款。 马上在测试环境中建立同样特征的测试数据,果然重现了bug,并找到了问题的根源。

反思

有些bug会很难重现,而之所以难,根本原因在于忽略了出现bug的测试场景(包括数据、环境等因素)。导致这个bug出现的根源在于测试不够全面。如果在测试中尽量覆盖边际情况,就可以避免多数类似问题的出现。

产品环境中仅有的问题

产品环境和测试环境的差异在哪里?

场景1

这不是一个功能性bug:有一个页面在产品环境中出现了不应该有的滚动条,却十分影响页面的美观。对照测试环境和产品环境之后,发现是因为在产品环境中一个链接的url过长导致信息框出现了滚动条。

场景2

页面上有个回退链接,会回退到之前访问的页面。有些时候却回退到了网站的首页,在测试环境中无论如何也不能重现这个bug。在产品环境下,发现出现问题的页面都是从https跳转过来。查看代码,果然没有考虑这种情况。

场景3

在用某些关键词搜索的时候,会出现500错误。而在测试环境中却无法重现。非常幸运的是客户发来了产品环境的log,经过分析,发现问题在于产品环境中集成的第三方工具提供的有些数据会导致程序错误。

反思

这 些都是典型的由于测试环境和产品环境数据或者环境不一致引发的问题。如果能在测试环境中尽量保持数据的拟真性、环境的真实性,则可以尽量避免这些问题在发 布之后才被发现。但从另一方面看,出现这些问题的根本原因在于代码不够完善。场景一中前端代码的包容性不够好;场景二中引起问题的代码有一些比如hard code的bad smell,却没有被及时修复;而场景三的代码是有漏洞的,它没有很优雅地处理数据获取失败时的情况。

无法获知起因的问题

有些用户遇到了问题,但我们无法或者没有时间去一一获取这些用户的信息。

场景

新系统上线之后,所有老用户都得重新接受新的网站条款。但有些用户无法点击接受条款的按钮,严重阻碍了这些用户的回访。

反思

这 个问题可能只出现在千分之一的用户里面,试图去获取所有这些用户的客户端环境(浏览器版本)很困难,而客户又要求我们立即解决问题。所以,试图去重现问题 已经不可能了。不如凭着经验审查一下原有的代码是否有瑕疵,前端代码是否浏览器敏感。然后给出一个更通用、更完美的方案。这类问题是无法避免的,除非花费 大量时间对所有客户端环境都进行测试。但这类问题是可以解决的,比如上面那个bug我们就通过采用兼容性更好的代码解决了。

真实用户体验的问题

真实用户才是真正的测试人员。

场景1

一个页面有分页功能。用户来到了第n页,进行了一个操作之后,重定向到了第一页。而用户显然希望能继续回到第n页。

场景2

几个按钮应该排成一行,但当用户输入一个很长的名字之后,出现了换行。

反思

这类问题不算是bug,但却影响了用户的体验。测试人员虽然会站在用户的角度去测试功能,但最好的测试人员其实就是真实用户。如果有条件能在发布之前,让一些真实用户参与测试,是发现这类问题的最好方法。

遗留的疑难问题

既然是遗留的问题,肯定是难以解决的问题。

场景

系统中有几个暂时不影响发布系统使用的bug,它们被一直拖到了发布之后,因为之前“难以解决”。

反思

虽 然这些bug最终都被修复,但这是一种不正确的方式。疑难问题不应该留到发布之后:在发布之后能解决的问题,那么在发布之前也一样可以解决;如果在发布之 前确实无法解决,那么就应该选择其它方案。如果在发布之后花了很长时间也解决不了这些bug,就进退两难了。同时,维护团队一定要保证部分核心开发成员的 继续留任。如果在交付产品之后,开发团队立即全部撤离,而把系统的维护交给一群对业务和实现完全不熟悉的人,是一种很不负责任的态度。

不会有问题的问题

这个功能不是已经经过验收了么?

场景

第一眼看到客户报的有些bug时,脑中飘过的第一个想法是:这个功能肯定经过详尽的测试,而且肯定经过了客户的验收,怎么可能有问题呢?但在测试环境中确实重现了bug。

反思

之 所以在看到这些bug的时候比较难以置信,是因为我们以为这些功能已经被测试过,或者我们知道这些功能曾经被测试过。但事实是,有些功能可能根本就没有被 测过;而有些功能曾经被测过,但没有被自动化测试覆盖。同时,我发现后一类问题往往都是在last mile中被引入:在发布之前,为了解决性能问题,我们需要对设计或者实现进行一些大规模的改动;而到后期,测试人员因为功能性需求的结束而退出了团队。 当时非常担心大规模的改动会引起一些问题,在发布之后验证了这种担心。这类问题其实是可以尽量避免的:首先,更早开始性能测试和性能优化,以避免在 last mile进行大量改动;其次,last mile可能会出现赶工的情况,有大量功能的改动或者设计的变更,这时候是测试人员最不应该离开团队的时候。

不是问题的问题

客户坚持说,这里有问题。

场景1

客户说:在使用firefox浏览器的回退按钮时,能看到不实时的信息。

场景2

客户说:有一个bug… 过了一会儿,客户又说:bug好像没了。不过,总之问题出现过,你得去修复。

反思

这 些不是问题的问题,浪费了维护团队的大量时间。发布之后,维护团队需要及时地修复很多bug,应该尽量地排除不必要的干扰。在一个维护团队中,也需要维护 一个流程。我们的维护团队,由两个开发人员和一个测试人员构成。测试人员负责接纳并验证所有的bug,开发人员负责修复bug,再经测试人员测试,最后经 由客户验收。通过坚持这个流程,让团队的每个成员,都关心自己最应该关心的问题,而不应该被其它事情干扰。在发布之后,需要更加快速的反馈,而严格地执行 流程,能明显地提高团队的效率。

总结

系统在发布之后经历了一段时间的考验,bug不多,并且基本没有出现影响系统使用的bug;同时,维护团队保持了高效的反馈,及时地修复了大部分bug。高质量的代码和快速地反馈得到了客户的认可。

现在回头看这些在发布之后出现的问题,我相信,没有一个系统能确保全部避免。不过从我的反思中可以看到,这类问题大多是开发过程中的“不完美”而遗留下来的隐患,如果能做得更好,我们就可以尽早发现这些问题,以避免这些问题在发布之后才出现。

出发

虽然工作了这么多年,但还没有在一个正式的项目从头就开始参与的经历。

工作以后的第一个实验项目是在Trilogy University的手机B2C网站,算是从开始就参与,但当时懵懵懂懂,没有太多的感受。后来陆陆续续在Trilogy做了几个项目,都是从半路杀进去。到了TW,做了两个项目,也都是在项目的中途加入。

虽然这也有好处,比如提升了在已有代码基础上如何快速上手,改进既有架构设计等能力。但没有从头开始参与过一个项目,总有种人生不完整的感觉。再优秀的框架,也是别人先搭建好的。从头开始做项目所能经历的事情,比如工具的准备,环境的搭建,架构的设计,需求的摸索,客户的初期沟通等等,都是陌生的。

把一件东西从无到有搭建起来,这不免让我期待这种机会。

这不,它来了。

这次项目的客户是一个旅游网站,本来有机会去澳洲做inception,但因为种种原因没有成行。倒不可惜,因为在项目正式开始之前,我们就忙碌着做准备工作了。说实话,我更庆幸澳洲没有成行,准备工作不失一些很有价值的东西。虽然不见得带来技术能力提升,但至少经历过了。

Web认证方法探视Git学习机器安装和环境准备用Ldap实现Web认证,JRuby,Restlet….

这个过程像是在黑暗中前行,有点担忧,但随着跟客户的沟通、以及准备工作的积极进行,目标正一点点变得清晰。原先拿到proposal时的一头雾水已经消失,心里也越来越踏实。这个过程也有一丝丝的兴奋和成就感,应用什么技术、使用什么框架、搭建怎样的环境,都成了我们可以决定的事情。

下个礼拜项目就要正式开始了,经过几周的准备工作,团队正蓄势待发。出发吧,唱着亡命之徒。

技术写作

最近写了几篇技术文章

我写技术文章的主要原因是:对此门技术感兴趣,或者需要在最近了解和掌握此门技术。

我写技术文章的主要目的是:

1. 通过写作来对此门技术加深更多的了解。通过写文章来学习技术是一个非常好的途径,因为当你要将此门技术讲解给别人听的时候,你需要对它有个更深的了解。高中老师一直劝告我们要以老师而不是学生的心态去学习,就是同一个道理。

2. 希望借此途径与共同的爱好者分享以及讨论。

3. 帮助自己记忆。我的一个毛病是容易忘记,所以需要记录下来以备后用。我已经屡次通过实践证明在自己写的文章上能更快地寻回记忆。

需要做哪些准备:当然是了解和熟悉此门技术,不管你用什么途径–阅读或者练习等等。

怎样写技术文章:除非有些非常枯燥抽象的概念,我希望把技术文章写得通俗易懂一些,并通过举一些现实的例子来解释它,使它更易被读者接受和吸收。反观之,如果不能简单明了地说清楚一个问题,说明我自己也还没有很好地掌握。

写技术文章还是挺耗时间的,一般一篇看似较短的文章,比如这篇,也花了我几个小时。不过,当完成此篇文章的时候,确实有一种满足感。有满足感,就够了。

乘时间机器,看敏捷旅程

programmer0811

BOSCO系统是一个在线品牌管理系统,此项目的客户是一家跨国酒店集团,旗下拥有多个世界著名的酒店品牌。BOSCO系统将服务标准化、标准符合度审 查、改进流程管理等酒店品牌管理的工作内容整合到一个信息系统中,来提高相关人员的工作效率。目前BOSCO系统已经被全球十个酒店品牌、超过1000家 酒店使用,用户超过8000人。

BOSCO系统的开发基于Ruby On Rails,在项目的开发过程中应用了敏捷开发方法。在开发此系统的8个月中,经历了15个迭代和3次发布。通过这种与客户紧密合作的工作方式我们按时交 付了系统,并得到了客户的高度认可。笔者作为开发团队的一员,从开发者的角度对基于Ruby On Rails的敏捷开发实践总结出一些心得体会,其中有成功的经验,也有失败的教训,愿与敏捷开发爱好者分享。

在我们的系统中,有一个有趣的类,叫做TimeMachine,用于修改UAT服务器的系统时间。但现在,让它带我们穿梭时空吧。

TimeMachine.go_to(”2008-04-15″)

半路接手 关键字:Knowledge Transfer,结对编程

2008年4月,BOSCO系统漂洋过海来到中国。此时,它已经在美国经过了3个月的开发,且完成了第一次发布。现在要开始它的移植过程(从美国团 队的大脑移植到中国团队的大脑)。给我们的资源并不多,只有3个项目原有工作人员:一个BA(业务分析师),两个Dev(开发人员)。但时间却相当有限: 一个月。

虽然BOSCO系统的开发时间并不长,但Rails强大的表现力使系统已经足够庞大到让我们担忧Knowledge Transfer能否在约定的时间内完成。而且中国的开发团队中,真正有Rails开发经验的只有一人。如何在这么短的时间内,掌握这么庞大的系统,成了 摆在眼前的挑战。

我想很多遇到过此种情况的开发人员都不会对以下几种经历感到陌生:

  • 阅读大量关于系统原理和项目架构的文档,但它们并不能直观、准确地反应项目情况。
  • 参加各种标榜着“Knowledge Transfer”且耗时长久的会议,但收益甚少。
  • 独自面对系统时不知从何下手,不断地寻求原有开发人员的帮助。
  • 由于不熟悉系统造成在开发过程中不断犯错,贡献了很多“垃圾”代码,并影响了系统开发进度。

其实对于开发人员而言,掌握一个项目最重要的是提升对项目本身的“熟悉度”。此熟悉度代表对陌生的技术知识,代码的编写风格,开发的习惯,程序的架构,环境的搭建等等的掌握和了解。而结对编程是快速掌握这些知识技能的秘诀,还是拿事实来说话吧。

两个礼拜之后,作为先行和两个美国Dev结对的同事已经能够带新人了,而且开发速度不亚于两个美国 Dev。短短两个礼拜,中国团队对项目的整体架构,不说了然于心,但也是熟门熟路了;对于一些story,也能得心应手开始实现了。这是我们自己也预想不到的速度。

之所以能在如此短的时间内掌握项目,就是拜“结对编程”所带来的好处所赐。有人会说,结对编程到底有什么魔法?其实没有魔法,它只是简单地实践了很多人都 懂得的道理:学习一件东西最快速的手段就是动手去做。而结对编程,不仅能让你有立即动手去做的机会,而且边上还有个让你观察学习、给你指导的老师。这是我 对结对编程的初次体验,让我非常兴奋。还记得一个朋友给我电话告诉我“项目中的新人不能快速掌握知识的问题”时,我告诉他:不要犹豫了,结对编程吧。

结对编程在ThoughtWorks是一种常态,它作为敏捷开发中一项备受推崇的实践也已经被业界所熟知。但真正开始实施的公司并不多,这是一种奇怪的现 象,但同时也是可以理解的现实。首先是因为未有机会体验结对编程美妙的开发团队或许还有所疑虑,其次是来自老板、客户等非开发人员的压力。 Knowledge Transfer作为每个团队都可能会经历的事情,不失为开始尝试结对编程的好时机。我相信,一当你开始尝试,你就不愿意停止。

TimeMachine.go_to(”2008-05-15″)

加速前进 关键字:最佳实践

随着Knowledge Transfer期满、美国同事离去,客户一度很担心中国团队能否掌控项目,能否保持开发速度。但在快速接手项目之后,项目开发随即进入了加速通道。在完 全接手项目的第一个迭代之后,中国团队已经赶上了美国同事的开发速度。在这个过程中,除了因为团队的快速学习能力之外,还有我们保持的一些最佳实践让我们 赢得了胜利。下面,让我简单介绍其中一些。

一致的开发机器:我们有6个开发人员,也就是3对pair。Pair的3台机器都是漂亮精致的Mac Mini。当然这不是重点,重点在于,3台机器都有一致的开发环境:一致的程序目录,一致的安装软件,一致的快捷命令等等。完全一模一样的配置使pair 在进行切换时,不会对另外一台机器感到丝毫的陌生,这有助于开发人员迅速进入开发状态。

快捷化常用命令:在开发时,我们每天都要进行无数次相同的一些操作:进入rails开发根目录,启动web server,签入代码等等。比如当你要进入rails开发目录,你就得打入命令:cd /Users/rails/workspace/bosco/rails_root。这是一个简单的命令,但同时也是一个繁琐的命令,特别是当我们每天要 进行多次这样的操作时,就会浪费大量的时间。这时,如果你为它加一个alias,把这个枯燥的操作用一个简单的命令”rr”代替,它会帮你节省多少时间 呢?效率的提升有时就在于一些简单的事情。

不做简单重复的劳动:每天的工作总会有一些事情需要简单重复的劳动。比如当你要验证一项功能时,你需要手工打开网页,然后按 着流程一步一步操作,来看实现是否正确等。而每当此时,我们的选择是:绝不做简单重复的劳动,而是实现一个自动化的脚本来帮助我们进行这些操作。自动化不 同的任务,可以选择不同的工具,比如selenium,ruby,shell等等。后来我在《The Productive Programmer》中读到Neal Ford大师如是说:“手工执行简单重复的任务会让你变傻,会消耗你的注意力,而注意力是最重要的生产力之源。找出一种聪明的方法来自动化这些任务,这会 让你变得聪明,因为你能从中学到一些东西。”

每天早上的code diff:在早晨的站立会议结束后,我们并没有马上着手清扫story。所有开发人员 会挤在一台机器前,查看前一天的code diff。大家会看到所有成员在前一天的工作中修改的代码,相关的开发人员会对自己的代码作出解释。当有人对一段修改有疑问时,我们就会对此段代码进行讨 论。或者是实现上的一些逻辑漏洞,或者是一些不规范的代码编写,或者是一些可能的性能改进,我们总能对一些代码提出疑问,并提出改进意见。Code diff过程是对站立会议以及结对编程的补充,是对代码质量的进一步检验,有助于团队对代码的了解,也促进了代码质量的提升。短短十分钟,何乐而不为?

每周的技术session:每周的技术session是我们一直保持的良好传统,团队从中受益匪浅。项目中总会遇到一些难题,或许是 一种陌生的技术,或许是一个难解的问题。而此时,总会有人站出来说:“让我来讲讲这个吧”。或许这项技术对于主持者也是完全陌生的,但不要紧,接下来一周 紧张的学习已经足够(这也会促使他更加快速地学习)。一周之后,主持者就为我们带来了一道“丰盛的大餐”。在session中,团队成员都会踊跃发言,并 乐于抛出任何疑问让大家讨论。讨论是最能产生火花的,来自每个人不同的思考会让你对问题了解得更加深刻。讨论帮助我们解开了一些困扰已久的疑问,并加深了 对一些技术的理解,比如:CSS,Memory Cache, REST, Ajax,Ruby的对象模型等等。通过技术的讨论和学习,团队成员的整体开发能力得到了提升,这极大地促进了项目的开发速度。

团队需要勇于尝试一些实践来促进团队的开发效率和提升团队的整体能力。上面所提及的有些实践都是我们在平时的工作中摸索、体会和总结出来的。比如每周的技术session,就是在成功地开展了一次之后,被保留和坚持下来了。

如何发现一些值得尝试的实践?这看似很难,其实很简单。在项目中遇到的问题,工作中偶然发现的一些事情,别人的经验,或者是你自己的一些想法,都可 能是对一项实践的启发。你唯一要做的,就是勇于尝试。“从来没有人这么做过”,或者“别人都不这么做”不是你不能这么做的理由。

TimeMachine.go_to(”2008-08-01″)

挑战难题

当项目开发进入第5个月,我们遇到了一些真正的困难,它们分别是“历史”story和系统性能优化问题。

当项目遇到困难时,正是审视敏捷实践的最好机会。TDD,简单设计,持续集成,重构等等,让我们看看这些实践在项目开发中显现的力量吧。

笑看历史 关键字:简单设计,TDD(测试驱动设计)

历史story:作为全球质量管理员我要查看一个酒店的标准审查历史记录从而跟踪并比较酒店在各个时期的标准符合度。

简单分解一下这个Story,它要求提供的功能是:

  • 系统在一些比如标准变化,酒店审核等事件发生时能保存当时的数据
  • 系统应该提供一个界面,让用户通过选择特定的日期查看历史信息。

跟通常一样,我们经过estimation(当时估的是5个points,并不太大),以及tasking(简单的设计)之后,并开始实现这个story。

刚开始,这个story如预期一样顺利地开发下去了。一天,两天,到了第三天,我们突然发现在实现一些功能时有点举步维艰。但通过一些“邪恶”的手段,我 们还是解决了那些问题,虽然觉得那样的实现并不是最佳实现。到了第四天,在实现另一些功能时,我们陷入了绝境:实现的过于复杂让代码改动极其困难,通过了 这边的测试,那边的测试就失败了,如此反复。还差两天迭代就要结束了,这个story却深陷泥沼,团队陷入担忧之中。这时,正如团队一成员所说,忍无可 忍,让我们重新跳出来看一下原来的设计吧。

基于实现中遇到的困难,我们讨论了原先的设计,发现了其中的一些缺陷,并迅速找到了一个更好的设计。新设计雏形初具,再次动手吧。改动代码,很多原先的测 试应声失败了。好事情,失败的测试告诉我们哪里出了问题,过去修正那些失败的测试吧。红,绿,红,绿,按照这样的节奏,出乎所有人意料的事情发生了:一个 下午,仅仅是一个下午,我们把历史story完成了!

因为这个设计并不在技术层面,而在业务层面,所以在这里我并不想多讲story的详情。但发生这样的问题,是否会让你怀疑敏捷实践“简单设计”呢?如果你有此怀疑,我的想法就正好跟你相反,因为通过这个story让我更加相信“简单设计”的好处。

首先,即使现在回头看,我们还是认为就算当初花几倍的时间去做设计,也难免犯同样的错误。其次,检验设计优劣的最佳工具就是代码本身,及早地应用设计于代 码,让代码来告诉你设计正确与否。这正是在这个story陷入泥沼时我们的反应:实现不能继续,来看看设计的问题吧。再次,之后我们的设计之所以如此成功 地符合了业务的需求,是因为前一次失败的经历让我们对代码有了更进一步的了解,对问题有了更清晰的洞察。谜团是在行进中解开的,并不是一开始就能知晓。最 后,我想我们唯一应该改进的,就是在发现开发出现困难时,能更早地跳出来,从设计角度分析一下问题。

新的设计实现能在一个下午完成,是因为高测试覆盖率为正确的重构实施提供了安全保障。测试总能迅速地提醒我们哪些地方出了错,以保证重构的正确。其 次,良好的代码结构,也是代码能如此快速地修改完成的原因。这些都是TDD开发带来的好处,TDD开发不仅能提供高测试覆盖率,也能带来良好的代码设计。 这就是越来越多的人把TDD称之为测试驱动设计(Test-Driven Design)的原因。

TDD给设计带来的一些好处:

  • TDD迫使你在编写代码之前,考虑更多对象之间的交互。
  • TDD迫使你把对象的创建封装在一个更好的层次上。
  • TDD会让你写出更加小而内聚的方法,从而使方法的重用以及纠错变得更加方便、快速。

百倍加速 关键字:性能,重构,YAGNI

随着项目进入后期,功能的开发已经基本完成。此时,我们面临的是系统的性能问题:发布一个标准竟然要5个小时,这是不能忍受的。由于发布标准需要在一个事 务内完成,而且它需要修改大量数据。所以在这5个小时内,数据库的很多表都被锁定,系统几乎处于瘫痪状态。于是,顶着巨大的压力,我们开始了性能优化之 旅。

先通过添加日志找出最耗时的操作,然后仔细地跟踪和分析这块代码。我们随即发现了一个需时十分钟的操作,这个方法的时间复杂度是n2。但这段代码犯了一个低级错误,它可以被优化成一个n复杂度的方法。立即修改,修改后由于消除了大量重复操作,它的耗时竟然不超过一秒。“秒杀十分钟”就这样诞生了。随着我们进一步的跟踪和分析,多处设计上的问题暴露出来了。几天的改进之后, 5个小时的操作缩减到了1个小时。

但显然,一个小时的操作还是不能为客户所接受的。于是,继续优化。马上,我们就发现标准发布操作对很多标准都进行了重新算分,但实际上只需对有改动的标准 算分即可。而没有改动的标准,可以把它的得分结果存于数据库,下次用到时从数据库读取即可。通过这个“缓存”,我们再次大大得提升了速度。运行之,整个操 作竟然只需5分钟!5个小时的操作,到现在的5分钟,百倍加速也!

这个性能优化问题是个典型的案例,它给了我们很多启示。首先,在遇到性能问题时,先别忙着埋怨平台的速度,还是先看看设计里面存在的问题吧。其次, 也是我们可以改进的一点是:在TDD“红-绿-重构”的标准开发节奏中,如果我们多花一点时间在“绿”之后进行重构,就可以避免犯一些低级错误。

或许有人会说,如果你在刚开始就做更多的设计就能避免到最后出现这样的性能问题。但我想说,非也,你忘了:你将不需要它(YAGNI)。预想开发是 个迷人的陷阱,或许可以在刚开始就花费十倍的时间去避免这个性能问题的产生。但问题时,在刚开始你怎么知道哪些是必要的设计,而哪些是浪费呢?敏捷团队注 重的是给客户创造真正有价值的业务,而不是花费大量时间去制造一个“华而不实”的系统。

“只在确实需要时才提供功能”,这是我们遵循的准则。

最后,性能改进之所以如此顺利,得益于我们遵循的一些敏捷实践。比如TDD带来的高测试覆盖率,保证了重构的正确性。在BOSCO项目的开发中,不 乏这样的大型重构,我们从不害怕修改代码,因为有坚实的测试代码为重构撑起了安全网。而反之,重构促进了代码结构的优化,提升了系统性能。

从这两个问题我们可以看到:

  • 敏捷实践促进了系统设计的灵活,代码结构的优化,系统质量的提升。
  • 各项敏捷实践之间其实是相辅相成,相互促进的。

BOSCO脚印

说了这么多,让我们来看看BOSCO系统的真实代码状态吧。

+————+——-+——-+———+———+—–+——-+

| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |

+————+——-+——-+———+———+—–+——-+

| Controllers| 1923 | 1602 | 35 | 232 | 6 | 4 |

| Helpers | 1451 | 1210 | 8 | 206 | 25 | 3 |

| Models | 4332 | 3551 | 63 | 566 | 8 | 4 |

| Libraries | 2810 | 2337 | 37 | 237 | 6 | 7 |

+————+——-+——-+———+———+—–+——-+

| Total | 10516 | 8700 | 143 | 1241 | 8 | 4.5 |

+————+——-+——-+———+———+—–+——-+

Code LOC: 8700 Test LOC: 12962 Code to Test Ratio: 1:1.5

(注:由于rake stats对rspec的统计失准,故略去。对于测试代码的总量统计,可见Test LOC。同时,rake stats只统计ruby代码,而不包含html,javascript等其它代码。)

另外,从四月到九月的5个月中,共提交代码2400次。

我们可以从以上数据看到:

  • 代码设计的优良:平均每个类不超过十个方法,每个方法不超过五行代码。如果除去特殊的Helpers模块,平均每个类不超过7个方法。
  • 高测试覆盖率:测试代码与功能代码的比例是1.5:1,行覆盖率达到90%以上。
  • 开发人员的熟练度:每对pair每天6次的提交速度足以证明开发团队对于系统的熟悉度。

另外,考虑到Ruby On Rails作为特别擅长开发Web应用的框架所具备的强大表现力,我们相信此系统的复杂度不亚于很多规模在十多万行代码的系统。

TimeMachine.go_to(”2008-09-17″)

时间定格在BOSCO项目第三次发布的那一天。除了需要一段时间的bug修复和数据准备之外,发布并没有给工作带来太多不同。我们从未因为发布的到来而惊 慌,也从未在发布时忙得不可开交。正如团队一成员所说,每次我们都是在等待发布,安静地等待着发布的到来。因为合理的安排和控制,让一切尽在掌握中。

发布成功,欢呼之余,让我们回头看看项目中的哪些实践保障了发布的顺利完成吧。

客户协作 关键字:PM、BA,还有Dev

曾经有一个同事突发感想说:“在ThoughtWorks做程序员是幸福的”。这不禁让我有很多感触,是的,在ThoughtWorks做程序开发 是一件幸福的事情。当你有任何关于业务逻辑上的疑问时,坐在身旁、招之即来的BA总能给你准确地解答;当你坐着投入地进行开发时,可能你从未想过,之所以 有这么良好的环境,是因为PM帮你阻挡了一切不该有的干扰。

PM和BA在背后付出的需求分析、客户交流等工作,让开发人员能把大多数时间和精力花在编码上。

BA在每天的站立会议之后,会有一个跟客户之间的BA站立会议。在这个会议上,他们会讨论各个业务的细节,力保每个业务细节的正确性。正是业务的正确性,让每天的开发工作能顺利进行。

PM每天会用很多时间跟客户交流项目进度,确保客户了解项目状态。并在每天工作结束之后总结一封报告邮件,这封邮件包含了对一天工作的总结,各个story的进度情况等。通过这封邮件,客户能及时地了解进度情况。

那Dev能在客户协作方面贡献什么呢?有人觉得,开发人员的任务就是从BA那里拿到story,并严格按照需求开发,而无需做与客户协作沟通等工作。其实 这并不准确,Dev完全可以在客户协作方面发挥自己的作用,因为对于整个系统架构的了解,Dev有时候比BA,甚至比客户自己还了解他们真正需要的是什 么。当我们拿到需求时,我们可以从开发者,以及整体系统架构设计者的角度考虑一下需求的可行性、必要性。有时我们会直觉有些需求并不是客户真正想要的,或 者我们可以通过另外更简单的方式给客户提供同样的功能。经过一些分析,以及跟BA的讨论之后,BA通常会接受我们的观点并与客户进一步讨论。在这个过程 中,Dev起到了帮助客户认清真正需求的作用。这是一个双赢的结局,开发人员不需要为一些无必要的功能而增加不必要的工作,影响架构的稳定性;同时,客户 可以不必为一些价值不高,甚至没有价值的功能而付出昂贵的代价。

所以,客户协作并不只是PM和BA的工作,Dev作为系统的开发者,应该从他们的角度帮助客户找到对客户真正有价值的业务。

同时,信任,是客户协作的基础。团队与客户之间只有真正信任了,才能更好地合作。我们和客户之间的相处,就如朋友,在平时的工作中甚至会经常拿对方开开玩笑。

团队合作 关键字:Retrospective,Feedback,持续改进

大家都说,这是我们呆过最开心的一个团队,因为从不缺少欢声笑语。而我觉得,这更是一个正直的团队。无论谁有优异的表现,我们从不吝啬赞扬;无论谁犯错时,我们也会毫不犹豫地指出。

每两周一次的retrospective提供了一个寻找团队问题的好机会,在每次的回顾反省中,我们都会找出一些项目中的问题,并在接下来的工作中给予改进。这样团队才能保持持续的进步。

有人会问,为什么在我们的团队中就不能保持这样良好的气氛,无法保持正直的态度呢?究其深层次的原因,首先需要公司的制度让团队的成员处于平等的地 位,不应该有谁是在“管理”谁。PM,BA,Dev之所以需要不同的职位,只是因为工作类别不同而已。PM负责管理客户期望,BA负责需求分析,而Dev 负责的是项目实现。

其次,团队作为一个整体,不应为任何问题追究到个人,而应把它归为团队集体的责任。

平等的地位和责任集体所有制,会让每个成员更具主人翁精神,也会让团队更加紧密地凝聚在一起。

团队成员的相互信任和紧密合作是项目成功的根基。

TimeMachine.go_to(“Future”)

敏捷方法并没有高深的理论,有的只是一些简单的实践。正是这些简单的实践,提升了开发人员的效率,促进了项目质量的提升,保证了项目最后的成功。

在未来的路上,让我们一起寻找更好的敏捷实践吧。

– 发表于《程序员》杂志2008年11月刊。

Next Page »