背景
最近我们自研的云原生发布平台新支持了一种发布场景,简单来说client里面会把k8s里面的各资源文件比如:
deploy.yaml
service.yaml
ingress.yaml
pvc.yaml
分别用字符串变量保存,然后通过jenkins开源库sdk传给一个带参数的jenkins pipeline job,来触发该job的运行,但是client在调用jenkins job的build方法(实际post的总数据是大于8KB)的时候程序了报了如下异常,
Exception:Request Header Fields Too Large
排查
由于我们的jenkins和client发布平台程序都是运行在K8s里面,client里面的用的是jenkins开源库通过Ingress域名访问的jenkins master,所以开始怀疑是Kubernetes Ingress Nginx的参数设置的过小导致,跟Header有关的nginx参数如下:
# body (和本次异常无关,顺带记录一下) client_body_buffer_size 10K; client_max_body_size 8m; # header client_header_buffer_size 2m; large_client_header_buffers 4 8k;
含义如下:
client_body_buffer_size:
Nginx分配给请求数据的Buffer大小,如果请求的数据小于client_body_buffer_size直接将数据先在内存中存储。如果请求的值大于client_body_buffer_size小于client_max_body_size,就会将数据先存储到临时文件中,使用client_body_temp 指定的路径中,默认该路径值是/tmp/.
所以配置的client_body_temp地址,一定让执行的Nginx的用户组有读写权限。否则,当传输的数据大于client_body_buffer_size,写进临时文件失败会报错。
client_max_body_size:
默认 1M,表示 客户端请求服务器最大允许大小,在“Content-Length”请求头中指定。如果请求的正文数据大于client_max_body_size,HTTP协议会报错 413 Request Entity Too Large。就是说如果请求的正文大于client_max_body_size,一定是失败的。如果需要上传大文件,一定要修改该值
client_header_buffer_size:
假设client_header_buffer_size的配置为1k,如果(请求行+请求头)的大小如果没超过1k,放行请求。如果(请求行+请求头)的大小如果超过1k,则以large_client_header_buffers配置为准
large_client_header_buffers:
请求行+请求头的总大小不能超过32k(4 * 8k)
在我们调整k8s ingress nginx的相关配置后
# header client_header_buffer_size 2m; large_client_header_buffers 4 2m;
注意上述配置,可以不用修改k8s ingress controller 的全局配置,可以在应用级别配置,需要用k8s ingress controller的Snippets配置,具体参考:Advanced Configuration with Snippets | NGINX Ingress Controller
使用上述配置后,报错依然没有消失,为了排除nginx的干扰,我们直接通过访问service来继续测试:
curl --user user:pwd http://jenkins-svc.demo:8080/jenkins/job/my-test/config.xml
发现依然报错,然后我们在去看jenkins k8s master的日志,发现了端倪:
WARNING eclipse.jetty.http.HttpParser#parseFields: Header is too large 8193>8192
到这里可以确认是jetty容器的设置,导致接受到的请求被解析失败了
修复
我们的jenkins是运行在k8s里面,而jetty又是jenkis内嵌的服务器,还没有办法直接调参数,传统的jetty用法,我们是把应用丢进jetty的webapps目录和tomcat类似,所以是可以直接去调ini文件参数的,当如果应用是引用jetty-core.jar,把web容器做成了内嵌服务,那么很多参数都是硬编码的,所以调起来很不方便。
经过查询,发现jenkins有个JENKINS_OPTS参数,可以调整,在k8s里面这个参数的内容是挂在secret里面,所以,我们直接在secret里面新增下面的内容即可(单位Byte),具体大小可以根据业务调整:
"--requestHeaderSize=258140" (252KB)
问题本质
上面解决方案确实有用,但却不是这个问题的本质原因,经过查看我们用的jenkis开源库的源码:
com.offbytwo.jenkins jenkins-client 0.3.8
发现其代码里,将post请求转为url拼接:
public QueueReference build(Mapparams, Map fileParams, boolean crumbFlag) throws IOException { String qs = join(Collections2.transform(params.entrySet(), new MapEntryToQueryStringPair()), "&"); ExtractHeader location = client.post(url + "buildWithParameters?" + qs,null, ExtractHeader.class, fileParams, crumbFlag); return new QueueReference(location.getLocation()); }
如果是在body的data里,就不会有问题,因为body是没限制的,而url的长度也就是header长度是有限制的,这也是为什么GET请求和POST的请求的一个最大区别,这个确实是坑了,因为源码是这样,所以为了快速修复,我们采用加大了jetty容器的header检查限制,这样可快速修复问题,也不用改动源码再次发布,那样影响会比较大。
最后推荐大家,如果使用开源库操作jenkins,可以使用官网推荐的jenkins-rest:
com.cdancy jenkins-rest 0.0.27
总结
云原生时代,很多应用跑在Kubernetes里面很方便,但相应的请求访问链路也变多了,这样就会导致排查问题起来相对比较困难,因为不仅仅涉及应用程序,还会和应用请求经过的中间层nginx,业务网关,以及k8s本身网络等依赖的组件都可能有关联,排查问题时,可以用排除法,逐步缩小问题范围,这样排查起来就高效多了。