Davids實操筆記:Spring Cloud使用Zuul和Ribbon實現灰度發佈

Spring Cloud使用Zuul和Ribbon做灰度發佈

關注可以查看更多粉絲專享blog~

公司產品線採用的是Spring Cloud(Dalston.SR1)、Spring Boot(1.5.x)、Spring MVC、Mybatis、Redis構建的微服務、服務數量60+,之前規定是每週二中午12點-2點發布,由於用戶訪問量的上升這樣用戶體驗特別差,之前爲了解決這個問題做過一次不停機發布方案,採用的是Spring Cloud優雅停機,具體方式如下:

  1. maven添加actuator依賴
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. application.yml中配置
endpoints.shutdown.enabled:  true  #啓用shutdown端點,以便支持優雅停機
#endpoints.shutdown.sensitive:  false  #禁用密碼驗證(可選)
  1. 在任意一臺服務器上利用curl發送shutdown命令
curl -X POST http://ip:端口/shutdown
或者
curl -d "" http://ip:端口/shutdown

回憶過去

在Jenkins腳本中配置打包完畢之後先對要發佈的那臺服務器發送shutdown命令,會收到{“message”:“Shutting down, bye…”},問題就出在這裏,服務器在收到該指令之後會從eureka退出,有時候是從eureka直接註銷,但是有時候會出現(down)標識,這個時候服務器還可以接收到請求,但是已經開始發佈了,所以用戶請求如果負載到該機器就會出現服務器錯誤,體驗很差。難道要告訴用戶我們在發佈,現在不能用嗎?NO!要不凌晨再發布吧?NONONO!!!所以開始了探索之路。

展望未來

網上有很多關於Spring Cloud灰度發佈的策略,包括K8S,Apollo、Ribbon等,K8S成本太高了,pass!我們配置中心採用的是Spring Cloud Config所以Apollo pass!最終採用的是zuul和Ribbon來做灰度發佈,其實主要方式就是網關攔截請求,通過識別請求頭中的特定標識來識別是否灰度用戶,從而將用戶路由到灰度服務上面。我們沒有灰度測試流程所以本次我只做了後半部分,在灰度發佈的時候將用戶路由到正常服務上面,待發布完成一臺之後調用服務的/health接口確定啓動成功,則灰度剩下的機器,由剛發佈完的機器提供服務以此來避免服務器錯誤的情況。做到隨時發佈,其實裏面還有一些問題,比如接口涉及到版本問題這麼簡單粗暴是不可行的,後面的路還很長,本次先記錄我的不停機發布之旅。

大致流程分爲以下幾步

1、定義自定義灰度策略配置文件

GrayRibbonConfig(在GrayRibbonConfig類中指定我們的灰度發佈規則類)

import com.*.*.*.ExcludeFromComponetScan;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;

/**
 * ribbon要排除在ComponentScan之外故新建此包 by david
 */
@Slf4j
@Configuration
@ExcludeFromComponetScan
public class GrayRibbonConfig {

    @Bean
    public IRule grayRule() {
        log.info("---customize gray publish---");
        return new GrayRule();
    }


}

RibbonClientConfig(使用@RibbonClients(defaultConfiguration = GrayRibbonConfig.class)指定客戶端路由配置爲GrayRibbonConfig類,該註解爲全局配置,如果有服務需要特殊處理則需要使用@RibbonClient(configuration = Xxx.class)自定義配置)

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ConfigurationBasedServerList;

import org.springframework.cloud.netflix.ribbon.RibbonClients;

import lombok.extern.slf4j.Slf4j;

/**
 * 自定義灰度策略
 */
@Slf4j
@RibbonClients(defaultConfiguration = GrayRibbonConfig.class)
public class RibbonClientConfig {

    public static class BazServiceList extends ConfigurationBasedServerList {
        public BazServiceList(IClientConfig config) {
            super.initWithNiwsConfig(config);
        }
    }
}

2、重寫Ribbon的Rule

在這裏插入圖片描述
核心實現GrayRule,各微服務需要依賴此包這樣Ribbon就可以控制內部接口的訪問,在zuul中也要放置同樣的代碼三個類,zuul控制外部訪問,我們代碼的模式是@FeignClient(value = 具體的服務)如果我們只想網關的話那麼各微服務就不用了拉包了,由網關去分發所有請求,我們指向的是具體服務所以微服務需要一來到這三個類,這樣就能加載配置了,由於這些配置卸載common包裏面,common包裏面各種依賴太多了,不想讓zuul變得臃腫所以就在網關又放置了一份,當然也可以專門配置一個jar來做gray。

import com.*.*.*.GrayConstants;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.*.*.*.*.redis.JedisClusterUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import lombok.extern.slf4j.Slf4j;

/**
 * 自定義灰度發佈規則 2019-11-19 by david
 */
@Slf4j
@Service
public class GrayRule extends ZoneAvoidanceRule {

    /**
     * 在choose方法中,自定義規則,返回的Server就是具體選擇出來的服務
     *
     * @param key 服務key
     * @return 可用server
     */
    @Override
    public Server choose(Object key) {
		// 獲取負載均衡接口
        ILoadBalancer loadBalancer = this.getLoadBalancer();
		// 獲取到所有存活的服務
        List<Server> allServers = loadBalancer.getAllServers();
		// 獲取到需要路由的服務
        List<Server> serverList = this.getPredicate().getEligibleServers(allServers, key);
        log.info("[gray choose] key:{}; allServers:{}; serverList:{}", key, allServers, serverList);
		// 如果服務列表爲空則返回null	        
		if (CollectionUtils.isEmpty(serverList)) {
            log.warn("=====GrayRule choose serverList isEmpty key:{}=====", key);
            return null;
        }
        // 灰度開關,檢查是否開啓灰度服務開啓時掃描灰度列表,避免每次掃描列表增大開銷
        String switchValue = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
        if (StringUtils.isBlank(switchValue) || "0".equals(switchValue)) {
            return getRandom(serverList);
        }
        // 灰度服務列表
        final Map<String, String> grayAddress = JedisClusterUtils.hgetAll(GrayConstants.GRAY_ADDRESS);
        if (CollectionUtils.isEmpty(grayAddress)) {
            log.info("[choose] : grayAddress isEmpty return serverList:{}", serverList);
            return getRandom(serverList);
        }
        List<String> grayServers = new ArrayList<>(grayAddress.keySet());
        // 查找非灰度服務並返回
        List<Server> noGrayServerList = serverList.stream().filter(x -> !grayServers.contains(x.getHostPort())).collect(Collectors.toList());
        return noGrayServerList.isEmpty() ? null : getRandom(noGrayServerList);
    }

    /**
     * 隨機返回一個可用服務
     *
     * @param serverList 服務列表
     * @return 隨機獲取的服務
     */
    private static Server getRandom(List<Server> serverList) {
        return CollectionUtils.isEmpty(serverList) ? null : serverList.get(ThreadLocalRandom.current().nextInt(serverList.size()));
    }

}

3、定義訪問接口

定義訪問接口用於操作灰度服務,我將這些接口定義在zuul裏面了GrayController,將灰度服務放入redis的hash對象中,然後getAll如果灰度服務數量特別大,慎用!如果灰度服務忘記關閉則24小時之後自動關閉,這個根據實際情況而定,爲了做壓力測試,所以設置的過期時間比較長

import com.*.*.StringUtils;
import com.*.*.constant.GrayConstants;
import com.*.*.*.redis.JedisClusterUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("gray")
public class GrayController {

    private static final Integer GRAY_TIME_OUT = 24 * 60 * 60;
    private static final String GRAY_OPEN = "1";

    /**
     * 開啓灰度發佈開關
     */
    @GetMapping("openGray")
    public String openGray() {
        log.info("openGray start");
        JedisClusterUtils.set(GrayConstants.GRAY_SWITCH, GRAY_OPEN, GRAY_TIME_OUT);
        log.info("openGray end");
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 關閉灰度發佈開關
     */
    @GetMapping("closeGray")
    public String closeGray() {
        log.info("closeGray start");
        JedisClusterUtils.del(GrayConstants.GRAY_SWITCH);
        JedisClusterUtils.del(GrayConstants.GRAY_ADDRESS);
        log.info("closeGray end");
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 設置灰度發佈服務
     */
    @GetMapping("setGrayServer")
    public String setGrayServer(@RequestParam("grayHostPort") String grayHostPort) {
        log.info("setGrayServer start grayHostPort:{}", grayHostPort);
        String grayStatus = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
        if (StringUtils.isEmpty(grayStatus) || !GRAY_OPEN.equals(grayStatus)) {
            JedisClusterUtils.set(GrayConstants.GRAY_SWITCH, GRAY_OPEN, GRAY_TIME_OUT);
        }
        JedisClusterUtils.hset(GrayConstants.GRAY_ADDRESS, grayHostPort, GRAY_OPEN);
        JedisClusterUtils.expire(GrayConstants.GRAY_ADDRESS, GRAY_TIME_OUT);
        log.info("setGrayServer end grayHostPort:{}", grayHostPort);
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 移除灰度服務
     */
    @GetMapping("removeGrayServer")
    public String removeGrayServer(@RequestParam("grayHostPort") String grayHostPort) {
        log.info("removeGrayServer start grayHostPort:{}", grayHostPort);
        JedisClusterUtils.hdel(GrayConstants.GRAY_ADDRESS, grayHostPort);
        log.info("removeGrayServer end grayHostPort:{}", grayHostPort);
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 獲取灰度發佈狀態
     */
    @GetMapping("getGrayStatus")
    public String getGrayStatus() {
        log.info("getGrayStatus start");
        String status = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
        log.info("getGrayStatus end :{}", status);
        return GRAY_OPEN.equals(status) ? "open" : "close";
    }

    /**
     * 獲取灰度發佈中的服務
     */
    @GetMapping("getGrayServer")
    public List<String> getGrayServer() {
        log.info("getGrayServer start");
        Map<String, String> grayServer = JedisClusterUtils.hgetAll(GrayConstants.GRAY_ADDRESS);
        log.info("getGrayServer end grayServer:{}", grayServer);
        return CollectionUtils.isEmpty(grayServer) ? new ArrayList<>() : new ArrayList<>(grayServer.keySet());
    }

}

4、測試

基本開發已經完成了,後面就開始測試效果啦,測試工具使用的是JMeter,策略是挑選10個核心服務和5個非核心服務用腳本控制滾動發佈,構建啓動完成之後間歇10分鐘然後又發持續24小時,期間JMeter對這些服務中的高頻,且涉及到內部調用的接口進行壓力測試200線程/s,觀察error情況,24H 0 error。棒棒棒!由於本人太懶了,過了好久了纔來寫這篇博客,當時的JMeter截圖沒有保存,所以還是要及時記筆記,大家一起努力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章