美文网首页分布式吃瓜SRE磐石Base分布式
使用Consul做服务注册与发现

使用Consul做服务注册与发现

作者: 肥兔子爱豆畜子 | 来源:发表于2022-01-15 18:22 被阅读0次

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.hostspring.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等等。下次再介绍。

参考

使用Consul做服务发现的若干姿势 | 波斯马 (bossma.cn)

相关文章

网友评论

    本文标题:使用Consul做服务注册与发现

    本文链接:https://www.haomeiwen.com/subject/xvgthrtx.html