allow to disable application layer heartbeat to reduce traffic cost (#2758)

fix #2754
This commit is contained in:
fatedier 2022-01-13 14:26:07 +08:00 committed by GitHub
parent 4bfc89d988
commit 293003fcdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 228 additions and 146 deletions

View File

@ -311,16 +311,27 @@ func (ctl *Control) msgHandler() {
}() }()
defer ctl.msgHandlerShutdown.Done() defer ctl.msgHandlerShutdown.Done()
var hbSendCh <-chan time.Time
// TODO(fatedier): disable heartbeat if TCPMux is enabled.
// Just keep it here to keep compatible with old version frps.
if ctl.clientCfg.HeartbeatInterval > 0 {
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second) hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
defer hbSend.Stop() defer hbSend.Stop()
hbSendCh = hbSend.C
}
var hbCheckCh <-chan time.Time
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux {
hbCheck := time.NewTicker(time.Second) hbCheck := time.NewTicker(time.Second)
defer hbCheck.Stop() defer hbCheck.Stop()
hbCheckCh = hbCheck.C
}
ctl.lastPong = time.Now() ctl.lastPong = time.Now()
for { for {
select { select {
case <-hbSend.C: case <-hbSendCh:
// send heartbeat to server // send heartbeat to server
xl.Debug("send heartbeat to server") xl.Debug("send heartbeat to server")
pingMsg := &msg.Ping{} pingMsg := &msg.Ping{}
@ -329,7 +340,7 @@ func (ctl *Control) msgHandler() {
return return
} }
ctl.sendCh <- pingMsg ctl.sendCh <- pingMsg
case <-hbCheck.C: case <-hbCheckCh:
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second { if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout") xl.Warn("heartbeat timeout")
// let reader() stop // let reader() stop

View File

@ -249,7 +249,7 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
if svr.cfg.TCPMux { if svr.cfg.TCPMux {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 20 * time.Second fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard fmuxCfg.LogOutput = io.Discard
session, err = fmux.Client(conn, fmuxCfg) session, err = fmux.Client(conn, fmuxCfg)
if err != nil { if err != nil {

View File

@ -61,6 +61,9 @@ pool_count = 5
# if tcp stream multiplexing is used, default is true, it must be same with frps # if tcp stream multiplexing is used, default is true, it must be same with frps
tcp_mux = true tcp_mux = true
# specify keep alive interval for tcp mux.
# only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60
# your proxy name will be changed to {user}.{proxy} # your proxy name will be changed to {user}.{proxy}
user = your_name user = your_name
@ -89,7 +92,8 @@ tls_enable = true
# start = ssh,dns # start = ssh,dns
# heartbeat configure, it's not recommended to modify the default value # heartbeat configure, it's not recommended to modify the default value
# the default value of heartbeat_interval is 10 and heartbeat_timeout is 90 # The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
# to disable it.
# heartbeat_interval = 30 # heartbeat_interval = 30
# heartbeat_timeout = 90 # heartbeat_timeout = 90

View File

@ -92,7 +92,7 @@ oidc_skip_expiry_check = false
oidc_skip_issuer_check = false oidc_skip_issuer_check = false
# heartbeat configure, it's not recommended to modify the default value # heartbeat configure, it's not recommended to modify the default value
# the default value of heartbeat_timeout is 90 # the default value of heartbeat_timeout is 90. Set negative value to disable it.
# heartbeat_timeout = 90 # heartbeat_timeout = 90
# user_conn_timeout configure, it's not recommended to modify the default value # user_conn_timeout configure, it's not recommended to modify the default value
@ -121,6 +121,9 @@ subdomain_host = frps.com
# if tcp stream multiplexing is used, default is true # if tcp stream multiplexing is used, default is true
tcp_mux = true tcp_mux = true
# specify keep alive interval for tcp mux.
# only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60
# custom 404 page for HTTP requests # custom 404 page for HTTP requests
# custom_404_page = /path/to/404.html # custom_404_page = /path/to/404.html

View File

@ -86,6 +86,9 @@ type ClientCommonConf struct {
// the server must have TCP multiplexing enabled as well. By default, this // the server must have TCP multiplexing enabled as well. By default, this
// value is true. // value is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// User specifies a prefix for proxy names to distinguish them from other // User specifies a prefix for proxy names to distinguish them from other
// clients. If this value is not "", proxy names will automatically be // clients. If this value is not "", proxy names will automatically be
// changed to "{user}.{proxy_name}". By default, this value is "". // changed to "{user}.{proxy_name}". By default, this value is "".
@ -129,11 +132,11 @@ type ClientCommonConf struct {
DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"` DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"`
// HeartBeatInterval specifies at what interval heartbeats are sent to the // HeartBeatInterval specifies at what interval heartbeats are sent to the
// server, in seconds. It is not recommended to change this value. By // server, in seconds. It is not recommended to change this value. By
// default, this value is 30. // default, this value is 30. Set negative value to disable it.
HeartbeatInterval int64 `ini:"heartbeat_interval" json:"heartbeat_interval"` HeartbeatInterval int64 `ini:"heartbeat_interval" json:"heartbeat_interval"`
// HeartBeatTimeout specifies the maximum allowed heartbeat response delay // HeartBeatTimeout specifies the maximum allowed heartbeat response delay
// before the connection is terminated, in seconds. It is not recommended // before the connection is terminated, in seconds. It is not recommended
// to change this value. By default, this value is 90. // to change this value. By default, this value is 90. Set negative value to disable it.
HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"` HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"`
// Client meta info // Client meta info
Metas map[string]string `ini:"-" json:"metas"` Metas map[string]string `ini:"-" json:"metas"`
@ -163,6 +166,7 @@ func GetDefaultClientConf() ClientCommonConf {
AssetsDir: "", AssetsDir: "",
PoolCount: 1, PoolCount: 1,
TCPMux: true, TCPMux: true,
TCPMuxKeepaliveInterval: 60,
User: "", User: "",
DNSServer: "", DNSServer: "",
LoginFailExit: true, LoginFailExit: true,
@ -189,13 +193,11 @@ func (cfg *ClientCommonConf) Complete() {
} }
func (cfg *ClientCommonConf) Validate() error { func (cfg *ClientCommonConf) Validate() error {
if cfg.HeartbeatInterval <= 0 { if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
return fmt.Errorf("invalid heartbeat_interval")
}
if cfg.HeartbeatTimeout < cfg.HeartbeatInterval { if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
} }
}
if cfg.TLSEnable == false { if cfg.TLSEnable == false {
if cfg.TLSCertFile != "" { if cfg.TLSCertFile != "" {

View File

@ -274,6 +274,7 @@ func Test_LoadClientCommonConf(t *testing.T) {
AssetsDir: "./static9", AssetsDir: "./static9",
PoolCount: 59, PoolCount: 59,
TCPMux: true, TCPMux: true,
TCPMuxKeepaliveInterval: 60,
User: "your_name", User: "your_name",
LoginFailExit: true, LoginFailExit: true,
Protocol: "tcp", Protocol: "tcp",

View File

@ -118,6 +118,9 @@ type ServerCommonConf struct {
// from a client to share a single TCP connection. By default, this value // from a client to share a single TCP connection. By default, this value
// is true. // is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// Custom404Page specifies a path to a custom 404 page to display. If this // Custom404Page specifies a path to a custom 404 page to display. If this
// value is "", a default page will be displayed. By default, this value is // value is "", a default page will be displayed. By default, this value is
// "". // "".
@ -154,7 +157,7 @@ type ServerCommonConf struct {
TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"` TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"`
// HeartBeatTimeout specifies the maximum time to wait for a heartbeat // HeartBeatTimeout specifies the maximum time to wait for a heartbeat
// before terminating the connection. It is not recommended to change this // before terminating the connection. It is not recommended to change this
// value. By default, this value is 90. // value. By default, this value is 90. Set negative value to disable it.
HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"` HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"`
// UserConnTimeout specifies the maximum time to wait for a work // UserConnTimeout specifies the maximum time to wait for a work
// connection. By default, this value is 10. // connection. By default, this value is 10.
@ -194,6 +197,7 @@ func GetDefaultServerConf() ServerCommonConf {
DetailedErrorsToClient: true, DetailedErrorsToClient: true,
SubDomainHost: "", SubDomainHost: "",
TCPMux: true, TCPMux: true,
TCPMuxKeepaliveInterval: 60,
AllowPorts: make(map[int]struct{}), AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5, MaxPoolCount: 5,
MaxPortsPerClient: 0, MaxPortsPerClient: 0,

View File

@ -139,6 +139,7 @@ func Test_LoadServerCommonConf(t *testing.T) {
TLSTrustedCaFile: "ca.crt", TLSTrustedCaFile: "ca.crt",
SubDomainHost: "frps.com", SubDomainHost: "frps.com",
TCPMux: true, TCPMux: true,
TCPMuxKeepaliveInterval: 60,
UDPPacketSize: 1509, UDPPacketSize: 1509,
HTTPPlugins: map[string]plugin.HTTPPluginOptions{ HTTPPlugins: map[string]plugin.HTTPPluginOptions{
@ -189,6 +190,7 @@ func Test_LoadServerCommonConf(t *testing.T) {
LogMaxDays: 3, LogMaxDays: 3,
DetailedErrorsToClient: true, DetailedErrorsToClient: true,
TCPMux: true, TCPMux: true,
TCPMuxKeepaliveInterval: 60,
AllowPorts: make(map[int]struct{}), AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5, MaxPoolCount: 5,
HeartbeatTimeout: 90, HeartbeatTimeout: 90,

View File

@ -400,12 +400,19 @@ func (ctl *Control) manager() {
defer ctl.allShutdown.Start() defer ctl.allShutdown.Start()
defer ctl.managerShutdown.Done() defer ctl.managerShutdown.Done()
var heartbeatCh <-chan time.Time
if ctl.serverCfg.TCPMux || ctl.serverCfg.HeartbeatTimeout <= 0 {
// Don't need application heartbeat here.
// yamux will do same thing.
} else {
heartbeat := time.NewTicker(time.Second) heartbeat := time.NewTicker(time.Second)
defer heartbeat.Stop() defer heartbeat.Stop()
heartbeatCh = heartbeat.C
}
for { for {
select { select {
case <-heartbeat.C: case <-heartbeatCh:
if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second { if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout") xl.Warn("heartbeat timeout")
return return

View File

@ -406,7 +406,7 @@ func (svr *Service) HandleListener(l net.Listener) {
go func(ctx context.Context, frpConn net.Conn) { go func(ctx context.Context, frpConn net.Conn) {
if svr.cfg.TCPMux { if svr.cfg.TCPMux {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 20 * time.Second fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard fmuxCfg.LogOutput = io.Discard
session, err := fmux.Server(frpConn, fmuxCfg) session, err := fmux.Server(frpConn, fmuxCfg)
if err != nil { if err != nil {

View File

@ -0,0 +1,48 @@
package features
import (
"fmt"
"time"
"github.com/fatedier/frp/test/e2e/framework"
. "github.com/onsi/ginkgo"
)
var _ = Describe("[Feature: Heartbeat]", func() {
f := framework.NewDefaultFramework()
It("disable application layer heartbeat", func() {
serverPort := f.AllocPort()
serverConf := fmt.Sprintf(`
[common]
bind_addr = 0.0.0.0
bind_port = %d
heartbeat_timeout = -1
tcp_mux_keepalive_interval = 2
`, serverPort)
remotePort := f.AllocPort()
clientConf := fmt.Sprintf(`
[common]
server_port = %d
log_level = trace
heartbeat_interval = -1
heartbeat_timeout = -1
tcp_mux_keepalive_interval = 2
[tcp]
type = tcp
local_port = %d
remote_port = %d
`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
// run frps and frpc
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
time.Sleep(5 * time.Second)
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
})
})