Embulk 20150411

Preview:

Citation preview

Hiroshi NakamuraSoftware EngineerTreasure Data, K.K.

『Embulk』に見るモダンJavaの実践的テクニック~並列分散処理システムの実装手法~

1

#ccc_cd4 / #embulk

#ccc_cd4 / #embulk

Today’s talk

Embulkとは > バルクデータ転送の難しさ > Embulkのアプローチ > アーキテクチャ概要

Java実装技術 > Java 7ネイティブ > Guiceによるコンポーネント間の接続 > ServiceLoaderによる拡張 > Jacksonによるモデルクラス、Immutable > Nettyバッファアロケータ、Unsafe

2

#ccc_cd4 / #embulk

Embulkとは? - http://embulk.org/

> オープンソースのバルクデータ転送ツール > “A” から “B” へレコードを転送

> プラグイン機構 > 多様な “A” と “B” の組み合わせ

> データ連携を容易に > システム構築の頭痛の種の一つ

Storage, RDBMS, NoSQL, Cloud Service, …

broken records, error recovery, maintenance,

performance, …

3

#ccc_cd4 / #embulk

Embulk committers

Hiroshi Nakamura @nahi

Muga Nishizawa @muga_nishizawa

Sadayuki Furuhashi @frsyuki

4

#ccc_cd4 / #embulk

バルクデータ転送の難しさ

> 入力データの正規化

> エラー処理

> メンテナンス

> 性能

#ccc_cd4 / #embulk

入力データ正規化の難しさ

データエンコーディングのバリエーション

> null、時刻、浮動小数点

> 改行、エスケープ、レコード/カラム区切り

> 文字コード、圧縮有無

→ 試行によるデータ正規化

#ccc_cd4 / #embulk

エラー処理の難しさ

例外値の扱い ネットワークエラーからの復旧 ディスクフルからの復旧 重複データ転送の回避

→ データバリデーション、リトライ、リジューム

#ccc_cd4 / #embulk

メンテナンスの難しさ

継続的な動作の確保 データ転送要件変更への対応

→ ドキュメント、汎用化、OSS化

#ccc_cd4 / #embulk

性能の問題

転送データ量は通常増えていく 対象レコードも増えたりする

→ 並列・分散処理

#ccc_cd4 / #embulk

バルクデータ転送の例

指定された 10GB CSV file をPostgreSQLにロード > 1. コマンド叩いてみる → 失敗 > 2. データを正規化するスクリプトを作成

”20150127T190500Z”→“2015-01-27 19:05:00 UTC”に “null”→“\N”に変換 元データを見ながら気付く限り…

> 3. 再度チャレンジ → 取り込まれたが元データと合わない “Inf”→“Infinity”に変換

> 4. ひたすら繰り返す > 5. うっかりレコードが重複して取り込まれた…

#ccc_cd4 / #embulk

バルクデータ転送の例

指定された 10GB CSV file をPostgreSQLにロード > 6. スクリプトが完成 > 7. cronに登録して毎日バルクデータロードするよう登録 > 8. ある日別の原因でエラーに…

不正なUTF-8 byte sequenceをU+FFFDに変換

#ccc_cd4 / #embulk

バルクデータ転送の例

過去の日次 10GB CSV file を 730個 取り込む(2年分) > 1. たいていのスクリプトは遅い

> 最適化してる暇がない > 1ファイル1時間、エラー発生しなかったとして1ヶ月

> 2. 並列データロードするようスクリプト変更 > 3. ある日ディスクフル/ネットワークエラーで失敗

> どこまで読み込まれた? > 4. 障害後に再開し易いよう、データロード単位を調整 > 5. 安全な再開機能をスクリプトに追加

#ccc_cd4 / #embulk

システム構築の頭痛の種

様々な転送データ、データストレージ

> CSV, TSV, JSON, XML, MessagePack, SequenceFile, RCFile

> S3, Salesforce.com, Google Cloud Storage, BigQuery, Elasticsearch

> MySQL, PostgreSQL, Oracle, MS SQL Server, Amazon Redshift, Redis, MongoDB

#ccc_cd4 / #embulk

HDFS

MySQL

Amazon S3

CSV Files

SequenceFile

Salesforce.com

Elasticsearch

Cassandra

Hive

Redis

Broken script :(

Sometimes fails :(

No one can fix :(

14

#ccc_cd4 / #embulk

HDFS

MySQL

Amazon S3

CSV Files

SequenceFile

Salesforce.com

Elasticsearch

Cassandra

Hive

Redis

Broken script :(

Sometimes fails :(

No one can fix :(

N x Mscripts!

> Poor error handling > No retrying / resuming > Low performance > Often no maitainers

15

#ccc_cd4 / #embulk

Embulkのアプローチ

> プラグインアーキテクチャ

> 入力データ正規化支援: guess, preview

> 並列・分散実行

> 繰り返し実行

> トランザクション制御

16

#ccc_cd4 / #embulk

HDFS

MySQL

Amazon S3

CSV Files

SequenceFile

Salesforce.com

Elasticsearch

Cassandra

Hive

Redis

Broken script :(

Sometimes fails :(

No one can fix :(

17

#ccc_cd4 / #embulk

HDFS

MySQL

Amazon S3

CSV Files

SequenceFile

Salesforce.com

Elasticsearch

Cassandra

Hive

Redis

Reliable framework :-)

18

#ccc_cd4 / #embulk

HDFS

MySQL

Amazon S3

CSV Files

SequenceFile

Salesforce.com

Elasticsearch

Cassandra

Hive

Redis

PluginsPlugins

Reusable plugins

19

#ccc_cd4 / #embulk

プラグインアーキテクチャ

拡張ポイントが再利用可能なコンポーネントを定義

従っている限りフレームワークの恩恵を受けられる

> 並列処理、繰り返し実行、エラー処理、リカバリ

20

#ccc_cd4 / #embulk

Embulkプラグインの例

RubyGemsとして配布 - http://www.embulk.org/plugins/ > DB > Oracle, MySQL, PostgreSQL, Amazon Redshift

> 検索エンジン > Elasticsearch

> クラウドサービス > Salesforce.com > Amazon S3 > Google Cloud Storage, Google BigQuery

> ファイルフォーマット > CSV, TSV, JSON, XML > pcap packet capture files > gzip, bzip2, zip, tar, cpio

21

#ccc_cd4 / #embulk

デモ

> guessとpreview

> 並列・分散実行

> 繰り返し実行

> トランザクション処理

> プラグインのサンプル

22

#ccc_cd4 / #embulk

Embulkアーキテクチャ概要

#ccc_cd4 / #embulk

InputPlugin OutputPlugin

Executor pluginFilter plugin

Filter pluginFilter plugins

records

Threads, MapReduce

records

convert, …

input, … output.

24

records

config

#ccc_cd4 / #embulk

InputPlugin

FileInput plugin

OutputPluginDecoder plugin

Parser plugin

HDFS, S3,Riak CS, …

gzip, bzip2,aes, …

CSV, JSON,pcap, …

buffer

buffer

Filter pluginFilter plugin

Filter plugins

records

records

Executor plugin

25

records

config

#ccc_cd4 / #embulk

InputPlugin

FileInput plugin

OutputPlugin

FileOutput plugin

Encoder plugin

Formatter plugin

Decoder plugin

Parser plugin

HDFS, S3,Riak CS, …

gzip, bzip2,aes, …

CSV, JSON,pcap, …

buffer

bufferbuffer

buffer

Filter pluginFilter plugin

Filter plugins

recordsrecords

Executor plugin

26

records

config

#ccc_cd4 / #embulk

Embulkアーキテクチャ概要

4種のプラグインとそれを組み上げるフレームワーク

1. Executor: 実行

2. Input: バルクデータのレコード群を取り込み

FileInput, Decoder, Parser: ファイル操作

3. Filter: レコードに対するデータ操作

4. Output: バルクデータのレコード群を出力

FileOutput, Encoder, Formatter: ファイル操作

#ccc_cd4 / #embulk

InputPlugin OutputPlugin

Executor pluginFilter plugin

Filter pluginFilter plugins

28

records records

records

config diff

#ccc_cd4 / #embulk

InputPlugin OutputPlugin

Executor pluginFilter plugin

Filter pluginFilter plugins

29

task

schema

report

task

schema

reportrecords records

task

schemarecords

resume state

config

#ccc_cd4 / #embulk

Embulkの実装技術

> Java 7ネイティブ

> Guiceによるコンポーネント間の接続

> ServiceLoaderによる拡張

> Jacksonによるモデルクラス

> Immutableなモデルクラス

> Nettyバッファアロケータ

> sun.misc.Unsafeによるバッファコピー回避

30

#ccc_cd4 / #embulk

Java 7ネイティブ

try-with-resources ファイル操作:Files & Paths API

※Date and TimeはJRubyの実装を利用

try (SetCurrentThreadName dontCare = new SetCurrentThreadName(“transaction”)){ return doRun(config);}

Path basePath = Paths.get(“.”).normalize();Path file = basePath.resolve(“relative.csv”);

#ccc_cd4 / #embulk

Guiceによるコンポーネント間の接続

32

public class EmbulkService{ protected final Injector injector;

public EmbulkService(ConfigSource systemConfig) { ImmutableList.Builder<Module> modules = ImmutableList.builder(); modules.add(new SystemConfigModule(systemConfig)); modules.add(new ExecModule()); modules.add(new ExtensionServiceLoaderModule(systemConfig)); modules.add(new BuiltinPluginSourceModule()); modules.add(new JRubyScriptingModule(systemConfig)); injector = Guice.createInjector(modules.build()); }

public Injector getInjector() { return injector; }}

#ccc_cd4 / #embulk

Guiceによるコンポーネント間の接続

33

public class EmbulkService{ protected final Injector injector;

public EmbulkService(ConfigSource systemConfig) { ImmutableList.Builder<Module> modules = ImmutableList.builder(); modules.add(new SystemConfigModule(systemConfig)); modules.add(new ExecModule()); modules.add(new ExtensionServiceLoaderModule(systemConfig)); modules.add(new BuiltinPluginSourceModule()); modules.add(new JRubyScriptingModule(systemConfig)); injector = Guice.createInjector(modules.build()); }

public Injector getInjector() { return injector; }}

public class ExecModule implements Module{ @Override public void configure(Binder binder) { ... binder.bind(LocalThreadExecutor.class).in(Scopes.SINGLETON); registerPluginTo(binder, ExecutorPlugin.class, "local", LocalExecutorPlugin.class);

#ccc_cd4 / #embulk

Guiceによるコンポーネント間の接続

34

public class EmbulkService{ protected final Injector injector;

public EmbulkService(ConfigSource systemConfig) { ImmutableList.Builder<Module> modules = ImmutableList.builder(); modules.add(new SystemConfigModule(systemConfig)); modules.add(new ExecModule()); modules.add(new ExtensionServiceLoaderModule(systemConfig)); modules.add(new BuiltinPluginSourceModule()); modules.add(new JRubyScriptingModule(systemConfig)); injector = Guice.createInjector(modules.build()); }

public Injector getInjector() { return injector; }}

public class ExecModule implements Module{ @Override public void configure(Binder binder) { ... binder.bind(LocalThreadExecutor.class).in(Scopes.SINGLETON); registerPluginTo(binder, ExecutorPlugin.class, "local", LocalExecutorPlugin.class);

public class LocalExecutorPlugin implements ExecutorPlugin{ private final ExecutorService executor;

@Inject public LocalExecutorPlugin(LocalThreadExecutor executor) { this.executor = executor.getExecutorService(); ... (InjectedPluginSource) public T newPlugin(Injector injector){ return (T) new FileInputRunner((FileInputPlugin) injector.getInstance(impl));

#ccc_cd4 / #embulk

Guiceによるコンポーネント間の接続

XMLでなくすべてJavaで書く系のDI

Annotationによる宣言的なDI、injectorによる動的なDIの組み合わせ > 動的なモジュール差し替えがし易い

#ccc_cd4 / #embulk

ServiceLoaderによる拡張

public class ExtensionServiceLoaderModule implements Module{ private final ConfigSource systemConfig;

public ExtensionServiceLoaderModule(ConfigSource systemConfig) { this.systemConfig = systemConfig; }

@Override public void configure(Binder binder) { ServiceLoader<Extension> serviceLoader = ServiceLoader.load(Extension.class, classLoader); for (Extension extension : serviceLoader) { for (Module module : extension.getModules(systemConfig)) { module.configure(binder); } } }}

#ccc_cd4 / #embulk

ServiceLoaderによる拡張

jarをclasspathに入れるだけでモジュール追加/差し替え

簡単でClassLoaderいじるよりは安全

標準Plugin群の登録に利用

※Pluginの読み込みはClassLoader

#ccc_cd4 / #embulk

Jacksonによるモデルクラス(task)

public class CsvParserPlugin implements ParserPlugin{ public interface PluginTask extends Task, LineDecoder.DecoderTask, TimestampParser.ParserTask { @Config("columns") public SchemaConfig getSchemaConfig();

@Config("header_line") @ConfigDefault("null") public Optional<Boolean> getHeaderLine();

@Config("skip_header_lines") @ConfigDefault("0") public int getSkipHeaderLines(); public void setSkipHeaderLines(int n);

@Config("delimiter") @ConfigDefault("\",\"") public char getDelimiterChar();

#ccc_cd4 / #embulk

InputPlugin OutputPlugin

Executor pluginFilter plugin

Filter pluginFilter plugins

39

task

schema

report

task

schema

reportrecords records

task

schemarecords

config

config diffresume state

#ccc_cd4 / #embulk

Jacksonによるモデルクラス(schema)

public class ColumnConfig{ private final String name; private final Type type;

@JsonCreator public ColumnConfig( @JsonProperty("name") String name, @JsonProperty("type") Type type) { this.name = name; this.type = type; }

@JsonProperty("name") public String getName() { return name; }

@JsonProperty("type") public Type getType() { return type; }}

#ccc_cd4 / #embulk

Jacksonによるモデルクラス

デ/シリアライズが重要

> 並列・分散実行のため

> Ruby <-> Javaのやりとりのため

IDL生成でなくすべてJavaで書く系のモデル

#ccc_cd4 / #embulk

Immutableなモデルクラス

ソースコード中のfinalなメンバー変数の割合

> Presto:83%(4714変数)

> Embulk:72%(255変数)

> Cassandra:59%(2348変数)

> Elasticsearch:51%(6871変数)

> Nashorn (OpenJDK 8):43%(852変数)

> JRuby:40%(3154変数)

> Hadoop:31%(9280変数)

> Hive:23%(4600変数)

#ccc_cd4 / #embulk

Nettyバッファアロケータ

レコード群のためのメモリをすべて自前管理

> OutOfMemoryが起きる前に検出

> GCコスト削減

複数のバルクロードセッションをサーバプロセス内で同時実行可能に

#ccc_cd4 / #embulk

Nettyバッファアロケータ

レコード群のためのメモリをすべて自前管理

> OutOfMemoryが起きる前に検出

> GCコスト削減

複数のバルクロードセッションをサーバプロセス内で同時実行可能に

public Buffer allocate(int minimumCapacity){ int size = MINIMUM_BUFFER_SIZE; while (size < minimumCapacity) { size *= 2; } return new NettyByteBufBuffer(nettyBuffer.buffer(size));}

#ccc_cd4 / #embulk

Unsafe

airlift/slice - sun.misc.Unsafe APIのwrapper

> バイト列の直接操作(デ/シリアライズ)

> コピー削減

参考: http://frsyuki.hatenablog.com/entry/

2014/03/12/155231

#ccc_cd4 / #embulk

Unsafe

airlift/slice - sun.misc.Unsafe APIのwrapper

> バイト列の直接操作(デ/シリアライズ)

> コピー削減

参考: http://frsyuki.hatenablog.com/entry/

2014/03/12/155231

public void addRecord(){ // record header bufferSlice.setInt(position, nextVariableLengthDataOffset); bufferSlice.setBytes(position + 4, nullBitSet); count++;

this.position += nextVariableLengthDataOffset; this.nextVariableLengthDataOffset = fixedRecordSize; Arrays.fill(nullBitSet, (byte) 0);

// flush if next record will not fit in this buffer if (buffer.capacity() < position + nextVariableLengthDataOffset + stringReferenceSize) { flush(); }}

47ユーザー

転送管理コンソールから実行

トレジャークラウドストレージ

EmbulkWorker

管理コンソールからアクセス

S3のインポートから,エクスポートまでを完全自動化

#ccc_cd4 / #embulk

Contributing to the Embulk project

> Pull-requests & issues on Github > Posting blogs

> “使ってみた” > “コードを読んでみた” > “ここがイケてる / イケてない”

> Talking on Twitter with a word “embulk" > Writing & releasing plugins > Windows support > Integration to other software

> ETL tools, Fluentd, Hadoop, Presto, …

48

1. Distributed Systems Engineer

2. Integration Engineer

3. Software Engineer, MPP DBMS

4. Sales Engineer

5. Technical Support Engineer

(日本,東京,丸の内)

https://jobs.lever.co/treasure-data

We’re hiring!

ANALYTICS INFRASTRUCTURE. SIMPLIFIED IN THE CLOUD.

50

#ccc_cd4 / #embulk

FluentdとEmbulk

52

This?

53

Or this?

M x N → M + N

Nagios

MongoDB

Hadoop

Alerting

Amazon S3

Analysis

Archiving

MySQL

Apache

Frontend

Access logs

syslogd

App logs

System logs

Backend

Databasesbuffer/filter/route

#ccc_cd4 / #embulk

FluentdとEmbulk

ストリーミングデータコレクターvs バルクデータローダー

ストリーミングデータか、転送単位がはっきりしているバルクデータか

任意データ vs データバリデーション・正規化

即時 vs トランザクション性

#ccc_cd4 / #embulk

Embulkの実行

インストール

guess preview 繰り返し実行

56

# install $ wget https://bintray.com/artifact/download/

embulk/maven/embulk-0.2.0.jar -o embulk.jar $ chmod 755 embulk.jar

Installing embulk

Bintray releases

Embulk is released on Bintray

wget embulk.jar

# install $ wget https://bintray.com/artifact/download/

embulk/maven/embulk-0.2.0.jar -o embulk.jar $ chmod 755 embulk.jar

# guess $ vi partial-config.yml $ ./embulk guess partial-config.yml

-o config.yml

Guess format & schema in: type: file paths: [data/examples/] out: type: example

in: type: file paths: [data/examples/] decoders: - {type: gzip} parser: charset: UTF-8 newline: CRLF type: csv delimiter: ',' quote: '"' header_line: true columns: - name: time type: timestamp format: '%Y-%m-%d %H:%M:%S' - name: account type: long - name: purchase type: timestamp format: '%Y%m%d' - name: comment type: string out: type: example

guess

by guess plugins

# install $ wget https://bintray.com/artifact/download/

embulk/maven/embulk-0.2.0.jar -o embulk.jar $ chmod 755 embulk.jar

# guess $ vi partial-config.yml $ ./embulk guess partial-config.yml

-o config.yml

# preview $ ./embulk preview config.yml $ vi config.yml # if necessary

+--------------------------------------+---------------+--------------------+ | time:timestamp | uid:long | word:string | +--------------------------------------+---------------+--------------------+ | 2015-01-27 19:23:49 UTC | 32,864 | embulk | | 2015-01-27 19:01:23 UTC | 14,824 | jruby | | 2015-01-28 02:20:02 UTC | 27,559 | plugin | | 2015-01-29 11:54:36 UTC | 11,270 | fluentd | +--------------------------------------+---------------+--------------------+

Preview & fix config

# install $ wget https://bintray.com/artifact/download/

embulk/maven/embulk-0.2.0.jar -o embulk.jar $ chmod 755 embulk.jar

# guess $ vi partial-config.yml $ ./embulk guess partial-config.yml

-o config.yml

# preview $ ./embulk preview config.yml $ vi config.yml # if necessary

# run $ ./embulk run config.yml -o config.yml

in: type: file paths: [data/examples/] decoders: - {type: gzip} parser: charset: UTF-8 newline: CRLF type: csv delimiter: ',' quote: '"' header_line: true columns: - name: time type: timestamp format: '%Y-%m-%d %H:%M:%S' - name: account type: long - name: purchase type: timestamp format: '%Y%m%d' - name: comment type: string last_paths: [data/examples/sample_001.csv.gz] out: type: example

Deterministic run

in: type: file paths: [data/examples/] decoders: - {type: gzip} parser: charset: UTF-8 newline: CRLF type: csv delimiter: ',' quote: '"' header_line: true columns: - name: time type: timestamp format: '%Y-%m-%d %H:%M:%S' - name: account type: long - name: purchase type: timestamp format: '%Y%m%d' - name: comment type: string last_paths: [data/examples/sample_002.csv.gz] out: type: example

Repeat

# install $ wget https://bintray.com/artifact/download/

embulk/maven/embulk-0.2.0.jar -o embulk.jar $ chmod 755 embulk.jar

# guess $ vi partial-config.yml $ ./embulk guess partial-config.yml

-o config.yml

# preview $ ./embulk preview config.yml $ vi config.yml # if necessary

# run $ ./embulk run config.yml -o config.yml

# repeat $ ./embulk run config.yml -o config.yml $ ./embulk run config.yml -o config.yml

#ccc_cd4 / #embulk

Writing Embulk plugins

62

InputPlugin

module Embulk class InputExample < InputPlugin Plugin.register_input('example', self)

def self.transaction(config, &control) # read config task = { 'message' => config.param('message', :string, default: nil) } threads = config.param('threads', :int, default: 2)

columns = [ Column.new(0, 'col0', :long), Column.new(1, 'col1', :double), Column.new(2, 'col2', :string), ]

# BEGIN here

commit_reports = yield(task, columns, threads)

# COMMIT here puts "Example input finished"

return {} end

def run(task, schema, index, page_builder) puts "Example input thread #{@index}…"

10.times do |i| @page_builder.add([i, 10.0, "example"]) end @page_builder.finish

commit_report = { } return commit_report end end end

OutputPlugin

module Embulk class OutputExample < OutputPlugin Plugin.register_output('example', self)

def self.transaction( config, schema, processor_count, &control) # read config task = { 'message' => config.param('message', :string, default: "record") }

puts "Example output started." commit_reports = yield(task) puts "Example output finished. Commit reports = #{commit_reports.to_json}"

return {} end

def initialize(task, schema, index) puts "Example output thread #{index}..." super @message = task.prop('message', :string) @records = 0 end

def add(page) page.each do |record| hash = Hash[schema.names.zip(record)] puts "#{@message}: #{hash.to_json}" @records += 1 end end

def finish end

def abort end

def commit commit_report = { "records" => @records } return commit_report end end end

GuessPlugin

# guess_gzip.rb module Embulk

class GzipGuess < GuessPlugin Plugin.register_guess('gzip', self)

GZIP_HEADER = "\x1f\x8b".force_encoding('ASCII-8BIT').freeze

def guess(config, sample_buffer) if sample_buffer[0,2] == GZIP_HEADER return {"decoders" => [{"type" => "gzip"}]} end return {} end end

end

# guess_ module Embulk

class GuessNewline < TextGuessPlugin Plugin.register_guess('newline', self)

def guess_text(config, sample_text) cr_count = sample_text.count("\r") lf_count = sample_text.count("\n") crlf_count = sample_text.scan(/\r\n/).length if crlf_count > cr_count / 2 && crlf_count > lf_count / 2 return {"parser" => {"newline" => "CRLF"}} elsif cr_count > lf_count / 2 return {"parser" => {"newline" => "CR"}} else return {"parser" => {"newline" => "LF"}} end end end

end

#ccc_cd4 / #embulk

Releasing to RubyGems

Examples > embulk-plugin-postgres-json.gem

> https://github.com/frsyuki/embulk-plugin-postgres-json > embulk-plugin-redis.gem

> https://github.com/komamitsu/embulk-plugin-redis > embulk-plugin-input-sfdc-event-log-files.gem

> https://github.com/nahi/embulk-plugin-input-sfdc-event-log-files

#ccc_cd4 / #embulk

plugin bundle

> embulk bundle <dir> > Gemfile

Attention!

Presto Presto Presto Presto

Presto Presto Presto Presto

Presto Presto Presto Presto

Presto Presto Presto Presto

Presto Presto Presto Presto

68

Attention!

Hive Hive Hive Hive

Hive Hive Hive Hive

Hive Hive Hive Hive

Hive Hive Hive Hive

69

Hive Hive Hive Hive

Hive Hive HiveHive

PrestoPrestogres

hba.conf

PostgreSQL

70

Recommended