From 88b99c5cd9090ecde825c0f455e67bcbc65aac70 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 12 Mar 2026 22:16:03 +0800 Subject: [PATCH] feat: Add OpenResty performance tuning options and validation - Implemented validation for OpenResty configuration options including worker processes, connections, timeouts, and caching parameters. - Added new validation functions for positive integers, booleans, and specific formats (e.g., size values, proxy buffers, cache levels). - Updated the option management to include OpenResty parameters in the database and ensure they can be modified via the management interface. - Enhanced the settings page to allow users to configure OpenResty parameters with appropriate validation and error handling. - Added unit tests for the new validation logic to ensure correctness. - Updated documentation to reflect the new OpenResty configuration options and their usage. --- VERSION | 2 +- atsf_server/common/constants.go | 31 + atsf_server/controller/option.go | 131 +++ atsf_server/controller/option_test.go | 38 + atsf_server/model/option.go | 131 +++ .../settings/components/settings-page.tsx | 804 +++++++++++++++++- docs/app-config.md | 8 +- 7 files changed, 1137 insertions(+), 8 deletions(-) create mode 100644 atsf_server/controller/option_test.go diff --git a/VERSION b/VERSION index 8a77c33a..0f0e2628 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.x \ No newline at end of file +0.5.x diff --git a/atsf_server/common/constants.go b/atsf_server/common/constants.go index db4bf8bd..1046fcd0 100644 --- a/atsf_server/common/constants.go +++ b/atsf_server/common/constants.go @@ -55,6 +55,37 @@ var AgentHeartbeatInterval = 30000 // milliseconds var AgentSyncInterval = 30000 // milliseconds var AgentUpdateRepo = "Rain-kl/ATSFlare" +// V5 OpenResty performance settings (hot-reloadable via Option table) +var OpenRestyWorkerProcesses = "auto" +var OpenRestyWorkerConnections = 4096 +var OpenRestyWorkerRlimitNofile = 65535 +var OpenRestyEventsUse = "" +var OpenRestyEventsMultiAcceptEnabled = false +var OpenRestyKeepaliveTimeout = 65 +var OpenRestyKeepaliveRequests = 1000 +var OpenRestyClientHeaderTimeout = 15 +var OpenRestyClientBodyTimeout = 15 +var OpenRestySendTimeout = 30 +var OpenRestyProxyConnectTimeout = 5 +var OpenRestyProxySendTimeout = 60 +var OpenRestyProxyReadTimeout = 60 +var OpenRestyProxyBufferingEnabled = true +var OpenRestyProxyBuffers = "16 16k" +var OpenRestyProxyBufferSize = "8k" +var OpenRestyProxyBusyBuffersSize = "64k" +var OpenRestyGzipEnabled = true +var OpenRestyGzipMinLength = 1024 +var OpenRestyGzipCompLevel = 5 +var OpenRestyCacheEnabled = false +var OpenRestyCachePath = "" +var OpenRestyCacheLevels = "1:2" +var OpenRestyCacheInactive = "30m" +var OpenRestyCacheMaxSize = "1g" +var OpenRestyCacheKeyTemplate = "$scheme$proxy_host$request_uri" +var OpenRestyCacheLockEnabled = true +var OpenRestyCacheLockTimeout = "5s" +var OpenRestyCacheUseStale = "error timeout updating http_500 http_502 http_503 http_504" + const ( RoleGuestUser = 0 RoleCommonUser = 1 diff --git a/atsf_server/controller/option.go b/atsf_server/controller/option.go index 1d7638b7..6e493d1b 100644 --- a/atsf_server/controller/option.go +++ b/atsf_server/controller/option.go @@ -7,10 +7,18 @@ import ( "fmt" "github.com/gin-gonic/gin" "net/http" + "regexp" "strconv" "strings" ) +var ( + openRestySizePattern = regexp.MustCompile(`^\d+[kKmMgG]?$`) + openRestyProxyBuffersPattern = regexp.MustCompile(`^\d+\s+\d+[kKmMgG]?$`) + openRestyCacheLevelsPattern = regexp.MustCompile(`^\d{1,2}(?::\d{1,2}){0,2}$`) + openRestyDurationTokenPattern = regexp.MustCompile(`^\d+[smhdwSMHDW]$`) +) + func validateRateLimitOption(key string, value string) error { maxDurationSeconds := int(common.RateLimitKeyExpirationDuration.Seconds()) @@ -35,6 +43,122 @@ func validateRateLimitOption(key string, value string) error { } } +func validatePositiveIntegerOption(key string, value string) error { + intValue, err := strconv.Atoi(value) + if err != nil || intValue <= 0 { + return fmt.Errorf("%s 必须为大于 0 的整数", key) + } + return nil +} + +func validateBooleanOption(key string, value string) error { + switch value { + case "true", "false": + return nil + default: + return fmt.Errorf("%s 必须为 true 或 false", key) + } +} + +func validateOpenRestyOption(key string, value string) error { + trimmed := strings.TrimSpace(value) + + switch key { + case "OpenRestyWorkerProcesses": + if trimmed == "auto" { + return nil + } + return validatePositiveIntegerOption(key, trimmed) + case "OpenRestyWorkerConnections", + "OpenRestyWorkerRlimitNofile", + "OpenRestyKeepaliveTimeout", + "OpenRestyKeepaliveRequests", + "OpenRestyClientHeaderTimeout", + "OpenRestyClientBodyTimeout", + "OpenRestySendTimeout", + "OpenRestyProxyConnectTimeout", + "OpenRestyProxySendTimeout", + "OpenRestyProxyReadTimeout", + "OpenRestyGzipMinLength": + return validatePositiveIntegerOption(key, trimmed) + case "OpenRestyGzipCompLevel": + if err := validatePositiveIntegerOption(key, trimmed); err != nil { + return err + } + level, _ := strconv.Atoi(trimmed) + if level > 9 { + return fmt.Errorf("%s 不能大于 9", key) + } + return nil + case "OpenRestyEventsUse": + if trimmed == "" { + return nil + } + switch trimmed { + case "epoll", "kqueue", "poll", "select", "rtsig", "/dev/poll", "eventport": + return nil + default: + return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key) + } + case "OpenRestyEventsMultiAcceptEnabled", + "OpenRestyProxyBufferingEnabled", + "OpenRestyGzipEnabled", + "OpenRestyCacheEnabled", + "OpenRestyCacheLockEnabled": + return validateBooleanOption(key, trimmed) + case "OpenRestyProxyBuffers": + if openRestyProxyBuffersPattern.MatchString(trimmed) { + return nil + } + return fmt.Errorf("%s 格式必须类似 \"16 16k\"", key) + case "OpenRestyProxyBufferSize", "OpenRestyProxyBusyBuffersSize", "OpenRestyCacheMaxSize": + if openRestySizePattern.MatchString(trimmed) { + return nil + } + return fmt.Errorf("%s 格式必须为整数或带 k/m/g 单位的大小值", key) + case "OpenRestyCachePath": + if strings.ContainsAny(trimmed, "\r\n\t") { + return fmt.Errorf("%s 不能包含换行或制表符", key) + } + return nil + case "OpenRestyCacheLevels": + if openRestyCacheLevelsPattern.MatchString(trimmed) { + return nil + } + return fmt.Errorf("%s 格式必须类似 \"1:2\" 或 \"1:2:2\"", key) + case "OpenRestyCacheInactive", "OpenRestyCacheLockTimeout": + if openRestyDurationTokenPattern.MatchString(trimmed) { + return nil + } + return fmt.Errorf("%s 格式必须为带单位的时长,例如 30m 或 5s", key) + case "OpenRestyCacheKeyTemplate": + if trimmed == "" { + return fmt.Errorf("%s 不能为空", key) + } + if strings.ContainsAny(trimmed, "\r\n") { + return fmt.Errorf("%s 不能包含换行", key) + } + return nil + case "OpenRestyCacheUseStale": + if trimmed == "" { + return fmt.Errorf("%s 不能为空", key) + } + allowedTokens := map[string]struct{}{ + "error": {}, "timeout": {}, "invalid_header": {}, "updating": {}, + "http_500": {}, "http_502": {}, "http_503": {}, "http_504": {}, + "http_403": {}, "http_404": {}, "http_429": {}, "off": {}, + } + for _, token := range strings.Fields(trimmed) { + if _, ok := allowedTokens[token]; !ok { + return fmt.Errorf("%s 包含不支持的值 %q", key, token) + } + } + return nil + default: + return nil + } +} + // GetOptions godoc // @Summary List editable options // @Tags Options @@ -114,6 +238,13 @@ func UpdateOption(c *gin.Context) { }) return } + if err = validateOpenRestyOption(option.Key, option.Value); err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } err = model.UpdateOption(option.Key, option.Value) if err != nil { c.JSON(http.StatusOK, gin.H{ diff --git a/atsf_server/controller/option_test.go b/atsf_server/controller/option_test.go new file mode 100644 index 00000000..8e3ca522 --- /dev/null +++ b/atsf_server/controller/option_test.go @@ -0,0 +1,38 @@ +package controller + +import "testing" + +func TestValidateOpenRestyOption(t *testing.T) { + testCases := []struct { + name string + key string + value string + wantErr bool + }{ + {name: "worker processes auto", key: "OpenRestyWorkerProcesses", value: "auto"}, + {name: "worker processes number", key: "OpenRestyWorkerProcesses", value: "8"}, + {name: "worker processes invalid", key: "OpenRestyWorkerProcesses", value: "0", wantErr: true}, + {name: "events use empty", key: "OpenRestyEventsUse", value: ""}, + {name: "events use invalid", key: "OpenRestyEventsUse", value: "io_uring", wantErr: true}, + {name: "proxy buffers valid", key: "OpenRestyProxyBuffers", value: "16 16k"}, + {name: "proxy buffers invalid", key: "OpenRestyProxyBuffers", value: "16x16k", wantErr: true}, + {name: "cache max size valid", key: "OpenRestyCacheMaxSize", value: "2g"}, + {name: "cache max size invalid", key: "OpenRestyCacheMaxSize", value: "2gb", wantErr: true}, + {name: "cache inactive valid", key: "OpenRestyCacheInactive", value: "30m"}, + {name: "cache inactive invalid", key: "OpenRestyCacheInactive", value: "30", wantErr: true}, + {name: "cache use stale valid", key: "OpenRestyCacheUseStale", value: "error timeout http_500"}, + {name: "cache use stale invalid", key: "OpenRestyCacheUseStale", value: "error whatever", wantErr: true}, + {name: "gzip level valid", key: "OpenRestyGzipCompLevel", value: "9"}, + {name: "gzip level invalid", key: "OpenRestyGzipCompLevel", value: "10", wantErr: true}, + } + + for _, testCase := range testCases { + err := validateOpenRestyOption(testCase.key, testCase.value) + if testCase.wantErr && err == nil { + t.Fatalf("%s: expected error", testCase.name) + } + if !testCase.wantErr && err != nil { + t.Fatalf("%s: unexpected error: %v", testCase.name, err) + } + } +} diff --git a/atsf_server/model/option.go b/atsf_server/model/option.go index aa21c5e4..021f2f1c 100644 --- a/atsf_server/model/option.go +++ b/atsf_server/model/option.go @@ -55,6 +55,35 @@ func InitOptionMap() { common.OptionMap["AgentSyncInterval"] = strconv.Itoa(common.AgentSyncInterval) common.OptionMap["NodeOfflineThreshold"] = strconv.Itoa(int(common.NodeOfflineThreshold.Milliseconds())) common.OptionMap["AgentUpdateRepo"] = common.AgentUpdateRepo + common.OptionMap["OpenRestyWorkerProcesses"] = common.OpenRestyWorkerProcesses + common.OptionMap["OpenRestyWorkerConnections"] = strconv.Itoa(common.OpenRestyWorkerConnections) + common.OptionMap["OpenRestyWorkerRlimitNofile"] = strconv.Itoa(common.OpenRestyWorkerRlimitNofile) + common.OptionMap["OpenRestyEventsUse"] = common.OpenRestyEventsUse + common.OptionMap["OpenRestyEventsMultiAcceptEnabled"] = strconv.FormatBool(common.OpenRestyEventsMultiAcceptEnabled) + common.OptionMap["OpenRestyKeepaliveTimeout"] = strconv.Itoa(common.OpenRestyKeepaliveTimeout) + common.OptionMap["OpenRestyKeepaliveRequests"] = strconv.Itoa(common.OpenRestyKeepaliveRequests) + common.OptionMap["OpenRestyClientHeaderTimeout"] = strconv.Itoa(common.OpenRestyClientHeaderTimeout) + common.OptionMap["OpenRestyClientBodyTimeout"] = strconv.Itoa(common.OpenRestyClientBodyTimeout) + common.OptionMap["OpenRestySendTimeout"] = strconv.Itoa(common.OpenRestySendTimeout) + common.OptionMap["OpenRestyProxyConnectTimeout"] = strconv.Itoa(common.OpenRestyProxyConnectTimeout) + common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout) + common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout) + common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled) + common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers + common.OptionMap["OpenRestyProxyBufferSize"] = common.OpenRestyProxyBufferSize + common.OptionMap["OpenRestyProxyBusyBuffersSize"] = common.OpenRestyProxyBusyBuffersSize + common.OptionMap["OpenRestyGzipEnabled"] = strconv.FormatBool(common.OpenRestyGzipEnabled) + common.OptionMap["OpenRestyGzipMinLength"] = strconv.Itoa(common.OpenRestyGzipMinLength) + common.OptionMap["OpenRestyGzipCompLevel"] = strconv.Itoa(common.OpenRestyGzipCompLevel) + common.OptionMap["OpenRestyCacheEnabled"] = strconv.FormatBool(common.OpenRestyCacheEnabled) + common.OptionMap["OpenRestyCachePath"] = common.OpenRestyCachePath + common.OptionMap["OpenRestyCacheLevels"] = common.OpenRestyCacheLevels + common.OptionMap["OpenRestyCacheInactive"] = common.OpenRestyCacheInactive + common.OptionMap["OpenRestyCacheMaxSize"] = common.OpenRestyCacheMaxSize + common.OptionMap["OpenRestyCacheKeyTemplate"] = common.OpenRestyCacheKeyTemplate + common.OptionMap["OpenRestyCacheLockEnabled"] = strconv.FormatBool(common.OpenRestyCacheLockEnabled) + common.OptionMap["OpenRestyCacheLockTimeout"] = common.OpenRestyCacheLockTimeout + common.OptionMap["OpenRestyCacheUseStale"] = common.OpenRestyCacheUseStale common.OptionMap["GlobalApiRateLimitNum"] = strconv.Itoa(common.GlobalApiRateLimitNum) common.OptionMap["GlobalApiRateLimitDuration"] = strconv.FormatInt(common.GlobalApiRateLimitDuration, 10) common.OptionMap["GlobalWebRateLimitNum"] = strconv.Itoa(common.GlobalWebRateLimitNum) @@ -175,6 +204,108 @@ func updateOptionMap(key string, value string) { if value != "" { common.AgentUpdateRepo = value } + case "OpenRestyWorkerProcesses": + if strings.TrimSpace(value) != "" { + common.OpenRestyWorkerProcesses = value + } + case "OpenRestyWorkerConnections": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyWorkerConnections = v + } + case "OpenRestyWorkerRlimitNofile": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyWorkerRlimitNofile = v + } + case "OpenRestyEventsUse": + common.OpenRestyEventsUse = value + case "OpenRestyEventsMultiAcceptEnabled": + common.OpenRestyEventsMultiAcceptEnabled = value == "true" + case "OpenRestyKeepaliveTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyKeepaliveTimeout = v + } + case "OpenRestyKeepaliveRequests": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyKeepaliveRequests = v + } + case "OpenRestyClientHeaderTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyClientHeaderTimeout = v + } + case "OpenRestyClientBodyTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyClientBodyTimeout = v + } + case "OpenRestySendTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestySendTimeout = v + } + case "OpenRestyProxyConnectTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyProxyConnectTimeout = v + } + case "OpenRestyProxySendTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyProxySendTimeout = v + } + case "OpenRestyProxyReadTimeout": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyProxyReadTimeout = v + } + case "OpenRestyProxyBufferingEnabled": + common.OpenRestyProxyBufferingEnabled = value == "true" + case "OpenRestyProxyBuffers": + if strings.TrimSpace(value) != "" { + common.OpenRestyProxyBuffers = value + } + case "OpenRestyProxyBufferSize": + if strings.TrimSpace(value) != "" { + common.OpenRestyProxyBufferSize = value + } + case "OpenRestyProxyBusyBuffersSize": + if strings.TrimSpace(value) != "" { + common.OpenRestyProxyBusyBuffersSize = value + } + case "OpenRestyGzipEnabled": + common.OpenRestyGzipEnabled = value == "true" + case "OpenRestyGzipMinLength": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyGzipMinLength = v + } + case "OpenRestyGzipCompLevel": + if v, err := strconv.Atoi(value); err == nil && v > 0 { + common.OpenRestyGzipCompLevel = v + } + case "OpenRestyCacheEnabled": + common.OpenRestyCacheEnabled = value == "true" + case "OpenRestyCachePath": + common.OpenRestyCachePath = value + case "OpenRestyCacheLevels": + if strings.TrimSpace(value) != "" { + common.OpenRestyCacheLevels = value + } + case "OpenRestyCacheInactive": + if strings.TrimSpace(value) != "" { + common.OpenRestyCacheInactive = value + } + case "OpenRestyCacheMaxSize": + if strings.TrimSpace(value) != "" { + common.OpenRestyCacheMaxSize = value + } + case "OpenRestyCacheKeyTemplate": + if strings.TrimSpace(value) != "" { + common.OpenRestyCacheKeyTemplate = value + } + case "OpenRestyCacheLockEnabled": + common.OpenRestyCacheLockEnabled = value == "true" + case "OpenRestyCacheLockTimeout": + if strings.TrimSpace(value) != "" { + common.OpenRestyCacheLockTimeout = value + } + case "OpenRestyCacheUseStale": + if strings.TrimSpace(value) != "" { + common.OpenRestyCacheUseStale = value + } case "GlobalApiRateLimitNum": if v, err := strconv.Atoi(value); err == nil && v > 0 { common.GlobalApiRateLimitNum = v diff --git a/atsf_server/web/features/settings/components/settings-page.tsx b/atsf_server/web/features/settings/components/settings-page.tsx index 5eaf9183..ecda4999 100644 --- a/atsf_server/web/features/settings/components/settings-page.tsx +++ b/atsf_server/web/features/settings/components/settings-page.tsx @@ -35,6 +35,7 @@ import { PrimaryButton, ResourceField, ResourceInput, + ResourceSelect, ResourceTextarea, SecondaryButton, ToggleField, @@ -72,6 +73,36 @@ const defaultOperationFields = { AgentSyncInterval: '30000', NodeOfflineThreshold: '120000', AgentUpdateRepo: 'Rain-kl/ATSFlare', + OpenRestyWorkerProcesses: 'auto', + OpenRestyWorkerConnections: '4096', + OpenRestyWorkerRlimitNofile: '65535', + OpenRestyEventsUse: '', + OpenRestyEventsMultiAcceptEnabled: false, + OpenRestyKeepaliveTimeout: '65', + OpenRestyKeepaliveRequests: '1000', + OpenRestyClientHeaderTimeout: '15', + OpenRestyClientBodyTimeout: '15', + OpenRestySendTimeout: '30', + OpenRestyProxyConnectTimeout: '5', + OpenRestyProxySendTimeout: '60', + OpenRestyProxyReadTimeout: '60', + OpenRestyProxyBufferingEnabled: true, + OpenRestyProxyBuffers: '16 16k', + OpenRestyProxyBufferSize: '8k', + OpenRestyProxyBusyBuffersSize: '64k', + OpenRestyGzipEnabled: true, + OpenRestyGzipMinLength: '1024', + OpenRestyGzipCompLevel: '5', + OpenRestyCacheEnabled: false, + OpenRestyCachePath: '', + OpenRestyCacheLevels: '1:2', + OpenRestyCacheInactive: '30m', + OpenRestyCacheMaxSize: '1g', + OpenRestyCacheKeyTemplate: '$scheme$proxy_host$request_uri', + OpenRestyCacheLockEnabled: true, + OpenRestyCacheLockTimeout: '5s', + OpenRestyCacheUseStale: + 'error timeout updating http_500 http_502 http_503 http_504', GlobalApiRateLimitNum: '300', GlobalApiRateLimitDuration: '180', GlobalWebRateLimitNum: '300', @@ -170,6 +201,27 @@ function formatSecondsLabel(value: string) { return `${seconds} 秒`; } +function isPositiveInteger(value: string) { + const parsed = Number.parseInt(value, 10); + return !Number.isNaN(parsed) && parsed > 0; +} + +function isSizeValue(value: string) { + return /^\d+[kKmMgG]?$/.test(value.trim()); +} + +function isProxyBuffersValue(value: string) { + return /^\d+\s+\d+[kKmMgG]?$/.test(value.trim()); +} + +function isDurationToken(value: string) { + return /^\d+[smhdwSMHDW]$/.test(value.trim()); +} + +function isCacheLevelsValue(value: string) { + return /^\d{1,2}(?::\d{1,2}){0,2}$/.test(value.trim()); +} + function buildDiscoveryCommand(serverUrl: string, discoveryToken: string) { return [ `curl -fsSL ${installerScriptUrl} | bash -s -- \\`, @@ -309,6 +361,53 @@ export function SettingsPage() { AgentSyncInterval: optionMap.AgentSyncInterval ?? '30000', NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000', AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare', + OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto', + OpenRestyWorkerConnections: + optionMap.OpenRestyWorkerConnections ?? '4096', + OpenRestyWorkerRlimitNofile: + optionMap.OpenRestyWorkerRlimitNofile ?? '65535', + OpenRestyEventsUse: optionMap.OpenRestyEventsUse ?? '', + OpenRestyEventsMultiAcceptEnabled: toBoolean( + optionMap.OpenRestyEventsMultiAcceptEnabled, + false, + ), + OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '65', + OpenRestyKeepaliveRequests: + optionMap.OpenRestyKeepaliveRequests ?? '1000', + OpenRestyClientHeaderTimeout: + optionMap.OpenRestyClientHeaderTimeout ?? '15', + OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15', + OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30', + OpenRestyProxyConnectTimeout: + optionMap.OpenRestyProxyConnectTimeout ?? '5', + OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60', + OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60', + OpenRestyProxyBufferingEnabled: toBoolean( + optionMap.OpenRestyProxyBufferingEnabled, + true, + ), + OpenRestyProxyBuffers: optionMap.OpenRestyProxyBuffers ?? '16 16k', + OpenRestyProxyBufferSize: optionMap.OpenRestyProxyBufferSize ?? '8k', + OpenRestyProxyBusyBuffersSize: + optionMap.OpenRestyProxyBusyBuffersSize ?? '64k', + OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true), + OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? '1024', + OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? '5', + OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false), + OpenRestyCachePath: optionMap.OpenRestyCachePath ?? '', + OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? '1:2', + OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m', + OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g', + OpenRestyCacheKeyTemplate: + optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$proxy_host$request_uri', + OpenRestyCacheLockEnabled: toBoolean( + optionMap.OpenRestyCacheLockEnabled, + true, + ), + OpenRestyCacheLockTimeout: optionMap.OpenRestyCacheLockTimeout ?? '5s', + OpenRestyCacheUseStale: + optionMap.OpenRestyCacheUseStale ?? + 'error timeout updating http_500 http_502 http_503 http_504', GlobalApiRateLimitNum: optionMap.GlobalApiRateLimitNum ?? '300', GlobalApiRateLimitDuration: optionMap.GlobalApiRateLimitDuration ?? '180', GlobalWebRateLimitNum: optionMap.GlobalWebRateLimitNum ?? '300', @@ -987,6 +1086,249 @@ export function SettingsPage() { /> + + void runBusyAction( + 'operation-openresty-runtime', + async () => { + if ( + operationFields.OpenRestyWorkerProcesses !== 'auto' && + !isPositiveInteger( + operationFields.OpenRestyWorkerProcesses, + ) + ) { + throw new Error( + 'worker_processes 必须为 auto 或大于 0 的整数。', + ); + } + + const integerFields = [ + [ + 'worker_connections', + operationFields.OpenRestyWorkerConnections, + ], + [ + 'worker_rlimit_nofile', + operationFields.OpenRestyWorkerRlimitNofile, + ], + [ + 'keepalive_timeout', + operationFields.OpenRestyKeepaliveTimeout, + ], + [ + 'keepalive_requests', + operationFields.OpenRestyKeepaliveRequests, + ], + [ + 'client_header_timeout', + operationFields.OpenRestyClientHeaderTimeout, + ], + [ + 'client_body_timeout', + operationFields.OpenRestyClientBodyTimeout, + ], + [ + 'send_timeout', + operationFields.OpenRestySendTimeout, + ], + ] as const; + + for (const [label, value] of integerFields) { + if (!isPositiveInteger(value)) { + throw new Error(`${label} 必须为大于 0 的整数。`); + } + } + + await saveOptionEntries( + [ + [ + 'OpenRestyWorkerProcesses', + operationFields.OpenRestyWorkerProcesses.trim(), + ], + [ + 'OpenRestyWorkerConnections', + operationFields.OpenRestyWorkerConnections.trim(), + ], + [ + 'OpenRestyWorkerRlimitNofile', + operationFields.OpenRestyWorkerRlimitNofile.trim(), + ], + [ + 'OpenRestyEventsUse', + operationFields.OpenRestyEventsUse.trim(), + ], + [ + 'OpenRestyEventsMultiAcceptEnabled', + String( + operationFields.OpenRestyEventsMultiAcceptEnabled, + ), + ], + [ + 'OpenRestyKeepaliveTimeout', + operationFields.OpenRestyKeepaliveTimeout.trim(), + ], + [ + 'OpenRestyKeepaliveRequests', + operationFields.OpenRestyKeepaliveRequests.trim(), + ], + [ + 'OpenRestyClientHeaderTimeout', + operationFields.OpenRestyClientHeaderTimeout.trim(), + ], + [ + 'OpenRestyClientBodyTimeout', + operationFields.OpenRestyClientBodyTimeout.trim(), + ], + [ + 'OpenRestySendTimeout', + operationFields.OpenRestySendTimeout.trim(), + ], + ], + 'OpenResty 运行参数已保存。', + ); + }, + ) + } + disabled={busyKey === 'operation-openresty-runtime'} + > + {busyKey === 'operation-openresty-runtime' + ? '保存中...' + : '保存运行调优'} + + } + > +
+ + + setOperationFields((previous) => ({ + ...previous, + OpenRestyWorkerProcesses: event.target.value, + })) + } + placeholder="auto" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyWorkerConnections: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyWorkerRlimitNofile: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyEventsUse: event.target.value, + })) + } + > + + + + + + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyEventsMultiAcceptEnabled: checked, + })) + } + /> + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyKeepaliveTimeout: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyKeepaliveRequests: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyClientHeaderTimeout: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyClientBodyTimeout: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestySendTimeout: event.target.value, + })) + } + /> + +
+
-

+

Discovery Token

-

+

{discoveryToken || '未生成'}

@@ -1433,6 +1775,462 @@ export function SettingsPage() {
+
+ + void runBusyAction( + 'operation-openresty-proxy', + async () => { + const timeoutFields = [ + operationFields.OpenRestyProxyConnectTimeout, + operationFields.OpenRestyProxySendTimeout, + operationFields.OpenRestyProxyReadTimeout, + ]; + + for (const value of timeoutFields) { + if (!isPositiveInteger(value)) { + throw new Error( + '代理超时参数必须为大于 0 的整数秒。', + ); + } + } + if ( + !isProxyBuffersValue( + operationFields.OpenRestyProxyBuffers, + ) + ) { + throw new Error( + 'proxy_buffers 格式必须类似 "16 16k"。', + ); + } + if ( + !isSizeValue( + operationFields.OpenRestyProxyBufferSize, + ) || + !isSizeValue( + operationFields.OpenRestyProxyBusyBuffersSize, + ) + ) { + throw new Error( + '缓冲大小必须为整数或带 k/m/g 单位的值。', + ); + } + + await saveOptionEntries( + [ + [ + 'OpenRestyProxyConnectTimeout', + operationFields.OpenRestyProxyConnectTimeout.trim(), + ], + [ + 'OpenRestyProxySendTimeout', + operationFields.OpenRestyProxySendTimeout.trim(), + ], + [ + 'OpenRestyProxyReadTimeout', + operationFields.OpenRestyProxyReadTimeout.trim(), + ], + [ + 'OpenRestyProxyBufferingEnabled', + String( + operationFields.OpenRestyProxyBufferingEnabled, + ), + ], + [ + 'OpenRestyProxyBuffers', + operationFields.OpenRestyProxyBuffers.trim(), + ], + [ + 'OpenRestyProxyBufferSize', + operationFields.OpenRestyProxyBufferSize.trim(), + ], + [ + 'OpenRestyProxyBusyBuffersSize', + operationFields.OpenRestyProxyBusyBuffersSize.trim(), + ], + ], + 'OpenResty 反代缓冲参数已保存。', + ); + }, + ) + } + disabled={busyKey === 'operation-openresty-proxy'} + > + {busyKey === 'operation-openresty-proxy' + ? '保存中...' + : '保存代理调优'} + + } + > +
+ + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxyConnectTimeout: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxySendTimeout: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxyReadTimeout: event.target.value, + })) + } + /> + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxyBufferingEnabled: checked, + })) + } + /> + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxyBuffers: event.target.value, + })) + } + placeholder="16 16k" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxyBufferSize: event.target.value, + })) + } + placeholder="8k" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyProxyBusyBuffersSize: event.target.value, + })) + } + placeholder="64k" + /> + +
+
+ + void runBusyAction( + 'operation-openresty-cache', + async () => { + if ( + !isPositiveInteger( + operationFields.OpenRestyGzipMinLength, + ) + ) { + throw new Error( + 'gzip_min_length 必须为大于 0 的整数。', + ); + } + const gzipLevel = Number.parseInt( + operationFields.OpenRestyGzipCompLevel, + 10, + ); + if ( + Number.isNaN(gzipLevel) || + gzipLevel < 1 || + gzipLevel > 9 + ) { + throw new Error( + 'gzip_comp_level 必须在 1 到 9 之间。', + ); + } + if (operationFields.OpenRestyCacheEnabled) { + if (!operationFields.OpenRestyCachePath.trim()) { + throw new Error( + '启用缓存时必须填写 proxy_cache_path 目录。', + ); + } + if ( + !isCacheLevelsValue( + operationFields.OpenRestyCacheLevels, + ) || + !isDurationToken( + operationFields.OpenRestyCacheInactive, + ) || + !isSizeValue( + operationFields.OpenRestyCacheMaxSize, + ) || + !isDurationToken( + operationFields.OpenRestyCacheLockTimeout, + ) + ) { + throw new Error( + '缓存 levels、inactive、max_size 或 lock_timeout 格式不合法。', + ); + } + if ( + !operationFields.OpenRestyCacheKeyTemplate.trim() + ) { + throw new Error( + '启用缓存时必须填写缓存 Key 模板。', + ); + } + } + + await saveOptionEntries( + [ + [ + 'OpenRestyGzipEnabled', + String(operationFields.OpenRestyGzipEnabled), + ], + [ + 'OpenRestyGzipMinLength', + operationFields.OpenRestyGzipMinLength.trim(), + ], + [ + 'OpenRestyGzipCompLevel', + operationFields.OpenRestyGzipCompLevel.trim(), + ], + [ + 'OpenRestyCachePath', + operationFields.OpenRestyCachePath.trim(), + ], + [ + 'OpenRestyCacheLevels', + operationFields.OpenRestyCacheLevels.trim(), + ], + [ + 'OpenRestyCacheInactive', + operationFields.OpenRestyCacheInactive.trim(), + ], + [ + 'OpenRestyCacheMaxSize', + operationFields.OpenRestyCacheMaxSize.trim(), + ], + [ + 'OpenRestyCacheKeyTemplate', + operationFields.OpenRestyCacheKeyTemplate.trim(), + ], + [ + 'OpenRestyCacheLockEnabled', + String(operationFields.OpenRestyCacheLockEnabled), + ], + [ + 'OpenRestyCacheLockTimeout', + operationFields.OpenRestyCacheLockTimeout.trim(), + ], + [ + 'OpenRestyCacheUseStale', + operationFields.OpenRestyCacheUseStale.trim(), + ], + [ + 'OpenRestyCacheEnabled', + String(operationFields.OpenRestyCacheEnabled), + ], + ], + 'OpenResty 压缩与缓存参数已保存。', + ); + }, + ) + } + disabled={busyKey === 'operation-openresty-cache'} + > + {busyKey === 'operation-openresty-cache' + ? '保存中...' + : '保存压缩与缓存'} + + } + > +
+
+ + setOperationFields((previous) => ({ + ...previous, + OpenRestyGzipEnabled: checked, + })) + } + /> + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheEnabled: checked, + })) + } + /> + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyGzipMinLength: event.target.value, + })) + } + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyGzipCompLevel: event.target.value, + })) + } + /> + +
+
+ + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCachePath: event.target.value, + })) + } + placeholder="/var/cache/openresty/atsflare" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheLevels: event.target.value, + })) + } + placeholder="1:2" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheInactive: event.target.value, + })) + } + placeholder="30m" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheMaxSize: event.target.value, + })) + } + placeholder="1g" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheKeyTemplate: event.target.value, + })) + } + /> + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheLockEnabled: checked, + })) + } + /> + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheLockTimeout: event.target.value, + })) + } + placeholder="5s" + /> + + + + setOperationFields((previous) => ({ + ...previous, + OpenRestyCacheUseStale: event.target.value, + })) + } + /> + +
+
+
+
{feedback ? ( diff --git a/docs/app-config.md b/docs/app-config.md index 76cf7058..bf456b74 100644 --- a/docs/app-config.md +++ b/docs/app-config.md @@ -13,7 +13,7 @@ Server 当前支持两类启动配置: 此外,部分运行时参数已迁入数据库 `Option` 表,可在管理端设置页中热更新,例如 Agent 运行参数与限流阈值。 -第五版(0.5.x)计划继续复用 `Option` 表承载 OpenResty 性能优化参数与缓存参数,不单独引入新的配置中心。 +第五版(0.5.x)继续复用 `Option` 表承载 OpenResty 性能优化参数与缓存参数,不单独引入新的配置中心。 ### 1.1 Server 命令行参数 @@ -119,9 +119,9 @@ volumes: * 限流窗口上限不能超过 `RateLimitKeyExpirationDuration`,当前为 20 分钟 * 限流按来源 IP 统计,若前置了 Nginx/CDN/LB,应正确透传真实客户端 IP -### 1.2.2 第五版规划中的 OpenResty 优化配置项 +### 1.2.2 第五版当前支持的 OpenResty 优化配置项 -以下配置项用于第五版开发文档定义,目标是在管理端「运维设置」中统一维护,并参与版本渲染。除特别说明外,均计划保存在 `Option` 表中。 +以下配置项当前已接入管理端「运维设置」,并统一保存在 `Option` 表中。它们会参与第五版后续的配置渲染与发布链路;当前阶段优先完成参数录入、默认值和校验能力。 | 配置项 | 作用 | 计划默认值 | | --- | --- | --- | @@ -157,7 +157,7 @@ volumes: 说明: -* 第五版第一批仅开放稳定、可校验、可回滚的常用性能项;更多指令后续按相同模式扩展 +* 第五版第一批当前仅开放稳定、可校验、可回滚的常用性能项;更多指令后续按相同模式扩展 * 所有大小、时长、布尔和整数参数都应在 Server 保存前完成校验 * `OpenRestyCacheEnabled=false` 时,缓存目录与缓存参数应允许留空或回退到默认值 * 任何包含路径的配置项都必须在 Agent 落盘前再次校验可写性与安全边界