在我们最近关于Kubernetes的序列,Kubernetes的服务网格,第一部分:顶级服务质量一文中,细心的读者注意到,linkerd是使用DaemonSet而不是sidecar进程安装的。在这篇文章中,我们将解释我们为什么(以及如何)这样做。
作为服务网格,链接器被设计为与应用程序代码一起运行,管理和监视服务间通信,包括执行服务发现,重连,负载平衡和协议升级。
乍一看,这听起来非常适合Kubernetes的sidecar部署。毕竟,Kubernetes的一个特征就是它的pod模型。作为sidecar的部署理论上很简单,具有明确的失败语义,并且我们已经花费了大量时间来优化此用例的链接器。
然而,sidecar模型也有一个缺点:每一个pod的部署将意味着每一个pod消耗的资源都将翻倍。如果你的服务是轻量级的,并且运行了许多实例,比如Monzo( 在linkerd和Kubernetes之上建立了一个完整的仓库),那么使用sidecars的成本将会相当高。
我们可以通过为每个主机部署链接器而不是每个pod来降低资源成本。这样使资源消耗以主机为规模,这显然比以pod来计数的增量级少得多。而且,幸运的是,Kubernetes为此提供了 DaemonSet。
不幸的是,对于链接器,每个主机的部署比使用DaemonSet要复杂一些。以下是我们如何使用Kubernetes中部署每个主机来解决服务网格问题。
服务网格的一个定义特征是其将应用程序通信与传输通信分开的能力。例如,如果服务A和B使用HTTP,则服务网格可能会将其转换为HTTPS而不通知应用程序。服务网格也可以做连接池,准入控制或其他传输层功能,对应用程序也是透明的。
为了完全做到这一点,链接器必须在每个请求的发送端和接收端代理本地实例。例如,对于HTTP到HTTPS的升级,链接器必须能够启动和终止TLS。在DaemonSet中,通过链接器的请求路径如下图所示:
正如你所看到的,一个请求从主机1上的Pod A开始发往主机2上的Pod J必须通过Pod A上的本地主机链接实例,然后到主机2的链接实例,最后到Pod J。这条路径介绍了链接器必须解决的三个问题:
以下是我们如何解决这三个问题的技术细节。如果您只想了解与Kubernetes DaemonSet一起使用的链接器,请参阅上一篇博客文章!
由于DaemonSet使用KuberneteshostPort
,我们知道链接器在主机IP固定的端口上运行。因此,为了将请求发送到运行在同一台机器上的链接进程,我们需要确定其主机的IP地址。
在Kubernetes 1.4及更高版本中,这些信息可以通过下载的API直接获得。这里是一个除了hello-world.yml之外的例子,它显示了如何将节点名称传给应用程序:
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: http_proxy
value: $(NODE_NAME):4140
args:
- "-addr=:7777"
- "-text=Hello"
- "-target=world"
(注意,这个例子设置了http_proxy
环境变量使其可以直接对所有通过本地主机链接器连接的HTTP调用可用。尽管这种方法适用于大多数HTTP应用程序,但是非HTTP应用程序仍然需要一些额外的步骤)。
在1.4版以前的Kubernetes版本中,这些信息仍然可用,但是不能直接使用。我们提供了一个简单的脚本查询Kubernetes API来获取主机IP; 这个脚本的输出可以被应用程序使用,或者用来构建一个如上个例子所示的http_proxy
环境变量。
以下是来自hello-world-legacy.yml的摘录,其中展示了如何将主机IP传递到应用程序中:
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NS
valueFrom:
fieldRef:
fieldPath: metadata.namespace
command:
- "/bin/sh"
- "-c"
- "http_proxy=`hostIP.sh`:4140 helloworld -addr=:7777 -text=Hello -target=world"
注意hostIP.sh
脚本要求将pod的名称和环境变量中设置的pod的命名空间一致。
在我们的服务网格部署中,传出请求不应直接发送到目标应用程序,而是发送到在该应用程序的主机上运行的链接器。为此,我们可以利用linkerd 0.8.0引入的一个强大的新特性,称之为转换器,它可以对链接到路由的目标地址进行任意的后续处理。在这种情况下,我们可以使用DaemonSet转换器自动将目标地址转换为目标主机上运行的DaemonSet pod的地址。例如,输出路由器的链接器配置会将所有请求像发送给目标app一样发送到与目标应用程序位于同一主机上的链接器:
routers:
- protocol: http
label: outgoing
interpreter:
kind: default
transformers:
- kind: io.l5d.k8s.daemonset
namespace: default
port: incoming
service: l5d
...
当一个请求最终传输到目标pod的链接器实例时,它必须被正确地路由到pod本身。为此,我们使用localnode
转换器将路由限制为仅在当前主机上运行的pod。链接器配置示例:
routers:
- protocol: http
label: incoming
interpreter:
kind: default
transformers:
- kind: io.l5d.k8s.localnode
...
将链接器部署为Kubernetes DaemonSet是两全其美的选择 - 它允许我们完成服务网格的所有目标(如透明TLS,协议升级,延迟感知负载平衡等),同时减少每个主机的链接器实例规模而不是每个pod。
有关完整的工作示例,请参阅上一篇博文,或下载示例应用程序。对于这个配置或其他关于链接器的帮助,请发送到我们活跃的Slack或者在linkerd话题上发表一个话题 。