漫谈分布式系统(18) -- 跑的快不够,还要写的快


这是《漫谈分布式系统》系列的第 18 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。  

啰嗦的 MapReduce

前面几篇文章,我们介绍了一些解决分布式计算框架性能的方法。这样,在扩展性不断带来计算能力的情况下,我们又更高效地使用了这些计算能力。

这就解决了大数据应用开发的一个关键问题:执行效率问题。

但还有一个影响大数据应用的效率问题 -- 开发效率,依然困扰着开发人员。

import java.io.IOException;
import java.util.StringTokenizer;


import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;


public class WordCount {


  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{


    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();


    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }


  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();


    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }


  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

上面展示的是 Hadoop 官网上的 MapReduce 版本的 WordCount 的 代码。说又臭又长一点都不过分。

这还真是最简单的 WordCount,稍微复杂点的逻辑,肯能就需要很多个比这还复杂的 MR 程序了。

这个开发和调试成本是企业生产无法接受的。必须得想出路。

MR 天生长这样,已经没救了,但我们可以从它身上找出问题,来指导新的方案。

MR 这么难写,最根本的问题出在编程范式(Programming Paradigm)上。

编程范式有很多分类,如最常见的面向过程、面向对象、函数式等等。而影响 MR 的是这样一组:

  • 命令式编程(Imperative Programming),侧重过程,一步步怎么做(How),对机器更友好,

  • 声明式编程(Declarative Programming),侧重目标,想要干什么(What),对人更友好。


命令式编程更底层更细节,你能拥有十足的掌控力,但也要承担与之而来的繁琐。而声明式编程,则舍弃对重复工作的掌控力,来换取劳动力的解放。

很显然,MR 属于命令式编程。在大数据领域,MR 的难兄难弟还有流处理领域的 Storm。

不过,必须要强调的是,声明式编程也只是在命令式编程的基础上,对特定目标做的封装而已。这很好理解,因为机器只接受指令,并不理解也无法直接达到目标。

而特定目标一定是有限的,所以命令式编程和声明式编程并不会彼此取代。而是像编程语言一样,各有各的适用场景。

回到大数据应用开发的场景下,很显然,我们需要声明式编程的拯救。

等等,声明式编程最典型的两个子类,就是 DSL 和函数式编程。这个系列早期的文章我们提过,MapReduce 就是借鉴的函数式编程里的 map() 和 reduce() ,怎么突然又变成命令式了

问题就出在 MR 是用 Java 写的,而在 Java 里,class 和 function 都不是一等公民,所以没法像典型的函数式编程语言那样几行搞定,只能老老实实一行行定义 class、function/method。没办法,Java 的啰嗦是出了名的。

当然,Java 也在进步,在引入 Lambda 后,Java 也能看起来更函数式了。再结合 Stream API,MR 也可以写的很简单了:

public class WordCount {
  
  public static void main(String[] args) throws IOException {
      Path path = Paths.get("src/main/resources/book.txt");
      Map<String, Long> wordCount = Files.lines(path).flatMap(line -> Arrays.stream(line.trim().split("\s")))
              .map(word -> word.replaceAll("[^a-zA-Z]", "").toLowerCase().trim())
              .filter(word -> word.length() > 0)
              .map(word -> new SimpleEntry<>(word, 1))
              .collect(groupingBy(SimpleEntry::getKey, counting()));


      wordCount.forEach((k, v) -> System.out.println(String.format("%s ==>> %d", k, v)));


  }
}

Hive,给大象插上翅膀

MapReduce 的 SQL 化,导致了 Hive 的诞生。

从上面 Hive 的架构示意图,可以提取一些重点:

  • 在服务端,Hive 由 HiveServer2 和 Hive MetaStore Server 组成,

  • 其中,HiveServer2 负责接收客户端请求,并对 SQL 做解析、计划、优化和执行,

  • 而 Hive MetaStore Server 则负责管理保存在 MetaStore DB 中的元数据,

  • 对客户端,Hive 提供标准 JDBC 和 ODBC 支持,并提供了自己的 JDBC 实现 beeline 作为默认客户端,

  • HiveServer2 会将客户端的 SQL 解析成 MR 任务并提交到 YARN 执行,以处理保存在 HDFS 上的数据。

总的来看,Hive 可以说是一个分布式数据库:

  • 数据的存储,数据虽然保存在 HDFS 上,但元数据,即 schema 的定义和管理由 Hive 负责,我们会在后面的文章中专门讲,

  • 数据的计算,支持 SQL 的解析和执行,是这篇文章关注的重点。

对于 WordCount 的例子,我们就可以创建这么一张表:

CREATE TABLE tmp.word_count (
  word string
) 
ROW FORMAT DELIMITED FIELDS TERMINATED BY ' '
LOCATION '<some_hdfs_location>'

然后只要这么一句 SQL(在 Hive 里叫 HQL)就能得到想要结果了:

SELECT 
  word, count(*) 
FROM 
  tmp.word_count
GROUP BY 
  word;

毫无疑问,简单太多了。

我们用 explain 命令看下,SQL 语句是怎么变成 MR 任务的。

可以很清晰的看到 Map 和 Reduce 阶段,以及其中的 group、count 等操作。而前面几次提到过,复杂的逻辑需要多个 MR 任务串连完成,在 Hive 里自然大都可以只用一条 SQL 实现,但底层会解析为很多 Stage,每个 Stage 就可能(但不一定)对应一个 MR 任务。

另外,除了写起来简单,Hive 还是个 interactive 的环境,省去了繁琐的编译、调试和部署流程,这无疑又进一步降低了开发成本。

Hive 很快成为 SQL on Hadoop 的默认选择,大大降低了 Hadoop 下的数据处理成本,很快得到非常广泛的应用。同时吸引了一些不太会编程但能熟练使用 SQL 的分析人员,这又扩大的 Hadoop 的流行范围。

Spark 也能 SQL

然后,前面几篇文章我们已经说过,MR 很慢,并引出了 Spark。难道开发效率和执行效率只能二选一吗

当然不是。上面刚说过,Hive 可以分为(元)数据存储和计算两部分。算的慢,那把计算部分替换成 Spark 不就好了?!

如上图所示,把执行引擎从 MR 改成 Spark,这就是有名的 AMPLab 的又一个作品 Shark 做的事情,主要开发者也是 Spark 的核心开发者。

这还没完,Spark 是个有野心的项目,Hive 的限制越来越阻碍 Shark 的发展。于是 Shark 团队决定停止 Shark 开发,转而另起炉灶,完全基于 Spark 的开发了 Spark SQL。当然,保留了对 Hive 的兼容。

而 Hive 社区也没有停止进化,受 Shark 的启发,也开始推进 Hive on XX 的项目,底层支持不同的执行引擎,包括 MR、Spark、Tez 等。不过 Hive on Spark 发展的并没有 Spark SQL 好。

上图是 Spark SQL 的架构示意图。概括下要点如下:

  • Spark SQL 提供了三种接入方式:Spark ThriftServer、Spark SQL CLI、DataFrame API,

  • Spark ThriftServer 是从 Hive ThriftServer 改造而来,很自然地支持 JDBC、ODBC、Thrift 连接,

  • Spark SQL CLI 是 Spark 自带的命令行工具,bin/spark-sql 即可交互式的查询数据,注意,和 beeline 不一样的是,这个 CLI 并不会连接 Spark ThrfitServer,而是独立的 driver,反倒是 beenline 可以直连 Spark ThriftServer,

  • DataFrame/Dataset API 可以通过 Java、Scala、Python、R 等语言调用来执行 SQL 语句,

  • SQL 和 DataFrame 都将被 Catalyst 解析和优化,其中需要利用元数据,元数据可以从 Hive 读取,也可以通过 Data Source API 从数据文件读取,如 Parquet 文件自带了 schema 信息,

  • Catalyst 优化后生成以 RDD 为基础的物理执行计划,交由 Spark Core 提交执行,对数据的读写通过 Data Source API 处理。

值得一提的是,Spark SQL 的 SQL 很显然对标 HQL,可以直接写 SQL 做交互式的查询,而 DataFrame/Dataset API 就大致相当于 MR 的 lambda+stream 写法了:

df.groupBy('word').count()

或者直接写成这样:

df.sql("select word, count(*) from table tmp.word_count")

TL;DR

  • 前面几篇文章对性能的优化,解决了执行效率问题,但开发效率问题依然困扰着开发者,

  • MapReduce 写起来很啰嗦,主要是其编程范式的影响,

  • 编程范式有很多分类,如命令式和声明式,前者告诉计算机怎么做,后者只说明做什么,

  • 但 MR 来源于函数式编程里的 map() 和 reduce(),应该就是声明式的,主要是被 Java 拖累了,

  • 引入 lambda 后的 Java,也能写出很简洁的 MR 程序了,

  • 但这样的声明式还不够彻底和简单,我们需要 DSL,尤其是 DSL 的代表 SQL,

  • 于是有了 Hive,接受 SQL 语句并翻译成 MR 任务,迅速成为了默认的 SQL on Hadoop 方案,

  • Spark 性能比 MR 更好,于是有了 Spark SQL,使得开发效率和执行效率都得到兼顾,


细心的人肯定已经发现,能够被 SQL 处理的数据都是带 schema 的,也就是所谓结构化数据(Structured Data)。

对于非结构化数据, SQL 就无能为力了。还得 Spark 的 RDD 或 Hadoop MapReduce 处理。

而对 Spark 而言,结构化数据用 Spark SQL,非结构化数据用 Spark Core(RDD),就能分别替代 Hive 和 MapReduce 了。这也是我们推荐的选择。

并且,结构化数据带来的 schema 信息,不仅使得我们能通过 SQL 很方便的处理,也为额外的性能优化提供了可能。

下一篇,我们就一起了解下 SQL 性能优化。

关联阅读

漫谈分布式系统(15) -- 扩展性的最后障碍

漫谈分布式系统(16) -- 搞定 worker 的性能问题

漫谈分布式系统(17) --  决战 Shuffle

从 Spark 的数据结构演进说开

原创不易

关注/分享/赞赏

给我坚持的动力

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值