콘텐츠로 이동

실행 모델

가이드 홈 | 이전: 모니터링 | 다음: Backpressure — 처리보다 도착이 빠를 때

이 장을 읽고 나면

한 Spot 안에서 무엇이 함께 실행되고 무엇이 줄을 서는지, 그 경계를 무엇이 정하는지 알 수 있다. 이 장의 코드는 예제 저장소의 Bingo 샘플 README에서 가져왔으며, README의 「내려받기와 설치」·「빌드」·「실행」 절을 따르면 실행 모델 예제를 재현할 수 있다.

Spot과 Actor는 "자기 앞으로 온 일을 한 번에 하나씩 처리한다"로 요약했다. 이 장은 그 문장이 어디까지 참인지 다룬다 — 어떤 작업이 같은 줄에 서고, 어떤 작업이 나뉘며, 그 경계를 정하는 것이 무엇인가.

1. 작업이 대기하는 queue

Spot으로 들어오는 작업은 다음 queue로 나뉘어 대기한다. Spot 자신에게 온 packet과 timer는 Spot queue에, 그 Spot에 속한 Actor 앞으로 온 payload는 Actor queue에 들어간다.

Actor 앞으로 온 message는 Spot queue를 거치지 않는다. Spot의 callback이 그 message를 받아 Actor에게 넘겨주는 구조가 아니라, 처음부터 Actor queue로 들어간다.

queue 들어간다 들어가지 않는다
Spot queue Spot 앞 payload, 일치한 Logical Multicast payload, timer callback, Actor의 join·leave와 lifecycle callback Actor 업무 payload
Instance Spot의 queue Spot 앞 payload와 timer callback Actor에 관한 전부. 등록 시점에 거부한다
Actor queue Actor 업무 payload —

Instance Spot에 Actor membership이나 Logical Multicast 구독을 등록하려 하면 실행 중이 아니라 등록하거나 Spot을 준비하는 시점에 거부된다.

2. 직렬화 범위를 정하는 것

서로 다른 queue의 작업을 동시에 실행할 수 있는지는 Spot의 종류와 execution mode가 정한다.

직렬화 범위 상태 소유
Entry Spot Spot queue와 Actor queue를 각각 직렬화한다. 서로 다른 queue는 동시에 실행할 수 있다 Actor가 각자 소유한다. Actor 사이에 공유하는 상태는 외부 저장소에 둔다
User Spot SpotWide(기본) Spot handler, member Actor handler, timer, lifecycle callback 전체를 공통 gate 하나로 직렬화한다 Spot instance가 소유한다. Actor와 공유하는 상태에도 별도 동기화가 필요하지 않다
User Spot PerActor Actor별, Spot lane별로 각각 직렬화한다. 서로 다른 lane은 동시에 실행할 수 있다 Actor가 각자 소유한다. lane 사이에 공유하는 상태는 외부 저장소에 둔다
Instance Spot Spot queue의 handler와 timer를 직렬화한다. Actor queue가 없다 Spot instance가 소유한다

↗ 크게 보기

Execution mode는 factory를 등록할 때 고정하며 실행 중에는 바꾸지 않는다.

3. SpotWide — gate 하나를 지나는 실행

기본값은 SpotWide다. 그 Spot으로 향하는 모든 callback이 — 다른 Actor의 message도, timer도, lifecycle callback도 — 공통 gate 하나를 지나 한 lane에서 turn 하나씩 실행된다.

↗ 크게 보기

같은 순간에 실행되는 turn이 없으므로 handler는 Spot과 member Actor의 상태를 락 없이 평범한 코드로 직접 다룬다. Relocation에서도 Spot과 소속 Actor가 한 단위로 함께 이동한다. 대신 오래 걸리는 callback 하나가 그 Spot의 다음 callback 전체를 지연시킨다.

기본값이라 적지 않아도 되지만, 적어 두면 그 Spot이 어느 mode로 실행되는지가 등록에 드러난다.

// spot_wide is the default. Naming it here keeps the choice visible:
// every callback of this room runs through one gate.
.add_spot_factory<bingo_room_spot_t> (sample_names_t::room_spot)
.set_execution_mode (user_spot_execution_mode_t::spot_wide)

4. PerActor — lane마다 하나

PerActor는 Actor마다 독립적으로 실행해야 처리량을 얻는 경우에 고른다. Spot 자체는 상태를 두지 않고 실행 단위로만 사용한다.

서로 다른 lane이 동시에 실행되므로 여러 Actor가 함께 바꾸는 상태와 Spot 단위의 예약은 Redis나 database 같은 외부 저장소에 둔다. Factory의 이동 정책은 RecreateOnRelocation만 사용할 수 있다.

Entry Spot도 같은 모델이다. Spot queue와 Actor queue가 나뉘어 있으므로 같은 제약을 받는다.

5. 직렬 실행과 thread 점유

직렬 실행은 thread 하나를 계속 점유한다는 뜻이 아니다. Handler가 대기 지점에 도달하면 실행 thread는 다른 일을 처리하지만, 그 turn은 handler가 끝날 때까지 유지된다. SpotWide에서는 그동안 같은 Spot의 다음 callback을 시작하지 않는다.

Async와 Yield는 모두 operation을 제출하고 application 결과를 기다린다. Async의 await는 thread만 반납하고 현재 turn은 유지한다. 짧은 대기에만 Async를 사용한다. 오래 걸리는 I/O 동안 같은 Spot의 다음 turn을 실행해야 하면 Yield를 사용한다.

Yield는 결과를 기다리기 시작할 때 현재 turn을 끝내고, 같은 Spot에서 한 번에 하나만 실행하게 하는 shared gate를 반납하는 terminator다. 기다리는 동안 같은 Spot에서 대기하던 turn이 실행되고, 응답이 오면 continuation은 그 gate를 다시 얻어 queue 끝의 새 turn으로 이어서 실행한다.

↗ 크게 보기

Yield 뒤에는 그 사이 다른 turn이 상태를 바꿨을 수 있으므로, 재개한 뒤 확인하고서만 상태를 바꾼다. operation에 넘길 값은 Yield 전에 현재 turn 안에서 복사한다. SpotWide의 member Actor가 Yield하면 shared Spot gate만 반납한다. 그 Actor의 다음 message가 먼저 실행되지 않도록 Actor queue의 차례는 유지되지만, 다른 Actor·Spot handler·timer는 실행할 수 있다.

Yield를 쓸 수 있는 곳은 shared turn이 있는 SpotWide User Spot과 Instance Spot이다. Entry Spot과 PerActor User Spot은 shared turn이 없으므로 Yield를 제공하지 않는다. 이를 제공하는 request·worker와 같은 call에서는 언어별 terminator 이름이 다음과 같다.

언어 Yield terminator
C#/.NET Yield(...)
C++ .yield()
Java .yield(...)
Kotlin .yield()
Node/TypeScript .yield()

↗ 크게 보기

다음은 Bingo room join의 player record 요청이다. 각 탭에서 표시한 Yield terminator 줄이 turn을 반납하는 지점이다.

.yield<get_player_record_res_t>() 줄에서 room Spot turn을 반납한다.

get_player_record_res_t record;
try {
    get_player_record_req_t get_record;
    get_record.set_actor_id (actor.actor_id);
    record = co_await _context->outbound ()
               .request (sample_names_t::api_channel, std::move (get_record))
               .yield<get_player_record_res_t> ();
}
catch (...) {
    _pending_joins.erase (actor.actor_id);
    throw;
}
const auto resumed = _pending_joins.find (actor.actor_id);
if (resumed == _pending_joins.end () || resumed->second.room_id () != request.room_id ()
    || !_game.can_accept_player ()) {
    _pending_joins.erase (actor.actor_id);
    co_await _context->leave_actor (actor);
    co_return;
}

6. 관련 문서