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: ,

使用Hystrix提高系统可用性

March 19th, 2014 2 comments

今天稍微复杂点的互联网应用,服务端基本都是分布式的,大量的服务支撑起整个系统,服务之间也难免有大量的依赖关系,依赖都是通过网络连接起来。

hystrix-healthy

(图片来源:https://github.com/Netflix/Hystrix/wiki)

然而任何一个服务的可用性都不是 100% 的,网络亦是脆弱的。当我依赖的某个服务不可用的时候,我自身是否会被拖死?当网络不稳定的时候,我自身是否会被拖死?这些在单机环境下不太需要考虑的问题,在分布式环境下就不得不考虑了。假设我有5个依赖的服务,他们的可用性都是99.95%,即一年不可用时间约为4个多小时,那么是否意味着我的可用性最多就是 99.95% 的5次方,99.75%(近乎一天),再加上网络不稳定因素、依赖服务可能更多,可用性会更低。考虑到所依赖的服务必定会在某些时间不可用,考虑到网络必定会不稳定,我们应该怎么设计自身服务?即,怎么为出错设计?

Michael T. Nygard 在在精彩的《Release It!》一书中总结了很多提高系统可用性的模式,其中我认为非常重要的两条是:

  1. 使用超时
  2. 使用断路器

第一条,通过网络调用外部依赖服务的时候,都必须应该设置超时。在健康的情况下,一般局域往的一次远程调用在几十毫秒内就返回了,但是当网络拥堵的时候,或者所依赖服务不可用的时候,这个时间可能是好多秒,或者压根就僵死了。通常情况下,一次远程调用对应了一个线程或者进程,如果响应太慢,或者僵死了,那一个进程/线程,就被拖死,短时间内得不到释放,而进程/线程都对应了系统资源,这就等于说我自身服务资源会被耗尽,导致自身服务不可用。假设我的服务依赖于很多服务,其中一个非核心的依赖如果不可用,而且没有超时机制,那么这个非核心依赖就能拖死我的服务,尽管理论上即使没有它我在大部分情况还能健康运转的。

断路器其实我们大家都不陌生(你会换保险丝么?),如果你家没有断路器,当电流过载,或者短路的时候,电路不断开,电线就会升温,造成火灾,烧掉房子。有了断路器之后,电流过载的时候,保险丝就会首先烧掉,断开电路,不至于引起更大的灾难(只不过这个时候你得换保险丝)。

cb

当我们的服务访问某项依赖有大量超时的时候,再让新的请求去访问已经没有太大意义,那只会无谓的消耗现有资源。即使你已经设置超时1秒了,那明知依赖不可用的情况下再让更多的请求,比如100个,去访问这个依赖,也会导致100个线程1秒的资源浪费。这个时候,断路器就能帮助我们避免这种资源浪费,在自身服务和依赖之间放一个断路器,实时统计访问的状态,当访问超时或者失败达到某个阈值的时候(如50%请求超时,或者连续20次请失败),就打开断路器,那么后续的请求就直接返回失败,不至于浪费资源。断路器再根据一个时间间隔(如5分钟)尝试关闭断路器(或者更换保险丝),看依赖是否恢复服务了。

超时机制和断路器能够很好的保护我们的服务,不受依赖服务不可用的影响太大。然而具体实现这两个模式还是有一定的复杂度的,所幸 Netflix 开源的 Hystrix框架 帮我们大大简化了超时机制和断路器的实现。

hystrix-logo

先上POM依赖:

使用Hystrix,需要通过Command封装对远程依赖的调用:

然后在需要的时候调用这个Command:

上述是同步调用,当然如果业务逻辑允许且更追求性能,或许可以选择异步调用:

该例中,不论 dependencyService.call() 自身有没有超时机制(可能你会发现很多远程调用接口自身并没有给你提供超时机制),用 HystrixCommand 封装过后,超时是强制的,默认超时时间是1秒,当然你可以根据需要自己在构造函数中调节 Command 的超时时间,例如说2秒:

当Hystrix执行命令超时后,它会抛出如下的异常:

注意异常信息中包含“MyCommand timed-out and no fallback available.”,也就是说 Hystrix 执行命令超时或者失败之后,是会尝试去调用一个 fallback 的,这个 fallback 即一个备用方案,要为 HystrixCommand 提供 fallback,只要重写 protected String getFallback() 方法即可。

一般情况下,Hystrix 会为 Command 分配专门的线程池,池中的线程数量是固定的,这也是一个保护机制,假设你依赖很多个服务,你不希望对其中一个服务的调用消耗过多的线程以致于其他服务都没线程调用了。默认这个线程池的大小是10,即并发执行的命令最多只能有是个了,超过这个数量的调用就得排队,如果队伍太长了(默认超过5),Hystrix就立刻走 fallback 或者抛异常。

根据你的具体需要,你可能会想要调整某个Command的线程池大小,例如你对某个依赖的调用平均响应时间为200ms,而峰值的QPS是200,那么这个并发至少就是 0.2 x 200 = 40 (Little’s Law),考虑到一定的宽松度,这个线程池的大小设置为60可能比较合适:

说了这么多,还没提到Hystrix的断路器,其实对于使用者来说,断路器机制默认是启用的,但是编程接口默认几乎不需要关心这个,机制和前面讲的也差不多,Hystrix会统计命令调用,看其中失败的比例,默认当超过50%失败后,开启断路器,那之后一段时间的命令调用直接返回失败(或者走fallback),5秒之后,Hystrix再尝试关闭断路器,看看请求是否能正常响应。下面的几行Hystrix源码展示了它如何统计失败率的:

其中 failure 表示命令本身发生错误、success 自然不必说,timeout 是超时、threadPoolRejected 表示当线程池满后拒绝的命令调用、shortCircuited 表示断路器打开后拒绝的命令调用,semaphoreRejected 使用信号量机制(而不是线程池)拒绝的命令调用。

本文并不打算完整地介绍 Hystrix,这里只是介绍了为什么要用 Hystrix 以及使用它需要关心的一些基本核心概念,Hystrix 是 Netflix 的核心中间件,在保证他们系统可用性上起到了非常核心的作用,它还有更多的功能都在文档完整地介绍了:https://github.com/Netflix/Hystrix/wiki,其中最重要的而且本文没有介绍的,可能就是监控功能了,当系统足够复杂,相互依赖错综发杂的时候,快速定位到故障点,是运维非常关心的问题。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/03/19/improve-system-availability-with-hystrix/

Categories: 架构与性能 Tags: ,