What have you found for these years?

2010-05-19

download models from heroku

updated 2010-05-20 00:20
喔對了,還有丟給 heroku console 的資料,
狀況也很奇怪。我一定要把 exit 放在另一段傳送,
他才真的會把 connection close 掉。
我原本都是放在一起,但怎麼試都怪怪的。
不能像 irb 一樣有斷行也是一件很不方便的事...
好在都寫成程式的話一個 gsub 就解決了。

-

heroku 本身有提供一個 heroku db:pull,
先不提之前對於 bigint 的 datatype 有問題,
現在就算把這修好了,也還是不能跑在 ruby 1.9 下,
一時看不出來問題是什麼,看起來就是整個 process 不會動...
永遠停在 0%

但就算換成 1.8.7 去跑,結果還是讓我很不滿意。
第一點是速度實在是有夠慢的,可能要跑好幾個小時才能抓下一個 table.
第二點是中斷後,要再接上去好像有點問題。這問題跟第一點有關,
如果速度夠快的話,其實就算不能 resume 也不是什麼大不了的事,
重跑就好,根本沒差。

此外要使用 heroku db:pull, 需要安裝 taps
> gem dep -r taps
Gem taps-0.3.6
json_pure (>= 1.2.0, < 1.5.0, runtime)
rack (>= 1.0.1, runtime)
rest-client (~> 1.4.0, runtime)
sequel (~> 3.10.0, runtime)
sinatra (~> 1.0.0, runtime)
sqlite3-ruby (~> 1.2.0, runtime)

這個 dependency 也太誇張了吧?
我真不懂,明明只是要 pg_dump, 搞這麼麻煩幹嘛啊 @_@
需要 sinatra, sequel, sqlite3-ruby 這三個也非常奇怪。

同時他的 heroku db:pull 好像還一定要把 local database
準備好,讓他下載 table 資料下來後同時灌進去 database.
我真的覺得給個 *.sql 檔讓我 restore 會快很多....
尤其我可能根本就不想 restore 進去,畢竟不一定是希望把
資料倒回開發環境吧?開發環境裡可能還有別的東西,不想混在一起。

-

於是我能想到最簡單的方法,就是用 heroku console 連上去,
接著把資料用 tcp socket 直接丟到某台 server 上面。
我之前都是首動作這件事,用 eventmachine 開一個 server,
receive_data 就把資料倒到某一個 file 裡面,
最後再用 nginx 跑一個 http server 去 serve 那個 file,
我再 wget 回到我的電腦上。

...超麻煩 @@

於是決定寫個小程式一次解決這件事。上面還多個 nginx 是因為
假設開發機器沒有 external ip, 所以多個這樣的步驟。
現在決定直接假設至少可以用 ssh tunnel (抱歉,我不知道確切怎麼稱呼)
因此就可以去掉 nginx 那段,由 eventmachine 寫入檔案即可。

那麼問題就只剩下:
1) 去 heroku console 撈資料
2) 跑 eventmachine 接資料

這兩件事當然都能由 eventmachine 一次解決,
所有跟 external resource 有關的東西,都可以透過
eventmachine 去做。除了 watch file 以外,
也有 keyboard event 可以用.. 雖然我沒試過。

所以 (1) 就是 EM.popen('heroku console', MyConnection)
然後 (2) 就是 EM.start_server

EM.run{
EM.start_server(LocalHost, LocalPort, DownloadContact)
EM.popen(Command, UploadContact)
}

這邊我本來是寫成讓 heroku console 直接就把所有資料一次傳回來,
很不幸的是,他的 console 實在是太不穩太容易爆炸了。而且看不太出來是什麼錯。
因此我只好寫成一次只跑固定的量,然後在 popen 結束之後,
重新再跑一次 popen. 當然這樣就會變得慢很多,因為等同於要不斷
啟動關閉 ruby... 把 heroku console 跑起來並不是一件很快的事。 :(

結果單單這樣寫還是會爆炸!! heroku console 會噴記憶體不足的錯誤 orz
我一開始還一直以為是我電腦的記憶體不足,納悶了半天,想說哪需要很多記憶體 @@
只好把 popen 再改成 sleep 一段時間:
EM.add_timer(seconds){ EM.popen(Command, UploadContact) }

那個 seconds 是根據一次抓取的量算出來的,我一次抓 100k 筆資料,
估計大概要等 15 秒以上才比較不會碰到記憶體不足的錯誤...

同時考慮到透過網路傳輸的資料理所當然要壓縮一下,就用了 gzip 壓縮。
雖然我到現在還是不太清楚 gzip 跟 deflate 之間到底有什麼關係..
100k 筆資料沒壓大概 19M, 壓了之後約是 5M. 資料格式是 marshal @@
為了方便起見,目的是在本地端營造跟 heroku 上一樣的環境,
也避免接觸到 database, 乾脆直接 marshal 起來...

測試結果看起來是沒問題啦 @@"
下載 1m 筆資料... 一分鐘內可能有點勉強,但 5~10 分鐘應該沒問題。
相較 taps 感覺實在好太多了啊.. dependency 也只有 eventmachine 而已。
當然要用 gzip 會需要 zlib, 但我想正常來說這一定是有裝的,
因為 rubygems 本身就要 zlib 了,沒裝 zlib 連 rubygems 都不能用。

然後在 local 端跑這些資料超快的啦 ~ XD
在 heroku console 上跑很多 i/o 都有問題... 執行效能也很差。
他這 console 在測試和觀察東西時很方便,要跑一些程式就很痛苦...。
如果以後這邊能做好一點,這隻程式可能就用不到了。全文如下:
require 'rubygems' if RUBY_VERSION < '1.9.1'
require 'eventmachine'
require 'zlib'

RemoteHost = '0.1.2.3'
RemotePort = 41829
LocalHost = '127.0.0.1'
LocalPort = 8080

Offset = 0
Limit = 100000
Command = "heroku console #{ARGV.join(' ')}"

WaitFactor = 6000.0

module G
class << self
attr_accessor :done
attr_writer :offset, :conn
def offset; @offset ||= Offset; end
def conn ; @conn ||= 0; end
def stop
puts("INFO: done!")
EM.stop
end
end
end

module DownloadContact
def post_init
@filename = "contact-#{sprintf('%02d', G.offset/Limit)}.gz"
@file = File.open(@filename, 'wb')
G.offset += Limit
G.conn += 1
end

def unbind
@file.close
puts("INFO: #{@filename} saved.")
G.stop if G.done
G.conn -= 1
end

def receive_data data
@file << data
end
end

module UploadContact
def post_init
send_data <<-RUBY.squeeze(' ').gsub(/\n/, ';') + "\n"
require 'socket'
require 'zlib'
Contact.find_by_sql('SET statement_timeout TO 0')
sock = TCPSocket.new('#{RemoteHost}', '#{RemotePort}')
gzip = Zlib::GzipWriter.new(sock)
contacts = Contact.all(:offset => #{G.offset}, :limit => #{Limit})
gzip << Marshal.dump(contacts)
gzip.close
contacts = nil
GC.start
Contact.count
RUBY
send_data("exit\n")
end

def receive_data data
(@buffer ||= []) << data
puts "DEBUG: heroku: #{data}" if $DEBUG
end

def unbind
count = @buffer.join.match(/\=> (\d+)/)[1].to_i
puts "DEBUG: count: #{count}" if $DEBUG
if G.offset < count
puts("INFO: #{count - G.offset} remaining.")
seconds = Limit / WaitFactor
puts("INFO: Wait #{'%.2f' % seconds} for heroku.")
EM.add_timer(seconds){ EM.popen(Command, UploadContact) }
else
G.done = true
G.stop if G.conn == 0
end
end
end

EM.run{
EM.start_server(LocalHost, LocalPort, DownloadContact)
EM.popen(Command, UploadContact)
}

0 retries:

Post a Comment

All texts are licensed under CC Attribution 3.0