Consul是HashiCorp公司出品的做服务注册与发现的分布式中间件。经过初步的实践,发现其特点有:
- 分Server和Client两种模式,可以搭建网格式架构。
- 全部只1个可执行文件,部署启动简单。
- Server集群使用Raft协议确保数据强一致性和高可用性,而所有节点之间通过Gossip协议共享整个集群节点的状态。
- Server节点存服务数据、而Client节点是无状态的负责通过RPC调用转发服务查询请求到Server节点。
- 客户端可以缓存服务数据、不用每次都查询Client节点而对Server节点产生压力。
下面介绍一下完整的一次初步实践过程。
Consul网格搭建
3台虚机,部署如下:
192.20.33.54 启动1个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 启动1个Consul client , 启动provider应用:
./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 启动1个Consul client , 启动consumer应用:
./consul agent -data-dir=/tmp/consul -node=consul-client55 -bind 192.20.33.55 -client 0.0.0.0 -join 192.20.33.54
说明:上面为了方便是启动了1个Consul server + 2个Consul client,但生产环境要用3 + N的方案,3个Consul server组成集群,N个节点用于部署业务服务、且每个节点上面都启动1个Consul client,使用-join加入Consul网格。
服务提供者和消费者应用
provider和consumer是两个springboot应用,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系列,实现层使用的是基于netflix Ribbon开发的Consul客户端负责均衡组件。
服务提供者引入上述依赖后,启动时自动通过本机的Consul Client注册到Consul网格。(经验证不需要添加@EnableDiscoveryClient
)
服务消费者的服务发现依靠进程内的客户端负载均衡组件、自动发现并选择健康状态的服务实例。启动类加上@EnableDiscoveryClient
注解,代码里对RestTemplate添加@LoadBalanced
注解即可实现服务发现和负载均衡调用。或者按照如下方式,可以不添加@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 对端的应用名, 例如user
* @param interfaceUrl 对端应用的接口地址(包括servlet-contextPath), 例如/user/customer/getUserInfo
* @param clazz 接口返回类型
* */
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 + "当前无可用节点");
URI serviceAddress = instance.getUri();
String serviceFullUrl = serviceAddress + "/" + interfaceUrl;
log.info("serviceFullUrl:"+serviceFullUrl);
return restTemplate.getForObject(serviceFullUrl, clazz);
}
/**
* @param serviceName 对端的应用名, 例如user
* @param interfaceUrl 对端应用的接口地址(包括servlet-contextPath)
* @param clazz 接口返回类型
* */
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 + "当前无可用节点");
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; //总连接数
private Integer defaultMaxPerRoute = 100; //单个路由最大连接数
private Integer connectTimeout = 2000; // 与远程服务器建立连接的时间ms
private Integer connectionRequestTimeout = 2000; // 从connection manager获取连接的超时时间ms
private Integer socketTimeout = 5000; // 建立连接之后,等待远程服务器返回数据的时间,也就是两个数据包(请求包和响应包)之间不活动的最大时间ms
private Integer validateAfterInactivity = 5000; //连接进入不活动状态多久之后再取得的时候进行有效性校验
private Integer clientDefaultKeepAliveTime = 60000; //客户端默认keep-alive时间ms
private Integer maxIdleTime = 30000; //连接最大空闲时间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) {
// 如果服务端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) {
}
}
}
// 否则设置客户端默认keep-alive超时时间
return restTemplateProperties.getClientDefaultKeepAliveTime();
}
};
return HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.evictExpiredConnections() //设置后台线程剔除失效连接和空闲超时连接
.evictIdleConnections(restTemplateProperties.getMaxIdleTime(), TimeUnit.MILLISECONDS)
.setKeepAliveStrategy(keepAliveStrategy).build();
}
}
主要是在RestTemplate基础上封装了一个LbRestTemlate:在RestTemplate调用之前先用LoadBalancerClient获取调用的服务地址。然后对RestTemplate的实现所用的Apache HttpClient的参数进行了配置,包括连接池参数,链接Keep-Alive策略,失效连接检测等等。
关于应用配置
Spring Cloud是提供了如下配置项,用于配置应用连接Consul的地址端口、以及应用在Consul中识别的服务名的:
#服务提供者
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.serviceName=provider
#服务消费者
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.register=false
但是按照3+N这种网格架构来说,只要约定节点本地的Consul Client端口保持默认的8500,那么其实就不需要配置spring.cloud.consul.host
和spring.cloud.consul.port
了。另外spring.cloud.consul.discovery.serviceName
也可以不配置,默认会去取spring.application.name
,而后者我们约定应用都必须配置。
也就是说,应用不需要额外的服务注册与发现相关的配置。真正做到了应用对所在部署环境无感,这一点对服务网格化和云原生架构思想尤为重要。
Dome演示
测试接口:
curl http://192.20.33.55:8090/consumer/frontend/test
管理控制台:
http://192.20.33.54:8500/ui/dc1/services

关于Raft协议与Gossip协议在Consul中的应用
- Raft协议负责Consul server节点的选主、以及写日志复制。
1、节点从follower变candidate时向其他节点进行拉票,获得超过半数节点投票的节点当选主节点。
2、主节点负责集群的写请求处理、先写本地半提交、通知从节点进行半提交、超过半数从节点回复后主节点提交本地并回复客户端成功,主节点心跳通知从节点从半提交进行提交。
- Gossip协议负责Consul集群中节点之间的信息扩散。注意,不包括服务列表信息,只包含节点的状态信息。
1、新的consul节点加入或退出
2、consul节点上有新的服务注册或健康状态发生变化
3、Consul Server集群的状态发生改变了,比如新的选主。
- Raft是强一致性协议,Gossip是最终一致性协议
有关如何使用Consul搭建服务注册与服务发现就先介绍到这里,Consul的功能比较丰富、玩法很灵活,比如CDN服务发现、多数据中心、KV存储、与其他负载均衡中间件比如Nginx等集成、搭配Envoy实现真正的Service Mesh等等。下次再介绍。
网友评论