使用Consul做服務(wù)注冊(cè)與發(fā)現(xiàn)

Consul是HashiCorp公司出品的做服務(wù)注冊(cè)與發(fā)現(xiàn)的分布式中間件。經(jīng)過(guò)初步的實(shí)踐,發(fā)現(xiàn)其特點(diǎn)有:

  • 分Server和Client兩種模式,可以搭建網(wǎng)格式架構(gòu)。
  • 全部只1個(gè)可執(zhí)行文件,部署啟動(dòng)簡(jiǎn)單。
  • Server集群使用Raft協(xié)議確保數(shù)據(jù)強(qiáng)一致性和高可用性,而所有節(jié)點(diǎn)之間通過(guò)Gossip協(xié)議共享整個(gè)集群節(jié)點(diǎn)的狀態(tài)。
  • Server節(jié)點(diǎn)存服務(wù)數(shù)據(jù)、而Client節(jié)點(diǎn)是無(wú)狀態(tài)的負(fù)責(zé)通過(guò)RPC調(diào)用轉(zhuǎn)發(fā)服務(wù)查詢請(qǐng)求到Server節(jié)點(diǎn)。
  • 客戶端可以緩存服務(wù)數(shù)據(jù)、不用每次都查詢Client節(jié)點(diǎn)而對(duì)Server節(jié)點(diǎn)產(chǎn)生壓力。

下面介紹一下完整的一次初步實(shí)踐過(guò)程。

Consul網(wǎng)格搭建

3臺(tái)虛機(jī),部署如下:
192.20.33.54 啟動(dòng)1個(gè)Consul server:

./consul agent -server -bootstrap-expect=1 -data-dir=/tmp/consul -node=consul-server -bind 192.20.33.54 -client=0.0.0.0 -ui

192.20.33.53 啟動(dòng)1個(gè)Consul client , 啟動(dòng)provider應(yīng)用:

./consul agent -data-dir=/tmp/consul -node=consul-client53 -bind 192.20.33.53 -client 0.0.0.0 -join 192.20.33.54

192.20.33.55 啟動(dòng)1個(gè)Consul client , 啟動(dòng)consumer應(yīng)用:

./consul agent -data-dir=/tmp/consul -node=consul-client55 -bind 192.20.33.55 -client 0.0.0.0 -join 192.20.33.54

說(shuō)明:上面為了方便是啟動(dòng)了1個(gè)Consul server + 2個(gè)Consul client,但生產(chǎn)環(huán)境要用3 + N的方案,3個(gè)Consul server組成集群,N個(gè)節(jié)點(diǎn)用于部署業(yè)務(wù)服務(wù)、且每個(gè)節(jié)點(diǎn)上面都啟動(dòng)1個(gè)Consul client,使用-join加入Consul網(wǎng)格。

服務(wù)提供者和消費(fèi)者應(yīng)用

provider和consumer是兩個(gè)springboot應(yīng)用,Spring Boot版本2.1.13.RELEASE,Spring Cloud版本Greenwich.SR6

build.gradle文件:

plugins {
    id 'org.springframework.boot' version '2.1.13.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}


version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
 
repositories {
    mavenLocal()
    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
}

dependencyManagement {
     imports {
         mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR6'
     }
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-test')

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
    
    compile group: 'org.apache.httpcomponents', name: 'fluent-hc', version:'4.5.3'
    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.73'
    implementation 'org.projectlombok:lombok:1.18.22'
    annotationProcessor('org.projectlombok:lombok') 
}

主要依賴spring-cloud-starter-consul-discovery,Spring抽象封裝了一層通用接口、即所謂LoadBalancerClient系列,實(shí)現(xiàn)層使用的是基于netflix Ribbon開(kāi)發(fā)的Consul客戶端負(fù)責(zé)均衡組件。

服務(wù)提供者引入上述依賴后,啟動(dòng)時(shí)自動(dòng)通過(guò)本機(jī)的Consul Client注冊(cè)到Consul網(wǎng)格。(經(jīng)驗(yàn)證不需要添加@EnableDiscoveryClient
服務(wù)消費(fèi)者的服務(wù)發(fā)現(xiàn)依靠進(jìn)程內(nèi)的客戶端負(fù)載均衡組件、自動(dòng)發(fā)現(xiàn)并選擇健康狀態(tài)的服務(wù)實(shí)例。啟動(dòng)類(lèi)加上@EnableDiscoveryClient注解,代碼里對(duì)RestTemplate添加@LoadBalanced注解即可實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)和負(fù)載均衡調(diào)用?;蛘甙凑杖缦路绞?,可以不添加@EnableDiscoveryClient@LoadBalanced注解。

@Slf4j
@Component
public class LbRestTemplate {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Autowired
    private LoadBalancerClient loadBalancer;
    
    @PostConstruct
    public void initLbContext() {
        ServiceInstance instance = loadBalancer.choose("DummyService");
        log.info("與Consul Client建立通信...");
    }
    
    public LbRestTemplate(RestTemplate restTemplate, LoadBalancerClient loadBalancer){
        this.restTemplate = restTemplate;
        this.loadBalancer = loadBalancer;
    }
    
    /**
     * @param serviceName 對(duì)端的應(yīng)用名, 例如user
     * @param interfaceUrl 對(duì)端應(yīng)用的接口地址(包括servlet-contextPath), 例如/user/customer/getUserInfo
     * @param clazz 接口返回類(lèi)型
     * */
    public <T extends Object> T getForBean(String serviceName, String interfaceUrl, Class<T> clazz){
        ServiceInstance instance = loadBalancer.choose(serviceName);
        if(null == instance)
            throw new RuntimeException(serviceName + "當(dāng)前無(wú)可用節(jié)點(diǎn)");
        URI serviceAddress = instance.getUri();
        String serviceFullUrl = serviceAddress + "/" + interfaceUrl;

        log.info("serviceFullUrl:"+serviceFullUrl);

        return restTemplate.getForObject(serviceFullUrl, clazz);
    }
    
    /**
     * @param serviceName 對(duì)端的應(yīng)用名, 例如user
     * @param interfaceUrl 對(duì)端應(yīng)用的接口地址(包括servlet-contextPath)
     * @param clazz 接口返回類(lèi)型
     * */
    public <T extends Object> T postForBean(String serviceName, String interfaceUrl, Object requestBody, Class<T> clazz){
        ServiceInstance instance = loadBalancer.choose(serviceName);
        if(null == instance)
            throw new RuntimeException(serviceName + "當(dāng)前無(wú)可用節(jié)點(diǎn)");
        URI serviceAddress = instance.getUri();

        String serviceFullUrl = serviceAddress + "/" + interfaceUrl;

        log.info("serviceFullUrl:"+serviceFullUrl);

        return restTemplate.postForObject(serviceFullUrl, requestBody, clazz);

    }

}
@Data
@Configuration
@ConfigurationProperties(prefix="resttemplate")
public class RestTemplateProperties {
    
    private Integer maxTotal = 200; //總連接數(shù)
    private Integer defaultMaxPerRoute = 100; //單個(gè)路由最大連接數(shù)
    private Integer connectTimeout = 2000;      // 與遠(yuǎn)程服務(wù)器建立連接的時(shí)間ms
    private Integer connectionRequestTimeout = 2000;    // 從connection manager獲取連接的超時(shí)時(shí)間ms
    private Integer socketTimeout = 5000;   // 建立連接之后,等待遠(yuǎn)程服務(wù)器返回?cái)?shù)據(jù)的時(shí)間,也就是兩個(gè)數(shù)據(jù)包(請(qǐng)求包和響應(yīng)包)之間不活動(dòng)的最大時(shí)間ms
    private Integer validateAfterInactivity = 5000; //連接進(jìn)入不活動(dòng)狀態(tài)多久之后再取得的時(shí)候進(jìn)行有效性校驗(yàn)
    private Integer clientDefaultKeepAliveTime = 60000; //客戶端默認(rèn)keep-alive時(shí)間ms
    private Integer maxIdleTime = 30000;    //連接最大空閑時(shí)間ms
    
}
@Slf4j
@Configuration
public class RestTemplateConfig {
    
    @Autowired
    private RestTemplateProperties restTemplateProperties;
    
    @Bean
    public RestTemplate restTemplate() {
        log.info("restTemplate初始化...");
        return new RestTemplate(httpRequestFactory());
    }

    @Bean
    public ClientHttpRequestFactory httpRequestFactory() {
        return new HttpComponentsClientHttpRequestFactory(httpClient());
    }

    @Bean
    public HttpClient httpClient() {
        //支持http和https
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory()).build();
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(restTemplateProperties.getMaxTotal());
        connectionManager.setDefaultMaxPerRoute(restTemplateProperties.getDefaultMaxPerRoute());
        connectionManager.setValidateAfterInactivity(restTemplateProperties.getValidateAfterInactivity());
        
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(restTemplateProperties.getConnectTimeout())
                .setSocketTimeout(restTemplateProperties.getSocketTimeout())
                .setConnectionRequestTimeout(restTemplateProperties.getConnectionRequestTimeout()).build();

        // keep-alive策略
        ConnectionKeepAliveStrategy keepAliveStrategy = new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                // 如果服務(wù)端response返回了Keep-Alive header
                HeaderElementIterator it = new BasicHeaderElementIterator(
                        response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && param.equalsIgnoreCase("timeout")) {
                        try {
                            return Long.parseLong(value) * 1000;
                        } catch (NumberFormatException ignore) {
                        }
                    }
                }
                // 否則設(shè)置客戶端默認(rèn)keep-alive超時(shí)時(shí)間
                return restTemplateProperties.getClientDefaultKeepAliveTime();
            }
        };

        return HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                .evictExpiredConnections()  //設(shè)置后臺(tái)線程剔除失效連接和空閑超時(shí)連接
                .evictIdleConnections(restTemplateProperties.getMaxIdleTime(), TimeUnit.MILLISECONDS)
                .setKeepAliveStrategy(keepAliveStrategy).build();
        }
}

主要是在RestTemplate基礎(chǔ)上封裝了一個(gè)LbRestTemlate:在RestTemplate調(diào)用之前先用LoadBalancerClient獲取調(diào)用的服務(wù)地址。然后對(duì)RestTemplate的實(shí)現(xiàn)所用的Apache HttpClient的參數(shù)進(jìn)行了配置,包括連接池參數(shù),鏈接Keep-Alive策略,失效連接檢測(cè)等等。

關(guān)于應(yīng)用配置

Spring Cloud是提供了如下配置項(xiàng),用于配置應(yīng)用連接Consul的地址端口、以及應(yīng)用在Consul中識(shí)別的服務(wù)名的:

#服務(wù)提供者
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.serviceName=provider
#服務(wù)消費(fèi)者
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.register=false

但是按照3+N這種網(wǎng)格架構(gòu)來(lái)說(shuō),只要約定節(jié)點(diǎn)本地的Consul Client端口保持默認(rèn)的8500,那么其實(shí)就不需要配置spring.cloud.consul.hostspring.cloud.consul.port了。另外spring.cloud.consul.discovery.serviceName也可以不配置,默認(rèn)會(huì)去取spring.application.name,而后者我們約定應(yīng)用都必須配置。
也就是說(shuō),應(yīng)用不需要額外的服務(wù)注冊(cè)與發(fā)現(xiàn)相關(guān)的配置。真正做到了應(yīng)用對(duì)所在部署環(huán)境無(wú)感,這一點(diǎn)對(duì)服務(wù)網(wǎng)格化和云原生架構(gòu)思想尤為重要。

Dome演示

測(cè)試接口:

curl http://192.20.33.55:8090/consumer/frontend/test

管理控制臺(tái):
http://192.20.33.54:8500/ui/dc1/services

關(guān)于Raft協(xié)議與Gossip協(xié)議在Consul中的應(yīng)用
  • Raft協(xié)議負(fù)責(zé)Consul server節(jié)點(diǎn)的選主、以及寫(xiě)日志復(fù)制。

1、節(jié)點(diǎn)從follower變candidate時(shí)向其他節(jié)點(diǎn)進(jìn)行拉票,獲得超過(guò)半數(shù)節(jié)點(diǎn)投票的節(jié)點(diǎn)當(dāng)選主節(jié)點(diǎn)。

2、主節(jié)點(diǎn)負(fù)責(zé)集群的寫(xiě)請(qǐng)求處理、先寫(xiě)本地半提交、通知從節(jié)點(diǎn)進(jìn)行半提交、超過(guò)半數(shù)從節(jié)點(diǎn)回復(fù)后主節(jié)點(diǎn)提交本地并回復(fù)客戶端成功,主節(jié)點(diǎn)心跳通知從節(jié)點(diǎn)從半提交進(jìn)行提交。

  • Gossip協(xié)議負(fù)責(zé)Consul集群中節(jié)點(diǎn)之間的信息擴(kuò)散。注意,不包括服務(wù)列表信息,只包含節(jié)點(diǎn)的狀態(tài)信息。

1、新的consul節(jié)點(diǎn)加入或退出

2、consul節(jié)點(diǎn)上有新的服務(wù)注冊(cè)或健康狀態(tài)發(fā)生變化

3、Consul Server集群的狀態(tài)發(fā)生改變了,比如新的選主。

  • Raft是強(qiáng)一致性協(xié)議,Gossip是最終一致性協(xié)議

有關(guān)如何使用Consul搭建服務(wù)注冊(cè)與服務(wù)發(fā)現(xiàn)就先介紹到這里,Consul的功能比較豐富、玩法很靈活,比如CDN服務(wù)發(fā)現(xiàn)、多數(shù)據(jù)中心、KV存儲(chǔ)、與其他負(fù)載均衡中間件比如Nginx等集成、搭配Envoy實(shí)現(xiàn)真正的Service Mesh等等。下次再介紹。

參考

使用Consul做服務(wù)發(fā)現(xiàn)的若干姿勢(shì) | 波斯馬 (bossma.cn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容