修复二开 agent 流量增量丢失: 基线改上报确认后推进并落盘持久化
- xray 新增快照/确认接口 GetTrafficSnapshot/CommitTrafficSnapshot/ RestoreTrafficBaseline, 与面板 GetTraffic 的 StatsLastValues 完全隔离 - agent 改走快照: 仅主控 HTTP 200 后才推进基线, 失败轮增量由累计 计数做差在下轮自动补齐; 已确认基线原子落盘, agent 重启恢复续差 - 核心重启计数归零的从零规则保留; Onlines 仍每轮新鲜采集直报 - 新增 11 个测试覆盖断网恢复/连续失败补齐/重启恢复/计数归零/ 面板语义隔离, xray 与 agent 两包全绿
This commit is contained in:
+143
-22
@@ -43,6 +43,12 @@ type Config struct {
|
||||
// config. Needed when agent and master share a host (e.g. testing),
|
||||
// otherwise both xrays fight over the default 62789. 0 = use master's.
|
||||
ApiPort int `json:"api_port"`
|
||||
// BaselinePath is where the last master-acknowledged traffic counters
|
||||
// are persisted, so an agent restart keeps diffing against them
|
||||
// instead of swallowing unreported traffic into a fresh baseline.
|
||||
// Empty: main derives it next to the agent config file; the agent
|
||||
// falls back to the bin folder.
|
||||
BaselinePath string `json:"baseline_path,omitempty"`
|
||||
}
|
||||
|
||||
// Agent represents the node agent
|
||||
@@ -54,6 +60,7 @@ type Agent struct {
|
||||
xrayProc *xray.Process
|
||||
xrayAPI xray.XrayAPI
|
||||
apiPort int
|
||||
baselineLoaded bool
|
||||
}
|
||||
|
||||
// New creates a new agent instance
|
||||
@@ -331,36 +338,73 @@ type userEntry struct {
|
||||
Down int64 `json:"down"`
|
||||
}
|
||||
|
||||
// collectAndReportTraffic queries the local xray stats API for per-poll
|
||||
// deltas and posts them to the master. The XrayAPI instance is kept across
|
||||
// polls so delta baselines survive; xray restarts are handled (counters
|
||||
// resetting to zero are treated as fresh).
|
||||
func (a *Agent) collectAndReportTraffic() error {
|
||||
if a.xrayProc == nil || !a.xrayProc.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
if a.apiPort <= 0 {
|
||||
return nil
|
||||
}
|
||||
if err := a.xrayAPI.Init(a.apiPort); err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.xrayAPI.Close()
|
||||
// statsClient is the slice of the xray stats API the traffic reporter
|
||||
// uses. The snapshot/commit pair is the loss-proofing contract: snapshots
|
||||
// never advance the baseline, commits do.
|
||||
type statsClient interface {
|
||||
GetTrafficSnapshot() (*xray.TrafficSnapshot, error)
|
||||
CommitTrafficSnapshot(*xray.TrafficSnapshot)
|
||||
RestoreTrafficBaseline(map[string]int64)
|
||||
GetOnlineUsers() ([]xray.OnlineUser, error)
|
||||
}
|
||||
|
||||
traffics, clientTraffics, err := a.xrayAPI.GetTraffic()
|
||||
// trafficBaselineFile is the on-disk form of the last committed baseline.
|
||||
type trafficBaselineFile struct {
|
||||
Version int `json:"version"`
|
||||
Values map[string]int64 `json:"values"`
|
||||
}
|
||||
|
||||
// baselinePath resolves where the acknowledged traffic baseline lives.
|
||||
func (a *Agent) baselinePath() string {
|
||||
if a.config.BaselinePath != "" {
|
||||
return a.config.BaselinePath
|
||||
}
|
||||
return filepath.Join(config.GetBinFolderPath(), "agent-traffic-baseline.json")
|
||||
}
|
||||
|
||||
// loadTrafficBaseline reads a persisted baseline. A missing file is a
|
||||
// normal first run; a corrupt one is reported so the caller can log it —
|
||||
// both yield no values and the reporter falls back to a baseline-only
|
||||
// first snapshot rather than crashing the agent.
|
||||
func loadTrafficBaseline(path string) (map[string]int64, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var f trafficBaselineFile
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return nil, fmt.Errorf("parse traffic baseline: %w", err)
|
||||
}
|
||||
return f.Values, nil
|
||||
}
|
||||
|
||||
// saveTrafficBaseline persists the baseline atomically (write-then-rename)
|
||||
// so a crash mid-write can never leave a torn file behind.
|
||||
func saveTrafficBaseline(path string, values map[string]int64) error {
|
||||
data, err := json.Marshal(trafficBaselineFile{Version: 1, Values: values})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
onlineUsers, _ := a.xrayAPI.GetOnlineUsers()
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// buildTrafficReport shapes a snapshot plus the freshly polled online
|
||||
// users into the wire report, dropping zero-traffic entries. Onlines are
|
||||
// point-in-time data: they ride along with each report and are never part
|
||||
// of the baseline bookkeeping.
|
||||
func buildTrafficReport(snap *xray.TrafficSnapshot, onlineUsers []xray.OnlineUser) trafficReport {
|
||||
report := trafficReport{}
|
||||
for _, t := range traffics {
|
||||
for _, t := range snap.Traffics {
|
||||
if t == nil || !t.IsInbound || t.Up+t.Down <= 0 {
|
||||
continue
|
||||
}
|
||||
report.Inbounds = append(report.Inbounds, trafficEntry{Tag: t.Tag, Up: t.Up, Down: t.Down})
|
||||
}
|
||||
for _, ct := range clientTraffics {
|
||||
for _, ct := range snap.ClientTraffics {
|
||||
if ct == nil || ct.Email == "" || ct.Up+ct.Down <= 0 {
|
||||
continue
|
||||
}
|
||||
@@ -369,10 +413,16 @@ func (a *Agent) collectAndReportTraffic() error {
|
||||
for _, u := range onlineUsers {
|
||||
report.Onlines = append(report.Onlines, u.Email)
|
||||
}
|
||||
if len(report.Inbounds) == 0 && len(report.Users) == 0 && len(report.Onlines) == 0 {
|
||||
return nil
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func (r trafficReport) empty() bool {
|
||||
return len(r.Inbounds) == 0 && len(r.Users) == 0 && len(r.Onlines) == 0
|
||||
}
|
||||
|
||||
// postTrafficReport sends one report to the master; only an HTTP 200
|
||||
// counts as acknowledged.
|
||||
func (a *Agent) postTrafficReport(report trafficReport) error {
|
||||
url := fmt.Sprintf("%s/panel/api/agent/traffic", a.config.MasterURL)
|
||||
body, _ := json.Marshal(report)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
@@ -393,6 +443,77 @@ func (a *Agent) collectAndReportTraffic() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportTrafficWith runs one snapshot → report → commit cycle.
|
||||
//
|
||||
// The committed baseline advances ONLY after the master acknowledges the
|
||||
// report (HTTP 200). Core counters are cumulative, so when a report fails
|
||||
// nothing is lost: the next snapshot diffs against the same baseline and
|
||||
// re-includes everything unreported, exactly once. The acknowledged
|
||||
// baseline is persisted after every commit, and restored before the first
|
||||
// snapshot of a restarted agent, so an agent restart cannot swallow the
|
||||
// unreported tail into a fresh baseline either.
|
||||
//
|
||||
// A poll with nothing to send still commits: it establishes the very
|
||||
// first baseline, and keeps the persisted copy fresh.
|
||||
func (a *Agent) reportTrafficWith(sc statsClient, post func(trafficReport) error) error {
|
||||
if !a.baselineLoaded {
|
||||
a.baselineLoaded = true
|
||||
if values, err := loadTrafficBaseline(a.baselinePath()); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Warningf("Traffic baseline at %s unusable (%v); starting from a fresh baseline", a.baselinePath(), err)
|
||||
}
|
||||
} else if len(values) > 0 {
|
||||
sc.RestoreTrafficBaseline(values)
|
||||
}
|
||||
}
|
||||
|
||||
snap, err := sc.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
onlineUsers, _ := sc.GetOnlineUsers()
|
||||
|
||||
report := buildTrafficReport(snap, onlineUsers)
|
||||
if !report.empty() {
|
||||
if err := post(report); err != nil {
|
||||
// No commit: the baseline stays where the master last
|
||||
// acknowledged it, and these deltas are reported again
|
||||
// (grown) on the next poll.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sc.CommitTrafficSnapshot(snap)
|
||||
if err := saveTrafficBaseline(a.baselinePath(), snap.BaselineValues()); err != nil {
|
||||
// The in-memory commit stands — rolling it back would re-report
|
||||
// traffic the master already counted. The stale file self-heals
|
||||
// on the next successful poll; only a crash before that could
|
||||
// re-report a single interval.
|
||||
logger.Warningf("Failed to persist traffic baseline: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectAndReportTraffic snapshots the local xray counters and reports
|
||||
// the deltas since the last master-acknowledged baseline. The XrayAPI
|
||||
// instance is kept across polls so baselines survive; xray restarts are
|
||||
// handled (counters resetting to zero are treated as fresh).
|
||||
func (a *Agent) collectAndReportTraffic() error {
|
||||
if a.xrayProc == nil || !a.xrayProc.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
if a.apiPort <= 0 {
|
||||
return nil
|
||||
}
|
||||
if err := a.xrayAPI.Init(a.apiPort); err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.xrayAPI.Close()
|
||||
|
||||
return a.reportTrafficWith(&a.xrayAPI, a.postTrafficReport)
|
||||
}
|
||||
|
||||
// LoadConfig loads agent config from file
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
Reference in New Issue
Block a user