2011년 독서 목록
오늘의 교훈 - python programming
회사에서 종종 이런저런 실험을 위해 스크립트 프로그래밍을 할 때가 있다. 나는 대체로 문법이 깔끔하고 사용이 편한 python을 사용하는데 종종 예상하지 못한 상황을 만나서 당황하는 경우가 있다. 물론 그 덕분에 새로 배우는 것도 많지만.
마침 요 며칠 사이에도 그런 경험이 있어서 교훈삼아 잊지 않기 위해 적어둔다.
지금 하는 실험은 커다란 텍스트 문서를 변환해서 시스템에서 뭔가 처리를 하는 과정이 필요하다. 원본 데이터를 가공하기 위해서 짤막한 python 스크립트를 짰다.
처음에는 단순하게 생각해서 텍스트 파일을 열어서 죽 읽어들인 다음, 한 줄씩 원하는 형식으로 변환해서 결과 파일에 쓰도록 하였다. 우선 시험삼아 약 1천만 라인 정도 결과물을 생성해 보았는데, 30분은 족히 넘겨서 기대한 시간보다 훨씬 더 많이 걸렸다. 전체 파일을 변환하려면 훨씬 더 많은 시간이 걸릴 것이므로 성능 향상이 필요했다.
곰곰 생각해보니 파일 출력을 매 줄마다 하는 부분이 문제가 될 것 같았다. 그렇다면 읽은 내용을 버퍼에 저장해서 한번에 쓰면 되지 않겠나하는 생각에 코드를 약간 고쳤다. 역시 1천만 라인으로 테스트해보니 훨씬 빨라진 것이 보인다. 올커니, 이제 되었구나하고 기쁜 마음에 8개 프로세스에서 동시에 스크립트를 실행했다.
순간 침묵. 어쩐 일인지 시작하자마자 서버가 먹통이 되었다. 분명히 메모리를 너무 많이 잡아먹어서 다른 명령을 못받는 것이었다. 그때가 금요일 저녁 7시 10분 전. 친구와 약속이 있어서 반드시 7시에 나가야하는 상황이었다. 원래는 금요일에 퇴근하면서 스크립트를 실행시켜놓고 월요일 출근과 동시에 상큼하게 완성된 결과물을 보고 싶었는데. 약속을 미룰 수도 없는 터라 부랴부랴 원격으로 프로세스 종료 명령을 내리고 (종료되었는지 확인도 못하고) 운이 좋기만을 빌며 사무실을 나섰다.
월요일이 휴일인 덕분에 화요일에 출근해서 서버에 접속하니 다행히 종료는 정상적으로 되었다. 그런데 이번엔 뭐가 문제였을까. 다시 코드를 보며 곰곰이 생각했다. 아하, 1천만 라인을 한꺼번에 버퍼에 넣어서 쓰려고 하니 메모리를 턱없이 많이 잡는군. 그렇다면 버퍼 크기를 제한해서 적당히 잘라서 처리하도록 해야겠다. 삼십 분 정도 뚝딱뚝딱 고쳐서 입출력 버퍼를 적절히 조절하게 만들었다. 이제는 안심이지. 8개 프로세스를 돌려도 잘 돈다. 각각 1억 라인을 처리하는 것도 불과 1시간 남짓. 맨 처음 코드가 그 1/10을 처리하는 데 걸린 시간과 비슷했다.
8억 라인의 데이터를 사용해서 실험을 마친 뒤 데이터를 더 늘려보기로 했다. 이번엔 각각 다섯 배인 5억 라인씩 처리하도록. 그런데 이전처럼 8개 프로세스를 실행하니 뭔가 불길하다. 이번에도 서버의 메모리 사용량이 100%를 넘기면서 cpu 사용량은 크게 줄어들고 버벅거리기 시작했다. 이상하다 싶어서 두 배로 늘려보니 이번에는 잘 돈다. 다시 메모리 사용량을 확인했더니 신기하게도 데이터 크기에 비례해서 메모리 사용량이 증가하고 있었다. 이럴리가 없는데?
아무리 코드를 들여다봐도 전체 데이터 크기에 따라서 메모리 사용량이 늘어날 건덕지가 없다. python이 내가 모르는 최적화라도 해서 바이트코드가 멋대로 바뀌는 것인가. 프로세스에 메모리 제한을 걸어야 할까. 이런저런 생각을 하면서 구글에서 답이 될만한 사이트를 찾다가 한가지 힌트를 얻었다. 원하는 데이터 크기만큼 loop를 돌리는데, 이때 의도하지 않은 리스트가 생성되는 것. 1부터 5억까지 범위를 갖는 리스트이니 당연히 메모리를 많이 차지할 수밖에.
위와 같은 문제는 평소에 스크립트를 사용할 때는 발견할 일이 없다. 그만큼 큰 리스트를 사용할 일이 없으니까. 그래서 당연히 c에서 만드는 loop과 동일하게 취급해 왔는데, 이번처럼 특수한 경우에는 완전히 동일한 동작을 한다고 볼 수는 없는 것이다. 무의식적으로 일대일 대응인 것처럼 생각했지만 환경이 바뀌고 적용 대상의 규모가 달라지면서 무시했던 비용이 무시못할 수준으로 증가한 것이다.
어쨌든 위 문제도 리스트를 사용하지 않는 loop를 써서 해결했다. 결국 사흘동안 세 번 정도 코드를 수정한 셈인데, 그때마다 매번 새로운 문제가 등장해서 해결책을 찾아야했다. 하지만 일이 끝나고 되돌아보니 원인은 한가지다. 코드 설계에 신중하지 못했다는 점.
조금 더 세세히 들어가자면 스크립트 언어를 너무 쉽게만 생각해서 scalability를 고려하지 않았거니와 평소에 무시했던 메모리 사용 문제도 문제의 규모에 따라서 고려해야 할 때도 있다는 점. 그리고 어떤 문제가 발생했을 때 차근차근 뜯어보면 단순한 해결책이 있다는 점. 실제로 내가 마지막의 loop 문제를 해결하지 못했다면 다른 방식으로 메모리 포화를 해결해야 했을 것이다. (실제로 ulimit이나 python의 memory manager를 찾아보기도 했다. 하지만 이런 방법을 사용해도 근본적인 메모리 할당 문제는 그대로 남아있어서 성능 저하는 피하지 못했을 것이다,) 엔지니어링의 기본이기도 하지만, 근본적인 문제를 해결함으로써 가장 큰 효과를 볼 수 있고, 의외로 그 해결책은 단순할 수 있다 - 간단한 설계 오류라든가.
앞으로 일하면서 대규모 데이터를 처리하고 그에 따른 메모리 문제는 항상 따라다닐 것이므로 항상 염두에 두고 있어야겠다. 아직 이런 습관이 들지 않은 것을 보면 나도 아직 초보는 초보인 듯.
- 2011/10/05 23:22 에 작성