微服务的自动发现

在熟悉了的基本操作后,我们来讨论下如何实现微服务的自动发现。

Service是在Pod基础上做的另一层抽象,通过虚拟IP的方式,提供了统一的代理入口和负载均衡。

Service本身不会创建Pod,而是通过标签的方式与已有Pod产生关联,这与Deployment是类似的。因此,在创建第一个Service前,我们需要先应用之前的lmsia-abc-server-deployment,具体可参考前一节Kubernetes 快速入门

下面来看一下Service描述文件,lmsia-abc-server-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: lmsia-abc-server-service
spec:
  selector:
    app: lmsia-abc-server 
  ports:
  - name: http
    protocol: TCP
    port: 8080
  - name: rpc 
    protocol: TCP
    port: 3000

与Deployment相比,上述Service的描述文件更简单一些。

  • kind: 类型是Service
  • metadata.name: 定义了Service名字
  • spec.selector.app: 定义了要关联的Pod标签
  • spec.ports: 定义了需要进行负载均衡的端口,这里定义了两套需要负载均衡的端口,http的8080和rpc的3000。

有了描述文件后,我们来应用服务:

kubectl apply -f lmsia-abc-server-service.yaml

service "lmsia-abc-server-service" created

成功创建Service后,可以使用'describe service'来查看:

kubectl describe service lmsia-abc-server-service

Name:              lmsia-abc-server-service
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"lmsia-abc-server-service","namespace":"default"},"spec":{"ports":[{"name":"htt...
Selector:          app=lmsia-abc-server
Type:              ClusterIP
IP:                10.109.20.138
Port:              http  8080/TCP
TargetPort:        8080/TCP
Endpoints:         172.17.0.4:8080,172.17.0.5:8080
Session Affinity:  None
Events:            <none>

上面返回的结果中,有一些关键信息:

  • Type: 指的是ServiceType,ClusterIP是仅供集群内访问的负载均衡IP。类似的,如果想将虚拟IP暴露给集群外,可以使用NodePort等,具体可以参考官方文档Publising Service Types
  • IP: 服务提同的虚拟IP地址。
  • Port: 微服务进程上的端口,即HTTP的8080和RPC的3000
  • TargetPort: 虚拟IP对外提供负载均衡的端口,由于我们未单独制定,默认是与上述Port保持一致的。
  • Endpoints:我们在Deployment中定义的两个Pod。Service通过虚拟IP将流量分发到这两个后端Pod上。

让我们来验证下负载均衡的配置是否生效,由于rpc接口的数据格式较为复杂,在此我们仅验证http端口。

首先登录到minikube

minikube ssh

curl http://10.109.20.138:8080/lmsia-abc/api/
Hello, REST

curl http://10.109.20.138:8080/lmsia-abc/api/
Hello, REST

我们执行了两次,都成功了,那么这个请求真的被均匀地分发到后端的进程上了么?我们需要验证一下。

首先获取两个容器的ID


# list pod
kubectl get pods -l app=lmsia-abc-server
NAME                                          READY     STATUS    RESTARTS   AGE
lmsia-abc-server-deployment-bd4949ff9-7bgvq   1/1       Running   0          16m
lmsia-abc-server-deployment-bd4949ff9-mlmlq   1/1       Running   0          16m

# get container id for pod1
kubectl describe pod lmsia-abc-server-deployment-bd4949ff9-7bgvq
...
Name:           lmsia-abc-server-deployment-bd4949ff9-7bgvq
...
Containers:
  lmsia-abc-server-ct:
    Container ID:   docker://a146ee545d11638a331d1696e7e6e3c88cc3231b97f3eb50c63cb9f50724cf2c
...

# get container id for pod 2
kubectl describe pod
...
Name:           lmsia-abc-server-deployment-bd4949ff9-mlmlq
...
Containers:
  lmsia-abc-server-ct:
    Container ID:   docker://608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a
...

登录minikube集群,分别查看两个Container的日志

minikube ssh

# check pod 1 access log
$ docker exec -i -t a146ee545d11638a331d1696e7e6e3c88cc3231b97f3eb50c63cb9f50724cf2c cat /app/logs/access_log.2018-05-14.log
10.0.2.15 - - [14/May/2018:07:27:57 +0000] "GET /lmsia-abc/api/ HTTP/1.1" 200 11

# check pod 2 access log
$ docker exec -i -t 608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a cat /app/logs/access_log.2018-05-14.log
10.0.2.15 - - [14/May/2018:07:27:56 +0000] "GET /lmsia-abc/api/ HTTP/1.1" 200 11

这里需要说明下'docker exec -i -t',是针对Docker容器执行命令,要执行的命令即后面的cat /app/logs....

查看了两个Pod对应的Container的日志,可以发现:虽然我们的curl是访问的虚拟IP,但是流量被均衡地分发到了2个后端容器上。至此,我们已经通过Service实现了多节点的自动负载均衡。

需要指出的是:Kubernetes的虚拟IP内置了多种实现,目前以ipvs性能最好,具体可以查看Virtual IPs and service proxies

现在让我们来回顾下这一节的标题:"微服务的自动发现"。对于服务发现这个需求,我们目前的效果似乎并不这么完美,为什么这样说呢?我们目前是通过虚拟IP直接访问的服务,但在实际生产环境中,每个Service创建的虚拟IP并不固定,我们不可能将这些虚拟IP分别配置在依赖的众多微服务中。

幸运的是,Kubernetes早就为我们解决了这个问题。在创建Service的同时,Kubernetes还为我们创建了一条DNS记录,我们可以通过域名直接访问虚拟IP:

docker exec -i -t 608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a busybox wget -q -O - http://lmsia-abc-server-service:8080/lmsia-abc/api/

Hello, REST

如上所示,通过lmsia-abc-server-service这个域名,就可以成功地访问虚拟IP了。对于ClusterIP的Service,域名的默认组成是'服务名.服务所在命名空间.svc.cluster.集群域名',或者简单使用服务名1,上面例子中我们采用的就是后者。

让我们用一张图来回顾下服务发现、负载均衡流程:

基于Kubernetes的服务发现与负载均衡

如上图所示:

  1. 约定好微服务Service的命名方式
  2. 通过DNS服务获取微服务Service对应的虚拟IP(VIP)
  3. 访问VIP和端口(3000)
  4. Kubernetes的VIP自动完成了负载均衡,转发到后端Service B的3个节点(Pod/Docker)上

至此,我们借助Kubernetes的Service功能,"近似完美"地实现了服务的注册与发现。

为什么讲"近似完美"呢?这里还会有一个小坑。熟悉DNS协议的朋友知道,为了提升查询效率,DNS被设计成可以多级缓存的。在Java的JVM虚拟机上,也会进行DNS缓存,但这个缓存有效期默认是-1即永久。这也就意味着,如果我们删除这个Service重新创建,那么虚拟IP的变更将不会自动反馈到相应微服务的JVM中。

为了解决这个小坑,一般建议修改JVM的安全设置,修改缓存TTL时间,具体可以参考亚马逊AWS的这篇介绍

我们为本章构建的Docker镜像也自动解决了这个问题:

FROM anapsix/alpine-java:8_server-jre

WORKDIR /app

RUN mkdir -p /app/logs

ADD lmsia-abc-server.jar /app

CMD ["java", "-jar", "lmsia-abc-server.jar"]

其中anapsix/alpine-java:8_server-jre是我们依赖的基础镜像,它将DNS Cache设置为了10秒钟,读者也可以直接使用这个基础镜像。

需要特别说明的时:若想使用上述的自动发现机制,必须使用Kubernetes的DNS服务,它默认是开启的:

kubectl -n kube-system get svc kube-dns

NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP   3d

通过Kubernetes创建的Pod(Docker),已经自动配置了上述DNS。若想在在集群外使用这个DNS,有两种方案:


  1. 这一特性并未记录在官方文档中,本书假设该特性持续有效。


书籍推荐