Spark中的错误处理

从网路冷眼的微博上看到这一篇文: Try again, Apache Spark!, 主要解释了为何Spark的函数式和异步使得错误处理过程更加复杂,读罢受益匪浅,简单翻译并加入自己的见解, 分享一下.

1. 典型错误处理

在很多语言中, exceptions用来标识程序的异常行为.如果你需要单独处理一类异常,你将要用到try-catch语句来包裹引起异常的语句.

try{
    someMethod()
} catch(Exception e) {
// Handle the exception behaviour in some way
}

在Scala语言中,上述语句变为:

try {
    someMethod
} catch {
case e:Exception => // Handle the exception behaviour in some way
}

注意观察,处理异常语句的位置和引起异常语句的位置.前者应该包围后者.

2.异步执行和异常处理

Spark使用RDD作为基本单元来构建基于大量数据的算法.

RDD上你有两个操作:转换 transformation行动 actions.转换操作会通过前一个RDD构建一个新的RDD.比如mapflatMap.

val lines:RDD[String]=sc.textFile("large_file.txt")
val tokens = lines.flatMap(_ split " ")

行动操作则基于RDD计算结果出来.然后返回给驱动程序或者保存到外部的存储系统(HDFS,HBase)等

tokens.saveAsTextFile("/some/uotput/file.txt")

最后,关于RDD, 你需要记住:

although you can define new RDDs any time, Spark computes them only in a lazy fashion —that is, the first time they are used in an action.

RDD上处理转换操作的时候, 可能会出错和抛出异常.通常的处理方法就是把转换操作用try-catch包裹起来

val lines: RDD[String] = sc.textFile("large_file.txt")
try {
    val tokens = lines.flatMap(_ split " ")
     // This transformation can throw an exception
     .map(s => s(10))
} catch {
    case e : StringIndexOutOfBoundsException => 
    // Doing something in response of the exception
}
tokens.saveAsTextFile("/some/output/file.txt")

不幸的是, 转换里的代码直到第一次行动 执行时才会真的执行.也就是说上面的处理异常的代码是完全无用的.我们能做的也就只能时把行动 操作用try-catch包裹起来

val lines: RDD[String] = sc.textFile("large_file.txt")
val tokens = 
    lines.flatMap(_ split " ")
    .map(s => s(10))
try {
    // This try-catch block catch all the exceptions thrown by the 
    // preceding transformations. 
    tokens.saveAsTextFile("/some/output/file.txt")
} catch {
    case e : StringIndexOutOfBoundsException => 
    // Doing something in response of the exception
}

你可以看到,我们丢失了异常处理的位置

使用这种方法,我们会丢失抛出异常的元素.此外,Spark是用来处理大量数据的:我们能确定我们的目的就是仅仅因为一个RDD里一个元素的错误就要阻塞整个执行过程吗?

3. 函数式编程和异常处理

第二种处理方法则是将try-catch移动到转换 操作中. 以上代码变为:

    val tokens = 
    lines.flatMap(_ split " ")
       .map {
         s => try {
             s(10)
           } catch {
              case e : StringIndexOutOfBoundsException => 
                // What the hell can we return in this case?
           }
       }   // end of map

通过这么做,我们重新获得了位置 特征! 但是,通过这种方法,我们又引入了另一个问题.先前说过: 一个转换操作从旧的RDD里构建了一个新的RDD.转换操作map的偏函数输入的原始类型是String=>Char

为了保留这个特征,我们不得不在case语句中返回一个Char或他的子类型.我们该怎么选择?空的字符?一个特殊的字符? 这些选择显然迟早会造成其他的问题.

从这个僵局中逃脱出来的唯一办法就是重整monad.粗略的说monad是为简单的类型提供额外属性的泛型容器. Scala至少提供了三中不同的monad类型来帮助我们处理异常情况.

  1. Option 和他的两个子类,Some[T]None.这个monad提供了一个列表,其中有零或一个元素.当我们对错误情况的详情不感兴趣的时候,可以使用他们.
  2. Either 和他的两个子类,Left[T]Right[K].这个monad可以返回两个不同类型的对象,TK,分别表述正常行为和异常行为.
  3. Try和他的两个子类,Success[T]Failure[T].它和Either很像. 使用泛型T替代Left子类. Failure总是Throwable的一个子类.(Try在Scala 2.10引入)

然后, 如果你的目标是RDD处理过程中的异常, 那么Try[T]就能完美的满足你的需求. 这个奇妙的类型的半生对象中自带一个有用的apply工厂方法, 让你直接从计算结果中构建一个Success或者Failure对象.

// ...omissis...
val tokens = 
lines.flatMap(_ split " ")
   .map (s => Try(s(10)))

如果计算过程产生了一个值, 那么Success[T]就会构建,其他情况则由Failure构建. 这个类型是不可变的.Failure 类型可以通过函数get来访问异常中的错误信息(Failure.get).

所以,你的RDD[T]将会变成RDD[Try[T]].通过此操作,我们又可以开心的使用相同的数据结构来处理数据和异常了.

4. 链式操作

现在,我们有了RDD[Try[T]]了.我们怎么用这个类型的实例呢? 因为这个类型是个monad,所以我么可以用mapflatMap来处理.

如果你不得不转换一个Try对象, 你可以使用map方法.但是如何在链式计算中连续使用Try呢,如果只使用map那么Try[B]就会变为Try[Try[B]].所以我们应该使用flatMap,将Try[B]中的B取出来,再算.

// ...omissis...
  lines.flatMap(_ split " ")
   .map (s => Try(s(10)))
   // Using a flatMap the final type will be a RDD[Try[Char]] 
   // and not a RDD[Try[Try[Char]]]
   .flatMap(x => Try(x(20)))

总结

有时候,你的程序仅仅需要从RDD[Try[T]]中获取Success或者Failure实例.最为推荐的方法是collect

// successes has type RDD[Int], no more Try monad
val successes = 
    rdd.collect {
    // The method is applied only to elements of type Success.
    case Success(x) => x
  } 

你可能感兴趣的:(spark,scala,spark,scala)