ServiceComb微服务升级实践

时间:2024-04-09 22:57:45

 

在使用ServiceComb时,大家关注最多的是微服务注册发现、高性能、服务治理和无状态等特性,其中无状态之后就可以随意起停,但是在运维时,我们发现并不是这么回事。因为如果直接杀掉进程、再重新启动,可能会有正在处理的事务,会导致业务报错,还有就是杀掉的进程并不会马上被调用端感知,调用端会出现大量的异常,即使配置重试、隔离机制,也只能减少一部分异常,所以在正常升级过程中,我们探寻如何零异常也是一个富有挑战性工作。

  1. 优雅停机

实现原理:ServiceComb在启动时,通过向JVM注册一个Hook来响应操作系统的信号量,Runtime.getRuntime().addShutdownHook(new Thread(this::destroy)); 所以优雅停机依赖于JVM的ShutdownHook机制,会在以下情况下会被触发:

  • 程序正常退出,当最后一个非守护(no-daemon)线程退出或者进程退出方法被执行,例如System.exit方法被执行
  • 虚拟机被人为终止,例如^C,kill pid等(kill -9 pid不会触发)
  1. 服务提供方,停止时,先把服务状态标记为STOPPING,不再接受新的请求,新的请求会被拒绝,客户端可以配合重试机制重试到别的节点。然后去服务中心注销本实例,等所有已接收的请求处理完毕,最后等所有线程执行完毕
  2. 服务消费者,先标记服务状态为STOPPING,直接拒绝发起新的调用请求,等待当前已发送的请求响应,如果超时,则强制关闭。

使用方法:kill pid

在实际使用过程中,我们可能会碰到执行kill pid不能杀掉进程:

  1. 信号量被拦截,根本没有触发ServiceComb的hook。

有一次产品使用时,发现使用kill pid不能触发优雅停机,但是kill -1 pid可以,最后定位发现是使用的一个中间件,它设置了Signal处理用户输入信号,导致ServiceComb的Hook没有被触发

  1. 进程启动了自己定制的线程池或用户的非守护线程,ServiceComb停机时,没有停止对应的线程。

排查这种情况可以先查看ServiceComb日志,是否有ServiceComb is closing now...如果有表示已经触发,如果有表示已经触发到了ServiceComb的优雅停机机制。是否有ServiceComb had closed表示ServiceComb已经关闭完了。如果进程还没有退出,说明进程肯定还有非守护线程在运行,具体可以通过jstack查看线程状态,判断是哪些线程仍然在执行。

如果需要主动关闭业务线程,实现BootListener接口,支持SpringBean和SPI两种加载机制,例如:

import org.apache.servicecomb.core.BootListener;

import org.springframework.stereotype.Component;

 

@Component

public class DemoBootListener implements BootListener {

 

    @Override

    public void onBootEvent(BootEvent event) {

        if (!EventType.AFTER_CLOSE.equals(event.getEventType())) {

            return;

        }

        // 响应关闭事件,关闭业务相关,比如数据库连接,定时任务等

        close_self_threads();

    }

}

 

 

保护措施:在kill pid后,增加一个超时保护措施,如果时间范围内还没有kill成功,则调用kill -9 pid强制杀掉进程

遗留&优化:在优雅停机时,向服务中心注销时,并不会等服务中心通知所有服务消费者实例成功后再停机,服务中心会延迟两秒通知到消费者。所以在这短暂过程中还会有请求过来,如果流量较大还会触发隔离,还可能产生不必要的告警。

  1. 滚动升级

ServiceComb对优雅停机的良好支持,结合优雅停机,可以很好地实现滚动升级

ServiceComb微服务升级实践

比如服务A调用服务B,现在服务B有三个实例B1/B2/B3,可以按照以下步骤升级:

  1. 首先升级实例B1,先对B1执行优雅停机,B1会从注册中心中注销实例
  2. 服务中心会通知消费端A 实例B1已经注销
  3. 服务A刷新本地缓存,不会再请求到实例B1
  4. 对B1进行升级(二进制文件替换或者镜像替换),然后重新启动
  5. 实例B1重新注册
  6. 服务中心会通知消费端A 有实例B1注册
  7. 服务A刷新本地缓存,请求会重新分发到实例B1
  8. 然后依次升级其他的实例B2 B3

滚动升级一把都要结合部署系统,下面结合华为云ServiceStage来看看具体部署实现:

  1. 优雅上线

服务新的版本上线启动后,如何知道这个实例能否正常提供服务,如果实例启动正常,但是不能正常提供业务服务,比如数据库配置错误,这样会导致业务异常,升级失败。ServiceComb支持启动时设置实例状态,当服务启动时,可以先设置为TESTING,实例可以在服务中心正常注册,也能被发现,但是其它服务不会调用这个实例。启动拨测服务,对该实例接口进行拨测验证,只有所有接口都测试通过后,再把该实例状态改为UP,其它服务才会把流量分发到该实例。

ServiceComb微服务升级实践

  1. 实例B1升级完成,向服务中心注册实例,状态为TESTING
  2. 服务中心通知服务A服务B1注册,服务A刷新服务B实例,由于是TESTING状态,不会发起调用
  3. 拨测服务开始对实例B1进行接口测试
  4. 4.1 测试完成后,会调用服务中心接口,把状态改成UP

4.2 可以增加一个通知接口,告诉实例已经拨测成功,实例B1标记该版本拨测成功

  1. 服务中心通知服务A实例发生变化,服务A刷新实例,把流量分发到实例B1

优雅上线一般是服务升级时才需要,所以需要区分升级和重启两种场景,所以在初次升级时,启动脚本可以检测该版本是否已经成功启动过,如果没有,则把实例状态设置为TESTING,否则不设置实例状态。拨测服务拨测成功后,可以调用实例接口告诉该实例已经拨测成功,实例可以在该接口设置该版本是否已经成功拨测。

 

  1. 灰度升级

ServiceComb支持灰度升级时基于两个功能:

  1. 隐式传参,ServiceComb提供InvocationContext的context上下文,该上下文的参数可以在服务之间传递
  2. 路由扩展,ServiceComb提供了易扩展的负载均衡能力,其中包括Discovery机制和ServerListFilter机制,详细可参考ServiceComb的负载均衡文档

在使用ServiceComb中,碰到最多的就是一下两种情况,1、小版本小特性升级 2、大版本全网升级

小版本、小特性升级,新的特性只给符合特定要求的用户使用。比如某商城,促销服务上线了一种新型促销功能,只对VIP用户开放

ServiceComb微服务升级实践

我们可以利用HttpServerFilter机制,根据参数,对实例版本进行过滤。也可以使用CSE提供的页面进行灰度设置

 

大版本全网升级,在团队大项目中,这种场景很常见,几个项目组经过一两个月开发,统一到现网升级,该版本性能、可靠性都没经过生产环境检验,所以先升级到灰度,让部分用户先体验,然后再决定是否全网升级

ServiceComb微服务升级实践

灰度节点统一打上标签,实例注册时带上该标签属性,这个可以通过ServiceComb提供的org.apache.servicecomb.serviceregistry.api.PropertyExtended机制来实现,例如:

public class GrayPropertiesReader implements PropertyExtended {

    private static final Logger LOGGER = LoggerFactory.getLogger(GrayPropertiesReader.class);

    @Override

    public Map<String, String> getExtendedProperties()

    {

        boolean isGray = false;

        try {

           // 得到是否是灰度节点

            isGray = Configurator.getInstance().isGrayMode();

        } catch (Exception e) {

            LOGGER.warn("Read gray properties failed.", e);

        }

        Map<String, String> grayProperties = new HashMap<>();

        if (isGray) {

            grayProperties.put(ContextKeys.X_IS_GRAY, "1");

        }

        else {

            grayProperties.put(ContextKeys.X_IS_GRAY, "0");

        }

        return grayProperties;

    }

}

然后扩展实现一个ServerListFilterExt,使用SPI机制来加载它,如下:

public class GrayServerListFilter implements ServerListFilterExt {

 

    private static final Logger LOGGER = LoggerFactory.getLogger(GrayServerListFilter.class);

    private static final String GRAY_FLAG = "1";

 

    @Override

    public List<ServiceCombServer> getFilteredListOfServers(List<ServiceCombServer> servers, Invocation invocation) {

        boolean grayFlag = false;

        Object xgray = invocation.getContext("x-is-gray");

        if (GRAY_FLAG.equals(xgray)) {

            grayFlag = true;

        }

        List<ServiceCombServer> retList = new ArrayList<>();

        if (servers != null && !servers.isEmpty()) {

            for (ServiceCombServer server : servers) {

                if (server.getInstance() != null) {

                    String gray = server.getInstance().getProperties().get("x-is-gray");

                    if (grayFlag && GRAY_FLAG.equals(gray)) {

                        retList.add(server);

                    } else {

                        retList.add(server);

                    }

                }

            }

        }

        if (retList.isEmpty()) {

            LOGGER.error("Fail to find provider service instance , gray flag is {}", grayFlag);

            throw new InvocationException(new HttpStatus(400, "no instance"), "find non instance in mode " + grayFlag);

        }

        return retList;

    }

}