├── .github
└── FUNDING.yml
├── .gitignore
├── 001
├── README.md
├── client.go
└── server.go
├── 002
├── README.md
├── client.go
├── go.mod
└── server.go
├── 003
├── README.md
├── client.go
├── go.mod
└── server.go
├── 004
├── README.md
├── backend.go
├── client.go
├── go.mod
└── server.go
├── 005
├── README.md
├── backend.go
├── client.go
├── go.mod
└── server.go
├── 006
├── README.md
├── backend.go
├── client.go
├── go.mod
└── server.go
├── 007
├── README.md
├── client.go
├── client.service
├── go.mod
├── server.go
├── server.service
└── terraform.tfvars
├── 008
├── README.md
├── client.go
├── client.service
├── go.mod
├── main.tf
├── server.go
├── server.service
└── terraform.tfvars
├── 009
├── README.md
├── backend.go
├── backend.service
├── client.go
├── client.service
├── go.mod
├── main.tf
├── server.go
├── server.service
└── terraform.tfvars
├── 010
├── Makefile
├── README.md
├── client.go
├── client.service
├── main.tf
├── server.c
├── server.service
├── server_xdp.c
└── terraform.tfvars
├── LICENSE
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: mas-bandwidth
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
23 | # grrr
24 | .DS_Store
25 | go.mod
--------------------------------------------------------------------------------
/001/README.md:
--------------------------------------------------------------------------------
1 | # 001
2 |
3 | First attempt.
4 |
5 | Keep it simple and just see how quickly we can naively send UDP packets from a client to server, and just reply with an 8 byte UDP packet containing the hash.
6 |
7 | To run:
8 |
9 | ```console
10 | go run server.go
11 | ```
12 |
13 | then in another terminal:
14 |
15 | ```console
16 | go run client.go
17 | ```
18 |
19 | Results:
20 |
21 | ```console
22 | gaffer@batman 001 % go run client.go
23 | starting 100 clients
24 | sent delta 9100, received delta 9100
25 | sent delta 9100, received delta 9100
26 | sent delta 9100, received delta 9100
27 | sent delta 9200, received delta 9200
28 | sent delta 9100, received delta 9100
29 | sent delta 9100, received delta 9100
30 | sent delta 9100, received delta 9100
31 | sent delta 9114, received delta 9100
32 | sent delta 9186, received delta 9192
33 | ^C
34 | received shutdown signal
35 | shutting down
36 | done.
37 | ```
38 |
39 | Results on localhost interface on an old iMacPro, I can easily do around 100 clients worth of packets without any drops (~10k packets per-second).
40 |
41 | Each client is on its own thread, and sleeps for 10ms before sending each packet. I believe we see ~9100 packets per-second sent because the sleeps run a bit long, on average.
42 |
43 | Somewhere around 400-500 clients I start to see some drops.
44 |
45 | At 1000 clients, I see signficant packet loss:
46 |
47 | ```console
48 | gaffer@batman 001 % go run client.go
49 | starting 1000 clients
50 | sent delta 99773, received delta 57616
51 | sent delta 98627, received delta 56149
52 | sent delta 98336, received delta 53192
53 | ^C
54 | received shutdown signal
55 | shutting down
56 | done.
57 | ```
58 |
--------------------------------------------------------------------------------
/001/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const NumClients = 100
17 | const MaxPacketSize = 1500
18 |
19 | var quit uint64
20 | var packetsSent uint64
21 | var packetsReceived uint64
22 |
23 | func ParseAddress(input string) net.UDPAddr {
24 | address := net.UDPAddr{}
25 | ip_string, port_string, err := net.SplitHostPort(input)
26 | if err != nil {
27 | address.IP = net.ParseIP(input)
28 | address.Port = 0
29 | return address
30 | }
31 | address.IP = net.ParseIP(ip_string)
32 | address.Port, _ = strconv.Atoi(port_string)
33 | return address
34 | }
35 |
36 | func main() {
37 |
38 | fmt.Printf("starting %d clients\n", NumClients)
39 |
40 | serverAddress := ParseAddress("127.0.0.1:40000")
41 |
42 | var wg sync.WaitGroup
43 |
44 | for i := 0; i < NumClients; i++ {
45 | go func(clientIndex int) {
46 | wg.Add(1)
47 | runClient(clientIndex, &serverAddress)
48 | wg.Done()
49 | }(i)
50 | }
51 |
52 | termChan := make(chan os.Signal, 1)
53 |
54 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
55 |
56 | ticker := time.NewTicker(time.Second)
57 |
58 | prev_sent := uint64(0)
59 | prev_received := uint64(0)
60 |
61 | for {
62 | select {
63 | case <-termChan:
64 | fmt.Printf("\nreceived shutdown signal\n")
65 | atomic.StoreUint64(&quit, 1)
66 | case <-ticker.C:
67 | sent := atomic.LoadUint64(&packetsSent)
68 | received := atomic.LoadUint64(&packetsReceived)
69 | sent_delta := sent - prev_sent
70 | received_delta := received - prev_received
71 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
72 | prev_sent = sent
73 | prev_received = received
74 | }
75 | quit := atomic.LoadUint64(&quit)
76 | if quit != 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("shutting down\n")
82 |
83 | wg.Wait()
84 |
85 | fmt.Printf("done.\n")
86 | }
87 |
88 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
89 |
90 | addr := net.UDPAddr{
91 | Port: 0,
92 | IP: net.ParseIP("127.0.0.1"),
93 | }
94 |
95 | conn, err := net.ListenUDP("udp", &addr)
96 | if err != nil {
97 | panic(fmt.Sprintf("could not create udp socket: %v", err))
98 | }
99 | defer conn.Close()
100 |
101 | buffer := make([]byte, MaxPacketSize)
102 |
103 | go func() {
104 | for {
105 | packetBytes, _, err := conn.ReadFromUDP(buffer)
106 | if err != nil {
107 | break
108 | }
109 | if packetBytes != 8 {
110 | continue
111 | }
112 | atomic.AddUint64(&packetsReceived, 1)
113 | }
114 | }()
115 |
116 | for {
117 | quit := atomic.LoadUint64(&quit)
118 | if quit != 0 {
119 | break
120 | }
121 | packetData := make([]byte, 100)
122 | rand.Read(packetData)
123 | conn.WriteToUDP(packetData[:], serverAddress)
124 | atomic.AddUint64(&packetsSent, 1)
125 | time.Sleep(time.Millisecond*10)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/001/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "hash/fnv"
7 | "encoding/binary"
8 | )
9 |
10 | const ServerPort = 40000
11 | const MaxPacketSize = 1500
12 |
13 | func main() {
14 |
15 | fmt.Printf("starting server on port %d\n", ServerPort)
16 |
17 | addr := net.UDPAddr{
18 | Port: ServerPort,
19 | IP: net.ParseIP("127.0.0.1"),
20 | }
21 |
22 | conn, err := net.ListenUDP("udp", &addr)
23 | if err != nil {
24 | panic(fmt.Sprintf("could not create udp socket: %v", err))
25 | }
26 | defer conn.Close()
27 |
28 | buffer := make([]byte, MaxPacketSize)
29 |
30 | for {
31 | packetBytes, from, err := conn.ReadFromUDP(buffer)
32 | if err != nil {
33 | fmt.Printf("udp receive error: %v\n", err)
34 | break
35 | }
36 | if packetBytes != 100 {
37 | continue
38 | }
39 | hash := fnv.New64a()
40 | hash.Write(buffer[:packetBytes])
41 | data := hash.Sum64()
42 | responsePacket := [8]byte{}
43 | binary.LittleEndian.PutUint64(responsePacket[:], data)
44 | conn.WriteToUDP(responsePacket[:], from)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/002/README.md:
--------------------------------------------------------------------------------
1 | # 002
2 |
3 | Second attempt.
4 |
5 | Increase socket send and receive buffer sizes to 2MB.
6 |
7 | https://medium.com/@CameronSparr/increase-os-udp-buffers-to-improve-performance-51d167bb1360
8 |
9 | To run:
10 |
11 | ```console
12 | go run server.go
13 | ```
14 |
15 | then in another terminal:
16 |
17 | ```console
18 | go run client.go
19 | ```
20 |
21 | Results:
22 |
23 | ```console
24 | gaffer@batman 002 % go run client.go
25 | starting 1000 clients
26 | sent delta 99583, received delta 54445
27 | sent delta 96432, received delta 57645
28 | sent delta 98198, received delta 54189
29 | sent delta 98059, received delta 56692
30 | sent delta 98449, received delta 55664
31 | sent delta 98163, received delta 54361
32 | ^C
33 | received shutdown signal
34 | shutting down
35 | done.
36 | ```
37 |
38 | No change. On MacOS it is not the socket buffer sizes. They're already big enough (at this small scale) by default.
39 |
--------------------------------------------------------------------------------
/002/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const NumClients = 1000
17 | const MaxPacketSize = 1500
18 |
19 | var quit uint64
20 | var packetsSent uint64
21 | var packetsReceived uint64
22 |
23 | func ParseAddress(input string) net.UDPAddr {
24 | address := net.UDPAddr{}
25 | ip_string, port_string, err := net.SplitHostPort(input)
26 | if err != nil {
27 | address.IP = net.ParseIP(input)
28 | address.Port = 0
29 | return address
30 | }
31 | address.IP = net.ParseIP(ip_string)
32 | address.Port, _ = strconv.Atoi(port_string)
33 | return address
34 | }
35 |
36 | func main() {
37 |
38 | fmt.Printf("starting %d clients\n", NumClients)
39 |
40 | serverAddress := ParseAddress("127.0.0.1:40000")
41 |
42 | var wg sync.WaitGroup
43 |
44 | for i := 0; i < NumClients; i++ {
45 | go func(clientIndex int) {
46 | wg.Add(1)
47 | runClient(clientIndex, &serverAddress)
48 | wg.Done()
49 | }(i)
50 | }
51 |
52 | termChan := make(chan os.Signal, 1)
53 |
54 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
55 |
56 | ticker := time.NewTicker(time.Second)
57 |
58 | prev_sent := uint64(0)
59 | prev_received := uint64(0)
60 |
61 | for {
62 | select {
63 | case <-termChan:
64 | fmt.Printf("\nreceived shutdown signal\n")
65 | atomic.StoreUint64(&quit, 1)
66 | case <-ticker.C:
67 | sent := atomic.LoadUint64(&packetsSent)
68 | received := atomic.LoadUint64(&packetsReceived)
69 | sent_delta := sent - prev_sent
70 | received_delta := received - prev_received
71 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
72 | prev_sent = sent
73 | prev_received = received
74 | }
75 | quit := atomic.LoadUint64(&quit)
76 | if quit != 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("shutting down\n")
82 |
83 | wg.Wait()
84 |
85 | fmt.Printf("done.\n")
86 | }
87 |
88 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
89 |
90 | addr := net.UDPAddr{
91 | Port: 0,
92 | IP: net.ParseIP("127.0.0.1"),
93 | }
94 |
95 | conn, err := net.ListenUDP("udp", &addr)
96 | if err != nil {
97 | panic(fmt.Sprintf("could not create udp socket: %v", err))
98 | }
99 | defer conn.Close()
100 |
101 | buffer := make([]byte, MaxPacketSize)
102 |
103 | go func() {
104 | for {
105 | packetBytes, _, err := conn.ReadFromUDP(buffer)
106 | if err != nil {
107 | break
108 | }
109 | if packetBytes != 8 {
110 | continue
111 | }
112 | atomic.AddUint64(&packetsReceived, 1)
113 | }
114 | }()
115 |
116 | for {
117 | quit := atomic.LoadUint64(&quit)
118 | if quit != 0 {
119 | break
120 | }
121 | packetData := make([]byte, 100)
122 | rand.Read(packetData)
123 | conn.WriteToUDP(packetData[:], serverAddress)
124 | atomic.AddUint64(&packetsSent, 1)
125 | time.Sleep(time.Millisecond*10)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/002/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/002
2 |
--------------------------------------------------------------------------------
/002/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "hash/fnv"
7 | "encoding/binary"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | )
12 |
13 | const ServerPort = 40000
14 | const MaxPacketSize = 1500
15 | const SocketBufferSize = 4*1024*1024
16 |
17 | func main() {
18 |
19 | fmt.Printf("starting server on port %d\n", ServerPort)
20 |
21 | addr := net.UDPAddr{
22 | Port: ServerPort,
23 | IP: net.ParseIP("127.0.0.1"),
24 | }
25 |
26 | conn, err := net.ListenUDP("udp", &addr)
27 | if err != nil {
28 | panic(fmt.Sprintf("could not create udp socket: %v", err))
29 | }
30 | defer conn.Close()
31 |
32 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
33 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
34 | }
35 |
36 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
37 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
38 | }
39 |
40 | buffer := make([]byte, MaxPacketSize)
41 |
42 | go func() {
43 | for {
44 | packetBytes, from, err := conn.ReadFromUDP(buffer)
45 | if err != nil {
46 | break
47 | }
48 | if packetBytes != 100 {
49 | continue
50 | }
51 | hash := fnv.New64a()
52 | hash.Write(buffer[:packetBytes])
53 | data := hash.Sum64()
54 | responsePacket := [8]byte{}
55 | binary.LittleEndian.PutUint64(responsePacket[:], data)
56 | conn.WriteToUDP(responsePacket[:], from)
57 | }
58 | }()
59 |
60 | termChan := make(chan os.Signal, 1)
61 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
62 | <-termChan
63 | }
64 |
--------------------------------------------------------------------------------
/003/README.md:
--------------------------------------------------------------------------------
1 | # 003
2 |
3 | Third attempt.
4 |
5 | SO_REUSEPORT
6 |
7 | https://medium.com/high-performance-network-programming/performance-optimisation-using-so-reuseport-c0fe4f2d3f88
8 |
9 | To run:
10 |
11 | ```console
12 | go get
13 | go run server.go
14 | ```
15 |
16 | then in another terminal:
17 |
18 | ```console
19 | go run client.go
20 | ```
21 |
22 | Results:
23 |
24 | ```console
25 | glenn@hulk:~/udp/003$ go run client.go
26 | starting 1000 clients
27 | sent delta 97545, received delta 97545
28 | sent delta 94000, received delta 94000
29 | sent delta 94110, received delta 94003
30 | sent delta 94559, received delta 94666
31 | sent delta 95927, received delta 95869
32 | sent delta 95014, received delta 95072
33 | sent delta 94000, received delta 93992
34 | sent delta 94000, received delta 94008
35 | ^C
36 | received shutdown signal
37 | shutting down
38 | done.
39 | ```
40 |
41 | Once again, no change.
42 |
43 | The reason is that we're sending all the packets from the same source IP address to the same dest IP address, so the hash is always the same, and they end up getting processed on one core only.
44 |
--------------------------------------------------------------------------------
/003/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const NumClients = 1000
17 | const MaxPacketSize = 1500
18 |
19 | var quit uint64
20 | var packetsSent uint64
21 | var packetsReceived uint64
22 |
23 | func ParseAddress(input string) net.UDPAddr {
24 | address := net.UDPAddr{}
25 | ip_string, port_string, err := net.SplitHostPort(input)
26 | if err != nil {
27 | address.IP = net.ParseIP(input)
28 | address.Port = 0
29 | return address
30 | }
31 | address.IP = net.ParseIP(ip_string)
32 | address.Port, _ = strconv.Atoi(port_string)
33 | return address
34 | }
35 |
36 | func main() {
37 |
38 | fmt.Printf("starting %d clients\n", NumClients)
39 |
40 | serverAddress := ParseAddress("127.0.0.1:40000")
41 |
42 | var wg sync.WaitGroup
43 |
44 | for i := 0; i < NumClients; i++ {
45 | go func(clientIndex int) {
46 | wg.Add(1)
47 | runClient(clientIndex, &serverAddress)
48 | wg.Done()
49 | }(i)
50 | }
51 |
52 | termChan := make(chan os.Signal, 1)
53 |
54 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
55 |
56 | ticker := time.NewTicker(time.Second)
57 |
58 | prev_sent := uint64(0)
59 | prev_received := uint64(0)
60 |
61 | for {
62 | select {
63 | case <-termChan:
64 | fmt.Printf("\nreceived shutdown signal\n")
65 | atomic.StoreUint64(&quit, 1)
66 | case <-ticker.C:
67 | sent := atomic.LoadUint64(&packetsSent)
68 | received := atomic.LoadUint64(&packetsReceived)
69 | sent_delta := sent - prev_sent
70 | received_delta := received - prev_received
71 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
72 | prev_sent = sent
73 | prev_received = received
74 | }
75 | quit := atomic.LoadUint64(&quit)
76 | if quit != 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("shutting down\n")
82 |
83 | wg.Wait()
84 |
85 | fmt.Printf("done.\n")
86 | }
87 |
88 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
89 |
90 | addr := net.UDPAddr{
91 | Port: 0,
92 | IP: net.ParseIP("127.0.0.1"),
93 | }
94 |
95 | conn, err := net.ListenUDP("udp", &addr)
96 | if err != nil {
97 | return
98 | }
99 | defer conn.Close()
100 |
101 | buffer := make([]byte, MaxPacketSize)
102 |
103 | go func() {
104 | for {
105 | packetBytes, _, err := conn.ReadFromUDP(buffer)
106 | if err != nil {
107 | break
108 | }
109 | if packetBytes != 8 {
110 | continue
111 | }
112 | atomic.AddUint64(&packetsReceived, 1)
113 | }
114 | }()
115 |
116 | for {
117 | quit := atomic.LoadUint64(&quit)
118 | if quit != 0 {
119 | break
120 | }
121 | packetData := make([]byte, 100)
122 | rand.Read(packetData)
123 | conn.WriteToUDP(packetData[:], serverAddress)
124 | atomic.AddUint64(&packetsSent, 1)
125 | time.Sleep(time.Millisecond*10)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/003/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/003
2 |
--------------------------------------------------------------------------------
/003/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "hash/fnv"
7 | "encoding/binary"
8 | "context"
9 | "os"
10 | "os/signal"
11 | "syscall"
12 | "golang.org/x/sys/unix"
13 | )
14 |
15 | const NumThreads = 64
16 | const ServerPort = 40000
17 | const MaxPacketSize = 1500
18 | const SocketBufferSize = 100*1024*1024
19 |
20 | func main() {
21 |
22 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
23 |
24 | for i := 0; i < NumThreads; i++ {
25 | go func(threadIndex int) {
26 | runServerThread(threadIndex)
27 | }(i)
28 | }
29 |
30 | termChan := make(chan os.Signal, 1)
31 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
32 | <-termChan
33 | }
34 |
35 | func runServerThread(threadIndex int) {
36 |
37 | lc := net.ListenConfig{
38 | Control: func(network string, address string, c syscall.RawConn) error {
39 | err := c.Control(func(fileDescriptor uintptr) {
40 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
41 | if err != nil {
42 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
43 | }
44 | })
45 | return err
46 | },
47 | }
48 |
49 | lp, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:40000")
50 | if err != nil {
51 | panic(fmt.Sprintf("could not bind socket: %v", err))
52 | }
53 |
54 | conn := lp.(*net.UDPConn)
55 |
56 | defer lp.Close()
57 |
58 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
59 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
60 | }
61 |
62 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
63 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
64 | }
65 |
66 | buffer := make([]byte, MaxPacketSize)
67 |
68 | for {
69 | packetBytes, from, err := conn.ReadFromUDP(buffer)
70 | if err != nil {
71 | break
72 | }
73 | if packetBytes != 100 {
74 | continue
75 | }
76 | hash := fnv.New64a()
77 | hash.Write(buffer[:packetBytes])
78 | data := hash.Sum64()
79 | responsePacket := [8]byte{}
80 | binary.LittleEndian.PutUint64(responsePacket[:], data)
81 | conn.WriteToUDP(responsePacket[:], from)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/004/README.md:
--------------------------------------------------------------------------------
1 | # 004
2 |
3 | Naive implementation of the server <-> backend over HTTP.
4 |
5 | To run:
6 |
7 | ```console
8 | go get
9 | go run backend.go
10 | ```
11 |
12 | in another terminal:
13 |
14 | ```console
15 | go run server.go
16 | ```
17 |
18 | in another terminal:
19 |
20 | ```console
21 | go run client.go
22 | ```
23 |
24 | Results:
25 |
26 | ```console
27 | glenn@hulk:~/udp/004$ go run client.go
28 | starting 1000 clients
29 | sent delta 89355, received delta 1502
30 | sent delta 74633, received delta 332
31 | sent delta 82034, received delta 166
32 | sent delta 84410, received delta 4
33 | sent delta 93893, received delta 0
34 | sent delta 93719, received delta 0
35 | sent delta 90368, received delta 0
36 | sent delta 82220, received delta 5
37 | sent delta 75389, received delta 24
38 | sent delta 64667, received delta 13
39 | sent delta 60097, received delta 179
40 | sent delta 52373, received delta 181
41 | sent delta 70531, received delta 47
42 | sent delta 83631, received delta 2
43 | sent delta 60504, received delta 351
44 | sent delta 60579, received delta 82
45 | sent delta 64227, received delta 75
46 | sent delta 61369, received delta 2
47 | sent delta 94000, received delta 0
48 | sent delta 94689, received delta 0
49 | sent delta 94000, received delta 0
50 | sent delta 94716, received delta 0
51 | sent delta 93285, received delta 0
52 | sent delta 94000, received delta 0
53 | sent delta 93835, received delta 0
54 | sent delta 94132, received delta 0
55 | sent delta 94035, received delta 0
56 | ^C
57 | received shutdown signal
58 | shutting down
59 | done.
60 | ```
61 |
62 | NOPE.
63 |
64 |
65 |
--------------------------------------------------------------------------------
/004/backend.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "io"
7 | "hash/fnv"
8 | "net/http"
9 | "encoding/binary"
10 | )
11 |
12 | const BackendPort = 50000
13 |
14 | func main() {
15 | fmt.Printf("starting backend on port %d\n", BackendPort)
16 | http.HandleFunc("/hash", hash)
17 | err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", BackendPort), nil)
18 | if err != nil {
19 | fmt.Printf("error: error starting http server: %v", err)
20 | os.Exit(1)
21 | }
22 | }
23 |
24 | func hash(w http.ResponseWriter, req *http.Request) {
25 | request, err := io.ReadAll(req.Body)
26 | if err != nil || len(request) != 100 {
27 | w.WriteHeader(http.StatusBadRequest)
28 | return
29 | }
30 | hash := fnv.New64a()
31 | hash.Write(request)
32 | data := hash.Sum64()
33 | response := [8]byte{}
34 | binary.LittleEndian.PutUint64(response[:], data)
35 | w.Write(response[:])
36 | return
37 | }
38 |
--------------------------------------------------------------------------------
/004/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const NumClients = 1000
17 | const MaxPacketSize = 1500
18 |
19 | var quit uint64
20 | var packetsSent uint64
21 | var packetsReceived uint64
22 |
23 | func ParseAddress(input string) net.UDPAddr {
24 | address := net.UDPAddr{}
25 | ip_string, port_string, err := net.SplitHostPort(input)
26 | if err != nil {
27 | address.IP = net.ParseIP(input)
28 | address.Port = 0
29 | return address
30 | }
31 | address.IP = net.ParseIP(ip_string)
32 | address.Port, _ = strconv.Atoi(port_string)
33 | return address
34 | }
35 |
36 | func main() {
37 |
38 | fmt.Printf("starting %d clients\n", NumClients)
39 |
40 | serverAddress := ParseAddress("127.0.0.1:40000")
41 |
42 | var wg sync.WaitGroup
43 |
44 | for i := 0; i < NumClients; i++ {
45 | go func(clientIndex int) {
46 | wg.Add(1)
47 | runClient(clientIndex, &serverAddress)
48 | wg.Done()
49 | }(i)
50 | }
51 |
52 | termChan := make(chan os.Signal, 1)
53 |
54 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
55 |
56 | ticker := time.NewTicker(time.Second)
57 |
58 | prev_sent := uint64(0)
59 | prev_received := uint64(0)
60 |
61 | for {
62 | select {
63 | case <-termChan:
64 | fmt.Printf("\nreceived shutdown signal\n")
65 | atomic.StoreUint64(&quit, 1)
66 | case <-ticker.C:
67 | sent := atomic.LoadUint64(&packetsSent)
68 | received := atomic.LoadUint64(&packetsReceived)
69 | sent_delta := sent - prev_sent
70 | received_delta := received - prev_received
71 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
72 | prev_sent = sent
73 | prev_received = received
74 | }
75 | quit := atomic.LoadUint64(&quit)
76 | if quit != 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("shutting down\n")
82 |
83 | wg.Wait()
84 |
85 | fmt.Printf("done.\n")
86 | }
87 |
88 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
89 |
90 | addr := net.UDPAddr{
91 | Port: 0,
92 | IP: net.ParseIP("127.0.0.1"),
93 | }
94 |
95 | conn, err := net.ListenUDP("udp", &addr)
96 | if err != nil {
97 | return
98 | }
99 | defer conn.Close()
100 |
101 | buffer := make([]byte, MaxPacketSize)
102 |
103 | go func() {
104 | for {
105 | packetBytes, _, err := conn.ReadFromUDP(buffer)
106 | if err != nil {
107 | break
108 | }
109 | if packetBytes != 8 {
110 | continue
111 | }
112 | atomic.AddUint64(&packetsReceived, 1)
113 | }
114 | }()
115 |
116 | for {
117 | quit := atomic.LoadUint64(&quit)
118 | if quit != 0 {
119 | break
120 | }
121 | packetData := make([]byte, 100)
122 | rand.Read(packetData)
123 | conn.WriteToUDP(packetData[:], serverAddress)
124 | atomic.AddUint64(&packetsSent, 1)
125 | time.Sleep(time.Millisecond*10)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/004/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/004
2 |
--------------------------------------------------------------------------------
/004/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "context"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "bytes"
12 | "net/http"
13 | "golang.org/x/sys/unix"
14 | )
15 |
16 | const BackendURL = "http://127.0.0.1:50000/hash"
17 | const NumThreads = 64
18 | const ServerPort = 40000
19 | const MaxPacketSize = 1500
20 | const SocketBufferSize = 100*1024*1024
21 |
22 | func main() {
23 |
24 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
25 |
26 | for i := 0; i < NumThreads; i++ {
27 | go func(threadIndex int) {
28 | runServerThread(threadIndex)
29 | }(i)
30 | }
31 |
32 | termChan := make(chan os.Signal, 1)
33 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
34 | <-termChan
35 | }
36 |
37 | func runServerThread(threadIndex int) {
38 |
39 | lc := net.ListenConfig{
40 | Control: func(network string, address string, c syscall.RawConn) error {
41 | err := c.Control(func(fileDescriptor uintptr) {
42 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
43 | if err != nil {
44 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
45 | }
46 | })
47 | return err
48 | },
49 | }
50 |
51 | lp, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:40000")
52 | if err != nil {
53 | panic(fmt.Sprintf("could not bind socket: %v", err))
54 | }
55 |
56 | conn := lp.(*net.UDPConn)
57 |
58 | defer lp.Close()
59 |
60 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
61 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
62 | }
63 |
64 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
65 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
66 | }
67 |
68 | buffer := make([]byte, MaxPacketSize)
69 |
70 | for {
71 | packetBytes, from, err := conn.ReadFromUDP(buffer)
72 | if err != nil {
73 | break
74 | }
75 | if packetBytes != 100 {
76 | continue
77 | }
78 | request := buffer[:packetBytes]
79 | go func() {
80 | response := PostBinary(BackendURL, request)
81 | if len(response) != 8 {
82 | return
83 | }
84 | conn.WriteToUDP(response[:], from)
85 | }()
86 | }
87 | }
88 |
89 | func PostBinary(url string, data []byte) []byte {
90 | buffer := bytes.NewBuffer(data)
91 | request, _ := http.NewRequest("POST", url, buffer)
92 | request.Header.Add("Content-Type", "application/octet-stream")
93 | httpClient := &http.Client{}
94 | response, err := httpClient.Do(request)
95 | if err != nil {
96 | fmt.Printf("post error: %v", err)
97 | return nil
98 | }
99 | if response.StatusCode != 200 {
100 | fmt.Printf("got response %d", response.StatusCode)
101 | return nil
102 | }
103 | body, error := io.ReadAll(response.Body)
104 | if error != nil {
105 | fmt.Printf("could not read response: %v", err)
106 | return nil
107 | }
108 | response.Body.Close()
109 | return body
110 | }
111 |
--------------------------------------------------------------------------------
/005/README.md:
--------------------------------------------------------------------------------
1 | # 005
2 |
3 | Reuse HTTP connections.
4 |
5 | https://blog.cubieserver.de/2022/http-connection-reuse-in-go-clients/
6 |
7 | To run:
8 |
9 | ```console
10 | go get
11 | go run backend.go
12 | ```
13 |
14 | in another terminal:
15 |
16 | ```console
17 | go run server.go
18 | ```
19 |
20 | in another terminal:
21 |
22 | ```console
23 | go run client.go
24 | ```
25 |
26 | Results:
27 |
28 | ```console
29 | glenn@hulk:~/udp/005$ go run client.go
30 | starting 1000 clients
31 | sent delta 95851, received delta 1401
32 | sent delta 95510, received delta 1445
33 | sent delta 95254, received delta 1419
34 | sent delta 95256, received delta 1423
35 | sent delta 94706, received delta 1469
36 | sent delta 95750, received delta 1342
37 | sent delta 94960, received delta 1433
38 | sent delta 94764, received delta 1418
39 | ^C
40 | received shutdown signal
41 | shutting down
42 | done.
43 | ```
44 |
45 | Better...
46 |
--------------------------------------------------------------------------------
/005/backend.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "io"
7 | "hash/fnv"
8 | "net/http"
9 | "encoding/binary"
10 | )
11 |
12 | const BackendPort = 50000
13 |
14 | func main() {
15 | fmt.Printf("starting backend on port %d\n", BackendPort)
16 | http.HandleFunc("/hash", hash)
17 | err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", BackendPort), nil)
18 | if err != nil {
19 | fmt.Printf("error: error starting http server: %v", err)
20 | os.Exit(1)
21 | }
22 | }
23 |
24 | func hash(w http.ResponseWriter, req *http.Request) {
25 | request, err := io.ReadAll(req.Body)
26 | if err != nil || len(request) != 100 {
27 | w.WriteHeader(http.StatusBadRequest)
28 | return
29 | }
30 | hash := fnv.New64a()
31 | hash.Write(request)
32 | data := hash.Sum64()
33 | response := [8]byte{}
34 | binary.LittleEndian.PutUint64(response[:], data)
35 | w.Write(response[:])
36 | return
37 | }
38 |
--------------------------------------------------------------------------------
/005/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const NumClients = 1000
17 | const MaxPacketSize = 1500
18 |
19 | var quit uint64
20 | var packetsSent uint64
21 | var packetsReceived uint64
22 |
23 | func ParseAddress(input string) net.UDPAddr {
24 | address := net.UDPAddr{}
25 | ip_string, port_string, err := net.SplitHostPort(input)
26 | if err != nil {
27 | address.IP = net.ParseIP(input)
28 | address.Port = 0
29 | return address
30 | }
31 | address.IP = net.ParseIP(ip_string)
32 | address.Port, _ = strconv.Atoi(port_string)
33 | return address
34 | }
35 |
36 | func main() {
37 |
38 | fmt.Printf("starting %d clients\n", NumClients)
39 |
40 | serverAddress := ParseAddress("127.0.0.1:40000")
41 |
42 | var wg sync.WaitGroup
43 |
44 | for i := 0; i < NumClients; i++ {
45 | go func(clientIndex int) {
46 | wg.Add(1)
47 | runClient(clientIndex, &serverAddress)
48 | wg.Done()
49 | }(i)
50 | }
51 |
52 | termChan := make(chan os.Signal, 1)
53 |
54 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
55 |
56 | ticker := time.NewTicker(time.Second)
57 |
58 | prev_sent := uint64(0)
59 | prev_received := uint64(0)
60 |
61 | for {
62 | select {
63 | case <-termChan:
64 | fmt.Printf("\nreceived shutdown signal\n")
65 | atomic.StoreUint64(&quit, 1)
66 | case <-ticker.C:
67 | sent := atomic.LoadUint64(&packetsSent)
68 | received := atomic.LoadUint64(&packetsReceived)
69 | sent_delta := sent - prev_sent
70 | received_delta := received - prev_received
71 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
72 | prev_sent = sent
73 | prev_received = received
74 | }
75 | quit := atomic.LoadUint64(&quit)
76 | if quit != 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("shutting down\n")
82 |
83 | wg.Wait()
84 |
85 | fmt.Printf("done.\n")
86 | }
87 |
88 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
89 |
90 | addr := net.UDPAddr{
91 | Port: 0,
92 | IP: net.ParseIP("127.0.0.1"),
93 | }
94 |
95 | conn, err := net.ListenUDP("udp", &addr)
96 | if err != nil {
97 | return
98 | }
99 | defer conn.Close()
100 |
101 | buffer := make([]byte, MaxPacketSize)
102 |
103 | go func() {
104 | for {
105 | packetBytes, _, err := conn.ReadFromUDP(buffer)
106 | if err != nil {
107 | break
108 | }
109 | if packetBytes != 8 {
110 | continue
111 | }
112 | atomic.AddUint64(&packetsReceived, 1)
113 | }
114 | }()
115 |
116 | for {
117 | quit := atomic.LoadUint64(&quit)
118 | if quit != 0 {
119 | break
120 | }
121 | packetData := make([]byte, 100)
122 | rand.Read(packetData)
123 | conn.WriteToUDP(packetData[:], serverAddress)
124 | atomic.AddUint64(&packetsSent, 1)
125 | time.Sleep(time.Millisecond*10)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/005/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/005
2 |
--------------------------------------------------------------------------------
/005/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "context"
7 | "io"
8 | "os"
9 | "time"
10 | "os/signal"
11 | "syscall"
12 | "bytes"
13 | "net/http"
14 | "golang.org/x/sys/unix"
15 | )
16 |
17 | const BackendURL = "http://127.0.0.1:50000/hash"
18 | const NumThreads = 64
19 | const ServerPort = 40000
20 | const MaxPacketSize = 1500
21 | const SocketBufferSize = 100*1024*1024
22 |
23 | func main() {
24 |
25 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
26 |
27 | for i := 0; i < NumThreads; i++ {
28 | go func(threadIndex int) {
29 | runServerThread(threadIndex)
30 | }(i)
31 | }
32 |
33 | termChan := make(chan os.Signal, 1)
34 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
35 | <-termChan
36 | }
37 |
38 | func runServerThread(threadIndex int) {
39 |
40 | httpTransport := http.Transport{MaxIdleConnsPerHost: 1000}
41 |
42 | lc := net.ListenConfig{
43 | Control: func(network string, address string, c syscall.RawConn) error {
44 | err := c.Control(func(fileDescriptor uintptr) {
45 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
46 | if err != nil {
47 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
48 | }
49 | })
50 | return err
51 | },
52 | }
53 |
54 | lp, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:40000")
55 | if err != nil {
56 | panic(fmt.Sprintf("could not bind socket: %v", err))
57 | }
58 |
59 | conn := lp.(*net.UDPConn)
60 |
61 | defer lp.Close()
62 |
63 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
64 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
65 | }
66 |
67 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
68 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
69 | }
70 |
71 | buffer := make([]byte, MaxPacketSize)
72 |
73 | for {
74 | packetBytes, from, err := conn.ReadFromUDP(buffer)
75 | if err != nil {
76 | break
77 | }
78 | if packetBytes != 100 {
79 | continue
80 | }
81 | request := buffer[:packetBytes]
82 | httpClient := &http.Client{Transport: &httpTransport, Timeout: 10 * time.Second}
83 | response := PostBinary(httpClient, BackendURL, request)
84 | if len(response) != 8 {
85 | return
86 | }
87 | conn.WriteToUDP(response[:], from)
88 | }
89 | }
90 |
91 | func PostBinary(httpClient *http.Client, url string, data []byte) []byte {
92 | buffer := bytes.NewBuffer(data)
93 | request, _ := http.NewRequest("POST", url, buffer)
94 | request.Header.Add("Content-Type", "application/octet-stream")
95 | response, err := httpClient.Do(request)
96 | if err != nil {
97 | return nil
98 | }
99 | defer response.Body.Close()
100 | if response.StatusCode != 200 {
101 | return nil
102 | }
103 | body, error := io.ReadAll(response.Body)
104 | if error != nil {
105 | return nil
106 | }
107 | return body
108 | }
109 |
--------------------------------------------------------------------------------
/006/README.md:
--------------------------------------------------------------------------------
1 | # 006
2 |
3 | Batch 1000 UDP packets per-http request.
4 |
5 | To run:
6 |
7 | ```console
8 | go get
9 | go run backend.go
10 | ```
11 |
12 | in another terminal:
13 |
14 | ```console
15 | go run server.go
16 | ```
17 |
18 | in another terminal:
19 |
20 | ```console
21 | go run client.go
22 | ```
23 |
24 | Results:
25 |
26 | ```console
27 | glenn@hulk:~/udp/006$ go run client.go
28 | starting 1000 clients
29 | sent delta 96544, received delta 54116
30 | sent delta 96771, received delta 92884
31 | sent delta 95436, received delta 95000
32 | sent delta 95456, received delta 94000
33 | sent delta 95951, received delta 97174
34 | sent delta 95473, received delta 93094
35 | sent delta 96212, received delta 96732
36 | sent delta 95523, received delta 104000
37 | sent delta 95964, received delta 94615
38 | sent delta 96412, received delta 96624
39 | sent delta 93194, received delta 81761
40 | sent delta 96762, received delta 102685
41 | sent delta 96001, received delta 100033
42 | sent delta 96520, received delta 91282
43 | sent delta 95962, received delta 98000
44 | sent delta 95105, received delta 95000
45 | sent delta 95542, received delta 97493
46 | sent delta 95896, received delta 94507
47 | sent delta 96073, received delta 94000
48 | sent delta 96125, received delta 99000
49 | ^C
50 | received shutdown signal
51 | shutting down
52 | done.
53 | ```
54 |
55 | As close as I can get. I'm out of ideas for making the HTTP stuff any faster. I think I'm hitting IO limits on my machine.
56 |
57 | It's time to start going horizontal for the clients, so we can have different source IP addresses hashing to different cores on the server.
58 |
59 |
--------------------------------------------------------------------------------
/006/backend.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "io"
7 | "hash/fnv"
8 | "net/http"
9 | "encoding/binary"
10 | )
11 |
12 | const BackendPort = 50000
13 | const RequestsPerBlock = 1000
14 | const RequestSize = 4 + 2 + 100
15 | const ResponseSize = 4 + 2 + 8
16 | const BlockSize = RequestsPerBlock * RequestSize
17 |
18 | func main() {
19 | fmt.Printf("starting backend on port %d\n", BackendPort)
20 | http.HandleFunc("/hash", hash)
21 | err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", BackendPort), nil)
22 | if err != nil {
23 | fmt.Printf("error: error starting http server: %v", err)
24 | os.Exit(1)
25 | }
26 | }
27 |
28 | func hash(w http.ResponseWriter, req *http.Request) {
29 | request, err := io.ReadAll(req.Body)
30 | if err != nil || len(request) != BlockSize {
31 | w.WriteHeader(http.StatusBadRequest)
32 | return
33 | }
34 | requestIndex := 0
35 | responseIndex := 0
36 | response := [ResponseSize*RequestsPerBlock]byte{}
37 | for i := 0; i < RequestsPerBlock; i++ {
38 | copy(response[responseIndex:responseIndex+6], request[requestIndex:requestIndex+6])
39 | hash := fnv.New64a()
40 | hash.Write(request)
41 | data := hash.Sum64()
42 | binary.LittleEndian.PutUint64(response[responseIndex+6:responseIndex+6+8], data)
43 | requestIndex += RequestSize
44 | responseIndex += ResponseSize
45 | }
46 | w.Write(response[:])
47 | }
48 |
--------------------------------------------------------------------------------
/006/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const NumClients = 1000
17 | const MaxPacketSize = 1500
18 |
19 | var quit uint64
20 | var packetsSent uint64
21 | var packetsReceived uint64
22 |
23 | func ParseAddress(input string) net.UDPAddr {
24 | address := net.UDPAddr{}
25 | ip_string, port_string, err := net.SplitHostPort(input)
26 | if err != nil {
27 | address.IP = net.ParseIP(input)
28 | address.Port = 0
29 | return address
30 | }
31 | address.IP = net.ParseIP(ip_string)
32 | address.Port, _ = strconv.Atoi(port_string)
33 | return address
34 | }
35 |
36 | func main() {
37 |
38 | fmt.Printf("starting %d clients\n", NumClients)
39 |
40 | serverAddress := ParseAddress("127.0.0.1:40000")
41 |
42 | var wg sync.WaitGroup
43 |
44 | for i := 0; i < NumClients; i++ {
45 | go func(clientIndex int) {
46 | wg.Add(1)
47 | runClient(clientIndex, &serverAddress)
48 | wg.Done()
49 | }(i)
50 | }
51 |
52 | termChan := make(chan os.Signal, 1)
53 |
54 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
55 |
56 | ticker := time.NewTicker(time.Second)
57 |
58 | prev_sent := uint64(0)
59 | prev_received := uint64(0)
60 |
61 | for {
62 | select {
63 | case <-termChan:
64 | fmt.Printf("\nreceived shutdown signal\n")
65 | atomic.StoreUint64(&quit, 1)
66 | case <-ticker.C:
67 | sent := atomic.LoadUint64(&packetsSent)
68 | received := atomic.LoadUint64(&packetsReceived)
69 | sent_delta := sent - prev_sent
70 | received_delta := received - prev_received
71 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
72 | prev_sent = sent
73 | prev_received = received
74 | }
75 | quit := atomic.LoadUint64(&quit)
76 | if quit != 0 {
77 | break
78 | }
79 | }
80 |
81 | fmt.Printf("shutting down\n")
82 |
83 | wg.Wait()
84 |
85 | fmt.Printf("done.\n")
86 | }
87 |
88 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
89 |
90 | addr := net.UDPAddr{
91 | Port: 0,
92 | IP: net.ParseIP("127.0.0.1"),
93 | }
94 |
95 | conn, err := net.ListenUDP("udp", &addr)
96 | if err != nil {
97 | return
98 | }
99 | defer conn.Close()
100 |
101 | buffer := make([]byte, MaxPacketSize)
102 |
103 | go func() {
104 | for {
105 | packetBytes, _, err := conn.ReadFromUDP(buffer)
106 | if err != nil {
107 | break
108 | }
109 | if packetBytes != 8 {
110 | continue
111 | }
112 | atomic.AddUint64(&packetsReceived, 1)
113 | }
114 | }()
115 |
116 | for {
117 | quit := atomic.LoadUint64(&quit)
118 | if quit != 0 {
119 | break
120 | }
121 | packetData := make([]byte, 100)
122 | rand.Read(packetData)
123 | conn.WriteToUDP(packetData[:], serverAddress)
124 | atomic.AddUint64(&packetsSent, 1)
125 | time.Sleep(time.Millisecond*10)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/006/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/006
2 |
--------------------------------------------------------------------------------
/006/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "context"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "time"
12 | "bytes"
13 | "net/http"
14 | "encoding/binary"
15 | "golang.org/x/sys/unix"
16 | )
17 |
18 | const NumThreads = 64
19 | const ServerPort = 40000
20 | const SocketBufferSize = 1024*1024*1024
21 | const RequestsPerBlock = 1000
22 | const RequestSize = 4 + 2 + 100
23 | const BlockSize = RequestsPerBlock * RequestSize
24 | const ResponseSize = 4 + 2 + 8
25 |
26 | var socket [NumThreads]*net.UDPConn
27 |
28 | var backendAddress net.UDPAddr
29 |
30 | func GetAddress(name string, defaultValue string) net.UDPAddr {
31 | valueString, ok := os.LookupEnv(name)
32 | if !ok {
33 | valueString = defaultValue
34 | }
35 | value, err := net.ResolveUDPAddr("udp", valueString)
36 | if err != nil {
37 | panic(fmt.Sprintf("invalid address in envvar %s", name))
38 | }
39 | return *value
40 | }
41 |
42 | func main() {
43 |
44 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
45 |
46 | backendAddress = GetAddress("BACKEND_ADDRESS", "127.0.0.1:50000")
47 |
48 | fmt.Printf("backend address is %s\n", backendAddress.String())
49 |
50 | for i := 0; i < NumThreads; i++ {
51 | createServerSocket(i)
52 | }
53 |
54 | for i := 0; i < NumThreads; i++ {
55 | go func(threadIndex int) {
56 | runServerThread(threadIndex)
57 | }(i)
58 | }
59 |
60 | termChan := make(chan os.Signal, 1)
61 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
62 | <-termChan
63 | }
64 |
65 | func createServerSocket(threadIndex int) {
66 |
67 | lc := net.ListenConfig{
68 | Control: func(network string, address string, c syscall.RawConn) error {
69 | err := c.Control(func(fileDescriptor uintptr) {
70 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
71 | if err != nil {
72 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
73 | }
74 | })
75 | return err
76 | },
77 | }
78 |
79 | lp, err := lc.ListenPacket(context.Background(), "udp", "0.0.0.0:40000")
80 | if err != nil {
81 | panic(fmt.Sprintf("could not bind socket: %v", err))
82 | }
83 |
84 | conn := lp.(*net.UDPConn)
85 |
86 | socket[threadIndex] = conn
87 | }
88 |
89 | func runServerThread(threadIndex int) {
90 |
91 | backendURL := fmt.Sprintf("http://%s/hash", backendAddress.String())
92 |
93 | conn := socket[threadIndex]
94 |
95 | defer conn.Close()
96 |
97 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
98 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
99 | }
100 |
101 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
102 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
103 | }
104 |
105 |
106 | index := 0
107 | block := make([]byte, BlockSize)
108 |
109 | for {
110 |
111 | if index == BlockSize {
112 | go func(request []byte) {
113 | httpClient := &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 1000}, Timeout: 10 * time.Second}
114 | response := PostBinary(httpClient, backendURL, request)
115 | if len(response) == ResponseSize * RequestsPerBlock {
116 | responseIndex := 0
117 | for i := 0; i < RequestsPerBlock; i++ {
118 | ip := response[responseIndex:responseIndex+4]
119 | port := binary.LittleEndian.Uint16(response[responseIndex+4:responseIndex+6])
120 | from := net.UDPAddr{IP: ip, Port: int(port)}
121 | socket[threadIndex].WriteToUDP(response[responseIndex+6:responseIndex+6+8], &from)
122 | responseIndex += ResponseSize
123 | }
124 | }
125 | }(block)
126 | block = make([]byte, BlockSize)
127 | index = 0
128 | }
129 |
130 | packetBytes, from, err := conn.ReadFromUDP(block[index+6:index+6+100])
131 | if err != nil {
132 | break
133 | }
134 |
135 | if packetBytes != 100 {
136 | continue
137 | }
138 |
139 | copy(block[index:], from.IP.To4())
140 |
141 | binary.LittleEndian.PutUint16(block[index+4:index+6], uint16(from.Port))
142 |
143 | index += RequestSize
144 | }
145 | }
146 |
147 | func PostBinary(client *http.Client, url string, data []byte) []byte {
148 | buffer := bytes.NewBuffer(data)
149 | request, _ := http.NewRequest("POST", url, buffer)
150 | request.Header.Add("Content-Type", "application/octet-stream")
151 | response, err := client.Do(request)
152 | if err != nil {
153 | fmt.Printf("error: posting request: %v\n", err)
154 | return nil
155 | }
156 | defer response.Body.Close()
157 | if response.StatusCode != 200 {
158 | fmt.Printf("error: status code is %d\n", response.StatusCode)
159 | return nil
160 | }
161 | body, error := io.ReadAll(response.Body)
162 | if error != nil {
163 | fmt.Printf("error: reading response: %v\n", error)
164 | return nil
165 | }
166 | return body
167 | }
168 |
--------------------------------------------------------------------------------
/007/README.md:
--------------------------------------------------------------------------------
1 | # 007
2 |
3 | Go to google cloud with terraform, run 10 n1-standard-8 VMs with 1000 clients each to get 10K players each against a c3-highcpu-176
4 |
5 | Remove the server <-> backend comms for the moment, and focus on the client to server UDP packets.
6 |
7 | To run:
8 |
9 | ```console
10 | terraform init
11 | terraform apply
12 | ```
13 |
14 | You'll need significant quota for n1 cores and c3 instances in your google cloud to be able to run. If you don't have it, edit the main.tf for different instance types.
15 |
16 | Result:
17 |
18 | 
19 |
20 | Even with the c3-highcpu-176 for the server, we can only get 10-25k clients max. Above this, UDP packets start to get dropped.
21 |
22 | You can see these drops on the server with:
23 |
24 | ```
25 | sudo apt install net-tools -y
26 | netstat -anus
27 | ```
28 |
29 | The problem is that with c3 class machines:
30 |
31 | "Using gVNIC, the maximum number of queues per vNIC is 16. If the calculated number is greater than 16, ignore the calculated number, and assign each vNIC 16 queues instead."
32 |
33 | https://cloud.google.com/compute/docs/network-bandwidth
34 |
35 | This means even though we have a massive amount of cores, only 16 are actively available to receive packets. When we overload these, UDP packets are dropped on receive, even though we are only using a fraction of the CPU available.
36 |
--------------------------------------------------------------------------------
/007/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const StartPort = 10000
17 | const MaxPacketSize = 1500
18 | const SocketBufferSize = 256*1024*1024
19 |
20 | var numClients int
21 |
22 | var quit uint64
23 | var packetsSent uint64
24 | var packetsReceived uint64
25 |
26 | func GetInt(name string, defaultValue int) int {
27 | valueString, ok := os.LookupEnv(name)
28 | if !ok {
29 | return defaultValue
30 | }
31 | value, err := strconv.ParseInt(valueString, 10, 64)
32 | if err != nil {
33 | return defaultValue
34 | }
35 | return int(value)
36 | }
37 |
38 | func GetAddress(name string, defaultValue string) net.UDPAddr {
39 | valueString, ok := os.LookupEnv(name)
40 | if !ok {
41 | valueString = defaultValue
42 | }
43 | value, err := net.ResolveUDPAddr("udp", valueString)
44 | if err != nil {
45 | panic(fmt.Sprintf("invalid address in envvar %s", name))
46 | }
47 | return *value
48 | }
49 |
50 | func main() {
51 |
52 | serverAddress := GetAddress("SERVER_ADDRESS", "127.0.0.1:40000")
53 |
54 | numClients = GetInt("NUM_CLIENTS", 1)
55 |
56 | fmt.Printf("starting %d clients\n", numClients)
57 |
58 | fmt.Printf("server address is %s\n", serverAddress.String())
59 |
60 | var wg sync.WaitGroup
61 |
62 | for i := 0; i < numClients; i++ {
63 | go func(clientIndex int) {
64 | wg.Add(1)
65 | runClient(clientIndex, &serverAddress)
66 | wg.Done()
67 | }(i)
68 | }
69 |
70 | termChan := make(chan os.Signal, 1)
71 |
72 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
73 |
74 | ticker := time.NewTicker(time.Second)
75 |
76 | prev_sent := uint64(0)
77 | prev_received := uint64(0)
78 |
79 | for {
80 | select {
81 | case <-termChan:
82 | fmt.Printf("\nreceived shutdown signal\n")
83 | atomic.StoreUint64(&quit, 1)
84 | case <-ticker.C:
85 | sent := atomic.LoadUint64(&packetsSent)
86 | received := atomic.LoadUint64(&packetsReceived)
87 | sent_delta := sent - prev_sent
88 | received_delta := received - prev_received
89 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
90 | prev_sent = sent
91 | prev_received = received
92 | }
93 | quit := atomic.LoadUint64(&quit)
94 | if quit != 0 {
95 | break
96 | }
97 | }
98 |
99 | fmt.Printf("shutting down\n")
100 |
101 | wg.Wait()
102 |
103 | fmt.Printf("done.\n")
104 | }
105 |
106 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
107 |
108 | addr := net.UDPAddr{
109 | Port: StartPort + clientIndex,
110 | IP: net.ParseIP("0.0.0.0"),
111 | }
112 |
113 | conn, err := net.ListenUDP("udp", &addr)
114 | if err != nil {
115 | return // IMPORTANT: to get as many clients as possible on one machine, if we can't bind to a specific port, just ignore and carry on
116 | }
117 | defer conn.Close()
118 |
119 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
120 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
121 | }
122 |
123 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
124 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
125 | }
126 |
127 | buffer := make([]byte, MaxPacketSize)
128 |
129 | go func() {
130 | for {
131 | packetBytes, _, err := conn.ReadFromUDP(buffer)
132 | if err != nil {
133 | break
134 | }
135 | if packetBytes != 8 {
136 | continue
137 | }
138 | atomic.AddUint64(&packetsReceived, 1)
139 | }
140 | }()
141 |
142 | packetData := make([]byte, 100)
143 |
144 | rand.Read(packetData)
145 |
146 | for {
147 | quit := atomic.LoadUint64(&quit)
148 | if quit != 0 {
149 | break
150 | }
151 | for i := 0; i < 10; i++ {
152 | conn.WriteToUDP(packetData[:], serverAddress)
153 | }
154 | atomic.AddUint64(&packetsSent, 10)
155 | time.Sleep(time.Millisecond*100)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/007/client.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Client Service
3 |
4 | [Service]
5 | ExecStart=/app/client
6 | EnvironmentFile=/app/client.env
7 | Restart=always
8 | RestartSec=30
9 | TimeoutStopSec=90
10 |
--------------------------------------------------------------------------------
/007/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/007
2 |
--------------------------------------------------------------------------------
/007/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "context"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "bytes"
12 | "net/http"
13 | "golang.org/x/sys/unix"
14 | )
15 |
16 | const NumThreads = 64
17 | const ServerPort = 40000
18 | const SocketBufferSize = 1024*1024*1024
19 |
20 | var socket [NumThreads]*net.UDPConn
21 |
22 | func main() {
23 |
24 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
25 |
26 | for i := 0; i < NumThreads; i++ {
27 | createServerSocket(i)
28 | }
29 |
30 | for i := 0; i < NumThreads; i++ {
31 | go func(threadIndex int) {
32 | runServerThread(threadIndex)
33 | }(i)
34 | }
35 |
36 | termChan := make(chan os.Signal, 1)
37 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
38 | <-termChan
39 | }
40 |
41 | func createServerSocket(threadIndex int) {
42 |
43 | lc := net.ListenConfig{
44 | Control: func(network string, address string, c syscall.RawConn) error {
45 | err := c.Control(func(fileDescriptor uintptr) {
46 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
47 | if err != nil {
48 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
49 | }
50 | })
51 | return err
52 | },
53 | }
54 |
55 | lp, err := lc.ListenPacket(context.Background(), "udp", "0.0.0.0:40000")
56 | if err != nil {
57 | panic(fmt.Sprintf("could not bind socket: %v", err))
58 | }
59 |
60 | conn := lp.(*net.UDPConn)
61 |
62 | socket[threadIndex] = conn
63 | }
64 |
65 | func runServerThread(threadIndex int) {
66 |
67 | conn := socket[threadIndex]
68 |
69 | defer conn.Close()
70 |
71 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
72 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
73 | }
74 |
75 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
76 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
77 | }
78 |
79 | buffer := make([]byte, 1500)
80 |
81 | for {
82 |
83 | packetBytes, from, err := conn.ReadFromUDP(buffer[:])
84 | if err != nil {
85 | break
86 | }
87 |
88 | if packetBytes != 100 {
89 | continue
90 | }
91 |
92 | var dummy [8]byte
93 | socket[threadIndex].WriteToUDP(dummy[:], from)
94 | }
95 | }
96 |
97 | func PostBinary(client *http.Client, url string, data []byte) []byte {
98 | buffer := bytes.NewBuffer(data)
99 | request, _ := http.NewRequest("POST", url, buffer)
100 | request.Header.Add("Content-Type", "application/octet-stream")
101 | response, err := client.Do(request)
102 | if err != nil {
103 | fmt.Printf("error: posting request: %v\n", err)
104 | return nil
105 | }
106 | defer response.Body.Close()
107 | if response.StatusCode != 200 {
108 | fmt.Printf("error: status code is %d\n", response.StatusCode)
109 | return nil
110 | }
111 | body, error := io.ReadAll(response.Body)
112 | if error != nil {
113 | fmt.Printf("error: reading response: %v\n", error)
114 | return nil
115 | }
116 | return body
117 | }
118 |
--------------------------------------------------------------------------------
/007/server.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Server Service
3 |
4 | [Service]
5 | ExecStart=/app/server
6 | Restart=always
7 | RestartSec=30
8 | TimeoutStopSec=90
9 |
--------------------------------------------------------------------------------
/007/terraform.tfvars:
--------------------------------------------------------------------------------
1 |
2 | google_org_id = "434699063105"
3 | google_billing_account = "012279-A33489-722F96"
4 | google_location = "US"
5 | google_region = "us-central1"
6 | google_zone = "us-central1-a"
7 | google_zones = ["us-central1-a"] # IMPORTANT: keep to a single zone to make sure bandwidth is free during load testing
8 |
9 | tag = "043" # increment this each time you want to recreate the VMs
10 |
--------------------------------------------------------------------------------
/008/README.md:
--------------------------------------------------------------------------------
1 | # 008
2 |
3 | Reduce server to c3-highcpu-44, since we only really get 16 threads per-NIC to receive packets with.
4 |
5 | To run:
6 |
7 | ```console
8 | terraform init
9 | terraform apply
10 | ```
11 |
12 | Result:
13 |
14 | We can still hit 10k - 25k clients, but at a much lower CPU cost.
15 |
--------------------------------------------------------------------------------
/008/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const StartPort = 10000
17 | const MaxPacketSize = 1500
18 | const SocketBufferSize = 256*1024*1024
19 |
20 | var numClients int
21 |
22 | var quit uint64
23 | var packetsSent uint64
24 | var packetsReceived uint64
25 |
26 | func GetInt(name string, defaultValue int) int {
27 | valueString, ok := os.LookupEnv(name)
28 | if !ok {
29 | return defaultValue
30 | }
31 | value, err := strconv.ParseInt(valueString, 10, 64)
32 | if err != nil {
33 | return defaultValue
34 | }
35 | return int(value)
36 | }
37 |
38 | func GetAddress(name string, defaultValue string) net.UDPAddr {
39 | valueString, ok := os.LookupEnv(name)
40 | if !ok {
41 | valueString = defaultValue
42 | }
43 | value, err := net.ResolveUDPAddr("udp", valueString)
44 | if err != nil {
45 | panic(fmt.Sprintf("invalid address in envvar %s", name))
46 | }
47 | return *value
48 | }
49 |
50 | func main() {
51 |
52 | serverAddress := GetAddress("SERVER_ADDRESS", "127.0.0.1:40000")
53 |
54 | numClients = GetInt("NUM_CLIENTS", 1)
55 |
56 | fmt.Printf("starting %d clients\n", numClients)
57 |
58 | fmt.Printf("server address is %s\n", serverAddress.String())
59 |
60 | var wg sync.WaitGroup
61 |
62 | for i := 0; i < numClients; i++ {
63 | go func(clientIndex int) {
64 | wg.Add(1)
65 | runClient(clientIndex, &serverAddress)
66 | wg.Done()
67 | }(i)
68 | }
69 |
70 | termChan := make(chan os.Signal, 1)
71 |
72 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
73 |
74 | ticker := time.NewTicker(time.Second)
75 |
76 | prev_sent := uint64(0)
77 | prev_received := uint64(0)
78 |
79 | for {
80 | select {
81 | case <-termChan:
82 | fmt.Printf("\nreceived shutdown signal\n")
83 | atomic.StoreUint64(&quit, 1)
84 | case <-ticker.C:
85 | sent := atomic.LoadUint64(&packetsSent)
86 | received := atomic.LoadUint64(&packetsReceived)
87 | sent_delta := sent - prev_sent
88 | received_delta := received - prev_received
89 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
90 | prev_sent = sent
91 | prev_received = received
92 | }
93 | quit := atomic.LoadUint64(&quit)
94 | if quit != 0 {
95 | break
96 | }
97 | }
98 |
99 | fmt.Printf("shutting down\n")
100 |
101 | wg.Wait()
102 |
103 | fmt.Printf("done.\n")
104 | }
105 |
106 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
107 |
108 | addr := net.UDPAddr{
109 | Port: StartPort + clientIndex,
110 | IP: net.ParseIP("0.0.0.0"),
111 | }
112 |
113 | conn, err := net.ListenUDP("udp", &addr)
114 | if err != nil {
115 | return // IMPORTANT: to get as many clients as possible on one machine, if we can't bind to a specific port, just ignore and carry on
116 | }
117 | defer conn.Close()
118 |
119 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
120 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
121 | }
122 |
123 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
124 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
125 | }
126 |
127 | buffer := make([]byte, MaxPacketSize)
128 |
129 | go func() {
130 | for {
131 | packetBytes, _, err := conn.ReadFromUDP(buffer)
132 | if err != nil {
133 | break
134 | }
135 | if packetBytes != 8 {
136 | continue
137 | }
138 | atomic.AddUint64(&packetsReceived, 1)
139 | }
140 | }()
141 |
142 | packetData := make([]byte, 100)
143 |
144 | rand.Read(packetData)
145 |
146 | for {
147 | quit := atomic.LoadUint64(&quit)
148 | if quit != 0 {
149 | break
150 | }
151 | for i := 0; i < 10; i++ {
152 | conn.WriteToUDP(packetData[:], serverAddress)
153 | }
154 | atomic.AddUint64(&packetsSent, 10)
155 | time.Sleep(time.Millisecond*100)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/008/client.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Client Service
3 |
4 | [Service]
5 | ExecStart=/app/client
6 | EnvironmentFile=/app/client.env
7 | Restart=always
8 | RestartSec=30
9 | TimeoutStopSec=90
10 |
--------------------------------------------------------------------------------
/008/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/008
2 |
--------------------------------------------------------------------------------
/008/main.tf:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------------------
2 |
3 | variable "google_org_id" { type = string }
4 | variable "google_billing_account" { type = string }
5 | variable "google_location" { type = string }
6 | variable "google_region" { type = string }
7 | variable "google_zones" { type = list(string) }
8 | variable "google_zone" { type = string }
9 |
10 | variable "tag" { type = string }
11 |
12 | # ----------------------------------------------------------------------------------------
13 |
14 | terraform {
15 | required_providers {
16 | google = {
17 | source = "hashicorp/google"
18 | version = "~> 5.0.0"
19 | }
20 | }
21 | }
22 |
23 | provider "google" {
24 | region = var.google_region
25 | zone = var.google_zone
26 | }
27 |
28 | # ----------------------------------------------------------------------------------------
29 |
30 | resource "random_id" "postfix" {
31 | byte_length = 8
32 | }
33 |
34 | resource "google_project" "udp" {
35 | name = "UDP Test"
36 | project_id = "udp-${random_id.postfix.hex}"
37 | org_id = var.google_org_id
38 | billing_account = var.google_billing_account
39 | }
40 |
41 | # ----------------------------------------------------------------------------------------
42 |
43 | locals {
44 | services = [
45 | "compute.googleapis.com", # compute engine
46 | "storage.googleapis.com", # cloud storage
47 | ]
48 | }
49 |
50 | resource "google_project_service" "udp" {
51 | count = length(local.services)
52 | project = google_project.udp.project_id
53 | service = local.services[count.index]
54 | timeouts {
55 | create = "30m"
56 | update = "40m"
57 | }
58 | disable_dependent_services = true
59 | }
60 |
61 | # ----------------------------------------------------------------------------------------
62 |
63 | data "local_file" "client_go" {
64 | filename = "client.go"
65 | }
66 |
67 | data "local_file" "client_service" {
68 | filename = "client.service"
69 | }
70 |
71 | data "local_file" "server_go" {
72 | filename = "server.go"
73 | }
74 |
75 | data "local_file" "server_service" {
76 | filename = "server.service"
77 | }
78 |
79 | data "local_file" "go_mod" {
80 | filename = "go.mod"
81 | }
82 |
83 | data "archive_file" "source_zip" {
84 | type = "zip"
85 | output_path = "source-${var.tag}.zip"
86 | source {
87 | filename = "client.go"
88 | content = data.local_file.client_go.content
89 | }
90 | source {
91 | filename = "client.service"
92 | content = data.local_file.client_service.content
93 | }
94 | source {
95 | filename = "server.go"
96 | content = data.local_file.server_go.content
97 | }
98 | source {
99 | filename = "server.service"
100 | content = data.local_file.server_service.content
101 | }
102 | source {
103 | filename = "go.mod"
104 | content = data.local_file.go_mod.content
105 | }
106 | }
107 |
108 | resource "google_storage_bucket" "source" {
109 | name = "${var.google_org_id}_udp_source"
110 | project = google_project.udp.project_id
111 | location = "US"
112 | force_destroy = true
113 | public_access_prevention = "enforced"
114 | uniform_bucket_level_access = true
115 | }
116 |
117 | resource "google_storage_bucket_object" "source_zip" {
118 | name = "source-${var.tag}.zip"
119 | source = "source-${var.tag}.zip"
120 | content_type = "application/zip"
121 | bucket = google_storage_bucket.source.id
122 | }
123 |
124 | # ----------------------------------------------------------------------------------------
125 |
126 | resource "google_service_account" "udp_runtime" {
127 | project = google_project.udp.project_id
128 | account_id = "udp-runtime"
129 | display_name = "UDP Runtime Service Account"
130 | }
131 |
132 | resource "google_project_iam_member" "udp_runtime_compute_viewer" {
133 | project = google_project.udp.project_id
134 | role = "roles/compute.viewer"
135 | member = google_service_account.udp_runtime.member
136 | }
137 |
138 | resource "google_storage_bucket_iam_member" "udp_runtime_storage_admin" {
139 | bucket = google_storage_bucket.source.name
140 | role = "roles/storage.objectAdmin"
141 | member = google_service_account.udp_runtime.member
142 | depends_on = [google_storage_bucket.source]
143 | }
144 |
145 | # ----------------------------------------------------------------------------------------
146 |
147 | resource "google_compute_network" "udp" {
148 | name = "udp"
149 | project = google_project.udp.project_id
150 | auto_create_subnetworks = false
151 | }
152 |
153 | resource "google_compute_subnetwork" "udp" {
154 | name = "udp"
155 | project = google_project.udp.project_id
156 | ip_cidr_range = "10.0.0.0/16"
157 | region = var.google_region
158 | network = google_compute_network.udp.id
159 | private_ip_google_access = true
160 | }
161 |
162 | resource "google_compute_router" "router" {
163 | name = "router-to-internet"
164 | network = google_compute_network.udp.id
165 | project = google_project.udp.project_id
166 | region = var.google_region
167 | }
168 |
169 | resource "google_compute_router_nat" "nat" {
170 | name = "nat"
171 | project = google_project.udp.project_id
172 | router = google_compute_router.router.name
173 | region = var.google_region
174 | nat_ip_allocate_option = "AUTO_ONLY"
175 | source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
176 | }
177 |
178 | resource "google_compute_firewall" "allow_ssh" {
179 | name = "allow-ssh"
180 | project = google_project.udp.project_id
181 | direction = "INGRESS"
182 | network = google_compute_network.udp.id
183 | source_ranges = ["130.211.0.0/22", "35.191.0.0/16", "35.235.240.0/20"]
184 | allow {
185 | protocol = "tcp"
186 | ports = ["22"]
187 | }
188 | target_tags = ["allow-ssh"]
189 | }
190 |
191 | resource "google_compute_firewall" "allow_http" {
192 | name = "allow-http"
193 | project = google_project.udp.project_id
194 | direction = "INGRESS"
195 | network = google_compute_network.udp.id
196 | source_ranges = ["0.0.0.0/0"]
197 | allow {
198 | protocol = "tcp"
199 | ports = ["50000"]
200 | }
201 | target_tags = ["allow-http"]
202 | }
203 |
204 | resource "google_compute_firewall" "allow_udp" {
205 | name = "allow-udp"
206 | project = google_project.udp.project_id
207 | direction = "INGRESS"
208 | network = google_compute_network.udp.id
209 | source_ranges = ["0.0.0.0/0"]
210 | allow {
211 | protocol = "udp"
212 | }
213 | target_tags = ["allow-udp"]
214 | }
215 |
216 | # ----------------------------------------------------------------------------------------
217 |
218 | resource "google_compute_instance_template" "client" {
219 |
220 | name = "client-${var.tag}"
221 |
222 | project = google_project.udp.project_id
223 |
224 | machine_type = "n1-standard-8"
225 |
226 | network_interface {
227 | network = google_compute_network.udp.id
228 | subnetwork = google_compute_subnetwork.udp.id
229 | access_config {}
230 | }
231 |
232 | tags = ["allow-ssh"]
233 |
234 | disk {
235 | source_image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
236 | auto_delete = true
237 | boot = true
238 | disk_type = "pd-ssd"
239 | }
240 |
241 | metadata = {
242 | startup-script = <<-EOF2
243 | #!/bin/bash
244 | NEEDRESTART_SUSPEND=1 apt update -y
245 | NEEDRESTART_SUSPEND=1 apt upgrade -y
246 | NEEDRESTART_SUSPEND=1 apt install golang-go unzip -y
247 | mkdir /app
248 | cd /app
249 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
250 | unzip *.zip
251 | export HOME=/app
252 | go get
253 | go build client.go
254 | cat < /app/client.env
255 | NUM_CLIENTS=1000
256 | SERVER_ADDRESS=${google_compute_instance.server.network_interface[0].network_ip}:40000
257 | EOF
258 | cat < /etc/sysctl.conf
259 | net.core.rmem_max=1000000000
260 | net.core.wmem_max=1000000000
261 | net.core.netdev_max_backlog=10000
262 | EOF
263 | sysctl -p
264 | cp client.service /etc/systemd/system/client.service
265 | systemctl daemon-reload
266 | systemctl start client.service
267 | EOF2
268 | }
269 |
270 | service_account {
271 | email = google_service_account.udp_runtime.email
272 | scopes = ["cloud-platform"]
273 | }
274 |
275 | lifecycle {
276 | create_before_destroy = true
277 | }
278 | }
279 |
280 | resource "google_compute_region_instance_group_manager" "client" {
281 | target_size = 10
282 | name = "client"
283 | project = google_project.udp.project_id
284 | region = var.google_region
285 | distribution_policy_zones = var.google_zones
286 | version {
287 | instance_template = google_compute_instance_template.client.id
288 | name = "primary"
289 | }
290 | base_instance_name = "client"
291 | update_policy {
292 | type = "PROACTIVE"
293 | minimal_action = "REPLACE"
294 | most_disruptive_allowed_action = "REPLACE"
295 | max_surge_fixed = 10
296 | max_unavailable_fixed = 0
297 | replacement_method = "SUBSTITUTE"
298 | }
299 | depends_on = [google_compute_instance_template.client]
300 | }
301 |
302 | # ----------------------------------------------------------------------------------------
303 |
304 | resource "google_compute_address" "server_address" {
305 | name = "server-${var.tag}-address"
306 | project = google_project.udp.project_id
307 | }
308 |
309 | resource "google_compute_instance" "server" {
310 |
311 | name = "server-${var.tag}"
312 | project = google_project.udp.project_id
313 | machine_type = "c3-highcpu-44"
314 | zone = var.google_zone
315 | tags = ["allow-ssh", "allow-udp"]
316 |
317 | allow_stopping_for_update = true
318 |
319 | boot_disk {
320 | initialize_params {
321 | image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
322 | }
323 | }
324 |
325 | network_interface {
326 | network = google_compute_network.udp.id
327 | subnetwork = google_compute_subnetwork.udp.id
328 | access_config {
329 | nat_ip = google_compute_address.server_address.address
330 | }
331 | }
332 |
333 | metadata = {
334 | startup-script = <<-EOF2
335 | #!/bin/bash
336 | NEEDRESTART_SUSPEND=1 apt update -y
337 | NEEDRESTART_SUSPEND=1 apt upgrade -y
338 | NEEDRESTART_SUSPEND=1 apt install golang-go unzip -y
339 | mkdir /app
340 | cd /app
341 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
342 | unzip *.zip
343 | export HOME=/app
344 | go get
345 | go build server.go
346 | cat < /etc/sysctl.conf
347 | net.core.rmem_max=1000000000
348 | net.core.wmem_max=1000000000
349 | net.core.netdev_max_backlog=10000
350 | EOF
351 | sysctl -p
352 | cp server.service /etc/systemd/system/server.service
353 | sysctl -w net.core.rmem_max=1000000000
354 | sysctl -w net.core.wmem_max=1000000000
355 | systemctl daemon-reload
356 | systemctl start server.service
357 | EOF2
358 | }
359 |
360 | service_account {
361 | email = google_service_account.udp_runtime.email
362 | scopes = ["cloud-platform"]
363 | }
364 | }
365 |
366 | # ----------------------------------------------------------------------------------------
367 |
--------------------------------------------------------------------------------
/008/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "context"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "bytes"
12 | "net/http"
13 | "golang.org/x/sys/unix"
14 | )
15 |
16 | const NumThreads = 64
17 | const ServerPort = 40000
18 | const SocketBufferSize = 1024*1024*1024
19 |
20 | var socket [NumThreads]*net.UDPConn
21 |
22 | func main() {
23 |
24 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
25 |
26 | for i := 0; i < NumThreads; i++ {
27 | createServerSocket(i)
28 | }
29 |
30 | for i := 0; i < NumThreads; i++ {
31 | go func(threadIndex int) {
32 | runServerThread(threadIndex)
33 | }(i)
34 | }
35 |
36 | termChan := make(chan os.Signal, 1)
37 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
38 | <-termChan
39 | }
40 |
41 | func createServerSocket(threadIndex int) {
42 |
43 | lc := net.ListenConfig{
44 | Control: func(network string, address string, c syscall.RawConn) error {
45 | err := c.Control(func(fileDescriptor uintptr) {
46 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
47 | if err != nil {
48 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
49 | }
50 | })
51 | return err
52 | },
53 | }
54 |
55 | lp, err := lc.ListenPacket(context.Background(), "udp", "0.0.0.0:40000")
56 | if err != nil {
57 | panic(fmt.Sprintf("could not bind socket: %v", err))
58 | }
59 |
60 | conn := lp.(*net.UDPConn)
61 |
62 | socket[threadIndex] = conn
63 | }
64 |
65 | func runServerThread(threadIndex int) {
66 |
67 | conn := socket[threadIndex]
68 |
69 | defer conn.Close()
70 |
71 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
72 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
73 | }
74 |
75 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
76 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
77 | }
78 |
79 | buffer := make([]byte, 1500)
80 |
81 | for {
82 |
83 | packetBytes, from, err := conn.ReadFromUDP(buffer[:])
84 | if err != nil {
85 | break
86 | }
87 |
88 | if packetBytes != 100 {
89 | continue
90 | }
91 |
92 | var dummy [8]byte
93 | socket[threadIndex].WriteToUDP(dummy[:], from)
94 | }
95 | }
96 |
97 | func PostBinary(client *http.Client, url string, data []byte) []byte {
98 | buffer := bytes.NewBuffer(data)
99 | request, _ := http.NewRequest("POST", url, buffer)
100 | request.Header.Add("Content-Type", "application/octet-stream")
101 | response, err := client.Do(request)
102 | if err != nil {
103 | fmt.Printf("error: posting request: %v\n", err)
104 | return nil
105 | }
106 | defer response.Body.Close()
107 | if response.StatusCode != 200 {
108 | fmt.Printf("error: status code is %d\n", response.StatusCode)
109 | return nil
110 | }
111 | body, error := io.ReadAll(response.Body)
112 | if error != nil {
113 | fmt.Printf("error: reading response: %v\n", error)
114 | return nil
115 | }
116 | return body
117 | }
118 |
--------------------------------------------------------------------------------
/008/server.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Server Service
3 |
4 | [Service]
5 | ExecStart=/app/server
6 | Restart=always
7 | RestartSec=30
8 | TimeoutStopSec=90
9 |
--------------------------------------------------------------------------------
/008/terraform.tfvars:
--------------------------------------------------------------------------------
1 |
2 | google_org_id = "434699063105"
3 | google_billing_account = "012279-A33489-722F96"
4 | google_location = "US"
5 | google_region = "us-central1"
6 | google_zone = "us-central1-a"
7 | google_zones = ["us-central1-a"] # IMPORTANT: keep to a single zone to make sure bandwidth is free during load testing
8 |
9 | tag = "046" # increment this each time you want to recreate the VMs
10 |
--------------------------------------------------------------------------------
/009/README.md:
--------------------------------------------------------------------------------
1 | # 009
2 |
3 | Bring back the server <-> backend HTTP comms in google cloud.
4 |
5 | To run:
6 |
7 | ```console
8 | terraform init
9 | terraform apply
10 | ```
11 |
12 | Result:
13 |
14 | BOOM.
15 |
16 |
17 |
18 | We're simply asking too much of the HTTP client. It can't keep up with the UDP packets.
19 |
20 | 10k clients, 100 packets per-second = 10,000 * 100 = 1,000,000 UDP packets per-second.
21 |
22 | We've already fixed it so it's not a HTTP requests per-second problem. Batching 1000 UDP packets, per-HTTP request, we turn it into 1000 requests per-second, but... each request is 1000 * 100 bytes = 100 kilobytes long.
23 |
24 | At this point it's clear this is an impossible problem. Or at least a problem that is waaaaaaaaaaaay outside the scope of anything reasonable to implement for a take home programmer test.
25 |
26 | It's entirely IO bound at this point, not anything we can fix by writing code. Indeed, attempting to write code past this point would be an exercise in frustration without understanding that it's IO bound.
27 |
28 | As to next steps, the logical next step is to fix the IO issue by adding a second virtual NIC to the server VM, and a virtual network and subnetwork just for HTTP traffic. This would fix the IO boundness, and then we have a solution that conservatively handles 10k clients with two c3-highcpu-44 VMs, one for the server and one for the backend.
29 |
30 | Since the expected load is 1M clients, we can then scale this horizontally to get an estimate of the cost to run the VMs in google cloud:
31 |
32 | Since we can do 10k clients with 2 c3-highcpu-44 machines, and we need 1M clients: we need 100 * 2 * c3-highcpu-44 = 200 * $2405.57 = $481,114 USD per-month just for VMs.
33 |
34 | We also need to consider egress bandwidth. It's roughly 10c per-GB egress. The response packets are just 8 bytes for the hash, but we need to add 28 bytes for IP and UDP header. Not sure if I should add ethernet header or not to the calculation, so let's just go with 36 bytes per-response UDP packet.
35 |
36 | 1M clients * 100 response packets per-second * 36 bytes = 100M * 36 bytes = 100,000,000 * 36 bytes/sec = 3,600,000,000 bytes/sec = 3.6GB/sec.
37 |
38 | 2,592,000 seconds in a month, so 2,592,000 * 3.6GB = 9,331,200 GB per-month.
39 |
40 | At $0.1 per-GB, we get an egress bandwidth charge of: $933,120 USD per-month.
41 |
42 | But this is not all, we also need to consider that we'll put a load balancer in front of the UDP servers. (Assume we can pair each server to a backend instance, so there are no load balancer costs between the server and backend).
43 |
44 | Ingress traffic to load balancers is billed at ~1c per-GB on google cloud.
45 |
46 | We have 28+100 bytes per-UDP packet, and 1M clients sending 100 packets per-second.
47 |
48 | 1M clients * 100 request packets per-second * 128 bytes = 12.8GB/sec.
49 |
50 | 2,592,000 seconds in a month, so 2,592,000 * 12.8GB = 9,331,200 GB per-month.
51 |
52 | Ingress traffic to the UDP load balancer is $331,776 USD per-month.
53 |
54 | Total cost: $1,746,010 USD per-month.
55 |
--------------------------------------------------------------------------------
/009/backend.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "io"
7 | "hash/fnv"
8 | "net/http"
9 | "encoding/binary"
10 | )
11 |
12 | const BackendPort = 50000
13 | const RequestsPerBlock = 100
14 | const RequestSize = 4 + 2 + 100
15 | const ResponseSize = 4 + 2 + 8
16 | const BlockSize = RequestsPerBlock * RequestSize
17 |
18 | func main() {
19 | fmt.Printf("starting backend on port %d\n", BackendPort)
20 | http.HandleFunc("/hash", hash)
21 | err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", BackendPort), nil)
22 | if err != nil {
23 | fmt.Printf("error: error starting http server: %v", err)
24 | os.Exit(1)
25 | }
26 | }
27 |
28 | func hash(w http.ResponseWriter, req *http.Request) {
29 | request, err := io.ReadAll(req.Body)
30 | if err != nil || len(request) != BlockSize {
31 | w.WriteHeader(http.StatusBadRequest)
32 | return
33 | }
34 | requestIndex := 0
35 | responseIndex := 0
36 | response := [ResponseSize*RequestsPerBlock]byte{}
37 | for i := 0; i < RequestsPerBlock; i++ {
38 | copy(response[responseIndex:responseIndex+6], request[requestIndex:requestIndex+6])
39 | hash := fnv.New64a()
40 | hash.Write(request)
41 | data := hash.Sum64()
42 | binary.LittleEndian.PutUint64(response[responseIndex+6:responseIndex+6+8], data)
43 | requestIndex += RequestSize
44 | responseIndex += ResponseSize
45 | }
46 | w.Write(response[:])
47 | }
48 |
--------------------------------------------------------------------------------
/009/backend.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Backend Service
3 |
4 | [Service]
5 | ExecStart=/app/backend
6 | EnvironmentFile=/app/server.env
7 | Restart=always
8 | RestartSec=30
9 | TimeoutStopSec=90
10 |
--------------------------------------------------------------------------------
/009/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const StartPort = 10000
17 | const MaxPacketSize = 1500
18 | const SocketBufferSize = 256*1024*1024
19 |
20 | var numClients int
21 |
22 | var quit uint64
23 | var packetsSent uint64
24 | var packetsReceived uint64
25 |
26 | func GetInt(name string, defaultValue int) int {
27 | valueString, ok := os.LookupEnv(name)
28 | if !ok {
29 | return defaultValue
30 | }
31 | value, err := strconv.ParseInt(valueString, 10, 64)
32 | if err != nil {
33 | return defaultValue
34 | }
35 | return int(value)
36 | }
37 |
38 | func GetAddress(name string, defaultValue string) net.UDPAddr {
39 | valueString, ok := os.LookupEnv(name)
40 | if !ok {
41 | valueString = defaultValue
42 | }
43 | value, err := net.ResolveUDPAddr("udp", valueString)
44 | if err != nil {
45 | panic(fmt.Sprintf("invalid address in envvar %s", name))
46 | }
47 | return *value
48 | }
49 |
50 | func main() {
51 |
52 | serverAddress := GetAddress("SERVER_ADDRESS", "127.0.0.1:40000")
53 |
54 | numClients = GetInt("NUM_CLIENTS", 1)
55 |
56 | fmt.Printf("starting %d clients\n", numClients)
57 |
58 | fmt.Printf("server address is %s\n", serverAddress.String())
59 |
60 | var wg sync.WaitGroup
61 |
62 | for i := 0; i < numClients; i++ {
63 | go func(clientIndex int) {
64 | wg.Add(1)
65 | runClient(clientIndex, &serverAddress)
66 | wg.Done()
67 | }(i)
68 | }
69 |
70 | termChan := make(chan os.Signal, 1)
71 |
72 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
73 |
74 | ticker := time.NewTicker(time.Second)
75 |
76 | prev_sent := uint64(0)
77 | prev_received := uint64(0)
78 |
79 | for {
80 | select {
81 | case <-termChan:
82 | fmt.Printf("\nreceived shutdown signal\n")
83 | atomic.StoreUint64(&quit, 1)
84 | case <-ticker.C:
85 | sent := atomic.LoadUint64(&packetsSent)
86 | received := atomic.LoadUint64(&packetsReceived)
87 | sent_delta := sent - prev_sent
88 | received_delta := received - prev_received
89 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
90 | prev_sent = sent
91 | prev_received = received
92 | }
93 | quit := atomic.LoadUint64(&quit)
94 | if quit != 0 {
95 | break
96 | }
97 | }
98 |
99 | fmt.Printf("shutting down\n")
100 |
101 | wg.Wait()
102 |
103 | fmt.Printf("done.\n")
104 | }
105 |
106 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
107 |
108 | addr := net.UDPAddr{
109 | Port: StartPort + clientIndex,
110 | IP: net.ParseIP("0.0.0.0"),
111 | }
112 |
113 | conn, err := net.ListenUDP("udp", &addr)
114 | if err != nil {
115 | return // IMPORTANT: to get as many clients as possible on one machine, if we can't bind to a specific port, just ignore and carry on
116 | }
117 | defer conn.Close()
118 |
119 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
120 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
121 | }
122 |
123 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
124 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
125 | }
126 |
127 | buffer := make([]byte, MaxPacketSize)
128 |
129 | go func() {
130 | for {
131 | packetBytes, _, err := conn.ReadFromUDP(buffer)
132 | if err != nil {
133 | break
134 | }
135 | if packetBytes != 8 {
136 | continue
137 | }
138 | atomic.AddUint64(&packetsReceived, 1)
139 | }
140 | }()
141 |
142 | packetData := make([]byte, 100)
143 |
144 | rand.Read(packetData)
145 |
146 | for {
147 | quit := atomic.LoadUint64(&quit)
148 | if quit != 0 {
149 | break
150 | }
151 | for i := 0; i < 10; i++ {
152 | conn.WriteToUDP(packetData[:], serverAddress)
153 | }
154 | atomic.AddUint64(&packetsSent, 10)
155 | time.Sleep(time.Millisecond*100)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/009/client.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Client Service
3 |
4 | [Service]
5 | ExecStart=/app/client
6 | EnvironmentFile=/app/client.env
7 | Restart=always
8 | RestartSec=30
9 | TimeoutStopSec=90
10 |
--------------------------------------------------------------------------------
/009/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mas-bandwidth/udp/008
2 |
--------------------------------------------------------------------------------
/009/main.tf:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------------------
2 |
3 | variable "google_org_id" { type = string }
4 | variable "google_billing_account" { type = string }
5 | variable "google_location" { type = string }
6 | variable "google_region" { type = string }
7 | variable "google_zones" { type = list(string) }
8 | variable "google_zone" { type = string }
9 |
10 | variable "tag" { type = string }
11 |
12 | # ----------------------------------------------------------------------------------------
13 |
14 | terraform {
15 | required_providers {
16 | google = {
17 | source = "hashicorp/google"
18 | version = "~> 5.0.0"
19 | }
20 | }
21 | }
22 |
23 | provider "google" {
24 | region = var.google_region
25 | zone = var.google_zone
26 | }
27 |
28 | # ----------------------------------------------------------------------------------------
29 |
30 | resource "random_id" "postfix" {
31 | byte_length = 8
32 | }
33 |
34 | resource "google_project" "udp" {
35 | name = "UDP Test"
36 | project_id = "udp-${random_id.postfix.hex}"
37 | org_id = var.google_org_id
38 | billing_account = var.google_billing_account
39 | }
40 |
41 | # ----------------------------------------------------------------------------------------
42 |
43 | locals {
44 | services = [
45 | "compute.googleapis.com", # compute engine
46 | "storage.googleapis.com", # cloud storage
47 | ]
48 | }
49 |
50 | resource "google_project_service" "udp" {
51 | count = length(local.services)
52 | project = google_project.udp.project_id
53 | service = local.services[count.index]
54 | timeouts {
55 | create = "30m"
56 | update = "40m"
57 | }
58 | disable_dependent_services = true
59 | }
60 |
61 | # ----------------------------------------------------------------------------------------
62 |
63 | data "local_file" "client_go" {
64 | filename = "client.go"
65 | }
66 |
67 | data "local_file" "client_service" {
68 | filename = "client.service"
69 | }
70 |
71 | data "local_file" "server_go" {
72 | filename = "server.go"
73 | }
74 |
75 | data "local_file" "server_service" {
76 | filename = "server.service"
77 | }
78 |
79 | data "local_file" "backend_go" {
80 | filename = "backend.go"
81 | }
82 |
83 | data "local_file" "backend_service" {
84 | filename = "backend.service"
85 | }
86 |
87 | data "local_file" "go_mod" {
88 | filename = "go.mod"
89 | }
90 |
91 | data "archive_file" "source_zip" {
92 | type = "zip"
93 | output_path = "source-${var.tag}.zip"
94 | source {
95 | filename = "client.go"
96 | content = data.local_file.client_go.content
97 | }
98 | source {
99 | filename = "client.service"
100 | content = data.local_file.client_service.content
101 | }
102 | source {
103 | filename = "server.go"
104 | content = data.local_file.server_go.content
105 | }
106 | source {
107 | filename = "server.service"
108 | content = data.local_file.server_service.content
109 | }
110 | source {
111 | filename = "backend.go"
112 | content = data.local_file.backend_go.content
113 | }
114 | source {
115 | filename = "backend.service"
116 | content = data.local_file.backend_service.content
117 | }
118 | source {
119 | filename = "go.mod"
120 | content = data.local_file.go_mod.content
121 | }
122 | }
123 |
124 | resource "google_storage_bucket" "source" {
125 | name = "${var.google_org_id}_udp_source"
126 | project = google_project.udp.project_id
127 | location = "US"
128 | force_destroy = true
129 | public_access_prevention = "enforced"
130 | uniform_bucket_level_access = true
131 | }
132 |
133 | resource "google_storage_bucket_object" "source_zip" {
134 | name = "source-${var.tag}.zip"
135 | source = "source-${var.tag}.zip"
136 | content_type = "application/zip"
137 | bucket = google_storage_bucket.source.id
138 | }
139 |
140 | # ----------------------------------------------------------------------------------------
141 |
142 | resource "google_service_account" "udp_runtime" {
143 | project = google_project.udp.project_id
144 | account_id = "udp-runtime"
145 | display_name = "UDP Runtime Service Account"
146 | }
147 |
148 | resource "google_project_iam_member" "udp_runtime_compute_viewer" {
149 | project = google_project.udp.project_id
150 | role = "roles/compute.viewer"
151 | member = google_service_account.udp_runtime.member
152 | }
153 |
154 | resource "google_storage_bucket_iam_member" "udp_runtime_storage_admin" {
155 | bucket = google_storage_bucket.source.name
156 | role = "roles/storage.objectAdmin"
157 | member = google_service_account.udp_runtime.member
158 | depends_on = [google_storage_bucket.source]
159 | }
160 |
161 | # ----------------------------------------------------------------------------------------
162 |
163 | resource "google_compute_network" "udp" {
164 | name = "udp"
165 | project = google_project.udp.project_id
166 | auto_create_subnetworks = false
167 | }
168 |
169 | resource "google_compute_subnetwork" "udp" {
170 | name = "udp"
171 | project = google_project.udp.project_id
172 | ip_cidr_range = "10.0.0.0/16"
173 | region = var.google_region
174 | network = google_compute_network.udp.id
175 | private_ip_google_access = true
176 | }
177 |
178 | resource "google_compute_router" "router" {
179 | name = "router-to-internet"
180 | network = google_compute_network.udp.id
181 | project = google_project.udp.project_id
182 | region = var.google_region
183 | }
184 |
185 | resource "google_compute_router_nat" "nat" {
186 | name = "nat"
187 | project = google_project.udp.project_id
188 | router = google_compute_router.router.name
189 | region = var.google_region
190 | nat_ip_allocate_option = "AUTO_ONLY"
191 | source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
192 | }
193 |
194 | resource "google_compute_firewall" "allow_ssh" {
195 | name = "allow-ssh"
196 | project = google_project.udp.project_id
197 | direction = "INGRESS"
198 | network = google_compute_network.udp.id
199 | source_ranges = ["130.211.0.0/22", "35.191.0.0/16", "35.235.240.0/20"]
200 | allow {
201 | protocol = "tcp"
202 | ports = ["22"]
203 | }
204 | target_tags = ["allow-ssh"]
205 | }
206 |
207 | resource "google_compute_firewall" "allow_http" {
208 | name = "allow-http"
209 | project = google_project.udp.project_id
210 | direction = "INGRESS"
211 | network = google_compute_network.udp.id
212 | source_ranges = ["0.0.0.0/0"]
213 | allow {
214 | protocol = "tcp"
215 | ports = ["50000"]
216 | }
217 | target_tags = ["allow-http"]
218 | }
219 |
220 | resource "google_compute_firewall" "allow_udp" {
221 | name = "allow-udp"
222 | project = google_project.udp.project_id
223 | direction = "INGRESS"
224 | network = google_compute_network.udp.id
225 | source_ranges = ["0.0.0.0/0"]
226 | allow {
227 | protocol = "udp"
228 | }
229 | target_tags = ["allow-udp"]
230 | }
231 |
232 | # ----------------------------------------------------------------------------------------
233 |
234 | resource "google_compute_instance_template" "client" {
235 |
236 | name = "client-${var.tag}"
237 |
238 | project = google_project.udp.project_id
239 |
240 | machine_type = "n1-standard-8"
241 |
242 | network_interface {
243 | network = google_compute_network.udp.id
244 | subnetwork = google_compute_subnetwork.udp.id
245 | access_config {}
246 | }
247 |
248 | tags = ["allow-ssh"]
249 |
250 | disk {
251 | source_image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
252 | auto_delete = true
253 | boot = true
254 | disk_type = "pd-ssd"
255 | }
256 |
257 | metadata = {
258 | startup-script = <<-EOF2
259 | #!/bin/bash
260 | NEEDRESTART_SUSPEND=1 apt update -y
261 | NEEDRESTART_SUSPEND=1 apt upgrade -y
262 | NEEDRESTART_SUSPEND=1 apt install golang-go unzip -y
263 | mkdir /app
264 | cd /app
265 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
266 | unzip *.zip
267 | export HOME=/app
268 | go get
269 | go build client.go
270 | cat < /app/client.env
271 | NUM_CLIENTS=1000
272 | SERVER_ADDRESS=${google_compute_instance.server.network_interface[0].network_ip}:40000
273 | EOF
274 | cat < /etc/sysctl.conf
275 | net.core.rmem_max=1000000000
276 | net.core.wmem_max=1000000000
277 | net.core.netdev_max_backlog=10000
278 | EOF
279 | sysctl -p
280 | cp client.service /etc/systemd/system/client.service
281 | systemctl daemon-reload
282 | systemctl start client.service
283 | EOF2
284 | }
285 |
286 | service_account {
287 | email = google_service_account.udp_runtime.email
288 | scopes = ["cloud-platform"]
289 | }
290 |
291 | lifecycle {
292 | create_before_destroy = true
293 | }
294 | }
295 |
296 | resource "google_compute_region_instance_group_manager" "client" {
297 | target_size = 10
298 | name = "client"
299 | project = google_project.udp.project_id
300 | region = var.google_region
301 | distribution_policy_zones = var.google_zones
302 | version {
303 | instance_template = google_compute_instance_template.client.id
304 | name = "primary"
305 | }
306 | base_instance_name = "client"
307 | update_policy {
308 | type = "PROACTIVE"
309 | minimal_action = "REPLACE"
310 | most_disruptive_allowed_action = "REPLACE"
311 | max_surge_fixed = 10
312 | max_unavailable_fixed = 0
313 | replacement_method = "SUBSTITUTE"
314 | }
315 | depends_on = [google_compute_instance_template.client]
316 | }
317 |
318 | # ----------------------------------------------------------------------------------------
319 |
320 | resource "google_compute_address" "server_address" {
321 | name = "server-${var.tag}-address"
322 | project = google_project.udp.project_id
323 | }
324 |
325 | resource "google_compute_instance" "server" {
326 |
327 | name = "server-${var.tag}"
328 | project = google_project.udp.project_id
329 | machine_type = "c3-highcpu-44"
330 | zone = var.google_zone
331 | tags = ["allow-ssh", "allow-udp"]
332 |
333 | allow_stopping_for_update = true
334 |
335 | boot_disk {
336 | initialize_params {
337 | image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
338 | }
339 | }
340 |
341 | network_interface {
342 | network = google_compute_network.udp.id
343 | subnetwork = google_compute_subnetwork.udp.id
344 | access_config {
345 | nat_ip = google_compute_address.server_address.address
346 | }
347 | }
348 |
349 | metadata = {
350 | startup-script = <<-EOF2
351 | #!/bin/bash
352 | NEEDRESTART_SUSPEND=1 apt update -y
353 | NEEDRESTART_SUSPEND=1 apt upgrade -y
354 | NEEDRESTART_SUSPEND=1 apt install golang-go unzip -y
355 | mkdir /app
356 | cd /app
357 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
358 | unzip *.zip
359 | export HOME=/app
360 | go get
361 | go build server.go
362 | cat < /app/server.env
363 | BACKEND_ADDRESS=${google_compute_instance.backend.network_interface[0].network_ip}:40000
364 | EOF
365 | cat < /etc/sysctl.conf
366 | net.core.rmem_max=1000000000
367 | net.core.wmem_max=1000000000
368 | net.core.netdev_max_backlog=10000
369 | EOF
370 | sysctl -p
371 | cp server.service /etc/systemd/system/server.service
372 | systemctl daemon-reload
373 | systemctl start server.service
374 | EOF2
375 | }
376 |
377 | service_account {
378 | email = google_service_account.udp_runtime.email
379 | scopes = ["cloud-platform"]
380 | }
381 | }
382 |
383 | # ----------------------------------------------------------------------------------------
384 |
385 | resource "google_compute_address" "backend_address" {
386 | name = "backend-${var.tag}-address"
387 | project = google_project.udp.project_id
388 | }
389 |
390 | resource "google_compute_instance" "backend" {
391 |
392 | name = "backend-${var.tag}"
393 | project = google_project.udp.project_id
394 | machine_type = "c3-highcpu-44"
395 | zone = var.google_zone
396 | tags = ["allow-ssh", "allow-udp"]
397 |
398 | allow_stopping_for_update = true
399 |
400 | boot_disk {
401 | initialize_params {
402 | image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
403 | }
404 | }
405 |
406 | network_interface {
407 | network = google_compute_network.udp.id
408 | subnetwork = google_compute_subnetwork.udp.id
409 | access_config {
410 | nat_ip = google_compute_address.backend_address.address
411 | }
412 | }
413 |
414 | metadata = {
415 | startup-script = <<-EOF2
416 | #!/bin/bash
417 | NEEDRESTART_SUSPEND=1 apt update -y
418 | NEEDRESTART_SUSPEND=1 apt upgrade -y
419 | NEEDRESTART_SUSPEND=1 apt install golang-go unzip -y
420 | mkdir /app
421 | cd /app
422 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
423 | unzip *.zip
424 | export HOME=/app
425 | go get
426 | go build backend.go
427 | cat < /etc/sysctl.conf
428 | net.core.rmem_max=1000000000
429 | net.core.wmem_max=1000000000
430 | net.core.netdev_max_backlog=10000
431 | EOF
432 | sysctl -p
433 | cp backend.service /etc/systemd/system/backend.service
434 | systemctl daemon-reload
435 | systemctl start backend.service
436 | EOF2
437 | }
438 |
439 | service_account {
440 | email = google_service_account.udp_runtime.email
441 | scopes = ["cloud-platform"]
442 | }
443 | }
444 |
445 | # ----------------------------------------------------------------------------------------
446 |
--------------------------------------------------------------------------------
/009/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "context"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "time"
12 | "encoding/binary"
13 | "bytes"
14 | "net/http"
15 | "golang.org/x/sys/unix"
16 | )
17 |
18 | const NumThreads = 64
19 | const ServerPort = 40000
20 | const SocketBufferSize = 1024*1024*1024
21 | const RequestsPerBlock = 1000
22 | const RequestSize = 4 + 2 + 100
23 | const BlockSize = RequestsPerBlock * RequestSize
24 | const ResponseSize = 4 + 2 + 8
25 |
26 | var socket [NumThreads]*net.UDPConn
27 |
28 | var backendAddress net.UDPAddr
29 |
30 | func GetAddress(name string, defaultValue string) net.UDPAddr {
31 | valueString, ok := os.LookupEnv(name)
32 | if !ok {
33 | valueString = defaultValue
34 | }
35 | value, err := net.ResolveUDPAddr("udp", valueString)
36 | if err != nil {
37 | panic(fmt.Sprintf("invalid address in envvar %s", name))
38 | }
39 | return *value
40 | }
41 |
42 | func main() {
43 |
44 | fmt.Printf("starting %d server threads on port %d\n", NumThreads, ServerPort)
45 |
46 | backendAddress = GetAddress("BACKEND_ADDRESS", "127.0.0.1:50000")
47 |
48 | fmt.Printf("backend address is %s\n", backendAddress.String())
49 |
50 | for i := 0; i < NumThreads; i++ {
51 | createServerSocket(i)
52 | }
53 |
54 | for i := 0; i < NumThreads; i++ {
55 | go func(threadIndex int) {
56 | runServerThread(threadIndex)
57 | }(i)
58 | }
59 |
60 | termChan := make(chan os.Signal, 1)
61 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
62 | <-termChan
63 | }
64 |
65 | func createServerSocket(threadIndex int) {
66 |
67 | lc := net.ListenConfig{
68 | Control: func(network string, address string, c syscall.RawConn) error {
69 | err := c.Control(func(fileDescriptor uintptr) {
70 | err := unix.SetsockoptInt(int(fileDescriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
71 | if err != nil {
72 | panic(fmt.Sprintf("failed to set reuse port socket option: %v", err))
73 | }
74 | })
75 | return err
76 | },
77 | }
78 |
79 | lp, err := lc.ListenPacket(context.Background(), "udp", "0.0.0.0:40000")
80 | if err != nil {
81 | panic(fmt.Sprintf("could not bind socket: %v", err))
82 | }
83 |
84 | conn := lp.(*net.UDPConn)
85 |
86 | socket[threadIndex] = conn
87 | }
88 |
89 | func runServerThread(threadIndex int) {
90 |
91 | backendURL := fmt.Sprintf("http://%s/hash", backendAddress.String())
92 |
93 | conn := socket[threadIndex]
94 |
95 | defer conn.Close()
96 |
97 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
98 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
99 | }
100 |
101 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
102 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
103 | }
104 |
105 | index := 0
106 | block := make([]byte, BlockSize)
107 |
108 | for {
109 |
110 | if index == BlockSize {
111 | go func(request []byte) {
112 | httpClient := &http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 1000}, Timeout: 10 * time.Second}
113 | response := PostBinary(httpClient, backendURL, request)
114 | if len(response) == ResponseSize * RequestsPerBlock {
115 | responseIndex := 0
116 | for i := 0; i < RequestsPerBlock; i++ {
117 | ip := response[responseIndex:responseIndex+4]
118 | port := binary.LittleEndian.Uint16(response[responseIndex+4:responseIndex+6])
119 | from := net.UDPAddr{IP: ip, Port: int(port)}
120 | socket[threadIndex].WriteToUDP(response[responseIndex+6:responseIndex+6+8], &from)
121 | responseIndex += ResponseSize
122 | }
123 | }
124 | }(block)
125 | block = make([]byte, BlockSize)
126 | index = 0
127 | }
128 |
129 | packetBytes, from, err := conn.ReadFromUDP(block[index+6:index+6+100])
130 | if err != nil {
131 | break
132 | }
133 |
134 | if packetBytes != 100 {
135 | continue
136 | }
137 |
138 | copy(block[index:], from.IP.To4())
139 |
140 | binary.LittleEndian.PutUint16(block[index+4:index+6], uint16(from.Port))
141 |
142 | index += RequestSize
143 | }
144 | }
145 |
146 | func PostBinary(client *http.Client, url string, data []byte) []byte {
147 | buffer := bytes.NewBuffer(data)
148 | request, _ := http.NewRequest("POST", url, buffer)
149 | request.Header.Add("Content-Type", "application/octet-stream")
150 | response, err := client.Do(request)
151 | if err != nil {
152 | fmt.Printf("error: posting request: %v\n", err)
153 | return nil
154 | }
155 | defer response.Body.Close()
156 | if response.StatusCode != 200 {
157 | fmt.Printf("error: status code is %d\n", response.StatusCode)
158 | return nil
159 | }
160 | body, error := io.ReadAll(response.Body)
161 | if error != nil {
162 | fmt.Printf("error: reading response: %v\n", error)
163 | return nil
164 | }
165 | return body
166 | }
167 |
--------------------------------------------------------------------------------
/009/server.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Server Service
3 |
4 | [Service]
5 | ExecStart=/app/server
6 | Restart=always
7 | RestartSec=30
8 | TimeoutStopSec=90
9 |
--------------------------------------------------------------------------------
/009/terraform.tfvars:
--------------------------------------------------------------------------------
1 |
2 | google_org_id = "434699063105"
3 | google_billing_account = "012279-A33489-722F96"
4 | google_location = "US"
5 | google_region = "us-central1"
6 | google_zone = "us-central1-a"
7 | google_zones = ["us-central1-a"] # IMPORTANT: keep to a single zone to make sure bandwidth is free during load testing
8 |
9 | tag = "049" # increment this each time you want to recreate the VMs
10 |
--------------------------------------------------------------------------------
/010/Makefile:
--------------------------------------------------------------------------------
1 |
2 | KERNEL = $(shell uname -r)
3 |
4 | .PHONY: build
5 | build: server.c server_xdp.o
6 | gcc -O2 -g server.c -o server -lxdp /usr/src/linux-headers-$(KERNEL)/tools/bpf/resolve_btfids/libbpf/libbpf.a -lz -lelf
7 |
8 | server_xdp.o: server_xdp.c
9 | clang -O2 -g -Ilibbpf/src -target bpf -c server_xdp.c -o server_xdp.o
10 |
11 | .PHONY: clean
12 | clean:
13 | rm -f server
14 | rm -f *.o
--------------------------------------------------------------------------------
/010/README.md:
--------------------------------------------------------------------------------
1 | # 010
2 |
3 | The only way to pass this test is to break the rules of the test.
4 |
5 | Drop the server <-> backend HTTP requests and switch from Golang to XDP.
6 |
7 | To run:
8 |
9 | ```console
10 | terraform init
11 | terraform apply
12 | ```
13 |
14 | Result:
15 |
16 |
17 |
18 | We can now run 100k clients for each c3-highcpu-44 instance, so we only need ten of them now instead of 200. This is a 20X cost saving from $481,114 USD per-month to $24,055 for the VMs.
19 |
20 | But there are still the egress bandwidth charges, and these dominate. Surely at such a scale, the egress bandwidth price would be greatly reduced with sales negotiations with Google Cloud, but let's go a step further.
21 |
22 | Let's run the system in bare metal.
23 |
24 |
25 |
26 | I love https://datapacket.com. They are an excellent bare metal hosting company. Picking the fattest bare metal server they have with a 40GB bandwidth plan, there are no ingress or egress bandwidth charges past the the monthly cost.
27 |
28 | Using XDP I can hit line rate on a 10G, 40G or 100G NIC.
29 |
30 | 100 packets per-second * 1M players = 100M packets per-second is pretty close to line rate for a 100G NIC for 100 byte packets. Assume I could request 100G bandwidth at twice the price. This is a bit too close for comfort, so I'd probably double up and get a second machine and load balance between them, so now the cost is 4X.
31 |
32 | The total cost for 1M clients is now: $33,720 USD per-month.
33 |
34 | $1,746,010 / $33,720 = 50X reduction in cost.
35 |
36 | Can we take it even further? Yes!
37 |
38 | If we needed to scale up more, at some point XDP is not fast enough.
39 |
40 | We could purchase and install a netronome NIC that would run the XDP hash function in hardware. Alternatively, we could explore implementing the hash on a programmable NIC using P4.
41 |
42 | If we need to scale up even further, perhaps another 100 - 1000X, we could scale out horizontally with multiple bare metal machines with NICs that have onboard FPGA and implement the hash there. _Although, this is mildly insane._
43 |
44 | What's the moral of the story?
45 |
46 | 1. Work out how much it costs
47 | 2. Prototype it, load test it and really understand the problem
48 | 3. Don't be afraid to break the rules to get it to scale :)
49 |
--------------------------------------------------------------------------------
/010/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 | "time"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "strconv"
12 | "sync/atomic"
13 | "math/rand"
14 | )
15 |
16 | const StartPort = 10000
17 | const MaxPacketSize = 1500
18 | const SocketBufferSize = 256*1024*1024
19 |
20 | var numClients int
21 |
22 | var quit uint64
23 | var packetsSent uint64
24 | var packetsReceived uint64
25 |
26 | func GetInt(name string, defaultValue int) int {
27 | valueString, ok := os.LookupEnv(name)
28 | if !ok {
29 | return defaultValue
30 | }
31 | value, err := strconv.ParseInt(valueString, 10, 64)
32 | if err != nil {
33 | return defaultValue
34 | }
35 | return int(value)
36 | }
37 |
38 | func GetAddress(name string, defaultValue string) net.UDPAddr {
39 | valueString, ok := os.LookupEnv(name)
40 | if !ok {
41 | valueString = defaultValue
42 | }
43 | value, err := net.ResolveUDPAddr("udp", valueString)
44 | if err != nil {
45 | panic(fmt.Sprintf("invalid address in envvar %s", name))
46 | }
47 | return *value
48 | }
49 |
50 | func main() {
51 |
52 | serverAddress := GetAddress("SERVER_ADDRESS", "127.0.0.1:40000")
53 |
54 | numClients = GetInt("NUM_CLIENTS", 1)
55 |
56 | fmt.Printf("starting %d clients\n", numClients)
57 |
58 | fmt.Printf("server address is %s\n", serverAddress.String())
59 |
60 | var wg sync.WaitGroup
61 |
62 | for i := 0; i < numClients; i++ {
63 | go func(clientIndex int) {
64 | wg.Add(1)
65 | runClient(clientIndex, &serverAddress)
66 | wg.Done()
67 | }(i)
68 | }
69 |
70 | termChan := make(chan os.Signal, 1)
71 |
72 | signal.Notify(termChan, os.Interrupt, syscall.SIGTERM)
73 |
74 | ticker := time.NewTicker(time.Second)
75 |
76 | prev_sent := uint64(0)
77 | prev_received := uint64(0)
78 |
79 | for {
80 | select {
81 | case <-termChan:
82 | fmt.Printf("\nreceived shutdown signal\n")
83 | atomic.StoreUint64(&quit, 1)
84 | case <-ticker.C:
85 | sent := atomic.LoadUint64(&packetsSent)
86 | received := atomic.LoadUint64(&packetsReceived)
87 | sent_delta := sent - prev_sent
88 | received_delta := received - prev_received
89 | fmt.Printf("sent delta %d, received delta %d\n", sent_delta, received_delta)
90 | prev_sent = sent
91 | prev_received = received
92 | }
93 | quit := atomic.LoadUint64(&quit)
94 | if quit != 0 {
95 | break
96 | }
97 | }
98 |
99 | fmt.Printf("shutting down\n")
100 |
101 | wg.Wait()
102 |
103 | fmt.Printf("done.\n")
104 | }
105 |
106 | func runClient(clientIndex int, serverAddress *net.UDPAddr) {
107 |
108 | addr := net.UDPAddr{
109 | Port: StartPort + clientIndex,
110 | IP: net.ParseIP("0.0.0.0"),
111 | }
112 |
113 | conn, err := net.ListenUDP("udp", &addr)
114 | if err != nil {
115 | return // IMPORTANT: to get as many clients as possible on one machine, if we can't bind to a specific port, just ignore and carry on
116 | }
117 | defer conn.Close()
118 |
119 | if err := conn.SetReadBuffer(SocketBufferSize); err != nil {
120 | panic(fmt.Sprintf("could not set socket read buffer size: %v", err))
121 | }
122 |
123 | if err := conn.SetWriteBuffer(SocketBufferSize); err != nil {
124 | panic(fmt.Sprintf("could not set socket write buffer size: %v", err))
125 | }
126 |
127 | buffer := make([]byte, MaxPacketSize)
128 |
129 | go func() {
130 | for {
131 | packetBytes, _, err := conn.ReadFromUDP(buffer)
132 | if err != nil {
133 | break
134 | }
135 | if packetBytes != 8 {
136 | continue
137 | }
138 | atomic.AddUint64(&packetsReceived, 1)
139 | }
140 | }()
141 |
142 | packetData := make([]byte, 100)
143 |
144 | rand.Read(packetData)
145 |
146 | for {
147 | quit := atomic.LoadUint64(&quit)
148 | if quit != 0 {
149 | break
150 | }
151 | for i := 0; i < 10; i++ {
152 | conn.WriteToUDP(packetData[:], serverAddress)
153 | }
154 | atomic.AddUint64(&packetsSent, 10)
155 | time.Sleep(time.Millisecond*100)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/010/client.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Client Service
3 |
4 | [Service]
5 | ExecStart=/app/client
6 | EnvironmentFile=/app/client.env
7 | Restart=always
8 | RestartSec=30
9 | TimeoutStopSec=90
10 |
--------------------------------------------------------------------------------
/010/main.tf:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------------------
2 |
3 | variable "google_org_id" { type = string }
4 | variable "google_billing_account" { type = string }
5 | variable "google_location" { type = string }
6 | variable "google_region" { type = string }
7 | variable "google_zones" { type = list(string) }
8 | variable "google_zone" { type = string }
9 |
10 | variable "tag" { type = string }
11 |
12 | # ----------------------------------------------------------------------------------------
13 |
14 | terraform {
15 | required_providers {
16 | google = {
17 | source = "hashicorp/google"
18 | version = "~> 5.0.0"
19 | }
20 | }
21 | }
22 |
23 | provider "google" {
24 | region = var.google_region
25 | zone = var.google_zone
26 | }
27 |
28 | # ----------------------------------------------------------------------------------------
29 |
30 | resource "random_id" "postfix" {
31 | byte_length = 8
32 | }
33 |
34 | resource "google_project" "udp" {
35 | name = "UDP Test"
36 | project_id = "udp-${random_id.postfix.hex}"
37 | org_id = var.google_org_id
38 | billing_account = var.google_billing_account
39 | }
40 |
41 | # ----------------------------------------------------------------------------------------
42 |
43 | locals {
44 | services = [
45 | "compute.googleapis.com", # compute engine
46 | "storage.googleapis.com", # cloud storage
47 | ]
48 | }
49 |
50 | resource "google_project_service" "udp" {
51 | count = length(local.services)
52 | project = google_project.udp.project_id
53 | service = local.services[count.index]
54 | timeouts {
55 | create = "30m"
56 | update = "40m"
57 | }
58 | disable_dependent_services = true
59 | }
60 |
61 | # ----------------------------------------------------------------------------------------
62 |
63 | data "local_file" "client_go" {
64 | filename = "client.go"
65 | }
66 |
67 | data "local_file" "client_service" {
68 | filename = "client.service"
69 | }
70 |
71 | data "local_file" "server_c" {
72 | filename = "server.c"
73 | }
74 |
75 | data "local_file" "server_xdp_c" {
76 | filename = "server_xdp.c"
77 | }
78 |
79 | data "local_file" "server_service" {
80 | filename = "server.service"
81 | }
82 |
83 | data "local_file" "go_mod" {
84 | filename = "go.mod"
85 | }
86 |
87 | data "local_file" "makefile" {
88 | filename = "Makefile"
89 | }
90 |
91 | data "archive_file" "source_zip" {
92 | type = "zip"
93 | output_path = "source-${var.tag}.zip"
94 | source {
95 | filename = "client.go"
96 | content = data.local_file.client_go.content
97 | }
98 | source {
99 | filename = "client.service"
100 | content = data.local_file.client_service.content
101 | }
102 | source {
103 | filename = "server.c"
104 | content = data.local_file.server_c.content
105 | }
106 | source {
107 | filename = "server_xdp.c"
108 | content = data.local_file.server_xdp_c.content
109 | }
110 | source {
111 | filename = "server.service"
112 | content = data.local_file.server_service.content
113 | }
114 | source {
115 | filename = "go.mod"
116 | content = data.local_file.go_mod.content
117 | }
118 | source {
119 | filename = "Makefile"
120 | content = data.local_file.makefile.content
121 | }
122 | }
123 |
124 | resource "google_storage_bucket" "source" {
125 | name = "${var.google_org_id}_udp_source"
126 | project = google_project.udp.project_id
127 | location = "US"
128 | force_destroy = true
129 | public_access_prevention = "enforced"
130 | uniform_bucket_level_access = true
131 | }
132 |
133 | resource "google_storage_bucket_object" "source_zip" {
134 | name = "source-${var.tag}.zip"
135 | source = "source-${var.tag}.zip"
136 | content_type = "application/zip"
137 | bucket = google_storage_bucket.source.id
138 | }
139 |
140 | # ----------------------------------------------------------------------------------------
141 |
142 | resource "google_service_account" "udp_runtime" {
143 | project = google_project.udp.project_id
144 | account_id = "udp-runtime"
145 | display_name = "UDP Runtime Service Account"
146 | }
147 |
148 | resource "google_project_iam_member" "udp_runtime_compute_viewer" {
149 | project = google_project.udp.project_id
150 | role = "roles/compute.viewer"
151 | member = google_service_account.udp_runtime.member
152 | }
153 |
154 | resource "google_storage_bucket_iam_member" "udp_runtime_storage_admin" {
155 | bucket = google_storage_bucket.source.name
156 | role = "roles/storage.objectAdmin"
157 | member = google_service_account.udp_runtime.member
158 | depends_on = [google_storage_bucket.source]
159 | }
160 |
161 | # ----------------------------------------------------------------------------------------
162 |
163 | resource "google_compute_network" "udp" {
164 | name = "udp"
165 | project = google_project.udp.project_id
166 | auto_create_subnetworks = false
167 | }
168 |
169 | resource "google_compute_subnetwork" "udp" {
170 | name = "udp"
171 | project = google_project.udp.project_id
172 | ip_cidr_range = "10.0.0.0/16"
173 | region = var.google_region
174 | network = google_compute_network.udp.id
175 | private_ip_google_access = true
176 | }
177 |
178 | resource "google_compute_router" "router" {
179 | name = "router-to-internet"
180 | network = google_compute_network.udp.id
181 | project = google_project.udp.project_id
182 | region = var.google_region
183 | }
184 |
185 | resource "google_compute_router_nat" "nat" {
186 | name = "nat"
187 | project = google_project.udp.project_id
188 | router = google_compute_router.router.name
189 | region = var.google_region
190 | nat_ip_allocate_option = "AUTO_ONLY"
191 | source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
192 | }
193 |
194 | resource "google_compute_firewall" "allow_ssh" {
195 | name = "allow-ssh"
196 | project = google_project.udp.project_id
197 | direction = "INGRESS"
198 | network = google_compute_network.udp.id
199 | source_ranges = ["130.211.0.0/22", "35.191.0.0/16", "35.235.240.0/20"]
200 | allow {
201 | protocol = "tcp"
202 | ports = ["22"]
203 | }
204 | target_tags = ["allow-ssh"]
205 | }
206 |
207 | resource "google_compute_firewall" "allow_http" {
208 | name = "allow-http"
209 | project = google_project.udp.project_id
210 | direction = "INGRESS"
211 | network = google_compute_network.udp.id
212 | source_ranges = ["0.0.0.0/0"]
213 | allow {
214 | protocol = "tcp"
215 | ports = ["50000"]
216 | }
217 | target_tags = ["allow-http"]
218 | }
219 |
220 | resource "google_compute_firewall" "allow_udp" {
221 | name = "allow-udp"
222 | project = google_project.udp.project_id
223 | direction = "INGRESS"
224 | network = google_compute_network.udp.id
225 | source_ranges = ["0.0.0.0/0"]
226 | allow {
227 | protocol = "udp"
228 | }
229 | target_tags = ["allow-udp"]
230 | }
231 |
232 | # ----------------------------------------------------------------------------------------
233 |
234 | resource "google_compute_instance_template" "client" {
235 |
236 | name = "client-${var.tag}"
237 |
238 | project = google_project.udp.project_id
239 |
240 | machine_type = "n1-standard-8"
241 |
242 | network_interface {
243 | network = google_compute_network.udp.id
244 | subnetwork = google_compute_subnetwork.udp.id
245 | access_config {}
246 | }
247 |
248 | tags = ["allow-ssh"]
249 |
250 | disk {
251 | source_image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
252 | auto_delete = true
253 | boot = true
254 | disk_type = "pd-ssd"
255 | }
256 |
257 | metadata = {
258 | startup-script = <<-EOF2
259 | #!/bin/bash
260 | NEEDRESTART_SUSPEND=1 apt update -y
261 | NEEDRESTART_SUSPEND=1 apt upgrade -y
262 | NEEDRESTART_SUSPEND=1 apt install golang-go unzip -y
263 | mkdir /app
264 | cd /app
265 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
266 | unzip *.zip
267 | export HOME=/app
268 | go get
269 | go build client.go
270 | cat < /app/client.env
271 | NUM_CLIENTS=1000
272 | SERVER_ADDRESS=${google_compute_instance.server.network_interface[0].network_ip}:40000
273 | EOF
274 | cat < /etc/sysctl.conf
275 | net.core.rmem_max=1000000000
276 | net.core.wmem_max=1000000000
277 | net.core.netdev_max_backlog=10000
278 | EOF
279 | sysctl -p
280 | cp client.service /etc/systemd/system/client.service
281 | systemctl daemon-reload
282 | systemctl start client.service
283 | EOF2
284 | }
285 |
286 | service_account {
287 | email = google_service_account.udp_runtime.email
288 | scopes = ["cloud-platform"]
289 | }
290 |
291 | lifecycle {
292 | create_before_destroy = true
293 | }
294 | }
295 |
296 | resource "google_compute_region_instance_group_manager" "client" {
297 | target_size = 100
298 | name = "client"
299 | project = google_project.udp.project_id
300 | region = var.google_region
301 | distribution_policy_zones = var.google_zones
302 | version {
303 | instance_template = google_compute_instance_template.client.id
304 | name = "primary"
305 | }
306 | base_instance_name = "client"
307 | update_policy {
308 | type = "PROACTIVE"
309 | minimal_action = "REPLACE"
310 | most_disruptive_allowed_action = "REPLACE"
311 | max_surge_fixed = 10
312 | max_unavailable_fixed = 0
313 | replacement_method = "SUBSTITUTE"
314 | }
315 | depends_on = [google_compute_instance_template.client]
316 | }
317 |
318 | # ----------------------------------------------------------------------------------------
319 |
320 | resource "google_compute_address" "server_address" {
321 | name = "server-${var.tag}-address"
322 | project = google_project.udp.project_id
323 | }
324 |
325 | resource "google_compute_instance" "server" {
326 |
327 | name = "server-${var.tag}"
328 | project = google_project.udp.project_id
329 | machine_type = "c3-highcpu-44"
330 | zone = var.google_zone
331 | tags = ["allow-ssh", "allow-udp"]
332 |
333 | allow_stopping_for_update = true
334 |
335 | boot_disk {
336 | initialize_params {
337 | image = "ubuntu-os-cloud/ubuntu-minimal-2204-lts"
338 | }
339 | }
340 |
341 | network_interface {
342 | network = google_compute_network.udp.id
343 | subnetwork = google_compute_subnetwork.udp.id
344 | access_config {
345 | nat_ip = google_compute_address.server_address.address
346 | }
347 | }
348 |
349 | advanced_machine_features {
350 | threads_per_core = 1
351 | }
352 |
353 | metadata = {
354 |
355 | startup-script = <<-EOF2
356 |
357 | #!/bin/bash
358 |
359 | NEEDRESTART_SUSPEND=1 apt autoremove -y
360 | NEEDRESTART_SUSPEND=1 apt update -y
361 | NEEDRESTART_SUSPEND=1 apt upgrade -y
362 | NEEDRESTART_SUSPEND=1 apt dist-upgrade -y
363 | NEEDRESTART_SUSPEND=1 apt full-upgrade -y
364 | NEEDRESTART_SUSPEND=1 apt install libcurl3-gnutls-dev build-essential vim golang-go wget libsodium-dev flex bison clang unzip libc6-dev-i386 gcc-12 dwarves libelf-dev pkg-config m4 libpcap-dev net-tools -y
365 | NEEDRESTART_SUSPEND=1 apt install linux-headers-`uname -r` linux-tools-`uname -r` -y
366 | NEEDRESTART_SUSPEND=1 apt autoremove -y
367 |
368 | sudo cp /sys/kernel/btf/vmlinux /usr/lib/modules/`uname -r`/build/
369 |
370 | mkdir /app
371 | cd /app
372 | gsutil cp gs://${var.google_org_id}_udp_source/source-${var.tag}.zip .
373 | unzip *.zip
374 |
375 | wget https://github.com/xdp-project/xdp-tools/releases/download/v1.4.2/xdp-tools-1.4.2.tar.gz
376 | tar -zxf xdp-tools-1.4.2.tar.gz
377 | cd xdp-tools-1.4.2
378 | ./configure
379 | make -j && make install
380 |
381 | cd lib/libbpf/src
382 | make -j && make install
383 | ldconfig
384 |
385 | cd /app
386 | make
387 |
388 | cat < /etc/sysctl.conf
389 | net.core.rmem_max=1000000000
390 | net.core.wmem_max=1000000000
391 | net.core.netdev_max_backlog=10000
392 | EOF
393 | sysctl -p
394 |
395 | cp server.service /etc/systemd/system/server.service
396 |
397 | systemctl daemon-reload
398 |
399 | systemctl start server.service
400 |
401 | EOF2
402 | }
403 |
404 | service_account {
405 | email = google_service_account.udp_runtime.email
406 | scopes = ["cloud-platform"]
407 | }
408 | }
409 |
410 | # ----------------------------------------------------------------------------------------
411 |
--------------------------------------------------------------------------------
/010/server.c:
--------------------------------------------------------------------------------
1 | /*
2 | UDP server XDP program (Userspace)
3 |
4 | Runs on Ubuntu 22.04 LTS 64bit with Linux Kernel 6.5+ *ONLY*
5 | */
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 |
19 | struct bpf_t
20 | {
21 | int interface_index;
22 | struct xdp_program * program;
23 | bool attached_native;
24 | bool attached_skb;
25 | };
26 |
27 | int bpf_init( struct bpf_t * bpf, const char * interface_name )
28 | {
29 | // we can only run xdp programs as root
30 |
31 | if ( geteuid() != 0 )
32 | {
33 | printf( "\nerror: this program must be run as root\n\n" );
34 | return 1;
35 | }
36 |
37 | // find the network interface that matches the interface name
38 | {
39 | bool found = false;
40 |
41 | struct ifaddrs * addrs;
42 | if ( getifaddrs( &addrs ) != 0 )
43 | {
44 | printf( "\nerror: getifaddrs failed\n\n" );
45 | return 1;
46 | }
47 |
48 | for ( struct ifaddrs * iap = addrs; iap != NULL; iap = iap->ifa_next )
49 | {
50 | if ( iap->ifa_addr && ( iap->ifa_flags & IFF_UP ) && iap->ifa_addr->sa_family == AF_INET )
51 | {
52 | struct sockaddr_in * sa = (struct sockaddr_in*) iap->ifa_addr;
53 | if ( strcmp( interface_name, iap->ifa_name ) == 0 )
54 | {
55 | printf( "found network interface: '%s'\n", iap->ifa_name );
56 | bpf->interface_index = if_nametoindex( iap->ifa_name );
57 | if ( !bpf->interface_index )
58 | {
59 | printf( "\nerror: if_nametoindex failed\n\n" );
60 | return 1;
61 | }
62 | found = true;
63 | break;
64 | }
65 | }
66 | }
67 |
68 | freeifaddrs( addrs );
69 |
70 | if ( !found )
71 | {
72 | printf( "\nerror: could not find any network interface matching '%s'", interface_name );
73 | return 1;
74 | }
75 | }
76 |
77 | // load the server_xdp program and attach it to the network interface
78 |
79 | printf( "loading server_xdp...\n" );
80 |
81 | bpf->program = xdp_program__open_file( "server_xdp.o", "server_xdp", NULL );
82 | if ( libxdp_get_error( bpf->program ) )
83 | {
84 | printf( "\nerror: could not load server_xdp program\n\n");
85 | return 1;
86 | }
87 |
88 | printf( "server_xdp loaded successfully.\n" );
89 |
90 | printf( "attaching server_xdp to network interface\n" );
91 |
92 | int ret = xdp_program__attach( bpf->program, bpf->interface_index, XDP_MODE_NATIVE, 0 );
93 | if ( ret == 0 )
94 | {
95 | bpf->attached_native = true;
96 | }
97 | else
98 | {
99 | printf( "falling back to skb mode...\n" );
100 | ret = xdp_program__attach( bpf->program, bpf->interface_index, XDP_MODE_SKB, 0 );
101 | if ( ret == 0 )
102 | {
103 | bpf->attached_skb = true;
104 | }
105 | else
106 | {
107 | printf( "\nerror: failed to attach server_xdp program to interface\n\n" );
108 | return 1;
109 | }
110 | }
111 |
112 | return 0;
113 | }
114 |
115 | void bpf_shutdown( struct bpf_t * bpf )
116 | {
117 | assert( bpf );
118 |
119 | if ( bpf->program != NULL )
120 | {
121 | if ( bpf->attached_native )
122 | {
123 | xdp_program__detach( bpf->program, bpf->interface_index, XDP_MODE_NATIVE, 0 );
124 | }
125 | if ( bpf->attached_skb )
126 | {
127 | xdp_program__detach( bpf->program, bpf->interface_index, XDP_MODE_SKB, 0 );
128 | }
129 | xdp_program__close( bpf->program );
130 | }
131 | }
132 |
133 | static struct bpf_t bpf;
134 |
135 | volatile bool quit;
136 |
137 | void interrupt_handler( int signal )
138 | {
139 | (void) signal; quit = true;
140 | }
141 |
142 | void clean_shutdown_handler( int signal )
143 | {
144 | (void) signal;
145 | quit = true;
146 | }
147 |
148 | static void cleanup()
149 | {
150 | bpf_shutdown( &bpf );
151 | fflush( stdout );
152 | }
153 |
154 | int main( int argc, char *argv[] )
155 | {
156 | signal( SIGINT, interrupt_handler );
157 | signal( SIGTERM, clean_shutdown_handler );
158 | signal( SIGHUP, clean_shutdown_handler );
159 |
160 | if ( argc != 2 )
161 | {
162 | printf( "\nusage: server \n\n" );
163 | return 1;
164 | }
165 |
166 | const char * interface_name = argv[1];
167 |
168 | if ( bpf_init( &bpf, interface_name ) != 0 )
169 | {
170 | cleanup();
171 | return 1;
172 | }
173 |
174 | while ( !quit )
175 | {
176 | usleep( 1000000 );
177 | }
178 |
179 | cleanup();
180 |
181 | printf( "\n" );
182 |
183 | return 0;
184 | }
185 |
--------------------------------------------------------------------------------
/010/server.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UDP Server Service
3 |
4 | [Service]
5 | ExecStart=/app/server ens3
6 | WorkingDirectory=/app
7 | Restart=always
8 | RestartSec=30
9 | TimeoutStopSec=90
10 |
--------------------------------------------------------------------------------
/010/server_xdp.c:
--------------------------------------------------------------------------------
1 | /*
2 | UDP server XDP program
3 |
4 | Replies to 100 byte UDP packets sent to port 40000 with the fnv1a 64bit hash (8 bytes)
5 |
6 | USAGE:
7 |
8 | clang -Ilibbpf/src -g -O2 -target bpf -c server_xdp.c -o server_xdp.o
9 | sudo cat /sys/kernel/debug/tracing/trace_pipe
10 | */
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 |
23 | #if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \
24 | __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
25 | #define bpf_ntohs(x) __builtin_bswap16(x)
26 | #define bpf_htons(x) __builtin_bswap16(x)
27 | #elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && \
28 | __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
29 | #define bpf_ntohs(x) (x)
30 | #define bpf_htons(x) (x)
31 | #else
32 | # error "Endianness detection needs to be set up for your compiler?!"
33 | #endif
34 |
35 | //#define DEBUG 1
36 |
37 | #if DEBUG
38 | #define debug_printf bpf_printk
39 | #else // #if DEBUG
40 | #define debug_printf(...) do { } while (0)
41 | #endif // #if DEBUG
42 |
43 | static void reflect_packet( void * data, int payload_bytes )
44 | {
45 | struct ethhdr * eth = data;
46 | struct iphdr * ip = data + sizeof( struct ethhdr );
47 | struct udphdr * udp = (void*) ip + sizeof( struct iphdr );
48 |
49 | __u16 a = udp->source;
50 | udp->source = udp->dest;
51 | udp->dest = a;
52 | udp->check = 0;
53 | udp->len = bpf_htons( sizeof(struct udphdr) + payload_bytes );
54 |
55 | __u32 b = ip->saddr;
56 | ip->saddr = ip->daddr;
57 | ip->daddr = b;
58 | ip->tot_len = bpf_htons( sizeof(struct iphdr) + sizeof(struct udphdr) + payload_bytes );
59 | ip->check = 0;
60 |
61 | char c[ETH_ALEN];
62 | memcpy( c, eth->h_source, ETH_ALEN );
63 | memcpy( eth->h_source, eth->h_dest, ETH_ALEN );
64 | memcpy( eth->h_dest, c, ETH_ALEN );
65 |
66 | __u16 * p = (__u16*) ip;
67 | __u32 checksum = p[0];
68 | checksum += p[1];
69 | checksum += p[2];
70 | checksum += p[3];
71 | checksum += p[4];
72 | checksum += p[5];
73 | checksum += p[6];
74 | checksum += p[7];
75 | checksum += p[8];
76 | checksum += p[9];
77 | checksum = ~ ( ( checksum & 0xFFFF ) + ( checksum >> 16 ) );
78 | ip->check = checksum;
79 | }
80 |
81 | SEC("server_xdp") int server_xdp_filter( struct xdp_md *ctx )
82 | {
83 | void * data = (void*) (long) ctx->data;
84 |
85 | void * data_end = (void*) (long) ctx->data_end;
86 |
87 | struct ethhdr * eth = data;
88 |
89 | if ( (void*)eth + sizeof(struct ethhdr) < data_end )
90 | {
91 | if ( eth->h_proto == __constant_htons(ETH_P_IP) ) // IPV4
92 | {
93 | struct iphdr * ip = data + sizeof(struct ethhdr);
94 |
95 | if ( (void*)ip + sizeof(struct iphdr) < data_end )
96 | {
97 | if ( ip->protocol == IPPROTO_UDP ) // UDP
98 | {
99 | struct udphdr * udp = (void*) ip + sizeof(struct iphdr);
100 |
101 | if ( (void*)udp + sizeof(struct udphdr) <= data_end )
102 | {
103 | if ( udp->dest == __constant_htons(40000) )
104 | {
105 | __u8 * payload = (void*) udp + sizeof(struct udphdr);
106 | int payload_bytes = data_end - (void*)payload;
107 | if ( payload_bytes == 100 && (void*)payload + 100 <= data_end ) // IMPORTANT: for the verifier
108 | {
109 | reflect_packet( data, 8 );
110 | __u64 hash = 0xCBF29CE484222325;
111 | for ( int i = 0; i < 100; i++ )
112 | {
113 | hash ^= payload[i];
114 | hash *= 0x00000100000001B3;
115 | }
116 | payload[0] = ( hash ) & 0xFF;
117 | payload[1] = ( hash >> 8 ) & 0xFF;
118 | payload[2] = ( hash >> 16 ) & 0xFF;
119 | payload[3] = ( hash >> 24 ) & 0xFF;
120 | payload[4] = ( hash >> 32 ) & 0xFF;
121 | payload[5] = ( hash >> 40 ) & 0xFF;
122 | payload[6] = ( hash >> 48 ) & 0xFF;
123 | payload[7] = ( hash >> 56 );
124 | bpf_xdp_adjust_tail( ctx, -( payload_bytes - 8 ) );
125 | return XDP_TX;
126 | }
127 | else
128 | {
129 | return XDP_DROP;
130 | }
131 | }
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
138 | return XDP_PASS;
139 | }
140 |
141 | char _license[] SEC("license") = "GPL";
142 |
--------------------------------------------------------------------------------
/010/terraform.tfvars:
--------------------------------------------------------------------------------
1 |
2 | google_org_id = "434699063105"
3 | google_billing_account = "012279-A33489-722F96"
4 | google_location = "US"
5 | google_region = "us-central1"
6 | google_zone = "us-central1-a"
7 | google_zones = ["us-central1-a"] # IMPORTANT: keep to a single zone to make sure bandwidth is free during load testing
8 |
9 | tag = "057" # change this each time you want to recreate the VMs
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2024, Más Bandwidth LLC
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # udp
2 |
3 | Example source code for https://mas-bandwidth.com/writing-highly-scalable-backends-in-udp
4 |
--------------------------------------------------------------------------------