微服务与Kubernetes容器云的边界的考量,其实就是思考微服务要不要跨多个Kubernetes集群的问题。比较理想的情况是微服务和Kubernetes完全对齐,也就是一套微服务运行在一套Kubernetes集群上。在这种情况下,微服务、配置中心+注册中心都在相同的Kubernetes集群中。当微服务指向配置中心时,写配置中心的ServiceName即可,网络I/O路径较短。否则,需要通过Kubernetes Ingress访问注册中心(如果容器云的SDN采用overlay模式),网络延迟将会较大。这在微服务数量较多、变更较频繁的时候更为明显。
但是,如果微服务跨多个Kubernetes,会有什么问题呢?
我们先看两种实现方式。
1)配置+注册中心在一个Kubernetes集群上:如果Kubernetes集群的SDN用的是underlay网络,那么其他Kubernetes集群注册的时候,由于其Pod IP和宿主机IP在同一个网络平面,使得注册中心能够准确识别到Pod的IP。
这种方式的弊端体现在如下三个方面。
2)配置+注册中心不在一个Kubernetes集群上:如果Kubernetes集群的SDN方案用的是overlay网络,那么其他Kubernetes集群注册的时候,由于Pod IP和宿主机IP不在同一个网络平面,导致注册中心不能准确识别Pod的IP,只能识别到Pod所在Kubernetes宿主机的IP(Pod以SNAT的方式访问集群外部)。想要解决这个问题,可以考虑使用Pod的多网络平面,也就是给Pod增加第二个虚拟网卡,挂载数据中心到同一个网络平面的IP。这种方式类似macvlan、ipvlan,不用再单独配置DNS,但弊端是当宿主机上启动的macvlan数量较多时,网卡性能会下降。
以上两种实现方式各有优劣势。笔者看来,如果Spring Cloud的边界远大于一个Kubernetes边界,想让一套Spring Cloud分布在很多个Kubernetes集群时,最好把微服务配置中心和注册中心从Kubernetes集群中独立出来,放在虚拟机或者物理机上。这样做的好处是让这个配置+注册中心离所有Kubernetes集群网络都比较近。而且,在虚拟机或者物理机上部署配置+注册中心,当需要注册微服务的时候,也不必再经过类似Ingress的环节,性能也会得到提升。此外,我们可以针对独立的配置+注册中心做高可用或者容灾方案。
接下来,我们介绍如何选择微服务注册中心和配置中心。
注册中心本质就是一个Query函数,即Si=F(ServiceName)。ServiceName为查询服务参数,Si为服务可用的列表(IP:Port)。
为了方便读者理解服务注册,我们参照图2-25进行介绍,将ServiceA的三个实例注册到注册中心。
图2-25 三个实例注册到注册中心
1)服务提供方将三个实例注册到注册中心。
2)服务调用方想要调用ServiceA,通过ServiceName去注册中心查询。然后注册中心通过Si=F(ServiceName)查询出服务的IP:Port列表。
3)服务调用方通过IP:Port列表去调用服务。
接下来,我们考虑一个问题:对于ServiceA来说,如果在注册中心注册的时候,只成功注册两个实例,那么是否应该允许服务调用方访问Service的两个实例?在真实的微服务中,我们一定希望服务调用方能访问ServiceA注册成功的两个实例,而不是必须等三个实例都注册成功后才能被访问。
此外,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性。
如图2-26所示,我们将服务注册中心集群中的三个实例分别部署到三个机房。每个机房各有两个微服务。如果机房3的网络出现问题,不能与机房1和机房2进行通信,结果会怎样?
图2-26 机房3的网络出现问题
如果是强一致性的注册中心(CAP模型中的CP模型),那么机房3中的实例3由于是少数节点,将会被终止运行。结果是,不仅ServerE和ServiceF不能访问机房1和机房2的服务,这两个服务之间的访问也会出问题。
那么,针对微服务的注册中心,我们如何选择?有3个思路。
关于应用级注册中心,我们选取几个主流的开源方案进行对比,如表2-12所示。整体而言,针对Java类应用,Nacos作为应用级注册中心具有很大的优势。
表2-12 注册中心方案对比
从表2-12可以看到,ZooKeeper、etcd、Consul都是CP模型。而etcd和ZooKeeper都不支持跨数据中心部署。因此,我们在选择微服务的服务注册中心时,可以选择Nacos。
使用应用级注册中心的优缺点如下。
具体的,使用应用级注册中心,需要考虑Pod的服务注册实现。
接下来我们看看平台侧的服务注册中心。
如果程序员决定用Kubernetes做服务发现,实现不同服务之间的调用,那么就需要使用Kubernetes的Service名称。Service名称是可以固定的。
Kubernetes/OpenShift中Service有短名和长名两种。以图2-27为例,jws-app就是Service的短名,Service的长名的格式是<sevrvice_name>.<namespace>.svc.cluser.local,例如jws-app.web.svc.cluser.local。Service短名可以自动补充成长名,由OpenShift中的DNS实现,具体将在后面介绍。
图2-27 Service名称
如果在两个不同的Namespace中有两个相同的Service短名,微服务调用是否会出现混乱?程序员的代码里是否要写Service全名?
首先,从容器云集群管理员的角度来看,对于所有项目,例如几十个或者更多,会觉得在不同Namespace中存在相同的Service短名是可能的(比如Namespace A中有名为acat的Service,Namespace B中也有名为acat的Service)。但从程序员的角度来看,他只是容器云的使用者,只拥有自己负责的Namespace的管理权,不能访问其他Namespace。而且绝大多数情况下,同一个业务项目的微服务一般会运行在同一个Namespace中,如果使用短名称(只写Service名称),则默认会自动补全成当前Namespace的FQDN。只有在跨Namespace调用的时候才必须写全名。
所以,如果程序员写的程序用到了Service名称,那么,真正进行应用的Pod之间的通信时,也必然会以Service名称去查找。通过Service名称解析为Service ClusterIP,然后经过Kube-proxy(默认为iptables模式)的负载均衡设备最终选择一个实际的Pod IP。找到Pod IP之后,接下来就会进行实际的数据交换,与Service并无关联。
使用平台注册中心的优缺点如下。
注册中心的整体选择思路主要从三个维度考量:应用是否跨开发语言,微服务的边界是否大于Kubernetes集群,以及是否限定应用的Service名称。
下面我们看5种情况。
1)应用跨语言,微服务边界不大于一个Kubernetes集群,不限定应用的Service名称:使用Kubernetes平台的etcd。
2)应用跨语言,微服务边界大于一个Kubernetes集群,不限定应用的Service名称:使用应用级注册中心,而且每种语言都需要设置自己的注册中心。
3)应用不跨开发语言,微服务不大于一个Kubernetes边界,不限定应用的Service名称:使用Kubernetes平台的etcd。
4)应用不跨开发语言,微服务大于一个Kubernetes边界,不限定应用的Service名称:使用一个应用级注册中心。
5)限定应用的Service名称:使用应用级注册中心。
配置中心存储的是独立于应用的只读变量。除此之外,配置中心还需要有权限控制,并且可以进行多个不同集群的配置管理。
与注册中心一样,配置中心同样有以下3个选择思路。
对于平台侧的配置中心,Kubernetes/OpenShift默认的配置管理是ConfigMap,即通过ConfigMap方式给应用注册配置。ConfigMap的访问权限由Kubernetes/OpenShift自身的RBAC提供。
应用级配置中心如表2-13所示。整体而言,针对Java类应用,Apollo作为应用级别配置中心具有很大的优势。
表2-13 注册中心方案选择
配置中心的整体选择思路主要从两个维度考量:应用是否跨开发语言以及微服务的边界是否大于Kubernetes集群。
下面我们看4种情况。
1)应用跨语言,微服务边界不大于一个Kubernetes集群:使用Kubernetes平台的ConfigMap。
2)应用跨语言,微服务边界大于一个Kubernetes集群:使用应用级配置中心,而且每种语言都需要设置自己的配置中心。
3)应用不跨开发语言,微服务不大于一个Kubernetes边界:使用Kubernetes平台的ConfigMap。
4)应用不跨开发语言,微服务大于一个Kubernetes边界,不限定应用的Service名称:使用应用级配置中心。
在本节中,我们介绍平台与应用级相结合的注册和配置中心的实现。需要指出的是,这种方式只适合Spring Cloud部署在单个Kubernetes集群的情况,此前这种方式被大量使用,但绝不是一个好的方法。笔者之所以展开介绍,是想让读者直观了解配置中心和注册中心的实际效果。
以图2-28为例,我们将整套微服务部署到一个Namespace中,从图中可以看到配置中心(service-config)和注册中心(service-registry)的SVC和端口号(https://github.com/davidsajare/spring-cloud-on-openshift.git)。
图2-28 配置中心和注册中心
微服务部署完后,业务微服务在指定配置中心的地址是service-config:8888。而service-config到IP地址的解析,由Kubernetes中的CoreDNS完成。例如,我们查看card-service部署中的环境变量,configure server指向http://service-config:8888/,如图2-29所示。
图2-29 configure server的配置
在微服务注册时,先指定访问配置中心(service-config:8888),然后配置中心(service-config)的Profile定义了注册中心的地址和端口号(service-registry:8761),以便微服务能够在注册中心进行注册,效果如图2-30所示。
图2-30 微服务访问配置中心和注册中心
我们查看配置中心(service-config:8888)名为openshift的配置文件,如图2-31所示。
图2-31 名为openshift的配置文件
图2-31所示的配置指定了注册中心的地址和端口号(service-registry:8761),即Eureka的地址和端口号:
"eureka.instance.instance-id": "${POD_NAME:${spring.application.name}}:${server.port}", "eureka.instance.hostname": "${HOSTNAME:${spring.application.name}}",
查看service-registry的环境变量,如图2-32所示。
图2-32 service-registry的环境变量
上面第一段代码,带入变量后,显示注册中心的两个Pod名和端口号:
#第一个注册中心Pod "eureka.instance.instance-id":service-registry-0:service-registry:8761 "eureka.instance.hostname": service-registry-0.service-registry:service-registry #第二个注册中心Pod "eureka.instance.instance-id":service-registry-1:service-registry:8761 "eureka.instance.hostname": service-registry-1.service-registry:service-registry
接下来,我们看一个微服务gateway-3-gjgfz的启动过程,观察它如何完成服务注册。Pod启动后,读取了关于配置中心的环境变量:
Starting the Java application using /opt/jboss/container/java/run/run-java.sh ... INFO exec java -Dspring.profiles.active=openshift -Dspring.cloud.config.uri=http://service-config:8888/ -javaagent:/usr/share/java/prometheus-jmx-exporter/jmx_prometheus_javaagent.jar= 9779:/opt/jboss/container/prometheus/etc/jmx-exporter-config.yaml -XX:+Us eParallelOldGC -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio= 4 -XX:AdaptiveSizePolicyWeight=90 -XX:MaxMetaspace Size=100m -XX:+ExitOnOutOfMemoryError -cp "." -jar /deployments/ gateway-0.0.1-SNAPSHOT.jar
然后gateway-3-gjgfz微服务很快获取到配置中心中名为openshift为的配置文件:
2021-03-13 01:58:44.020 INFO 1 --- [main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at:http://service-config:8888/ 2021-03-13 01:58:44.153 INFO 1 --- [main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=gateway, profiles=[openshift], label=null, version=null,
接下来完成服务注册:
2021-03-13 01:58:46.835 INFO 1 --- [ main] DiscoveryClientOptionalArgsConfiguration : Eureka HTTP Client uses RestTemplate. 2021-03-13 01:58:46.969 INFO 1 --- [ main] o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING 2021-03-13 01:58:47.042 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1 2021-03-13 01:58:47.049 INFO 1 --- [ main] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Disable delta property : false 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Force full registry fetch : false 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Application is null : false 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Application version is -1: true 2021-03-13 01:58:47.073 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server 2021-03-13 01:58:47.146 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : The response status is 200 2021-03-13 01:58:47.149 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 30 2021-03-13 01:58:47.152 INFO 1 --- [ main] c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 4 2021-03-13 01:58:47.157 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1615600727156 with initial instances count: 7 2021-03-13 01:58:47.158 INFO 1 --- [ main] o.s.c.n.e.s.EurekaServiceRegistry : Registering application GATEWAY with eureka with status UP 2021-03-13 01:58:47.159 INFO 1 --- [ main] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1615600727159, current=UP, previous=STARTING] 2021-03-13 01:58:47.162 INFO 1 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_GATEWAY/gateway-3- gjgfz:8080: registering service... 2021-03-13 01:58:47.216 INFO 1 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_GATEWAY/gateway-3- gjgfz:8080 - registration status: 204 2021-03-13 01:58:47.312 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080 2021-03-13 01:58:47.313 INFO 1 --- [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080 2021-03-13 01:58:47.332 INFO 1 --- [ main] com.demo.gateway.Application : Started Application in 4.556 seconds (JVM running for 5.166) 2021-03-13 02:03:47.076 INFO 1 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration 2021-03-13 02:08:47.077 INFO 1 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
微服务注册成功后,我们到注册中心查看注册成功的应用,如图2-33所示。
图2-33 在注册中心查看注册成功的应用
总结如下:
1)在上述Spring Cloud代码中,业务微服务访问配置中心,用的是Kubernetes ServiveName:Port,然后读取配置中心的配置,去注册中心注册;
2)微服务在注册中心注册成功后,记录端点的信息是Pod Hostname:Port。
注意,Kubernetes和Spring Cloud完全1:1对应是比较理想的情况,这时候微服务之间的互访、微服务访问配置中心和注册中心,都是在内部完成的。