• 经过周末的休息,重新回到工作之中,而我面对的依然是那个需要优化的发布过程。正如我在之前提到过的,在上周末结束工作之前,我为这周的工作留下了一个尾巴:需要算分的企业。

    回顾一下我在之前列出的公式:
        需要算分的企业 * (真正算分的时间 + 保存历史的时间)

    从这个公式中,我们可以清晰的看到,需要算分企业的多少将直接决定循环次数的多少。但需要算分的企业真的能减少,会不会破坏业务逻辑,我们还不是特别清楚。所以,我们先要与我们可爱的BA(业务分析师)进行讨论,因为他们是项目组里面最了解业务的人。目前的逻辑是,只要有新的发布,那就会把与发布相关品牌的所有企业拿出来算分,按照我们的分析,其实没有这个必要,因为这次发布的内容可能并不会影响到有些企业。经过与BA的讨论,BA认同了我们的观点,接下来,要做的就是用实际数据测试一下,如果真的这么做了,会不会有改变。测试的数据,比较让人兴奋,我们随机的做了一次发布,涉及到的企业数就下降到原来的一半,这意味着,运行时间接近缩减到原来的一半。虽然我们需要增加一些逻辑判断企业是否需要算分,但对比于能够削减的时间而言,简直就是微不足道。那个让我们吃了午饭还有空闲的程序,已经大幅度的缩减了,至少现在我们已经敢把所有的程序都跑完了。

    继续分析日志,我们又找到一些算分时间特别长的企业,之所以这些企业之前没有发现,主要还是归功于运行时间太长,我们没有勇气运行完所有的程序,或者说,不想偷懒到那种地步。把一个企业单独拿出来算分,运行时间在25秒中以上,其中保存历史大约10秒种,也就是纯粹算分时间在15秒左右。这可是经过之前的优化,否则时间会更长。

    我们打开代码继续分析,我和Pair突然发现了一个问题,还是与分类相关。之前提到过,我们一次计算需要算6类的分数,但是,对于我们计算的目标而言,它只可能属于一个分类,所以,其它5类的结果必然没有任何意义。之所以之前计算了所有类别,主要是考虑到它可能会涉及到多个类别,但目前的需求已经统一到一个类别,也就是需求已经变了,但代码没有变,虽然代码很成功的覆盖了目前的情况,但也为之付出了代价。理解了这一点,我们又一次修改了代码,真正算分的操作降到了10秒左右。

    从需要算分的企业优化尝到甜头的我们,看到削减大规模计算次数的好处。所以,我们继续试图从目前的代码中发现一些可以削减计算的目标。果不其然,我们还是有机会的。

    我们每次发布其实可能只有几个需要算分的目标,但这里的计算却把这个企业所涉及的所有目标全部拿出来算分,这显然是一个非常浪费时间的操作。我们用来测试的企业,之所以被我们找了出来,原因就是它有113个需要算分的目标,而事实上,我们的发布只有1个。不过,之前一直没有保留其它算分目标的计算结果,因为,每次都重新计算,所以,倒也不必保留。不过,现在要想提高性能,我们就不得不采用空间换时间的策略,把计算过的结果保存下来,只有这次发布涉及到的目标才重新计算。我们的选择是在数据库表中增加一个字段保存计算结果,不过,考虑到之前一直没有采用这种方法,我们就在程序中加以处理,当这个字段为空的时候,就重新计算这个字段的结果。第一次跑这个程序的时候,我们并没有明显感觉到程序运行加快,因为数据库表中这个字段都是空,所有的数据都需要重新计算,但第二次计算时,速度明显加快了需要,1秒以内就可以完成。当然这个数字取决于一次发布包含的目标个数,显然,比起10秒好了很多。

    我们用这段代码对产品数据进行测试,第一次依然是一个漫长的过程,不过,第二次发布明显快了很多,大约在20分钟左右,所有的操作就宣告完成,对,就是那个曾经耗时几个小时的过程。

    我想,这次优化至此就要告一段落了。现在的这段程序优化到已经可以接受的程度了,在最开始的那个公式中,除了保存历史部分,其他部分都已经做了足够的优化。当然,我相信,保存历史部分也可以优化,但目前来说,就到这里了。

    有人在《<秒杀十分钟>之后》之后留言:
    玩企业应用的,只要精通数据库和SQL就足够了。

    我并不是太同意这个说法,从我写的这三篇blog中,可以看出,其实,真正属于SQL优化的少之又少,大部分都是逻辑的分析上。所以,如果有时间抱怨程序设计语言慢(比如Ruby),不妨先考虑一下自己的程序逻辑是不是真的写对了。

    做了三天性能优化的工作,这是最近一段时间玩得最High的几天。眼瞅着程序几倍、几十倍的提升着,心里非常痛快,当然,第一天成百上千倍的提升是最快乐的。

    这就是程序设计的快乐之处!

  • zdonking在《秒杀十分钟》中留言:
    只是把一个10分钟的运算变成瞬间,就能解决“发布一次要5个小时”的问题吗? 难道这个10分钟的方法 在发布过程中被反复调用?

    好吧!我承认,《秒杀十分钟》只是故事的开始,那只是我当天所做的工作,所以,这个故事还有后续。

    做程序的人都知道,优化一个循环中的内容,往往比只调用一次的操作来得更加有效,因为循环里面的东西会执行很多次,看似一小点的浪费,累计起来也是很可怕的。《秒杀十分钟》所做的优化其实只是一个只调用一次的操作,对它的优化并不会让整个发布操作提高很多,但是如果不优化这部分的话,那么每次到循环之前,我们都要等待非常漫长的一段时间。当然,如果喜欢偷懒,置之不理也许是一个非常不错的选择。

    在克服了前面一个漫长的过程之后,我们站在了循环的面前。这个循环里面是一个重算分的操作,因为每次发布都会带来一些改变,所以,需要重算分来保证正确性。

    一如既往,优化之前,首先要找到一些慢的地方。所以,在代码加了日志,记录不同部分的调用时间。测试依然用的是产品数据,这样做的好处是,能够尽可能贴近实际情况,而且因为数据量够大,运行可以消耗很长时间,这时候,你可以选择做一些其他的,别人一旦问起,大可以骄傲的说,跑测试呢!这种行为俗称偷懒。

    当我们吃完午饭回来的时候,和我Pair的同事决定杀死这个进程,真的要把5个小时的程序运行完,我们今天真的可以休息了,更重要的是,这时在日志里已经有足够我们进行下一步分析的结果了。我们在巨大的日志中搜索,大多数算分的操作都是在可以接受的范围内,不过,我们还是找到了一些扎眼的数据,我们把一个超过35秒的算分过程单独拿了出来,它也许可以帮助我们发现问题所在。

    这个算分的方法里面实际上包括了两个部分,一部分是真正的算分,另一部分是算分之后,要保存历史信息。把这个两个部分分离开来的话,算分部分大约是25秒,保存历史部分大约是10秒。我的主要目标瞄准了算分部分。

    这个操作大概的逻辑是这样的,有6类的分数,我们需要分别为这6个类别计算得分。我们进一步加了些日志,分别为这6类统计时间,测试结果告诉我们,我发现有一个计算失分点的操作很耗时,而且这个操作是每个类别都需要的。进一步打开这个计算失分点的操作,我突然发现,这个里面计算出现了“类别”。我们每个失分点计算的操作都会计算所有类别的失分点,然后,每个类别都根据自己的类别取到失分点。这意味着什么?6个类别就把失分点计算了6次,而每一次的计算都是计算所有的类别的失分点,换句话说,本来应该一次完成的操作就多了5次,而且这还是一个耗时的操作。好在rails本身还为我们做了一些缓存,否则,这个操作的耗时会更多。找到问题之后,解决起来就容易了,我们把这个操作提取出来,对于每次算分,失分点只计算一次就可以了。再次运行我们的程序,算分部分的时间降到了10秒左右。

    在测试过程中,我和Pair的同事一直在观察日志,我们发现了一个奇怪的问题,计算一个企业得分的过程中,会不断有查询其它企业的信息。在整体运行的时候,这个操作就显得尤为扎眼。很快,我们定位到出现这个查询的代码。经过分析,我们发现,这是由于一个关联查询带来的结果。正如我们这里分析的,其实,我们根本不需要这些信息。经过对这个方法的分析,我们发现其实,这里也是一个SQL查询就可以搞定,换句话说,我们不知不觉中,我们又多出许多数据库查询来。经过努力,我们把这段方法变成了一句SQL,再次运行那个曾经复杂的操作,算分部分降到7秒以下。其实这个方法是系统中一个比较基本的方法,对它的修改不仅仅会对我们这里的优化有意义,对其他部分也是有好处的。

    把一个25秒的操作优化到7秒,这就是我第二天优化工作的结果。虽然不能说循环中的类似部分都有相同的提高,但一定量的提高肯定是有的。

    测试显示,这部分已经不再那么漫长,但整个发布过程依然很漫长,所以,我们可走的路还很多。算分时间主要是
        需要算分的企业 * (真正算分的时间 + 保存历史的时间)

    大致看了一下代码,保存历史部分可优化的空间可能不会太大,所以,我们下一个目标瞄准了需要算分的企业。

  • 我们系统有一个发布的功能,这个发布非常慢,因为它牵扯到很多内容,所以,我们把这个发布变成了异步操作。不过,最近一段时间,这个发布变本加厉的慢了,据PM在UAT上测试的结果,发布一次要5个小时。于是,我今天决定看看为什么这个发布会慢到这个地步。

    当我分阶段为这个发布过程加上日志之后,定位到了一个非常慢的方法。我简单测试了一下这个方法,耗时在十分钟以上。这个方法要做的是,如果一个对象所关联的一些对象都已经被废弃的话,那么就把这个对象废弃。这里的废弃实际上就是一个标记的工作。说起来很简单,但是真正理解这个方法所做的事却花了我很多时间,这段代码的实现是这样的:
    * 它在关联对象的表进行查找,找到那些那些关联对象都已经废弃的情况,然后,找到这些关联对象所指的目标对象,也就是这些目标对象应该废弃,由此得到一个目标对象数组。
    * 遍历目标对象表,如果这个目标对象在目标对象数组中,那么把它标记为废弃。

    确实不好理解吧!如果你理解了这段代码的意思,你可能会问为什么会这么做,我也想知道。

    当我用产品数据库进行测试时,第一步产生的目标对象数组包含超过12000个对象,而第二步遍历目标对象表则有近8000个对象。那么判断目标对象是否在目标对象数组的操作就是一个上亿规模的操作,这是这个操作缓慢的原因。可以预见,随着这个系统使用的增多,这两个表里面的内容会越来越多,这个操作的规模会越来越大,那么这个方法就会越来越慢。

    之前曾有同事解决过一个问题,就是这里。问题是这样的,这里的操作有很长时间没有访问数据库,所有有时候,数据库连接会超时。我在这里做了一个统计,真正需要标记为废弃的对象大约只有800个,而前面提到了,这是一个上亿的数据规模,也就是说,大部分时间,系统是在空跑,而且你知道,Ruby并不是以高性能著称的程序设计语言,所以,会长时间没有任何操作。当时的解决方案是,定期进行一个对连接进行verify,告诉连接我们还活着。这是一个治标不治本的解决方案。

    简单分析一下,我们便不难发现一些问题:
    * 目标对象表一共只有8000个对象,为什么目标对象数组会超过12000。
    * 既然目标对象数据包含了所有要废弃的对象,为什么还要遍历目标对象表。

    简单这样一想,似乎解决方案就在眼前了,删除第二步操作,把第一步得到的数组做uniq,去除相同的内容,然后,遍历得到的数组,将所有的目标对象废弃。按照这个思路,把对象拿到内存中,进行处理的话,那么我们要做的是,把所有的对象找出来,然后遍历,修改状态,之后保存数据。内存操作虽好,但最终还需要写回数据库,这样算下来,这里要进行的数据库访问就是1(查找)+ N(保存)。

    且慢,我们的目标是什么?其实,这只是一个标记对象的过程,也就是一个更新表中一个字段的操作,解决问题的关键就在于如何写更新条件。更重要的是,如果这样做的话,一条SQL语句就可以搞定问题。

    好吧!我承认,我是一个SQL白痴。但我可以找到人帮忙,于是,我请了一个DBA帮忙,果不其然,当DBA明白了这里的需求,很快就帮我们写出了所需的SQL语句。

    当我们敲下回车的时候,瞬间操作就完成了。大于十分钟的操作,再见了!

  • 2008-07-31

    跨时区工作

    此刻,早上7点,我,一个ThoughtWorker,已经出现在公司。

    无论如何,这个时间出现公司,绝对是一种异常现象,没办法,因为我们项目组要开IPM(Iteration Plan Meeting)。看着同事们惺忪的睡眼,我知道,他们的心情与我是类似的。

    这么早来到公司开会,实在是不得已而为之,因为IPM通常要开两到三个小时,用以规划接下来一个迭代要做的事情,更重要的原因是,参加这个会的部分人与我们是隔“洋”相望,所以,我们的早上,对他们而言,是晚上。为了能让他们不致于熬夜到太晚,我们只好在早7点来开会。

    在这个跨时区团队中已经工作了几个月,这是第一次真正在这样一个团队中工作。

    每天早上Stand Up的时候,我们需要进入到电话会议系统中,在电话的那一段是我们这个团队的另外一部分,包括我们身在美国的同事以及我们的客户。我们相互介绍工作情况,有些需要特别注意的点,就会在这里彼此提醒。之后,他们放心地去睡觉,我们开始一天的工作。

    我们普通的工作一天,通常是从Mingle——我们公司开发的一个敏捷项目管理工具——开始的。打开Mingle,我们通常会看到美国团队的工作成果。目前美国那边的同事主要负责一些分析和QA的工作,所以,他们的工作成果往往意味着用户一些需求的调整或是一些bug,这些都被记录在Mingle的卡上,我们会从中选取优先级比较高的卡来做。

    之前有一段时间,美国团队有几个人进行开发,那段时间,我们每天早上要做的就是更新代码之后看一下diff,看看我们之前一天做了什么,还有他们在我们睡觉的时候做了什么。另外,我们在Mingle上还专门开辟一个页面,叫做Dev Standup,双方会把工作中遇到的问题记录在这里,以便另外一方知道发生了什么,有时,我们也会记录一些困惑,有相关经验的同事看到了就会给出自己的理解,所以,查看Dev Standup,也成了每天早上的工作之一。

    结束一天的开发工作之前,我们要保证自己所有的测试能够通过,包括单元测试和验收测试。持续集成工具上的红色就是最危险的警告,它提示我们,这会给美国团队留下一个噩梦的开始。测试通过之后,我们还会部署一下UAT(User Acceptance Testing)环境进行部署,这样,不仅仅是我们的同事,我们的客户也可以看见我们辛勤一天的劳动成果。

    无论如何,早上7点的IPM,是我在这个项目的一个巨大挑战。当然,按照公司通常的做法,早来可以早走,不过,今天不可以,因为晚上会有一个公司老大的Office Update,更重要的是,今天晚上还有公司内部的Wii网球比赛。
  • 2008-07-26

    一较短长

    眼下正在做的项目有希望延长,对于这个结果,有人欢喜有人忧。

    公司有个不成文的规定,在一个项目上工作一段时间之后,可以申请轮换,也就是说,可以申请去做别的项目。这样做的目的,防止长时间在一个项目上做下去产生厌烦心理。通常这个期限是三个月,当然,根据人员和项目的不同,会有所差别。

    最近一段时间,我个人一直在做一些很短期的项目,一两个月的项目,所以,根本没有机会体验从项目中轮换的感觉。眼下的这个项目,如果按期结束,就已经过了三个月的期限,如果项目延长,那是不是要轮换呢?

    有人希望轮换,因为期限已过,换到别的项目,或者开始一个新的项目,这样,可以丰富自己的体验,更重要的是,这个项目中最有价值的部分已经做完了,继续做下去,只能与那些价值不大的部分纠缠,这样,会显得比较无聊。

    关于价值,我很认同这里的说法,但是,那些价值只是业务本身的价值。我们是除了要为客户带来价值,还要为自己增值。

    在东软的时候,我曾经在一个项目上待了两年。两年时间里,我经历了那个项目从无到有,再不断进化的过程,那个项目是我在工作中投入最大的一个项目,我曾经为了能给它再做一个版本,在那个部门多待了一年。当然,它给我回报也是巨大的,它让我从一个毕业生成长为一个真正的程序员,它让我具备可以编写良好代码的能力,它让我对软件开发有了属于自己的认识,为我日后继续前进奠定了基础。

    所以,我知道长期项目的价值。

    需求变化是考验设计的真正标准,长期项目必然会经历需求变更,这会促使我们更好思考设计的正确性。
    度过甜蜜阶段之后,项目才会暴露出各种各样的问题:管理、过程、人员等等,对这些问题的思考,可以帮助我们在日后的工作选择一个更好的路。
    项目做长,早晚会遇到资源不足的情况,也就是需要所谓“优化”,而与有限资源做斗争是编程的一大乐趣。
    长期项目往往伴随着人员变更,对个人而言,这通常意味着机会的来临。
    ……

    所以,我希望做下去。

    现在这个项目对我来说,也有很多特别之处:第一次做真正意义的Rails项目,也是第一次做Web开发;第一次和外国同事协同工作;第一次体验跨时区工作;第一次比较完整的体验敏捷软件开发; 第一次在项目中大量的扮演Coach的角色;第一次在Mac上写程序……

    最初的那股新鲜劲已经过去了,我已经开始思考我在这个项目中遇到各种各样有趣的东西。更长时间的实践给为我的思考带来更多的依据,也可以让我更好的探索一些做事方法。

    工作有两个维度,广度和深度。短期项目有助增加自己的广度,长期项目则让自己深入,其实,两个方面都是必要的。