Delve로 디버깅 #

이 가이드는 Docker 컨테이너 내에서 Nakama를 실행하는 데 이미 익숙한 사용자를 위한 설명입니다. 자세한 내용은 Docker Compose로 Nakama 설치 문서를 참조하세요.

main.go 파일 #

이 가이드에서는 Delve를 사용하여 디버그할 수 있는 단일 테스트 RPC 함수를 등록하는 예제 main.go 파일을 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/heroiclabs/nakama-common/runtime"
)
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	logger.Debug("GO SERVER RUNTIME CODE LOADED")
	initializer.RegisterRpc("RpcTest", RpcTest)
	return nil
}

func RpcTest(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	logger.Debug("RpcTest RPC called")

    payloadExists := false
    
    if payload != "" {
        payloadExists = true
    }

    return fmt.Sprintf("{ \"payloadExists\": %v }", payloadExists), nil
}

Dockerfile 생성 #

dlv(으)로 디버깅할 수 있는 방식으로 서버 런타임 코드를 빌드하려면 런타임 코드와 Nakama 바이너리가 최적화 없이 빌드되도록 하는 Dockerfile을 만들어야 합니다.

새 Dockerfile을 만들고 Dockerfile과(와) 같이 호출합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM registry.heroiclabs.com/heroiclabs/nakama-pluginbuilder:3.16.0 AS go-builder
ENV GO111MODULE on
ENV CGO_ENABLED 1
WORKDIR /backend
COPY go.mod .
COPY vendor/ vendor/
COPY *.go .

RUN apt-get update && \
    apt-get -y upgrade && \
    apt-get install -y --no-install-recommends gcc libc6-dev

RUN go build --trimpath --gcflags "all=-N -l" --mod=vendor --buildmode=plugin -o ./backend.so
RUN go install github.com/go-delve/delve/cmd/dlv@latest

FROM registry.heroiclabs.com/heroiclabs/nakama-dsym:3.16.0

COPY --from=go-builder /go/bin/dlv /nakama
COPY --from=go-builder /backend/backend.so /nakama/data/modules/
COPY local.yml /nakama/data/

ENTRYPOINT [ "/bin/bash" ]

여기에 주의 사항이 있습니다. 이 go-builder 단계에서, 결과 플러그인 파일에서 최적화를 효과적으로 비활성화하는 --gcflags "all=-N -l" 플래그를 사용하여 Go 런타임 플러그인을 빌드할 수 있는 몇 가지 패키지를 설치합니다.

또한 RUN go install github.com/go-delve/delve/cmd/dlv@latest을(를) 사용하여 dlv을(를) 설치합니다. 다음 단계에서 이것을 최종 docker 이미지에 복사합니다.

마지막 Docker 단계에서는 표준 nakama 이미지가 아닌 nakama-dsym 이미지를 사용합니다. 이는 최적화가 비활성화된 Nakama 바이너리를 제공하는 이미지로, 다른 플러그인과 마찬가지로 dlv(으)로 실행하기에 적합합니다.

그런 다음 dlv 바이너리와 서버 런타임 플러그인 및 local.yml 구성 파일을 정상적으로 복사합니다.

여기서는 Nakama가 자동으로 시작하지 않도록 기본값 ENTRYPOINT을(를) 재정의해야 합니다. 이렇게 하면 컨테이너에 docker exec을(를) 수행하여 dlv을(를) 실행할 수 있습니다.

Docker Compose 파일 추가 #

디버깅에 사용할 docker-compose.yml 파일은 Docker Compose로 Nakama 설치 문서에 있는 표준 파일과 유사하지만 몇 가지 사항을 조정했습니다. 여기서 Dockerfile 이미지를 빌드하고 nakama 서버에서 entrypoint 속성을 제거하여 컨테이너를 실행할 때 Nakama가 실행되지 않도록 합니다. 또한 컨테이너 내에서 dlv을(를) 성공적으로 실행하는 데 필요한 몇 가지 값을 설정합니다. 이런 값은 security_opt, stdin_opentty 속성입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
version: '3'
services:
  postgres:
    command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
    environment:
      - POSTGRES_DB=nakama
      - POSTGRES_PASSWORD=localdb
    expose:
      - "8080"
      - "5432"
    image: postgres:12.2-alpine
    ports:
      - "5432:5432"
      - "8080:8080"
    volumes:
      - data:/var/lib/postgresql/data
  nakama:
    build:
        context: .
        dockerfile: Dockerfile
    depends_on:
      - postgres
    expose:
      - "7349"
      - "7350"
      - "7351"
      - "2345"
    healthcheck:
      test: ["CMD", "/nakama/nakama", "healthcheck"]
      interval: 10s
      timeout: 5s
      retries: 5
    links:
      - "postgres:db"
    ports:
      - "7349:7349"
      - "7350:7350"
      - "7351:7351"
      - "2345:2345"
    restart: unless-stopped
    security_opt:
      - "seccomp:unconfined"
    stdin_open: true
    tty: true
volumes:
  data:

Docker로 실행 #

Docker 구성 파일이 완료되면 이제 Nakama 및 Postgres 컨테이너를 시작합니다:

1
docker compose up

컨테이너가 실행 중인 상태에서 새 터미널 창을 열고 Nakama docker 컨테이너(여기서 server-nakama-1은(는) Nakama 컨테이너 이름)로 exec합니다.

1
docker exec -it server-nakama-1 /bin/bash

이렇게 하면 Nakama 컨테이너 내부의 bash 쉘로 이동됩니다.

Nakama 데이터베이스 수동 마이그레이션 #

여기에서 먼저 Nakama에 사용할 Postgres 데이터베이스를 마이그레이션해야 합니다.

1
/nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama

Delve로 Nakama 실행 #

데이터베이스가 마이그레이션되면 이제 Delve를 통해 Nakama를 실행할 수 있습니다. Nakama 바이너리 경로와 Nakama 구성 플래그 사이의 추가 --에 유의하십시오. 이것을 포함하지 않으면 dlv가 해당 플래그를 Nakama 바이너리에 올바르게 전달하지 않습니다.

1
./dlv --log --log-output=debugger exec /nakama/nakama -- --config /nakama/data/local.yml --database.address postgres:localdb@postgres:5432/nakama

여기서 dlv exec 명령을 사용하여 미리 컴파일된 Nakama 바이너리를 실행하고 디버그 세션을 시작합니다. dlv 명령줄 인터페이스가 표시됩니다:

1
(dlv) 

Delve를 사용하여 사용자 지정 서버 런타임 코드 디버깅 #

Delve는 이제 진행 방법을 알려주는 사용자 입력을 대기하고 있습니다. 이 시점에서 continue을(를) 입력하면 Delve가 Nakama를 계속 실행하고 표준 Nakama 서버 출력 로그도 볼 수 있습니다. 그러나 Nakama는 시작이 완료되면 종료 신호를 기다리는 상태가 되며, Delve가 끼어들 수 없기 때문에 이 작업을 수행하면 런타임 코드를 디버그할 수 없습니다.

대신 Nakama가 이 대기 단계에 들어가기 전에 중단점을 설정해야 합니다. 그러나 그 후에 사용자 지정 서버 런타임 플러그인이 로드됩니다.

이를 위해 main.go의 181번째 줄에 중단점을 설정해 보겠습니다.

1
(dlv) break main.go:181

중단점을 확인할 때 다음과 유사한 출력이 표시되어야 합니다.

1
2
2022-06-30T12:29:48Z info layer=debugger created breakpoint: &api.Breakpoint{ID:1, Name:"", Addr:0x1f266cd, Addrs:[]uint64{0x1f266cd}, File:"github.com/heroiclabs/nakama/v3/main.go", Line:181, FunctionName:"main.main", Cond:"", HitCond:"", Tracepoint:false, TraceReturn:false, Goroutine:false, Stacktrace:0, Variables:[]string(nil), LoadArgs:(*api.LoadConfig)(nil), LoadLocals:(*api.LoadConfig)(nil), WatchExpr:"", WatchType:0x0, VerboseDescr:[]string(nil), HitCount:map[string]uint64{}, TotalHitCount:0x0, Disabled:false, UserData:interface {}(nil)}
Breakpoint 1 set at 0x1f266cd for main.main() github.com/heroiclabs./v3/main.go:181

이제 중단점에 도달할 때까지 Nakama를 계속 실행해 보겠습니다.

1
2
(dlv) continue
> main.main() github.com/heroiclabs./v3/main.go:181 (hits goroutine(1):1 total:1) (PC: 0x1f266cd)

이 시점에서 사용자 지정 서버 런타임 코드가 로드되어야 합니다. libraries 명령을 사용하고 플러그인 .so 파일을 찾아 이를 확인할 수 있습니다.

1
2
3
4
5
6
7
(dlv) libraries
0. 0x7f8099127000 /lib/x86_64-linux-gnu/libdl.so.2
1. 0x7f8099106000 /lib/x86_64-linux-gnu/libpthread.so.0
2. 0x7f8098f45000 /lib/x86_64-linux-gnu/libc.so.6
3. 0x7f8099131000 /lib64/ld-linux-x86-64.so.2
4. 0x7f8062758000 /lib/x86_64-linux-gnu/libnss_files.so.2
5. 0x7f806177c000 /nakama/data/modules/backend.so

위에서 /nakama/data/modules/backend.so(으)로 나열된 사용자 지정 서버 런타임 코드가 표시됩니다.

이 예에서 사용자 지정 서버 런타임에는 단순히 Nakama 서버 콘솔에 메시지를 로그아웃하고 페이로드가 제공되었는지 여부를 나타내는 값과 함께 JSON 응답을 반환하는 RpcTest 함수가 있습니다.

이 함수가 존재하는지 확인합니다:

1
2
(dlv) funcs RpcTest
heroiclabs.com/nakama-server-sandbox.RpcTest

이 함수에 중단점을 설정할 수 있습니다.

1
2
3
(dlv) break RpcTest
2022-06-30T12:37:16Z info layer=debugger created breakpoint: &api.Breakpoint{ID:2, Name:"", Addr:0x7f395d54970a, Addrs:[]uint64{0x7f395d54970a}, File:"heroiclabs.com/nakama-server-sandbox/main.go", Line:14, FunctionName:"heroiclabs.com/nakama-server-sandbox.RpcTest", Cond:"", HitCond:"", Tracepoint:false, TraceReturn:false, Goroutine:false, Stacktrace:0, Variables:[]string(nil), LoadArgs:(*api.LoadConfig)(nil), LoadLocals:(*api.LoadConfig)(nil), WatchExpr:"", WatchType:0x0, VerboseDescr:[]string(nil), HitCount:map[string]uint64{}, TotalHitCount:0x0, Disabled:false, UserData:interface {}(nil)}
Breakpoint 2 set at 0x7f395d54970a for heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:14

그런 다음 계속 실행합니다:

1
(dlv) continue

지금 Nakama 콘솔로 이동하여 사용자로부터 이 Rpc를 트리거하면 이 함수를 디버그할 수 있습니다.

중단점에 도달하는 즉시 Delve로부터 제어권이 반환되므로 다양한 항목을 검사할 수 있습니다:

1
2
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:14 (hits goroutine(276):1 total:1) (PC: 0x7f395d54970a)
(dlv)

이제 제일 먼저 args 명령을 사용하여 RPC에 전달된 인수를 검사합니다.

1
2
3
4
5
6
7
8
(dlv) args
ctx = context.Context(*context.valueCtx) 0xbeef000000000008
logger = github.com/heroiclabs/nakama-common/runtime.Logger(*github.com/heroiclabs/nakama/v3/server.RuntimeGoLogger) 0xbeef000000000108
db = ("*database/sql.DB")(0xc0004781a0)
nk = github.com/heroiclabs/nakama-common/runtime.NakamaModule(*github.com/heroiclabs/nakama/v3/server.RuntimeGoNakamaModule) 0xbeef000000000308
payload = "{\"hello\":\"world\"}"
~r0 = ""
~r1 = error nil

위에서 보면 일반적인 RPC 인수(컨텍스트, 로거, 데이터베이스 등)와 함께 RPC에 전달된 JSON 페이로드의 값도 볼 수 있습니다.

다음으로 실행 함수로 들어가서 로거 실행에 대한 호출을 확인한 다음 로컬 변수를 검사할 수 있습니다. nextlocals 명령을 사용하여 이를 수행할 수 있습니다.

1
2
3
4
(dlv) next
2022-06-30T13:18:38Z debug layer=debugger nexting
{"level":"debug","ts":"2022-06-30T13:18:38.878Z","caller":"nakama-server-sandbox/main.go:17","msg":"RpcTest RPC called","runtime":"go","rpc_id":"RpcTest"}
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:19 (PC: 0x7f819f58d830)

21행에 도달할 때까지 next을(를) 호출하여 계속 실행한 다음 locals을(를) 사용하여 로컬 변수를 검사합니다.

1
2
3
4
5
(dlv) next
2022-06-30T13:18:46Z debug layer=debugger nexting
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:21 (PC: 0x7f819f58d835)
(dlv) locals
payloadExists = false

next을(를) 사용하여 계속 실행하고 payloadExists 변경 값을 관찰해 보겠습니다.

1
2
3
4
5
(dlv) next
2022-06-30T13:22:57Z debug layer=debugger nexting
> heroiclabs.com/nakama-server-sandbox.RpcTest() heroiclabs.com.-server-sandbox/main.go:25 (PC: 0x7f819f58d84d)
(dlv) locals
payloadExists = true

여기에서 payloadExists의 값이 true으로 평가되어 해당 변수 값을 바꾸는 if 명령문으로 인해 변경되었음을 알 수 있습니다.

실행을 완전히 계속하고 continue을(를) 호출하여 RPC가 반환되도록 합니다.

1
(dlv) continue

이제 Nakama 콘솔 API 탐색기의 RPC에서 적절한 응답을 볼 수 있습니다.

1
2
3
{
  "payloadExists": true
}

추가 디버깅 #

위의 내용은 사용자 지정 서버 런타임 RPC 실행을 단계별로 실행하고 인수 및 로컬 변수를 검사하고 계속 실행하는 매우 기본적인 예를 보여줍니다.

Delve는 강력한 디버거이며 사용 가능한 명령에 대해 자세히 알아보려면 공식 Delve CLI 문서를 참조하세요.