Gestione dei processi per il server Web Go

Sono un nuovo programmatore di Go, proveniente dal mondo dello sviluppo di applicazioni e servizi web. Ci scusiamo se questa è una domanda da derpare di herp, ma il mio cercare su google per una risposta non ha trovato nulla. Inoltre, si tratta di un territorio di errore del server borderline, ma poiché sono più interessato alle interfacce API / programmatiche che sto chiedendo qui.

Ho scritto un piccolo programma utilizzando il web server integrato del pacchetto net/http . Mi sto preparando per la distribuzione in produzione, ma sono un po ‘incerto sul processo di modellazione del web server Go e su come dovrei distribuire.

Nello specifico, negli ambienti a cui sono abituato (PHP, Ruby, Python) abbiamo un server web (Apache, Nginx, ecc.) Che si trova di fronte alla nostra applicazione e configuriamo questi server web per utilizzare un certo numero di processi / thread di lavoro e configurare il numero di connessioni HTTP (S) individuali che ogni thread dovrebbe elaborare.

Non sono stato in grado di trovare informazioni su come il web server di Go gestisce questo o informazioni pratiche su come ridimensionare / pianificare su scala per un server web Go.

cioè – se ho un semplice programma in esecuzione, pronto a gestire una richiesta HTTP

 func main() { http.HandleFunc("/", processRequest) http.ListenAndServe(":8000", nil) } 

quante connessioni manterrà HandleFunc in una sola volta? O inizierà a bloccare quando si apre una connessione, e servirà solo la prossima connessione una volta che la connessione si chiude?

O dovrei semplicemente non preoccuparmi di questo e mandare tutto in una routine? Ma se lo faccio, come faccio a impedire che il sistema si impantanasse in troppi thread di esecuzione?

Sto praticamente cercando di

  1. Comprendere la modalità di processo del server Web go
  2. Trova le funzionalità integrate di cui disponi per ottimizzare questo e / o qualsiasi altro pacchetto standard utilizzi

Come ho detto, sono molto nuovo per andare, quindi se mi manca completamente la trama su questo per favore fatemelo sapere!

Ottimizzazione / configurazione del server HTTP

Il tipo che implementa il server HTTP è http.Server . Se non si crea un http.Server ad es. Perché si chiama la funzione http.ListenAndServe() , viene creato un http.Server per te:

 func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } 

Quindi, se vuoi twittare / personalizzare il server HTTP, quindi crearne uno tu stesso e chiamare da solo il suo metodo Server.ListenAndServe() . http.Server è una struttura, il suo valore zero è una configurazione valida. Vedi il suo doc quali campi ha e quindi cosa puoi modificare / configurare.

La “Gestione processo” del server HTTP è documentata su Server.Serve() :

Serve accetta le connessioni in entrata sul Listener l, creando una nuova goroutine di servizio per ciascuna . Le goroutine di servizio leggono le richieste e poi chiamano srv.Handler per rispondere a loro. Il servizio restituisce sempre un errore non nullo.

Quindi ogni richiesta HTTP in arrivo viene gestita nella sua nuova goroutine, nel senso che vengono servite contemporaneamente. Sfortunatamente l’API non documenta alcun modo per saltare e cambiare il modo in cui funziona.

E guardando all’attuale implementazione (Go 1.6.2), non c’è neanche un modo non documentato per farlo. server.go , attualmente linea # 2107-2139 :

 2107 func (srv *Server) Serve(l net.Listener) error { 2108 defer l.Close() 2109 if fn := testHookServerServe; fn != nil { 2110 fn(srv, l) 2111 } 2112 var tempDelay time.Duration // how long to sleep on accept failure 2113 if err := srv.setupHTTP2(); err != nil { 2114 return err 2115 } 2116 for { 2117 rw, e := l.Accept() 2118 if e != nil { 2119 if ne, ok := e.(net.Error); ok && ne.Temporary() { 2120 if tempDelay == 0 { 2121 tempDelay = 5 * time.Millisecond 2122 } else { 2123 tempDelay *= 2 2124 } 2125 if max := 1 * time.Second; tempDelay > max { 2126 tempDelay = max 2127 } 2128 srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) 2129 time.Sleep(tempDelay) 2130 continue 2131 } 2132 return e 2133 } 2134 tempDelay = 0 2135 c := srv.newConn(rw) 2136 c.setState(c.rwc, StateNew) // before Serve can return 2137 go c.serve() 2138 } 2139 } 

Come puoi vedere nella riga n. 2137, la connessione è servita incondizionatamente su una nuova goroutine, quindi non c’è nulla che tu possa fare al riguardo.

Limitare le goroutine “operaie”

Se vuoi limitare il numero di richieste di servizi di goroutine, puoi comunque farlo.

Puoi limitarli su più livelli. Per limitare il livello dell’ascoltatore, vedi la risposta di Darigaaz. Per limitare il livello del gestore, continua a leggere.

Ad esempio, è ansible inserire un codice per ciascuna delle funzioni http.Handler o gestore ( http.HandlerFunc ), che procede solo se il numero di richieste simultanee di goroutine di richiesta è inferiore a un limite specificato.

Esistono numerosi costrutti per tale codice di limitazione della sincronizzazione. Un esempio potrebbe essere: creare un canale bufferizzato con la capacità che è il limite desiderato. Ogni gestore deve prima inviare un valore su questo canale, quindi eseguire il lavoro. Quando il gestore ritorna, deve ricevere un valore dal canale: quindi è meglio farlo in una funzione differita (non dimenticare di “ripulire” se stesso).

Se il buffer è pieno, una nuova richiesta che tenta di inviare sul canale verrà bloccata: attendi fino a quando una richiesta termina il suo lavoro.

Si noti che non è necessario iniettare questo codice limitativo a tutti i gestori, è ansible utilizzare un modello “middleware”, un nuovo tipo di gestore che avvolge i gestori, esegue questo processo di limitazione della sincronizzazione e chiama il gestore wrapper nel mezzo di esso.

Il vantaggio di limitare nel gestore (al contrario di limitare in Listeners) è che nel gestore sappiamo cosa fa il gestore, quindi possiamo fare una limitazione selettiva (ad esempio possiamo scegliere di limitare alcune richieste come le operazioni del database e non limitare altri come servire risorse statiche) o possiamo creare arbitrariamente gruppi di limiti multipli e distinti ai nostri bisogni (ad esempio limitare le richieste simultanee di db a 10 max, limitare le richieste statiche a 100 max, limitare le pesanti richieste computazionali a 3 max) ecc. Possiamo anche realizzare facilmente limitazioni come illimitate (o limite massimo) per gli utenti registrati / paganti e limiti bassi per gli utenti anonimi / non paganti.

Si noti inoltre che è ansible persino eseguire la limitazione della velocità in un singolo luogo, senza utilizzare i middleware. Creare un “gestore principale” e passarlo a http.ListenAndServe() (o Server.ListenAndServe() ). In questo gestore principale eseguire la limitazione della velocità (ad esempio utilizzando un canale bufferizzato come menzionato sopra) e inoltrare semplicemente la chiamata a http.ServeMux si sta utilizzando.

Ecco un semplice esempio che utilizza http.ListenAndServe() e il multiplexer predefinito del pacchetto http ( http.DefaultServeMux ) per la dimostrazione. Limita le richieste concorrenti a 2:

 func fooHandler(w http.ResponseWriter, r *http.Request) { log.Println("Foo called...") time.Sleep(3 * time.Second) w.Write([]byte("I'm Foo")) log.Println("Foo ended.") } func barHandler(w http.ResponseWriter, r *http.Request) { log.Println("Bar called...") time.Sleep(3 * time.Second) w.Write([]byte("I'm Bar")) log.Println("Bar ended.") } var ch = make(chan struct{}, 2) // 2 concurrent requests func mainHandler(w http.ResponseWriter, r *http.Request) { ch <- struct{}{} defer func() { <-ch }() http.DefaultServeMux.ServeHTTP(w, r) } func main() { http.HandleFunc("/foo", fooHandler) http.HandleFunc("/bar", barHandler) panic(http.ListenAndServe(":8080", http.HandlerFunc(mainHandler))) } 

Distribuzione

Le applicazioni Web scritte in Go non richiedono server esterni per controllare i processi, poiché il server web Go stesso gestisce le richieste contemporaneamente.

Quindi puoi avviare il tuo server web scritto in Go as-is: il server web Go è pronto per la produzione.

Naturalmente, se lo desideri, puoi utilizzare altri server per attività aggiuntive (ad esempio, gestione HTTPS, autenticazione / authorization, routing, bilanciamento del carico tra più server).

ListenAndServe avvia un server HTTP con un determinato indirizzo e gestore. Il gestore è solitamente nullo, il che significa usare DefaultServeMux . Gestire e HandleFunc aggiungere i gestori a DefaultServeMux .

Guarda http.Server , molti campi sono opzionali e funzionano bene con i valori predefiniti.

Ora consente di guardare http.ListenAndServe , non è affatto difficile

 func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } 

quindi il server predefinito è semplicissimo da creare.

 func (srv *Server) ListenAndServe() error { addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) } func (srv *Server) Serve(l net.Listener) error { defer l.Close() if fn := testHookServerServe; fn != nil { fn(srv, l) } var tempDelay time.Duration // how long to sleep on accept failure if err := srv.setupHTTP2(); err != nil { return err } for { rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve() } } 

Ascolta su “addr” e accetta ogni connessione, quindi genera una goroutine per gestire ogni connessione in modo indipendente. (HTTP / 2.0 è leggermente diverso, ma è lo stesso in generale).

Se vuoi controllare le connessioni hai 2 opzioni:

  1. Crea un server personalizzato (le sue 3 righe di codice) con il server. ConnState callback e controlla le connessioni client da lì. (ma saranno comunque accettati dal kernel)

  2. Crea server personalizzati con la tua implementazione di net.Listener (come LimitedListener ) e controlla le connessioni da lì, in questo modo avrai la massima potenza sulle connessioni.

Poiché http.Server predefinito non ha modo di essere fermato, il secondo modo è l’unico modo per terminare con garbo il listener. È ansible combinare due metodi per implementare diverse strategie, ma è già stato fatto.