Skip to content

Commit ccc932e

Browse files
Anddd7dkostyrev
authored andcommitted
feat: Add grpc timeouts annotations (kubernetes#11258)
* ✨ feat: add grpc timeouts with proxy settings if backend is grpc * πŸ“ docs: Documentation only changes * πŸ› fix: uppercase for protocol * πŸ“ docs: grpc timeouts example * πŸ“ docs: add links and default values for proxy timeout * πŸ§ͺ test: add e2e test for timeout * πŸ› fix: upgrade to 1.0.6 to fix nil pointer * πŸ› fix: lint * πŸ§ͺ test: trigger ci
1 parent 88b1804 commit ccc932e

File tree

8 files changed

+224
-7
lines changed

8 files changed

+224
-7
lines changed

β€Ždocs/examples/grpc/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,9 @@ This example demonstrates how to route traffic to a gRPC service through the Ing
166166
167167
### Notes on using response/request streams
168168

169+
> `grpc_read_timeout` and `grpc_send_timeout` will be set as `proxy_read_timeout` and `proxy_send_timeout` when you set backend protocol to `GRPC` or `GRPCS`.
170+
169171
1. If your server only does response streaming and you expect a stream to be open longer than 60 seconds, you will have to change the `grpc_read_timeout` to accommodate this.
170172
2. If your service only does request streaming and you expect a stream to be open longer than 60 seconds, you have to change the
171173
`grpc_send_timeout` and the `client_body_timeout`.
172174
3. If you do both response and request streaming with an open stream longer than 60 seconds, you have to change all three timeouts: `grpc_read_timeout`, `grpc_send_timeout` and `client_body_timeout`.
173-
174-
Values for the timeouts must be specified as e.g. `"1200s"`.
175-
176-
> On the most recent versions of ingress-nginx, changing these timeouts requires using the `nginx.ingress.kubernetes.io/server-snippet` annotation. There are plans for future releases to allow using the Kubernetes annotations to define each timeout separately.

β€Ždocs/user-guide/nginx-configuration/annotations.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,12 @@ In some scenarios is required to have different values. To allow this we provide
698698
- `nginx.ingress.kubernetes.io/proxy-next-upstream-tries`
699699
- `nginx.ingress.kubernetes.io/proxy-request-buffering`
700700

701+
If you indicate [Backend Protocol](#backend-protocol) as `GRPC` or `GRPCS`, the following grpc values will be set and inherited from proxy timeouts:
702+
703+
- [`grpc_connect_timeout=5s`](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_connect_timeout), from `nginx.ingress.kubernetes.io/proxy-connect-timeout`
704+
- [`grpc_send_timeout=60s`](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_send_timeout), from `nginx.ingress.kubernetes.io/proxy-send-timeout`
705+
- [`grpc_read_timeout=60s`](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_read_timeout), from `nginx.ingress.kubernetes.io/proxy-read-timeout`
706+
701707
Note: All timeout values are unitless and in seconds e.g. `nginx.ingress.kubernetes.io/proxy-read-timeout: "120"` sets a valid 120 seconds proxy read timeout.
702708

703709
### Proxy redirect

β€Ždocs/user-guide/nginx-configuration/configmap.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,14 +1101,20 @@ See NGINX [client_max_body_size](https://nginx.org/en/docs/http/ngx_http_core_mo
11011101

11021102
Sets the timeout for [establishing a connection with a proxied server](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_connect_timeout). It should be noted that this timeout cannot usually exceed 75 seconds.
11031103

1104+
It will also set the [grpc_connect_timeout](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_connect_timeout) for gRPC connections.
1105+
11041106
## proxy-read-timeout
11051107

11061108
Sets the timeout in seconds for [reading a response from the proxied server](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout). The timeout is set only between two successive read operations, not for the transmission of the whole response.
11071109

1110+
It will also set the [grpc_read_timeout](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_read_timeout) for gRPC connections.
1111+
11081112
## proxy-send-timeout
11091113

11101114
Sets the timeout in seconds for [transmitting a request to the proxied server](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_send_timeout). The timeout is set only between two successive write operations, not for the transmission of the whole request.
11111115

1116+
It will also set the [grpc_send_timeout](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_send_timeout) for gRPC connections.
1117+
11121118
## proxy-buffers-number
11131119

11141120
Sets the number of the buffer used for [reading the first part of the response](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers) received from the proxied server. This part usually contains a small response header.

β€Žgo.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ require (
4848
)
4949

5050
require (
51+
github.com/Anddd7/pb v0.0.0-20240425032658-369b0f6a404c
5152
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
5253
github.com/BurntSushi/toml v1.3.2 // indirect
5354
github.com/beorn7/perks v1.0.1 // indirect

β€Žgo.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,8 @@ cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcP
597597
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
598598
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
599599
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
600+
github.com/Anddd7/pb v0.0.0-20240425032658-369b0f6a404c h1:uhBf0CHXi7nCFZXxHV7l1cBcYFEEVRK4FYxvm1l9lKg=
601+
github.com/Anddd7/pb v0.0.0-20240425032658-369b0f6a404c/go.mod h1:vYWKbnXd2KAZHUECLPzSE0Er3FgiEmOdPtxwSIRihck=
600602
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
601603
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
602604
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=

β€Žrootfs/etc/nginx/template/nginx.tmpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,13 @@ stream {
14811481
proxy_next_upstream_timeout {{ $location.Proxy.NextUpstreamTimeout }};
14821482
proxy_next_upstream_tries {{ $location.Proxy.NextUpstreamTries }};
14831483

1484+
{{ if or (eq $location.BackendProtocol "GRPC") (eq $location.BackendProtocol "GRPCS") }}
1485+
# Grpc settings
1486+
grpc_connect_timeout {{ $location.Proxy.ConnectTimeout }}s;
1487+
grpc_send_timeout {{ $location.Proxy.SendTimeout }}s;
1488+
grpc_read_timeout {{ $location.Proxy.ReadTimeout }}s;
1489+
{{ end }}
1490+
14841491
{{/* Add any additional configuration defined */}}
14851492
{{ $location.ConfigurationSnippet }}
14861493

β€Žtest/e2e/annotations/grpc.go

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import (
2222
"fmt"
2323
"strings"
2424

25+
delaypb "github.com/Anddd7/pb/grpcbin"
2526
pb "github.com/moul/pb/grpcbin/go-grpc"
2627
"github.com/onsi/ginkgo/v2"
2728
"github.com/stretchr/testify/assert"
2829
"google.golang.org/grpc"
2930
"google.golang.org/grpc/credentials"
31+
"google.golang.org/grpc/credentials/insecure"
3032
"google.golang.org/grpc/metadata"
3133
corev1 "k8s.io/api/core/v1"
3234
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,16 +37,17 @@ import (
3537
"k8s.io/ingress-nginx/test/e2e/framework"
3638
)
3739

38-
const echoHost = "echo"
40+
const (
41+
echoHost = "echo"
42+
host = "grpc"
43+
)
3944

4045
var _ = framework.DescribeAnnotation("backend-protocol - GRPC", func() {
4146
f := framework.NewDefaultFramework("grpc", framework.WithHTTPBunEnabled())
4247

4348
ginkgo.It("should use grpc_pass in the configuration file", func() {
4449
f.NewGRPCFortuneTellerDeployment()
4550

46-
host := "grpc"
47-
4851
annotations := map[string]string{
4952
"nginx.ingress.kubernetes.io/backend-protocol": "GRPC",
5053
}
@@ -259,4 +262,89 @@ var _ = framework.DescribeAnnotation("backend-protocol - GRPC", func() {
259262
metadata := res.GetMetadata()
260263
assert.Equal(ginkgo.GinkgoT(), metadata["content-type"].Values[0], "application/grpc")
261264
})
265+
266+
ginkgo.It("should return OK when request not exceed timeout", func() {
267+
f.NewGRPCBinDelayDeployment()
268+
269+
proxyTimeout := "10"
270+
ingressName := "grpcbin-delay"
271+
272+
annotations := make(map[string]string)
273+
annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "GRPC"
274+
annotations["nginx.ingress.kubernetes.io/proxy-connect-timeout"] = proxyTimeout
275+
annotations["nginx.ingress.kubernetes.io/proxy-send-timeout"] = proxyTimeout
276+
annotations["nginx.ingress.kubernetes.io/proxy-read-timeout"] = proxyTimeout
277+
278+
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, ingressName, 50051, annotations)
279+
280+
f.EnsureIngress(ing)
281+
282+
f.WaitForNginxServer(host,
283+
func(server string) bool {
284+
return strings.Contains(server, fmt.Sprintf("grpc_connect_timeout %ss;", proxyTimeout)) &&
285+
strings.Contains(server, fmt.Sprintf("grpc_send_timeout %ss;", proxyTimeout)) &&
286+
strings.Contains(server, fmt.Sprintf("grpc_read_timeout %ss;", proxyTimeout))
287+
})
288+
289+
conn, err := grpc.Dial(
290+
f.GetNginxIP()+":80",
291+
grpc.WithTransportCredentials(insecure.NewCredentials()),
292+
grpc.WithAuthority(host),
293+
)
294+
assert.Nil(ginkgo.GinkgoT(), err, "error creating a connection")
295+
defer conn.Close()
296+
297+
client := delaypb.NewGrpcbinServiceClient(conn)
298+
299+
res, err := client.Unary(context.Background(), &delaypb.UnaryRequest{
300+
Data: "hello",
301+
})
302+
assert.Nil(ginkgo.GinkgoT(), err)
303+
304+
metadata := res.GetResponseAttributes().RequestHeaders
305+
assert.Equal(ginkgo.GinkgoT(), metadata["content-type"], "application/grpc")
306+
assert.Equal(ginkgo.GinkgoT(), metadata[":authority"], host)
307+
})
308+
309+
ginkgo.It("should return Error when request exceed timeout", func() {
310+
f.NewGRPCBinDelayDeployment()
311+
312+
proxyTimeout := "10"
313+
ingressName := "grpcbin-delay"
314+
315+
annotations := make(map[string]string)
316+
annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "GRPC"
317+
annotations["nginx.ingress.kubernetes.io/proxy-connect-timeout"] = proxyTimeout
318+
annotations["nginx.ingress.kubernetes.io/proxy-send-timeout"] = proxyTimeout
319+
annotations["nginx.ingress.kubernetes.io/proxy-read-timeout"] = proxyTimeout
320+
321+
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, ingressName, 50051, annotations)
322+
323+
f.EnsureIngress(ing)
324+
325+
f.WaitForNginxServer(host,
326+
func(server string) bool {
327+
return strings.Contains(server, fmt.Sprintf("grpc_connect_timeout %ss;", proxyTimeout)) &&
328+
strings.Contains(server, fmt.Sprintf("grpc_send_timeout %ss;", proxyTimeout)) &&
329+
strings.Contains(server, fmt.Sprintf("grpc_read_timeout %ss;", proxyTimeout))
330+
})
331+
332+
conn, err := grpc.Dial(
333+
f.GetNginxIP()+":80",
334+
grpc.WithTransportCredentials(insecure.NewCredentials()),
335+
grpc.WithAuthority(host),
336+
)
337+
assert.Nil(ginkgo.GinkgoT(), err, "error creating a connection")
338+
defer conn.Close()
339+
340+
client := delaypb.NewGrpcbinServiceClient(conn)
341+
342+
_, err = client.Unary(context.Background(), &delaypb.UnaryRequest{
343+
Data: "hello",
344+
RequestAttributes: &delaypb.RequestAttributes{
345+
Delay: 15,
346+
},
347+
})
348+
assert.Error(ginkgo.GinkgoT(), err)
349+
})
262350
})

β€Žtest/e2e/framework/grpc_delay.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package framework
18+
19+
import (
20+
"github.com/onsi/ginkgo/v2"
21+
"github.com/stretchr/testify/assert"
22+
appsv1 "k8s.io/api/apps/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/fields"
26+
"k8s.io/apimachinery/pkg/util/intstr"
27+
)
28+
29+
// NewGRPCBinDelayDeployment creates a new single replica
30+
// deployment of the grpcbin image in a particular namespace
31+
func (f *Framework) NewGRPCBinDelayDeployment() {
32+
f.NewNewGRPCBinDelayDeploymentWithReplicas(1)
33+
}
34+
35+
// NewNewGRPCBinDelayDeploymentWithReplicas creates a new deployment of the
36+
// grpcbin image in a particular namespace. Number of replicas is configurable
37+
func (f *Framework) NewNewGRPCBinDelayDeploymentWithReplicas(replicas int32) {
38+
name := "grpcbin-delay"
39+
40+
deployment := &appsv1.Deployment{
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: name,
43+
Namespace: f.Namespace,
44+
},
45+
Spec: appsv1.DeploymentSpec{
46+
Replicas: NewInt32(replicas),
47+
Selector: &metav1.LabelSelector{
48+
MatchLabels: map[string]string{
49+
"app": name,
50+
},
51+
},
52+
Template: corev1.PodTemplateSpec{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Labels: map[string]string{
55+
"app": name,
56+
},
57+
},
58+
Spec: corev1.PodSpec{
59+
TerminationGracePeriodSeconds: NewInt64(0),
60+
Containers: []corev1.Container{
61+
{
62+
Name: name,
63+
Image: "ghcr.io/anddd7/grpcbin:v1.0.6",
64+
Env: []corev1.EnvVar{},
65+
Ports: []corev1.ContainerPort{
66+
{
67+
Name: "grpc",
68+
ContainerPort: 50051,
69+
},
70+
},
71+
},
72+
},
73+
},
74+
},
75+
},
76+
}
77+
78+
d := f.EnsureDeployment(deployment)
79+
80+
err := waitForPodsReady(f.KubeClientSet, DefaultTimeout, int(replicas), f.Namespace, &metav1.ListOptions{
81+
LabelSelector: fields.SelectorFromSet(fields.Set(d.Spec.Template.ObjectMeta.Labels)).String(),
82+
})
83+
assert.Nil(ginkgo.GinkgoT(), err, "failed to wait for to become ready")
84+
85+
service := &corev1.Service{
86+
ObjectMeta: metav1.ObjectMeta{
87+
Name: name,
88+
Namespace: f.Namespace,
89+
},
90+
Spec: corev1.ServiceSpec{
91+
Ports: []corev1.ServicePort{
92+
{
93+
Name: "grpc",
94+
Port: 50051,
95+
TargetPort: intstr.FromInt(50051),
96+
Protocol: "TCP",
97+
},
98+
},
99+
Selector: map[string]string{
100+
"app": name,
101+
},
102+
},
103+
}
104+
105+
f.EnsureService(service)
106+
107+
err = WaitForEndpoints(f.KubeClientSet, DefaultTimeout, name, f.Namespace, int(replicas))
108+
assert.Nil(ginkgo.GinkgoT(), err, "waiting for endpoints to become ready")
109+
}

0 commit comments

Comments
Β (0)