预防错误的设计

October 28th, 2014 No comments

上周参加了一个 Michael Feathers 的 workshop,整个 workshop 内容非常多,其中大部分是围绕他那本著名的《修改代码的艺术》所阐述,不过除此之外 Michael 还以 Beyond Error Handling, Using Design to Prevent Errors 为主题,讲了不少如何优雅地处理错误的做法和思路。其中的一个例子涉及并融合了面向对象和函数式的相关知识,引发我的一些思考,本文就此做一些总结。

例子的代码很典型也很简单,简化后,主要涉及三个类,分别是 Inventory, Item, 和 Sale,代码如下:

想象我们去超市购物,然后到收银台结账,当我们把一车东西交给收银员的时候,她会对每件东西(Item)逐一扫描条形码(barcode),扫描条形码的时候系统会去库存(Inventory)中查询这件东西的名称和价格,扫描完成后,收银员会计算总价格(subtotal)。上述代码所表述的就是这个场景。

读者不妨花5分钟理解一下上述代码,接着考虑一个问题,如果扫描的条形码在库存中不存在怎么办?具体来说 inventory.itemForBarcode(barcode); 返回 null 怎么办?下面的所有讨论都围绕这个问题展开。

null 判断

如果我们随意把 null 传来传去,什么都不干,那必然早晚会出现 NullPointerException ,那么最简单的做法是加上 null 判断以保证错误的 null 不会再被传出去:

这可能是我们能见到的最常见的做法,代码中到处充满了 null 判断,在某种程度上把代码主要想表达的主干逻辑给污染了,过多 null 判断影响了代码的可读性。如果所有使用变量的地方都要去检查 null,那代码必然会恶心的不行,实际中我们不会这么干,因为变量的值什么来的,很多时候也是由我们自身控制的。但如果我们在使用第三方的代码,那为了保护自己,检查 null 则是比较明智的选择了。因此,在自己的代码库内部,尽量避免传 null 并去掉 null 判断;而在集成第三方代码的时候,则多防御性地使用 null 判断

如果我们设计一个 API,那显式地告诉别人是否可以传入 null,是否会返回 null,则是很好的习惯,例如:

上述代码表示传入的 barcode 不能为 null(如果是 null,不保证会发生什么后果),而返回的 Item 则可能是 null,这样实现的时候我就不需要去判断 barcode 是否为 null,另外用这个接口的人也明确知道自己要检查返回是否为 null。@Nullable 和 @Notnull 都已经在 jsr-305 中明确定义。

异常

避免 null 判断的一种最直接做法就是抛出异常,例如:

遇到 ItemNotFoundException 怎么办?继续往上抛?处理掉?记个日志然后吃掉?在这个上下文中,我无法给出明确的答案,具体选择哪种方式还得看具体场景,通常来说,**先想想当你遇个异常的时候,你的用户希望得到怎样的结果?然后基于这点来指导你的异常处理方式。**

异常在很多情况下是很适用的,例如磁盘坏了,那抛出 IOException 让系统 Fail Fast 往往是一种很好的处理方式,然后异常也有不少问题,首先 Java 中的 Checked Exception 很容易让 API 变得丑陋无比;其次异常的跳跃式的抛来抛去,也让有 goto 的感觉,很难 follow 。

有没有更好的办法保持 API 的简洁,同时也能避免 null 判断呢?

Null Object 模式

在面向对象设计中,我们常常可以使用 Null Object Pattern 来去掉 null 检查逻辑或者异常捕捉,该例中,我们可以创建一个 NotFoundItem ,它继承自 Item,不过 name 和 price 比较特殊:

然后再 Invetory 中适当地返回 NotFoundItem:

这样,所有使用 Inventory.itemForBarcode() 的地方都不需要特殊的错误处理了,例如在 Sale 类中,addBarcode() 和 subtotal() 都能正常工作。想象一下,如果有五个、十个、或者更多的类使用 Inventory.itemForBarcode(),这样可以简化多少代码!因此,如果有可能,尽量在下层完成错误处理,因为越往上层,需要的错误处理代码往往越多。这实际上是契合 Robustness Principle 的,这条原则是这么说的:

Code that sends commands or data to other machines (or to other programs on the same machine) should conform completely to the specifications, but code that receives input should accept non-conformant input as long as the meaning is clear.

Inventory.itemForBarcode() 能够接受不合法的 barcode ,但是它返回的 Item 是符合接口定义的,没有特殊性。

到目前为止一切似乎看起来很美好,但实际上 Null Object 模式不是完美的,想象一下,如果我们要在 Sale 类中加入这样一个逻辑:如果购买物品的数量达到了10,则有5%的折扣。显然 NotFoundItem 会破坏这样的逻辑,扫描1个合法 barcode 加9个不合法的 barcode 也会造成 5% 折扣。

Option

我们花了大量的精力对付 null,事实上 null 这个概念的发明人 Tony Hoare 也说:

I call it my billion-dollar mistake.

是否有其他更好的方案来解决这个问题呢?让我们来到函数式的世界,看看 Scala 是怎么对付 null 的。Scala 内置了一个特殊类型,叫做 Option,顾名思义,可以认为 Option 可能包含一个值,也可能不包含,例如我们可以修改 Inventory.itemForBarcode() 如下:

这一段 Scala 代码也比较好理解,*itemForBarcode* 接收一个 String 参数,返回 Option[Item],而*Option[Item]* 有两种子类型,一种是 Some[Item],表示有实际的 Item,另外一种是 None,表示没有。

现在 Sale.addBarcode() 是这么处理 Option[Item] 的:

代码中对 Option[Item] 进行了迭代访问,与迭代一个集合的做法完全一样,这么做的优雅之处在于,如果 Option[Item]是 None,迭代中的逻辑不会被执行,也不会有任何副作用,与我们迭代一个空的集合一样。当然,如果 Option[Item]是 Some[Item],则 item 会被取出来并执行相应的逻辑。因此我们可以简化地把 Option 认为是一个包含1个或者0个元素的集合类。

事实上 Scala 的 Library 中大量使用了 Option,例如,由于 Scala 中的 Map 实际上有方法返回 Option,因此 Inventory.itemForBarcode() 可以简化成:

现在,*Inventory.itemForBarcode()* 接口的协议是:我会返回一个 Option[Item],您自己去迭代里面的内容。有没有可能把接口简化下?让用户(这里的 Sale )不必关心迭代呢?让接口的协议变成:我会去找 Item,找到的话帮你执行逻辑 blabla ……

高阶函数( Lambda 表达式)

要回答上面的问题,我们得看一下另一个 Scala 的函数式特性,那就是高阶函数,如果是 Java 8 ,那就是 Lambda 表达式,我们可以这样定义 Inventory.itemForBarcode(),它接收一个 barcode 和一个接收 Item 的函数 f :

如果能找到 item ,f 就会执行,现在 Sale.addBarcode() 变成了:

面向对象设计中有一条著名的原则:Tell, Don’t Ask,《Smalltalk by Example》一书的作者这样描述该法则:

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.

从 Inventory 获取数据( item 也好,null 也好,Option[Item] 也好),然后根据其内容是否存在再做操作,更多是过程式思想;相对的,扔给 Inventory 一个 barcode 和一段函数则是所谓的 Tell :去查一查,查到就干这个!

上述的代码用 Java 8 Lambda 表达式也能轻松实现,但如果是 Java 6/7 ,就比较麻烦了,你得搞一个 interface,然后传一个匿名内部类,非常麻烦。

小结

整个过程我接触了 null, 异常, Null Object Pattern, Option, 高阶函数等概念,这里有过程式编程范式、面向对象编程范式、函数式编程范式,这是最让我惊异的,各种编程范式对于错误处理有着不同的方式,你会发现 Java 作为一门面向对象语言其实糅合了不少过程式的处理方式,而 Scala 或者 Java 8 则有函数式的处理方式,而且总体上来说函数式的处理方式更加优雅。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/10/28/using-design-to-prevent-errors/

Scala元编程之Dynamic

August 31st, 2014 No comments

scala_logo

有一点 Ruby 经验的朋友都知道 Ruby 的元编程威力十分强大,Rails 中的 ActiveRecord 就是一个极佳的例子,Metaprogramming Ruby 对 Ruby 对元编程原理有着非常深入的阐述。此外,七周七语言 中也有一段介绍 Ruby 元编程的实例代码,主要是用到了 method_missing:

这段代码简单实现了一道非常著名的 Coding Kata 练习题,要实现的逻辑是把 I, II, IX 之类的罗马数字转换成阿拉伯数字,规则可参考 维基百科 解释,不过这不是本文的重点,这段代码中最有意思的是这么一行:

Ruby 支持定义一个 method_missing 方法,它带有一个名字和多个可选参数,当 RVM 执行 Ruby 代码发现类 Roman 不存在 XC 方法的时候,就会去看是否有 method_missing 实现,这该例中实现了,就继续执行下去,相当于 Roman.XC 是动态生成的一个方法,可以近似地说,我们写的代码会在运行时生成代码并执行,这就是元编程。就像生成罗马数字的阿拉伯数字一样,传统的编程方式,要实现的方法数量几乎是无限的,而元编程能让我们动态的生成方法,适当使用,非常优雅。

Ruby 是动态类型的语言,因此能做到上述功能一点不奇怪,而 Scala 是静态类型的,要让他动态地生成方法似乎不太容易,不过从 Scala 2.10 引入的 Dynamic 毫无困难地做到了与 method_missing 几乎一致的功能,下面是 Roman 的 Scala 版:

上述代码通过 import scala.language.dynamics 开启 Dynamic 功能,然后让需要实现动态方法的 Roman 继承 Dynamic trait 以获得 Dynamic 特性,最关键的是这一行:

它接受一个 String 参数并返回 Int,这正是调用的时候代码 Roman.VI 所期望的,对比 Ruby 一个 method_missing 方法,Scala 共提供了四个方法:

- applyDynamic
- applyDynamicNamed
- selectDynamic
- updateDynamic

它们各自对应的用法可以参考 Scala 2.10: class OhMy extends Dynamic !

Scala Dynamic 和 Ruby method_missing 看起来差不多,不同的是 Ruby 少了一点点代码,而 Scala 多了些编译器检查。对于喜爱 Scala 或者在工作中需要编写 Scala 的码农来说,多了一个本来只有在 Ruby 那里才听到的特性,那是再好的消息不过了。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/08/31/scala-metaprogramming-dynamic/

Categories: 编程语言 Tags: , ,

dig与dns基本理论——解析和缓存

August 4th, 2014 2 comments

DNS(Domain Name System,域名系统)也许是我们在网络中最常用到的服务,它把容易记住的域名,如 www.google.com 翻译成人类不易记住的IP地址,如 173.194.127.132,以便在方便人类输入的基础上让计算机能够精确地在网络中找到目标机器。dig(Domain Information Groper)是一个比较常用的 dns 分析及调试工具,一般 Linux 和 Mac 都已默认安装。本文将基于 dig 的使用解释 dns 的基本知识。

解析

如果我们想知道 www.juvenxu.com 被解析到什么 IP 地址,可以用如下命令(我知道可以用ping,但本文主要围绕 dig 讲述):

命令行参数 +noall 和 +answer 只是为了输出更精简。

暂时忽略第3、4行,着重看最后一行,它分为5部分,第1部分是我们查询的域名;第2部分是缓存失效时间ttl(秒),暂且不关心;第3部分是 IN,表示类别 Internet,一般也都是这个类别;第4部分很重要,表示这行记录的种类(type),该行种类是 A(Address),说明这是一个A记录,描述域名对应的地址;第5部分好理解,就是地址了。

我们输入了 dig 命令和想查询的域名,得到了结果 IP 地址,本地的 dig 命令自然不可能知道世界上所有域名对应的 IP 地址(或者说A记录),那是谁告诉它的呢?

加上 +stats 让 dig 打印基本的统计信息,其中第7行告诉我们 SERVER 是 192.168.1.1,53是DNS使用的 UDP(有时候可以是TCP)端口。dig 默认使用 /etc/resolv.conf 配置的域名服务器:

我们可以指定 dig 使用其他域名服务器帮我们解析域名,例如用 8.8.8.8:

这条命令中的 @8.8.8.8 表示使用 8.8.8.8 解析域名。

有些互联网服务部署在全球各个地方,这个时候服务提供方就希望中国的用户访问中国机房IP,美国的用户访问美国机房IP,这个时候两个地方的域名服务器返回的结果就会不一样,我们可以用类似上述的命令验证这一点。读者可以参考国内外DNS服务器地址列表,用各个国家的服务器 dig 一下知名站点如 google.com 试试。

不过即便如此,192.168.1.1 也好,8.8.8.8 也好,都不可能知道全球所有的域名信息,事实上,全世界数以亿次的域名信息分布在无数的域名服务器上,我们可以看一下维护 www.juvenxu.com 域名信息的服务器是什么:

我们在 dig www.juvenxu.com 后面加了 ns 参数,表示希望得到域名服务器信息(而不是默认的A记录),不过这次查询我们没有得到任何结果。没有关系,这往往表示该域名的上一级域名(juvenxu.com)的域名服务器管理这个子域名,因此我们 dig juvenxu.com:

结果有两行记录,和 dig A记录结果一样,各自分成5部分,第1,2,3部分和A记录结果含义一致,第4部分表示表示种类(type)是 NS(Name Server,域名服务器),第5部分自然就是域名服务器的地址了。

现在我们知道 dig 请求域名服务器 192.168.1.1 juvenxu.com 的域名信息,而 ns13.ixwebhosting.com 和 ns14.ixwebhosting.com 保存了 juvenxu.com 的域名信息,那么 192.168.1.1 怎么知道如何联系上 ns13.ixwebhosting.com 或 ns14.ixwebhosting.com 呢?这就涉及到 DNS 的分布式架构了。

整个 DNS 服务是按照树形的结构分布的,最顶级是根域名,根域名服务器保存了所有定级域名服务器的信息,顶级域名包括 com, net, edu, org, cn 等等,各个定级域名服务器又包含了下一级的域名服务器信息,如 com 域名服务器包含了 microsoft.com, amazon.com 这些域的服务器信息,这样一层层往下:

dns_structure

我们可以使用 dig 的 +trace 选项来仔细查看这个过程:

dig 直接请求的 192.168.1.1 并不知道 juvenxu.com 的 A记录是什么,它就去让 dig 去问根域名服务器,根域名服务器也不知道,让 dig 去问 com 的域名服务器,com 域名服务器也不知道,就让 dig 去问 ns13.ixwebhosting.com 和 ns14.ixwebhosting.com,而我们知道,这两个域名服务器保存了 juvenxu.com 的 A记录。这种一问一答的解析方式,叫做迭代解析(Iterative resolution)。这种方式下,每个域名服务器只是给出它所知道的最佳答案。

另外一种解析方式是递归解析(recursive resolution),如果我们不给 dig 加上 +trace 参数,dig 就会给域名服务器 192.168.1.1 一个递归解析请求,等于告诉是让 192.168.1.1 直接负责去解析到最终答案,然后直接返回给 dig。通常情况下客户端(dig)对域名服务器发起的递归解析请求,而域名服务器对其他域名服务器的请求(如 192.168.1.1 对根域名服务器)是迭代解析请求。

缓存

如果世界上所有网络设备(PC,服务器,手机,平板电脑……)的每次网络请求都要访问 DNS 根服务器,那这个量就实在是太大了,根服务器肯定受不了。考虑到一个域名和其IP的对应关系其实不会太常变化,因此在各个域名服务器缓存之前请求过的域名A记录就是个不错的策略。基于此,对于一个域名如 douban.com 来说,域名服务器分成两类,一类是切切实实保存该域名信息的服务器,称为权威域名服务器(Authoritative Name Server),其余的仅仅是缓存其域名信息的服务器,就是缓存域名服务器。

例如,如果我很久以来第一次 dig douban.com,就会得到类似这样的输出:

这段输出比较长,我们重点看15-17行,即 AUTHORITY SECTION 部分,由于我们长久以来第一次 dig douban.com,因此就近的域名服务器还没有 192.168.1.1 还没有缓存 douban.com 的 A记录,因此它会解析到 douban.com 的权威域名服务器,再返回给我们结果,这个结果是最新的,是“权威”的。

我们回头在看13行A记录的第二部分 ttl,即缓存失效时间,值是35,表示该缓存35秒后失效,换言之,35秒内我们再请求 douban.com 的话,192.168.1.1 会直接返回缓存的A记录:

这时就没有 AUTHORITY SECTION 了,说明结果来自缓存域名服务器,而不是权威域名服务器。

如果权威域名服务器改变了A记录信息,而缓存服务器还没有过期,我们怎么去主动了解最新的A记录呢?或者说,怎么主动去权威服务器查找A记录信息呢?简单,解析的时候指定域名服务器即可:

缓存机制能够减轻域名服务器尤其是根域名服务器和定级域名服务器的压力,让域名解析过程更快,当然和所有缓存一样,副作用也是有的,当权威服务器修改域名信息的时候,缓存服务器的信息就过时了,要等TTL失效后才能解析到最新正确的信息。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/08/04/dig-and-basic-dns-resolving-and-cache/

Categories: DevOps Tags: ,

基于Cucumber和Scala的验收测试

July 8th, 2014 No comments

Cucumber是一款富有盛名的 BDD(行为驱动开发)框架,特别是在 Ruby 社区,它有着很高的知名度,然而大家可能不了解的是,Cucumber 通过 cucumber-jvm 很好地支持了 jvm 平台上的各类语言,包括 Java、Clojure、Scala、Groovy 等等。我工作中的产品代码是 Java,直接用 Ruby 测试 Java 应用程序有很多不便(比如用调用 Java 客户端,使用 Java 工具类库),而用 Java/Scala 等语言测试就比较方便,实际中我们使用的是 Scala,主要是它更简洁,这也是一个我们锻炼学习新兴语言的机会。本文介绍如何搭建一套基于 Cucumber 和 Scala 的测试环境。

首先,基本的 POM 依赖如下(这里使用 Maven,如果你用 Gradle 或者 sbt,只能自行搜索方案了):

上述三个依赖分别是 cucumber-jvm 的核心类库、scala支持库、以及junit集成库,集成junit主要是方便IDE和Maven通过JUnit运行Cucumber。除此之外,你当然还需要Scala依赖,包括 scala-compiler、scala-library和我们后面会用到的 scalatest:

为了让Maven能够编译Scala,还需要配置 maven-scala-plugin:

基本的项目配置就绪,接下来编写我的 cucumber feature 文件,sample.feature,该文件位于项目 src/test/resources/ 下的 features/ 子目录下,为了方便组织 feature 文件,我一般把所有 feature 文件都放在这里,由于如何写 feature 不是本文重点,例子就很简单了:

该 feature 对应了两个 StepDef,我用 Scala 编写,位于 src/test/scala/目录,具体类为 com.juvenxu.cucumber.scala.steps.SampleStepDefs

* 该例中 ScalaDsl 定义了 Given, When, Then 这些 DSL,When 和 Then 的参数是一个用来匹配 Step 的正则表达式,Scala 的三引号用起来更方便,再也不用给 String 中的引号转义了。
* trait EN 定义了 DSL 的英文表达,如果你想写“假设”、“当”、“那么”等中文,可以用 trait ZH_CN。
* ShouldMatchers 是 scalatest 的东西,代码中的 result should be(arg0) 就是基于 scalatest 的断言,对比 Java 的测试框架,表述力更强大。

最后我们还需要一点简单的代码来驱动 Cucumber,它也用 Scala 编写,位于 src/test/scala/目录,具体类为 com.juvenxu.cucumber.scala.RunCukesTest

* @RunWith 就是告诉 JUnit 用我的 Cucumber 这个 Runner 来跑测试。
* @Cucumber.Options 中的 format 指定生成的测试报告,后续可以 target/ 目录下找到。
* @Cucumber.Options 中的 features 指定 features 文件的路径,该例中是 classpath 下的 features 目录下。

至此,整个项目的目录结构是这样的:

在命令行运行 mvn clean install 能看到如下输出:

关于和 IDE 的集成 IntelliJ IDEA 做得很棒,只需要安装一些插件就可以了,需要安装的插件包括:Scala,Cucumber for Java,以及 Cucumber for Scala,效果如图:

cukes_features

cukes_feature_sample

实际工作中使用 Cucumber 的体会,主要还是它会迫使我们把测试写清楚,用自然语言地方式描述测试,就必然要求你把测试想得比较清楚,对比我们在其他代码库中随处可见的各种莫名其妙的 TestCase,Cucumber 测试的可维护性高很多,通过测试就能快速了解一个系统的各类功能。技术上来说,它并不比纯的 JUnit TestCase 高明多少。

最后,我和江平兄一起翻译的《Cucumber:行为驱动开发指南》是本不错的学习 Cucumber 的参考书籍,虽然书中的代码都是 Ruby 的,但思路完全是一致的,特此推荐。

cucumber_book


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/07/08/acceptance-test-with-cucumber-and-scala/

Categories: 测试 Tags: , , , ,

ScrumGathering上海2014

June 8th, 2014 4 comments

6月5-7日是一年一度的、以敏捷为主题的ScrumGathering大会,今年我作为讲师身份参加,除了能够免费参会这一好处之外,还可以总结下自己最近一年的思考,另外还能见到不少许久未见的朋友。然而令人意外的是,这次参会得以有机会和自己一直很崇敬的Dave Thomas详谈,这远远超出了我对大会的期望。

如果你参加过以敏捷为主题的大会,你就会发现很多热度很高的词:Agile、转型、Scrum、CSM、Lean、TDD…… 这些名字都是领域的专家发明的,专家们信誓旦旦地告诉你,他们能解决你面临的问题。但是不知道大家想过没有,在软件开发这个行业,解决问题最终基本都会落实到写代码上,然后在大会上,能看到代码的地方聊聊无几,这是不是有点问题?

第一眼看到 Dave 的时候,他腿上放着个Macbook Air在休息区和朋友申健在聊天,一开始没留意,后来有人告诉我这是 Dave,我略吃惊,忙求引荐。Say Hi 之后立刻说我看过 The Pragmatic Programmer,喜欢 Pragmatic BookShelf …… 一堆真心的“奉承”话,我的确曾经有一段时间把 The Pragmatic Programmer 一书放床头,每晚翻翻,细细体会,这书也是我看过的最好的IT书籍之一。客气完毕之后,我问了他对 Reactive思想和 Akka 的看法,Dave肯定了我的想法,认为这是编程模型发展的方向(虽然他也说了,这玩意儿几十年前就有了),然后他就开始high了,开始给我推荐 Elixir,我说这语言我没听说过啊,哦,记得好像有本书,当时以为是一个什么类库呢。Dave于是就打开Mac给我写Elixir代码,尼玛,写代码!在一个以敏捷为主题的大会上,一墙之隔的会场内,有讲师在讲酷炫的名词呢。Dave顺手给我写一段Elixir代码,类似于Java 8 Stream API的东西,基于消息传递,看起来非常简洁,很像Bash。我们还聊了Pragmatic Bookshelf,说起《七周七语言》,说起《番茄工作法图解》,Dave还透露了他们计划出版的《more seven languages in seven weeks》,里面会介绍 lua,elixir,和其他另外5种我都没听说过的语言。

Juven_and_dave

大会上我的话题是”务实地拥抱极限编程”,大意摘要如下:

1. 极限编程作为一个比较流行的敏捷方法,大家都比较熟悉了,尤其是TDD、持续集成、结对编程、重构能具体实践。
2. 但是大家是否忘记了极限编程的价值(Values)?
3. 极限编程推崇的价值是 Feedback, Communication, Simplicity, Courage, Respect
4. 极限编程实践都有其对应的价值,例如TDD就对应了Feedback
5. 我们不应该只盯着实践,我们应该思考价值,并总结能实实在在解决自己问题的实践
6. 演讲用实际的例子介绍我们怎么遵循价值、总结实践处理具体问题。


(slideshare被墙,点此下载pdf)

敏捷要解决实际问题,现有实践能解决问题那当然就直接用,现有实践搞不定的时候,我们需要寻找其他办法,寻找其他办法的过程中,也要有指导,解决完问题了,也得有总结,这个时候,极限编程的Values就是很高的指导及方向。

有幸我把自己的topic提前和Dave分享并得到了他的肯定,并且后续他也走进会场听了我的中文演讲,据我观察这是他在整个大会中听的唯一的一场演讲,这给我受宠若惊的感觉。Dave的演讲是《敏捷已死,但敏捷精神长存》,核心思想还是围绕了Pragmatic这个词,他的意思说,大家别听一像火鸡一般自以为是的专家兜售玄乎的方法,大家要自己实践、自己思考、自己总结,这才是敏捷精神,如果大家只做专家们所兜售的敏捷,那敏捷就死了。另外他介绍自己的时候,说自己是个Programmer,而且到现在还是每天在写代码。Dave是敏捷宣言的作者之一,反观我们周围,那么多说敏捷的人,多久没写过一行代码了?

我认真反思了一下自己,很多时候我宁愿看书而不愿拿电脑敲一个DEMO,觉得弄明白理论后,代码自然写起来很简单,然后实际会发现写代码很慢,很吃力,看似简单的理论,看似理解了,写代码的时候还是磕磕绊绊。另外,随着工作年限的增加,很多时候解决问题更多是协调沟通而不是写代码,这样练习的机会就渐渐变少,看来需要刻意弥补。

有时候我会问自己,为什么做程序员,很大一个原因就是一开始编程所感受到那种神奇,而现在,有多久没那种感受了?生活虽然不易,还是不能忘了初心。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/06/08/scrumgathering-shanghai-2014/

Categories: 敏捷 Tags: , ,

不做运维,不懂日志——日志编码

April 22nd, 2014 1 comment

一个成功的软件,全力开发的时间可能占其整个生命周期的1/4还不到,软件发布后要运维(Operation),运维的视角和开发的视角是很不一样的,但是有一点,运维的数据能反哺开发,同时,开发的时候也得考虑可运维性,其中非常重要的一点是日志,没有日志,运维就瞎了大半。怎么写日志,就得从运维的需求来看了,通常会有以下一些常见的场景(已典型互联网应用为例):

1. 访问来源,包括访问量,访问者数据,如用户名、IP等等。
2. 基于上一点细化,访问的接口,读、写、删……
3. 软件系统内部的核心链路,比如我这有个系统要在中美直接同步文件,那同步的情况运维的时候就要掌握。
4. 软件系统对其他所依赖系统的访问情况,比如我这个系统依赖一个分布式缓存,那访问缓存的量、是否超时等情况需要了解。
5. 系统异常,比如磁盘满了。

记录这些信息的目的大抵有:帮助分析系统容量方便扩容;在系统某些部分工作不正常的时候及早发现;发生严重故障后方面定位问题原因。认识到这些需求后,下一步就是怎么实现的问题了。

前面提到的5点,有些可以通过抛异常实现,例如访问分布式缓存超时,有些则显然不是异常,例如就是正常的缓存访问。我觉得可以用一种统一、规范的方式记录,这种方法就是打码。我记得以前用Windows 98/2000的时候,经常会遇到蓝屏,蓝屏上会有一堆我看不懂的英文,并且总是伴随着一个错误码。

bluescreen

虽然我看这玩意儿没一点好心情,但我相信微软的工程师肯定能从那个奇怪的状态码上判断出是哪里出了问题,硬盘坏道?光驱卡死?诸如此类……其实类似的做法数据库也用,比如MySQL

用统一的代码表示错误(也可以表示正常但核心的业务点)最大的好处就是便于搜索、统计和分析,在动辄数以万行记的日志文件中寻找感兴趣的信息,一页一页翻看是不现实的,稍微做过点运维的必然会用上 grep,awk,wc 等工具,这个时候如果信息都有代码标识,那真是再方便不过了!例如,我用代码 FS_DOWN_200 表示对系统的正常下载访问,日志是写在 monitor.log 文件中的,我就可以使用一行shell统计4月22号5点到6点之间的正常访问量:

具体每条日志记录什么,那就是更详细的了,基本就是时间、日志编码、额外的有用信息,如:

除了时间和日志编码外,还有响应时间(216ms)和具体访问的文件名。

当然如果你有日志监控和分析系统就更棒了!你就可以在系统中录入关键字监控,比如每分钟统计次数,然后看一天、一周的访问量趋势图。进一步的,如果这个量发生异常,让系统发出报警。如果没有关键字,从海量日志中分析纷繁复杂形态各异的信息,再监控,是非常难的一件事情。

为什么要把日志代码设计成 FS_DOWN_200 这样子的,下面稍微解释下,这个代码分成三段:
1. FS: 表示我们的系统,这是最高的级别,公司中有很多系统,那各自定义自己的标识。
2. DOWN: 表示我们系统中的一个核心业务点或者对其他依赖系统的访问,还可以是UP(上传),SYNC(同步),或者TAIR(对缓存系统访问)。
3. 200: 具体健康码,参考HTTP规范,200表示OK,其他包括404(不存在),504(超时)等等。

有了这些代码,再结合公司的监控系统,我们做统计分析就非常方便了,每天多少下载、多少上传、多少成功、多少失败、对其他依赖系统访问多少量、多少失败率,一目了然。进一步的加上监控,当某些值突然发生变化,比如下载量/上传量暴跌、访问其他系统依赖超时大量增多,就能及时响应。

日志对于运维实在太重要了,而如果不接触运维,又怎能理解其真正的需求,因此我说,不做运维,不懂日志

本实践受 @linux_chinaRelease It! 17.4 小节启发,表示感谢。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/04/22/log-with-code/

Categories: DevOps Tags: ,