개발 기록

터미널 창 60개를 동시에 띄우면 생기는 일 — 연결 슬롯 예산 만들기

SSH 터미널을 가상 데스크톱마다 독립 프로세스로 띄웠더니 동시 접속이 몰려 터졌다. 슬롯 예산과 프로세스 격리로 해결한 과정.

윈도우에서 SSH 터미널을 쓰는데, 가상 데스크톱(VD)마다 작업을 나눠서 한다. VD 하나에 프로젝트 하나. 그러다 보니 터미널 창이 수십 개가 됐다.

이걸 하나의 앱 프로세스로 관리하면 문제가 생긴다. 렌더러가 죽거나 SSH 세션 하나가 문제를 일으키면 전부 같이 죽는다. 60개 세션을 다시 붙이는 데 몇 분이 걸린다.

그래서 VD마다 독립 프로세스로 띄우기로 했다. 그리고 새로운 문제를 만났다.

60개를 한 번에 띄우면 머신이 멈춘다

처음엔 단순하게 만들었다. 스크립트가 60개 프로세스를 순서대로 실행한다.

결과는 좋지 않았다. Electron(Chromium) 시작이 동시에 60번 일어나고, 각 프로세스가 시작하자마자 공유 세션 목록을 스캔한다. CPU와 디스크가 동시에 몰린다. 머신이 몇십 초간 반응하지 않는다.

해결은 단순했다. 실행 간격을 둔다.

.\run-vds.ps1 -Count 60 -StartIndex 1 -Prefix vd

기본 500ms 간격으로 띄운다. 60개면 30초에 걸쳐 순차적으로 뜬다. 전체가 뜨는 시간은 비슷하거나 오히려 짧다. 몰아서 넣으면 서로 경쟁하느라 더 느리다.

간격은 조정 가능하지만, 측정한 뒤에만 바꾸라고 문서에 적어뒀다. 이런 값은 “빠르게 하고 싶어서” 줄이다가 원래 문제로 돌아간다.

SSH 연결에는 슬롯 예산이 필요했다

더 어려운 문제는 SSH 연결이었다.

각 VD가 여러 세션을 가지고 있고, 뜨자마자 다 연결하려 한다. 60개 VD × 세션 몇 개면 동시 SSH 핸드셰이크가 수백 개다. 서버 쪽에서 연결을 거부하거나, 로컬에서 파일 디스크립터가 부족해진다.

두 단계로 제한을 걸었다.

VD 내부 제한 — 한 VD는 동시에 최대 3개까지만 연결을 시도한다.

머신 전체 제한 — 같은 공유 상태를 쓰는 모든 VD 프로세스가 머신 단위로 24개의 활성 TCP/KEX 슬롯을 나눠 쓴다.

두 단계가 다 필요했다. VD 내부 제한만 두면 VD가 60개일 때 여전히 180개가 동시에 뜬다. 머신 전체 제한만 두면 한 VD가 슬롯을 다 먹고 다른 VD가 굶는다.

사람을 기다리는 동안에는 슬롯을 놓아준다

여기서 실제로 겪은 문제가 하나 있었다.

SSH 연결 중에 비밀번호나 passphrase, 호스트 키 확인 같은 대화형 프롬프트가 뜰 수 있다. 이때 그 연결은 사람의 입력을 기다린다. 몇 초일 수도 있고, 자리를 비웠으면 몇 분일 수도 있다.

처음 구현에서는 이 대기 중에도 슬롯을 쥐고 있었다. 그래서 한 VD에서 프롬프트 세 개가 떠 있으면, 머신 전체 슬롯 24개 중 3개가 아무 일도 안 하면서 묶여 있었다. 프롬프트가 많이 뜨면 다른 VD들이 전부 멈춘다.

고친 방식은 이렇다. VD 내부 슬롯은 유지하되, 머신 전체 슬롯은 놓아준다.

VD 내부 슬롯을 유지하는 이유는 그 VD 안에서의 순서를 지키기 위해서다. 머신 전체 슬롯을 놓아주는 이유는 사람을 기다리는 건 자원을 쓰는 게 아니기 때문이다.

일반화하면 이렇다. 동시성 제한을 걸 때는 실제로 자원을 쓰는 구간과 그냥 기다리는 구간을 구분해야 한다. 둘을 같이 세면 한가한 대기가 바쁜 작업을 막는다.

이름 붙이기와 단일 인스턴스

프로세스를 여러 개 띄우니 “이건 어느 VD의 창인가”를 구분해야 했다.

이름을 붙여 실행한다.

.\run.ps1 -InstanceName vd-01

같은 이름으로 다시 실행하면 새 프로세스를 띄우지 않고 기존 창에 포커스를 준다. 이게 없으면 실수로 두 번 눌렀을 때 같은 세션을 두 프로세스가 경쟁적으로 열려고 한다.

여기서 작은 함정이 있었다. 인덱스 자리수 문제다. vd-1, vd-2 식으로 만들면 개수가 100을 넘을 때 자리수가 바뀌면서 기존 이름과 안 맞는다. 그래서 최소 두 자리 고정 형식(vd-01)을 쓴다. 나중에 100개를 넘겨도 vd-07은 그대로 vd-07이다.

이름과 프로필 디렉토리를 분리한 것도 같은 맥락이다. 서로 다른 이름에 같은 프로필 경로를 주면 단일 인스턴스 락을 공유하게 되어, 동시에 뜰 수 없다. 이건 의도된 동작이지만 모르면 버그로 보인다.

연결이 끊겨도 셸은 살아 있게

마지막으로, SSH가 끊기면 작업이 날아가는 문제가 있었다.

원격 셸을 tmux를 백엔드로 붙여 해결했다. 다만 tmux UI를 그대로 보여주지 않는다. 화면 렌더링, 스크롤, 선택, 붙여넣기는 터미널 앱이 계속 담당하고, tmux는 보이지 않는 지속성 계층으로만 쓴다.

이렇게 한 이유는 tmux UI가 겹치면 스크롤과 마우스 선택이 이상해지기 때문이다. 이미 my_tmux로 관리되는 워크스페이스가 있으면 거기에 직접 붙고, 그 위에 tmux를 또 중첩하지 않는다.

원격 헬퍼가 없는 서버에서는 원래의 SSH 시작 절차로 자동 폴백한다. 지속성 기능은 있으면 좋은 것이지 없으면 못 쓰는 것이 되면 안 된다.

정리

  • 프로세스를 많이 띄울 때는 시작 간격을 둔다. 몰아서 넣으면 서로 경쟁해 더 느려진다
  • 동시성 제한은 두 단계가 필요하다. 개별 제한만 두면 전체가 터지고, 전체 제한만 두면 하나가 독점한다
  • 사람 입력을 기다리는 대기는 자원 사용이 아니다. 슬롯을 놓아줘야 다른 작업이 진행된다
  • 인덱스 이름은 자리수를 고정한다. 나중에 개수가 늘면 이름이 바뀐다
  • 부가 기능(지속성 셸)은 지원 안 되는 환경에서 폴백되어야 한다. 전제 조건이 되면 안 된다