Concurrency Model for
             huge MySQL data
                Mu-Fan Teng(@ryudoawaru)
                 @ RubyConf Taiwan 2012




12年12月8⽇日星期六
緣起

               Background




12年12月8⽇日星期六
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.




12年12月8⽇日星期六
The purpose




12年12月8⽇日星期六
Transcoding to UTF8



12年12月8⽇日星期六
Try



12年12月8⽇日星期六
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.


12年12月8⽇日星期六
Failed!



12年12月8⽇日星期六
Cause
               • Too big size for most text editor.
               • Many mis-encoding text.




12年12月8⽇日星期六
Let’s reinvent the
                     wheel!


12年12月8⽇日星期六
The new work flow
               • Connect DB
               • Transcode
               • Output db rows to SQL insert statement.
               • Write SQL file



12年12月8⽇日星期六
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)}




12年12月8⽇日星期六
Thanks for Ruby 1.9’s
                Awesome Encoding
               class which supports
                     Big5-UAO.


12年12月8⽇日星期六
Reduces almost 80% of
                 encoding problem.


12年12月8⽇日星期六
But the file size is too
                   big to wait for
                    transcoding!


12年12月8⽇日星期六
So I have to find the
               concurrency model to
                   make it faster.


12年12月8⽇日星期六
Experiment Target
               • Test the difference of performance
                 between thread and fork model.




12年12月8⽇日星期六
H&W Platform
               • 4 Cores Core2Quad CPU@2.5G
               • 8GB RAM
               • 1*SSD
               • MacOS 10.8
               • MRI 1.9.3p194


12年12月8⽇日星期六
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


12年12月8⽇日星期六
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




12年12月8⽇日星期六
Result of 100k*4 rows




12年12月8⽇日星期六
Thread




12年12月8⽇日星期六
Fork




12年12月8⽇日星期六
Fork




12年12月8⽇日星期六
Circumstance
               • Thread
                ‣ CPU utilization rate between 105 and
                   125 percent.
               • Fork
                ‣ The rate changes frequently between
                   processes.



12年12月8⽇日星期六
GVL still effects



12年12月8⽇日星期六
Try again



12年12月8⽇日星期六
Decompose the steps
                to find how to skip
                       GVL.


12年12月8⽇日星期六
Experiment No.2



12年12月8⽇日星期六
Minify the process to
                 query DB only.


12年12月8⽇日星期六
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




12年12月8⽇日星期六
Result of 100k*4 rows




12年12月8⽇日星期六
It seems the Mysql2
               Gem can skip GVL.


12年12月8⽇日星期六
Experiment NO.3



12年12月8⽇日星期六
Limit the experiment
               to I/O operation only.


12年12月8⽇日星期六
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




12年12月8⽇日星期六
12年12月8⽇日星期六
Result of 100k*4 rows




12年12月8⽇日星期六
Change I/O to different
                    files.


12年12月8⽇日星期六
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




12年12月8⽇日星期六
Result reversed




12年12月8⽇日星期六
Implement the same
                change to the first
                   experiment.


12年12月8⽇日星期六
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




12年12月8⽇日星期六
Dose not effect any




12年12月8⽇日星期六
Conclusion
                        Thread       fork      normal

      MySQL2-read        Fast        Fast        x

      Transcoding &
                         Slow      Very fast     x
         iteration
       Write to the
                       Very slow     Slow       Fast
        same I/O
       Write to the
                         Fast        Slow       Fast
       different I/O


12年12月8⽇日星期六
There is no effective
               and 「all-around」
               concurrency model.


12年12月8⽇日星期六
The small I/O can’t
                 release GVL.


12年12月8⽇日星期六
Kosaki-san’s slide




12年12月8⽇日星期六
Matz is not a threading guy




12年12月8⽇日星期六
End




12年12月8⽇日星期六

Concurrency model for mysql data processing@rubyconf.tw 2012

  • 1.
    Concurrency Model for huge MySQL data Mu-Fan Teng(@ryudoawaru) @ RubyConf Taiwan 2012 12年12月8⽇日星期六
  • 2.
    緣起 Background 12年12月8⽇日星期六
  • 3.
    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. 12年12月8⽇日星期六
  • 4.
  • 5.
  • 6.
  • 7.
    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. 12年12月8⽇日星期六
  • 8.
  • 9.
    Cause • Too big size for most text editor. • Many mis-encoding text. 12年12月8⽇日星期六
  • 10.
    Let’s reinvent the wheel! 12年12月8⽇日星期六
  • 11.
    The new workflow • Connect DB • Transcode • Output db rows to SQL insert statement. • Write SQL file 12年12月8⽇日星期六
  • 12.
    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)} 12年12月8⽇日星期六
  • 13.
    Thanks for Ruby1.9’s Awesome Encoding class which supports Big5-UAO. 12年12月8⽇日星期六
  • 14.
    Reduces almost 80%of encoding problem. 12年12月8⽇日星期六
  • 15.
    But the filesize is too big to wait for transcoding! 12年12月8⽇日星期六
  • 16.
    So I haveto find the concurrency model to make it faster. 12年12月8⽇日星期六
  • 17.
    Experiment Target • Test the difference of performance between thread and fork model. 12年12月8⽇日星期六
  • 18.
    H&W Platform • 4 Cores Core2Quad CPU@2.5G • 8GB RAM • 1*SSD • MacOS 10.8 • MRI 1.9.3p194 12年12月8⽇日星期六
  • 19.
    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 12年12月8⽇日星期六
  • 20.
    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 12年12月8⽇日星期六
  • 21.
    Result of 100k*4rows 12年12月8⽇日星期六
  • 22.
  • 23.
  • 24.
  • 25.
    Circumstance • Thread ‣ CPU utilization rate between 105 and 125 percent. • Fork ‣ The rate changes frequently between processes. 12年12月8⽇日星期六
  • 26.
  • 27.
  • 28.
    Decompose the steps to find how to skip GVL. 12年12月8⽇日星期六
  • 29.
  • 30.
    Minify the processto query DB only. 12年12月8⽇日星期六
  • 31.
    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 12年12月8⽇日星期六
  • 32.
    Result of 100k*4rows 12年12月8⽇日星期六
  • 33.
    It seems theMysql2 Gem can skip GVL. 12年12月8⽇日星期六
  • 34.
  • 35.
    Limit the experiment to I/O operation only. 12年12月8⽇日星期六
  • 36.
    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 12年12月8⽇日星期六
  • 37.
  • 38.
    Result of 100k*4rows 12年12月8⽇日星期六
  • 39.
    Change I/O todifferent files. 12年12月8⽇日星期六
  • 40.
    procs = sql_raws.mapdo |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 12年12月8⽇日星期六
  • 41.
  • 42.
    Implement the same change to the first experiment. 12年12月8⽇日星期六
  • 43.
    procs = sqls.mapdo |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 12年12月8⽇日星期六
  • 44.
    Dose not effectany 12年12月8⽇日星期六
  • 45.
    Conclusion Thread fork normal MySQL2-read Fast Fast x Transcoding & Slow Very fast x iteration Write to the Very slow Slow Fast same I/O Write to the Fast Slow Fast different I/O 12年12月8⽇日星期六
  • 46.
    There is noeffective and 「all-around」 concurrency model. 12年12月8⽇日星期六
  • 47.
    The small I/Ocan’t release GVL. 12年12月8⽇日星期六
  • 48.
  • 49.
    Matz is nota threading guy 12年12月8⽇日星期六
  • 50.