使用K8s的一些经验和体会
坑
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 可以极大地提高你的生产力。
记住,为了技术而技术是没有意义的。
使用K8s的一些经验和体会的更多相关文章
- Spring Boot 项目转容器化 K8S 部署实用经验分享
转载自:https://cloud.tencent.com/developer/article/1477003 我们知道 Kubernetes 是 Google 开源的容器集群管理系统,它构建在目前流 ...
- 我个人的Java学习经验(一家之言)
声明:本文只是我的个人经验之谈,或者连经验之谈都算不上,因为我觉得自己还是个新手,没有什么经验可谈,就算是我分享一下自己从开始学习Java到现在的一些心路历程吧,各位看官暂且看吧,欢迎交流.第一部分算 ...
- 猫哥网络编程系列:HTTP PEM 万能调试法
注:本文内容较长且细节较多,建议先收藏再阅读,原文将在 Github 上维护与更新. 在 HTTP 接口开发与调试过程中,我们经常遇到以下类似的问题: 为什么本地环境接口可以调用成功,但放到手机上就跑 ...
- 随感一:android handler传值更改ui
handler+looper传值更改activity的UI 博客开了一段时间,一直想写点自己的学习经验及体会,等着以后长时间不用再要用到的时候直接拿过来上手.想了想,之前用到handler, 看了几篇 ...
- 手机淘宝UWP
各位园主好! bug 走势: 哪天bug 足够少,哪天就可以发布了 :) 2015/10/23: 49 2015/10/26: 40 2015/10/27: 36 2015/10/28: 30 20 ...
- 分享10条Visual Studio 2012的开发使用技巧
使用Visual Studio 2012有一段时间了,并不是追赶潮流,而是被逼迫无可奈何.客户要求的ASP.NET MVC 4的项目,要用.NET 4.5来运行.经过一段时间的摸索,得到一点经验和体会 ...
- NoSQL数据库介绍
NoSQL在2010年风生水起,大大小小的Web站点在追求高性能高可靠性方面,不由自主都选择了NoSQL技术作为优先考虑的方面.今年伊始,InfoQ中文站有幸邀请到凤凰网的孙立先生,为大家分享他之于N ...
- 第三届“HTML5峰会”变身“iWeb峰会”8月来袭
第三届“HTML5峰会”——2000人规模的“iWeb峰会”将于8月16日在北京召开.本次大会由HTML5梦工场主办,是在前两届“HTML5峰会”基础上的延伸和升华. 三年以来,HTML5梦工场致力于 ...
- wpf 面试题目
初级工程师 解释什么是依赖属性,它和以前的属性有什么不同?为什么在WPF会使用它?什么是样式什么是模板绑定(Binding )的基础用法解释这几个类的作用及关系: Visual, UIElement, ...
随机推荐
- uniapp中使用picker中的注意事项
APP端中picker点击后不弹出: 1.请确保picker标签里面嵌套了一个view,并且view里面有值 2.请确保picker中的默认值的格式跟该picker类型的值对应 例如下面: <v ...
- 一个实现浏览器网页与本地程序之间进行双向调用的轻量级、强兼容、可扩展的插件开发平台—PluginOK中间件
通过PluginOK中间件插件平台(原名本网通WebRunLocal)可实现在网页中的JavaScript脚本无障碍访问本地电脑的硬件.调用本地系统的API及相关组件,同时可彻底解决ActiveX组件 ...
- 设置定时任务用rman删除归档日志脚本
之前使用数据库数据迁移过程中出现产生大量归档日志的情况(由于迁移的目标库是DG,必须开启归档). 为避免出现归档空间爆掉的情况,设置定时任务删除系统当前时间30分钟前的归档日志,脚本如下: cat d ...
- Hbase备份以及清表脚本
脚本主要是方便自己工作使用,服务器环境中配置了hbase相关环境变量 1.hbase备份脚本 #!/bin/bash tableList=("table1" "table ...
- vue封装API接口
第一步: 首先引入axios 然后创建两个文件夹api和http http.js 里面的 1 import axios from 'axios';//引入axios 2 3 //环境的切换 开发环境( ...
- CCNP第二天之复习CCNA
1.静态路由的扩展配置: (1).环回接口: 在设备上用于测试TCP/IP协议栈能否正常使用.默认没有.需要手工创建 R1(config)#interface loopback 1 ...
- [.NET] - 在Socket编程中遇到的问题总结
问题1.无法访问已释放的对象. 对象名:"System.Net.Sockets.Socket" 产生这个scenario的原因是程序中的某个地方调用到了socket.close后, ...
- python初学者-代码规范
一.编程规范 1.缩进(代码块) 类定义.函数定义.选择结构.循环结构.with块.行尾的冒号表示缩进的开始. python程序是依靠代码块的缩进来体现代码之间的逻辑关系,缩进结束就表示一个代码块结束 ...
- python序列(十)字典
字典是无序可变序列. 定义字典是,每个元素的键和值用冒号分隔,元素之间用逗号分隔,所有的元素放在一对大括号"{ }"中. 字典中的键可以为任意不可变数据,比如.整数.实数.复数.字 ...
- 敏捷史话(一):用一半的时间做两倍的事——Scrum之父Jeff Sutherland
普通的人生大抵相似,传奇的人生各有各的传奇.Jeff就是这样的传奇人物,年近80的他从来没有"廉颇老矣尚能饭否"的英雄迟暮,不久前还精神矍铄地与好几百名中国学生进行线上交流,积极回 ...