mac 上学习k8s系列(9)nginx-ingress lua

2022-08-02 19:25:08 浏览数 (1)

本篇将在mac 上学习k8s系列(8)external auth的基础上基于nginx-ingress 的access_by_lua_block redis 来实现一个全局的rate limiter:用nginx lua连接redis,用redis计数来做集群粒度的rate-limit。

ngx_lua 模块提供了配置指令 access_by_lua,用于在 access 请求处理阶段插入用户 Lua 代码。这条指令运行于 access 阶段的末尾,因此总是在 allow 和 deny 这样的指令之后运行,虽然它们同属 access 阶段。一般我们通过 access_by_lua 在 ngx_access 这样的模块检查过客户端 IP 地址之后,再通过 Lua 代码执行一系列更为复杂的请求验证操作,比如实时查询数据库或者其他后端服务,以验证当前用户的身份或权限。

首先,我们启动一个简单的redis服务,一般redis、mysql等有状态的服务,我们一般通过pv使用nfs服务存储,并且使用使用StatefulSet创建redis-cluster集群节点和headless service,保证每个pod的ip不变。本文重点介绍如何使用access_by_lua_block,所以搭建一个简单的无状态的redis。

创建一个ConfigMap来保存redis的配置:

代码语言:javascript复制
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: redis-config
  labels:
    app: redis
data:
  redis.conf: |-
    dir /srv
    port 6379
    bind 0.0.0.0
    appendonly yes
    daemonize no
    #protected-mode no
    requirepass 123456
    pidfile /srv/redis-6379.pid

然后通过DeployMent启动redis的pod

代码语言:javascript复制
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  labels:
    app: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:latest
        command:
          - "sh"
          - "-c"
          - "redis-server /usr/local/redis/redis.conf"
        ports:
        - containerPort: 6379
        resources:
          limits:
            cpu: 1000m
            memory: 1024Mi
          requests:
            cpu: 1000m
            memory: 1024Mi
        livenessProbe:
          tcpSocket:
            port: 6379
          initialDelaySeconds: 300
          timeoutSeconds: 1
          periodSeconds: 10
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          tcpSocket:
            port: 6379
          initialDelaySeconds: 5
          timeoutSeconds: 1
          periodSeconds: 10
          successThreshold: 1
          failureThreshold: 3
        volumeMounts:
        - name: config
          mountPath:  /usr/local/redis/redis.conf
          subPath: redis.conf
      volumes:
      - name: config
        configMap:
          name: redis-config

启动redis服务

代码语言:javascript复制
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
spec:
  type: NodePort
  ports:
    - name: tcp
      port: 6379
      nodePort: 30379
  selector:
    app: redis

测试下redis服务是否正常启动了:

代码语言:javascript复制
 %redis-cli -h 127.0.0.1 -p 30379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:30379>

接着,我们创建Ingress

代码语言:javascript复制
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-with-auth
  annotations:
    nginx.ingress.kubernetes.io/server-snippet: |
      access_by_lua_block {
          local header = ngx.req.get_headers()
          if header.token then
              local LIMIT = 100
              local DELAY = 10

              local red = require "resty.redis"
              local redis = red:new()
              redis:set_timeout(1000)
              local ok, err = redis:connect("redis.default.svc.cluster.local", 6379)
              if not ok then
                  ngx.status = 500
                  ngx.say("<h1>系统开小差了</h1>",err)
                  return
              end

              local res, err = redis:auth("123456")
              if not res then
                  ngx.status = 500
                  ngx.say("<h1>系统开小差了</h1>")
                  return
              end

              local now = ngx.now()
              local ok, err = redis:eval('local oldest = redis.call("lindex", ARGV[1], -1);if oldest then if redis.call("llen", ARGV[1]) >= tonumber(KEYS[1]) then if (ARGV[2] - oldest) < tonumber(KEYS[2]) then return nil end end end;redis.call("lpush", ARGV[1], ARGV[2]);redis.call("expire", ARGV[1], KEYS[1]);redis.call("ltrim", ARGV[1], 0, KEYS[1]); return 1', 2, LIMIT-1, DELAY, "limit:"..ngx.md5(header.token), now)

              if ok ~= 1 then
                  ngx.status = 519
                  ngx.say("<h1>系统繁忙</h1>")
                  redis:set_keepalive(10000, 100)
                  return
              end

              redis:set_keepalive(10000, 100)
          end
      }
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
   - http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: apple-service
            port:
              number: 5678

我们在注解的nginx.ingress.kubernetes.io/server-snippet: 段插入了lua代码:access_by_lua_block,通过openresty的redis 包来连接redis:

代码语言:javascript复制
local red = require "resty.redis"
local redis = red:new()
redis:set_timeout(1000)
local ok, err = redis:connect("redis.default.svc.cluster.local", 6379)

通过执行redis lua脚本来进行限流

代码语言:javascript复制
local now = ngx.now()
local ok, err = redis:eval('
  local oldest = redis.call("lindex", ARGV[1], -1);
    if oldest 
      then if redis.call("llen", ARGV[1]) >= tonumber(KEYS[1]) 
        then if (ARGV[2] - oldest) < tonumber(KEYS[2]) 
        then return nil 
      end
     end 
     end;
   redis.call("lpush", ARGV[1], ARGV[2]);
   redis.call("expire", ARGV[1], KEYS[1]);
   redis.call("ltrim", ARGV[1], 0, KEYS[1]); 
   return 1',
2, LIMIT-1, DELAY, "limit:"..ngx.md5(header.token), now)

通过滑动窗口算法,做用户token维度的限流。

首先我们测试下

代码语言:javascript复制
kubectl apply -f ingress.yaml 
curl -H "token:12344" 127.0.0.1/apple       
/apple

一个请求的情况下正常工作了,我们来压测下

代码语言:javascript复制
% ab -n 1000 -c 100 -H "token:12344" 127.0.0.1/apple   
Percentage of the requests served within a certain time (ms)
  50%     54
  66%     77
  75%     90
  80%     99
  90%    122
  95%    145
  98%    189
  99%    212
 100%    258 (longest request)

100个并发是正常工作的,我们换成200个呢

代码语言:javascript复制
� -n 1000 -c 200 -H "token:12344" 127.0.0.1/apple       
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
apr_socket_recv: Connection reset by peer (54)
Total of 1 requests completed

限流量,至此我们完成了access_by_lua_block redis 全局的rate limiter的功能。其中也遇到了各种问题,下一节给大家介绍分析思路。

0 人点赞