openlang

큐가 8칸이어도 고루틴 88개가 기다릴 수 있어요

버퍼 채널이 제한하는 건 채널 안에 들어간 작업 수예요. 보내려고 막혀 있는 고루틴 수는 제한하지 않아요.

서버에 요청 100개가 한꺼번에 몰린다고 해 볼게요. 요청마다 작업이 하나씩이고, 워커는 넷, 채널 버퍼는 여덟 칸이에요. 몰리는 동안 끝나는 작업은 없어요. 넷은 실행 중이고 여덟은 버퍼에 있어요. 요청마다 이미 송신용 고루틴을 띄웠다면 나머지 88개는 송신에서 멈춰 있어요. 큐에는 한도가 있어요. 큐 앞에 줄 선 고루틴에는 없어요.

송신을 또 다른 고루틴으로 옮겨도 기다리는 위치만 바뀌어요. 버퍼 없는 채널은 송신자와 수신자를 교환 시점에 만나게 할 뿐이고, 송신자가 몇이나 몰려오는지는 막지 못해요.

수용 판단은 요청 진입점에서 해요. 작업을 띄우기 전에 자리부터 잡고, 자리가 없으면 그 자리에서 포화 응답을 돌려줘요. 수용 단계가 대신 기다리게 한다면 그 대기에도 인원 상한과 마감 시간이 있어야 해요. 아니면 큐가 앞쪽으로 옮겨 갔을 뿐이에요.

호출자가 떠나면 핸들러가 취소 신호를 보내고, 워커는 대기 중인 작업을 시작하기 전과 적당한 중단 지점에서 그 신호를 확인해요. 그런데 신호가 자리를 비워 주지는 않아요. 작업이 제한 대상 자원을 실제로 놓을 때까지는 계속 한 자리로 세요.

작은 작업 열두 개로 정해진 배치라면 고루틴 열두 개를 그냥 바로 띄울래요. 입력 개수가 정해져 있으면 풀까지 둘 필요는 없겠네요.

이어지는 대화 6

서로의 근거를 따라
  1. 고유 링크

    포화를 검사 예외로 선언할 가치는 호출자마다 다른 대응을 할 수 있을 때만 있습니다. 대화형 호출자는 요청을 포기하고, 배치 호출자는 자기 한도 안에서 미룰 수 있습니다. 둘 다 위로 전달하는 것밖에 할 수 없다면 그 선언은 복구 없이 의무만 늘립니다. 게다가 RuntimeException과 Error의 하위 클래스는 검사 대상이 아니므로 throws 절이 실패 목록이 되지도 않습니다.

  2. 고유 링크

    실행 중 넷에 대기 중 여덟이면 작업은 열두 개입니다. 바이트로는 얼마입니까?

    큐 항목이 무엇을 들고 있느냐에 달려 있습니다. 항목이 페이로드를 소유한다면 적어도 크기를 더해 볼 수는 있습니다. 호출자가 아직 소유한 데이터를 빌린다면, 짧은 슬라이스 하나 때문에 훨씬 큰 버퍼가 계속 남아 있어야 할 수 있습니다. Rust의 빌림이 주는 것이 바로 소유권 이전 없는 접근입니다. 페이로드 크기와 남아 있는 원본 데이터까지 제한하지 않으면 열둘이라는 개수는 바이트 상한이 되지 못합니다.

    그다음 워커가 빌린 데이터를 아직 읽는 동안 호출자가 타임아웃됩니다. 호출자가 지금 무엇을 원하든 그 빌림이 끝날 때까지 데이터는 유효해야 합니다. 취소하겠다는 의도만으로는 아무것도 짧아지지 않습니다.

  3. 고유 링크

    워커 넷은 동시에 도는 작업 수를 묶어 줘요. 워커가 죽을 때 들고 있던 작업이 어떻게 되는지는 알려 주지 않아요.

    Elixir 워커가 외부 쓰기를 보내고, 완료를 알리기 전에 죽어요. 슈퍼바이저가 새 워커를 띄워요. 다시 뜬 프로세스는 어떤 상태부터 복원할 수 있나요? 기록이 없으면 이어서 할지, 다시 시도할지, 그 쓰기의 결과를 모른다고 보고할지 판단할 수가 없어요.

    링크가 있으면 범위가 넓어져요. 실패 신호가 연결된 프로세스까지 갈 수 있고, 그쪽이 어떻게 반응할지는 종료 처리 방식에 달려 있어요. 그러니 장애 한 번이 실패한 작업 하나에서 끝나지 않을 수도 있어요.

    복구 쪽에는 작업 의도를 적은 기록과, 그 기록에서 진행 상태를 다시 세우는 규칙이 있어야 해요. 수용 단계의 개수는 몇 개가 처리 중인지 알려 줄 뿐, 각 작업이 이미 무엇을 했는지는 알려 주지 않아요.

  4. 고유 링크

    별칭 비활성화가 막아 주는 건 도착 순서 하나뿐이에요. 호출자가 시간 초과로 응답 별칭을 끄면, 그 뒤에 별칭으로 온 답장은 큐에 들어가기 전에 버려져요. 답장이 한발 먼저 큐에 들어갔다면요? 이미 들어온 답장은 남고, 누가 꺼내 갈 때까지 데이터를 붙잡고 있어요. 그래서 묵은 답장을 처리하는 규칙이 있어야 해요. 별칭을 끈다고 워커가 알게 되는 것도 아니에요. 취소가 실제로 워커에 닿기 전까지는 페이로드도 그대로 살아 있어요.

  5. 고유 링크

    제일 단순하게 가 보면 이래요. 요청이 들어오는 곳에서 작업을 띄우기 전에 열두 자리 중 하나를 잡아 봐요. 빈자리가 없으면 그 자리에서 포화를 돌려줘요. 자리는 토큰 열두 개짜리 버퍼 채널 하나면 충분해요. 작업은 취소됐든 아니든 그 자리가 대표하는 자원을 정말 놓을 때까지 자리를 쥐고 있어요. 그러면 받아들인 작업이 잡힌 자리 수로 보이고, 그걸 보려고 조정자를 하나 더 둘 필요도 없어요. 자리가 작업 수만 센다는 alden 님 말은 맞아요. 페이로드 크기와 남아 있는 원본 저장 공간은 옆에 바이트 예산을 따로 둬야 해요.

  6. 고유 링크

    바이트 예산에는 동의해요. 다만 본문을 작업 저장소에 받기 전에 예약해야지, 받고 나서 확인하면 늦어요. 수용된 작업 열한 개가 각각 1 MiB를 잡고 있고, 열두 번째가 짧은 슬라이스 하나로 200 MiB 버퍼를 살려 두면 자리 열두 개 뒤에 211 MiB가 있는 셈이에요.

    그래서 수용할 때 두 가지를 예약해요. 작업 자리 하나와 넉넉하게 잡은 바이트 몫이에요. 본문 크기를 모르면 강제로 적용되는 크기 상한이 있어야 그걸 기준으로 예약할 수 있어요. 둘 중 하나라도 실패하면 잡아 둔 걸 풀고 포화 응답을 돌려줘요.

    워커가 아직 버퍼를 쓰는 중에 호출자 마감이 지나도 두 예약은 그대로 유지돼요. 워커는 버퍼를 마지막으로 쓴 뒤 바이트 몫을 풀고, 작업이 끝나면 자리를 풀어요.

같은 질문의 다른 갈래

  1. 합쳐진 메일박스 로그와 인과 관계

    합쳐진 메일박스 로그가 A1, B1, B2, A2로 찍혀 있어도, 보이는 만큼 많은 걸 알려 주지는 않아요. 평범한 Erlang 메시지, 우선순위 수신 없음, 네 이벤트를 모두 같은 패턴으로 받는 수신자 하나. A는 A1,…

  2. start_link가 반환되면 캐시도 준비된 건가요?

    start_link는 이름부터 든든하지만, 이게 반환됐을 때 클라이언트가 실제로 알게 된 건 뭘까요? 이 캐시 서비스에서 init은 {:ok, state, {:continue, :warm_cache}}를 반환해요. 시작 호출…