背景
项目经常会出现多个迭代并行开发测试的场景,因此需要后台的存储资源共享但后台服务并存多个版本多个测试环境,以方便进行多迭代版本的并行开发测试。
要求:系统要在测试域名不变的前提下,在页面提供一个浮层去切换不同的测试环境,方便测试同学进行不同需求的并行测试。
后台方案
后台主要需要解决的问题包含如下几个方面:多版本服务的并行运行、请求如何转发、配置文件处理以及定时任务抢占问题的解决等。
下面针对这些问题,文档一一进行解答。
1, 多个版本服务的并行在测试环境并行运行
这里我们通过不同的k8s服务名做到不同版本服务的并行运行,多个服务属于同一个namespace。通过流水线新增一个服务名的选择框,并修改k8s配置,实现多个后台服务能够并行运行。
2, 相同域名下以及路径下如何将请求转发到不同的后台服务
经过和前端同学的讨论,我们决定使用对请求的header中特定的字段值来确定请求如何被转发的方案。
那在ingress中,如何实现服务名到服务地址的解析呢?
在ingress中,服务的DNS名称是根据服务的名称和命名空间自动生成的,服务的DNS名称的格式是<service-name>.<namespace>.svc.cluster.local,其中<service-name>是服务的名称,<namespace>是服务所在的命名空间。而ingress会自动将此dns名称解析为服务的地址。这里要注意ingress转发时使用的是默认的80端口,还要手动在dns名称后添加端口号完成完整的服务地址。
那解决了服务名到服务地址的映射关系之后。那如何通过header的ch-env参数值决定服务的转发呢?
尽管k8s的ingress对nginx服务的定制化转发规则支持的并不太好,但它的server-snippet
可以支持配置自定义的转发规则,这些规则会填充到nginx配置文件的server块中。通过if和proxy_pass完成服务的转发。
但测试时,我们发现nginx中if规则非常特殊。它和编程语言直觉中的if规则很不一样。可能会导致意想不到的后果。下面我就举几个例子:
location ~ /(api|t)/.+ {
set $target_service "backend";
if ($http_ch_env = "dev-a") {
set $target_service "backend-test";
proxy_pass http://$target_service.xxhub-dev.svc.cluster.local:8000;
}
}
请求header带有Ch-Env: dev-a的请求会正确转发到backend-test
服务去。
location ~ /(api|t)/.+ {
set $target_service "backend";
if ($http_ch_env = "dev-a") {
set $target_service "backend-test";
proxy_pass http://$target_service.xxhub-dev.svc.cluster.local:8000;
}
if ($http_ch_env_extra = "extra") {
}
}
请求header带有Ch-Env: dev-a和Ch-Env-Extra: extra的直接500, 没有被转发到任何服务。
location ~ /(api|t)/.+ {
set $target_service "backend";
if ($http_ch_env = "dev-a") {
set $target_service "backend-test";
proxy_pass http://$target_service.xxhub-dev.svc.cluster.local:8000;
}
if ($http_ch_env_extra = "extra") {
}
proxy_pass http://$target_service.xxhub-dev.svc.cluster.local:8000;
}
请求header带有Ch-Env: dev-a和Ch-Env-Extra: extra的直接又被转发到了正确的服务了。
这里我经过实践之后,使用如下的配置就可以正常工作了。
location ~ /(api|t)/.+ {
set $target_service "backend";
if ($http_ch_env = "dev-a") {
set $target_service "backend-a";
add_header Ch-Env dev-a always;
}
if ($http_ch_env = "dev-b") {
set $target_service "backend-b";
add_header Ch-Env dev-b always;
}
add_header Ch-Env dev always;
proxy_pass http://$target_service.xxhub-dev.svc.cluster.local:8000;
}
但这样做的话,并没有解决if的问题,当if的条件后续更加复杂时,很有可能会出现意想不到的问题,详情可以参考如下网页:
If is Evil… when used in location context | NGINX
How nginx "location if" works | Human & Machine (agentzh.blogspot.com) 哪有什么办法可以代替if嘛?
还是有的,替代方案为将参数和环境配置在map中,直接使用nginx的map完成环境到服务名的映射,这样就可以避免使用if了。
map $http_ch_env $target_dev_service {
default backend;
dev-a backend-test;
}
map $http_ch_env $target_dev_header {
default dev;
dev-a dev-a;
}
由于nginx ingress中无法直接操作http块的内容,所以需要将此map写道ingress nginx的configmap中,之后完成引用。
location ~ /(api|t)/.+ {
add_header Ch-Env $target_dev_header always;
proxy_pass http://$target_dev_service.xxhub-dev.svc.cluster.local:8000;
}
这个方法也是折腾了很久,问了腾讯云,他给出的说法是因为我们是自建的nginx ingress,比较特殊所以无法设置。其他的nginxIngress 遇到这个annotations 不属于自己配置的,会跳过,你们自建的它不跳过,会校验 。
2,消除了if的不确定性影响后,是不是这种方法就没有任何问题了呢?并不是。
和在spec.rule中设置转发规则不同,在server-snippet中设置的location转发条件并不会继承在annotations中设置的指令规则。因此假如在annotations中设置了全局的proxy-body-size,proxy-read-timeout以及rewrite等规则。还需要在server-snippet的location再设置一次。也就是说以后每次在ingress加全局配置的时候,还要在ingress中再加一次,还挺麻烦。。。
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 100m
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/rewrite-target: /api/$2
nginx.ingress.kubernetes.io/server-snippet: |
location ~ /xxhub/api(/|$)(.*) {
if ($http_ch_env = "dev-a") {
add_header Ch-Env dev-a always;
proxy_pass http://backend-test.xxhub.svc.cluster.local:8000;
}
add_header Ch-Env test always;
client_max_body_size 100M;
proxy_read_timeout 150;
proxy_send_timeout 150;
rewrite "(?i)/xxhub/api(/|$)(.*)" /api/$2 break;
proxy_pass http://backend.xxhub.svc.cluster.local:8000;
}
nginx.ingress.kubernetes.io/use-regex: "true"
那还有没有其他的方法呢?
大概调研了下,可以使用一个更高级的Ingress Controller,例如Traefik或者Istio。这些Ingress Controller支持更复杂的路由规则,包括基于请求的属性来动态路由到不同的服务。
3,定时任务的抢占处理
虽然需要搭建多测试环境,但大部分时间也只有一套环境经常用于测试。所以我们多测试环境使用了同一套配置文件,这样不仅扩展环境更加方便,而且当并行开发需要修改配置文件时,不需要额外去拉平配置文件版本。
但这样做的话,假如配置文件的定时任务是打开的,就会造成定时任务被多环境抢占的问题。如果来处理这个问题呢?
不同环境共用一套配置文件,流水线在部署时,脚本自动更改某一环境的定时任务开关。
# IsEnableCron: false # 是否启用定时任务
# 根据server name来确定是否替换定时任务开关
if [ "${serverName}" != "backend" ]; then
echo -e "replace IsEnableCron"
sed -i 's/IsEnableCron: *true/IsEnableCron: false/g' \
${WORKSPACE}/${appResourceName}/configmap/${grayRegionEnvList}/config/files/*.yaml
fi
优点就是用户部署前不需要了解其他流水线的设置情况。但缺点是只有总开关,粒度太大,不够灵活,只能某个环境开启定时任务。
后续如果需要更细粒度的控制定时任务的开关,可以考虑环境加版本号的控制方法来处理定时任务抢占问题。