# DevOps案例——Etsy
## 1. 公司介绍
Etsy成立于2005年,是一个电子商务平台(C2C),以手工艺制品买卖为主要特色,其核心理念是社区化的C2C平台经营,基于此理念网站发展出了六大功能:论坛、虚拟实验室、聊天室、小组、微件、博客。
#### 网站功能
1. 虚拟实验室:Etsy为用户设置的在线课堂,学习与社交的方式,可以与手工制品培训人员互动学习
2. 聊天室:公共聊天室,鼓励交流,增强社区黏性
3. 小组:主题群组,组织线下聚会,通过聊天室和虚拟实验室进行线上交流
4. 微件:用户插入到社交网站的标志性图标等
5. 博客:面向Etsy社区的精美手工艺杂志,艺术家介绍、新手指南、主题活动通知,内容质量非常高,吸引用户
#### 技术栈
PHP
#### 业务规模:2014年数据
1. 200多个国家的4200W用户
2. 100W活跃卖家
3. 13亿美元销售额
#### 系统规模
1. 访问量(Visit):60W/月
2. 页面浏览量(PV):15亿/月
#### 部署能力
每天超过60次部署
#### 开发策略:
1. 反复做许多小的、持续的变更=需要=>每天多次部署
2. 开发人员确保:是否有足够的信心来部署这个变更
#### 组织结构
1. 600员工
2. 核心团队,团队规模、角色配置
3. 核心平台团队:10~15人:整体负责服务器及系统层与应用层的接口,为上层开发特性的工程师们提供服务,如数据库接入、图片存储系统、异步任务管理等
#### 问题
1. 业务与组织规模的快速扩张带来工程与管理问题,比如构建压力大,发布堵车等
## 2. 组织与文化
### 文化
原技术总监,现任CEO $$Chad$$ $$Dickerson$$ :
1. 代码是工艺品(Code As Craft)
2. 开发和运维是目标统一的合作者(交付团队)
### 组织
每个团队规模:3~7名工程师+1名产品经理+1名设计师
### Etsy模式
#### 组织文化
##### 1. 工程师的快乐
1. 尊重工程师
2. 良好的工作环境
3. 丰盛的午餐
4. 每个人都可以部署代码(充分授权)
easy deploys == developer happiness
*本质:快速获得反馈,快速获得成就感*
5. 发布火车(排队)
##### 2. 委派运维
内容:出席开发会议,参与新特性的发布,掌握开发团队的工作情况,以便提出自己的专业意见,比如第三方接口的稳定性等,但是该运维工程师不是只为该团队服务。
目的:,
1. 让开发了解运维如何进行(专业信息的双向流通),培养开发的自运维能力
*搜索团队已经能够处理日常的运维问题,当无法处理时,他们会反馈给专业的运维团队处理*
2. 运维团队会根据掌握的产品发布时间和内容预估基础设施需求,提前计划,寻求解决方案,比如服务器需求、新工具与技术的研究
*互相了解对方的工作,理解对方的痛苦和努力,避免因为不了解而造成的误解,避免埋怨,提供正向的协作和沟通环境*
3. 打破部门墙,增强开放性的协作和交流,实现更频繁的跨组织协作和沟通
##### 3. 其他
1. 不要设置DevOps团队或者岗位
2. 学习型文化,不惧怕失败
3. 团队的目标统一
#### 频繁部署
#### 跟踪每次发布
#### 度量一切
#### 测试用例分组
参考
1. https://codeascraft.com/2010/02/10/code-as-craft/
2. https://codeascraft.com/2010/02/17/dev-ops-cooperation/
3. https://codeascraft.com/2011/06/06/optimizing-for-developer-happiness/
4. https://codeascraft.com/2012/02/13/the-etsy-way/
5. https://codeascraft.com/2011/02/04/how-does-etsy-manage-development-and-operations/
*多篇文章总结而成,分布于2010~2012年*
## 3. 增量构建
### 原则
主干代码一定是时刻保持可发布到产品环境的状态
### 需求
在提交前必须验证变更
### 问题
1. 在开发者VM上无法完整通过所有测试套件
2. 在集成环境,执行一次完整测试需要3小时以上,使用CI集群可以降低到30分钟
3. 在集成环境,由于失败后重新构建和修改后重新构建导致时间远超过30分钟
### 原因
1. 集成环境测试执行通过慢的原因是在集成之前,从没有真正完整执行过一次测试套件,导致集成环境测试失败率高、反复多次
2. 在提交前(开发环境VM),开发人员无法在合理(较短)的时间内完整执行一次测试套件
3. 多名开发在单台机器上执行测试套件会相互之间干扰,且影响性能
### 实践内容:
1. 基于稳定版本的增量式部署和测试(Etsy使用PHP语言,动态语言)
使用svn diff(Git同理)和稳定版本(trunk latest)对比,并将补丁文件覆盖到持续集成环境中的稳定版本之上。该方法可以实现多人同时部署更新并进行测试,相互之间不影响(并行测试)
2. 具体方法
1)Create a new Jenkins Freestyle Project(or copy an existing) and
Select Parameterized Build
File Parameter: patch.diff
String Parameter: username
Set up the SCM as usual
Add an Execute Shell build step: Apply the patch.diff
Use $username in the recipient list of the e-mail publisher
2)Write a short bash script that
Creates a patch
Sends a cURL request with the patch and $USERNAME to start a build of the Jenkins job
代码示例:
```
#!/bin/bash
HUDSON="ci.example.com"
LOCATION="/home/$USER/working_copy"
PATCH='patch.diff'
cd $LOCATION
svn diff > $PATCH
file_param="{'name': 'patch.diff', 'file': 'file0'}"
user_param="{'name': 'executor', 'value': '$USER'}"
args=("$@")
for ((i=0;i<${#args[@]};i++)); do
curl -F file0=@$LOCATION/$PATCH
-F json="{'parameter': [$file_param, $user_param]}"
-F Submit=Build http://$HUDSON/job/try-${args[i]}/build
done
```
3. 文化
在提交之前一定要try一次,是团队自觉遵守的规范
4. 工具
[TryLib](www.oschina.net/p/trylib) 是简单的PHP库,帮助你生成工作副本之间的差异报告,发送到Jenkins,在最新代码分支上运行测试套件。Try将Jenkins和Deployinator连接起来。
### 参考
1. Mozilla的TryServer:https://wiki.mozilla.org/ReleaseEngineering/TryServer
2. https://codeascraft.com/2011/10/11/did-you-try-it-before-you-committed/
*2011年10月*
## 4. 构建集群
### 原则
1. test in a clone of production environment
2. keep the build fast
### 背景
1. 业务与团队扩充,测试套件的执行压力增加,每天超过1950次构建与测试需要执行,如果在提高执行效率,避免拖慢交付流水线
### 问题
1. 开发者在自己的VM中执行测试,破坏了“在类生产环境进行测试”的持续集成原则,因为VM上可能存在开发自己安装的软件
2. 团队大家遵守规范和文化,不会破坏可发布的状态,采用主干开发并使用,使用Try(通过Jenkins将开发者提交的代码增量应用到具有稳定版本代码库的服务器上,并且执行所有的构建与测试)执行提交前验证,导致每天需要执行的测试套件次数增长较快(10个开发者,平均每天515次try,超过13700次测试套件的执行)
3. 不同的构建/测试任务对CPU和磁盘I/O的消耗各不相同,同时执行多个同类型的任务会导致资源瓶颈,构建服务器压力较大
4. 构建服务器上,多个构建/测试任务同时执行时,容易导致工作区内容冲突
### 需求
1. 测试套件执行时间保持在5分钟内
2. 需要根据测试的不同类型,灵活的将测试分配到不同的服务器上执行,以免造成资源的负载过高,形成卡顿
3. 工作区内容独立,即文件系统独立
### 实践
核心策略:尽量在主干分支上开发
#### Etsy的测试主要可以分为两类:
| 类型 | 测试阶段 |量级 |说明|
| :-- | :-- | :-- |:--|
|CPU消耗型| 单元测试 | 轻量级 |对CPU的压力较大|
|I/O消耗型| 集成测试 | 重量级 |对磁盘I/O压力较大|
#### 虚拟化构建环境
1. 基于LXC的虚拟化技术,每个容器都是独立的构建/测试环境,均衡工作负载。
2. 容器和Executor的比例是1:1,每1个独立的容器都是1个slave,实现了资源隔离,避免了多个Executor访问同一个workspace目录,也更加便于使用Virtual Madness实现构建环境的自动化提供
3. 采用Divide and Consur策略和Jenkins的master project插件,实现测试套件的并发执行
#### Virtual Madness
Virtual Madness是Etsy管理容器化构建环境平台,Etsy采用LXC来实现构建环境容器化(此时还没有Docker)
注:在Etsy,构建服务器采用实体机(**刀片式服务器,每个刀片可以视为一台单独的服务器**),具体配置如下:
*2U的超微服务器,4个刀片,每个刀片挂载3块SSD,共12块盘*
##### Virtual Madness界面
其中会显示容器的运行状态,容器被挂载到哪个Jenkins上,容器所在的物理磁盘等(BOB代表容器)

*注:每一列就是一个刀片,共3块盘,每一列划分为不同的用途,如BUILDTEST01.NY4DEV即构建与测试用的服务器,在纽约*
##### 一键创建容器
Etsy坚持一键部署原则,构建环境管理也一样
1. 选择容器创建位置
如图所示,Host下拉列表会默认选中第一个充足磁盘空间的主机,实践中,每台主机一般为14个容器(逐渐可支持更多容器)。点击**Make it**即可创建容器

**物理磁盘的选择规则:**每个刀片的第1块最多起4个容器,第2\3块磁盘各自最多起5个容器(4+5+5=14),如此,第一块磁盘有足够的空间以支持操作系统的使用并提供基础LVM卷(包含容器的初始化配置内容,会分发到每个容器中)
2. 使用lvcreate和mkfs.ext3创建容器时创建并挂载一个空的LVM卷
3. 将基础LVM卷的内容复制到该卷中
4. 使用Chef完成容器的配置
5. 挂载到Jenkins上并分配Executor标签(使用groovy脚本实现容器和Jenkins的连接)

**规则:每块磁盘同时只能允许执行一个重量级任务**
##### 容器分类:**CI HEAVY** \ **CI ANY** \ **TRY HEAVY** \ **TRY ANY**
1. 环境维度:
1. CI环境是后台自动执行,用于构建和更完善的测试
2. TRY环境是开发者使用,用于自行构建和进行测试
2. 任务维度:
1. HEAVY任务是指重量级任务,比如集成测试,对IO要求较高
2. ANY任务是指轻量级的任务,比如单元测试,对CPU要求较高
##### Virtual Madness其他功能
1. 容器也可以批量创建,如图所示:

2. 可以重建基础容器

3. 每个容器的IP地址也是依靠DNS反向解析查到返回NXDOMAIN(即该IP可用)后才分配给容器的。Etsy用到了nsupdate管理DNS
### 参考资料
https://codeascraft.com/2013/09/23/lxc-running-14000-tests-per-day-and-beyond-part-1/
https://codeascraft.com/2013/09/23/lxc-automating-containers-aka-virtual-madness-part-2/
*2013年9月*
## 5. 测试分组
### 原则
Fail fast
Fast, Reliable Tests
### 问题
每天超过25次部署,每次部署都需要执行测试
### 需求
部署耗时需要保持在20分钟内,长时间的部署过程会导致部署队列拥堵,降低效率,也会打击部署积极性
### 实践
**主干测试**(trunk tests):部署之前必须执行的测试,用于测试Etsy的产品功能,Etsy拥有7000个主干测试(持续增长中,并非都是单元测试,并非所有测试都要在部署前执行)
**将重量级测试套件根据相似性拆分为多个轻量级的测试套件**
#### 5分钟、11分钟、20分钟
如果主干测试失败,部署将会暂停,工程师一般将会在5分钟内解决该问题,然后重新执行测试,通过之后本次部署成功结束,考虑失败的情况,自动化主干测试必须在11分钟内完成,才有足够的剩余时间重新测试,以免部署超过20分钟太多
#### 测试执行策略
从前到后顺序执行完所有的主干测试(大于7000)需要耗时超过30分钟,如何实现11分钟:
1. 对测试进行分组,选择性执行
2. 将测试分发到10台Jenkins构建服务器上并行执行
**测试的执行场景**
1. 每次提交时执行
2. 点击测试按钮时执行
3. 点击发布按钮时执行
#### 测试分类
##### 单元测试(Unit Tests)
单元测试只针对一个类,无需与数据库、文件或其他基础设施的交互。Etsy拥有超过4500个单元测试,且必须在部署前执行,耗时1分钟
##### 集成测试(Integration Tests)
集成测试需要与基础设施交互,如数据库、消息队列、缓存服务等,Etsy使用PHPUnit连接DBUnit(模拟数据库服务),Etsy的[PHPUnit扩展](https://github.com/etsy/phpunit-extensions)
在Etsy的测试套件中,集成测试最慢,如果顺序执行需要耗时20分钟,**并行执行则只需耗时8分钟**
##### 网络测试(Network Tests)
部分集成测试也需要访问网络资源,如使用第三方服务发送邮件等,Etsy建议尽量避免需要依赖网络访问的测试
##### 冒烟测试(Smoke Tests)
冒烟测试是系统级别的测试,测试目标是部署完成系统,通过PHPUnit执行Curl命令,并且使用断言比对返回结果的header和其他数据
##### 功能测试(Functional Tests)
类似冒烟测试,基于GUI驱动的端到端功能测试的测试目标也是部署在测试环境的系统,Etsy使用Cucumber和Selenium进行功能测试,基于Xvfb的虚拟桌面环境驱动Firefox进行测试
功能测试脚本需要在开发和维护上投入较大工作量,在Etsy,端到端的功能测试只用于最重要的关键功能。在单元测试、集成测试、冒烟测试之后,功能测试只作为最终的验证
*编者注:功能测试对重要功能的验证是很有意义的,从用户操作的角度进行验证。比如Dropbox有一次部署将用户验证功能关闭了,导致极大的安全问题,类似这样的关键功能应该增加最后一道门槛。*
#### 不稳定测试(Intermittent Tests)
不稳定测试:偶尔会失败的测试,但是对于开发很有帮助,此类测试不能纳入主干测试,因此Etsy建立了PHPUnit中的flaky组,当开发者认为测试不够稳定时,可以将其划分到flaky组中,并在其他场景下使用
*零容忍对待不稳定测试*
#### 慢测试(Slow Tests)
符合80/20原则,少数测试占用了主要的测试执行时间,工程师会将耗时长的测试划分到slow组,失败和取消最慢的20个慢测试,测试套件执行时间会有明显提升,此类测试依然会在某些场景下执行,但是主干测试中不进行
#### 睡眠测试和时间测试(Sleep Tests and Time Tests)
好的测试不会使用sleep(),也不会依赖于系统时间。
追求测试覆盖率会导致出现低质量的测试用例,尤其是维护历史系统时,Etsy允许这类测试存在,但是不在部署前执行这些测试
### 工具
1. PHPUnit,使用其他@group的注释功能,从逻辑上将测试划分为多个子集。
2. Jenkins,使用XUnit插件,选择性执行测试
```
dbunit
database
network
flaky
sleep
slow
```
*解释:PHPUnit任务只会执行dbunit,不会执行database,network, flaky, sleep,slow;所有单元测试必须被执行。*
### 总结
1. 识别并分组执行测试
2. 并行执行测试
3. 只保留对部署对重要的测试
##### 文化
Etsy的部署由工程师管理,而非运维或者其他发布管理团队
理由:你写的代码,你自己发布到线上。$$Even $$ $$dogs $$ $$deploy $$ $$code$$
授权工程师对测试用例进行分类
### 参考
https://codeascraft.com/2011/04/20/divide-and-concur/
*2011年4月*