http://blog.csdn.net/cuirong1986/article/details/8465630
Chapter 3. Filtering Patterns
本章的模式有一个共同点:不会改变原来的记录。这种模式是找到一个数据的子集,或者更小,例如取前十条,或者很大,例如结果去重。这种过滤器模式跟前面章节的不同是,从更小的粒度认识数据,例如特殊用户生成的记录,或文本中用得最多的前10个动词。简单的说,过滤器允许你更清楚的看清数据,像在显微镜下一样。也可以认为是搜索的一种形式。如果你对找出所有有着特殊信息的记录感兴趣,你就可以过滤出不匹配搜索条件的记录。
抽样,一种通用的过滤程序,是指取出数据的一个样本,比如,取某字段值最高的几个,或随机取几个记录。取样能用来得到更小的,仍具有代表性的子数据集,在其上面进行分析,就不必处理庞大的数据集。很多机器学习算法在大数据集上运行低效,所以需要建模使其可运行在更小的子数据集。
子样本也可以用于开发目的。简单的抽取前一千条记录不是好的抽样方法,因为取的数据相似并不能代表整个数据集。一个均匀的抽样能够提供更好的数据的视图,并允许你的程序或分析能在现实数据上完成,甚至取样非常小。
本章列出了四种这样的模式:filtering, Bloom filtering, top ten, and distinct。找出数据一部分的方法有很多,每种模式都有细微的差别,甚至都在做相同的事情。
我们将会看到MapReduce中几个简明的使用。Filtering, Bloom filtering,和 simple random可以只使用map端完成,不用reducer。
Filtering
Pattern Description
最基本的模式,filtering,为其它模式充当了一种抽象形式。Filtering基于某种条件评估每一条记录,决定它的去留。
Intent
过滤出我们不感兴趣的数据并保存起来。
考虑用一个评估方法f,处理记录,如果返回true,保留,如果返回false,丢掉。
Motivation
你的数据集很大,你想更专注于数据的一部分并可能在上面做随后的分析。这个自己可能对数据集有代表意义或者就像在大海中捞针。不管用怎样的方式,你需要使用MapReduce并行处理数据,并找到需要的数据,过程可能有点费劲。
例如,你可能只对含有Hadoop的记录感兴趣,指的是那些文本中有hadoop词,或有hadoop标签的记录。Filtering能找到跟hadoop相关的记录并保存,丢掉其它记录。
像hadoop一样的大数据处理系统,通常是关于把所有数据拿到一个本地机器。Filtering就是这种把数据子集拉出来并发送到分析程序的方法。Filtering也用于放大匹配条件的你好奇的数据。对子数据集的探索可能会导致代价更昂贵,并且基于更小数据集上行为的分析更复杂。
Applicability
Filtering有着广泛的应用。仅需要做的是用指定的规则解析出特定的“records”并决定是否保存。
Structure
过滤器模式可能是这本书里最简单的。图3-1展示了结构图。
map(key, record):
if we want to keep record then
emit key,value
Figure 3-1. The structure of the filter pattern
Filtering是MapReduce中唯一不需要reduce的。因为它不处理聚合运算。每一条记录单独处理评估去留。
Mapper对每条输入记录应用评估方法。输出键值对跟输入一样,因为记录不会改变。如果评估方法返回true就输出。
Consequences
Job的输出是通过了选择条件的子记录集。如果格式不变,在大数据集上跑的job也能在过滤后的数据集上跑。
Known uses
Closer view of data
准备个特殊的记录有共性或有趣的子数据集,做进一步检查。例如,马里兰的电话局可能只关心国际通话中马里兰的去电。
Tracking a thread of events
抽取一系列连续事件作为大数据集的案例研究。例如,你可以通过分析apache web服务器的日志了解特殊用户怎样与你的网站交互。一个特殊用户的行为可能遍布于其它行为中,所以很难找出发生了什么。靠根据用户的ip过滤,就能够得到特殊用户行为更好的视图。
Distributed grep
Grep,一个使用正则表达式找出感兴趣的文本行的强大的工具,很容易并行应用正则表达式匹配每一行并输出。
Data cleansing
数据有时是脏的,或者很难理解,未完成,和错误的格式。数据可能丢失了字段,date类型的字段不能格式化成date,或二进制数据的随即字节可能存在。Filtering能检验每条记录是否结构良好,并去除任何垃圾数据。
Simple random samping
如果你想得到数据集的一个简单随机采样,可以使用filtering,用一个评估方法随即返回true或false。在简单随即采样中,数据集中的每条记录都有相同的概率被选中。可以根据源数据的数量算出百分比,得到要返回的记录的数量。例如,数据集有一万亿,你想得到一百万数据,那么评估方法应该每一百万次返回一次true,因为一百万个一百万是一万亿。
Removing low scoring data
如果你能根据排序给数据评分,你可以根据不满足某一临界条件来过滤数据。如果之前已经知道有些数据对分析也没意义,可以给这些记录评比较低的分。这跟随后讲到的top ten模式有相同的目的,除了数据量。
Resemblances
Sql:过滤器模式跟select语句中使用where条件是等同的。记录保持不变,一些被简单的过滤,例如:
SELECT * FROM table WHERE value < 3;
Pig:filter是关键词
b = FILTER a BY value < 3;
Performance analysis
这种模式基本上同MapReduce等效率,因为job只有map。下面是map-only job高效的原因:
·由于没有reducer,就少了数据在map和reduce之间传输数据的阶段。所有的map任务处理本地数据,然后放回本地磁盘。
·由于没有reducer,排序阶段和reduce阶段时没有的。通常不会占用太多时间,但积少成多。
需要注意的一件事情是:输出文件的数量和大小。因为job只有mapper,得到的输出文件都是以part-m-为前缀的。你会发现如果过滤掉很多数据,输出文件的数据量会很少,这会在一段时间后NameNode的可扩展性上带来问题。
如果你顾虑这些小文件,但容忍job运行骚微长一点,可以使用identity reducer收集结果集。这样,mapper会把全部输出数据给reducer,但reducer不会做任何处理,仅仅每个reducer输出一个文件。合适的reducer数量取决于将被写到文件系统的数据量的大小和你想处理多少小文件。
Filtering Examples
Distributed grep
Grep 作为流行的文本过滤工具可以追溯到unix和类unix系统中的使用。对一个文件进行行扫描,匹配指定的样式就输出。我们要在大文本数据上并行做正则表达式搜索。在这个例子中,我们将展示在MapReduce中怎样应用正则表达式。
Mapper code。Mapper很简单,因为使用java内建api处理正则表达式。如果文本行匹配样式,就输出这一行。否则忽略这一行。我们使用setup方法获取job配置的正则。
public static class GrepMapper extends Mapper<Object, Text, NullWritable, Text> { private String mapRegex = null; public void setup(Context context) throws IOException, InterruptedException { mapRegex = context.getConfiguration().get("mapregex"); } public void map(Object key, Text value, Context context) throws IOException, InterruptedException { if (value.toString().matches(mapRegex)) { context.write(NullWritable.get(), value); } } }
只有map,没有combiner,没有reducer。所有输出记录被写到本地文件系统。
Simple Random Sampling
在srs中,我们想抽取大数据的每条记录都同等概率被选择的子数据集,这样有效减小数据集,并在更易于管理的部分数据上做有代表性的分析工作。
实现srs作为过滤操作不是filtering模式的一种程序,但结构是相同的。取代以记录内容为过滤条件的方法,这里生成一个随机数,用来对应一个值,保留对应的记录。
Mapper code。在mapper代码里,从setup方法里获取过滤器的百分率配置值,在map方法里会用到。
Map中,检查随机数的生成。随机数在0到1之间,所以根据与临界值的比较,可以决定是否保留记录。
public static class SRSMapper extends Mapper<Object, Text, NullWritable, Text> { private Random rands = new Random(); private Double percentage; protected void setup(Context context) throws IOException, InterruptedException { // Retrieve the percentage that is passed in via the configuration // like this: conf.set("filter_percentage", .5); // for .5% String strPercentage = context.getConfiguration() .get("filter_percentage"); percentage = Double.parseDouble(strPercentage) / 100.0; } public void map(Object key, Text value, Context context) throws IOException, InterruptedException { if (rands.nextDouble() < percentage) { context.write(NullWritable.get(), value); } } }
在这个只有map的job里,没有combiner或reducer。所有输出记录被写到本地文件系统。当使用一个小的百分比,你会发现文件数据量小且文件数量多。如果是这样,就把reducer数量设为1,但不指定reducer类。这样会告诉MapReduce框架执行默认的identity reducer把map的输出收集为一个文件。随后可以用hadoop fs –cat查看文件。
Bloom Filtering
Pattern Description
Bloom filtering跟前面的模式做同样的事情,但对每条记录的评估方法不一样。
Intent
这种过滤器是指我们保存预先定义的值得集合。如果输出由一点错误是没关系的,因为我们还打算进一步检查。这些预先定义的值叫热点值。
对每条记录,抽取一种特点。如果这种特点是预定义的值集合里面的成员,保留,否则丢掉,或反之。
Motivation
Bloom filtering在查看每一条记录和决定是否保留问题上跟通用的filtering相似。然而,有两个主要不同点区别于通用filtering。首先,我们想使用热点值,基于某种集合的隶属关系过滤数据。例如:如果用户字段是预先定义的用户列表里的,我们保留或去除这条记录。其次,集合的隶属关系用bloom filter来评估,在附录A有描述。在某种意义上,bloom filtering是一种不关心join右边数据值得join操作。(左连接)
这种模式跟第五章的replicated join模式有点相像。拿一个列表跟另一个比较,做一些join逻辑的排序,仅使用map任务。取代replicated join中用分布式缓存复制热点值列表到各处,我们发送一个bloom filter对象到分布式缓存。这样就允许使用的bloom filter对象取代列表本身,这允许执行更大的数据集的操作,因为bloom filter更简洁。而且不存在列表大小受内存限制的情况,只受bloom filter定义的feature limitations限制。
使用bloom filter,在这种情况下计算集合隶属关系有一种后果:有时会得到一种错的判断。就是说,一个值被判断为集合的元素而实际上不是。如果bloom filter判断出一个值不是集合的成员,我们必须保证它的正确性。关于为什么这种情况发生的更多信息,参考附录 A。然而,在一些情况下,这不是大问题。这章最后的一个例子代码中,我们将会收集相当大量的有趣的单词,如果一条记录中包含了有趣单词中的一个,保留该条记录。我们做这个的目的是想靠去掉不感兴趣的内容而使我们的数据更有意义。如果使用bloom filter代表单词列表,有时一个单词可能成为列表的成员,虽然列表不应该有它。这种情况下,如果我们意外保存了一些记录,我们任然要达到我们过滤掉大多数垃圾数据的目的。
Applicability
下面是使用bloom filtering的条件:
·数据能被分割为记录,就像filtering里的。
·能从每条记录抽取的特性都在热点值里。
·要有一个预先设定的热点值得条目的集合。
·能容忍一些错误。(例如不应该通过检查的通过了)
Structure
Figure 3-2. The structure of the Bloom filtering pattern
图3-2展示了bloom filtering的结构,和它是怎样分成两个主要组件的。首先,要训练出值得列表。结果被存在hdfs上。下面是filtering的MapReduce job,跟本章前一个模式相同,除了用到分布式缓存。因为记录被一条条分析并且没有聚合要做,所以没有reducer。
第一阶段训练值得列表。即从存储的地方load数据并把每个条目加到Bloom filter。训练好的bloom filter对象存储到已知的hdfs目录。
第二步,做具体的过滤。Map任务启动后,从分布式缓存加载bloom filter。然后,在map方法里,迭代记录检查时候满足隶属热点值列表。每条记录或者通过或者没通过隶属关系的检查。
当数据改变的时候,bloom filter需要重新训练。因此使用懒加载模式设置bloom filter是合适的。
Consequences
Job的输出是那些通过了bloom filter资格测试的子数据集。你应该预料到输出数据中的一些记录可能并不在热点值中,因为bloom filter有一定几率出错。
Known uses
Removing most of the nonwatched values
最简单的例子是去除不常用的值。例如,你只对有含有10000个单词的列表里的单词感兴趣,用hadoop处理。你拿到这个数据列表,训练出bloom filter,然后检查文本数据,看看每条记录是否命中其中的一个单词,命中保存继续执行,没命中不保存。虽然可能得到一些错误的记录,但也没多大问题,因为你已经去掉了大多数数据。
Prefiltering a data set for an expensive set membership check
有时,检查某个值是否是集合的成员的代价是昂贵的。例如,你可能做涉及到webservice或外部数据库去检验值是否在集合中。这种情形可能非常稀少,但可能突然出现。替代周期性的把列表放到集群,你可以在数据源所在系统产生一个bloom filter并放进去。一旦在适当的位置部署了bloom filter并过滤掉大部分数据,你可以对数据的来源做第二次检查。如果bloom filter能去掉95%以上的数据,你将看到在外部只需要命中剩下的5%。使用这种途径,可以达到100%的准确率,并不会对外部系统带来大量查询的性能问题。
随后的第五章,我们会看到一种模式叫“Reduce Side Join with Bloom Filtering”,就是用bloom filter减少发送到reducers的数据量。提前决定记录是否是我们想要的,能显著减少网络带宽。
Resemblances
Bloom filter在数据分析领域相对较新,很可能因为在大数据性能方面特别收益于这种之前方法论里没有的东西。在hive sql和pig里,bloom filter可以实现为用户自定义的方法,但作为本书写作时,并没有立即可用的原生功能。
Performance analysis
这种模式的性能非常类似于简单filtering。从分布式缓存加载bloom filter花不了多少时间,因为数据相对较小。根据bloom filter检查值也是相对轻松的操作,因为每次都执行常数级别的时间。
Bloom Filtering Examples
Hot list
bloom filter一个最基本的应用正如它所设计之目的:描述数据集。作为本例,bloom filter用一些热点关键词列表训练。我们使用bloom filter测试评论里的每个单词是否在热点列表里。如果返回true,整个记录输出。否则忽略。这里我们不关心bloom filter产生的不可避免的false误报为true的情况。下一个例子详细说明一种使用HBase验证positive bloom filter的方法。
问题:给出用户评论数据,过滤出绝大多数不包含指定关键词的评论。
Bloom filter training。为了演示怎样使用hadoopbloom filters,下面的代码段生成预先决定的单词的集合。这是一个通用的程序,输入参数为一个gzip文件或含有gzip文件的目录,文件里元素的数量,希望的误报率,最终输出文件名。
public class BloomFilterDriver { public static void main(String[] args) throws Exception { // Parse command line arguments Path inputFile = new Path(args[0]); int numMembers = Integer.parseInt(args[1]); float falsePosRate = Float.parseFloat(args[2]); Path bfFile = new Path(args[3]); // Calculate our vector size and optimal K value based on approximations int vectorSize = getOptimalBloomFilterSize(numMembers, falsePosRate); int nbHash = getOptimalK(numMembers, vectorSize); // Create new Bloom filter BloomFilter filter = new BloomFilter(vectorSize, nbHash, Hash.MURMUR_HASH); System.out.println("Training Bloom filter of size " + vectorSize + " with " + nbHash + " hash functions, " + numMembers + " approximate number of records, and " + falsePosRate + " false positive rate"); // Open file for read String line = null; int numElements = 0; FileSystem fs = FileSystem.get(new Configuration()); for (FileStatus status : fs.listStatus(inputFile)) { BufferedReader rdr = new BufferedReader(new InputStreamReader( new GZIPInputStream(fs.open(status.getPath())))); System.out.println("Reading " + status.getPath()); while ((line = rdr.readLine()) != null) { filter.add(new Key(line.getBytes())); ++numElements; } rdr.close(); } System.out.println("Trained Bloom filter with " + numElements + " entries."); System.out.println("Serializing Bloom filter to HDFS at " + bfFile); FSDataOutputStream strm = fs.create(bfFile); filter.write(strm); strm.flush(); strm.close(); System.exit(0); } }
代码中,一个新的bloomfilter对象由最优矢量大小和基于输入参数的最优数量的hash方法(k)。liststatus返回的每个文件按行读,每一行都用来训练bloom filter。所有输入文件处理完后,bloom filter以输入参数为名字序列化到文件中。因为bloomfilter也是writable对象,所以序列化不值一提。简单的使用FileSystem对象创建一个流 FSDataOutputStream,把这个流传给过滤器的写方法,然后刷新并关闭流。
随后bloom filter能很容易得从hdfs反序列化回来。仅仅使用FileSystem对象打开文件并传给bloomfilter的readfilelds方法。Bloom filter的反序列化会在下面map代码的setup方法里演示。
Mapper code。Hadoop框架为每个mapper在执行大量的map调用之前,调用一次setup方法。这里,在map方法使用之前,bloom filter从分布式缓存反序列化回来。分布式缓存是hadoop通用功能,能够保证hdfs上的文件也会在需要这个文件的每个task的本地文件系统也存在。Bloom filter就被填充了一个热点单词列表。
在map方法里,评论从每个输入记录中提取。评论被标记为单词,每个单词清洗掉无关字符。清洗后的单词就可使用bloom filter测试。
Notice:bloom filter是在单词的字节层次上训练。单词相同但大小写不同,会被认为不同。除非你的逻辑需要大小写敏感,最好训练和测试之前转换成小写。
public static class BloomFilteringMapper extends Mapper<Object, Text, Text, NullWritable> { private BloomFilter filter = new BloomFilter(); protected void setup(Context context) throws IOException, InterruptedException { // Get file from the DistributedCache URI[] files = DistributedCache.getCacheFiles(context .getConfiguration()); System.out.println("Reading Bloom filter from: " + files[0].getPath()); // Open local file for read. DataInputStream strm = new DataInputStream(new FileInputStream(files[0].getPath())); // Read into our Bloom filter. filter.readFields(strm); strm.close(); } public void map(Object key, Text value, Context context) throws IOException, InterruptedException { Map<String, String> parsed = transformXmlToMap(value.toString()); // Get the value for the comment String comment = parsed.get("Text"); StringTokenizer tokenizer = new StringTokenizer(comment); // For each word in the comment while (tokenizer.hasMoreTokens()) { // If the word is in the filter, output the record and break String word = tokenizer.nextToken(); if (filter.membershipTest(new Key(word.getBytes()))) { context.write(value, NullWritable.get()); break; } } } }
HBase Query using a Bloom filter
Bloom filters能帮助费劲的任务减少不必要的代价。下面的例子,bloom filter使用至少有1500声誉值的用户id训练。在查询hbase获取更多用户信息之前,我们使用bloom filter 做初始环境的测试。依靠消除不必要的查询,我们加速执行时间。
问题:给出用户评论列表,过滤掉声誉不超过1500的用户的评论。
Mapper code。跟前面的例子一样,用到了反序列化。这个bloom filter使用有声誉的至少1500用户的id训练。仅仅是所有用户的1.5%,所以将会过滤出大量不需要的查询。除了bloom filter,hbase table的连接也会在setup里获取。
在map方法里,抽取用户的id,用bloom filter做检查,如果检查通过,hbase会用这个id去hbase table查询出用户的所有数据。这里,靠验证用户真实的声誉至少1500,来作废可能出现的误报错误。
public static class BloomFilteringMapper extends Mapper<Object, Text, Text, NullWritable> { private BloomFilter filter = new BloomFilter(); private HTable table = null; protected void setup(Context context) throws IOException, InterruptedException { // Get file from the Distributed Cache URI[] files = DistributedCache.getCacheFiles(context .getConfiguration()); System.out.println("Reading Bloom filter from: " + files[0].getPath()); // Open local file for read. DataInputStream strm = new DataInputStream(new FileInputStream( files[0].getPath())); // Read into our Bloom filter. filter.readFields(strm); strm.close(); // Get HBase table of user info Configuration hconf = HBaseConfiguration.create(); table = newHTable(hconf, "user_table"); } public void map(Object key, Text value, Context context) throws IOException, InterruptedException { Map<String, String> parsed = transformXmlToMap(value.toString()); // Get the value for the comment String userid = parsed.get("UserId"); // If this user ID is in the set if (filter.membershipTest(new Key(userid.getBytes()))) { // Get the reputation from the HBase table Result r = table.get(new Get(userid.getBytes())); int reputation = Integer.parseInt(newString(r.getValue( "attr".getBytes(), "Reputation".getBytes()))); // If the reputation is at least 1500, // write the record to the file system if (reputation >= 1500) { context.write(value, NullWritable.get()); } } } }
Query Buffer Optimization
前面查询hbase的例子方法相对幼稚。这里只是为了演示怎么应用这种模式,可以进一步优化。Hbase提供了分批查询,所以理想的情况下,可以预缓存一定大小的查询结果。这个常数取决于内存能充裕的存多少查询。然后把查询刷进hbase执行进一步处理并返回结果。如果昂贵的操作能缓存,这是推荐的做法。只需要记得在mapper或reducer的cleanup方法里刷新缓存。Context对象能用来写输出,就像map或reduce方法。
附:
/** * Gets the optimal Bloom filter sized based on the input parameters and the * optimal number of hash functions. * * @param numElements The number of elements used to train the set. * @param falsePosRate The desired false positive rate. * @return The optimal Bloom filter size. */ public static int getOptimalBloomFilterSize(int numElements, float falsePosRate) { return (int) (-numElements * (float) Math.log(falsePosRate) / Math.pow(Math.log(2), 2)); } /** * Gets the optimal-k value based on the input parameters. * * @param numElements The number of elements used to train the set. * @param vectorSize The size of the Bloom filter. * @return The optimal-k value, rounded to the closest integer. */ public static int getOptimalK(float numElements, float vectorSize) { return (int) Math.round(vectorSize * Math.log(2) / numElements); }