-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OCPBUGS-14914: Set tunnel and server timeouts only at the backend level #536
base: master
Are you sure you want to change the base?
Conversation
@alebedev87: This pull request references Jira Issue OCPBUGS-14914, which is invalid:
Comment The bug has been updated to refer to the pull request using the external bug tracker. In response to this: Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
6a1044e
to
ef08ed2
Compare
/jira refresh |
@alebedev87: This pull request references Jira Issue OCPBUGS-14914, which is valid. The bug has been moved to the POST state. 3 validation(s) were run on this bug
Requesting review from QA contact: The bug has been updated to refer to the pull request using the external bug tracker. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
@alebedev87: This pull request references Jira Issue OCPBUGS-14914, which is valid. 3 validation(s) were run on this bug
Requesting review from QA contact: In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
Tested it with 4.15.0-0.ci.test-2023-11-16-093712-ci-ln-dr45512-latest % oc get route % oc annotate route myedge haproxy.router.openshift.io/timeout-tunnel=70m % oc -n openshift-ingress rsh router-default-d78ddc6f-bprk5 sh-4.4$
sh-4.4# wget --no-check-certificate --limit-rate=1000 --delete-after https://myedge-default.apps.ci-ln-9r4m3fk-72292.origin-ci-int-gce.dev.rhcloud.com/oc -k oc.tmp 0%[ ] 239.88K 1000 B/s in 4m 6s --2023-11-17 04:38:15-- (try:20) https://myedge-default.apps.ci-ln-9r4m3fk-72292.origin-ci-int-gce.dev.rhcloud.com/oc oc.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp.tmp 3%[++++++ ] 4.62M 1000 B/s in 3m 55s 2023-11-17 04:42:09 (1000 B/s) - Read error at byte 4847481/121359408 (The TLS connection was non-properly terminated.). Giving up. sh-4.4#
04:41:53.345283 IP (tos 0x0, ttl 62, id 24165, offset 0, flags [DF], proto TCP (6), length 628) |
Back to WIP to address the config warning about missing
|
4113831
to
6096a39
Compare
/test e2e-agnostic |
/test e2e-metal-ipi-ovn-ipv6 |
@alebedev87: This pull request references Jira Issue OCPBUGS-14914, which is valid. 3 validation(s) were run on this bug
Requesting review from QA contact: The bug has been updated to refer to the pull request using the external bug tracker. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
I still see these errors in the most recent test results:
Did you mean remove the WIP label? |
/label qe-approved |
@alebedev87 could you add an e2e test for this? Also, I think we need to address the "middle server" issue. Could you add some more details in the description that explains the solution design? |
6096a39
to
ff40068
Compare
The warning about the server timeout in the middle backends is addressed by setting them with the maximum timeout among all the routes. This was one of the suggestions from the upstream issue. |
@candita: yes, I plan to add a test case for this fix in the cluster-ingress-operator's e2e. |
Tested it again with 4.16.0-0.test-2023-12-25-011937-ci-ln-2g1gk12-latest, WebSocket Client was disconnected after about 30 seconds. ` sh-4.4# ./run-cli.sh real 0m30.100s |
tested it with 4.16.0-0.test-2024-01-03-013347-ci-ln-mfg1zck-latest, the default clientTimeout is 30s, and the default erverTimeout is 300s, after configured clientTimeout and serverTimeout with 2h, websocket client was disconnected after 70m as expected % oc -n openshift-ingress-operator get ingresscontroller default -oyaml | grep -A3 tuningOptions: % oc -n tunnel-timeout get route hello-openshift -oyaml | grep -A1 annotations: sh-4.4# ./run-cli.sh real 70m0.112s % oc get clusterversion |
/label qe-approved |
Tested serverTimeout with 10s, 120s, 600s and the default, the function worked as expected in a created route
backend be_http:default:unsec-server3 timeout check 5000ms
backend be_http:default:unsec-server3 |
/retitle OCPBUGS-14914: set timeouts on the backend level Wrong PR was WIPed, sorry. |
/label qe-approved |
@@ -425,6 +424,9 @@ frontend fe_sni | |||
########################################################################## | |||
# backend for when sni does not exist, or ssl term needs to happen on the edge | |||
backend be_no_sni | |||
{{- with $value := maxTimeoutFirstMatchedAndClipped .State "haproxy.router.openshift.io/timeout" $timeSpecPattern (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s" }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you call the special maxTimeoutFirstMatchedAndClipped
here but continue to call clipHAProxyTimeoutValue
for other backend stanzas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want the maximum of all the backend timeouts to be set on the middle backends (be_sni
, be_no_sni
). Otherwise we get the warning during the config reloading.
@@ -312,6 +308,9 @@ frontend public_ssl | |||
# traffic | |||
########################################################################## | |||
backend be_sni | |||
{{- with $value := maxTimeoutFirstMatchedAndClipped .State "haproxy.router.openshift.io/timeout" $timeSpecPattern (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s" }} | |||
timeout server {{ $value }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this annotation ever being applied before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't understand which annotation you were referring to. timeout server
was not set in be_sni
before, it's needed to prevent this warning message.
The middle backends (be_sni, be_no_sni) are updated with the server timeout set to the maximum value among all routes from the configuration. This prevents a warning message during config reload.
ff40068
to
e165421
Compare
#536 (comment) addressed. |
@alebedev87: This pull request references Jira Issue OCPBUGS-14914, which is valid. 3 validation(s) were run on this bug
Requesting review from QA contact: The bug has been updated to refer to the pull request using the external bug tracker. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. |
/test e2e-upgrade |
@alebedev87: all tests passed! Full PR test history. Your PR dashboard. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here. |
Tested it with default, 10s, 90s, 70m on 4.16.0-0.test-2024-02-01-013554-ci-ln-g418bqb-latest
oc get clusterversionNAME VERSION AVAILABLE PROGRESSING SINCE STATUS sh-4.4# ./run-cli.sh real 0m15.130s real 0m10.132s real 0m30.128s sh-4.4# ./run-cli.sh real 1m30.138s sh-4.4# ./run-cli.sh real 70m0.140s
|
/label qe-approved |
@@ -312,6 +308,9 @@ frontend public_ssl | |||
# traffic | |||
########################################################################## | |||
backend be_sni | |||
{{- with $value := maxTimeoutFirstMatchedAndClipped .State "haproxy.router.openshift.io/timeout" $timeSpecPattern (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s" }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you need the same value in both be_sni
and be_no_sni
, you can avoid computing the value twice using something like {{- $maxServerTimeout := maxTimeoutFirstMatchedAndClipped .State "haproxy.router.openshift.io/timeout" $timeSpecPattern (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s" }}
at the top of the file. (Based on the other review comments, it might also be helpful to add a {{- /* comment */}}
at the same place explaining why we need to compute the max timeout.)
timeout server-fin {{ firstMatch $timeSpecPattern (env "ROUTER_DEFAULT_SERVER_FIN_TIMEOUT") "1s" }} | ||
timeout http-request {{ firstMatch $timeSpecPattern (env "ROUTER_SLOWLORIS_TIMEOUT") "10s" }} | ||
timeout http-keep-alive {{ firstMatch $timeSpecPattern (env "ROUTER_SLOWLORIS_HTTP_KEEPALIVE") "300s" }} | ||
|
||
# Long timeout for WebSocket connections. | ||
timeout tunnel {{ firstMatch $timeSpecPattern (env "ROUTER_DEFAULT_TUNNEL_TIMEOUT") "1h" }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you need to specify timeout tunnel
on be_sni
or be_no_sni
? Is HAProxy more lenient about not specifying timeout tunnel
than it is about not specifying timeout server
?
Does omitting timeout tunnel
result in an infinite timeout, similarly to omitting timeout server
, or does it fall back to using the server timeout as the tunnel timeout? Crucially, if you have a route that has a tunnel timeout that is greater than any route's server timeout, is the tunnel timeout respected, or does the server timeout in be_sni
/be_no_sni
cause the connection to be terminated before the route's tunnel timeout?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you need to specify timeout tunnel on be_sni or be_no_sni? Is HAProxy more lenient about not specifying timeout tunnel than it is about not specifying timeout server?
Yes. Only the missing server timeout gives a warning message. I'm not sure the warning reports a real problem though taking into account that the bottom backends will still have the server timeout set.
Does omitting timeout tunnel result in an infinite timeout, similarly to omitting timeout server, or does it fall back to using the server timeout as the tunnel timeout? Crucially, if you have a route that has a tunnel timeout that is greater than any route's server timeout, is the tunnel timeout respected, or does the server timeout in be_sni/be_no_sni cause the connection to be terminated before the route's tunnel timeout?
Regarding the link between server and tunnel timeouts. I discovered that the tunnel timeout supersedes server timeout in the TCP proxies. Note that the middle backends in the router configuration are TCP proxies. So setting tunnel timeout in default
or the middle backends (be_sni
/be_no_sni
) results into interference in the server timeout behavior set on the bottom backend level. I didn't see the opposite though: the server timeout set on default
or middle backends didn't influence the tunnel timeout set on the bottom backends.
@@ -581,10 +583,10 @@ backend {{ genBackendNamePrefix $cfg.TLSTermination }}:{{ $cfgIdx }} | |||
{{- end }} | |||
tcp-request content reject if !whitelist | |||
{{- end }} | |||
{{- with $value := clipHAProxyTimeoutValue (firstMatch $timeSpecPattern (index $cfg.Annotations "haproxy.router.openshift.io/timeout")) }} | |||
{{- with $value := clipHAProxyTimeoutValue (firstMatch $timeSpecPattern (index $cfg.Annotations "haproxy.router.openshift.io/timeout") (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s") }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these additional env
calls add some overhead. As Andy says, "Trips across the Go/Template boundary can be expensive, particularly when you have many, many routes (10,000+)." Consider adding {{- $defaultServerTimeout := firstMatch $timeSpecPattern (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s" }}
at the top of the template and using {{- with $value := clipHAProxyTimeoutValue (firstMatch $timeSpecPattern (index $cfg.Annotations "haproxy.router.openshift.io/timeout") $defaultServerTimeout) }}
here.
As noted in #483 (review), clipHAProxyTimeoutValue (firstMatch $timeSpecPattern foo)
could be simplified as clipHAProxyTimeoutValue foo
. However, that might be better left as a separate change.
...And actually, now that I look at it again, I believe clipHAProxyTimeoutValue (firstMatch $timeSpecPattern) foo
will silently ignore invalid values while clipHAProxyTimeoutValue foo
will log them. Logging errors for invalid values might be a positive change, but it's an additional behavioral change that you might not want to tie to OCPBUGS-14914.
...Although thinking about it still further, I wonder: Is maxTimeoutFirstMatchedAndClipped
going to cause clipHAProxyTimeoutValue
to log an error for each route that has an invalid annotation value, where before this change those invalid values get filtered out by firstMatch
before clipHAProxyTimeoutValue
has a chance to report any errors?
{{- with $value := clipHAProxyTimeoutValue (firstMatch $timeSpecPattern (index $cfg.Annotations "haproxy.router.openshift.io/timeout") (env "ROUTER_DEFAULT_SERVER_TIMEOUT") "30s") }} | ||
timeout server {{ $value }} | ||
{{- end }} | ||
{{- with $value := clipHAProxyTimeoutValue (firstMatch $timeSpecPattern (index $cfg.Annotations "haproxy.router.openshift.io/timeout-tunnel") (index $cfg.Annotations "haproxy.router.openshift.io/timeout") (env "ROUTER_DEFAULT_TUNNEL_TIMEOUT") "1h") }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace env "ROUTER_DEFAULT_SERVER_TIMEOUT"
and env "ROUTER_DEFAULT_TUNNEL_TIMEOUT"
each with a variable that is computed just once, if possible.
timeout := clipHAProxyTimeoutValue(firstMatch(pattern, append([]string{cfg.Annotations[annotation]}, values...)...)) | ||
if timeout != "" { | ||
// No error handling because clipHAProxyTimeoutValue returns | ||
// a valid timeout or an empty string. The latter is already handled. | ||
timeoutDuration, _ := time.ParseDuration(timeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a lot of redundant computation:
maxTimeoutFirstMatchedAndClipped
parses each route's timeout annotation value usingfirstMatch
, then (if the value is valid)clipHAProxyTimeoutValue
, thentime.ParseDuration
.fe_sni
callsmaxTimeoutFirstMatchedAndClipped
, so you're parsing each valid value 3 times.fe_no_sni
also callsmaxTimeoutFirstMatchedAndClipped
, so you're parsing each valid value 3 more times.- The backend calls
clipHAProxyTimeoutValue (firstMatch $timeSpecPattern ...)
, so you're parsing each valid value 2 more times.
Granted, the regexp cache that Andy added in #268 avoids the cost of compiling the same regexp repeatedly, but you're still repeatedly doing a regexp match and then time.ParseDuration
on the same value, right?
"parseIPList": parseIPList, //parses the list of IPs/CIDRs (IPv4/IPv6) | ||
"clipHAProxyTimeoutValue": clipHAProxyTimeoutValue, //clips extraordinarily high timeout values to be below the maximum allowed timeout value | ||
"parseIPList": parseIPList, //parses the list of IPs/CIDRs (IPv4/IPv6) | ||
"maxTimeoutFirstMatchedAndClipped": maxTimeoutFirstMatchedAndClipped, //finds the maximum timeout managed by a given annotation among all the routes, matches it against a given pattern, and clips |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep in mind that this list of helper functions is effectively an API. We cannot rename or remove a helper function without potentially breaking third-party templates, so we tend to prefer defining general and composable helpers. That said, I think it is worth putting this functionality in a single specialized helper to avoid excessive performance impact.
annotation string | ||
pattern string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add test cases with different values for annotation
and pattern
?
{ | ||
name: "Route timeout clipped", | ||
state: map[ServiceAliasConfigKey]ServiceAliasConfig{ | ||
"test:route1": { | ||
Annotations: map[string]string{ | ||
"haproxy.router.openshift.io/timeout": "999999999s", | ||
}, | ||
}, | ||
}, | ||
annotation: "haproxy.router.openshift.io/timeout", | ||
pattern: `[1-9][0-9]*(us|ms|s|m|h|d)?`, | ||
values: []string{"30s"}, | ||
expected: "2147483647ms", | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about a test case with values: []string{"999999999s"}
?
name: "Default timeout is maximum", | ||
state: map[ServiceAliasConfigKey]ServiceAliasConfig{ | ||
"test:route1": {}, | ||
"test:route2": {}, | ||
"test:route3": {}, | ||
}, | ||
annotation: "haproxy.router.openshift.io/timeout", | ||
pattern: `[1-9][0-9]*(us|ms|s|m|h|d)?`, | ||
values: []string{"30s"}, | ||
expected: "30s", | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should at least one of the routes specify a value? It makes sense to me to have a test case for "some routes specify timeouts, but the default timeout is maximum" .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, what happens if you have 0 routes?
This PR removes the tunnel and server timeouts in the global
default
section and sets them on all the backends. The route annotations for the timeouts continue to work as they did before.As suggested in the upstream issue, the middle backends (
be_sni
andbe_no_sni
) are set with the maximum of all the route timeouts to avoid the following warning message:Test using haproxy-timeout-tunnel repository:
The e2e test: openshift/cluster-ingress-operator#1013.