前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >落地k8s容易出现13个实践错误

落地k8s容易出现13个实践错误

原创
作者头像
iginkgo18
修改2021-09-07 10:14:33
1.6K0
修改2021-09-07 10:14:33
举报
文章被收录于专栏:devops_k8sdevops_k8s

1 简介

在我们多年使用kubernetes的经验中,我们有幸看到了很多集群(在GCP,AWS和Azure上都是托管的和非托管的),并且我们看到一些错误在不断重复。

2 错误

2.1 resources - requests 和 limits

通常未设置CPU请求或将CPU请求设置得太低(这样我们就可以在每个节点上容纳很多Pod),因此节点的使用量过大。在需求旺盛的时间,节点的CPU被充分利用,我们的工作负载仅获得“所申请的资源”,并且受到CPU throttled,从而导致应用程序延迟,超时等增加。

BestEffort(请不要这么做)

代码语言:javascript
复制
resources: {}

非常低CPU

代码语言:javascript
复制
    resources:
      requests:
        cpu: "1m"

另一方面,即使节点的CPU没有得到充分利用,拥有CPU限制也会不必要地限制Pod,这又会导致延迟增加。围绕linux内核中的CPU CFS配额和基于设置的cpu限制并关闭CFS配额的cpu节制进行了公开讨论。 CPU限制可能导致更多的问题,无法解决。

内存过量使用会给您带来更多麻烦。达到CPU限制将导致节流,达到内存限制将使您的Pod被杀死。见过OOMkill吗?是的,这就是我们正在谈论的那个。想要最小化它发生的频率?请勿过度使用您的内存,并使用保证的QoS(服务质量)设置的内存请求等于限制。

Burstable (更容易被OOM杀死):

代码语言:javascript
复制
resources:
      requests:
        memory: "128Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: 2

Guaranteed

代码语言:javascript
复制
    resources:
      requests:
        memory: "128Mi"
        cpu: 2
      limits:
        memory: "128Mi"
        cpu: 2

那么在设置资源时有什么可以帮助您的呢?

您可以使用metrics-server查看pod(及其中的容器)的当前cpu和内存使用情况。很有可能,您已经在运行它。只需运行以下命令:

代码语言:javascript
复制
kubectl top pods
kubectl top pods --containers
kubectl top nodes

但是,这些仅显示当前用法。但是您最终想及时查看这些使用情况指标(以回答诸如:高峰,昨天早晨等情况下的cpu使用情况之类的问题)。为此,您可以使用Prometheus,DataDog等。他们只是从指标服务器中获取指标并进行存储,然后就可以对其进行查询和绘制图形。

VerticalPodAutoscaler可以帮助您自动执行此手动过程-及时查看cpu/mem的使用情况,并再次基于此设置新的请求和限制。

有效利用计算资源并非易事。就像一直在玩俄罗斯方块。如果发现自己在平均利用率较低(例如〜10%)的情况下为计算付出了高昂的代价,则可能需要检查基于AWS Fargate或Virtual Kubelet的产品,这些产品更多地利用了无服务器/按使用量计费模式。

Pod 请求:这是调度程序用来放置 pods 的主要参考值。来自 Kubernetes 文档

过滤步骤会找到一组 Node 节点,它们可以用来调度 Pod 。例如,PodFitsResources 过滤器检查候选节点是否具有足够的可用资源来满足 Pod 的特定资源请求。

在内部,我们以这种方式使用应用程序请求;我们依据应用程序在正常工作负载下的实际需求估计来设置 Pod 请求。这样,调度程序能够根据实际放置节点。最初,我们希望将请求值设置为更高,以确保每个 Pod 都有足够的资源,但是当我们这样做时,我们注意到调度时间大大增加,甚至有些 Pod 完全无法调度。这点类似于我们没有指定资源请求时观察到的行为。在这种情况下,调度程序经常会“逐出” Pod 而无法重新调度它们,这是由于控制器不知道应用程序需要多少资源,这也是调度算法的关键组成部分。

Pod 限制:这是 Pod 的直接限制;它表示集群允许容器使用的最大资源。我们再来看一下官方文档……

如果你为该容器设置了4GiB的内存限制,则 kubelet(和容器运行时)将强制执行该限制。运行时可防止容器使用超出配置的资源限制。例如:当容器中的进程尝试消耗的内存大小超过允许的内存时,系统内核将终止尝试分配的进程,并出现内存不足(OOM)错误。

容器可以使用比其请求更多的资源,但永远不能超过其限制。正确设置这个值非常重要。理想情况下,你希望让 Pod 的资源需求在进程的生命周期中发生变化,而又不会干扰系统中的其他进程——这是限制的目标。不幸的是,我无法提供具体的设置值,但我们按照以下过程进行调整:

  1. 使用负载测试工具,我们模拟基本流量,并观察 Pod 的资源使用情况(内存和 CPU)。
  2. 我们将 Pod 请求设置为任意低(同时将 Pod 资源限制保持在请求值的5倍左右)并观察。当请求太少时,该进程将无法启动,并经常引发神秘的 Go 运行时错误。

更高的资源限制导致更难的 Pod 调度;因为它需要具有足够的可用资源的目标节点。试想一下你可能在资源限制很高(例如4GB内存)的情况下运行轻量级 Web 服务器进程,这个进程你可能需要水平扩展,并且每个新容器都需要被调度到至少具有 4GB 可用内存的节点上。如果该节点不存在,则你的集群需要引入一个新节点来处理该 Pod,这可能需要一些时间才能启动。务必在资源请求和限制之间取得最小的“界限”,以确保快速平稳地扩展。

2.2 liveness和readiness probes

默认情况下,未指定活动性和就绪性探针。有时它会一直保持下去…… 但是,如果出现不可恢复的错误,您的服务将如何重新启动?负载平衡器如何知道特定的Pod可以开始处理流量?或处理更多流量?

人们通常不知道这两者之间的区别。

  • 如果探测失败,活动探测将重新启动您的Pod
  • 就绪探针会在kubernetes服务失败的Pod失败时断开连接(您可以在kubectl get端点中进行检查),并且不再有流量发送给它,直到探针再次成功

并且在整个POD生命周期内都运行。这个很重要。

人们常常认为,准备就绪探针仅在开始时就运行,以告知Pod何时就绪,并且可以开始为流量提供服务。但这只是其用例之一。

另一个是要判断在Pod的生命周期内,Pod是否变得太热而无法处理过多的流量(或昂贵的计算),以至于我们不让它做更多的工作来让她冷静下来,那么就绪性探测成功了,我们开始再次发送更多流量。在这种情况下(当准备就绪探测失败时),活动探测也失败会适得其反。您为什么要重新启动运行良好的Pod?

有时,未定义任何一个探针比定义错误的探针要好。如上所述,如果活力探针等于准备就绪探针,那么您将遇到很大麻烦。您可能想从仅定义就绪探针开始,因为活动探针很危险

如果您的任何共享依赖项均关闭,则不要使任何一个探针失败,否则将导致所有Pod的级联失败。

Liveness 探针:“指示容器是否正在运行。如果 Liveness 探针失败, kubelet 将杀死容器,并且容器将接受其重新启动策略。如果容器不提供 Liveness 探针,则默认状态为成功。”—— Kubernetes Docs Liveness 探针需要轻量,因为它们使用频繁,并且需要在应用运行时通知 Kubernetes。请注意,如果将其设置为每秒运行一次,那么每秒将增加一个额外的请求流量,因此请考虑处理该请求所需的那些额外资源。在 GumGum,我们的 Liveness 探针设置为在应用程序的主要组件运行时响应,但是数据(例如来自远程数据库或缓存)可能尚未完全可用时。我们通常是这样实现的,设置一个特定的“健康”状态,该状态仅返回 200 响应代码。这很好地表明您的进程已启动并且可以处理请求(但尚未处理流量)。 Readiness 探针:“指示容器是否准备好处理请求。如果 Readiness 探针失败,则端点控制器将从与 Pod 匹配的所有服务的端点中删除 Pod 的 IP 地址。” Readiness 探针的运行成本要高很多,因为它们需要通过和后端的交互来标明整个应用程序正在运行并准备好接收请求。关于是否应该访问数据库,社区中存在很多争论。考虑到它确实造成的开销(这些检查运行频繁,但是可以调整),我们决定对于某些应用程序,只有从数据库返回记录后,我们才“提供流量”。通过使用经过深思熟虑的 Readiness 探针,我们已经能够实现更高水平的可用性以及零停机时间部署。 如果你应用程序的 Readiness 探针确实需要访问数据库,请确保数据库查询尽可能简单。例如……

代码语言:javascript
复制
SELECT small_item FROM table LIMIT 1

如下是在k8s中配置这两个值的示例:

代码语言:javascript
复制
livenessProbe:  
httpGet:    
path: /api/liveness     
port: http  
readinessProbe:   
httpGet:     
path: /api/readiness     
port: http  periodSeconds: 2

如下是一些其他配置选项:

  • initialDelaySeconds —— 容器启动多少秒后探针开始运行
  • periodSeconds —— 探针两次探测之间的等待间隔
  • timeoutSeconds —— Pod 被认为处于故障状态前的秒数。传统的超时时间。
  • failureThreshold —— 重启信号发送到 Pod 之前,Pod 探针需要检测失败的次数。
  • successThreshold —— Pod 进入就绪状态之前探针必须检测成功多少次(在 Pod 启动或恢复的故障事件后)

2.3 为每一个http服务设置LoadBalancer

您的集群中可能有更多的http服务,您想向外界公开。

如果您将kubernetes服务公开为以下类型:LoadBalancer,则其控制器(特定于供应商)将配置和协调外部LoadBalancer(不一定是L7负载均衡器,更可能是L4 lb),并且这些资源可能会变得昂贵(外部静态ipv4地址,每秒钟的价格…)。

在这种情况下,共享一个外部负载均衡器可能更有意义,并且您将服务公开为类型:NodePort。更好的是,部署像nginx-ingress-controller(或traefik)之类的东西作为暴露给外部负载均衡器的单个NodePort端点,并基于kubernetes Ingress资源在集群中路由流量。

彼此对话的其他集群内(微)服务可以通过ClusterIP服务和开箱即用的dns服务发现进行对话。注意不要使用其公共DNS/IP,因为这可能会影响其延迟和云成本。

2.4 无集群感知的autoscaling

在群集中添加节点或从群集中删除节点时,您不应考虑一些简单的指标,例如这些节点的cpu利用率。在调度Pod时,您需要根据Pod和节点的亲和力,污点和容忍度,资源请求,QoS等许多调度约束进行决策。拥有无法理解这些约束的外部自动缩放器可能很麻烦。

想象有一个新的Pod要调度,但是请求所有可用的CPU并且Pod停留在Pending状态。外部自动缩放器可查看当前使用的平均CPU(未请求),并且不会扩展(不会添加其他节点)。该Pod不会被调度。

扩展(从群集中删除节点)总是比较困难。假设您有一个有状态的Pod(已附加持久性卷),并且由于持久性卷通常是属于特定可用性区域的资源,并且不会在该区域中复制,因此您的自定义自动伸缩器将删除带有该Pod的节点,并且调度程序无法对其进行调度转移到另一个节点上,因为它受到永久性磁盘所在的唯一可用性区域的很大限制。 Pod再次陷入待处理状态。

该社区广泛使用在群集中运行的集群自动缩放器,并与大多数主要的公共云供应商API集成在一起,可以理解所有这些限制,并且在上述情况下可以向外扩展。它还将确定它是否可以在不影响我们设置的任何约束的情况下正常扩展,并节省您的计算成本。

2.5 没有使用IAM/RBAC

不要将具有永久秘钥的IAM用户用于机器和应用程序,而要使用角色和服务帐户生成临时秘钥。

我们经常看到它-在应用程序配置中对访问和秘密密钥进行硬编码,当您手握Cloud IAM时就永远不会rotate秘钥。在适当的地方使用IAM角色和服务帐户代替用户。

跳过kube2iam,直接按照此博文中的说明使用服务帐户的IAM角色。

代码语言:javascript
复制
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role
  name: my-serviceaccount
  namespace: default

一个注解。很简单,不是吗? 另外,在不需要时,也不要授予服务帐户或实例配置文件管理员和群集管理员的权限。这有点困难,尤其是在k8s RBAC中,但仍然值得努力。

2.6 self anti-affinities for pods

运行例如某个部署的3个pod副本,节点关闭,所有副本都随之关闭。 所有副本都在一个节点上运行? Kubernetes是否应该具有魔力并提供HA ?!

您不能指望kubernetes调度程序对您的Pod强制执行反亲和。您必须明确定义它们。

代码语言:javascript
复制
// omitted for brevity
      labels:
        app: zk
// omitted for brevity
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"

这将确保将Pod调度到不同的节点(仅在调度时而不是在执行时进行检查,因此需要requiredDuringSchedulingIgnoredDuringExecution);

我们正在谈论不同节点名称上的podAntiAffinity-拓扑关键字:“ http://kubernetes.io/hostname”-不在不同的可用区域。如果您确实需要适当的HA,请更深入地研究该主题。

2.7 没有设置Poddisruptionbudget

您在kubernetes上运行生产工作负载。您的节点和集群必须不时升级或停用。

PodDisruptionBudget(pdb)是一种API,用于在集群管理员和集群用户之间提供服务保证。 确保创建pdb以避免由于耗尽节点而造成不必要的服务中断。

代码语言:javascript
复制
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

将其作为集群用户,您会告诉集群管理员:“嘿,我在这里有此zookeeper服务,无论您要做什么,我都希望至少有2个副本始终可用”。

2.8 共享集群中有更多租户或环境

Kubernetes命名空间不提供任何强隔离。

人们似乎希望,如果将非生产性工作负载分离到一个命名空间,然后将生产性工作转换为生产性命名空间,那么一个工作负载将永远不会影响另一个工作负载。可以实现某种程度的公平-资源请求和限制,配额,优先级类-和隔离-亲和力,容忍度,污点(或节点选择器)-以“物理”方式分离数据平面中的工作负载,但这种分离相当复杂。

如果您需要将两种类型的工作负载都放在同一集群中,则必须承担复杂性。如果您不需要它,并且拥有另一个集群对您而言相对简单(例如在公共云中),则将其放在其他集群中以实现更高的隔离级别。

externalTrafficPolicy: Cluster

经常看到这种情况,所有流量都在群集内路由到默认情况下具有externalTrafficPolicy:Cluster的NodePort服务。这意味着NodePort在群集中的每个节点上都打开,因此您可以使用它们中的任何一个与所需的服务(一组Pod)进行通信。

通常,以NodePort服务为目标的实际Pod仅在这些节点的子集上运行。这意味着,如果我与未运行Pod的节点通信,则会将流量转发到另一个节点,从而导致额外的网络跃点和增加的延迟(如果节点位于不同的AZ /数据中心中,则延迟可能会很高,并且有额外的出口成本);

设置externalTrafficPolicy:kubernetes服务上的Local不会在每个Node上打开该NodePort,而只会在实际运行pod的节点上打开。如果您使用外部负载均衡器(如AWS ELB一样)对其端点进行健康检查,它将开始仅将流量发送到应该去往的那些节点,从而改善了延迟,计算开销,出口费用。

可能是,您有像traefik或nginx-ingress-controller这样的东西被暴露为NodePort(或也使用NodePort的LoadBalancer)也可以处理您的入口http流量路由,并且此设置可以大大减少此类请求的延迟。

2.9 bonus: 使用latest tag

应该停止使用:latest并开始固定版本。

2.10 测试集群给control planne太多压力

太多的测试集群,比如我们做一些压测实验或是故障恢复。最终您会拥有成千上万个对象)控制平面中的对象),或者您不断从kube-api中刮取并编辑大量内容(用于自动缩放,cicd,监视,事件日志,控制器等)。会给我们的控制面造成很大的压力。

另外,检查提供SLA/SLO和托管kubernetes。供应商可能会保证控制平面(或其子组件)的可用性,但不能保证您向其发送的请求的p99延迟。换句话说,您可能会在10分钟内执行kubectl获取节点并获得正确答案,但这仍然没有违反服务保证。

2.11 通过钩子和初始容器自定义行为

我们使用 Kubernetes 系统的主要目标之一就是尝试为开发人员提供尽可能零停机的部署,这个目标很难实现,由于应用程序关闭并清理已利用资源的方式多种多样。我们遇到特别困难的一个应用是 Nginx。我们注意到,当我们启动这些 Pod 的滚动部署时,活动连接在成功终止之前已被删除。经过广泛的在线研究,事实证明 Kubernetes 在终止 Pod 之前并没有等待 Nginx 清理其连接。使用停止前钩子,我们能够注入此功能,并通过此更改实现了零停机时间。

代码语言:javascript
复制
lifecycle:  
preStop:
exec:
  command: ["/usr/local/bin/nginx-killer.sh"] 

nginx-killer.sh

代码语言:javascript
复制
#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
echo "Waiting while shutting down nginx..."
sleep 10
done

另一个非常有用的例子是使用初始化容器来处理应用程序的特定启动任务。某些受欢迎的 Kubernetes 项目,例如 Istio,也利用初始化容器将 Envoy 处理代码注入到 Pod 中。如果你有繁重的数据库迁移进程需要在应用程序启动之前运行,则这特别有用。你也可以为此进程设置更高的资源限制,而对主应用程序不使用该限制。

另一个常见的模式是向初始化容器授予秘密访问权限,该容器将这些凭据暴露给主容器;防止来自主应用程序 Pod 的未经授权的秘密访问。与往常一样,来自文档

初始化容器可以安全地运行实用程序或自定义代码,否则它们会使应用容器镜像的安全性降低。通过将不必要的工具分开,您可以限制应用容器镜像的攻击面。

2.12 设置默认Pod网络策略

Kubernetes 使用一种“扁平”的网络拓扑,默认情况下,所有 Pod 都可以直接相互通信。在某些情况下,这是不希望的,甚至是不必要的。潜在的安全隐患是,如果被利用,则单个易受攻击的应用程序可以为攻击者提供完全访问权限访问网络中的所有 Pod。像在许多安全领域中一样,最小访问策略也适用于此,理想情况下,创建网络策略时会明确指定允许哪些容器到容器的连接。

例如,以下是一个简单的策略,该策略将拒绝特定命名空间的所有入口流量:

代码语言:javascript
复制
---
apiVersion: networking.k8s.io/v1 
kind: NetworkPolicy 
metadata:   
name: default-deny-ingress 
spec:   
podSelector: {}   
policyTypes:   
- Ingress

以下是此配置的可视化图像:

2.13 内核调优

最后,将更先进的技术放到最后。Kubernetes 是一个非常灵活的平台,皆在让你以自己认为合适的方式运行工作负载。在 GumGum,我们有许多高性能应用程序,它们对资源的需求非常苛刻。在进行了广泛的负载测试之后,我们发现我们的一个应用程序正在使用默认的 Kubernetes 设置努力满足预期的流量负载。但是,Kubernetes 允许我们运行特权容器,该特权容器可以修改仅适用于特定运行 Pod 的内核参数。以下是我们用来修改 Pod 允许的最大连接数的示例:

代码语言:javascript
复制
initContainers:
- name: sysctl
  image: alpine:3.10
  securityContext:
      privileged: true
   command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

这是一种不经常需要的更高级的技术。如果你的应用程序难以在高负载下保持运行,则可能需要尝试调整其中一些参数。与往常一样,可以在官方文档中找到有关此过程和可以调整的值的更多信息。

3 小结

不要指望一切都会自动进行-Kubernetes并非灵丹妙药。不好的应用甚至在kubernetes上也将是不好的应用(实际上甚至比坏更糟)。如果不小心,可能会导致很多复杂性,压力大且控制速度慢,并且没有DR策略。不要期望开箱即用的多租户和高可用性。花一些时间使您的应用程序云原生。

尽管 Kubernetes 似乎是一种现成的“开箱即用”解决方案,但是你需要采取一些关键步骤来确保应用程序的平稳运行。在将应用程序转换为在 Kubernetes 上运行的整个过程中,不断进行负载均衡测试是很重要的;运行您的应用程序,对其进行负载测试,观察指标和扩展行为,基于该数据调整你的配置,然后重复。对你期望的流量保持实际,并使其超过该限制,以查看可能首先损坏的组件。使用这种迭代方法,你可能仅使用这些建议的一部分就能成功,或者可能需要更深入的调整。经常问自己以下问题:

  1. 我的应用程序的资源占用量是多少,它将如何变化?
  2. 该服务的实际扩展要求是什么?预计将处理多少平均流量和高峰流量?
  3. 我们期望该服务多久横向扩展一次?需要多长时间这些新的 Pod 才能接受流量。
  4. 我们的 Pod 会优雅地终止吗?它们是否需要?我们能否实现零停机时间部署?
  5. 如何使我的安全风险最小化,并控制任何被攻击的 Pod 所带来的影响?我的服务是否具有不需要的权限或访问权限?

Kubernetes 提供了一个令人难以置信的平台,使你可以利用最佳实践在整个集群中部署数千个服务。正如人们所说,并非所有软件都是平等的。有时你的应用程序可能需要更多的工作,而且值得庆幸的是,Kubernetes 为我们提供了调整的手段来帮助我们实现期望的技术目标。通过结合使用资源请求和限制,Liveness 和 Readiness 检查,初始化容器,网络策略以及自定义内核调整,我相信您可以在获得出色基准性能的同时,仍具有弹性和快速的可扩展性。

首发于kubernetes solutions

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 简介
  • 2 错误
    • 2.1 resources - requests 和 limits
      • 2.2 liveness和readiness probes
        • 2.3 为每一个http服务设置LoadBalancer
          • 2.4 无集群感知的autoscaling
            • 2.5 没有使用IAM/RBAC
              • 2.6 self anti-affinities for pods
                • 2.7 没有设置Poddisruptionbudget
                  • 2.8 共享集群中有更多租户或环境
                    • 2.9 bonus: 使用latest tag
                      • 2.10 测试集群给control planne太多压力
                        • 2.11 通过钩子和初始容器自定义行为
                          • 2.12 设置默认Pod网络策略
                            • 2.13 内核调优
                            • 3 小结
                            相关产品与服务
                            容器服务
                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档