一次Kubernetes Pod内存异常导致的测试环境耗时异常问题排查过程

概述

在使用公司内部后台系统测试环境时发现一个请求加载慢的问题,简简单单的列表,查询MongoDB数据库,测试环境不过几百上千条数据而已,请求耗时居然高达5~6秒:
在这里插入图片描述
作为对比,生产环境的请求响应截图如下:
在这里插入图片描述
经过持续跟进,该后台系统所有列表页面测试环境普遍比生产环境慢,不管是MongoDB还是MySQL数据库。

既然不是一个页面,也就是说查询的数据库类型不止一种,查询的DB和表不止一个,可排除因为测试环境和生产环境数据库表的索引不一致导致的。

是的,来到这家公司,发现之前根本就没有一个完善、规范、可审计、可追踪的数据库表变更上线审批工单系统;不管是开源的还是自研的,都没有。入职3个月来,收拾各种烂摊子,搭建并维护一个简陋版的开源SQL审计上线平台Archery。但是不能保证同一张DB数据表,测试和生产环境的表定义Schema相同。

另外,不管是测试还是生产环境,应用发布都是基于Git Tag。使用GitLab的compare功能,不难得知代码是同一套。于是把问题的症结抛给运维。但是没有得到很好的答复。

事实上,同后端架构技术交接一样,运维交接也是零,没有任何Wiki记录文档,没有任何交接文档,自己摸索去吧。基础设施,包括Kubernetes、网络、ELK、Nginx配置、网络转发,也是各种乱七八糟。

排查

测试环境请求慢

上面两个请求耗时异常慢的接口,都是在backend服务,都是从gateway-b网关服务转发到具体的业务承载服务。

gateway有如下两个Pod:
在这里插入图片描述
请求转发时,随机选择一个Pod节点,默认情况下ELK查看的是所有Pod里搜集到的应用日志。如果只想查看某个Pod的日志,要么在ELK日志查询平台指定IP:
在这里插入图片描述

要么使用Rancher的日志查看功能:
在这里插入图片描述

另一个Pod:
在这里插入图片描述
上面的日志截图不完全,一个比较完全的网关转发层日志记录截图如下:在这里插入图片描述
gateway只是一个网关转发层,接口耗时还是得去看一下具体的接收请求的服务,如backend服务,找到如下日志:
在这里插入图片描述
截图里的日期时间以及TraceId不是重点。可看到backend服务使用ControllerLogAop记录requestBody和responseBody日志,某一次真实请求耗时仅12ms。算上请求跨微服务转发,也不可能长达几秒。所以问题应该在网关层应用上。

另外,关于日志记录多扯一句,由于所有应用都是经过gateway网关服务转发,完全可以在gateway服务里记录接口请求的requestBody和responseBody。除了在gateway里记录请求日志。在真正承载业务请求的若干个服务里也冗余Ctrl + C/V若干个ControllerLogAop类。也就是说,两层日志记录。

PS:这个测试环境请求慢的问题,优先级很低,重启可以解决,有空就去排查,前前后后1个多月搜集到若干个截图,还没定位到问题根源,也没有彻底解决。

可以看到日志打印类是PermissionFilter,看下源码(有删减):

@Slf4j
@Component
public class PermissionFilter implements GlobalFilter, Ordered {
    private static final String BLACK_TOKEN = "BLACK_TOKEN:";
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.header}")
    private String tokenHeader;
    @Value("${gwb.referer}")
    private String imsHost;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        final int NO_OPERATION_PERMISSION_CODE = 9641;
        final int AUTH_FAILED = 9642;
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String requestPath = request.getURI().getPath();
        log.info(requestPath);
        long s1 = System.currentTimeMillis();
        long s3 = 0;
        HttpHeaders headers = request.getHeaders();
        String username = headers.getFirst("username");
        if (!requestPath.contains("/auth/login/ldap")) {
            Assert.notNull(username, "header中的username不能为空");
            final String requestHeader = headers.getFirst(this.tokenHeader);
            Boolean invalid;
            String blackToken = null;
            if (StringUtils.isEmpty(requestHeader)) {
                log.error("token为空!");
                invalid = true;
            } else {
                try {
                    long s2 = System.currentTimeMillis();
                    log.info("header time consuming:{}ms", s2 - s1);
                    String authToken = requestHeader.substring(7);
                    blackToken = (String) redisTemplate.opsForValue().get(BLACK_TOKEN + authToken);
                    invalid = jwtTokenUtil.isTokenExpired(authToken);
                    String tokenName = jwtTokenUtil.getUsernameFromToken(authToken);
                    s3 = System.currentTimeMillis();
                    log.info("redis and token time consuming:{}ms", s3 - s2);
                    if (!username.equals(tokenName)) {
                        Response<Void> response = Response.error(AUTH_FAILED, "token非法!");
                        log.info("token中用户与username不一致!");
                        DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                        return response.writeWith(Mono.just(bodyDataBuffer));
                    }
                } catch (Exception e) {
                    log.error("jwt校验发生异常!", e);
                    invalid = true;
                }
            }
            if (invalid || !ObjectUtils.isEmpty(blackToken)) {
                Response<Void> response = Response.error(AUTH_FAILED, "token已失效!");
                log.info("token失效!");
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
            String postData = (String) redisTemplate.opsForValue().get(username);
            HashSet<String> roles;
            if (StringUtils.isBlank(postData)) {
                roles = Sets.newHashSet();
            } else {
                roles = (HashSet<String>) JSON.parseObject(postData).get("roles");
            }
            long s4 = System.currentTimeMillis();
            log.info("redis time consuming:{}ms", s4 - s3);
            // 初始值,默认为false,表示无权限
            AtomicBoolean isPermission = new AtomicBoolean(false);
            if (roles.contains(requestPath)) {
                log.info("path={}", requestPath);
                isPermission.set(true);
            } else {
                roles.forEach(role -> {
                    if (requestPath.contains(role)) {
                        log.info("role={}", role);
                        log.info("path={}", requestPath);
                        isPermission.set(true);
                    }
                });
            }
            // 停止转发没有用户登录的请求
            if (!isPermission.get()) {
                Response<Void> response = Response.error(NO_OPERATION_PERMISSION_CODE, "权限不足,请检查配置!");
                log.info("用户没有操作权限");
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
            long s5 = System.currentTimeMillis();
            log.info("other time consuming:{}ms", s5 - s4);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE;
    }
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

测试环境XXL-Job任务调度异常

上面的问题并没有定位到根源。于此同时,微服务若干个定时任务采用XXL-Job调度平台,基于Spring Cloud Gateway来实现请求转发,参考Spring@Scheduled定时任务接入XXL-JOB的一种方案-基于SC Gateway。

测试环境定时调度任务收到如下执行异常告警邮件:
在这里插入图片描述
进入测试环境的XXL-Job管理平台,查看调度日志:
在这里插入图片描述
可知问题是偶发,具体的错误日志:

[com.aaaaa.gateway.config.SampleXxlJob#httpJobHandler]-[99]-[Thread-72] java.net.ConnectException: Connection refused (Connection refused)
    at java.base/java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.base/java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:399)
    at java.base/java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:242)
    at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:224)
    at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:403)
    at java.base/java.net.Socket.connect(Socket.java:591)

熟悉的连接被拒绝:java.net.ConnectException: Connection refused

进一步分析应用层日志,8点5分和5点5分两次的定时任务执行成功:
在这里插入图片描述
打印xxlJob调度执行返回数据=这一行日志,也就是有回调动作的,才算是任务执行成功。

实际上,任务调度已经随机下发成功,即选择一个Kubernetes Pod成功,只是没有收到执行成功的回调。

穷途末路

上面两个问题都定位不到根源,穷途末路。

本地Debug模式启动gateway网关应用,借助于IDEA插件Profiler,也没分析出个啥。

本地Debug模式启动包括gateway网关应用在内的多个服务,通过gateway转发请求到别的服务,如backend,速度也很快,Postman显示不到1s。

考虑到本地可以连接到测试环境Redis节点,编写单元测试:

@Test
public void testRedis() {
    long s1 = System.currentTimeMillis();
    String postData = (String) redisTemplate.opsForValue().get("my.domain.name");
    HashSet<String> roles = (HashSet<String>) JSON.parseObject(postData).get("roles");
    long s2 = System.currentTimeMillis();
    log.info("time consuming:{}ms", s2 - s1);
}

多次执行结果:

time consuming:130ms
time consuming:114ms

本地连接Redis速度挺快,不到150ms。为啥测试环境kubernetes集群连接Redis取数据耗时,短的要1s左右,长的要10s左右???

分析过SkyWalking Dashboard,没看出个啥。

Kubernetes Pod内存不一致

分析kubernetes Pod。借助于Prometheus + Grafana提供的分析面板Dashboard:
在这里插入图片描述
发现两处不太正常的地方:

  • 两个Pod内存指标数据不一致,差距有点大。

具体来说,一个Pod Current内存是1.419GiB
在这里插入图片描述
另一个是2.013GiB。
在这里插入图片描述

  • 都是保持着持续上涨的趋势

从1月24日应用发布以来到2月4日,两个Pod的Limit和Requested不变,是一条直线。其中Requested都是512MiB,Limit都是4GiB。

Current和Cache一直保持增长,Current总是大于Cache。截图没有体现出来,截止到2月4日,Current为1.580GiB,Cache为1.502GiB:
另一个Pod差不多也是这样的增长趋势:
在这里插入图片描述
但在1月29日凌晨左右,Cache超过Current保持一路高升趋势,到2月4日Cache高达3.193GiB,Current高达2.405GiB:
在这里插入图片描述
其余指标,如CPU和Network IO一直都很平稳。

参考

  • kubernetes-pod-high-cache-memory-usage

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/372564.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

机器学习中的有监督学习和无监督学习

有监督学习 简单来说&#xff0c;就是人教会计算机学会做一件事。 给算法一个数据集&#xff0c;其中数据集中包含了正确答案&#xff0c;根据这个数据集&#xff0c;可以对额外的数据希望得到一个正确判断&#xff08;详见下面的例子&#xff09; 回归问题 例如现在有一个…

【算法】枚举——蓝桥杯、日期统计、特殊日期(位数之和)、2023、特殊日期(倍数)、跑步锻炼

文章目录 蓝桥杯日期统计特殊日期&#xff08;位数之和&#xff09;2023特殊日期&#xff08;倍数&#xff09;跑步锻炼 蓝桥杯 日期统计 日期统计 如果暴力枚举100个数的八次循环那就是1016次运算&#xff0c;时间复杂度太高了&#xff0c;好在前四次的2023是确定的&#xf…

【实用原创】20个Python自动化脚本,解放双手、事半功倍

在当今的快节奏工作环境中&#xff0c;自动化不再是一种奢侈&#xff0c;而是提高效率和精确性的必需手段。Python&#xff0c;以其易于学习和强大的功能而闻名&#xff0c;成为实现各种自动化任务的理想选择。无论是数据处理、报告生成&#xff0c;还是日常的文件管理&#xf…

如何配置SSH实现无公网ip远程连接访问Deepin操作系统

&#x1f4d1;前言 本文主要是配置SSH实现无公网ip远程连接访问Deepin操作系统的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️** &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &…

06 MP之自动填充+SQL执行的语句和速度分析

1. 自动填充 在项目中有一些属性&#xff0c;比如常见的创建时间和更新时间可以设置为自动填充。 1.1 实例 需求: 将创建时间和更新时间设置为自动填充, 这样每次插入数据时可以不用理会这两个字段 1.1.1 在数据库增加字段 默认开启驼峰映射 createTime --> create_time…

Linux环境下的基本指令

最便捷Linux环境就是用云服务器&#xff0c;下载一个远程终端软件进行操作即可。 远程终端软件这里我比较推荐XShell软件&#xff0c;下载官网https://www.netsarang.com/products/xsh_overview.html 下载安装的时候选择 "home/school" 则为免费版本。 查看 Linux …

加速大规模商业化!量子信息公司Infleqtion收购两家集成硅光子公司

​内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 编辑丨慕一 编译/排版丨卉可 沛贤 深度好文&#xff1a;1200字丨10分钟阅读 近期&#xff0c;美国量子信息公司Infleqtion宣布成功收购两家集成硅光子公司&#xff1a;SiNoptiq公司和Morton…

从小白到入门webrtc音视频通话

0. 写在前面 先会骑车&#xff0c;再研究为什么这么骑&#xff0c;才是我认为学习技术的思路&#xff0c;底部付了demo例子&#xff0c;根据例子上面的介绍即可运行。 1. 音视频通话要用到的技术简介 websocket 介绍&#xff1a;1. 服务器可以向浏览器推送信息&#xff1b;2…

CSS的Day05(浮动+flex布局)

跟着黑马程序员的课&#xff0c;稍稍对CSS的了解 常见的显示模式&#xff1a;行内、块级、行内块 在HTML中&#xff0c;标准流也称为文档流或普通流&#xff0c;是指元素按照其在HTML文档中的出现顺序依次排列的方式。在标准流中&#xff0c;元素会自动占据父容器的空间&#…

哪些骨传导蓝牙立体声耳机好?骨传导蓝牙立体声耳机高性价比推荐

对许多人来说&#xff0c;音乐已成为他们日常生活的一部分。不论是作为运动的动力还是休闲放松时的柔和旋律&#xff0c;优质的耳机能极大地丰富我们的听觉享受。如果你对传统入耳式的不适感到厌烦&#xff0c;那么骨传导蓝牙立体声耳机将会是你理想的替代品。很多人就问了&…

for循环的多重跳出

for的多重跳出 1.前言2.标签使用3.使用异常的方式 本文在jdk17中测试通过 1.前言 前段时间面试时&#xff0c;面试官问我多重for循环如何跳出&#xff0c;我懵了&#xff0c;今天特别的研究了一下 本文主要说的不是continue与break&#xff0c;而是少用的另类操作 1.continue:…

支持向量机

支持向量机&#xff08;Support Vector Machine&#xff0c;SVM&#xff09;是一个非常优雅的算法&#xff0c;具有非常完善的数学理论&#xff0c;常用于数据分类&#xff0c;也可以用于数据的回归预测中。支持向量机在许多领域都有广泛的应用&#xff0c;如文本分类、图像识别…

SpringBoot中的WebMvcConfigurer

SpringBoot中的WebMvcConfigurer 一、WebMvcConfigurer二、页面跳转控制器三、数据格式化1.Formatter\<T>2.内容转换器 四、拦截器 一、WebMvcConfigurer WebMvcConfigurer 作为配置类&#xff0c;采用 JavaBean 的形式来代替传统的 XML 配置文件形式&#xff0c;进而针…

【Linux】文件周边002之初步理解文件管理(打开的文件)

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.&#xff08;打开…

SSH免密切换服务器案例-ssh协议(公钥和私钥)

公钥和私钥理解 公钥提供加密&#xff0c;私钥解密&#xff0c;公钥可以共享&#xff0c;私钥不可以。举例公钥相当于锁头&#xff0c;可以给别人用&#xff0c;钥匙相当于私钥&#xff0c;只能开自己发出去的锁头&#xff0c;也就是私钥和公钥成对&#xff0c;私钥只能解密对…

Go指针探秘:深入理解内存与安全性

目录 1. 指针的基础1.1 什么是指针&#xff1f;1.2 内存地址与值的地址1.2.1 内存中的数据存储1.2.2 如何理解值的地址 2. Go中的指针操作2.1 指针类型和值2.1.1 基本数据类型的指针2.1.2 复合数据类型的指针 2.2 如何获取一个指针值2.3 指针&#xff08;地址&#xff09;解引用…

框架学习Maven

声明&#xff1a;本文来源于黑马程序员PDF讲义 做为一名Java开发工程师&#xff0c;后端 Web开发技术是我们学习的重点&#xff0c;后端Web开发技术的学习&#xff0c;我们会先学习Java项目的构建工具&#xff1a;Maven 初识Maven Maven是Apache旗下的一个开源项目&#xff…

建筑行业数字化:从设计到运维的全面革新

随着科技的快速发展&#xff0c;数字化技术在各行各业中的应用越来越广泛。建筑行业作为传统产业&#xff0c;也在积极拥抱数字化技术&#xff0c;以提高效率、降低成本并实现可持续发展。本文将主要探讨建筑行业数字化的几个关键领域&#xff0c;包括建筑设计数字化、施工管理…

【爬虫作业】python爬虫作业——爬取汽车之家

爬取汽车之家期末作业&#xff1a; 代码如下所示&#xff1a; import random import timeimport requests #发送网络请求 import parsel import csv # 1.发送网络请求 headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like G…

LFU缓存(Leetcode460)

例题&#xff1a; 分析&#xff1a; 这道题可以用两个哈希表来实现&#xff0c;一个hash表&#xff08;kvMap&#xff09;用来存储节点&#xff0c;另一个hash表&#xff08;freqMap&#xff09;用来存储双向链表&#xff0c;链表的头节点代表最近使用的元素&#xff0c;离头节…