求助!批处理随机复制一个文件到指定文件夹出错...

这是一篇技术教程,我会用很简单的文字表达清楚自己的意思,只要你识字就能看懂,就能学到知识。

按照我的理解,批处理的本质,是一堆DOS命令按一定顺序排列而形成的集合。

OK,never claver and get to business(闲话少说言归正传)。批处理,也称为批处理脚本,英文译为 BATCH ,批处理文件后缀BAT就取的前三个字母。它的构成没有固定格式,只要遵守以下这条就 ok 了:每一行可视为一个命令,每个命令里可以含多条子命令,从第一行开始执行,直到最后一行结束,它运行的平台是 DOS。批处理有一个很鲜明的特点:使用方便、灵活,功能强大,自动化程度高。我不想让自己写的教程枯燥无味,因为牵缠到代码(批处理的内容算是代码吧?)的问题本来就是枯燥的,很少有人能面对满屏幕的代码而静下心来。所以我会用很多简单实用的例子让读这篇教程的朋友去体会批处理的那四射的魅力,感受它那古灵精怪的性格,不知不觉中爱上批处理(晕,怎么又是爱?到底批处理和爱有什么关系?答案:没有!)。再说句“闲话”:要学好批处理,DOS 基础一定要牢!当然脑子灵活也是很重要的一方面。

3、利用vbs延迟函数,精确度毫秒,误差1000毫秒内

上面的运行结果显示实际延时了5500毫秒,多出来的500毫秒时建立和删除临时文件所耗费的时间。误差在一秒之内。

4、仅用批处理命令实现任意时间延迟,精确度10毫秒,误差50毫秒内

仅用批处理命令就可以实现延迟操作。

实现原理:首先设定要延迟的毫秒数,然后用循环累加时间,直到累加时间大于等于延迟时间。

误差:windows系统时间只能精确到10毫秒,所以理论上有可能存在10毫秒误差。
      经测试,当延迟时间大于500毫秒时,上面的延迟程序一般不存在误差。当延迟时间小于500毫秒时,可能有几十毫秒误差,为什么?因为延迟程序本身也是有运行时间的,同时系统时间只能精确到10毫秒。

为了方便引用,可将上面的例子改为子程序调用形式:

下面给出一个模拟进度条的程序。如果将它运用在你自己的程序中,可以使你的程序更漂亮。

解说:“set /p a=■<nul”的意思是:只显示提示信息“■”且不换行,也不需手工输入任何信息,这样可以使每个“■”在同一行逐个输出。“ping /n 0 127.1>nul”是输出每个“■”的时间间隔,ping /n 0表示不执行这个命令,所以会比ping出去的时间更短,也就是即每隔多少时间最短输出一个“■”。当然你也可以改为1或2或3等使时间延长

十一、特殊字符的输入及应用

(如果要继续输入特殊字符请再次按ctrl+p,然后ctrl+某个字母)

以上是特殊字符的输入方法,选自[英雄]教程,很管用的。也就是用编辑程序edit输入特殊字符,然后保存为一文本文件,再在windows下打开此文件,复制其中的特殊符号即可。

一些简单的特殊符号可以在dos命令窗口直接输入,并用重定向保存为文本文件。
“^G”是用Ctrl+G或Alt+007输入(按住Alt后,只能按小键盘的数字),输入多个^G可以产生多声鸣响。

退格键表示删除左边的字符,此键不能在文档中正常输入,但可以通过edit编辑程序录入并复制出来。即“”。

利用退格键,配合空格覆盖,可以设计闪烁文字效果

:: 输出一些退格符将光标置于该行的最左端(退格符的数量可以自己调整)。

::输出空格将之前输出的文字覆盖掉。

::再次输出退格符将光标置于该行的最左端,这里的退格符数量一定不能比前面的

空格数少,否则光标不能退到最左端。

解说:主要是利用set命令的/p,表示后等号面的字符都是提示字符,然后在用退格键,让光标置于该行的最左端,但是原来的文字还在,然后使用空格作为输入提示符,所以就会覆盖前面的文字,然后再次输出退格符将光标置于该行的最左端,循环执行。如果你把ping命令的次数改为4,使延迟增长,就能看到光标的位置变化了。

十二、随机数(%random%)的应用技巧

2的15次方等于32768,上面的0~32767实际就是15位二进制数的范围。

那么,如何获取100以内的随机数呢?很简单,将%RANDOM%按100进行求余运算即可,见例子。

总结:利用系统变量%random%,求余数运算%%,字符串处理等,可以实现很多随机处理。

通过上面的学习,我们知道,%random%可以产生0到32767之间的随机数,但是,如何才能得到一定范围内的随机数呢? 
我们可以使用通用的算法公式如下: 
注:批处理中求模得用两个%%符号。 
  比如,我们想获得4到12之间的随机数,就可以这样来使用,代码如下:

思考题目:生成给定位数的随机密码
解答思路:将26个英文字母或10数字以及其它特殊字符组成一个字符串,随机抽取其中的若干字符。

说明:本例涉及到变量嵌套和命令嵌套的应用,见后。

十三、变量嵌套 与 命令嵌套

    和其它编程语言相比,dos功能显得相对简单,要实现比较复杂的功能,需要充分运用各种技巧,变量嵌套与命令嵌套就是此类技巧之一。

先复习一下前面的“字符串截取”的关键内容:

百分号如果需要当成单一字符,必须写成%%

以上是dos变量处理的通用格式,如果其中的m、n为变量,那么这种情况就是变量嵌套了。

什么是命令嵌套呢?简单的说,首先用一条dos命令生成一个字符串,而这个字符串是另一条dos命令,用call语句调用字符串将其执行,从而得到最终结果。

运行命令字符串生成最终结果为:
请按任意键继续. . .

Web和越来越多基于HTTP/REST的API使得请求/响应的交互模式变得如此普遍,以至于很容易将其视为理所当然。但是我们应当记住,这并不是构建系统的唯一途径,其他方法也有其优点。下面我们来区分三种不同类型的系统:

在线服务(或称在线系统)

服务等待客户请求或指令的到达。当收到请求或指令时,服务试图尽可能快地处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,而可用性同样非常重要。

批处理系统(或称离线系统)

批处理系统接收大量的输入数据,运行一个作业来处理数据,并产生输出数据。作业往往需要执行一段时间,所以用户通常不会等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理一定大小的输入数据集所需的时间)。

流处理系统(或称近实时系统)

流处理介于在线与离线/批处理之间(所以有时称为近实时或近线处理)。与批处理系统类似,流处理系统处理输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久即可对事件进行处理,而批处理作业则使用固定的一组输入数据进行操作。这种差异使得流处理系统比批处理系统具有更低的延迟。流处理是在批处理的基础上进行的。

批处理是构建可靠、可扩展与可维护应用的重要组成部分。例如,2004年发表的著名批处理算法MapReduce“使得Google具有如此大规模的可扩展性能力”。该算法随后在各种开源数据系统中被陆续实现,包括Hadoop、CouchDB和MongoDB。

使用UNIX工具进行批处理

我们从一个简单的例子开始。假设有一个Nginx服务器,每次响应请求时都会在日志文件中追加一行记录,使用的是默认的访问日志格式。

假设想找出网站中前五个最受欢迎的网页,可以在UNIX shell中执行下列操作:

② 将每一行按空格分割成不同的字段,每行只输出第七个字段,即请求的URL地址。
③ 按字母顺序排列URL地址列表。如果某个URL被请求过n次,那么排序后,结果中将包含连续n次的重复URL。
④ uniq命令通过检查两条相邻的行是否相同来过滤掉其输入中的重复行。-c选项为输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。
⑤ 第二种排序按每行起始处的数字(-n)排序,也就是URL的请求次数。然后以反向(-r)顺序输出结果,即结果中最大的数字首先返回。
⑥ 最后,head只输出输入中的前五行(-n 5),并丢弃其他数据。

该命令序列的输出如下所示:

该命令行的功能强大,将在几秒钟内处理千兆字节的日志文件,你可以轻松修改分析指令以满足自己的需求。例如,如果要省略CSS文件,将awk参数更改为'$7 !~ /\.css$/ {print $7}'即可。如果想得到请求次数最多的客户端IP地址而不是页面,那么就将awk参数更改为'{print $1}'

你也可以写一个简单的程序来做同样的事情,例如Ruby。Ruby脚本需要一个URL的内存哈希表,其中每个URL地址都会映射到被访问的次数。UNIX流水线例子中则没有这样的哈希表,而是依赖于对URL列表进行排序,在这个URL列表中多次出现的相同URL仅仅是简单重复。

哪种方法更好呢?这取决于有多少个不同的网址。对于大多数中小型网站,也许可以在内存中存储所有不同的URL,并且可以为每个URL提供一个计数器。在此示例中,作业的工作集仅取决于不同URL的数量:如果单个URL有一百万条日志条目,则哈希表中所需的空间表仍然只是一个URL加上计数器的大小。如果这个工作集足够小,那么内存哈希表工作正常。

如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与“SSTables和LSM-Tree”中讨论的原理相同:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序的段可以合并为一个更大的排序文件。归并排序在磁盘上有良好的顺序访问模式。

GNU Coreutils (Linux)中的sort实用程序通过自动唤出到磁盘的方式支持处理大于内存的数据集,且排序可以自动并行化以充分利用多核。这意味着之前简单的UNIX命令链可以很容易地扩展到大数据集,而不会耗尽内存。从磁盘读取输入文件倒可能会成为瓶颈。

我们可以非常容易地使用类似例子中命令链来分析日志文件,这是UNIX的关键设计思想之一。通过管道将程序连接起来的想法成为如今的UNIX哲学,一系列的在开发人员和UNIX用户中逐渐变得流行的设计原则。

对于这种哲学有更为完整的描述:

  1. 每个程序做好一件事。如果要做新的工作,则建立一个全新的程序,而不是通过增加新“特征”使旧程序变得更加复杂。
  2. 期待每个程序的输出成为另一个尚未确定的程序的输入。不要将输出与无关信息混淆在一起。避免使用严格的表格状或二进制输入格式。不要使用交互式输入。
  3. 尽早尝试设计和构建软件,甚至是操作系统。需要扔掉那些笨拙的部分时不要犹豫,并立即进行重建。
  4. 优先使用工具来减轻编程任务,即使你不得不额外花费时间去构建工具,并且预期在使用完成后会将其中一些工具扔掉。

这种哲学(自动化、快速原型设计、增量式迭代、测试友好、将大型项目分解为可管理的模块等)听起来非常像敏捷开发与DevOps运动。

像bash这样的UNIX shell可以让我们轻松地将这些小程序组合成强大的数据处理作业。尽管这些程序中是由不同人所编写的,但它们可以灵活地结合在一起。那么,UNIX是如何实现这种可组合性的呢?

如果希望某个程序的输出成为另一个程序的输入,也就意味着这些程序必须使用相同的数据格式,换句话说,需要兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序都必须使用相同的输入/输出接口。

在UNIX中,这个接口就是文件(更准确地说,是文件描述符),文件只是一个有序的字节序列。这是一个非常简单的接口,因此可以使用相同的接口表示许多不同的东西:文件系统上的实际文件,到另一个进程(UNIX socket,stdin,stdout)的通信通道,设备驱动程序(比如/dev/audio或/dev/lpo),表示TCP连接的套接字等。

按照惯例,许多(但不是全部)UNIX程序将这个字节序列视为ASCII文本。awk,sort,uniq和head都将它们的输入文件视为由\n(换行符,ASCII OxOA)字符分隔的记录列表,所有这些程序都使用标准相同的记录分隔符以支持交互操作。对每条记录(即一行输入)的解析则没有明确定义。

UNIX工具的另一个特点是使用标准输入和标准输出。如果运行一个程序而不指定任何参数,那么标准输入来自键盘,标准输出为屏幕。当然,也可以将文件作为输入或将输出重定向到文件。管道允许将一个进程的stdout附加到另一个进程的stdin(具有小的内存缓冲区,而不需要将全部中间数据流写入磁盘)。

程序仍然可以在需要时直接读取和写入文件。但如果程序不依赖特定的文件路径,只使用stdin和stdout,则UNIX工具可以达到最佳效果。这允许shell用户以任何他们想要的方式连接输入和输出,程序并不知道也不关心输入来自哪里以及输出到哪里。也可以说这是一种松耦合,后期绑定或控制反转。将输入/输出的布线连接与程序逻辑分开,可以更容易地将小工具组合成更大的系统。

用户甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。程序只需要从stdin读取输入并输出至stdout,从而参与数据处理流水线。

UNIX工具如此成功的部分原因在于,它可以非常轻松地观察事情的进展:

  • UNIX命令的输入文件通常被视为是不可变的。这意味着可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。
  • 可以在任何时候结束流水线,将输出管道输送到less,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。
  • 可以将流水线某个阶段的输出写入文件,并将该文件用作下一阶段的输入。这使得用户可以重新启动后面的阶段,而无需重新运行整个流水线。

然而,UNIX工具的最大局限在于它们只能在一台机器上运行,而这正是像Hadoop这样的工具的工作场景。

MapReduce有点像分布在数千台机器上的UNIX工具。和大多数UNIX工具一样,运行MapReduce作业通常不会修改输入,除了生成输出外没有任何副作用。

UNIX工具使用stdin和stdout作为输入和输出,而MapReduce作业在分布式文件系统上读写文件。在Hadoop的MapReduce实现中,该文件系统被称为HDFS。除HDFS外,还有其他各种分布式文件系统,诸如Amazon S3,Azure Blob存储和OpenStack Swift对象存储服务也有很多相似之处。

与网络连接存储(NAS)和存储区域网络(SAN)架构的共享磁盘方法相比,HDFS基于无共享原则。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和特殊网络基础设施。而无共享方法则不需要特殊硬件,只需要通过传统数据中心网络连接的计算机。

HDFS包含一个在每台机器上运行的守护进程,并会开放一个网络服务以允许其他节点访问存储在该机器上的文件(假设数据中心的每台节点都附带一些本地磁盘)。名为NameNode的中央服务器会跟踪哪个文件块存储在哪台机器上。因此,从概念上讲,HDFS创建了一个庞大的文件系统,来充分利用每个守护进程机器上的磁盘资源。

考虑到机器和磁盘的容错,文件块被复制到多台机器上。复制意味着位于多个机器上的相同数据的多个副本;或者像Reed-Solomon代码这样的纠删码方案,相比于副本技术,纠删码可以以更低的存储开销来恢复数据。在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,而不依赖特殊的硬件。

MapReduce是一个编程框架,可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。最简单的理解方法是参考本章前面的“简单日志分析”中的Web日志分析示例。MapReduce中的数据处理模式与此非常相似:

  1. 读取一组输入文件,并将其分解成记录。在Web日志示例中,每个记录都是日志中的一行。
  2. 调用mapper函数从每个输入记录中提取一个键值对。在前面的例子中,mapper函数是awk '{print $7}':它提取URL($7)作为关键字。
  3. 按关键字将所有的键值对排序。在日志示例中,这由第一个sort命令完成。
  4. 调用reducer函数遍历排序后的键值对。如果同一个键出现多次,排序会使它们在列表中相邻,所以很容易组合这些值,而不必在内存中保留过多状态。在前面的例子中,reducer是由uniq -c命令实现的,该命令对具有相同关键字的相邻记录进行计数。

这四个步骤可以由一个MapReduce作业执行。步骤2(map)和4(reduce)是用户编写自定义数据处理的代码。步骤1(将文件分解成记录)由输入格式解析器处理。步骤3中的排序步骤sort隐含在MapReduce中,无需用户编写,mapper的输出始终会在排序之后再传递给reducer

要创建MapReduce作业,需要实现两个回调函数,即mapper和reducer,其行为如下:

每个输入记录都会调用一次mapper程序,其任务是从输入记录中提取关键字和值。对于每个输入,它可以生成任意数量的键值对(包括空记录)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。

MapReduce框架使用由mapper生成的键值对,收集属于同一个关键字的所有值,并使用迭代器调用reducer以使用该值的集合。Reducer可以生成输出记录(例如相同URL出现的次数)。

在Web日志示例中,第5步由第二个sort命令按请求数对URL进行排序。在MapReduce中,如果需要第二个排序阶段,则可以编写另一个MapReduce作业并将第一个作业的输出用作第二个作业的输入。这样来看的话,mapper的作用是将数据放入一个适合排序的表单中,而reducer的作用则是处理排序好的数据。

下图展示了Hadoop MapReduce作业中的数据流。其并行化基于分区实现:作业的输入通常是HDFS中的一个目录,且输入目录中的每个文件或文件块都被视为一个单独的分区,可以由一个单独的map任务来处理。

一个输入文件的大小通常是几百兆字节。只要有足够的空闲内存和CPU资源,MapReduce调度器会尝试在输入文件副本的某台机器上运行mapper任务。这个原理被称为将计算靠近数据:它避免将输入文件通过网络进行复制,减少了网络负载,提高了访问局部性。

大多数情况下,MapReduce框架首先要复制代码到该节点,然后启动map任务并开始读取输入文件,每次将一条记录传递给回调函数mapper。mapper的输出由键值对组成。

Reduce任务中的计算也被分割成块。Map任务的数量由输入文件块的数量决定,而reduce任务的数量则是由作业的作者来配置的。为了确保具有相同关键字的所有键值对都在相同的reducer任务中处理,框架使用关键字的哈希值来确定哪个reduce任务接收特定的键值对。

键值对必须进行排序。如果数据集太大,可能无法在单台机器上使用常规排序算法。事实上,排序是分阶段进行的。首先,每个map任务都基于关键字哈希值,按照reducer对输出进行分区。每一个分区都被写入mapper程序所在本地磁盘上的已排序文件,使用的技术类似“SSTables和LSM-Trees”。

当mapper完成读取输入文件并写入经过排序的输出文件,MapReduce调度器就会通知reducer开始从mapper中获取输出文件。reducer与每个mapper相连接,并按照其分区从mapper中下载排序后的键值对文件。按照reducer分区,排序和将数据分区从mapper复制到reducer,这样一个过程被称为shuffle。

reduce任务从mapper中获取文件并将它们合并在一起,同时保持数据的排序。因此,如果不同的mapper使用相同的关键字生成记录,则这些记录会在合并后的reducer输入中位于相邻位置。

reducer通过关键字和迭代器进行调用,而迭代器逐步扫描所有具有相同关键字的记录。reducer可以使用任意逻辑来处理这些记录,并且生成任意数量的输出记录。这些输出记录被写入分布式文件系统中的文件。

单个MapReduce作业可以解决的问题范围有限。回顾一下日志分析的例子,一个MapReduce作业可以确定每个URL页面的浏览次数,但不是最受欢迎的那些URL,因为这需要第二轮排序。

因此,将MapReduce作业链接到工作流中是非常普遍的,这样,作业的输出将成为下一个作业的输入。Hadoop MapReduce框架对工作流并没有任何特殊的支持,所以链接方式是通过目录名隐式完成的:第一个作业必须配置为将其输出写入HDFS中的指定目录,而第二个作业必须配置为读取相同的目录名作为输入。从MapReduce框架的角度来看,它们仍然是两个独立的作业。

因此,链接方式的MapReduce作业并不像UNIX命令流水线(它直接将一个进程的输出作为输入传递至另一个进程,只需要很小的内存缓冲区),而更像是一系列命令,其中每个命令的输出被写入临时文件,下一个命令从临时文件中读取。

在数据库中,如果执行的查询只涉及少量记录,那么数据库通常会使用索引来加速查找。如果查询涉及到join操作,则可能需要对多个索引进行查找。然而,MapReduce没有索引的概念,至少不是通常意义上的索引。

在批处理的背景下讨论join时,我们主要是解决数据集内存在关联的所有事件。例如,假设一个作业是同时为所有用户处理数据,而不仅仅是为一个特定的用户查找数据(这可以通过索引更高效地完成)。

示例:分析用户活动事件

下图给出了批处理作业中典型的join示例。图中左侧是事件日志,描述登录用户在网站上的活动右侧是用户数据库。

分析任务可能需要将用户活动与用户描述信息相关联:例如,如果描述中包含用户年龄或出生日期,则系统可以确定哪些年龄组最受欢迎。但是,活动事件中仅包含用户标识,而不包含完整的用户描述信息。而在每个活动事件中嵌入这些描述信息又会太浪费。因此,活动事件需要与用户描述数据库进行join。

join的最简单实现是逐个遍历活动事件,并在(远程服务器上的)用户数据库中查询每个遇到的用户ID。该方案首先是可行的,但性能会非常差:吞吐量将受到数据库服务器的往返时间的限制,本地缓存的有效性将很大程度上取决于数据的分布,并且同时运行的大量并行查询很容易使数据库不堪重负。

为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)在一台机器上进行。如果通过网络对每条记录进行随机访问则请求太慢。而且,考虑到远程数据库中的数据可能会发生变化,查询远程数据库意味着会增加批处理作业的不确定性。

因此,更好的方法是获取用户数据库的副本(例如,使用ETL进程从数据库备份中提取数据),并将其放入与用户活动事件日志相同的分布式文件系统。然后,可以将用户数据库放在HDFS中的一组文件中,并将用户活动记录放在另一组文件中,使用MapReduce将所有相关记录集中到一起,从而有效地处理它们

mapper的目的是从每个输入记录中提取关键字和值,在上图的例子中,这个关键字就是用户ID:一组mapper会扫描活动事件(提取用户ID作为关键字,而活动事件作为值),而另一组mapper将会遍历用户数据库(提取用户ID作为关键字,用户出生日期作为值)。过程如下图所示。

当MapReduce框架通过关键字对mapper输出进行分区,然后对键值对进行排序时,结果是所有活动事件和用户ID相同的用户记录在reducer的输入中彼此相邻。MapReduce作业可以对记录进行排序,以便reducer会首先看到用户数据库中的记录,然后按时间戳顺序查看活动事件,这种技术称为次级排序。

然后reducer可以很容易地执行真正的join逻辑:为每个用户ID调用一次reducer函数。由于次级排序,第一个值应该是来自用户数据库的出生日期记录,Reducer将出生日期存储在局部变量中,然后使用相同的用户ID遍历活动事件,输出相应的已观看网址和观看者年龄。随后的MapReduce作业可以计算每个URL的查看者年龄分布,并按年龄组进行聚合。

由于reducer每次处理一个特定用户ID的所有记录,因此只需要将用户记录在内存中保存一次,而不需要通过网络发出任何请求。这个算法被称为排序-合并join,因为mapper的输出是按关键字排序的,然后reducer将来自join两侧的已排序记录列表合并在一起。

除了join之外,“将相关数据放在一起”模式的另一个常见用法是通过某个关键字(如SQL中的GROUP BY子句)对记录进行分组。所有具有相同关键字的记录形成一个组,然后在每个组内执行某种聚合操作。

使用MapReduce实现这种分组操作的最简单方法是设置mapper,使其生成的键值对使用所需的分组关键字。然后,分区和排序过程将相同reducer中所有具有相同关键字的记录集合在一起。因此,在MapReduce上实现的分组和join看起来非常相似。

分组的另一个常见用途是收集特定用户会话的所有活动事件,以便发现用户的活动序列,称为会话流程。例如,可以使用这种分析来确定选择网站新版本的用户是否比选择旧版本(A/B测试)的用户更有可能产生购买行为,或计算某个营销活动是否有效。

如果有多个Web服务器处理用户请求,则特定用户的活动事件很可能分散在各个不同服务器的日志文件中。可以通过使用会话cookie、用户ID或类似的标识符作为分组关键字来实现访问流程,将特定用户的所有活动事件放在一起,同时将不同用户的事件分配到不同的分区。

如果与单个关键字相关的数据量非常大,那么会破坏掉“将所有具有相同关键字的记录放在一起”的模式。例如,在社交网络中,大多数用户会有上百人关注者,但少数名人则可能有数百万的追随者。

在单个reducer中收集与名人相关的所有活动可能会导致严重的数据倾斜,某个reducer必须处理比其他reducer更多的记录。由于MapReduce作业只有在其所有mapper和reducer都完成时才能完成,因此所有后续作业必须等待最慢的reducer完成之后才能开始。

如果join输入中存在热键,则可以使用算法进行补偿。例如,Pig中的倾斜join方法首先运行一个抽样作业来确定哪些属于热键。在真正开始执行join时,mapper将任何与热键有关的记录发送到随机选择的若干个reducer中的一个(传统MapReduce基于关键字哈希来确定性地选择reducer)。对于join的其他输入,与热键相关的记录需要被复制到所有处理该关键字的reducer中

这种技术将处理热键的工作分散到多个reducer上,可以更好地实现并行处理,代价是不得不将join的其他输入复制到多个reducer。这种技术也非常类似“负载倾斜与热点”所讨论的技术,使用随机化来缓解分区数据库中的热点。

Hive的倾斜join优化采取了另一种方法。它需要在表格元数据中明确指定热键,并将与这些键相关的记录与其余文件分开存放。在该表上执行join时,它将对热键使用map端join。

使用热键对记录进行分组并汇总时,可以分两个阶段进行分组。第一个MapReduce阶段将记录随机发送到reducer,以便每个reducer对热键的记录子集执行分组,并为每个键输出更紧凑的聚合值。然后第二个MapReduce作业将来自所有第一阶段reducer的值合并为每个键的单一值。

上一节描述的join算法在reducer中执行实际的join逻辑,因此被称为reduce端join。mapper负责准备输入数据:从每个输入记录中提取关键字和值,将键值对分配给reducer分区,并按关键字排序。

Reduce端join方法的优点是不需要对输入数据做任何假设:无论其属性与结构如何,mapper都可以将数据处理好以准备join。缺点是所有这些排序,复制到reducer以及合并reducer输入可能会是非常昂贵的操作,这取决于可用的内存缓冲区,当数据通过MapReduce阶段时,数据可能需要写入磁盘若干次。

另一方面,如果可以对输入数据进行某些假设,则可以通过使用所谓的map端join来加快速度。这种方法使用了一个缩减版本的MapReduce作业,其中没有reducer,也没有排序。相反,每个mapper只需从分布式文件系统中读取输入文件块,然后将输出文件写入文件系统即可。

假设对于上面的例子,用户数据库可以完全放入内存。在这种情况下,当mapper程序执行时,它可以首先将用户数据库从分布式文件系统读取到内存的哈希表中。然后,mapper程序扫描用户活动事件,并简单地查找哈希表中每个事件的用户ID。

这种是实现map端join最简单的方法,特别适合大数据集与小数据集join,尤其是小数据集能够全部加载到每个mapper的内存中。

Map任务依然可以有多个:大数据集的每个文件块对应一个mapper,每个mapper还负责将小数据集全部加载到内存中。这种简单而有效的算法被称为广播哈希join:“广播”一词主要是指大数据集每个分区的m apper还读取整个小数据集(即小数据集实际被“广播”给大数据集)﹐“哈希”意味着使用哈希表。

另一种方法并不需要将小数据集加载至内存哈希表中,而是将其保存在本地磁盘上的只读索引中。由于频繁访问,索引大部分内容其实是驻留在操作系统的页面缓存中,因此这种方法可以提供与内存哈希表几乎一样快的随机访问性能,而实际上并不要求整个数据集读入内存。

如果以相同的方式对map端join的输入进行了分区,则哈希join方法可以独立作用于每个分区。在上例中,可以根据用户ID的最后一位十进制数字(因此每一边都有10个分区)来分配活动事件和用户数据库中的记录。例如,Mapper 3首先将所有以3结尾的ID的用户加载到哈希表中,然后扫描ID以3结尾的每个用户的所有活动事件。

如果分区操作正确完成,就可以确定所有要join的记录都位于相同编号的分区中,因此每个mapper只需从每个输入数据集中读取一个分区就足够了。这样的优点是每个mapper都可以将较少的数据加载到其哈希表中。

这种方法只适用于两个join的输入具有相同数量的分区,并且根据相同的关键字和相同的哈希函数将记录分配至分区。如果输入是由之前已经执行过这个分组的MapReduce作业生成的,那么这是一个合理的假设。

如果输入数据集不仅以相同的方式进行分区,而且还基于相同的关键字进行了排序,则可以应用map端join的另一种变体。这时,输入是否足够小以载入内存并不重要,因为mapper可以执行通常由reducer执行的合并操作:按关键字升序增量读取两个输入文件,并且匹配具有相同关键字的记录。

如果map端合并join是可能的,则意味着先前的MapReduce作业会首先将输入数据集引入到这个经过分区和排序的表单中。原则上,join可以在之前作业的reduce阶段进行。但是,在独立的map作业中执行合并join更为合适,例如,除了特定的join操作之外,分区和排序后的数据集还可用于其他目的。

当下游作业使用MapReduce join的输出时,map端或reduce端join的不同选择会影响到输出结构。reduce端join的输出按join关键字进行分区和排序,而map端join的输出按照与大数据集相同的方式进行分区和排序(因为对大数据集的每个文件块都会启动一个map任务,无论是使用分区join还是广播join)。

正如所讨论的,map端join也存在对输入数据集的大小、排序和分区方面的假设。在优化join策略时,了解分布式文件系统中数据集的物理布局非常重要:仅仅知道编码格式和数据存储目录的名称是不够的,还必须知道数据分区数量,以及分区和排序的关键字。

批处理即不是事务处理,也不是分析。批处理与分析更为接近,因为批处理过程通常会扫描大部分的输入数据集。但是,MapReduce作业的工作流与分析中SQL查询不同。批处理过程的输出通常不是报告,而是其他类型的数据结构。

Google最初使用MapReduce的目的是为其搜索引擎建立索引,这个索引被实现为5到10个MapReduce作业的工作流。尽管Google后来不再使用MapReduce,但是如果从构建搜索索引的角度来看MapReduce,会更加有助于理解(即使在今天,Hadoop MapReduce仍然是构建Lucene/Solr索引的好方法)。

像Lucene这样的全文搜索索引是这样工作的:它是一个文件(术语字典),可以在其中有效地查找特定关键字,并找到包含该关键字的所有文档ID列表(发布列表)。这是一个非常简单的搜索索引视图,实际上它还需要各种附加数据,以便按相关性对检索结果进行排序、拼写检查、解析同义词等等,但基本原则类似。

如果需要对一组固定文档进行全文检索,则批处理是构建索引的有效方法:mapper根据需要对文档集进行分区,每个reducer构建其分区索引,并将索引文件写入分布式文件系统。并行处理非常适用于构建这样的文档分区索引。

由于按关键字查询搜索索引是只读操作,因此这些索引文件一旦创建就是不可变的。如果索引的文档集合发生更改,则可以选择定期重新运行整个索引工作流,并在完成后用新的索引文件批量替换之前的索引文件。

如果只有少量文档发生了变化,这种方法在计算上可能比较昂贵,增量建立索引是一种替代方法。如果要添加、删除或更新索引中的文档,Lucene会生成新的段文件,并在后台异步合并和压缩段文件。

搜索索引只是批处理工作流输出的一个示例。批处理的另一个常见用途是构建机器学习系统,如分类器(例如垃圾邮件过滤器,异常检测,图像识别)和推荐系统(例如你可能认识的人,你可能感兴趣的产品或相关搜索)。

这些批量作业的输出通常是写入某种数据库:例如,在用户数据库中通过用户ID进行查询以获取建议的好友,或者在产品数据库中通过产品ID查询以获取相关产品列表。

查询数据库需要在处理用户请求的Web应用中进行,而这些请求通常与批处理作业架构是分离的。那么批处理过程的输出如何返回至数据库中以供Web应用查询?

最明显的选择可能是直接在mapper或reducer中使用你最喜欢的数据库客户端软件包(如果其支持批处理的话),而批处理作业则直接写入至数据库服务器,一次写入一条记录。这样的方法可行,但并不是一个好方案:

  • 为每个记录发送一个网络请求比批处理任务的正常吞吐量要慢几个数量级,性能很差。
  • MapReduce作业经常并行处理许多任务。如果所有的mapper或reducer都同时写入同一个输出数据库,并以批处理期望的速率写入,那么数据库很容易过载,其查询性能会受到影响。
  • 通常情况下,MapReduce为作业输出提供了一个干净的“全有或全无”的保证:如果作业成功,则结果就是只运行一次任务的输出,即使中间发生了某些任务失败但最终重试成功。如果整个作业失败,则不会产生输出。然而从作业内部写入外部系统会产生外部可见的副作用,而这种副作用无法彻底屏蔽。

更好的解决方案是,批处理作业创建一个全新的数据库,并将其作为文件写入分布式文件系统中的作业输出目录。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。各种键值存储都支持构建MapReduce作业中的数据库文件,包括Voldemort,Terrapin,ElephantDB和HBase批量加载。

前面讨论过的UNIX设计哲学倡导明确的数据流:一个程序读取输入并写回输出。在这个过程中,输入保持不变,任何以前的输出都被新输出完全替换,并且没有其他副作用。这意味着可以随心所欲地重新运行一个命令,进行调整或调试,而不会扰乱系统状态。

MapReduce作业的输出处理遵循相同的原理。将输入视为不可变,避免副作用(例如对外部数据库的写入),批处理作业不仅实现了良好的性能,而且更容易维护:

  • 如果在代码中引入了漏洞,输出错误或者损坏,那么可以简单地回滚到先前版本,然后重新运行该作业,将再次生成正确的输出;或者更简单的办法是将旧的输出保存在不同的目录中,然后切换回原来的目录。
  • 与发生错误即意味着不可挽回的损害相比,易于回滚的特性更有利于快速开发新功能。
  • 如果map或reduce任务失败,MapReduce框架会自动重新安排作业并在同一个输入上再次运行。如果失败是由于代码漏洞造成的,那么它会一直崩溃,最终导致作业在数次尝试之后失败。但是如果故障是由于暂时问题引起的,则可以实现容错。
  • 相同的文件可用作各种不同作业的输入,其中包括监控作业,它可以搜集相关运行指标,并评估其他作业的输出是否满足预期特性(例如,将其与前一次运行的输出进行比较并测量差异)。
  • 与UNIX工具类似,MapReduce作业将逻辑与连线(配置输入和输出目录)分开,从而可以更好地隔离问题,重用代码。

尽管MapReduce在20世纪末变得非常流行并被大量炒作,但它只是分布式系统的许多可能的编程模型之一。取决于具体的数据量、数据结构以及处理类型,其他工具可能更适合特定的计算。

MapReduce是一个有用的学习工具,因为它是分布式文件系统的一个相当清晰和简单的抽象。然而,MapReduce执行模型本身也存在一些问题,这些问题并没有通过增加另一个抽象层次而得到解决,而且在某些类型的处理中表现出糟糕的性能。

下面我们会看到一些批处理的替代方案。

如前所述,每个MapReduce作业都独立于其他任何作业。作业与其他任务的主要联系点是分布式文件系统上的输入和输出目录。如果希望一个作业的输出成为第二个作业的输入,则需要将第二个作业的输入目录配置为与第一个作业的输出目录相同,并且外部工作流调度程序必须仅在第一个作业已经完成后才开始第二个作业。

如果第一个作业的输出是要在组织内部广泛分发的数据集,则此设置是合理的。在这种情况下,需要能够通过名称来引用它,并将其用作多个不同作业的输入。将数据发布到分布式文件系统中众所周知的位置可以实现松耦合,这样作业就不需要知道谁在生成输出或者消耗输出。

但是,在很多情况下,我们知道一个作业的输出只能用作另一个作业的输入,这个作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是中间状态:一种将数据从一个作业传递到下一个作业的方式。

相比之下,本章开头的日志分析示例使用UNIX管道将一个命令的输出与另一个命令的输入连接起来。管道并不完全实现中间状态,而是只使用一个小的内存缓冲区,逐渐将输出流式传输到输入。

与UNIX管道相比,MapReduce完全实体化中间状态的方法有一些不利之处:

  • MapReduce作业只有在前面作业中的所有任务都完成时才能启动,而通过UNIX管道连接的进程同时启动,输出一旦生成就会被使用。
  • Mapper在很多情况下是冗余的:它们只是读取刚刚由reducer写入的同一个文件,并为下一个分区和排序阶段做准备。在许多情况下,mapper代码可能是之前reducer的一部分:如果reducer的输出被分区和排序的方式与mapper输出相同,那么不同阶段的reducer可以直接链接在一起,而不需要与mapper阶段交错。
  • 将中间状态存储在分布式文件系统中意味着这些文件被复制到多个节点,对于这样的临时数据来说通常是大材小用了。

我要回帖

更多关于 批处理复制文件夹到指定目录 的文章

 

随机推荐