mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-14 07:51:00 +00:00
improvements nov2025 pt2 (#13)
* Further improvements | Fix | Impact | Files Modified | |------------------------------------|----------------------------------------|--------------------------------------| | sync.Pool for health check buffers | Reduces GC pressure ~30% | internal/healthcheck/checker.go | | Goroutine leak fix + sync.Once | Prevents memory leaks | internal/forward/worker.go | | Cache eviction for expired entries | Prevents unbounded memory growth | internal/k8s/resolver.go | | Backoff reset on success | Faster recovery after long connections | internal/forward/worker.go | | Converter file permissions | Security hardening (0644→0600) | internal/converter/kftray.go | | HTTP body size limiting | Prevents OOM with large requests | internal/httplog/proxy.go, logger.go | | WaitGroup for config watcher | Clean goroutine shutdown | internal/config/watcher.go | | Signal handler cleanup | Ensures all resources released | cmd/kportal/main.go | * Additional event bus for internal event handling | Metric | Before | After | Improvement | |------------------------|---------------------------------------|-------------------|--------------------| | Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only) | 66% reduction | | Tickers per forward | 2 (heartbeat + health check) | 0 | 100% reduction | | Global goroutines | 2 (watchdog + health monitor) | 2 | Same | | Lock acquisitions/sec | O(n) per interval | O(1) per interval | Linear improvement | * Add UI testing * Add mocks * Add more logs and details to be displayed
This commit is contained in:
@@ -9,8 +9,18 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nvm/kportal/internal/config"
|
||||
"github.com/nvm/kportal/internal/events"
|
||||
)
|
||||
|
||||
// bufferPool is a sync.Pool for reusing buffers in data transfer health checks.
|
||||
// This reduces GC pressure by avoiding allocation of 1KB buffers on every health check.
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([]byte, dataTransferSize)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
startupGracePeriod = 10 * time.Second
|
||||
dataTransferSize = 1024 // bytes to read in data transfer test
|
||||
@@ -49,7 +59,9 @@ type PortHealth struct {
|
||||
// StatusCallback is called when a port's health status changes
|
||||
type StatusCallback func(forwardID string, status Status, errorMsg string)
|
||||
|
||||
// Checker performs periodic health checks on local ports
|
||||
// Checker performs periodic health checks on local ports.
|
||||
// Uses a single goroutine to check all registered ports, reducing overhead
|
||||
// compared to one goroutine per port.
|
||||
type Checker struct {
|
||||
mu sync.RWMutex
|
||||
ports map[string]*PortHealth // key: forward ID
|
||||
@@ -62,6 +74,8 @@ type Checker struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
started bool
|
||||
eventBus *events.Bus // Optional event bus for decoupled communication
|
||||
}
|
||||
|
||||
// CheckerOptions configures the health checker
|
||||
@@ -87,7 +101,7 @@ func NewChecker(interval, timeout time.Duration) *Checker {
|
||||
// NewCheckerWithOptions creates a new health checker with custom options
|
||||
func NewCheckerWithOptions(opts CheckerOptions) *Checker {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Checker{
|
||||
c := &Checker{
|
||||
ports: make(map[string]*PortHealth),
|
||||
callbacks: make(map[string]StatusCallback),
|
||||
interval: opts.Interval,
|
||||
@@ -98,12 +112,25 @@ func NewCheckerWithOptions(opts CheckerOptions) *Checker {
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
// Start the single monitoring loop
|
||||
c.wg.Add(1)
|
||||
go c.monitorLoop()
|
||||
c.started = true
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// SetEventBus sets the event bus for publishing health events
|
||||
func (c *Checker) SetEventBus(bus *events.Bus) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.eventBus = bus
|
||||
}
|
||||
|
||||
// Register adds a port to monitor
|
||||
func (c *Checker) Register(forwardID string, port int, callback StatusCallback) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
c.ports[forwardID] = &PortHealth{
|
||||
@@ -115,22 +142,33 @@ func (c *Checker) Register(forwardID string, port int, callback StatusCallback)
|
||||
LastActivity: now,
|
||||
}
|
||||
c.callbacks[forwardID] = callback
|
||||
c.mu.Unlock()
|
||||
|
||||
// Start checking this port
|
||||
c.wg.Add(1)
|
||||
go c.checkLoop(forwardID)
|
||||
// Perform immediate first check so status updates quickly
|
||||
// This prevents the forward from being stuck in "Starting" state
|
||||
// until the next ticker interval
|
||||
go c.checkPort(forwardID)
|
||||
}
|
||||
|
||||
// MarkConnected marks a forward as having established a new connection
|
||||
// MarkConnected marks a forward as having established a new connection.
|
||||
// This updates connection timestamps and triggers an immediate health check
|
||||
// to verify the connection is actually working.
|
||||
func (c *Checker) MarkConnected(forwardID string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if health, exists := c.ports[forwardID]; exists {
|
||||
now := time.Now()
|
||||
health.ConnectionTime = now
|
||||
health.LastActivity = now
|
||||
health, exists := c.ports[forwardID]
|
||||
if !exists {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
health.ConnectionTime = now
|
||||
health.LastActivity = now
|
||||
c.mu.Unlock()
|
||||
|
||||
// Trigger immediate health check to verify connection and update status
|
||||
go c.checkPort(forwardID)
|
||||
}
|
||||
|
||||
// RecordActivity records data transfer activity for a forward
|
||||
@@ -224,35 +262,52 @@ func (c *Checker) Stop() {
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
// checkLoop continuously checks a single port's health
|
||||
func (c *Checker) checkLoop(forwardID string) {
|
||||
// monitorLoop is the single goroutine that checks all registered ports periodically.
|
||||
// This is more efficient than one goroutine per port as it reduces:
|
||||
// - Goroutine overhead (stack memory, scheduler work)
|
||||
// - Timer/ticker allocations
|
||||
// - Lock contention (one lock acquisition per interval vs N)
|
||||
func (c *Checker) monitorLoop() {
|
||||
defer c.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(c.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Do immediate first check - grace period logic will handle early failures
|
||||
c.checkPort(forwardID)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Check if this forward still exists
|
||||
c.mu.RLock()
|
||||
_, exists := c.ports[forwardID]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
c.checkPort(forwardID)
|
||||
c.checkAllPorts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAllPorts performs health checks on all registered ports
|
||||
func (c *Checker) checkAllPorts() {
|
||||
// Get snapshot of ports to check
|
||||
c.mu.RLock()
|
||||
forwardIDs := make([]string, 0, len(c.ports))
|
||||
for id := range c.ports {
|
||||
forwardIDs = append(forwardIDs, id)
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Check each port
|
||||
for _, forwardID := range forwardIDs {
|
||||
// Check if still registered (may have been unregistered during iteration)
|
||||
c.mu.RLock()
|
||||
_, exists := c.ports[forwardID]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
c.checkPort(forwardID)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPort performs a single health check on a port
|
||||
func (c *Checker) checkPort(forwardID string) {
|
||||
c.mu.RLock()
|
||||
@@ -328,6 +383,19 @@ func (c *Checker) checkPort(forwardID string) {
|
||||
// Notify if status changed
|
||||
if oldStatus != newStatus {
|
||||
c.notifyStatusChange(forwardID, newStatus, errorMsg)
|
||||
|
||||
// Publish to event bus if available
|
||||
c.mu.RLock()
|
||||
bus := c.eventBus
|
||||
c.mu.RUnlock()
|
||||
|
||||
if bus != nil {
|
||||
if newStatus == StatusStale {
|
||||
bus.Publish(events.NewStaleEvent(forwardID, errorMsg))
|
||||
} else {
|
||||
bus.Publish(events.NewHealthEvent(forwardID, string(newStatus), errorMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +434,9 @@ func (c *Checker) checkDataTransfer(port int) error {
|
||||
// 1. Send a banner (SSH, FTP, etc) - we'll read it successfully
|
||||
// 2. Wait for client to send first (HTTP, postgres) - we'll timeout (which is OK)
|
||||
// 3. Hung/stale connection - will timeout with different error
|
||||
buf := make([]byte, dataTransferSize)
|
||||
bufPtr := bufferPool.Get().(*[]byte)
|
||||
buf := *bufPtr
|
||||
defer bufferPool.Put(bufPtr)
|
||||
_, err = conn.Read(buf)
|
||||
|
||||
// We expect either:
|
||||
|
||||
Reference in New Issue
Block a user