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

记一次关于TIME_WAIT TCP连接数的性能优化

January 19th, 2014 8 comments

一天下午3点多,我们的OPS打电话给我(你懂的,OPS打电话过来不会是请你喝咖啡),我连忙接起电话,电话那头的声音很急迫:“content集群有报警,CPU负载已经到了50多,你快过来一起看看”,我挂了电话连忙赶过去,OPS坐在另一幢楼的同一层,好在两幢楼是相连的,过去很方便。边走我心里边嘀咕,content集群实际上是一个反向代理,上面配置了上百条apache http url rewrite 规则,每条规则的作用就是把一个url路径转换成另外一个url路径,发送到某个后端的集群上,然后把返回结果转出去,几乎没有计算量,不应该有高负载啊。 

来到OPS座位上,他已经在往content集群里加机器了,这个集群为网站提供图片读取服务,负载这么高,服务肯定不稳定了,因此当务之急肯定是让服务恢复稳定。对任何线上故障来说,恢复服务肯定是第一优先级,问题排查的优先级排在后面。大概5分钟之后,新加的20%机器开始对外服务了,幸运的是,效果立竿见影,集群负载立刻降到了5以下,我输入了几条url测试了下,服务已经恢复稳定了,我俩都松了口气,这次故障没有造成长时间大范围的影响。

接下来我们开始分析造成CPU高负载的原因,这个集群是有监控的,报警也是监控系统发出的,于是我们一个个看监控项:CPU、内存、磁盘消耗、网卡流量、QPS(每秒处理请求数)、响应时间、TCP连接状态等等……然后我们发现了几个问题:
  1. QPS和响应时间没有监控,妈的,这意味着我们无法快速得到访问量和服务稳定性的直接数据。
  2. 从网卡流量我们发现,下午3点左右是访问高峰期,这很可能是诱发CPU负载过高的外因。
  3. TCP连接数中,TIME_WAIT连接数异常地高,扩容前最高到了30k多!扩容后降到了27k以下。

关于第一条,我给自己记了个任务,待当前的问题处理完毕之后,花点时间配合OPS把这个监控完善起来。

第三条关于TIME_WAIT的数据非常可疑,可惜的我我大学学习的那些网络知识早还给老师了,于是只能现补了,开Google查资料,花了点时间了解了TCP连接的TIME_WAIT是怎么回事。TCP连接建立起来传输数据,这个时候对两边来说,连接的状态是ESTABLISHED,数据传输完毕后,连接的其中一方会主动发起断开连接,断开连接的整个过程是四次握手,握手完毕后,断开连接发起方会进入TIME_WAIT的状态。默认情况下,TIME_WAIT的连接是不会被操作系统回收的,只有到了CLOSED状态后,操作系统才会回收,而且默认情况TIME_WAIT连接会持续两个MSL时间,然后通过如下命令我发现这个时间是60s:

因此如果每一个请求都新建一个连接,服务完之后断开连接,一分钟内就会积累大量的TIME_WAIT连接。监控数据显示,单台机器在比较高峰时刻ESTABLISHED数为1.5k的时候,TIME_WAIT连接数接近30k。正是当TCP连接数超过这30k的时候,系统变得不稳定,CPU负载变得很高,为什么会连接数超过30k会使得系统不稳定,我还不清楚,但我需要让TIME_WAIT连接数降下来。
 
第一反应的解决办法是使用TCP的keep-alive模式,让出问题的content集群在访问后端服务器的时候,不要每次都新建连接并断开连接,而是采用keep-alive长连接模式,content集群和后端服务器集群都是内部机器,IP相对固定,长连接完全合情合理,重用连接不仅能够节省建立连接和断开连接的消耗,而且必然能大大降低TIME_WAIT连接的数量。我先看了后端服务器,都是支持keep-alive的,现在就看content怎么主动开启keep-alive请求了。
翻开配置看了下,发现content集群做反响代理是用了apache的mod_rewrite模块,具体配置大概是这样的:

意思就是说把符合前面正则表达式的请求,转化url之后发到back-end-server,而由于历史原因,这样的配置有100条左右。那怎么配置mod_rewrite模块让它支持keep-alive呢?我捜了半天的Google,把mod_rewrite模块的文档前前后后翻了好几遍,得到的结论是:mod_rewrite不支持keep-alive。

这时候该怎么办?综合各方面意见,有三种比较直接的解决方案:
  1. 啥都不干,直接加机器让平均每台的TIME_WAIT达不到30k,简单粗暴,但显然会造成很大的机器浪费。
  2. 用Nginx替换Apache,很多人都这么和我推荐,但是,抛开Nginx是否合适这个场景不说(我还不太了解),光迁移这100条正则表达式转发规则就要命,更何况还有一大堆其他配置,成本和风险都很高的。
  3. 让操作系统主动回收TIME_WAIT连接,可以通过配置系统的tcp_tw_recycle和tcp_tw_reuse来实现,网上有一大堆这方面的资料。这么做也有缺点,运维成本增加了,每台机器都得去做这个特殊配置,重启后还会丢失,会比较麻烦,我总觉得这是治标不治本的方法。
抓破脑门想不到更好的办法,我就在扩容20%机器的基础上,再调节了tcp_tw_reuse,我没有调节tcp_tw_recycle是因为查阅文档发现当后端有负载均衡的时候,开启它有潜在的风险,此事就此告一段落,然后我始终放不下。
 
一个月之后,由于同事遇到了类似的问题,激起了我想再次优化TIME_WAIT连接数的欲望。又花了半天看了很多文档及邮件列表之后,我想,作为一个完全合理的需求,为什么mod_rewrite不支持keep-alive呢?而与之功能类似(但不支持复杂正则表达式匹配)的mod_proxy模块,是能很好的支持keep-alive的。从apache的用户邮件列表的一篇帖子中我发现,keep-alive功能是被有意地从2.2版本开始移除的,而且apache的维护者Igor Galić还说了这么一句:“你为什么要用mod_rewrite做代理?”,莫非我们之前的用法都是错误的?忽然间,思路有了,mod_rewrite的强项是url重写,mod_proxy的强项是代理,那就把代理交给mod_proxy,mod_rewrite只管url域名后面相对路径的重写,不就可以了吗?
 
尝试了一下,得到下面的配置:

对比之前的配置,现在的RewriteRule用了PT模式而不是P模式,P模式是强制代理网络连接,而PT则只是做路径转换,另外代理后的路径以/fs1开头。在此基础上,这里又配置了ProxyPass(mod_proxy),所有/fs1路径开头的请求,转到back-end-server上,并开启keep-alive。拿几条规则做了测试之后,发现TIME_WAIT数有一些下降,于是我在集群中找了一台机器,调整了几条访问量比较大的规则,放线上观察。一天之后,没有收到任何问题,我就把全线的配置都更新了,效果非常明显,之前ESTABLISHED连接1.5k的时候,TIME_WAIT连接数近30k,现在由于keep-alive的开启,TIME_WAIT连接数最多只到6k左右,降到了原来的1/5,也几乎不再可能成为系统的瓶颈。之后我们在类似的场景中发现,如此优化能降低系统响应时间,这也是好理解的,因此不用重复建立连接了,响应自然更快。
 
至此,短时间内我们基本不用考虑content集群的扩容了,因为这是集群的主要作用是反向代理,之前遇到了网络瓶颈现在得以解决,而其他系统资源如CPU、内存之类都还是比较富足的。考虑到网站发展比较迅猛,流量每个月都在上涨,而这个集群的规模比较大,因此这次优化实际上节省了不小数量的机器,那可是白花花的银子啊。
 
这是我第一次在机器量级比较大的系统上做性能优化,有几点可能已经被前人说烂了的总结:
  1. 系统出问题的时候,第一优先级的任务是恢复系统服务。
  2. 系统监控必须要完善,否则做优化的时候简直是抓瞎。
  3. 找到瓶颈,优化才有意义,否则基本是浪费精力。
  4. 计算机系统的基本功还是必须的,该例中就需要对TCP协议有了基本的理解。
  5. 虽然计算机越来越便宜,但是在该例的分布式的服务场景中,一堆便是数十台高性能的物理机,加上运维成本,加上性能差给用户体验带来的损害,其实性能差的代价非常昂贵。因此,设计系统、编写代码的时候,还是要想着节省计算资源。
Categories: 架构与性能 Tags: ,