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.host和spring.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等等。下次再介紹。