最近项目在对部署在香港节点的网站服务做资源加载提速,本文主要记录了在kubernetes的原生nginx ingress中引入server cache的流程和方案,
里面涉及的都是最为常见的技术,并没有什么新的东西,算是一个总结和记录。
背景和前提
当前已上线的网站都是基于Nginx Ingress
+ Hugo/Vue Server
的模式部署的,发布采用 ArgoCD(GitOps)
+ Jenkins
,大致的流程如下:
准备
Nginx做Server端的缓存目前有2种方式比较常见:
- 基于外置的中间件做数据缓存,比如Nginx官方Redis Module给出的部署介绍,
这种方式的好处就是: 缓存集中,有利于命中和清理,同时不会增加nginx本身的业务复杂度,但他也有问题,就是会引入部署的成本。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
server {
location / {
set $redis_key $uri;
redis_pass name:6379;
default_type text/html;
error_page 404 = /fallback;
}
location = /fallback {
proxy_pass backend;
}
}
|
- 基于Nginx本身的
proxy_cache
实现,这个是官方很早就引入的功能,直接利用本地磁盘做缓存存储,考虑到网站本身的规模和系统复杂度,
我们选择了nginx自带的proxy_cache
加kubernetes中的memory emptyDir
做服务端缓存。
环境准备
Nginx本身已经包含了proxy_cache的模块,不过关于purge的功能却仅在商业版本中才包含,所以我们需要自己引入并制作镜像,下面是社区推荐的第三方
的purge模块:
https://github.com/FRiCKLE/ngx_cache_purge
很遗憾他并不支持批量的缓存清除,从README里面的配置指导可以看出,我们需要对缓存的资源进行逐个清除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
http {
proxy_cache_path /tmp/cache keys_zone=tmpcache:10m;
server {
location / {
proxy_pass http://127.0.0.1:8000;
proxy_cache tmpcache;
proxy_cache_key $uri$is_args$args;
}
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge tmpcache $1$is_args$args;
}
}
}
|
而且,这个仓库已经6年没人维护了,好在最近nginx社区自己fork并维护起来了地址,
最关键的是作者还加入了我们正想要的Partial Keys
功能 :) 。
Nginx ingress关于怎么制作自定义镜像的指导比较少,不过从Makefile里面能快速搜索到他们的Base Image
而且制作镜像的流程非常清晰,基于现有的流程做扩展和第三方Module引入还是比较方便的。
1
2
3
4
5
6
7
8
9
10
|
现有引入的Module:
nginx-http-auth-digest
ngx_http_substitutions_filter_module
nginx-opentracing
opentracing-cpp
zipkin-cpp-opentracing
dd-opentracing-cpp
ModSecurity-nginx (only supported in x86_64)
brotli
geoip2
|
完整的改动已经放到了我们组织的fork仓库,
需要注意的一个点就是nginx ingress官方为了支持多架构和性能,在构建镜像时引入了buildx组件, 他跟我们原生的docker build有一点区别是他并不会默认导出制作好的镜像,需要我们在命令中具体指明,如:
1
2
|
#具体文档,请移步: https://github.com/docker/buildx
docker buildx build --output=type=docker .
|
有了基础镜像,在制作controller的镜像时,只需要将BASE_IMAGE替换为我们生成好的镜像即可, 如:
1
|
BASE_IMAGE=opensourceways/ingress-nginx-with-purge:0.0.1 make image
|
有了镜像,可以在镜像中通过nginx -V
确认模块以加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
➜ ~ docker run -it --rm opensourceway/ingress-nginx-controller-with-purge-cache:v0.35.0.0 nginx -V
nginx version: nginx/1.19.2
built by gcc 9.3.0 (Alpine 9.3.0)
built with OpenSSL 1.1.1g 21 Apr 2020
TLS SNI support enabled
<content skipped>
--add-module=/tmp/build/ngx_devel_kit-0.3.1
--add-module=/tmp/build/set-misc-nginx-module-0.32
--add-module=/tmp/build/headers-more-nginx-module-0.33
--add-module=/tmp/build/nginx-http-auth-digest-cd8641886c873cf543255aeda20d23e4cd603d05
--add-module=/tmp/build/ngx_http_substitutions_filter_module-bc58cb11844bc42735bbaef7085ea86ace46d05b
--add-module=/tmp/build/lua-nginx-module-0.10.17 --add-module=/tmp/build/stream-lua-nginx-module-0.0.8
--add-module=/tmp/build/lua-upstream-nginx-module-0.07
--add-module=/tmp/build/nginx-influxdb-module-5b09391cb7b9a889687c0aa67964c06a2d933e8b
--add-dynamic-module=/tmp/build/nginx-opentracing-0.9.0/opentracing
--add-dynamic-module=/tmp/build/ModSecurity-nginx-b55a5778c539529ae1aa10ca49413771d52bb62e
--add-dynamic-module=/tmp/build/ngx_http_geoip2_module-3.3
--add-module=/tmp/build/nginx_ajp_module-bf6cd93f2098b59260de8d494f0f4b1f11a84627
--add-module=/tmp/build/ngx_brotli --add-module=/tmp/build/ngx_cache_purge-2.5.1
|
有了镜像,下一步就是配置了。
主要配置
proxy_cache_path
:
1
2
3
|
Syntax: proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [min_free=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time] [purger=on|off] [purger_files=number] [purger_sleep=time] [purger_threshold=time];
Default: —
Context: http
|
这个是cache的全局配置,其中几个比较重要的字段介绍如下:
path
: 缓存的配置路径
keys_zone:<name:size>
: 缓存的key区域,当开启某个domain的访问缓存时,使用proxy_cache
指定对应的key_zone, 注意缓存的key是存储在内存中的,所以需要指定存储的上限,根据官方文档,1mb基本可以存储8k个key。
use_temp_path
:是否启用temp目录用于上游返回内容的保存,nginx的cache在开启temp_path的时候会有2步,保存结果到temp目录,拷贝到cache目录,建议关闭。
inactive
: 缓存的保留时间retention
, 如果超过时间资源没有被访问,缓存将被清除。
max_size
: 缓存数据的最大占用空间,超出后空间将被清理(基于最近最少使用原则),
levels
+ proxy_cache_key
: 这2个字段配合起来,决定了我们缓存存储的路径,其中level决定了存储的目录层级以及目录名,cache_key决定了最终的文件名,举个例子,比如我的配置如下:
1
2
|
proxy_cache_key $uri$is_args$args;
proxy_cache_path /tmp/statics_cache levels=1:2 keys_zone=statics_cache:50m use_temp_path=off max_size=500m inactive=1h;
|
当我们访问http://somedomain.com/public/img/grafana_icon.svg
的时候,对proxy_cache_key
(/img/grafana_icon.svg)进行MD5计算,得:
1
2
|
➜ ~ md5 -s "/public/img/grafana_icon.svg"
MD5 ("/public/img/grafana_icon.svg") = e07fe02651e785ebf74c2c5c0abae094
|
那我们最终的存储路径就是:/tmp/statics_cache/4/09/e07fe02651e785ebf74c2c5c0abae094, 其中目录信息反向截图。
proxy_cache_lock
: cache锁,在多个请求访问同样资源,且开启cache的情况下,只有一个请求有权限去生成cache或访问cache。
proxy_cache_valid
: cache的有效时间,超期的缓存将从上游重新获取, 我们可以基于返回结果做差异化配置,比如proxy_cache_valid 404 1m;
设置404近保留1分钟。
proxy_ignore_headers
:忽略响应中的某些header,比如: proxy_ignore_headers Cache-Control
就能忽略上游的缓存策略,避免对我们的server cache造成影响。
proxy_cache_methods
: 缓存的http方法,默认会包含GET和HEAD。
proxy_cache_min_uses
: 设置资源访问多少次后再缓存,提高阈值能确保高频率的资源被缓存和使用。
基于上面的配置,我们就能在nginx-ingress中configmap中引入http-snippet, 修改deployment确保emptyDir挂载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
apiVersion: v1
data:
http-snippet: |
proxy_cache_path /tmp/statics_cache levels=1:2 keys_zone=statics_cache:50m use_temp_path=off max_size=500m inactive=1h;
kind: ConfigMap
metadata:
name: nginx-configuration
namespace: ingress-nginx
# other attribute in deployment are ignored.
volumes:
- name: nginx-proxy-cache
emptyDir:
medium: Memory
sizeLimit: "500Mi"
|
启用cache
当需要网站的server cache时,需要在具体的Ingress中做如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
nginx.ingress.kubernetes.io/proxy-buffering: "on"
nginx.ingress.kubernetes.io/server-snippet: |
proxy_cache statics_cache;
proxy_cache_lock on;
proxy_cache_key $server_name$uri$is_args$args;
proxy_ignore_headers Cache-Control;
proxy_ignore_headers Set-Cookie;
proxy_cache_valid 60m;
add_header X-Cache-Status $upstream_cache_status;
location ~ /purge(/.*) {
allow 127.0.0.1;
allow 172.20.0.0/16;
deny all;
proxy_cache_purge statics_cache $server_name$1$is_args$args;
}
|
说明如下:
proxy_buffering
: 这个是开启缓存的前提,nginx ingress默认是关闭的。
add_header
: 在返回的Header中添加cache status (MISS,HIT,EXPIRED),用于查看缓存命中情况,如果我们在location server等多处有定义add_header, 需要注意的一点是:
1
2
|
# from nginx document
There could be several add_header directives. These directives are inherited from the previous configuration level if and only if there are no add_header directives defined on the current level.
|
proxy_cache_key
: 在nginx ingress中我们一般会服务多个domain,所以这里的key需要引入server_name避免相互干扰。
开启Purge
首先并不是所有的情况下都需要purge,毕竟现在有很多解决方案可以确保版本更新后缓存自动失效. 比如在生成资源时对文件名做hash:
1
|
zh-cn_image_0183048952.d04e2e5f.png
|
或者是在请求中带上发布版本的版本号参数:
这些都会导致缓存的key失效,从而避免干扰,不过我们并不能保证所有的情况下都能满足,所以需要支持通过API调用清理失效的缓存,purge模块支持我们配置单独的API用于网站缓存资源清理(前缀匹配):
1
2
3
4
5
6
|
location ~ /purge(/.*) {
allow 127.0.0.1
allow 172.20.0.0/16;
deny all;
proxy_cache_purge statics_cache $server_name$1$is_args$args;
}
|
上面的配置段需要同样添加到server-snippet中,且仅支持内网访问,注意:proxy_cache_purge
的key需要跟proxy_cache_key
保持一致。
我们通过API测试,结果是some-domain
的缓存资源都会被清除:
1
|
$: curl -H "Host:some-domain" -k https://<nginx-endpoint>/purge/*
|
1
2
3
4
|
<html>
<head><title>Successful purge</title></head>
<body bgcolor="white"><center><h1>Successful purge</h1><p>Key : some-domain/*</p></center></body>
</html>
|
不过,我们还有一个问题: nginx ingress都是多实例的,清理单个实例还不够,当前原生的Kubernetes中并没有类似的概念可以支持我们一次性清理,只能考虑将purge的请求复制多份到endpoints分别处理, 大致的逻辑如下Stackoverflow:
1
2
3
|
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token);
curl https://kubernetes.default.svc/api/v1/namespaces/default/endpoints --silent \
--header "Authorization: Bearer $TOKEN" --insecure | jq -rM ".items[].subsets[].addresses[].ip" | xargs curl
|
我们搭建了一个Basic Auth的Http Server用于响应流水线中的清除缓存动作。
有了他我们就差最后一步,跟发布的流水线对接, 前文有提到我们的发布流程是基于Jenkins + ArgoCD
实现的,
考虑到ArgoCD本身就有Webhook机制,所以我们的应用的部署代码仓库需要新增如下资源:
触发Purge
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
|
apiVersion: batch/v1
kind: Job
metadata:
name: website-nginx-purge
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
ttlSecondsAfterFinished: 600
template:
spec:
containers:
- name: purge-trigger
image: curlimages/curl:7.72.0
env:
- name: USERNAME
valueFrom:
secretKeyRef:
key: username
name: purge-secrets
- name: PASSWORD
valueFrom:
secretKeyRef:
key: password
name: purge-secrets
command:
- curl
- -u
- $(USERNAME):$(PASSWORD)
- --fail
- https://<api-endpoints>/nginx-purger/purge?hostname=<hostname-to-purge>
restartPolicy: Never
backoffLimit: 2
|
这样一旦我们更新网站,ArgoCD的Sync行为结束,nginx ingress中的cache就会被自动清除:
1
2
3
|
nginx cache purged for host: some-domain on ingress instance 172.20.0.103
nginx cache purged for host: some-domain on ingress instance 172.20.0.144
nginx cache purged for host: some-domain on ingress instance 172.20.0.56
|
需要注意的是:
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
: 能确保每次synced后Job资源删除后执行。
- argo的application不能设置为自动Sync,这样会导致缓存被频繁清除。
全文完。