DDIA:批处理和 MPP 数据库千丝万缕

批处理工作流的输出我们已经讨论了串起 MapReduce 工作流的一些算法 , 但我们忽略了一个重要的问题:当工作流结束后,处理结果是什么?我们一开始是为什么要跑这些任务来着?
对于数据库查询场景,我们会区分事务型处理场景(OLTP)和分析性场景(OLAP,参见事务型还是分析型) 。我们观察到,OLTP 场景下的查询通常只会涉及很小的一个数据子集,因此通常会使用索引加速查询,然后将结果展示给用户(例如,使用网页展示) 。另一方面 , 分析型查询通常会扫描大量的数据记录,执行分组(grouping)和聚集(aggregating)等统计操作 , 然后以报表的形式呈现给用户:比如某个指标随时间的变化曲线、依据某种排序方式的前十个数据条目、将数据按子类分解并统计其分布 。这些报表通常会用于辅助分析员或者经理进行商业决策 。
那批处理处于一个什么位置呢?它既不是事务型,也不是分析型 。当让,从输入数据量的角度来说,批处理更接近分析型任务 。然而 , 一组 MapReduce 任务组成的执行流通常和用于分析型的 SQL 查询并不相同(参见 Hadoop 和分布式数据库的对比) 。批处理的输出通常不是一个报表,而是另外某种格式的数据 。
构建查询索引谷歌发明 MapReduce 大数据处理框架的最初动机就是解决搜索引擎的索引问题,开始时通过 5~10 个 MapReduce 工作流来为搜索引擎来构建索引 。尽管谷歌后面将 MapReduce 使用拓展到了其他场景,仔细考察构建搜索引擎索引的过程,有助于深入地了解 MapReduce(当然,即使到今天,Hadoop MapReduce 仍不失为一个给 Lucene/Solr 构建索引的好办法) 。
我们在“全文索引和模糊索引”一节粗策略的探讨过像 Lucene 这样的全文索引引擎是如何工作的:倒排索引是一个词表(the term dictionary),利用该词表,你可以针对关键词快速地查出对应文档列表(the postings list) 。当然,这是一个很简化的理解 , 在实践中,索引还需要很多其他信息,包括相关度,拼写订正,同义词合并等等,但其背后的原理是不变的 。
如果你想在一个固定文档集合上构建全文索引,批处理非常合适且高效:

  1. MApper 会将文档集合按合适的方式进行分区
  2. Reducer 会对每个分区构建索引
  3. 最终将索引文件写回分布式文件系统
构建这种按文档分区(document-partitioned , 与 term-partitioned 相对 , 参见分片和次级索引)的索引,可以很好地并发生成 。由于使用关键词进行索引查询是一种只读操作,因此,这些索引文件一旦构建完成,就是不可变的(immutable) 。
如果被索引的文档集发生变动,一种应对策略是,定期针对所有文档重跑全量索引构建工作流(workflow),并在索引构建完时使用新的索引对旧的进行整体替换 。如果两次构建之间,仅有一小部分文档发生了变动,则这种方法代价实在有点高 。但也有优点,索引构建过程很好理解:文档进去,索引出来 。
当然,我们也可以增量式的构建索引 。我们在第三章讨论过,如果你想增加、删除或者更新文档集,Lucene 就会构建新的索引片段,并且异步地将其与原有索引进行归并(merge)和压实(compact) 。我们将会在第十一章就增量更新进行更深入的讨论 。
以 KV 存储承接批处理输出搜索索引只是批处理工作流一种可能的输出 。批处理其他的用途还包括构建机器学习系统,如分类器(classifiers,如 垃圾邮件过滤,同义词检测,图片识别)和推荐系统(recommendation system,如你可能认识的人,可能感兴趣的产品或者相关的检索) 。
这些批处理任务的输出通常在某种程度是数据库:如,一个可以通过用户 ID 来查询其可能认识的人列表的数据库,或者一个可以通过产品 ID 来查询相关产品的数据库 。
web 应用会查询这些数据库来处理用户请求,这些应用通常不会跟 Hadooop 生态部署在一块 。那么,如何让批处理的输出写回数据库,以应对 web 应用的查询?
最直观的做法是,在 Mapper 或者 Reducer 代码逻辑中,使用相关数据库的客户端库,将 Mapper 或者 Reducer 的输出直接写入数据库服务器,每次一个记录 。这种方式能够工作(须假设防火墙允许我们直接从 Hadoop 中访问生产环境的数据库服务器),但往往并不是一个好的做法: