第 2回 “Learning Spark” 読書会
Chap. 3: “Programmingwith RDDs”
– RDDsプログラミング –
@data sciesotist
2015/3/29
1 “This chapter introduces...”• Sparkにおけるデータ (RDDs) の取り扱いを紹介する
• Sparkにおけるすべての操作はRDDsの生成、RDDsの変換、RDDsの計算であらわされる
• データサイエンティストもエンジニアもこの章を読んでね
• できれば、手元で動作確認しながら読んで欲しい
• コードはGitHubで公開している
……だそうです。
1
2 “RDD Basics” (1)• RDD (Resilient Distributed Datasets) はイミュータブル*1な分散コレクションである
• RDDは複数のパーティションに分割され、それぞれ異なるノードで処理される
• 外部のデータセットを読み込むか、コレクションオブジェクトを分散することで生成できる
*1 Immutable: 不変2
3 “RDD Basics” (2)
>>> lines = sc.textFile("README.md")
• 作成したRDDsには2つの処理が適用できる
■ Transformations: RDDsから別の新しいRDDsを生成する
■ Actions: RDDsに対して処理を適用した結果を出力する
3
4 “RDD Basics” (3) Transformations• Transformationsの例として、条件に一致する要素を抽出して、新しいRDDを作成するフィルタリングがある
>>> pythonLines = lines.filter(lambda line:
"Python" in line)
4
参考: 主なTransformations (1)
関数 機能map(func) 各要素にfuncを適用した結果
を新しいRDDとして返すfilter(func) 各要素のうちfuncに一致する
ものを抽出して返すflatMap(func) 各要素にfuncを適用した結果
を展開 (flatten) して返すmapPartitions(func) 各パーティションに対して
funcを適用した結果を返すmapPartitionsWithIndex(func) パーティションのインデック
スを指定してmapPartitionsを実行した結果を返す
5
参考: 主なTransformations (2)
関数 機能sample(Rep,fraction,seed) RDDから条件を指定してサンプ
リングした結果を返すunion(Dataset) データを結合した結果を返すintersection(Dataset) データ間の共通要素を返すdistinct() データセットから重複したデー
タを除いた結果を返すgroupByKey() Key-Value形式のデータについ
てKeyを集計した結果を返すreduceByKey(func) Key-Value形式のデータについ
てfuncを適用した結果を返す
6
参考: 主なTransformations (3)
関数 機能aggregateByKey(zero)(seq,comb) Key-Value形式のデータについ
て、Valueに処理を行い集計した結果を返す
sortByKey() Key-Value形式のデータについてKeyでソートした結果を返す
join(Dataset) データセットどうしをJoinした結果を返す
cogroup(Dataset) データセットどうしを同じKeyでグルーピングした結果を返す
cartesian(Dataset) データセットどうしを直積でJoinした結果を返す
7
参考: 主なTransformations (4)
関数 機能pipe(command) 各要素にシェルコマンドを適用
した結果を返すcoalesce(num) 指定した数までパーティション
数を減らすrepartition(num) データを再配置する
repartitionAndSort
WithinPartitions(partitioner)
データを指定した方法で再配置し、ソートした結果を返す
……とがんばってまとめたものの、4章に表があるんだよねorz
8
5 “RDD Basics” (4) Actions• Actionsの例として、RDDから最初の1要素を抽出するfirst()がある
>>> pythonLines.first()
• Actionsは、処理結果をドライバプログラムに返すか、外部ストレージシステム (HDFSなど)に保存する
9
6 “RDD Basics” (5) TransformationsとActionsの違い
• SparkがRDDsを処理する際の動作が異なる
• RDDsを定義しても、最初に使用される時まで処理は実行されない (lazy fashion)
• 即座に処理すると、メモリの無駄になる• first()アクションも、条件にマッチする最初の1行までしかデータを読み込まない
10
7 “RDD Basics” (6) 永続化 (1)• RDDsはActionsの実行のたびに処理される
• RDDsを繰り返し使いたい場合はRDD.persist()を使う
Level Space used CPU time In memory On disk
MEMORY ONLY 高 低 Y N
MEMORY AND DISK 高 中 一部 一部
MEMORY ONLY SER 低 高 Y N
MEMORY AND DISK SER 低 高 一部 一部
DISK ONLY 低 高 N Y
MEMORY ONLY 2 など パーティションの複製を作成する
OFF HEAP Tachyonファイルシステムに保存する
11
8 “RDD Basics” (6) 永続化 (2)• データの一部をメモリ上に永続化し、繰り返しクエリを発行する時に使うことが多いだろう
>>> pythonLines.persist()
>>> pythonLines.count()
3
>>> pythonLines.first()
u’high-level APIs in...
12
9 “RDD Basics” のまとめ1. データからRDDsを作る
2. Transformationsは新しいRDDsを作る
3. persist()でRDDsを永続化できる
4. ActionsはRDDsを分散処理する
• cache()もデフォルト設定でpersist()を実行したのと同じ結果を得られる
13
10 “Creating RDDs” (1)
• RDDsの作り方には2種類ある
1. 外部データセットを読み込む方法
2. コレクションを並列化 (Parallelizing)させる方法
• 簡単なのはsc.parallelize()を使う方法 (上記2)
• ファイルからの読み込みはすでに行ってきた14
11 “Creating RDDs” (2)
# Pythonの場合lines = sc.parallelize(["pandas","i like pandas"])
# Scalaの場合val lines =
sc.parallelize(list("pandas","i like pandas"))
# Javaの場合 (実際には1行で)JavaRDD<String> lines =
sc.parallelize(Arrrays.asList("pandas","i like pandas"));
15
12 “RDD Operations” (1)• TransformationsとActionsがある (再掲)
• TransformationsはRDDsを返す
• Actionsは他のデータ型を返す
• SparkにおけるTransformationsとActionsの動作の違いを理解することは重要なので、詳しく見ていく
16
13 “RDD Operations” (2) Transformations
• Transformationsは新しいRDDsを生成する
• RDDsに対してActionsが実行されるまでは、処理されない (Lazy Evaluation;遅延評価)
• 多くのTransformationsは “要素指向”(element-wise)で、各要素に対して作用する
• ログファイルlog.txtからエラーメッセージだけを抽出する例を見ていく
17
14 “RDD Operations” (3) Transformations
# Pythonの場合inputRDD=sc.textFile("log.txt")errorsRDD=inputRDD.filter(lambda x: "error" in x)
# Scalaの場合val inputRDD=sc.textFile("log.txt")val errosRDD=inputRDD.filter(line =>
line.contains("error"))
18
15 “RDD Operations” (4) Transformations
# Javaの場合JavaRDD<String> inputRDD = sc.textFile("log.txt")JavaRDD<String> errorsRDD = inputRDD.filter(new Function<String,Boolean>(){public Boolean call(String x){
return x.contains("error");}});
• filter()はinputRDDを変更しない• 新しいerrorsRDDオブジェクトが生成される
19
16 “RDD Operations” (5) Transformations
• 今度はunion()を用いてwarningまたはerrorを検索する
# Pythonの場合warnRDD=inputRDD.filter(lambda x: "warning" in x)badLinesRDD=errorsRDD.union(warnRDD)
• union()は2つのRDDsを処理する
• RDDsを生成する際の操作手順を保持しているので、RDDsが失われた際に復旧できる
20
17 “RDD Operations” (6) Transformations
inputRDD
errorsRDD
warnRDD
badLinesRDD
filter()
filter()
union()
Lineage GraphまたはDAG (有向無閉路グラフ) というらしい
21
18 “RDD Operations” (7) Actions
• データセットに対して “何か” を実行し、出力するのがActions
• ログ処理の例を続けて、badLinesRDDの情報を出力する方法を見ていく
• count()はデータの件数を返す• take()は指定した件数の要素を返す
22
19 “RDD Operations” (8) Actions
# Pythonの例print "Input had" + badLinesRDD.count()+ "concerning lines"print "Here are 10 examples:"for line in badLinesRDD.take(10):print line
# Scalaの例println("Input had" + badLinesRDD.count()+ "concerning lines"println("Here are 10 examples:")badLinesRDD.take(10).foreach(println)
23
20 “RDD Operations” (9) Actions
# Javaの例System.out.println("Input had"+ badLinesRDD.count() + "concerning lines")System.out.println("Here are 10 examples:")for(String line: badLinesRDD.take(10)){System.out.println(line);}
• 注意: collect()はRDDs内の全データをメモリに展開するため、サイズの大きいRDDsに対して使用すべきではない
24
21 “RDD Operations” (10) Actions
• 大量データをHDFSやAmazon S3などのストレージに保存するにはsaveAsTextFile()やsaveAsSequenceFile()などを使う
• 注意: アクションを実行するたびに、RDDsはゼロから再生成される
• これを避けるために、persist()を用いてRDDsを永続化することができる
25
22 “RDD Operations” (11) Lazy Evaluation
• RDDsは「遅延評価」される• HaskelやLINQのような関数型言語ではよくあるパラダイム• 内部では処理手順だけが (まずは)記録される• sc.textFile()を実行しても、すぐにはデータは読み込まれない• (ディスクアクセスなどを考慮して)処理手順に悩む必要がなく、効率的な開発、実行ができる
26
23 “Passing Functions to Spark” (1)• ドライバプログラムからSparkに処理を渡すために
Passing Functionsが用いられる
• Pythonでは3つの形式で利用できる (ラムダ式、トップレベル関数、ローカル関数)
word = rdd.filter(lambda s: "error" in s)def containsErrot(s):return "error" in s
word = rdd.filter(containsError)
• 全てのデータがワーカーノードに送信されるので注意
27
参考: データの一部を参照するつもりで大変なことになる例
• フィルタリングの結果ではなく、データ全体への参照になっている?
class SearchFuntions(object):
def __init__(self,query):
self.query = query
def isMatch(self,s):
return self.query in s
def getMatchesFunctionReference(self,rdd):
return rdd.filter(self.isMatch)
def getMatchesMemberReference(self,rdd):
return rdd.filter(lambfs x: self.query in x)
28
参考: 正しくデータの一部だけを参照する例
class WordFunctions(object):
...
def getMatchesNoReference(self,rdd):
query = self.query
return rdd.filter(lambda x: query in x)
• プログラミングはよくわかりませんが、変数のスコープとかそういう話?(当日) 誰か教えてください
29
24 “Passing Functions to Spark” (2)• Scalaでも同様に全データを参照しないよう注意する
class SearchFunctions(val query:String){def isMatch(s:String): Boolean = {s.contains(query)
}def getMatchesFunctionReference(rdd: RDD[String]): RDD[String] = {# 注意: 全データを参照してしまうrdd.map(isMatch)
}def getMatchesFieldReference(rdd: RDD[String]): RDD[String] = {# 注意: 全データを参照してしまうrdd.map(x => x.split(query))
}def getMatchesNoReference(rdd: RDD[String]): RDD[String] = {# データを一部だけ参照するval query_ = this.queryrdd.map(x => x.split(query_))
}}
30
25 “Passing Functions to Spark” (3)• Javaではorg.apache.spark.api.java.functionパッケージのインターフェースの実装にあたる
# 匿名内部クラスを使う場合RDD<String> errors = lines.filter(
new Function<String,Boolean>(){
public Boolean call(String x){
return x.contains("error");}
}
31
26 “Passing Functions to Spark” (4)
# 名前付きクラスを使う場合class ContainsError implements
Function<String,Boolean>(){
public Boolean call(string x){
return x.contains("error");}
}
RDD<String> errors =
lines.filter(new ContainsError());
32
27 “Passing Functions to Spark” (5)
# function class with parametersを使う場合class Contains implements
Function<String,Boolean>{
private String query;
public Contains(String query){
this.query = query;}
public Boolean call(String x){
return x.contains(query);}
}
33
28 “Passing Functions to Spark” (6)• Java 8ではラムダ式が使えるので、簡潔な実装になる
• 詳しくはOracleやDatabrickのページを見てね
RDD<String> errors = lines.filter(
s -> s.contains("error"));
• 匿名内部クラスでもラムダ式でも、final修飾子のついた変数をSparkに渡すことができる(というのがどう良いことなのかよくわかりませんが)
34
29 “Common Transformations andActions” (1)
• RDDsに対する基本的なTransformationsとActionsを見てきた
• ここからは、RDDsのタイプごとに、固有の操作を紹介していく
35
30 “Common Transformations andActions” (2) Basic RDDs
• すべてのタイプのRDDsに適用可能な操作を見ていく
• まずはTransformations
• map()はRDDsの各要素に処理を適用し、新しいRDDsに出力する
• map()が返す値は必ずしも元のRDDsと同じ型ではない
• filter()はRDDsの各要素のうち、フィルタ条件を満たすものだけを新しいRDDsに出力する
36
31 “Common Transformations andActions” (3) Basic RDDs
inputRDD
{1,2,3,4}
map(x=>x*x)
filter(x=>x!=1)
mappedRDD
{1,4,9,16}
filteredRDD
{2,3,4}
37
32 “Common Transformations andActions” (4) Basic RDDs
# Pythonでのmap処理の例nums = sc.parallelize([1,2,3,4])squared = nums.map(lambda x: x * x).collect()for num in squared:
print "%i " % (num)
# Scalaでのmap処理の例val input = sc.parallelize(List(1,2,3,4))val result = input.map(x => x * x)println(result.collect().mkString(","))
38
33 “Common Transformations andActions” (5) Basic RDDs
# Javaでのmap処理の例JavaRDD<Integer> rdd = sc.parallelize(Arrays.asList(1,2,3,4));JavaRDD<Integer> result = rdd.map(
new Function<Integer,Integer>(){public Integer call(Integer x) {return x*x; }
});
System.out.println(StringUtils.join(result.collect(),","));
39
34 “Common Transformations andActions” (6) Basic RDDs
• flatMap()はRDDsの個々の要素に処理を行った後、要素を展開 (flatten) して出力する
inputRDD{“coffee panda”, “happy
panda”, “happiest
panda party”}
map(tokenize)
flatMap(tokenize)
mappedRDD{[“coffee”, “panda”],[“happy”,
“panda”],[“happiest”, “panda”, “party”]}
filteredRDD{“coffe”, “panda”, “happy”, “panda”,
“happiest”, “panda”, “party”}
tokenize(“coffe panda”=>List(“coffee”,”panda”)
40
35 “Common Transformations andActions” (7) Basic RDDs
• 以下の4つの操作は、2つのRDDsが、同じタイプである必要がある
• distinct(): 重複を除去する (負荷が高い)
• union(): RDDsを結合する (重複を許容する)
• intersection(): 共通要素を出力する(重複は排除される、さらに負荷が高い)
• subtract(): 共通しない要素を出力する
41
36 “Common Transformations andActions” (8) Basic RDDs
• cartesian()でRDDsを直積結合できる
• 片方のRDDsの1要素ごとに、もう一方のRDDsのすべての要素を対応付けて出力する
• ユーザー間の類似性の判定などを行う際に便利• 非常に負荷が高い
42
37 “Common Transformations andActions” (9) Basic RDDs
• 続いてActionsについて
• もっとも一般的なActionsはreduce()
• 2つの要素を処理して、1つの要素として返す
• 似たようなものとしてfold()がある
• 違いはカウンタ (zero value)を与えるかどうか
• 移動平均を算出するときなどに必要43
38 “Common Transformations andActions” (10) Basic RDDs
# Pythonの場合sum = rdd.reduce(lambda x,y: x + y)
# Scalaの場合val sum = rdd.reduce((x,y) => x + y)
# Javaの場合Integer sum = rdd.reduce(new Function2<Integer,Integer,Integer>(){public Integer call(Integer x, Integer y)
{ return x + y; } });
44
39 “Common Transformations andActions” (11) Basic RDDs
• aggregate()は処理対象のRDDsと処理後のRDDsが同じ型である必要がない
• 引数は、(1) zero value、(2)各要素の処理、(3) 集約処理
• collect()はRDDsのすべての要素を返す
• take(n)はデータをn個抽出する
• top(n)はデータを先頭からn件抽出する
• takeSample()はデータをサンプリング抽出する45
40 “Common Transformations andActions” (12) Basic RDDs
• foreach()は個々の要素に処理を実行し、返り値はない
• count()は要素の件数を返す• countByValue()は要素ごとにmapし、件数を返す
46
41 “Common Transformations and Actions”(13) Converting Between RDD Types
• 特定の型のRDDsにしか適用できない関数もある (mean()やjoin()など)
• Scalaでは型変換は暗黙のうちに行われる
• Javaでは型変換を明示的に行う必要がある
• Pythonではすべての関数が同じpyspark.RDDクラスに実装されている
47
42 “Persistence” (1)• persist()でRDDsを永続化できる
• 反復処理などを行う際に便利
• 一部が消失しても、再計算して復旧できる
• あらかじめ複製を作っておくこともできる
• ScalaとJavaのデフォルト動作は、シリアライズせずにヒープ領域に保存する
• Pythonではデフォルトでシリアライズして保存する
48
43 “Persistence” (2)• インメモリファイルシステムであるTachyonに保存することもできる
• 詳細はTachyonのドキュメントを参照
• persist()は最初のActionsの前に実行する
• メモリのサイズを超えるRDDsを永続化する場合、古い領域から削除され、アクセスの際に再計算される
• unpersist()で永続化を解除できる
49
44 “Conclusion”• 3章ではRDDsの実行モデルと多数のさまざまな処理を紹介した
• 3章がわかれば、Sparkのコアは理解している
• 4章ではRDDsの特殊な形態であるKey-Valueペアについて扱う
• その後の章で、さまざまなデータソースの扱いやSparkContextの応用的な話題を取り上げる
50