带有太强个人色彩的系统难以大获成功。一旦最初设计基本完成且足够鲁棒时,由于经验迥然、观点各异的人的加入,真正的考验才刚刚开始。
—— Donald Knuth
-
服务(在线系统,online systems) 服务(service)类型的系统会等待客户端发来的请求或指令。当收到一个请求时,服务会试图尽快的处理它,然后将返回应答。响应时间通常是衡量一个服务性能的最主要指标,且可用性通常很重要(如果客户端不能够触达服务,则用户可能会收到一条报错消息)。之前章节我们主要在讨论此类系统。 -
批处理系统(离线系统,offline systems) 一个批处理系统通常会接受大量数据作为输入,然后在这批数据上跑任务(job),进而产生一些数据作为输出。任务通常会运行一段时间(从数分钟到数天不等),因此一般来说没有用户会死等任务结束。相反,批处理任务通常会周期性的执行(例如,每天一次)。吞吐量(throughput,处理单位数据量所耗费的时间)通常是衡量批处理任务最主要指标。我们本文及后续两篇文章会主要围绕该类型系统进行讨论。 -
流式系统(近实时系统,near-real-time systems) 流式处理介于在线处理和离线处理(批处理)之间(因此也被称为近实时,near-real-time,或者准在线处理,nearline processing)。和批处理系统一样,流式处理系统接受输入,产生一些输出(而不是对请求做出响应,因此更像批处理而非服务)。然而,一个流式任务通常会在事件产生不久后就对其进行处理,与之相对,一个批处理任务通常会攒够一定尺寸的输入数据才会进行处理。这种区别让流式处理系统比同样功能的批处理系统具有更低的延迟。由于流式处理基于批处理,因此我们在以后再讨论它。
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"
$remote_addr - $remote_user [$time_local] "$request"
$status $body_bytes_sent "$http_referer" "$http_user_agent"
因此,上面一行日志的意思是,在 2015 年的 2 月 27 号,UTC 时间 17:55:11 ,服务器从 IP 为 216.58.210.78 的客户端收到了一条请求,请求路径为/css/typography.css。该用户没有经过认证,因此用户位置显示了一个连字符(-)。响应状态码是 200(即,该请求成功了),响应大小是 3377 字节。web 浏览器是 Chrome 49,由于该资源在 http://martin.kleppmann.com/ 网站中被引用,因此浏览器加载了该 CSS 文件。
cat /var/log/nginx/access.log | #(1)
awk '{print $7}' | #(2)
sort | #(3)
uniq -c | #(4)
sort -r -n | #(5)
head -n 5 #(6)
-
读取给定日志文件 -
将每一行按空格分成多个字段,然后取出第七个,即我们关心的 URL 字段。在上面的例子中,即:/css/typography.css -
按字符序对所有 url 进行排序。如果某个 url 出现了 n 次,则排序后他们会连着出现 n 次。 -
uniq 命令会将输入中相邻的重复行过滤掉。-c 选项告诉命令输出一个计数:对于每个 URL,输出其重复的次数。 -
第二个 sort 命令会按每行起始数字进行排序(-n),即按请求次数多少进行排序。-r 的意思是按出现次数降序排序,不加该参数默认是升序的。 -
最后,head 命令会只输出前 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
除了链式组合 Unix 命令,你也可以写一个简单的小程序来达到同样的目的。如,使用 Ruby,会有类似如下代码:
counts = Hash.new(0) # (1)
File.open('/var/log/nginx/access.log') do |file|
file.each do |line|
url = line.split[6] # (2)
counts[url] += 1 # (3)
end
end
top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5] # (4)
top5.each{|count, url| puts "#{count} #{url}" } # (5)
-
counts 是一个哈希表,为每个出现过的 URL 保存一个计数器,计数器初始值为 0。 -
对于每行日志,提取第六个字段作为 URL( ruby 的数组下标从 0 开始)。 -
对当前行包含的 URL 的计数器增加 1 。 -
对哈希表中的 URL 按计数值降序排序,取前五个结果。 -
打印这五个结果。
Ruby 脚本在内存中保存了 URL 的哈希表,记录每个 URL 到其出现次数的映射。Unix 管道例子中并没有这样一个哈希表。作为替代,它将所有 URL 进行排序,从而让所有相同的 URL 聚集到一块,从而对 URL 出现次数进行统计。
那种方法更好一些呢?这取决于你有多少个不同的 URL。对于大部分中小尺度的网站,你大概率能够把所有 URL 都放到(比如 1 G) 内存中,并为每个 URL 配一个计数器。在该例子中,该任务的工作集(任务需要访问的内存的大小)仅取决于不同 URL 的数量:假设有上百万条日志,但都只针对同一个 URL ,则哈希表所需空间为该 URL 尺寸加上对应计数器尺寸(当然,哈希表本身也是占一些空间的)。如果工作集足够小,则基于内存的哈希表能够很好地工作——即使在笔记本电脑上。
但如果,任务的工作集大于可用内存,则排序方式更有优势,因为能够充分利用磁盘空间。其原理类似我们在 SSTables 和 LSM-Trees 🔗一节中提到的:可以在内存中分批次对部分进行排序,然后将有序的数据作为文件写入磁盘中,最后将多个有序文件合并为更大的有序文件。归并排序会对数据进行顺序访问,因此在磁盘上性能较好。(为顺序 IO 优化是之前 🔗反复讨论过的主题,这里也出现了)
我们能够通过简单的组合 Unix 工具来进行复杂的日志文件处理并非巧合:这正是 Unix 的核心设计思想之一,且该思想在今天也仍然非常重要。让我来深入的探究一下其背后哲学,以看看有什么可以借鉴的。
Doug McIlroy,Unix 管道(pipe)的发明人,在 1964 年是这样描述管道的:“我们需要一种像软管一样可以将不同程序连接到一块的方法——当数据准备好以其他方式处理时,只需要接上就行。IO 也应该以这种方式工作”。管道的类比到今天仍然存在,并且成了 Unix 哲学的一部分。Unix 哲学是一组在 Unix 用户和开发者中很流行的设计原则,在 1978 年被表述为:
-
每一个程序专注干一件小事。在想做一个新任务时,新造一个轮子,而非向已有的程序中增加新的“功能”。 -
每个程序的输出成为其他程序(即便下一个程序还没有确定)的输入。不要在输出中混入无关信息(比如在数据中混入日志信息),避免使用严格的列式数据(数据要面向行,以行为最小粒度?)或者二进制数据格式。不要使用交互式输入。 -
尽快的设计和构建软件,即便复杂如操作系统,也最好在几周内完成(译注:这里翻译稍微有些歧义,即到底是尽快迭代还是尽早让用户试用,当然他们最终思想差不多,即构造最小可用模型,试用-迭代)。对于丑陋部分,不要犹豫,立即推倒重构。 -
相比不成熟的帮助,更倾向于使用工具完成编程任务,即使可能会进行反复构建相似的工具,并且在用完之后大部分工具就再也不会用到。
-
Unix 命令的输入文件通常被当做是不可变的。这意味着,你可以使用不同命令行参数,针对同样的输入跑很多次,而不用担心会损坏输入文件。 -
你可以在多个命令组成的处理流水线的任意环节停下来,将该环节的输出打到 less 工具中,以查看输出格式是否满足预期。这种可以对运行环节随意切片查看运行状态的能力对调试非常友好。 -
你可以将一个流水线环节的输出写入到文件中,并将该文件作为流水线下一个环节的输入。这样即使你中断了流水线的执行,之后想重启时就不用重新跑流水线所有环节。
-
一致性和共识:分布式的共识:原子提交和两阶段提交 🔗 -
一致性和共识:分布式系统离不开的顺序保证:顺序和因果、序列号和全序广播 🔗 -
一致性和共识:什么是分布式系统中的一致性?🔗 -
分布式系统中的麻烦事(下):真相由多数派定义?🔗 -
分布式系统中的麻烦事(中):为什么时钟它不可靠?🔗 -
分布式系统中的麻烦事(上):系统故障,以及不可靠的网络🔗 -
事务:分布式系统中的事务之最强隔离级别 🔗 -
事务:分布式系统中的事务之那些事务隔离级别 🔗 -
事务:分布式系统中的事务之那些棘手的概念们 🔗 -
分区:分布式系统中的副本和分片、KV 分片、分片均衡,以及路由请求 🔗 -
冗余:分布式数据之多副本的主从模型选择 🔗 -
冗余:分布式数据之多副本的读写流程 🔗 -
编码与演进:如何设计性能良好的编码?你该知道这几种数据流模型 🔗 -
编码与演进:性能好的编码如何设计?以常见的编码工具 JSON、CSV 等为例 🔗 -
存储与查询:数据库底层到底是如何处理查询和存储 🔗 -
数据模型和查询语言:以 图数据库 为例,讲解如何分析数据模型和考量查询语言 🔗 -
数据模型和查询语言:以 NoSQL 和 NewSQL 为例,讲解如何分析数据模型和考量查询语言 🔗 -
开篇:一个优秀的数据系统该有的三个特性:可靠、可扩展、可维护 🔗
对图数据库 NebulaGraph 感兴趣?欢迎前往 GitHub ✨ 查看源码:https://github.com/vesoft-inc/nebula;
🤗 广告时间:NebulaGraph 技术社区年度征文活动正在进行中,距离活动结束 3 天 12 小时。点击图片了解活动详情^^


