├── .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 | image 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 | ![image](https://github.com/mas-bandwidth/udp/assets/696656/3db5afa7-ad3a-46c4-8f70-b702054f74fb) 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 | image 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 | image 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 | Screenshot 2024-04-14 at 10 06 40 AM 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 | --------------------------------------------------------------------------------