在使用ServiceComb时,大家关注最多的是微服务注册发现、高性能、服务治理和无状态等特性,其中无状态之后就可以随意起停,但是在运维时,我们发现并不是这么回事。因为如果直接杀掉进程、再重新启动,可能会有正在处理的事务,会导致业务报错,还有就是杀掉的进程并不会马上被调用端感知,调用端会出现大量的异常,即使配置重试、隔离机制,也只能减少一部分异常,所以在正常升级过程中,我们探寻如何零异常也是一个富有挑战性工作。
- 优雅停机
实现原理:ServiceComb在启动时,通过向JVM注册一个Hook来响应操作系统的信号量,Runtime.getRuntime().addShutdownHook(new Thread(this::destroy)); 所以优雅停机依赖于JVM的ShutdownHook机制,会在以下情况下会被触发:
- 程序正常退出,当最后一个非守护(no-daemon)线程退出或者进程退出方法被执行,例如System.exit方法被执行
- 虚拟机被人为终止,例如^C,kill pid等(kill -9 pid不会触发)
- 服务提供方,停止时,先把服务状态标记为STOPPING,不再接受新的请求,新的请求会被拒绝,客户端可以配合重试机制重试到别的节点。然后去服务中心注销本实例,等所有已接收的请求处理完毕,最后等所有线程执行完毕
- 服务消费者,先标记服务状态为STOPPING,直接拒绝发起新的调用请求,等待当前已发送的请求响应,如果超时,则强制关闭。
使用方法:kill pid
在实际使用过程中,我们可能会碰到执行kill pid不能杀掉进程:
- 信号量被拦截,根本没有触发ServiceComb的hook。
有一次产品使用时,发现使用kill pid不能触发优雅停机,但是kill -1 pid可以,最后定位发现是使用的一个中间件,它设置了Signal处理用户输入信号,导致ServiceComb的Hook没有被触发
- 进程启动了自己定制的线程池或用户的非守护线程,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强制杀掉进程
遗留&优化:在优雅停机时,向服务中心注销时,并不会等服务中心通知所有服务消费者实例成功后再停机,服务中心会延迟两秒通知到消费者。所以在这短暂过程中还会有请求过来,如果流量较大还会触发隔离,还可能产生不必要的告警。
- 滚动升级
ServiceComb对优雅停机的良好支持,结合优雅停机,可以很好地实现滚动升级
比如服务A调用服务B,现在服务B有三个实例B1/B2/B3,可以按照以下步骤升级:
- 首先升级实例B1,先对B1执行优雅停机,B1会从注册中心中注销实例
- 服务中心会通知消费端A 实例B1已经注销
- 服务A刷新本地缓存,不会再请求到实例B1
- 对B1进行升级(二进制文件替换或者镜像替换),然后重新启动
- 实例B1重新注册
- 服务中心会通知消费端A 有实例B1注册
- 服务A刷新本地缓存,请求会重新分发到实例B1
- 然后依次升级其他的实例B2 B3
滚动升级一把都要结合部署系统,下面结合华为云ServiceStage来看看具体部署实现:
- 优雅上线
服务新的版本上线启动后,如何知道这个实例能否正常提供服务,如果实例启动正常,但是不能正常提供业务服务,比如数据库配置错误,这样会导致业务异常,升级失败。ServiceComb支持启动时设置实例状态,当服务启动时,可以先设置为TESTING,实例可以在服务中心正常注册,也能被发现,但是其它服务不会调用这个实例。启动拨测服务,对该实例接口进行拨测验证,只有所有接口都测试通过后,再把该实例状态改为UP,其它服务才会把流量分发到该实例。
- 实例B1升级完成,向服务中心注册实例,状态为TESTING
- 服务中心通知服务A服务B1注册,服务A刷新服务B实例,由于是TESTING状态,不会发起调用
- 拨测服务开始对实例B1进行接口测试
- 4.1 测试完成后,会调用服务中心接口,把状态改成UP
4.2 可以增加一个通知接口,告诉实例已经拨测成功,实例B1标记该版本拨测成功
- 服务中心通知服务A实例发生变化,服务A刷新实例,把流量分发到实例B1
优雅上线一般是服务升级时才需要,所以需要区分升级和重启两种场景,所以在初次升级时,启动脚本可以检测该版本是否已经成功启动过,如果没有,则把实例状态设置为TESTING,否则不设置实例状态。拨测服务拨测成功后,可以调用实例接口告诉该实例已经拨测成功,实例可以在该接口设置该版本是否已经成功拨测。
- 灰度升级
ServiceComb支持灰度升级时基于两个功能:
- 隐式传参,ServiceComb提供InvocationContext的context上下文,该上下文的参数可以在服务之间传递
- 路由扩展,ServiceComb提供了易扩展的负载均衡能力,其中包括Discovery机制和ServerListFilter机制,详细可参考ServiceComb的负载均衡文档
在使用ServiceComb中,碰到最多的就是一下两种情况,1、小版本小特性升级 2、大版本全网升级
小版本、小特性升级,新的特性只给符合特定要求的用户使用。比如某商城,促销服务上线了一种新型促销功能,只对VIP用户开放
我们可以利用HttpServerFilter机制,根据参数,对实例版本进行过滤。也可以使用CSE提供的页面进行灰度设置
大版本全网升级,在团队大项目中,这种场景很常见,几个项目组经过一两个月开发,统一到现网升级,该版本性能、可靠性都没经过生产环境检验,所以先升级到灰度,让部分用户先体验,然后再决定是否全网升级
灰度节点统一打上标签,实例注册时带上该标签属性,这个可以通过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; } } |