1.4.5.2
This commit is contained in:
338
EdgeNode/internal/stats/bandwidth_stat_manager.go
Normal file
338
EdgeNode/internal/stats/bandwidth_stat_manager.go
Normal file
@@ -0,0 +1,338 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedBandwidthStatManager = NewBandwidthStatManager()
|
||||
|
||||
const bandwidthTimestampDelim = 2 // N秒平均,更为精确
|
||||
|
||||
func init() {
|
||||
if !teaconst.IsMain {
|
||||
return
|
||||
}
|
||||
|
||||
events.On(events.EventLoaded, func() {
|
||||
goman.New(func() {
|
||||
SharedBandwidthStatManager.Start()
|
||||
})
|
||||
})
|
||||
|
||||
events.OnClose(func() {
|
||||
SharedBandwidthStatManager.Cancel()
|
||||
|
||||
err := SharedBandwidthStatManager.Save()
|
||||
if err != nil {
|
||||
remotelogs.Error("STAT", "save bandwidth stats failed: "+err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type BandwidthStat struct {
|
||||
Day string `json:"day"`
|
||||
TimeAt string `json:"timeAt"`
|
||||
UserId int64 `json:"userId"`
|
||||
ServerId int64 `json:"serverId"`
|
||||
|
||||
CurrentBytes int64 `json:"currentBytes"`
|
||||
CurrentTimestamp int64 `json:"currentTimestamp"`
|
||||
MaxBytes int64 `json:"maxBytes"`
|
||||
TotalBytes int64 `json:"totalBytes"`
|
||||
|
||||
CachedBytes int64 `json:"cachedBytes"`
|
||||
AttackBytes int64 `json:"attackBytes"`
|
||||
CountRequests int64 `json:"countRequests"`
|
||||
CountCachedRequests int64 `json:"countCachedRequests"`
|
||||
CountAttackRequests int64 `json:"countAttackRequests"`
|
||||
CountWebsocketConnections int64 `json:"countWebsocketConnections"`
|
||||
UserPlanId int64 `json:"userPlanId"`
|
||||
}
|
||||
|
||||
// BandwidthStatManager 服务带宽统计
|
||||
type BandwidthStatManager struct {
|
||||
m map[string]*BandwidthStat // serverId@day@time => *BandwidthStat
|
||||
|
||||
pbStats []*pb.ServerBandwidthStat
|
||||
|
||||
lastTime string // 上一次执行的时间
|
||||
|
||||
ticker *time.Ticker
|
||||
locker sync.Mutex
|
||||
|
||||
cacheFile string
|
||||
}
|
||||
|
||||
func NewBandwidthStatManager() *BandwidthStatManager {
|
||||
return &BandwidthStatManager{
|
||||
m: map[string]*BandwidthStat{},
|
||||
ticker: time.NewTicker(1 * time.Minute), // 时间小于1分钟是为了更快速地上传结果
|
||||
cacheFile: Tea.Root + "/data/stat_bandwidth.cache",
|
||||
}
|
||||
}
|
||||
|
||||
func (this *BandwidthStatManager) Start() {
|
||||
// 初始化DAU统计
|
||||
{
|
||||
err := SharedDAUManager.Init()
|
||||
if err != nil {
|
||||
remotelogs.Error("DAU_MANAGER", "initialize DAU manager failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 从上次数据中恢复
|
||||
this.locker.Lock()
|
||||
this.recover()
|
||||
this.locker.Unlock()
|
||||
|
||||
// 循环上报数据
|
||||
for range this.ticker.C {
|
||||
err := this.Loop()
|
||||
if err != nil && !rpc.IsConnError(err) {
|
||||
remotelogs.Error("BANDWIDTH_STAT_MANAGER", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *BandwidthStatManager) Loop() error {
|
||||
var regionId int64
|
||||
nodeConfig, _ := nodeconfigs.SharedNodeConfig()
|
||||
if nodeConfig != nil {
|
||||
regionId = nodeConfig.RegionId
|
||||
}
|
||||
|
||||
var now = time.Now()
|
||||
var day = timeutil.Format("Ymd", now)
|
||||
var currentTime = timeutil.FormatTime("Hi", now.Unix()/300*300) // 300s = 5 minutes
|
||||
|
||||
if this.lastTime == currentTime {
|
||||
return nil
|
||||
}
|
||||
this.lastTime = currentTime
|
||||
|
||||
var pbStats = []*pb.ServerBandwidthStat{}
|
||||
|
||||
// 历史未提交记录
|
||||
if len(this.pbStats) > 0 {
|
||||
var expiredTime = timeutil.FormatTime("Hi", time.Now().Unix()-1200) // 只保留20分钟
|
||||
|
||||
for _, stat := range this.pbStats {
|
||||
if stat.TimeAt > expiredTime {
|
||||
pbStats = append(pbStats, stat)
|
||||
}
|
||||
}
|
||||
this.pbStats = nil
|
||||
}
|
||||
|
||||
var ipStatMap = SharedDAUManager.ReadStatMap()
|
||||
|
||||
this.locker.Lock()
|
||||
for key, stat := range this.m {
|
||||
if stat.Day < day || stat.TimeAt < currentTime {
|
||||
// 防止数据出现错误
|
||||
if stat.CachedBytes > stat.TotalBytes || stat.CountCachedRequests == stat.CountRequests {
|
||||
stat.CachedBytes = stat.TotalBytes
|
||||
}
|
||||
|
||||
if stat.AttackBytes > stat.TotalBytes {
|
||||
stat.AttackBytes = stat.TotalBytes
|
||||
}
|
||||
|
||||
var ipKey = "server_" + stat.Day + "_" + types.String(stat.ServerId)
|
||||
|
||||
pbStats = append(pbStats, &pb.ServerBandwidthStat{
|
||||
Id: 0,
|
||||
UserId: stat.UserId,
|
||||
ServerId: stat.ServerId,
|
||||
Day: stat.Day,
|
||||
TimeAt: stat.TimeAt,
|
||||
Bytes: stat.MaxBytes / bandwidthTimestampDelim,
|
||||
TotalBytes: stat.TotalBytes,
|
||||
CachedBytes: stat.CachedBytes,
|
||||
AttackBytes: stat.AttackBytes,
|
||||
CountRequests: stat.CountRequests,
|
||||
CountCachedRequests: stat.CountCachedRequests,
|
||||
CountAttackRequests: stat.CountAttackRequests,
|
||||
CountWebsocketConnections: stat.CountWebsocketConnections,
|
||||
CountIPs: ipStatMap[ipKey],
|
||||
UserPlanId: stat.UserPlanId,
|
||||
NodeRegionId: regionId,
|
||||
})
|
||||
delete(this.m, key)
|
||||
}
|
||||
}
|
||||
this.locker.Unlock()
|
||||
|
||||
if len(pbStats) > 0 {
|
||||
// 上传
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = rpcClient.ServerBandwidthStatRPC.UploadServerBandwidthStats(rpcClient.Context(), &pb.UploadServerBandwidthStatsRequest{ServerBandwidthStats: pbStats})
|
||||
if err != nil {
|
||||
this.pbStats = pbStats
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBandwidth 添加带宽数据
|
||||
func (this *BandwidthStatManager) AddBandwidth(userId int64, userPlanId int64, serverId int64, peekBytes int64, totalBytes int64) {
|
||||
if serverId <= 0 || (peekBytes == 0 && totalBytes == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
var now = fasttime.Now()
|
||||
var timestamp = now.Unix() / bandwidthTimestampDelim * bandwidthTimestampDelim // 将时间戳均分成N等份
|
||||
var day = now.Ymd()
|
||||
var timeAt = now.Round5Hi()
|
||||
var key = types.String(serverId) + "@" + day + "@" + timeAt
|
||||
|
||||
// 增加TCP Header尺寸,这里默认MTU为1500,且默认为IPv4
|
||||
const mtu = 1500
|
||||
const tcpHeaderSize = 20
|
||||
if peekBytes > mtu {
|
||||
peekBytes += peekBytes * tcpHeaderSize / mtu
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
stat, ok := this.m[key]
|
||||
if ok {
|
||||
// 此刻如果发生用户ID(userId)的变化也忽略,等N分钟后有新记录后再换
|
||||
|
||||
if stat.CurrentTimestamp == timestamp {
|
||||
stat.CurrentBytes += peekBytes
|
||||
} else {
|
||||
stat.CurrentBytes = peekBytes
|
||||
stat.CurrentTimestamp = timestamp
|
||||
}
|
||||
if stat.CurrentBytes > stat.MaxBytes {
|
||||
stat.MaxBytes = stat.CurrentBytes
|
||||
}
|
||||
|
||||
stat.TotalBytes += totalBytes
|
||||
} else {
|
||||
this.m[key] = &BandwidthStat{
|
||||
Day: day,
|
||||
TimeAt: timeAt,
|
||||
UserId: userId,
|
||||
UserPlanId: userPlanId,
|
||||
ServerId: serverId,
|
||||
CurrentBytes: peekBytes,
|
||||
MaxBytes: peekBytes,
|
||||
TotalBytes: totalBytes,
|
||||
CurrentTimestamp: timestamp,
|
||||
}
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
// AddTraffic 添加请求数据
|
||||
func (this *BandwidthStatManager) AddTraffic(serverId int64, cachedBytes int64, countRequests int64, countCachedRequests int64, countAttacks int64, attackBytes int64, countWebsocketConnections int64) {
|
||||
var now = fasttime.Now()
|
||||
var day = now.Ymd()
|
||||
var timeAt = now.Round5Hi()
|
||||
var key = types.String(serverId) + "@" + day + "@" + timeAt
|
||||
this.locker.Lock()
|
||||
// 只有有记录了才会添加
|
||||
stat, ok := this.m[key]
|
||||
if ok {
|
||||
stat.CachedBytes += cachedBytes
|
||||
stat.CountRequests += countRequests
|
||||
stat.CountCachedRequests += countCachedRequests
|
||||
stat.CountAttackRequests += countAttacks
|
||||
stat.AttackBytes += attackBytes
|
||||
stat.CountWebsocketConnections += countWebsocketConnections
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *BandwidthStatManager) Inspect() {
|
||||
this.locker.Lock()
|
||||
logs.PrintAsJSON(this.m)
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
func (this *BandwidthStatManager) Map() map[int64]int64 /** serverId => max bytes **/ {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
var m = map[int64]int64{}
|
||||
for _, v := range this.m {
|
||||
m[v.ServerId] = v.MaxBytes / bandwidthTimestampDelim
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Save 保存到本地磁盘
|
||||
func (this *BandwidthStatManager) Save() error {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
if len(this.m) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := json.Marshal(this.m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Remove(this.cacheFile)
|
||||
return os.WriteFile(this.cacheFile, data, 0666)
|
||||
}
|
||||
|
||||
// Cancel 取消上传
|
||||
func (this *BandwidthStatManager) Cancel() {
|
||||
this.ticker.Stop()
|
||||
}
|
||||
|
||||
// 从本地缓存文件中恢复数据
|
||||
func (this *BandwidthStatManager) recover() {
|
||||
cacheData, err := os.ReadFile(this.cacheFile)
|
||||
if err == nil {
|
||||
var m = map[string]*BandwidthStat{}
|
||||
err = json.Unmarshal(cacheData, &m)
|
||||
if err == nil && len(m) > 0 {
|
||||
var lastTime = ""
|
||||
for _, stat := range m {
|
||||
if stat.Day != fasttime.Now().Ymd() {
|
||||
continue
|
||||
}
|
||||
if len(lastTime) == 0 || stat.TimeAt > lastTime {
|
||||
lastTime = stat.TimeAt
|
||||
}
|
||||
}
|
||||
if len(lastTime) > 0 {
|
||||
var availableTime = timeutil.FormatTime("Hi", (time.Now().Unix()-300) /** 只保留5分钟的 **/ /300*300) // 300s = 5 minutes
|
||||
if lastTime >= availableTime {
|
||||
this.m = m
|
||||
this.lastTime = lastTime
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = os.Remove(this.cacheFile)
|
||||
}
|
||||
}
|
||||
99
EdgeNode/internal/stats/bandwidth_stat_manager_test.go
Normal file
99
EdgeNode/internal/stats/bandwidth_stat_manager_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package stats_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/stats"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBandwidthStatManager_Add(t *testing.T) {
|
||||
var manager = stats.NewBandwidthStatManager()
|
||||
manager.AddBandwidth(1, 0, 1, 10, 10)
|
||||
manager.AddBandwidth(1, 0, 1, 10, 10)
|
||||
manager.AddBandwidth(1, 0, 1, 10, 10)
|
||||
time.Sleep(1 * time.Second)
|
||||
manager.AddBandwidth(1, 0, 1, 85, 85)
|
||||
time.Sleep(1 * time.Second)
|
||||
manager.AddBandwidth(1, 0, 1, 25, 25)
|
||||
manager.AddBandwidth(1, 0, 1, 75, 75)
|
||||
manager.Inspect()
|
||||
}
|
||||
|
||||
func TestBandwidthStatManager_Loop(t *testing.T) {
|
||||
var manager = stats.NewBandwidthStatManager()
|
||||
manager.AddBandwidth(1, 0, 1, 10, 10)
|
||||
manager.AddBandwidth(1, 0, 1, 10, 10)
|
||||
manager.AddBandwidth(1, 0, 1, 10, 10)
|
||||
err := manager.Loop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBandwidthStatManager_Add(b *testing.B) {
|
||||
var manager = stats.NewBandwidthStatManager()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
var i int
|
||||
for pb.Next() {
|
||||
i++
|
||||
manager.AddBandwidth(1, 0, int64(i%100), 10, 10)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkBandwidthStatManager_Slice(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var pbStats = []*pb.ServerBandwidthStat{}
|
||||
for j := 0; j < 100; j++ {
|
||||
var stat = &stats.BandwidthStat{}
|
||||
pbStats = append(pbStats, &pb.ServerBandwidthStat{
|
||||
Id: 0,
|
||||
UserId: stat.UserId,
|
||||
ServerId: stat.ServerId,
|
||||
Day: stat.Day,
|
||||
TimeAt: stat.TimeAt,
|
||||
Bytes: stat.MaxBytes / 2,
|
||||
TotalBytes: stat.TotalBytes,
|
||||
CachedBytes: stat.CachedBytes,
|
||||
AttackBytes: stat.AttackBytes,
|
||||
CountRequests: stat.CountRequests,
|
||||
CountCachedRequests: stat.CountCachedRequests,
|
||||
CountAttackRequests: stat.CountAttackRequests,
|
||||
CountWebsocketConnections: stat.CountWebsocketConnections,
|
||||
NodeRegionId: 1,
|
||||
})
|
||||
}
|
||||
_ = pbStats
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBandwidthStatManager_Slice2(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var statsSlice = []*stats.BandwidthStat{}
|
||||
for j := 0; j < 100; j++ {
|
||||
var stat = &stats.BandwidthStat{}
|
||||
statsSlice = append(statsSlice, stat)
|
||||
}
|
||||
_ = statsSlice
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBandwidthStatManager_Slice3(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var statsSlice = make([]*stats.BandwidthStat, 2000)
|
||||
for j := 0; j < 100; j++ {
|
||||
var stat = &stats.BandwidthStat{}
|
||||
statsSlice[j] = stat
|
||||
}
|
||||
}
|
||||
}
|
||||
275
EdgeNode/internal/stats/dau_manager.go
Normal file
275
EdgeNode/internal/stats/dau_manager.go
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/idles"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/trackers"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedDAUManager = NewDAUManager()
|
||||
|
||||
type IPInfo struct {
|
||||
IP string
|
||||
ServerId int64
|
||||
}
|
||||
|
||||
type DAUManager struct {
|
||||
isReady bool
|
||||
|
||||
cacheFile string
|
||||
|
||||
ipChan chan IPInfo
|
||||
ipTable *kvstore.Table[[]byte] // server_DATE_serverId_ip => nil
|
||||
|
||||
statMap map[string]int64 // server_DATE_serverId => count
|
||||
statLocker sync.RWMutex
|
||||
|
||||
cleanTicker *time.Ticker
|
||||
}
|
||||
|
||||
// NewDAUManager DAU计算器
|
||||
func NewDAUManager() *DAUManager {
|
||||
return &DAUManager{
|
||||
cacheFile: Tea.Root + "/data/stat_dau.cache",
|
||||
statMap: map[string]int64{},
|
||||
cleanTicker: time.NewTicker(24 * time.Hour),
|
||||
ipChan: make(chan IPInfo, 8192),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *DAUManager) Init() error {
|
||||
// recover from cache
|
||||
_ = this.recover()
|
||||
|
||||
// create table
|
||||
store, storeErr := kvstore.DefaultStore()
|
||||
if storeErr != nil {
|
||||
return storeErr
|
||||
}
|
||||
|
||||
db, dbErr := store.NewDB("dau")
|
||||
if dbErr != nil {
|
||||
return dbErr
|
||||
}
|
||||
|
||||
{
|
||||
table, err := kvstore.NewTable[[]byte]("ip", kvstore.NewNilValueEncoder())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.AddTable(table)
|
||||
this.ipTable = table
|
||||
}
|
||||
|
||||
{
|
||||
table, err := kvstore.NewTable[uint64]("stats", kvstore.NewIntValueEncoder[uint64]())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.AddTable(table)
|
||||
}
|
||||
|
||||
// clean expires items
|
||||
goman.New(func() {
|
||||
idles.RunTicker(this.cleanTicker, func() {
|
||||
err := this.CleanStats()
|
||||
if err != nil {
|
||||
remotelogs.Error("DAU_MANAGER", "clean stats failed: "+err.Error())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// dump ip to kvstore
|
||||
goman.New(func() {
|
||||
// cache latest IPs to reduce kv queries
|
||||
var cachedIPs []IPInfo
|
||||
var maxIPs = runtime.NumCPU() * 8
|
||||
if maxIPs <= 0 {
|
||||
maxIPs = 8
|
||||
} else if maxIPs > 64 {
|
||||
maxIPs = 64
|
||||
}
|
||||
|
||||
var day = fasttime.Now().Ymd()
|
||||
|
||||
Loop:
|
||||
for ipInfo := range this.ipChan {
|
||||
// check day
|
||||
if fasttime.Now().Ymd() != day {
|
||||
day = fasttime.Now().Ymd()
|
||||
cachedIPs = []IPInfo{}
|
||||
}
|
||||
|
||||
// lookup cache
|
||||
for _, cachedIP := range cachedIPs {
|
||||
if cachedIP.IP == ipInfo.IP && cachedIP.ServerId == ipInfo.ServerId {
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
|
||||
// add to cache
|
||||
cachedIPs = append(cachedIPs, ipInfo)
|
||||
if len(cachedIPs) > maxIPs {
|
||||
cachedIPs = cachedIPs[1:]
|
||||
}
|
||||
|
||||
_ = this.processIP(ipInfo.ServerId, ipInfo.IP)
|
||||
}
|
||||
})
|
||||
|
||||
// dump to cache when close
|
||||
events.OnClose(func() {
|
||||
_ = this.Close()
|
||||
})
|
||||
|
||||
this.isReady = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DAUManager) AddIP(serverId int64, ip string) {
|
||||
select {
|
||||
case this.ipChan <- IPInfo{
|
||||
IP: ip,
|
||||
ServerId: serverId,
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (this *DAUManager) processIP(serverId int64, ip string) error {
|
||||
if !this.isReady {
|
||||
return nil
|
||||
}
|
||||
|
||||
// day
|
||||
var date = fasttime.Now().Ymd()
|
||||
|
||||
{
|
||||
var key = "server_" + date + "_" + types.String(serverId) + "_" + ip
|
||||
found, err := this.ipTable.Exist(key)
|
||||
if err != nil || found {
|
||||
return err
|
||||
}
|
||||
|
||||
err = this.ipTable.Set(key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var key = "server_" + date + "_" + types.String(serverId)
|
||||
this.statLocker.Lock()
|
||||
this.statMap[key] = this.statMap[key] + 1
|
||||
this.statLocker.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DAUManager) ReadStatMap() map[string]int64 {
|
||||
this.statLocker.Lock()
|
||||
var statMap = this.statMap
|
||||
this.statMap = map[string]int64{}
|
||||
this.statLocker.Unlock()
|
||||
return statMap
|
||||
}
|
||||
|
||||
func (this *DAUManager) Flush() error {
|
||||
return this.ipTable.DB().Store().Flush()
|
||||
}
|
||||
|
||||
func (this *DAUManager) TestInspect(t *testing.T) {
|
||||
err := this.ipTable.DB().Inspect(func(key []byte, value []byte) {
|
||||
t.Log(string(key), "=>", string(value))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *DAUManager) Close() error {
|
||||
this.cleanTicker.Stop()
|
||||
|
||||
this.statLocker.Lock()
|
||||
var statMap = this.statMap
|
||||
this.statMap = map[string]int64{}
|
||||
this.statLocker.Unlock()
|
||||
|
||||
if len(statMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
statJSON, err := json.Marshal(statMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(this.cacheFile, statJSON, 0666)
|
||||
}
|
||||
|
||||
func (this *DAUManager) CleanStats() error {
|
||||
if !this.isReady {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tr = trackers.Begin("STAT:DAU_CLEAN_STATS")
|
||||
defer tr.End()
|
||||
|
||||
// day
|
||||
{
|
||||
var date = timeutil.Format("Ymd", time.Now().AddDate(0, 0, -2))
|
||||
err := this.ipTable.DeleteRange("server_", "server_"+date)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DAUManager) Truncate() error {
|
||||
return this.ipTable.Truncate()
|
||||
}
|
||||
|
||||
func (this *DAUManager) recover() error {
|
||||
data, err := os.ReadFile(this.cacheFile)
|
||||
if err != nil || len(data) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Remove(this.cacheFile)
|
||||
|
||||
var statMap = map[string]int64{}
|
||||
err = json.Unmarshal(data, &statMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var today = timeutil.Format("Ymd")
|
||||
for key := range statMap {
|
||||
var pieces = strings.Split(key, "_")
|
||||
if pieces[1] != today {
|
||||
delete(statMap, key)
|
||||
}
|
||||
}
|
||||
this.statMap = statMap
|
||||
return nil
|
||||
}
|
||||
135
EdgeNode/internal/stats/dau_manager_test.go
Normal file
135
EdgeNode/internal/stats/dau_manager_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package stats_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/stats"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDAUManager_AddIP(t *testing.T) {
|
||||
var manager = stats.NewDAUManager()
|
||||
err := manager.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
manager.AddIP(1, "127.0.0.1")
|
||||
manager.AddIP(1, "127.0.0.2")
|
||||
manager.AddIP(1, "127.0.0.3")
|
||||
manager.AddIP(1, "127.0.0.4")
|
||||
manager.AddIP(1, "127.0.0.2")
|
||||
manager.AddIP(1, "127.0.0.3")
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
err = manager.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("======")
|
||||
manager.TestInspect(t)
|
||||
}
|
||||
|
||||
func TestDAUManager_AddIP_Many(t *testing.T) {
|
||||
var manager = stats.NewDAUManager()
|
||||
err := manager.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var before = time.Now()
|
||||
defer func() {
|
||||
t.Log("cost:", time.Since(before).Seconds()*1000, "ms")
|
||||
}()
|
||||
|
||||
var count = 1
|
||||
|
||||
if testutils.IsSingleTesting() {
|
||||
count = 10_000
|
||||
}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
manager.AddIP(int64(rands.Int(1, 10)), testutils.RandIP())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDAUManager_CleanStats(t *testing.T) {
|
||||
var manager = stats.NewDAUManager()
|
||||
err := manager.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var before = time.Now()
|
||||
defer func() {
|
||||
t.Log("cost:", time.Since(before).Seconds()*1000, "ms")
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
_ = manager.Flush()
|
||||
}()
|
||||
|
||||
err = manager.CleanStats()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDAUManager_TestInspect(t *testing.T) {
|
||||
var manager = stats.NewDAUManager()
|
||||
err := manager.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
manager.TestInspect(t)
|
||||
}
|
||||
|
||||
func TestDAUManager_Truncate(t *testing.T) {
|
||||
var manager = stats.NewDAUManager()
|
||||
err := manager.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = manager.Truncate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = manager.Flush()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDAUManager_AddIP_Cache(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var cachedIPs []stats.IPInfo
|
||||
var maxIPs = 128
|
||||
b.Log("maxIPs:", maxIPs)
|
||||
for i := 0; i < maxIPs; i++ {
|
||||
cachedIPs = append(cachedIPs, stats.IPInfo{
|
||||
IP: testutils.RandIP(),
|
||||
ServerId: 1,
|
||||
})
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var ip = "1.2.3.4"
|
||||
for _, cacheIP := range cachedIPs {
|
||||
if cacheIP.IP == ip && cacheIP.ServerId == 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
543
EdgeNode/internal/stats/http_request_stat_manager.go
Normal file
543
EdgeNode/internal/stats/http_request_stat_manager.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
iplib "github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/monitor"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/agents"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/trackers"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StatItem struct {
|
||||
Bytes int64
|
||||
CountRequests int64
|
||||
CountAttackRequests int64
|
||||
AttackBytes int64
|
||||
}
|
||||
|
||||
var SharedHTTPRequestStatManager = NewHTTPRequestStatManager()
|
||||
|
||||
// HTTPRequestStatManager HTTP请求相关的统计
|
||||
// 这里的统计是一个辅助统计,注意不要因为统计而影响服务工作性能
|
||||
type HTTPRequestStatManager struct {
|
||||
ipChan chan string
|
||||
userAgentChan chan string
|
||||
firewallRuleGroupChan chan string
|
||||
|
||||
cityMap map[string]*StatItem // serverId@country@province@city => *StatItem ,不需要加锁,因为我们是使用channel依次执行的
|
||||
providerMap map[string]int64 // serverId@provider => count
|
||||
systemMap map[string]int64 // serverId@system@version => count
|
||||
browserMap map[string]int64 // serverId@browser@version => count
|
||||
|
||||
dailyFirewallRuleGroupMap map[string]int64 // serverId@firewallRuleGroupId@action => count
|
||||
|
||||
serverCityCountMap map[string]int16 // serverIdString => count cities
|
||||
serverSystemCountMap map[string]int16 // serverIdString => count systems
|
||||
serverBrowserCountMap map[string]int16 // serverIdString => count browsers
|
||||
|
||||
totalAttackRequests int64
|
||||
|
||||
locker sync.Mutex
|
||||
|
||||
monitorTicker *time.Ticker
|
||||
uploadTicker *time.Ticker
|
||||
}
|
||||
|
||||
// NewHTTPRequestStatManager 获取新对象
|
||||
func NewHTTPRequestStatManager() *HTTPRequestStatManager {
|
||||
return &HTTPRequestStatManager{
|
||||
ipChan: make(chan string, 10_000), // TODO 将来可以配置容量
|
||||
userAgentChan: make(chan string, 10_000), // TODO 将来可以配置容量
|
||||
firewallRuleGroupChan: make(chan string, 10_000), // TODO 将来可以配置容量
|
||||
cityMap: map[string]*StatItem{},
|
||||
providerMap: map[string]int64{},
|
||||
systemMap: map[string]int64{},
|
||||
browserMap: map[string]int64{},
|
||||
dailyFirewallRuleGroupMap: map[string]int64{},
|
||||
|
||||
serverCityCountMap: map[string]int16{},
|
||||
serverSystemCountMap: map[string]int16{},
|
||||
serverBrowserCountMap: map[string]int16{},
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动
|
||||
func (this *HTTPRequestStatManager) Start() {
|
||||
// 上传请求总数
|
||||
this.monitorTicker = time.NewTicker(1 * time.Minute)
|
||||
events.OnKey(events.EventQuit, this, func() {
|
||||
this.monitorTicker.Stop()
|
||||
})
|
||||
goman.New(func() {
|
||||
for range this.monitorTicker.C {
|
||||
if this.totalAttackRequests > 0 {
|
||||
monitor.SharedValueQueue.Add(nodeconfigs.NodeValueItemAttackRequests, maps.Map{"total": this.totalAttackRequests})
|
||||
this.totalAttackRequests = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.uploadTicker = time.NewTicker(30 * time.Minute)
|
||||
if Tea.IsTesting() {
|
||||
this.uploadTicker = time.NewTicker(10 * time.Second) // 在测试环境下缩短Ticker时间,以方便我们调试
|
||||
}
|
||||
remotelogs.Println("HTTP_REQUEST_STAT_MANAGER", "start ...")
|
||||
events.OnKey(events.EventQuit, this, func() {
|
||||
remotelogs.Println("HTTP_REQUEST_STAT_MANAGER", "quit")
|
||||
this.uploadTicker.Stop()
|
||||
})
|
||||
|
||||
// 上传Ticker
|
||||
goman.New(func() {
|
||||
for range this.uploadTicker.C {
|
||||
var tr = trackers.Begin("UPLOAD_REQUEST_STATS")
|
||||
err := this.Upload()
|
||||
tr.End()
|
||||
if err != nil {
|
||||
if !rpc.IsConnError(err) {
|
||||
remotelogs.Error("HTTP_REQUEST_STAT_MANAGER", "upload failed: "+err.Error())
|
||||
} else {
|
||||
remotelogs.Warn("HTTP_REQUEST_STAT_MANAGER", "upload failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// 分析Ticker
|
||||
for {
|
||||
err := this.Loop()
|
||||
if err != nil {
|
||||
if rpc.IsConnError(err) {
|
||||
remotelogs.Warn("HTTP_REQUEST_STAT_MANAGER", err.Error())
|
||||
} else {
|
||||
remotelogs.Error("HTTP_REQUEST_STAT_MANAGER", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddRemoteAddr 添加客户端地址
|
||||
func (this *HTTPRequestStatManager) AddRemoteAddr(serverId int64, remoteAddr string, bytes int64, isAttack bool) {
|
||||
if len(remoteAddr) == 0 {
|
||||
return
|
||||
}
|
||||
if remoteAddr[0] == '[' { // 排除IPv6
|
||||
return
|
||||
}
|
||||
var index = strings.Index(remoteAddr, ":")
|
||||
var ip string
|
||||
if index < 0 {
|
||||
ip = remoteAddr
|
||||
} else {
|
||||
ip = remoteAddr[:index]
|
||||
}
|
||||
if len(ip) > 0 {
|
||||
var s string
|
||||
if isAttack {
|
||||
s = strconv.FormatInt(serverId, 10) + "@" + ip + "@" + types.String(bytes) + "@1"
|
||||
} else {
|
||||
s = strconv.FormatInt(serverId, 10) + "@" + ip + "@" + types.String(bytes) + "@0"
|
||||
}
|
||||
select {
|
||||
case this.ipChan <- s:
|
||||
default:
|
||||
// 超出容量我们就丢弃
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddUserAgent 添加UserAgent
|
||||
func (this *HTTPRequestStatManager) AddUserAgent(serverId int64, userAgent string, ip string) {
|
||||
if len(userAgent) == 0 || strings.ContainsRune(userAgent, '@') /** 非常重要,防止后面组合字符串时出现异常 **/ {
|
||||
return
|
||||
}
|
||||
|
||||
// 是否包含一些知名Agent
|
||||
if len(userAgent) > 0 && len(ip) > 0 && agents.IsAgentFromUserAgent(userAgent) {
|
||||
agents.SharedQueue.Push(ip)
|
||||
}
|
||||
|
||||
select {
|
||||
case this.userAgentChan <- strconv.FormatInt(serverId, 10) + "@" + userAgent:
|
||||
default:
|
||||
// 超出容量我们就丢弃
|
||||
}
|
||||
}
|
||||
|
||||
// AddFirewallRuleGroupId 添加防火墙拦截动作
|
||||
func (this *HTTPRequestStatManager) AddFirewallRuleGroupId(serverId int64, firewallRuleGroupId int64, actions []*waf.ActionConfig) {
|
||||
if firewallRuleGroupId <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
this.totalAttackRequests++
|
||||
|
||||
for _, action := range actions {
|
||||
select {
|
||||
case this.firewallRuleGroupChan <- strconv.FormatInt(serverId, 10) + "@" + strconv.FormatInt(firewallRuleGroupId, 10) + "@" + action.Code:
|
||||
default:
|
||||
// 超出容量我们就丢弃
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop 单个循环
|
||||
func (this *HTTPRequestStatManager) Loop() error {
|
||||
select {
|
||||
case ipString := <-this.ipChan:
|
||||
// serverId@ip@bytes@isAttack
|
||||
var pieces = strings.Split(ipString, "@")
|
||||
if len(pieces) < 4 {
|
||||
return nil
|
||||
}
|
||||
var serverIdString = pieces[0]
|
||||
var ip = pieces[1]
|
||||
|
||||
var result = iplib.LookupIP(ip)
|
||||
if result != nil && result.IsOk() {
|
||||
this.locker.Lock()
|
||||
if result.CountryId() > 0 {
|
||||
var key = serverIdString + "@" + types.String(result.CountryId()) + "@" + types.String(result.ProvinceId()) + "@" + types.String(result.CityId())
|
||||
stat, ok := this.cityMap[key]
|
||||
if !ok {
|
||||
// 检查数量
|
||||
if this.serverCityCountMap[serverIdString] > 128 { // 限制单个服务的城市数量,防止数量过多
|
||||
this.locker.Unlock()
|
||||
return nil
|
||||
}
|
||||
this.serverCityCountMap[serverIdString]++ // 需要放在限制之后,因为使用的是int16
|
||||
|
||||
stat = &StatItem{}
|
||||
this.cityMap[key] = stat
|
||||
}
|
||||
stat.Bytes += types.Int64(pieces[2])
|
||||
stat.CountRequests++
|
||||
if types.Int8(pieces[3]) == 1 {
|
||||
stat.AttackBytes += types.Int64(pieces[2])
|
||||
stat.CountAttackRequests++
|
||||
}
|
||||
}
|
||||
|
||||
if result.ProviderId() > 0 {
|
||||
this.providerMap[serverIdString+"@"+types.String(result.ProviderId())]++
|
||||
} else if utils.IsLocalIP(ip) { // 局域网IP
|
||||
this.providerMap[serverIdString+"@258"]++
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
case userAgentString := <-this.userAgentChan:
|
||||
var atIndex = strings.Index(userAgentString, "@")
|
||||
if atIndex < 0 {
|
||||
return nil
|
||||
}
|
||||
var serverIdString = userAgentString[:atIndex]
|
||||
var userAgent = userAgentString[atIndex+1:]
|
||||
|
||||
var result = SharedUserAgentParser.Parse(userAgent)
|
||||
var osInfo = result.OS
|
||||
if len(osInfo.Name) > 0 {
|
||||
dotIndex := strings.Index(osInfo.Version, ".")
|
||||
if dotIndex > -1 {
|
||||
osInfo.Version = osInfo.Version[:dotIndex]
|
||||
}
|
||||
this.locker.Lock()
|
||||
|
||||
var systemKey = serverIdString + "@" + osInfo.Name + "@" + osInfo.Version
|
||||
_, ok := this.systemMap[systemKey]
|
||||
if !ok {
|
||||
if this.serverSystemCountMap[serverIdString] < 128 { // 限制最大数据,防止攻击
|
||||
this.serverSystemCountMap[serverIdString]++
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
this.systemMap[systemKey]++
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
var browser, browserVersion = result.BrowserName, result.BrowserVersion
|
||||
if len(browser) > 0 {
|
||||
dotIndex := strings.Index(browserVersion, ".")
|
||||
if dotIndex > -1 {
|
||||
browserVersion = browserVersion[:dotIndex]
|
||||
}
|
||||
this.locker.Lock()
|
||||
|
||||
var browserKey = serverIdString + "@" + browser + "@" + browserVersion
|
||||
_, ok := this.browserMap[browserKey]
|
||||
if !ok {
|
||||
if this.serverBrowserCountMap[serverIdString] < 256 { // 限制最大数据,防止攻击
|
||||
this.serverBrowserCountMap[serverIdString]++
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
this.browserMap[browserKey]++
|
||||
}
|
||||
this.locker.Unlock()
|
||||
}
|
||||
case firewallRuleGroupString := <-this.firewallRuleGroupChan:
|
||||
this.locker.Lock()
|
||||
this.dailyFirewallRuleGroupMap[firewallRuleGroupString]++
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upload 上传数据
|
||||
func (this *HTTPRequestStatManager) Upload() error {
|
||||
// 上传统计数据
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 拷贝数据
|
||||
this.locker.Lock()
|
||||
var cityMap = this.cityMap
|
||||
var providerMap = this.providerMap
|
||||
var systemMap = this.systemMap
|
||||
var browserMap = this.browserMap
|
||||
var dailyFirewallRuleGroupMap = this.dailyFirewallRuleGroupMap
|
||||
|
||||
this.cityMap = map[string]*StatItem{}
|
||||
this.providerMap = map[string]int64{}
|
||||
this.systemMap = map[string]int64{}
|
||||
this.browserMap = map[string]int64{}
|
||||
this.dailyFirewallRuleGroupMap = map[string]int64{}
|
||||
|
||||
this.serverCityCountMap = map[string]int16{}
|
||||
this.serverSystemCountMap = map[string]int16{}
|
||||
this.serverBrowserCountMap = map[string]int16{}
|
||||
|
||||
this.locker.Unlock()
|
||||
|
||||
// 上传限制
|
||||
var maxCities int16 = 32
|
||||
var maxProviders int16 = 32
|
||||
var maxSystems int16 = 64
|
||||
var maxBrowsers int16 = 64
|
||||
nodeConfig, _ := nodeconfigs.SharedNodeConfig()
|
||||
if nodeConfig != nil {
|
||||
var serverConfig = nodeConfig.GlobalServerConfig // 复制是为了防止在中途修改
|
||||
if serverConfig != nil {
|
||||
var uploadConfig = serverConfig.Stat.Upload
|
||||
if uploadConfig.MaxCities > 0 {
|
||||
maxCities = uploadConfig.MaxCities
|
||||
}
|
||||
if uploadConfig.MaxProviders > 0 {
|
||||
maxProviders = uploadConfig.MaxProviders
|
||||
}
|
||||
if uploadConfig.MaxSystems > 0 {
|
||||
maxSystems = uploadConfig.MaxSystems
|
||||
}
|
||||
if uploadConfig.MaxBrowsers > 0 {
|
||||
maxBrowsers = uploadConfig.MaxBrowsers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pbCities = []*pb.UploadServerHTTPRequestStatRequest_RegionCity{}
|
||||
var pbProviders = []*pb.UploadServerHTTPRequestStatRequest_RegionProvider{}
|
||||
var pbSystems = []*pb.UploadServerHTTPRequestStatRequest_System{}
|
||||
var pbBrowsers = []*pb.UploadServerHTTPRequestStatRequest_Browser{}
|
||||
|
||||
// 城市
|
||||
for k, stat := range cityMap {
|
||||
var pieces = strings.SplitN(k, "@", 4)
|
||||
var serverId = types.Int64(pieces[0])
|
||||
pbCities = append(pbCities, &pb.UploadServerHTTPRequestStatRequest_RegionCity{
|
||||
ServerId: serverId,
|
||||
CountryId: types.Int64(pieces[1]),
|
||||
ProvinceId: types.Int64(pieces[2]),
|
||||
CityId: types.Int64(pieces[3]),
|
||||
CountRequests: stat.CountRequests,
|
||||
CountAttackRequests: stat.CountAttackRequests,
|
||||
Bytes: stat.Bytes,
|
||||
AttackBytes: stat.AttackBytes,
|
||||
})
|
||||
}
|
||||
if len(cityMap) > int(maxCities) {
|
||||
var newPBCities = []*pb.UploadServerHTTPRequestStatRequest_RegionCity{}
|
||||
sort.Slice(pbCities, func(i, j int) bool {
|
||||
return pbCities[i].CountRequests > pbCities[j].CountRequests
|
||||
})
|
||||
var serverCountMap = map[int64]int16{} // serverId => count
|
||||
for _, city := range pbCities {
|
||||
serverCountMap[city.ServerId]++
|
||||
if serverCountMap[city.ServerId] > maxCities {
|
||||
continue
|
||||
}
|
||||
newPBCities = append(newPBCities, city)
|
||||
}
|
||||
if len(pbCities) != len(newPBCities) {
|
||||
pbCities = newPBCities
|
||||
}
|
||||
}
|
||||
|
||||
// 运营商
|
||||
for k, count := range providerMap {
|
||||
var pieces = strings.SplitN(k, "@", 2)
|
||||
var serverId = types.Int64(pieces[0])
|
||||
pbProviders = append(pbProviders, &pb.UploadServerHTTPRequestStatRequest_RegionProvider{
|
||||
ServerId: serverId,
|
||||
ProviderId: types.Int64(pieces[1]),
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
if len(providerMap) > int(maxProviders) {
|
||||
var newPBProviders = []*pb.UploadServerHTTPRequestStatRequest_RegionProvider{}
|
||||
sort.Slice(pbProviders, func(i, j int) bool {
|
||||
return pbProviders[i].Count > pbProviders[j].Count
|
||||
})
|
||||
var serverCountMap = map[int64]int16{}
|
||||
for _, provider := range pbProviders {
|
||||
serverCountMap[provider.ServerId]++
|
||||
if serverCountMap[provider.ServerId] > maxProviders {
|
||||
continue
|
||||
}
|
||||
newPBProviders = append(newPBProviders, provider)
|
||||
}
|
||||
if len(pbProviders) != len(newPBProviders) {
|
||||
pbProviders = newPBProviders
|
||||
}
|
||||
}
|
||||
|
||||
// 操作系统
|
||||
for k, count := range systemMap {
|
||||
var pieces = strings.SplitN(k, "@", 3)
|
||||
var serverId = types.Int64(pieces[0])
|
||||
pbSystems = append(pbSystems, &pb.UploadServerHTTPRequestStatRequest_System{
|
||||
ServerId: serverId,
|
||||
Name: pieces[1],
|
||||
Version: pieces[2],
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
if len(systemMap) > int(maxSystems) {
|
||||
var newPBSystems = []*pb.UploadServerHTTPRequestStatRequest_System{}
|
||||
sort.Slice(pbSystems, func(i, j int) bool {
|
||||
return pbSystems[i].Count > pbSystems[j].Count
|
||||
})
|
||||
var serverCountMap = map[int64]int16{}
|
||||
for _, system := range pbSystems {
|
||||
serverCountMap[system.ServerId]++
|
||||
if serverCountMap[system.ServerId] > maxSystems {
|
||||
continue
|
||||
}
|
||||
newPBSystems = append(newPBSystems, system)
|
||||
}
|
||||
if len(pbSystems) != len(newPBSystems) {
|
||||
pbSystems = newPBSystems
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览器
|
||||
for k, count := range browserMap {
|
||||
var pieces = strings.SplitN(k, "@", 3)
|
||||
var serverId = types.Int64(pieces[0])
|
||||
pbBrowsers = append(pbBrowsers, &pb.UploadServerHTTPRequestStatRequest_Browser{
|
||||
ServerId: serverId,
|
||||
Name: pieces[1],
|
||||
Version: pieces[2],
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
if len(browserMap) > int(maxBrowsers) {
|
||||
var newPBBrowsers = []*pb.UploadServerHTTPRequestStatRequest_Browser{}
|
||||
sort.Slice(pbBrowsers, func(i, j int) bool {
|
||||
return pbBrowsers[i].Count > pbBrowsers[j].Count
|
||||
})
|
||||
var serverCountMap = map[int64]int16{}
|
||||
for _, browser := range pbBrowsers {
|
||||
serverCountMap[browser.ServerId]++
|
||||
if serverCountMap[browser.ServerId] > maxBrowsers {
|
||||
continue
|
||||
}
|
||||
newPBBrowsers = append(newPBBrowsers, browser)
|
||||
}
|
||||
if len(pbBrowsers) != len(newPBBrowsers) {
|
||||
pbBrowsers = newPBBrowsers
|
||||
}
|
||||
}
|
||||
|
||||
// 防火墙相关
|
||||
var pbFirewallRuleGroups = []*pb.UploadServerHTTPRequestStatRequest_HTTPFirewallRuleGroup{}
|
||||
for k, count := range dailyFirewallRuleGroupMap {
|
||||
var pieces = strings.SplitN(k, "@", 3)
|
||||
pbFirewallRuleGroups = append(pbFirewallRuleGroups, &pb.UploadServerHTTPRequestStatRequest_HTTPFirewallRuleGroup{
|
||||
ServerId: types.Int64(pieces[0]),
|
||||
HttpFirewallRuleGroupId: types.Int64(pieces[1]),
|
||||
Action: pieces[2],
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
|
||||
// 检查是否有数据
|
||||
if len(pbCities) == 0 &&
|
||||
len(pbProviders) == 0 &&
|
||||
len(pbSystems) == 0 &&
|
||||
len(pbBrowsers) == 0 &&
|
||||
len(pbFirewallRuleGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 上传数据
|
||||
_, err = rpcClient.ServerRPC.UploadServerHTTPRequestStat(rpcClient.Context(), &pb.UploadServerHTTPRequestStatRequest{
|
||||
Month: timeutil.Format("Ym"),
|
||||
Day: timeutil.Format("Ymd"),
|
||||
RegionCities: pbCities,
|
||||
RegionProviders: pbProviders,
|
||||
Systems: pbSystems,
|
||||
Browsers: pbBrowsers,
|
||||
HttpFirewallRuleGroups: pbFirewallRuleGroups,
|
||||
})
|
||||
if err != nil {
|
||||
// 是否包含了invalid UTF-8
|
||||
if strings.Contains(err.Error(), "string field contains invalid UTF-8") {
|
||||
for _, system := range pbSystems {
|
||||
system.Name = utils.ToValidUTF8string(system.Name)
|
||||
system.Version = utils.ToValidUTF8string(system.Version)
|
||||
}
|
||||
for _, browser := range pbBrowsers {
|
||||
browser.Name = utils.ToValidUTF8string(browser.Name)
|
||||
browser.Version = utils.ToValidUTF8string(browser.Version)
|
||||
}
|
||||
|
||||
// 再次尝试
|
||||
_, err = rpcClient.ServerRPC.UploadServerHTTPRequestStat(rpcClient.Context(), &pb.UploadServerHTTPRequestStatRequest{
|
||||
Month: timeutil.Format("Ym"),
|
||||
Day: timeutil.Format("Ymd"),
|
||||
RegionCities: pbCities,
|
||||
RegionProviders: pbProviders,
|
||||
Systems: pbSystems,
|
||||
Browsers: pbBrowsers,
|
||||
HttpFirewallRuleGroups: pbFirewallRuleGroups,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
57
EdgeNode/internal/stats/http_request_stat_manager_test.go
Normal file
57
EdgeNode/internal/stats/http_request_stat_manager_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
iplib "github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPRequestStatManager_Loop_Region(t *testing.T) {
|
||||
err := iplib.InitDefault()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var manager = NewHTTPRequestStatManager()
|
||||
manager.AddRemoteAddr(11, "202.196.0.20", 0, false)
|
||||
manager.AddRemoteAddr(11, "202.196.0.20", 0, false) // 重复添加一个测试相加
|
||||
manager.AddRemoteAddr(11, "8.8.8.8", 0, false)
|
||||
|
||||
/**for i := 0; i < 100; i++ {
|
||||
manager.AddRemoteAddr(11, strconv.Itoa(rands.Int(10, 250))+"."+strconv.Itoa(rands.Int(10, 250))+"."+strconv.Itoa(rands.Int(10, 250))+".8")
|
||||
}**/
|
||||
err = manager.Loop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logs.PrintAsJSON(manager.cityMap, t)
|
||||
logs.PrintAsJSON(manager.providerMap, t)
|
||||
|
||||
err = manager.Upload()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ok")
|
||||
}
|
||||
|
||||
func TestHTTPRequestStatManager_Loop_UserAgent(t *testing.T) {
|
||||
var manager = NewHTTPRequestStatManager()
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Macintosh; Intel Mac OS X 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", "")
|
||||
manager.AddUserAgent(1, "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", "")
|
||||
err := manager.Loop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logs.PrintAsJSON(manager.systemMap, t)
|
||||
logs.PrintAsJSON(manager.browserMap, t)
|
||||
|
||||
err = manager.Upload()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ok")
|
||||
}
|
||||
303
EdgeNode/internal/stats/traffic_stat_manager.go
Normal file
303
EdgeNode/internal/stats/traffic_stat_manager.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/events"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/monitor"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedTrafficStatManager = NewTrafficStatManager()
|
||||
|
||||
type TrafficItem struct {
|
||||
UserId int64
|
||||
Bytes int64
|
||||
CachedBytes int64
|
||||
CountRequests int64
|
||||
CountCachedRequests int64
|
||||
CountAttackRequests int64
|
||||
AttackBytes int64
|
||||
PlanId int64
|
||||
CheckingTrafficLimit bool
|
||||
}
|
||||
|
||||
func (this *TrafficItem) Add(anotherItem *TrafficItem) {
|
||||
this.Bytes += anotherItem.Bytes
|
||||
this.CachedBytes += anotherItem.CachedBytes
|
||||
this.CountRequests += anotherItem.CountRequests
|
||||
this.CountCachedRequests += anotherItem.CountCachedRequests
|
||||
this.CountAttackRequests += anotherItem.CountAttackRequests
|
||||
this.AttackBytes += anotherItem.AttackBytes
|
||||
}
|
||||
|
||||
// TrafficStatManager 区域流量统计
|
||||
type TrafficStatManager struct {
|
||||
itemMap map[string]*TrafficItem // [timestamp serverId] => *TrafficItem
|
||||
domainsMap map[int64]map[string]*TrafficItem // serverIde => { timestamp @ domain => *TrafficItem }
|
||||
|
||||
pbItems []*pb.ServerDailyStat
|
||||
pbDomainItems []*pb.UploadServerDailyStatsRequest_DomainStat
|
||||
|
||||
locker sync.Mutex
|
||||
|
||||
totalRequests int64
|
||||
}
|
||||
|
||||
// NewTrafficStatManager 获取新对象
|
||||
func NewTrafficStatManager() *TrafficStatManager {
|
||||
var manager = &TrafficStatManager{
|
||||
itemMap: map[string]*TrafficItem{},
|
||||
domainsMap: map[int64]map[string]*TrafficItem{},
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Start 启动自动任务
|
||||
func (this *TrafficStatManager) Start() {
|
||||
// 上传请求总数
|
||||
var monitorTicker = time.NewTicker(1 * time.Minute)
|
||||
events.OnKey(events.EventQuit, this, func() {
|
||||
monitorTicker.Stop()
|
||||
})
|
||||
goman.New(func() {
|
||||
for range monitorTicker.C {
|
||||
if this.totalRequests > 0 {
|
||||
monitor.SharedValueQueue.Add(nodeconfigs.NodeValueItemRequests, maps.Map{"total": this.totalRequests})
|
||||
this.totalRequests = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 上传统计数据
|
||||
var duration = 5 * time.Minute
|
||||
if Tea.IsTesting() {
|
||||
// 测试环境缩短上传时间,方便我们调试
|
||||
duration = 30 * time.Second
|
||||
}
|
||||
var ticker = time.NewTicker(duration)
|
||||
events.OnKey(events.EventQuit, this, func() {
|
||||
remotelogs.Println("TRAFFIC_STAT_MANAGER", "quit")
|
||||
ticker.Stop()
|
||||
})
|
||||
remotelogs.Println("TRAFFIC_STAT_MANAGER", "start ...")
|
||||
for range ticker.C {
|
||||
err := this.Upload()
|
||||
if err != nil {
|
||||
if !rpc.IsConnError(err) {
|
||||
remotelogs.Error("TRAFFIC_STAT_MANAGER", "upload stats failed: "+err.Error())
|
||||
} else {
|
||||
remotelogs.Warn("TRAFFIC_STAT_MANAGER", "upload stats failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加流量
|
||||
func (this *TrafficStatManager) Add(userId int64, serverId int64, domain string, bytes int64, cachedBytes int64, countRequests int64, countCachedRequests int64, countAttacks int64, attackBytes int64, countWebsocketConnections int64, checkingTrafficLimit bool, planId int64) {
|
||||
if serverId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 添加到带宽
|
||||
SharedBandwidthStatManager.AddTraffic(serverId, cachedBytes, countRequests, countCachedRequests, countAttacks, attackBytes, countWebsocketConnections)
|
||||
|
||||
if bytes == 0 && countRequests == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
this.totalRequests++
|
||||
|
||||
var timestamp = fasttime.Now().UnixFloor(300)
|
||||
var key = strconv.FormatInt(timestamp, 10) + strconv.FormatInt(serverId, 10)
|
||||
this.locker.Lock()
|
||||
|
||||
// 总的流量
|
||||
item, ok := this.itemMap[key]
|
||||
if !ok {
|
||||
item = &TrafficItem{
|
||||
UserId: userId,
|
||||
}
|
||||
this.itemMap[key] = item
|
||||
}
|
||||
item.Bytes += bytes
|
||||
item.CachedBytes += cachedBytes
|
||||
item.CountRequests += countRequests
|
||||
item.CountCachedRequests += countCachedRequests
|
||||
item.CountAttackRequests += countAttacks
|
||||
item.AttackBytes += attackBytes
|
||||
item.CheckingTrafficLimit = checkingTrafficLimit
|
||||
item.PlanId = planId
|
||||
|
||||
// 单个域名流量
|
||||
if len(domain) < 128 {
|
||||
var domainKey = types.String(timestamp) + "@" + domain
|
||||
serverDomainMap, ok := this.domainsMap[serverId]
|
||||
if !ok {
|
||||
serverDomainMap = map[string]*TrafficItem{}
|
||||
this.domainsMap[serverId] = serverDomainMap
|
||||
}
|
||||
|
||||
domainItem, ok := serverDomainMap[domainKey]
|
||||
if !ok {
|
||||
domainItem = &TrafficItem{}
|
||||
serverDomainMap[domainKey] = domainItem
|
||||
}
|
||||
domainItem.Bytes += bytes
|
||||
domainItem.CachedBytes += cachedBytes
|
||||
domainItem.CountRequests += countRequests
|
||||
domainItem.CountCachedRequests += countCachedRequests
|
||||
domainItem.CountAttackRequests += countAttacks
|
||||
domainItem.AttackBytes += attackBytes
|
||||
}
|
||||
|
||||
this.locker.Unlock()
|
||||
}
|
||||
|
||||
// Upload 上传流量
|
||||
func (this *TrafficStatManager) Upload() error {
|
||||
var regionId int64
|
||||
nodeConfig, _ := nodeconfigs.SharedNodeConfig()
|
||||
if nodeConfig != nil {
|
||||
regionId = nodeConfig.RegionId
|
||||
}
|
||||
|
||||
client, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
this.locker.Lock()
|
||||
|
||||
var itemMap = this.itemMap
|
||||
var domainMap = this.domainsMap
|
||||
|
||||
// reset
|
||||
this.itemMap = map[string]*TrafficItem{}
|
||||
this.domainsMap = map[int64]map[string]*TrafficItem{}
|
||||
|
||||
this.locker.Unlock()
|
||||
|
||||
// 服务统计
|
||||
var pbServerStats = []*pb.ServerDailyStat{}
|
||||
for key, item := range itemMap {
|
||||
timestamp, err := strconv.ParseInt(key[:10], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverId, err := strconv.ParseInt(key[10:], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pbServerStats = append(pbServerStats, &pb.ServerDailyStat{
|
||||
UserId: item.UserId,
|
||||
ServerId: serverId,
|
||||
NodeRegionId: regionId,
|
||||
Bytes: item.Bytes,
|
||||
CachedBytes: item.CachedBytes,
|
||||
CountRequests: item.CountRequests,
|
||||
CountCachedRequests: item.CountCachedRequests,
|
||||
CountAttackRequests: item.CountAttackRequests,
|
||||
AttackBytes: item.AttackBytes,
|
||||
CheckTrafficLimiting: item.CheckingTrafficLimit,
|
||||
PlanId: item.PlanId,
|
||||
CreatedAt: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
// 域名统计
|
||||
const maxDomainsPerServer = 20
|
||||
var pbDomainStats = []*pb.UploadServerDailyStatsRequest_DomainStat{}
|
||||
for serverId, serverDomainMap := range domainMap {
|
||||
// 如果超过单个服务最大值,则只取前N个
|
||||
var shouldTrim = len(serverDomainMap) > maxDomainsPerServer
|
||||
var tempItems []*pb.UploadServerDailyStatsRequest_DomainStat
|
||||
|
||||
for key, item := range serverDomainMap {
|
||||
var pieces = strings.SplitN(key, "@", 2)
|
||||
if len(pieces) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 修正数据
|
||||
if item.CachedBytes > item.Bytes || item.CountCachedRequests == item.CountRequests {
|
||||
item.CachedBytes = item.Bytes
|
||||
}
|
||||
|
||||
var pbItem = &pb.UploadServerDailyStatsRequest_DomainStat{
|
||||
ServerId: serverId,
|
||||
Domain: pieces[1],
|
||||
Bytes: item.Bytes,
|
||||
CachedBytes: item.CachedBytes,
|
||||
CountRequests: item.CountRequests,
|
||||
CountCachedRequests: item.CountCachedRequests,
|
||||
CountAttackRequests: item.CountAttackRequests,
|
||||
AttackBytes: item.AttackBytes,
|
||||
CreatedAt: types.Int64(pieces[0]),
|
||||
}
|
||||
if !shouldTrim {
|
||||
pbDomainStats = append(pbDomainStats, pbItem)
|
||||
} else {
|
||||
tempItems = append(tempItems, pbItem)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldTrim {
|
||||
sort.Slice(tempItems, func(i, j int) bool {
|
||||
return tempItems[i].CountRequests > tempItems[j].CountRequests
|
||||
})
|
||||
|
||||
pbDomainStats = append(pbDomainStats, tempItems[:maxDomainsPerServer]...)
|
||||
}
|
||||
}
|
||||
|
||||
// 历史未提交记录
|
||||
if len(this.pbItems) > 0 || len(this.pbDomainItems) > 0 {
|
||||
var expiredAt = time.Now().Unix() - 1200 // 只保留20分钟
|
||||
|
||||
for _, item := range this.pbItems {
|
||||
if item.CreatedAt > expiredAt {
|
||||
pbServerStats = append(pbServerStats, item)
|
||||
}
|
||||
}
|
||||
this.pbItems = nil
|
||||
|
||||
for _, item := range this.pbDomainItems {
|
||||
if item.CreatedAt > expiredAt {
|
||||
pbDomainStats = append(pbDomainStats, item)
|
||||
}
|
||||
}
|
||||
this.pbDomainItems = nil
|
||||
}
|
||||
|
||||
if len(pbServerStats) == 0 && len(pbDomainStats) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = client.ServerDailyStatRPC.UploadServerDailyStats(client.Context(), &pb.UploadServerDailyStatsRequest{
|
||||
Stats: pbServerStats,
|
||||
DomainStats: pbDomainStats,
|
||||
})
|
||||
if err != nil {
|
||||
// 加回历史记录
|
||||
this.pbItems = pbServerStats
|
||||
this.pbDomainItems = pbDomainStats
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
EdgeNode/internal/stats/traffic_stat_manager_test.go
Normal file
42
EdgeNode/internal/stats/traffic_stat_manager_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTrafficStatManager_Add(t *testing.T) {
|
||||
manager := NewTrafficStatManager()
|
||||
for i := 0; i < 100; i++ {
|
||||
manager.Add(1, 1, "goedge.cn", 1, 0, 0, 0, 0, 0, 0, false, 0)
|
||||
}
|
||||
t.Log(manager.itemMap)
|
||||
}
|
||||
|
||||
func TestTrafficStatManager_Upload(t *testing.T) {
|
||||
manager := NewTrafficStatManager()
|
||||
for i := 0; i < 100; i++ {
|
||||
manager.Add(1, 1, "goedge.cn"+types.String(rands.Int(0, 10)), 1, 0, 1, 0, 0, 0, 0, false, 0)
|
||||
}
|
||||
err := manager.Upload()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ok")
|
||||
}
|
||||
|
||||
func BenchmarkTrafficStatManager_Add(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
var manager = NewTrafficStatManager()
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
manager.Add(1, 1, "goedge.cn"+types.String(rand.Int63()%10), 1024, 1, 0, 0, 0, 0, 0, false, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
146
EdgeNode/internal/stats/user_agent_parser.go
Normal file
146
EdgeNode/internal/stats/user_agent_parser.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fnv"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
|
||||
memutils "github.com/TeaOSLab/EdgeNode/internal/utils/mem"
|
||||
syncutils "github.com/TeaOSLab/EdgeNode/internal/utils/sync"
|
||||
"github.com/mssola/useragent"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SharedUserAgentParser = NewUserAgentParser()
|
||||
|
||||
const userAgentShardingCount = 8
|
||||
|
||||
// UserAgentParser UserAgent解析器
|
||||
type UserAgentParser struct {
|
||||
cacheMaps [userAgentShardingCount]map[uint64]UserAgentParserResult
|
||||
pool *sync.Pool
|
||||
mu *syncutils.RWMutex
|
||||
|
||||
maxCacheItems int
|
||||
gcTicker *time.Ticker
|
||||
gcIndex int
|
||||
}
|
||||
|
||||
// NewUserAgentParser 获取新解析器
|
||||
func NewUserAgentParser() *UserAgentParser {
|
||||
var parser = &UserAgentParser{
|
||||
pool: &sync.Pool{
|
||||
New: func() any {
|
||||
return &useragent.UserAgent{}
|
||||
},
|
||||
},
|
||||
cacheMaps: [userAgentShardingCount]map[uint64]UserAgentParserResult{},
|
||||
mu: syncutils.NewRWMutex(userAgentShardingCount),
|
||||
}
|
||||
|
||||
for i := 0; i < userAgentShardingCount; i++ {
|
||||
parser.cacheMaps[i] = map[uint64]UserAgentParserResult{}
|
||||
}
|
||||
|
||||
parser.init()
|
||||
return parser
|
||||
}
|
||||
|
||||
// 初始化
|
||||
func (this *UserAgentParser) init() {
|
||||
var maxCacheItems = 10_000
|
||||
var systemMemory = memutils.SystemMemoryGB()
|
||||
if systemMemory >= 16 {
|
||||
maxCacheItems = 40_000
|
||||
} else if systemMemory >= 8 {
|
||||
maxCacheItems = 30_000
|
||||
} else if systemMemory >= 4 {
|
||||
maxCacheItems = 20_000
|
||||
}
|
||||
this.maxCacheItems = maxCacheItems
|
||||
|
||||
this.gcTicker = time.NewTicker(5 * time.Second)
|
||||
goman.New(func() {
|
||||
for range this.gcTicker.C {
|
||||
this.GC()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Parse 解析UserAgent
|
||||
func (this *UserAgentParser) Parse(userAgent string) (result UserAgentParserResult) {
|
||||
// 限制长度
|
||||
if len(userAgent) == 0 || len(userAgent) > 256 {
|
||||
return
|
||||
}
|
||||
|
||||
var userAgentKey = fnv.HashString(userAgent)
|
||||
var shardingIndex = int(userAgentKey % userAgentShardingCount)
|
||||
|
||||
this.mu.RLock(shardingIndex)
|
||||
cacheResult, ok := this.cacheMaps[shardingIndex][userAgentKey]
|
||||
if ok {
|
||||
this.mu.RUnlock(shardingIndex)
|
||||
return cacheResult
|
||||
}
|
||||
this.mu.RUnlock(shardingIndex)
|
||||
|
||||
var parser = this.pool.Get().(*useragent.UserAgent)
|
||||
parser.Parse(userAgent)
|
||||
result.OS = parser.OSInfo()
|
||||
result.BrowserName, result.BrowserVersion = parser.Browser()
|
||||
result.IsMobile = parser.Mobile()
|
||||
this.pool.Put(parser)
|
||||
|
||||
// 忽略特殊字符
|
||||
if len(result.BrowserName) > 0 {
|
||||
for _, r := range result.BrowserName {
|
||||
if r == '$' || r == '"' || r == '\'' || r == '<' || r == '>' || r == ')' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.mu.Lock(shardingIndex)
|
||||
this.cacheMaps[shardingIndex][userAgentKey] = result
|
||||
this.mu.Unlock(shardingIndex)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MaxCacheItems 读取能容纳的缓存最大数量
|
||||
func (this *UserAgentParser) MaxCacheItems() int {
|
||||
return this.maxCacheItems
|
||||
}
|
||||
|
||||
// Len 读取当前缓存数量
|
||||
func (this *UserAgentParser) Len() int {
|
||||
var total = 0
|
||||
for i := 0; i < userAgentShardingCount; i++ {
|
||||
this.mu.RLock(i)
|
||||
total += len(this.cacheMaps[i])
|
||||
this.mu.RUnlock(i)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// GC 回收多余的缓存
|
||||
func (this *UserAgentParser) GC() {
|
||||
var total = this.Len()
|
||||
if total > this.maxCacheItems {
|
||||
for {
|
||||
var shardingIndex = this.gcIndex
|
||||
|
||||
this.mu.Lock(shardingIndex)
|
||||
total -= len(this.cacheMaps[shardingIndex])
|
||||
this.cacheMaps[shardingIndex] = map[uint64]UserAgentParserResult{}
|
||||
this.gcIndex = (this.gcIndex + 1) % userAgentShardingCount
|
||||
this.mu.Unlock(shardingIndex)
|
||||
|
||||
if total <= this.maxCacheItems {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
EdgeNode/internal/stats/user_agent_parser_result.go
Normal file
14
EdgeNode/internal/stats/user_agent_parser_result.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"github.com/mssola/useragent"
|
||||
)
|
||||
|
||||
type UserAgentParserResult struct {
|
||||
OS useragent.OSInfo
|
||||
BrowserName string
|
||||
BrowserVersion string
|
||||
IsMobile bool
|
||||
}
|
||||
119
EdgeNode/internal/stats/user_agent_parser_test.go
Normal file
119
EdgeNode/internal/stats/user_agent_parser_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package stats_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/stats"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUserAgentParser_Parse(t *testing.T) {
|
||||
var parser = stats.NewUserAgentParser()
|
||||
for i := 0; i < 4; i++ {
|
||||
t.Log(parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/1"))
|
||||
t.Log(parser.Parse("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAgentParser_Parse_Unknown(t *testing.T) {
|
||||
var parser = stats.NewUserAgentParser()
|
||||
t.Log(parser.Parse("Mozilla/5.0 (Wind 10.0; WOW64; rv:49.0) Apple/537.36 (KHTML, like Gecko) Chr/88.0.4324.96 Sa/537.36 Test/1"))
|
||||
t.Log(parser.Parse(""))
|
||||
}
|
||||
|
||||
func TestUserAgentParser_Memory(t *testing.T) {
|
||||
var stat1 = &runtime.MemStats{}
|
||||
runtime.ReadMemStats(stat1)
|
||||
|
||||
var parser = stats.NewUserAgentParser()
|
||||
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/" + types.String(rands.Int(0, 1_000_000)))
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
|
||||
var stat2 = &runtime.MemStats{}
|
||||
runtime.ReadMemStats(stat2)
|
||||
|
||||
t.Log("max cache items:", parser.MaxCacheItems())
|
||||
t.Log("cache:", parser.Len(), "usage:", (stat2.HeapInuse-stat1.HeapInuse)>>20, "MB")
|
||||
}
|
||||
|
||||
func TestNewUserAgentParser_GC(t *testing.T) {
|
||||
if !testutils.IsSingleTesting() {
|
||||
return
|
||||
}
|
||||
|
||||
var parser = stats.NewUserAgentParser()
|
||||
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/" + types.String(rands.Int(0, 100_000)))
|
||||
}
|
||||
|
||||
time.Sleep(60 * time.Second) // wait to gc
|
||||
t.Log(parser.Len(), "cache items")
|
||||
}
|
||||
|
||||
func TestNewUserAgentParser_Mobile(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
var parser = stats.NewUserAgentParser()
|
||||
for _, userAgent := range []string{
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
||||
"Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
} {
|
||||
a.IsTrue(parser.Parse(userAgent).IsMobile)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUserAgentParser_Parse_Many_LimitCPU(b *testing.B) {
|
||||
runtime.GOMAXPROCS(4)
|
||||
|
||||
var parser = stats.NewUserAgentParser()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/" + types.String(rands.Int(0, 1_000_000)))
|
||||
}
|
||||
})
|
||||
b.Log(parser.Len())
|
||||
}
|
||||
|
||||
func BenchmarkUserAgentParser_Parse_Many(b *testing.B) {
|
||||
var parser = stats.NewUserAgentParser()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/" + types.String(rands.Int(0, 1_000_000)))
|
||||
}
|
||||
})
|
||||
b.Log(parser.Len())
|
||||
}
|
||||
|
||||
func BenchmarkUserAgentParser_Parse_Few_LimitCPU(b *testing.B) {
|
||||
runtime.GOMAXPROCS(4)
|
||||
|
||||
var parser = stats.NewUserAgentParser()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/" + types.String(rands.Int(0, 100_000)))
|
||||
}
|
||||
})
|
||||
b.Log(parser.Len())
|
||||
}
|
||||
|
||||
func BenchmarkUserAgentParser_Parse_Few(b *testing.B) {
|
||||
var parser = stats.NewUserAgentParser()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
parser.Parse("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 Test/" + types.String(rands.Int(0, 100_000)))
|
||||
}
|
||||
})
|
||||
b.Log(parser.Len())
|
||||
}
|
||||
Reference in New Issue
Block a user