Async PHP의 진실: Fibers, epoll, 그리고 PHP 8.6

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

그러다 새로운 PHP 8.6 Polling API에 대해 읽게 되었습니다. 그것은 제가 모든 것을 바라보는 방식을 바꾸어 놓았습니다.

비동기가 내부적으로 어떻게 작동하는지에 대해 제가 배운 내용을 공유합니다.

IO 문제

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

만약 그 호출에 300ms가 걸린다면, CPU는 300ms 동안 아무것도 하지 않습니다. 대기(sleep) 상태로 머물게 되죠. 이것이 바로 블로킹(blocking) I/O입니다.

만약 세 개의 API 호출이 있다면:

  • API A: 300ms
  • API B: 400ms
  • API C: 200ms

순차적 총합: 900ms. 비동기 총합: 400ms (가장 느린 호출의 시간).

비동기를 사용하면 데이터를 기다리는 동안 프로세스가 다른 작업을 수행할 수 있습니다.

select vs. epoll

비동기 처리를 하려면 어떤 소켓에 데이터가 준비되었는지 알아야 합니다.

  1. select() PHP는 버전 4부터 stream_select()를 사용해 왔습니다. 이는 커널에 소켓 목록을 감시하도록 요청하는 방식으로 작동합니다. 문제점: 데이터가 도착할 때마다 전체 목록을 다시 스캔해야 합니다. 이를 '재스캔 비용(rescan tax)'이라고 합니다. 또한 약 1,024개의 연결 제한이 있습니다.

  2. epoll (Linux) / kqueue (macOS) 이것들은 커널 기능입니다. 목록을 스캔하는 대신, 커널이 '준비 목록(ready-list)'을 유지합니다. 그리고 어떤 특정 소켓이 준비되었는지만 알려줍니다. 덕분에 추가 작업 없이도 수천 개의 연결까지 확장할 수 있습니다.

epoll은 PHP 기능이 아닙니다. Linux 기능입니다. Go, Rust, Node.js 모두 이를 사용합니다.

Fibers: 일시 정지 버튼

PHP 8.1에서 Fibers가 도입되었습니다. 저는 Fibers가 스스로 깨어날 것이라고 생각했습니다. 하지만 그렇지 않습니다.

Fiber는 일시 정지된 비디오와 같습니다. 누군가 $fiber->resume()을 호출할 때까지 정지 상태로 유지됩니다.

이벤트 루프(Event Loop)는 단지 언제 resume()을 호출할지 결정하는 PHP 코드 조각일 뿐입니다.

비동기 I/O에는 세 가지 요소가 필요합니다:

  • 일시 정지(Pause): Fibers (PHP 8.1 코어)
  • 결정(Decide): 이벤트 루프 (일반 PHP 코드)
  • 인지(Know): 커널 폴링 (epoll/kqueue)

PHP 8.6 이전에는 PHP에 "일시 정지"와 "결정" 부분은 있었지만, "인지" 부분은 오래된 select()나 느린 C 확장 기능에 의존해야 했습니다.

PHP 8.6은 이 간극을 메웁니다. 코어에 네이티브 Polling API를 도입했습니다. 이제 PHP는 추가 확장 기능 없이도 epoll이나 kqueue를 직접 사용할 수 있습니다.

요점 정리

만약 PHP-FPM과 함께 Laravel을 사용하고 있다면, 당장 무엇을 바꿀 필요는 없습니다.

하지만 이것만은 이해해 두세요. 비동기는 마법이 아닙니다. 그저 대기 시간을 관리하는 스마트한 방법일 뿐입니다.

단순히 코드를 소비하는 데 그치지 마세요. 간단한 스크립트를 실행해 보고, 망가뜨려 보세요. 그것이 진정으로 배우는 방법입니다.

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