루비에서의 메모리 릭
Posted by 미스란디르
발단은 mechanize-0.6.4.
코카스님이 mechanize-0.6.4를 가지고 OBot(이게 뭘까? =3) 를 돌리면 메모리 릭이 생긴다고 하시길래, 과연 정말 그런지 확인해 봤다.
다음과 같은 간단한 코드를 돌려보았다.
m = WWW::Mechanize.new
m.max_history = 2
loop {
m.get 'http://some.domain.tld/blank_page.html'
}
예로 든 url은 물론 없는 url이고, 실제로는 한글자짜리 html을 만들어서 테스트해봤다.
몇 분 안지나서 수십메가를 잡아먹기 시작한다.
으음. 과연 무엇이 문제일까. 0.6.3의 mehchanize.rb 를 0.6.4에다 복사하고 다시 돌려봤다. 이건 문제가 없다!
이럴땐 무식한게 최고다. 짐작이 안갈땐 손으로 해본다.
한부분씩 고쳐가면서 테스트를 해봤다.
그 결과.
body = StringIO.new
total = 0
response.read_body { |part|
total += part.length
body.write(part)
}
fetch_page 메서드 안의 저 부분이 들어가면서 부터 문제가 생긴다는 것을 알게 되었다. 으음. 혹시 StringIO가 버그인가? 이녀석의 구현은 C로 되어있다. 혹시 GC가 오동작하나? 해서 stringio의 mark함수를 살펴봤으나.. 이거 오직 하나 String객체만 포함할 뿐 다른건 없다. 그리고 free도 간단하기 그지 없는데다, StringIO테스트 코드를 만들어봤지만 역시 leak은 없었다.
StringIO를 String으로 바꿔서 저 코드를 돌려봐도 마찬가지.
그렇다면.. block?
read_body에 붙은 블록의 코드를 다 지우고 블록 뼈대만 남겨봤다. 앗! 역시 마찬가지다.
사실 fetch_code의 정의는
def fetch_page(uri, request, cur_page=current_page(), request_data=[])
이런데, 여기서 current_page()가 무엇인고 하면, @history 라는 배열에서 마지막 녀석을 돌려주는 메서드다. 그리고 @history는 @history_max 갯수만큼만 보관하도록 되어있다. add_to_hitory 메서드가 바로 그것.
코드를 보자.
def add_to_history(page)
@history.push(page)
if @max_history and @history.size > @max_history
# keep only the last @max_history entries
@history = @history[@history.size - @max_history, @max_history]
end
end
뭐 복잡한거 하나도 없다. 미리 최대 길이가 정해져 있으면 그이상 들어왔을 때 나머지를 빼버린다. 저러면 계속 history에 추가되더라도 나머지 page들에 대한(유효기간이 지난) 참조는 더이상 없어지므로 GC대상이 되어서 사라져야 한다. 그러나 mechanize-0.6.4의 경우는 저 객체들이 GC한테 안걸리는데, 그 이유가 무엇일까?
짐작 되는 원인은 이렇다. 용의자는 바로 블록이 클로져라는 사실이다. 클로져는 어떤식으로든 그 자신의 정의될때의 컨텍스트를 가지고 있어야 한다. 예를들면,
def method(arg)
lambda { arg }
end
l = method(2)
l.call => 2
@method(2)@라고 호출하면 arg는 함수 안에서만 유효하고 곧 사라져야한다. 그러나 클로져의 특성상 함수의 실행이 끝나더라도 컨텍스트를 저장하게 되고, arg의 대한 참조가 바로 저곳에 들있게 된다. 아직 루비 소스에 대해서 잘 모르기 때문에 블록이 어떻게 구현되었는지는 정확히 몰라도, GC가 보기에 arg는 저 블록이 존재하는 한 살아있다고 판단할 것이 분명하다.
결국 클로져라는 이점이 발목을 잡은셈. 아직 완전히 추적이 끝나지 않았지만, 이런 상황도 있을 수 있다는 것을 기록 하고자 글을 쓴다.
문제가 뭔지 제대로 파악한 사람은 좀 알려주시면 고맙겠다.
—
class MyMech
def initialize
@arr = []
end
def sabjil(curr = @arr.last)
x = lambda {}
add_to_arr([x])
end
def add_to_arr(item)
@arr.push(item)
if @arr.size > 2
@arr = @arr[@arr.size - 2, 2]
end
end
end
s = MyMech.new
while true
s.sabjil
end
이게 버그가 생기는건 당연하다. 순차적으로 계속 참조하거덩. 이를 어쩐다…
