从网路冷眼的微博上看到这一篇文: Try again, Apache Spark!, 主要解释了为何Spark的函数式和异步使得错误处理过程更加复杂,读罢受益匪浅,简单翻译并加入自己的见解, 分享一下.
在很多语言中, 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
}
注意观察,处理异常语句的位置和引起异常语句的位置.前者应该包围后者.
Spark使用RDD
作为基本单元来构建基于大量数据的算法.
在RDD
上你有两个操作:转换 transformation和行动 actions.转换操作会通过前一个RDD构建一个新的RDD.比如map
和flatMap
.
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
里一个元素的错误就要阻塞整个执行过程吗?
第二种处理方法则是将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
类型来帮助我们处理异常情况.
Option
和他的两个子类,Some[T]
和None
.这个monad
提供了一个列表,其中有零或一个元素.当我们对错误情况的详情不感兴趣的时候,可以使用他们.Either
和他的两个子类,Left[T]
和Right[K]
.这个monad
可以返回两个不同类型的对象,T
和K
,分别表述正常行为和异常行为.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]]
.通过此操作,我们又可以开心的使用相同的数据结构来处理数据和异常了.
现在,我们有了RDD[Try[T]]
了.我们怎么用这个类型的实例呢? 因为这个类型是个monad
,所以我么可以用map
和flatMap
来处理.
如果你不得不转换一个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
}