Туториали

Build a Scalable Go WebSocket Service: Backpressure, Auth, and Reliability

Изградете скалабилна Go WebSocket услуга: повратен притисок, автентикација и доверливост

WebSocket серверот лесно се демонстрира, а изненадувачки тешко се управува. Ракувањето ретко е проблемот. Вистинскиот инженеринг започнува кога илјадници клиенти остануваат поврзани, еден читач престанува да прима, Redis исчезнува, ингеренциите истекуваат или распоредувањето бара секој јазол да се исклучи без протекување на горутини.

Овој туторијал гради Go услуга ориентирана кон продукциска употреба, со строго ограничување на поврзувањата по јазол, ограничени излезни редици, исфрлање на бавни клиенти, автентицирани ракувања, ping/pong отчукувања, постепено исклучување, проверки на здравјето, метрики и fan-out поддржан од Redis низ реплики.

Архитектура и договор за испорака

Секој процес ги поседува своите локални WebSocket поврзувања. Пораката од клиентот се валидира, се означува со автентицираниот идентитет и јазолот што објавува, а потоа се објавува во Redis. Секој процес се претплатува на истиот канал и го проследува секој настан до своите локални клиенти.

Дизајнот намерно користи Redis Pub/Sub. Тој е брз и соодветен за краткотрајно присуство, контролни табли, известувања и ажурирања за соработка во живо, но не е траен. Исклучен претплатник може да пропушти настани. Апликациите што бараат репродукција треба да користат траен дневник, како Redis Streams или друг брокер, и да воведат ID на пораки, потврди и задржување.

Меморијата останува ограничена на две места:

  • MAX_CLIENTS ги ограничува поврзувањата што ги прифаќа еден процес.
  • Секој клиент добива излезен канал со фиксна големина. Ако се наполни, тој клиент се исклучува наместо да го забави целиот хаб или да троши неограничена меморија.

Сите јазли примаат пораки преку Redis, вклучувајќи го и јазолот што ја испратил пораката. Тој единствен пат избегнува двојна локална испорака и ја одржува семантиката на редоследот конзистентна во рамките на ограничувањата на Pub/Sub.

Предуслови и распоред на проектот

Ви треба Go 1.22 или понов, Docker со Compose за примерот за распоредување и OpenSSL за генерирање тајна. Апликацијата користи github.com/gorilla/websocket 1.5.3 и github.com/redis/go-redis/v9 9.7.0.

realtime/
├── cmd/
│   ├── check/main.go
│   └── token/main.go
├── Dockerfile
├── compose.yaml
├── go.mod
├── haproxy.cfg
└── main.go

Креирајте go.mod:

module example.com/realtime

go 1.22.0

require (
	github.com/gorilla/websocket v1.5.3
	github.com/redis/go-redis/v9 v9.7.0
)

Имплементирајте ја WebSocket услугата

Серверот одржува точно еден читач и еден запишувач по поврзување, согласно договорот за конкурентност на Gorilla WebSocket. Само read pump обработува рамки од клиентот. Само write pump испраќа обични рамки и отчукувања.

package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/gorilla/websocket"
	"github.com/redis/go-redis/v9"
)

const (
	writeWait = 10 * time.Second
	pongWait  = 60 * time.Second
	pingEvery = 25 * time.Second
	maxFrame  = 4096
	queueSize = 64
)

type counters struct {
	connected, accepted, published, publishErrors, slowDisconnects atomic.Int64
}

type config struct {
	addr, redisURL, channel, node string
	secret                       []byte
	maxClients                   int
	origins                      map[string]struct{}
}

type app struct {
	ctx     context.Context
	cfg     config
	redis   *redis.Client
	hub     *hub
	slots   chan struct{}
	metrics *counters
	pumps   sync.WaitGroup
}

type hub struct {
	register, unregister chan *client
	inbound             chan []byte
	metrics             *counters
}

type client struct {
	app  *app
	conn *websocket.Conn
	send chan []byte
	user string
}

func env(name, fallback string) string {
	if value := os.Getenv(name); value != "" {
		return value
	}
	return fallback
}

func loadConfig() config {
	maxClients, err := strconv.Atoi(env("MAX_CLIENTS", "5000"))
	if err != nil || maxClients < 1 {
		panic("MAX_CLIENTS must be a positive integer")
	}
	secret := []byte(os.Getenv("AUTH_SECRET"))
	if len(secret) < 32 {
		panic("AUTH_SECRET must contain at least 32 characters")
	}
	origins := make(map[string]struct{})
	for _, origin := range strings.Split(env("ALLOWED_ORIGINS", ""), ",") {
		if origin = strings.TrimSpace(origin); origin != "" {
			origins[origin] = struct{}{}
		}
	}
	host, _ := os.Hostname()
	return config{
		addr:       env("LISTEN_ADDR", ":8080"),
		redisURL:   env("REDIS_URL", "redis://127.0.0.1:6379/0"),
		channel:    env("REDIS_CHANNEL", "ws:broadcast"),
		node:       env("NODE_ID", host),
		secret:     secret,
		maxClients: maxClients,
		origins:    origins,
	}
}

func (a *app) authenticate(r *http.Request) (string, error) {
	scheme, token, ok := strings.Cut(r.Header.Get("Authorization"), " ")
	if !ok || !strings.EqualFold(scheme, "Bearer") {
		return "", fmt.Errorf("missing bearer token")
	}
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return "", fmt.Errorf("malformed token")
	}
	signingInput := parts[0] + "." + parts[1]
	supplied, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		return "", fmt.Errorf("malformed signature")
	}
	mac := hmac.New(sha256.New, a.cfg.secret)
	mac.Write([]byte(signingInput))
	if subtle.ConstantTimeCompare(supplied, mac.Sum(nil)) != 1 {
		return "", fmt.Errorf("invalid signature")
	}
	subjectBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return "", fmt.Errorf("malformed subject")
	}
	subject := string(subjectBytes)
	if subject == "" || len(subject) > 128 {
		return "", fmt.Errorf("invalid subject")
	}
	expiry, err := strconv.ParseInt(parts[1], 10, 64)
	if err != nil || time.Now().Unix() >= expiry {
		return "", fmt.Errorf("expired token")
	}
	return subject, nil
}

func (h *hub) run(ctx context.Context) {
	clients := make(map[*client]struct{})
	drop := func(c *client, slow bool) {
		if _, exists := clients[c]; !exists {
			return
		}
		delete(clients, c)
		close(c.send)
		c.conn.Close()
		h.metrics.connected.Add(-1)
		if slow {
			h.metrics.slowDisconnects.Add(1)
		}
	}

	for {
		select {
		case c := <-h.register:
			clients[c] = struct{}{}
			h.metrics.connected.Add(1)
		case c := <-h.unregister:
			drop(c, false)
		case message := <-h.inbound:
			for c := range clients {
				select {
				case c.send <- message:
				default:
					drop(c, true)
				}
			}
		case <-ctx.Done():
			for c := range clients {
				drop(c, false)
			}
			return
		}
	}
}

func (c *client) readPump() {
	defer c.conn.Close()
	c.conn.SetReadLimit(maxFrame)
	_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error {
		return c.conn.SetReadDeadline(time.Now().Add(pongWait))
	})

	for {
		messageType, data, err := c.conn.ReadMessage()
		if err != nil {
			return
		}
		if messageType != websocket.TextMessage {
			continue
		}
		var input struct {
			Body string `json:"body"`
		}
		if json.Unmarshal(data, &input) != nil || input.Body == "" {
			continue
		}
		event := struct {
			Type, User, Body, Node, At string
		}{
			Type: "message", User: c.user, Body: input.Body,
			Node: c.app.cfg.node, At: time.Now().UTC().Format(time.RFC3339Nano),
		}
		payload, err := json.Marshal(event)
		if err != nil {
			return
		}

		ctx, cancel := context.WithTimeout(c.app.ctx, 1500*time.Millisecond)
		err = c.app.redis.Publish(ctx, c.app.cfg.channel, payload).Err()
		cancel()
		if err != nil {
			c.app.metrics.publishErrors.Add(1)
			slog.Error("publish failed", "user", c.user, "error", err)
			_ = c.conn.WriteControl(
				websocket.CloseMessage,
				websocket.FormatCloseMessage(1011, "broker unavailable"),
				time.Now().Add(writeWait),
			)
			return
		}
		c.app.metrics.published.Add(1)
	}
}

func (c *client) writePump() {
	ticker := time.NewTicker(pingEvery)
	defer ticker.Stop()

	for {
		select {
		case message, ok := <-c.send:
			_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				_ = c.conn.WriteControl(
					websocket.CloseMessage,
					websocket.FormatCloseMessage(1001, "server closing"),
					time.Now().Add(writeWait),
				)
				return
			}
			if c.conn.WriteMessage(websocket.TextMessage, message) != nil {
				return
			}
		case <-ticker.C:
			_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if c.conn.WriteMessage(websocket.PingMessage, nil) != nil {
				return
			}
		case <-c.app.ctx.Done():
			return
		}
	}
}

func (a *app) serveWS(w http.ResponseWriter, r *http.Request) {
	user, err := a.authenticate(r)
	if err != nil {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	select {
	case a.slots <- struct{}{}:
	default:
		w.Header().Set("Retry-After", "5")
		http.Error(w, "connection capacity reached", http.StatusServiceUnavailable)
		return
	}

	upgrader := websocket.Upgrader{
		ReadBufferSize:  4096,
		WriteBufferSize: 4096,
		CheckOrigin: func(r *http.Request) bool {
			origin := r.Header.Get("Origin")
			if origin == "" {
				return true
			}
			_, allowed := a.cfg.origins[origin]
			return allowed
		},
	}
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		<-a.slots
		return
	}
	c := &client{app: a, conn: conn, send: make(chan []byte, queueSize), user: user}
	select {
	case a.hub.register <- c:
	case <-a.ctx.Done():
		conn.Close()
		<-a.slots
		return
	}

	a.metrics.accepted.Add(1)
	var once sync.Once
	cleanup := func() {
		once.Do(func() {
			conn.Close()
			select {
			case a.hub.unregister <- c:
			case <-a.ctx.Done():
			}
			<-a.slots
		})
	}

	a.pumps.Add(2)
	go func() {
		defer a.pumps.Done()
		c.readPump()
		cleanup()
	}()
	go func() {
		defer a.pumps.Done()
		c.writePump()
		cleanup()
	}()
}

func subscribe(ctx context.Context, rdb *redis.Client, channel string, inbound chan<- []byte) {
	backoff := time.Second
	for ctx.Err() == nil {
		pubsub := rdb.Subscribe(ctx, channel)
		_, err := pubsub.Receive(ctx)
		if err == nil {
			backoff = time.Second
			for {
				message, receiveErr := pubsub.ReceiveMessage(ctx)
				if receiveErr != nil {
					err = receiveErr
					break
				}
				select {
				case inbound <- []byte(message.Payload):
				case <-ctx.Done():
					pubsub.Close()
					return
				}
			}
		}
		pubsub.Close()
		if ctx.Err() != nil {
			return
		}
		slog.Error("subscription interrupted", "error", err, "retry_in", backoff)
		timer := time.NewTimer(backoff)
		select {
		case <-timer.C:
		case <-ctx.Done():
			timer.Stop()
			return
		}
		if backoff < 15*time.Second {
			backoff *= 2
		}
	}
}

func main() {
	cfg := loadConfig()
	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	options, err := redis.ParseURL(cfg.redisURL)
	if err != nil {
		panic(err)
	}
	options.DialTimeout = 2 * time.Second
	options.ReadTimeout = 2 * time.Second
	options.WriteTimeout = 2 * time.Second
	options.PoolTimeout = 2 * time.Second
	options.PoolSize = 20
	rdb := redis.NewClient(options)

	startupCtx, startupCancel := context.WithTimeout(ctx, 2*time.Second)
	err = rdb.Ping(startupCtx).Err()
	startupCancel()
	if err != nil {
		panic(fmt.Sprintf("Redis unavailable: %v", err))
	}

	metrics := &counters{}
	h := &hub{
		register: make(chan *client), unregister: make(chan *client),
		inbound: make(chan []byte, 1024), metrics: metrics,
	}
	a := &app{
		ctx: ctx, cfg: cfg, redis: rdb, hub: h,
		slots: make(chan struct{}, cfg.maxClients), metrics: metrics,
	}
	go h.run(ctx)
	go subscribe(ctx, rdb, cfg.channel, h.inbound)

	mux := http.NewServeMux()
	mux.HandleFunc("/ws", a.serveWS)
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
		checkCtx, checkCancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
		defer checkCancel()
		if rdb.Ping(checkCtx).Err() != nil {
			http.Error(w, "not ready", http.StatusServiceUnavailable)
			return
		}
		w.WriteHeader(http.StatusOK)
	})
	mux.HandleFunc("/metrics", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/plain; version=0.0.4")
		fmt.Fprintf(w,
			"ws_connected %d\nws_accepted_total %d\nws_published_total %d\n"+
				"ws_publish_errors_total %d\nws_slow_disconnects_total %d\n",
			metrics.connected.Load(), metrics.accepted.Load(),
			metrics.published.Load(), metrics.publishErrors.Load(),
			metrics.slowDisconnects.Load(),
		)
	})

	server := &http.Server{
		Addr: cfg.addr, Handler: mux,
		ReadHeaderTimeout: 5 * time.Second,
		IdleTimeout:       60 * time.Second,
		MaxHeaderBytes:    16 << 10,
	}
	serverErrors := make(chan error, 1)
	go func() {
		slog.Info("server listening", "address", cfg.addr, "node", cfg.node)
		serverErrors <- server.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
	case err := <-serverErrors:
		if err != nil && err != http.ErrServerClosed {
			slog.Error("HTTP server stopped", "error", err)
		}
		cancel()
	}

	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
	_ = server.Shutdown(shutdownCtx)
	shutdownCancel()
	cancel()

	done := make(chan struct{})
	go func() {
		a.pumps.Wait()
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		slog.Warn("timed out waiting for connection pumps")
	}
	_ = rdb.Close()
}

Серверот намерно го изоставува http.Server.WriteTimeout. Вообичаениот краток рок за одговор може да прекине долгоактивни надградени поврзувања. Наместо тоа, WebSocket запишувањата се ограничени поединечно со рокови.

Издавање краткотрајни токени за автентикација

Овој компактен HMAC формат на токен е соодветен кога една доверлива услуга за автентикација издава ингеренции за овој WebSocket слој. Содржи base64url субјект, Unix истекување и SHA-256 HMAC. Не е претставен како JWT и не треба да се меша со JWT алатки.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"os"
	"time"
)

func main() {
	if len(os.Args) != 3 {
		panic("usage: token SUBJECT DURATION")
	}
	secret := []byte(os.Getenv("AUTH_SECRET"))
	if len(secret) < 32 {
		panic("AUTH_SECRET must contain at least 32 characters")
	}
	duration, err := time.ParseDuration(os.Args[2])
	if err != nil || duration <= 0 {
		panic("duration must be positive, for example 15m")
	}
	subject := base64.RawURLEncoding.EncodeToString([]byte(os.Args[1]))
	unsigned := fmt.Sprintf("%s.%d", subject, time.Now().Add(duration).Unix())
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(unsigned))
	fmt.Printf("%s.%s\n", unsigned,
		base64.RawURLEncoding.EncodeToString(mac.Sum(nil)))
}

JavaScript во прелистувач не може да постави произволно заглавие Authorization при природното WebSocket ракување. За распоредувања во прелистувач, заменете ја нормалната автентицирана сесија за краткотраен билет за еднократна употреба и пренесете го билетот во URL-то. Осигурете се дека граничниот прокси ги маскира низните барања од дневниците. Не ставајте API клучеви за повеќекратна употреба во URL-адреси.

Контејнеризирајте и скалирајте хоризонтално

Контејнерот работи без root привилегии. HAProxy ја завршува јавната HTTP врска и ги распределува надградените поврзувања меѓу реплики на апликацијата; на WebSocket не му требаат лепливи сесии бидејќи секој јазол го користи споделениот брокер.

FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/realtime .

FROM alpine:3.20
RUN addgroup -S app && adduser -S -G app app
COPY --from=build /out/realtime /usr/local/bin/realtime
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/realtime"]

Креирајте haproxy.cfg:

global
    log stdout format raw local0

defaults
    mode http
    log global
    option httplog
    timeout connect 3s
    timeout client 1h
    timeout server 1h
    timeout http-request 10s

resolvers docker
    nameserver dns 127.0.0.11:53
    resolve_retries 3
    timeout resolve 1s
    timeout retry 1s
    hold valid 10s

frontend public
    bind *:8080
    default_backend websocket_nodes

backend websocket_nodes
    balance leastconn
    option httpchk GET /readyz
    http-check expect status 200
    server-template app 1-10 app:8080 check resolvers docker init-addr libc,none

Креирајте compose.yaml:

services:
  app:
    build: .
    environment:
      AUTH_SECRET: "${AUTH_SECRET:?set AUTH_SECRET}"
      REDIS_URL: "redis://redis:6379/0"
      REDIS_CHANNEL: "ws:broadcast"
      ALLOWED_ORIGINS: "https://app.example.com"
      MAX_CLIENTS: "5000"
    expose:
      - "8080"
    depends_on:
      redis:
        condition: service_healthy
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    restart: unless-stopped

  redis:
    image: redis:7.4-alpine
    command: ["redis-server", "--save", "", "--appendonly", "no"]
    expose:
      - "6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 1s
      retries: 20
    restart: unless-stopped

  proxy:
    image: haproxy:3.0-alpine
    volumes:
      - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro"
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      - app
    restart: unless-stopped

Генерирајте зависности и стартувајте три реплики на апликацијата:

go mod tidy
export AUTH_SECRET="$(openssl rand -base64 32)"
docker compose up --build --scale app=3

Примерот го врзува HAProxy само за loopback. Во продукција, завршете TLS на зајакнат граничен прокси, изложете ја само наменетата HTTPS порта преку заштитниот ѕид на домаќинот и чувајте го Redis на приватна мрежа. Далечинско распоредување на Redis треба да користи автентициран rediss:// URL со проверка на сертификатот. Не го изложувајте /metrics јавно; ограничете го со мрежна политика или посебен внатрешен listener.

Потврдете ја испораката меѓу поврзувања

Додајте cmd/check/main.go. Тој отвора две автентицирани поврзувања, објавува преку едното и го потврдува приемот преку другото.

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/gorilla/websocket"
)

func connect(token string) *websocket.Conn {
	headers := http.Header{"Authorization": {"Bearer " + token}}
	conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/ws", headers)
	if err != nil {
		panic(err)
	}
	return conn
}

func main() {
	if len(os.Args) != 2 {
		panic("usage: check TOKEN")
	}
	first := connect(os.Args[1])
	second := connect(os.Args[1])
	defer first.Close()
	defer second.Close()

	if err := first.WriteJSON(map[string]string{"body": "cross-node hello"}); err != nil {
		panic(err)
	}
	_ = second.SetReadDeadline(time.Now().Add(5 * time.Second))
	_, message, err := second.ReadMessage()
	if err != nil {
		panic(err)
	}
	fmt.Println(string(message))
}
TOKEN="$(go run ./cmd/token alice 15m)"
go run ./cmd/check "$TOKEN"
curl -fsS http://127.0.0.1:8080/readyz
curl -fsS http://127.0.0.1:8080/metrics

Полето node на настанот ја идентификува репликата што објавува. Со неколку долготрајни клиенти, политиката на HAProxy за најмалку поврзувања ги распределува поврзувањата меѓу здрави јазли, додека Redis ја прави испораката независна од тоа која реплика го поседува кое било поврзување.

Вежби за дефекти и оперативно прилагодување

Паузирајте го Redis со docker compose pause redis. Подготвеноста треба да врати 503, циклусите за претплата треба повторно да се обидуваат со ограничен експоненцијален backoff, а клиент што се обидува да објави треба да добие код за затворање 1011 наместо лажен успех. Продолжете го со docker compose unpause redis; претплатите треба автоматски да се обноват.

Тестирајте го повратниот притисок со клиент што го завршува ракувањето, но никогаш не чита. Штом неговата редица од 64 пораки се наполни, ws_slow_disconnects_total треба да се зголеми, а здравите клиенти треба да продолжат да примаат пораки. Ова е суштинското својство на изолација: еден бавен потрошувач чини едно поврзување, не целиот хаб.

Планирањето на капацитетот мора да ги земе предвид дескрипторите на датотеки, стековите на горутини, TLS баферите на работ, клиентските редици со 64 места и максималната рамка од 4 KiB. Датотеката Compose го зголемува ограничувањето на контејнерот, но плафонот за дескриптори на датотеки на домаќинот исто така мора да биде проверен и конфигуриран од администратор. Зголемувајте ги ограничувањата само откако ќе ги измерите меморијата и однесувањето при повторно поврзување.

Додајте ограничување на стапката на ракување на ниво на работ, ограничувања на поврзувања по идентитет или извор и ограничување на издавањето токени. Ротирајте ги тајните за автентикација со стратегија на преклопување, наместо моментално да го замените единствениот клуч. За посилно управување со клучеви, користете верзионирани клучеви за потпишување и вклучете идентификатор на клуч во форматот на токенот.

Вообичаени продукциски дефекти

  • Секој клиент дели еден запишувач: блокиран сокет ги запира неповрзаните корисници. Задржете еден ограничен запишувач по поврзување.
  • Излезните редици растат динамички: исцрпувањето на меморијата само доаѓа подоцна. Фиксирајте го капацитетот и дефинирајте политика за исфрлање.
  • Овозможено е само TCP keepalive: мртвите врсници може да преживеат предолго преку проксија. Користете ping/pong рокови на ниво на апликација.
  • Јазолот што ја испраќа пораката емитува локално и преку Redis: локалните клиенти примаат дупликати. Користете еден пат на испорака.
  • Redis Pub/Sub се опишува како сигурно складиште: тој обезбедува fan-out во живо, не репродукција или трајна потврда.
  • Проверките на потеклото се оневозможени: автентицираните сесии во прелистувачот стануваат ранливи на киднапирање на WebSocket меѓу страници.
  • Подготвеноста и живоста се идентични: нездрава зависност може да остави реплика што прима нови поврзувања. Одделете го здравјето на процесот од подготвеноста за сообраќај.

Конечна листа за проверка

  • Неважечките, изменетите и истечените токени добиваат HTTP 401 пред надградбата.
  • Недозволените потекла од прелистувачот не успеваат при ракувањето.
  • Процесот одбива поврзувања над MAX_CLIENTS со HTTP 503.
  • Ping/pong роковите ги отстрануваат мртвите врсници.
  • Бавните потрошувачи се исклучуваат без да ги блокираат здравите клиенти.
  • Операциите со Redis имаат експлицитни рокови за поврзување, базен, читање, запишување и секое објавување.
  • Повеќе реплики разменуваат пораки преку споделениот канал.
  • Дефект на Redis предизвикува неуспех на подготвеноста, а објавувањето се затвора искрено.
  • SIGTERM го прекинува прифаќањето, ги затвора клиентите и кратко чека за pump-овите.
  • TLS, правилата на заштитниот ѕид, пристапот до метриките и изложеноста на Redis се контролираат на границите на распоредувањето.

Скалабилна WebSocket услуга не се дефинира според тоа колку брзо прифаќа надградба. Таа се дефинира според ограничувањата што ги спроведува кога мрежата станува неправедна. Ограничете го секој ресурс, направете го дефектот видлив и исклучете ги клиентите на кои системот не може безбедно да им служи. Тие избори претвораат постојан сокет од оперативна обврска во сигурна продукциска примитива.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.