11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Open Matchのデータの流れを追う

Last updated at Posted at 2019-12-19

グレンジ Advent Calendar 2019 20日目担当の中陳です。

弊社ではゲームマッチングシステムとしてOpen Matchを導入しています。
Open MatchはGoogleが開発しているオープンソースのゲームマッチメイキングフレームワークです。

Open Matchのアーキテクチャの説明については、公式ドキュメントを参照してください。
https://open-match.dev/site/docs/guides/matchmaker/

今年の「Google Cloud INSIDE Games & Apps」で発表した資料もよければご覧ください。
https://storage.googleapis.com/gweb-cloudnext19.appspot.com/event-assets/tokyo/materials/D1-5-I08.pdf

Google Cloud Japan Customer Engineer Advent Calendar 2019」でも、12/22に「Open Match 超入門」が予定されているようなので、興味がある人はチェックしてみてください。

Open Matchの理解を深めるために、デモを動かしそのデータの流れを追ってみたいと思います。
データのやり取りはRedisを介して行われています。

準備

下記ドキュメントを参考に Core Open Match と Demo をインストールします。

# Install the core Open Match services.
kubectl apply --namespace open-match \
  -f https://open-match.dev/install/v0.8.0/yaml/01-open-match-core.yaml
kubectl apply --namespace open-match \
  -f https://open-match.dev/install/v0.8.0/yaml/06-open-match-override-configmap.yaml \
  -f https://open-match.dev/install/v0.8.0/yaml/07-open-match-default-evaluator.yaml
kubectl create namespace open-match-demo
kubectl apply --namespace open-match-demo \
  -f https://open-match.dev/install/v0.8.0/yaml/02-open-match-demo.yaml
kubectl port-forward --namespace open-match-demo service/om-demo 51507:51507

http://localhost:51507/ にアクセスすると、次のように表示されます。

{
  "clients": {
    "fakeplayer_0": {
      "Status": "Sleeping (pretend this is playing a match...)",
      "Assignment": {
        "connection": "85.98.86.16:2222"
      }
    },
    "fakeplayer_1": {
      "Status": "Main Menu",
      "Assignment": null
    },
    "fakeplayer_2": {
      "Status": "Waiting match with ticket Id bnrnjbh4tpl76e14h460",
      "Assignment": null
    },
    "fakeplayer_3": {
      "Status": "Sleeping (pretend this is playing a match...)",
      "Assignment": {
        "connection": "85.98.86.16:2222"
      }
    },
    "fakeplayer_4": {
      "Status": "Waiting match with ticket Id bnrnjf14tpl76e14h470",
      "Assignment": null
    }
  },
  "director": {
    "Status": "Sleeping",
    "LatestMatches": [
      {
        "match_id": "profile-1v1-time-2019-12-16T12:33:59.73-num-0",
        "match_profile": "1v1",
        "match_function": "a-simple-1v1-matchfunction",
        "tickets": [
          {
            "id": "bnrnjbp4tpl76e14h46g"
          },
          {
            "id": "bnrnjb14tpl76e14h45g"
          }
        ]
      }
    ]
  },
  "uptime": 1374
}

プレイヤ (Client) が Open Match (Frontend) に対してリクエストを送り、マッチが完了するのを待ちます。
Director は Open Match (Backend) マッチ実行をリクエストし、完了したマッチに対してゲームサーバーの割り当てを行います。

下記デモのソースコードをベースに追っていきます。

Redisのデータの流れの確認には、MONITOR コマンドを使います。

127.0.0.1:6379> MONITOR

Client → Open Match (Frontend)

チケット作成

CreateTicketでチケット作成を行います。

clients.go
	s.Status = "Creating Open Match Ticket"
	update(s)

	var ticketId string
	{
		req := &pb.CreateTicketRequest{
			Ticket: &pb.Ticket{},
		}

		resp, err := fe.CreateTicket(ctx, req)
		if err != nil {
			panic(err)
		}
		ticketId = resp.Ticket.Id
	}
1プレイヤ目
1576500156.947800 [0 10.1.13.146:55264] "MULTI"
1576500156.947826 [0 10.1.13.146:55264] "SET" "bnrnnf14tpl76e14h5a0" "\n\x14bnrnnf14tpl76e14h5a0"
1576500156.947901 [0 10.1.13.146:55264] "EXPIRE" "bnrnnf14tpl76e14h5a0" "43200"
1576500156.947906 [0 10.1.13.146:55264] "EXEC"
1576500156.948580 [0 10.1.13.146:55264] "MULTI"
1576500156.948589 [0 10.1.13.146:55264] "SADD" "ic$bnrnnf14tpl76e14h5a0" "allTickets"
1576500156.948737 [0 10.1.13.146:55264] "ZADD" "allTickets" "0" "bnrnnf14tpl76e14h5a0"
1576500156.948750 [0 10.1.13.146:55264] "EXEC"
2プレイヤ目
1576500159.893873 [0 10.1.13.146:59696] "MULTI"
1576500159.893893 [0 10.1.13.146:59696] "SET" "bnrnnfp4tpl76e14h5b0" "\n\x14bnrnnfp4tpl76e14h5b0"
1576500159.893903 [0 10.1.13.146:59696] "EXPIRE" "bnrnnfp4tpl76e14h5b0" "43200"
1576500159.893909 [0 10.1.13.146:59696] "EXEC"
1576500159.894889 [0 10.1.13.146:59696] "MULTI"
1576500159.894901 [0 10.1.13.146:59696] "SADD" "ic$bnrnnfp4tpl76e14h5b0" "allTickets"
1576500159.895011 [0 10.1.13.146:59696] "ZADD" "allTickets" "0" "bnrnnfp4tpl76e14h5b0"
1576500159.895022 [0 10.1.13.146:59696] "EXEC"

以下のデータが書き込まれています。

  • チケットIDのキーにチケット情報
  • チケットIDに紐付くインデックスキー
  • インデックスキー(ここではallTickets)にチケットIDと検索値

マッチ完了待ち

GetAssignmentsでマッチ結果を待ちます。
結果のConnection文字列が設定されいる場合にマッチ完了となります。

clients.go
	s.Status = fmt.Sprintf("Waiting match with ticket Id %s", ticketId)
	update(s)

	var assignment *pb.Assignment
	{
		req := &pb.GetAssignmentsRequest{
			TicketId: ticketId,
		}

		stream, err := fe.GetAssignments(ctx, req)
		for assignment.GetConnection() == "" {
			resp, err := stream.Recv()
			if err != nil {
				// For now we don't expect to get EOF, so that's still an error worthy of panic.
				panic(err)
			}

			assignment = resp.Assignment
		}

		err = stream.CloseSend()
		if err != nil {
			panic(err)
		}
	}
1プレイヤ目
1576500156.951293 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.051971 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.153294 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.254225 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
2プレイヤ目
1576500159.897335 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500159.998534 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500160.100184 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500160.200872 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"

チケットIDキーがループで参照されています。
チケットのAssignment情報に変更があった場合に、クライアントに返されます。

Director → Open Match (Backend)

マッチ実行

FetchMatchesを使って、マッチ実行及び、マッチ結果の受け取りを行います。
このデモでは、1v1の条件なしのマッチ実行を行っています。

director.go
	s.Status = "Match Match: Sending Request"
	ds.Update(s)

	var matches []*pb.Match
	{
		req := &pb.FetchMatchesRequest{
			Config: &pb.FunctionConfig{
				Host: "om-function.open-match-demo.svc.cluster.local",
				Port: 50502,
				Type: pb.FunctionConfig_GRPC,
			},
			Profiles: []*pb.MatchProfile{
				{
					Name: "1v1",
					Pools: []*pb.Pool{
						{
							Name: "Everyone",
						},
					},
				},
			},
		}

		stream, err := be.FetchMatches(ds.Ctx, req)
		if err != nil {
			panic(err)
		}

		for {
			resp, err := stream.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				panic(err)
			}
			matches = append(matches, resp.GetMatch())
		}
	}
プールからチケット取得
1576500160.702074 [0 10.1.13.148:52540] "ZRANGEBYSCORE" "proposed_ticket_ids" "1576500100701965766" "1576500160701965766"
1576500160.702482 [0 10.1.13.148:52540] "ZRANGEBYSCORE" "allTickets" "-Inf" "+Inf"
1576500160.703530 [0 10.1.13.148:52540] "MGET" "bnrnnf14tpl76e14h5a0" "bnrnnf94tpl76e14h5ag" "bnrnnfp4tpl76e14h5b0"

Frontendで書き込まれたインデックスキーから値の範囲を指定してチケット情報を取得しています。
ここでは検索条件がないので、allTicketsの全てを取得しています。
このデータがマッチングロジックに渡されます。

マッチ提案
1576500165.703594 [0 10.1.13.147:47004] "MULTI"
1576500165.703622 [0 10.1.13.147:47004] "ZADD" "proposed_ticket_ids" "1576500165703326682" "bnrnnfp4tpl76e14h5b0"
1576500165.703640 [0 10.1.13.147:47004] "ZADD" "proposed_ticket_ids" "1576500165703326682" "bnrnnf14tpl76e14h5a0"
1576500165.703664 [0 10.1.13.147:47004] "EXEC"

マッチングロジックから提案されたチケットIDが検索から除外されるように追加されています。

割り当て

マッチが完了したら、AssignTicketsでゲームサーバーの割り当てを行います。
ここではランダムなアドレスを設定しています。

director.go
	s.Status = "Assigning Players"
	ds.Update(s)

	for _, match := range matches {
		ids := []string{}

		for _, t := range match.Tickets {
			ids = append(ids, t.Id)
		}

		req := &pb.AssignTicketsRequest{
			TicketIds: ids,
			Assignment: &pb.Assignment{
				Connection: fmt.Sprintf("%d.%d.%d.%d:2222", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)),
			},
		}

		resp, err := be.AssignTickets(ds.Ctx, req)
		if err != nil {
			panic(err)
		}

		_ = resp
	}
割り当て
1576500165.730838 [0 10.1.13.157:35400] "GET" "bnrnnfp4tpl76e14h5b0"
1576500165.731933 [0 10.1.13.157:35400] "GET" "bnrnnf14tpl76e14h5a0"
1576500165.732428 [0 10.1.13.157:35400] "MULTI"
1576500165.732438 [0 10.1.13.157:35400] "SET" "bnrnnfp4tpl76e14h5b0" "\n\x14bnrnnfp4tpl76e14h5b0\x1a\x16\n\x14156.106.164.233:2222"
1576500165.732448 [0 10.1.13.157:35400] "EXPIRE" "bnrnnfp4tpl76e14h5b0" "43200"
1576500165.732453 [0 10.1.13.157:35400] "EXEC"
1576500165.733721 [0 10.1.13.157:35400] "MULTI"
1576500165.733735 [0 10.1.13.157:35400] "SET" "bnrnnf14tpl76e14h5a0" "\n\x14bnrnnf14tpl76e14h5a0\x1a\x16\n\x14156.106.164.233:2222"
1576500165.733769 [0 10.1.13.157:35400] "EXPIRE" "bnrnnf14tpl76e14h5a0" "43200"
1576500165.733779 [0 10.1.13.157:35400] "EXEC"

チケットIDキーに対して、割り当て情報を設定を行っています。

インデックス削除
1576500165.735677 [0 10.1.13.157:35396] "MULTI"
1576500165.735686 [0 10.1.13.157:35396] "EXEC"
1576500165.736289 [0 10.1.13.157:35396] "SMEMBERS" "ic$bnrnnfp4tpl76e14h5b0"
1576500165.736528 [0 10.1.13.157:35396] "MULTI"
1576500165.736541 [0 10.1.13.157:35396] "ZREM" "allTickets" "bnrnnfp4tpl76e14h5b0"
1576500165.736550 [0 10.1.13.157:35396] "DEL" "ic$bnrnnfp4tpl76e14h5b0"
1576500165.736554 [0 10.1.13.157:35396] "EXEC"
1576500165.737618 [0 10.1.13.157:35396] "SMEMBERS" "ic$bnrnnf14tpl76e14h5a0"
1576500165.738435 [0 10.1.13.157:35396] "MULTI"
1576500165.738445 [0 10.1.13.157:35396] "ZREM" "allTickets" "bnrnnf14tpl76e14h5a0"
1576500165.738452 [0 10.1.13.157:35396] "DEL" "ic$bnrnnf14tpl76e14h5a0"
1576500165.738457 [0 10.1.13.157:35396] "EXEC"
1576500165.739455 [0 10.1.13.157:35396] "MULTI"
1576500165.739465 [0 10.1.13.157:35396] "ZREM" "proposed_ticket_ids" "bnrnnfp4tpl76e14h5b0"
1576500165.739472 [0 10.1.13.157:35396] "ZREM" "proposed_ticket_ids" "bnrnnf14tpl76e14h5a0"
1576500165.739477 [0 10.1.13.157:35396] "EXEC"

割り当てが完了したチケットが検索対象にならないように、インデックスから削除を行っています。

最後に

デモを実際に動かして、簡単にですがデータの流れを追ってみました。
Open Matchの理解に少しでも役立てればと思います。

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?