mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
perf(sub): cache each inbound's settings without clients for subscription bodies (#6758)
Every subscription fetch read and scanned each inbound's whole settings blob, about 25 MB at 100k clients, although subscription bodies resolve their clients from the clients table and never use its clients array. Each inbound's settings minus the clients array are now cached, keyed by inbound and shared by all its subscribers; the inbound row, clients, stats and hosts are still read per fetch, and WireGuard and AmneziaWG keep full settings for their peers. Panel edits purge the cache through an invalidator registry the subscription server releases on stop, with a 10 s TTL as the backstop. At 100k clients a hit takes 2.1 ms on SQLite (72.5 ms before) and 20.7 ms on PostgreSQL (332 ms).
This commit is contained in:
+57
-11
@@ -77,10 +77,13 @@ type SubService struct {
|
||||
// with the clients array left out; generators read only inbound-level
|
||||
// fields (encryption, method, version, …) from it.
|
||||
settingsByInbound map[int]map[string]any
|
||||
hostsByInbound map[int][]*model.Host
|
||||
hostGroupOrders map[string]hostEntryOrder
|
||||
hostsFirst bool
|
||||
sharedGroupSni map[string]string
|
||||
// settingsCache is shared by this service's requests (ForRequest copies the pointer); only
|
||||
// subscription bodies read through it, never panel link rendering.
|
||||
settingsCache *inboundSettingsCache
|
||||
hostsByInbound map[int][]*model.Host
|
||||
hostGroupOrders map[string]hostEntryOrder
|
||||
hostsFirst bool
|
||||
sharedGroupSni map[string]string
|
||||
}
|
||||
|
||||
// NewSubService creates a new subscription service with the given configuration.
|
||||
@@ -90,6 +93,13 @@ func NewSubService(remarkTemplate string) *SubService {
|
||||
}
|
||||
}
|
||||
|
||||
// enableSettingsCache gives the subscription server's service its inbound settings cache, purged
|
||||
// on every panel mutation; release unhooks it when the server stops.
|
||||
func (s *SubService) enableSettingsCache() (release func()) {
|
||||
s.settingsCache = newInboundSettingsCache(settingsCacheTTL)
|
||||
return service.RegisterSubDataInvalidator(s.settingsCache.purgeAll)
|
||||
}
|
||||
|
||||
// ForRequest returns a shallow copy with request-scoped state populated.
|
||||
// Subscription controllers share one base SubService, so request-specific
|
||||
// fields such as address and nodesByID must live on a per-request copy.
|
||||
@@ -725,10 +735,22 @@ func subscriptionExpiryFromClient(nowMs, expiryTime int64) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// getInboundsBySubId loads the subscriber's inbounds; hosts are read per request, never cached.
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
inbounds, err := s.loadInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.loadSubscriptionHosts(inbounds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func (s *SubService) loadInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where(`id in (
|
||||
cached := s.settingsCache != nil && s.subscriptionBody
|
||||
query := db.Model(model.Inbound{}).Where(`id in (
|
||||
SELECT DISTINCT inbounds.id
|
||||
FROM inbounds
|
||||
JOIN client_inbounds ON client_inbounds.inbound_id = inbounds.id
|
||||
@@ -736,17 +758,41 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
|
||||
WHERE
|
||||
inbounds.protocol in ('vmess','vless','trojan','shadowsocks','hysteria','wireguard','amneziawg','mtproto','tuic','masque')
|
||||
AND clients.sub_id = ? AND inbounds.enable = ?
|
||||
)`, subId, true).Order("sub_sort_index ASC").Order("id ASC").Find(&inbounds).Error
|
||||
if err != nil {
|
||||
)`, subId, true).Order("sub_sort_index ASC").Order("id ASC")
|
||||
if cached {
|
||||
query = query.Omit("settings")
|
||||
}
|
||||
var inbounds []*model.Inbound
|
||||
if err := query.Find(&inbounds).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cached {
|
||||
for _, inbound := range inbounds {
|
||||
settings, err := s.subscriptionSettings(inbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbound.Settings = settings
|
||||
}
|
||||
}
|
||||
s.indexStatsBySubId(subId)
|
||||
if err := s.loadSubscriptionHosts(inbounds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
// subscriptionSettings is an inbound's settings for a subscription body: WireGuard and AmneziaWG
|
||||
// keep their peers, every other protocol gets the shared copy without the clients array.
|
||||
func (s *SubService) subscriptionSettings(inbound *model.Inbound) (string, error) {
|
||||
load := func() (string, error) {
|
||||
var settings string
|
||||
err := database.GetDB().Model(&model.Inbound{}).Select("settings").Where("id = ?", inbound.Id).Row().Scan(&settings)
|
||||
return settings, err
|
||||
}
|
||||
if inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG {
|
||||
return load()
|
||||
}
|
||||
return s.settingsCache.settingsFor(inbound.Id, load)
|
||||
}
|
||||
|
||||
// indexStatsBySubId loads the traffic rows for just this subscriber's clients
|
||||
// into statsByEmail so statsForClient can resolve a client's usage on any of
|
||||
// its inbounds. It replaces preloading every matched inbound's ClientStats,
|
||||
|
||||
Reference in New Issue
Block a user