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
globs:
description:
globs: **/**/*.go
alwaysApply: false
---
# Cursor Rules

View file

@ -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
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_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
View file

@ -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']

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package nginx
import (
"os/exec"
"os"
"regexp"
"runtime"
"strconv"
@ -10,29 +10,51 @@ import (
)
type NginxConfigInfo struct {
WorkerProcesses int `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"`
ProcessMode string `json:"process_mode"`
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
func GetNginxWorkerConfigInfo() (*NginxConfigInfo, error) {
result := &NginxConfigInfo{
WorkerProcesses: 1,
WorkerConnections: 1024,
ProcessMode: "manual",
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
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()
})
}