you-men

Java应用程序的奇怪案例

​ 在微服务和容器化方面,工程师倾向于避免使用 Java,这主要是由于 Java 臭名昭著的内存管理。但是,现在情况发生了改变,过去几年来 Java 的容器兼容性得到了改善。毕竟,大量的系统(例如Apache Kafka和Elasticsearch)在 Java 上运行。

​ 回顾 2017-18 年度,我们有一些应用程序在 Java 8 上运行。这些应用程序通常很难理解像 Docker 这样的容器环境,并因堆内存问题和异常的垃圾回收趋势而崩溃。我们了解到,这是由于 JVM 无法使用Linuxcgroup和namespace造成的,而它们是容器化技术的核心。

但是,从那时起,Oracle 一直在不断提高 Java 在容器领域的兼容性。甚至 Java 8 的后续补丁都引入了实验性的 JVM标志来解决这些,XX:+UnlockExperimentalVMOptions和XX:+UseCGroupMemoryLimitForHeap。

​ 但是,尽管做了所有的这些改进,不可否认的是,Java 在内存占用方面仍然声誉不佳,与 Python 或 Go 等同行相比启动速度慢。这主要是由 JVM 的内存管理和类加载器引起的。

​ 现在,如果我们必须选择 Java,请确保版本为 11 或更高。并且 Kubernetes 的内存限制要在 JVM 最大堆内存(-Xmx)的基础上增加 1GB,以留有余量。也就是说,如果 JVM 使用 8GB 的堆内存,则我们对该应用程序的 Kubernetes 资源限制为 9GB。

Kubernetes生命周期管理: 升级

​ Kubernetes 生命周期管理(例如升级或增强)非常繁琐,尤其是如果已经在 裸金属或虚拟机 上构建了自己的集群。对于升级,我们已经意识到,最简单的方法是使用最新版本构建新集群,并将工作负载从旧版本过渡到新版本。节点原地升级所做的努力和计划是不值得的。

​ Kubernetes 具有多个活动组件,需要升级保持一致。从 Docker 到 Calico 或 Flannel 之类的 CNI 插件,你需要仔细地将它们组合在一起才能正常工作。虽然像 Kubespray、Kubeone、Kops 和 Kubeaws 这样的项目使它变得更容易,但它们都有缺点.

​ 我们在 RHEL 虚拟机上使用 Kubespray 构建了自己的集群。Kubespray 非常棒,它具有用于构建、添加和删除新节点、升级版本的 playbook,以及我们在生产环境中操作 Kubernetes 所需的几乎所有内容。但是,用于升级的 playbook 附带了免责声明,以避免我们跳过子版本。因此,必须经过所有中间版本才能到达目标版本。

​ 关键是,如果你打算使用 Kubernetes 或已经在使用 Kubernetes,请考虑生命周期活动以及解决这一问题的方案。构建和运行集群相对容易一些,但是生命周期维护是一个全新的体验,具有多个活动组件。

构建和部署

​ 在准备重新设计整个构建和部署流水线之前, 我们的构建过程和部署必须经历 Kubernetes 世界的完整转型。不仅在 Jenkins 流水线中进行了大量的重构,而且还使用了诸如 Helm 之类的新工具,策划了新的 git 流和构建、标签化 docker 镜像,以及版本化 helm 的部署 chart。

​ 你需要一种策略来维护代码,以及 Kubernetes 部署文件、Docker 文件、Docker 镜像、Helm chart,并设计一种方法将它们组合在一起。

经过几次迭代,我们决定采用以下设计:

  • 应用程序代码及其 helm chart 放在各自的 git 存储库中。这使我们可以分别对它们进行版本控制(语义版本控制)。
  • 然后,我们将 chart 版本与应用程序版本关联起来,并使用它来跟踪发布。例如,app-1.2.0使用charts-1.1.0进行部署。如果只更改 Helm 的 values 文件,则只更改 chart 的补丁版本(例如,从1.1.0到1.1.1)。所有这些版本均由每个存储库中的RELEASE.txt中的发行说明规定。
  • 对于我们未构建或修改代码的系统应用程序,例如 Apache Kafka 或 Redis ,工作方式有所不同。也就是说,我们没有两个 git 存储库,因为 Docker 标签只是 Helm chart 版本控制的一部分。如果我们更改了 docker 标签以进行升级,则会升级 chart 标签的主要版本。

存活和就绪探针(双刃剑)

​ Kubernetes 的存活探针和就绪探针是自动解决系统问题的出色功能。它们可以在发生故障时重启容器,并将流量从不正常的实例进行转移。但是,在某些故障情况下,这些探针可能会变成一把双刃剑,并会影响应用程序的启动和恢复,尤其是有状态的应用程序,例如消息平台或数据库。

​ 我们的 Kafka 系统就是这个受害者。我们运行了一个3 Broker 3 Zookeeper有状态副本集,该状态集的ReplicationFactor为 3,minInSyncReplica为 2。当系统意外故障或崩溃导致 Kafka 启动时,问题发生了。这导致它在启动期间运行其他脚本来修复损坏的索引,根据严重性,此过程可能需要 10 到 30 分钟。由于增加了时间,存活探针将不断失败,从而向 Kafka 发出终止信号以重新启动。这阻止了 Kafka 修复索引并完全启动。

​ 唯一的解决方案是在存活探针设置中配置initialDelaySeconds,以在容器启动后延迟探针评估。但是,问题在于很难对此加以评估。有些恢复甚至需要一个小时,因此我们需要提供足够的空间来解决这一问题。但是,initialDelaySeconds越大,弹性的速度就越慢,因为在启动失败期间 Kubernetes 需要更长的时间来重启容器。

​ 因此,折中的方案是评估initialDelaySeconds字段的值,以在 Kubernetes 中的弹性与应用程序在所有故障情况(磁盘故障、网络故障、系统崩溃等)下成功启动所花费的时间之间取得更好的平衡 。

​ 更新:如果你使用最新版本,Kubernetes 引入了第三种探针类型,称为“启动探针”,以解决此问题。从 1.16 版开始提供 alpha 版本,从 1.18 版开始提供 beta 版本。
启动探针会禁用就绪和存活检查,直到容器启动为止,以确保应用程序的启动不会中断。

公开外部IP

​ 我们了解到,使用静态外部 IP 公开服务会对内核的连接跟踪机制造成巨大代价。除非进行完整的计划,否则它很轻易就破坏了扩展性。

​ 我们的集群运行在Calico for CNI上,在 Kubernetes 内部采用BGP作为路由协议,并与边缘路由器对等。对于 Kubeproxy,我们使用IP Tables模式。我们在 Kubernetes 中托管着大量的服务,通过外部 IP 公开,每天处理数百万个连接。由于来自软件定义网络的所有 SNAT 和伪装,Kubernetes 需要一种机制来跟踪所有这些逻辑流。为此,它使用内核的Conntrack and netfilter工具来管理静态 IP 的这些外部连接,然后将其转换为内部服务 IP,然后转换为 pod IP。所有这些都是通过conntrack表和 IP 表完成的。

​ 但是conntrack表有其局限性。一旦达到限制,你的 Kubernetes 集群(如下所示的 OS 内核)将不再接受任何新连接。在 RHEL 上,可以通过这种方式进行检查。

sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_maxnet.netfilter.nf_conntrack_count = 167012
net.netfilter.nf_conntrack_max = 262144

​ 解决此问题的一些方法是使用边缘路由器对等多个节点,以使连接到静态 IP 的传入连接遍及整个集群。因此,如果你的集群中有大量的计算机,累积起来,你可以拥有一个巨大的conntrack表来处理大量的传入连接。
回到 2017 年我们刚开始的时候,这一切就让我们望而却步,但最近,Calico 在 2019 年对此进行了详细研究,标题为“为什么 conntrack 不再是你的朋友”。

安全方面

​ 对于大部分 Kubernetes 用户来说,安全是无关紧要的,或者说没那么紧要,就算考虑到了,也只是敷衍一下,草草了事。实际上 Kubernetes 提供了非常多的选项可以大大提高应用的安全性,只要用好了这些选项,就可以将绝大部分的攻击抵挡在门外。为了更容易上手,我将它们总结成了几个最佳实践配置,大家看完了就可以开干了。当然,本文所述的最佳安全实践仅限于 Pod 层面,也就是容器层面,于容器的生命周期相关,至于容器之外的安全配置(比如操作系统啦、k8s 组件啦),以后有机会再唠。

为容器配置Security Context

​ 大部分情况下容器不需要太多的权限,我们可以通过 Security Context 限定容器的权限和访问控制,只需加上 SecurityContext 字段:

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
    securityContext:

禁用allowPrivilegeEscalation

​ allowPrivilegeEscalation=true 表示容器的任何子进程都可以获得比父进程更多的权限。最好将其设置为 false,以确保 RunAsUser 命令不能绕过其现有的权限集。

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
    securityContext:
      allowPrivilegeEscalation: false

不要使用root用户

​ 为了防止来自容器内的提权攻击,最好不要使用 root 用户运行容器内的应用。UID 设置大一点,尽量大于 3000。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  securityContext:
    runAsUser: <UID higher than 1000>
    runAsGroup: <UID higher than 3000>

限制CPU和内存资源

requests和limits都加上

不比挂载Service Account Token

​ ServiceAccount 为 Pod 中运行的进程提供身份标识,怎么标识呢?当然是通过 Token 啦,有了 Token,就防止假冒伪劣进程。如果你的应用不需要这个身份标识,可以不必挂载:

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  automountServiceAccountToken: false

确保seccomp设置正确

​ 对于 Linux 来说,用户层一切资源相关操作都需要通过系统调用来完成,那么只要对系统调用进行某种操作,用户层的程序就翻不起什么风浪,即使是恶意程序也就只能在自己进程内存空间那一分田地晃悠,进程一终止它也如风消散了。seccomp(secure computing mode)就是一种限制系统调用的安全机制,可以可以指定允许那些系统调用。

​ 对于 Kubernetes 来说,大多数容器运行时都提供一组允许或不允许的默认系统调用。通过使用 runtime/default 注释或将 Pod 或容器的安全上下文中的 seccomp 类型设置为 RuntimeDefault,可以轻松地在 Kubernetes 中应用默认值。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
  annotations:
    seccomp.security.alpha.kubernetes.io/pod: "runtime/default"

默认的seccomp 配置文件应该为大多数工作负载提供足够的权限,如果你有更多的需求,可以自定义配置文件.

限制容器的capabilities

​ 容器依赖于传统的Unix安全模型,通过控制资源所属用户和组的权限,来达到对资源的权限控制。以 root 身份运行的容器拥有的权限远远超过其工作负载的要求,一旦发生泄露,攻击者可以利用这些权限进一步对网络进行攻击。
​ 默认情况下,使用 Docker 作为容器运行时,会启用 NET_RAW capability,这可能会被恶意攻击者进行滥用。因此,建议至少定义一个PodSecurityPolicy(PSP),以防止具有 NET_RAW 功能的容器启动。
通过限制容器的 capabilities,可以确保受攻击的容器无法为攻击者提供横向攻击的有效路径,从而缩小攻击范围。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: <specific user>
  capabilities:
  drop:
    -NET_RAW
    -ALL

是否一定需要Kubernetes?

​ 它是一个复杂的平台,具有自己的一系列挑战,尤其是在构建和维护环境方面的开销。它将改变你的设计、思维、架构,并需要提高技能和扩大团队规模以适应转型。

​ 但是,如果你在云上并且能够将 Kubernetes 作为一种“服务”使用,它可以减轻平台维护带来的大部分开销,例如“如何扩展内部网络 CIDR?”或“如何升级我的 Kubernetes 版本?”

​ 今天,我们意识到,你需要问自己的第一个问题是“你是否一定需要 Kubernetes?”。这可以帮助你评估所遇到的问题以及 Kubernetes 解决该问题的重要性。

​ Kubernetes 转型并不便宜,为此支付的价格必须确实证明“你的”用例的必要性及其如何利用该平台。如果可以,那么 Kubernetes 可以极大地提高你的生产力。

记住,为了技术而技术是没有意义的。

相关文章: