赞助商

[译]从LinkedIn,Apache Kafka到Unix哲学

lytword2015-09-14楼主
原文链接:
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中学到了一些皮毛,那它在今天将发展成什么样子。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/134226037.jpg[/img]


特别是,我觉得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个空格分隔部件:


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/-306847819.jpg[/img]


现在我们已经提取出了路径,接下来就可以确定服务器上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


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/-766589424.jpg[/img]


如果你并不熟悉Unix工具的话,上述命令看起来有点难懂,但它真的很强大。这几条简单的命令能够在几秒钟内处理千兆字节的日志文件,而且你可以根据需要,非常容易地更改分析内容。比如说,你现在想统计访问次数最多的客户端IP地址,而不是最热门的那几个网页,只需更改awk的参数'{print $1}'

按需求使用这些组合命令awk, sed, grep, sort, uniq , xargs的话,海量数据分析能够在几分钟内完成,其性能表现让人出乎意料。这不是巧合,是Unix设计哲学的结果。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/-1246790721.jpg[/img]


Unix哲学就是一套设计准则, 在20世纪60年代末与70年代初,这些准则是在设计和实现Unix系统时才逐渐出现的。关于Unix哲学有非常多的阐述,但有两点脱颖而出,由Doug McIlroy, Elliot Pinson 和Berk Tague在1978年描述如下:

1. 每个程序只做好一件事。如果有新的任务需求,那就编写一个新的程序而不是在一个旧的程序上加一个新的“功能”,使其越来越复杂。

2. 期望每个程序的输出都能是其他程序的输入,即使是未知的程序。

这些准则是能把各种程序连接成管道的基础,而有了管道就能完成复杂的处理任务。这里的核心思想就是一个程序不知道或者说不需要关心它的输入是从哪里来的,输出要往哪里去:可能是一个文件,或者操作系统的其他程序,又或者是完全由某个开发者开发的程序。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1962725447.jpg[/img]


操作系统附带的工具都是通用的,但是它们被设计成能够组合起来执行特定任务的较大的程序。

Unix的设计者遵循这种程序设计方法所带来的好处有点像几十年后出现的Agile 和DevOps的成果:脚本与自动化,快速原型编码(rapid prototyping),增量迭代,友好的测试(being friendly to experimentation),以及将大型项目分解成可控的模块。再加上CA的改变。(Plus ?a change.)


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1526264852.jpg[/img]


当你在shell里为2个命令加上管道的标示符,那么shell就会同时启动这2个命令程序,然后将第一个程序处理的输出结果作为第二个程序的输入。这种连接机构由操作系统提供管道系统调用服务。
请注意,这种线性处理不是由程序本身来完成的,而是靠shell——这就使得每个程序之间是“松耦合”,这使得程序不用担心它们的输入从哪里来,输出要往哪里去。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/956585814.jpg[/img]


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


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1861235921.jpg[/img]


他们也很早就意识到进程间的通信机制(管道)与读写文件机制非常相似。我们现在称之为输入重定向(用一个文件内容作为一个程序的输入)和输出重定向(将一个程序的结果输出到一个文件)。

Unix程序之所以能够有这么高的组合灵活性,是因为这些程序都遵循相同的接口:大多数程序都有一个数据输入流(stdin)和两个输出流(stdout常规数据输出流和stderr错误与诊断信息输出流)。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/176194616.jpg[/img]


程序通常除了读stdin流和写stdout流之外,它们还可以做其它的事,比如读取和写入文件,在网络上通信,或者绘制一个用户界面。然而,该stdin/stdout通信被认为是数据从一个Unix工具流向另一个的最主要的途径。

其实,最令人高兴的事莫过于任何人可以使用任意语言轻松地实现stdin/stdout接口。你可以开发自己的工具,只要其遵循这个接口,那么你的工具能和其他标准工具一样高效,并能作为操作系统的一部分。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1347099347.jpg[/img]


举个例子,当你想分析一个web服务器的日志文件,或许你想知道来自每个国家的访问量有多少。但是这个日志并没有告诉你国家信息,只是告诉了你IP地址,那么你可以通过IP地理数据库将IP地址转换成国家。默认情况下,你的操作系统并没有附带这个数据库,但是你可以编写一个将IP地址放进stdin流,将输出国家放进stdout流的工具。

一旦你把这个工具写好了,你就可以将它使用在我们之前讨论过的数据处理管道里,它将会工作地很好。如果你已经使用了Unix一段时间,那么这样做似乎很容易,但是我想强调这样做非常了不起:你自己的代码程序与操作系统附带的那些工具地位是一样的。

图形用户界面的程序和Web应用似乎不那么容易能够被拓展或者像这样串起来。你不能用管道将Gmail传送给一个独立的搜索引擎应用,然后将结果输出到wiki上。但是现在是个例外,跟往常不一样的是,现在也有程序能够像Unix工具一样能够协同工作。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/-1842205947.jpg[/img]


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


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1953468426.jpg[/img]


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


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1015817052.jpg[/img]


Unix系统里可组合性和拓展性是指什么?客户端(Clients)能做任何他们喜欢的事(因为他们是程序代码),但是DB Server大多是在做存储和检索数据的工作,运行你写的任意代码并不是它们的首要任务。

也就是说,许多数据库提供了一些方法,你能用自己的代码去扩展数据库服务器功能。例如,在许多关系型数据库中,让你自己写存储过程,基本的程序语言如PL / SQL(和一些让你在通用编程语言上能运行代码比如JavaScript)。然而,你可以在存储过程中所做的事情是有限的。

其他拓展方式,像某些数据库支持用户自定义数据类型(这是Postgres的早期设计目标),或者支持可插拔的数据引擎。基本上,这些都是插件的接口:
你可以在数据库服务器中运行你的代码,只要你的模块遵循一个特定用途的数据库服务器的插件接口。

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


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/1923472712.jpg[/img]


这种设计的结果是,你不能用管道将一个数据库与另一个连接起来,即使他们有相同的数据模型。你也不能将自己的代码插入到数据库的内部处理管道(除非该服务器已明确提供了一个扩展点,如触发器)。

我觉得数据库设计是很以自我为中心的。数据库似乎认为它是你的宇宙的中心:这可能是你要存储和查询数据,数据真正来源,和所有查询最终抵达的唯一地方。你得到管道数据最近的方式是通过批量加载和批量倾倒(bulk-dumping)(备份)操作,但这些操作不能真正使用到数据库的任何特性,如查询规划和索引。

如果数据库遵循Unix的设计思想,那么它将是基于一小部分核心原语,你可以很容易地进行结合,拓展和随意更换。而实际上,数据库犹如极其复杂,庞大的野兽。Unix也承认操作系统不会让你真的为所欲为,但是它鼓励你去拓展它,你或许只需一个程序就能实现数据库系统想要实现所有的功能。


[img=LinkedIn,ApacheKafka,Unix]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150911/133146643.jpg[/img]


在只有一个数据库的简单应用中,这种设计可能还不错。

然而,在许多复杂的应用中,他们用各种不同的方式处理他们的数据:对于OLTP需要快速随机存取,数据分析需要大序列扫描,全文搜索需要倒排索引,用于连接的数据图索引,推荐引擎需要机器学习系统,消息通知需要的推送机制,快速读取需要各种不同的缓存表示数据,等等。

一个通用数据库可以尝试将所有这些功能集中在一个产品上(“一个适合所有”),但十有八九,这个数据库不会为了某个特定的功能而只执行一个工具程序。在实践中,你可以经常通过联合各种不同的数据存储和检索系统得到最好的结果:例如,你可以把相同的数据并将其存储在关系数据库中,方便其随机访问,在Elasticsearch进行全文搜索,在Hadoop中做柱状格式分析,并以非规范化格式在memcached中缓存。

完整内容点此查看
回复
高级模式
解放号
解放号

加入小组

一个平台,一种生活,解放号 启航