동기(Sync) PHP 개발자로서 비동기(Async) PHP에 대해 배운 것들

저는 수년간 Laravel을 사용해 왔습니다. 동기 방식의 PHP를 사용했죠. 요청이 들어오면 프로세스가 실행되고, 응답이 나갑니다. 비동기는 전혀 필요하지 않았습니다.

그러다 새로운 PHP 8.6 Polling API에 대해 읽게 되었습니다. 이는 PHP가 작업을 처리하는 방식에 대한 제 관점을 바꾸어 놓았습니다.

비동기가 어떻게 작동하는지 그 구조를 살펴보겠습니다.

Blocking I/O의 문제점

API를 호출하면 코드는 대기합니다. 예시: $response = Http::get('https://api.example.com');

만약 해당 API 호출에 300ms가 걸린다면, PHP 프로세스는 300ms 동안 아무것도 하지 않습니다. 대기(sleep) 상태로 머물게 되죠. 이 과정에서 메모리를 점유하고 워커(worker) 슬롯을 차지합니다. 만약 모든 워커가 대기 상태라면, 서버는 새로운 요청을 받지 못하게 됩니다.

비동기(Async) 솔루션

비동기를 사용하면 그 300ms 동안 다른 작업을 수행할 수 있습니다. 기다리는 대신 다른 태스크를 실행하는 것이죠.

하지만 데이터가 언제 도착했는지는 어떻게 알 수 있을까요? 여기서 커널(kernel)이 등장합니다.

Polling의 진화

1. select()

PHP는 PHP 4 시절부터 stream_select()를 지원해 왔습니다. 이는 커널에 "이 소켓들에 준비된 데이터가 있나요?"라고 묻는 방식입니다. 문제는 재검사 비용(rescan tax)입니다. 연결이 10,000개라면 매번 전체 목록을 커널에 보내야 합니다. 이는 속도가 느리고 한계에 부딪히게 됩니다.

2. epoll / kqueue

이것은 언어의 기능이 아닌 커널의 기능입니다. Linux는 epoll을 사용하고, macOS는 kqueue를 사용합니다. 전체 목록을 스캔하는 대신, 커널이 준비된 목록(ready-list)을 유지합니다. 데이터가 있는 특정 소켓이 무엇인지에 대해서만 알려주는 방식이죠. 덕분에 추가 비용 없이 수천 개의 연결까지 확장할 수 있습니다.

3. Fibers (PHP 8.1)

Fibers를 사용하면 호출 스택의 어느 곳에서든 함수를 일시 중지할 수 있습니다. Fiber는 스스로 깨어나지 않습니다. 마치 일시 정지된 YouTube 영상과 같습니다. 다시 재생하려면 누군가가 $fiber->resume()을 호출해야 합니다.

잃어버린 고리: PHP 8.6

비동기 I/O에는 세 가지 요소가 필요합니다: • 일시 중지(Pause): Fibers (현재 PHP 코어에 포함됨) • 결정(Decide): 이벤트 루프(The Event Loop, 일반 PHP 코드) • 인지(Know): 커널 폴링(Kernel Polling, 이 부분이 공백이었음)

지금까지 PHP는 오래된 도구나 C 확장을 사용하지 않고는 어떤 소켓이 준비되었는지 '알 수 있는' 네이티브한 방법이 부족했습니다.

PHP 8.6은 이 간극을 메워줍니다. 코어에 네이티브 Polling API를 도입함으로써, Linux에서는 epoll을, Mac에서는 kqueue를 자동으로 사용하게 됩니다.

전체적인 그림

비동기는 마법이 아닙니다. 이벤트 루프는 단지 Fiber의 resume()을 언제 호출할지 결정하는 PHP 코드일 뿐입니다.

Fibers는 일시 중지할 수 있는 능력을 제공합니다. epoll은 언제 다시 시작할지 아는 지능을 제공합니다.

만약 동기 방식의 PHP만 사용한다면, 당장 Laravel 앱을 변경할 필요는 없습니다. 하지만 이 모델을 이해하면 ReactPHP나 Amp 같은 비동기 라이브러리를 훨씬 쉽게 마스터할 수 있습니다.

단순히 소비하는 데 그치지 말고 직접 만들어 보세요. 코드를 직접 실행하며 어떻게 작동하는지 확인해 보시기 바랍니다.

Source: https://dev.to/alamriku/sync-php-developer-hisebe-async-php-bujhte-giye-yaa-shikhlaam-fibers-epoll-aar-php-86-462j