Skip to content

Commit bd02e1e

Browse files
committed
feat: add proxy libary
1 parent 3451b68 commit bd02e1e

File tree

13 files changed

+2235
-0
lines changed

13 files changed

+2235
-0
lines changed

pkg/proxykit/README.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
## Proxy Kit
2+
3+
A production-grade reverse proxy library implemented in Go. It not only provides high performance and multiple load balancing strategies, but also supports **dynamic route management**, allowing you to add or remove backend servers at runtime via API — without restarting the service.
4+
5+
<br>
6+
7+
### Core Features
8+
9+
* **Dynamic Service Discovery**: Add or remove backend nodes in real-time through HTTP APIs.
10+
* **High Performance Core**: Built on `net/http/httputil` with deeply optimized connection pooling for effortless high-concurrency handling.
11+
* **Rich Load Balancing Strategies**: Includes Round Robin, The Least Connections, and IP Hash.
12+
* **Active Health Checks**: Automatically detects and isolates unhealthy nodes, and brings them back online once they recover.
13+
* **Multi-route Support**: Distribute traffic to different backend groups based on path prefixes.
14+
15+
<br>
16+
17+
### Example of Usage
18+
19+
```go
20+
package main
21+
22+
import (
23+
"log"
24+
"net/http"
25+
"time"
26+
"github.com/go-dev-frame/sponge/pkg/proxykit"
27+
)
28+
29+
func main() {
30+
prefixPath := "/proxy/"
31+
32+
// 1. Create the route manager
33+
manager := proxykit.NewRouteManager()
34+
35+
// 2. Initialize backend targets
36+
initialTargets := []string{"http://localhost:8081", "http://localhost:8082"}
37+
backends, _ := proxykit.ParseBackends(prefixPath, initialTargets)
38+
proxykit.StartHealthChecks(backends, proxykit.HealthCheckConfig{Interval: 5 * time.Second})
39+
balancer := proxykit.NewRoundRobin(backends)
40+
41+
// Register route
42+
apiRoute, err := manager.AddRoute(prefixPath, balancer)
43+
if err != nil {
44+
log.Fatalf("Could not add initial route: %v", err)
45+
}
46+
47+
// 3. Build standard library mux
48+
mux := http.NewServeMux()
49+
50+
// 4. Register proxy handler (same as gin.Any)
51+
mux.Handle(prefixPath, apiRoute.Proxy)
52+
53+
// 5. Management API (corresponds to /endpoints/...)
54+
mux.HandleFunc("/endpoints/add", manager.HandleAddBackends)
55+
mux.HandleFunc("/endpoints/remove", manager.HandleRemoveBackends)
56+
mux.HandleFunc("/endpoints/list", manager.HandleListBackends)
57+
mux.HandleFunc("/endpoints", manager.HandleGetBackend)
58+
59+
// 6. Other normal routes
60+
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
61+
w.Header().Set("Content-Type", "application/json")
62+
w.Write([]byte(`{"message": "pong"}`))
63+
})
64+
65+
// 7. Start HTTP server
66+
log.Println("HTTP server with dynamic proxy started on http://localhost:8080")
67+
log.Printf("Proxying requests from %s*\n", prefixPath)
68+
log.Println("Management API available at /endpoints/*")
69+
70+
err = http.ListenAndServe(":8080", mux)
71+
if err != nil {
72+
log.Fatal("ListenAndServe:", err)
73+
}
74+
}
75+
```
76+
77+
<br>
78+
79+
### Management API Guide
80+
81+
After the proxy is started, you can manage backend services dynamically via the following APIs.
82+
83+
#### 1. List all backends
84+
85+
Retrieve all backend nodes and their health status for the given route.
86+
87+
* **GET** `/endpoints/list?prefixPath=/api/`
88+
89+
```json
90+
{
91+
"prefixPath": "/api/",
92+
"targets": [
93+
{"target": "http://localhost:8081", "healthy": true}
94+
]
95+
}
96+
```
97+
98+
#### 2. Add backend nodes
99+
100+
Dynamically scale out. New nodes will automatically enter the health check loop and start receiving traffic.
101+
102+
* **POST** `/endpoints/add`
103+
* **Body**:
104+
105+
```json
106+
{
107+
"prefixPath": "/api/",
108+
"targets": ["http://localhost:8083", "http://localhost:8084"]
109+
}
110+
```
111+
112+
#### 3. Remove backend nodes
113+
114+
Dynamically scale in. Health checks for removed nodes will stop automatically.
115+
116+
* **POST** `/endpoints/remove`
117+
* **Body**:
118+
119+
```json
120+
{
121+
"prefixPath": "/api/",
122+
"targets": ["http://localhost:8081"]
123+
}
124+
```
125+
126+
#### 4. Inspect a single backend node
127+
128+
* **GET** `/endpoints?prefixPath=/api/&target=http://localhost:8082`
129+
130+
```json
131+
{
132+
"target": "http://localhost:8082",
133+
"healthy": true
134+
}
135+
```

pkg/proxykit/backend.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package proxykit
2+
3+
import (
4+
"net/http"
5+
"net/http/httputil"
6+
"net/url"
7+
"strings"
8+
"sync"
9+
"sync/atomic"
10+
"time"
11+
)
12+
13+
// DefaultTransport returns a http.Transport optimized for reverse proxy usage.
14+
func DefaultTransport() *http.Transport {
15+
return &http.Transport{
16+
Proxy: http.ProxyFromEnvironment,
17+
// MaxIdleConnsPerHost controls the maximum number of idle (keep-alive)
18+
// connections to keep per-host. Increasing this value can reduce the cost
19+
// of creating new TCP connections under high concurrency.
20+
MaxIdleConnsPerHost: 100,
21+
// MaxIdleConns is the total maximum number of idle connections maintained by the client.
22+
MaxIdleConns: 200,
23+
// IdleConnTimeout is the maximum amount of time an idle connection remains open before closing.
24+
IdleConnTimeout: 90 * time.Second,
25+
// TLSHandshakeTimeout is the maximum time allowed for the TLS handshake.
26+
TLSHandshakeTimeout: 10 * time.Second,
27+
// ExpectContinueTimeout is the maximum time to wait for the server's first response header.
28+
ExpectContinueTimeout: 1 * time.Second,
29+
}
30+
}
31+
32+
// Backend encapsulates the backend server information.
33+
type Backend struct {
34+
URL *url.URL
35+
isHealthy atomic.Bool
36+
activeConns atomic.Int64
37+
proxy *httputil.ReverseProxy
38+
stopHealthCheck chan struct{} // Used to stop the health check goroutine
39+
stopOnce sync.Once // Ensures stop is called only once
40+
}
41+
42+
// ParseBackends helper function: converts a list of URL strings into []*Backend
43+
func ParseBackends(prefixPath string, targets []string) ([]*Backend, error) {
44+
var backends []*Backend
45+
for _, t := range targets {
46+
u, err := url.Parse(t)
47+
if err != nil {
48+
return nil, err
49+
}
50+
backends = append(backends, NewBackend(prefixPath, u))
51+
}
52+
return backends, nil
53+
}
54+
55+
// NewBackend creates a new Backend instance.
56+
func NewBackend(prefixPath string, u *url.URL) *Backend {
57+
proxy := httputil.NewSingleHostReverseProxy(u)
58+
59+
// Use the optimized Transport
60+
proxy.Transport = DefaultTransport()
61+
62+
originalDirector := proxy.Director
63+
proxy.Director = func(req *http.Request) {
64+
originalDirector(req)
65+
req.URL.Path = strings.TrimPrefix(req.URL.Path, strings.TrimSuffix(prefixPath, "/"))
66+
req.Header.Set("X-Forwarded-Host", req.Host)
67+
req.Header.Set("X-Origin-Host", u.Host)
68+
}
69+
70+
b := &Backend{
71+
URL: u,
72+
proxy: proxy,
73+
stopHealthCheck: make(chan struct{}),
74+
}
75+
76+
b.isHealthy.Store(true) // initialize as healthy by default
77+
return b
78+
}
79+
80+
// StopHealthCheck stops the health check goroutine associated with this backend.
81+
func (b *Backend) StopHealthCheck() {
82+
b.stopOnce.Do(func() {
83+
close(b.stopHealthCheck)
84+
})
85+
}
86+
87+
func (b *Backend) SetHealthy(healthy bool) {
88+
b.isHealthy.Store(healthy)
89+
}
90+
91+
func (b *Backend) IsHealthy() bool {
92+
return b.isHealthy.Load()
93+
}
94+
95+
func (b *Backend) GetActiveConns() int64 {
96+
return b.activeConns.Load()
97+
}
98+
99+
func (b *Backend) IncrementActiveConns() {
100+
b.activeConns.Add(1)
101+
}
102+
103+
func (b *Backend) DecrementActiveConns() {
104+
b.activeConns.Add(-1)
105+
}

0 commit comments

Comments
 (0)