learn-tech/专栏/Kubernetes入门实战课/20Service:微服务架构的应对之道.md
2024-10-16 06:37:41 +08:00

16 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        20 Service微服务架构的应对之道
                        你好我是Chrono。

在前面的课里我们学习了Deployment和DaemonSet这两个API对象它们都是在线业务只是以不同的策略部署应用Deployment创建任意多个实例Daemon为每个节点创建一个实例。

这两个API对象可以部署多种形式的应用而在云原生时代微服务无疑是应用的主流形态。为了更好地支持微服务以及服务网格这样的应用架构Kubernetes又专门定义了一个新的对象Service它是集群内部的负载均衡机制用来解决服务发现的关键问题。

今天我们就来看看什么是Service、如何使用YAML来定义Service以及如何在Kubernetes里用好Service。

为什么要有Service

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

不过,在应用程序快速版本迭代的同时,另一个问题也逐渐显现出来了,就是“服务发现”。

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

这种“动态稳定”对于现在流行的微服务架构来说是非常致命的试想一下后台Pod的IP地址老是变来变去客户端该怎么访问呢如果不处理好这个问题Deployment和DaemonSet把Pod管理得再完善也是没有价值的。

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

但LVS、Nginx毕竟不是云原生技术所以Kubernetes就按照这个思路定义了新的API对象Service。

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

下面的这张图来自Kubernetes官网文档比较清楚地展示了Service的工作原理

你可以看到这里Service使用了iptables技术每个节点上的kube-proxy组件自动维护iptables规则客户不再关心Pod的具体地址只要访问Service的固定IP地址Service就会根据iptables规则转发请求给它管理的多个Pod是典型的负载均衡架构。

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

如何使用YAML描述Service

知道了Service的基本工作原理我们来看看怎么为Service编写YAML描述文件。

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

现在相信你很容易写出Service的YAML文件头了吧

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

同样的能否让Kubernetes为我们自动创建Service的YAML样板呢还是使用命令 kubectl create 吗?

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

因为在Kubernetes里提供服务的是Pod而Pod又可以用Deployment/DaemonSet对象来部署所以 kubectl expose 支持从多种对象创建服务Pod、Deployment、DaemonSet都可以。

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

比如,如果我们要为[第18讲]里的ngx-dep对象生成Service命令就要这么写

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

生成的Service YAML大概是这样的

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

spec: selector: app: ngx-dep

ports:

  • port: 80 targetPort: 80 protocol: TCP

你会发现Service的定义非常简单在“spec”里只有两个关键字段selector 和 ports。

selector 和Deployment/DaemonSet里的作用是一样的用来过滤出要代理的那些Pod。因为我们指定要代理Deployment所以Kubernetes就为我们自动填上了ngx-dep的标签会选择这个Deployment对象部署的所有Pod。

从这里你也可以看到Kubernetes的这个标签机制虽然很简单却非常强大有效很轻松就关联上了Deployment的Pod。

ports 就很好理解了里面的三个字段分别表示外部端口、内部端口和使用的协议在这里就是内外部都使用80端口协议是TCP。

当然,你在这里也可以把 ports 改成“8080”等其他的端口这样外部服务看到的就是Service给出的端口而不会知道Pod的真正服务端口。

为了让你看清楚Service与它引用的Pod的关系我把这两个YAML对象画在了下面的这张图里需要重点关注的是 selector、targetPort 与Pod的关联

如何在Kubernetes里使用Service

在使用YAML创建Service对象之前让我们先对第18讲里的Deployment做一点改造方便观察Service的效果。

首先我们创建一个ConfigMap定义一个Nginx的配置片段它会输出服务器的地址、主机名、请求的URI等基本信息

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'; } }

然后我们在Deployment的“template.volumes”里定义存储卷再用“volumeMounts”把配置文件加载进Nginx容器里

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

spec: replicas: 2 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

这两处修改用到了[第14讲]里的知识,如果你还没有熟练掌握,可以回去复习一下。

部署这个Deployment之后我们就可以创建Service对象了用的还是 kubectl apply

kubectl apply -f svc.yml

创建之后,用命令 kubectl get 就可以看到它的状态:

你可以看到Kubernetes为Service对象自动分配了一个IP地址“10.96.240.115”这个地址段是独立于Pod地址段的比如第17讲里的10.10.xx.xx。而且Service对象的IP地址还有一个特点它是一个“虚地址”不存在实体只能用来转发流量。

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

kubectl describe svc ngx-svc

截图里显示Service对象管理了两个endpoint分别是“10.10.0.232:80”和“10.10.1.86:80”初步判断与Service、Deployment的定义相符那么这两个IP地址是不是Nginx Pod的实际地址呢

我们还是用 kubectl get pod 来看一下,加上参数 -o wide

kubectl get pod -o wide

把Pod的地址与Service的信息做个对比我们就能够验证Service确实用一个静态IP地址代理了两个Pod的动态IP地址。

那怎么测试Service的负载均衡效果呢

因为Service、 Pod的IP地址都是Kubernetes集群的内部网段所以我们需要用 kubectl exec 进入到Pod内部或者ssh登录集群节点再用curl等工具来访问Service

kubectl exec -it ngx-dep-6796688696-r2j6t -- sh

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

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

kubectl delete pod ngx-dep-6796688696-r2j6t

由于Pod被Deployment对象管理删除后会自动重建而Service又会通过controller-manager实时监控Pod的变化情况所以就会立即更新它代理的IP地址。通过截图你就可以看到有一个IP地址“10.10.1.86”消失了换成了新的“10.10.1.87”它就是新创建的Pod。

你也可以再尝试一下使用“ping”来测试Service的IP地址

会发现根本ping不通因为Service的IP地址是“虚”的只用于转发流量所以ping无法得到回应数据包也就失败了。

如何以域名的方式使用Service

到这里Service的基本用法就讲得差不多了不过它还有一些高级特性值得了解。

我们先来看看DNS域名。

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

使用DNS域名之前我们要先了解一个新的概念名字空间namespace

注意它与我们在[第2讲]里说的用于资源隔离的Linux namespace技术完全不同千万不要弄混了。Kubernetes只是借用了这个术语但目标是类似的用来在集群里实现对API对象的隔离和分组。

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

kubectl get ns

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

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

Service对象的域名完全形式是“对象.名字空间.svc.cluster.local”但很多时候也可以省略后面的部分直接写“对象.名字空间”甚至“对象名”就足够了默认会使用对象所在的名字空间比如这里就是default

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

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

比起Docker这无疑是一个巨大的进步而且对比其他微服务框架如Dubbo、Spring Cloud由于服务发现机制被集成在了基础设施里也会让应用的开发更加便捷。

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

如何让Service对外暴露服务

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

Service对象有一个关键字段“type”表示Service是哪种类型的负载均衡。前面我们看到的用法都是对集群内部Pod的负载均衡所以这个字段的值就是默认的“ClusterIP”Service的静态IP地址只能在集群内访问。

除了“ClusterIP”Service还支持其他三种类型分别是“ExternalName”“LoadBalancer”“NodePort”。不过前两种类型一般由云服务商提供我们的实验环境用不到所以接下来就重点看“NodePort”这个类型。

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

让我们修改一下Service的YAML文件加上字段“type”

apiVersion: v1 ... spec: ... type: NodePort

然后创建对象,再查看它的状态:

kubectl get svc

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

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

比如我现在所在的服务器是“192.168.10.208”在这台主机上用curl访问Kubernetes集群的两个节点“192.168.10.210”“192.168.10.220”就可以得到Nginx Pod的响应数据

我把NodePort与Service、Deployment的对应关系画成了图你看了应该就能更好地明白它的工作原理

学到这里你是不是觉得NodePort类型的Service很方便呢。

不过它也有一些缺点。

第一个缺点是它的端口数量很有限。Kubernetes为了避免端口冲突默认只在“30000~32767”这个范围内随机分配只有2000多个而且都不是标准端口号这对于具有大量业务应用的系统来说根本不够用。

第二个缺点是它会在每个节点上都开端口然后使用kube-proxy路由到真正的后端Service这对于有很多计算节点的大集群来说就带来了一些网络通信成本不是特别经济。

第三个缺点它要求向外界暴露节点的IP地址这在很多时候是不可行的为了安全还需要在集群外再搭一个反向代理增加了方案的复杂度。

虽然有这些缺点但NodePort仍然是Kubernetes对外提供服务的一种简单易行的方式在其他更好的方式出现之前我们也只能使用它。

小结

好了今天我们学习了Service对象它实现了负载均衡和服务发现技术是Kubernetes应对微服务、服务网格等现代流行应用架构的解决方案。

我再小结一下今天的要点:

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

课下作业

最后是课下作业时间,给你留两个思考题:

为什么Service的IP地址是静态且虚拟的出于什么目的有什么好处 你了解负载均衡技术吗它都有哪些算法Service会用哪种呢

欢迎在留言区分享你的思考以输出带动自己输入。到今天你已经完成2/3的专栏学习了回看一起学过的内容不知你收获如何呢。

如果觉得有帮助,不妨分享给自己身边的朋友一起学习,我们下节课再见。