Skip to content

Commit a6030d2

Browse files
committed
降低状态持久化写盘频率
1 parent 1ce8d9c commit a6030d2

5 files changed

Lines changed: 157 additions & 36 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Docker 镜像中的默认路径为:
9797
| 环境变量 | 默认值 | 说明 |
9898
| --- | --- | --- |
9999
| `CONFIG_PATH` | `/app/config/config.json` | 主配置文件 |
100-
| `STATS_PATH` | `/app/data/stats.json` | 月流量与状态持久化文件 |
100+
| `STATS_PATH` | `/app/data/stats.json` | 月流量与状态持久化文件;每 60 秒写入,关键操作与正常退出时立即写入 |
101101
| `WEB_DIR` | `/app/web` | WebUI 静态文件目录 |
102102
| `HTTP_ADDR` | `:80` | WebUI 与 HTTP API 监听地址 |
103103
| `AGENT_ADDR` | `:35601` | 客户端 TCP 上报监听地址 |

server/app.go

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,22 @@ type App struct {
5555
document ConfigDocument
5656
runtime RuntimeConfig
5757

58-
nodeMu sync.RWMutex
59-
nodes map[string]*NodeState
60-
connectionID atomic.Uint64
61-
generation atomic.Uint64
62-
agentRunning atomic.Bool
63-
reloadWrites atomic.Int32
58+
nodeMu sync.RWMutex
59+
nodes map[string]*NodeState
60+
connectionID atomic.Uint64
61+
generation atomic.Uint64
62+
agentRunning atomic.Bool
63+
reloadPending atomic.Bool
6464

6565
certMu sync.RWMutex
6666
certs map[string]*CertState
6767

68-
statsWake chan struct{}
6968
persistMu sync.Mutex
7069
logger *log.Logger
7170
}
7271

72+
const statsFlushInterval = time.Minute
73+
7374
func NewApp(opts Options) (*App, error) {
7475
doc, runtime, err := readConfig(opts.ConfigPath)
7576
if err != nil {
@@ -83,7 +84,6 @@ func NewApp(opts Options) (*App, error) {
8384
cancel: cancel,
8485
nodes: make(map[string]*NodeState),
8586
certs: make(map[string]*CertState),
86-
statsWake: make(chan struct{}, 1),
8787
logger: log.New(os.Stdout, "serverstatus ", log.LstdFlags|log.Lmicroseconds),
8888
}
8989
app.applyValidatedConfig(doc, runtime, false)
@@ -94,7 +94,6 @@ func NewApp(opts Options) (*App, error) {
9494
func (a *App) StartBackground() {
9595
go a.statsLoop()
9696
go a.sslLoop()
97-
a.wakeStatsWriter()
9897
}
9998

10099
func (a *App) Close() {
@@ -216,8 +215,12 @@ func (a *App) applyValidatedConfig(doc ConfigDocument, runtime RuntimeConfig, di
216215
_ = conn.Close()
217216
}
218217
}
219-
a.reloadWrites.Store(2)
220-
a.wakeStatsWriter()
218+
if disconnect {
219+
a.reloadPending.Store(true)
220+
if err := a.PersistStats(); err != nil {
221+
a.logger.Printf("write stats after config change: %v", err)
222+
}
223+
}
221224
}
222225

223226
func sameServerIdentity(left, right ServerConfig) bool {
@@ -246,33 +249,29 @@ func (a *App) disconnectAll(reason string) {
246249
}
247250

248251
func (a *App) statsLoop() {
249-
ticker := time.NewTicker(time.Second)
252+
ticker := time.NewTicker(statsFlushInterval)
250253
defer ticker.Stop()
251254
for {
252255
select {
253256
case <-a.ctx.Done():
254257
return
255258
case <-ticker.C:
256-
case <-a.statsWake:
257259
}
258260
if err := a.PersistStats(); err != nil {
259261
a.logger.Printf("write stats: %v", err)
260262
}
261263
}
262264
}
263265

264-
func (a *App) wakeStatsWriter() {
265-
select {
266-
case a.statsWake <- struct{}{}:
267-
default:
268-
}
269-
}
270-
271266
func (a *App) SnapshotStats() map[string]any {
272-
return a.snapshotStats(false)
267+
result := a.snapshotStats()
268+
if a.reloadPending.Swap(false) {
269+
result["reload"] = true
270+
}
271+
return result
273272
}
274273

275-
func (a *App) snapshotStats(consumeReload bool) map[string]any {
274+
func (a *App) snapshotStats() map[string]any {
276275
runtime := a.RuntimeSnapshot()
277276
now := time.Now()
278277
servers := make([]any, 0, len(runtime.Servers))
@@ -325,19 +324,13 @@ func (a *App) snapshotStats(consumeReload bool) map[string]any {
325324
"sslcerts": a.sslSnapshot(runtime.SSLCerts, now),
326325
"updated": strconv.FormatInt(now.Unix(), 10),
327326
}
328-
if a.reloadWrites.Load() > 0 {
329-
result["reload"] = true
330-
if consumeReload {
331-
a.reloadWrites.Add(-1)
332-
}
333-
}
334327
return result
335328
}
336329

337330
func (a *App) PersistStats() error {
338331
a.persistMu.Lock()
339332
defer a.persistMu.Unlock()
340-
return writeStatsFile(a.opts.StatsPath, a.snapshotStats(true))
333+
return writeStatsFile(a.opts.StatsPath, a.snapshotStats())
341334
}
342335

343336
func monthResetWindow(now time.Time, monthStart int) bool {
@@ -452,7 +445,9 @@ func (a *App) ResetTraffic(username string) (map[string]any, *APIError) {
452445
node.LastNetworkIn, node.LastNetworkOut = networkIn, networkOut
453446
server := node.Config
454447
a.nodeMu.Unlock()
455-
a.wakeStatsWriter()
448+
if err := a.PersistStats(); err != nil {
449+
a.logger.Printf("write stats after traffic reset for %q: %v", username, err)
450+
}
456451
return map[string]any{
457452
"server": server,
458453
"stats": map[string]any{

server/app_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"net"
6+
"os"
57
"testing"
8+
"time"
69
)
710

11+
func readPersistedServer(t *testing.T, app *App) map[string]any {
12+
t.Helper()
13+
data, err := os.ReadFile(app.opts.StatsPath)
14+
if err != nil {
15+
t.Fatal(err)
16+
}
17+
var stats map[string]any
18+
if err := json.Unmarshal(data, &stats); err != nil {
19+
t.Fatal(err)
20+
}
21+
servers, ok := stats["servers"].([]any)
22+
if !ok || len(servers) != 1 {
23+
t.Fatalf("unexpected persisted servers: %#v", stats["servers"])
24+
}
25+
server, ok := servers[0].(map[string]any)
26+
if !ok {
27+
t.Fatalf("unexpected persisted server: %#v", servers[0])
28+
}
29+
return server
30+
}
31+
832
func TestTrafficBaselinesResetIndependently(t *testing.T) {
933
node := &NodeState{LastNetworkIn: 100, LastNetworkOut: 0}
1034
updateTrafficBaselines(node, 150, 500, false)
@@ -48,3 +72,110 @@ func TestDisconnectPreservesOfflineDisplayMetadata(t *testing.T) {
4872
t.Fatalf("offline display metadata was discarded: %#v", serverStats)
4973
}
5074
}
75+
76+
func TestStatsPersistenceUsesLiveMemoryBetweenFlushes(t *testing.T) {
77+
if statsFlushInterval != time.Minute {
78+
t.Fatalf("unexpected stats flush interval: %s", statsFlushInterval)
79+
}
80+
81+
app := newTestApp(t, minimalTestConfig())
82+
if err := app.PersistStats(); err != nil {
83+
t.Fatal(err)
84+
}
85+
initialInfo, err := os.Stat(app.opts.StatsPath)
86+
if err != nil {
87+
t.Fatal(err)
88+
}
89+
90+
app.nodeMu.Lock()
91+
node := app.nodes["s01"]
92+
node.Connected = true
93+
node.ConnectionID = 7
94+
app.nodeMu.Unlock()
95+
if !app.updateAgent("s01", 7, AgentStats{CPU: 42, NetworkIn: 1000, NetworkOut: 2000}) {
96+
t.Fatal("agent update was rejected")
97+
}
98+
99+
live := app.SnapshotStats()["servers"].([]any)[0].(map[string]any)
100+
if live["cpu"] != 42 {
101+
t.Fatalf("live snapshot was not updated: %#v", live)
102+
}
103+
beforeFlushInfo, err := os.Stat(app.opts.StatsPath)
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
if !os.SameFile(initialInfo, beforeFlushInfo) {
108+
t.Fatal("reading the live snapshot unexpectedly rewrote stats.json")
109+
}
110+
if _, exists := readPersistedServer(t, app)["cpu"]; exists {
111+
t.Fatal("agent update reached disk before the next persistence run")
112+
}
113+
114+
if err := app.PersistStats(); err != nil {
115+
t.Fatal(err)
116+
}
117+
afterFlushInfo, err := os.Stat(app.opts.StatsPath)
118+
if err != nil {
119+
t.Fatal(err)
120+
}
121+
if os.SameFile(beforeFlushInfo, afterFlushInfo) {
122+
t.Fatal("updated stats were not persisted")
123+
}
124+
if cpu := readPersistedServer(t, app)["cpu"]; cpu != float64(42) {
125+
t.Fatalf("unexpected persisted CPU value: %#v", cpu)
126+
}
127+
128+
if err := app.PersistStats(); err != nil {
129+
t.Fatal(err)
130+
}
131+
cleanInfo, err := os.Stat(app.opts.StatsPath)
132+
if err != nil {
133+
t.Fatal(err)
134+
}
135+
if os.SameFile(afterFlushInfo, cleanInfo) {
136+
t.Fatal("fixed-interval persistence unexpectedly skipped a write")
137+
}
138+
}
139+
140+
func TestReloadNoticeIsIndependentFromPersistence(t *testing.T) {
141+
app := newTestApp(t, minimalTestConfig())
142+
app.reloadPending.Store(true)
143+
if err := app.PersistStats(); err != nil {
144+
t.Fatal(err)
145+
}
146+
data, err := os.ReadFile(app.opts.StatsPath)
147+
if err != nil {
148+
t.Fatal(err)
149+
}
150+
var persisted map[string]any
151+
if err := json.Unmarshal(data, &persisted); err != nil {
152+
t.Fatal(err)
153+
}
154+
if _, exists := persisted["reload"]; exists {
155+
t.Fatal("reload notice must not be persisted to stats.json")
156+
}
157+
if reloaded, _ := app.SnapshotStats()["reload"].(bool); !reloaded {
158+
t.Fatal("pending reload notice was not returned by the live endpoint")
159+
}
160+
if _, exists := app.SnapshotStats()["reload"]; exists {
161+
t.Fatal("reload notice was returned more than once")
162+
}
163+
}
164+
165+
func TestResetTrafficPersistsImmediately(t *testing.T) {
166+
app := newTestApp(t, minimalTestConfig())
167+
app.nodeMu.Lock()
168+
node := app.nodes["s01"]
169+
node.Connected = true
170+
node.HasUpdate = true
171+
node.Stats = AgentStats{NetworkIn: 1234, NetworkOut: 5678}
172+
app.nodeMu.Unlock()
173+
174+
if _, apiErr := app.ResetTraffic("s01"); apiErr != nil {
175+
t.Fatal(apiErr)
176+
}
177+
persisted := readPersistedServer(t, app)
178+
if persisted["last_network_in"] != float64(1234) || persisted["last_network_out"] != float64(5678) {
179+
t.Fatalf("traffic reset was not persisted immediately: %#v", persisted)
180+
}
181+
}

server/ssl.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ func (a *App) executeSSLCheck(key string, config SSLCertConfig) {
9898
if err != nil {
9999
state.LastError = err.Error()
100100
a.certMu.Unlock()
101-
a.wakeStatsWriter()
102101
return
103102
}
104103
state.ExpireTS = expireTS
@@ -126,7 +125,6 @@ func (a *App) executeSSLCheck(key string, config SSLCertConfig) {
126125
}
127126
}
128127
a.certMu.Unlock()
129-
a.wakeStatsWriter()
130128

131129
for _, alert := range alerts {
132130
if err := a.sendCallback(config.Callback, alert.message, "ServerStatusSSL"); err != nil {

server/tcp_server.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ func (a *App) connectAgent(username, password string, conn net.Conn, family int)
176176
node.Pong = false
177177
node.Online4 = family == 4
178178
node.Online6 = family == 6
179-
a.wakeStatsWriter()
180179
return id, append([]MonitorConfig(nil), a.runtime.Monitors...), nil
181180
}
182181

@@ -194,7 +193,6 @@ func (a *App) disconnectAgent(username string, conn net.Conn, connectionID uint6
194193
node.HasUpdate = false
195194
node.Pong = false
196195
a.nodeMu.Unlock()
197-
a.wakeStatsWriter()
198196
time.AfterFunc(25*time.Second, func() {
199197
if a.ctx.Err() != nil {
200198
return
@@ -226,7 +224,6 @@ func (a *App) updateAgent(username string, connectionID uint64, update AgentStat
226224
node.HasUpdate = true
227225
node.LastUpdate = time.Now()
228226
a.nodeMu.Unlock()
229-
a.wakeStatsWriter()
230227
a.evaluateWatchdogs(username, false)
231228
return true
232229
}

0 commit comments

Comments
 (0)