feat(nginx): performance optimization #850

This commit is contained in:
Jacky 2025-04-11 12:27:18 +00:00
parent b59da3e7e8
commit 9d4070a211
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
29 changed files with 4250 additions and 2031 deletions

View file

@ -1,6 +1,6 @@
--- ---
description: backend description:
globs: globs: **/**/*.go
alwaysApply: false alwaysApply: false
--- ---
# Cursor Rules # Cursor Rules

View file

@ -1,6 +1,6 @@
--- ---
description: frontend description:
globs: globs: app/**/*.tsx,app/**/*.vue,app/**/*.ts,app/**/*.js,app/**/*.json
alwaysApply: false 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. 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
View 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)
}

View file

@ -23,4 +23,8 @@ func InitRouter(r *gin.RouterGroup) {
r.POST("nginx/stub_status", ToggleStubStatus) r.POST("nginx/stub_status", ToggleStubStatus)
r.POST("nginx_log", nginx_log.GetNginxLogPage) r.POST("nginx_log", nginx_log.GetNginxLogPage)
r.GET("nginx/directives", GetDirectives) r.GET("nginx/directives", GetDirectives)
// Performance optimization endpoints
r.GET("nginx/performance", GetPerformanceSettings)
r.POST("nginx/performance", UpdatePerformanceSettings)
} }

1
app/components.d.ts vendored
View file

@ -71,7 +71,6 @@ declare module 'vue' {
ATag: typeof import('ant-design-vue/es')['Tag'] ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] 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'] BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default'] ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default'] ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']

View file

@ -54,6 +54,33 @@ export interface NginxPerformanceInfo {
process_mode: string // worker进程配置模式'auto'或'manual' 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 = { const ngx = {
build_config(ngxConfig: NgxConfig) { build_config(ngxConfig: NgxConfig) {
return http.post('/ngx/build_config', ngxConfig) return http.post('/ngx/build_config', ngxConfig)
@ -94,6 +121,14 @@ const ngx = {
get_directives(): Promise<DirectiveMap> { get_directives(): Promise<DirectiveMap> {
return http.get('/nginx/directives') 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 export default ngx

View file

@ -4,72 +4,6 @@
const notifications: Record<string, { title: () => string, content: (args: any) => string }> = { 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 // site module notifications
'Delete Remote Site Error': { 'Delete Remote Site Error': {
title: () => $gettext('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'), 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), 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 export default notifications

View file

@ -11,4 +11,5 @@ export default {
4047: () => $gettext('Sites-enabled directory not exist'), 4047: () => $gettext('Sites-enabled directory not exist'),
4048: () => $gettext('Streams-available directory not exist'), 4048: () => $gettext('Streams-available directory not exist'),
4049: () => $gettext('Streams-enabled 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

View file

@ -61,6 +61,9 @@ const newPath = computed(() => {
const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string) const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string)
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
// Use Vue 3.4+ useTemplateRef for InspectConfig component
const inspectConfigRef = useTemplateRef<InstanceType<typeof InspectConfig>>('inspectConfig')
async function init() { async function init() {
const { name } = route.params const { name } = route.params
@ -200,6 +203,8 @@ function save() {
} }
else { else {
data.value = r data.value = r
// Run test after saving to verify configuration
inspectConfigRef.value?.test()
} }
}) })
}) })
@ -254,6 +259,7 @@ function openHistory() {
<InspectConfig <InspectConfig
v-show="!addMode" v-show="!addMode"
ref="inspectConfig"
/> />
<CodeEditor v-model:content="data.content" /> <CodeEditor v-model:content="data.content" />
<FooterToolBar> <FooterToolBar>

View file

@ -7,6 +7,7 @@ import { useUserStore } from '@/pinia'
import { useGlobalStore } from '@/pinia/moudule/global' import { useGlobalStore } from '@/pinia/moudule/global'
import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue' import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue' import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
import PerformanceOptimization from './components/PerformanceOptimization.vue'
import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue' import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
import PerformanceTablesCard from './components/PerformanceTablesCard.vue' import PerformanceTablesCard from './components/PerformanceTablesCard.vue'
import ProcessDistributionCard from './components/ProcessDistributionCard.vue' import ProcessDistributionCard from './components/ProcessDistributionCard.vue'
@ -182,6 +183,9 @@ onMounted(() => {
<div v-if="nginxInfo" class="performance-dashboard"> <div v-if="nginxInfo" class="performance-dashboard">
<!-- Top performance metrics card --> <!-- Top performance metrics card -->
<ACard class="mb-4" :title="$gettext('Performance Metrics')" :bordered="false"> <ACard class="mb-4" :title="$gettext('Performance Metrics')" :bordered="false">
<template #extra>
<PerformanceOptimization />
</template>
<PerformanceStatisticsCard :nginx-info="nginxInfo" /> <PerformanceStatisticsCard :nginx-info="nginxInfo" />
</ACard> </ACard>

View file

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NginxPerformanceInfo } from '@/api/ngx' import type { NginxPerformanceInfo } from '@/api/ngx'
import { computed, defineProps } from 'vue'
const props = defineProps<{ const props = defineProps<{
nginxInfo: NginxPerformanceInfo nginxInfo: NginxPerformanceInfo

View 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>

View file

@ -7,7 +7,6 @@ import {
InfoCircleOutlined, InfoCircleOutlined,
ThunderboltOutlined, ThunderboltOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { computed, defineProps } from 'vue'
const props = defineProps<{ const props = defineProps<{
nginxInfo: NginxPerformanceInfo nginxInfo: NginxPerformanceInfo
@ -28,80 +27,82 @@ const maxRPS = computed(() => {
</script> </script>
<template> <template>
<ARow :gutter="[16, 24]"> <div>
<!-- Maximum RPS --> <ARow :gutter="[16, 24]">
<ACol :xs="24" :sm="12" :md="8" :lg="6"> <!-- Maximum RPS -->
<AStatistic <ACol :xs="24" :sm="12" :md="8" :lg="6">
:value="maxRPS" <AStatistic
:value-style="{ color: '#1890ff', fontSize: '24px' }" :value="maxRPS"
> :value-style="{ color: '#1890ff', fontSize: '24px' }"
<template #prefix> >
<ThunderboltOutlined /> <template #prefix>
</template> <ThunderboltOutlined />
<template #title> </template>
{{ $gettext('Max Requests Per Second') }} <template #title>
<ATooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')"> {{ $gettext('Max Requests Per Second') }}
<InfoCircleOutlined class="ml-1 text-gray-500" /> <ATooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
</ATooltip> <InfoCircleOutlined class="ml-1 text-gray-500" />
</template> </ATooltip>
</AStatistic> </template>
<div class="text-xs text-gray-500 mt-1"> </AStatistic>
worker_processes ({{ nginxInfo.worker_processes }}) × worker_connections ({{ nginxInfo.worker_connections }}) <div class="text-xs text-gray-500 mt-1">
</div> worker_processes ({{ nginxInfo.worker_processes }}) × worker_connections ({{ nginxInfo.worker_connections }})
</ACol> </div>
</ACol>
<!-- Maximum concurrent connections --> <!-- Maximum concurrent connections -->
<ACol :xs="24" :sm="12" :md="8" :lg="6"> <ACol :xs="24" :sm="12" :md="8" :lg="6">
<AStatistic <AStatistic
:title="$gettext('Max Concurrent Connections')" :title="$gettext('Max Concurrent Connections')"
:value="nginxInfo.worker_processes * nginxInfo.worker_connections" :value="nginxInfo.worker_processes * nginxInfo.worker_connections"
:value-style="{ color: '#52c41a', fontSize: '24px' }" :value-style="{ color: '#52c41a', fontSize: '24px' }"
> >
<template #prefix> <template #prefix>
<ApiOutlined /> <ApiOutlined />
</template> </template>
</AStatistic> </AStatistic>
<div class="text-xs text-gray-500 mt-1"> <div class="text-xs text-gray-500 mt-1">
{{ $gettext('Current usage') }}: {{ ((nginxInfo.active / (nginxInfo.worker_processes * nginxInfo.worker_connections)) * 100).toFixed(2) }}% {{ $gettext('Current usage') }}: {{ ((nginxInfo.active / (nginxInfo.worker_processes * nginxInfo.worker_connections)) * 100).toFixed(2) }}%
</div> </div>
</ACol> </ACol>
<!-- Requests per connection --> <!-- Requests per connection -->
<ACol :xs="24" :sm="12" :md="8" :lg="6"> <ACol :xs="24" :sm="12" :md="8" :lg="6">
<AStatistic <AStatistic
:value="requestsPerConnection" :value="requestsPerConnection"
:precision="2" :precision="2"
:value-style="{ color: '#3a7f99', fontSize: '24px' }" :value-style="{ color: '#3a7f99', fontSize: '24px' }"
> >
<template #title> <template #title>
{{ $gettext('Requests Per Connection') }} {{ $gettext('Requests Per Connection') }}
<ATooltip :title="$gettext('Total Requests / Total Connections')"> <ATooltip :title="$gettext('Total Requests / Total Connections')">
<InfoCircleOutlined class="ml-1 text-gray-500" /> <InfoCircleOutlined class="ml-1 text-gray-500" />
</ATooltip> </ATooltip>
</template> </template>
<template #prefix> <template #prefix>
<DashboardOutlined /> <DashboardOutlined />
</template> </template>
</AStatistic> </AStatistic>
<div class="text-xs text-gray-500 mt-1"> <div class="text-xs text-gray-500 mt-1">
{{ $gettext('Higher value means better connection reuse') }} {{ $gettext('Higher value means better connection reuse') }}
</div> </div>
</ACol> </ACol>
<!-- Total Nginx processes --> <!-- Total Nginx processes -->
<ACol :xs="24" :sm="12" :md="8" :lg="6"> <ACol :xs="24" :sm="12" :md="8" :lg="6">
<AStatistic <AStatistic
:title="$gettext('Total Nginx Processes')" :title="$gettext('Total Nginx Processes')"
:value="nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other" :value="nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other"
:value-style="{ color: '#722ed1', fontSize: '24px' }" :value-style="{ color: '#722ed1', fontSize: '24px' }"
> >
<template #prefix> <template #prefix>
<CloudServerOutlined /> <CloudServerOutlined />
</template> </template>
</AStatistic> </AStatistic>
<div class="text-xs text-gray-500 mt-1"> <div class="text-xs text-gray-500 mt-1">
{{ $gettext('Workers') }}: {{ nginxInfo.workers }}, {{ $gettext('Master') }}: {{ nginxInfo.master }}, {{ $gettext('Others') }}: {{ nginxInfo.cache + nginxInfo.other }} {{ $gettext('Workers') }}: {{ nginxInfo.workers }}, {{ $gettext('Master') }}: {{ nginxInfo.master }}, {{ $gettext('Others') }}: {{ nginxInfo.cache + nginxInfo.other }}
</div> </div>
</ACol> </ACol>
</ARow> </ARow>
</div>
</template> </template>

View file

@ -2,7 +2,6 @@
import type { NginxPerformanceInfo } from '@/api/ngx' import type { NginxPerformanceInfo } from '@/api/ngx'
import type { TableColumnType } from 'ant-design-vue' import type { TableColumnType } from 'ant-design-vue'
import { InfoCircleOutlined } from '@ant-design/icons-vue' import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { computed, defineProps, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
nginxInfo: NginxPerformanceInfo nginxInfo: NginxPerformanceInfo

View file

@ -50,7 +50,7 @@ const thisYear = new Date().getFullYear()
<h3> <h3>
{{ $gettext('Project Team') }} {{ $gettext('Project Team') }}
</h3> </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> <h3>
{{ $gettext('Build with') }} {{ $gettext('Build with') }}
</h3> </h3>

View file

@ -1,7 +1,7 @@
package nginx package nginx
import ( import (
"os/exec" "os"
"regexp" "regexp"
"runtime" "runtime"
"strconv" "strconv"
@ -10,29 +10,51 @@ import (
) )
type NginxConfigInfo struct { type NginxConfigInfo struct {
WorkerProcesses int `json:"worker_processes"` WorkerProcesses int `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"` WorkerConnections int `json:"worker_connections"`
ProcessMode string `json:"process_mode"` 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 // GetNginxWorkerConfigInfo Get Nginx config info of worker_processes and worker_connections
func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) { func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
result := &NginxConfigInfo{ result := &NginxConfigInfo{
WorkerProcesses: 1, WorkerProcesses: 1,
WorkerConnections: 1024, WorkerConnections: 1024,
ProcessMode: "manual", ProcessMode: "manual",
KeepaliveTimeout: 65,
Gzip: "off",
GzipMinLength: 1,
GzipCompLevel: 1,
ClientMaxBodySize: "1m",
ServerNamesHashBucketSize: 32,
ClientHeaderBufferSize: "1k",
ClientBodyBufferSize: "8k",
} }
// Get worker_processes config confPath := GetConfPath("nginx.conf")
cmd := exec.Command("nginx", "-T") if confPath == "" {
output, err := cmd.CombinedOutput() return nil, errors.New("failed to get nginx.conf path")
if err != nil {
return result, errors.Wrap(err, "failed to get nginx config")
} }
// 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 // Parse worker_processes
wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`) 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" { if matches[1] == "auto" {
result.WorkerProcesses = runtime.NumCPU() result.WorkerProcesses = runtime.NumCPU()
result.ProcessMode = "auto" result.ProcessMode = "auto"
@ -44,9 +66,57 @@ func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
// Parse worker_connections // Parse worker_connections
wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`) 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]) 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 return result, nil
} }

148
internal/nginx/perf_opt.go Normal file
View 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()
})
}