如何实现在用户无感的情况下修复线上问题

我们经常会遇到一些线上需要及时修复的问题,但是如何在用户无感知的情况下悄悄的修复线上问题呢?今天聊一聊一些常见的在用户无感的情况下发布线上系统的方案。

1、Nginx实现方案

1.1、集群情况下的修复方案

现在一般情况下我们的系统都是集群部署的,如下所示的集群服务:

假设订单服务中存在一点小bug需要及时修复,为了不影响线上的用户继续下单,我们需要在不停机的情况下发布新代码,那么我们可以利用Nginx的故障转移特性来实现新代码的发布,其过程如下所示:

(1)先将订单服务A停掉,此时Nginx就不会将订单请求转发到服务A上,由于服务B是可以对外提供服务的,所以Nginx将请求都转发到服务B上继续处理用户请求,如下图所示:

(2)服务A上发布订单的新代码,当服务A发布完成之后再去重新发布订单服务B上的新代码。

这样我们通过Nginx实现了在用户无感的情况发布新代码到服务上。

1.2、单机情况下的修复方案

有时候我们有的服务是没有做集群部署而是单机部署,这个时候我们如何实现用户无感的情况下发布新代码呢?

运维人员部署新的物流服务代码到服务器的8081端口上,然后运维人员修改nginx的配置并重新启动nginx让请求到转发到8081上,由于nginx的启动是很快的所以整体的影响并不大。这样也可以实现用户无感情况下的发布新代码。

2、SpringBoot实现无感发布

Nginx实现方案是需要人为的干预才可以实现新代码的无感发布,有没有一种无需人为干预就可以实现自动化的用户无感发布新代码呢?其实SpringBoot可以无需借助任何第三方工具的情况下就是能实现无感发布新代码,其流程如下所示:

运维人员先部署商品服务新代码到服务的8081端口上,等8081端口上的服务部署完成之后,在SpringBoot服务上配置的脚本命令优雅的杀掉8080端口上的服务(因为通过优雅停止8080服务端口上的这样既可以保证用户的无感发布,也可以实现只有在8080服务上所有的用户线程都执行结束后才会停止原先的8080端口上的服务),最后将8081端口替换成8080端口,这样通过SpringBoot自身就实现无感发布新代码。

这里的停机是优雅停机,优雅停机的实现是通过kill -2搭配SpringBoot的命令配置:

server: 
  shutdown: graceful

我们常见的kill -9是杀掉进程的,这种方式比较粗暴,因为一旦执行了kill -9之后,无论是否我们的用户线程执完成都会直接退出,那么此时就有可能出现一些意向不到问题发生,这样就自己给自己找麻烦。

SpringBoot实现用户无感发布并且优雅停机的核心代码如下所示:

@SpringBootApplication 
public class DemoApplication { 
    private final static Integer DEFAULT_PORT = 8080; 
    public static void main(String[] args) { 
        String[] newArgs = args.clone(); 
        boolean needChangePort = false; 
        //判断8080端口是否被占用 
        if(isPortInUse(DEFAULT_PORT)){ 
            newArgs = new String[args.length + 1]; 
            System.arraycopy(args,0, newArgs, 0,args.length); 
            newArgs[newArgs.length - 1] = "--server.port = 8081"; 
            needChangePort = true; 
        } 
        //启动服务 
        ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class, newArgs); 
        //判断是否需要进行端口的变换 
        if (needChangePort) { 
            String killCommand = String.format("lsof -i :%d |grep LISTEN |awk '{print $2}' |xargs kill -2", DEFAULT_PORT); 
            try { 
                //执行优雅停机的脚本 
                Runtime.getRuntime().exec(new String[]{"sh", "-c", killCommand}).waitFor(); 
                //通过判断是否原先服务已经停机 
                while (isPortInUse(DEFAULT_PORT)){ 
                } 
                //切换端口 8081 ----> 8082 
                ServletWebServerFactory servletWebServerFactory = applicationContext.getBean(ServletWebServerFactory.class) 
                ((TomcatServletWebServerFactory)servletWebServerFactory).setPort(DEFAULT_PORT); 
                WebServer webServer = servletWebServerFactory.getWebServer(applicationContext.getBean(ServletContextInitializer.class)); 
                webServer.start(); 
            } catch (Exception e){ 
                e.printStackTrace(); 
            } 
        } 
    } 
   //判断端口是否被占用 
    public static boolean isPortInUse(int port){ 
        try (ServerSocket serverSocket = new ServerSocket(port)){ 
            return false; 
        }catch (IOException e){   //端口已经被占用 
            return true; 
        } 
    } 
}

总结:

(1)通过Nginx可以实现用户无感的代码不停机的发布新代码

(2)通过Springboot自身配置可以实现代码的不停机发布。

8