Xray-core icon indicating copy to clipboard operation
Xray-core copied to clipboard

add balance optimal strategy

Open badO1a5A90 opened this issue 5 years ago • 13 comments

原来的 balancer 只有一种策略,即随机策略, balancer 在配置的一组 outbound 中每次随机选择一个作为实际的outbound, 比较鸡肋.

这里增加了一种新的策略, 且以后还可以增加不同的策略模式. 如果不指定使用策略, 不会有任何影响。

此策略包含自动择优选择,故障转移 即可以择优选择一组 outbound 中最佳的一个作为实际outbound, 因此在当前 outbound 故障/不稳定时,也自然会进行故障转移. 并且可以自行配置权重及其他参数进行控制

策略原理

此策略的原理是, 为 balancer 配置一组 outbound 后, balancer 按时间间隔 (可配置)使用每个 outbound 向指定的目标URL(可配置)进行N次(次数可配置)访问进行测速. 策略计算测速的速率(多次测速则取平均值), 乘以此 outbound 的权重(可配置), 计算得到最终得分. 所有outbound同时开始,并且在指定时间(可配置)内必须结束(在指定时间内未能完成测试的得分为0). 结束所有测试后, 策略将选择得分最高的 outbound 作为此 balancer 后续处理连接请求时实际使用的outbound, 持续到下一轮测试.

配置方式

首先假设有3个outbound,名称为 proxy1,proxy2,proxy3 原配置方式为

"balancers": [
    {
        "tag": "balancer",
        "selector": ["proxy1","proxy2","proxy3"]
    }
]

如果还是用随机策略, 仍用此配置即可,对于每个新连接 balancer 将在 ["proxy1","proxy2","proxy3"] 中随机选择一个作为 outbound.

新的 balancer 配置如下

"balancers": [
    {
        "tag": "balancer",
        "selector": ["proxy1","proxy2","proxy3"]
        "strategy": "optimal",
        "optimalSettings": {
              "timeout": 10000,
              "interval": 20000,
              "url": "https://about.google",
              "count": 1,
              "weights": [
                {
                  "tag": "proxy1",
                  "weight": 150
                },
                {
                  "tag": "proxy2",
                  "weight": 150
                }
              ]
            }
     }
]

配置简要说明: strategy: 指示使用哪一种策略, 填"optimal"为使用最优策略, 不填或者填"random"即为以前的随机策略, 以后可以增加更多的 strategy .

optimalSettings是使用"optimal"时的具体配置.

  • timeout: 超时时间,单位毫秒,10000为10秒, 默认5秒.即测试需要在多长时间内结束,超过时间不能完成测试的得分为0
  • interval: 测试间隔,单位毫秒,默认10分钟.
  • url: 测试使用的URL, 默认为"https://www.google.com",可以指定任意你喜欢的地址,比如一个小文件.
  • count: 测试次数,默认为1. 多次将取平均值.
  • weights:是一个数组,用于配置每个outbound的权重,如果没指定权重,默认为100.

实例说明

实例1

最简单的情况, 用户有2个服务器, 于是在客户端配置了两个 outbound "proxy1"和 "proxy2" 指向两个服务器. 其中 "proxy1"为较快常用线路,"proxy2"为备用线路。 希望在proxy1不稳定,或服务器down掉以后可以自动切换到"proxy2",常用场景是普通正常上网场景. 则可以这样配置.

{
  "routing": {
    "domainStrategy": "AsIs",
    "rules": [
      {
        "type": "field",
        "inboundTag": [
          "http-in",
          "socks-in"
        ],
        "balancerTag": "balancer"
      }
    ],
    "balancers": [
      {
        "tag": "balancer",
        "selector": [
          "proxy1",
          "proxy2"
        ],
        "strategy": "optimal",
        "optimalSettings": {
          "timeout": 2000,
          "interval": 30000,
          "url": "https://about.google",
          "count": 1,
          "weights": [
            {
              "tag": "proxy1",
              "weight": 200
            },
            {
              "tag": "proxy2",
              "weight": 100
            }
          ]
        }
      }
    ]
  }
}

路由配置此处将http和socks的outbound指向balancer仅为示范, (路由配置可以非常多样化,此处不展开), 关键是使用 "balancerTag": "balancer" 来指定 balancer

以上配置, 每隔30秒,proxy1和proxy2,都会访问 "https://about.google" ,进行测速. 测试需要在2秒内完成,否则认为超时.(一次完整 "https://about.google" 访问大小约80k,一般0.x秒即可完成测试), 测试次数1次. 通常情况下,因为proxy1权重为proxy2两倍,且已经假设理论上常用线路proxy1即使不加权也应快于proxy2. 因此每一轮测试后, 会有以下情况:

  • 正常情况下,proxy1 得分超过 proxy2, balancer 将一直保持使用 proxy1 作为实际 outbound.
  • 当proxy1网络不稳定 ,proxy1得分低于 proxy2. balancer会选择此时更优的 proxy2 作为实际 outbound 来增强体验
  • 当proxy1发生故障,此时 proxy1 得分为0, balancer 将使用 proxy2 作为实际 outbound, 可视为故障转移。
  • 当proxy1网络稳定后或从故障中恢复, 下一轮的测试 proxy1得分又将高于 proxy2, 此时 balancer 又会重新选择, 从proxy2 切换回 proxy1 作为实际outbound. 即 每隔30秒, balancer都会再次尝试选择当前最优的线路作为出口, 并且在接下去30秒保持使用此线路作为outbound,然后再次进行测试和选择.

根据实际的线路情况和需求, 可以通过控制测速的间隔 和 设计合理权重值 来获取最佳体验.

  • 测速的间隔越短, 对网络和服务器状态的响应越灵敏, 但也可能带来一些其他问题
  • 权重则可以比较精确的自行根据需求来控制选择的优先度

实例2

较复杂的情况, 假设用户有6个服务器, 其中 1-3线路类似,4-5线路类似,6线路类似.用户希望可以按3条线路择优选择,并且每条线路中的各个服务器轮询进行负载 可以如下配置,注意:下面的配置为不完整配置, 旨在说明模式

配置较长, 请点击此处展开查看
{
  "outbounds": [
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "IP1",
            "port": 443,
            "users": [
              {
                "id": "",
                "flow": "xtls-rprx-splice",
                "encryption": "none"
              }
            ]
          },
          {
            "address": "IP2",
            "port": 443,
            "users": [
              {
                "id": "",
                "flow": "xtls-rprx-splice",
                "encryption": "none"
              }
            ]
          },
          {
            "address": "IP3",
            "port": 443,
            "users": [
              {
                "id": "",
                "flow": "xtls-rprx-splice",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "tag": "proxy1",
      "streamSettings": {
        "network": "tcp",
        "security": "xtls",
        "xtlsSettings": {
          "serverName": "test1.com"
        }
      }
    },
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "IP4",
            "port": 443,
            "users": [
              {
                "id": "",
                "flow": "xtls-rprx-splice",
                "encryption": "none"
              }
            ]
          },
          {
            "address": "IP5",
            "port": 443,
            "users": [
              {
                "id": "",
                "flow": "xtls-rprx-splice",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "tag": "proxy2",
      "streamSettings": {
        "network": "tcp",
        "security": "xtls",
        "xtlsSettings": {
          "serverName": "test2.com"
        }
      }
    },
    {
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "IP6",
            "port": 443,
            "users": [
              {
                "id": "",
                "flow": "xtls-rprx-splice",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "tag": "proxy3",
      "streamSettings": {
        "network": "tcp",
        "security": "xtls",
        "xtlsSettings": {
          "serverName": "test3.com"
        }
      }
    }
  ],
  "routing": {
    "domainStrategy": "IPOnDemand",
    "rules": [
      {
        "type": "field",
        "inboundTag": [
          "http-in",
          "socks-in"
        ],
        "balancerTag": "balancer"
      }
    ],
    "balancers": [
      {
        "tag": "balancer",
        "selector": [
          "proxy1",
          "proxy2",
          "proxy3"
        ],
        "strategy": "optimal",
        "optimalSettings": {
          "timeout": 5000,
          "interval": 30000,
          "url": "https://about.google",
          "count": 1,
          "weights": [
            {
              "tag": "proxy1",
              "weight": 300
            },
            {
              "tag": "proxy2",
              "weight": 200
            },
            {
              "tag": "proxy3",
              "weight": 100
            }
          ]
        }
      }
    ]
  }
}

其中 "vnext": 中的服务器组是轮询的. balancer 和 实例1 类似 , 每30秒进行一次测试, 选择当时最好的 proxy 作为 outbound, 如果此 proxy 内有多个服务器,则轮询进行负载. 权重指定了300:200:100,即主要倾向选择proxy1,其次倾向选择proxy2,当proxy1,proxy2均不稳定或者故障,倾向于使用proxy3.

其他用法技巧和注意点

  • 权重是个很有用的东西, 根据实际情况各不相同, 可以通过控制权重, 实现各种目的。
    • 比如可以将两个outbound权值设置为proxy1远比proxy2大,从而实现仅仅是故障转移的效果.
    • 可以打开debug级别的log,观察测试输出的每个outbound的score,仔细进行权重调整,来获得最想要的效果。(实际用例很多,比如B的质量可能不如A, 但因为B的可用流量较大, 希望大部分情况下优先选择B, 此时可以适当提高权重,提高B的score大多数情况下超过A, 来优先选择使用B)
  • 实际的使用场景不同, 可以通过指定不同的URL使得测试更接近实际场景, 比如设置URL为 "https://about.google" 和较短的timeout ,则接近网页浏览为主的场景. 而指定一个文件下载和较长的timeout,则接近以大流量为主的场景.
  • 测试间隔决定了对网络状态,服务器状态响应的敏感度, 但同时也会多耗费流量(视URL不同而不同),以及可能下面这个问题.
  • 某些站点或软件对IP有比较严格的限制, 如果balancer中不同的proxy质量较平均,权重也指定相近, 测试间隔指定较短,那么频繁进行测试可能会导致频繁切换proxy, 即频繁切换最终的出口IP访问这些站点, 可能并不合适, 此时请小心配置分流固定出口或者指定合理的权重, 避免频繁切换.
  • 但上述情况中, 如果proxy指向的是的不同的中转,中转均转发到同一落地机,即可避免上述切换最终出口IP的情况,并且可以在多个中转服务器中进行最佳的proxy选择中转.
  • 即 可以通过各种参数的配合, 灵活实现各种目的. 其他技巧想到再补充吧.

badO1a5A90 avatar Jan 15 '21 01:01 badO1a5A90

客户端跟服务端都定义一个相同的空域名,测速阶段客户端对这个空域名进行访问,服务端收到这个空域名的请求后直接返回一个预设定的回复是不是好点。 比如定义空域名为“blank.com”,客户端测速时向所有代理服务器发送“blank.com”请求,代理服务器收到请求后检查域名,如果是"blank.com"直接返回一个1KB(或者更大)的回复。 这样就省去服务端一直向测速url发送请求接收回复的动作了。

lizkes avatar Jan 24 '21 05:01 lizkes

Nice work. 策略方面没有问题, 测试方面需要加强. 目前影响代理体验的服务器因素有三个: 延迟, 带宽,, 稳定性. 目前大部分有自动测试功能的软件都只是做了延迟测试, 但晚上网络高峰时期大部分代理都会出现间断性的丢包, 这时延迟测试反而成了弊端, 自动检测极易选择不稳定的低延迟代理. 目前只有 shadowsocks-rust 同时检测延迟和稳定性, 实际使用下来体验也是最好的, @badO1a5A90 你可以移植下么.

LazyZhu avatar Jan 24 '21 18:01 LazyZhu

Nice work. 策略方面没有问题, 测试方面需要加强. 目前影响代理体验的服务器因素有三个: 延迟, 带宽,, 稳定性. 目前大部分有自动测试功能的软件都只是做了延迟测试, 但晚上网络高峰时期大部分代理都会出现间断性的丢包, 这时延迟测试反而成了弊端, 自动检测极易选择不稳定的低延迟代理. 目前只有 shadowsocks-rust 同时检测延迟和稳定性, 实际使用下来体验也是最好的, @badO1a5A90 你可以移植下么.

这个策略的测试并非是测试延迟,是基于实际速率。 实际观测中高峰期低延迟高丢包的线路得分是远低于其他线路的。

badO1a5A90 avatar Jan 24 '21 18:01 badO1a5A90

Nice work. 策略方面没有问题, 测试方面需要加强. 目前影响代理体验的服务器因素有三个: 延迟, 带宽,, 稳定性. 目前大部分有自动测试功能的软件都只是做了延迟测试, 但晚上网络高峰时期大部分代理都会出现间断性的丢包, 这时延迟测试反而成了弊端, 自动检测极易选择不稳定的低延迟代理. 目前只有 shadowsocks-rust 同时检测延迟和稳定性, 实际使用下来体验也是最好的, @badO1a5A90 你可以移植下么.

shadowsocks-rust的需要研究下。。。

badO1a5A90 avatar Jan 24 '21 18:01 badO1a5A90

Nice work. 策略方面没有问题, 测试方面需要加强. 目前影响代理体验的服务器因素有三个: 延迟, 带宽,, 稳定性. 目前大部分有自动测试功能的软件都只是做了延迟测试, 但晚上网络高峰时期大部分代理都会出现间断性的丢包, 这时延迟测试反而成了弊端, 自动检测极易选择不稳定的低延迟代理. 目前只有 shadowsocks-rust 同时检测延迟和稳定性, 实际使用下来体验也是最好的, @badO1a5A90 你可以移植下么.

这个策略的测试并非是测试延迟,是基于实际速率。 实际观测中高峰期低延迟高丢包的线路得分是远低于其他线路的。

对于持续丢包的代理有效, 但实际情况是代理间歇性丢包呢? 唯一的解决方法是调低测试间隔. shadowsocks-rust 默认是通过计算代理前10分钟的综合评分(延迟和稳定性)连选择的.

LazyZhu avatar Jan 24 '21 18:01 LazyZhu

对于持续丢包的代理有效, 但实际情况是代理间歇性丢包呢? 唯一的解决方法是调低测试间隔.

确实如此, 目前调低测试间隔才会对网络的稳定性更敏感. 另一种方式是加长测试的时间(即增加下载量的大小)

shadowsocks-rust 默认是通过计算代理前10分钟的综合评分(延迟和稳定性)连选择的.

这个其实算是具体的策略了, 他的稳定性是基于什么评估的?

badO1a5A90 avatar Jan 24 '21 18:01 badO1a5A90

shadowsocks-rust 默认是通过计算代理前10分钟的综合评分(延迟和稳定性)连选择的.

10分钟综合评估似乎更能体现网络一段时间的整体稳定性. 只看当前测试(+短间隔测试)应该更能对当前的波动敏感. 或许这里也可以增加设置--(基于多长时间/次数的得分进行综合评估)

badO1a5A90 avatar Jan 24 '21 18:01 badO1a5A90

对于持续丢包的代理有效, 但实际情况是代理间歇性丢包呢? 唯一的解决方法是调低测试间隔.

确实如此, 目前调低测试间隔才会对网络的稳定性更敏感. 另一种方式是加长测试的时间(即增加下载量的大小)

shadowsocks-rust 默认是通过计算代理前10分钟的综合评分(延迟和稳定性)连选择的.

这个其实算是具体的策略了, 他的稳定性是基于什么评估的?

https://github.com/shadowsocks/shadowsocks-rust/blob/master/crates/shadowsocks-service/src/local/loadbalancing/server_stat.rs#L71

    // Score = (norm_lat * 1.0 + prop_err * 3.0 + stdev * 1.0) / 5.0
    //
    // 1. The lower latency, the better
    // 2. The lower errored count, the better
    // 3. The lower latency's stdev, the better
    let score = (nrtt * SCORE_RTT_WEIGHT + self.fail_rate * SCORE_FAIL_WEIGHT + nstdev * SCORE_STDEV_WEIGHT)
        / (SCORE_RTT_WEIGHT + SCORE_FAIL_WEIGHT + SCORE_STDEV_WEIGHT);

详情可以看他的代码

LazyZhu avatar Jan 24 '21 18:01 LazyZhu

@badO1a5A90 https://github.com/v2fly/v2ray-core/pull/589 同样通过采样数量计算标准差的方式来选择线路.

LazyZhu avatar Jan 27 '21 11:01 LazyZhu

这个 PR 可以merge 了吗? 需要这个功能

recall704 avatar Sep 01 '21 08:09 recall704

功能不错 期待打磨

hfdem avatar Sep 09 '21 11:09 hfdem

Please merge this!

ghost avatar Sep 23 '21 03:09 ghost

请问 1.5.2 的code 要怎么将这篇也给修改上去呢? 想自己修改编译后,发现有很多代码跟commits不太相同了

是基于运营商可能会对伺服器限速, 找了好多才找到这、发现还没 merge上去 很喜欢这个功能 1.5.1的Observatory 连接检查 反而只是检查服务器是不是有在线上而已?

LiMoon avatar Jan 05 '22 20:01 LiMoon

这个PR还有后续跟进的计划吗?

ha-ku avatar Jan 27 '23 14:01 ha-ku

What is holding this back from a merge?

ghost avatar Feb 17 '23 23:02 ghost

One can feel its absence.

MrMohebi avatar Jul 23 '23 05:07 MrMohebi

这个pr什么时候可以合并啊?

yogurt7771 avatar Sep 18 '23 02:09 yogurt7771

非常期待这个PR的合并

DarkCWK avatar Nov 07 '23 22:11 DarkCWK

能不能加个轮询策略?

clcc2019 avatar Jan 04 '24 10:01 clcc2019

考察了一下 v2fly 的 leastload 综合看来它的实现更加完整 比如 记录多次结果平均和方差 测量排除本地网络不通 Observer Balancer 配置分离 API 支持 等等 所以目前先合了 leastload

~~亚瑟哥常回来看看~~

yuhan6665 avatar Feb 18 '24 04:02 yuhan6665