mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat(nginx): performance optimization #850
This commit is contained in:
parent
b59da3e7e8
commit
9d4070a211
29 changed files with 4250 additions and 2031 deletions
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
description: backend
|
||||
globs:
|
||||
description:
|
||||
globs: **/**/*.go
|
||||
alwaysApply: false
|
||||
---
|
||||
# Cursor Rules
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
description: frontend
|
||||
globs:
|
||||
description:
|
||||
globs: app/**/*.tsx,app/**/*.vue,app/**/*.ts,app/**/*.js,app/**/*.json
|
||||
alwaysApply: false
|
||||
---
|
||||
You are an expert in TypeScript, Node.js, Vite, Vue.js, Vue Router, Pinia, VueUse, Ant Design Vue, and UnoCSS, with a deep understanding of best practices and performance optimization techniques in these technologies.
|
||||
|
|
36
api/nginx/performance.go
Normal file
36
api/nginx/performance.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package nginx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/uozi-tech/cosy"
|
||||
)
|
||||
|
||||
// GetPerformanceSettings retrieves current Nginx performance settings
|
||||
func GetPerformanceSettings(c *gin.Context) {
|
||||
// Get Nginx worker configuration info
|
||||
perfInfo, err := nginx.GetNginxWorkerConfigInfo()
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, perfInfo)
|
||||
}
|
||||
|
||||
// UpdatePerformanceSettings updates Nginx performance settings
|
||||
func UpdatePerformanceSettings(c *gin.Context) {
|
||||
var perfOpt nginx.PerfOpt
|
||||
if !cosy.BindAndValid(c, &perfOpt) {
|
||||
return
|
||||
}
|
||||
|
||||
err := nginx.UpdatePerfOpt(&perfOpt)
|
||||
if err != nil {
|
||||
cosy.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
GetPerformanceSettings(c)
|
||||
}
|
|
@ -23,4 +23,8 @@ func InitRouter(r *gin.RouterGroup) {
|
|||
r.POST("nginx/stub_status", ToggleStubStatus)
|
||||
r.POST("nginx_log", nginx_log.GetNginxLogPage)
|
||||
r.GET("nginx/directives", GetDirectives)
|
||||
|
||||
// Performance optimization endpoints
|
||||
r.GET("nginx/performance", GetPerformanceSettings)
|
||||
r.POST("nginx/performance", UpdatePerformanceSettings)
|
||||
}
|
||||
|
|
1
app/components.d.ts
vendored
1
app/components.d.ts
vendored
|
@ -71,7 +71,6 @@ declare module 'vue' {
|
|||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||
BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
|
||||
ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
|
||||
ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
|
||||
|
|
|
@ -54,6 +54,33 @@ export interface NginxPerformanceInfo {
|
|||
process_mode: string // worker进程配置模式:'auto'或'manual'
|
||||
}
|
||||
|
||||
export interface NginxConfigInfo {
|
||||
worker_processes: number
|
||||
worker_connections: number
|
||||
process_mode: string
|
||||
keepalive_timeout: number
|
||||
gzip: string
|
||||
gzip_min_length: number
|
||||
gzip_comp_level: number
|
||||
client_max_body_size: string
|
||||
server_names_hash_bucket_size: number
|
||||
client_header_buffer_size: string
|
||||
client_body_buffer_size: string
|
||||
}
|
||||
|
||||
export interface NginxPerfOpt {
|
||||
worker_processes: string
|
||||
worker_connections: string
|
||||
keepalive_timeout: string
|
||||
gzip: string
|
||||
gzip_min_length: string
|
||||
gzip_comp_level: string
|
||||
client_max_body_size: string
|
||||
server_names_hash_bucket_size: string
|
||||
client_header_buffer_size: string
|
||||
client_body_buffer_size: string
|
||||
}
|
||||
|
||||
const ngx = {
|
||||
build_config(ngxConfig: NgxConfig) {
|
||||
return http.post('/ngx/build_config', ngxConfig)
|
||||
|
@ -94,6 +121,14 @@ const ngx = {
|
|||
get_directives(): Promise<DirectiveMap> {
|
||||
return http.get('/nginx/directives')
|
||||
},
|
||||
|
||||
get_performance(): Promise<NginxConfigInfo> {
|
||||
return http.get('/nginx/performance')
|
||||
},
|
||||
|
||||
update_performance(params: NginxPerfOpt): Promise<NginxConfigInfo> {
|
||||
return http.post('/nginx/performance', params)
|
||||
},
|
||||
}
|
||||
|
||||
export default ngx
|
||||
|
|
|
@ -4,72 +4,6 @@
|
|||
|
||||
const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {
|
||||
|
||||
// cluster module notifications
|
||||
'Reload Remote Nginx Error': {
|
||||
title: () => $gettext('Reload Remote Nginx Error'),
|
||||
content: (args: any) => $gettext('Reload Nginx on %{node} failed, response: %{resp}', args),
|
||||
},
|
||||
'Reload Remote Nginx Success': {
|
||||
title: () => $gettext('Reload Remote Nginx Success'),
|
||||
content: (args: any) => $gettext('Reload Nginx on %{node} successfully', args),
|
||||
},
|
||||
'Restart Remote Nginx Error': {
|
||||
title: () => $gettext('Restart Remote Nginx Error'),
|
||||
content: (args: any) => $gettext('Restart Nginx on %{node} failed, response: %{resp}', args),
|
||||
},
|
||||
'Restart Remote Nginx Success': {
|
||||
title: () => $gettext('Restart Remote Nginx Success'),
|
||||
content: (args: any) => $gettext('Restart Nginx on %{node} successfully', args),
|
||||
},
|
||||
|
||||
// cert module notifications
|
||||
'Certificate Expired': {
|
||||
title: () => $gettext('Certificate Expired'),
|
||||
content: (args: any) => $gettext('Certificate %{name} has expired', args),
|
||||
},
|
||||
'Certificate Expiration Notice': {
|
||||
title: () => $gettext('Certificate Expiration Notice'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
|
||||
},
|
||||
'Certificate Expiring Soon': {
|
||||
title: () => $gettext('Certificate Expiring Soon'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
|
||||
},
|
||||
'Certificate Expiring Soon_1': {
|
||||
title: () => $gettext('Certificate Expiring Soon'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
|
||||
},
|
||||
'Certificate Expiring Soon_2': {
|
||||
title: () => $gettext('Certificate Expiring Soon'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in 1 day', args),
|
||||
},
|
||||
'Sync Certificate Error': {
|
||||
title: () => $gettext('Sync Certificate Error'),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args),
|
||||
},
|
||||
'Sync Certificate Success': {
|
||||
title: () => $gettext('Sync Certificate Success'),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args),
|
||||
},
|
||||
|
||||
// config module notifications
|
||||
'Sync Config Error': {
|
||||
title: () => $gettext('Sync Config Error'),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args),
|
||||
},
|
||||
'Sync Config Success': {
|
||||
title: () => $gettext('Sync Config Success'),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args),
|
||||
},
|
||||
'Rename Remote Config Error': {
|
||||
title: () => $gettext('Rename Remote Config Error'),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed', args),
|
||||
},
|
||||
'Rename Remote Config Success': {
|
||||
title: () => $gettext('Rename Remote Config Success'),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully', args),
|
||||
},
|
||||
|
||||
// site module notifications
|
||||
'Delete Remote Site Error': {
|
||||
title: () => $gettext('Delete Remote Site Error'),
|
||||
|
@ -175,6 +109,72 @@ const notifications: Record<string, { title: () => string, content: (args: any)
|
|||
title: () => $gettext('All Recovery Codes Have Been Used'),
|
||||
content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
|
||||
},
|
||||
|
||||
// cluster module notifications
|
||||
'Reload Remote Nginx Error': {
|
||||
title: () => $gettext('Reload Remote Nginx Error'),
|
||||
content: (args: any) => $gettext('Reload Nginx on %{node} failed, response: %{resp}', args),
|
||||
},
|
||||
'Reload Remote Nginx Success': {
|
||||
title: () => $gettext('Reload Remote Nginx Success'),
|
||||
content: (args: any) => $gettext('Reload Nginx on %{node} successfully', args),
|
||||
},
|
||||
'Restart Remote Nginx Error': {
|
||||
title: () => $gettext('Restart Remote Nginx Error'),
|
||||
content: (args: any) => $gettext('Restart Nginx on %{node} failed, response: %{resp}', args),
|
||||
},
|
||||
'Restart Remote Nginx Success': {
|
||||
title: () => $gettext('Restart Remote Nginx Success'),
|
||||
content: (args: any) => $gettext('Restart Nginx on %{node} successfully', args),
|
||||
},
|
||||
|
||||
// cert module notifications
|
||||
'Certificate Expired': {
|
||||
title: () => $gettext('Certificate Expired'),
|
||||
content: (args: any) => $gettext('Certificate %{name} has expired', args),
|
||||
},
|
||||
'Certificate Expiration Notice': {
|
||||
title: () => $gettext('Certificate Expiration Notice'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
|
||||
},
|
||||
'Certificate Expiring Soon': {
|
||||
title: () => $gettext('Certificate Expiring Soon'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
|
||||
},
|
||||
'Certificate Expiring Soon_1': {
|
||||
title: () => $gettext('Certificate Expiring Soon'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in %{days} days', args),
|
||||
},
|
||||
'Certificate Expiring Soon_2': {
|
||||
title: () => $gettext('Certificate Expiring Soon'),
|
||||
content: (args: any) => $gettext('Certificate %{name} will expire in 1 day', args),
|
||||
},
|
||||
'Sync Certificate Error': {
|
||||
title: () => $gettext('Sync Certificate Error'),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args),
|
||||
},
|
||||
'Sync Certificate Success': {
|
||||
title: () => $gettext('Sync Certificate Success'),
|
||||
content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args),
|
||||
},
|
||||
|
||||
// config module notifications
|
||||
'Sync Config Error': {
|
||||
title: () => $gettext('Sync Config Error'),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args),
|
||||
},
|
||||
'Sync Config Success': {
|
||||
title: () => $gettext('Sync Config Success'),
|
||||
content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args),
|
||||
},
|
||||
'Rename Remote Config Error': {
|
||||
title: () => $gettext('Rename Remote Config Error'),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed', args),
|
||||
},
|
||||
'Rename Remote Config Success': {
|
||||
title: () => $gettext('Rename Remote Config Success'),
|
||||
content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully', args),
|
||||
},
|
||||
}
|
||||
|
||||
export default notifications
|
||||
|
|
|
@ -11,4 +11,5 @@ export default {
|
|||
4047: () => $gettext('Sites-enabled directory not exist'),
|
||||
4048: () => $gettext('Streams-available directory not exist'),
|
||||
4049: () => $gettext('Streams-enabled directory not exist'),
|
||||
4050: () => $gettext('Nginx conf not include conf.d directory'),
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -61,6 +61,9 @@ const newPath = computed(() => {
|
|||
const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string)
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
// Use Vue 3.4+ useTemplateRef for InspectConfig component
|
||||
const inspectConfigRef = useTemplateRef<InstanceType<typeof InspectConfig>>('inspectConfig')
|
||||
|
||||
async function init() {
|
||||
const { name } = route.params
|
||||
|
||||
|
@ -200,6 +203,8 @@ function save() {
|
|||
}
|
||||
else {
|
||||
data.value = r
|
||||
// Run test after saving to verify configuration
|
||||
inspectConfigRef.value?.test()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -254,6 +259,7 @@ function openHistory() {
|
|||
|
||||
<InspectConfig
|
||||
v-show="!addMode"
|
||||
ref="inspectConfig"
|
||||
/>
|
||||
<CodeEditor v-model:content="data.content" />
|
||||
<FooterToolBar>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useUserStore } from '@/pinia'
|
|||
import { useGlobalStore } from '@/pinia/moudule/global'
|
||||
import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
|
||||
import PerformanceOptimization from './components/PerformanceOptimization.vue'
|
||||
import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
|
||||
import PerformanceTablesCard from './components/PerformanceTablesCard.vue'
|
||||
import ProcessDistributionCard from './components/ProcessDistributionCard.vue'
|
||||
|
@ -182,6 +183,9 @@ onMounted(() => {
|
|||
<div v-if="nginxInfo" class="performance-dashboard">
|
||||
<!-- Top performance metrics card -->
|
||||
<ACard class="mb-4" :title="$gettext('Performance Metrics')" :bordered="false">
|
||||
<template #extra>
|
||||
<PerformanceOptimization />
|
||||
</template>
|
||||
<PerformanceStatisticsCard :nginx-info="nginxInfo" />
|
||||
</ACard>
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||
import { computed, defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nginxInfo: NginxPerformanceInfo
|
||||
|
|
327
app/src/views/dashboard/components/PerformanceOptimization.vue
Normal file
327
app/src/views/dashboard/components/PerformanceOptimization.vue
Normal file
|
@ -0,0 +1,327 @@
|
|||
<script setup lang="ts">
|
||||
import type { NginxConfigInfo, NginxPerfOpt } from '@/api/ngx'
|
||||
import type { CheckedType } from '@/types'
|
||||
import ngx from '@/api/ngx'
|
||||
import {
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
// Size units
|
||||
const sizeUnits = ['k', 'm', 'g']
|
||||
|
||||
// Size values and units
|
||||
const maxBodySizeValue = ref<number>(1)
|
||||
const maxBodySizeUnit = ref<string>('m')
|
||||
const headerBufferSizeValue = ref<number>(1)
|
||||
const headerBufferSizeUnit = ref<string>('k')
|
||||
const bodyBufferSizeValue = ref<number>(8)
|
||||
const bodyBufferSizeUnit = ref<string>('k')
|
||||
|
||||
// Performance settings modal
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const performanceConfig = ref<NginxConfigInfo>({
|
||||
worker_processes: 1,
|
||||
worker_connections: 1024,
|
||||
process_mode: 'manual',
|
||||
keepalive_timeout: 65,
|
||||
gzip: 'off',
|
||||
gzip_min_length: 1,
|
||||
gzip_comp_level: 1,
|
||||
client_max_body_size: '1m',
|
||||
server_names_hash_bucket_size: 32,
|
||||
client_header_buffer_size: '1k',
|
||||
client_body_buffer_size: '8k',
|
||||
})
|
||||
|
||||
// Open modal and load performance settings
|
||||
async function openPerformanceModal() {
|
||||
visible.value = true
|
||||
await fetchPerformanceSettings()
|
||||
}
|
||||
|
||||
// Load performance settings
|
||||
async function fetchPerformanceSettings() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ngx.get_performance()
|
||||
performanceConfig.value = data
|
||||
|
||||
// Parse size values and units
|
||||
parseSizeValues()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to get Nginx performance settings:', error)
|
||||
message.error($gettext('Failed to get Nginx performance settings'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Parse size values from config
|
||||
function parseSizeValues() {
|
||||
// Parse client_max_body_size
|
||||
const maxBodySize = performanceConfig.value.client_max_body_size
|
||||
const maxBodyMatch = maxBodySize.match(/^(\d+)([kmg])?$/i)
|
||||
if (maxBodyMatch) {
|
||||
maxBodySizeValue.value = Number.parseInt(maxBodyMatch[1])
|
||||
maxBodySizeUnit.value = (maxBodyMatch[2] || 'm').toLowerCase()
|
||||
}
|
||||
|
||||
// Parse client_header_buffer_size
|
||||
const headerSize = performanceConfig.value.client_header_buffer_size
|
||||
const headerMatch = headerSize.match(/^(\d+)([kmg])?$/i)
|
||||
if (headerMatch) {
|
||||
headerBufferSizeValue.value = Number.parseInt(headerMatch[1])
|
||||
headerBufferSizeUnit.value = (headerMatch[2] || 'k').toLowerCase()
|
||||
}
|
||||
|
||||
// Parse client_body_buffer_size
|
||||
const bodySize = performanceConfig.value.client_body_buffer_size
|
||||
const bodyMatch = bodySize.match(/^(\d+)([kmg])?$/i)
|
||||
if (bodyMatch) {
|
||||
bodyBufferSizeValue.value = Number.parseInt(bodyMatch[1])
|
||||
bodyBufferSizeUnit.value = (bodyMatch[2] || 'k').toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
// Format size values before saving
|
||||
function formatSizeValues() {
|
||||
performanceConfig.value.client_max_body_size = `${maxBodySizeValue.value}${maxBodySizeUnit.value}`
|
||||
performanceConfig.value.client_header_buffer_size = `${headerBufferSizeValue.value}${headerBufferSizeUnit.value}`
|
||||
performanceConfig.value.client_body_buffer_size = `${bodyBufferSizeValue.value}${bodyBufferSizeUnit.value}`
|
||||
}
|
||||
|
||||
// Save performance settings
|
||||
async function savePerformanceSettings() {
|
||||
loading.value = true
|
||||
try {
|
||||
// Format size values
|
||||
formatSizeValues()
|
||||
|
||||
const params: NginxPerfOpt = {
|
||||
worker_processes: performanceConfig.value.process_mode === 'auto' ? 'auto' : performanceConfig.value.worker_processes.toString(),
|
||||
worker_connections: performanceConfig.value.worker_connections.toString(),
|
||||
keepalive_timeout: performanceConfig.value.keepalive_timeout.toString(),
|
||||
gzip: performanceConfig.value.gzip,
|
||||
gzip_min_length: performanceConfig.value.gzip_min_length.toString(),
|
||||
gzip_comp_level: performanceConfig.value.gzip_comp_level.toString(),
|
||||
client_max_body_size: performanceConfig.value.client_max_body_size,
|
||||
server_names_hash_bucket_size: performanceConfig.value.server_names_hash_bucket_size.toString(),
|
||||
client_header_buffer_size: performanceConfig.value.client_header_buffer_size,
|
||||
client_body_buffer_size: performanceConfig.value.client_body_buffer_size,
|
||||
}
|
||||
const data = await ngx.update_performance(params)
|
||||
performanceConfig.value = data
|
||||
|
||||
// Parse the returned values
|
||||
parseSizeValues()
|
||||
|
||||
message.success($gettext('Performance settings saved successfully'))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to save Nginx performance settings:', error)
|
||||
message.error($gettext('Failed to save Nginx performance settings'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle worker process mode
|
||||
function handleProcessModeChange(checked: CheckedType) {
|
||||
performanceConfig.value.process_mode = checked ? 'auto' : 'manual'
|
||||
if (checked) {
|
||||
performanceConfig.value.worker_processes = navigator.hardwareConcurrency || 4
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle GZIP compression
|
||||
function handleGzipChange(checked: CheckedType) {
|
||||
performanceConfig.value.gzip = checked ? 'on' : 'off'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Performance Optimization Button -->
|
||||
<AButton
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openPerformanceModal"
|
||||
>
|
||||
<template #icon>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
{{ $gettext('Optimize Performance') }}
|
||||
</AButton>
|
||||
|
||||
<!-- Performance Optimization Modal -->
|
||||
<AModal
|
||||
v-model:open="visible"
|
||||
:title="$gettext('Optimize Nginx Performance')"
|
||||
:mask-closable="false"
|
||||
:ok-button-props="{ loading }"
|
||||
@ok="savePerformanceSettings"
|
||||
>
|
||||
<ASpin :spinning="loading">
|
||||
<AForm layout="vertical">
|
||||
<AFormItem
|
||||
:label="$gettext('Worker Processes')"
|
||||
:help="$gettext('Number of concurrent worker processes, auto sets to CPU core count')"
|
||||
>
|
||||
<ASpace>
|
||||
<ASwitch
|
||||
:checked="performanceConfig.process_mode === 'auto'"
|
||||
:checked-children="$gettext('Auto')"
|
||||
:un-checked-children="$gettext('Manual')"
|
||||
@change="handleProcessModeChange"
|
||||
/>
|
||||
<AInputNumber
|
||||
v-if="performanceConfig.process_mode !== 'auto'"
|
||||
v-model:value="performanceConfig.worker_processes"
|
||||
:min="1"
|
||||
:max="32"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span v-else>{{ performanceConfig.worker_processes }} ({{ $gettext('Auto') }})</span>
|
||||
</ASpace>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('Worker Connections')"
|
||||
:help="$gettext('Maximum number of concurrent connections')"
|
||||
>
|
||||
<AInputNumber
|
||||
v-model:value="performanceConfig.worker_connections"
|
||||
:min="512"
|
||||
:max="65536"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('Keepalive Timeout')"
|
||||
:help="$gettext('Connection timeout period')"
|
||||
>
|
||||
<ASpace>
|
||||
<AInputNumber
|
||||
v-model:value="performanceConfig.keepalive_timeout"
|
||||
:min="0"
|
||||
:max="999"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span>{{ $gettext('seconds') }}</span>
|
||||
</ASpace>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('GZIP Compression')"
|
||||
:help="$gettext('Enable compression for content transfer')"
|
||||
>
|
||||
<ASwitch
|
||||
:checked="performanceConfig.gzip === 'on'"
|
||||
:checked-children="$gettext('On')"
|
||||
:un-checked-children="$gettext('Off')"
|
||||
@change="handleGzipChange"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('GZIP Min Length')"
|
||||
:help="$gettext('Minimum file size for compression')"
|
||||
>
|
||||
<ASpace>
|
||||
<AInputNumber
|
||||
v-model:value="performanceConfig.gzip_min_length"
|
||||
:min="0"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span>{{ $gettext('KB') }}</span>
|
||||
</ASpace>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('GZIP Compression Level')"
|
||||
:help="$gettext('Compression level, 1 is lowest, 9 is highest')"
|
||||
>
|
||||
<AInputNumber
|
||||
v-model:value="performanceConfig.gzip_comp_level"
|
||||
:min="1"
|
||||
:max="9"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('Client Max Body Size')"
|
||||
:help="$gettext('Maximum client request body size')"
|
||||
>
|
||||
<AInputGroup compact style="width: 180px">
|
||||
<AInputNumber
|
||||
v-model:value="maxBodySizeValue"
|
||||
:min="1"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<ASelect v-model:value="maxBodySizeUnit" style="width: 60px">
|
||||
<ASelectOption v-for="unit in sizeUnits" :key="unit" :value="unit">
|
||||
{{ unit.toUpperCase() }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AInputGroup>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('Server Names Hash Bucket Size')"
|
||||
:help="$gettext('Server names hash table size')"
|
||||
>
|
||||
<AInputNumber
|
||||
v-model:value="performanceConfig.server_names_hash_bucket_size"
|
||||
:min="32"
|
||||
:step="32"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('Client Header Buffer Size')"
|
||||
:help="$gettext('Client request header buffer size')"
|
||||
>
|
||||
<AInputGroup compact style="width: 180px">
|
||||
<AInputNumber
|
||||
v-model:value="headerBufferSizeValue"
|
||||
:min="1"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<ASelect v-model:value="headerBufferSizeUnit" style="width: 60px">
|
||||
<ASelectOption v-for="unit in sizeUnits" :key="unit" :value="unit">
|
||||
{{ unit.toUpperCase() }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AInputGroup>
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem
|
||||
:label="$gettext('Client Body Buffer Size')"
|
||||
:help="$gettext('Client request body buffer size')"
|
||||
>
|
||||
<AInputGroup compact style="width: 180px">
|
||||
<AInputNumber
|
||||
v-model:value="bodyBufferSizeValue"
|
||||
:min="1"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<ASelect v-model:value="bodyBufferSizeUnit" style="width: 60px">
|
||||
<ASelectOption v-for="unit in sizeUnits" :key="unit" :value="unit">
|
||||
{{ unit.toUpperCase() }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AInputGroup>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</ASpin>
|
||||
</AModal>
|
||||
</div>
|
||||
</template>
|
|
@ -7,7 +7,6 @@ import {
|
|||
InfoCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { computed, defineProps } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nginxInfo: NginxPerformanceInfo
|
||||
|
@ -28,6 +27,7 @@ const maxRPS = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ARow :gutter="[16, 24]">
|
||||
<!-- Maximum RPS -->
|
||||
<ACol :xs="24" :sm="12" :md="8" :lg="6">
|
||||
|
@ -104,4 +104,5 @@ const maxRPS = computed(() => {
|
|||
</div>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import type { NginxPerformanceInfo } from '@/api/ngx'
|
||||
import type { TableColumnType } from 'ant-design-vue'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { computed, defineProps, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nginxInfo: NginxPerformanceInfo
|
||||
|
|
|
@ -50,7 +50,7 @@ const thisYear = new Date().getFullYear()
|
|||
<h3>
|
||||
{{ $gettext('Project Team') }}
|
||||
</h3>
|
||||
<p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a></p>
|
||||
<p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a> <a href="https://github.com/akinoccc">@Akino</a></p>
|
||||
<h3>
|
||||
{{ $gettext('Build with') }}
|
||||
</h3>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package nginx
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
@ -13,6 +13,14 @@ type NginxConfigInfo struct {
|
|||
WorkerProcesses int `json:"worker_processes"`
|
||||
WorkerConnections int `json:"worker_connections"`
|
||||
ProcessMode string `json:"process_mode"`
|
||||
KeepaliveTimeout int `json:"keepalive_timeout"`
|
||||
Gzip string `json:"gzip"`
|
||||
GzipMinLength int `json:"gzip_min_length"`
|
||||
GzipCompLevel int `json:"gzip_comp_level"`
|
||||
ClientMaxBodySize string `json:"client_max_body_size"` // with unit
|
||||
ServerNamesHashBucketSize int `json:"server_names_hash_bucket_size"`
|
||||
ClientHeaderBufferSize string `json:"client_header_buffer_size"` // with unit
|
||||
ClientBodyBufferSize string `json:"client_body_buffer_size"` // with unit
|
||||
}
|
||||
|
||||
// GetNginxWorkerConfigInfo Get Nginx config info of worker_processes and worker_connections
|
||||
|
@ -21,18 +29,32 @@ func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
|
|||
WorkerProcesses: 1,
|
||||
WorkerConnections: 1024,
|
||||
ProcessMode: "manual",
|
||||
KeepaliveTimeout: 65,
|
||||
Gzip: "off",
|
||||
GzipMinLength: 1,
|
||||
GzipCompLevel: 1,
|
||||
ClientMaxBodySize: "1m",
|
||||
ServerNamesHashBucketSize: 32,
|
||||
ClientHeaderBufferSize: "1k",
|
||||
ClientBodyBufferSize: "8k",
|
||||
}
|
||||
|
||||
// Get worker_processes config
|
||||
cmd := exec.Command("nginx", "-T")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to get nginx config")
|
||||
confPath := GetConfPath("nginx.conf")
|
||||
if confPath == "" {
|
||||
return nil, errors.New("failed to get nginx.conf path")
|
||||
}
|
||||
|
||||
// Read the current configuration
|
||||
content, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read nginx.conf")
|
||||
}
|
||||
|
||||
outputStr := string(content)
|
||||
|
||||
// Parse worker_processes
|
||||
wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
|
||||
if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
|
||||
if matches := wpRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
if matches[1] == "auto" {
|
||||
result.WorkerProcesses = runtime.NumCPU()
|
||||
result.ProcessMode = "auto"
|
||||
|
@ -44,9 +66,57 @@ func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
|
|||
|
||||
// Parse worker_connections
|
||||
wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
|
||||
if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
|
||||
if matches := wcRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.WorkerConnections, _ = strconv.Atoi(matches[1])
|
||||
}
|
||||
|
||||
// Parse keepalive_timeout
|
||||
ktRe := regexp.MustCompile(`keepalive_timeout\s+(\d+);`)
|
||||
if matches := ktRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.KeepaliveTimeout, _ = strconv.Atoi(matches[1])
|
||||
}
|
||||
|
||||
// Parse gzip
|
||||
gzipRe := regexp.MustCompile(`gzip\s+(on|off);`)
|
||||
if matches := gzipRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.Gzip = matches[1]
|
||||
}
|
||||
|
||||
// Parse gzip_min_length
|
||||
gzipMinRe := regexp.MustCompile(`gzip_min_length\s+(\d+);`)
|
||||
if matches := gzipMinRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.GzipMinLength, _ = strconv.Atoi(matches[1])
|
||||
}
|
||||
|
||||
// Parse gzip_comp_level
|
||||
gzipCompRe := regexp.MustCompile(`gzip_comp_level\s+(\d+);`)
|
||||
if matches := gzipCompRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.GzipCompLevel, _ = strconv.Atoi(matches[1])
|
||||
}
|
||||
|
||||
// Parse client_max_body_size with any unit (k, m, g)
|
||||
cmaxRe := regexp.MustCompile(`client_max_body_size\s+(\d+[kmg]?);`)
|
||||
if matches := cmaxRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.ClientMaxBodySize = matches[1]
|
||||
}
|
||||
|
||||
// Parse server_names_hash_bucket_size
|
||||
hashRe := regexp.MustCompile(`server_names_hash_bucket_size\s+(\d+);`)
|
||||
if matches := hashRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.ServerNamesHashBucketSize, _ = strconv.Atoi(matches[1])
|
||||
}
|
||||
|
||||
// Parse client_header_buffer_size with any unit (k, m, g)
|
||||
headerRe := regexp.MustCompile(`client_header_buffer_size\s+(\d+[kmg]?);`)
|
||||
if matches := headerRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.ClientHeaderBufferSize = matches[1]
|
||||
}
|
||||
|
||||
// Parse client_body_buffer_size with any unit (k, m, g)
|
||||
bodyRe := regexp.MustCompile(`client_body_buffer_size\s+(\d+[kmg]?);`)
|
||||
if matches := bodyRe.FindStringSubmatch(outputStr); len(matches) > 1 {
|
||||
result.ClientBodyBufferSize = matches[1]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
148
internal/nginx/perf_opt.go
Normal file
148
internal/nginx/perf_opt.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tufanbarisyildirim/gonginx/config"
|
||||
"github.com/tufanbarisyildirim/gonginx/dumper"
|
||||
"github.com/tufanbarisyildirim/gonginx/parser"
|
||||
)
|
||||
|
||||
// PerfOpt represents Nginx performance optimization settings
|
||||
type PerfOpt struct {
|
||||
WorkerProcesses string `json:"worker_processes"` // auto or number
|
||||
WorkerConnections string `json:"worker_connections"` // max connections
|
||||
KeepaliveTimeout string `json:"keepalive_timeout"` // timeout in seconds
|
||||
Gzip string `json:"gzip"` // on or off
|
||||
GzipMinLength string `json:"gzip_min_length"` // min length to compress
|
||||
GzipCompLevel string `json:"gzip_comp_level"` // compression level
|
||||
ClientMaxBodySize string `json:"client_max_body_size"` // max body size (with unit: k, m, g)
|
||||
ServerNamesHashBucketSize string `json:"server_names_hash_bucket_size"` // hash bucket size
|
||||
ClientHeaderBufferSize string `json:"client_header_buffer_size"` // header buffer size (with unit: k, m, g)
|
||||
ClientBodyBufferSize string `json:"client_body_buffer_size"` // body buffer size (with unit: k, m, g)
|
||||
}
|
||||
|
||||
// UpdatePerfOpt updates the Nginx performance optimization settings
|
||||
func UpdatePerfOpt(opt *PerfOpt) error {
|
||||
confPath := GetConfPath("nginx.conf")
|
||||
if confPath == "" {
|
||||
return errors.New("failed to get nginx.conf path")
|
||||
}
|
||||
|
||||
// Read the current configuration
|
||||
content, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read nginx.conf")
|
||||
}
|
||||
|
||||
// Create a backup file
|
||||
backupPath := fmt.Sprintf("%s.backup.%d", confPath, time.Now().Unix())
|
||||
err = os.WriteFile(backupPath, content, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create backup file")
|
||||
}
|
||||
|
||||
// Parse the configuration
|
||||
p := parser.NewStringParser(string(content), parser.WithSkipValidDirectivesErr())
|
||||
conf, err := p.Parse()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse nginx.conf")
|
||||
}
|
||||
|
||||
// Process the configuration and update performance settings
|
||||
updateNginxConfig(conf.Block, opt)
|
||||
|
||||
// Dump the updated configuration
|
||||
updatedConf := dumper.DumpBlock(conf.Block, dumper.IndentedStyle)
|
||||
|
||||
// Write the updated configuration
|
||||
err = os.WriteFile(confPath, []byte(updatedConf), 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write updated nginx.conf")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateNginxConfig updates the performance settings in the Nginx configuration
|
||||
func updateNginxConfig(block config.IBlock, opt *PerfOpt) {
|
||||
if block == nil {
|
||||
return
|
||||
}
|
||||
|
||||
directives := block.GetDirectives()
|
||||
// Update main context directives
|
||||
updateOrAddDirective(block, directives, "worker_processes", opt.WorkerProcesses)
|
||||
|
||||
// Look for events, http, and other blocks
|
||||
for _, directive := range directives {
|
||||
if directive.GetName() == "events" && directive.GetBlock() != nil {
|
||||
// Update events block directives
|
||||
eventsBlock := directive.GetBlock()
|
||||
eventsDirectives := eventsBlock.GetDirectives()
|
||||
updateOrAddDirective(eventsBlock, eventsDirectives, "worker_connections", opt.WorkerConnections)
|
||||
} else if directive.GetName() == "http" && directive.GetBlock() != nil {
|
||||
// Update http block directives
|
||||
httpBlock := directive.GetBlock()
|
||||
httpDirectives := httpBlock.GetDirectives()
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "keepalive_timeout", opt.KeepaliveTimeout)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "gzip", opt.Gzip)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "gzip_min_length", opt.GzipMinLength)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "gzip_comp_level", opt.GzipCompLevel)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "client_max_body_size", opt.ClientMaxBodySize)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "server_names_hash_bucket_size", opt.ServerNamesHashBucketSize)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "client_header_buffer_size", opt.ClientHeaderBufferSize)
|
||||
updateOrAddDirective(httpBlock, httpDirectives, "client_body_buffer_size", opt.ClientBodyBufferSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateOrAddDirective updates a directive if it exists, or adds it to the block if it doesn't
|
||||
func updateOrAddDirective(block config.IBlock, directives []config.IDirective, name string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Search for existing directive
|
||||
for _, directive := range directives {
|
||||
if directive.GetName() == name {
|
||||
// Update existing directive
|
||||
if len(directive.GetParameters()) > 0 {
|
||||
directive.GetParameters()[0].Value = value
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we need to add a new directive
|
||||
// Create a new directive and add it to the block
|
||||
// This requires knowledge of the underlying implementation
|
||||
// For now, we'll use the Directive type from gonginx/config
|
||||
newDirective := &config.Directive{
|
||||
Name: name,
|
||||
Parameters: []config.Parameter{{Value: value}},
|
||||
}
|
||||
|
||||
// Add the new directive to the block
|
||||
// This is specific to the gonginx library implementation
|
||||
switch block := block.(type) {
|
||||
case *config.Config:
|
||||
block.Block.Directives = append(block.Block.Directives, newDirective)
|
||||
case *config.Block:
|
||||
block.Directives = append(block.Directives, newDirective)
|
||||
case *config.HTTP:
|
||||
block.Directives = append(block.Directives, newDirective)
|
||||
}
|
||||
}
|
||||
|
||||
// sortDirectives sorts directives alphabetically by name
|
||||
func sortDirectives(directives []config.IDirective) {
|
||||
sort.SliceStable(directives, func(i, j int) bool {
|
||||
// Ensure both i and j can return valid names
|
||||
return directives[i].GetName() < directives[j].GetName()
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue