Bowen's Blog

Respect My Authorita.

Essential Wget

| Comments

什么wget


wget是*nix下常用的下载文件或者测试web服务的工具, wget支持多种网络协议,但是从介绍看,wget更像是一个文件下载工具。

获取网页源码


wget默认会将网页或者文件直接下载保存,所以直接显示返回内容的话,可以用 -q关闭wget的输出,然后用-O - 将输出重定向到标准输出,然后用cat将内容打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~> wget echo.httpkit.com
--2015-02-10 11:01:50--  http://echo.httpkit.com/
Resolving echo.httpkit.com... 50.112.251.120
Connecting to echo.httpkit.com|50.112.251.120|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 374 [application/json]
Saving to: ‘index.html’

~> wget -qO - echo.httpkit.com | cat
......
 "headers": {
   "x-forwarded-for": "210.74.157.146",
   "host": "echo.httpkit.com",
   "user-agent": "Wget/1.14 (darwin12.5.0)",
   "accept": "*/*"
 },
 ......

文件下载


wget缺省会将url中最后的文件作为文件名并且将文件保存在本地,用下面的命令可以将文件保存到指定的目录,这点比curl要好。

1
~> wget -P /opt http://ergonlogic.com/files/boxes/debian-current.box

通过代理访问资源


有两种方式可以让wget时使用proxy,第一种是通过设置环境变量,http_proxy, https_proxy

1
~> export http_proxy="http://proxy:3128"

让proxy对一些地址无效。

1
~> export no_proxy="127.0.0.1,localhost.localdomain"

另外一种是在运行命令时直接指定使用的proxy或者不使用proxy。

1
2
~> wget  -e http_proxy=127.0.0.1:8080 www.baidu.com
~> wget --no-proxy www.baidu.com

同样是不使用proxy,curl的--noproxy是需要指定一个exclude的列表,但是wget的--no-proxy就是 针对当前访问资源,不使用proxy。

花式下载和断点续传


对于wildcard支持,wget的介绍感觉有点乱,提供的参数看上去不怎么友好,想实现和curl中一样的效果,可能要换个方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~> wget  http://iambowen.github.io/images/{1..3}.png
--2015-02-17 14:19:35--  http://iambowen.github.io/images/1.png
Resolving iambowen.github.io... 103.245.222.133
Connecting to iambowen.github.io|103.245.222.133|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 227632 (222K) [image/png]
Saving to: ‘1.png’
--2015-02-17 14:19:35--  http://iambowen.github.io/images/2.png
Reusing existing connection to iambowen.github.io:80.
HTTP request sent, awaiting response... 200 OK
Length: 350735 (343K) [image/png]
Saving to: ‘2.png’
.....
Saving to: ‘3.png’

获取headers信息


wget可以用-S参数来查看返回的header信息。

1
2
3
4
5
6
7
~> wget -qS www.baidu.com
 HTTP/1.1 200 OK
 Date: Tue, 10 Feb 2015 08:27:27 GMT
 Content-Type: text/html; charset=utf-8
 Transfer-Encoding: chunked
 Connection: Keep-Alive
 ......

返回redirection信息


不同于curl,wget缺省就是follow redirections。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~> wget -qS www.google.com
 HTTP/1.1 302 Found
 Location: http://www.google.com.hk/url?sa=p&hl=zh-CN&pref=hkredirect&pval=yes&q=http://www.google.com.hk/%3Fgws_rd%3Dcr&ust=1423559992840904&usg=AFQjCNFkDUqlbUFFIcVDOEXMELBQnEsZIA
 Cache-Control: private
 Content-Type: text/html; charset=UTF-8
 ......
 HTTP/1.1 302 Found
 Location: http://www.google.com.hk/?gws_rd=cr
 Cache-Control: private
 ......
 HTTP/1.1 200 OK
 Date: Tue, 10 Feb 2015 09:19:23 GMT
 Expires: -1
 ......

加入验证的请求


1
~> wget http://user:password@echo.httpkit.com?queryString

伪装user agent的请求


1
2
3
4
5
6
~> wget --user-agent="Mozilla: Gay" -qO- echo.httpkit.com | cat
"headers": {
  "host": "echo.httpkit.com",
  "user-agent": "Mozilla: Gay",
  "accept": "*/*"
}

-U的作用和--user-agent一样, wget的option parse是用getopt实现,所以可以理解-U的存在。

request with HTTP header


使用--header=String去添加header,可以在一个请求中设置多次。

1
2
3
4
5
6
7
8
9
10
11
~> wget --header="Cartman: is bitch"  --header="JB: stands for Justin Bieber" -qO- echo.httpkit.com | cat
.....
 "headers": {
   "x-forwarded-for": "210.74.157.146",
   "host": "echo.httpkit.com",
   "user-agent": "Wget/1.14 (darwin12.5.0)",
   "accept": "*/*",
   "cartman": "is bitch",
   "jb": "stands for Justin Bieber"
 },
.....

HTTP 请求


wget支持自定义HTTP请求类型,可以用--method=去指定HTTP请求的方法,注意,比较老的版本中没有提供这项功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~> wget --method="PUT" --header='Content-Type: application/json' --body-data="firstName=Kris&lastName=Jordan" echo.httpkit.com
{
"method": "PUT",
......
"headers": {
  "x-forwarded-for": "210.74.157.146",
  "host": "echo.httpkit.com",
  "user-agent": "Wget/1.16.1 (darwin14.0.0)",
  "accept": "*/*",
  "accept-encoding": "identity",
  "content-type": "application/json",
  "content-length": "30"
},
"body": "firstName=Kris&lastName=Jordan",
......
}

如果是用POST方法则简单了许多,直接指定请求的body string就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~> wget --header='Content-Type: application/json' --post-data="firstName=Kris&lastName=Jordan" -qO- echo.httpkit.com | cat
{
"method": "POST",
......
"headers": {
  "x-forwarded-for": "210.74.157.146",
  "host": "echo.httpkit.com",
  "user-agent": "Wget/1.16.1 (darwin14.0.0)",
  "accept": "*/*",
  "accept-encoding": "identity",
  "content-type": "application/json",
  "content-length": "30"
},
"body": "firstName=Kris&lastName=Jordan",
......
}

组装body的方式比curl要稍微简单些,但是不太直观,不是很喜欢。

处理cookies


保存cookie到本地

1
2
3
4
5
6
7
8
~> wget --save-cookies cookies.txt -q www.baidu.com
~> less cookies.txt
# HTTP cookie file.
# Generated by Wget on 2015-02-17 16:05:04.
# Edit at your own risk.

.baidu.com      TRUE    /       FALSE   3571643951      BAIDUPSID       3603BF98B1056E609FE72D7D976C0235
.baidu.com      TRUE    /       FALSE   3571643951      BAIDUID 3603BF98B1056E609FE72D7D976C0235:FG=1

请求时携带cookie

1
~> wget --load-cookies cookies.txt  -q www.baidu.com

除此之外,还可以在请求时的header中添加cookie信息。

1
wget --no-cookies --header "Cookie: name=value"

FYI


wget还有一些其他的黑科技,比如可以下载整个网站之类的,不过平常应该不太会用到。 Anyway,还是觉得curl要比wget好用些-_-!

Apache Rewrite Rules

| Comments

什么是Apache


Apache是一款开源的HTTP服务器,长期占据服务器市场第一名的位置。

什么是URL rewriting


有时候,为了让URL对用户友好,方便记忆,或是SEO优化,我们会使用更加简单的URL,比如http://example.com/help.html 但是实际开发人员命名的静态html文件名为help_2014_12_12.html,在这种情况下,就需要重写URL,让服务器 返回正确的页面(只是个例子),亦或是原来的页面更换了新的域名,需要告诉搜索引擎或者浏览器新的地址,都需要 重写URL。URL重写后,会返回给浏览器重写后的链接以及3xx的返回码,然后浏览器会请求重写后链接的内容。

301 和 302的区别


  • 301: 永久性转移(Permanently Moved)
  • 302: 暂时性转移(Temporarily Moved)

从效果上来讲它们都会造成跳转,区别在于SEO。如果是301跳转,那么搜索引擎将A页面的Pagerank导入 到跳转后的B页面,搜索引擎以后会忽略A页面,只在搜索结果中显示B页面。如果是302跳转,搜索引擎会保留A页面 的PR,但是显示的内容可能B页面的。

Apache rewrite module


mod_rewrite是Apache中提供得基于规则的rewriting引擎,加载模块后重启apache就可以使用了。

1
2
3
4
5
6
7
8
9
10
11
vagrant@vagrant-ubuntu-trusty-64:~$ apachectl -t -D DUMP_MODULES | grep rewrite
# 如果没有加载
vagrant@vagrant-ubuntu-trusty-64:~$ a2enmod rewrite
Enabling module rewrite.
Could not create /etc/apache2/mods-enabled/rewrite.load: Permission denied
vagrant@vagrant-ubuntu-trusty-64:~$ sudo a2enmod rewrite
Enabling module rewrite.
To activate the new configuration, you need to run:
service apache2 restart
vagrant@vagrant-ubuntu-trusty-64:~$ less /etc/apache2/mods-enabled/rewrite.load
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so

http.conf文件中可以引用具体的rewrite规则文件,如下

1
2
3
4
RewriteEngine on
RewriteLog /var/log/www/server.rewrite.log
RewriteLogLevel 1
Include /web/conf/httpd/conf.d/rewrites.conf

根据需求将rewrite的规则写入rewrites.conf中即可。

rewrite配置文件的写法

基本上来说,重写URL就是在􏰭􏰆􏰖􏰉􏰃􏰏􏰆􏰓􏰊􏰕􏰌RewriteCond下,执行自定义的RewriteRule,通过Server Variables 以及正则等匹配的方式,返回结果以及正确的返回码。

域名的转换

1
2
RewriteCond  %{HTTP_HOST}   ^www\.example\.com$  [NC]
RewriteRule  ^(.*)$         http://www.example2.com/$1  [R=301,L]

第一行是触发URL重写的条件,Flag NC 表示匹配时无视大小写,第二行表示在前一个条件满足时重写URL, 同时返回301, L表示本次重写URL的行为结束。

临时更改页面地址

1
RewriteRule ^page.html$   new_page.html   [R=302,L]

更改 Query String

1
2
3
RewriteCond %{QUERY_STRING} a=something
RewriteCond %{QUERY_STRING} b=(else1|else2)
RewriteRule ^/some_query_here$ http://www.example3.com/redirect? [R=301,L]

在Query String匹配 “a=something"的前提下,如果后面的Query String匹配b=else1或者b=else2 则重写URL,?表示截断URL。

最近在做一张卡的时候,要重写的URL类似

1
http://www.example.com/#category=Water Ball

如果直接将URL写在RewriteRule里面,Apache缺省会将URL中的特殊字符转义,比如会变成

1
http://www.example.com/%23category=Water%20Ball

要防止Apache将其转义,可以在Flag中加NE,同时注意Flag之间不能有空格。

1
2
RewriteCond %{QUERY_STRING} b=something
RewriteRule ^/some_query_here$ http://www.example.com/#category=Walter\ Ball? [R=301,NE,L]

体会

Apache的URL重写规则感觉还是比较简单,而且很强大。

references

  1. rewriting for beginners
  2. rewrite cheat sheet

Dimensions of Test

| Comments

前几周给一个客户介绍了关于持续交付的内容,他们特别关注概念以及细节,当时我在介绍自动化测试的时候, 分类比较简单,感觉回答的不是很好,于是重新整理下思路,总结成文。

软件测试的定义


对于软件测试的定义不尽相同,其经典定义是:在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。

软件测试的分类


测试的分类很多,可以按照不同的角度去划分,比如

  1. 功能/非功能测试
  2. 静态/动态测试(是否执行程序)
  3. 白盒/黑盒/灰盒测试(是否关心软件内部结构和具体实现)
  4. 按照软件开发过程划分

我自己更倾向于按照软件开发的过程去划分,同时,将测试的范围扩大到软件的整个生命周期,不止是开发测试,还要包括部署、产品的监控以及对基础架构的测试。如此,则有以下(不限于)分类:

  • Unit Testing
  • Integration Testing
  • Functional Testing
  • Smoke Testing
  • E2E Testing
  • Regression Testing
  • Acceptance Testing
  • Exploratory Testing
  • Performance Testing
  • Infrastructure Testing

以上除接受测试和探索性测试之外,都应当是自动化的。

Test Pyramid(软件测试金字塔)


测试金字塔是Mike Cohn提出的概念,意思是测试从上到下-功能性测试到单元测试,数量由少到多,呈一个金字塔形。

pyramid

上图引自Martin Fowler的测试金字塔的文章,其实不用他说,写过测试的都知道,单元测试的成本最低,运行 速度最快,但是到功能性测试,需要启动UI/浏览器去测试,成本很高,所以单元测试可以多考虑edge case,而 功能性测试只考虑happy path。曾经参与过一个项目,基本上严格执行BDD的开发方式,用Cucumber写了大量的测试, 但是每个产品的功能测试,运行完成都会超过一个小时,最后只能通过切分测试,多加几个Jenkins Slave去并行运行,才将构建的时间降到半个小时左右。

Unit Testing(单元测试)


对于每个开发人员来说,对于单元测试来说应该都很熟悉。在我看来,写单元测试的好处在于:

  1. 可重复自动运行,验证成本很小
  2. 代码为文档,测试可以帮助理解程序或者业务逻辑
  3. 单元测试一般粒度较小,因此发现问题,定位会比较快

当然,也有人反对单元测试,认为大多数的单元测试都是废柴,比如这位,可能很多认为在代码或者逻辑不太改变的情况下,单元测试在运行之后就没有什么太大的用处,但是一旦代码有任何调整,有单元测试是你重构的信心来源。 单元测试做到什么粒度,也是有争议的,我司曾经提出过功能性单元测试(Functional Level Unit Testing),可以认为是单元测试粒度放大,针对具体的功能。 单元测试的覆盖率应该是多少,也有争议,我当时的项目做到了后端代码100%测试覆盖率,js代码做到了70%-90%的测试覆盖率。有些人认为只针对关键功能,比如算法写单元测试就可以了,80% 就是很不错得测试覆盖率了。不过,上次苹果的一个安全漏洞,就是一段很简单的代码,如果有一个test case覆盖到的话,应该就不会出问题了。 单元测试的代码往往要多于产品代码,有时候维护也是个问题。

TDD(测试驱动开发)


提到单元测试就不得不说一下TDD测试驱动开发了,测试驱动开发就是在写实现代码之前先写测试,遵循下面的步骤: 1. 写测试,运行,失败(红) 2. 实现代码,运行测试,成功(绿) 3. 重构,运行测试,成功(绿)

这也就是TDD的两顶帽子,功能实现和重构,每次只能戴上其中一顶。 上面的描述缺少了TDD实践中一个很重要的环节,tasking,也就是将story分解成粒度更小的test case,然后划定实现的步骤,小步快跑。

从实际的项目中来看,TDD对于理清需求是有帮助的,有助于业务实现的正确性,保证了测试覆盖率,而且一旦建立 这种节奏,稳步向前带来的自信感非常好。 目前TDD的使用在国内外也是有争议的,我觉得很大程度上和敏捷社区如何推动这个技术实践的态度有关系,过于 强势,“不做TDD就怎么怎么了”,很容易让人反感,说实话我也很反感这样。用不用TDD和你的业务成功与否没有 直接的关联,加之TDD实际上降低了对程序员的要求(责任感>能力),所以切实实践过或者是大神们认为它有缺点 是很正常的一件事情,不过水平一般且没有实际的体会就不要附和了。

去年这个话题炒得很热,一定程度上因为DHH(Rails框架的创始人)的一篇文章TDD is Dead,之后他和Uncle Bob以及我司的Martin Fowler展开了一系列的论战。这种论战过程有趣,结果没什么意思,因为大神都比较顽固,不容易放弃自己的想法,不过不论他们怎么说,"Long Live The Testing",这点是要承认的。

我个人对于TDD,觉得还是要宽容些,保持责任心和实现对功能的测试更加重要,不用纠结于是不是T-DD出来的代码。

Integration Testing(集成测试)


集成测试就是各个模块组装进行测试,比如A依赖B模块,作为单元测试可能我只需要Mock或者Stub,但是集成测试会输入合适数据,真实的让A去调用B服务,测试是否会满足期望的结果。

现在微服务的这种架构方式很流行,一个简单的例子就是前后端分离,前端与不同的微服务交互,如此部署,维护, 扩展都会更加容易。在这样的架构下,集成测试是一个问题,Pact Testing是一个可选的方式。Pact是基于CDC - Consumer driven contracts的一个实现。

我们可以将前端理解为Consumer,后端理解为Provider,完整的测试过程如下:

  1. Consumer提出要请求的Provider的API格式,传入的数据以及期望的返回,运行测试后生成pact(json)文件并将其push到Provider的repo中
  2. Provider CI运行Pact测试,因为API还没有实现,测试会失败
  3. Provider的开发人员根据Pact文件中约定的Restful API以及Input/Output数据,实现并且通过测试。

目前Pact已经有多种语言的实现,比如ruby, Java/Scala等。

Functional Testing


功能测试,就是对产品的功能进行验证。对于web项目来说,就是启动服务,通过浏览器访问测试。曾经做过的Ruby 项目是使用cucumber去进行测试,其实依赖的工具是Capybara和Webdriver/Selenium。讲到这里就不得不提 BDD - Behavior Driven Development

BDD


BDD更关注从业务的角度出发,用Business Readable DSL的方式去描述一种实例行为,期望应用可以满足预期。 Cucumber是其一种实现,其测试的基本的格式如下:

1
2
3
Given I am a super user
When I log in the website
Then I should see "Admin" on the top right

通过在每个step中写一些Assertion,可以判断功能是否实现等。 我的第一个项目是严格遵循BDD的流程,基本顺序如下:

  1. 在接到一个需求时,按照描述,先写Cucumber测试
  2. 运行测试,失败,然后编写对应的单元测试
  3. 运行单元测试用例失败,实现之并让其通过
  4. 运行Cucumber测试,通过
  5. 重复1-4直至完全满足需求

做的好的话,整个开发的节奏会非常流畅。BDD原本期望是让BA或者产品经理去读或者写测试描述,但是很少有BA 或者产品经理回去阅读这些测试,写就更不可能了。程序员自己写的话,表达很难贴近正确的业务行为,加上不正确的 添加测试(通常Cucumber测试只检查Happy Path),导致测试越来越多,运行越来越慢,所有测试跑完的时间,远超过 我seed一些测试数据,启动浏览器手工测试快些。 以前的团队有很好的BA,在Story的Acceptance Criteria中,会将用例写的很清晰,这样基本上不用怎么修改就可以 写好Cucumber测试的step,现在倡导全功能团队,BA和QA这两个Role几乎都没有了,客户也不怎么提BDD了……。

Smoke Testing(冒烟测试)


冒烟测试是确认软件的基本功能是否正常的测试。

E2E Testing(端到端测试)


端到端测试,可以理解为整个系统在现实世界中的走完一个完整流程的测试。领域不同,端到端测试的概念不尽相同。 我们当时的端到端测试只cover了与其他系统的接入点功能的验证以及基本的流程的验证。因为要访问不同的服务, 而这些服务的可用性不一定很高,同时在运行测试时需要保证其他的系统都使用了合适的版本,当时的E2E测试经常 会出问题,修复也很麻烦,一定程度上得怪亚马逊EC2机器性能或者网络的问题-_-!

Regression Testing(回归测试)


回归测试,确保新加入的功能没有破坏已有的功能。讲到这里就得介绍下,在持续交付的前提下,我们通常用两种方式:

  1. Feature Toggle
  2. Feature Branch

用来保证实现新功能同时不影响旧功能。

Feature Toggle译为特性开关,就是通过配置文件/数据库来判断是否使用新特性的代码,其实就是读入配置文件,然后用if/else去判断用新或者是旧的代码。 这个的好处在于你可以只在主干上开发,保证不破坏现有的功能,同时还能保证持续交付/部署。要付出的代价就是写针对toggle打开或者关闭情况下, 两种状态不同功能的测试,如果有两个toggle,那么可能得针对四种状态写测试。在回归测试的时候会关闭所有Toggle,对应的还有Progression测试, 既打开enable所有toggle,新功能的测试应该都通过。如此测试的数量会大大增加,流水线构建的速度也会随之减慢。

Feature Branch就是在新的Branch上开发新功能,坏处就是与主干merge太晚,可能会出现merge hell,很难一下修复太多冲突。 相比之下Feature Toggle要更好些,但是注意最好不要加太多Toggle。

Acceptance Testing(接收测试)


就是开发或者测试将实现好的功能,在测试环境中,给business people做show case,然后证明所有的需求都正确实现了。只有他们说OK,story卡才能认为是done了。

Exploratory Testing(探索性测试)


新产品上线前,找一群对产品完全不了解的人,让它们试用,以期发现一些诡异的bug或者使用不合理的地方。做 过一次,还真的发现了bug。

Performance Testing(性能测试)


dark release 的时候,针对产品环境,使用典型的应用场景,进行性能测试,比如并发数等等。常用的工具有 ab,Jmeter, Gattling等。Jmeter 感觉配置比较麻烦,Gattling是基于Scala的,用起来还不错。

Infrastructure Test(面向基础架构的测试)


面向基础架构的测试是近年来提出的一种概念,应该上过我司的Tech Radar。 随着DevOps运动的兴起,"Infrastructure as Code"概念深入人心, 既然“基础架构即代码”,那么针对 这些代码,我们也可以做基本的单元测试等。比如puppet , chef。甚至有Babushka这样的为sysadmin准备的TDD工具。 除了开发环节,对线上的产品环境也做测试, 比如:

  1. Chaos Monkey是NetFlix推出的帮助测试系统 稳定性的工具,它会随机的干掉产品环境在AWS上一些节点,如果系统的弹性很好,那么它会自动恢复这些节点。 这是一种防御性的测试,同时通过测试的反馈,逐步的改进产品的稳定性和弹性。
  2. Nagios是一个监控工具,它可以测试产品环境的应用的健康状况以及服务器的监控状况。
  3. 用类似Gomez的工具去检测应用的可用性状况。
  4. 针对第三方的平台,如CDN,通常都是得手工配置,可以通过写单独的测试来验证配置正确,如CDN服务商提供的一些特殊的Header或者返回码。

那么问题来了


如果产品环境出现一个bug,该如何处理?下面是我觉得一个不错的处理方式。

  1. 定位问题
  2. 根据bug的场景,写一个失败的测试
  3. 修复这个测试
  4. 打包/部署测试环境/接受测试/部署产品环境

结论


场景不同,测试的方式方案不尽相同,但如果能保证最大程度的自动化,那将是极好的。

免责申明


本人不是测试专精,如果有误导或者描述不准确的地方,请轻喷。

References


Solve Request Entity Too Large Issue

| Comments

Sonatype Nexus是一个Maven的仓库管理器。它的好处在于:

  1. 作为其他Maven仓库的镜像或者代理,加快本地构建速度,同时节省带宽
  2. 作为私有仓库,可以保存自定义的artifacts,支持多种文件类型,jar,rpm等

最近我们将nexus从美西的AWS数据中心迁移到了悉尼的数据中心,迁移后的Nexus架构如下:

  1. 新建一个t2.micro的instance,挂载一个100GB大小的EBS block,并nexus工作目录放在其中
  2. 新建一个ELB,指向该instance,并限制访问的IP地址来源
  3. 在R53中建立域名的记录,并将其指向该ELB

为了保证Nexus环境在遇到问题时可以快速恢复,我们做了两件事情:

  1. 保证用代码可以重现整个Nexus的基础架构,这点通过CloudFormation结合AWS CLI工具实现
  2. 为了保证数据的完整性,我们在Nexus上运行cron job,每天给EBS block做一次snapshot

最近在更新了Nexus 上传artifact用户的credential之后,有其他组反馈往Nexus上publish artifact失败。 我和同事开始以为是有人又修改了密码,重新设置后依然不行。

这里要说一下Nexus的用户权限管理,它的权限可以细化到能不能用网页的方式登陆,可不可以往某个 repository中上传文件等。所以,如果发现有credential不能网页登陆Nexus,不代表这个用户不能够上传 artifact。

于是我和同事登陆到了Nexus的instance上面,一边执行上传artifact的任务,一边查看log。发现Nginx的 access log返回的是413 - Request Entity Too Large。原来,Nginx配置中client_max_body_size 这项可以限制请求的body大小,我们要上传的jar文件大概有19Mb,远超其默认的大小,所以请求出错,难怪本地也会返回java.net.SocketException. 于是,先在Nginx的配置文件的http部分添加:

1
client_max_body_size 50M;

保存之后,检查Nginx配置,reload:

1
2
3
4
5
root@aws nginx # nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

root@aws nginx # nginx -s reload

再次尝试上传,果然成功了。

之所以以前没有出现过这样的问题,是因为构建工具直接访问的是Nexus的8081端口,而其本身是没有这样的上传 文件大小限制的。我们在做migration时用了Nginx只是为了做代理,映射80->8081。但是在有ELB的前提下, 已经没有必要再使用Nginx了。所以下一步要做的事情就是更新cloudformation stack,让ELB的80和8081 端口都指向Nexus instance的8081端口即可。

A PIR for a Production Failure

| Comments

客户这边对产品环境的事故严重性分为5级定义:

  1. Sev 1 一级事故: 服务完全不可用,并且影响到了客户;
  2. Sev 2 二级事故: 产品的主要功能对于一小部分用户不可用,或者产品的次要功能对于大多数用户不可用;
  3. Sev 3 三级事故: 不重要的功能不能使用,对外部可见,但是影响不大,不属于严重事故;
  4. Sev 4 四级事故: 没有显著的影响,错误只是内部可见;
  5. Sev 5 五级事故: 不期而至的错误,只发生在团队内部;

举个例子来说,比如主站挂掉,这就是个一级错误,但是如果某个服务的产品数据库不能连接,造成的影响要小些, 可能就是二级事故了。蓝绿部署时inactive环境部署失败的话,这应该算是4级事故。将团队用的CI master 以及DB干掉,这个应该算是5级事故了。

一般来说,产品在上线之前,都会经过build pipeline, test/staging环境的各种测试,正常情况下它都是 应该工作的,所以出问题的情况都是非常巧合,甚至令人觉得匪夷所思。这也是这些事故有趣之所在了。

前几天,我就引起了这么一起Sev 2事故。我们做了一次PIR回顾 了这次事故,并且提出了相应的改进办法。

应用的功能


收集用户信息的静态页面

技术栈


  1. 纯前端项目,用Grunt/Bower等前端工作流,Karma做JS测试框架,requirejs做模块管理,前端框架用Backbone;
  2. 后端的API基于Scala,集成测试通过Pact(CDC)的测试方式;

系统架构


  1. 应用部署在AWS的S3上;
  2. 最前面用Akamai做CDN;
  3. 通过JSON的方式对后端数据读写;

Build Pipline以及部署方式


  1. 本地开发,测试运行后提交代码, PR/Review/Merge
  2. CI上面运行单元测试以及Pact集成测试,并且打包生成zip文件
  3. unzip-to-s3这个工具将文件上传到staging S3 bucket 和 production S3 bucket 如果我知道这玩意直接进入production,当时说不定会去多看几眼……。

事故回顾


  1. 我提交footer修改的PR,客户回复LGTM,然后merge
  2. 2天后客服人员通过Zendesk提交ticket说有几个客户抱怨产品好像不大好用了
  3. Tech Leader立刻召集产品相关的几个开发和运维人员检查,发现引用cache busting的js的script tag不知道怎么出问题了
  4. 在没有确定最根本的原因时,它们尝试revert了我的代码,然后手动修改了产品环境的文件……-_-!
  5. 找到了最根本的原因,是其中一个用来做cache busting的module突然升级,然后在package.json它是以~0.0.10的方式被引用的,如此在npm install的时候,它会自动升级小版本,没想到那个版本有问题,引入了一个bug。然后打包的过程在Bamboo build上,没人会去仔细看build log。
  6. 拿回我的提交,将cache busting模块的版本锁定,重新部署后恢复正常

事故后的Action


  1. 增加相关的测试用例,部署到staging和production后添加相关smoke测试以检验一切加载正常;
  2. 用到的cache busting库在github没有很多人参与,所以重新考虑换一个更加稳定,用得人多点的模块;
  3. 尝试将zendesk服务和pagerduty集成,如此开发团队会更快得到来自客户的反馈会;
  4. 思考下这种直接进入产品环境的持续交付的方式是不是合适的,或者如何去改进;
  5. 考虑对项目中使用到的模块用专门的build做升级时检查;
  6. 测试时要针对整个打包好的assets;
  7. 通过语义化(sematic)工具结合pagerduty报警;

对于纯前端的这种app,监控确实不如后端(可以用现成的工具如NewRelic)好做,目前的方式是通过Nagios做Active check,检查bucket中对应 的文件是否存在,实际的意义并不大,我觉得监控产品环境的集成页面会更有价值些,比如测试相应时间,如果响应 时间超过某个值,比如10s,或者返回的html中没有包含必要的元素,就认为它可能会有问题。

上面的最后一条也不错,通过API收集前端请求的次数,将数据发送到类似Graphite之类的工具,然后用Skyline之类 的工具进行数据分析,如果有些数据(如访问量)突然高于或者低于某个值,就可以发送警报到pagerduty。 另外,对于产品中引用的第三方类库/模块,除了固定使用的版本之外,还有必要搭建测试最新类库集成后的功能 完整性的build,这个叫做Canary Build(金丝雀构建),在Thoughtworks最新 的Tech Radar中有提到。

最后,希望我不会因为制造太多的产品事故而被炒-_-!

Running Containers in Docker

| Comments

既然有了boot2docker,不妨下几个容器来玩玩。先看下本地的docker服务的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
docker@boot2docker:~$ docker info
Containers: 0
Images: 0
Storage Driver: aufs
Root Dir: /mnt/sda1/var/lib/docker/aufs
Dirs: 0
Execution Driver: native-0.2
Kernel Version: 3.16.7-tinycore64
Operating System: Boot2Docker 1.4.1 (TCL 5.4); master : 86f7ec8 - Tue Dec 16 23:11:29 UTC 2014
CPUs: 4
Total Memory: 1.961 GiB
Name: boot2docker
ID: L5JR:QYWN:KEDD:FQ4O:66IY:FUE6:OXTA:SWAE:UV2R:LFCF:7H6H:PTS5
Debug mode (server): true
Debug mode (client): false
Fds: 10
Goroutines: 11
EventsListeners: 0
Init Path: /usr/local/bin/docker
Docker Root Dir: /mnt/sda1/var/lib/docker

docker@boot2docker:~$ ps aux | grep [/]usr/local/bin/docker
690 root     /usr/local/bin/docker -d -D -g /var/lib/docker -H unix:// -H tcp://0.0.0.0:2376 --tlsverify --tlscacert=/var/lib/boot2docker/tls/ca.pem --tlscert=/var/lib/boot2docker/tls/server.pem --tlskey=/var/lib/boot2docker/tls/serverkey.pem

从docker的后台进程可以看到,后台服务监听2376端口,同时支持tls加密协议。

Docker的官方镜像的入口在Docker Hub Registry,镜像文件放在了亚马逊AWS上,这对于国内的用户就比较悲剧了,因为要么是访问不到镜像文件,要么就是下载太慢。万幸的是,国内现在已经有了docker registry 的mirror - docker.cn。 所以在pull或者运行容器的时候,需要在镜像前加上registry的ip或者主机名。我觉得更好的方式是可以将registry写在配置文件中,这样可以避免这个麻烦,不过目前还没有看到这样的解决方案。 So

1
2
3
4
5
6
7
8
9
10
docker@boot2docker:~$ docker run -i -t docker.cn/docker/ubuntu /bin/bash
Unable to find image 'docker.cn/docker/ubuntu:latest' locally
Pulling repository docker.cn/docker/ubuntu
8eaa4ff06b53: Download complete
511136ea3c5a: Download complete
3b363fd9d7da: Download complete
607c5d1cca71: Download complete
f62feddc05dc: Download complete
Status: Downloaded newer image for docker.cn/docker/ubuntu:latest
root@0b8874d6f2c8:/#

这条命令的意思是保持标准输入打开状态-i,并且Docker要为容器分配一个伪终端(tty)-t。Docker daemon首先在本地寻找名为docker.cn/docker/ubuntu的镜像,找不到的话,在去docker.cn下载。 从镜像下载的方式我们大概可以判断整个镜像是分层的,像git一样有版本管理,这个就是aufs文件系统的作用。镜像下载完成,容器启动后执行了bash命令,可以认为是来到了容器运行的操作系统中。 之后就可以像使用一个Ubuntu系统一样去使用这个容器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
root@0b8874d6f2c8:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
6: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link
valid_lft forever preferred_lft forever

可以看到容器有自己的eth0网络设备以及由Docker分配的一个ip地址,和host machine没太大区别。

1
2
3
root@0b8874d6f2c8:/# vim
bash: vim: command not found
root@0b8874d6f2c8:/# apt-get install -y vim

没有vim,赶快装一个。

随之退出控制台,容器也停止运行。

1
2
3
4
5
docker@boot2docker:~$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
docker@boot2docker:~$ docker ps -l
CONTAINER ID        IMAGE                           COMMAND             CREATED             STATUS                          PORTS               NAMES
0b8874d6f2c8        docker.cn/docker/ubuntu:14.04   "/bin/bash"         15 hours ago        Exited (0) About a minute ago                       naughty_darwin

使用docker ps命令可以查看运行时的容器,对于停止的容器,可以通过 docker ps -l 或者 docker ps -a可以看到所有的容器。 容器启动时通过--name选项可以指定容器的名字,缺省会随机给出一个名字,很诡异的一个设计,不能理解。 有名字的好处就是就是容易记忆或者查看,否则如果只能按照containerid那串hash code使用,想来就有些头大。

重新启动停止的容器

1
2
3
4
5
docker@boot2docker:~$ docker start 0b8874d6f2c8
0b8874d6f2c8
docker@boot2docker:~$ docker ps
CONTAINER ID        IMAGE                           COMMAND             CREATED             STATUS              PORTS               NAMES
0b8874d6f2c8        docker.cn/docker/ubuntu:14.04   "/bin/bash"         15 hours ago        Up 3 seconds                            naughty_darwin

通过docker attach命令可以重新连接到容器上。

1
2
3
docker@boot2docker:~$ docker attach 0b8874d6f2c8
root@0b8874d6f2c8:/# which vim
/usr/bin/vim

上次安装的vim依然在,可见docker保存了容器的状态(persistence)。 前面所创建的容器是交互式的容器,而实际的使用当中,我们需要容器去运行程序或者服务,这种又称为daemonized container. 下面是一个简单的例子:

1
2
3
4
5
docker@boot2docker:~$ docker run --name awesomeness -d ubuntu /bin/sh -c "while :; do echo hello; sleep 5; done"
1bda4fdc3a3b869d1dc58bb4564b08e1bc0c0f24372f48afb8bcc4291cc94929
docker@boot2docker:~$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
1bda4fdc3a3b        ubuntu:latest       "/bin/sh -c 'while :   55 seconds ago      Up 54 seconds                           awesomeness

使用docker logs命令可以查看容器的日志:

1
2
3
4
5
6
7
docker@boot2docker:~$ docker logs awesomeness
hello
hello
hello
hello
hello
hello

加上-t选项可以输出时间戳,-f跟踪最新的日志:

1
2
3
4
5
6
docker@boot2docker:~$ docker logs -ft awesomeness
2015-01-17T13:17:43.827474614Z hello
2015-01-17T13:17:48.842826627Z hello
2015-01-17T13:17:53.847642897Z hello
2015-01-17T13:17:58.856081585Z hello
2015-01-17T13:18:03.863587257Z hello

查看容器内的进程:

1
2
3
4
docker@boot2docker:~$ docker top awesomeness
PID                 USER                COMMAND
922                 root                /bin/sh -c while :; do echo hello; sleep 5; done
1454                root                sleep 5

通过Docker在容器内部启动新的进程,执行新的任务:

1
2
3
4
docker@boot2docker:~$ docker exec -d awesomeness touch /tmp/testfile
docker@boot2docker:~$ docker exec -t -i  awesomeness  /bin/bash
root@1bda4fdc3a3b:/# ls -al /tmp/testfile
-rw-r--r-- 1 root root 0 Jan 17 14:03 /tmp/testfile

不得不说docker的发展真的很快,我最开始使用的时候docker的版本大概是在0.6,当时的容器技术还是使用lxc, 只能用lxc attach命令才能连接到运行的容器的控制台。现在使用这么简单,必须要点个赞。

停止运行的容器:

1
docker stop awesomeness   # 用containerid 1bda4fdc3a3b 效果相同

在容器意外退出时,自动重启容器:

1
docker@boot2docker:~$ docker run --restart=always --name awesomeness -d ubuntu /bin/sh -c "while :; do echo hello; sleep 5; done"

还可以设置重启的次数,--restart=on-failure:5。这个功能也很不错,让容器拥有了一定的自恢复(self-healing)能力,

查看容器的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
docker@boot2docker:~$ docker inspect awesomeness
[{
  "AppArmorProfile": "",
  "Args": [
  "-c",
  "while :; do echo hello; sleep 5; done"
  ],
  "Config": {
    "AttachStderr": false,
    "AttachStdin": false,
    "AttachStdout": false,
    "Cmd": [
    "/bin/sh",
    "-c",
    "while :; do echo hello; sleep 5; done"
    ],
    "CpuShares": 0,
    "Cpuset": "",
    "Domainname": "",
    "Entrypoint": null,
........

inspect可以返回JSON格式的详细配置信息,不过这个信息略多,通过--format可以格式化输出的内容,假设我只想看到容器的IP地址:

1
2
docker@boot2docker:~$ docker inspect --format '' awesomeness
172.17.0.3

有点像调用了jq命令。

删除容器的命令是docker rm CONTAINER_ID or CONTAINER_NAME,这个操作只能针对已经停止运行的容器。删除所有容器的命令是:

1
docker rm `docker ps -a -q`

这个命令会返回所有容器的ID,然后执行删除操作。实际的使用中我们可能只是删除部分的容器,可能就得结合cut,grep或者awk等命令来拿到要删除的容器ID/名字,然后再去执行删除操作。

Introduction to Boot2docker

| Comments

boot2docker 是一个轻量级的Linux发行版,它基于Tiny Core Linux,主要用于运行Docker容器。 它的主要特性有:

  1. 有AUFS文件系统支持的最新内核, 最新的docker版本
  2. 通过自动挂载/var/lib/docker来实现容器的持久性
  3. 通过磁盘自动挂载实现SSH key的持久性
  4. 通过Host-only网络设置,可以很容易的访问到docker映射的端口

除此之外,它运行时只占用27MB左右内存,启动时间大约只要5s,同时它还支持多个平台。因为容器技术是需要linux高版本的支持,所以在Windows和OSX上运行,同时要享受容器带来的资源红利,都是以虚拟机的形式运行。 本质上和Vagrant启动虚拟机运行Docker没有什么太大区别,不过使用更加方便,资源消耗较少(个人浅见)。

安装


OSX下用brew就可以直接安装:

1
brew install boot2docker

Windows用户请自觉安装Linux发行版。

初始化


创建一个新的boot2docker虚拟机

1
boot2docker init

启动


1
2
3
4
5
6
7
8
9
10
11
12
~> boot2docker up
Waiting for VM and Docker daemon to start...
....................oooo
Started.
Writing /Users/docker/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/docker/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/docker/.boot2docker/certs/boot2docker-vm/key.pem

To connect the Docker client to the Docker daemon, please set:
export DOCKER_TLS_VERIFY=1
export DOCKER_HOST=tcp://192.168.59.103:2376
export DOCKER_CERT_PATH=/Users/docker/.boot2docker/certs/boot2docker-vm

最新的docker支持了tls加密通信方式,要连接Docker后台的Docker客户端,需要export以上的几个环境变量。

杂项命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~> boot2docker info
{
  "Name": "boot2docker-vm",
  "UUID": "f7cdbcf4-31f5-4dc0-8d4a-83d38a573a50",
  "Iso": "/Users/docker/.boot2docker/boot2docker.iso",
  "State": "running",
  "CPUs": 4,
  "Memory": 2048,
  "VRAM": 8,
  "CfgFile": "/Users/docker/VirtualBox VMs/boot2docker-vm/boot2docker-vm.vbox",
  "BaseFolder": "/Users/docker/VirtualBox VMs/boot2docker-vm",
  "OSType": "",
  "Flag": 0,
  "BootOrder": null,
  "DockerPort": 0,
  "SSHPort": 2022,
  "SerialFile": "/Users/docker/.boot2docker/boot2docker-vm.sock"
}

可以看到虚拟机的配置

1
2
3
~> boot2docker ip

The VM's Host only interface IP address is: 192.168.59.103

可以查看boot2docker vm的ip地址。

1
boot2docker ssh

可以ssh到boot2docker虚拟机中,如果用free -m命令查看下当前使用到的内存,发现果然只用了几十兆,nice.

共享目录

boot2docker虚拟机缺省的共享目录是host machine的/Users目录,想添加新的共享目录的,除了通过VirtualBox的虚拟机配置界面添加之外,还可以通过VBoxManage命令

1
2
3
4
5
6
7
8
9
10
boot2docker stop # 确保虚拟机处于关闭状态

VBoxManage sharedfolder add boot2docker-vm --name test --hostpath ~/github

boot2docker start

boot2docker ssh

root@boot2docker:~# mkdir -p /mnt/test
root@boot2docker:~# mount -t vboxsf test /mnt/test

即可。


boot2docker 提供的功能还不止这些,通过 boot2docker -h可以查看其他功能命令及用法,至于在虚拟机内使用docker,那就是另外一个问题了。

Essential SSH

| Comments

什么是SSH


  • Secure Shell (SSH)* 是一种加密网络协议,主要用作以下用途:

  • secure data communication (安全数据传输)

  • remote command-line login (远程命令行登陆)
  • remote command execution (远程命令执行)
  • other secure network services between two networked computers (节点间安全网络服务)

相比telnet,ftp这些明文传输的协议,SSH要安全的多。

基本工作原理


SSH是传输层协议,有不同的版本(有SSH1和SSH2),SSH协议框架分为三部分:

  1. 传输层协议: 提供正向加密(如果一次会话被破解,不会影响到前面会话的安全性)的服务器验证,数据安全性,数据完整性。还可以提供压缩功能。
  2. 用户认证协议: 连接服务器的用户验证。
  3. 连接协议: 在底层的SSH链接基础上的多路复用的多个逻辑链路。

SSH连接过程如下图: ssh connection

大致可以分为如下的步骤:

  1. TCP 3次握手建立连接
  2. 版本号协商,让客户端和服务器端使用相同的协议版本通信
  3. 客户端和服务器端为使用公钥算法,加密算法等协商,得出最终要使用的算法
  4. 秘钥交换阶段,首先客户端会在 ~/.ssh/known_hosts等文件中查找服务器主机信息
  5. 如果没有找到,则提示客户端是否接受服务器签名,接受服务器的public key(/etc/ssh/ssh_host_rsa_key.pub),该key会被保存到~/.ssh/known_hosts
  6. 如果找到,服务器和客户端用算法(如Diffie–Hellman算法)交换秘钥,生成会话秘钥和会话ID,其中ID用于认证过程,会话秘钥用于数据的加密解密
  7. 用户认证阶段,有两种方式,一种是public key的验证方式,如果服务器在自己的~/.ssh/authorized_keys等文件中没有找到客户端的public key,验证失败,反之则成功;
  8. public key 认证失败后,会回退到密码认证,这个过程是加密的
  9. 认证完成后就是会话请求以及交互会话阶段,数据双向加密传输,服务器端执行从客户端传输的命令,然后将结果返回给客户端

以上只是ssh会话过程的一个简单描述,其实过程远比这个复杂,如果对细节比较关心,除了查阅材料,还可以让ssh打印冗余信息,来获取交互过程中具体发生的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[vagrant@bogon ~]$ ssh -vvv localhost
OpenSSH_5.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: Applying options for *
debug2: ssh_connect: needpriv 0
debug1: Connecting to localhost [::1] port 22.
debug1: Connection established.
debug1: identity file /home/vagrant/.ssh/identity type -1
debug1: identity file /home/vagrant/.ssh/identity-cert type -1
debug1: identity file /home/vagrant/.ssh/id_rsa type -1
debug1: identity file /home/vagrant/.ssh/id_rsa-cert type -1
debug1: identity file /home/vagrant/.ssh/id_dsa type -1
debug1: identity file /home/vagrant/.ssh/id_dsa-cert type -1
debug1: Remote protocol version 2.0, remote software version OpenSSH_5.3
..........

传输过程中数据一致性的保证,是通过计算所传输数据的校验码,md5等,传输完成后比对,就可以知道数据是不是完整的,有没有被更改。

生成key pair


ssh-keygen命令就可以生成一对key/pair,加密的算法和长度都是可选的,也可以选择对private key做密码保护,默认生成的key pair是在home目录的的.ssh目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[vagrant@bogon ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/vagrant/.ssh/id_rsa): something
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in something.
Your public key has been saved in something.pub.
The key fingerprint is:
a5:0d:9d:f7:9a:c7:55:3b:8f:d3:6e:73:92:4e:b1:aa vagrant@bogon.something.com
The key's randomart image is:
+--[ RSA 2048]----+
|                 |
|         . .     |
|        . + .   .|
|         = . .  o|
|        S .   oo.|
|             + *o|
|            o B.o|
|             +o+o|
|          E....o+|
+-----------------+

默认生成的key pair为~/.ssh/id_rsa~/.ssh/id_rsa.pub

服务器端配置文件


对于服务器端来说,配置文件在/etc/ssh目录下,其中sshd_config包含所有的配置,选项很多,举个简单的例子,企业版的Github的ssh服务运行在不同的 端口,如61000,那么修改配置文件sshd_config:

1
Port 61000

重启sshd

1
service sshd restart

之后就可以通过61000端口进行ssh连接

1
ssh -p 61000

客户端配置文件


客户端的配置文件默认在~/.ssh/目录下,其中known_hosts放置remote host及其public key,authorized_keys文件保存允许以public key验证方式登陆的主机。 主要的配置文件是~/.ssh/config,其中可以设置remote host相关信息等, 如下:

1
2
3
4
Host remotehost
  User user
  Port 7777
  HostName remotehost.example.com

如果没有以上的配置,那么想ssh到remotehost.example.com就需要以下的操作:

1
ssh -p 7777 user@remotehost.example.com

加入以上配置后,就只需像下面一样:

1
ssh remotehost

远程命令执行


其实比较简单了,将要执行的命令放在最后就可以了

1
ssh user@remotehost 'yum install -y package'

这种用法常见于自动化脚本中,或者登录到服务器时不需要执行交互式命令,如重启服务,安装包之类的。

以public key验证方式登陆远程服务器


其实就是将客户端的public key加入服务器端的~/.ssh/authorized_keys文件中,有以下两种方式:

1
ssh-copy-id user@remotehost

或者土一点

1
cat ~/.ssh/id_rsa.pub | ssh user@remotehost 'cat >> ~/.ssh/authorized_keys'

这样就可以实现无密码登陆服务器。我们的测试环境在亚马逊的AWS上,为了使用统一的工具去远程登陆所有的主机,在用亚马逊的API生成instance时候,同时将预先生成的public key放在instance的authorized_keys文件中,在工具远程登陆instance时,使用了下面的命令:

1
ssh -i private_key user@instance

既可以实现对测试环境下所有主机的访问。

Forward Agent


现实中有如下的一种使用场景,本机可以public key验证方式登陆remotehost1和remotehost2,但是从remotehost1到remotehost2必须以密码验证的方式登陆。这个问题可以通过forward agent解决,一种是用ssh-add添加private key:

1
ssh-add ~/.ssh/id_rsa

然后在ssh到remotehost1时启用forward agent:

1
ssh -A user@remotehost1

如此在登陆到remotehost1时,可以直接ssh到remotehost2而不需要输入密码,在remotehost1上面输入ssh-add -l可以发现本机的private key被带到了remotehost1上。

另一种是修改本机的ssh配置文件,针对remotehost1开启forward agent:

1
2
3
4
Host remotehost1
  User user
  HostName remotehost1
  ForwardAgent yes

就不再需要加 -A的选项了。

建立socket proxy


ssh强大的地方在于它可以的很容易建立起隧道或者socket代理,如果你有一个在国外的vps,分分钟就可以建立一个安全的代理,访问到墙外的网站。

1
ssh -D 8888 user@remotehost

执行完这个命令后,一个socket代理就搭建完成,在Firefox的网络设置中,配置socket代理,服务器为 127.0.0.1 端口为8888,就可以使用了。注意, 这个socket代理在Chrome下的的一些proxy插件如SwitchSharp下不工作,原因不太清楚。

SSH 端口转发


为了方便访问内网的产品环境的服务器,通常我们都会设置一些跳转机(bastion),只有登录到这台机器上时才能去访问内网的其他服务器。有这样的一种应用场景, 我希望访问在内网的staging环境的mysql_server服务器,但是我只有bastion机器的访问权限,怎么办呢,可以通过port forwarding来解决。

1
ssh -L 33306:mysql_server:3306 bastion -luser

之后用mysql客户端就可以连接本地的33306端口来访问数据库,所有的请求通过bastion机器转发给mysql_server.

1
mysql -h 127.0.0.1 -p 33306 -u user -p

除了远程的端口转发,本地也可以做类似的事情。比如现在有这么一种需求,我希望本地开发环境的服务器运行在80端口,而通常这些服务器在开发模式都运行>1024的端口下,这个也可以通过端口转发来完成:

1
2
3
python -m SimpleHTTPServer   # 启动在8000端口

sudo ssh -L 80:127.0.0.1:8000 user@127.0.0.1

如此就可以在浏览器输入localhost去访问服务,不需要再加端口号。当然,实现这个需求的方式有很多种,如iptables等,这里讲的只是其中一种方法。

SSH 反向端口转发


基于IPv4地址数量以及安全的考虑,公司或者学校的网络都是通过NAT管理的,出口的IP地址可能只有几个。可以用:

1
curl ifconfig.me/ip

获取自己的NAT ip地址。 如果我想在自己家中访问到公司的网络,那么可以用反向端口转发的方式在NAT上打个“洞”。假设家里的路由器已开通了DDNS服务,域名为home.oicp.net,那么我可以在公司的主机上做如下的事情:

1
ssh -R 8888:127.0.0.1:22 user@home.oicp.net

如此,我就建立了一个反向连接的隧道,从家中的路由器服务器上,我做如下的操作:

1
ssh -p 8888 user@127.0.0.1

就可以访问到公司的主机了。 同理,可以通过类似的方式访问到公司内部的资源或者一些服务,前提是公司没有提供VPN,否则不推荐这种方式,不符合安全守则。

文件传输


用ssh进行文件传输,一般我们听到这个时候都会想到scp或者rsync,其实ssh也可以,只不过方式略微hack一些。

1
tar cvf file | ssh user@remotehost 'tar xv '

其实就是用tar命令将文件打包然后写到标准输入,再经由管道在远程机器解开文件。虽然这是通过加密方式传输,但始终觉得这种方式不甚靠谱,一旦传输因为网络原因断开,就得重新传输,不能续传。

总结

ssh常见的用法大概就是这些,详细的原理以及其他用法可以参见后面给出的一些链接。

references


1 2 3 4 5