From a4b0a98df3372419b85b7851d53be3a2a14150c3 Mon Sep 17 00:00:00 2001 From: Marcos Nils Date: Mon, 13 Mar 2017 18:07:20 -0300 Subject: [PATCH] Scaling (#109) Make PWD scalable --- .gitignore | 2 +- config/config.go | 7 ++++++- docker-compose.yml | 33 ++++++++++++++++++++++++++------- handlers/get_session.go | 2 -- handlers/new_session.go | 15 +++++++++++++-- handlers/ping.go | 19 ++++++++++++++++++- haproxy/haproxy.cfg | 32 ++++++++++++++++++++++++++++++++ pwd/.gitignore | 1 - services/session.go | 13 +++++++------ 9 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 haproxy/haproxy.cfg delete mode 100644 pwd/.gitignore diff --git a/.gitignore b/.gitignore index 5c31a8e..f99ed7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ play-with-docker node_modules - +pwd/* diff --git a/config/config.go b/config/config.go index 6c64bcd..d4316b4 100644 --- a/config/config.go +++ b/config/config.go @@ -2,12 +2,17 @@ package config import "flag" -var SSLPortNumber, PortNumber, Key, Cert string +var SSLPortNumber, PortNumber, Key, Cert, SessionsFile, PWDContainerName, PWDCName string +var MaxLoadAvg float64 func ParseFlags() { flag.StringVar(&PortNumber, "port", "3000", "Give a TCP port to run the application") flag.StringVar(&SSLPortNumber, "sslPort", "3001", "Give a SSL TCP port") flag.StringVar(&Key, "key", "./pwd/server-key.pem", "Server key for SSL") flag.StringVar(&Cert, "cert", "./pwd/server.pem", "Give a SSL cert") + flag.StringVar(&SessionsFile, "save", "./pwd/sessions", "Tell where to store sessions file") + flag.StringVar(&PWDContainerName, "name", "pwd", "Container name used to run PWD (used to be able to connect it to the networks it creates)") + flag.StringVar(&PWDCName, "cname", "host1", "CNAME given to this host") + flag.Float64Var(&MaxLoadAvg, "maxload", 100, "Maximum allowed load average before failing ping requests") flag.Parse() } diff --git a/docker-compose.yml b/docker-compose.yml index 706bc1e..97566b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,35 @@ version: '2' services: - pwd: + haproxy: + container_name: proxy + image: haproxy + ports: + - "80:8080" + - "443:8443" + + volumes: + - ./haproxy:/usr/local/etc/haproxy + pwd1: # pwd daemon container always needs to be named this way - container_name: pwd + container_name: pwd1 # use the latest golang image image: golang # go to the right place and starts the app - command: /bin/sh -c 'cd /go/src/github.com/franela/play-with-docker; go run api.go' - ports: - # app exposes port 3000 - - "80:3000" - - "443:3001" + command: /bin/sh -c 'cd /go/src/github.com/franela/play-with-docker; go run api.go -save ./pwd/sessions1 -name pwd1 -cname host1' + volumes: + # since this app creates networks and launches containers, we need to talk to docker daemon + - /var/run/docker.sock:/var/run/docker.sock + # mount the box mounted shared folder to the container + - $GOPATH/src:/go/src + environment: + GOOGLE_RECAPTCHA_DISABLED: "true" + pwd2: + # pwd daemon container always needs to be named this way + container_name: pwd2 + # use the latest golang image + image: golang + # go to the right place and starts the app + command: /bin/sh -c 'cd /go/src/github.com/franela/play-with-docker; go run api.go -save ./pwd/sessions2 -name pwd2 -cname host2' volumes: # since this app creates networks and launches containers, we need to talk to docker daemon - /var/run/docker.sock:/var/run/docker.sock diff --git a/handlers/get_session.go b/handlers/get_session.go index f302ec6..a86bfe4 100644 --- a/handlers/get_session.go +++ b/handlers/get_session.go @@ -2,7 +2,6 @@ package handlers import ( "encoding/json" - "log" "net/http" "github.com/franela/play-with-docker/services" @@ -12,7 +11,6 @@ import ( func GetSession(rw http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) sessionId := vars["sessionId"] - log.Println(sessionId) session := services.GetSession(sessionId) diff --git a/handlers/new_session.go b/handlers/new_session.go index 5831c74..fc39deb 100644 --- a/handlers/new_session.go +++ b/handlers/new_session.go @@ -1,13 +1,20 @@ package handlers import ( + "encoding/json" "fmt" "log" "net/http" + "github.com/franela/play-with-docker/config" "github.com/franela/play-with-docker/services" ) +type NewSessionResponse struct { + SessionId string `json:"session_id"` + Hostname string `json:"hostname"` +} + func NewSession(rw http.ResponseWriter, req *http.Request) { req.ParseForm() if !services.IsHuman(req) { @@ -25,11 +32,15 @@ func NewSession(rw http.ResponseWriter, req *http.Request) { log.Println(err) //TODO: Return some error code } else { + + hostname := fmt.Sprintf("%s.%s", config.PWDCName, req.Host) // If request is not a form, return sessionId in the body if req.Header.Get("X-Requested-With") == "XMLHttpRequest" { - rw.Write([]byte(s.Id)) + resp := NewSessionResponse{SessionId: s.Id, Hostname: hostname} + rw.Header().Set("Content-Type", "application/json") + json.NewEncoder(rw).Encode(resp) return } - http.Redirect(rw, req, fmt.Sprintf("/p/%s", s.Id), http.StatusFound) + http.Redirect(rw, req, fmt.Sprintf("http://%s/p/%s", hostname, s.Id), http.StatusFound) } } diff --git a/handlers/ping.go b/handlers/ping.go index 66a41be..beba80a 100644 --- a/handlers/ping.go +++ b/handlers/ping.go @@ -1,6 +1,23 @@ package handlers -import "net/http" +import ( + "log" + "net/http" + + "github.com/franela/play-with-docker/config" + "github.com/shirou/gopsutil/load" +) func Ping(rw http.ResponseWriter, req *http.Request) { + // Get system load average of the last 5 minutes and compare it against a threashold. + + a, err := load.Avg() + if err != nil { + log.Println("Cannot get system load average!", err) + } else { + if a.Load5 > config.MaxLoadAvg { + log.Printf("System load average is too high [%f]\n", a.Load5) + rw.WriteHeader(http.StatusInsufficientStorage) + } + } } diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg new file mode 100644 index 0000000..e9dbd09 --- /dev/null +++ b/haproxy/haproxy.cfg @@ -0,0 +1,32 @@ +defaults + mode http + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + +frontend http-in + bind *:8080 + + acl host_localhost hdr(host) localhost + acl host_pwd1 hdr_reg(host) -i ^.*\.?host1\.localhost$ + acl host_pwd2 hdr_reg(host) -i ^.*\.?host2\.localhost$ + + use_backend all if host_localhost + use_backend pwd1 if host_pwd1 + use_backend pwd2 if host_pwd2 + +backend all + balance roundrobin + + option httpchk GET /ping HTTP/1.0 + http-check expect rstatus 200 + default-server inter 3s fall 3 rise 2 + + server node1 pwd1:3000 check + server node2 pwd2:3000 check + +backend pwd1 + server node1 pwd1:3000 + +backend pwd2 + server node2 pwd2:3000 diff --git a/pwd/.gitignore b/pwd/.gitignore deleted file mode 100644 index 29f5e4b..0000000 --- a/pwd/.gitignore +++ /dev/null @@ -1 +0,0 @@ -sessions.gob diff --git a/services/session.go b/services/session.go index bc7b74a..5e0ae30 100644 --- a/services/session.go +++ b/services/session.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/client" + "github.com/franela/play-with-docker/config" "github.com/googollee/go-socket.io" "github.com/prometheus/client_golang/prometheus" "github.com/twinj/uuid" @@ -241,13 +242,13 @@ func NewSession(duration time.Duration) (*Session, error) { log.Printf("Network [%s] created for session [%s]\n", s.Id, s.Id) // Connect PWD daemon to the new network - ip, err := ConnectNetwork("pwd", s.Id, "") + ip, err := ConnectNetwork(config.PWDContainerName, s.Id, "") if err != nil { log.Println("ERROR NETWORKING") return nil, err } s.PwdIpAddress = ip - log.Printf("Connected pwd to network [%s]\n", s.Id) + log.Printf("Connected %s to network [%s]\n", config.PWDContainerName, s.Id) // Schedule peridic tasks execution s.SchedulePeriodicTasks() @@ -290,7 +291,7 @@ func setGauges() { } func LoadSessionsFromDisk() error { - file, err := os.Open("./pwd/sessions.gob") + file, err := os.Open(config.SessionsFile) if err == nil { decoder := gob.NewDecoder(file) err = decoder.Decode(&sessions) @@ -322,7 +323,7 @@ func LoadSessionsFromDisk() error { if s.PwdIpAddress == "" { log.Fatal("Cannot load stored sessions as they don't have the pwd ip address stored with them") } - if _, err := ConnectNetwork("pwd", s.Id, s.PwdIpAddress); err != nil { + if _, err := ConnectNetwork(config.PWDContainerName, s.Id, s.PwdIpAddress); err != nil { if strings.Contains(err.Error(), "Could not attach to network") { log.Printf("Network for session [%s] doesn't exist. Removing all instances and session.", s.Id) CloseSession(s) @@ -331,7 +332,7 @@ func LoadSessionsFromDisk() error { return err } } else { - log.Printf("Connected pwd to network [%s]\n", s.Id) + log.Printf("Connected %s to network [%s]\n", config.PWDContainerName, s.Id) // Schedule peridic tasks execution s.SchedulePeriodicTasks() @@ -346,7 +347,7 @@ func LoadSessionsFromDisk() error { func saveSessionsToDisk() error { rw.Lock() defer rw.Unlock() - file, err := os.Create("./pwd/sessions.gob") + file, err := os.Create(config.SessionsFile) if err == nil { encoder := gob.NewEncoder(file) err = encoder.Encode(&sessions)