docker-wine으로 windows 서버 돌리기

Sunyeop Lee·2022년 1월 3일
0

개인 프로젝트에서 COM object 형태로 제공되는 API를 사용할 일이 있는데, 처음에는 Vagrant Cloud에 있는 docker-windows box를 이용해서 windows 쿠버네티스 노드를 셋업해서 사용했었다. 그리고 여러가지 이유로 윈도우즈를 쓰기 싫었다. 대략 이유를 몇가지 들어보자면 다음과 같다: 적고보니 불만이 참 많다

  • 도커파일 작성이 까다로움 - line-continuation에 \대신 `를 쓰고 host의 커널 버전에 따라 baseimage의 tag를 다르게 써야함
  • linux에서 빌드하려면 원격 도커를 써야함 - context transfer에 많은 시간을 써야한다.
  • 그래서 jenkins agent도 셋업해봤지만 리소스를 많이 쓴다.
  • grpc-health-probe가 prebuilt windows binary를 제공하지 않아서 그냥 헬스체크 포기함 (귀찮아...)
  • 리부팅했을때 docker 서비스가 시작하지 못하는 케이스가 종종 발생함
  • VirtualBox가 리소스를 많이 사용함 (CPU, memory, IO, etc…)
  • 라이센스 문제 - 돈을 내거나 체험판 라이센스를 열심히 연장해야한다. paas로 서버를 옮기면 무조건 돈내야됨

근데 이 문제들이 wine을 써서 잘 돌아가기만 하면 다 해결돼서 행복할 것 같았다. 나는 프로페셔널한 삽질 전문가니까, 진지하게 삽질할 각오를 하고 wine을 시도해봤다. Docker hub에 scotty-hardy/docker-wine를 base image로 삼아서 python installer를 넣고 돌리도록 했다. 참고로 python installer는 interaction 없이 설치할 수 있도록 옵션을 제공한다. 나는 아래처럼 했다.

FROM scottyhardy/docker-wine:stable-6.0.2

ADD https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.4.6/grpc_health_probe-linux-amd64 /usr/bin/grpc_health_probe
RUN chmod a+x /usr/bin/grpc_health_probe

ADD https://www.python.org/ftp/python/3.8.6/python-3.8.6.exe python.exe
RUN xvfb-run wine python.exe /quiet PrependPath=1 CompileAll=1 && rm -f python.exe

(후략)

wine 버전에 영향을 크게 받아 삽질을 좀 했지만 결국 신기하게도 서버가 떴다 ㅋㅋ 근데 곧 여러가지 문제에 부닥쳤다. (근데 다 해결해서 지금은 행복하다 ㅎㅎ)

한글 인코딩이 깨진다

이건 API 문제인데 ko_KR.EUC-KR 을 먹여서 해결했다 (제발.. UTF-8 합시다)

DISPLAY가 있어야한다 (?)

내 서버에서는 COM object를 사용하고 이건 Window를 생성해서 window message를 교환하기 때문에 DISPLAY가 필요하다 (...) 다행이도 Xvfb를 쓰면 디스플레이 하드웨어 없이도 필요한 환경을 구성해줄 수 있다.

grpc가 많은 양의 트래픽을 내려줄때 connection이 먹통됨

결론적으론 grpc에서 버그를 찾아서 PR 날렸다. native windows에서는 재현하지는 못했는데 WSASend 구현 차이인듯 하다. 아무튼 머지되면 후기 쓰겠다. (빨리 리뷰 좀 ..)
버그는 WSASend가 incompletely send할때 grpc가 제대로 핸들링하지 못하는 이슈다. 즉, WSASend MSDN 문서에 있는 다음 부분을 고려하지 않은것.

Note  The successful completion of a WSASend does not indicate that the data was successfully delivered.

자세한 내용은 후기때 쓰겠다. 근데 지금 PR 보낸지 8일 됐는데 리뷰해줄 낌새도 없다. python grpc 라이브러리를 윈도우용으로 빌드하는게 공식지원이 안돼서 개인적으로 고쳐쓰기도 어려워서 짜증날뻔 했는데 운좋게 workaround를 찾았다. 그것은 바로 lo (localhost)를 통해서 서버에 연결하도록 하는거다. 이렇게 하면 WSASend가 incompletely send하지 않는다. 아래처럼 socat을 썼다.

socat TCP-LISTEN:7777,fork,reuseaddr TCP4:localhost:7778

시그널 핸들링 문제로 graceful shutdown 처리가 안된다

이건 VirtualBox 쓸때도 있던 문젠지 잘 기억이 안난다. 요약하면 pod spec에 tty: true로 설정하지 않으면 signal handler (SetConsoleCtrlHandler)가 안먹는 문제. 근데 tty: true를 주면 stdout/stderr로 찍히는 로그가 80자마다 wrapping 된다 ...
그래서 tty를 false로 하면서 signal handler를 동작하게 하려고 고민하던중 드는 생각... 근데 왜 tty가 true여야하지? 리눅스에서는 tty 존재 여부와 관계 없이 signal handler가 동작한다. 그러면 tty 그 자체가 아닌 tty가 true일때 딸려오는 사이드 이펙트가 중요한거 아닐까? 그러다가 AllocConsole을 이용해 console을 만들어주면 SetConsoleCtrlHandler가 되지 않을까 했는데, 된다. (?)
그리고 쿠버네티스에서 pod이 종료될때는 컨테이너에 SIGTERM을 쏜다고 한다. docker 스펙에 따르면 pid 1번에 SIGTERM을 쏘는거다. 근데 SetConsoleCtrlHandler로 SIGTERM 핸들링이 안되는 것 같아서 dumb-init을 써서 SIGTERM을 SIGINT로 rewrite해서 처리했다.

ENTRYPOINT ["/usr/bin/dumb-init", "--rewrite", "15:2", "--"]

이래도 몇가지 문제가 남는데, SIGINT를 쏘면 서버가 시그널을 처리하다가 죽는다??? 그래서 프로세스 구조를 확인해봤는데 다음과 같다.

dumb-init
\_xvfb-run
  \_Xvfb
  \_socat
  \_python.exe

여기서 몇가지 가정을 해볼 수 있다.
1. xvfb-run이 SIGINT를 받으면 죽는다?
2. SIGINT가 python.exe에만 전달되지 않는다?

1은 xvfb-run wine python 명령으로 ctrl-c를 날려보고 잘 동작함을 확인했다. 그러면 2라는건데 바로 1의 실험에서 ctrl-c를 날리니 Xvfb가 죽는 것을 확인했다??? 그렇다. 원인은 바로 Xvfb 서버도 SIGINT를 받고 죽어서였다. 산넘어 산이다
그래서 dumb-init의 소스코드를 까봤는데 아래와 같은 부분이 있다.

void forward_signal(int signum) {
    signum = translate_signal(signum);
    if (signum != 0) {
        kill(use_setsid ? -child_pid : child_pid, signum);
        DEBUG("Forwarded signal %d to children.\n", signum);
    } else {
        DEBUG("Not forwarding signal %d to children (ignored).\n", signum);
    }
}

use_setsid는 --single-child 혹은 -c 플래그를 줬을때 0이 된다. 즉 default값이 true인데, kill(-child_pid)가 되어 child process group에 SIGINT를 전달하게 되는 것이다. ps efj 명령을 통해 process group을 확인해보니 SIGINT가 xvfb-run, Xvfb, socat, python.exe에 전달되는 것 같았다. 그래서 -c를 주고 프로세스 구조를 다음과 같이 만들기 위해 간단한 트릭을 사용했다.

dumb-init
\_python.exe
  \_Xvfb
  \_socat

바로 exec를 사용하는 것이다. xvfb-run은 쉘 스크립트로 되어있는데 xvfb-run이 처리하고 남은 인자들을 "$@"를 써서 실행시켜준다. 기존에 xvfb-run wine python server.py 했던 것을 xvfb-run exec wine python server.py로 바꿨고, 프로세스 구조가 위처럼 개선됐다. 이제 dumb-init이 single-child 모드로 돌면서 SIGTERM을 받으면 SIGINT로 변환해 python에만 시그널을 전달한다.

wine 도입 후기

간단하게 쓰려고 했는데 워낙 삽질을 많이 했던 터라 글이 길어졌다. 아무튼 wine을 도입하면서 wine, grpc 소스코드를 까보기도 하고 wireshark로 패킷을 까보기도 했는데 많은걸 배웠던 것 같다:

  • docker/kubernetes에서 SIGTERM을 쏘는 구체적인 방법
  • linux에서 init process가 하는 일
  • windows에서 console이 있어야 signal handling이 된다
  • dumb-init은 signal rewrite를 지원한다
  • kill(2)의 인자가 negative integer일수도 있다..! manual

사실 wine 도입하겠다고 결정했을때는 문제가 생기면 무조건 wine 문제일거라고 생각했는데 의외로 wine에는 문제가 별로 없었다. 심지어 WINEDEBUG 기능을 활용해 grpc 버그를 트러블슈팅하는데도 큰 도움이 됐다. 지금은 거의 모든 이슈를 해결하고 wine 컨테이너를 잘 운영하고 있어서 행복하다. 끝.

1개의 댓글

comment-user-thumbnail
2023년 4월 18일

어려운 말인데... 멋집니다

답글 달기