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를 통해 사용하는데요, 그 정의는 다음과 같습니다.
만약 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 하게 처리되어 있다면, 도대체 뭐가 문제일까요? 세가지 정도 말씀 드릴 수 있겠습니다.
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에 영향을 주는 것입니다.
ZTS환경에서 Segmentation Fault를 발생시키는 버그가 더 많이 발견되고 있습니다.
버그가 있을때 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 조합을 사용하시기 바랍니다.
요약
Apache Worker + mod_php(ZTS) 조합을 Production 에 사용하지 마세요. 다음 문서를 참고하시기 바랍니다.
Apache Prefork VS Apache Worker
아시다시피 Apache Prefork는 Process기반이고, Apache Worker는 Thread 기반입니다.
Prefork의 경우
Apache Master Process는 Apache Child Process를 관리하는 역할을 하며, Apache Child Process가 요청을 처리하게 됩니다.
Worker의 경우
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를 통해 사용하는데요, 그 정의는 다음과 같습니다.
만약 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 하게 처리되어 있다면, 도대체 뭐가 문제일까요? 세가지 정도 말씀 드릴 수 있겠습니다.
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 조합을 사용하시기 바랍니다.