push模型 | pull模型 | |
描述 | 服务端主动发送数据给客户端 | 客户端主动从服务端拉取数据,通常客户端会定时拉取 |
实时性 | 较好,收到数据后可立即发送给客户端 | 一般,取决于pull的间隔时间 |
服务端状态 | 需要保存push状态,哪些客户端已经发送成功,哪些发送失败 | 服务端无状态 |
客户端状态 | 无需额外保存状态 | 需保存当前拉取的信息的状态,以便在故障或者重启的时候恢复 |
状态保存 | 集中式,集中在服务端 | 分布式,分散在各个客户端 |
负载均衡 | 服务端统一处理和控制 | 客户端之间做分配,需要协调机制,如使用zookeeper |
其他 |
服务端需要做流量控制,无法最大化客户端的处理能力。 其次,在客户端故障情况下,无效的push对服务端有一定负载。 |
客户端的请求可能很多无效或者没有数据可供传输,浪费带宽和服务器处理能力 |
缺点方案 | 服务器端的状态存储是个难点,可以将这些状态转移到db或者key-value存储,来减轻server压力。 |
针对实时性的问题,可以将push加入进来,push小数据的通知信息,让客户端再来主动pull。 针对无效请求的问题,可以设置逐渐延长间隔时间的策略,以及合理设计协议尽量缩小请求数据包来节省带宽。 |
无论在协作进程还是在同一进程的协作子过程层面上,unix设计风格都运用“做单件事并做好的方法“,强调用定义良好的进程间通信或共享文件来连通小型进程,提倡将程序分解为更简单的子进程,并专注考虑这些子进程间的接口,这至少需要通过以下三种方法来实现:
1、降低进程生成的开销(思考下的process)
2、提供方法(shellout、io重定向、管道、消息传递和socket)简化进程间通信
3、提倡使用能由管道和socket传递的简单、透明的文本数据格式
unix ipc方法的分类:
1、将任务转给专门程序,如system(3),popen调用等,称为shellout
2、pipe、重定向和过滤器,如bc和dc
3、包装器,隐藏shell管线的复杂细节。
4、安全包装器和bernstein链
5、主/从进程
6、对等进程间通信:
(1)临时文件
(2)信号
(3)系统守护程序和常规信号
(4)socket
(5)共享内存,mmap
远程过程调用(rpc)的缺憾:
1、rpc接口很难做到可显,如果不编写和被监控程序同样复杂的专用工具,也难以监控程序的行为。rpc接口和库一样具有版本不兼容的问题,由于是分布式的,因此更难被追查。
2、类型标记越丰富的接口往往越复杂,因而越脆弱。随着时间的推移,由于在接口之间传递的类型总量逐渐变大,单个类型越来越复杂,这些接口往往产生类型本体蠕变问题。这是因为接口比字符串更容易失配;如果两端程序的本体不能正确匹配,要让它们通信肯定很难,纠错更是难上加难。
3、支持rpc的常见理由是它比文本流方法允许“更丰富”的接口,但是接口的功能之一就是充当阻隔点,防止模块的实现细节彼此泄漏,因此,支持rpc的主要理由同时恰恰证明了rpc增加而不是降低了程序的全局复杂度。
unix传统强烈赞成透明、可显的接口,这是unix文化不断坚持文本协议ipc的动力。
esr在这里还谈到xml-rpc和soap等协议,认为是rpc和unix对文本流支持的一种融合,遗憾的是soap本身也成为一种重量级、不那么透明的协议了,尽管它也是文本协议。
线程是有害的:
线程是那些进程生成昂贵、ipc功能薄弱的操作系统所特有的概念。
尽管线程通常具有独立的局部变量栈,它们却共享同一个全局内存,在这个共享地址空间管理竞争和临界区的任务相当困难,而且成为增加整体复杂度和滋生bug的温床。除了普通的竞争问题之外,还产生了一类新问题:时序依赖。
当工具的作用不是控制而是增加复杂度的时候,最好扔掉从零开始。
(注,这里谈的微型语言,就是现在比较热门的词汇dsl)
对软件错误模式进行的大量研究得出的一个最一致的结论是,程序员每百行程序出错率和所使用的编程语言在很大程度上是无关的。更高级的语言可以用更少的行数完成更多的任务,也意味着更少的bug。
防御设计不良微型语言的唯一方法是知道如何设计一个好的微型语言。
语言分类法:
对微型语言的功能测试:不读手册可以编写吗?
现代微型语言,要么就完全通用而不紧凑,要么就非常不通用而紧凑;不通用也不紧凑的语言则完全没有竞争力。
一些引申想法:我认为这个评判标准也可以用在任何编程语言上,以此来判断一些语言,c语言既通用又紧凑,java是通用而不紧凑,、python之类的脚本语言也是如此,正则表达式(如果也算语言的话)是不通用而紧凑,也是通用而紧凑,awk却是既不通用也不紧凑,xslt也可以归入不通用不紧凑的行列;javascript是个另类,按理说它也是不通用不紧凑,说它不通用是因为它的主要应用范围还是局限在web开发的ui上,实际上javascript也是门通用语言,但是很少会有人会用javascript去写批处理脚本,javascript显然是不紧凑的,太多的边边角角甚至奇奇怪怪的东西需要你去注意,然而就是这样一门不通用不紧凑的语言现在却非常有前途,只能说时势所然。
设计微型语言:
1、选择正确的复杂度,要的是数据文件格式,还是微型语言?
2、扩展和嵌入语言
3、编写自定义语法,yacc和lex
4、慎用宏,宏的主要问题是滥用带来的奇怪、不透明的代码,以及对错误诊断的扰乱。
5、语言还是应用协议。
软件设计有两种方式:一种是设计得极为简洁,没有看得到的缺陷;另一种是设计得极为复杂,有缺陷也看不出来。第一种方式的难度要大得多。
模块化代码的首要特质就是封装,api在模块间扮演双重角色,实现层面作为模块之间的滞塞点,阻止各自的内部细节被相邻模块知晓;在设计层面,正是api真正定义了整个体系。
养成在编码前为api编写一段非正式书面描述的习惯,是一个非常好的方法。
模块的最佳大小,逻辑行200-400行,物理行在400-800之间。
紧凑性就是一个设计能否装入人脑中的特性。测试软件紧凑性的一个简单方法是:一个有经验的用户通常需要用户手册吗?如果不需要,那么这个设计是紧凑的。
理解紧凑性可以从它的“反面”来理解,紧凑性不等于“薄弱”,如果一个设计构建在易于理解且利于组合的抽象概念上,则 这个系统能在具有非常强大、灵活的功能的同时保持紧凑。紧凑也不等同于“容易学习”:对于某些紧凑 设计而言,在掌握其精妙的内在基础概念模型之前,要理解这个设计相当困难;但一旦理解了这个概念模型,整个视角就会改变,紧凑的奥妙也就十分简单了。紧凑也不意味着“小巧”。即使一个设计良好的系统,对有经验的用户来说没什么特异之处、“一眼”就能看懂,但仍然可能包含很多部分。
评测一个api紧凑性的经验法则是:api的入口点通常在7个左右,或者按《代码大全2》的说法,7 2和7-2的范围内。
重构技术中的很多坏味道,特别是重复代码,是违反正交性的明显例子,“重构的原则性目标就是提高正交性”。
dry原则,或者称为spot原则(single point of truth)——真理的单点性。重复的不仅仅是代码,还包括数据结构,数据结构模型应该最小化,提倡寻找一种数据结构,使得模型中的状态跟真实世界系统的状态能够一一对应。
要提高设计的紧凑性,有一个精妙但强大的方法,就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造,将任务的核心形式化,建立明确的模型。
文本流是非常有用的通用格式,无需专门工具就可以很容易地读写和编辑文本流,这些格式是透明的。如果担心性能问题,就在协议层面之上或之下压缩文本协议流,最终产生的设计会比二进制协议更干净,性能可能更好。使用二进制协议的唯一正当理由是:如果要处理大批量的数据,因而确实关注能否在介质上获得最大位密度,或者是非常关心将数据转化为芯片核心结构所必须的时间或指令开销。
数据文件元格式:
1、dsv风格,delimiter-seperated values
使用分隔符来分隔值,例如/etc/passwd
适合场景:数据为列表,名称(首个字段)为关键字,而且记录通常很短(小于80个字符)
2、rfc 822格式
互联网电子邮件信息采用的文本格式,使用属性名 冒号 值的形式,记录属性每行存放一个,如http 1.1协议。
适合场景:任何带属性的或者与电子邮件类似的信息,非常适合具有不同字段集合而字段中数据层次又扁平的记录。
3、cookie-jar格式。简单使用跟随%%的新行符(或者有时只有一个%)作为记录分隔符,很适用于记录非结构化文本的情况。
适合场景:词以上结构没有自然顺序,而且结构不易区别的文本段,或适用于搜索关键字而不是文本上下文的文本段。
4、record-jar格式,cookie-jar和rfc-822的结合,形如
这样的格式。
适合场景:那些类似dsv文件,但又有可变字段数据而且可能伴随无结构文本的字段属性关系集合。
5、xml格式,适合复杂递归和嵌套数据结构的格式,并且经常可以在无需知道数据语义的情况下仅通过语法检查就能发现形式不良损坏或错误生成的数据。缺点在于无法跟传统unix工具协作。
6、windows ini格式,形如
这样的格式
适合场景:数据围绕指定的记录或部分能够自然分成“名称-属性对”两层组织结构。
unix文本文件格式的约定:
1、如果可能,以新行符结束的每一行只存一个记录
2、如果可能,每行不超过80个字符
3、使用”#“引入注释
4、支持反斜杠约定
5、在每行一条记录的格式中,使用冒号或连续的空白作为字段分隔符。
6、不要过分区分tab和whitespace
7、优先使用十六进制而不是八进制。
8、对于复杂的记录,使用“节(stanza)”格式,要么让记录格式和rfc 822电子邮件头类似,用冒号终止的字段名关键字作为引导字段。
9、在节格式中,支持连续行,多个逻辑行折叠成一个物理行
10、要么包含一个版本号,要么将格式设计成相互独立的的自描述字节块。
11、注意浮点数取整。
12、不要仅对文件的一部分进行压缩或者二进制编码。
应用协议元格式
1、经典的互联网应用元协议 rfc 3117《论应用协议的设计》,如smtp、pop3、imap等协议
2、作为通用应用协议的http,在http上构建专用协议,如互联网打印协议(ipp)
3、beep:块可扩展交换协议,既支持c/s模式,又支持p2p模式
4、xmp-rpc、soap和jabber,基于xml的协议。
美在计算科学中的地位,要比在其他任何技术中的地位都重要,因为软件是太复杂了。美是抵御复杂的终极武器。
如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。
如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而设计,这个软件系统就是可显的。
不要让调试工具仅仅成为一种事后追加或者用过就束之高阁的东西。它们是通往代码的窗口:不要只在墙上凿出粗糙的洞,要修整这些洞并装上窗。如果打算让代码一直可被维护,就始终必须让光照进去。例如fetchmail的-v选项将处理smtp、pop的处理过程打印到标准输出,使得fetchmail行为具有可显性。
在“这个设计能行吗?”之后要提出的头几个问题就是“别人能读懂这个设计吗?这个设计优雅吗?”我们希望,此时大家已经很清楚,这些问题不是废话,优雅不是一种奢侈。在人类对软件的反映中,这些品质对于减少软件bug和提高软件长期维护性是最基本的。
要追求代码的透明性,最有效的方法是很简单,就是不要在具体操作的代码上叠放太多的抽象层。
oo语言使得抽象变得容易——也许是太容易了。oo语言鼓励“具有厚重的胶合和复杂层次”的体系。当问题领域真的很复杂,确实需要大量抽象时,这可能是好事,但如果coder到头来用复杂的办法做简单的事情——仅仅是为他们能够这样做,结果便适得其反。
所有的oo语言都显示出某种使程序员陷入过度分层陷阱的倾向。对象框架和对象浏览器并不能代替良好的设计和文档,但却常常被混为一谈。过多的层次破坏了透明性:我们很难看清这些层次,无法在头脑中理清代码到底是怎样运行的。简洁、清晰和透明原则通通被破坏了,结果代码中充满了晦涩的bug,始终存在维护问题。
胶合层中的“智能数据”却经常不代表任何程序处理的自然实体——仅仅只是胶合物而已(典型现象就是抽象类和混入(mixin)类的不断扩散)
oo抽象的另一个副作用就是程序往往丧失了优化的机会。
oo在其取得成功的领域(gui、仿真和图形)之所以能成功,主要原因之一可能是因为在这些领域里很难弄错类型的本体问题。例如,在gui和图形系统中,类和可操作的可见对象之间有相当自然的映射关系。
unix风格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相组合。
太多的oo设计就像是意大利空心粉一样,把“is-a”和“have a”的关系弄得一团糟,或者以厚胶合层为特征,在这个胶合层中,许多对象的存在似乎只不过是为了在陡峭的抽象金字塔上占个位置罢了。这些设计都不透明,它们晦涩难懂并且难以调试。
为透明性和可显性而编码:
1、程序调用层次中(不考虑递归)最大的静态深度是多少?如果大于四,就要当心。
2、代码是否具有强大、明显的不变性质(约束)?不变性质帮助人们推演代码和发现有问题的情况。
3、每个api中各个函数调用是否正交?或者是否存在太多的magic flags或者模式位?
4、是否存在一些顺手可用的关键数据结构或全局唯一的记录器,捕获系统的高层次状态?这个状态是否容易被形象化和检验,还是分布在数目众多的各个全局变量或对象中而难以找到?
5、程序的数据结构或分类和它们所代表的外部实体之间,是否存在清晰的一对一映射。
6、是否容易找到给定函数的代码部分?不仅单个函数、模块,还有整个代码,需要花多少精力才能读懂?
7、代码增加了特殊情况还是避免了特殊情况?每一个特殊情况可能对任何其他特殊情况产生影响:所有隐含的冲突都是bug滋生的温床。然而更重要的是,特殊情况使得代码更难理解。
8、代码中有多少个magic number?通过审查是否很容易查出代码中的限制(比如关键缓冲区的大小)?
隐藏细节和无法访问细节有着重要区别。不要过度保护。
无论何时碰到涉及编辑某类复杂二进制对象的设计问题时,unix传统都提倡首先考虑,是否能够编写一个能够在可编辑的文本格式和二进制格式之间来回进行无损转换的工具?这类工具可称为文本化器(textualizer).
宁愿抛弃、重建代码也不愿修补那些蹩脚的代码。
“代码是活代码、睡代码还是死代码?”活代码周围存在一个非常活跃的开发社团。睡代码之所以“睡着”,经常是因为对作者而言,维护代码的痛苦超过了代码本身的效用。死代码则是睡得太久,重新实现一段等价代码更容易。
unix哲学是自下而上,而不是自上而下的,注重实效,立足于丰富的经验,你不会在正规方法学和标准中找到它。
unix管道的发明人doug mcilroy曾经说过:
1、让每个程序就做好一件事,如果有新任务就重新开始,不要往新程序中加入功能而搞的复杂。
2、假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序是未知的。输出中不要有无关的信息干扰,避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。
3、尽可能早将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下应该是几星期内,对抽劣的代码别犹豫,扔掉重写。
4、优先使用工具,而非拙劣的帮助来减轻编程任务的负担,工欲善其事,必先利其器。
rob pike在《notes on c programming》中提到:
原则1:你无法断行程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
原则2:估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
原则3:花哨的算法,在n很小的适合通常很慢,而n通常很小。花哨算法的常数复杂度很大,除非你确定n一直很大,否则不要用花哨算法(即使n很大,也要优先考虑原则2)。
原则4:花哨的算法比简单的算法更容易出bug ,更难实现。尽量使用简单的算法配合简单的数据结构。
原则5:数据压倒一切。如果已经选择了正确的数据结构并且把一切组织得井井有条,正确的算法也就不言自明,编程的核心是数据结构,而不是算法。
原则6:没有原则6.
ken thompson对原则4做了强调:
拿不准就穷举。
unix哲学的17条原则:
1、模块原则:简洁的接口拼合简单的部件。
2、清晰原则:清晰胜于机巧。
3、组合原则:设计时考虑拼接组合。
4、分离原则:策略同机制分离,接口同引擎分离。
5、简洁原则:设计要简洁,复杂度能低则低。
6、吝啬原则:除非却无他法,不要编写庞大的程序。
7、透明性原则:设计要可见,以便审查和调试。
8、健壮原则:健壮源于透明与简洁。
9、表示原则:把知识叠入数据,以求逻辑质朴而健壮。
10、通俗原则:接口设计避免标新立异。
11、缄默原则:如果一个程序没什么好说的,就沉默。
12、补救原则:出现异常时,马上退出,并给出足够错误信息。
13、经济原则:宁花机器一分,不花程序员一秒。
14、生成原则:避免手工hack,尽量编写程序去生成程序。
15、优化原则:雕琢前先要有原型,跑之前先学会走。
16、多样原则:绝不相信所谓“不二法门”的断言。
17、扩展原则:设计着眼未来,未来总是比预想来得快。
unix哲学之一言以蔽之:kiss
keep it simple,stupid!
应用unix哲学:
1、只要可行,一切都应该做成与来源和目标无关的过滤器。
2、数据流应尽可能的文本化(这样可以用标准工具来查看和过滤)。
3、数据库部署和应用协议应尽可能文本化(让人阅读和编辑)。
4、复杂的前端(用户界面)和后端应该泾渭分明。
5、如果可能,用c编写前,先用解释性语言搭建原型。
6、当且仅当只用一门编程语言会提高程序复杂度时,混用语言编程才比单一语言编程来得好。
7、宽收严发(对接收的东西要包容,对输出的东西要严格)
8、过滤时,不需要丢弃的消息绝不丢。
9、小就是美。在确保完成任务的基础上,程序功能尽可能的少。
最后强调的是态度:
要良好地运用unix哲学,你就应该不断地追求卓越,你必须相信,程序设计是一门技艺,值得你付出所有的智慧、创造力和激情。否则,你的视线就不会超越那些简单、老套的设计和实现;你就会在应该思考的时候急急忙忙跑去编程。你就会在该无情删繁就简的时候反而把问题复杂化——然后你还会反过来奇怪你的代码怎么会那么臃肿,那么难以调试。
要良好地运用unix哲学,你应该珍惜你的时间绝不浪费。一旦某人已经解决了某个问题,就直接拿来利用,不要让骄傲或偏见拽住你又去重做一遍。永远不要蛮干;要多用巧劲,省下力气在需要的时候用,好钢用到刀刃上。善用工具,尽可能将一切自动化。
软件设计和实现是一门充满快乐的艺术,一种高水平的游戏。如果这种态度对你来说听起来有些荒谬,或者令你隐约感到有些困窘,那么请停下来,想一想,问问自己是不是已经把什么给遗忘了。如果只是为了赚钱或者打发时间,你为什么要搞软件设计,而不是别的什么呢?你肯定曾经也认为软件设计值得你付出激情……
要良好地运用unix哲学,你需要具备(或者找回)这种态度。你需要用心。你需要去游戏。你需要乐于探索。
操作系统的风格元素:
1、什么是操作系统的统一性理念
2、多任务能力
3、协作进程(ipc)
4、内部边界
5、文件属性和记录结构
6、二进制文件格式
7、首选用户界面风格
8、目标受众
9、开发的门坎
本文档可以作为使用hadoop分布式文件系统用户的起点,无论是将hdfs应用在一个hadoop集群中还是作为一个单独的分布式文件系统使用。hdfs被设计成可以马上在许多环境中工作起来,那么一些hdfs的运行知识肯定能大大地帮助你对一个集群做配置改进和诊断。
hdfs是hadoop应用的主要分布式存储。一个hdfs集群由一个管理文件系统元数据的namenode,和存储实际
数据的一些datanode组成。hdfs的架构在这里有详细描述。这个用户指南主要提供给需要跟hdfs集群打交道的用户或者管理员。hdfs架构文章
中的图描绘了namenode、datanode和客户端们之间的基本交互。本质上,客户端与namenode通讯获取或者修改文件的元数据,与
datanode进行实际的io操作。
下面的列表应该是大多数用户关心的hdfs突出特点。斜体字的术语将在后面详细描述。
1)hadoop,包括hdfs,非常适合廉价机器上的分布式存储和分布式处理。它是容错的、可伸缩的,并且非常易于扩展。并且,以简单性和适用性著称的map-reduce是hadoop不可或缺的组成部分。
2)hdfs的默认配置适合于大多数安装的应用。通常情况下,只有在一个非常大规模的集群上才需要修改默认配置。
3)hdfs是用java编写的,支持大多数平台。
4)支持shell命令行风格的hdfs目录交互。
5)namenode和datanode都内建了web服务器,可以方便地查看集群的状态
6)hdfs经常性地实现新的特性和改进,下面是hdfs中的一些有用特性的子集:
文件许可和授权
rack awareness:当调度任务和分配存储的时候将节点的物理位置考虑进去。
safemode(安全模式):用于维护的一个管理状态
fsck: 诊断文件系统的一个工具,用来查找丢失的文件或者block
rebalancer:当数据在datanode间没有均匀分布的时候,用于重新平衡集群的工具
升级和回滚:当hadoop软件升级,在升级遇到不可预期的问题的时候,可以回滚到hdfs升级前的状态
二级namenode:帮助namenode维持包含了hdfs修改的日志的文件(edits日志文件,下文谈到)大小在限制范围内。
下面的文档描述了一个hadoop集群的安装和设置:
本文档的剩余部分假设你已经搭设并运行了一个至少拥有一个datanode的hdfs。基于本文档的目的,namenode和datanode可以运行在同一台机器上。
namenode和datanode分别跑了一个内置的web服务器,来展现集群当前状态的一些基本信息。在默认配置
下,namenode的凯发k8网页登录首页地址是http://namenode:50070(namenode就是namenode节点所在机器ip或者名称)。这个
页面列出了集群中的所有datanode以及集群的基本统计。web接口同样可以用于浏览文件系统(点击namenode凯发k8网页登录首页上的“browse
the file system"链接)。
hadoop包括了多种shell风格的命令,用于跟hdfs或者hadoop支持的其他文件系统交互。命令
bin/hadoop fs -help 可以列出hadoop shell支持的命令。更进一步,bin/hadoop fs -help
command
可以展现特定命令command的帮助细节。这些命令支持一般文件系统的操作,例如拷贝文件、修改文件权限等。同时也支持了部分hdfs特有的命令,例如
修改文件的replication因子。
'bin/hadoop dfsadmin' 命令支持一些hdfs管理功能的操作。'bin/hadoop dfsadmin -help'可以列出所有当前支持的命令。例如:
namenode将对文件系统的修改存储在一个原生文件系统文件中(名为edits的文件)。当namenode启动的时
候,它从映像文件(fsimage)读取hdfs的状态,然后将edits日志文件中的修改作用在此内存状态上,接着将得到的新的hdfs状态写回
fsimage,后续的正常操作开始于一个空的edits日志文件。由于namenode仅仅在启动的时候将fsimage和edits合并,因此在一个
大的集群上经过一定时间操作后,edits文件将会非常大。由此带来的一个副作用就是下次namenode的重新启动将花费很长时间。二级
namenode就是为了解决这个问题,它会周期性地合并fsimage和edits日志文件,并且将edits日志文件的大小保持在限制范围内。通常它
会跑在另一个机器上,因为它的内存要求跟主namenode一样。二级namenode可以通过'bin/start-dfs.sh'启动在conf
/masters配置文件里配置的节点上。
hdfs的数据可能不会总是在datanode之间分布得很一致。一个常见的原因是往现有的集群中加入了新的datanode。当分配block的时候,namenode依据几个参数来决定哪个datanode来接受这些block。一些需要考虑的因素如下:
1)一个block的副本存放在正在写该block的节点上
2)需要将一个block的副本扩展到其他机架上,防止因为整个机架故障导致的数据丢失。
3)副本之一通常放在同一个机架的另一个节点上,减少跨机架的网络io
4)将hdfs数据均匀一致地分布在集群中的datanode上。
基于这些相互竞争的因素,数据可能不会在datanode之间扩展得一致。hdfs给管理员提供了一个工具,用来分析block的分配情况和在datanode之间重新平衡数据。这个功能暂未实现,它的描述可以在这个 文档中看到,记录编号.
典型的大规模hadoop集群是部署在数个机架上的,那么显然同一个机架内的节点间的网络通讯比之不同机架间节点间的网 络通讯更可取。另外,namenode会尝试将block的副本分布在数个机架中以提高容错性。hadoop让集群管理员来决定某个节点从属于哪个机架, 通过配置变量dfs.network.script来实现。当这个脚本有配置的时候,每个节点都运行该脚本来决定它的rackid。默认安装假设所有的节 点从属于同一个机架。这个特性和配置进一步的阐述在这个文档,编号为 。
当namenode启动的时候,它从fsimage和edits日志两个文件中加载文件系统的状态。然后等待 datanode报告他们的block信息,以便防止namenode在确认block副本是否足够前过早地开始复制block。这段时间的 namenode就是处于所谓safemode状态。处于safemode的namenode也是hdfs集群的只读模型,此时不允许任何对文件系统或者 block的修改。正常情况下,namenode会在开始后自动退出safemode。如果有需要,hdfs可以通过'bin/hadoop dfsadmin -safemode'命令显式地进入safemode状态。namenode的web凯发k8网页登录首页显示当前的safemode是否打开。更详细的描述和配置可以参 考方法的javadoc。
译 注:详细介绍下safemode的配置参数,在safemode状态,namenode会等待所有的datanode报告他们自己的block信息,看看 所有的block的副本是否达到最低要求的数目,这个数目可以通过dfs.replication.min参数配置,默认是1,也就是至少要求有一个副 本。当报告合格的datanode的数目达到一定百分比,namenode才会离开safemode状态。这个百分比也是可配置的,通过 dfs.safemode.threshold.pct参数,默认是0.999f(也就是要求99.9%的datanode 合格)。namenode在合格的datanode数目达到要求的时候,并不是马上离开safemode状态,会有一个扩展时间,让剩余的 datanode来报告block信息,这个扩展时间默认是30秒,可以通过dfs.safemode.extension参数配置,单位是毫秒。
hdfs提供了fsck命令用来检测各种各样的不一致性。fsck被设计用来报告各种文件的问题,例如某个文件丢失的
block,block的副本数目是否低于设置等。不同于传统的一般原生文件系统的fsck命令,hdfs的fsck命令并不修正所检测到的错误。通常情
况下,namenode会自动修正大多数可以被修复的错误,hdfs的fsck不是hadoop shel的命令,可以通过'bin/hadoop
fsck'执行,可以运行在整个文件系统上或者一个文件子集上。
当升级某个集群的hadoop的时候,正如任何软件的升级一样,可能会引入新的bug或者不兼容的修改导致现有的应用出 现过去没有发现的问题。在所有重要的hdfs安装应用中,是不允许出现因丢失任何数据需要从零开始重启hdfs的情况。hdfs允许管理员恢复到 hadoop的早期版本,并且将集群的状态回滚到升级前。hdfs的升级细节请参考 。hdfs在任何时间只能有一个备份,因此在升级前,管理员需要通过'bin/hadoop dfsadmin -finalizeupgrade'命令移除现有的备份。下面简要描述了典型的升级过程:
1)在升级hadoop前,如果已经存在备份,需要先结束(finalize)它。可以通过'dfsadmin -upgradeprogress status'命令查询集群是否需要执行finalize
2)停止集群,分发部署新版本的hadoop
3)执行新版本的hadoop,通过添加 -upgrade 选项,例如/bin/start-dfs.sh -upgrade
4)大多数情况下,集群在升级后可以正常运行。一旦新的hdfs在运行若干天的操作后没有出现问题,那么就可以结束(finalize)这次升级。请注意,在升级前删除的文件并不释放在datanode上的实际磁盘空间,直到集群被结束(finalize)升级前。
5)如果有需要回到老版本的hadoop,那么可以:
a)停止集群,分发部署老版本的hadoop
b)通过rollback选项启动集群,例如bin/start-dfs.sh -rollback
文件许可的设计与其他平台(如linux) 的文件系统类似。在当前实现,安全被限制在简单的文件许可上。启动namenode的用户被作为hdfs的超级用户。hdfs的未来版本将支持网络验证, 例如kerberos方案(译注:mit开发的一个验证系统)的用户验证以及数据传输的加密。更详细的讨论参考。
hadoop正运行在成千上万个节点的集群上。 列 出了一些部署hadoop在大规模集群上的组织和机构。hdfs在每个集群上只有一个namenode节点,namenode节点上可用内存是当前伸缩性 的主要限制。在非常大规模的集群上,增加hdfs中存储的文件的平均大小,将可以帮助提高集群的大小而不用增加namenode的内存需求。默认的配置可 能不适合非常大规模的集群应用。页列出了对于大规模hadoop集群的配置改进建议。
本用户指南可作为使用hdfs很好的一个起点,在本文档持续改进的同时,有一些非常有价值的关于hadoop和hdfs的文档资料可供参考。下列资料可作为进一步探索的起点:
1、副本的存放,副本的存放是hdfs可靠性和性能的关键。hdfs采用一种称为rack-aware的策略来改进数据的可靠性、有效性和网络带宽的利
用。这个策略实现的短期目标是验证在生产环境下的表现,观察它的行为,构建测试和研究的基础,以便实现更先进的策略。庞大的hdfs实例一般运行在多个机
架的计算机形成的集群上,不同机架间的两台机器的通讯需要通过交换机,显然通常情况下,同一个机架内的两个节点间的带宽会比不同机架间的两台机器的带宽
大。
通过一个称为rack
awareness的过程,namenode决定了每个datanode所属的rack
id。一个简单但没有优化的策略就是将副本存放在单独的机架上。这样可以防止整个机架(非副本存放)失效的情况,并且允许读数据的时候可以从多个机架读
取。这个简单策略设置可以将副本分布在集群中,有利于组件失败情况下的负载均衡。但是,这个简单策略加大了写的代价,因为一个写操作需要传输block到
多个机架。
在大多数情况下,replication因子是3,hdfs的存放策略是将一个副本存放在本地机架上的节点,一个副本放在同一机架上的另一个节点,最后一
个副本放在不同机架上的一个节点。机架的错误远远比节点的错误少,这个策略不会影响到数据的可靠性和有效性。三分之一的副本在一个节点上,三分之二在一个
机架上,其他保存在剩下的机架中,这一策略改进了写的性能。
2、副本的选择,为了降低整体的带宽消耗和读延时,hdfs会尽量让reader读最近的副本。如果在reader的同一个机架上有一个副本,那么就读该副本。如果一个hdfs集群跨越多个数据中心,那么reader也将首先尝试读本地数据中心的副本。
3、safemode
namenode启动后会进入一个称为safemode的特殊状态,处在这个状态的namenode是不会进行数据块的复制的。namenode从所有的
datanode接收心跳包和blockreport。blockreport包括了某个datanode所有的数据块列表。每个block都有指定的最
小数目的副本。当namenode检测确认某个datanode的数据块副本的最小数目,那么该datanode就会被认为是安全的;如果一定百分比(这
个参数可配置)的数据块检测确认是安全的,那么namenode将退出safemode状态,接下来它会确定还有哪些数据块的副本没有达到指定数目,并将
这些block复制到其他datanode。
五、文件系统元数据的持久化
namenode存储hdfs的元数据。对于任何对文件元数据产生修改的操作,namenode都使用一个称为editlog的事务日志记录下来。例如,
在hdfs中创建一个文件,namenode就会在editlog中插入一条记录来表示;同样,修改文件的replication因子也将往
editlog插入一条记录。namenode在本地os的文件系统中存储这个editlog。整个文件系统的namespace,包括block到文件
的映射、文件的属性,都存储在称为fsimage的文件中,这个文件也是放在namenode所在系统的文件系统上。
namenode在内存中保存着整个文件系统namespace和文件blockmap的映像。这个关键的元数据设计得很紧凑,因而一个带有4g内存的
namenode足够支撑海量的文件和目录。当namenode启动时,它从硬盘中读取editlog和fsimage,将所有editlog中的事务作
用(apply)在内存中的fsimage
,并将这个新版本的fsimage从内存中flush到硬盘上,然后再truncate这个旧的editlog,因为这个旧的editlog的事务都已经
作用在fsimage上了。这个过程称为checkpoint。在当前实现中,checkpoint只发生在namenode启动时,在不久的将来我们将
实现支持周期性的checkpoint。
datanode并不知道关于文件的任何东西,除了将文件中的数据保存在本地的文件系统上。它把每个hdfs数据块存储在本地文件系统上隔离的文件中。
datanode并不在同一个目录创建所有的文件,相反,它用启发式地方法来确定每个目录的最佳文件数目,并且在适当的时候创建子目录。在同一个目录创建
所有的文件不是最优的选择,因为本地文件系统可能无法高效地在单一目录中支持大量的文件。当一个datanode启动时,它扫描本地文件系统,对这些本地
文件产生相应的一个所有hdfs数据块的列表,然后发送报告到namenode,这个报告就是blockreport。
六、通讯协议
所有的hdfs通讯协议都是构建在tcp/ip协议上。客户端通过一个可配置的端口连接到namenode,通过clientprotocol与
namenode交互。而datanode是使用datanodeprotocol与namenode交互。从clientprotocol和
datanodeprotocol抽象出一个远程调用(rpc),在设计上,namenode不会主动发起rpc,而是是响应来自客户端和
datanode 的rpc请求。
七、健壮性
hdfs的主要目标就是实现在失败情况下的数据存储可靠性。常见的三种失败:namenode
failures, datanode failures和网络分割(network
partitions)。
1、硬盘数据错误、心跳检测和重新复制
每个datanode节点都向namenode周期性地发送心跳包。网络切割可能导致一部分datanode跟namenode失去联系。
namenode通过心跳包的缺失检测到这一情况,并将这些datanode标记为dead,不会将新的io请求发给它们。寄存在dead
datanode上的任何数据将不再有效。datanode的死亡可能引起一些block的副本数目低于指定值,namenode不断地跟踪需要复制的
block,在任何需要的情况下启动复制。在下列情况可能需要重新复制:某个datanode节点失效,某个副本遭到损坏,datanode上的硬盘错
误,或者文件的replication因子增大。
2、集群均衡
hdfs支持数据的均衡计划,如果某个datanode节点上的空闲空间低于特定的临界点,那么就会启动一个计划自动地将数据从一个datanode搬移
到空闲的datanode。当对某个文件的请求突然增加,那么也可能启动一个计划创建该文件新的副本,并分布到集群中以满足应用的要求。这些均衡计划目前
还没有实现。
3、数据完整性
从某个datanode获取的数据块有可能是损坏的,这个损坏可能是由于datanode的存储设备错误、网络错误或者软件bug造成的。hdfs客户端
软件实现了hdfs文件内容的校验和。当某个客户端创建一个新的hdfs文件,会计算这个文件每个block的校验和,并作为一个单独的隐藏文件保存这些
校验和在同一个hdfs
namespace下。当客户端检索文件内容,它会确认从datanode获取的数据跟相应的校验和文件中的校验和是否匹配,如果不匹配,客户端可以选择
从其他datanode获取该block的副本。
4、元数据磁盘错误
fsimage和editlog是hdfs的核心数据结构。这些文件如果损坏了,整个hdfs实例都将失效。因而,namenode可以配置成支持维护多
个fsimage和editlog的拷贝。任何对fsimage或者editlog的修改,都将同步到它们的副本上。这个同步操作可能会降低
namenode每秒能支持处理的namespace事务。这个代价是可以接受的,因为hdfs是数据密集的,而非元数据密集。当namenode重启的
时候,它总是选取最近的一致的fsimage和editlog使用。
namenode在hdfs是单点存在,如果namenode所在的机器错误,手工的干预是必须的。目前,在另一台机器上重启因故障而停止服务的namenode这个功能还没实现。
5、快照
快照支持某个时间的数据拷贝,当hdfs数据损坏的时候,可以恢复到过去一个已知正确的时间点。hdfs目前还不支持快照功能。
八、数据组织
1、数据块
兼容hdfs的应用都是处理大数据集合的。这些应用都是写数据一次,读却是一次到多次,并且读的速度要满足流式读。hdfs支持文件的write-
once-read-many语义。一个典型的block大小是64mb,因而,文件总是按照64m切分成chunk,每个chunk存储于不同的
datanode
2、步骤
某个客户端创建文件的请求其实并没有立即发给namenode,事实上,hdfs客户端会将文件数据缓存到本地的一个临时文件。应用的写被透明地重定向到
这个临时文件。当这个临时文件累积的数据超过一个block的大小(默认64m),客户端才会联系namenode。namenode将文件名插入文件系
统的层次结构中,并且分配一个数据块给它,然后返回datanode的标识符和目标数据块给客户端。客户端将本地临时文件flush到指定的
datanode上。当文件关闭时,在临时文件中剩余的没有flush的数据也会传输到指定的datanode,然后客户端告诉namenode文件已经
关闭。此时namenode才将文件创建操作提交到持久存储。如果namenode在文件关闭前挂了,该文件将丢失。
上述方法是对通过对hdfs上运行的目标应用认真考虑的结果。如果不采用客户端缓存,由于网络速度和网络堵塞会对吞估量造成比较大的影响。
3、流水线复制
当某个客户端向hdfs文件写数据的时候,一开始是写入本地临时文件,假设该文件的replication因子设置为3,那么客户端会从namenode
获取一张datanode列表来存放副本。然后客户端开始向第一个datanode传输数据,第一个datanode一小部分一小部分(4kb)地接收数
据,将每个部分写入本地仓库,并且同时传输该部分到第二个datanode节点。第二个datanode也是这样,边收边传,一小部分一小部分地收,存储
在本地仓库,同时传给第三个datanode,第三个datanode就仅仅是接收并存储了。这就是流水线式的复制。
九、可访问性
hdfs给应用提供了多种访问方式,可以通过dfsshell通过命令行与hdfs数据进行交互,可以通过java
api调用,也可以通过c语言的封装api访问,并且提供了浏览器访问的方式。正在开发通过webdav协议访问的方式。具体使用参考文档。
十、空间的回收
1、文件的删除和恢复
用户或者应用删除某个文件,这个文件并没有立刻从hdfs中删除。相反,hdfs将这个文件重命名,并转移到/trash目录。当文件还在/trash目
录时,该文件可以被迅速地恢复。文件在/trash中保存的时间是可配置的,当超过这个时间,namenode就会将该文件从namespace中删除。
文件的删除,也将释放关联该文件的数据块。注意到,在文件被用户删除和hdfs空闲空间的增加之间会有一个等待时间延迟。
当被删除的文件还保留在/trash目录中的时候,如果用户想恢复这个文件,可以检索浏览/trash目录并检索该文件。/trash目录仅仅保存被删除
文件的最近一次拷贝。/trash目录与其他文件目录没有什么不同,除了一点:hdfs在该目录上应用了一个特殊的策略来自动删除文件,目前的默认策略是
删除保留超过6小时的文件,这个策略以后会定义成可配置的接口。
2、replication因子的减小
当某个文件的replication因子减小,namenode会选择要删除的过剩的副本。下次心跳检测就将该信息传递给datanode,
datanode就会移除相应的block并释放空间,同样,在调用setreplication方法和集群中的空闲空间增加之间会有一个时间延迟。
参考资料:
hdfs
java api: http://hadoop.apache.org/core/docs/current/api/
hdfs
source code: http://hadoop.apache.org/core/version_control.html