通过Delve调试 #

本指南假定您已熟悉在Docker容器内运行Nakama。更多信息,请参阅通过Docker Compose安装Nakama文档。

main.go文件 #

我们在本指南中使用一个示例性main.go文件,此文件注册单个测试RPC函数,以便可以通过Delve进行调试。

 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进行调试的方式构建服务器运行时代码,我们需要制作一个Dockerfile,以确保在没有优化的情况下构建运行时代码和Nakama二进制都。

创建新的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.22.0 AS 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.22.0

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

ENTRYPOINT [ "/bin/bash" ]

此处有几个注意事项。在builder步骤中,我们安装了一些包,因此可以使用--gcflags "all=-N -l"标志构建Go运行时插件,有效禁用所得插件文件中的优化。

我们还使用RUN go install github.com/go-delve/delve/cmd/dlv@latest安装dlv。我们将在下一步将其复制到docker映像中。

在最终Docker步骤中,我们使用nakama-dsym映像而不是标准nakama映像。此映像提供的Nakama二进制文件中优化已禁用,因此适合与dlv一起运行,就像插件一样。

然后正常复制dlv二进制文件,以及服务器运行时插件和local.yml配置。

这里的另一个修改是我们覆盖了默认ENTRYPOINT,这样Nakama就不会尝试自动启动。我们可以docker exec到容器内,并自行运行dlv

添加Docker Compose文件 #

我们将用于调试的docker-compose.yml文件与通过Docker Compose安装Nakama文档中的标准文件相似,但我们做了一些调整。此处我们确保构建自己的Dockerfile映像,移除nakama服务器上的entrypoint属性,确保在运行容器时,Nakama不启动。我们还设置了在容器内成功运行dlv所需的一些值。这些属性是security_optstdin_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:

Exec’ing到Docker中 #

完成Docker配置文件后,我们现在将启动Nakama和Postgres容器:

1
docker compose up

伴随着我们容器的运行,打开一个新的终端窗口,并exec到Nakama docker容器中(其中,server-nakama-1是Nakama容器的名称)。

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

这会将您放到Nakama容器中的bash壳内。

手动迁移Nakama数据库 #

从这里开始,我们将首先确保迁移Postgres数据库以便与Nakama结合使用。

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列出。

对于本例,自定义服务器运行时有一个RpcTest函数,它记录传送到Nakama服务器控制台的消息,并返回一个JSON响应,该响应的值指示是否提供了有效负载。

验证函数的存在:

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)

通过调用next继续执行,直到到达第21行,然后使用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的值发生了变化,因为if语句求值为true,因此该变量的值发生变化。

我们调用continue,完全继续执行,让RPC返回。

1
(dlv) continue

现在,您应该在Nakama控制台API Explorer中看到来自RPC的适当响应。

1
2
3
{
  "payloadExists": true
}

进一步调试 #

上面演示了一个非常基本的示例,即逐步执行自定义服务器运行时RPC,检查参数与局部变量,并继续执行。

Delve是一个功能强大的调试器,如果您想进一步了解可用的命令,请查看Delve CLI官方文档