embulk 20150411
TRANSCRIPT
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