首页 > 其他分享 >K8S测试环境重启微服务遇到的问题---思路1

K8S测试环境重启微服务遇到的问题---思路1

时间:2024-03-15 22:33:59浏览次数:25  
标签:服务 --- 实例 测试环境 org import Pod K8S cloud

场景

测试环境,采用k8s容器化部署,通过rancher在web界面对资源进行管理;
各项目组有独立的网关,多个微服务(根据业务功能、高内聚低耦合划分);

网关是基于spring-cloud-gateway,定制扩展了一些功能,如鉴权、限流等;
微服务是基于spring-cloud各组件,eureka、ribbon、hystrix等;

由于是测试环境使用,网关和各微服务都只部署了1个节点;

持续集成(git+jenkins+docker+harbor+rancher)

当开发修改项目代码、自测检查后,提交git仓库,通过jenkins构建打包生成docker镜像并推送至harbor,
然后通过rancher界面,点击重新部署按钮,重启部署最新的服务。

这是一套测试环境的持续集成流程,从开发到部署。

通常网关不会经常修改,微服务会经常修改,如bug修复、新功能开发、代码优化等。

当在rancher点击重启部署,工作负载的pod会停止旧的启动新的,整个重启过程需要时间,在这个过程中会出现服务不可用的情况。
这也是测试同学经常反馈"抱怨"的:每当开发修改代码重启服务时,会出现频繁报错,影响测试工作的正常进行。

分析

由于测试环境只部署了1个节点,微服务重启,这个过程导致服务有一定时间不可用。

之前写过1篇博客探讨过微服务的优雅停机:Spring-Cloud-Gateway+Ribbon+Eureka微服务优雅停机实践
但这里测试环境只部署了1个节点。单个服务从停止到启动,需要一定耗时。

看看rancher里的缩放/升级策略

  • 滚动: 先启动新 Pod,再停止旧 Pod。
  • 滚动: 先停止旧 Pod,再启动新 Pod。
  • 删除所有 Pod,然后重新开始。
  • 自定义

默认第1个是先启动新Pod,再停止旧Pod。
在重新部署过程中也发现,新Pod是先启动,旧Pod在进行停止和删除的。

注意到界面上缩放/升级策略里还有2个时间配置:

  • 最短准备时间:0
  • 进度截止时间:600

这里把最短准备时间改为200,即200秒新Pod才被视为可用,这样给服务留了启动时间。

调整这个配置后,当有代码修改重新部署微服务,发现仍然有一定的时间服务不可用,只是比之前的情况好一些。

由于客户端(如web、app、小程序)是调用网关,通过网关路由调用微服务接口的,即客户端 -> spring-cloud-gateway网关服务 -> 微服务。

接口出现不可用,推测是在重启过程中,网关调用到了不可用Pod里的服务。

项目里网关spring-cloud-gateway通过ribbon进行调用方的负载均衡;
网关、各微服务的注册中心是eureka-server;
ribbon获取各服务节点及状态是通过eureka-client来实现的;

由于eureka-server是CAP理论里的AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C);
它本身设计了多级缓存(readWriteCacheMapreadOnlyCacheMap);
eureka-client是从缓存中获取数据,有可能服务停止过程,而获取的实例(InstanceInfo)的状态(status)是UP

ribbon里也设计了缓存,参考DynamicServerListLoadBalancerPollingServerListUpdater等;

因此当服务重启,1个Pod在停止时,可能服务已停止不可用了,但ribbon里还有该节点的Server,并且状态是存活(isAlive()方法返回true),
该节点仍然能被负载均衡算法选取到,发起调用造成调用失败。

打开spring-cloud-gateway包的LoadBalancerClientFilter类,梳理它使用ribbon进行负载均衡选取实例调用,它的choose方法:

protected ServiceInstance choose(ServerWebExchange exchange) {
    return loadBalancer.choose(
            ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}

这里的loadBalancerLoadBalancerClient类型,ribbon实现类为RibbonLoadBalancerClient,查看它的choose方法:

public ServiceInstance choose(String serviceId, Object hint) {
    Server server = getServer(getLoadBalancer(serviceId), hint);
    if (server == null) {
        return null;
    }
    return new RibbonServer(serviceId, server, isSecure(server, serviceId),
            serverIntrospector(serviceId).getMetadata(server));
}

可以看到,它使用ILoadBalancer进行实例Server的选取。
进一步跟踪代码,了解到ribbon默认使用ZoneAwareLoadBalancerZoneAvoidanceRule,限于篇幅这里不在赘述。

借助arthas去看一看ILoadBalancer里缓存的实例列表:

  1. 在rancher里进入网关服务的控制台安装arthas
mkdir arthas
cd arthas
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
  1. 通过arthas里的ongl调用ribbon获取所有实例的方法
java -jar arthas
ognl -c xxx '@com.xxx.utils.SpringContextUtil@getBean(@org.springframework.cloud.netflix.ribbon.SpringClientFactory@class).getLoadBalancer("service-xxx").getAllServers()'

注:

  • SpringContextUtil是项目里的工具类,通过实现ApplicationContextAware接口获取ApplicationContext实例,提供了便捷获取Spring Bean的方法
  • -c xxx里的xxx 是hash值,可通过sc -d com.xxx.utils.SpringContextUtil查看
  • getLoadBalancer("service-xxx")里的service-xxx是网关调用具体微服务的服务名称

在rancher重新部署微服务,多次执行上面的ognl命令,发现返回的实例列表,从1个变更2个最后变为1个;
当新POD重启成功后,实例列表变为2个,而当旧POD停止时,实例列表仍然是2个,需要等一会儿才变为1个;
这样验证了上面对网关spring-cloud-gateway通过ribbon进行调用方的负载均衡的分析。

解决

有了上述分析和验证,开始思考解决方法。

注意到spring-cloud-gateway的LoadBalancerClientFilter类,它是一个GlobalFilter
考虑对它进行定制,实现一个定制的GlobalFilter用于使用,重写它的choose()方法,保证返回的实例是可用的。

原方法里使用了ribbon的RibbonLoadBalancerClient来选取实例,但ribbon有缓存机制问题。

因为想到直接用eureka-client来获取实例;
eureka-client也有缓存怎么办?InstanceInfo里的status不是准确的;
注意到InstanceInfo里有个时间戳lastUpdatedTimestamp字段,表示最后更新时间,
根据缩放/升级策略,新Pod启用,老Pod停止,那么当新Pod里服务启动成功,让每次选取都选取新的实例即可;
这样老Pod在停止服务的过程中,实例不被选取到,即解决了调用失败的问题。

定制的GlobalFilter代码如下:

import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.List;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;

/**
 * @author cdfive
 */
@Slf4j
@Setter
@Profile(value = {"test"})
public class LatestUpdateTimeLoadBalancerClientFilter implements GlobalFilter, Ordered {

    private static final Log log = LogFactory.getLog(LatestUpdateTimeLoadBalancerClientFilter.class);

    protected LoadBalancerClient loadBalancer;

    private LoadBalancerProperties properties;

    private EurekaClient eurekaClient;

    public LatestUpdateTimeLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties, EurekaClient eurekaClient) {
        this.loadBalancer = loadBalancer;
        this.properties = properties;
        this.eurekaClient = eurekaClient;
    }

    @Override
    public int getOrder() {
        return LoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER - 1;
    }

    @Override
    @SuppressWarnings("Duplicates")
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
        if (url == null
                || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }
        // preserve the original url
        addOriginalRequestUrl(exchange, url);

        if (log.isTraceEnabled()) {
            log.trace("LoadBalancerClientFilter url before: " + url);
        }

        final ServiceInstance instance = choose(exchange);

        if (instance == null) {
            throw NotFoundException.create(properties.isUse404(),
                    "Unable to find instance for " + url.getHost());
        }

        URI uri = exchange.getRequest().getURI();

        // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
        // if the loadbalancer doesn't provide one.
        String overrideScheme = instance.isSecure() ? "https" : "http";
        if (schemePrefix != null) {
            overrideScheme = url.getScheme();
        }

        URI requestUrl = loadBalancer.reconstructURI(
                new DelegatingServiceInstance(instance, overrideScheme), uri);

        if (log.isTraceEnabled()) {
            log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
        }

        exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
        return chain.filter(exchange);
    }

    protected ServiceInstance choose(ServerWebExchange exchange) {
        URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String serviceId = url.getHost();

        List<InstanceInfo> instanceInfos = eurekaClient.getInstancesByVipAddress(serviceId, false);
        log.info("debugStart=>" + instanceInfos.size());
        InstanceInfo info = null;
        Long time = null;
        for (InstanceInfo instanceInfo : instanceInfos) {
            log.debug(instanceInfo.getInstanceId() + "=>" + instanceInfo.getStatus());
            if (InstanceInfo.InstanceStatus.UP.equals(instanceInfo.getStatus()) && (time == null || instanceInfo.getLastUpdatedTimestamp() > time)) {
                time = instanceInfo.getLastUpdatedTimestamp();
                info = instanceInfo;
            }
        }

        if (info != null) {
            log.info("debugEnd=>" + info.getInstanceId() + "," + info.getStatus());
            return new EurekaDiscoveryClient.EurekaServiceInstance(info);
        }

        return null;
    }
}

注:

  • EurekaClient在网关项目的Spring容器有,这里通过构造方法注入,用@Autowired注解也可

  • 通过eurekaClient.getInstancesByVipAddress获取服务的实例列表,通过比较选取里面lastUpdatedTimestamp最大的1个实例

  • instanceInfo.getStatus()判断值为InstanceStatus.UP,由于缓存问题判断效果是不能确保的,因此还要加上lastUpdatedTimestamp

  • 通过debugStart=>debugEnd=>里打印的日志信息,方便查看每次调用时节点总数和选取的节点信息

  • 通过@Profile(value = {"test"})注解,标识该Filter仅测试环境使用

  • 实现Ordered接口的int getOrder() 方法,优先于系统的LoadBalancerClientFilter执行

参考

标签:服务,---,实例,测试环境,org,import,Pod,K8S,cloud
From: https://www.cnblogs.com/cdfive2018/p/18075364

相关文章

  • 浙大恩特客户资源管理系统-RegulatePriceAction接口SQL注入
    简介杭州恩软信息技术有限公司(浙大恩特)提供外贸管理软件、外贸客户管理软件等外贸软件,是一家专注于外贸客户资源管理及订单管理产品及服务的综合性公司。漏洞简介浙大恩特客户资源管理系统-RegulatePriceAction接口存在SQL注入漏洞,可以利用该漏洞获取数据库中的信息(例如,管理......
  • 趣玩法(2)---生物还原
    孟德尔的豌豆杂交实验,上代码#include<bits/stdc++.h>usingnamespacestd;stringf1[1000086],f2[1000086];intmain(){srand((unsignedlonglongint)time(NULL));strings1="DD",s2="dd";longlongGG=0,Gg=0,gg=0;//Genelonglong......
  • 深度学习入门基于python的理论与实现-第四章神经网络的学习(个人向笔记)
    目录从数据中学习损失函数均方误差(MSE)交叉熵误差mini_batch学习mini_batch版交叉熵误差的实现从数据中学习神经网络的"学习"的学习是指从训练数据自动获取最有权重参数的过程。神经网络的特征就是可以从数据中学习即由数据自动决定权重参数的值。机器学习通常是认为确定一些......
  • 【机器学习】机器学习创建算法第2篇:K-近邻算法【附代码文档】
    机器学习(算法篇)完整教程(附代码资料)主要内容讲述:机器学习算法课程定位、目标,K-近邻算法,1.1K-近邻算法简介,1.2k近邻算法api初步使用定位,目标,学习目标,1什么是K-近邻算法,1Scikit-learn工具介绍,2K-近邻算法API,3案例,4小结。K-近邻算法,1.3距离度量学习目标,1欧式距离,2......
  • q1-投资理财-2024.3.15
    q1-投资理财-2024.3.15​ 兴趣使然,在20岁接触到了股票,虽然没怎么赚钱并且一直都在赔钱,不过在家没有别的盈利能力,股票和期货成为搞钱的内容,期货我想碰的是鸡蛋期货,一般都是12月可能有小幅度上涨,整体一直下跌到2月份,有时候234月都是下跌的,一直到5月份会到底然后上涨到7月8月份,有的......
  • JS04-对象
    对象使用对象声明人对象属性和方法都要写在对象里面letperson={ uname:'甘雨',age:2000,sex:'女',sayHi:function(){console.log('Hi~~~~~~~~~')}}1.访问属性对象.属性名​console.log(person.uname)......
  • 通天星CMSV6车载定位监控平台 SQL注入漏洞复现(XVE-2023-23744)
    0x01产品简介通天星CMSV6车载定位监控平台拥有以位置服务、无线3G/4G视频传输、云存储服务为核心的研发团队,专注于为定位、无线视频终端产品提供平台服务,通天星CMSV6产品覆盖车载录像机、单兵录像机、网络监控摄像机、行驶记录仪等产品的视频综合平台。0x02漏洞概述该漏洞......
  • Profinet转CC-Link网关使用指南
    本文为您提供CCLINK转Profinet网关(XD-PNCR20)使用指南,教您如何快速配置和集成网关,实现CCLINK与Profinet的互联。CCLINK转Profinet网关(XD-PNCR20)是一个经过自主研发的先进设备,CCLINK转Profinet网关设备的研发旨在实现CCLINK总线和Profinet网络之间的完美连接,从而实现各种总线系统的......
  • 更新用户基本信息-完成参数校验(2024-3-15)
    实体参数校验@NotNull@NotEmpty@Email接口方法的实体参数上添加@Validated注解@PutMapping("/update")publicResultupdate(@RequestBody@ValidatedUseruser){userService.update(user);returnResult.success();}@NotNullprivate......
  • 华为OD机试真题-欢乐的周末-2024年OD统一考试(C卷)
    题目描述:小华和小为是很要好的朋友,他们约定周末一起吃饭。通过手机交流,他们在地图上选择了多个聚餐地点(由于自然地形等原因,部分聚餐地点不可达),求小华和小为都能到达的聚餐地点有多少个?输入描述:第一行输入m和n,m代表地图的长度,n代表地图的宽度。第二行开始具体输入地图信息,......