Async\Pool: 범용 리소스 풀
풀이 필요한 이유
코루틴으로 작업할 때 I/O 디스크립터 공유 문제가 발생합니다. 두 코루틴이 같은 소켓을 동시에 사용하여 서로 다른 패킷을 쓰거나 읽으면, 데이터가 섞이고 결과를 예측할 수 없게 됩니다. 따라서 동일한 PDO 객체를 다른 코루틴에서 단순히 사용할 수 없습니다!
반면, 각 코루틴마다 별도의 연결을 반복해서 생성하는 것은 매우 낭비적인 전략입니다. 이는 동시 I/O의 장점을 상쇄합니다. 따라서 외부 API, 데이터베이스 및 기타 리소스와 상호 작용하기 위해 일반적으로 연결 풀이 사용됩니다.
풀이 이 문제를 해결합니다: 리소스가 미리 생성되고, 요청 시 코루틴에 제공되며, 재사용을 위해 반환됩니다.
use Async\Pool;
// HTTP 연결 풀
$pool = new Pool(
factory: fn() => new HttpConnection('api.example.com'),
destructor: fn($conn) => $conn->close(),
min: 2,
max: 10,
);
// 코루틴이 연결을 가져와 사용하고 반환
$conn = $pool->acquire();
$response = $conn->request('GET', '/users');
$pool->release($conn);풀 생성
$pool = new Pool(
factory: fn() => createResource(), // 리소스 생성 방법
destructor: fn($r) => $r->close(), // 리소스 파괴 방법
healthcheck: fn($r) => $r->ping(), // 리소스가 살아있는지?
beforeAcquire: fn($r) => $r->isValid(), // 제공 전 확인
beforeRelease: fn($r) => !$r->isBroken(), // 반환 전 확인
min: 2, // 2개 미리 생성
max: 10, // 최대 10개 리소스
healthcheckInterval: 30000, // 30초마다 확인
);| 매개변수 | 용도 | 기본값 |
|---|---|---|
factory | 새 리소스를 생성합니다. 필수 | -- |
destructor | 풀에서 제거될 때 리소스를 파괴합니다 | null |
healthcheck | 주기적 확인: 리소스가 아직 살아있는지? | null |
beforeAcquire | 제공 전 확인. false -- 파괴하고 다음 것을 가져옴 | null |
beforeRelease | 반환 전 확인. false -- 파괴, 반환하지 않음 | null |
min | 미리 생성할 리소스 수 (프리워밍) | 0 |
max | 최대 리소스 수 (유휴 + 사용 중) | 10 |
healthcheckInterval | 백그라운드 상태 확인 간격 (ms, 0 = 비활성화) | 0 |
획득과 반환
블로킹 Acquire
// 리소스가 사용 가능해질 때까지 대기 (무한)
$resource = $pool->acquire();
// 최대 5초 대기
$resource = $pool->acquire(timeout: 5000);풀이 가득 찬 경우 (모든 리소스가 사용 중이고 max에 도달), 코루틴은 일시 중단되고 다른 코루틴이 리소스를 반환할 때까지 기다립니다. 다른 코루틴은 계속 실행됩니다.
타임아웃 시에는 PoolException이 아니라 Async\TimeoutException이 발생합니다. 타임아웃은 풀의 고장이 아닙니다. 풀은 정상이며 단지 바쁠 뿐이고, 만료된 것은 여러분이 지정한 마감 시간입니다. PoolException은 풀 자체를 쓸 수 없다는 뜻입니다(닫힘, 초기화되지 않음, 팩토리 실패).
주의: TimeoutException은 PoolException이 아니라 Exception을 상속하므로 catch (PoolException)으로는 타임아웃을 잡을 수 없습니다.
try {
$resource = $pool->acquire(timeout: 5000);
} catch (Async\TimeoutException $e) {
// 5초 안에 리소스가 반환되지 않음 -- 재시도하거나 백오프
} catch (Async\PoolException $e) {
// 풀이 닫힘 -- 재시도는 무의미
}논블로킹 tryAcquire
$resource = $pool->tryAcquire();
if ($resource === null) {
echo "All resources are busy, let's try later\n";
} else {
// 리소스 사용
$pool->release($resource);
}tryAcquire()는 리소스를 사용할 수 없는 경우 즉시 null을 반환합니다. 코루틴은 일시 중단되지 않습니다.
Release
$resource = $pool->acquire();
try {
doWork($resource);
} finally {
// 중요: 항상 리소스를 풀에 반환하세요!
$pool->release($resource);
}beforeRelease가 설정되어 있고 false를 반환하면, 리소스는 손상된 것으로 간주되어 풀에 반환되는 대신 파괴됩니다.
통계
echo $pool->count(); // 총 리소스 (유휴 + 사용 중)
echo $pool->idleCount(); // 유휴, 제공 준비 완료
echo $pool->activeCount(); // 현재 코루틴이 사용 중풀 닫기
$pool->close();닫을 때:
- 모든 대기 중인 코루틴이
PoolException을 받음 - 모든 유휴 리소스가
destructor를 통해 파괴됨 - 사용 중인 리소스는 이후
release시 파괴됨
Healthcheck: 백그라운드 검사
healthcheckInterval이 설정되면 풀이 유휴 리소스를 주기적으로 검사합니다. 죽은 리소스는 파괴되고 새 것으로 교체됩니다 (수량이 min 아래로 떨어진 경우).
$pool = new Pool(
factory: fn() => new DatabaseConnection($dsn),
destructor: fn($conn) => $conn->close(),
healthcheck: fn($conn) => $conn->ping(), // 확인: 연결이 살아있는지?
min: 3,
max: 10,
healthcheckInterval: 10000, // 10초마다
);Healthcheck는 유휴 리소스에만 작동합니다. 사용 중인 리소스는 검사하지 않습니다.
서킷 브레이커
풀은 서비스 가용성 관리를 위한 서킷 브레이커 패턴을 구현합니다.
세 가지 상태
| 상태 | 동작 |
|---|---|
ACTIVE | 모든 것이 작동, 요청이 통과 |
INACTIVE | 서비스 사용 불가, acquire()가 예외를 발생 |
RECOVERING | 테스트 모드, 제한된 요청 |
use Async\CircuitBreakerState;
// 상태 확인
$state = $pool->getState(); // CircuitBreakerState::ACTIVE
// 수동 제어
$pool->deactivate(); // INACTIVE로 전환
$pool->recover(); // RECOVERING으로 전환
$pool->activate(); // ACTIVE로 전환전략을 통한 자동 관리
use Async\CircuitBreakerStrategy;
class MyStrategy implements CircuitBreakerStrategy
{
private int $failures = 0;
private int $openedAt = 0;
public function reportSuccess(mixed $source): void {
$this->failures = 0;
$source->activate();
}
public function reportFailure(mixed $source, \Throwable $error): void {
$this->failures++;
if ($this->failures >= 5) {
$this->openedAt = time();
$source->deactivate();
}
}
public function shouldRecover(): bool {
return time() - $this->openedAt >= 30;
}
}
$pool->setCircuitBreakerStrategy(new MyStrategy());전략은 자동으로 호출됩니다:
reportSuccess()-- 리소스가 풀에 성공적으로 반환될 때reportFailure()--beforeRelease가false를 반환할 때 (리소스 손상)
리소스 수명 주기
실전 예제: Redis 연결 풀
use Async\Pool;
use function Async\spawn;
use function Async\await;
$redis = new Pool(
factory: function() {
$conn = new Redis();
$conn->connect('127.0.0.1', 6379);
return $conn;
},
destructor: fn($conn) => $conn->close(),
healthcheck: fn($conn) => $conn->ping(),
min: 2,
max: 20,
healthcheckInterval: 15000,
);
// 100개의 코루틴이 20개의 연결을 통해 Redis에서 동시에 읽기
$coroutines = [];
for ($i = 0; $i < 100; $i++) {
$coroutines[] = spawn(function() use ($redis, $i) {
$conn = $redis->acquire(timeout: 3000);
try {
return $conn->get("key:$i");
} finally {
$redis->release($conn);
}
});
}
$results = array_map(fn($c) => await($c), $coroutines);
$redis->close();PDO 풀
PDO의 경우, 풀링을 완전히 투명하게 만드는 Async\Pool과의 내장 통합이 있습니다. 수동 acquire/release 대신 풀이 뒤에서 자동으로 관리됩니다.
자세히 보기: PDO 풀
다음 단계
- Async\Pool 아키텍처 -- 내부 구조, 다이어그램, C API
- PDO 풀 -- PDO를 위한 투명 풀
- 코루틴 -- 코루틴 작동 방식
- 채널 -- 코루틴 간 데이터 교환