feat: Website Load balancer config support fail_timeout (#10796)

Refs https://github.com/1Panel-dev/1Panel/issues/10565
This commit is contained in:
CityFun
2025-10-28 08:28:16 +00:00
committed by GitHub
parent aa55a18ed8
commit fb6b2729c6
16 changed files with 317 additions and 166 deletions
+7 -6
View File
@@ -73,12 +73,13 @@ type NginxUpstream struct {
}
type NginxUpstreamServer struct {
Server string `json:"server"`
Weight int `json:"weight"`
FailTimeout string `json:"failTimeout"`
MaxFails int `json:"maxFails"`
MaxConns int `json:"maxConns"`
Flag string `json:"flag"`
Server string `json:"server"`
Weight int `json:"weight"`
FailTimeout int `json:"failTimeout"`
FailTimeoutUnit string `json:"failTimeoutUnit"`
MaxFails int `json:"maxFails"`
MaxConns int `json:"maxConns"`
Flag string `json:"flag"`
}
var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
+3 -85
View File
@@ -3094,39 +3094,7 @@ func (w WebsiteService) GetLoadBalances(id uint) ([]dto.NginxUpstream, error) {
upstream.Algorithm = dName
}
}
var servers []dto.NginxUpstreamServer
for _, ups := range up.UpstreamServers {
server := dto.NginxUpstreamServer{
Server: ups.Address,
}
parameters := ups.Parameters
if weight, ok := parameters["weight"]; ok {
num, err := strconv.Atoi(weight)
if err == nil {
server.Weight = num
}
}
if maxFails, ok := parameters["max_fails"]; ok {
num, err := strconv.Atoi(maxFails)
if err == nil {
server.MaxFails = num
}
}
if failTimeout, ok := parameters["fail_timeout"]; ok {
server.FailTimeout = failTimeout
}
if maxConns, ok := parameters["max_conns"]; ok {
num, err := strconv.Atoi(maxConns)
if err == nil {
server.MaxConns = num
}
}
for _, flag := range ups.Flags {
server.Flag = flag
}
servers = append(servers, server)
}
upstream.Servers = servers
upstream.Servers = getNginxUpstreamServers(up.UpstreamServers)
}
}
res = append(res, upstream)
@@ -3160,33 +3128,7 @@ func (w WebsiteService) CreateLoadBalance(req request.WebsiteLBCreate) error {
if req.Algorithm != "default" {
upstream.UpdateDirective(req.Algorithm, []string{})
}
servers := make([]*components.UpstreamServer, 0)
for _, server := range req.Servers {
upstreamServer := &components.UpstreamServer{
Address: server.Server,
}
parameters := make(map[string]string)
if server.Weight > 0 {
parameters["weight"] = strconv.Itoa(server.Weight)
}
if server.MaxFails > 0 {
parameters["max_fails"] = strconv.Itoa(server.MaxFails)
}
if server.FailTimeout != "" {
parameters["fail_timeout"] = server.FailTimeout
}
if server.MaxConns > 0 {
parameters["max_conns"] = strconv.Itoa(server.MaxConns)
}
if server.Flag != "" {
upstreamServer.Flags = []string{server.Flag}
}
upstreamServer.Parameters = parameters
servers = append(servers, upstreamServer)
}
upstream.UpstreamServers = servers
upstream.UpstreamServers = parseUpstreamServers(req.Servers)
config.Block.Directives = append(config.Block.Directives, &upstream)
defer func() {
@@ -3245,31 +3187,7 @@ func (w WebsiteService) UpdateLoadBalance(req request.WebsiteLBUpdate) error {
if req.Algorithm != "default" {
up.UpdateDirective(req.Algorithm, []string{})
}
var servers []*components.UpstreamServer
for _, server := range req.Servers {
upstreamServer := &components.UpstreamServer{
Address: server.Server,
}
parameters := make(map[string]string)
if server.Weight > 0 {
parameters["weight"] = strconv.Itoa(server.Weight)
}
if server.MaxFails > 0 {
parameters["max_fails"] = strconv.Itoa(server.MaxFails)
}
if server.FailTimeout != "" {
parameters["fail_timeout"] = server.FailTimeout
}
if server.MaxConns > 0 {
parameters["max_conns"] = strconv.Itoa(server.MaxConns)
}
if server.Flag != "" {
upstreamServer.Flags = []string{server.Flag}
}
upstreamServer.Parameters = parameters
servers = append(servers, upstreamServer)
}
up.UpstreamServers = servers
up.UpstreamServers = parseUpstreamServers(req.Servers)
}
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
+87
View File
@@ -9,6 +9,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
@@ -1533,3 +1534,89 @@ func getServer(website model.Website) (*components.Server, error) {
server := servers[0]
return server, nil
}
func parseTimeString(input string) (int, string, error) {
input = strings.TrimSpace(input)
re := regexp.MustCompile(`^(\d+)([smhdw]?)$`)
matches := re.FindStringSubmatch(input)
if len(matches) < 2 {
return 0, "", fmt.Errorf("invalid time format: %s", input)
}
value, err := strconv.Atoi(matches[1])
if err != nil {
return 0, "", fmt.Errorf("invalid number: %s", matches[1])
}
unit := matches[2]
if unit == "" {
unit = "s"
}
return value, unit, nil
}
func parseUpstreamServers(reqServers []dto.NginxUpstreamServer) []*components.UpstreamServer {
var servers []*components.UpstreamServer
for _, server := range reqServers {
upstreamServer := &components.UpstreamServer{
Address: server.Server,
}
parameters := make(map[string]string)
if server.Weight > 0 {
parameters["weight"] = strconv.Itoa(server.Weight)
}
if server.MaxFails > 0 {
parameters["max_fails"] = strconv.Itoa(server.MaxFails)
}
if server.FailTimeout > 0 {
parameters["fail_timeout"] = fmt.Sprintf("%d%s", server.FailTimeout, server.FailTimeoutUnit)
}
if server.MaxConns > 0 {
parameters["max_conns"] = strconv.Itoa(server.MaxConns)
}
if server.Flag != "" {
upstreamServer.Flags = []string{server.Flag}
}
upstreamServer.Parameters = parameters
servers = append(servers, upstreamServer)
}
return servers
}
func getNginxUpstreamServers(upstreamServers []*components.UpstreamServer) []dto.NginxUpstreamServer {
var servers []dto.NginxUpstreamServer
for _, ups := range upstreamServers {
server := dto.NginxUpstreamServer{
Server: ups.Address,
}
parameters := ups.Parameters
if weight, ok := parameters["weight"]; ok {
num, err := strconv.Atoi(weight)
if err == nil {
server.Weight = num
}
}
if maxFails, ok := parameters["max_fails"]; ok {
num, err := strconv.Atoi(maxFails)
if err == nil {
server.MaxFails = num
}
}
if failTimeout, ok := parameters["fail_timeout"]; ok {
server.FailTimeout, server.FailTimeoutUnit, _ = parseTimeString(failTimeout)
}
if maxConns, ok := parameters["max_conns"]; ok {
num, err := strconv.Atoi(maxConns)
if err == nil {
server.MaxConns = num
}
}
for _, flag := range ups.Flags {
server.Flag = flag
}
servers = append(servers, server)
}
return servers
}
+2 -1
View File
@@ -607,7 +607,8 @@ export namespace Website {
interface NginxUpstreamServer {
server: string;
weight: number;
failTimeout: string;
failTimeout: number;
failTimeoutUnit: string;
maxFails: number;
maxConns: number;
flag: string;
+3
View File
@@ -2600,6 +2600,9 @@ const message = {
strategyDown: 'Down',
strategyBackup: 'Backup',
ipHashBackupErr: 'IP hash does not support backup nodes',
failTimeout: 'Failure timeout',
failTimeoutHelper:
'The time window length for server health checks. When the cumulative number of failures reaches the threshold within this period, the server will be temporarily removed and retried after the same duration. Default 10 seconds',
staticChangePHPHelper: 'Currently a static website, you can switch to a PHP website',
proxyCache: 'Reverse Proxy Cache',
+3
View File
@@ -2585,6 +2585,9 @@ const message = {
strategyDown: 'Baja',
strategyBackup: 'Backup',
ipHashBackupErr: 'IP hash does not support backup nodes',
failTimeout: 'Tiempo de espera de fallo',
failTimeoutHelper:
'La duración de la ventana de tiempo para las comprobaciones de estado del servidor. Cuando el número acumulado de fallos alcanza el umbral dentro de este período, el servidor se eliminará temporalmente y se reintentará después de la misma duración. Por defecto 10 segundos',
staticChangePHPHelper: 'Actualmente es un sitio estático, puedes cambiarlo a PHP',
proxyCache: 'Caché de proxy inverso',
+3
View File
@@ -2517,6 +2517,9 @@ const message = {
strategyDown: '無効',
strategyBackup: 'バックアップ',
ipHashBackupErr: 'IPハッシュはバックアップノードをサポートしていません',
failTimeout: '障害タイムアウト',
failTimeoutHelper:
'サーバーのヘルスチェックの時間ウィンドウの長さこの期間内に累積障害回数がしきい値に達するとサーバーは一時的に削除され同じ時間経過後に再試行されますデフォルト 10 ',
staticChangePHPHelper: '現在は静的ウェブサイトですがPHPウェブサイトに切り替えることができます',
proxyCache: 'リバースプロキシキャッシュ',
+3
View File
@@ -2473,6 +2473,9 @@ const message = {
strategyDown: '비활성화',
strategyBackup: '백업',
ipHashBackupErr: 'IP 해시는 백업 노드를 지원하지 않습니다',
failTimeout: '장애 시간 초과',
failTimeoutHelper:
'서버 상태 점검 시간 창 길이. 이 기간 동안 누적 실패 횟수가 임계값에 도달하면 서버가 일시적으로 제거되고 동일한 시간 후에 재시도됩니다. 기본값 10초',
staticChangePHPHelper: '현재 정적 웹사이트이며 PHP 웹사이트로 전환할 수 있습니다.',
proxyCache: '리버스 프록시 캐시',
+3
View File
@@ -2577,6 +2577,9 @@ const message = {
strategyDown: 'Lumpuh',
strategyBackup: 'Sandaran',
ipHashBackupErr: 'Hash IP tidak menyokong nod sandaran',
failTimeout: 'Masa tamat kegagalan',
failTimeoutHelper:
'Panjang tetingkap masa untuk pemeriksaan kesihatan pelayan. Apabila bilangan kegagalan terkumpul mencapai ambang dalam tempoh ini, pelayan akan dikeluarkan buat sementara waktu dan dicuba semula selepas tempoh yang sama. Lalai 10 saat',
staticChangePHPHelper: 'Kini laman web statik, boleh ditukar ke laman web PHP.',
proxyCache: 'Cache Proksi Terbalik',
+3
View File
@@ -2581,6 +2581,9 @@ const message = {
strategyDown: 'Desativar',
strategyBackup: 'Backup',
ipHashBackupErr: 'Hash IP não suporta nós de backup',
failTimeout: 'Tempo limite de falha',
failTimeoutHelper:
'O comprimento da janela de tempo para verificações de integridade do servidor. Quando o número acumulado de falhas atinge o limite dentro deste período, o servidor será removido temporariamente e repetido após a mesma duração. Padrão 10 segundos',
staticChangePHPHelper: 'Atualmente um site estático, pode ser alterado para um site PHP.',
proxyCache: 'Cache de Proxy Reverso',
+3
View File
@@ -2578,6 +2578,9 @@ const message = {
strategyDown: 'Отключить',
strategyBackup: 'Резервный',
ipHashBackupErr: 'IP хэш не поддерживает резервные узлы',
failTimeout: 'Таймаут отказа',
failTimeoutHelper:
'Длина временного окна для проверки работоспособности сервера. Когда кумулятивное количество отказов достигает порога в течение этого периода, сервер будет временно удален и повторно проверен через тот же промежуток времени. По умолчанию 10 секунд',
staticChangePHPHelper: 'В настоящее время статический сайт, можно переключить на PHP сайт.',
proxyCache: 'Кэш Обратного Прокси',
+3
View File
@@ -2634,6 +2634,9 @@ const message = {
strategyDown: 'Kapalı',
strategyBackup: 'Yedek',
ipHashBackupErr: 'IP 哈希不支持备用节点',
failTimeout: 'Hata zaman aşımı',
failTimeoutHelper:
'Sunucu sağlık kontrolleri için zaman penceresi uzunluğu. Bu süre içinde biriken hata sayısı eşiğe ulaştığında, sunucu geçici olarak kaldırılacak ve aynı süre sonunda yeniden denenir. Varsayılan 10 saniye',
staticChangePHPHelper: 'Şu anda statik bir web sitesi, PHP web sitesine geçiş yapabilirsiniz',
proxyCache: 'Ters Vekil Önbelleği',
+3
View File
@@ -2418,6 +2418,9 @@ const message = {
strategyDown: '停用',
strategyBackup: '備用',
ipHashBackupErr: 'IP 雜湊不支援備用節點',
failTimeout: '故障超時',
failTimeoutHelper:
'服務器健康檢查的時間窗口長度在該時間段內累計失敗次數達到閾值時服務器將被暫時移除並在經過相同時長後重新嘗試默認 10 ',
staticChangePHPHelper: '目前為靜態網站可切換為 PHP 網站',
proxyCache: '反向代理快取',
+3
View File
@@ -2411,6 +2411,9 @@ const message = {
strategyDown: '停用',
strategyBackup: '备用',
ipHashBackupErr: 'IP 哈希不支持备用节点',
failTimeout: '故障超时',
failTimeoutHelper:
'服务器健康检查的时间窗口长度在该时间段内累计失败次数达到阈值时服务器将被暂时移除并在经过相同时长后重新尝试默认 10 ',
staticChangePHPHelper: '当前为静态网站可以切换为 PHP 网站',
proxyCache: '反代缓存',
@@ -30,7 +30,9 @@
<el-tag type="success">{{ $t('website.weight') }}: {{ item.weight }}</el-tag>
</td>
<td v-if="item.failTimeout != ''">
<el-tag type="warning">{{ $t('website.failTimeout') }}: {{ item.failTimeout }}</el-tag>
<el-tag type="warning">
{{ $t('website.failTimeout') }}: {{ item.failTimeout }} {{ item.failTimeoutUnit }}
</el-tag>
</td>
<td v-if="item.maxFails > 0">
<el-tag type="danger">{{ $t('website.maxFails') }}: {{ item.maxFails }}</el-tag>
@@ -21,78 +21,126 @@
</el-select>
<span class="input-help">{{ getHelper(item.algorithm) }}</span>
</el-form-item>
<el-row :gutter="20" v-for="(server, index) of item.servers" :key="index">
<el-col :span="7">
<el-form-item
:label="index == 0 ? $t('setting.address') : ''"
:prop="`servers.${index}.server`"
:rules="rules.server"
>
<el-input
type="string"
v-model="item.servers[index].server"
:placeholder="index > 0 ? $t('setting.address') : ''"
></el-input>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item
:label="index == 0 ? $t('website.weight') : ''"
:prop="`servers.${index}.weight`"
:rules="rules.weight"
>
<el-input type="number" v-model.number="item.servers[index].weight"></el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item
:label="index == 0 ? $t('website.maxFails') : ''"
:prop="`servers.${index}.maxFails`"
:rules="rules.maxFails"
>
<el-input type="number" v-model.number="item.servers[index].maxFails"></el-input>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item
:label="index == 0 ? $t('website.maxConns') : ''"
:prop="`servers.${index}.maxConns`"
:rules="rules.maxConns"
>
<el-input type="number" v-model.number="item.servers[index].maxConns"></el-input>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item
:label="index == 0 ? $t('website.strategy') : ''"
:prop="`servers.${index}.flag`"
:rules="rules.flag"
>
<el-select v-model="item.servers[index].flag" clearable>
<el-option
v-for="flag in getStatusStrategy()"
:label="flag.label"
:key="flag.value"
:value="flag.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="3" v-if="index == 0">
<el-form-item :label="$t('commons.button.add') + $t('website.server')">
<el-button @click="addServer">
<el-icon><Plus /></el-icon>
</el-button>
</el-form-item>
</el-col>
<el-col :span="3" v-else>
<el-form-item>
<el-button @click="removeServer(index)" link type="primary">
<el-icon><Delete /></el-icon>
</el-button>
</el-form-item>
</el-col>
</el-row>
<div>
<el-card v-for="(server, index) of item.servers" :key="index" class="server-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="server-title">
<el-icon><Monitor /></el-icon>
<span class="ml-2">{{ $t('website.server') }} - {{ server.server }}</span>
</span>
<el-button
v-if="item.servers.length > 1"
text
type="danger"
icon="Delete"
size="small"
@click="removeServer(index)"
></el-button>
</div>
</template>
<div>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item
:label="$t('setting.address')"
:prop="`servers.${index}.server`"
:rules="rules.server"
>
<el-input
v-model="item.servers[index].server"
:placeholder="'example.com:8080'"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.weight')"
:prop="`servers.${index}.weight`"
:rules="rules.weight"
>
<el-input type="number" v-model.number="item.servers[index].weight"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.strategy')"
:prop="`servers.${index}.flag`"
:rules="rules.flag"
>
<el-select v-model="item.servers[index].flag" clearable>
<el-option
v-for="flag in getStatusStrategy()"
:label="flag.label"
:key="flag.value"
:value="flag.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.maxFails')"
:prop="`servers.${index}.maxFails`"
:rules="rules.maxFails"
>
<el-input type="number" v-model.number="item.servers[index].maxFails">
<template #append>{{ $t('commons.units.time') }}</template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item :prop="`servers.${index}.failTimeout`" :rules="rules.failTimeout">
<template #label>
<span class="inline-flex items-center">
{{ $t('website.failTimeout') }}
<el-tooltip :content="$t('website.failTimeoutHelper')" placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</span>
</template>
<el-input type="number" v-model.number="item.servers[index].failTimeout">
<template #append>
<el-select
v-model.number="item.servers[index].failTimeoutUnit"
class="!w-24"
>
<el-option
v-for="(unit, index) in Units"
:key="index"
:label="unit.label"
:value="unit.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.maxConns')"
:prop="`servers.${index}.maxConns`"
:rules="rules.maxConns"
>
<el-input type="number" v-model.number="item.servers[index].maxConns"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<el-button class="add-server-btn" type="primary" plain @click="addServer" icon="Plus">
{{ $t('commons.button.add') + $t('website.server') }}
</el-button>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -114,6 +162,8 @@ import { MsgError, MsgSuccess } from '@/utils/message';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { getAlgorithms, getStatusStrategy } from '@/global/mimetype';
import { Website } from '@/api/interface/website';
import { Monitor, QuestionFilled } from '@element-plus/icons-vue';
import { Units } from '@/global/mimetype';
const rules = ref<any>({
name: [Rules.appName],
@@ -124,7 +174,8 @@ const rules = ref<any>({
type: Array,
},
maxFails: [checkNumberRange(1, 1000)],
maxConns: [checkNumberRange(1, 1000)],
maxConns: [checkNumberRange(0, 10000)],
failTimeout: [checkNumberRange(1, 300)],
});
interface LoadBalanceOperate {
@@ -137,6 +188,12 @@ const lbForm = ref<FormInstance>();
const initServer = () => ({
server: '',
weight: undefined,
maxFails: undefined,
maxConns: undefined,
failTimeout: undefined,
failTimeoutUnit: 's',
flag: '',
});
const open = ref(false);
@@ -188,11 +245,15 @@ const acceptParams = async (req: LoadBalanceOperate) => {
const weight = server.weight == 0 ? undefined : server.weight;
const maxFails = server.maxFails == 0 ? undefined : server.maxFails;
const maxConns = server.maxConns == 0 ? undefined : server.maxConns;
const failTimeout = server.failTimeout == 0 ? undefined : server.failTimeout;
const failTimeoutUnit = server.failTimeoutUnit || 's';
servers.push({
server: server.server,
weight: weight,
maxFails: maxFails,
maxConns: maxConns,
failTimeout: failTimeout,
failTimeoutUnit: failTimeoutUnit,
flag: server.flag,
});
});
@@ -215,6 +276,9 @@ const handleServers = () => {
if (!server.maxConns || server.maxConns == '') {
server.maxConns = 0;
}
if (!server.failTimeout || server.failTimeout == '') {
server.failTimeout = 0;
}
}
};
@@ -229,6 +293,9 @@ const rollBackServers = () => {
if (server.maxConns == 0) {
server.maxConns = undefined;
}
if (server.failTimeout == 0) {
server.failTimeout = undefined;
}
}
};
@@ -271,3 +338,48 @@ defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.server-card {
margin-bottom: 16px;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: var(--el-color-primary);
}
:deep(.el-card__header) {
padding: 12px 20px;
background-color: var(--el-fill-color-light);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 20px;
.server-title {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
color: var(--el-text-color-primary);
}
}
.add-server-btn {
width: 100%;
height: 48px;
border-style: dashed;
font-size: 14px;
margin-top: 8px;
&:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
</style>