Slide 1

Slide 1 text

Concurrency Model for huge MySQL data processing Mu-Fan Teng(@ryudoawaru) @ RubyConf Taiwan 2012 13年1月9⽇日星期三

Slide 2

Slide 2 text

緣起 Background 13年1月9⽇日星期三

Slide 3

Slide 3 text

Legacy environment • A Mysql Database with 2.3gb data with Big5 charset and ISO-8859-1 encoding. • The biggest table in DB is 1.5gb. 13年1月9⽇日星期三

Slide 4

Slide 4 text

The purpose 13年1月9⽇日星期三

Slide 5

Slide 5 text

Transcoding to UTF8 13年1月9⽇日星期三

Slide 6

Slide 6 text

Try 13年1月9⽇日星期三

Slide 7

Slide 7 text

Work Flow 1. mysqldump with -default-character- set=latin1 parameter to generate SQL file. 2. Transcoding SQL file with tool like iconv/ bsdconv. 3. Edit transcoded SQL file to avoid 「slash」 problem. 4. Restore SQL file to new DB. 13年1月9⽇日星期三

Slide 8

Slide 8 text

Failed! 13年1月9⽇日星期三

Slide 9

Slide 9 text

Cause • Too big size for most text editor. • Many mis-encoding text. 13年1月9⽇日星期三

Slide 10

Slide 10 text

Let’s reinvent the wheel! 13年1月9⽇日星期三

Slide 11

Slide 11 text

The new work flow • Connect DB • Transcode • Output db rows to SQL insert statement. • Write SQL file 13年1月9⽇日星期三

Slide 12

Slide 12 text

CORES_COUNT = 4 LIMIT = ARGV[0].to_i || 10000 sqls = CORES_COUNT.times.map do |x| sprintf("SELECT * FROM cdb_posts ORDER BY pid LIMIT %d OFFSET %d;", LIMIT, (x * LIMIT)) end class String def to_my_val "'#{Mysql2::Client.escape self.force_encoding(‘Big5- UAO’).encode('UTF-8', :invalid => :replace, :undef => :replace, :replace => '??')}'" end end procs = sqls.map do |sql| Proc.new do |out| Mysql2::Client.new(database: DBNAME, reconnect: true, encoding: 'latin1').query(sql).each(as: :array) do |row| out.print "INSERT INTO `cdb_posts` VALUES (#{row.map(&:to_my_val).join(',')});\n" end end end procs.each{|p| p.call(OUT)} 13年1月9⽇日星期三

Slide 13

Slide 13 text

Thanks for Ruby 1.9’s Awesome Encoding class which supports Big5-UAO. 13年1月9⽇日星期三

Slide 14

Slide 14 text

Reduces almost 80% of encoding problem. 13年1月9⽇日星期三

Slide 15

Slide 15 text

But the file size is too big to wait for transcoding! 13年1月9⽇日星期三

Slide 16

Slide 16 text

So I have to find the concurrency model to make it faster. 13年1月9⽇日星期三

Slide 17

Slide 17 text

Experiment Target • Test the difference of performance between thread and fork model. 13年1月9⽇日星期三

Slide 18

Slide 18 text

H&W Platform • 4 Cores Core2Quad [email protected] • 8GB RAM • 1*SSD • MacOS 10.8 • MRI 1.9.3p194 13年1月9⽇日星期三

Slide 19

Slide 19 text

DBNAME = 'wwwfsc' CORES_COUNT = 4 ForceEncoding = 'Big5-UAO' LIMIT = ARGV[0].to_i || 10000 OUT = '/dev/null' sqls = CORES_COUNT.times.map do |x| sprintf("SELECT * FROM cdb_posts ORDER BY pid LIMIT %d OFFSET %d;", LIMIT, (x * LIMIT)) end class String def to_my_val "'#{Mysql2::Client.escape self.force_encoding(ForceEncoding).encode('UTF-8', :invalid => :replace, :undef => :replace, :replace => '??')}'" end end procs = sqls.map do |sql| Proc.new do |out| open(out,'w') do |io| Mysql2::Client.new(database: DBNAME, reconnect: true, encoding: 'latin1').query(sql).each(as: :array) do |row| io.print "INSERT INTO `cdb_posts` VALUES (#{row.map(&:to_my_val).join(',')});\n" end end end end 13年1月9⽇日星期三

Slide 20

Slide 20 text

Benchmark.bm(15) do |x| x.report("Thread"){procs.map{|p| Thread.new{p.call(OUT)} }.each(&:join)} x.report("Fork"){procs.each{|p| fork{p.call(OUT)} }; Process.waitall} x.report("Normal"){procs.each{|p| p.call(OUT)}} end 13年1月9⽇日星期三

Slide 21

Slide 21 text

Result of 100k*4 rows 13年1月9⽇日星期三

Slide 22

Slide 22 text

Thread 13年1月9⽇日星期三

Slide 23

Slide 23 text

Fork 13年1月9⽇日星期三

Slide 24

Slide 24 text

Fork 13年1月9⽇日星期三

Slide 25

Slide 25 text

Circumstance • Thread ‣ CPU utilization rate between 105 and 125 percent. • Fork ‣ The rate changes frequently between processes. 13年1月9⽇日星期三

Slide 26

Slide 26 text

GVL still effects 13年1月9⽇日星期三

Slide 27

Slide 27 text

Try again 13年1月9⽇日星期三

Slide 28

Slide 28 text

Decompose the steps to find how to skip GVL. 13年1月9⽇日星期三

Slide 29

Slide 29 text

Experiment No.2 13年1月9⽇日星期三

Slide 30

Slide 30 text

Minify the process to query DB only. 13年1月9⽇日星期三

Slide 31

Slide 31 text

DBNAME = 'wwwfsc' CORES_COUNT = 4 limit = ARGV[0].to_i || 10000 sqls = CORES_COUNT.times.map do |x| sprintf("SELECT * FROM cdb_posts ORDER BY pid LIMIT %d OFFSET %d;", limit, (x * limit)) end procs = CORES_COUNT.times.map do |x| Proc.new do client = Mysql2::Client.new(database: DBNAME, reconnect: true) result = client.query(sqls[x]) end end Benchmark.bmbm(15) do |x| x.report("Thread"){procs.map{|p| Thread.new{p.call} }.each(&:join)} x.report("Fork"){procs.each{|p| fork{p.call} }; Process.waitall} x.report("Normal"){procs.each(&:call)} end 13年1月9⽇日星期三

Slide 32

Slide 32 text

Result of 100k*4 rows 13年1月9⽇日星期三

Slide 33

Slide 33 text

It seems the Mysql2 Gem can skip GVL. 13年1月9⽇日星期三

Slide 34

Slide 34 text

Experiment NO.3 13年1月9⽇日星期三

Slide 35

Slide 35 text

Limit the experiment to I/O operation only. 13年1月9⽇日星期三

Slide 36

Slide 36 text

client = Mysql2::Client.new(database: DBNAME, reconnect: true, encoding: 'latin1') sql_raws = sqls.map do |sql| arr = [] client.query(sql).each(as: :array) do |row| arr << "(#{row.map(&:to_my_val).join(',')})" end arr end procs = sql_raws.map do |arr| proc do io = open('/dev/null','w') io.write "INSERT INTO `cdb_posts` VALUES " io.write arr.join(',') io.write "\n" io.close end end Benchmark.bm(15) do |x| x.report("Thread"){procs.map{|p| Thread.new{p.call} }.each(&:join)} x.report("Fork"){procs.each{|p| fork{p.call} }; Process.waitall} x.report("Normal"){procs.each{|p| p.call}} end 13年1月9⽇日星期三

Slide 37

Slide 37 text

13年1月9⽇日星期三

Slide 38

Slide 38 text

Result of 100k*4 rows 13年1月9⽇日星期三

Slide 39

Slide 39 text

Change I/O to different files. 13年1月9⽇日星期三

Slide 40

Slide 40 text

procs = sql_raws.map do |arr| proc do io = Tempfile.new(SecureRandom.uuid)#open('/dev/null','w') puts io.path io.write "INSERT INTO `cdb_posts` VALUES " io.write arr.join(',') io.write "\n" io.close end end 13年1月9⽇日星期三

Slide 41

Slide 41 text

Result reversed 13年1月9⽇日星期三

Slide 42

Slide 42 text

Implement the same change to the first experiment. 13年1月9⽇日星期三

Slide 43

Slide 43 text

procs = sqls.map do |sql| Proc.new do io = Tempfile.new(SecureRandom.uuid) Mysql2::Client.new(database: DBNAME, reconnect: true, encoding: 'latin1').query(sql).each(as: :array) do |row| io.write "INSERT INTO `cdb_posts` VALUES (#{row.map(&:to_my_val).join(',')});\n" end io.close end end 13年1月9⽇日星期三

Slide 44

Slide 44 text

Dose not effect any 13年1月9⽇日星期三

Slide 45

Slide 45 text

Conclusion Thread fork normal MySQL2-read Fast Fast x Transcoding & iteration Slow Very fast x Write to the same I/O Very slow Slow Fast Write to the different I/O Fast Slow Fast 13年1月9⽇日星期三

Slide 46

Slide 46 text

There is no effective and 「all-around」 concurrency model. 13年1月9⽇日星期三

Slide 47

Slide 47 text

The small I/O can’t release GVL. 13年1月9⽇日星期三

Slide 48

Slide 48 text

Kosaki-san’s slide 13年1月9⽇日星期三

Slide 49

Slide 49 text

Matz is not a threading guy 13年1月9⽇日星期三

Slide 50

Slide 50 text

End 13年1月9⽇日星期三