在k8s中,我们经常透过Ingress resource来对外暴露服务,在实际生产中,我们可能还会需要对这些暴露的服务实现限流,这个能力一方面可以通过k8s内部的微网关来实现(比如将NGINX部署微k8s内的微网关),也可能会通过sidecar的方式实现对每个pod单元的限流(如将NGINX作为sidecar部署到业务pod内,并通过NGINX集群同步的方式实现整体限流)。
但如果我们是一个比较典型的南北流量,且内部并没有部署微网关或者sidecar,那么可以由应用自身来解决这个问题,尽管应用自己可以解决该问题,但是作为现代基础架构显然需要有更多的能力来减少应用的非功能开发,从而加快应用的开发速度和简化应用代码。
在k8s的入口进行限流,根据实际Ingres controller(IC)的实现,有多种不同的方法,比如,如果是F5作为ingress controller,那么可以通过F5的本身的直接配置实现连接限制,也可以借助iRule实现更加复杂的基于7层的请求限流。但是有时候即便将F5作为Ingress controller但受制于组织关系的不同,业务部门未必能够通过k8s较为原生的方式来直接调整F5上的配置。我们还会见到客户将NGINX用作七层Ingress controller,而将F5作为四层IC,这个时候业务或者应用管理者更希望通过自己更加熟悉的NGINX来进行七层的请求限流。以下我们就从NGINX的角度来看如何在IC上实现限流。
挑战
当我们需要在Ingress Controller进行服务限流时,由于IC的特性,用户往往希望有更加熟悉和简洁的配置方式,而不希望在决定执行限流的时候修改大量的yaml配置和重新加载IC的配置,因为这可能会导致错误的部署或者性能的降级。
由于IC一般都是多实例,构建IC集群则需要集群能够与k8s配合实现对IC实例变化的及时感知,动态的管理集群内的IC实例。限流控制粒度需以集群层面来管控,这就要求IC能够对状态性数据在集群内同步。限流的执行动作也需要简化,一个API call就可以启动或者关闭整个集群的限流动作,无需对每个IC实例执行API call
IC上的配置的业务一般有两种形态,一是复用同一个域名借助不同的URI path来路由到后端不同的svc endpoints上,二是使用不同的域名路由不同的svc endpoints,这就使得限流要能够灵活的基于这两种不同的维度进行,可以针对不同的URI path,也可以针对不同的hostname进行限流
最后就是对整个集群的限流结果,共享信息的同步状态都能有对应的metrics输出或者Dashboard来展现。
NGINX本身提供了较好的限流机制,可以根据不同的维度信息来实现限流,结合Plus的keyval可以实现更强大的run time的限流动作,同时结合k8s的接口进行集群的自动化管理,本文讲解如何在k8s的Ingress controller场景下实现免重载配置来实现业务限流动作。
下一篇文章则重点关注在IC集群的构建与限流状态同步方面。
在以下的讲解中将不关注NGINX本身关于限流以及keyval的配置,文章假设读者已经了解相关配置方法,如果您对NGINX本身如何实现限流以及keyval API控制不了解的话,建议先观看以下B站的F5 Networks培训视频:
https://www.bilibili.com/video/av93331807?from=search&seid=17474344322595621253
https://www.bilibili.com/video/av92079372?from=search&seid=17474344322595621253
需求描述:
在一个域名下的两个不同path,分别由两个不同的k8s svc提供服务
1. 需要实现对每个不同的svc进行请求级限,限制不同客户端的请求速率,并能够实现免重载方式来控制是否启动限流
2. 如果有必要,通过免重载方式实现该域名的整体级别服务限制
需求分析:
上述两个需求,都需要免重载方式来控制,这可以通过Plus的API来控制keyval来实现,通过keyval实现一个KV开关,可以控制针对不同uri以及域名来开启
需求还需要对同一域名的不同的svc来控制,对于ingress,这里的两个不同svc在NGINX上表现为两个不同的location,因此需要实现针对location来控制是否开启限流功能;同样,对域名的整体限流则可以通过对 server_name进行限流控制。
需求实现:
首先由于IC下的NGINX配置方式和正常的NGINX配置方法有所不同,但归根结底最后的配置是NGINX的配置,所以我们首先来看上述需求在NGINX的伪配置是怎么样的,然后我们只需要将这些配置按照IC下的配置方式来实现配置即可。
伪配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
http block: ########### 设置一个keyval,里面存储KV键值对,例如json格式: { "limitper_server": { “cafe.example.com”: "1" }, "limitreq_uri": { "/coffee": "1", "/tea": "0" } } ########## #上述Key的value通过NGINX Plus的API来控制调整,无需reload配置 keyval_zone zone=limitreq_uri:64k type=prefix; keyval $uri $enablelimit zone=limitreq_uri; #根据请求的uri在这里通过前缀匹配的方式来查找,比如 /coffee/latte这个请求,将会匹配 "/coffee" 从而获取值=1,这个值将会被放入变量$enablelimit中 ##### keyval_zone zone=limitper_server:64k; keyval $server_name $enableserverlimit zone=limitper_server; #根据server的匹配,如果$server_name等于cafe.example.com 那么keyval查找的结果就是1,这个1会被放入变量$enableserverlimit中 map $enablelimit $limit_key { default ""; 1 $binary_remote_addr; } ##上述配置,根据变量$enablelimit的值,决定 $limit_key的值,例如如果$enablelimit=1, 那么$limit_key 此时的值是$binary_remote_addr所代表的客户端地址的二进制值,如果$enablelimit值为其它的,则选择default的值,这里为空 map $enableserverlimit $limit_key_servername { default ""; 1 $server_name; } ##类似以上解释,这里是根据变量$enableserverlimit的值来决定 $limit_key_servername的值 limit_req_zone $limit_key zone=req_zone_10:1m rate=10r/s; limit_req_zone $limit_key zone=req_zone_20:1m rate=20r/s; limit_req_zone $limit_key_servername zone=perserver:10m rate=50r/s; ##利用map获取到的$limit_key或$limit_key_servername来配置不同的limit_req_zone,这里根据客户端地址($binary_remote_addr)配置两个两个不同的zone,分别对应不同的限制速率。 ##针对server_name($limit_key_servername)的匹配设置一个perserver的zone,限制速率为50RPS ##注意,这里有个小技巧,如果$limit_key 或 $limit_key_servername的值为空,那么后面引用该limit_req_zone的命令实际不会生效 location block: location /coffe { limit_req zone=req_zone_10 burst=1 nodelay; limit_req zone=perserver nodelay; #调用上述zone,实现真正的限流,这里配置了两个限流条件。如果这两个条件都生效,那么在这里是同时限制,既然限制访问该URI的客户端速率,有限制该server——name的总速率,如果任意一个有效,则该条生效。 root /usr/share/nginx/html; } location /tea { limit_req zone=req_zone_20 burst=5 nodelay; limit_req zone=perserver nodelay; #同coffee的解释 root /usr/share/nginx/html; } |
从上述的配置可以看出,我们只要通过控制keyval的值,就可以控制让不同location里的limit_req是否生效,而且可以分开独立控制。
NGINX IC上的实现:
这里基础的服务和Ingress依旧以经典的cafe.example.com来部署两个不同的svc,tea和coffee
上述配置中,可以看到http 上下文中有一段配置,location上下文中也有一段配置,这些配置在NGINX IC中所提供的configmap或者annotation里都没有对应的配置,所以我们需要通过configmap中的http snippets,以及ingress annotations中的location snippets来实现这些配置的注入.
- http上下文中的配置注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
[root@k8s-master-v1-16 common]# pwd /root/selab/kubernetes-ingress/deployments/common [root@k8s-master-v1-16 common]# cat nginx-config-keyval-map.yaml kind: ConfigMap apiVersion: v1 metadata: name: nginx-config namespace: nginx-ingress data: http-snippets: | #keyval_zone zone=limitreq_uri:64k type=prefix; keyval_zone zone=limitreq_uri:64k; keyval $uri $enablelimit zone=limitreq_uri; keyval_zone zone=limitper_server:64k; keyval $server_name $enableserverlimit zone=limitper_server; map $enablelimit $limit_key { default ""; 1 $binary_remote_addr; } map $enableserverlimit $limit_key_servername { default ""; 1 $server_name; } limit_req_zone $limit_key zone=req_zone_10:1m rate=10r/s; limit_req_zone $limit_key zone=req_zone_20:1m rate=20r/s; limit_req_zone $limit_key_servername zone=perserver:10m rate=50r/s; server { listen 9999; root /usr/share/nginx/html; access_log off; allow 172.16.0.0/16; allow 192.168.1.0/24; deny all; location /api { api write=on; } } |
上述配置中,通过NGINX IC所读取的nginx-config 这个configmap来填入snippets,其中的data内容将会被写入NGINX的http区块下
- location中的注入:
location是非全局配置,需要通过Ingress的annotation来写入,注意以下代码中的location-snippets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[root@k8s-master-v1-16 complete-example]# pwd /root/selab/kubernetes-ingress/examples/complete-example [root@k8s-master-v1-16 complete-example]# cat cafe-ingress-annotation.yaml apiVersion: extensions/v1beta1 kind: Ingress metadata: name: cafe-ingress annotations: nginx.org/location-snippets: | limit_req zone=req_zone_10 burst=1 nodelay; limit_req zone=perserver nodelay; spec: tls: - hosts: - cafe.example.com secretName: cafe-secret rules: - host: cafe.example.com http: paths: - path: /tea backend: serviceName: tea-svc servicePort: 80 - path: /coffee backend: serviceName: coffee-svc servicePort: 80 |
通过kubectl创建以上configmap以及Ingress
测试:
首先,需要通过keyval的API来写入相关KV键值,
curl -X GET "http://172.16.10.211:7777/api/5/http/keyvals/" -H "accept: application/json"
假设最终写入的配置如下:
1 2 3 4 5 6 7 |
{ "limitper_server": {}, "limitreq_uri": { "/coffee": "1", "/tea": "0" } } |
上述配置可以看出,只对/coffee的访问进行速率限制,来测试验证一下,简单的发一些请求,看输出,可以看到很多非200的错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Server Software: nginx/1.17.3 Server Hostname: cafe.example.com Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384,2048,256 Server Temp Key: ECDH X25519 253 bits TLS Server Name: cafe.example.com Document Path: /coffee Document Length: 158 bytes Concurrency Level: 100 Time taken for tests: 12.520 seconds Complete requests: 465 Failed requests: 425 (Connect: 0, Receive: 0, Length: 425, Exceptions: 0) Non-2xx responses: 425 |
而NGINX的日志可以看到类似如下的日志信息,可以看出针对/coffee的限制奇效了
1 2 |
[root@k8s-master-v1-16 complete-example]# kubectl logs nginx-ingress-794778674c-sdqh8 -n nginx-ingress --tail=20 2020/03/15 14:58:26 [error] 54#54: *6506 limiting requests, excess: 1.960 by zone "req_zone_10", client: 192.168.1.254, server: cafe.example.com, request: "GET /coffee HTTP/1.0", host: "cafe.example.com" |
注:限流的信息统计也可以通过NGINX API接口curl -X GET "http://your-nginx-ip/api/6/http/limit_reqs/" -H "accept: application/json"来获取,获得的返回格式类似:
1 2 3 4 5 6 7 |
{ "passed": 15, "delayed": 4, "rejected": 0, "delayed_dry_run": 1, "rejected_dry_run": 2 } |
如果对/tea发送请求,可以看到统计中没有错误统计,而nginx日志中无limiting的日志
1 2 3 4 5 6 7 |
Document Path: /tea Document Length: 153 bytes Concurrency Level: 100 Time taken for tests: 20.719 seconds Complete requests: 1000 Failed requests: 0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[root@k8s-master-v1-16 complete-example]# kubectl logs nginx-ingress-794778674c-sdqh8 -n nginx-ingress --tail=20 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" 192.168.1.254 - - [15/Mar/2020:15:02:51 +0000] "GET /tea HTTP/1.0" 200 153 "-" "ApacheBench/2.3" "-" |
上述测试说明针对path的控制已经生效
进一步测试,将针对path的限流取消,改为对server_name的限流,这个时候,无论访问哪个path都将发生限流. keyval如下:
1 2 3 4 5 6 7 8 9 |
{ "limitper_server": { "cafe.example.com": "1" }, "limitreq_uri": { "/coffee": "0", "/tea": "0" } } |
发起/tea或者/coffee的请求,会发现统计中都有大量非200错误,且日志提示了限流的启动:
1 2 3 4 5 6 7 8 9 10 |
Document Path: /tea Document Length: 153 bytes Concurrency Level: 100 Time taken for tests: 11.588 seconds Complete requests: 1000 Failed requests: 914 (Connect: 0, Receive: 0, Length: 914, Exceptions: 0) 2020/03/15 15:10:33 [error] 54#54: *9650 limiting requests, excess: 0.600 by zone "perserver", client: 192.168.1.254, server: cafe.example.com, request: "GET /tea HTTP/1.0", host: "cafe.example.com" |
1 2 3 4 5 6 7 |
Concurrency Level: 100 Time taken for tests: 20.065 seconds Complete requests: 1000 Failed requests: 901 (Connect: 0, Receive: 0, Length: 901, Exceptions: 0) 2020/03/15 15:11:41 [error] 53#53: *10586 limiting requests, excess: 0.100 by zone "perserver", client: 192.168.1.254, server: cafe.example.com, request: "GET /coffee HTTP/1.0", host: "cafe.example.com" |
针对不同svc配置不同速率的限制
如果你仔细的看上述内容,应该已经注意到,ingress的annotation里写的只是zone=req_zone_10 这个zone指定的速率限制,这实际上会导致不同的location都配置这同样的注入。那么如何实现不同的path引用不同的limit zone呢?这里就需要NGINX支持的mergeable type的ingress,所谓mergeable ingress就是NGINX容许在k8s里配置一个master类型的ingress,这个ingress里内容会和子类型“minion”的ingress来合并。因此上述的cafe-ingress可以改写以下配置,在不同的子ingress下配置不同的limit req zone:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
[root@k8s-master-v1-16 complete-example]# cat cafe-ingress-mergeable.yaml apiVersion: extensions/v1beta1 kind: Ingress metadata: name: cafe-ingress-master annotations: kubernetes.io/ingress.class: "nginx" nginx.org/mergeable-ingress-type: "master" spec: tls: - hosts: - cafe.example.com secretName: cafe-secret rules: - host: cafe.example.com --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: cafe-ingress-teasvc-minion annotations: kubernetes.io/ingress.class: "nginx" nginx.org/mergeable-ingress-type: "minion" nginx.org/location-snippets: | limit_req zone=req_zone_10 burst=1 nodelay; limit_req zone=perserver nodelay; spec: rules: - host: cafe.example.com http: paths: - path: /tea backend: serviceName: tea-svc servicePort: 80 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: cafe-ingress-coffeesvc-minion annotations: kubernetes.io/ingress.class: "nginx" nginx.org/mergeable-ingress-type: "minion" nginx.org/location-snippets: | limit_req zone=req_zone_20 burst=1 nodelay; limit_req zone=perserver nodelay; spec: rules: - host: cafe.example.com http: paths: - path: /coffee backend: serviceName: coffee-svc servicePort: 80 |
写入NGINX的最终配置为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
location /coffee { # location for minion default/cafe-ingress-coffeesvc-minion proxy_http_version 1.1; limit_req zone=req_zone_20 burst=1 nodelay; limit_req zone=perserver nodelay; proxy_connect_timeout 60s; proxy_read_timeout 60s; proxy_send_timeout 60s; client_max_body_size 1m; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering on; proxy_pass http://default-cafe-ingress-coffeesvc-minion-cafe.example.com-coffee-svc-80; } location /tea { # location for minion default/cafe-ingress-teasvc-minion proxy_http_version 1.1; limit_req zone=req_zone_10 burst=1 nodelay; limit_req zone=perserver nodelay; proxy_connect_timeout 60s; proxy_read_timeout 60s; proxy_send_timeout 60s; client_max_body_size 1m; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering on; proxy_pass http://default-cafe-ingress-teasvc-minion-cafe.example.com-tea-svc-80; } |
从上述的NGINX最终配置可以看出,不同的location以及具备了不同的限流条件。
总结
通过对keyval(plus特性),变量map,以及巧用limit_req_zone的limitkey的赋值来控制限流是否生效,同时在IC中借助灵活的snippets快速扩展IC本身未提供的configmap key或annotations,实现更加灵活的NGINX IC配置。Keyval的API接口使得可以在无需reload NGINX配置文件的前提下实现限流的动态性。
运维系统可通过prometheus等工具通过NGINX暴露的metrics对业务API的upstream或者location等维度统计信息进行采集(例如RPS、http状态响应码、pod单元的业务延迟时间、业务服务pod健康度),设置相关指标,当指标满足条件后触发报警或响应动作,相关的自动化工具只需直接调度NGINX 的API接口即可快速进行业务API的限流。
后续
在上述实践中,最终我们实现了基于API动态的控制限流,但在整个实践中都是以个IC实例来测试的,没有考虑在多IC实例的情况下,如何基于NGINX集群级别来进行限流,同时也没有考虑keyval配置的持久化问题。下一篇将就这两点进行阐述。
原创内容,转载请注明本站
https://myf5.net
文章评论