[译]从LinkedIn,Apache Kafka到Unix哲学
Posted 架构师
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译]从LinkedIn,Apache Kafka到Unix哲学相关的知识,希望对你有一定的参考价值。
当我在为我的书做 研究时,我意识到现代软件工程仍然需要从20世纪70年代学习很多东西。在这样一个快速发展的领域,我们往往有一种倾向,认为旧观念一无是处??因此,最 终我们不得不一次又一次地为同样的教训买单,这真艰难。尽管现在电脑已经越来越快,数据量也越来越大,需求也越来越复杂,许多老观点至今仍有很大的用武之 地。
在这篇文章中,我想强调一个陈旧的观念,但它现在更应该被关注:Unix哲学(philosophy)。我将展示这种哲学与主流数据库设计方式截然不同的原因;并探索如果现代分布式数据系统从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"
许多工具能够利用这些日志文件,并生成您的网站流量报告,但为了练练手,我们建立一个自己的工具,使用一些基本的Unix工具,在我们的网站上确定5个最热门的网址。首先,我们需要提取出被请求的URL路径,这里我们可以使用awk.
awk并不知道nginx的日志格式??它只是将日志文件当作文本文件处理。默认情况下,awk一次只能处理一行输入,一行靠空格分隔,使之能够作为变量的空格分隔部件$1, $2, etc。在nginx的日志示例中,请求的URL路径是第7个空格分隔部件:
现在我们已经提取出了路径,接下来就可以确定服务器上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
按需求使用这些组合命令awk, sed, grep, sort, uniq , xargs的话,海量数据分析能够在几分钟内完成,其性能表现让人出乎意料。这不是巧合,是Unix设计哲学的结果。
Unix哲学就是一套设计准 则, 在20世纪60年代末与70年代初,这些准则是在设计和实现Unix系统时才逐渐出现的。关于Unix哲学有非常多的阐述,但有两点脱颖而出,由Doug McIlroy, Elliot Pinson 和Berk Tague在1978年描述如下:
1. 每个程序只做好一件事。如果有新的任务需求,那就编写一个新的程序而不是在一个旧的程序上加一个新的“功能”,使其越来越复杂。
2. 期望每个程序的输出都能是其他程序的输入,即使是未知的程序。
这些准则是能把各种程序连接成管道的基础,而有了管道就能完成复杂的处理任务。这里的核心思想就是一个程序不知道或者说不需要关心它的输入是从哪里来的,输出要往哪里去:可能是一个文件,或者操作系统的其他程序,又或者是完全由某个开发者开发的程序。
操作系统附带的工具都是通用的,但是它们被设计成能够组合起来执行特定任务的较大的程序。
Unix的设计者遵循这种程序 设计方法所带来的好处有点像几十年后出现的Agile 和DevOps的成果:脚本与自动化,快速原型编码(rapid prototyping),增量迭代,友好的测试(being friendly to experimentation),以及将大型项目分解成可控的模块。再加上CA的改变。(Plus ?a change.)
当你在shell里为2个命令加上管道的标示符,那么shell就会同时启动这2个命令程序,然后将第一个程序处理的输出结果作为第二个程序的输入。这种连接机构由操作系统提供管道系统调用服务。
请注意,这种线性处理不是由程序本身来完成的,而是靠shell??这就使得每个程序之间是“松耦合”,这使得程序不用担心它们的输入从哪里来,输出要往哪里去。
1964年,管道(Pipe)由Doug McIlroy发明,他首次在Bell实验室内部备忘录里将其描述为:“我们需要一些连接各种程序的方法就像花园里的软管??当它成为另一种必要的消息数据时,需要拧入其他的消息段。” Dennis Richie后来将他的观点写进了备忘录。
他们也很早就意识到进程间的通信机制(管道)与读写文件机制非常相似。我们现在称之为输入重定向(用一个文件内容作为一个程序的输入)和输出重定向(将一个程序的结果输出到一个文件)。
Unix程序之所以能够有这么高的组合灵活性,是因为这些程序都遵循相同的接口:大多数程序都有一个数据输入流(stdin)和两个输出流(stdout常规数据输出流和stderr错误与诊断信息输出流)。
程序通常除了读stdin流和写stdout流之外,它们还可以做其它的事,比如读取和写入文件,在网络上通信,或者绘制一个用户界面。然而,该stdin/stdout通信被认为是数据从一个Unix工具流向另一个的最主要的途径。
其实,最令人高兴的事莫过于任何人可以使用任意语言轻松地实现stdin/stdout接口。你可以开发自己的工具,只要其遵循这个接口,那么你的工具能和其他标准工具一样高效,并能作为操作系统的一部分。
一旦你把这个工具写好了,你就可以将它使用在我们之前讨论过的数据处理管道里,它将会工作地很好。如果你已经使用了Unix一段时间,那么这样做似乎很容易,但是我想强调这样做非常了不起:你自己的代码程序与操作系统附带的那些工具地位是一样的。
图形用户界面的程序和Web应用似乎不那么容易能够被拓展或者像这样串起来。你不能用管道将Gmail传送给一个独立的搜索引擎应用,然后将结果输出到wiki上。但是现在是个例外,跟往常不一样的是,现在也有程序能够像Unix工具一样能够协同工作。
换个话题。在Unix系统开发的同时,关系型数据模型就被提出来了,不久就演变成了SQL,被运用到很多主流的数据库中。许多数据库实际上仍在Unix系统上运行。这是否意味着它们也遵循Unix哲学?
在大多数据库系统中数据流与 Unix工具中非常不同。不同于使用stdin流和stdout流作为通信渠道,数据库系统中使用DB server以及多个client。客户端(Client)发送查询(queries)来读取或写入服务器上的数据,server端处理查询 (queries)并发送响应给客户端(Client)。这种关系从根本上是不对称的:客户和服务器都是不同的角色。
Unix系统里可组合性和拓展性是指什么?客户端(Clients)能做任何他们喜欢的事(因为他们是程序代码),但是DB Server大多是在做存储和检索数据的工作,运行你写的任意代码并不是它们的首要任务。
也就是说,许多数据库提供了一些方法,你能用自己的代码去扩展数据库服务器功能。例如,在许多关系型数据库中,让你自己写存储过程,基本的程序语言如PL / SQL(和一些让你在通用编程语言上能运行代码比如javascript)。然而,你可以在存储过程中所做的事情是有限的。
其他拓展方式,像某些数据库支持用户自定义数据类型(这是Postgres的早期设计目标),或者支持可插拔的数据引擎。基本上,这些都是插件的接口:
你可以在数据库服务器中运行你的代码,只要你的模块遵循一个特定用途的数据库服务器的插件接口。
这种扩展方式并不是与我们看到的Unix工具那样的可组合性一样。这种插件接口完全由数据库服务器控制,并从属于它。你写的扩展代码就像是数据库服务器家中一个访客,而不是一个平等的合作伙伴。
这种设计的结果是,你不能用管道将一个数据库与另一个连接起来,即使他们有相同的数据模型。你也不能将自己的代码插入到数据库的内部处理管道(除非该服务器已明确提供了一个扩展点,如触发器)。
我觉得数据库设计是很以自我为 中心的。数据库似乎认为它是你的宇宙的中心:这可能是你要存储和查询数据,数据真正来源,和所有查询最终抵达的唯一地方。你得到管道数据最近的方式是通过 批量加载和批量倾倒(bulk-dumping)(备份)操作,但这些操作不能真正使用到数据库的任何特性,如查询规划和索引。
如果数据库遵循Unix的设计 思想,那么它将是基于一小部分核心原语,你可以很容易地进行结合,拓展和随意更换。而实际上,数据库犹如极其复杂,庞大的野兽。Unix也承认操作系统不 会让你真的为所欲为,但是它鼓励你去拓展它,你或许只需一个程序就能实现数据库系统想要实现所有的功能。
在只有一个数据库的简单应用中,这种设计可能还不错。
然而,在许多复杂的应用中,他们用各种不同的方式处理他们的数据:对于OLTP需要快速随机存取,数据分析需要大序列扫描,全文搜索需要倒排索引,用于连接的数据图索引,推荐引擎需要机器学习系统,消息通知需要的推送机制,快速读取需要各种不同的缓存表示数据,等等。
一个通用数据库可以尝试将所有 这些功能集中在一个产品上(“一个适合所有”),但十有八九,这个数据库不会为了某个特定的功能而只执行一个工具程序。在实践中,你可以经常通过联合各种 不同的数据存储和检索系统得到最好的结果:例如,你可以把相同的数据并将其存储在关系数据库中,方便其随机访问,在Elasticsearch进行全文搜 索,在Hadoop中做柱状格式分析,并以非规范化格式在memcached中缓存。
当你需要整合不同的数据库,缺乏Unix风格的组合性对于整合来说是一个严重的限制。(我已经完成了从Postgres中用管道将数据输出到其他应用程序,但这还有很长的路要走,直到我们可以简单地用管道将任一数据库中的数据导出到其他数据库。)
我们说Unix工具可组合性是因为它们都实现相同的接口??stdin,stdout和stderr??它们都是文件描述符,即:可以像文件一样读写的字节流。这个接口很简单以致于任何人都可以很容易地实现它,但它也足够强大,你可以使用它做任何东西。
因为所有的Unix工具实现相 同的接口,我们把它称为一个统一的接口。这就是为什么你可以毫不犹豫地用管道将gunzip数据输出WC中去,即使开发这两个工具的作者可能从来没有交流 过。这就像乐高积木,它们都用相同的模式实现节位和槽位,让你堆乐高积木的时候能够随心所欲,不用管它们的形状,大小和颜色。
Unix文件描述符的统一接口 并不仅仅适用于输入和输出的过程,它是一个非常广泛的应用模式。如果你在文件系统上打开一个文件,你将得到一个文件描述符。管道和Unix套接字提供一个 文件标识符,这个标示符能够在同一机器上为其它程序提供一个通信通道。在Linux中,/dev下的虚拟文件是设备驱动程序的接口,所以你在这里面可以跟 USB端口甚至GPU打交道。/proc下的虚拟文件是内核的API,但是它是以文件形式存在,你可以使用相同的工具,以普通文件的方式访问它。
即使是通过TCP连接到另外一台机器上的程序也是一个文件描述符,虽然BSD套接字API(最常用来建立TCP连接)不像Unix。Plan 9显示,即使是网络可以被完全集成到相同的统一接口中去。
可以做这样一个类比,所有东西 在Unix里都是一个文件。这样的统一性从逻辑上来说将Unix下的工具就像一根线分成了很多段,使其更加能够灵活组合。 sed 根本就不需要关心与其交互的是一个管道还是其他的程序,或者一个套接字,或者设备驱动程序,又或者是一个真正在文件系统上的文件。因为这些都是一样的。
一个文件是一些字符流,或许在某个位置会有文件末尾(EOF)的标识,这就说明这个字符流到此为止了(字符流可以是任意长度,因此程序不能提前预知这个输入流有多长)
一些工具(如gzip)纯粹是操作字节流,而不关心数据的结构是什么样子。但是大多数工具需要对输入流进行转码,以便能做更有用的事情。为此,大多数Unix工具在一行的每个记录上,在制表符或空格或逗号分隔的区域上使用ASCII码。
如今,这种字节流文件显然是一种很好的统一接口的体现。然而,Unix的实现者对文件的处理却是截然不同的。例如,他们也许用函数回调接口处理方式,使用一个事务在进程与进程之间传递记录。或者他们用共享内存的方式(像之后的System V IPC o和 mmap一样)。又或者使用比特流而不是字节流的处理方式。
在某种意义上讲,字节流是能够达统一的最低标准? ?可能是最简单的接口。一切都可以用字节流来表示,但是对于传输媒介来说根本不知道它是什么(与另一个进程连接的管道,磁盘文件、TCP 连接、磁带等等)。这也是一种劣势,我们稍后再讨论这个问题。
我们看到Unix为软件开发带来了很多很好的设计原则,而数据库系统走的却是另一条大道。我很高兴能够看到这样一个未来:我们能从这两家学习到各自的核心思想,然后将它们结合起来。
那么怎么样把Unix哲学运用到21世纪的数据系统中,使其变得更好呢?在接下来的内容中,我将探索数据库系统的世界到底会变成什么样。
首先,让我们承认,Unix并不完美。尽管我认为简单,统一接口的字节流是非常成功的,这使得这个生态系统拥有灵活性,可组合性,以及拥有功能强大的工具,但Unix也有一定的局限性:
1. 它只能在单一机器上使用。随着应用程序需要处理更多数据和流量,并要求更高的正常运行时间,因此分布式系统将成为必然趋势。虽然TCP连接似乎能够被当成 文件处理,但我不认为这是合理的方案:因为这只在双方连接都已经打开的情况下工作,而且这里还有一点语义混乱的味道(somewhat messy edge case semantics)。
纵然TCP很好,但作为分布式管道的实现,它过于低级了。
2. Unix中管道被设计成只有一个发送者进程和一个接受者进程。你不能通过管道将输出发送到多个进程,或者从几个进程中收集输入。(你可以用tee分支一条管道,但一个管道本身就是一对一。)
3. ASCII文本(或者,UTF-8)能够很好使数据更加可控(explorable),但这很快就会变得很糟糕。每个进程需要给各自的输入进行转码:首先,将字节流分解成记录(通常通过换行符分隔,当然有人主张用 0x1e-ACSII记录分割器)。然后将记录再分解成各个域,就像在前文awk提到的$7。出现在数据中的分隔字符需要以某种方式进行转义。即使是一个相当简单的工具如xargs,约有大半的命令选项来确定输入需要怎样进行解析。基于文本接口使用起来相当好,但回想起来,我敢肯定,更丰富的数据模型,清晰的事务模式会更好些。
4. Unix处理进程通常不能长久的运行。例如,如果处于管道中间的处理进程崩溃了,那么没有办法从当前输入管道恢复,使得整个管道任务失败,必须从头开始运 行。如果这些命令运行只有几秒钟那是没有问题的,但如果一个应用程序预计需要连续运行多年,这就需要更好的容错能力。
我想我们可以找到一个解决方案,既克服这些缺点,又传承Unix哲学。
最令人兴奋的事是,这样的解决方案其实早就存在,那就是这两个开源项目?? Kafka和Samza,它们协同工作能够提供分布式流处理服务。
你也许在这个博客其他文章中已经了解到这两个项目,Kafka是一个可扩展分布式消息代理,而Samza是一个框架,这个框架让你的代码能够生产和消费数据流。
事实上,当你用Unix标准去剖析Kafka,它看起来很像一个管道??将一个进程的输出与另一个进程的输入相连。Samza看起来更像一个标准的库,这个库可以帮助你读stdin流和写stdout流(还有一些有用的功能,如部署机制,状态管理,度量工具(metrics)和监测)。
Kafka 和Samzaz中,流处理任务的风格,有点像Unix传统的精简且可组合的工具。
1. 在Unix中,操作系统内核提供了一个管道,一个进程从另一个进程中获取字节流的传输机制。
2. 在流处理中,Kafka提供了发布-订阅流(publish-subscribe streams),一个流处理任务能够从另一个流处理任务获取消息的传输机制。
Kafka解决了我们前面讨论过的有关Unix 管道的缺点:
1. 单机限制被解除:Kafka本身就是分布式的,并且任何使用它的流处理器也可以分布在多台机器上。
2. Unix管道连接一个进程的输出与一个进程的输出,而Kafka流可以有多个生产者和消费者。多输入对于在多台机器上分布的服务至关重要,而多输出使卡夫 卡更像一个广播频道。这非常有用,因为它允许相同的数据流独立地、因为不同的目的而被消耗(包括监控和审核的目的,这些往往是外部应用程序本身)。在 Kafka中,消费者往往来去都比较自由,而不会影响其他消费者。
3. Kafka还提供了良好的容错性:数据被复制到多个Kafka节点,所以如果一个节点失败,另一个节点可以自动接管。如果某个流处理器节点出错并重新启动,那它可以在其最后一个检查点恢复处理操作。
4. Kafka提供的是一个消息流而不是字节流,这个消息流存储了第一步输入的转码状态(将字节流分解成序列化记录)。每个消息流其实是一个字节数组,因此你 可以使用你最喜欢的序列化格式来定制你的消息:JSON, XML, Avro, Thrift或者Protocol Buffers,这些都是合理的选择。将一种编码标准化是非常有意义的,Confluent为Avro提供了一种非常好的架构管理支持。这使得应用程序能用有意义的字段名称的作为处理对象,不必担心输入解析或输出转义。它还提供良好的事务推进支持而不会破坏兼容性。
关于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,就并没有这种有序性。
所以我们知道Unix工具和流处理器看上去十分相似。都是读相同输入流,然后以某种方式修改或改转换它,并产生一个输出流,从某种程度上这都来自于输入。
更重要的是,处理工作不修改输 入(input)本身:它仍然是不可改变的。如果你在相同的输入文件上运行AWK,该文件还是处于未修改的状态(除非你明确选择覆写它)。同时,大多数 Unix工具是确定的,即如果你给他们同样的输入,他们总是产生相同的输出。这意味着你可以重新运行相同的命令,想多少次就多少次,然后逐步迭代成你想做 的工作程序。这是个很棒的实验,因为如果你混乱地进行处理,你还是可以随时返回到你的原始数据。
这种确定性和无副作用的效果处理看起来很像函数式编程。这并不意味着你必须使用象 Haskell 那样的函数式编程语言 (如果你想这么做的话也非常欢迎),但你仍然能从函数式代码中收获很多。
这种类Unix设计准则的 Kafka,使其能够构建一个大型的可组合的系统。在大型的公司中,不同的团队能够通过Kafka发布各自的数据。每个团队能够独立的开发和维护处理任务 ??消费各种流和生产新的流。因为一个流可以有很多独立的消费者,产生一个新的消费者不需要事先协调。
我们将这种思想成为流数据平台。在这种架构中,Kafka数据流扮演的是不同团队系统沟通的通道。每个组在整个系统中只是负责自己的那部分,并且将这一块事情做好。正如Unix工具能够被组合而完成数据处理任务一样,分布式流系统也以被组合成一个超大规模的处理组织。
Unix 方法是通过降低耦合性来控制大系统的复杂性: 多亏了流接口的统一性,每个部件能够独立的开发和部署。由于良好的容错性和管道的缓存性 (Kafka),当问题发生在系统的某个部分时,它仍然只是局部。并且策略管理允许对数据结构作出更改使其更加安全,以便每个团队可以加快脚步而不打乱其他团队的步伐。
为总结全文,让我们思考下发生在 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职位浏览”的这样一个例子。
·END·
我们都是架构师!
架构师订阅号,关注获取更多技术分享
工作邮箱:admin@137x.com
以上是关于[译]从LinkedIn,Apache Kafka到Unix哲学的主要内容,如果未能解决你的问题,请参考以下文章