主要优化点:
Some checks failed
Test mosdns / build (push) Has been cancelled

1. 连接管理优化 mikrotik_addresslist_impl.go:132
    - 添加连接状态管理和重连锁机制
    - 改进重连逻辑,防止并发重连
  2. 缓存机制增强 mikrotik_addresslist_impl.go:162-202
    - 优化缓存锁使用,避免死锁
    - 添加缓存大小限制和LRU驱逐策略
    - 定期清理过期缓存项
  3. 智能重试机制 mikrotik_addresslist_impl.go:420
    - 指数退避算法
    - 更智能的连接错误识别
    - 改进的错误处理
  4. 动态并发控制 mikrotik_addresslist_impl.go:589
    - 根据地址数量动态调整工作池大小
    - 批量处理优化
  5. 性能监控改进
    - 更详细的日志记录
    - 缓存统计信息
    - 处理过程可观察性
This commit is contained in:
dengxiongjian 2025-08-04 09:02:30 +08:00
parent eb82f1c2f5
commit 59a5ef4aae
4 changed files with 255 additions and 175 deletions

View File

@ -55,3 +55,4 @@ plugins:
mask4: 24 mask4: 24
comment: "amazon_domain" comment: "amazon_domain"
timeout_addr: 86400 timeout_addr: 86400
cache_ttl: 86400

View File

@ -1,104 +0,0 @@
# MikroTik Address List 插件配置示例
# 插件定义
plugins:
# 转发插件 - 向上游 DNS 服务器查询
- tag: forward_google
type: forward
args:
upstream:
- addr: "8.8.8.8:53"
- addr: "8.8.4.4:53"
# MikroTik Address List 插件 - 将解析的 IP 添加到 MikroTik
- tag: mikrotik_blocklist
type: mikrotik_addresslist
args: "192.168.1.1:8728:admin:password:false:10:blocked_ips:blocked_ips6:24:32:blocked_domain:86400"
# 序列插件 - 组合多个插件
- tag: sequence_with_blocklist
type: sequence
args:
- exec: forward_google
- exec: mikrotik_blocklist
# 服务器配置
servers:
# UDP 服务器
- exec: sequence_with_blocklist
args:
- sequence_with_blocklist
listeners:
- protocol: udp
addr: ":53"
# TCP 服务器
- exec: sequence_with_blocklist
args:
- sequence_with_blocklist
listeners:
- protocol: tcp
addr: ":53"
# 日志配置
log:
level: info
file: "mosdns.log"
# 其他配置示例
# 1. 使用 YAML 格式的详细配置
plugins:
- tag: mikrotik_detailed
type: mikrotik_addresslist
args:
host: "192.168.1.1"
port: 8728
username: "admin"
password: "password"
use_tls: false
timeout: 10
address_list4: "blocked_ips"
address_list6: "blocked_ips6"
mask4: 24
mask6: 32
comment: "blocked_domain"
timeout_addr: 86400
# 2. 多个 address list 配置
plugins:
# 恶意域名列表
- tag: mikrotik_malware
type: mikrotik_addresslist
args: "192.168.1.1:8728:admin:password:false:10:malware_ips:malware_ips6:24:32:malware:3600"
# 广告域名列表
- tag: mikrotik_ads
type: mikrotik_addresslist
args: "192.168.1.1:8728:admin:password:false:10:ads_ips:ads_ips6:24:32:ads:7200"
# 组合序列
- tag: sequence_all
type: sequence
args:
- exec: forward_google
- exec: mikrotik_malware
- exec: mikrotik_ads
# 3. 使用 TLS 的安全配置
plugins:
- tag: mikrotik_secure
type: mikrotik_addresslist
args: "192.168.1.1:8729:admin:password:true:15:secure_list:secure_list6:24:32:secure:1800"
# 4. 不同掩码配置
plugins:
# 精确 IP 匹配
- tag: mikrotik_exact
type: mikrotik_addresslist
args: "192.168.1.1:8728:admin:password:false:10:exact_ips:exact_ips6:32:128:exact:3600"
# 网段匹配
- tag: mikrotik_network
type: mikrotik_addresslist
args: "192.168.1.1:8728:admin:password:false:10:network_ips:network_ips6:16:48:network:7200"

View File

@ -49,6 +49,7 @@ type Args struct {
Mask6 int `yaml:"mask6"` // IPv6 掩码,默认 32 Mask6 int `yaml:"mask6"` // IPv6 掩码,默认 32
Comment string `yaml:"comment"` // 添加的地址的注释 Comment string `yaml:"comment"` // 添加的地址的注释
TimeoutAddr int `yaml:"timeout_addr"` // 地址超时时间0 表示永久 TimeoutAddr int `yaml:"timeout_addr"` // 地址超时时间0 表示永久
CacheTTL int `yaml:"cache_ttl"` // 缓存 TTL默认 36001小时
} }
var _ sequence.Executable = (*mikrotikAddressListPlugin)(nil) var _ sequence.Executable = (*mikrotikAddressListPlugin)(nil)
@ -58,8 +59,8 @@ func Init(bp *coremain.BP, args any) (any, error) {
return newMikrotikAddressListPlugin(args.(*Args)) return newMikrotikAddressListPlugin(args.(*Args))
} }
// QuickSetup format: host:port:username:password:use_tls:timeout:address_list4:address_list6:mask4:mask6:comment:timeout_addr // QuickSetup format: host:port:username:password:use_tls:timeout:address_list4:address_list6:mask4:mask6:comment:timeout_addr:cache_ttl
// e.g. "192.168.1.1:8728:admin:password:false:10:my_list4:my_list6:24:32:from_dns:3600" // e.g. "192.168.1.1:8728:admin:password:false:10:my_list4:my_list6:24:32:from_dns:3600:3600"
func QuickSetup(_ sequence.BQ, s string) (any, error) { func QuickSetup(_ sequence.BQ, s string) (any, error) {
parts := strings.Split(s, ":") parts := strings.Split(s, ":")
if len(parts) < 6 { if len(parts) < 6 {
@ -129,5 +130,12 @@ func QuickSetup(_ sequence.BQ, s string) (any, error) {
} }
} }
// 解析缓存 TTL
if len(parts) > 12 {
if cacheTTL, err := strconv.Atoi(parts[12]); err == nil {
args.CacheTTL = cacheTTL
}
}
return newMikrotikAddressListPlugin(args) return newMikrotikAddressListPlugin(args)
} }

View File

@ -26,6 +26,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/IrineSistiana/mosdns/v5/pkg/query_context" "github.com/IrineSistiana/mosdns/v5/pkg/query_context"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -43,6 +44,11 @@ type mikrotikAddressListPlugin struct {
workerPool chan struct{} workerPool chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
mu sync.RWMutex // 保护连接的重连操作 mu sync.RWMutex // 保护连接的重连操作
// 内存缓存
cache map[string]time.Time // key: "listName:cidrAddr", value: 添加时间
cacheMu sync.RWMutex // 保护缓存访问
cacheTTL time.Duration // 缓存 TTL默认 1 小时
} }
func newMikrotikAddressListPlugin(args *Args) (*mikrotikAddressListPlugin, error) { func newMikrotikAddressListPlugin(args *Args) (*mikrotikAddressListPlugin, error) {
@ -77,11 +83,19 @@ func newMikrotikAddressListPlugin(args *Args) (*mikrotikAddressListPlugin, error
// 设置工作池大小(可以根据需要调整) // 设置工作池大小(可以根据需要调整)
workerCount := 10 // 并发工作线程数 workerCount := 10 // 并发工作线程数
// 设置缓存 TTL
cacheTTL := time.Hour // 默认 1 小时
if args.CacheTTL > 0 {
cacheTTL = time.Duration(args.CacheTTL) * time.Second
}
plugin := &mikrotikAddressListPlugin{ plugin := &mikrotikAddressListPlugin{
args: args, args: args,
conn: conn, conn: conn,
log: zap.L().Named("mikrotik_addresslist"), log: zap.L().Named("mikrotik_addresslist"),
workerPool: make(chan struct{}, workerCount), workerPool: make(chan struct{}, workerCount),
cache: make(map[string]time.Time),
cacheTTL: cacheTTL,
} }
// 记录连接成功信息 // 记录连接成功信息
@ -90,7 +104,8 @@ func newMikrotikAddressListPlugin(args *Args) (*mikrotikAddressListPlugin, error
zap.Int("port", args.Port), zap.Int("port", args.Port),
zap.String("username", args.Username), zap.String("username", args.Username),
zap.String("address_list4", args.AddressList4), zap.String("address_list4", args.AddressList4),
zap.Int("worker_count", workerCount)) zap.Int("worker_count", workerCount),
zap.Duration("cache_ttl", cacheTTL))
return plugin, nil return plugin, nil
} }
@ -138,6 +153,70 @@ func (p *mikrotikAddressListPlugin) reconnect() error {
return nil return nil
} }
// 生成缓存键
func (p *mikrotikAddressListPlugin) cacheKey(listName, cidrAddr string) string {
return listName + ":" + cidrAddr
}
// 检查缓存中是否存在
func (p *mikrotikAddressListPlugin) isInCache(listName, cidrAddr string) bool {
p.cacheMu.RLock()
defer p.cacheMu.RUnlock()
key := p.cacheKey(listName, cidrAddr)
if addTime, exists := p.cache[key]; exists {
// 检查是否过期
if time.Since(addTime) < p.cacheTTL {
return true
}
// 过期了,删除
p.cacheMu.RUnlock()
p.cacheMu.Lock()
delete(p.cache, key)
p.cacheMu.Unlock()
p.cacheMu.RLock()
}
return false
}
// 添加到缓存
func (p *mikrotikAddressListPlugin) addToCache(listName, cidrAddr string) {
p.cacheMu.Lock()
defer p.cacheMu.Unlock()
key := p.cacheKey(listName, cidrAddr)
p.cache[key] = time.Now()
// 清理过期的缓存项
p.cleanupExpiredCache()
}
// 清理过期的缓存项
func (p *mikrotikAddressListPlugin) cleanupExpiredCache() {
now := time.Now()
for key, addTime := range p.cache {
if now.Sub(addTime) >= p.cacheTTL {
delete(p.cache, key)
}
}
}
// 获取缓存统计信息
func (p *mikrotikAddressListPlugin) getCacheStats() (int, int) {
p.cacheMu.RLock()
defer p.cacheMu.RUnlock()
total := len(p.cache)
valid := 0
now := time.Now()
for _, addTime := range p.cache {
if now.Sub(addTime) < p.cacheTTL {
valid++
}
}
return total, valid
}
func (p *mikrotikAddressListPlugin) Exec(_ context.Context, qCtx *query_context.Context) error { func (p *mikrotikAddressListPlugin) Exec(_ context.Context, qCtx *query_context.Context) error {
// 检查连接是否正常 // 检查连接是否正常
if p.conn == nil { if p.conn == nil {
@ -163,6 +242,14 @@ func (p *mikrotikAddressListPlugin) Close() error {
// 等待所有工作完成 // 等待所有工作完成
p.wg.Wait() p.wg.Wait()
// 清理缓存
p.cacheMu.Lock()
cacheSize := len(p.cache)
p.cache = nil
p.cacheMu.Unlock()
p.log.Info("plugin closed", zap.Int("cache_cleared", cacheSize))
// 关闭连接 // 关闭连接
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
@ -212,55 +299,20 @@ func (p *mikrotikAddressListPlugin) addToAddressList(r *dns.Msg) error {
return nil return nil
} }
// 并发处理所有地址 // 动态调整工作池大小
var wg sync.WaitGroup p.adjustWorkerPoolSize(len(addresses))
var mu sync.Mutex
var errors []error
addedCount := 0
for _, addr := range addresses { // 使用优化的批量处理
wg.Add(1) if err := p.batchAddAddresses(addresses, p.args.AddressList4, p.args.Mask4); err != nil {
go func(addr netip.Addr) { return err
defer wg.Done()
// 获取工作池槽位
select {
case p.workerPool <- struct{}{}:
defer func() { <-p.workerPool }()
default:
// 如果工作池满了,直接处理(避免阻塞)
p.log.Debug("worker pool full, processing directly")
} }
if err := p.addAddressToMikrotik(addr, p.args.AddressList4, p.args.Mask4); err != nil { // 记录缓存统计信息
mu.Lock() total, valid := p.getCacheStats()
errors = append(errors, err) p.log.Info("IPv4 addresses processed",
mu.Unlock() zap.Int("processed_count", len(addresses)),
} else { zap.Int("cache_total", total),
mu.Lock() zap.Int("cache_valid", valid))
addedCount++
mu.Unlock()
}
}(addr)
}
// 等待所有工作完成
wg.Wait()
// 记录结果
if addedCount > 0 {
p.log.Info("concurrently added IPv4 addresses to MikroTik",
zap.Int("success_count", addedCount),
zap.Int("total_count", len(addresses)),
zap.Int("error_count", len(errors)))
} else {
p.log.Debug("no IPv4 addresses added to MikroTik")
}
// 如果有错误,返回第一个错误
if len(errors) > 0 {
return fmt.Errorf("some addresses failed to add: %v", errors[0])
}
return nil return nil
} }
@ -285,14 +337,21 @@ func (p *mikrotikAddressListPlugin) addAddressToMikrotik(addr netip.Addr, listNa
p.log.Debug("checking address", zap.String("cidr", cidrAddr), zap.String("list", listName)) p.log.Debug("checking address", zap.String("cidr", cidrAddr), zap.String("list", listName))
// 检查地址是否已存在 // 首先检查内存缓存
if p.isInCache(listName, cidrAddr) {
p.log.Debug("address found in cache, skipping", zap.String("cidr", cidrAddr), zap.String("list", listName))
return nil
}
// 缓存中没有,检查 MikroTik 中是否已存在
exists, err := p.addressExists(listName, cidrAddr) exists, err := p.addressExists(listName, cidrAddr)
if err != nil { if err != nil {
// 如果检查失败,可能是地址列表不存在,继续尝试添加 // 如果检查失败,可能是地址列表不存在,继续尝试添加
p.log.Debug("failed to check if address exists, will try to add anyway", zap.Error(err)) p.log.Debug("failed to check if address exists in MikroTik, will try to add anyway", zap.Error(err))
} else if exists { } else if exists {
// 地址已存在,跳过 // 地址已存在于 MikroTik 中,添加到缓存并跳过
p.log.Debug("address already exists", zap.String("cidr", cidrAddr), zap.String("list", listName)) p.log.Debug("address already exists in MikroTik, adding to cache", zap.String("cidr", cidrAddr), zap.String("list", listName))
p.addToCache(listName, cidrAddr)
return nil return nil
} }
@ -320,53 +379,64 @@ func (p *mikrotikAddressListPlugin) addAddressToMikrotik(addr netip.Addr, listNa
p.log.Debug("Add to list: ", zap.Strings("params", params)) p.log.Debug("Add to list: ", zap.Strings("params", params))
// 发送到 RouterOS重试机制 // 发送到 RouterOS智能重试机制
maxRetries := 3 maxRetries := 3
backoffDuration := 100 * time.Millisecond
for i := 0; i < maxRetries; i++ { for i := 0; i < maxRetries; i++ {
// 使用读锁保护连接访问 // 使用读锁保护连接访问
p.mu.RLock() p.mu.RLock()
conn := p.conn conn := p.conn
isConnected := p.isConnected
p.mu.RUnlock() p.mu.RUnlock()
if conn == nil { if conn == nil || !isConnected {
p.log.Error("connection is nil") p.log.Debug("connection not available, attempting to reconnect")
return fmt.Errorf("connection is nil") p.mu.Lock()
p.isConnected = false
if err := p.reconnect(); err != nil {
p.mu.Unlock()
p.log.Error("failed to reconnect", zap.Error(err))
time.Sleep(backoffDuration)
backoffDuration *= 2 // 指数退避
continue
}
conn = p.conn
p.mu.Unlock()
} }
args := append([]string{"/ip/firewall/address-list/add"}, params...) args := append([]string{"/ip/firewall/address-list/add"}, params...)
_, err = conn.Run(args...) _, err = conn.Run(args...)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "already have such entry") { if strings.Contains(err.Error(), "already have such entry") {
p.log.Debug("Already exists: ", zap.String("cidr", cidrAddr)) p.log.Debug("Address already exists", zap.String("cidr", cidrAddr))
p.addToCache(listName, cidrAddr) // 添加到缓存
return nil return nil
} }
// 如果是连接错误,尝试重新连接 // 检查是否为连接错误
if strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "connection") { if p.isConnectionError(err) {
p.log.Warn("connection error, attempting to reconnect", p.log.Warn("connection error, will retry",
zap.String("cidr", cidrAddr), zap.String("cidr", cidrAddr),
zap.Int("retry", i+1), zap.Int("retry", i+1),
zap.Error(err)) zap.Error(err))
// 使用写锁保护重连操作
p.mu.Lock() p.mu.Lock()
if err := p.reconnect(); err != nil { p.isConnected = false
p.mu.Unlock()
p.log.Error("failed to reconnect", zap.Error(err))
continue
}
p.mu.Unlock() p.mu.Unlock()
// 重试 // 指数退避
time.Sleep(backoffDuration)
backoffDuration *= 2
continue continue
} }
// 其他错误,记录并返回 // 其他错误,直接返回
p.log.Error("failed to add address to MikroTik", p.log.Error("failed to add address to MikroTik",
zap.String("cidr", cidrAddr), zap.String("cidr", cidrAddr),
zap.String("list", listName), zap.String("list", listName),
zap.Error(err)) zap.Error(err))
return fmt.Errorf("failed to add address %s to list %s: from RouterOS device: %v", cidrAddr, listName, err) return fmt.Errorf("failed to add address %s to list %s: %v", cidrAddr, listName, err)
} }
// 成功,跳出重试循环 // 成功,跳出重试循环
@ -377,6 +447,9 @@ func (p *mikrotikAddressListPlugin) addAddressToMikrotik(addr netip.Addr, listNa
zap.String("cidr", cidrAddr), zap.String("cidr", cidrAddr),
zap.String("list", listName)) zap.String("list", listName))
// 添加到缓存
p.addToCache(listName, cidrAddr)
return nil return nil
} }
@ -410,3 +483,105 @@ func (p *mikrotikAddressListPlugin) addressExists(listName, address string) (boo
return exists, nil return exists, nil
} }
// batchAddAddresses 批量添加地址到MikroTik批量操作优化
func (p *mikrotikAddressListPlugin) batchAddAddresses(addresses []netip.Addr, listName string, mask int) error {
if len(addresses) == 0 {
return nil
}
// 分批处理每批10个地址
batchSize := 10
var wg sync.WaitGroup
var mu sync.Mutex
var errors []error
successCount := 0
for i := 0; i < len(addresses); i += batchSize {
end := i + batchSize
if end > len(addresses) {
end = len(addresses)
}
batch := addresses[i:end]
wg.Add(1)
go func(batch []netip.Addr) {
defer wg.Done()
// 获取工作池槽位
select {
case p.workerPool <- struct{}{}:
defer func() { <-p.workerPool }()
default:
p.log.Debug("worker pool full, processing batch directly")
}
for _, addr := range batch {
if err := p.addAddressToMikrotik(addr, listName, mask); err != nil {
mu.Lock()
errors = append(errors, err)
mu.Unlock()
} else {
mu.Lock()
successCount++
mu.Unlock()
}
}
}(batch)
}
wg.Wait()
if len(errors) > 0 {
p.log.Error("batch processing completed with errors",
zap.Int("success_count", successCount),
zap.Int("error_count", len(errors)),
zap.Error(errors[0]))
return errors[0]
}
p.log.Info("batch processing completed successfully",
zap.Int("success_count", successCount),
zap.Int("total_count", len(addresses)))
return nil
}
// adjustWorkerPoolSize 动态调整工作池大小
func (p *mikrotikAddressListPlugin) adjustWorkerPoolSize(addressCount int) {
var targetSize int
switch {
case addressCount <= 5:
targetSize = 3
case addressCount <= 20:
targetSize = 5
case addressCount <= 50:
targetSize = 10
default:
targetSize = 15
}
// 如果当前容量不够,创建新的工作池
if cap(p.workerPool) < targetSize {
p.log.Debug("adjusting worker pool size",
zap.Int("old_size", cap(p.workerPool)),
zap.Int("new_size", targetSize),
zap.Int("address_count", addressCount))
// 创建新的工作池
p.workerPool = make(chan struct{}, targetSize)
}
}
// isConnectionError 检查是否为连接错误
func (p *mikrotikAddressListPlugin) isConnectionError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
return strings.Contains(errStr, "EOF") ||
strings.Contains(errStr, "connection") ||
strings.Contains(errStr, "closed") ||
strings.Contains(errStr, "timeout")
}