성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>container에 실행 중인 Golang 프로세스를 디버깅하는 방법</h1> <p> 관련해서는 이미 설명이 잘 나와 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Attach to a process in the Docker container ; <a target='tab' href='https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#attach-to-a-process-in-the-docker-container'>https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#attach-to-a-process-in-the-docker-container</a> Attach to a process on a remote machine ; <a target='tab' href='https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#attach-to-a-process-on-a-remote-machine'>https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#attach-to-a-process-on-a-remote-machine</a> </pre> <br /> 가만 보니까, Golang에서의 delve 도구가 debugger 기능도 있지만, <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/framework/tools/sos-dll-sos-debugging-extension'>닷넷 환경과 비교하면 sos.dll 확장 기능</a>도 갖고 있는 식입니다. 그래서 빠르게 디버깅을 하는 경우라면 위의 링크에서와 같이 GoLand 등의 IDE를 빌리지 않고 그냥 container 내에서 delve 도구를 이용한 진단을 하는 것이 더 간편합니다.<br /> <br /> 자, 그럼 우선 delve를 container 내에 복사해야 하는데요, 간단하게는 다음과 같이 빌드할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > go-delve/delve (Installation) ; <a target='tab' href='https://github.com/go-delve/delve/tree/master/Documentation/installation'>https://github.com/go-delve/delve/tree/master/Documentation/installation</a> </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>pwd</span> /root # <span style='color: blue; font-weight: bold'>git clone https://github.com/go-delve/delve</span> # <span style='color: blue; font-weight: bold'>cd delve</span> # <span style='color: blue; font-weight: bold'>go install github.com/go-delve/delve/cmd/dlv</span> </pre> <br /> 그럼 아래의 경로에 설치가 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>find / -name dlv 2>/dev/null</span> /root/delve/cmd/dlv /root/go/bin/dlv // wsl # <span style='color: blue; font-weight: bold'>find / -path /mnt -prune -o -name dlv 2>/dev/null</span> </pre> <br /> 이후부터는, dlv를 이용해 현재 실행 중인 Go 프로세스를 연결해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Golang: debugging a running process ; <a target='tab' href='https://joonas.fi/today-i-learned/2022/golang-debugging-a-running-process/'>https://joonas.fi/today-i-learned/2022/golang-debugging-a-running-process/</a> go-delve/delve - Configuration and Command History ; <a target='tab' href='https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md'>https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md</a> </pre> <br /> 디버깅할 수 있습니다. 대상이 되는 프로세스의 PID를 먼저 알아내고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://www.sysnet.pe.kr/2/0/13107'>apt-get install -y procps</a> # <span style='color: blue; font-weight: bold'>ps -ef</span> UID PID PPID C STIME TTY TIME CMD ...[생략]... root <span style='color: blue; font-weight: bold'>139</span> 1 2 Jul21 ? 02:47:00 /usr/local/...생략].../test_go root 12816 0 0 02:34 pts/0 00:00:00 bash root 23870 12816 0 05:13 pts/0 00:00:00 ps -ef </pre> <br /> 연결한 다음,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>dlv attach 139</span> Type 'help' for list of commands. (dlv) </pre> <br /> dlv가 지원하는 명령어를 이용해 필요한 정보를 얻어내면 됩니다. 가령, 현재 실행 중인 모든 go routine을 이렇게 열거할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 만약 go routine들의 호출 스택을 함께 얻고 싶다면: "goroutines -t" (dlv) <span style='color: blue; font-weight: bold'>goroutines</span> Goroutine 1 - User: /usr/local/go/src/net/fd_unix.go:173 net.(*netFD).accept (0x4dc195) [IO wait 456216h2m14.227784133s] Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [force gc (idle) 456216h2m14.227809372s] Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [GC sweep wait] Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [GC scavenge wait] Goroutine 5 - User: /app/testapp/main.go:24 main.main.func1 (0x55239f) [chan receive 456216h2m14.227837918s] Goroutine 6 - User: /usr/local/go/src/runtime/time.go:193 time.Sleep (0x4608ee) [sleep] Goroutine 8 - User: /usr/local/go/src/net/fd_posix.go:56 net.(*netFD).Read (0x4db089) [IO wait] Goroutine 18 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [finalizer wait] Goroutine 19 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [select 456216h2m14.227877433s] Goroutine 20 - User: /usr/local/go/src/runtime/sigqueue.go:169 os/signal.signal_recv (0x4604d8) (thread 111) Goroutine 25 - User: /usr/local/go/src/net/fd_posix.go:56 net.(*netFD).Read (0x4db089) [IO wait] Goroutine 166 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [GC worker (idle) 456216h2m14.227913564s] ...[생략]... Goroutine 330 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x436f36) [GC worker (idle)] [51 goroutines] </pre> <br /> 이후 특정 go routine을 선택하거나,<br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (dlv) <span style='color: blue; font-weight: bold'>goroutine 6</span> Switched from 0 to 6 (thread 1054) </pre> <br /> 혹은 그것의 호출 스택을 확인하는 것도 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (dlv) <span style='color: blue; font-weight: bold'>goroutine 6 stack</span> 0 0x0000000000436f36 in runtime.gopark at /usr/local/go/src/runtime/proc.go:367 1 0x00000000004608ee in time.Sleep at /usr/local/go/src/runtime/time.go:193 2 0x000000000054a8dc in testapp/testapp.myfunc at /app/testapp/network_manager.go:118 3 0x000000000054e6b2 in testapp/testapp.mymain·dwrap·1 at /app/testapp/mymain.go:58 4 0x0000000000463a61 in runtime.goexit at /usr/local/go/src/runtime/asm_amd64.s:1581 </pre> <br /> 프로그램을 계속 진행하려면, continue 명령을 사용하고, 다시 디버그 모드로 진입하려면 Ctrl+C 키를 누릅니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (dlv) <span style='color: blue; font-weight: bold'>continue</span> // Ctrl + C received SIGINT, stopping process (will not forward signal) > runtime.futex() /usr/local/go/src/runtime/sys_linux_amd64.s:520 (PC: 0x465823) Warning: debugging optimized function 515: CMPQ AX, $0xfffffffffffff001 516: JLS 2(PC) 517: MOVL $0xf1, 0xf1 // crash 518: RET 519: => 520: // Call the function stored in _cgo_munmap using the GCC calling convention. 521: // This must be called on the system stack. 522: TEXT runtime·callCgoMunmap(SB),NOSPLIT,$16-16 523: MOVQ addr+0(FP), DI 524: MOVQ n+8(FP), SI 525: MOVQ _cgo_munmap(SB), AX (dlv) </pre> <br /> 대충 감이 오시죠? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 문제는 container 환경에서는 기본적으로 attach 권한이 없다는 점입니다. 그래서 이런 오류가 발생하는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>dlv attach 139</span> Could not attach to pid 139: this could be caused by a kernel security setting, try writing "0" to /proc/sys/kernel/yama/ptrace_scope </pre> <br /> 검색해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > after upgrade gdb won't attach to process ; <a target='tab' href='https://askubuntu.com/questions/41629/after-upgrade-gdb-wont-attach-to-process'>https://askubuntu.com/questions/41629/after-upgrade-gdb-wont-attach-to-process</a> Could not attach to pid:#### this could be caused by a kernel security setting, try writing "0" to /proc/sys/kernel/yama/ptrace_scope ; <a target='tab' href='https://github.com/microsoft/vscode-go/issues/3098'>https://github.com/microsoft/vscode-go/issues/3098</a> </pre> <br /> ptrace_scope의 값을 바꾸면 해결할 수 있다고 하는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>echo 0 | tee /proc/sys/kernel/yama/ptrace_scope</span> tee: /proc/sys/kernel/yama/ptrace_scope: Read-only file system 0 </pre> <br /> 저렇게 "Read-only file system"이라는 오류 메시지와 함께 설정이 안 됩니다. 왜냐하면 container 환경 자체가 권한이 부족한 체로 실행되는데 이를 위해 run 단계에서부터 <a target='tab' href='https://www.sysnet.pe.kr/2/0/11723#privileged'>--privileged 옵션</a>을 줘야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > $ <span style='color: blue; font-weight: bold'>docker run --privileged -ti b5eddc1a9465 /bin/bash</span> </pre> <br /> 간혹 "docker exec" 시에 줘도 되는 것처럼 답변하는 것들이 있는데 소용없습니다. "run" 단계에서부터 적용돼 있어야 합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그래서, docker run 단계에서 신경 쓰지 않았다면 dlv를 이용해 디버깅을 할 수 없습니다. 만약, 특정 버그가 자주 발생하는 거라면 상관없겠지만 어쩌다 발생하는 거라면 container를 다시 실행해봐야 한다는 것이 아쉬울 수 있는데요, 다행히 "<a target='tab' href='https://joonas.fi/today-i-learned/2022/golang-debugging-a-running-process/'>Golang: debugging a running process</a>" 글을 보면 재미있는 팁이 나옵니다.<br /> <br /> 즉, 그런 경우에도 그냥 container를 다시 시작하지 말고 "kill" 명령을 사용해 프로세스를 종료시키면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > $ <span style='color: blue; font-weight: bold'>kill -QUIT [...pid...]</span> </pre> <br /> 최소한 해당 프로세스가 실행 중인 "go routines"들의 호출 스택을 "SIGQUIT: quit" 메시지와 함께 stderr 출력을 통해 얻을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > goroutine 53 [running]: SIGQUIT: quit PC=0x465821 m=0 sigcode=0 goroutine 0 [idle]: runtime.futex() /usr/local/go/src/runtime/sys_linux_amd64.s:519 +0x21 runtime.futexsleep(0x7fff2b570cf0, 0x442fd3, 0xc00002f800) /usr/local/go/src/runtime/os_linux.go:44 +0x36 runtime.notesleep(0x6ce730) /usr/local/go/src/runtime/lock_futex.go:160 +0x87 runtime.mPark() /usr/local/go/src/runtime/proc.go:1441 +0x2a runtime.stoplockedm() /usr/local/go/src/runtime/proc.go:2602 +0x65 runtime.schedule() /usr/local/go/src/runtime/proc.go:3299 +0x3d runtime.park_m(0xc000187040) /usr/local/go/src/runtime/proc.go:3516 +0x14d runtime.mcall() /usr/local/go/src/runtime/asm_amd64.s:307 +0x43 goroutine 1 [IO wait, 1624 minutes]: ...[생략]... goroutine 34 [syscall, 1624 minutes]: ...[생략]... goroutine 53 [IO wait]: internal/poll.runtime_pollWait(0x7fe654115dd0, 0x72) /usr/local/go/src/runtime/netpoll.go:234 +0x89 internal/poll.(*pollDesc).wait(0xc000268180, 0xc00027e000, 0x0) /usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32 internal/poll.(*pollDesc).waitRead(...) /usr/local/go/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Read(0xc000268180, {0xc00027e000, 0x1000, 0x1000}) /usr/local/go/src/internal/poll/fd_unix.go:167 +0x25a net.(*netFD).Read(0xc000268180, {0xc00027e000, 0x40e234, 0x13}) /usr/local/go/src/net/fd_posix.go:56 +0x29 net.(*conn).Read(0xc00021c0b0, {0xc00027e000, 0x7fe67b5e3108, 0x18}) /usr/local/go/src/net/net.go:183 +0x45 bufio.(*Reader).fill(0xc000208420) /usr/local/go/src/bufio/bufio.go:101 +0x103 bufio.(*Reader).ReadByte(0xc000208420) /usr/local/go/src/bufio/bufio.go:253 +0x2c ...[생략]... goroutine 1448716 [IO wait]: ...[생략]... goroutine 1448561 [sleep]: ...[생략]... rax 0xca rbx 0x0 rcx 0x465823 rdx 0x0 rdi 0x6ce730 rsi 0x80 rbp 0x7fff2b570cc0 rsp 0x7fff2b570c78 r8 0x0 r9 0x0 r10 0x0 r11 0x286 r12 0x43d5c0 r13 0x0 r14 0x6ce200 r15 0x7fe654ac2403 rip 0x465821 rflags 0x286 cs 0x33 fs 0x0 gs 0x0 </pre> <br /> "<a target='tab' href='https://joonas.fi/today-i-learned/2022/golang-debugging-a-running-process/'>Golang: debugging a running process</a>" 글에서는 stderr에 대한 출력이 어떻게 redirection 되었는지 확인 방법도 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>ps -ef</span> UID PID PPID C STIME TTY TIME CMD root 1 0 0 10:57 ? 00:00:00 /bin/bash -c /test root <span style='color: blue; font-weight: bold'>52</span> 1 0 10:57 ? 00:00:04 /usr/local/lib/testapp root 554 0 0 11:11 pts/0 00:00:00 /bin/bash root 568 554 0 11:11 pts/0 00:00:00 ps -ef # <span style='color: blue; font-weight: bold'>ls -al /proc/52/fd/2</span> lrwx------ 1 root root 64 Jul 26 11:11 /proc/52/fd/2 -> /tmp/test.log </pre> <br /> 해당 Go Process를 "kill -QUIT ..."으로 종료시키면 바로 저곳으로 출력이 되는 것입니다. 게다가 stderr을 /dev/null과 같은 출력으로 우회시킨 경우에도 strace 등의 도구를 이용해 API 수준에서 가로채기를 한 후,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // apt-get install -y strace # <span style='color: blue; font-weight: bold'>strace -p 52 -s 512 -ewrite 2> /tmp/strace_52.log</span> </pre> <br /> 다른 shell을 띄워 해당 프로세스를 종료하고 지정한 로그 파일을,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > # <span style='color: blue; font-weight: bold'>kill -QUIT 52</span> # <span style='color: blue; font-weight: bold'>cat /tmp/strace_52.log</span> strace: Process 52 attached --- SIGQUIT {si_signo=SIGQUIT, si_code=SI_USER, si_pid=927, si_uid=0} --- write(2, "SIGQUIT: quit", 13) = 13 write(2, "\n", 1) = 1 write(2, "PC=", 3) = 3 write(2, "0x465821", 8) = 8 write(2, " m=", 3) = 3 write(2, "0", 1) = 1 write(2, " sigcode=", 9) = 9 write(2, "0", 1) = 1 write(2, "\n", 1) = 1 ...[생략]... </pre> <br /> API 호출 단위로 확인할 수 있습니다. 출력이 너무 원시적이어서 확인이 좀 어려운데, 이것에 대해 새롭게 포매팅해주는 스크립트를 이용하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > psobot/format_stacktrace_from_strace.py ; <a target='tab' href='https://gist.github.com/psobot/6814658'>https://gist.github.com/psobot/6814658</a> </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > import sys import re output = sys.stdin.readlines() r = re.compile(r'write\(2, "(.+?)", \d+\)\s+= \d+') print "".join([x.replace(r'\n', "\n").replace(r'\t', "\t") for x in sum([r.findall(o) for o in output], [])]) </pre> <br /> 다시 아래와 같은 식의 출력으로 정제할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > SIGQUIT: quit PC=0x464ce1 m=0 sigcode=0 goroutine 0 [idle]: runtime.futex() /usr/local/go/src/runtime/sys_linux_amd64.s:552 +0x21 ...[생략]... </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
6186
(왼쪽의 숫자를 입력해야 합니다.)