1. 程式人生 > 其它 >聊聊微服務釋出重啟

聊聊微服務釋出重啟

技術標籤:JAVA隨筆spring優雅停機微服務java

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. 其他中介軟體服務下線

  • 參考以上原理,檢視對應中介軟體服務原理