playwright 이라는 헤드리스 브라우저로 스크래핑을 돌리는 컨테이너가 자꾸 의문사하는 현상이 발생함.

로그를 봤는데도 이건 대체 왜죽었지 싶은거임;


1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cc265d58970b8cb86e8008492bc37df9324de1cf9e


그냥 event emitter 걸고 req 들어올 때마다 header 긁어오는 부분에서 에러가 남

이미 닫힌 page, context, browser 에 대해서 저런 event emitter 가 돌아가고 있다는데

그렇게 코드를 안 짜서 뭐지 싶었음


그래서 일단은 의심이 가는 범인들을 한 번 추림


0번용의자. 메모리릭

1번용의자. 라이브러리(우분투 패키지든, 애플리케이션 패키지든)


경험 상 컨테이너가 죽는건 거의 OOM 맞고 뻗었던 경우가 많아서 일단은 0번으로 메모리릭을 뒀음


일단은 라이브러리부터 싹 다 업데이트 하고 이제 메모리 릭을 어떻게 잡을까

그러니까 프로파일링을 어떻게 할 거냐 고민을 했음


https://nodejs.org/en/docs/guides/diagnostics/memory/using-heap-snapshot

Memory Diagnostics - Using Heap Snapshot | Node.jsNode.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.nodejs.org


node.js 에선 이런 heap snapshot 기능으로 프로파일링을 할 수 있긴 한데,
뭔가 컨테이너로 한번 싸놨으니 좀 편한거 없나? 라는 생각을 하고 좀 더 찾아봄



https://github.com/grafana/pyroscope

GitHub - grafana/pyroscope: Continuous Profiling Platform. Debug performance issues down to a single line of codeContinuous Profiling Platform. Debug performance issues down to a single line of code - GitHub - grafana/pyroscope: Continuous Profiling Platform. Debug performance issues down to a single line of ...github.com



그러다보니 pyroscope 라는걸 알게됨

이미 grafana 에 loki + promtail 써서 로그 쌓는 중이라
컨테이너 하나 더올리고 스크래퍼 애플리케이션에 에이전트 물리면 되는 상황이어서 후다닥 구성함



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd245d599717d6fca2228c63ac0ea2889361009d

1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd245d5e9a45e51d81e881e40f336dc5c810189b

1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd245d5f9e5b689125af89bd9f6465f3dfb54e4c07

1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd245d5f9abd89de0d3b017e14a10a2b052818b408


근데 문제가 생김

쓰던 이미지(playwright docker 이미지)에서 pyroscope 라이브러리 설치가 안되는거임

pyroscope 가 pprof 라는 라이브러리를 내부적으로 또 사용해야 해서 추가적인 설치를 하는데,

얘의 베이스 이미지가 이거저거 경량화한 버전 기반이라 pprof 설치에 필요한 것이 없어서 그런게 아닐까 추측해봄



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd215d5e9fc7f16a550cb1bfaa0fb9c16d6b8844f8


그래서 multistage build 로 build 따로 execution 따로 분리시켰음

build 할 때는 경량화 되지 않은 순정 node.js 이미지를 사용하고,

해당 build 의 결과를 이제 COPY --from 으로 execution stage 의 이미지에 옮겨넣음


이렇게 하니 성공적으로 잘 구동됨



그리고 그렇게 프로파일링을 한 지 하루가 지난 뒤 확인해봄



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd205d599bca91e1ec3c87603d23ffdc6c1b44c54f


열심히 우상향 중



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd235d5998543a1370bb3841d4a843ad042ceb754e


flame graph

왼쪽이 프로파일링 시작한 지 4시간 지난거, 오른쪽이 24시간 지난건데

playwright 점유율이 꾸준히 올라가고 있었음


잡았다 요놈



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cd225d5f99bef671b6b2cfa65bd863b70dbdf0645a


좀비 프로세스도 게속해서 양산 중이었음

내가 중간에 ... 으로 생략해서 그렇지, 약 100개 정도로 어마어마하게 많았음


이게 대체 뭔가 싶어서 defunct(zombie) process 가 뭐고, 이게 왜 생기는건가 확인해 봄

여기서부턴 잠깐 운영체제 이야


운영체제에선 어떤 process 의 child process 가 맡은 일을 끝내
커널에서 해당 child process 의 parent process 에게 SIGCHLD 라는 signal 을 보냄.

해당 signal 을 들은 process 는 wait() 을 실행하고, child process 들 중 이제 종료시켜야 할 애들은 없나 탐색을 진행하고
해당되는 process 들은 수거함

그리고 이렇게 수거된 process 들은 PID 1 인 init process 가 알아서 잘 관리해줌

갑자기 parent process 가 죽은 orphan process 들도 마찬가지로


근데 일부 docker image 엔 이런 역할을 하는 init process 가 없음

특히, slim 태그가 붙은 등의 경량화 작업을 거친 이미지들의 경우엔 높은 확률로 존재하지 않음


https://github.com/Yelp/dumb-init

GitHub - Yelp/dumb-init: A minimal init system for Linux containersA minimal init system for Linux containers. Contribute to Yelp/dumb-init development by creating an account on GitHub.github.com


나말고도 이런 이유로 고통을 겪는 사람들이 많았는지 해결책도 다행히 있었음


대표적으로, 미국의 테크회사중 하나인 yelp 가 이런 문제를 해결하기 위한 dumb-init 이라는 프로세스를 만듬

왜 이런문제가 생기는지, 그래서 자기들이 이걸 왜 만들었고 어떻게 만들었는지를 잘 설명해 줘서 재밌다

README.md 에 아주 잘 나와있음



https://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html

Introducing dumb-init, an init system for Docker containersIntroducing dumb-init, an init system for Docker containersengineeringblog.yelp.com


yelp 의 Engineering blog 에 적은 dumb-init 에 대한 글인데, 관심이 가는 사람은 이것도 읽어보는걸 추천함




https://github.com/krallin/tini

GitHub - krallin/tini: A tiny but valid `init` for containersA tiny but valid `init` for containers. Contribute to krallin/tini development by creating an account on GitHub.github.com


* 추가로, 같은 일을 하지만 더 경량화 된 init process 인 tini 도 존재함



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14ca245d589a4969f371d5a5b55e12328ea04b85f8ff


여튼, 위에서 소개한 dumb-init 을 설치하고

ENTRYPOINT 에 넣어줌


ac5939a7001cb9428d3e33749737ebbc0969aa0a17945f58a8167d3125aaf905c8cf8b3adbcb20d1d318f6


제발 잡혀라 기도를 하고 이틀이 지남

놀러갔다 옴



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14ca275d5f976212d58aa0489b876d0efcd61fe8133b


하… 애매한데?

잡히긴 잡혔는데 여전히 또 어딘가에서 줄줄 새고 있음


ps -e 로 확인해보니 좀비 프로세스는 더 이상 안만들고 있었음

그리고 여전히 메모리 점유율이 올라가는 건 playwright 라이브러리


https://github.com/microsoft/playwright/issues/15400

‘나같이 고통을 호소하는 사람이 또 있을까?’ 하는 생각에 해당 라이브러리 이슈탭에 검색을 해 봤는데, 있었다


https://github.com/microsoft/playwright/issues/15400

[Question] Why does it seem like playwright is leaking memory? · Issue #15400 · microsoft/playwrightHi! I was curious on why playwright does not seem to let some memory be garbage collected no matter how old it is. As an easy replicable example: With playwright launch chromium Make a page and mak...github.com


“이건 테스트를 위한 라이브러리인 만큼 context 객체를 .close() 로 명시적으로 종료하지 않으면 테스트 디버깅 등을 위해서 정보를 계속 쌓음.

context.close() 로 끄삼”



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14ca2d5d5e996cee1ba1ea5a50c8a830da0a1fee2945



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14ca2c5d589ee7e0c9ae873f3e16d48e8588e889a475

추가완료




ac5939a7001cb9428d3e33749737ebbc0969aa0a17945f58a8167d3125aaf905c8cf8b3adbcb20d1d318f6

배포하고 다시 하루를 기다려봄




1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cb255d5b9ca166669b77808d6e6154d2a1cb6f6b4f

점점 잡혀가는데, 또 어딘가에서 새고 있었다


https://github.com/microsoft/playwright/issues/6319

[BUG] Memory increases when same context is used · Issue #6319 · microsoft/playwrightContext: Playwright Version: Latest (today is 26/044/2021) Operating System: Linux Node.js version: tested on both node.js version Browser: chromium Describe the bug I'm watching full-js apps (e.g ...github.com


이슈탭을 좀 더 찾아보니, 애초에 이 문제로 고통받고 있는 사람들이 많았고 그게 1년간 지속된 모양이다

같은 context 를 사용하면 사용할수록 메모리가 증가한다는데,

얘내들도 이거 좀 잡아야겠다 생각했는지 이슈 얼리고 우선순위로 올려서 개선중이라고 하는 모습을 볼 수 있었다



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cb245d5e9a1b2ea1e7f890b0dda0884c43794667

그리고 저 위의 사항에 해당되는 내 코드는 이거.

네트워크 트랜잭션이 들어올 때마다 저런 일을 수행하기에, 같은 context 에서 여러 번 무언가를 반복하고 그것이 쌓이게 된다.

그래서 실제로 flame graph 를 보았을 때, req.allHeaders() 의 부분이 상위에 있었다.


이 로직은 request 가 들어올 때마다 cookie 포함 header 를 전부 갖고와서 대충 이런 저런 작업을 해왔을 때 필요했는데,

이제 더 이상 이건 안 해도 되겠다고 판단해서 그냥 제거했다.


그리고 그 결과는...




1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cb215d5a9956c28193bc2f062452af9336790cb838


편-안



---


후기


관찰이 가능한가? 측정이 가능한가? 는 정말 중요한 문제라는 걸 깨달았다.

인스턴스 접속해서 sudo docker logs hotdeal-scrapper 로 로그 확인하기가 귀찮아서 모니터링 환경을 구축한건데, 

이렇게 구축해놓으니 조금만 더 얹으면 돼서 도움이 되더라


docker-compose 로 구동중이라 restart policy 에 재부팅 적어주면 어차피 죽어도 다시 살아나니 그만이긴 한데 

반복적으로 발생하는 문제는 해결하는 것이 장기적으로도 이로울 거 같고, 

흥미로운 도전 과제라 생각해서 이거저거 해 본 게 큰 요 며칠이었음


그리고 부끄럽게도 여태껏 프로그램을 짜면서 메모리 문제를 겪어본 적이 없었는데,
이번에 겪어서 삽질을 좀 해본 덕에 ‘이렇게 트러블슈팅하면 되겠다’ 라는 경험 하나가 생겼다

주변에 잘하는 사람들에게 헬프콜을 쳐서 의심이 가는 핵심 원인들 위주로 찾아본 덕분도 컸다


---


기타



1ebec223e0dc2bae61ab96e746837770141f1315c2300c671f0e14cb225d5e9ba52fe34a39a4599ff8444b0098c204e9



휴학시절에 yelp 서류 위이이잉 당했었는데, 졸업하고 나서도 다시 한 번 지원해 봐야지

너네 dumb-init 진짜 멋있었다고 이야기하면서 ㅋㅋ


---