apisix 企业落地实践经验
apisix 可以独立于 k8s 运行,apisix 在 k8s 中运行,需要 apisix-ingress-controller 组件同步上游数据 (主要是 Pod IP)
apisix 的配置数据比较灵活,可以通过 API 修改,如果是在 k8s 中,则可以通过 CRD 配置,并且优先考虑 CRD,因为 API 配置的数据最终会被 CRD 覆盖,默认 1 小时进行一次 CRD 全量同步
这里就引申出大部分云原生网关的设计思想,数据面(apisix)和控制面(apisix-ingress-controller),数据面可以单独部署,独立于 k8s 之外,此时使用 API 配置,如果在 k8s 中,应该避免 API 操作,或者保证 API 操作的数据不和 CRD 的冲突,另外 CRD 的数据是持久化在 k8s etcd 中的,而 API 数据是持久化在 apisix 的 etcd 中,考虑数据维护性和恢复可用性,优先CRD(建议前期可以 API,配和 dashboard 快速上手,后期把 API 的往 CRD 迁移)
CRD 的优势:如果 apisix 各个组件,甚至 apisix 的 etcd 都挂了,也没关系,找可用的节点快速部署恢复,etcd 重新搭建,也可以马上恢复。减少维护 k8s etcd,apisix etcd 2 套 etcd 的压力
在大部分云原生网关组件中,svc 不再发挥作用了,pod 的请求负载均衡由网关直连 pod ip,这样可以实现多样化的负载均衡策略,灰度策略等,在 7 层扩展丰富的能力。svc 的 iptables 实现 LB 策略很有限,一般是 iptables 规则做概率转发,如果切换到 ipvs 可以丰富一点,不过本质都是 4 层转发,要实现复杂场景还是需要 apisix 这种云原生网关组件(或者走 ebpf,服务网格等方向,扩展 4 层转发的能力,从一些业界案例来看,建议还是 apisix 这种基于 nginx 容易落地一点)
Proxy Protocol
如何使用 proxy 协议,在配置文件中,有这部分
# proxy_protocol: # PROXY Protocol configuration
# listen_http_port: 9181 # APISIX listening port for HTTP traffic with PROXY protocol.
# listen_https_port: 9182 # APISIX listening port for HTTPS traffic with PROXY protocol.
# enable_tcp_pp: true # Enable the PROXY protocol when stream_proxy.tcp is set.
# enable_tcp_pp_to_upstream: true # Enable the PROXY protocol.
nginx 中配置,这个配置还是复杂的,具体要测试验证一下,一般 listen 80 proxy_protocol 配置
stream {
server {
listen 80 proxy_protocol;
...
proxy_pass backend:port;
}
}
注意:需要双方都启用 proxy 协议,否则容易出问题
提醒:apisix 的 proxy 协议,根据我看官方文档和 Github 的 issue 来看,proxy 协议的端口不能和原本的 80,443 共用一个,看到 issue 上还有很多人在讨论,一般大家使用这个协议,主要还是为了传递源 ip,但是具体落地上分开使用 2 个端口,我没遇到这样的场景,都是希望使用同一个端口,所以这块感觉还不能满足需求,或者有更好的落地方案,需要注意一下
获取真实ip
如果网络过了 NAT,通常就不能取到真实 ip 了,以下是 apisix 的一些部署场景
case:
- nginx -> apisix -> pod
- apisix -> pod
- vip -> apisix
重点在于,各层转发,是不是传递真实 ip,如果是 7 层,保证 X-Forwarded-For 设置正确 如果是 4 层,保证能用 proxy 协议传递 ip
nginx 配置补充说明:
-
使用 proxy_add_x_forwarded_for,把传递代理后的 ip 加到后面,这样 X-Forwarded-For 可以读到整个代理链路的 ip 列表 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-
设置这一层代理的真实 ip proxy_set_header X-Real-IP $proxy_add_x_forwarded_for;
k8s 部署架构说明
各种部署架构传递 ip 场景:
-
主机网络模式 这种模式直连到主机网络,没有问题
-
SLB 这种模式主要看厂商,要支持源 ip 传递
-
externalIPs 推荐没有 SLB 的首选模式,首先 hostnetwork 不利于 Pod 变更,externalIPs 很好解决这种问题
NodePort 要设置,如果是 ClusterIP,从 external ip 进去的流量可以负载均衡,转发到其它 pod,如果要保留源 ip,就得配置 externalTrafficPolicy: Local,这个配置必须要 NodePort 模式,此时相当于把发往 external ip 80 的请求直接转发给 pod(内核 iptables 实现),不需要 NAT,可以保留源 ip,但是失去 svc 负载均衡能力,多实例情况下,一般前面要挂一个 VIP,实现高可用架构
配置参考,nodePort 不发挥作用了,可以在网络防火墙策略上关闭,只开放 80,443:
spec:
ports:
- name: apisix-gateway
protocol: TCP
port: 80
targetPort: 80
nodePort: 30238
- name: tls
protocol: TCP
port: 443
targetPort: 443
nodePort: 32435
selector:
app.kubernetes.io/instance: apisix
app.kubernetes.io/name: apisix
type: NodePort
externalIPs:
- 10.90.xx.xx
sessionAffinity: None
externalTrafficPolicy: Local
扩展:4 层转发 PROXY 协议不是唯一解决方案,如果是一些硬件负载均衡,透明代理也可以做到保留源 IP,具体技术细节就比较广了
比如这个模块 TOA,内核安装后,可以从扩展的 tcp 头部中取到源 ip (具体原理就是 lb 把源 ip 写到扩展头部中,上游服务通过 TOA 模块,hack 的方式修改内核系统调用,实现从扩展的 tcp 头部中取源 ip 这种方式不改变源 ip,保证 lb 的下游能正确回包。它修改了内核的 getpeername 系统调用,而 nginx 这种软件取 ip 也是调用 getpeername getpeername 是一个系统调用,它被用于套接字编程中,以获取与某个套接字(socket)相关联的对端(peer)的地址信息)
https://github.com/Huawei/TCP_option_address
https://baijiahao.baidu.com/s?id=1752059279143587469&wfr=spider&for=pc
路由配置
- 路由匹配,通过proxy-rewrite插件,走正则,需求比较多的场景,适合一个host给多个服务使用,服务使用路由区别,转发给上游的时候,路由会去掉(正则有一定的性能损耗),要注意斜杆必须按照下面的方式配置
- X-Forwarded-Prefix 设置,apisix没有带这个头,有些服务需要,比如swagger
- 开启websocket,默认不开启
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: mytest
namespace: apisix
spec:
http:
- backends:
- serviceName: mytest
servicePort: 80
match:
hosts:
- test-server.abc.com
paths:
- /abc/test/v1.0/*
name: mytest
plugins:
- config:
headers:
set:
X-Forwarded-Prefix: /abc/test/v1.0/
regex_uri:
- /abc/test/v1.0/(.*)
- /$1
enable: true
name: proxy-rewrite
websocket: true
上游服务配置
- 上游服务超时时间配置
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
name: mytest
namespace: apisix
spec:
timeout:
connect: 100s
read: 300s
send: 300s
安装
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-subdir-external-provisioner-local nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--set nfs.server=10.90.32.382 \
--set nfs.path=/home/k8s
kubectl patch storageclass <your-class-name> -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage-retain
annotations:
storageclass.kubernetes.io/is-default-class: 'true'
provisioner: cluster.local/nfs-subdir-external-provisioner-local
parameters:
onDelete: retain
pathPattern: ${.PVC.namespace}/${.PVC.name}/${.PVC.annotations.nfs.io/storage-path}
reclaimPolicy: Retain
volumeBindingMode: Immediate
kubectl create ns ingress-apisix
helm install apisix apisix/apisix \
--set gateway.type=NodePort \
--set ingress-controller.enabled=true \
--set etcd.persistence.storageClass="managed-nfs-storage-retain" \
--set etcd.persistence.size="4Gi" \
--namespace ingress-apisix \
--set ingress-controller.config.apisix.serviceNamespace=ingress-apisix