Node.js v10.5.0 Worker PR FAQ

이 글은 nodejs 10.5.0에 실험적 기능(experimental feature)으로 추가된 worker의 PR FAQ를 정리한 글입니다. 원문은 https://gist.github.com/benjamingr/3d5e86e2fb8ae4abe2ab98ffe4758665 에서 확인하실 수 있습니다.

Worker PR FAQ

The content here is mostly copied material written by Anna (@addaleax) and people on the PR and copied here by myself (@benjamingr). Big Props to @AyushG3112 for a lot of these questions.

Corrections, improvements and suggestions from anyone are welcome.

Q: 이 PR에는 무엇이 포함되어 있나요?

A: 이 PR은 Node.js에 스레딩 지원을 추가합니다. 여기에는 표준화된 공유 메모리와 락(mutex)이 포함됩니다.

Q: 워커? 스레드?

A: 지금은 하나의 워커가 하나의 스레드를 사용하고 각 스레드가 하나의 워커입니다. 나중에는 1:1 스레딩 모델 대신에 n:m 모듈을 도입할 수도 있습니다. 원본 PR에는 유저랜드 라이브러리가 이것을 가능케 하는 것을 포함하고 있습니다.

Q: 왜 이렇게 하나요? 왜 이 PR은 CPU 중점 작업에만 추천하고 있나요?

A: 가장 큰 목적은 일반적인 Node.js 작업을 하는데 쓰는게 아니고, CPU를 많이 사용하는 작업을 다른 스레드로 분리하도록 하는 것입니다. 일반적으로 모든 libuv 기반 API 사용이 가능하지만, 우리가 가장 피해야 하는 것은 사용자가 많은 스레드를 생성해서 각각의 스레드가 동기 작업을 하는 것을 돕는겁니다 – 제 생각으로 이 방식은 Node.js의 목적과는 어긋나다고 생각합니다. ?

단언컨대 Node.js는 "코드가 블락되지 않고 돌아가기 때문에" 인기를 끌 수 있게 되었다고 봅니다, I/O 작업을 위해 스레드를 사용할 필요가 전혀 없죠 (스레드의 직접 사용은 어렵습니다), 하지만 대신 콜백을 지원하는 비동기 API를 갖고 있습니다.

Q: 워커의 플랫폼, V8 Isolate, V8 환경, libuv와 메인 스레드는 어떤 관계인가요? 1:1 인가요 아니면 1:다 인가요?

A: 이 PR에서:

  • 플랫폼은 프로세스 스코프를 갖습니다.
  • 각 스레드는 1:1로 Isolate, IsolateData, Environment, uv_loop_t의 튜플과 대응합니다.

일반적으로 각각의 Isolate가 여러개의 환경을 갖거나, 각각의 uv_loop_t가 여러 환경을 갖는 것은 생각해볼 수 있는 부분입니다, 하지만 우리는 여기서 이 부분을 구현하진 않았습니다.

Q: 실행 환경: 부모의 환경(environ **)이 워커에 복제되거나 사용될 수 있나요?

A: env 값에 대해 이야기한다고 가정해보자면: env는 (적어도 유닉스 시스템에선)프로세스별로 할당됩니다, 따라서 워커는 메인 스레드가 env를 사용할 때 방해하지 않도록 읽기 전용의 복사본을 갖게 됩니다.

Q: 스레딩이 *Sync API와의 race condition을 만들진 않나요?

A: 스레드와 *Sync API 사이에 race condition이 생길 수 있는 점은 충분히 가능한 부분입니다, 하지만 그것들을 비동기 API로부터 만드는 것과 아무 차이가 없습니다.

Q: 어떤 객체가 스레드끼리 공유될 수 있고, 어떻게 하나요?

A: 스레드간 통신은 대체로 MessageChannel Web API로 만듭니다. ArrayBuffer를 전송하는 것과 SharedArrayBuffer를 통해 메모리를 공유하는 것이 지원됩니다.

Q: mutex/semaphore와 같은 것들을 사용할 수 있나요?

A: 멀티 프로세스를 쓸 때와 차이가 아예 없습니다! :) 이 PR은 SharedArrayBuffer를 지원하기 때문에 우리는 Atomics.wait()Atomics.wake()를 이용해 pure JS에서 실제 mutex나 다른 동기화를 위한 요소를 구현할 수 있습니다.

Q: 이 PR로 동기 블록이 가능한가요? "진짜" 뮤텍스를 제공하나요?

A: 네, 그게 실제로 Atomics.wait이 하는 일입니다. 모든 사람이 이걸 좋아하진 않지만 적어도 이런 문제를 해결할 수는 있을거고 이 구현과 함께 사용될 수 있습니다. :)

Q: globals는 스레드끼리 공유되나요?

A: globals는 워커별로 지정됩니다. 이것들은 JS 객체이기 때문에 각각의 독립적인 heap 공간에 상주합니다, 이것이 의미하는 것은 globals는 현재 V8 디자인으로서는 공유될 수 없고, 하나의 global 객체를 수정해도 다른 곳에 영향을 끼치지 않는다는 점입니다.

Q: cluster처럼 스레드간 서버 소켓 공유가 가능한가요?

A: 네, (우리가 자식 프로세스에서 하는 것과 비슷하게 IPC 메커니즘을 다루는 것과는 반대로) libuv가 이벤트 루프간 핸들을 전송하는 것을 지원하는 것이 더 좋겠지만, 이렇게 하는게 어려운 일은 아닙니다, 하지만 초기 PR의 스코프 밖입니다.

Q: 두개의 스레드가 동시에 같은 포트로 listen하려고 하면 어떤 일이 일어나나요?

A: 두 개의 프로세스가 같은 포트로 listen하려고 할 때와 동일한 일이 발생합니다. – 예외가 발생하죠.

Q: 모듈은 어떻게 불리나요?

A: 제안을 환영합니다, 현재(작업중)는 worker로 부르고 있습니다만 모두가 이 이름을 좋아하지 않고 저 패키지의 소유자가 Node.js에서 같이 작업하는 것에 관심이 있지도 않고 이름을 주지도 않기 때문입니다. 이 부분은 아직 논의중인 사항입니다.

Q: 코드와 논의중인 사항은 어디에 있나요?

A: 여기에 코드와 changes가 있습니다

Q: 스레드가 메인 스레드인지 확인할 수 있나요?

A: require('worker').isMainThread로 확인할 수 있습니다.

Q: 왜 상대경로나 함수를 워커에 넘길 수 없나요?

A: 현재 워커는 절대경로를 필요로 합니다. __filename__dirname을 사용해 상대경로를 만들 수 있습니다. 이 부분은 강한 제한은 아니지만 이 PR에서 스코프를 제한하는 것은 선택사항입니다.

다른 옵션은 나중에 고려될 예정입니다.

Q: 워커에서 uncaught exception이 발생하면 어떻게 작동하나요?

A: 현재는 해당 상황에서 워커 스레드만 멈추기 되고 메인 스레드에 있는 워커 객체에 error 이벤트가 발생하게 됩니다. 예외가 unhandled라 하더라도 메인 스레드를 멈추게 하지 않습니다.

(test/parallel/test-worker-uncaught-exception.js 코드에서 이 동작을 테스트해볼 수 있습니다 – 만약 워커에서 발생한 예외가 부모까지 멈추게 한다면 이벤트 리스너라 하더라도 버그이니 저에게 바로 알려주세요!) :)

Q: 네이티브 애드온이 작동하나요? 클러스터? child_process?

A: 아직은 안됩니다, 하지만 향후에 가능하게끔 계획되어 있습니다. 워커에서 자식 프로세스와 클러스터를 띄우는 것은 가능합니다.

Q: 메시징: 워커와 메인간 통신인가요? 또는 워커끼리도 되나요? 브로드캐스트 채널인가요 아니면 P2P 채널인가요?

MessageChannel API의 구현부는 1:1 모델을 따릅니다, 따라서 브로드캐스팅은 가능하지 않습니다. 채널은 부모와 자식 스레드 사이에 있지만, 새로운 채널을 만들고 그것들을 존재하는 채널을 통해 전송할 수 있기 때문에 워커간 메시지를 원한다면 할 수 있습니다.

Q: 워커가 signal을 받을 수 있나요?

A: 아니요, 우연하게도 불가능합니다, signal은 프로세스 단위 정보이기 때문에 비활성화 되어있습니다. 만약 이게 좋은 아이디어라고 생각된다면 우리가 이걸 수정할 순 있겠죠.

Q: node inspector가 돌아가나요? 크롬 개발자도구로 워커를 디버깅할 수 있나요?

A: 처음엔 안됐습니다, 하지만 높은 우선순위로 개발 예정입니다. 도움이 필요합니다.

Q: 리소스: 지금 시점에서 직접 다룰 수 있는 워커 스레드의 어떤 네이티브 속성이 사용 가능한가요?

A: 현재: 아무것도 안됩니다. :) addaleax/node@9a72555에서 힙 사이즈를 제한하는 몇몇 아이디어가 나왔지만, V8의 API가 이 시점에서 좋은 에러 핸들링을 허용할 거라고 보지 않습니다. 따라서, 저는 이걸 PR에 포함시키지 않았습니다. 이건 결국 제가 원하던 바 였음에도 불구하고요.

사용 가능한 스택의 크기는 현재 V8에서 프로세스 스코프입니다, 따라서 저는 이 시점에서 제한되는 어떤 점이 있다고 생각하지 않습니다.

Q: async_hook과는 어떻게 동작하나요?

A: async hooks의 동작은 원래대로 동작합니다, 차이점이 있다면 추가적인 빌트인 객체로 인해 몇몇 async ID가 달라지게 됩니다 (예시. 메인 스크립트의 execution ID). 만약 사용자가 완전히 같은 값에 의존하지 않게 허용한다면 문제되지 않습니다.

PR의 마지막 커밋에서 테스트를 확인해보세요. – 슬프게도 많은 async_hook 테스트가 세부적인 부분에 과하게 의존하고 있는 탓에 지금은 Worker에서 스킵되고 있습니다, 하지만 일반적으로 async_hooks 테스트는 다른 것들과 비슷하게 돌아갑니다.

Q: process.memoryUsage() 리포트는 어떻게 나오나요? 워커 단위로 나올 것 같은데 힙 합산으로 나오나요?

A: rss는 프로세스 단위입니다. 나머지 값은 Isolate/워커 단위로 나옵니다. 이에 관해 문서에 기록해뒀습니다. :)

Q: AsyncHooks이 각 워커마다 독립되어 있는데, 그럼 워커마다 커뮤니케이션을 맞출 수 있는 방법이 있나요? 아니면 메타데이터를 워커간에 전송하고 AsyncResource를 만들어서 필요에 따라 사용하는 것이 사용자에게 달려있나요?

A: 네, 각 워커는 독립적인 AsyncHooks의 셋을 가집니다. 이 시점에서, 정보와 같은 것들을 수동으로 커뮤니케이션 해야 합니다. (예로 다른 MessageChannel을 통해)

우리는 이걸 위한 내장 유틸리티를 구현할 수 있었습니다, 하지만 해봐야 확실하게 강력하진 않을겁니다, 왜냐하면 이 기능은 완전히 비동기로 작동해야 하고 따라서 이것은 워커쪽으로부터 간단한 정보만 읽을 수 있습니다. 객체를 inspect할 수 있다던가 하는 대신에 말이죠.

Q: 예로 누군가 http 서버를 여러 워커에서 만든다던가 하는 것을 금지할 계획이 있나요?

A: 아니요, 그리고 저 개인적으로 기술적으로 제한을 할 이유가 없는데도 사용자가 무엇을 하는 것을 인위적으로 제한하는 것은 좋지 않은 것이라 생각합니다.

하지만, 우리는 사용자에게 동기 I/O 코드를 만들고 워커에게 떠넘기는 방식 등에 대해 경고할 필요가 있습니다 – 이것은 아마 그 누구도 돕진 않을겁니다.

Q: 이 구현과 child_process/cluster는 무엇이 다른가요?

A:

워커는 개념상으로는 child_processcluster와 매우 유사합니다. 몇몇 눈에 띌만한 차이점은 다음과 같습니다:

  • 워커간 커뮤니케이션은 어렵습니다: child_process IPC와는 다르게 JSON을 사용하지 않습니다, 하지만 브라우저에서 postMessage()하는 것과 동일한 로직을 사용합니다.
    • 이걸 더 최적화 할 수 있고 더 빨라야 하더라도, 이것은 어쩔 수 없이 빠르지 없습니다. (JSON이 얼마나 이곳저곳에서 사용되어 왔는지와 이걸 더 빠르게 하기 위해서 얼마나 노력을 해왔는지를 명심해주시기 바랍니다.)
    • 직렬화된 데이터는 실제로 프로세스를 떠날 필요가 전혀 없습니다, 따라서 결론적으로 커뮤니케이션간에 더 적은 오버헤드가 따릅니다.
    • typed arrays 형태의 메모리는 전송되거나, 워커/메인스레드 사이에 공유될 수 있습니다, 이는 몇몇 케이스에서 정말로 빠른 통신을 가능하게 해줍니다.
    • 네트워크 소켓과 같은 핸들은 (아직) 전송되거나 공유될 수 없습니다.
  • worker의 일부 기능(예: process.chdir())이 프로세스 상태, 네이티브 애드온 로딩 등에 영향을 끼치기 때문에 worker 내부에서 사용할 수 있는 API에는 약간의 제한이 있습니다.
  • 각각의 워커는 고유의 이벤트 루프를 갖습니다, 하지만 리소스중 일부는 워커들 사이에 공유됩니다. (예: 파일시스템 동작을 위한 libuv 스레드 풀)