Merge branch 'develop' of git.ailuowan.com:zzw/lone-services into develop
CI / changes (push) Successful in 2s
CI / docker-bff (push) Successful in 18s
CI / docker-product (push) Has been skipped
CI / docker-admin (push) Successful in 3m25s

This commit is contained in:
2026-08-12 10:20:53 +08:00
10 changed files with 390 additions and 494 deletions
+3 -3
View File
@@ -73,7 +73,7 @@ jobs:
- name: Docker build - name: Docker build
run: | run: |
docker build \ docker buildx build --load \
-f bff/Dockerfile \ -f bff/Dockerfile \
-t ${{ vars.REGISTRY }}/$NAME:latest \ -t ${{ vars.REGISTRY }}/$NAME:latest \
. .
@@ -98,7 +98,7 @@ jobs:
- name: Docker build - name: Docker build
run: | run: |
docker build \ docker buildx build --load \
-f product/Dockerfile \ -f product/Dockerfile \
-t ${{ vars.REGISTRY }}/$NAME:latest \ -t ${{ vars.REGISTRY }}/$NAME:latest \
. .
@@ -123,7 +123,7 @@ jobs:
- name: Docker build - name: Docker build
run: | run: |
docker build \ docker buildx build --load \
-f admin/Dockerfile \ -f admin/Dockerfile \
-t ${{ vars.REGISTRY }}/$NAME:latest \ -t ${{ vars.REGISTRY }}/$NAME:latest \
. .
+7 -3
View File
@@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
FROM golang:1.26.5-alpine AS builder FROM golang:1.26.5-alpine AS builder
WORKDIR /src WORKDIR /src
@@ -11,10 +13,13 @@ COPY pkg ./pkg
COPY admin/go.mod admin/go.sum ./admin/ COPY admin/go.mod admin/go.sum ./admin/
WORKDIR /src/admin WORKDIR /src/admin
RUN go mod download RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY admin/ ./ COPY admin/ ./
RUN go build -ldflags="-s -w" -o /out/admin . RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -ldflags="-s -w" -o /out/admin .
FROM alpine:3.22 FROM alpine:3.22
@@ -27,7 +32,6 @@ RUN apk add --no-cache tzdata \
&& echo "Asia/Shanghai" > /etc/timezone && echo "Asia/Shanghai" > /etc/timezone
COPY --from=builder /out/admin . COPY --from=builder /out/admin .
COPY admin/etc/admin.yaml ./etc/admin.yaml
EXPOSE 10200 EXPOSE 10200
+1
View File
@@ -0,0 +1 @@
etc/bff.yaml
+8 -3
View File
@@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
FROM golang:1.26.5-alpine AS builder FROM golang:1.26.5-alpine AS builder
WORKDIR /src WORKDIR /src
@@ -11,10 +13,13 @@ COPY pkg ./pkg
COPY bff/go.mod bff/go.sum ./bff/ COPY bff/go.mod bff/go.sum ./bff/
WORKDIR /src/bff WORKDIR /src/bff
RUN go mod download RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY bff/ ./ COPY bff/ ./
RUN go build -ldflags="-s -w" -o /out/bff . RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -ldflags="-s -w" -o /out/bff .
FROM alpine:3.22 FROM alpine:3.22
@@ -27,7 +32,7 @@ RUN apk add --no-cache tzdata \
&& echo "Asia/Shanghai" > /etc/timezone && echo "Asia/Shanghai" > /etc/timezone
COPY --from=builder /out/bff . COPY --from=builder /out/bff .
COPY bff/etc/ ./etc/ COPY bff/etc/*.pb ./etc/
EXPOSE 10000 EXPOSE 10000
-29
View File
@@ -1,29 +0,0 @@
Name: bff-service
Host: 0.0.0.0
Port: 10000
Nacos:
Hosts:
- rnacos:8848
NamespaceId: test
Group: LONE_SERVICES
RegisterIP: bff
Response:
IsDebug: true # true 不加密
EncryptKey: "M17fcaPeHDr05H17fcaPH1deDH17l3Bd"
Upstreams:
- Name: product
Grpc:
Target: product-service
Timeout: 5000
ProtoSets:
- etc/product.pb
- Name: admin
Grpc:
Target: admin-service
Timeout: 5000
ProtoSets:
- etc/admin.pb
-30
View File
@@ -1,30 +0,0 @@
services:
rnacos:
image: qingpan/rnacos:stable
restart: unless-stopped
ports:
- "8848:8848"
volumes:
- ./rnacos/data:/app/data
environment:
RNACOS_SERVER_PORT: 8848
RNACOS_ENABLE_NO_AUTH_CONSOLE: "true"
apisix:
image: apache/apisix:3.17.0-debian
restart: unless-stopped
volumes:
- ./apisix/config.yaml:/usr/local/apisix/conf/config.yaml:ro
- ./apisix/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro
# 单文件挂载到 /usr/local/apisix/auth.lua
- ./apisix/lua/auth.lua:/usr/local/apisix/auth.lua:ro
ports:
- "9080:9080"
- "9443:9443"
networks:
- default
networks:
default:
external: true
name: services-network
+363 -363
View File
@@ -1,363 +1,363 @@
package discovery package discovery
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
type Config struct { type Config struct {
Hosts []string Hosts []string
NamespaceId string NamespaceId string
Group string Group string
} }
type Instance struct { type Instance struct {
ServiceName string ServiceName string
IP string IP string
Port uint64 Port uint64
Group string Group string
} }
type ServiceInstance struct { type ServiceInstance struct {
IP string IP string
Port uint64 Port uint64
ServiceName string ServiceName string
Healthy bool Healthy bool
Weight float64 Weight float64
Enabled bool Enabled bool
Ephemeral bool Ephemeral bool
ClusterName string ClusterName string
Metadata map[string]string Metadata map[string]string
} }
var ( var (
mu sync.Mutex mu sync.Mutex
baseURL string baseURL string
namespace string namespace string
group string group string
httpClient = &http.Client{Timeout: 5 * time.Second} httpClient = &http.Client{Timeout: 5 * time.Second}
current Instance current Instance
inited bool inited bool
registered bool registered bool
stopBeat chan struct{} stopBeat chan struct{}
pickMu sync.Mutex pickMu sync.Mutex
pickRR = map[string]uint64{} pickRR = map[string]uint64{}
) )
func Init(cfg Config) error { func Init(cfg Config) error {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
if inited { if inited {
return nil return nil
} }
if len(cfg.Hosts) == 0 { if len(cfg.Hosts) == 0 {
return fmt.Errorf("discovery: Hosts is empty") return fmt.Errorf("discovery: Hosts is empty")
} }
host, port, err := parseHostPort(cfg.Hosts[0]) host, port, err := parseHostPort(cfg.Hosts[0])
if err != nil { if err != nil {
return err return err
} }
group = cfg.Group group = cfg.Group
if group == "" { if group == "" {
group = "default" group = "default"
} }
namespace = cfg.NamespaceId namespace = cfg.NamespaceId
baseURL = fmt.Sprintf("http://%s:%d", host, port) baseURL = fmt.Sprintf("http://%s:%d", host, port)
inited = true inited = true
return nil return nil
} }
func Register(inst Instance) error { func Register(inst Instance) error {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
if !inited { if !inited {
return fmt.Errorf("discovery: call Init first") return fmt.Errorf("discovery: call Init first")
} }
g := inst.Group g := inst.Group
if g == "" { if g == "" {
g = group g = group
} }
form := url.Values{} form := url.Values{}
form.Set("ip", inst.IP) form.Set("ip", inst.IP)
form.Set("port", strconv.FormatUint(inst.Port, 10)) form.Set("port", strconv.FormatUint(inst.Port, 10))
form.Set("serviceName", joinGroupService(g, inst.ServiceName)) form.Set("serviceName", joinGroupService(g, inst.ServiceName))
form.Set("groupName", g) form.Set("groupName", g)
form.Set("weight", "10") form.Set("weight", "10")
form.Set("enable", "true") form.Set("enable", "true")
form.Set("healthy", "true") form.Set("healthy", "true")
form.Set("ephemeral", "true") form.Set("ephemeral", "true")
form.Set("clusterName", "DEFAULT") form.Set("clusterName", "DEFAULT")
if namespace != "" { if namespace != "" {
form.Set("namespaceId", namespace) form.Set("namespaceId", namespace)
} }
if err := doForm(http.MethodPost, "/nacos/v1/ns/instance", form); err != nil { if err := doForm(http.MethodPost, "/nacos/v1/ns/instance", form); err != nil {
return fmt.Errorf("discovery: register: %w", err) return fmt.Errorf("discovery: register: %w", err)
} }
current = inst current = inst
current.Group = g current.Group = g
registered = true registered = true
if stopBeat != nil { if stopBeat != nil {
close(stopBeat) close(stopBeat)
} }
stopBeat = make(chan struct{}) stopBeat = make(chan struct{})
go heartbeatLoop(stopBeat, current) go heartbeatLoop(stopBeat, current)
return nil return nil
} }
func Deregister() error { func Deregister() error {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
if !inited || !registered { if !inited || !registered {
return nil return nil
} }
if stopBeat != nil { if stopBeat != nil {
close(stopBeat) close(stopBeat)
stopBeat = nil stopBeat = nil
} }
form := url.Values{} form := url.Values{}
form.Set("ip", current.IP) form.Set("ip", current.IP)
form.Set("port", strconv.FormatUint(current.Port, 10)) form.Set("port", strconv.FormatUint(current.Port, 10))
form.Set("serviceName", joinGroupService(current.Group, current.ServiceName)) form.Set("serviceName", joinGroupService(current.Group, current.ServiceName))
form.Set("groupName", current.Group) form.Set("groupName", current.Group)
form.Set("clusterName", "DEFAULT") form.Set("clusterName", "DEFAULT")
form.Set("ephemeral", "true") form.Set("ephemeral", "true")
if namespace != "" { if namespace != "" {
form.Set("namespaceId", namespace) form.Set("namespaceId", namespace)
} }
if err := doForm(http.MethodDelete, "/nacos/v1/ns/instance", form); err != nil { if err := doForm(http.MethodDelete, "/nacos/v1/ns/instance", form); err != nil {
return fmt.Errorf("discovery: deregister: %w", err) return fmt.Errorf("discovery: deregister: %w", err)
} }
registered = false registered = false
return nil return nil
} }
func GetInstances(serviceName string, groupName ...string) ([]ServiceInstance, error) { func GetInstances(serviceName string, groupName ...string) ([]ServiceInstance, error) {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
if !inited { if !inited {
return nil, fmt.Errorf("discovery: call Init first") return nil, fmt.Errorf("discovery: call Init first")
} }
g := group g := group
if len(groupName) > 0 && groupName[0] != "" { if len(groupName) > 0 && groupName[0] != "" {
g = groupName[0] g = groupName[0]
} }
q := url.Values{} q := url.Values{}
q.Set("serviceName", joinGroupService(g, serviceName)) q.Set("serviceName", joinGroupService(g, serviceName))
q.Set("groupName", g) q.Set("groupName", g)
q.Set("healthyOnly", "true") q.Set("healthyOnly", "true")
if namespace != "" { if namespace != "" {
q.Set("namespaceId", namespace) q.Set("namespaceId", namespace)
} }
body, err := doGet("/nacos/v1/ns/instance/list", q) body, err := doGet("/nacos/v1/ns/instance/list", q)
if err != nil { if err != nil {
return nil, fmt.Errorf("discovery: select instances: %w", err) return nil, fmt.Errorf("discovery: select instances: %w", err)
} }
var resp listResponse var resp listResponse
if err := json.Unmarshal(body, &resp); err != nil { if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("discovery: decode instances: %w", err) return nil, fmt.Errorf("discovery: decode instances: %w", err)
} }
out := make([]ServiceInstance, 0, len(resp.Hosts)) out := make([]ServiceInstance, 0, len(resp.Hosts))
for _, h := range resp.Hosts { for _, h := range resp.Hosts {
out = append(out, ServiceInstance{ out = append(out, ServiceInstance{
IP: h.IP, IP: h.IP,
Port: uint64(h.Port), Port: uint64(h.Port),
ServiceName: h.ServiceName, ServiceName: h.ServiceName,
Healthy: h.Healthy, Healthy: h.Healthy,
Weight: h.Weight, Weight: h.Weight,
Enabled: h.Enabled, Enabled: h.Enabled,
Ephemeral: h.Ephemeral, Ephemeral: h.Ephemeral,
ClusterName: h.ClusterName, ClusterName: h.ClusterName,
Metadata: h.Metadata, Metadata: h.Metadata,
}) })
} }
return out, nil return out, nil
} }
// Pick returns one healthy instance for serviceName using round-robin. // Pick returns one healthy instance for serviceName using round-robin.
func Pick(serviceName string, groupName ...string) (*ServiceInstance, error) { func Pick(serviceName string, groupName ...string) (*ServiceInstance, error) {
list, err := GetInstances(serviceName, groupName...) list, err := GetInstances(serviceName, groupName...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(list) == 0 { if len(list) == 0 {
return nil, fmt.Errorf("discovery: no healthy instance for %q", serviceName) return nil, fmt.Errorf("discovery: no healthy instance for %q", serviceName)
} }
pickMu.Lock() pickMu.Lock()
idx := pickRR[serviceName] % uint64(len(list)) idx := pickRR[serviceName] % uint64(len(list))
pickRR[serviceName]++ pickRR[serviceName]++
pickMu.Unlock() pickMu.Unlock()
inst := list[idx] inst := list[idx]
return &inst, nil return &inst, nil
} }
type listResponse struct { type listResponse struct {
Hosts []hostInfo `json:"hosts"` Hosts []hostInfo `json:"hosts"`
} }
type hostInfo struct { type hostInfo struct {
IP string `json:"ip"` IP string `json:"ip"`
Port float64 `json:"port"` Port float64 `json:"port"`
ServiceName string `json:"serviceName"` ServiceName string `json:"serviceName"`
Healthy bool `json:"healthy"` Healthy bool `json:"healthy"`
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Ephemeral bool `json:"ephemeral"` Ephemeral bool `json:"ephemeral"`
ClusterName string `json:"clusterName"` ClusterName string `json:"clusterName"`
Metadata map[string]string `json:"metadata"` Metadata map[string]string `json:"metadata"`
} }
type beatResponse struct { type beatResponse struct {
ClientBeatInterval int64 `json:"clientBeatInterval"` ClientBeatInterval int64 `json:"clientBeatInterval"`
} }
func heartbeatLoop(stop <-chan struct{}, inst Instance) { func heartbeatLoop(stop <-chan struct{}, inst Instance) {
interval := 5 * time.Second interval := 5 * time.Second
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-stop: case <-stop:
return return
case <-ticker.C: case <-ticker.C:
if d := sendBeat(inst); d > 0 { if d := sendBeat(inst); d > 0 {
ticker.Reset(d) ticker.Reset(d)
} }
} }
} }
} }
func sendBeat(inst Instance) time.Duration { func sendBeat(inst Instance) time.Duration {
groupedName := joinGroupService(inst.Group, inst.ServiceName) groupedName := joinGroupService(inst.Group, inst.ServiceName)
beat, _ := json.Marshal(map[string]any{ beat, _ := json.Marshal(map[string]any{
"ip": inst.IP, "ip": inst.IP,
"port": inst.Port, "port": inst.Port,
"serviceName": groupedName, "serviceName": groupedName,
"weight": 10, "weight": 10,
"ephemeral": true, "ephemeral": true,
"cluster": "DEFAULT", "cluster": "DEFAULT",
}) })
q := url.Values{} q := url.Values{}
q.Set("serviceName", groupedName) q.Set("serviceName", groupedName)
q.Set("groupName", inst.Group) q.Set("groupName", inst.Group)
q.Set("ip", inst.IP) q.Set("ip", inst.IP)
q.Set("port", strconv.FormatUint(inst.Port, 10)) q.Set("port", strconv.FormatUint(inst.Port, 10))
q.Set("beat", string(beat)) q.Set("beat", string(beat))
if namespace != "" { if namespace != "" {
q.Set("namespaceId", namespace) q.Set("namespaceId", namespace)
} }
req, err := http.NewRequest(http.MethodPut, baseURL+"/nacos/v1/ns/instance/beat?"+q.Encode(), nil) req, err := http.NewRequest(http.MethodPut, baseURL+"/nacos/v1/ns/instance/beat?"+q.Encode(), nil)
if err != nil { if err != nil {
return 0 return 0
} }
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return 0 return 0
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 { if resp.StatusCode >= 300 {
return 0 return 0
} }
var br beatResponse var br beatResponse
if json.Unmarshal(body, &br) == nil && br.ClientBeatInterval > 0 { if json.Unmarshal(body, &br) == nil && br.ClientBeatInterval > 0 {
return time.Duration(br.ClientBeatInterval) * time.Millisecond return time.Duration(br.ClientBeatInterval) * time.Millisecond
} }
return 0 return 0
} }
func doForm(method, path string, form url.Values) error { func doForm(method, path string, form url.Values) error {
req, err := http.NewRequest(method, baseURL+path, strings.NewReader(form.Encode())) req, err := http.NewRequest(method, baseURL+path, strings.NewReader(form.Encode()))
if err != nil { if err != nil {
return err return err
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 { if resp.StatusCode >= 300 {
return fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) return fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
} }
if s := strings.TrimSpace(string(body)); s != "" && s != "ok" && s != "true" { if s := strings.TrimSpace(string(body)); s != "" && s != "ok" && s != "true" {
// rnacos / nacos 成功时通常返回 "ok" // rnacos / nacos 成功时通常返回 "ok"
var ok bool var ok bool
if err := json.Unmarshal(body, &ok); err == nil && ok { if err := json.Unmarshal(body, &ok); err == nil && ok {
return nil return nil
} }
if s != "ok" { if s != "ok" {
return fmt.Errorf("unexpected response: %s", s) return fmt.Errorf("unexpected response: %s", s)
} }
} }
return nil return nil
} }
func doGet(path string, q url.Values) ([]byte, error) { func doGet(path string, q url.Values) ([]byte, error) {
resp, err := httpClient.Get(baseURL + path + "?" + q.Encode()) resp, err := httpClient.Get(baseURL + path + "?" + q.Encode())
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resp.StatusCode >= 300 { if resp.StatusCode >= 300 {
return nil, fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) return nil, fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
} }
return body, nil return body, nil
} }
func parseHostPort(addr string) (string, uint64, error) { func parseHostPort(addr string) (string, uint64, error) {
host, portStr, ok := strings.Cut(addr, ":") host, portStr, ok := strings.Cut(addr, ":")
if !ok || host == "" || portStr == "" { if !ok || host == "" || portStr == "" {
return "", 0, fmt.Errorf("discovery: invalid host %q, want host:port", addr) return "", 0, fmt.Errorf("discovery: invalid host %q, want host:port", addr)
} }
port, err := strconv.ParseUint(portStr, 10, 64) port, err := strconv.ParseUint(portStr, 10, 64)
if err != nil { if err != nil {
return "", 0, fmt.Errorf("discovery: invalid port in %q: %w", addr, err) return "", 0, fmt.Errorf("discovery: invalid port in %q: %w", addr, err)
} }
return host, port, nil return host, port, nil
} }
func joinGroupService(groupName, serviceName string) string { func joinGroupService(groupName, serviceName string) string {
if groupName == "" || strings.Contains(serviceName, "@@") { if groupName == "" || strings.Contains(serviceName, "@@") {
return serviceName return serviceName
} }
return groupName + "@@" + serviceName return groupName + "@@" + serviceName
} }
+1
View File
@@ -0,0 +1 @@
etc/product.yaml
+7 -3
View File
@@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
FROM golang:1.26.5-alpine AS builder FROM golang:1.26.5-alpine AS builder
WORKDIR /src WORKDIR /src
@@ -11,10 +13,13 @@ COPY pkg ./pkg
COPY product/go.mod product/go.sum ./product/ COPY product/go.mod product/go.sum ./product/
WORKDIR /src/product WORKDIR /src/product
RUN go mod download RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY product/ ./ COPY product/ ./
RUN go build -ldflags="-s -w" -o /out/product . RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -ldflags="-s -w" -o /out/product .
FROM alpine:3.22 FROM alpine:3.22
@@ -27,7 +32,6 @@ RUN apk add --no-cache tzdata \
&& echo "Asia/Shanghai" > /etc/timezone && echo "Asia/Shanghai" > /etc/timezone
COPY --from=builder /out/product . COPY --from=builder /out/product .
COPY product/etc/product.yaml ./etc/product.yaml
EXPOSE 10100 EXPOSE 10100
-60
View File
@@ -1,60 +0,0 @@
Name: product-service
ListenOn: 0.0.0.0:10100
Mode: dev
Log:
ServiceName: product-service
Mode: file # console | file | volume
Encoding: plain
Level: info
Path: logs
KeepDays: 7
Compress: false
# Rotation: daily
# MaxSize: 100
# MaxBackups: 10
Nacos:
Hosts:
- rnacos:8848
NamespaceId: test
Group: LONE_SERVICES
RegisterIP: product
Mysql:
Host: mysql
Port: 3306
User: root
Password: "123123"
Database: dms-product
Charset: utf8mb4
Prefix:
# Host: "39.106.171.204"
# Port: 33066
# User: root
# Password: "MOLXRZNOU4Y4"
# Database: dms-product
# Charset: utf8mb4
# Prefix:
# ReadHost: mysql-slave
# ReadPort: 3306
# ReadUser: root
# ReadPassword: "123123"
# ReadDatabase: dms-product
BizRedis:
Host: redis
Port: 6379
Password: ""
DB: 0
AppLog:
Path: logs
InfoFile: info.log
ErrorFile: error.log
FatalFile: fatal.log
MaxSize: 100
MaxBackups: 10
MaxAge: 30