2026: 임대 ProxyMac Mac mini에서의 OpenClaw stdio 버퍼링, MCP JSON-RPC 정지 및 파이프 백프레셔
홍콩, 일본, 한국, 싱가포르, 미국의 ProxyMac Mac mini에서 OpenClaw는 종종 stdio로 MCP 서버를 등록합니다: 게이트웨이가 자식 프로세스를 띄우고 stdin/stdout에서 JSON-RPC를 주고받으며 각 메시지가 즉시 도착하기를 기대합니다. 실패 징후는 조용해 짜증납니다: 첫 도구 호출은 성공한 뒤 자식이 출력을 멈추는데 top은 CPU가 놀고 있는 것처럼 보입니다. 열 번 중 아홉 번 범인은 “나쁜 모델”이 아니라 stdio 버퍼링과파이프 백프레셔입니다. 자식은 TTY를 더 이상 보지 못해완전 블록 버퍼로 바뀌고 부분 응답은 버퍼가 찰 때까지 libc에 머뭅니다. 한편 부모는 영원히 오지 않는 구분자를 기다리며 read()에 걸리거나, 부모가 파이프를 비우지 않아 자식이 write()에서 막힙니다. 이 글은 아키텍처를 설명하고 증상 매트릭스로라인 대 블록 버퍼링을 대비하며, launchd 아래PTY 래퍼와 생 파이프를 비교하고, Python(PYTHONUNBUFFERED=1, python -u), POSIX 필터(stdbuf -oL), Node 스트림(stdout을 flowing으로 소비) 완화를 나열한 뒤 MCP 설정, 게이트웨이 재시작, 배포 문제 해결, ulimits에 연결되는5단계 분류를 제공합니다——실제로는 디스크립터 고갈이 “멈춤”으로 위장한 경우를 위해서입니다.
macOS에서 stdio MCP 아키텍처(항상 참이어야 할 것)
세 협력 프로세스를 떠올리세요: (A) OpenClaw 게이트웨이, (B) MCP 서버 바이너리나 스크립트, (C) 선택적 헬퍼 필터(jq, 언어 런타임). 쓰기에서 블록하는 참가자가 있는 동안 다른 쪽이 같은 순환 의존의 읽기를 기다리면 교착이 생깁니다. stdio 전송은 stderr 규율도 물려받습니다: stderr로 진행 바를 뿌리는 수다스러운 라이브러리는 아무도 소비하지 않으면 커널 파이프 버퍼를 채웁니다.
- 플러시마다 JSON 한 메시지라는 기대는 비현실적입니다——libc는 JSON 경계를 모릅니다.
- 큰 응답에는 스트리밍 리더가 필요합니다. 게이트웨이가 전체 페이로드를 RAM에 버퍼하면 MCP 의미와 무관한 “정지”를 느낍니다.
- launchd는 대화형 셸 rc를 읽지 않습니다——환경 일치는 수동입니다. 환경 변경 후 순서 있는 재시작은 업그레이드/롤백 패턴을 보세요.
라인 버퍼링과 블록 버퍼링(터미널이 “동작”하는 이유)
많은 CLI는 isatty(stdout)가 참일 때 라인 버퍼링, stdout이 파이프일 때 블록 버퍼링(종종 4~8 KiB 배수)을 씁니다. 헤드리스 LaunchAgent 아래에서 MCP 서버는 갑자기 파이프 작성자가 됩니다——터미널에서 “라이브”처럼 보이던 로그는 버퍼가 찰 때까지 묶입니다. 그 지연은 LLM이 이미 끝났는데도 “모델이 멈췄다”처럼 보이게 합니다.
stdbuf -oL -eL로 감싸세요——프로덕션 plist에 굽기 전에 지연을 재세요.
MCP 정지 증상 매트릭스
| 신호 | MCP 버그보다 가능성 높은 원인 | 입증/반증 | 다음 링크 |
|---|---|---|---|
| 첫 RPC는 OK, 두 번째가 영원히 멈춤 | 블록 버퍼 stdout | 동일 바이너리를 script -q /dev/null 아래 PTY 스모크 테스트 | 이 글 |
| CPU는 꽉, RAM은 평탄 | 빈 fd를 읽는 타이트 스핀 | sample pid 5 -file /tmp/st.txt로 샘플 | 문제 해결 |
Too many open files | Ulimit, MCP 팬아웃 | launchctl limit maxfiles 대 프로세스 소프트 한도 | Ulimits |
| 로그 로테이션 후 간헐적 | SIGHUP 처리/재오픈 fd | 타임스탬프를 newsyslog와 상관 | 로깅 |
LaunchAgent 아래 PTY 래퍼와 생 파이프
일부 팀은 script, unbuffer, 또는 맞춤 PTY 부모로 MCP 서버를 감싸 자식이 대화형이라 믿게 합니다. 트레이드오프: PTY는CPU와 복사 오버헤드를 더하지만 버퍼링 놀라움 한 클래스를 없앱니다. 생 파이프는 저렴하지만 자식의규율 있는 flush나 stdbuf 시밈을 요구합니다. 의식적으로 선택하세요——서버마다 스타일을 섞으면 온콜이 혼란스럽습니다.
툴체인 완화(LaunchAgent EnvironmentVariables에 복사)
Python: PYTHONUNBUFFERED=1을 보내거나 python3 -u를 호출합니다. 패키지 CLI는 지원 버전에서 sys.stdout.reconfigure(line_buffering=True)를 부르는 엔트리포인트를 선호하세요. Node: stdout을 flowing 모드로 소비하세요——자식 프로세스 간 파이프에서 스트림을 멈추는 것은 흔한 발등총입니다. Go / Rust: 소스를 통제한다면 각 JSON-RPC 프레임 뒤에 명시적으로 flush하세요. 셸 필터: 자동화에서 파이프로 tail할 때 grep --line-buffered를 기억하세요.
<key>EnvironmentVariables</key>
<dict>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
<key>NODE_OPTIONS</key>
<string>--max-old-space-size=4096</string>
</dict>
에이전트를 다시 쓰기 전 stdio 5단계 분류
- 재현:
ssh에서 plist의ProgramArguments를 대화형 셸에 그대로 붙여넣기——갑자기 되면 환경/T 차이가 있습니다. - strace 유사: macOS에서 짧게
sudo fs_usage -w -f filesys | grep mcp로 쓰기 정체를 봅니다(프로덕션에서는 신중히). - stderr 분리를 로테이션 파일로. 디버그 스팸이 stdout의 JSON-RPC와 경쟁하지 않게 합니다.
- 소크 테스트: 완화를 켠 뒤 합성 대형 페이로드로 부하를 겁니다.
- 전송 결정: stdio가 여전히 취약하면 설정 가이드에 따라 HTTP MCP를 계획합니다.
FAQ
파이프 버퍼 sysctl을 올리면 고쳐지나요? 최후 수단으로 취급하세요——근본 원인은 보통 읽기/쓰기 리듬의 불균형이지 버퍼 크기만이 아닙니다.
MCP 서버가 stdout에 로그를 써야 하나요? stdout은 JSON-RPC만. 사람용 로그는 stderr나 구조화 파일로.
Apple Silicon이 버퍼를 바꾸나요? 아니요——M4는 버그에 더 빨리 닿게 할 뿐 libc 기본에서 면역이 아닙니다.
stdio MCP를 단단히 하기에 ProxyMac Mac mini가 맞는 이유
Apple Silicon M4 mini는 HK / JP / KR / SG / US에 있으며 상시 가동 베어 메탈에서 매일 밤 같은 stdio 그래프를 재생하고, 호출하는 SaaS API 옆에 리전 용량을 붙이고 LaunchAgent 저장소 옆에 도움말 링크를 둘 수 있습니다. 사람이 TCC 프롬프트를 승인해야 하면 VNC로 대체하고, 네트워크가 망설임을 주입하면 같은 릴리스 트레인의 IPv6 Happy Eyeballs SSH를 읽으세요.