转:各大互联网公司架构演进之路汇总

原文地址:各大互联网公司架构演进之路汇总 by HollisChuang
请转载时务必保留文章的上述原始出处。

Web

支付宝和蚂蚁花呗的技术架构及实践
支付宝的高可用与容灾架构演进
聚划算架构演进和系统优化 (视频+PPT)
淘宝交易系统演进之路 (专访)
淘宝数据魔方技术架构解析
淘宝技术发展历程和架构经验分享(视频+PPT)(2.3日更新)
高德——快速转型时期的稳定性架构实践(视频+PPT)(2.3日更新)
秒杀系统架构分析与实战
腾讯社区搜索架构演进(视频+PPT)
京东峰值系统设计
京东咚咚架构演进
新浪微博平台架构
微博图床架构揭秘
微博推荐架构的演进
当当网系统分级与海量信息动态发布实践
当当网架构演进及规划实现(视频+PPT)
LinkedIn架构这十年
Facebook’s software architecture(英文)
从0到100——知乎架构变迁史
豆瓣的基础架构
搜狗搜索广告检索系统-弹性架构演进之路(视频+PPT)
小米网抢购系统开发实践
小米抢购限流峰值系统「大秒」架构解密
海尔电商峰值系统架构设计最佳实践
唯品会峰值系统架构演变
1号店电商峰值与流式计算
蘑菇街如何在双11中创造99.99%的可用性
麦包包峰值架构实践
苏宁易购:商品详情系统架构设计
携程的技术演进之路
篱笆网技术架构性能演进(视频+PPT)
从技术细节看美团的架构(1.26日更新)
美团云的网络架构演进之路(2.3日更新)
百度开放云大数据技术演进历程(视频+PPT)(2.3日更新)
途牛供应链系统的架构演进(视频+PPT)(2.3日更新)
Airbnb架构要点分享(2.3日更新)
12306核心模型设计思路和架构设计(2.20日更新)

无线

阿里无线技术架构演进
支付宝钱包客户端技术架构(2.3日更新)
手机淘宝构架演化实践
手淘技术架构演进细节
手机淘宝移动端接入网关基础架构演进之路
微信后台系统的演进之路
微信红包的架构设计简介
微信Android客户端架构演进之路
Android QQ音乐架构演进(视频+PPT)
快的打车架构实践
Uber 四年时间增长近 40 倍,背后架构揭秘
Uber容错设计与多机房容灾方案
大众点评移动应用的架构演进(视频+PPT)
饿了么移动APP的架构演进

其他

魅族实时消息推送架构
魅族云端同步的架构实践和协议细节


欢迎补充!~

从LinkedIn,Apache Kafka到Unix哲学

原文链接:
http://www.confluent.io/blog/apache-kafka-samza-and-the-Unix-philosophy-of-distributed-data
作者:Martin Kleppmann
译者:杰微刊-macsokolot(@gmail.com)

当我在为我的书做研究时,我意识到现代软件工程仍然需要从20世纪70年代学习很多东西。在这样一个快速发展的领域,我们往往有一种倾向,认为旧观念一无是处——因此,最终我们不得不一次又一次地为同样的教训买单,这真艰难。尽管现在电脑已经越来越快,数据量也越来越大,需求也越来越复杂,许多老观点至今仍有很大的用武之地。

在这篇文章中,我想强调一个陈旧的观念,但它现在更应该被关注:Unix哲学(philosophy)。我将展示这种哲学与主流数据库设计方式截然不同的原因;并探索如果现代分布式数据系统从Unix中学到了一些皮毛,那它在今天将发展成什么样子。

LinkedIn,ApacheKafka,Unix

特别是,我觉得Unix管道与ApacheKafka有很多相似之处,正是由于这些相似性使得那些大规模应用拥有良好的架构特性。但在我们深入了解它之前,让我稍稍跟你提一下关于Unix哲学的基础。或许,你之前就已经见识过Unix工具的强大之处——但我还是用一个大家相互都能讨论的具体例子来开始吧。
假设你有一个web服务器,每次有请求,它就向日志文件里写一个条目。假设使用nginx的默认访问日志格式,那么这行日志可能看起来像这样:
216.58.210.78 – – [27/Feb/2015:17:55:11 +0000] “GET /css/typography.css HTTP/1.1”
200 3377 “http://martin.kleppmann.com/” “Mozilla/5.0 (Macintosh; Intel Mac OS X
10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36”
(这里实际上只有一行,分成多行只是方便阅读。)此行的日志表明,服务器在2015年2月27日17:55:11从客户端地址216.58.210.78收到了一个文件请求/css/typography.css。它还记录了其他各种细节,包括浏览器的用户代理字符串。
许多工具能够利用这些日志文件,并生成您的网站流量报告,但为了练练手,我们建立一个自己的工具,使用一些基本的Unix工具,在我们的网站上确定5个最热门的网址。首先,我们需要提取出被请求的URL路径,这里我们可以使用awk.
awk并不知道nginx的日志格式——它只是将日志文件当作文本文件处理。默认情况下,awk一次只能处理一行输入,一行靠空格分隔,使之能够作为变量的空格分隔部件$1, $2, etc。在nginx的日志示例中,请求的URL路径是第7个空格分隔部件:

LinkedIn,ApacheKafka,Unix

现在我们已经提取出了路径,接下来就可以确定服务器上5个最热门的网站,如下所示:
这一系列的命令执行后输出的结果是这样的:
4189 /favicon.ico
3631 /2013/05/24/improving-security-of-ssh-private-keys.html
2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html
1369 /
915 /css/typography.css

LinkedIn,ApacheKafka,Unix

如果你并不熟悉Unix工具的话,上述命令看起来有点难懂,但它真的很强大。这几条简单的命令能够在几秒钟内处理千兆字节的日志文件,而且你可以根据需要,非常容易地更改分析内容。比如说,你现在想统计访问次数最多的客户端IP地址,而不是最热门的那几个网页,只需更改awk的参数'{print $1}’
按需求使用这些组合命令awk, sed, grep, sort, uniq , xargs的话,海量数据分析能够在几分钟内完成,其性能表现让人出乎意料。这不是巧合,是Unix设计哲学的结果。

LinkedIn,ApacheKafka,Unix

Unix哲学就是一套设计准则, 在20世纪60年代末与70年代初,这些准则是在设计和实现Unix系统时才逐渐出现的。关于Unix哲学有非常多的阐述,但有两点脱颖而出,由Doug McIlroy, Elliot Pinson 和Berk Tague在1978年描述如下:
1. 每个程序只做好一件事。如果有新的任务需求,那就编写一个新的程序而不是在一个旧的程序上加一个新的“功能”,使其越来越复杂。
2. 期望每个程序的输出都能是其他程序的输入,即使是未知的程序。
这些准则是能把各种程序连接成管道的基础,而有了管道就能完成复杂的处理任务。这里的核心思想就是一个程序不知道或者说不需要关心它的输入是从哪里来的,输出要往哪里去:可能是一个文件,或者操作系统的其他程序,又或者是完全由某个开发者开发的程序。

LinkedIn,ApacheKafka,Unix

操作系统附带的工具都是通用的,但是它们被设计成能够组合起来执行特定任务的较大的程序。
Unix的设计者遵循这种程序设计方法所带来的好处有点像几十年后出现的Agile 和DevOps的成果:脚本与自动化,快速原型编码(rapid prototyping),增量迭代,友好的测试(being friendly to experimentation),以及将大型项目分解成可控的模块。再加上CA的改变。(Plus ?a change.)

LinkedIn,ApacheKafka,Unix

当你在shell里为2个命令加上管道的标示符,那么shell就会同时启动这2个命令程序,然后将第一个程序处理的输出结果作为第二个程序的输入。这种连接机构由操作系统提供管道系统调用服务。

请注意,这种线性处理不是由程序本身来完成的,而是靠shell——这就使得每个程序之间是“松耦合”,这使得程序不用担心它们的输入从哪里来,输出要往哪里去。

LinkedIn,ApacheKafka,Unix

1964年,管道(Pipe)由Doug McIlroy发明,他首次在Bell实验室内部备忘录里将其描述为:“我们需要一些连接各种程序的方法就像花园里的软管——当它成为另一种必要的消息数据时,需要拧入其他的消息段。” Dennis Richie后来将他的观点写进了备忘录

LinkedIn,ApacheKafka,Unix

他们也很早就意识到进程间的通信机制(管道)与读写文件机制非常相似。我们现在称之为输入重定向(用一个文件内容作为一个程序的输入)和输出重定向(将一个程序的结果输出到一个文件)。
Unix程序之所以能够有这么高的组合灵活性,是因为这些程序都遵循相同的接口:大多数程序都有一个数据输入流(stdin)和两个输出流(stdout常规数据输出流和stderr错误与诊断信息输出流)。

LinkedIn,ApacheKafka,Unix

程序通常除了读stdin流和写stdout流之外,它们还可以做其它的事,比如读取和写入文件,在网络上通信,或者绘制一个用户界面。然而,该stdin/stdout通信被认为是数据从一个Unix工具流向另一个的最主要的途径。
其实,最令人高兴的事莫过于任何人可以使用任意语言轻松地实现stdin/stdout接口。你可以开发自己的工具,只要其遵循这个接口,那么你的工具能和其他标准工具一样高效,并能作为操作系统的一部分。

LinkedIn,ApacheKafka,Unix

举个例子,当你想分析一个web服务器的日志文件,或许你想知道来自每个国家的访问量有多少。但是这个日志并没有告诉你国家信息,只是告诉了你IP地址,那么你可以通过IP地理数据库将IP地址转换成国家。默认情况下,你的操作系统并没有附带这个数据库,但是你可以编写一个将IP地址放进stdin流,将输出国家放进stdout流的工具。
一旦你把这个工具写好了,你就可以将它使用在我们之前讨论过的数据处理管道里,它将会工作地很好。如果你已经使用了Unix一段时间,那么这样做似乎很容易,但是我想强调这样做非常了不起:你自己的代码程序与操作系统附带的那些工具地位是一样的。
图形用户界面的程序和Web应用似乎不那么容易能够被拓展或者像这样串起来。你不能用管道将Gmail传送给一个独立的搜索引擎应用,然后将结果输出到wiki上。但是现在是个例外,跟往常不一样的是,现在也有程序能够像Unix工具一样能够协同工作。

LinkedIn,ApacheKafka,Unix

换个话题。在Unix系统开发的同时,关系型数据模型就被提出来了,不久就演变成了SQL,被运用到很多主流的数据库中。许多数据库实际上仍在Unix系统上运行。这是否意味着它们也遵循Unix哲学?

LinkedIn,ApacheKafka,Unix

在大多数据库系统中数据流与Unix工具中非常不同。不同于使用stdin流和stdout流作为通信渠道,数据库系统中使用DB server以及多个client。客户端(Client)发送查询(queries)来读取或写入服务器上的数据,server端处理查询(queries)并发送响应给客户端(Client)。这种关系从根本上是不对称的:客户和服务器都是不同的角色。

LinkedIn,ApacheKafka,Unix

Unix系统里可组合性和拓展性是指什么?客户端(Clients)能做任何他们喜欢的事(因为他们是程序代码),但是DB Server大多是在做存储和检索数据的工作,运行你写的任意代码并不是它们的首要任务。
也就是说,许多数据库提供了一些方法,你能用自己的代码去扩展数据库服务器功能。例如,在许多关系型数据库中,让你自己写存储过程,基本的程序语言如PL / SQL(和一些让你在通用编程语言上能运行代码比如JavaScript)。然而,你可以在存储过程中所做的事情是有限的。
其他拓展方式,像某些数据库支持用户自定义数据类型(这是Postgres的早期设计目标),或者支持可插拔的数据引擎。基本上,这些都是插件的接口:

你可以在数据库服务器中运行你的代码,只要你的模块遵循一个特定用途的数据库服务器的插件接口。
这种扩展方式并不是与我们看到的Unix工具那样的可组合性一样。这种插件接口完全由数据库服务器控制,并从属于它。你写的扩展代码就像是数据库服务器家中一个访客,而不是一个平等的合作伙伴。

LinkedIn,ApacheKafka,Unix

这种设计的结果是,你不能用管道将一个数据库与另一个连接起来,即使他们有相同的数据模型。你也不能将自己的代码插入到数据库的内部处理管道(除非该服务器已明确提供了一个扩展点,如触发器)。
我觉得数据库设计是很以自我为中心的。数据库似乎认为它是你的宇宙的中心:这可能是你要存储和查询数据,数据真正来源,和所有查询最终抵达的唯一地方。你得到管道数据最近的方式是通过批量加载和批量倾倒(bulk-dumping)(备份)操作,但这些操作不能真正使用到数据库的任何特性,如查询规划和索引。
如果数据库遵循Unix的设计思想,那么它将是基于一小部分核心原语,你可以很容易地进行结合,拓展和随意更换。而实际上,数据库犹如极其复杂,庞大的野兽。Unix也承认操作系统不会让你真的为所欲为,但是它鼓励你去拓展它,你或许只需一个程序就能实现数据库系统想要实现所有的功能。

LinkedIn,ApacheKafka,Unix

在只有一个数据库的简单应用中,这种设计可能还不错。
然而,在许多复杂的应用中,他们用各种不同的方式处理他们的数据:对于OLTP需要快速随机存取,数据分析需要大序列扫描,全文搜索需要倒排索引,用于连接的数据图索引,推荐引擎需要机器学习系统,消息通知需要的推送机制,快速读取需要各种不同的缓存表示数据,等等。
一个通用数据库可以尝试将所有这些功能集中在一个产品上(“一个适合所有”),但十有八九,这个数据库不会为了某个特定的功能而只执行一个工具程序。在实践中,你可以经常通过联合各种不同的数据存储和检索系统得到最好的结果:例如,你可以把相同的数据并将其存储在关系数据库中,方便其随机访问,在Elasticsearch进行全文搜索,在Hadoop中做柱状格式分析,并以非规范化格式在memcached中缓存。
当你需要整合不同的数据库,缺乏Unix风格的组合性对于整合来说是一个严重的限制。(我已经完成了从Postgres中用管道将数据输出到其他应用程序,但这还有很长的路要走,直到我们可以简单地用管道将任一数据库中的数据导出到其他数据库。)

LinkedIn,ApacheKafka,Unix

我们说Unix工具可组合性是因为它们都实现相同的接口——stdin,stdout和stderr——它们都是文件描述符,即:可以像文件一样读写的字节流。这个接口很简单以致于任何人都可以很容易地实现它,但它也足够强大,你可以使用它做任何东西。
因为所有的Unix工具实现相同的接口,我们把它称为一个统一的接口。这就是为什么你可以毫不犹豫地用管道将gunzip数据输出WC中去,即使开发这两个工具的作者可能从来没有交流过。这就像乐高积木,它们都用相同的模式实现节位和槽位,让你堆乐高积木的时候能够随心所欲,不用管它们的形状,大小和颜色。

LinkedIn,ApacheKafka,Unix

Unix文件描述符的统一接口并不仅仅适用于输入和输出的过程,它是一个非常广泛的应用模式。如果你在文件系统上打开一个文件,你将得到一个文件描述符。管道和Unix套接字提供一个文件标识符,这个标示符能够在同一机器上为其它程序提供一个通信通道。在Linux中,/dev下的虚拟文件是设备驱动程序的接口,所以你在这里面可以跟USB端口甚至GPU打交道。/proc下的虚拟文件是内核的API,但是它是以文件形式存在,你可以使用相同的工具,以普通文件的方式访问它。
即使是通过TCP连接到另外一台机器上的程序也是一个文件描述符,虽然BSD套接字API(最常用来建立TCP连接)不像Unix。Plan 9显示,即使是网络可以被完全集成到相同的统一接口中去。
可以做这样一个类比,所有东西在Unix里都是一个文件。这样的统一性从逻辑上来说将Unix下的工具就像一根线分成了很多段,使其更加能够灵活组合。 sed 根本就不需要关心与其交互的是一个管道还是其他的程序,或者一个套接字,或者设备驱动程序,又或者是一个真正在文件系统上的文件。因为这些都是一样的。

LinkedIn,ApacheKafka,Unix

一个文件是一些字符流,或许在某个位置会有文件末尾(EOF)的标识,这就说明这个字符流到此为止了(字符流可以是任意长度,因此程序不能提前预知这个输入流有多长)
一些工具(如gzip)纯粹是操作字节流,而不关心数据的结构是什么样子。但是大多数工具需要对输入流进行转码,以便能做更有用的事情。为此,大多数Unix工具在一行的每个记录上,在制表符或空格或逗号分隔的区域上使用ASCII码。
如今,这种字节流文件显然是一种很好的统一接口的体现。然而,Unix的实现者对文件的处理却是截然不同的。例如,他们也许用函数回调接口处理方式,使用一个事务在进程与进程之间传递记录。或者他们用共享内存的方式(像之后的System V IPC ommap一样)。又或者使用比特流而不是字节流的处理方式。
在某种意义上讲,字节流是能够达统一的最低标准— —可能是最简单的接口。一切都可以用字节流来表示,但是对于传输媒介来说根本不知道它是什么(与另一个进程连接的管道,磁盘文件、TCP 连接、磁带等等)。这也是一种劣势,我们稍后再讨论这个问题。

LinkedIn,ApacheKafka,Unix

我们看到Unix为软件开发带来了很多很好的设计原则,而数据库系统走的却是另一条大道。我很高兴能够看到这样一个未来:我们能从这两家学习到各自的核心思想,然后将它们结合起来。
那么怎么样把Unix哲学运用到21世纪的数据系统中,使其变得更好呢?在接下来的内容中,我将探索数据库系统的世界到底会变成什么样。

LinkedIn,ApacheKafka,Unix

首先,让我们承认,Unix并不完美。尽管我认为简单,统一接口的字节流是非常成功的,这使得这个生态系统拥有灵活性,可组合性,以及拥有功能强大的工具,但Unix也有一定的局限性:
1.  它只能在单一机器上使用。随着应用程序需要处理更多数据和流量,并要求更高的正常运行时间,因此分布式系统将成为必然趋势。虽然TCP连接似乎能够被当成文件处理,但我不认为这是合理的方案:因为这只在双方连接都已经打开的情况下工作,而且这里还有一点语义混乱的味道(somewhat messyedge case semantics)。
纵然TCP很好,但作为分布式管道的实现,它过于低级了。
2. Unix中管道被设计成只有一个发送者进程和一个接受者进程。你不能通过管道将输出发送到多个进程,或者从几个进程中收集输入。(你可以用tee分支一条管道,但一个管道本身就是一对一。)
3. ASCII文本(或者,UTF-8)能够很好使数据更加可控(explorable),但这很快就会变得很糟糕。每个进程需要给各自的输入进行转码:首先,将字节流分解成记录(通常通过换行符分隔,当然有人主张用 0x1e-ACSII记录分割器)。然后将记录再分解成各个域,就像在前文awk提到的$7。出现在数据中的分隔字符需要以某种方式进行转义。即使是一个相当简单的工具如xargs,约有大半的命令选项来确定输入需要怎样进行解析。基于文本接口使用起来相当好,但回想起来,我敢肯定,更丰富的数据模型,清晰的事务模式会更好些。
4. Unix处理进程通常不能长久的运行。例如,如果处于管道中间的处理进程崩溃了,那么没有办法从当前输入管道恢复,使得整个管道任务失败,必须从头开始运行。如果这些命令运行只有几秒钟那是没有问题的,但如果一个应用程序预计需要连续运行多年,这就需要更好的容错能力。

我想我们可以找到一个解决方案,既克服这些缺点,又传承Unix哲学。

LinkedIn,ApacheKafka,Unix

最令人兴奋的事是,这样的解决方案其实早就存在,那就是这两个开源项目——KafkaSamza,它们协同工作能够提供分布式流处理服务。
你也许在这个博客其他文章中已经了解到这两个项目,Kafka是一个可扩展分布式消息代理,而Samza是一个框架,这个框架让你的代码能够生产和消费数据流。

LinkedIn,ApacheKafka,Unix

事实上,当你用Unix标准去剖析Kafka,它看起来很像一个管道——将一个进程的输出与另一个进程的输入相连。Samza看起来更像一个标准的库,这个库可以帮助你读stdin流和写stdout流(还有一些有用的功能,如部署机制,状态管理,度量工具(metrics)和监测)。
Kafka 和Samza中,流处理任务的风格,有点像Unix传统的精简且可组合的工具。
1.  在Unix中,操作系统内核提供了一个管道,一个进程从另一个进程中获取字节流的传输机制。
2.  在流处理中,Kafka提供了发布-订阅流(publish-subscribe streams),一个流处理任务能够从另一个流处理任务获取消息的传输机制。

LinkedIn,ApacheKafka,Unix

Kafka解决了我们前面讨论过的有关Unix 管道的缺点:
1. 单机限制被解除:Kafka本身就是分布式的,并且任何使用它的流处理器也可以分布在多台机器上。
2. Unix管道连接一个进程的输出与一个进程的输出,而Kafka流可以有多个生产者和消费者。多输入对于在多台机器上分布的服务至关重要,而多输出使卡夫卡更像一个广播频道。这非常有用,因为它允许相同的数据流独立地、因为不同的目的而被消耗(包括监控和审核的目的,这些往往是外部应用程序本身)。在Kafka中,消费者往往来去都比较自由,而不会影响其他消费者。
3. Kafka还提供了良好的容错性:数据被复制到多个Kafka节点,所以如果一个节点失败,另一个节点可以自动接管。如果某个流处理器节点出错并重新启动,那它可以在其最后一个检查点恢复处理操作。
4. Kafka提供的是一个消息流而不是字节流,这个消息流存储了第一步输入的转码状态(将字节流分解成序列化记录)。每个消息流其实是一个字节数组,因此你可以使用你最喜欢的序列化格式来定制你的消息:JSON, XML, Avro, Thrift或者Protocol Buffers,这些都是合理的选择。将一种编码标准化是非常有意义的,Confluent为Avro提供了一种非常好的架构管理支持。这使得应用程序能用有意义的字段名称的作为处理对象,不必担心输入解析或输出转义。它还提供良好的事务推进支持而不会破坏兼容性。

LinkedIn,ApacheKafka,Unix

关于Kafka与Unix管道还是有一些不同的,这里指的提一提:
1. 上文提到,Unix管道提供字节流,而Kafka提供的是消息流。特别值得注意的是,如果有多个进程同时对相同的流进行写入:在一个字节流,来自不同作者字节流可以交叉存取,将导致出现无法解析错误。因为消息具有粗粒度(coarser-grained)和自包含(self-contained)的特性,它们就可以安全地进行交叉存取,使其多个进程能够安全地同时写入相同的流。
2. Unix管道只是一个较小的内存中的缓冲区,而Kafka持续地将所有消息都写入到磁盘。在这方面,Kafka不太像一个管道,更像是一个写临时文件的进程,而其他几个进程不断地读取这个文件,通过的尾缀-f(每个消费者独立的跟踪该文件)。Kafka的这种方式提供了更好的容错性,因为它允许在消费者出错并重新启动的情况下,不跳过消息。Kafka自动能将这些“临时”文件分割成段,并能在日程配置里配置旧段垃圾收集计划。
3. 在Unix中,如果在管道中的消费进程读取数据非常缓慢,导致缓冲区已满并阻塞了管道中的发送进程。这是背压式(backpressure)的一种。在Kafka中,生产者和消费者都更解耦: 慢消费者有输入缓冲池,所以它不会使生产者或其他消费者慢下来。只要缓冲区在Kafka的可用磁盘空间内,较慢的消费者也能后来居上。这使得系统对个别缓慢组件的不太敏感,而组成更强大的整体。
4. 在Kafka中数据流称为一个topic,你可以参考它的名字(这使它更像Unix中被称之为的管道pipe)。一个Unix程序管道通常是运行一次,所以管道通常不需要确切的名字。另一方面,一个长期运行的应用程序随着时间的迁移通常有比特添加,删除或替换,所以你需要名字,以告诉系统你想连接到哪里。命名也有助于检索和管理。
尽管有这些不同,我仍然认为Kafka是作为分布式数据的Unix管道。例如,他们有一个共同点是,Kafka让信息有一个固定的顺序(就像Unix管道,使字节流有一个固定的顺序一样)。对于事件日志数据,这是一个非常有用的属性:事件发生的顺序通常是有意义的,这需要保护好。其他类型的消息代理,像AMQP和JMS,就并没有这种有序性。

LinkedIn,ApacheKafka,Unix

所以我们知道Unix工具和流处理器看上去十分相似。都是读相同输入流,然后以某种方式修改或改转换它,并产生一个输出流,从某种程度上这都来自于输入。
更重要的是,处理工作不修改输入(input)本身:它仍然是不可改变的。如果你在相同的输入文件上运行AWK,该文件还是处于未修改的状态(除非你明确选择覆写它)。同时,大多数Unix工具是确定的,即如果你给他们同样的输入,他们总是产生相同的输出。这意味着你可以重新运行相同的命令,想多少次就多少次,然后逐步迭代成你想做的工作程序。这是个很棒的实验,因为如果你混乱地进行处理,你还是可以随时返回到你的原始数据。
这种确定性和无副作用的效果处理看起来很像函数式编程。这并不意味着你必须使用象 Haskell 那样的函数式编程语言 (如果你想这么做的话也非常欢迎),但你仍然能从函数式代码中收获很多。

LinkedIn,ApacheKafka,Unix

这种类Unix设计准则的Kafka,使其能够构建一个大型的可组合的系统。在大型的公司中,不同的团队能够通过Kafka发布各自的数据。每个团队能够独立的开发和维护处理任务——消费各种流和生产新的流。因为一个流可以有很多独立的消费者,产生一个新的消费者不需要事先协调。
我们将这种思想成为流数据平台。在这种架构中,Kafka数据流扮演的是不同团队系统沟通的通道。每个组在整个系统中只是负责自己的那部分,并且将这一块事情做好。正如Unix工具能够被组合而完成数据处理任务一样,分布式流系统也以被组合成一个超大规模的处理组织
Unix 方法是通过降低耦合性来控制大系统的复杂性: 多亏了流接口的统一性,每个部件能够独立的开发和部署。由于良好的容错性和管道的缓存性 (Kafka),当问题发生在系统的某个部分时,它仍然只是局部。并且策略管理允许对数据结构作出更改使其更加安全,以便每个团队可以加快脚步而不打乱其他团队的步伐。

LinkedIn,ApacheKafka,Unix

为总结全文,让我们思考下发生在 LinkedIn的 一个真实的例子。如你所知,公司可以在 LinkedIn 上发布他们空缺的职位,求职者可以浏览并申请这些职位。那么,如果 LinkedIn 会员 (用户) 查看了这些发布的职位,会发生什么?
知道谁看过哪些职位非常有用,因此该服务会处理职位浏览记录,随即发布一个事件给Kafka,类似于“会员123在789时刻浏览了编号为456的职位”。
现在这些信息都在Kafka里了,那么它将被用来干好多有用的事情:
1. 监视系统:公司用LinkedIn发布他们空缺的职位,因此确保该网站能够正常的工作很重要。如果职位浏览率意外地骤降,那么就应该给出警示,因为这暗示着这里存在问题,需要展开调查。
2.相关推荐:持续给用户看同样的一种东西,那他会很恼火,因此跟踪并统计用户浏览哪些职位和次数的是一种好的做法,这样就能将这些数据给评分程序。持续跟踪哪些用户浏览了什么也能够对推荐进行协同过滤(用户既浏览了X,也浏览了Y)。
3.防止滥用:LinkedIn并不希望人们能够把所有职位都浏览,然后提交垃圾邮件,或者违反网站服务条款。知道谁在做什么是检测和阻止滥用的第一步。
4.职位海报分析:发布职位空缺的公司希望看到统计数据(谷歌分析的一种方式),谁正在查看他们的帖子,例如,他们可以测试哪些措辞能够吸引最佳候选者。
5.导入到Hadoop和数据仓库:可以是LinkedIn的内部业务分析,可以为高层管理人员的提供向导,用于处理数字数据——能在华尔街进行发布,用于A / B测试评估,等等。
所有这些系统都是复杂的,由不同的团队来维护。Kafka提供了一个可容错,可扩展式的管道。基于Kafka的数据流的平台,允许所有这些不同的系统能够独立开发,并以强大的方式连接和集成。
如果你喜欢这篇文章,你将同样会喜欢由O’Reilly出版的Designing Data-Intensive Applications
感谢 Jay Kreps, Gwen Shapira,Michael Noll,Ewen Cheslack-Postava,Jason Gustafson, 和Jeff Hartley 为这篇文章的初稿提出了意见和建议,也要感谢Jay提供了“LinkedIn职位浏览”的这样一个例子。

转:从开发人员到管理者,我学到了什么

技术工程师的晋升之路

技术工程师的晋升之路大体上可以分为两个方向,一是在技术方向上作为Individual Contributor(IC)继续深造,二是以管理者的身份管理工程师团队。IC这条路比较单纯,能走多远主要取决于个人的技术实力与经验。与IC相比,转型为管理者可以说是一种颠覆性的变化。管理者的工作不再是以个人或项目的成功作为主要目标,而是专注于整个团队的成功。这种目标的变化将直接改变管理者的心态与工作方式,以及与他人的互动方式。可以说,向管理者的转型是一个具有更大挑战性的选择。

David Haney来自于Stack Overflow团队,他是开源软件dache的作者,也曾在InfoQ上发表文章。今年二月,Haney被提升为工程经理。在担任经理的几个月中,他感受到了许多完全不同的挑战。在近期的一篇博文中,Haney详细地记录了这段时间以来所领悟的管理方法心得。

信任第一

Haney认为:要成为一个成功的管理者,首要条件就是取得团队成员的信任。因为没有谁愿意为一个他所不信任的人工作,这一点往往是员工离职的首要因素,其重要性显而易见。但信任不是凭空得来的,你必须表现出你是一个值得依赖的管理者。在这一点上Haney有一定的优势,因为他已经在作为团队成员的一年时间内凭着良好的表现博得了大家的信赖。如果没有共同工作的经历,那么就需要更大的耐心去逐步建立你的信任度。

为了取得团队的信任,Haney特别列举了以下几个要点,这是作为一名优秀的管理者最重要的品质:

  • 与员工之间的对话是一种隐私,不要在人背后嚼舌根。
  • 保持一种开放的姿态,让员工感到可以无所顾忌地与你谈论任何话题。
  • 一言九鼎,说到的就要做到。
  • 不要承诺一些你办不到的事,较好的说法是:“我尽力而为,但无法保证一定能做到。”
  • 绝对不要撒谎!员工不仅会失去对你的信任,而且一见到你就会远远躲开。
  • 不要逃避困难,即使有些事情确实令你感到不舒服,但逃避只会令问题失控。
  • 不要对员工宣泄你的情绪,也不要在背后指指点点。如果确实需要宣泄一下压力,也应当寻找一种合适的渠道,而不是冲着你的员工发火。
  • 公开表扬,私下批评。不要在整个团队面前让人感到难堪。
  • 保持客观与公正,不要在团队中搞特殊化待遇。

人类无法像代码一样伸缩自如

作为开发者,Haney能够写出具有高度可伸缩性的代码。但一名管理者却无法像代码一样做到伸缩自如。正如你很难在20个与会者之间达成一致一样,要有效地管理好20个员工也是一件难以完成的任务。Haney认为,向管理者直接汇报的员工数量最好在4至10人之间。一旦超出了这个数量,你就无法对每个人都给予足够的关注,这会让员工感到被忽视,乃至怀疑自己在团队中的重要性。同时过多的人反而会降低工作效率,因此最好是启用另一名管理者来分担你的工作。

将处理人际关系作为工作重心

对于开发者来说,人际关系往往会对工作产生干扰,而成为一名管理者之后,这一点就成为了你的工作重心。在Haney看来,人际关系不仅限于你的团队,还包括与其它团队打交道,这同样也应当具有较高的优先级。同时应当尽量表现出一定的灵活性,以配合其它团队的时间安排。

另一项重要的工作是与员工定期进行一对一的谈话,让员工借此机会表达他们的想法、意见、困难,并且通过员工的反馈改进管理者的工作。Haney每隔三周就会与每位员工进行一次45分钟的谈话,在他看来,这对他的工作产生了很大的帮助。

要注意的是,如果无故地推迟或取消原定的谈话,会令员工感到遭受轻视。因此要尽可

能避免这种情况的出现,它会使员工的士气受到很大的影响。

接受令人不安的谈话

在Haney看来,当你还是一位开发者的时候,你可能会时常与其他人抱怨一些令你感到不爽的人或者公司政策,这很正常。而作为经理,你的工作就是帮助他们直接处理好这些问题。你应当鼓励员工之间开诚布公地对话,而不是与同事在私底下嘀咕。同时,优秀的管理者也应当为员工树立一个良好的榜样。

如果有某位员工直接找到你投诉某人,你应当做的是让双方坐到一起,让他们坦诚地表达他们的感受。然后居中调解,以解决他们的问题。这种对话开始时往往会令人感到尴尬与不安,但你必须接受它,并让它成为团队文化的一部分。Haney认为,经过数次这种对话之后,员工就应当能够做到在无需他参与的情况下自行开展这种对话。重要的是确保问题最终能够得以解决,而不是随着时间的推移而变得越来越严重。

衡量你的成功并不容易

开发者有一个很大的优势,他们的工作都是为了解决某个特定的问题,其范围总是确定的,并且其成果总是能够很快地显现出来。与之相比,管理者的工作似乎没有那么具体,因为这些工作的影响往往是无形的,例如员工的想法、感受、软技能以及他们的总体成长。并且这些工作没有一个具体的开始与结束时间,因为学习与进步是永无止境的。

Haney认为,管理者的任务总是与帮助员工成长有关,而这些工作的成就感通常来自于他人的反馈。比如有员工对于你的建议与反馈表示感谢,或是有人告诉你他看到团队产生了积极的变化,以及来自于团队外部的称赞等等。这些反馈能够有效地表现出你的工作是否达到预期,但管理者需要保持足够的耐心。

没有人了解你的实际工作

开发者的工作是非常透明的,其他人可以很清楚地了解你的工作内容和完成的任务,代码库里的签入记录也会清晰地反映出你的工作。与之相对的是,管理者总是在做一些幕后的工作,这很容易给别人造成一种印象:这家伙根本什么事都不干!而Haney认为,这种误解是因为他与员工的谈话往往是私下进行的,他坚持通过一对一的形式与员工谈论沟通技巧以及人际关系上的问题。由于这种交流的私密性,使其他人无法了解管理者的工作情况,因此产生了管理者不干活的误解。

在Haney看来,只有通过团队成员的各方面不断提高,才能够表现出管理者的工作价值。这是一个缓慢的过程,但当团队发现团队之间的人员互动正朝着积极的方面转变时,他们就会体会到管理者的辛勤工作。随着员工的成长与进步,团队将变得愈加强大与高效,员工们也会承认你为团队所做出的贡献。

你的成功就是他人的成功

在这篇博文的最后,Haney简单地总结了他的观点。管理工作最重要的是取得信任,通过直接的对话解决问题(虽然这种对话有时会令人不安),在幕后完成你的工作,同时保证工作方式的直接与公正性。管理工作具有很高的挑战性,同时也能够带来极大的回报。

最后,Haney引用了动画“飞出个未来”中的一名名言,它非常贴切地表现出了管理者的工作特性。

“如果某件事你做的足够出色,那么人们甚至不会感觉到它。”

 

转自 infoq http://www.infoq.com/cn/news/2015/08/developer-to-manager

6行代码实现一个 id 发号器

id 发号器的问题,@一乐 的这篇文章说的很透彻了:http://weibo.com/p/1001603800404851831206 但参考实现就显得有些复杂。最近在雪球工作中正好需要用到发号器,于是用 Lua 在 Redis 上实现了一个最简单的:


-- usage: redis-cli -h 10.10.201.100 -p 10401 EVAL "$(cat getID.lua)" 1 XID:01:02
local arg = KEYS[1]
-- project id must has 2 digits, 01 - 15
local pid = tonumber(string.sub(arg, 5, 6))
-- instance id must has 2 digits, 01 - 15
local iid = tonumber(string.sub(arg, 8, 9))
local idnum = redis.call("INCR", "ID_IDX") % 65536
local sec = redis.call("TIME")[1] - 1420041600
return sec*16777216 + pid*1048576 + iid*65536 + idnum

解释:

  • id 总长度 52bit,为了兼容 js,php,flex 等语言 long 类型最长只能 52 bit
  • 最高 28 bit 为秒级时间戳,因为位数限制,不能从 1970.1.1 开始,-1420041600 表示从 2015.1.1 开始,大约可以使用10年(3106天)
  • 接下来4个bit为 project id,客户端传入,区分业务
  • 再接下来4个bit为 instance id,HA 用的,支持最多16个instance。如果业务只需要“秒级粗略有序”,比如发微博或发评论,则可以多个 instance 同时使用,不需要做任何特殊处理;如果业务需要“因果有序”,比如某个user短期内快速做的多个操作必须有因果顺序(程序化交易,必须先卖再买,几个毫秒内完成),那么就需要做一些特殊处理,要么用 uid 做一致性hash,或者像我们这样偷懒:一段时间内固定使用一个 instance
  • 最低16个bit是 sequence id,每个 instance 支持每秒 65535 个 id。bit数再大,redis 该成为瓶颈了。
  • twitter和微博都是把 instance id 写死到 server 端,这样 server端就变成有状态的了:16个instance,每个 instance 都与其它的配置不一样。在雪球我们不希望 server 端有状态,于是设计成 instance id 由客户端传入,server 端退化成一个普通的 redis cache server (不需要 rdb,不需要 aof,宕机重启即可)
  • 几个注意点:宕机不能自动立即重启,必须间隔1秒以上,避免 sequence id 重复导致 id 重复;迁移时必须先 kill 老的 instance,再启动新的,也是为了避免 sequence id 重复。

id 发号器说简单也确实挺简单。任何一个技术点,只要理解了本质,大约都是这么简单罢。

转:Coding 孙宇聪:《人,技术与流程》

我先做一下自我介绍,我是 07 年加入的 Google,在 Moutain View 总部任Google SRE,今年年初回国加入 Coding。

在 Google 我参与了两个 Project, 第一个就是 Youtube, 其中包括 Video transcoding, streaming 等,Google 的量很大,每个月会有 1PB 级别的存储量,存储,转码后,我们还做 Golbal CDN ,最大的时候峰值达到 10 TB,我们在全球 10 万个节点,每台机器都是 24 核跑满状态。然后我从 Youtube 团队离开加入 Google Cloud Platform Team。我们做的主要工作是管理 Google 全球的机器,大概有 100 万台左右。我离开 Google 之前做的就是 Omega Project,一个集群管理系统,管理 Google 整个云平台的任务调度,协作。可能很多人会说“然并卵”,因为这是都是国内不存在的网站。(笑)

 

从 Google 回来在 Coding 做 CTO,对我来说也是人生的一个大改变。最近我在知乎看到一个好问题,“从大公司离开到小公司当 CTO 是怎样的体验”,我摘抄了一个好答案:“ 顶着 CTO 的名号,招聘,培训,鼓励程序猿,拉网线,查机房,装系统这都是 CTO 要做的事;讨论方案,推方案,定方案,确定进度,拖延进度,安抚程序猿,挨老板骂,安抚老板,这也是 CTO 的职务。” 不包括 Coding,而我的工作还包括 Coding,很伤心。(笑)

 

所以我把这个问题作了归类:

 

CTO 是什么:

第一、他在公司里是一个鼓励师,(我这个形象做鼓励师其实也不是很合适)

第二、可能是网管,这个我确实干过,接网线,架服务器,

第三、可能就是受气包,程序员对我很不满,老板对我也很不满。

 

那我们把这三个角色做为一个对应:

 

第一、就是研发人员上的管理,包括怎么招人合适,怎样让这些人更好地协作;

第二、是研发技术和研发环境的管理,就是你有了这些人怎样让大家更好地协作在一起,更高效地开发;

第三、就是研发流程的管理,怎样让公司这台机器转得更加灵活,顺畅。

 

那这里我提出了三点要素,人,技术与流程,这是一个公司研发体系中不可缺少的关键要素。

 

那我们就先来说一说人,创业公司都需要全栈工程师。这个全栈工程师长的什么样子呢?一般来说他就像是一个大侠,踩着白马,手持金枪,什么问题都能解决,一人单挑好几百人。可是有些时候我们招不到全栈工程师,我们就招了一些全干工程师,这也是我们最近发明的一个词汇。这个人就是他什么都干,写完网站后写 iOS 最后写安卓。这样的人也很难找,更难的是把这些招来的人员合理地安排在一起,合理地组织起来协作。

 

要想解释 Coding 在研发人员管理上的演变,我首先讲一下 Coding 的服务架构,这是我们去年 5 月份(第一版本上线) 的服务架构,非常简单只有一个 core 程序,经过一年多的演变,现在变成了这个样子。我特意写的字很小,因为不想让大家看清楚,这里面其实错综复杂也不一定对。一个简单的架构是如何一步步演变复杂的呢? 这里我想先介绍一个康威定律,就是

 

“任何一个设计系统的组织,最终产生的设计都等同于组织之内、之间的沟通结构”。

 

回想起 Coding 以前的交流方式,老板说”我们要做一个新的功能”,大家就开始把功能拆,前端需要怎么改,要改谁来做,后端要不要改,改的话谁来,服务层,DB 层,测试,部署,每个人做的事情都不一样,每个人都只做一件事情,每个人都做流水线上的一个螺丝钉。这样会造成什么情况呢?前端程序员在等后端程序员,什么时候把接口接上了再开工,后端程序员在等数据库,什么时候加上这个字段我再开工,测试和部署就更在等了。每次一开项目进度会议时,老板问功能做的什么程度了,前端说后端没写好接口,后端说数据库还没搞定,经常是这个状态。

 

那么 Coding 今天采用了一个组织方式,我们叫他全员全栈,这个全栈的意思和普通意义上的全栈意义不是很一样,这个全栈我们指的是产品,功能上的全栈。具体是怎么实现的呢?其实对任何一个比较合格的程序员来说,语言并不是他的瓶颈,也不应该作为他的限制条件。我们更希望的是我们公司里更多的是这种全栈工程师。我们在做任何功能的时候,我都会和这个人说你要负责这个功能从头到尾的实现,你需要改前端就改前端,需要改后端就改后端。做这个事情可能刚开始有一定难度,后端工程师对前端不是很熟悉,前端工程师对后端不是很熟悉,那我们就用一些其他方法去克服它。这个过程就强迫了组织内部的 Knowledge sharing

 

很多大公司的工程师可能只关心这一小块,比如说前端工程师只写了这一点前端的东西,后端我一概不管,原因是我不敢动,大家也不让我动。在小公司里面,每一个人都要了解公司的使用的各种技术,我们共同努力去降低系统的复杂度。 全员全栈是我们公司的一个战略方向,我们希望每个人都有主人翁的意识,在做需求的时候他可以从前到后一直跟到完,但是想要把全员全栈做好就要做研发环境和研发工具的调整,接下来我们讲我们是如何通过技术手段来协助的。

 

技术手段在一个研发环境里有三个要素:

第一个就是代码如何管理,如何操作

第二个就是运行,前端后端代码运行起来要求繁杂,如何调节。

第三点就是你写好之后怎么部署、运维。开发环境和生产环境经常打不通。

 

为了代码这方面讲好,我先来给大家介绍下在 Google 当码农是一种怎样的体验。Google 最强大的就是他的研发体系经过了 10 多年的积累和锤炼,现在是一套非常高效的体系。那这个“不存在”的公司是怎样管理自己的代码呢?

 

第一点,整个公司只有一个仓库,一个版本控制系统。这个东西说起来简单,其实很难实现的,很多人觉得这个东西好像可行,但他细想难道我们用的都是SVN?很明显不是,那 SVN 没有用,Git 也没用,那如何做到只有一个仓库呢?只有一个仓库带来的问题就是所有程序代码都在一个目录下,如果你有一个特别大的硬盘,你把整个公司全都 down 下来,他就是一个目录下的很多子目录,可能几十级子目录,可能整个公司代码有几百 G。那如何同步,管理,操作,这个都是 Google 的独门秘方。

 

这个有什么好处呢?

第一,你可以看到任何人的代码,你可以看到这个服务是如何实现的,为何会出现异常,会有什么错误;

第二,它带来一种更高效的复用方式,比如我写了一个小程序,我 Gmail 的一个程序用了一段 bigtable 的代码,那我直接在代码里引用过去即可,反正大家都在一个目录下,编译环境也一样,有了这个 repo 之后,还有一套编译系统,这个编译系统能做到一键编译任何程序,我运行一个 Gmail 也是一行程序,一个命令是就是编译程序 build 加上 Gmail 的路径即可编译,编译 Go,Python,C++全都是一个命令,不用你去关心底层是如何实现的,运行起来即可。有了这个之后就做到了部署,开发很容易,就是说如果我写业务代码需要用到一个服务,那我可以很容易地启动这个服务,还可以对该服务做小改动后再次运行起来,这都很容易。

 

这里说多了大家又会说:道理我们都懂,但是如何实现呢?

 

那 Coding 怎么组织我们的代码呢?因为我们确实没有 Google 集中式的代码服务,为了调整我们的代码结构,让大家更好地复用,引用彼此的代码,又因为我们本身提供 Git 服务,那我们也是用了 Git 仓库的方式,但我们每个项目都是一个独立的仓库,比如我们前端代码放一个仓库里,后端代码放一个仓库了,各个服务都放在不同仓库里。

 

这种方式就产生一个问题,如何同步?

 

我们用的是 Google 开源的东西叫 Android repo,大家做安卓开发时可能用到过,这个 repo 是什么意思呢?就是他定义了一个 Workspace,这个 Workspace 有一个固定的格式/结构,然后你统一用 repo 这个工具去 sync,这个 repo 应该到哪个 commit ,那个 repo 应该到哪个 commit,全公司都用同样一种 Workspace 的方式,就保证了每个人看到的代码都是一致的。

 

做了这个代码结构定义的好处就是:

第一,我们有代码阅读功能,比如我们 Coding 的代码阅读可以根据这个 Workspace,可以看到每个项目的代码,互相引用的状态;

第二就是质量分析,你可以针对这个 Workspace 做质量分析。这个 Workspace 有一个 repo.sh 这个是一个命令,比较关键的是有一个 default.xml 是这个命令的配置文件,然后大家打开 Workspace 运行 repo sync 即可,它会自动更新每个组件到最新版本。我们也鄙视 xml 但是没办法,这个程序就是这么写的,其实很简单,里面主要定义了很多 Project,这个 Project 可能是代码仓库的路径映射到了本地的路径,它有一个比较先进的功能就是它可以同时 sync 好几个 Project,就是你一敲 repo.sh sync -g=4 就是开 4 个线程去同步。实践起来这个是很容易的,有了这个东西后打开了这个局面,为大家下一步搞开发环境做了基础铺垫。

 

工具有了,那我们真正想要的开发环境是什么呢?

 

我从 Google 的得来的感触就是我们想要一个统一的,代码化的,可复制的,可重现的一个开发环境。

 

第一、每当新同事来公司后,他带着自己的电脑或用公司的电脑,他所用的工具是不一样的,环境是不一样的,怎样能让公司的代码在自己机器上跑起来其实是一件比较困难的事情。大家的解决办法可能是通过写文档,但这是一件非常痛苦,浪费时间的事情。

 

第二就是说如果你这个开发环境如果不可以复制,重现,那你的自动化测试怎么做?不能说某个人手动配一下,跑一下,今天挂明天又改吧?

 

那如何做到统一化的开发环境?我们做法就是定义了一个通用的接口,我认为这个编译系统的内部实现是无所谓的,怎样都可以,但它的接口比实现要重要一百倍。大家可以想下如果你每个程序,每个组件都是用同一个方法去 Build,去运行,这是一种怎样的体验?我们定义了一个 build.sh 和 package.sh ,这个 build 就是说我用 Java,Python,Ruby也好,我就定义 build.sh 它最后能产生一个结果能把这个东西 build 好,对我来说我不关心下面的程序怎么来写,但我只要 build 它,我改了一行代码它能够 build 出新的东西即可。这是我们现在比较土的做法。

 

Google 最近开源了 bazel ,它是用 Java 写的编译工具,它实际上就是 build 命令,之后两个反斜杠代表整个 Workspace 的根,然后 coding 是一个 project,server 是 project 的 target。有了这个东西之后其实你 build 任何项目你都考虑的是说它逻辑上的分层而不是物理上的分层,逻辑上的分层就是我要 build 一个 coding server,那这个 coding server 可能里面引用了其他的第三方库,头文件,Ruby 程序,Java 程序都无所谓,我只要说我能 build 一个 coding server 来即可,那这个 bazel 更好的就是它可以自动处理递归依赖,就是你这个 rule 可以依赖到另外一个 rule。

 

有了这个编译后我们还需要有可复制,可重用的开发环境,那这个开发环境我们怎么做的呢?

我们用的是 Vagrant 和 docker。Vagrant 是 VM 的一个管理工具,它可以生成一个新的 VM 来,这个 VM 使用代码来定义的, 然后把每一个服务作为一个 docker 服务在 VM 来运行的。

 

Vagrant 其实做了三件事:

第一件事是它从一个指定的地方下载 Base Box,Base Box 是我们自己做的,比如说是一个 Ubuntu 镜像加一些本地依赖.

第二就是它支持脚本定义,你可以运行 shell 脚本来定制,再选一个所谓的 Provider,这个 Provider 就是你可以比如说本地是 Virtual Box 的 Provider,远端和很多云厂商可以对接。

 

进行完这两个操作之后,他就产生了一个 VM,这个 VM 就是一个你可以一键 ssh 进去,它自动把你所有东西都搭配好,这是一个属于你的开发环境,那有了这个东西之后,整个公司可以有一套一致的开发环境,因为是一个 VM,它在哪台机子上运行的方式都一样,所有的依赖库都可以放进去,所以最后的结果就是我们有一个几G的镜像放在我们内网里,每个新同事来了后我们让他装一个 Vagrant 或 Virtual Box,然后他敲一个命令,自动把镜像下载,启动,然后一键把整个 Coding 的项目在他的机器上跑起来。

 

做到了这种可重用的开发环境,我们还做到了所谓的一键运行。一键运行是和开发另外一个层面的东西,它既不关心这个东西是怎么构建出来的,我只关心我启动这个服务,比如 Coding 开发环境的时候我需要哪些服务,那我们采用的就是一个自己写的脚本来给 Docker 做编排,这个配置文件里定义了一些 Job,每个 Job 有一些 image(运行程序的版本、环境变量等)。其实很多时候我们都是用 go run 命令一键启动很多服务和镜像,达到了我们刚才说到的统一的代码化的,可复制,可重现的开发环境。

 

技术工具是我最怀念 Google 的地方,因为这些工具带来很多好处: 首先他鼓励公司内部的协作, 每个人写程序时不是首先想到自己搞一个小东西出来,我们更多的时候是去看公司里面其他人做了一个什么东西,它是怎么实现的,能不能引用过来,能不能使用,能不能把常用的类库都抽离出来;

第二它可以让新人很快上手,我们新同事也好,老同事也好,更多的场景是老同事换 Project,以前可能只是使用者,瞬间就可以变成开发者。开发环境的无缝切换无形中降低了很多公司运行的成本。

第三点就是它让自动化变为可能。刚才我们是装了 Vagrant 执行一个命令就搞定了,这个东西同样可以作为自动化的东西来实现,比如每次我们在发 Code Review 时,我们后台就可以自动启动一个新的 VM,把所有东西下载后运行测试,最后给出改动对错,造成影响等结果,这都是可以自动化的。这点我觉得是一个关键点,接下来有了这个环境之后你有了质量分析,工具后它才能够进行,才能有更多生产力工具接入进来。

 

最后我再讲一讲流程的管理。

 

作为一个 CTO,我们小小的梦想是持续交付更好的软件。老板说你作为一头老黄牛就得不停地往前跑,你也能够按时完成任务。

 

公司内部有很多实现方式:第一就是 Code Review, 这个流程工具毁誉参半:

 

第一点就是这个 Code Review 绝对是脑抽检测器,每个人都有脑抽的时候,评审期就是缓冲期,让你想一想你真的是要做这件事情吗?你让别人帮你检查一下是很有好处的。

第二是Code Review 是一种很好的知识分享的方式。我一个人在写这个功能时我把代码交给别人去看,那另一个人就会对这个有了解,将来他可能来做你的功能,你换过去做他的工作。鼓励公司内部的知识共享是很重要的。

第三它是一个 idea generation ,大家在做知识共享时很容易发现自己另外一个地方也发现了这个问题,我是这么解决的,你为什么不这么解决?有什么更好的办法解决吗?Code Review 为促进交流来做这件事。

 

那 Code Review 做的不好时有哪些方面呢:

 

第一就是程序员鄙视链,就是老程序员对新程序员很鄙视,说你写的东西太次了,我都懒的看。这个是很不好的一种行为,我们一定要避免这种东西。我们做了一些程序上的要求,比如你每次 Review 时必须把整篇文档都看完,不能说我看了一行发现太烂了不看了,你改好了我再来看,这是不允许的。

 

第二我们讲到 Ownership ,就是你在 Code Review 时这个代码属于谁的,我们讲代码评审是谁写代码谁负责,你写了 rm -rf 你就要对此负责,我看了之后觉得你写的有道理,但我没看出 bug, 这是你的问题不是评审者的问题。这是我们在Google,Coding 实践出来的经验,

 

第三点就是拥抱变化,就是我原来可能某员工写了一个程序,他觉得我写的这个程序实在是太好了,你们不要改,一改我就看不懂了。所以他对任何改动都非常抵触,这也是错误的,不论在 Google 还是 Coding ,我们坚持的一大原则就是说我们有 Business case 你就可以改代码。你只能去把这个代码写得更好,不能不允许他改,这个是关键,你所写的代码都是属于公司的,那公司怎么能用代码来做更好的事情呢,你要讲清你的道理,你为什么要改这段代码,改了有什么好处,把这个东西变成了技术上的讨论。而不是一个职责,权限上的讨论。

 

所以我觉得这几点是把 Code Review 做的更好比较关键的几点。接下来说一下 Release Schedule 的问题。

 

因为创业公司和大公司不一样,每个公司都有自己的 Release Schedule,以前我们基本靠吼,今天我们要上线,所以全员就上线,我们今天要 Release 那就 Release,你如果和老板吼今天上不了线,老板想了想那就算了吧,明天上,并不是很严肃。我们在内部推行的改革是我们要把冲刺变成长跑,我们已经创业一年多了,不能总是冲刺状态了。我们要变成长跑,能够持续不断地交付高质量的软件,而不是每天上完线后加班到后半夜才把它改好。

 

方式很简单,就是一周钉死 Release 两次,我说服老板每周两次就够了,三次就多了。Google 一个月才 Release 一次,大项目半年才 Release 一次,我们创业公司能做到这点已经很好了。一周 Release 两次是什么概念,星期一就要上 Staging 测试环境,星期二就要 Release,星期三又要上 Staging,星期四又要 Release,留给大家周五写写程序,修修 Bug。其实一周 Release 两次也很频繁,但是保证现阶段我们的转化率,我们采用了这个方式。把这个东西摊在桌面上来讲有什么好处呢,他让大家可以计划我这个功能下周二上还是下周四上,你和项目经理打仗的时候有理有据了,他说你看你是星期二上还是星期四上,程序员说星期二可能搞不定,那我们星期四来吧,一来二去就把这个冲刺改成了长跑。

 

接下来还有一点,我们要区分 Feture Team 和 Infrastructure Team, Infrastructure Team 也是我们以前谷歌的一个名词。他讲的是什么意思呢?就是说虽然全员都在做业务逻辑,但我们一定要抽出一定时间来推行技术演进,你不能说每天我上来就是写代码,拷贝粘贴,这样搞得大家所有东西都非常混乱。我们在内部提出了三个点:

 

第一叫 Coding One,就是我们公司内部所有的项目,所有的技术,所有的服务都争取用同一套方式来运行。比如说用同一个 Java 版本。这就是一件很难的事情,我觉得好多公司都做不到,用同一个第三方类库,这也很难,所以用起来类库都不一样,但干起来事情是一样的。这样每个人代码看起来都好像是差不多,但是又不一样,所以 Coding One 就是解决这个问题,就是 Java 版本,第三方库的版本,第三方库的类型,编译方式,运行环境,启动方式,都应该是一样的。这样会降低很多程序员之间内耗,提高大家的效率。

 

第二就是 Coding Two,刚创业的时候大家觉得我们都用一台机器,所有东西都跑在上面,这个东西如果坏了那就全挂了。每次更新大家都觉得太危险了,我们还是后半夜进行吧,十二点还不够半夜,我们三四点钟进行吧。因为你这个东西没有备份,没有任何的灰度。我们叫 Coding Two,以前是从零到一的过程,现在要把从一到二的过程迈过去。从一到二的意思是你这个东西可以是多份的,你这个东西挂了那边还可以继续顶上,这个程序你做发布的时候你可以先发布给自己用,用好了再发给用户,这才是负责任的方式。

 

第三点: Coding CI 是我要讲到的最后一点,最后我们的终极目标,就是所谓的 Push On Green ,意思就是你提交了代码一分钟之内只要所有的测试都跑动过,马上就上生产环境,大家可以想一想在自己公司里能做到这一点吗?如果不能那是为什么?你程序员写了代码,我们认为程序员写的代码都是善意的,你经过了 Code Review,业务流程上都没问题,他为什么不能直接去上生产环境呢?Push On Green 就是终极考验,来考验你这个研发体系能不能做到这一点,如果能做到这一点你才是好的研发体系。

 

这就是我和大家分享的三点,人,技术和流程,我认为这在一个公司里是一个齿轮一样组合起来的,公司我们可以说是一个大机器,如果将机器润滑,将齿轮转得更好,结合得更紧密,这就是我们想追求的目标。谢谢大家。

转自 扣钉Coding 微信公众号

转:iOS应用架构谈 开篇

转自:iOS应用架构谈 开篇  http://casatwy.com/iosying-yong-jia-gou-tan-kai-pian.html

缘由

之前安居客iOS app的第二版架构大部分内容是我做的,期间有总结了一些经验。在将近一年之后,前同事zzz在微信朋友圈上发了一个问题:假如问你一个iOS or Android app的架构,你会从哪些方面来说呢?

当时看到这个问题正好在乘公车回家的路上,闲来无聊就答了一把。在zzz在微信朋友圈上追问了几个问题之后,我觉得有必要开个博客专门来讲讲一些个人见解。

其实对于iOS客户端应用的架构来说,复杂度不亚于服务端,但侧重点和入手点却跟服务端不太一样。比如客户端应用就不需要考虑类似C10K的问题,正常的app就根本不需要考虑。

这系列文章我会主要专注在iOS应用架构方面,很多方案也是基于iOS技术栈的特点而建立的。因为我个人不是很喜欢写Java,所以Android这边的我就不太了解了。如果你是Android开发者,你可以侧重看我提出的一些架构思想,毕竟不管做什么,思路是相通的,实现手段不同罢了。

当我们讨论客户端应用架构的时候,我们在讨论什么?

 

其实市面上大部分应用不外乎就是颠过来倒过去地做以下这些事情:

    ---------------     ---------------     ---------------     ---------------
    |             |     |             |     |             |     |             |
    | 调用网络API  | --> |   展现列表    | --> |  选择列表    | --> |   展现单页   |
    |             |     |             |     |             |     |             |
    ---------------     ---------------     ---------------     ---------------
                               ^                                        |
                               |                                        |
                               |                                        |
                               ------------------------------------------

简单来说就是调API,展示页面,然后跳转到别的地方再调API,再展示页面。

 

那这特么有毛好架构的?

 

非也,非也。 —- 包不同 《天龙八部》

App确实就是主要做这些事情,但是支撑这些事情的基础,就是做架构要考虑的事情。

  • 调用网络API
  • 页面展示
  • 数据的本地持久化
  • 动态部署方案

上面这四大点,稍微细说一下就是:

  • 如何让业务开发工程师方便安全地调用网络API?然后尽可能保证用户在各种网络环境下都能有良好的体验?
  • 页面如何组织,才能尽可能降低业务方代码的耦合度?尽可能降低业务方开发界面的复杂度,提高他们的效率?
  • 当数据有在本地存取的需求的时候,如何能够保证数据在本地的合理安排?如何尽可能地减小性能消耗?
  • iOS应用有审核周期,如何能够通过不发版本的方式展示新的内容给用户?如何修复紧急bug?

上面几点是针对App说的,下面还有一些是针对团队说的:

  • 收集用户数据,给产品和运营提供参考
  • 合理地组织各业务方开发的业务模块,以及相关基础模块
  • 每日app的自动打包,提供给QA工程师的测试工具

一时半会儿我还是只能想到上面这三点,事实上应该还会有很多,想不起来了。

所以当我们讨论客户端应用架构的时候,我们讨论的差不多就是这些问题。

这系列文章要回答那些问题?

 

这系列文章主要是回答以下这些问题:

  1. 网络层设计方案?设计网络层时要考虑哪些问题?对网络层做优化的时候,可以从哪些地方入手?
  2. 页面的展示、调用和组织都有哪些设计方案?我们做这些方案的时候都要考虑哪些问题?
  3. 本地持久化层的设计方案都有哪些?优劣势都是什么?不同方案间要注意的问题分别都是什么?
  4. 要实现动态部署,都有哪些方案?不同方案之间的优劣点,他们的侧重点?

本文要回答那些问题?

 

上面细分出来的四个问题,我会分别在四篇文章里面写。那么这篇文章就是来讲一些通识啥的,也是开个坑给大家讨论通识问题的。

架构设计的方法

 

所有事情最难的时候都是开始做的时候,当你开始着手设计并实现某一层的架构乃至整个app的架构的时候,很有可能会出现暂时的无从下手的情况。以下方法论是我这些年总结出来的经验,每个架构师也一定都有一套自己的方法论,但一样的是,不管你采用什么方法,全局观、高度的代码审美能力、灵活使用各种设计模式一定都是贯穿其中的。欢迎各位在评论区讨论。

第一步:搞清楚要解决哪些问题,并找到解决这些问题的充要条件

 

你必须得清楚你要做什么,业务方希望要什么。而不是为了架构而架构,也不是为了体验新技术而改架构方案。以前是MVC,最近流行MVVM,如果过去的MVC是个好架构,没什么特别大的缺陷,就不要推倒然后搞成MVVM。

关于充要条件我也要说明一下,有的时候系统提供的函数是需要额外参数的,比如read函数。还有翻页的时候,当前页码也是充要条件。但对于业务方来说,这些充要条件还能够再缩减。

比如read,需要给出file descriptor,需要给出buf,需要给出size。但是对于业务方来说,充要条件就只要file descriptor就够了。再比如翻页,其实业务方并不需要记录当前页号,你给他暴露一个loadNextPage这样的方法就够了。

搞清楚对于业务方而言的真正充要条件很重要!这决定了你的架构是否足够易用。另外,传的参数越少,耦合度相对而言就越小,你替换模块或者升级模块所花的的代价就越小。

第二步:问题分类,分模块

 

这个不用多说了吧。

第三步:搞清楚各问题之间的依赖关系,建立好模块交流规范并设计模块

 

关键在于建立一套统一的交流规范。这一步很能够体现架构师在软件方面的价值观,虽然存在一定程度上的好坏优劣(比如胖Model和瘦Model),但既然都是架构师了,基本上是不会设计出明显很烂的方案的,除非这架构师还不够格。所以这里是架构师价值观输出的一个窗口,从这一点我们是能够看出架构师的素质的。

另外要注意的是,一定是建立一套统一的交流规范,不是两套,不是多套。你要坚持你的价值观,不要摇摆不定。要是搞出各种五花八门的规范出来,一方面有不切实际的炫技嫌疑,另一方面也会带来后续维护的灾难。

第四步:推演预测一下未来可能的走向,必要时添加新的模块,记录更多的基础数据以备未来之需

 

很多称职的架构师都会在这时候考虑架构未来的走向,以及考虑做完这一轮架构之后,接下来要做的事情。一个好的架构虽然是功在当代利在千秋的工程,但绝对不是一个一劳永逸的工程。软件是有生命的,你做出来的架构决定了这个软件它这一生是坎坷还是幸福。

第五步:先解决依赖关系中最基础的问题,实现基础模块,然后再用基础模块堆叠出整个架构

 

这一步也是验证你之前的设计是否合理的一步,随着这一步的推进,你很有可能会遇到需要对架构进行调整的情况。这个阶段一定要吹毛求疵高度负责地去开发,不要得过且过,发现架构有问题就及时调整。否则以后调整的成本就非常之大了。

第六步:打点,跑单元测试,跑性能测试,根据数据去优化对应的地方

 

你得用这些数据去向你的boss邀功,你也得用这些数据去不断调整你的架构。

总而言之就是要遵循这些原则:自顶向下设计(1,2,3,4步),自底向上实现(5),先测量,后优化(6)。

什么样的架构师是好架构师?

 

  1. 每天都在学习,新技术新思想上手速度快,理解速度快

做不到这点,你就是码农

  1. 业务出身,或者至少非常熟悉公司所处行业或者本公司的业务

做不到这点,你就是运维

  1. 熟悉软件工程的各种规范,踩过无数坑。不会为了完成需求不择手段,不推崇quick & dirty

做不到这点,你比较适合去竞争对手那儿当工程师

  1. 及时承认错误,不要觉得承认错误会有损你架构师的身份

做不到这点,公关行业比较适合你

  1. 不为了炫技而炫技

做不到这点,你就是高中编程爱好者

  1. 精益求精

做不到这点,(我想了好久,但我还是不知道你适合去干什么。)

什么样的架构叫好架构?

 

  1. 代码整齐,分类明确,没有common,没有core
  2. 不用文档,或很少文档,就能让业务方上手
  3. 思路和方法要统一,尽量不要多元
  4. 没有横向依赖,万不得已不出现跨层访问
  5. 对业务方该限制的地方有限制,该灵活的地方要给业务方创造灵活实现的条件
  6. 易测试,易拓展
  7. 保持一定量的超前性
  8. 接口少,接口参数少
  9. 高性能

以上是我判断一个架构是不是好架构的标准,这是根据重要性来排列的。客户端架构跟服务端架构要考虑的问题和侧重点是有一些区别的。下面我会针对每一点详细讲解一下:

代码整齐,分类明确,没有common,没有core

代码整齐是每一个工程师的基本素质,先不说你搞定这个问题的方案有多好,解决速度有多快,如果代码不整齐,一切都白搭。因为你的代码是要给别人看的,你自己也要看。如果哪一天架构有修改,正好改到这个地方,你很容易自己都看不懂。另外,破窗理论提醒我们,如果代码不整齐分类不明确,整个架构会随着一次一次的拓展而越来越混乱。

分类明确的字面意思大家一定都了解,但还有一个另外的意思,那就是:不要让一个类或者一个模块做两种不同的事情。如果有类或某模块做了两种不同的事情,一方面不适合未来拓展,另一方面也会造成分类困难。

不要搞Common,Core这些东西。每家公司的架构代码库里面,最恶心的一定是这两个名字命名的文件夹,我这么说一定不会错。不要开Common,Core这样的文件夹,开了之后后来者一定会把这个地方搞得一团糟,最终变成Common也不Common,Core也不Core。要记住,架构是不断成长的,是会不断变化的。不是每次成长每次变化,都是由你去实现的。如果真有什么东西特别小,那就索性为了他单独开辟一个模块就好了,小就小点,关键是要有序。

不用文档,或很少文档,就能让业务方上手

谁特么会去看文档啊,业务方他们已经被产品经理逼得很忙了。所以你要尽可能让你的API名字可读性强,对于iOS来说,objc这门语言的特性把这个做到了极致,函数名长就长一点,不要紧。

 

    好的函数名:
        - (NSDictionary *)exifDataOfImage:(UIImage *)image atIndexPath:(NSIndexPath *)indexPath;

    坏的函数名:
        - (id)exifData:(UIImage *)image position:(id)indexPath callback:(id<ErrorDelegate>)delegate;

    为什么坏?
        1. 不要直接返回id或者传入id,实在不行,用id<protocol>也比id好。如果连这个都做不到,你要好好考虑你的架构是不是有问题。
        2. 要告知业务方要传的东西是什么,比如要传Image,那就写上ofImage。如果要传位置,那就要写上IndexPath,而不是用position这么笼统的东西
        3. 没有任何理由要把delegate作为参数传进去,一定不会有任何情况不得不这么做的。而且delegate这个参数根本不是这个函数要解决的问题的充要条件,如果你发现你不得不这么做,那一定是架构有问题!

思路和方法要统一,尽量不要多元

解决一个问题会有很多种方案,但是一旦确定了一种方案,就不要在另一个地方采用别的方案了。也就是做架构的时候,你得时刻记住当初你决定要处理这样类型的问题的方案是什么,以及你的初衷是什么,不要摇摆不定。

另外,你当初设立这个模块一定是有想法有原因的,要记录下你的解决思路,不要到时候换个地方你又灵光一现啥的,引入了其他方案,从而导致异构。

要是一个框架里面解决同一种类似的问题有各种五花八门的方法或者类,我觉得做这个架构的架构师一定是自己都没想清楚就开始搞了。

没有横向依赖,万不得已不出现跨层访问

没有横向依赖是很重要的,这决定了你将来要对这个架构做修补所需要的成本有多大。要做到没有横向依赖,这是很考验架构师的模块分类能力和是否熟悉业务的。

跨层访问是指数据流向了跟自己没有对接关系的模块。有的时候跨层访问是不可避免的,比如网络底层里面信号从2G变成了3G变成了4G,这是有可能需要跨层通知到View的。但这种情况不多,一旦出现就要想尽一切办法在本层搞定或者交给上层或者下层搞定,尽量不要出现跨层的情况。跨层访问同样也会增加耦合度,当某一层需要整体替换的时候,牵涉面就会很大。

对业务方该限制的地方有限制,该灵活的地方要给业务方创造灵活实现的条件

把这点做好,很依赖于架构师的经验。架构师必须要有能力区分哪些情况需要限制灵活性,哪些情况需要创造灵活性。比如对于Core Data技术栈来说,ManagedObject理论上是可以出现在任何地方的,那就意味着任何地方都可以修改ManagedObject,这就导致ManagedObjectContext在同步修改的时候把各种不同来源的修改同步进去。这时候就需要限制灵活性,只对外公开一个修改接口,不暴露任何ManagedObject在外面。

如果是设计一个ABTest相关的API的时候,我们又希望增加它的灵活性。使得业务方不光可以通过Target-Action的模式实现ABtest,也要可以通过Block的方式实现ABTest,要尽可能满足灵活性,减少业务方的使用成本。

易测试易拓展

老生常谈,要实现易测试易拓展,那就要提高模块化程度,尽可能减少依赖关系,便于mock。另外,如果是高度模块化的架构,拓展起来将会是一件非常容易的事情。

保持一定量的超前性

这一点能看出架构师是否关注行业动态,是否能准确把握技术走向。保持适度的技术上的超前性,能够使得你的架构更新变得相对轻松。

另外,这里的超前性也不光是技术上的,还有产品上的。谁说架构师就不需要跟产品经理打交道了,没事多跟产品经理聊聊天,听听他对产品未来走向的畅想,你就可以在合理的地方为他的畅想留一条路子。同时,在创业公司的环境下,很多产品需求其实只是为了赶产品进度而产生的妥协方案,最后还是会转到正轨的。这时候业务方可以不实现转到正规的方案,但是架构这边,是一定要为这种可预知的改变做准备的。

接口少,接口参数少

越少的接口越少的参数,就能越降低业务方的使用成本。当然,充要条件还是要满足的,如何在满足充要条件的情况下尽可能地减少接口和参数数量,这就能看出架构师的功力有多深厚了。

高性能

为什么高性能排在最后一位?

高性能非常重要,但是在客户端架构中,它不是第一考虑因素。原因有下:

 

  • 客户端业务变化非常之快,做架构时首要考虑因素应当是便于业务方快速满足产品需求,因此需要尽可能提供简单易用效果好的接口给业务方,而不是提供高性能的接口给业务方。
  • 苹果平台的性能非常之棒,正常情况下很少会出现由于性能不够导致的用户体验问题。
  • 苹果平台的优化手段相对有限,甚至于有些时候即便动用了无所不用其极的手段乃至不择手段牺牲了稳定性,性能提高很有可能也只不过是100ms到90ms的差距。10%的性能提升对于服务端来说很不错了,因为服务端动不动就是几十万上百万的访问量,几十万上百万个10ms是很可观的。但是对于客户端的用户来说,他无法感知这10ms的差别,如果从10s优化成9s用户还是有一定感知的,但是100ms变90ms,我觉得吧,还是别折腾了。

 

但是!不重要不代表用不着去做,关于性能优化的东西,我会对应放到各系列文章里面去。比如网络层优化,那就会在网络层方案的那篇文章里面去写,对应每层架构都有每层架构的不同优化方案,我都会在各自文章里面一一细说。

2015-4-2 11:28 补: 关于架构分层?

 

昨晚上志豪看了这篇文章之后说,看到你这个题目本来我是期望看到关于架构分层相关的东西的,但是你没写。

嗯,确实没写,当时没写的原因是感觉这个没什么好写的。前面谈论到架构的方法的时候,关于问题分类分模块这一步时,架构分层也属于这一部分,给我一笔带过了。

既然志豪提出来了这个问题,我想可能大家关于这个也会有一些想法和问题,那么我就在这儿讲讲吧。

其实分层这种东西,真没啥技术含量,全凭架构师的经验和素质。

 

我们常见的分层架构,有三层架构的:展现层、业务层、数据层。也有四层架构的:展现层、业务层、网络层、本地数据层。这里说三层四层,跟TCP/IP所谓的五层或者七层不是同一种概念。再具体说就是:你这个架构在逻辑上是几层那就几层,具体每一层叫什么,做什么,没有特定的规范。这主要是针对模块分类而言的。

也有说MVC架构,MVVM架构的,这种层次划分,主要是针对数据流动的方向而言的。

在实际情况中,针对数据流动方向做的设计和针对模块分类做的设计是会放在一起的,也就是说,一个MVC架构可以是四层:展现层、业务层、网络层、本地数据层。

那么,为什么我要说这个?

大概在五六年前,业界很流行三层架构这个术语。然后各种文档资料漫天的三层架构,并且喜欢把它与MVC放在一起说,MVC三层架构/三层架构MVC,以至于很多人就会认为三层架构就是MVCMVC就是三层架构。其实不是的。三层架构里面其实没有Controller的概念,而且三层架构描述的侧重点是模块之间的逻辑关系。MVCController的概念,它描述的侧重点在于数据流动方向。

好,为什么流行起来的是三层架构,而不是四层架构五层架构

 

因为所有的模块角色只会有三种:数据管理者数据加工者数据展示者,意思也就是,笼统说来,软件只会有三层,每一层扮演一个角色。其他的第四层第五层,一般都是这三层里面的其中之一分出来的,最后都能归纳进这三层的某一层中去,所以用三层架构来描述就比较普遍。

那么我们怎么做分层?

 

应该如何做分层,不是在做架构的时候一开始就考虑的问题。虽然我们要按照自顶向下的设计方式来设计架构,但是一般情况下不适合直接从三层开始。一般都是先确定所有要解决的问题,先确定都有哪些模块,然后再基于这些模块再往下细化设计。然后再把这些列出来的问题和模块做好分类。分类之后不出意外大多数都是三层。如果发现某一层特别庞大,那就可以再拆开来变成四层,变成五层。


举个例子:你要设计一个即时通讯的服务端架构,怎么分层?

记住,不要一上来就把三层架构的规范套上去,这样做是做不出好架构的。

你要先确定都需要解决哪些问题。这里只是举例子,我随意列出一点意思意思就好了:

  1. 要解决用户登录、退出的问题
  2. 解决不同用户间数据交流的问题
  3. 解决用户数据存储的问题
  4. 如果是多台服务器的集群,就要解决用户连接的寻址问题

 

解决第一个问题需要一个链接管理模块,链接管理模块一般是通过链接池来实现。
解决第二个问题需要有一个数据交换模块,从A接收来的数据要给到B,这个事情由这个模块来做。
解决第三个问题需要有个数据库,如果是服务于大量用户,那么就需要一个缓冲区,只有当需要存储的数据达到一定量时才执行写操作。
解决第四个问题可以有几种解决方案,一个是集群中有那么几台服务器作为寻路服务器,所有寻路的服务交给那几台去做,那么你需要开发一个寻路服务的Daemon。或者用广播方式寻路,但如果寻路频次非常高,会造成集群内部网络负载特别大。这是你要权衡的地方,目前流行的思路是去中心化,那么要解决网络负载的问题,你就可以考虑配置一个缓存。

于是我们有了这些模块:

链接管理、数据交换、数据库及其配套模块、寻路模块

做到这里还远远没有结束,你要继续针对这四个模块继续往下细分,直到足够小为止。但是这里只是举例子,所以就不往下深究了。

另外,我要提醒你的是,直到这时,还是跟几层架构毫无关系的。当你把所有模块都找出来之后,就要开始整理你的这些模块,很有可能架构图就是这样:

    链接管理  收发数据                     收发数据
        数据交换                       /         
                               链接管理        数据交换
    寻路服务          ========                   /  
                    ========/            数据库服务   寻路服务
        数据库服务           /

然后这些模块分完之后你看一下图,嗯,1、2、3,一共三层,所以那就是三层架构啦。在这里最消耗脑力最考验架构师功力的地方就在于:找到所有需要的模块, 把模块放在该放的地方

这个例子侧重点在于如何分层,性能优化、数据交互规范和包协议、数据采集等其他一系列必要的东西都没有放进去,但看到这里,相信你应该了解架构师是怎么对待分层问题的了吧?

 

对的,答案就是没有分层。所谓的分层都是出架构图之后的事情了。所以你看别的架构师在演讲的时候,上来第一句话差不多都是:”这个架构分为以下几层…”。但考虑分层的问题的时机绝对不是一开始就考虑的。另外,模块一定要把它设计得独立性强,这其实是门艺术活。

 

另外,这虽然是服务端架构,但是思路跟客户端架构是一样的,侧重点不同罢了。之所以不拿客户端架构举例子,是因为这方面的客户端架构苹果已经帮你做好了绝大部分事情,没剩下什么值得说的了。

2015-4-5 12:15 补:关于Common文件夹?

 

评论区MatrixHero提到一点:

 

关于common文件夹的问题,仅仅是文件夹而已,别无他意。如果后期维护出了代码混乱可能是因为,和服务器沟通协议不统一,或代码review不及时。应该有专人维护公共类。

 

这是针对我前面提出的不要Common,不要Core而言的,为什么我建议大家不要开Common文件夹?我打算分几种情况给大家解释一下。

一般情况下,我们都会有一些属于这个项目的公共类,比如取定位坐标,比如图像处理。这些模块可能非常小,就h和m两个文件。单独拎出来成为一个模块感觉不够格,但是又不属于其他任何一个模块。于是大家很有可能就会把它们放入Common里面,我目前见到的大多数工程和大多数文档里面的代码都喜欢这么做。在当时来看,这么做看不出什么问题,但关键在于:软件是有生命,会成长的。当时分出来的小模块,很有可能会随着业务的成长,逐渐发展成大模块,发展成大模块后,可以再把它从Common移出来单独成立一个模块。这个在理论上是没有任何问题的,然而在实际操作过程中,工程师在拓张这个小模块的时候,不太容易会去考虑横向依赖的问题,因为当时这些模块都在Common里面,直接进行互相依赖是非常符合直觉的,而且也不算是不遵守规范。然而要注意的是,这才是Commom代码混乱的罪魁祸首,Common文件夹纵容了不精心管理依赖的做法。当Common里面的模块依赖关系变得复杂,再想要移出来单独成立一个模块,就不是当初设置Common时想的等规模大了再移除也不迟那么简单了。

 

另外,Common有的时候也不仅仅是一个文件夹。

 

在使用Cocoapods来管理项目库的时候,Common往往就是一个pod。这个pod里面会有A/B/C/D/E这些函数集或小模块。如果要新开一个app或者Demo,势必会使用到Common这个pod,这么做,往往会把不需要包含的代码也包含进去,我对项目有高度洁癖,这种情况会让我觉得非常不舒服。

 


 

举个例子:早年安居客的app还不是集齐所有新房二手房租房业务的。当你刚开始写新房这个app的时候,创建了一个Common这个pod,这里面包含了一些对于新房来说比较Common的代码,也包含了对于这个app来说比较Common的代码。过了半年或者一年,你要开始二手房这个app,我觉得大多数人都会选择让二手房也包含这个Common,于是这个Common很有可能自己走上另一条发展的道路。等到了租房这个业务要开app的时候,Common已经非常之庞大,相信这时候的你也不会去想整理Common的事情了,先把租房搞定,于是Common最终就变成了一坨屎。

就对于上面的例子来说,还有一个要考虑的是,分出来的三个业务很有可能会有三个Common,假设三个Common里面都有公共的功能,交给了三个团队去打理,如果遇到某个子模块需要升级,那么三个Common里面的这个子模块都要去同步升级,这是个很不效率的事情。另外,很有可能三个Common到最后发展成彼此不兼容,但是代码相似度非常之高,这个在架构上,是属于分类条理不清

就在去年年中的时候,安居客决定将三个业务归并到同一个App。好了,如果你是架构师,面对这三个Common,你打算怎么办?要想最快出成果,那就只好忍受代码冗余,赶紧先把架子搭起来再说,否则你面对的就是剪不断理还乱的Common。此时Common就已经很无奈地变成一坨屎了。这样的Common,你自己说不定也搞不清楚它里面到底都有些什么了,交给任何一个人去打理,他都不敢做彻底的整理的。

 


 

还有就是,Common本身就是一个粒度非常大的模块。在阿里这样大规模的团队中,即便新开一个业务,都需要在整个app的环境下开发,为什么?因为模块拆分粒度不够,要想开一个新业务,必须把其他业务的代码以及依赖全部拉下来,然后再开新入口,你的新业务才能进行正常的代码编写和调试。然而你的新业务其实只依赖首页入口、网络库等这几个小模块,不需要依赖其他那么多的跟你没关系的业务。现在每次打开天猫的项目,我都要等个两三分钟,这非常之蛋疼。

但是大家真的不知道这个原因吗?知道了这个原因,为什么没人去把这些粒度不够细的模块整理好?在我看来,这件事没人敢做。

  1. 原来大家用的好好的,手段烂就烂一点,你改了你能保证不出错?
  2. 这么复杂的东西,短期之内你肯定搞不好,任务量和工时都不好估,你leader会觉得你在骗工时玩自己的事情。
  3. 就算你搞定了,QA这边肯定再需要做一次全面的回归测试,任务量极大,难以说服他们配合你的工作。

花这么大的成本只是为了减少开启项目时候等待IDE打开时的那几分钟时间?我想如果我是你leader,我也应该不会批准你做这样的事情的。所以,与其到了后面吃这个苦头,不如一开始做架构的时候就不要设置Common,到后面就能省力很多。架构师的工作为什么是功在当代利在千秋,架构师的素质为什么对团队这么重要?我觉得这里就是一个最好的体现。

 


 

简而言之,不建议开Common的原因如下:

  1. Common不仅仅是一个文件夹,它也会是一个Pod。不管是什么,在Common里面很容易形成错综复杂的小模块依赖,在模块成长过程中,会纵容工程师不注意依赖的管理,乃至于将来如果要将模块拆分出去,会非常的困难。
  2. Common本身与细粒度模块设计的思想背道而驰,属于一种不合适的偷懒手段,在将来业务拓张会成为阻碍。
  3. 一旦设置了Common,就等于给地狱之门打开了一个小缝,每次业务迭代都会有一些不太好分类的东西放入Common,这就给维护Common的人带来了非常大的工作量,而且这些工作量全都是体力活,非常容易出错。

 

那么,不设Common会带来哪些好处?

 

  1. 强迫工程师在业务拓张的时候将依赖管理的事情考虑进去,让模块在一开始发展的时候就有自己的土壤,成长空间和灵活度非常大。
  2. 减少各业务模块或者Demo的体积,不需要的模块不会由于Common的存在而包含在内。
  3. 可维护性大大提高,模块升级之后要做的同步工作非常轻松,解放了那个苦逼的Common维护者,更多的时间可以用在更实质的开发工作上。
  4. 符合细粒度模块划分的架构思想。

Common的好处只有一个,就是前期特别省事儿。然而它的坏处比好处要多太多。不设置Common,再小的模块再小的代码也单独拎出来,最多就是Podfile里面要多写几行,多写几行最多只花费几分钟。但若要消除Common所带来的罪孽,不是这几分钟就能搞定的事情。既然不用Common的好处这么多,那何乐而不为呢?

假设将来你的项目中有一个类是用来做Location的,哪怕只有两个文件,也给他开一个模块就叫Location。如果你的项目中有一个类是用来做ImageProcess的,那也开一个模块就叫ImageProcess。不要都放到Common里面去,将来你再开新的项目或者新的业务,用Location就写依赖Location,用ImageProcess就写依赖ImageProcess,不要再依赖Common了,这样你的项目也好管理,管理Common的那个人日子过得也轻松(这个人其实都可以不需要了,把他的工资加到你头上不是更好?:D),将来要升级,顾虑也少。

结束

 

一下子挖了个大坑,在开篇里扯了一些淡。

 

嗯,干货会在后续的系列文章里面扑面而来的!

转:我为什么不再成长?

最近跟很多没毕业、刚毕业、或者工作3年以内的设计师聊天,了解了许多他们在不同时间点上的困惑,我想总结一些我的想法供大家参考,第一个话题是:我为什么不再成长?

成长总是一个最频繁的话题,很多在一定时间里总感觉到成长受到了限制,那么到底是什么决定了你的成长。在我看来,成长的决定因素(成长因子)有四点:

  1. 你所在组织的成长
  2. 你领导的成长
  3. 你伙伴的成长
  4. 你自己的努力程度

在这里,我把你自己的努力程度放在了最后,在我看来,依靠自己的精神力成长是最不讨巧的方式,但是事实上,很多抱怨无法成长的设计师,即没有处于一个快速成长的组织、被一个无法成长的领导管理、周围没有快速成长的伙伴、自己又缺乏自我成长的驱动力。因此,如果你认为自己没有成长,只需要问自己四个问题:

  1. 我的公司在成长吗?
  2. 我的领导者在成长吗?
  3. 我的伙伴在成长吗?
  4. 我自己的努力程度有多少?

这里并不是鼓励大家为自己寻找外部的借口,而是告诉大家成长的最佳方式是“借势”。我们往往陷入一个误区,在有意愿成长时忽视环境的影响,陷入自我学习的执拗;在成长受挫时又开始抱怨环境不好。那么如何识别公司、领导、和伙伴的成长呢?

公司成长

对于一间规模在50人一下的小公司,公司整体业务量的成长就是全部,最好的办法是和人力资源与销售人员聊天,他们掌握着一间公司的供给和需求,成长往往带来的是供需两旺,人力资源和销售往往是最忙的,听他们抱怨什么、看他们的状态可以体会到公司的成长。并不是所有公司都对员工进行财务透明,了解营收(Revenue)、净利润(Net Profit)、人均营收(Revenue Per Employee)等财务指标也是判断公司成长最直接的手段。

而对于成百上千规模的大公司而言,情况有些复杂,对于设计师而言,我指的就是互联网公司,你所需要关注的是“你所在产品线业务的成长”。作为设计师的你,一定可以判断你所在的产品处于什么样的生命周期,一个互联网产品的生命周期可以超不过五年,最旺盛的增长期也许只有一年左右,当产品成长趋于平缓,在组织级别你能获得的成长因子就越小。

领导者成长

可能很多人忽略了领导者成长的重要性。实际上这是最朴素的道理,如果你的领导者年复一年地重复一件事情,不追求更高的挑战,他就不会把他重复做的、对你来说更具有挑战性的事情交给你。有时候就是这样,你的高度由你领导者的高度决定,如果他甘于现状停滞不前,你再怎么努力都没有办法。

判断一个领导是否成长的方式有很多,首先,成长需要保持学习习惯,我个人而言,我不赞同纯粹用“高情商管理高智商”的设计管理者,也许在业务上它是可以持久有效的,但对于设计专业性而言,是缺乏同理心的。一个优秀的设计管理者应该具备体系化的知识结构,并且不断对自己的知识体系进行补充和更新,他所带领的设计团队才可能形成成长的习惯。

其次,看他是否敢于冒险,成长的结果往往是对真实能力更高的判断,最终体现在对风险的容忍度,一个不断开拓新领域挑战未知的设计领导者一定是期待或正在高成长的,对于设计师的你而言,了解一下你领导的行踪,离客户和市场越近、离风险越近。

伙伴成长

羊群效应对成长的影响巨大,如果仅仅由工作结果来判断伙伴是否成长有些武断,更好的办法是在以下几个方面进行观察:

  1. 他是否关心组织的成长:了解业务发展、尝试接近客户、尝试风险更高业务;
  2. 他是否关心领导者的成长:关注领导的发展方向、与他建立联系以及跟随他的关注点;
  3. 他是否关心其他伙伴的成长:帮助其他成员的成长、建立更亲密的伙伴关系;
  4. 最后才是他自己的成长习惯:8小时工作之外的充电、阅读和总结的习惯等。

如果一个团队里的大部分人都把组织、领导者、伙伴的成长当成成长最重要的因素看待,整个团队成长的群体效应就会变得更大。如果你的伙伴:

  1. 不关心组织的成长,只把组织当成简历里的一条经历;
  2. 不关心领导者的成长,只有职位上的跟随而我情感关联;
  3. 不关心其他伙伴的成长,只有竞争而无合作;

无论你再怎么努力成长,最后的结果都是事倍功半。

努力程度

在我看来,个人的努力程度是效率最低的一种成长方式,但在一个已经具备前三项成长因子(组织、领导者、伙伴)的环境中,它也许是让你脱颖而出的最关键要素。那么到底需要在什么地方努力呢?

首先,工作习惯高于工作技能:什么是工作习惯,它细致到一张工工整整待办事务的列表、一个一直跟随你的笔记本、按照时间和项目排序的工作文件夹、熟练使用各个工具的快捷键、专注的工作状态,而并非把PS或AI用到极致,一个资深设计师和初级设计师的区别往往在于工作习惯,而不在于技能熟练程度,那么请问,你所引以为骄傲的工作习惯是什么?

其次,设计思维高于设计技巧:一个完整“设计观”的培养要比设计技巧更加重要,设计思维的培养来自于由外及里、来自于各种知识的相互激发,前者需要更加端到端的项目(从想法到实现)以及深入真实世界的历练、后者则需要大量相关知识的阅读(美学、方法论、哲学、经济、政治、文化、艺术史等)。而现实的情况是大量的年轻设计师口口声声说设计不止是美、追求美好的产品而不究其里;养成了只转不读、马克一下的阅读习惯,阅读也过于单一、过度追求干货,而不自己产生干货。

最后,职业性格高于职业规划:过早规划自己的职业完全没有意义,也往往是幼稚的,知识工作者的特点就是技能的加速贬值,我们所积累的技能和知识可能在两年内完全不具备市场竞争力,而作为一个设计师的职业性格,可能受益于你整个职业生涯。每个人对设计师职业性格的定义可能有所不同,对我来说,我所欣赏的职业性格是“使命感”、“跟随”、和“不拒绝”,“使命感”是不和这个大环境妥协、追求尊严和理想;“跟随”是学会欣赏和接近世界上最优秀的设计人;“不拒绝”是不拒绝任何可以帮助人的小事情,哪怕是做一个“免费的图标”。

如果你自认为自己是一个努力的人,就请问自己,在过去的几年里,你的努力让你形成了什么样的工作习惯、如何定义自己的设计思维、以及你的职业性格是什么。

写在最后

这些不成文的想法,都是在最近一个月与超过8位设计师交流的结果,他们有的已经在这个行业里打磨了8年、有的在BAT公司里遇到职业天花板、有的在海外追求自己的设计理想、有的还只是在校的学生。他们的很多疑问和困惑都帮助我思考作为智力工作者的设计师应该追寻一个什么样的成长之路,接下来,还有许多其他话题,包括:

  • 大公司还是小公司?
  • 创业是个好选择吗?
  • 全才还是专才?
  • 离开还是继续?

趁休假的时间,我会把它们一一记录。

新年快乐。

 

转自:http://www.tuzei8.com/2014/12/growth-of-a-designer/

转:Docker,云时代的程序交付方式

要说最近一年云计算业界有什么大事件?Google Compute Engine
的正式发布?Azure入华?还是AWS落地中国?留在每个人大脑中的印象可能各不相同,但要是让笔者来排名的话那么Docker绝对应该算是第一位的。如果你之前听说过它的话,那么也许你会说“没错,就是它”,因为几乎世界各地的开发、运维都在谈论着Docker;如果你还没听说过Docker,那么我真的建议你花上10分钟来阅读本文。

1. Docker简介

1.1. 什么是Docker?

Docker是一个重新定义了程序开发测试、交付和部署过程的开放平台。Docker也是容器技术的一种,它运行于Linux宿主机之上,每个运行的容器都是相互隔离的,也被称为轻量级虚拟技术或容器型虚拟技术。而且它有点类似Java的编译一次,到处运行,Docker则可以称为构建一次,在各种平台上运行,包括本地服务器和云主机等(Build once,run anywhere)。

容器就是集装箱,我们的代码都被打包到集装箱里;Docker就是搬运工,帮你把应用运输到世界各地,而且是超高速。

Docker是开源软件,代码托管在GitHub上,使用Go语言编写。Go可以称得上是互联网时代专门为开发分布式、高并发系统而生的编程语言。Docker也可以说是Go语言的一个杀手级应用,而且在Docker生态圈里很多软件也都是使用Go语言编写的。

1.2. Docker历史

Docker项目始于2013年3月,由当时的PaaS服务提供商dotCloud开发,dotClound也是YCombinator S10的毕业生。尽管Docker项目很年轻,到现在也只有15个月而已,然而它的发展势头如此之猛已经让很多人感叹不已了。

2013年10月dotCloud公司名字也由dotCloud, Inc.改为Docker, Inc.,集中更多的精力放到了Docker相关的研发上。

1.3. Docker的技术基石

在进入Docker的世界之前,我们先来看一下Docker实现所依赖的一些技术。

实际上Docker的出现离不开很多Linux kernel提供的功能,甚至可以说Docker在技术上并没有什么特别重大的创新之处,利用的都是已经非常成熟的Linux技术而已,这些技术早在Solaris 10或Linux Kernel 2.6的时候就有了。可以毫不夸张的说Docker就是“站在了巨人的肩膀上”。

下面我们就先来了解一下Docker主要利用的Linux技术。

1.3.1. 容器技术

容器(Container)有时候也被称为操作系统级虚拟化,以区别传统的Hypervisor虚拟技术。它不对硬件进行模拟,只是作为普通进程运行于宿主机的内核之上。

在容器中运行的一般都是一个简易版的Linux系统,有root用户权限、init系统(采用LXC容器的情况下)、进程id、用户id以及网络属性。

容器技术在云计算时代已经被大量使用。Google公司的Joe Beda在今年5月做了一次题为《Containers At Scale — At Google, the Google Cloud Platform and Beyond》注 1的演讲,在其中提到“Everything at Google runs in a container”,每周启动容器次数竟然多达20亿次。

注 1 https://speakerdeck.com/jbeda/containers-at-scale

很多PaaS平台都是基于容器技术实现的,比如目前最成功的PaaS平台Heroku。此外,还有比较著名的开源PaaS平台Cloud Foundry的Warden以及Google的Lmctfy(Let Me Contain That For You)注 2等。

注 2 Let Me Contain That For You,http://github.com/google/lmctfy

1.3.2. LXC

这也是在Linux下使用比较广泛的容器方案。基本上我们可以认为Linux containers = cgroups(资源控制) + namespaces(容器隔离)。

LXC很成熟很强大,然而它却不好使用,比如它不方便在多台机器间移动,不方便创建管理,不可重复操作,也不方便共享等等,相对于开发人员来说,它只是系统管理员的玩具。Docker的出现很好的解决了这些问题,它将容器技术的使用成本拉低到了一个平民价格。

1.3.3. namespaces

这是用来为容器提供进程隔离的技术,每个容器都有自己的命名空间,比如pid/net/ipc/mnt/uts等命名空间,以及为容器提供不同的hostname。namespace能保证不同的容器之间不会相互影响,每个容器都像是一个独立运行着的OS一样。

1.3.4. cgroups

cgroups是一个Google贡献的项目,它主要用来对共享资源的分配、限制、审计及管理,比如它可以为每个容器分配CPU、内存以及blkio等的使用限额等。cgroups使得容器能在宿主机上能友好的相处,并公平的分配资源以及杜绝资源滥用的潜在风险。

不管是传统的LXC还是Docker的libcontainer,都使用了Kernel的这些功能来实现容器功能。

1.3.5. 联合文件系统

联合文件系统是一个分层的轻量、高性能文件系统。Docker之所以这么吸引人,很大程度上在于其在镜像管理上所做出的创新。而联合文件系统正是构建Docker镜像的基础。

AUFS(AnotherUnionFS)是一个分层的基于Copy On Write技术的文件系统,支持Union Mount,就是将具有不同文件夹结构的镜像层进行叠加挂载,让它们看上去就像是一个文件系统那样。

1.4. 容器技术VS虚拟机技术

容器技术和Hypervisor技术虽然不属于同一层次的概念,但是作为具有计算能力的应用运行载体来说,它们还是有一定的共通性和竞争关系,这里作此对比完全是为了加深读者对容器技术的理解而已。

容器技术 虚拟机技术
占用磁盘空间 小,甚至几十KB(镜像层的情况) 非常大,上GB
启动速度 快,几秒钟 慢,几分钟
运行形态 直接运行于宿主机的内核上,不同容器共享同一个Linux内核 运行于Hypervisior上
并发性 一台宿主机可以启动成千上百个容器 最多几十个虚拟机
性能 接近宿主机本地进程 逊于宿主机
资源利用率

比如开源PaaS实现软件tsuru最初使用的是基于虚拟机的技术,创建一个应用程序需要5分钟左右的时间,而在采用Docker之后,已经将这个时间缩短到了10秒钟了注 3

注 3 tsuru and docker by Andrews Medina https://speakerdeck.com/andrewsmedina/tsuru-and-docker

1.5. 我们能用Docker干什么?

Docker可以应用在各种场景下,比如公司内部开发测试使用,或者作为共有或者私有PaaS平台等。

现在PaaS平台的发展已经非常成熟了,这里我们只罗列一些在开发中使用Docker技术可能会给我们带来的益处。

1.5.1 在开发中

构建开发环境变得简单

简单包括几个方面的意思

  • 快速:只需docker run即可
  • 共享:通过Dockerfile或者Registry
  • 自动化:一切代码化的东西都可以自动化
  • 统一:每个人的开发环境都是一模一样的

设想我们要基于Nginx/PHP、MySQL和Redis开发,我们可以创建3个Docker镜像保存到公司私有的Registry中去,每个开发人员使用的时候是需要执行docker run redis即可以享用自己独有的Redis服务了,而且这3个容器不管从占用磁盘空间还是运行性能来说,都比虚拟机要好很多。

1.5.2. 在测试中

解决环境构建问题

有时候构建测试的环境是一项费时费力的工作,而Docker能让这变得轻松。如果你的测试比较简单的话,甚至直接拿开发构建的镜像就可以开始了。

消除环境不一致导致的问题

“在我的机器上运行的好好的,怎么到你那里就不行了?”,我想超过半数的程序员都曾经说过类似的话。如果对导致这一问题的原因进行统计的话,我想排在第一位的应该非“环境不一致”莫属了,这包括操作系统和软件的版本、环境变量、文件路径等。

使用Docker的话你再也不用为此烦恼了。因为你交付的东西不光是你的代码、配置文件、数据库定义,还包括你的应用程序运行的环境:OS加上各种中间件、类库 + 你的应用程序。

1.5.3. 部署和运维

基于容器的部署和自动化

Docker定义了重新打包程序的方法。

Docker容器 + 用户应用 = 部署单位(构件)

Docker可以看作是用代码编写出来的国际集装箱,它可以把任何应用及相关依赖项打包成一个轻量、可移植(Portable)、自包涵的容器。

以前部署代码都是代码级别的,有了Docker,则可以进行容器级别的部署。这样带来的最大的好处就是开发者本地测试、CI服务器测试、测试人员测试,以及生产环境运行的都可以是同一个Docker镜像。

快速进行横向扩展

Docker容器的启动速度很快,可以瞬间启动大量容器,所以在非常适合在业务高峰期进行横向扩展。这比传统的启动EC2实例或者物理机可要快多了。

天生的和云计算技术相结合

当然,由于Docker具有很好的移植性,所以它更强大的地方还在于和云环境结合使用。

Docker容器是可移植,或者说跨平台。将来的应用部署可能是在本地进行打包(成Docker镜像)然后传送到云端运行,至于是AWS还是GCE这不是问题,Docker都能在其上运行。这样不仅能在一定程度上解决vendor-lockin的问题,同时也使得在不同的云服务提供商之间迁移也变得简单。尤其是未来在使用多云(multi-cloud)环境的时候,这将非常便利。

笔者认为基于IaaS + 容器技术的应用交付、部署方式将来一定会成为一种流行的方式。

进行Blue-green部署

「Blue-green deployment」这个词最初出现在《Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation 》一书,后经ThoughtWorks的Martin Fowler发扬光大注 4

注 4 http://martinfowler.com/bliki/BlueGreenDeployment.html

Blue-green deployment方法其实很简单,就是保持两套一样的生产环境,而实际上只有一套环境真正的对外提供服务,而另一套环境则处于待机状态。部署的时候,我们会先上线到蓝色环境中,如果测试没有问题了,再将路由切换到新的服务上。

Blue-green部署能带来如下好处。

  • 最小化停机时间
  • 快速回滚
  • hot standby

而未来的开发和部署和可能就会像下面这样进行了。

  • ① 开发人员将代码push到Git仓库
  • ② CI工具通过webhook得到最新代码,构建Docker镜像并启动容器进行测试。
  • ③ 测试通过后将镜像打标签后push到私有镜像Registry
  • ④ CI工具通知CD工具
  • ⑤ CD工具通过Mesos/Marathon等进行基于容器的部署
  • ⑥ 测试没有问题后进行容器的切换(即Blue-green切换)

2. Docker架构解析

2.1. Docker整体结构

Docker是一个构建、发布、运行分布式应用的平台(见下图),Docker平台由Docker Engine(运行环境 + 打包工具)、Docker Hub(API + 生态系统)两部分组成。

Docker生态环境

从图中我们可以看到,Docker的底层是各种Linux OS以及云计算基础设施,而上层则是各种应用程序和管理工具,每层之间都是通过API来通信的。

Docker引擎

Docker引擎是一组开源软件,位于Docker平台的核心位置。它提供了容器运行时以及打包、管理等工具。

Docker引擎可以直观理解为就是在某一台机器上运行的Docker程序,实际上它是一个C/S结构的软件,有一个后台守护进程在运行,每次我们运行docker命令的时候实际上都是通过RESTful Remote API来和守护进程进行交互的,即使是在同一台机器上也是如此。

Docker Hub

Docker Hub是一个云端的分布式应用服务,它专注于内容、协作和工作流。Docker Hub除了可以托管、下载、查找Docker镜像之外,还提供了包括更管理、团队协作、生命周期流程自动化等功能,以及对第三方工具和服务的集成。

2.2. Docker镜像(image)

2.2.1. Docker镜像

Docker镜像是Docker系统中的构建模块(Build Component),是启动一个Docker容器的基础。

Docker镜像位于bootfs之上,实际上bootfs在系统启动后会被卸载的。Docker镜像(Images)是分层的,这得益于其采用的联合文件系统,前面我们已经介绍过了。镜像是有继承(父子)关系的,每一层镜像的下面一层称为父镜像,没有父镜像的称为基础镜像(Base Iamge,其实叫做Root Image可能更确切,不过这可能容易和rootfs混淆)。

2.2.2. 镜像仓库

我们可以将Docker镜像仓库理解为Git仓库。Dcoker镜像仓库分为远程和本地,本地的概念好理解,而一般来说远程仓库就是Registry,包括官方的或者自建的私有Registry;我们通过docker pulldocker push命令在本地和远程之间进行镜像传输。

Docker镜像的命名规则和GitHub也很像。比如我们自己创建的仓库名称都是类似liubin/redis这样格式的,前面的liubin是用户名或namespace,后面是仓库名。

不过我们前面已经看到运行的ubuntu镜像的时候是仓库名就是ubuntu,而不带用户名前缀,这是表明它是由官方制作的,或者由官方认可的第三方制作的镜像。我们可以认为官方仓库提供的镜像都是安全的、最新的,所以也可以放心使用。

2.3. Docker容器(Container)

容器是一个基于Docker镜像创建、包含为了运行某一特定程序的所有需要的OS、软件、配置文件和数据,是一个可移植的运行单元。在宿主机来看,它只不过是一个简单的用户进程而已。

容器启动的时候,Docker会在镜像最上层挂载一个read-write的文件系统,即上图中标记为writable的Container层,容器将跑在这个文件系统上。这层可写的文件系统是容器中才有的概念,如果我们对此容器进行commit操作,那么该层文件系统则会被提交为一个新的只读的镜像层,并位于镜像层的最上面的。

我们可以认为Docker镜像是“静”的”.exe”文件,只在“硬盘”上;而容器是“动”的,是在“内存中”的,要想启动一个容器,需要先把”.exe”装载到内存。

镜像和容器具有如下的转换关系:

  • 镜像 -> docker run -> 容器
  • 容器 -> docker commit -> 镜像

有时候我们经常会将两个名称混用,不过这并不会影响我们的理解。

2.4. Docker Registry

Docker Registry是Docker架构中的分发模块,它用来存储Docker镜像,我们可以将它理解为GitHub。

Docker Hub是一个官方的Docker Registry,也是Docker镜像的默认存储位置。

当然从安全管理的角度上来说,我们可能更愿意在自己公司内部托管一个私有的Docker Registry,这可以通过使用Docker官方提供的Registry注 5软件实现。

注 5 Docker Registry https://github.com/dotcloud/docker-registry

运行私有Registry非常简单,这也是一个典型的Docker风格的应用发布例子。

docker run –p 5000:5000 registry 

3. 使用Docker

3.1. 初识容器

3.1.1. 创建并启动容器

这里我们假定各位读者已经在自己的机器上安装好了Docker。Docker主要的命令就是docker了,它的参数很多,关于它的具体使用方法,可以参考官方的文档注 6,这里我们只简单的介绍其中一些常用的用法。

注 6 https://docs.docker.com/reference/commandline/cli/ 和 https://docs.docker.com/reference/run/

启动一个容器很简单,我们只需要运行docker run命令就可以了注 6

注 6 为了方便区分,本文中运行命令的时候如果提示符为$,表示实在宿主机(Ubuntu)中,如果是#,则表示是在Docker容器中

$ sudo docker run -i -t ubuntu /bin/bash Unable to find image 'ubuntu' locally Pulling repository ubuntu e54ca5efa2e9: Pulling dependent layers ... 省略 ... 6c37f792ddac: Download complete ... 省略 ... root@81874a4a6d2e:/# 

docker run命令会启动一个容器。参数ubuntu指定了我们需要运行的镜像名称,后面的bash则指定了要运行的命令,注意这个命令是容器中的命令,而不是宿主机中的命令。参数-i用来为容器打开标准输入以和宿主机进行交互,-t则会为容器分配一个终端。

在第一次启动某镜像的时候,如果我们本地还没有这个镜像,则Docker会先从远程仓库(Docker Hub)将容器的镜像下载下来,下载完成之后才会启动容器。

注意Docker里有一个很重要的概念就是容器ID或者镜像ID,比如这个例子里的e54ca5efa2e9。这个ID是一个容器或者镜像的唯一标识,它的长度为64位,不过很多时候都可以简写为12位,这也和Git很像。

3.1.2. 让Docker容器在后台运行

这时候我们可以使用-d参数来通过守护模式启动一个容器,这样容器将会在后台一直运行下去。这非常适合运行服务类程序。如果需要,我们可以再通过docker attach命令连接到运行中的容器。

3.1.3. 常用命令

docker ps

docker ps用来查看正在运行中的容器。

从下面的输出结果我们可以看出该容器状态(STATUS列)为已经停止执行,且没有错误(Exited后面的状态码)。


$ sudo docker ps -a 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 
60bab6f881e5 ubuntu:latest /bin/bash 14 minutes ago Exited (0) 5 seconds ago agitated_hopper 

docker ps命令的常用参数(及组合)如下。

  • -a: 查看所有容器,包括已经停止运行的。
  • -l: 查看刚刚启动的容器。
  • -q: 只显示容器ID
  • -l -q: 则可以返回刚启动的容器ID。
docker stop/start/restart

docker stop用来停止运行中的容器,同时你还可以用docker start来重新启动一个已经停止的容器。

docker restart可以重启一个运行中的容器。这就相当于对一个容器先进行stopstart

3.2. 深入了解Docker镜像

在对Docker容器有一个简单的感性认识之后,我们再来深入了解一下Docker镜像的概念。

Docker镜像实际上就是一个tarball,它是一个能完整运行的OS系统,这非常像OS或VM镜像。它里面有基础OS、各种软件包及类库等。我们启动一个容器,相当于是启动了一个“基础OS”。

3.2.1. 标签(Tag)

我们还可以为镜像打标签,这也和Git非常相似。其实你也可能在前面留意到了,docker images的输出中有一列就是TAG的。我们在执行docker build或者docker commit的时候都可以同时为仓库名称指定一个TAG,格式为user_name/repo_name:tag,如果没有指定这个TAG,则默认为latest

3.2.2. 常见镜像操作

这里我们再介绍一下对镜像常见的一些操作。

查看本地镜像列表

docker images命令用来列出当前系统中的所有本地镜像,即我们已经通过docker run或者docker pull下载下来的镜像,镜像文件保存在本地的/var/lib/docker文件夹下。

下载镜像到本地

只需要运行docker pull命令即可,命令非常简单,问题在于你的网路速度和连通性。

删除镜像

docker rmi用来从本地仓库中删除一个不再需要的镜像,即”rm image”的缩写。

3.3. 构建镜像

我们可以创建自己的Docker镜像,在我们的日常工作中会经常进行镜像构建操作。构建Docker镜像非常简单,而且方法也有几种。

3.3.1. 手工创建

这个方法最简单直接的方法。其流程是启动一个容器,在里面进行一些列安装、配置操作,然后运行docker commit命令来将容器commit为一个新镜像。


$ sudo docker run -t -i ubuntu bash 
root@c4be1df52810:/# apt-get update 
root@c4be1df52810:/# apt-get -y install redis-server 
root@c4be1df52810:/# exit 

通过下面的命令得到刚才容器的ID号并进行commit操作。


$ sudo docker ps -q -l c4be1df52810 
$ sudo docker commit -m="manually created image" -a="bin liu <liubin0329@gmail.com>" 
-run='{"CMD":["/usr/bin/redis-server"], "PortSpecs": ["6379"]}' c4be1df52810 liubin/redis:manually 
Warning: '-run' is deprecated, it will be removed soon. See usage. 
744ce29b2fcf0ad7ad8b2a89c874db51376c3fdd65d1f4f0c6f233b72f8c3400 

注意上面的警告信息,在docker commit命令指定-run选项已经不被推荐了,这里为了说明这个例子而故意使用了这个选项。建议创建镜像还是使用Dockerfile的方式,即能将创建过程代码化、透明化,还能进行版本化。

再次运行docker images命令,就应该能看到我们刚才通过docker commit命令创建的镜像了(镜像ID为744ce29b2fcf,镜像名为liubin/redis:manually)。

3.3.2. 使用Dockerfile文件

使用Dockerfile构建Docker镜像

这是一个官方推荐的方法,即将构建镜像的过程代码化,比如要安装什么软件,拷贝什么文件,进行什么样的配置等都用代码进行描述,然后运行docker build命令来创建镜像文件。官方的自动构建即是基于保存在GitHub等代码托管服务上的Dockerfile进行的。Dockerfile即是具体的用于构建的配置文件名,也是这类文件的类型名称。

使用Dockerfile构建Docker镜像非常简单,我们只需要创建一个名为Dockerfile的文件,并编写相应的安装、配置脚本就可以了。我们还是以上面安装Redis服务为例,看看如何使用Dockerfile构建一个镜像。

首先,创建一个redis文件夹(文件夹名任意,无任何限制),并进入该文件夹,然后创建一个Dockerfile文件。这个文件的文件名是固定的,其内容如下。


FROM ubuntu MAINTAINER bin liu <liubin0329@gmail.com> 
RUN apt-get update 
RUN apt-get -y install redis-server 
EXPOSE 6379 ENTRYPOINT ["/usr/bin/redis-server"] 

Dockerfile文件的语法非常简单,每一行都是一条指令,注释则以#开头。每条指令都是“指令名称 参数”的形式,指令名称一般都是大写。比如FROM指令表明了我们的镜像的基础镜像(严格来说叫父镜像,我们的所有操作都将以此镜像为基础),这里是ubuntu,但实际上它可以是存在的任何镜像,比如liubin/rubyRUN指令则用来在构建过程中执行各种命令、脚本,比如这里是apt-get命令,你也可以指定一个很复杂很长的脚本文件路径。AUFS有42层文件系统的限制注 7,这时候我们可以通过在RUN指令中执行多条命令,即cmd1 && cmd2 && cmd3 && ...这种形式就可以可避免该问题了。EXPOSE表示此镜像将对外提供6379端口的服务。ENTRYPOINT则指定了启动该镜像时的默认运行程序。

注 7 https://github.com/dotcloud/docker/issues/1171

具体的Dockerfile语法在官方网站注 8有详细说明,相信花个10分钟就能通读一遍,这里唯一比较容易混淆的就是ENTRYPOINTCMD指令了,关于它们的区别,还是留作每位读者自己的课题去研究一下吧。

注 8 https://docs.docker.com/reference/builder/

Dockerfile准备好了之后,运行docker build命令即可构建镜像了。

$ sudo docker build -t liubin/redis:dockerfile . 

这里-t表示为构建好的镜像设置一个仓库名称和Tag(如果省略Tag的话则默认使用latest)。最后的一个.表示Dockerfile文件的所在路径,由于我们是在同一文件夹下运行docker build命令,所以使用了.

由于篇幅所限,这里我们就省略了docker build命令的输出。不过如果你亲自动手执行docker build命令的话,那么从它的输出应该很容易理解,Dockerfile里的每一条指令,都对应着构建过程中的每一步,而且每一步都会生成一个新的类似容器的哈希值一样的镜像层ID。也正是这些层,使得镜像能共享很多信息,并且能进行版本管理、继承和分支关系管理等。这除了能节省大量磁盘空间之外,还能在构建镜像的时候通过使用已经构建过的层(即缓存)来大大加快了镜像构建的速度。比如在我们在使用Dockerfile进行构建镜像时,如果在某一步出错了,那么实际上之前步骤的操作已经被提交了,修改Dockerfile后再次进行构建的话,Docker足够聪明到则会从出错的地方开始重新构建,因为前面的指令执行结构都已经被缓存了。

如果你使用docker history命令来查看该镜像的历史信息,你会发现它的输出和docker build的记录是相匹配的,每一条Dockerfile中的指令都会创建一个镜像层。此命令还能查看每个镜像层所占空间大小,即SIZE列的内容。比如本例中MAINTAINER这样指令,实际上它只是关于镜像的元数据,并不占用额外的磁盘空间,所以它的层大小为0字节。而RUN apt-get -y install redis-server创建的层则会在镜像中增加文件,所以是需要占用磁盘空间的。

自动构建(Automated Builds)

Docker Hub的目的之一就是要成为应用程序交换的中转站,它还支持自动构建功能。自动构建的Dockerfile可以托管在GitHub或者Bitbucket上,当我们将代码提交并push到托管仓库的时候,Docker Hub会自动通过webhook来启动镜像构建任务。

配置自动构建很简单,只需要在Docker Hub中绑定GitHub或者Bitbucket账号就可以了,如何具体操作这里不做详细说明了。

3.3.3. 使用Packer

Packer注 10是一个通过配置文件创建一致机器镜像(identical machine images)的非常方便的工具。Packer同样出自Vagrant的作者Mitchell Hashimoto之手。它支持虚拟机VirtualBox和VMWare等虚拟机软件,以及Amazon EC2、DigitalOcean、GCE以及OpenStack等云平台,最新版的Packer也增加了对Docker的支持。

注 10 http://www.packer.io/

Packer的使用也比较简单,这里我们就举例说明了,读者可以自己试一下。

3.4. 发布镜像

如果你愿意,还可以将在本地制作镜像push到Docker Hub上和其他人分享你的工作成果。

首先你要有一个Docker Hub账号并已经为登录状态,这样才能往Docker Hub上push镜像文件。注册Docker Hub账号只能通过网站注册注 11,这里我们假设各位读者已经拥有Docker Hub了账号。

注 11 https://hub.docker.com/

登录Docker Hub通过docker login命令。

登录成功后,我们就可以push镜像了。注意这里我们没有指定Tag,Docker知道如何去做。

$ sudo docker push liubin/redis 

我们前面说过,镜像文件是分层的,很多镜像文件可以共用很多层。比如我们这次往服务器push镜像的时候,实际push的只有一层(744ce29b2fcf)而已,这是因为我们的镜像文件是基于ubuntu这个base镜像创建的,而ubuntu镜像早已经在远程仓库中了。

我们在层744ce29b2fcf中对应的操作是bash命令,并在容器中安装了Redis。而这次修改只有不到6M的容量增加,而如果只是修改配置文件的话,那么一次push操作可能只需要耗费几K的网络带宽而已。

4. DockerCon14总结

首届Docker大会(DockerCon14)于当地时间6月9日~6月10日在旧金山举行。相对于计划中的500个参会名额,最终有超过900人报名,并提交了超过150个演讲申请。

关于这次Docker大会的更多信息可以参考其官方网站:http://www.dockercon.com/。

4.1. Docker官方发布的产品和服务

4.1.1. Docker 1.0的发布及商业支持

在这次大会上最重要的事情莫过于Docker 1.0的发布了。Docker 1.0已经可以在Red Hat、Debian、Ubuntu、Fedora、SuSE等主流Linux系统下运行,在功能、稳定性以及软件质量上都已经达到了企业使用的标准,文档也更加系统、完善。并且提供了Docker Hub云服务,方便开发者和企业进行应用分发。最重要的是Docker, Inc.还宣布了对Docker的商业支持,尤其是对Docker 1.0版本的长期支持。此外,Docker, Inc.还会提供Docker相关的培训、咨询等工作。

4.1.2. Docker Engine + Docker Hub

同时从1.0开始,Docker的架构也发生了较大的变化。Docker已经从单一的软件转变为了一个构建、发布、运行分布式应用的平台。

新的Docker平台由Docker Engine(运行环境 + 打包工具)、Docker Hub(API + 生态系统)两部分组成。

Docker引擎

Docker引擎是一组开源软件,位于Docker平台的核心位置。它提供了容器运行时以及打包、管理等工具。

Docker Hub

Docker Hub是一个云端的分布式应用服务,它专注于内容、协作和工作流。

Docker Hub可以看作是原来Docker index服务的升级版。Docker Hub除了可以托管Docker镜像之外,还提供了包括更管理、团队协作、生命周期流程自动化等功能,以及对第三方工具和服务的集成。

在Docker, Inc.看来,典型的基于Docker Hub的软件开发生命周期为:在本地基于Docker引擎开发 -> 打包应用程序 -> 将应用程序push到Docker Hub -> 从Docker Hub上下载此应用镜像并运行。它将镜像构建的任务交给Dev,将镜像部署的任务交给Ops。

4.1.3. 新组件

Docker Engine也有了一些新的变化,而部分功能实际上早在Docker 0.9就开始提供了。如果你还在运行Docker 0.8及其以前的版本的话,那么还是及早升级的比较好。

libswarm

libswarm是一个”toolkit for composing network services”。它定义了标准接口用于管理和编配一个分布式系统,并提供了一致的API。libswarm打算支持各种编配系统,虽然它看上去更像个高层接口封装的API而已。

libcaontainer

libcontainer是一个容器的参考实现,它通过Go语言实现来使用Linux的命名空间等技术,而不需要额外的外部依赖。

实际上在Docker 0.9的时候这个模块就已经分离出来了,到了1.0的时候,此模块成为了独立项目并且可以单独使用。并且从0.9版本的时候开始Docker就已经开始就采用libcontainer来代替LXC作为默认的容器实现方式了,LXC变成了可选项之一。

libchan

libchan现在是Docker的标准通信层,被称为网络上的go channel,普通的Go channel只能运行在单机上,而libchan可以跨Unix socket或纯TCP/TLS/HTTP2/SPDY/Websocket等运行。使用libchan,可以非常方便的进行任意结构的消息传递、实时双工异步通信、并发编程及同步等。

http://www.slideshare.net/shykes/docker-the-road-ahead

4.2. 大公司的热情

如果看一下演讲嘉宾列表注 13,你一定会感叹这阵容太豪华了。不错,很多演讲嘉宾都来自大型互联网公司,比如Facebook、Twitter、Google、Heroku、Yelp以及Group等,很多还都是VP、CTO等高级别的管理人员,可见这次大会规格之高,分量之重。并且他们中的很多人还都进入到了Docker治理委员会。

注 13 http://www.dockercon.com/speakers.html

4.2.1. Google

前面我们已经介绍了Google公司内部的服务都是跑在容器之中的,Google对Docker也表现出了相当浓厚的兴趣。除了他们负责基础设施的VP Eric Brewer进行了主题为《Robust Containers》的演讲之外,他们还介绍了自己开源容器管理软件Kubernetes和对容器资源进行监控的cAdvisor。

4.2.2. Red Hat

Red Hat Enterprise Linux 7版将内置Docker,虽然版本还是0.11,不过很快就会升级的。另外Atomic项目也是Red Hat主导开发的。

4.3. 其它感受

其他一些笔者认为比较有意思的就是使用基于Mesos工具群来对容器进行集群管理了。比如Twitter和Groupon都做了使用Mesos + Aurora/Marathon + ZooKeeper在数据中心进行资源分配和管理的分享;甚至在Twitter看来,数据中心也可以看做是一台计算机,Mesos就是这台计算机的OS。

另外就像我们前面在Docker使用场景中介绍过的那样,很多公司都在使用Docker进行持续集成。

5. Docker现状及展望

在本节我们将会站在一个开放的角度和更高的层次来审视一下Docker的现状,包括其问题点,以及对Docker将来的可能性做一些肤浅的推测。

5.1. 生态系统

Docker的发展离不开其生态系统注 14,我们学习Docker也同样需对其生态系统有所了解。我们可以从下面三点来审视一下Docker当前的发展状况。

注 14 关于Docker的生态环境,大家也可以参考网上有人制作的一份思维导图。http://www.mindmeister.com/389671722/docker-ecosystem

5.1.1. 厂商支持

前面我们已经说过了,包括RedHat等在内的Linux发行商以及Google、AWS、Rackspace等云服务提供商都表示对Docker非常浓厚的兴趣,甚至已经进行了非常深入的实践。从这一点上来说,Docker有非常好的政治背景。

5.1.2. 开源项目

围绕Docker的开源项目就更多了,主要有以下几类,我们将挑选出一些比较有意思且开发较活跃的项目进行简单介绍。

PaaS平台

PaaS平台大多基于容器技术,Docker天生就适合做PaaS。

  • Flynn

Flynn是一个高度模块化的下一代开源PaaS实现。Flynn分为两层,Layer 0是底层,也叫资源层,基于Google的Omega论文注 15开发,这一层也包括服务发现。Layer 1则用来进行部署、管理应用程序。Flynn目前开发比较活跃,是一个值得关注的开源项目,而且今年夏天很可能就会发布1.0的版本了。

注 15 http://eurosys2013.tudos.org/wp-content/uploads/2013/paper/Schwarzkopf.pdf

https://flynn.io/

  • Deis

Deis是一个支持共有和私有PaaS的开源实现。它支持运行使用Ruby, Python, Node.js, Java, PHP和Go等语言进行应用开发,并能部署到AWS, Rackspace和DigitalOcean等云上。

http://deis.io/

CI/CD(持续集成/持续部署)

由于Docker的沙箱性、创建速度快等特性,它与生俱来也适合进行CI/CD。很多基于Docker的CI/CD开源方案和服务如雨后春笋般的涌现出来。

  • Drone

开源的支持各种语言的CI工具,并且提供了CI/CD服务Drone.io

https://drone.io/

  • Strider CD

开源的CI/CD方案,集成GitHub。

http://stridercd.com/

私有仓库托管(Registry)/容器托管

这类服务主要进行私有仓库的托管,根据用户的托管仓库数量收费。Doccker Hub也提供私有仓库的收费套餐。

  • Quay

Quay除了能托管私有镜像之外,还能和GitHub集成,使用Dockerfile进行镜像构建。

https://quay.io/

  • Shippable

Shippable支持Github和Bitbucket,并且提供100%免费的服务,包括私有仓库。

https://www.shippable.com/

  • Orchard

Orchard也是一个和StackDock类似的Docker托管服务,它提供了便捷的命令行工具来运行各种Docker命令。同时它也提供免费的私有Registry服务,前面介绍的Fig工具就是此公司开发的。

https://www.orchardup.com/

笔者认为传统的云计算服务提供商除了在云主机上提供对容器的支持之外,说不定将来还会提供专门托管容器的服务。

开发管理工具

软件工程师天生就是闲不住和想尽一切办法要提高自己效率的一群人。这里我们简单介绍两个方便进行Docker开发的工具。

  • Shipyard

Shipyard是一个Docker镜像和容器管理工具,除了基本的镜像构建,容器启动等功能之外,它还具有在浏览器中attach到容器的功能,并通过hipache16来进行容器之间的连接。同时它也支持跨节点的Docker管理和容器Metrics采集。

注 16 Hipache: a distributed HTTP and websocket proxy https://github.com/dotcloud/hipache

https://github.com/shipyard/shipyard

  • Fig

Fig是一个为了提高基于Docker开发的效率而创建的工具,它通过一个配置文件来管理多个Docker容器,非常适合组合使用多个容器进行开发的场景。

http://orchardup.github.io/fig/index.html

5.1.3. 社区

Docker开发社区非常活跃,除了35名全职员工(外加一只乌龟)之外,还有450名左右的外部代码贡献者。到目前Docker Hub已经拥有超过16000多个应用,在GitHub上也有超过7000个Docker相关的项目,其中不乏很多受关注度非常高的项目。

在Twitter上,科技媒体上以及个人Blog上,每天都能看到很多关于Docker的内容。

线下社区活动也在蓬勃展开中。在世界范围内除了南极洲,Docker Meetup已经遍布35个国家100多个城市,北京在今年3月8日举行了国内第一次的Docker Meetup,当时有超过40人报名参加。而且第二次北京Docker Meetup将在七月中举行,目前正在紧锣密鼓的筹备之中。

5.2. 运用中的问题点

虽然Docker很火,有时候我们也需要反过来看看它还有哪些不令我们满意的地方,或者说在使用上还存有疑虑。当然这里的问题都是笔者个人主观看法,只是非常片面的一部分,各位读者一定要带着批判性的思维去理解它。

5.2.1. Debug、调优

查看日志可能是最简单直接的方式了。当然也有很多人都会在Docker容器中运行一个SSHD服务,然后通过SSH登录到容器中去,不过不建议使用这种方法。

官方推荐使用nsenter注 17工具来完成类似的工作,通过它可以进入到指定的namespace中并控制一个容器。

注 17 https://github.com/jpetazzo/nsenter

5.2.2. 数据管理

这里所说的数据包括数据库文件,Log,用户上传的文件等。

在容器中要想处理数据文件,可能最简单的方式就是通过共享卷标来实现,即docker run -v。但是随之带来的问题是既然是文件,都存在备份问题,如何备份?用ftp或者在容器和宿主机之间共享文件夹的方式?而且随着容器数量的增多,对共享卷标的管理也势必会更复杂。

笔者认为理想的解决方法就是使用云服务,比如数据库使用RDS,文件使用S3。如果不想使用云服务,则可以考虑自己通过FastDFS等实现自己的“云存储”。Log则通过fluentd/logstash进行集计再用Graphite/Kibana等进行可视化。

5.2.3. 如何和配置管理工具配合使用

到底在容器时代,还需不需要传统的Puppet或Chef这样的配置管理工具?当然,从配置管理工具的角度来说,他们都不会放弃对Docker的支持,比如Puppet就已经增加了对Docker(安装、管理镜像和容器)的支持。

但随着不可变基础设施的普及注 18,幂等性将不再重要,因为我们的容器只需要配置一次。要对容器做出修改,可能只需要修改Dockerfile/manifest/recipe文件重新Provisioning即可。而且也不需要在容器内部安装任何agent,这样的话类似Ansible这样纯SSH的配置管理工具比较适合对Docker进行配置。甚至还可能出现专门为Docker的更简单的配置管理工具。

注 18 笔者个人偏见而已

5.2.4. 安全性

是软件就会存在bug,包括安全漏洞,Docker也不例外。就在今年6月份,Docker刚爆出了一个容器逸出的漏洞注 19。不管是Hypervisor技术还是容器技术,安全问题始终都是一个不可避免的话题,虽然它们出问题的几率要比中间件软件(Apache,Nginx、Tomcat)和软件框架(Struts、Rails)等的概率要小很多。

注 19 http://blog.docker.com/category/security-2/

事后Docker, Inc.还是比较积极的面对了这件事,除了及时披露详细情况之外,还着重强调了他们的安全政策。

5.2.5. 有状态和无状态容器

在不可变基础设施(Immutable Infrastructure)里,一切都可以分为有状态(stateful)的和无状态(stateless)的,容器也不例外。容器似乎更适合跑无状态的服务,然而业内对如何分别对待这两种服务还没有太好的最佳实践。

5.3. 对Docker展望

最后再容笔者斗胆对Docker的将来做一些展望。除了Docker本身自己会蓬勃发展之外,围绕Docker的生态圈必将更加成熟和强大。

5.3.1. 集群管理(Orchestration)和服务发现(Service Discovery)

相对于对单台机器进行Provisioning而言,云环境下则需要对多台机器进行Orchestration。Orchestration这个词翻译过来就是编排、编配的意思,我们也可以理解为集群管理。它主要由两部分工作组成:

  • 监控服务器,发现变化(软硬件异常、网络异常、正常变更等)
  • 根据监视事件采取相应的行动。
服务发现

在松耦合的分布式环境下,应用程序不一定跑在同一台机上,甚至是跨越数据中心的。这时候服务发现就显得格外重要了。

  • Zookeeper

Chubby注 20可以称得上是很多服务发现、集群管理软件的鼻祖了,比如Zookeeper注 21,这些软件都提供数据存储、leader选举、元数据存储、分布式锁、事件监听(或watch,监视)等功能。

注 20 http://research.google.com/archive/chubby.html

注 21 http://zookeeper.apache.org/

  • etcd

etcd注 22很新也很轻量,安装很简单,配置也不复杂,所以非常适合入门。etcd存储的是key-value格式的数据。

etcd是CoreOS的一个组件。同时CoreOS提供了一个基于公有云的服务发现服务discovery.etcd.io。

注 22 https://github.com/coreos/etcd

此外,我们还可以有Skydns/Skydock注 23、Discoverd注 24等选择。

注 23 基于DNS的服务发现。https://github.com/crosbymichael/skydock

注 24 Flynn的一个组件,它目前是基于etcd的,但是也可以扩展诸如Zookeeper等分布式存储机制。https://github.com/flynn/discoverd

集群管理

围绕Docker使用场景的开源集群管理软件有很多,比如Geard、Fleet、Consul及Serf等,这些软件都是随着Docker应运而生的;此外还有很多老牌的集群管理软件,比如Mesos等也可以很好的结合Docker使用。

  • Serf和Consul

Serf注 25是一个基于Gossip协议去中心的服务器发现和集群管理工具,它非常轻量,高可用并具备容错机制。

注 25 http://www.serfdom.io/

Consul注 26是一个服务发现和集群配置共享的软件,除了K/V store功能之外,它还支持跨数据中心及容错功能,并能进行服务健康监测。

注 26 http://www.consul.io/

这两个软件都Vagrant作者所在公司HashiCorp注 27发布的产品,这个公司也值得大家关注。

注 27 http://www.hashicorp.com/products

  • Apache Mesos & Marathon & deimos & etc.

Mesos用于对多个节点的资源进行管理,它将多台服务器作为一台“虚拟机”看待,并在这台虚拟机上分配资源,用户通过使用framework进行资源管理。Marathon是一个Mesos的framework,用来启动、管理需要长时间运行的任务。deimos则是一个为Mesos准备的Docker插件。

其它工具

Cloud Foundry在5月份发布的Docker版的BOSH工具,有兴趣的读者可以参考一下Decker注 28项目。

注 28 Decker = Docker + Cloud Foundry. http://www.cloudcredo.com/decker-docker-cloud-foundry/

另外Clocker注 29这个项目也比较有意思,它基于Apache Brooklyn(目前还在孵化器中),能在多云环境下基于Docker容器进行应用部署。这个项目的扩展性很好,非常方便自己定制。不过项目还太年轻,要想使用的话恐怕还需要些时日。

注 29 https://github.com/brooklyncentral/clocker

5.3.2. 和OS的深度结合

在Fedora上使用的systemd注 30就已经提供了集成容器和虚拟机的功能。

注 30 systemd是用来替代Linux中init系统的系统软件,目前已经在Fedora/RHEL等中采用

Docker除了能在各种主流Linux上使用之外,还出现了有专为运行Docker容器而定制的OS了,比如CoreOS注 31,RedHat的Atomic注 32

注 31 https://coreos.com/ ,在6月末刚刚宣布获得了八百万美元的A轮融资
注 32 http://www.projectatomic.io/

CoreOS

CoreOS是一个精简版的Linux,可以运行在既有硬件或者云上,它也是一个最近备受关注的项目。CoreOS不提供类似yum或者apt类似的包管理工具,你不需要在CoreOS中安装软件,而是让程序都在Docker容器中去运行。CoreOS使用systemd和fleet来对容器进行管理,通过etcd进行服务发现和配置信息共享。

Atomic

Project Atomic是最近才发布的一个项目,它也是一个瘦身版的Linux,只包含systemd/geard注 33/rpm-OSTree以及Docker组件,专门用来部署和管理Docker容器。它能在接近硬件裸机级别上高性能的运行大量容器,而且它还是基于SELinux的,在安全上也有保障。

注 33 http://openshift.github.io/geard/

5.3.3. Container技术规范化和兼容性

就在DockerCon14开始的前一天,Flynn发布了Pinkerton,一个支持在其它容器中使用Docker镜像的技术。

而另一方面,我们知道除了LXC,Docker之外,还有很多其它容器技术,比如Zones,jail和LMCTFY等,那么试想这么多的容器之上,是否有统一接口、互相兼容或者在容器上加一层封装的可能性呢?比如让一种容器的镜像,能运行到其它容器中?Docker容器已经能互相连接了,会不会异构的容器之间也能进行某种交互呢?

6. 总结

Docker虽然入门和使用起来非常简单,但整个生态系统还是挺庞大的,而且其底层技术也都很复杂,由于篇幅有限及笔者学识不精,也只能说一些皮毛之事,最多只能算是抛块砖而已;而且笔者也有一种意犹未尽的感觉,但是由于篇幅所限,不能说到面面俱到,更多的内容,还请各位读者自己去深入挖掘。

总之笔者认为Docker还是非常有趣的一个东西,值得大家花些时间体验一下,相信在各位的工作中多多少少都能用的上Docker。

 

 

转自:http://liubin.org/2014/08/11/docker-cloud-app-delivery-style/

丰田生产方式的启发

众所周知,日本车在全世界都是很受欢迎的。究其开端,很多人会想到20世纪70年代的石油危机,认为是油价高涨,为尺寸小、油耗低的日本车打开了市场。这固然可以解释一部分原因,但另一方面,为何日本车能够持续受到欢迎,为何日本车能摆脱“价廉质差”的形象,既有优惠的价格又有优异的品质(缺陷率常年很低)。这一切,都与日本汽车厂商所采用的“精益生产”,尤其是丰田开创的“丰田生产方式”(Toyota Product System, TPS)有很大的关联。最近因为与供应链打交道很多,我花了些时间学习这种生产方式。有趣的是,我发现,它的价值不只限于汽车行业,甚至不只限于制造业,对其它许多行业(包括软件行业)。所以下面我讲讲丰田生产方式给我的启示。

 

“丰田生产方式”给我印象最深的要求是,员工必须同时对工作和工艺负责。自从福特发明了“流水线”之后,工人似乎成了机器的附庸,只是完成机器暂时无法完成的工作。生产的终极形态,就是把一切工作变成简单重复劳动,用机器执行。所以,工人的工作也应该简单机械,比如每天按照生产线的运作,以一定节奏拧紧某个螺丝,就是一种典型。而在丰田生产方式下,工人不但要完成简单机械的本职工作,还必须对工艺负责,也就是理解该工作的意义,思考并不断思考改进自己的工作过程——他们既有这个义务,也有这个权力。结果,整条生产线就好像具备了不断改进的活力,而不再由少数专职的“专家”负责优化(实际上专家也负不了那么多责任)。

软件开发/互联网虽然看起来是光鲜亮丽的高科技行业,但不少时候是达不到这种要求的。许多公司并不要求员工去思考和改进,许多员工也更愿意只做简单重复劳动,不愿意开动脑筋去思考和改进自己的工作。所以,无论是工作质量还是工作效率,其实都停留在相当低的水平上。而许多公司的解决办法,无非是找一些“技术牛人”来负责,这就好像汽车生产厂找“工艺专家”来优化生产一样,或许有效果,但不会太明显。与“对工作和工艺负责”的工人类似,有些程序员会积极开动脑筋编写或者学习一些工具软件来改进自己的工作,他们或许不能编写复杂的框架或精深的算法,但确实堪称优秀的程序员——就好比生产线上优秀的工人。可以说,如果公司的大部分员工都具有这样的意识和习惯,公司也支持和提倡这样的工作方式,那么其产品一定不会太差。

“丰田生产方式”中的还有一点要求,即员工一定不能只了解自己的工作,还需要了解自己工作的上下游,学会从配合、团队的角度来理解自己的工作。这样做一方面可以提高配合的流畅程度,降低沟通的成本;另一方面,公司不必准备专门的“后备团队”来应付突发情况——如果某个员工突然缺勤,其上下游工位的成员完全可以临时补缺,不致影响整条生产线。为了做到这一点,公司会要求新员工入职之后不是立即投入最终岗位,而是先在整个生产线(或上下游)的各个环节都工作一定时间,最后才确定具体的工作岗位(在《改变世界的机器》里,作者希望访问本田工厂的一位公关负责人,却被告知“他刚刚入职,现在正在生产线上参加例行劳动”,这种安排的普遍性可见一斑)。

在软件开发中,我们也经常看到“过分专业分工”导致的问题问题。比如程序员就只懂开发,丝毫不懂数据库或者运维知识,全然不理解软件最终价值是提供服务,结果每次数据库或系统出一点问题则束手无策,只能等数据库管理员或运维工程师出面。结果每次耗时相当长,结果却未必让人满意。还有些时候,客服和技术部门之间存在巨大的鸿沟。客服只负责简单传达客户的意见,根本无法帮忙判断问题的性质和解决成本;技术部门只从技术出发解答问题,根本不知道也不愿意了解问题给客户造成了多么严重的影响。其结果就是每次出问题必然扯皮推诿,许多问题解决成本居高不下。

为了保证质量,“丰田生产方式”对质量有严格的要求。在传统的生产过程中,很多企业都会设立专门的质量部门,而且通常是设定在生产线的末端,以保证最终产品的质量。与此相反的是,“丰田生产方式”会把质量落实到生产中的每个环节,其理由是:如果把质量管理的责任全部落在最终的质量部门,生产线上的人就会认为,自己工作时只要照章操作即可,至于质量,反正最后有质量部门去操心。这样,即便质量部门能从严把关,避免缺陷产品出厂,成本也是相当高昂的。而在丰田生产方式下,为了让每个环节都要对质量负责,每个生产环节甚至都具备发现异常时中断生产线的权力。

我曾经反复提倡“程序员要对自己的程序负责”,其实也是这个意思。扩展开来说,要做好真正的产品,产品经理要对质量负责,设计要对质量负责,程序员要对质量负责,测试要对质量负责,运维也需要对质量负责……。这里说的“负责”,不只是纸面上的责任,不是“我做好我份内事就行,最后产品能不能行,有人操心,我管不了”的工作态度,而是工作的使命感,自愿自发地从最终产品的角度来完成好自己的工作。我们已经无数次地看过这样的现象:产品质量无比差劲,谁都不能满意,最后追求起来,却是人人都没有责任。要解决这种问题,就必须把质量意识落实到每一个参与者身上(我曾经看过一本讲质量的书《质量免费》,说的也是这个道理)。

如果遇到意想不到的问题,通常大家都知道要做的是“找到问题的原因并解决”,但很多时候这只是走个形式而已,许多问题还是会一再出现。而丰田生产方式要求,遇到问题寻找原因,一定要问“五个为什么”。例如针对一次机器的故障,五步故障排除法是这样进行的:首先,操作人员询问:机器为何停止?不久会得到答案:由于超负荷工作使熔丝断开。其次,操作人员接着询问:为什么出现超负荷工作?得到答案:轴承没有得到足够润滑。再次,操作人员再次询问:为什么轴承没有得到足够润滑?得到答案:润滑油泵没有很好泵油。接着,操作人员深入询问:为什么油泵没有正常泵油?得到答案:油泵轴严重磨损。最后,操作人员继续寻根究底:为什么油泵轴严重磨损?得到答案:油泵没有装移动式保护罩,金属屑掉入油泵。至此,操作人员找到问题的症结所在,于是给油泵装上保护罩,使赃物不会落入,杜绝了同样的故障再次发生。为什么要问“5个”而不是3个或者4个呢?这是一个经验得到的数值,其主要目的还是要求对问题求根究底,不能敷衍。

在软件开发中,很多问题的解决也是相当敷衍的。面对运行过程中的异常问题,很多时候上级也会要求排查原因,结果却多是:“应该是系统出错了”、“可能网络断了一下吧”、“这个类库大概就是有这类问题,我也没办法”。结果,同样的问题还是不断地出现,每次都需要不断地耗费人力物力财力来解决。很多时候,只有上级领导发火要求“撤查”,才能得到相对满意的答案。相比制造行业下普通工人都必须回答“五个为什么”的要求,许多软件开发行业的从业人员真应该汗颜。

有意思的是,“丰田生产方式”不但对员工提出了要求,对机器也提出了要求。通常的机器,只要能高效地完成其本职工作,就算合格。但丰田生产方式中,机器不但要负责自己的本职工作,还需要具备错误侦测能力。也就是说,一旦出现异常,就能够自我发现并报警。一旦发出报警,生产线上会有专门的人员,以最高优先级来处理异常情况。这样,就避免了机器发生错误,停工或生产次品,却迟迟不能被发现的现象。

在我刚刚开始工作的时候,曾经被项目经理反复教训“你以为自己还是学生写程序吗?根本不考虑各种异常情况,也不做对应的处理”。后来换了其它工作,又被严格要求程序必须能健康运行,要能时刻把控自己程序的运行状态,及时发现异常情况。当时都很难理解,程序要完成自己的本职工作之外,还需要做那么多额外的工作。后来回想起来,才觉得庆幸自己很早领悟到软件开发的“正道”。可是放眼望去,还有那么多系统没有自我侦错能力,导致事故不断,而程序员们每天忙得焦头烂额,却始终不得解放。

以上说了“丰田生产方式”给我启发最深的几点。如果非要说有什么神奇之处,那就是不把人当成机器的附庸,而是让人和机器有机结合——即便做体力劳动的工人,也不能阉割脑力劳动,仅此而已。如果这种生产方式真的有效,为什么只有日本汽车厂商能做好,欧美汽车厂商却做不到呢?我想一个主要的答案在于,这种生产方式对人员有相当高的要求(尤其是培养教育生产线上的工人,其难度远超一般人的想象),并且落实起来需要很长的磨合时间。日本的国民素质相对较高,工作的流动性相对较低,所以有相当长的时间来进行磨合,把人与机器,人与人的配合协调到相当顺畅的程度。据统计,一家工厂从开始导入丰田生产方式,到最终顺畅运转,很可能要花费十年甚至二十年的时间(参考《改变世界的机器》)。不过一旦确立了这种生产方式,工厂几乎可以持续地以很高的质量和效率进行生产。几乎实现了“职员终身制”的日本公司,天然就具备这样的条件。

从这个角度出发,我们也可以理解很多风险投资人说“投资看的是人和团队,而不是项目”的理由。团队的组成,成员的配合,文化的建立,都不是一朝一夕的工夫。但是,如果一支团队的组成合理、配合顺畅、文化健康,找到合适项目并取得成功的几率是相当大的。相反,草台班子即便误打误撞赶上了一个机会,也很难继续成功,这样的例子实在太多了。

最后补充点题外话:日本汽车的质量普遍胜过欧美汽车,原因有很多,绝不仅仅是“丰田生产方式”那么简单。比如日本车的设计更侧重市场先行,更愿意采用成熟的技术,而欧洲车的设计更侧重技术先行,所以天然就要冒更多的风险。再比如,日本汽车厂商更倾向于与供应商构建合适健康的合作生态,而美国汽车厂商更喜欢硬梆梆的“合同采购”模式……

附:

对丰田生产方式感兴趣的朋友,可以阅读《丰田生产方式》《改变世界的机器》

 

转自:乱象,印迹  丰田生产方式的启发

 

分布式系统的事务处理

当我们在生产线上用一台服务器来提供数据服务的时候,我会遇到如下的两个问题:

1)一台服务器的性能不足以提供足够的能力服务于所有的网络请求。

2)我们总是害怕我们的这台服务器停机,造成服务不可用或是数据丢失。

于是我们不得不对我们的服务器进行扩展,加入更多的机器来分担性能上的问题,以及来解决单点故障问题。 通常,我们会通过两种手段来扩展我们的数据服务:

1)数据分区:就是把数据分块放在不同的服务器上(如:uid % 16,一致性哈希等)。

2)数据镜像:让所有的服务器都有相同的数据,提供相当的服务。

对于第一种情况,我们无法解决数据丢失的问题,单台服务器出问题时,会有部分数据丢失。所以,数据服务的高可用性只能通过第二种方法来完成——数据的冗余存储(一般工业界认为比较安全的备份数应该是3份,如:Hadoop和Dynamo)。 但是,加入更多的机器,会让我们的数据服务变得很复杂,尤其是跨服务器的事务处理,也就是跨服务器的数据一致性。这个是一个很难的问题。 让我们用最经典的Use Case:“A帐号向B帐号汇钱”来说明一下,熟悉RDBMS事务的都知道从帐号A到帐号B需要6个操作:

  1. 从A帐号中把余额读出来。
  2. 对A帐号做减法操作。
  3. 把结果写回A帐号中。
  4. 从B帐号中把余额读出来。
  5. 对B帐号做加法操作。
  6. 把结果写回B帐号中。

为了数据的一致性,这6件事,要么都成功做完,要么都不成功,而且这个操作的过程中,对A、B帐号的其它访问必需锁死,所谓锁死就是要排除其它的读写操作,不然会有脏数据的问题,这就是事务。那么,我们在加入了更多的机器后,这个事情会变得复杂起来:

 

1)在数据分区的方案中:如果A帐号和B帐号的数据不在同一台服务器上怎么办?我们需要一个跨机器的事务处理。也就是说,如果A的扣钱成功了,但B的加钱不成功,我们还要把A的操作给回滚回去。这在跨机器的情况下,就变得比较复杂了。

2)在数据镜像的方案中:A帐号和B帐号间的汇款是可以在一台机器上完成的,但是别忘了我们有多台机器存在A帐号和B帐号的副本。如果对A帐号的汇钱有两个并发操作(要汇给B和C),这两个操作发生在不同的两台服务器上怎么办?也就是说,在数据镜像中,在不同的服务器上对同一个数据的写操作怎么保证其一致性,保证数据不冲突?

同时,我们还要考虑性能的因素,如果不考虑性能的话,事务得到保证并不困难,系统慢一点就行了。除了考虑性能外,我们还要考虑可用性,也就是说,一台机器没了,数据不丢失,服务可由别的机器继续提供。 于是,我们需要重点考虑下面的这么几个情况:

1)容灾:数据不丢、结点的Failover

2)数据的一致性:事务处理

3)性能:吞吐量 、 响应时间

前面说过,要解决数据不丢,只能通过数据冗余的方法,就算是数据分区,每个区也需要进行数据冗余处理。这就是数据副本:当出现某个节点的数据丢失时可以从副本读到,数据副本是分布式系统解决数据丢失异常的唯一手段。所以,在这篇文章中,简单起见,我们只讨论在数据冗余情况下考虑数据的一致性和性能的问题。简单说来:

1)要想让数据有高可用性,就得写多份数据。

2)写多份的问题会导致数据一致性的问题。

3)数据一致性的问题又会引发性能问题

这就是软件开发,按下了葫芦起了瓢。

一致性模型

说起数据一致性来说,简单说有三种类型(当然,如果细分的话,还有很多一致性模型,如:顺序一致性,FIFO一致性,会话一致性,单读一致性,单写一致性,但为了本文的简单易读,我只说下面三种):

1)Weak 弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些cache系统,网络游戏其它玩家的数据和你没什么关系,VOIP这样的系统,或是百度搜索引擎(呵呵)。

2)Eventually 最终一致性:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。

3)Strong 强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS,Azure Table都是强一致性的。

从这三种一致型的模型上来说,我们可以看到,Weak和Eventually一般来说是异步冗余的,而Strong一般来说是同步冗余的,异步的通常意味着更好的性能,但也意味着更复杂的状态控制。同步意味着简单,但也意味着性能下降。 好,让我们由浅入深,一步一步地来看有哪些技术:

Master-Slave

首先是Master-Slave结构,对于这种加构,Slave一般是Master的备份。在这样的系统中,一般是如下设计的:

1)读写请求都由Master负责。

2)写请求写到Master上后,由Master同步到Slave上。

从Master同步到Slave上,你可以使用异步,也可以使用同步,可以使用Master来push,也可以使用Slave来pull。 通常来说是Slave来周期性的pull,所以,是最终一致性。这个设计的问题是,如果Master在pull周期内垮掉了,那么会导致这个时间片内的数据丢失。如果你不想让数据丢掉,Slave只能成为Read-Only的方式等Master恢复。

当然,如果你可以容忍数据丢掉的话,你可以马上让Slave代替Master工作(对于只负责计算的结点来说,没有数据一致性和数据丢失的问题,Master-Slave的方式就可以解决单点问题了) 当然,Master Slave也可以是强一致性的, 比如:当我们写Master的时候,Master负责先写自己,等成功后,再写Slave,两者都成功后返回成功,整个过程是同步的,如果写Slave失败了,那么两种方法,一种是标记Slave不可用报错并继续服务(等Slave恢复后同步Master的数据,可以有多个Slave,这样少一个,还有备份,就像前面说的写三份那样),另一种是回滚自己并返回写失败。(注:一般不先写Slave,因为如果写Master自己失败后,还要回滚Slave,此时如果回滚Slave失败,就得手工订正数据了)你可以看到,如果Master-Slave需要做成强一致性有多复杂。

Master-Master

Master-Master,又叫Multi-master,是指一个系统存在两个或多个Master,每个Master都提供read-write服务。这个模型是Master-Slave的加强版,数据间同步一般是通过Master间的异步完成,所以是最终一致性。 Master-Master的好处是,一台Master挂了,别的Master可以正常做读写服务,他和Master-Slave一样,当数据没有被复制到别的Master上时,数据会丢失。很多数据库都支持Master-Master的Replication的机制。

另外,如果多个Master对同一个数据进行修改的时候,这个模型的恶梦就出现了——对数据间的冲突合并,这并不是一件容易的事情。看看Dynamo的Vector Clock的设计(记录数据的版本号和修改者)就知道这个事并不那么简单,而且Dynamo对数据冲突这个事是交给用户自己搞的。就像我们的SVN源码冲突一样,对于同一行代码的冲突,只能交给开发者自己来处理。(在本文后后面会讨论一下Dynamo的Vector Clock)

Two/Three Phase Commit

这个协议的缩写又叫2PC,中文叫两阶段提交。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。 两阶段提交的算法如下:

第一阶段

  1. 协调者会问所有的参与者结点,是否可以执行提交操作。
  2. 各个参与者开始事务执行的准备工作:如:为资源上锁,预留资源,写undo/redo log……
  3. 参与者响应协调者,如果事务的准备工作成功,则回应“可以提交”,否则回应“拒绝提交”。

第二阶段

  • 如果所有的参与者都回应“可以提交”,那么,协调者向所有的参与者发送“正式提交”的命令。参与者完成正式提交,并释放所有资源,然后回应“完成”,协调者收集各结点的“完成”回应后结束这个Global Transaction。
  • 如果有一个参与者回应“拒绝提交”,那么,协调者向所有的参与者发送“回滚操作”,并释放所有资源,然后回应“回滚完成”,协调者收集各结点的“回滚”回应后,取消这个Global Transaction。

我们可以看到,2PC说白了就是第一阶段做Vote,第二阶段做决定的一个算法,也可以看到2PC这个事是强一致性的算法。在前面我们讨论过Master-Slave的强一致性策略,和2PC有点相似,只不过2PC更为保守一些——先尝试再提交。 2PC用的是比较多的,在一些系统设计中,会串联一系列的调用,比如:A -> B -> C -> D,每一步都会分配一些资源或改写一些数据。比如我们B2C网上购物的下单操作在后台会有一系列的流程需要做。如果我们一步一步地做,就会出现这样的问题,如果某一步做不下去了,那么前面每一次所分配的资源需要做反向操作把他们都回收掉,所以,操作起来比较复杂。现在很多处理流程(Workflow)都会借鉴2PC这个算法,使用 try -> confirm的流程来确保整个流程的能够成功完成。 举个通俗的例子,西方教堂结婚的时候,都有这样的桥段:

1)牧师分别问新郎和新娘:你是否愿意……不管生老病死……(询问阶段)

2)当新郎和新娘都回答愿意后(锁定一生的资源),牧师就会说:我宣布你们……(事务提交)

这是多么经典的一个两阶段提交的事务处理。 另外,我们也可以看到其中的一些问题, A)其中一个是同步阻塞操作,这个事情必然会非常大地影响性能。 B)另一个主要的问题是在TimeOut上,比如,

1)如果第一阶段中,参与者没有收到询问请求,或是参与者的回应没有到达协调者。那么,需要协调者做超时处理,一旦超时,可以当作失败,也可以重试。

2)如果第二阶段中,正式提交发出后,如果有的参与者没有收到,或是参与者提交/回滚后的确认信息没有返回,一旦参与者的回应超时,要么重试,要么把那个参与者标记为问题结点剔除整个集群,这样可以保证服务结点都是数据一致性的。

3)糟糕的情况是,第二阶段中,如果参与者收不到协调者的commit/fallback指令,参与者将处于“状态未知”阶段,参与者完全不知道要怎么办,比如:如果所有的参与者完成第一阶段的回复后(可能全部yes,可能全部no,可能部分yes部分no),如果协调者在这个时候挂掉了。那么所有的结点完全不知道怎么办(问别的参与者都不行)。为了一致性,要么死等协调者,要么重发第一阶段的yes/no命令。

两段提交最大的问题就是第3)项,如果第一阶段完成后,参与者在第二阶没有收到决策,那么数据结点会进入“不知所措”的状态,这个状态会block住整个事务。也就是说,协调者Coordinator对于事务的完成非常重要,Coordinator的可用性是个关键。 因些,我们引入三段提交,三段提交在Wikipedia上的描述如下,他把二段提交的第一个段break成了两段:询问,然后再锁资源。最后真正提交。

三段提交的核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源

理论上来说,如果第一阶段所有的结点返回成功,那么有理由相信成功提交的概率很大。这样一来,可以降低参与者Cohorts的状态未知的概率。也就是说,一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了。这一点很重要。

其实,三段提交是一个很复杂的事情,实现起来相当难,而且也有一些问题。

看到这里,我相信你有很多很多的问题,你一定在思考2PC/3PC中各种各样的失败场景,你会发现Timeout是个非常难处理的事情,因为网络上的Timeout在很多时候让你无所事从,你也不知道对方是做了还是没有做。于是你好好的一个状态机就因为Timeout成了个摆设

一个网络服务会有三种状态:1)Success,2)Failure,3)Timeout,第三个绝对是恶梦,尤其在你需要维护状态的时候

Two Generals Problem(两将军问题)

Two Generals Problem 两将军问题是这么一个思维性实验问题: 有两支军队,它们分别有一位将军领导,现在准备攻击一座修筑了防御工事的城市。这两支军队都驻扎在那座城市的附近,分占一座山头。一道山谷把两座山分隔开来,并且两位将军唯一的通信方式就是派各自的信使来往于山谷两边。不幸的是,这个山谷已经被那座城市的保卫者占领,并且存在一种可能,那就是任何被派出的信使通过山谷是会被捕。 请注意,虽然两位将军已经就攻击那座城市达成共识,但在他们各自占领山头阵地之前,并没有就进攻时间达成共识。两位将军必须让自己的军队同时进攻城市才能取得成功。因此,他们必须互相沟通,以确定一个时间来攻击,并同意就在那时攻击。如果只有一个将军进行攻击,那么这将是一个灾难性的失败。 这个思维实验就包括考虑他们如何去做这件事情。下面是我们的思考:

1)第一位将军先发送一段消息“让我们在上午9点开始进攻”。然而,一旦信使被派遣,他是否通过了山谷,第一位将军就不得而知了。任何一点的不确定性都会使得第一位将军攻击犹豫,因为如果第二位将军不能在同一时刻发动攻击,那座城市的驻军就会击退他的军队的进攻,导致他的军对被摧毁。

2)知道了这一点,第二位将军就需要发送一个确认回条:“我收到您的邮件,并会在9点的攻击。”但是,如果带着确认消息的信使被抓怎么办?所以第二位将军会犹豫自己的确认消息是否能到达。

3)于是,似乎我们还要让第一位将军再发送一条确认消息——“我收到了你的确认”。然而,如果这位信使被抓怎么办呢?

4)这样一来,是不是我们还要第二位将军发送一个“确认收到你的确认”的信息。

靠,于是你会发现,这事情很快就发展成为不管发送多少个确认消息,都没有办法来保证两位将军有足够的自信自己的信使没有被敌军捕获。

这个问题是无解的。两个将军问题和它的无解证明首先由E.A.Akkoyunlu,K.Ekanadham和R.V.Huber于1975年在《一些限制与折衷的网络通信设计》一文中发表,就在这篇文章的第73页中一段描述两个黑帮之间的通信中被阐明。 1978年,在Jim Gray的《数据库操作系统注意事项》一书中(从第465页开始)被命名为两个将军悖论。作为两个将军问题的定义和无解性的证明的来源,这一参考被广泛提及。

这个实验意在阐明:试图通过建立在一个不可靠的连接上的交流来协调一项行动的隐患和设计上的巨大挑战。

从工程上来说,一个解决两个将军问题的实际方法是使用一个能够承受通信信道不可靠性的方案,并不试图去消除这个不可靠性,但要将不可靠性削减到一个可以接受的程度。比如,第一位将军排出了100位信使并预计他们都被捕的可能性很小。在这种情况下,不管第二位将军是否会攻击或者受到任何消息,第一位将军都会进行攻击。另外,第一位将军可以发送一个消息流,而第二位将军可以对其中的每一条消息发送一个确认消息,这样如果每条消息都被接收到,两位将军会感觉更好。然而我们可以从证明中看出,他们俩都不能肯定这个攻击是可以协调的。他们没有算法可用(比如,收到4条以上的消息就攻击)能够确保防止仅有一方攻击。再者,第一位将军还可以为每条消息编号,说这是1号,2号……直到n号。这种方法能让第二位将军知道通信信道到底有多可靠,并且返回合适的数量的消息来确保最后一条消息被接收到。如果信道是可靠的话,只要一条消息就行了,其余的就帮不上什么忙了。最后一条和第一条消息丢失的概率是相等的。

两将军问题可以扩展成更变态的拜占庭将军问题 (Byzantine Generals Problem),其故事背景是这样的:拜占庭位于现在土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了防御目的,因此每个军队都分隔很远,将军与将军之间只能靠信差传消息。 在战争的时候,拜占庭军队内所有将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,军队可能有叛徒和敌军间谍,这些叛徒将军们会扰乱或左右决策的过程。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,这就是拜占庭将军问题。

Paxos算法

Wikipedia上的各种Paxos算法的描述非常详细,大家可以去围观一下。

Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个「一致性算法」以保证每个节点看到的指令一致。一个通用的一致性算法可以应用在许多场景中,是分布式计算中的重要问题。从20世纪80年代起对于一致性算法的研究就没有停止过。

Notes:Paxos算法是莱斯利·兰伯特(Leslie Lamport,就是 LaTeX 中的”La”,此人现在在微软研究院)于1990年提出的一种基于消息传递的一致性算法。由于算法难以理解起初并没有引起人们的重视,使Lamport在八年后1998年重新发表到ACM Transactions on Computer Systems上(The Part-Time Parliament)。即便如此paxos算法还是没有得到重视,2001年Lamport 觉得同行无法接受他的幽默感,于是用容易接受的方法重新表述了一遍(Paxos Made Simple)。可见Lamport对Paxos算法情有独钟。近几年Paxos算法的普遍使用也证明它在分布式一致性算法中的重要地位。2006年Google的三篇论文初现“云”的端倪,其中的Chubby Lock服务使用Paxos作为Chubby Cell中的一致性算法,Paxos的人气从此一路狂飙。(Lamport 本人在 他的blog 中描写了他用9年时间发表这个算法的前前后后)

注:Amazon的AWS中,所有的云服务都基于一个ALF(Async Lock Framework)的框架实现的,这个ALF用的就是Paxos算法。我在Amazon的时候,看内部的分享视频时,设计者在内部的Principle Talk里说他参考了ZooKeeper的方法,但他用了另一种比ZooKeeper更易读的方式实现了这个算法。

简单说来,Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos算法基本上来说是个民主选举的算法——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以Paxos算法需要集群中的结点是单数)。

这个算法有两个阶段(假设这个有三个结点:A,B,C):

第一阶段:Prepare阶段

A把申请修改的请求Prepare Request发给所有的结点A,B,C。注意,Paxos算法会有一个Sequence Number(你可以认为是一个提案号,这个数不断递增,而且是唯一的,也就是说A和B不可能有相同的提案号),这个提案号会和修改请求一同发出,任何结点在“Prepare阶段”时都会拒绝其值小于当前提案号的请求。所以,结点A在向所有结点申请修改请求的时候,需要带一个提案号,越新的提案,这个提案号就越是是最大的。

如果接收结点收到的提案号n大于其它结点发过来的提案号,这个结点会回应Yes(本结点上最新的被批准提案号),并保证不接收其它<n的提案。这样一来,结点上在Prepare阶段里总是会对最新的提案做承诺。

优化:在上述 prepare 过程中,如果任何一个结点发现存在一个更高编号的提案,则需要通知 提案人,提醒其中断这次提案。

第二阶段:Accept阶段

如果提案者A收到了超过半数的结点返回的Yes,然后他就会向所有的结点发布Accept Request(同样,需要带上提案号n),如果没有超过半数的话,那就返回失败。

当结点们收到了Accept Request后,如果对于接收的结点来说,n是最大的了,那么,它就会修改这个值,如果发现自己有一个更大的提案号,那么,结点就会拒绝修改。

我们可以看以,这似乎就是一个“两段提交”的优化。其实,2PC/3PC都是分布式一致性算法的残次版本,Google Chubby的作者Mike Burrows说过这个世界上只有一种一致性算法,那就是Paxos,其它的算法都是残次品。

我们还可以看到:对于同一个值的在不同结点的修改提案就算是在接收方被乱序收到也是没有问题的。

关于一些实例,你可以看一下Wikipedia中文中的“Paxos样例”一节,我在这里就不再多说了。对于Paxos算法中的一些异常示例,大家可以自己推导一下。你会发现基本上来说只要保证有半数以上的结点存活,就没有什么问题。

多说一下,自从Lamport在1998年发表Paxos算法后,对Paxos的各种改进工作就从未停止,其中动作最大的莫过于2005年发表的Fast Paxos。无论何种改进,其重点依然是在消息延迟与性能、吞吐量之间作出各种权衡。为了容易地从概念上区分二者,称前者Classic Paxos,改进后的后者为Fast Paxos。

总结

Google App Engine的co-founder Ryan Barrett在2009年的google i/o上的演讲《Transaction Across DataCenter》(视频: http://www.youtube.com/watch?v=srOgpXECblk

前面,我们说过,要想让数据有高可用性,就需要冗余数据写多份。写多份的问题会带来一致性的问题,而一致性的问题又会带来性能问题。这就是著名的CAP理论:一致性,可用性,分区容忍性,你只可能要其中的两个。

NWR模型

最后我还想提一下Amazon Dynamo的NWR模型。这个NWR模型把CAP的选择权交给了用户,让用户自己的选择你的CAP中的哪两个

所谓NWR模型。N代表N个备份,W代表要写入至少W份才认为成功,R表示至少读取R个备份。配置的时候要求W+R > N。 因为W+R > N, 所以 R > N-W 这个是什么意思呢?就是读取的份数一定要比总备份数减去确保写成功的倍数的差值要大。

也就是说,每次读取,都至少读取到一个最新的版本。从而不会读到一份旧数据。当我们需要高可写的环境的时候,我们可以配置W = 1 如果N=3 那么R = 3。 这个时候只要写任何节点成功就认为成功,但是读的时候必须从所有的节点都读出数据。如果我们要求读的高效率,我们可以配置 W=N R=1。这个时候任何一个节点读成功就认为成功,但是写的时候必须写所有三个节点成功才认为成功。

NWR模型的一些设置会造成脏数据的问题,因为这很明显不是像Paxos一样是一个强一致的东西,所以,可能每次的读写操作都不在同一个结点上,于是会出现一些结点上的数据并不是最新版本,但却进行了最新的操作。

所以,Amazon Dynamo引了数据版本的设计。也就是说,如果你读出来数据的版本是v1,当你计算完成后要回填数据后,却发现数据的版本号已经被人更新成了v2,那么服务器就会拒绝你。版本这个事就像“乐观锁”一样。

但是,对于分布式和NWR模型来说,版本也会有恶梦的时候——就是版本冲的问题,比如:我们设置了N=3 W=1,如果A结点上接受了一个值,版本由v1 -> v2,但还没有来得及同步到结点B上(异步的,应该W=1,写一份就算成功),B结点上还是v1版本,此时,B结点接到写请求,按道理来说,他需要拒绝掉,但是他一方面并不知道别的结点已经被更新到v2,另一方面他也无法拒绝,因为W=1,所以写一分就成功了。于是,出现了严重的版本冲突。

Amazon的Dynamo把版本冲突这个问题巧妙地回避掉了——版本冲这个事交给用户自己来处理。

于是,Dynamo引入了Vector Clock(矢量钟?!)这个设计。这个设计让每个结点各自记录自己的版本信息,也就是说,对于同一个数据,需要记录两个事:1)谁更新的我,2)我的版本号是什么。

下面,我们来看一个操作序列:

1)一个写请求,第一次被节点A处理了。节点A会增加一个版本信息(A,1)。我们把这个时候的数据记做D1(A,1)。 然后另外一个对同样key的请求还是被A处理了于是有D2(A,2)。这个时候,D2是可以覆盖D1的,不会有冲突产生。

2)现在我们假设D2传播到了所有节点(B和C),B和C收到的数据不是从客户产生的,而是别人复制给他们的,所以他们不产生新的版本信息,所以现在B和C所持有的数据还是D2(A,2)。于是A,B,C上的数据及其版本号都是一样的。

3)如果我们有一个新的写请求到了B结点上,于是B结点生成数据D3(A,2; B,1),意思是:数据D全局版本号为3,A升了两新,B升了一次。这不就是所谓的代码版本的log么?

4)如果D3没有传播到C的时候又一个请求被C处理了,于是,以C结点上的数据是D4(A,2; C,1)。

5)好,最精彩的事情来了:如果这个时候来了一个读请求,我们要记得,我们的W=1 那么R=N=3,所以R会从所有三个节点上读,此时,他会读到三个版本:

    • A结点:D2(A,2)
    • B结点:D3(A,2;  B,1);
    • C结点:D4(A,2;  C,1)

6)这个时候可以判断出,D2已经是旧版本(已经包含在D3/D4中),可以舍弃。

7)但是D3和D4是明显的版本冲突。于是,交给调用方自己去做版本冲突处理。就像源代码版本管理一样。

很明显,上述的Dynamo的配置用的是CAP里的A和P。

我非常推大家都去看看这篇论文:《Dynamo:Amazon’s Highly Available Key-Value Store》,如果英文痛苦,你可以看看译文(译者不详)。

(全文完)

(转载自 酷 壳 – CoolShell.cn ,请勿用于任何商业用途)