Scaling Go Applications: Difference between revisions
Line 119: | Line 119: | ||
==Creating a Load Balancer== | ==Creating a Load Balancer== | ||
This is a very simple load balancer to start. Each webrequest coming in contains it's request, response and a channel | |||
<syntaxhighlight lang="go"> | |||
type webRequest struct { | |||
r *http.Request | |||
w http.ResponseWriter | |||
doneCh chan struct{} | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
Two go routines are launched, one to process requests and one to listen for the requests | |||
<syntaxhighlight lang="go"> | |||
func main() { | |||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | |||
doneCh := make(chan struct{}) | |||
requestCh <- &webRequest{r: r, w: w, doneCh: doneCh} | |||
<-doneCh | |||
}) | |||
go processRequests() | |||
go http.ListenAndServeTLS(":2000", "cert.pem", "key.pem", nil) | |||
log.Println("Server started, press <ENTER> to exit") | |||
fmt.Scanln() | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
The process keeps a list of app servers, the current index and a client | |||
<syntaxhighlight lang="go"> | |||
var ( | |||
appservers = []string{} | |||
currentIndex = 0 | |||
client = http.Client{Transport: &transport} | |||
) | |||
</syntaxhighlight> | |||
<br> | |||
The process requests listens for requests on the channel, increments the index to ensure the next server is used. | |||
<syntaxhighlight lang="go"> | |||
func processRequests() { | |||
for { | |||
select { | |||
case request := <-requestCh: | |||
println("request") | |||
if len(appservers) == 0 { | |||
request.w.WriteHeader(http.StatusInternalServerError) | |||
request.w.Write([]byte("No app servers found")) | |||
request.doneCh <- struct{}{} | |||
continue | |||
} | |||
currentIndex++ | |||
if currentIndex == len(appservers) { | |||
currentIndex = 0 | |||
} | |||
host := appservers[currentIndex] | |||
go processRequest(host, request) | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
Finally the requests are processed. | |||
*Incoming Request is copied to a new app server request | |||
*Request is processed | |||
*Response headers a copied back to original request | |||
*Incoming Request is marked as done | |||
<syntaxhighlight lang="go"> | |||
func processRequest(host string, request *webRequest) { | |||
hostURL, _ := url.Parse(request.r.URL.String()) | |||
hostURL.Scheme = "https" | |||
hostURL.Host = host | |||
println(host) | |||
println(hostURL.String()) | |||
req, _ := http.NewRequest(request.r.Method, hostURL.String(), request.r.Body) | |||
for k, v := range request.r.Header { | |||
values := "" | |||
for _, headerValue := range v { | |||
values += headerValue + " " | |||
} | |||
req.Header.Add(k, values) | |||
} | |||
resp, err := client.Do(req) | |||
if err != nil { | |||
request.w.WriteHeader(http.StatusInternalServerError) | |||
request.doneCh <- struct{}{} | |||
return | |||
} | |||
for k, v := range resp.Header { | |||
values := "" | |||
for _, headerValue := range v { | |||
values += headerValue + " " | |||
} | |||
request.w.Header().Add(k, values) | |||
} | |||
io.Copy(request.w, resp.Body) | |||
request.doneCh <- struct{}{} | |||
} | |||
</syntaxhighlight> | |||
==Caching== | ==Caching== | ||
==Centralized Logging== | ==Centralized Logging== |
Revision as of 01:39, 29 January 2021
Introduction
Resources
When we look at scaling we must consider the following resources.
- Network Bandwidth
- Processing Power
- Available Memory
- Data Storage
For this page we will be looking a the challenges around breaking an application in to several servers and following an architecture such as below
This not meant to be THE solution but a walkthrough on some of the challenges to scale. This will be split into
- Initial Optimizations
- Creating a Load Balancer
- Caching
- Centralized Logging
AGAIN this is an approach and of course you can use third-party cloud solutions. This one will be confined to Docker and GO.
Initial Optimizations
Content Compression
One of the easiest things to do is to change the http messages to use compression in our messages.
Gzip Handler
For this we write a GzipHandler.
package util
import (
"compress/gzip"
"net/http"
"strings"
)
type CloseableResponseWriter interface {
http.ResponseWriter
Close()
}
type gzipResponseWriter struct {
http.ResponseWriter
*gzip.Writer
}
func (w gzipResponseWriter) Write(data []byte) (int, error) {
return w.Writer.Write(data)
}
func (w gzipResponseWriter) Close() {
w.Writer.Close()
}
func (w gzipResponseWriter) Header() http.Header {
return w.ResponseWriter.Header()
}
type closeableResponseWriter struct {
http.ResponseWriter
}
func (w closeableResponseWriter) Close() {}
func GetResponseWriter(w http.ResponseWriter, req *http.Request) CloseableResponseWriter {
if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
gRW := gzipResponseWriter{
ResponseWriter: w,
Writer: gzip.NewWriter(w),
}
return gRW
} else {
return closeableResponseWriter{ResponseWriter: w}
}
}
type GzipHandler struct{}
func (h *GzipHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
responseWriter := GetResponseWriter(w, r)
defer responseWriter.Close()
http.DefaultServeMux.ServeHTTP(responseWriter, r)
}
Implement on Server
To we can now use this within the server.
func main() {
...
go http.ListenAndServe(":3000", new(util.GzipHandler))
Move to HTTP/2
This provides
- Header compression
- Reusable Connections
- Server Push
To move to http/2 we need to run over https. Within go you can run this but I do mine manually
go run /usr/lib/go/src/crypto/tls/generate_cert.go -host localhost
Then to use you just need to change the ListenAndServ to ListenAndServTLS and add the cert and key. I did not have problems using the self signed certs but here is what some people have done.
package data
import (
"crypto/tls"
"flag"
"net/http"
)
var dataServiceUrl = flag.String("dataservice", "https://localhost:4000", "Address of the data service provider")
func init() {
tr := http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
http.DefaultClient = &http.Client{Transport: &tr}
}
Creating a Load Balancer
This is a very simple load balancer to start. Each webrequest coming in contains it's request, response and a channel
type webRequest struct {
r *http.Request
w http.ResponseWriter
doneCh chan struct{}
}
Two go routines are launched, one to process requests and one to listen for the requests
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
doneCh := make(chan struct{})
requestCh <- &webRequest{r: r, w: w, doneCh: doneCh}
<-doneCh
})
go processRequests()
go http.ListenAndServeTLS(":2000", "cert.pem", "key.pem", nil)
log.Println("Server started, press <ENTER> to exit")
fmt.Scanln()
}
The process keeps a list of app servers, the current index and a client
var (
appservers = []string{}
currentIndex = 0
client = http.Client{Transport: &transport}
)
The process requests listens for requests on the channel, increments the index to ensure the next server is used.
func processRequests() {
for {
select {
case request := <-requestCh:
println("request")
if len(appservers) == 0 {
request.w.WriteHeader(http.StatusInternalServerError)
request.w.Write([]byte("No app servers found"))
request.doneCh <- struct{}{}
continue
}
currentIndex++
if currentIndex == len(appservers) {
currentIndex = 0
}
host := appservers[currentIndex]
go processRequest(host, request)
}
}
}
Finally the requests are processed.
- Incoming Request is copied to a new app server request
- Request is processed
- Response headers a copied back to original request
- Incoming Request is marked as done
func processRequest(host string, request *webRequest) {
hostURL, _ := url.Parse(request.r.URL.String())
hostURL.Scheme = "https"
hostURL.Host = host
println(host)
println(hostURL.String())
req, _ := http.NewRequest(request.r.Method, hostURL.String(), request.r.Body)
for k, v := range request.r.Header {
values := ""
for _, headerValue := range v {
values += headerValue + " "
}
req.Header.Add(k, values)
}
resp, err := client.Do(req)
if err != nil {
request.w.WriteHeader(http.StatusInternalServerError)
request.doneCh <- struct{}{}
return
}
for k, v := range resp.Header {
values := ""
for _, headerValue := range v {
values += headerValue + " "
}
request.w.Header().Add(k, values)
}
io.Copy(request.w, resp.Body)
request.doneCh <- struct{}{}
}