自从公司所有服务迁移到k8s之后,快站的c端页面打开就很少能够使用到页面缓存。打开https://yongyong.kuaizhan.com ,谷歌浏览器里打开检查, 就会发现,调用如下:
红色标注的地方x-cache-status: MISS证明没有击中缓存,还是直接调用的后端服务来获取页面信息。如果x-cache-status: HIT则代表击中缓存。 这时页面的响应速度会有比较大的提升。为了让读者能够更全面的了解整个调用链路,这里我来展示一下我们公司的服务部署结构图
这个图里面,我简单说一下C端页面的一个访问流程,访问c端链接, 经过外网负载均衡kzxx(这个项目是nginx+openresty主要承担快站的外网和内网的路由分发、频率限制等功能) 访问到gateway(这是用go写的一个网关服务,主要的作用是鉴权、域名识别、转发服务等功能)。gateway通过k8s service dns name 比如 html-render.kuaizhan-xx.svc.cluster.local:80把当前的请求转发到c端的服务html-render,服务里面部署的是nginx + PHP的组合。 html-render服务使用的是nginx内置的缓存,具体配置如下:
如果100s之内访问过某个容器,就会在当前容器里面保存有当前请求域名+路径的缓存值,所以下次如果还能请求到这个容器,就可以利用上nginx缓存。 因为快站c端的qps比较高,所以针对html-render部署的pod容器个数就会很多,这样同一个请求其实会被打散到不同的容器里,所以根本不能很好的利用上 缓存。
我先来讲讲一致性hash是怎么一回事吧,毕竟这篇文章和一致性hash有很大的关系。一致性Hash算法使用取模的方法,取模法针对nginx upstream的数量 进行取模,而一致性Hash算法是对2^32取模,什么意思呢?简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环, 如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下:
整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1, 以此类推,2、3、4、5、6……直到2^32-1, 也就是说0点左侧的第一个点代表2^32-1, 0和232-1在零点中方向重合,我们把这个由232个点组成的圆环称为Hash环。
服务器这时可以使用Hash进行一个哈希, 具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,如下图所示:
使用如下算法定位数据访问到相应服务器: 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器! 如下图所示
如果某台机器宕机。受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。 相应地新增一台机器。受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。
这么一看一致性hash的好处就可见一斑。但是我们其实还是需要关注热点问题。服务节点太少时,容易因为节点分部不均匀而造成数据倾斜。严重的话, 可能会存在连锁反应造成雪崩。
为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制, 即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。 如下图所示
这样我们就可以比较完美的解决热点问题。对于nginx来说需要在upstream里指定weight字段,作为server权重,对应虚拟节点数目。
在讲解今天的主角ingress-nginx-controller之前,我很想和大家分享一下k8s的控制器。我们要懂得控制器的一个大概运作原理,才能更好 地理解ingress-nginx-controller. (这段内容参考极客时间张磊大神分享的自定义控制器,感兴趣的可以去看看). 话不多说,先来个图:
控制器要做的第一件事,是从 Kubernetes 的 APIServer 里获取它所关心的对象,比如Ingress。 APIServer 是master节点下,负责API服务的。以及集群的持久化数据,也都是由APIServer处理之后保存到etcd中。
其余两个组件 kube-scheduler: 负责容器调度, kube-controller-manager: 负责容器编排。 继续说控制器. 获取对象的操作,依靠的是一个叫作 Informer(通知器)的代码库完成的。
Informer的第一个职责是同步本地缓存。 Informer 与 API 对象是一一对应的,所以传递给自定义控制器的,正是一个 Ingress 对象的 Informer(Ingress Informer). Ingress Informer 使用 ingressClient,跟 APIServer 建立了连接。不过,真正负责维护这个连接的,则是 Informer 所使用的 Reflector 包。 Reflector 使用的是一种叫作 ListAndWatch 的方法,来“获取”并“监听”这些 Ingress 对象实例的变化。 在 ListAndWatch 机制下,一旦 APIServer 端有新的 Ingress 实例被创建、删除或者更新, Reflector 都会收到“事件通知”。这时,该事件及它对应的 API 对象这个组合,就被称为增量(Delta), 它会被放进一个 Delta FIFO Queue(增量先进先出队列)中。而另一方面,Informer 会不断地从这个 Delta FIFO Queue 里读取(Pop)增量。 每拿到一个增量,Informer 就会判断这个增量里的事件类型,然后创建或者更新本地对象的缓存。这个缓存,在 Kubernetes 里一般被叫作 Store。
Informer的第二个职责,则是根据这些事件的类型,触发事先注册好的 ResourceEventHandler。 这些 Handler,需要在创建控制器的时候注册给它对应的 Informer。所以操作API对象的代码逻辑是在handler里实现的。 而具体的处理操作,都是将该事件对应的 API 对象加入到工作队列中。
所谓 Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。 它是自定义控制器跟 APIServer 进行数据同步的重要组件。更具体地说,Informer 通过一种叫作 ListAndWatch 的方法, 把 APIServer 中的 API 对象缓存在了本地,并负责更新和维护这个缓存。
接下来控制循环(Control Loop),则会不断地从这个工作队列里拿到这些 Key,然后开始执行真正的控制逻辑。 控制循环会等待 Informer 完成一次本地缓存的数据同步操作;并且直接通过goroutine 启动一个(或者并发启动多个)“无限循环”的任务。 真正处理API对象的逻辑则在循环当中完成。
所以控制器其实是根据informer的reflector来监听并更新API对象,并通过EventHandler处理好对象放入worker queue中,最终由 (control loop)从工作队列中取到key,执行真正的逻辑操作。
这时候我想问大家一个问题,为什么 Informer 和自定义控制器的控制循环之间,一定要使用一个工作队列来进行 ? 我觉得主要的原因在于 Informer 和 控制循环分开是为了解耦,防止控制循环执行过慢把Informer拖死。 这样的设计也是为了匹配双方速度不一致,有一个中间的队列来做协调.
千呼万唤始出来,我们的主角终于闪亮登场了。 官方网址https://kubernetes.github.io/ingress-nginx/ 首先来说一下nginx控制器是如何工作的
Ingress控制器的目标是汇编配置文件(nginx.conf)。主要目的是在配置文件中进行任何更改后需要重新加载NGINX. 需要特别注意的是,我们不会在仅影响上游配置的更改中重新加载Nginx(即,在部署应用程序时端点EndPoints更改)。 因为使用了lua-nginx-module实现这一目标
在某些情况下,可以避免重新加载,尤其是在端点发生更改时.例如: pod 重启或被替换.
在每个端点更改上,控制器从其看到的所有服务中获取端点并生成相应的Backend对象。然后,将这些对象发送到在Nginx内部运行的Lua处理程序。 Lua代码又将这些后端存储在共享内存区域中。然后,对于在balancer_by_lua上下文中运行的每个请求, Lua代码将检测到应该从哪个端点选择上游对等方,并应用配置的负载均衡算法来选择对等方。 在具有频繁部署应用程序的相对较大的集群中,此功能节省了大量Nginx重载,否则可能会影响响应延迟.
说了这么多. 总结一下,nginx控制器随着ingress的更改,而需要重新reload。但是为了避免这种reload操作,我们可以使用ConfigMap更改配置。或者 当endpoints修改的时候,控制器会根据lua-nginx-module来自动更新endpoints。
首先来看一下deployment.yaml文件:
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: nginx-ingress-clusterrole
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: nginx-ingress-role
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
# Defaults to "-"
# Here: "-"
# This has to be adapted if you change either parameter
# when launching the nginx-ingress-controller.
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: nginx-ingress-role-nisa-binding
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nginx-ingress-role
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: nginx-ingress-clusterrole-nisa-binding
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nginx-ingress-clusterrole
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
spec:
serviceAccountName: nginx-ingress-serviceaccount
containers:
- name: nginx-ingress-controller
image: registry.cn-hangzhou.aliyuncs.com/google_containers/nginx-ingress-controller:0.25.0
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
- --annotations-prefix=nginx.ingress.kubernetes.io
securityContext:
allowPrivilegeEscalation: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
# www-data -> 33
runAsUser: 33
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
---
看了这么多配置,是不是心里一慌。别怕,我来简单解释一下这个yaml文件要做什么:
分配这个权限的目的是为了使nginx-ingress-controller能够充当跨集群的入口。 这些权限被授予名为nginx-ingress-clusterrole的ClusterRole
上面的文件配置完,需要配置service yaml
kind: Service
apiVersion: v1
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: tcp-80-80-cluster
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
sessionAffinity: None
公司的k8s集群是在腾讯云搭建的版本是v1.13,所以执行以下命令来创建ingress-nginx-controller
kubectl create namespace ingress-nginx
kubectl apply -f ingress-nginx-deployment.yaml
kubectl apply -f ingress-nginx-service.yaml
执行上述三个命令,就在k8s集群里新建了一个ingress 控制器 因为默认一个ingress,只能指定一个控制器来控制它的一个声明周期,所以我们看看ingress的配置
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kuaizhan-wap
namespace: kuaizhan-cl
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/upstream-hash-by: "$host$request_uri"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host:
http:
paths:
- path: /
backend:
serviceName: html-render-pre
servicePort: 80
kubectl apply -f html-render-ingress.yaml
执行这个命令的同时,ingress nginx控制器会自动更新当前ingress的配置,
kubectl exec -n ingress-nginx nginx-ingress-controller-5cd5655c57-fjq7f cat /etc/nginx/nginx.conf
接下来要模拟多个请求访问服务,看看配置的一致性hash是否生效,这里使用http_load工具,因为它支持多个不同url的请求来并发访问服务
http_load -rate 100 -seconds 10 url.log
-rate 简写-p :含义是每秒的访问频率
-seconds简写-s :含义是总计的访问时间
url.log里面写了很多个快站c端的url
不得不看看源码如何处理的一致性hash。首先我观测到nginx.conf文件中关于upstream的代码
发现它是通过lua动态获取的endpoints数据,调用了balance.balance()的代码,于是我在github上下载了ingress-nginx的项目,找到了 balance.lua文件,这里执行的获取balance的操作
发现问题了没有,原来一致性hash只支持传入单个变量,如果我传入的是 h o s t host hostrequest_uri,最后会给我变成host$request_uri,这个变量,通过ngx.var 获取不到任何值!!
找到了问题关键,本来想在github上针对ingress-nginx项目提一个issue,但是一搜索发现已经有人提出了这个问题,并且有一个merge request正是 关于这块逻辑的优化,支持多个参数的一致性hash,不过最终没有通过代码提审。感兴趣的伙伴可以看看这个issue
最终把 h o s t host hostrequest_uri改成$host就可以均匀分配到每个节点。
但其实这样的配置还是有问题的,就是当服务的节点数增多,其实就会和我上文讲到的一致性hash原理那样,同样存在热点问题。 这时候想到的方式就是控制器有没有类似weight的参数配置,但是翻阅整个文档,都没有发现有这个参数的配置,只好查看源码。发现
代码里默认配置了weight为1。这样只能后端服务部署多个节点。 来解决没有虚拟节点带来的热点问题。
但是别急,nginx控制器还是想到了这一点 官方文档描述"subset" hashing can be enabled setting nginx.ingress.kubernetes.io/upstream-hash-by-subset: "true". This maps requests to subset of nodes instead of a single one. upstream-hash-by-subset-size determines the size of each subset (default 3).
大致意思是说,使用子集的方式。这会将请求映射到节点的子集,而不是单个请求。子集大小的上游哈希值确定每个子集的大小(默认为3)。这样其实就可以 在一致性hash和热点之间做出一个比较好的平衡.
如果只是单纯的想用负载均衡的功能,可以指定nginx.ingress.kubernetes.io/service-upstream为true。 因为默认情况下,NGINX入口控制器使用NGINX上游配置中所有端点(Pod IP /端口)的列表。 对于零停机时间部署之类的事情,这可能是理想的,因为它减少了Pod上下时重新加载NGINX配置的需求。避免了pod容器个数发生更新,而更新lua的动态内存。
讲了这么多,想必大家对ingress-nginx-controller有了一个大概的认识,但是我还是大家如果要使用nginx控制器的话,需要多看看文档, 选择适合你们自己公司的一个方案。我们要勇于拥抱新的技术。
欢迎大家关注我公众号,有问题可以私我
原文链接:https://xhb3909.github.io/2019/12/16/ingress-nginx-controller/#avoiding-reloads