질문을 삭제하지 말아주세요.!
 
2
1
0

요약

Apache Worker + mod_php(ZTS) 조합을 Production 에 사용하지 마세요. 다음 문서를 참고하시기 바랍니다.

Apache Prefork VS Apache Worker

아시다시피 Apache Prefork는 Process기반이고, Apache Worker는 Thread 기반입니다.

Prefork의 경우

  • Apache Master Process with PHP NTS Module
    • Apache Child Process with PHP NTS Module
    • Apache Child Process with PHP NTS Module
    • Apache Child Process with PHP NTS Module
    • ...

Apache Master Process는 Apache Child Process를 관리하는 역할을 하며, Apache Child Process가 요청을 처리하게 됩니다.

Worker의 경우

  • Apache Master Process with PHP ZTS Module
    • Apache Child Process with PHP ZTS Module
      • Apache Worker Thread with PHP ZTS Module
      • Apache Worker Thread with PHP ZTS Module
      • Apache Worker Thread with PHP ZTS Module
      • ...
    • Apache Child Process with PHP ZTS Module
      • Apache Worker Thread with PHP ZTS Module
      • Apache Worker Thread with PHP ZTS Module
      • Apache Worker Thread with PHP ZTS Module
      • ...
    • ...

Worker의 경우 PHP request를 처리하는 것이 Apache Worker Thread내의 PHP 모듈이 됩니다.

잠깐, 그런데 이상하지 않나요? 왜 Apache Master Process에서 바로 Thread를 관리하지 않고, 중간에 Apache Child Process를 두었을까요? 이렇게 Hybrid한 형태로 설계한 이유가 무엇일까요? 한번 생각해보시기 바랍니다.

비교.

Prefork과 Worker의 구조가 위와 같다면 어떤것을 사용하는 것이 더 좋을까요? Memory 관점에서 보자면, 당연히 Worker가 우위에 있습니다. 그리고 Memory에 우위에 있다는 것은 더 많은 동시 접속자를 허용할 수 있다는 말과 동일합니다. 동시 접속자가 1000이라면, Prefork의 경우 1000개의 프로세스가 필요하나, Worker의 경우 1000개의 Thread + Apache Child Process수(Child 당 Worker Thread 수를 100개로 설정했다면 10이 되겠네요)가 필요합니다.

응답시간의 관점에서 볼까요? 동일한 요청 하나를 처리하는데 Prefork가 빠를까요? Worker가 빠를까요? Prefork가 더 빠릅니다. 허나 Worker와 비교해 의미있는 수치 이상 빠르다고 차마 말하지는 못하겠습니다. (왜 제가 Prefork가 더 빠르다고 생각하는지 PHP NTS 와 ZTS 를 설명하며 다루겠습니다.)

Worker가 Memory관점에서 분명히 우위에 있고, 응답시간이 Prefork에 비해 조금 느리다곤 하나 그 차이가 미미하다고 한다면, 당연히 Worker를 써야 겠네요? 허나 그렇지 않습니다. 우린 한가지 더 중요한 지표를 다루지 않았는데요, 바로 '안정성'입니다. Apache Worker + mod_php(ZTS) 조합은 Apache Prefork + mod_php(NTS) 조합에 비해 안정성이 떨어집니다. 그래서 PHP.net 은 Production 환경에 Apache Worker + mod_php(ZTS) 조합을 사용하지 말라고 권하고 있는 것입니다. (http://php.net/manual/en/faq.installation.php#faq.installation.apache2)

그렇다면, 왜 Apache Worker + mod_php(ZTS)조합은 안정성이 떨어지는 걸까요? 이 질문에 답하기 위해선 PHP NTS와 PHP ZTS를 설명해야 합니다.

PHP NTS VS PHP ZTS

Apache Prefork에 올라가는 apache PHP 모듈은 NTS 입니다. (만약 Thread -- http://php.net/manual/kr/class.thread.php -- 를 사용하고자 한다면 ZTS로 빌드된 모듈을 사용해야 합니다.)) 이때 NTS는 Non Thread Safe 의 약자이고, ZTS는 Zend Thread Safe의 약자입니다. 그렇다면 NTS는 왜 Thread Safe 하지 않으며, 왜 ZTS는 Thread Safe 할까요? 그리고 왜 php.ini의 옵션으로 thread_safe 여부를 결정할 수 없고, 새로 빌드를 해야 할까요?

PHP 엔진 내에는 여러 전역 변수가 존재합니다. PHP 소스 코드의 zend_globals.h, zend_globals_macros.h를 보면 그 중 zend_compiler_globals, zend_executor_globals 구조체를 구경할 수 있습니다. PHP 엔진과 PHP Extension은 이와 같은 전역 변수를 Macro를 통해 사용하는데요, 그 정의는 다음과 같습니다.

//TSRM.h
#ifdef ZTS
	#define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element)
#endif
...
//zend_globals_macro.h
/* Executor */
#ifdef ZTS
	#define EG(v) TSRMG(executor_globals_id, zend_executor_globals *, v)
#else //이 경우 NTS입니다.
	#define EG(v) (executor_globals.v)
	extern ZEND_API zend_executor_globals executor_globals;
#endif

만약 PHP의 함수 테이블에 접근하고자 한다면 EG(function_table) 로 접근할 수 있습니다. 조금 어려워 보일 수 있으나 요점은 간단합니다. NTS에서 executor_globals라는 전역 변수는 오직 단 하나만 존재하나, ZTS는 Thread의 갯수만큼 존재한다는 것입니다.(Thread Local 변수라고 생각하시면 됩니다.) 그리고 Thread의 global 변수에 접근하기 위해 tsrm_ls라는 Thread Context 변수를 사용합니다. 여기서 또한 알 수 있는 것은, NTS의 경우는 전역 공간에 존재하는 변수를 바로 사용하면 되나, ZTS의 경우는 몇가지 작업을 거친다는 것입니다. 먼저 tsrm_ls라는 Thread Context 변수를 얻어야 하고(항상 얻지는 않구요, PHP 엔진의 함수들 인자 마지막에 항상 전달 합니다. 자세한 내용은 생략합니다 ^^;;) global 변수의 id를 이용해 마침내 Thread의 global 변수에 접근합니다. ZTS가 NTS에 비해 몇가지 연산이 추가되었지요? 이것이 제가 ZTS가 NTS보다 단일 Request 처리를 할때 좀 더 느리다고 생각하는 이유입니다.

이제 다시 안정성 이야기로 돌아가 보겠습니다.


저렇게 PHP 엔진 내의 모든 전역 변수가 Thread Safe 하게 처리되어 있다면, 도대체 뭐가 문제일까요? 세가지 정도 말씀 드릴 수 있겠습니다.

  1. Thread Safe 하다는 목표는 종국에는 실패할 수 밖에 없습니다. 바로 Segmentation Fault 때문입니다. Apache Prefork + mod_php(NTS) 조합에서는 특정 Process에 Segmentation Fault가 발생했다고 했을때 해당 Process만이 죽습니다. Apache Worker + mod_php(ZTS) 조합의 경우 특정 Thread에 Segmentation Fault가 발생했다고 했을때 해당 Thread를 소유하는 Process 내의 모든 Thread가 죽습니다. 한 Thread의 동작이 다른 Thread에 영향을 주는 것입니다.
  2. ZTS환경에서 Segmentation Fault를 발생시키는 버그가 더 많이 발견되고 있습니다.
  3. 버그가 있을때 NTS가 재현하기 더욱 쉽습니다.

Apache Worker가 Master Process + 여러 Thread 조합이 아닌 Master Process + Child Process + Thread 로 구성된 까닭중 하나가 위 1의 문제 때문이라고 전 생각합니다. Master Process + Thread 조합일 경우 하나의 Thread 에서 Segmentation Fault가 발생하면 Apache 전부가 죽어버립니다. 허나 Master Process + Child Process + Thread 조합일 경우는 Apache 전부가 죽지 않고 Segmentation Fault를 발생시킨 Child Process만 죽습니다.

ZTS 환경에서 Segmentation Fault가 발생시키는 버그가 더 많은 까닭은, PHP 엔진에서 사용하는 전역 변수 뿐 아니라 각 PHP extension 가 사용하는 extension만의 전역 변수도 모두 Thread Local 변수로 사용해야 하고, 제작자가 신경써야 할 부분이 더 많기 때문입니다.

결론

그래서, PHP Apache Module로 서버를 구성하신다면 Apache Prefork를 사용하시기 바랍니다. Threaded MPM의 이점을 누리고 싶으시다면 Apache Worker or Event + PHP fpm 또는 Nginx + PHP fpm 조합을 사용하시기 바랍니다.

    CommentAdd your comment...