Trong Ruby Magic edition này, chúng ta học về streaming files trong Ruby, cách class IO handle reading files không load hoàn toàn vào memory, và cách nó đọc files per line bằng buffering read bytes. Let's dive right in!
"Slurping" và Streaming Files
Ruby File.read method đọc file và trả về full content.
irb> content = File.read("log/production.log")
=> "I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\/articles\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\nI, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendering articles/index.html.erb within layouts/application\nD, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Article Load (0.3ms) SELECT \"articles\".* FROM \"articles\"\nI, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendered articles/index.html.erb within layouts/application (1.7ms)\nI, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\n"
Internally, này open file, đọc content, close file, trả content as single string. Bằng "slurping" file content at once, nó kept in memory cho đến khi cleaned up bởi Ruby garbage collector.
Ví dụ, muốn uppercase tất cả characters trong file và write sang file khác. Dùng File.read, get content, gọi String#upcase trên string kết quả, pass uppercased string cho File.write.
irb> upcased = File.read("log/production.log").upcase
=> "I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] STARTED GET \"\/ARTICLES\" FOR 127.0.0.1 AT 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] PROCESSING BY ARTICLESCONTROLLER#INDEX AS HTML\nI, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] RENDERING ARTICLES/INDEX.HTML.ERB WITHIN LAYOUTS/APPLICATION\nD, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] ARTICLE LOAD (0.3MS) SELECT \"ARTICLES\".* FROM \"ARTICLES\"\nI, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] RENDERED ARTICLES/INDEX.HTML.ERB WITHIN LAYOUTS/APPLICATION (1.7MS)\nI, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] COMPLETED 200 OK IN 5MS (VIEWS: 3.4MS | ACTIVERECORD: 0.3MS)\n"
irb> File.write("log/upcased.log", upcased)
=> 896
Cách này work cho small files, nhưng đọc whole file vào memory problematic khi deal larger files. Ví dụ, parse 14-gigabyte log file, đọc whole file at once expensive operation. Content file kept in memory, app memory footprint grows considerably. Dẫn đến memory swapping và OS kill app process.
May mắn, Ruby cho phép đọc file line by line dùng File.foreach. Thay vì đọc full content at once, execute passed block cho mỗi line.
Kết quả enumerable, nên hoặc yield block cho mỗi line, hoặc trả Enumerator object nếu không pass block. Cho phép đọc bigger files không load all content vào memory at once.
irb> File.foreach("log/production.log") { |line| p line }
"I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\/articles\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\n"
"I, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\n"
"I, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendering articles/index.html.erb within layouts/application\n"
"D, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Article Load (0.3ms) SELECT \"articles\".* FROM \"articles\"\n"
"I, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendered articles/index.html.erb within layouts/application (1.7ms)\n"
"I, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\n"
Để uppercase whole file, đọc từ input file line by line, uppercase, append vào output file.
irb> File.open("upcased.log", "a") do |output|
irb* File.foreach("production.log") { |line| output.write(line.upcase) }
irb> end
=> nil
Vậy đọc file line by line work không load whole file như thế nào? Để hiểu, peel back layers around reading files. Xem closer Ruby IO class.
I/O và Ruby IO Class
Mặc dù File.read và File.foreach tồn tại, documentation File class không list chúng. Thực tế, bạn không find bất kỳ file reading/writing methods nào trong File class documentation, vì chúng inherited từ parent IO class.
I/O
I/O device là device transfer data to/from computer, ví dụ keyboards, displays, hard drives. Thực hiện Input/Output, hoặc I/O, bằng đọc hoặc produce streams of data.
Đọc/ghi files từ hard drive là common I/O gặp nhất. Other I/O types bao gồm socket communication, logging output terminal, input từ keyboard.
Class IO trong Ruby handle tất cả input/output như đọc/ghi files. Vì đọc files không khác đọc từ I/O stream khác, File class trực tiếp inherit methods như IO.read và IO.foreach.
irb> IO.foreach("log/production.log") { |line| p line }
"I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\/articles\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\n"
... (tương tự output trước)
File.foreach equivalent IO.foreach, dùng IO class version cho same result.
Reading I/O Streams Via Kernel
Internally, Ruby IO class reading/writing abilities based trên abstractions around kernel system calls. OS kernel take care reading/writing I/O devices.
Opening Files
IO.sysopen open file bằng yêu cầu kernel put reference đến file trong file table và tạo file descriptor trong process file descriptor table.
File Descriptors và File Table
Open file trả về file descriptor — integer dùng access I/O resource.
Mỗi process có own file descriptor table keep file descriptors in memory, mỗi descriptor point đến entry trong system-wide file table.
Để read/write I/O resource, process pass file descriptor cho kernel qua system call. Kernel access file thay process, vì processes không access file table.
Open files không keep content in memory, nhưng file descriptor table có thể fill up, nên good practice luôn close files sau open. Methods wrap File.open như File.read làm auto, cũng như ones taking block.
Ví dụ này, gọi IO.sysopen trực tiếp. Pass filename, method tạo file descriptor dùng reference open file sau.
irb> IO.sysopen("log/production.log")
=> 9
Tạo IO instance cho Ruby read/write, pass file descriptor cho IO.new.
irb> file_descriptor = IO.sysopen("log/production.log")
=> 9
irb> io = IO.new(file_descriptor)
=> #
Close I/O stream và remove reference file từ files table, gọi IO#close trên IO instance.
irb> io.close
=> nil
Reading Bytes và Moving Cursors
IO#sysread đọc số bytes từ IO object.
irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\/articles\" "
Ví dụ dùng IO instance tạo trước bằng pass file descriptor integer cho IO.new. Đọc và trả first 64 bytes từ file bằng gọi IO#sysread với 64 argument.
irb> io.sysread(64)
=> "for 127.0.0.1 at 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:"
Lần đầu request bytes từ file, cursor moved automatically, gọi IO#sysread trên same instance lại produce next 64 bytes.
Moving the Cursor
IO.sysseek manually move cursor đến location trong file.
irb> io.sysseek(32)
=> 32
irb> io.sysread(64)
=> "9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started "
irb> io.sysseek(0)
=> 0
irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\/articles\" "
Ví dụ move position 32, read 64 bytes dùng IO#sysread. Gọi IO.sysseek lại với 0, jump back beginning file, cho phép đọc first 64 bytes again.
Reading Files Line by Line
Giờ biết IO class convenience methods open IO streams, read bytes, move cursor position.
Methods như IO.foreach và IO#gets request lines line by line thay vì per number of bytes. Không có performant way look ahead find next newline và take all bytes until position, nên Ruby take care splitting file content.
class MyIO
def initialize(filename)
fd = IO.sysopen(filename)
@io = IO.new(fd)
end
def each(&block)
line = ""
while (c = @io.sysread(1)) != $/
line << c
end
block.call(line)
each(&block)
rescue EOFError
@io.close
end
end
Implementation này, #each method lấy bytes từ file dùng IO#sysread one at a time, cho đến byte là $/ (newline). Khi tìm newline, stop taking bytes và call passed block với line đó.
Solution work nhưng inefficient vì gọi IO.sysread cho every byte in file.
Buffering File Content
Ruby smarter về cách làm bằng keep internal buffer của file content. Thay vì đọc file one byte at a time, lấy 512 bytes at once và check newlines trong returned bytes. Nếu có, trả portion before newline và keep rest in memory as buffer. Nếu buffer không include newline, fetch 512 bytes more cho đến khi tìm thấy.
class MyIO
def initialize(filename)
fd = IO.sysopen(filename)
@io = IO.new(fd)
@buffer = ""
end
def each(&block)
@buffer << @io.sysread(512) until @buffer.include?($/)
line, @buffer = @buffer.split($/, 2)
block.call(line)
each(&block)
rescue EOFError
@io.close
end
end
Ví dụ này, #each method add bytes vào internal @buffer variable chunks 512 bytes cho đến khi @buffer include newline. Khi đó, split buffer by first newline. Phần đầu là line, phần sau là new buffer.
Passed block gọi với line và remaining @buffer kept cho next loop.
Bằng buffering file content, số I/O calls reduced بينما dividing file logical chunks.
Streaming Files
Tóm lại, streaming files work bằng ask OS kernel open file, rồi read bytes bit by bit. Khi đọc file per line trong Ruby, data taken từ file 512 bytes at a time và split up in "lines" sau đó.
Kết thúc overview I/O và streaming files Ruby. Chúng tôi muốn biết thoughts bạn về bài này, hoặc questions. Luôn tìm topics investigate và explain, nếu có anything magical Ruby bạn muốn đọc, đừng ngần ngại let us now @AppSignal!