5.1 介绍服务

5.1 介绍服务

Kubernetes服务是一种为一组功能相同的pod提供单一不变的接入点的资源。当服务存在时,它的IP地址和端口不会改变。客户端通过IP地址和端口号建立连接,这些连接会被路由到提供该服务的任意一个pod上。通过这种方式,客户端不需要知道每个单独的提供服务的pod的地址,这样这些pod就可以在集群中随时被创建或移除。

结合实例解释服务

回顾一下有前端web服务器和后端数据库服务器的例子。有很多pod提供前端服务,而只有一个pod提供后台数据库服务。需要解决两个问题才能使系统发挥作用。

外部客户端无须关心服务器数量而连接到前端pod上。

前端的pod需要连接后端的数据库。由于数据库运行在pod中,它可能会在集群中移来移去,导致IP地址变化。当后台数据库被移动时,无须对前端pod重新配置。

通过为前端pod创建服务,并且将其配置成可以在集群外部访问,可以暴露一个单一不变的IP地址让外部的客户端连接pod。同理,可以为后台数据库pod创建服务,并为其分配一个固定的IP地址。尽管pod的IP地址会改变,但是服务的IP地址固定不变。另外,通过创建服务,能够让前端的pod通过环境变量或DNS以及服务名来访问后端服务。

5.1.1 创建服务

服务的后端可以有不止一个pod。服务的连接对所有的后端pod是负载均衡的。但是要如何准确地定义哪些pod属于服务哪些不属于呢?

或许还记得在ReplicationController和其他的pod控制器中使用标签选择器来指定哪些pod属于同一组。服务使用相同的机制。

在前面的章节中,通过创建ReplicationController运行了三个包含Node.js应用的pod。再次创建ReplicationController并且确认pod启动运行,在这之后将会为这三个pod创建一个服务。

通过kubectl expose创建服务

创建服务的最简单的方法是通过kubectl expose,在第2章中曾使用这种方法来暴露创建的ReplicationController。像创建ReplicationController时使用的pod选择器那样,利用expose命令和pod选择器来创建服务资源,从而通过单个的IP和端口来访问所有的pod。

现在,除了使用expose命令,可以通过将配置的YAML文件传递到Kubernetes API服务器来手动创建服务。

通过YAML描述文件来创建服务

使用以下代码清单中的内容创建一个名为kubia-svc.yaml的文件。

代码清单5.1 服务的定义:kubia-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - port: 80  #服务可用端口
    targetPort: 8080 #转发的容器端口
  selector:
    app: kubia #具有app=kubia标签的pod都属于该服务

附:deployment文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubia
  labels:
    app: kubia
spec:
  replicas: 3
  selector:
    matchLabels:
      app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia:1.0
        ports:
        - containerPort: 8080

创建了一个名叫kubia的服务,它将在端口80接收请求并将连接路由到具有标签选择器是app=kubia的pod的8080端口上。

接下来通过使用kubectl create发布文件来创建服务。

检测新的服务

在发布完YAML文件后,可以在命名空间下列出来所有的服务资源,并可以发现新的服务已经被分配了一个内部集群IP。

$ kubectl get svc
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubia   ClusterIP   10.111.56.158   <none>        80/TCP    11s

列表显示分配给服务的IP地址是 10.111.56.158 。因为只是集群的IP地址,只能在集群内部可以被访问。服务的主要目标就是使集群内部的其他pod可以访问当前这组pod,但通常也希望对外暴露服务。如何实现将在之后讲解。现在,从集群内部使用创建好的服务并了解服务的功能。

从内部集群测试服务

可以通过以下几种方法向服务发送请求:

显而易见的方法是创建一个pod,它将请求发送到服务的集群IP并记录响应。可以通过查看pod日志检查服务的响应。

使用ssh远程登录到其中一个Kubernetes节点上,然后使用curl命令。

可以通过 kubectl exec 命令在一个已经存在的pod中执行curl命令。

我们来学习最后一种方法——如何在已有的pod中运行命令。

在运行的容器中远程执行命令

可以使用kubectl exec命令远程地在一个已经存在的pod容器上执行任何命令。这样就可以很方便地了解pod的内容、状态及环境。用 kubectl get pod 命令列出所有的pod,并且选择其中一个作为exec命令的执行目标(在下述例子中,选择kubia-7nog1 pod作为目标)。也可以获得服务的集群IP(比如使用 kubectl get svc 命令),当执行下述命令时,请确保替换对应pod的名称及服务IP地址。

$ kubectl exec kubia-7d5b548867-fnvl5 -- curl -s http://10.111.56.158

如果之前使用过ssh命令登录到一个远程系统,会发现kubectl exec没有特别大的不同之处。

为什么是双横杠?

双横杠(--)代表着kubectl命令项的结束。在两个横杠之后的内容是指在pod内部需要执行的命令。如果需要执行的命令并没有以横杠开始的参数,横杠也不是必需的。

回顾一下在运行命令时发生了什么。在一个pod容器上,利用Kubernetes去执行curl命令。curl命令向一个后端有三个pod服务的IP发送了HTTP请求,Kubernetes服务代理截取的该连接,在三个pod中任意选择了一个pod,然后将请求转发给它。Node.js在pod中运行处理请求,并返回带有pod名称的HTTP响应。接着,curl命令向标准输出打印返回值,该返回值被kubectl截取并打印到宕主机的标准输出。

在之前的例子中,在pod主容器中以独立进程的方式执行了curl命令。这与容器真正的主进程和服务通信并没有什么区别。

配置服务上的会话亲和性

如果多次执行同样的命令,每次调用执行应该在不同的pod上。因为服务代理通常将每个连接随机指向选中的后端pod中的一个,即使连接来自于同一个客户端。

另一方面,如果希望特定客户端产生的所有请求每次都指向同一个pod,可以设置服务的sessionAffinity属性为ClientIP(而不是None,None是默认值),如下面的代码清单所示。

userspace 代理模式

代码清单5.2 会话亲和性被设置成ClientIP的服务的例子

apiVersion: v1
kind: Service
spec:
  sessionAffinity: ClientIP
......

这种方式将会使服务代理将来自同一个client IP的所有请求转发至同一个pod上。作为练习,创建额外的服务并将会话亲和性设置为ClientIP,并尝试向其发送请求。

Kubernetes仅仅支持两种形式的会话亲和性服务:None和ClientIP。你或许惊讶竟然不支持基于cookie的会话亲和性的选项,但是你要了解Kubernetes 服务不是在HTTP层面上工作。服务处理TCP和UDP包,并不关心其中的载荷内容。因为cookie是HTTP协议中的一部分,服务并不知道它们,这就解释了为什么会话亲和性不能基于cookie。

同一个服务暴露多个端口

创建的服务可以暴露一个端口,也可以暴露多个端口。比如,你的pod监听两个端口,比如HTTP监听8080端口、HTTPS监听8443端口,可以使用一个服务从端口80和443转发至pod端口8080和8443。在这种情况下,无须创建两个不同的服务。通过一个集群IP,使用一个服务就可以将多个端口全部暴露出来。

注意 在创建一个有多个端口的服务的时候,必须给每个端口指定名字。

以下代码清单中展示了多端口服务的规格。

代码清单5.3 在服务定义中指定多端口

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  selector:
    app: kubia  #标签选择器适用于整个服务
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
    - name: https
      protocol: TCP
      port: 443
      targetPort: 8081

注意 标签选择器应用于整个服务,不能对每个端口做单独的配置。如果不同的pod有不同的端口映射关系,需要创建两个服务。

之前创建的kubia pod不在多个端口上侦听,因此可以练习创建一个多端口服务和一个多端口pod。

使用命名的端口 在这些例子中,通过数字来指定端口,但是在服务spec中也可以给不同的端口号命名,通过名称来指定。这样对于一些不是众所周知的端口号,使得服务spec更加清晰。在服务spec中按名称引用这些端口,如下面的代码清单所示。

代码清单5.5 在服务中引用命名pod

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  selector:
    app: kubia  #标签选择器适用于整个服务
  ports:
    - name: http
      port: 80
      targetPort: http #将80端口映射到容器中被称为http的端口
    - name: https
      port: 443
      targetPort: https #将443端口映射到容器中被称为https的端口

为什么要采用命名端口的方式?最大的好处就是即使更换端口号也无须更改服务spec。你的pod现在对http服务用的是8080,但是假设过段时间你决定将端口更换为80呢?

如果你采用了命名的端口,仅仅需要做的就是改变spec pod 中的端口号(当然你的端口号的名称没有改变)。在你的pod向新端口更新时,根据pod收到的连接(8080端口在旧的pod上、80端口在新的pod上),用户连接将会转发到对应的端口号上。

5.1.2 服务发现

通过创建服务,现在就可以通过一个单一稳定的IP地址访问到pod。在服务整个生命周期内这个地址保持不变。在服务后面的pod可能删除重建,它们的IP地址可能改变,数量也会增减,但是始终可以通过服务的单一不变的IP地址访问到这些pod。

但客户端pod如何知道服务的IP和端口?是否需要先创建服务,然后手动查找其IP地址并将IP传递给客户端pod的配置选项?当然不是。Kubernetes还为客户端提供了发现服务的IP和端口的方式。

通过环境变量发现服务

在pod开始运行的时候,Kubernetes会初始化一系列的环境变量指向现在存在的服务。如果你创建的服务早于客户端pod的创建,pod上的进程可以根据环境变量获得服务的IP地址和端口号。

在一个运行pod上检查环境,去了解这些环境变量。现在已经了解了通过kubectl exec命令在pod上运行一个命令,但是由于服务的创建晚于pod的创建,那么关于这个服务的环境变量并没有设置,这个问题也需要解决。

在查看服务的环境变量之前,首先需要删除所有的pod使得ReplicatSet创建全新的pod。在无须知道pod的名字的情况下就能删除所有的pod,就像这样:

$ kubectl delete po --all

现在列出所有新的pod,然后选择一个作为kubectl exec命令的执行目标。一旦选择了目标pod,通过在容器中运行env来列出所有的环境变量,如下面的代码清单所示。

代码清单5.6 容器中和服务相关的环境变量

$ k exec kubia-7d5b548867-4qn2x -- env

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBIA_SERVICE_HOST=10.111.56.158 # 服务的集群IP
KUBIA_SERVICE_PORT=80 #服务所在的端口

在集群中定义了两个服务:kubernetes和kubia(之前在用kubectl get svc命令的时候应该见过);所以,列表中显示了和这两个服务相关的环境变量。在本章开始部分,创建了kubia服务,在和其有关的环境变量中有 KUBIA_SERVICE_HOSTKUBIA_SERVICE_PORT ,分别代表了kubia服务的IP地址和端口号。

回顾本章开始部分的前后端的例子,当前端pod需要后端数据库服务pod时,可以通过名为 backend-database 的服务将后端pod暴露出来,然后前端pod通过环境变量 BACKEND_DATABASE_SERVICE_HOSTBACKEND_DATABASE_SERVICE_PORT 去获得IP地址和端口信息。

注意 服务名称中的横杠被转换为下画线,并且当服务名称用作环境变量名称中的前缀时,所有的字母都是大写的。

环境变量是获得服务IP地址和端口号的一种方式,为什么不用DNS域名?为什么Kubernetes中没有DNS服务器,并且允许通过DNS来获得所有服务的IP地址?事实证明,它的确如此!

通过DNS发现服务

还记得第3章中在kube-system命名空间下列出的所有pod的名称吗?其中一个pod被称作kube-dns,当前的kube-system的命名空间中也包含了一个具有相同名字的响应服务。

k get svc -A

就像名字的暗示,这个pod运行DNS服务,在集群中的其他pod都被配置成使用其作为dns(Kubernetes通过修改每个容器的/etc/resolv.conf文件实现)。运行在pod上的进程DNS查询都会被Kubernetes自身的DNS 服务器响应,该服务器知道系统中运行的所有服务。

注意 pod是否使用内部的DNS服务器是根据pod中spec的dnsPolicy属性来决定的。

每个服务从内部DNS 服务器中获得一个DNS条目,客户端的pod在知道服务名称的情况下可以通过全限定域名(FQDN)来访问,而不是诉诸于环境变量。

通过FQDN连接服务

再次回顾前端-后端的例子,前端pod可以通过打开以下FQDN的连接来访问后端数据库服务:

backend-database.default.svc.cluster.local

backend-database对应于服务名称,default表示服务在其中定义的名称空间,而svc.cluster.local是在所有集群本地服务名称中使用的可配置集群域后缀。

注意 客户端仍然必须知道服务的端口号。如果服务使用标准端口号(例如,HTTP的80端口或Postgres的5432端口),这样是没问题的。如果并不是标准端口,客户端可以从环境变量中获取端口号。

连接一个服务可能比这更简单。如果前端pod和数据库pod在同一个命名空间下,可以省略 svc.cluster.local 后缀,甚至命名空间。因此可以使用 backend-database 来指代服务。这简单到不可思议,不是吗?

尝试一下。尝试使用FQDN来代替IP去访问kubia服务。另外,必须在一个存在的pod上才能这样做。已经知道如何通过 kubectl exec 在一个pod的容器上去执行一个简单的命令,但是这一次不是直接运行curl命令,而是运行bash shell,这样可以在容器上运行多条命令。在第2章中,当想进入容器启动Docker时,调用 docker exec-it bash 命令,这与此很相似。

在pod容器中运行shell

可以通过kubectl exec命令在一个pod容器上运行bash(或者其他形式的shell)。通过这种方式,可以随意浏览容器,而无须为每个要运行的命令执行kubectl exec。

注意 shell的二进制可执行文件必须在容器镜像中可用才能使用。

为了正常地使用shell,kubectl exec 命令需要添加–it选项:

$ kubectl exec -it kubia-3inly -- bash

现在进入容器内部,根据下述的任何一种方式使用curl命令来访问kubia服务:

# curl http://kubia.custom.svc.cluster.local
Hey there, this is kubia-7d5b548867-8wnls. Your IP is ::ffff:172.18.0.1.
# curl http://kubia.custom
Hey there, this is kubia-7d5b548867-8wnls. Your IP is ::ffff:172.18.0.1.
# curl http://kubia
Hey there, this is kubia-7d5b548867-m4bfz. Your IP is ::ffff:172.18.0.1.

在请求的URL中,可以将服务的名称作为主机名来访问服务。因为根据每个pod容器DNS解析器配置的方式,可以将命名空间和svc.cluster.local后缀省略掉。查看一下容器中的/etc/resilv.conf文件就明白了。

# cat /etc/resolv.conf
nameserver 10.96.0.10
search test.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

无法ping通服务IP的原因

在继续之前还有最后一问题。了解了如何创建服务,很快地去自己创建一个。但是,不知道什么原因,无法访问创建的服务。

大家可能会尝试通过进入现有的pod,并尝试像上一个示例那样访问该服务来找出问题所在。然后,如果仍然无法使用简单的curl命令访问服务,也许会尝试ping 服务 IP以查看服务是否已启动。现在来尝试一下:

# ping kubia
PING kubia.test.svc.cluster.local (10.99.93.78): 56 data bytes
^C--- kubia.test.svc.cluster.local ping statistics ---
91 packets transmitted, 0 packets received, 100% packet loss

嗯,curl这个服务是工作的,但是却ping不通。这是因为服务的集群IP是一个虚拟IP,并且只有在与服务端口结合时才有意义。将在第11章中解释这意味着什么,以及服务是如何工作的。在这里提到这个问题,因为这是用户在尝试调试异常服务时会做的第一件事(ping服务的IP),而服务的IP无法ping通会让大多数人措手不及。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352