聊聊微服务发布重启

1. 概述

在微服务架构体系下,服务的每一个节点都不应该是单点。每个服务都是集群部署,这样服务的发布会非常频繁。对于在大流量请求下,如何保证服务节点的发布过程中不影响任何一个业务请求往往会被忽略掉。
在分布式系统中,一个请求的异常很可能就会导致一笔业务处理失败。业务如果没有自修复能力的话,这笔业务就会中断,往往需要人工介入,甚者会收到用户投诉。
所以,在分布式微服务体系下,服务的优雅发布也是不能忽略的一部分。

2. 服务优雅发布流程

2.1 服务的发布流程

  • 服务自检
    • 服务初始化配置是否正确
    • 依赖的中间件是否连接成功(如:db, redis, mq 等)
    • 可参考spring boot actuator
  • 导入流量
    • 流量导入应该按照比率递增,避免突然的高流量导致内部的连接池等组件异常

2.2 服务的停止流程

  • 入口流量
    • http调用
    • rpc调用
    • MQ消费逻辑
    • 定时任务
    • 其他触发业务流程入口
  • 关闭入口流量
    • 关闭流量导入不应该使当前应用实例对应调用拒绝服务,应该通过服务注册中心下线对应实例发布的服务,通常由于通知的延后性,要保证服务下线之后依然能够提供点对点调用
    • 关闭流量入口要保证现有处理中的业务流程正常执行完成

2.3 服务上线下线

基于以上的服务优雅发布平台的逻辑实现,我们可以实现服务的某一个临时下线和上线的功能。临时下线能支持线上某些特殊问题的排查。此时能保留线上运行环境,但此节点不对线上业务提供服务。

  • 下线:通知服务注册中心下线某个实例全部服务
  • 上线:通知服务中心中心发布某个实例全部服务

3 服务下线的实现

  • 通常微服务RPC服务发布都类似(上图)流程,RPC服务的下线流程比较简单,只需要在注册中心将本实例发布的服务取消注册即可
  • MQ的话只需要停止消息的消费逻辑
  • 定时调度任务只需要将本实例排除即可
  • 以下提供几种中间件下线的示例代码

3.1. dubbo服务下线

支持QOS的dubbo版本

import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.registry.Registry;
import org.apache.dubbo.registry.RegistryFactory;
import org.apache.dubbo.rpc.model.ApplicationModel;
import org.apache.dubbo.rpc.model.ProviderModel;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;

/**
 * dubbo服务上线下线
 */
public class DubboManager {

    public void onlineDubbo() {
        managerDubboService(false);
    }

    public void offlineDubbo() {
        managerDubboService(true);
    }

    private void managerDubboService(boolean disabled) {
        Collection<ProviderModel> providerModelList = ApplicationModel.getServiceRepository().getExportedServices();
        RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
        for (ProviderModel providerModel : providerModelList) {
            List<ProviderModel.RegisterStatedURL> statedUrls = providerModel.getStatedUrl();
            for (ProviderModel.RegisterStatedURL statedUrl : statedUrls) {
                if (disabled) {
                    if (statedUrl.isRegistered()) {
                        Registry registry = registryFactory.getRegistry(statedUrl.getRegistryUrl());
                        registry.unregister(statedUrl.getProviderUrl());
                        statedUrl.setRegistered(false);
                    }
                } else {
                    if (!statedUrl.isRegistered()) {
                        Registry registry = registryFactory.getRegistry(statedUrl.getRegistryUrl());
                        registry.register(statedUrl.getProviderUrl());
                        statedUrl.setRegistered(true);
                    }
                }
            }
        }
    }
}

低版本的dubbo实现(不够优雅)

public void dubboOnline() {
    Collection<Registry> registries = getAllDubboRegistry();
    for (Registry registry : registries) {
        if (registry instanceof ZookeeperRegistry) {
            ZookeeperRegistry zkr = (ZookeeperRegistry) registry;
            Set<URL> registeredURL = zkr.getRegistered();
            for (URL url : registeredURL) {
                try {
                    log.info("doRegister for {}", url);
                    Method method = ReflectionUtils.findMethod(ZookeeperRegistry.class, "doRegister", URL.class);
                    method.setAccessible(true);
                    ReflectionUtils.invokeMethod(method, zkr, url);
                } catch (Exception e) {
                    log.error("doRegister for " + url.toString() + " error", e);
                }
            }
        } else {
            log.warn("not zookeeper registry, can not online");
        }
    }
}

public void dubboOffline() {
    Collection<Registry> registries = getAllDubboRegistry();
    for (Registry registry : registries) {
        if (registry instanceof ZookeeperRegistry) {
            // 暂时实现基于zookeeper注册中心的上线下线功能,其他注册服务暂时未实现
            ZookeeperRegistry zkr = (ZookeeperRegistry) registry;
            Set<URL> registeredURL = zkr.getRegistered();
            for (URL url : registeredURL) {
                try {
                    log.info("doUnregister for {}", url);
                    Method method = ReflectionUtils.findMethod(ZookeeperRegistry.class, "doUnregister", URL.class);
                    method.setAccessible(true);
                    ReflectionUtils.invokeMethod(method, zkr, url);
                } catch (Exception e) {
                    log.error("doUnregister for " + url.toString() + " error", e);
                }
            }
        } else {
            log.warn("not zookeeper registry, can not offline");
        }
    }
}

private Collection<Registry> getAllDubboRegistry() {
    return AbstractRegistryFactory.getRegistries();
}

3.2. HTTP服务下线

  1. HTTP服务可以通过Eureka注册中心提供,只要取消对应实例的注册
  2. 基于nginx的反向代理实现, 配置健康检查nginx
# nginx upstreeam配置
http1.1:
        upstream test.domain {
            server 192.168.1.1:80;
            server 192.168.1.2:80;
            #http健康检查
            check_keepalive_requests 10;
            check_http_send "HEAD /health/status HTTP/1.1\r\nConnection: keep-alive\r\n\r\n";
            check_http_expect_alive http_2xx http_3xx;
        }
        
http1.0:
        upstream test.domain {
            server 192.168.10.1:80;
            server 192.168.10.2:80;
            #http健康检查
            check interval=3000 rise=2 fall=5 timeout=1000 type=http;
            check_http_send "HEAD /health/status HTTP/1.0\r\n\r\n";
            check_http_expect_alive http_2xx http_3xx;
        }

interval=3000 代表重试间隔3s;
rise=2 练习2次检测OK后标识后端为up状态;
fall=5 尝试5次后失败标识为down状态;
timeout=1000 标识每次检查的超时时间为1s;
type=http 代表检查类型是http;
check_http_send "HEAD /health/status HTTP/1.0\r\n\r\n"; 指定健康检查页面及http协议;
check_http_expect_alive http_2xx http_3xx; 预期返回结果。

3.3. RabbitMQ服务下线

public void rabbitMqOffline() {
    for (SimpleMessageListenerContainer container : getAllRabbitMqContainer()) {
        container.stop();
    }
}

public void rabbitMqOnline() {
    for (SimpleMessageListenerContainer container : getAllRabbitMqContainer()) {
        container.start();
    }
}

private Collection<SimpleMessageListenerContainer> getAllRabbitMqContainer() {
    Set<SimpleMessageListenerContainer> set = Sets.newHashSet();
    // 获取spring上下文对象
    ApplicationContext context = getApplicationContext();
    do {
        Collection<SimpleMessageListenerContainer> listenerContainers = context.getBeansOfType(SimpleMessageListenerContainer.class).values();
        set.addAll(listenerContainers);
        context = context.getParent();
    } while (context != null);

    return Collections.unmodifiableCollection(set);
}

3.4. ElasticJob调度下线

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import com.dangdang.ddframe.job.lite.internal.storage.JobNodePath;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.util.env.IpUtils;

public class ElasticJobManager {

    // 基于spring的注入
    @Autowired(required = false)
    private List<CoordinatorRegistryCenter> list;

    /**
     * 停止当前节点的任务调度
     */
    public void disableCurrentServerScheduler() {
        setCurrentScheduler(IpUtils.getIp(), true);
    }

    /**
     * 恢复当前节点的任务调度
     */
    public void enableCurrentServerScheduler() {
        setCurrentScheduler(IpUtils.getIp(), false);
    }

    private void setCurrentScheduler(String serverIp, boolean disabled) {
        if (list == null) {
            return;
        }

        for (CoordinatorRegistryCenter regCenter : list) {
            List<String> jobNames = regCenter.getChildrenKeys("/");
            for (String each : jobNames) {
                if (regCenter.isExisted(new JobNodePath(each).getServerNodePath(serverIp))) {
                    JobNodePath jobNodePath = new JobNodePath(each);
                    String serverNodePath = jobNodePath.getServerNodePath(serverIp);
                    if (disabled) {
                        regCenter.persist(serverNodePath, "DISABLED");
                    } else {
                        regCenter.persist(serverNodePath, "");
                    }
                }
            }
        }
    }
}

3.5. 其他中间件服务下线

  • 参考以上原理,查看对应中间件服务原理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值