네이티브 Polling API를 정의하는 PHP RFC가 승인되어 언어의 master 브랜치에 병합되었습니다. 이로 인해 개발자들은 파일 디스크립터를 폴링할 수 있는 빠르고 OS 레벨의 방식을 갖게 되었습니다. 최근 추가된 Fibers와 결합하여, PHP는 이제 비동기 코드를 위한 내장된 효율적인 기반을 갖추게 되었습니다. 이는 그동안 라이브러리들이 자체적인 우회 방법을 고안해야 했던 공백을 메워줍니다.
Fibers와 이벤트 루프의 분리
Fibers는 저수준 제어 흐름 프리미티브(primitive)입니다. 함수가 선택한 시점에서 실행을 일시 중단하고, 나중에 중단된 지점에서 정확히 다시 시작할 수 있게 해줍니다. 결정적으로, fiber는 소켓, 타이머 또는 기타 I/O 소스에 대해 전혀 알지 못합니다. 단순히 요청에 따라 일시 중지하고 다시 시작할 뿐입니다.
이벤트 루프는 일시 중지된 fiber를 언제 재개할지 결정하는 스케줄러입니다. 전형적인 비동기 스택에서 루프는 파일 디스크립터 세트를 감시하며, 읽기 또는 쓰기가 가능해질 때까지 기다렸다가 적절한 fiber를 깨웁니다.
PHP에는 이전에 Fibers가 있었지만, 어떤 디스크립터가 준비되었는지 운영체제에 물어볼 수 있는 네이티브 메커니즘이 부족했습니다. 그 결과 stream_select()나 외부 확장에 의존해야 했으며, 모든 비동기 라이브러리는 Linux의 epoll, BSD의 kqueue, Windows의 IOCP 등을 위한 자체 백엔드를 작성해야 했습니다.
새로운 Polling API가 이 빈틈을 채워줍니다. 이 API는 OS의 폴링 기능(Linux의 epoll, BSD/macOS의 kqueue 등)을 얇게 감싼 래퍼(wrapper)를 제공합니다. Polling API는 Fibers를 대체하는 것이 아니라, 이벤트 루프에 필요한 속도와 확장성을 제공할 뿐입니다.
ReactPHP, Amp 및 관련 라이브러리에 이 변화가 중요한 이유
ReactPHP와 Amp v3는 OS 폴링 메커니즘 위에 자체적인 추상화 계층을 구축해 왔습니다. 이러한 계층에는 특정 플랫폼에 최적화된 여러 코드 경로가 포함되어 있으며, 커널 변경 사항에 맞춰 지속적으로 동기화해야 합니다. 네이티브 Polling API가 도입되면 라이브러리들은 이러한 복잡한 구조(plumbing) 대부분을 제거하고 단일한 코어 제공 호출에 의존할 수 있습니다.
- 유지보수 – 플랫폼별 분기(branch)가 줄어들면 버그가 감소하고 보안 검토 범위가 좁아집니다.
- 성능 – 네이티브 호출이 epoll/kqueue와 직접 통신합니다.
- 이식성 – "vanilla" PHP에서 실행되는 코드가 이제 별도의 선택적 확장 없이도 지원되는 모든 운영 체제에서 동일한 기본 성능을 얻을 수 있습니다.
반면, Swoole은 자체 이벤트 루프와 코루틴 시스템을 탑재한 완전한 런타임 대체제로서의 위치를 유지합니다. Polling API는 Swoole의 트레이드오프(trade-offs)에 영향을 미치지 않습니다. 초저지연(ultra-low latency)이나 커스텀 메모리 관리가 필요한 개발자들은 여전히 Swoole을 별도의 옵션으로 고려할 것입니다.
간단한 벤치마크 결과가 보여주는 사실
최소한의 스케줄러를 사용하여 단일 Io\Poll\Context로 여러 fiber를 관리하며 로우 소켓(raw sockets)을 통해 여러 URL을 가져오는 테스트를 진행했습니다. 두 가지 관찰 결과가 나타났습니다.
- 속도 – 동시 요청 수를 늘려도 총 경과 시간이 증가하지 않습니다. 요청 배치는 가장 느린 단일 요청만큼 빠르게 완료됩니다. 즉, 동시성 오버헤드가 사실상 제로에 가깝습니다.
- CPU 비용 –
stream_select()를 사용하면 스트림 수가 증가함에 따라 CPU 사용량이 눈에 띄게 상승합니다. 함수가 호출될 때마다 모든 디스크립터를 반복해서 확인해야 하기 때문입니다. 반면 Polling API의 비용은 스트림이 3개에서 30개로 늘어나도 일정하게 유지됩니다. 이는 단일 시스템 호출로 많은 디스크립터를 모니터링할 수 있는 커널의 능력 덕분입니다.
개발자가 지금 해야 할 일
- 새로운 API와 직접적으로 일치하는 fiber-native 스타일을 선호한다면 Amp v3를 채택하세요. Amp v3의 공개 인터페이스는 이미 하위 폴러(poller)에 매핑되어 있으므로, 코드를 다시 작성하지 않고도 이점을 누릴 수 있습니다.
- 루프의 생명주기(lifecycle)에 대한 명시적인 제어를 선호한다면 ReactPHP를 계속 사용하세요.
- 프로덕션 워크로드(production workloads)를 위해 커스텀 스케줄러를 만드는 것은 피하세요. 프로덕션 환경을 위해 직접 스케줄러를 작성하지 마십시오.
