Skip to content

Commit

Permalink
Docs: Counters with High Density
Browse files Browse the repository at this point in the history
Implements documentation for the High Density Integration pattern
using Counters.

Leaves in the "label locking" technique as well, as it is still a valid
technique!

Closes googleforgames#3655
  • Loading branch information
markmandel committed Jan 10, 2025
1 parent 97c1f51 commit 9f4153a
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 22 deletions.
3 changes: 2 additions & 1 deletion build/includes/website.mk
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ site-images: $(site_path)/static/diagrams/canary-testing.puml.png
site-images: $(site_path)/static/diagrams/allocation-player-capacity-tracking.puml.png
site-images: $(site_path)/static/diagrams/allocation-player-capacity-list.puml.png
site-images: $(site_path)/static/diagrams/reusing-gameservers.puml.png
site-images: $(site_path)/static/diagrams/high-density.puml.png
site-images: $(site_path)/static/diagrams/high-density-label-lock.puml.png
site-images: $(site_path)/static/diagrams/high-density-counters.puml.png

# generate pngs from dot files
%.dot.png: %.dot
Expand Down
115 changes: 94 additions & 21 deletions site/content/en/docs/Integration Patterns/high-density-gameservers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,104 @@ use of resources to run multiple concurrent game sessions from within a single `
The tradeoff here is that this requires more management on behalf of the integrated game server process and external
systems, since it works around the common Kubernetes and/or Agones container lifecycle.

Utilising the new allocation `gameServerState` filter as well as the existing ability to edit the
Here are two different approaches to solving this problem with Agones:

## Session/Room Counters

{{< beta title="Counters And Lists" gate="CountsAndLists" >}}

Utilising the allocation `gameServerState` filter as well as the new ability to add Counter capacity and counts to
`GameServer` records at both [allocation time]({{% ref "/docs/Reference/gameserverallocation.md" %}}), and from
within the game server process, [via the SDK][sdk-counter], means Agones is able to atomically track how many sessions
are available on a given a `GameServer` from the list of potentially Ready or Allocated `GameServers` when making an
allocation request.

By also using Counters, we can provide Agones the allocation metadata it needs to pack appropriately across the high
density `GameServer` instances as well.

<a href="../../../diagrams/high-density-counters.puml.png" target="_blank">
<img src="../../../diagrams/high-density-counters.puml.png" alt="High Density Allocation Diagram (Session Counters)" />
</a>

### Example `GameServerAllocation`

The below `Allocation` will first attempt to find a `GameServer` from the `Fleet` `simple-game-server` that is already
Allocated and also available capacity under the `rooms` Counter.

If an Allocated `GameServer` does not exist with available capacity, then use the next selector to allocate a Ready
`GameServer` from the `simple-game-server` `Fleet`.

Whichever condition is met, once allocation is made against a `GameServer`, the `rooms` Counter will be incremented by
one, thereby decrementing the available capacity of the `room` Counter on the `GameServer` instance. Once there is no
available capacity on the most full `GameServer`, the allocation will look for the next least full `GameServer` to
ensure packing across `GameServer` instances.

It will then be up to the game server process to decrement the `rooms` Counter via the SDK when a session comes to end,
to increase the amount of available capacity within the `GameServer` instance.

```yaml
apiVersion: allocation.agones.dev/v1
kind: GameServerAllocation
spec:
scheduling: Packed
priorities:
- type: Counter
key: rooms
order: Ascending # Ensures the least full "rooms" get allocated first.
selectors:
# Check if there is an already Allocated GameServer with room for at least one more session.
- gameServerState: Allocated
matchLabels:
agones.dev/fleet: simple-game-server
counters:
rooms:
minAvailable: 1
# If we can't find an Allocated GameServer, then go get a `Ready` `GameServer`.
- gameServerState: Ready
matchLabels:
agones.dev/fleet: simple-game-server
counters:
rooms:
minAvailable: 1 # not 100% necessary, since our Ready GameServers don't change their count value, but a good practice.
counters:
rooms:
action: Increment
amount: 1 # Bump up the room count by one on Allocation.
```
## GameServer Label Locking
Utilising the allocation `gameServerState` filter as well as the existing ability to edit the
`GameServer` labels at both [allocation time]({{% ref "/docs/Reference/gameserverallocation.md" %}}), and from
within the game server process, [via the SDK][sdk],
within the game server process, [via the SDK][sdk-label],
means Agones is able to atomically remove a `GameServer` from the list of potentially allocatable
`GameServers` at allocation time, and then return it back into the pool of allocatable `GameServers` if and when the
game server process deems that is has room to host another game session.
game server process deems that is has room to host another game session.

The downside to this approach is that there is no packing across re-allocated `GameServer` instances, but it is a very
flexible approach if utilising Counters or Lists is not a viable option.

<a href="../../../diagrams/high-density.puml.png" target="_blank">
<img src="../../../diagrams/high-density.puml.png" alt="High Density Allocation Diagram" />
<a href="../../../diagrams/high-density-label-lock.puml.png" target="_blank">
<img src="../../../diagrams/high-density-label-lock.puml.png" alt="High Density Allocation Diagram (Label Lock)" />
</a>

{{< alert title="Info" color="info">}}
To watch for Allocation events, there is the initial `GameServer.status.state` change from `Ready` to `Allocated`,
but it is also useful to know that the value of `GameServer.metadata.annotations["agones.dev/last-allocated"]` will
change as it is set by Agones with each allocation with the current timestamp, regardless of if there
change as it is set by Agones with each allocation with the current timestamp, regardless of if there
is a state change or not.
{{< /alert >}}

## Example `GameServerAllocation`
### Example `GameServerAllocation`

The below `Allocation` will first attempt to find a `GameServer` from the `Fleet` `simple-udp` that is already
The below `Allocation` will first attempt to find a `GameServer` from the `Fleet` `simple-game-server` that is already
Allocated and also has the label `agones.dev/sdk-gs-session-ready` with the value of `true`.

The above condition indicates that the matching game server process behind the matched `GameServer` record is able to
accept another game session at this time.

If an Allocated `GameServer` does not exist with the desired labels, then use the next selector to allocate a Ready
`GameServer` from the `simple-udp` `Fleet`.
`GameServer` from the `simple-game-server` `Fleet`.

Whichever condition is met, once allocation is made against a `GameServer`, its label of `agones.dev/sdk-gs-session-ready`
will be set to the value of `false` and it will no longer match the first selector, thereby removing it from any
Expand All @@ -56,34 +126,37 @@ kind: GameServerAllocation
spec:
selectors:
- matchLabels:
agones.dev/fleet: simple-udp
agones.dev/fleet: simple-game-server
agones.dev/sdk-gs-session-ready: "true" # this is important
gameServerState: Allocated # new state filter: allocate from Allocated servers
- matchLabels:
agones.dev/fleet: simple-udp
agones.dev/fleet: simple-game-server
gameServerState: Ready # Allocate out of the Ready Pool (which would be default, so backward compatible)
metadata:
labels:
agones.dev/sdk-gs-session-ready: "false" # this removes it from the pool
```

{{< alert title="Info" color="info">}}
{{% alert title="Info" color="info" %}}
It's important to note that the labels that the `GameServer` process use to add itself back into the pool of
allocatable instances, must start with the prefix `agones.dev/sdk-`, since only labels that have this prefix are
available to be [updated from the SDK][sdk].
{{< /alert >}}
available to be [updated from the SDK]({{% ref "/docs/Guides/Client SDKs/_index.md#setlabelkey-value" %}}).
{{% /alert %}}

## Consistency

Agones, and Kubernetes itself are built as eventually consistent, self-healing systems. To that end, it is worth
noting that there may be minor delays between each of the operations in the above flow. For example, depending on the
cluster load, it may take up to a second for an [SDK driven label change][sdk] on a `GameServer` record to be
visible to the Agones allocation system. We recommend building your integrations with Agones with this in mind.
Agones, and Kubernetes itself are built as eventually consistent, self-healing systems. To that end, it is worth
noting that there may be minor delays between each of the operations in either of the above flows. For example,
depending on the cluster load, it may take approximately a second for an SDK driven
[counter change][sdk-counter] or [label change][sdk-label] on a `GameServer` record to be visible to the Agones
allocation system. We recommend building your integrations with Agones with this in mind.

## Next Steps

* View the details about [using the SDK][sdk] to set
labels on the `GameServer`.
* Read the [Counters and Lists]({{< ref "/docs/Guides/counters-and-lists.md" >}}) guide.
* View the details about [using the SDK]({{% ref "/docs/Guides/Client SDKs/_index.md#setlabelkey-value" %}}) to change
values on the `GameServer`.
* Check all the options available on [`GameServerAllocation`]({{% ref "/docs/Reference/gameserverallocation.md" %}}).

[sdk]: {{% ref "/docs/Guides/Client SDKs/_index.md#setlabelkey-value" %}}
[sdk-label]: {{% ref "/docs/Guides/Client SDKs/_index.md#setlabelkey-value" %}}
[sdk-counter]: {{% ref "/docs/Guides/Client SDKs/_index.md#counters-and-lists" %}}
131 changes: 131 additions & 0 deletions site/static/diagrams/high-density-counters.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
@startuml
/'
Copyright 2024 Google LLC All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
'/

participant Matchmaker
participant Agones
participant "Game Server\nProcess" as Binary
participant SDK
participant "GameServer\nResource" as GameServer
box "Game Server Pod"
participant Binary
participant SDK
end box

== GameServer Start ==

Agones -> GameServer: GameServer created through\na <i>Fleet</i> configuration
note left
<i>Fleet</i> configuration includes a <i>rooms</i> Counter with an initial value of 0
and a capacity set to the total number of sessions that be can be hosted on a <i>GameServer</i>.
end note
activate GameServer
GameServer -> Binary: Agones creates a Pod with the\nconfigured Game Server Container
activate Binary
activate SDK
Binary -> SDK: SDK.WatchGameServer()
note right
Use the SDK Watch function
to watch and react to allocation
events
end note

Binary -> SDK: SDK.Ready()
note right
Call <i>Ready()</i> when the
Game Server can take player
connections and is able to
be allocated.
end note
GameServer <-- SDK: Update to <i>Ready</i> State

== No allocated <i>GameServers</i> ==

Matchmaker -> Agones: Create: <i>GameServerAllocation</i>
note left
The <i>GameServerAllocation</i> is implemented to
optionally select an already allocated <i>GameServer</i>
with a available capacity under Counter <i>rooms</i>
of 1. If one cannot be found, allocate a <i>Ready</i>
<i>GameServer</i> instead.

Since at this stage there are no Allocated <i>GameServer<i>
Agones will allocate a <i>Ready</i> GameServer.
end note
Agones -> GameServer: Finds a <i>Ready</i> <i>GameServer</i>.\n\nSets <i>status.state</i> to <i>Allocated</i> State\nand <i>status.counters["rooms"}.count++</i>\nand <i>metadata.annotations["agones.dev/last-allocated"] = current timestamp</i>
note left
By incrementing the <i>rooms</i> counter by one,
this reduces the available capacity for this
<i>GameServer</i> by one.
end note
Matchmaker <-- Agones : <i>GameServerAllocation</i> is returned\nwith <i>GameServer</i> details\nincluding IP and port to connect to.

SDK --> Binary: Sends SDK.WatchGameServer()\nevent for Allocation.
note right
This initial allocation can be determined
as a change in <i>GameServer.status.state</i>
from <i>Ready</i> to <i>Allocated</i>, as well as an
increment of one on
<i>GameServer.status.counters["rooms"].count</i>
end note

== Allocated <i>GameServers</i> with available capacity for more sessions ==

Matchmaker -> Agones: Create: <i>GameServerAllocation</i>
note left
The <i>GameServerAllocation</i> will this time find the
Allocated <i>GameServer</i> with the <i>rooms</i> Counter
with available capacity, inculcating it has room for
more game sessions.
end note
Agones -> GameServer: Finds the <i>Allocated</i> <i>GameServer</i> where\n<i>status["rooms"].count < status["rooms"].capacity</i>.\n\nSets <i>status.counters["rooms"}.count++</i>\nand <i>metadata.annotations["agones.dev/last-allocated"] = current timestamp</i>
note right
This is a <i>GameServer</i> that has room
for another concurrent game session.
end note
Matchmaker <-- Agones: returns <i>Allocated GameServer</i> record

SDK --> Binary: Sends SDK.WatchGameServer()\nevent for Allocation.
note right
The game server process can watch for an
increment of <i>status.counters["rooms"}.count</i> and/or
change in <i>metadata.annotations["agones.dev/last-allocated"]</i>
to determine if there is an allocation event.
end note

alt <i>GameServer</i> finishes a game session
Binary -> SDK: SDK.Beta().DecrementCounter("rooms", 1)
SDK --> GameServer: Sets <i>status["rooms"].count--</i>
end alt
note right
When a session ends, decrement the session counter
to increase the available capacity on this <i>GameServer</i>.
end note

== <i>GameServer</i> has completed <i>n</i> number of complete sessions ==

Binary -> SDK: SDK.Shutdown()
note left
The <i>GameServer</i> process tracks the total number of sessions
that it hosts, and after <i>n</i> number, calls <i>Shutdown()</i>
to delete the <i>GameServer</i> resource and backing Pod.
end note
SDK --> GameServer: Update to <i>Shutdown</i> state.
Agones -> GameServer: Deletes GameServer resource and backing Pod.
destroy Binary
destroy SDK
destroy GameServer
@enduml
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
File renamed without changes

0 comments on commit 9f4153a

Please sign in to comment.