505 lines
16 KiB
Go
505 lines
16 KiB
Go
// hcd — подсистема health-check узла балансировщика (§7 дизайн-концепции
|
||||
|
|
// hpnn_v1, адаптированная к одноузловому прототипу).
|
|||
|
|
//
|
|||
|
|
// Демон проверяет живость членов пула, ведёт их состояние с гистерезисом и
|
|||
|
|
// отражает состав пула в датапасе: пересчитывает Maglev-раскладку слотов и
|
|||
|
|
// заливает таблицу слотов OVS одной атомарной транзакцией.
|
|||
|
|
//
|
|||
|
|
// Пробы уходят с уникального адреса узла в сегменте бэкенда, а не с VIP —
|
|||
|
|
// иначе ответ вернулся бы не тому, кто проверял (§7.1).
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"flag"
|
|||
|
|
"fmt"
|
|||
|
|
"log"
|
|||
|
|
"net"
|
|||
|
|
"net/http"
|
|||
|
|
"os"
|
|||
|
|
"os/exec"
|
|||
|
|
"os/signal"
|
|||
|
|
"sort"
|
|||
|
|
"sync"
|
|||
|
|
"syscall"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// --- конфигурация ------------------------------------------------------------
|
|||
|
|
|
|||
|
|
type Config struct {
|
|||
|
|
Bridge string `json:"bridge"`
|
|||
|
|
OFVersion string `json:"of_version"`
|
|||
|
|
PoolID uint32 `json:"pool_id"`
|
|||
|
|
Slots int `json:"slots"`
|
|||
|
|
SlotTable int `json:"slot_table"`
|
|||
|
|
DNATTable int `json:"dnat_table"`
|
|||
|
|
Probe string `json:"probe"`
|
|||
|
|
HTTPPath string `json:"http_path"`
|
|||
|
|
Interval string `json:"interval"`
|
|||
|
|
Timeout string `json:"timeout"`
|
|||
|
|
Rise int `json:"rise"`
|
|||
|
|
Fall int `json:"fall"`
|
|||
|
|
API string `json:"api"`
|
|||
|
|
ApplyScript string `json:"apply_script"`
|
|||
|
|
Members []*Member `json:"members"`
|
|||
|
|
|
|||
|
|
interval time.Duration
|
|||
|
|
timeout time.Duration
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type Member struct {
|
|||
|
|
ID int `json:"id"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Address string `json:"address"`
|
|||
|
|
Port int `json:"port"`
|
|||
|
|
Weight int `json:"weight"`
|
|||
|
|
Source string `json:"source"`
|
|||
|
|
|
|||
|
|
mu sync.Mutex
|
|||
|
|
state string // up | down
|
|||
|
|
admin string // enabled | drain
|
|||
|
|
okStreak int
|
|||
|
|
failStreak int
|
|||
|
|
lastChange time.Time
|
|||
|
|
lastErr string
|
|||
|
|
lastLatency time.Duration
|
|||
|
|
probes uint64
|
|||
|
|
failures uint64
|
|||
|
|
transitions uint64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *Member) Key() string { return fmt.Sprintf("%s:%d", m.Address, m.Port) }
|
|||
|
|
|
|||
|
|
func (m *Member) WeightOrDefault() int {
|
|||
|
|
if m.Weight <= 0 {
|
|||
|
|
return 1
|
|||
|
|
}
|
|||
|
|
return m.Weight
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Active — член участвует в раскладке слотов: жив и не выведен на дренаж.
|
|||
|
|
func (m *Member) Active() bool {
|
|||
|
|
m.mu.Lock()
|
|||
|
|
defer m.mu.Unlock()
|
|||
|
|
return m.state == "up" && m.admin == "enabled"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type memberView struct {
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Address string `json:"address"`
|
|||
|
|
Port int `json:"port"`
|
|||
|
|
State string `json:"state"`
|
|||
|
|
Admin string `json:"admin"`
|
|||
|
|
Active bool `json:"active"`
|
|||
|
|
Since string `json:"since"`
|
|||
|
|
LastError string `json:"last_error,omitempty"`
|
|||
|
|
LatencyMS int64 `json:"latency_ms"`
|
|||
|
|
Probes uint64 `json:"probes"`
|
|||
|
|
Failures uint64 `json:"failures"`
|
|||
|
|
Transitions uint64 `json:"transitions"`
|
|||
|
|
Slots int `json:"slots"`
|
|||
|
|
Source string `json:"probe_source"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- пробы -------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
// probe возвращает nil, если член ответил. Источник соединения жёстко
|
|||
|
|
// привязан к адресу узла в сегменте этого члена.
|
|||
|
|
func probe(ctx context.Context, cfg *Config, m *Member) error {
|
|||
|
|
dialer := &net.Dialer{
|
|||
|
|
Timeout: cfg.timeout,
|
|||
|
|
LocalAddr: &net.TCPAddr{IP: net.ParseIP(m.Source)},
|
|||
|
|
}
|
|||
|
|
target := fmt.Sprintf("%s:%d", m.Address, m.Port)
|
|||
|
|
|
|||
|
|
if cfg.Probe == "tcp" {
|
|||
|
|
conn, err := dialer.DialContext(ctx, "tcp", target)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return conn.Close()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
client := &http.Client{
|
|||
|
|
Timeout: cfg.timeout,
|
|||
|
|
Transport: &http.Transport{
|
|||
|
|
DialContext: dialer.DialContext,
|
|||
|
|
DisableKeepAlives: true, // каждая проба — новое соединение
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
|||
|
|
fmt.Sprintf("http://%s%s", target, cfg.HTTPPath), nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
resp, err := client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|||
|
|
return fmt.Errorf("код ответа %d", resp.StatusCode)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// observe применяет результат пробы с гистерезисом и возвращает true, если
|
|||
|
|
// состояние члена изменилось.
|
|||
|
|
func observe(m *Member, cfg *Config, err error, latency time.Duration) bool {
|
|||
|
|
m.mu.Lock()
|
|||
|
|
defer m.mu.Unlock()
|
|||
|
|
|
|||
|
|
m.probes++
|
|||
|
|
m.lastLatency = latency
|
|||
|
|
changed := false
|
|||
|
|
|
|||
|
|
if err == nil {
|
|||
|
|
m.lastErr = ""
|
|||
|
|
m.failStreak = 0
|
|||
|
|
m.okStreak++
|
|||
|
|
if m.state != "up" && m.okStreak >= cfg.Rise {
|
|||
|
|
m.state = "up"
|
|||
|
|
m.lastChange = time.Now()
|
|||
|
|
m.transitions++
|
|||
|
|
changed = true
|
|||
|
|
log.Printf("член %s (%s) -> up после %d успешных проб", m.Name, m.Key(), m.okStreak)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
m.failures++
|
|||
|
|
m.lastErr = err.Error()
|
|||
|
|
m.okStreak = 0
|
|||
|
|
m.failStreak++
|
|||
|
|
if m.state != "down" && m.failStreak >= cfg.Fall {
|
|||
|
|
m.state = "down"
|
|||
|
|
m.lastChange = time.Now()
|
|||
|
|
m.transitions++
|
|||
|
|
changed = true
|
|||
|
|
log.Printf("член %s (%s) -> down после %d неудач: %v", m.Name, m.Key(), m.failStreak, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return changed
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- применение раскладки ----------------------------------------------------
|
|||
|
|
|
|||
|
|
type Agent struct {
|
|||
|
|
cfg *Config
|
|||
|
|
|
|||
|
|
mu sync.Mutex
|
|||
|
|
table []int
|
|||
|
|
digest string
|
|||
|
|
applies uint64
|
|||
|
|
errors uint64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// apply пересчитывает раскладку и, если она изменилась, заливает таблицу
|
|||
|
|
// слотов атомарным бандлом. force заставляет залить даже неизменившуюся —
|
|||
|
|
// нужно после перезаливки всего пайплайна, которая стирает слоты.
|
|||
|
|
func (a *Agent) apply(force bool) error {
|
|||
|
|
table := BuildSlotTable(a.cfg.PoolID, a.cfg.Members, a.cfg.Slots)
|
|||
|
|
digest := Digest(table)
|
|||
|
|
|
|||
|
|
a.mu.Lock()
|
|||
|
|
unchanged := digest == a.digest && !force
|
|||
|
|
a.mu.Unlock()
|
|||
|
|
if unchanged {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bundle := FlowBundle(table, a.cfg.SlotTable, a.cfg.DNATTable)
|
|||
|
|
path := "/var/run/openvswitch/slots.bundle"
|
|||
|
|
if err := os.WriteFile(path, []byte(bundle), 0o644); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ovs-ofctl bundle выполняет delete+add как одну транзакцию: датапас не
|
|||
|
|
// проходит через состояние с полупустой таблицей слотов.
|
|||
|
|
out, err := exec.Command("ovs-ofctl", "-O", a.cfg.OFVersion, "bundle", a.cfg.Bridge, path).CombinedOutput()
|
|||
|
|
a.mu.Lock()
|
|||
|
|
defer a.mu.Unlock()
|
|||
|
|
if err != nil {
|
|||
|
|
a.errors++
|
|||
|
|
return fmt.Errorf("ovs-ofctl bundle: %v: %s", err, out)
|
|||
|
|
}
|
|||
|
|
a.table = table
|
|||
|
|
a.digest = digest
|
|||
|
|
a.applies++
|
|||
|
|
|
|||
|
|
counts := SlotCounts(table)
|
|||
|
|
parts := make([]string, 0, len(counts))
|
|||
|
|
for _, m := range a.cfg.Members {
|
|||
|
|
if n, ok := counts[m.ID]; ok {
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s=%d", m.Name, n))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
sort.Strings(parts)
|
|||
|
|
if len(table) == 0 {
|
|||
|
|
log.Printf("раскладка применена: живых членов нет, fail-close (трафик на VIP отбрасывается)")
|
|||
|
|
} else {
|
|||
|
|
log.Printf("раскладка применена: слоты %v, дайджест %s", parts, digest)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (a *Agent) slotsOf(id int) int {
|
|||
|
|
a.mu.Lock()
|
|||
|
|
defer a.mu.Unlock()
|
|||
|
|
return SlotCounts(a.table)[id]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- HTTP API ----------------------------------------------------------------
|
|||
|
|
|
|||
|
|
func (a *Agent) memberByName(name string) *Member {
|
|||
|
|
for _, m := range a.cfg.Members {
|
|||
|
|
if m.Name == name {
|
|||
|
|
return m
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (a *Agent) setAdmin(w http.ResponseWriter, r *http.Request, admin string) {
|
|||
|
|
name := r.URL.Query().Get("member")
|
|||
|
|
m := a.memberByName(name)
|
|||
|
|
if m == nil {
|
|||
|
|
http.Error(w, fmt.Sprintf("член %q не найден\n", name), http.StatusNotFound)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
m.mu.Lock()
|
|||
|
|
prev := m.admin
|
|||
|
|
m.admin = admin
|
|||
|
|
m.mu.Unlock()
|
|||
|
|
if prev != admin {
|
|||
|
|
log.Printf("член %s: admin %s -> %s", m.Name, prev, admin)
|
|||
|
|
}
|
|||
|
|
if err := a.apply(false); err != nil {
|
|||
|
|
log.Printf("ошибка применения: %v", err)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "член %s: admin=%s\n", m.Name, admin)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (a *Agent) status() []memberView {
|
|||
|
|
out := make([]memberView, 0, len(a.cfg.Members))
|
|||
|
|
for _, m := range a.cfg.Members {
|
|||
|
|
m.mu.Lock()
|
|||
|
|
v := memberView{
|
|||
|
|
Name: m.Name, Address: m.Address, Port: m.Port,
|
|||
|
|
State: m.state, Admin: m.admin,
|
|||
|
|
Active: m.state == "up" && m.admin == "enabled",
|
|||
|
|
LastError: m.lastErr, LatencyMS: m.lastLatency.Milliseconds(),
|
|||
|
|
Probes: m.probes, Failures: m.failures, Transitions: m.transitions,
|
|||
|
|
Source: m.Source,
|
|||
|
|
}
|
|||
|
|
if !m.lastChange.IsZero() {
|
|||
|
|
v.Since = m.lastChange.Format(time.RFC3339)
|
|||
|
|
}
|
|||
|
|
m.mu.Unlock()
|
|||
|
|
v.Slots = a.slotsOf(m.ID)
|
|||
|
|
out = append(out, v)
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (a *Agent) serve() {
|
|||
|
|
mux := http.NewServeMux()
|
|||
|
|
|
|||
|
|
mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
a.mu.Lock()
|
|||
|
|
digest := a.digest
|
|||
|
|
a.mu.Unlock()
|
|||
|
|
w.Header().Set("Content-Type", "application/json")
|
|||
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|||
|
|
"pool_id": a.cfg.PoolID,
|
|||
|
|
"slots": a.cfg.Slots,
|
|||
|
|
"slot_table_digest": digest,
|
|||
|
|
"probe": a.cfg.Probe,
|
|||
|
|
"interval": a.cfg.Interval,
|
|||
|
|
"rise": a.cfg.Rise,
|
|||
|
|
"fall": a.cfg.Fall,
|
|||
|
|
"members": a.status(),
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Человекочитаемый вид для lbctl health: форматирование здесь избавляет
|
|||
|
|
// образ балансировщика от зависимости на jq или python.
|
|||
|
|
mux.HandleFunc("/status.txt", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
a.mu.Lock()
|
|||
|
|
digest := a.digest
|
|||
|
|
a.mu.Unlock()
|
|||
|
|
if digest == "" {
|
|||
|
|
digest = "-"
|
|||
|
|
}
|
|||
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|||
|
|
fmt.Fprintf(w, "пул %d: слотов %d, проба %s каждые %s (rise=%d fall=%d)\n",
|
|||
|
|
a.cfg.PoolID, a.cfg.Slots, a.cfg.Probe, a.cfg.Interval, a.cfg.Rise, a.cfg.Fall)
|
|||
|
|
fmt.Fprintf(w, "дайджест раскладки: %s\n\n", digest)
|
|||
|
|
fmt.Fprintf(w, "%-6s %-17s %-10s %-9s %-7s %7s %7s %7s %5s %s\n",
|
|||
|
|
"ЧЛЕН", "АДРЕС", "СОСТОЯНИЕ", "ADMIN", "В ПУЛЕ", "СЛОТОВ", "ПРОБ", "НЕУДАЧ", "МС", "ИСТОЧНИК ПРОБ")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
inPool := "нет"
|
|||
|
|
if v.Active {
|
|||
|
|
inPool = "да"
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "%-6s %-17s %-10s %-9s %-7s %7d %7d %7d %5d %s\n",
|
|||
|
|
v.Name, fmt.Sprintf("%s:%d", v.Address, v.Port), v.State, v.Admin, inPool,
|
|||
|
|
v.Slots, v.Probes, v.Failures, v.LatencyMS, v.Source)
|
|||
|
|
if v.LastError != "" {
|
|||
|
|
fmt.Fprintf(w, " последняя ошибка: %s\n", v.LastError)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
a.mu.Lock()
|
|||
|
|
applies, errs := a.applies, a.errors
|
|||
|
|
a.mu.Unlock()
|
|||
|
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_member_up Член пула жив по данным проб\n# TYPE hpnn_member_up gauge\n")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
up := 0
|
|||
|
|
if v.State == "up" {
|
|||
|
|
up = 1
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "hpnn_member_up{member=%q,address=%q} %d\n", v.Name, v.Address, up)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_member_active Член участвует в раскладке слотов\n# TYPE hpnn_member_active gauge\n")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
act := 0
|
|||
|
|
if v.Active {
|
|||
|
|
act = 1
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "hpnn_member_active{member=%q} %d\n", v.Name, act)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_member_slots Слотов у члена пула\n# TYPE hpnn_member_slots gauge\n")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
fmt.Fprintf(w, "hpnn_member_slots{member=%q} %d\n", v.Name, v.Slots)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_probes_total Всего проб\n# TYPE hpnn_probes_total counter\n")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
fmt.Fprintf(w, "hpnn_probes_total{member=%q} %d\n", v.Name, v.Probes)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_probe_failures_total Неудачных проб\n# TYPE hpnn_probe_failures_total counter\n")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
fmt.Fprintf(w, "hpnn_probe_failures_total{member=%q} %d\n", v.Name, v.Failures)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_probe_latency_ms Задержка последней пробы\n# TYPE hpnn_probe_latency_ms gauge\n")
|
|||
|
|
for _, v := range a.status() {
|
|||
|
|
fmt.Fprintf(w, "hpnn_probe_latency_ms{member=%q} %d\n", v.Name, v.LatencyMS)
|
|||
|
|
}
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_slot_table_applies_total Заливок таблицы слотов\n# TYPE hpnn_slot_table_applies_total counter\nhpnn_slot_table_applies_total %d\n", applies)
|
|||
|
|
fmt.Fprintf(w, "# HELP hpnn_slot_table_errors_total Ошибок заливки\n# TYPE hpnn_slot_table_errors_total counter\nhpnn_slot_table_errors_total %d\n", errs)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
mux.HandleFunc("/drain", func(w http.ResponseWriter, r *http.Request) { a.setAdmin(w, r, "drain") })
|
|||
|
|
mux.HandleFunc("/enable", func(w http.ResponseWriter, r *http.Request) { a.setAdmin(w, r, "enabled") })
|
|||
|
|
|
|||
|
|
// Вызывается из apply.sh: перезаливка всего пайплайна стирает таблицу
|
|||
|
|
// слотов, поэтому её нужно восстановить в актуальном составе.
|
|||
|
|
mux.HandleFunc("/reapply", func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
if err := a.apply(true); err != nil {
|
|||
|
|
http.Error(w, err.Error()+"\n", http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
a.mu.Lock()
|
|||
|
|
digest := a.digest
|
|||
|
|
a.mu.Unlock()
|
|||
|
|
fmt.Fprintf(w, "таблица слотов перезалита, дайджест %s\n", digest)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
log.Printf("API на http://%s (/status, /metrics, /drain, /enable, /reapply)", a.cfg.API)
|
|||
|
|
if err := http.ListenAndServe(a.cfg.API, mux); err != nil {
|
|||
|
|
log.Fatalf("API: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- запуск ------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
func loadConfig(path string) (*Config, error) {
|
|||
|
|
raw, err := os.ReadFile(path)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
cfg := &Config{}
|
|||
|
|
if err := json.Unmarshal(raw, cfg); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if cfg.interval, err = time.ParseDuration(cfg.Interval); err != nil {
|
|||
|
|
return nil, fmt.Errorf("interval: %w", err)
|
|||
|
|
}
|
|||
|
|
if cfg.timeout, err = time.ParseDuration(cfg.Timeout); err != nil {
|
|||
|
|
return nil, fmt.Errorf("timeout: %w", err)
|
|||
|
|
}
|
|||
|
|
if len(cfg.Members) == 0 {
|
|||
|
|
return nil, fmt.Errorf("пустой список членов пула")
|
|||
|
|
}
|
|||
|
|
for _, m := range cfg.Members {
|
|||
|
|
// Стартуем с down: член войдёт в раскладку только после Rise
|
|||
|
|
// успешных проб. Балансировать на непроверенный бэкенд нельзя.
|
|||
|
|
m.state = "down"
|
|||
|
|
m.admin = "enabled"
|
|||
|
|
}
|
|||
|
|
return cfg, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
path := flag.String("config", "/var/run/openvswitch/hc.json", "путь к конфигурации")
|
|||
|
|
flag.Parse()
|
|||
|
|
|
|||
|
|
log.SetFlags(log.LstdFlags)
|
|||
|
|
log.SetPrefix("[hcd] ")
|
|||
|
|
|
|||
|
|
cfg, err := loadConfig(*path)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Fatalf("конфигурация: %v", err)
|
|||
|
|
}
|
|||
|
|
log.Printf("пул %d: %d членов, %d слотов, проба %s каждые %s (rise=%d fall=%d)",
|
|||
|
|
cfg.PoolID, len(cfg.Members), cfg.Slots, cfg.Probe, cfg.Interval, cfg.Rise, cfg.Fall)
|
|||
|
|
|
|||
|
|
agent := &Agent{cfg: cfg}
|
|||
|
|
// Стартовое состояние — пустой пул: до первых успешных проб датапас
|
|||
|
|
// работает в fail-close.
|
|||
|
|
if err := agent.apply(true); err != nil {
|
|||
|
|
log.Printf("ошибка стартового применения: %v", err)
|
|||
|
|
}
|
|||
|
|
go agent.serve()
|
|||
|
|
|
|||
|
|
stop := make(chan os.Signal, 1)
|
|||
|
|
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(cfg.interval)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-stop:
|
|||
|
|
log.Printf("остановка")
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
var wg sync.WaitGroup
|
|||
|
|
changed := make([]bool, len(cfg.Members))
|
|||
|
|
for i, m := range cfg.Members {
|
|||
|
|
wg.Add(1)
|
|||
|
|
go func(i int, m *Member) {
|
|||
|
|
defer wg.Done()
|
|||
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout)
|
|||
|
|
defer cancel()
|
|||
|
|
start := time.Now()
|
|||
|
|
err := probe(ctx, cfg, m)
|
|||
|
|
changed[i] = observe(m, cfg, err, time.Since(start))
|
|||
|
|
}(i, m)
|
|||
|
|
}
|
|||
|
|
wg.Wait()
|
|||
|
|
|
|||
|
|
for _, c := range changed {
|
|||
|
|
if c {
|
|||
|
|
if err := agent.apply(false); err != nil {
|
|||
|
|
log.Printf("ошибка применения: %v", err)
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|