MVP Single Node with Manual Config in Docker
This commit is contained in:
commit
8212699f23
27 files changed
+3818
No files matched your search
@@ -0,0 +1,19 @@
|
||||
FROM golang:1.23-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod ./
|
||||
COPY main.go ./
|
||||
RUN CGO_ENABLED=0 go build -trimpath -o /out/backend .
|
||||
|
||||
FROM alpine:3.20
|
||||
# iproute2 — установка маршрутов на клиентские префиксы; curl нужен для
|
||||
# проверки маршрутизации между приватными сегментами; tzdata — корректная
|
||||
# локальная дата в ответе.
|
||||
RUN apk add --no-cache iproute2 curl tzdata
|
||||
COPY --from=build /out/backend /usr/local/bin/backend
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
|
||||
CMD curl -fsS http://127.0.0.1:8080/healthz >/dev/null || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Маршруты бэкенда (профиль A дизайна hpnn_v1, §3.2):
|
||||
# - default остаётся на шлюзе Docker (.254) — собственный исходящий трафик
|
||||
# бэкенда идёт мимо балансировщика;
|
||||
# - через балансировщик маршрутизируются только клиентские префиксы и
|
||||
# соседний приватный сегмент, то есть ответы на балансируемые сессии и
|
||||
# транзит между сегментами.
|
||||
set -eu
|
||||
|
||||
: "${LB_GW:?не задан LB_GW}"
|
||||
: "${VIA_LB_ROUTES:=}"
|
||||
|
||||
for net in $(echo "$VIA_LB_ROUTES" | tr ',' ' '); do
|
||||
ip route replace "$net" via "$LB_GW"
|
||||
echo "[backend] маршрут $net via $LB_GW"
|
||||
done
|
||||
|
||||
echo "[backend] таблица маршрутизации:"
|
||||
ip route
|
||||
|
||||
exec /usr/local/bin/backend "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
module hpnn/backend
|
||||
|
||||
go 1.22
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Бэкенд стенда hpnn_v2: показывает имя хоста, дату и время, а также реальный
|
||||
// адрес клиента. Последнее — ключевая проверка схемы DNAT-без-SNAT: если в
|
||||
// поле client стоит адрес из клиентской подсети, а не адрес балансировщика,
|
||||
// значит трансляция источника нигде не выполняется.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var requests atomic.Uint64
|
||||
|
||||
func localAddr(r *http.Request) string {
|
||||
if a, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
|
||||
return a.String()
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func main() {
|
||||
listen := os.Getenv("LISTEN")
|
||||
if listen == "" {
|
||||
listen = ":8080"
|
||||
}
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = "unknown"
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Одна строка на запрос: удобно читать вывод цикла из 20 curl.
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
n := requests.Add(1)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprintf(w, "backend=%-6s time=%s client=%-21s served=%-21s req=%d\n",
|
||||
host,
|
||||
time.Now().Format("2006-01-02 15:04:05 MST"),
|
||||
r.RemoteAddr,
|
||||
localAddr(r),
|
||||
n)
|
||||
})
|
||||
|
||||
// Развёрнутый вид — для ручного осмотра в браузере.
|
||||
mux.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) {
|
||||
n := requests.Add(1)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprintf(w, "Имя хоста : %s\n", host)
|
||||
fmt.Fprintf(w, "Дата и время : %s\n", time.Now().Format("2006-01-02 15:04:05 MST -07:00"))
|
||||
fmt.Fprintf(w, "Адрес клиента : %s\n", r.RemoteAddr)
|
||||
fmt.Fprintf(w, "Адрес назначения: %s\n", localAddr(r))
|
||||
fmt.Fprintf(w, "Host-заголовок : %s\n", r.Host)
|
||||
fmt.Fprintf(w, "Запрос № : %d\n", n)
|
||||
})
|
||||
|
||||
// Цель health-проб балансировщика.
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprintln(w, "ok")
|
||||
})
|
||||
|
||||
// Длинный ответ: тело отдаётся по строке в секунду. Нужен, чтобы держать
|
||||
// открытую сессию во время смены состава пула и проверять, что уже
|
||||
// установленное соединение не рвётся. /slow?seconds=20
|
||||
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
|
||||
seconds := 10
|
||||
if v := r.URL.Query().Get("seconds"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 120 {
|
||||
seconds = n
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
flusher, _ := w.(http.Flusher)
|
||||
for i := 1; i <= seconds; i++ {
|
||||
fmt.Fprintf(w, "backend=%s tick=%d/%d time=%s\n", host, i, seconds,
|
||||
time.Now().Format("15:04:05"))
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: listen,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("backend %s слушает %s", host, listen)
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user