【k8s】Service微服务架构的应对之道(十)


前言

在云原生时代,微服务无疑是应用的主流形态。为了更好地支持微服务以及服务网格这样的应用架构,Kubernetes 又专门定义了一个新的对象:Service,它是集群内部的负载均衡机制,用来解决服务发现的关键问题。在 Kubernetes Service 文档中Service被定义为将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法。


提示:以下是本篇文章正文内容,下面案例可供参考

一、为什么要有 Service

有了 Deployment 和 DaemonSet,我们在集群里发布应用程序的工作轻松了很多。借助 Kubernetes 强大的自动化运维能力,我们可以把应用的更新上线频率由以前的月、周级别提升到天、小时级别,让服务质量更上一层楼。

在 Kubernetes 集群里 Pod 的生命周期是比较短暂的,虽然 Deployment 和 DaemonSet 可以维持 Pod 总体数量的稳定,但在运行过程中,难免会有 Pod 销毁又重建,这就会导致 Pod 集合处于动态的变化之中。

这导致了一个问题: 如果一组 Pod(称为后端)为集群内的其他 Pod(称为前端)提供功能, 那么前端如何找出并跟踪要连接的 IP 地址,以便前端可以使用提供工作负载的后端部分?

业内早就有解决方案来针对这样不稳定的后端服务,那就是负载均衡,典型的应用有LVS、Nginx 等等。它们在前端与后端之间加入了一个中间层,屏蔽后端的变化,为前端提供一个稳定的服务。

因此 Kubernetes 定义了一个新的 API 对象:Service

二、Service 的工作原理

Service 的工作原理和 LVS、Nginx 差不多,Kubernetes 会给它分配一个静态 IP 地址,然后它再去自动管理、维护后面动态变化的 Pod 集合,当客户端访问 Service,它就根据某种策略,把流量转发给后面的某个 Pod

在这里插入图片描述
这张图展示的是 Service 的 iptables 代理模式。每个节点上的kube-proxy 组件自动维护 iptables 规则,客户不再关心 Pod 的具体地址,只要访问 Service 的固定 IP 地址Service 就会根据 iptables 规则转发请求给它管理的多个 Pod,这是典型的负载均衡架构。此外,使用 iptables 处理流量具有较低的系统开销,因为流量由 Linux netfilter 处理, 而无需在用户空间和内核空间之间切换。 这种方法也可能更可靠。

Service 并不是只能使用 iptables 来实现负载均衡,它还有另外两种实现技术:性能更差的 userspace 和性能更好的 ipvs,但这些都属于底层细节,我们不需要刻意关注。

三、使用 YAML 描述 Service

我们还是可以用命令 kubectl api-resources 查看它的基本信息,可以知道它的简称是 svcapiVersionv1。注意,这说明它与 Pod 一样,属于 Kubernetes 的核心对象,不关联业务应用,与 Job、Deployment 是不同的。

这里 Kubernetes 又表现出了行为上的不一致。虽然它可以自动创建 YAML 样板,但不是用命令 kubectl create,而是另外一个命令 kubectl expose,也许 Kubernetes 认为 expose 能够更好地表达 Service 暴露服务地址的意思吧。

使用 kubectl expose 指令时还需要用参数 --port--target-port 分别指定映射端口和容器端口,而 Service 自己的 IP 地址和后端 Pod 的 IP 地址可以自动生成,用法上和 Docker 的命令行参数 -p 很类似,只是略微麻烦一点。

例如,我们对 Deployment 笔记中的 ngx-dep 对象生成 Service ,命令就应该这样写:

$ export out="--dry-run=client -o yaml"
$ kubectl expose deploy ngx-dep --port=80 --target-port=80 $out

下面是剔除一些字段后的 YAML:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: ngx-dep

可以发现,Service 的定义非常简单,spec 中只有两个关键字 selectorports

  • selector 和 Deployment/DaemonSet 里的作用是一样的,用来过滤出要代理的那些 Pod。因为我们指定要代理 Deployment,所以 Kubernetes 就为我们自动填上了 ngx-dep 的标签,会选择这个 Deployment 对象部署的所有 Pod。
  • ports 就很好理解了,里面的三个字段分别表示外部端口、内部端口和使用的协议,在这里就是内外部都使用 80 端口,协议是 TCP

四、在 Kubernetes 中使用 Service

在 YAML 创建 Service 对象之前,我们要先对 Deployment 笔记中ngx-dep 做一点改造:方便观察 Service 的效果。

首先,创建一个 ConfigMap,定义一个 Nginx 的配置片段,它会响应服务器地址、主机名、请求的 URI 等信息:

# ngx-conf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ngx-conf

data:
  default.conf: |
    server {
      listen 80;
      location / {
        default_type text/plain;
        return 200
          'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
      }
    }
$ kubectl apply -f ngx-conf.yaml

然后在 Deployment 中的 template.volumes 里定义存储卷,再用 volumeMounts 将配置文件加载到容器中:

# ngx-dep.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep

spec:
  replicas: 3
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
      - name: ngx-conf-vol
        configMap:
          name: ngx-conf

      containers:
      - image: nginx:alpine
        name: nginx
        ports:
        - containerPort: 80

        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: ngx-conf-vol
$ kubectl apply -f ngx-conf.yaml

部署这个 Deployment 之后,我们就可以创建 Service 对象了:

# ngx-svc.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: ngx-dep
  name: ngx-dep
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: ngx-dep
$ kubectl apply -f ngx-svc.yaml

创建之后,我们就可以查看该 Service 对象的状态:

$ kubelet get svc -o wide
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE   SELECTOR
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   2d    <none>
ngx-svc      ClusterIP   10.96.21.182   <none>        80/TCP    10s   app=ngx-dep

Kubernetes 为 Service 对象自动分配了一个 IP 地址 10.96.21.182,这个地址段是独立于 Pod 地址段的(10.10.xx.xx)。而且 Service 对象的 IP 地址还有一个特点,它是一个虚地址,不存在实体,只能用来转发流量。

想要看 Service 代理了哪些后端的 Pod,可以使用 describe 子命令:

$ kubectl describe svc ngx-svc
Name:              ngx-svc
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=ngx-dep
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.21.182
IPs:               10.96.21.182
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.10.1.34:80,10.10.1.35:80,10.10.1.36:80
Session Affinity:  None
Events:            <none>

显示 Service 对象管理了 3 个 endpoint:10.10.1.34:80, 10.10.1.35:80, 10.10.1.36:80

下面查看一下 Pod 详情:

$ kubectl get pods -o wide 
NAME                       READY   STATUS    RESTARTS       AGE   IP           NODE         NOMINATED NODE   READINESS GATES
ngx-dep-545884c69c-9q6gd   1/1     Running   0              39m   10.10.1.34   k8s-worker01      <none>           <none>
ngx-dep-545884c69c-rbfpn   1/1     Running   0              39m   10.10.1.35   k8s-worker01      <none>           <none>
ngx-dep-545884c69c-vzdf2   1/1     Running   0              39m   10.10.1.36   k8s-worker01      <none>           <none>
redis-ds-8zmdb             1/1     Running   1 (140m ago)   23h   10.10.0.39   k8s-master01   <none>           <none>
redis-ds-tp9sd             1/1     Running   1 (140m ago)   23h   10.10.1.30   k8s-worker01      <none>           <none

接下来测试负载均衡的效果,因为 Service、 Pod 的 IP 地址都是 Kubernetes 集群的内部网段,所以我们需要用kubectl exec 进入到 Pod 内部(或者 ssh 登录集群节点),再用 curl 等工具来访问 Service:

$ kubectl exec -it ngx-dep-545884c69c-9q6gd -- sh
/ # curl 10.96.21.182
srv : 10.10.1.34:80
host: ngx-dep-545884c69c-9q6gd
uri : GET 10.96.21.182 /
date: 2023-03-20T03:04:55+00:00
/ # curl 10.96.21.182
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.96.21.182 /
date: 2023-03-20T03:04:59+00:00
/ # curl 10.96.21.182
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.96.21.182 /
date: 2023-03-20T03:05:02+00:00

Pod 里,用 curl 访问 Service 的 IP 地址,就会看到它把数据转发给后端的 Pod,输出信息会显示具体是哪个 Pod 响应了请求,就表明 Service 确实完成了对 Pod 的负载均衡任务。

再试着删除一个 Pod,看看 Service 是否会更新后端 Pod 的信息,实现自动化的服务发现:

$ kubectl delete pod ngx-dep-545884c69c-9q6gd 
pod "ngx-dep-545884c69c-9q6gd" deleted
$ kubectl describe svc ngx-svc
Name:              ngx-svc
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=ngx-dep
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.21.182
IPs:               10.96.21.182
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.10.1.35:80,10.10.1.36:80,10.10.1.37:80
Session Affinity:  None
Events:            <none>

可以看到一个 IP 地址为 10.10.1.37 的 Pod 被添加进来了。
由于 Pod 被 Deployment 对象管理,删除后会自动重建,而 Service 又会通过controller-manager 实时监控 Pod 的变化情况,所以就会立即更新它代理的 IP 地址。

五、以域名方式使用 Service

Service 还有一些高级特性值得了解。

首先是 DNS 域名。Service 对象的 IP 地址是静态的,保持稳定,这在微服务里确实很重要,不过数字形式的 IP 地址用起来还是不太方便。这个时候 Kubernetes DNS 插件就派上了用处,它可以为 Service 创建易写易记的域名,让 Service 更容易使用。

使用 DNS 域名之前,我们要先了解一个新的概念:名字空间 namespace,它被用来在集群里实现对 API 对象的隔离和分组。

namespace 的简写是 ns,使用命令 kubectl get ns 来查看当前集群里都有哪些名字空间,也就是说 API 对象有哪些分组:

$ kubectl get ns 
NAME              STATUS   AGE
default           Active   2d
kube-flannel      Active   2d
kube-node-lease   Active   2d
kube-public       Active   2d
kube-system       Active   2d

Kubernetes 有一个默认的名字空间 default,如果不显式指定,API 对象都会在这个 default 名字空间里。而其他的名字空间都有各自的用途,比如 kube-system 就包含了 apiserver、etcd 等核心组件的 Pod

因为 DNS 是一种层次结构,为了避免太多的域名导致冲突,Kubernetes 就把名字空间作为域名的一部分,减少了重名的可能性。

Service 对象的域名完全形式是:

Object.namespace.svc.cluster.local

但很多时候也可以省略后面的部分,直接写object.namesapce 甚至 object 就足够了,默认会使用对象所在的名字空间(比如这里就是 default)。

现在我们来试验一下 DNS 域名的用法,还是先 kubectl exec 进入 Pod,然后用 curl 访问 ngx-svcngx-svc.default 等域名:

$ kubectl exec -it ngx-dep-545884c69c-rbfpn -- sh
/ # curl ngx-svc
srv : 10.10.1.37:80
host: ngx-dep-545884c69c-mmrnm
uri : GET ngx-svc /
date: 2023-03-20T04:05:02+00:00
/ # curl ngx-svc.default
srv : 10.10.1.35:80
host: ngx-dep-545884c69c-rbfpn
uri : GET ngx-svc.default /
date: 2023-03-20T04:05:02+00:00

可以看到,现在我们就不再关心 Service 对象的 IP 地址,只需要知道它的名字,就可以用 DNS 的方式去访问后端服务。

顺便说一下,Kubernetes 也为每个 Pod 分配了域名,形式是 IP Address.namespace.pod.cluster.local,但需要把 IP 地址里的 . 改成 - 。比如地址 10.10.1.87,它对应的域名就是 10-10-1-87.default.pod

这里是 Kubernetes 文档 - Service 与 Pod 的 DNS

六、让 Service 对外暴露服务

由于 Service 是一种负载均衡技术,所以它不仅能够管理 Kubernetes 集群内部的服务,还能够担当向集群外部暴露服务的重任。

Service 对象有一个关键字段 type,表示 Service 是哪种类型的负载均衡。

  • ClusterIP - 对集群内部 Pod 的负载均衡,Service 的静态 IP 地址只能在集群内访问
  • ExternalName - 通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如 foo.bar.example.com)。 无需创建任何类型代理查看文档
  • LoadBalancer - 一般依赖云服务提供商 查看文档
  • NodePort - 通过每个节点上的 IP 和静态端口(NodePort)暴露服务

在实验环境里我们使用NodePort
如果在使用命令kubectl expose 的时候加上参数 --type=NodePort,或者在 YAML 里添加字段 type:NodePort,那么 Service 除了会对后端的 Pod 做负载均衡之外,还会在集群里的每个节点上创建一个独立的端口,用这个端口对外提供服务,这也正是 NodePort 这个名字的由来。

下面我们给 Service 的 YAML 文件加上 type 字段:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  type: NodePort
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: ngx-dep

$ kubectl apply -f ngx-svc.yaml
service/ngx-svc configured
$ kubectl get svc -o wide 
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE     SELECTOR
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        2d5h    <none>
ngx-svc      NodePort    10.96.21.182   <none>        80:30952/TCP   5h25m   app=ngx-dep

就会看到 TYPE 变成了 NodePort,而在 PORT 列里的端口信息也不一样,除了集群内部使用的 80 端口,还多出了一个 30952 端口,这就是 Kubernetes 在节点上为 Service 创建的专用映射端口。

因为这个端口号属于节点,外部能够直接访问,所以现在我们就可以不用登录集群节点或者进入 Pod 内部,直接在集群外使用任意一个节点的 IP 地址,就能够访问 Service 和它代理的后端服务了。

比如我使用宿主机 IP: 10.0.0.12 访问集群内的 Worker1 节点 IP: 10.0.0.12:30952 就可以得到 Nginx Pod 的响应数据:

$ curl 10.0.0.12:30952
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.0.0.12 /
date: 2023-03-20T08:20:21+00:00

NodePort 类型的 Service 有下面几个缺点:

  • 端口数量很有限。Kubernetes 为了避免端口冲突,默认只在“30000~32767”这个范围内随机分配,只有 2000 多个,而且都不是标准端口号,这对于具有大量业务应用的系统来说根本不够用
  • 它会在每个节点上都开端口,然后使用 kube-proxy 路由到真正的后端 Service,这对于有很多计算节点的大集群来说就带来了一些网络通信成本,不是特别经济
  • 它要求向外界暴露节点的 IP 地址,这在很多时候是不可行的,为了安全还需要在集群外再搭一个反向代理,增加了方案的复杂度

总结

  1. Pod 的生命周期很短暂,会不停地创建销毁,所以就需要用 Service 来实现负载均衡,它由 Kubernetes 分配固定的 IP 地址,能够屏蔽后端的 Pod 变化。
  2. Service 对象使用与 Deployment、DaemonSet 相同的“selector”字段,选择要代理的后端 Pod,是松耦合关系。
  3. 基于 DNS 插件,我们能够以域名的方式访问 Service,比静态 IP 地址更方便。
  4. 名字空间是 Kubernetes 用来隔离对象的一种方式,实现了逻辑上的对象分组,Service 的域名里就包含了名字空间限定。
  5. Service 的默认类型是“ClusterIP”,只能在集群内部访问,如果改成“NodePort”,就会在节点上开启一个随机端口号,让外界也能够访问内部的服务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值