背景

最近我们自研的云原生发布平台新支持了一种发布场景,简单来说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开源库的源码:

        <dependency>
            <groupId>com.offbytwo.jenkins</groupId>
            <artifactId>jenkins-client</artifactId>
            <version>0.3.8</version>
        </dependency>

发现其代码里,将post请求转为url拼接:

    public QueueReference build(Map<String, String> params, Map<String, File> 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:

        <dependency>
            <groupId>com.cdancy</groupId>
            <artifactId>jenkins-rest</artifactId>
            <version>0.0.27</version>
        </dependency>

总结

云原生时代,很多应用跑在Kubernetes里面很方便,但相应的请求访问链路也变多了,这样就会导致排查问题起来相对比较困难,因为不仅仅涉及应用程序,还会和应用请求经过的中间层nginx,业务网关,以及k8s本身网络等依赖的组件都可能有关联,排查问题时,可以用排除法,逐步缩小问题范围,这样排查起来就高效多了。

Logo

开源、云原生的融合云平台

更多推荐