1 of 29

Building high performance MO Game server

2016.04 GoCon�By @methane

2 of 29

About me

Twitter, github: methane

KLab Inc.

DB.SetConnMaxLifetime() author.� https://goo.gl/xGGkxd

3 of 29

Puzzle Wonderland

https://www.puzlan.com/https://www.youtube.com/watch?v=PLru7j4Q-1A

Turn based multiplay.

Dozens messages per second.

4 of 29

Backend

Game Server

Game Server

Game Server

Game Server

Lobby Server

Lobby Server

Lobby Server

MySQL

Websocket

ELB

http

5 of 29

Game Server

  • Room based messaging --- like chat
  • Commands
    • Send message to (all | others | owner)
      • Message is arbitrary binary data
    • Join, Leave, Kick
    • Room property, Player property
      • Key-Value

6 of 29

Lobby Server

  • Create new room
  • List rooms
    • Filter by room property

7 of 29

Sorry, it's not an OSS

I chose development speed and destructive changes.

8 of 29

Today's topic

Patterns and practices to build high performance network server

9 of 29

BTW, we use gb too

Pros:

Not require go15vendor. (Better tools support)

Cons:

Can't use quickfix (See gb#406)

10 of 29

Game Server

11 of 29

The Go Programming Language

http://www.gopl.io/

Pragmatic

Especially, Exercises are very pragmatic

Japanese version may be available

12 of 29

GOPL 8.10 Example: Chat Server

13 of 29

Step 1. Using buffered channel

Unbuffered channel is blocking when sending or receiving.

When there are many rooms, server can scale for CPU cores. But higher goroutine switching rate has significant performance penalty.

14 of 29

Problem: Nondeterministic order

chat-original/chat.go

broadcaster():

select {

case msg := <-messages:

for cli := range clients {

cli <- msg

}

case cli := <-entering:

clients[cli] = true

handleConn():

messages <- who + " has arrived"

entering <- ch

15 of 29

Solution: Command pattern

chat-step1/chat.go

broadcaster():

for cmd := range cmdQueue {

switch cmd := cmd.(type) {

case cmdEnter:

clients[cmd.c] = true

case cmdMessage:

for cli := range clients {

cli <- cmd.m

}

handleConn():

cmdQueue <- cmdMessage{who + " has arrived"}

cmdQueue <- cmdEnter{ch}

16 of 29

Benchmark

c4.8xlarge, 1room, 100 clients, 1000 msg/clients

before: 39sec

step1: 19.8sec

17 of 29

Step 2. Buffering I/O

I/O system call may be most heavy part of network server.

Read buffering is easier than Write buffering since Write buffering needs flushing.

Larger chunk size when high load. It makes easy to take a balance of latency and throughput.

18 of 29

GOPL chat server

Reader: Buffered already

input := bufio.NewScanner(conn)

for input.Scan() {

messages <- who + ": " + input.Text()

}

Writer: Not buffered

for msg := range ch {

fmt.Fprintln(conn, msg) // NOTE: ignoring network errors

}

19 of 29

chat-step2/chat.go

// restrict concurrent write

var wsem = make(chan struct{}, 2)

func clientWriter(conn net.Conn, ch <-chan string) {

var c chan struct{}

buf := bytes.Buffer{}

defer conn.Close()

for {

select {

case msg, ok := <-ch:

if !ok {

buf.WriteTo(conn)

return

}

fmt.Fprintln(&buf, msg)

c = wsem

case c <- struct{}{}:

_, err := buf.WriteTo(conn)

<-c

if err != nil {

return

}

if buf.Len() == 0 {

c = nil

}

}

20 of 29

Benchmark step2

before: 39sec

step1: 19.8sec

step2: 4.1sec

21 of 29

Lower packet/sec, frames/sec

  • server network performance
  • client network performance
  • client battery and CPU
  • better per-frame compression

22 of 29

Step 3. Coping with slow clients

When socket send buffer is full, write may block for a long time. It may affects to other clients.

  • Room blocks while sending message to channel when channel buffer is full.
  • Long blocked write() keeps counting semaphore we introduced previous step.

23 of 29

Solution

  • Room appends message to buffer directly.
  • Smarter Write() rate control
  • Use conn.SetWriteDeadline() to faster timeout.

Very difficult. I haven't establish "pattern".

24 of 29

Sorry, no sample code...

25 of 29

Other practices and patterns

26 of 29

Use pprof, especially goroutine profile

CPU profile is useful only when CPU usage is problem.

Goroutine profile (debug=1) is useful in many other cases.

  • Blocking point which can be bottleneck
  • Goroutine leak

27 of 29

Further reading about pprof

28 of 29

Zero time cache pattern

// N concurrent request : 1 concurrent query

// No consistency issue caused by cache lifetime

// This pattern is used to list rooms waiting new members.

now := time.Now()

c.Lock(); defer c.Unlock()

if now < last {

last = time.Now()

cached = query()

}

return cached

// see also cachedquery pkg in sample code repository

29 of 29

Thank you

INADA Naoki

@methane

KLab Inc.