原文地址
前言
看了相关的资料,我们选择consul作为注册中心,不用nacos的原因是nacos太大了,不用eureka的原因是已经停止维护,其他的Istio,Linkerd等都是主要基于Kubernetes的,所以我们这里选择consul,参考1,参考2,参考3
部署
基本部署
首先,根据官方对于架构的介绍,我们至少需要一个server,甚至可以不需要client,也可以完成功能,我们这里使用单个server的方案。虽然多个server可用保证可用性,参考官网文档的Consensus Protocol部分,我们应当使用3个server节点去部署。但是这个对于单服务这种部署是没有意义的。因为单服务中,我们自己的服务只能注册到其中一个服务器,那么如果这个服务器挂了,我们在这个服务器上所有注册的服务都挂了。他的容灾策略从我目前所看到的文档总结来说,如果不增加一层自主转发的情况下(比如nginx?我没有具体尝试,不是特别确定是否可行),是保证不同服务器的数据互通以及服务器对外输出的高可用,但是起码对于spring-cloud而言,单个服务实例只能注册到一个服务器上,虽然他能发现本集群的其他服务器上注册的服务,但是如果该服务器一旦挂掉,上面注册的服务实例也会挂掉,所以在单台服务器下这种部署是没有意义的,这里可以参考Registering Multiple Same-Host Services和A question about the registration of the consul cluster。这里官方Docker部署文档,并没有进行相关说明,甚至其他其余的两个服务都没有开放注册端口,非常的误导人
这里也给出多server和client的方案以防万一
version: "3.7"
services:
consul-server-y:
image: 'hashicorp/consul:latest'
restart: always
container_name: 'consul-server-y'
hostname: 'consul-server-y'
ports:
- '9527:8500'
- '19527:8600/tcp'
- '19527:8600/udp'
volumes:
- ./server-y.json:/consul/config/server-y.json:ro
- ./certs/:/consul/config/certs/:ro
command: "agent -bootstrap-expect=3"
consul-server-x:
image: 'hashicorp/consul:latest'
restart: always
container_name: 'consul-server-x'
hostname: 'consul-server-x'
ports:
- '9528:8500'
- '19528:8600/tcp'
- '19528:8600/udp'
volumes:
- ./server-x.json:/consul/config/server-x.json:ro
- ./certs/:/consul/config/certs/:ro
command: "agent -bootstrap-expect=3"
consul-server-z:
image: 'hashicorp/consul:latest'
restart: always
container_name: 'consul-server-z'
hostname: 'consul-server-z'
ports:
- '9529:8500'
- '19529:8600/tcp'
- '19529:8600/udp'
volumes:
- ./server-z.json:/consul/config/server-z.json:ro
- ./certs/:/consul/config/certs/:ro
command: "agent -bootstrap-expect=3"
consul-client:
image: 'hashicorp/consul:latest'
container_name: consul-client
restart: always
volumes:
- ./client.json:/consul/config/client.json:ro
- ./certs/:/consul/config/certs/:ro
command: "agent"
服务端配置文件,三个配置文件都差不多,照葫芦画瓢稍微改下就行
{
"node_name": "consul-server-x",
"server": true,
"ui_config": {
"enabled": true
},
"retry_join": ["consul-server-y", "consul-server-z"],
"data_dir": "/consul/data",
"addresses": {
"http": "0.0.0.0"
},
"encrypt": "aPuGh+5UDskRAbkLaXRzFoSOcSM+5vAK+NEYOWHJH7w=",
"verify_incoming": false,
"verify_outgoing": false,
"verify_server_hostname": false
}
客户端配置文件
{
"node_name": "consul-client",
"data_dir": "/consul/data",
"retry_join": ["consul-server-y", "consul-server-x", "consul-server-z"],
"encrypt": "aPuGh+5UDskRAbkLaXRzFoSOcSM+5vAK+NEYOWHJH7w=",
"verify_incoming": false,
"verify_outgoing": false,
"verify_server_hostname": false
}
这里如果需要保证高可用,服务需要启动三个实例,分别注册到9527,9528,9529端口,这样才能保证在其中一个server挂掉的情况,我们自己服务的高可用,或者只启动一个实例使用中间层做转发,但是如果这样consul服务的功能就看起来不是很完整了
ACL
Access Control Lists,需要配置注册中心的访问权限,首先需要添加访问权限在server的json配置文件中增加
{
"acl":{
"enabled": true,
"default_policy":"deny",
"enable_token_persistence": true
}
}
- enabled 是否开启ACL
- default_policy 自定义权限需要将其设置为deny
- enable_token_persistence token持久化到磁盘
然后进入集群内任意server中,执行命令
consul acl bootstrap
获取SecretID,然后在登录框内输入密钥id即可
由于我们这里修改了默认的策略,所以此时获得的SecretID只能用来登录,并不能用来注册,我们需要配置角色和权限来生成可以注册的密钥。当然如果简单使用,不设置default_policy,使用默认allow然后拿生成的SecretID就可以直接注册了
我们这里登录ui进行先新建策略,可以将策略定在node(server/client)上,也可以定在服务或者配置上,参考官方策略配置,我们这里需要服务注册的token所以定在服务上,这里如果需要完成服务的注册和发现,还需要对于node节点有读权限,否则无法发现其他服务
node_prefix "" {
policy = "read"
}
service_prefix "" {
policy = "write"
}
更多配置参考官方文档
然后再去角色页面新建角色,配置角色所拥有的策略选择刚才新建的策略,最后新建token选择刚才的角色即可(当然也可以不建角色角色直接配置策略在token上),至此我们获得一个可以用于服务注册的token
Gossip encryption
我们需要在集群中配置相同的密钥encrypt才能保证集群通信的可用性,我们现在需要生成自己的密钥,在容器内执行命令
consul keygen
#ABCDEFGHIKOPF123456781234567=
获取密钥,然后在各个server中添加配置文件即可
{
"encrypt": "ABCDEFGHIKOPF123456781234567=",
"encrypt_verify_incoming": true,
"encrypt_verify_outgoing": true
}
mTLS
参考官方文档,进入任意server执行命令
consul tls ca create
consul tls cert create -server -dc dc1
然后在宿主机将文件拷贝出来,放入之前设置的docker映射卷目录中
docker cp e36:dc1-server-consul-0-key.pem ./
docker cp e36:dc1-server-consul-0.pem ./
docker cp e36:consul-agent-ca.pem ./
这里需要注意拷贝出来的文件的权限问题,否则docker容器可能没有读权限
chmod 644 dc1-server-consul-0-key.pem
最后增加mTLS相关认证配置在server和client配置文件中
{
"verify_incoming": true,
"verify_outgoing": true,
"verify_server_hostname": true,
"ca_file": "/consul/config/certs/consul-agent-ca.pem",
"cert_file": "/consul/config/certs/dc1-server-consul-0.pem",
"key_file": "/consul/config/certs/dc1-server-consul-0-key.pem"
}
HTTPS
看下SSL和mTLS的关系,具体配置这里就不赘述了,比较简单也不是必须的。运营商申请子域名,配ssl证书,最后配一下nginx,然后关闭测试端口,这样我们基本的生产环境的的consul就配置完成了
服务的注册和发现
我们这里选择使用Spring Cloud的项目进行服务注册和发现,这里核心参考spring-cloud-consul官方文档
我们使用openfeign进行服务间调用,故而这里引用的核心依赖有
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--有一部分文档这里引用的是spring-cloud-starter-consul-all,不建议这么做,会导致部分配置属性如enabled功能失效
参考 https://github.com/spring-cloud/spring-cloud-consul/issues/309-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--识别bootstrap文件了-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--如果作为基础服务就不需要openfeign了-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
首先启动类增加注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
添加consul相关配置
spring:
cloud:
consul:
enabled: true
#你的域名或者你的服务器ip地址
host: your_domin_or_your_ip_address
#这里如果使用nginx转发则为80或者443端口,否则为你的docker-compose中配置的端口,记得配置防火墙
port: 443
discovery:
#如果有需要这里需要配置不同实例标识
instance-id: ${spring.application.name}
#如果无法从consul地址ping到service地址,那么consul的健康检测是无法通过的,需要持续由服务去ping到consul来保证服务的可用性
#但是目前版本心跳的ping并不会带上token需要写一个切面去处理
#正式环境不建议这么写,consul+springcloud目前我的使用情况是,使用心跳注册在服务关闭时候无法检测到服务关闭仍然在active状态原因不明
#所以以服务稳定为前提最好还是使用host+port+healthPath的方式在证书环境注册
heartbeat:
enabled: true
#在ACL步骤配置的可以用于服务注册的token
acl-token: your_token
#如果这里nginx配置了证书,那么我们需要HTTPS协议,默认HTTP不用设置
scheme: HTTPS
如果服务死亡,springcloudconsul在目前的4.0.2的版本还不支持consul的deregister_critical_service_after字段去自动取消注册,起码在我的环境中health-check-critical-timeout等这种字段是不能如期望生效的
/**
* Timeout to deregister services critical for longer than timeout (e.g. 30m).
* Requires consul version 7.x or higher.
*/
private String healthCheckCriticalTimeout;
导致死掉的服务仍然会在平衡策略中被请求到,目前没有比较好的处理的方法,只能手动删除,或者写定时任务脚本,参考Consul not deregistering zombie services和Consul deregister ‘failing’ services,以及其他参考1,参考2,参考3,这里摘录脚本(不推荐写脚本,有这功夫不如手写注册模块)
leader="$(curl http://ONE-OF-YOUR-CLUSTER:8500/v1/status/leader | sed
's/:8300//' | sed 's/"//g')"
while :
do
serviceID="$(curl http://$leader:8500/v1/health/state/critical | ./jq '.[0].ServiceID' | sed 's/"//g')"
node="$(curl http://$leader:8500/v1/health/state/critical | ./jq '.[0].Node' | sed 's/"//g')"
echo "serviceID=$serviceID, node=$node"
size=${#serviceID}
echo "size=$size"
if [ $size -ge 7 ]; then
curl --request PUT http://$node:8500/v1/agent/service/deregister/$serviceID
else
break
fi
done
curl http://$leader:8500/v1/health/state/critical
还是聊一句,spring-cloud-consul的维护进度确实太慢了,看了一些issue基本有问题就是欢迎PR,所以有不忙的兄弟可以去PR,如果项目比较重要的目前阶段还是建议手写注册模块,使用他的配置去注册坑实在是有点多
如果由consul可以ping到service地址且是通过hostname可以调用到,那么则不需要任何连接配置,如果不是通过host则需要在discovery中提示service的地址,参考此问题的说明。如果不能ping到,那么为了维持服务在consul的可用标志,我们需要让服务不断给consul发心跳,这种情况下由于consul本身的问题,在目前版本并不会带上acl-token,我们这里给出一个切面处理,代码来源,该问题已经在2023.3.29的spring-cloud-starter-consul-discovery:4.0.2版本中修复,不需要再增加切面
@Aspect
@Configuration
@RequiredArgsConstructor
@ConditionalOnConsulEnabled
@ConditionalOnProperty("spring.cloud.consul.discovery.acl-token")
@Slf4j
public class ConsulClientAspect {
private final ConsulDiscoveryProperties consulDiscoveryProperties;
@PostConstruct
public void init() {
log.info("Hooking ConsulClient.agentCheckPass(String) calls to enforce acl token passing");
}
/**
* Trap calls made to {@link ConsulClient#agentCheckPass(String)} and call the overridden method variant with ACL
* token
*/
@SneakyThrows
@Around("execution (* com.ecwid.consul.v1.ConsulClient.agentCheckPass(String))")
public Response<Void> trapAgentCheckPass(final ProceedingJoinPoint joinPoint) {
final String checkId = (String) joinPoint.getArgs()[0];
final ConsulClient client = (ConsulClient) joinPoint.getThis();
return client.agentCheckPass(checkId, null, this.consulDiscoveryProperties.getAclToken());
}
}