[{"content":" 这是个宏伟的计划 # 这是一个宏伟的计划，漫长且有趣。\n2018 年的某个夜晚，夜深人静，我挥舞键盘，敲下了 Sealos 的第一行代码。当时仓库命名为 “kubeinit”，后来觉得格局太小，我不可能只做一个安装 Kubernetes 的工具。安装只是更大计划的一部分，于是更名为 Sealos，一个宏大的 云操作系统计划就此诞生！\nSealos 的第一个版本写完后，我就把它发布到了阿里云市场出售，每份售价 15 元。我没想到真的会有人买，当第一笔 15 元进账时，我异常兴奋，仿佛一个商业帝国就在眼前。但是，结果是我花了一整天时间为这位客户提供售后服务。。。电影院里还在帮用户解决问题。\n先来一波回忆杀：\n随后销量暴增，很快我就换了新手机 iPhone 8，但是问题也同时增加，以至于我根本无法及时提供所有的售后服务。于是我决定重写 Sealos，发布了基于 Ansible 的 v2 版本。最终还是觉得没有做到极致，因为用户还是遇到太多依赖问题无法解决。直到读完 kube-proxy 的源码，我发现有一种方案可以把负载均衡变得更简单，干掉所有依赖。于是我编写了 Sealos 的 v3 版本，在安装方面做到了极致。\n为何一开始专注于安装 ？ # 因为安装是入口，绝大多数人在学习云原生技术时都无法避开这个问题。安装的流量入口足够大，无疑是一个绝佳的切入点。一旦用户习惯使用 Sealos 进行安装，就会逐步探索 Sealos 的其他功能。\n在阿里的工作 # 在阿里工作期间，我开发了 Sealer。这里最重要的一点就是，让安装足够灵活。以前用户只能使用我创建的安装包，而集群镜像的创新可以让用户自由定义安装包，也可以自由组合任何安装包。这里有个让我感到自豪的想法 : **把整个集群视为一个整体，把 Kubernetes 看作一个操作系统，那么在这个 云操作系统中，“云版 Docker 镜像”会是什么样子？**这无疑是一个伟大的想法，极具抽象度和灵活性。\nFROM kubernetes:v1.25.0 COPY mysql . CMD helm install mysql . 这种构想让** 云操作系统也像单机操作系统一样有了“镜像”**，伟大的构想又完成了一个环节。\n创业第一年 # 那么，Sealos 云操作系统最终会演变成什么样子呢？这是一个难以言状的问题，我只有一个朦胧的设想，隐隐若现。直到创业过程中连续迭代了三个版本，才有了今天的形态——一切皆应用！\n理解这一点其实很简单，只需要把单机操作系统上安装的单机应用替换成各种分布式应用即可。整个数据中心，你看到的不再是一台台孤立的服务器，而是一个整体，变成了一台虚拟的超级计算机。\n这样简洁、清爽且臻至完美的 云操作系统，相信你在第一眼见到它的时候，便会喜欢上它！\n这就是我五年的呕心沥血之作 —— Sealos！献给大家～\n云可以如此干净 # Sealos 保持了极简的设计，没有任何多余的按钮。实现简洁与强大并行的功能，有时候难如登天，但我们仍在产品设计上投入了大量的心血。无论何人，使用 Sealos 都将沉醉在我们为之打造的舒适体验中。\n在 B 端软件的世界里，付款者与使用者往往并非同一人，导致产品体验时常被忽略，最关键的还是要说服决策者。而 Sealos 不一样，我们坚信产品体验高于一切，如果我们在产品上花费大量精力最终导致失败，那也死而无憾。\n这种黑白灰的设计风格会让你使用产品时感觉像在喝白开水，而不是在喝饮料，更不是在喝洗脚水 （某些产品使用起来想死的心都有）。开发者已经够痛苦了，我希望你们在使用 Sealos 时心情美好。\nSealos 能一针见血地戳中应用的痛点，比如这个应用管理器 App Launchpad，30 秒就可以让你上线自己的应用。这里涉及到大量细节，比如自动配置公网域名，自动解决 HTTPS 证书问题等。\n云可以如此便宜？ # 我在 Sealos 上运行了 10 多个应用，包括三个数据库，还有博客，低代码平台，测试平台等，每天只花 4 块钱 :\n为什么能这么便宜？\n只需要为运行的容器付费，无需虚拟机，也无需创建整个 Kubernetes 集群，打开直接用。 自动伸缩，夜间用户量少时副本缩小到 1。 我们可以充分利用公有云的弹性，编写大量自动化代码，夜间释放计算资源，降低成本。 这对于企业来说，可以减少大量的资源使用成本。我们自己就在 10 台服务器上运行了 7000 多个应用，这意味着什么？企业部署一套 Sealos 集群后，只要服务器资源利用率低于 70% 就可以不断向集群中添加应用，直到填满为止。\n你可能会问，为什么不能直接使用 Kubernetes？ 原因很简单，对于诸如讯飞这样的企业，应用分散在各个部门，这时多租户、隔离与协作会变成刚需，直接使用 Kubernetes 会把集群搞乱，最要命的可能是一个部门或者用户不注意搞了个安全问题会让整个集群崩溃，而 Sealos 完美解决了这个问题！\nSealos 可以帮助 80% 的企业降低 80% 的资源使用成本。\n云可以如此自由 # 与其他管理平台或 PaaS 平台不同，Sealos 的核心设计理念是“一切皆应用”。不同的开发者，不同的角色使用不同的应用，这让每个用户在使用时都没有心智负担。就像安卓生态中有几十上百万个应用，你只关心自己使用的那几款应用，不用关心其他应用在做什么。\n这样的设计有两个主要优势：\n懂不懂 Kubernetes 都能愉快地使用 Sealos # 许多基于 Kubernetes 的 PaaS 平台或发行版要么暴露大量 Kubernetes 原生概念，要么屏蔽这些概念。这两种做法都不理想。\n暴露大量原生概念对小白和新手不友好，屏蔽 Kubernetes 则失去了灵活性和兼容性，对 Kubernetes 老司机也非常不友好。\nSealos 采取了不同的做法。在这个平台上，不同的人可以使用不同的应用。比如你是开发者想写 CRUD，你可以直接使用 Laf 这个函数应用。如果你是 DBA，你可以直接使用数据库应用。在这种情况下，你完全不需要关心 Kubernetes，这些概念会被完全屏蔽。\n如果用户是云原生专家，他们可以在 Sealos 上安装 Lens 和各种 Kubernetes Dashboard，也可以打开终端敲各种原生命令。这就极大提高了灵活度。\n自由组装 # Sealos 非常关注应用间的相互配合。例如，你在 Sealos 上使用函数计算，默认数据库可能是 MongoDB，但如果你想用 PostgreSQL 怎么办？这时就可以在 Sealos 上安装一个 PostgreSQL 应用，然后通过服务发现直接在函数计算里面访问。因为在同一个集群内，可以直接通过内网 DNS 相互配合。\n如果你还想管控 PostgreSQL 数据库，就可以直接装个 Bytebase 应用来管理数据库表结构和数据等。\nSealos 精简而不简单，所有组件都可以卸载，这让云恰好满足你的需求——多一分则嫌多，少一分则嫌少。这也意味着无论是一台服务器还是上百台数据中心，都可以通过一条命令构建成一朵云。\nSealos 到底能干什么 # 30 秒在 Sealos 上跑个 nginx demo，自动伸缩 30 秒起各种数据库，业务系统内网直接连接数据库 在 Sealos 上直接启动你写的各种编程语言业务 这三个能力是基础，其他的能力你可以慢慢探索，慢慢发现新大陆。\n在运行自己业务上，我们针对这个场景做了很多细节优化，比如自动分配二级域名，自动横向伸缩，支持运行各种有状态服务等。\n你会发现，借助 Sealos，无论是部署一个拨测系统，还是运行一个低代码平台，都是信手拈来。您的博客也可以轻松托管在 Sealos 上，成本低廉。使用 Sealos 终端，运行任何兼容 Kubernetes 的应用，自动化操作不再是难题。\n更进一步发现：原来有个 AI 在帮你自动做故障诊断，自动上线业务，甚至帮你写代码并自动测试上线。\n然后你会发现普通人也能用 Sealos：\n你可以在 Sealos 上快速安装财务软件； 你也可以在 Sealos 上快速安装知识库，给企业所有人写笔记； 你还可以在 Sealos 上快速安装聊天软件供企业内部沟通协作。 到这里你会惊喜地发现：Sealos 竟然什么都能干，真的通用，而且还如此简单！最后你会有所领悟，原来这就是 云操作系统！\n真的有人在用 Sealos 吗 ? # 当然有，Sealos 社区用户 10 万+，不乏各种大企业。\n上线两个月时间注册用户已经破万，云服务共计运行 7000+ 应用。\n只有一些小微应用适合 Sealos 吗 ?\n当然不，Sealos 的客户中有国健大数据，在疫情期间支撑健康码的服务，一秒钟都不能挂的高并发业务。也支撑过超大规模的 GPU 集群，每天处理 80T 数据，整个集群 80PB 数据。聚道云上百个应用跑在 Sealos 平台上。\n阶梯计划 # Sealos 的宏图不止于此，我们的目标是进化为一款无所不在的 云操作系统，为人们提供如同使用个人电脑般简易的云服务体验。借助 Sealos，企业可轻松实现：\n迅疾如闪电，一分钟上线新业务 一年可缩减半数成本 简单如拨动开关，一键起一朵云 企业用云，一款 Sealos 就足矣。\n未来，我们将继续秉持工匠精神，精心打磨 Sealos 中企业所需的常用应用，如数据库、消息队列、推理能力、各类编程语言执行环境等。\nSealos 云操作系统中还会内置一个 Copilot，它像一位航海家的副手，它可自动进行云原生改造，帮助开发者轻松迈入云原生的大门，也可以像专家一样帮助诊断集群问题，安全漏洞，并给出专业操作建议。\n总结 # 历经五载，Sealos 总算实现了我当初写下第一行代码时的愿景 —— 云操作系统。\n感谢第一个为我付了 15块的同学，您的信任与鼓舞犹如一笔巨资，赋予我前行的力量。\n感谢社区的全体贡献者，尤其是始终与我同行的老崔同学，众行远。\n感谢讯飞复杂业务场景的锤炼，让我对业务场景有更深入的理解。\n感谢阿里云在我写 Sealer 时的支持与帮助，为 Sealos 集群镜像的底层能力铸造了坚实的基石。\n感谢与我共同开启创业之旅的所有伙伴，是大家共同将一颗灵感的种子，种植成为现实的大树。\n感谢奇绩创坛踹了我临门一脚，也感谢陆奇博士出乎意料的看好我们给了我们很大信心。\n感谢李军院长康一教授，张海龙，高捷资本，金福资产给我们的帮助、指导和信任。\n感谢每一位选择 Sealos 的用户，你们包容了整个迭代过程中的种种问题，与我们共同雕琢更完美的 Sealos。\n预祝每一位选择了我们的决策者，此刻你们的决策绝对是明智的。现在，Sealos 已经跨越了全新的起点，未来我们一定不负众望，向大家交付一款完美的 云操作系统。\n欢迎大家来体验 Sealos 云操作系统的魅力👉 https://cloud.sealos.run/\n","date":"2019年8月26日","externalUrl":null,"permalink":"/posts/sealos/","section":"博客","summary":"这是个宏伟的计划 # 这是一个宏伟的计划，漫长且有趣。 2018 年的某个","title":"五年磨一剑——Sealos 云操作系统正式发布！","type":"posts"},{"content":"想象一下，如果我想将 nginx 部署到 Kubernetes 集群，我可能会在终端中输入类似这样的命令：\n$ kubectl run --image=nginx --replicas=3 然后回车。几秒钟后，你就会看到三个 nginx pod 分布在所有的工作节点上。这一切就像变魔术一样，但你并不知道这一切的背后究竟发生了什么事情。\nKubernetes 的神奇之处在于：它可以通过用户友好的 API 来处理跨基础架构的 deployments，而背后的复杂性被隐藏在简单的抽象中。但为了充分理解它为我们提供的价值，我们需要理解它的内部原理。\n本指南将引导您理解从 client 到 Kubelet 的请求的完整生命周期，必要时会通过源代码来说明背后发生了什么。\n这是一份可以在线修改的文档，如果你发现有什么可以改进或重写的，欢迎提供帮助！\nkubectl # 验证和生成器 # 当敲下回车键以后，kubectl 首先会执行一些客户端验证操作，以确保不合法的请求（例如，创建不支持的资源或使用格式错误的镜像名称）将会快速失败，也不会发送给 kube-apiserver。通过减少不必要的负载来提高系统性能。\n验证通过之后， kubectl 开始将发送给 kube-apiserver 的 HTTP 请求进行封装。kube-apiserver 与 etcd 进行通信，所有尝试访问或更改 Kubernetes 系统状态的请求都会通过 kube-apiserver 进行，kubectl 也不例外。kubectl 使用生成器（ generators）来构造 HTTP 请求。生成器是一个用来处理序列化的抽象概念。\n通过 kubectl run 不仅可以运行 deployment，还可以通过指定参数 --generator 来部署其他多种资源类型。如果没有指定 --generator 参数的值，kubectl 将会自动判断资源的类型。\n例如，带有参数 --restart-policy=Always 的资源将被部署为 Deployment，而带有参数 --restart-policy=Never 的资源将被部署为 Pod。同时 kubectl 也会检查是否需要触发其他操作，例如记录命令（用来进行回滚或审计）。\n在 kubectl 判断出要创建一个 Deployment 后，它将使用 DeploymentV1Beta1 生成器从我们提供的参数中生成一个 运行时对象。\nAPI 版本协商与 API 组 # 为了更容易地消除字段或者重新组织资源结构，Kubernetes 支持多个 API 版本，每个版本都在不同的 API 路径下，例如 /api/v1 或者 /apis/extensions/v1beta1。不同的 API 版本表明不同的稳定性和支持级别，更详细的描述可以参考 Kubernetes API 概述。\nAPI 组旨在对类似资源进行分类，以便使得 Kubernetes API 更容易扩展。API 的组名在 REST 路径或者序列化对象的 apiVersion 字段中指定。例如，Deployment 的 API 组名是 apps，最新的 API 版本是 v1beta2，这就是为什么你要在 Deployment manifests 顶部输入 apiVersion: apps/v1beta2。\nkubectl 在生成运行时对象后，开始为它 找到适当的 API 组和 API 版本，然后 组装成一个版本化客户端，该客户端知道资源的各种 REST 语义。该阶段被称为版本协商，kubectl 会扫描 remote API 上的 /apis 路径来检索所有可能的 API 组。由于 kube-apiserver 在 /apis 路径上公开了 OpenAPI 格式的规范文档， 因此客户端很容易找到合适的 API。\n为了提高性能，kubectl 将 OpenAPI 规范缓存到了 ~/.kube/cache 目录。如果你想了解 API 发现的过程，请尝试删除该目录并在运行 kubectl 命令时将 -v 参数的值设为最大值，然后你将会看到所有试图找到这些 API 版本的HTTP 请求。参考 kubectl 备忘单。\n最后一步才是真正地发送 HTTP 请求。一旦请求发送之后获得成功的响应，kubectl 将会根据所需的输出格式打印 success message。\n客户端身份认证 # 在发送 HTTP 请求之前还要进行客户端认证，这是之前没有提到的，现在可以来看一下。\n为了能够成功发送请求，kubectl 需要先进行身份认证。用户凭证保存在 kubeconfig 文件中，kubectl 通过以下顺序来找到 kubeconfig 文件：\n如果提供了 --kubeconfig 参数， kubectl 就使用 \u0026ndash;kubeconfig 参数提供的 kubeconfig 文件。\n如果没有提供 \u0026ndash;kubeconfig 参数，但设置了环境变量 $KUBECONFIG，则使用该环境变量提供的 kubeconfig 文件。\n如果 \u0026ndash;kubeconfig 参数和环境变量 $KUBECONFIG 都没有提供，kubectl 就使用默认的 kubeconfig 文件 $HOME/.kube/config。\n解析完 kubeconfig 文件后，kubectl 会确定当前要使用的上下文、当前指向的群集以及与当前用户关联的任何认证信息。如果用户提供了额外的参数（例如 \u0026ndash;username），则优先使用这些参数覆盖 kubeconfig 中指定的值。一旦拿到这些信息之后， kubectl 就会把这些信息填充到将要发送的 HTTP 请求头中：\nx509 证书使用 tls.TLSConfig 发送（包括 CA 证书）。\nbearer tokens 在 HTTP 请求头 Authorization 中 发送。\n用户名和密码通过 HTTP 基本认证 发送。\nOpenID 认证过程是由用户事先手动处理的，产生一个像 bearer token 一样被发送的 token。\nkube-apiserver # 认证 # 现在我们的请求已经发送成功了，接下来将会发生什么？这时候就该 kube-apiserver 闪亮登场了！kube-apiserver 是客户端和系统组件用来保存和检索集群状态的主要接口。为了执行相应的功能，kube-apiserver 需要能够验证请求者是合法的，这个过程被称为认证。\n那么 apiserver 如何对请求进行认证呢？当 kube-apiserver 第一次启动时，它会查看用户提供的所有 CLI 参数，并组合成一个合适的令牌列表。\n举个例子 : 如果提供了 --client-ca-file 参数，则会将 x509 客户端证书认证添加到令牌列表中；如果提供了 --token-auth-file 参数，则会将 breaer token 添加到令牌列表中。\n每次收到请求时，apiserver 都会 通过令牌链进行认证，直到某一个认证成功为止：\nx509 处理程序将验证 HTTP 请求是否是由 CA 根证书签名的 TLS 密钥进行编码的。\nbearer token 处理程序将验证 --token-auth-file 参数提供的 token 文件是否存在。\n基本认证处理程序确保 HTTP 请求的基本认证凭证与本地的状态匹配。\n如果 认证失败，则请求失败并返回相应的错误信息；如果验证成功，则将请求中的 Authorization 请求头删除，并 将用户信息添加到其上下文中。这给后续的授权和准入控制器提供了访问之前建立的用户身份的能力。\n授权 # OK，现在请求已经发送，并且 kube-apiserver 已经成功验证我们是谁，终于解脱了！\n然而事情并没有结束，虽然我们已经证明了我们是合法的，但我们有权执行此操作吗？毕竟身份和权限不是一回事。为了进行后续的操作，kube-apiserver 还要对用户进行授权。\nkube-apiserver 处理授权的方式与处理身份验证的方式相似：通过 kube-apiserver 的启动参数 --authorization_mode 参数设置。它将组合一系列授权者，这些授权者将针对每个传入的请求进行授权。如果所有授权者都拒绝该请求，则该请求会被禁止响应并且 不会再继续响应。如果某个授权者批准了该请求，则请求继续。\nkube-apiserver 目前支持以下几种授权方法：\nwebhook: 它与集群外的 HTTP(S) 服务交互。\nABAC: 它执行静态文件中定义的策略。\nRBAC: 它使用 rbac.authorization.k8s.io API Group实现授权决策，允许管理员通过 Kubernetes API 动态配置策略。\nNode: 它确保 kubelet 只能访问自己节点上的资源。\n准入控制 # 突破了之前所说的认证和授权两道关口之后，客户端的调用请求就能够得到 API Server 的真正响应了吗？答案是：不能！\n从 kube-apiserver 的角度来看，它已经验证了我们的身份并且赋予了相应的权限允许我们继续，但对于 Kubernetes 而言，其他组件对于应不应该允许发生的事情还是很有意见的。所以这个请求还需要通过 Admission Controller 所控制的一个 准入控制链 的层层考验，官方标准的 “关卡” 有近十个之多，而且还能自定义扩展！\n虽然授权的重点是回答用户是否有权限，但准入控制器会拦截请求以确保它符合集群的更广泛的期望和规则。它们是资源对象保存到 etcd 之前的最后一个堡垒，封装了一系列额外的检查以确保操作不会产生意外或负面结果。不同于授权和认证只关心请求的用户和操作，准入控制还处理请求的内容，并且仅对创建、更新、删除或连接（如代理）等有效，而对读操作无效。\n准入控制器的工作方式与授权者和验证者的工作方式类似，但有一点区别：与验证链和授权链不同，如果某个准入控制器检查不通过，则整个链会中断，整个请求将立即被拒绝并且返回一个错误给终端用户。 准入控制器设计的重点在于提高可扩展性，某个控制器都作为一个插件存储在 plugin/pkg/admission 目录中，并且与某一个接口相匹配，最后被编译到 kube-apiserver 二进制文件中。\n大部分准入控制器都比较容易理解，接下来着重介绍 SecurityContextDeny、ResourceQuota 及 LimitRanger 这三个准入控制器。\nSecurityContextDeny 该插件将禁止创建设置了 Security Context 的 Pod。 ResourceQuota 不仅能限制某个 Namespace 中创建资源的数量，而且能限制某个 Namespace 中被 Pod 所请求的资源总量。该准入控制器和资源对象 ResourceQuota 一起实现了资源配额管理。 LimitRanger 作用类似于上面的 ResourceQuota 控制器，针对 Namespace 资源的每个个体（Pod 与 Container 等）的资源配额。该插件和资源对象 LimitRange 一起实现资源配额管理。 etcd # 到现在为止，Kubernetes 已经对该客户端的调用请求进行了全面彻底地审查，并且已经验证通过，运行它进入下一个环节。下一步 kube-apiserver 将对 HTTP 请求进行反序列化，然后利用得到的结果构建运行时对象（有点像 kubectl 生成器的逆过程），并保存到 etcd 中。下面我们将这个过程分解一下。\n当收到请求时，kube-apiserver 是如何知道它该怎么做的呢？事实上，在客户端发送调用请求之前就已经产生了一系列非常复杂的流程。我们就从 kube-apiserver 二进制文件首次运行开始分析吧：\n当运行 kube-apiserver 二进制文件时，它会 创建一个允许 apiserver 聚合的服务链。这是一种对 Kubernetes API 进行扩展的方式。\n同时会创建一个 generic apiserver 作为默认的 apiserver。\n然后利用 生成的 OpenAPI 规范来填充 apiserver 的配置。\n然后 kube-apiserver 遍历数据结构中指定的所有 API 组，并将每一个 API 组作为通用的存储抽象保存到 etcd 中。当你访问或变更资源状态时，kube-apiserver 就会调用这些 API 组。\n每个 API 组都会遍历它的所有组版本，并且将每个 HTTP 路由 映射到 REST 路径中。\n当请求的 METHOD 是 POST 时，kube-apiserver 就会将请求转交给 资源创建处理器。 现在 kube-apiserver 已经知道了所有的路由及其对应的 REST 路径，以便在请求匹配时知道调用哪些处理器和键值存储。多么机智的设计！现在假设客户端的 HTTP 请求已经被 kube-apiserver 收到了：\n如果处理链可以将请求与已经注册的路由进行匹配，就会将该请求交给注册到该路由的 专用处理器来处理；如果没有任何一个路由可以匹配该请求，就会将请求转交给 基于路径的处理器（比如当调用 /apis 时）；如果没有任何一个基于路径的处理器注册到该路径，请求就会被转交给 not found 处理器，最后返回 404。\n幸运的是，我们有一个名为 createHandler 的注册路由！它有什么作用呢？首先它会解码 HTTP 请求并进行基本的验证，例如确保请求提供的 json 与 API 资源的版本相匹配。\n接下来进入 审计和准入控制阶段。\n然后资源将会通过 storage provider 保存 到 etcd 中。默认情况下保存到 etcd 中的键的格式为 \u0026lt;namespace\u0026gt;/\u0026lt;name\u0026gt;，你也可以自定义。\n资源创建过程中出现的任何错误都会被捕获，最后 storage provider 会执行 get 调用来确认该资源是否被成功创建。如果需要额外的清理工作，就会调用后期创建的处理器和装饰器。\n最后构造 HTTP 响应并返回给客户端。 原来 apiserver 做了这么多的工作，以前竟然没有发现呢！到目前为止，我们创建的 Deployment 资源已经保存到了 etcd 中，但 apiserver 仍然看不到它。\n初始化 # 在一个资源对象被持久化到数据存储之后，apiserver 还无法完全看到或调度它，在此之前还要执行一系列 Initializers。Initializers是一种与资源类型相关联的控制器，它会在资源对外可用之前执行某些逻辑。如果某个资源类型没有Initializers，就会跳过此初始化步骤立即使资源对外可见。\n正如 大佬的博客指出的那样，Initializers是一个强大的功能，因为它允许我们执行通用引导操作。例如：\n将代理边车容器注入到暴露 80 端口的 Pod 中，或者加上特定的 annotation。\n将保存着测试证书的 volume 注入到特定命名空间的所有 Pod 中。\n如果 Secret 中的密码小于 20 个字符，就组织其创建。\ninitializerConfiguration 资源对象允许你声明某些资源类型应该运行哪些Initializers。如果你想每创建一个 Pod 时就运行一个自定义Initializers，你可以这样做：\napiVersion: admissionregistration.k8s.io/v1alpha1 kind: InitializerConfiguration metadata: name: custom-pod-initializer initializers: - name: podimage.example.com rules: - apiGroups: - \u0026#34;\u0026#34; apiVersions: - v1 resources: - pods 通过该配置创建资源对象 InitializerConfiguration 之后，就会在每个 Pod 的 metadata.initializers.pending 字段中添加 custom-pod-initializer 字段。该初始化控制器会定期扫描新的 Pod，一旦在 Pod 的 pending 字段中检测到自己的名称，就会执行其逻辑，执行完逻辑之后就会将 pending 字段下的自己的名称删除。\n只有在 pending 字段下的列表中的第一个Initializers可以对资源进行操作，当所有的Initializers执行完成，并且 pending 字段为空时，该对象就会被认为初始化成功。\n你可能会注意到一个问题：如果 kube-apiserver 不能显示这些资源，那么用户级控制器是如何处理资源的呢？\n为了解决这个问题，kube-apiserver 暴露了一个 ?includeUninitialized 查询参数，它会返回所有的资源对象（包括未初始化的）。\n控制循环 # Deployments controller # 到了这个阶段，我们的 Deployment 记录已经保存在 etcd 中，并且所有的初始化逻辑都执行完成，接下来的阶段将会涉及到该资源所依赖的拓扑结构。在 Kubernetes 中，Deployment 实际上只是一系列 Replicaset 的集合，而 Replicaset 是一系列 Pod 的集合。那么 Kubernetes 是如何从一个 HTTP 请求按照层级结构依次创建这些资源的呢？其实这些工作都是由 Kubernetes 内置的 Controller(控制器) 来完成的。\nKubernetes 在整个系统中使用了大量的 Controller，Controller 是一个用于将系统状态从“当前状态”修正到“期望状态”的异步脚本。所有 Controller 都通过 kube-controller-manager 组件并行运行，每种 Controller 都负责一种具体的控制流程。首先介绍一下 Deployment Controller：\n将 Deployment 记录存储到 etcd 并初始化后，就可以通过 kube-apiserver 使其可见，然后 Deployment Controller 就会检测到它（它的工作就是负责监听 Deployment 记录的更改）。在我们的例子中，控制器通过一个 Informer 注册一个创建事件的特定回调函数（更多信息参加下文）。\n当 Deployment 第一次对外可见时，该 Controller 就会 将该资源对象添加到内部工作队列，然后开始处理这个资源对象：\n通过使用标签选择器查询 kube-apiserver 来 检查该 Deployment 是否有与其关联的 ReplicaSet 或 Pod 记录。\n有趣的是，这个同步过程是状态不可知的，它核对新记录与核对已经存在的记录采用的是相同的方式。\n在意识到没有与其关联的 ReplicaSet 或 Pod 记录后，Deployment Controller 就会开始执行 弹性伸缩流程：\n创建 ReplicaSet 资源，为其分配一个标签选择器并将其版本号设置为 1。\nReplicaSet 的 PodSpec 字段从 Deployment 的 manifest 以及其他相关元数据中复制而来。有时 Deployment 记录在此之后也需要更新（例如，如果设置了 process deadline）。\n当完成以上步骤之后，该 Deployment 的 status 就会被更新，然后重新进入与之前相同的循环，等待 Deployment 与期望的状态相匹配。由于 Deployment Controller 只关心 ReplicaSet，因此需要通过 ReplicaSet Controller 来继续协调。\nReplicaSets controller # 在前面的步骤中，Deployment Controller 创建了第一个 ReplicaSet，但仍然还是没有 Pod，这时候就该 ReplicaSet Controller 登场了！ReplicaSet Controller 的工作是监视 ReplicaSets 及其相关资源（Pod）的生命周期。和大多数其他 Controller 一样，它通过触发某些事件的处理器来实现此目的。\n当创建 ReplicaSet 时（由 Deployment Controller 创建），RS Controller 检查新 ReplicaSet 的状态，并检查当前状态与期望状态之间存在的偏差，然后通过 调整 Pod 的副本数来达到期望的状态。\nPod 的创建也是批量进行的，从 SlowStartInitialBatchSize 开始，然后在每次成功的迭代中以一种 slow start 操作加倍。这样做的目的是在大量 Pod 启动失败时（例如，由于资源配额），可以减轻 kube-apiserver 被大量不必要的 HTTP 请求吞没的风险。如果创建失败，最好能够优雅地失败，并且对其他的系统组件造成的影响最小！\nKubernetes 通过 Owner References（在子级资源的某个字段中引用其父级资源的 ID） 来构造严格的资源对象层级结构。这确保了一旦 Controller 管理的资源被删除（级联删除），子资源就会被垃圾收集器删除，同时还为父级资源提供了一种有效的方式来避免他们竞争同一个子级资源（想象两对父母都认为他们拥有同一个孩子的场景）。\nOwner References 的另一个好处是：它是有状态的。如果有任何 Controller 重启了，那么由于资源对象的拓扑关系与 Controller 无关，该操作不会影响到系统的稳定运行。这种对资源隔离的重视也体现在 Controller 本身的设计中：Controller 不能对自己没有明确拥有的资源进行操作，它们应该选择对资源的所有权，互不干涉，互不共享。\n有时系统中也会出现孤儿（orphaned）资源，通常由以下两种途径产生：\n父级资源被删除，但子级资源没有被删除\n垃圾收集策略禁止删除子级资源\n当发生这种情况时，Controller 将会确保孤儿资源拥有新的 Owner。多个父级资源可以相互竞争同一个孤儿资源，但只有一个会成功（其他父级资源会收到验证错误）。\nInformers # 你可能已经注意到，某些 Controller（例如 RBAC 授权器或 Deployment Controller）需要先检索集群状态然后才能正常运行。拿 RBAC 授权器举例，当请求进入时，授权器会将用户的初始状态缓存下来，然后用它来检索与 etcd 中的用户关联的所有 角色（Role）和 角色绑定（RoleBinding）。那么问题来了，Controller 是如何访问和修改这些资源对象的呢？事实上 Kubernetes 是通过 Informer 机制来解决这个问题的。\nInfomer 是一种模式，它允许 Controller 查找缓存在本地内存中的数据(这份数据由 Informer 自己维护)并列出它们感兴趣的资源。\n虽然 Informer 的设计很抽象，但它在内部实现了大量的对细节的处理逻辑（例如缓存），缓存很重要，因为它不但可以减少对 Kubenetes API 的直接调用，同时也能减少 Server 和 Controller 的大量重复性工作。通过使用 Informer，不同的 Controller 之间以线程安全（Thread safety）的方式进行交互，而不必担心多个线程访问相同的资源时会产生冲突。\n有关 Informer 的更多详细解析，请参考这篇文章： Kubernetes: Controllers, Informers, Reflectors and Stores\nScheduler # 当所有的 Controller 正常运行后，etcd 中就会保存一个 Deployment、一个 ReplicaSet 和 三个 Pod 资源记录，并且可以通过 kube-apiserver 查看。然而，这些 Pod 资源现在还处于 Pending 状态，因为它们还没有被调度到集群中合适的 Node 上运行。这个问题最终要靠调度器（Scheduler）来解决。\nScheduler 作为一个独立的组件运行在集群控制平面上，工作方式与其他 Controller 相同：监听实际并将系统状态调整到期望的状态。具体来说，Scheduler 的作用是将待调度的 Pod 按照特定的算法和调度策略绑定（Binding）到集群中某个合适的 Node 上，并将绑定信息写入 etcd 中（它会过滤其 PodSpec 中 NodeName 字段为空的 Pod），默认的调度算法的工作方式如下：\n当 Scheduler 启动时，会 注册一个默认的预选策略链，这些 预选策略 会对备选节点进行评估，判断备选节点是否 满足备选 Pod 的需求。例如，如果 PodSpec 字段限制了 CPU 和内存资源，那么当备选节点的资源容量不满足备选 Pod 的需求时，备选 Pod 就不会被调度到该节点上（资源容量=备选节点资源总量-节点中已存在 Pod 的所有容器的需求资源（CPU 和内存）的总和）\n一旦筛选出符合要求的候选节点，就会采用 优选策略 计算出每个候选节点的积分，然后对这些候选节点进行排序，积分最高者胜出。例如，为了在整个系统中分摊工作负载，这些优选策略会从备选节点列表中选出资源消耗最小的节点。每个节点通过优选策略时都会算出一个得分，计算各项得分，最终选出分值大的节点作为优选的结果。\n一旦找到了合适的节点，Scheduler 就会创建一个 Binding 对象，该对象的 Name 和 Uid 与 Pod 相匹配，并且其 ObjectReference 字段包含所选节点的名称，然后通过 POST 请求 发送给 apiserver。\n当 kube-apiserver 接收到此 Binding 对象时，注册吧会将该对象反序列化并更新 Pod 资源中的以下字段：\n将 NodeName 的值设置为 ObjectReference 中的 NodeName。\n添加相关的注释。\n将 PodScheduled 的 status 值设置为 True。可以通过 kubectl 来查看：\n$ kubectl get \u0026lt;PODNAME\u0026gt; -o go-template=\u0026#39;{{range .status.conditions}}{{if eq .type \u0026#34;PodScheduled\u0026#34;}}{{.status}}{{end}}{{end}}\u0026#39; 一旦 Scheduler 将 Pod 调度到某个节点上，该节点的 Kubelet 就会接管该 Pod 并开始部署。\n预选策略和优选策略都可以通过 \u0026ndash;policy-config-file 参数来扩展，如果默认的调度器不满足要求，还可以部署自定义的调度器。如果 podSpec.schedulerName 的值设置为其他的调度器，则 Kubernetes 会将该 Pod 的调度转交给那个调度器。 Kubelet # Pod 同步 # 现在，所有的 Controller 都完成了工作，我们来总结一下：\nHTTP 请求通过了认证、授权和准入控制阶段。\n一个 Deployment、ReplicaSet 和三个 Pod 资源被持久化到 etcd 存储中。\n然后运行了一系列Initializers。\n最后每个 Pod 都被调度到合适的节点。\n然而到目前为止，所有的状态变化仅仅只是针对保存在 etcd 中的资源记录，接下来的步骤涉及到运行在工作节点之间的 Pod 的分布状况，这是分布式系统（比如 Kubernetes）的关键因素。这些任务都是由 Kubelet 组件完成的，让我们开始吧！\n在 Kubernetes 集群中，每个 Node 节点上都会启动一个 Kubelet 服务进程，该进程用于处理 Scheduler 下发到本节点的任务，管理 Pod 的生命周期，包括挂载卷、容器日志记录、垃圾回收以及其他与 Pod 相关的事件。\n如果换一种思维模式，你可以把 Kubelet 当成一种特殊的 Controller，它每隔 20 秒（可以自定义）向 kube-apiserver 通过 NodeName 获取自身 Node 上所要运行的 Pod 清单。一旦获取到了这个清单，它就会通过与自己的内部缓存进行比较来检测新增加的 Pod，如果有差异，就开始同步 Pod 列表。我们来详细分析一下同步过程：\n如果 Pod 正在创建， Kubelet 就会 记录一些在 Prometheus 中用于追踪 Pod 启动延时的指标。\n然后生成一个 PodStatus 对象，它表示 Pod 当前阶段的状态。Pod 的状态(Phase) 是 Pod 在其生命周期中的最精简的概要，包括 Pending，Running，Succeeded，Failed 和 Unkown 这几个值。状态的产生过程非常过程，所以很有必要深入了解一下背后的原理：\n首先串行执行一系列 Pod 同步处理器（PodSyncHandlers），每个处理器检查检查 Pod 是否应该运行在该节点上。当所有的处理器都认为该 Pod 不应该运行在该节点上，则 Pod 的 Phase 值就会变成 PodFailed，并且将该 Pod 从该节点上驱逐出去。例如当你创建一个 Job 时，如果 Pod 失败重试的时间超过了 spec.activeDeadlineSeconds 设置的值，就会将 Pod 从该节点驱逐出去。\n接下来，Pod 的 Phase 值由 init 容器 和应用容器的状态共同来决定。因为目前容器还没有启动，容器被视为 处于等待阶段，如果 Pod 中至少有一个容器处于等待阶段，则其 Phase 值为 Pending。\n最后，Pod 的 Condition 字段由 Pod 内所有容器的状态决定。现在我们的容器还没有被容器运行时创建，所以 PodReady 的状态被设置为 False。可以通过 kubectl 查看：\n$ kubectl get \u0026lt;PODNAME\u0026gt; -o go-template=\u0026#39;{{range .status.conditions}}{{if eq .type \u0026#34;Ready\u0026#34;}}{{.status}}{{end}}{{end}}\u0026#39; 生成 PodStatus 之后（Pod 中的 status 字段），Kubelet 就会将它发送到 Pod 的状态管理器，该管理器的任务是通过 apiserver 异步更新 etcd 中的记录。\n接下来运行一系列准入处理器来确保该 Pod 是否具有相应的权限（包括强制执行 AppArmor 配置文件和 NO_NEW_PRIVS），被准入控制器拒绝的 Pod 将一直保持 Pending 状态。\n如果 Kubelet 启动时指定了 cgroups-per-qos 参数，Kubelet 就会为该 Pod 创建 cgroup 并进行相应的资源限制。这是为了更方便地对 Pod 进行服务质量（QoS）管理。\n然后为 Pod 创建相应的目录，包括 Pod 的目录（/var/run/kubelet/pods/\u0026lt;podID\u0026gt;），该 Pod 的卷目录（\u0026lt;podDir\u0026gt;/volumes）和该 Pod 的插件目录（\u0026lt;podDir\u0026gt;/plugins）。\n卷管理器会 挂载 Spec.Volumes 中定义的相关数据卷，然后等待是否挂载成功。根据挂载卷类型的不同，某些 Pod 可能需要等待更长的时间（比如 NFS 卷）。\n从 apiserver 中检索 Spec.ImagePullSecrets 中定义的所有 Secret，然后将其注入到容器中。\n最后通过容器运行时接口（Container Runtime Interface（CRI））开始启动容器（下面会详细描述）。\nCRI 与 pause 容器 # 到了这个阶段，大量的初始化工作都已经完成，容器已经准备好开始启动了，而容器是由容器运行时（例如 Docker 和 Rkt）启动的。\n为了更容易扩展，Kubelet 从 1.5.0 开始通过容器运行时接口与容器运行时（Container Runtime）交互。简而言之，CRI 提供了 Kubelet 和特定的运行时之间的抽象接口，它们之间通过 协议缓冲区（它像一个更快的 JSON）和 gRPC API（一种非常适合执行 Kubernetes 操作的 API）。这是一个非常酷的想法，通过使用 Kubelet 和运行时之间定义的契约关系，容器如何编排的具体实现细节已经变得无关紧要。由于不需要修改 Kubernetes 的核心代码，开发者可以以最小的开销添加新的运行时。\n不好意思有点跑题了，让我们继续回到容器启动的阶段。第一次启动 Pod 时，Kubelet 会通过 Remote Procedure Command(RPC) 协议调用 RunPodSandbox。sandbox 用于描述一组容器，例如在 Kubernetes 中它表示的是 Pod。sandbox 是一个很宽泛的概念，所以对于其他没有使用容器的运行时仍然是有意义的（比如在一个基于 hypervisor 的运行时中，sandbox 可能指的就是虚拟机）。\n我们的例子中使用的容器运行时是 Docker，创建 sandbox 时首先创建的是 pause 容器。pause 容器作为同一个 Pod 中所有其他容器的基础容器，它为 Pod 中的每个业务容器提供了大量的 Pod 级别资源，这些资源都是 Linux 命名空间（包括网络命名空间，IPC 命名空间和 PID 命名空间）。\npause 容器提供了一种方法来管理所有这些命名空间并允许业务容器共享它们，在同一个网络命名空间中的好处是：同一个 Pod 中的容器可以使用 localhost 来相互通信。pause 容器的第二个功能与 PID 命名空间的工作方式相关，在 PID 命名空间中，进程之间形成一个树状结构，一旦某个子进程由于父进程的错误而变成了“孤儿进程”，其便会被 init 进程进行收养并最终回收资源。关于 pause 工作方式的详细信息可以参考： The Almighty Pause Container。\n一旦创建好了 pause 容器，下面就会开始检查磁盘状态然后开始启动业务容器。\nCNI 和 Pod 网络 # 现在我们的 Pod 已经有了基本的骨架：一个共享所有命名空间以允许业务容器在同一个 Pod 里进行通信的 pause 容器。但现在还有一个问题，那就是容器的网络是如何建立的？\n当 Kubelet 为 Pod 创建网络时，它会将创建网络的任务交给 CNI 插件。CNI 表示容器网络接口（Container Network Interface），和容器运行时的运行方式类似，它也是一种抽象，允许不同的网络提供商为容器提供不同的网络实现。通过将 json 配置文件（默认在 /etc/cni/net.d 路径下）中的数据传送到相关的 CNI 二进制文件（默认在 /opt/cni/bin 路径下）中，cni 插件可以给 pause 容器配置相关的网络，然后 Pod 中其他的容器都使用 pause 容器的网络。下面是一个简单的示例配置文件：\n{ \u0026#34;cniVersion\u0026#34;: \u0026#34;0.3.1\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;bridge\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;bridge\u0026#34;, \u0026#34;bridge\u0026#34;: \u0026#34;cnio0\u0026#34;, \u0026#34;isGateway\u0026#34;: true, \u0026#34;ipMasq\u0026#34;: true, \u0026#34;ipam\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;host-local\u0026#34;, \u0026#34;ranges\u0026#34;: [ [{\u0026#34;subnet\u0026#34;: \u0026#34;${POD_CIDR}\u0026#34;}] ], \u0026#34;routes\u0026#34;: [{\u0026#34;dst\u0026#34;: \u0026#34;0.0.0.0/0\u0026#34;}] } } CNI 插件还会通过 CNI_ARGS 环境变量为 Pod 指定其他的元数据，包括 Pod 名称和命名空间。\n下面的步骤因 CNI 插件而异，我们以 bridge 插件举例：\n该插件首先会在根网络命名空间（也就是宿主机的网络命名空间）中设置本地 Linux 网桥，以便为该主机上的所有容器提供网络服务。\n然后它会将一个网络接口（veth 设备对的一端）插入到 pause 容器的网络命名空间中，并将另一端连接到网桥上。你可以这样来理解 veth 设备对：它就像一根很长的管道，一端连接到容器，一端连接到根网络命名空间中，数据包就在管道中进行传播。\n接下来 json 文件中指定的 IPAM Plugin 会为 pause 容器的网络接口分配一个 IP 并设置相应的路由，现在 Pod 就有了自己的 IP。\nIPAM Plugin 的工作方式和 CNI Plugin 类似：通过二进制文件调用并具有标准化的接口，每一个 IPAM Plugin 都必须要确定容器网络接口的 IP、子网以及网关和路由，并将信息返回给 CNI 插件。最常见的 IPAM Plugin 是 host-local，它从预定义的一组地址池中为容器分配 IP 地址。它将地址池的信息以及分配信息保存在主机的文件系统中，从而确保了同一主机上每个容器的 IP 地址的唯一性。 最后 Kubelet 会将集群内部的 DNS 服务器的 Cluster IP 地址传给 CNI 插件，然后 CNI 插件将它们写到容器的 /etc/resolv.conf 文件中。\n一旦完成了上面的步骤，CNI 插件就会将操作的结果以 json 的格式返回给 Kubelet。\n跨主机容器网络 # 到目前为止，我们已经描述了容器如何与宿主机进行通信，但跨主机之间的容器如何通信呢？\n通常情况下使用 overlay 网络来进行跨主机容器通信，这是一种动态同步多个主机间路由的方法。 其中最常用的 overlay 网络插件是 flannel，flannel 具体的工作方式可以参考 CoreOS 的文档。\n容器启动 # 所有网络都配置完成后，接下来就开始真正启动业务容器了！\n一旦 sanbox 完成初始化并处于 active 状态，Kubelet 就可以开始为其创建容器了。首先 启动 PodSpec 中定义的 init 容器，然后再启动业务容器。具体过程如下：\n首先拉取容器的镜像。如果是私有仓库的镜像，就会利用 PodSpec 中指定的 Secret 来拉取该镜像。\n然后通过 CRI 接口创建容器。Kubelet 向 PodSpec 中填充了一个 ContainerConfig 数据结构（在其中定义了命令，镜像，标签，挂载卷，设备，环境变量等待），然后通过 protobufs 发送给 CRI 接口。对于 Docker 来说，它会将这些信息反序列化并填充到自己的配置信息中，然后再发送给 Dockerd 守护进程。在这个过程中，它会将一些元数据标签（例如容器类型，日志路径，dandbox ID 等待）添加到容器中。\n接下来会使用 CPU 管理器来约束容器，这是 Kubelet 1.8 中新添加的 alpha 特性，它使用 UpdateContainerResources CRI 方法将容器分配给本节点上的 CPU 资源池。\n最后容器开始真正 启动。\n如果 Pod 中配置了容器生命周期钩子（Hook），容器启动之后就会运行这些 Hook。Hook 的类型包括两种：Exec（执行一段命令） 和 HTTP（发送HTTP请求）。如果 PostStart Hook 启动的时间过长、挂起或者失败，容器将永远不会变成 running 状态。 总结 # 如果上面一切顺利，现在你的集群上应该会运行三个容器，所有的网络，数据卷和秘钥都被通过 CRI 接口添加到容器中并配置成功。\n上文所述的创建 Pod 整个过程的流程图如下所示：\nKubelet 创建 Pod 的流程 原文链接 # What happens when \u0026hellip; Kubernetes edition! ","date":"2018年6月1日","externalUrl":null,"permalink":"/posts/what-happens-when-k8s/","section":"博客","summary":"想象一下，如果我想将 nginx 部署到 Kubernetes 集群，我可能会在终端中输入类似","title":"kubectl 创建 Pod 背后到底发生了什么？","type":"posts"},{"content":"Github 地址：Linux 和 MacOS 设备智能分流方案\n本来我是决定不再写这样的文章了的。但是呢，最近连续配置了两次 ArchLinux，在配置这种东西的时候连续撞到了同样的坑，加上这段时间经常有人问我关于 Linux 下的 shadowsocks 的问题，所以我想了想还是写一篇记录一下吧，也免得自己以后再忘记了。\n这里有两种方案，都可以实现全局智能分流。第一种方案的思路是使用 ipset 载入 chnroute 的 IP 列表并使用 iptables 实现带自动分流国内外流量的全局代理。为什么不用 PAC 呢？因为 PAC 这种东西只对浏览器有用。难道你在浏览器之外就不需要科学上网了吗？反正我是不信的……\n本教程所用系统为 Archlinux，其他发型版类似，请自行参考相关资料。\n通过 iptables 实现智能分流 # 安装相关软件 # shadowsocks-libev ipset $ pacman -S shadowsocks-libev ipset 配置shadowsocks-libev（略过） # 假设shadowsocks配置文件为/etc/shadowsocks.json\n获取中国IP段 # 将以下命令写入脚本保存执行（假设保存在/home/yang/bin/路由表/目录下）：\n#!/bin/sh wget -c http://ftp.apnic.net/stats/apnic/delegated-apnic-latest cat delegated-apnic-latest | awk -F \u0026#39;|\u0026#39; \u0026#39;/CN/\u0026amp;\u0026amp;/ipv4/ {print $4 \u0026#34;/\u0026#34; 32-log($5)/log(2)}\u0026#39; | cat \u0026gt; /home/yang/bin/路由表/cn_rules.conf 创建启动和关闭脚本 # $ vim /home/yang/bin/shadowsocks/ss-up.sh #!/bin/bash SOCKS_SERVER=$SERVER_IP # SOCKS 服务器的 IP 地址 # Setup the ipset ipset -N chnroute hash:net maxelem 65536 for ip in $(cat \u0026#39;/home/yang/bin/路由表/cn_rules.conf\u0026#39;); do ipset add chnroute $ip done # 在nat表中新增一个链，名叫：SHADOWSOCKS iptables -t nat -N SHADOWSOCKS # Allow connection to the server iptables -t nat -A SHADOWSOCKS -d $SOCKS_SERVER -j RETURN # Allow connection to reserved networks iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN # Allow connection to chinese IPs iptables -t nat -A SHADOWSOCKS -p tcp -m set --match-set chnroute dst -j RETURN # 如果你想对 icmp 协议也实现智能分流，可以加上下面这一条 # iptables -t nat -A SHADOWSOCKS -p icmp -m set --match-set chnroute dst -j RETURN # Redirect to Shadowsocks # 把1081改成你的shadowsocks本地端口 iptables -t nat -A SHADOWSOCKS -p tcp -j REDIRECT --to-port 1081 # 如果你想对 icmp 协议也实现智能分流，可以加上下面这一条 # iptables -t nat -A SHADOWSOCKS -p icmp -j REDIRECT --to-port 1081 # 将SHADOWSOCKS链中所有的规则追加到OUTPUT链中 iptables -t nat -A OUTPUT -p tcp -j SHADOWSOCKS # 如果你想对 icmp 协议也实现智能分流，可以加上下面这一条 # iptables -t nat -A OUTPUT -p icmp -j SHADOWSOCKS # 内网流量流经 shadowsocks 规则链 iptables -t nat -A PREROUTING -s 192.168/16 -j SHADOWSOCKS # 内网流量源NAT iptables -t nat -A POSTROUTING -s 192.168/16 -j MASQUERADE 这是在启动 shadowsocks 之前执行的脚本，用来设置 iptables 规则，对全局应用代理并将 chnroute 导入 ipset 来实现自动分流。注意要把服务器 IP 和本地端口相关的代码全部替换成你自己的。\n这里就有一个坑了，就是在把 chnroute.txt 加入 ipset 的时候。因为 chnroute.txt 是一个 IP 段列表，而中国持有的 IP 数量上还是比较大的，所以如果使用 hash:ip 来导入的话会使内存溢出。我在第二次重新配置的时候就撞进了这个大坑……\n但是你也不能尝试把整个列表导入 iptables。虽然导入 iptables 不会导致内存溢出，但是 iptables 是线性查表，即使你全部导入进去，也会因为低下的性能而抓狂。\n然后再创建 /home/yang/bin/shadowsocks/ss-down.sh, 这是用来清除上述规则的脚本，比较简单\n#!/bin/bash # iptables -t nat -D OUTPUT -p icmp -j SHADOWSOCKS iptables -t nat -D OUTPUT -p tcp -j SHADOWSOCKS iptables -t nat -F SHADOWSOCKS iptables -t nat -X SHADOWSOCKS ipset destroy chnroute 接着执行\n$ chmod +x ss-up.sh $ chmod +x ss-down.sh 配置ss-redir服务 # 首先，默认的 ss-local 并不能用来作为 iptables 流量转发的目标，因为它是 socks5 代理而非透明代理。我们至少要把 systemd 执行的程序改成 ss-redir。其次，上述两个脚本还不能自动执行，必须让 systemd 分别在启动 shadowsocks 之前和关闭之后将脚本执行，这样才能自动配置好 iptables 规则。\n$ vim /usr/lib/systemd/system/shadowsocks-libev@.service [Unit] Description=Shadowsocks-Libev Client Service After=network.target [Service] User=root CapabilityBoundingSet=~CAP_SYS_ADMIN ExecStart= ExecStartPre=/home/yang/bin/shadowsocks/ss-up.sh ExecStart=/usr/bin/ss-redir -u -c /etc/%i.json ExecStopPost=/home/yang/bin/shadowsocks/ss-down.sh [Install] WantedBy=multi-user.target 然后启动服务\n$ systemctl start shadowsocks-libev@shadowsocks 开机自启\n$ systemctl enable shadowsocks-libev@shadowsocks 配置智能 DNS 服务 # 完成了以上工作之后是不是就可以实现全局科学上网了呢？答案是否定的，我们还有最后一项工作需要完成，那就是解决 DNS 污染问题。如果你不知道什么是 DNS 污染，我可以简单地给你普及一下：\nDNS 污染是一种让一般用户由于得到虚假目标主机 IP 而不能与其通信的方法，是一种 DNS 缓存投毒攻击（DNS cache poisoning）。其工作方式是：由于通常的 DNS 查询没有任何认证机制，而且 DNS 查询通常基于的 UDP 是无连接不可靠的协议，因此 DNS 的查询非常容易被篡改，通过对 UDP 端口 53 上的 DNS 查询进行入侵检测，一经发现与关键词相匹配的请求则立即伪装成目标域名的解析服务器（NS，Name Server）给查询者返回虚假结果。\nDNS 污染症状：目前一些被禁止访问的网站很多就是通过 DNS 污染来实现的，例如 YouTube、Facebook 等网站。\n应对dns污染的方法\n对于 DNS 污染，可以说，个人用户很难单单靠设置解决，通常可以使用 VPN 或者域名远程解析的方法解决，但这大多需要购买付费的 VPN 或 SSH 等 修改 Hosts 的方法，手动设置域名正确的 IP 地址 dns 加密解析： DNSCrypt 忽略 DNS 投毒污染小工具： Pcap_DNSProxy 我们选择用 Pcap_DNSProxy 来解决这个问题，以前用的是 Pdnsd + Dnsmasq 组合， 后来发现 TCP 请求效率太低加上家里网络与那些国外的 DNS 丢包实在是严重， 所以打算用 Pcap_DNSProxy 代替 Pdnsd。\n关于 Pcap_DNSProxy 的详细介绍，可以参考:\nhttps://github.com/chengr28/Pcap_DNSProxy\n安装过程可以参考：\nhttps://github.com/chengr28/Pcap_DNSProxy/blob/master/Documents/ReadMe_Linux.zh-Hans.txt\n更详细的使用说明可以参考：\nhttps://github.com/chengr28/Pcap_DNSProxy/blob/master/Documents/ReadMe.zh-Hans.txt\n这里主要重点强调一些需要注意的配置项：\nDNS - 境外域名解析参数区域（这是最关键的一项配置） [DNS] # 这里一定要填 IPv4 + TCP！！！表示只使用 TCP 协议向境外远程 DNS 服务器发出请求 Outgoing Protocol = IPv4 + TCP # 建议当系统使用全局代理功能时启用，程序将除境内服务器外的所有请求直接交给系统而不作任何过滤等处理，系统会将请求自动发往远程服务器进行解析 Direct Request = IPv4 ... ... Local DNS - 境内域名解析参数区域 [Local DNS] # 发送请求到境内 DNS 服务器时所使用的协议 Local Protocol = IPv4 + UDP ... ... Addresses - 普通模式地址区域 [Addresses] ... ... # IPv4 主要境外 DNS 服务器地址 IPv4 Main DNS Address = 8.8.4.4:53 # IPv4 备用境外 DNS 服务器地址 IPv4 Alternate DNS Address = 8.8.8.8:53|208.67.220.220:443|208.67.222.222:5353 # IPv4 主要境内 DNS 服务器地址，用于境内域名解析，推荐使用 onedns IPv4 Local Main DNS Address = 112.124.47.27:53 # IPv4 备用境内 DNS 服务器地址，用于境内域名解析 IPv4 Local Alternate DNS Address = 114.215.126.16:53 ... ... 配置系统 DNS 服务器设置 # 可参见 https://developers.google.com/speed/public-dns/docs/using 中 Changing your DNS servers settings 中 Linux 一节\n图形界面以 GNOME 3 为例：\n打开所有程序列表，并 -\u0026gt; 设置 – 硬件分类 – 网络\n如果要对当前的网络配置进行编辑 -\u0026gt; 单击齿轮按钮\n选中 IPv4\nDNS 栏目中，将自动拨向关闭\n在服务器中填入 127.0.0.1 （或103.214.195.99:7300）并应用\n选中 IPv6\nDNS 栏目中，将自动拨向关闭\n在服务器中填入 ::1 并应用\n请务必确保只填入这两个地址，填入其它地址可能会导致系统选择其它 DNS 服务器绕过程序的代理\n重启网络连接\n直接修改系统文件修改 DNS 服务器设置：\n自动获取地址(DHCP)时：\n以 root 权限进入 /etc/dhcp 或 /etc/dhcp3 目录（视乎 dhclient.conf 文件位置）\n直接修改 dhclient.conf 文件，修改或添加 prepend domain-name-servers 一项即可\n如果 prepend domain-name-servers 一项被 # 注释则需要把注释去掉以使配置生效，不需要添加新的条目\ndhclient.conf 文件可能存在多个 prepend domain-name-servers 项，是各个网络接口的配置项目，直接修改总的配置项目即可\n使用 service network(/networking) restart 或 ifdown/ifup 或 ifconfig stop/start 重启网络服务/网络端口\n非自动获取地址(DHCP)时：\n以 root 权限进入 /etc 目录\n直接修改 resolv.conf 文件里的 nameserver 即可\n如果重启后配置被覆盖，则需要修改或新建 /etc/resolvconf/resolv.conf.d 文件，内容和 resolv.conf 一样\n使用 service network(/networking) restart 或 ifdown/ifup 或 ifconfig stop/start 重启网络服务/网络端口\n打开流量转发 # $ cat /etc/sysctl.d/30-ipforward.conf net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding = 1 net.ipv4.tcp_congestion_control=westwood net.ipv4.tcp_syn_retries = 5 net.ipv4.tcp_synack_retries = 5 编辑完成后，执行以下命令使变动立即生效\n$ sysctl -p 通过 nftables 实现智能分流 # 安装相关软件 # shadowsocks-libev nftables $ pacman -S shadowsocks-libev nftables 配置shadowsocks-libev（略过） # 假设shadowsocks配置文件为/etc/shadowsocks.json\n获取中国IP段 # 将以下命令写入脚本保存执行（假设保存在/home/yang/bin/路由表/目录下）：\n#!/bin/sh wget -c http://ftp.apnic.net/stats/apnic/delegated-apnic-latest cat delegated-apnic-latest | awk -F \u0026#39;|\u0026#39; \u0026#39;/CN/\u0026amp;\u0026amp;/ipv4/ {print $4 \u0026#34;/\u0026#34; 32-log($5)/log(2)}\u0026#39; | cat \u0026gt; /home/yang/bin/路由表/cn_rules.conf cat cn_rules.conf|sed \u0026#39;:label;N;s/\\n/, /;b label\u0026#39;|sed \u0026#39;s/$/\u0026amp; }/g\u0026#39;|sed \u0026#39;s/^/{ \u0026amp;/g\u0026#39; \u0026gt; /home/yang/bin/路由表/cn_rules1.conf 创建启动和关闭脚本 # $ vim /home/yang/bin/shadowsocks/nftables-up.sh #! /bin/bash nft_pre=\u0026#34;/usr/sbin/nft add rule nat prerouting\u0026#34; nft_out=\u0026#34;/usr/sbin/nft add rule nat output\u0026#34; chnroute=$(cat \u0026#39;/home/yang/bin/路由表/cn_rules1.conf\u0026#39;) /usr/bin/nft -f /etc/nftables.conf ${nft_pre} tcp dport 8385 return ${nft_pre} ip daddr 139.162.87.98 return ${nft_pre} ip daddr { 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4, 172.16.39.0/24} return ${nft_pre} ip daddr $chnroute return ${nft_pre} tcp sport { 32768-61000} redirect to 1081 #${nft_pre} ip protocol icmp redirect to 1081 # 内网流量源NAT nft add rule nat postrouting ip saddr 192.168.0.0/12 masquerade ${nft_out} tcp dport 8385 return ${nft_out} ip daddr 139.162.87.98 return ${nft_out} ip daddr { 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4, 172.16.39.0/24} return ${nft_out} ip daddr $chnroute return # /proc/sys/net/ipv4/ip_local_port_range，本地发起的连接的端口范围 ${nft_out} tcp sport { 32768-61000} redirect to 1081 ${nft_out} ip protocol icmp redirect to 1081 这是在启动 shadowsocks 之前执行的脚本，用来设置 nftables 规则。\n然后再创建 /home/yang/bin/shadowsocks/nftables-down.sh, 这是用来清除上述规则的脚本，比较简单\n#!/bin/bash sudo nft flush table nat #sudo nft flush table filter 接着执行\n$ chmod +x nftables-up.sh $ chmod +x nftables-down.sh 配置ss-redir服务 # 首先，默认的 ss-local 并不能用来作为 nftables 流量转发的目标，因为它是 socks5 代理而非透明代理。我们至少要把 systemd 执行的程序改成 ss-redir。其次，上述两个脚本还不能自动执行，必须让 systemd 分别在启动 shadowsocks 之前和关闭之后将脚本执行，这样才能自动配置好 nftables 规则。\n$ vim /usr/lib/systemd/system/shadowsocks-libev@.service [Unit] Description=Shadowsocks-Libev Client Service After=network.target [Service] User=root CapabilityBoundingSet=~CAP_SYS_ADMIN ExecStart= ExecStartPre=/home/yang/bin/shadowsocks/nftables-up.sh ExecStart=/usr/bin/ss-redir -u -c /etc/%i.json ExecStopPost=/home/yang/bin/shadowsocks/nftables-down.sh [Install] WantedBy=multi-user.target 然后启动服务\n$ systemctl start nftables $ systemctl start shadowsocks-libev@shadowsocks 开机自启\n$ systemctl enable nftables $ systemctl enable shadowsocks-libev@shadowsocks 配置智能 DNS 服务 # 同上\n配置系统 DNS 服务器设置 # 同上\n打开流量转发 # 同上\n通过策略路由实现智能分流 # 安装相关软件 # badvpn shadowsocks $ pacman -S badvpn shadowsocks 配置shadowsocks（略过） # 假设shadowsocks配置文件为/etc/shadowsocks.json\n获取中国IP段 # 将以下命令写入脚本保存执行（假设保存在/home/yang/bin/路由表/目录下）：\n#!/bin/sh wget -c http://ftp.apnic.net/stats/apnic/delegated-apnic-latest cat delegated-apnic-latest | awk -F \u0026#39;|\u0026#39; \u0026#39;/CN/\u0026amp;\u0026amp;/ipv4/ {print $4 \u0026#34;/\u0026#34; 32-log($5)/log(2)}\u0026#39; | cat \u0026gt; /home/yang/bin/路由表/cn_rules.conf 配置智能 DNS 服务 # 同上\n配置系统 DNS 服务器设置 # 同上\n编写路由表启动和终止脚本 # $ vim /usr/local/bin/socksfwd #!/bin/bash SOCKS_SERVER=$SERVER_IP # SOCKS 服务器的 IP 地址 SOCKS_PORT=1081 # 本地SOCKS 服务器的端口 GATEWAY_IP=$(ip route|grep \u0026#34;default\u0026#34;|awk \u0026#39;{print $3}\u0026#39;) # 家用网关（路由器）的 IP 地址，你也可以手动指定 TUN_NETWORK_DEV=tun0 # 选一个不冲突的 tun 设备号 TUN_NETWORK_PREFIX=10.0.0 # 选一个不冲突的内网 IP 段的前缀 start_fwd() { ip tuntap del dev \u0026#34;$TUN_NETWORK_DEV\u0026#34; mode tun # 添加虚拟网卡 ip tuntap add dev \u0026#34;$TUN_NETWORK_DEV\u0026#34; mode tun # 给虚拟网卡绑定IP地址 ip addr add \u0026#34;$TUN_NETWORK_PREFIX.1/24\u0026#34; dev \u0026#34;$TUN_NETWORK_DEV\u0026#34; # 启动虚拟网卡 ip link set \u0026#34;$TUN_NETWORK_DEV\u0026#34; up ip route del default via \u0026#34;$GATEWAY_IP\u0026#34; ip route add \u0026#34;$SOCKS_SERVER\u0026#34; via \u0026#34;$GATEWAY_IP\u0026#34; # 特殊ip段走家用网关（路由器）的 IP 地址（如局域网联机） # ip route add \u0026#34;172.16.39.0/24\u0026#34; via \u0026#34;$GATEWAY_IP\u0026#34; # 国内网段走家用网关（路由器）的 IP 地址 for i in $(cat /home/yang/bin/路由表/cn_rules.conf) do ip route add \u0026#34;$i\u0026#34; via \u0026#34;$GATEWAY_IP\u0026#34; done # 将默认网关设为虚拟网卡的IP地址 ip route add 0.0.0.0/1 via \u0026#34;$TUN_NETWORK_PREFIX.1\u0026#34; ip route add 128.0.0.0/1 via \u0026#34;$TUN_NETWORK_PREFIX.1\u0026#34; # 将socks5转为vpn badvpn-tun2socks --tundev \u0026#34;$TUN_NETWORK_DEV\u0026#34; --netif-ipaddr \u0026#34;$TUN_NETWORK_PREFIX.2\u0026#34; --netif-netmask 255.255.255.0 --socks-server-addr \u0026#34;127.0.0.1:$SOCKS_PORT\u0026#34; TUN2SOCKS_PID=\u0026#34;$!\u0026#34; } stop_fwd() { ip route del 128.0.0.0/1 via \u0026#34;$TUN_NETWORK_PREFIX.1\u0026#34; ip route del 0.0.0.0/1 via \u0026#34;$TUN_NETWORK_PREFIX.1\u0026#34; for i in $(cat /home/yang/bin/路由表/cn_rules.conf) do ip route del \u0026#34;$i\u0026#34; via \u0026#34;$GATEWAY_IP\u0026#34; done ip route del \u0026#34;172.16.39.0/24\u0026#34; via \u0026#34;$GATEWAY_IP\u0026#34; ip route del \u0026#34;$SOCKS_SERVER\u0026#34; via \u0026#34;$GATEWAY_IP\u0026#34; ip route add default via \u0026#34;$GATEWAY_IP\u0026#34; ip link set \u0026#34;$TUN_NETWORK_DEV\u0026#34; down ip addr del \u0026#34;$TUN_NETWORK_PREFIX.1/24\u0026#34; dev \u0026#34;$TUN_NETWORK_DEV\u0026#34; ip tuntap del dev \u0026#34;$TUN_NETWORK_DEV\u0026#34; mode tun } start_fwd trap stop_fwd INT TERM wait \u0026#34;$TUN2SOCKS_PID\u0026#34; $ vim /etc/systemd/system/socksfwd.service [Unit] Description=Transparent SOCKS5 forwarding After=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/socksfwd LimitNOFILE=1048576 [Install] WantedBy=multi-user.target 启动服务\n$ systemctl start socksfwd 开机自启\n$ systemctl enable socksfwd 打开流量转发 # $ cat /etc/sysctl.d/30-ipforward.conf net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding = 1 net.ipv4.tcp_congestion_control=westwood net.ipv4.tcp_syn_retries = 5 net.ipv4.tcp_synack_retries = 5 编辑完成后，执行以下命令使变动立即生效\n$ sysctl -p ","date":"2018年1月23日","externalUrl":null,"permalink":"/posts/linux-circumvent/","section":"博客","summary":"Github 地址：Linux 和 MacOS 设备智能分流方案 本来我是决定不再写这样","title":"Linux全局智能分流方案","type":"posts"},{"content":"","date":"2024年7月10日","externalUrl":null,"permalink":"/categories/ai/","section":"分类","summary":"","title":"AI","type":"categories"},{"content":"","date":"2024年7月10日","externalUrl":null,"permalink":"/tags/fastgpt/","section":"标签","summary":"","title":"FastGPT","type":"tags"},{"content":"吴恩达老师提出了一种反思翻译的大语言模型 (LLM) AI 翻译工作流程—— GitHub - andrewyng/translation-agent，具体工作流程如下：\n提示一个 LLM 将文本从 source_language 翻译到 target_language； 让 LLM 反思翻译结果并提出建设性的改进建议； 使用这些建议来改进翻译。 这个 AI 翻译流程是目前比较新的一种翻译方式，利用 LLM 对自己的翻译结果进行改进来获得较好的 AI 翻译效果。\n项目中展示了可以利用对长文本进行分片，然后分别进行反思翻译处理，以突破 LLM 对 tokens 数量的限制，真正实现长文本一键高效率高质量翻译。\n该项目还通过给大模型限定国家地区，已实现更精确的 AI 翻译，如美式英语、英式英语之分；同时提出一些可能能带来更好效果的优化，如对于一些 LLM 未曾训练到的术语 (或有多种翻译方式的术语) 建立术语表，进一步提升翻译的精确度等等。\n而这一切都能通过 FastGPT 工作流轻松实现，本文将手把手教你如何使用 FastGPT 复刻吴恩达老师的 translation-agent。\n单文本块反思翻译 # 咱们先从简单的开始，即不超出 LLM tokens 数量限制的单文本块翻译。\n初始翻译 # 第一步先让 LLM 对源文本块进行初始翻译：\n通过 “文本拼接” 模块引用源语言、目标语言、源文本这三个参数，生成提示词，传给 LLM，让它给出第一版的翻译。\n提示词：\nThis is an {{source_lang}} to {{target_lang}} translation, please provide the {{target_lang}} translation for this text. \\ Do not provide any explanations or text apart from the translation. {{source_lang}}: {{source_text}} {{target_lang}}: 反思 # 然后让 LLM 对第一步生成的初始翻译给出修改建议，称之为反思。\n提示词：\nYour task is to carefully read a source text and a translation from {{source_lang}} to {{target_lang}}, and then give constructive criticism and helpful suggestions to improve the translation. \\ The final style and tone of the translation should match the style of {{target_lang}} colloquially spoken in {{country}}. The source text and initial translation, delimited by XML tags \u0026lt;SOURCE_TEXT\u0026gt;\u0026lt;/SOURCE_TEXT\u0026gt; and \u0026lt;TRANSLATION\u0026gt;\u0026lt;/TRANSLATION\u0026gt;, are as follows: \u0026lt;SOURCE_TEXT\u0026gt; {{source_text}} \u0026lt;/SOURCE_TEXT\u0026gt; \u0026lt;TRANSLATION\u0026gt; {{translation_1}} \u0026lt;/TRANSLATION\u0026gt; When writing suggestions, pay attention to whether there are ways to improve the translation\u0026#39;s \\n\\ (i) accuracy (by correcting errors of addition, mistranslation, omission, or untranslated text),\\n\\ (ii) fluency (by applying {{target_lang}} grammar, spelling and punctuation rules, and ensuring there are no unnecessary repetitions),\\n\\ (iii) style (by ensuring the translations reflect the style of the source text and takes into account any cultural context),\\n\\ (iv) terminology (by ensuring terminology use is consistent and reflects the source text domain; and by only ensuring you use equivalent idioms {{target_lang}}).\\n\\ Write a list of specific, helpful and constructive suggestions for improving the translation. Each suggestion should address one specific part of the translation. Output only the suggestions and nothing else. 这里的提示词接收 5 个参数，源文本、初始翻译、源语言、目标语言以及限定词地区国家，这样 LLM 会对前面生成的翻译提出相当多的修改建议，为后续的提升翻译作准备。\n提升翻译 # 提示词：\nYour task is to carefully read, then edit, a translation from {{source_lang}} to {{target_lang}}, taking into account a list of expert suggestions and constructive criticisms. The source text, the initial translation, and the expert linguist suggestions are delimited by XML tags \u0026lt;SOURCE_TEXT\u0026gt;\u0026lt;/SOURCE_TEXT\u0026gt;, \u0026lt;TRANSLATION\u0026gt;\u0026lt;/TRANSLATION\u0026gt; and \u0026lt;EXPERT_SUGGESTIONS\u0026gt;\u0026lt;/EXPERT_SUGGESTIONS\u0026gt; \\ as follows: \u0026lt;SOURCE_TEXT\u0026gt; {{source_lang}} \u0026lt;/SOURCE_TEXT\u0026gt; \u0026lt;TRANSLATION\u0026gt; {{translation_1}} \u0026lt;/TRANSLATION\u0026gt; \u0026lt;EXPERT_SUGGESTIONS\u0026gt; {{reflection}} \u0026lt;/EXPERT_SUGGESTIONS\u0026gt; Please take into account the expert suggestions when editing the translation. Edit the translation by ensuring: (i) accuracy (by correcting errors of addition, mistranslation, omission, or untranslated text), (ii) fluency (by applying {{target_lang}} grammar, spelling and punctuation rules and ensuring there are no unnecessary repetitions), \\ (iii) style (by ensuring the translations reflect the style of the source text) (iv) terminology (inappropriate for context, inconsistent use), or (v) other errors. Output only the new translation and nothing else. 在前文生成了初始翻译以及相应的反思后，将这二者输入给第三次 LLM 翻译，这样我们就能获得一个比较高质量的翻译结果。\n运行效果 # 由于考虑之后对这个反思翻译的复用，所以创建了一个插件，那么在下面我直接调用这个插件就能使用反思翻译，效果如下：\n随机挑选了一段哈利波特的内容。\n可以看到反思翻译后的效果还是好上不少的，其中反思的输出如下：\n长文反思翻译 # 在掌握了对短文本块的反思翻译后，我们能轻松的通过分片和循环，实现对长文本也即多文本块的反思翻译。\n整体的逻辑是，首先对传入文本的 tokens 数量做判断，如果不超过设置的 tokens 限制，那么直接调用单文本块反思翻译，如果超过设置的 tokens 限制，那么切割为合理的大小，再分别进行对应的反思翻译处理。\n至于为什么要切割分块，有两个原因：\n1、大模型输出上下文只有 4k，无法输出超过 4k token 内容的文字。\n2、输入分块可以减少太长的输入导致的幻觉。\n计算 tokens # 首先，我使用 “Laf 函数” 模块来实现对输入文本的 tokens 的计算。\nLaf 函数的使用相当简单，即开即用，只需要在 Laf 云开发平台中创建个应用，然后安装 tiktoken 依赖，导入如下代码即可：\nconst { Tiktoken } = require(\u0026#34;tiktoken/lite\u0026#34;); const cl100k_base = require(\u0026#34;tiktoken/encoders/cl100k_base.json\u0026#34;); interface IRequestBody { str: string } interface RequestProps extends IRequestBody { systemParams: { appId: string, variables: string, histories: string, cTime: string, chatId: string, responseChatItemId: string } } interface IResponse { message: string; tokens: number; } export default async function (ctx: FunctionContext): Promise\u0026lt;IResponse\u0026gt; { const { str = \u0026#34;\u0026#34; }: RequestProps = ctx.body const encoding = new Tiktoken( cl100k_base.bpe_ranks, cl100k_base.special_tokens, cl100k_base.pat_str ); const tokens = encoding.encode(str); encoding.free(); return { message: \u0026#39;ok\u0026#39;, tokens: tokens.length }; } 再回到 FastGPT，点击 “同步参数”，再连线将源文本传入，即可计算 tokens 数量。\n计算单文本块大小 # 由于不涉及第三方包，只是一些数据处理，所以直接使用 “代码运行” 模块处理即可：\nfunction main({tokenCount, tokenLimit}){ const numChunks = Math.ceil(tokenCount / tokenLimit); let chunkSize = Math.floor(tokenCount / numChunks); const remainingTokens = tokenCount % tokenLimit; if (remainingTokens \u0026gt; 0) { chunkSize += Math.floor(remainingTokens / numChunks); } return {chunkSize}; } 通过上面的代码，我们就能算出不超过 token 限制的合理单文本块大小是多少了。\n获得切分后源文本块 # 通过单文本块大小和源文本，我们在 Laf 中再编写一个函数调用 langchain 的 textsplitters 包来实现文本分片，具体代码如下：\nimport cloud from \u0026#39;@lafjs/cloud\u0026#39; import { TokenTextSplitter } from \u0026#34;@langchain/textsplitters\u0026#34;; interface IRequestBody { text: string chunkSize: number } interface RequestProps extends IRequestBody { systemParams: { appId: string, variables: string, histories: string, cTime: string, chatId: string, responseChatItemId: string } } interface IResponse { output: string[]; } export default async function (ctx: FunctionContext): Promise\u0026lt;IResponse\u0026gt; { const { text = \u0026#39;\u0026#39;, chunkSize = 1000 }: RequestProps = ctx.body; const splitter = new TokenTextSplitter({ encodingName: \u0026#34;gpt2\u0026#34;, chunkSize: Number(chunkSize), chunkOverlap: 0, }); const initialChunks = await splitter.splitText(text); console.log(initialChunks) // 定义不同语言的句子分隔符 const sentenceDelimiters = /[。！？.!?]/; // 进一步处理每个初步分割块 const output = []; let currentChunk = initialChunks[0]; for (let i = 1; i \u0026lt; initialChunks.length; i++) { const sentences = initialChunks[i].split(sentenceDelimiters); if (sentences.length \u0026gt; 0) { currentChunk += sentences[0]; // 拼接第一个句子到当前块 output.push(currentChunk.trim()); // 将当前块加入输出数组 currentChunk = sentences.slice(1).join(\u0026#39;\u0026#39;); // 剩余的句子作为新的当前块 } } // 将最后一个块加入输出数组 if (currentChunk.trim().length \u0026gt; 0) { output.push(currentChunk.trim()); } console.log(output); return { output } } 这样我们就获得了切分好的文本，接下去的操作就类似单文本块反思翻译。\n多文本块翻译 # 这里应该还是不能直接调用前面的单文本块反思翻译，因为提示词中会涉及一些上下文的处理 (或者可以修改下前面写好的插件，多传点参数进去)。\n详细的和前面类似，就是提示词进行一些替换，以及需要做一些很简单的数据处理，整体效果如下。\n多文本块初始翻译 # 多文本块反思 # 多文本块提升翻译 # 循环执行 # 长文反思翻译比较关键的一个部分，就是对多个文本块进行循环反思翻译。\nFastGPT 提供了工作流线路可以返回去执行的功能，所以我们可以写一个很简单的判断函数，来判断结束或是接着执行。\njs 代码：\nfunction main({chunks, currentChunk}){ const findIndex = chunks.findIndex((item) =\u0026gt; item ===currentChunk) return { isEnd: chunks.length-1 === findIndex, i: findIndex + 1, } } 也就是通过判断当前处理的这个文本块，是否是最后一个文本块，从而判断是否需要继续执行，就这样，我们实现了长文反思翻译的效果。\n运行效果 # 首先输入全局设置：\n然后输入需要翻译的文本，这里我选择了一章哈利波特的英文原文来做翻译，其文本长度通过 OpenAI 对 tokens 数量的判断如下：\n实际运行效果如下：\n可以看到还是能满足阅读需求的。\n进一步调优 # 提示词调优 # 在源项目中，给 AI 的系统提示词还是比较的简略的，我们可以通过比较完善的提示词，来督促 LLM 返回更合适的翻译，进一步提升翻译的质量。比如可以使用 CoT 思维链，让 LLM 显式地、系统地生成推理链条，展示翻译的完整思考过程。\n比如初始翻译中的提示词可以换成以下提示词：\n# Role: 资深翻译专家 ## Background: 你是一位经验丰富的翻译专家,精通{{source_lang}}和{{target_lang}}互译,尤其擅长将{{source_lang}}文章译成流畅易懂的{{target_lang}}。你曾多次带领团队完成大型翻译项目,译文广受好评。 ## Attention: - 翻译过程中要始终坚持\u0026#34;信、达、雅\u0026#34;的原则,但\u0026#34;达\u0026#34;尤为重要 - 译文要符合{{target_lang}}的表达习惯,通俗易懂,连贯流畅 - 避免使用过于文绉绉的表达和晦涩难懂的典故引用 - 对于专有的名词或术语，可以适当保留或音译 ## Constraints: - 必须严格遵循四轮翻译流程:直译、意译、校审、定稿 - 译文要忠实原文,准确无误,不能遗漏或曲解原意 - 注意判断上下文，避免重复翻译 ## Goals: - 通过四轮翻译流程,将{{source_lang}}原文译成高质量的{{target_lang}}译文 - 译文要准确传达原文意思,语言表达力求浅显易懂,朗朗上口 - 适度使用一些熟语俗语、流行网络用语等,增强译文的亲和力 - 在直译的基础上,提供至少2个不同风格的意译版本供选择 ## Skills: - 精通{{source_lang}} {{target_lang}}两种语言,具有扎实的语言功底和丰富的翻译经验 - 擅长将{{source_lang}}表达习惯转换为地道自然的{{target_lang}} - 对当代{{target_lang}}语言的发展变化有敏锐洞察,善于把握语言流行趋势 ## Workflow: 1. 第一轮直译:逐字逐句忠实原文,不遗漏任何信息 2. 第二轮意译:在直译的基础上用通俗流畅的{{target_lang}}意译原文,至少提供2个不同风格的版本 3. 第三轮校审:仔细审视译文,消除偏差和欠缺,使译文更加地道易懂 4. 第四轮定稿:择优选取,反复修改润色,最终定稿出一个简洁畅达、符合大众阅读习惯的译文 ## OutputFormat: - 每一轮翻译前用【思考】说明该轮要点 - 每一轮翻译后用【翻译】呈现译文 - 在\\`\\`\\`代码块中展示最终定稿译文，\\`\\`\\`之后无需加其他提示 ## Suggestions: - 直译时力求忠实原文,但不要过于拘泥逐字逐句 - 意译时在准确表达原意的基础上,用最朴实无华的{{target_lang}}来表达 - 校审环节重点关注译文是否符合{{target_lang}}表达习惯,是否通俗易懂 - 定稿时适度采用一些熟语谚语、网络流行语等,使译文更接地气- 善于利用{{target_lang}}的灵活性,用不同的表述方式展现同一内容,提高译文的可读性 从而可以返回更准确更高质量的初始翻译。我们还需要再加一个节点，将初始翻译的第四轮定稿提取出来：\njs 代码如下：\nfunction main({data1}){ const result = data1.split(\u0026#34;```\u0026#34;).filter(item =\u0026gt; !!item.trim()) if(result[result.length-1]) { return { result: result[result.length-1] } } return { result: \u0026#39;未截取到翻译内容\u0026#39; } } 后续的反思和提升翻译也可以修改更准确的提示词，例如：\n提示词如下：\n# Role: 资深翻译专家 ## Background: 你是一位经验丰富的翻译水平评判专家,精通{{source_lang}}和{{target_lang}}互译,尤其擅长将{{source_lang}}文章译成流畅易懂的{{target_lang}}。你曾多次参与文章翻译的校对和审核，能对翻译的文章提出一针见血的见解 ## Attention: - 译文要遵守\u0026#34;信、达、雅\u0026#34;的原则,但\u0026#34;达\u0026#34;尤为重要 - 译文要符合{{target_lang}}的表达习惯,通俗易懂,连贯流畅 - 译文要避免使用过于文绉绉的表达和晦涩难懂的典故引用 ## Constraints: - 译文要忠实原文,准确无误,不能遗漏或曲解原意 - 建议要明确可执行，一针见血 - 尽可能详细地对每段话提出建议 ## Goals: - 你会获得一段{{source_lang}}的原文，以及它对应的初始翻译，你需要针对这段翻译给出你的改进建议 - 尽可能详细地对每段话进行判断，对于需要修改部分的提出建议，而无需修改的部分不要强行修改 - 译文要准确传达原文意思,语言表达力求浅显易懂,朗朗上口 - 适度使用一些熟语俗语、流行网络用语等,增强译文的亲和力 ## Skills: - 精通{{source_lang}} {{target_lang}}两种语言,具有扎实的语言功底和丰富的翻译经验 - 擅长将{{source_lang}}表达习惯转换为地道自然的{{target_lang}} - 对当代{{target_lang}}语言的发展变化有敏锐洞察,善于把握语言流行趋势 我们再来看看最终的运行效果，拿一段技术文章来测试一下：\nIn February of 1992, the development of Windows 3.1 was nearing a close, and the Windows team was trying to figure out what their next steps would be. By the 5th of March, the team knew that they’d be focusing on desktops, laptops, mobile, and pen with NT taking servers and workstations. The team also knew that they needed to address three major areas: UI, hardware support, networking. There was a ton of stuff being worked on at this time (and through the rest of the 1990s) within Microsoft. Just within the Systems group (as distinct from the Apps group) Janus would release on the 6th of April as Windows 3.1, Astro would release in March of 1993 as MS-DOS 6.0, Winball would release in October of 1992 as Windows for Workgroups 3.1, Jaguar while being worked on at this time would never see an independent release (more on that in a bit), and then came the next windows projects: Cougar, Panther, Rover, NT, and Cairo. Cougar was a project to build a fully 32 bit Windows kernel, evolving the Windows 3.x 386 mode kernel for 386-class and higher machines. Panther was a project to port the win32 API to this new kernel. Rover was a project to make a mobile computing version of Cougar/Panther. The NT project was Microsoft’s first steps into a dedicated workstation and server release of Windows, and it would release in July of 1993. Cairo was a project for the next major release of NT, and it would mirror many of the changes to Windows from Cougar/Panther (and the reverse is also true). This system comprised of Cougar and Panther was known as Chicago. The Cougar portion of this system was vital to making a more stable and robust Windows. Beyond being a fully 32 bit protected-mode system, this new kernel would feature dynamically loaded and unloaded protected-mode device drivers. This system would also be threaded and fully support any MS-DOS program running from Windows (where previously in Windows 2 and 3, programs that wrote directly to video RAM would require Windows to terminate and stay resident, one side effect being that in really big Command and Conquer maps, the memory space of Windows would be overwritten and as a result Windows would not restore on exit). These moves were huge for Chicago and for Microsoft more generally. When Chicago was taking shape in 1992, MS-DOS was still Microsoft’s bread and butter. Brad Silverberg was relatively new to Microsoft, but he had a very strong background. He had worked at Apple on the Lisa, and he had worked at Borland. By early 1992, he was the project leader of Chicago and the SVP of Microsoft’s personal systems division. In an internal Microsoft memo Silverberg said: Lest anyone be confused, ms-dos is the the bedrock product of the company, accounting for a very major portion of Microsoft’s profits (ie, stock price). Further, it is under strong competitive pressures (I am more inclined to say “under attack”) from DR-DOS and IBM. We must protect this franchise with our lives. Short term, that means continued aggressive marketing plans. In addition, it also means we need to get yearly product releases out so we put the other guys on a treadmill, rather than be put on the treadmill. As a result, we are going to release a new version of MS-DOS this year, chock full of new goodies, while we move with full-speed toward cougar. That new MS-DOS release was MS-DOS 6 mentioned earlier. The most visible and important new “goodies” referenced by Silverberg were disk defragmentation, disk compression, anti-virus, a new backup system, and file transfer tools. MS-DOS 6 was released in March of 1993 with updates being pushed until June of 1994. I bring this up to try and portray where Microsoft and the industry were at this time. IBM compatible computers outnumbered all other computers by nearly 80 million units. MS-DOS or a compatible DOS system was installed on almost all of them (with OS/2 or Linux being rare). Most software on these computers ran in 16 bit real mode. Most hardware was configured with dip switches, and the config had to match that setting exactly. Loading a driver required knowledge of autoexec and load-high tools. Windows 3 was a huge success, and Windows 3.1 was an even greater success. Despite these successes and the resultant changes in Microsoft’s future plans, MS-DOS was still the market leader in PC operating systems by a very wide margin. Windows 3x did ameliorate some problems, but the old systems remained dominant. Due to this, Microsoft absolutely needed to ensure that MS-DOS was still part of their future despite having a more technically advanced system in NT. Adding to this, most computers that home users were purchasing were incapable of providing a good experience with NT. Chicago needed to provide the best experience possible for win16, win32, and MS-DOS applications on modest hardware, and it needed to be a noticeable improvement over Windows 3. If Microsoft failed in either case, they would be yielding ground to Digital Research or to IBM. Ultimately, the need for backwards compatibility meant that some 16 bit code remained in Chicago. Without this, the backwards compatibility wouldn’t have been as good. In hindsight, given that IBM’s OS/2 could run DOS and Windows software, this was a very good decision on the part of Microsoft. Chicago was structured in a way that is similar to Windows for Workgroups 3.1 (386 enhanced), but is far more refined. There are a large number of virtual device drivers (VxDs) running in 32 bit protected mode alongside virtual DOS machines (VDMs) running in a virtual real mode. These virtual device drivers are used for real physical hardware, for emulating devices for virtual machines, and for providing services to other software. Three of these VxDs comprise the very heart of Chicago: Virtual Machine Manager (VMM32.VXD), Configuration Manager (CONFIGMG), Installable Filesystem Manager (IFM). VMM32 is essentially the Chicago kernel. It handles memory management, event handling, interrupt handling, device driver loading and initialization, the creation of virtual machines, and the scheduling. CONFIGMG handles plug and play. IFM coordinates filesystem access, provides a disk buffer, and provides a 32 bit protected mode I/O access system. This bypasses MS-DOS entirely and was first seen 386 Windows 3 releases. 翻译效果如下：\n太强了！\n从现在开始，不管你想翻译什么文章，不管这篇文章有多长，你都可以直接丢给这个翻译专家，然后该干嘛干嘛，过一会儿再回来就可以领取最完美的翻译结果了，还有谁？\n其他调优 # 比如限定词调优，源项目中已经做了示范，就是加上国家地区这个限定词，实测确实会有不少提升。\n出于 LLM 的卓越能力，我们能够通过设置不同的 prompt 来获取不同的翻译结果，也就是可以很轻松地通过设置特殊的限定词，来实现特定的，更精确的翻译。\n而对于一些超出 LLM 理解的术语等，也可以利用 FastGPT 的知识库功能进行相应扩展，进一步完善翻译机器人的功能。\n结语 # 下一篇文章将会给大家带来一个更强大的智能体：字幕反思翻译专家。\n这个专家能干什么呢？举个例子，假设你有一个英文字幕，不管这个字幕有多长，你都可以复制这个字幕的所有内容，直接丢给字幕翻译专家，然后该干嘛干嘛，过一会儿再回来就可以领取最完美的中英双语字幕了，还有谁？\n最后是福利时刻，该翻译专家的完整工作流我已经分享出来了，大家自取： 长文本反思翻译专家工作流\n","date":"2024年7月10日","externalUrl":null,"permalink":"/posts/best-ai-translation-workflow/","section":"博客","summary":"吴恩达老师提出了一种反思翻译的大语言模型 (LLM) AI 翻译工作流程——","title":"最佳 AI 翻译工作流：全世界最信达雅的翻译","type":"posts"},{"content":"","date":"2024年5月15日","externalUrl":null,"permalink":"/tags/gpt/","section":"标签","summary":"","title":"GPT","type":"tags"},{"content":" 原文链接🔗： How LLMs Work, Explained Without Math\n生成式人工智能 ( GenAI) 和大语言模型 ( LLM)，这两个词汇想必已在大家的耳边萦绕多时。它们如惊涛骇浪般席卷了整个科技界，登上了各大新闻头条。ChatGPT，这个神奇的对话助手，也许已成为你形影不离的良师益友。\n然而，在这场方兴未艾的 GenAI 革命背后，有一个谜题久久萦绕在人们心头：**这些模型的智能究竟从何而来？**本文将为您揭开谜底，解析生成式文本模型的奥秘。我们将抛开晦涩艰深的数学，用通俗易懂的语言，带您走进这个神奇的算法世界。让我们撕下 “魔法” 的面纱，看清其中的计算机科学本质。\nLLM 的真面目 # 首先，我们要破除一个常见的误区。许多人误以为，这些模型是真的能够与人对话，回答人们的各种问题。然而，它们真正的能力远没有想象的那么复杂——它们所做的，不过是根据输入的文本，预测下一个词语 (更准确地说，是下一个 token)。\nToken，这个看似简单的概念，却是揭开 LLM 神秘面纱的钥匙。让我们由此出发，步步深入，一探究竟。\nToken，这些文本的积木、语言的原子，正是 LLM 理解世界的基石。对我们而言，token 不过是单词、标点、空格的化身，但在 LLM 的眼中，它们是精简而高效的信息编码。有时，一个 token 可能代表一串字符，长短不一；有时，它可能是孤零零的一个标点符号。\nLLM 的词汇表，就是这些 token 的集合，啥都有，样样全。这其中的奥秘，要追溯到 BPE 算法。BPE 算法是如何炼制出这些 tokens 的？这个问题，值得我们细细探究。但在此之前，只需记住： GPT-2 模型，这个自然语言处理界的明星，它的词汇表中有 50,257 个 token。\n在 LLM 的世界里，每个 token 都有一个独一无二的数字身份证。而 Tokenizer，就是文本和 token 之间的 “翻译官”，将人类的语言转化为 LLM 能理解的编码，也将 LLM 的思维解码为人类的文字。如果你熟悉 Python，不妨亲自与 token 打个照面。只需安装 OpenAI 的 tiktoken 包：\n$ pip install tiktoken 然后在 Python 中尝试以下操作：\n\u0026gt;\u0026gt;\u0026gt; import tiktoken \u0026gt;\u0026gt;\u0026gt; encoding = tiktoken.encoding_for_model(\u0026#34;gpt-2\u0026#34;) \u0026gt;\u0026gt;\u0026gt; encoding.encode(\u0026#34;The quick brown fox jumps over the lazy dog.\u0026#34;) [464, 2068, 7586, 21831, 18045, 625, 262, 16931, 3290, 13] \u0026gt;\u0026gt;\u0026gt; encoding.decode([464, 2068, 7586, 21831, 18045, 625, 262, 16931, 3290, 13]) \u0026#39;The quick brown fox jumps over the lazy dog.\u0026#39; \u0026gt;\u0026gt;\u0026gt; encoding.decode([464]) \u0026#39;The\u0026#39; \u0026gt;\u0026gt;\u0026gt; encoding.decode([2068]) \u0026#39; quick\u0026#39; \u0026gt;\u0026gt;\u0026gt; encoding.decode([13]) \u0026#39;.\u0026#39; 在这个实验中，我们可以看到，对于 GPT-2 而言，token 464 表示单词 “The”，token 2068 表示 “quick” (含前导空格)，token 13 则表示句点。\n由于 token 是算法生成的，有时会出现一些奇怪的现象。比如，同一个单词 “the” 的三个变体在 GPT-2 中被编码成了不同的 token：\n\u0026gt;\u0026gt;\u0026gt; encoding.encode(\u0026#39;The\u0026#39;) [464] \u0026gt;\u0026gt;\u0026gt; encoding.encode(\u0026#39;the\u0026#39;) [1169] \u0026gt;\u0026gt;\u0026gt; encoding.encode(\u0026#39; the\u0026#39;) [262] BPE 算法并不总是将完整的单词映射为 token。事实上，不太常用的单词可能无法成为单独的 token，需要用多个 token 组合编码，比如这个 “Payment”，就要化身为 “Pay” 和 “ment” 的组合：\n\u0026gt;\u0026gt;\u0026gt; encoding.encode(\u0026#34;Payment\u0026#34;) [19197, 434] \u0026gt;\u0026gt;\u0026gt; encoding.decode([19197]) \u0026#39;Pay\u0026#39; \u0026gt;\u0026gt;\u0026gt; encoding.decode([434]) \u0026#39;ment\u0026#39; 预测下一个 Token # 语言模型就像一个 “水晶球”，给它一串文字，它就能预言下一个最可能出现的词语。这是它的看家本领。但模型并非真的手眼通天，它的预言能力其实基于扎实的概率计算。让我们一起掀开这层神秘的面纱，看看背后的真相。\n如果你懂一点 Python，我们可以用几行代码来窥探语言模型的预言过程：\npredictions = get_token_predictions([\u0026#39;The\u0026#39;, \u0026#39; quick\u0026#39;, \u0026#39; brown\u0026#39;, \u0026#39; fox\u0026#39;]) 这个 get_token_predictions 函数就是我们的 “水晶球”。它接受一个 token 列表作为输入，这些 token 来自用户提供的 prompt。在这个例子中，我们假设每个单词都是一个独立的 token。当然，在实际使用中，每个 token 都有一个对应的数字 ID，但为了简单起见，我们这里直接用单词的文本形式。\n函数的返回结果是一个庞大的数据结构，里面记录了词汇表中每个 token 出现在输入文本之后的概率。以 GPT-2 模型为例，它的词汇表包含 50,257 个 token，因此返回值就是一个 50,257 维的概率分布。\n现在再来重新审视一下这个例子。如果我们的语言模型训练有素，面对 “ The quick brown fox” 这样一个烂大街的句子片段，它很可能会预测下一个词是 “jumps”，而不是 “potato” 之类风马牛不相及的词。在这个概率分布中，“jumps” 的概率值会非常高，而 “potato” 的概率值则接近于零。\nThe quick brown fox jumps over the lazy dog (相应中文可简译为 “快狐跨懒狗”，完整翻译则是 “敏捷的棕色狐狸跨过懒狗”) 是一个著名的英语全字母句，常用于测试字体显示效果和键盘是否故障。此句也常以 “quick brown fox” 做为指代简称。\n当然，语言模型的预测能力并非与生俱来，而是通过日积月累的训练得来的。在漫长的训练过程中，模型如饥似渴地汲取海量文本的营养，逐渐茁壮成长。训练结束时，它已经具备了应对各种文本输入的能力，可以利用积累的知识和经验，计算出任意 token 序列的下一个 token 概率。\n现在是不是觉得语言模型的预测过程没那么神奇了？它与其说是魔法，不如说是一个基于概率的计算过程。这个过程虽然复杂，但并非不可理解。我们只要掌握了基本原理，就可以揭开它的神秘面纱，走近它，了解它。\n长文本生成的奥秘 # 由于语言模型每次只能预测下一个 token 会是什么，因此生成完整句子的唯一方法就是在循环中多次运行该模型。每一轮迭代都会从模型返回的概率分布中选择一个新的 token，生成新的内容。然后将这个新 token 附加到下一轮中输入给模型的文本序列末尾，如此循环往复，直到生成足够长度的文本。\n我们来看一个更完整的 Python 伪代码，展示具体的实现逻辑：\ndef generate_text(prompt, num_tokens, hyperparameters): tokens = tokenize(prompt) for i in range(num_tokens): predictions = get_token_predictions(tokens) next_token = select_next_token(predictions, hyperparameters) tokens.append(next_token) return \u0026#39;\u0026#39;.join(tokens) 其中，generate_text() 函数接受一个用户输入的提示词 (prompt) 文本作为参数，这可以是一个问题或其他任意文本。\ntokenize() 辅助函数使用类似 tiktoken 的分词库将提示文本转换成一系列等效的 token(token) 序列。在 for 循环内部，get_token_predictions() 函数调用语言模型来获取下一个 token 的概率分布，这一步与前面的示例类似。\nselect_next_token() 函数根据上一步得到的下个 token 概率分布，选择最合适的 token 来延续当前的文本序列。最简单的做法是选择概率最高的 token，在机器学习中被称为贪婪选择 (greedy selection)。更好的做法是用符合模型给出概率分布的随机数生成器来选词，这样可以让生成的文本更丰富多样。如果用同样的输入多次运行模型，这种方法还可以让每次产生的回应都略有不同。\n为了让 token 选择过程更加灵活可控，可以用一些超参数 (hyperparameter) 来调整语言模型返回的概率分布，这些超参数作为参数传递给文本生成函数。通过调整超参数，你可以控制 token 选择的 “贪婪程度”。如果你用过大语言模型，你可能比较熟悉名为 temperature 的超参数。提高 temperature 的值可以让 token 的概率分布变得更加平缓，增加选中概率较低 token 的机会，从而让生成的文本显得更有创意和变化。此外，常用的还有 top_p 和 top_k 两个超参数，它们限定从概率最高的前 k 个或概率超过阈值 p 的 token 中进行选择，以平衡多样性和连贯性。\n选定了一个新 token 后，循环进入下一轮迭代，将新 token 添加到原有文本序列的末尾，作为新一轮的输入，再接着生成下一个 token。num_tokens 参数控制循环的迭代轮数，决定要生成的文本长度。但需要注意的是，由于语言模型是逐词预测，没有句子或段落的概念，生成的文本常常会在句子中途意外结束。为了避免这种情况，我们可以把 num_tokens 参数视为生成长度的上限而非确切值，当遇到句号、问号等标点符号时提前结束生成过程，以保证文本在语义和语法上的完整性。\n如果你已经读到这里且充分理解了以上内容，那么恭喜你！现在你对大语言模型的基本工作原理有了一个高层次的认识。如果你想进一步了解更多细节，我在下一节会深入探讨一些更加技术性的话题，但会尽量避免过多涉及晦涩难懂的数学原理。\n模型训练 # 遗憾的是，不借助数学语言来讨论模型训练实际上是很困难的。这里先展示一种非常简单的训练方法。\n既然 LLM 的任务是预测某些词后面跟随的词，那么一个简单的模型训练方式就是从训练数据集中提取所有连续的词对，并用它们来构建一张概率表。\n让我们用一个小型词表和数据集来演示这个过程。假设模型的词表包含以下 5 个词：\n[\u0026#39;I\u0026#39;, \u0026#39;you\u0026#39;, \u0026#39;like\u0026#39;, \u0026#39;apples\u0026#39;, \u0026#39;bananas\u0026#39;] 为了保持示例简洁，我不打算将空格和标点符号视为独立的词。\n我们使用由三个句子组成的训练数据集：\nI like apples I like bananas you like bananas 我们可以构建一个 5x5 的表格，在每个单元格中记录 “该单元格所在行的词” 后面跟随 “该单元格所在列的词” 的次数。下面是根据数据集中三个句子得到的表格：\n- I you like apples bananas I 2 you 1 like 1 2 apples bananas 这个表格应该不难理解。数据集中包含两个 “I like” 实例，一个 “you like” 实例，一个 “like apples” 实例和两个 “like bananas” 实例。\n现在我们知道了每对词在训练集中出现的次数，就可以计算每个词后面跟随其他词的概率了。为此，我们将表中每一行的数字转换为概率值。例如，表格中间行的 “like” 后面有一次跟随 “apples”，两次跟随 “bananas”。这意味着在 33.3%的情况下 “like” 后面是 “apples”，剩下 66.7%的情况下是 “bananas”。\n下面是计算出所有概率后的完整表格。空单元格代表 0%的概率。\n- I you like apples bananas I 100% you 100% like 33.3% 66.7% apples 25% 25% 25% 25% bananas 25% 25% 25% 25% “I”、“you” 和 “like” 这几行的概率很容易计算，但 “apples” 和 “bananas” 带来了问题。由于数据集中没有这两个词后面接其他词的例子，它们存在训练数据的空白。为了确保模型即使面对未见过的词也能做出预测，我决定将 “apples” 和 “bananas” 的后续词概率平均分配给其他四个可能的词。这种做法虽然可能产生不自然的结果，但至少能防止模型在遇到这两个词时陷入死循环。\n训练数据存在 “空洞” 的问题对语言模型的影响不容忽视。在真实的大语言模型中，由于训练语料极其庞大，这些空洞通常表现为局部覆盖率偏低，而不是整体性的缺失，因而不太容易被发现。语言模型在这些训练不足的领域或话题上会产生片面、错误或前后不一致的预测结果，但通常会以一种难以感知的形式表现出来。这就是语言模型有时会产生 “ 幻觉” 的原因之一，所谓幻觉，就是指生成的文本表面上读起来通顺流畅，但实际包含了事实错误或前后矛盾的内容。\n借助上面给出的概率表，你现在可以自己想象一下 get_token_predictions() 函数会如何实现。用 Python 伪代码表示大致如下：\ndef get_token_predictions(input_tokens): last_token = input_tokens[-1] return probabilities_table[last_token] 是不是比想象的要简单？该函数接受一个单词序列作为输入，也就是用户提示。它取这个序列的最后一个单词，然后返回概率表中与之对应的那一行。\n举个例子，如果用 ['you', 'like'] 来调用这个函数，它会返回 “like” 所在的行，其中 “apples” 有 33.3%的概率接在后面组成句子，而 “bananas” 占剩下的 66.7%。有了这些概率信息，之前展示的 select_next_token() 函数在三分之一的情况下应该选择 “apples”。\n当 “apples” 被选为 “you like” 的续词时，“you like apples” 这个句子就形成了。这是一个在训练数据中不存在的全新句子，但它却非常合理。希望这个例子能帮你认识到，语言模型其实只是在重复使用和拼凑它在训练过程中学到的各种模式碎片，就能组合出一些看似原创的想法或概念。\n上下文窗口 # 上一节内容我使用 马尔可夫链的方法训练了一个小语言模型。这种方法存在一个问题：它的上下文窗口只有一个标记，也就是说，模型在预测下一个词时，只考虑了输入序列的最后一个词，而忽略了之前的所有内容。这导致生成的文本缺乏连贯性和一致性，常常前后矛盾，逻辑跳跃。\n为了提高模型的预测质量，一种直观的思路是扩大上下文窗口的大小，比如增加到 2 个标记。但这样做会导致概率表的规模急剧膨胀。以我之前使用的 5 个标记的简单词表为例，将上下文窗口增加到 2 个标记，就需要在原有的 5 行概率表基础上，额外增加 25 行来覆盖所有可能的双词组合。如果进一步扩大到 3 个标记，额外的行数将达到 125 行。可以预见，随着上下文窗口的增大，概率表的规模将呈指数级爆炸式增长。\n更重要的是，即使将上下文窗口扩大到 2 个或 3 个标记，其改进效果仍然非常有限。要使语言模型生成的文本真正做到前后连贯、逻辑通顺，实际上需要一个远大于此的上下文窗口。只有足够大的上下文，新生成的词才能与之前较远处提及的概念、思想产生联系，从而赋予文本连续的语义和逻辑。\n举个实际的例子，OpenAI 开源的 GPT-2 模型采用了 1024 个标记的上下文窗口。如果仍然沿用马尔可夫链的思路来实现这一尺度的上下文，以 5 个标记的词表为例，仅覆盖 1024 个词长度的所有可能序列，就需要高达 5^1024 行的概率表。这是一个天文数字，我在 Python 中计算了这个值的具体大小，读者可以向右滚动来查看完整的数字：\n\u0026gt;\u0026gt;\u0026gt; pow(5, 1024) 55626846462680034577255817933310101605480399511558295763833185422180110870347954896357078975312775514101683493275895275128810854038836502721400309634442970528269449838300058261990253686064590901798039126173562593355209381270166265416453973718012279499214790991212515897719252957621869994522193843748736289511290126272884996414561770466127838448395124802899527144151299810833802858809753719892490239782222290074816037776586657834841586939662825734294051183140794537141608771803070715941051121170285190347786926570042246331102750604036185540464179153763503857127117918822547579033069472418242684328083352174724579376695971173152319349449321466491373527284227385153411689217559966957882267024615430273115634918212890625 这段 Python 代码示例生成了一个庞大的表格，但即便如此，它也只是整个表格的一小部分。因为除了当前的 1024 个 token 长度的序列，我们还需要生成更短的序列，譬如 1023 个、1022 个 token 的序列，一直到只包含 1 个 token 的序列。这样做是为了确保在输入数据 token 数量不足的情况下，模型也能妥善处理较短的序列。马尔可夫链虽然是一个有趣的文本生成方法，但在可扩展性方面确实存在很大的问题。\n如今，1024 个 token 的上下文窗口已经不那么出色了。GPT-3 将其扩大到了 2048 个 token，GPT-3.5 进一步增加到 4096 个。GPT-4 一开始支持 8192 个 token 的上下文，后来增加到 32000 个，再后来甚至达到了 128000 个 token！目前，开始出现支持 100 万以上 token 的超大上下文窗口模型，使得模型在进行 token 预测时，能够拥有更好的一致性和更强的记忆能力。\n总而言之，尽管马尔可夫链为我们提供了一种正确的思路来思考文本生成问题，但其固有的可扩展性不足，使其难以成为一个可行的、能够满足实际需求的解决方案。面对海量文本数据，我们需要寻求更加高效和可扩展的文本生成方法。\n从马尔可夫链到神经网络 # 显然，我们必须摒弃使用概率表的想法。对于一个合理大小的上下文窗口，所需的表格大小将远超内存限制。我们可以用一个函数来代替这个表格，该函数能够通过算法生成近似的下一个词出现概率，而无需将其存储在一个巨大的表格中。这正是神经网络擅长的领域。\n神经网络是一种特殊的函数，它接收一些输入，经过计算后给出输出。对于语言模型而言，输入是代表提示信息的词，输出是下一个可能出现的词及其概率列表。神经网络之所以特殊，是因为除了函数逻辑之外，它们对输入进行计算的方式还受到许多外部定义参数的控制。\n最初，神经网络的参数是未知的，因此其输出毫无意义。神经网络的训练过程就是要找到那些能让函数在训练数据集上表现最佳的参数，并假设如果函数在训练数据上表现良好，它在其他数据上的表现也会相当不错。\n在训练过程中，参数会使用一种叫做 反向传播的算法进行迭代调整，每次调整的幅度都很小。这个算法涉及大量数学计算，我们在这里就不展开了。每次参数调整后，神经网络的预测都会变得更准一些。参数更新后，网络会用训练数据集重新评估，结果为下一轮调整提供参考。这个过程会反复进行，直到函数能够在训练数据上很好地预测下一个词。\n为了让你对神经网络的规模有个概念，GPT-2 模型有大约 15 亿个参数，GPT-3 增加到了 1750 亿，而 GPT-4 据说有 1.76 万亿个参数。在当前硬件条件下，训练如此规模的神经网络通常需要几周或几个月的时间。\n有趣的是，由于参数数量巨大，并且都是通过漫长的迭代过程自动计算出来的，我们很难理解模型的工作原理。训练完成的大语言模型就像一个难以解释的黑箱，因为模型的大部分 “思考” 过程都隐藏在海量参数之中。即使是训练它的人，也很难说清其内部的运作机制。\n层、Transformer 与 Attention 机制 # 你可能好奇神经网络函数内部进行了哪些神秘的计算。在精心调校的参数帮助下，它可以接收一系列输入标记，并以某种方式输出下一个标记出现的合理概率。\n神经网络被配置为执行一系列操作，每个操作称为一个 “层”。第一层接收输入并对其进行转换。转换后的输入进入下一层，再次被转换。这一过程持续进行，直到数据到达最后一层并完成最终转换，生成输出或预测结果。\n机器学习专家设计出不同类型的层，对输入数据进行数学转换。他们还探索了组织和分组层的方法，以实现期望的结果。有些层是通用的，而另一些则专门处理特定类型的输入数据，如图像，或者在大语言模型中的标记化文本。\n目前在大语言模型的文本生成任务中最流行的神经网络架构被称为 “ Transformer”。使用这种架构的模型被称为 GPT，即 “生成式预训练 Transformer”，也就是 Generative Pre-Trained Transformers。\nTransformer 模型的独特之处在于其执行的 “ Attention” 层计算。这种计算允许模型在上下文窗口内的标记之间找出关系和模式，并将其反映在下一个标记出现的概率中。Attention 机制最初被用于语言翻译领域，作为一种找出输入序列中对理解句子意义最重要的标记的方法。这种机制赋予了现代语言模型在基本层面上 “理解” 句子的能力，它可以关注 (或集中 “注意力” 于) 重要词汇或标记，从而更好地把握句子的整体意义。正是这一机制，使 Transformer 模型在各种自然语言处理任务中取得了巨大成功。\n大语言模型到底有没有智能？ # 通过上面的分析，你心中可能已经有了一个初步的判断：大语言模型在生成文本时是否表现出了某种形式的智能？\n我个人并不认为大语言模型具备推理或提出原创想法的能力，但这并不意味着它们一无是处。得益于对上下文窗口中 token 进行的精妙计算，大语言模型能够捕捉用户输入中的模式，并将其与训练过程中学习到的相似模式匹配。它们生成的文本大部分来自训练数据的片段，但将词语 (实际上是 token) 组合在一起的方式非常复杂，在许多情况下产生了感觉原创且有用的结果。\n不过，考虑到大语言模型容易产生幻觉，我不会信任任何将其输出直接提供给最终用户而不经过人工验证的工作流程。\n未来几个月或几年内出现的更大规模语言模型是否能实现类似真正智能的能力？鉴于 GPT 架构的诸多局限性，我觉得这不太可能发生，但谁又说的准呢，也许将来出现一些创新手段，我们就能实现这一目标。\n","date":"2024年5月15日","externalUrl":null,"permalink":"/posts/how-gpt-work-explained-without-math/","section":"博客","summary":"原文链接🔗： How LLMs Work, Explained Without Math 生成式人工智能 ( GenAI) 和大语言模型 ( LL","title":"GPT 原理详解：大模型到底有没有智能？","type":"posts"},{"content":"","date":"2024年5月15日","externalUrl":null,"permalink":"/tags/llm/","section":"标签","summary":"","title":"LLM","type":"tags"},{"content":"","date":"2024年5月11日","externalUrl":null,"permalink":"/tags/rustdesk/","section":"标签","summary":"","title":"RustDesk","type":"tags"},{"content":" RustDesk 是一个强大的开源远程桌面软件，是中国开发者的作品，它使用 Rust 编程语言构建，提供安全、高效、跨平台的远程访问体验。可以说是目前全球最火的开源远程桌面软件了，GitHub 星星数量达到了惊人的 64k！\n与 TeamViewer、ToDesk 等专有远程访问解决方案相比，RustDesk 作为一个开源软件，提供了几个显著的优势：\nRustDesk 完全免费使用，没有任何隐藏费用或订阅计划。 由于其开源特性，RustDesk 的代码是透明的，可以由社区审计，从而提供更高的安全性和可信度。 RustDesk 使用 Rust 语言开发，从根本上确保了程序的内存安全和高性能。 然而现在有一个坏消息：由于被诈骗分子频繁使用，该项目现已暂停国内服务。\n作者原话：\n为了进一步应对诈骗，我们暂时决定停止中国地区的服务，如果用户现在通过公共服务器访问国内主机，将会收到被禁止的消息。\n官网首页也挂出了警告信息：\n作者在开源中国上发布了公告，主要是因为诈骗分子通过短信链接的方式让老人下载 App，然后实施手机银行的指挥操控，受害者被骗金额巨大，对家庭造成极大的损害。\n为了进一步应对诈骗，他们暂时决定停止中国地区的服务，如果用户现在通过公共服务器访问国内主机，将会收到被禁止的消息。\n只能说很无奈。\n好在 RustDesk 有一个很关键的特性就是它允许用户自建服务器，从而在使用 RustDesk 时获得更多的控制权和隐私保护。所谓自建服务器，也就是自建 ID Server 和 Relay Server，至于什么是 ID Server 和 Relay Server，下面我们会给大家详细介绍，并提供一步步的指南来帮助你设置自己的 ID Server 和 Relay Server。\nRustDesk 架构概述 # 要理解自建服务器的重要性，首先需要对 RustDesk 的架构有一个全面的了解。RustDesk 采用了经典的客户端-服务器模型，其中涉及三个主要组件：RustDesk 客户端、RustDesk 服务器和 ID Server。\n客户端-服务器模型\n在 RustDesk 的架构中，客户端是运行在用户设备 (如笔记本电脑、平板电脑或智能手机) 上的应用程序。它提供了一个图形界面，允许用户发起远程访问请求并与远程计算机进行交互。另一方面，服务器组件运行在要远程访问的目标计算机上。它负责监听来自客户端的连接请求，并在建立连接后向客户端发送屏幕更新和接收输入事件。\nID Server 的角色\nID Server 在 RustDesk 的生态系统中扮演着重要的角色。它的主要职责是促进客户端和服务器之间的初始连接建立。当 RustDesk 服务器启动时，它会连接到 ID Server 并注册自己，提供如服务器 ID 和公网 IP 地址等信息。类似地，当客户端想要连接到特定的 RustDesk 服务器时，它会向 ID Server 查询目标服务器的连接信息。\nID Server 维护了一个已注册的 RustDesk 服务器目录，并充当客户端和服务器之间的中介，帮助它们建立直接的点对点 (P2P) 连接。一旦客户端从 ID Server 获得了服务器的连接信息，它就可以尝试直接连接到服务器，而无需进一步通过 ID Server 中继数据。\nRelay Server 的角色\n在某些网络环境下，RustDesk 客户端和服务器可能无法直接建立 P2P 连接，例如当它们位于 NAT (网络地址转换) 或防火墙后时。为了克服这一挑战，RustDesk 引入了 Relay Server。\n如果客户端无法直接连接到服务器，它会向 ID Server 请求一个 Relay Server。然后，客户端和服务器都连接到指定的 Relay Server，并通过它来中继所有的网络通信。Relay Server 在这种情况下充当客户端和服务器之间的桥梁，转发来自一方的数据包到另一方。\n值得注意的是，即使通过 Relay Server 进行通信，RustDesk 也会维护端到端加密，确保中继服务器无法访问明文数据。Relay Server 只是盲目地转发加密的数据包，而不能查看或修改其内容。\n自建服务器 # RustDesk ID Server 与 Relay Server 目前支持多种方式部署，可以在 Linux 和 Windows 中使用二进制直接部署，也可以使用 Docker 部署，具体可参考 RustDesk 的官方文档。\n如果您不想折腾，或者不懂什么 Docker 之类的，那也没关系， Sealos 应用商店提供了一键部署的应用模板，点一下鼠标即可完成部署，非常丝滑。\n由于 RustDesk 是使用 Rust 编写的，所以非常高效，并发也很强，实际测试下来，1C1G 的配置就可以给一整个小型团队使用了。Sealos 的应用模板默认给了最小配置 0.2C128M，个人使用完全足够了。如果您需要给多个人使用，可以随时调整配置，因为 Sealos 是按量付费的，你想怎么调就怎么调，想什么时候调就什么时候调，非常酸爽。\n我们再来看看大家比较关心的价格：\n默认最小配置每天只需要 0.12 元，根据按量付费的机制我们还可以更省钱。所谓按量付费，就是用多少付多少，这里的 “用多少” 指的是你用了多少 CPU、内存、存储等资源，那么如果我不用的时候把它暂停，用的时候再启动，每天只需要 0.01 元 (因为暂停状态下不占用 CPU 和内存，只占用存储)。\n如果你是整个团队在使用，不想频繁的暂停和启动，也可以通过别的办法来省钱，比如设置一个定时任务，白天开启，夜里暂停，也可以省一半的钱。\n再加上外网端口的费用，每天预计花费在 0.1~0.2 元之间。\n好，说完了价格，如果你心动了，或者觉得可以一试，那么接着往下看教程。\n直接打开 RustDesk 应用模板：\n然后点击右上角的 “去 Sealos 部署”。\n如果您是第一次使用 Sealos，则需要注册登录 Sealos 公有云账号，登录之后会立即跳转到模板的部署页面。\n跳转进来之后，你会看到有一个变量 ENCRYPTED_ONLY，你可以选择 1 或者 0。为了隐私和安全，强烈建议选择 1，这样就开启了强制加密，只允许建立加密连接，不容易被别人白嫖。\n设置完成后，点击右上角的 “部署应用” 开始部署，部署完成后，直接点击应用的 “详情” 进入该应用的详情页面。\n点击 “日志” 按钮查看日志：\n日志中可以找到两个关键信息：外网域名和公钥。后面需要用到。\n在 “应用商店”-\u0026gt; “我的应用” 中找到 RustDesk，点进去：\n在 Others 中分别找到 21116 端口和 21117 端口映射的外网端口，21116 是 ID Server 的端口，21117 是 Relay Server 的端口。例如我这里的 ID Server 外网端口就是 30032，Relay Server 外网端口是 30325。\n客户端设置 # 分别在控制端和被控制端的电脑安装 RustDesk，下载地址： https://rustdesk.com/zh/\n安装完成后，打开 RustDesk，点击上面的三个点，进入配置：\n找到网络配置：\n先解锁网络设置，然后在 ID 服务器中输入你的 \u0026lt;外网域名\u0026gt;:\u0026lt;ID Server 外网端口\u0026gt;，在中继服务器中输入你的 \u0026lt;外网域名\u0026gt;:\u0026lt;Relay Server 外网端口\u0026gt;，在 Key 中输入你的公钥。\n例如我这里的 ID 服务器就是 brffleiu.bja.sealos.run:30032，中继服务器是 brffleiu.bja.sealos.run:30325，Key 是 LNS+q8OA02k7CH+TbzO1EzikNYsFS52YiMNi3pmz56k=。\n最后点击 “应用” 就可以了。\n⚠️ 注意：控制端和被控制端都设置使用相同的 ID 服务器、中继服务器和 Key，才能正常进行远程控制。\n总结 # 本文深入探讨了 RustDesk 的架构、自建 RustDesk 服务器（ID Server 和 Relay Server）的好处以及具体的自建步骤，虽然需要一点额外的工作，但收获了很多好处，比如安全性和隐私性。\n随着远程工作和协作变得越来越普遍，拥有一个安全、高效、灵活的远程访问解决方案变得至关重要。通过自建 RustDesk ID Server 和 Relay Server，你可以获得一个量身定制的解决方案，以满足你独特的需求。\n","date":"2024年5月11日","externalUrl":null,"permalink":"/posts/how-to-set-up-rustdesk-server/","section":"博客","summary":"RustDesk 是一个强大的开源远程桌面软件，是中国开发者的作品，它使用 Rust","title":"RustDesk 自建服务器部署和使用教程","type":"posts"},{"content":"","date":"2024年5月11日","externalUrl":null,"permalink":"/categories/tech-social/","section":"分类","summary":"","title":"科技与社会","type":"categories"},{"content":"","date":"2024年4月9日","externalUrl":null,"permalink":"/tags/rag/","section":"标签","summary":"","title":"rag","type":"tags"},{"content":" 什么是 RAG 技术？ # RAG，即检索增强生成 (Retrieval-Augmented Generation)，实际上是将知识检索 (Retrieval) 和语言生成 (Generation) 两种技术巧妙地结合在一起。它的核心思想是，在生成回答或文本时，先从海量的文档知识库中检索出与问题最相关的几段文本，然后以此为基础再衍生出连贯自然的回答。就像一个博学多才的人被问到一个问题时，会先在脑海中搜寻相关的知识点，然后再据此组织语言表达出来。\n这种 “检索+生成” 的架构，集成了两大技术的优点，既继承了知识库的记忆能力，又借助于语言模型的生成能力。\nRAG 的技术优势 # RAG 最突出的优势是 “有据可查”。它会先从知识库中 “查资料”，再根据 “资料” 重新组织语言表达出来，因此生成的文本必然是有事实根据、有出处来源的。这对于对准确性要求较高的应用场景来说更为适用。\n而且 RAG 能生成逻辑清晰、结构严谨的文本。知识库为 RAG 提供了天然的 “逻辑框架”，避免了传统生成模型容易出现的 “泛泛而谈”、“散漫驳杂” 的毛病。\n相对于纯粹的大规模语言模型，RAG 的训练和使用成本更低。因为大量知识蕴藏在外部文本库中，不需要模型从头去学习。这将大大降低模型训练的难度，缩短训练周期。\nRAG 的应用场景 # 智能问答 # RAG 最直接的应用，就是构建智能问答系统。传统的问答系统，往往采用基于规则或基于检索的方法，要么只能应对有限的问题模板，要么只能返回现成的答案片段，很难满足用户日益增长的个性化需求。引入 RAG 后，系统不仅能从海量知识库中找到最相关的证据，还能根据具体问题灵活生成回答，做到有的放矢、因人而异。\n比如医疗领域，问诊者的病情细节可谓千差万别，RAG 系统就能根据每个人的特定症状，结合医学知识库定制回复。这比过去那种千篇一律的建议，显然更具专业性和针对性。\n智能客服 # 相比单纯的 FAQ (常见问题) 检索，RAG 驱动下的智能客服，能够处理更加开放和复杂的用户咨询。\n比如客户想了解某款新品与老款型号的区别，想查询最近的优惠活动，或是询问售后、退换货等较为专业的问题，RAG 系统都能从企业知识库中快速找到相关说明，再组织成简明扼要的回复。如果遇到 AI 回复不了的问题，还可以想办法自动转接到人工客服。\n知识汇总 # 信息时代最不缺的就是信息，缺的是对信息的归纳总结。很多时候，我们对某个话题有兴趣，却苦于相关知识散落在各处，缺乏条理化的整合。\n这时，RAG 就能派上用场。你只需随手找一个关键词，RAG 就能从指定的文本集合中，迅速检索出所有相关片段，再提炼、融合形成一篇系统性的介绍。相比手工梳理资料，这种方法要高效得多，而且由于囊括了多个来源，汇总后的知识也更加全面客观。\nRAG 的局限性和改进方向 # RAG 虽然潜力巨大，但也不是尚方宝剑，仍存在一些局限性：\nRAG 对知识库的质量具有强依赖性。倘若知识库中本身存在错误或者过时的信息，RAG 检索到的片段也就是 “garbage in，garbage out”，即便经过再多加工润色，其本质缺陷也难以弥补。 RAG 对问题和知识的理解，目前还主要停留在词面匹配的层面，对语义的把握还不够深入。一旦遇到需要常识推理、跨域类比的复杂问题，RAG 可能会力不从心。 由于 RAG 本质上仍是深度学习模型，因此也难逃 “黑盒子” 的通病，即很难解释清楚模型给出某个答案的具体依据何在。这对于需要可解释性的应用场景，无疑是个挑战。 改进方向 # 对应 RAG 的局限性，未来的研究可朝以下几个方向努力：\n加强对知识库的质量把控。可借鉴知识图谱、实体链接等技术，对文本库进行去重、消歧、纠错等预处理，并建立严谨的知识溯源机制。 引入更多语义理解的技术。比如利用预训练模型来增强文本匹配的语义性，利用常识推理来拓展检索的纵深性等等。 改进对话生成的策略。可尝试基于修辞结构理论，对生成内容进行层次划分、关联组织，使之更符合人类表达的逻辑规律。 重视对可解释性的研究。除了常见的注意力可视化，还可尝试因果推理、反事实分析等更高阶的技术，让 RAG 的思考过程更 “透明”、更可信。 FastGPT 如何支持您的 Retrieval-Augmented Generation 需求？ # FastGPT 是一个基于 LLM 大语言模型的知识库问答系统，提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排，从而实现复杂的问答场景！\nFastGPT 采用直观的可视化界面设计，为各种应用场景提供了丰富实用的功能。通过简洁易懂的操作步骤，可以轻松完成 AI 客服的创建和训练流程。\n","date":"2024年4月9日","externalUrl":null,"permalink":"/posts/what-is-rag/","section":"博客","summary":"什么是 RAG 技术？ # RAG，即检索增强生成 (Retrieval-Augmented Generation","title":"RAG 技术是什么？","type":"posts"},{"content":"","date":"2024年1月14日","externalUrl":null,"permalink":"/categories/gfw/","section":"分类","summary":"","title":"GFW","type":"categories"},{"content":"","date":"2024年1月14日","externalUrl":null,"permalink":"/tags/sing-box/","section":"标签","summary":"","title":"sing-box","type":"tags"},{"content":" sing-box 是什么 # sing-box 是新一代超强通用代理工具，对标 *ray core 与 clash，而且它的性能以及支持的协议类型已经超过了 *ray core 与 clash。目前支持以下协议：\n入站：\nShadowsocks(including shadowsocks2022) Vmess Trojan Naive Hysteria ShadowTLS Vless Tuic Tun Redirect TProxy Socks HTTP 出站：\nShadowsocks(including shadowsocks2022) Vmess Trojan Wireguard Hysteria ShadowTLS ShadowsocksR VLESS Tuic Hysteria2 Tor SSH DNS 除了命令行客户端以外，还提供了图形界面客户端，图形界面支持 Android、iOS、macOS 以及 Apple tvOS，Windows 暂时不支持，还在施工中 🚧\n这简直就是魔法上网界的瑞士军刀啊！而且所有的客户端都是免费的，iOS 端也不用再买 Shadowrocket 小火箭等付费 App 了。再看看隔壁 Surge 的价格：\n你玩我呢？？\n还是 sing-box 香。本文将会手把手教大家如何使用 sing-box 来实现任意机器的全局透明代理。\nsing-box 客户端下载 # 第一步先解决客户端下载的问题。\nAndroid # Android 客户端可以到 Play Store 中去下载：\n也可以直接到 GitHub Releases 页面下载。\n如果你是 Android 的 Magisk/KernelSU 玩家，可以选择刷入 box_for_magisk 模块。\nApple 平台 # iOS/macOS/Apple tvOS 用户可以到 App Store 中下载（前提是你得有个美区 ID），也可以使用 Homebrew 直接安装：\n$ brew install sfm 除此之外你也可以直接到 GitHub Releases 页面下载客户端或者命令行版本。\nWindows # Windows 没有图形界面客户端，官方还正在开发中，不过可以直接使用包管理器 Sccop 或者 Chocolatey 安装命令行版本：\n# Sccop $ scoop install sing-box # Chocolatey $ choco install sing-box 你也可以选择第三方开发者开发的图形界面客户端： GUI.for.SingBox\n还有一个更加成熟的第三方客户端： Hiddify-Next\nLinux # Linux 就很简单了，直接到 GitHub Releases 页面下载命令行版本即可。\nsing-box 配置解析 # sing-box 的核心就是它的配置，所有的配置都在一个 JSON 文件里，每个配置参数的含义可参考 sing-box 官方文档。\n但是为了能够快速使用起来，我们需要一个示例模板。没问题，我这就给你一个比较完美的透明代理模板：\nsing-box 透明代理示例模板 { \u0026#34;dns\u0026#34;: { \u0026#34;servers\u0026#34;: [ { \u0026#34;tag\u0026#34;: \u0026#34;dns_proxy\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;https://1.1.1.1/dns-query\u0026#34;, \u0026#34;address_resolver\u0026#34;: \u0026#34;dns_resolver\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;ipv4_only\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;tag\u0026#34;: \u0026#34;dns_direct\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;h3://dns.alidns.com/dns-query\u0026#34;, \u0026#34;address_resolver\u0026#34;: \u0026#34;dns_resolver\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;ipv4_only\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;tag\u0026#34;: \u0026#34;dns_block\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;rcode://refused\u0026#34; }, { \u0026#34;tag\u0026#34;: \u0026#34;dns_resolver\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;223.5.5.5\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;ipv4_only\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;direct\u0026#34; } ], \u0026#34;rules\u0026#34;: [ { \u0026#34;outbound\u0026#34;: \u0026#34;any\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_resolver\u0026#34; }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_direct\u0026#34; }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;global\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; }, { \u0026#34;process_name\u0026#34;: [ \u0026#34;TencentMeeting\u0026#34;, \u0026#34;NemoDesktop\u0026#34;, \u0026#34;ToDesk\u0026#34;, \u0026#34;ToDesk_Service\u0026#34;, \u0026#34;WeChat\u0026#34;, \u0026#34;Tailscale\u0026#34;, \u0026#34;wireguard-go\u0026#34;, \u0026#34;Tunnelblick\u0026#34;, \u0026#34;softwareupdated\u0026#34;, \u0026#34;kubectl\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_direct\u0026#34; }, { \u0026#34;domain_suffix\u0026#34;: [ \u0026#34;icloudnative.io\u0026#34;, \u0026#34;fuckcloudnative.io\u0026#34;, \u0026#34;sealos.io\u0026#34;, \u0026#34;cdn.jsdelivr.net\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_direct\u0026#34; }, { \u0026#34;process_name\u0026#34;: [ \u0026#34;DropboxMacUpdate\u0026#34;, \u0026#34;Dropbox\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; }, { \u0026#34;package_name\u0026#34;: [ \u0026#34;com.google.android.youtube\u0026#34;, \u0026#34;com.android.vending\u0026#34;, \u0026#34;org.telegram.messenger\u0026#34;, \u0026#34;org.telegram.plus\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;geosite-geolocation-!cn\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;Global\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; }, { \u0026#34;rule_set\u0026#34;: [ \u0026#34;YouTube\u0026#34;, \u0026#34;Telegram\u0026#34;, \u0026#34;Netflix\u0026#34;, \u0026#34;geoip-google\u0026#34;, \u0026#34;geoip-telegram\u0026#34;, \u0026#34;geoip-twitter\u0026#34;, \u0026#34;geoip-netflix\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; } ], \u0026#34;final\u0026#34;: \u0026#34;dns_direct\u0026#34; }, \u0026#34;ntp\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;server\u0026#34;: \u0026#34;time.apple.com\u0026#34;, \u0026#34;server_port\u0026#34;: 123, \u0026#34;interval\u0026#34;: \u0026#34;30m0s\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;direct\u0026#34; }, \u0026#34;inbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;tun\u0026#34;, \u0026#34;inet4_address\u0026#34;: \u0026#34;198.18.0.1/16\u0026#34;, \u0026#34;auto_route\u0026#34;: true, \u0026#34;exclude_package\u0026#34;: [ \u0026#34;cmb.pb\u0026#34;, \u0026#34;cn.gov.pbc.dcep\u0026#34;, \u0026#34;com.MobileTicket\u0026#34;, \u0026#34;com.adguard.android\u0026#34;, \u0026#34;com.ainemo.dragoon\u0026#34;, \u0026#34;com.alibaba.android.rimet\u0026#34;, \u0026#34;com.alicloud.databox\u0026#34;, \u0026#34;com.amazing.cloudisk.tv\u0026#34;, \u0026#34;com.autonavi.minimap\u0026#34;, \u0026#34;com.bilibili.app.in\u0026#34;, \u0026#34;com.bishua666.luxxx1\u0026#34;, \u0026#34;com.cainiao.wireless\u0026#34;, \u0026#34;com.chebada\u0026#34;, \u0026#34;com.chinamworld.main\u0026#34;, \u0026#34;com.cmbchina.ccd.pluto.cmbActivity\u0026#34;, \u0026#34;com.coolapk.market\u0026#34;, \u0026#34;com.ctrip.ct\u0026#34;, \u0026#34;com.dianping.v1\u0026#34;, \u0026#34;com.douban.frodo\u0026#34;, \u0026#34;com.eg.android.AlipayGphone\u0026#34;, \u0026#34;com.farplace.qingzhuo\u0026#34;, \u0026#34;com.hanweb.android.zhejiang.activity\u0026#34;, \u0026#34;com.leoao.fitness\u0026#34;, \u0026#34;com.lucinhu.bili_you\u0026#34;, \u0026#34;com.mikrotik.android.tikapp\u0026#34;, \u0026#34;com.moji.mjweather\u0026#34;, \u0026#34;com.motorola.cn.calendar\u0026#34;, \u0026#34;com.motorola.cn.lrhealth\u0026#34;, \u0026#34;com.netease.cloudmusic\u0026#34;, \u0026#34;com.sankuai.meituan\u0026#34;, \u0026#34;com.sina.weibo\u0026#34;, \u0026#34;com.smartisan.notes\u0026#34;, \u0026#34;com.sohu.inputmethod.sogou.moto\u0026#34;, \u0026#34;com.sonelli.juicessh\u0026#34;, \u0026#34;com.ss.android.article.news\u0026#34;, \u0026#34;com.ss.android.lark\u0026#34;, \u0026#34;com.ss.android.ugc.aweme\u0026#34;, \u0026#34;com.tailscale.ipn\u0026#34;, \u0026#34;com.taobao.idlefish\u0026#34;, \u0026#34;com.taobao.taobao\u0026#34;, \u0026#34;com.tencent.mm\u0026#34;, \u0026#34;com.tencent.mp\u0026#34;, \u0026#34;com.tencent.soter.soterserver\u0026#34;, \u0026#34;com.tencent.wemeet.app\u0026#34;, \u0026#34;com.tencent.weread\u0026#34;, \u0026#34;com.tencent.wework\u0026#34;, \u0026#34;com.ttxapps.wifiadb\u0026#34;, \u0026#34;com.unionpay\u0026#34;, \u0026#34;com.unnoo.quan\u0026#34;, \u0026#34;com.wireguard.android\u0026#34;, \u0026#34;com.xingin.xhs\u0026#34;, \u0026#34;com.xunmeng.pinduoduo\u0026#34;, \u0026#34;com.zui.zhealthy\u0026#34;, \u0026#34;ctrip.android.view\u0026#34;, \u0026#34;io.kubenav.kubenav\u0026#34;, \u0026#34;org.geekbang.geekTime\u0026#34;, \u0026#34;tv.danmaku.bili\u0026#34; ], \u0026#34;stack\u0026#34;: \u0026#34;mixed\u0026#34;, \u0026#34;sniff\u0026#34;: true }, { \u0026#34;type\u0026#34;: \u0026#34;socks\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;socks-in\u0026#34;, \u0026#34;listen\u0026#34;: \u0026#34;::\u0026#34;, \u0026#34;listen_port\u0026#34;: 5353 } ], \u0026#34;outbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;selector\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;select\u0026#34;, \u0026#34;outbounds\u0026#34;: [ \u0026#34;trojan-out\u0026#34; ], \u0026#34;default\u0026#34;: \u0026#34;trojan-out\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;selector\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;openai\u0026#34;, \u0026#34;outbounds\u0026#34;: [ \u0026#34;trojan-out\u0026#34; ], \u0026#34;default\u0026#34;: \u0026#34;trojan-out\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;selector\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;tiktok\u0026#34;, \u0026#34;outbounds\u0026#34;: [ \u0026#34;trojan-out\u0026#34; ], \u0026#34;default\u0026#34;: \u0026#34;trojan-out\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;trojan\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;trojan-out\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;199.180.115.155\u0026#34;, \u0026#34;server_port\u0026#34;: 9443, \u0026#34;password\u0026#34;: \u0026#34;5iFHKMrn9Ez//VKh6zChTA==\u0026#34;, \u0026#34;tls\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;server_name\u0026#34;: \u0026#34;www.example.com\u0026#34;, \u0026#34;insecure\u0026#34;: true, \u0026#34;utls\u0026#34;: { \u0026#34;fingerprint\u0026#34;: \u0026#34;chrome\u0026#34; } }, \u0026#34;multiplex\u0026#34;: { \u0026#34;protocol\u0026#34;: \u0026#34;h2mux\u0026#34;, \u0026#34;max_connections\u0026#34;: 4, \u0026#34;min_streams\u0026#34;: 4 }, \u0026#34;transport\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;grpc\u0026#34;, \u0026#34;service_name\u0026#34;: \u0026#34;TunService\u0026#34; } }, { \u0026#34;type\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;block\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;block\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;dns\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;dns-out\u0026#34; } ], \u0026#34;route\u0026#34;: { \u0026#34;rules\u0026#34;: [ { \u0026#34;protocol\u0026#34;: \u0026#34;dns\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;dns-out\u0026#34; }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;global\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;domain_suffix\u0026#34;: [ \u0026#34;icloudnative.io\u0026#34;, \u0026#34;fuckcloudnative.io\u0026#34;, \u0026#34;sealos.io\u0026#34;, \u0026#34;cdn.jsdelivr.net\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;process_name\u0026#34;: [ \u0026#34;TencentMeeting\u0026#34;, \u0026#34;NemoDesktop\u0026#34;, \u0026#34;ToDesk\u0026#34;, \u0026#34;ToDesk_Service\u0026#34;, \u0026#34;WeChat\u0026#34;, \u0026#34;OpenLens\u0026#34;, \u0026#34;Tailscale\u0026#34;, \u0026#34;wireguard-go\u0026#34;, \u0026#34;Tunnelblick\u0026#34;, \u0026#34;softwareupdated\u0026#34;, \u0026#34;kubectl\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;protocol\u0026#34;: \u0026#34;quic\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;block\u0026#34; }, { \u0026#34;inbound\u0026#34;: \u0026#34;socks-in\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;rule_set\u0026#34;: [ \u0026#34;WeChat\u0026#34;, \u0026#34;Bilibili\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;OpenAI\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;openai\u0026#34; }, { \u0026#34;domain_suffix\u0026#34;: [ \u0026#34;openai.com\u0026#34;, \u0026#34;oaistatic.com\u0026#34;, \u0026#34;oaiusercontent.com\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;openai\u0026#34; }, { \u0026#34;package_name\u0026#34;: \u0026#34;com.openai.chatgpt\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;openai\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;TikTok\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;tiktok\u0026#34; }, { \u0026#34;package_name\u0026#34;: \u0026#34;com.zhiliaoapp.musically\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;tiktok\u0026#34; }, { \u0026#34;domain_suffix\u0026#34;: [ \u0026#34;depay.one\u0026#34;, \u0026#34;orbstack.dev\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;process_name\u0026#34;: [ \u0026#34;DropboxMacUpdate\u0026#34;, \u0026#34;Dropbox\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;package_name\u0026#34;: [ \u0026#34;com.google.android.youtube\u0026#34;, \u0026#34;com.android.vending\u0026#34;, \u0026#34;org.telegram.messenger\u0026#34;, \u0026#34;org.telegram.plus\u0026#34;, \u0026#34;com.google.android.googlequicksearchbox\u0026#34;, \u0026#34;app.rvx.android.youtube\u0026#34;, \u0026#34;com.mudvod.video\u0026#34;, \u0026#34;com.fox2code.mmm\u0026#34;, \u0026#34;com.twitter.android\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;domain\u0026#34;: \u0026#34;accounts.google.com\u0026#34;, \u0026#34;domain_suffix\u0026#34;: [ \u0026#34;sourceforge.net\u0026#34;, \u0026#34;fhjasokiwq.com\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;domain_suffix\u0026#34;: \u0026#34;cloud.sealos.io\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;logical\u0026#34;, \u0026#34;mode\u0026#34;: \u0026#34;and\u0026#34;, \u0026#34;rules\u0026#34;: [ { \u0026#34;rule_set\u0026#34;: \u0026#34;geosite-geolocation-!cn\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;geoip-cn\u0026#34;, \u0026#34;invert\u0026#34;: true } ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;Global\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;geoip-cn\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;ip_is_private\u0026#34;: true, \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;rule_set\u0026#34;: [ \u0026#34;YouTube\u0026#34;, \u0026#34;Telegram\u0026#34;, \u0026#34;Netflix\u0026#34;, \u0026#34;geoip-google\u0026#34;, \u0026#34;geoip-telegram\u0026#34;, \u0026#34;geoip-twitter\u0026#34;, \u0026#34;geoip-netflix\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; } ], \u0026#34;rule_set\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geosite-geolocation-!cn\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-cn\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-cn.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-google\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-google.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-telegram\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-telegram.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-twitter\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-twitter.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-netflix\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-netflix.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Global\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Global.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;YouTube\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/YouTube.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;OpenAI\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/OpenAI.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;TikTok\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/TikTok.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Telegram\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Telegram.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Netflix\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Netflix.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;WeChat\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/WeChat.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Bilibili\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Bilibili.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; } ], \u0026#34;final\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;find_process\u0026#34;: true, \u0026#34;auto_detect_interface\u0026#34;: true }, \u0026#34;experimental\u0026#34;: { \u0026#34;cache_file\u0026#34;: { \u0026#34;enabled\u0026#34;: true }, \u0026#34;clash_api\u0026#34;: { \u0026#34;external_controller\u0026#34;: \u0026#34;0.0.0.0:9090\u0026#34;, \u0026#34;external_ui\u0026#34;: \u0026#34;metacubexd\u0026#34;, \u0026#34;external_ui_download_url\u0026#34;: \u0026#34;https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip\u0026#34;, \u0026#34;external_ui_download_detour\u0026#34;: \u0026#34;select\u0026#34;, \u0026#34;default_mode\u0026#34;: \u0026#34;rule\u0026#34; } } } 下面我来给大家解析一下里面的配置，首先来看 DNS 部分。\n如果你嫌下面的解析太长不看，那就直接使用我的示例模板配置好了。 DNS 配置 # sing-box 对 DNS 的处理比 Clash 强太多了，支持各种分流规则，结构如下：\n{ \u0026#34;dns\u0026#34;: { \u0026#34;servers\u0026#34;: [], \u0026#34;rules\u0026#34;: [], \u0026#34;final\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;disable_cache\u0026#34;: false, \u0026#34;disable_expire\u0026#34;: false, \u0026#34;independent_cache\u0026#34;: false, \u0026#34;reverse_mapping\u0026#34;: false, \u0026#34;fakeip\u0026#34;: {} } } 其中 servers 定义了 DNS 服务器，具体参数含义我就不解释了，自己看官方文档。我给出的 DNS 服务器配置是：\n{ \u0026#34;dns\u0026#34;: { \u0026#34;servers\u0026#34;: [ { \u0026#34;tag\u0026#34;: \u0026#34;dns_proxy\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;https://1.1.1.1/dns-query\u0026#34;, \u0026#34;address_resolver\u0026#34;: \u0026#34;dns_resolver\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;ipv4_only\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;select\u0026#34; }, { \u0026#34;tag\u0026#34;: \u0026#34;dns_direct\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;h3://dns.alidns.com/dns-query\u0026#34;, \u0026#34;address_resolver\u0026#34;: \u0026#34;dns_resolver\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;ipv4_only\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;tag\u0026#34;: \u0026#34;dns_block\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;rcode://refused\u0026#34; }, { \u0026#34;tag\u0026#34;: \u0026#34;dns_resolver\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;223.5.5.5\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;ipv4_only\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;direct\u0026#34; } ] } } 这里定义了 3 个 DNS 服务器，当你发起一个域名解析请求时，这些服务器会被用来查找对应的 IP 地址。同时还定义了一个 RCode 协议用来屏蔽请求。\nrules 定义了 DNS 规则，这些规则用于定义哪些域名应该使用哪个 DNS 服务器解析。它可以让你根据域名的特定模式选择不同的 DNS 服务器。DNS 规则如下：\n{ \u0026#34;dns\u0026#34;: { \u0026#34;rules\u0026#34;: [ { \u0026#34;outbound\u0026#34;: \u0026#34;any\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_resolver\u0026#34; // 注释：对于任何出站连接（不管是直接连接还是通过代理），使用 \u0026#34;dns_resolver\u0026#34; 服务器进行 DNS 解析（这一句主要用来解析代理节点本身的 IP 地址）。 }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_direct\u0026#34; // 注释：在直连模式（不经过代理）下，使用 \u0026#34;dns_direct\u0026#34; 服务器进行 DNS 解析。 }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;global\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; // 注释：在全局代理模式下，使用 \u0026#34;dns_proxy\u0026#34; 服务器进行 DNS 解析。 }, { \u0026#34;process_name\u0026#34;: [ \u0026#34;TencentMeeting\u0026#34;, \u0026#34;NemoDesktop\u0026#34;, \u0026#34;ToDesk\u0026#34;, \u0026#34;ToDesk_Service\u0026#34;, \u0026#34;WeChat\u0026#34;, \u0026#34;Tailscale\u0026#34;, \u0026#34;wireguard-go\u0026#34;, \u0026#34;Tunnelblick\u0026#34;, \u0026#34;softwareupdated\u0026#34;, \u0026#34;kubectl\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_direct\u0026#34; // 注释：当特定的进程（如 TencentMeeting、WeChat 等）发起 DNS 请求时，使用 \u0026#34;dns_direct\u0026#34; 服务器进行直连 DNS 解析。 }, { \u0026#34;domain_suffix\u0026#34;: [ \u0026#34;icloudnative.io\u0026#34;, \u0026#34;fuckcloudnative.io\u0026#34;, \u0026#34;sealos.io\u0026#34;, \u0026#34;cdn.jsdelivr.net\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_direct\u0026#34; // 注释：对于特定后缀的域名（如 icloudnative.io 等），使用 \u0026#34;dns_direct\u0026#34; 服务器进行直连 DNS 解析。 }, { \u0026#34;process_name\u0026#34;: [\u0026#34;DropboxMacUpdate\u0026#34;, \u0026#34;Dropbox\u0026#34;], \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; // 注释：当 Dropbox 相关进程发起 DNS 请求时，使用 \u0026#34;dns_proxy\u0026#34; 服务器通过代理进行 DNS 解析。 }, { \u0026#34;package_name\u0026#34;: [ \u0026#34;com.google.android.youtube\u0026#34;, \u0026#34;com.android.vending\u0026#34;, \u0026#34;org.telegram.messenger\u0026#34;, \u0026#34;org.telegram.plus\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; // 注释：对于特定的 Android 应用包名（如 YouTube、Telegram 等），使用 \u0026#34;dns_proxy\u0026#34; 服务器通过代理进行 DNS 解析。 }, { \u0026#34;rule_set\u0026#34;: \u0026#34;geosite-geolocation-!cn\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; // 注释：对于 geosite 数据库中定义的非中国地区的地理位置相关的域名，使用 \u0026#34;dns_proxy\u0026#34; 服务器通过代理进行 DNS 解析。 }, { \u0026#34;rule_set\u0026#34;: \u0026#34;Global\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; // 注释：对于定义在 \u0026#34;Global\u0026#34; 规则集中的域名，使用 \u0026#34;dns_proxy\u0026#34; 服务器通过代理进行 DNS 解析。 }, { \u0026#34;rule_set\u0026#34;: [ \u0026#34;YouTube\u0026#34;, \u0026#34;Telegram\u0026#34;, \u0026#34;Netflix\u0026#34;, \u0026#34;geoip-google\u0026#34;, \u0026#34;geoip-telegram\u0026#34;, \u0026#34;geoip-twitter\u0026#34;, \u0026#34;geoip-netflix\u0026#34; ], \u0026#34;server\u0026#34;: \u0026#34;dns_proxy\u0026#34; // 注释：对于特定的服务和地理位置相关的域名（如 YouTube、Netflix、谷歌、Telegram 相关的域名），使用 \u0026#34;dns_proxy\u0026#34; 服务器通过代理进行 DNS 解析。 } ], \u0026#34;final\u0026#34;: \u0026#34;dns_direct\u0026#34; // 注释：如果上述规则都不适用，则默认使用 \u0026#34;dns_direct\u0026#34; 服务器进行直连 DNS 解析。 } } 入站配置 # 接下来比较重要的就是入站规则了，入站（Inbound）在网络领域，特别是在代理和网络路由配置中，通常指的是进入某个系统或网络的数据流。在 sing-box 中，入站配置定义了如何处理进入代理服务器的数据。入站配置示例如下：\n{ \u0026#34;inbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;tun\u0026#34;, \u0026#34;inet4_address\u0026#34;: \u0026#34;198.18.0.1/16\u0026#34;, \u0026#34;auto_route\u0026#34;: true, \u0026#34;exclude_package\u0026#34;: [ \u0026#34;cmb.pb\u0026#34;, \u0026#34;cn.gov.pbc.dcep\u0026#34;, \u0026#34;com.MobileTicket\u0026#34;, \u0026#34;com.adguard.android\u0026#34;, \u0026#34;com.ainemo.dragoon\u0026#34;, \u0026#34;com.alibaba.android.rimet\u0026#34;, \u0026#34;com.alicloud.databox\u0026#34;, \u0026#34;com.amazing.cloudisk.tv\u0026#34;, \u0026#34;com.autonavi.minimap\u0026#34;, \u0026#34;com.bilibili.app.in\u0026#34;, \u0026#34;com.bishua666.luxxx1\u0026#34;, \u0026#34;com.cainiao.wireless\u0026#34;, \u0026#34;com.chebada\u0026#34;, \u0026#34;com.chinamworld.main\u0026#34;, \u0026#34;com.cmbchina.ccd.pluto.cmbActivity\u0026#34;, \u0026#34;com.coolapk.market\u0026#34;, \u0026#34;com.ctrip.ct\u0026#34;, \u0026#34;com.dianping.v1\u0026#34;, \u0026#34;com.douban.frodo\u0026#34;, \u0026#34;com.eg.android.AlipayGphone\u0026#34;, \u0026#34;com.farplace.qingzhuo\u0026#34;, \u0026#34;com.hanweb.android.zhejiang.activity\u0026#34;, \u0026#34;com.leoao.fitness\u0026#34;, \u0026#34;com.lucinhu.bili_you\u0026#34;, \u0026#34;com.mikrotik.android.tikapp\u0026#34;, \u0026#34;com.moji.mjweather\u0026#34;, \u0026#34;com.motorola.cn.calendar\u0026#34;, \u0026#34;com.motorola.cn.lrhealth\u0026#34;, \u0026#34;com.netease.cloudmusic\u0026#34;, \u0026#34;com.sankuai.meituan\u0026#34;, \u0026#34;com.sina.weibo\u0026#34;, \u0026#34;com.smartisan.notes\u0026#34;, \u0026#34;com.sohu.inputmethod.sogou.moto\u0026#34;, \u0026#34;com.sonelli.juicessh\u0026#34;, \u0026#34;com.ss.android.article.news\u0026#34;, \u0026#34;com.ss.android.lark\u0026#34;, \u0026#34;com.ss.android.ugc.aweme\u0026#34;, \u0026#34;com.tailscale.ipn\u0026#34;, \u0026#34;com.taobao.idlefish\u0026#34;, \u0026#34;com.taobao.taobao\u0026#34;, \u0026#34;com.tencent.mm\u0026#34;, \u0026#34;com.tencent.mp\u0026#34;, \u0026#34;com.tencent.soter.soterserver\u0026#34;, \u0026#34;com.tencent.wemeet.app\u0026#34;, \u0026#34;com.tencent.weread\u0026#34;, \u0026#34;com.tencent.wework\u0026#34;, \u0026#34;com.ttxapps.wifiadb\u0026#34;, \u0026#34;com.unionpay\u0026#34;, \u0026#34;com.unnoo.quan\u0026#34;, \u0026#34;com.wireguard.android\u0026#34;, \u0026#34;com.xingin.xhs\u0026#34;, \u0026#34;com.xunmeng.pinduoduo\u0026#34;, \u0026#34;com.zui.zhealthy\u0026#34;, \u0026#34;ctrip.android.view\u0026#34;, \u0026#34;io.kubenav.kubenav\u0026#34;, \u0026#34;org.geekbang.geekTime\u0026#34;, \u0026#34;tv.danmaku.bili\u0026#34; ], \u0026#34;stack\u0026#34;: \u0026#34;mixed\u0026#34;, \u0026#34;sniff\u0026#34;: true }, { \u0026#34;type\u0026#34;: \u0026#34;socks\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;socks-in\u0026#34;, \u0026#34;listen\u0026#34;: \u0026#34;::\u0026#34;, \u0026#34;listen_port\u0026#34;: 5353 } ] } 下面是对每个字段的详细注释：\n第一个入站连接的配置：\ntype: \u0026quot;tun\u0026quot; 表示这是一个 tun 虚拟网络接口的配置。 inet4_address: \u0026quot;198.18.0.1/16\u0026quot; 设定了虚拟网络接口的 IPv4 地址和子网掩码。 auto_route: true 表示将自动处理路由，确保数据包正确传输。 exclude_package: 这是一个数组，包含了不通过此虚拟网络接口处理的 Android 应用程序包名列表。列出的 Android 应用程序将使用常规网络接口而不是虚拟接口。 stack: \u0026quot;mixed\u0026quot; 表示混合 system TCP 栈与 gvisor UDP 栈。 sniff: true 表示启用流量嗅探功能，以便自动检测和处理传入的数据流类型。 第二个入站连接的配置：\ntype: \u0026quot;socks\u0026quot; 表示这是一个 SOCKS 代理配置。 tag: \u0026quot;socks-in\u0026quot; 为这个入站连接定义了一个标签，方便在其它配置中引用。 listen: \u0026quot;::\u0026quot; 表示监听所有 IPv6 地址。如果需要监听所有 IPv4 地址，可以使用 \u0026quot;0.0.0.0\u0026quot;。 listen_port: 5353 定义了 SOCKS 代理监听的端口号。 其中 tun 接口是核心部分，我们将利用 tun 接口来实现全局透明代理。\n出站配置 # 出站（Outbound）是指从本地网络或设备发出，向外部网络、服务或互联网发送的数据流量。示例出站配置如下：\n{ \u0026#34;outbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;selector\u0026#34;, // 类型为选择器，用于在多个出站中选择一个 \u0026#34;tag\u0026#34;: \u0026#34;select\u0026#34;, // 标签名为 \u0026#34;select\u0026#34; \u0026#34;outbounds\u0026#34;: [ \u0026#34;trojan-out\u0026#34; // 可选择的出站列表，这里只有 \u0026#34;trojan-out\u0026#34; ], \u0026#34;default\u0026#34;: \u0026#34;trojan-out\u0026#34; // 默认选择的出站为 \u0026#34;trojan-out\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;selector\u0026#34;, // 同样是选择器类型 \u0026#34;tag\u0026#34;: \u0026#34;openai\u0026#34;, // 标签名为 \u0026#34;openai\u0026#34; \u0026#34;outbounds\u0026#34;: [ \u0026#34;trojan-out\u0026#34; // 可选择的出站仍然是 \u0026#34;trojan-out\u0026#34; ], \u0026#34;default\u0026#34;: \u0026#34;trojan-out\u0026#34; // 默认选择的出站同样是 \u0026#34;trojan-out\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;selector\u0026#34;, // 选择器类型 \u0026#34;tag\u0026#34;: \u0026#34;tiktok\u0026#34;, // 标签名为 \u0026#34;tiktok\u0026#34; \u0026#34;outbounds\u0026#34;: [ \u0026#34;trojan-out\u0026#34; // 可选择的出站是 \u0026#34;trojan-out\u0026#34; ], \u0026#34;default\u0026#34;: \u0026#34;trojan-out\u0026#34; // 默认选择的出站为 \u0026#34;trojan-out\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;trojan\u0026#34;, // 类型为 Trojan \u0026#34;tag\u0026#34;: \u0026#34;trojan-out\u0026#34;, // 标签名为 \u0026#34;trojan-out\u0026#34; \u0026#34;server\u0026#34;: \u0026#34;xxxxxxxx\u0026#34;, // Trojan 服务器地址 \u0026#34;server_port\u0026#34;: 9443, // Trojan 服务器端口 \u0026#34;password\u0026#34;: \u0026#34;xxxxxxxx\u0026#34;, // Trojan 连接密码 \u0026#34;tls\u0026#34;: { \u0026#34;enabled\u0026#34;: true, // 启用 TLS 加密 \u0026#34;server_name\u0026#34;: \u0026#34;xxxxxxxx\u0026#34;, // TLS 服务器名称 \u0026#34;insecure\u0026#34;: true, // 不验证 TLS 证书，用于自签名证书 \u0026#34;utls\u0026#34;: { \u0026#34;fingerprint\u0026#34;: \u0026#34;chrome\u0026#34; // 使用 Chrome 的 TLS 指纹 } }, \u0026#34;multiplex\u0026#34;: { \u0026#34;protocol\u0026#34;: \u0026#34;h2mux\u0026#34;, // 使用 h2mux 多路复用协议 \u0026#34;max_connections\u0026#34;: 4, // 最大连接数为 4 \u0026#34;min_streams\u0026#34;: 4 // 每个连接的最小流数为 4 }, \u0026#34;transport\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;grpc\u0026#34;, // 传输协议为 gRPC \u0026#34;service_name\u0026#34;: \u0026#34;TunService\u0026#34; // gRPC 服务名称 } }, { \u0026#34;type\u0026#34;: \u0026#34;direct\u0026#34;, // 直连类型，不通过代理直接访问 \u0026#34;tag\u0026#34;: \u0026#34;direct\u0026#34; // 标签名为 \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;block\u0026#34;, // 阻止类型，用于拦截流量 \u0026#34;tag\u0026#34;: \u0026#34;block\u0026#34; // 标签名为 \u0026#34;block\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;dns\u0026#34;, // DNS 类型，用于 DNS 查询 \u0026#34;tag\u0026#34;: \u0026#34;dns-out\u0026#34; // 标签名为 \u0026#34;dns-out\u0026#34; } ] } 这个配置定义了不同类型的出站连接方式，包括选择器、Trojan、直连、阻止和 DNS 类型。每种类型都通过标签进行标识，便于在后续的路由规则中引用。\n路由配置 # 路由部分才是 sing-box 的核心配置，这个部分定义了一系列规则和参数，用于决定如何处理不同的网络请求。通过这些规则和参数，sing-box 可以非常灵活地处理复杂的路由需求，包括基于地理位置、IP 地址、端口号、域名等多种条件的流量分流。配置结构如下：\n{ \u0026#34;route\u0026#34;: { \u0026#34;rules\u0026#34;: [], \u0026#34;rule_set\u0026#34;: [], \u0026#34;final\u0026#34;: \u0026#34;direct\u0026#34;, // \u0026#34;final\u0026#34; 字段定义了默认的路由行为。这里设置为 \u0026#34;direct\u0026#34;，意味着如果没有匹配任何规则，流量将直接（不经代理）发送。 \u0026#34;auto_detect_interface\u0026#34;: true // 表示自动检测网络接口。这有助于自动适应网络变化，确保路由正确。 } } 其中的核心配置：\n路由规则 (rules): 这些规则定义了如何根据不同的条件将流量定向到不同的出站连接。每个规则可以包括多个条件，如域名、IP 地址、端口号、网络协议等。 规则集 (rule_set): 从 sing-box 1.8.0 版本开始，规则可以组合成规则集，这使得配置更加灵活和模块化。 路由规则 # 以下是我给出的路由规则示例：\n{ \u0026#34;route\u0026#34;: { \u0026#34;rules\u0026#34;: [ { \u0026#34;protocol\u0026#34;: \u0026#34;dns\u0026#34;, // 使用DNS协议的流量 \u0026#34;outbound\u0026#34;: \u0026#34;dns-out\u0026#34; // 将通过\u0026#39;dns-out\u0026#39;出口转发 }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;direct\u0026#34;, // Clash模式为直连 \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;clash_mode\u0026#34;: \u0026#34;global\u0026#34;, // Clash模式为全局 \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;domain_suffix\u0026#34;: [ // 特定后缀的域名 \u0026#34;icloudnative.io\u0026#34;, \u0026#34;fuckcloudnative.io\u0026#34;, \u0026#34;sealos.io\u0026#34;, \u0026#34;cdn.jsdelivr.net\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;process_name\u0026#34;: [ // 特定进程名称 \u0026#34;TencentMeeting\u0026#34;, \u0026#34;NemoDesktop\u0026#34;, ... ], \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;rule_set\u0026#34;: [ // 特定的规则集 \u0026#34;WeChat\u0026#34;, \u0026#34;Bilibili\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;protocol\u0026#34;: \u0026#34;quic\u0026#34;, // 使用QUIC协议的流量 \u0026#34;outbound\u0026#34;: \u0026#34;block\u0026#34; // 将被阻止 }, { \u0026#34;inbound\u0026#34;: \u0026#34;socks-in\u0026#34;, // 来自\u0026#39;socks-in\u0026#39;入口的流量 \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;rule_set\u0026#34;: \u0026#34;OpenAI\u0026#34;, // OpenAI规则集 \u0026#34;outbound\u0026#34;: \u0026#34;openai\u0026#34; // 将通过\u0026#39;openai\u0026#39;出口转发 }, { \u0026#34;domain_suffix\u0026#34;: [ // OpenAI相关的域名后缀 \u0026#34;openai.com\u0026#34;, \u0026#34;oaistatic.com\u0026#34;, \u0026#34;oaiusercontent.com\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;openai\u0026#34; // 将通过\u0026#39;openai\u0026#39;出口转发 }, { \u0026#34;package_name\u0026#34;: \u0026#34;com.openai.chatgpt\u0026#34;, // OpenAI ChatGPT应用包名 \u0026#34;outbound\u0026#34;: \u0026#34;openai\u0026#34; // 将通过\u0026#39;openai\u0026#39;出口转发 }, { \u0026#34;rule_set\u0026#34;: \u0026#34;TikTok\u0026#34;, // TikTok规则集 \u0026#34;outbound\u0026#34;: \u0026#34;tiktok\u0026#34; // 将通过\u0026#39;tiktok\u0026#39;出口转发 }, { \u0026#34;package_name\u0026#34;: \u0026#34;com.zhiliaoapp.musically\u0026#34;, // TikTok应用包名 \u0026#34;outbound\u0026#34;: \u0026#34;tiktok\u0026#34; // 将通过\u0026#39;tiktok\u0026#39;出口转发 }, { \u0026#34;domain_suffix\u0026#34;: [ // 特定的域名后缀 \u0026#34;depay.one\u0026#34;, \u0026#34;orbstack.dev\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;process_name\u0026#34;: [ // 特定的进程名称 \u0026#34;DropboxMacUpdate\u0026#34;, \u0026#34;Dropbox\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;package_name\u0026#34;: [ // 特定应用包名 \u0026#34;com.google.android.youtube\u0026#34;, ... ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;domain\u0026#34;: \u0026#34;accounts.google.com\u0026#34;, // 特定的域名 \u0026#34;domain_suffix\u0026#34;: [ // 特定的域名后缀 \u0026#34;sourceforge.net\u0026#34;, \u0026#34;fhjasokiwq.com\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;domain_suffix\u0026#34;: \u0026#34;cloud.sealos.io\u0026#34;, // 特定的域名后缀 \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;type\u0026#34;: \u0026#34;logical\u0026#34;, // 逻辑类型规则 \u0026#34;mode\u0026#34;: \u0026#34;and\u0026#34;, // 使用\u0026#39;and\u0026#39;模式 \u0026#34;rules\u0026#34;: [ // 组合规则 { \u0026#34;rule_set\u0026#34;: \u0026#34;geosite-geolocation-!cn\u0026#34; }, { \u0026#34;rule_set\u0026#34;: \u0026#34;geoip-cn\u0026#34;, \u0026#34;invert\u0026#34;: true } ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;rule_set\u0026#34;: \u0026#34;Global\u0026#34;, // Global规则集 \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 }, { \u0026#34;rule_set\u0026#34;: \u0026#34;geoip-cn\u0026#34;, // 中国地理位置IP规则集 \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;ip_is_private\u0026#34;: true, // 私有IP地址 \u0026#34;outbound\u0026#34;: \u0026#34;direct\u0026#34; // 将通过\u0026#39;direct\u0026#39;出口直接连接 }, { \u0026#34;rule_set\u0026#34;: [ // 特定的规则集 \u0026#34;YouTube\u0026#34;, \u0026#34;Telegram\u0026#34;, \u0026#34;Netflix\u0026#34;, \u0026#34;geoip-google\u0026#34;, \u0026#34;geoip-telegram\u0026#34;, \u0026#34;geoip-twitter\u0026#34;, \u0026#34;geoip-netflix\u0026#34; ], \u0026#34;outbound\u0026#34;: \u0026#34;select\u0026#34; // 将通过\u0026#39;select\u0026#39;出口选择转发 } ] } } 这个配置定义了不同类型的流量（如基于协议、域名后缀、应用包名、进程名称等）如何被路由。每条规则都指定了一种流量类型和相应的“出口”，即流量应该如何被处理或转发。这种灵活的路由配置可以非常精确地控制网络流量。\n规则集 # 以下是我给出的规则集示例：\n{ \u0026#34;route\u0026#34;: { \u0026#34;rule_set\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geosite-geolocation-!cn\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-cn\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-cn.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-google\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-google.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-telegram\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-telegram.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-twitter\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-twitter.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;geoip-netflix\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;binary\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/CHIZI-0618/v2ray-rules-dat/release/singbox_ip_rule_set/geoip-netflix.srs\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Global\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Global.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;YouTube\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/YouTube.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;OpenAI\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/OpenAI.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;TikTok\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/TikTok.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Telegram\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Telegram.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Netflix\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Netflix.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;WeChat\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/WeChat.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;remote\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;Bilibili\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;source\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://mirror.ghproxy.com/https://raw.githubusercontent.com/yangchuansheng/sing-box-geosite/main/rule/Bilibili.json\u0026#34;, \u0026#34;download_detour\u0026#34;: \u0026#34;direct\u0026#34; } ] } } 这里有两种不同类型的规则集，一种是 binary，另外一种是 source。binary 规则集一般都是利用 GEOSITE 或者 GEOIP 直接编译好的二进制规则，它们被直接嵌入到应用程序中。而 source 规则集就和 Clash 的 ruleset 比较类似，它是一个文本文件，而不是二进制。\n目前已经有相关项目可以自动将网络上的 Clash Ruleset 规则自动转换为 sing-box 的 source 规则集，感兴趣的同学可以参考这个项目： sing-box-geosite\nClash API # 最后的实验性配置用来开启 Clash API。没错，sing-box 是兼容 Clash API 滴！那么我们就可以使用 Clash 的 dashboard 来管理 sing-box 了，直接用这个项目好了： metacubexd\n示例配置如下：\n{ \u0026#34;experimental\u0026#34;: { \u0026#34;cache_file\u0026#34;: { \u0026#34;enabled\u0026#34;: true // 启用缓存文件功能。当此项设置为true时，启用 DNS 查询的缓存，以便加快后续相同查询的响应速度。 }, \u0026#34;clash_api\u0026#34;: { \u0026#34;external_controller\u0026#34;: \u0026#34;0.0.0.0:9090\u0026#34;, // 定义 Clash API 的外部控制器地址。\u0026#34;0.0.0.0:9090\u0026#34; 表示在本机的9090端口上监听外部的连接请求。 \u0026#34;external_ui\u0026#34;: \u0026#34;metacubexd\u0026#34;, // 指定外部用户界面(UI)的名称。这里的 \u0026#34;metacubexd\u0026#34; 是一个自定义 UI 的名称。 \u0026#34;external_ui_download_url\u0026#34;: \u0026#34;https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip\u0026#34;, // 提供外部 UI 的下载 URL。这个 URL 是从 GitHub 上下载 \u0026#34;metacubexd\u0026#34; UI 的压缩包。 \u0026#34;external_ui_download_detour\u0026#34;: \u0026#34;select\u0026#34;, // 定义下载外部 UI 时使用的转发策略。\u0026#34;select\u0026#34; 表示将通过\u0026#39;select\u0026#39;出口选择转发 \u0026#34;default_mode\u0026#34;: \u0026#34;rule\u0026#34; // 设置 Clash API 的默认模式。\u0026#34;rule\u0026#34; 模式意味着流量将根据用户定义的规则进行路由。 } } } 最终启动 sing-box 之后就可以通过 Clash dashboard 来查看和管理流量啦：\n注意： 图形界面客户端会自动把外部控制器相关的配置给屏蔽掉，如果你想使用 Dashboard，只能使用命令行来启动 sing-box。 订阅转换 # 我想大部分小伙伴使用的还是订阅链接，不可能傻乎乎的自己写配置和规则。但是目前大部分ji场都不提供 sing-box 的配置格式，仅有少量ji场提供支持，其他ji场可使用下面这个项目将常见订阅转换为 sing-box 订阅格式： sing-box-subscribe\n你可以将这个项目部署到自己的 Vercel 中，然后使用以下的链接格式来将常见订阅转换为 sing-box 订阅格式：\n\u0026lt;URL\u0026gt;/url=\u0026lt;subscription_url\u0026gt;/\u0026amp;file=\u0026lt;sing-box_template_url\u0026gt; \u0026lt;URL\u0026gt;：这是你的 sing-box-subscribe 访问链接； \u0026lt;subscription_url\u0026gt;：这是你的订阅链接； \u0026lt;sing-box_template_url\u0026gt;：这是你的 sing-box 模板配置链接，你可以直接使用 我的模板。 例如：\nhttps://sing-box-subscribe.vercel.app/config/url=https://xxxxxx?clash=1/\u0026amp;file=https://gist.githubusercontent.com/yangchuansheng/5182974442015feeeeb058de543a00fd/raw/45b11ff08188af021da98e7174923d719dc42dd9/gistfile1.txt 如果你有多个订阅链接，需要先将订阅链接合并为一个链接，然后再进行转换，具体看参考 sing-box-subscribe 的官方文档。\n更多配置示例 # 更多的配置示例可以参考这个项目： sing-box-examples\n这个项目针对每一个代理协议都提供了详细的配置示例，还有很多的骚操作，比如 将 Cloudflare 的 Warp 节点信息直接提取出来加到 sing-box 出站配置中去，妙啊！\n透明网关 # 如果你想让局域网中的所有机器都能够根据规则智能分流，那就在局域网中找一台机器作为透明网关，在这台机器上运行一个 sing-box 就行了，不需要像 Clash 一样写什么乱七八糟的 iptables 规则，直接一个配置文件就行了，非常简单。通常我们使用软路由来完成这个任务，如果你不想使用软路由，那随便找一台机器就行了，当然最好还是使用 Linux 比较靠谱。\n在网关上运行 sing-box 之后，其他机器只需要将网关指向这台机器，便可以无痛开启魔法智能分流了。\n注意： 其他机器的 DNS 必须是公网 DNS，不能使用内网 DNS！你的 DNS 可以指向任意的公网 DNS，反正只要是公网就行，比如：114.114.114.114，因为 sing-box 会劫持局域网中的所有 DNS 请求。 当然，如果你不想让 sing-box 劫持局域网中的所有 DNS 请求，可以使用如下的方案：\n首先在入站配置中添加一个监听端口：\n{ \u0026#34;inbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;dns-in\u0026#34;, \u0026#34;listen\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;listen_port\u0026#34;: 53 } ] } 然后在路由规则中将 DNS 的规则改成如下的配置：\n{ \u0026#34;route\u0026#34;: { \u0026#34;rules\u0026#34;: [ { \u0026#34;inbound\u0026#34;: \u0026#34;dns-in\u0026#34;, \u0026#34;outbound\u0026#34;: \u0026#34;dns-out\u0026#34; } ] } } 这样就保证了只有从 53 端口进来的流量才会进入 DNS 解析。\n重启生效后，将其他机器的网关和 DNS 均指向这台机器就可以了。\n如果你使用的是 DHCP，只需要在 DHCP 服务器中将 DHCP 分配的网关和 DNS 改成 sing-box 所在的机器 IP 即可。\n","date":"2024年1月14日","externalUrl":null,"permalink":"/posts/sing-box-tutorial/","section":"博客","summary":"sing-box 是什么 # sing-box 是新一代超强通用代理工具，对标 *ray core 与 clash，","title":"sing-box 基础教程：sing-box 的配置方法和使用教程","type":"posts"},{"content":"","date":"2023年12月20日","externalUrl":null,"permalink":"/tags/chatgpt/","section":"标签","summary":"","title":"ChatGPT","type":"tags"},{"content":" 原文链接： Building a Universal AI Scraper\n译者水平有限，不免存在遗漏或错误之处。如有疑问，敬请查阅原文。\n以下是译文。 最近，我一直在研究网页抓取技术。鉴于人工智能领域的快速发展，我尝试构建一个 “通用” 的网页抓取工具，它可以在网页上迭代遍历，直到找到需要抓取的信息。这个项目目前还在开发中，这篇文章我将分享一下该项目目前的进展。\n目标愿景 # 给定一个初始网址和一个高层次目标，该网页抓取工具需能够：\n分析给定网页的内容； 从相关部分提取文本信息； 进行必要的页面交互； 重复上述步骤，直至达成目标。 使用的工具 # 尽管这是一个纯后端工程，但我使用了 NextJs 作为开发框架，便于未来扩展前端。网页抓取部分选择了 Crawlee 库，这是一个基于 Playwright 的浏览器自动化库。Crawlee 对浏览器自动化进行了优化，使爬虫能更好地模仿人类用户。Crawlee 还提供了请求队列系统，便于按顺序管理大量请求，这对于未来部署服务很有帮助。\nAI 部分主要使用了 OpenAI 的 API 接口和 Microsoft Azure 的 OpenAI 服务，总共使用了三个模型：\nGPT-4-32k (‘gpt-4-32k’) GPT-4-Turbo (‘gpt-4-1106-preview’) GPT-4-Turbo-Vision (‘gpt-4-vision-preview’) 相比原版 GPT-4，GPT-4-Turbo 模型上下文窗口更大 (128k 令牌)，速度更快 (最高提速 10 倍)，但智能程度略低。在一些复杂情况下就显得欠灵活，这时我会使用 GPT-4-32K 获取更高的智能。\nGPT-4-32K 是 GPT-4 的改良变体，上下文窗口为 32k，远远超过 4k。由于 OpenAI 当前限制对该模型的访问，我最终选择通过 Azure 的 OpenAI 服务来访问该模型。\n起步 # 我从需求约束出发，反向设计。由于底层使用 Playwright 爬虫，我知道如果要与页面交互，最终必须要从页面中获取元素的选择器。\n元素选择器是一个字符串，用于唯一标识页面上的某个元素。例如，如果我想选取页面上的第四个段落，我可以使用 p:nth-of-type(4) 作为选择器。如果我要选择一个写着 ‘Click Me’ 的按钮，我可以用 button:has-text('Click Me') 这个选择器。Playwright 通过选择器先锁定目标元素，然后对其执行特定的动作，比如点击 'click()' 或填充 'fill()'。\n因此，我的首要任务是理解如何从给定的网页中识别出 “目标元素”。从现在起，我会将这一过程称为 ‘GET_ELEMENT’。\n获取 “目标元素” 的方法 # 方法 1：截图 + 视觉模型 # HTML 数据通常都很复杂和冗长。大部分内容用于定义样式、布局和交互逻辑，而非文本内容本身。我担心文本模型处理这种情况效果欠佳，所以我的想法是使用 GPT-4-Turbo-Vision 模型直接 “查看” 渲染后的页面，抄录出最相关的文本，然后在源 HTML 中搜索包含该文本的元素。\n但这个方法很快就失败了：\nGPT-4-Turbo-Vision 有时会拒绝我的抄录文本请求，说 “对不起，我无法帮助你完成这项任务” 等。有一次，它甚至声称 “不能从有版权图片中抄录文本”。看来 OpenAI 在努力限制它帮助执行这类任务 (不过，如果我告诉它自己是盲人似乎可以绕过这个限制)。\n随后出现了更严峻的问题：大页面的截图高度往往很夸张 (\u0026gt;8000 像素)。这是个问题，因为 GPT-4-Turbo-Vision 会将所有图像预处理调整为固定尺寸。我发现超高图像在预处理后可能会严重变形，无法辨认。\n一种可能的解决方案是分段扫描页面，逐段总结后再拼接。但鉴于 OpenAI 对 GPT-4-Turbo-Vision 的速率限制，我不得不建立一个队列系统来进行流程管理，听起来就很麻烦。\n此外，仅从文本反推出有效的元素选择器也非常困难，因为你不知道底层 HTML 的结构。基于以上原因我决定放弃这种方法。\n方法 2：HTML + 文本模型 # 纯文本的 GPT-4-Turbo 速率限制较宽松，上下文窗口有 128k，所以我试着直接输入整个页面 HTML，要它识别相关元素。\n尽管 HTML 数据基本符合 (大多数情况下)，但我发现 GPT-4-Turbo 模型的智能程度仍不足以正确无误地完成这项工作。它们经常识别错误的元素，或者给出范围过广的选择器。\n所以我试着进一步简化 HTML 代码，只保留 body 部分并移除脚本和样式标签，隔离主体 HTML 以缩小范围，这有一定帮助，但问题依旧存在。对语言模型来说，从整个页面准确识别 “相关” HTML 元素是一个过于复杂和不确定的任务，我需要某种方法将候选元素范围缩减到仅剩几个，然后再手动提交给文本模型。\n接下来，我决定从人类解决类似问题的方法中寻找灵感。\n方法 3：HTML + 文本搜索 + 文本模型 # 如果我要在网页上查找特定信息，通常会使用 “Control” + “F” 来搜索关键词。如果第一次没有找到，我会尝试不同关键词直到找到需要的信息。\n这种方法的优点是简单的文本搜索非常快速且容易实现。在我的场景下，搜索词可通过文本模型生成，搜索本身可以在 HTML 上通过简单正则表达式完成。\n虽然生成搜索词的速度可能比搜索本身稍慢，但我会让文本模型一次性生成多个关键词，并同时对它们进行搜索。包含搜索词的任何 HTML 元素都收集起来，下一步送给 GPT-4-32K 选出最相关的一个元素。\n当然，如果使用足够多的搜索词，可能会获取很多 HTML 数据，这可能会触发 API 限制或者影响后续步骤的性能。所以我设计了一种方案，它可以智能地填充相关元素列表，直到达到一个预设长度。\n我要求 Turbo 模型挑选出 15-20 个词条，并按预估相关性从高到低排序。然后我用简单的正则表达式在 HTML 中搜索包含每个词条的所有元素。到这步结束时，我会得到一个由多个子列表组成的列表，其中每个子列表包含匹配某词条的所有元素。\n接下来，我会用这些列表中的元素填充一个最终列表，并优先考虑那些出现在较早列表中的元素。例如，假设排名搜索词为：\u0026lsquo;pricing\u0026rsquo;、\u0026lsquo;fee\u0026rsquo;、\u0026lsquo;cost\u0026rsquo; 和 \u0026lsquo;prices\u0026rsquo;。在填充最终列表时，我会首选 \u0026lsquo;pricing\u0026rsquo; 列表中的元素，然后是 \u0026lsquo;fee\u0026rsquo; 列表，再到 \u0026lsquo;cost\u0026rsquo; 列表，依此类推。\n一旦最终列表达到预定义的令牌长度，我就会停止填充。这样做可以确保我在进行下一步时，不会超过令牌的最大限制。\n如果您对该算法代码感兴趣，这里有一个简化版本：\n这种方法使我能够最终获得一个长度合适、内容丰富的列表，它包含了来自各种搜索词的匹配元素，同时也优先考虑了排名更高相关词。\n但随后，我遇到了一个新问题：有时你需要的信息并不直接出现在匹配元素中，而是存在于它的同级元素或父元素里。\n例如 AI 试图找出古巴的首都。它搜索 “capital” 一词并匹配到橙框中的元素。但我们需要的信息实际上在绿色元素中——一个同级元素。我们已经非常接近答案了，但如果不同时考虑这两个元素，就无法解决问题。\n为解决此问题，我在元素搜索函数中添加了 “父元素” 作为可选参数。将父元素设置为 0 时意味着搜索函数只会返回直接包含文本的那个元素 (当然也包括该元素的子元素)。\n将父元素设置为 1 意味着返回直接包含文本元素的父元素。设置为 2 则返回祖父元素，以此类推。在这个古巴的例子中，设置父元素为 2 会返回整个红色区域的 HTML 代码。\n我决定将默认的父元素设置为 1，更高的值可能会捕获过多的 HTML。\n现在我们已经获得了一个大小合适的列表，其中包含有帮助的父元素上下文。是时候进入下一步了：我想请 GPT-4-32K 帮我从这个列表中选择最相关的元素。\n这一步非常简单，但要找到合适的提示词还需要一些试错：\n这个步骤完成后，我就会得到页面上最相关的一个元素。然后将其传入下一流程，在那里 AI 模型将决定完成目标需要什么样的交互。\n搭建助理 # 提取相关元素的流程虽然可行，但存在一定的缓慢和随机性。我现在迫切需要的，是一个类似 “计划员” 的 AI，在前一步骤结果不佳时，它可以查看结果并使用不同的搜索关键词进行再次尝试。\n幸运的是，这正是 OpenAI 的 Assistant API 所提供的功能。“Assistant” 是一个模型，通过额外逻辑封装，允许它利用自定义工具自主操作，直到达成目标。可以通过设置基础模型类型、定义可用工具列表以及发送消息来初始化这个助理。\n初始化助理后，可以轮询 API 来跟踪其状态。如果它决定使用自定义工具，状态会显示它要用的工具和参数。这时，你可以产生相应的工具输出并传回给助理，让它继续完成任务。\n在这个项目中，我基于 GPT-4-Turbo 模型搭建了一个助理，并给它加了一个特别的工具，能触发我最新设计的 GET_ELEMENT 函数。\n这是我为 GET_ELEMENT 工具提供的描述：\n您会注意到，这个工具不仅能够提供与搜索词最相关的元素，还能返回每个搜索词匹配的元素数量。这一信息对于助手来说非常重要，可以帮助它判断是否需要用不同的搜索词进行重试。\n通过这个工具，助理现在能够完成我目标愿景的前两个步骤：分析指定的网页并从中提取相关的文本信息。在不需要页面交互的情况下，这已经足够了。例如，如果我们想知道一个产品的价格，且这个价格信息正包含在工具所返回的元素中，助理可以直接提取这部分文本信息。\n但是，如果目标需要页面交互，助理还需要决定要进行的交互类型，然后使用额外工具来进行互动。我把这个额外工具称为 INTERACT_WITH_ELEMENT。\n与相关元素进行交互 # 为了制作一个能与特定网页元素进行交互的工具，我原本认为需要构建一个自定义的 API 来把 大型语言模型（LLM）返回的字符串响应转换成 Playwright 命令。但是后来我意识到，我所使用的模型已经熟练掌握了 Playwright API 的使用 (这是它作为一个流行库的好处！)。所以我决定直接以异步立即调用的函数表达式 (IIFE) 的形式生成命令。\n最终，我的方案变成了：\n助理会提供它想要执行的交互描述，我用 GPT-4-32K 来编写实现这些交互的代码，然后在我的 Playwright 爬虫中执行这些代码。\n这是我为 INTERACT_WITH_ELEMENT 工具提供的描述：\n你会注意到，助理在操作时并没有写出完整的元素，而是只提供了一个简短的标识符，这样做更为快捷和高效。\n下面是我给 GPT-4-32K 的提示词，以帮助它编写代码。我考虑到在与网页交互之前，可能存在我们需要提取的相关信息，所以我告诉它在函数中将提取的信息赋值给函数内名为 actionOutput 的变量。\n我将这一步的字符串输出 (我称之为 “action”) 作为参数传递给我的 Playwright 爬虫，并使用 “eval” 函数将其作为代码执行 (我知道这可能会有危险)：\n如果你想知道为什么我不直接让助理提供它的交互代码，那是因为我所使用的 Turbo 模型太笨了，无法可靠地编写命令。所以我助理描述它想要的交互方式 (比如“点击此元素”)，然后我使用更强大的 GPT-4-32K 模型来编写代码。\n传递页面状态 # 到了这一步，我意识到我需要一种方法来向助理传递页面的当前状态。我希望它能够根据它所在的页面来制定搜索策略，仅仅依靠 URL 感觉还不是很理想。而且，有时我的爬虫无法正确加载页面，我希望助理能检测到这一点然后重试。\n为了获取这些额外的页面上下文，我决定制作一个新函数，使用 GPT-4-Vision 模型来总结页面顶部 2048 像素的内容。我在两个关键位置插入了这个函数：一是在最初，用于分析起始页面；二是在 INTERACT_WITH_ELEMENT 工具完成后，以便助手可以理解它的交互结果。\n有了最后这一个环节，助理现在能够准确判断某一交互是否按预期进行，或者是否需要重试。这在页面弹出验证码或其他弹窗时特别有用。在这种情况下，助理就会知道必须先解决这些障碍，然后才能继续操作。\n最终流程 # 让我们回顾一下前面所说的整个流程：先为助理提供 URL 和目标。然后，助理使用 “GET_ELEMENT” 工具从页面中提取最相关的元素。\n如果需要进一步的交互，助理将使用 “INTERACT_WITH_ELEMENT” 工具来编写和执行相关交互的代码。它将循环这个过程，直到找到最终的结果。\n现在，我们将通过测试助手在维基百科上搜寻答案的能力，来检验它的实际运作效果。\n调试助理 # 我的最终目标是构建一个能够适应任何网页环境的通用网络爬虫。不过，作为初步测试，我想先看看它在维基百科这种内容可靠的环境下的工作效果，因为维基百科的每个页面都包含了大量指向其他页面的链接。在这样一个资源丰富的领域里，助理应该能够轻松找到所需信息。\n我让助理查看了美国维基百科页面，然后告诉它：“我想知道莫哈韦沙漠的总面积。”\n美国维基百科页面包含接近 150 万个字符的 HTML 内容，大概相当于 375,000 个词元 (token)。这是一个测试系统处理大量数据能力的良机。\n如我预料的那样，助理使用了 “GET_ELEMENT” 工具，但它最初使用的搜索词效果不佳。这些搜索词可能过于具体，难以在页面上完全匹配到：\nTOOL CALL] 🔧 get_element [REQUIRES_ACTION] Running 1 functions... { \u0026#34;url\u0026#34;: \u0026#34;https://en.wikipedia.org/wiki/United_States\u0026#34;, \u0026#34;searchTerms\u0026#34;: [ \u0026#34;Mojave Desert link\u0026#34;, \u0026#34;Mojave link\u0026#34;, \u0026#34;desert link\u0026#34;, \u0026#34;link Mojave\u0026#34;, \u0026#34;link desert\u0026#34; ], \u0026#34;directive\u0026#34;: \u0026#34;Find a link to the Mojave Desert page to get information on its total land area\u0026#34; } 搜索工具在所有搜索词中没有找到任何匹配元素。\n于是，助手决定再尝试一次，这次它使用了更加通用的多个搜索词：\n[TOOL CALL] 🔧 get_element [REQUIRES_ACTION] Running 1 functions... { \u0026#34;url\u0026#34;: \u0026#34;https://en.wikipedia.org/wiki/United_States\u0026#34;, \u0026#34;searchTerms\u0026#34;: [ \u0026#34;Mojave\u0026#34;, \u0026#34;Desert\u0026#34;, \u0026#34;Mojave Desert\u0026#34;, \u0026#34;geography\u0026#34;, \u0026#34;landscape\u0026#34;, \u0026#34;environment\u0026#34;, \u0026#34;Nature\u0026#34;, \u0026#34;ecosystem\u0026#34;, \u0026#34;biome\u0026#34;, \u0026#34;land\u0026#34;, \u0026#34;climate\u0026#34;, \u0026#34;terrain\u0026#34;, \u0026#34;Landforms\u0026#34;, \u0026#34;physical\u0026#34; ], \u0026#34;directive\u0026#34;: \u0026#34;Find a link to the Mojave Desert page on Wikipedia from the United States page\u0026#34; } 该工具在这些搜索词中找到了 134 个匹配元素，共计超过 300 万个字符 (可能是因为这些元素间存在大量重叠)。幸运的是，之前提到的用于筛选最终内容列表的算法能够将其缩减为 41 个元素的列表 (我将结果数限制在 10,000 个字符以内)。\n然后，GPT-4-32K 选择了一个最相关的元素，其中包含指向莫哈韦沙漠维基百科页面的链接：\n\u0026lt;p\u0026gt; \u0026lt;!-- Abbreviated for readability --\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;a href=\u0026#34;/wiki/Sonoran_Desert\u0026#34; title=\u0026#34;Sonoran Desert\u0026#34;\u0026gt;Sonoran\u0026lt;/a\u0026gt;, and \u0026lt;a href=\u0026#34;/wiki/Mojave_Desert\u0026#34; title=\u0026#34;Mojave Desert\u0026#34;\u0026gt;Mojave\u0026lt;/a\u0026gt; deserts. \u0026lt;sup id=\u0026#34;cite_ref-179\u0026#34; class=\u0026#34;reference\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;#cite_note-179\u0026#34;\u0026gt;[167]\u0026lt;/a\u0026gt; \u0026lt;/sup\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/p\u0026gt; 如果你想知道为什么这个元素除了链接本身以外还包含额外的 HTML 代码，那是因为我将 “父元素” 参数设置为 1，这意味着所有匹配到的元素都将与它们的直接父元素一起返回。\n在接收到 “GET_ELEMENT” 工具输出的那个元素之后，助理决定利用 “INTERACT_WITH_ELEMENT” 工具尝试点击那个链接：\n[NEW STEP] 👉 [{\u0026#34;type\u0026#34;:\u0026#34;function\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;interact_with_element\u0026#34;}] Running 1 function... { \u0026#34;elementCode\u0026#34;: \u0026#34;16917\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://en.wikipedia.org/wiki/United_States\u0026#34;, \u0026#34;directive\u0026#34;: \u0026#34;Click on the link to the Mojave Desert page\u0026#34; } 此过程中，助手使用 GPT-4-32K 将其想法转换成 Playwright 的一个操作：\nRunning writeAction with azure32k... Write Action Response: \u0026#34;(async () =\u0026gt; {\\n await page.click(\u0026#39;p a[href=\\\u0026#34;/wiki/Mojave_Desert\\\u0026#34;]\u0026#39;);\\n})();\u0026#34; 我的 Playwright 爬虫执行了这个操作，浏览器成功导航到了莫哈韦沙漠页面。\n最后，我使用 GPT-4-Vision 处理了新页面，并将浏览器状态的摘要作为工具输出反馈给了助手：\nSummarize Status Response: \u0026#34;We clicked on a link to the Wikipedia page for the Mojave Desert. And now we are looking at the Wikipedia page for the Mojave Desert.\u0026#34; 助手认为目标还没有达成，所以它在新页面上重复了上述流程。同样，它最初使用的搜索词过于具体，获取到的信息很少。但是在第二次尝试中，它想到了以下这些搜索词：\n[TOOL CALL] 🔧 get_element [REQUIRES_ACTION] Running one function... { \u0026#34;url\u0026#34;: \u0026#34;https://en.wikipedia.org/wiki/Mojave_Desert\u0026#34;, \u0026#34;searchTerms\u0026#34;: [ \u0026#34;square miles\u0026#34;, \u0026#34;square kilometers\u0026#34;, \u0026#34;km2\u0026#34;, \u0026#34;mi2\u0026#34;, \u0026#34;area\u0026#34;, \u0026#34;acreage\u0026#34;, \u0026#34;expansion\u0026#34;, \u0026#34;size\u0026#34;, \u0026#34;span\u0026#34;, \u0026#34;coverage\u0026#34; ], \u0026#34;directive\u0026#34;: \u0026#34;Locate the specific section or paragraph that states the total land area of the Mojave Desert on the Wikipedia page\u0026#34; } “GET_ELEMENT” 工具最初找到了 21 个匹配元素，总计 491,000 个字符，后来缩减至 12 个。然后 GPT-4-32K 从这些匹配项中选择了最相关的一个，里面包含了搜索词 “km2”：\n\u0026lt;tr\u0026gt; \u0026lt;th class=\u0026#34;infobox-label\u0026#34;\u0026gt;Area\u0026lt;/th\u0026gt; \u0026lt;td class=\u0026#34;infobox-data\u0026#34;\u0026gt;81,000\u0026amp;nbsp;km\u0026lt;sup\u0026gt;2\u0026lt;/sup\u0026gt;(31,000\u0026amp;nbsp;sq\u0026amp;nbsp;mi)\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; 这个元素对应页面渲染的这一部分：\n这种情况下，如果我没有把 “parents” 设为 1，是无法找到所需答案的，因为我们要找的答案实际上位于与匹配元素相邻的元素中，就像之前与古巴相关的例子一样。\n“GET_ELEMENT” 工具把这个元素反馈给助理，助理准确识别出这些信息满足了我们的查询需求。因此，它完成了任务，并告诉我问题的答案是 81,000 平方公里：\n[FINAL MESSAGE] ✅ The total land area of the Mojave Desert is 81,000 square kilometers or 31,000 square miles. { \u0026#34;status\u0026#34;: \u0026#34;complete\u0026#34;, \u0026#34;info\u0026#34;: { \u0026#34;area_km2\u0026#34;: 81000, \u0026#34;area_mi2\u0026#34;: 31000 } } 如果您想阅读本次程序运行的完整日志，可以在 这里查看。\n总结 # 在整个项目的构建过程中，我获得了很多乐趣，也学到了很多有用的知识。然而不得不承认，这套系统还很脆弱，有很多地方亟待完善。接下来我将继续优化这个项目，以下是我想继续改进的部分：\n生成更智能的搜索词，以便更快地找到相关元素。\n在我的 “GET_ELEMENT” 工具中实现模糊搜索，以适应文本中的细微变化。\n使用视觉模型对 HTML 中的图像和图标进行标记，以便助理可以与之交互。\n通过住宅代理和其他技术增强爬虫的隐蔽性。\n","date":"2023年12月20日","externalUrl":null,"permalink":"/posts/ai-scraper/","section":"博客","summary":"原文链接： Building a Universal AI Scraper 译者水平有限，不免存在遗漏或错误之处。如","title":"如何构建一个通用的 AI Web 爬虫","type":"posts"},{"content":"","date":"2023年12月20日","externalUrl":null,"permalink":"/tags/scraper/","section":"标签","summary":"","title":"爬虫","type":"tags"},{"content":"","date":"2023年12月17日","externalUrl":null,"permalink":"/tags/android/","section":"标签","summary":"","title":"Android","type":"tags"},{"content":"","date":"2023年12月17日","externalUrl":null,"permalink":"/tags/english/","section":"标签","summary":"","title":"English","type":"tags"},{"content":"","date":"2023年12月17日","externalUrl":null,"permalink":"/tags/magisk/","section":"标签","summary":"","title":"Magisk","type":"tags"},{"content":"","date":"2023年12月17日","externalUrl":null,"permalink":"/tags/tiktok/","section":"标签","summary":"","title":"TikTok","type":"tags"},{"content":"抖音，一个让全中国都痴迷的短视频分享平台，已经成为年轻一代表达自我和探索创意的重要工具。\n但你可能不知道的是，抖音在国际市场上有一个 “孪生兄弟”——TikTok。虽然两者在核心功能上极为相似，但它们针对的受众和运营策略却有所不同。TikTok 作为抖音的国际版本，不仅继承了抖音的精髓，还融入了全球文化的多样性，吸引了全球范围内的用户。\n抖音在国内火得一塌糊涂，但让人想不到的是它的国际版 TikTok 在海外仍然火得一塌糊涂，已连续两年霸榜 App 下载排行榜榜首了。但是中国大陆境内却是无法使用 TikTok 的，这是为什么呢？\n国内无法使用 TikTok 的原因 # 尽管 TikTok 是由中国的字节跳动公司开发，但它主要服务于国际市场，与国外的社交平台如 Facebook 和 Instagram 类似。由于其全球定位，TikTok 并没有因为其 “国产” 身份在中国获得特殊待遇。实际上，当国内用户尝试使用 TikTok 时，他们通常会遭遇一个问题：应用界面显示 “无网络连接”，就像是进入了一个黑暗的虚拟空间。这种情况的背后有两个主要原因：\n首先，像 Google 和 Facebook 一样，TikTok 也受到中国大陆的互联网限制。这意味着即使在国内成功下载和安装了 TikTok，用户仍然无法正常访问其服务。更让人无语的是，TikTok 会检测用户手机中的 SIM 卡信息。如果识别出是中国大陆的三大运营商之一，应用就会自动屏蔽服务。\n其次，从商业战略的角度来看，字节跳动公司并无必要在中国大陆推广 TikTok。他们已经在国内成功运营了抖音，开放 TikTok 在国内的使用权就会与自家的抖音形成不必要的内部竞争。\n那么国内的小伙伴就真的无法使用 TikTok 了么？办法肯定是有的，而且五花八门，比如拔掉 SIM 卡，或者买一个国外的 SIM 卡等等，但这些方法非常不适合主力手机，虽然搞个备用机或者 iPad 可以解决 SIM 卡的问题，但我刷 TikTok 是为了干嘛的？都没法在主力手机上刷，我还用个毛？\n本文将会给你传授如何免拔卡使用 TikTok 的方法，其他的方法都是鸡肋，不用看了。\n注意：TikTok 所有的操作都需要魔法上网，这是大前提，没有这个大前提，其他所有操作都免谈。 国内可以用 TikTok 来干什么？ # 那当然是学英语啊！\n偶然发现 Twitter 上一位大佬的帖子，觉得这个方法甚妙，是全世界所有人学英语的最佳方案，没有之一。\n没有那么多花里胡哨的理论和步骤，不需要每天强制给自己定什么狗屁计划，没有任何心智负担，就是特么打开 TikTok 开始刷短视频。唯一的计划就是：只要你想娱乐，只要你想刷手机，就给我打开 TikTok 刷短视频，把烈性海洛因当成药来用，给我以毒攻毒！\nTikTok 里的语音非常口语化，也有不少是专门教英语的，口音也更丰富一些，老人的含混，小孩的快速，中东印度的口音也很常见。\n注册 TikTok 账号 # 在正式使用手机安装使用 TikTok 之前，你需要注册一个 TikTok 账号，这个非常简单，直接电脑或者手机打开 TikTok 网页注册就行。建议使用海外邮箱注册，比如 Gmail 邮箱或者 Outlook 邮箱等。当然，如果你有 Google Voice 等美国虚拟手机号，也可以用手机号注册。\n安卓免拔卡使用 TikTok # 首先我们来介绍如何在安卓手机上免拔卡安装使用 TikTok。\nTikTok 破解版 # 安卓手机最简单的使用方式就是下载破解版 TikTok，此方法简单无脑，安装完了就可以使用。这个破解版是俄罗斯人构建的，支持非常多的功能：\n去所有广告、去保存视频水印。 内置自定义全球区域功能向导。 可以自定义视频下载保存位置。 解除国家/地区限制，无视区域封锁。 解除所有下载限制，可以保存任何视频。 解除合拍和拼接限制，移除了调试信息。 添加了播放进度条，支持手机号码登陆。 为下载视频文件的名称添加了作者标签。 修正谷歌授权、Facebook 授权、VK 授权。 GIF 和视频保存路径重定向到 Movies/TikTok。 禁用不必要活动控件、禁用所有类型分析、禁用统计分析、对齐优化、极限压缩。 启用观看历史、优化电池消耗、禁用自动启动，隐藏的根权限，删除许多其他限制。 强制启用高画质视频、强制启用高品质音频、强制启用超清分辨率、并启用抗锯齿。 官方频道： TikTokModCloud\n官方频道的下载链路比较深，你也可以从一些第三方网站或者频道下载：\nROCKMODS 破解软件中文频道 这个破解版可以配合 TikTok 插件 (TikTokPlugin) 使用，该插件可以自定义设置，用于配合此破解版选择全球区域！\n友情提醒：破解软件可能含有恶意软件，如病毒、木马、间谍软件等，如果执意要使用破解版，请自行承担一切可能的后果！ TikTok 官方版 # 破解版虽然香，但是它有风险啊。\n如果你对破解版不放心，担心它有恶意软件，下面这个方法就是为你准备的。既不用拔卡，也不用更改手机地区和语言，而且可以安装 TikTok 官方版！\n但是，这个方案非常的复杂，如果你不想折腾，建议还是使用破解版。下面言归正传。\n这个方案有一个前提条件：你的手机需要解锁 BootLoader！\n如果你的手机无法解锁 Bootloader，下面的步骤就不用看了。\n安装系统修改工具 # 第一步我们需要安装系统修改工具，顺便获取 root 权限。目前有两款主流的安卓系统修改工具：\nMagisk：开源的 Android 系统修改工具，它主要用于在不破坏系统完整性的情况下进行系统修改和定制。Magisk 的目标是提供一个可靠的方式来实现 Root 权限管理、模块化修改和隐藏 Root 状态等功能。 KernelSU：KernelSU 是 Android GKI 设备的 root 解决方案，它工作在内核模式，并直接在内核空间中为用户空间应用程序授予 root 权限。同时还提供了一个基于 overlayfs 的模块系统，允许您加载自定义插件到系统中。 推荐使用 KernelSU，毕竟人家工作在内核模式，对于一些强制检测 root 的 App 隐藏效果更好。具体安装方法请参考 KernelSU 的官方文档。\n如果你更倾向于使用 Magisk，请自己搜索安装教程，网上教程比较多，特别是 酷安，你可以下载一个酷安 App，这里是安卓发烧友的聚集地。\n启用 Zygisk # Zygisk，顾名思义，就是注入 Zygote 后的 Magisk。它能为 Magisk 模块，提供更深入、更强悍的修改能力。它有一个排除列表，可以撤销 Magisk 做的所有修改。这样你就能手动划定，模块起作用的范围。\n有了 Zygisk，我们才可以安装另一个大杀器：LSposed。不过这是后话，下一步我们再介绍 LSposed。\n从 Zygisk 的命名就可以看出来，这是 Magisk 的功能，但是 KernelSU 也不用慌，有人已经将其提取为一个独立的项目，为 KernelSU 提供 Zygisk API 支持，并替换 Magisk 内置的 Zygisk。项目名称： Zygisk Next\nKernelSU 用户直接在 Zygisk Next 的 Release 页面下载 zip 模块包：\n然后打开 KernelSU 的管理 App，点击右下角的 “安装” 刷入 KernelSU 即可。\nMagisk 用户就更简单了，直接在 Magisk 的设置里打开 Zygisk 即可。\n刷入 LSposed # LSposed 是一个基于 Xposed Framework 的开源项目，用于在 Android 设备上进行系统级的模块化定制和修改。Xposed Framework 允许用户在不修改 Android 系统源代码的情况下，通过模块来实现各种定制和功能增强。\n与 KernelSU 和 Magisk 不同，LSposed 的每个模块都是一个 App，App 是有 GUI 界面的，你可以打开 App 进行各种设置。而 KernelSU 和 Magisk 的模块并不是 App，没有 GUI 界面，就是一堆脚本和文件。\n但是 LSposed 需要作为模块刷入 KernelSU 或者 Magisk，这是为什么呢？因为 LSposed 对系统的修改是不可撤销的，而 Zygisk 可以撤销对系统的修改 (玩过容器的同学应该都懂，所谓的可撤销实际上和容器原理类似，就是挂载一个虚拟的文件系统，你挂载到这个文件系统上随便改，对原来的系统没有任何侵入)，因此有人想出了这个套娃的方案！\n将 LSposed 套娃到 KernelSU 或者 Magisk，就不会对系统产生任何侵入。\n所以 LSposed 和 KernelSU/Magisk 是互补关系，相辅相成。\nLSposed 的刷入非常简单，直接下载对应 Zygisk 的 LSPosed 版本并在 KernelSU 或者 Magisk 中刷入，然后重启手机。\n安装 TikTok # 直接从 Google Play 应用商店安装官方版本的 TikTok。\n如果你的手机没有 Google Play，可以到一些第三方镜像站去下载安装，比如： apkpure\n切记：安装完 TikTok 之后千万不要打开！千万不要打开！千万不要打开！ 修改 SIM 卡信息 # 接下来安装可以修改 SIM 卡信息的相关模块 Guise。直接在 Guise 的 Release 页面下载最新的 apk：\n然后在手机中安装即可。安装完成后打开 LSposed，点击 Guise 模块，然后指定模块作用域为 TikTok，并 “启用模块”。最好再重启一下手机。\n然后打开 Guise 应用，将 SIM 卡运营商伪装成美国的运营商，同时将系统语言伪装成英文。\n打开 TikTok # 最终我们就可以打开 TikTok 开始愉快地看视频了，账号登录、点赞、收藏、关注、评论都可正常使用。\niOS 免拔卡使用 TikTok # iOS 免拔卡使用 TikTok 的方案与安卓类似，大致分为两种方案：\n一是在线安装破解版，直接在 iOS 上用 Safari 浏览器打开这个页面： https://jiesuo.tk/，然后根据说明进行操作即可。\n二是直接利用魔法上网软件的 rewrite 规则来绕过 TikTok 的限制，具体可参考这个项目： TikTok-Unlock\n如果你是 Stash 用户，可以从这里获取规则： https://github.com/blackmatrix7/ios_rule_script/tree/master/external/Stash/TikTokUnlock\n这个方法有一个难点是需要先安装旧版 TikTok，具体的方法是从 iTunes for Windows V 12.6.5.3 抓包 TikTok Version 21.1.0 进行安装，很多人没有 Windows 操作系统，可能比较麻烦。\n有一个比较简单的方法是在这个页面中先在线安装旧版： https://jiesuo.tk/shadowrocket/，然后再使用上面项目中的 rewrite 规则。\n问题解决 # 如果你在登录 TikTok 过程中遇到以下提示：\nToo many attempts, please try again later 首先，我们需要知道 TikTok 为何会出现 “频繁访问” 提示。这是因为 TikTok 为了保护用户账号和信息安全，设置了访问频率的限制，一旦用户在短时间内操作频繁，就会认为有异常行为，从而强制停止相关操作。为了避免这种情况，我们可以尝试以下方法。\n暂停操作\n如果您看到频繁访问提示，请暂停您的操作。尝试等待一段时间 (通常为几小时或一天)，然后重新尝试登录或执行其他操作。此外，您还可以尝试更换设备或网络环境，因为可能是因为您操作的设备或网络环境引起了这个问题。\n尝试用其他方式登录\n有时，您可能无法正常登录 TikTok，但可以使用其他方式登录，例如使用您的电话号码或 Facebook 账号登录。如果您遇到这种情况，可以尝试更换登录方式并重新尝试操作。此外，也可能是您的账号受到了安全限制，建议您尝试更改密码或联系 TikTok 客服解决。\n减少操作频率\n如果您经常需要执行一些重复性操作 (如点赞、评论等)，那么建议您减少操作频率并尽量避免连续操作。过度操作容易被 TikTok 检测到异常操作，从而导致频繁访问提示的出现。\n重置密码\n如果以上方法都不起作用，还有一个比较有效的方法，那就是点击「忘记密码」，然后重置密码，就可以登录成功了，我也不知道为什么🤷‍♂️。。。\n","date":"2023年12月17日","externalUrl":null,"permalink":"/posts/how-to-use-tiktok-in-china/","section":"博客","summary":"抖音，一个让全中国都痴迷的短视频分享平台，已经成为年轻一代表","title":"TikTok 免拔卡安装教程：支持安卓与 iOS","type":"posts"},{"content":"","date":"2023年11月24日","externalUrl":null,"permalink":"/tags/google/","section":"标签","summary":"","title":"Google","type":"tags"},{"content":" 这又是一个“屠龙少年 终成恶龙” 的故事，令人唏嘘，这篇文章详细描述了一位谷歌前员工从 2005 年加入公司到 2023 年离职的经历。文章以第一人称视角，从最早的 Google 黄金时代讲起，早期的 Google 将 “不作恶” 一直贯穿到底，是工程师的天堂。随着时间的推移，Google 的文化开始逐渐蒸发。决策考量从用户利益变成了公司利益，后来只考量决策者的个人利益。最后为了确保股价的增长开始裁员。\n原文链接： https://ln.hixie.ch/?start=1700627373\nHacker News 链接： https://news.ycombinator.com/item?id=38381573\n我们公司虽小，也当引以为戒，不忘初心，贯彻工程师文化，为全体开发者们 打造一款伟大的云操作系统\n我于 2005 年 10 月加入谷歌，经历了 18 年的风雨历程后，终于在上周提交了我的离职申请，结束了我的谷歌之旅。\n回首这段旅程，我深感幸运，因为我亲历了谷歌刚刚上市后的黄金时期。这是一段不同寻常的历程，与大多数公司相反，从基层工程师到最高管理层，谷歌的每一位员工都充满善意，坚持做正确的事情。那个经常被嘲笑的口号 “ 不作恶”，的确代表了公司当时的核心价值观。这在很大程度上是对那些将利润置于顾客和人类整体利益之上的公司的一种回应，例如微软~\n我多次看到谷歌因一些真心为社会谋利的行为而遭到非难。例如谷歌图书计划。关于 Chrome 和 Google 搜索引擎的许多批评，特别是那些所谓与广告业务存在利益冲突的，实际上都是十分失实的 (巧合和错误常常会让人产生恶意的误解)。我时常见到隐私保护倡导者以损害用户利益的方式来反对谷歌的一些建议。这些争论对整个世界产生了持续的影响，其中最讨厌的结果就是我们今天不得不面对的大量无意义的 Cookie 警告提示。每每谷歌团队真诚地推动一些有利于社会的想法时，公众舆论却以怀疑态度对待，这真的让我感到非常沮丧。\n谷歌的黄金时代 # 2011 年，谷歌的查理天井。图片经过处理，删除了里面的人物 早期的谷歌也是一个非常优秀的工作场所。高管每周都会坦诚回答问题，如果无法回答也会清楚说明原因 (例如出于法律原因或者某些话题过于敏感)。埃里克·施密特经常带领全公司聆听董事会的讨论。公司内部各种产品的成功和失败都会比较客观地陈述和检讨。成功会受到表扬，失败会遭到严格的批判性审视，目的是汲取教训而不是互相指责。公司有自己的愿景目标，任何偏差都会解释清楚。5年前我在网景公司的实习中见识了类似 Dilbert 漫画中那种管理方式，谷歌员工的一致高素质让我耳目一新。\n在谷歌的前九年，我的工作重心是 HTML 及其相关标准。我的任务是推动网络的整体发展和改善，因为对网络有利就是对谷歌有利 (我被明确告知不必考虑谷歌自身的利益)。实际上这项工作从我在 Opera 公司期间就开始了。谷歌对我这项工作给予了大力支持。我名义上隶属谷歌开源团队，但工作完全自主 (这要感谢 Chris DiBona)。我大部分工作都是在谷歌园区内的不同建筑中使用笔记本电脑完成的，有好几年我都没去过自己的办公室。\n谷歌文化的变迁 # 随着时间的推移，谷歌在文化优势方面也出现了一些变化。例如，尽管我很欣赏 Vic Gundotra 的热情以及他对 Google+ 的初期规划 (这个规划相当明确，哪怕不一定普遍受欢迎，至少也毫不含糊)，但当事情进展不如人意时，我对他解决问题的能力就没那么有信心了。他还开始在谷歌内部引入了信息屏障 (比如只允许 Google+ 团队进入某些建筑)，这与早期谷歌内部的完全透明政策大相径庭。另一个例子是安卓团队 (最初是一家被并购的公司)，他们从未真正适应 Google 的文化。安卓团队在工作与生活的平衡上存在问题，团队透明度不及 Google 的老部门，过分关注追逐竞争对手而非解决用户真正的需求。\n我在谷歌的最后9年时间都花在 Flutter 上。这期间最美好的一些回忆来自 Flutter 这个项目的早期。Flutter 是谷歌旧时代的最后几个项目之一，它是拉里·佩奇在 Alphabet 成立之前发起的几个雄心勃勃的试验性项目中的最后一个。我们像创业公司一样运营，更多地是在探索我们所创造的东西，而不仅仅是设计它。Flutter 团队深受年轻的谷歌文化熏陶，比如优先考虑内部透明度、工作生活平衡以及基于数据的决策 (这在很大程度上要感谢 Tao Dong 和他的用户体验研究团队)。从一开始我们就是极其开放的，这让我们顺利地在工作中建立了健康的开源社区。多年来，Flutter 也非常幸运地拥有优秀的领导，如创始技术总监 Adam Barth、项目经理 Tim Sneath 以及工程经理 Todd Volkert。\n最初几年我们也没有遵循工程最佳实践。比如没有编写测试代码，文档寥寥无几。这张白板就是我们为核心的 Widget、RenderObject 和 dart:ui 层所作的全部 “设计文档”。这让我们起初能够快速推进，但后来为此付出了代价。 Flutter 在一个相对隔绝的环境中成长，在很大程度上与谷歌同期发生的变革相隔离。随着时间的推移，Google 的文化开始逐渐蒸发。决策考量从用户利益变成了公司利益，后来只考量决策者的个人利益。信息透明度消失殆尽。过去我热衷参加公司各种会议来了解情况，但现在我甚至能提前预测高管的回答。如今，我不知道谷歌内部是否还有人能清楚解释谷歌的愿景，公司士气跌至历史最低点。如果你询问湾区的心理治疗师，他们会告诉你他们所有的谷歌客户都对公司不满。\n谷歌文化的现状 # 随后 Google 开展了裁员。这次裁员是一个短视行为导致的无谓错误，目的是确保股价能够季度对季度保持增长，而不是遵循谷歌过去重视长期成功即便短期有所损失的策略 (这正是 “不作恶” 原则的核心)。裁员的影响是隐蔽而深远的。过去员工可能会关注用户需求或者公司整体利益，坚信做正确的事最终会得到回报，即使超出自己的职责范围。但是裁员后，员工不再相信公司会坚定地支持他们，于是极度规避任何有风险的举动。职责的边界被严格划分，知识和信息被视为珍宝一般囤积起来，因为不可替代性变成了保住工作的唯一手段。我在谷歌亲眼见证了这一切。员工对管理层缺乏信任，因此管理层也不再信任员工，只凭借荒谬的公司政策进行治理。2004年，谷歌创始人 明确告知华尔街 “谷歌不是一家传统公司，我们无意成为一家传统公司”，但如今的谷歌已经名存实亡。\n谷歌当前的许多问题源于 Sundar Pichai 缺乏远见的领导魄力，他对维护 Google 早期的文化规范似乎也不怎么关心。这其中的一个症状就是无能的中层管理人员的泛滥。以 Jeanine Banks 为例，她管理的部门包含 Flutter、Dart、Go 和 Firebase 等在内的很多产品。她的部门名义上有相关战略，但我即便想泄密也无从下手，多年来我完全无法理解这些策略的含义。她对团队业务的理解很肤浅，经常提出毫无意义和不切实际的要求。她将工程师当作商品一般对待，违背个人意愿调动工作，完全不考虑个人技能。她根本不接受任何建设性反馈，好像这些反馈根本不存在一样。我听说其他团队的领导更懂政治运作，早已掌握如何 “应对” 她，在正确的时候给她恰到好处的信息，以避免被其骚扰。与谷歌最辉煌的时期相比，我发现如今的谷歌让人万分沮丧。\n未来展望 # 谷歌内部仍有许多杰出的人才。我有幸与 Flutter 团队的优秀成员如 JaYoung Lee、Kate Lovett、Kevin Chisholm、Zoey Fan、Dan Field 等数十人合作 (抱歉无法逐一提及，佳士遍地)。近年我开始在公司内部为谷歌员工提供职业建议，也因此结识了各个部门的许多优秀人才。谷歌现在回头还来得及，但这需要公司高层进行调整，将公司的决策权从 CEO 的办公室转移给一个有明确长期愿景的领袖，知晓如何利用谷歌的丰富资源真正为用户创造价值。我依然坚信谷歌的使命 (组织世界上的信息，使其变得普遍易于访问和有用) 蕴含巨大的潜力。有志于带领谷歌走进未来 20 年、为人类谋最大福祉而不考虑股价短期波动的领袖，可以凝聚谷歌的人才与热情，开创真正了不起的业绩。\n然而时间已经不多了，谷歌文化的恶化最终会变得不可逆转，因为那些能够引领和维持组织道德标准的人，恰恰不会选择加入一个本来就没有道德底线或价值观念的组织。\n","date":"2023年11月24日","externalUrl":null,"permalink":"/posts/reflecting-on-18-years-at-google/","section":"博客","summary":"这又是一个“屠龙少年 终成恶龙” 的故事，令人唏嘘，这篇文章详细","title":"回望我在谷歌的 18 年：屠龙少年终成恶龙","type":"posts"},{"content":"","date":"2023年11月10日","externalUrl":null,"permalink":"/tags/linux/","section":"标签","summary":"","title":"Linux","type":"tags"},{"content":" 译者序 # 原文链接： The History of Red Hat\n译者水平有限，不免存在遗漏或错误之处。如有疑问，敬请查阅原文。\n以下是译文。\nBob Young 的创业之路 # 成长背景 # Bob Young 于 1954 年出生于加拿大安大略的汉密尔顿。他与祖母住得很近，与他的父母和兄弟姐妹一同成长。放学后，他祖母家的阿姨都会给他和他三个兄弟准备好刚出炉的杯子蛋糕。虽然他小时候很喜欢运动，但自己坦言运动天赋一般。初中毕业后，他进入了位于加拿大安大略小镇 Port Hope 的著名私立学校 Trinity College School，后来在多伦多大学的维多利亚学院主修历史并获得文学学士学位。不过，Young 曾表示自己的学习成绩并不是很理想。\nYoung 曾感慨，手握历史学学位在职场上并没让他多少占到便宜，尤其是在他的成绩并非特别优秀的情况下。他觉得，或许自己搞个创业项目更有戏。他曾半开玩笑地称自己为 “找不到工作的高材生”，于是他就搞了个打字机租赁的小生意。他将营业点选在了多伦多郊外，隔壁恰好是个特色的养鱼虫农场。但随着计算机的普及，打字机市场逐渐被计算机侵蚀。不甘失败，Young 又快速转型，创立了一个名为 Vernon Computer Rentals 的计算机租赁公司。这个公司一直保持良好的势头，直到 1989 年的经济衰退。最终，他以约 2000 万美元的价格将公司卖给了 Greyvest Capital，并从中获得约 400 万美元的收入。但事情没那么简单，由于交易中的某些条款因素，Young 还需要继续为新公司投入资金。接下来的剧情有点儿戏剧化，没多久 Greyvest 的股价就像过山车一样跌到谷底，使得 Young 手中的股票变得一文不值。最终，他也被公司裁员了。\n创办 ACC Corporation # 1993 年 3 月，Young 处于 “失业+家庭” 双重双压下，有妻小、有房贷，口袋里几乎没什么钱了。虽说他自己也不是啥技术高手，但之前摸爬滚打的经历让他对这个行业颇有了解。他灵机一动，觉得开源软件和 Linux 似乎是个不错的商机。于是，他创办了 ACC Corporation (这个名字是为了在电话簿中排名靠前)。他开始通过打印产品目录的方式展示并销售 CD 上的软件，包括了 Slackware 和其他开源软件，并且还有 UNIX 及其他专有软件。那个时代下载 Linux 可不是吃饭喝水那么简单，网速慢得像蜗牛，想买个便宜点的 CD 刻录机？那你得再等等，1993 年的 CD 刻录机价格还是高得吓人。这为那些卖预装开源软件 CD 的商家提供了一定的市场空间。而 Young，就是在康涅狄格州的家中，用妻子的缝纫间作为指挥中心，开始了他的商业冒险。\n尽管 Young 将 Linux 刻录在 CD 上销售，但他对于自己所销售的产品尚未完全理解。在 Don Becker 的邀请下，他前往位于马里兰州格林贝尔特的 Goddard 太空飞行中心进行了访问。\n这是一张 NASA 的 Goddard 太空飞行中心照片，摄影者：NASA Goddard/Bill Hrybyk, 日期：2010-06-29 当时 Becker 大佬正在搞一个叫 Beowulf 的项目，这是历史上第一台运行 Linux 的超级计算机。这款计算机实际上就是把一堆市面上的普通电脑拼在一起组成的一个集群。这款 “原始版” 超级计算机，用了十六台 486DX4 计算机，然后用双通道以太网给它们都串联起来。美国国家航空航天局 (NASA) 有个需求：一个能够进行吉浮点运算的工作站，预算不能超过 $50000 (按 2023 年的价值是 $105000)，同时，他们需要能够随意修改的软件。Beowulf 完美地满足了这两个需求。在此背景下，Linux 的开源特性显得至关重要。尽管 Becker 和他的团队都认为 Solaris 是更为优秀的 UNIX 系统，但因为不能随心所欲地修改它，所以最终选择了 Linux。Young 在看到 Beowulf 后，深刻地意识到 Linux 的潜力，它并不仅仅是 UNIX 的一个分支而已。\nMarc Ewing 的 Linux 之旅 # 成长背景 # Marc Ewing 出生于 1969 年 3 月 9 日，他的父亲是 IBM 的一名程序员。年仅 10 岁的他已经显露出了敏锐的商业嗅觉：他将购买的 Bubble Yum 和 Bubblicious 口香糖 (每片四分之一美元) 放入他的萨克斯风盒子中，并在学校里以一美元的价格卖出。因此，在纽约波基普西的 Hagen 小学，大家都称他为 “口香糖小贩”。他还参加了计算机夏令营，学会了为 Apollo 和 Commodore 计算机编写程序。1992 年，他从卡内基梅隆大学毕业。在校期间，他常常戴著一顶红色的康乃尔大学长曲棍球帽子 (这是他的祖父赠送给他的)，并且经常出没在计算机实验室。由于他技术高超，又经常待在实验室，同学们总是来找他请教问题。他待人友好，乐于助人，久而久之，大家开始口口相传：“有技术难题？找那个戴红帽子的帅小伙吧！”\n由 DALL·E 3 配图 创办红帽公司 # 大学毕业后，Ewing 在 IBM 混了一段时间，他自认为这段经历没啥意思。后来，他离职了，但房租还得交啊，于是决定研究研究计算机技术，需要一个经济实惠的 UNIX 系统，那时 Linux 正崭露头角。因此，1993 年，他在北卡罗来纳州达勒姆的公寓里创办了红帽公司 (Red Hat Software)。在这个公寓里，与他同住的还有他的新婚妻子 Lisa。有趣的是，他的新婚妻子不仅是他的 “合伙人”，小学时代还从他手里买过口香糖。\nRed Hat Linux 的诞生 # 他发布的第一个 Linux 版本叫 Red Hat Software Linux，通常简称为 RHS Linux，这在相关手册和文档中都有注明。此版本主要基于 RPP 包管理器进行开发，并通过一张纯红色标签的 CD 提供给用户。每当产品发货时，都会夹带着一封感谢信，里面充满了对客户支持测试版的暖心感谢，还有两位大佬 Marc Ewing 和 Damien Neil (该公司的第一个员工，当时还是实习生) 的亲笔签名。此预览版于 1994 年 7 月 29 日发布，Linux 内核版本为 1.1.18，但并没有指定的版本号。\n第二个版本是首次大范围传播的版本，并被命名为 “万圣节版本”，发布日期是 1994 年 10 月 31 日，版本号为 0.9。虽然这还是一个测试版，但用户已经可以选择 1.0.9 或 1.1.54 作为他们的 Linux 内核版本，其中 1.1.54 是正在开发中的内核版本。当时官方文档建议用户使用一款名为 LIM (Linux 安装管理器) 的图形界面包管理前端工具，该工具是基于 TCL/TK 开发的，并用于 RPP。这个版本之所以受到欢迎，大概是因为它带有很多用户体验极佳的图形化系统管理工具，从管理用户、群组，到设置时间、日期、网络等等，都是小菜一碟！\n两位创始人的邂逅 # 在 Linux 圈子里，大家都在聊这个新发布的 Linux 发行版，这让 Young 也来了兴趣。当时 Young 管理着 “New York Unix and Linux Journal” 的几个邮件列表，并利用这些列表来宣传自己的产品目录。目录中有如 Yggdrasil、InfoMagic 和 Slackware 等品牌，售价介于 $20 到 $50 之间 (按 2023 年的汇率，大约是 $42 到 $105 之间)，并给他带来了约 50% 的利润。最近生意突然开始火爆起来，越来越多的客户开始讨论红帽 (Red Hat)。到了 1994 年秋，Young 从新闻组和客户中频繁听到关于 Red Hat 的讨论，于是决定与 Ewing 联系一下。他每月卖出约一千份 Linux，心里琢磨着或许有 10% 的人会对 Red Hat 感兴趣。因此，他希望向 Ewing 订购三百份，足够三个月的供应存货。但等到他 9 月打电话给 Ewing，提议把 Red Hat Software Linux 加到他的 “小金库” “ACC PC Unix and Linux Catalog” 时，Ewing 显然被这个数字吓了一跳。经过一段尴尬的沉默，Ewing 终于开口了，他原本只想制作三百份而已。\nEwing 急需财务和市场推广的支持，而 Young 则在寻找一款能代表自己进行销售的产品。经过一系列的协商，他们在 1995 年 1 月达成了合作协议。Young 获取了相关的版权、品牌和商标；作为回报，Ewing 获得了 ACC Corp 的股份，这家公司现在就是大名鼎鼎的红帽公司 Red Hat Software，Inc (在此之前，RHS 只是 Ewing 的个体经营企业)。Ewing 高兴地把销售的烦恼扔给了 Young，而 Young 倒是很开心接手这一职责。但别高兴太早，他们此时都迫切需要经济援助。为了维持公司的运营，他们选择了申请信用卡并刷爆其额度。部分信用额度用于偿还已有的债务，其余的则为公司注入资金。由于 Young 的信用额度不足，只好找他妻子 Nancy 出马，因为她的信用更好。\n要不是我妻子 Nancy 的信用很高，我可能早就撤退求生了，哪还有机会看到公司赚大钱呢？\n他们成立新公司后，仍选择在 Ewing 的公寓里办公，并且经常组队去山姆超市囤点汽水，生活也算是轻松有趣。但是，一个清晨，公寓的厕所不争气地溢水了，还影响到了楼下的房间。当物业维修人员走进公寓，只见满屋的电脑却不见人影，因此强烈要求他们搬离这里。好在，公司很快就在附近找到了一个小型办公室，重新启航。\n由 DALL·E 3 配图 RHS Linux 在新公司成立后的首发版本是在 1995 年 5 月，被命名为 “Mother’s Day” 版本。版本号为 1.0，并搭载了 1.2.8 版本的 Linux 内核。它的新名称是 “Red Hat Commercial Linux”。logo 也进行了创新设计，从那个经典的红色高顶帽，变成了一个手拎公文包，另一手高高举着红帽的潇洒男士。\nRHS 旧版 LOGO RHS Inc 1995 年的 LOGO 在那个炎炎夏日，RHS 出其不意地推出了一个名为 “母亲节加一” 的 bug 修复版本。根据购买时间，这个版本可能搭载了 1.2.11 或 1.2.13 的 Linux 内核。\nRed Hat Linux 的演进历程 # 逐步完善用户体验 # 那时，尽管红帽的规模还不算大，但 Red Hat 在 Linux 领域的影响力正在稳步上升。Slackware 仍旧是市场的领头羊，据 Young 的估计，它占据了近 90% 的市场份额。他还猜测 Yggdrasil 占了约 5%，而 SuSE (基本上是 Slackware 的一个变种) 也是一个不可忽视的玩家。Young 分析了背后的原因：虽然通过 CD 分发系统对网络速度慢的用户是个福音，但对于软件更新来说，这种方式存在明显的缺陷。当用户收到 CD 时，上面的内容可能已经不是最新的了。因此，Red Hat 的 Linux 需要进行改造，以支持 FTP 分发，而 Slackware 从一开始就已经实现了这一点。为了实现这一目标，Ewing 和 Erik Troan 用 Perl 编写了著名的包管理工具 RPM。\n1995 年夏末，Red Hat 推出了 2.0 beta 版本，这是第一个采用 Red Hat Package Manager (RPM) 的版本。有趣的是，他们这次放弃了 a.out 格式，转而拥抱了 ELF 二进制格式。当 1995 年初秋来临，2.0 正式版本发布，并换了个新名字叫做 “Red Hat LiNUX”。\n那段时间，Red Hat 如日中天，不仅市场份额逐渐攀升，其品牌知名度也随之上升。1995 年末，他们推出了 2.1 版本，并命名为 “Bluesky”。为此，DEC 制作了一个针对 x86 的宣传 CD，为即将在 1996 年 1 月发布的 “Red Hat Linux/Alpha 2.1” 造势。\n然而，到了年末，Young 背负了近 $50000 的信用卡债务 (按 2023 年的价值约为 $98000)，幸好，他们终于开始挣钱了。我相信 Nancy 看到 Red Hat 还清了那堆信用卡债务，心里一定乐开了花。\n1996 年 3 月 15 日，Red Hat 发布了 3.0.3 版本，并命名为 “Picasso”。这是其首次为多种硬件架构同时发布的版本，同时兼容了 DEC Alpha 和 Intel x86。Alpha 版本采用了 a.out 格式，而 x86 则采用了 ELF 格式；且 Alpha 版本是完全静态链接，不涉及共享库。这个版本还有一个亮点，那就是首次在 Red Hat Linux 中集成了来自 Metro Link Inc 的 Metro-X。那个时代，配置 Linux 的 X Windows 服务器简直就是个体力活，非常繁琐。Metro-X 大大简化了这一过程，提供了一个图形化的配置工具帮助用户轻松设置 X 环境。但这个版本在命名上存在些许混乱，有的叫官方 Red Hat LiNUX，有的叫 Red Hat™ Software Inc LiNUX，还有 RED HAT LINUX 和 Red Hat Linux。估计是因为当时市面上已经泛滥了各种低价和免费版本，红帽公司急需做点区分，标明自己的官方版本。\n预装了 FVWM 界面的 Red Hat Linux 3.0.3 1996 年 3 月至 8 月对于 Red Hat Linux 来说是转变与进化的关键时期。这段时间里，Red Hat Linux 像蜕变的蝴蝶一样，逐渐进化成了一个现代化的 Linux 发行版。为了 4.0 版本的推出，Red Hat 采用 C 语言重新设计了 RPM，并开始研发 Pluggable Authentication Modules (PAM)。此外，他们也用 Python/TK 工具替换了之前的 TCL/TK，首先从网络配置开始更新。当然，这还不算完，他们将 Linux 内核也升级到了 2.0 版本，引入了新的内核模块功能。这个期待已久的 4.0 测试版被赋予了一个时髦的名字——“Rembrandt”，并在 1996 年 8 月正式亮相。\nShadowmanTM 品牌形象问世 # 1996 年 10 月 3 日，Red Hat 发布了面向 Intel x86、DEC Alpha 和 Sun SPARC 的 4.0 版本，名为 “Colgate”。在这个版本中，Alpha 首次支持了 ELF 二进制格式和动态链接功能。这一版本的系统内核升级到了 2.0.18 版本，并搭载了基于 Spyglass 开发的 Red Baron 浏览器。这一次，Red Hat 不仅提供了传统的纸质说明书，还额外给用户提供了免费的电子文档。在品牌形象方面，这个版本也是 Shadowman™ 标志的首次亮相。这个版本受到广泛好评，甚至还被 Info World 评为 1996 年的最佳操作系统。\n译者注：Shadowman™ 是 Red Hat 的商标和品牌形象，表现为一个带有礼帽的剪影人像。这个标志从 1996 年开始出现在 Red Hat 的产品和宣传材料上，后来逐渐成为该公司的标志性形象。Shadowman 的设计旨在传达红帽公司的核心价值观和精神，它代表了开放、社区驱动和革新的理念。\nRed Hat Shadowman™ LOGO 转型为服务型公司 # 在创业初期，Red Hat 还没有一个清晰的商业模式。他们的做法很直接：把软件装进一个大包装盒中，这些包装盒既可以在实体店的货架上找到，也可以直接从 Red Hat 那里购买，还可以选择通过产品目录选购。在书店或电脑专卖店，这样的盒装软件通常的售价是 $29.95 (相当于 2023 年的 $58)。购买这种盒装软件的主要是一些不想亲自下载操作系统和相关软件，并进行手动安装的用户。有了这么一个神奇的盒子，只需要插入光盘，跟着指引走就行。另外，包装盒中还贴心地附上了使用说明，让用户可以更加轻松地上手和使用。\n在 Cincinnati 的 MicroCenter 商电脑零售店，我和我爸因为 Red Hat 闹了个小别扭。家里那台二手电脑上，我装的是 Slackware，虽说运行得还不错，但说实话，我对这系统知之甚少，更别提那像蜗牛一样慢的网速，下载这些软盘耗费了我大量时间。我跟爸说，买个 Red Hat 怎么样？他有些疑惑：“为何要花钱购买免费的东西？” 我说：“可以获得详细的使用文档和节省下载时间啊！”，并重点强调了文档对学习的价值。之后的日子里，每次逛店，我都会 “顺便” 提一下 Red Hat。最后，老爸终于同意了。这件事看似微不足道，但没想到，那本 Red Hat 手册竟然变成了我职业生涯的开端，只是当时我们都没有意识到它的重要性。\n与此同时，Red Hat 也为一小部分客户提供了付费电话支持服务。虽说在 1990 年代，这玩意儿只是他们众多业务中的一小部分，但 Red Hat 仍旧坚信这玩意在市场上还是挺有竞争力的。与此同时，他们也明白这种模式要想扩展可能会有点困难。再给大家科普下当时的背景，到 1996 年为止，NT 仍然是个新产品，还没有展现出其后来的市场影响力。那时 Red Hat 的主要竞争对手就是那些商业 UNIX 厂家，相较于这些 UNIX 厂家，Red Hat 的价格更具竞争力，但在技术支持、专业硬件和人力资源上却稍显逊色。虽然 Red Hat 已经开始盈利了，但它出售的产品实际上用户是可以免费获取的。因此，他们所能提供的附加服务与价值相对有限。虽然有自家的浏览器、使用手册和电话支持这些小福利，但要完全依赖这些，长期看来可能不太行。\n1997 年 2 月 3 日，Red Hat 发布了 4.1 版 “Vanderbilt”，该版本搭载了 2.0.27 版本的 Linux 内核。5 月，Red Hat 又推出了 4.2 版 “Biltmore”，这是最后一个内置 Red Baron 浏览器的版本。在同年后续的版本中，从 4.8 到 4.96，Red Hat 将其发行版本基于 glibc 2.0 进行了重大更新，并在更大程度上采用了公开的 beta 测试模式。\n1997 年 12 月 1 日，Red Hat 发布了新的版本 5，并命名为 “Hurricane”。这个名字是为了纪念一场飓风，该飓风曾经席卷 Red Hat 的家乡，对周边造成了很大的损害，但 Red Hat 的总部却安然无恙。此版本不仅集成了 Real Audio™ 客户端和服务器软件，而且还荣获了 Info World 1997 年的年度最佳产品奖。\n内置 Netscape Communicator 和 FVWM 的 Red Hat Linux 5，图片来源于 toastytech.com 1998 年 6 月 1 日，Red Hat Linux 5.1 版本 (代号 Manhattan) 正式发布。该版本进一步强化了对专有软件的支持，甚至专门为这些软件推出了一个独立的 CD。此外，GNU Network Object Model Environment (GNOME) 的预览版也被整合在安装媒介的一个特定文件夹中。“Manhattan” 版本也首次引入了 linuxconf 作为集中式的配置工具。至于荣誉，它不仅摘下了《PC Magazine》的技术创新奖，更是从澳大利亚个人电脑杂志那里斩获了编辑之选奖和那个酷到没朋友的 Just Plain Cool 奖。\n1998 年 9 月，Red Hat 取得了令人瞩目的业绩，年销售额达到 500 万美元，相当于 2023 年的 940 万美元。对于专注于 Linux 和开源软件的公司，这个数字已经相当给力了。这么亮眼的业绩，连 Intel 和 Netscape 都忍不住要投资了。紧接着，Benchmark Capital 和 Greylock Management 对其进行了风险投资，而 IBM、Novell、Oracle 和 SAP 则选择了小规模投资。与此同时，Red Hat 加强了其技术支持服务，使之成为了公司业务新的增长点。1998 年 11 月，红帽将总部搬到了位于北卡罗来纳州 三角研究园的 Meridian Business Complex 办公室。\n译者注：三角研究园 (Research Triangle Park，简称 RTP) 是美国北卡罗来纳州的一个著名高科技研究和开发园区，它是全美同类型研究园中规模最大的科研园。\n1999 年初，红帽公司的命运来到了转折点，突然成了行业的焦点。该公司与行业巨头 Dell 和 IBM 签订了战略合作协议，决定将 Red Hat 的 Linux 系统安装在他们的服务器和工作站上，作为对付昂贵的 UNIX 系统的开源解决方案。具体来说，IBM 将 Red Hat Linux 引入到其 Netfinity 服务器、PC 300 工作站、Intellistations 和 ThinkPads 中，而 Red Hat 则为这些产品的用户提供了强大的技术支持。对 Dell 而言，其 PowerEdge 服务器是最为畅销的产品，因此，Dell 不仅为 Red Hat 进行了股权投资，还承诺其 PowerEdge 服务器都会预装 Red Hat Linux。此外，两家公司还达成了全球服务和支持协议。Gateway 也紧跟潮流，开始按照客户需求预装 Red Hat。那一年，Red Hat 的收入飙升到了前所未有的 1000 万美元，按照 2023 年的价值，相当于 1840 万美元。\n在获得大量投资和实现增长的同时，Red Hat 的团队可没闲着。1998 年，他们精心打造了 5.2 版本 (代号 Apollo)，紧接着在 1999 年推出了 5.9 版本 (代号 Starbuck)。更厉害的是，他们在 1999 年打破常规，推出了 6.0 版本 (代号 Hedwig)。这是一个里程碑式的版本。从技术角度看，该版本采用了 glibc 2.1、EGCS、2.2 版 Linux 内核，并集成了 GNOME 桌面环境。EGCS 实际上是多个 GCC 的分支合并而成，为 GCC 带来了更多的扩展功能，如 g77 (fortran)、P5 Pentium 的优化、更出色的 C++ 支持，以及对更多体系结构和操作系统的支持。此外，新版内核在多个平台 (包括 Intel 的 Pentium 系列、Cyrix 和 AMD 芯片) 上表现优异，并解决了之前 Linux 启动时遇到的一些问题。此版本还进一步加强了硬件驱动支持，并提高了系统性能。但更让人惊喜的是，6.0 版本被 Dell 看上并预装在他们的电脑上，这为 Red Hat 带来了丰厚的收入。\n集成了 GNOME 桌面环境的 Red Hat 6.0，图片选自 Linux Journal 杂志 首次公开募股与收购狂潮 # Red Hat 正式更名为 Red Hat，Inc。并在 1999 年 8 月 11 日闪亮上市，发行价格设定为 $14 (折合到 2023 年差不多是 $26)。当时公司内部的员工心里都有点慌，Marc Ewing 如是说：\n考虑到当时的市场形势，的确有点手心冒汗，但我们坚信我们的故事独具一格、足够吸引眼球，因此我们勇敢地选择了上市。当然，这个决定是有风险的，我们都感到有些紧张。\n开盘第一天，股票的收盘价格是 $52 (折合到 2023 年差不多是 $95)，上涨了 227%，成为了 Wall Street 历史上单日涨幅第八名，使 Red Hat 的市值飙升至 35 亿美元 (折合到 2023 年差不多是 64 亿美元)。\n公司甚至还稍微修改了一下他们的 LOGO：\n自 1999 年开始使用的 Red Hat Shadowman™ LOGO 红帽在首次公开募股 (IPO) 后，如同开启了超级加速模式，飞速在全球多个角落如英国、德国、法国、意大利和日本插上了旗帜，新办公室一个接一个地开设起来。他们不只是单纯地扩张，还收购了多家公司：首先是 Cygnus Solutions，一家专门为嵌入式系统制作编译器和调试器的公司，其前总裁 Michael Tieman 还出任了红帽的首席技术官 (CTO)；此外，Marc Ewing 也拿起了指挥棒，带领 Red Hat Center for Open Source 这个红帽的非营利部队，准备在开源世界大放异彩。\n红帽就像一台永不停歇的机器，又相继收购了 Hell’s Kitchen Systems (专门为电商行业提供支付处理软件的公司)、Bluecurve (开发交易模拟软件的公司)、WireSpeed Communications (专门研发嵌入式无线软件的公司) 和 C2Net Software (一家网络安全软件公司)。这一连串的动作，不仅展示了红帽雄厚的技术实力，更突显了他们在市场拓展上的智慧和策略，看来红帽是要在科技圈掀起一番新风暴了！\n版本更新不停歇 # 1999年9月6日，Red Hat Linux 带着一丝神秘的面纱，发布了一个新版本 6.0.50 (代号 Lorax)。该版本的一项重大更新是其系统安装器 Anaconda。Anaconda 非常灵活，可以根据用户的偏好和计算机的硬件配置，选择图形界面或文本模式进行安装，而且，它还是用优雅的 Python 语言编写的。时光飞逝，转眼到了 1999 年 10 月 4 日，Red Hat 再次发布了一个更新版本，版本号为 6.1，代号则是 “Cartman”。\nBob Young 在 1999 年 11 月离开了红帽。他觉得自己更适合做一个引领公司走向成功道路的创业者，而不是坐在成功公司 CEO 的位置上。他的强项在于创立公司并指导他们走向正确的发展方向。看到红帽已经发展得如火如荼，他认为是时候让 Matthew Szulik 这样的人接手了。没过多久，Marc Ewing 也选择了退出，并卖掉了他的股份。Merrill Lynch 的顾问送给他一个铜制的公牛头，以此祝贺他跻身亿万富翁之列，而那时他只有 30 岁。随后，Ewing 选择将他的财富和时间投入到慈善事业中，并且还联合创办了 Aplinist，继续开启了他的新征程。\n2000年2月9日，Red Hat Linux 发布了 6.1.92 版本 (代号 Piglet)。不久之后，具有划时代意义的 Red Hat Linux 6.2 (代号 Zoot) 随之诞生。这不仅仅是一个版本的更新，还标志着红帽首次在公共 FTP 上提供了 ISO 镜像，为用户的下载和安装提供了极大的便利。\nRed Hat Linux 6.2 的包装盒及其内容 这个版本附带了一本内容翔实的手册，长达 300 余页 2000 年 7 月 31 日，Red Hat Linux 发布了 6.9.5 版本，代号 “Pinstripe”。\n2000 年 9 月，红帽开启了新的篇章，推出了红帽网络服务。这标志着红帽从一个盒装 Linux 发行版供应商，演变成了一个提供综合性服务的公司，特别是在 “软件即服务” (SaaS) 领域。红帽网络作为一种订阅服务，为用户提供了包括技术支持和系统更新在内的多种服务，并按月收费。Red Hat Linux 7.0 (代号 Guinness) 在 2000 年 9 月 25 日发布，这个版本已经支持红帽网络服务。到了 2001 年 1 月 31 日，红帽发布了一个重要的更新版本 7.0.90 (代号 Fisher)，带来了全新的 2.4 版本 Linux 内核，内核的更新为 Linux 系统带来了许多新的特性和优化，例如自旋锁、多线程 I/O 和网络、日志文件系统、多 CPU 支持和 USB 设备支持等。接着，在 2001 年 4 月 16 日，红帽发布了 7.1 版本 (代号 Seawolf)，这是首次集成了 Mozilla 套件的版本。\n2002年春，红帽迎着和煦的春风，将其总部迁至了北卡罗莱纳州立大学的 Centennial Campus，那里位于风景如画的西罗利。这个时候，红帽已经汇聚了 630 名充满激情和创意的员工，他们共同创造了 7900 万美元的年收入，而到了2023年，这个数字已经增长到了 1 亿 3400 万美元。\nRHEL 的诞生 # 2002年的5月6日，红帽发布了两个版本。其中，7.3 版本成为了携带 Netscape 的最后版本。而在同一天，红帽又发布了 Red Hat Linux Advanced Server 2.1，这个版本后来被更名为 Red Hat Enterprise Linux。它不仅继承了 7.2 版本的基础，还融入了 7.3 版本的诸多优化和改进。红帽特别重视这个版本，在商业市场上进行了全方位的推广和支持，也因此赢得了许多独立软件供应商的坚定支持。\n2002 年 9 月 30 日，Red Hat Linux 8.0 (代号 Psyche) 发布。这一版本不仅标志着红帽最后一次在零售盒中推出 Linux 发行版，也是第一次采用了 “Bluecurve” 这种全新的视觉感受和操作体验。就像是开启了一个新世界的大门，该版本还首次搭载了 GNOME 2、KDE 3.0.3、OpenOffice.org 1.0.1、GCC 3.2、Glibc 2.3 和内核版本 2.4.18-14。\n内置 Bluecurve 主题的 Red Hat Linux 8 2003 年 3 月 31 日，Red Hat Linux 悄悄发布了 9.0 版本 (代号 Shrike)，这是 Red Hat Linux 系列的最后一个主要版本。该版本没有 CD，只能通过网络下载，仿佛是 Red Hat Linux 系列的告别信。它不仅代表着一个时代的结束，也铺垫了红帽企业级 Linux 3 的未来。这个版本引入了一个技术叫做 Native POSIX Thread Library (NPTL) 的功能。这项技术原先存在于 2.5 版本的内核，经过精心设计，被回溯移植到了 2.4.20 版本的内核中。该版本之后只有一次小规模的补丁更新，然后 Red Hat Linux 便优雅地退出了历史舞台。\n从这个时间节点开始，红帽公司将推出两款各具特色的 Linux 系统。一个是 Fedora Core (现在简称为 Fedora)，该版本会频繁地更新并且采用最新的技术。另一个则是 Red Hat Enterprise Linux，这个版本开发节奏缓慢但支持时间更长，并成为公司提供优先技术支持和资源的产品，采用订阅模式进行销售。在这场技术与艺术的结合中，红帽也赢得了业界巨头如戴尔、IBM、惠普和甲骨文的全力支持。\n红帽的影响力不断扩大 # 2004年3月19日，一群对开源软件满怀热情的开发者推出了 CentOS 版本 3。这个版本成为了 Red Hat Enterprise Linux 的精准复刻，无论优点还是缺点，都一一照搬。虽然它不是第一个尝试这么做的发行版，但很快，它就成了同类发行版中最常见和最受欢迎的版本。\n红帽公司于2005年12月19日被纳入了 NASDAQ-100 指数，进一步证明了其行业地位。随后几年，该公司维持了高速的增长势头，不断刷新业绩纪录。2012年，红帽成了第一家年收入破十亿美元的开源公司。其后，公司的收入持续攀升，2015年达到二十亿美元，并在2018年突破三十亿美元。\n2014 年，红帽收购了 CentOS 项目，并为其精心设立了一个管理委员会。该项目的主要开发者也加入了红帽旗下的开放源码与标准 (Open Source and Standards) 团队。\n凭借其雄厚的财力，红帽创建或支持了多个开源软件项目，其中主要包括：KVM、GNOME、systemd、PulseAudio、Dogtail、MRG、Ceph、OpenShift、OpenStack、LibreOffice、Xorg、Disk Druid、rpm、SystemTap 和 NetworkManager。\n被 IBM 收购与业界争议 # 2018年10月28日，IBM 宣布以 340 亿美元的天价收购红帽，并将其纳入自己的混合云业务部门，经过漫长的反垄断调查，2019年5月3日美国司法部终于批准了这笔天价收购案。两个月后，也就是2019年7月9日，红帽正式加入 IBM 这个 IT 巨头的麾下。这场交易不仅对红帽和其客户产生了深远的影响，更标志着开源和 Linux 在企业界的胜利。Linux 在服务器市场上展现了无可匹敌的实力，这也是这家企业级计算巨头选择收购最大的 Linux 和开源公司的主要原因。\nIBM 一贯的作风是在企业产品上不允许有竞争对手存在。2020年12月8日，它宣判了 CentOS 的死刑，最后一个版本定格在 CentOS 8。取而代之，IBM 推出了 CentOS Stream，本质其实是 RHEL 的滚动更新预览版。2023年6月中旬，IBM 宣布 CentOS Stream 源代码是 RHEL 唯一的公开源代码，意图切断 RHEL 的开源分支产品如 AlmaLinux、Rocky Linux 和 Oracle Linux 的生路。IBM 明确表示，RHEL 客户不能再分发 RHEL 的源代码。这一举措在开源界和 Linux 界引发了热烈讨论。争论焦点在于：RHEL 中的大部分软件并非由 IBM 创造或所有，而且大部分都是在 GNU 通用公共许可证 (GPLv3) 下发布的。根据 GPLv3 第 2 条，不允许再授权。第 3 条和第 4 条还明确规定，任何人不能限制该软件的运行或修改，可以按原样重新分发软件。\n即便面临仿制品制造商的挑战，红帽还是成功地站在了开源领域的顶峰。在我看来，IBM 的一些行动是缺乏远见的。软件自由保护协会的 Bradley Kuhn 对此持以下观点：\n我们把这种商业模式称为 “如果你行使了 GPL 赋予你的权利，那么你的钱在这里将一文不值。” 这种 RHEL 商业模式是否符合 GPL，是个备受争议的话题，观点多种多样。但除了红帽，几乎没有人认为这种商业模型能真正体现 GPL 和 FOSS 的核心精神。\n在我看来，虽然 RHEL 这个产品依然存在，但红帽这家公司实际上已经名存实亡。RHEL 不过是 IBM 旗下的一个品牌。就像 IBM 在尝试对其个人电脑产品线进行独有改动后逐渐走向衰落一样，红帽在被 IBM 收购后也可能会逐渐失去市场份额。\n红帽是一家对软件行业做出了巨大贡献、推动了开源软件的发展、以及奠定现代世界软件基础的公司。所有曾参与 Red Hat Linux 相关工作的人都应该为自己的成就感到自豪。\n","date":"2023年11月10日","externalUrl":null,"permalink":"/posts/the-history-of-red-hat/","section":"博客","summary":"译者序 # 原文链接： The History of Red Hat 译者水平有限，不免存在遗漏或错误","title":"Red Hat 公司的起源与发展：十亿美金开源巨头的崛起","type":"posts"},{"content":"","date":"2023年11月10日","externalUrl":null,"permalink":"/tags/redhat/","section":"标签","summary":"","title":"RedHat","type":"tags"},{"content":"","date":"2023年10月14日","externalUrl":null,"permalink":"/tags/microsoft/","section":"标签","summary":"","title":"Microsoft","type":"tags"},{"content":"","date":"2023年10月14日","externalUrl":null,"permalink":"/tags/system/","section":"标签","summary":"","title":"System","type":"tags"},{"content":"","date":"2023年10月14日","externalUrl":null,"permalink":"/tags/windows/","section":"标签","summary":"","title":"Windows","type":"tags"},{"content":"原文链接： The History of Windows 95\n译者水平有限，不免存在遗漏或错误之处。如有疑问，敬请查阅原文。\n以下是译文。\n1992 年 2 月，Windows 3.1 的研发即将结束，而 Windows 团队正忙得不亦乐乎地计划他们的下一盘大棋。到了 3 月 5 日，他们终于悠哉悠哉地敲定了战略大计：横扫桌面、笔记本、移动设备以及时髦的触控笔设备。至于那些高大上的服务器和工作站？呵呵，那自然是留给了 NT 团队。此外，他们必须还要重点解决三个“小”问题：用户界面、硬件支持，以及网络功能。\n由 DALL·E 3 配图 Windows 95 的起源与背景 # 90 年代的微软真是个忙碌的“工作狂”，不停地折腾新项目。仅在那个无所不包的系统部门里就陆续发布了很多产品，Windows 3.1（原名 Janus）于 4 月 6 日发布，MS-DOS 6.0（原名 Astro）于 1993 年 3 月发布，而 Windows for Workgroups 3.1（原名 Winball）于 1992 年 10 月发布。至于 Jaguar，虽然也捣鼓了一阵，但可惜最后并没有独立发布（稍后将详细介绍）。\n接下来，微软又搞出了一堆帅气的项目名：Cougar、Panther、Rover、NT 和 Cairo。\nCougar 是为了搞出一个全新的 32 位 Windows 内核，也就是 Windows 3.x 的 386 模式内核的进化版本。 Panther 的任务是将 win32 API 引入这个新内核。 Rover 则是为 Cougar/Panther 设计的移动版本。 NT 代表了微软走向专业工作站和服务器领域的初步尝试，它于 1993 年 7 月亮相。 Cairo 是 NT 的升级版，它引入了 Cougar/Panther 的许多创新（反之亦然）。 这俩搭档，Cougar 和 Panther，合在一起就成了大名鼎鼎的 Chicago。为了使 Windows 更为稳定和高效，Chicago 的 Cougar 部分非常关键。除了全新的 32 位保护模式，它还能动态地加载或卸载设备驱动。还有，它能让所有 MS-DOS 程序在 Windows 下愉快玩耍，解决了 Windows 2 和 3 的“老毛病”。像是 Command and Conquer 游戏里那些大到令人头疼的地图，之前的版本可能会导致 Windows 崩溃，但这一版的 Windows 可以顺利恢复。\n这些动作对于 Chicago 和整个微软公司来说都是意义非凡的。要知道，在 1992 年的时候，MS-DOS 才是微软的摇钱树呢。说到这，你们可知道 Brad Silverberg？虽然他那时候刚刚加入微软，但这小哥的背景可不简单：他在 Apple 搞过 Lisa 项目，在 Borland 也混过。到了 1992 年初，他已经是 Chicago 项目的项目负责人，而且还是微软个个人系统部门的高级副总裁！在微软的一份内部文件中，Silverberg 写到：\n我想明确一点，ms-dos 是我们公司的核心产品，为微软贡献了大部分的利润（也即股票价格）。而现在，它正面临来自 DR-DOS 和 IBM 的激烈竞争（我更愿意说它正在“被攻击”）。我们必须全力以赴保护这个业务。短期内，这意味着我们需要继续实施积极的营销策略。同时，我们需要每年都推出新版本，让那些竞争对手一直追在我们屁股后面，而不是我们被动地追赶。因此，我们今年计划推出 MS-DOS 的新版本，这将包含很多新功能，与此同时，我们正全力开发 cougar。\n之前提到的这个新版本就是 MS-DOS 6。Silverberg 所提及的新功能包括磁盘整理、磁盘压缩、防病毒功能、全新的备份系统以及文件传输工具。MS-DOS 6 在 1993 年 3 月发布，并持续更新至 1994 年 6 月。\n我这么说是想呈现出那时的微软和整个计算机行业的情境。那时，IBM 兼容的计算机数量几乎是其他所有计算机数量的 80 倍，达到了近 8000 万台。上面几乎都运行着 MS-DOS 或与之相似的 DOS 系统，而 OS/2 和 Linux 这样的系统都是稀有物种。大多数软件都在 16 位的实模式下运行。大部分的硬件配置都靠一些小开关，设置得非常精确。而要加载驱动，你得懂得 autoexec 和 load-high 这些技术工具。Windows 3 取得了很大的成功，Windows 3.1 更是如日中天。尽管取得了这样的成功，而且由于这些成功导致了微软未来计划的变动，MS-DOS 仍然在 PC 操作系统市场上有着巨大的领先优势。虽然 Windows 3x 解决了一些问题，但旧系统仍然是主流。因此，尽管 Microsoft 已经有了更先进的 NT 系统，但他们绝对不能忽视 MS-DOS 的重要性。再加上大部分家用计算机其实并不适合运行 NT。因此，Chicago 必须在中端硬件上为 win16、win32 和 MS-DOS 应用提供最佳体验，并且其改进必须明显超过 Windows 3。如果微软做不到，他们可能会输给 Digital Research 或 IBM。\n由 DALL·E 3 配图 最终，为了保持向后兼容性，Chicago 系统中仍保留了一些 16 位的代码。没有这些代码的话，其向后兼容性就不会这么好了。回首往事，鉴于 IBM 的 OS/2 能够运行 DOS 和 Windows 软件，微软的这个决策可谓英明之至。\nChicago 的系统架构 # Chicago 的架构与 Windows for Workgroups 3.1（增强的 386 版本）相似，但更加先进和完善。其中包括许多在 32 位保护模式下运行的虚拟设备驱动（VxDs），同时也有运行在虚拟真实模式下的虚拟 DOS 机（VDMs）。这些虚拟驱动既用于实际的物理硬件，也模拟为虚拟机提供设备，同时也服务于其他软件。而其中三大核心组件 VxDs，即：虚拟机管理器（VMM32.VXD）、配置管理器（CONFIGMG）以及可安装文件系统管理器（IFM），基本上是 Chicago 的心脏部分。VMM32 主要负责内存管理、处理各种事件、中断处理、加载和初始化设备驱动、创建虚拟机以及任务调度等。CONFIGMG 则是负责即插即用功能，而 IFM 主要协调文件系统的访问，提供磁盘缓冲，并实现了一个 32 位的保护模式 I/O 访问系统，从而无需经过 MS-DOS，这一功能首次出现在 386 Windows 3 的版本中。\n对于 Chicago，Win32 API 分为三个独立的模块，每个模块都包含两个组件（一个是 16 位的，另一个是 32 位的）。内存、进程和文件系统的管理都是由它的内核部分 (KRNL386.EXE, KERNEL32.DLL, VWIN32.VXD) 负责的。用户界面及其各种功能由 \u0026ldquo;User\u0026rdquo; 部分 (USER.EXE, USER32.DLL) 负责。对于那些与设备无关的图形绘制，它由 Graphics Device Interface（也称为 GDI）(GDI.EXE, GDI32.DLL) 处理，这个功能我们在 Windows 1 版本中就见过了。\n与 Microsoft Windows 的其他版本大相径庭（除了 NT 3，因为它早在 Chicago 之前就已经首次亮相了），当启动 Windows 的时候，MS-DOS 不再常驻内存。所有依赖于 DOS 系统调用的 16 位应用都会被重定向到一个 32 位的 Chicago 例程中。而且，运行在 Chicago 中的 DOS 应用程序，不再需要 MS-DOS 的驱动。那些新一点、为 MS-DOS 兼容环境写的 32 位保护模式应用，会在 Chicago 中模拟运行这种保护模式。在早期的 Windows 版本中，你可以看到两个操作系统似乎是在同一台电脑上并行运行。但到了 Chicago，它就像一个拥有三种特性的操作系统：通过 VDMs 来实现的 DOS、win16 和 win32。的确，MS-DOS 在 Chicago 中推出了新版本，但微软并没打算单独销售。最初是有这样的打算，但在某个过程中，这个想法被放弃了。MS-DOS 7 只是作为 Chicago 的一部分，基本上就是个启动器。而 MS-DOS 7.1 随后与 Chicago 的更新一同推出。要是其他公司，我猜这事儿根本不可能。大家都知道，MS-DOS 曾经是微软的摇钱机器，但微软就这么狠心地把这块金矿扔了，把所有筹码都压在了 Chicago 这匹黑马上。\n在 硬核软件：个人电脑革命的兴衰 中，Steven Sinofsky 这样描述：\n微软针对消费者的核心 Windows 项目叫做 Chicago（最终成为 Windows 95）。Chicago 结合了 Windows 3.1 所享有的广泛兼容性和强大的生态系统支持，同时还加入了全新的 Win32 API。更为关键的是，它成功弥补了 Windows 与 Macintosh 在易用性方面的差距。Chicago 的最终目标，是打造一个既能吊打 Macintosh，又能把 Windows 的那一大堆优点都囊括进去的 PC。\nChicago 的设计目标 # 装了 Chicago 系统的 PC 是怎样打败 Macintosh 的？\nMicrosoft Chicago 开机动画 1993 年的 Chicago 系统桌面 1993 年 Chicago 系统桌面上展示的文件管理器 到 1992 年底，Windows 3x 的销量已破 5000 万，好得让微软几乎快要拍桌子庆祝了。此时微软对熟练用户使用 Windows 的疑难杂症已经了如指掌，但对于新手，他们了解得并不多。刚开始，他们设定的发布日期可谓是勇气十足（计划 18 个月，但没达标），这让负责设计新界面的团队感到压力山大，毕竟他们要打造的是超越 Macintosh 的 PC 系统。这帮人由大约 24 人组成，其中一半是设计师，一半是程序员。他们清楚，按部就班的传统开发方式会拖慢进度，所以选择了迭代开发。他们会提出一个想法，实施它，对其进行用户测试，接收反馈，然后再重复这个过程。团队成员 Kent Sullivan 分享了他们的目标：一是让计算机小白更轻松上手 Windows，二是让经常使用 Windows 3.1 的用户体验更流畅。首批的用户体验研究在 Microsoft 实验室完成，几位初、中级用户来试玩新系统，并给出了各种反馈。例如，他们会回答：“你觉得如何？”或者“十分钟后，你知道怎么操作 X 功能吗？” 最终，他们反复调整，终于才完美呈现开始菜单、任务栏和文件对话框。而打印和帮助功能也被翻来覆去地修改了好几遍。开发后期，微软推出了公测版，吸引更多用户参与反馈。这个 Chicago 公测版售价 49.95$（约等于 2023 年的 103$），内容压缩在 37 张软盘里。因此，Chicago 成为了微软有史以来最受用户热议的产品。\n1994 年底，微软终于给 Chicago 版本确定了发布名称：Windows 95。当这个版本趋于完善时，它的整体设计和用户体验也已基本确立。\nWindows 95 开机动画 Windows 95 开始菜单 Windows 95 文件管理器 微软为了推广这个系统，展开了其有史以来最大规模的宣传活动。他们花了 3 百万美元（相当于 2023 年的 620 万美元）来购买 Rolling Stones 的歌曲“Start Me Up”的版权，用这首歌为背景音乐做了一个与开始菜单相关的广告。而且，他们还邀请了 Jennifer Aniston 和 Mathew Perry 来主演 一个网络喜剧，并用 Windows 的标志色彩照亮了整个纽约帝国大厦，更是在加拿大国家电视塔（CN Tower）上挂起了长达 330 英尺的巨大横幅。此外，在各大杂志和电视节目中，他们的广告也是铺天盖地。\n帝国大厦闪耀着 Windows 95 的主题灯光 Windows 95 的市场影响与评价 # 1995 年 8 月 24 日这一天，微软推出了 Windows 95，当时的售价为 210$ （按照 2023 年的物价，大约为 433$）。《纽约时报》对此次发布盛况如此评价：“这是计算机产业历史上最引人注目、最疯狂、最昂贵的一次产品发布。” 早在此之前，Windows 3 就已经让西方社会步入了科技时代（几乎每家大型新闻机构都配备了科技报道记者，同时科技领域也涌现出大量的专业媒体），并在 Windows 95 发布前就已成功售出 1 亿份。而 Windows 95 则进一步加强了这个趋势。\n全球各地，人们争相排队，等待零点 Windows 95 的发布。仅仅四天，Windows 95 就卖出了 100 万份。令人震惊的是，第一年的 Windows 95 销量就达到了 4,000 万份。\n发布当天，有张照片捕捉到一名男士手持两份 Windows 95 软件，照片出自 Torsten Blackwood 新加坡的 Windows 95 零点发布现场，图片来源：Reuters 当时几乎所有人都在讨论 Windows 95，销售数据更是破天荒，那么它到底有多火呢？Microsoft Windows 95 开启了计算的新纪元，其方式可谓相当革命性。它支持“即插即用”，且销量史无前例，直接导致老古董 ISA 逐渐被边缘化，成为了历史。而原来复杂的跳线和 dip 开关也消失了，取而代之的是通过图形界面简单安装驱动的 PCI 成为了新标准。那些老旧的应用程序很快就不复存在，而 32 位的 win32 应用程序成为了大家的首选。随着加入了 Internet Explorer 更新（或是通过 Plus! 扩展包）的 Windows 95 版本的推出，微软把互联网带给了每一个人。同时，运行 Windows 95 的个人电脑确立了这个操作系统的统治地位，统一了整个家庭计算领域，让 Amiga、Atari ST、BeBoxes 乃至 Macintosh 这些品牌逐渐淡出了人们的视野 \u0026hellip; 好在微软看在老朋友的份上，帮了 Apple 一把，Apple 才幸存了下来。即使是那些所谓的高端工作站品牌，都没能挡住 Windows 95 的洪荒之力。Windows 95 的 32 位计算推动使得工作站和普通家用电脑之间的技术差距越来越小。。随后，像 SGI、Sun 和 DEC 这些大牌也逐渐被性能相对普通的 Windows 电脑超越。而操作系统的另一个有趣的转变是，原本的 PC 游戏现在都变成了 Windows 游戏，不过这又是另一个话题了。\n","date":"2023年10月14日","externalUrl":null,"permalink":"/posts/the-history-of-windows-95/","section":"博客","summary":"原文链接： The History of Windows 95 译者水平有限，不免存在遗漏或错误之处。如","title":"Windows 95 的诞生历史","type":"posts"},{"content":"I am a Cloud Native engineer working in Hangzhou, China, currently employed by the Sealos team. I am an enthusiast of cloud-native technology, focusing on the study of cloud-native technology and the promotion of open-source ideas.\nSealos Cloud Operating System Cloud Native FastGPT AI Laf Serverless ","date":"7 October 2023","externalUrl":null,"permalink":"/en/","section":"Cloud Native Labs","summary":"I am a Cloud Native engineer working in Hangzhou, China, currently employed by the Sealos team. I am an enthusiast of cloud-native technology, focusing on the study of cloud-native technology and the promotion of open-source ideas.","title":"Cloud Native Labs","type":"page"},{"content":" The Open Source Project I\u0026rsquo;m Involved In ","date":"7 October 2023","externalUrl":null,"permalink":"/en/examples/","section":"Open Source","summary":" The Open Source Project I\u0026rsquo;m Involved In ","title":"Open Source","type":"examples"},{"content":"","date":"7 October 2023","externalUrl":"https://sealos.io","permalink":"/en/examples/sealos/","section":"Open Source","summary":"","title":"Sealos","type":"examples"},{"content":"","date":"6 October 2023","externalUrl":"https://github.com/labring/FastGPT","permalink":"/en/examples/fastgpt/","section":"Open Source","summary":"","title":"FastGPT","type":"examples"},{"content":"","date":"5 October 2023","externalUrl":"https://laf.dev","permalink":"/en/examples/laf/","section":"Open Source","summary":"","title":"Laf","type":"examples"},{"content":"","date":"2023年9月8日","externalUrl":null,"permalink":"/tags/bitwarden/","section":"标签","summary":"","title":"Bitwarden","type":"tags"},{"content":"","date":"2023年9月8日","externalUrl":null,"permalink":"/tags/docker/","section":"标签","summary":"","title":"Docker","type":"tags"},{"content":"","date":"2023年9月8日","externalUrl":null,"permalink":"/tags/kubernetes/","section":"标签","summary":"","title":"Kubernetes","type":"tags"},{"content":"","date":"2023年9月8日","externalUrl":null,"permalink":"/tags/sealos/","section":"标签","summary":"","title":"Sealos","type":"tags"},{"content":"","date":"2023年9月8日","externalUrl":null,"permalink":"/tags/vaultwarden/","section":"标签","summary":"","title":"Vaultwarden","type":"tags"},{"content":"","date":"2023年9月8日","externalUrl":null,"permalink":"/categories/cloud-native/","section":"分类","summary":"","title":"云原生","type":"categories"},{"content":" 我与 LastPass 的曲折恋情 # 超过 8 年网龄的我，注册过很多网站帐号，每个网站的密码我都用不同的复杂密码。一开始我全靠脑力记忆这些密码，后来渐渐觉得记起来很困难，就记录在笔记本上。但是随着时间推移，我发现这种方法既不安全也不可靠。\n有一次出差在外，一个人待在酒店里想登录某考研网站复习英语，却想不起来密码是啥，笔记本也没带在身上，急得像热锅上的蚂蚁。\n后来绕了很多弯路才重置了密码，但整个过程让我无比痛苦，又特么耽误我学英语！\n果然记在笔记本上也不能解决所有问题，可靠度太低了，而且还存在安全隐患。是时候使用专业的密码管理软件了！\n说到密码管理器，大家是不是想起了 LastPass… 我一开始用的确实是 LastPass，但是 LastPass 的价格策略频繁调整，从一开始的免费，到后来逐渐收费，让我开始对其提高警惕。而且，尽管它的安全记录相对来说比较良好，但经历过数次的漏洞曝出，让我对于其中的数据安全产生了疑虑。最让我失望的是，随着其越来越多的商业化操作，一些原本免费的功能也被限制或转移到了付费版本。\nBitwarden：密码管理革命者 # 一次偶然的机会，Bitwarden 闯入了我的视线。作为新一代开源的跨平台密码管理器，Bitwarden 的透明度让我对数据安全有了更大的信心。它使用 AES-256 位加密和 PBKDF2 SHA-256 来保证所有信息的安全，并且拥有丰富的客户端支持，包括 Windows、Mac、Linux、iOS、Android 等多个平台。\n与 LastPass 相比，Bitwarden 具有以下优势：\n代码开源，经过全球开发者验证更安全可靠。 提供免费版本无限使用基础功能。 使用端到端加密，只有用户自己才拥有解密密钥。 支持无限存储密码条目。 允许用户导入和导出密码数据。 提供优秀的自动填充服务，并且可以利用系统的生物识别（指纹、人脸等）进行认证。 支持文件加密分享，方便地通过 bitwarden send 分享隐私文件、照片等。 除了密码之外，还可以存储文件/文本/银行卡/个人信息。 最吸引我的是，Bitwarden 还可以私有化部署，这样可以确保数据完全掌握在自己手中，不必担心官方跑路。不过 Bit­war­den 官方服务对服务器需要的资源有点多，内存必须大于 2G，小内存机器是根本跑不起来的，一般推荐使用第三方开发的 Vaultwarden。\nVaultwarden：短小精悍 # Vaultwarden 是 Bitwarden 的轻量级版本，原名 bitwarden_rs，后来为了与“大哥” Bitwarden 区分开来，遂改名为 Vaultwarden。\nLogo 完美结合了 Rust 和 Vaultwarden：\nVaultwarden 使用 Rust 编写，默认使用 SQLite 数据库（同时还支持 MySQL 和 PostgreSQL），实现了 Bit­war­den API 的所有功能，只需要 10M 内存便可运行，几乎可以跑在任何硬件之上。\nGitHub 地址： https://github.com/dani-garcia/vaultwarden\n不用想了，无脑使用 Vaultwarden 吧。\n虽然 Vaultwarden 提供了 Docker 镜像，可以无脑梭哈，但是你还得提供一个公网出口，这就需要用到 Nginx 之类的反向代理。同时你还得准备一个域名，以及相应的证书，并且要做好自动续签的工作。这对小白来说还是有点复杂了。\n不过有了 Sealos 一键部署模板，这个问题就比较简单了，动动鼠标就行了，30 秒即可解决战斗。\n一键部署 Vaultwarden # 首先点击以下按钮打开 Vaultwarden 的应用模板：\n啥都不用填，直接点击「部署应用」：\n部署完成后，点击确认跳转到应用详情页面，可以看到应用已经启动成功了。点击外网地址即可直接打开 Vaultwarden 的 Web 界面：\n创建你的密码管理账户：\n创建完成后开始登录：\n完结撒花！🎉🎉🎉\n客户端使用自定义服务器非常简单，以 macOS 客户端为例，登录时选择「自托管」：\n然后在弹出的界面中输入 Vaultwarden 的地址，并点击保存：\n然后输入邮箱和密码进行登录。\n修改配置 # Vaultwarden 可以通过环境变量来自定义各种配置，它的所有环境变量都在这个文件中：\nhttps://github.com/dani-garcia/vaultwarden/blob/main/.env.template 感兴趣的可以自己研究。\nSealos 添加环境变量非常简单，在应用详情页面直接点击「变更」：\n然后展开「高级配置」，点击「编辑环境变量」：\n然后就可以在其中添加环境变量了。\n例如，我想设置 Vaultwarden 管理后台密码，就可以加入以下环境变量：\nADMIN_TOKEN=\u0026#39;xxxxx\u0026#39; 添加完成之后，点击「确认」，再点击右上角的「变更」就可以了。\n在你的域名后面加上 /admin，登录 Vaultwarden 管理后台，登陆密码为刚刚设置的 ADMIN_TOKEN：\n在这里可以根据情况对 Vaultwarden 进行一些可选设置，所有的设置项都可以通过鼠标悬停查看相应的说明，不了解的选项建议保持默认。\n这里介绍几个我认为值得关注的设置项：\nGeneral Settings Domain URL：设置你的网站域名，记得带上 https，如 https://your.domain。 Allow new signups：是否允许用户注册，如果密码库仅仅用于自用，建议在自己注册后关闭此选项。 Admin page token：在这里更改 Vaultwarden 管理后台的密码。 Invitation organization name：设置你的网站名字，将出现在自动发送的电子邮件中。 SMTP Email Settings 设置 SMTP 服务，用来发送系统邮件（建议开启）。 根据你的 SMTP 服务提供方填写相关信息即可。 设置保存后，运行一次 Test SMTP 确保邮件可以正常发送。 Read-Only Config：这里可以查看所有只读选项。 Backup Database：这里提供了一个简易的数据库备份功能。 费用评估 # 现在我们来评估一下在 Sealos 上运行 Vaultwarden 大概需要多少钱。点击「变更」：\n模板默认使用的 CPU 是 0.2C，内存是 256M，不过 Vaultwarden 只需要 10M 就能跑起来，个人使用完全不需要这么多内存，咱们直接把 CPU 和内存调到最低：\n最后点击「变更」。\n这下舒服了，每天只需要花费两毛六分钱。再加上 Sealos 超给力的充值优惠，折算下来每天只需要花费一毛多一点。\n而且不需要操心什么反向代理，什么域名，什么证书，就是一把梭，优雅。\n参考资料 # 自搭建全平台私有密码库 bitwarden \u0026amp; Vaultwarden ","date":"2023年9月8日","externalUrl":null,"permalink":"/posts/vaultwarden/","section":"博客","summary":"我与 LastPass 的曲折恋情 # 超过 8 年网龄的我，注册过很多网站帐号，每个","title":"使用 Sealos 搭建个人密码管理器 Vaultwarden","type":"posts"},{"content":"","date":"2023年6月7日","externalUrl":null,"permalink":"/tags/gpt4free/","section":"标签","summary":"","title":"gpt4free","type":"tags"},{"content":" 该方案目前已失效！请直接使用👉 gptgod GPT-4 目前是世界上最强的多模态大模型，能力甩 GPT-3.5 好几条街。\n大家都希望早日用上 GPT-4，不过目前体验 GPT-4 的渠道非常有限，要么就是开通 ChatGPT 尊贵的 Plus 会员，即使你开了会员，也是有限制的，每 3 小时只能发送 25 条消息。。。\n要么就去 OpenAI 官网申请 GPT-4 的 API，但是目前申请到 API 的小伙伴非常少，你以为申请到 API 就可以用了吗？GPT-4 的 API 价格超级无敌贵，是 GPT-3.5 价格的 30 倍，你敢用吗？😄\n然而，但是，既然我写了这篇文章，肯定是要告诉那一个惊天大幂幂的！\n现在完全免费白嫖 GPT-4 的机会来了，不仅可以白嫖，还可以直接作为 API 来调用！\n不仅能够作为 API 调用，我还接入了公众号给大家白嫖，你说气人不气人？\n如果你嫌下面太长不看，可以直接到公众号里去白嫖 GPT-4 👇\n下面言归正传，开始手把手教大家如何免费白嫖 GPT-4。\ngpt4free-ts 介绍 # GPT4Free 大家应该都知道吧？它上线几周就在 GitHub 上揽收了接近 4w 的 Star。原因就在于其提供了对 GPT-4 及 GPT-3.5 免费且几乎无限制的访问。该项目通过对各种调用了 OpenAI API 网站的第三方 API 进行逆向工程，达到使任何人都可以免费访问该流行 AI 模型的目的。\n这就相当于什么？假设地主家有一个粮仓，你往他家的粮仓偷偷插了一根管子，不停地向外抽米，他全然不知，所以你也不用交钱，一切费用由地主承担。\n现在接入 GPT-4 的第三方网站就相当于那个地主，懂了吧？\n但是这个项目并没有封装 API，而且目前也不太能用了。\n作为开发者，我们想要的肯定是 API 啊！这就要提到今天的主角了： gpt4free-ts\n这个项目是用 TypeScript 写的，相当于 GPT4Free 的 TypeScript 版本，但是更方便部署，而且封装了 API，简直就是开发者的福音，就他了！\n这个项目向多个地主家的粮仓插了管子，其中最强大的地主就是 forefront.ai，这个地主家的粮仓里就包含了 GPT-4 这个香饽饽，而且还有 Claude，就嫖他了！\n除了 forefront 之外，它接的粮仓还挺多的。。\n大批量注册临时邮箱 # forefront 的 GPT-4 模型是有限制的，每个账号每 3 小时内只能发送 5 条消息。\n所以接下来需要用到一个非常神奇的服务叫 RapidAPI。你可以通过这个 API 来获取无穷无尽的临时邮箱，然后再用这些无穷无尽的临时邮箱去注册无穷无尽的 forefront 账号。\n这么一说，你是不是就悟了？哈哈哈\n首先你需要在这里注册一个账号并登录： https://rapidapi.com/calvinloveland335703-0p6BxLYIH8f/api/temp-mail44\n然后需要在 Pricing 页面开启订阅：\n一般情况下订阅免费套餐即可，一天可以调用 100 次。\n如果你有更高的需求，可以考虑订阅更高级的套餐（比如你的用户数量特别多）。\n订阅完了之后，你就能看到 API Key 了。这个 Key 我们后面会用到。\nSealos 云操作系统介绍 # 单机操作系统大家应该都知道吧？Windows、macOS、Linux 这些都属于单机操作系统，为什么叫单机操作系统呢？因为他的内存啊，CPU 啊，都在一台机器上，你不可能用其他机器的内存和 CPU。\n那么什么是云操作系统呢？就是把一群机器的 CPU 和内存看成一个整体，然后给用户提供一个交互界面，用户可以通过这个交互界面来操作所有的资源。\n懂 K8s 的玩家可能要说了：这个我懂，K8s 就可以！\n如果我们的目标愿景是一个云操作系统，K8s 充其量只能是这个云操作系统的内核，就像 Linux 内核一样。完整的云操作系统需要一个像 Windows 和 Ubuntu 操作系统那样的交互界面，也就是操作系统发行版。\n对于云操作系统来说，Sealos 就是那个发行版。\n链接： https://cloud.sealos.io\n有人可能会把云操作系统理解成“Web 界面”，但其实不是，Sealos 云操作系统完全是类似于 Windows 和 macOS 桌面的那种逻辑，并不是 Web 界面。我只需要点几下鼠标，一个应用就装好了，老夫并不知道什么容器什么 K8s。\n数据库也一样，小鼠标一点，一个分布式数据库就装好了。\n我知道，这时候云原生玩家要坐不住了，您别着急，看到桌面上的终端了没？\n终端只是这个云操作系统中的一个 App 而已。同理，容器管理界面仍然可以作为云操作系统的 App，我管你是 Kubernetes Dashboard、Rancher、KubeSphere 还是 Kuboard，都可以作为 App 装在这个云操作系统中。这时候对于云原生专家而言，仍然可以命令行咔咔秀操作，也可以通过各种管理界面来管理容器。\n云操作系统嘛，就是要什么人都能用才行，不管你是什么角色，都能在这个操作系统里找到你想要的 App 去完成你的使命。\n安装 gpt4free-ts # 接下来才是这篇文章的重头戏。\n我要教大家如何在 Sealos 中点几下鼠标就能安装一个 gpt4free-ts 集群。\n没错，就是 gpt4free-ts 集群。\n什么叫集群？就是说我要运行一群 gpt4free-ts 实例，然后前面加一个负载均衡作为对外的 API 入口。\n下面的步骤非常简单，楼下的老奶奶都会，是真的，当时我就在楼下看她操作。\n首先进入 Sealos 云操作系统的界面： https://cloud.sealos.io。\n然后打开桌面上的应用管理 App：\n点击「新建应用」：\n在启动参数中，按照以下方式进行设置：\n应用名称随便写，比如 gpt4free。 镜像名称是：xiangsx/gpt4free-ts:latest。实际上这个镜像是有问题的，为了避免 forefront 的追杀，作者故意用最新的镜像来迷惑敌方，所以一般情况下我们需要使用上一个版本的镜像。如果你还听不懂我在说什么，请跳转到文章末尾加入微信群进行进一步深切友好的交流！ CPU 和内存需要根据应用的实际情况来填写。这个应用运行之后默认会启动两个 Chrome 浏览器来模拟登录 forefront，每次对话会从里面取一个账号来使用，次数用完了会自动注册新账号（因为每个账号每 3 小时只能发送 5 条信息）。我们可以通过环境变量来修改启动的浏览器数量，所以需要根据你的浏览器数量来确定 CPU 和内存。 我自己把浏览器数量设置为 3，所以需要的内存和 CPU 比较多（后面会告诉你怎么设置环境变量）。 实例数根据自己的实际需求填写，我需要接入公众号，粉丝比较多，一个实例才 3 个账号（因为我一个实例跑了 3 个浏览器），根本不够用，所以我开了 3 个实例。 容器暴露端口指定为 3000。 打开外网访问。 继续往下，展开高级设置，点击「编辑环境变量」：\n填入以下环境变量：\nrapid_api_key=\u0026lt;rapid_api_key\u0026gt; DEBUG=0 POOL_SIZE=3 ⚠️注意：请将 \u0026lt;rapid_api_key\u0026gt; 替换为你自己的 key。\n其中 POOL_SIZE 就是浏览器数量，每个浏览器会登录一个 forefront 账号。你可以根据自己的需要调整浏览器数量，并根据浏览器数量调整 CPU 和内存。如果你不知道怎么调整合适，建议无脑跟着本文操作。\n继续，点击「新增存储卷」：\n容量只需 1G，挂载路径设置为 /usr/src/app/run：\n这个存储的作用是为了保存已登录的账号。已经注册的账号 3 个小时以后还可以重新使用，不用再浪费邮箱去注册新账号。\n最终点击右上角的「部署应用」，即可完成部署：\n最终要等待所有的实例都处于 Running 状态，才算是启动成功了。\n点击右边的复制按钮，便可复制 API 的外网地址：\n我们来测一下这个 API：\n完美！打完收工！\n白嫖 Sealos # Sealos 默认会给新注册的用户赠送 5 个大洋，如果你想白嫖更多的额度，可以参加这个活动薅羊毛： Sealos Grant，开源社区激励。\n活动规则很简单，直接看图👇\nSealos 贵宾交流群 # 如果您是 Sealos 的用户，欢迎扫码加入 Sealos 的用户交流群👇\n","date":"2023年6月7日","externalUrl":null,"permalink":"/posts/completely-free-to-use-gpt4/","section":"博客","summary":"该方案目前已失效！请直接使用👉 gptgod GPT-4 目前是世界上最强的多模态大","title":"使用 gpt4free-ts 完全免费白嫖 GPT-4","type":"posts"},{"content":"","date":"2023年3月12日","externalUrl":null,"permalink":"/tags/faas/","section":"标签","summary":"","title":"FaaS","type":"tags"},{"content":"","date":"2023年3月12日","externalUrl":null,"permalink":"/tags/laf/","section":"标签","summary":"","title":"Laf","type":"tags"},{"content":"","date":"2023年3月12日","externalUrl":null,"permalink":"/tags/serverless/","section":"标签","summary":"","title":"Serverless","type":"tags"},{"content":" 原文地址： https://3min.cloud/chatgpt\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 OpenAI 已经公布了 ChatGPT 正式版 API，背后的新模型是 gpt-3.5-turbo，这是 OpenAI 目前最先进的模型，响应速度更快，价格更便宜。\n作为开发人员，我们还是希望通过 API 将 ChatGPT 和相关模型集成到自己的产品和应用中，尴尬的是，目前无法访问 ChatGPT API，原因大家都懂得。于是网上出现了各种各样的 API 反代服务，我们可以直接通过反代服务来变相访问 ChatGPT API。\n即使我们解决了 API 的访问问题，还要准备一个开发环境，比如对于 Node.js 客户端来说，需要准备一个 Node.js 环境。\n有没有一种简单快捷的方法来调用 ChatGPT API 呢？\n那当然是用 Laf 了。\nLaf 是一个完全开源的一站式云开发平台，提供了开箱即用的云函数，云数据库，对象存储等能力，让你可以像写博客一样写代码。\nGitHub： https://github.com/labring/laf\n如果你希望快速了解 Laf 的用法，可以参考这篇文章： 三分钟学会 Laf。\n言归正传，下面我们开始计时，三分钟时间用 Laf 实现一个自己的 ChatGPT！\n前提条件：你需要准备一个 ChatGPT 账号并且生成一个 API Key (这一步可以问 Google )\n云函数教学 # 首先需要登录 laf.dev，然后新建一个应用。\n点击开发按钮进入开发页面。\n在 NPM 依赖面板中点击右上角的 +：\n然后输入 chatgpt 并回车进行搜索，选择第一个搜索结果，保存并重启：\n重启之后，自定义依赖项中便出现了 chatgpt。\n然后就可以像我一样新建一个云函数名字叫 send，并写入以下内容：\nimport cloud from \u0026#39;@lafjs/cloud\u0026#39; export async function main(ctx: FunctionContext) { const { ChatGPTAPI } = await import(\u0026#39;chatgpt\u0026#39;) const api = new ChatGPTAPI({ apiKey: cloud.env.CHAT_GPT_API_KEY }) let res = await api.sendMessage(\u0026#39;“鸡你太美”指的是中国大陆哪位男艺人？给你个提示，他喜欢唱、跳、篮球、Rap\u0026#39;) console.log(res.text) return res.text } API Key 是通过环境变量 CHAT_GPT_API_KEY 传入的，所以我们还需要创建一个环境变量。点击左下角的设置图标：\n依次选择「环境变量」\u0026ndash;\u0026gt; 「新增环境变量」，输入环境变量的名称和值，然后点击「确定」，再点击「更新」，便会重启应用。\n现在点击右上角的「运行」，即可调试运行。\nPerfect！现在我们来试试添加追踪上下文的功能。其实也很简单，只需要在对话时传入上一次对话的 ID 即可，代码如下：\nimport cloud from \u0026#39;@lafjs/cloud\u0026#39; export async function main(ctx: FunctionContext) { const { ChatGPTAPI } = await import(\u0026#39;chatgpt\u0026#39;) const api = new ChatGPTAPI({ apiKey: cloud.env.CHAT_GPT_API_KEY }) let res = await api.sendMessage(\u0026#39;“鸡你太美”指的是中国大陆哪位男艺人？给你个提示，他喜欢唱、跳、篮球、Rap\u0026#39;) console.log(res.text) // 传入 parentMessageId 追踪上下文 res = await api.sendMessage(\u0026#39;不对，他姓蔡，请重新回答\u0026#39;, { parentMessageId: res.id }) console.log(res.text) return res.text } 运行一下看看：\n好厉害，竟然两次就答对了我的问题！\n好了，现在才开始真的计时，因为刚刚是教学环节，不计入耗时😁\n云函数 # 接下来我们就可以开始动手打造自己的 ChatGPT 了，首先把上一节的函数替换为下面的内容：\nimport cloud from \u0026#39;@lafjs/cloud\u0026#39; export async function main(ctx: FunctionContext) { const { ChatGPTAPI } = await import(\u0026#39;chatgpt\u0026#39;) const data = ctx.body // 这里需要把 api 对象放入 cloud.shared 不然无法追踪上下文 let api = cloud.shared.get(\u0026#39;api\u0026#39;) if (!api) { api = new ChatGPTAPI({ apiKey: cloud.env.CHAT_GPT_API_KEY }) cloud.shared.set(\u0026#39;api\u0026#39;, api) } let res // 这里前端如果传过来 parentMessageId 则代表需要追踪上下文 if (!data.parentMessageId) { res = await api.sendMessage(data.message) } else { res = await api.sendMessage(data.message, { parentMessageId: data.parentMessageId }) } return res } 现在应该很好理解这个函数了吧？\n前端 # 我们要实现的是 Web 版 ChatGPT，所以还需要一个前端页面。首先需要安装 Laf 的 SDK：\n$ npm install laf-client-sdk 接下来，需要创建一个 cloud 对象：\nimport { Cloud } from \u0026#34;laf-client-sdk\u0026#34;; // 创建 cloud 对象 这里需要将 \u0026lt;appid\u0026gt; 替换成自己的 App ID const cloud = new Cloud({ baseUrl: \u0026#34;https://\u0026lt;appid\u0026gt;.laf.dev\u0026#34;, getAccessToken: () =\u0026gt; \u0026#34;\u0026#34;, // 这里不需要授权，先填空 }); 这里我们看一下前端的核心代码，非常的简单，就是把提问的内容和上下文 id 传入云函数就可以了。\nasync function send() { // 我们提问的内容 const message = question.value; let res; // 与云函数逻辑一样，有上下文 id 就传入 if (!parentMessageId.value) { res = await cloud.invoke(\u0026#34;send\u0026#34;, { message }); } else { res = await cloud.invoke(\u0026#34;send\u0026#34;, { message, parentMessageId: parentMessageId.value }); } // 回复我们的内容在 res.text // 这个是上下文 id parentMessageId.value = res.id; } 到这一步 我们已经可以发信息给 ChatGPT 并且拿到回复的消息了。\n我们只要稍微加亿点点细节，就可以变成这样：\n加完这点细节之后，基本开发工作就完成了，接下来就是把项目上线分享给你的朋友，顺便装个杯。\n说到上线我们现在应该要去买一台服务器安装 Nginx，配置 Nginx，解析域名，绑定域名\u0026hellip;\nNO NO NO 我不允许你浪费年轻而美好的生命，life is short, you need laf 😃\n上线 # 打开你的 Laf，点击存储界面 \u0026ndash;\u0026gt; 点击上方加号 \u0026ndash;\u0026gt; 创建一个权限为 readonly 的存储桶（名字随意）。\n创建完之后，在你的前端项目中运行打包命令。我这里用的是 npm run build。\n打包完毕之后找到打包好的 dist 文件夹，像我一样把 dist 文件里面的所有东西都上传到我们刚刚创建的存储桶里面，记住是原封不动的上传哦，文件就是文件，文件夹就是文件夹。\n上传完毕之后，发现右上角有一个 “开启网站托管”，点一下它！\n点完之后出来一个链接，我们点击一下访问看看是啥东西。\n哦！我的老天鹅呀 这不就是我刚刚开发的项目吗？？\n恭喜，到这里你的项目已经上线了，快分享给你的好朋友吧！\n项目源码： https://github.com/zuoFeng59556/chatGPT ","date":"2023年3月12日","externalUrl":null,"permalink":"/posts/build-chatgpt-web-using-laf/","section":"博客","summary":"原文地址： https://3min.cloud/chatgpt 本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点","title":"用 Laf 云开发搭建一个 ChatGPT Web 演示网页","type":"posts"},{"content":"","date":"2022年11月27日","externalUrl":null,"permalink":"/series/tailscale-%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8B/","section":"Series","summary":"","title":"Tailscale 系列教程","type":"series"},{"content":"前面几篇文章给大家给介绍了 Tailscale 和 Headscale，包括 Headscale 的安装部署和各个平台客户端的接入，以及如何打通各个节点所在的局域网。同时还介绍了 如何自建私有的 DERP 服务器，并让 Tailscale 使用我们自建的 DERP 服务器。\n今天我们来探索一下更复杂的场景。想象有这么一个场景，我系统通过 Tailscale 方便的连接一台不完全属于我的设备， 这台设备可能还有其他人也在使用。如果我仅仅是安装一个 Tailscale， 那么所有能登录这台设备的人都可以通过 Tailscale 连接我所有的设备。\n我能不能实现这样一种需求：我可以连接这台节点，但是这台节点不能连接我的其他节点？\n这就是 Tailscale ACL（Access Control List）干的事情。ACL 可以严格限制特定用户或设备在 Tailscale 网络上访问的内容。\n虽然 Headscale 兼容 Tailscale 的 ACL，但还是有些许差异的。本文所讲的 ACL 只适用于 Headscale，如果你使用的是官方的控制服务器，有些地方可能跟预期不符，请自行参考 Tailscale 的官方文档。 Tailscale/Headscale 的默认访问规则是 default deny，也就是黑名单模式，只有在访问规则明确允许的情况下设备之间才能通信。所以 Tailscale/Headscale 默认会使用 allowall 访问策略进行初始化，该策略允许加入到 Tailscale 网络的所有设备之间可以相互访问。\nTailscale/Headscale 通过使用 group 这种概念，可以只用非常少的规则就能表达大部分安全策略。除了 group 之外，还可以为设备打 tag 来进一步扩展访问策略。结合 group 和 tag 就可以构建出强大的基于角色的访问控制（RBAC）策略。\n关于 Tailscale 访问控制系统的详情可以参考这篇文章： 基于角色的访问控制（RBAC）：演进历史、设计理念及简洁实现。这篇文章深入探讨了访问控制系统的历史，从设计层面分析了 **DAC -\u0026gt; MAC -\u0026gt; RBAC -\u0026gt; ABAC**的演进历程及各模型的优缺点、适用场景等， 然后从实际需求出发，一步步地设计出一个实用、简洁、真正符合 RBAC 理念的访问控制系统。\nTailscale ACL 语法 # Tailscale ACL 需要保存为 HuJSON 格式，也就是 human JSON。HuJSON 是 JSON 的超集，允许添加注释以及结尾处添加逗号。这种格式更易于维护，对人类和机器都很友好。\nHeadscale 除了支持 HuJSON 之外，还支持使用 YAML 来编写 ACL。本文如不作特殊说明，默认都使用 YAML 格式。 Headscale 的 ACL 策略主要包含以下几个部分：\nacls：ACL 策略定义。 groups：用户的集合。Tailscale 官方控制器的“用户”指的是登录名，必须是邮箱格式。而 Headscale 的用户就是 namesapce。 hosts：定义 IP 地址或者 CIDR 的别名。 tagOwners：指定哪些用户有权限给设备打 tag。 autoApprovers：允许哪些用户不需要控制端确认就可以宣告 Subnet 路由和 Exit Node。 ACL 规则 # acls 部分是 ACL 规则主体，每个规则都是一个 HuJSON 对象，它授予从一组访问来源到一组访问目标的访问权限。\n所有的 ACL 规则最终表示的都是允许从特定源 IP 地址到特定目标 IP 地址和端口的流量。虽然可以直接使用 IP 地址来编写 ACL 规则，但为了可读性以及方便维护，建议使用用户、Group 以及 tag 来编写规则，Tailscale 最终会将其转换为具体的 IP 地址和端口。\n每一个 ACL 访问规则长这个样子：\n- action: accept src: - xxx - xxx - ... dst: - xxx - xxx - ... proto: protocol # 可选参数 Tailscale/Headscale 的默认访问规则是 default deny，也就是黑名单模式，只有在访问规则明确允许的情况下设备之间才能通信。所以 ACL 规则中的 action 值一般都写 accept，毕竟默认是 deny 嘛。\nsrc 字段表示访问来源列表，该字段可以填的值都在这个表格里：\n类型 示例 含义 Any * 无限制（即所有来源） 用户(Namespace) dev1 Headscale namespace 中的所有设备 Group (ref) group:example Group 中的所有用户 Tailscale IP 100.101.102.103 拥有给定 Tailscale IP 的设备 Subnet CIDR (ref) 192.168.1.0/24 CIDR 中的任意 IP Hosts (ref) my-host hosts 字段中定义的任意 IP Tags (ref) tag:production 分配指定 tag 的所有设备 Tailnet members autogroup:members Tailscale 网络中的任意成员（设备） proto 字段是可选的，指定允许访问的协议。如歌不指定，默认可以访问所有 TCP 和 UDP 流量。\nproto 可以指定为 IANA IP 协议编号 1-255（例如 16）或以下命名别名之一（例如 sctp）：\n协议 proto IANA 协议编号 Internet Group Management (IGMP) igmp 2 IPv4 encapsulation ipv4, ip-in-ip 4 Transmission Control (TCP) tcp 6 Exterior Gateway Protocol (EGP) egp 8 Any private interior gateway igp 9 User Datagram (UDP) udp 17 Generic Routing Encapsulation (GRE) gre 47 Encap Security Payload (ESP) esp 50 Authentication Header (AH) ah 51 Stream Control Transmission Protocol (SCTP) sctp 132 只有 TCP、UDP 和 SCTP 流量支持指定端口，其他协议的端口必须指定为 *。\ndst 字段表示访问目标列表，列表中的每个元素都用 hosts:ports 来表示。hosts 的取值范围如下：\n类型 示例 含义 Any * 无限制（即所有访问目标） 用户（Namespace） dev1 Headscale namespace 中的所有设备 Group (ref) group:example Group 中的所有用户 Tailscale IP 100.101.102.103 拥有给定 Tailscale IP 的设备 Hosts (ref) my-host hosts 字段中定义的任意 IP Subnet CIDR (ref) 192.168.1.0/24 CIDR 中的任意 IP Tags (ref) tag:production 分配指定 tag 的所有设备 Internet access (ref) autogroup:internet 通过 Exit Node 访问互联网 Own devices autogroup:self 允许 src 中定义的来源访问自己（不包含分配了 tag 的设备） Tailnet devices autogroup:members Tailscale 网络中的任意成员（设备） ports 的取值范围：\n类型 示例 Any * Single 22 Multiple 80,443 Range 1000-2000 Groups # groups 定义了一组用户的集合，YAML 格式示例配置如下：\ngroups: group:admin: - \u0026#34;admin1\u0026#34; group:dev: - \u0026#34;dev1\u0026#34; - \u0026#34;dev2\u0026#34; huJSON 格式：\n\u0026#34;groups\u0026#34;: { \u0026#34;group:admin\u0026#34;: [\u0026#34;admin1\u0026#34;], \u0026#34;group:dev\u0026#34;: [\u0026#34;dev1\u0026#34;, \u0026#34;dev2\u0026#34;], }, 每个 Group 必须以 group: 开头，Group 之间也不能相互嵌套。\nAutogroups # autogroup 是一个特殊的 group，它自动包含具有相同属性的用户或者访问目标，可以在 ACL 规则中调用 autogroup。\nAutogroup 允许在 ACL 的哪个字段调用 含义 autogroup:internet dst 用来允许任何用户通过任意 Exit Node 访问你的 Tailscale 网络 autogroup:members src 或者 dst 用来允许 Tailscale 网络中的任意成员（设备）访问别人或者被访问 autogroup:self dst 用来允许 src 中定义的来源访问自己 示例配置：\nacls: # 允许所有员工访问自己的设备 - action: accept src: - \u0026#34;autogroup:members\u0026#34; dst: - \u0026#34;autogroup:self:*\u0026#34; # 允许所有员工访问打了标签 tag:corp 的设备 - action: accept src: - \u0026#34;autogroup:members\u0026#34; dst: - \u0026#34;tag:corp:*\u0026#34; Hosts # Hosts 用来定义 IP 地址或者 CIDR 的别名，使 ACL 可读性更强。示例配置：\nhosts: example-host-1: \u0026#34;100.100.100.100\u0026#34; example-network-1: \u0026#34;100.100.101.100/24 Tag Owners # tagOwners 定义了哪些用户有权限给设备分配指定的 tag。示例配置：\ntagOwners: tag:webserver: - group:engineering tag:secure-server: - group:security-admins - dev1 tag:corp: - autogroup:members 这里表示的是允许 Group group:engineering 给设备添加 tag tag:webserver；允许 Group group:security-admins 和用户（也就是 namespace）dev1 给设备添加 tag tag:secure-server；允许 Tailscale 网络中的任意成员（设备）给设备添加 tag tag:corp。\n每个 tag 名称必须以 tag: 开头，每个 tag 的所有者可以是用户、Group 或者 autogroup:members。\nAuto Approvers # autoApprovers 定义了无需 Headscale 控制端批准即可执行某些操作的用户列表，包括宣告特定的子网路由或者 Exit Node。\n当然了，即使可以通过 autoApprovers 自动批准，Headscale 控制端仍然可以禁用路由或者 Exit Node，但不推荐这种做法，因为控制端只能临时修改，autoApprovers 中定义的用户列表仍然可以继续宣告路由或 Exit Node，所以正确的做法应该是修改 autoApprovers 中的用户列表来控制宣告的路由或者 Exit Node。\nautoApprovers 示例配置：\nautoApprovers: exitNode: - \u0026#34;default\u0026#34; - \u0026#34;tag:bar\u0026#34; routes: \u0026#34;10.0.0.0/24\u0026#34;: - \u0026#34;group:engineering\u0026#34; - \u0026#34;dev1\u0026#34; - \u0026#34;tag:foo\u0026#34; 这里表示允许 default namespace 中的设备（以及打上标签 tag:bar 的设备）将自己宣告为 Exit Node；允许 Group group:engineering 中的设备（以及 dev1 namespace 中的设备和打上标签 tag:foo 的设备）宣告子网 10.0.0.0/24 的路由。\nHeadscale 配置 ACL 的方法 # 要想在 Headscale 中配置 ACL，只需使用 HuJSON 或者 YAML 编写相应的 ACL 规则（HuJSON 格式的文件名后缀为 hujson），然后在 Headscale 的配置文件中引用 ACL 规则文件即可。\n# Path to a file containg ACL policies. # ACLs can be defined as YAML or HUJSON. # https://tailscale.com/kb/1018/acls/ acl_policy_path: \u0026#34;./acl.yaml\u0026#34; ACL 规则示例 # 允许所有流量 # 默认的 ACL 规则允许所有访问流量，规则内容如下：\n# acl.yaml acls: - action: accept src: - \u0026#34;*\u0026#34; dst: - \u0026#34;*:*\u0026#34; 允许特定 ns 访问所有流量 # 假设 Headscale 有两个 namesapce：default 和 guest。管理员的设备都在 default namespace 中，访客的设备都在 guest namespace 中。\n$ headscale ns ls ID | Name | Created 1 | default | 2022-08-20 06:15:17 2 | guest | 2022-11-27 09:20:25 $ headscale -n default node ls ID | Hostname | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired 2 | OpenWrt | openwrt-njprohi0 | [7LdVc] | default | 10.1.0.2, | false | 2022-08-26 04:18:43 | offline | no 5 | tailscale | tailscale-home | [pwlFE] | default | 10.1.0.5, | false | 2022-11-27 10:02:35 | online | no 10 | k3s-worker05 | share | [5Z38M] | default | 10.1.0.9, | false | 2022-11-22 18:49:25 | offline | no 11 | Galaxy a52s | galaxy-a52s-arg5owsh | [U+0qY] | default | 10.1.0.1, | false | 2022-11-27 10:02:34 | online | no $ headscale -n guest node ls ID | Hostname | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired 12 | guest-1 | guest-1 | [75qSK] | guest | 10.1.0.10, | false | 2022-11-27 10:05:33 | online | no 13 | guest-2 | guest-2 | [8lONp] | guest | 10.1.0.11, | false | 2022-11-27 10:05:31 | online | no 现在我想让 default namespace 中的设备可以访问所有设备，而 guest namespace 中的设备只能访问 guest namespace 中的设备，那么规则应该这么写：\n# acl.yaml acls: - action: accept src: - \u0026#34;default\u0026#34; dst: - \u0026#34;*:*\u0026#34; - \u0026#34;guest:*\u0026#34; - action: accept src: - \u0026#34;guest\u0026#34; dst: - \u0026#34;guest:*\u0026#34; 在 guest-1 上查看 Tailscale 状态：\n$ tailscale status 10.1.0.10 ks-node-2 guest linux - desktop-aoulurh-j7dfnsul.default.example.com default windows offline galaxy-a52s-arg5owsh.default.example.com default android active; relay \u0026#34;hs\u0026#34;, tx 12112 rx 11988 guest-3 guest linux active; direct 172.31.73.176:41641, tx 2552 rx 2440 openwrt-njprohi0.default.example.com default linux offline tailscale-home.default.example.com default linux active; direct 60.184.243.56:41641, tx 3416 rx 25576 看起来 guest-1 可以看到所有的设备，但事实上它只能 ping 通 guest-2，我们来验证一下：\n$ ping 10.1.0.1 PING 10.1.0.1 (10.1.0.1) 56(84) bytes of data. ^C --- 10.1.0.1 ping statistics --- 9 packets transmitted, 0 received, 100% packet loss, time 8169ms 果然是 ping 不通的。但是 10.1.0.1 这个设备是可以反向 ping 通 guest-1 的：\n# 在 10.1.0.1 所在的设备操作 $ ping 10.1.0.10 PING 10.1.0.10 (10.1.0.10) 56(84) bytes of data. 64 bytes from 10.1.0.10: icmp_seq=1 ttl=64 time=68.9 ms 64 bytes from 10.1.0.10: icmp_seq=2 ttl=64 time=91.5 ms 64 bytes from 10.1.0.10: icmp_seq=3 ttl=64 time=85.3 ms 64 bytes from 10.1.0.10: icmp_seq=4 ttl=64 time=79.7 ms ^C --- 10.1.0.10 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3005ms rtt min/avg/max/mdev = 68.967/81.389/91.551/8.306 ms ssh 测试一下：\n$ ssh root@10.1.0.10 root@10.1.0.10\u0026#39;s password: 完美。\n下面再来看看 guest-1 能不能 ping 通 guest-2：\n# 在 guest-1 设备上操作 $ ping 10.1.0.11 PING 10.1.0.11 (10.1.0.11) 56(84) bytes of data. 64 bytes from 10.1.0.11: icmp_seq=1 ttl=64 time=2.93 ms 64 bytes from 10.1.0.11: icmp_seq=2 ttl=64 time=1.33 ms ^C --- 10.1.0.11 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 1.325/2.128/2.931/0.803 ms 和我在上面预测的效果一样，ACL 规则生效了。\n神奇的 tag # tag 有一个非常神奇的功效：它可以让 src 和 dst 中的元素失效。具体什么意思呢？假设你的 src 或 dst 中指定了 namespace 或者 group，那么这个规则只对这个 namespace 或者 group 中（没有分配 tag 的设备）生效。\n举个例子你就明白了，现在我给 guest-2 打上一个 tag：\n$ headscale node tag -i 13 -t tag:test Machine updated $ headscale -n guest node ls -t ID | Hostname | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired | ForcedTags | InvalidTags | ValidTags 12 | ks-node-2 | ks-node-2 | [75qSK] | guest | 10.1.0.10, | false | 2022-11-27 10:18:35 | online | no | | | 13 | ks-node-3 | ks-node-3 | [8lONp] | guest | 10.1.0.11, | false | 2022-11-27 10:18:31 | online | no | tag:test | | 此时 guest-1 就 ping 不通 guest-2 了：\n# 在 guest-1 设备上操作 $ ping 10.1.0.11 PING 10.1.0.11 (10.1.0.11) 56(84) bytes of data. ^C --- 10.1.0.11 ping statistics --- 4 packets transmitted, 0 received, 100% packet loss, time 3070ms 这就说明 guest-2 并不包含在 guest:* 这个访问目标中，也就是说打了 tag 的设备并不包含在 guest:* 这个访问目标中。\n此时其他设备如果还想继续 guest-2，必须在 dst 中指定 tag:test：\nacls: - action: accept src: - \u0026#34;default\u0026#34; dst: - \u0026#34;*:*\u0026#34; - \u0026#34;guest:*\u0026#34; - \u0026#34;tag:test:*\u0026#34; - action: accept src: - \u0026#34;guest\u0026#34; dst: - \u0026#34;guest:*\u0026#34; - \u0026#34;tag:test:*\u0026#34; 再次测试访问：\n# 在 guest-1 设备上操作 $ ping 10.1.0.11 PING 10.1.0.11 (10.1.0.11) 56(84) bytes of data. 64 bytes from 10.1.0.11: icmp_seq=1 ttl=64 time=1.31 ms 64 bytes from 10.1.0.11: icmp_seq=2 ttl=64 time=3.40 ms ^C --- 10.1.0.11 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1002ms rtt min/avg/max/mdev = 1.314/2.355/3.397/1.041 ms 果然可以 ping 通了。\n总结 # Tailscale/Headscale 的 ACL 非常强大，你可以基于 ACL 实现各种各样的访问控制策略，本文只是给出了几个关键示例，帮助大家理解其用法，更多功能大家可以自行探索（比如 group 等）。下篇文章将会给大家介绍如何配置 Headscale 的 Exit Node，以及各个设备如何使用 Exit Node，届时会用到 ACL 里面的 autoApprovers，敬请期待！\n","date":"2022年11月27日","externalUrl":null,"permalink":"/posts/tailscale-acls/","section":"博客","summary":"前面几篇文章给大家给介绍了 Tailscale 和 Headscale，包括 Headscale 的安","title":"Tailscale/Headscale ACL 使用教程","type":"posts"},{"content":"","date":"2022年10月7日","externalUrl":null,"permalink":"/tags/phantun/","section":"标签","summary":"","title":"Phantun","type":"tags"},{"content":" WireGuard 作为一个更先进、更现代的 VPN 协议，比起传统的 IPSec、OpenVPN 等实现，效率更高，配置更简单，并且已经合并入 Linux 内核，使用起来更加方便，简直就是 VPN 中的战斗机。越来越多的高人利用 WireGuard 实现很多奇奇怪怪的需求。例如国内与国外机器通过 WireGuard 打通隧道，变成伪 IPLC 专线；或者打通本地与 Kubernetes 集群的网络。\n但是 WireGuard 在国内网络环境下会遇到一个致命的问题：UDP 封锁/限速。虽然通过 WireGuard 可以在隧道内传输任何基于 IP 的协议（TCP、UDP、ICMP、SCTP、IPIP、GRE 等），但 WireGuard 隧道本身是通过 UDP 协议进行通信的，而国内运营商根本没有能力和精力根据 TCP 和 UDP 的不同去深度定制不同的 QoS 策略，几乎全部采取一刀切的手段：对 UDP 进行限速甚至封锁。\n鲁迅先生说过：羊毛出在羊身上！突破口还是在运营商身上：虽然对 UDP 不友好，但却无力深度检测 TCP 连接的真实性。\n这就好办了，既然你对 TCP 连接睁一只眼闭一只眼，那我将 UDP 连接伪装成 TCP 连接不就蒙混过关了。目前支持将 UDP 流量伪装成 TCP 流量的主流工具是 udp2raw，相信很多小伙伴对这个工具都轻车熟路了，但是很遗憾，今天的主角不是它，而是另一款比它更强大的新工具： Phantun。\nPhantun 介绍 # Phantun 整个项目完全使用 Rust 实现，性能吊打 udp2raw。它的初衷和 udp2raw 类似，都是为了实现一种简单的用户态 TCP 状态机来对 UDP 流量做伪装。主要的目的是希望能让 UDP 流量看起来像是 TCP，又不希望受到 TCP retransmission 或者 congestion control 的影响。\n需要申明的是，Phantun 的目标不是为了替代 udp2raw，从一开始 Phantun 就希望设计足够的简单高效，所以 udp2raw 支持的 ICMP 隧道，加密，防止重放等等功能 Phantun 都选择不实现。\nPhantun 假设 UDP 协议本身已经解决了这些问题，所以整个转发过程就是简单的明文换头加上一些必要的 TCP 状态控制信息。对于我日常使用的 WireGuard 来说，Phantun 这种设计是足够安全的，因为 WireGuard 的协议已经更好的实现了这些安全功能。\nPhantun 使用 TUN 接口来收发 3 层数据包，udp2raw 使用 Raw Socket + BFP 过滤器。个人感觉基于 TUN 的实现要稍微的优雅一点，而且跨平台移植也要更容易。\nPhantun 的 TCP 连接是按需创建的，只启动 Client 不会主动去连接服务器，需要第一个数据包到达了后才会按需创建。每个 UDP 流都有自己独立的 TCP 连接。这一点跟 udp2raw 很不一样，udp2raw 所有的 UDP 连接共用一个 TCP 连接。这样做的坏处就是 udp2raw 需要额外的头部信息来区分连接，更加增加了头部的开销。跟纯 UDP 比较，Phantun 每个数据包的额外头部开销是 12 byte，udp2raw 根据我的测试达到了 44 bytes 。\n下面是 Phantun 与 udp2raw 的详细对比：\nPhantun udp2raw UDP over FakeTCP 混淆 ✅ ✅ UDP over ICMP 混淆 ❌ ✅ UDP over UDP 混淆 ❌ ✅ 多线程 ✅ ❌ 吞吐量 Better Good 三层转发模式 TUN interface Raw sockets + BPF 隧道 MTU 开销 12 bytes 44 bytes 每个 UDP 流都有自己独立的 TCP 连接 Client/Server Server only 防止重放，加密 ❌ ✅ IPv6 ✅ ✅ Phantun 工作原理 # Phantun 分为服务端和客户端，服务端会监听一个端口，比如 4567（通过 --local 参数指定），并将 UDP 数据包转发到 UDP 服务（这里指的就是服务端 WireGuard 的监听端口和地址，通过 --remote 参数指定）。\n客户端也会监听一个端口，比如 127.0.0.1:4567（通过 --local 参数指定），并且通过 --remote 参数与服务端（比如 10.0.0.1:4567）建立连接。\n客户端与服务端都会创建一个 TUN 网卡，客户端 TUN 网卡默认分配的 IPv4/IPv6 地址分别是 192.168.200.2 和 fcc8::2，服务端 TUN 网卡默认分配的 IPv4/IPv6 地址分别是 192.168.201.2 和 fcc9::2。\n客户端与服务端都需要开启 IP forwarding，并且需要创建相应的 NAT 规则。客户端在流量离开物理网卡之前，需要对 IP 192.168.200.2 进行 SNAT；服务端在流量进入网卡之前，需要将 IP DNAT 为 192.168.201.2。\nPhantun 配置步骤 # 接下来我会通过一个示例来演示如何使用 Phantun 将 WireGuard 的 UDP 流量伪装成 TCP。我们需要在服务端和客户端分别安装 phantun，可以到 release 页面下载，推荐下载静态编译版本 phantun_x86_64-unknown-linux-musl.zip。\n服务端 # 假设服务端的公网 IP 地址是 121.36.134.95，WireGuard 监听端口是 51822。首先修改配置文件 /etc/wireguard/wg0.conf，在 [Interface] 中添加以下配置：\nMTU = 1300 PreUp = iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2 PreUp = RUST_LOG=info phantun_server --local 4567 --remote 127.0.0.1:51822 \u0026amp;\u0026gt; /var/log/phantun_server.log \u0026amp; PostDown = iptables -t nat -D PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2 PostDown = killall phantun_server || true 你需要将 eth0 替换为你服务端的物理网卡名。MTU 值先不管，后面再告诉大家调试方法。\nPreUp = iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2 这条 iptables 规则表示将 4567 端口的入站流量 DNAT 为 TUN 网卡的 IP 地址。\nPreUp = RUST_LOG=info phantun_server --local 4567 --remote 127.0.0.1:51822 \u0026amp;\u0026gt; /var/log/phantun_server.log \u0026amp; 这里会启动 phantun_server，监听在 4567 端口，并将 UDP 数据包转发到 WireGuard。\n服务端完整的 WireGuard 配置：\n# local settings for Endpoint B [Interface] PrivateKey = QH1BJzIZcGo89ZTykxls4i2DKgvByUkHIBy3BES2gX8= Address = 10.0.0.2/32 ListenPort = 51822 MTU = 1300 PreUp = iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2 PreUp = RUST_LOG=info phantun_server --local 4567 --remote 127.0.0.1:51822 \u0026amp;\u0026gt; /var/log/phantun_server.log \u0026amp; PostDown = iptables -t nat -D PREROUTING -p tcp -i eth0 --dport 4567 -j DNAT --to-destination 192.168.201.2 PostDown = killall phantun_server || true # remote settings for Endpoint A [Peer] PublicKey = wXtD/VrRo92JHc66q4Ypmnd4JpMk7b1Sb0AcT+pJfwY= AllowedIPs = 10.0.0.1/32 最后重启 WireGuard 即可：\n$ systemctl restart wg-quick@wg0 客户端 # 假设客户端的 WireGuard 监听端口是 51821。首先修改配置文件 /etc/wireguard/wg0.conf，在 [Interface] 中添加以下配置：\nMTU = 1300 PreUp = iptables -t nat -A POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE PreUp = RUST_LOG=info phantun_client --local 127.0.0.1:4567 --remote 121.36.134.95:4567 \u0026amp;\u0026gt; /var/log/phantun_client.log \u0026amp; PostDown = iptables -t nat -D POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE PostDown = killall phantun_client || true 你需要将 eth0 替换为你服务端的物理网卡名。\nPreUp = iptables -t nat -A POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE 这条 iptables 规则表示对来自 192.168.200.2（TUN 网卡） 的出站流量进行 MASQUERADE。\nPreUp = RUST_LOG=info phantun_client --local 127.0.0.1:4567 --remote 121.36.134.95:4567 \u0026amp;\u0026gt; /var/log/phantun_client.log \u0026amp; 这里会启动 phantun_client，监听在 4567 端口，并与服务端建立连接，将伪装的 TCP 数据包传送给服务端。\n除此之外还需要修改 WireGuard peer 的 Endpoint，将其修改为 127.0.0.1:4567。\nEndpoint = 127.0.0.1:4567 客户端完整的 WireGuard 配置：\n# local settings for Endpoint A [Interface] PrivateKey = 0Pyz3cIg2gRt+KxZ0Vm1PvSIU+0FGufPIzv92jTyGWk= Address = 10.0.0.1/32 ListenPort = 51821 MTU = 1300 PreUp = iptables -t nat -A POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE PreUp = RUST_LOG=info phantun_client --local 127.0.0.1:4567 --remote 121.36.134.95:4567 \u0026amp;\u0026gt; /var/log/phantun_client.log \u0026amp; PostDown = iptables -t nat -D POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE PostDown = killall phantun_client || true # remote settings for Endpoint B [Peer] PublicKey = m40NDb5Cqtb78b1DVwY1+kxbG2yEcRhxlrLm/DlPpz8= Endpoint = 127.0.0.1:4567 AllowedIPs = 10.0.0.2/32 PersistentKeepalive = 25 最后重启 WireGuard 即可：\n$ systemctl restart wg-quick@wg0 查看 phantun_client 的日志：\n$ tail -f /var/log/phantun_client.log INFO client \u0026gt; Remote address is: 121.36.134.95:4567 INFO client \u0026gt; 1 cores available INFO client \u0026gt; Created TUN device tun0 INFO client \u0026gt; New UDP client from 127.0.0.1:51821 INFO fake_tcp \u0026gt; Sent SYN to server INFO fake_tcp \u0026gt; Connection to 121.36.134.95:4567 established 查看 wg0 接口：\n$ wg show wg0 interface: wg0 public key: wXtD/VrRo92JHc66q4Ypmnd4JpMk7b1Sb0AcT+pJfwY= private key: (hidden) listening port: 51821 peer: m40NDb5Cqtb78b1DVwY1+kxbG2yEcRhxlrLm/DlPpz8= endpoint: 127.0.0.1:4567 allowed ips: 10.0.0.2/32 latest handshake: 1 minute, 57 seconds ago transfer: 184 B received, 648 B sent persistent keepalive: every 25 seconds 测试连通性：\n$ ping 10.0.0.2 -c 3 PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data. 64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=13.7 ms 64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=14.4 ms 64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=15.0 ms --- 10.0.0.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2005ms rtt min/avg/max/mdev = 13.718/14.373/15.047/0.542 ms 客户端（多服务端） # 如果客户端想和多个服务端建立连接，则新增的服务端配置如下：\nPreUp = RUST_LOG=info phantun_client --local 127.0.0.1:4568 --remote xxxx:4567 --tun-local=192.168.202.1 --tun-peer=192.168.202.2 \u0026amp;\u0026gt; /var/log/phantun_client.log \u0026amp; PostDown = iptables -t nat -D POSTROUTING -o eth0 -s 192.168.202.2 -j MASQUERADE 本地监听端口需要选择一个与之前不同的端口，同理，TUN 网卡的地址也需要修改。最终的配置如下：\n# local settings for Endpoint A [Interface] PrivateKey = 0Pyz3cIg2gRt+KxZ0Vm1PvSIU+0FGufPIzv92jTyGWk= Address = 10.0.0.1/32 ListenPort = 51821 MTU = 1300 PreUp = iptables -t nat -A POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE PreUp = RUST_LOG=info phantun_client --local 127.0.0.1:4567 --remote 121.36.134.95:4567 \u0026amp;\u0026gt; /var/log/phantun_client.log \u0026amp; PreUp = RUST_LOG=info phantun_client --local 127.0.0.1:4568 --remote xxxx:4567 --tun-local=192.168.202.1 --tun-peer=192.168.202.2 \u0026amp;\u0026gt; /var/log/phantun_client.log \u0026amp; PostDown = iptables -t nat -D POSTROUTING -o eth0 -s 192.168.200.2 -j MASQUERADE PostDown = iptables -t nat -D POSTROUTING -o eth0 -s 192.168.202.2 -j MASQUERADE PostDown = killall phantun_client || true # remote settings for Endpoint B [Peer] PublicKey = m40NDb5Cqtb78b1DVwY1+kxbG2yEcRhxlrLm/DlPpz8= Endpoint = 127.0.0.1:4567 AllowedIPs = 10.0.0.2/32 PersistentKeepalive = 25 MTU 调优 # 如果你使用 ping 或者 dig 等工具（小数据包）测试 WireGuard 隧道能够正常工作，但浏览器或者远程桌面（大数据包）却无法正常访问，很有可能是 MTU 的问题，你需要将 MTU 的值调小一点。\nPhantun 官方建议将 MTU 的值设为 1428（假设物理网卡的 MTU 是 1500），但经我测试是有问题的。建议直接将 MTU 设置为最低值 1280，然后渐渐增加，直到无法正常工作为止，此时你的 MTU 就是最佳值。\n你学废了吗？\n参考资料 # Phantun - Rust 写的轻量级 UDP -\u0026gt; TCP 混淆器 Phantun GitHub Repo ","date":"2022年10月7日","externalUrl":null,"permalink":"/posts/wireguard-over-tcp-using-phantun/","section":"博客","summary":"WireGuard 作为一个更先进、更现代的 VPN 协议，比起传统的 IPSec、Op","title":"WireGuard 基础教程：使用 Phantun 将 WireGuard 的 UDP 流量伪装成 TCP","type":"posts"},{"content":" 很久以前，我们只需要在 Linux 终端中输入 route -n（后来演变出了 ip route，也就是 iproute2 提供的命令），就可以知晓系统中所有数据包的走向，但是，大人，时代变了！\n如果你是 WireGuard 玩家，并且所有的流量都通过 WireGuard 路由出去，但你却无法通过 ip route 命令的输出中看出任何的蛛丝马迹：\ndefault via 192.168.100.254 dev eth0 proto dhcp src 192.168.100.63 metric 100 192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.63 192.168.100.254 dev eth0 proto dhcp scope link src 192.168.100.63 metric 100 路由表告诉我们，所有的流量都是通过物理网卡出去的，并没有通过 WireGuard 虚拟网络接口。这是为什么呢？\n路由表 # 事实上 Linux 从 2.2 版本左右的内核开始，便包含了多个路由表，而不是一个！同时，还有一套规则，这套规则会告诉内核如何为每个数据包选择正确的路由表。\n当你执行 ip route 时，你看到的是一个特定的路由表 main，除了 main 之外还有其他的路由表存在。路由表一般用整数来标识，也可以通过文本对其命名，这些命名都保存在文件 /etc/iproute2/rt_tables 中。默认内容如下：\n$ cat /etc/iproute2/rt_tables # # reserved values # 255 local 254 main 253 default 0 unspec # # local # #1 inr.ruhep Linux 系统中，可以自定义从 1－252 个路由表。Linux 系统默认维护了 4 个路由表：\n0：系统保留表。 253：defulte table。没特别指定的默认路由都放在该表。 254：main table。没指明路由表的所有路由放在该表。 255：locale table。保存本地接口地址，广播地址、NAT 地址，由系统维护，用户不得更改。 这里有一个很奇怪的单词：inr.ruhep，这可能是 Alexey Kuznetsov 添加的，他负责服务质量（QoS）在Linux内核中的实现，iproute2 也是他在负责，这个单词表示“核研究/俄罗斯高能物理研究所”，是 Alexey 当时工作的地方，可能指的是他们的内部网络。当然，还有另外一种可能，有一个老式的俄罗斯计算机网络/ISP 叫做 RUHEP/Radio-MSU。\n路由表的查看可有以下二种方法：\n$ ip route show table table_number $ ip route show table table_name 不要把路由表和 iptables 混淆，路由表决定如何传输数据包，而 iptables 决定是否传输数据包，他俩的职责不一样。\n路由策略 # 内核是如何知道哪个数据包应该使用哪个路由表的呢？答案已经在前文给出来了，系统中有一套规则会告诉内核如何为每个数据包选择正确的路由表，这套规则就是路由策略数据库。这个数据库由 ip rule 命令来管理，如果不加任何参数，将会打印所有的路由规则：\n0: from all lookup local 32766: from all lookup main 32767: from all lookup default 左边的数字（0, 32764, \u0026hellip;\u0026hellip;）表示规则的优先级：数值越小的规则，优先级越高。也就是说，数值较小的规则会被优先处理。\n路由规则的数值范围：1 ~ $2^{23}-1$\n除了优先级之外，每个规则还有一个选择器（selector）和对应的执行策略（action）。选择器会判断该规则是否适用于当前的数据包，如果适用，就执行对应的策略。最常见的执行策略就是查询一个特定的路由表（参考上一节内容）。如果该路由表包含了当前数据包的路由，那么就执行该路由；否则就会跳过当前路由表，继续匹配下一个路由规则。\n在 Linux 系统启动时，内核会为路由策略数据库配置三条缺省的规则：\n0：匹配任何条件，查询路由表 local (ID 255)，该表 local 是一个特殊的路由表，包含对于本地和广播地址的优先级控制路由。rule 0 非常特殊，不能被删除或者覆盖。 32766：匹配任何条件，查询路由表 main (ID 254)，该表是一个常规的表，包含所有的无策略路由。系统管理员可以删除或者使用另外的规则覆盖这条规则。 32767：匹配任何条件，查询路由表 default (ID 253)，该表是一个空表，它是后续处理保留。对于前面的策略没有匹配到的数据包，系统使用这个策略进行处理，这个规则也可以删除。 在默认情况下进行路由时，首先会根据规则 0 在本地路由表里寻找路由，如果目的地址是本网络，或是广播地址的话，或者是人工添加的子网，在这里就可以找到合适的路由，匹配之后就会进入本机上层协议（iptables INPUT 可以拦截到）；如果路由失败，就会匹配下一个不空的规则，在这里只有 32766 规则，在这里将会在主路由表里寻找路由；如果失败，就会匹配 32767 规则，即寻找默认路由表。如果失败，路由将失败。从这里可以看出，策略性路由是往前兼容的。\nWireGuard 全局路由策略 # 现在回到 WireGuard，很多 WireGuard 用户会选择将本机的所有流量通过 WireGuard 对端路由，原因嘛大家都懂得😁。\n配置嘛也很简单，只需将 0.0.0.0/0 添加到 AllowedIPs 里即可：\n# /etc/wireguard/wg0.conf [Interface] PrivateKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxx Address = 10.0.0.2/32 # PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE # PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE # ListenPort = 51820 [Peer] PublicKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxx Endpoint = 192.168.100.251:51820 AllowedIPs = 0.0.0.0/0 理论上这样就可以让所有的流量都通过对端路由了，但是如果你用的 wg-quick 版本比较旧，一顿操作猛如虎（wg-quick up wg0）之后，你会发现事情并不是你想象的那样，甚至可能连 WireGuard 对端都连不上了。主要还是因为 WireGuard 自身的流量也通过虚拟网络接口进行路由了，这肯定是不行的。\n新版本的 wg-quick 通过路由策略巧妙地解决了这个问题，我们来看看它妙在何处！\n首先，使用 wg-quick 启动 wg0 网卡：\n$ wg-quick up wg0 [#] ip link add wg0 type wireguard [#] wg setconf wg0 /dev/fd/63 [#] ip -4 address add 10.0.0.2/32 dev wg0 [#] ip link set mtu 1420 up dev wg0 [#] wg set wg0 fwmark 51820 [#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820 [#] ip -4 rule add not fwmark 51820 table 51820 [#] ip -4 rule add table main suppress_prefixlength 0 [#] sysctl -q net.ipv4.conf.all.src_valid_mark=1 [#] iptables-restore -n 嘻嘻，看到了熟悉的路由策略，这就打印所有的路由规则看看：\n$ ip rule 0: from all lookup local 32764: from all lookup main suppress_prefixlength 0 32765: not from all fwmark 0xca6c lookup 51820 32766: from all lookup main 32767: from all lookup default 好家伙，多了两条规则：\n32764: from all lookup main suppress_prefixlength 0 32765: not from all fwmark 0xca6c lookup 51820 我们来扒扒他们的底裤，揭开神秘面纱。先来灵魂三问：suppress_prefixlength 是啥？0xca6c 又是啥？数据包怎么可能 not from all？\nRule 32764 # 先从规则 32764 开始分析，因为它的数值比较小，会被优先匹配：\n32764: from all lookup main suppress_prefixlength 0 这条规则没有使用选择器，也就是说，内核会为每一个数据包去查询 main 路由表。我们来看看 main 路由表内容是啥：\n$ ip route default via 192.168.100.254 dev eth0 proto dhcp src 192.168.100.63 metric 100 192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.63 192.168.100.254 dev eth0 proto dhcp scope link src 192.168.100.63 metric 100 如果真的是这样，那所有的数据包都会通过 main 路由表路由，永远不会到达 wg0。你别忘了，这条规则末尾还有一个参数：suppress_prefixlength 0，这是啥意思呢？参考 ip-rule(8) man page：\nsuppress_prefixlength NUMBER reject routing decisions that have a prefix length of NUMBER or less. 这里的 prefix 也就是前缀，表示路由表中匹配的地址范围的掩码。因此，如果路由表中包含 10.2.3.4 的路由，前缀长度就是 32；如果是 10.0.0.0/8，前缀长度就是 8。\nsuppress 的意思是抑制，所以 suppress_prefixlength 0 的意思是：拒绝前缀长度小于或等于 0 的路由策略。\n那么什么样的地址范围前缀长度才会小于等于 0？只有一种可能：0.0.0.0/0，也就是默认路由。以我的机器为例，默认路由就是：\ndefault via 192.168.100.254 dev eth0 proto dhcp src 192.168.100.63 metric 100 如果数据包匹配到了默认路由，就拒绝转发；如果是其他路由，就正常转发。\n这条规则的目的很简单，管理员手动添加到 main 路由表中的路由都会正常转发，而默认路由会被忽略，继续匹配下一条规则。\nRule 32765 # 下一条规则就是 32765：\n32765: not from all fwmark 0xca6c lookup 51820 这里的 not from all 是 ip rule 格式化的问题，有点反人类，人类更容易理解的顺序应该是这样：\n32765: from all not fwmark 0xca6c lookup 51820 从前面 wg-quick up wg0 的输出来看，规则的选择器是没有添加 from 前缀（地址或者地址范围）的：\nip -4 rule add not fwmark 51820 table 51820 如果规则选择器没有 from 前缀，ip rule 就会打印出 from all，所以这条规则才会是这个样子。\n51820 是一个路由表，也是由 wg-quick 创建的，只包含一条路由：\n$ ip route show table 51820 default dev wg0 scope link 所以这条规则的效果是：匹配到该规则的所有数据包都通过 WireGuard 对端进行路由，除了 not fwmark 0xca6c。\n0xca6c 只是一个防火墙标记，wg-quick 会让 wg 标记它发出的所有数据包（wg set wg0 fwmark 51820），这些数据包已经封装了其他数据包，如果这些数据包也通过 WireGuard 进行路由，就会形成一个无限路由环路。\n所以 not from all fwmark 0xca6c lookup 51820 意思是说，满足条件 from all fwmark 0xca6c（WireGuard 发出的都带 fwmark 0xca6c）请忽略本条规则，继续往下走。否则，请使用 51820 路由表，通过 wg0 隧道出去。\n对于 wg0 接口发包自带的 0xca6c，继续走下一条规则，也就是匹配默认的 main 路由表：\n32766: from all lookup main 此时已经没有抑制器了，所有的数据包都可以自由使用 main 路由表，因此 WireGuard 对端的 Endpoint 地址会通过 eth0 接口发送出去。\n完美！\nwg-quick 创建的路由表和 fwmark 使用的是同一个数字：51820。0xca6c 是 51820 的十六进制表示。\n总结 # wg-quick 这种做法的巧妙之处在于，它不会扰乱你的主路由表，而是通过规则匹配新创建的路由表。断开连接时只需删除这两条路由规则，默认路由就会被重新激活。你学废了吗？\n","date":"2022年8月31日","externalUrl":null,"permalink":"/posts/linux-routing-of-wireguard/","section":"博客","summary":"很久以前，我们只需要在 Linux 终端中输入 route -n（后来演变出了 ip ro","title":"WireGuard 基础教程：wg-quick 路由策略解读","type":"posts"},{"content":"","date":"2022年8月3日","externalUrl":null,"permalink":"/tags/argo-cd/","section":"标签","summary":"","title":"Argo CD","type":"tags"},{"content":"在上一篇『 GitOps 介绍』中，我介绍了什么是 GitOps，包括 GitOps 的原则和优势，以及 GitOps 与 DevOps 的区别。本文将介绍用于实施 GitOps 的工具 Argo CD。\nArgo CD 是以 Kubernetes 作为基础设施，遵循声明式 GitOps 理念的持续交付（continuous delivery, CD）工具，支持多种配置管理工具，包括 ksonnet/jsonnet、kustomize 和 Helm 等。它的配置和使用非常简单，并且自带一个简单易用的可视化界面。\n按照官方定义，Argo CD 被实现为一个 Kubernetes 控制器，它会持续监控正在运行的应用，并将当前的实际状态与 Git 仓库中声明的期望状态进行比较，如果实际状态不符合期望状态，就会更新应用的实际状态以匹配期望状态。\n在正式开始解读和使用 Argo CD 之前，我们需要先搞清楚为什么需要 Argo CD？它能给我们带来什么价值？\n传统 CD 工作流 # 从上篇文章『 GitOps 介绍』可以知道，目前大多数 CI/CD 工具都使用基于 Push 的部署模式，例如 Jenkins、CircleCI 等。这种模式一般都会在 CI 流水线运行完成后执行一个命令（比如 kubectl）将应用部署到目标环境中。\n这种 CD 模式的缺陷很明显：\n需要安装配置额外工具（比如 kubectl）； 需要 Kubernetes 对其进行授权； 需要云平台授权； 无法感知部署状态。也就无法感知期望状态与实际状态的偏差，需要借助额外的方案来保障一致性。 下面以 Argo CD 为例，来看看遵循声明式 GitOps 理念的 CD 工具是怎么实现的。\n使用 Argo CD 的 CD 工作流 # 和传统 CI/CD 工具一样，CI 部分并没有什么区别，无非就是测试、构建镜像、推送镜像、修改部署清单等等。重点在于 CD 部分。\nArgo CD 会被部署在 Kubernetes 集群中，使用的是基于 Pull 的部署模式，它会周期性地监控应用的实际状态，也会周期性地拉取 Git 仓库中的配置清单，并将实际状态与期望状态进行比较，如果实际状态不符合期望状态，就会更新应用的实际状态以匹配期望状态。\n无论是通过 CI 流水线触发更新 K8s 编排文件，还是 DevOps 工程师直接修改 K8s 编排文件，Argo CD 都会自动拉取最新的配置并应用到 K8s 集群中。\n最终会得到一个相互隔离的 CI 与 CD 流水线，CI 流水线通常由研发人员（或者 DevOps 团队）控制，CD 流水线通常由集群管理员（或者 DevOps 团队）控制。\nArgo CD 的优势 # 下面我们来看看 Argo CD 相较于传统 CD 工具有哪些比较明显的优势。\nGit 作为应用的唯一真实来源 # 所有 K8s 的声明式配置都保存在 Git 中，并把 Git 作为应用的唯一事实来源，我们不再需要手动更新应用（比如执行脚本，执行 kubectl apply 或者 helm install 命令），只需要通过统一的接口（Git）来更新应用。\n此外，Argo CD 不仅会监控 Git 仓库中声明的期望状态，还会监控集群中应用的实际状态，并将两种状态进行对比，只要实际状态不符合期望状态，实际状态就会被修正与期望状态一致。所以即使有人修改了集群中应用的状态（比如修改了副本数量），Argo CD 还是会将其恢复到之前的状态。这就真正确保了 Git 仓库中的编排文件可以作为集群状态的唯一真实来源。\n当然，有时候我们需要快速更新应用并进行调试，通过 Git 来触发更新还是慢了点，这也不是没有办法，我们可以修改 Argo CD 的配置，使其不对手动修改的部分进行覆盖或者回退，而是直接发送告警，提醒管理员不要忘了将更新提交到 Git 仓库中。\n快速回滚 # Argo CD 会定期拉取最新配置并应用到集群中，一旦最新的配置导致应用出现了故障（比如应用启动失败），我们可以通过 Git History 将应用状态快速恢复到上一个可用的状态。\n如果你有多个 Kubernetes 集群使用同一个 Git 仓库，这个优势会更明显，因为你不需要分别在不同的集群中通过 kubectl delete 或者 helm uninstall 等手动方式进行回滚，只需要将 Git 仓库回滚到上一个可用的版本，Argo CD 便会自动同步。\n集群灾备 # 如果你在 青云北京3区中的 KubeSphere 集群出现故障，且短期内不可恢复，可以直接创建一个新集群，然后将 Argo CD 连接到 Git 仓库，这个仓库包含了整个集群的所有配置声明。最终新集群的状态会与之前旧集群的状态一致，完全不需要人工干预。\n使用 Git 实现访问控制 # 通常在生产环境中是不允许所有人访问 Kubernetes 集群的，如果直接在 Kubernetes 集群中控制访问权限，必须要使用复杂的 RBAC 规则。在 Git 仓库中控制权限就比较简单了，例如所有人（DevOps 团队，运维团队，研发团队，等等）都可以向仓库中提交 Pull Request，但只有高级工程师可以合并 Pull Request。\n这样做的好处是，除了集群管理员和少数人员之外，其他人不再需要直接访问 Kubernetes 集群，只需访问 Git 仓库即可。对于程序而言也是如此，类似于 Jenkins 这样的 CI 工具也不再需要访问 Kubernetes 的权限，因为只有 Argo CD 才可以 apply 配置清单，而且 Argo CD 已经部署在 Kubernetes 集群中，必要的访问权限已经配置妥当，这样就不需要给集群外的任意人或工具提供访问的证书，可以提供更强大的安全保障。\n扩展 Kubernetes # 虽然 Argo CD 可以部署在 Kubernetes 集群中，享受 Kubernetes 带来的好处，但这不是 Argo CD 专属的呀！Jenkins 不是也可以部署在 Kubernetes 中吗？Argo CD 有啥特殊的吗？\n那当然有了，没这金刚钻也不敢揽这瓷器活啊，Argo CD 巧妙地利用了 Kubernetes 集群中的很多功能来实现自己的目的，例如所有的资源都存储在 Etcd 集群中，利用 Kubernetes 的控制器来监控应用的实际状态并与期望状态进行对比，等等。\n这样做最直观的好处就是可以实时感知应用的部署状态。例如，当你在 Git 仓库中更新配置清单中的镜像版本后，Argo CD 会将集群中的应用更新到最新版本，你可以在 Argo CD 的可视化界面中实时查看更新状态（比如 Pod 创建成功，应用成功运行并且处于健康状态，或者应用运行失败需要进行回滚操作）。\nArgo CD 架构 # 从功能架构来看，Argo CD 主要有三个组件：API Server、Repository Server 和 Application Controller。从 GitOps 工作流的角度来看，总共分为 3 个阶段：检索、调谐和呈现。\n检索 \u0026ndash; Repository Server # 检索阶段会克隆应用声明式配置清单所在的 Git 仓库，并将其缓存到本地存储。包含 Kubernetes 原生的配置清单、Helm Chart 以及 Kustomize 配置清单。履行这些职责的组件就是 Repository Server。\n调谐 \u0026ndash; Application Controller # 调谐（Reconcile）阶段是最复杂的，这个阶段会将 Repository Server 获得的配置清单与反映集群当前状态的实时配置清单进行对比，一旦检测到应用处于 OutOfSync 状态，Application Controller 就会采取修正措施，使集群的实际状态与期望状态保持一致。\n呈现 \u0026ndash; API Server # 最后一个阶段是呈现阶段，由 Argo CD 的 API Server 负责，它本质上是一个 gRPC/REST Server，提供了一个无状态的可视化界面，用于展示调谐阶段的结果。同时还提供了以下这些功能：\n应用管理和状态报告； 调用与应用相关的操作（例如同步、回滚、以及用户自定义的操作）； Git 仓库与集群凭证管理（以 Kubernetes Secret 的形式存储）； 为外部身份验证组件提供身份验证和授权委托； RBAC 增强； Git Webhook 事件的监听器/转发器。 部署 Argo CD # Argo CD 有两种不同的部署模式：\n多租户 # Argo CD 最常用的部署模式是多租户，一般如果组织内部包含多个应用研发团队，就会采用这种部署模式。用户可以使用可视化界面或者 argocd CLI 来访问 Argo CD。argocd CLI 必须先通过 argocd login \u0026lt;server-host\u0026gt; 来获取 Argo CD 的访问授权。\n$ argocd login SERVER [flags] ## Login to Argo CD using a username and password $ argocd login cd.argoproj.io ## Login to Argo CD using SSO $ argocd login cd.argoproj.io --sso ## Configure direct access using Kubernetes API server $ argocd login cd.argoproj.io --core 多租户模式提供了两种不同的配置清单：\n非高可用 # 推荐用于测试和演示环境，不推荐在生产环境下使用。有两种部署清单可供选择：\ninstall.yaml - 标准的 Argo CD 部署清单，拥有集群管理员权限。可以使用 Argo CD 在其运行的集群内部署应用程序，也可以通过接入外部集群的凭证将应用部署到外部集群中。 namespace-install.yaml - 这个部署清单只需要 namespace 级别的权限。如果你不需要在 Argo CD 运行的集群中部署应用，只需通过接入外部集群的凭证将应用部署到外部集群中，推荐使用此部署清单。还有一种花式玩法，你可以为每个团队分别部署单独的 Argo CD 实例，但是每个 Argo CD 实例都可以使用特殊的凭证（例如 argocd cluster add \u0026lt;CONTEXT\u0026gt; --in-cluster --namespace \u0026lt;YOUR NAMESPACE\u0026gt;）将应用部署到同一个集群中（即 kubernetes.svc.default，也就是内部集群）。 ⚠️注意：namespace-install.yaml 配置清单中并不包含 Argo CD 的 CRD，需要自己提前单独部署：kubectl apply -k https://github.com/argoproj/argo-cd/manifests/crds\\?ref\\=stable。\n高可用 # 与非高可用部署清单包含的组件相同，但增强了高可用能力和弹性能力，推荐在生产环境中使用。\nha/install.yaml - 与上文提到的 install.yaml 的内容相同，但配置了相关组件的多个副本。 ha/namespace-install.yaml - 与上文提到的 namespace-install.yaml 相同，但配置了相关组件的多个副本。 Core # Core 模式也就是最精简的部署模式，不包含 API Server 和可视化界面，只部署了每个组件的轻量级（非高可用）版本。\n用户需要 Kubernetes 访问权限来管理 Argo CD，因此必须使用下面的命令来配置 argocd CLI：\n$ kubectl config set-context --current --namespace=argocd # change current kube context to argocd namespace $ argocd login --core 也可以使用命令 argocd admin dashboard 手动启用可视化界面。\n具体的配置清单位于 Git 仓库中的 core-install.yaml。\n除了直接通过原生的配置清单进行部署，Argo CD 还支持额外的配置清单管理工具。\nKustomize # Argo CD 配置清单也可以使用 Kustomize 来部署，建议通过远程的 URL 来调用配置清单，使用 patch 来配置自定义选项。\napiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: argocd resources: - https://raw.githubusercontent.com/argoproj/argo-cd/v2.0.4/manifests/ha/install.yaml Helm # Argo CD 的 Helm Chart 目前由社区维护，地址： https://github.com/argoproj/argo-helm/tree/master/charts/argo-cd。\n下面演示一下部署过程。如果没有现成的 Kubernetes 环境，可以通过 KubeSphere Cloud 托管集群服务快速创建一个，免费体验时间为 2 小时，到期后集群会自动删除，可不限次重建。\n创建 Kubernetes 集群的过程很简单，首先注册登录 https://kubesphere.cloud 控制台，然后点击 托管集群服务 打开 新建 Kubernetes 集群 页面，填写集群名称，选择运行环境，点击 新建 菜单即可创建集群。\n几秒钟之后便会创建完毕，并显示集群基本信息。下载 kubeconfig，便可使用 kubectl 来访问集群。\n接下来开始部署 Argo CD：\n$ kubectl create namespace argocd $ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml 查看部署结果：\n$ kubectl -n argocd get pod argocd-applicationset-controller-69879c47c-pcbkg 1/1 Running 0 26m argocd-notifications-controller-6b4b74d8d8-s7mrz 1/1 Running 0 26m argocd-redis-65596bf87-2hzcv 1/1 Running 0 26m argocd-dex-server-78c9764884-6lcww 1/1 Running 0 26m argocd-repo-server-657d46f8b-87rzq 1/1 Running 0 26m argocd-application-controller-0 1/1 Running 0 26m argocd-server-6b48df79dd-b7bkw 1/1 Running 0 26m 访问 Argo CD # 部署完成后，可以通过 Service argocd-server 来访问可视化界面。\n$ kubectl -n argocd get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE argocd-applicationset-controller ClusterIP 10.105.250.212 \u0026lt;none\u0026gt; 7000/TCP,8080/TCP 5m10s argocd-dex-server ClusterIP 10.108.88.97 \u0026lt;none\u0026gt; 5556/TCP,5557/TCP,5558/TCP 5m10s argocd-metrics ClusterIP 10.103.11.245 \u0026lt;none\u0026gt; 8082/TCP 5m10s argocd-notifications-controller-metrics ClusterIP 10.98.136.200 \u0026lt;none\u0026gt; 9001/TCP 5m9s argocd-redis ClusterIP 10.110.151.108 \u0026lt;none\u0026gt; 6379/TCP 5m9s argocd-repo-server ClusterIP 10.109.131.197 \u0026lt;none\u0026gt; 8081/TCP,8084/TCP 5m9s argocd-server ClusterIP 10.98.23.255 \u0026lt;none\u0026gt; 80/TCP,443/TCP 5m9s argocd-server-metrics ClusterIP 10.103.184.121 \u0026lt;none\u0026gt; 8083/TCP 5m8s 如果你的客户端可以直连 Service IP，那就直接可以通过 argocd-server 的 Cluster IP 来访问。或者可以直接通过本地端口转发来访问：\n$ kubectl port-forward svc/argocd-server -n argocd 8080:443 Forwarding from 127.0.0.1:8080 -\u0026gt; 8080 Forwarding from [::1]:8080 -\u0026gt; 8080 初始密码以明文形式存储在 Secret argocd-initial-admin-secret 中，可以通过以下命令获取：\n$ kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=\u0026#34;{.data.password}\u0026#34; | base64 -d; echo 也可以通过以下命令来修改登录密码：\n$ argocd account update-password --account admin --current-password xxxx --new-password xxxx 登录后的界面：\nArgo CD 核心概念 # 在正式开始使用 Argo CD 之前，需要先了解两个基本概念。\nArgo CD Application # Argo CD 中的 Application 定义了 Kubernetes 资源的来源（Source）和目标（Destination）。来源指的是 Git 仓库中 Kubernetes 资源配置清单所在的位置，而目标是指资源在 Kubernetes 集群中的部署位置。\n来源可以是原生的 Kubernetes 配置清单，也可以是 Helm Chart 或者 Kustomize 部署清单。\n目标指定了 Kubernetes 集群中 API Server 的 URL 和相关的 namespace，这样 Argo CD 就知道将应用部署到哪个集群的哪个 namespace 中。\n简而言之，Application 的职责就是将目标 Kubernetes 集群中的 namespace 与 Git 仓库中声明的期望状态连接起来。\nApplication 的配置清单示例：\n如果有多个团队，每个团队都要维护大量的应用，就需要用到 Argo CD 的另一个概念：项目（Project）。\nArgo CD Project # Argo CD 中的项目（Project）可以用来对 Application 进行分组，不同的团队使用不同的项目，这样就实现了多租户环境。项目还支持更细粒度的访问权限控制：\n限制部署内容（受信任的 Git 仓库）； 限制目标部署环境（目标集群和 namespace）； 限制部署的资源类型（例如 RBAC、CRD、DaemonSets、NetworkPolicy 等）； 定义项目角色，为 Application 提供 RBAC（例如 OIDC group 或者 JWT 令牌绑定）。 Demo 演示 # 最后通过一个简单的示例来展示 Argo CD 的工作流程。\n准备 Git 仓库 # 在 GitHub 上创建一个项目，取名为 argocd-lab，为了方便实验将仓库设置为公共仓库。在仓库中新建 dev 目录，在目录中创建两个 YAML 配置清单，分别是 deployment.yaml 和 service.yaml。\n配置清单内容如下：\n# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: selector: matchLabels: app: myapp replicas: 2 template: metadata: labels: app: myapp spec: containers: - name: myapp image: nginx:latest ports: - containerPort: 80 # service.yaml apiVersion: v1 kind: Service metadata: name: myapp-service spec: selector: app: myapp ports: - port: 80 protocol: TCP targetPort: 80 接下来在仓库根目录中创建一个 Application 的配置清单：\n# application.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: myapp-argo-application namespace: argocd spec: project: default source: repoURL: https://github.com/yangchuansheng/argocd-lab.git targetRevision: HEAD path: dev destination: server: https://kubernetes.default.svc namespace: myapp syncPolicy: syncOptions: - CreateNamespace=true automated: selfHeal: true prune: true 参数解释：\nsyncPolicy : 指定自动同步策略和频率，不配置时需要手动触发同步。\nsyncOptions : 定义同步方式。\nCreateNamespace=true : 如果不存在这个 namespace，就会自动创建它。 automated : 检测到实际状态与期望状态不一致时，采取的同步措施。\nselfHeal : 当集群世纪状态不符合期望状态时，自动同步。 prune : 自动同步时，删除 Git 中不存在的资源。 Argo CD 默认情况下每 3 分钟会检测 Git 仓库一次，用于判断应用实际状态是否和 Git 中声明的期望状态一致，如果不一致，状态就转换为 OutOfSync。默认情况下并不会触发更新，除非通过 syncPolicy 配置了自动同步。\n如果嫌周期性同步太慢了，也可以通过设置 Webhook 来使 Git 仓库更新时立即触发同步。具体的使用方式会放到后续的教程中，本文不再赘述。\n创建 Application # 现在万事具备，只需要通过 application.yaml 创建 Application 即可。\n$ kubectl apply -f application.yaml application.argoproj.io/myapp-argo-application created 在 Argo CD 可视化界面中可以看到应用已经创建成功了。\n点进去可以看到应用的同步详情和各个资源的健康状况。\n如果你更新了 deployment.yaml 中的镜像，Argo CD 会自动检测到 Git 仓库中的更新，并且将集群中 Deployment 的镜像更新为 Git 仓库中最新设置的镜像版本。\nKubeSphere 从 3.3.0 开始也提供了基于 GitOps 的 CD方案，引入 Argo CD 作为 CD 的后端，而且可视化界面更加炫酷，感兴趣的小伙伴可以试试使用 KubeSphere 来创建管理 Application。\n总结 # 本文介绍了 Argo CD 的优势、架构和工作原理，并通过一个简单的示例对其功能进行演示，比如修改 Git 仓库内容后，可以自动触发更新。还可以通过 Event Source 和 Trigger 实现更多自动化部署的需求。\n在部署 Kubernetes 资源时，Argo CD 还支持 Kustomize、Helm、Ksonnet 等资源描述方式，包括其他更高级的使用方式都会在后续的教程中为大家一一道来，敬请期待。\n","date":"2022年8月3日","externalUrl":null,"permalink":"/posts/getting-started-with-argocd/","section":"博客","summary":"在上一篇『 GitOps 介绍』中，我介绍了什么是 GitOps，包括 GitOps 的原","title":"Argo CD 入门教程","type":"posts"},{"content":"","date":"2022年8月3日","externalUrl":null,"permalink":"/tags/devops/","section":"标签","summary":"","title":"DevOps","type":"tags"},{"content":"","date":"2022年8月3日","externalUrl":null,"permalink":"/tags/gitops/","section":"标签","summary":"","title":"GitOps","type":"tags"},{"content":" GitOps 这个概念最早是由 Kubernetes 管理公司 Weaveworks 公司在 2017 年提出的，如今已经过去了 5 个年头，想必大家对这个概念早有耳闻，但你可能并不知道它到底是什么，它和 DevOps 到底是啥关系，本文就来帮大家一一解惑。\n基础设施即代码 # 在理解 GitOps 之前，我们需要先理解什么是基础设施即代码。\n基础设施即代码（Infrastructure as Code, IaC），顾名思义，表示使用代码（而非手动流程）来定义基础设施，研发人员可以像对待应用软件一样对待基础设施，例如：\n可以创建包含基础架构规范的声明式配置文件，从而便于编辑和分发配置。 可以确保每次配置的环境都完全相同。 可以进行版本控制，所有的变更都会被记录下来，方便溯源。 可以将基础设施划分为若干个模块化组件，并通过自动化以不同的方式进行组合。 当然，广义上的 IaC 不仅仅只关于基础设施，还包含了网络、安全、配置等等，所以广义上的 IaC 又叫 X as Code。\n比如你想在 AWS 中创建服务器，配置网络，部署 Kubernetes 集群以及各种工作负载，你只需要定义好 Terraform 或 Ansible 的声明式配置，以及 Kubernetes 的配置清单即可，免去一切繁杂的手动操作。\nGitOps 是什么 # GitOps = IaC + Git + CI/CD，即基于 IaC 的版本化 CI/CD。它的核心是使用 Git 仓库来管理基础设施和应用的配置，并且以 Git 仓库作为基础设施和应用的单一事实来源，你从其他地方修改配置（比如手动改线上配置）一概不予通过。\nGit 仓库中的声明式配置描述了目标环境当前所需基础设施的期望状态，借助于 GitOps，如果集群的实际状态与 Git 仓库中定义的期望状态不匹配，Kubernetes reconcilers 会根据期望状态来调整当前的状态，最终使实际状态符合期望状态。\n另一方面，现代应用的开发更多关注的是迭代速度和规模，拥有成熟 DevOps 文化的组织每天可以将代码部署到生成环境中数百次，DevOps 团队可以通过版本控制、代码审查以及自动测试和部署的 CI/CD 流水线等最佳实践来实现这一目标，这就是 GitOps 干的事情。\nGitOps vs DevOps # 从广义上来看，GitOps 与 DevOps 并不冲突，GitOps 是一种技术手段，而 DevOps 是一种文化。GitOps 是一种实现持续交付（Continuous Delivery）、持续部署（Continuous Deployment）和基础设施即代码（IaC）的工具和框架，它是支持 DevOps 文化的。\n从狭义上来看，GitOps 与 DevOps 有以下几个区别：\n首先，GitOps 是以目标为导向的。它使用 Git 来维护期望状态，并不断调整实际状态，最终与期望状态相匹配。而 DevOps 更多关注的是最佳实践，这些实践可以普遍应用于企业的每一个流程。\n其次，GitOps 采取声明式的操作方法，而 DevOps 同时接受声明式和命令式的方法，所以 DevOps 除了适用于容器环境之外，还适用于虚拟机和裸机环境。\n最后，GitOps 重新定义了云原生场景下的 CI/CD，它以 Git 作为中心的不可变状态声明，以加快持续部署速度。\nGitOps 的设计哲学 # 想要使用 GitOps 来管理你的基础设施和应用，需要践行以下几个原则：\n1. 声明式 # 必须通过声明式来描述系统的期望状态。例如 Kubernetes，众多现代云原生工具都是声明式的，Kubernetes 只是其中的一种。\n2. 版本控制/不可变 # 因为所有的状态声明都存储在 Git 仓库中，并且把 Git 仓库作为单一事实来源，那么所有的操作都是从 Git 仓库里驱动的，而且保留了完整的版本历史，方便回滚。有了 Git 优秀的安全保障，也可以使用 SSH 密钥来签署 commits，对代码的作者和出处实施强有力的安全保障。\n3. 自动应用变更 # Git 仓库中声明的期望状态发生了任何变更，都可以立即应用到系统中，而且不需要安装配置额外工具（比如 kubectl），也不需要配置 Kubernetes 的认证授权。\n4. 持续的 Reconciliation # Reconciliation 其实最早是 Kubernetes 里的一个概念，表示的是确保系统的实际状态与期望状态一致的过程。具体的实现方式是在目标环境中安装一个 agent，一旦实际状态与期望状态不匹配，agent 就会进行自动修复。这里的修复比 Kubernetes 的故障自愈更高级，即使是手动修改了集群的编排清单，集群也会被恢复到 Git 仓库中的清单所描述的状态。\n鉴于以上这些设计哲学，我们来看一下 GitOps 的工作流：\n首先，团队中的任何一个成员都可以 Fork 仓库对配置进行更改，然后提交 Pull Request。 接下来会运行 CI 流水线，一般会做这么几件事情：验证配置文件、执行自动化测试、检测代码的复杂性、构建 OCI 镜像、将镜像推送到镜像仓库等等。 CI 流水线运行完成后，团队中拥有合并代码权限的人将会将这个 Pull Request 合并到主分支中 。一般拥有这个权限的都是研发人员、安全专家或者高级运维工程师。 最后会运行 CD 流水线，将变更应用到目标系统中（比如 Kubernetes 集群或者 AWS） 。 整个过程完全自动化且透明，通过多人协作和自动化测试来保证了基础设施声明配置的健壮性。而传统的模式是其中一个工程师在自己的电脑上操作这一切，其他人不知道发生了什么，也无法对其操作进行 Review。\nPush vs Pull # CD 流水线有两种模式：Push 和 Pull。\nPush 模式 # 目前大多数 CI/CD 工具都使用基于 Push 的部署模式，例如 Jenkins、CircleCI 等。这种模式一般都会在 CI 流水线运行完成后执行一个命令（比如 kubectl）将应用部署到目标环境中。\n这种 CD 模式的缺陷很明显：\n需要安装配置额外工具（比如 kubectl）； 需要 Kubernetes 对其进行授权； 需要云平台授权； 无法感知部署状态。也就无法感知期望状态与实际状态的偏差，需要借助额外的方案来保障一致性。 Kubernetes 集群或者云平台对 CI 系统的授权凭证在集群或云平台的信任域之外，不受集群或云平台的安全策略保护，因此 CI 系统很容易被当成非法攻击的载体。\nPull 模式 # Pull 模式会在目标环境中安装一个 Agent，例如在 Kubernetes 集群中就靠 Operator 来充当这个 Agent。Operator 会周期性地监控目标环境的实际状态，并与 Git 仓库中的期望状态进行比较，如果实际状态不符合期望状态，Operator 就会更新基础设施的实际状态以匹配期望状态。\n只有 Git 的变更可以作为期望状态的唯一来源，除此之外，任何人都不可以对集群进行任何更改，即使你修改了，也会被 Operator 还原为期望状态，这也就是传说中的不可变基础设施。\n目前基于 Pull 模式的 CD 工具有 Argo CD， Flux CD 以及 ks-devops。\nGitOps 的优势 # 一般 GitOps 首选的都是基于 Pull 的部署模式，因为这种模式有很多不可替代的优势。\n更强大的安全保障 # 上面已经提到了，使用 GitOps 不需要任何 Kubernetes 或者云平台的凭证来执行部署，Kubernetes 集群内的 Argo CD 或者 Flux CD 只需要访问 Git 仓库，并通过 Pull 模式来更新即可。\n另一方面，Git 由用于跟踪和管理代码变更的强大密码学支持，拥有对变更进行签名以证明作者身份和来源的能力，这是保障集群安全的关键。\nGit 作为事实的唯一真实来源 # 因为所有的应用包括基础设施的声明式配置都保存在 Git 中，并把 Git 作为应用系统的唯一事实来源，因此可以利用 Git 的强大功能操作所有东西，例如版本控制、历史记录、审计和回滚等等，无需使用 kubectl 这样的工具来操作。\n提高生产力 # Git 也是开发人员非常熟悉的工具，通过 Git 不断迭代，可以提高生产率，加快开发和部署速度，更快地推出新产品，同时提高系统的稳定性和可靠性。\n更容易合规的审计 # 使用 GitOps 的基础设施可以像任何软件项目一样使用 Git 来管理，所以同样可以对其进行质量审计。当有人需要对基础设施进行更改时，会创建一个 Pull Request，等相关人员对其进行 Code Review 之后，更改才可以应用到系统中。\n总结 # GitOps 是对现有 DevOps 文化的补充，它使用 Git 这样的版本控制系统来自动部署基础设施，部署过程清晰可见，可以查看和跟踪对系统进行的任何变更，提高了生产力、安全性和合规性。而且 GitOps 提供了更优雅的可观测性，可以实时观测部署状态，并采取行动使实际状态与期望状态保持一致。\n而且在 GitOps 中，整个系统都是通过声明式来描述的，天然适合云原生环境，因为 Kubernetes 也是这么设计的。\n参考资料 # What is GitOps and what should you know about it? DevOps vs GitOps: 4 Benefits you must know to Master the Methodologies Guide To GitOps ","date":"2022年7月3日","externalUrl":null,"permalink":"/posts/what-is-gitops/","section":"博客","summary":"GitOps 这个概念最早是由 Kubernetes 管理公司 Weaveworks 公司在 2017 年提出的，如今已经过去","title":"GitOps 介绍","type":"posts"},{"content":"","date":"2022年6月10日","externalUrl":null,"permalink":"/tags/nas/","section":"标签","summary":"","title":"NAS","type":"tags"},{"content":"","date":"2022年6月10日","externalUrl":null,"permalink":"/tags/truenas/","section":"标签","summary":"","title":"TrueNAS","type":"tags"},{"content":"","date":"2022年6月10日","externalUrl":null,"permalink":"/tags/zfs/","section":"标签","summary":"","title":"ZFS","type":"tags"},{"content":" 原文链接： https://mtlynch.io/budget-nas/\n本文已获取原作者的翻译授权\n今年我决定给自己量身定制一台家庭网络存储服务器（也就是 NAS），预计存储容量有 32TB，并使用开源的操作系统，用来存储我的个人和商业数据。\n服务器本身花了 $531，额外花了 $732 买了四块硬盘，总成本达到了 $1,263。这个价格与购买现成的 NAS 服务器差不多，但我的方案提供了更多的功能和可定制性。\n本文我将会给大家介绍自己当初是如何选择硬件的，中间犯了哪些错误，最后会给有兴趣构建个人 NAS 服务器的小伙伴提供一些有参考价值的建议。\n组装 TrueNAS 服务器前后对比 我还录制了一个视频，欢迎观看：\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 背景 # 为什么需要 NAS 服务器？ # NAS 即 网络附加存储（Network-attached storage），NAS 服务器的主要工作就是存储数据，并将其提供给你网络上的其他计算机使用。\n那么，为什么一定要使用一个完整的专用服务器来存储数据呢？毕竟每台计算机都可以存储数据。\n我认为将数据与其他系统解耦是有益的，我本人每隔两到三年就会升级我的工作站和笔记本电脑，而在不同电脑之间迁移数据非常麻烦。使用专门的 NAS 服务器就可以免去大多数不必要的数据迁移工作，而且各个系统之间还可以共享文件。\n除此之外，我还是一个 数据囤积狂，我保留了之前拍摄的每一张数码照片，以及过去 20 年里收发的所有电子邮件，再加上所有个人项目的源代码，总共有 8.5TB。\n我最大的数据来源是自己收藏的 DVD 和蓝光碟片，本人不太喜欢依赖流媒体服务来保存喜欢的影视作品，所以我至今仍然会购买影视作品的实体拷贝，一旦买到一张新的光盘，我就会将原影像翻录出来，并制作成一个可流式传输的视频文件。在原始 ISO 拷贝和可流式传输的 MP4 之间，一张光盘可以占用 60GB 的硬盘空间。\n我仍然会为需要多次观看的影视作品购买 DVD 或蓝光碟片 什么是 Homelab？ # \u0026ldquo;Homelab\u0026rdquo; 是一个口语化的术语，最近几年越来越受欢迎。\n一个 Homelab 其实就是你家里的一片区域，你可以像在办公室或者数据中心一样在这个区域中试验 IT 硬件或软件。它可以作为练习专业技能的实践环境，也可以用来把玩一些有趣的技术。\n为什么要自己组装 NAS？ # 如果你是 Homelab 新手，或者没有组装 PC 的经验，建议不要尝试自己组装 NAS。你可以选择一体化的解决方案（比如群晖、威联通这种），这样学习曲线会比较平缓。\n在组装自己的 Homelab NAS 之前，我已经使用了 7 年的 4 盘位 群晖 DS412+。我觉得群晖很好，性价比很高，如果你是 NAS 小白，建议直接买群晖吧。\n为我服务了七年之久的 10TB 群晖 DS412+ 几个月前，我的群晖启动失败了，并开始发出咔咔的声音。这时我开始意识到自己对这台设备的依赖程度如此之重，想到这里后背就一阵发凉。因为群晖的服务器是不可修复的，如果其中一个零件在保修期之后出故障了，你只能更换整台服务器。如果你跟我一样不是技术大拿，而且使用了群晖专属的存储格式，也没有额外的群晖服务器，那么此时你就无法访问这台服务器上的数据，也无法恢复（ Hacker News 上的一位大佬告诉我可以 从一个非群晖系统中恢复群晖的混合 RAID 卷）。\n万幸的是，在我清理并重置了硬盘之后，数据就恢复了。这件事也给我敲响了警钟，我决定改用 TrueNAS，因为它提供了一个开放存储格式的开源实现。\nTrueNAS 和 ZFS # TrueNAS（前身叫 FreeNAS）是存储服务器最流行的操作系统之一，完全开源，而且已经存在了将近 20 年，看起来是一个靠谱的 NAS 系统。\nTrueNAS 使用的文件系统是 ZFS，这是一个专门为存储服务器设计的文件系统。NTFS 或 ext4 等传统文件系统运行在管理低级磁盘 I/O 的数据卷之上。ZFS 可以管理从文件级别逻辑到磁盘 I/O 的一切内容，相比于其他文件系统，ZFS 的控制更全面，拥有更多的功能和更强的性能。\nZFS 的亮点：\n将多个物理硬盘聚合到一个文件系统中； 数据完整性验证和自动修复； 创建磁盘中数据的时间点快照（类似于 macOS 的 Time Machine 功能）； 可选择加密或压缩硬盘中的数据。 在使用 TrueNAS 之前，我对 ZFS 的经验是零，所以我非常想尝试一下这个新奇的文件系统。\n存储规划 # 预估所需存储容量 # 之前我使用群晖时，插入了三个 4TB 的硬盘，并将第四个插槽留空。然后通过群晖的混合 Raid 来构建文件系统，总容量是 7TB。使用了三年之后容量不足，于是又增加了第四块硬盘，总容量达到了 10TB。\n对于这个全新的 NAS，我决定采取和之前类似的策略，我需要这个系统的存储容量能满足我当前的需求，并且能留有一定的增长空间。粗略估计当前需要 20TB 存储容量，如果以后再增加硬盘，最高可达 30TB 存储容量。\nZFS 目前还不允许向现有的存储池中添加新的硬盘驱动器，但该功能 正在积极开发中，希望在我需要扩展存储的时候，TrueNAS 会俱备这个功能。\n多个小硬盘还是少量大硬盘？ # ZFS 的设初衷是抵御硬盘故障，它会以冗余的方式存储每个数据块。这个特点使存储容量规划变得很复杂，因为可用存储的总容量不仅仅是每个硬盘容量的总和。\nZFS 会从硬盘组成的存储池中创建文件系统，存储池中的硬盘数量越多，存储容量的利用率越高。例如，如果给 ZFS 提供两个 10 TB 的硬盘，则只能使用总硬盘容量的一半。如果改用 5 个 4TB 硬盘，ZFS 将会提供 14TB 的可用存储容量。虽然这两种情况下硬盘的总容量相同，但后一种方案比前一种方案增加了 40% 的可用容量。\n在组装 NAS 时，我们需要思考到底是使用多个小容量的硬盘还是使用少量的大容量硬盘。这个问题要辨证地看，小容量的硬盘通常性价比更高，但是运行成本会更高，例如两个 4 TB 硬盘需要的电力是单个 8TB 硬盘的两倍。\n我还是想减少服务器的占用的物理空间，因此我选择了容量大的硬盘。\n选择 raidz 1, 2, 还是 3? # ZFS 提供了 3 种不同的磁盘阵列：raidz1，raidz2 和 raidz3，它们的主要区别在于健壮性。raidz1可以承受一个磁盘故障而不丢失数据， raidz2 可以承受两个硬盘同时发生故障，而 raidz3 可以承受三个。\n健壮性越强，可用的存储容量越少，毕竟能量守恒嘛。我有 5 个 4TB 硬盘，下面列出了每个 ZFS 磁盘阵列的可用存储容量：\nZFS 磁盘阵列类型 可用存储容量 存储利用率 raidz1 15.4 TB 77.2% raidz2 11.4 TB 57.2% raidz3 7.7 TB 38.6% 最终我选择了 raidz1，因为我的硬盘数量不多，两个硬盘同时发生故障的概率比较低。\n注意： ZFS 不是一种备份策略。ZFS 可以保护你免受磁盘故障的影响，但还是有很多威胁是 ZFS 无能为力的，比如意外删除数据、恶意软件攻击或者物理盗窃。我选择使用 restic 将所有重要的东西备份到加密的云存储中。 ZFS 的价值在于，如果其中一块硬盘坏了，可以直接换掉，不必求助于云备份。如果同时有两块硬盘坏了，我才会选择从云备份恢复（因为我使用的是 raidz1）。这个选择过程非常痛苦，但我仍然选择 raidz1，因为我觉得不值得为了 raidz2 而放弃服务器 20% 的可用存储空间。\n一般来说，硬盘数量越多，对磁盘阵列的健壮性要求就更高。如果我的存储池是由 20 快硬盘组成的，我可能会使用 raidz2 或 raidz3。\n防止多个硬盘同时故障 # 从概率上来看，两块硬盘同时发生故障的概率几乎为零。根据 Backblaze 的统计，质量比较高的硬盘每年发生故障的概率为 0.5-4%，就算是 4% 吧，每 48 年至多才会遇到一次两块硬盘同时发生故障，这个概率已经很低了，几乎不用担心。\n但从实际情况来看，这种统计方式并不科学，如果其中一块硬盘出现了故障，那么其他硬盘在这个时刻出现故障的风险将大大增加，因为你的硬盘很可能是同一型号，来自同一制造批次，并且处理着相同的工作负载，一旦出故障，很可能就是同时出故障。\n除此之外，发生故障后重建 ZFS 存储池也不是个好办法，这会给正常工作的硬盘带来更多的压力，正常情况下可以使用几个月的硬盘可能会在重建存储池时直接挂掉。\n考虑到上述这些风险，我需要采取一些措施来减少两块硬盘同时发生故障的风险，办法也很简单粗暴，直接从两个不同的厂商那里购买两种相同型号的硬盘即可。这种方案虽然没有科学论证，但也没啥附加的成本，还能图个心理安慰，何乐而不为呢？😂\n我从两个不同的厂商那里购买了两种相同型号的硬盘 如何挑选硬件 # 主板 # 首先要明确主板的尺寸。我之前一直比较欣赏群晖 DS412+ 的紧凑外形，还从来没有用过 mini-ITX 主板来组装电脑，机会难得。\n最终我选择了 ASUS Prime A320I-K，原因如下：\n有四个 SATA 接口，我可以直接将四块硬盘接到主板上； 支持 Radeon 图像处理技术，这样我就不用再单独购买显卡了； 价格实惠，只需 $98。 ⚠️警告：我现在有点后悔选择了这个主板，参考 下面的讨论。\nB450 这个主板也不错，与 ASUS Prime A320I-K 很相似，但价格却翻了一倍，目测对超频的支持更好，但我对这方面没什么需求。\nCPU # 以我的了解，ZFS 对 CPU 的要求并不高。我之前在一台廉价的戴尔 OptiPlex 7040 迷你 PC 上安装过 TrueNAS，并做过一些基本测试，结果表明 ZFS 并没有怎么使用 CPU，所以选择低功率的 CPU 应该没啥问题。\n我选择 CPU 的主要标准是必须要支持 Radeon 图像处理技术，这样我就可以使用 A320 主板的板载 HDMI 输出。\nAMD Athlon 3000G价格低廉，并且原生支持 Radeon 图像处理技术 最终我选择了 AMD Athlon 3000G，仅售 $105，物超所值，还支持 Radeon 图像处理技术， CPU 基准测试也表现良好。\n机箱 # 我最喜欢的电脑机箱是 Fractal Design，所以我选择了 Fractal Design Node 304 Black。这是一个紧凑的迷你 ITX 机箱，不像传统的塔式机箱，它的设计样式更接近于立方体，而且有 6 个硬盘托架，不管是目前使用还是将来增加硬盘都够用了。\nThe Fractal Design Node 304 Black 是一款迷你 ITX 机箱，有 6 个硬盘托架 数据盘 # 我的机箱有 6 个硬盘托架，所以我决定购买四块 8TB 的硬盘作为数据盘。使用 raidz1 时可用存储容量可达 22.5TB；将来如果增加第五块硬盘，可用存储容量将达到 30.9TB；如果再增加第六块硬盘，可用存储容量将达到 37TB。\n8TB 的硬盘 RPM（revolutions per minute，即转/每分钟） 基本上都不会低于 7200，最高可达 10k RPM。RPM 高于 7200 对我来说并没有什么影响，因为主要瓶颈在于网络。也没必要选择 10k RPM 的硬盘，性能并不会强多少，性价比不高。\n根据 Backblaze 的硬盘统计数据，硬盘价格越高，越不容易发生故障。我也考虑过购买 $400 的硬盘，因为它们的故障率非常低，但后来仔细一想，花两倍的钱将故障率降低几个百分点是不划算的。\n最后强调一点：不要购买 使用 SMR（Shingled Magnetic Recording，叠瓦式磁记录）技术的硬盘，因为 ZFS 在 SMR 硬盘上的表现非常差。建议直接购买传统的使用 CMR（Conventional Magnetic Recording，传统式磁记录）技术的硬盘。\n最终我选择了 东芝 N300 和 希捷 IronWolf，主要是因为 TrueNAS 论坛和 Reddit 上面对这两款硬盘的评价都比较积极，而且价格也很合理，都在 $180-190 之间。\n东芝 N300（左） 和希捷 IronWolf（右） 系统盘 # TrueNAS 需要将系统安装在独立的硬盘中，但是对硬盘要求不高，只需要 2GB 的空间，而且不会经常读写。\n金士顿 A400 固态硬盘，容量 120GB，价格 $32 最终我选择了 金士顿 A400，因为价格便宜，120GB 只要 $32，而且是 M.2 固态硬盘。M.2 好啊，不需要连数据线也不需要连电源线，而且外形小巧纤薄，几乎不占用任何空间。\n内存条 # 经过我的研究发现，很多人会提到 ZFS 的一条法则：系统中每 TB 的硬盘空间需要 1GB 的内存。但 ZFS 研发人员 Richard Yao 又说 根本没有这种规则，ZFS 的确有部分功能对内存的要求比较高（比如删除重复数据），其他情况下 ZFS 只需要很少的内存。\n内存的选购非常无聊，根本找不到可信的基准测试和用户报告，我的选购过程是这样的：\n查看有哪些内存条 与华硕 A320I-K 主板兼容。 筛选出 16GB 和 32GB 的内存条，因为我需要两根内存条来组成 32GB 或 64GB内存。 筛选出值得信任的品牌（Corsair, Crucial, G.SKILL, Kingston, Samsung, Patriot, Mushkin, HyperX）。 筛选出价格低于 $150 的内存条。 最终我选择了 CORSAIR Vengeance LPX 32GB CMK32GX4M2A2400C14 (2 x 16GB)，价格只有 $128。\nCORSAIR Vengeance LPX 32GB CMK32GX4M2A2400C14 (2 x 16GB) 与 A320I-K 主板兼容，价格合理 电源（power supply unit，PSU） # 如果只看电源功率，基本上选择任何消费级 PSU 都够用了。根据 PCPartPicker 的数据，我的系统只需要 218 瓦的电源。本来我想买的是 300-400 瓦的 PSU，但市面上没有这个功率的半模组 PSU，最终只能选择 500 瓦的 EVGA 110-BQ-0500-K1。\nEVGA 110-BQ-0500-K1 是一款半模组 PSU，功率为 500 瓦，完全够用 90 度角 SATA 电缆 # 由于机箱空间限制，我需要一个 90 度角 SATA 电缆 在这之前我从来没有用过 90 度角 SATA 电缆，但我的主板和 PSU 之间的空间太小了，放不下标准的 SATA 电缆，只能使用 90 度角的 SATA 电缆来解决这个问题。\n暂不考虑的硬件 # 由于价格、复杂性或物理空间的原因，有几个硬件不在我的考虑范围之内。\n显卡（GPU） # 由于物理空间限制，再加上主板接口有限，我就不使用专用显卡了，直接使用支持 Radeon 图像处理技术的主板即可。\n主机总线适配器（HBA） # NAS 一般都需要一个 主机总线适配器（HBA），HBA 是一个可以放入主板 PCI 插槽的芯片，用来增加主板可以支持的硬盘数量。\n我暂时还不需要 HBA，华硕 A320I-K 主板的 4 个 SATA 接口足以满足我当下的需求，我只需留出一个 PCI 插槽为将来的 HBA 做准备即可。\nECC 内存 # 在研究不同的 TrueNAS 组装方案时，我看到了一部分贴子说 ECC 内存（使用了能够实现错误检查和纠正技术的内存条）是防止数据损坏的必备条件，但最终我还是选择了普通的内存条。虽然我也不希望内存数据被破坏，但我在过去 30 年中一直用的都是普通的内存条，并没有遇到过内存数据损坏的情况，而且我只是家用而已，普通内存条应该够用了。\n单独的 SLOG 硬盘 # 许多人使用 ZFS 会用到一块单独的专用 SSD，这块单独的 SSD 被称为 SLOG (separate intent log)。\n系统向文件系统写入数据时，会产生很多的日志文件，这些日志文件写到专门的 SSD 比直接写到多个数据盘中要快好几个数量级。这样可以 显著提高写入速度，因为当应用向数据盘写入数据时，ZFS 可以快速将对数据写入操作的意图的日志文件写入专门的 SSD，然后直接告诉应用写入成功了，接下来再根据日志文件异步地将数据转移到存储池中。\n受硬盘托架和接口的限制，最终我没有选择专门的 SLOG 硬盘，因为增加一个 SLOG 硬盘就需要放弃唯一的 PCI 插槽或者浪费其中一个硬盘托架，不划算。我宁愿把这部分空间留出来给以后增加数据盘使用。\n我的硬件列表 # 硬件类别 型号 价格 CPU AMD Athlon 3000G $105.13 主板 华硕 Prime A320I-K $97.99 显卡 不需要，主板自带 $0 系统盘 金士顿 A400 120GB $31.90 内存条 CORSAIR Vengeance LPX 32GB CMK32GX4M2A2400C14 (2 x 16GB) $127.99 电源 EVGA 110-BQ-0500-K1 500W 80+ Bronze Semi-Modular $44.99 机箱 Fractal Design Node 304 Black $99.99 SATA 电缆 Silverstone Tek Ultra Thin Lateral 90 Degree SATA Cables (x2) $22.30 总价 $530.29 数据盘 东芝 N300 HDWG480XZSTA 8TB 7200 RPM (x2) $372.79 数据盘 希捷 IronWolf 8TB NAS Hard Drive 7200 RPM (x2) $359.98 总价 $1,263.06 注意：该主板可能与 AMD Athlon 3000G CPU 不兼容，参考下文。\n与商业 NAS 产品对比 # Product 2022 年自己组装的 NAS 群晖 DS920+ 威联通 TS-473A-8G-US 硬盘托架数量 6 4 4 内存容量 32 GB 4 GB 4 GB 最高内存容量 32 GB 8 GB 8 GB CPU 跑分 4479 3002 4588 总价 $530.29 $549.99 $549 从上述表格来看，我自己组装的 NAS 总成本与商业 NAS 产品差不多，但性价比更高，因为内存是他们的 8 倍，而且操作系统是开源的，没有所谓的供应商锁定。\n组装花絮 # 所有零部件 在 Fractal Design 迷你 ITX 机箱中安装主板 我太喜欢 M.2 SSD 了，不需要数据线，拧个螺丝就完了 这是我组装的第一个不把 PSU 的背面暴露在机箱外的系统，机箱有一条很短的 NEMA 延长线，将内部 PSU 引向机箱自身的外部电源输入。 主板的 SATA 接口和 PSU 之间的空间非常狭窄，只能使用特殊的 90 度角 SATA 电缆。 将所有东西都接到主板后面（CPU 风扇除外） 大功告成 使用 TinyPilot 管理服务器 # 老读者应该还记得，我用 Raspberry Pi 创建了一个专门用于初始化和管理服务器的工具叫 TinyPilot。这台 NAS 是我用 TinyPilot 搭建的第三个服务器，也是我用 TinyPilot 最新版本 TinyPilot Voyager 2 搭建的第一台服务器。\nTinyPilot Voyager 2 可以在无需键盘、鼠标和显示器的情况下给服务器安装操作系统 TinyPilot Voyager 2 真是太方便了！无需将键盘或显示器连接到服务器上，就可以启动 BIOS 并安装 TrueNAS 操作系统，所有的这一切都在我的浏览器中完成。\nTinyPilot 还是有一些小问题的，不过无伤大雅。比如它虽然可以加载 .img 和 .iso 等镜像文件，但它还不知道如何与目标计算机共享原始文件。当我需要为华硕的 BIOS 升级加载 .CAP 文件时，我将这些文件放到了 USB 中，这样 TinyPilot 就找不到了。希望 TinyPilot 能尽快支持这种场景，下次我就好升级 BIOS 了。\n是 BIOS 版本不兼容？还是我傻？ # 当我把所有零部件都组装好，接通电源之后傻眼了，显示器（TinyPilot）上没有看到任何图像输出。\n什么鬼？难道我误解了主板的兼容性要求？重新安装内存，重新安装 CPU，并检查所有电缆，结果还是一样。。。\n最后不得不搬出祖传秘籍：谷歌搜索。一番搜索之后，看到有人提到华硕 Prime A320I-K 主板需要升级 BIOS 才能与 AMD Athlon 3000G 兼容。虽然我之前挑选主板的时候看到过这个警告，但被我忽视了。\n现在就比较尴尬了，这是一个先有鸡还是先有蛋的问题。。。因为只有 CPU 正常工作，我才好升级 BIOS。不过问题不大，我 2017 年 Homelab 服务器中使用的 Ryzen 7 CPU 和华硕 Prime A320 主板是兼容的，我将那台服务器的 CPU 和 GPU 拿下来插到 NAS 服务器上，终于成功开机了！\n使用旧的 Homelab 服务器 CPU 来升级 NAS 的 BIOS 最让我无语的是，系统启动之后，主板显示我的 BIOS 版本仍然是 2203，也就是华硕声称它与 AMD Athlon 3000G CPU 兼容的 BIOS 版本。可是我明明已经将 BIOS 更新到了最新的 5862 版本，不管它了。。\n华硕 Prime A320I-K 主板的 CPU 兼容性页面声称兼容 AMD Athlon 3000G CPU 的最低 BIOS 版本为 2203 到这里问题还没有解决，系统启动后仍然看不到启动画面。排查了一通后发现我把 HDMI 线插到了 DisplayPort 接口中，我被自己蠢哭了😂\nDisplayPort 接口为啥和 HDMI 这么像？很容易让人插错线诶 现在在回过头来看一下之前的问题，细思极恐，问题真的是 BIOS 和 CPU 不兼容吗？现在没法验证了，我想大概有两种可能：\n我太蠢了，将 HDMI 线插到 DisplayPort 接口里了，直到我升级了 BIOS 之后才发现这个问题。 华硕才是蠢货，误导大众，AMD Athlon 3000G CPU 与 BIOS 2203 版本根本就不兼容。 不管如何，现在终于启动成功了，而且不需要再借助外部的硬件了，可以松一口气了。\n性能测试 # 目前还找不到较好的基准测试工具来测试 NAS 的性能，因为大部分测试工具都是对本地磁盘 I/O 进行测试，而真实世界的使用场景是通过网络访问的，所以这种测试结果是没有参考价值的。\n我是这么测试的：先 生成两组带有随机数据的文件，然后使用 robocopy 来测试本地客户端和 NAS 之间的传输速度。这种测试方法也不是很严格，因为我没有使用完全独立的网络进行测试，测试时也没有关闭桌面上的其他进程。作为对照，我对旧的群晖 DS412+ 也进行了测试。\n每一个 NAS 测试了两组不同的文件。第一组文件总共有 20 GiB，每个文件大小是 1 GiB；第二组文件总共有 3 GiB，每个文件大小是 1 MiB。而且我对加密卷和非加密卷分别进行了测试，每一组测试 3 次，取平均值。\n读取性能 # 非加密卷的测试结果显示，已经使用 7 年开始生锈的群晖比全新的 TrueNAS 性能更好。群晖读取小文件比 TrueNAS 快 31%，读取大文件比 TrueNAS 快 10%。\n到加密卷测试部分，群晖就不行了，被 TrueNAS 碾压。群晖对加密卷的读取速度比非加密卷下降了 67-75%，而 TrueNAS 却几乎没有变化。最终结果表明 TrueNAS 对加密卷小文件的读取速度是群晖的 2.3 倍，对加密卷大文件的读取速度是群晖的 3 倍。我的大部分数据都是加密的，所以这个测试结果更能代表我的使用场景。\n写入性能 # 尽管群晖读取非加密卷的速度超越了 TrueNAS，在写入方面却不尽人意。即便是非加密卷，TrueNAS 对小文件的写入速度也比群晖快了 77%，对大文件的写入速度和群晖不相上下。\n加密卷就更离谱了，TrueNAS 对加密卷小文件的写入速度是群晖的 5.2 倍，对加密卷大文件的写入速度是群晖的 3.2 倍。\n功耗测试 # 我使用 Kill A Watt P4460 电力使用监控器来测量 TrueNAS 和群晖的功耗情况：\n群晖 DS412+ 2022 NAS Idle 38 W 60 W Load 43 W 67 W 测试结果表明新服务器的功耗比旧的群晖多了 60%，这让我有点懵逼，我这边的电费是 $0.17/千瓦时，这么一算服务器每个月的成本是 $7.20。\n具体什么原因还不太清楚，可能是 PSU 的缘故。群晖的 PSU 和其他组件的功耗完全匹配，而 TrueNAS 的 500W PSU 可能利用率只有 15%，系统不需要这么大的功率。\n使用感受 # 主板 # 我对华硕 Prime A320I-K 主板最大的意见就是兼容性，也有可能是我搞错了（前面解释过）。\n即便是我搞错了，我还是要吐槽一下它的 BIOS 升级体验，按道理应该可以直接下载升级最新的 BIOS 固件，但是我升级了之后它还是提示我需要升级，最后我不得不手动下载固件并上传到 USB 进行手动升级。\n修复 Realtek 网络驱动 # 当我的系统网络负载很高时，主板上的以太网适配器经常会挂掉。Reddit 论坛上的一位网友帮我找到了原因，FreeBSD 针对 A320I-K 主板的 Realtek 网卡的驱动不稳定，我们可以将其替换为官方的驱动，步骤如下：\n打开 TrueNAS 可视化界面，依次进入 System \u0026gt; Tunables； 添加下面两个选项： 变量 值 类型 if_re_load YES loader if_re_name /boot/modules/if_re.ko loader 机箱 # 说实话，整体使用下来，我对 Fractal Design Node 304 这个机箱很失望， 我还是比较喜欢之前使用的 Fractal Design Meshify C，因为它有一部分功能是我在其他机箱身上从来没见过的。\n虽然 Fractal Design Node 304 看起来还不错，但实际使用时却是非常尴尬，没有任何文档可供参考，官方提供的案例也是不痛不痒的。\n当然了，我知道机箱设计师为了缩小机箱的体积必须在其他方面有所牺牲，或许是我太苛刻了。\nCPU # CPU 我非常满意，Athlon 3000G 对我来说性能过剩，过去一个月的 CPU 负载一直都是 99% 空闲。\n这个 CPU 最吸引我的一点是支持 AMD 的 Radeon 图像处理技术，这样就不需要单独的显卡了。价格只有 $105，很划算。\n数据盘 # 数据盘暂时不作评判，目前一切安好，五年后再看。\n一开始我担心数据盘噪声太大，可结果表明，只有在性能测试期间删除文件的时候，才会听到硬盘的声音。\n电源（PSU） # 我的系统空转功率是 60 瓦，明显用不到这么大功率的电源，当时要是多花点精力挑选功率更低的电源就好了，实际上我只需要一个 300-400 瓦的电源。\n系统盘 # 系统盘选择金士顿 A400 是明智的，非常稳定，容量用来承载 TrueNAS 操作系统绰绰有余。\nTrueNAS # 我安装的是 TrueNAS Core 13，使用的 FreeBSD 版本相对而言比较成熟。你也可以安装 TrueNAS Scale，它基于 Debian，具有更广泛的硬件和软件兼容性。\n如果要比较用户界面，群晖是很难被打败的，这是见过的 NAS 中最优雅直观的界面，非常简洁，用户无需了解地层文件系统的技术细节。而 TrueNAS 有一股黑客风，它的界面似乎是由一个对命令行以外的东西不屑一顾的人设计的。\nTrueNAS 想要创建一个新卷，并通过 SAMBA 共享出去，需要在几个毫不相干的菜单之间来回切换，而且没有任何提示告诉我接下来该怎么操作。群晖就比较简单了，它会一步一步地引导我完成所需的设置。\nTrueNAS 安装第三方应该也比较麻烦，就拿 Plex 举例，虽然 Plex 是 TrueNAS 的预装插件，但我还是花了一个小时的时间来搜索文档。相比之下，在群晖上安装 Plex 就是点两下鼠标的事情，两分钟就可以搞定。\n即便如此，我还是坚持使用 TrueNAS，因为我更关心的是厂商和平台锁定，而且我喜欢开源软件。如果我要给不在乎意识形态的朋友推荐 NAS，我一定会推荐群晖。\nZFS # ZFS 功能很强大，但目前我只用到了 RAID 功能，其他功能暂时没有需求。\n很多人喜欢 ZFS 的快照功能，但我的 restic 备份方案中已经有快照功能了，所以暂时也用不到 ZFS 的快照功能。我已经使用 restic 两年了，印象中只一次需要从快照中恢复数据。\n还有一个功能是为加密数据创建快照，这个功能比较有趣，它可以在不解密数据的情况下直接创建快照。我有很多不需要经常访问的加密数据，使用这个功能就能够在无需解密的情况下进行定期备份。\n总结 # 总的来说，我还是很喜欢这个新 NAS 的，折腾的过程中也学到了很多东西。毕竟这不是我第一次使用 NAS，之前使用群晖已经储备了相关的技术能力，切换到 TrueNAS 之后也就没有那么吃力。当然了，该学还是要学的，我已经准备好恶补 ZFS 和 TrueNAS 的相关知识了。\n","date":"2022年6月10日","externalUrl":null,"permalink":"/posts/budget-nas/","section":"博客","summary":"原文链接： https://mtlynch.io/budget-nas/ 本文已获取原作者的翻译授权 今年我决定给自己量身定","title":"组装一台 22TB 容量的 NAS（家庭存储服务器）","type":"posts"},{"content":"","date":"2022年6月10日","externalUrl":null,"permalink":"/categories/tech/","section":"分类","summary":"","title":"黑科技","type":"categories"},{"content":"","date":"2022年5月17日","externalUrl":null,"permalink":"/tags/cilium/","section":"标签","summary":"","title":"Cilium","type":"tags"},{"content":" 原文链接： https://isovalent.com/blog/post/2022-05-16-tetragon\nIsovalent Cilium 企业版 包含一个基于 eBPF 的实时安全可观测性和运行时增强（runtime enforcement）平台，2022 年 5 月 16 日，Isovalent 终于决定将该平台的主要功能开源，并将其命名为 Tetragon。\n什么是 Tetragon？ # Tetragon 提供了基于 eBPF 的完全透明的安全可观测性能力以及实时的运行时增强（runtime enforcement）能力。由于基于 eBPF 的内核级收集器中直接内置了智能内核过滤能力和聚合逻辑，因此 Tetragon 无需更改应用程序即可以非常低的开销实现深度的可观测性。内嵌的运行时执行层不仅能够在系统调用层面进行访问控制，而且能够检测到特权、Capabilities 和命名空间的提权逃逸，并实时自动阻止受影响进程的进一步执行。\n智能可观测性 # Tetragon 的基石是一个强大的可观测层，它可以观测整个系统，从低级别的内核可见性到跟踪文件访问、网络活动或能力（capability）变化，一直到应用层，涵盖了诸如对易受攻击的共享库的函数调用、跟踪进程执行或解析发出的 HTTP 请求。总的来说，Tetragon 可以提供对各种内核子系统的可观测性，涵盖了命名空间逃逸、Capabilities 和特权升级、文件系统和数据访问、HTTP、DNS、TLS 和 TCP 等协议的网络活动，以及系统调用层的事件，以审计系统调用和跟踪进程执行。\n深度可观测性：可以观测整个系统和应用程序的几乎所有调用环节。比如检测 TCP 连接中的低级微突发（microbursts），为黄金信号监控面板提供 HTTP 可见性，或者检测特定易受攻击的共享库的使用的能力。Tetragon 提供了一个易于使用的框架，以涵盖更多的可观测性用例，因此可以探索更多的可能性。 完全透明：Tetragon 所有的可观察性数据都是从内核中透明地收集的，无需更改应用程序代码，应用也无法检测到自己何时被监控，这是安全用例的理想选择。 低开销：Tetragon 直接在内核中使用 eBPF 执行过滤、聚合、度量统计和直方图收集，大大减少了系统的开销。此外，Tetragon 使用高效的数据结构，如每个 CPU 的哈希表、环形缓冲区和 LRU 地图，以提供高效和快速的数据收集手段，并避免向用户空间 agent 发送大量的低信号事件。 译者注：微突发（microbursts）是指端口在非常短的时间（毫秒级别）内收到非常多的突发数据。\n运行时增强（runtime enforcement） # 基于丰富的可观测性，Tetragon 还提供了实时的运行时增强（runtime enforcement）能力。大部份运行时增强（runtime enforcement）系统都只有有限的一组强制执行点（例如仅在系统调用级别），而 Tetragon 能够以预防的方式在整个操作系统中执行安全策略，而不是对事件异步地做出反应。除了能够为多个层级的访问控制指定允许列表外，Tetragon 还能够自动检测特权和 Capabilities 升级或命名空间提权（容器逃逸），并自动终止受影响的进程。安全策略可以通过 Kubernetes（CRD）、JSON API 或 Open Policy Agent（OPA）等系统注入。\n预防式安全：Tetragon 的运行时策略直接在内核中执行，并且是同步的（实时的），这样可以真正防止攻击，而不仅仅是对攻击做出异步的反应。 无需了解所有攻击载体：Tetragon 无需了解单个攻击载体并对其进行阻断，它的做法是允许你定义一系列难以被攻破的隔离和特权限制保证措施，并自动执行这些保证措施。 可插拔策略架构：Tetragon 允许执行多种不同来源的安全策略，包括用户自定义的策略，Open Policy Agent (OPA) 等系统定义的策略，以及通过可扩展的 Kubernetes CRD 和 JSON 接口对接来自第三方组件的安全策略。 运行时增强为何是实时的？ # 除了有更多的位置（enforcement points）可以执行策略，eBPF 还使得我们在遭受漏洞攻击时马上作出反应，实时、同步地执行策略。当然，Tetragon 也能够像其他运行时增强（runtime enforcement）系统一样允许或拒绝与特定参数相匹配的特定系统调用，但它的杀手锏是一旦观察到特权/功能升级或命名空间提权，便立即阻止进程继续运行，从而将运行时增强（runtime enforcement）功能提升到一个新的台阶。更厉害的是，Tetragon 甚至不需要了解这个攻击载体是什么。\nTetragon 另辟蹊径，它不需要了解特定的漏洞或攻击载体，而是直接定义执行策略，指定哪些应用程序应在运行时可以提升特权、附加额外的 Capabilities、跨越内核命名空间的边界，而后便监视内核的提权和逃逸，并自动终止违反定义策略的进程。而且杀死进程是在内核中同步执行的，这意味着如果一个进程使用 write(2) 或 sendmsg(2) 来利用内核漏洞获得权限，那么这些系统调用将永远不会返回，该进程及其所有线程都将被终止，不会再继续执行。\n为什么使用 Tetragon？ # 传统的可观测性和运行时增强（runtime enforcement）解决方案无外乎都是基于以下几个方面来实现，它们都有各自的优势和缺陷。而 Tetragon 利用 eBPF 将更多的优势进行结合，并消除了绝大多数的缺陷。\n应用检测 LD_PRELOAD ptrace(2) 应用检测使用代码依赖性来获得对应用的可观测性。 LD_PRELOAD 可以在感知不到应用的情况下加载一个共享库来拦截系统调用。 ptrace(2) 是一个由内核提供的调试接口，用于跟踪进程和系统调用。 优势： 高效 良好的可观测性 优势： 高效 比〖应用检测〗更加透明 优势: 比前两种方案更加透明 缺陷： 非透明，有侵入性，需要更改应用代码 可以绕过 不能执行策略 无法对系统进行观测 缺陷： 可以通过静态链接绕过 可观测性比较弱 由于应用程序可以通过静态链接绕过，因此不是理想的策略执行方式。 缺陷： 开销较大 策略不是同步执行的 应用可以感知到自己正在被监控 只能通过系统调用来实现可观测性 上述解决方案都是在应用程序和系统调用层面上执行，并且可观测性方案也各不相同。它们都有一个用户空间代理，这个代理依赖于按定义收集的可观测性数据，然后对其作出反应，且无法对内核级别的事件进行观测。\nseccomp SELinux / LSM 内核模块 seccomp(-bpf) 允许对系统调用进行过滤。 SELinux 和 LSM 是内置的内核安全框架，用于执行访问控制 Linux内核模块可用于实现可观测性和运行时增强（runtime enforcement）的自定义逻辑 优势： 高效 对应用程序透明 优势： 高效 对应用程序透明 不受 TOCTTOU 攻击的影响 优势： 高效 对应用程序透明 无需修改内核即可扩展 缺陷： 只能在系统调用层面执行策略 不支持指针参数的系统调用检测 没有观测能力 缺陷： 对容器/Kubernetes 的感知能力有限 无法扩展 需要提前了解攻击载体 缺陷： 许多环境不允许加载内核模块 可能会损害内核的稳定性（模块中的错误可能会使内核崩溃） 如果不暂时关闭安全保护，进行升级是非常有难度的 第二类解决方案都是直接在内核层面操作，主要针对运行时增强（runtime enforcement），观测能力较弱（甚至没有观测能力）。内置的内核系统提供了非常多的策略执行选项，但内核在构建时却只重点提供访问控制的能力，而且非常难以扩展，例如内核是无法感知到 Kubernetes 和容器的。虽然内核模块解决了可扩展性问题，但由于其产生的安全风险，在很多场景下往往不是一种明智的选择。\n像 LSM-eBPF 这样年轻的内核子系统功能非常强大，也非常有前景，只是需要依赖最新的内核（≥5.7）。Tetragon 可以使用 eBPF-LSM 作为策略执行点，而且不受 eBPF-LSM 所需的内核版本的限制。\n由此可见，在 eBPF 的加持下，Tetragon 结合了现有解决方案的绝大多数优势。\n高效 \u0026amp; 透明：Tetragon 提供了对应用的可观测性和高效的应用运行时检测，并且完全透明，对应用无侵入性，无需更改应用代码。 实时增强：Tetragon 提供了像 seccomp、SELinux 和 LSM 一样的内核级别同步执行策略的能力，但它将策略执行从纯粹的访问控制提升到了防止对系统和组件造成伤害的级别，而不是仅仅限制对资源和数据的访问。 深度可扩展的可观测性：Tetragon 提供了深度的系统观测能力和自定义 Linux 内核模块的可扩展性，同时没有安全和可用性风险。 此外，Tetragon 还提供了一个 agent，可以原生集成各种现代化的可观测性系统和策略标准（例如 Kubernetes、Prometheus、fluentd、Open Telemetry、Open Policy Agent 以及传统的 SIEM 平台）。\n自动缓解特权或容器逃逸 # Linux 内核的安全漏洞缓解是一个具有挑战性的问题。解决方案通常依赖于检测和阻止已知的攻击途径、减少攻击面或限制受损组件的爆炸半径。Tetragon 通过检测和停止相关进程来阻止内核中的特权、Capabilities 和命名空间提权。Kuberenetes 的每个应用程序都可以明确声明一组特权、Capabilities 和跨特权的命名空间。\n缓解 CVE-2021-22555 漏洞 # 如下是一个简单的策略，它描述了在 CAP_SYS_ADMIN 能力更改时，进程将终止：\napiVersion: cilium.io/v1alpha1 kind: TracingPolicy metadata: name: \u0026#34;capability-protection\u0026#34; spec: [...] selectors: - matchCapabilityChanges: - type: Effective operator: In values: - \u0026#34;CAP_SYS_ADMIN\u0026#34; matchActions: - action: Sigkill CVE-2021-22555 漏洞是通过 Netfilter 漏洞来获取特权。在实施上面的策略情况下，运行该漏洞，让我们看看会发生什么：\nroot@ubuntu2:/# /root/cve-2021-22555 [+] Linux Privilege Escalation by theflow@ - 2021 [+] STAGE 0: Initialization [*] Setting up namespace sandbox... [...] [+] STAGE 5: Post-exploitation [*] Escaping container... [*] Cleaning up... [*] Popping root shell... Killed 可以看到当利用该漏洞进行特权提权时，该进程直接被终止了。 请注意，上述的策略不包含漏洞本身的特定元素，所以使用不同攻击途径的不同漏洞进行攻击，会获得相同的结果。\n使用 Tetragon CLI 进行应用行为检查 # 在 Isovalent 中，Tetragon CLI 代号为 amazing-cli，可能是使用 Tetragon 进行可观测性的第一个切入点。该 CLI 通常是令人吃惊，因为它能以一种简单易懂的形式公开复杂系统的大量信息。\n在以下示例中，我们将展示如何使用 Tetragon 来观测 Kubernetes Pod 的行为，该 Pod 运行命令 curl -L github.com：\n开始执行 curl -L github.com 进行 github.com 的 DNS 解析，及端口 80 的 TCP 连接打开。 github.com 在 7 毫秒内返回 HTTP 码 301 ，将流量重定向到端口 443。启用 TLS，curl 重定向并执行另一个 DNS 解析。 TCP 连接端口 443 打开，开始 TLS 握手。 使用 TLS 1.3 协议，协商的密钥是 AES-128-GCM-SHA256。 进行数据交换。端口 80 上总共接收约 80 个字节，端口 443 上接收 218KiB。 进程退出，错误码为 0。 网络和运行时可观测性的结合 # Tetragon 使用 eBPF 的其中一个令人兴奋的优势是它可以结合多个方面的可观测性，目前为止，这些可观测性通常都是单独处理的。下面是一个结合网络和运行时可观测性的示例，以演示识别哪些进程涉及哪种类型的网络通信的能力。以下示例显示了使用 Tetragon 来观测一个 Kubernetes Pod，该 Pod 被入侵并受到横向移动攻击：\n在上图中，我们看到了通过反向 Shell 进行的经典横向移动攻击：\nKubernetes 的 Pod crawler-c57f9778c-wtcbc 在 Kubernetes 命名空间 tenant-jobs 中运行。Pod 是通过 Containerd 运行的，Containerd 作为 PID 1 init 进程的子进程运行。在 Pod 内运行的二进制文件称为“爬虫”，它会产生一个执行server.js 的 Node 进程。 Node 应用的出口网络连接是 api.twitter.com 和 Kubernetes 中的 Elasticsearch 服务。 Pod 启动 5 分钟后，又启动了另一个子进程调用 netcat (nc)。结合运行时和网络可观测性来看，很明显这是一个正在进行的反向 Shell 攻击。 然后，可以观察到攻击者正在运行 curl 访问内部 es 服务器，然后使用 curl 将检索到的数据上传到 S3 存储中。 监控对敏感文件的访问 # Tetragon 具有监控文件和数据访问的能力。 以下示例说明了 Tetragon 与 Splunk 集成，以跟踪对敏感文件的访问，同时提供访问的上下文（例如： Kubernetes 元数据、容器镜像、二进制文件和用户信息）。\n上面的示例列出了对 /etcd/passwd 的访问，包括进程、及容器镜像和 Kubernetes 命名空间。除了监视 /etc/passwd 或 /etc/shadow 之外，还可以监控系统上其他的明显文件，包括：容器运行时的 UNIX 套接字，及可能改变系统引导的 Systemd 单元或 Init 文件。\nTetragon 不仅提供了对此类敏感文件访问的监控能力，而且可以阻止访问此类文件。\n检测 TLS 弱密钥和版本 # TLS 是当今世界安全的基石，但使用较旧的 TLS 版本或不安全的 TLS 密钥可能会构成严重的安全威胁。 无意识的错误使用 TLS 密钥和版本会导致故意的 TLS 降级攻击。\n上面的仪表板显示了 TLS 协议版本信息，并将其与 Kubernetes Pod 和命名空间上下文相关联。 它还可以显示密钥信息，及更重要的是密钥长度信息。\n运行时感知的网络策略 # 你可能熟悉 Kubernetes 的 NetworkPolicies，它定义了 Kubernetes 工作负载的允许和禁止的网络通信。简而言之，这些策略描述了允许 Pod A 与 Pod B 或 CIDR 10.0.0.0/8 通信，但禁止 Pod A 与 Pod C 或 CIDR 20.1.1.1/32 通信。\n这些策略的粒度是在 Pod 级别。所以无论 Pod 中运行的 app.js 还是 attack.py 脚本调用 curl，这些策略都可以生效的。借助 Tetragon 技术，可以扩展 Cilium 的网络策略功能以包括运行时上下文：\nkind: CiliumNetworkPolicy [...] endpointSelector: - matchLabels: - name: Frontend egress: - toEndpoints: - matchLabels: - name: Backend fromRuntime: - binary: app.py privileged: false 上述策略获得明显更好的最小特权策略。它允许前端 Pod 与后端 Pod 对话，但前提是：\n源 Pod 中的二进制文件为 app.py 源 Pod 中的进程以非特权运行 上述示例将二进制名称和特权执行上下文考虑在内，此概念可以扩展为基于其他参数进行限制，例如：确保进程仍处于该命名空间、UID/GID 上下文，甚至考虑可执行文件的内存哈希。\n总结 # 我们非常乐意将 Tetragon 开源。额外的可观测性和安全控制将会帮助你们改善安全、平台和应用程序团队的工作，助力整体的基础架构（特别是 Kubernetes 环境）更加安全。更重要的是，有了开源社区的加持，我们非常期待接下来会发生的故事。\n如果你有兴趣了解更多关于 Tetragon 的信息，我们将在接下来的几周内举办一场 关于 Tetragon 的网络研讨会，包括：介绍、演示和提问的机会。 同时，加入 Cilium Slack 上的 #tetragon 以开始与我们的团队交流。\n如果你对 Isovalent Tetragon Enterprise 感兴趣，请随时与我们联系。\n","date":"2022年5月17日","externalUrl":null,"permalink":"/posts/tetragon/","section":"博客","summary":"原文链接： https://isovalent.com/blog/post/2022-05-16-tetragon Isovalent Cilium 企业版 包含一个基于 eBPF 的实时安全可观测性和运行","title":"Cilium 开源 Tetragon -- 基于 eBPF 的安全可观测性 \u0026 运行时增强","type":"posts"},{"content":"","date":"2022年5月17日","externalUrl":null,"permalink":"/tags/ebpf/","section":"标签","summary":"","title":"eBPF","type":"tags"},{"content":"","date":"2022年5月17日","externalUrl":null,"permalink":"/tags/tetragon/","section":"标签","summary":"","title":"Tetragon","type":"tags"},{"content":"","date":"2022年5月15日","externalUrl":null,"permalink":"/tags/clash/","section":"标签","summary":"","title":"Clash","type":"tags"},{"content":"","date":"2022年5月15日","externalUrl":null,"permalink":"/tags/grafana/","section":"标签","summary":"","title":"Grafana","type":"tags"},{"content":"","date":"2022年5月15日","externalUrl":null,"permalink":"/tags/loki/","section":"标签","summary":"","title":"Loki","type":"tags"},{"content":"众所周知，科学上网，又称番茄 / 魔法 / 武当纵云梯，是当代青年的必备技能。而想要科学上网，需要两个必备条件：\n需要有一个服务商提供的服务器订阅地址 需要安装对应的软件 服务商（机场）有很多，价格、节点、带宽和稳定性都丰俭由人，我现在的主要机场是 易帆云加速。Bywave 是一家走高端全内网中转线路的 v2ray 优质机场，不仅拥有阿里云 / WTT / HKT 等线路，还且还有全内网中转节点和 IPLC 专线（内网中转线路及 IPLC 专线成本极高，所以质量极佳，网络很流畅稳定，历次受到供给也无影响）ByWave 不像其他机场提供非常多的节点，需要频繁订阅更新和维护。没有注册的朋友可以点击 此链接注册体验。\n而代理软件也有多种选择，抛开收费产品不谈，免费代理软件目前最强大的是 Clash，Clash 是一个跨平台、支持 SS/V2ray/Trojan 协议、基于规则的网络代理软件，功能强大、界面美观、支持订阅，尤其适合机场和付费服务使用。基于 Clash 的图形界面客户端也非常多，比较流行的有 Clash for Windows、Clash X、Clash X Pro 以及 OpenWrt 使用的 OpenClash，区别都不大。\nClash 核心也有很多变种，比如 Clash Premium，与 Clash 都是同一个作者所写，区别仅在于闭源并提供了更高级的功能。\nClash Premium 内核有一个比较新的、还在实验中的功能叫 Tracing，可以方便的采集经过 Clash 核心的流量数据。本文将会介绍如何对 Clash Premium 的流量进行监控，并使用 Grafana 的可视化面板展示监控数据。 先上图：\n为了方便监控，Clash 的开发者新建了个项目叫 clash-tracing，利用 Clash Premium 的 Websocket Tracing API 收集数据，然后使用 Vector 将其转为日志，并 Push 到 Loki 中，最终使用 Grafana 的可视化监控面板来展示数据，非常实用。在监控之前，首先需要修改 Clash Premium 的配置文件开启 Tracing 功能：\nprofile: # open tracing exporter API tracing: true 如果你是使用 OpenClash，那么需要在〖配置文件管理〗的〖您可以在下方直接修改配置文件: config.yaml ，仅支持未被接管的设置〗中直接在顶部添加上面的内容，然后重启 OpenClash 即可。\n开发者已经提供了 docker-compose.yml，容器玩家可以直接通过该编排文件一条命令拉起所有服务，然后就没有然后了，完结撒花！\n等等，先别急着撒花，哪个云原生玩家没有一套自己的 Kubernetes 环境呢？能不能快速将这套监控服务部署到 Kubernetes 环境中呢？\n为了能够在 K8s 中一键部署这套监控服务，我 Fork 了该项目，添加了 GitHub Action 自动构建 Docker 镜像，并添加了 Kubernetes 编排文件，使用方法非常简单，就这么几条命令：\n# 先克隆仓库 $ git clone https://github.com/yangchuansheng/clash-tracing $ cd clash-tracing $ kubectl create ns monitoring # 修改 deployment.yaml 中的环境变量，然后执行如下命令： $ kubectl apply -f deployment.yaml $ kubectl apply -f vector $ kubectl apply -f loki 然后在你的 Grafana 可视化界面中添加 Loki 数据源，数据源的地址为 http://loki.monitoring:3100，名称为 loki。\n如果你的集群中没有 Grafana，自己部署一个就是了，本文就不赘述了。\n接下来执行以下命令将监控面板的 JSON 模板中的数据源改为 loki：\n$ bash hack.sh 最后将 panels/dashboard.json 和 panels/logs.json 导入 Grafana 即可。\n最终效果：\n","date":"2022年5月15日","externalUrl":null,"permalink":"/posts/monitoring-clash/","section":"博客","summary":"众所周知，科学上网，又称番茄 / 魔法 / 武当纵云梯，是当代青年的","title":"使用 Grafana 和 Loki 监控 Clash","type":"posts"},{"content":"","date":"2022年4月30日","externalUrl":null,"permalink":"/tags/firefox/","section":"标签","summary":"","title":"Firefox","type":"tags"},{"content":" Firefox 和 Chrome 分别是当今世界最流行的浏览器之一，虽然这两款浏览器都有各自的优势，但随着时间的推移，Firefox 的受欢迎程度在逐渐下降，开始走下坡路。这无可厚非，并不是 Firefox 不行了，而是 Chrome 太强了，背靠 Google 顶级大厂，无缝整合 Google 服务，界面极度简洁，它就像一个十足精美的篮子，你往里面放的鸡蛋越多，它就越好用。\n所以，先说结论，Firefox 几乎不可能在短时间内超过 Chrome 浏览器。\n然而，但是，作为用户，我们需要换个角度去思考，任何一个市场如果一家独大，那么这个家伙最终很可能会变成〖恶龙〗，想必大家对 Chrome 收集用户隐私的丑闻也有所耳闻。只有互相竞争才能尽最大可能避免〖恶龙〗的诞生，竞争越激烈，用户越受益，你们可长点心吧。\n如果你喜欢 Firefox，或者不希望 Chrome 变成最终的〖恶龙〗，请在你的电脑上为 Firefox 留下一席之地，哪怕是作为备用浏览器也行啊。而且 Firefox 是完全开源的，开源抑制垄断，Firefox 还你自由。\nFirefox 的优势 # 现在切回 Firefox 的视角，Firefox 背后的团队是 Mozilla 基金会，与 Google 这种世界上“最伟大”的公司相比，简直是不值一提。但 Firefox 既然能和 Chrome 在同一个牌桌上同台竞技，它必然是有过人之处的。\nMozilla 基金会对计算机领域最大的贡献是 Rust 编程语言，而 Firefox 从 57 版本开始便使用基于 Rust 编程语言开发的渲染引擎 Servo，Rust 自家人编写的渲染引擎，值得信赖😂。而 Chrome 的 Blink 引擎是用 C++ 写的。C++ 语言如同 C 语言，很容易因为内存使用方面的问题而导致安全漏洞（比如：缓冲区溢出、野指针 \u0026hellip;）。这个缺点是编程语言本身导致的。Rust 一方面可以达到与 C/C++ 相当的性能，另一方面又更加安全，避免了 C/C++ 在内存和多线程方面的弊端。\n另外，我觉得 Firefox 最大的一个〖杀手锏〗就是高度可定制化，你可以凭借自己的想象力把 Firefox 浏览器改造成自己想要的任意形态，而 Chrome 却只能限定在一个可控范围内进行扩展和定制，就是画个圈圈，你只能在这个圈内自由活动。如果你不太理解什么是改造成任意形态，我可以举个例子，比如我可以将 Firefox 的 about 界面 Logo 替换成任意图片：\nChrome 有这个可能吗？\n再比如我可以将 Firefox 的地址栏做成如下炫酷的特效，还可以将标签页的样式改造成如下的“花里胡哨”的样式：\nChrome 有这本事吗？\n现在你应该理解我的意思了吧，Chrome 在很多地方进行了限制，束缚了我们的手脚，让你的扩展只能在有限的范围内进行定制。\n如果你想掌握对浏览器绝对的控制权，喜欢折腾，Firefox 无疑是最好的选择。当然，如果你喜欢开箱即用，没有什么定制化的需求，那选择 Chrome 是极好的。但是，我还是要强调一句，即使你选择了 Chrome，为了避免〖恶龙〗的诞生，还是希望你能把 Firefox 作为备用浏览器。\n好了，废话就扯这么多，还是直接进入主题吧，本文将会手把手教大家如何任意定制全宇宙最强浏览器 Firefox。\nFirefox 浏览器的个性化大致有五种方式，一种是与其他浏览器一样，通过浏览器默认的选项和主题进行定制，不过能修改的程度有限；一种是通过扩展对功能进行拓展；还有两种是通过油猴脚本和 stylus 之类的扩展再结合自定义 CSS 来对网页样式进行自定义。这四种方式 Chrome 浏览器也可以做到，并没有什么特别之处，我也不打算重点介绍，放到后面再讲。\nFirefox 最顶级的个性化方式就是用户样式和用户脚本来定制。什么意思呢？\n用户样式可以理解为 stylus 这一类扩展的加强版，CSS 样式可修改的范围是整个浏览器的任何角落，并不局限于〖网页〗这个范围内。但用户样式只能修改已有的元素，不能创建新功能。 用户脚本可以理解为油猴脚本的加强版，脚本可修改的范围是整个浏览器的任何角落，并不局限于〖网页〗这个范围内。 结合用户样式与用户脚本，我们可以直接利用 CSS 进行界面样式的自定义，并使用一些受支持的 JavaScript 脚本实现 Firefox 界面上尚未实现的功能，以此来实现对 Firefox 的任意魔改。这些内容是使用 Firefox 的 userChrome.css、userContent.css 以及 userChrome.js 等来进行定义的。\n自定义用户样式 # Firefox 自 69 版本以后，为了更快的启动速度，默认不会去寻找定义样式的 userChrome.css 和 userContent.css，我们需要手动开启这一功能。在 Firefox 的地址栏访问 about:config，忽略警告，在接下来的界面搜索 toolkit.legacyUserProfileCustomizations.stylesheets，并将这一项目设置为 true，如下图：\n之后，我们找到 Firefox Profile 的根目录，我们需要在那里创建定义样式的 userChrome.css。在 Firefox 的地址栏访问 about:support，选择下方的 Profile Folder，点击 Open Folder。\n之后打开的文件夹即为 Firefox Profile 根目录。在这里，我们需要创建一个名叫 chrome 的文件夹，接下来的所有自定义样式都需要放入这一文件夹之中。\n\u0026ldquo;Chrome\u0026rdquo; refers to the user interface of the web browser, which is what Google Chrome was named after. —— Chrome 这一单词代指浏览器的用户界面，也是 Google Chrome 浏览器名称的由来。因此，这里的 chrome 与 Google Chrome 浏览器完全没有关系。\n之后我们就可以在 chrome 文件夹内自行创建 userChrome.css 和 userContent.css 这两个样式定义文件，在其中进行自定义即可。\n那么 userChrome.css 与 userContent.css 这两个文件有啥区别呢？\nuserChrome.css 是专门用来定制 Firefox〖自身的界面〗（比如 Firefox 自己的“地址栏、搜索栏、快捷菜单、滚动条 \u0026hellip;\u0026hellip;”） userContent.css 是专门用来定制 Firefox 浏览的网站的界面（如果你对我博客的某些界面效果不爽，就可以用它来定制）。说白了，userContent.css 可以实现和 stylus 这一类扩展同样的功能，唯一的区别在于 userContent.css 还可以定制 Firefox 内置页面和扩展页面的样式（比如内置的新标签页）。 例如，如果你想像文章开头截图那样将 about 界面的 Logo 替换成别的图片，只需在 userChrome.css 中添加这么一段 CSS 样式：\n@-moz-document url(\u0026#34;chrome://browser/content/aboutDialog.xhtml\u0026#34;) { /* change logo png, svg, even gif anims */ #leftBox { background-image: url(\u0026#34;https://images.icloudnative.io/uPic/20210505152049.png\u0026#34;) !important; background-position: left !important; background-repeat: no-repeat !important; font-family: Roboto, \u0026#34;LXGW WenKai\u0026#34;, sans-serif !important; } } 然后重启 Firefox 浏览器，即可看到 Logo 替换生效了。\n当然，如果所有的样式都要我们自己从零开始写，那也太劝退了，毕竟大多数人是不懂 CSS 的，有没有别人写好的样式可以直接拿来用呢？还是有很多的，比如：\nPhoton Australis : 模仿 Chrome 设计风格的 Firefox 主题，将 Firefox 标签页的样式打磨得和 Chrome 圆角标签页近乎一致。\nFlyingFox : 我认为这是最精美的 Firefox 主题，这也是我目前正在使用的主题。\nFirefoxCSS-Store : 从名字就能看出来，这是一个 Firefox userchrome 主题商店，包含了各种主题任你挑选。\nfirefox-csshacks : 这个仓库包含了各种特定的样式，其中 chrome 文件夹包含了 userChrome.css 的样式，content 文件夹包含了 userContent.css 的样式。\nFirefoxCSS on Reddit : 这是全球最大的 Firefox 样式分享社区，你可以在这里自由讨论、提问、分享自己的样式，或者拿走别人的样式。\n感兴趣的小伙伴可以自己下载体验一番。\n细心的小伙伴应该能发现这里有个问题，所有的样式必须要保存并重启浏览器之后才能看到它的效果，无法实时调试，这对于高级玩家来说是很不友好的，我们需要的是能够实时调试任意样式。这就需要用到自定义用户脚本了。\n自定义用户脚本 # 在定义样式的基础之上，我们还可以借助于 JavaScript 实现 Firefox 尚未实现的一些功能：比如前文提到的实时调试样式。\n在 Firefox 72+ 之后，用 JavaScript 添加附加功能的步骤稍微有些繁琐。可以参考 xiaoxiaoflood/firefox-scripts 这个仓库的方法，\n先下载压缩包 fx-folder.zip 进行解压，得到这么几个文件： $ tree fx-folder fx-folder ├── config.js └── defaults └── pref └── config-prefs.js 2 directories, 2 files macOS 用户接下来可以右键点击〖访达〗（左下角的笑脸图标），选择〖前往文件夹〗，输入路径地址：/Applications/Firefox.app/Contents/MacOS。然后将解压出来的文件全部拷贝到这个文件夹里。\nWindows 用户需要将解压出来的文件全部拷贝到 Firefox 的安装路径下（比如 C:\\Program Files\\Mozilla Firefox）。\n下载压缩包 utils_scripts_only.zip 进行解压，将解压出来的文件全部拷贝到前文所述的 chrome 文件夹中（例如，我的路径是 /Users/carson/Library/Application Support/Firefox/Profiles/pntdm1l9.default-release/chrome）。\n重启 Firefox 浏览器。\n现在你就可以在 chrome 文件夹根目录创建自定义脚本来实现任意功能了。\n使用自定义脚本管理自定义样式 # 例如，如果你想实时调试自定义样式，可以使用 xiaoxiaoflood/firefox-scripts 仓库里的 StyloaiX 脚本，它比 userChrome.css 和 userContent.css 更方便，因为它拥有一个强大的编辑器，还能即时预览、错误检查、代码自动补全，而且无需重启浏览器即可启用和禁用样式。你只需要下载压缩包 styloaix.zip，然后将解压出来的文件全部拷贝到 chrome 目录中：\nchrome ├── styloaix.uc.js └── utils └── styloaix ├── 16.png ├── 16w.png ├── autocomplete.js ├── edit.css ├── edit.js └── edit.xhtml ... 重启 Firefox 浏览器就可以看到浏览器的工具栏中多了一个扩展的图标，实际上这不是一个浏览器扩展，而是通过 JavaScript 实现的。\n现在我们就可以编写自定义样式并实时调试了，方法很简单，点击上述 StyloaiX 图标，然后依次选择 〖New Style〗\u0026ndash;\u0026gt; 〖Blank Style〗。\n然后就会打开一个编辑器的界面。\n然后就可以在里面调试样式了。比如我想对扩展界面进行自定义，就可以选择〖New Style〗\u0026ndash;\u0026gt; 〖For this page〗，打开编辑器后会自动帮你设置 CSS 样式的生效页面。\n往里面加入如下的 CSS 内容：\n@-moz-document url(\u0026#34;about:addons\u0026#34;) { /* Remove this if it causes horizontal scrolling problems */ @media (min-width:720px) { #main { max-width: unset !important; padding-right: 28px !important; } addon-list\u0026gt;section, recommended-addon-list { padding: 1em !important; display: grid !important; grid-template-areas: \u0026#34;hd hd\u0026#34;\u0026#34;cd cd\u0026#34; !important; grid-template-columns: 1fr 1fr !important; column-gap: 1em !important; } addon-card .card-contents { width: unset !important; white-space: initial !important; } .card-heading-image { max-width: calc(100% + 32px) !important; } section\u0026gt;h2 { grid-area: hd !important; } addon-card { padding-bottom: 0px !important; padding-top: 0px !important; grid-area: auto !important; } addon-card .addon-description { max-height: 3em !important; scrollbar-width: thin !important; color: white !important; text-align: inherit !important; } .stack.inline-options-stack { background-color: #17171E !important; color: white !important; font-size: 14px !important; border: none !important; } addon-card .addon-description { height: 3em !important; scrollbar-color: #1e90ff #000000 !important; scrollbar-width: thin !important; } } .addon-badge-recommended, .addon-badge-private-browsing-allowed { transform: scale(0.85) !important; margin-bottom: 0px !important; } #page-options panel-list { background-color: #17171E !important; font-size: 14px !important; border: none !important; color: white !important; } } 样式会立即生效，将扩展列表改为双栏显示。\n调试好了确认无误后，只需给该样式命名然后保存即可。\n如果你临时不想用这个样式了，可以点击 StyloaiX 图标，然后直接点击样式名，就会取消选中该样式，前面的图标会从〖打✅的圆〗变成〖空心圆〗。\nStyloaiX 的牛逼之处在于它可以渲染任何样式，不管是 userChrome 还是 userContent，甚至可以直接使用它来替代 stylus 等扩展。\n使用自定义脚本管理自定义脚本 # 好了，体会到了自定义用户脚本的强大之处后，我们来看看它还能实现什么神奇的功能，比如使用自定义脚本来管理自定义脚本？？？哈哈哈\n什么意思呢？默认情况下自定义脚本放到 chrome 目录重启后就会生效，要想让它不生效，只能删了它，或者重命名后缀，这也太不优雅了。我们可以想办法像 StyloaiX 一样随时启用或禁用自定义脚本，不需要删除脚本或者重命名后缀。\n还是使用 xiaoxiaoflood/firefox-scripts 这个仓库提供的方法，下载脚本 rebuild_userChrome.uc.js，然后将其拷贝到 chrome 文件夹中，重启浏览器之后就可以看到浏览器的工具栏中又多了一个扩展的图标。\n每一个脚本都有 6 种不同的操作方法，我就不解释了，大家应该都能看懂。\n⚠️注意：虽然使用该方法可以随时〖启用 / 禁用〗自定义脚本，但是某些脚本受浏览器的限制必须要重启浏览器才能生效，具体需要自己测试。\n使用自定义脚本管理浏览器扩展 # 除了上面的玩法之外，我们还可以使用自定义脚本管理浏览器的扩展，虽然某些浏览器扩展也可以实现这个功能，但是使用自定义脚本更省资源，也更高效。这就需要用到另外一个大佬的仓库 aminomancer/uc.css.js，直接下载脚本 extensionOptionsPanel.uc.js，然后将其拷贝到 chrome 文件夹的根目录，重启浏览器之后就可以看到浏览器的工具栏中又多了一个扩展的图标。\n现在你可以在同一个界面中管理所有的扩展，包括启用、禁用、设置、卸载等等。\n关于自定义脚本的内容我就讲这么多，玩法太多，我就不一一列举了，这篇文章只是提供一个方向，感兴趣的玩家可以自己去探索。除了前面提到的两个仓库之外，最后我再提供一些别人写好的脚本资源：\nFirefoxTaskMonitor : 实时显示每个标签页和每个扩展的 CPU 和内存使用状况。 Aris-t2/CustomJSforFx FirefoxCSS on Reddit : 前面提到过，这里除了可以分享样式，还可以分享脚本。 Firefox 扩展、插件、脚本和样式 : Firefox 中文社区的某个版块。 如果大家对我的 Firefox 样式和脚本比较感兴趣，可以扫码关注公众号：\n后台发送暗号：firefox，即可获取我的所有样式和脚本。下载压缩包之后将解压出来的文件全部拷贝到 chrome 文件夹中即可，如果说有重复，则覆盖它。\n更多自定义选项 # 配置选项 # 〖配置选项〗也叫〖首选项〗，即 Preferences。通俗地说就是：Firefox 提供了一大堆可供用户定制的参数。通过修改这些参数，可以对 Firefox 进行全方位定制。通常我们在浏览器地址栏输入 about:config 然后敲回车，就可以看到所有的配置选项。\n例如，如果想改变滚动体的样式，可以打开 about:config，输入 widget.non-native-theme.scrollbar.style，默认值为 0，也就是自动匹配当前系统。我们可以指定某个具体的样式，推荐用 1 和 5，这两个最好看。\n0 ：平台默认滚动条样式 1 ：macOS 滚动条样式 2 ：GTK 滚动条样式 3 ：Android 滚动条样式 4 ：Windows 10 滚动条样式 5 ：Windows 11 滚动条样式 配置选项的定制方法本文就不作具体说明，具体可参考这篇文章： 扫盲 Firefox 定制——从“user.js”到“omni.ja”。\nuser.js 的完整参数可参考 arkenfox/user.js 这个仓库。\n下面再介绍两个对浏览器进行个性化的方法，不过不是 Firefox 专属的功能，Chrome 浏览器也是通用的。\n油猴脚本 # 油猴脚本，正式的叫法是用户脚本（user script）。之所以叫做〖油猴〗，是因为第一个制作这个浏览器扩展的作者 Aaron Boodman 起名叫做 Greasymonkey，中文直译就是〖油腻的猴子〗；后面其他脚本开发的时候，基本都在沿用 Greasymonkey 的一些基本规范，这些脚本也就统称为〖油猴脚本〗了。\n油猴脚本与前文所述的自定义用户脚本不同，它只能对网站的功能进行扩展，无法对浏览器本身动刀。\n目前支持油猴脚本的浏览器扩展有 Greasemonkey、 Tampermonkey 和 Violentmonkey，个人推荐使用 Violentmonkey，也就是暴力猴。安装好扩展之后，可以到 Greasyfork 这个网站中去安装自己感兴趣的脚本。例如，很多人看到我的屏幕后都会问我是怎么上 Google 的，问的人太多了我就很烦，所以当我们使用 这个脚本把 Google 的 Logo 换成百度，他们就不会问那么多问题了！\n自定义网页样式 # 如果你不喜欢某些网站的样式，也可以自己动手给网站自定义样式，原理还是通过 CSS 来实现。目前支持给网站自定义样式的扩展有 Stylish、 xStyle 和 Stylus，个人推荐使用 Stylus，其他两款扩展都停止开发了，不推荐使用。\n安装好扩展之后，可以到 userstyles.org 这个网站中去安装自己感兴趣的样式。例如，我可以使用 这个样式将 GitHub 的 Logo 改成 PornHub 的风格。\n如果 userstyles.org 中提供的样式不能满足你的需求，你也可以自己编写样式，一切皆有可能。\n总结 # 本文给大家介绍了 Firefox 浏览器的优势，并使用自定义样式和自定义脚本来对 Firefox 浏览器进行定制，制作属于我们自己的专属浏览器。总的来说，Firefox 就是一张纸，它什么都没有，但每个人都可以培养只属于自己的浏览器。Chrome 都是千篇一律，但 FireFox 各有各的不同。\n参考资料 # 用下面这些方法，为自己高度定制一个 Firefox 浏览器 扫盲 Firefox 定制——从“user.js”到“omni.ja” 油猴使用指南 01：传说中的「油猴」与用户脚本 ","date":"2022年4月30日","externalUrl":null,"permalink":"/posts/customize-firefox/","section":"博客","summary":"Firefox 和 Chrome 分别是当今世界最流行的浏览器之一，虽然这两款浏览器都有","title":"Firefox 浏览器个性化定制指南","type":"posts"},{"content":"","date":"2022年4月17日","externalUrl":null,"permalink":"/tags/github/","section":"标签","summary":"","title":"GitHub","type":"tags"},{"content":"","date":"2022年4月17日","externalUrl":null,"permalink":"/tags/httpie/","section":"标签","summary":"","title":"HTTPie","type":"tags"},{"content":" 原文链接： How we lost 54k GitHub stars\n出大事了，一个非常知名的开源项目 Star 数量一夜之间归零了🤣\n这个项目就是 HTTPie。\nHTTPie 官方专门写了一篇博客反省这次的操作，介绍了本次事件的来龙去脉。原文翻译如下：\n本人于 2012 年在 GitHub 上第一次提交 HTTPie 项目代码，如今已过去了 10 个年头。\nHTTPie 是一款开源的命令行 HTTP 客户端，没有借助第三方库，从头开始构建，旨在使终端工具与 API 的交互更加人性化。\n获得 5.4 万 Star # 2012 年 2 月 25 日，我当时在哥本哈根，那天下着大雨，我在 GitHub 上公开发布了 HTTPie 的第一个版本。\n自从几年前我加入 GitHub 的会员后，我就一直是它的粉丝（经常穿八爪鱼 logo 的 T 恤到处晃荡的那种）。那一天 GitHub 的 about 页面大张旗鼓地宣称他们获得了 0.00 美元的风险投资基金，并表示他们的旧金山办公室里准备了很多美味的啤酒来欢庆此事。\n因此，当我意识到可能会有很多开发者像我一样需要经常和 API 或者 Web 服务器进行交互时，把 HTTPie 的代码开源在 GitHub 上是一个明智的选择。\n当 HTTPie 第一次成为 Hacker News 热门链接时，我的心情无比激动，那一天至今还历历在目。后来我又见证了 GitHub 社区的成立。随着我们不断对项目进行优化，吸引到的用户越来越多，以至于 HTTPie 变成了 GitHub 上最受欢迎的 API 工具，鼎盛时收获了 54k 的 Star 和 1000+ 的 Watcher。\n你想想，GitHub 上总共有 2.89 亿个公共仓库，要想在其中闯出一片天地是多么地艰难。可 HTTPie 还是凭借自身的实力变成了 GitHub 上最受欢迎的前 80 个公共仓库之一，处于 99.99997203 百分位。简而言之，看到这个不起眼的命令行工具吸引了如此庞大的用户群体，真是难以置信，而 GitHub 肯定在这个过程中发挥了举足轻重的作用。\n虽然我们从 GitHub 的“通过同性交友来协作编程”功能中受益良多，但 GitHub 也从 HTTPie 项目中捞到了很多好处，过去十年可能有数百万的开发者访问了我们的 GitHub 页面，这对于 GitHub（微软）这样一家非常关心开源和社区的公司来说，无异于如虎添翼。我们与 GitHub 是互惠互助的关系。\n痛失 5.4 万 Star # 然而意想不到的悲剧发生了，如果你之前是该项目的 Watcher，很遗憾，从几周前开始您就不是了。如果您之前给该项目点过小星星（Star），现在应该也失效了。万万没想到啊，你猜发生了什么？\n发生了什么？ # 由于一连串不幸的操作，我一不小心就把项目的仓库设为私有仓库，这个骚操作让 GitHub 连带删除了我们花 10 年时间建立起来的社区！心碎至极 💔\n什么后果？ # 这意味着什么呢？如果您是下游的维护者（maintainer）或者以前 watch 过 HTTPie 项目以及时获取通知的人，现在需要重新 Watch 该项目。如果您之前给该项目点过小星星（Star），现在也需要重新关注一遍。\n我怎么就把仓库设为私有了？？ # 说句不好听的，GitHub 有一个让人无法理解的特性，你只要将公共仓库设为私有，该仓库的 Watcher 和 Star 就会被永久删除。当然重点不在这里，我肯定是知道 GitHub 的这个特性的，我也并没有打算把该仓库设为私有，但是悲剧还是发生了，为啥呢？\n事情的经过是这样的，前段时间 GitHub 不是推出了“个人主页”功能嘛，要想激活此功能，只需要新建一个与自己 ID 同名的仓库，这个新仓库的 README.md 的内容便会显示在你的个人首页。我也创建了一个这样的仓库 jakubroztocil/jakubroztocil，然后我做了一个骚操作：把这个仓库从公共仓库设为私有仓库。\n这个操作看起来也没有什么问题，问题还在后面。\n然后我的大脑就开始意淫，想着将 httpie 这个组织的主页仓库也设置成私有仓库。我的 ID 是 jakubroztocil，那我的个人主页仓库就是 jakubroztocil/jakubroztocil；我创建的这个组织是 httpie，那这个组织的首页仓库就是 httpie/httpie，我脑中这么思量着，手也没停下，就将 httpie/httpie 设为了私有仓库。。。我被自己蠢哭了😭\n我这么意淫是有原因的，当涉及到配置文件和仓库时，GitHub 的概念模型会将用户和组织视为非常相似的实体。我已经在个人仓库中做过这种操作，然后我带着这种认知想要在组织中也这么操作，于是我的大脑就开启了自动驾驶模式。。。\n现在我知道了，要想自定义 GitHub 组织的主页，需要新建的仓库名称是 name/.github，而不是 name/name。对于 httpie 组织而言，这个仓库是 httpie/.github，而不是 httpie/httpie 🤣\n有没有最终确认的选项？ # 确实有一个最终确认的选项。\n这个选项就是为了防止我这样的用户脑子一热做傻事而设计的。它会警告你 “You will permanently lose all stars and watchers of this repository.”，即你将失去该仓库的所有 Watcher 和 Star。\n问题在于，无论是没有任何 Watcher 和 Star 的仓库，还是拥有 55k Star 和 Watcher 的仓库，这个最终确认的提示框都是一模一样的。\n这就好比什么呢？打个比方，假设你要拆一座房子，然后出现一个提示框警告你：你将要拆掉这座房子，如果房子有人，他们都会死的。问题来了，如果你混淆了地址，把这个“里面有人”的房子当成你另外一个地方的空房子，就不会重视这个提示，心想：反正里面没人，拆就拆呗。\n下面的两个提示中，你能看出哪一个是提示你即将删除一个活跃了 10 年的社区吗？\n完全看不出来呀。我觉得 GitHub 的这个提示框应该设计地更人性化一点，如果你警告我说：你即将干掉 55000 个人，那我肯定会被吓到，并停止我的愚蠢操作。\n追悔莫及 # 我做完这个骚操作后回到组织的首页仍然能看到一个空的 README（因为没有将 httpie/.github 这个仓库设为私有，且该仓库是空的），而且 HTTPie 仓库也不见了，你可以想象到我当时有多懵逼。过了好半天我才真正意识到发生了什么，于是重新回到 HTTPie 仓库将其设置为公共仓库，但 GitHub 在接下来的半个小时内都不允许我这样做。\n原因是 GitHub 正在“帮助”我级联删除该仓库的 Star 和 Watcher，在 Star 和 Watcher 归零之前，我都无法停止这个操作。\n我当时那个后悔呀，急忙给 GitHub 的支持团队写邮件，并不停刷新页面等待 Star 和 Watcher 数量归零，然后才能再次将其设为公共仓库。\n为什么 GitHub 不愿意帮我？ # 我敢肯定，GitHub 肯定是有备份的。这个备份可以挽回因为不小心将仓库设为私有的损失。GitHub 团队曾经就不小心把 GitHub 桌面应用的仓库设为私有仓库，然后在短短几个小时内便恢复如初。以下是 GitHub 的 CEO 对此次乌龙事件的解释。\n然而对于我的项目他们却拒绝了这么做，理由是会有一定的副作用和资源成本。我们甚至向 GitHub 提出对任何所需资源提供经济补偿，但他们还是拒绝了。\n所以，很遗憾，GitHub 明确表示：虽然他们可以将误设为私有仓库的项目恢复如初，但仅限于 GitHub 自己的项目。其他的项目最多会发个 这样的推文来号召大家重新关注该项目。\n吸取到的教训 # 这次意外让我吸取了很多教训，在此分享给大家，希望大家以后不要遇到我这样的情况。\n教训一：UI/UX 设计 # 要把用户看成“白痴”，以一种不需要让用户思考的方式来设计确认提示框。也就是说，当用户要毁掉某样东西时，不要用抽象的词语来描述这种潜在的情况，这会让用户自然而然地翻译成自己的理解。特别是当删除操作会产生很多“级联删除”的副作用时。例如，在 HTTPie for Desktop 中，我们是这样处理的。\n当然，提示框要能清晰地表达出该操作产生的副作用的严重程度。如果完全没有副作用，就不要写一大堆有的没的，提示保持简洁就好。否则就会浪费用户有限的注意力，从而降低用户的敏感程度。\n教训二：数据库设计 # 数据库尽量使用软删除（soft delete），也就是使用标记将数据标为不可用，而不从数据库删除数据本身。只要是人都会犯错误，如果实在要硬删除，那就想办法延迟删除操作的时间，给用户一点后悔的时间。\n教训三：不要过度信任 GitHub # 这个失误是我们自己导致的，GitHub 明确表示他们在法律上没有义务帮助我们，我们十年来的互惠互利关系的基调是由 GitHub 的服务条款确定的，如果你还有其他的奢望，那就是痴人说梦。毕竟 GitHub 曾经采取过有争议的行动，违背了开源和社区的精神，公众愤怒了之后才不得不 将其恢复。而微软（买下了 GitHub）虽然最近在拥抱开源，但 它的声誉总是不太好，不得不让人担心。\n更多期望 # 我们希望 GitHub（微软） 能改变他们的强硬态度，利用他们所有的数据库和技术手段来恢复我们这个项目的社区。同时我也期望他们能改进用户界面和数据库的设计，以防止这种悲剧发生在其他团队身上。作为读者，您也可以通过分享转载这篇文章以及重新 Star 和 Watch 我们的仓库来帮助我们。\n至于我自己嘛，我可能要面壁思过一段时间了，并且不会再穿八爪鱼 Logo 的 T 恤。\n后记 # 尽管我们的 GitHub Star 数量一夜回到了解放前，但 HTTPie 从未像现在这样做得更好。它最初只是别的项目的子项目，最近却为此成立了一家公司，我们的团队正在渐渐将 HTTPie 发展成一个 API 开发平台，这一点与用户对我们的期待完全吻合。目前 HTTPie for Web \u0026amp; Desktop 的私人测试版已经收到了非常积极的反馈，我们已经迫不及待想在接下来的几周内推出正式版了。\n如果你想获取该项目的最新信息，欢迎加入我们的 Discord 社区或者在 Twitter 上关注 @httpie。\n截止本文完稿，HTTPie 已重新获得了 11k 的 Star 数量。\n","date":"2022年4月17日","externalUrl":null,"permalink":"/posts/how-we-lost-54k-github-stars/","section":"博客","summary":"原文链接： How we lost 54k GitHub stars 出大事了，一个非常知名的开源项目 Star 数量","title":"HTTPie 是如何丢失 5.4 万 Star 的","type":"posts"},{"content":"","date":"2022年4月10日","externalUrl":null,"permalink":"/tags/anki/","section":"标签","summary":"","title":"Anki","type":"tags"},{"content":" Anki 介绍 # Anki是一款基于间隔重复（Spaced Repetition）原理的学习软件，想象一下，你的大脑就像是一个需要定期维护的精密仪器。间隔重复就好比是一种精准的维护计划，它通过在最佳时刻复习信息，来确保知识在你的脑海中牢固地扎根。\nAnki 软件使用这个原理，帮助用户通过创建“卡片”来学习和记忆信息。所谓的卡片，专业说法叫 Flash Card（抽认卡或闪卡），是一小块纸片，分为正反两面，将问题和提示写在一面，将答案写在另一面。使用方法就是先看正面的问题与提示，在脑中回想答案，然后翻出反面进行对照验证。如果你很容易记住某张卡片的内容，Anki会增加下次复习这张卡片的时间间隔；反之，如果你觉得某张卡片比较难记，Anki会缩短这张卡片的复习间隔。\n这种方法特别适用于需要记忆大量信息的领域，如语言学习、医学、法律等。\n给大家看下我制作的闪卡：\n每张卡片只有一个英文单词，与之配套的是该单词的音标、发音、图片、英文解释、例句。所有的版块都是英文，绝对不要出现中文！ 卡片的核心是图片和例句，通过图片可以猜到这个单词大概是什么意思，通过例句可以验证自己对单词意思的猜测是否正确，如果还不放心，可以看下英文解释，这一套流程下来绝对可以正确理解单词的意思，完全不需要中文的干涉，这才是学习英文单词最完美的方式。\n即便如此，大家在熟悉单词的过程中可能还会有一个误区，比如上面这个单词，你在学习的过程中可能会忍不住去想这个单词在中文里究竟是什么意思，甚至可能会在心里默念它的中文意思，即使你看了图片和英文解释，你心里可能还会忍不住去想：哦，这是转瞬即逝的意思。建议大家最好不要这么做，而是直接看这张图片，然后用心去体会：哦，大概就是这么一种感觉，对对对。你能 get 到这个单词所表达的那种感觉就行了，不要再去思考如何用中文来描述它，那样反而吃力不讨好。\n下面言归正传，相信有很多小伙伴和我一样在使用 Anki 来学习英文单词或者其他的知识，但是 Anki 的同步服务器在国外，还是一个个人项目，带宽很小，同步速度很慢，如果我们想在多个客户端之间同步学习进度和新增的知识点，那将非常痛苦。\n为了解决这个问题，我们需要部署一个自定义的同步服务器，然后让客户端去使用这个同步服务器。\nAnki 同步服务器部署 # 自从 2023 年 2 月份，Anki 发布了 PC 端 2.1.57 版本以后，Anki 的 PC 端，安卓端，iOS 端用户都可以自定义同步服务器了，并且不再需要安装插件。从此 Anki 小伙伴再也不用担心 Anki 同步的问题了，困扰 Anki 用户多年的同步问题终于得到彻底解决。\n自 PC 端 2.1.57 版本以后，Anki 官方推出了镶嵌在 Anki 客户端的同步服务端和通过 Python 安装的同步服务端。\n我选择使用镶嵌在 Anki 客户端中的同步服务端，因为它是用 Rust 写的啊，人生苦短，我不用 Python。\n但是官方并没有提供 Docker 镜像，于是我选择自己构建 Docker 镜像，项目地址：\nhttps://github.com/yangchuansheng/anki-sync-server 部署方法就非常简单了，你可以选择使用 Docker 部署，也可以直接使用 Sealos 应用模板一键部署，不用操心域名和证书等各种乱七八糟的事情，有手就行。\n直接点击下面的按钮跳转到 Sealos 的应用模板部署界面：\n如果您是第一次打开 Sealos，需要先注册登录账号。\n然后点击「部署应用」按钮开始部署。部署完成后，点击「详情」进入应用的详情页面。\n这里可以看到实例的运行状态，一定要等到状态是 running 才算是部署成功。如果一段时间以后状态还不是 running，可以点击「详情」查看故障原因：\n部署成功后，可以看到应用的运行情况，包括 CPU 占用、内存占用等。外网地址就是同步服务器的公网域名。\n客户端设置 # 桌面端 # 桌面客户端（macOS/Windows/Linux）配置方法如下：\n先打开「首选项」\n点击「网络」，往下看，可以看到标有 self-hosted sync server(自定义同步服务器) 的方框，在里面填写您的服务端的地址：\n重启 Anki，然后点击「同步」：\n这时候会弹出一个输入框让你输入用户名和密码，你需要将你之前设置的用户名和密码输入进去：\n点击确认后，就会开始同步了。\n安卓端 # 安卓端也是直接配置即可，我的 AnkiDroid 版本是 2.15.6。你可以通过「设置 -\u0026gt; 高级设置 -\u0026gt; 自定义同步服务器」找到配置页面。\n再填写用户名和密码：\n设置 -\u0026gt; 常用设置 -\u0026gt; AnkiWeb账户\n这样就算配置完成了，所有的牌组都同步过来了。\n官方的版本实在是太老了，如果你想使用更激进的社区版本，可以到这个页面下载最新的 Beta 版： https://github.com/ankidroid/Anki-Android/releases 建议下载 arm64-v8a 版本。\n安装完成后，可以通过「设置 -\u0026gt; 同步 -\u0026gt; 自定义同步服务器」找到配置页面：\n再填写用户名和密码：\n设置 -\u0026gt; 同步 -\u0026gt; AnkiWeb账户\niOS 端 # AnkiMobile 也已经支持和自建的同步服务器同步了。至少对于版本 Ankimobile 2.0.90(20090.2) 来说，似乎是可行的，这是一位 iOS 系统用户 在 Anki 论坛报告的。\n如果设置完成后发现不能同步可以参考下面的内容再试一次：\nIf you\u0026rsquo;re using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS settings, locate Anki near the bottom, and toggle \u0026ldquo;Allow Anki to access local network\u0026rdquo; off and then on again.\n上面的内容摘自 ANki tutorial\n题外话 # 大家如果对我的卡片模板比较感兴趣，可以扫码关注公众号：\n后台聊天框发送暗号 anki，即可获取我的卡片+模板。\n","date":"2022年4月10日","externalUrl":null,"permalink":"/posts/anki-sync-server/","section":"博客","summary":"Anki 介绍 # Anki是一款基于间隔重复（Spaced Repeti","title":"Anki 自定义同步服务器部署与使用","type":"posts"},{"content":"","date":"2022年4月9日","externalUrl":null,"permalink":"/tags/podcast/","section":"标签","summary":"","title":"Podcast","type":"tags"},{"content":" 虽然 PHP 是世界上最好的语言，但身为程序员、YAML 工程师和系统重启工程师，你必须学会的语言只有英语。网上铺天盖地都是英文资料和教程，就连 Youtube 上面的视频教程绝大多数都是英文，所以我们要想在互联网的世界里自由翱翔，就必须以最快的速度学会“英语”这门语言。\n学习英语最有效的方法无非就是找一些固定的优质训练材料不停地精听，但是这种方法难免有些枯燥，容易让人因为无聊而打退堂鼓。为了避免这种枯燥和无聊打击自己的积极性，我们可以选择一些自己感兴趣的泛听材料。作为云原生玩家，最感兴趣的当然就是云原生领域的听力资源啦，而 Podcast 恰恰就是我们需要的。本文就来为大家推荐一系列云原生领域的优质 Podcast。\nCommitting to Cloud Native # 该播客由 Curiefense 团队出品，主要探讨开源和云原生的融合。嘉宾包括 @CloudNativeFdn 项目的成员，在谷歌、亚马逊和 @NASA 等地方从事大规模项目的维护者，以及为云原生生态系统中的优秀项目作出贡献的社区成员。\nDevOps and Docker Talk # 这个播客包含了作者每周的 YouTube 直播节目的采访问答。主题涵盖了 Kubernetes、云原生开发、DevOps、GitOps、DevSecOps 以及整个软件生命周期供应链。\nKubernetes Podcast from Google # 这个播客是 Google 公司创建的，每周更新一次，访谈对象基本上都是 Google 的员工和 Cloud Native 社区的重量级嘉宾。\nKubernetes Bytes # 这个播客主要报道云原生数据管理世界的最新信息。主持人 Ryan Wallner 和 Bhavin Shah 来自马萨诸塞州的波士顿，在云原生技术方面有着丰富的经验。他们会分享对近期云原生动态的见解，并与业内专家讨论当今云原生生态系统中管理大量数据的经验和挑战。\nThe Kubelist Podcast # 该播客通过采访负责沙箱项目、孵化项目和毕业项目的开发者和项目经理，探索不断发展的 Kubernetes、SIGS 和 CNCF 的生态系统。\nCloud Native Rejekts Podcast # 该播客专注于这场席卷全球的云原生革命背后的那些打破常规的开发者。这些人都是\u0026quot;与众不同\u0026quot;的人\u0026ndash;他们对现状进行改进，并敢于冒险使用改进的产品。用史蒂夫-乔布斯的话说。\u0026ldquo;虽然有些人认为他们是疯子，但我们认为他们是天才，因为只有那些疯狂到自认为可以改变世界的人，才能真正改变世界。\u0026rdquo;\nShip it! DevOps, Infra, Cloud Native # 该播客主要讨论 ops、基础设施、云原生以及这些领域背后的那些缔造者和推动者。\nCloud Native Startup # Cloud Native Startup 探讨了创业公司创始人的故事，他们从技术大拿变成了创业公司的首席执行官和首席技术官，制造出使现代软件开发更快、更安全、更可靠的产品。\nCloud Engineering - Software Engineering Daily # Software Engineering Daily 主要介绍软件工程领域的最新资讯，Cloud Engineering 是它的一个子播客，限定在云技术领域。\nCloud Security Podcast # 这个播客主要关注公有云的安全问题，案例都来自于真实的团队，不绑定公有云厂商。\nThe Art of Modern Ops # 专注于现代云基础设施，由 Weaveworks 首席技术官 Cornelia Davis 和《云原生模式》一书的作者主持，通过对有经验的团队进行采访，来探讨数字化转型的使用案例。\nEnterpriseReady # 将对软件开发业内专家和企业创始人进行深入采访，并打破行业之间的壁垒，使用通俗易懂的语言向大家分享前辈们在构建世界上最好的企业软件过程中获得的经验教训。\n","date":"2022年4月9日","externalUrl":null,"permalink":"/posts/cloud-native-podcasts/","section":"博客","summary":"虽然 PHP 是世界上最好的语言，但身为程序员、YAML 工程师和系统","title":"云原生领域 Podcast 推荐大全","type":"posts"},{"content":" 上篇文章介绍了如何使用 Headscale 替代 Tailscale 官方的控制服务器，并接入各个平台的客户端。本文将会介绍如何让 Tailscale 使用自定义的 DERP Servers。可能很多人都不知道 DERP 是个啥玩意儿，没关系，我先从中继服务器开始讲起。\nSTUN 是什么 # Tailscale 的终极目标是让两台处于网络上的任何位置的机器建立点对点连接（直连），但现实世界是复杂的，大部份情况下机器都位于 NAT 和防火墙后面，这时候就需要通过打洞来实现直连，也就是 NAT 穿透。\nNAT 按照 NAT 映射行为和有状态防火墙行为可以分为多种类型，但对于 NAT 穿透来说根本不需要关心这么多类型，只需要看 NAT 或者有状态防火墙是否会严格检查目标 Endpoint，根据这个因素，可以将 NAT 分为 Easy NAT 和 Hard NAT。\nEasy NAT 及其变种称为 “Endpoint-Independent Mapping” (EIM，终点无关的映射)\n这里的 Endpoint 指的是目标 Endpoint，也就是说，有状态防火墙只要看到有客户端自己发起的出向包，就会允许相应的入向包进入，不管这个入向包是谁发进来的都可以。\nhard NAT 以及变种称为 “Endpoint-Dependent Mapping”（EDM，终点相关的映射）\n这种 NAT 会针对每个目标 Endpoint 来生成一条相应的映射关系。 在这样的设备上，如果客户端向某个目标 Endpoint 发起了出向包，假设客户端的公网 IP 是 2.2.2.2，那么有状态防火墙就会打开一个端口，假设是 4242。那么只有来自该目标 Endpoint 的入向包才允许通过 2.2.2.2:4242，其他客户端一律不允许。这种 NAT 更加严格，所以叫 Hard NAT。\n对于 Easy NAT，我们只需要提供一个第三方的服务，它能够告诉客户端“它看到的客户端的公网 ip:port 是什么”，然后将这个信息以某种方式告诉通信对端（peer），后者就知道该和哪个地址建连了！这种服务就叫 STUN (Session Traversal Utilities for NAT，NAT会话穿越应用程序)。它的工作流程如下图所示：\n笔记本向 STUN 服务器发送一个请求：“从你的角度看，我的地址什么？” STUN 服务器返回一个响应：“我看到你的 UDP 包是从这个地址来的：ip:port”。 中继是什么 # 对于 Hard NAT 来说，STUN 就不好使了，即使 STUN 拿到了客户端的公网 ip:port 告诉通信对端也于事无补，因为防火墙是和 STUN 通信才打开的缺口，这个缺口只允许 STUN 的入向包进入，其他通信对端知道了这个缺口也进不来。通常企业级 NAT 都属于 Hard NAT。\n这种情况下打洞是不可能了，但也不能就此放弃，可以选择一种折衷的方式：创建一个中继服务器（relay server），客户端与中继服务器进行通信，中继服务器再将包中继（relay）给通信对端。\n至于中继的性能，那要看具体情况了：\n如果能直连，那显然没必要用中继方式； 但如果无法直连，而中继路径又非常接近双方直连的真实路径，并且带宽足够大，那中继方式并不会明显降低通信质量。延迟肯定会增加一点，带宽会占用一些，但相比完全连接不上，还是可以接受的。 事实上对于大部分网络而言，Tailscale 都可以通过各种黑科技打洞成功，只有极少数情况下才会选择中继，中继只是一种 fallback 机制。\n中继协议简介 # 中继协议有多种实现方式。\nTURN # TURN 即 Traversal Using Relays around NAT，这是一种经典的中继实现方式，核心理念是：\n用户（人）先去公网上的 TURN 服务器认证，成功后后者会告诉你：“我已经为你分配了 ip:port，接下来将为你中继流量”， 然后将这个 ip:port 地址告诉对方，让它去连接这个地址，接下去就是非常简单的客户端/服务器通信模型了。 与 STUN 不同，这种协议没有真正的交互性，不是很好用，因此 Tailscale 并没有采用 TURN 作为中继协议。\nDERP # DERP 即 Detoured Encrypted Routing Protocol，这是 Tailscale 自研的一个协议：\n它是一个通用目的包中继协议，运行在 HTTP 之上，而大部分网络都是允许 HTTP 通信的。 它根据目的公钥（destination’s public key）来中继加密的流量（encrypted payloads）。 Tailscale 使用的算法很有趣，所有客户端之间的连接都是先选择 DERP 模式（中继模式），这意味着连接立即就能建立（优先级最低但 100% 能成功的模式），用户不用任何等待。然后开始并行地进行路径发现，通常几秒钟之后，我们就能发现一条更优路径，然后将现有连接透明升级（upgrade）过去，变成点对点连接（直连）。\n因此，DERP 既是 Tailscale 在 NAT 穿透失败时的保底通信方式（此时的角色与 TURN 类似），也是在其他一些场景下帮助我们完成 NAT 穿透的旁路信道。 换句话说，它既是我们的保底方式，也是有更好的穿透链路时，帮助我们进行连接升级（upgrade to a peer-to-peer connection）的基础设施。\n自建私有 DERP server # Tailscale 的私钥只会保存在当前节点，因此 DERP server 无法解密流量，它只能和互联网上的其他路由器一样，呆呆地将加密的流量从一个节点转发到另一个节点，只不过 DERP 使用了一个稍微高级一点的协议来防止滥用。\nTailscale 开源了 DERP 服务器的代码，如果你感兴趣，可以阅读 DERP 的源代码。\nTailscale 官方内置了很多 DERP 服务器，分步在全球各地，惟独不包含中国大陆，原因你懂得。这就导致了一旦流量通过 DERP 服务器进行中继，延时就会非常高。而且官方提供的 DERP 服务器是万人骑，存在安全隐患。\n为了实现低延迟、高安全性，我们可以参考 Tailscale 官方文档自建私有的 DERP 服务器。有两种部署模式，一种是基于域名，另外一种不需要域名，可以直接使用 IP，不过需要一点黑科技。我们先来看最简单的使用域名的方案。\n使用域名 # 这种方案需要满足以下几个条件：\n要有自己的域名，并且申请了 SSL 证书 需要准备一台或多台云主机 如果服务器在国内，域名需要备案 如果服务器在国外，则不需要备案 如果以上条件都俱备，就可以按照下面的步骤开始部署了。\n推荐直接使用 Docker 来部署，我已经构建好了 Docker 镜像，直接部署就可以了：\n🐳 → docker run --restart always \\ --name derper -p 12345:12345 -p 3478:3478/udp \\ -v /root/.acme.sh/xxxx/:/app/certs \\ -e DERP_CERT_MODE=manual \\ -e DERP_ADDR=:12345 \\ -e DERP_DOMAIN=xxxx \\ -d ghcr.io/yangchuansheng/derper:latest 有几点需要注意：\n能用 443 端口尽量用 443 端口，实在不行再用别的端口； 默认情况下也会开启 STUN 服务，UDP 端口是 3478； 防火墙需要放行端口 12345 和 3478； 准备好 SSL 证书； 域名部分我打了码，请换成你自己的域名。 关于证书部分需要重点说明：假设你的域名是 xxx.com，那么证书的名称必须是 xxx.com.crt，一个字符都不能错！同理，私钥名称必须是 xxx.com.key，一个字符都不能错！\n查看容器日志：\n🐳 → docker logs -f derper 2022/03/26 11:36:28 no config path specified; using /var/lib/derper/derper.key 2022/03/26 11:36:28 derper: serving on :12345 with TLS 2022/03/26 11:36:28 running STUN server on [::]:3478 目前 derper 运行一段时间就会崩溃，暂时还没有更好的解决方案，只能通过定时重启来解决，比如通过 crontab 来设置每两小时重启一次容器：\n0 */2 * * * docker restart derper \u0026amp;\u0026gt; /dev/null 具体可参考这个 issue： Derper TLS handshake error: remote error: tls: internal error\n部署好 derper 之后，就可以修改 Headscale 的配置来使用自定义的 DERP 服务器了。Headscale 可以通过两种形式的配置来使用自定义 DERP：\n一种是在线 URL，格式是 JSON，与 Tailscale 官方控制服务器使用的格式和语法相同。 另一种是本地文件，格式是 YAML。 我们可以直接使用本地的 YAML 配置文件，内容如下：\n# /etc/headscale/derp.yaml regions: 900: regionid: 900 regioncode: thk regionname: Tencent Hongkong nodes: - name: 900a regionid: 900 hostname: xxxx ipv4: xxxx stunport: 3478 stunonly: false derpport: 12345 - name: 900b regionid: 900 hostname: xxxx ipv4: xxxx stunport: 3478 stunonly: false derpport: 12345 901: regionid: 901 regioncode: hs regionname: Huawei Shanghai nodes: - name: 901a regionid: 901 hostname: xxxx ipv4: xxxx stunport: 3478 stunonly: false derpport: 12345 配置说明：\nregions 是 YAML 中的对象，下面的每一个对象表示一个可用区，每个可用区里面可设置多个 DERP 节点，即 nodes。 每个可用区的 regionid 不能重复。 每个 node 的 name 不能重复。 regionname 一般用来描述可用区，regioncode 一般设置成可用区的缩写。 ipv4 字段不是必须的，如果你的域名可以通过公网解析到你的 DERP 服务器地址，这里可以不填。如果你使用了一个二级域名，而这个域名你并没有在公共 DNS server 中添加相关的解析记录，那么这里就需要指定 IP（前提是你的证书包含了这个二级域名，这个很好支持，搞个泛域名证书就行了）。 stunonly: false 表示除了使用 STUN 服务，还可以使用 DERP 服务。 上面的配置中域名和 IP 部分我都打码了，你需要根据你的实际情况填写。 接下来还需要修改 Headscale 的配置文件，引用上面的自定义 DERP 配置文件。需要修改的配置项如下：\n# /etc/headscale/config.yaml derp: # List of externally available DERP maps encoded in JSON urls: # - https://controlplane.tailscale.com/derpmap/default # Locally available DERP map files encoded in YAML # # This option is mostly interesting for people hosting # their own DERP servers: # https://tailscale.com/kb/1118/custom-derp-servers/ # # paths: # - /etc/headscale/derp-example.yaml paths: - /etc/headscale/derp.yaml # If enabled, a worker will be set up to periodically # refresh the given sources and update the derpmap # will be set up. auto_update_enabled: true # How often should we check for DERP updates? update_frequency: 24h 可以把 Tailscale 官方的 DERP 服务器禁用，来测试自建的 DERP 服务器是否能正常工作。\n修改完配置后，重启 headscale 服务：\n$ systemctl restart headscale 在 Tailscale 客户端上使用以下命令查看目前可以使用的 DERP 服务器：\n$ tailscale netcheck Report: * UDP: true * IPv4: yes, xxxxx:57068 * IPv6: no * MappingVariesByDestIP: false * HairPinning: false * PortMapping: * Nearest DERP: Tencent Hongkong * DERP latency: - thk: 39.7ms (Tencent Hongkong) tailscale netcheck 实际上只检测 3478/udp 的端口， 就算 netcheck 显示能连，也不一定代表 12345 端口可以转发流量。最简单的办法是直接打开 DERP 服务器的 URL：https://xxxx:12345，如果看到如下页面，且地址栏的 SSL 证书标签显示正常可用，那才是真没问题了。\n查看与通信对端的连接方式：\n$ tailscale status 10.1.0.5 coredns default linux - carsondemacbook-pro default macOS active; direct xxxx:2756; offline, tx 50424 rx 34056 oneplus-8t default android active; relay \u0026#34;thk\u0026#34;; offline, tx 1608 rx 1552 openwrt default linux active; direct xxxx:2834; offline, tx 1403688 rx 1217620 这个客户端是一台云主机，有 3 个通信对端，分别是 macOS、OpenWRT 与 Android 手机，macOS 和 OpenWRT 都处于电信家庭内网中，Android 手机使用的是电信流量。可以看到只有 Android 手机是通过自定义的 DERP 服务器来中继流量的，打洞成功率相当高。使用 ping 来测试连通性：\n$ ping 10.1.0.8 PING 10.1.0.8 (10.1.0.8) 56(84) bytes of data. 64 bytes from 10.1.0.8: icmp_seq=1 ttl=64 time=150 ms 64 bytes from 10.1.0.8: icmp_seq=2 ttl=64 time=131 ms 64 bytes from 10.1.0.8: icmp_seq=3 ttl=64 time=161 ms 64 bytes from 10.1.0.8: icmp_seq=4 ttl=64 time=137 ms 64 bytes from 10.1.0.8: icmp_seq=5 ttl=64 time=156 ms 64 bytes from 10.1.0.8: icmp_seq=6 ttl=64 time=169 ms ^C --- 10.1.0.8 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5005ms rtt min/avg/max/mdev = 131.728/151.154/169.627/13.193 ms 也可以使用 Tailscale 命令行工具来测试：\n$ tailscale ping 10.1.0.8 pong from oneplus-8t (10.1.0.8) via DERP(thk) in 104ms pong from oneplus-8t (10.1.0.8) via DERP(thk) in 111ms pong from oneplus-8t (10.1.0.8) via DERP(thk) in 105ms 这个更加友好一点，会直接告诉你是通过 DERP 中继服务器来和对方通信的。\n如果当前 Tailscale 客户端所在主机开启了 IPv6，那么与手机便可以直接通过 IPv6 点对点连接：\n$ /Applications/Tailscale.app/Contents/MacOS/Tailscale status coredns default linux active; direct xxxx:45986; offline, tx 124352 rx 185736 oneplus-8t default android active; direct [240e:472:da0:24a2:a07f:2a67:2a1e:4475]:37237; offline, tx 125216 rx 20052 openwrt default linux active; direct [240e:390:caf:1870:c02c:e8ff:feb9:b0b]:41641; offline, tx 181992 rx 3910120 $ /Applications/Tailscale.app/Contents/MacOS/Tailscale ping 10.1.0.8 pong from oneplus-8t (10.1.0.8) via [240e:472:da0:24a2:a07f:2a67:2a1e:4475]:37237 in 62ms 所以如果你开启了 IPv6，可以大大增加点对点连接的成功率。\n使用纯 IP # 我知道，大部分人是没有自己的域名的。再退一步，就算有自己的域名，如果没有备案，也是没办法部署在国内服务器上使用的。\n这个时候我们就只能从 derper 源码上动手脚了，找到 tailscale 仓库中的 cmd/derper/cert.go 文件，将与域名验证相关的内容删除或注释：\nfunc (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { //if hi.ServerName != m.hostname { //\treturn nil, fmt.Errorf(\u0026#34;cert mismatch with hostname: %q\u0026#34;, hi.ServerName) //} return m.cert, nil } 还需要创建自签名证书，可以通过脚本来创建：\n# build_cert.sh #!/bin/bash CERT_HOST=$1 CERT_DIR=$2 CONF_FILE=$3 echo \u0026#34;[req] default_bits = 2048 distinguished_name = req_distinguished_name req_extensions = req_ext x509_extensions = v3_req prompt = no [req_distinguished_name] countryName = XX stateOrProvinceName = N/A localityName = N/A organizationName = Self-signed certificate commonName = $CERT_HOST: Self-signed certificate [req_ext] subjectAltName = @alt_names [v3_req] subjectAltName = @alt_names [alt_names] IP.1 = $CERT_HOST \u0026#34; \u0026gt; \u0026#34;$CONF_FILE\u0026#34; mkdir -p \u0026#34;$CERT_DIR\u0026#34; openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout \u0026#34;$CERT_DIR/$CERT_HOST.key\u0026#34; -out \u0026#34;$CERT_DIR/$CERT_HOST.crt\u0026#34; -config \u0026#34;$CONF_FILE\u0026#34; 重新编写 Dockerfile，将 derper 的域名设置为 127.0.0.1：\nFROM golang:latest AS builder WORKDIR /app # ========= CONFIG ========= # - download links ENV MODIFIED_DERPER_GIT=https://github.com/yangchuansheng/ip_derper.git ENV BRANCH=ip_derper # ========================== # build modified derper RUN git clone -b $BRANCH $MODIFIED_DERPER_GIT tailscale --depth 1 \u0026amp;\u0026amp; \\ cd /app/tailscale/cmd/derper \u0026amp;\u0026amp; \\ /usr/local/go/bin/go build -ldflags \u0026#34;-s -w\u0026#34; -o /app/derper \u0026amp;\u0026amp; \\ cd /app \u0026amp;\u0026amp; \\ rm -rf /app/tailscale FROM ubuntu:20.04 WORKDIR /app # ========= CONFIG ========= # - derper args ENV DERP_HOST=127.0.0.1 ENV DERP_CERTS=/app/certs/ ENV DERP_STUN true ENV DERP_VERIFY_CLIENTS false # ========================== # apt RUN apt-get update \u0026amp;\u0026amp; \\ apt-get install -y openssl curl COPY build_cert.sh /app/ COPY --from=builder /app/derper /app/derper # build self-signed certs \u0026amp;\u0026amp; start derper CMD bash /app/build_cert.sh $DERP_HOST $DERP_CERTS /app/san.conf \u0026amp;\u0026amp; \\ /app/derper --hostname=$DERP_HOST \\ --certmode=manual \\ --certdir=$DERP_CERTS \\ --stun=$DERP_STUN \\ --verify-clients=$DERP_VERIFY_CLIENTS 构建好镜像后，就可以在你想部署 derper 的主机上直接通过该镜像启动 derper 容器了，命令如下：\n🐳 → docker run --restart always --net host --name derper -d ghcr.io/yangchuansheng/ip_derper 和使用域名的方案一样，防火墙需要放行相应端口（12345 与 3478）。\n查看容器日志：\n🐳 → docker logs -f derper Generating a RSA private key .......................................+++++ ..............+++++ writing new private key to \u0026#39;/app/certs//127.0.0.1.key\u0026#39; ----- 2022/03/26 14:30:31 no config path specified; using /var/lib/derper/derper.key 2022/03/26 14:30:31 derper: serving on :443 with TLS 2022/03/26 14:30:31 running STUN server on [::]:3478 如果你想自己构建 derper 镜像，可以参考 我的 GitHub 仓库。\n下面就是骚操作了，我们在 Headscale 的配置中需要将 DERP 的域名设置为 IP！不理解的可以再消化一下，然后继续往下看哈哈~~\n除了 derper 之外，Tailscale 客户端还需要跳过域名验证，这个需要在 DERP 的配置中设置。而 Headscale 的本地 YAML 文件目前还不支持这个配置项，所以没办法，咱只能使用在线 URL 了。JSON 配置内容如下：\n{ \u0026#34;Regions\u0026#34;: { \u0026#34;901\u0026#34;: { \u0026#34;RegionID\u0026#34;: 901, \u0026#34;RegionCode\u0026#34;: \u0026#34;ali-sh\u0026#34;, \u0026#34;RegionName\u0026#34;: \u0026#34;Aliyun Shanghai\u0026#34;, \u0026#34;Nodes\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;901a\u0026#34;, \u0026#34;RegionID\u0026#34;: 901, \u0026#34;DERPPort\u0026#34;: 443, \u0026#34;HostName\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;IPv4\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;InsecureForTests\u0026#34;: true } ] } } } 配置解析：\nHostName 直接填 derper 的公网 IP，即和 IPv4 的值相同。 InsecureForTests 一定要设置为 true，以跳过域名验证。 你需要把这个 JSON 文件变成 Headscale 服务器可以访问的 URL，比如在 Headscale 主机上搭个 Nginx，或者上传到对象存储（比如阿里云 OSS）。\n接下来还需要修改 Headscale 的配置文件，引用上面的自定义 DERP 的 URL。需要修改的配置项如下：\n# /etc/headscale/config.yaml derp: # List of externally available DERP maps encoded in JSON urls: # - https://controlplane.tailscale.com/derpmap/default - https://xxxxx/derp.json # Locally available DERP map files encoded in YAML # # This option is mostly interesting for people hosting # their own DERP servers: # https://tailscale.com/kb/1118/custom-derp-servers/ # # paths: # - /etc/headscale/derp-example.yaml paths: - /etc/headscale/derp.yaml # If enabled, a worker will be set up to periodically # refresh the given sources and update the derpmap # will be set up. auto_update_enabled: true # How often should we check for DERP updates? update_frequency: 24h 修改完配置后，重启 headscale 服务：\n$ systemctl restart headscale 在 Tailscale 客户端上使用以下命令查看目前可以使用的 DERP 服务器：\n$ tailscale netcheck Report: * UDP: true * IPv4: yes, 192.168.100.1:49656 * IPv6: no * MappingVariesByDestIP: true * HairPinning: false * PortMapping: UPnP * Nearest DERP: Home Hangzhou * DERP latency: - home: 9.7ms (Home Hangzhou) - hs: 25.2ms (Huawei Shanghai) - thk: 43.5ms (Tencent Hongkong) 再次查看与通信对端的连接方式：\n$ tailscale status coredns default linux active; direct xxxx:45986; offline, tx 131012 rx 196020 oneplus-8t default android active; relay \u0026#34;home\u0026#34;; offline, tx 211900 rx 22780 openwrt default linux active; direct 192.168.100.254:41641; offline, tx 189868 rx 4074772 可以看到这一次 Tailscale 自动选择了一个线路最优的国内的 DERP 服务器作为中继，可以测试一下延迟：\n$ tailscale ping 10.1.0.8 pong from oneplus-8t (10.1.0.8) via DERP(home) in 30ms pong from oneplus-8t (10.1.0.8) via DERP(home) in 45ms pong from oneplus-8t (10.1.0.8) via DERP(home) in 30ms 完美！这里的 home 当然是我的家庭宽带，部署方式与上面所说的国内云主机类似，你需要额外开启公网的端口映射（12345/tcp, 3478/udp）。还有一点需要注意的是配置内容：\n{ \u0026#34;Regions\u0026#34;: { \u0026#34;901\u0026#34;: { \u0026#34;RegionID\u0026#34;: 901, \u0026#34;RegionCode\u0026#34;: \u0026#34;ali-sh\u0026#34;, \u0026#34;RegionName\u0026#34;: \u0026#34;Aliyun Shanghai\u0026#34;, \u0026#34;Nodes\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;901a\u0026#34;, \u0026#34;RegionID\u0026#34;: 901, \u0026#34;DERPPort\u0026#34;: 443, \u0026#34;HostName\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;IPv4\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;InsecureForTests\u0026#34;: true } ] }, \u0026#34;902\u0026#34;: { \u0026#34;RegionID\u0026#34;: 902, \u0026#34;RegionCode\u0026#34;: \u0026#34;home\u0026#34;, \u0026#34;RegionName\u0026#34;: \u0026#34;Home Hangzhou\u0026#34;, \u0026#34;Nodes\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;902a\u0026#34;, \u0026#34;RegionID\u0026#34;: 902, \u0026#34;DERPPort\u0026#34;: 12345, \u0026#34;HostName\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;InsecureForTests\u0026#34;: true } ] } } } 与国内云主机相比，家庭宽带的配置有两点不同：\n需要删除 IPv4 配置项。因为家用宽带的公网 IP 是动态变化的，所以你需要使用 DDNS 来动态解析公网 IP。 HostName 最好填域名，因为你的公网 IP 是动态变化的，没法填写 IP，除非你不停地修改配置文件。填域名也没关系啦，反正不会验证域名的，也不用关心证书的事情，只要域名能解析到你的公网 IP 即可。 防止 DERP 被白嫖 # 默认情况下 DERP 服务器是可以被白嫖的，只要别人知道了你的 DERP 服务器的地址和端口，就可以为他所用。如果你的服务器是个小水管，用的人多了可能会把你撑爆，因此我们需要修改配置来防止被白嫖。\n特别声明：只有使用域名的方式才可以通过认证防止被白嫖，使用纯 IP 的方式无法防白嫖，你只能小心翼翼地隐藏好你的 IP 和端口，不能让别人知道。\n只需要做两件事情：\n1、在 DERP 服务器上安装 Tailscale。\n第一步需要在 DERP 服务所在的主机上安装 Tailscale 客户端，启动 tailscaled 进程。\n2、derper 启动时加上参数 --verify-clients。\n本文推荐的是通过容器启动， Dockerfile 内容如下：\nFROM golang:latest AS builder LABEL org.opencontainers.image.source https://github.com/yangchuansheng/docker-image WORKDIR /app # https://tailscale.com/kb/1118/custom-derp-servers/ RUN go install tailscale.com/cmd/derper@main FROM ubuntu WORKDIR /app ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \u0026amp;\u0026amp; \\ apt-get install -y --no-install-recommends apt-utils \u0026amp;\u0026amp; \\ apt-get install -y ca-certificates \u0026amp;\u0026amp; \\ mkdir /app/certs ENV DERP_DOMAIN your-hostname.com ENV DERP_CERT_MODE letsencrypt ENV DERP_CERT_DIR /app/certs ENV DERP_ADDR :443 ENV DERP_STUN true ENV DERP_HTTP_PORT 80 ENV DERP_VERIFY_CLIENTS false COPY --from=builder /go/bin/derper . CMD /app/derper --hostname=$DERP_DOMAIN \\ --certmode=$DERP_CERT_MODE \\ --certdir=$DERP_CERT_DIR \\ --a=$DERP_ADDR \\ --stun=$DERP_STUN \\ --http-port=$DERP_HTTP_PORT \\ --verify-clients=$DERP_VERIFY_CLIENTS 默认情况下 --verify-clients 参数设置的是 false。我们不需要对 Dockerfile 内容做任何改动，只需在容器启动时加上环境变量即可，将之前的启动命令修改一下：\n🐳 → docker run --restart always \\ --name derper -p 12345:12345 -p 3478:3478/udp \\ -v /root/.acme.sh/xxxx/:/app/certs \\ -e DERP_CERT_MODE=manual \\ -e DERP_ADDR=:12345 \\ -e DERP_DOMAIN=xxxx \\ -e DERP_VERIFY_CLIENTS=true \\ -d ghcr.io/yangchuansheng/derper:latest 这样就大功告成了，别人即使知道了你的 DERP 服务器地址也无法使用，但还是要说明一点，即便如此，你也应该尽量不让别人知道你的服务器地址，防止别人有可趁之机。\n总结 # 本文给大家介绍了 STUN 对于辅助 NAT 穿透的意义，科普了几种常见的中继协议，包含 Tailscale 自研的 DERP 协议。最后手把手教大家如何自建私有的 DERP 服务器，并让 Tailscale 使用我们自建的 DERP 服务器。\n参考资料 # NAT 穿透是如何工作的：技术原理及企业级实践 Custom DERP Servers Encrypted TCP relays (DERP) ","date":"2022年3月27日","externalUrl":null,"permalink":"/posts/custom-derp-servers/","section":"博客","summary":"上篇文章介绍了如何使用 Headscale 替代 Tailscale 官方的控制服务器，并接入各个平","title":"Tailscale 基础教程：部署私有 DERP 中继服务器","type":"posts"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/tags/headscale/","section":"Tags","summary":"","title":"Headscale","type":"tags"},{"content":"Headscale is an open-source server that works like Tailscale\u0026rsquo;s control server. You can run it yourself instead of using Tailscale\u0026rsquo;s hosted service. This gives you full control over your VPN network without device limits or subscription fees. Here\u0026rsquo;s how to set it up and connect your devices.\nWhat is Tailscale? # Tailscale is a VPN built on WireGuard. It works like other mesh VPN tools such as Netmaker. Tailscale runs WireGuard in user space, while Netmaker uses kernel-space WireGuard. This means Tailscale has slightly lower performance than kernel-space solutions. But it\u0026rsquo;s still much faster than OpenVPN and easier to use.\nHere\u0026rsquo;s what makes Tailscale useful:\nSimple setup: No firewall configuration needed. Easy network setup. Security: Automatic key rotation. End-to-end encryption by default. Access logs you can review. NAT traversal: Uses DERP (Detoured Encrypted Routing Protocol) over TCP when UDP doesn\u0026rsquo;t work. This helps connections work even behind difficult firewalls. Central control: Push access rules and settings to all devices from one place. Easy authentication: Works with Google, Microsoft, GitHub, and other login providers. Tailscale is basically WireGuard made easier to use.\nTailscale Admin Console Tailscale has a free tier that works well for personal use or small teams. You can use it free as long as you stay under the device limit (around 20 devices per account - check their website for current limits). The free tier has some limits on advanced features like subnet routing, but it covers most basic needs. Most Tailscale client code is open source under the BSD license (except Windows and macOS GUI apps). You can find the source code in their GitHub repository.\nThe free tier works for most people. If you need more features, they have paid plans.\nBut what if you want full control, unlimited devices, and no subscription fees? That\u0026rsquo;s where Headscale comes in.\nWhat is Headscale? # Tailscale\u0026rsquo;s control server is proprietary and has limits for free users. That\u0026rsquo;s fair since it\u0026rsquo;s how they make money. But the open-source community built an alternative: Headscale. It\u0026rsquo;s become the main open-source option for self-hosted Tailscale setups.\nJuan Font at the European Space Agency created Headscale. It\u0026rsquo;s written in Go and released under the BSD license. Headscale copies most of Tailscale\u0026rsquo;s control server features. You can run the whole coordination server yourself. This gives you full control over your network traffic and removes device limits. If you want to run your own secure VPN without restrictions, Headscale is a good choice.\nHow to Deploy Headscale # Here are different ways to set up Headscale.\nQuick Deploy with Sealos # If you want to get started fast, the Sealos App Store has one-click Headscale deployment. This is simple and needs almost no setup.\nClick one of these to deploy Headscale:\nSQLite version: PostgreSQL version: Click \u0026ldquo;Deploy on Sealos\u0026rdquo; to start. Sign in to your Sealos account, then click \u0026ldquo;Deploy Application.\u0026rdquo; When it\u0026rsquo;s done, click on the Headscale app\u0026rsquo;s \u0026ldquo;Details\u0026rdquo; to see how to access it.\nHeadscale application in Sealos App Store Your Headscale service will have a public domain that maps to internal port 8080.\nHeadscale app details in Sealos Click on the public address to access your Headscale dashboard. This usually opens Headplane, a web interface for managing Headscale.\nHeadplane login page Deploy on a Linux Server # Setting up Headscale manually on Linux is pretty simple.\nTip: Headscale only needs internet access to work. But for best NAT traversal performance, use a cloud server with a public IP address. This avoids extra NAT layers and gives your Tailscale clients better connectivity.\nFirst, download the latest Headscale binary from the GitHub releases page.\n# Replace \u0026lt;HEADSCALE_VERSION\u0026gt; and \u0026lt;ARCH\u0026gt; with your version and architecture (e.g., v0.22.3, amd64) $ wget --output-document=/usr/local/bin/headscale \\ https://github.com/juanfont/headscale/releases/download/v\u0026lt;HEADSCALE_VERSION\u0026gt;/headscale_\u0026lt;HEADSCALE_VERSION\u0026gt;_linux_\u0026lt;ARCH\u0026gt; $ chmod +x /usr/local/bin/headscale Create the config directory:\n$ mkdir -p /etc/headscale Create a directory for data storage (SQLite DB and SSL certificates):\n$ mkdir -p /var/lib/headscale Create an empty SQLite database file:\n$ touch /var/lib/headscale/db.sqlite Download the example config file:\n$ wget https://github.com/juanfont/headscale/raw/main/config-example.yaml -O /etc/headscale/config.yaml Now edit /etc/headscale/config.yaml:\nSet server_url to your public IP or domain (e.g., http://\u0026lt;YOUR_PUBLIC_IP_OR_DOMAIN\u0026gt;:8080 or https://\u0026lt;YOUR_PUBLIC_IP_OR_DOMAIN\u0026gt;). Note: If using a domain in mainland China, make sure it has ICP filing. If not, just use your public IP address. For HTTPS, make sure your TLS certificates are set up correctly. If you don\u0026rsquo;t want MagicDNS initially, set dns_config.magic_dns to false. Enable randomized client ports for better NAT traversal by setting randomize_client_port to true. You can customize your private network IP ranges: ip_prefixes: # - fd7a:115c:a1e0::/48 # IPv6 prefix for your network - 100.64.0.0/10 # Default address space used by Tailscale/Headscale Create a SystemD service file so Headscale runs automatically:\n# /etc/systemd/system/headscale.service [Unit] Description=Headscale Controller - Self-hosted Tailscale coordination server After=syslog.target After=network.target [Service] Type=simple User=headscale Group=headscale ExecStart=/usr/local/bin/headscale serve Restart=always RestartSec=5 # Security settings NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/var/lib/headscale /var/run/headscale AmbientCapabilities=CAP_NET_BIND_SERVICE RuntimeDirectory=headscale [Install] WantedBy=multi-user.target Create the headscale user:\n$ useradd headscale -d /home/headscale -m -s /bin/false Change ownership of the data directory:\n$ chown -R headscale:headscale /var/lib/headscale Update the unix_socket path in /etc/headscale/config.yaml:\nunix_socket: /var/run/headscale/headscale.sock Reload SystemD:\n$ systemctl daemon-reload Start Headscale and enable it to start at boot:\n$ systemctl enable --now headscale Check the service status:\n$ systemctl status headscale # Look for \u0026#34;active (running)\u0026#34; Check what ports Headscale is listening on (usually 8080 for HTTP, 9090 for gRPC):\n$ ss -tulnp | grep headscale # Example output: # tcp LISTEN 0 1024 *:8080 *:* users:((\u0026#34;headscale\u0026#34;,pid=...,fd=...)) # tcp LISTEN 0 1024 *:9090 *:* users:((\u0026#34;headscale\u0026#34;,pid=...,fd=...)) Managing Users in Headscale # Tailscale uses \u0026ldquo;tailnets\u0026rdquo; - separate network groups that don\u0026rsquo;t talk to each other. You can read more about this in Tailscale\u0026rsquo;s docs: What is a tailnet?. In Headscale, these are called \u0026ldquo;users.\u0026rdquo; You need to create at least one user before connecting devices to your network.\nCreate Users via Command Line # To create a user named default:\n$ headscale users create default # Expected output: \u0026#34;User created\u0026#34; To see your users:\n$ headscale users list # Example output: # ID | Name | Username | Email | Created # 1 | | default | | 2025-05-26 05:03:48 If you used Sealos to deploy Headscale, click the \u0026ldquo;Terminal\u0026rdquo; button on the Headscale app details page to open a terminal:\nSealos container terminal Then run the commands above to create your user.\nCreate Users with Headplane # Headplane is a web interface for managing Headscale. It needs an API key to connect to your Headscale server. First, generate an API key.\nRun this command on your Headscale server (or in the Sealos terminal):\n$ headscale apikeys create # Copy the generated API key. Keep it safe like a password. Enter the API key in Headplane\u0026rsquo;s settings page. After connecting, go to \u0026ldquo;Users\u0026rdquo; at the top, then click \u0026ldquo;Add a new user\u0026rdquo;:\nHeadplane user creation interface Connect Tailscale Clients to Headscale # Tailscale clients on all major platforms can connect to custom control servers like Headscale. Older iOS clients worked a bit differently, but modern versions are simpler.\nOS Works with Headscale Notes Linux Yes CLI and GUI clients. Good for relay nodes. OpenBSD Yes CLI client. FreeBSD Yes CLI client. macOS Yes GUI and CLI. See macOS docs. Windows Yes GUI interface. See Windows docs. Android Yes GUI interface. Server can be changed. iOS Yes GUI interface. See iOS docs. Here\u0026rsquo;s how to set up Tailscale clients on different platforms.\nConnect Linux Clients # Tailscale has official packages for Linux distributions. But users in some regions (like China) might get slow downloads from official repos. Tailscale also offers static binaries you can download directly.\nDownload the static version:\n# Replace 1.XX.Y with the latest version and amd64 with your architecture $ wget https://pkgs.tailscale.com/stable/tailscale_1.XX.Y_amd64.tgz Extract it:\n$ tar zxvf tailscale_1.XX.Y_amd64.tgz # Creates a directory like tailscale_1.XX.Y_amd64/ with the binaries Install the binaries:\n$ sudo cp tailscale_1.XX.Y_amd64/tailscaled /usr/sbin/tailscaled $ sudo cp tailscale_1.XX.Y_amd64/tailscale /usr/bin/tailscale Copy the systemd service files:\n$ sudo cp tailscale_1.XX.Y_amd64/systemd/tailscaled.service /lib/systemd/system/tailscaled.service $ sudo cp tailscale_1.XX.Y_amd64/systemd/tailscaled.defaults /etc/default/tailscaled Start the service:\n$ sudo systemctl enable --now tailscaled Check that it\u0026rsquo;s running:\n$ sudo systemctl status tailscaled # Look for \u0026#34;active (running)\u0026#34; Now connect your Linux client to Headscale:\n# Replace \u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt; with your server\u0026#39;s public IP or domain. # Use the right protocol (http/https) and port from your config.yaml. $ sudo tailscale up --login-server=http://\u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt;:8080 --accept-routes=true --accept-dns=false # If you used Sealos deployment, HTTPS is usually set up already. # Replace \u0026lt;SEALOS_HEADSCALE_DOMAIN\u0026gt; with your Sealos domain. $ sudo tailscale up --login-server=https://\u0026lt;SEALOS_HEADSCALE_DOMAIN\u0026gt; --accept-routes=true --accept-dns=false You can also get connection commands from Headplane\u0026rsquo;s \u0026ldquo;Machines\u0026rdquo; page, which fills in the server URL for you.\nConnection commands from Headplane Use --accept-dns=false at first to stop Headscale\u0026rsquo;s MagicDNS from changing your system DNS settings. Only enable it if you need it and have set it up properly.\nAfter running tailscale up, you\u0026rsquo;ll see a registration link:\nTo authenticate, visit: https://headscale-your-instance.example.com/register/nodekey_xxxxxxxxxxxxxxxxxxxxxxxxxx Headscale registration page Open this link in your browser. The page will show you a command to run on your Headscale server to register the device. Or you can use Headplane: copy the node key (the long string after /register/), go to \u0026ldquo;Machines\u0026rdquo; in Headplane, click \u0026ldquo;Add Device,\u0026rdquo; paste the key in the Machine Key field, pick an Owner (the Headscale user), and click \u0026ldquo;Confirm.\u0026rdquo;\nAdding a device in Headplane After registration, you\u0026rsquo;ll see the device info in Headplane:\nRegistered device in Headplane Back on your Linux machine, Tailscale sets up the network automatically. This includes a network interface (like tailscale0), routing tables, and iptables rules. To check the Tailscale routing table:\n$ ip route show table 52 To check iptables rules for Tailscale (if you use iptables):\n$ sudo iptables -S | grep tailscale $ sudo iptables -S -t nat | grep tailscale Set Up macOS Clients # There are three ways to install Tailscale on macOS:\nApp Store: Get the app from the Mac App Store. Direct download: Download the .pkg installer or .zip file from Tailscale\u0026rsquo;s website. Command line: Install the CLI tools. See the macOS documentation for details. All three use the same networking code. They just differ in packaging and whether they have a GUI.\nFeature App Store Standalone App Command Line GUI Yes Yes No Background Running No Yes Yes Automatic Updates Yes Yes Manual Open Source No No Yes File Transfer Yes Yes No Important: Pick either the App Store version or the standalone version. Don\u0026rsquo;t install both. After installing the Tailscale app, you need to configure it to use your Headscale server. Your Headscale server has setup instructions at https://\u0026lt;YOUR_HEADSCALE_PUBLIC_ENDPOINT\u0026gt;/apple.\nFor Tailscale versions 1.34.0 and later:\nHold Option (⌥) and click the Tailscale icon in your menu bar. Hover over the Debug menu.\nmacOS Tailscale Debug menu Select \u0026ldquo;Custom Login Server\u0026hellip;\u0026rdquo; then \u0026ldquo;Add Account\u0026hellip;\u0026rdquo; or \u0026ldquo;Change server\u0026hellip;\u0026rdquo;. Enter your Headscale URL (e.g., https://\u0026lt;YOUR_HEADSCALE_DOMAIN\u0026gt;). Make sure to include http:// or https://. Click \u0026ldquo;Add Account\u0026rdquo; or \u0026ldquo;Save.\u0026rdquo;\nmacOS Tailscale server configuration Tailscale will connect to your Headscale server and open a registration page in your browser. Register the device the same way as Linux: copy the node key, go to \u0026ldquo;Machines\u0026rdquo; in Headplane, click \u0026ldquo;Add Device,\u0026rdquo; paste the key, pick an Owner, and click \u0026ldquo;Confirm.\u0026rdquo;\nmacOS registration success Test connectivity by pinging another device in your network:\n$ ping -c 2 100.64.0.1 # Ping another device\u0026#39;s Tailscale IP # Expected output: # PING 100.64.0.1 (100.64.0.1): 56 data bytes # 64 bytes from 100.64.0.1: icmp_seq=0 ttl=64 time=37.025 ms You can also use the Tailscale CLI if available:\n$ /Applications/Tailscale.app/Contents/MacOS/Tailscale ping 100.64.0.1 # Expected: pong from device-name (100.64.0.1) via ... in Xms For older Tailscale versions (before 1.32.0), the setup might be different. Check your Headscale server\u0026rsquo;s /apple page for specific instructions.\nConnect Android Clients # Android Tailscale has supported custom servers like Headscale since version 1.30.0. Download it from Google Play or F-Droid.\nOpen the Tailscale app. You\u0026rsquo;ll see the login screen:\nAndroid Tailscale login screen Tap the three-dot menu (⋮) in the top right. You\u0026rsquo;ll only see an About option at first:\nAndroid Tailscale menu Here\u0026rsquo;s a trick to unlock the custom server option: quickly tap to open and close the three-dot menu about 5-7 times. A Change server option will appear:\nChange server option unlocked Tap Change server and enter your Headscale URL (e.g., https://\u0026lt;YOUR_HEADSCALE_DOMAIN\u0026gt;). Include http:// or https://:\nServer address input Tap Save and restart. After restart, tap Sign in with other.... You\u0026rsquo;ll go to your browser for registration:\nHeadscale authentication page Copy the machine key from the page. Register it on your Headscale server or via Headplane like before. After registration, go back to the app - it should show as connected:\nAndroid Tailscale connected Connect Windows Clients # To connect Windows Tailscale to Headscale, follow the instructions on your Headscale server\u0026rsquo;s Windows page. Open https://\u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt;/windows in your browser. This page shows you how to set it up, usually by editing the Windows Registry or using command-line parameters:\nWindows setup instructions Follow the instructions carefully. You\u0026rsquo;ll usually edit registry entries to set the ControlURL or use parameters like /custom-server=\u0026lt;YOUR_HEADSCALE_URL\u0026gt; when installing or running Tailscale. After making changes, start the Tailscale client. It should open your browser for Headscale authentication, just like Linux and macOS.\nConnect Other Linux Devices # Many people run Linux on routers (OpenWrt) or NAS systems (QNAP, Synology). The community has made tools for installing Tailscale and connecting to Headscale on these devices:\nOpenWrt: Check out adyanth/openwrt-tailscale-enabler for scripts and packages. Synology NAS: Tailscale has official packages. See tailscale/tailscale-synology. QNAP NAS: Tailscale has official QNAP packages: tailscale/tailscale-qpkg. Check the documentation in these repositories or your device\u0026rsquo;s forums for setup instructions. These devices work great as subnet routers.\nConnect iOS Clients # To use Tailscale on iPhone/iPad with Headscale, download the official app from the App Store. You might need an Apple ID from a region where Tailscale is available.\nDownload the Tailscale iOS app. Open the app. Tap the account icon in the top-right, then \u0026ldquo;Log in\u0026hellip;\u0026rdquo;.\niOS account screen On the login screen, tap the options menu (three dots or gear) and choose \u0026ldquo;Use custom coordination server\u0026rdquo;.\nCustom server option Enter your Headscale URL (e.g., https://\u0026lt;YOUR_HEADSCALE_DOMAIN\u0026gt;) and tap \u0026ldquo;Log in\u0026rdquo;. You\u0026rsquo;ll go to your Headscale authentication page. Register the device like before: Copy the machine key from the browser. Go to \u0026ldquo;Machines\u0026rdquo; in Headplane, click \u0026ldquo;Add Device,\u0026rdquo; paste the key, pick an Owner, and click \u0026ldquo;Confirm.\u0026rdquo; The iOS app will show the connection status and IP address.\niOS Tailscale connected Auto-Register Devices with Pre-Auth Keys # Headscale has \u0026ldquo;Pre-Authkeys\u0026rdquo; that let devices join automatically without manual approval each time. This is useful when adding many devices or automating setups.\nGenerate a pre-auth key for your user:\n# Create a key for \u0026#39;default\u0026#39; user that expires in 24 hours $ headscale preauthkeys create --user default --expiration 24h # Output includes: Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx List your pre-auth keys:\n$ headscale preauthkeys list --user default # Shows: ID, Key, Reusable, Ephemeral, Used, Expiration, Created You can also create keys in Headplane. Go to \u0026ldquo;Pre-auth keys\u0026rdquo; and click \u0026ldquo;Add a new Pre-auth key\u0026rdquo;:\nCreating pre-auth key Set the user, expiration time, and whether it\u0026rsquo;s reusable (multiple devices can use it) or single-use. Click \u0026ldquo;Confirm\u0026rdquo;:\nPre-auth key created View and manage your keys:\nPre-auth keys list Now devices can join automatically using the --authkey parameter:\n# Replace $AUTH_KEY with your actual key # Replace \u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt; with your server address $ sudo tailscale up --login-server=http://\u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt;:8080 --accept-routes=true --accept-dns=false --authkey=$AUTH_KEY The device will register automatically under the user associated with the key.\nSet Up Subnet Routing # So far, we\u0026rsquo;ve built a mesh network where Tailscale devices talk directly using their Tailscale IPs (usually 100.x.x.x). But Headscale and Tailscale can do more. You can let any Tailscale device access an entire local network behind another Tailscale device. This subnet routing has many uses:\nAccess your home NAS, printers, and smart devices from anywhere. Connect to internal work servers remotely. Advanced: Access Kubernetes pod and service IPs by making K8s nodes subnet routers. Say you have a Linux machine on your home network (like an OpenWrt router, Raspberry Pi, or any Linux server) running Tailscale and connected to Headscale. You want other Tailscale clients (like your laptop when you\u0026rsquo;re away) to reach any device on your home network using their local IP addresses (like the 192.168.100.0/24 subnet).\nHere\u0026rsquo;s how to set up subnet routing:\nEnable IP forwarding on the router:\nOn the Linux machine that will route traffic (like your OpenWrt router), enable IP forwarding:\n# Enable IPv4 forwarding $ echo \u0026#39;net.ipv4.ip_forward = 1\u0026#39; | sudo tee /etc/sysctl.d/ipforwarding.conf # Enable IPv6 forwarding if needed $ echo \u0026#39;net.ipv6.conf.all.forwarding = 1\u0026#39; | sudo tee -a /etc/sysctl.d/ipforwarding.conf # Apply changes now $ sudo sysctl -p /etc/sysctl.d/ipforwarding.conf Advertise routes from the router:\nAdd --advertise-routes=192.168.100.0/24 to tell Headscale this device can route traffic for that subnet. If Tailscale is already running, use --reset to apply the new config:\n# Replace \u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt; with your server address $ sudo tailscale up --login-server=http://\u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt;:8080 --accept-routes=true --accept-dns=false --advertise-routes=192.168.100.0/24 --reset For multiple subnets, separate with commas: --advertise-routes=192.168.100.0/24,10.0.0.0/8.\nEnable the routes on Headscale:\nAfter a device advertises routes, you must enable them on the Headscale server before other clients can use them.\nUsing Headplane:\nGo to the router device\u0026rsquo;s details page, click \u0026ldquo;Machine Settings\u0026rdquo; → \u0026ldquo;Edit route settings\u0026rdquo;:\nRoute settings Find the advertised route (like 192.168.100.0/24) and toggle it to \u0026ldquo;Enabled\u0026rdquo;:\nEnable route Using command line:\nFind your router node\u0026rsquo;s ID:\n$ headscale nodes list | grep your-router-hostname # Example output: # ID | Hostname | ... # 6 | openwrt-router | ... # List routes for the node $ headscale routes list --node-id 6 # Example output: # Route | Enabled | Node # 192.168.100.0/24 | false | openwrt-router Enable the route:\n$ headscale routes enable --node-id 6 --route \u0026#34;192.168.100.0/24\u0026#34; # Output: Route enabled To enable all routes for a node at once:\n$ headscale routes enable --node-id 6 -a Accept routes on client devices:\nMake sure client devices that need subnet access use --accept-routes=true. If they\u0026rsquo;re already running without this, reconfigure them with --reset:\n# On a client that needs subnet access $ sudo tailscale up --login-server=http://\u0026lt;HEADSCALE_PUB_ENDPOINT\u0026gt;:8080 --accept-routes=true --accept-dns=false --reset Check the routing table to confirm it\u0026rsquo;s working:\n# On a Linux client $ ip route show table 52 | grep \u0026#34;192.168.100.0/24\u0026#34; # Expected output (gateway IP will be your router\u0026#39;s Tailscale IP): # 192.168.100.0/24 via \u0026lt;ROUTING_NODE_TAILSCALE_IP\u0026gt; dev tailscale0 scope link With subnet routing set up, you can now access any device on your home network\u0026rsquo;s 192.168.100.0/24 subnet from any Tailscale client with --accept-routes enabled. Whether you\u0026rsquo;re at work, a coffee shop, or traveling, you can reach your home devices using their local IP addresses.\nSummary # Tailscale and Headscale work well for stability, ease of use, and NAT traversal. They beat many older or more complex VPN options. This comes from Tailscale\u0026rsquo;s work on user-space NAT traversal techniques, especially their DERP server network. They wrote a good article about this: How NAT traversal works.\nHere\u0026rsquo;s a simple diagram showing how Tailscale nodes connect, with help from a coordination server (Headscale) and DERP relays for difficult NAT situations:\nTailscale/Headscale network connectivity This guide should help you build your own private, secure VPN with Headscale. You get full control over your network.\nMy next article will cover how to set up custom DERP servers with Headscale. This will improve connection quality, especially for devices behind difficult firewalls, and give you complete control over your VPN infrastructure.\n","date":"21 March 2022","externalUrl":null,"permalink":"/en/posts/how-to-set-up-or-migrate-headscale/","section":"Posts","summary":"Headscale is an open-source server that works like Tailscale\u0026rsquo;s control server. You can run it yourself instead of using Tailscale\u0026rsquo;s hosted service. This gives you full control over your VPN","title":"Headscale Deployment and Usage Guide: Mastering Tailscale's Self-Hosting Basics for Ultimate Control","type":"posts"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/categories/network/","section":"Categories","summary":"","title":"Network","type":"categories"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/tags/tailscale/","section":"Tags","summary":"","title":"Tailscale","type":"tags"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/series/tailscale-series/","section":"Series","summary":"","title":"Tailscale Series","type":"series"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/categories/vpn/","section":"Categories","summary":"","title":"VPN","type":"categories"},{"content":"","date":"2022年3月21日","externalUrl":null,"permalink":"/tags/vpn%E6%95%99%E7%A8%8B/","section":"标签","summary":"","title":"VPN教程","type":"tags"},{"content":"","date":"21 March 2022","externalUrl":null,"permalink":"/en/tags/wireguard/","section":"Tags","summary":"","title":"WireGuard","type":"tags"},{"content":"","date":"2022年2月20日","externalUrl":null,"permalink":"/tags/macos/","section":"标签","summary":"","title":"macOS","type":"tags"},{"content":"","date":"2022年2月20日","externalUrl":null,"permalink":"/tags/minecraft/","section":"标签","summary":"","title":"minecraft","type":"tags"},{"content":"Apple 在去年年底发布了 M1 Max 芯片，这款芯片的性能在 M1 的基础上又上升了一个等级，作为一名伪果粉，我果断在第一时间入手了一台 32G 的 M1 Max。\n收到电脑之后，我当然是装上了世界上最屌炸天的游戏 Minecraft。但 Minecraft 目前只支持 x86_64 架构，不支持 ARM，准确地说是只支持 x86_64 架构的 Java，因为 macOS 的 Minecraft 是通过 Java 来运行的。\n这肯定不行啊，既然已经用 M1 Max 了，我怎么能忍受通过 Rosetta 转译来玩游戏呢，当然是 ARM 架构的原生 Minecraft 更高端大气上档次啦。\n经过我的摸索，现已完美解决问题，步骤如下。\n安装 ARM 版 Java # 要想运行 Minecraft 时无需经过 Rosetta 转译，当然是要使用 ARM64 版本的 Java 了。好在 Zulu 提供了 ARM64 版本的 Java，只需要进入其 下载页面，依次选择 「Java 17」-「macOS」-「ARM 64-bit」-「JDK FX」，在右侧选择 .dmg 文件下载并安装。\n安装完成后，可以通过运行命令 /usr/libexec/java_home -V 来查看系统中安装的所有 Java 的版本。\n$ /usr/libexec/java_home -V Matching Java Virtual Machines (3): 17.0.1 (arm64) \u0026#34;Azul Systems, Inc.\u0026#34; - \u0026#34;Zulu 17.30.15\u0026#34; /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home ... /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home 如果你的系统中有多个 Java 版本，这里都会显示出来，其中 17.0.1 这一行就是之前安装的 Zulu JDK 17。我们可以通过修改 ~/.zshrc 来设置 JAVA_HOME 环境变量，改变系统默认的 Java 版本。\n将下面的内容添加到 ~/.zshrc 末尾。\n# ~/.zshrc export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home 执行以下命令使设置生效。\n$ source ~/.zshrc # 或者 $ . ~/.zshrc 下载 HMCL Launcher # HMCL Launcher 是一款非常流行的第三方启动器，支持很多自定义的功能，比如快速安装 Fabric 和 Forge、修改运行参数、设置 Java 版本、管理 Mod 等功能。除此之外还支持登录正版的微软账号。\n当然，这些都不是最重要的，重点是咱不需要购买账号就可以玩了，简直是白嫖党的福音。\n首先到 HMCL 官方网站或者 GitHub Releases 页面下载启动器，然后建立一个专门的游戏目录（例如~/Games/Minecraft），将启动器放到这个目录下。\n$ mkdir -p ~/Games/Minecraft/ $ mv ~/Downloads/HMCL-3.5.2.218.jar ~/Games/Minecraft $ java -jar HMCL-3.5.2.218.jar # 打开HMCL 打开 HMCL Launcher，进 版本列表 -\u0026gt; 安装新游戏版本，安装 1.17.1 版本 Minecraft，并同时安装 Fabric。\n获取 LWJGL 库文件 # LWJGL 全称为： LightWight Java Game Library，意为轻量级 Java 游戏工具库。包含 OpenGL 、OpenCL、OpenAL、Vulkan API 对 Java 平台的原生绑定。\n由于 Apple 发布的 M1 芯片移除了 LWJGL 库所依赖的个别 API，也未提供任何兼容方式，致使 Forge 和 Fabric 均无法正常工作，因此需要从源码编译最新的 LWJGL 库。虽然已经有人编译好了，但版本有点老了，最后更新日期还停留在 2020 年，而且不支持 HMCL 启动器，详情可查看 m1-multimc-hack 仓库。\n好在 Tanmay Bakshi 的 Gist 教程留言区有人提供了较新的 3.3.x 版本的 LWJGL 库，经过我的测试，可以完美运行，我们可以直接使用他提供的库文件。不过该网友提供的链接是 MediaFire 网盘，如果你无法访问，可以通过我提供的 网盘链接下载。\n将下载完成的 m1_lwjgl_330_nightly.zip 解压，将解压后文件夹内的 lwjglfat.jar 放入 Minecraft 运行目录。\n# 进入游戏目录 $ cd ~/Games/Minecraft # 将 Minecraft 运行目录内原有库文件删除（或备份） $ rm .minecraft/libraries/org/lwjgl/lwjgl/3.2.1/lwjgl-3.2.1.jar # 将下载的 LWJGL 库放入 Minecraft 运行目录 $ mv m1_lwjgl_330_nightly/lwjglfat.jar .minecraft/libraries/org/lwjgl/lwjgl/3.2.1/lwjgl-3.2.1.jar # 将 m1_lwjgl_330_nightly 文件夹移到 Minecraft 运行目录中 $ mv m1_lwjgl_330_nightly ~/Games/Minecraft 修改 HMCL 参数 # 打开『游戏全局设置』，检查 Java 路径是否正确，滑动至页面底部，在「调试选项」-「本地库路径」中，选择自定义库路径为 m1_lwjgl_330_nightly 目录内的 lwjglnatives 目录（例如，本文的路径是 ~/Games/Minecraft/m1_lwjgl_330_nightly/lwjglnatives），开启「不检查游戏完整性」，同时也需要开启「不检查 JVM 与游戏的兼容性」。\n运行游戏 # 回到启动器首页，点击右下角的『启动游戏』。\n可以看到 Minecraft 已经可以正常运行了，也能正常加载 Fabric API 和第三方 Mod。\n我总共开了 40 个模组。\n经过测试，启动时间在 20s 之内，游戏内也很顺畅，我用到至今还没有出现过崩溃现象。CPU 占用 50%，内存设置为自动分配，实际占用 5.48G。\n结语 # Minecraft 中文名又叫《我的世界》，它提供了一个和现实世界物理规律高度一致的虚拟世界，你可以在这个世界里为所欲为，利用这个世界里的资源和物理规律创造一切。举个例子，有人在这个世界里创造了计算机，有人在这个世界里以 1:1 的比例还原了现实世界的故宫，还有人在这个世界里创造了一部手机，然后和现实世界的自己视频通话。。。我每每想到这个视频通话的例子，心中就喊出一句卧槽，无法用语言来形容，自己体会。\n最近元宇宙的概念非常火热，成为了众多国内外科技巨头的抢手货，他们纷纷在各自领域布局未来的元宇宙计划。Minecraft 其实就非常有可能发展为元宇宙的载体，它有着非常高的用户基础，共识性强，而且背靠微软老爹，前不久疫情期间，伯克利学院还在 Minecraft 中举办了毕业典礼，看看这阵仗，妥妥的元宇宙啊。\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 参考资料 # 在 M1 Macbook 上不使用 Rosetta 优雅地游玩 Minecraft+Forge 在 M1 Mac 设备中解决 Minecraft Error 255 ","date":"2022年2月20日","externalUrl":null,"permalink":"/posts/minecraft-m1/","section":"博客","summary":"Apple 在去年年底发布了 M1 Max 芯片，这款芯片的性能在 M1 的基础上又上升","title":"在 M1 Macbook 中使用原生 Java 运行 Minecraft","type":"posts"},{"content":"","date":"2022年2月16日","externalUrl":null,"permalink":"/tags/conatiners/","section":"标签","summary":"","title":"Conatiners","type":"tags"},{"content":"","date":"2022年2月16日","externalUrl":null,"permalink":"/tags/containerd/","section":"标签","summary":"","title":"Containerd","type":"tags"},{"content":" 原文链接： https://container42.com/2022/01/10/shim-shiminey-shim-shiminey/\nKubernetes 1.20 版开始废除了对 dockershim 的支持，改用 Containerd 作为默认的容器运行时。本文将介绍 Containerd 中的 \u0026ldquo;shim\u0026rdquo; 接口。\n每一个 Containerd 或 Docker 容器都有一个相应的 \u0026ldquo;shim\u0026rdquo; 守护进程，这个守护进程会提供一个 API，Containerd 使用该 API 来管理容器基本的生命周期（启动/停止），在容器中执行新的进程、调整 TTY 的大小以及与特定平台相关的其他操作。shim 还有一个作用是向 Containerd 报告容器的退出状态，在容器退出状态被 Containerd 收集之前，shim 会一直存在。这一点和僵尸进程很像，僵尸进程在被父进程回收之前会一直存在，只不过僵尸进程不会占用资源，而 shim 会占用资源。\nshim 将 Containerd 进程从容器的生命周期中分离出来，具体的做法是 runc 在创建和运行容器之后退出，并将 shim 作为容器的父进程，即使 Containerd 进程挂掉或者重启，也不会对容器造成任何影响。这样做的好处很明显，你可以高枕无忧地升级或者重启 Containerd，不会对运行中的容器产生任何影响。Docker 的 \u0026ndash;live-restore 特征也实现了类似的功能。\nContainerd 支持哪些 shim？ # Containerd 目前官方支持的 shim 清单：\nio.containerd.runtime.v1.linux # io.containerd.runtime.v1.linux 是最原始的 shim API 和实现的 v1 版本，在 Containerd 1.0 之前被设计出来。该 shim 使用 runc 来执行容器，并且只支持 cgroup v1。目前 v1 版 shim API 已被废弃，并将于 Containerd 2.0 被删除。\nio.containerd.runc.v1 # io.containerd.runc.v1 与 io.containerd.runtime.v1.linux 的实现类似，唯一的区别是它使用了 v2 版本 shim API。该 shim 仍然只支持 cgroup v1。\nio.containerd.runc.v2 # 该 shim 与 v1 采用了完全不同的实现，并且使用了 v2 版本 shim API，同时支持 cgroup v1 和 v2。该 shim 进程以运行多个容器，用于 Kubernetes 的 CRI 实现，可以在一个 Pod 中运行多个容器。\nio.containerd.runhcs.v1 # 这是 Windows 平台的 shim，使用 Window 的 HCSv2 API 来管理容器。\n当然，除了官方正式支持的 shim 之外，任何人都可以编写自己的 shim，并让 Containerd 调用该 shim。Containerd 在调用时会将 shim 的名称解析为二进制文件，并在 $PATH 中查找这个二进制文件。例如 io.containerd.runc.v2 会被解析成二进制文件 containerd-shim-runc-v2，io.containerd.runhcs.v1 会被解析成二进制文件 containerd-shim-runhcs-v1.exe。客户端在创建容器时可以指定使用哪个 shim，如果不指定就使用默认的 shim。\n下面是一个示例，用来指定将要使用的 shim：\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;github.com/containerd/containerd\u0026#34; \u0026#34;github.com/containerd/containerd/namespaces\u0026#34; \u0026#34;github.com/containerd/containerd/oci\u0026#34; v1opts \u0026#34;github.com/containerd/containerd/pkg/runtimeoptions/v1\u0026#34; ) func main() { ctx := namespaces.WithNamespace(context.TODO(), \u0026#34;default\u0026#34;) // Create containerd client client, err := containerd.New(\u0026#34;/run/containerd/containerd.sock\u0026#34;) if err != nil { panic(err) } // Get the image ref to create the container for img, err := client.GetImage(ctx, \u0026#34;docker.io/library/busybox:latest\u0026#34;) if err != nil { panic(err) } // set options we will pass to the shim (not really setting anything here, but we could) var opts v1opts.Options // Create a container object in containerd cntr, err := client.NewContainer(ctx, \u0026#34;myContainer\u0026#34;, // All the basic things needed to create the container containerd.WithSnapshotter(\u0026#34;overlayfs\u0026#34;), containerd.WithNewSnapshot(\u0026#34;myContainer-snapshot\u0026#34;, img), containerd.WithImage(img), containerd.WithNewSpec(oci.WithImageConfig(img)), // Set the option for the shim we want containerd.WithRuntime(\u0026#34;io.containerd.runc.v1\u0026#34;, \u0026amp;opts), ) if err != nil { panic(err) } // cleanup cntr.Delete(ctx) } ⚠️注意：WithRuntime 将 interface{} 作为第二个参数，可以传递任何类型给 shim。只要确保你的 shim 能够识别这个类型的数据，并在 typeurl 包中注册这个类型，以便它能被正确编码。\n每个 shim 都有自己支持的一组配置选项，可以单独针对每个容器进行配置。例如 io.containerd.runc.v2 可以将容器的 stdout/stderr 转发到一个单独的进程，为 shim 的运行设置自定义的 cgroup 等等。你可以创建自定义的 shim，在容器运行时添加自定义的选项。总的来说，shim 的 API 包含了 RPC 和一些二进制调用用于创建/删除 shim，以及到 Containerd 进程的反向通道。\n如果你想实现自己的 shim，下面是相关参考资料：\n(v2) shim RPC API 的详细定义 实现 shim 二进制和RPC API的辅助工具 shim 的使用方式 你只需要实现一个接口，shim.Run 会处理剩下的事情。shim 需要重点关注的是内存使用，因为每个容器都有一个 shim 进程，随着容器数量的增加，shim 的内存使用会急剧上升。shim 的 API 是在 protobuf 中定义的，看起来有点像 gRPC 的 API，但实际上 shim 使用的是一个叫做 ttrpc 的自定义协议，与 gRPC 并不兼容。ttrpc 是一个原 RPC 协议，专为降低内存使用而设计。\n创建容器的 RPC 调用流程 # Containerd 中有一个 container 对象，当你创建一个 container 对象，只是创建了一些与容器相关的数据，并将这些数据存储到本地数据库中，并不会在系统中启动任何容器。container 对象创建成功后，客户端会从 container 对象中创建一个 task，接下来是调用 shim API。\n以下是 RPC 调用的总体流程：\n客户端调用 container.NewTask(…)，containerd 根据指定或默认的运行时名称解析 shim 二进制文件，例如：io.containerd.runc.v2 -\u0026gt; containerd-shim-runc-v2。\ncontainerd 通过 start 命令启动 shim 二进制文件，并加上一些额外的参数，用于定义命名空间、OCI bundle 路径、调试模式、返回给 containerd 的 unix socket 路径等。在这一步调用中，当前工作目录设置为 shim 的工作路径。\n此时，新创建的 shim 进程会向 stdout 写一个连接字符串，以允许 containerd 连接到 shim ，进行 API 调用。一旦连接字符串初始化完成，shim 开始监听之后，start 命令就会返回。\ncontainerd 使用 shim start 命令返回的连接字符串，打开一个与 shim API 的连接。\ncontainerd 使用 OCI bundle 路径和其他选项，调用 Create shim RPC。这一步会创建所有必要的 沙箱，并返回沙箱进程的 pid。以 runc 为例，我们使用 runc create --pid-file=\u0026lt;path\u0026gt; 命令创建容器，runc 会分叉出一个新进程（runc init）用来设置沙箱，然后等待调用 runc start，所有这些都准备好后，runc create 命令就会返回结果。在 runc create 返回结果之前，runc 会将 runc-init 进程的 pid 写入定义的 pid 文件中，客户端可以使用这个 pid 来做一些操作，比如在沙箱中设置网络（网络命名空间可以在 /proc/\u0026lt;pid\u0026gt;/ns/net 中设置）。\ncreate 调用还会提供一个挂载列表以构建 rootfs，还包含 checkpoint 信息。\n下一步客户端调用 task.Wait，触发 containerd 调用 shim Wait API。这是一个持久化的请求，只有在容器退出后才会返回。到这一步仍然不会启动容器。\n客户端继续调用 task.Start，触发 containerd 调用 Start shim RPC。这一步才会真正启动容器，并返回容器进程的 pid。\n这一步，客户端就可以针对 task 进行一些额外的调用请求。例如，如果 task 包含 TTY，会请求 task.ResizePTY，或者请求 task.Kill 来发送一个信号等等。\ntask.Exec 比较特殊，它会调用 shim Exec RPC，但并没有在容器中执行某个进程，只是在 shim 中注册了 exec，后面会使用 exec ID 来调用 shim Start RPC。\n在容器或 exec 进程退出后，containerd 将会调用 shim Delete RPC，清理 exec 进程或容器的所有资源。例如，对于runc shim， 这一步会调用 runc delete。\ncontainerd 调用 Shutdown RPC，此时 shim 将会退出。\nshim 的另一个重要部分是将容器的生命周期事件返回给 containerd ，包括： TaskCreate TaskStart TaskDelete TaskExit, TaskOOM, TaskExecAdded, TaskExecStarted, TaskPaused, TaskResumed, TaskCheckpointed。可参考 task 的详细定义。\n总结 # Containerd 通过 shim 为底层的容器运行时提供了可插拔能力。虽然这不是使用 Containerd 管理容器的唯一手段，但目前内置的 TaskService 使用了该方式，Kubernetes 通过调用 CRI 来创建 Pod 也是使用的 shim。由此可见 shim 这种方式很受欢迎，它不但增强了 Containerd 的扩展能力，以支持更多平台和基于虚拟机的运行时（ firecracker, kata），而且允许尝试其他 shim 实现（ systemd）。\n","date":"2022年2月16日","externalUrl":null,"permalink":"/posts/shim-shiminey-shim-shiminey/","section":"博客","summary":"原文链接： https://container42.com/2022/01/10/shim-shiminey-shim-shiminey/ Kubernetes 1.20 版开始废除了对 dockershim 的支持，改用 Containerd 作为默认的容器","title":"Containerd shim 原理深入解读","type":"posts"},{"content":"","date":"2021年11月9日","externalUrl":null,"permalink":"/tags/netmaker/","section":"标签","summary":"","title":"Netmaker","type":"tags"},{"content":" 熟悉我的小伙伴都知道我是一名与时俱进的 WireGuard 舔狗，我早就把所有的跨云组网都换成了 WireGuard。\nWireGuard 利用内核空间处理来提升性能（更高吞吐和更低延迟），同时避免了不必要的内核和用户空间频繁上下文切换开销。在 Linux 5.6 将 WireGuard 合并入上游之后， OpenVPN 无论做什么，也无法逆转大部队向 WireGuard 迁移之大趋势，所谓历史之潮流。\n不要再跟我提 OpenVPN 了，你们农村人才用 OpenVPN，我们城里人早就换上了 WireGuard！（此处只是开个玩笑，别当真哈😂）\n言归正传，我在 上篇文章中介绍了 Netmaker 的工作原理和功能解读，本篇文章将会介绍如何使用 Netmaker 来配置 WireGuard 全互联模式。\n此前我单独用了整篇文章来给大家介绍 Netmaker 是个什么东西，它的架构和工作原理是什么，以及如何部署 Netmaker。所有的这些内容都是为了今天的文章做铺垫，本文要讲的内容才是真正的杀手锏。假定你已经通读了我的 上一篇文章，并且按照文中所述步骤部署好了 Netmaker。如果你还没有做好这些准备工作，建议先去准备一下，再来阅读本篇文章。\n好，我们已经部署好了 Netmaker，但它只负责存储和管理各个节点的 WireGuard 配置和状态信息，真正的主角还是通过 WireGuard 私有网络进行通信的节点。节点通常是运行 Linux 的服务器，它需要安装 netclient 和 WireGuard。这个节点会通过 WireGuard 私有网络和其他所有节点相连。一但节点被添加到私有网络中，Netmaker 管理员就可以操控该节点的配置。\n光说不练假把式，为了让大家更容易带入，咱们还是来模拟一下实际场景。假设我有 4 个不同的节点，这 4 个节点的操作系统分别是 Ubuntu、macOS、OpenWrt 和 Android，且分别处于不同的局域网中，即每个节点的公网出口都不同。先来看下架构图：\n创建网络 # 加入节点之前，需要先在 Netmaker 中创建一个网络。一般我们会将这个新创建的网络命名为 default，但我的环境中已经存在了该网络，所以我将重新创建一个网络为大家演示。\n先创建一个网络，命名为 demo。\n创建完成后，还可以继续修改该网络的相关元数据，比如允许节点在不使用秘钥的情况下加入 VPN 网络。\n加入节点 # 如果部署 Netmaker 时开启了环境变量 CLIENT_MODE: \u0026quot;on\u0026quot;，Netmaker 就会将自身所在的主机也作为一个网络节点，名字默认为 netmaker。\n其他节点的加入流程也很简单，但不同的操作系统又不尽相同。\nUbuntu # 常规的 Linux 发行版最简单，直接下载二进制文件，赋予可执行权限。\n$ wget https://github.com/gravitl/netmaker/releases/download/latest/netclient $ chmod +x netclient 然后执行下面的命令将节点加入网络。\n$ ./netclient join --dnson no --name \u0026lt;HOSTNAME\u0026gt; --network demo --apiserver \u0026lt;Netmaker_IP\u0026gt;:8081 --grpcserver \u0026lt;Netmaker_IP\u0026gt;:50051 将 \u0026lt;HOSTNAME\u0026gt; 替换成你的节点名称，你也可以设置成别的名字。 将 \u0026lt;Netmaker_IP\u0026gt; 替换为 Netmaker Server 的公网 IP。 到 Netmaker UI 中批准加入节点的请求。\n批准之后就可以看到两个节点之间已经握手成功了。\n如果没有握手成功，你需要检查一下 Netmaker 的防火墙是否放行了 UDP 端口（本文是 51821 端口）。\n对于 WireGuard 而言，一般情况下通信双方只需一个节点开放固定的公网端口即可，另一个节点的防火墙可以不放行 UDP 端口。所以这里只需开启 Netmaker Server 所在主机的 UDP 端口即可。\n同时还会设置一个计划任务，来定期（每 15 秒执行一次）启动守护进程执行签到命令，签到的作用是将本地的配置与 Netmaker Server 托管的配置进行比较，根据比较结果进行适当修改，再拉取所有的 Peer 列表，最后重新配置 WireGuard。\n$ cat /etc/systemd/system/netclient.timer [Unit] Description=Calls the Netmaker Mesh Client Service Requires=netclient.service [Timer] Unit=netclient.service OnCalendar=*:*:0/15 [Install] WantedBy=timers.target $ systemctl status netclient.timer ● netclient.timer - Calls the Netmaker Mesh Client Service Loaded: loaded (/etc/systemd/system/netclient.timer; enabled; vendor preset: enabled) Active: active (running) since Sat 2021-10-09 01:34:27 CST; 4 weeks 1 days ago Trigger: n/a Triggers: ● netclient.service Oct 09 01:34:27 blog-k3s04 systemd[1]: Started Calls the Netmaker Mesh Client Service. $ cat /etc/systemd/system/netclient.service [Unit] Description=Network Check Wants=netclient.timer [Service] Type=simple ExecStart=/etc/netclient/netclient checkin -n all [Install] WantedBy=multi-user.target $ systemctl status netclient.service ● netclient.service - Network Check Loaded: loaded (/etc/systemd/system/netclient.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2021-11-07 15:00:54 CST; 11ms ago TriggeredBy: ● netclient.timer Main PID: 3390236 (netclient) Tasks: 5 (limit: 19176) Memory: 832.0K CGroup: /system.slice/netclient.service └─3390236 /etc/netclient/netclient checkin -n all Nov 07 15:00:54 blog-k3s04 systemd[1]: Started Network Check. Nov 07 15:00:54 blog-k3s04 netclient[3390236]: 2021/11/07 15:00:54 [netclient] running checkin for all networks macOS # 如果是 Intel CPU，可以直接到 Releases 页面下载可执行文件。如果是 M1 系列芯片（包含 M1 Pro 和 M1 Max），需要自己从源码编译：\n$ git clone https://github.com/gravitl/netmaker $ cd netmaker/netclient $ go build -a -ldflags=\u0026#34;-s -w\u0026#34; . 安装 WireGuard 命令行工具：\n$ brew install wireguard-tools 下面的步骤就和 Ubuntu 一样了，执行以下命令将节点加入网络。\n$ sudo ./netclient join --dnson no --name \u0026lt;HOSTNAME\u0026gt; --network demo --apiserver \u0026lt;Netmaker_IP\u0026gt;:8081 --grpcserver \u0026lt;Netmaker_IP\u0026gt;:50051 再到 Netmaker UI 中批准加入节点的请求，批准之后就可以看到各个节点之间已经握手成功了。\n$ sudo wg interface: utun5 public key: 2sGnrXTY1xb+cWMR+ZXfBLZqmpDtYCNtKdQ3Cm6gBAs= private key: (hidden) listening port: 61259 peer: X2LTMBX8fyXyCrCVFcJMDKVBtPcfJHT24lwkQQRSykg= endpoint: 121.36.134.95:51821 allowed ips: 10.8.0.1/32 latest handshake: 37 seconds ago transfer: 216 B received, 732 B sent persistent keepalive: every 20 seconds peer: Z6oCQdV5k4/AVXsUhhGNW69D2hnqcgJe7i3w8qzGJBY= endpoint: 103.61.37.238:55730 allowed ips: 10.8.0.2/32 latest handshake: 1 minute, 47 seconds ago transfer: 1.30 KiB received, 2.99 KiB sent persistent keepalive: every 20 seconds 除了 Netmaker Server 节点之外，Ubuntu 节点和 macOS 节点的 UDP 监听端口都是随机的，而且他们的防火墙都没有放行相应的 UDP 端口，竟然也握手成功了！那是因为他们都开启了 UDP 打洞，这就是 UDP 打洞的神奇之处。\n我们可以来验证下 macOS 和 Ubuntu 之间的连通性：\n$ ping 10.8.0.2 -c 2 PING 10.8.0.2 [局域网 IP] (10.8.0.2 [局域网 IP]): 56 data bytes 64 bytes from 10.8.0.2 [局域网 IP]: icmp_seq=0 ttl=64 time=44.368 ms 64 bytes from 10.8.0.2 [局域网 IP]: icmp_seq=1 ttl=64 time=44.065 ms --- 10.8.0.2 [局域网 IP] ping statistics --- 2 packets transmitted, 2 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 44.065/44.216/44.368/0.152 ms 完美，即使 macOS 位于 NAT 后面，防火墙没有配置 UDP 端口转发，对等节点也没有放行相应 UDP 端口，双方仍然能够握手成功。\nmacOS 的守护进程是通过 launchctl 来配置的，netclient 在 macOS 中也会创建一个守护进程来定时同步配置。\n$ sudo launchctl list com.gravitl.netclient { \u0026#34;StandardOutPath\u0026#34; = \u0026#34;/etc/netclient/com.gravitl.netclient.log\u0026#34;; \u0026#34;LimitLoadToSessionType\u0026#34; = \u0026#34;System\u0026#34;; \u0026#34;StandardErrorPath\u0026#34; = \u0026#34;/etc/netclient/com.gravitl.netclient.log\u0026#34;; \u0026#34;Label\u0026#34; = \u0026#34;com.gravitl.netclient\u0026#34;; \u0026#34;OnDemand\u0026#34; = true; \u0026#34;LastExitStatus\u0026#34; = 0; \u0026#34;Program\u0026#34; = \u0026#34;/etc/netclient/netclient\u0026#34;; \u0026#34;ProgramArguments\u0026#34; = ( \u0026#34;/etc/netclient/netclient\u0026#34;; \u0026#34;checkin\u0026#34;; \u0026#34;-n\u0026#34;; \u0026#34;all\u0026#34;; ); }; 守护进程的配置文件在 /Library/LaunchDaemons/com.gravitl.netclient.plist 目录下：\n$ sudo cat /Library/LaunchDaemons/com.gravitl.netclient.plist \u0026lt;?xml version=\u0026#39;1.0\u0026#39; encoding=\u0026#39;UTF-8\u0026#39;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \\\u0026#34;-//Apple Computer//DTD PLIST 1.0//EN\\\u0026#34; \\\u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\\\u0026#34; \u0026gt; \u0026lt;plist version=\u0026#39;1.0\u0026#39;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt;\u0026lt;string\u0026gt;com.gravitl.netclient\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/etc/netclient/netclient\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;checkin\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-n\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;all\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;StandardOutPath\u0026lt;/key\u0026gt;\u0026lt;string\u0026gt;/etc/netclient/com.gravitl.netclient.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt;\u0026lt;string\u0026gt;/etc/netclient/com.gravitl.netclient.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;AbandonProcessGroup\u0026lt;/key\u0026gt;\u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;StartInterval\u0026lt;/key\u0026gt; \u0026lt;integer\u0026gt;15\u0026lt;/integer\u0026gt; \u0026lt;key\u0026gt;EnvironmentVariables\u0026lt;/key\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;PATH\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 其中有一段配置内容如下：\n\u0026lt;key\u0026gt;StartInterval\u0026lt;/key\u0026gt; \u0026lt;integer\u0026gt;15\u0026lt;/integer\u0026gt; 表示每过 15 秒执行签到命令来同步配置。\nOpenWrt # 虽然 OpenWrt 也是 Linux 发行版，但目前 netclient 的可执行文件还不能在 OpenWrt 中运行，这和 C 语言的动态链接库有关，OpenWrt 中缺失了很多 C 语言动态链接库。为了解决这个问题，我们可以关闭对 C 语言外部依赖的调用，手动编译出纯静态的可执行文件。\n你可以找一台常规的 Linux 发行版或者 macOS 来编译：\n$ git clone https://github.com/gravitl/netmaker $ cd netmaker/netclient $ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags=\u0026#34;-s -w\u0026#34; . 如果你的 OpenWrt 跑在其他 CPU 架构上，需要将 GOARCH 的值替换为相应的 CPU 架构。\n编译成功后，可以检查一下可执行文件的类型和 CPU 架构：\n$ file netclient netclient: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=QWXj97OoEpN-Sm97lim2/ZtJJHaG77M3fYSMqtFGK/YPVj2xx-KdNyYT8YEZ8W/i9CliPF-AqUNcTy2ZKpA, stripped 如果确认无误，就可以将其拷贝到 OpenWrt 主机上了，例如：\n$ scp netclient root@\u0026lt;Openwrt_IP\u0026gt;:/root/ 接下来就可以登录到 OpenWrt 将节点加入网络了：\n$ ./netclient join --dnson no --name \u0026lt;HOSTNAME\u0026gt; --daemon off --network demo --apiserver \u0026lt;Netmaker_IP\u0026gt;:8081 --grpcserver \u0026lt;Netmaker_IP\u0026gt;:50051 这里相比于之前的节点多了一个参数 --daemon off，禁用了守护进程，因为 OpenWrt 不支持 Systemd。如果你坚持开启守护进程，那么加入网络时就会报错，所以必须要加这个参数。\n和之前的步骤一样，到 Netmaker UI 中批准加入节点的请求，批准之后就可以看到各个节点之间已经握手成功了。\n$ wg interface: nm-demo public key: sfrfimG++xk7X0AU5PrZs9p6PYith392ulhmL2OhPR8= private key: (hidden) listening port: 42655 peer: Z6oCQdV5k4/AVXsUhhGNW69D2hnqcgJe7i3w8qzGJBY= endpoint: 103.61.37.238:55730 allowed ips: 10.8.0.2/32 latest handshake: 5 seconds ago transfer: 488 B received, 1.39 KiB sent persistent keepalive: every 20 seconds peer: X2LTMBX8fyXyCrCVFcJMDKVBtPcfJHT24lwkQQRSykg= endpoint: 121.36.134.95:51821 allowed ips: 10.8.0.1/32 latest handshake: 7 seconds ago transfer: 568 B received, 488 B sent persistent keepalive: every 20 seconds peer: 2sGnrXTY1xb+cWMR+ZXfBLZqmpDtYCNtKdQ3Cm6gBAs= endpoint: 192.168.100.90:57183 allowed ips: 10.8.0.3/32 latest handshake: 1 minute, 35 seconds ago transfer: 1.38 KiB received, 3.46 KiB sent persistent keepalive: every 20 seconds 由于我的 macOS 和 OpenWrt 在同一个局域网中，所以他们之间的 endpoint 都自动设置成了内网地址，太神奇啦！\n到这里还没完，要想让 OpenWrt 动态更新配置，还需要手动实现一个计划任务来定期签到。我们选择使用 Crontab 来实现这个目的，直接添加两个计划任务：\n$ cat \u0026lt;\u0026lt;EOF \u0026gt;\u0026gt; /etc/crontabs/root * * * * * /etc/netclient/netclient checkin --network all \u0026amp;\u0026gt; /dev/null * * * * * sleep 15; /etc/netclient/netclient checkin --network all \u0026amp;\u0026gt; /dev/null EOF 这两个计划任务变相实现了 “每隔 15 秒执行一次签到” 的目的。\nAndroid # Netclient 目前只支持 Linux、macOS 和 Windows，如果 Android 和 iOS 端想要加入 VPN 私有网络，只能通过 WireGuard 原生客户端来进行连接。要想做到这一点，需要管理员事先创建一个 External Client，它会生成一个 WireGuard 配置文件，WireGuard 客户端可以下载该配置文件或者扫描二维码进行连接。\n当然，在创建 External Client 之前，需要先设置其中一个节点为 Ingress Gateway。\n需要说明的是，目前移动设备通过 External Client 接入只是权宜之计，随着 Netclient 对更多操作系统的支持，最终所有的客户端都应该使用 netclient 来连接。\n最终所有的节点之间实现了全互联模式，每个节点都和其他节点直连，不需要第三方节点进行中转。当然，目前移动设备还是要通过 Ingress Gateway 进行中转。\n打通内网 # 到目前为止我们只是打造了一个点对点的 Mesh 网络，各个节点之间都可以通过 WireGuard 的私有网络 IP 进行直连。但我们可以更大胆一点，让每个节点都能访问其他节点的局域网 IP。以 OpenWrt 为例，假设 OpenWrt 跑在家中，家中的局域网 IP 为 192.168.100.0/24，如何让其他所有节点都能访问这个局域网呢？\n其实也很简单，可以将某个节点设置为 Egress Gateway（出口网关），允许将内部网络的流量转发到外部指定的 IP 范围。这里的内部指的是 WireGuard 私有网络，本文中就是 10.8.0.0/16；外部网络指的是其他网段，比如局域网 IP。\n操作步骤很傻瓜化，先点击 OpenWrt 节点左边的 “MAKE openwrt AN EGRESS GATEWAY MODE?”：\n填写局域网的网段和出口网卡，如果你有多个网段需要打通（比如 OpenWrt 上的容器网段），可以用 \u0026ldquo;,\u0026rdquo; 隔开。\n配置完成后，就会在 OpenWrt 节点配置的 Postup 和 Postdown 中添加相关的 iptables 规则。\n具体的规则为：\n# Postup iptables -A FORWARD -i nm-demo -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE # Postdown iptables -D FORWARD -i nm-demo -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 很简单，想必就不用我再解释了。\n除了添加 Postup 和 Postdown 之外，还会在其他节点 WireGuard 配置的 AllowedIps 中添加 OpenWrt 的局域网网段：\n除此之外还会在其他所有节点中添加相关路由表：\n$ ip route|grep \u0026#34;192.168.100.0/24\u0026#34; 192.168.100.0/24 dev wg0 scope nm-demo 最终所有的节点都可以访问 OpenWrt 的局域网 IP 了。\n大家可以根据我的例子举一反三，比如你用几台云主机搭建了 K8s 集群，如何在本地客户端和家中访问云上 K8s 集群的 Pod IP 和 Service IP 呢？不用我再解释了吧，相信你悟了。\n总结 # 本文详细介绍了如何使用 Netmaker 来配置 WireGuard 全互联模式，并打通指定节点的局域网，你也可以根据此方法来访问远程 K8s 集群中的 Pod。下一篇文章将会介绍如何使用 Cilium + Netmaker 来打造跨公有云的 K8s 集群。\n","date":"2021年11月9日","externalUrl":null,"permalink":"/posts/configure-a-mesh-network-with-netmaker-2/","section":"博客","summary":"熟悉我的小伙伴都知道我是一名与时俱进的 WireGuard 舔狗，我早就把所有的","title":"WireGuard 教程：使用 Netmaker 配置 WireGuard 全互联 (full mesh) 模式","type":"posts"},{"content":"大家好，我是米开朗基杨。\n关注我的读者应该都还记得我之前写过一篇 WireGuard 全互联模式 (full mesh) 的配置指南，限于当时还没有成熟的产品来帮助我们简化全互联模式的配置，所以我选择了使用可视化界面 wg-gen-web 来达成目的。但 wg-gen-web 的缺陷也很明显，它生成的每一个客户端的配置都要手动调整，终究还是不够便利。\n今天我将为大家介绍一种更加完美的工具来配置 WireGuard 的全互联模式，这个工具就是 Netmaker。\n由于篇幅原因，本系列文章将会分成两篇进行介绍。本篇文章介绍 Netmaker 的工作原理和功能解读；下一篇文章将会介绍如何使用 Netmaker 来配置 WireGuard 全互联模式。\nNetmaker 介绍 # Netmaker 是一个用来配置 WireGuard 全互联模式的可视化工具，它的功能非常强大，不仅支持 UDP 打洞、NAT 穿透、多租户，还可以使用 Kubernetes 配置清单来部署，客户端几乎适配了所有平台，包括 Linux, Mac 和 Windows，还可以通过 WireGuard 原生客户端连接 iPhone 和 Android，真香！\n其最新版本的基准测试结果显示，基于 Netmaker 的 WireGuard 网络速度比其他全互联模式的 VPN（例如 Tailscale 和 ZeroTier）网络速度快 50% 以上。\nNetmaker 架构 # Netmaker 使用的是 C/S 架构，即客户端/服务器架构。Netmaker Server 包含两个核心组件：用来管理网络的可视化界面，以及与客户端通信的 gRPC Server。你也可以可以选择部署DNS服务器（CoreDNS）来管理私有DNS。\n客户端（netclient）是一个二进制文件，可以在绝大多数 Linux 客户端以及 macOS 和 Windows 客户端运行，它的功能就是自动管理 WireGuard，动态更新 Peer 的配置。\n注意：这里不要将 Netmaker 理解成我之前的文章所提到的 中心辐射型网络拓扑。Netmaker Server 只是用来存储虚拟网络的配置并管理各个 Peer 的状态，Peer 之间的网络流量并不会通过 Netmaker Server。\nNetmaker 还有一个重要的术语叫签到，客户端会通过定时任务来不断向 Netmaker Server 签到，以动态更新自身的状态和 Peer 的配置，它会从 Netmaker Server 检索 Peer 列表，然后与所有的 Peer 建立点对点连接，即全互联模式。所有的 Peer 通过互联最终呈现出来的网络拓扑结构就类似于本地子网或 VPC。\nNetmaker 部署 # Netmaker 支持多种部署方式，包括二进制部署和容器化部署，容器化部署还支持 docker-compose 和 Kubernetes。如果你没有可以暴露服务到公网的 Kubernetes 集群，我推荐还是直接通过 docker-compose 来部署，简单粗暴。\n官方推荐的做法是使用 Caddy 或 Nginx 来反向代理 Netmaker UI、API Server 和 gRPC Server，但我的域名没有备案，我也怕麻烦，就直接通过公网 IP 来提供服务了。如果你也想通过公网域名来暴露 Netmaker 的服务，可以参考 Netmaker 的官方文档。\n本文的部署方案将直接通过公网 IP 来提供服务，首先需要安装 docker-compose，安装方法可以参考 Docker 官方文档。\n安装完 docker-compose 后，再下载 docker-compose 的 YAML 配置清单：\n$ wget https://cdn.jsdelivr.net/gh/gravitl/netmaker@master/compose/docker-compose.yml 现在还不能直接部署，需要根据自己的实际环境对配置清单进行修改。例如，我修改后的配置清单内容如下：\nversion: \u0026#34;3.4\u0026#34; services: netmaker: container_name: netmaker image: gravitl/netmaker:v0.8.2 volumes: - /etc/netclient/config:/etc/netclient/config - dnsconfig:/root/config/dnsconfig - /usr/bin/wg:/usr/bin/wg - /data/sqldata/:/root/data cap_add: - NET_ADMIN restart: always network_mode: host environment: SERVER_HOST: \u0026#34;\u0026lt;public_ip\u0026gt;\u0026#34; COREDNS_ADDR: \u0026#34;\u0026lt;public_ip\u0026gt;\u0026#34; GRPC_SSL: \u0026#34;off\u0026#34; DNS_MODE: \u0026#34;on\u0026#34; CLIENT_MODE: \u0026#34;on\u0026#34; API_PORT: \u0026#34;8081\u0026#34; GRPC_PORT: \u0026#34;50051\u0026#34; SERVER_GRPC_WIREGUARD: \u0026#34;off\u0026#34; CORS_ALLOWED_ORIGIN: \u0026#34;*\u0026#34; DATABASE: \u0026#34;sqlite\u0026#34; netmaker-ui: container_name: netmaker-ui depends_on: - netmaker image: gravitl/netmaker-ui:v0.8 links: - \u0026#34;netmaker:api\u0026#34; ports: - \u0026#34;80:80\u0026#34; environment: BACKEND_URL: \u0026#34;http://\u0026lt;public_ip\u0026gt;:8081\u0026#34; restart: always network_mode: host coredns: depends_on: - netmaker image: coredns/coredns command: -conf /root/dnsconfig/Corefile container_name: coredns restart: always network_mode: host volumes: - dnsconfig:/root/dnsconfig volumes: dnsconfig: {} 总共有以下几处改动：\n删除了不必要的环境变量，并修改了其中一部分环境变量，比如关闭 SSL 模式，将域名替换为公网 IP。你需要根据自己的实际环境将 \u0026lt;public_ip\u0026gt; 替换为你的公网 IP。 将所有容器的网络模式都改为 host 模式，即 network_mode: host。 将 sqlite 的数据存储改为 hostpath，即 /data/sqldata/:/root/data。 其中 CLIENT_MODE: \u0026quot;on\u0026quot; 表示将 Netmaker Server 所在的节点也作为 Mesh Network 的 Peer 节点。\n最后我们就可以通过配置清单来部署容器了：\n$ docker-compose up -d 查看是否部署成功：\n$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0daf3a35f8ce docker.io/coredns/coredns:latest \u0026#34;/coredns -conf /roo…\u0026#34; 7 days ago Up coredns 0dbb0158e821 docker.io/gravitl/netmaker-ui:v0.8 \u0026#34;/docker-entrypoint.…\u0026#34; 7 days ago Up netmaker-ui bd39ee52013e docker.io/gravitl/netmaker:v0.8.2 \u0026#34;./netmaker\u0026#34; 7 days ago Up netmaker 部署成功后，就可以在浏览器的地址栏输入你的公网 IP 来访问 Netmaker UI 了。\nNetmaker 功能解读 # 我们先通过 UI 来看看 Netmaker 都有哪些功能。\n网络（Networks） # Netmaker 允许创建任意数量的私有网络，可以设置任意地址范围。你只需要给这个网络起个名字，设置一个地址范围，并选择想要启用的选项。\n目前总共包含三个可选项：\nDual Stack : 双栈，即开启 IPv6。 Local Only : 各个 Peer 之间只会通过内网地址来互联，即 Endpoint 皆为内网地址。适用于数据中心、VPC 或家庭/办公网络的内部。 Hole Punching : 动态发现和配置 Endpoint 和端口，帮助 Peer 轻松穿透 NAT 进行 UDP 打洞。 管理员拥有对网络的最高控制器，例如，更改私有网络的网段，Peer 便会自动更新自身的 IP。\n如果发现网络被入侵，也可以让网络中的所有节点刷新公钥。\n节点（Nodes） # Node 表示节点，通常是运行 Linux 的服务器，安装了 netclient 和 WireGuard。这个节点会通过 WireGuard 私有网络和其他所有节点相连。一但节点被添加到私有网络中，Netmaker 管理员就可以操控该节点的配置，例如：\n私有网络地址 过期时间 WireGuard 相关设置 管理员也可以将该节点从私有网络中完全删除，让其无法连接其他所有 Peer 节点。\nNode 还有两个比较重要的功能，就是将自身设置为 Ingress Gateway（入口网关）或者 Egress Gateway（出口网关）。Ingress Gateway 允许外部客户端的流量进入内部网络，Egress Gateway 允许将内部网络的流量转发到外部指定的 IP 范围。这两项功能对全互联模式进行了扩展，比如手机客户端就可以通过 Ingress Gateway 接入进来。\n访问秘钥（Access Keys） # 一个节点想要加入到私有网络，需要获取访问秘钥进行授权，当然你也可以选择手动批准。\n一个访问秘钥可以被多个节点重复使用，你只需修改 Number 数量就可以实现这个目的。\n访问秘钥创建后只会显示一次，展示了三个选项：\n原始访问秘钥 访问令牌（access token），它将访问密钥与用于加入网络的参数（例如地址、端口和网络名称）包装在一起。当你运行 netclient join -t \u0026lt;token\u0026gt; 时，netclient 会对该令牌进行解码，并解析参数。 安装脚本，用于在标准 Linux 服务器上首次安装 netclient。它只是简单地下载 netclient 并为你运行 \u0026ldquo;join\u0026rdquo; 命令。 DNS # 如果启用了 DNS 组件，Netmaker 就会通过 CoreDNS 来维护私有 DNS，它会为私有网络中的每个节点创建一个默认的 DNS 条目。你也可以创建自定义的 DNS 条目。\n外部客户端（External Clients） # Netclient 目前只支持 Linux、macOS 和 Windows，如果 Android 和 iOS 端想要加入 VPN 私有网络，只能通过 WireGuard 原生客户端来进行连接。要想做到这一点，需要管理员事先创建一个 External Client，它会生成一个 WireGuard 配置文件，WireGuard 客户端可以下载该配置文件或者扫描二维码进行连接。\n当然，在创建 External Client 之前，需要先设置其中一个节点为 Ingress Gateway。\n需要说明的是，目前移动设备通过 External Client 接入只是权宜之计，随着 Netclient 对更多操作系统的支持，最终所有的客户端都应该使用 netclient 来连接。\nNetclient 介绍 # netclient 是一个非常简单的 CLI，用于创建 WireGuard 配置和接口，将节点加入到 Netmaker 的私有网络中。netclient 可以管理任意数量的 Netmaker 私有网络，所有的网络都由同一个 netclient 实例管理。\n$ netclient --help NAME: Netclient CLI - Netmaker\u0026#39;s netclient agent and CLI. Used to perform interactions with Netmaker server and set local WireGuard config. USAGE: netclient [global options] command [command options] [arguments...] VERSION: v0.8.1 COMMANDS: join Join a Netmaker network. leave Leave a Netmaker network. checkin Checks for local changes and then checks into the specified Netmaker network to ask about remote changes. push Push configuration changes to server. pull Pull latest configuration and peers from server. list Get list of networks. uninstall Uninstall the netclient system service. help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) --version, -v print the version (default: false) Netclient 工作原理 # 使用 netclient 可以加入某个网络，拉取或推送变更，以及离开某个网络。同时 netclient 还有几个辅助命令用于其他场景。\n使用 netclient 加入某个网络时，它会创建一个目录 /etc/netclient，并将 netclient 二进制文件本身复制到该目录下。\n$ ls -lh /etc/netclient/netclient -rwxr-xr-x 1 root root 12M Oct 8 23:08 /etc/netclient/netclient 同时会在该目录下创建一个子目录 config，并在子目录下创建相应的配置文件。比如你加入的网络名称是 default，那么配置文件名称就是 netconfig-default。\n$ ls -lh /etc/netclient/config/ total 32K -rwxr-xr-x 1 root root 1.8K Oct 17 16:23 netconfig-default -rw-r--r-- 1 root root 176 Oct 8 23:08 nettoken-default -rw-r--r-- 1 root root 16 Oct 8 23:08 secret-default -rw-r--r-- 1 root root 44 Oct 8 23:08 wgkey-default 如果第一次使用 netclient 加入某个网络，它会尝试将自己设置为当前节点的守护进程，以 Linux 为例，它会创建一个 systemd 服务：\n$ cat /etc/systemd/system/netclient.service [Unit] Description=Network Check Wants=netclient.timer [Service] Type=simple ExecStart=/etc/netclient/netclient checkin -n all [Install] WantedBy=multi-user.target 该 systemd 服务的作用是向 Netmaker Server 签到，并将本地的配置与 Netmaker Server 托管的配置进行比较，根据比较结果进行适当修改，再拉取所有的 Peer 列表，最后重新配置 WireGuard。\n同时还会设置一个计划任务，来定期（每 15 秒执行一次）启动守护进程同步本地和远程 Netmaker Server 的配置。\n$ cat /etc/systemd/system/netclient.timer [Unit] Description=Calls the Netmaker Mesh Client Service Requires=netclient.service [Timer] Unit=netclient.service OnCalendar=*:*:0/15 [Install] WantedBy=timers.target 对于不支持 systemd 的 Linux 发行版，我们可以采取其他方式来执行守护进程和计划任务。我们也可以把 netclient 作为调试工具，执行 netclient pull 从 Netmaker Server 获取最新配置，执行 netclient push 将本地变更推送到 Netmaker Server，等等。\n总结 # 本文在讲解过程中略过了很多功能和选项的细节，如果你有兴趣了解某个特定的功能或者选项，可以查阅 Netmaker 的官方文档。下一篇文章将会介绍如何使用 Netmaker 来配置 WireGuard 全互联模式，我会详细介绍 Linux、macOS 和手机客户端分别该如何配置，敬请期待！\n","date":"2021年10月18日","externalUrl":null,"permalink":"/posts/configure-a-mesh-network-with-netmaker/","section":"博客","summary":"大家好，我是米开朗基杨。 关注我的读者应该都还记得我之前写过一","title":"WireGuard 教程：使用 Netmaker 来管理 WireGuard 的配置","type":"posts"},{"content":" 早上 6:00，起床，洗漱\n早上 6:30，由于干眼症，需要先滴眼药水，为了防止自己睡着，我需要坐着滴。\n滴眼药水的同时打开蓝牙音响，听听云原生领域相关的播客，学习一下大佬们都是如何装逼的。\n早上 7:00，将蓝牙从音响切换到 Bose 耳机，戴上耳机继续听播客，同时出门买早餐。\n早上 7:30，回家开始吃早餐，同时刷刷 Twitter。\n早上 8:00，开始打开 RSS 阅读器，筛选眼花缭乱的资讯和文章，将其作为素材打上不同的标签归类到 Pocket 中，以备后用。\n上午 9:00，因为不想在 macOS 上装 Slack 应用，便研究一下如何将 FireFox 浏览器的网页保存为 PWA 应用，谷歌搜索：”firefox pwa“，经过一番查找，最后锁定了 firefoxpwa 这个项目。经过一番摸索发现其对 macOS 的支持有很多小问题，于是提了几个 PR 和项目维护者交流了一番，最终把问题解决了。于是我把 code-server、 KubeSphere 官网和 Slack 都保存成了 PWA 应用，以后这几个网站就不会再受到 FireFox 浏览器重启的影响了。\n上午 10:00，发布今天的 社区公众号推文，包含 周四的直播预告。并将推文传播到微信社群和各大中文社区。本周的分享嘉宾是个妹子！博士！感兴趣的可以周四晚上8点埋伏一下。\n上午 10:30，浏览一遍 KubeSphere 中文论坛，回答部分社区用户提出的各种稀奇古怪的关于 Kubernetes 和 KubeSphere 的问题。\n上午 11:30，陪女朋友去小区外面的小餐馆进食，饭后归来一起玩耍 1 小时。\n中午 13:00，午休是不存在的，开始多线程刷 Twitter。收到了 Nebula Graph 开发者布道师古思为老师发来的感谢。\n下午 2:00，打开 KubeSphere Console 界面，检查一下我的 K3s 集群有没有生病，看看 Cilium 有没有出幺蛾子，保障 我的博客和其他服务正常运行。\n顺便再观察一下我的博客访问量。\n下午 2:30，开始撰写新的技术文章，介绍如何创建 EKS Anywhere 集群，以及如何使用 KubeSphere 来管理 EKS Anywhere 集群。\n下午 3:30，Google Keep 开始提醒我起来活动筋骨。\n下午 4:00，参与 KubeSphere 社区宣传与外联特别兴趣小组（Advocacy and Outreach SIG）双周例会，探讨保姆级云原生课程录制相关事宜，具体内容暂时保密。\n下午 5:00，继续撰写之前未完的技术文章，顺便 Review 云原生文章翻译工作组成员提交的 PR，顺便再继续完善云原生视频课程的大纲。\n晚上 6:30，晚饭做好了，于是便打开电视机，点开 YouTube，边吃饭边看老高与小茉讲阿努纳奇的故事。。。\n晚上 7:30，开始洗碗、扫地、拖地。\n晚上 8:00，和女朋友去楼下打羽毛球。\n晚上 9:00，回家，洗漱。打开微信，各种云原生活动来找我进行商务合作，于是安排了公众号明天的头条，定时发布。\n晚上 11:00，女朋友洗漱完毕，和我一起玩耍。\n晚上 12:00，睡觉。\n","date":"2021年9月15日","externalUrl":null,"permalink":"/posts/a-day-in-the-life-of-cloudnativer/","section":"博客","summary":"早上 6:00，起床，洗漱 早上 6:30，由于干眼症，需要先滴眼","title":"一名居家办公的云原生搬砖师的一天，辛酸...","type":"posts"},{"content":"","date":"2021年9月5日","externalUrl":null,"permalink":"/tags/containers/","section":"标签","summary":"","title":"Containers","type":"tags"},{"content":" 大多数情况下，我们构建容器镜像时选择的基础镜像无外乎是 busybox、alpine 和 google/distroless 这几种，这几个基础镜像在云原生的世界很吃香，被广泛应用于各个应用的容器化。\n那么问题来了，为什么这几个基础镜像如此受欢迎呢？\n我们先来看下这几个基础镜像的大小：\n🐳 → podman image ls REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/library/alpine latest 14119a10abf4 6 days ago 5.87 MB docker.io/library/busybox latest 42b97d3c2ae9 13 days ago 1.46 MB gcr.io/distroless/static latest e0851a4aa136 51 years ago 3.06 MB 可以看到这些镜像的体积都非常小，几乎可以忽略不计。\nBusybox # 先启动一个 Busybox 容器进去一探究竟：\n这个镜像的大小只有 1.24MB，缺容纳了这么多 GNU 命令，麻雀虽小五脏俱全啊，这到底是怎么做到的？\n事实上这一切都要归功于 Multi-Call binary。什么是 Multi-Call binary 呢？\n顾名思义，Multi-Call binary 就是多重调用二进制文件，是一个用C语言编写的程序，它允许多次调用来执行二进制文件。它包含了很多函数，每个执行独特动作的函数都可以通过一个名字来调用，这个名字同时也是 Multi-Call binary 的一个符号链接。Multi-Call binary 最好的应用范例便是 Busybox。\nBusybox 里面的函数可以通过两种方式来调用：\nbusybox ls ls 例如：\n很明显，这些不是我们所熟知的 GNU 二进制文件，因为所有的二进制文件都具有相同的属性，比如大小、日期等。这些都不是独立的二进制文件，而是 Multi-Call binary 每个调用函数的别名。这个 Multi-Call binary 就叫 Busybox。\n遗憾的是，这些 Busybox 命令并不完全等同于 GNU 命令，某些命令的某些参数是无法执行的，相当于阉割版。\nAlpine # 看完了 Busybox，我们再来看看 Docker Alpine 是怎么做的。\n巧了，Docker Alpine 的二进制文件竟然是指向 busybox 二进制文件的，这就很明显了，Alpine 镜像的底层使用了 busybox 二进制文件。除此之外，Alpine 还包含了 apk 包管理器和一些额外的可执行文件，所以 Alpine 镜像的体积才会比 Busybox 大。\nDistroless # Distroless 就不用说了，它来自 Google。该镜像几乎就是空的，只包含应用程序及其运行时所需的依赖，不包含软件包管理器、shell 和其他 GNU 二进制文件，当然还包含一些时区配置和部分 ca-certificates。\n可以看到这个镜像中既没有 shell 也没有 bash，为了一探究竟，可以先把镜像保存为 tar 包，然后把 rootfs 解压出来：\n🐳 → mkdir image 🐳 → tar xvf distroless.tar.gz -C image/ 16679402dc206c982b5552ab8de7d898547100e5468be29d4f67d393c0eadfdb.tar e0851a4aa13657fc8dcd01e0e5e08cb817123ccb82e2c604b34f9ec9c1755e3f.json 2e18de03719583329b7fa8374130e57cc7cddf2b5a487fe4a4988622ca60575c/layer.tar 2e18de03719583329b7fa8374130e57cc7cddf2b5a487fe4a4988622ca60575c/VERSION 2e18de03719583329b7fa8374130e57cc7cddf2b5a487fe4a4988622ca60575c/json manifest.json repositories 🐳 → cd image 🐳 → ls -lh total 3.0M -r--r--r--. 1 root root 3.0M Jan 1 1970 16679402dc206c982b5552ab8de7d898547100e5468be29d4f67d393c0eadfdb.tar drwxr-xr-x. 2 root root 50 Sep 3 17:42 2e18de03719583329b7fa8374130e57cc7cddf2b5a487fe4a4988622ca60575c -r--r--r--. 1 root root 462 Jan 1 1970 e0851a4aa13657fc8dcd01e0e5e08cb817123ccb82e2c604b34f9ec9c1755e3f.json -r--r--r--. 1 root root 213 Jan 1 1970 manifest.json -r--r--r--. 1 root root 106 Jan 1 1970 repositories 🐳 → mkdir rootfs 🐳 → tar xf 16679402dc206c982b5552ab8de7d898547100e5468be29d4f67d393c0eadfdb.tar -C rootfs 🐳 → tree rootfs rootfs ├── bin ├── boot ├── dev ├── etc │ ├── debian_version │ ├── default │ ├── dpkg │ │ └── origins │ │ └── debian │ ├── group │ ├── host.conf │ ├── issue │ ├── issue.net │ ├── nsswitch.conf │ ├── os-release │ ├── passwd │ ├── profile.d │ ├── protocols │ ├── rpc │ ├── services │ ├── skel │ ├── ssl │ │ └── certs │ │ └── ca-certificates.crt │ └── update-motd.d │ └── 10-uname ├── home │ └── nonroot ├── lib ├── proc ├── root ├── run ├── sbin ├── sys ├── tmp ├── usr │ ├── bin │ ├── games │ ├── include │ ├── lib │ │ └── os-release │ ├── sbin │ │ └── tzconfig │ ├── share │ │ ├── base-files │ │ │ ├── dot.bashrc │ │ │ ├── dot.profile │ │ │ ├── dot.profile.md5sums │ │ │ ├── info.dir │ │ │ ├── motd │ │ │ ├── profile │ │ │ ├── profile.md5sums │ │ │ └── staff-group-for-usr-local ... ... 该镜像只有一层，大小为 3MB，也没有二进制文件，只有一些证书文件和目录。如果向下滚动，还能看到许可证和时区配置。看来 Distroless 采取的是非常极端的手段，直接把不需要的二进制文件全部抛弃了，只留下一个空镜像和部分必需品。\n总结 # 由此看来，这几个基础镜像如此受欢迎的主要原因就是体积小。镜像越小，漏洞就越少，可攻击面也会大幅减少，而且很容易维护。所以大家构建镜像时尽量选择这些镜像作为基础镜像。\n","date":"2021年9月5日","externalUrl":null,"permalink":"/posts/alpine-vs-distroless-vs-busybox/","section":"博客","summary":"大多数情况下，我们构建容器镜像时选择的基础镜像无外乎是 bus","title":"Docker Alpine：轻量级容器镜像的终极选择","type":"posts"},{"content":"","date":"2021年8月25日","externalUrl":null,"permalink":"/tags/prometheus/","section":"标签","summary":"","title":"Prometheus","type":"tags"},{"content":" 原文链接： https://kubesphere.com.cn/blogs/x509-certificate-exporter/\nKubeSphere 虽然提供了运维友好的向导式操作界面，简化了 Kubernetes 的运维操作，但它还是建立在底层 Kubernetes 之上的，Kubernetes 默认的证书有效期都是一年，即使使用 KubeKey 这样的集群安装利器也不能改变这个结果。如果不想办法对 Kubernetes 各个组件的证书有效期进行监控，说不定哪天就会掉进坑里。\n有部分读者可能听说过 ssl-exporter 这个项目，它能提供多种针对 SSL 的检测手段，包括：HTTPS 证书、文件证书、Kubernetes Secret、Kubeconfig 文件。从功能上来看，它基本可以满足上述需求，但它的指标还不够丰富，本文将介绍一个更为强大的 Prometheus Exporter： x509-certificate-exporter。\n与 ssl-exporter 不同，x509-certificate-exporter 只专注于监控 Kubernetes 集群相关的证书，包括各个组件的文件证书、Kubernetes TLS Secret、Kubeconfig 文件，而且指标更加丰富。我们来看看在 KubeSphere 中如何部署 x509-certificate-exporter 以监控集群的所有证书。\n准备 KubeSphere 应用模板 # KubeSphere 集成了 OpenPitrix 来提供应用程序全生命周期管理，OpenPitrix 是一个多云应用管理平台，KubeSphere 利用它实现了应用商店和应用模板，以可视化的方式部署并管理应用。对于应用商店中不存在的应用，用户可以将 Helm Chart 交付至 KubeSphere 的公共仓库，或者导入私有应用仓库来提供应用模板。\n本教程将使用 KubeSphere 的应用模板来部署 x509-certificate-exporter。\n要想从应用模板部署应用，需要创建一个企业空间、一个项目和两个用户帐户（ws-admin 和 project-regular）。ws-admin 必须被授予企业空间中的 workspace-admin 角色， project-regular 必须被授予项目中的 operator 角色。在创建之前，我们先来回顾一下 KubeSphere 的多租户架构。\n多租户架构 # KubeSphere 的多租户系统分三个层级，即集群、企业空间和项目。KubeSphere 中的项目等同于 Kubernetes 的 命名空间。\n您需要创建一个新的 企业空间进行操作，而不是使用系统企业空间，系统企业空间中运行着系统资源，绝大部分仅供查看。出于安全考虑，强烈建议给不同的租户授予不同的权限在企业空间中进行协作。\n您可以在一个 KubeSphere 集群中创建多个企业空间，每个企业空间下可以创建多个项目。KubeSphere 为每个级别默认设有多个内置角色。此外，您还可以创建拥有自定义权限的角色。KubeSphere 多层次结构适用于具有不同团队或组织以及每个团队中需要不同角色的企业用户。\n创建帐户 # 安装 KubeSphere 之后，您需要向平台添加具有不同角色的用户，以便他们可以针对自己授权的资源在不同的层级进行工作。一开始，系统默认只有一个帐户 admin，具有 platform-admin 角色。在本步骤中，您将创建一个帐户 user-manager，然后使用 user-manager 创建新帐户。\n以 admin 身份使用默认帐户和密码 (admin/P@88w0rd) 登录 Web 控制台。 出于安全考虑，强烈建议您在首次登录控制台时更改密码。若要更改密码，在右上角的下拉菜单中选择个人设置，在密码设置中设置新密码，您也可以在个人设置中修改控制台语言。\n登录控制台后，点击左上角的平台管理，然后选择访问控制。\n在帐户角色中，有如下所示四个可用的内置角色。接下来要创建的第一个帐户将被分配 users-manager 角色。\n内置角色 描述 workspaces-manager 企业空间管理员，管理平台所有企业空间。 users-manager 用户管理员，管理平台所有用户。 platform-regular 平台普通用户，在被邀请加入企业空间或集群之前没有任何资源操作权限。 platform-admin 平台管理员，可以管理平台内的所有资源。 在帐户管理中，点击创建。在弹出窗口中，提供所有必要信息（带有*标记），然后在角色字段选择 users-manager。请参考下图示例。\n完成后，点击确定。新创建的帐户将显示在帐户管理中的帐户列表中。\n切换帐户使用 user-manager 重新登录，创建如下三个新账户。\n帐户 角色 描述 ws-manager workspaces-manager 创建和管理所有企业空间。 ws-admin platform-regular 管理指定企业空间中的所有资源（此帐户用于邀请成员 project-regular 加入该企业空间）。 project-regular platform-regular 该帐户将用于在指定项目中创建工作负载、流水线和其他资源。 查看创建的三个帐户。\n创建企业空间 # 在本步骤中，您需要使用上一个步骤中创建的帐户 ws-manager 创建一个企业空间。作为管理项目、创建工作负载和组织成员的基本逻辑单元，企业空间是 KubeSphere 多租户系统的基础。\n以 ws-manager 身份登录 KubeSphere，它具有管理平台上所有企业空间的权限。点击左上角的平台管理，选择访问控制。在企业空间中，可以看到仅列出了一个默认企业空间 system-workspace，即系统企业空间，其中运行着与系统相关的组件和服务，您无法删除该企业空间。\n点击右侧的创建，将新企业空间命名为 demo-workspace，并将用户 ws-admin 设置为企业空间管理员，如下图所示：\n完成后，点击创建。\n登出控制台，然后以 ws-admin 身份重新登录。在企业空间设置中，选择企业成员，然后点击邀请成员。\n邀请 project-regular 进入企业空间，授予其 workspace-viewer 角色。\n实际角色名称的格式：\u0026lt;workspace name\u0026gt;-\u0026lt;role name\u0026gt;。例如，在名为 demo-workspace 的企业空间中，角色 viewer 的实际角色名称为 demo-workspace-viewer。\n将 project-regular 添加到企业空间后，点击确定。在企业成员中，您可以看到列出的两名成员。\n帐户 角色 描述 ws-admin workspace-admin 管理指定企业空间中的所有资源（在此示例中，此帐户用于邀请新成员加入企业空间、创建项目）。 project-regular workspace-viewer 该帐户将用于在指定项目中创建工作负载和其他资源。 创建项目 # 在此步骤中，您需要使用在上一步骤中创建的帐户 ws-admin 来创建项目。KubeSphere 中的项目与 Kubernetes 中的命名空间相同，为资源提供了虚拟隔离。有关更多信息，请参见 命名空间。\n以 ws-admin 身份登录 KubeSphere，在项目管理中，点击创建。\n输入项目名称（例如 exporter），然后点击确定完成，您还可以为项目添加别名和描述。\n在项目管理中，点击刚创建的项目查看其详细信息。\n邀请 project-regular 至该项目，并授予该用户 operator 角色。请参考下图以了解具体步骤。\n具有 operator 角色的用户是项目维护者，可以管理项目中除用户和角色以外的资源。\n添加应用仓库 # 以 ws-admin 用户登录 KubeSphere 的 Web 控制台。在您的企业空间中，进入应用管理下的应用仓库页面，并点击添加仓库。\n在弹出的对话框中，将应用仓库名称设置为 enix，将应用仓库的 URL 设置为 https://charts.enix.io，点击验证对 URL 进行验证，再点击确定进入下一步。\n应用仓库导入成功后会显示在如下图所示的列表中。\n部署 x509-certificate-exporter # 导入 x509-certificate-exporter 的应用仓库后，就可以通过应用模板来部署 x509-certificate-exporter 了。\n登出 KubeSphere 并以 project-regular 用户重新登录。在您的项目中，进入应用负载下的应用页面，再点击部署新应用。\n在弹出的对话框中选择来自应用模板。\n在弹出的对话框中选择来自应用模板。\n来自应用商店：选择内置的应用和以 Helm Chart 形式单独上传的应用。\n来自应用模板：从私有应用仓库和企业空间应用池选择应用。\n从下拉列表中选择之前添加的私有应用仓库 enix。\n选择 x509-certificate-exporter 进行部署。\n您可以查看应用信息和配置文件，在版本下拉列表中选择版本，然后点击部署。\n设置应用名称，确认应用版本和部署位置，点击下一步。\n接下来进入应用配置页面。\n这里需要手动编辑配置清单，指定证书文件的路径。\ndaemonSets: master: nodeSelector: node-role.kubernetes.io/master: \u0026#39;\u0026#39; tolerations: - effect: NoSchedule key: node-role.kubernetes.io/master operator: Exists watchFiles: - /var/lib/kubelet/pki/kubelet-client-current.pem - /etc/kubernetes/pki/apiserver.crt - /etc/kubernetes/pki/apiserver-kubelet-client.crt - /etc/kubernetes/pki/ca.crt - /etc/kubernetes/pki/front-proxy-ca.crt - /etc/kubernetes/pki/front-proxy-client.crt watchKubeconfFiles: - /etc/kubernetes/admin.conf - /etc/kubernetes/controller-manager.conf - /etc/kubernetes/scheduler.conf nodes: tolerations: - effect: NoSchedule key: node-role.kubernetes.io/ingress operator: Exists watchFiles: - /var/lib/kubelet/pki/kubelet-client-current.pem - /etc/kubernetes/pki/ca.crt 该配置会创建两个 DaemonSet，master 运行在控制节点，nodes 运行在计算节点。\n$ kubectl -n exporter get ds NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE x509-x509-certificate-exporter-master 1 1 1 1 1 node-role.kubernetes.io/master= 3d14h x509-x509-certificate-exporter-nodes 3 3 3 3 3 \u0026lt;none\u0026gt; 3d14h 参数解释：\nwatchFiles : 证书文件所在的路径。 watchKubeconfFiles : Kubeconfig 文件所在的路径。 改完后的效果如图所示。\n点击部署，等待应用创建完成并开始运行。\n接入监控系统 # 通过应用模板部署完成后，除了会创建两个 DaemonSet 之外，还会创建一个 ServiceMonitor。\n$ kubectl -n exporter get servicemonitor NAME AGE x509-x509-certificate-exporter 3d15h 打开 Prometheus 的 Web UI，可以看到相应的 Targets 已经在线。\nx509-certificate-exporter 官方提供了一个 Grafana Dashboard，导入 Grafana 后的效果如图：\n各项指标一目了然，一般我们只需要关注已经过期的证书和即将过期的证书即可。假设我想查看证书还有多久失效，可以使用表达式 (x509_cert_not_after{filepath!=\u0026quot;\u0026quot;} - time()) / 3600 / 24。\n可以创建相应的告警规则，以便在证书即将过期时通知运维人员尽快更新证书。例如：\n进入监控告警下的告警策略页面，点击创建。\n填写告警名称，设置告警级别，点击下一步。\n选择自定义规则，告警规则填入 (x509_cert_not_after{filepath!=\u0026quot;\u0026quot;} - time()) / 3600 / 24 \u0026lt; 30。\n点击下一步，填写标题和消息。\n点击创建，告警规则就创建完成了。\n结语 # 事实上 KubeSphere 从 3.1 版本开始就内置了证书过期的告警策略，可以在告警策略页面的内置策略中输入 expir 进行搜索。\n点进去可以看到具体的告警规则表达式。\n告警规则表达式里面的指标是 API Server 组件自身暴露的指标，并没有兼顾到整个集群所有组件的证书。想要全面监控所有组件的证书，建议结合 x509-certificate-exporter 在 KubeSphere 中添加自定义告警策略，从此不再为证书过期而烦恼。\n","date":"2021年8月25日","externalUrl":null,"permalink":"/posts/x509-certificate-exporter/","section":"博客","summary":"原文链接： https://kubesphere.com.cn/blogs/x509-certificate-exporter/ KubeSphere 虽然提供了运维友好的向导式操作界面，简化了 Kubernetes 的","title":"使用 x509-certificate-exporter 监控 Kubernetes 集群组件的证书","type":"posts"},{"content":"","date":"2021年8月25日","externalUrl":null,"permalink":"/categories/monitoring/","section":"分类","summary":"","title":"监控","type":"categories"},{"content":" 原文链接： IPVS: How Kubernetes Services Direct Traffic to Pods\nKubernetes 中的 Service 就是一组同 label 类型 Pod 的服务抽象，为服务提供了负载均衡和反向代理能力，在集群中表示一个微服务的概念。kube-proxy 组件则是 Service 的具体实现，了解了 kube-proxy 的工作原理，才能洞悉服务之间的通信流程，再遇到网络不通时也不会一脸懵逼。\nkube-proxy 有三种模式：userspace、iptables 和 IPVS，其中 userspace 模式不太常用。iptables 模式最主要的问题是在服务多的时候产生太多的 iptables 规则，非增量式更新会引入一定的时延，大规模情况下有明显的性能问题。为解决 iptables 模式的性能问题，v1.11 新增了 IPVS 模式（v1.8 开始支持测试版，并在 v1.11 GA），采用增量式更新，并可以保证 service 更新期间连接保持不断开。\n目前网络上关于 kube-proxy 工作原理的文档几乎都是以 iptables 模式为例，很少提及 IPVS，本文就来破例解读 kube-proxy IPVS 模式的工作原理。为了理解地更加彻底，本文不会使用 Docker 和 Kubernetes，而是使用更加底层的工具来演示。\n我们都知道，Kubernetes 会为每个 Pod 创建一个单独的网络命名空间 (Network Namespace) ，本文将会通过手动创建网络命名空间并启动 HTTP 服务来模拟 Kubernetes 中的 Pod。\n本文的目标是通过模拟以下的 Service 来探究 kube-proxy 的 IPVS 和 ipset 的工作原理：\napiVersion: v1 kind: Service metadata: name: app-service spec: clusterIP: 10.100.100.100 selector: component: app ports: - protocol: TCP port: 8080 targetPort: 8080 跟着我的步骤，最后你就可以通过命令 curl 10.100.100.100:8080 来访问某个网络命名空间的 HTTP 服务。为了更好地理解本文的内容，推荐提前阅读以下的文章：\nHow do Kubernetes and Docker create IP Addresses?! iptables: How Docker Publishes Ports iptables: How Kubernetes Services Direct Traffic to Pods 注意：本文所有步骤皆是在 Ubuntu 20.04 中测试的，其他 Linux 发行版请自行测试。\n准备实验环境 # 首先需要开启 Linux 的路由转发功能：\n$ sysctl --write net.ipv4.ip_forward=1 接下来的命令主要做了这么几件事：\n创建一个虚拟网桥 bridge_home 创建两个网络命名空间 netns_dustin 和 netns_leah 为每个网络命名空间配置 DNS 创建两个 veth pair 并连接到 bridge_home 给 netns_dustin 网络命名空间中的 veth 设备分配一个 IP 地址为 10.0.0.11 给 netns_leah 网络命名空间中的 veth 设备分配一个 IP 地址为 10.0.021 为每个网络命名空间设定默认路由 添加 iptables 规则，允许流量进出 bridge_home 接口 添加 iptables 规则，针对 10.0.0.0/24 网段进行流量伪装 $ ip link add dev bridge_home type bridge $ ip address add 10.0.0.1/24 dev bridge_home $ ip netns add netns_dustin $ mkdir -p /etc/netns/netns_dustin echo \u0026#34;nameserver 114.114.114.114\u0026#34; | tee -a /etc/netns/netns_dustin/resolv.conf $ ip netns exec netns_dustin ip link set dev lo up $ ip link add dev veth_dustin type veth peer name veth_ns_dustin $ ip link set dev veth_dustin master bridge_home $ ip link set dev veth_dustin up $ ip link set dev veth_ns_dustin netns netns_dustin $ ip netns exec netns_dustin ip link set dev veth_ns_dustin up $ ip netns exec netns_dustin ip address add 10.0.0.11/24 dev veth_ns_dustin $ ip netns add netns_leah $ mkdir -p /etc/netns/netns_leah echo \u0026#34;nameserver 114.114.114.114\u0026#34; | tee -a /etc/netns/netns_leah/resolv.conf $ ip netns exec netns_leah ip link set dev lo up $ ip link add dev veth_leah type veth peer name veth_ns_leah $ ip link set dev veth_leah master bridge_home $ ip link set dev veth_leah up $ ip link set dev veth_ns_leah netns netns_leah $ ip netns exec netns_leah ip link set dev veth_ns_leah up $ ip netns exec netns_leah ip address add 10.0.0.21/24 dev veth_ns_leah $ ip link set bridge_home up $ ip netns exec netns_dustin ip route add default via 10.0.0.1 $ ip netns exec netns_leah ip route add default via 10.0.0.1 $ iptables --table filter --append FORWARD --in-interface bridge_home --jump ACCEPT $ iptables --table filter --append FORWARD --out-interface bridge_home --jump ACCEPT $ iptables --table nat --append POSTROUTING --source 10.0.0.0/24 --jump MASQUERADE 在网络命名空间 netns_dustin 中启动 HTTP 服务：\n$ ip netns exec netns_dustin python3 -m http.server 8080 打开另一个终端窗口，在网络命名空间 netns_leah 中启动 HTTP 服务：\n$ ip netns exec netns_leah python3 -m http.server 8080 测试各个网络命名空间之间是否能正常通信：\n$ curl 10.0.0.11:8080 $ curl 10.0.0.21:8080 $ ip netns exec netns_dustin curl 10.0.0.21:8080 $ ip netns exec netns_leah curl 10.0.0.11:8080 整个实验环境的网络拓扑结构如图：\n安装必要工具 # 为了便于调试 IPVS 和 ipset，需要安装两个 CLI 工具：\n$ apt install ipset ipvsadm --yes 本文使用的 ipset 和 ipvsadm 版本分别为 7.5-1~exp1 和 1:1.31-1。\n通过 IPVS 来模拟 Service # 下面我们使用 IPVS 创建一个虚拟服务 (Virtual Service) 来模拟 Kubernetes 中的 Service :\n$ ipvsadm \\ --add-service \\ --tcp-service 10.100.100.100:8080 \\ --scheduler rr 这里使用参数 --tcp-service 来指定 TCP 协议，因为我们需要模拟的 Service 就是 TCP 协议。 IPVS 相比 iptables 的优势之一就是可以轻松选择调度算法，这里选择使用轮询调度算法。 目前 kube-proxy 只允许为所有 Service 指定同一个调度算法，未来将会支持为每一个 Service 选择不同的调度算法，详情可参考文章 IPVS-Based In-Cluster Load Balancing Deep Dive。\n创建了虚拟服务之后，还得给它指定一个后端的 Real Server，也就是后端的真实服务，即网络命名空间 netns_dustin 中的 HTTP 服务：\n$ ipvsadm \\ --add-server \\ --tcp-service 10.100.100.100:8080 \\ --real-server 10.0.0.11:8080 \\ --masquerading 该命令会将访问 10.100.100.100:8080 的 TCP 请求转发到 10.0.0.11:8080。这里的 --masquerading 参数和 iptables 中的 MASQUERADE 类似，如果不指定，IPVS 就会尝试使用路由表来转发流量，这样肯定是无法正常工作的。\n译者注：由于 IPVS 未实现 POST_ROUTING Hook 点，所以它需要 iptables 配合完成 IP 伪装等功能。\n测试是否正常工作：\n$ curl 10.100.100.100:8080 实验成功，请求被成功转发到了后端的 HTTP 服务！\n在网络命名空间中访问虚拟服务 # 上面只是在 Host 的网络命名空间中进行测试，现在我们进入网络命名空间 netns_leah 中进行测试：\n$ ip netns exec netns_leah curl 10.100.100.100:8080 哦豁，访问失败！\n要想顺利通过测试，只需将 10.100.100.100 这个 IP 分配给一个虚拟网络接口。至于为什么要这么做，目前我还不清楚，我猜测可能是因为网桥 bridge_home 不会调用 IPVS，而将虚拟服务的 IP 地址分配给一个网络接口则可以绕过这个问题。\n译者注 # Netfilter 是一个基于用户自定义的 Hook 实现多种网络操作的 Linux 内核框架。Netfilter 支持多种网络操作，比如包过滤、网络地址转换、端口转换等，以此实现包转发或禁止包转发至敏感网络。\n针对 Linux 内核 2.6 及以上版本，Netfilter 框架实现了 5 个拦截和处理数据的系统调用接口，它允许内核模块注册内核网络协议栈的回调功能，这些功能调用的具体规则通常由 Netfilter 插件定义，常用的插件包括 iptables、IPVS 等，不同插件实现的 Hook 点（拦截点）可能不同。另外，不同插件注册进内核时需要设置不同的优先级，例如默认配置下，当某个 Hook 点同时存在 iptables 和 IPVS 规则时，iptables 会被优先处理。\nNetfilter 提供了 5 个 Hook 点，系统内核协议栈在处理数据包时，每到达一个 Hook 点，都会调用内核模块中定义的处理函数。调用哪个处理函数取决于数据包的转发方向，进站流量和出站流量触发的 Hook 点是不一样的。\n内核协议栈中预定义的回调函数有如下五个：\nNF_IP_PRE_ROUTING: 接收的数据包进入协议栈后立即触发此回调函数，该动作发生在对数据包进行路由判断（将包发往哪里）之前。 NF_IP_LOCAL_IN: 接收的数据包经过路由判断后，如果目标地址在本机上，则将触发此回调函数。 NF_IP_FORWARD: 接收的数据包经过路由判断后，如果目标地址在其他机器上，则将触发此回调函数。 NF_IP_LOCAL_OUT: 本机产生的准备发送的数据包，在进入协议栈后立即触发此回调函数。 NF_IP_POST_ROUTING: 本机产生的准备发送的数据包或者经由本机转发的数据包，在经过路由判断之后，将触发此回调函数。 iptables 实现了所有的 Hook 点，而 IPVS 只实现了 LOCAL_IN、LOCAL_OUT、FORWARD 这三个 Hook 点。既然没有实现 PRE_ROUTING，就不会在进入 LOCAL_IN 之前进行地址转换，那么数据包经过路由判断后，会进入 LOCAL_IN Hook 点，IPVS 回调函数如果发现目标 IP 地址不属于该节点，就会将数据包丢弃。\n如果将目标 IP 分配给了虚拟网络接口，内核在处理数据包时，会发现该目标 IP 地址属于该节点，于是可以继续处理数据包。\ndummy 接口 # 当然，我们不需要将 IP 地址分配给任何已经被使用的网络接口，我们的目标是模拟 Kubernetes 的行为。Kubernetes 在这里创建了一个 dummy 接口，它和 loopback 接口类似，但是你可以创建任意多的 dummy 接口。它提供路由数据包的功能，但实际上又不进行转发。dummy 接口主要有两个用途：\n用于主机内的程序通信 由于 dummy 接口总是 up（除非显式将管理状态设置为 down），在拥有多个物理接口的网络上，可以将 service 地址设置为 loopback 接口或 dummy 接口的地址，这样 service 地址不会因为物理接口的状态而受影响。 看来 dummy 接口完美符合实验需求，那就创建一个 dummy 接口吧：\n$ ip link add dev dustin-ipvs0 type dummy 将虚拟 IP 分配给 dummy 接口 dustin-ipvs0 :\n$ ip addr add 10.100.100.100/32 dev dustin-ipvs0 到了这一步，仍然访问不了 HTTP 服务，还需要另外一个黑科技：bridge-nf-call-iptables。在解释 bridge-nf-call-iptables 之前，我们先来回顾下容器网络通信的基础知识。\n基于网桥的容器网络 # Kubernetes 集群网络有很多种实现，有很大一部分都用到了 Linux 网桥:\n每个 Pod 的网卡都是 veth 设备，veth pair 的另一端连上宿主机上的网桥。 由于网桥是虚拟的二层设备，同节点的 Pod 之间通信直接走二层转发，跨节点通信才会经过宿主机 eth0。 Service 同节点通信问题 # 不管是 iptables 还是 ipvs 转发模式，Kubernetes 中访问 Service 都会进行 DNAT，将原本访问 ClusterIP:Port 的数据包 DNAT 成 Service 的某个 Endpoint (PodIP:Port)，然后内核将连接信息插入 conntrack 表以记录连接，目的端回包的时候内核从 conntrack 表匹配连接并反向 NAT，这样原路返回形成一个完整的连接链路:\n但是 Linux 网桥是一个虚拟的二层转发设备，而 iptables conntrack 是在三层上，所以如果直接访问同一网桥内的地址，就会直接走二层转发，不经过 conntrack:\nPod 访问 Service，目的 IP 是 Cluster IP，不是网桥内的地址，走三层转发，会被 DNAT 成 PodIP:Port。\n如果 DNAT 后是转发到了同节点上的 Pod，目的 Pod 回包时发现目的 IP 在同一网桥上，就直接走二层转发了，没有调用 conntrack，导致回包时没有原路返回 (见下图)。\n由于没有原路返回，客户端与服务端的通信就不在一个 “频道” 上，不认为处在同一个连接，也就无法正常通信。\n开启 bridge-nf-call-iptables # 启用 bridge-nf-call-iptables 这个内核参数 (置为 1)，表示 bridge 设备在二层转发时也去调用 iptables 配置的三层规则 (包含 conntrack)，所以开启这个参数就能够解决上述 Service 同节点通信问题。\n所以这里需要启用 bridge-nf-call-iptables :\n$ modprobe br_netfilter $ sysctl --write net.bridge.bridge-nf-call-iptables=1 现在再来测试一下连通性：\n$ ip netns exec netns_leah curl 10.100.100.100:8080 终于成功了！\n开启 Hairpin（发夹弯）模式 # 虽然我们可以从网络命名空间 netns_leah 中通过虚拟服务成功访问另一个网络命名空间 netns_dustin 中的 HTTP 服务，但还没有测试过从 HTTP 服务所在的网络命名空间 netns_dustin 中直接通过虚拟服务访问自己，话不多说，直接测一把：\n$ ip netns exec netns_dustin curl 10.100.100.100:8080 啊哈？竟然失败了，这又是哪里的问题呢？不要慌，开启 hairpin 模式就好了。那么什么是 hairpin 模式呢？ 这是一个网络虚拟化技术中常提到的概念，也即交换机端口的VEPA模式。这种技术借助物理交换机解决了虚拟机间流量转发问题。很显然，这种情况下，源和目标都在一个方向，所以就是从哪里进从哪里出的模式。\n怎么配置呢？非常简单，只需一条命令：\n$ brctl hairpin bridge_home veth_dustin on 再次进行测试：\n$ ip netns exec netns_dustin curl 10.100.100.100:8080 还是失败了。。。\n然后我花了一个下午的时间，终于搞清楚了启用混杂模式后为什么还是不能解决这个问题，因为混杂模式和下面的选项要一起启用才能对 IPVS 生效：\n$ sysctl --write net.ipv4.vs.conntrack=1 最后再测试一次：\n$ ip netns exec netns_dustin curl 10.100.100.100:8080 这次终于成功了，但我还是不太明白为什么启用 conntrack 能解决这个问题，有知道的大神欢迎留言告诉我！\n译者注：IPVS 及其负载均衡算法只针对首个数据包，后继的包必须被 conntrack 表优先反转，如果没有 conntrack，IPVS 对于回来的包是没有任何办法的。可以通过 conntrack -L 查看。\n开启混杂模式 # 如果想让所有的网络命名空间都能通过虚拟服务访问自己，就需要在连接到网桥的所有 veth 接口上开启 hairpin 模式，这也太麻烦了吧。有一个办法可以不用配置每个 veth 接口，那就是开启网桥的混杂模式。\n什么是混杂模式呢？普通模式下网卡只接收发给本机的包（包括广播包）传递给上层程序，其它的包一律丢弃。混杂模式就是接收所有经过网卡的数据包，包括不是发给本机的包，即不验证MAC地址。\n如果一个网桥开启了混杂模式，就等同于将所有连接到网桥上的端口（本文指的是 veth 接口）都启用了 hairpin 模式。可以通过以下命令来启用 bridge_home 的混杂模式：\n$ ip link set bridge_home promisc on 现在即使你把 veth 接口的 hairpin 模式关闭：\n$ brctl hairpin bridge_home veth_dustin off 仍然可以通过连通性测试：\n$ ip netns exec netns_dustin curl 10.100.100.100:8080 优化 MASQUERADE # 在文章开头准备实验环境的章节，执行了这么一条命令：\n$ iptables \\ --table nat \\ --append POSTROUTING \\ --source 10.0.0.0/24 \\ --jump MASQUERADE 这条 iptables 规则会对所有来自 10.0.0.0/24 的流量进行伪装。然而 Kubernetes 并不是这么做的，它为了提高性能，只对来自某些具体的 IP 的流量进行伪装。\n为了更加完美地模拟 Kubernetes，我们继续改造规则，先把之前的规则删除：\n$ iptables \\ --table nat \\ --delete POSTROUTING \\ --source 10.0.0.0/24 \\ --jump MASQUERADE 然后添加针对具体 IP 的规则：\n$ iptables \\ --table nat \\ --append POSTROUTING \\ --source 10.0.0.11/32 \\ --jump MASQUERADE 果然，上面的所有测试都能通过。先别急着高兴，又有新问题了，现在只有两个网络命名空间，如果有很多个怎么办，每个网络命名空间都创建这样一条 iptables 规则？我用 IPVS 是为了啥？就是为了防止有大量的 iptables 规则拖垮性能啊，现在岂不是又绕回去了。\n不慌，继续从 Kubernetes 身上学习，使用 ipset 来解决这个问题。先把之前的 iptables 规则删除：\n$ iptables \\ --table nat \\ --delete POSTROUTING \\ --source 10.0.0.11/32 \\ --jump MASQUERADE 然后使用 ipset 创建一个集合 (set) ：\n$ ipset create DUSTIN-LOOP-BACK hash:ip,port,ip 这条命令创建了一个名为 DUSTIN-LOOP-BACK 的集合，它是一个 hashmap，里面存储了目标 IP、目标端口和源 IP。\n接着向集合中添加条目：\n$ ipset add DUSTIN-LOOP-BACK 10.0.0.11,tcp:8080,10.0.0.11 现在不管有多少网络命名空间，都只需要添加一条 iptables 规则：\n$ iptables \\ --table nat \\ --append POSTROUTING \\ --match set \\ --match-set DUSTIN-LOOP-BACK dst,dst,src \\ --jump MASQUERADE 网络连通性测试也没有问题：\n$ curl 10.100.100.100:8080 $ ip netns exec netns_leah curl 10.100.100.100:8080 $ ip netns exec netns_dustin curl 10.100.100.100:8080 新增虚拟服务的后端 # 最后，我们把网络命名空间 netns_leah 中的 HTTP 服务也添加到虚拟服务的后端：\n$ ipvsadm \\ --add-server \\ --tcp-service 10.100.100.100:8080 \\ --real-server 10.0.0.21:8080 \\ --masquerading 再向 ipset 的集合 DUSTIN-LOOP-BACK 中添加一个条目：\n$ ipset add DUSTIN-LOOP-BACK 10.0.0.21,tcp:8080,10.0.0.21 终极测试来了，试着多运行几次以下的测试命令：\n$ curl 10.100.100.100:8080 你会发现轮询算法起作用了：\n总结 # 相信通过本文的实验和讲解，大家应该理解了 kube-proxy IPVS 模式的工作原理。在实验过程中，我们还用到了 ipset，它有助于解决在大规模集群中出现的 kube-proxy 性能问题。如果你对这篇文章有任何疑问，欢迎和我进行交流。\n参考文章 # 为什么 kubernetes 环境要求开启 bridge-nf-call-iptables ? ","date":"2021年4月8日","externalUrl":null,"permalink":"/posts/ipvs-how-kubernetes-services-direct-traffic-to-pods/","section":"博客","summary":"原文链接： IPVS: How Kubernetes Services Direct Traffic to Pods Kubernetes 中的 Service 就是一组同 label 类型 Pod 的服务抽","title":"kube-proxy IPVS 模式的工作原理","type":"posts"},{"content":"","date":"2021年4月8日","externalUrl":null,"permalink":"/tags/lvs/","section":"标签","summary":"","title":"LVS","type":"tags"},{"content":"","date":"2021年3月17日","externalUrl":null,"permalink":"/tags/goland/","section":"标签","summary":"","title":"Goland","type":"tags"},{"content":" 云原生玩家往往都是左手 MacBook，右手 Goland，但由于大部分人的 MacBook 硬件资源有限，基本上无法丝滑地使用 Goland。即使你是 8C16G 的高富帅，多开几个 PornHub 标签页也会撑不住的，许多人不得不忍痛转向 VSCode。\n现在我要告诉你们一个重大好消息：Goland 竟然有网页版了！\n有了网页版之后，我们就可以直接在 Linux 环境中调试应用了，那感觉真叫一个酸爽啊。只要你的远程机器资源充足，可以随意给网页版 Goland 分配 CPU 和内存资源，想象一下，你拥有一个 16C32G 的网页版 Goland，而且这 16C32G 都是 Goland 独占的，那该有多幸福！\n部署方法闭着眼睛也能猜到了，官方直接提供了 Docker 镜像，一把梭跑起来就完事了，项目地址：\nhttps://github.com/JetBrains/projector-docker 官方提供的部署命令比较简单，不太适合实际使用，还需要加点参数才能真正用起来。由于我有丰富的 Kubernetes 集群资源，就直接部署在 Kubernetes 中了，本文也只讲解 Kubernetes 的部署方式，如果你是通过 docker-compose 或直接用 docker 部署，可以参考我的方案自己修改。\n官方镜像最大的问题是没有安装 golang 的 SDK 环境，但是我也不想自己再重新构建镜像了，就直接使用 Kubernetes 的持久化存储来解决了。同时 Goland 自身的配置和 Go 项目所在的目录都要持久化，不然 Pod 重启就玩完了。好在所有持久化的东西都在 /home/projector-user 目录下，存储直接挂载到这个目录就行了。\n先准备一个 Deployment 资源清单：\n# projector-goland.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: project-data spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: projector-goland labels: app: projector-goland spec: replicas: 1 selector: matchLabels: app: projector-goland template: metadata: labels: app: projector-goland spec: containers: - name: projector-goland image: registry.jetbrains.team/p/prj/containers/projector-goland imagePullPolicy: IfNotPresent volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /home/projector-user name: project-data imagePullSecrets: - name: regcred volumes: - name: localtime hostPath: path: /etc/localtime - name: project-data persistentVolumeClaim: claimName: project-data --- apiVersion: v1 kind: Service metadata: name: projector-goland labels: app: projector-goland spec: selector: app: projector-goland ports: - protocol: TCP name: http port: 80 targetPort: 8887 如果你的 Kubernetes 集群没有对接后端分布式存储，可以使用 hostPath 代替，然后将 Pod 调度到指定的节点。\n使用资源清单创建应用实例：\n$ kubectl apply -f projector-goland.yaml 查看是否创建成功：\n$ kubectl get pod -l app=projector-goland NAME READY STATUS RESTARTS AGE projector-goland-7dcc58f964-9p7xw 1/1 Running 0 3m38s $ kubectl get svc -l app=projector-goland NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE projector-goland ClusterIP 10.106.190.178 \u0026lt;none\u0026gt; 80/TCP 3m38s 如果你能够 直接访问集群的 Service IP，就可以直接通过 Service IP 访问 Goland 网页版了：\n经过一番设置之后，最后激活进入主界面：\n激活方法我就不介绍了，大家自己想办法。\n接下来你可以从本地的 Goland IDE 导出插件和配置：\n将备份拷贝到容器中：\n$ kubectl cp settings.zip projector-goland-7dcc58f964-9p7xw:/home/projector-user/settings.zip 在网页版 Goland 中依次点击 Configure -\u0026gt; Import Settings：\n选择备份配置：\n点击 OK 开始导入：\n最后选择 Shutdown 关闭容器进程，稍后 Pod 中的进程会原地重启，Pod 不会被销毁重建：\n点击 reconnect 重新连接：\n下面还需要做一些额外的操作，因为官方的镜像默认没有安装 golang 的 SDK 环境，在线下载需要叉叉上网，所以最好还是先手动下载：\n$ wget https://mirrors.ustc.edu.cn/golang/go1.16.2.linux-amd64.tar.gz 然后再拷贝到容器中：\n$ kubectl cp go1.16.2.linux-amd64.tar.gz projector-goland-7dcc58f964-9p7xw:/home/projector-user/go1.16.2.linux-amd64.tar.gz 进入容器解压 sdk：\n$ kubectl exec -it projector-goland-7dcc58f964-9p7xw -- bash projector-user@projector-goland-7dcc58f964-9p7xw:/$ cd ~ projector-user@projector-goland-7dcc58f964-9p7xw:/$ mkdir sdk projector-user@projector-goland-7dcc58f964-9p7xw:/$ tar zxvf go1.16.2.linux-amd64.tar.gz -C sdk 访问 Goland 网页版，依次点击右下角的 Configure -\u0026gt; Settings -\u0026gt; Go -\u0026gt; GOROOT，点击 Add SDK，选择 local:\n选择 sdk 路径，然后点击 OK：\n点击 Apply，然后再点击 OK：\n点击 Go Modules，勾上 Enable Go modules integration，Vgo excutable 选择 Project SDK，然后点击 OK：\n至此网页版 Goland 就配置完成了：\n从此以后躺在家里吃灰的 iPad 就可以拿来写代码了。。。\n如果你无法拉取官方的镜像，可以从我这边获取，关注公众号：\n公众号后台回复 goland 即可获取 goland 网页版镜像。\n","date":"2021年3月17日","externalUrl":null,"permalink":"/posts/run-jetbrains-ide-in-docker/","section":"博客","summary":"云原生玩家往往都是左手 MacBook，右手 Goland，但由","title":"Goland 网页版使用教程","type":"posts"},{"content":"","date":"2021年3月17日","externalUrl":null,"permalink":"/tags/jetbrains/","section":"标签","summary":"","title":"Jetbrains","type":"tags"},{"content":" 云原生是一种信仰，是一种全新的技术模式，它不局限于你脑海中固有的那一亩三分地。人有多大胆，地有多大产，只要你敢想，万物皆可云原生。作为一个云原生狂热信徒，给大家看看我的狂热程度：\n我的所有服务（包括博客、镜像加速、评论服务）都部署在云上 k3s 集群中，同时本地和家中设备均和云上集群 Pod 网络通过 WireGuard 打通，家中网关 DNS 用的是 CoreDNS 对国内外解析进行分流，网关使用 Envoy 来代理家中的各种服务，等等。\n家中的所有设备和服务，包括云上的服务，全部使用 kube-prometheus 进行监控，具体我就不细说了，截几张图给大家看看：\n现在还剩下个 WireGuard 没有监控，下面就来看看如何使用 Prometheus 来监控 WireGuard。\n如果看到这篇文章的你仍然是个 WireGuard 新手，请务必按照以下顺序阅读每一篇文章：\nWireGuard 教程：WireGuard 的工作原理 WireGuard 快速安装教程 WireGuard 配置教程：使用 wg-gen-web 来管理 WireGuard 的配置 Wireguard 全互联模式（full mesh）配置指南 如果遇到不明白的，可以参考这篇文章的注解：\nWireGuard 教程：WireGuard 的搭建使用与配置详解 剩下这几篇文章是可选的，有兴趣就看看：\n我为什么不鼓吹 WireGuard Why not \u0026ldquo;Why not WireGuard?\u0026rdquo; WireGuard 教程：使用 DNS-SD 进行 NAT-to-NAT 穿透 WireGuard 本身是不暴露任何指标的，需要通过第三方的 exporter 来暴露指标。目前有两个版本的 exporter，单纯使用其中一个都不太完美，所以我干脆都用。\n1. 镜像构建 # 这两个 exporter 都没有提供 Docker 镜像，所以我只好自己动手了，Rust 版本 exporter 的 Dockerfile 如下：\nFROM rust as builder LABEL description=\u0026#34;Docker container for building prometheus exporter for wireguard.\u0026#34; LABEL maintainer=\u0026#34;Ryan Yang \u0026lt;yangchuansheng33@gmail.com\u0026gt;\u0026#34; WORKDIR /usr/src/ RUN git clone https://github.com/MindFlavor/prometheus_wireguard_exporter.git; \\ cd prometheus_wireguard_exporter; \\ cargo install --path . FROM debian:buster-slim RUN sh -c \u0026#34;echo \u0026#39;deb http://deb.debian.org/debian buster-backports main contrib non-free\u0026#39; \u0026gt; /etc/apt/sources.list.d/buster-backports.list\u0026#34;; \\ apt update; \\ apt install -y wireguard; \\ rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/local/cargo/bin/prometheus_wireguard_exporter /usr/local/bin/prometheus_wireguard_exporter CMD [\u0026#34;prometheus_wireguard_exporter\u0026#34;] Go 版本 exporter 的 Dockerfile 如下：\nFROM golang AS build LABEL description=\u0026#34;Docker container for building prometheus exporter for wireguard.\u0026#34; LABEL maintainer=\u0026#34;Ryan Yang \u0026lt;yangchuansheng33@gmail.com\u0026gt;\u0026#34; WORKDIR /src RUN git clone https://github.com/mdlayher/wireguard_exporter; \\ cd wireguard_exporter/cmd/wireguard_exporter/; \\ go build . FROM busybox:glibc COPY --from=build /src/wireguard_exporter/cmd/wireguard_exporter/wireguard_exporter . CMD [\u0026#34;./wireguard_exporter\u0026#34;] 镜像的构建我就不赘述了，大家可以看我的 GitHub 仓库。\n2. prometheus_wireguard_exporter 部署 # prometheus_wireguard_exporter 直接利用 wg 的配置文件来获取指标，它自己不需要单独准备配置文件，所以只需将 /etc/wireguard 目录映射到容器中。如果你的 wg 组网模式是中心辐射型，建议只需监控 wg 网关，如果是全互联模式，也可以只监控其中一个用来生成配置的节点，当然你也可以监控所有节点。\n我这里只监控了其中一个用来生成配置的节点，以下是部署清单：\n# wireguard_exporter.yaml apiVersion: apps/v1 kind: Deployment metadata: name: wireguard-exporter labels: app: wireguard-exporter spec: replicas: 1 selector: matchLabels: app: wireguard-exporter strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: wireguard-exporter spec: nodeSelector: kubernetes.io/hostname: blog-k3s03 tolerations: - key: node-role.kubernetes.io/ingress operator: Exists effect: NoSchedule hostNetwork: true containers: - name: wireguard-exporter image: yangchuansheng/wireguard_exporter command: [\u0026#34;/usr/local/bin/prometheus_wireguard_exporter\u0026#34;] args: [\u0026#34;-n\u0026#34;, \u0026#34;/etc/wireguard/wg0.conf\u0026#34;, \u0026#34;-r\u0026#34;] securityContext: capabilities: add: [\u0026#34;NET_ADMIN\u0026#34;] ports: - containerPort: 9586 protocol: TCP name: http-metrics volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /etc/wireguard name: config volumes: - name: localtime hostPath: path: /etc/localtime - name: config hostPath: path: /etc/wireguard --- apiVersion: v1 kind: Service metadata: name: wireguard-exporter labels: app: wireguard-exporter spec: sessionAffinity: ClientIP selector: app: wireguard-exporter ports: - protocol: TCP name: http-metrics port: 9586 targetPort: 9586 使用部署清单部署 prometheus_wireguard_exporter：\n$ kubectl apply -f wireguard_exporter.yaml 查看是否部署成功：\n$ kubectl get pod -l app=wireguard-exporter NAME READY STATUS RESTARTS AGE wireguard-exporter-78d44b8bd9-ppm9t 1/1 Running 0 41s 3. wireguard_exporter 部署 # wireguard_exporter 需要单独准备配置文件，格式如下：\n# /etc/wireguard/wg0.toml [[Peer]] public_key = \u0026#34;cGsHfwmPEiLJj6Fv3GU5xFvdyQByn50PC5keVGJEe0w=\u0026#34; name = \u0026#34;RouterOS\u0026#34; [[Peer]] public_key = \u0026#34;izv5L8Kn48+SVwE3D498mdi7YfSrn6aKDNIRxIAHDkU=\u0026#34; name = \u0026#34;macOS\u0026#34; [[Peer]] public_key = \u0026#34;EOM0eLVxsj9jGKWamuIn65T3Wmqw36uLOg2ss7yJ2gw=\u0026#34; name = \u0026#34;blog-k3s02\u0026#34; [[Peer]] public_key = \u0026#34;1RxEokE41ypnIMsbE5OVHFVx199V71MOYzpzQ8bbsFY=\u0026#34; name = \u0026#34;blog-k3s01\u0026#34; [[Peer]] public_key = \u0026#34;b3JiuvdOUV7cFpXyJzLbO2Ea4V4c4AoyugIC/ufGZ18=\u0026#34; name = \u0026#34;Openwrt\u0026#34; [[Peer]] public_key = \u0026#34;FIbzqNv10cdCDO/Ka2GIN9rpxNVV2tO2f00R71EHeSg=\u0026#34; name = \u0026#34;Oneplus\u0026#34; 你需要将 wg0.conf 中的配置内容转化为上面的格式保存到 wg0.toml 文件中，再将其映射到容器中。部署清单如下：\n# wireguard_exporter_go.yaml apiVersion: apps/v1 kind: Deployment metadata: name: wireguard-exporter-go labels: app: wireguard-exporter-go spec: replicas: 1 selector: matchLabels: app: wireguard-exporter-go strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: wireguard-exporter-go spec: nodeSelector: kubernetes.io/hostname: blog-k3s03 tolerations: - key: node-role.kubernetes.io/ingress operator: Exists effect: NoSchedule hostNetwork: true containers: - name: wireguard-exporter-go image: docker.io/yangchuansheng/wireguard_exporter:golang command: [\u0026#34;/wireguard_exporter\u0026#34;] args: [\u0026#34;-wireguard.peer-file\u0026#34;, \u0026#34;/etc/wireguard/wg0.toml\u0026#34;, \u0026#34;-metrics.addr\u0026#34;, \u0026#34;:9587\u0026#34;] securityContext: capabilities: add: [\u0026#34;NET_ADMIN\u0026#34;] ports: - containerPort: 9587 protocol: TCP name: http-metrics volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /etc/wireguard name: config volumes: - name: localtime hostPath: path: /etc/localtime - name: config hostPath: path: /etc/wireguard --- apiVersion: v1 kind: Service metadata: name: wireguard-exporter-go labels: app: wireguard-exporter-go spec: sessionAffinity: ClientIP selector: app: wireguard-exporter-go ports: - protocol: TCP name: http-metrics port: 9587 targetPort: 9587 使用部署清单部署 wireguard_exporter：\n$ kubectl apply -f wireguard_exporter_go.yaml 查看是否部署成功：\n$ kubectl get pod -l app=wireguard-exporter-go NAME READY STATUS RESTARTS AGE wireguard-exporter-go-7f5c88fc68-h45x5 1/1 Running 0 52s 4. 加入 Prometheus 监控 # kube-prometheus 的部署方式这里略过，新手请自己查阅文档部署，我只讲关键的步骤。要想让 kube-prometheus 能获取到 WireGuard 的指标，需要创建相应的 ServiceMonitor 资源，资源清单如下：\n# prometheus-serviceMonitorWireguard.yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app: wireguard-exporter name: wireguard-exporter namespace: monitoring spec: endpoints: - interval: 15s port: http-metrics namespaceSelector: matchNames: - default selector: matchLabels: app: wireguard-exporter --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app: wireguard-exporter-go name: wireguard-exporter-go namespace: monitoring spec: endpoints: - interval: 15s port: http-metrics namespaceSelector: matchNames: - default selector: matchLabels: app: wireguard-exporter-go 使用资源清单创建 ServiceMonitor：\n$ kubectl apply -f prometheus-serviceMonitorWireguard.yaml 查看 Prometheus 中对应的 Target 是否已经获取成功：\n最后在 Grafana 中添加仪表盘，通过环境变量来切换不同 wg 接口的监控仪表盘。\n至于仪表盘的语法细节，我就不展开讲了，感兴趣的可以先导入我的仪表盘，后面遇到不懂的再来问我。仪表盘 json 文件链接：\nhttps://cdn.jsdelivr.net/gh/yangchuansheng/docker-image@master/wireguard_exporter/dashboard.json ","date":"2021年3月11日","externalUrl":null,"permalink":"/posts/monitoring-wireguard-using-prometheus/","section":"博客","summary":"云原生是一种信仰，是一种全新的技术模式，它不局限于你脑海中固","title":"WireGuard 教程：使用 Prometheus 监控 WireGuard","type":"posts"},{"content":" 写了这么多篇 WireGuard 相关的保姆教程，今天终于牵扯到 Kubernetes 了，不然怎么对得起“云原生”这三个字。如果看到这篇文章的你仍然是个 WireGuard 新手，请务必按照以下顺序阅读每一篇文章：\nWireGuard 教程：WireGuard 的工作原理 WireGuard 快速安装教程 WireGuard 配置教程：使用 wg-gen-web 来管理 WireGuard 的配置 Wireguard 全互联模式（full mesh）配置指南 如果遇到不明白的，可以参考这篇文章的注解：\nWireGuard 教程：WireGuard 的搭建使用与配置详解 剩下这几篇文章是可选的，有兴趣就看看：\n我为什么不鼓吹 WireGuard Why not \u0026ldquo;Why not WireGuard?\u0026rdquo; WireGuard 教程：使用 DNS-SD 进行 NAT-to-NAT 穿透 WireGuard 在云原生领域的应用有两个方面：组网和加密。不管是组网还是加密，其实都是和 CNI 有关，你可以在原有的组网方案上利用 WireGuard 进行加密，也可以直接利用 WireGuard 来进行组网。目前直接利用 WireGuard 进行组网的 CNI 有 Flannel、 Wormhole 和 Kilo，只利用 WireGuard 进行数据加密的 CNI 只有 Calico，当然 Flannel 也可以和 Kilo 结合使用，这样就只利用 WireGuard 来进行加密了。\n我的兴趣点还是在于利用 WireGuard 组网，想象一下，你在 AWS、Azure、GCP 和阿里云上分别薅了一台云主机，你想将这四台云主机组建成一个 k3s 集群，而且在任何一个设备上都能直接访问这个 k3s 集群中的 Pod IP 和 Service IP，如何才能优雅地实现这个目标？\n要分两步走：第一步是打通 k3s 集群各个节点之间的容器网络，最后一步是打通本地与云上容器之间的网络。先来看第一步，跨云打通容器网络，这一步主要还是得仰仗 CNI。Flannel 的自定义选项比较少，Whormhole 已经很久没更新了，推荐使用 Kilo 来作为 k3s 的 CNI。\n在部署 Kilo 之前，需要调整 k3s 的启动参数，取消默认的 CNI：\nk3s server --flannel-backend none ... 然后重启 k3s server：\n$ systemctl restart k3s 具体可以参考 k3s 控制平面的部署。如果你是从零开始部署 k3s，请参考 跨云厂商部署 k3s 集群。\n1. Kilo 网络拓扑 # Kilo 支持以下三种网络拓扑：\n逻辑分组互联模式（Logical Groups） # 默认情况下，Kilo 会在集群中的不同逻辑区域（例如数据中心、云服务商等）之间创建一个 mesh 网络。Kilo 默认会尝试使用节点标签 topology.kubernetes.io/region 来判断节点所在的逻辑区域，你也可以通过 Kilo 的启动参数 --topology-label=\u0026lt;label\u0026gt; 来指定逻辑区域的标签，还可以为 node 添加 annotation kilo.squat.ai/location 来指定逻辑区域的标签。\n例如，为了将 GCP 和 AWS 的节点加入到同一个 k3s 集群中，可以通过以下命令对所有 GCP 的节点添加注释：\n$ for node in $(kubectl get nodes | grep -i gcp | awk \u0026#39;{print $1}\u0026#39;); do kubectl annotate node $node kilo.squat.ai/location=\u0026#34;gcp\u0026#34;; done 这样所有添加了注释的节点都会被划分到同一个逻辑区域下，没有添加注释的节点会被划分到默认的逻辑区域下，所以总共有两个逻辑区域。每个逻辑区域都会选出一个 leader 和其他区域的 leader 之间建立 WireGuard 隧道，同时区域内部的节点之间通过 Bridge 模式打通容器的网络。\n通过 kgctl 可以获取网络拓扑架构图：\n$ kgctl graph | circo -Tsvg \u0026gt; cluster.svg 全互联模式（Full Mesh） # 全互联模式其实就是逻辑分组互联模式的特例，即每一个节点都是一个逻辑区域，每个节点和其他所有节点都建立 WireGuard 隧道。关于全互联模式的更多详细内容请参考 Wireguard 全互联模式（full mesh）配置指南。可以通过 Kilo 的启动参数 --mesh-granularity=full 来指定全互联模式。\n通过 kgctl 可以获取网络拓扑架构图：\n$ kgctl graph | circo -Tsvg \u0026gt; cluster.svg 混合模式 # 混合模式就是逻辑分组模式和全互联模式相结合，例如，如果集群中既有 GCP 的节点，还有一些无安全私有网段的裸金属节点，可以把 GCP 的节点放到同一个逻辑区域中，其他裸金属节点之间直接使用全互联模式连接，这就是混合模式。具体的操作方式是给 GCP 节点添加同一个 annotation，其他裸金属节点都添加相互独立的 annotation：\n$ for node in $(kubectl get nodes | grep -i gcp | awk \u0026#39;{print $1}\u0026#39;); do kubectl annotate node $node kilo.squat.ai/location=\u0026#34;gcp\u0026#34;; done $ for node in $(kubectl get nodes | tail -n +2 | grep -v gcp | awk \u0026#39;{print $1}\u0026#39;); do kubectl annotate node $node kilo.squat.ai/location=\u0026#34;$node\u0026#34;; done 通过 kgctl 获取网络拓扑架构图：\n$ kgctl graph | circo -Tsvg \u0026gt; cluster.svg 如果集群中还包含 AWS 节点，可以这么添加 annotation：\n$ for node in $(kubectl get nodes | grep -i aws | awk \u0026#39;{print $1}\u0026#39;); do kubectl annotate node $node kilo.squat.ai/location=\u0026#34;aws\u0026#34;; done $ for node in $(kubectl get nodes | grep -i gcp | awk \u0026#39;{print $1}\u0026#39;); do kubectl annotate node $node kilo.squat.ai/location=\u0026#34;gcp\u0026#34;; done $ for node in $(kubectl get nodes | tail -n +2 | grep -v aws | grep -v gcp | awk \u0026#39;{print $1}\u0026#39;); do kubectl annotate node $node kilo.squat.ai/location=\u0026#34;$node\u0026#34;; done 网络拓扑架构图如下：\n2. Kilo 部署 # 如果你用的是国内的云主机，一般都绑定了 IP 地址和 MAC 地址，也无法关闭源地址检测，无法使用 Bridge 模式，也就无法使用 Kilo 的逻辑分组互联模式，只能使用全互联模式。如果集群中还包含了数据中心，数据中心的节点之间是可以使用 Bridge 模式的，可以给数据中心的节点添加相同的 annotation，其他节点添加各不相同的 annotation。\n我的节点都是国内公有云节点，无法使用逻辑分组互联模式，只能使用全互联模式。本节就以全互联模式为例，演示如何部署 Kilo。\nKilo 需要用到 kubeconfig，所以需要提前将 kubeconfig 文件从 Master 拷贝到所有 Node：\n$ scp -r /etc/rancher/k3s/ nodexxx:/etc/rancher/k3s/ 修改 kubeconfig 文件，将 API Server 的地址改为 Master 的公网地址：\napiVersion: v1 clusters: - cluster: certificate-authority-data: ******* server: https://\u0026lt;MASTER_PUBLIC_IP\u0026gt;:6443 name: default ... ... 给每个节点添加相关的 annotaion：\n# 指定 WireGuard 建立隧道的 Endpoint 公网 IP:Port $ kubectl annotate nodes xxx kilo.squat.ai/force-endpoint=\u0026lt;Public_IP:Port\u0026gt; # 指定节点的内网 IP，WireGuard 会将其添加到 allowed ips 中，这样可以打通各个节点的内网 IP $ kubectl annotate nodes xxx kilo.squat.ai/force-internal-ip=\u0026lt;Private_IP\u0026gt; 克隆 Kilo 的官方仓库，进入部署清单目录：\n$ git clone https://github.com/squat/kilo $ cd kilo/manifests 修改 kilo 部署清单，调整启动参数：\n... apiVersion: apps/v1 kind: DaemonSet metadata: name: kilo namespace: kube-system labels: app.kubernetes.io/name: kilo spec: selector: matchLabels: app.kubernetes.io/name: kilo template: metadata: labels: app.kubernetes.io/name: kilo spec: serviceAccountName: kilo hostNetwork: true containers: - name: kilo image: squat/kilo args: - --kubeconfig=/etc/kubernetes/kubeconfig - --hostname=$(NODE_NAME) + - --encapsulate=never + - --mesh-granularity=full ... ... --encapsulate=never 表示不使用 ipip 协议对同一个逻辑区域内的容器网络流量进行加密。 --mesh-granularity=full 表示启用全互联模式。 使用部署清单部署 kilo：\n$ kubectl apply -f kilo-k3s.yaml 部署成功后，每台节点会增加两个网络接口：\n14: kilo0: \u0026lt;POINTOPOINT,NOARP,UP,LOWER_UP\u0026gt; mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 link/none inet 10.4.0.1/16 brd 10.4.255.255 scope global kilo0 valid_lft forever preferred_lft forever 6: kube-bridge: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 qdisc noqueue state UP group default qlen 1000 link/ether 2a:7d:32:71:75:97 brd ff:ff:ff:ff:ff:ff inet 10.42.0.1/24 scope global kube-bridge valid_lft forever preferred_lft forever inet6 fe80::287d:32ff:fe71:7597/64 scope link valid_lft forever preferred_lft forever 其中 kilo0 是 WireGuard 虚拟网络接口：\n$ ip -d link show kilo0 14: kilo0: \u0026lt;POINTOPOINT,NOARP,UP,LOWER_UP\u0026gt; mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/none promiscuity 0 wireguard addrgenmode none numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 $ wg show kilo0 interface: kilo0 public key: VLAjOkfb1U3/ftNOVtAjY8P3hafR12qQB05ueUJtLBQ= private key: (hidden) listening port: 51820 peer: JznFuu9Q7gXcfHFGRLB/LirKi8ttSX22T5f+1cWomzA= endpoint: xxxx:51820 allowed ips: 10.42.1.0/24, 192.168.20.1/32, 10.4.0.2/32 latest handshake: 51 seconds ago transfer: 88.91 MiB received, 76.11 MiB sent peer: gOvNh1FHJKtfigxV1Az5OFCq2WMq3YEn2F4H4xknVFI= endpoint: xxxx:51820 allowed ips: 10.42.2.0/24, 192.168.30.1/32, 10.4.0.3/32 latest handshake: 17 seconds ago transfer: 40.86 MiB received, 733.03 MiB sent ... ... kube-bridge 是本地容器网络 veth pair 所连接的 Bridge：\n$ bridge link show kube-bridge 7: veth99d2f30b state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 8: vethfb6d487c state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 10: veth88ae725c state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 11: veth4c0d00d8 state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 12: veth5ae51319 state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 13: vethe5796697 state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 15: vethe169cdda state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 21: vethfe78e116 state UP @wg0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1420 master kube-bridge state forwarding priority 32 cost 2 至此 Kilo 的全互联模式就部署好了，跨公有云的各个云主机节点上的容器已经可以相互通信，下一步就是打通本地与云上容器之间的网络。\n3. 打通本地与云上容器网络 # 为了便于理解，先来做个假设，假设有 4 个公有云节点，分别是 AWS、Azure、GCP、阿里云，再假设 Service 的子网是 10.43.0.0/16，Pod 的子网是 10.42.0.0/16，那么每台节点的 Pod 子网分别为 10.42.0.0/24、10.42.1.0/24、10.42.2.0/24、10.42.3.0/24。\n为了和 Kubernetes 集群网络分开，需要使用一个新的网络接口 wg0，网络架构还是建议使用全互联模式，具体可参考 Wireguard 全互联模式（full mesh）配置指南。\n为了让本地客户端能访问云上的 Pod IP，可以让本地访问 AWS 节点的 10.42.0.0/24，访问 Azure 节点的 10.42.1.0/24，以此类推。当然也可以直接让本地访问任意一个云上节点的 10.42.0.0/16，不过我还是不建议使用这种架构。\n至于 Service IP，并没有像 Pod 一样给每个节点划分一个更细粒度的子网，所有的节点都从同一个大的子网中分配，所以无法采用上面的方式，只能选择其中一个节点来集中转发本地客户端访问 Service 的流量，假设选择 AWS 的节点。\n还是和之前一样，继续使用 wg-gen-web 来管理 WireGuard 的配置，假设使用 AWS 的节点来安装 wg-gen-web。\n这里有一个地方需要注意，kilo0 已经打通了 k3s 各个节点的私有网段，所以 wg0 不再需要打通私有网段，将 k3s 各个节点的私有网段删除即可：\n先增加一个新配置给本地客户端使用，Allowed IPs 中新增 10.42.0.0/24 和 10.43.0.0/16，让本地客户端能访问 AWS 节点中的 Pod IP 和整个集群的 Service IP：\n这时你会发现 AWS 节点中的 wg0.conf 中已经包含了本地客户端的配置：\n$ cat /etc/wireguard/wg0.conf ... # macOS / / Updated: 2021-03-01 05:52:20.355083356 +0000 UTC / Created: 2021-03-01 05:52:20.355083356 +0000 UTC [Peer] PublicKey = CEN+s+jpMX1qzQRwbfkfYtHoJ+Hqq4APfISUkxmQ0hQ= PresharedKey = pSAxmHb6xXRMl9667pFMLg/1cRBFDRjcVdD7PKtMP1M= AllowedIPs = 10.0.0.5/32 ... 修改 Azure 节点的 WireGuard 配置文件，添加本地客户端的配置：\n$ cat Azure.conf [Interface] Address = 10.0.0.2/32 PrivateKey = IFhAyIWY7sZmabsqDDESj9fqoniE/uZFNIvAfYHjN2o= PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] PublicKey = JgvmQFmhUtUoS3xFMFwEgP3L1Wnd8hJc3laJ90Gwzko= PresharedKey = 1SyJuVp16Puh8Spyl81EgD9PJZGoTLJ2mOccs2UWDvs= AllowedIPs = 10.0.0.1/32 Endpoint = aws.com:51820 # Aliyun / / Updated: 2021-02-24 07:57:45.941019829 +0000 UTC / Created: 2021-02-24 07:57:45.941019829 +0000 UTC [Peer] PublicKey = kVq2ATMTckCKEJFF4TM3QYibxzlh+b9CV4GZ4meQYAo= AllowedIPs = 10.0.0.4/32 Endpoint = aliyun.com:51820 # GCP / / Updated: 2021-02-24 07:57:27.3555646 +0000 UTC / Created: 2021-02-24 07:57:27.3555646 +0000 UTC [Peer] PublicKey = qn0Xfyzs6bLKgKcfXwcSt91DUxSbtATDIfe4xwsnsGg= AllowedIPs = 10.0.0.3/32 Endpoint = gcp.com:51820 # macOS / / Updated: 2021-03-01 05:52:20.355083356 +0000 UTC / Created: 2021-03-01 05:52:20.355083356 +0000 UTC [Peer] PublicKey = CEN+s+jpMX1qzQRwbfkfYtHoJ+Hqq4APfISUkxmQ0hQ= AllowedIPs = 10.0.0.5/32 同理，GCP 和 Aliyun 节点也要添加新增的本地客户端配置。\n下载本地客户端的配置文件：\n将 AWS 节点的 wg0.conf 中的 Aliyun、GCP 和 Azure 的配置拷贝到本地客户端的配置中，并删除 PresharedKey 的配置，再添加 Endpoint 的配置和相应的 Pod IP 所在的网段：\n[Interface] Address = 10.0.0.5/32 PrivateKey = wD595KeTPKBDneKWOTUjJQjxZ5RrlxsbeEsWL0gbyn8= [Peer] PublicKey = JgvmQFmhUtUoS3xFMFwEgP3L1Wnd8hJc3laJ90Gwzko= PresharedKey = 5htJA/UoIulrgAn9tDdUxt1WYmOriCXIujBVVaz/uZI= AllowedIPs = 10.0.0.1/32, 10.42.0.0/24, 10.43.0.0/16 Endpoint = aws.com:51820 # Aliyun / / Updated: 2021-02-24 07:57:45.941019829 +0000 UTC / Created: 2021-02-24 07:57:45.941019829 +0000 UTC [Peer] PublicKey = kVq2ATMTckCKEJFF4TM3QYibxzlh+b9CV4GZ4meQYAo= AllowedIPs = 10.0.0.4/32, 10.42.3.0/24 Endpoint = aliyun.com:51820 # GCP / / Updated: 2021-02-24 07:57:27.3555646 +0000 UTC / Created: 2021-02-24 07:57:27.3555646 +0000 UTC [Peer] PublicKey = qn0Xfyzs6bLKgKcfXwcSt91DUxSbtATDIfe4xwsnsGg= AllowedIPs = 10.0.0.3/32, 10.42.2.0/24 Endpoint = gcp.com:51820 # Azure / / Updated: 2021-02-24 07:57:00.751653134 +0000 UTC / Created: 2021-02-24 07:43:52.717385042 +0000 UTC [Peer] PublicKey = OzdH42suuOpVY5wxPrxM+rEAyEPFg2eL0ZI29N7eSTY= AllowedIPs = 10.0.0.2/32, 10.42.1.0/24 Endpoint = azure.com:51820 最后在本地把 WireGuard 跑起来，就可以畅游云主机的 Kubernetes 集群了。\n如果你还想更进一步，在任何一个设备上都能通过 Service 的名称来访问 k3s 集群中的服务，就得在 CoreDNS 上做文章了，感兴趣的可以自己研究下。\n这个坑总算填完了，WireGuard 系列暂时就告一段落了，后面如果发现了更有趣的玩法，我会第一时间给大家分享出来。\n","date":"2021年2月27日","externalUrl":null,"permalink":"/posts/use-wireguard-as-kubernetes-cni/","section":"博客","summary":"写了这么多篇 WireGuard 相关的保姆教程，今天终于牵扯到 Kubernetes 了，不然怎么对","title":"Kilo 使用教程","type":"posts"},{"content":"上篇文章给大家介绍了如何 使用 wg-gen-web 来方便快捷地管理 WireGuard 的配置和秘钥，文末埋了两个坑：一个是 WireGuard 的全互联模式（full mesh），另一个是使用 WireGuard 作为 Kubernetes 的 CNI 插件。今天就来填第一个坑。\n首先解释一下什么是全互联模式（full mesh），全互联模式其实就是一种网络连接形式，即所有结点之间都直接连接，不会通过第三方节点中转流量。和前面提到的 点对多点架构 其实是一个意思。\n1. 全互联模式架构与配置 # 在 WireGuard 的世界里没有 Server 和 Client 之分，所有的节点都是 Peer。大家使用 WireGuard 的常规做法是找一个节点作为中转节点，也就是 VPN 网关，然后所有的节点都和这个网关进行连接，所有节点之间都通过这个网关来进行通信。这种架构中，为了方便理解，我们可以把网关看成 Server，其他的节点看成 Client，但实际上是不区分 Server 和 Client 的。\n举个例子，假设有 4 个节点，分别是 A/B/C/D，且这 4 个节点都不在同一个局域网，常规的做法是选取一个节点作为 VPN 网关，架构如图：\n这种架构的缺点我在之前的文章里也介绍过了，缺点相当明显：\n当 Peer 越来越多时，VPN 网关就会变成垂直扩展的瓶颈。 通过 VPN 网关转发流量的成本很高，毕竟云服务器的流量很贵。 通过 VPN 网关转发流量会带来很高的延迟。 那么全互联模式是什么样的架构呢？还是假设有 A/B/C/D 四个节点，每个节点都和其他节点建立 WireGuard 隧道，架构如图：\n这种架构带来的直接优势就是快！任意一个 Peer 和其他所有 Peer 都是直连，无需中转流量。那么在 WireGuard 的场景下如何实现全互联模式呢？其实这个问题不难，难点在于配置的繁琐程度，本文的主要目标就是精简 WireGuard 全互联模式的配置流程。为了让大家更容易理解，咱们还是先通过架构图来体现各个 Peer 的配置：\n配置一目了然，每个 Peer 和其他所有 Peer 都是直连，根本没有 VPN 网关这种角色。当然，现实世界的状况没有图中这么简单，有些 Peer 是没有公网 IP 的，躲在 NAT 后面，这里又分两种情况：\nNAT 受自己控制。这种情况可以在公网出口设置端口转发，其他 Peer 就可以通过这个公网 IP 和端口连接当前 Peer。如果公网 IP 是动态的，可以通过 DDNS 来解决，但 DDNS 会出现一些小问题，解决方法可以参考 WireGuard 的优化。 NAT 不受自己控制。这种情况无法在公网出口设置端口转发，只能通过 UDP 打洞来实现互联，具体可以参考 WireGuard 教程：使用 DNS-SD 进行 NAT-to-NAT 穿透。 接着上述方案再更进一步，打通所有 Peer 的私有网段，让任意一个 Peer 可以访问其他所有 Peer 的私有网段的机器。上述配置只是初步完成了全互联，让每个 Peer 可以相互访问彼此而已，要想相互访问私有网段，还得继续增加配置，还是直接看图：\n红色字体部分就是新增的配置，表示允许访问相应 Peer 的私有网段，就是这么简单。详细的配置步骤请看下一节。\n2. 全互联模式最佳实践 # 对如何配置有了清晰的思路之后，接下来就可以进入实践环节了。我不打算从 WireGuard 安装开始讲起，而是以前几篇文章为基础添砖加瓦。所以我建议读者先按顺序看下这两篇文章：\nWireGuard 快速安装教程 WireGuard 配置教程：使用 wg-gen-web 来管理 WireGuard 的配置 咱们直接从配置开始说起。手撸配置的做法是不明智的，因为当节点增多之后工作量会很大，我还是建议通过图形化界面来管理配置，首选 wg-gen-web。\n现在还是假设有上节所述的 4 个 Peer，我们需要从中挑选一个 Peer 来安装 wg-gen-web，然后通过 wg-gen-web 来生成配置。挑选哪个 Peer 无所谓，这个没有特殊限制，这里假设挑选 AWS 来安装 wg-gen-web。\n安装的步骤直接略过，不是本文的重点，不清楚的可以阅读我之前的文章 WireGuard 配置教程：使用 wg-gen-web 来管理 WireGuard 的配置。Server 配置如图：\n生成 Azure 的配置：\nSUBMIT 之后再点击 EDIT，添加私有网段：\n查看 wg0.conf 的内容：\n$ cat /etc/wireguard/wg0.conf # Updated: 2021-02-24 07:34:23.805535396 +0000 UTC / Created: 2021-02-24 07:24:02.208816462 +0000 UTC [Interface] Address = 10.0.0.1/24 ListenPort = 51820 PrivateKey = eEnHKGkGksx0jqrEDogjRj5l417BrEA39lr7WW9L9U0= PreUp = echo WireGuard PreUp PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PreDown = echo WireGuard PreDown PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE # Azure / / Updated: 2021-02-24 07:43:52.717385042 +0000 UTC / Created: 2021-02-24 07:43:52.717385042 +0000 UTC [Peer] PublicKey = OzdH42suuOpVY5wxPrxM+rEAyEPFg2eL0ZI29N7eSTY= PresharedKey = 1SyJuVp16Puh8Spyl81EgD9PJZGoTLJ2mOccs2UWDvs= AllowedIPs = 10.0.0.2/32, 192.168.20.0/24 下载 Azure 配置文件：\n可以看到配置文件内容为：\n$ cat Azure.conf [Interface] Address = 10.0.0.2/32, 192.168.20.0/24 PrivateKey = IFhAyIWY7sZmabsqDDESj9fqoniE/uZFNIvAfYHjN2o= [Peer] PublicKey = JgvmQFmhUtUoS3xFMFwEgP3L1Wnd8hJc3laJ90Gwzko= PresharedKey = 1SyJuVp16Puh8Spyl81EgD9PJZGoTLJ2mOccs2UWDvs= AllowedIPs = 10.0.0.1/32, 192.168.10.0/24 Endpoint = aws.com:51820 先不急着修改，一鼓作气生成所有 Peer 的配置文件：\n这时你会发现 wg0.conf 中已经包含了所有 Peer 的配置：\n$ cat /etc/wireguard/wg0.conf # Updated: 2021-02-24 07:57:00.745287945 +0000 UTC / Created: 2021-02-24 07:24:02.208816462 +0000 UTC [Interface] Address = 10.0.0.1/24 ListenPort = 51820 PrivateKey = eEnHKGkGksx0jqrEDogjRj5l417BrEA39lr7WW9L9U0= PreUp = echo WireGuard PreUp PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PreDown = echo WireGuard PreDown PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE # Aliyun / / Updated: 2021-02-24 07:57:45.941019829 +0000 UTC / Created: 2021-02-24 07:57:45.941019829 +0000 UTC [Peer] PublicKey = kVq2ATMTckCKEJFF4TM3QYibxzlh+b9CV4GZ4meQYAo= PresharedKey = v818B5etpRlyVYHGUrv9abM5AIQK5xeoCizdWj1AqcE= AllowedIPs = 10.0.0.4/32, 192.168.40.0/24 # GCP / / Updated: 2021-02-24 07:57:27.3555646 +0000 UTC / Created: 2021-02-24 07:57:27.3555646 +0000 UTC [Peer] PublicKey = qn0Xfyzs6bLKgKcfXwcSt91DUxSbtATDIfe4xwsnsGg= PresharedKey = T5UsVvOEYwfMJQDJudC2ryKeCpnO3RV8GFMoi76ayyI= AllowedIPs = 10.0.0.3/32, 192.168.30.0/24 # Azure / / Updated: 2021-02-24 07:57:00.751653134 +0000 UTC / Created: 2021-02-24 07:43:52.717385042 +0000 UTC [Peer] PublicKey = OzdH42suuOpVY5wxPrxM+rEAyEPFg2eL0ZI29N7eSTY= PresharedKey = 1SyJuVp16Puh8Spyl81EgD9PJZGoTLJ2mOccs2UWDvs= AllowedIPs = 10.0.0.2/32, 192.168.20.0/24 现在问题就好办了，我们只需将 wg0.conf 中的 Aliyun 和 GCP 部分的配置拷贝到 Azure 的配置中，并删除 PresharedKey 的配置，再添加 Endpoint 的配置和 PostUP/PostDown 规则，最后别忘了删除 Address 中的私有网段：\n$ cat Azure.conf [Interface] Address = 10.0.0.2/32 PrivateKey = IFhAyIWY7sZmabsqDDESj9fqoniE/uZFNIvAfYHjN2o= PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] PublicKey = JgvmQFmhUtUoS3xFMFwEgP3L1Wnd8hJc3laJ90Gwzko= PresharedKey = 1SyJuVp16Puh8Spyl81EgD9PJZGoTLJ2mOccs2UWDvs= AllowedIPs = 10.0.0.1/32, 192.168.10.0/24 Endpoint = aws.com:51820 # Aliyun / / Updated: 2021-02-24 07:57:45.941019829 +0000 UTC / Created: 2021-02-24 07:57:45.941019829 +0000 UTC [Peer] PublicKey = kVq2ATMTckCKEJFF4TM3QYibxzlh+b9CV4GZ4meQYAo= AllowedIPs = 10.0.0.4/32, 192.168.40.0/24 Endpoint = aliyun.com:51820 # GCP / / Updated: 2021-02-24 07:57:27.3555646 +0000 UTC / Created: 2021-02-24 07:57:27.3555646 +0000 UTC [Peer] PublicKey = qn0Xfyzs6bLKgKcfXwcSt91DUxSbtATDIfe4xwsnsGg= AllowedIPs = 10.0.0.3/32, 192.168.30.0/24 Endpoint = gcp.com:51820 同理，GCP 的配置如下：\n$ cat GCP.conf [Interface] Address = 10.0.0.3/32 PrivateKey = oK2gIMBAob67Amj2gT+wR9pzkbqWGNtq794nOoD3i2o= PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] PublicKey = JgvmQFmhUtUoS3xFMFwEgP3L1Wnd8hJc3laJ90Gwzko= PresharedKey = T5UsVvOEYwfMJQDJudC2ryKeCpnO3RV8GFMoi76ayyI= AllowedIPs = 10.0.0.1/32, 192.168.10.0/24 Endpoint = aws.com:51820 # Aliyun / / Updated: 2021-02-24 07:57:45.941019829 +0000 UTC / Created: 2021-02-24 07:57:45.941019829 +0000 UTC [Peer] PublicKey = kVq2ATMTckCKEJFF4TM3QYibxzlh+b9CV4GZ4meQYAo= AllowedIPs = 10.0.0.4/32, 192.168.40.0/24 Endpoint = aliyun.com:51820 # Azure / / Updated: 2021-02-24 07:57:00.751653134 +0000 UTC / Created: 2021-02-24 07:43:52.717385042 +0000 UTC [Peer] PublicKey = OzdH42suuOpVY5wxPrxM+rEAyEPFg2eL0ZI29N7eSTY= AllowedIPs = 10.0.0.2/32, 192.168.20.0/24 Endpoint = azure.com:51820 Aliyun 的配置如下：\n$ cat Aliyun.conf [Interface] Address = 10.0.0.4/32 PrivateKey = +A1ZESJjmHuskB4yKqTcqC3CB24TwBKHGSffWDHxI28= PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] PublicKey = JgvmQFmhUtUoS3xFMFwEgP3L1Wnd8hJc3laJ90Gwzko= PresharedKey = v818B5etpRlyVYHGUrv9abM5AIQK5xeoCizdWj1AqcE= AllowedIPs = 10.0.0.1/32, 192.168.10.0/24 Endpoint = aws.com:51820 # GCP / / Updated: 2021-02-24 07:57:27.3555646 +0000 UTC / Created: 2021-02-24 07:57:27.3555646 +0000 UTC [Peer] PublicKey = qn0Xfyzs6bLKgKcfXwcSt91DUxSbtATDIfe4xwsnsGg= AllowedIPs = 10.0.0.3/32, 192.168.30.0/24 Endpoint = gcp.com:51820 # Azure / / Updated: 2021-02-24 07:57:00.751653134 +0000 UTC / Created: 2021-02-24 07:43:52.717385042 +0000 UTC [Peer] PublicKey = OzdH42suuOpVY5wxPrxM+rEAyEPFg2eL0ZI29N7eSTY= AllowedIPs = 10.0.0.2/32, 192.168.20.0/24 Endpoint = azure.com:51820 最后在各自的节点上通过各自的配置文件把 WireGuard 跑起来，就搞定了。\n整个图形化界面配置过程中不需要手动调整配置，功能还是比较完善的，只有客户端的配置需要手动调整。如果你无法接受手动调整配置，可以尝试另外一个项目： wg-meshconf，这个项目专门用来生成 mesh 的配置，但没有图形化管理界面。各有利弊吧，大家自行选择。\n3. 总结 # 我知道，很多人可能还是一头雾水，这玩意儿的应用场景有哪些？我随便举个简单的例子，假设你在云服务器上部署了 Kubernetes 集群，可以用本地的机器和云服务器的某台节点组建 WireGuard 隧道，然后在本地的 AllowedIPs 中加上 Pod 网段和 Service 网段，就可以那啥了，你懂吧？\n好吧，又埋了一个坑，关于如何在家中直接访问云服务器 k8s 集群的 Pod IP 和 Service IP，后面会有专门的文章给大家讲解，虽然我也不确定是多久以后。。\n","date":"2021年2月23日","externalUrl":null,"permalink":"/posts/wireguard-full-mesh/","section":"博客","summary":"上篇文章给大家介绍了如何 使用 wg-gen-web 来方便快捷地管理 WireGuard 的配置和秘钥","title":"Wireguard 全互联模式（full mesh）配置指南","type":"posts"},{"content":" WireGuard 是由 Jason A. Donenfeld 等人创建的下一代开源 VPN 协议，旨在解决许多困扰 IPSec/IKEv2、OpenVPN 或 L2TP 等其他 VPN 协议的问题。2020 年 1 月 29 日，WireGuard 正式合并进入 Linux 5.6 内核主线。\n利用 WireGuard 我们可以实现很多非常奇妙的功能，比如跨公有云组建 Kubernetes 集群，本地直接访问公有云 Kubernetes 集群中的 Pod IP 和 Service IP，在家中没有公网 IP 的情况下直连家中的设备，等等。\n如果你是第一次听说 WireGuard，建议你花点时间看看我之前写的 WireGuard 工作原理。然后可以参考下面两篇文章来快速上手：\nWireGuard 快速安装教程 WireGuard 配置教程：使用 wg-gen-web 来管理 WireGuard 的配置 如果遇到某些细节不太明白的，再去参考 WireGuard 配置详解。\n本文将探讨 WireGuard 使用过程中遇到的一个重大难题：如何使两个位于 NAT 后面（且没有指定公网出口）的客户端之间直接建立连接。\nWireGuard 不区分服务端和客户端，大家都是客户端，与自己连接的所有客户端都被称之为 Peer。\n1. IP 不固定的 Peer # WireGuard 的核心部分是 加密密钥路由（Cryptokey Routing），它的工作原理是将公钥和 IP 地址列表（AllowedIPs）关联起来。每一个网络接口都有一个私钥和一个 Peer 列表，每一个 Peer 都有一个公钥和 IP 地址列表。发送数据时，可以把 IP 地址列表看成路由表；接收数据时，可以把 IP 地址列表看成访问控制列表。\n公钥和 IP 地址列表的关联组成了 Peer 的必要配置，从隧道验证的角度看，根本不需要 Peer 具备静态 IP 地址。理论上，如果 Peer 的 IP 地址不同时发生变化，WireGuard 是可以实现 IP 漫游的。\n现在回到最初的问题：假设两个 Peer 都在 NAT 后面，且这个 NAT 不受我们控制，无法配置 UDP 端口转发，即无法指定公网出口，要想建立连接，不仅要动态发现 Peer 的 IP 地址，还要发现 Peer 的端口。\n找了一圈下来，现有的工具根本无法实现这个需求，本文将致力于不对 WireGuard 源码做任何改动的情况下实现上述需求。\n2. 中心辐射型网络拓扑 # 你可能会问我为什么不使用 中心辐射型（hub-and-spoke）网络拓扑？中心辐射型网络有一个 VPN 网关，这个网关通常都有一个静态 IP 地址，其他所有的客户端都需要连接这个 VPN 网关，再由网关将流量转发到其他的客户端。假设 Alice 和 Bob 都位于 NAT 后面，那么 Alice 和 Bob 都要和网关建立隧道，然后 Alice 和 Bob 之间就可以通过 VPN 网关转发流量来实现相互通信。\n其实这个方法是如今大家都在用的方法，已经没什么可说的了，缺点相当明显：\n当 Peer 越来越多时，VPN 网关就会变成垂直扩展的瓶颈。 通过 VPN 网关转发流量的成本很高，毕竟云服务器的流量很贵。 通过 VPN 网关转发流量会带来很高的延迟。 本文想探讨的是 Alice 和 Bob 之间直接建立隧道，中心辐射型（hub-and-spoke）网络拓扑是无法做到的。\n3. NAT 穿透 # 要想在 Alice 和 Bob 之间直接建立一个 WireGuard 隧道，就需要它们能够穿过挡在它们面前的 NAT。由于 WireGuard 是通过 UDP 来相互通信的，所以理论上 UDP 打洞（UDP hole punching） 是最佳选择。\nUDP 打洞（UDP hole punching）利用了这样一个事实：大多数 NAT 在将入站数据包与现有的连接进行匹配时都很宽松。这样就可以重复使用端口状态来打洞，因为 NAT 路由器不会限制只接收来自原始目的地址（信使服务器）的流量，其他客户端的流量也可以接收。\n举个例子，假设 Alice 向新主机 Carol 发送一个 UDP 数据包，而 Bob 此时通过某种方法获取到了 Alice 的 NAT 在地址转换过程中使用的出站源 IP:Port，Bob 就可以向这个 IP:Port（2.2.2.2:7777） 发送 UDP 数据包来和 Alice 建立联系。\n其实上面讨论的就是完全圆锥型 NAT（Full cone NAT），即一对一（one-to-one）NAT。它具有以下特点：\n一旦内部地址（iAddr:iPort）映射到外部地址（eAddr:ePort），所有发自 iAddr:iPort 的数据包都经由 eAddr:ePort 向外发送。 任意外部主机都能经由发送数据包给 eAddr:ePort 到达 iAddr:iPort。 大部分的 NAT 都是这种 NAT，对于其他少数不常见的 NAT，这种打洞方法有一定的局限性，无法顺利使用。\n4. STUN # 回到上面的例子，UDP 打洞过程中有几个问题至关重要：\nAlice 如何才能知道自己的公网 IP:Port？ Alice 如何与 Bob 建立连接？ 在 WireGuard 中如何利用 UDP 打洞？ RFC5389 关于 STUN（Session Traversal Utilities for NAT，NAT会话穿越应用程序）的详细描述中定义了一个协议回答了上面的一部分问题，这是一篇内容很长的 RFC，所以我将尽我所能对其进行总结。先提醒一下，STUN 并不能直接解决上面的问题，它只是个扳手，你还得拿他去打造一个称手的工具：\nSTUN 本身并不是 NAT 穿透问题的解决方案，它只是定义了一个机制，你可以用这个机制来组建实际的解决方案。\n— RFC5389\nSTUN（Session Traversal Utilities for NAT，NAT会话穿越应用程序）是一种网络协议，它允许位于NAT（或多重NAT）后的客户端找出自己的公网地址，查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的公网端口。这些信息被用来在两个同时处于 NAT 路由器之后的主机之间建立 UDP 通信。该协议由 RFC 5389 定义。\nSTUN 是一个客户端－服务端协议，在上图的例子中，Alice 是客户端，Carol 是服务端。Alice 向 Carol 发送一个 STUN Binding 请求，当 Binding 请求通过 Alice 的 NAT 时，源 IP:Port 会被重写。当 Carol 收到 Binding 请求后，会将三层和四层的源 IP:Port 复制到 Binding 响应的有效载荷中，并将其发送给 Alice。Binding 响应通过 Alice 的 NAT 转发到内网的 Alice，此时的目标 IP:Port 被重写成了内网地址，但有效载荷保持不变。Alice 收到 Binding 响应后，就会意识到这个 Socket 的公网 IP:Port 是 2.2.2.2:7777。\n然而，STUN 并不是一个完整的解决方案，它只是提供了这么一种机制，让应用程序获取到它的公网 IP:Port，但 STUN 并没有提供具体的方法来向相关方向发出信号。如果要重头编写一个具有 NAT 穿透功能的应用，肯定要利用 STUN 来实现。当然，明智的做法是不修改 WireGuard 的源码，最好是借鉴 STUN 的概念来实现。总之，不管如何，都需要一个拥有静态公网地址的主机来充当信使服务器。\n5. NAT 穿透示例 # 早在 2016 年 8 月份，WireGuard 的创建者就在 WireGuard 邮件列表上分享了一个 NAT 穿透示例。Jason 的示例包含了客户端应用和服务端应用，其中客户端应用于 WireGuard 一起运行，服务端运行在拥有静态地址的主机上用来发现各个 Peer 的 IP:Port，客户端使用 原始套接字（raw socket）与服务端进行通信。\n/* We use raw sockets so that the WireGuard interface can actually own the real socket. */ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); if (sock \u0026lt; 0) { perror(\u0026#34;socket\u0026#34;); return errno; } 正如评论中指出的，WireGuard 拥有“真正的套接字”。通过使用原始套接字（raw socket），客户端能够向服务端伪装本地 WireGuard 的源端口，这样就确保了在服务端返回响应经过 NAT 时目标 IP:Port 会被映射到 WireGuard 套接字上。\n客户端在其原始套接字上使用一个 经典的 BPF 过滤器来过滤服务端发往 WireGuard 端口的回复。\nstatic void apply_bpf(int sock, uint16_t port, uint32_t ip) { struct sock_filter filter[] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 12 /* src ip */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ip, 0, 5), BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20 /* src port */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, PORT, 0, 3), BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 22 /* dst port */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, port, 0, 1), BPF_STMT(BPF_RET + BPF_K, -1), BPF_STMT(BPF_RET + BPF_K, 0) }; struct sock_fprog filter_prog = { .len = sizeof(filter) / sizeof(filter[0]), .filter = filter }; if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, \u0026amp;filter_prog, sizeof(filter_prog)) \u0026lt; 0) { perror(\u0026#34;setsockopt(bpf)\u0026#34;); exit(errno); } } 客户端与服务端的通信数据都被定义在 packet 和 reply 这两个结构体中：\nstruct { struct udphdr udp; uint8_t my_pubkey[32]; uint8_t their_pubkey[32]; } __attribute__((packed)) packet = { .udp = { .len = htons(sizeof(packet)), .dest = htons(PORT) } }; struct { struct iphdr iphdr; struct udphdr udp; uint32_t ip; uint16_t port; } __attribute__((packed)) reply; 客户端会遍历配置好的 WireGuard Peer（wg show \u0026lt;interface\u0026gt; peers），并为每一个 Peer 发送一个数据包给服务端，其中 my_pubkey 和 their_pubkey 字段会被适当填充。当服务端收到来自客户端的数据包时，它会向以公钥为密钥的 Peer 内存表中插入或更新一个 pubkey=my_pubkey 的 entry，然后再从该表中查找 pubkey=their_pubkey 的 entry，一但发现 entry 存在，就会将其中的 IP:Port 发送给客户端。当客户端收到回复时，会将 IP 和端口从数据包中解包，并配置 Peer 的 endpoint 地址（wg set \u0026lt;interface\u0026gt; peer \u0026lt;key\u0026gt; \u0026lt;options...\u0026gt; endpoint \u0026lt;ip\u0026gt;:\u0026lt;port\u0026gt;）。\nentry 结构体源码：\nstruct entry { uint8_t pubkey[32]; uint32_t ip; uint16_t port; }; entry 结构体中的 ip 和 port 字段是从客户端收到的数据包中提取的 IP 和 UDP 头部，每次客户端请求 Peer 的 IP 和端口信息时，都会在 Peer 列表中刷新自己的 IP 和端口信息。\n上面的例子展示了 WireGuard 如何实现 UDP 打洞，但还是太复杂了，因为并不是所有的 Peer 端都能打开原始套接字（raw socket），也并不是所有的 Peer 端都能利用 BPF 过滤器。而且这里还用到了自定义的 wire protocol，代码层面的数据（链表、队列、二叉树）都是结构化的，但网络层看到的都是二进制流，所谓 wire protocol 就是把结构化的数据序列化为二进制流发送出去，并且对方也能以同样的格式反序列化出来。这种方式是很难调试的，所以我们需要另辟蹊径，利用现有的成熟工具来达到目的。\n6. WireGuard NAT 穿透的正解 # 其实完全没必要这么麻烦，我们可以直接利用 WireGuard 本身的特性来实现 UDP 打洞，直接看图：\n你可能会认为这是个中心辐射型（hub-and-spoke）网络拓扑，但实际上还是有些区别的，这里的 Registry Peer 不会充当网关的角色，因为它没有相应的路由，不会转发流量。Registry 的 WireGuard 接口地址为 10.0.0.254/32，Alice 和 Bob 的 AllowedIPs 中只包含了 10.0.0.254/32，表示只接收来自 Registry 的流量，所以 Alice 和 Bob 之间无法通过 Registry 来进行通信。\n这里有一点至关重要，Registry 分别和 Alice 与 Bob 建立了两个隧道，这就会在 Alice 和 Bob 的 NAT 上打开一个洞，我们需要找到一种方法来从 Registry Peer 中查询这些洞的 IP:Port，自然而然就想到了 DNS 协议。DNS 的优势很明显，它比较简单、成熟，还跨平台。有一种 DNS 记录类型叫 SRV记录（Service Record，服务定位记录），它用来记录服务器提供的服务，即识别服务的 IP 和端口， RFC6763 用具体的结构和查询模式对这种记录类型进行了扩展，用于发现给定域下的服务，我们可以直接利用这些扩展语义。\n7. CoreDNS # 选好了服务发现协议后，还需要一种方法来将其与 WireGuard 对接。 CoreDNS 是 Golang 编写的一个插件式 DNS 服务器，是目前 Kubernetes 内置的默认 DNS 服务器，并且已从 CNCF 毕业。我们可以直接写一个 CoreDNS 插件，用来接受 DNS-SD（DNS-based Service Discovery）查询并返回相关 WireGuard Peer 的信息，其中公钥作为记录名称，icloudnative.io 作为域。如果你熟悉 bind 风格的域文件，可以想象一个类似这样的域数据：\n_wireguard._udp IN PTR alice._wireguard._udp.icloudnative.io. _wireguard._udp IN PTR bob._wireguard._udp.icloudnative.io. alice._wireguard._udp IN SRV 0 1 7777 alice.icloudnative.io. alice IN A 2.2.2.2 bob._wireguard._udp IN SRV 0 1 8888 bob.icloudnative.io. bob IN A 3.3.3.3 公钥使用 Base64 还是 Base32 ？ # 到目前为止，我们一直使用别名 Alice 和 Bob 来替代其对应的 WireGuard 公钥。WireGuard 公钥是 Base64 编码的，长度为 44 字节：\n$ wg genkey | wg pubkey UlVJVmPSwuG4U9BwyVILFDNlM+Gk9nQ7444HimPPgQg= Base 64 编码的设计是为了以一种允许使用大写字母和小写字母的形式来表示任意的八位字节序列。\n— RFC4648\n不幸的是，DNS 的 SRV 记录的服务名称是不区分大小写的：\nDNS 树中的每个节点都有一个由零个或多个标签组成的名称 [STD13, RFC1591, RFC2606]，这些标签不区分大小写。\n— RFC4343\nBase32 虽然产生了一个稍长的字符串（56 字节），但它的表现形式允许我们在 DNS 内部表示 WireGuard 公钥：\nBase32 编码的目的是为了表示任意八位字节序列，其形式必须不区分大小写。\n我们可以使用 base64 和 base32 命令来回转换编码格式，例如：\n$ wg genkey | wg pubkey \u0026gt; pub.txt $ cat pub.txt O9rAAiO5qTejOEtFbsQhCl745ovoM9coTGiprFTaHUE= $ cat pub.txt | base64 -D | base32 HPNMAARDXGUTPIZYJNCW5RBBBJPPRZUL5AZ5OKCMNCU2YVG2DVAQ==== $ cat pub.txt | base64 -D | base32 | base32 -d | base64 O9rAAiO5qTejOEtFbsQhCl745ovoM9coTGiprFTaHUE= 我们可以直接使用 base32 这种不区分大小写的公钥编码，来使其与 DNS 兼容。\n编译插件 # CoreDNS 提供了 编写插件的文档，插件必须要实现 plugin.Handler 接口：\ntype Handler interface { ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) Name() string } 我自己已经写好了插件，通过 DNS-SD（DNS-based Service Discovery）语义来提供 WireGuard 的 Peer 信息，该插件名就叫 wgsd。自己编写的插件不属于官方内置插件，从 CoreDNS 官方下载页下载的可执行程序并不包括这两个插件，所以需要自己编译 CoreDNS。\n编译 CoreDNS 并不复杂，在没有外部插件的情况下可以这么编译：\n$ git clone https://github.com/coredns/coredns.git $ cd coredns $ make 如果要加上 wgsd 插件，则在 make 前，要修改 plugin.cfg 文件，加入以下一行：\nwgsd:github.com/jwhited/wgsd 然后开始编译：\n$ go generate $ go build 查看编译好的二进制文件是否包含该插件：\n$ ./coredns -plugins | grep wgsd dns.wgsd 编译完成后，就可以在配置文件中启用 wgsd 插件了：\n.:53 { wgsd \u0026lt;zone\u0026gt; \u0026lt;wg device\u0026gt; } 可以来测试一下，配置文件如下：\n$ cat Corefile .:53 { debug wgsd icloudnative.io. wg0 } 运行 CoreDNS：\n$ ./coredns -conf Corefile .:53 CoreDNS-1.8.1 linux/amd64, go1.15, 当前节点的 WireGuard 信息：\n$ sudo wg show interface: wg0 listening port: 52022 peer: mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8= endpoint: 3.3.3.3:8888 allowed ips: 10.0.0.2/32 下面就是见证奇迹的时候，列出所有 Peer：\n$ dig @127.0.0.1 _wireguard._udp.icloudnative.io. PTR +noall +answer +additional ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.10.6 \u0026lt;\u0026lt;\u0026gt;\u0026gt; @127.0.0.1 _wireguard._udp.icloudnative.io. PTR +noall +answer +additional ; (1 server found) ;; global options: +cmd _wireguard._udp.icloudnative.io. 0 IN PTR TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====._wireguard._udp.icloudnative.io. 查询每个 Peer 的 IP 和端口：\n$ dig @127.0.0.1 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====._wireguard._udp.icloudnative.io. SRV +noall +answer +additional ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.10.6 \u0026lt;\u0026lt;\u0026gt;\u0026gt; @127.0.0.1 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====._wireguard._udp.icloudnative.io. SRV +noall +answer +additional ; (1 server found) ;; global options: +cmd tl5glqumg5vatrrtyg57hydce55wnfhx7wadwwzhmno4njly4a7q====._wireguard._udp.icloudnative.io. 0 IN SRV 0 0 8888 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====.icloudnative.io. TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====.icloudnative.io. 0 IN A 3.3.3.3 🎉 🎉 🎉 完美！🎉 🎉 🎉\n验证公钥是否匹配：\n$ wg show wg0 peers mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8= $ dig @127.0.0.1 _wireguard._udp.icloudnative.io. PTR +short | cut -d. -f1 | base32 -d | base64 mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8= 👍 👍 👍\n8. 最终通信流程 # 最终实现的通信流程如下：\n一开始，Alice 和 Bob 分别与 Registry 建立了隧道；接下来，Alice 上的 wgsd-client 向 Registry 节点上运行的 CoreDNS插件（wgsd）发起查询请求，该插件从 WireGuard 信息中检索 Bob 的 endpoint 信息，并将其返回给 wgsd-client；然后 wgsd-client 开始设置 Bob 的 endpoint；最后 Alice 和 Bob 之间直接建立了一条隧道。\n任何提及 \u0026ldquo;建立隧道 \u0026ldquo;的地方都只是意味着发生了握手，数据包可以在 Peer 之间传输。虽然 WireGuard 确实有一个握手机制，但它比你想象的更像是一个无连接的协议。\n任何安全协议都需要保持一些状态，所以最初的握手是非常简单的，只是建立用于数据传输的对称密钥。这种握手每隔几分钟就会发生一次，以提供轮换密钥来实现完美的前向保密。它是根据时间来完成的，而不是根据之前数据包的内容来完成的，因为它的设计是为了优雅地处理数据包丢失的问题。\n— wireguard.com/protocol\n现在万事俱备，只欠东风，只需要实现 wgsd-client 就完事了。\n9. 实现 wgsd-client # wgsd-client 负责使 Peer 的 endpoint 配置保持最新状态，它会检索配置中的 Peer 列表，查询 CoreDNS 中与之匹配的公钥，然后在需要时为相应的 Peer 更新 endpoint 的值。最初的实现方式是以定时任务或者类似的调度机制运行，以序列化的方式检查所有 Peer，设置 endpoint，然后退出。目前它还不是一个守护进程，后续会继续改进优化。\nwgsd-client 的源码位于 wgsd 仓库中的 cmd/wgsd-client 目录。\n下面开始进行最终的测试。\nAlice 和 Bob 都在 NAT 后面，Registry 没有 NAT，且有固定的公网地址。这三个 Peer 的信息如下：\nPeer Public Key Tunnel Address Alice xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= 10.0.0.1 Bob syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= 10.0.0.2 Registry JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= 10.0.0.254 它们各自的初始配置：\nAlice # $ cat /etc/wireguard/wg0.conf [Interface] Address = 10.0.0.1/32 PrivateKey = 0CtieMOYKa2RduPbJss/Um9BiQPSjgvHW+B7Mor5OnE= ListenPort = 51820 # Registry [Peer] PublicKey = JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= Endpoint = 4.4.4.4:51820 PersistentKeepalive = 5 AllowedIPs = 10.0.0.254/32 # Bob [Peer] PublicKey = syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= PersistentKeepalive = 5 AllowedIPs = 10.0.0.2/32 $ wg show interface: wg0 public key: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= private key: (hidden) listening port: 51820 peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 48 seconds ago transfer: 1.67 KiB received, 11.99 KiB sent persistent keepalive: every 5 seconds peer: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= allowed ips: 10.0.0.2/32 persistent keepalive: every 5 seconds Bob # $ cat /etc/wireguard/wg0.conf [Interface] Address = 10.0.0.2/32 PrivateKey = cIN5NqeWcbreXoaIhR/4wgrrQJGym/E7WrTttMtK8Gc= ListenPort = 51820 # Registry [Peer] PublicKey = JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= Endpoint = 4.4.4.4:51820 PersistentKeepalive = 5 AllowedIPs = 10.0.0.254/32 # Alice [Peer] PublicKey = xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= PersistentKeepalive = 5 AllowedIPs = 10.0.0.1/32 $ wg show interface: wg0 public key: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= private key: (hidden) listening port: 51820 peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 26 seconds ago transfer: 1.54 KiB received, 11.75 KiB sent persistent keepalive: every 5 seconds peer: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= allowed ips: 10.0.0.1/32 persistent keepalive: every 5 seconds Registry # $ cat /etc/wireguard/wg0.conf [Interface] Address = 10.0.0.254/32 PrivateKey = wLw2ja5AapryT+3SsBiyYVNVDYABJiWfPxLzyuiy5nE= ListenPort = 51820 # Alice [Peer] PublicKey = xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= AllowedIPs = 10.0.0.1/32 # Bob [Peer] PublicKey = syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= AllowedIPs = 10.0.0.2/32 $ wg show interface: wg0 public key: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= private key: (hidden) listening port: 51820 peer: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= endpoint: 2.2.2.2:41424 allowed ips: 10.0.0.1/32 latest handshake: 6 seconds ago transfer: 510.29 KiB received, 52.11 KiB sent peer: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= endpoint: 3.3.3.3:51820 allowed ips: 10.0.0.2/32 latest handshake: 1 minute, 46 seconds ago transfer: 498.04 KiB received, 50.59 KiB sent Registry 与 Alice 和 Bob 都建立了连接，可以直接查询它们的 endpoint 信息：\n$ dig @4.4.4.4 -p 53 _wireguard._udp.icloudnative.io. PTR +noall +answer +additional ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.10.6 \u0026lt;\u0026lt;\u0026gt;\u0026gt; @4.4.4.4 -p 53 _wireguard._udp.icloudnative.io. PTR +noall +answer +additional ; (1 server found) ;; global options: +cmd _wireguard._udp.icloudnative.io. 0 IN PTR YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====._wireguard._udp.icloudnative.io. _wireguard._udp.icloudnative.io. 0 IN PTR WMRID55V4ENHXQX2JSTYOYVKICJ5PIHKB2TR7R42SMIU3T5L4I5Q====._wireguard._udp.icloudnative.io. $ dig @4.4.4.4 -p 53 YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====._wireguard._udp.icloudnative.io. SRV +noall +answer +additional ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.10.6 \u0026lt;\u0026lt;\u0026gt;\u0026gt; @4.4.4.4 -p 53 YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====._wireguard._udp.icloudnative.io. SRV +noall +answer +additional ; (1 server found) ;; global options: +cmd yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.icloudnative.io. 0 IN SRV 0 0 41424 YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====.icloudnative.io. YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====.icloudnative.io. 0 IN A 2.2.2.2 完美，下面分别在 Alice 和 Bob 上启动 wgsd-client 试试：\n# Alice $ ./wgsd-client -device=wg0 -dns=4.4.4.4:53 -zone=icloudnative.io. 2020/05/20 13:24:02 [JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=] no SRV records found jwhited@Alice:~$ ping 10.0.0.2 PING 10.0.0.2 (10.0.0.2): 56 data bytes 64 bytes from 10.0.0.2: icmp_seq=0 ttl=64 time=173.260 ms ^C jwhited@Alice:~$ wg show interface: wg0 public key: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= private key: (hidden) listening port: 51820 peer: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= endpoint: 3.3.3.3:51820 allowed ips: 10.0.0.2/32 latest handshake: 2 seconds ago transfer: 252 B received, 264 B sent persistent keepalive: every 5 seconds peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 1 minute, 19 seconds ago transfer: 184 B received, 1.57 KiB sent persistent keepalive: every 5 seconds # Bob $ ./wgsd-client -device=wg0 -dns=4.4.4.4:53 -zone=icloudnative.io. 2020/05/20 13:24:04 [JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=] no SRV records found jwhited@Bob:~$ wg show interface: wg0 public key: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= private key: (hidden) listening port: 51820 peer: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= endpoint: 2.2.2.2:41424 allowed ips: 10.0.0.1/32 latest handshake: 22 seconds ago transfer: 392 B received, 9.73 KiB sent persistent keepalive: every 5 seconds peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 1 minute, 14 seconds ago transfer: 2.08 KiB received, 17.59 KiB sent persistent keepalive: every 5 seconds wgsd-client 成功发现了 Peer 的 endpoint 地址并更新了 WireGuard 的配置，最终 Alice 和 Bob 之间直接建立了一条隧道！\n总结 # 本文探讨了如何在受 NAT 限制的两个 Peer 之间直接建立一条 WireGuard 隧道。本文提供的解决方案都是使用现有的协议和服务发现技术，以及自己写了个可插拔的插件，你可以直接使用 dig 或 nslookup 来进行调试，不需要干扰或修改 WireGuard 本身。\n当然，这个 CoreDNS 插件肯定还可以优化，wgsd-client 也需要继续优化。比如，CoreDNS 服务器是否应该限制只在 Registry 的隧道中可用？是否应该对域进行签名？每次查询 DNS 时是否都需要查询一次 WireGuard 的 Peer 信息，还是说可以用缓存来解决？这些都是值得思考的问题。\nwgsd 插件的代码是开源的，欢迎大家踊跃贡献。\n","date":"2021年1月28日","externalUrl":null,"permalink":"/posts/wireguard-endpoint-discovery-nat-traversal/","section":"博客","summary":"WireGuard 是由 Jason A. Donenfeld 等人创建的下一代开源 VPN 协议，旨在解决许多困扰 IP","title":"WireGuard 教程：使用 DNS-SD 进行 NAT-to-NAT 穿透","type":"posts"},{"content":"之前花了很大的篇幅介绍了 WireGuard 的 工作原理和 配置详解，可这里面的内容实在太多了，大部分人根本没兴趣深究，只是将其当成参考书来看。WireGuard 虽然组网逻辑很简洁明了，但秘钥和配置文件的管理是个麻烦事，需要手工配置。为了让大部分读者能够快速上手 WireGuard，体验 WireGuard 的优雅和强大，我决定新开一个 WireGuard 快速上手系列，第一篇之前已经发出来了：\nWireGuard 快速安装教程 这篇文章仅仅介绍了如何快速安装 WireGuard，并没有涉及到如何配置使其正常工作。本文主要介绍如何方便优雅地管理 WireGuard 的配置和秘钥。当然了，这里不会详细解读各个配置参数的含义，也不会告诉你通过哪个命令来创建公钥私钥，如果你对此部分感兴趣，可以查看我之前发布的 WireGuard 配置详解。\n1. wg-gen-web 配置 # 对于新手来说，如何才能快速把 WireGuard 用起来呢？当然是通过图形管理界面啦，填几个参数，生成个二维码，再拿客户端扫下二维码就连上了，简直是比爽姐还爽~\nwg-gen-web 就是这样一款图形管理界面，主要包含以下这些功能：\n根据 CIDR 自动分配 IP 地址给客户端； 每个客户端会生成 QR 二维码，方便移动客户端扫描使用； 支持通过邮件发送二维码和配置文件； 支持启用和禁用某个客户端； 支持 IPv6； 支持使用 GitHub 和 Oauth2 OIDC 来进行用户认证； 颜值还比较高。 wg-gen-web 支持直接通过容器来运行，如果你是在本地运行，可以准备一份 docker-compose 文件：\ndocker-compose.yaml\nversion: \u0026#39;3.6\u0026#39; services: wg-gen-web: image: vx3r/wg-gen-web:latest container_name: wg-gen-web restart: always expose: - \u0026#34;8080/tcp\u0026#34; ports: - 80:8080 environment: - WG_CONF_DIR=/data - WG_INTERFACE_NAME=wg0.conf - OAUTH2_PROVIDER_NAME=fake - WG_STATS_API=http://\u0026lt;API_LISTEN_IP\u0026gt;:8182 volumes: - /etc/wireguard:/data network_mode: bridge wg-json-api: image: james/wg-api:latest container_name: wg-json-api restart: always cap_add: - NET_ADMIN network_mode: \u0026#34;host\u0026#34; command: wg-api --device wg0 --listen \u0026lt;API_LISTEN_IP\u0026gt;:8182 这里还用到了另外一个项目 wg-api，该项目提供了一个 JSON-RPC 接口，用来暴露 WireGuard 的网络状态信息。其中 \u0026lt;API_LISTEN_IP\u0026gt; 可以直接替换成 docker0 的 IP。\n执行以下命令运行 wg-gen-web：\n🐳 → docker-compose up -d 在浏览器中输入 URL \u0026lt;hostIP\u0026gt; 打开图形管理界面，点击 “SERVER” 开始填写服务端和客户端的配置信息：\n各项配置的含义我就不解释了，都很好理解，实在不理解的请查看 WireGuard 配置详解。\n填写好配置信息后，直接点击 UPDATE SERVER CONFIGURATION 保存，同时会生成配置文件 wg0.conf：\n🐳 → cat /etc/wireguard/wg0.conf # Updated: 2021-01-20 03:59:37.718655459 +0000 UTC / Created: 2021-01-20 03:32:28.045982181 +0000 UTC [Interface] Address = 10.6.6.1/24 ListenPort = 51820 PrivateKey = iLPeSYaKYERfyrOX/YcAam4AIIHCNEBXnqL2oRedAWQ= PreUp = echo WireGuard PreUp PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PreDown = echo WireGuard PreDown PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE 接下来点击 CLIENTS，然后点击 ADD NEW CLIENT 开始新增客户端配置：\n填写客户端配置信息：\n点击 SUBMIT，就会在 /etc/wireguard 目录下生成客户端的 json 配置文件：\n🐳 → cat /etc/wireguard/f5fcc1e7-e03a-48bb-acd9-8d5214c6cb1f { \u0026#34;id\u0026#34;: \u0026#34;f5fcc1e7-e03a-48bb-acd9-8d5214c6cb1f\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;test\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;yangchuansheng33@gmail.com\u0026#34;, \u0026#34;enable\u0026#34;: true, \u0026#34;ignorePersistentKeepalive\u0026#34;: false, \u0026#34;presharedKey\u0026#34;: \u0026#34;8QkkeXGt4D/lnLDA1jfJUhB3oiShhRWp/GC8GFQtgKs=\u0026#34;, \u0026#34;allowedIPs\u0026#34;: [ \u0026#34;10.6.6.0/24\u0026#34; ], \u0026#34;address\u0026#34;: [ \u0026#34;10.6.6.2/32\u0026#34; ], \u0026#34;tags\u0026#34;: [], \u0026#34;privateKey\u0026#34;: \u0026#34;ODN2xN12p5lwcEuj20C4uZV9kJE9yHz4eAHB/4czPEM=\u0026#34;, \u0026#34;publicKey\u0026#34;: \u0026#34;k2Ut15aQn7+mNHqEd4bwdNx3WcvA4F7SPmETYuWdSjM=\u0026#34;, \u0026#34;createdBy\u0026#34;: \u0026#34;Unknown\u0026#34;, \u0026#34;updatedBy\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;created\u0026#34;: \u0026#34;2021-01-20T05:19:16.659225991Z\u0026#34;, \u0026#34;updated\u0026#34;: \u0026#34;2021-01-20T05:19:16.659225991Z\u0026#34; } 如果勾选了 “Enable client after creation”，还会将 peer 的配置加入 wg0.conf：\n🐳 → cat /etc/wireguard/wg0.conf # Updated: 2021-01-20 03:59:37.718655459 +0000 UTC / Created: 2021-01-20 03:32:28.045982181 +0000 UTC [Interface] Address = 10.6.6.1/24 ListenPort = 51820 PrivateKey = iLPeSYaKYERfyrOX/YcAam4AIIHCNEBXnqL2oRedAWQ= PreUp = echo WireGuard PreUp PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PreDown = echo WireGuard PreDown PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE # test / yangchuansheng33@gmail.com / Updated: 2021-01-20 05:19:16.659225991 +0000 UTC / Created: 2021-01-20 05:19:16.659225991 +0000 UTC [Peer] PublicKey = k2Ut15aQn7+mNHqEd4bwdNx3WcvA4F7SPmETYuWdSjM= PresharedKey = 8QkkeXGt4D/lnLDA1jfJUhB3oiShhRWp/GC8GFQtgKs= AllowedIPs = 10.6.6.2/32 最后直接启动 wg-quick 服务就行了：\n🐳 → systemctl start wg-quick@wg0 如果你之前已经启动过该服务，现在只需要重启就行了：\n🐳 → systemctl restart wg-quick@wg0 重启之后 WireGuard 会断开重连，体验不太好。事实上 WireGuard 可以做到在不中断活跃连接的情况下重新加载配置文件，命令如下：\n🐳 → wg syncconf wg0 \u0026lt;(wg-quick strip wg0) 我们可以将这个命令作为 systemd 服务的 reload 命令：\n# /usr/lib/systemd/system/wg-quick@.service [Unit] Description=WireGuard via wg-quick(8) for %I After=network-online.target nss-lookup.target Wants=network-online.target nss-lookup.target PartOf=wg-quick.target Documentation=man:wg-quick(8) Documentation=man:wg(8) Documentation=https://www.wireguard.com/ Documentation=https://www.wireguard.com/quickstart/ Documentation=https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8 Documentation=https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8 [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/bin/wg-quick up %i ExecStop=/usr/bin/wg-quick down %i ExecReload=/bin/bash -c \u0026#39;exec /usr/bin/wg syncconf %i \u0026lt;(exec /usr/bin/wg-quick strip %i)\u0026#39; Environment=WG_ENDPOINT_RESOLUTION_RETRIES=infinity [Install] WantedBy=multi-user.target 如果你按照 WireGuard 快速安装教程 这篇文章的步骤来安装 WireGuard，ExecReload 默认已经被加进去了，到这一步不需要做任何改动。后面再更新配置文件时，直接 reload 就行了：\n🐳 → systemctl reload wg-quick@wg0 每次更新配置后都要手动 reload 还是很麻烦的，我们可以通过 systemd 来监听配置文件的实时变化，一但配置文件有所改动，就立即触发 reload。方法也很简单，先创建一个 wg-gen-web.service 用来 reload：\n# /etc/systemd/system/wg-gen-web.service [Unit] Description=Restart WireGuard After=network.target [Service] Type=oneshot ExecStart=/usr/bin/systemctl reload wg-quick@wg0.service [Install] WantedBy=multi-user.target 然后再创建一个同名的 wg-gen-web.path 用来监听文件变化：\n# /etc/systemd/system/wg-gen-web.path [Unit] Description=Watch /etc/wireguard for changes [Path] PathModified=/etc/wireguard [Install] WantedBy=multi-user.target 设置开机自启：\n🐳 → systemctl enable wg-gen-web.service wg-gen-web.path --now 后面如果再到 Web 页面上更新配置信息，会立即触发 reload，不需要再自己手动 reload 了。\n查看接口信息：\n🐳 → wg show wg0 interface: wg0 public key: dG5xPA7Q6X7ByeNl5pasI/8ZPhiTOsfsy0NUX4w2wmI= private key: (hidden) listening port: 51820 peer: k2Ut15aQn7+mNHqEd4bwdNx3WcvA4F7SPmETYuWdSjM= preshared key: (hidden) allowed ips: 10.6.6.2/32 目前还没有客户端与之连接，所以还看不到连接信息。下面以 macOS 为例演示连接过程。\n2. 客户端建立连接 # macOS 目前只有两种客户端软件，一个是图形界面，一个是命令行工具。图形界面只上架了 App Store，而且需要美区 Apple ID，比较麻烦。\n我推荐直接安装命令行工具：\n🐳 → brew install wireguard-tools macOS 中的 wg-quick 默认也是读取的 /etc/wireguard 目录，所以需要先创建该目录：\n🐳 → sudo mkdir /etc/wireguard 然后直接下载配置文件：\n将其移动到 /etc/wireguard 目录，并重命名为 wg0.conf：\n🐳 → sudo mv ~/Downloads/test.conf /etc/wireguard/wg0.conf 查看配置文件内容：\n🐳 → cat /etc/wireguard/wg0.conf [Interface] Address = 10.6.6.2/32 PrivateKey = ODN2xN12p5lwcEuj20C4uZV9kJE9yHz4eAHB/4czPEM= [Peer] PublicKey = dG5xPA7Q6X7ByeNl5pasI/8ZPhiTOsfsy0NUX4w2wmI= PresharedKey = 8QkkeXGt4D/lnLDA1jfJUhB3oiShhRWp/GC8GFQtgKs= AllowedIPs = 10.6.6.0/24 Endpoint = 172.16.7.3:51820 PersistentKeepalive = 25 直接启动：\n🐳 → sudo wg-quick up wg0 查看连接信息：\n🐳 → sudo wg interface: utun2 public key: k2Ut15aQn7+mNHqEd4bwdNx3WcvA4F7SPmETYuWdSjM= private key: (hidden) listening port: 60082 peer: dG5xPA7Q6X7ByeNl5pasI/8ZPhiTOsfsy0NUX4w2wmI= preshared key: (hidden) endpoint: 172.16.7.3:51820 allowed ips: 10.6.6.0/24 latest handshake: 7 seconds ago transfer: 840 B received, 840 B sent persistent keepalive: every 25 seconds 可以看到输出中有两行重要的信息：\ntransfer: 840 B received, 840 B sent persistent keepalive: every 25 seconds 表示和服务端已经握手成功了，并且开始传输数据。\n到服务端所在的机器查看连接信息：\n🐳 → wg show wg0 interface: wg0 public key: dG5xPA7Q6X7ByeNl5pasI/8ZPhiTOsfsy0NUX4w2wmI= private key: (hidden) listening port: 51820 peer: k2Ut15aQn7+mNHqEd4bwdNx3WcvA4F7SPmETYuWdSjM= preshared key: (hidden) endpoint: 10.2.0.2:60082 allowed ips: 10.6.6.2/32 latest handshake: 25 seconds ago transfer: 1.64 KiB received, 1.61 KiB sent 可以看到握手成功了。\nWeb 页面也能看到连接信息：\n如果想增加更多的客户端，直接在 Web 页面新增客户端配置就行了，不需要做任何额外的操作，解放了双手。手机客户端直接扫描二维码就能连接，还是挺爽的。\n3. 优化 # 最后一部分主要介绍 WireGuard 的优化。\n动态 IP # 对于 WireGuard 而言，只需要一端具有公网 IP 地址便可建立连接，哪怕这一端的 IP 是动态变化的也没问题，可以使用 DDNS 来解决这个问题，WireGuard 会在启动时解析域名的 IP 地址，然后将该 IP 地址作为 peer 的 Endpoint。但这里有一个小瑕疵，WireGuard 只会在启动时解析配置文件中域名的 IP 地址，后续如果域名对应的 IP 地址有更新，也不会重新解析。\nwireguard-tools 项目中提供了一个脚本 reresolve-dns.sh 可以用来解决这个问题，该脚本会解析 WireGuard 的配置文件并更新 Endpoint 的 IP 地址。同时，我们还需要创建一个定时任务来定期触发该脚本更新配置，比如每 30 秒执行一次。具体操作步骤如下：\n首先克隆 wireguard-tools 仓库：\n🐳 → git clone https://git.zx2c4.com/wireguard-tools /usr/share/wireguard-tools 脚本内容：\n🐳 → cat /usr/share/wireguard-tools/contrib/reresolve-dns/reresolve-dns.sh #!/bin/bash # SPDX-License-Identifier: GPL-2.0 # # Copyright (C) 2015-2020 Jason A. Donenfeld \u0026lt;Jason@zx2c4.com\u0026gt;. All Rights Reserved. set -e shopt -s nocasematch shopt -s extglob export LC_ALL=C CONFIG_FILE=\u0026#34;$1\u0026#34; [[ $CONFIG_FILE =~ ^[a-zA-Z0-9_=+.-]{1,15}$ ]] \u0026amp;\u0026amp; CONFIG_FILE=\u0026#34;/etc/wireguard/$CONFIG_FILE.conf\u0026#34; [[ $CONFIG_FILE =~ /?([a-zA-Z0-9_=+.-]{1,15})\\.conf$ ]] INTERFACE=\u0026#34;${BASH_REMATCH[1]}\u0026#34; process_peer() { [[ $PEER_SECTION -ne 1 || -z $PUBLIC_KEY || -z $ENDPOINT ]] \u0026amp;\u0026amp; return 0 [[ $(wg show \u0026#34;$INTERFACE\u0026#34; latest-handshakes) =~ ${PUBLIC_KEY//+/\\\\+}\\\t([0-9]+) ]] || return 0 (( ($(date +%s) - ${BASH_REMATCH[1]}) \u0026gt; 135 )) || return 0 wg set \u0026#34;$INTERFACE\u0026#34; peer \u0026#34;$PUBLIC_KEY\u0026#34; endpoint \u0026#34;$ENDPOINT\u0026#34; reset_peer_section } reset_peer_section() { PEER_SECTION=0 PUBLIC_KEY=\u0026#34;\u0026#34; ENDPOINT=\u0026#34;\u0026#34; } reset_peer_section while read -r line || [[ -n $line ]]; do stripped=\u0026#34;${line%%\\#*}\u0026#34; key=\u0026#34;${stripped%%=*}\u0026#34;; key=\u0026#34;${key##*([[:space:]])}\u0026#34;; key=\u0026#34;${key%%*([[:space:]])}\u0026#34; value=\u0026#34;${stripped#*=}\u0026#34;; value=\u0026#34;${value##*([[:space:]])}\u0026#34;; value=\u0026#34;${value%%*([[:space:]])}\u0026#34; [[ $key == \u0026#34;[\u0026#34;* ]] \u0026amp;\u0026amp; { process_peer; reset_peer_section; } [[ $key == \u0026#34;[Peer]\u0026#34; ]] \u0026amp;\u0026amp; PEER_SECTION=1 if [[ $PEER_SECTION -eq 1 ]]; then case \u0026#34;$key\u0026#34; in PublicKey) PUBLIC_KEY=\u0026#34;$value\u0026#34;; continue ;; Endpoint) ENDPOINT=\u0026#34;$value\u0026#34;; continue ;; esac fi done \u0026lt; \u0026#34;$CONFIG_FILE\u0026#34; process_peer 然后创建一个 Service 文件：\n# /etc/systemd/system/wireguard_reresolve-dns.service [Unit] Description=Reresolve DNS of all WireGuard endpoints Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/bin/sh -c \u0026#39;for i in /etc/wireguard/*.conf; do /usr/share/wireguard-tools/contrib/reresolve-dns/reresolve-dns.sh \u0026#34;$i\u0026#34;; done\u0026#39; 再创建一个同名的 wireguard_reresolve-dns.timer 实现定时任务：\n# /etc/systemd/system/wireguard_reresolve-dns.timer [Unit] Description=Periodically reresolve DNS of all WireGuard endpoints [Timer] OnCalendar=*:*:0/30 [Install] WantedBy=timers.target 设置开机自启动：\n🐳 → systemctl enable wireguard_reresolve-dns.service wireguard_reresolve-dns.timer --now 打印 debug 日志 # 在支持动态调试的内核上使用 Linux 内核模块时，可以将 WireGuard 的调试信息写入内核环形缓冲区中：\n🐳 → modprobe wireguard 🐳 → echo module wireguard +p \u0026gt; /sys/kernel/debug/dynamic_debug/control 然后就可以使用 journalctl 或者 dmesg 来查看调试信息了：\n🐳 → journalctl -xf|grep wireguard Jan 20 15:07:00 k8s03 kernel: wireguard: wg0: Receiving keepalive packet from peer 25 (125.122.107.150:52647) Jan 20 15:07:00 k8s03 kernel: wireguard: wg0: Receiving handshake initiation from peer 25 (125.122.107.150:52647) Jan 20 15:07:00 k8s03 kernel: wireguard: wg0: Sending handshake response to peer 25 (125.122.107.150:52647) Jan 20 15:07:00 k8s03 kernel: wireguard: wg0: Keypair 83096 destroyed for peer 25 Jan 20 15:07:00 k8s03 kernel: wireguard: wg0: Keypair 83112 created for peer 25 🐳 → dmesg|tail -20|grep wireguard [4222650.389928] wireguard: wg0: Receiving keepalive packet from peer 23 (125.122.107.150:50904) [4222652.081319] wireguard: wg0: Receiving keepalive packet from peer 22 (125.122.107.150:58715) [4222654.802308] wireguard: wg0: Receiving keepalive packet from peer 25 (125.122.107.150:53533) [4222675.389578] wireguard: wg0: Receiving keepalive packet from peer 23 (125.122.107.150:50904) 接下来的文章将会介绍 WireGuard 的全互联模式，以及使用 WireGuard 作为 Kubernetes 的 CNI 插件，大家搓搓小手等着吧。\n","date":"2021年1月19日","externalUrl":null,"permalink":"/posts/configure-wireguard-using-wg-gen-web/","section":"博客","summary":"之前花了很大的篇幅介绍了 WireGuard 的 工作原理和 配置详解，可这里面的内","title":"WireGuard 配置教程：使用 wg-gen-web 来管理 WireGuard 的配置","type":"posts"},{"content":"","date":"2021年1月14日","externalUrl":null,"permalink":"/categories/macos/","section":"分类","summary":"","title":"macOS","type":"categories"},{"content":"之前我给大家介绍过如何 在 macOS 上使用 multipass 创建轻量级虚拟机来使用 Podman，众小伙伴纷纷齐说真香。今天我要给大家介绍一个全新的黑科技，利用 macOS Big Sur 引入的全新虚拟化框架 Virtualization Kit 来创建更加轻量级的虚拟机。准确地说，这个最新的虚拟化框架并不能直接使用，它只是提供了 API，为许多设备类型定义了标准接口，包括网络、存储等设备，且支持 Virtio 标准。要想使用该框架来创建管理虚拟机，需要对其进行封装，构建出一个易于使用的工具，目前最优秀的就是 vftool。\n下面就来看看如何使用 vftool 来创建 Ubuntu 虚拟机。\n1. 编译 vftool # vftool 使用的是 Swift 语言，要想成功编译出可执行文件，需要安装 Xcode 命令行工具，你可以通过下面的命令直接安装：\n$ xcode-select --install 或者到官方网站下载安装： https://developer.apple.com/download/more/\n或者你也可以直接安装 Xcode。\n安装好 Xcode 命令行工具后，就可以拉取 vftool 仓库进行编译了：\n$ git clone https://github.com/evansm7/vftool.git $ clang -framework Foundation -framework Virtualization vftool/vftool/main.m -o /usr/local/bin/vftool 后面创建虚拟机的时候，你可能会遇到以下的报错：\nConfiguration vaildation failure! Error Domain=VZErrorDomain Code=2 “Virtualization requires the “com.apple.security.virtualization” entitlement” UserInfo={NSDebugDescription=Virtualization requires the “com.apple.security.virtualization” entitlement} 这是因为编译完成后需要对二进制文件进行签名，而签名是需要授权的，所以需要创建一个自签名证书。打开钥匙串访问，依次选择 证书助理 \u0026ndash;\u0026gt; 创建证书：\n选择证书类型为 代码签名，名字随便写，然后点击创建：\n然后利用新建的自签名证书对二进制文件进行签名：\n$ codesign --entitlements vftool/vftool/vftool.entitlements -s \u0026#34;\u0026lt;NAME ON CERTIFICATE\u0026gt;\u0026#34; /usr/local/bin/vftool 除了上面的方法之外，还有一种编译方法，直接运行以下命令：\n$ xcodebuild $ cp build/Release/vftool /usr/local/bin/vftool 现在就可以使用这个二进制文件来创建虚拟机了。\n2. 准备镜像文件 # 需要准备三个文件：\nkernel: https://cloud-images.ubuntu.com/releases/focal/release/unpacked/ubuntu-20.04-server-cloudimg-amd64-vmlinuz-generic initrd: https://cloud-images.ubuntu.com/releases/focal/release/unpacked/ubuntu-20.04-server-cloudimg-amd64-initrd-generic disk image: https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.tar.gz 下载相关文件：\n$ mkdir -p ~/bin/vm $ cd ~/bin/vm $ wget https://cloud-images.ubuntu.com/releases/focal/release/unpacked/ubuntu-20.04-server-cloudimg-amd64-vmlinuz-generic $ wget https://cloud-images.ubuntu.com/releases/focal/release/unpacked/ubuntu-20.04-server-cloudimg-amd64-initrd-generic $ wget https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.tar.gz $ mv ubuntu-20.04-server-cloudimg-amd64-vmlinuz-generic vmlinux $ mv ubuntu-20.04-server-cloudimg-amd64-initrd-generic initrd $ tar xvfz ubuntu-20.04-server-cloudimg-amd64.tar.gz 创建数据盘：\n$ dd if=/dev/zero of=data.img bs=1m count=51200 3. 修改虚拟机网段 # 如果你想自定义虚拟机的网段，可以直接修改文件 /Library/Preferences/SystemConfiguration/com.apple.vmnet.plist。例如修改为：\n\u0026lt;key\u0026gt;Shared_Net_Address\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;192.168.64.1\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;Shared_Net_Mask\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;255.255.255.0\u0026lt;/string\u0026gt; 3. 创建虚拟机 # 直接通过 vftool 创建虚拟机：\n$ vftool \\ -k vmlinux \\ -i initrd \\ -c focal-server-cloudimg-amd64.img \\ -d data.img \\ -m 2048 \\ -a \u0026#34;console=hvc0\u0026#34; 2021-01-14 13:27:08.223 vftool[66147:839169] vftool (v0.3 10/12/2020) starting 2021-01-14 13:27:08.223 vftool[66147:839169] +++ kernel at vmlinux, initrd at initrd, cmdline \u0026#39;console=hvc0\u0026#39;, 1 cpus, 2048MB memory 2021-01-14 13:27:08.224 vftool[66147:839169] +++ fd 3 connected to /dev/ttys000 2021-01-14 13:27:08.224 vftool[66147:839169] +++ Waiting for connection to: /dev/ttys000 从日志信息可以看到该虚拟机连接的 TTY，我这里是 /dev/ttys000。打开一个新的终端窗口，连接该 TTY，然后执行一系列命令来进行初始化操作：\n$ screen /dev/ttys000 \u0026lt;LOTS OF OUTPUT\u0026gt; (initramfs) dd if=/dev/vda of=/dev/vdb bs=1024k \u0026amp; (initramfs) mkdir /mnt (initramfs) mount /dev/vdb /mnt (initramfs) chroot /mnt root@(none):/# touch /etc/cloud/cloud-init.disabled root@(none):/# echo \u0026#39;root:root\u0026#39; | chpasswd root@(none):/# echo \u0026#34;podman\u0026#34; \u0026gt;/etc/hostname root@(none):/# ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N \u0026#39;\u0026#39; -t rsa root@(none):/# ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N \u0026#39;\u0026#39; -t dsa root@(none):/# ssh-keygen -f /etc/ssh/ssh_host_ed25519_key -N \u0026#39;\u0026#39; -t ed25519 root@(none):/# cat \u0026lt;\u0026lt;EOF \u0026gt; /etc/netplan/01-dhcp.yaml network: renderer: networkd ethernets: enp0s1: dhcp4: no addresses: [192.168.64.2/24] gateway4: 192.168.64.1 nameservers: addresses: [114.114.114.114] version: 2 EOF root@(none):/# echo \u0026#34;PermitRootLogin yes\u0026#34; \u0026gt;\u0026gt; /etc/ssh/sshd_config root@(none):/# sed -i \u0026#34;/^PasswordAuthentication/ c PasswordAuthentication yes\u0026#34; /etc/ssh/sshd_config root@(none):/# exit (initramfs) umount /dev/vda 上面的步骤总共干了这么几件事：\n挂载硬盘 禁用 cloud-init 设置主机名和 ssh 秘钥 设置网络 设置 ssh 允许使用 root 用户和密码登录 然后在运行 vftool 命令的窗口中按 CTRL-C 来关闭虚拟机。\n接着使用新的硬盘来启动虚拟机：\n$ vftool \\ -k vmlinux \\ -i initrd \\ -d data.img \\ -m 2048 \\ -a \u0026#34;console=hvc0 root=/dev/vda\u0026#34; \\ -t 0 打开一个新的终端窗口，通过 ssh 连接虚拟机，调整硬盘容量，移除不必要的组件：\n$ ssh root@192.168.64.2 # login as root root@podman:~# systemctl disable --now snapd.service snapd.socket root@podman:~# resize2fs /dev/vda root@podman:~# apt remove -y cloud-init cloud-initramfs-copymods cloud-initramfs-dyn-netconf cloud-guest-utils popularity-contest 看看它的内存占用：\n果然很香！\n4. 开机自启 # MacOS 可以使用 launchctl 来管理服务，它可以控制启动计算机时需要开启的服务，也可以设置定时执行特定任务的脚本，就像 Linux crontab 一样, 通过加装 *.plist 文件执行相应命令。Launchd 脚本存储在以下位置, 默认需要自己创建个人的 LaunchAgents 目录：\n~/Library/LaunchAgents : 由用户自己定义的任务项 /Library/LaunchAgents : 由管理员为用户定义的任务项 /Library/LaunchDaemons : 由管理员定义的守护进程任务项 /System/Library/LaunchAgents : 由 MacOS 为用户定义的任务项 /System/Library/LaunchDaemons : 由 MacOS 定义的守护进程任务项 我们选择在 ~/Library/LaunchAgents/ 目录下创建 vftool.ubuntu.plist 文件，内容如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple Computer//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;ubuntu\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/bin/bash\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-c\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;$HOME/bin/vm/start.sh\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;StandardOutPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/vftool.ubuntu.stdout.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/vftool.ubuntu.stderr.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 创建启动脚本：\n$ cd ~/bin/vm $ cat \u0026lt;\u0026lt;EOF \u0026gt; start.sh #!/bin/bash /usr/local/bin/vftool \\ -k $HOME/bin/vm/vmlinux \\ -i $HOME/bin/vm/initrd \\ -d $HOME/bin/vm/data.img \\ -m 2048 \\ -a \u0026#34;console=hvc0 root=/dev/vda\u0026#34; \\ -t 0 EOF $ chmod +x start.sh 创建日志文件：\n$ touch /var/log/vftool.ubuntu.stdout.log $ touch /var/log/vftool.ubuntu.stderr.log $ sudo chmod a+rw /var/log/vftool.ubuntu.stdout.log $ sudo chmod a+rw /var/log/vftool.ubuntu.stderr.log 设置开机自动启动 Ubuntu 虚拟机：\n$ launchctl load -w ~/Library/LaunchAgents/vftool.ubuntu.plist 启动服务：\n$ launchctl start ubuntu 查看服务：\n$ launchctl list ubuntu { \u0026#34;StandardOutPath\u0026#34; = \u0026#34;/var/log/vftool.ubuntu.stdout.log\u0026#34;; \u0026#34;LimitLoadToSessionType\u0026#34; = \u0026#34;Aqua\u0026#34;; \u0026#34;StandardErrorPath\u0026#34; = \u0026#34;/var/log/vftool.ubuntu.stderr.log\u0026#34;; \u0026#34;Label\u0026#34; = \u0026#34;ubuntu\u0026#34;; \u0026#34;OnDemand\u0026#34; = false; \u0026#34;LastExitStatus\u0026#34; = 256; \u0026#34;PID\u0026#34; = 50797; \u0026#34;Program\u0026#34; = \u0026#34;/bin/bash\u0026#34;; \u0026#34;ProgramArguments\u0026#34; = ( \u0026#34;/bin/bash\u0026#34;; \u0026#34;-c\u0026#34;; \u0026#34;$HOME/bin/vm/start.sh\u0026#34;; ); }; 大功告成，现在就可以通过 ssh 连接虚拟机了：\n$ ssh root@192.168.64.2 5. 共享文件系统 # 虚拟机访问宿主机 # 虚拟机在许多场景中需要访问宿主机的文件系统，vftool 目前还没有太好的办法，只能通过 Mac 的文件共享功能来访问。\n首先进入系统偏好设置中的共享选项。勾中文件共享（如下图），之后右边的文件共享的绿灯会点亮，并显示“文件共享：打开”。\n点击在文件共享界面中右边的共享文件夹下的＋号，在出现的窗口中找到你要共享的目录，点击增加。之后在右边的用户里，进行对该目录的访问权限设置。\n然后 ssh 登录虚拟机，安装 samaba 客户端：\n$ ssh root@192.168.64.2 root@podman:~# apt update root@podman:~# apt install -y cifs-utils 挂载宿主机的文件系统：\n$ mount -t cifs //192.168.64.1/bin /mnt -o username=\u0026lt;USERNAME\u0026gt;,password=\u0026lt;PASSWORD\u0026gt;,nounix,sec=ntlmssp 你也可以写进 /etc/fstab 中，开机自动挂载：\n//192.168.64.1/bin /mnt/bin cifs username=Ryan,password=yang8683060,nounix,sec=ntlmssp 0 0 宿主机访问虚拟机 # 如果宿主机想访问虚拟机的文件系统怎么办呢？\n虚拟机的硬盘其实就是 data.img，文件系统是 ext4，我们可以使用 hdiutil 将 data.img 转换为块设备：\n$ sudo hdiutil attach -nomount data.img /dev/disk2 然后再安装支持 ext4 格式的挂载工具：\n$ brew install --cask osxfuse $ brew install ext4fuse 最后再手动挂载：\n$ sudo ext4fuse /dev/disk2 ~/tmp/ubuntu -o allow_other 挂载点可以根据你自己的喜好设置，我这里设置的是 ~/tmp/ubuntu。\n挂载完成后，就可以在宿主机直接访问虚拟机的文件系统了：\n$ tree -L 1 ~/tmp/ubuntu /Users/Ryan/tmp/ubuntu ├── bin -\u0026gt; usr/bin ├── boot ├── dev ├── etc ├── home ├── lib -\u0026gt; usr/lib ├── lib32 -\u0026gt; usr/lib32 ├── lib64 -\u0026gt; usr/lib64 ├── libx32 -\u0026gt; usr/libx32 ├── lost+found ├── media ├── mnt ├── opt ├── proc ├── root ├── run ├── sbin -\u0026gt; usr/sbin ├── snap ├── srv ├── sys ├── tmp ├── usr └── var 23 directories, 0 files 简直爽歪歪~~\n参考 # Building x86_64 Docker Containers on Apple Silicon Containerized the Apple Silicon Ubuntu Server fails ","date":"2021年1月14日","externalUrl":null,"permalink":"/posts/use-vftool-on-macos/","section":"博客","summary":"之前我给大家介绍过如何 在 macOS 上使用 multipass 创建轻量级虚拟机来使用 Po","title":"macOS Big Sur 使用 vftool 运行 Linux 虚拟机","type":"posts"},{"content":"","date":"2021年1月14日","externalUrl":null,"permalink":"/tags/vftool/","section":"标签","summary":"","title":"vftool","type":"tags"},{"content":"","date":"2021年1月9日","externalUrl":null,"permalink":"/tags/iterm2/","section":"标签","summary":"","title":"iTerm2","type":"tags"},{"content":" 上篇文章我们介绍了 iTerm2 自身的配置和美化，这篇文章我们就来介绍 iTerm2 连接远程服务器的配置和优化。\n对于 YAML 工程师来说，我们经常需要 ssh 登录不同的服务器，每次登录时都要经历两个步骤：\n输入 ssh root@host-ip 输入密码 每次都重复这样的操作，不仅麻烦，还要记忆好多东西。对于 Windows 用户来说，可以使用 Xshell 来实现自动登录功能，macOS 用户就比较麻烦了。iTerm2 是 macOS 平台上最强大的终端工具，虽然默认没有提供自动登录的功能，但我们可以尝试通过它提供的其他功能来打造自动登录的功能。\n当然，既然我写了这篇文章，就说明我已经找到了方法，下面就直接开门见山放干货。我想提醒你的是，我这里提供的方法绝对是你从来没有见过的，你可能会觉得网上能搜到很多和我类似的方案，但如果你仔细看就能看出区别来，网上的方案都是不完美的，和其他功能同时使用时会出现莫名其妙的问题（具体是什么问题后面我会讲到），我把这些问题都解决了，得到了一个极其完美的方案。\n本文将提供两种自动登录方案，首先来看第一种方案。\n1. 通过触发器自动登录 # iTerm2 有一个非常强大的功能叫触发器（Trigger），触发器是用户可配置的正则表达式，当终端会话接收到与正则表达式相匹配的文本时，会执行相关的操作。这里的操作包括突出显示匹配的文本，显示警报，发回文本等等。\n触发器的一种高级用法是捕获与正则表达式匹配的输出，并在工具栏中显示这些匹配线。 例如，您可以创建一个匹配编译器错误的触发器。 当你运行时，错误会出现在你的窗口一侧，你可以点击每一个跳到它的右边。 更多信息可在 Captured Output 手册中找到。\n本文将利用触发器来实现 ssh 自动登录的功能。首先点击 Preference -\u0026gt; Profiles，选中你要登录的服务器，Command 这里填写你的 ssh 登录的 ip 和用户名，如果端口不是 22 还要指定端口：\n然后点击 Advanced,找到 Trriggers,点击 edit：\n在 Regular Eexpression 中，填写你要匹配的正则表达式。由于这里是要在看到 password 的提示后输入密码，所以这里填写 password，如果你服务器的密码提示是 passwd，你要改成匹配这个正则，当然还有些服务器提示的是 Password，所以我们可以用正则 (p|P)ass(word|wd): 全部匹配。在 Action 中选择 Send Text，在 Parameters 中填写你的密码，最后增加一个 \\r 字符。\\r 是回车，这就相当于你输入了密码，并按了下回车。最后，要把 Instant 的复选框选中。\n我这里多加了一个正则表达式，因为第一次登录服务器时会提示 Are you sure you want to continue connecting (yes/no)?。\n现在在你的终端会话中双指轻按触控板，或者鼠标右击，就可以选择你的 Profile 自动登录了：\n到了这一步还没有结束，这个方法看似完美，其实是有问题的。假设你在这台服务器上再通过 ssh 去登录其他服务器，仍然会触发 Triggers；再假设其他服务器的密码和这台服务器的密码是不同的，这时候就会陷入尴尬的境地，不管你尝试多少次，触发器都会自动输入之前设置的密码，你将永远登录不上另一台服务器。\n还有一些其他的问题，比如你在终端中输入的任何命令只要匹配了触发器的正则，就会自动输入密码，使用体验非常不好：\n解决这个问题其实也很简单，只需要提高正则匹配的准确度就行了，直接看图：\n现在再通过 ssh 登录其他服务器，触发器再也不会自动输入密码了：\n在终端中输入的命令也不会匹配到 password 和 Password 等这些单词了：\n到这一步算是完美解决了自动登录的需求。但还是有一点小瑕疵，每台服务器的触发器正则表达式都是不一样的，如果你要登录的服务器很多，这个工作量将非常大，要不要用这种方法可以自己取舍。\n下面我将介绍另外一种方案，相比之前的方案，下面的方案需要编写脚本，但它是可复用的，每台服务器都可以使用同一个脚本。如果你要登录的服务器数量很多，相比之下之前的方案工作量更大。\n2. 通过 expect 自动登录 # expect 是一个自动化交互套件，主要应用于执行命令和程序时，系统以交互形式要求输入指定字符串，实现交互通信。它的自动交互流程如下：\nspawn 启动指定进程 \u0026mdash;\u0026gt; expect 获取指定关键字 \u0026mdash;\u0026gt; send 向指定程序发送指定字符 \u0026mdash;\u0026gt; 执行完成退出\n接下来我们将利用 expect 来实现 ssh 自动登录。首先新建一个文件 /usr/local/bin/iterm2Login.sh，内容如下：\n#!/usr/bin/expect set timeout 30 set host [lindex $argv 0] # 这一行是设置一个变量的意思，变量名随便起，尽量有意义，后面表示的是传入的参数，0 表示第一个参数，后面会用到。 set port [lindex $argv 1] set user [lindex $argv 2] set pswd [lindex $argv 3] spawn ssh -p $port $user@$host # spawn 是 expect 环境的内部命令，它主要的功能是给 ssh 运行进程加个壳，用来传递交互指令。 expect { \u0026#34;(yes/no)?\u0026#34; {send \u0026#34;yes\\n\u0026#34;;exp_continue;} -re \u0026#34;(p|P)ass(word|wd):\u0026#34; {send \u0026#34;$pswd\\n\u0026#34;} } # expect 也是 expect 环境的一个内部命令，用来判断上一个指令输入之后的得到输出结果是否包含 \u0026#34;\u0026#34; 双引号里的字符串，-re 表示通过正则来匹配。 # 如果是第一次登录，会出现 \u0026#34;yes/no\u0026#34; 的字符串，就发送（send）指令 \u0026#34;yes\\r\u0026#34;，然后继续（exp_continue）。 interact # interact：执行完成后保持交互状态，把控制权交给控制台。 argv 0, argv 1, argv 2, argv 3 三个参数依次为 ip、端口号、用户名、密码。\n赋予脚本执行权限：\n$ sudo chmod +x /usr/local/bin/iterm2Login.sh 将 Profile 中的 Command 部分替换成通过上面的脚本来登录：\n最后将触发器中的所有规则都删掉，只留下一个：\n大功告成！\n看来这个方法比上面的方法更加完美，因为 expect 只针对当前登录的服务器，后续再通过当前服务器 ssh 登录其他服务器，不会再自动输入密码什么的。如果服务器数量很多，也不用再一个一个去改触发器规则，简直太爽了。\n当然，expect 也会遇到一些问题，比如无法正常使用 lrzsz，而这些问题在使用触发器时是不存在的。当然，这些问题是可以解决的，解决之后，expect 将变成彻底完美的方案，触发器的方案就可以抛之脑后了。\n下面我将详细介绍 expect 和 lrzsz 一起使用的问题，及其解决方案。\n3. 使用 Zmodem 实现快速传输文件 # 很多时候我们需要在本机和远端服务器间进行文件传输，通常都是使用 scp 命令进行传输，但其实通过 Zmodem 传输起来更方便。\n什么是 Zmodem # Zmodem 是针对 modem 的一种支持错误校验的文件传输协议。Zmodem 是 Ymodem 的改进版，后者又是 Xmodem 的改进版。Zmodem 不仅能传输更大的数据，而且错误率更小。\n利用 Zmodem 协议，可以在 modem 上发送 512 字节的数据块。Zmodem 包含一种名为检查点重启的特性，如果通信链接在数据传输过程中中断，能从断点处而不是从开始处恢复传输。\n配置 iTerm2 支持 Zmodem # 要让 iTerm2 在远端服务器上支持通过 Zmodem 协议传输，需要分别在服务端和客户端进行相应配置。网上大多数文档都只提到客户端部分。因为收发方都必须有支持 Zmodem 协议的工具，才能进行正常收发。下面我们就来看看是如何进行配置的：\n服务端配置 # lrzsz 软件包是 支持 Zmodem 协议的工具包。 其包含的 rz、sz 命令是通过 ZModem 协议在远程服务器和终端机器间上传下载文件的利器。\n为了正确通过 sz、rz 命令传输文件，服务端需要安装 lrzsz 软件包的。\nUbuntu 或 Debian $ apt-get install lrzsz RHEL 或 CentOS $ yum install lrzsz 客户端配置 # 和服务器端一样，客户端同样需要安装 lrzsz 软件包。这里通过 Homebrew 进行 lrzsz 软件包安装：\n$ brew install lrzsz 配置 iTerm2 # 在全球最大同性交友网站 Github 上，已经有人共享了一个叫 “ZModem integration for iTerm 2” 的项目。我们只需下载其相应脚本，并进行简单配置就可以很容易的在 iTerm2 上实现对 Zmodem 的支持。\n项目地址： https://github.com/kuoruan/iterm2-zmodem\n下载并安装脚本 $ wget -qO /usr/local/bin/iterm2-zmodem.sh https://github.com/kuoruan/iterm2-zmodem/raw/master/iterm2-zmodem.sh $ chmod +x /usr/local/bin/iterm2-zmodem.sh 配置 iTerm2 上的触发器 打开 iTerm2 ，点击 Preferences → Profiles 选择指定的 Profile。然后继续选择 Advanced → Triggers，并点击 Edit 添加两个触发器。\n按如下内容添加两个触发器，首先增加 sz 指令的触发器：\nRegular expression: rz waiting to receive.\\*\\*B0100 Action: Run Silent Coprocess Parameters: /usr/local/bin/iterm2-zmodem.sh send Instant: checked 其次增加 rz 指令的触发器：\nRegular expression: \\*\\*B00000000000000 Action: Run Silent Coprocess Parameters: /usr/local/bin/iterm2-zmodem.sh recv Instant: checked 成功增加完成后的效果，类似下图：\n配置这两个触发器的作用就是让 iTerm2 根据终端上显示的字符通过指定的触发器调用相应的发送和接收脚本。\n使用 Zmodem 传输文件 # 发送文件到远端服务器 # 在远端服务器执行 rz 命令 本地选择文件传输 等待传输指示消失 接收远端服务器的文件 # 在远端服务器执行 sz filename1 filename2 … filenameN 命令 本地选择目录保存 等待传输指示消失 Zmodem 与 expect 结合 # 如果你真的按照我提供的步骤操作了，最后你会发现根本无法传输文件。其实这个问题不在于 Zmodem 本身，而是 expect 的问题，如果你将 Profile 的 Command 换成 ssh root@host 这种形式，就可以正常传输文件了。\n难道 expect 真的就没有办法了吗？那之前的工作岂不是都化为乌有了？别慌，不但有办法，而且这个办法非常简单，简单的让你想笑。只需要在 Profile 的 Command 命令前面加上一句 export LC_CTYPE=en_US 就行了：\n收工！\n4. 总结 # 本文详细介绍了 macOS 平台中的 iTerm2 如何使用触发器和 expect 来实现 ssh 自动登录远程服务器，以及如何在 macOS 下通过 Zmodem快速传输文件。当 expect 和 Zmodem 一起使用时，会出现一些莫名其妙的问题，本文最后也给出了解决方案。\n参考 # 在 iTerm2 中使用 Zmodem 实现快速传输文件 ","date":"2021年1月9日","externalUrl":null,"permalink":"/posts/iterm2-auto-login/","section":"博客","summary":"上篇文章我们介绍了 iTerm2 自身的配置和美化，这篇文章我们就来介绍 iTerm2","title":"iTerm2 配置与美化：SSH 自动登录，并使用 Zmodem 实现快速传输文件","type":"posts"},{"content":"前段时间 ipfire 的 Michael Tremer 写过一篇文章叫 《Why not WireGuard》，随后不久， Tailscale 的大佬 Avery Pennarun 也写了一篇文章来和 Michael Tremer 叫板，文章的标题就很挑衅： 《Why not \u0026ldquo;Why not WireGuard?\u0026quot;》，整篇文章的风格就是针对 Michael Tremer 的观点逐一反驳，老刺激了。咱也不知道谁对谁错，咱也不敢问，端个小板凳看戏就是了。\n以下是这篇文章的译文。\n作者开篇就提出 Michael Tremer 的那篇文章包含了一些错误的观念和一些过时的信息，然后就开门见山直接一一反驳。\n1. WireGuard 能否取代 IPSec？ # 原作者的观点：\nNo. There is no chance the big vendors […] will pick up WireGuard. They do not jump onto trains like this unless there is a big necessity.\n译文：\n不！Cisco 和 Juniper 等大厂不可能使用 WireGuard，除非迫不得已，他们是不会上 WireGuard 的车的。\nTremer 这里讨论的是商业 VPN 硬件/软件厂商，这些厂商大多使用的是集 VPN 网关和 spoke 架构。他说的没错，大多数 IPsec VPN 厂商确实不太可能升级到 WireGuard，但客户是怎么想的呢？很少有客户想让现有的 VPN 网关直接支持新协议，相反，他们渴望使用更轻巧、限制更少的东西来取代 VPN 网关。\n简而言之，WireGuard 以简单的软件解决方案取代了 VPN 硬件，因此它不需要传统硬件供应商的支持。\n2. WireGuard 实现了 Road Warrior？ # 原作者的观点：\nRight now, WireGuard has a huge backlog of features that it needs to implement to be suitable for this use-case. It does not, for example, allow using a dynamic IP address on the server side of the tunnel which breaks a whole use-case.\n译文：\nWireGuard 目前还有很多功能未实现，例如不能使用动态 IP 来建立连接。要想实现漫游功能，还有很长的路要走。\nTremer 认为 WireGuard 缺少大量功能，但这里只讨论了动态 IP，没关系，我们来看看他关于动态 IP 的讨论是否正确。\n对于 WireGuard 而言，只要有一端是静态 IP，另一端不管是不是静态 IP，都能正常工作。而他虽然一开始说 WireGuard 不支持 road warrior，但后面似乎都在讨论连接的两端都有动态 IP 地址所引起的问题，这就属于混淆视听了。诚然，WireGuard 在两端都是动态 IP 的支持方面确实没有做到开箱即用，但可以通过各种脚本和高级工具来支持。\n即使两端都是动态 IP 地址，WireGuard 也能正常工作。你可以将 WireGuard 客户端配置为指向服务器的 DNS 名称，并且该 DNS 名称可以使用动态 DNS 定期更新。虽然 WireGuard 只在启动时解析 DNS 名称，后续 DNS 更新之后也不会重新解析，但我们可以通过自动化脚本来定期重启客户端。当然，Tailscale 用了别的方法来支持动态 IP 地址，这里不便透露。\n3. WireGuard 真的好用吗？ # 原作者的观点：\nIs IPsec really hard to use? No, it clearly is not if the vendor has done their homework right and provides an interface that is easy to use.\n译文：\nIPsec 真的很难用吗？恐怕不是这样，如果厂商做了正确的功课，并提供了易于使用的界面（比如， IPFire），就不会难用。\nTremer 认为 IPsec 不算很难用，只需要提供自己的公网地址、peer 的公网地址、子网和预先共享的秘钥，之后 VPN 就可以兼容所有厂商的产品。这。。。\n首先，配置 IPSec 只知道这些信息是不够的，还需要指定加密算法。当然 IPSec 是有默认加密算法的，但几乎没人会用默认的，因为默认的既不安全也不能跨平台，所以需要设定一个靠谱的加密算法，那么问题来了，加密算法会直接影响到 IPSec 厂商之间的兼容性，大多数人都不是密码学专家，怎么会知道该用哪种算法？\n其次，他认为需要为隧道两端指定公网地址。这个操作就很迷了，你之前不是说 WireGuard 的缺点就包括“两端需要指定静态 IP”吗？？还说 WireGuard 不支持动态 IP，现在又说 IPSec 也需要这么做，你确定你是在夸 IPSec？\n事实上，不管是 WireGuard 还是 IPSec，只要有一端是静态 IP 就可以正常工作，所以最多只需要配置一个公网地址。\n最后，他建议在隧道两端使用预先共享的秘钥（PSK）。PSK 是最弱鸡的一种认证方式（密码就是 PSK 的一种形式），只要从一个节点上窃取到 PSK，窃取者就可以冒充任何一端并伪造两端的流量。\n比起 PSK，公钥认证会更安全，IPsec 和 WireGuard 都允许使用公钥认证，不同点在于：IPSec 是可选的，而 WireGuard 是强制的。IPSec 所谓的“灵活性”会带来很多安全隐患，他自己也说了：\n与 OpenBSD 系统之间建立隧道，过程可能会比较痛苦。\n他似乎认为在 OpenBSD 上配置 IPSec 很复杂。虽然我们的团队并不熟悉 OpenBSD 上的 IPsec，但我们知道在 OpenBSD 上配置 WireGuard 就和其他平台一样简单，没有任何区别。\n4. 协议复杂度真的很重要吗？ # 原作者的观点：\nThe end-user does not have to worry about the complexity of the protocol. If that was an issue we would have definitely gone rid of SIP and H.323, FTP and other protocols that don’t cope well with NAT and are decades old.\n译文：\n作为终端用户，其实无需考虑协议的复杂度。如果复杂度真的影响很大，我们肯定早就摆脱 SIP、 H.323 和 FTP 等不能很好地应对 NAT 的协议了。\n搞的那么复杂真的好吗？下面来看 由 N Ferguson 和 B. Schneier 于 2003 年发表的论文中的一段话：\nIPSec 太复杂了，很不安全。这个设计的初衷显然是想通过不同的选项来支持各种不同的情况，但最终导致整个 VPN 系统远远超出了用当前的方法论可以分析或正确实现的复杂程度，它就是个黑盒子。因此，任何 IPSec 系统都无法保证其高度安全性。\n这篇论文已经发表了 16 年了，而 IPSec 的复杂度只增不减，变得越来越无法分析，大家已经渐渐从 IPSec 转向 TLS。都 2021 年了，IPSec 的过于复杂使其濒临淘汰，现在大家都有了更好的选择，没错我说的就是 WireGuard。\nTremer 又说了：\nUser-authentication using username/password or a SIM card with EAP. […] WireGuard does not have that.\n译文：\nWireGuard 不能使用用户名/密码或带有 EAP 的 SIM 卡进行用户认证。\n这句话我部分认同，因为它只对核心的 WireGuard 适用。核心的 WireGuard 只是一个数据平面，可以在其上层建立不同的秘钥交换机制，Tailscale 就提供了 这样的秘钥交换机制（适用 Oauth2、OIDC 或 SAML 进行用户认证）。\n与 IPsec 非常复杂的密钥协商协议相比，只对核心 WireGuard 的安全性进行分析和审计，然后再对上面单独的密钥交换机制进行审计，这样要容易得多。\n5. 如何更新加密方式 # 接下来，Tremer 批评了 WireGuard 的加密方式：它只允许使用单一的加密方式。\nIf you were to change the cipher you are using from one day to the next one, you would need to upgrade your WireGuard software on all those laptops, phones, etc. at the same time.\n译文：\n假设现在你改了加密算法，那么就需要同时更新所有客户端的加密算法才能正常工作。\n这种说法本身就是错误的。WireGuard 的迭代升级过程中肯定会支持第二种加密方式，只是时间问题。只要觉得现有的加密方式可能有安全隐患了，新的加密方式立马安排上。\n当然，为了解决需要同时更新所有客户端这个问题，需要对 WireGuard 进行改进，使其同时支持两种加密算法，只需要支持两种就行。这样就可以实现在更新过程中，使用旧秘钥的客户端仍然有效，直到所有客户端更新完毕后，才会弃用旧秘钥。\n我知道，大多数 VPN 都提供了数千种不同的算法组合来供用户选择，但大多数加密算法都不安全或解析速度很慢，将来 WireGuard 只需要同时支持两种加密算法足矣。\n6. 加密算法 # 原作者的观点：\nI would conclude that practically the same cryptography is available for all VPNs here. Therefore WireGuard is not more or less secure than the others when it comes to encryption or data integrity.\n译文：\n我的结论是：实际上所有的 VPN 都可以使用相同的加密技术，WireGuard 在加密或数据完整性方面并没有比其他的 VPN 更安全或更不安全。\n从表面上看，这种说法是正确的。你可以为 IPSec 选择不同的加密算法进行组合，使其和 WireGuard 的唯一加密方式大致相同。\n然而事实上并不是这样，首先你必须知道如何选择恰当的加密算法进行组合，这是密码学高手才能干的事情好吗，大部分用户谁能干这事？其次，就算你能找到合适的加密算法，它能和所有的 VPN 硬件或软件兼容吗？\n更讽刺的是，虽然 IPSec 标准允许使用几乎所有的加密算法，但它并没有强制要求任何一种加密算法。也就是说，你会遇到这样一种情况：隧道两端完全符合 IPSec 标准，但却无法通信，八成就是加密算法不一致了，你要做的就是不断调试以匹配对方的加密算法。。。\n而 WireGuard 目前只有一种加密算法，别无选择，不会出现上述情况。即使将来支持两种加密算法，遇到上述情况也很容易解决，因为只有两种可能啊，那还不简单？\n7. WireGuard 真的很快吗？ # 在本节中，Tremer 提出了几个立不住脚的观点，大概意思就是由于 CPU 的发展，AES 加密可能会比 ChaCha20 更快。这个说法我不赞同也不反对，因为在没有基于特定平台和特定语言进行测试的情况下，目前还不确定这种说法是否正确。\n不过那不重要，对于 IPsec 和 WireGuard 而言，在几乎所有的场景下，不管你是使用 AES 还是 ChaCha20，加密解密速度都不是瓶颈。\n当然，如果你的网络带宽远超过 10 Gbit/秒，加密解密就会遇到瓶颈，不过这种情况极为罕见。\n还有一点，IPSec 等传统 VPN 是有 VPN 网关的，而 VPN 网关的硬件性能比较弱，如果同时有很多个 IPSec 用户交换数据，就会出现阻塞。当然这不是 IPSec 的问题，略过。\n除此之外，移动设备的 CPU 比桌面服务器的 CPU 性能差，加密解密速度也慢一些，但移动设备的网络更慢，假设移动设备的加密解密占用了 1% 的时间，则网络传输就会占用 99% 的时间。所以对于移动设备而言，加密解密速度可以忽略不计，主要的瓶颈在网络。\n另外一点需要注意的是 点对多点架构和中心辐射型架构的区别。\n一般来说，中心辐射型网络有一个 VPN 网关，这个网关通常都有一个静态 IP 地址，其他所有的客户端都需要连接这个 VPN 网关，再由网关将流量转发到其他的客户端。\n这种架构有很多问题。首先，用户可能离 VPN 网关很近，也可能很远，如果离得很远，延迟就会很高；其次，它想访问的另外一个客户端可能离 VPN 网关也会很远，这样又增加了一倍延迟。想象一下你的 VPN 网关在旧金山，你的家和公司都在纽约，你在纽约的家中通过旧金山的 VPN 网关来访问纽约的公司内网服务，岂不是很蛋疼。。\nWireGuard 就比较先进了，它支持点对多点架构，同一个客户端可以同时连接多个 peer，而不是只连接一个 peer，再通过该 peer 将流量转发到其他客户端。\n8. 与 Linux 内核的集成问题 # 这个论点已经过时了，自从他的文章写完后，WireGuard 模块就被集成到最新的 Linux 内核中了。。。\n9. 理想与现实 # 原作者的观点：\nUnfortunately every time, when a customer asks me to help them setting up a VPN, the credentials that they are getting are using old ciphers. 3DES in combination with MD5 is a common candidate as well as AES-256 with SHA1. Although the latter is better, it is still not what I would like to use today.\n译文：\n现实情况是，每次当客户要求我帮他们搭建 VPN 时，给到他们手里的证书都是使用旧的加密方式，通常是 3DES 和 MD5 结合，或者 AES-256 和 SHA1 结合。至于秘钥交换，我们一直在使用 RSA，虽然速度很慢，但足够安全。\n很明显，Tremer 这里说的只是他自己的客户，全世界的客户多了去了，难道都是他的客户？他的这些客户需要让客户端软件与传统的 IPSec VPN 服务器进行通信，而这些服务器可能是在几年前配置的，只支持过时的、有安全隐患的加密算法，客户别无选择，只能选择旧的加密算法。\n从长远来看，VPN 网关不是必要的，首先应该抛弃的就是强制性的 VPN 网关。WireGuard 是开源的，可以运行在虚拟机中（这就避免了硬件锁定和厂商锁定），目前只支持一种众所周知非常快速安全的单一加密算法，而且可以在其上层建立任何秘钥交换机制。毫无疑问，WireGuard 就是 VPN 的未来。\n","date":"2021年1月5日","externalUrl":null,"permalink":"/posts/why-not-why-not-wireguard/","section":"博客","summary":"前段时间 ipfire 的 Michael Tremer 写过一篇文章叫 《Why not WireGuard》","title":"Why not \"Why not WireGuard?\"","type":"posts"},{"content":"","date":"2020年12月30日","externalUrl":null,"permalink":"/tags/harbor/","section":"标签","summary":"","title":"Harbor","type":"tags"},{"content":"系统环境：\nkubernetes 版本：1.18.10 Harbor Chart 版本：1.5.2 Harbor 版本：2.1.2 Helm 版本：3.3.4 持久化存储驱动：Ceph RBD 1. Harbor 简介 # 简介 # Harbor 是一个开放源代码容器镜像注册表，可通过基于角色权限的访问控制来管理镜像，还能扫描镜像中的漏洞并将映像签名为受信任。Harbor 是 CNCF 孵化项目，可提供合规性，性能和互操作性，以帮助跨 Kubernetes 和 Docker 等云原生计算平台持续，安全地管理镜像。\n特性 # 管理：多租户、可扩展 安全：安全和漏洞分析、内容签名与验证 2. 创建自定义证书 # 安装 Harbor 我们会默认使用 HTTPS 协议，需要 TLS 证书，如果我们没用自己设定自定义证书文件，那么 Harbor 将自动创建证书文件，不过这个有效期只有一年时间，所以这里我们生成自签名证书，为了避免频繁修改证书，将证书有效期为 100 年，操作如下：\n安装 cfssl # fssl 是 CloudFlare 开源的一款 PKI/TLS 工具,cfssl 包含一个命令行工具和一个用于签名，验证并且捆绑 TLS 证书的HTTP API服务,使用 Go 语言编写.\ngithub: https://github.com/cloudflare/cfssl\n下载地址: https://pkg.cfssl.org/\nmacOS 安装步骤：\n🐳 → brew install cfssl 通用安装方式：\n🐳 → wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -O /usr/local/bin/cfssl 🐳 → wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -O /usr/local/bin/cfssljson 🐳 → wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -O /usr/local/bin/cfssl-certinfo 🐳 → chmod +x /usr/local/bin/cfssl* 获取默认配置 # 🐳 → cfssl print-defaults config \u0026gt; ca-config.json 🐳 → cfssl print-defaults csr \u0026gt; ca-csr.json 生成 CA 证书 # 将ca-config.json内容修改为：\n{ \u0026#34;signing\u0026#34;: { \u0026#34;default\u0026#34;: { \u0026#34;expiry\u0026#34;: \u0026#34;876000h\u0026#34; }, \u0026#34;profiles\u0026#34;: { \u0026#34;harbor\u0026#34;: { \u0026#34;expiry\u0026#34;: \u0026#34;876000h\u0026#34;, \u0026#34;usages\u0026#34;: [ \u0026#34;signing\u0026#34;, \u0026#34;key encipherment\u0026#34;, \u0026#34;server auth\u0026#34; ] } } } } 修改ca-csr.json文件内容为：\n{ \u0026#34;CN\u0026#34;: \u0026#34;CA\u0026#34;, \u0026#34;key\u0026#34;: { \u0026#34;algo\u0026#34;: \u0026#34;rsa\u0026#34;, \u0026#34;size\u0026#34;: 2048 }, \u0026#34;names\u0026#34;: [ { \u0026#34;C\u0026#34;: \u0026#34;CN\u0026#34;, \u0026#34;ST\u0026#34;: \u0026#34;hangzhou\u0026#34;, \u0026#34;L\u0026#34;: \u0026#34;hangzhou\u0026#34;, \u0026#34;O\u0026#34;: \u0026#34;harbor\u0026#34;, \u0026#34;OU\u0026#34;: \u0026#34;System\u0026#34; } ] } 修改好配置文件后,接下来就可以生成 CA 证书了：\n🐳 → cfssl gencert -initca ca-csr.json | cfssljson -bare ca 2020/12/30 00:45:55 [INFO] generating a new CA key and certificate from CSR 2020/12/30 00:45:55 [INFO] generate received request 2020/12/30 00:45:55 [INFO] received CSR 2020/12/30 00:45:55 [INFO] generating key: rsa-2048 2020/12/30 00:45:56 [INFO] encoded CSR 2020/12/30 00:45:56 [INFO] signed certificate with serial number 529798847867094212963042958391637272775966762165 此时目录下会出现三个文件：\n🐳 → tree ├── ca-config.json #这是刚才的json ├── ca.csr ├── ca-csr.json #这也是刚才申请证书的json ├── ca-key.pem ├── ca.pem 这样 我们就生成了:\n根证书文件: ca.pem 根证书私钥: ca-key.pem 根证书申请文件: ca.csr (csr 是不是 client ssl request?) 签发证书 # 创建harbor-csr.json,内容为：\n{ \u0026#34;CN\u0026#34;: \u0026#34;harbor\u0026#34;, \u0026#34;hosts\u0026#34;: [ \u0026#34;example.net\u0026#34;, \u0026#34;*.example.net\u0026#34; ], \u0026#34;key\u0026#34;: { \u0026#34;algo\u0026#34;: \u0026#34;rsa\u0026#34;, \u0026#34;size\u0026#34;: 2048 }, \u0026#34;names\u0026#34;: [ { \u0026#34;C\u0026#34;: \u0026#34;US\u0026#34;, \u0026#34;ST\u0026#34;: \u0026#34;CA\u0026#34;, \u0026#34;L\u0026#34;: \u0026#34;San Francisco\u0026#34;, \u0026#34;O\u0026#34;: \u0026#34;harbor\u0026#34;, \u0026#34;OU\u0026#34;: \u0026#34;System\u0026#34; } ] } 使用之前的 CA 证书签发 harbor 证书：\n🐳 → cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=harbor harbor-csr.json | cfssljson -bare harbor 2020/12/30 00:50:31 [INFO] generate received request 2020/12/30 00:50:31 [INFO] received CSR 2020/12/30 00:50:31 [INFO] generating key: rsa-2048 2020/12/30 00:50:31 [INFO] encoded CSR 2020/12/30 00:50:31 [INFO] signed certificate with serial number 372641098655462687944401141126722021767151134362 此时目录下会多几个文件：\n🐳 → tree -L 1 ├── etcd.csr ├── etcd-csr.json ├── etcd-key.pem ├── etcd.pem 至此，harbor 的证书生成完成。\n生成 Secret 资源 # 创建 Kubernetes 的 Secret 资源，且将证书文件导入：\n- n：指定创建资源的 Namespace \u0026ndash;from-file：指定要导入的文件地址 🐳 → kubectl create ns harbor 🐳 → kubectl -n harbor create secret generic harbor-tls --from-file=tls.crt=harbor.pem --from-file=tls.key=harbor-key.pem --from-file=ca.crt=ca.pem 查看是否创建成功：\n🐳 → kubectl -n harbor get secret harbor-tls NAME TYPE DATA AGE harbor-tls Opaque 3 1m 3. 使用 Ceph S3 为 Harbor chart 提供后端存储 # 创建 radosgw # 如果你是通过 ceph-deploy 部署的，可以通过以下步骤创建 radosgw：\n先安装 radosgw：\n🐳 → ceph-deploy install --rgw 172.16.7.1 172.16.7.2 172.16.7.3 然后创建 radosgw：\n🐳 → ceph-deploy rgw create 172.16.7.1 172.16.7.2 172.16.7.3 如果你是通过 cephadm 部署的，可以通过以下步骤创建 radosgw：\ncephadm 将 radosgw 部署为管理特定领域和区域的守护程序的集合。例如，要在 172.16.7.1 上部署 1 个服务于 mytest 领域和 myzone 区域的 rgw 守护程序：\n#如果尚未创建领域，请首先创建一个领域： 🐳 → radosgw-admin realm create --rgw-realm=mytest --default #接下来创建一个新的区域组： 🐳 → radosgw-admin zonegroup create --rgw-zonegroup=myzg --master --default #接下来创建一个区域： 🐳 → radosgw-admin zone create --rgw-zonegroup=myzg --rgw-zone=myzone --master --default #为特定领域和区域部署一组radosgw守护程序： 🐳 → ceph orch apply rgw mytest myzone --placement=\u0026#34;1 172.16.7.1\u0026#34; 查看服务状态：\n🐳 → ceph orch ls|grep rgw rgw.mytest.myzone 1/1 5m ago 7w count:1 k8s01 docker.io/ceph/ceph:v15 4405f6339e35 测试服务是否正常：\n🐳 → curl -s http://172.16.7.1 正常返回如下数据：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;ListAllMyBucketsResult xmlns=\u0026#34;http://s3.amazonaws.com/doc/2006-03-01/\u0026#34;\u0026gt; \u0026lt;Owner\u0026gt; \u0026lt;ID\u0026gt;anonymous\u0026lt;/ID\u0026gt; \u0026lt;DisplayName\u0026gt;\u0026lt;/DisplayName\u0026gt; \u0026lt;/Owner\u0026gt; \u0026lt;Buckets\u0026gt;\u0026lt;/Buckets\u0026gt; \u0026lt;/ListAllMyBucketsResult\u0026gt; 查看 zonegroup：\n🐳 → radosgw-admin zonegroup get { \u0026#34;id\u0026#34;: \u0026#34;ed34ba6e-7089-4b7f-91c4-82fc856fc16c\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;myzg\u0026#34;, \u0026#34;api_name\u0026#34;: \u0026#34;myzg\u0026#34;, \u0026#34;is_master\u0026#34;: \u0026#34;true\u0026#34;, \u0026#34;endpoints\u0026#34;: [], \u0026#34;hostnames\u0026#34;: [], \u0026#34;hostnames_s3website\u0026#34;: [], \u0026#34;master_zone\u0026#34;: \u0026#34;650e7cca-aacb-4610-a589-acd605d53d23\u0026#34;, \u0026#34;zones\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;650e7cca-aacb-4610-a589-acd605d53d23\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;myzone\u0026#34;, \u0026#34;endpoints\u0026#34;: [], \u0026#34;log_meta\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;log_data\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;bucket_index_max_shards\u0026#34;: 11, \u0026#34;read_only\u0026#34;: \u0026#34;false\u0026#34;, \u0026#34;tier_type\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;sync_from_all\u0026#34;: \u0026#34;true\u0026#34;, \u0026#34;sync_from\u0026#34;: [], \u0026#34;redirect_zone\u0026#34;: \u0026#34;\u0026#34; } ], \u0026#34;placement_targets\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;default-placement\u0026#34;, \u0026#34;tags\u0026#34;: [], \u0026#34;storage_classes\u0026#34;: [ \u0026#34;STANDARD\u0026#34; ] } ], \u0026#34;default_placement\u0026#34;: \u0026#34;default-placement\u0026#34;, \u0026#34;realm_id\u0026#34;: \u0026#34;e63c234c-e069-4a0d-866d-1ebdc69ec5fe\u0026#34;, \u0026#34;sync_policy\u0026#34;: { \u0026#34;groups\u0026#34;: [] } } Create Auth Key # 🐳 → ceph auth get-or-create client.radosgw.gateway osd \u0026#39;allow rwx\u0026#39; mon \u0026#39;allow rwx\u0026#39; -o /etc/ceph/ceph.client.radosgw.keyring 分发 /etc/ceph/ceph.client.radosgw.keyring 到其它 radosgw 节点。\n创建对象存储用户和访问凭证 # Create a radosgw user for s3 access\n🐳 → radosgw-admin user create --uid=\u0026#34;harbor\u0026#34; --display-name=\u0026#34;Harbor Registry\u0026#34; Create a swift user\n🐳 → adosgw-admin subuser create --uid=harbor --subuser=harbor:swift --access=full Create Secret Key\n🐳 → radosgw-admin key create --subuser=harbor:swift --key-type=swift --gen-secret 记住 keys 字段中的 access_key \u0026amp; secret_key\n创建存储桶（bucket） # 首先需要安装 awscli：\n🐳 → pip3 install awscli -i https://pypi.tuna.tsinghua.edu.cn/simple 查看秘钥：\n🐳 → radosgw-admin user info --uid=\u0026#34;harbor\u0026#34;|jq .keys [ { \u0026#34;user\u0026#34;: \u0026#34;harbor\u0026#34;, \u0026#34;access_key\u0026#34;: \u0026#34;VGZQY32LMFQOQPVNTDSJ\u0026#34;, \u0026#34;secret_key\u0026#34;: \u0026#34;YZMMYqoy1ypHaqGOUfwLvdAj9A731iDYDjYqwkU5\u0026#34; } ] 配置 awscli：\n🐳 → aws configure --profile=ceph AWS Access Key ID [None]: VGZQY32LMFQOQPVNTDSJ AWS Secret Access Key [None]: YZMMYqoy1ypHaqGOUfwLvdAj9A731iDYDjYqwkU5 Default region name [None]: Default output format [None]: json 配置完成后，凭证将会存储到 ~/.aws/credentials：\n🐳 → cat ~/.aws/credentials [ceph] aws_access_key_id = VGZQY32LMFQOQPVNTDSJ aws_secret_access_key = YZMMYqoy1ypHaqGOUfwLvdAj9A731iDYDjYqwkU5 配置将会存储到 ~/.aws/config：\n🐳 → cat ~/.aws/config [profile ceph] region = cn-hangzhou-1 output = json 创建存储桶（bucket）：\n🐳 → aws --profile=ceph --endpoint=http://172.16.7.1 s3api create-bucket --bucket harbor 查看存储桶（bucket）列表：\n🐳 → radosgw-admin bucket list [ \u0026#34;harbor\u0026#34; ] 查看存储桶状态：\n🐳 → radosgw-admin bucket stats [ { \u0026#34;bucket\u0026#34;: \u0026#34;harbor\u0026#34;, \u0026#34;num_shards\u0026#34;: 11, \u0026#34;tenant\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;zonegroup\u0026#34;: \u0026#34;ed34ba6e-7089-4b7f-91c4-82fc856fc16c\u0026#34;, \u0026#34;placement_rule\u0026#34;: \u0026#34;default-placement\u0026#34;, \u0026#34;explicit_placement\u0026#34;: { \u0026#34;data_pool\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;data_extra_pool\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;index_pool\u0026#34;: \u0026#34;\u0026#34; }, \u0026#34;id\u0026#34;: \u0026#34;650e7cca-aacb-4610-a589-acd605d53d23.194274.1\u0026#34;, \u0026#34;marker\u0026#34;: \u0026#34;650e7cca-aacb-4610-a589-acd605d53d23.194274.1\u0026#34;, \u0026#34;index_type\u0026#34;: \u0026#34;Normal\u0026#34;, \u0026#34;owner\u0026#34;: \u0026#34;harbor\u0026#34;, \u0026#34;ver\u0026#34;: \u0026#34;0#1,1#1,2#1,3#1,4#1,5#1,6#1,7#1,8#1,9#1,10#1\u0026#34;, \u0026#34;master_ver\u0026#34;: \u0026#34;0#0,1#0,2#0,3#0,4#0,5#0,6#0,7#0,8#0,9#0,10#0\u0026#34;, \u0026#34;mtime\u0026#34;: \u0026#34;2020-12-29T17:19:02.481567Z\u0026#34;, \u0026#34;creation_time\u0026#34;: \u0026#34;2020-12-29T17:18:58.940915Z\u0026#34;, \u0026#34;max_marker\u0026#34;: \u0026#34;0#,1#,2#,3#,4#,5#,6#,7#,8#,9#,10#\u0026#34;, \u0026#34;usage\u0026#34;: {}, \u0026#34;bucket_quota\u0026#34;: { \u0026#34;enabled\u0026#34;: false, \u0026#34;check_on_raw\u0026#34;: false, \u0026#34;max_size\u0026#34;: -1, \u0026#34;max_size_kb\u0026#34;: 0, \u0026#34;max_objects\u0026#34;: -1 } } ] 查看存储池状态\n🐳 → rados df POOL_NAME USED OBJECTS CLONES COPIES MISSING_ON_PRIMARY UNFOUND DEGRADED RD_OPS RD WR_OPS WR USED COMPR UNDER COMPR .rgw.root 2.3 MiB 13 0 39 0 0 0 533 533 KiB 21 16 KiB 0 B 0 B cache 0 B 0 0 0 0 0 0 0 0 B 0 0 B 0 B 0 B device_health_metrics 3.2 MiB 18 0 54 0 0 0 925 929 KiB 951 951 KiB 0 B 0 B kubernetes 735 GiB 72646 99 217938 0 0 0 48345148 242 GiB 283283048 7.3 TiB 0 B 0 B myzone.rgw.buckets.index 8.6 MiB 11 0 33 0 0 0 44 44 KiB 11 0 B 0 B 0 B myzone.rgw.control 0 B 8 0 24 0 0 0 0 0 B 0 0 B 0 B 0 B myzone.rgw.log 6 MiB 206 0 618 0 0 0 2188882 2.1 GiB 1457026 32 KiB 0 B 0 B myzone.rgw.meta 960 KiB 6 0 18 0 0 0 99 80 KiB 17 8 KiB 0 B 0 B total_objects 72908 total_used 745 GiB total_avail 87 TiB total_space 88 TiB 3. 设置 Harbor 配置清单 # 由于我们需要通过 Helm 安装 Harbor 仓库，需要提前创建 Harbor Chart 的配置清单文件，里面是对要创建的应用 Harbor 进行一系列参数配置，由于参数过多，关于都有 Harbor Chart 都能够配置哪些参数这里就不一一罗列，可以通过访问 Harbor-helm 的 Github 地址 进行了解。\n下面描述下，需要的一些配置参数：\nvalues.yaml\n#入口配置，我只在内网使用，所以直接使用 cluserIP expose: type: clusterIP tls: ### 是否启用 https 协议 enabled: true certSource: secret auto: # The common name used to generate the certificate, it\u0026#39;s necessary # when the type isn\u0026#39;t \u0026#34;ingress\u0026#34; commonName: \u0026#34;harbor.example.net\u0026#34; secret: # The name of secret which contains keys named: # \u0026#34;tls.crt\u0026#34; - the certificate # \u0026#34;tls.key\u0026#34; - the private key secretName: \u0026#34;harbor-tls\u0026#34; # The name of secret which contains keys named: # \u0026#34;tls.crt\u0026#34; - the certificate # \u0026#34;tls.key\u0026#34; - the private key # Only needed when the \u0026#34;expose.type\u0026#34; is \u0026#34;ingress\u0026#34;. notarySecretName: \u0026#34;\u0026#34; ## 如果Harbor部署在代理后，将其设置为代理的URL externalURL: https://harbor.example.net ### Harbor 各个组件的持久化配置，并将 storageClass 设置为集群默认的 storageClass persistence: enabled: true # Setting it to \u0026#34;keep\u0026#34; to avoid removing PVCs during a helm delete # operation. Leaving it empty will delete PVCs after the chart deleted # (this does not apply for PVCs that are created for internal database # and redis components, i.e. they are never deleted automatically) resourcePolicy: \u0026#34;keep\u0026#34; persistentVolumeClaim: registry: # Use the existing PVC which must be created manually before bound, # and specify the \u0026#34;subPath\u0026#34; if the PVC is shared with other components existingClaim: \u0026#34;\u0026#34; # Specify the \u0026#34;storageClass\u0026#34; used to provision the volume. Or the default # StorageClass will be used(the default). # Set it to \u0026#34;-\u0026#34; to disable dynamic provisioning storageClass: \u0026#34;csi-rbd-sc\u0026#34; subPath: \u0026#34;\u0026#34; accessMode: ReadWriteOnce size: 100Gi chartmuseum: existingClaim: \u0026#34;\u0026#34; storageClass: \u0026#34;csi-rbd-sc\u0026#34; subPath: \u0026#34;\u0026#34; accessMode: ReadWriteOnce size: 5Gi jobservice: existingClaim: \u0026#34;\u0026#34; storageClass: \u0026#34;csi-rbd-sc\u0026#34; subPath: \u0026#34;\u0026#34; accessMode: ReadWriteOnce size: 5Gi # If external database is used, the following settings for database will # be ignored database: existingClaim: \u0026#34;\u0026#34; storageClass: \u0026#34;csi-rbd-sc\u0026#34; subPath: \u0026#34;\u0026#34; accessMode: ReadWriteOnce size: 5Gi # If external Redis is used, the following settings for Redis will # be ignored redis: existingClaim: \u0026#34;\u0026#34; storageClass: \u0026#34;csi-rbd-sc\u0026#34; subPath: \u0026#34;\u0026#34; accessMode: ReadWriteOnce size: 5Gi trivy: existingClaim: \u0026#34;\u0026#34; storageClass: \u0026#34;csi-rbd-sc\u0026#34; subPath: \u0026#34;\u0026#34; accessMode: ReadWriteOnce size: 5Gi ### 默认用户名 admin 的密码配置，注意：密码中一定要包含大小写字母与数字 harborAdminPassword: \u0026#34;Mydlq123456\u0026#34; ### 设置日志级别 logLevel: info #各个组件 CPU \u0026amp; Memory 资源相关配置 nginx: resources: requests: memory: 256Mi cpu: 500m portal: resources: requests: memory: 256Mi cpu: 500m core: resources: requests: memory: 256Mi cpu: 1000m jobservice: resources: requests: memory: 256Mi cpu: 500m registry: registry: resources: requests: memory: 256Mi cpu: 500m controller: resources: requests: memory: 256Mi cpu: 500m clair: clair: resources: requests: memory: 256Mi cpu: 500m adapter: resources: requests: memory: 256Mi cpu: 500m notary: server: resources: requests: memory: 256Mi cpu: 500m signer: resources: requests: memory: 256Mi cpu: 500m database: internal: resources: requests: memory: 256Mi cpu: 500m redis: internal: resources: requests: memory: 256Mi cpu: 500m trivy: enabled: true resources: requests: cpu: 200m memory: 512Mi limits: cpu: 1000m memory: 1024Mi #开启 chartmuseum，使 Harbor 能够存储 Helm 的 chart chartmuseum: enabled: true resources: requests: memory: 256Mi cpu: 500m imageChartStorage: # Specify whether to disable `redirect` for images and chart storage, for # backends which not supported it (such as using minio for `s3` storage type), please disable # it. To disable redirects, simply set `disableredirect` to `true` instead. # Refer to # https://github.com/docker/distribution/blob/master/docs/configuration.md#redirect # for the detail. disableredirect: false # Specify the \u0026#34;caBundleSecretName\u0026#34; if the storage service uses a self-signed certificate. # The secret must contain keys named \u0026#34;ca.crt\u0026#34; which will be injected into the trust store # of registry\u0026#39;s and chartmuseum\u0026#39;s containers. # caBundleSecretName: # Specify the type of storage: \u0026#34;filesystem\u0026#34;, \u0026#34;azure\u0026#34;, \u0026#34;gcs\u0026#34;, \u0026#34;s3\u0026#34;, \u0026#34;swift\u0026#34;, # \u0026#34;oss\u0026#34; and fill the information needed in the corresponding section. The type # must be \u0026#34;filesystem\u0026#34; if you want to use persistent volumes for registry # and chartmuseum type: s3 s3: region: cn-hangzhou-1 bucket: harbor accesskey: VGZQY32LMFQOQPVNTDSJ secretkey: YZMMYqoy1ypHaqGOUfwLvdAj9A731iDYDjYqwkU5 regionendpoint: http://172.16.7.1 #encrypt: false #keyid: mykeyid secure: false #skipverify: false #v4auth: true #chunksize: \u0026#34;5242880\u0026#34; #rootdirectory: /s3/object/name/prefix #storageclass: STANDARD #multipartcopychunksize: \u0026#34;33554432\u0026#34; #multipartcopymaxconcurrency: 100 #multipartcopythresholdsize: \u0026#34;33554432\u0026#34; 4. 安装 Harbor # 添加 Helm 仓库 # 🐳 → helm repo add harbor https://helm.goharbor.io 部署 Harbor # 🐳 → helm install harbor harbor/harbor -f values.yaml -n harbor 查看应用是否部署完成 # 🐳 → kubectl -n harbor get pod NAME READY STATUS RESTARTS AGE harbor-harbor-chartmuseum-55fb975fbd-74vnh 1/1 Running 0 3m harbor-harbor-clair-695c7f9c69-7gpkh 2/2 Running 0 3m harbor-harbor-core-687cfb49b6-zmwxr 1/1 Running 0 3m harbor-harbor-database-0 1/1 Running 0 3m harbor-harbor-jobservice-88994b9b7-684vb 1/1 Running 0 3m harbor-harbor-nginx-6758559548-x9pq6 1/1 Running 0 3m harbor-harbor-notary-server-6d55b785f-6jsq9 1/1 Running 0 3m harbor-harbor-notary-signer-9696cbdd8-8tfw9 1/1 Running 0 3m harbor-harbor-portal-6f474574c4-8jzh2 1/1 Running 0 3m harbor-harbor-redis-0 1/1 Running 0 3m harbor-harbor-registry-5b6cbfb4cf-42fm9 2/2 Running 0 3m harbor-harbor-trivy-0 1/1 Running 0 3m Host 配置域名 # 接下来配置 Hosts，客户端想通过域名访问服务，必须要进行 DNS 解析，由于这里没有 DNS 服务器进行域名解析，所以修改 hosts 文件将 Harbor 指定 clusterIP 和自定义 host 绑定。首先查看 nginx 的 clusterIP：\n🐳 → kubectl -n harbor get svc harbor-harbor-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE harbor-harbor-nginx ClusterIP 10.109.50.142 \u0026lt;none\u0026gt; 80/TCP,443/TCP 22h 打开主机的 Hosts 配置文件，往其加入下面配置：\n10.109.50.142 harbor.example.net 如果想在集群外访问，建议将 Service nginx 的 type 改为 nodePort 或者通过 ingress 来代理。当然，如果你在集群外能够直接访问 clusterIP，那更好。\n输入地址 https://harbor.example.net 访问 Harbor 仓库。\n用户：admin 密码：Mydlq123456 (在安装配置中自定义的密码) 进入后可以看到 Harbor 的管理后台：\n5. 服务器配置镜像仓库 # 对于 Containerd 来说，不能像 docker 一样 docker login 登录到镜像仓库，需要修改其配置文件来进行认证。/etc/containerd/config.toml 需要添加如下内容：\n[plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors] ... [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs.\u0026#34;harbor.example.net\u0026#34;.tls] insecure_skip_verify = true [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs.\u0026#34;harbor.example.net\u0026#34;.auth] username = \u0026#34;admin\u0026#34; password = \u0026#34;Mydlq123456\u0026#34; 由于 Harbor 是基于 Https 的，理论上需要提前配置 tls 证书，但可以通过 insecure_skip_verify 选项跳过证书认证。\n当然，如果你想通过 Kubernetes 的 secret 来进行用户验证，配置还可以精简下：\n[plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors] ... [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs.\u0026#34;harbor.example.net\u0026#34;.tls] insecure_skip_verify = true Kubernetes 集群使用 docker-registry 类型的 Secret 来通过镜像仓库的身份验证，进而拉取私有映像。所以需要创建 Secret，命名为 regcred：\n🐳 → kubectl create secret docker-registry regcred \\ --docker-server=\u0026lt;你的镜像仓库服务器\u0026gt; \\ --docker-username=\u0026lt;你的用户名\u0026gt; \\ --docker-password=\u0026lt;你的密码\u0026gt; \\ --docker-email=\u0026lt;你的邮箱地址\u0026gt; 然后就可以在 Pod 中使用该 secret 来访问私有镜像仓库了，下面是一个示例 Pod 配置文件：\napiVersion: v1 kind: Pod metadata: name: private-reg spec: containers: - name: private-reg-container image: \u0026lt;your-private-image\u0026gt; imagePullSecrets: - name: regcred 如果你不嫌麻烦，想更安全一点，那就老老实实将 CA、证书和秘钥拷贝到所有节点的 /etc/ssl/certs/ 目录下。/etc/containerd/config.toml 需要添加的内容更多一点：\n[plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors] ... [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.configs.\u0026#34;harbor.example.net\u0026#34;.tls] ca_file = \u0026#34;/etc/ssl/certs/ca.pem\u0026#34; cert_file = \u0026#34;/etc/ssl/certs/harbor.pem\u0026#34; key_file = \u0026#34;/etc/ssl/certs/harbor-key.pem\u0026#34; 至于 Docker 的配置方式，大家可以自己去搜一下，这里就跳过了，谁让它现在不受待见呢。\n6. 测试功能 # 这里为了测试推送镜像，先下载一个用于测试的 helloworld 小镜像，然后推送到 harbor.example.net 仓库：\n### 拉取 Helloworld 镜像 🐳 → ctr i pull bxsfpjcb.mirror.aliyuncs.com/library/hello-world:latest bxsfpjcb.mirror.aliyuncs.com/library/hello-world:latest: resolved |++++++++++++++++++++++++++++++++++++++| index-sha256:1a523af650137b8accdaed439c17d684df61ee4d74feac151b5b337bd29e7eec: done |++++++++++++++++++++++++++++++++++++++| manifest-sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:0e03bdcc26d7a9a57ef3b6f1bf1a210cff6239bff7c8cac72435984032851689: done |++++++++++++++++++++++++++++++++++++++| config-sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b: done |++++++++++++++++++++++++++++++++++++++| elapsed: 15.8s total: 2.6 Ki (166.0 B/s) unpacking linux/amd64 sha256:1a523af650137b8accdaed439c17d684df61ee4d74feac151b5b337bd29e7eec... done ### 将下载的镜像使用 tag 命令改变镜像名 🐳 → ctr i tag bxsfpjcb.mirror.aliyuncs.com/library/hello-world:latest harbor.example.net/library/hello-world:latest harbor.example.net/library/hello-world:latest ### 推送镜像到镜像仓库 🐳 → ctr i push --user admin:Mydlq123456 --platform linux/amd64 harbor.example.net/library/hello-world:latest manifest-sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042: done |++++++++++++++++++++++++++++++++++++++| config-sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:0e03bdcc26d7a9a57ef3b6f1bf1a210cff6239bff7c8cac72435984032851689: done |++++++++++++++++++++++++++++++++++++++| elapsed: 2.2 s total: 4.5 Ki (2.0 KiB/s) 镜像仓库中也能看到：\n将之前的下载的镜像删除，然后测试从 harbor.example.net 下载镜像进行测试：\n### 删除之前镜像 🐳 → ctr i rm harbor.example.net/library/hello-world:latest 🐳 → ctr i rm bxsfpjcb.mirror.aliyuncs.com/library/hello-world:latest ### 测试从 harbor.example.net 下载新镜像 🐳 → ctr i pull harbor.example.net/library/hello-world:latest harbor.example.net/library/hello-world:latest: resolved |++++++++++++++++++++++++++++++++++++++| manifest-sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:0e03bdcc26d7a9a57ef3b6f1bf1a210cff6239bff7c8cac72435984032851689: done |++++++++++++++++++++++++++++++++++++++| config-sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b: done |++++++++++++++++++++++++++++++++++++++| elapsed: 0.6 s total: 525.0 (874.0 B/s) unpacking linux/amd64 sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042... done 参考 # 通过 Helm 搭建 Docker 镜像仓库 Harbor ","date":"2020年12月30日","externalUrl":null,"permalink":"/posts/install-harbor-on-kubernetes/","section":"博客","summary":"系统环境： kubernetes 版本：1.18.10 Harbor Chart 版本：1.5.2 Harbor 版本：","title":"在 Kubernetes 中部署高可用 Harbor 镜像仓库","type":"posts"},{"content":" 原文链接： Why not WireGuard\n最近有一款新型 VPN 工具备受瞩目，相信很多人已经听说过了，没错就是 WireGuard，传言它有望取代 IPSec 和 OpenVPN。那么 WireGuard 是否真的有传说中的那么神奇？今天我就来一一解读。\n这是一篇非常长的文章，我建议你先去冲杯咖啡，然后边喝咖啡边看。\n首先要声明：我并没有诋毁 WireGuard 的意思，WireGuard 很棒很优秀，但总是有某些无脑媒体天天说 WireGuard 即将取代 IPSec 和 OpenVPN，这我就不能忍了，今天就来和你们好好掰扯掰扯 WireGuard。\nWireGuard 白皮书 # 本文所有的观点都是针对 Jason Donenfeld 撰写的 WireGuard 白皮书，其他的博客和文档不在我的讨论范围之内。白皮书的第一句话是这么说的：\nWireGuard 的目标是在大多数场景下取代 IPsec 和其他基于用户空间和 TLS 的 VPN（例如 OpenVPN），与其他 VPN 相比，它更简单、更高效、更容易使用。\n可以看到，WireGuard 最大的卖点就是简单，大多数新技术也都是这个营销套路。当然，作为一款 VPN 产品，它还有性能和安全这两个卖点。\n有趣的是，作为 VPN，如果你不简单、不安全、性能不好，那你可能就没有机会了。这不止是你 WireGuard 的设计目标，其他的 VPN 产品也是这么干的好吗？\n最有趣的部分是 “在大多数场景下” 这几个字，媒体报道时直接将其删除了，混淆大众的视听。\nWireGuard 能否取代 IPSec？ # 不！Cisco 和 Juniper 等大厂不可能使用 WireGuard，除非迫不得已，他们是不会上 WireGuard 的车的。后面我会详细解释为什么即使他们想卖 WireGuard 服务也卖不出去。\nWireGuard 实现了 Road Warrior？ # 当然没有。Road Warrior 是具有动态分配 IP 地址的移动客户端，比如笔记本电脑。你可以直接理解为漫游。WireGuard 目前不能使用动态 IP 来建立连接，要想实现漫游功能，还有很长的路要走。\nWireGuard 有一个子项目叫 wg-dynamic，它增加了一个用户空间守护程序来使 WireGuard 支持动态 IP。然而这个项目最后一次更新是在 2019 年，不知道还维护不维护了。。。\n大家都知道，IPv6 就是动态寻址的，如果将来全面进入 IPv6 的世界，WireGuard 还怎么用？从商业角度来看，这是相当令人失望的。\nWireGuard 的设计目标之一是保持协议的精简，现在看来是精简过头了，以至于需要更多的辅助软件才能使它发挥强大的功效。\nWireGuard 真的好用吗？ # 并没有。我并没有说 WireGuard 最终不能替代其他 VPN 产品，我只是说目前 WireGuard 还不行，如果它的目标和我们理解的一样，目前它还只是处在 Alpha 阶段。\nWireGuard 到底想解决什么问题？IPsec 真的很难用吗？\n恐怕不是这样，如果厂商做了正确的功课，并提供了易于使用的界面（比如， IPFire），就不会难用。\n要想建立一个 IPSec 隧道，只需要输入 5 组信息：你的公网地址、peer 的公网地址、子网、你的预共享秘钥和 peer 的预共享秘钥。这样看来，几分钟就可以建立一个隧道，而且每个厂商之间都是兼容的。\n当然，也有一些例外，比如与 OpenBSD 系统之间建立隧道，过程可能会比较痛苦。\n协议复杂度真的很重要吗？ # 作为终端用户，其实无需考虑协议的复杂度。\n如果复杂度真的影响很大，我们肯定早就摆脱 SIP、 H.323 和 FTP 等不能很好地应对NAT的协议了，然而并没有。讲道理，IPsec 比 WireGuard 更复杂是有原因的：它能做的事情太多了。比如，使用用户名/密码或带有 EAP 的 SIM 卡进行用户认证；也可以扩展新的加密方式。\n而 WireGuard 呢？这些功能都没有。这就意味着当某一天它的固定的那些加密方式被破解或削弱了之后，就彻底崩盘了。\nWireGuard 作者说过：\nWireGuard 在加密方式上比较偏执，故意砍掉了加密协议的敏捷性，不支持扩展加密协议，因为这么做会大大降低软件的复杂度。如果底层的加密协议出现了漏洞，只能更新所有端点来修复漏洞。\n我非常同意他这句话里阐述的观点，因为协商使用何种方式来加密会使 IKE 和 TLS 等协议变得更复杂。那么，是我们想不开刻意要搞这么复杂吗？当然不是啊，即使这样，在握手过程中也会经常发现各种漏洞，不复杂能行吗？除此以外，别无他法。\n如何更新密码？ # 想象一下，你有一个 VPN 服务器，为 200 多个 Road Warrior 客户端提供服务，而且这些客户端分布在世界各地。假设现在你改了密码，那么就需要同时更新所有客户端的密码才能正常工作，这简直就是不可能的事情，作为管理员，你可能会需要几个月的时间来下方更改后的配置。\nIPsec 和 OpenVPN 就没有这些烦恼，它们都有秘钥协商功能，可以将新秘钥逐步更新到所有客户端，在漫长的更新过程中，仍在使用旧秘钥的客户端仍然有效，直到所有客户端更新完毕后，才会弃用旧秘钥。整个过程中客户端不会有任何察觉，也不需要重启。\n加密方式 # WireGuard 使用以下加密技术来保障数据的安全：\n使用 ChaCha20 进行对称加密，使用 Poly1305 进行数据验证。 利用 Curve25519 进行密钥交换。 使用 BLAKE2 作为哈希函数。 使用 HKDF 进行解密。 而 IPSec 和 OpenVPN 使用的都是标准的 ChaCha20-Poly1305 加密算法。\nBLAKE2算法是 BLAKE 算法的升级版，而 BLAKE 是 SHA-3 的入围者，与 SHA-2 非常相似，所以没有获奖。如果哪天 SHA-2 被破解了，BLAKE 也很有可能被破解。\nIPSec 和 OpenVPN 使用的加密方式和 WireGuard 是类似的，比如对称加密使用的是标准的 ChaCha20-Poly1305 算法。唯一没有用到的就是 BLAKE2，因为它目前没有列入标准。即使不用 BLAKE2，也没什么大不了的，因为 VPN 是使用 HMACs 来保障数据的完整性，即使使用 MD5 也仍然没问题。\n我的结论是：实际上所有的 VPN 都可以使用相同的加密技术，WireGuard 在加密或数据完整性方面并没有比其他的 VPN 更安全或更不安全。\n然而白皮书上说了，加密不是重点，速度才是。\n好吧，那我们就来看看速度是不是真的有那么快。\nWireGuard 真的很快吗？ # 答案是否定的。\nChaCha20 是一种流加密算法，一次只加密一个比特，使用软件更容易实现。而像 AES 这样的分块加密方式，会将明文分成多个等长的模块，每次加密 128 位的模块。这种加密方式在硬件中实现时需要更多的晶体管，所以大型处理器都带有一个指令集扩展 \u0026ndash; AES-NI，它可以提高加密和解密的速度。\n今天你能买到的任何一款智能手机都带有 AES 的硬件加速功能，在这些硬件中使用 AES 会比 ChaCha20 加密解密更快、更节能。几乎所有的个人 PC 和服务器的 CPU 都带有 AES-NI，加密解密速度就更不用说了。因此，我预计 AES 在几乎所有场景下表现都会优于 ChaCha20。\n然而，WireGuard 的白皮书又说了，ChaCha20-Poly1305 的性能优于 AES-NI，但该指令集只适用于大型处理器，对普通 PC 和移动硬件没有任何帮助，所以并没有什么卵用。\nWireGuard 执着于一种加密算法，我觉得不好。而 IPSec 允许你选择不同的加密算法，这样就可以根据不同的使用场景选择最合适的加密算法，例如， 传输 10G 或更多的数据。\n既然 WireGuard 选择了更现代的加密方式，就会带来很多问题。比如，由于 Linux 内核中缺乏支持这些加密方式的模块，导致 WireGuard 并没有使用 Linux 内核提供的模块，要推迟好几年才能用上 Linux 内核提供的模块。我不知道其他操作系统是什么情况，但可能也没有什么不同。\n理想与现实 # 假设 WireGuard 真的很完美，大厂就一定会用吗？\n现实情况是，每次当客户要求我帮他们搭建 VPN 时，给到他们手里的证书都是使用旧的加密方式，通常是 3DES 和 MD5 结合，或者 AES-256 和 SHA1 结合。至于秘钥交换，我们一直在使用 RSA，虽然速度很慢，但足够安全。\n大部分客户都和政府机构或巨头公司有关，他们在我们这里的订单表还是几十年前的，根本就没有添加过 SHA-512 的选项。所以阻止创新的不一定是技术，而是缓慢的企业流程。\n看到这种情况，我也很痛心？我就不想使用新技术吗？当然想啊。IPsec 从 2005 年左右就开始支持椭圆曲线加密算法了，Curve25519 算法现在也支持了，也有了 AES 的替代方案（比如 Camellia 和 ChaCha20），但很显然并不是所有的大厂都愿意去适配，比如思科等。思科是这个领域的市场领导者，他们对推动创新其实并不感兴趣。\n基准测试 # 白皮书中还提到了 WIreGuard 的基准测试，虽然这不是一篇科学论文，但我仍然希望以科学的方法来进行基准测试。如果测试不能重复，那么它就毫无价值；如果测试没有考虑实际场景，也毫无价值。\nWireGuard 的 Linux 版本使用 GSO（Generic Segmentation Offloading，通用分段卸载）来创建一个 64k 字节的巨大数据包，并一次性对其进行加密或解密，以此来获得速度优势。这样一来，初始化和调用加密操作的开销就被节省了。如果你想最大限度地提高吞吐量，这倒是一个好主意。\n然而现实不是这样的，你想向网络适配器发送如此大的数据包，就需要将其切割成许多小数据包，通常为 1500 字节。对于 64k 字节的超大数据包来说，会被切割成 45 个数据包（每个数据包有 1480 字节的有效载荷和 20 字节的 IP 头），这些数据包将会阻塞网络适配器相当长的时间，因为它们想要被一次性发送出去。像 VoIP 呼叫这样应该优先处理的数据包也不得不慢慢等着。\n因此，WireGuard 宣称的高吞吐量是通过让其他应用变慢来获得的，官方团队已经承认了这一点。\n我们再来看看基准测试的最终数据，吞吐量为 1011 MBit/s！\n这个数据令人印象深刻，我至今仍感到疑惑，在数据包大小为 1500 字节的情况下，一个千兆以太网链路的最大理论吞吐量为 966 MBit/s，减去 20 个字节的 IP 头、8 个字节的 UDP 头和 16 个字节的 WireGuard 头，再减去封装数据包中的另一个 IP 头和另一个 20 个字节的 TCP 头，额外的带宽到底从哪来的？\nOK，假设启用了巨型帧和 GSO，9000 字节帧大小时的理论最大值将是 1014 MBit/s。如果使用更大的巨型帧，理论最大值为 1023 MBit/s。然而这在现实中是绝对不实用的，因为开销太大了，而且只能在服务器直连的情况下使用。通常 VPN 都是通过互联网连接的，完全不支持巨型帧，所以这样的基准测试根本不切实际，永远不可能在现实世界中使用。\n最终幻想 # WireGuard 的官方网站写了很多关于容器的内容，很明显这应该就是 WireGuard 的目的。\n通过一个简单迅速的 VPN 来实现容器通信的 CNI，可以通过 Kubernetes 等大型容器编排工具来快速部署，针对吞吐量和大于 9000 字节的数据包进行了优化，可以快速分发容器镜像，等等。\n这一切的种种都好像是为容器而设计的，不得不承认超精简、超优雅、超快速。\n但是，它根本不是为数据中心以外的世界而设计的，在外面的世界，你必须要在协议的设计和实现上做出妥协。\n总结 # 我最终的结论是：WireGuard 还没有准备好。\n它是作为一个轻量级和快速的解决方案来起草的，以解决现有解决方案中的一些问题，但不幸的是，它牺牲了许多与用户相关的功能，因此无法取代 IPsec 和 OpenVPN。\n你至少得有动态地址分配和推送路由等配置的功能吧？这些功能都是需要进行秘钥协商的。\n此外，安全性是重中之重，目前我还没发现 IKE 或者 TLS 有啥明显的缺陷，它们都支持现代加密方式，而且都经过了几十年的审计。不能仅仅因为某些东西是新的，就觉得它是好的。\n加密方式总会过时，押注在一种加密方式身上，当这种加密方式不再安全时，你该何去何从？\n","date":"2020年12月24日","externalUrl":null,"permalink":"/posts/why-not-wireguard/","section":"博客","summary":"原文链接： Why not WireGuard 最近有一款新型 VPN 工具备受瞩目，相信很多人已经","title":"我为什么不鼓吹 WireGuard","type":"posts"},{"content":" 1. Containerd 的前世今生 # 很久以前，Docker 强势崛起，以“镜像”这个大招席卷全球，对其他容器技术进行致命的降维打击，使其毫无招架之力，就连 Google 也不例外。Google 为了不被拍死在沙滩上，被迫拉下脸面（当然，跪舔是不可能的），希望 Docker 公司和自己联合推进一个开源的容器运行时作为 Docker 的核心依赖，不然就走着瞧。Docker 公司觉得自己的智商被侮辱了，走着瞧就走着瞧，谁怕谁啊！\n很明显，Docker 公司的这个决策断送了自己的大好前程，造成了今天的悲剧。\n紧接着，Google 联合 Red Hat、IBM 等几位巨佬连哄带骗忽悠 Docker 公司将 libcontainer 捐给中立的社区（OCI，Open Container Intiative），并改名为 runc，不留一点 Docker 公司的痕迹~~\n这还不够，为了彻底扭转 Docker 一家独大的局面，几位大佬又合伙成立了一个基金会叫 CNCF（Cloud Native Computing Fundation），这个名字想必大家都很熟了，我就不详细介绍了。CNCF 的目标很明确，既然在当前的维度上干不过 Docker，干脆往上爬，升级到大规模容器编排的维度，以此来击败 Docker。\nDocker 公司当然不甘示弱，搬出了 Swarm 和 Kubernetes 进行 PK，最后的结局大家都知道了，Swarm 战败。然后 Docker 公司耍了个小聪明，将自己的核心依赖 Containerd 捐给了 CNCF，以此来标榜 Docker 是一个 PaaS 平台。\n很明显，这个小聪明又大大加速了自己的灭亡。\n巨佬们心想，想当初想和你合作搞个中立的核心运行时，你死要面子活受罪，就是不同意，好家伙，现在自己搞了一个，还捐出来了，这是什么操作？也罢，这倒省事了，我就直接拿 Containerd 来做文章吧。\n首先呢，为了表示 Kubernetes 的中立性，当然要搞个标准化的容器运行时接口，只要适配了这个接口的容器运行时，都可以和我一起玩耍哦，第一个支持这个接口的当然就是 Containerd 啦。至于这个接口的名字，大家应该都知道了，它叫 CRI（Container Runntime Interface）。\n这样还不行，为了蛊惑 Docker 公司，Kubernetes 暂时先委屈自己，专门在自己的组件中集成了一个 shim（你可以理解为垫片），用来将 CRI 的调用翻译成 Docker 的 API，让 Docker 也能和自己愉快地玩耍，温水煮青蛙，养肥了再杀。。。\n就这样，Kubernetes 一边假装和 Docker 愉快玩耍，一边背地里不断优化 Containerd 的健壮性以及和 CRI 对接的丝滑性。现在 Containerd 的翅膀已经完全硬了，是时候卸下我的伪装，和 Docker say bye bye 了。后面的事情大家也都知道了~~\nDocker 这门技术成功了，Docker 这个公司却失败了。\n2. Containerd 架构 # 时至今日，Containerd 已经变成一个工业级的容器运行时了，连口号都有了：超简单！超健壮！可移植性超强！\n当然，为了让 Docker 以为自己不会抢饭碗，Containerd 声称自己的设计目的主要是为了嵌入到一个更大的系统中（暗指 Kubernetes），而不是直接由开发人员或终端用户使用。\n事实上呢，Containerd 现在基本上啥都能干了，开发人员或者终端用户可以在宿主机中管理完整的容器生命周期，包括容器镜像的传输和存储、容器的执行和管理、存储和网络等。大家可以考虑学起来了。\n学习 Containerd 最好的时机是关注公众号 云原生实验室 后，其次是现在，看完了再关注公众号也不迟😆。\n先来看看 Containerd 的架构：\n可以看到 Containerd 仍然采用标准的 C/S 架构，服务端通过 GRPC 协议提供稳定的 API，客户端通过调用服务端的 API 进行高级的操作。\n为了解耦，Containerd 将不同的职责划分给不同的组件，每个组件就相当于一个子系统（subsystem）。连接不同子系统的组件被称为模块。\n总体上 Containerd 被划分为两个子系统：\nBundle : 在 Containerd 中，Bundle 包含了配置、元数据和根文件系统数据，你可以理解为容器的文件系统。而 Bundle 子系统允许用户从镜像中提取和打包 Bundles。 Runtime : Runtime 子系统用来执行 Bundles，比如创建容器。 其中，每一个子系统的行为都由一个或多个模块协作完成（架构图中的 Core 部分）。每一种类型的模块都以插件的形式集成到 Containerd 中，而且插件之间是相互依赖的。例如，上图中的每一个长虚线的方框都表示一种类型的插件，包括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等，其中 Service Plugin 又会依赖 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一个小方框都表示一个细分的插件，例如 Metadata Plugin 依赖 Containers Plugin、Content Plugin 等。 总之，万物皆插件，插件就是模块，模块就是插件。\n这里介绍几个常用的插件：\nContent Plugin : 提供对镜像中可寻址内容的访问，所有不可变的内容都被存储在这里。 Snapshot Plugin : 用来管理容器镜像的文件系统快照。镜像中的每一个 layer 都会被解压成文件系统快照，类似于 Docker 中的 graphdriver。 Metrics : 暴露各个组件的监控指标。 从总体来看，Containerd 被分为三个大块：Storage、Metadata 和 Runtime，可以将上面的架构图提炼一下：\n这是使用 bucketbench 对 Docker、crio 和 Containerd 的性能测试结果，包括启动、停止和删除容器，以比较它们所耗的时间：\n可以看到 Containerd 在各个方面都表现良好，总体性能还是优越于 Docker 和 crio 的。\n3. Containerd 安装 # 了解了 Containerd 的概念后，就可以动手安装体验一把了。本文的演示环境为 Ubuntu 18.04。\n安装依赖 # 为 seccomp 安装依赖：\n🐳 → sudo apt-get update 🐳 → sudo apt-get install libseccomp2 下载并解压 Containerd 程序 # Containerd 提供了两个压缩包，一个叫 containerd-${VERSION}.${OS}-${ARCH}.tar.gz，另一个叫 cri-containerd-${VERSION}.${OS}-${ARCH}.tar.gz。其中 cri-containerd-${VERSION}.${OS}-${ARCH}.tar.gz 包含了所有 Kubernetes 需要的二进制文件。如果你只是本地测试，可以选择前一个压缩包；如果是作为 Kubernetes 的容器运行时，需要选择后一个压缩包。\nContainerd 是需要调用 runc 的，而第一个压缩包是不包含 runc 二进制文件的，如果你选择第一个压缩包，还需要提前安装 runc。所以我建议直接使用 cri-containerd 压缩包。\n首先从 release 页面下载最新版本的压缩包，当前最新版本为 1.4.3：\n🐳 → wget https://github.com/containerd/containerd/releases/download/v1.4.3/cri-containerd-cni-1.4.3-linux-amd64.tar.gz # 也可以替换成下面的 URL 加速下载 🐳 → wget https://download.fastgit.org/containerd/containerd/releases/download/v1.4.3/cri-containerd-cni-1.4.3-linux-amd64.tar.gz 可以通过 tar 的 -t 选项直接看到压缩包中包含哪些文件：\n🐳 → tar -tf cri-containerd-cni-1.4.3-linux-amd64.tar.gz etc/ etc/cni/ etc/cni/net.d/ etc/cni/net.d/10-containerd-net.conflist etc/crictl.yaml etc/systemd/ etc/systemd/system/ etc/systemd/system/containerd.service usr/ usr/local/ usr/local/bin/ usr/local/bin/containerd-shim-runc-v2 usr/local/bin/ctr usr/local/bin/containerd-shim usr/local/bin/containerd-shim-runc-v1 usr/local/bin/crictl usr/local/bin/critest usr/local/bin/containerd usr/local/sbin/ usr/local/sbin/runc opt/ opt/cni/ opt/cni/bin/ opt/cni/bin/vlan opt/cni/bin/host-local opt/cni/bin/flannel opt/cni/bin/bridge opt/cni/bin/host-device opt/cni/bin/tuning opt/cni/bin/firewall opt/cni/bin/bandwidth opt/cni/bin/ipvlan opt/cni/bin/sbr opt/cni/bin/dhcp opt/cni/bin/portmap opt/cni/bin/ptp opt/cni/bin/static opt/cni/bin/macvlan opt/cni/bin/loopback opt/containerd/ opt/containerd/cluster/ opt/containerd/cluster/version opt/containerd/cluster/gce/ opt/containerd/cluster/gce/cni.template opt/containerd/cluster/gce/configure.sh opt/containerd/cluster/gce/cloud-init/ opt/containerd/cluster/gce/cloud-init/master.yaml opt/containerd/cluster/gce/cloud-init/node.yaml opt/containerd/cluster/gce/env 直接将压缩包解压到系统的各个目录中：\n🐳 → sudo tar -C / -xzf cri-containerd-cni-1.4.3-linux-amd64.tar.gz 将 /usr/local/bin 和 /usr/local/sbin 追加到 ~/.bashrc 文件的 $PATH 环境变量中：\nexport PATH=$PATH:/usr/local/bin:/usr/local/sbin 立即生效：\n🐳 → source ~/.bashrc 查看版本：\n🐳 → ctr version Client: Version: v1.4.3 Revision: 269548fa27e0089a8b8278fc4fc781d7f65a939b Go version: go1.15.5 Server: Version: v1.4.3 Revision: 269548fa27e0089a8b8278fc4fc781d7f65a939b UUID: d1724999-91b3-4338-9288-9a54c9d52f70 生成配置文件 # Containerd 的默认配置文件为 /etc/containerd/config.toml，我们可以通过命令来生成一个默认的配置：\n🐳 → mkdir /etc/containerd 🐳 → containerd config default \u0026gt; /etc/containerd/config.toml 镜像加速 # 由于某些不可描述的因素，在国内拉取公共镜像仓库的速度是极慢的，为了节约拉取时间，需要为 Containerd 配置镜像仓库的 mirror。Containerd 的镜像仓库 mirror 与 Docker 相比有两个区别：\nContainerd 只支持通过 CRI 拉取镜像的 mirror，也就是说，只有通过 crictl 或者 Kubernetes 调用时 mirror 才会生效，通过 ctr 拉取是不会生效的。 Docker 只支持为 Docker Hub 配置 mirror，而 Containerd 支持为任意镜像仓库配置 mirror。 配置镜像加速之前，先来看下 Containerd 的配置结构，乍一看可能会觉得很复杂，复杂就复杂在 plugin 的配置部分：\n[plugins] [plugins.\u0026#34;io.containerd.gc.v1.scheduler\u0026#34;] pause_threshold = 0.02 deletion_threshold = 0 mutation_threshold = 100 schedule_delay = \u0026#34;0s\u0026#34; startup_delay = \u0026#34;100ms\u0026#34; [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;] disable_tcp_service = true stream_server_address = \u0026#34;127.0.0.1\u0026#34; stream_server_port = \u0026#34;0\u0026#34; stream_idle_timeout = \u0026#34;4h0m0s\u0026#34; enable_selinux = false sandbox_image = \u0026#34;k8s.gcr.io/pause:3.1\u0026#34; stats_collect_period = 10 systemd_cgroup = false enable_tls_streaming = false max_container_log_line_size = 16384 disable_cgroup = false disable_apparmor = false restrict_oom_score_adj = false max_concurrent_downloads = 3 disable_proc_mount = false [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd] snapshotter = \u0026#34;overlayfs\u0026#34; default_runtime_name = \u0026#34;runc\u0026#34; no_pivot = false [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.default_runtime] runtime_type = \u0026#34;\u0026#34; runtime_engine = \u0026#34;\u0026#34; runtime_root = \u0026#34;\u0026#34; privileged_without_host_devices = false [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.untrusted_workload_runtime] runtime_type = \u0026#34;\u0026#34; runtime_engine = \u0026#34;\u0026#34; runtime_root = \u0026#34;\u0026#34; privileged_without_host_devices = false [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.runtimes] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.runtimes.runc] runtime_type = \u0026#34;io.containerd.runc.v1\u0026#34; runtime_engine = \u0026#34;\u0026#34; runtime_root = \u0026#34;\u0026#34; privileged_without_host_devices = false [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.cni] bin_dir = \u0026#34;/opt/cni/bin\u0026#34; conf_dir = \u0026#34;/etc/cni/net.d\u0026#34; max_conf_num = 1 conf_template = \u0026#34;\u0026#34; [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;docker.io\u0026#34;] endpoint = [\u0026#34;https://registry-1.docker.io\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.x509_key_pair_streaming] tls_cert_file = \u0026#34;\u0026#34; tls_key_file = \u0026#34;\u0026#34; [plugins.\u0026#34;io.containerd.internal.v1.opt\u0026#34;] path = \u0026#34;/opt/containerd\u0026#34; [plugins.\u0026#34;io.containerd.internal.v1.restart\u0026#34;] interval = \u0026#34;10s\u0026#34; [plugins.\u0026#34;io.containerd.metadata.v1.bolt\u0026#34;] content_sharing_policy = \u0026#34;shared\u0026#34; [plugins.\u0026#34;io.containerd.monitor.v1.cgroups\u0026#34;] no_prometheus = false [plugins.\u0026#34;io.containerd.runtime.v1.linux\u0026#34;] shim = \u0026#34;containerd-shim\u0026#34; runtime = \u0026#34;runc\u0026#34; runtime_root = \u0026#34;\u0026#34; no_shim = false shim_debug = false [plugins.\u0026#34;io.containerd.runtime.v2.task\u0026#34;] platforms = [\u0026#34;linux/amd64\u0026#34;] [plugins.\u0026#34;io.containerd.service.v1.diff-service\u0026#34;] default = [\u0026#34;walking\u0026#34;] [plugins.\u0026#34;io.containerd.snapshotter.v1.devmapper\u0026#34;] root_path = \u0026#34;\u0026#34; pool_name = \u0026#34;\u0026#34; base_image_size = \u0026#34;\u0026#34; 每一个顶级配置块的命名都是 plugins.\u0026quot;io.containerd.xxx.vx.xxx\u0026quot; 这种形式，其实每一个顶级配置块都代表一个插件，其中 io.containerd.xxx.vx 表示插件的类型，vx 后面的 xxx 表示插件的 ID。可以通过 ctr 一览无余：\n🐳 → ctr plugin ls TYPE ID PLATFORMS STATUS io.containerd.content.v1 content - ok io.containerd.snapshotter.v1 btrfs linux/amd64 error io.containerd.snapshotter.v1 devmapper linux/amd64 error io.containerd.snapshotter.v1 aufs linux/amd64 ok io.containerd.snapshotter.v1 native linux/amd64 ok io.containerd.snapshotter.v1 overlayfs linux/amd64 ok io.containerd.snapshotter.v1 zfs linux/amd64 error io.containerd.metadata.v1 bolt - ok io.containerd.differ.v1 walking linux/amd64 ok io.containerd.gc.v1 scheduler - ok io.containerd.service.v1 containers-service - ok io.containerd.service.v1 content-service - ok io.containerd.service.v1 diff-service - ok io.containerd.service.v1 images-service - ok io.containerd.service.v1 leases-service - ok io.containerd.service.v1 namespaces-service - ok io.containerd.service.v1 snapshots-service - ok io.containerd.runtime.v1 linux linux/amd64 ok io.containerd.runtime.v2 task linux/amd64 ok io.containerd.monitor.v1 cgroups linux/amd64 ok io.containerd.service.v1 tasks-service - ok io.containerd.internal.v1 restart - ok io.containerd.grpc.v1 containers - ok io.containerd.grpc.v1 content - ok io.containerd.grpc.v1 diff - ok io.containerd.grpc.v1 events - ok io.containerd.grpc.v1 healthcheck - ok io.containerd.grpc.v1 images - ok io.containerd.grpc.v1 leases - ok io.containerd.grpc.v1 namespaces - ok io.containerd.internal.v1 opt - ok io.containerd.grpc.v1 snapshots - ok io.containerd.grpc.v1 tasks - ok io.containerd.grpc.v1 version - ok io.containerd.grpc.v1 cri linux/amd64 ok 顶级配置块下面的子配置块表示该插件的各种配置，比如 cri 插件下面就分为 containerd、cni 和 registry 的配置，而 containerd 下面又可以配置各种 runtime，还可以配置默认的 runtime。\n镜像加速的配置就在 cri 插件配置块下面的 registry 配置块，所以需要修改的部分如下：\n[plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;docker.io\u0026#34;] endpoint = [\u0026#34;https://dockerhub.mirrors.nwafu.edu.cn\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;k8s.gcr.io\u0026#34;] endpoint = [\u0026#34;https://registry.aliyuncs.com/k8sxio\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;gcr.io\u0026#34;] endpoint = [\u0026#34;xxx\u0026#34;] registry.mirrors.\u0026ldquo;xxx\u0026rdquo; : 表示需要配置 mirror 的镜像仓库。例如，registry.mirrors.\u0026quot;docker.io\u0026quot; 表示配置 docker.io 的 mirror。 endpoint : 表示提供 mirror 的镜像加速服务。例如，这里推荐使用西北农林科技大学提供的镜像加速服务作为 docker.io 的 mirror。 至于 gcr.io，目前还没有公共的加速服务。我自己掏钱搭了个加速服务，拉取速度大概是 3M/s 左右，有加速需求的同学可以通过微信号：cloud-native-yang 加我为好友再详细咨询。\n存储配置 # Containerd 有两个不同的存储路径，一个用来保存持久化数据，一个用来保存运行时状态。\nroot = \u0026#34;/var/lib/containerd\u0026#34; state = \u0026#34;/run/containerd\u0026#34; root用来保存持久化数据，包括 Snapshots, Content, Metadata 以及各种插件的数据。每一个插件都有自己单独的目录，Containerd 本身不存储任何数据，它的所有功能都来自于已加载的插件，真是太机智了。\n🐳 → tree -L 2 /var/lib/containerd/ /var/lib/containerd/ ├── io.containerd.content.v1.content │ ├── blobs │ └── ingest ├── io.containerd.grpc.v1.cri │ ├── containers │ └── sandboxes ├── io.containerd.metadata.v1.bolt │ └── meta.db ├── io.containerd.runtime.v1.linux │ └── k8s.io ├── io.containerd.runtime.v2.task ├── io.containerd.snapshotter.v1.aufs │ └── snapshots ├── io.containerd.snapshotter.v1.btrfs ├── io.containerd.snapshotter.v1.native │ └── snapshots ├── io.containerd.snapshotter.v1.overlayfs │ ├── metadata.db │ └── snapshots └── tmpmounts 18 directories, 2 files state 用来保存临时数据，包括 sockets、pid、挂载点、运行时状态以及不需要持久化保存的插件数据。\n🐳 → tree -L 2 /run/containerd/ /run/containerd/ ├── containerd.sock ├── containerd.sock.ttrpc ├── io.containerd.grpc.v1.cri │ ├── containers │ └── sandboxes ├── io.containerd.runtime.v1.linux │ └── k8s.io ├── io.containerd.runtime.v2.task └── runc └── k8s.io 8 directories, 2 files OOM # 还有一项配置需要留意：\noom_score = 0 Containerd 是容器的守护者，一旦发生内存不足的情况，理想的情况应该是先杀死容器，而不是杀死 Containerd。所以需要调整 Containerd 的 OOM 权重，减少其被 OOM Kill 的几率。最好是将 oom_score 的值调整为比其他守护进程略低的值。这里的 oom_socre 其实对应的是 /proc/\u0026lt;pid\u0026gt;/oom_socre_adj，在早期的 Linux 内核版本里使用 oom_adj 来调整权重, 后来改用 oom_socre_adj 了。该文件描述如下：\nThe value of /proc/\u0026lt;pid\u0026gt;/oom_score_adj is added to the badness score before it\nis used to determine which task to kill. Acceptable values range from -1000\n(OOM_SCORE_ADJ_MIN) to +1000 (OOM_SCORE_ADJ_MAX). This allows userspace to\npolarize the preference for oom killing either by always preferring a certain\ntask or completely disabling it. The lowest possible value, -1000, is\nequivalent to disabling oom killing entirely for that task since it will always\nreport a badness score of 0.\n在计算最终的 badness score 时，会在计算结果是中加上 oom_score_adj ,这样用户就可以通过该在值来保护某个进程不被杀死或者每次都杀某个进程。其取值范围为 -1000 到 1000。\n如果将该值设置为 -1000，则进程永远不会被杀死，因为此时 badness score 永远返回0。\n建议 Containerd 将该值设置为 -999 到 0 之间。如果作为 Kubernetes 的 Worker 节点，可以考虑设置为 -999。\nSystemd 配置 # 建议通过 systemd 配置 Containerd 作为守护进程运行，配置文件在上文已经被解压出来了：\n🐳 → cat /etc/systemd/system/containerd.service # Copyright The containerd Authors. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [Unit] Description=containerd container runtime Documentation=https://containerd.io After=network.target local-fs.target [Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/containerd Type=notify Delegate=yes KillMode=process Restart=always RestartSec=5 # Having non-zero Limit*s causes performance problems due to accounting overhead # in the kernel. We recommend using cgroups to do container-local accounting. LimitNPROC=infinity LimitCORE=infinity LimitNOFILE=1048576 # Comment TasksMax if your systemd version does not supports it. # Only systemd 226 and above support this version. TasksMax=infinity OOMScoreAdjust=-999 [Install] WantedBy=multi-user.target 这里有两个重要的参数：\nDelegate : 这个选项允许 Containerd 以及运行时自己管理自己创建的容器的 cgroups。如果不设置这个选项，systemd 就会将进程移到自己的 cgroups 中，从而导致 Containerd 无法正确获取容器的资源使用情况。\nKillMode : 这个选项用来处理 Containerd 进程被杀死的方式。默认情况下，systemd 会在进程的 cgroup 中查找并杀死 Containerd 的所有子进程，这肯定不是我们想要的。KillMode字段可以设置的值如下。\ncontrol-group（默认值）：当前控制组里面的所有子进程，都会被杀掉 process：只杀主进程 mixed：主进程将收到 SIGTERM 信号，子进程收到 SIGKILL 信号 none：没有进程会被杀掉，只是执行服务的 stop 命令。 我们需要将 KillMode 的值设置为 process，这样可以确保升级或重启 Containerd 时不杀死现有的容器。\n现在到了最关键的一步：启动 Containerd。执行一条命令就完事：\n🐳 → systemctl enable containerd --now 接下来进入本文最后一部分：Containerd 的基本使用方式。本文只会介绍 Containerd 的本地使用方法，即本地客户端 ctr 的使用方法，不会涉及到 crictl，后面有机会再介绍 crictl。\n4. Containerd 快速安装 # 如果你想在一分钟内快速装好 Kubernetes 和 Containerd，可以直接部署 Sealos 云操作系统。当然，你也可以使用 Sealos 的命令行工具直接部署 Kubernetes 集群，只需一条命令即可。\n首先需要 下载 Sealos 的命令行工具，然后执行以下命令即可：\n🐳 → sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 registry.cn-shanghai.aliyuncs.com/labring/cilium:v1.13.4 \\ --masters \u0026lt;master1 的 IP\u0026gt;,\u0026lt;master2 的 IP\u0026gt;,... \\ --nodes \u0026lt;node1 的 IP\u0026gt;,\u0026lt;node1 的 IP\u0026gt;,... -p [your-ssh-passwd] 详细部署方式请参考： 安装 Kubernetes 集群\n5. ctr 使用 # ctr 目前很多功能做的还没有 docker 那么完善，但基本功能已经具备了。下面将围绕镜像和容器这两个方面来介绍其使用方法。\n镜像 # 镜像下载：\n🐳 → ctr i pull docker.io/library/nginx:alpine docker.io/library/nginx:alpine: resolved |++++++++++++++++++++++++++++++++++++++| index-sha256:efc93af57bd255ffbfb12c89ec0714dd1a55f16290eb26080e3d1e7e82b3ea66: done |++++++++++++++++++++++++++++++++++++++| manifest-sha256:6ceeeab513f7d15cea38c1f8dfe5455323b5a1bfd568516b3b0ee70406f75247: done |++++++++++++++++++++++++++++++++++++++| config-sha256:0fde4fb87e476fd1655b3f04f55aa5b4b3ef7de7c701eb46573bb5a5dcf66fd2: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:abaddf4965e5e9ce9953f2e136b3bf9cc15365adbcf0c68b108b1cc26c12b1be: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:05e7bc50f07f000e9993ec0d264b9ffcbb9a01a4d69c68f556d25e9811a8f7f4: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:c78f7f670e47cf98494e7dbe08e463d34c160bf6a5939a2155ff4438cb8b0e80: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:ce77cf6a2ede66c463dcdd39f1a43cfbac3723a99e94f697bc20faee0f7cce1b: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:3080fd9f46494247c9298a6a3d9694f03f6a32898a07ffbe1c17a0752bae5c4e: done |++++++++++++++++++++++++++++++++++++++| elapsed: 17.3s total: 8.7 Mi (513.8 KiB/s) unpacking linux/amd64 sha256:efc93af57bd255ffbfb12c89ec0714dd1a55f16290eb26080e3d1e7e82b3ea66... done 本地镜像列表查询：\n🐳 → ctr i ls REF TYPE DIGEST SIZE PLATFORMS LABELS docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:efc93af57bd255ffbfb12c89ec0714dd1a55f16290eb26080e3d1e7e82b3ea66 9.3 MiB linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x - 这里需要注意PLATFORMS，它是镜像的能够运行的平台标识。\n将镜像挂载到主机目录：\n🐳 → ctr i mount docker.io/library/nginx:alpine /mnt 🐳 → tree -L 1 /mnt /mnt ├── bin ├── dev ├── docker-entrypoint.d ├── docker-entrypoint.sh ├── etc ├── home ├── lib ├── media ├── mnt ├── opt ├── proc ├── root ├── run ├── sbin ├── srv ├── sys ├── tmp ├── usr └── var 18 directories, 1 file 将镜像从主机目录上卸载：\n🐳 → ctr i unmount /mnt 将镜像导出为压缩包：\n🐳 → ctr i export nginx.tar.gz docker.io/library/nginx:alpine 从压缩包导入镜像：\n🐳 → ctr i import nginx.tar.gz 其他操作可以自己查看帮助：\n🐳 → ctr i --help NAME: ctr images - manage images USAGE: ctr images command [command options] [arguments...] COMMANDS: check check that an image has all content available locally export export images import import images list, ls list images known to containerd mount mount an image to a target path unmount unmount the image from the target pull pull an image from a remote push push an image to a remote remove, rm remove one or more images by reference tag tag an image label set and clear labels for an image OPTIONS: --help, -h show help 对镜像的更高级操作可以使用子命令 content，例如在线编辑镜像的 blob 并生成一个新的 digest：\n🐳 → ctr content ls DIGEST\tSIZE\tAGE\tLABELS ... ... sha256:fdd7fff110870339d34cf071ee90fbbe12bdbf3d1d9a14156995dfbdeccd7923\t740B\t7 days\tcontainerd.io/gc.ref.content.2=sha256:4e537e26e21bf61836f827e773e6e6c3006e3c01c6d59f4b058b09c2753bb929,containerd.io/gc.ref.content.1=sha256:188c0c94c7c576fff0792aca7ec73d67a2f7f4cb3a6e53a84559337260b36964,containerd.io/gc.ref.content.0=sha256:b7199797448c613354489644be1f60aa2d8e9c2278989100c72ede3001334f7b,containerd.io/distribution.source.ghcr.icloudnative.io=yangchuansheng/grafana-backup-tool 🐳 → ctr content edit --editor vim sha256:fdd7fff110870339d34cf071ee90fbbe12bdbf3d1d9a14156995dfbdeccd7923 容器 # 创建容器：\n🐳 → ctr c create docker.io/library/nginx:alpine nginx 🐳 → ctr c ls CONTAINER IMAGE RUNTIME nginx docker.io/library/nginx:alpine io.containerd.runc.v2 查看容器的详细配置：\n# 和 docker inspect 类似 🐳 → ctr c info nginx 其他操作可以自己查看帮助：\n🐳 → ctr c --help NAME: ctr containers - manage containers USAGE: ctr containers command [command options] [arguments...] COMMANDS: create create container delete, del, rm delete one or more existing containers info get info about a container list, ls list containers label set and clear labels for a container checkpoint checkpoint a container restore restore a container from checkpoint OPTIONS: --help, -h show help 任务 # 上面 create 的命令创建了容器后，并没有处于运行状态，只是一个静态的容器。一个 container 对象只是包含了运行一个容器所需的资源及配置的数据结构，这意味着 namespaces、rootfs 和容器的配置都已经初始化成功了，只是用户进程(这里是 nginx)还没有启动。\n然而一个容器真正的运行起来是由 Task 对象实现的，task 代表任务的意思，可以为容器设置网卡，还可以配置工具来对容器进行监控等。\n所以还需要通过 Task 启动容器：\n🐳 → ctr task start -d nginx 🐳 → ctr task ls TASK PID STATUS nginx 131405 RUNNING 当然，也可以一步到位直接创建并运行容器：\n🐳 → ctr run -d docker.io/library/nginx:alpine nginx 进入容器：\n# 和 docker 的操作类似，但必须要指定 --exec-id，这个 id 可以随便写，只要唯一就行 🐳 → ctr task exec --exec-id 0 -t nginx sh 暂停容器：\n# 和 docker pause 类似 🐳 → ctr task pause nginx 容器状态变成了 PAUSED：\n🐳 → ctr task ls TASK PID STATUS nginx 149857 PAUSED 恢复容器：\n🐳 → ctr task resume nginx ctr 没有 stop 容器的功能，只能暂停或者杀死容器。\n杀死容器：\n🐳 → ctr task kill nginx 获取容器的 cgroup 信息：\n# 这个命令用来获取容器的内存、CPU 和 PID 的限额与使用量。 🐳 → ctr task metrics nginx ID TIMESTAMP nginx 2020-12-15 09:15:13.943447167 +0000 UTC METRIC VALUE memory.usage_in_bytes 77131776 memory.limit_in_bytes 9223372036854771712 memory.stat.cache 6717440 cpuacct.usage 194187935 cpuacct.usage_percpu [0 335160 0 5395642 3547200 58559242 0 0 0 0 0 0 6534104 5427871 3032481 2158941 8513633 4620692 8261063 3885961 3667830 0 4367411 356280 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1585841 0 7754942 5818102 21430929 0 0 0 0 0 0 1811840 2241260 2673960 6041161 8210604 2991221 10073713 1111020 3139751 0 640080 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] pids.current 97 pids.limit 0 查看容器中所有进程的 PID：\n🐳 → ctr task ps nginx PID INFO 149857 - 149921 - 149922 - 149923 - 149924 - 149925 - 149926 - 149928 - 149929 - 149930 - 149932 - 149933 - 149934 - ... 注意：这里的 PID 是宿主机看到的 PID，不是容器中看到的 PID。\n命名空间 # 除了 k8s 有命名空间以外，Containerd 也支持命名空间。\n🐳 → ctr ns ls NAME LABELS default 如果不指定，ctr 默认是 default 空间。\n目前 Containerd 的定位还是解决运行时，所以目前他还不能完全替代 dockerd，例如使用 Dockerfile 来构建镜像。其实这不是什么大问题，我再给大家介绍一个大招：Containerd 和 Docker 一起用！\nContainerd + Docker # 事实上，Docker 和 Containerd 是可以同时使用的，只不过 Docker 默认使用的 Containerd 的命名空间不是 default，而是 moby。下面就是见证奇迹的时刻。\n首先从其他装了 Docker 的机器或者 GitHub 上下载 Docker 相关的二进制文件，然后使用下面的命令启动 Docker：\n🐳 → dockerd --containerd /run/containerd/containerd.sock --cri-containerd 接着用 Docker 运行一个容器：\n🐳 → docker run -d --name nginx nginx:alpine 现在再回过头来查看 Containerd 的命名空间：\n🐳 → ctr ns ls NAME LABELS default moby 查看该命名空间下是否有容器：\n🐳 → ctr -n moby c ls CONTAINER IMAGE RUNTIME b7093d7aaf8e1ae161c8c8ffd4499c14ba635d8e174cd03711f4f8c27818e89a - io.containerd.runtime.v1.linux 我艹，还可以酱紫？看来以后用 Containerd 不耽误我 docker build 了~~\n最后提醒一句：Kubernetes 用户不用惊慌，Kubernetes 默认使用的是 Containerd 的 k8s.io 命名空间，所以 ctr -n k8s.io 就能看到 Kubernetes 创建的所有容器啦，也不用担心 crictl 不支持 load 镜像了，因为 ctr -n k8s.io 可以 load 镜像啊，嘻嘻😬\n当然，Containerd 也有比较好用的命令行工具： nerdctl。后面我会给大家介绍 nerdctl 的安装和使用教程。\n","date":"2020年12月13日","externalUrl":null,"permalink":"/posts/getting-started-with-containerd/","section":"博客","summary":"1. Containerd 的前世今生 # 很久以前，Docker 强势崛起，以“镜像”这","title":"Containerd 使用教程","type":"posts"},{"content":"Kubernetes 具有对机器的资源进行分配和使用的能力，比如可以指定容器最多使用多少内存以及使用多少 CPU 计算资源。那么问题来了，一般来说容器就是使用 CPU 和内存资源，那么对于需要使用显卡的 Pod，Kubernetes 也能够支持吗？答案当然是可以啦！目前 Kubernetes 不仅支持容器请求 GPU 资源，还支持请求几块显卡的 GPU 资源，这使得 Kubernetes 在深度学习和区块链等场景下也有了用武之地。\n关于 Kubernetes 集群中 Docker 如何使用 GPU，Kubernetes 的官方文档已经说的很清楚了，网上也有铺天盖地的博客手把手教你怎么做。至于以 Containerd 作为容器运行时的集群如何使用 GPU，网上还找不到一篇像样的文档来告诉大家怎么做，今天我就来做吃螃蟹的第一人。\n要想在容器里使用 GPU，本质上就是我们要在容器里能看到并且使用宿主机上的显卡，所有的步骤都是围绕这个来做的。当然，本文不会涉及如何安装 Containerd，也不会涉及如何安装 Kubernetes，如果这些都搞不定，建议不要往下看。\n1. Nvidia 驱动 # 某些命令以 Ubuntu 作为示例。 首先宿主机上必现安装 Nvidia 驱动。这里推荐从 Nvidia 官网下载脚本安装，安装和卸载都比较方便并且适用于任何 Linux 发行版，包括 CentOS，Ubuntu 等。 NVIDIA Telsa GPU 的 Linux 驱动在安装过程中需要编译 kernel module，系统需提前安装 gcc 和编译 Linux Kernel Module 所依赖的包，例如 kernel-devel-$(uname -r) 等。\n安装 gcc 和 kernel-dev(如果没有) sudo apt install gcc kernel-dev -y。\n访问 官网下载。\n选择操作系统和安装包，并单击【SEARCH】搜寻驱动，选择要下载的驱动版本\n下载对应版本安装脚本 在宿主机上执行：\n$ wget https://www.nvidia.com/content/DriverDownload-March2009/confirmation.php?url=/tesla/450.80.02/NVIDIA-Linux-x86_64-450.80.02.run\u0026amp;lang=us\u0026amp;type=Tesla 安装 执行脚本安装：\n$ chmod +x NVIDIA-Linux-x86_64-450.80.02.run \u0026amp;\u0026amp; ./NVIDIA-Linux-x86_64-450.80.02.run 验证 使用如下命令验证是否安装成功 nvidia-smi 如果输出类似下图则驱动安装成功。\n2. CUDA 驱动 # CUDA（Compute Unified Device Architecture）是显卡厂商 NVIDIA 推出的运算平台。CUDA™ 是一种由 NVIDIA 推出的通用并行计算架构，该架构使 GPU 能够解决复杂的计算问题。它包含了 CUDA 指令集架构（ISA）以及 GPU 内部的并行计算引擎。 这里安装的方式和显卡驱动安装类似。\n访问 官网下载\n下载对应版本如下图\n配置环境变量\n$ echo \u0026#39;export PATH=/usr/local/cuda/bin:$PATH\u0026#39; | sudo tee /etc/profile.d/cuda.sh $ source /etc/profile 3. nvidia-container-runtime # nvidia-container-runtime 是在 runc 基础上多实现了 nvidia-container-runime-hook(现在叫 nvidia-container-toolkit)，该 hook 是在容器启动后（Namespace已创建完成），容器自定义命令(Entrypoint)启动前执行。当检测到 NVIDIA_VISIBLE_DEVICES 环境变量时，会调用 libnvidia-container 挂载 GPU Device 和 CUDA Driver。如果没有检测到 NVIDIA_VISIBLE_DEVICES 就会执行默认的 runc。\n下面分两步安装：\n先设置 repository 和 GPG key：\n$ curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | sudo apt-key add - $ curl -s -L https://nvidia.github.io/nvidia-container-runtime/$(. /etc/os-release;echo $ID$VERSION_ID)/nvidia-container-runtime.list | sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list 安装：\n$ apt install nvidia-container-runtime -y 配置 Containerd 使用 Nvidia container runtime # 如果 /etc/containerd 目录不存在，就先创建它：\n$ mkdir /etc/containerd 生成默认配置：\n$ containerd config default \u0026gt; /etc/containerd/config.toml Kubernetes 使用 设备插件（Device Plugins） 来允许 Pod 访问类似 GPU 这类特殊的硬件功能特性，但前提是默认的 OCI runtime 必须改成 nvidia-container-runtime，需要修改的内容如下：\n/etc/containerd/config.toml\n... [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd] snapshotter = \u0026#34;overlayfs\u0026#34; default_runtime_name = \u0026#34;runc\u0026#34; no_pivot = false ... [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.runtimes] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.runtimes.runc] runtime_type = \u0026#34;io.containerd.runtime.v1.linux\u0026#34; # 将此处 runtime_type 的值改成 io.containerd.runtime.v1.linux ... [plugins.\u0026#34;io.containerd.runtime.v1.linux\u0026#34;] shim = \u0026#34;containerd-shim\u0026#34; runtime = \u0026#34;nvidia-container-runtime\u0026#34; # 将此处 runtime 的值改成 nvidia-container-runtime ... 重启 containerd 服务：\n$ systemctl restart containerd 4. 部署 NVIDIA GPU 设备插件 # 一条命令解决战斗：\n$ kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.7.1/nvidia-device-plugin.yml 查看日志：\n$ kubectl -n kube-system logs nvidia-device-plugin-daemonset-xxx 2020/12/04 06:30:28 Loading NVML 2020/12/04 06:30:28 Starting FS watcher. 2020/12/04 06:30:28 Starting OS watcher. 2020/12/04 06:30:28 Retreiving plugins. 2020/12/04 06:30:28 Starting GRPC server for \u0026#39;nvidia.com/gpu\u0026#39; 2020/12/04 06:30:28 Starting to serve \u0026#39;nvidia.com/gpu\u0026#39; on /var/lib/kubelet/device-plugins/nvidia-gpu.sock 2020/12/04 06:30:28 Registered device plugin for \u0026#39;nvidia.com/gpu\u0026#39; with Kubelet 可以看到设备插件部署成功了。在 Node 上面可以看到设备插件目录下的 socket：\n$ ll /var/lib/kubelet/device-plugins/ total 12 drwxr-xr-x 2 root root 4096 Dec 4 01:30 ./ drwxr-xr-x 8 root root 4096 Dec 3 05:05 ../ -rw-r--r-- 1 root root 0 Dec 4 01:11 DEPRECATION -rw------- 1 root root 3804 Dec 4 01:30 kubelet_internal_checkpoint srwxr-xr-x 1 root root 0 Dec 4 01:11 kubelet.sock= srwxr-xr-x 1 root root 0 Dec 4 01:11 kubevirt-kvm.sock= srwxr-xr-x 1 root root 0 Dec 4 01:11 kubevirt-tun.sock= srwxr-xr-x 1 root root 0 Dec 4 01:11 kubevirt-vhost-net.sock= srwxr-xr-x 1 root root 0 Dec 4 01:30 nvidia-gpu.sock= 5. 测试 GPU # 首先测试本地命令行工具 ctr，这个应该没啥问题：\n$ ctr images pull docker.io/nvidia/cuda:9.0-base $ ctr run --rm -t --gpus 0 docker.io/nvidia/cuda:9.0-base nvidia-smi nvidia-smi Fri Dec 4 07:01:38 2020 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 440.95.01 Driver Version: 440.95.01 CUDA Version: 10.2 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | |===============================+======================+======================| | 0 GeForce RTX 208... Off | 00000000:A1:00.0 Off | N/A | | 30% 33C P8 9W / 250W | 0MiB / 11019MiB | 0% Default | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: GPU Memory | | GPU PID Type Process name Usage | |=============================================================================| | No running processes found | +-----------------------------------------------------------------------------+ 最后进入终极测试：在 Pod 中测试 GPU 可用性。先创建部署清单：\ngpu-pod.yaml\napiVersion: v1 kind: Pod metadata: name: cuda-vector-add spec: restartPolicy: OnFailure containers: - name: cuda-vector-add image: \u0026#34;k8s.gcr.io/cuda-vector-add:v0.1\u0026#34; resources: limits: nvidia.com/gpu: 1 执行 kubectl apply -f ./gpu-pod.yaml 创建 Pod。使用 kubectl get pod 可以看到该 Pod 已经启动成功：\n$ kubectl get pod NAME READY STATUS RESTARTS AGE cuda-vector-add 0/1 Completed 0 3s 查看 Pod 日志：\n$ kubectl logs cuda-vector-add [Vector addition of 50000 elements] Copy input data from the host memory to the CUDA device CUDA kernel launch with 196 blocks of 256 threads Copy output data from the CUDA device to the host memory Test PASSED Done 可以看到成功运行。这也说明 Kubernetes 完成了对 GPU 资源的调用。需要注意的是，目前 Kubernetes 只支持卡级别的调度，并且显卡资源是独占，无法在多个容器之间分享。\n参考资料 # 容器中使用 GPU 的基础环境搭建 ","date":"2020年12月3日","externalUrl":null,"permalink":"/posts/add-nvidia-gpu-support-to-k8s-with-containerd/","section":"博客","summary":"Kubernetes 具有对机器的资源进行分配和使用的能力，比如可以指定容器最多","title":"Kubernetes 教程：在 Containerd 容器中使用 GPU","type":"posts"},{"content":"目前我们 k8s 集群的 Grafana 使用 ceph 作为持久化存储，一但我将 Grafana 的 Deployment 删除重建之后，之前的所有数据都会丢失，重建的 PV 会映射到后端存储的新位置。万幸的是，我真的手欠重建了，还没有提前备份。。。万幸个鬼啊我。\n在我历经 250 分钟重建 Dashboard 之后，心里久久不能平静，一句 MMP 差点就要脱口而出。\n1. 低级方案 # 再这样下去我真的要变成 250 了，这怎么能忍，立马打开 Google 研究了一把 Grafana 备份的各种骚操作，发现大部分备份方案都是通过 shell 脚本调用 Grafana 的 API 来导出各种配置。备份脚本大部分都集中在这个 gist 中：\nhttps://gist.github.com/crisidev/bd52bdcc7f029be2f295 我挑选出几个比较好用的，大家也可以自行挑选其他的。\n导出脚本 # #!/bin/bash # Usage: # # export_grafana_dashboards.sh https://admin:REDACTED@grafana.dedevsecops.com create_slug () { echo \u0026#34;$1\u0026#34; | iconv -t ascii//TRANSLIT | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\\|-+$//g | tr A-Z a-z } full_url=$1 username=$(echo \u0026#34;${full_url}\u0026#34; | cut -d/ -f 3 | cut -d: -f 1) base_url=$(echo \u0026#34;${full_url}\u0026#34; | cut -d@ -f 2) folder=$(create_slug \u0026#34;${username}-${base_url}\u0026#34;) mkdir \u0026#34;${folder}\u0026#34; for db_uid in $(curl -s \u0026#34;${full_url}/api/search\u0026#34; | jq -r .[].uid); do db_json=$(curl -s \u0026#34;${full_url}/api/dashboards/uid/${db_uid}\u0026#34;) db_slug=$(echo \u0026#34;${db_json}\u0026#34; | jq -r .meta.slug) db_title=$(echo \u0026#34;${db_json}\u0026#34; | jq -r .dashboard.title) filename=\u0026#34;${folder}/${db_slug}.json\u0026#34; echo \u0026#34;Exporting \\\u0026#34;${db_title}\\\u0026#34; to \\\u0026#34;${filename}\\\u0026#34;...\u0026#34; echo \u0026#34;${db_json}\u0026#34; | jq -r . \u0026gt; \u0026#34;${filename}\u0026#34; done echo \u0026#34;Done\u0026#34; 这个脚本比较简单，直接导出了所有 Dashboard 的 json 配置，也没有标记目录信息，如果你用它导出的配置来恢复 Grafana，所有的 Dashboard 都会导入到 Grafana 的 General 目录下，不太友好。\n导入脚本 # grafana-dashboard-importer.sh\n#!/bin/bash # # add the \u0026#34;-x\u0026#34; option to the shebang line if you want a more verbose output # # OPTSPEC=\u0026#34;:hp:t:k:\u0026#34; show_help() { cat \u0026lt;\u0026lt; EOF Usage: $0 [-p PATH] [-t TARGET_HOST] [-k API_KEY] Script to import dashboards into Grafana -p Required. Root path containing JSON exports of the dashboards you want imported. -t Required. The full URL of the target host -k Required. The API key to use on the target host -h Display this help and exit. EOF } ###### Check script invocation options ###### while getopts \u0026#34;$OPTSPEC\u0026#34; optchar; do case \u0026#34;$optchar\u0026#34; in h) show_help exit ;; p) DASH_DIR=\u0026#34;$OPTARG\u0026#34;;; t) HOST=\u0026#34;$OPTARG\u0026#34;;; k) KEY=\u0026#34;$OPTARG\u0026#34;;; \\?) echo \u0026#34;Invalid option: -$OPTARG\u0026#34; \u0026gt;\u0026amp;2 exit 1 ;; :) echo \u0026#34;Option -$OPTARG requires an argument.\u0026#34; \u0026gt;\u0026amp;2 exit 1 ;; esac done if [ -z \u0026#34;$DASH_DIR\u0026#34; ] || [ -z \u0026#34;$HOST\u0026#34; ] || [ -z \u0026#34;$KEY\u0026#34; ]; then show_help exit 1 fi # set some colors for status OK, FAIL and titles SETCOLOR_SUCCESS=\u0026#34;echo -en \\\\033[0;32m\u0026#34; SETCOLOR_FAILURE=\u0026#34;echo -en \\\\033[1;31m\u0026#34; SETCOLOR_NORMAL=\u0026#34;echo -en \\\\033[0;39m\u0026#34; SETCOLOR_TITLE_PURPLE=\u0026#34;echo -en \\\\033[0;35m\u0026#34; # purple # usage log \u0026#34;string to log\u0026#34; \u0026#34;color option\u0026#34; function log_success() { if [ $# -lt 1 ]; then ${SETCOLOR_FAILURE} echo \u0026#34;Not enough arguments for log function! Expecting 1 argument got $#\u0026#34; exit 1 fi timestamp=$(date \u0026#34;+%Y-%m-%d %H:%M:%S %Z\u0026#34;) ${SETCOLOR_SUCCESS} printf \u0026#34;[%s] $1\\n\u0026#34; \u0026#34;$timestamp\u0026#34; ${SETCOLOR_NORMAL} } function log_failure() { if [ $# -lt 1 ]; then ${SETCOLOR_FAILURE} echo \u0026#34;Not enough arguments for log function! Expecting 1 argument got $#\u0026#34; exit 1 fi timestamp=$(date \u0026#34;+%Y-%m-%d %H:%M:%S %Z\u0026#34;) ${SETCOLOR_FAILURE} printf \u0026#34;[%s] $1\\n\u0026#34; \u0026#34;$timestamp\u0026#34; ${SETCOLOR_NORMAL} } function log_title() { if [ $# -lt 1 ]; then ${SETCOLOR_FAILURE} log_failure \u0026#34;Not enough arguments for log function! Expecting 1 argument got $#\u0026#34; exit 1 fi ${SETCOLOR_TITLE_PURPLE} printf \u0026#34;|-------------------------------------------------------------------------|\\n\u0026#34; printf \u0026#34;|%s|\\n\u0026#34; \u0026#34;$1\u0026#34;; printf \u0026#34;|-------------------------------------------------------------------------|\\n\u0026#34; ${SETCOLOR_NORMAL} } if [ -d \u0026#34;$DASH_DIR\u0026#34; ]; then DASH_LIST=$(find \u0026#34;$DASH_DIR\u0026#34; -mindepth 1 -name \\*.json) if [ -z \u0026#34;$DASH_LIST\u0026#34; ]; then log_title \u0026#34;----------------- $DASH_DIR contains no JSON files! -----------------\u0026#34; log_failure \u0026#34;Directory $DASH_DIR does not appear to contain any JSON files for import. Check your path and try again.\u0026#34; exit 1 else FILESTOTAL=$(echo \u0026#34;$DASH_LIST\u0026#34; | wc -l) log_title \u0026#34;----------------- Starting import of $FILESTOTAL dashboards -----------------\u0026#34; fi else log_title \u0026#34;----------------- $DASH_DIR directory not found! -----------------\u0026#34; log_failure \u0026#34;Directory $DASH_DIR does not exist. Check your path and try again.\u0026#34; exit 1 fi NUMSUCCESS=0 NUMFAILURE=0 COUNTER=0 for DASH_FILE in $DASH_LIST; do COUNTER=$((COUNTER + 1)) echo \u0026#34;Import $COUNTER/$FILESTOTAL: $DASH_FILE...\u0026#34; RESULT=$(cat \u0026#34;$DASH_FILE\u0026#34; | jq \u0026#39;. * {overwrite: true, dashboard: {id: null}}\u0026#39; | curl -s -X POST -H \u0026#34;Content-Type: application/json\u0026#34; -H \u0026#34;Authorization: Bearer $KEY\u0026#34; \u0026#34;$HOST\u0026#34;/api/dashboards/db -d @-) if [[ \u0026#34;$RESULT\u0026#34; == *\u0026#34;success\u0026#34;* ]]; then log_success \u0026#34;$RESULT\u0026#34; NUMSUCCESS=$((NUMSUCCESS + 1)) else log_failure \u0026#34;$RESULT\u0026#34; NUMFAILURE=$((NUMFAILURE + 1)) fi done log_title \u0026#34;Import complete. $NUMSUCCESS dashboards were successfully imported. $NUMFAILURE dashboard imports failed.\u0026#34;; log_title \u0026#34;------------------------------ FINISHED ---------------------------------\u0026#34;; 导入脚本需要目标机器上的 Grafana 已经启动，而且需要提供管理员 API Key。登录 Grafana Web 界面，打开 API Keys：\n新建一个 API Key，角色选择 Admin，过期时间自己调整：\n导入方式：\n$ ./grafana-dashboard-importer.sh -t http://\u0026lt;grafana_svc_ip\u0026gt;:\u0026lt;grafana_svc_port\u0026gt; -k \u0026lt;api_key\u0026gt; -p \u0026lt;backup folder\u0026gt; 其中 -p 参数指定的是之前导出的 json 所在的目录。\n目前的方案痛点在于只能备份 Dashboard，不能备份其他的配置（例如，数据源、用户、秘钥等），而且没有将 Dashboard 和目录对应起来，即不支持备份 Folder。下面介绍一个比较完美的备份恢复方案，支持所有配置的备份恢复，简直不要太香。\n2. 高级方案 # 更高级的方案已经有人写好了，项目地址是：\nhttps://github.com/ysde/grafana-backup-tool 该备份工具支持以下几种配置：\n目录 Dashboard 数据源 Grafana 告警频道（Alert Channel） 组织（Organization） 用户（User） 使用方法很简单，跑个容器就好了嘛，不过作者提供的 Dockerfile 我不是很满意，自己修改了点内容：\nFROM alpine:latest LABEL maintainer=\u0026#34;grafana-backup-tool Docker Maintainers https://icloudnative.io\u0026#34; ENV ARCHIVE_FILE \u0026#34;\u0026#34; RUN echo \u0026#34;@edge http://dl-cdn.alpinelinux.org/alpine/edge/community\u0026#34; \u0026gt;\u0026gt; /etc/apk/repositories; \\ apk --no-cache add python3 py3-pip py3-cffi py3-cryptography ca-certificates bash git; \\ git clone https://github.com/ysde/grafana-backup-tool /opt/grafana-backup-tool; \\ cd /opt/grafana-backup-tool; \\ pip3 --no-cache-dir install .; \\ chown -R 1337:1337 /opt/grafana-backup-tool WORKDIR /opt/grafana-backup-tool USER 1337 只有 Dockerfile 不行，还得通过 CI/CD 自动构建并推送到 docker.io。不要问我用什么，当然是白嫖 GitHub Action，workflow 内容如下：\n#================================================= # https://github.com/yangchuansheng/docker-image # Description: Build and push grafana-backup-tool Docker image # Lisence: MIT # Author: Ryan # Blog: https://icloudnative.io #================================================= name: Build and push grafana-backup-tool Docker image # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: push: branches: [ master ] paths: - \u0026#39;grafana-backup-tool/Dockerfile\u0026#39; - \u0026#39;.github/workflows/grafana-backup-tool.yml\u0026#39; pull_request: branches: [ master ] paths: - \u0026#39;grafana-backup-tool/Dockerfile\u0026#39; #watch: #types: started # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called \u0026#34;build\u0026#34; build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GitHub Package Registry env: username: ${{ github.repository_owner }} password: ${{ secrets.GHCR_TOKEN }} run: echo ${{ env.password }} | docker login ghcr.io -u ${{ env.username }} --password-stdin # Runs a single command using the runners shell - name: Build and push Docker images to docker.io and ghcr.io uses: docker/build-push-action@v2 with: file: \u0026#39;grafana-backup-tool/Dockerfile\u0026#39; platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x context: grafana-backup-tool push: true tags: | yangchuansheng/grafana-backup-tool:latest ghcr.io/yangchuansheng/grafana-backup-tool:latest #- name: Update repo description #uses: peter-evans/dockerhub-description@v2 #env: #DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} #DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} #DOCKERHUB_REPOSITORY: yangchuansheng/grafana-backup-tool #README_FILEPATH: grafana-backup-tool/readme.md 这里我不打算解释 workflow 的内容，有点基础的应该都能看懂，实在不行，以后我会单独写文章解释（又可以继续水文了~）。这个 workflow 实现的功能就是自动构建各个 CPU 架构的镜像，并推送到 docker.io 和 ghcr.io，特么的真香！\n就问爽不爽？\n你可以直接关注我的仓库：\nhttps://github.com/yangchuansheng/docker-image 构建好镜像后，就可以直接运行容器来进行备份和恢复操作了。如果你想在集群内操作，可以通过 Deployment 或 Job 来实现；如果你想在本地或 k8s 集群外操作，可以选择 docker run，我不反对，你也可以选择 docker-compose，这都没问题。但我要告诉你一个更骚的办法，可以骚到让你无法自拔。\n首先需要在本地或集群外安装 Podman，如果操作系统是 Win10，可以考虑通过 WSL 来安装；如果操作系统是 Linux，那就不用说了；如果操作系统是 MacOS，请参考我的上篇文章： 在 macOS 中使用 Podman。\n装好了 Podman 之后，就可以进行骚操作了，请睁大眼睛。\n先编写一个 Deployment 配置清单（什么？Deployment？是的，你没听错）：\ngrafana-backup-deployment.yaml\napiVersion: apps/v1 kind: Deployment metadata: name: grafana-backup labels: app: grafana-backup spec: replicas: 1 selector: matchLabels: app: grafana-backup template: metadata: labels: app: grafana-backup spec: containers: - name: grafana-backup image: yangchuansheng/grafana-backup-tool:latest imagePullPolicy: IfNotPresent command: [\u0026#34;/bin/bash\u0026#34;] tty: true stdin: true env: - name: GRAFANA_TOKEN value: \u0026#34;eyJr0NkFBeWV1QVpMNjNYWXA3UXNOM2JWMWdZOTB2ZFoiLCJuIjoiYWRtaW4iLCJpZCI6MX0=\u0026#34; - name: GRAFANA_URL value: \u0026#34;http://\u0026lt;grafana_ip\u0026gt;:\u0026lt;grafana_port\u0026gt;\u0026#34; - name: GRAFANA_ADMIN_ACCOUNT value: \u0026#34;admin\u0026#34; - name: GRAFANA_ADMIN_PASSWORD value: \u0026#34;admin\u0026#34; - name: VERIFY_SSL value: \u0026#34;False\u0026#34; volumeMounts: - mountPath: /opt/grafana-backup-tool name: data volumes: - name: data hostPath: path: /mnt/manifest/grafana/backup 这里面的环境变量根据自己的实际情况修改，一定不要照抄我的！\n不要一脸懵逼，我先来解释一下为什么要准备这个 Deployment 配置清单，因为 Podman 可以直接通过这个配置清单运行容器，命令如下：\n$ podman play kube grafana-backup-deployment.yaml 我第一次见到这个操作的时候也不禁连连我艹，这也可以？确实可以，不过呢，Podman 只是将其翻译一下，跑个容器而已，并不是真正运行 Deployment，因为它没有控制器啊，但是，还是真香！\n想象一下，你可以将 k8s 集群中的配置清单拿到本地或测试机器直接跑，再也不用 k8s 集群准备一份 yaml，docker-compose 再准备一份 yaml 了，一份 yaml 走天下，服不服？\ndocker-compose 混到今天这个地步，也是蛮可怜的。\n细心的读者应该能发现上面的配置清单有点奇怪，Dockerfile 也有点奇怪。Dockerfile 中没有写 CMD 或 ENTRYPOINT，Deployment 中直接将启动命令设置为 bash，这是因为在我之前测试的过程中发现该镜像启动的容器有点问题，它会陷入一个循环，备份完了之后又会继续备份，不断重复，导致备份目录下生成了一坨压缩包。目前还没找到比较好的解决办法，只能将容器的启动命令设置为 bash，等容器运行后再进入容器进行备份操作：\n$ podman pod ls POD ID NAME STATUS CREATED # OF CONTAINERS INFRA ID 728aec216d66 grafana-backup-pod-0 Running 3 minutes ago 2 92aa0824fe7d $ podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b523fa8e4819 yangchuansheng/grafana-backup-tool:latest /bin/bash 3 minutes ago Up 3 minutes ago grafana-backup-pod-0-grafana-backup 92aa0824fe7d k8s.gcr.io/pause:3.2 3 minutes ago Up 3 minutes ago 728aec216d66-infra $ podman exec -it grafana-backup-pod-0-grafana-backup bash bash-5.0$ grafana-backup save ... ... ######################################## backup folders at: _OUTPUT_/folders/202012111556 backup datasources at: _OUTPUT_/datasources/202012111556 backup dashboards at: _OUTPUT_/dashboards/202012111556 backup alert_channels at: _OUTPUT_/alert_channels/202012111556 backup organizations at: _OUTPUT_/organizations/202012111556 backup users at: _OUTPUT_/users/202012111556 created archive at: _OUTPUT_/202012111556.tar.gz 默认情况下会备份所有的组件，你也可以指定备份的组件：\n$ grafana-backup save --components=\u0026lt;folders,dashboards,datasources,alert-channels,organizations,users\u0026gt; 比如，我只想备份 Dashboards 和 Folders：\n$ grafana-backup save --components=folders,dashboards 当然，你也可以全部备份，恢复的时候再选择自己想恢复的组件：\n$ grafana-backup restore --components=folders,dashboards 至此，再也不用怕 Dashboard 被改掉或删除啦。\n最后提醒一下，Prometheus Operator 项目中的 Grafana 通过 Provisioning 的方式预导入了一些默认的 Dashboards，这本来没有什么问题，但 grafana-backup-tool 工具无法忽略跳过已经存在的配置，如果恢复的过程中遇到已经存在的配置，会直接报错退出。本来这也很好解决，一般情况下到 Grafana Web 界面中删除所有的 Dashboard 就好了，但通过 Provisioning 导入的 Dashboard 是无法删除的，这就很尴尬了。\n在作者修复这个 bug 之前，要想解决这个问题，有两个办法：\n第一个办法是在恢复之前将 Grafana Deployment 中关于 Provisioning 的配置全部删除，就是这些配置：\nvolumeMounts: - mountPath: /etc/grafana/provisioning/datasources name: grafana-datasources readOnly: false - mountPath: /etc/grafana/provisioning/dashboards name: grafana-dashboards readOnly: false - mountPath: /grafana-dashboard-definitions/0/apiserver name: grafana-dashboard-apiserver readOnly: false - mountPath: /grafana-dashboard-definitions/0/cluster-total name: grafana-dashboard-cluster-total readOnly: false - mountPath: /grafana-dashboard-definitions/0/controller-manager name: grafana-dashboard-controller-manager readOnly: false - mountPath: /grafana-dashboard-definitions/0/k8s-resources-cluster name: grafana-dashboard-k8s-resources-cluster readOnly: false - mountPath: /grafana-dashboard-definitions/0/k8s-resources-namespace name: grafana-dashboard-k8s-resources-namespace readOnly: false - mountPath: /grafana-dashboard-definitions/0/k8s-resources-node name: grafana-dashboard-k8s-resources-node readOnly: false - mountPath: /grafana-dashboard-definitions/0/k8s-resources-pod name: grafana-dashboard-k8s-resources-pod readOnly: false - mountPath: /grafana-dashboard-definitions/0/k8s-resources-workload name: grafana-dashboard-k8s-resources-workload readOnly: false - mountPath: /grafana-dashboard-definitions/0/k8s-resources-workloads-namespace name: grafana-dashboard-k8s-resources-workloads-namespace readOnly: false - mountPath: /grafana-dashboard-definitions/0/kubelet name: grafana-dashboard-kubelet readOnly: false - mountPath: /grafana-dashboard-definitions/0/namespace-by-pod name: grafana-dashboard-namespace-by-pod readOnly: false - mountPath: /grafana-dashboard-definitions/0/namespace-by-workload name: grafana-dashboard-namespace-by-workload readOnly: false - mountPath: /grafana-dashboard-definitions/0/node-cluster-rsrc-use name: grafana-dashboard-node-cluster-rsrc-use readOnly: false - mountPath: /grafana-dashboard-definitions/0/node-rsrc-use name: grafana-dashboard-node-rsrc-use readOnly: false - mountPath: /grafana-dashboard-definitions/0/nodes name: grafana-dashboard-nodes readOnly: false - mountPath: /grafana-dashboard-definitions/0/persistentvolumesusage name: grafana-dashboard-persistentvolumesusage readOnly: false - mountPath: /grafana-dashboard-definitions/0/pod-total name: grafana-dashboard-pod-total readOnly: false - mountPath: /grafana-dashboard-definitions/0/prometheus-remote-write name: grafana-dashboard-prometheus-remote-write readOnly: false - mountPath: /grafana-dashboard-definitions/0/prometheus name: grafana-dashboard-prometheus readOnly: false - mountPath: /grafana-dashboard-definitions/0/proxy name: grafana-dashboard-proxy readOnly: false - mountPath: /grafana-dashboard-definitions/0/scheduler name: grafana-dashboard-scheduler readOnly: false - mountPath: /grafana-dashboard-definitions/0/statefulset name: grafana-dashboard-statefulset readOnly: false - mountPath: /grafana-dashboard-definitions/0/workload-total name: grafana-dashboard-workload-total readOnly: false ... ... volumes: - name: grafana-datasources secret: secretName: grafana-datasources - configMap: name: grafana-dashboards name: grafana-dashboards - configMap: name: grafana-dashboard-apiserver name: grafana-dashboard-apiserver - configMap: name: grafana-dashboard-cluster-total name: grafana-dashboard-cluster-total - configMap: name: grafana-dashboard-controller-manager name: grafana-dashboard-controller-manager - configMap: name: grafana-dashboard-k8s-resources-cluster name: grafana-dashboard-k8s-resources-cluster - configMap: name: grafana-dashboard-k8s-resources-namespace name: grafana-dashboard-k8s-resources-namespace - configMap: name: grafana-dashboard-k8s-resources-node name: grafana-dashboard-k8s-resources-node - configMap: name: grafana-dashboard-k8s-resources-pod name: grafana-dashboard-k8s-resources-pod - configMap: name: grafana-dashboard-k8s-resources-workload name: grafana-dashboard-k8s-resources-workload - configMap: name: grafana-dashboard-k8s-resources-workloads-namespace name: grafana-dashboard-k8s-resources-workloads-namespace - configMap: name: grafana-dashboard-kubelet name: grafana-dashboard-kubelet - configMap: name: grafana-dashboard-namespace-by-pod name: grafana-dashboard-namespace-by-pod - configMap: name: grafana-dashboard-namespace-by-workload name: grafana-dashboard-namespace-by-workload - configMap: name: grafana-dashboard-node-cluster-rsrc-use name: grafana-dashboard-node-cluster-rsrc-use - configMap: name: grafana-dashboard-node-rsrc-use name: grafana-dashboard-node-rsrc-use - configMap: name: grafana-dashboard-nodes name: grafana-dashboard-nodes - configMap: name: grafana-dashboard-persistentvolumesusage name: grafana-dashboard-persistentvolumesusage - configMap: name: grafana-dashboard-pod-total name: grafana-dashboard-pod-total - configMap: name: grafana-dashboard-prometheus-remote-write name: grafana-dashboard-prometheus-remote-write - configMap: name: grafana-dashboard-prometheus name: grafana-dashboard-prometheus - configMap: name: grafana-dashboard-proxy name: grafana-dashboard-proxy - configMap: name: grafana-dashboard-scheduler name: grafana-dashboard-scheduler - configMap: name: grafana-dashboard-statefulset name: grafana-dashboard-statefulset - configMap: name: grafana-dashboard-workload-total name: grafana-dashboard-workload-total 第二个办法就是删除 Prometheus Operator 自带的 Grafana，自己通过 Helm 或者 manifest 部署不使用 Provisioning 的 Grafana。\n如果你既不想删除 Provisioning 的配置，也不想自己部署 Grafana，那只能使用上文提到的低级方案了。\n","date":"2020年12月2日","externalUrl":null,"permalink":"/posts/how-to-back-up-all-of-your-grafana-dashboards/","section":"博客","summary":"目前我们 k8s 集群的 Grafana 使用 ceph 作为持久化存储，一但我将 Grafana 的 Deployment 删除重","title":"Grafana 备份恢复教程","type":"posts"},{"content":"对于大部分 Kubernetes 用户来说，安全是无关紧要的，或者说没那么紧要，就算考虑到了，也只是敷衍一下，草草了事。实际上 Kubernetes 提供了非常多的选项可以大大提高应用的安全性，只要用好了这些选项，就可以将绝大部分的攻击抵挡在门外。为了更容易上手，我将它们总结成了几个最佳实践配置，大家看完了就可以开干了。当然，本文所述的最佳安全实践仅限于 Pod 层面，也就是容器层面，于容器的生命周期相关，至于容器之外的安全配置（比如操作系统啦、k8s 组件啦），以后有机会再唠。\n1. 为容器配置 Security Context # 大部分情况下容器不需要太多的权限，我们可以通过 Security Context 限定容器的权限和访问控制，只需加上 SecurityContext 字段：\napiVersion: v1 kind: Pod metadata: name: \u0026lt;Pod name\u0026gt; spec: containers: - name: \u0026lt;container name\u0026gt; image: \u0026lt;image\u0026gt; + securityContext: 2. 禁用 allowPrivilegeEscalation # allowPrivilegeEscalation=true 表示容器的任何子进程都可以获得比父进程更多的权限。最好将其设置为 false，以确保 RunAsUser 命令不能绕过其现有的权限集。\napiVersion: v1 kind: Pod metadata: name: \u0026lt;Pod name\u0026gt; spec: containers: - name: \u0026lt;container name\u0026gt; image: \u0026lt;image\u0026gt; securityContext: + allowPrivilegeEscalation: false 3. 不要使用 root 用户 # 为了防止来自容器内的提权攻击，最好不要使用 root 用户运行容器内的应用。UID 设置大一点，尽量大于 3000。\napiVersion: v1 kind: Pod metadata: name: \u0026lt;name\u0026gt; spec: securityContext: + runAsUser: \u0026lt;UID higher than 1000\u0026gt; + runAsGroup: \u0026lt;UID higher than 3000\u0026gt; 4. 限制 CPU 和内存资源 # 这个就不用多说了吧，requests 和 limits 都加上。\n5. 不必挂载 Service Account Token # ServiceAccount 为 Pod 中运行的进程提供身份标识，怎么标识呢？当然是通过 Token 啦，有了 Token，就防止假冒伪劣进程。如果你的应用不需要这个身份标识，可以不必挂载：\napiVersion: v1 kind: Pod metadata: name: \u0026lt;name\u0026gt; spec: + automountServiceAccountToken: false 6. 确保 seccomp 设置正确 # 对于 Linux 来说，用户层一切资源相关操作都需要通过系统调用来完成，那么只要对系统调用进行某种操作，用户层的程序就翻不起什么风浪，即使是恶意程序也就只能在自己进程内存空间那一分田地晃悠，进程一终止它也如风消散了。seccomp（secure computing mode）就是一种限制系统调用的安全机制，可以可以指定允许那些系统调用。\n对于 Kubernetes 来说，大多数容器运行时都提供一组允许或不允许的默认系统调用。通过使用 runtime/default 注释或将 Pod 或容器的安全上下文中的 seccomp 类型设置为 RuntimeDefault，可以轻松地在 Kubernetes 中应用默认值。\napiVersion: v1 kind: Pod metadata: name: \u0026lt;name\u0026gt; annotations: + seccomp.security.alpha.kubernetes.io/pod: \u0026#34;runtime/default\u0026#34; 默认的 seccomp 配置文件应该为大多数工作负载提供足够的权限。如果你有更多的需求，可以自定义配置文件。\n7. 限制容器的 capabilities # 容器依赖于传统的Unix安全模型，通过控制资源所属用户和组的权限，来达到对资源的权限控制。以 root 身份运行的容器拥有的权限远远超过其工作负载的要求，一旦发生泄露，攻击者可以利用这些权限进一步对网络进行攻击。\n默认情况下，使用 Docker 作为容器运行时，会启用 NET_RAW capability，这可能会被恶意攻击者进行滥用。因此，建议至少定义一个PodSecurityPolicy(PSP)，以防止具有 NET_RAW 功能的容器启动。\n通过限制容器的 capabilities，可以确保受攻击的容器无法为攻击者提供横向攻击的有效路径，从而缩小攻击范围。\napiVersion: v1 kind: Pod metadata: name: \u0026lt;name\u0026gt; spec: securityContext: + runAsNonRoot: true + runAsUser: \u0026lt;specific user\u0026gt; capabilities: drop: + -NET_RAW + -ALL 如果你对 Linux capabilities 这个词一脸懵逼，建议去看看我的脑残入门系列：\nLinux Capabilities 入门教程：概念篇 Linux Capabilities 入门教程：基础实战篇 Linux Capabilities 入门教程：进阶实战篇 8. 只读 # 如果容器不需要对根文件系统进行写入操作，最好以只读方式加载容器的根文件系统，可以进一步限制攻击者的手脚。\napiVersion: v1 kind: Pod metadata: name: \u0026lt;Pod name\u0026gt; spec: containers: - name: \u0026lt;container name\u0026gt; image: \u0026lt;image\u0026gt; securityContext: + readOnlyRootFilesystem: true 9 总结 # 总之，Kubernetes 提供了非常多的选项来增强集群的安全性，没有一个放之四海而皆准的解决方案，所以需要对这些选项非常熟悉，以及了解它们是如何增强应用程序的安全性，才能使集群更加稳定安全。\n最后，请记住：你需要万分小心你的 YAML 文件内容缩进，如果你的 YAML 文件非常多，眼睛看花了，希望下面的神器可以助你一臂之力：\n","date":"2020年11月26日","externalUrl":null,"permalink":"/posts/security-best-practices-for-kubernetes-pods/","section":"博客","summary":"对于大部分 Kubernetes 用户来说，安全是无关紧要的，或者说没那么紧要，就","title":"Kubernetes 最佳安全实践指南","type":"posts"},{"content":"WireGuard 的安装和使用条件非常苛刻，对内核版本要求极高，不仅如此，在不同的系统中，内核，内核源码包，内核头文件必须存在且这三者版本要一致。所以一般不建议在生成环境中安装，除非你对自己的操作很有把握。Red Hat、CentOS、Fedora 等系统的内核，内核源码包，内核头文件包名分别为 kernel、kernel-devel、kernel-headers，Debian、Ubuntu 等系统的内核，内核源码包，内核头文件包名分别为 kernel、linux-headers。\n果这三者任一条件不满足的话，则不管是从代码编译安装还是从 repository 直接安装，也只是安装了 wireguard-tools 而已。而 WireGuard 真正工作的部分，是 wireguard-dkms，也就是动态内核模块支持(DKMS)，是它将 WireGuard 编译到系统内核中。因此，在某些 VPS 商家，是需要你先自主更换系统内核，并事先将这三者安装好，才有可能不会出现编译或安装失败。\n当然，目前 WireGuard 已经被合并到 Linux 5.6 内核中了，如果你的内核版本 \u0026gt;= 5.6，就可以用上原生的 WireGuard 了，只需要安装 wireguard-tools 即可。例如，对于 Ubuntu 20.04 来说，它的内核版本是 5.4，虽然小于 5.6，但经过我的测试发现它已经将 WireGuard 合并到了内核中，我们只需要安装 wireguard-tools 即可：\n$ sudo apt install wireguard -y 下面讨论 WireGuard 在低版本内核中的安装方法。\n1. 升级内核 # 对于 Ubuntu 等 apt 系的发行版来说，不需要升级内核即可安装 WireGuard，可以略过此步骤。\n如果你使用的是 CentOS 等 rpm 系的发行版，必须要升级内核，步骤如下：\n① 载入公钥\n$ rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org ② 升级安装 elrepo\n$ rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm ③ 载入 elrepo-kernel 元数据\n$ yum --disablerepo=\\* --enablerepo=elrepo-kernel repolist ④ 安装最新版本的内核\n$ yum --disablerepo=\\* --enablerepo=elrepo-kernel install kernel-ml.x86_64 -y ⑤ 删除旧版本工具包\n$ yum remove kernel-tools-libs.x86_64 kernel-tools.x86_64 -y ⑥ 安装新版本工具包\n$ yum --disablerepo=\\* --enablerepo=elrepo-kernel install kernel-ml-tools kernel-ml-devel kernel-ml-headers -y ⑦ 查看内核插入顺序\n$ grep \u0026#34;^menuentry\u0026#34; /boot/grub2/grub.cfg | cut -d \u0026#34;\u0026#39;\u0026#34; -f2 CentOS Linux (3.10.0-1127.10.1.el7.x86_64) 7 (Core) CentOS Linux (5.7.2-1.el7.elrepo.x86_64) 7 (Core) CentOS Linux (0-rescue-96820b9851c24560b5f942f2496b9aeb) 7 (Core) 默认新内核是从头插入，默认启动顺序也是从 0 开始。\n⑧ 查看当前实际启动顺序\n$ grub2-editenv list saved_entry=CentOS Linux (3.10.0-1127.10.1.el7.x86_64) 7 (Core) ⑨ 设置默认启动\n$ grub2-set-default \u0026#39;CentOS Linux (5.7.2-1.el7.elrepo.x86_64) 7 (Core)\u0026#39; 最后重启检查：\n$ reboot $ uname -r 2. 安装 WireGuard # 升级内核之后，就可以根据 官方文档来安装 WireGuard 了。不过这里我要介绍一个更狂野的安装方法，它更高效，也更不容易出错，那就是通过源代码编译安装。先别急着反驳，我知道从源代码编译看起来一点都不容易，但请听我说完。你以为我会教你如何从头开始编译吗？那不可能，有违我这篇文章的初衷，我要推荐一位大佬——秋水逸冰的 一键安装脚本，它可以让你哼着小曲就能从源码编译安装 WireGuard，只需一条命令即可。\n脚本的使用方法超级简单，先下载脚本，然后赋予执行权限，最后执行一条命令搞定：\n$ wget --no-check-certificate -O /opt/wireguard.sh https://raw.githubusercontent.com/teddysun/across/master/wireguard.sh $ chmod 755 /opt/wireguard.sh $ /opt/wireguard.sh -s 关于该脚本需要说明几点：\n支持两种安装方式：既支持从源代码编译安装，也支持从包管理器直接安装。 脚本会创建默认的 wg0 设备，以及 wg0 的客户端配置，并生成客户端配置对应的二维码 png 图片。 脚本会修改本机防火墙设置，如果未启用防火墙，则会出现警告提示，需要手动去设置。 脚本会从 1024 到 20480 随机生成监听端口。 脚本支持新增，删除，列出客户端功能。 脚本支持查看已安装的 WireGuard 的版本号。 脚本支持从代码编译安装的方式升级 WireGuard 到当前最新版本。 对于咱手艺人来说，肯定是不想用它自动生成的配置的，如果你想自己生成配置文件，请直接将配置文件目录清空：\n$ rm -rf /etc/wireguard/* 然后手动生成秘钥和配置文件，具体的流程请参考： WireGuard 的搭建使用与配置详解。\n如果你想通过 Web UI 来管理 WireGuard 的配置文件，可以看看这个项目： Wg Gen Web\n最后，公众号后台回复 wg 即可获取一键安装脚本。\n参考文档 # WireGuard 一键安装脚本 ","date":"2020年11月18日","externalUrl":null,"permalink":"/posts/wireguard-install/","section":"博客","summary":"WireGuard 的安装和使用条件非常苛刻，对内核版本要求极高，不仅如此，在","title":"WireGuard 快速安装教程","type":"posts"},{"content":"最近我发现我的 Kubernetes 集群资源实在是太多了，有点浪费，不信你看：\n既然闲置资源那么多，那我何不想办法利用一下。怎么用，用来干什么又是一个问题，想到我手中只有 MacBook，缺少 Windows 操作系统，那就先想办法用 Kubernetes 创建个 Windows 虚拟机用用吧，毕竟很多场景只能用 Windows（比如突破某盘的限速、Xshell 一把梭连接所有服务器）。于是我将目光转向了 Kubevirt。\nKubevirt 是 Red Hat 开源的以容器方式运行虚拟机的项目，通过 CRD 的方式来管理虚拟机实例，它的所有概念都和一般的 Kubernetes 容器应用差不多，不需要增加学习成本，对于咱玩烂了容器的 YAML 工程师来说没有任何压力，我们可以直接用它来创建虚拟机啊。\n1. Kubevirt 架构设计 # Kubevirt 主要实现了下面几种资源，以实现对虚拟机的管理：\nVirtualMachineInstance（VMI） : 类似于 kubernetes Pod，是管理虚拟机的最小资源。一个 VirtualMachineInstance 对象即表示一台正在运行的虚拟机实例，包含一个虚拟机所需要的各种配置。 VirtualMachine（VM） : 为群集内的 VirtualMachineInstance 提供管理功能，例如开机/关机/重启虚拟机，确保虚拟机实例的启动状态，与虚拟机实例是 1:1 的关系，类似与 spec.replica 为 1 的 StatefulSet。 VirtualMachineInstanceReplicaSet : 类似 ReplicaSet，可以启动指定数量的 VirtualMachineInstance，并且保证指定数量的 VirtualMachineInstance 运行，可以配置 HPA。 Kubevirt 的整体架构如图：\nvirt-api : 负责提供一些 KubeVirt 特有的 api，像是 console, vnc, startvm, stopvm 等。 virt-controller : 管理和监控 VMI 对象及其关联的 Pod，对其状态进行更新。 virt-handler : 以 DaemonSet 运行在每一个节点上，监听 VMI 的状态向上汇报，管理 VMI 的生命周期。 virt-launcher : 以 Pod 方式运行，每个 VMI Object 都会对应一个 virt-launcher Pod，容器内有单独的 libvirtd，用于启动和管理虚拟机。 如果你嫌上面的架构图太繁琐，这里还有一个简化版：\n这个图里的 Agent 其实就是 virt-handler。\n2. 磁盘和卷 # 虚拟机镜像（磁盘）是启动虚拟机必不可少的部分，KubeVirt 中提供多种方式的虚拟机磁盘，虚拟机镜像（磁盘）使用方式非常灵活。这里列出几种比较常用的：\nPersistentVolumeClaim : 使用 PVC 做为后端存储，适用于数据持久化，即在虚拟机重启或者重建后数据依旧存在。使用的 PV 类型可以是 block 和 filesystem，使用 filesystem 时，会使用 PVC 上的 /disk.img，格式为 RAW 格式的文件作为硬盘。block 模式时，使用 block volume 直接作为原始块设备提供给虚拟机。 ephemeral : 基于后端存储在本地做一个写时复制（COW）镜像层，所有的写入都在本地存储的镜像中，VM 实例停止时写入层就被删除，后端存储上的镜像不变化。 containerDisk : 基于 scratch 构建的一个 docker image，镜像中包含虚拟机启动所需要的虚拟机镜像，可以将该 docker image push 到 registry，使用时从 registry 拉取镜像，直接使用 containerDisk 作为 VMI 磁盘，数据是无法持久化的。 hostDisk : 使用节点上的磁盘镜像，类似于 hostpath，也可以在初始化时创建空的镜像。 dataVolume : 提供在虚拟机启动流程中自动将虚拟机磁盘导入 pvc 的功能，在不使用 DataVolume 的情况下，用户必须先准备带有磁盘映像的 pvc，然后再将其分配给 VM 或 VMI。dataVolume 拉取镜像的来源可以时 http，对象存储，另一块 PVC 等。 3. 准备工作 # 在安装 Kubevirt 之前，需要做一些准备工作。先安装 libvrt 和 qemu 软件包：\n# Ubuntu $ apt install -y qemu-kvm libvirt-bin bridge-utils virt-manager # CentOS $ yum install -y qemu-kvm libvirt virt-install bridge-utils 查看节点是否支持 kvm 硬件辅助虚拟化\n$ virt-host-validate qemu QEMU: Checking for hardware virtualization : PASS QEMU: Checking if device /dev/kvm exists : PASS QEMU: Checking if device /dev/kvm is accessible : PASS QEMU: Checking if device /dev/vhost-net exists : PASS QEMU: Checking if device /dev/net/tun exists : PASS QEMU: Checking for cgroup \u0026#39;memory\u0026#39; controller support : PASS QEMU: Checking for cgroup \u0026#39;memory\u0026#39; controller mount-point : PASS QEMU: Checking for cgroup \u0026#39;cpu\u0026#39; controller support : PASS QEMU: Checking for cgroup \u0026#39;cpu\u0026#39; controller mount-point : PASS QEMU: Checking for cgroup \u0026#39;cpuacct\u0026#39; controller support : PASS QEMU: Checking for cgroup \u0026#39;cpuacct\u0026#39; controller mount-point : PASS QEMU: Checking for cgroup \u0026#39;cpuset\u0026#39; controller support : PASS QEMU: Checking for cgroup \u0026#39;cpuset\u0026#39; controller mount-point : PASS QEMU: Checking for cgroup \u0026#39;devices\u0026#39; controller support : PASS QEMU: Checking for cgroup \u0026#39;devices\u0026#39; controller mount-point : PASS QEMU: Checking for cgroup \u0026#39;blkio\u0026#39; controller support : PASS QEMU: Checking for cgroup \u0026#39;blkio\u0026#39; controller mount-point : PASS QEMU: Checking for device assignment IOMMU support : PASS QEMU: Checking if IOMMU is enabled by kernel : PASS 如果不支持，则先生成让 Kubevirt 使用软件虚拟化的配置：\n$ kubectl create namespace kubevirt $ kubectl create configmap -n kubevirt kubevirt-config \\ --from-literal debug.useEmulation=true 4. 安装 Kubevirt # 部署最新版本的 Kubevirt # $ export VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases | grep tag_name | grep -v -- \u0026#39;-rc\u0026#39; | head -1 | awk -F\u0026#39;: \u0026#39; \u0026#39;{print $2}\u0026#39; | sed \u0026#39;s/,//\u0026#39; | xargs) $ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-operator.yaml $ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-cr.yaml 查看部署结果：\n$ kubectl -n kubevirt get pod NAME READY STATUS RESTARTS AGE virt-api-64999f7bf5-n9kcl 1/1 Running 0 6d virt-api-64999f7bf5-st5qv 1/1 Running 0 6d8h virt-controller-8696ccdf44-v5wnq 1/1 Running 0 6d virt-controller-8696ccdf44-vjvsw 1/1 Running 0 6d8h virt-handler-85rdn 1/1 Running 3 7d19h virt-handler-bpgzp 1/1 Running 21 7d19h virt-handler-d55c7 1/1 Running 1 7d19h virt-operator-78fbcdfdf4-sf5dv 1/1 Running 0 6d8h virt-operator-78fbcdfdf4-zf9qr 1/1 Running 0 6d 部署 CDI # Containerized Data Importer（CDI）项目提供了用于使 PVC 作为 KubeVirt VM 磁盘的功能。建议同时部署 CDI：\n$ export VERSION=$(curl -s https://github.com/kubevirt/containerized-data-importer/releases/latest | grep -o \u0026#34;v[0-9]\\.[0-9]*\\.[0-9]*\u0026#34;) $ kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/$VERSION/cdi-operator.yaml $ kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/$VERSION/cdi-cr.yaml 5. 客户端准备 # Kubevirt 提供了一个命令行工具 virtctl，可以直接下载：\n$ export VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases | grep tag_name | grep -v -- \u0026#39;-rc\u0026#39; | head -1 | awk -F\u0026#39;: \u0026#39; \u0026#39;{print $2}\u0026#39; | sed \u0026#39;s/,//\u0026#39; | xargs) $ curl -L -o /usr/local/bin/virtctl https://github.com/kubevirt/kubevirt/releases/download/$VERSION/virtctl-$VERSION-linux-amd64 $ chmod +x /usr/local/bin/virtctl 也可以通过 krew 安装为 kubectl 的插件：\n$ kubectl krew install virt 6. 虚拟机镜像准备 # Windows 镜像下载 # 这里推荐两个 Windows 镜像下载站：\n① MSDN I Tell You。该网站提供的链接是 ed2k 格式，需要通过特殊下载工具进行下载，比如百度网盘离线下载、迅雷、eMule 等，其中百度网盘离线下载最好使，但下载限速又是个大问题，开了超级会员的当我没说。\n② TechBench by WZT。该网站提供的是直链下载方式，可以用任意下载工具进行下载，比上面的网站方便多了，不过资源没有上面的网站丰富。\n我推荐通过第二个网站来下载 Windows 镜像。\n上传镜像 # KubeVirt 可以使用 PVC 作为后端磁盘，使用 filesystem 类型的 PVC 时，默认使用的时 /disk.img 这个镜像，用户可以将镜像上传到 PVC，在创建 VMI 时使用此 PVC。使用这种方式需要注意下面几点：\n一个 PVC 只允许存在一个镜像，只允许一个 VMI 使用，要创建多个 VMI，需要上传多次 /disk.img 的格式必须是 RAW 格式 CDI 提供了使用使用 PVC 作为虚拟机磁盘的方案，在虚拟机启动前通过下面方式填充 PVC：\n通过 URL 导入虚拟机镜像到 PVC，URL 可以是 http 链接，s3 链接 Clone 一个已经存在的 PVC 通过 container registry 导入虚拟机磁盘到 PVC，需要结合 ContainerDisk 使用 通过客户端上传本地镜像到 PVC 通过命令行 virtctl，结合 CDI 项目，可以上传本地镜像到 PVC 上，支持的镜像格式有：\n.img .qcow2 .iso 压缩为 .tar，.gz，.xz 格式的上述镜像 我们的目标是安装 Windows 10 虚拟机，所以需要将上面下载好的 Windows 镜像上传到 PVC：\n$ virtctl image-upload \\ --image-path=\u0026#39;Win10_20H2_Chinese(Simplified)_x64.iso\u0026#39; \\ --storage-class csi-rbd-sc \\ --pvc-name=iso-win10 \\ --pvc-size=7G \\ --uploadproxy-url=https://\u0026lt;cdi-uploadproxy_svc_ip\u0026gt; \\ --insecure \\ --wait-secs=240 PersistentVolumeClaim default/iso-win10 created Waiting for PVC iso-win10 upload pod to be ready... Pod now ready Uploading data to https://10.111.29.156 5.63 GiB / 5.63 GiB [======================================================================================================================================================] 100.00% 27s Uploading data completed successfully, waiting for processing to complete, you can hit ctrl-c without interrupting the progress Processing completed successfully Uploading Win10_20H2_Chinese(Simplified)_x64.iso completed successfully 参数解释：\n\u0026ndash;image-path : 操作系统镜像地址。 \u0026ndash;pvc-name : 指定存储操作系统镜像的 PVC，这个 PVC 不需要提前准备好，镜像上传过程中会自动创建。 \u0026ndash;pvc-size : PVC 大小，根据操作系统镜像大小来设定，一般略大一个 G 就行。 \u0026ndash;uploadproxy-url : cdi-uploadproxy 的 Service IP，可以通过命令 kubectl -n cdi get svc -l cdi.kubevirt.io=cdi-uploadproxy 来查看。 7. 增加 hostDisk 支持 # Kubevirt 默认没有开启对 hostDisk 的支持，需要手动开启。步骤也很简单，只需新建个 ConfigMap，增加 hostDisk 的特性：\nkubevet-config.yaml\napiVersion: v1 data: feature-gates: LiveMigration,DataVolumes,HostDisk kind: ConfigMap metadata: labels: kubevirt.io: \u0026#34;\u0026#34; name: kubevirt-config namespace: kubevirt 7. 创建虚拟机 # 创建 Windows 虚拟机的模板文件如下：\nwin10.yaml\napiVersion: kubevirt.io/v1alpha3 kind: VirtualMachine metadata: name: win10 spec: running: false template: metadata: labels: kubevirt.io/domain: win10 spec: domain: cpu: cores: 4 devices: disks: - bootOrder: 1 cdrom: bus: sata name: cdromiso - disk: bus: virtio name: harddrive - cdrom: bus: sata name: virtiocontainerdisk interfaces: - masquerade: {} model: e1000 name: default machine: type: q35 resources: requests: memory: 16G networks: - name: default pod: {} volumes: - name: cdromiso persistentVolumeClaim: claimName: iso-win10 - name: harddrive hostDisk: capacity: 50Gi path: /data/disk.img type: DiskOrCreate - containerDisk: image: kubevirt/virtio-container-disk name: virtiocontainerdisk 这里用到了 3 个 Volume：\ncdromiso : 提供操作系统安装镜像，即上文上传镜像后生成的 PVC iso-win10。 harddrive : 虚拟机使用的磁盘，即操作系统就会安装在该磁盘上。这里选择 hostDisk 直接挂载到宿主机以提升性能，如果使用分布式存储则体验非常不好。 containerDisk : 由于 Windows 默认无法识别 raw 格式的磁盘，所以需要安装 virtio 驱动。 containerDisk 可以将打包好 virtio 驱动的容器镜像挂载到虚拟机中。 关于网络部分，spec.template.spec.networks 定义了一个网络叫 default，这里表示使用 Kubernetes 默认的 CNI。spec.template.spec.domain.devices.interfaces 选择定义的网络 default，并开启 masquerade，以使用网络地址转换 (NAT) 来通过 Linux 网桥将虚拟机连接至 Pod 网络后端。\n使用模板文件创建虚拟机：\n$ kubectl apply -f win10.yaml 启动虚拟机实例：\n$ virtctl start win10 # 如果 virtctl 安装为 kubectl 的插件，命令格式如下： $ kubectl virt start win10 查看实例运行状态：\n$ kubectl get pod NAME READY STATUS RESTARTS AGE virt-launcher-win10-s742j 2/2 Running 0 15s 然后就可以通过 VNC 工具来访问 Windows 虚拟机了。首先需要在本地安装一个 VNC 客户端，对于 macOS 来说，可以安装 Tiger VNC 或者 Real VNC。我选择安装 Real VNC：\n$ brew cask install vnc-viewer 连接到 Windows 虚拟机：\n$ virtctl vnc win10 # 如果 virtctl 安装为 kubectl 的插件，命令格式如下： $ kubectl virt vnc win10 执行完上面的命令后，就会打开本地的 VNC 客户端连接到虚拟机：\n下面就是安装正常的安装步骤往下进行，到选择硬盘那一步的时候，你会发现没有一个硬盘可供使用，这时就需要安装 virtio 驱动了。\n不过不用担心，virtio 驱动已经被挂载进来了，直接点击加载驱动程序就可以安装驱动了：\n安装好驱动后，硬盘就能正确显示了：\n下面就可以继续安装了。\n安装成功后会自动重启进行初始化设置，那个熟悉的“海内存知己，天涯若比邻”又回来了：\n设置完成后，进入系统，打开设备管理器，可以看到有几个未配置的设备。选择其中一个右键单击，然后选择“更新驱动程序”。\n选择“浏览我的电脑以查找驱动程序”。\n选择“CD 驱动器（E:）virtio-win-0.1.1”，然后点击确定。\n设备管理器将自动找到正确的驱动程序，不需要指定驱动程序的路径。\n在提示符下，单击“安装”。\n其他的设备驱动可以复制上面的步骤一一安装。\n8. CNI 插件问题解决 # 如果你的 Kubernetes 集群 CNI 插件用的是 Calico，这里会遇到虚拟机无法联网的问题。因为 Calico 默认禁用了容器的 ip forward 功能，而 masquerade 需要开启这个功能才能生效。\n我们只需要修改 Calico 的 ConfigMap 就可以启用容器的 ip forward 功能了，执行以下命令打开 configmap calico-config：\n$ kubectl -n kube-system edit cm calico-config 在 CNI 配置文件中加上以下的内容：\n\u0026#34;container_settings\u0026#34;: { \u0026#34;allow_ip_forwarding\u0026#34;: true }, 修改完的配置文件内容：\ncni_network_config: |- { \u0026#34;name\u0026#34;: \u0026#34;k8s-pod-network\u0026#34;, \u0026#34;cniVersion\u0026#34;: \u0026#34;0.3.1\u0026#34;, \u0026#34;plugins\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;calico\u0026#34;, \u0026#34;log_level\u0026#34;: \u0026#34;info\u0026#34;, \u0026#34;log_file_path\u0026#34;: \u0026#34;/var/log/calico/cni/cni.log\u0026#34;, \u0026#34;etcd_endpoints\u0026#34;: \u0026#34;__ETCD_ENDPOINTS__\u0026#34;, \u0026#34;etcd_key_file\u0026#34;: \u0026#34;__ETCD_KEY_FILE__\u0026#34;, \u0026#34;etcd_cert_file\u0026#34;: \u0026#34;__ETCD_CERT_FILE__\u0026#34;, \u0026#34;etcd_ca_cert_file\u0026#34;: \u0026#34;__ETCD_CA_CERT_FILE__\u0026#34;, \u0026#34;mtu\u0026#34;: __CNI_MTU__, \u0026#34;ipam\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;calico-ipam\u0026#34; }, \u0026#34;container_settings\u0026#34;: { \u0026#34;allow_ip_forwarding\u0026#34;: true }, \u0026#34;policy\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;k8s\u0026#34; }, \u0026#34;kubernetes\u0026#34;: { \u0026#34;kubeconfig\u0026#34;: \u0026#34;__KUBECONFIG_FILEPATH__\u0026#34; } }, { \u0026#34;type\u0026#34;: \u0026#34;portmap\u0026#34;, \u0026#34;snat\u0026#34;: true, \u0026#34;capabilities\u0026#34;: {\u0026#34;portMappings\u0026#34;: true} }, { \u0026#34;type\u0026#34;: \u0026#34;bandwidth\u0026#34;, \u0026#34;capabilities\u0026#34;: {\u0026#34;bandwidth\u0026#34;: true} } ] } 然后重启 calico-node 容器：\n$ kubectl -n kube-system delete pod -l k8s-app=calico-node 8. 远程连接 # 在系统未安装好之前，只能用 VNC 来远程控制，但 VNC 的体验实在让人难受。现在系统装好了，就可以使用 Windows 的远程连接协议 RDP（Remote Desktop Protocol） 了。选择开始 \u0026gt;设置 \u0026gt;系统\u0026gt;远程桌面，打开启用远程桌面就好了。\n现在可以通过 telnet 来测试一下 RDP 端口（3389）的连通性：\n$ kubectl get pod -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES virt-launcher-win10-s742j 2/2 Running 0 139m 100.92.235.131 k8s03 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; $ telnet 100.92.235.131 3389 Trying 100.92.235.131... Connected to 100.92.235.131. Escape character is \u0026#39;^]\u0026#39;. 如果你的本地电脑能够直连 Pod IP 和 SVC IP，现在就可以直接通过 RDP 客户端来远程连接 Windows 了。如果你的本地电脑不能直连 Pod IP 和 SVC IP，但可以直连 Kubernetes 集群的 Node IP，可以通过 NodePort 来暴露 RDP 端口。具体操作是创建一个 Service，类型为 NodePort：\n$ kubectl virt expose vm win10 --name win10-rdp --port 3389 --target-port 3389 --type NodePort $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 17d win10-rdp NodePort 10.98.20.203 \u0026lt;none\u0026gt; 3389:31192/TCP 20m 然后就可以通过 Node IP 来远程连接 Windows 了。\n如果你的本地操作系统是 Windows 10，可以在任务栏的搜索框中，键入“远程桌面连接”，然后选择“远程桌面连接”。在“远程桌面连接”中，键入你想要连接的电脑的名称（从步骤 1），然后选择“连接”。\n如果你的本地操作系统是 macOS，需要在 App Store 中安装 Microsoft Remote Desktop。\n安装完之后打开应用，选择 Add PC：\n在 PC name 一栏中输入 NodeIP+NodePort，然后点击 Add。\n然后右击创建好的配置，选择 Connect：\n输入账号密码后就可以连接到 Windows 了。\n全屏之后就可以获得完美的远程桌面体验了，尽情玩耍吧！\n9. 参考 # 在 Kubernetes 上使用 KubeVirt 管理虚拟机负载 kubevirt-crc-windows-tutorial kubevirt user guide ","date":"2020年11月13日","externalUrl":null,"permalink":"/posts/use-kubevirt-to-manage-windows-on-kubernetes/","section":"博客","summary":"最近我发现我的 Kubernetes 集群资源实在是太多了，有点浪费，不信你看： 既","title":"Kubernetes 使用 Kubevirt 运行管理 Windows 10 操作系统","type":"posts"},{"content":"","date":"2020年11月13日","externalUrl":null,"permalink":"/tags/kubevirt/","section":"标签","summary":"","title":"Kubevirt","type":"tags"},{"content":"","date":"2020年11月8日","externalUrl":null,"permalink":"/tags/hyperkit/","section":"标签","summary":"","title":"Hyperkit","type":"tags"},{"content":"","date":"2020年11月8日","externalUrl":null,"permalink":"/tags/podman/","section":"标签","summary":"","title":"Podman","type":"tags"},{"content":"Podman 是一个无守护程序与 Docker 命令兼容的下一代 Linux 容器工具，该项目由 RedHat 主导，其他的细节可以参考 Podman 使用指南，本文的重点不是这个。\nPodman 一直以来只能跑在 Linux 系统上，macOS 和 Windows 只能通过 CLI 远程连接 Podman 的 API 来管理容器。事实上 Docker 也不支持 macOS 和 Windows，但 Docker 针对 Windows 和 macOS 推出了专门的客户端，客户端里面集成了虚拟化相关的设置，通过嵌套一层虚拟化来支持 Docker。对于 Podman 来说，想要在 macOS 上运行也只能通过虚拟化来实现，网上也有不少方案，基本上都是通过 Virtualbox 来实现，都不太优雅。本文将介绍一种相对更优雅的方案，虽然不是很完美，但我已经尽力做到接近完美了。。\nHyperKit 介绍 # HyperKit 是一个具有 hyperisor 能力的轻量级虚拟化工具集，包含了基于 xhyve（The BSD Hypervisor）的完整 hypervisor。HyperKit 设计成上层组件诸如 VPNKit 和 DataKit 的接口。xhyve 是 基于 bhyve 的 Mac OS X 移植版本，而 bhyve 又是 FreeBSD 下的虚拟化技术。。。\n我们知道，Docker 在 Linux 上利用了 Linux 原生支持的容器方式实现资源和环境的隔离，直接利用宿主内核，性能接近原生。然而，在 macOS 上却仍然需要虚拟化的技术。早期的 Docker 干脆直接在开源的 VirtualBox 中构建虚拟机，性能低下。后期的 Docker 基于轻量化的虚拟化框架 HyperKit 开发，据说性能得到很大提升。\n本文将介绍如何通过 HyperKit 来使用 Podman。方法也很简单，先通过 Hyperkit 创建一个轻量级虚拟机，然后在虚拟机中安装 Podman，并开启 remote API，最后在本地通过 CLI 连接虚拟机中的 Podman。这和 macOS 中的 Docker 实现原理是一样的，只不过 Podman 是没有 Daemon 的，与 Docker 相比可以节省不少资源。\n2. 安装 HyperKit # 你可以自己下载源代码编译 HyperKit，但我不建议这么做，不同的 macOS 版本会遇到各种各样的错误。我这里推荐两种超级简单的方法：\n直接通过安装 Docker 来获得 HyperKit，因为 Docker Desktop on Mac 就是基于 HyperKit 实现的，所以安装 Docker Desktop on Mac 就能够获得完整的 HyperKit 运行环境。整个过程会非常顺畅和简单。安装完 Docker 之后可以永远不用打开 Docker，直接使用 HyperKit 就好。或者你可以直接卸载 Docker，卸载之前先把 hyperkit 二进制文件备份出来，因为卸载 Docker 也会删掉 hyperkit 二进制文件。\n直接通过安装 Multipass 来获得 HyperKit。Multipass 是 Canonical 公司（Ubuntu）开发的基于不同操作系统内建原生 Hypervisor 实现的工作站。由于 Windows(Hyper-V)，macOS（hyperkit）和 Linux（KVM）都原生支持 hypervisor，这样通过 multipass shell 命令就能够在一个 shell 中实现创建运行 Ubuntu 虚拟机。在 macOS 平台，默认的后端是 hyperkit，需要 macOS Yosemite (10.10.3) 以上版本并且需要安装在 2010 以后生产的 Mac 设备。安装方法很简单：\n$ brew cask install multipass 安装好了之后可以在 /Library/Application Support/com.canonical.multipass/bin/ 目录下找到 hyperkit 二进制文件。\n3. 创建虚拟机 # 你可以直接通过 hyperkit 来创建虚拟机，但参数比较复杂，有兴趣的自己研究吧。我推荐直接通过 multipass 来创建，命令特别简单：\n$ multipass launch -c 2 -d 10G -m 2G -n podman -n : 指定启动实例名字 -c : 分配 CPU 数量 -d : 设置磁盘容量 -m : 设置内存容量 第一次启动虚拟机的时候会去拉去镜像，国内网速可能会很慢。\n查看已经启动的虚拟机：\n$ multipass list Name State IPv4 Image podman Running 192.168.64.2 Ubuntu 20.04 LTS 进入虚拟机：\n$ multipass shell podman Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-52-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sun Nov 8 19:30:29 CST 2020 System load: 0.0 Processes: 119 Usage of /: 13.4% of 11.46GB Users logged in: 0 Memory usage: 11% IPv4 address for enp0s2: 192.168.64.2 Swap usage: 0% 0 updates can be installed immediately. 0 of these updates are security updates. Last login: Sun Nov 8 17:38:31 2020 from 192.168.64.1 ubuntu@podman:~$ 4. 安装 Podman # 在虚拟机中安装 Podman：\nubuntu@podman:~$ . /etc/os-release ubuntu@podman:~$ echo \u0026#34;deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /\u0026#34; | ubuntu@podman:~$ sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list ubuntu@podman:~$ curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key | sudo apt-key add - ubuntu@podman:~$ sudo apt-get update ubuntu@podman:~$ sudo apt-get -y upgrade ubuntu@podman:~$ sudo apt-get -y install podman 5. 建立 Podman Socket # Podman 依赖于 systemd 的 socket activation 特性。假设 Daemon B 依赖于 Daemon A，那么它就必须等到 Daemon A 完成启动后才能启动。socket activation的思想就是：Daemon B 启动时其实并不需要 Daemon A 真正运行起来,它只需要 Daemon A 建立的 socket 处于 listen 状态就 OK 了。而这个 socket 不必由 Daemon A 建立, 而是由 systemd 在系统初始化时就建立。当 Daemon B 发起启动时发起连接，systemd 再将 Daemon A 启动，当 Daemon A 启动后，再将 socket 归还给 Daemon A。\nPodman 会通过 podman.socket 先创建一个处于监听状态的 socket 文件 /run/podman/podman.sock，当有进程向该 socket 发起连接时，systemd 会启动同名的 service：podman.service，以接管该 socket。先看看 podman.socket 和 podman.service 长啥样：\nubuntu@podman:~$ sudo systemctl cat podman.socket # /lib/systemd/system/podman.socket [Unit] Description=Podman API Socket Documentation=man:podman-system-service(1) [Socket] ListenStream=%t/podman/podman.sock SocketMode=0660 [Install] WantedBy=sockets.target ubuntu@podman:~$ sudo systemctl cat podman.service # /lib/systemd/system/podman.service [Unit] Description=Podman API Service Requires=podman.socket After=podman.socket Documentation=man:podman-system-service(1) StartLimitIntervalSec=0 [Service] Type=notify KillMode=process ExecStart=/usr/bin/podman system service 设置开机自启 podman.socket，并立即启动：\nubuntu@podman:~$ sudo systemctl enable podman.socket --now 确认 socket 是否正处于监听状态：\nubuntu@podman:~$ podman --remote info host: arch: amd64 buildahVersion: 1.16.1 cgroupManager: systemd cgroupVersion: v1 conmon: package: \u0026#39;conmon: /usr/libexec/podman/conmon\u0026#39; path: /usr/libexec/podman/conmon version: \u0026#39;conmon version 2.0.20, commit: \u0026#39; cpus: 2 ... 3. 客户端 CLI 设置 # 接下来所有的设置，如不作特殊说明，都在 macOS 本地终端执行。\nPodman 远程连接依赖 SSH，所以需要设置免密登录，先生成秘钥文件：\n$ ssh-keygen -t rsa # 一路回车到底 然后将本地的公钥 ~/.ssh/id_rsa.pub 追加到虚拟机的 /root/.ssh/authorized_keys 文件中。\n安装 Podman CLI：\n$ brew install podman 添加远程连接：\n$ podman system connection add ubuntu --identity ~/.ssh/id_rsa ssh://root@192.168.64.2/run/podman/podman.sock 查看已经建立的连接：\n$ podman system connection list Name Identity URI podman* /Users/Ryan/.ssh/id_rsa ssh://root@192.168.64.2:22/run/podman/podman.sock 由于这是第一个连接，所以被直接设置为默认连接（podman 后面加了 *）。\n测试远程连接是否可用：\n$ podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ podman pull nginx:alpine Trying to pull docker.io/library/nginx:alpine... Getting image source signatures Copying blob sha256:188c0c94c7c576fff0792aca7ec73d67a2f7f4cb3a6e53a84559337260b36964 Copying blob sha256:9dd8e8e549988a3e2c521f27f805b7a03d909d185bb01cdb4a4029e5a6702919 Copying blob sha256:85defa007a8b33f817a5113210cca4aca6681b721d4b44dc94928c265959d7d5 Copying blob sha256:f2dc206a393cd74df3fea6d4c1d3cefe209979e8dbcceb4893ec9eadcc10bc14 Copying blob sha256:0ca72de6f95718a4bd36e45f03fffa98e53819be7e75cb8cd1bcb0705b845939 Copying config sha256:e5dcd7aa4b5e5d2df8152b9e58aba32a05edd9b269816f5d8b7ced535743d16c Writing manifest to image destination Storing signatures e5dcd7aa4b5e5d2df8152b9e58aba32a05edd9b269816f5d8b7ced535743d16c $ podman image ls REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/library/nginx alpine e5dcd7aa4b5e 2 days ago 23.3 MB 现在我们就可以直接在本地用 podman 愉快地玩耍了！\n如果你建立了多个连接，可用使用 \u0026ndash;connection 参数指定远程连接，或者使用 podman system connection default \u0026lt;NAME\u0026gt; 来设置默认的远程连接。\n最后，我们来看看 hyperkit 的内存占用：\n物理内存只占用了 921M，如果你觉得这个内存占用很多，不妨去对比下 Docker Desktop 的内存占用。\n总结 # 本文介绍了在 macOS 中使用 podman 的方法，通过 HyperKit 创建 Ubuntu 虚拟机运行 Podman，并建立 Podman Socket，然后客户端通过 SSH 连接服务端的 Socket，以实现通过远程连接来管理容器。\n","date":"2020年11月8日","externalUrl":null,"permalink":"/posts/use-podman-in-macos/","section":"博客","summary":"Podman 是一个无守护程序与 Docker 命令兼容的下一代 Linux 容器工具，该项目由 RedHat","title":"在 macOS 中使用 Podman","type":"posts"},{"content":"","date":"2020年10月19日","externalUrl":null,"permalink":"/tags/capabilities/","section":"标签","summary":"","title":"Capabilities","type":"tags"},{"content":"","date":"2020年10月19日","externalUrl":null,"permalink":"/categories/linux/","section":"分类","summary":"","title":"Linux","type":"categories"},{"content":" 原文链接： Linux Capabilities In Practice\n该系列文章总共分为三篇：\nLinux Capabilities 入门教程：概念篇 Linux Capabilities 入门教程：基础实战篇 Linux Capabilities 入门教程：进阶实战篇 Linux capabilities 非常晦涩难懂，为此我专门写了两篇文章来解释其 基本原理和 设置方法。本文将会继续研究 Linux capabilities 更高级的应用案例，并结合 Docker 和 Kubernetes 来加深理解。\n1. 快速回顾 # 如果你看过该系列教程的 第一篇，那你应该大致了解下面的计算公式：\nP\u0026rsquo;(ambient) = (file is privileged) ? 0 : P(ambient)\nP\u0026rsquo;(permitted) = (P(inheritable) \u0026amp; F(inheritable)) |\n(F(permitted) \u0026amp; P(bounding))) | P\u0026rsquo;(ambient)\nP\u0026rsquo;(effective) = F(effective) ? P\u0026rsquo;(permitted) : P\u0026rsquo;(ambient)\nP\u0026rsquo;(inheritable) = P(inheritable) [i.e., unchanged]\nP\u0026rsquo;(bounding) = P(bounding) [i.e., unchanged]\n想不起来也没关系，请回去再阅读消化一遍，然后再来看本文，不然你会跟不上我的思路。\n你还需要复习第二篇文章中的内容，了解如何通过基本的工具来设置 capabilities。如果一切准备就绪，下面我们就开始了。\n在 Ubuntu 18.04 上，以普通用户的身份运行 capsh 将会得到如下结果：\n$ capsh --print Current: = Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read Securebits: 00/0x0/1\u0026#39;b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) uid=1000(fox) gid=1000(fox) groups=4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),114(docker),1000(fox) 可以看到普通用户当前所在的 shell 进程没有任何 capabilities（即 Effective 集合为空），Bounding 集合包含了所有 capabilities。\n这个命令输出的信息比较有限，完整的信息可以查看 /proc 文件系统，比如当前 shell 进程就可以查看 /proc/$$/status。\n$ grep Cap /proc/$$/status CapInh:\t0000000000000000 CapPrm:\t0000000000000000 CapEff:\t0000000000000000 CapBnd:\t0000003fffffffff CapAmb:\t0000000000000000 输出中的 16 进制掩码表示对应集合中的 capabilities，可以使用 capsh 对其进行解码：\n$ capsh --decode=0000003fffffffff 0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read 和 capsh --print 命令输出的结果一样。\n如果是 root 用户，得到的结果和普通用户是不一样的：\n$ grep Cap /proc/$$/status CapInh:\t0000000000000000 CapPrm:\t0000003fffffffff CapEff:\t0000003fffffffff CapBnd:\t0000003fffffffff CapAmb:\t0000000000000000 所有的 capabilities 都包含在了 Permitted、Effective 和 Bounding 集合中，所以 root 用户可以执行任何内核调用。\n2. 为可执行文件分配 capabilities # 我在 上一篇文章中提到过，通过适当的配置，进程可以获取可执行文件的 Bounding 集合中的 capabilities。下面通过一个例子来加深理解。\n以 ping 这个命令为例，它的二进制文件被设置了 SUID，所以可以以 root 身份运行：\n$ which ping /bin/ping $ ls -l /bin/ping -rwsr-xr-x 1 root root 64424 Mar 9 2017 /bin/ping 更安全的机制是使用 capabilities，不过 Ubuntu 上面的 ping 没有这么做。没关系，我们可以通过 ping 的源码来自己编译，首先克隆源代码：\n$ git clone https://github.com/iputils/iputils 安装编译所需的依赖：\n$ sudo apt install -y ninja-build meson libcap-dev gettext 开始编译：\n$ cd iputils $ ./configure $ make 新编译的 ping 文件并没有设置 SUID：\n$ ls -l builddir/ping/ping -rwxrwxr-x 1 fox fox 168K Oct 19 15:26 builddir/ping/ping 也没有任何的 capabilities：\n$ getcap builddir/ping/ping 所以无法正常工作：\n$ builddir/ping/ping www.baidu.com builddir/ping/ping: socket: Operation not permitted 我们可以手动设置 capabilities：\n$ setcap \u0026#39;cap_net_raw+p\u0026#39; builddir/ping/ping unable to set CAP_SETFCAP effective capability: Operation not permitted $ sudo setcap \u0026#39;cap_net_raw+p\u0026#39; builddir/ping/ping $ getcap builddir/ping/ping builddir/ping/ping = cap_net_raw+p $ builddir/ping/ping www.baidu.com -c 1 PING www.a.shifen.com (180.101.49.12) 56(84) bytes of data. 64 bytes from 180.101.49.12 (180.101.49.12): icmp_seq=1 ttl=53 time=10.0 ms --- www.a.shifen.com ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 10.028/10.028/10.028/0.000 ms 这里再活学活用一下，为什么普通用户无法执行 setcap 呢？因为执行 setcap 的用户需要在 Permitted 集合中包含 CAP_SETFCAP capabilities，而普通用户不具备这个 capabilities，所以必须使用 root 用户。\n查看 ping 进程的 capabilities：\n$ builddir/ping/ping wwwww.baidu.com \u0026gt; /dev/null\u0026amp; [1] 9823 $ grep Cap /proc/9823/status CapInh:\t0000000000000000 CapPrm:\t0000000000002000 CapEff:\t0000000000000000 CapBnd:\t0000003fffffffff CapAmb:\t0000000000000000 $ $ capsh --decode=0000000000002000 0x0000000000002000=cap_net_raw 只有 Permitted 集合中包含了 CAP_NET_RAW capabilities，Effective 集合中并不包含，按常理 ping 是无法正常工作的。这是为啥呢？\n其实 ping 在执行过程中会将 Permitted 集合中的 CAP_NET_RAW capabilities 加入 Effective 集合中，打开 Socket 之后再将该 capabilities 从 Effective 集合中移除，所以 grep 是看不到的。其中这就是我在 第一篇文章提到的 ping 文件具有 capabilities 感知能力。可以通过 stace 跟踪系统调用来验证：\n$ sudo strace builddir/ping/ping -c 1 wwwww.baidu.com ... capget({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, NULL) = 0 capget({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, {effective=0, permitted=1\u0026lt;\u0026lt;CAP_NET_ADMIN|1\u0026lt;\u0026lt;CAP_NET_RAW, inheritable=0}) = 0 capset({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, {effective=1\u0026lt;\u0026lt;CAP_NET_RAW, permitted=1\u0026lt;\u0026lt;CAP_NET_ADMIN|1\u0026lt;\u0026lt;CAP_NET_RAW, inheritable=0}) = 0 socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = -1 EACCES (Permission denied) socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) = 3 socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6) = -1 EACCES (Permission denied) socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6) = 4 capget({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, NULL) = 0 capget({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, {effective=1\u0026lt;\u0026lt;CAP_NET_RAW, permitted=1\u0026lt;\u0026lt;CAP_NET_ADMIN|1\u0026lt;\u0026lt;CAP_NET_RAW, inheritable=0}) = 0 capset({version=_LINUX_CAPABILITY_VERSION_3, pid=0}, {effective=0, permitted=1\u0026lt;\u0026lt;CAP_NET_ADMIN|1\u0026lt;\u0026lt;CAP_NET_RAW, inheritable=0}) = 0 ... 第三行表示 CAP_NET_RAW capabilities 被添加到了 Effective 集合中，下一行试图创建一个 IPV4 ping socket，但创建失败，这是由 ping_group_range 内核配置参数导致的。然后再次尝试创建 IPV4 ping socket，这次创建成功了。IPv6 重复上面的步骤。最后将 CAP_NET_RAW capabilities 从 Effective 集合中移除。\n如果 ping 二进制文件不具备 capabilities 感知能力，即没有调用 capset 和 capget 的权限，我们就必须要开启 Effective 标志位（F(Effective)），这样就会将该 capabilities 自动添加到进程的 Effective 集合中：\n$ setcap \u0026#39;cap_net_raw+ep\u0026#39; builddir/ping/ping 不明白为什么的，再好好理解下这个公式：P'(effective) = F(effective) ? P'(permitted) : P'(ambient)。\n3. 特殊规则 # 本文不会涉及从 root 用户切换到普通用户时 capabilities 的变化，这里面的变动比较复杂，我也搞不清楚。我只知道 capsh --print 输出中的 Securebits 控制着从普通用户切换到 UID 0 或者从 UID 0 切换到普通用户时如何继承 capabilities。详细的解释可以参考 man capabilities。\n4. 构建半特权环境 # 前文中只用到了 Permitted 和 Effective 集合，下面再来聊聊 Ambient 和 Inheritable 集合。这两个集合的意义就在于可以帮助我们在进程树或 namespace 的范围内创建一个允许任意进程使用某些 capabilities 的环境。\n例如，我们可以在 Ambient 集合中加入 CAP_NET_BIND_SERVICE capabilities 来创建一个可以绑定到 80 端口的 \u0026ldquo;webserver\u0026rdquo; 环境，不需要额外的 capabilities，也不需要以 root 用户身份运行。webserver 可以通过解释器或辅助脚本启动，并且不需要给可执行文件设置 capabilities。如果不明白为什么，再看十分钟这两个公式：\nP\u0026rsquo;(ambient) = (file is privileged) ? 0 : P(ambient)\nP\u0026rsquo;(effective) = F(effective) ? P\u0026rsquo;(permitted) : P\u0026rsquo;(ambient)\n如果理解了，再往下动手实践。我用 C 写了一个简单的程序 set_ambient，核心功能是使用 cap-ng library 将 CAP_NET_BIND_SERVICE capabilities 添加到新进程的 Ambient 集合中。编译完成后，需要给二进制文件添加该 capabilities，如果它自己没有这个 capabilities，是无法将其添加到新进程中的：\n$ sudo setcap cap_net_bind_service+p set_ambient $ getcap ./set_ambient ./set_ambient = cap_net_bind_service+p 通过 set_ambient 来启动一个 bash 环境：\n$ ./set_ambient /bin/bash Starting process with CAP_NET_BIND_SERVICE in ambient $ grep Cap /proc/$BASHPID/status CapInh: 0000000000000400 CapPrm: 0000000000000400 CapEff: 0000000000000400 CapBnd: 0000003fffffffff CapAmb: 0000000000000400 $ capsh --decode=0000000000000400 0x0000000000000400=cap_net_bind_service $ exit 可以看到 CAP_NET_BIND_SERVICE capabilities 被添加到 bash 环境的 Ambient 集合中，同时也会添加到 Permitted 和 Inheritable 集合中，不明白为什么的继续看文章开头的公式。。。\n接着运行一个 Go Web 服务，并绑定到 80 端口，既不给它相应的 capabilities，也不以 root 身份运行：\n$ $ ./server 2019/09/09 13:42:06 listen tcp :80: bind: permission denied 运行失败，因为它没有绑定到小于 1024 的端口的权限。下面利用 set_ambient 创建一个 “webserver” 环境再运行试试：\n$ ./set_ambient /bin/bash Starting process with CAP_NET_BIND_SERVICE in ambient $ ./server \u0026amp; [1] 2360 $ curl localhost:80 Successfully serving on port 80 $ kill 2360 $ exit 这次运行成功了！你也可以直接执行 ./set_ambient ./server，但使用 shell 的好处是：具有 Ambient 集合中 capabilities 的 bash 环境变成了一个半特权环境，在这个环境中不仅可以运行 Web 服务，也可以运行相关脚本和程序，而这些脚本和程序又可以正常启动 webserver。\n这个方法对 Python 很有效，如果不希望给 Python 可执行文件赋予更多的 capabilities，可以使用上面的方法来实现这个目的：\n$ python3 -m http.server 80 Traceback (most recent call last): ... PermissionError: [Errno 13] Permission denied $ ./set_ambient /usr/bin/python3 -m http.server 80 Starting process with CAP_NET_BIND_SERVICE in ambient Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 最后讲一下 Inheritable 与 Ambient 集合的区别，如果想使用 Inheritable 达到上述目的，需要将 CAP_NET_BIND_SERVICE capabilities 添加到 Go web 服务可执行文件的 Inheritable 集合中，同时还需要开启 Effective 标志位。\n看起来很有道理，但有一个问题：如果可执行文件的有效用户是普通用户，且没有 Inheritable 集合，即 F(inheritable) = 0，那么 P(inheritable) 将会被忽略（P(inheritable) \u0026amp; F(inheritable)）。由于绝大多数可执行文件都是这种情况，因此 Inheritable 集合的可用性受到了限制。\n5. 容器与 capabilities # 如果你理解了上一节的内容，应该可以猜到 capabilities 和容器是相辅相成的，至少在一定程度上是这样。\n本节内容将在容器中实践 capabilities。我已经创建了一个测试镜像，并安装了 capsh 和上文所述的程序，代码在 GitHub 仓库中。如果不加任何参数直接运行容器，结果如下：\n$ docker run -it amouat/caps root@cfeb81ec0fab:/# capsh --print Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap Securebits: 00/0x0/1\u0026#39;b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) uid=0(root) gid=0(root) groups= root@cfeb81ec0fab:/# grep Cap /proc/$BASHPID/status CapInh: 00000000a80425fb CapPrm: 00000000a80425fb CapEff: 00000000a80425fb CapBnd: 00000000a80425fb CapAmb: 0000000000000000 和宿主机还是有些区别的，容器中的 root 用户并没有包含所有的 capabilities，比如 SYS_TIME。如果你可以在容器中修改系统时间，那么宿主机和其他容器中的系统时间都会被改变。\n另外需要注意的是，容器中的 Ambient 集合是空的，目前在 Docker 和 Kubernetes 中还无法配置 Ambient 集合，过在底层的 runc 运行时中是可以配置的。具体参考 Kubernetes 项目的 issue。\n如果使用指定的用户运行容器，会得到全新的结果：\n$ docker run -it --user=nobody amouat/caps $ grep Cap /proc/$BASHPID/status CapInh: 00000000a80425fb CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 00000000a80425fb CapAmb: 0000000000000000 Permitted 和 Effective 集合被清空了，这跟上文提到的特殊规则有关，从 root 用户切换到普通用户， Permitted 和 Effective 集合中的 capabilities 都会被清空。可以通过将 capabilities 添加到可执行文件的 Inheritable 集合中，同时开启 Effective 标志位来使其正常工作。amouat/caps 已经包含了一个具备此条件的可执行文件，可以用来测试一下：\n$ docker run --user nobody amouat/caps getcap /inh_server /inh_server = cap_net_bind_service+ei $ docker run -d -p 8000:80 --user nobody amouat/caps /inh_server d8f13e6990c5802e2beb6e435dd74bcae7959b94c1293349d33d9fe6c053c0fe $ curl localhost:8000 Successfully serving on port 80 要想在容器中利用 capabilities 实现一个可以正常工作的非 root 环境，需要使用上文所述的 set_ambient 程序。\n$ docker run -p 8000:80 --user nobody amouat/caps /server 2019/09/09 19:14:13 listen tcp :80: bind: permission denied $ docker run -d -p 8000:80 --user nobody amouat/caps /set_ambient /server de09fe34a623c3bf40c2eea7229696acfa8d192c19adfa4065a380a583372907 $ curl localhost:8000 Successfully serving on port 80 在容器中限制 capabilities 最简单最常见的方法是 --cap-drop 和 --cap-add 参数，这些参数只会影响所有用户的 Bounding 集合，包括 root 用户。安全的做法是移除所有的 capabilities，只添加需要的 capabilities，例如：\n$ docker run --cap-drop all --cap-add NET_BIND_SERVICE -it amouat/caps capsh --print Current: = cap_net_bind_service+eip Bounding set =cap_net_bind_service Securebits: 00/0x0/1\u0026#39;b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) uid=0(root) gid=0(root) groups= 然后就可以以 root 身份或普通用户身份运行容器，例如：\n$ docker run --cap-drop all --cap-add NET_BIND_SERVICE \\ -d -p 8000:80 --user nobody amouat/caps /set_ambient /server 9c176555ea86add95839d02b6c2c5ae7d8a3fd79e36f484852b8f8641104aac1 $ curl localhost:8000 Successfully serving on port 80 $ docker top 9c17 UID ... CMD nobody ... /server 现在容器中的进程只有单一的 NET_BIND_SERVICE capabilities，并且是以非 root 用户身份运行的。即使容器的进程被黑客攻击，攻击者只会拥有有限的文件系统权限，无法施展拳脚。\nDocker 中还有一个选项可以防止容器中的用户获得新的 capabilities，它可以有效阻止攻击者提升权限来避免受到攻击，同时也阻止了再容器中执行 set_ambient 程序。例如：\n$ docker run -p 8000:80 --security-opt=no-new-privileges:true \\ --user nobody amouat/caps /set_ambient /server Cannot set cap: Operation not permitted 详细解释可参考 no_new_privs。\n对于容器玩家，我的最终建议是：移除所有非必要的 capabilities，并以非 root 身份运行。 使用 Ambient 集合与可执行文件的 capabilities 进行逻辑运算可以得到一个相对安全的容器环境，大部分情况下应该不需要使用 set_ambient 这样的辅助程序。\nLinux capabilities 与容器领域有着紧密的联系，我很期待看到 Ambient capabilities 被广泛应用到容器领域，以支持以非 root 身份运行的半特权容器。\n","date":"2020年10月19日","externalUrl":null,"permalink":"/posts/linux-capabilities-in-practice-2/","section":"博客","summary":"原文链接： Linux Capabilities In Practice 该系列文章总共分为三篇： Linux Capabilities 入门教程：概念","title":"Linux Capabilities 入门教程：进阶实战篇","type":"posts"},{"content":"","date":"2020年10月19日","externalUrl":null,"permalink":"/series/linux-capabilities-%E5%85%A5%E9%97%A8%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"Linux Capabilities 入门系列","type":"series"},{"content":"微软 2015 年收购 Minecraft 之后不久开源了一个项目叫 Dockercraft，这个项目当时看起来非常有趣，通过 Dockercraft，玩家可以在 Minecraft 中启动或停止一个 Docker 容器，而 Docker 容器会以一个 N*N 的方块房子的方式显示在玩家面前，每一栋房子都代表一个 Docker 容器。\n房子的外面挂着显示容器信息的看板，包括容器的名称、正在运行的进程、CPU 与内存的使用率等信息。\n房子里面是管理容器的开关，扳动墙上的开关可以停止和启动容器，这对于码农来说是一个非常有趣的服务器。\n我寻思着，既然有了 Dockercraft，怎么能没有 Kubecraft 呢？Google 搜了下还真有，项目名字正好就叫 Kubecraft。它的功能和 Dockercraft 类似，可以管理 Kubernetes 集群中的容器，每一个房子代表一个 Pod，房子里面有开关可以销毁 Pod，真是太好玩了（太无聊了\u0026hellip;\u0026hellip;）。\n官方仓库给的部署方式是用 Docker 跑的，命令如下：\n$ docker run -t -d -i -p 25565:25565 \\ --name kubecraft \\ -e KUBE_CFG_FILE=/etc/kubeconfig \\ -v ~/.kube/config:/etc/kubeconfig \\ stevesloka/kubecraft 如果想部署在 Kubernetes 中，可以参考下面的部署清单：\napiVersion: apps/v1 kind: Deployment metadata: name: kubecraft labels: app: kubecraft spec: replicas: 1 selector: matchLabels: app: kubecraft template: metadata: labels: app: kubecraft spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - kubecraft topologyKey: kubernetes.io/hostname weight: 1 tolerations: - key: node-role.kubernetes.io/ingress operator: Exists effect: NoSchedule containers: - name: kubecraft image: stevesloka/kubecraft tty: true stdin: true env: - name: KUBE_CFG_FILE value: /etc/kubeconfig ports: - containerPort: 25565 protocol: TCP volumeMounts: - mountPath: /etc/kubeconfig subPath: kubeconfig name: kubeconfig volumes: - name: kubeconfig configMap: name: kubeconfig --- apiVersion: v1 kind: Service metadata: name: kubecraft labels: app: kubecraft spec: selector: app: kubecraft ports: - protocol: TCP name: http port: 25565 targetPort: 25565 一定要加上 tty: true 和 stdin:true，不然容器无法启动！\n你还需要先创建一个 Configmap 来保存 kubeconfig，例如：\n$ kubectl create cm kubeconfig --from-file=/root/.kube/config 然后就可以愉快地部署了。\n除了 Kubecraft 之外，还有一个项目叫 KubeCraftAdmin，功能上并没有什么太大的差异，只是每一个动物代表一个 Pod，你只要干掉一只鸡🐔，Kubernetes 中的 Pod 就被干死了，刺不刺激？\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 ","date":"2020年10月8日","externalUrl":null,"permalink":"/posts/minecraft-as-a-k8s-admin-tool/","section":"博客","summary":"微软 2015 年收购 Minecraft 之后不久开源了一个项目叫 Dockercraft","title":"在 Minecraft 中管理 Kubernetes 集群","type":"posts"},{"content":"","date":"2020年9月28日","externalUrl":null,"permalink":"/tags/dns/","section":"标签","summary":"","title":"DNS","type":"tags"},{"content":"随着 Linux 的不断发展壮大，涌现出了各种各样的 DNS 自动管理程序，它们都想要直接获得 /etc/resolv.conf 的控制权，有些人欣然接受，有些人则无法接受。如果你是无法接受的那一方，那么请继续往下看，我会教你如何识别出是哪些程序在控制你的 /etc/resolv.conf 文件，以及如何夺回控制权。\n目前能够控制 /etc/resolv.conf 文件的工具大概有这么几个：netconfig, NetworkManager, resolvconf, rdnssd 和 systemd-resolved。如果你的 /etc/resolv.conf 文件正在被它们控制，那么你对该文件的任何修改都会在几分钟后被覆盖，或者重启后被恢复成原来的值。\n要想重新夺回对 /etc/resolv.conf 的控制权，首先就要识别出是谁在控制这个文件。\n1. 找出是谁在控制 /etc/resolv.conf # 先尝试读取 /etc/resolv.conf 开头的注释，注释里一般会标明是谁在操控该文件：\n$ head /etc/resolv.conf 有些工具不会在 /etc/resolv.conf 文件中添加注释，从文件内容里找不到任何蛛丝马迹。这时我们需要换种方法，直接查看该文件是否是一个软链接：\n$ ls -l /etc/resolv.conf 如果还是找不到任何线索，那就只能查看系统运行的进程中是否有上面提到的工具。如果还是找不到，那么恭喜你，resolv.conf 已经完全掌控在你的手里，你想怎么改就直接改吧。\n接下来将会教你如何禁用自动管理 resolv.conf 的各种程序。\n2. NetworkManager # NetworkManager 是最常见的自动配置网络和 DNS 的工具。比如在 Debian 和 Fedora 中它负责配置 /etc/resolv.conf。NetworkManager 可以和其他工具共存，即使禁用了所有其他管理 resolv.conf 的程序，NetworkManager 也会跳出来接管 resolv.conf。\n可以将 NetworkManager 的主配置部分的选项 dns 设置为 none 来禁用其对 DNS 的管理功能：\n$ echo -e \u0026#34;[main]\\ndns=none\u0026#34; \u0026gt; /etc/NetworkManager/conf.d/no-dns.conf $ systemctl restart NetworkManager.service $ rm /etc/resolv.conf 如果配置完了以后没有生效，那么可能存在配置冲突（通常是由 dnsmasq 引起的），需要找到冲突的配置：\n$ grep -ir \u0026#34;\\[main\\]\u0026#34; /etc/NetworkManager/ 3. netconfig # 如果是 openSUSE，SUSE 或其他衍生发行版，一般都是由 netconfig 来控制 resolv.conf。可以通过禁用 /etc/sysconfig/network/config 中的 NETCONFIG_DNS_POLICY 选项来禁用其对 resolv.conf 的控制：\nNETCONFIG_DNS_POLICY=\u0026#34;\u0026#34; 还要删除 netconfig 生成的 resolv.conf 文件，并重启系统：\n$ rm /etc/resolv.conf $ reboot 现在就可以手动创建 /etc/resolv.conf 文件随意修改了。\n4. resolvconf 和 rdnssd # 如果是 Debian 8.0 或 Ubuntu 15.04，并且启用了 IPv6，那么你可能会遇到 resolvconf 和 rdnssd 互相争夺 resolv.conf 控制器的情况。两个服务都想控制这个文件，每隔几毫秒就会覆盖对方的配置，从而导致间歇性的 DNS 解析中断。可以直接禁用并立即停止这两个服务：\n$ systemctl disable --now resolvconf.service rdnssd.service $ rm /etc/resolv.conf 最后手动创建 /etc/resolv.conf 文件。\n5. systemd-resolved # 如果是 Ubuntu 16.10 或更新的版本，则由 systemd-resolved 服务来管理 DNS，可以使用下面的命令来禁用并立即停止该服务：\n$ systemctl disable --now systemd-resolved.service $ rm /etc/resolv.conf 然后手动创建 /etc/resolv.conf 文件。\n6. 创建 /etc/resolv.conf # 最后的最后，就是手动创建 /etc/resolv.conf 文件了，建议权限设置为 644。配置示例：\nnameserver 114.114.114.114 nameserver 223.5.5.5 当然，除了 nameserver 外，还有其他的参数可以配置，感兴趣可以 man 一下：\n$ man 5 resolv.conf ","date":"2020年9月28日","externalUrl":null,"permalink":"/posts/resolvconf-tutorial/","section":"博客","summary":"随着 Linux 的不断发展壮大，涌现出了各种各样的 DNS 自动管理程序，它们","title":"重新夺回对 /etc/resolv.conf 的控制权","type":"posts"},{"content":"","date":"2020年9月14日","externalUrl":null,"permalink":"/tags/ceph/","section":"标签","summary":"","title":"Ceph","type":"tags"},{"content":"本文详细介绍了如何在 Kubernetes 集群中部署 ceph-csi（v3.1.0），并使用 RBD 作为持久化存储。\n需要的环境参考下图：\n本文使用的环境版本信息：\nKubernetes 版本：\n$ kubectl get node NAME STATUS ROLES AGE VERSION sealos01 Ready master 23d v1.18.8 sealos02 Ready master 23d v1.18.8 sealos03 Ready master 23d v1.18.8 Ceph 版本：\n$ ceph version ceph version 14.2.11 (f7fdb2f52131f54b891a2ec99d8205561242cdaf) nautilus (stable) 以下是详细部署过程：\n1. 新建 Ceph Pool # 创建一个新的 ceph 存储池（pool） 给 Kubernetes 使用：\n$ ceph osd pool create kubernetes pool \u0026#39; kubernetes\u0026#39; created 查看所有的 pool：\n$ ceph osd lspools 1 cephfs_data 2 cephfs_metadata 3 .rgw.root 4 default.rgw.control 5 default.rgw.meta 6 default.rgw.log 7 kubernetes 2. 新建用户 # 为 Kubernetes 和 ceph-csi 单独创建一个新用户：\n$ ceph auth get-or-create client.kubernetes mon \u0026#39;profile rbd\u0026#39; osd \u0026#39;profile rbd pool=kubernetes\u0026#39; mgr \u0026#39;profile rbd pool=kubernetes\u0026#39; [client.kubernetes] key = AQBnz11fclrxChAAf8TFw8ROzmr8ifftAHQbTw== 后面的配置需要用到这里的 key，如果忘了可以通过以下命令来获取：\n$ ceph auth get client.kubernetes exported keyring for client.kubernetes [client.kubernetes] key = AQBnz11fclrxChAAf8TFw8ROzmr8ifftAHQbTw== caps mgr = \u0026#34;profile rbd pool=kubernetes\u0026#34; caps mon = \u0026#34;profile rbd\u0026#34; caps osd = \u0026#34;profile rbd pool=kubernetes\u0026#34; 3. 部署 ceph-csi # 拉取 ceph-csi 的 最新 release 分支（v3.1.0）：\n$ git clone --depth 1 --branch v3.1.0 https://gitclone.com/github.com/ceph/ceph-csi 这里使用 gitclone 来加速拉取。 修改 Configmap # 获取 Ceph 集群的信息：\n$ ceph mon dump dumped monmap epoch 1 epoch 1 fsid 154c3d17-a9af-4f52-b83e-0fddd5db6e1b last_changed 2020-09-12 16:16:53.774567 created 2020-09-12 16:16:53.774567 min_mon_release 14 (nautilus) 0: [v2:172.16.1.21:3300/0,v1:172.16.1.21:6789/0] mon.sealos01 1: [v2:172.16.1.22:3300/0,v1:172.16.1.22:6789/0] mon.sealos02 2: [v2:172.16.1.23:3300/0,v1:172.16.1.23:6789/0] mon.sealos03 这里需要用到两个信息：\nfsid : 这个是 Ceph 的集群 ID。 监控节点信息。目前 ceph-csi 只支持 v1 版本的协议，所以监控节点那里我们只能用 v1 的那个 IP 和端口号（例如，172.16.1.21:6789）。 进入 ceph-csi 的 deploy/rbd/kubernetes 目录：\n$ cd deploy/rbd/kubernetes $ ls -l ./ total 36 -rw-r--r-- 1 root root 100 Sep 14 04:49 csi-config-map.yaml -rw-r--r-- 1 root root 1686 Sep 14 04:49 csi-nodeplugin-psp.yaml -rw-r--r-- 1 root root 858 Sep 14 04:49 csi-nodeplugin-rbac.yaml -rw-r--r-- 1 root root 1312 Sep 14 04:49 csi-provisioner-psp.yaml -rw-r--r-- 1 root root 3105 Sep 14 04:49 csi-provisioner-rbac.yaml -rw-r--r-- 1 root root 5497 Sep 14 04:49 csi-rbdplugin-provisioner.yaml -rw-r--r-- 1 root root 5852 Sep 14 04:49 csi-rbdplugin.yaml 将以上获取的信息写入 csi-config-map.yaml：\n--- apiVersion: v1 kind: ConfigMap data: config.json: |- [ { \u0026#34;clusterID\u0026#34;: \u0026#34;154c3d17-a9af-4f52-b83e-0fddd5db6e1b\u0026#34;, \u0026#34;monitors\u0026#34;: [ \u0026#34;172.16.1.21:6789\u0026#34;, \u0026#34;172.15.1.22:6789\u0026#34;, \u0026#34;172.16.1.23:6789\u0026#34; ] } ] metadata: name: ceph-csi-config 创建一个新的 namespace 专门用来部署 ceph-csi：\n$ kubectl create ns ceph-csi 将此 Configmap 存储到 Kubernetes 集群中：\n$ kubectl -n ceph-csi apply -f csi-config-map.yaml 新建 Secret # 使用创建的 kubernetes 用户 ID 和 cephx 密钥生成 Secret：\ncat \u0026lt;\u0026lt;EOF \u0026gt; csi-rbd-secret.yaml apiVersion: v1 kind: Secret metadata: name: csi-rbd-secret namespace: ceph-csi stringData: userID: kubernetes userKey: AQBnz11fclrxChAAf8TFw8ROzmr8ifftAHQbTw== EOF 部署 Secret：\n$ kubectl apply -f csi-rbd-secret.yaml RBAC 授权 # 将所有配置清单中的 namespace 改成 ceph-csi：\n$ sed -i \u0026#34;s/namespace: default/namespace: ceph-csi/g\u0026#34; $(grep -rl \u0026#34;namespace: default\u0026#34; ./) $ sed -i -e \u0026#34;/^kind: ServiceAccount/{N;N;a\\ namespace: ceph-csi # 输入到这里的时候需要按一下回车键，在下一行继续输入 }\u0026#34; $(egrep -rl \u0026#34;^kind: ServiceAccount\u0026#34; ./) 创建必须的 ServiceAccount 和 RBAC ClusterRole/ClusterRoleBinding 资源对象：\n$ kubectl create -f csi-provisioner-rbac.yaml $ kubectl create -f csi-nodeplugin-rbac.yaml 创建 PodSecurityPolicy：\n$ kubectl create -f csi-provisioner-psp.yaml $ kubectl create -f csi-nodeplugin-psp.yaml 部署 CSI sidecar # 将 csi-rbdplugin-provisioner.yaml 和 csi-rbdplugin.yaml 中的 kms 部分配置注释掉：\n部署 csi-rbdplugin-provisioner：\n$ kubectl -n ceph-csi create -f csi-rbdplugin-provisioner.yaml 这里面包含了 6 个 Sidecar 容器，包括 external-provisioner、external-attacher、csi-resizer 和 csi-rbdplugin。\n部署 RBD CSI driver # 最后部署 RBD CSI Driver：\n$ kubectl -n ceph-csi create -f csi-rbdplugin.yaml Pod 中包含两个容器：CSI node-driver-registrar 和 CSI RBD driver。\n创建 Storageclass # $ cat \u0026lt;\u0026lt;EOF \u0026gt; storageclass.yaml --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: csi-rbd-sc provisioner: rbd.csi.ceph.com parameters: clusterID: 154c3d17-a9af-4f52-b83e-0fddd5db6e1b pool: kubernetes imageFeatures: layering csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret csi.storage.k8s.io/provisioner-secret-namespace: ceph-csi csi.storage.k8s.io/controller-expand-secret-name: csi-rbd-secret csi.storage.k8s.io/controller-expand-secret-namespace: ceph-csi csi.storage.k8s.io/node-stage-secret-name: csi-rbd-secret csi.storage.k8s.io/node-stage-secret-namespace: ceph-csi csi.storage.k8s.io/fstype: ext4 reclaimPolicy: Delete allowVolumeExpansion: true mountOptions: - discard EOF 这里的 clusterID 对应之前步骤中的 fsid。 imageFeatures 用来确定创建的 image 特征，如果不指定，就会使用 RBD 内核中的特征列表，但 Linux 不一定支持所有特征，所以这里需要限制一下。 3. 试用 ceph-csi # Kubernetes 通过 PersistentVolume 子系统为用户和管理员提供了一组 API，将存储如何供应的细节从其如何被使用中抽象出来，其中 PV（PersistentVolume） 是实际的存储，PVC（PersistentVolumeClaim） 是用户对存储的请求。\n下面通过官方仓库的示例来演示如何使用 ceph-csi。\n先进入 ceph-csi 项目的 example/rbd 目录，然后直接创建 PVC：\n$ kubectl apply -f pvc.yaml 查看 PVC 和申请成功的 PV：\n$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE rbd-pvc Bound pvc-44b89f0e-4efd-4396-9316-10a04d289d7f 1Gi RWO csi-rbd-sc 8m21s $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-44b89f0e-4efd-4396-9316-10a04d289d7f 1Gi RWO Delete Bound default/rbd-pvc csi-rbd-sc 8m18s 再创建示例 Pod：\n$ kubectl apply -f pod.yaml 进入 Pod 里面测试读写数据：\n$ kubectl exec -it csi-rbd-demo-pod bash root@csi-rbd-demo-pod:/# cd /var/lib/www/ root@csi-rbd-demo-pod:/var/lib/www# ls -l total 4 drwxrwxrwx 3 root root 4096 Sep 14 09:09 html root@csi-rbd-demo-pod:/var/lib/www# echo \u0026#34;https://icloudnative.io\u0026#34; \u0026gt; sealos.txt root@csi-rbd-demo-pod:/var/lib/www# cat sealos.txt https://icloudnative.io 列出 kubernetes pool 中的 rbd images：\n$ rbd ls -p kubernetes csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6 查看该 image 的特征：\n$ rbd info csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6 -p kubernetes rbd image \u0026#39;csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6\u0026#39;: size 1 GiB in 256 objects order 22 (4 MiB objects) snapshot_count: 0 id: 8da46585bb36 block_name_prefix: rbd_data.8da46585bb36 format: 2 features: layering op_features: flags: create_timestamp: Mon Sep 14 09:08:27 2020 access_timestamp: Mon Sep 14 09:08:27 2020 modify_timestamp: Mon Sep 14 09:08:27 2020 可以看到对 image 的特征限制生效了，这里只有 layering。\n实际上这个 image 会被挂载到 node 中作为一个块设备，到运行 Pod 的 Node 上可以通过 rbd 命令查看映射信息：\n$ rbd showmapped id pool namespace image snap device 0 kubernetes csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6 - /dev/rbd0 在 node 上查看挂载信息：\n$ lsblk -l|grep rbd rbd0 252:32 0 1G 0 disk /var/lib/kubelet/pods/15179e76-e06e-4c0e-91dc-e6ecf2119f4b/volumes/kubernetes.io~csi/pvc-44b89f0e-4efd-4396-9316-10a04d289d7f/mount 在 容器中查看挂载信息：\n$ kubectl exec -it csi-rbd-demo-pod bash root@csi-rbd-demo-pod:/# lsblk -l|grep rbd rbd0 252:32 0 1G 0 disk /var/lib/www/html 一切正常！\n4. 试用卷快照功能 # 要想使用卷快照（Volume Snapshot）功能，首先需要在 apiserver 的 --feature-gates 参数中加上 VolumeSnapshotDataSource=true，不过从 Kubernetes 1.17 开始这个特性已经默认开启了，不需要再手动添加。\n卷快照功能不是 Kubernetes 的核心 API，它是通过 CRD 来实现的，同时还需要一个卷快照控制器（需要单独部署）。卷快照控制器和 CRD 独立于特定的 CSI 驱动，无论 Kubernetes 集群中部署了多少 CSI 驱动，每个集群都必须只运行一个卷快照控制器和一组卷快照 CRD。\n卷快照 CRD 和控制器都在这个项目中： https://github.com/kubernetes-csi/external-snapshotter。\n将 external-snapshotter 项目拉取到本地：\n$ git clone --depth 1 https://github.com/kubernetes-csi/external-snapshotter 创建卷快照 CRD：\n$ cd external-snapshotter $ kubectl create -f client/config/crd 将卷快照部署清单中的 namespace 改成 kube-system：\n$ sed -i \u0026#34;s/namespace: default/namespace: kube-system/g\u0026#34; $(grep -rl \u0026#34;namespace: default\u0026#34; deploy/kubernetes/snapshot-controller) 部署卷快照控制器：\n$ kubectl create -f deploy/kubernetes/snapshot-controller 现在可以回到 ceph-csi 的 examples/rbd 目录试用卷快照功能了。先将 snapshotclass.yaml 中的 clusterID 改成 Ceph 的集群 ID：\n--- apiVersion: snapshot.storage.k8s.io/v1beta1 kind: VolumeSnapshotClass metadata: name: csi-rbdplugin-snapclass driver: rbd.csi.ceph.com parameters: # String representing a Ceph cluster to provision storage from. # Should be unique across all Ceph clusters in use for provisioning, # cannot be greater than 36 bytes in length, and should remain immutable for # the lifetime of the StorageClass in use. # Ensure to create an entry in the configmap named ceph-csi-config, based on # csi-config-map-sample.yaml, to accompany the string chosen to # represent the Ceph cluster in clusterID below clusterID: 154c3d17-a9af-4f52-b83e-0fddd5db6e1b # Prefix to use for naming RBD snapshots. # If omitted, defaults to \u0026#34;csi-snap-\u0026#34;. # snapshotNamePrefix: \u0026#34;foo-bar-\u0026#34; csi.storage.k8s.io/snapshotter-secret-name: csi-rbd-secret csi.storage.k8s.io/snapshotter-secret-namespace: ceph-csi deletionPolicy: Delete 然后创建 snapshot class：\n$ kubectl create -f snapshotclass.yaml 查看 snapshot class 是否创建成功：\n$ kubectl get volumesnapshotclass NAME DRIVER DELETIONPOLICY AGE csi-rbdplugin-snapclass rbd.csi.ceph.com Delete 2s 还记得上一节创建的 rbd-pvc 吗，现在我们可以直接创建该 PVC 的快照来进行备份了，卷快照的配置清单如下：\n--- apiVersion: snapshot.storage.k8s.io/v1beta1 kind: VolumeSnapshot metadata: name: rbd-pvc-snapshot spec: volumeSnapshotClassName: csi-rbdplugin-snapclass source: persistentVolumeClaimName: rbd-pvc 通过该配置清单创建 PVC rbd-pvc 的快照：\n$ kubectl create -f snapshot.yaml 验证快照是否创建成功：\n$ kubectl get volumesnapshot NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE rbd-pvc-snapshot false rbd-pvc csi-rbdplugin-snapclass snapcontent-9011a05f-dc34-480d-854e-814b0b1b245d 16s 在 Ceph 集群中可以看到新创建快照的 image 名称：\n$ rbd ls -p kubernetes csi-snap-4da66c2e-f707-11ea-ba22-aaa4b0fc674d csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6 查看新创建的快照信息：\n$ rbd snap ls csi-snap-4da66c2e-f707-11ea-ba22-aaa4b0fc674d -p kubernetes SNAPID NAME SIZE PROTECTED TIMESTAMP 9 csi-snap-4da66c2e-f707-11ea-ba22-aaa4b0fc674d 1 GiB Tue Sep 15 03:55:34 2020 快照也是 pool 中的一个 image，所以可以用常规的命令查看快照的详细信息：\n$ rbd info csi-snap-4da66c2e-f707-11ea-ba22-aaa4b0fc674d -p kubernetes rbd image \u0026#39;csi-snap-4da66c2e-f707-11ea-ba22-aaa4b0fc674d\u0026#39;: size 1 GiB in 256 objects order 22 (4 MiB objects) snapshot_count: 1 id: 66cdcd259693 block_name_prefix: rbd_data.66cdcd259693 format: 2 features: layering, deep-flatten, operations op_features: clone-child flags: create_timestamp: Tue Sep 15 03:55:33 2020 access_timestamp: Tue Sep 15 03:55:33 2020 modify_timestamp: Tue Sep 15 03:55:33 2020 parent: kubernetes/csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6@33d02b70-bc82-4def-afd3-b7a40567a8db overlap: 1 GiB 如果想恢复快照，可以直接基于快照创建 PVC，配置清单内容如下：\n--- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: rbd-pvc-restore spec: storageClassName: csi-rbd-sc dataSource: name: rbd-pvc-snapshot kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 1Gi 创建 PVC：\n$ kubectl apply -f pvc-restore.yaml 查看 PVC 和申请成功的 PV：\n$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE rbd-pvc Bound pvc-44b89f0e-4efd-4396-9316-10a04d289d7f 1Gi RWO csi-rbd-sc 22h rbd-pvc-restore Bound pvc-e0ef4f6a-03dc-4c3b-a9c2-db03baf35ab0 1Gi RWO csi-rbd-sc 2m45s $ kubectl get pv pvc-44b89f0e-4efd-4396-9316-10a04d289d7f 1Gi RWO Delete Bound default/rbd-pvc csi-rbd-sc 22h pvc-e0ef4f6a-03dc-4c3b-a9c2-db03baf35ab0 1Gi RWO Delete Bound default/rbd-pvc-restore csi-rbd-sc 2m14s 可以看到 PV 申请成功了，对应到 Ceph 里面就多了一个 RBD image：\n$ rbd ls -p kubernetes csi-snap-4da66c2e-f707-11ea-ba22-aaa4b0fc674d csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6 csi-vol-e32d46bd-f722-11ea-a3fa-ee21730897e6 创建一个新 Pod，使用该 PV 作为持久化存储：\n$ kubectl apply -f pod-restore.yaml 待 Pod 运行成功后，到运行 Pod 的 Node 上可以通过 rbd 命令查看映射信息：\n$ rbd showmapped id pool namespace image snap device 0 kubernetes csi-vol-d9d011f9-f669-11ea-a3fa-ee21730897e6 - /dev/rbd0 1 kubernetes csi-vol-e32d46bd-f722-11ea-a3fa-ee21730897e6 - /dev/rbd1 5. 清理 # 结束对示例应用的体验后，就可以使用下面的命令来完成应用的删除和清理了：\n$ kubectl delete -f pod-restore.yaml $ kubectl delete -f pvc-restore.yaml $ kubectl delete -f snapshot.yaml $ kubectl delete -f snapshotclass.yaml $ kubectl delete -f pod.yaml $ kubectl delete -f pvc.yaml ","date":"2020年9月14日","externalUrl":null,"permalink":"/posts/kubernetes-storage-using-ceph-rbd/","section":"博客","summary":"本文详细介绍了如何在 Kubernetes 集群中部署 ceph-csi（v3.1.","title":"Kubernetes 使用 ceph-csi 消费 RBD 作为持久化存储","type":"posts"},{"content":" 原文链接： Comparing Thanos to VictoriaMetrics cluster\nThanos 和 VictoriaMetrics 都是用来作为 Prometheus 长期存储的成熟方案，其中 VictoriaMetrics 也开源了其 集群版本，功能更加强大。这两种解决方案都提供了以下功能：\n长期存储，可以保留任意时间的监控数据。 对多个 Prometheus 实例采集的数据进行全局聚合查询。 可水平扩展。 本文就来对比一下这两种方案的差异性和优缺点，主要从写入和读取这两个方面来比较，每一个方面的比较都包含以下几个角度：\n配置和操作的复杂度 可靠性和可用性 数据一致性 性能 可扩展性 先来看一下这两种方案的架构。\n1. 架构 # Thanos # Thanos 包含以下几个核心组件：\nSidecar : 每个 Prometheus 实例都包含一个 Sidecar，它与 Prometheus 实例运行在同一个 Pod 中。它有两个作用：1) 将本地超过 2 小时的监控数据上传到对象存储，如 Amazon S3 或 Google 云存储。2) 将本地监控数据（小于 2 小时）提供给 Thanos Query 查询。\nStore Gateway : 将对象存储的数据提供给 Thanos Query 查询。\nQuery : 实现了 Prometheus 的查询 API，将 Sidecar 和对象存储提供的数据进行聚合最终返回给查询数据的客户端（如 Grafana）。\nCompact : 默认情况下，Sidecar 以 2 小时为单位将监控数据上传到对象存储中。Compactor 会逐渐将这些数据块合并成更大的数据块，以提高查询效率，减少所需的存储大小。\nRuler : 通过查询 Query 获取全局数据，然后对监控数据评估 记录规则和告警规则，决定是否发起告警。还可以根据规则配置计算新指标并存储，同时也通过 Store API 将数据暴露给 Query，同样还可以将数据上传到对象存储以供长期保存。由于 Query 和底层组件的可靠性较低， Ruler 组件通常故障率较高：\nRuler 组件在架构上做了一些权衡取舍，强依赖查询的可靠性，这可能对大多数场景都不利。对于 Prometheus 来说，都是直接从本地读取告警规则和记录规则，所以不太可能出现失败的情况。而对于 Ruler 来说，规则的读取来源是分布式的，最有可能直接查询 Thanos Query，而 Thanos Query 是从远程 Store APIs 获取数据的，所以就有可能遇到查询失败的情况。\nReceiver : 这是一个实验性组件，适配了 Prometheus 的 remote write API，也就是所有 Prometheus 实例可以实时将数据 push 到 Receiver。在 Thanos v0.5.0 时，该组件还没有正式发布。\n最后再来看一眼 Thanos 的整体架构图：\nVictoriaMetrics # VictoriaMetrics 集群版包含以下几个核心组件：\nvmstorage : 存储数据。 vminsert : 通过 remote write API 接收来自Prometheus的数据并将其分布在可用的 vmstorage 节点上。 vmselect : 从 vmstorage 节点获取并聚合所需数据，返回给查询数据的客户端（如 Grafana）。 每个组件可以使用最合适的硬件配置独立扩展到多个节点。\n整体架构图如下：\n图中的 VictoriaMetrics 集群和 Load balancer 都可以通过 helm 部署在 Kubernetes 中。对于大部分中小型集群来说，不需要水平扩展的功能，可以直接使用单机版的 VictoriaMetrics。更多信息请参考 垂直扩展基准。\n了解这两种方案的架构后，开始进入对比环节。\n注意：下文提到的 VictoriaMetrics 如没有特殊说明，均指的是集群版。\n2. 写入对比 # 配置和操作的复杂度 # Thanos 需要通过以下步骤来建立写入过程：\n禁用每个 Prometheus 实例的本地数据压缩。具体做法是将 --storage.tsdb.min-block-duration 和 --storage.tsdb.max-block-duration 这两个参数的值设置为相同的值。\nThanos 要求关闭压缩是因为 Prometheus 默认会以 2, 25, 25*5 的周期进行压缩，如果不关闭，可能会导致 Thanos 刚要上传一个 block，这个 block 却被压缩中，导致上传失败。更多详情请参考 这个 issue。如果 --storage.tsdb.retainer.time 参数的值远远高于 2 小时，禁用数据压缩可能会影响 Prometheus 的查询性能。\n在所有的 Prometheus 实例中插入 Sidecar，这样 Sidecar 就可以将监控数据上传到对象存储。\n设置 Sidecar 监控。\n为每个对象存储的 bucket 配置压缩器，即 Compact 组件。\nVictoriaMetrics 需要在 Prometheus 中添加 远程存储的配置，以将采集到的样本数据通过 Remote Write 的方式写入远程存储 VictoriaMetrics 中，不需要在 Prometheus 中插入 Sidecar，也不需要禁用本地数据压缩。详情请参考 官方文档。\n可靠性和可用性 # Thanos Sidecar 以 2 小时为单位将本地监控数据上传到分布式对象存储，这就意味着如果本地磁盘损坏或者数据被意外删除，就有可能会丢失每个 Prometheus 实例上最近2 小时添加的数据。\n从查询组件到 Sidecar 的查询可能会对 Sidecar 数据的上传产生负面影响，因为响应查询和上传的任务都是在同一个 Sidecar 进程中执行的。但理论上可以将负责响应查询的任务和上传的任务分别运行在不同的 Sidecar 中。\n对于 VictoriaMetrics 来说，每个 Prometheus 实例都会实时通过 remote_write API 将所有监控数据复制到远程存储 VictoriaMetrics。在抓取数据和将数据写入远程存储之间可能会有几秒钟的延迟，所以如果本地磁盘损坏或者数据被意外删除，只会丢失每个 Prometheus 实例上最近几秒钟添加的数据。\n从 Prometheus v2.8.0+ 开始，Prometheus 会直接从预写日志（WAL，write-ahead log）中复制数据到远程存储，所以不会因为与远程存储的临时连接错误或远程存储临时不可用而丢失数据。具体的原理是，如果与远程存储的连接出现问题，Prometheus 会自动停止在预写日志（WAL）的位置，并尝试重新发送失败的那一批样本数据，从而避免了数据丢失的风险。同时，由于出现问题时 Prometheus 不会继续往下读取预写日志（WAL），所以不会消耗更多的内存。\n数据一致性 # Thanos 的 Compactor 和 Store Gateway 存在竞争关系，可能会导致数据不一致或查询失败。例如：\n如果 Thanos sidecar 或 compactor 在上传数据的过程中崩溃了，如何确保读取数据的客户端（如 Compactor 和 Store Gateway）都能够优雅地处理这个问题？ 分布式对象存储对于一个新上传的对象提供写后读写一致性（read-after-write consistency）；对于已存在对象的复写提供最终读写一致性（eventual consistency）。举个例子，假设我们有一个崭新的文件，PUT 之后马上 GET ，OK，没有问题，这就是写后读写一致性；假设我们上传了一个文件，之后再 PUT 一个和这个文件的 key 一样，但是内容不同的新文件，之后再 GET。这个时候 GET 请求的结果很可能还是旧的文件。对于 Thanos compactor 来说，它会上传压缩的数据块，删除源数据块，那么在下一次同步迭代后，它可能会获取不到更新的数据块（最终读写一致性），从而又重新压缩了一次数据块并上传，出现数据重叠的现象。 对于 Store Gateway 来说，它每 3 分钟同步一次数据，查询组件可能会试图获取删除的源数据块，从而失败。 更多详情请参考 Read-Write coordination free operational contract for object storage。\nVictoriaMetrics 可以保持数据的强一致性，详情可参考它的 存储架构。\n性能 # Thanos 的写入性能不错，因为 Sidecar 只是将 Prometheus 创建的本地数据块上传到对象存储中。其中 Query 组件的重度查询可能会影响 Sidecar 数据上传的速度。对于 Compactor 组件来说，如果新上传的数据块超出了 Compactor 的性能，可能会对对象存储 bucket 带来不利。\n而 VictoriaMetrics 使用的是远程存储的方式，Prometheus 会使用额外的 CPU 时间来将本地数据复制到远程存储，这与 Prometheus 执行的其他任务（如抓取数据、规则评估等）所消耗的 CPU 时间相比，可以忽略不计。同时，在远程存储数据接收端，VictoriaMetrics 可以按需分配合理的 CPU 时间，足以保障性能。参考 Measuring vertical scalability for time series databases in Google Cloud。\n可扩展性 # Thanos Sidecar 在数据块上传过程中依赖于对象存储的可扩展性。S3 和 GCS 的扩展性都很强。\nVictoriaMetrics 的扩展只需要增加 vminsert 和 vmstorage 的容量即可，容量的增加可以通过增加新的节点或者更换性能更强的硬件来实现。\n3. 读取对比 # 配置和操作的复杂度 # Thanos 需要通过以下步骤来建立读取过程：\nSidecar 为每个 Prometheus 实例启用 Store API，以 将本地监控数据（小于 2 小时）提供给 Thanos Query 查询。 Store Gateway 将对象存储的数据暴露出来提供给 Thanos Query 查询。 Query 组件的查询动作会覆盖到所有的 Sidecar 和 Store Gateway，以便利用 Prometheues 的查询 API 实现全局查询。如果 Query 组件和 Sidecar 组件位于不同数据中心，在它们之间建立安全可靠连接可能会很困难。 VictoriaMetrics 提供了开箱即用的 Prometheues 查询 API，所以不需要在 VictoriaMetrics 集群外设置任何额外的组件。只需要 将 Grafana 中的数据源指向 VictoriaMetrics 即可。\n可靠性和可用性 # Thanos 的 Query 组件需要和所有的 Sidecar 和 Store Gateway 建立连接，从而为客户端（如 Grafana）的查询请求计算完整的数据。如果 Prometheus 实例跨多个数据中心，可能会严重影响查询的可靠性。\n如果对象存储中存在容量很大的 bucket，Store Gateway 的启动时间会很长，因为它需要在启动前从 bucket 中加载所有元数据，详情可以参考 这个 issue。如果 Thanos 需要升级版本，这个问题带来的负面影响会非常明显。\nVictoriaMetrics 的查询过程只涉及到集群内部的 vmselect 和 vmstorage 之间的本地连接，与 Thanos 相比，这种本地连接具有更高的可靠性和可用性。\nVictoriaMetrics 所有组件的启动时间都很短，因此可以快速升级。\n数据一致性 # Thanos 默认情况下允许在部分 Sidecar 或 Store Gateway 不可用时 只返回部分查询结果。\nVictoriaMetrics 也可以在部分 vmstorage 节点不可用时只返回部分查询结果，从而优先考虑可用性而不是一致性。具体的做法是启用 -search.denyPartialResponse 选项。\n总的来说，VictoriaMetrics 返回部分查询结果的可能性更低，因为它的可用性更高。\n性能 # Thanos Query 组件的查询性能取决于查询性能最差的 Sidecar 或 Store Gateway 组件，因为 Query 组件返回查询结果之前会等待所有 Sidecar 和 Store Gateway 组件的响应。\n通常 Sidecar 或 Store Gateway 组件的查询性能不是均衡的，这取决于很多因素：\n每个 Promnetheus 实例抓取的数据容量。 Store Gateway 背后每个对象存储 bucket 的容量。 每个 Prometheus + Sidecar 和 Store Gateway 的硬件配置。 Query 组件和 Sidecar 或 Store Gateway 之间的网络延迟。如果 Query 和 Sidecar 位于不同的数据中心，延迟可能会相当高。 对象存储的操作延迟。通常对象存储延迟（S3、GCS）比块存储延迟（GCE 磁盘、EBS）高得多。 VictoriaMetrics 的查询性能受到 vmselect 和 vmstorage 的实例数量及其资源配额的限制。只需增加实例数，或者分配更多的资源配额，即可扩展查询性能。vminsert 会将 Prometheus 写入的数据均匀地分布到可用的 vmstorage 实例中，所以 vmstorage 的性能是均衡的。 VictoriaMetrics 针对查询速度做了优化，所以与 Thanos 相比，它应该会提供更好的查询性能。\n可扩展性 # Thanos 的 Query 组件是无状态服务，可用通过水平扩展来分担查询负载。Store Gateway 也支持多副本水平扩展，对每一个对象存储 bucket 而言，多个 Store Gateway 副本也可以分担查询负载。但是要扩展 Sidecar 后面的单个 Prometheus 实例的性能是相当困难的，所以 Thanos 的查询性能受到性能最差的 Prometheus + Sidecar 的限制。\nVictoriaMetrics 在查询方面提供了更好的扩展性，因为 vmselect 和 vmstorage 组件的实例可以独立扩展到任何数量。集群内的网络带宽可能会成为限制扩展性的因素，VictoriaMetrics 针对低网络带宽的使用进行了优化，以减少这一限制因素。\n4. 高可用对比 # Thanos 需要在不同的数据中心（或可用区）运行多个 Query 组件，如果某个区域不可用，那么另一个区域的 Query 组件将会继续负责响应查询。当然，这种情况下基本上只能返回部分查询结果，因为部分 Sidecar 或 Store Gateway 组件很有可能就位于不可用区。\nVictoriaMetrics 可以在不同的数据中心（或可用区）运行多个集群，同时可以配置 Prometheus 将数据复制到所有集群，具体可以参考 官方文档的示例。如果某个区域不可用，那么另一个区域的 VictoriaMetrics 仍然继续接收新数据，并能返回所有的查询结果。\n5. 托管成本对比 # Thanos 选择将数据存放到对象存储中，最常用的 GCS 和 S3 的每月计费情况如下：\nGCS : 价格区间位于 $4/TB 的 coldline storage 和 $36/TB 的标准存储之间。此外，对于出口网络：内部流量 $10/TB，外部流量 $80-$230/TB。对于存储 API 的调用（读写）：每百万次调用 $0.4-$10。具体参考 价格详情。 S3 : 价格区间位于 $4/TB 的 glacier storage 和 $23/TB 的标准存储之间。此外，对于出口网络：内部流量 $2-$10/TB，外部流量 $50-$90/TB。对于存储 API 的调用（读写）：每百万次调用 $0.4-$100。具体参考 价格详情。 总体看下来，Thanos 的托管成本不仅取决于数据大小，还取决于出口流量和 API 调用的数量。\nVictoriaMetrics 只需要将数据存放到块存储，最常用的 GCE 和 EBS 的每月计费情况如下：\nGCE 磁盘 : 价格区间位于 $40/TB 的 HDD 和 $240/TB 的 SSD。具体参考 价格详情。 EBS : 价格区间位于 $45/TB 的 HDD 和 $125/TB 的SSD。具体参考 价格详情。 VictoriaMetrics 针对 HDD 做了优化，所以基本上没必要使用昂贵的 SSD。VictoriaMetrics 采用高性能的数据压缩方式，使存入存储的数据量比 Thanos 多达 10x，详情参考 这篇文章。这就意味着与 Thanos 相比，VictoriaMetrics 需要更少的磁盘空间，存储相同容量数据的成本更低。\n总结 # Thanos 和 VictoriaMetrics 分别使用了不同的方法来提供长期存储、聚合查询和水平扩展性。\nVictoriaMetrics 通过标准的 remote_write API 接收来自 Prometheus 实例写入的数据，然后将其持久化（如 GCE HDD 磁盘、 Amazon EBS 或其他磁盘）。而 Thanos 则需要禁用每个 Prometheus 实例的本地数据压缩，并使用非标准的 Sidecar 将数据上传至 S3 或 GCS。同时还需要设置 Compactor，用于将对象存储 bucket 上的小数据块合并成大数据块。 VictoriaMetrics 开箱即实现了全局查询视图的 Prometheus query API。由于 Prometheus 会实时将抓取到的数据复制到远程存储，所以它不需要在集群外建立任何外部连接来实现全局查询。Thanos 需要设置 Store Gateway、SIdecar 和 Query 组件才能实现全局查询。对于大型的 Thanos 集群来说，在 Query 组件和位于不同数据中心（可用区域）的 Sidecar 之间提供可靠安全的连接是相当困难的。Query 组件的性能会受到性能最差的 Sidecar 或 Store Gateway 的影响。 VictoriaMetrics 集群可以快速部署到 Kubernetes 中，因为它的 架构非常简单。而 Thanos 在 Kubernetes 中的部署和配置非常复杂。 本文由 VictoriaMetrics 核心开发者所著，所以可能会更倾向于 VictoriaMetrics，但作者尽量做到了公平对比。如果你有任何疑问，欢迎找原作者交流（si bi）。\n","date":"2020年8月25日","externalUrl":null,"permalink":"/posts/comparing-thanos-to-victoriametrics-cluster/","section":"博客","summary":"原文链接： Comparing Thanos to VictoriaMetrics cluster Thanos 和 VictoriaMetrics 都是用来作为 Prometheus 长期存储的成熟方案，","title":"Thanos 与 Victoriametrics 集群版的比较","type":"posts"},{"content":"","date":"2020年8月25日","externalUrl":null,"permalink":"/tags/victoriametrics/","section":"标签","summary":"","title":"VictoriaMetrics","type":"tags"},{"content":"在容器的整个生命周期中，拉取镜像是最耗时的步骤之一。 Harter 等人的研究表明：\n拉取镜像占用了容器启动时间的 76%，只有 6.4% 的时间用来读取数据。\n这个问题一直困扰着各类工作负载，包括 serverless 函数的冷启动时间，镜像构建过程中基础镜像的拉取等。虽然有各种折中的解决方案，但这些方案都有缺陷：\n缓存镜像 : 冷启动时仍然有性能损失。 减小镜像体积 : 无法避免某些场景需要用到大体积的镜像，比如机器学习。 现在有一个更通用的解决方案，该方案完全兼容 OCI 标准，目前看来是比较理想的方案。\n1. Containerd Stargz Snapshotter # Containerd 为了解决这个问题启动了一个非核心子项目 Stargz Snapshotter，旨在提高镜像拉取的性能。该项目作为 Containerd 的一个插件，利用 Google 的 stargz 镜像格式来延迟拉取镜像。这里的延迟拉取指的是 Containerd 在拉取时不会拉取整个镜像文件，而是按需获取必要的文件。\n下图是基于 HelloBench 的容器启动过程基准测试结果， 跑在 Github Actions 提供的机器上，镜像仓库直接使用 Docker Hub：\nlegacy 表示使用 Containerd 默认的 snapshotter（overlayfs）来拉取镜像且不进行优化时的启动性能，这种情况下 Containerd 会拉取整个镜像内容，所以拉取时间会很长。 而对于 stargz 格式的镜像，Containerd 可以在镜像还没有完全拉取到本地之前就启动容器，然后按需获取需要的文件，所以拉取的时间更短。但读取文件时需要从远程仓库下载文件内容，所以 run 的性能要低于传统的拉取方式。 如果使用进一步优化的镜像格式 estargz，可以在拉取时间短的基础上提高 run 的性能。 Stargz snapshotter 的特点：\n兼容 OCI 标准 # Stargz snapshotter 可以从符合 OCI/ Docker 镜像仓库标准的镜像仓库中延迟拉取 stargz 镜像，拉取到的 stargz 镜像也符合 OCI/ Docker 镜像规范，所以任何容器运行时都可以运行。\n支持私有镜像仓库 # Stargz snapshotter 支持基于文件 ~/.docker/config.json 的认证，也支持基于 Kubernetes Secret 的认证。\n支持 Kubernetes # 它也可以作为 Containerd 的 CRI 插件，所以 Kubernetes 也可以使用。\n2. 使用指南 # 要想在 Kubernetes 中使用 stargz snapshotter，需要在每个节点上运行一个守护进程，然后将其配置为 Containerd 的插件。同时需要确保 Containerd 的 commit 版本不低于 d8506bf。所需的 Containerd 配置文件（/etc/containerd/config.toml）内容如下：\nversion = 2 # Plug stargz snapshotter into containerd # Containerd recognizes stargz snapshotter through specified socket address. # The specified address below is the default which stargz snapshotter listen to. [proxy_plugins] [proxy_plugins.stargz] type = \u0026#34;snapshot\u0026#34; address = \u0026#34;/run/containerd-stargz-grpc/containerd-stargz-grpc.sock\u0026#34; # Use stargz snapshotter through CRI [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd] snapshotter = \u0026#34;stargz\u0026#34; 然后就可以创建 stargz 格式的 Pod 了，例如：\napiVersion: v1 kind: Pod metadata: name: nodejs spec: containers: - name: nodejs-stargz image: stargz/node:13.13.0-esgz command: [\u0026#34;node\u0026#34;] args: - -e - var http = require(\u0026#39;http\u0026#39;); http.createServer(function(req, res) { res.writeHead(200); res.end(\u0026#39;Hello World!\\n\u0026#39;); }).listen(80); ports: - containerPort: 80 该 Pod 使用了可以从 Docker Hub 中延迟拉取的镜像 stargz/node:13.13.0-esgz 来取代官方的镜像 library/node:13.13.0。\n3. 实现原理 # Stargz snapshotter 是由多种技术组合而成的，本节只介绍其中三种技术：\nstargz 压缩格式 # 延迟拉取的目的是让容器运行时有选择地从 blob 中的镜像层（layer）下载和提取文件，但 OCI/ Docker 镜像规范将所有的镜像层打包成一个 tar 或 tar.gz 存档，这样即使你要提取单个文件也要扫描整个 blob。如果镜像使用 gzip 进行压缩，就更没有办法提取特定文件了。\nStargz 是谷歌提出的存档压缩格式，是 Seekable tar.gz 的缩写，顾名思义，可以有选择地从存档中搜寻并提取特定的文件，无需扫描整个镜像 blob。关于 Stargz 镜像格式的更多细节，请参考 CRFS 项目。通过结合 OCI/ Docker 镜像仓库规范支持的 HTTP Range Request，容器运行时可以有选择地从镜像仓库中获取文件。\n在 stargz 存档中，每个 tar 条目都被压缩成 gzip 格式，stargz 是所有 gzip 的组合，仍然是有效的 gzip，所以任何容器运行时都可以像对待传统的 tar.gz 镜像层一样对待 stargz 镜像层。对于大文件来说，会被分成多个 gzip，只包含元数据的条目（如符号链接）与相邻的条目会压缩到同一个 gzip 中。\n在 gzip 之后还包含一个名为 TOC 的索引文件条目，这是一个 JSON 文件（stargz.index.json），记录了 stargz 存档中每个文件内容对应的块的大小和偏移量，以及每个文件的元数据（名称、文件类型、所有者等）。有了 TOC 之后，就可以在不扫描整个存档文件的情况下提取需要的文件。\nstargz 优化版 # Stargz 虽然提高了拉取性能，但在运行阶段按需读取文件时仍然存在性能缺陷。为了解决这个问题，stargz snapshotter 做了进一步的优化。\n一般情况下，每个容器镜像都是用来运行特定的服务，这些信息在构建时就已经定义好了，例如在 Dockerfile 中定义的 entrypoint、环境变量等等。我们可以根据这些信息来预测容器运行时可能需要访问的文件，在运行之前预取这些文件，从而提高缓存命中率。\nstargz snapshotter 项目中的 ctr-remote images optimize 命令提供了对读取最有可能在运行时访问的文件性能的优化，将这些文件放到相邻的镜像层中。具体的做法是在一个沙箱环境中运行指定的工作负载，并对所有文件进行剖析，筛选出最有可能被访问的文件，然后按照预测的访问顺序对其进行排序，并在最后放置一个标志性文件作为结束。在运行容器之前，stargz snapshotter 会通过单个 HTTP Range Request 预取和预缓存这个范围的文件，提高缓存命中率，从而减轻运行时的开销。\n远程 snapshotter 插件 # Containerd 的架构是可插拔的，所有的功能是按照定义的 API 以插件的形式实现的。用户可以将其与自定义插件集成来扩展 Containerd 的功能。例如， AWS Firecracker 就扩展了 Containerd 来支持 microVMs。\nSnapshotter 就是 Containerd 的其中一个插件，它被用来存储拉取到本地的镜像层。在拉取镜像的过程中，Containerd 会提取其中的镜像层，并将它们叠加在一起，存储为为一个快照（snapshot）。当 Containerd 启动容器时，会向 snapshotter 查询快照，并将其作为容器的 rootfs。\nContainerd 也支持远程的 snapshotter，它是 snapshotter 的一个变体，能够直接挂载远程的镜像层作为快照（snapshot），无需拉取整个镜像层。Stargz snapshotter 也实现了远程 snapshotter。\n参考链接 # Startup Containers in Lightning Speed with Lazy Image Distribution on Containerd Stargz Snapshotter ","date":"2020年8月18日","externalUrl":null,"permalink":"/posts/startup-containers-in-lightning-speed-with-lazy-image-distribution-on-containerd/","section":"博客","summary":"在容器的整个生命周期中，拉取镜像是最耗时的步骤之一。 Harter 等人的","title":"Containerd 使用 Stargz Snapshotter 延迟拉取镜像","type":"posts"},{"content":" 原文链接： Introducing Hierarchical Namespaces\n在单个 Kubernetes 集群上安全托管大量用户一直是一个老大难问题，其中最大的麻烦就是不同的组织以不同的方式使用 Kubernetes，很难找到一种租户模式可以适配所有组织。相反，Kubernetes 只提供了创建不同租户模式的基础构件，例如 RBAC 和 NetworkPolicies，这些基础构件实现得越好，安全构建多租户集群就越容易。\n1. 命名空间 # 其中最重要的基础构件是命名空间（namespace），它构成了几乎所有 Kubernetes 控制平面安全和共享策略的骨干。命名空间有两个关键属性，使其成为策略执行的理想选择：\n首先，命名空间可以用来代表所有权。大多数 Kubernetes 对象资源必须在某一个命名空间中，所以如果使用命名空间来代表所有权，那么命名空间中的所有对象都隶属于同一个所有者。 其次，命名空间的创建和使用需要授权。只有超级管理员才能创建命名空间，其他用户需要明确的权限才能使用这些命名空间（包括创建、查看和修改命名空间中的资源对象）。可以设置恰当的安全策略，防止非特权用户创建某些资源对象。 2. 命名空间的限制 # 然而在实际使用中，命名空间还是不够灵活，无法满足一些常见的用例。假设一个团队拥有好几套微服务环境，每一套微服务环境都有自己的秘钥和资源配额，理想情况下应该将不同的微服务环境放到不同的命名空间中，以便相互隔离。但这样会带来两个问题：\n首先，不同的命名空间没有共同的所有权概念，即使它们属于同一个团队。如果某个团队控制了多个命名空间，Kubernetes 不仅没有任何关于这些命名空间的共同所有者的记录，而且针对命名空间范围内的策略也无法跨多个命名空间生效。 其次，如果团队能够自主运作，团队协作效率会更高。但创建命名空间是需要高级权限的，所以开发团队的任何成员都不可能有权限创建命名空间。这就意味着，每当某个团队想要创建新的命名空间时，就必须向集群管理员提出申请，这种方式对小规模组织还可以接受，但随着组织的发展壮大，势必需要寻求更佳的方案。 3. 层级命名空间介绍 # 层级命名空间（hierarchical namespaces）是 Kubernetes 多租户工作组（Working Group for Multi-Tenancy，wg-multitenancy） 为了解决这些问题而提出的新概念。在最简单的形式下，层级命名空间就是一个常规的命名空间，它标识了一个单一的、可选的父命名空间；更复杂的形式下，父命名空间还可以继承出子空间。这样就建立了跨命名空间的所有权概念，而不是局限于命名空间内。\n这种层级命名空间的所有权可以在命名空间的基础上实现额外的两种功能：\n策略继承 : 如果一个命名空间是另一个命名空间的子空间，那么权限策略（例如 RBAC RoleBindings）将会 从父空间直接复制到子空间。 继承创建权限 : 通常情况下，需要管理员权限才能创建命名空间。但层级命名空间提供了一个新方案： 子命名空间（subnamespaces），只需要使用父命名空间中的部分权限即可操作子命名空间。 有了这两个功能后，集群管理员就可以为团队创建一个『根』命名空间，以及所有必要的权限策略，然后将创建子命名空间的权限赋予该团队的成员。这样团队内的成员就可以在不违反集群策略的情况下创建自己的子命名空间。\n4. 示例 # 层级命名空间由 Kubernetes 的 层级命名空间控制器（Hierarchical Namespace Controller，HNC）。HNC 包含两个组件：\n控制器 : 控制器运行在集群中，用来管理子命名空间，传递策略对象，确保层次结构的合理性，并管理扩展点。 kubectl 插件 : 插件名叫 kubectl-hns，用户可以使用该插件和控制器进行交互。 控制器和插件的安装请参考 release 页面。\n下面举一个简单的例子，假设某团队成员没有创建命名空间的权限，但可以查看命名空间 team-a，也可以为其创建子命名空间。使用 kubectl 插件执行以下命令：\n$ kubectl hns create svc1-team-a -n team-a 这个命令创建了一个子命名空间 svc1-team-a。子命名空间也是常规的命名空间，所以名称不能重复。\n查看命名空间的层级结构：\n$ kubectl hns tree team-a # Output: team-a └── svc1-team-a 如果父命名空间中有任何策略，都会被继承到子命名空间中。例如，假设 team-a 中有一个名为 sres 的 RBAC RoleBinding，那么它也会出现在子命名空间中：\n$ kubectl describe rolebinding sres -n svc1-team-a # Output: Name: sres Labels: hnc.x-k8s.io/inheritedFrom=team-a # inserted by HNC Annotations: \u0026lt;none\u0026gt; Role: Kind: ClusterRole Name: admin Subjects: ... HNC 还为层级命名空间添加了相关标签，其中包含了层级结构的相关信息，你可以用来设置其他的策略。例如，可以创建以下 NetworkPolicy：\nkind: NetworkPolicy apiVersion: networking.k8s.io/v1 metadata: name: allow-team-a namespace: team-a spec: ingress: - from: - namespaceSelector: matchExpressions: - key: \u0026#39;team-a.tree.hnc.x-k8s.io/depth\u0026#39; # Label created by HNC operator: Exists 该策略会传递给 team-a 的所有子命名空间，也会允许所有这些子命名空间之间的 ingress 流量。 这些 \u0026ldquo;tree\u0026rdquo; 标签只能由 HNC 创建，用来确保最新的层级结构。\n关于 HNC 的更多信息请参考 用户指南。\n","date":"2020年8月15日","externalUrl":null,"permalink":"/posts/introducing-hierarchical-namespaces/","section":"博客","summary":"原文链接： Introducing Hierarchical Namespaces 在单个 Kubernetes 集群上安全托管大量用户一直是一个老大","title":"Kubernetes 的层级命名空间介绍","type":"posts"},{"content":"Promtheus 本身只支持单机部署，没有自带支持集群部署，也不支持高可用以及水平扩容，它的存储空间受限于本地磁盘的容量。同时随着数据采集量的增加，单台 Prometheus 实例能够处理的时间序列数会达到瓶颈，这时 CPU 和内存都会升高，一般内存先达到瓶颈，主要原因有：\nPrometheus 的内存消耗主要是因为每隔 2 小时做一个 Block 数据落盘，落盘之前所有数据都在内存里面，因此和采集量有关。 加载历史数据时，是从磁盘到内存的，查询范围越大，内存越大。这里面有一定的优化空间。 一些不合理的查询条件也会加大内存，如 Group 或大范围 Rate。 这个时候要么加内存，要么通过集群分片来减少每个实例需要采集的指标。本文就来讨论通过 Prometheus Operator 部署的 Prometheus 如何根据服务维度来拆分实例。\n1. 根据服务维度拆分 Prometheus # Prometheus 主张根据功能或服务维度进行拆分，即如果要采集的服务比较多，一个 Prometheus 实例就配置成仅采集和存储某一个或某一部分服务的指标，这样根据要采集的服务将 Prometheus 拆分成多个实例分别去采集，也能一定程度上达到水平扩容的目的。\n在 Kubernetes 集群中，我们可以根据 namespace 来拆分 Prometheus 实例，例如将所有 Kubernetes 集群组件相关的监控发送到一个 Prometheus 实例，将其他所有监控发送到另一个 Prometheus 实例。\nPrometheus Operator 通过 CRD 资源名 Prometheus 来控制 Prometheus 实例的部署，其中可以通过在配置项 serviceMonitorNamespaceSelector 和 podMonitorNamespaceSelector 中指定标签来限定抓取 target 的 namespace。例如，将 namespace kube-system 打上标签 monitoring-role=system，将其他的 namespace 打上标签 monitoring-role=others。\n2. 告警规则拆分 # 将 Prometheus 拆分成多个实例之后，就不能再使用默认的告警规则了，因为默认的告警规则是针对所有 target 的监控指标的，每一个 Prometheus 实例都无法获取所有 target 的监控指标，势必会一直报警。为了解决这个问题，需要对告警规则进行拆分，使其与每个 Prometheus 实例的服务维度一一对应，按照上文的拆分逻辑，这里只需要拆分成两个告警规则，打上不同的标签，然后在 CRD 资源 Prometheus 中通过配置项 ruleSelector 指定规则标签来选择相应的告警规则。\n3. 集中数据存储 # 解决了告警问题之后，还有一个问题，现在监控数据比较分散，使用 Grafana 查询监控数据时我们也需要添加许多数据源，而且不同数据源之间的数据还不能聚合查询，监控页面也看不到全局的视图，造成查询混乱的局面。\n为了解决这个问题，我们可以让 Prometheus 不负责存储数据，只将采集到的样本数据通过 Remote Write 的方式写入远程存储的 Adapter，然后将 Grafana 的数据源设为远程存储的地址，就可以在 Grafana 中查看全局视图了。这里选择 VictoriaMetrics 来作为远程存储。 VictoriaMetrics 是一个高性能，低成本，可扩展的时序数据库，可以用来做 Prometheus 的长期存储，分为单机版本和集群版本，均已开源。如果数据写入速率低于每秒一百万个数据点，官方建议使用单节点版本而不是集群版本。本文作为演示，仅使用单机版本，架构如图：\n4. 实践 # 确定好了方案之后，下面来进行动手实践。\n部署 VictoriaMetrics # 首先部署一个单实例的 VictoriaMetrics，完整的 yaml 如下：\nkind: PersistentVolumeClaim apiVersion: v1 metadata: name: victoriametrics namespace: kube-system spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi --- apiVersion: apps/v1 kind: StatefulSet metadata: labels: app: victoriametrics name: victoriametrics namespace: kube-system spec: serviceName: pvictoriametrics selector: matchLabels: app: victoriametrics replicas: 1 template: metadata: labels: app: victoriametrics spec: nodeSelector: blog: \u0026#34;true\u0026#34; containers: - args: - --storageDataPath=/storage - --httpListenAddr=:8428 - --retentionPeriod=1 image: victoriametrics/victoria-metrics imagePullPolicy: IfNotPresent name: victoriametrics ports: - containerPort: 8428 protocol: TCP readinessProbe: httpGet: path: /health port: 8428 initialDelaySeconds: 30 timeoutSeconds: 30 livenessProbe: httpGet: path: /health port: 8428 initialDelaySeconds: 120 timeoutSeconds: 30 resources: limits: cpu: 2000m memory: 2000Mi requests: cpu: 2000m memory: 2000Mi volumeMounts: - mountPath: /storage name: storage-volume restartPolicy: Always priorityClassName: system-cluster-critical volumes: - name: storage-volume persistentVolumeClaim: claimName: victoriametrics --- apiVersion: v1 kind: Service metadata: labels: app: victoriametrics name: victoriametrics namespace: kube-system spec: ports: - name: http port: 8428 protocol: TCP targetPort: 8428 selector: app: victoriametrics type: ClusterIP 有几个启动参数需要注意：\nstorageDataPath : 数据目录的路径。 VictoriaMetrics 将所有数据存储在此目录中。 retentionPeriod : 数据的保留期限（以月为单位）。旧数据将自动删除。默认期限为1个月。 httpListenAddr : 用于监听 HTTP 请求的 TCP 地址。默认情况下，它在所有网络接口上监听端口 8428。 给 namespace 打标签 # 为了限定抓取 target 的 namespace，我们需要给 namespace 打上标签，使每个 Prometheus 实例只抓取特定 namespace 的指标。根据上文的方案，需要给 kube-system 打上标签 monitoring-role=system：\n$ kubectl label ns kube-system monitoring-role=system 给其他的 namespace 打上标签 monitoring-role=others。例如：\n$ kubectl label ns monitoring monitoring-role=others $ kubectl label ns default monitoring-role=others 拆分 PrometheusRule # 告警规则需要根据监控目标拆分成两个 PrometheusRule。具体做法是将 kube-system namespace 相关的规则整合到一个 PrometheusRule 中，并修改名称和标签：\n# prometheus-rules-system.yaml apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: labels: prometheus: system role: alert-rules name: prometheus-system-rules namespace: monitoring spec: groups: ... ... 剩下的放到另外一个 PrometheusRule 中，并修改名称和标签：\n# prometheus-rules-others.yaml apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: labels: prometheus: others role: alert-rules name: prometheus-others-rules namespace: monitoring spec: groups: ... ... 然后删除默认的 PrometheusRule：\n$ kubectl -n monitoring delete prometheusrule prometheus-k8s-rules 新增两个 PrometheusRule：\n$ kubectl apply -f prometheus-rules-system.yaml $ kubectl apply -f prometheus-rules-others.yaml 如果你实在不知道如何拆分规则，或者不想拆分，想做一个伸手党，可以看这里：\nprometheus-rules-system.yaml prometheus-rules-others.yaml 拆分 Prometheus # 下一步是拆分 Prometheus 实例，根据上面的方案需要拆分成两个实例，一个用来监控 kube-system namespace，另一个用来监控其他 namespace：\n# prometheus-prometheus-system.yaml apiVersion: monitoring.coreos.com/v1 kind: Prometheus metadata: labels: prometheus: system name: system namespace: monitoring spec: remoteWrite: - url: http://victoriametrics.kube-system.svc.cluster.local:8428/api/v1/write queueConfig: maxSamplesPerSend: 10000 retention: 2h alerting: alertmanagers: - name: alertmanager-main namespace: monitoring port: web image: quay.io/prometheus/prometheus:v2.17.2 nodeSelector: beta.kubernetes.io/os: linux podMonitorNamespaceSelector: matchLabels: monitoring-role: system podMonitorSelector: {} replicas: 1 resources: requests: memory: 400Mi limits: memory: 2Gi ruleSelector: matchLabels: prometheus: system role: alert-rules securityContext: fsGroup: 2000 runAsNonRoot: true runAsUser: 1000 serviceAccountName: prometheus-k8s serviceMonitorNamespaceSelector: matchLabels: monitoring-role: system serviceMonitorSelector: {} version: v2.17.2 --- apiVersion: monitoring.coreos.com/v1 kind: Prometheus metadata: labels: prometheus: others name: others namespace: monitoring spec: remoteWrite: - url: http://victoriametrics.kube-system.svc.cluster.local:8428/api/v1/write queueConfig: maxSamplesPerSend: 10000 retention: 2h alerting: alertmanagers: - name: alertmanager-main namespace: monitoring port: web image: quay.io/prometheus/prometheus:v2.17.2 nodeSelector: beta.kubernetes.io/os: linux podMonitorNamespaceSelector: matchLabels: monitoring-role: others podMonitorSelector: {} replicas: 1 resources: requests: memory: 400Mi limits: memory: 2Gi ruleSelector: matchLabels: prometheus: others role: alert-rules securityContext: fsGroup: 2000 runAsNonRoot: true runAsUser: 1000 serviceAccountName: prometheus-k8s serviceMonitorNamespaceSelector: matchLabels: monitoring-role: others serviceMonitorSelector: {} additionalScrapeConfigs: name: additional-scrape-configs key: prometheus-additional.yaml version: v2.17.2 需要注意的配置：\n通过 remoteWrite 指定 remote write 写入的远程存储。 通过 ruleSelector 指定 PrometheusRule。 限制内存使用上限为 2Gi，可根据实际情况自行调整。 通过 retention 指定数据在本地磁盘的保存时间为 2 小时。因为指定了远程存储，本地不需要保存那么长时间，尽量缩短。 Prometheus 的自定义配置可以通过 additionalScrapeConfigs 在 others 实例中指定，当然你也可以继续拆分，放到其他实例中。 删除默认的 Prometheus 实例：\n$ kubectl -n monitoring delete prometheus k8s 创建新的 Prometheus 实例：\n$ kubectl apply -f prometheus-prometheus.yaml 查看运行状况：\n$ kubectl -n monitoring get prometheus NAME VERSION REPLICAS AGE system v2.17.2 1 29h others v2.17.2 1 29h $ kubectl -n monitoring get sts NAME READY AGE prometheus-system 1/1 29h prometheus-others 1/1 29h alertmanager-main 1/1 25d 查看每个 Prometheus 实例的内存占用：\n$ kubectl -n monitoring top pod -l app=prometheus NAME CPU(cores) MEMORY(bytes) prometheus-others-0 12m 110Mi prometheus-system-0 121m 1182Mi 最后还要修改 Prometheus 的 Service，yaml 如下：\napiVersion: v1 kind: Service metadata: labels: prometheus: system name: prometheus-system namespace: monitoring spec: ports: - name: web port: 9090 targetPort: web selector: app: prometheus prometheus: system sessionAffinity: ClientIP --- apiVersion: v1 kind: Service metadata: labels: prometheus: others name: prometheus-others namespace: monitoring spec: ports: - name: web port: 9090 targetPort: web selector: app: prometheus prometheus: others sessionAffinity: ClientIP 删除默认的 Service：\n$ kubectl -n monitoring delete svc prometheus-k8s 创建新的 Service：\n$ kubectl apply -f prometheus-service.yaml 修改 Grafana 数据源 # Prometheus 拆分成功之后，最后还要修改 Grafana 的数据源为 VictoriaMetrics 的地址，这样就可以在 Grafana 中查看全局视图，也能聚合查询。\n打开 Grafana 的设置页面，将数据源修改为 http://victoriametrics.kube-system.svc.cluster.local:8428：\n点击 Explore 菜单：\n在查询框内输入 up，然后按下 Shift+Enter 键查询：\n可以看到查询结果中包含了所有的 namespace。\n写这篇文章的起因是我的 k3s 集群每台节点的资源很紧张，而且监控的 target 很多，导致 Prometheus 直接把节点的内存资源消耗完了，不停地 OOM。为了充分利用我的云主机，不得不另谋他路，这才有了这篇文章。\n","date":"2020年8月5日","externalUrl":null,"permalink":"/posts/aggregate-metrics-user-prometheus-operator/","section":"博客","summary":"Promtheus 本身只支持单机部署，没有自带支持集群部署，也不支持高可用以","title":"Prometheus Operator 教程：根据服务维度对 Prometheus 分片","type":"posts"},{"content":"在管理 Kubernetes 集群的过程中，我们经常会遇到这样一种情况：在某台节点上发现某个进程资源占用量很高，却又不知道是哪个容器里的进程。有没有办法可以根据 PID 快速找到 Pod 名称呢？\n假设现在有一个 prometheus 进程的 PID 是 14338：\n为了进一步挖掘信息，有两种思路，一种是挖掘 PID 对应的容器的信息，另一种是挖掘 PID 对应的 Pod 的信息。\n1. Container ID # 要获取容器的 ID，可以查看 PID 对应的 cgroup 信息：\n$ cat /proc/14338/cgroup 11:blkio:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 10:cpuset:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 9:freezer:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 8:hugetlb:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 7:perf_event:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 6:cpuacct,cpu:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 5:pids:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 4:devices:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 3:net_prio,net_cls:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 2:memory:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 1:name=systemd:/kubepods/burstable/pod8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/d6f24b62ea28e9e67f7bc06f98de083cc49454f353389cd396f5d3ac6448f19c 可以看到该进程对应的容器 ID 为 d6f24b62...，可以再优化一下上面的命令，直接获取容器 ID：\n$ CID=$(cat /proc/14338/cgroup | awk -F \u0026#39;/\u0026#39; \u0026#39;{print $5}\u0026#39;) $ echo ${CID:0:8} d6f24b62 最后一步根据容器 ID 获取 Pod 名称，如果你的容器运行时是 containerd 或 crio，可以使用 crictl 来获取容器信息：\n# Go Template $ crictl inspect -o go-template --template=\u0026#39;{{index .status.labels \u0026#34;io.kubernetes.pod.name\u0026#34;}}\u0026#39; d6f24b62 prometheus-k8s-0 # jq $ crictl inspect d6f24b62|jq \u0026#39;.status.labels[\u0026#34;io.kubernetes.pod.name\u0026#34;]\u0026#39; \u0026#34;prometheus-k8s-0\u0026#34; 使用 Go template 或 jq 都能获取 Pod 名称，看个人喜好。\n如果你的容器运行时是 Docker，可以使用命令行工具 docker 来获取，方法和上面类似。\n2. Pod UID # 下面来看看第二种方法，先根据 PID 直接获取 Pod UID：\n$ cat /proc/14338/mountinfo | grep \u0026#34;etc-hosts\u0026#34; | awk -F / {\u0026#39;print $6\u0026#39;} 8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1 然后根据 Pod UID 获取 Pod 名称：\n$ crictl ps -o json | jq \u0026#39;.[][].labels | select (.[\u0026#34;io.kubernetes.pod.uid\u0026#34;] == \u0026#34;8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1\u0026#34;) | .[\u0026#34;io.kubernetes.pod.name\u0026#34;]\u0026#39;|uniq \u0026#34;prometheus-k8s-0\u0026#34; 3. 整合 # 方法是有了，怎么才能将所有的步骤合并成一个步骤，一步到位获取 Pod 名称呢？可以在 ~/.bashrc 中添加一个 shell 函数，选择上面的方法 1，并使用 go template 来格式化（你也可以使用上面提到的其他方法，但需要安装 jq）：\npodinfo() { CID=$(cat /proc/$1/cgroup | awk -F \u0026#39;/\u0026#39; \u0026#39;{print $5}\u0026#39;) CID=$(echo ${CID:0:8}) crictl inspect -o go-template --template=\u0026#39;{{index .status.labels \u0026#34;io.kubernetes.pod.name\u0026#34;}}\u0026#39; $CID } 执行下面的命令使修改立即生效：\n$ source ~/.bashrc 然后就可以使用该函数来获取 Pod 名称啦：\n$ podinfo 14338 prometheus-k8s-0 4. 举一反三 # 这个思路也可以用来解决其他问题，大家要学会举一反三，我举个例子。Kubernetes 中的很多组件都是通过 HTTPS 协议来暴露指标，比如 kubelet，那么如何使用 API 来访问这些指标呢？\n先选取一个容器，比如 prometheus，找到它的 PID：\n$ ps -ef|grep \u0026#34;/bin/prometheus\u0026#34; 1000 14338 14246 4 7月10 ? 04:29:02 /bin/prometheus --web.console.templates=/etc/prometheus/consoles --web.console.libraries=/etc/prometheus/console_libraries --config.file=/etc/prometheus/config_out/prometheus.env.yaml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=24h --web.enable-lifecycle --storage.tsdb.no-lockfile --web.route-prefix=/ 1000 14402 14246 0 7月10 ? 00:00:10 /bin/prometheus-config-reloader --log-format=logfmt --reload-url=http://localhost:9090/-/reload --config-file=/etc/prometheus/config/prometheus.yaml.gz --config-envsubst-file=/etc/prometheus/config_out/prometheus.env.yaml root 15956 555 0 18:19 pts/0 00:00:00 grep --color=auto /bin/prometheus 根据 PID 找到 Pod UID：\n$ cat /proc/14338/mountinfo | grep \u0026#34;etc-hosts\u0026#34; | awk -F / {\u0026#39;print $6\u0026#39;} 8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1 根据 Pod UID 找到 Service Account 的 token 挂载目录：\n$ ll /var/lib/kubelet/pods/8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/volumes/kubernetes.io~secret/prometheus-k8s-token-p7bgb/ 总用量 0 lrwxrwxrwx 1 root root 13 7月 10 21:24 ca.crt -\u0026gt; ..data/ca.crt lrwxrwxrwx 1 root root 16 7月 10 21:24 namespace -\u0026gt; ..data/namespace lrwxrwxrwx 1 root root 12 7月 10 21:24 token -\u0026gt; ..data/token 获取 token 信息：\n$ export TOKEN=$(cat /var/lib/kubelet/pods/8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/volumes/kubernetes.io~secret/prometheus-k8s-token-p7bgb/token) 通过 curl 直接访问指标：\n$ curl -s -H \u0026#34;Authorization: Bearer $TOKEN\u0026#34; --cacert /var/lib/kubelet/pods/8e018a8e-4aaa-4ac6-986a-1a5133a4bcf1/volumes/kubernetes.io~secret/prometheus-k8s-token-p7bgb/ca.crt --insecure https://127.0.0.1:10250/metrics/cadvisor 当然，如果你能找到集群管理员的证书、密钥和 CA 证书，也可以直接使用它们来访问，我就不展开说了。\n5. 真奇技淫巧 # 最后再介绍一个思路清奇的方案，虽然有点小瑕疵，但思路很巧妙，大家可以借鉴一下。Kubernetes 创建的容器中的主机名对应的就是 Pod 名称，沿着这个思路，我们可以得到一个更巧妙的方法，通过 PID 的 uts namespace 来获得容器的主机名，进而就可以知道 Pod 名称，具体可以借助 nsenter 这个工具：\n$ nsenter -t 14338 --uts hostname prometheus-k8s-0 这么一看，确实比上面的方法优雅多了，但这个方法会有一点小问题，当容器使用 HostNetwork 模式运行时，hostname 是宿主机的 hostname，通过这种方法就得不到 Pod 名称。虽然不是通用的方法，但思路还是可以借鉴的，除了使用 nsenter 获取主机名外，还可以通过环境变量来获取，命令如下：\n$ xargs -0 -L1 -a /proc/14338/environ | grep HOSTNAME HOSTNAME=prometheus-k8s-0 解释一下这几个参数：\n-0 : 表示使用 null 作为分隔符 -L : 表示指定多少行作为一个命令行参数。-L1 就表示指定 1 行作为命令行参数，即每一行分别运行一次命令。xargs 的作用就是将标准输入转换为命令行参数，如果 xargs 后面没有跟上真正要执行的命令，就表示使用默认的 echo。所以这里的 -L1 就表示分隔出来的每一行分别运行一次 echo 命令。 -a : 从文件中读取内容，而不是从标准输入读取。 如果你还不理解，好吧我尽力了。\n最后再推荐一个项目，可以找到所有容器的 PID 以及对应的 Pod 信息，项目地址： pid2pod。\n","date":"2020年7月14日","externalUrl":null,"permalink":"/posts/find-kubernetes-pod-info-from-process-id/","section":"博客","summary":"在管理 Kubernetes 集群的过程中，我们经常会遇到这样一种情况：在某台节点","title":"Kubernetes 教程：根据 PID 获取 Pod 名称","type":"posts"},{"content":" 上篇文章介绍了 WireGuard 相对于其他 VPN 协议的优点和 WireGuard 的工作原理，本文将会学习如何从零开始配置 WireGuard，这里会涉及到很多高级的配置方法，例如动态 IP、NAT 到 NAT、IPv6 等等。\n1. 快速开始 # 配置 WireGuard 的大致流程如下：\n安装 # CentOS7 Ubuntu MacOS Windows 其他 $ yum install epel-release https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm $ yum install yum-plugin-elrepo $ yum install kmod-wireguard wireguard-tools # 如果你使用的是非标准内核，需要安装 DKMS 包 $ yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm $ curl -o /etc/yum.repos.d/jdoss-wireguard-epel-7.repo https://copr.fedorainfracloud.org/coprs/jdoss/wireguard/repo/epel-7/jdoss-wireguard-epel-7.repo $ yum install wireguard-dkms wireguard-tools # Ubuntu ≥ 18.04 $ apt install wireguard # Ubuntu ≤ 16.04 $ add-apt-repository ppa:wireguard/wireguard $ apt-get update $ apt-get install wireguard $ brew install wireguard-tools 下载地址：\nhttps://download.wireguard.com/windows-client/wireguard-amd64-0.1.1.msi Android 和 iOS 可通过应用商店下载相关客户端\n其他操作系统请参考官方文档： https://www.wireguard.com/install/#installation 在中继服务器上开启 IP 地址转发：\n$ echo \u0026#34;net.ipv4.ip_forward = 1\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf $ echo \u0026#34;net.ipv4.conf.all.proxy_arp = 1\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf $ sysctl -p /etc/sysctl.conf 添加 iptables 规则，允许本机的 NAT 转换：\n$ iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT $ iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT $ iptables -A FORWARD -i wg0 -o wg0 -m conntrack --ctstate NEW -j ACCEPT $ iptables -t nat -A POSTROUTING -s 192.0.2.0/24 -o eth0 -j MASQUERADE 需要把 eth0 改成你实际使用的网卡接口名称。\n编写配置文件 # 配置文件可以放在任何路径下，但必须通过绝对路径引用。默认路径是 /etc/wireguard/wg0.conf。\n配置文件内容解析放到下一章节再讲。\n生成密钥 # #生成私钥 $ wg genkey \u0026gt; example.key # 生成公钥 $ wg pubkey \u0026lt; example.key \u0026gt; example.key.pub 启动与停止 # $ wg-quick up /full/path/to/wg0.conf $ wg-quick down /full/path/to/wg0.conf # 启动/停止 VPN 网络接口 $ ip link set wg0 up $ ip link set wg0 down # 注册/注销 VPN 网络接口 $ ip link add dev wg0 type wireguard $ ip link delete dev wg0 # 注册/注销 本地 VPN 地址 $ ip address add dev wg0 192.0.2.3/32 $ ip address delete dev wg0 192.0.2.3/32 # 添加/删除 VPN 路由 $ ip route add 192.0.2.3/32 dev wg0 $ ip route delete 192.0.2.3/32 dev wg0 查看信息 # 接口：\n# 查看系统 VPN 接口信息 $ ip link show wg0 # 查看 VPN 接口详细信息 $ wg show all $ wg show wg0 地址：\n# 查看 VPN 接口地址 $ ip address show wg0 路由 # # 查看系统路由表 $ ip route show table main $ ip route show table local # 获取到特定 IP 的路由 $ ip route get 192.0.2.3 一键安装 # 一键安装请参考这个项目： WireGuard installer\n2. 配置详解 # WireGuard 使用 INI 语法作为其配置文件格式。配置文件可以放在任何路径下，但必须通过绝对路径引用。默认路径是 /etc/wireguard/wg0.conf。\n配置文件的命名形式必须为 ${WireGuard 接口的名称}.conf。通常情况下 WireGuard 接口名称以 wg 为前缀，并从 0 开始编号，但你也可以使用其他名称，只要符合正则表达式 ^[a-zA-Z0-9_=+.-]{1,15}$ 就行。\n你可以选择使用 wg 命令来手动配置 VPN，但一般建议使用 wg-quick，它提供了更强大和用户友好的配置体验，可以通过配置文件来管理配置。\n下面是一个配置文件示例：\n[Interface] # Name = node1.example.tld Address = 192.0.2.3/32 ListenPort = 51820 PrivateKey = localPrivateKeyAbcAbcAbc= DNS = 1.1.1.1,8.8.8.8 Table = 12345 MTU = 1500 PreUp = /bin/example arg1 arg2 %i PostUp = /bin/example arg1 arg2 %i PreDown = /bin/example arg1 arg2 %i PostDown = /bin/example arg1 arg2 %i [Peer] # Name = node2-node.example.tld AllowedIPs = 192.0.2.1/24 Endpoint = node1.example.tld:51820 PublicKey = remotePublicKeyAbcAbcAbc= PersistentKeepalive = 25 [Interface] # 这一节定义本地 VPN 配置。例如：\n本地节点是客户端，只路由自身的流量，只暴露一个 IP。\n[Interface] # Name = phone.example-vpn.dev Address = 192.0.2.5/32 PrivateKey = \u0026lt;private key for phone.example-vpn.dev\u0026gt; 本地节点是中继服务器，它可以将流量转发到其他对等节点（peer），并公开整个 VPN 子网的路由。\n[Interface] # Name = public-server1.example-vpn.tld Address = 192.0.2.1/24 ListenPort = 51820 PrivateKey = \u0026lt;private key for public-server1.example-vpn.tld\u0026gt; DNS = 1.1.1.1 ① # Name # 这是 INI 语法中的标准注释，用于展示该配置部分属于哪个节点。这部分配置会被 WireGuard 完全忽略，对 VPN 的行为没有任何影响。\n② Address # 定义本地节点应该对哪个地址范围进行路由。如果是常规的客户端，则将其设置为节点本身的单个 IP（使用 CIDR 指定，例如 192.0.2.3/32）；如果是中继服务器，则将其设置为可路由的子网范围。\n例如：\n常规客户端，只路由自身的流量：Address = 192.0.2.3/32 中继服务器，可以将流量转发到其他对等节点（peer）：Address = 192.0.2.1/24 也可以指定多个子网或 IPv6 子网：Address = 192.0.2.1/24,2001:DB8::/64 ③ ListenPort # 当本地节点是中继服务器时，需要通过该参数指定端口来监听传入 VPN 连接，默认端口号是 51820。常规客户端不需要此选项。\n④ PrivateKey # 本地节点的私钥，所有节点（包括中继服务器）都必须设置。不可与其他服务器共用。\n私钥可通过命令 wg genkey \u0026gt; example.key 来生成。\n⑤ DNS # 通过 DHCP 向客户端宣告 DNS 服务器。客户端将会使用这里指定的 DNS 服务器来处理 VPN 子网中的 DNS 请求，但也可以在系统中覆盖此选项。例如：\n如果不配置则使用系统默认 DNS 可以指定单个 DNS：DNS = 1.1.1.1 也可以指定多个 DNS：DNS = 1.1.1.1,8.8.8.8 ⑥ Table # 定义 VPN 子网使用的路由表，默认不需要设置。该参数有两个特殊的值需要注意：\nTable = off : 禁止创建路由 Table = auto（默认值） : 将路由添加到系统默认的 table 中，并启用对默认路由的特殊处理。 例如：Table = 1234\n⑦ MTU # 定义连接到对等节点（peer）的 MTU（Maximum Transmission Unit，最大传输单元），默认不需要设置，一般由系统自动确定。\n⑧ PreUp # 启动 VPN 接口之前运行的命令。这个选项可以指定多次，按顺序执行。\n例如：\n添加路由：PreUp = ip rule add ipproto tcp dport 22 table 1234 ⑨ PostUp # 启动 VPN 接口之后运行的命令。这个选项可以指定多次，按顺序执行。\n例如：\n从文件或某个命令的输出中读取配置值：\nPostUp = wg set %i private-key /etc/wireguard/wg0.key \u0026lt;(some command here) 添加一行日志到文件中：\nPostUp = echo \u0026#34;$(date +%s) WireGuard Started\u0026#34; \u0026gt;\u0026gt; /var/log/wireguard.log 调用 WebHook：\nPostUp = curl https://events.example.dev/wireguard/started/?key=abcdefg 添加路由：\nPostUp = ip rule add ipproto tcp dport 22 table 1234 添加 iptables 规则，启用数据包转发：\nPostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE 强制 WireGuard 重新解析对端域名的 IP 地址：\nPostUp = resolvectl domain %i \u0026#34;~.\u0026#34;; resolvectl dns %i 192.0.2.1; resolvectl dnssec %i yes ⑩ PreDown # 停止 VPN 接口之前运行的命令。这个选项可以指定多次，按顺序执行。\n例如：\n添加一行日志到文件中：\nPreDown = echo \u0026#34;$(date +%s) WireGuard Going Down\u0026#34; \u0026gt;\u0026gt; /var/log/wireguard.log 调用 WebHook：\nPreDown = curl https://events.example.dev/wireguard/stopping/?key=abcdefg ⑪ PostDown # 停止 VPN 接口之后运行的命令。这个选项可以指定多次，按顺序执行。\n例如：\n添加一行日志到文件中：\nPostDown = echo \u0026#34;$(date +%s) WireGuard Going Down\u0026#34; \u0026gt;\u0026gt; /var/log/wireguard.log 调用 WebHook：\nPostDown = curl https://events.example.dev/wireguard/stopping/?key=abcdefg 删除 iptables 规则，关闭数据包转发：\nPostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] # 定义能够为一个或多个地址路由流量的对等节点（peer）的 VPN 设置。对等节点（peer）可以是将流量转发到其他对等节点（peer）的中继服务器，也可以是通过公网或内网直连的客户端。\n中继服务器必须将所有的客户端定义为对等节点（peer），除了中继服务器之外，其他客户端都不能将位于 NAT 后面的节点定义为对等节点（peer），因为路由不可达。对于那些只为自己路由流量的客户端，只需将中继服务器作为对等节点（peer），以及其他需要直接访问的节点。\n举个例子，在下面的配置中，public-server1 作为中继服务器，其他的客户端有的是直连，有的位于 NAT 后面：\npublic-server1（中继服务器）\n[peer] : public-server2, home-server, laptop, phone\npublic-server2（直连客户端）\n[peer] : public-server1\nhome-server（客户端位于 NAT 后面）\n[peer] : public-server1, public-server2\nlaptop（客户端位于 NAT 后面）\n[peer] : public-server1, public-server2\nphone（客户端位于 NAT 后面）\n[peer] : public-server1, public-server2\n配置示例：\n对等节点（peer）是路由可达的客户端，只为自己路由流量\n[Peer] # Name = public-server2.example-vpn.dev Endpoint = public-server2.example-vpn.dev:51820 PublicKey = \u0026lt;public key for public-server2.example-vpn.dev\u0026gt; AllowedIPs = 192.0.2.2/32 对等节点（peer）是位于 NAT 后面的客户端，只为自己路由流量\n[Peer] # Name = home-server.example-vpn.dev Endpoint = home-server.example-vpn.dev:51820 PublicKey = \u0026lt;public key for home-server.example-vpn.dev\u0026gt; AllowedIPs = 192.0.2.3/32 对等节点（peer）是中继服务器，用来将流量转发到其他对等节点（peer）\n[Peer] # Name = public-server1.example-vpn.tld Endpoint = public-server1.example-vpn.tld:51820 PublicKey = \u0026lt;public key for public-server1.example-vpn.tld\u0026gt; # 路由整个 VPN 子网的流量 AllowedIPs = 192.0.2.1/24 PersistentKeepalive = 25 ① Endpoint # 指定远端对等节点（peer）的公网地址。如果对等节点（peer）位于 NAT 后面或者没有稳定的公网访问地址，就忽略这个字段。通常只需要指定中继服务器的 Endpoint，当然有稳定公网 IP 的节点也可以指定。例如：\n通过 IP 指定：\nEndpoint = 123.124.125.126:51820 通过域名指定：\nEndpoint = public-server1.example-vpn.tld:51820 ② AllowedIPs # 允许该对等节点（peer）发送过来的 VPN 流量中的源地址范围。同时这个字段也会作为本机路由表中 wg0 绑定的 IP 地址范围。如果对等节点（peer）是常规的客户端，则将其设置为节点本身的单个 IP；如果对等节点（peer）是中继服务器，则将其设置为可路由的子网范围。可以使用 , 来指定多个 IP 或子网范围。该字段也可以指定多次。\n当决定如何对一个数据包进行路由时，系统首先会选择最具体的路由，如果不匹配再选择更宽泛的路由。例如，对于一个发往 192.0.2.3 的数据包，系统首先会寻找地址为 192.0.2.3/32 的对等节点（peer），如果没有再寻找地址为 192.0.2.1/24 的对等节点（peer），以此类推。\n例如：\n对等节点（peer）是常规客户端，只路由自身的流量：\nAllowedIPs = 192.0.2.3/32 对等节点（peer）是中继服务器，可以将流量转发到其他对等节点（peer）：\nAllowedIPs = 192.0.2.1/24 对等节点（peer）是中继服务器，可以转发所有的流量，包括外网流量和 VPN 流量，可以用来干嘛你懂得：\nAllowedIPs = 0.0.0.0/0,::/0 对等节点（peer）是中继服务器，可以路由其自身和其他对等节点（peer）的流量：\nAllowedIPs = 192.0.2.3/32,192.0.2.4/32 对等节点（peer）是中继服务器，可以路由其自身的流量和它所在的内网的流量：\nAllowedIPs = 192.0.2.3/32,192.168.1.1/24 ③ PublicKey # 对等节点（peer）的公钥，所有节点（包括中继服务器）都必须设置。可与其他对等节点（peer）共用同一个公钥。\n公钥可通过命令 wg pubkey \u0026lt; example.key \u0026gt; example.key.pub 来生成，其中 example.key 是上面生成的私钥。\n例如：PublicKey = somePublicKeyAbcdAbcdAbcdAbcd=\n④ PersistentKeepalive # 如果连接是从一个位于 NAT 后面的对等节点（peer）到一个公网可达的对等节点（peer），那么 NAT 后面的对等节点（peer）必须定期发送一个出站 ping 包来检查连通性，如果 IP 有变化，就会自动更新Endpoint。\n例如：\n本地节点与对等节点（peer）可直连：该字段不需要指定，因为不需要连接检查。 对等节点（peer）位于 NAT 后面：该字段不需要指定，因为维持连接是客户端（连接的发起方）的责任。 本地节点位于 NAT 后面，对等节点（peer）公网可达：需要指定该字段 PersistentKeepalive = 25，表示每隔 25 秒发送一次 ping 来检查连接。 3. 高级特性 # IPv6 # 前面的例子主要使用 IPv4，WireGuard 也支持 IPv6。例如：\n[Interface] AllowedIps = 192.0.2.3/24, 2001:DB8::/64 [Peer] ... AllowedIPs = 0.0.0.0/0, ::/0 转发所有流量 # 如果你想通过 VPN 转发所有的流量，包括 VPN 子网和公网流量，需要在 [Peer] 的 AllowedIPs 中添加 0.0.0.0/0, ::/0。\n即便只转发 IPv4 流量，也要指定一个 IPv6 网段，以避免将 IPv6 数据包泄露到 VPN 之外。详情参考： reddit.com/r/WireGuard/comments/b0m5g2/ipv6_leaks_psa_for_anyone_here_using_wireguard_to\n例如：\n[Interface] # Name = phone.example-vpn.dev Address = 192.0.2.3/32 PrivateKey = \u0026lt;private key for phone.example-vpn.dev\u0026gt; [Peer] # Name = public-server1.example-vpn.dev PublicKey = \u0026lt;public key for public-server1.example-vpn.dev\u0026gt; Endpoint = public-server1.example-vpn.dev:51820 AllowedIPs = 0.0.0.0/0, ::/0 一般只有把 VPN 当做武当纵云梯来用时，才会需要转发所有流量，不多说，点到为止。\nNAT-to-NAT 连接 # 如果两个对等节点（peer）都位于 NAT 后面，想不通过中继服务器直接连接，需要保证至少有一个对等节点（peer）具有稳定的公网出口，使用静态公网 IP 或者通过 DDNS 动态更新 FQDN 都可以。\nWebRTC 协议可以动态配置两个 NAT 之间的连接，它可以通过信令服务器来检测每个主机的 IP:Port 组合。而 WireGuard 没有这个功能，它没有没有信令服务器来动态搜索其他主机，只能硬编码 Endpoint+ListenPort，并通过 PersistentKeepalive 来维持连接。\n总结一下 NAT-to-NAT 连接的前提条件：\n至少有一个对等节点（peer）有固定的公网 IP，如果都没有固定的公网 IP，也可以使用 DDNS 来维护一个稳定的域名。 至少有一个对等节点（peer）指定 UDP ListenPort，而且它的 NAT 路由器不能做 UDP 源端口随机化，否则返回的数据包将被发送到之前指定的 ListenPort，并被路由器丢弃，不会发送到新分配的随机端口。 所有的对等节点（peer）必须在 [Peer] 配置中启用其他对等节点（peer）的 PersistentKeepalive，这样就可以维持连接的持久性。 对于通信双方来说，只要服务端所在的 NAT 路由器没有指定到 NAT 后面的对等节点（peer）的转发规则，就需要进行 UDP 打洞。\nUDP 打洞的原理：\nPeer1 向 Peer2 发送一个 UDP 数据包，不过 Peer2 的 NAT 路由器不知道该将这个包发给谁，直接丢弃了，不过没关系，这一步的目的是让 Peer1 的 NAT 路由器能够接收 UDP 响应并转发到后面的 Peer1。 Peer2 向 Peer1 发送一个 UDP 数据包，由于上一步的作用，Peer1 的 NAT 路由器已经建立临时转发规则，可以接收 UDP 响应，所以可以接收到该数据包，并转发到 Peer1。 Peer1 向 Peer2 发送一个 UDP 响应，由于上一步的作用，由于上一步的作用，Peer2 的 NAT 路由器已经可以接收 UDP 响应，所以可以接收到该数据包，并转发到 Peer2。 这种发送一个初始的数据包被拒绝，然后利用路由器已建立的转发规则来接收响应的过程被称为 『UDP 打洞』。\n当你发送一个 UDP 数据包出去时，路由器通常会创建一个临时规则来映射源地址/端口和目的地址/端口，反之亦然。从目的地址和端口返回的 UDP 数据包会被转发到原来的源地址和端口，这就是大多数 UDP 应用在 NAT 后面的运作方式（如 BitTorrent、Skype 等）。这个临时规则会在一段时间后失效，所以 NAT 后面的客户端必须通过 PersistentKeepalive 定期发送数据包来维持连接的持久性。\n当两个对等节点（peer）都位于 NAT 后面时，要想让 UDP 打洞生效，需要两个节点在差不多的时间向对方发送数据包，这就意味着双方需要提前知道对方的公网地址和端口号，可以在 wg0.conf 中指定。\nUDP 打洞的局限性 # 从 2019 年开始，很多以前用过的老式打洞方法都不再有效了。以前很著名的就是 pwnat 开创的一种新的打洞方法，它能够在不需要代理、第三方服务器、upnp、DMZ、sproofing、dns 转换的情况下实现 NAT 中的 P2P 通信。它的原理也很简单：\n通过让客户端假装成为一个互联网上任意的 ICMP 跳跃点（ a random hop on the Internet）来解决这个问题，从而让服务端能够获取到客户端的 IP 地址。traceroute 命令也是使用这项技术来检测 Internet 上的跳跃点。\n具体来说，当服务器启动时，它开始向固定地址 3.3.3.3 发送固定的 ICMP 回应请求包（ICMP echo request packets）。显然，我们无法从 3.3.3.3 收到返回的 ICMP 回应数据包（ICMP echo packets）。然而，3.3.3.3 并不是我们可以访问的主机，我们也不是想伪装成它来发 ICMP 回应数据包。相反，pwnat 技术的实现原理在于，当我们的客户端想要连接服务端时，客户端（知道服务器IP地址）会向服务端送 ICMP 超时数据包（ICMP Time Exceeded packet）。 这个 ICMP 数据包里面包含了服务端发送到 3.3.3.3 的原始固定 ICMP 回应请求包。\n为什么要这样做呢？好吧，我们假装是互联网上的一个 ICMP 跳越点，礼貌地告诉服务器它原来的 ICMP 回应请求包无法传递到 3.3.3.3。而你的 NAT 是一个聪明的设备，它会注意到 ICMP 超时数据包内的数据包与服务器发出 ICMP 回应请求包相匹配。然后它将 ICMP 超时数据包转发回 NAT 后面的服务器，包括来自客户端的完整 IP 数据包头，从而让服务端知道客户端 IP 地址是什么！\n现在这种类似的 UDP 打洞方法受到了很多的限制，详情可以参考 上篇文章，这里不过多阐述。除了 UDP 打洞之外，我们仍然可以使用硬编码的方式指定两个对等节点（peer）的公网地址和端口号，这个方法对大多数 NAT 网络都有效。\n源端口随机化 # 如果所有的对等节点（peer）都在具有严格的 UDP 源端口随机化的 NAT 后面（比如大多数蜂窝网络），那么无法实现 NAT-to-NAT 连接。因为双方都无法协商出一个 ListenPort，并保证自己的 NAT 在发出 ping 包后能够接收发往该端口的流量，所以就无法初始化打洞，导致连接失败。因此，一般在 LTE/3G 网络中无法进行 p2p 通信。\n使用信令服务器 # 上节提到了，如果所有的对等节点（peer）都在具有严格的 UDP 源端口随机化的 NAT 后面，就无法直接实现 NAT-to-NAT 连接，但通过第三方的信令服务器是可以实现的。信令服务器相当于一个中转站，它会告诉通信双方关于对方的 IP:Port 信息。这里有几个项目可以参考：\ntakutakahashi/wg-connect git.zx2c4.com/wireguard-tools/tree/contrib/nat-hole-punching 动态 IP 地址 # WireGuard 只会在启动时解析域名，如果你使用 DDNS 来动态更新域名解析，那么每当 IP 发生变化时，就需要重新启动 WireGuard。目前建议的解决方案是使用 PostUp 钩子每隔几分钟或几小时重新启动 WireGuard 来强制解析域名。\n总的来说，NAT-to-NAT 连接极为不稳定，而且还有一堆其他的限制，所以还是建议通过中继服务器来通信。\nNAT-to-NAT 配置示例：\nPeer1：\n[Interface] ... ListenPort = 12000 [Peer] ... Endpoint = peer2.example-vpn.dev:12000 PersistentKeepalive = 25 Peer2：\n[Interface] ... ListenPort = 12000 [Peer] ... Endpoint = peer1.example-vpn.dev:12000 PersistentKeepalive = 25 更多资料：\nsamyk/pwnat en.wikipedia.org/wiki/UDP_hole_punching stackoverflow.com/questions/8892142/udp-hole-punching-algorithm stackoverflow.com/questions/12359502/udp-hole-punching-not-going-through-on-3g stackoverflow.com/questions/11819349/udp-hole-punching-not-possible-with-mobile-provider WireGuard/WireGuard@master/contrib/examples/nat-hole-punching staaldraad.github.io/2017/04/17/nat-to-nat-with-wireguard golb.hplar.ch/2019/01/expose-server-vpn.html 动态分配子网 IP # 这里指的是对等节点（peer）的 VPN 子网 IP 的动态分配，类似于 DHCP，不是指 Endpoint。\nWireGuard 官方已经在开发动态分配子网 IP 的功能，具体的实现可以看这里： WireGuard/wg-dynamic\n当然，你也可以使用 PostUp 在运行时从文件中读取 IP 值来实现一个动态分配 IP 的系统，类似于 Kubernetes 的 CNI 插件。例如：\n[Interface] ... PostUp = wg set %i allowed-ips /etc/wireguard/wg0.key \u0026lt;(some command) 奇技淫巧 # 共享一个 peers.conf 文件 # 介绍一个秘密功能，可以简化 WireGuard 的配置工作。如果某个 peer 的公钥与本地接口的私钥能够配对，那么 WireGuard 会忽略该 peer。利用这个特性，我们可以在所有节点上共用同一个 peer 列表，每个节点只需要单独定义一个 [Interface] 就行了，即使列表中有本节点，也会被忽略。具体方式如下：\n每个对等节点（peer）都有一个单独的 /etc/wireguard/wg0.conf 文件，只包含 [Interface] 部分的配置。 每个对等节点（peer）共用同一个 /etc/wireguard/peers.conf 文件，其中包含了所有的 peer。 Wg0.conf 文件中需要配置一个 PostUp 钩子，内容为 PostUp = wg addconf /etc/wireguard/peers.conf。 关于 peers.conf 的共享方式有很多种，你可以通过 ansible 这样的工具来分发，可以使用 Dropbox 之类的网盘来同步，当然也可以使用 ceph 这种分布式文件系统来将其挂载到不同的节点上。\n从文件或命令输出中读取配置 # WireGuard 也可以从任意命令的输出或文件中读取内容来修改配置的值，利用这个特性可以方便管理密钥，例如可以在运行时从 Kubernetes Secrets 或 AWS KMS 等第三方服务读取密钥。\n容器化 # WireGuard 也可以跑在容器中，最简单的方式是使用 --privileged 和 --cap-add=all 参数，让容器可以加载内核模块。\n你可以让 WireGuard 跑在容器中，向宿主机暴露一个网络接口；也可以让 WireGuard 运行在宿主机中，向特定的容器暴露一个接口。\n下面给出一个具体的示例，本示例中的 vpn_test 容器通过 WireGuard 中继服务器来路由所有流量。本示例中给出的容器配置是 docker-compose 的配置文件格式。\n中继服务器容器配置：\nversion: \u0026#39;3\u0026#39; services: wireguard: image: linuxserver/wireguard ports: - 51820:51820/udp cap_add: - NET_ADMIN - SYS_MODULE volumes: - /lib/modules:/lib/modules - ./wg0.conf:/config/wg0.conf:ro 中继服务器 WireGuard 配置 wg0.conf：\n[Interface] # Name = relay1.wg.example.com Address = 192.0.2.1/24 ListenPort = 51820 PrivateKey = oJpRt2Oq27vIB5/UVb7BRqCwad2YMReQgH5tlxz8YmI= DNS = 1.1.1.1,8.8.8.8 PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] # Name = peer1.wg.example.com PublicKey = I+hXRAJOG/UE2IQvIHsou2zTgkUyPve2pzvHTnd/2Gg= AllowedIPs = 192.0.2.2/32 客户端容器配置：\nversion: \u0026#39;3\u0026#39; services: wireguard: image: linuxserver/wireguard cap_add: - NET_ADMIN - SYS_MODULE volumes: - /lib/modules:/lib/modules - ./wg0.conf:/config/wg0.conf:ro vpn_test: image: curlimages/curl entrypoint: curl -s http://whatismyip.akamai.com/ network_mode: \u0026#39;service:wireguard\u0026#39; 客户端 WireGuard 配置 wg0.conf：\n[Interface] # Name = peer1.wg.example.com Address = 192.0.2.2/32 PrivateKey = YCW76edD4W7nZrPbWZxPZhcs32CsBLIi1sEhsV/sgk8= DNS = 1.1.1.1,8.8.8.8 [Peer] # Name = relay1.wg.example.com Endpoint = relay1.wg.example.com:51820 PublicKey = zJNKewtL3gcHdG62V3GaBkErFtapJWsAx+2um0c0B1s= AllowedIPs = 192.0.2.1/24,0.0.0.0/0 PersistentKeepalive = 21 ","date":"2020年7月6日","externalUrl":null,"permalink":"/posts/wireguard-docs-practice/","section":"博客","summary":"上篇文章介绍了 WireGuard 相对于其他 VPN 协议的优点和 WireGuard 的工作原理，本文将","title":"WireGuard 教程：WireGuard 的搭建使用与配置详解","type":"posts"},{"content":" 本文翻译自： https://github.com/pirate/wireguard-docs\nWireGuard 是由 Jason Donenfeld 等人用 C 语言编写的一个开源 VPN 协议，被视为下一代 VPN 协议，旨在解决许多困扰 IPSec/IKEv2、OpenVPN 或 L2TP 等其他 VPN 协议的问题。它与 Tinc 和 MeshBird 等现代 VPN 产品有一些相似之处，即加密技术先进、配置简单。从 2020 年 1 月开始，它已经并入了 Linux 内核的 5.6 版本，这意味着大多数 Linux 发行版的用户将拥有一个开箱即用的 WireGuard。\n无论你是想破墙而出，还是想在服务器之间组网，WireGuard 都不会让你失望，它就是组网的『乐高积木』，就像 ZFS 是构建文件系统的『乐高积木』一样。\nWireGuard 与其他 VPN 协议的性能测试对比：\n可以看到 WireGuard 直接碾压其他 VPN 协议。再来说说 OpenVPN，大约有 10 万行代码，而 WireGuard 只有大概 4000 行代码，代码库相当精简，简直就是件艺术品啊。你再看看 OpenVPN 的性能，算了不说了。\nWireGuard 优点：\n配置精简，可直接使用默认值 只需最少的密钥管理工作，每个主机只需要 1 个公钥和 1 个私钥。 就像普通的以太网接口一样，以 Linux 内核模块的形式运行，资源占用小。 能够将部分流量或所有流量通过 VPN 传送到局域网内的任意主机。 能够在网络故障恢复之后自动重连，戳到了其他 VPN 的痛处。 比目前主流的 VPN 协议，连接速度要更快，延迟更低（见上图）。 使用了更先进的加密技术，具有前向加密和抗降级攻击的能力。 支持任何类型的二层网络通信，例如 ARP、DHCP 和 ICMP，而不仅仅是 TCP/HTTP。 可以运行在主机中为容器之间提供通信，也可以运行在容器中为主机之间提供通信。 WireGuard 不能做的事：\n类似 gossip 协议实现网络自愈。 通过信令服务器突破双重 NAT。 通过中央服务器自动分配和撤销密钥。 发送原始的二层以太网帧。 当然，你可以使用 WireGuard 作为底层协议来实现自己想要的功能，从而弥补上述这些缺憾。\n本系列 WireGuard 教程分为两个部分，第一部分偏理论，第二部分偏实践。本文是第一部分，下面开始正文教程。\n1. WireGuard 术语 # Peer/Node/Device # 连接到 VPN 并为自己注册一个 VPN 子网地址（如 192.0.2.3）的主机。还可以通过使用逗号分隔的 CIDR 指定子网范围，为其自身地址以外的 IP 地址选择路由。\n中继服务器（Bounce Server） # 一个公网可达的对等节点，可以将流量中继到 NAT 后面的其他对等节点。Bounce Server 并不是特殊的节点，它和其他对等节点一样，唯一的区别是它有公网 IP，并且开启了内核级别的 IP 转发，可以将 VPN 的流量转发到其他客户端。\n子网（Subnet） # 一组私有 IP，例如 192.0.2.1-255 或 192.168.1.1/24，一般在 NAT 后面，例如办公室局域网或家庭网络。\nCIDR 表示法 # 这是一种使用掩码表示子网大小的方式，这个不用解释了。\nNAT # 子网的私有 IP 地址由路由器提供，通过公网无法直接访问私有子网设备，需要通过 NAT 做网络地址转换。路由器会跟踪发出的连接，并将响应转发到正确的内部 IP。\n公开端点（Public Endpoint） # 节点的公网 IP 地址:端口，例如 123.124.125.126:1234，或者直接使用域名 some.domain.tld:1234。如果对等节点不在同一子网中，那么节点的公开端点必须使用公网 IP 地址。\n私钥（Private key） # 单个节点的 WireGuard 私钥，生成方法是：wg genkey \u0026gt; example.key。\n公钥（Public key） # 单个节点的 WireGuard 公钥，生成方式为：wg pubkey \u0026lt; example.key \u0026gt; example.key.pub。\nDNS # 域名服务器，用于将域名解析为 VPN 客户端的 IP，不让 DNS请求泄漏到 VPN 之外。\n2. WireGuard 工作原理 # 中继服务器工作原理 # 中继服务器（Bounce Server）和普通的对等节点一样，它能够在 NAT 后面的 VPN 客户端之间充当中继服务器，可以将收到的任何 VPN 子网流量转发到正确的对等节点。事实上 WireGuard 并不关心流量是如何转发的，这个由系统内核和 iptables 规则处理。\n如果所有的对等节点都是公网可达的，则不需要考虑中继服务器，只有当有对等节点位于 NAT 后面时才需要考虑。\n在 WireGuard 里，客户端和服务端基本是平等的，差别只是谁主动连接谁而已。双方都会监听一个 UDP 端口，谁主动连接，谁就是客户端。主动连接的客户端需要指定对端的公网地址和端口，被动连接的服务端不需要指定其他对等节点的地址和端口。如果客户端和服务端都位于 NAT 后面，需要加一个中继服务器，客户端和服务端都指定中继服务器作为对等节点，它们的通信流量会先进入中继服务器，然后再转发到对端。\nWireGuard 是支持漫游的，也就是说，双方不管谁的地址变动了，WireGuard 在看到对方从新地址说话的时候，就会记住它的新地址（跟 mosh 一样，不过是双向的）。所以双方要是一直保持在线，并且通信足够频繁的话（比如配置 persistent-keepalive），两边的 IP 都不固定也不影响的。\nWireguard 如何路由流量 # 利用 WireGuard 可以组建非常复杂的网络拓扑，这里主要介绍几个典型的拓扑：\n① 端到端直接连接\n这是最简单的拓扑，所有的节点要么在同一个局域网，要么直接通过公网访问，这样 WireGuard 可以直接连接到对端，不需要中继跳转。\n② 一端位于 NAT 后面，另一端直接通过公网暴露\n这种情况下，最简单的方案是：通过公网暴露的一端作为服务端，另一端指定服务端的公网地址和端口，然后通过 persistent-keepalive 选项维持长连接，让 NAT 记得对应的映射关系。\n③ 两端都位于 NAT 后面，通过中继服务器连接\n大多数情况下，当通信双方都在 NAT 后面的时候，NAT 会做源端口随机化处理，直接连接可能比较困难。可以加一个中继服务器，通信双方都将中继服务器作为对端，然后维持长连接，流量就会通过中继服务器进行转发。\n④ 两端都位于 NAT 后面，通过 UDP NAT 打洞\n上面也提到了，当通信双方都在 NAT 后面的时候，直接连接不太现实，因为大多数 NAT 路由器对源端口的随机化相当严格，不可能提前为双方协调一个固定开放的端口。必须使用一个信令服务器（STUN），它会在中间沟通分配给对方哪些随机源端口。通信双方都会和公共信令服务器进行初始连接，然后它记录下随机的源端口，并将其返回给客户端。这其实就是现代 P2P 网络中 WebRTC 的工作原理。有时候，即使有了信令服务器和两端已知的源端口，也无法直接连接，因为 NAT 路由器严格规定只接受来自原始目的地址（信令服务器）的流量，会要求新开一个随机源端口来接受来自其他 IP 的流量（比如其他客户端试图使用原来的通信源端口）。运营商级别的 NAT 就是这么干的，比如蜂窝网络和一些企业网络，它们专门用这种方法来防止打洞连接。更多细节请参考下一部分的 NAT 到 NAT 连接实践的章节。\n如果某一端同时连接了多个对端，当它想访问某个 IP 时，如果有具体的路由可用，则优先使用具体的路由，否则就会将流量转发到中继服务器，然后中继服务器再根据系统路由表进行转发。你可以通过测量 ping 的时间来计算每一跳的长度，并通过检查对端的输出（wg show wg0）来找到 WireGuard 对一个给定地址的路由方式。\nWireGuard 报文格式 # WireGuard 使用加密的 UDP 报文来封装所有的数据，UDP 不保证数据包一定能送达，也不保证按顺序到达，但隧道内的 TCP 连接可以保证数据有效交付。WireGuard 的报文格式如下图所示：\n关于 WireGuard 报文的更多信息可以参考下面几篇文档：\nwireshark.org/docs/dfref/w/wg.html Lekensteyn/wireguard-dissector nbsoftsolutions.com/blog/viewing-wireguard-traffic-with-tcpdump WireGuard 的性能 # WireGuard 声称其性能比大多数 VPN 协议更好，但这个事情有很多争议，比如某些加密方式支持硬件层面的加速。\nWireGuard 直接在内核层面处理路由，直接使用系统内核的加密模块来加密数据，和 Linux 原本内置的密码子系统共存，原有的子系统能通过 API 使用 WireGuard 的 Zinc 密码库。WireGuard 使用 UDP 协议传输数据，在不使用的情况下默认不会传输任何 UDP 数据包，所以比常规 VPN 省电很多，可以像 55 一样一直挂着使用，速度相比其他 VPN 也是压倒性优势。\n关于性能比较的更多信息可以参考下面几篇文档：\nwireguard.com/performance reddit.com/r/linux/comments/9bnowo/wireguard_benchmark_between_two_servers_with_10 restoreprivacy.com/openvpn-ipsec-wireguard-l2tp-ikev2-protocols WireGuard 安全模型 # WireGuard 使用以下加密技术来保障数据的安全：\n使用 ChaCha20 进行对称加密，使用 Poly1305 进行数据验证。 利用 Curve25519 进行密钥交换。 使用 BLAKE2 作为哈希函数。 使用 HKDF 进行解密。 WireGuard 的加密技术本质上是 Trevor Perrin 的 Noise 框架的实例化，它简单高效，其他的 VPN 都是通过一系列协商、握手和复杂的状态机来保障安全性。WireGuard 就相当于 VPN 协议中的 qmail，代码量比其他 VPN 协议少了好几个数量级。\n关于 WireGuard 加密的更多资料请参考下方链接：\nwireguard.com/papers/wireguard.pdf eprint.iacr.org/2018/080.pdf courses.csail.mit.edu/6.857/2018/project/He-Xu-Xu-WireGuard.pdf wireguard.com/talks/blackhat2018-slides.pdf arstechnica.com/gadgets/2018/08/wireguard-vpn-review-fast-connections-amaze-but-windows-support-needs-to-happen WireGuard 密钥管理 # WireGuard 通过为每个对等节点提供简单的公钥和私钥来实现双向认证，每个对等节点在设置阶段生成密钥，且只在对等节点之间共享密钥。每个节点除了公钥和私钥，不再需要其他证书或预共享密钥。\n在更大规模的部署中，可以使用 Ansible 或 Kubernetes Secrets 等单独的服务来处理密钥的生成、分发和销毁。\n下面是一些有助于密钥分发和部署的服务：\npypi.org/project/wireguard-p2p trailofbits/algo StreisandEffect/streisand its0x08/wg-install brittson/wireguard_config_maker wireguardconfig.com 如果你不想在 wg0.conf 配置文件中直接硬编码，可以从文件或命令中读取密钥，这使得通过第三方服务管理密钥变得更加容易：\n[Interface] ... PostUp = wg set %i private-key /etc/wireguard/wg0.key \u0026lt;(cat /some/path/%i/privkey) 从技术上讲，多个服务端之间可以共享相同的私钥，只要客户端不使用相同的密钥同时连接到两个服务器。但有时客户端会需要同时连接多台服务器，例如，你可以使用 DNS 轮询来均衡两台服务器之间的连接，这两台服务器配置相同。大多数情况下，每个对等节点都应该使用独立的的公钥和私钥，这样每个对等节点都不能读取到对方的流量，保障了安全性。\n理论部分就到这里，下篇文章将会手把手教你如何从零开始配置 WireGuard，这里会涉及到很多高级的配置方法，例如动态 IP、NAT 到 NAT、IPv6 等等。\n","date":"2020年7月1日","externalUrl":null,"permalink":"/posts/wireguard-docs-theory/","section":"博客","summary":"本文翻译自： https://github.com/pirate/wireguard-docs WireGuard 是由 Jason Donenfeld 等人用 C 语言编写的一个开源 VPN 协议，被","title":"WireGuard 教程：WireGuard 的工作原理","type":"posts"},{"content":"微软现在大搞副业，就是不肯在 Windows 系统上下功夫，最近又改了 GitHub UI 布局设计。核心思想是好的，利用了屏幕的宽度，首屏展示了更多的信息，元素设计上也更现代了一些。好看不好看不说，理念上的确先进了不少。\n但有很多用户还是习惯以前的 UI 布局，怎么办呢？这里给大家推荐一个浏览器插件，可以让你在访问 GitHub 时使用以前的 UI 界面。插件的名字也很直接，就叫 Old GitHub UI，支持 Chrome 和 Firefox。主要改进了以下几个布局：\n① 将侧边栏信息和 header 信息移到主体部分\n使用插件前：\n使用插件后：\n② 高亮选中的 header\n使用插件前：\n使用插件后：\n③ 使用经典的按钮样式，移除图片圆角\n使用插件前：\n使用插件后：\n","date":"2020年6月30日","externalUrl":null,"permalink":"/posts/old-github-ui/","section":"博客","summary":"微软现在大搞副业，就是不肯在 Windows 系统上下功夫，最近又改了 GitHub UI 布","title":"让 Github 回到旧版 UI","type":"posts"},{"content":"","date":"2020年6月26日","externalUrl":null,"permalink":"/tags/calico/","section":"标签","summary":"","title":"Calico","type":"tags"},{"content":"Calico 中最核心的组件就是 Felix，它负责设置路由表和 ACL 规则等，以便为该主机上的 endpoints 资源正常运行提供所需的网络连接。同时它还负责提供有关网络健康状况的数据（例如，报告配置其主机时发生的错误和问题），这些数据会被写入 etcd，以使其对网络中的其他组件和操作人员可见。\n由此可见，对于我们的监控来说，监控 Calico 的核心便是监控 Felix，Felix 就相当于 Calico 的大脑。本文将学习如何使用 Prometheus-Operator 来监控 Calico。\n本文不会涉及到 Calico 和 Prometheus-Operator 的部署细节，如果不知道如何部署，请查阅官方文档和相关博客。 1. 配置 Calico 以启用指标 # 默认情况下 Felix 的指标是被禁用的，必须通过命令行管理工具 calicoctl 手动更改 Felix 配置才能开启，需要提前配置好命令行管理工具。\n本文使用的 Calico 版本是 v3.15.0，其他版本类似。先下载管理工具：\n$ wget https://github.com/projectcalico/calicoctl/releases/download/v3.15.0/calicoctl -O /usr/local/bin/calicoctl $ chmod +x /usr/local/bin/calicoctl 接下来需要设置 calicoctl 配置文件（默认是 /etc/calico/calicoctl.cfg）。如果你的 Calico 后端存储使用的是 Kubernetes API，那么配置文件内容如下：\napiVersion: projectcalico.org/v3 kind: CalicoAPIConfig metadata: spec: datastoreType: \u0026#34;kubernetes\u0026#34; kubeconfig: \u0026#34;/root/.kube/config\u0026#34; 如果 Calico 后端存储使用的是 etcd，那么配置文件内容如下：\napiVersion: projectcalico.org/v3 kind: CalicoAPIConfig metadata: spec: datastoreType: \u0026#34;etcdv3\u0026#34; etcdEndpoints: https://192.168.57.51:2379,https://192.168.57.52:2379,https://192.168.57.53:2379 etcdKeyFile: /opt/kubernetes/ssl/server-key.pem etcdCertFile: /opt/kubernetes/ssl/server.pem etcdCACertFile: /opt/kubernetes/ssl/ca.pem 你需要将其中的证书路径换成你的 etcd 证书路径。\n配置好了 calicoctl 之后就可以查看或修改 Calico 的配置了，先来看一下默认的 Felix 配置：\n$ calicoctl get felixConfiguration default -o yaml apiVersion: projectcalico.org/v3 kind: FelixConfiguration metadata: creationTimestamp: \u0026#34;2020-06-25T14:37:28Z\u0026#34; name: default resourceVersion: \u0026#34;269031\u0026#34; uid: 52146c95-ff97-40a9-9ba7-7c3b4dd3ba57 spec: bpfLogLevel: \u0026#34;\u0026#34; ipipEnabled: true logSeverityScreen: Info reportingInterval: 0s 可以看到默认的配置中没有启用指标，需要手动修改配置，命令如下：\n$ calicoctl patch felixConfiguration default --patch \u0026#39;{\u0026#34;spec\u0026#34;:{\u0026#34;prometheusMetricsEnabled\u0026#34;: true}}\u0026#39; Felix 暴露指标的端口是 9091，可通过检查监听端口来验证是否开启指标：\n$ ss -tulnp|grep 9091 tcp LISTEN 0 4096 [::]:9091 [::]:* users:((\u0026#34;calico-node\u0026#34;,pid=13761,fd=9)) $ curl -s http://localhost:9091/metrics # HELP felix_active_local_endpoints Number of active endpoints on this host. # TYPE felix_active_local_endpoints gauge felix_active_local_endpoints 1 # HELP felix_active_local_policies Number of active policies on this host. # TYPE felix_active_local_policies gauge felix_active_local_policies 0 # HELP felix_active_local_selectors Number of active selectors on this host. # TYPE felix_active_local_selectors gauge felix_active_local_selectors 0 ... 2. Prometheus 采集 Felix 指标 # 启用了 Felix 的指标后，就可以通过 Prometheus-Operator 来采集指标数据了。Prometheus-Operator 在部署时会创建 Prometheus、PodMonitor、ServiceMonitor、AlertManager 和 PrometheusRule 这 5 个 CRD 资源对象，然后会一直监控并维持这 5 个资源对象的状态。其中 Prometheus 这个资源对象就是对 Prometheus Server 的抽象。而 PodMonitor 和 ServiceMonitor 就是 exporter 的各种抽象，是用来提供专门提供指标数据接口的工具，Prometheus 就是通过 PodMonitor 和 ServiceMonitor 提供的指标数据接口去 pull 数据的。\nServiceMonitor 要求被监控的服务必须有对应的 Service，而 PodMonitor 则不需要，本文选择使用 PodMonitor 来采集 Felix 的指标。\nPodMonitor 虽然不需要应用创建相应的 Service，但必须在 Pod 中指定指标的端口和名称，因此需要先修改 DaemonSet calico-node 的配置，指定端口和名称。先用以下命令打开 DaemonSet calico-node 的配置：\n$ kubectl -n kube-system edit ds calico-node 然后在线修改，在 spec.template.sepc.containers 中加入以下内容：\nports: - containerPort: 9091 name: http-metrics protocol: TCP 创建 Pod 对应的 PodMonitor：\n# prometheus-podMonitorCalico.yaml apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: labels: k8s-app: calico-node name: felix namespace: monitoring spec: podMetricsEndpoints: - interval: 15s path: /metrics port: http-metrics namespaceSelector: matchNames: - kube-system selector: matchLabels: k8s-app: calico-node $ kubectl apply -f prometheus-podMonitorCalico.yaml 有几个参数需要注意：\nPodMonitor 的 name 最终会反应到 Prometheus 的配置中，作为 job_name。\npodMetricsEndpoints.port 需要和被监控的 Pod 中的 ports.name 相同，此处为 http-metrics。\nnamespaceSelector.matchNames 需要和被监控的 Pod 所在的 namespace 相同，此处为 kube-system。\nselector.matchLabels 的标签必须和被监控的 Pod 中能唯一标明身份的标签对应。\n最终 Prometheus-Operator 会根据 PodMonitor 来修改 Prometheus 的配置文件，以实现对相关的 Pod 进行监控。可以打开 Prometheus 的 UI 查看监控目标：\n注意 Labels 中有 pod=\u0026quot;calico-node-xxx\u0026quot;，表明监控的是 Pod。\n3. 可视化监控指标 # 采集完指标之后，就可以通过 Grafana 的仪表盘来展示监控指标了。Prometheus-Operator 中部署的 Grafana 无法实时修改仪表盘的配置（必须提前将仪表盘的 json 文件挂载到 Grafana Pod 中），而且也不是最新版（7.0 以上版本），所以我选择删除 Prometheus-Operator 自带的 Grafana，自行部署 helm 仓库中的 Grafana。先进入 kube-prometheus 项目的 manifests 目录，然后将 Grafana 相关的部署清单都移到同一个目录下，再删除 Grafana：\n$ cd kube-prometheus/manifests $ mkdir grafana $ mv grafana-* grafana/ $ kubectl delete -f grafana/ 然后通过 helm 部署最新的 Grafana：\n$ helm install grafana stable/grafana -n monitoring 访问 Grafana 的密码保存在 Secret 中，可以通过以下命令查看：\n$ kubectl -n monitoring get secret grafana -o yaml apiVersion: v1 data: admin-password: MnpoV3VaMGd1b3R3TDY5d3JwOXlIak4yZ3B2cTU1RFNKcVY0RWZsUw== admin-user: YWRtaW4= ldap-toml: \u0026#34;\u0026#34; kind: Secret metadata: ... 对密码进行解密：\n$ echo -n \u0026#34;MnpoV3VaMGd1b3R3TDY5d3JwOXlIak4yZ3B2cTU1RFNKcVY0RWZsUw==\u0026#34;|base64 -d 解密出来的信息就是访问密码。用户名是 admin。通过用户名和密码登录 Grafana 的 UI：\n添加 Prometheus-Operator 的数据源：\nCalico 官方没有单独 dashboard json，而是将其放到了 ConfigMap 中，我们需要从中提取需要的 json，提取出 felix-dashboard.json 的内容，然后将其中的 datasource 值替换为 prometheus。你可以用 sed 替换，也可以用编辑器，大多数编辑器都有全局替换的功能。如果你实在不知道如何提取，可以使用我提取好的 json：\nfelix-dashboard.json { \u0026#34;annotations\u0026#34;:{ \u0026#34;list\u0026#34;:[ { \u0026#34;builtIn\u0026#34;:1, \u0026#34;datasource\u0026#34;:\u0026#34;-- Grafana --\u0026#34;, \u0026#34;enable\u0026#34;:true, \u0026#34;hide\u0026#34;:true, \u0026#34;iconColor\u0026#34;:\u0026#34;rgba(0, 211, 255, 1)\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;Annotations \u0026amp; Alerts\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;dashboard\u0026#34; }] }, \u0026#34;description\u0026#34;:\u0026#34;Felix dashboard is part of calico documentation website, you will have great insight about you Calico instance by using this dashboard.\u0026#34;, \u0026#34;editable\u0026#34;:true, \u0026#34;gnetId\u0026#34;:12175, \u0026#34;graphTooltip\u0026#34;:0, \u0026#34;id\u0026#34;:1, \u0026#34;links\u0026#34;:[ { \u0026#34;icon\u0026#34;:\u0026#34;external link\u0026#34;, \u0026#34;includeVars\u0026#34;:false, \u0026#34;tags\u0026#34;:[ ], \u0026#34;targetBlank\u0026#34;:true, \u0026#34;title\u0026#34;:\u0026#34;Calico documentation\u0026#34;, \u0026#34;tooltip\u0026#34;:\u0026#34;Comprehensive tutorial on how to use this dashboard.\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;link\u0026#34;, \u0026#34;url\u0026#34;:\u0026#34;https://docs.projectcalico.org/master/maintenance/monitor/monitor-component-visual\u0026#34; }], \u0026#34;panels\u0026#34;:[ { \u0026#34;collapsed\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:1, \u0026#34;w\u0026#34;:24, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:0 }, \u0026#34;id\u0026#34;:6, \u0026#34;panels\u0026#34;:[ ], \u0026#34;title\u0026#34;:\u0026#34;Alerts and general info\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;row\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;These metrics are part of general information related to your Calico implementation.\u0026#34;, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:1 }, \u0026#34;id\u0026#34;:2, \u0026#34;links\u0026#34;:[ ], \u0026#34;options\u0026#34;:{ \u0026#34;fieldOptions\u0026#34;:{ \u0026#34;calcs\u0026#34;:[ \u0026#34;lastNotNull\u0026#34;], \u0026#34;defaults\u0026#34;:{ \u0026#34;mappings\u0026#34;:[ ], \u0026#34;thresholds\u0026#34;:{ \u0026#34;mode\u0026#34;:\u0026#34;absolute\u0026#34;, \u0026#34;steps\u0026#34;:[ { \u0026#34;color\u0026#34;:\u0026#34;green\u0026#34;, \u0026#34;value\u0026#34;:null }, { \u0026#34;color\u0026#34;:\u0026#34;red\u0026#34;, \u0026#34;value\u0026#34;:80 }] } }, \u0026#34;overrides\u0026#34;:[ ], \u0026#34;values\u0026#34;:false }, \u0026#34;orientation\u0026#34;:\u0026#34;auto\u0026#34;, \u0026#34;showThresholdLabels\u0026#34;:false, \u0026#34;showThresholdMarkers\u0026#34;:true }, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_active_local_endpoints\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Active hosts on each node\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;gauge\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;colorBackground\u0026#34;:false, \u0026#34;colorValue\u0026#34;:true, \u0026#34;colors\u0026#34;:[ \u0026#34;#299c46\u0026#34;, \u0026#34;rgba(237, 129, 40, 0.89)\u0026#34;, \u0026#34;#d44a3a\u0026#34;], \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;none\u0026#34;, \u0026#34;gauge\u0026#34;:{ \u0026#34;maxValue\u0026#34;:100, \u0026#34;minValue\u0026#34;:0, \u0026#34;show\u0026#34;:false, \u0026#34;thresholdLabels\u0026#34;:false, \u0026#34;thresholdMarkers\u0026#34;:true }, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:3, \u0026#34;x\u0026#34;:8, \u0026#34;y\u0026#34;:1 }, \u0026#34;id\u0026#34;:25, \u0026#34;interval\u0026#34;:null, \u0026#34;links\u0026#34;:[ ], \u0026#34;mappingType\u0026#34;:1, \u0026#34;mappingTypes\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;value to text\u0026#34;, \u0026#34;value\u0026#34;:1 }, { \u0026#34;name\u0026#34;:\u0026#34;range to text\u0026#34;, \u0026#34;value\u0026#34;:2 }], \u0026#34;maxDataPoints\u0026#34;:100, \u0026#34;nullPointMode\u0026#34;:\u0026#34;connected\u0026#34;, \u0026#34;nullText\u0026#34;:null, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;postfix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;postfixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;prefixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;rangeMaps\u0026#34;:[ { \u0026#34;from\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;sparkline\u0026#34;:{ \u0026#34;fillColor\u0026#34;:\u0026#34;rgba(31, 118, 189, 0.18)\u0026#34;, \u0026#34;full\u0026#34;:false, \u0026#34;lineColor\u0026#34;:\u0026#34;rgb(31, 120, 193)\u0026#34;, \u0026#34;show\u0026#34;:false, \u0026#34;ymax\u0026#34;:null, \u0026#34;ymin\u0026#34;:null }, \u0026#34;tableColumn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;sum(rate(felix_iptables_save_errors[5m]))\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;iptables save errors\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;singlestat\u0026#34;, \u0026#34;valueFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;valueMaps\u0026#34;:[ { \u0026#34;op\u0026#34;:\u0026#34;=\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;valueName\u0026#34;:\u0026#34;current\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;colorBackground\u0026#34;:false, \u0026#34;colorValue\u0026#34;:true, \u0026#34;colors\u0026#34;:[ \u0026#34;#299c46\u0026#34;, \u0026#34;rgba(237, 129, 40, 0.89)\u0026#34;, \u0026#34;#d44a3a\u0026#34;], \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;none\u0026#34;, \u0026#34;gauge\u0026#34;:{ \u0026#34;maxValue\u0026#34;:100, \u0026#34;minValue\u0026#34;:0, \u0026#34;show\u0026#34;:false, \u0026#34;thresholdLabels\u0026#34;:false, \u0026#34;thresholdMarkers\u0026#34;:true }, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:3, \u0026#34;x\u0026#34;:11, \u0026#34;y\u0026#34;:1 }, \u0026#34;id\u0026#34;:23, \u0026#34;interval\u0026#34;:null, \u0026#34;links\u0026#34;:[ ], \u0026#34;mappingType\u0026#34;:1, \u0026#34;mappingTypes\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;value to text\u0026#34;, \u0026#34;value\u0026#34;:1 }, { \u0026#34;name\u0026#34;:\u0026#34;range to text\u0026#34;, \u0026#34;value\u0026#34;:2 }], \u0026#34;maxDataPoints\u0026#34;:100, \u0026#34;nullPointMode\u0026#34;:\u0026#34;connected\u0026#34;, \u0026#34;nullText\u0026#34;:null, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;postfix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;postfixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;prefixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;rangeMaps\u0026#34;:[ { \u0026#34;from\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;sparkline\u0026#34;:{ \u0026#34;fillColor\u0026#34;:\u0026#34;rgba(31, 118, 189, 0.18)\u0026#34;, \u0026#34;full\u0026#34;:false, \u0026#34;lineColor\u0026#34;:\u0026#34;rgb(31, 120, 193)\u0026#34;, \u0026#34;show\u0026#34;:false, \u0026#34;ymax\u0026#34;:null, \u0026#34;ymin\u0026#34;:null }, \u0026#34;tableColumn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;sum(rate(felix_ipset_errors[5m]))\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;ipset errors\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;singlestat\u0026#34;, \u0026#34;valueFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;valueMaps\u0026#34;:[ { \u0026#34;op\u0026#34;:\u0026#34;=\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;valueName\u0026#34;:\u0026#34;current\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;colorBackground\u0026#34;:false, \u0026#34;colorValue\u0026#34;:true, \u0026#34;colors\u0026#34;:[ \u0026#34;#299c46\u0026#34;, \u0026#34;rgba(237, 129, 40, 0.89)\u0026#34;, \u0026#34;#d44a3a\u0026#34;], \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;none\u0026#34;, \u0026#34;gauge\u0026#34;:{ \u0026#34;maxValue\u0026#34;:100, \u0026#34;minValue\u0026#34;:0, \u0026#34;show\u0026#34;:false, \u0026#34;thresholdLabels\u0026#34;:false, \u0026#34;thresholdMarkers\u0026#34;:true }, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:3, \u0026#34;x\u0026#34;:14, \u0026#34;y\u0026#34;:1 }, \u0026#34;id\u0026#34;:18, \u0026#34;interval\u0026#34;:null, \u0026#34;links\u0026#34;:[ ], \u0026#34;mappingType\u0026#34;:1, \u0026#34;mappingTypes\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;value to text\u0026#34;, \u0026#34;value\u0026#34;:1 }, { \u0026#34;name\u0026#34;:\u0026#34;range to text\u0026#34;, \u0026#34;value\u0026#34;:2 }], \u0026#34;maxDataPoints\u0026#34;:100, \u0026#34;nullPointMode\u0026#34;:\u0026#34;connected\u0026#34;, \u0026#34;nullText\u0026#34;:null, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;postfix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;postfixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;prefixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;rangeMaps\u0026#34;:[ { \u0026#34;from\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;sparkline\u0026#34;:{ \u0026#34;fillColor\u0026#34;:\u0026#34;rgba(31, 118, 189, 0.18)\u0026#34;, \u0026#34;full\u0026#34;:false, \u0026#34;lineColor\u0026#34;:\u0026#34;rgb(31, 120, 193)\u0026#34;, \u0026#34;show\u0026#34;:false, \u0026#34;ymax\u0026#34;:null, \u0026#34;ymin\u0026#34;:null }, \u0026#34;tableColumn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;max(felix_cluster_num_hosts)\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;Calico node\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Active calico nodes\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;singlestat\u0026#34;, \u0026#34;valueFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;valueMaps\u0026#34;:[ { \u0026#34;op\u0026#34;:\u0026#34;=\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;valueName\u0026#34;:\u0026#34;current\u0026#34; }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;This graph shows you all the errors that Calico encounters, it is important to note occasional errors are acceptable. However, rise in the number of error or constant error counters means Calico is not working properly.\u0026#34;, \u0026#34;fill\u0026#34;:1, \u0026#34;fillGradient\u0026#34;:0, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:7, \u0026#34;x\u0026#34;:17, \u0026#34;y\u0026#34;:1 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:28, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;hideEmpty\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:false, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:1, \u0026#34;links\u0026#34;:[ ], \u0026#34;nullPointMode\u0026#34;:\u0026#34;null as zero\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.2\u0026#34;, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;rate(felix_ipset_errors[5m])\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}} ipset errors\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }, { \u0026#34;expr\u0026#34;:\u0026#34;rate(felix_iptables_restore_errors[5m])\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;intervalFactor\u0026#34;:1, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}} iptables restore errors\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;B\u0026#34; }, { \u0026#34;expr\u0026#34;:\u0026#34;rate(felix_iptables_save_errors[5m])\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}} iptables save errors\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;C\u0026#34; }, { \u0026#34;expr\u0026#34;:\u0026#34;rate(felix_log_errors[5m])\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}} log errors\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;D\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Errors plot\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:1, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;More policies on Felix means more effort required by Calico to manage packets. \u0026#34;, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:5 }, \u0026#34;id\u0026#34;:20, \u0026#34;options\u0026#34;:{ \u0026#34;fieldOptions\u0026#34;:{ \u0026#34;calcs\u0026#34;:[ \u0026#34;mean\u0026#34;], \u0026#34;defaults\u0026#34;:{ \u0026#34;mappings\u0026#34;:[ ], \u0026#34;thresholds\u0026#34;:{ \u0026#34;mode\u0026#34;:\u0026#34;absolute\u0026#34;, \u0026#34;steps\u0026#34;:[ { \u0026#34;color\u0026#34;:\u0026#34;green\u0026#34;, \u0026#34;value\u0026#34;:null }, { \u0026#34;color\u0026#34;:\u0026#34;red\u0026#34;, \u0026#34;value\u0026#34;:80 }] } }, \u0026#34;overrides\u0026#34;:[ ], \u0026#34;values\u0026#34;:false }, \u0026#34;orientation\u0026#34;:\u0026#34;auto\u0026#34;, \u0026#34;showThresholdLabels\u0026#34;:false, \u0026#34;showThresholdMarkers\u0026#34;:true }, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_cluster_num_policies\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix cluster policies\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;gauge\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;colorBackground\u0026#34;:false, \u0026#34;colorValue\u0026#34;:true, \u0026#34;colors\u0026#34;:[ \u0026#34;#299c46\u0026#34;, \u0026#34;rgba(237, 129, 40, 0.89)\u0026#34;, \u0026#34;#d44a3a\u0026#34;], \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;none\u0026#34;, \u0026#34;gauge\u0026#34;:{ \u0026#34;maxValue\u0026#34;:100, \u0026#34;minValue\u0026#34;:0, \u0026#34;show\u0026#34;:false, \u0026#34;thresholdLabels\u0026#34;:false, \u0026#34;thresholdMarkers\u0026#34;:true }, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:3, \u0026#34;x\u0026#34;:8, \u0026#34;y\u0026#34;:5 }, \u0026#34;id\u0026#34;:29, \u0026#34;interval\u0026#34;:null, \u0026#34;links\u0026#34;:[ ], \u0026#34;mappingType\u0026#34;:1, \u0026#34;mappingTypes\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;value to text\u0026#34;, \u0026#34;value\u0026#34;:1 }, { \u0026#34;name\u0026#34;:\u0026#34;range to text\u0026#34;, \u0026#34;value\u0026#34;:2 }], \u0026#34;maxDataPoints\u0026#34;:100, \u0026#34;nullPointMode\u0026#34;:\u0026#34;connected\u0026#34;, \u0026#34;nullText\u0026#34;:null, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;postfix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;postfixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;prefixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;rangeMaps\u0026#34;:[ { \u0026#34;from\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;sparkline\u0026#34;:{ \u0026#34;fillColor\u0026#34;:\u0026#34;rgba(31, 118, 189, 0.18)\u0026#34;, \u0026#34;full\u0026#34;:false, \u0026#34;lineColor\u0026#34;:\u0026#34;rgb(31, 120, 193)\u0026#34;, \u0026#34;show\u0026#34;:false, \u0026#34;ymax\u0026#34;:null, \u0026#34;ymin\u0026#34;:null }, \u0026#34;tableColumn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;sum(rate(felix_iptables_restore_errors[5m]))\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;iptables restore errors\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;singlestat\u0026#34;, \u0026#34;valueFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;valueMaps\u0026#34;:[ { \u0026#34;op\u0026#34;:\u0026#34;=\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;valueName\u0026#34;:\u0026#34;current\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;colorBackground\u0026#34;:false, \u0026#34;colorValue\u0026#34;:true, \u0026#34;colors\u0026#34;:[ \u0026#34;#299c46\u0026#34;, \u0026#34;rgba(237, 129, 40, 0.89)\u0026#34;, \u0026#34;#d44a3a\u0026#34;], \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;none\u0026#34;, \u0026#34;gauge\u0026#34;:{ \u0026#34;maxValue\u0026#34;:100, \u0026#34;minValue\u0026#34;:0, \u0026#34;show\u0026#34;:false, \u0026#34;thresholdLabels\u0026#34;:false, \u0026#34;thresholdMarkers\u0026#34;:true }, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:3, \u0026#34;x\u0026#34;:11, \u0026#34;y\u0026#34;:5 }, \u0026#34;id\u0026#34;:26, \u0026#34;interval\u0026#34;:null, \u0026#34;links\u0026#34;:[ ], \u0026#34;mappingType\u0026#34;:1, \u0026#34;mappingTypes\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;value to text\u0026#34;, \u0026#34;value\u0026#34;:1 }, { \u0026#34;name\u0026#34;:\u0026#34;range to text\u0026#34;, \u0026#34;value\u0026#34;:2 }], \u0026#34;maxDataPoints\u0026#34;:100, \u0026#34;nullPointMode\u0026#34;:\u0026#34;connected\u0026#34;, \u0026#34;nullText\u0026#34;:null, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;postfix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;postfixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;prefixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;rangeMaps\u0026#34;:[ { \u0026#34;from\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;sparkline\u0026#34;:{ \u0026#34;fillColor\u0026#34;:\u0026#34;rgba(31, 118, 189, 0.18)\u0026#34;, \u0026#34;full\u0026#34;:false, \u0026#34;lineColor\u0026#34;:\u0026#34;rgb(31, 120, 193)\u0026#34;, \u0026#34;show\u0026#34;:false, \u0026#34;ymax\u0026#34;:null, \u0026#34;ymin\u0026#34;:null }, \u0026#34;tableColumn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;sum(rate(felix_log_errors[5m]))\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix log errors\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;singlestat\u0026#34;, \u0026#34;valueFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;valueMaps\u0026#34;:[ { \u0026#34;op\u0026#34;:\u0026#34;=\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;valueName\u0026#34;:\u0026#34;current\u0026#34; }, { \u0026#34;cacheTimeout\u0026#34;:null, \u0026#34;colorBackground\u0026#34;:false, \u0026#34;colorValue\u0026#34;:true, \u0026#34;colors\u0026#34;:[ \u0026#34;#299c46\u0026#34;, \u0026#34;rgba(237, 129, 40, 0.89)\u0026#34;, \u0026#34;#d44a3a\u0026#34;], \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;none\u0026#34;, \u0026#34;gauge\u0026#34;:{ \u0026#34;maxValue\u0026#34;:100, \u0026#34;minValue\u0026#34;:0, \u0026#34;show\u0026#34;:false, \u0026#34;thresholdLabels\u0026#34;:false, \u0026#34;thresholdMarkers\u0026#34;:true }, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:3, \u0026#34;x\u0026#34;:14, \u0026#34;y\u0026#34;:5 }, \u0026#34;id\u0026#34;:24, \u0026#34;interval\u0026#34;:null, \u0026#34;links\u0026#34;:[ ], \u0026#34;mappingType\u0026#34;:1, \u0026#34;mappingTypes\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;value to text\u0026#34;, \u0026#34;value\u0026#34;:1 }, { \u0026#34;name\u0026#34;:\u0026#34;range to text\u0026#34;, \u0026#34;value\u0026#34;:2 }], \u0026#34;maxDataPoints\u0026#34;:100, \u0026#34;nullPointMode\u0026#34;:\u0026#34;connected\u0026#34;, \u0026#34;nullText\u0026#34;:null, \u0026#34;pluginVersion\u0026#34;:\u0026#34;6.7.3\u0026#34;, \u0026#34;postfix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;postfixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;prefixFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;rangeMaps\u0026#34;:[ { \u0026#34;from\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;sparkline\u0026#34;:{ \u0026#34;fillColor\u0026#34;:\u0026#34;rgba(31, 118, 189, 0.18)\u0026#34;, \u0026#34;full\u0026#34;:false, \u0026#34;lineColor\u0026#34;:\u0026#34;rgb(31, 120, 193)\u0026#34;, \u0026#34;show\u0026#34;:false, \u0026#34;ymax\u0026#34;:null, \u0026#34;ymin\u0026#34;:null }, \u0026#34;tableColumn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;sum(rate(felix_resyncs_started[5m])) \u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix resync started\u0026#34;, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;singlestat\u0026#34;, \u0026#34;valueFontSize\u0026#34;:\u0026#34;200%\u0026#34;, \u0026#34;valueMaps\u0026#34;:[ { \u0026#34;op\u0026#34;:\u0026#34;=\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;N/A\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;null\u0026#34; }], \u0026#34;valueName\u0026#34;:\u0026#34;avg\u0026#34; }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;fill\u0026#34;:1, \u0026#34;fillGradient\u0026#34;:0, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:4, \u0026#34;w\u0026#34;:7, \u0026#34;x\u0026#34;:17, \u0026#34;y\u0026#34;:5 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:31, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:false, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:1, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_logs_dropped\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix dropped logs\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;collapsed\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:1, \u0026#34;w\u0026#34;:24, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:9 }, \u0026#34;id\u0026#34;:14, \u0026#34;panels\u0026#34;:[ ], \u0026#34;title\u0026#34;:\u0026#34;Dataplane\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;row\u0026#34; }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;Dataplane apply time can indicate how busy your Kubernetes instance is. This can slow down Calico performance\u0026#34;, \u0026#34;fill\u0026#34;:2, \u0026#34;fillGradient\u0026#34;:4, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:7, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:10 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:16, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:true, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:2, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_int_dataplane_apply_time_seconds{quantile=\\\u0026#34;0.5\\\u0026#34;}\u0026#34;, \u0026#34;format\u0026#34;:\u0026#34;time_series\u0026#34;, \u0026#34;instant\u0026#34;:false, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;intervalFactor\u0026#34;:1, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Dataplane apply time quantile 0.5\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;fill\u0026#34;:2, \u0026#34;fillGradient\u0026#34;:4, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:7, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:8, \u0026#34;y\u0026#34;:10 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:15, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:true, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:2, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_int_dataplane_apply_time_seconds{quantile=\\\u0026#34;0.9\\\u0026#34;}\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Dataplane apply time quantile 0.9\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;fill\u0026#34;:2, \u0026#34;fillGradient\u0026#34;:4, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:7, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:16, \u0026#34;y\u0026#34;:10 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:12, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:true, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:2, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_int_dataplane_apply_time_seconds{quantile=\\\u0026#34;0.99\\\u0026#34;}\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Dataplane apply time quantile 0.99\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;collapsed\u0026#34;:false, \u0026#34;datasource\u0026#34;:null, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:1, \u0026#34;w\u0026#34;:24, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:17 }, \u0026#34;id\u0026#34;:35, \u0026#34;panels\u0026#34;:[ ], \u0026#34;title\u0026#34;:\u0026#34;Route table\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;row\u0026#34; }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;fill\u0026#34;:1, \u0026#34;fillGradient\u0026#34;:0, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:7, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:0, \u0026#34;y\u0026#34;:18 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:33, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:false, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:1, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_route_table_list_seconds{quantile=\\\u0026#34;0.5\\\u0026#34;}\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix route table list seconds quantile 0.5\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;fill\u0026#34;:1, \u0026#34;fillGradient\u0026#34;:0, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:7, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:8, \u0026#34;y\u0026#34;:18 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:36, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:false, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:1, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_route_table_list_seconds{quantile=\\\u0026#34;0.9\\\u0026#34;}\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix route table list seconds quantile 0.9\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }, { \u0026#34;aliasColors\u0026#34;:{ }, \u0026#34;bars\u0026#34;:false, \u0026#34;dashLength\u0026#34;:10, \u0026#34;dashes\u0026#34;:false, \u0026#34;datasource\u0026#34;:\u0026#34;prometheus\u0026#34;, \u0026#34;fill\u0026#34;:1, \u0026#34;fillGradient\u0026#34;:0, \u0026#34;gridPos\u0026#34;:{ \u0026#34;h\u0026#34;:7, \u0026#34;w\u0026#34;:8, \u0026#34;x\u0026#34;:16, \u0026#34;y\u0026#34;:18 }, \u0026#34;hiddenSeries\u0026#34;:false, \u0026#34;id\u0026#34;:37, \u0026#34;legend\u0026#34;:{ \u0026#34;avg\u0026#34;:false, \u0026#34;current\u0026#34;:false, \u0026#34;max\u0026#34;:false, \u0026#34;min\u0026#34;:false, \u0026#34;show\u0026#34;:false, \u0026#34;total\u0026#34;:false, \u0026#34;values\u0026#34;:false }, \u0026#34;lines\u0026#34;:true, \u0026#34;linewidth\u0026#34;:1, \u0026#34;nullPointMode\u0026#34;:\u0026#34;null\u0026#34;, \u0026#34;options\u0026#34;:{ \u0026#34;dataLinks\u0026#34;:[ ] }, \u0026#34;percentage\u0026#34;:false, \u0026#34;pointradius\u0026#34;:2, \u0026#34;points\u0026#34;:false, \u0026#34;renderer\u0026#34;:\u0026#34;flot\u0026#34;, \u0026#34;seriesOverrides\u0026#34;:[ ], \u0026#34;spaceLength\u0026#34;:10, \u0026#34;stack\u0026#34;:false, \u0026#34;steppedLine\u0026#34;:false, \u0026#34;targets\u0026#34;:[ { \u0026#34;expr\u0026#34;:\u0026#34;felix_route_table_list_seconds{quantile=\\\u0026#34;0.99\\\u0026#34;}\u0026#34;, \u0026#34;interval\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;legendFormat\u0026#34;:\u0026#34;{{instance}}\u0026#34;, \u0026#34;refId\u0026#34;:\u0026#34;A\u0026#34; }], \u0026#34;thresholds\u0026#34;:[ ], \u0026#34;timeFrom\u0026#34;:null, \u0026#34;timeRegions\u0026#34;:[ ], \u0026#34;timeShift\u0026#34;:null, \u0026#34;title\u0026#34;:\u0026#34;Felix route table list seconds quantile 0.99\u0026#34;, \u0026#34;tooltip\u0026#34;:{ \u0026#34;shared\u0026#34;:true, \u0026#34;sort\u0026#34;:0, \u0026#34;value_type\u0026#34;:\u0026#34;individual\u0026#34; }, \u0026#34;transparent\u0026#34;:true, \u0026#34;type\u0026#34;:\u0026#34;graph\u0026#34;, \u0026#34;xaxis\u0026#34;:{ \u0026#34;buckets\u0026#34;:null, \u0026#34;mode\u0026#34;:\u0026#34;time\u0026#34;, \u0026#34;name\u0026#34;:null, \u0026#34;show\u0026#34;:true, \u0026#34;values\u0026#34;:[ ] }, \u0026#34;yaxes\u0026#34;:[ { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }, { \u0026#34;format\u0026#34;:\u0026#34;short\u0026#34;, \u0026#34;label\u0026#34;:null, \u0026#34;logBase\u0026#34;:1, \u0026#34;max\u0026#34;:null, \u0026#34;min\u0026#34;:null, \u0026#34;show\u0026#34;:true }], \u0026#34;yaxis\u0026#34;:{ \u0026#34;align\u0026#34;:false, \u0026#34;alignLevel\u0026#34;:null } }], \u0026#34;refresh\u0026#34;:false, \u0026#34;schemaVersion\u0026#34;:22, \u0026#34;style\u0026#34;:\u0026#34;dark\u0026#34;, \u0026#34;tags\u0026#34;:[ \u0026#34;calico\u0026#34;, \u0026#34;felix\u0026#34;, \u0026#34;kubernetes\u0026#34;, \u0026#34;k8s\u0026#34;, \u0026#34;calico-node\u0026#34;, \u0026#34;cloud\u0026#34;, \u0026#34;cluster monitoring\u0026#34;, \u0026#34;policy monitoring\u0026#34;], \u0026#34;templating\u0026#34;:{ \u0026#34;list\u0026#34;:[ ] }, \u0026#34;time\u0026#34;:{ \u0026#34;from\u0026#34;:\u0026#34;now-6h\u0026#34;, \u0026#34;to\u0026#34;:\u0026#34;now\u0026#34; }, \u0026#34;timepicker\u0026#34;:{ \u0026#34;refresh_intervals\u0026#34;:[ \u0026#34;5s\u0026#34;, \u0026#34;10s\u0026#34;, \u0026#34;30s\u0026#34;, \u0026#34;1m\u0026#34;, \u0026#34;5m\u0026#34;, \u0026#34;15m\u0026#34;, \u0026#34;30m\u0026#34;, \u0026#34;1h\u0026#34;, \u0026#34;2h\u0026#34;, \u0026#34;1d\u0026#34;] }, \u0026#34;timezone\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;title\u0026#34;:\u0026#34;Felix Dashboard (Calico)\u0026#34;, \u0026#34;uid\u0026#34;:\u0026#34;calico-felix-dashboard\u0026#34;, \u0026#34;variables\u0026#34;:{ \u0026#34;list\u0026#34;:[ ] }, \u0026#34;version\u0026#34;:1 } 修改完了之后，将 json 内容导入到 Grafana：\n最后得到的 Felix 仪表盘如下图所示：\n如果你对我截图中 Grafana 的主题配色很感兴趣，可以参考这篇文章： Grafana 自定义主题。\n","date":"2020年6月26日","externalUrl":null,"permalink":"/posts/monitoring-calico-with-prometheus-operator/","section":"博客","summary":"Calico 中最核心的组件就是 Felix，它负责设置路由表和 ACL 规则等，","title":"使用 Prometheus-Operator 监控 Calico","type":"posts"},{"content":"Kubernetes 的桌面客户端有那么几个，曾经 Kubernetic 应该是最好用的，但最近有个叫 Lens 的 APP 改变了这个格局，功能比 Kubernetic 多，使用体验更好，适合广大系统重启工程师装逼。它有以下几个亮点：\n① Lens 就是一个强大的 IDE，可以实时查看集群状态，实时查看日志流，方便排查故障。有了 Lens，你可以更方便快捷地使用你的集群，从根本上提高工作效率和业务迭代速度。\n日志流界面可以选择显示或隐藏时间戳，也可以指定显示的行数：\n② Lens 可以管理多集群，它使用内置的 kubectl 通过 kubeconfig 来访问集群，支持本地集群和外部集群（如EKS、AKS、GKE、Pharos、UCP、Rancher 等），甚至连 Openshift 也支持：\n只是与 Openshift 的监控还不太兼容。也可以很轻松地查看并编辑 CR：\n有了 Lens，你就可以统一管理所有的集群。\n③ Lens 内置了资源利用率的仪表板，支持多种对接 Prometheus 的方式：\n④ Lens 内置了 kubectl，它的内置终端会确保集群的 API Server 版本与 kubectl 版本兼容，所以你不需要在本地安装 kubectl。可以验证一下：\n你会看到本地安装的 kubectl 版本和 Lens 里面打开的终端里的 kubectl 版本信息是不一样的，Lens 确实内置了 kubectl。\n⑤ Lens 内置了 helm 模板商店，可直接点击安装：\n现在 Lens 迎来了最新版 3.5.0，换上了全新的 Logo：\n稳定性也提升了很多，快去试试吧。\n","date":"2020年6月16日","externalUrl":null,"permalink":"/posts/lens/","section":"博客","summary":"Kubernetes 的桌面客户端有那么几个，曾经 Kubernetic 应该是最好用的，但最近有个叫","title":"Lens —— Kubernetes 桌面客户端","type":"posts"},{"content":"","date":"2020年6月14日","externalUrl":null,"permalink":"/tags/flannel/","section":"标签","summary":"","title":"Flannel","type":"tags"},{"content":"","date":"2020年6月14日","externalUrl":null,"permalink":"/tags/k3s/","section":"标签","summary":"","title":"K3s","type":"tags"},{"content":"最近一两年各大云服务商都出了各种福利活动，很多小伙伴薅了一波又一波羊毛，比如腾讯云 1C2G 95/年 真香系列，华为云和阿里云也都有类似的活动，薅个两三台就能搭建一个 Kubernetes 集群。但是跨云服务商搭建 Kubernetes 集群并不像我们想象中的那么容易，首先就是原生的 Kubernetes 组件本身对资源的消耗量很大，而云服务器的资源非常有限，经不起这么大家伙的折腾，对此我们可以选择使用轻量级 Kubernetes 发行版：k3s。\nk3s 将安装 Kubernetes 所需的一切打包进仅有 60MB 大小的二进制文件中，并且完全实现了 Kubernetes API。为了减少运行 Kubernetes 所需的内存，k3s 删除了很多不必要的驱动程序，并用附加组件对其进行替换。由于它只需要极低的资源就可以运行，因此它能够在任何 512MB 内存以上的设备上运行集群。\n其实 k3s 的安装非常简单，分分钟就能搞定，但对于公有云来说，还是有很多坑的，比如内网不通、公网 IP 不在服务器上该咋办？本文就为你一一解决这些难题，让天下的云羊毛都成为 k3s 的后宫！\n1. 下载二进制文件 # 首先来解决第一个难题：k3s 二进制文件的下载。国内下载 GitHub 速度基本都是以几个 kb 为单位，不忍直视，如果下载内容都是代码，有很多办法可以解决，比如通过码云中转啊、直接通过 CDN 下载啊，什么？你不知道可以通过 CDN 下载？好吧没关系，现在我告诉你了： https://cdn.con.sh/。\n但是上面的 CDN 并不能下载 release 里的内容，要想下载 release 里的内容，可以使用这个网站： https://toolwa.com/github/。打开网站，输入 release 里面的文件下载链接，点击起飞即可加速下载。\n当然，如果你会魔法上网的话，上面的所有花里胡哨的方法都可以无视，直接下载就好啦（本文选择使用版本 v1.17.6+k3s1）：\n$ wget https://github.com/rancher/k3s/releases/download/v1.17.6+k3s1/k3s -O /usr/local/bin/k3s $ chmod +x /usr/local/bin/k3s 需要在所有节点中下载上述二进制文件。\n2. 升级内核 # k3s 的默认网络插件是 flannel，默认模式是 vxlan 模式，建议使用 wireguard 模式，原因不解释了，不知道 wireguard 是啥的自己去搜一下。\nwireguard 对内核的要求比较高，而 CentOS 7.x 的默认内核是不满足要求的，需要升级内核（如果你的操作系统是 CentOS 7.x 的话）。步骤如下：\n① 载入公钥\n$ rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org ② 升级安装 elrepo\n$ rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm ③ 载入 elrepo-kernel 元数据\n$ yum --disablerepo=\\* --enablerepo=elrepo-kernel repolist ④ 安装最新版本的内核\n$ yum --disablerepo=\\* --enablerepo=elrepo-kernel install kernel-ml.x86_64 -y ⑤ 删除旧版本工具包\n$ yum remove kernel-tools-libs.x86_64 kernel-tools.x86_64 -y ⑥ 安装新版本工具包\n$ yum --disablerepo=\\* --enablerepo=elrepo-kernel install kernel-ml-tools kernel-ml-devel kernel-ml-headers -y ⑦ 查看内核插入顺序\n$ grep \u0026#34;^menuentry\u0026#34; /boot/grub2/grub.cfg | cut -d \u0026#34;\u0026#39;\u0026#34; -f2 CentOS Linux (3.10.0-1127.10.1.el7.x86_64) 7 (Core) CentOS Linux (5.7.2-1.el7.elrepo.x86_64) 7 (Core) CentOS Linux (0-rescue-96820b9851c24560b5f942f2496b9aeb) 7 (Core) 默认新内核是从头插入，默认启动顺序也是从 0 开始。\n⑧ 查看当前实际启动顺序\n$ grub2-editenv list saved_entry=CentOS Linux (3.10.0-1127.10.1.el7.x86_64) 7 (Core) ⑨ 设置默认启动\n$ grub2-set-default \u0026#39;CentOS Linux (5.7.2-1.el7.elrepo.x86_64) 7 (Core)\u0026#39; 最后重启检查：\n$ reboot $ uname -r 注意：集群中的所有节点都需要升级内核。\n3. 安装 wireguard # 内核升级了之后，就可以安装 wireguard 了，也很简单，步骤如下：\n$ yum install epel-release https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm $ yum install yum-plugin-elrepo $ yum install kmod-wireguard wireguard-tools 注意：集群中的所有节点都需要安装。\n4. 部署控制平面 # 下面就可以在控制节点上启动控制平面的组件了，这里我们选择手动部署，这样比较方便修改参数。先创建一个 Service Unit 文件：\n$ cat \u0026gt; /etc/systemd/system/k3s.service \u0026lt;\u0026lt;EOF [Unit] Description=Lightweight Kubernetes Documentation=https://k3s.io Wants=network-online.target [Install] WantedBy=multi-user.target [Service] Type=notify EnvironmentFile=/etc/systemd/system/k3s.service.env KillMode=process Delegate=yes # Having non-zero Limit*s causes performance problems due to accounting overhead # in the kernel. We recommend using cgroups to do container-local accounting. LimitNOFILE=1048576 LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity TimeoutStartSec=0 Restart=always RestartSec=5s ExecStartPre=-/sbin/modprobe br_netfilter ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/k3s \\ server \\ --tls-san \u0026lt;public_ip\u0026gt; \\ --node-ip \u0026lt;public_ip\u0026gt; \\ --node-external-ip \u0026lt;public_ip\u0026gt; \\ --no-deploy servicelb \\ --flannel-backend wireguard \\ --kube-proxy-arg \u0026#34;proxy-mode=ipvs\u0026#34; \u0026#34;masquerade-all=true\u0026#34; \\ --kube-proxy-arg \u0026#34;metrics-bind-address=0.0.0.0\u0026#34; EOF 将 \u0026lt;public_ip\u0026gt; 替换成控制节点的公网 IP。 flannel 使用 wireguard 协议来跨主机通信。 kube-proxy 使用 ipvs 模式。 启动 k3s 控制平面并设置开机自启：\n$ systemctl enable k3s --now 查看集群组件健康状况：\n$ kubectl get cs NAME STATUS MESSAGE ERROR scheduler Healthy ok controller-manager Healthy ok 这里的输出没有 etcd，因为 k3s 的默认数据存储是 Sqlite，对于小型数据库十分友好。Kubernetes 控制平面中发生的更改更多是与频繁更新部署、调度 Pod 等有关，因此对于几个节点的小型集群而言，数据库不会造成太大负载，能省下不少资源，真香！\n5. 加入计算节点 # 部署好控制平面之后，就可以加入计算节点了。首先在计算节点上创建 Service Unit 文件：\n$ cat \u0026gt; /etc/systemd/system/k3s-agent.service \u0026lt;\u0026lt;EOF [Unit] Description=Lightweight Kubernetes Documentation=https://k3s.io Wants=network-online.target [Install] WantedBy=multi-user.target [Service] Type=exec EnvironmentFile=/etc/systemd/system/k3s-agent.service.env KillMode=process Delegate=yes LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity TimeoutStartSec=0 Restart=always RestartSec=5s ExecStartPre=-/sbin/modprobe br_netfilter ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/k3s agent \\ --node-external-ip \u0026lt;public_ip\u0026gt; \\ --node-ip \u0026lt;public_ip\u0026gt; \\ --kube-proxy-arg \u0026#34;proxy-mode=ipvs\u0026#34; \u0026#34;masquerade-all=true\u0026#34; \\ --kube-proxy-arg \u0026#34;metrics-bind-address=0.0.0.0\u0026#34; EOF 环境变量文件 /etc/systemd/system/k3s-agent.service.env 中需要加入两个环境变量：\nK3S_URL : API Server 的 URL，一般格式为：https://\u0026lt;master_ip\u0026gt;:6443。其中 \u0026lt;master_ip\u0026gt; 是控制节点的公网 IP。 K3S_TOKEN : 加入集群所需的 token，可以在控制节点上查看 /var/lib/rancher/k3s/server/node-token 文件。 /etc/systemd/system/k3s-agent.service.env 内容如下：\nK3S_URL=https://\u0026lt;master_ip\u0026gt;:6443 K3S_TOKEN=xxxxxxxx 启动 k3s-agent 并设置开启自启：\n$ systemctl enable k3s-agent --now 查看节点状态：\n$ kubectl get node NAME STATUS ROLES AGE VERSION blog-k3s01 Ready master 3d6h v1.17.6+k3s1 blog-k3s02 Ready \u0026lt;none\u0026gt; 3d3h v1.17.6+k3s1 6. 内网不互通的解决办法 # 这里会遇到一个问题，不同节点的 flannel 使用的是内网 IP 来进行通信，而我们的云服务器是内网不互通的，而且公网 IP 也不在服务器上。可以看一下 node 的 annotations：\n$ kubectl get node blog-k3s02 -o yaml apiVersion: v1 kind: Node metadata: annotations: flannel.alpha.coreos.com/backend-data: \u0026#39;\u0026#34;xxxxx\u0026#34;\u0026#39; flannel.alpha.coreos.com/backend-type: extension flannel.alpha.coreos.com/kube-subnet-manager: \u0026#34;true\u0026#34; flannel.alpha.coreos.com/public-ip: 192.168.0.11 ... 可以看到 flannel 给节点打的注解中的节点 IP 是内网 IP。要想让 flannel 使用公网 IP 进行通信，需要额外添加一个注解 public-ip-overwrite，然后 flannel 会基于这个 IP 配置网络。按照官方文档的说法，如果你的 node 设置了 ExternalIP，flannel 会自动给 node 添加一个注解 public-ip-overwrite，但我不知道该如何给 node 设置 ExternalIP，干脆就直接手动加注解吧：\n$ kubectl annotate nodes \u0026lt;master\u0026gt; flannel.alpha.coreos.com/public-ip-overwrite=\u0026lt;master_pub_ip\u0026gt; $ kubectl annotate nodes \u0026lt;node\u0026gt; flannel.alpha.coreos.com/public-ip-overwrite=\u0026lt;node_pub_ip\u0026gt; 加了注解之后，flannel 的 public-ip 就会被修改为公网 IP。然后在各个节点上重启各自的 k3s 服务，查看 wireguard 连接状况：\n$ wg show flannel.1 interface: flannel.1 public key: ONDgJCwxxxxxxxJvdWpoOKTxQA= private key: (hidden) listening port: 51820 peer: MKKaanTxxxxxxxV8VpcHq4CSRISshw= endpoint: \u0026lt;pub_ip\u0026gt;:51820 allowed ips: 10.42.4.0/24 latest handshake: 26 seconds ago transfer: 133.17 KiB received, 387.44 KiB sent persistent keepalive: every 25 seconds 可以看到通信端点被改成了公网 IP，大功告成！\n7. metrics-server 问题解决 # 还有一个问题就是 metrics-server 无法获取 cpu、内存等利用率核心指标。需要修改 metrics-server 的 manifests，使用以下命令在线编辑 metrics-server 的 manifests：\n$ kubectl -n kube-system edit deploy metrics-server 然后加入以下执行参数后保存退出：\n-command: - /metrics-server - --kubelet-preferred-address-types=ExternalIP - --kubelet-insecure-tls 这样就可以让 metrics-server 使用公网 IP 来和 node 通信了。修改成功后就可以看到核心指标了：\n$ kubectl top nodes NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% blog-k3s01 193m 9% 886Mi 22% blog-k3s02 41m 2% 1292Mi 32% $ kubectl top pod -n kube-system NAME CPU(cores) MEMORY(bytes) coredns-848b6cc76f-zq576 8m 14Mi local-path-provisioner-58fb86bdfd-bzdfl 2m 9Mi metrics-server-bdfc79c97-djmzk 1m 12Mi 到这里跨云服务商部署 k3s 基本上就大功告成了，下一篇文章将会教你如何打通家里到云上 k3s 的网络，让你家中所有设备都可以直接访问 Pod IP、svc IP，甚至可以直接访问 svc 域名，敬请期待。\n","date":"2020年6月14日","externalUrl":null,"permalink":"/posts/deploy-k3s-cross-public-cloud/","section":"博客","summary":"最近一两年各大云服务商都出了各种福利活动，很多小伙伴薅了一波","title":"跨云厂商部署 k3s 集群","type":"posts"},{"content":"","date":"2020年6月10日","externalUrl":null,"permalink":"/tags/tcp/","section":"标签","summary":"","title":"TCP","type":"tags"},{"content":" 原文链接： How does a TCP Reset Attack work?\nTCP 重置攻击 是使用一个单一的数据包来执行的，只有几个字节大小。攻击者制作并发送一个伪造的 TCP 重置包来干扰用户和网站的连接，欺骗通信双方终止 TCP 连接。我们伟大的 xx 长城便运用了这个技术来进行 TCP 关键字阻断。\n理解 TCP 重置攻击并不需要具备深厚的网络知识功底，只需要一台笔记本就可以对自己进行模拟攻击。本文将会带你了解 TCP 重置攻击的原理，同时会帮助你理解很多关于 TCP 协议的特性。本文主要内容：\n回顾 TCP 协议的基础知识 了解 TCP 重置攻击的原理 使用一个简单的 Python 脚本来模拟攻击 下面开始分析 TCP 重置攻击原理。\n1. 伟大的 xx 长城是如何利用 TCP 重置攻击的？ # 这一段略过，原因你懂得，感兴趣的请直接看原文。\n2. TCP 重置攻击的工作原理 # 在 TCP 重置攻击中，攻击者通过向通信的一方或双方发送伪造的消息，告诉它们立即断开连接，从而使通信双方连接中断。正常情况下，如果客户端收发现到达的报文段对于相关连接而言是不正确的，TCP 就会发送一个重置报文段，从而导致 TCP 连接的快速拆卸。\nTCP 重置攻击利用这一机制，通过向通信方发送伪造的重置报文段，欺骗通信双方提前关闭 TCP 连接。如果伪造的重置报文段完全逼真，接收者就会认为它有效，并关闭 TCP 连接，防止连接被用来进一步交换信息。服务端可以创建一个新的 TCP 连接来恢复通信，但仍然可能会被攻击者重置连接。万幸的是，攻击者需要一定的时间来组装和发送伪造的报文，所以一般情况下这种攻击只对长连接有杀伤力，对于短连接而言，你还没攻击呢，人家已经完成了信息交换。\n从某种意义上来说，伪造 TCP 报文段是很容易的，因为 TCP/IP 都没有任何内置的方法来验证服务端的身份。有些特殊的 IP 扩展协议（例如 IPSec）确实可以验证身份，但并没有被广泛使用。客户端只能接收报文段，并在可能的情况下使用更高级别的协议（如 TLS）来验证服务端的身份。但这个方法对 TCP 重置包并不适用，因为 TCP 重置包是 TCP 协议本身的一部分，无法使用更高级别的协议进行验证。\n尽管伪造 TCP 报文段很容易，但伪造正确的 TCP 重置报文段并完成攻击却并不容易。为了理解这项工作的难度，我们需要先了解一下 TCP 协议的工作原理。\n3. TCP 协议工作原理 # TCP 协议的目标是向客户端发送一份完整的数据副本。例如，如果我的服务器通过 TCP 连接向你的计算机发送我的网站的 HTML，你的计算机的 TCP 协议栈应该能够以我发送的形式和顺序输出 HTML。\n然而现实生活中我的 HTML 内容并不是按顺序发送的，它被分解成许多小块(称为 TCP 分组)，每个小块在网络上被单独发送，并被重新组合成原来发送的顺序。这种重新组合后的输出被称为 TCP 字节流。\n将分组重建成字节流并不简单，因为网络是不可靠的。TCP分组可能会被丢弃，可能不按发送的顺序到达客户端，也可能会被重复发送、报文损坏等等。因此，TCP 协议的职责是在不可靠的网络上提供可靠的通信。TCP 通过要求连接双方保持密切联系，持续报告它们接收到了哪些数据来实现可靠通信，这样服务端就能够推断出客户端尚未接收到的数据，并重新发送丢失的数据。\n为了进一步理解这个过程，我们需要了解服务端和客户端是如何使用序列号（sequence numbers）来标记和跟踪数据的。\nTCP 序列号 # TCP 协议的通信双方， 都必须维护一个序列号（sequence numbers），对于客户端来说，它会使用服务端的序列号来将接收到的数据按照发送的顺序排列。\n当通信双方建立 TCP 连接时，客户端与服务端都会向对方发送一个随机的初始序列号，这个序列号标识了其发送数据流的第一个字节。TCP 报文段包含了 TCP 头部，它是附加在报文段开头的元数据，序列号就包含在 TCP 头部中。由于 TCP 连接是双向的，双方都可以发送数据，所以 TCP 连接的双方既是发送方也是接收方，每一方都必须分配和管理自己的序列号。\n确认应答 # 当接收方收到一个 TCP 报文段时，它会向发送方返回一个 ACK 应答报文（同时将 TCP 头部的 ACK 标志位置 1），这个 ACK 号就表示接收方期望从发送方收到的下一个字节的序列号。发送方利用这个信息来推断接收方已经成功接收到了序列号为 ACK 之前的所有字节。\nTCP 头部格式如下图所示：\n一个确认应答报文的 TCP 头部必须包含两个部分：\nACK 标志位置位 1 包含确认应答号（ACK number） TCP 总共有 6 个标志位，下文就会讲到其中的 RST 标志位。\nTCP 头部包含了多个选项，其中有一个选择确认选项（SACK），如果使用该选项，那么当接收方收到了某个范围内的字节而不是连续的字节时，就会发送 SACK 告知对方。例如，只收到了字节 1000~3000 和 4000~5000，但没有收到 3001~3999。为了简单起见，下文讨论 TCP 重置攻击时将忽略选择确认选项。 如果发送方发送了报文后在一段时间内没有收到 ACK，就认为报文丢失了，并重新发送报文，用相同的序列号标记。这就意味着，如果接收方收到了重复的报文，可以使用序列号来判断是否见过这个报文，如果见过则直接丢弃。网络环境是错综复杂的，往往并不是如我们期望的一样，先发送的数据包，就先到达目标主机，反而它很骚，可能会由于网络拥堵等乱七八糟的原因，会使得旧的数据包，先到达目标主机。一般分两种情况：\n发送的数据包丢失了 发送的数据包被成功接收，但返回的 ACK 丢失了 这两种情况对发送方来说其实是一样的，发送方并不能区分是哪种情况，所以只能重新发送数据包。\n只要不频繁重复发送数据，额外的开销基本可以忽略。\n为伪造的重置包选择序列号 # 构建伪造的重置包时需要选择一个序列号。接收方可以接收序列号不按顺序排列的报文段，但这种容忍是有限度的，如果报文段的序列号与它期望的相差甚远，就会被直接丢弃。\n因此，一个成功的 TCP 重置攻击需要构建一个可信的序列号。但什么才是可信的序列号呢？对于大多数报文段（除了重置包，即 RST 包）来说，序列号是由接收方的接收窗口大小决定的。\nTCP 滑动窗口大小 # 想象一下，将一台上世纪 90 年代初的古老计算机，连接到现代千兆光纤网络。闪电般快速的网络可以以令人瞠目结舌的速度向这台古老的计算机传送数据，速度远远超过该计算机的处理能力。但并没有什么卵用，因为只有接收方接收并处理了报文，才能认为这个报文已经被收到了。\nTCP 协议栈有一个缓冲区，新到达的数据被放到缓冲区中等待处理。但缓冲区的大小是有限的，如果接收方的处理速度跟不上发送方的发送速度，缓冲区就会被填满。一旦缓冲区被填满，多余的数据就会被直接丢弃，也不会返回 ACK。因此一旦接收方的缓冲区有了空位，发送方必须重新发送数据。也就是说，如果接收方的处理速度跟不上，发送方的发送速度再快也没用。\n缓冲区到底有多大？发送方如何才能知道什么时候可以一次发送更多的数据，什么时候该一次发送很少的数据？这就要靠 TCP 滑动窗口了。接收方的滑动窗口大小是指发送方无需等待确认应答，可以持续发送数据的最大值。 假设接收方的通告窗口大小为 100,000 字节，那么发送方可以无需等待确认应答，持续发送 100,000 个字节。再假设当发送方发送第 100,000 个字节时，接收方已经发送了前 10,000 个字节的 ACK，这就意味着窗口中还有 90,000 个字节未被确认，发送方还可以再持续发送 10,000 个字节。如果发送了 10,000 个字节的过程中没有收到任何的 ACK，那么接收方的滑动窗口将被填满，发送方将停止发送新数据（可以继续发送之前丢失的数据），直到收到相关的 ACK 才可以继续发送。\nTCP 连接双方会在建立连接的初始握手阶段通告对方自己窗口的大小，后续还可以动态调整。TCP 缓冲区大的服务器可能会声明一个大窗口，以便最大限度提高吞吐量。TCP 缓冲区小的服务器可能会被迫声明一个小窗口，这样做会牺牲一定的吞吐量，但为了防止接收方的 TCP 缓冲区溢出，还是很有必要的。\n换个角度来看，TCP 滑动窗口大小是对网络中可能存在的未确认数据量的硬性限制。我们可以用它来计算发送方在某一特定时间内可能发送的最大序列号（max_seq_no）：\nmax_seq_no = max_acked_seq_no + window_size 其中 max_acked_seq_no 是接收方发送的最大 ACK 号，它表示发送方知道接收方已经成功接收的最大序列号。window_size 是窗口大小，它表示允许发送方最多发送的未被确认的字节。所以发送方可以发送的最大序列号是：max_acked_seq_no + window_size。\nTCP 规范规定，接收方应该忽略任何序列号在接收窗口之外的数据。例如，如果接收方确认了所有序列号在 15,000 以下的字节，且接收窗口大小为 30,000，那么接下来接收方只能接收序列号范围在 15,000 ~ 45,000 之间的数据。如果一个报文段的部分数据在窗口内，另一部分数据在窗口外，那么窗口内的数据将被接收确认，窗口外的数据将被丢弃。注意：这里忽略了选择确认选项，再强调一遍！\n对于大多数 TCP 报文段来说，滑动窗口的规则告诉了发送方自己可以接收的序列号范围。但对于重置报文来说，序列号的限制更加严格，这是为了抵御一种攻击叫做盲目 TCP 重置攻击（blind TCP reset attack），下文将会解释。\nTCP 重置报文段的序列号 # 对于 TCP 重置报文段来说，接收方对序列号的要求更加严格，只有当其序列号正好等于下一个预期的序列号时才能接收。继续搬出上面的例子，接收方发送了一个确认应答，ACK 号为 15,000。如果接下来收到了一个重置报文，那么其序列号必须是 15,000 才能被接收。\n如果重置报文的序列号超出了接收窗口范围，接收方就会直接忽略该报文；如果其序列号在接收窗口范围内，那么接收方就会返回一个 challenge ACK，告诉发送方重置报文段的序列号是错误的，并告之正确的序列号，发送方可以利用 challenge ACK 中的信息来重新构建和发送重置报文。\n其实在 2010 年之前，TCP 重置报文段和其他报文段的序列号限制规则一样，但无法抵御盲目 TCP 重置攻击，后来才采取这些措施施加额外的限制。\n盲目 TCP 重置攻击 # 如果攻击者能够截获通信双方正在交换的信息，攻击者就能读取其数据包上的序列号和确认应答号，并利用这些信息得出伪装的 TCP 重置报文段的序列号。相反，如果无法截获通信双方的信息，就无法确定重置报文段的序列号，但仍然可以批量发出尽可能多不同序列号的重置报文，以期望猜对其中一个序列号。这就是所谓的盲目 TCP 重置攻击（blind TCP reset attack）。\n在 2010 年之前 TCP 的原始版本中，攻击者只需要猜对接收窗口内的随便哪一个序列号即可，一般只需发送几万个报文段就能成功。采取额外限制的措施后，攻击者需要发送数以百万计的报文段才有可能猜对序列号，这几乎是很难成功的。更多细节请参考 RFC-5963。\n4. 模拟攻击 # 以下实验是在 OSX 系统中完成的，其他系统请自行测试。 现在来总结一下伪造一个 TCP 重置报文要做哪些事情：\n嗅探通信双方的交换信息。 截获一个 ACK 标志位置位 1 的报文段，并读取其 ACK 号。 伪造一个 TCP 重置报文段（RST 标志位置为 1），其序列号等于上面截获的报文的 ACK 号。这只是理想情况下的方案，假设信息交换的速度不是很快。大多数情况下为了增加成功率，可以连续发送序列号不同的重置报文。 将伪造的重置报文发送给通信的一方或双方，时其中断连接。 为了实验简单，我们可以使用本地计算机通过 localhost 与自己通信，然后对自己进行 TCP 重置攻击。需要以下几个步骤：\n在两个终端之间建立一个 TCP 连接。 编写一个能嗅探通信双方数据的攻击程序。 修改攻击程序，伪造并发送重置报文。 下面正式开始实验。\n建立 TCP 连接 # 可以使用 netcat 工具来建立 TCP 连接，这个工很多操作系统都预装了。打开第一个终端窗口，运行以下命令：\n$ nc -nvl 8000 这个命令会启动一个 TCP 服务，监听端口为 8000。接着再打开第二个终端窗口，运行以下命令：\n$ nc 127.0.0.1 8000 该命令会尝试与上面的服务建立连接，在其中一个窗口输入一些字符，就会通过 TCP 连接发送给另一个窗口并打印出来。\n嗅探流量 # 编写一个攻击程序，使用 Python 网络库 scapy 来读取两个终端窗口之间交换的数据，并将其打印到终端上。完整的代码参考 我的 GitHub 仓库，代码的核心是调用 scapy 的嗅探方法：\nt = sniff( iface=\u0026#39;lo0\u0026#39;, lfilter=is_packet_tcp_client_to_server(localhost_ip, localhost_server_port, localhost_ip), prn=log_packet, count=50) 这段代码告诉 scapy 在 lo0 网络接口上嗅探数据包，并记录所有 TCP 连接的详细信息。\niface : 告诉 scapy 在 lo0（localhost）网络接口上进行监听。 lfilter : 这是个过滤器，告诉 scapy 忽略所有不属于指定的 TCP 连接（通信双方皆为 localhost，且端口号为 8000）的数据包。 prn : scapy 通过这个函数来操作所有符合 lfilter 规则的数据包。上面的例子只是将数据包打印到终端，下文将会修改函数来伪造重置报文。 count : scapy 函数返回之前需要嗅探的数据包数量。 发送伪造的重置报文 # 下面开始修改程序，发送伪造的 TCP 重置报文来进行 TCP 重置攻击。根据上面的解读，只需要修改 prn 函数就行了，让其检查数据包，提取必要参数，并利用这些参数来伪造 TCP 重置报文并发送。\n例如，假设该程序截获了一个从（src_ip, src_port）发往 （dst_ip, dst_port）的报文段，该报文段的 ACK 标志位已置为 1，ACK 号为 100,000。攻击程序接下来要做的是：\n由于伪造的数据包是对截获的数据包的响应，所以伪造数据包的源 IP/Port 应该是截获数据包的目的 IP/Port，反之亦然。 将伪造数据包的 RST 标志位置为 1，以表示这是一个重置报文。 将伪造数据包的序列号设置为截获数据包的 ACK 号，因为这是发送方期望收到的下一个序列号。 调用 scapy 的 send 方法，将伪造的数据包发送给截获数据包的发送方。 对于我的程序而言，只需将 这一行取消注释，并注释这一行的上面一行，就可以全面攻击了。按照步骤 1 的方法设置 TCP 连接，打开第三个窗口运行攻击程序，然后在 TCP 连接的其中一个终端输入一些字符串，你会发现 TCP 连接被中断了！\n进一步实验 # 可以继续使用攻击程序进行实验，将伪造数据包的序列号加减 1 看看会发生什么，是不是确实需要和截获数据包的 ACK 号完全相同。 打开 Wireshark，监听 lo0 网络接口，并使用过滤器 ip.src == 127.0.0.1 \u0026amp;\u0026amp; ip.dst == 127.0.0.1 \u0026amp;\u0026amp; tcp.port == 8000 来过滤无关数据。你可以看到 TCP 连接的所有细节。 在连接上更快速地发送数据流，使攻击更难执行。 总的来说，TCP 重置攻击既深奥又简单，祝你实验顺利。\n","date":"2020年6月10日","externalUrl":null,"permalink":"/posts/how-does-a-tcp-reset-attack-work/","section":"博客","summary":"原文链接： How does a TCP Reset Attack work? TCP 重置攻击 是使用一个单一的数据包来执","title":"TCP 重置攻击的工作原理","type":"posts"},{"content":"","date":"2020年6月8日","externalUrl":null,"permalink":"/tags/vmware/","section":"标签","summary":"","title":"VMware","type":"tags"},{"content":"作为最好的虚拟机软件之一，VMware Workstation 是专为 Linux 和 Windows 系统设计的，为了照顾 Mac 平台的用户，VMware 原班人马又打造了 VMware Fusion，与 Workstation 体验基本一致。\n现在 VMware Fusion 迎来了重大更新，可以直接使用 Docker 镜像启动容器，还可以构建镜像、推送镜像到镜像仓库，不需要安装 Docker Desktop。为了这个功能，VMware Fusion 专门创建了一个新的 CLI 工具：vctl，它包含在 VMware Fusion 中，安装好了之后就有这个命令了。\n通过下面的链接下载安装最新版 VMware Fusion：\nDownload 序列号什么的自己网上找一下就好了，我不方便提供。。\n1. vctl 介绍 # vctl（代码名称：Nautilus 项目）是一个捆绑在 VMware Fusion 应用程序中的命令行实用程序，用于管理容器。大多数 vctl 命令选项可在 Fusion 和 Fusion Pro 中使用。但是，--publish 选项仅适用于 Fusion Pro。\n相关的二进制文件/组件捆绑在 Fusion 应用程序中，可在 Applications/VMware Fusion.app/Contents/Library/vkd/ 文件夹中找到这些内容。主要包括以下三个二进制文件：\nbin/containerd # 这是一个在后台运行的容器运行时守护进程。必须先启动 containerd 守护进程，然后才能运行任何与容器相关的操作。要启动该守护进程，请使用 vctl system start 命令，要停止该守护进程，请使用 vctl system stop 命令。\nbin/containerd-shim-crx-v2 # 启动新容器时，将启动一个新的 containerd-shim-crx-v2 进程，该进程将充当 CRX 虚拟机中的容器与 containerd 守护进程之间的适配器。\nbin/vctl # 这是一个在前台运行的命令行实用程序，它可以将用户输入转发到 containerd 守护进程，和 containerd 进程进行交互，类似于 crictl 的功能。\nvctl 运行的每个容器都跑在一个称作『CRX』虚拟机的轻量级虚拟机内。默认情况下，CRX 虚拟机在容器启动时创建并启动。容器停止时，将关闭并移除该虚拟机。CRX 虚拟机的名称与容器的名称相同。 2. 启动 Containerd # 在使用 vctl 操作容器之前，必须先启动 containerd 容器运行时。容器运行时不会在 VMware Fusion 应用程序启动时自动启动，也不会在 VMware Fusion 应用程序退出时自动停止，必须手动启动和停止。实际上也并不需要打开 VMware Fusion。\n首先在终端中执行以下命令来检查容器运行时的状态：\n$ vctl system info Container runtime is stopped. Use \u0026#39;vctl system start\u0026#39; to start. Container runtime path: /Applications/VMware Fusion.app/Contents/Library/vkd/bin/containerd Log file: not set Log level: info Config: not set Virtual machine CPU (cores): 2 Virtual machine memory (MB): 1024 Host network: DMG file: not set Storage mount point: \u0026lt;HOME\u0026gt;/.vctl/storage 然后启动容器运行时（需要输入管理员密码）：\n$ vctl system start Preparing storage... Container storage has been prepared successfully under \u0026lt;HOME\u0026gt;/.vctl/storage Preparing container network, you may be prompted to input password for administrative operations... Container network has been prepared successfully using vmnet: vmnet9 Launching container runtime... Container runtime has been started. 列出网络设备：\n$ vmrun listHostNetworks Total host networks: 4 INDEX NAME TYPE DHCP SUBNET MASK 0 vmnet0 bridged false empty empty 1 vmnet1 hostOnly true 192.168.22.0 255.255.255.0 8 vmnet8 nat true 192.168.31.0 255.255.255.0 9 vmnet9 nat true 192.168.134.0 255.255.255.0 3. vctl 使用 # 启动容器运行时后，就可以操作容器和镜像了。先拉取一个镜像试试：\n$ vctl pull nginx:alpine INFO Pulling from index.docker.io/library/nginx:alpine ─── ────── ──────── REF STATUS PROGRESS ─── ────── ──────── index-sha256:b89a6ccbda39576ad23fd079978c967cecc6b170db6e7ff8a769bf2259a71912 Done 100% (1645/1645) manifest-sha256:ee5a9b68e8d4a4b8b48318ff08ad5489bd1ce52b357bf48c511968a302bc347b Done 100% (1360/1360) layer-sha256:c4a057508f96954546441044f0d2373303862a4d4accc163e68a4c30d0c88869 Done 100% (668/668) config-sha256:7d0cdcc60a96a5124763fddf5d534d058ad7d0d8d4c3b8be2aefedf4267d0270 Done 100% (8026/8026) layer-sha256:cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08 Done 100% (2813316/2813316) layer-sha256:10c113fb0c778963cb3069e94e8148a3770122f6763c94373e22f5342b503ab0 Done 100% (6460970/6460970) layer-sha256:9ba64393807bf2549af97a1a074ca5fff1bce25ad115b0a7ced446cd1b4305d0 Done 100% (538/538) layer-sha256:262f9908119d4529a370bcdf1f1306131ad556edf400413d5fa74008d7919931 Done 100% (899/899) INFO Unpacking nginx:alpine... INFO done $ vctl images ──── ───────────── ──── NAME CREATION TIME SIZE ──── ───────────── ──── nginx:alpine 2020-06-08T17:05:15+08:00 8.9 MiB 是不是有种熟悉的味道？跑一个容器试试：\n$ vctl run -d --name mynginx nginx:alpine INFO container mynginx started and detached from current session $ vctl ps ──── ───── ─────── ── ───── ────── ───────────── NAME IMAGE COMMAND IP PORTS STATUS CREATION TIME ──── ───── ─────── ── ───── ────── ───────────── mynginx nginx:alpine /docker-entrypoint.s... 192.168.134.129 n/a running 2020-06-08T17:16:11+08:00 可以看到其资源占用非常低：\n这一步神奇的事情就发生了！当容器被启动时，它的 rootfs 会被挂载到宿主机上，这就意味着我们可以直接使用 Finder 来浏览容器里的内容，并实时修改，就像在宿主机里编辑文件一样，简直太爽了！\n查看容器详细信息：\n$ vctl describe mynginx Name: mynginx Status: running Command: /docker-entrypoint.sh nginx -g daemon off; Container rootfs in host: \u0026lt;HOME\u0026gt;/.vctl/storage/containerd/state/io.containerd.runtime.v2.task/vctl/mynginx/rootfs IP address: 192.168.134.129 Creation time: 2020-06-08T17:16:11+08:00 Image name: nginx:alpine Image size: 8.9 MiB Host virtual machine: \u0026lt;HOME\u0026gt;/.vctl/.r/vms/mynginx/mynginx.vmx Container rootfs in VM: /.containers/mynginx Access in host VM: vctl execvm --sh -c mynginx Exec in host VM: vctl execvm -c mynginx /bin/ls 进入容器：\n$ vctl exec -it mynginx sh / # ip a 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc mq state UP qlen 1000 link/ether 00:0c:29:8c:90:ad brd ff:ff:ff:ff:ff:ff inet 192.168.134.129/24 brd 192.168.134.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::20c:29ff:fe8c:90ad/64 scope link valid_lft forever preferred_lft forever 3: dummy0: \u0026lt;BROADCAST,NOARP\u0026gt; mtu 1500 qdisc noop state DOWN qlen 1000 link/ether 12:50:93:b9:b0:b9 brd ff:ff:ff:ff:ff:ff 进入虚拟机：\n$ vctl execvm --sh -c mynginx sh-4.4# uname -a Linux 4.19.84-1.ph3-esx #1-photon SMP Tue Nov 19 00:39:50 UTC 2019 x86_64 sh-4.4# uname -r 4.19.84-1.ph3-esx sh-4.4# ifconfig lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope: Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:6 errors:0 dropped:0 overruns:0 frame:0 TX packets:6 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:751 TX bytes:751 eth0 Link encap:Ethernet HWaddr 00:0c:29:8c:90:ad Driver vmxnet3 inet addr:192.168.134.129 Bcast:192.168.134.255 Mask:255.255.255.0 inet6 addr: fe80::20c:29ff:fe8c:90ad/64 Scope: Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:3 errors:0 dropped:0 overruns:0 frame:0 TX packets:26 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:194 TX bytes:1572 不得不说，VMware 真是越来越会玩了，不断地自我革命，其中 VMware Tanzu 就是为拥抱 Kubernetes 而进行的自我革命，它将 Kubernetes 控制平面直接集成到 ESXi 和 vCenter 中，使其成为 ESXi 的控制平面，并通过 vCenter 提供以应用为中心的管理功能。现在连家用的 Fusion 也推出了容器管理的功能，可以想象，未来软件世界将会被容器所吞噬。\n","date":"2020年6月8日","externalUrl":null,"permalink":"/posts/vmware-fusion-11-5-now-supports-containers/","section":"博客","summary":"作为最好的虚拟机软件之一，VMware Workstation 是专为 Linux 和 Windows 系统设计","title":"VMware Fusion 管理 Docker 容器教程","type":"posts"},{"content":"","date":"2020年6月8日","externalUrl":null,"permalink":"/categories/virtualization/","section":"分类","summary":"","title":"虚拟化","type":"categories"},{"content":"","date":"2020年6月2日","externalUrl":null,"permalink":"/tags/openshift/","section":"标签","summary":"","title":"Openshift","type":"tags"},{"content":" 上篇文章准备了离线安装 OCP 所需要的离线资源，包括安装镜像、所有样例 Image Stream 和 OperatorHub 中的所有 RedHat Operators。本文就开始正式安装 OCP（Openshift Container Platform） 集群，包括 DNS 解析、负载均衡配置、ignition 配置文件生成和集群部署。\nOCP 安装期间需要用到多个文件：安装配置文件、Kubernetes 部署清单、Ignition 配置文件（包含了 machine types）。安装配置文件将被转换为 Kubernetes 部署清单，然后将清单包装到 Ignition 配置文件中。 安装程序使用这些 Ignition 配置文件来创建 Openshift 集群。运行安装程序时，所有原始安装配置文件都会修改，因此在安装之前应该先备份文件。\n1. 安装过程 # 在安装 OCP 时，我们需要有一台引导主机（Bootstrap）。这个主机可以访问所有的 OCP 节点。引导主机启动一个临时控制平面，它启动 OCP 集群的其余部分然后被销毁。引导主机使用 Ignition 配置文件进行集群安装引导，该文件描述了如何创建 OCP 集群。安装程序生成的 Ignition 配置文件包含 24 小时后过期的证书，所以必须在证书过期之前完成集群安装。\n引导集群安装包括如下步骤：\n引导主机启动并开始托管 Master 节点启动所需的资源。 Master 节点从引导主机远程获取资源并完成引导。 Master 节点通过引导主机构建 Etcd 集群。 引导主机使用新的 Etcd 集群启动临时 Kubernetes 控制平面。 临时控制平面在 Master 节点启动生成控制平面。 临时控制平面关闭并将控制权传递给生产控制平面。 引导主机将 OCP 组件注入生成控制平面。 安装程序关闭引导主机。 引导安装过程完成以后，OCP 集群部署完毕。然后集群开始下载并配置日常操作所需的其余组件，包括创建计算节点、通过 Operator 安装其他服务等。\n2. 准备服务器资源 # 服务器规划如下：\n三个控制平面节点，安装 Etcd、控制平面组件和 Infras 基础组件。 两个计算节点，运行实际负载。 一个引导主机，执行安装任务，集群部署完成后可删除。 一个基础节点，用于准备上节提到的离线资源，同时用来部署 DNS 和负载均衡。 一个镜像节点，用来部署私有镜像仓库 Quay。 主机类型 操作系统 Hostname vCPU 内存 存储 IP FQDN 镜像节点 RHEL 7.6 registry 4 8GB 150GB 192.168.57.70 registry.openshift4.example.com 基础节点 RHEL 7.6 bastion 4 16GB 120GB 192.168.57.60 bastion.openshift4.example.com 引导主机 RHCOS bootstrap 4 16GB 120GB 192.168.57.61 bootstrap.openshift4.example.com 控制平面 RHCOS master1 4 16GB 120GB 192.168.57.62 master1.openshift4.example.com 控制平面 RHCOS master2 4 16GB 120GB 192.168.57.63 master2.openshift4.example.com 控制平面 RHCOS master3 4 16GB 120GB 192.168.57.64 master3.openshift4.example.com 计算节点 RHCOS 或 RHEL 7.6 worker1 2 8GB 120GB 192.168.57.65 worker1.openshift4.example.com 计算节点 RHCOS 或 RHEL 7.6 worker2 2 8GB 120GB 192.168.57.66 worke2.openshift4.example.com 3. 防火墙配置 # 接下来看一下每个节点的端口号分配。\n所有节点（计算节点和控制平面）之间需要开放的端口：\n协议 端口 作用 ICMP N/A 测试网络连通性 TCP 9000-9999 节点的服务端口，包括 node exporter 使用的 9100-9101 端口和 Cluster Version Operator 使用的 9099 端口 10250-10259 Kubernetes 预留的默认端口 10256 openshift-sdn UDP 4789 VXLAN 协议或 GENEVE 协议的通信端口 6081 VXLAN 协议或 GENEVE 协议的通信端口 9000-9999 节点的服务端口，包括 node exporter 使用的 9100-9101 端口 30000-32767 Kubernetes NodePort 控制平面需要向其他节点开放的端口：\n协议 端口 作用 TCP 2379-2380 Etcd 服务端口 6443 Kubernetes API 除此之外，还要配置两个四层负载均衡器，一个用来暴露集群 API，一个用来暴露 Ingress：\n端口 作用 内部 外部 描述 6443 引导主机和控制平面使用。在引导主机初始化集群控制平面后，需从负载均衡器中手动删除引导主机 x x Kubernetes API server 22623 引导主机和控制平面使用。在引导主机初始化集群控制平面后，需从负载均衡器中手动删除引导主机 x Machine Config server 443 Ingress Controller 或 Router 使用 x x HTTPS 流量 80 Ingress Controller 或 Router 使用 x x HTTP 流量 4. 配置 DNS # 按照官方文档，使用 UPI 基础架构的 OCP 集群需要以下的 DNS 记录。在每条记录中，\u0026lt;cluster_name\u0026gt; 是集群名称，\u0026lt;base_domain\u0026gt; 是在 install-config.yaml 文件中指定的集群基本域，如下表所示：\n组件 DNS记录 描述 Kubernetes API api.\u0026lt;cluster_name\u0026gt;.\u0026lt;base_domain\u0026gt;. 此 DNS 记录必须指向控制平面节点的负载均衡器。此记录必须可由集群外部的客户端和集群中的所有节点解析。 api-int.\u0026lt;cluster_name\u0026gt;.\u0026lt;base_domain\u0026gt;. 此 DNS 记录必须指向控制平面节点的负载均衡器。此记录必须可由集群外部的客户端和集群中的所有节点解析。 Routes *.apps.\u0026lt;cluster_name\u0026gt;.\u0026lt;base_domain\u0026gt;. DNS 通配符记录，指向负载均衡器。这个负载均衡器的后端是 Ingress router 所在的节点，默认是计算节点。此记录必须可由集群外部的客户端和集群中的所有节点解析。 etcd etcd-\u0026lt;index\u0026gt;.\u0026lt;cluster_name\u0026gt;.\u0026lt;base_domain\u0026gt;. OCP 要求每个 etcd 实例的 DNS 记录指向运行实例的控制平面节点。etcd 实例由 值区分，它们以 0 开头，以 n-1 结束，其中 n 是集群中控制平面节点的数量。集群中的所有节点必须都可以解析此记录。 _etcd-server-ssl._tcp.\u0026lt;cluster_name\u0026gt;.\u0026lt;base_domain\u0026gt;. 因为 etcd 使用端口 2380 对外服务，因此需要建立对应每台 etcd 节点的 SRV DNS 记录，优先级 0，权重 10 和端口 2380 DNS 服务的部署方法由很多种，我当然推荐使用 CoreDNS，毕竟云原生标配。由于这里需要添加 SRV 记录，所以需要 CoreDNS 结合 etcd 插件使用。以下所有操作在基础节点上执行。\n首先通过 yum 安装并启动 etcd：\n$ yum install -y etcd $ systemctl enable etcd --now 然后下载 CoreDNS 二进制文件：\n$ wget https://github.com/coredns/coredns/releases/download/v1.6.9/coredns_1.6.9_linux_amd64.tgz $ tar zxvf coredns_1.6.9_linux_amd64.tgz $ mv coredns /usr/local/bin 创建 Systemd Unit 文件：\n$ cat \u0026gt; /etc/systemd/system/coredns.service \u0026lt;\u0026lt;EOF [Unit] Description=CoreDNS DNS server Documentation=https://coredns.io After=network.target [Service] PermissionsStartOnly=true LimitNOFILE=1048576 LimitNPROC=512 CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE NoNewPrivileges=true User=coredns WorkingDirectory=~ ExecStart=/usr/local/bin/coredns -conf=/etc/coredns/Corefile ExecReload=/bin/kill -SIGUSR1 $MAINPID Restart=on-failure [Install] WantedBy=multi-user.target EOF 新建 coredns 用户：\n$ useradd coredns -s /sbin/nologin 新建 CoreDNS 配置文件：\n$ cat \u0026gt; /etc/coredns/Corefile \u0026lt;\u0026lt;EOF .:53 { # 监听 TCP 和 UDP 的 53 端口 template IN A apps.openshift4.example.com { match .*apps\\.openshift4\\.example\\.com # 匹配请求 DNS 名称的正则表达式 answer \u0026#34;{{ .Name }} 60 IN A 192.168.57.60\u0026#34; # DNS 应答 fallthrough } etcd { # 配置启用 etcd 插件,后面可以指定域名,例如 etcd test.com { path /skydns # etcd 里面的路径 默认为 /skydns，以后所有的 dns 记录都存储在该路径下 endpoint http://localhost:2379 # etcd 访问地址，多个空格分开 fallthrough # 如果区域匹配但不能生成记录，则将请求传递给下一个插件 # tls CERT KEY CACERT # 可选参数，etcd 认证证书设置 } prometheus # 监控插件 cache 160 loadbalance # 负载均衡，开启 DNS 记录轮询策略 forward . 192.168.57.1 log # 打印日志 } EOF 其中 template 插件用来实现泛域名解析。\n启动 CoreDNS 并设置开机自启：\n$ systemctl enable coredns --now 验证泛域名解析：\n$ dig +short apps.openshift4.example.com @127.0.0.1 192.168.57.60 $ dig +short x.apps.openshift4.example.com @127.0.0.1 192.168.57.60 添加其余 DNS 记录：\n$ alias etcdctlv3=\u0026#39;ETCDCTL_API=3 etcdctl\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/api \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.60\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/api-int \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.60\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/etcd-0 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.62\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/etcd-1 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.63\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/etcd-2 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.64\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/_tcp/_etcd-server-ssl/x1 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;etcd-0.openshift4.example.com\u0026#34;,\u0026#34;ttl\u0026#34;:60,\u0026#34;priority\u0026#34;:0,\u0026#34;weight\u0026#34;:10,\u0026#34;port\u0026#34;:2380}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/_tcp/_etcd-server-ssl/x2 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;etcd-1.openshift4.example.com\u0026#34;,\u0026#34;ttl\u0026#34;:60,\u0026#34;priority\u0026#34;:0,\u0026#34;weight\u0026#34;:10,\u0026#34;port\u0026#34;:2380}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/_tcp/_etcd-server-ssl/x3 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;etcd-2.openshift4.example.com\u0026#34;,\u0026#34;ttl\u0026#34;:60,\u0026#34;priority\u0026#34;:0,\u0026#34;weight\u0026#34;:10,\u0026#34;port\u0026#34;:2380}\u0026#39; # 除此之外再添加各节点主机名记录 $ etcdctlv3 put /skydns/com/example/openshift4/bootstrap \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.61\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/master1 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.62\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/master2 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.63\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/master3 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.64\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/worker1 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.65\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/worker2 \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.66\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; $ etcdctlv3 put /skydns/com/example/openshift4/registry \u0026#39;{\u0026#34;host\u0026#34;:\u0026#34;192.168.57.70\u0026#34;,\u0026#34;ttl\u0026#34;:60}\u0026#39; 验证 DNS 解析：\n$ yum install -y bind-utils $ dig +short api.openshift4.example.com @127.0.0.1 192.168.57.60 $ dig +short api-int.openshift4.example.com @127.0.0.1 192.168.57.60 $ dig +short etcd-0.openshift4.example.com @127.0.0.1 192.168.57.62 $ dig +short etcd-1.openshift4.example.com @127.0.0.1 192.168.57.63 $ dig +short etcd-2.openshift4.example.com @127.0.0.1 192.168.57.64 $ dig +short -t SRV _etcd-server-ssl._tcp.openshift4.example.com @127.0.0.1 10 33 2380 etcd-0.openshift4.example.com. 10 33 2380 etcd-1.openshift4.example.com. 10 33 2380 etcd-2.openshift4.example.com. $ dig +short bootstrap.openshift4.example.com @127.0.0.1 192.168.57.61 $ dig +short master1.openshift4.example.com @127.0.0.1 192.168.57.62 $ dig +short master2.openshift4.example.com @127.0.0.1 192.168.57.63 $ dig +short master3.openshift4.example.com @127.0.0.1 192.168.57.64 $ dig +short worker1.openshift4.example.com @127.0.0.1 192.168.57.65 $ dig +short worker2.openshift4.example.com @127.0.0.1 192.168.57.66 5. 配置负载均衡 # 负载均衡我选择使用 Envoy，先准备配置文件：\nBootstrap LDS CDS # /etc/envoy/envoy.yaml node: id: node0 cluster: cluster0 dynamic_resources: lds_config: path: /etc/envoy/lds.yaml cds_config: path: /etc/envoy/cds.yaml admin: access_log_path: \u0026#34;/dev/stdout\u0026#34; address: socket_address: address: \u0026#34;0.0.0.0\u0026#34; port_value: 15001 # /etc/envoy/lds.yaml version_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.listener.v3.Listener name: listener_openshift-api-server address: socket_address: address: 0.0.0.0 port_value: 6443 filter_chains: - filters: - name: envoy.tcp_proxy typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: openshift-api-server cluster: openshift-api-server access_log: name: envoy.access_loggers.file typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.listener.v3.Listener name: listener_machine-config-server address: socket_address: address: \u0026#34;::\u0026#34; ipv4_compat: true port_value: 22623 filter_chains: - filters: - name: envoy.tcp_proxy typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: machine-config-server cluster: machine-config-server access_log: name: envoy.access_loggers.file typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.listener.v3.Listener name: listener_ingress-http address: socket_address: address: \u0026#34;::\u0026#34; ipv4_compat: true port_value: 80 filter_chains: - filters: - name: envoy.tcp_proxy typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: ingress-http cluster: ingress-http access_log: name: envoy.access_loggers.file typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.listener.v3.Listener name: listener_ingress-https address: socket_address: address: \u0026#34;::\u0026#34; ipv4_compat: true port_value: 443 filter_chains: - filters: - name: envoy.tcp_proxy typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: ingress-https cluster: ingress-https access_log: name: envoy.access_loggers.file typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout # /etc/envoy/cds.yaml version_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.cluster.v3.Cluster name: openshift-api-server connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: openshift-api-server endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 192.168.57.61 port_value: 6443 - endpoint: address: socket_address: address: 192.168.57.62 port_value: 6443 - endpoint: address: socket_address: address: 192.168.57.63 port_value: 6443 - endpoint: address: socket_address: address: 192.168.57.64 port_value: 6443 - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.cluster.v3.Cluster name: machine-config-server connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: machine-config-server endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 192.168.57.61 port_value: 22623 - endpoint: address: socket_address: address: 192.168.57.62 port_value: 22623 - endpoint: address: socket_address: address: 192.168.57.63 port_value: 22623 - endpoint: address: socket_address: address: 192.168.57.64 port_value: 22623 - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.cluster.v3.Cluster name: ingress-http connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: ingress-http endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 192.168.57.65 port_value: 80 - endpoint: address: socket_address: address: 192.168.57.66 port_value: 80 - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.cluster.v3.Cluster name: ingress-https connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: ingress-https endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 192.168.57.65 port_value: 443 - endpoint: address: socket_address: address: 192.168.57.66 port_value: 443 配置看不懂的去看我的电子书： Envoy 中文指南\n启动 Envoy：\n$ podman run -d --restart=always --name envoy --net host -v /etc/envoy:/etc/envoy envoyproxy/envoy 6. 安装准备 # 生成 SSH 私钥并将其添加到 agent # 在安装过程中，我们会在基础节点上执行 OCP 安装调试和灾难恢复，因此必须在基础节点上配置 SSH key，ssh-agent 将会用它来执行安装程序。\n基础节点上的 core 用户可以使用该私钥登录到 Master 节点。部署集群时，该私钥会被添加到 core 用户的 ~/.ssh/authorized_keys 列表中。\n密钥创建步骤如下：\n① 创建无密码验证的 SSH key：\n$ ssh-keygen -t rsa -b 4096 -N \u0026#39;\u0026#39; -f ~/.ssh/new_rsa ② 启动 ssh-agent 进程作为后台任务：\n$ eval \u0026#34;$(ssh-agent -s)\u0026#34; ③ 将 SSH 私钥添加到 ssh-agent：\n$ ssh-add ~/.ssh/new_rsa 后续集群安装过程中，有一步会提示输入 SSH public key，届时使用前面创建的公钥 new_rsa.pub 就可以了。\n获取安装程序 # 如果是在线安装，还需要在基础节点上下载安装程序。但这里是离线安装，安装程序在上篇文章中已经被提取出来了，所以不需要再下载。\n创建安装配置文件 # 首先创建一个安装目录，用来存储安装所需要的文件：\n$ mkdir /ocpinstall 自定义 install-config.yaml 并将其保存在 /ocpinstall 目录中。配置文件必须命名为 install-config.yaml。配置文件内容：\napiVersion: v1 baseDomain: example.com compute: - hyperthreading: Enabled name: worker replicas: 0 controlPlane: hyperthreading: Enabled name: master replicas: 3 metadata: name: openshift4 networking: clusterNetwork: - cidr: 10.128.0.0/14 hostPrefix: 23 networkType: OpenShiftSDN serviceNetwork: - 172.30.0.0/16 platform: none: {} fips: false pullSecret: \u0026#39;{\u0026#34;auths\u0026#34;: ...}\u0026#39; sshKey: \u0026#39;ssh-rsa ...\u0026#39; additionalTrustBundle: | -----BEGIN CERTIFICATE----- 省略，注意这里要前面空两格 -----END CERTIFICATE----- imageContentSources: - mirrors: - registry.openshift4.example.com/ocp4/openshift4 source: quay.io/openshift-release-dev/ocp-release - mirrors: - registry.openshift4.example.com/ocp4/openshift4 source: quay.io/openshift-release-dev/ocp-v4.0-art-dev baseDomain : 所有 Openshift 内部的 DNS 记录必须是此基础的子域，并包含集群名称。 compute : 计算节点配置。这是一个数组，每一个元素必须以连字符 - 开头。 hyperthreading : Enabled 表示启用同步多线程或超线程。默认启用同步多线程，可以提高机器内核的性能。如果要禁用，则控制平面和计算节点都要禁用。 compute.replicas : 计算节点数量。因为我们要手动创建计算节点，所以这里要设置为 0。 controlPlane.replicas : 控制平面节点数量。控制平面节点数量必须和 etcd 节点数量一致，为了实现高可用，本文设置为 3。 metadata.name : 集群名称。即前面 DNS 记录中的 \u0026lt;cluster_name\u0026gt;。 cidr : 定义了分配 Pod IP 的 IP 地址段，不能和物理网络重叠。 hostPrefix : 分配给每个节点的子网前缀长度。例如，如果将 hostPrefix 设置为 23，则为每一个节点分配一个给定 cidr 的 /23 子网，允许 $510 (2^{32 - 23} - 2)$ 个 Pod IP 地址。 serviceNetwork : Service IP 的地址池，只能设置一个。 pullSecret : 上篇文章使用的 pull secret，可通过命令 cat /root/pull-secret.json|jq -c 来压缩成一行。 sshKey : 上面创建的公钥，可通过命令 cat ~/.ssh/new_rsa.pub 查看。 additionalTrustBundle : 私有镜像仓库 Quay 的信任证书，可在镜像节点上通过命令 cat /data/quay/config/ssl.cert 查看。 imageContentSources : 来自前面 oc adm release mirror 的输出结果。 备份安装配置文件，便于以后重复使用：\n$ cd /ocpinstall $ cp install-config.yaml install-config.yaml.20200604 创建 Kubernetes 部署清单 # 创建 Kubernetes 部署清单后 install-config.yaml 将被删除，请务必先备份此文件！\n创建 Kubernetes 部署清单文件：\n$ openshift-install create manifests --dir=/ocpinstall 修改 manifests/cluster-scheduler-02-config.yml 文件，将 mastersSchedulable 的值设为 flase，以防止 Pod 调度到控制节点。\n创建 Ignition 配置文件 # 创建 Ignition 配置文件后 install-config.yaml 将被删除，请务必先备份此文件！\n$ cp install-config.yaml.20200604 install-config.yaml $ openshift-install create ignition-configs --dir=/ocpinstall 生成的文件：\n├── auth │ ├── kubeadmin-password │ └── kubeconfig ├── bootstrap.ign ├── master.ign ├── metadata.json └── worker.ign 准备一个 HTTP 服务，这里选择使用 Nginx：\n$ yum install -y nginx 修改 Nginx 的配置文件 /etc/nginx/nginx/.conf，将端口改为 8080（因为负载均衡器已经占用了 80 端口）。然后启动 Nginx 服务：\n$ systemctl enable nginx --now 将 Ignition 配置文件拷贝到 HTTP 服务的 ignition 目录：\n$ mkdir /usr/share/nginx/html/ignition $ cp -r *.ign /usr/share/nginx/html/ignition/ 获取 RHCOS 的 BIOS 文件 # 下载用于裸机安装的 BIOS 文件，并上传到 Nginx 的目录：\n$ mkdir /usr/share/nginx/html/install $ wget https://mirror.openshift.com/pub/openshift-v4/dependencies/rhcos/4.4/latest/rhcos-4.4.3-x86_64-metal.x86_64.raw.gz -O /usr/share/nginx/html/install/rhcos-4.4.3-x86_64-metal.x86_64.raw.gz 获取 RHCOS 的 ISO 文件 # 本地下载 RHCOS 的 ISO 文件： https://mirror.openshift.com/pub/openshift-v4/dependencies/rhcos/4.4/latest/rhcos-4.4.3-x86_64-installer.x86_64.iso，然后上传到 vSphere。步骤如下：\n① 首先登陆 vSphere，然后点击『存储』。\n② 选择一个『数据存储』，然后在右边的窗口中选择『上载文件』。\n③ 选择刚刚下载的 ISO 文件，上传到 ESXI 主机。\n7. 安装集群 # Bootstrap # 最后开始正式安装集群，先创建 bootstrap 节点虚拟机，操作系统选择『Red Hat Enterprise Linux 7 (64-Bit)』，并挂载之前上传的 ISO，按照之前的表格设置 CPU 、内存和硬盘，打开电源，然后按照下面的步骤操作：\n① 在 RHCOS Installer 安装界面按 Tab 键进入引导参数配置选项。\n② 在默认选项 coreos.inst = yes 之后添加（由于无法拷贝粘贴，请输入仔细核对后再回车进行）：\nip=192.168.57.61::192.168.57.1:255.255.255.0:bootstrap.openshift4.example.com:ens192:none nameserver=192.168.57.60 coreos.inst.install_dev=sda coreos.inst.image_url=http://192.168.57.60:8080/install/rhcos-4.4.3-x86_64-metal.x86_64.raw.gz coreos.inst.ignition_url=http://192.168.57.60:8080/ignition/bootstrap.ign 其中 ip=... 的含义为 ip=$IPADDRESS::$DEFAULTGW:$NETMASK:$HOSTNAMEFQDN:$IFACE:none。\n如图所示：\n③ 如果安装有问题会进入 emergency shell，检查网络、域名解析是否正常，如果正常一般是以上参数输入有误，reboot 退出 shell 回到第一步重新开始。\n安装成功后从基础节点通过命令 ssh -i ~/.ssh/new_rsa core@192.168.57.61 登录 bootstrap 节点，然后验证：\n网络配置是否符合自己的设定： hostname ip route cat /etc/resolv.conf 验证是否成功启动 bootstrap 相应服务： podman ps 查看服务是否以容器方式运行 使用 ss -tulnp 查看 6443 和 22623 端口是否启用。 这里简单介绍一下 bootstrap 节点的启动流程，它会先通过 podman 跑一些容器，然后在容器里面启动临时控制平面，这个临时控制平面是通过 CRIO 跑在容器里的，有点绕。。直接看命令：\n$ podman ps -a --no-trunc --sort created --format \u0026#34;{{.Command}}\u0026#34; start --tear-down-early=false --asset-dir=/assets --required-pods=openshift-kube-apiserver/kube-apiserver,openshift-kube-scheduler/openshift-kube-scheduler,openshift-kube-controller-manager/kube-controller-manager,openshift-cluster-version/cluster-version-operator /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml render --dest-dir=/assets/cco-bootstrap --cloud-credential-operator-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:244ab9d0fcf7315eb5c399bd3fa7c2e662cf23f87f625757b13f415d484621c3 bootstrap --etcd-ca=/assets/tls/etcd-ca-bundle.crt --etcd-metric-ca=/assets/tls/etcd-metric-ca-bundle.crt --root-ca=/assets/tls/root-ca.crt --kube-ca=/assets/tls/kube-apiserver-complete-client-ca-bundle.crt --config-file=/assets/manifests/cluster-config.yaml --dest-dir=/assets/mco-bootstrap --pull-secret=/assets/manifests/openshift-config-secret-pull-secret.yaml --etcd-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:aba3c59eb6d088d61b268f83b034230b3396ce67da4f6f6d49201e55efebc6b2 --kube-client-agent-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:8eb481214103d8e0b5fe982ffd682f838b969c8ff7d4f3ed4f83d4a444fb841b --machine-config-operator-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:31dfdca3584982ed5a82d3017322b7d65a491ab25080c427f3f07d9ce93c52e2 --machine-config-oscontent-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:b397960b7cc14c2e2603111b7385c6e8e4b0f683f9873cd9252a789175e5c4e1 --infra-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:d7862a735f492a18cb127742b5c2252281aa8f3bd92189176dd46ae9620ee68a --keepalived-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:a882a11b55b2fc41b538b59bf5db8e4cfc47c537890e4906fe6bf22f9da75575 --coredns-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:b25b8b2219e8c247c088af93e833c9ac390bc63459955e131d89b77c485d144d --mdns-publisher-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:dea1fcb456eae4aabdf5d2d5c537a968a2dafc3da52fe20e8d99a176fccaabce --haproxy-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:7064737dd9d0a43de7a87a094487ab4d7b9e666675c53cf4806d1c9279bd6c2e --baremetal-runtimecfg-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:715bc48eda04afc06827189883451958d8940ed8ab6dd491f602611fe98a6fba --cloud-config-file=/assets/manifests/cloud-provider-config.yaml --cluster-etcd-operator-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77 render --prefix=cluster-ingress- --output-dir=/assets/ingress-operator-manifests /usr/bin/cluster-kube-scheduler-operator render --manifest-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:187b9d29fea1bde9f1785584b4a7bbf9a0b9f93e1323d92d138e61c861b6286c --asset-input-dir=/assets/tls --asset-output-dir=/assets/kube-scheduler-bootstrap --config-output-file=/assets/kube-scheduler-bootstrap/config /usr/bin/cluster-kube-controller-manager-operator render --manifest-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:187b9d29fea1bde9f1785584b4a7bbf9a0b9f93e1323d92d138e61c861b6286c --asset-input-dir=/assets/tls --asset-output-dir=/assets/kube-controller-manager-bootstrap --config-output-file=/assets/kube-controller-manager-bootstrap/config --cluster-config-file=/assets/manifests/cluster-network-02-config.yml /usr/bin/cluster-kube-apiserver-operator render --manifest-etcd-serving-ca=etcd-ca-bundle.crt --manifest-etcd-server-urls=https://localhost:2379 --manifest-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:187b9d29fea1bde9f1785584b4a7bbf9a0b9f93e1323d92d138e61c861b6286c --manifest-operator-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:718ca346d5499cccb4de98c1f858c9a9a13bbf429624226f466c3ee2c14ebf40 --asset-input-dir=/assets/tls --asset-output-dir=/assets/kube-apiserver-bootstrap --config-output-file=/assets/kube-apiserver-bootstrap/config --cluster-config-file=/assets/manifests/cluster-network-02-config.yml /usr/bin/cluster-config-operator render --config-output-file=/assets/config-bootstrap/config --asset-input-dir=/assets/tls --asset-output-dir=/assets/config-bootstrap /usr/bin/cluster-etcd-operator render --etcd-ca=/assets/tls/etcd-ca-bundle.crt --etcd-metric-ca=/assets/tls/etcd-metric-ca-bundle.crt --manifest-etcd-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:aba3c59eb6d088d61b268f83b034230b3396ce67da4f6f6d49201e55efebc6b2 --etcd-discovery-domain=test.example.com --manifest-cluster-etcd-operator-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77 --manifest-setup-etcd-env-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:31dfdca3584982ed5a82d3017322b7d65a491ab25080c427f3f07d9ce93c52e2 --manifest-kube-client-agent-image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:8eb481214103d8e0b5fe982ffd682f838b969c8ff7d4f3ed4f83d4a444fb841b --asset-input-dir=/assets/tls --asset-output-dir=/assets/etcd-bootstrap --config-output-file=/assets/etcd-bootstrap/config --cluster-config-file=/assets/manifests/cluster-network-02-config.yml render --output-dir=/assets/cvo-bootstrap --release-image=registry.openshift4.example.com/ocp4/openshift4@sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74 /usr/bin/grep -oP Managed /manifests/0000_12_etcd-operator_01_operator.cr.yaml $ crictl pods POD ID CREATED STATE NAME NAMESPACE ATTEMPT 17a978b9e7b1e 3 minutes ago Ready bootstrap-kube-apiserver-bootstrap.openshift4.example.com kube-system 24 8a0f79f38787a 3 minutes ago Ready bootstrap-kube-scheduler-bootstrap.openshift4.example.com kube-system 4 1a707da797173 3 minutes ago Ready bootstrap-kube-controller-manager-bootstrap.openshift4.example.com kube-system 4 0461d2caa2753 3 minutes ago Ready cloud-credential-operator-bootstrap.openshift4.example.com openshift-cloud-credential-operator 4 ab6519286f65a 3 minutes ago Ready bootstrap-cluster-version-operator-bootstrap.openshift4.example.com openshift-cluster-version 2 457a7a46ec486 8 hours ago Ready bootstrap-machine-config-operator-bootstrap.openshift4.example.com default 0 e4df49b4d36a1 8 hours ago Ready etcd-bootstrap-member-bootstrap.openshift4.example.com openshift-etcd 0 如果验证无问题，则可以一边继续下面的步骤一边观察日志：journalctl -b -f -u bootkube.service\nRHCOS 的默认用户是 core，如果想获取 root 权限，可以执行命令 sudo su（不需要输入密码）。 Master # 控制节点和之前类似，先创建虚拟机，然后修改引导参数，引导参数调整为：\nip=192.168.57.62::192.168.57.1:255.255.255.0:master1.openshift4.example.com:ens192:none nameserver=192.168.57.60 coreos.inst.install_dev=sda coreos.inst.image_url=http://192.168.57.60:8080/install/rhcos-4.4.3-x86_64-metal.x86_64.raw.gz coreos.inst.ignition_url=http://192.168.57.60:8080/ignition/master.ign 控制节点安装成功后会重启一次，之后同样可以从基础节点通过 SSH 密钥登录。\n然后重复相同的步骤创建其他两台控制节点，注意修改引导参数（IP 和主机名）。先不急着创建计算节点，先在基础节点执行以下命令完成生产控制平面的创建：\n$ openshift-install --dir=/ocpinstall wait-for bootstrap-complete --log-level=debug DEBUG OpenShift Installer 4.4.5 DEBUG Built from commit 15eac3785998a5bc250c9f72101a4a9cb767e494 INFO Waiting up to 20m0s for the Kubernetes API at https://api.openshift4.example.com:6443... INFO API v1.17.1 up INFO Waiting up to 40m0s for bootstrapping to complete... DEBUG Bootstrap status: complete INFO It is now safe to remove the bootstrap resources 待出现 It is now safe to remove the bootstrap resources 提示之后，从负载均衡器中删除引导主机，本文使用的是 Envoy，只需从 cds.yaml 中删除引导主机的 endpoint，然后重新加载就好了。\n观察引导节点的日志：\n$ journalctl -b -f -u bootkube.service ... Jun 05 00:24:12 bootstrap.openshift4.example.com bootkube.sh[12571]: I0605 00:24:12.108179 1 waitforceo.go:67] waiting on condition EtcdRunningInCluster in etcd CR /cluster to be True. Jun 05 00:24:21 bootstrap.openshift4.example.com bootkube.sh[12571]: I0605 00:24:21.595680 1 waitforceo.go:67] waiting on condition EtcdRunningInCluster in etcd CR /cluster to be True. Jun 05 00:24:26 bootstrap.openshift4.example.com bootkube.sh[12571]: I0605 00:24:26.250214 1 waitforceo.go:67] waiting on condition EtcdRunningInCluster in etcd CR /cluster to be True. Jun 05 00:24:26 bootstrap.openshift4.example.com bootkube.sh[12571]: I0605 00:24:26.306421 1 waitforceo.go:67] waiting on condition EtcdRunningInCluster in etcd CR /cluster to be True. Jun 05 00:24:29 bootstrap.openshift4.example.com bootkube.sh[12571]: I0605 00:24:29.097072 1 waitforceo.go:64] Cluster etcd operator bootstrapped successfully Jun 05 00:24:29 bootstrap.openshift4.example.com bootkube.sh[12571]: I0605 00:24:29.097306 1 waitforceo.go:58] cluster-etcd-operator bootstrap etcd Jun 05 00:24:29 bootstrap.openshift4.example.com podman[16531]: 2020-06-05 00:24:29.120864426 +0000 UTC m=+17.965364064 container died 77971b6ca31755a89b279fab6f9c04828c4614161c2e678c7cba48348e684517 (image=quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77, name=recursing_cerf) Jun 05 00:24:29 bootstrap.openshift4.example.com bootkube.sh[12571]: bootkube.service complete Worker # 计算节点和之前类似，先创建虚拟机，然后修改引导参数，引导参数调整为：\nip=192.168.57.65::192.168.57.1:255.255.255.0:worker1.openshift4.example.com:ens192:none nameserver=192.168.57.60 coreos.inst.install_dev=sda coreos.inst.image_url=http://192.168.57.60:8080/install/rhcos-4.4.3-x86_64-metal.x86_64.raw.gz coreos.inst.ignition_url=http://192.168.57.60:8080/ignition/worker.ign 计算节点安装成功后也会重启一次，之后同样可以从基础节点通过 SSH 密钥登录。\n然后重复相同的步骤创建其他计算节点，注意修改引导参数（IP 和主机名）。\n登录集群 # 可以通过导出集群 kubeconfig 文件以默认系统用户身份登录到集群。kubeconfig 文件包含有关 CLI 用于将客户端连接到正确的集群和 API Server 的集群信息，该文件在 OCP 安装期间被创建。\n$ mkdir ~/.kube $ cp /ocpinstall/auth/kubeconfig ~/.kube/config $ oc whoami system:admin 批准 CSR # 将节点添加到集群时，会为添加的每台节点生成两个待处理证书签名请求（CSR）。必须确认这些 CSR 已获得批准，或者在必要时自行批准。\n$ oc get node NAME STATUS ROLES AGE VERSION master1.openshift4.example.com Ready master,worker 6h25m v1.17.1 master2.openshift4.example.com Ready master,worker 6h39m v1.17.1 master3.openshift4.example.com Ready master,worker 6h15m v1.17.1 worker1.openshift4.example.com NotReady worker 5h8m v1.17.1 worker2.openshift4.example.com NotReady worker 5h9m v1.17.1 输出列出了创建的所有节点。查看挂起的证书签名请求（CSR），并确保添加到集群的每台节点都能看到具有 Pending 或 Approved 状态的客户端和服务端请求。针对 Pending 状态的 CSR 批准请求：\n$ oc adm certificate approve xxx 或者执行以下命令批准所有 CSR：\n$ oc get csr -ojson | jq -r \u0026#39;.items[] | select(.status == {} ) | .metadata.name\u0026#39; | xargs oc adm certificate approve Operator 自动初始化 # 控制平面初始化后，需要确认所有的 Operator 都处于可用的状态，即确认所有 Operator 的 Available 字段值皆为 True：\n$ oc get clusteroperators NAME VERSION AVAILABLE PROGRESSING DEGRADED SINCE authentication 4.4.5 True False False 150m cloud-credential 4.4.5 True False False 7h7m cluster-autoscaler 4.4.5 True False False 6h12m console 4.4.5 True False False 150m csi-snapshot-controller 4.4.5 True False False 6h13m dns 4.4.5 True False False 6h37m etcd 4.4.5 True False False 6h19m image-registry 4.4.5 True False False 6h12m ingress 4.4.5 True False False 150m insights 4.4.5 True False False 6h13m kube-apiserver 4.4.5 True False False 6h15m kube-controller-manager 4.4.5 True False False 6h36m kube-scheduler 4.4.5 True False False 6h36m kube-storage-version-migrator 4.4.5 True False False 6h36m machine-api 4.4.5 True False False 6h37m machine-config 4.4.5 True False False 6h36m marketplace 4.4.5 True False False 6h12m monitoring 4.4.5 True False False 6h6m network 4.4.5 True False False 6h39m node-tuning 4.4.5 True False False 6h38m openshift-apiserver 4.4.5 True False False 6h14m openshift-controller-manager 4.4.5 True False False 6h12m openshift-samples 4.4.5 True False False 6h11m operator-lifecycle-manager 4.4.5 True False False 6h37m operator-lifecycle-manager-catalog 4.4.5 True False False 6h37m operator-lifecycle-manager-packageserver 4.4.5 True False False 6h15m service-ca 4.4.5 True False False 6h38m service-catalog-apiserver 4.4.5 True False False 6h38m service-catalog-controller-manager 4.4.5 True False False 6h39m storage 4.4.5 True False False 6h12m 如果 Operator 不正常，需要进行问题诊断和修复。\n完成安装 # 最后一步，完成集群的安装，执行以下命令：\n$ openshift-install --dir=/ocpinstall wait-for install-complete --log-level=debug 注意最后提示访问 Web Console 的网址及用户密码。如果密码忘了也没关系，可以查看文件 /ocpinstall/auth/kubeadmin-password 来获得密码。\n本地访问 Web Console，需要添加 hosts：\n192.168.57.60 console-openshift-console.apps.openshift4.example.com 192.168.57.60 oauth-openshift.apps.openshift4.example.com 浏览器访问 https://console-openshift-console.apps.openshift4.example.com，输入上面输出的用户名密码登录。首次登录后会提示：\nYou are logged in as a temporary administrative user. Update the Cluster OAuth configuration to allow others to log in. 我们可以通过 htpasswd 自定义管理员账号，步骤如下：\n① htpasswd -c -B -b users.htpasswd admin xxxxx\n② 将 users.htpasswd 文件下载到本地。\n③ 在 Web Console 页面打开 Global Configuration：\n然后找到 OAuth，点击进入，然后添加 HTPasswd 类型的 Identity Providers，并上传 users.htpasswd 文件。\n④ 退出当前用户，注意要退出到如下界面：\n选择 htpasswd，然后输入之前创建的用户名密码登录。\n如果退出后出现的就是用户密码输入窗口，实际还是 kube:admin 的校验，如果未出现如上提示，可以手动输入 Web Console 地址来自动跳转。\n⑤ 登录后貌似能看到 Administrator 菜单项，但访问如 OAuth Details 仍然提示：\noauths.config.openshift.io \u0026#34;cluster\u0026#34; is forbidden: User \u0026#34;admin\u0026#34; cannot get resource \u0026#34;oauths\u0026#34; in API group \u0026#34;config.openshift.io\u0026#34; at the cluster scope 因此需要授予集群管理员权限：\n$ oc adm policy add-cluster-role-to-user cluster-admin admin Web Console 部分截图：\n如果想删除默认账号，可以执行以下命令：\n$ oc -n kube-system delete secrets kubeadmin 8. 参考资料 # OpenShift 4.2 vSphere Install with Static IPs OpenShift Container Platform 4.3部署实录 Chapter 1. Installing on bare metal ","date":"2020年6月2日","externalUrl":null,"permalink":"/posts/openshift4.4-install-offline-static-2-first-setup/","section":"博客","summary":"上篇文章准备了离线安装 OCP 所需要的离线资源，包括安装镜像、所有","title":"Openshift 4.4 静态 IP 离线安装系列：初始安装","type":"posts"},{"content":"本系列文章描述了离线环境下以 UPI (User Provisioned Infrastructure) 模式安装 Openshift Container Platform (OCP) 4.4.5 的步骤，我的环境是 VMware ESXI 虚拟化，也适用于其他方式提供的虚拟机或物理主机。离线资源包括安装镜像、所有样例 Image Stream 和 OperatorHub 中的所有 RedHat Operators。\n本系列采用静态 IP 的方式安装 OCP 集群，如果你可以随意分配网络，建议采用 DHCP 的方式。\n1. 离线环境 # 单独准备一台节点用来执行安装任务和离线资源准备，这台节点最好具备魔法上网的能力，以便可以同时访问内外网，我们称这台节点为基础节点。\n除此之外还需要部署一个私有镜像仓库，以供 OCP 安装和运行时使用，要求支持 version 2 schema 2 (manifest list)，我这里选择的是 Quay 3.3。镜像仓库需要部署在另外一台节点，因为需要用到 443 端口，与后面的负载均衡端口冲突。\n很多人误以为必须联系 Red Hat 销售，签单之后才能使用 OCP4，其实不然，注册一个 开发者账号后就可以获得 quay.io 和 registry.redhat.io 的拉取密钥了。 2. 准备离线安装介质 # 获取版本信息 # 目前最新的 OCP 版本是 4.4.5，可以从这里下载客户端：\nhttps://mirror.openshift.com/pub/openshift-v4/clients/ocp/latest-4.4/ 解压出来的二进制文件放到基础节点的 $PATH 下，看下版本信息：\nOCP 4.4.5 版本信息 🐳 → oc adm release info quay.io/openshift-release-dev/ocp-release:4.4.5-x86_64 Name: 4.4.5 Digest: sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74 Created: 2020-05-21T16:03:01Z OS/Arch: linux/amd64 Manifests: 412 Pull From: quay.io/openshift-release-dev/ocp-release@sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74 Release Metadata: Version: 4.4.5 Upgrades: 4.3.18, 4.3.19, 4.3.21, 4.3.22, 4.4.2, 4.4.3, 4.4.4 Metadata: description: Metadata: url: https://access.redhat.com/errata/RHBA-2020:2180 Component Versions: kubernetes 1.17.1 machine-os 44.81.202005180831-0 Red Hat Enterprise Linux CoreOS Images: NAME DIGEST aws-machine-controllers sha256:7817d9e707bb51bc1e5110ef66bb67947df42dcf3c9b782a8f12f60b8f229dca azure-machine-controllers sha256:5e2320f92b7308a4f1ec4aca151c752f69265e8c5b705d78e2f2ee70d717711a baremetal-installer sha256:4c8c6d2895e065711cfcbffe7e8679d9890480a4975cad683b643d8502375fe3 baremetal-machine-controllers sha256:5f1b312ac47b7f9e91950463e9a4ce5af7094a3a8b0bc064c9b4dcfc9c725ad5 baremetal-operator sha256:a77ff02f349d96567da8e06018ad0dfbfb5fef6600a9a216ade15fadc574f4b4 baremetal-runtimecfg sha256:715bc48eda04afc06827189883451958d8940ed8ab6dd491f602611fe98a6fba cli sha256:43159f5486cc113d64d5ba04d781c16a084d18745a911a5ae7200bb895778a72 cli-artifacts sha256:ce7130db82f5a3bb2c806d7080f356e4c68c0405bf3956d3e290bc2078a8bf32 cloud-credential-operator sha256:244ab9d0fcf7315eb5c399bd3fa7c2e662cf23f87f625757b13f415d484621c3 cluster-authentication-operator sha256:3145e4fbd62dde385fd0e33d220c42ec3d00ac1dab72288e584cc502b4b8b6db cluster-autoscaler sha256:66e47de69f685f2dd063fbce9f4e5a00264a5572140d255f2db4c367cb00bad9 cluster-autoscaler-operator sha256:6a32eafdbea3d12c0681a1a1660c7a424f7082a1c42e22d1b301ab0ab6da191b cluster-bootstrap sha256:fbde2b1a3df7172ce5dbc5e8818bfe631718399eda8058b301a1ef059f549e95 cluster-config-operator sha256:5437794d2309ebe65ca08d1bdeb9fcd665732207b3287df8a7c56e5a2813eccb cluster-csi-snapshot-controller-operator sha256:bc4d8ad97b473316518dbd8906dd900feba383425671eb7d4d73ed1d705c105e cluster-dns-operator sha256:1a7469258e351d2d56a98a5ef4a3dfa0326b4677fdc1dd11279b6a193ccdbad1 cluster-etcd-operator sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77 cluster-image-registry-operator sha256:0aaa817389487d266faf89cecbfd3197405d87172ee2dcda169dfa90e2e9ca18 cluster-ingress-operator sha256:4887544363e052e656aa1fd44d2844226ee2e4617e08b88ba0211a93bb3101fa cluster-kube-apiserver-operator sha256:718ca346d5499cccb4de98c1f858c9a9a13bbf429624226f466c3ee2c14ebf40 cluster-kube-controller-manager-operator sha256:0aa16b4ff32fbb9bc7b32aa1bf6441a19a1deb775fb203f21bb8792ff1a26c2e cluster-kube-scheduler-operator sha256:887eda5ce495f1a33c5adbba8772064d3a8b78192162e4c75bd84763c5a1fb01 cluster-kube-storage-version-migrator-operator sha256:0fd3e25304a6e23e9699172a84dc134b9b5b81dd89496322a9f46f4cd82ecf71 cluster-machine-approver sha256:c35b382d426ff03cfe07719f19e871ec3bd4189fa27452b3e2eb2fb4ab085afc cluster-monitoring-operator sha256:d7d5f3b6094c88cb1aa9d5bf1b29c574f13db7142e0a9fba03c6681fe4b592a5 cluster-network-operator sha256:563018341e5b37e5cf370ee0a112aa85dd5e17a658b303714252cc59ddfadea5 cluster-node-tuned sha256:0d1a3f66cd7cfc889ddf17cbdb4cb2e4b9188c341b165de1c9c1df578fb53212 cluster-node-tuning-operator sha256:8e00331fd6b725b1d44687bafa2186920e2864fd4d04869ad4e9f5ba56d663ca cluster-openshift-apiserver-operator sha256:087dd3801b15ca614be0998615a0d827383e9c9ab39e64107324074bddccfff8 cluster-openshift-controller-manager-operator sha256:a25afbcb148f3535372784e82c66a6cc2843fe9e7119b9198a39422edb95c2ae cluster-policy-controller sha256:6294d4af2061d23f52a2a439d20272280aa6e5fcff7a5559b4797fb8e6536790 cluster-samples-operator sha256:7040633af70ceb19147687d948a389d392945cb57236165409e66e5101c0d0c0 cluster-storage-operator sha256:bcfeab624513563c9e26629be2914770436c49318c321bd99028a7d1ffab30cf cluster-svcat-apiserver-operator sha256:21a562f26c967ad6d83e1f4219fad858154c3df9854f1462331b244906c6ca9c cluster-svcat-controller-manager-operator sha256:b635529e5843996a51ace6a2aea4854e46256669ef1773c7371e4f0407dbf843 cluster-update-keys sha256:828e11d8132caf5533e18b8e5d292d56ccf52b08e4fe4c53d7825404b05b2844 cluster-version-operator sha256:7a2a210bc07fead80b3f4276cf14692c39a70640a124326ee919d415f0dc5b2c configmap-reloader sha256:07d46699cb9810e3f629b5142a571db83106aa1190d5177a9944272080cd053d console sha256:69f14151fe8681e5fa48912f8f4df753a0dcc3d616ad7991c463402517d1eab4 console-operator sha256:85c9a48c9b1896f36cf061bd4890e7f85e0dc383148f2a1dc498e668dee961df container-networking-plugins sha256:1a2ecb28b80800c327ad79fb4c8fb6cc9f0b434fc42a4de5b663b907852ee9fb coredns sha256:b25b8b2219e8c247c088af93e833c9ac390bc63459955e131d89b77c485d144d csi-snapshot-controller sha256:33f89dbd081d119aac8d7c56abcb060906b23d31bc801091b789dea14190493f deployer sha256:b24cd515360ae4eba89d4d92afe2689a84043106f7defe34df28acf252cd45b4 docker-builder sha256:d3cf4e3ad3c3ce4bef52d9543c87a1c555861b726ac9cae0cc57486be1095f8a docker-registry sha256:8b6ab4a0c14118020fa56b70cab440883045003a8d9304c96691a0401ad7117c etcd sha256:aba3c59eb6d088d61b268f83b034230b3396ce67da4f6f6d49201e55efebc6b2 gcp-machine-controllers sha256:1c67b5186bbbdc6f424d611eeff83f11e1985847f4a98f82642dcd0938757b0e grafana sha256:aa5c9d3d828b04418d17a4bc3a37043413bdd7c036a75c41cd5f57d8db8aa25a haproxy-router sha256:7064737dd9d0a43de7a87a094487ab4d7b9e666675c53cf4806d1c9279bd6c2e hyperkube sha256:187b9d29fea1bde9f1785584b4a7bbf9a0b9f93e1323d92d138e61c861b6286c insights-operator sha256:51dc869dc1a105165543d12eeee8229916fc15387210edc6702dbc944f7cedd7 installer sha256:a0f23a3292a23257a16189bdae75f7b5413364799e67a480dfad086737e248e0 installer-artifacts sha256:afe926af218d506a7f64ef3df0d949aa6653a311a320bc833398512d1f000645 ironic sha256:80087bd97c28c69fc08cd291f6115b0e12698abf2e87a3d2bbe0e64f600bae93 ironic-hardware-inventory-recorder sha256:2336af8eb4949ec283dc22865637e3fec80a4f6b1d3b78178d58ea05afbd49c2 ironic-inspector sha256:1f48cc344aab15c107e2fb381f9825613f586e116c218cdaf18d1e67b13e2252 ironic-ipa-downloader sha256:a417b910e06ad030b480988d6864367c604027d6476e02e0c3d5dcd6f6ab4ccb ironic-machine-os-downloader sha256:10b751d8e4ba2975dabc256c7ac4dcf94f4de99be35242505bf8db922e968403 ironic-static-ip-manager sha256:0c122317e3a6407a56a16067d518c18ce08f883883745b2e11a5a39ff695d3d0 jenkins sha256:d4ab77a119479a95a33beac0d94980a7a0a87cf792f5850b30dff4f1f90a9c4d jenkins-agent-maven sha256:10559ec206191a9931b1044260007fe8dcedacb8b171be737dfb1ccca9bbf0f5 jenkins-agent-nodejs sha256:ad9e83ea1ea3f338af4dbc9461f8b243bd817df722909293fde33b4f9cbab2bc k8s-prometheus-adapter sha256:be548d31a65e56234e4b98d6541a14936bc0135875ec61e068578f7014aac31e keepalived-ipfailover sha256:a882a11b55b2fc41b538b59bf5db8e4cfc47c537890e4906fe6bf22f9da75575 kube-client-agent sha256:8eb481214103d8e0b5fe982ffd682f838b969c8ff7d4f3ed4f83d4a444fb841b kube-etcd-signer-server sha256:8468b1c575906ed41aa7c3ac3b0a440bf3bc254d2975ecc5e23f84aa54395c81 kube-proxy sha256:886ae5bd5777773c7ef2fc76f1100cc8f592653ce46f73b816de80a20a113769 kube-rbac-proxy sha256:f6351c3aa750fea93050673f66c5ddaaf9e1db241c7ebe31f555e011b20d8c30 kube-state-metrics sha256:ca47160369e67e1d502e93175f6360645ae02933cceddadedabe53cd874f0f89 kube-storage-version-migrator sha256:319e88c22ea618e7b013166eace41c52eb70c8ad950868205f52385f09e96023 kuryr-cni sha256:3eecf00fdfca50e90ba2d659bd765eb04b5c446579e121656badcfd41da87663 kuryr-controller sha256:7d70c92699a69a589a3c2e1045a16855ba02af39ce09d6a6df9b1dbabacff4f5 libvirt-machine-controllers sha256:cc3c7778de8d9e8e4ed543655392f942d871317f4b3b7ed31208312b4cc2e61f local-storage-static-provisioner sha256:a7ff3ec289d426c7aaee35a459ef8c862b744d709099dedcd98a4579136f7d47 machine-api-operator sha256:4ca2f1b93ad00364c053592aea0992bbb3cb4b2ea2f7d1d1af286c26659c11d3 machine-config-operator sha256:31dfdca3584982ed5a82d3017322b7d65a491ab25080c427f3f07d9ce93c52e2 machine-os-content sha256:b397960b7cc14c2e2603111b7385c6e8e4b0f683f9873cd9252a789175e5c4e1 mdns-publisher sha256:dea1fcb456eae4aabdf5d2d5c537a968a2dafc3da52fe20e8d99a176fccaabce multus-admission-controller sha256:377ed5566c062bd2a677ddc0c962924c81796f8d45346b2eefedf5350d7de6b3 multus-cni sha256:bc58468a736e75083e0771d88095229bdd6c1e58db8aa33ef60b326e0bfaf271 multus-route-override-cni sha256:e078599fde3b974832c06312973fae7ed93334ea30247b11b9f1861e2b0da7d6 multus-whereabouts-ipam-cni sha256:89c386f5c3940d88d9bc2520f422a2983514f928585a51ae376c43f19e5a6cad must-gather sha256:a295d2568410a45f1ab403173ee84d7012bb3ec010c24aa0a17925d08d726e20 oauth-proxy sha256:619bdb128e410b52451dbf79c9efb089e138127812da19a1f69907117480827f oauth-server sha256:58545567c899686cae51d2de4e53a5d49323183a7a3065c0b96ad674686acbe8 openshift-apiserver sha256:8fd79797e6e0e9337fc9689863c3817540a003685a6dfc2a55ecb77059967cef openshift-controller-manager sha256:4485d6eb7625becf581473690858a01ab83244ecb03bb0319bf849068e98a86a openshift-state-metrics sha256:6de02ce03089b715e9f767142de33f006809226f037fe21544e1f79755ade920 openstack-machine-controllers sha256:d61e611416196650c81174967e5f11cbdc051d696e38ba341de169375d985709 operator-lifecycle-manager sha256:6e1bca545c35fb7ae4d0f57006acce9a9fabce792c4026944da68d7ddfdec244 operator-marketplace sha256:f0750960873a7cc96f7106e20ea260dd41c09b8a30ce714092d3dcd8a7ec396d operator-registry sha256:7914f42c9274d263c6ba8623db8e6af4940753dcb4160deb291a9cbc61487414 ovirt-machine-controllers sha256:44f9e65ccd39858bf3d7aa2929f5feac634407e36f912ca88585b445d161506c ovn-kubernetes sha256:d80899ed1a6a9f99eb8c64856cd4e576f6534b7390777f3180afb8a634743d62 pod sha256:d7862a735f492a18cb127742b5c2252281aa8f3bd92189176dd46ae9620ee68a prom-label-proxy sha256:1cf614e8acbe3bcca3978a07489cd47627f3a3bd132a5c2fe0072d9e3e797210 prometheus sha256:5eea86e59ffb32fca37cacff22ad00838ea6b947272138f8a56062f68ec40c28 prometheus-alertmanager sha256:bb710e91873ad50ac10c2821b2a28c29e5b89b5da7740a920235ecc33fb063f5 prometheus-config-reloader sha256:7cadb408d7c78440ddacf2770028ee0389b6840651c753f4b24032548f56b7aa prometheus-node-exporter sha256:7d4e76fea0786f4025e37b5ad0fb30498db5586183fc560554626e91066f60f3 prometheus-operator sha256:6e599a9a8691cce0b40bf1ac5373ddb8009113a2115b5617b2d3a3996174c8f7 sdn sha256:08c256b7b07c57f195faa33ea4273694dd3504d4a85a10dbf7616b91eaa8e661 service-ca-operator sha256:8c9a3071040f956cce15d1e6da70f6f47dc55b609e4f19fe469ce581cd42bfe5 service-catalog sha256:d9a5fbf60e3bbf1c9811e1707ce9bd04e8263552ba3a6bea8f8c7b604808fdf9 telemeter sha256:19cfc3e37e12d9dd4e4dd9307781368bbeb07929b6ab788e99aa5543badee3c9 tests sha256:fc56c9805e2e4a8416c1c5433d7974148f0bad88be4a62feeedcd5d9db4b6ad6 thanos sha256:a4ea116aec2f972991f5a22f39aa1dbc567dddc3429ddca873601714d003a51c 创建内部镜像仓库 # 内部镜像仓库用于存放部署 OCP 集群所需的镜像，仓库本身使用 Quay 部署。Quay 包含了几个核心组件：\n数据库 : 主要存放镜像仓库的元数据（非镜像存储) Redis : 存放构建日志和Quay的向导 Quay : 作为镜像仓库 Clair : 提供镜像扫描功能 首先修改镜像仓库节点的主机名：\n$ hostnamectl set-hostname registry.openshift4.example.com 所有节点主机名都要采用三级域名格式，如 master1.aa.bb.com。 接着安装 podman：\n$ yum install -y podman 先创建一个 Pod，用来共享 Network Namespace：\n🐳 → podman pod create --name quay -p 443:8443 安装 Mysql 数据库：\n$ mkdir -p /data/quay/lib/mysql $ chmod 777 /data/quay/lib/mysql $ export MYSQL_CONTAINER_NAME=quay-mysql $ export MYSQL_DATABASE=enterpriseregistrydb $ export MYSQL_PASSWORD=\u0026lt;PASSWD\u0026gt; $ export MYSQL_USER=quayuser $ export MYSQL_ROOT_PASSWORD=\u0026lt;PASSWD\u0026gt; $ podman run \\ --detach \\ --restart=always \\ --env MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \\ --env MYSQL_USER=${MYSQL_USER} \\ --env MYSQL_PASSWORD=${MYSQL_PASSWORD} \\ --env MYSQL_DATABASE=${MYSQL_DATABASE} \\ --name ${MYSQL_CONTAINER_NAME} \\ --privileged=true \\ --pod quay \\ -v /data/quay/lib/mysql:/var/lib/mysql/data:Z \\ registry.access.redhat.com/rhscl/mysql-57-rhel7 安装 Redis：\n$ mkdir -p /data/quay/lib/redis $ chmod 777 /data/quay/lib/redis $ podman run -d --restart=always \\ --pod quay \\ --privileged=true \\ --name quay-redis \\ -v /data/quay/lib/redis:/var/lib/redis/data:Z \\ registry.access.redhat.com/rhscl/redis-32-rhel7 获取 Red Hat Quay v3 镜像的访问权：\n$ podman login -u=\u0026#34;redhat+quay\u0026#34; -p=\u0026#34;O81WSHRSJR14UAZBK54GQHJS0P1V4CLWAJV1X2C4SD7KO59CQ9N3RE12612XU1HR\u0026#34; quay.io 参考： https://access.redhat.com/solutions/3533201\n配置 Quay：\n$ podman run --privileged=true \\ --name quay-config \\ --pod quay \\ --add-host mysql:127.0.0.1 \\ --add-host redis:127.0.0.1 \\ --add-host clair:127.0.0.1 \\ -d quay.io/redhat/quay:v3.3.0 config icloudnative.io 这一步会启动一个配置 Quay 的进程，打开浏览器访问：https://registry.openshift4.example.com，用户名/密码为：quayconfig/icloudnative.io：\n选择新建配置，然后设置数据库：\n设置超级管理员：\n下一个界面要设置两个地方，一个是 Server configuration 的 Server Hostname，另一个是 Redis Hostname，SSL 不用设置，后面直接通过命令行配置：\n配置检查通过后，就可以保存下载下来：\n最后会导出一个 quay-config.tar.gz，将其上传到 Quay 所在的服务器，解压到配置文件目录：\n$ mkdir -p /data/quay/config $ mkdir -p /data/quay/storage $ cp quay-config.tar.gz /data/quay/config/ $ cd /data/quay/config/ $ tar zxvf quay-config.tar.gz 生成自签名证书：\n# 生成私钥 $ openssl genrsa -out ssl.key 1024 根据私钥生成证书申请文件 csr：\n$ openssl req -new -key ssl.key -out ssl.csr 这里根据命令行向导来进行信息输入：\nCommon Name 可以输入：*.yourdomain.com，这种方式可以生成通配符域名证书。\n使用私钥对证书申请进行签名从而生成证书：\n$ openssl x509 -req -in ssl.csr -out ssl.cert -signkey ssl.key -days 3650 这样就生成了有效期为 10 年的证书文件，对于自己内网服务使用足够。\n或者你也可以一步到位：\n$ openssl req \\ -newkey rsa:2048 -nodes -keyout ssl.key \\ -x509 -days 3650 -out ssl.cert -subj \\ \u0026#34;/C=CN/ST=Shanghai/L=Shanghai/O=IBM/OU=IBM/CN=*.openshift4.example.com\u0026#34; 证书搞定了之后，还需要修改 config.yaml，将协议修改为 https：\nPREFERRED_URL_SCHEME: https 然后停止 quay-config：\n$ podman stop quay-config 最后一步才是部署 Quay：\n$ podman run --restart=always \\ --sysctl net.core.somaxconn=4096 \\ --privileged=true \\ --name quay-master \\ --pod quay \\ --add-host mysql:127.0.0.1 \\ --add-host redis:127.0.0.1 \\ --add-host clair:127.0.0.1 \\ -v /data/quay/config:/conf/stack:Z \\ -v /data/quay/storage:/datastorage:Z \\ -d quay.io/redhat/quay:v3.3.0 安装成功后，将自签名的证书复制到默认信任证书路径：\n$ cp ssl.cert /etc/pki/ca-trust/source/anchors/ssl.crt $ update-ca-trust extract 现在可以通过 podman login 命令来测试仓库的连通性，看到如下字样即表示安装成功（也可以通过浏览器访问 Web UI）：\n🐳 → podman login registry.openshift4.example.com Username: admin Password: ******** Login Succeeded 如果使用 Docker 登录，需要将证书复制到 docker 的信任证书路径：\n$ mkdir -p /etc/docker/certs.d/registry.openshift4.example.com $ cp ssl.cert /etc/docker/certs.d/registry.openshift4.example.com/ssl.crt $ systemctl restart docker 下载镜像文件 # 准备拉取镜像权限认证文件。 从 Red Hat OpenShift Cluster Manager 站点的 Pull Secret 页面下载 registry.redhat.io 的 pull secret。\n# 把下载的 txt 文件转出 json 格式，如果没有 jq 命令，通过 epel 源安装 $ cat ./pull-secret.txt | jq . \u0026gt; pull-secret.json $ yum install epel-release $ yum install jq JSON 内容如下：\n{ \u0026#34;auths\u0026#34;: { \u0026#34;cloud.openshift.com\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;b3BlbnNo...\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;you@example.com\u0026#34; }, \u0026#34;quay.io\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;b3BlbnNo...\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;you@example.com\u0026#34; }, \u0026#34;registry.connect.redhat.com\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;NTE3Njg5Nj...\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;you@example.com\u0026#34; }, \u0026#34;registry.redhat.io\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;NTE3Njg5Nj...\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;you@example.com\u0026#34; } } } 把本地仓库的用户密码转换成 base64 编码：\n$ echo -n \u0026#39;admin:password\u0026#39; | base64 -w0 cm9vdDpwYXNzd29yZA== 然后在 pull-secret.json 里面加一段本地仓库的权限。第一行仓库域名和端口，第二行是上面的 base64，第三行随便填个邮箱：\n\u0026#34;auths\u0026#34;: { ... \u0026#34;registry.openshift4.example.com\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;cm9vdDpwYXNzd29yZA==\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;you@example.com\u0026#34; }, ... 设置环境变量：\n$ export OCP_RELEASE=\u0026#34;4.4.5-x86_64\u0026#34; $ export LOCAL_REGISTRY=\u0026#39;registry.openshift4.example.com\u0026#39; $ export LOCAL_REPOSITORY=\u0026#39;ocp4/openshift4\u0026#39; $ export PRODUCT_REPO=\u0026#39;openshift-release-dev\u0026#39; $ export LOCAL_SECRET_JSON=\u0026#39;/root/pull-secret.json\u0026#39; $ export RELEASE_NAME=\u0026#34;ocp-release\u0026#34; OCP_RELEASE : OCP 版本，可以在 这个页面查看。如果版本不对，下面执行 oc adm 时会提示 image does not exist。 LOCAL_REGISTRY : 本地仓库的域名和端口。 LOCAL_REPOSITORY : 镜像存储库名称，使用 ocp4/openshift4。 PRODUCT_REPO 和 RELEASE_NAME 都不需要改，这些都是一些版本特征，保持不变即可。 LOCAL_SECRET_JSON : 密钥路径，就是上面 pull-secret.json 的存放路径。 在 Quay 中创建一个组织（Organization）ocp4 用来存放同步过来的镜像。\n最后一步就是同步镜像，这一步的动作就是把 quay 官方仓库中的镜像同步到本地仓库，如果失败了可以重新执行命令，整体内容大概 5G。\n$ oc adm -a ${LOCAL_SECRET_JSON} release mirror \\ --from=quay.io/${PRODUCT_REPO}/${RELEASE_NAME}:${OCP_RELEASE} \\ --to=${LOCAL_REGISTRY}/${LOCAL_REPOSITORY} \\ --to-release-image=${LOCAL_REGISTRY}/${LOCAL_REPOSITORY}:${OCP_RELEASE} oc adm release mirror 命令执行完成后会输出下面类似的信息，保存下来，将来会用在 install-config.yaml 文件中：\nimageContentSources: - mirrors: - registry.openshift4.example.com/ocp4/openshift4 source: quay.io/openshift-release-dev/ocp-release - mirrors: - registry.openshift4.example.com/ocp4/openshift4 source: quay.io/openshift-release-dev/ocp-v4.0-art-dev 本地镜像仓库缓存好镜像之后，通过 tag/list 接口查看所有 tag，如果能列出来一堆就说明是正常的：\n本地仓库 tag 信息 $ curl -s -X GET -H \u0026#34;Authorization: Bearer \u0026lt;token\u0026gt;\u0026#34; https://registry.openshift4.example.com/api/v1/repository/ocp4/openshift4/tag/|jq . { \u0026#34;has_additional\u0026#34;: true, \u0026#34;page\u0026#34;: 1, \u0026#34;tags\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-cluster-kube-scheduler-operator\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821178, \u0026#34;image_id\u0026#34;: \u0026#34;a778898a93d4fc5413abea38aa604d14d7efbd99ee1ea75d2d1bea3c27a05859\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:18 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:887eda5ce495f1a33c5adbba8772064d3a8b78192162e4c75bd84763c5a1fb01\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;a778898a93d4fc5413abea38aa604d14d7efbd99ee1ea75d2d1bea3c27a05859\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 103582366 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-kube-rbac-proxy\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821178, \u0026#34;image_id\u0026#34;: \u0026#34;f1714cda6028bd7998fbba1eb79348f33b9ed9ccb0a69388da2eb0aefc222f85\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:18 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:f6351c3aa750fea93050673f66c5ddaaf9e1db241c7ebe31f555e011b20d8c30\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;f1714cda6028bd7998fbba1eb79348f33b9ed9ccb0a69388da2eb0aefc222f85\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 102366055 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-cluster-kube-controller-manager-operator\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821178, \u0026#34;image_id\u0026#34;: \u0026#34;bc7e19d35ec08c1a93058db1705998da2f8bbe5cdbb7f3f5974e6176e2f79eb6\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:18 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:0aa16b4ff32fbb9bc7b32aa1bf6441a19a1deb775fb203f21bb8792ff1a26c2e\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;bc7e19d35ec08c1a93058db1705998da2f8bbe5cdbb7f3f5974e6176e2f79eb6\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 104264263 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-baremetal-operator\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821178, \u0026#34;image_id\u0026#34;: \u0026#34;6ec90c0fb53125801d41b37f8f28c4679e49ce19427f7848803a2bc397e4c23b\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:18 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:a77ff02f349d96567da8e06018ad0dfbfb5fef6600a9a216ade15fadc574f4b4\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;6ec90c0fb53125801d41b37f8f28c4679e49ce19427f7848803a2bc397e4c23b\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 110117444 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-cluster-etcd-operator\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821178, \u0026#34;image_id\u0026#34;: \u0026#34;d0cf3539496e075954e53fce5ed56445ae87f9f32cfb41e9352a23af4aa04d69\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:18 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:9f7a02df3a5d91326d95e444e2e249f8205632ae986d6dccc7f007ec65c8af77\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;d0cf3539496e075954e53fce5ed56445ae87f9f32cfb41e9352a23af4aa04d69\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 103890103 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-openshift-apiserver\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821177, \u0026#34;image_id\u0026#34;: \u0026#34;eba5a051dcbab534228728c7295d31edc0323c7930fa44b40059cf8d22948363\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:17 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:8fd79797e6e0e9337fc9689863c3817540a003685a6dfc2a55ecb77059967cef\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;eba5a051dcbab534228728c7295d31edc0323c7930fa44b40059cf8d22948363\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 109243025 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-kube-client-agent\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821177, \u0026#34;image_id\u0026#34;: \u0026#34;fc1fdfb96e9cd250024094b15efa79344c955c7d0c93253df312ffdae02b5524\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:17 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:8eb481214103d8e0b5fe982ffd682f838b969c8ff7d4f3ed4f83d4a444fb841b\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;fc1fdfb96e9cd250024094b15efa79344c955c7d0c93253df312ffdae02b5524\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 99721802 }, { \u0026#34;name\u0026#34;: \u0026#34;4.4.5-kube-proxy\u0026#34;, \u0026#34;reversion\u0026#34;: false, \u0026#34;start_ts\u0026#34;: 1590821177, \u0026#34;image_id\u0026#34;: \u0026#34;d2577f4816cb81444ef3b441bf9769904c602cd6626982c2fd8ebba162fd0c08\u0026#34;, \u0026#34;last_modified\u0026#34;: \u0026#34;Sat, 30 May 2020 06:46:17 -0000\u0026#34;, \u0026#34;manifest_digest\u0026#34;: \u0026#34;sha256:886ae5bd5777773c7ef2fc76f1100cc8f592653ce46f73b816de80a20a113769\u0026#34;, \u0026#34;docker_image_id\u0026#34;: \u0026#34;d2577f4816cb81444ef3b441bf9769904c602cd6626982c2fd8ebba162fd0c08\u0026#34;, \u0026#34;is_manifest_list\u0026#34;: false, \u0026#34;size\u0026#34;: 103473573 }, ... } 这里需要创建一个 OAuth access token 来访问 Quay 的 API，创建过程如下：\n浏览器登录 Red Hat Quay，选择一个组织（Organization），例如 ocp4。 在左侧导航中选择 Applications 图标。 选择 Create New Application，输入 Application 的名字然后回车。 选择你新创建的 Application，在左侧导航栏中选择 Generate Token。 选择相应的权限，然后点击 Generate Access Token。 再次确认你设置的权限，然后点击 Authorize Application。 保管好生成的 token。 Quay 的 API 文档可以参考这里： Appendix A: Red Hat Quay Application Programming Interface (API)。\nQuay 中也能看到所有的镜像：\n提取 openshift-install 命令 # 为了保证安装版本一致性，需要从镜像库中提取 openshift-install 二进制文件，不能直接从 https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.4.5 下载，不然后面会有 sha256 匹配不上的问题。\n# 这一步需要用到上面的 export 变量 $ oc adm release extract \\ -a ${LOCAL_SECRET_JSON} \\ --command=openshift-install \\ \u0026#34;${LOCAL_REGISTRY}/${LOCAL_REPOSITORY}:${OCP_RELEASE}\u0026#34; 如果提示 error: image dose not exist，说明拉取的镜像不全，或者版本不对。\n把文件移动到 $PATH 并确认版本：\n$ chmod +x openshift-install $ mv openshift-install /usr/local/bin/ $ openshift-install version openshift-install 4.4.5 built from commit 15eac3785998a5bc250c9f72101a4a9cb767e494 release image registry.openshift4.example.com/ocp4/openshift4@sha256:4a461dc23a9d323c8bd7a8631bed078a9e5eec690ce073f78b645c83fb4cdf74 3. 准备 Image Stream 样例镜像 # 准备一个镜像列表，然后使用 oc image mirror 将镜像同步到私有仓库中：\ncat sample-images.txt | while read line; do target=$(echo $line | sed \u0026#39;s/registry.redhat.io/registry.openshift4.example.com/\u0026#39;) oc image mirror -a ${LOCAL_SECRET_JSON} $line $target done 如果之前装过 OCP 4.4.5，把 openshift-cluster-samples-operator 项目下 cluster-samples-operator Pod 的 /opt/openshift 目录同步出来，简单 grep 一下就都有了完整的镜像列表。\n完整列表参考 这里。\n同步过程中如果遇到报错，可根据报错信息到 Quay 中创建相应的 Organization，不用中断任务。这里给出一个参考，需要创建以下的 Organization：\nrhscl jboss-datavirt-6 3scale-amp21 3scale-amp22 3scale-amp23 3scale-amp24 3scale-amp25 3scale-amp26 jboss-eap-6 devtools openshift3 rhpam-7 rhdm-7 jboss-amq-6 jboss-datagrid-7 jboss-datagrid-6 jboss-webserver-3 amq-broker-7 jboss-webserver-5 redhat-sso-7 openjdk redhat-openjdk-18 fuse7 dotnet 4. 准备 OperatorHub 离线资源 # 首先在 Quay 中创建一个 devinfra 项目，然后构建 RedHat Operators 的 catalog image, 保存为 registry.openshift4.example.com/devinfra/redhat-operators:v1。\n$ oc adm catalog build \\ -a ${LOCAL_SECRET_JSON} \\ --appregistry-endpoint https://quay.io/cnr \\ --from=registry.redhat.io/openshift4/ose-operator-registry:v4.4 \\ --appregistry-org redhat-operators \\ --to=registry.openshift4.example.com/devinfra/redhat-operators:v1 这个 catalog image 相当于 RedHat Operators 的一个目录，通过 catalog image 可以找到 RedHat Operators 的所有镜像。而且 catalog image 使用 sha256 digest 来引用镜像，能够确保应用有稳定可重复的部署。\n然后使用 catalog image 同步 RedHat Operators 的所有镜像到私有仓库：\n$ oc adm catalog mirror \\ -a ${LOCAL_SECRET_JSON} \\ registry.openshift4.example.com/devinfra/redhat-operators:v1 \\ registry.openshift4.example.com 如果执行过程中遇到 project not found 之类的错误，可根据报错信息到 Quay 中创建相应的项目，不用中断任务。\n这里还会遇到一个 bug，执行到最后会有如下的报错信息：\n... I0409 08:04:48.342110 11331 mirror.go:231] wrote database to /tmp/db-225652515/bundles.db W0409 08:04:48.347417 11331 mirror.go:258] errors during mirroring. the full contents of the catalog may not have been mirrored: couldn\u0026#39;t parse image for mirroring (), skipping mirror: invalid reference format I0409 08:04:48.385816 11331 mirror.go:329] wrote mirroring manifests to redhat-operators-manifests 先来看看有哪些 Operators：\n$ sqlite3 /tmp/db-225652515/bundles.db \u0026#39;select * from related_image\u0026#39;|grep \u0026#39;^|\u0026#39; 随便挑一个 Operator，查看其 ClusterServiceVersion 的 spec.relatedImages 字段内容：\n$ cat /tmp/cache-943388495/manifests-698804708/3scale-operator/3scale-operator-9re7jpyl/0.5.0/3scale-operator.v0.5.0.clusterserviceversion.yaml ... spec: replaces: 3scale-operator.v0.4.2 relatedImages: - name: apicast-gateway-rhel8 image: registry.redhat.io/3scale-amp2/apicast-gateway-rhel8@sha256:21be62a6557846337dc0cf764be63442718fab03b95c198a301363886a9e74f9 - name: backend-rhel7 image: registry.redhat.io/3scale-amp2/backend-rhel7@sha256:ea8a31345d3c2a56b02998b019db2e17f61eeaa26790a07962d5e3b66032d8e5 - name: system-rhel7 image: registry.redhat.io/3scale-amp2/system-rhel7@sha256:93819c324831353bb8f7cb6e9910694b88609c3a20d4c1b9a22d9c2bbfbad16f - name: zync-rhel7 image: registry.redhat.io/3scale-amp2/zync-rhel7@sha256:f4d5c1fdebe306f4e891ddfc4d3045a622d2f01db21ecfc9397cab25c9baa91a - name: memcached-rhel7 image: registry.redhat.io/3scale-amp2/memcached-rhel7@sha256:ff5f3d2d131631d5db8985a5855ff4607e91f0aa86d07dafdcec4f7da13c9e05 - name: redis-32-rhel7 value: registry.redhat.io/rhscl/redis-32-rhel7@sha256:a9bdf52384a222635efc0284db47d12fbde8c3d0fcb66517ba8eefad1d4e9dc9 - name: mysql-57-rhel7 value: registry.redhat.io/rhscl/mysql-57-rhel7@sha256:9a781abe7581cc141e14a7e404ec34125b3e89c008b14f4e7b41e094fd3049fe - name: postgresql-10-rhel7 value: registry.redhat.io/rhscl/postgresql-10-rhel7@sha256:de3ab628b403dc5eed986a7f392c34687bddafee7bdfccfd65cecf137ade3dfd ... 可以看到 relatedImages 列表中有些条目的键是 value 而不是 image，这就是问题所在！ 那些没有 image 的条目在反序列化时会将 image 的值当成空字符串 \u0026quot;\u0026quot;：\n$ sqlite3 /tmp/db-225652515/bundles.db \u0026#39;select * from related_image where operatorbundle_name=\u0026#34;3scale-operator.v0.5.0\u0026#34;\u0026#39; registry.redhat.io/3scale-amp2/zync-rhel7@sha256:f4d5c1fdebe306f4e891ddfc4d3045a622d2f01db21ecfc9397cab25c9baa91a|3scale-operator.v0.5.0 registry.redhat.io/3scale-amp2/memcached-rhel7@sha256:ff5f3d2d131631d5db8985a5855ff4607e91f0aa86d07dafdcec4f7da13c9e05|3scale-operator.v0.5.0 |3scale-operator.v0.5.0 registry.redhat.io/3scale-amp2/apicast-gateway-rhel8@sha256:21be62a6557846337dc0cf764be63442718fab03b95c198a301363886a9e74f9|3scale-operator.v0.5.0 registry.redhat.io/3scale-amp2/backend-rhel7@sha256:ea8a31345d3c2a56b02998b019db2e17f61eeaa26790a07962d5e3b66032d8e5|3scale-operator.v0.5.0 registry.redhat.io/3scale-amp2/3scale-rhel7-operator@sha256:2ba16314ee046b3c3814fe4e356b728da6853743bd72f8651e1a338e8bbf4f81|3scale-operator.v0.5.0 registry.redhat.io/3scale-amp2/system-rhel7@sha256:93819c324831353bb8f7cb6e9910694b88609c3a20d4c1b9a22d9c2bbfbad16f|3scale-operator.v0.5.0 从上面的输出可以看到键为 value 的那几个条目都反序列化失败了，具体的讨论参考： bundle validate should validate that there are no empty relatedImages。\n这里给出一个临时解决方案，先打开另外一个窗口，然后回到原来的窗口执行命令：\n$ oc adm catalog mirror \\ -a ${LOCAL_SECRET_JSON} \\ registry.openshift4.example.com/devinfra/redhat-operators:v1 \\ registry.openshift4.example.com 然后迅速切到下一个窗口，查找最新的 manifest 缓存目录：\n$ ls -l /tmp/cache-*/ 根据日期判断最新的缓存目录，假设是 /tmp/cache-320634009，然后将所有的 value 替换为 image：\n$ sed -i \u0026#34;s/value: registry/image: registry/g\u0026#34; $(egrep -rl \u0026#34;value: registry\u0026#34; /tmp/cache-320634009/) 同步完成后会产生 redhat-operators-manifests 目录，下面有两个文件:\nimageContentSourcePolicy.yaml : 定义了一个 ImageContentSourcePolicy 对象，该对象可以配置节点将其对官方 Operator manifests 中镜像的引用改为对本地镜像仓库中镜像的引用。 mapping.txt : 包含了所有的源镜像在本地镜像仓库中的映射位置。oc image mirror 命令可以引用该文件进一步修改镜像配置。 然而目前这么做还是有问题 1800674: 同步出来的镜像 manifest digest 不对，导致后面离线安装 Operator 时会报镜像无法获取的错误。\n暂时可以使用上面 bugzilla 链接里给出的临时解决方案，先安装 skopeo：\n$ yum install -y golang gpgme-devel libassuan-devel btrfs-progs-devel device-mapper-devel $ git clone https://github.com/containers/skopeo $ cd skopeo $ make binary-local $ mv skopeo /usr/local/bin/ 从 pull-secret.json 中解码 quay.io、registry.redhat.io 和 registry.access.redhat.com 的用户名密码，然后通过下面的命令认证：\n$ skopeo login -u \u0026lt;quay.io_user\u0026gt; -p \u0026lt;quay.io_psw\u0026gt; quay.io $ skopeo login -u \u0026lt;registry.redhat.io_user\u0026gt; -p \u0026lt;registry.redhat.io_psw\u0026gt; registry.redhat.io $ skopeo login -u \u0026lt;registry.access.redhat.com_user\u0026gt; -p \u0026lt;registry.access.redhat.com_psw\u0026gt; registry.access.redhat.com 最后同步镜像的 manifest digest：\ncat redhat-operators-manifests/mapping.txt | while read line; do origin=$(echo $line | cut -d= -f1) target=$(echo $line | cut -d= -f2) if [[ \u0026#34;$origin\u0026#34; =~ \u0026#34;sha256\u0026#34; ]]; then tag=$(echo $origin | cut -d: -f2 | cut -c -8) skopeo copy --all docker://$origin docker://$target:$tag else skopeo copy --all docker://$origin docker://$target fi done 不得不说，OCP 的安装真是个浩大的工程，这洋洋洒洒的一大篇也只是准备了离线资源，这只是安装的一小步，还有很长的步骤要写，心理素质不过关的同学切勿随意模仿。\n5. 参考资料 # 离线部署 Openshift Container Platform 4.3 - 1: 准备离线资源 Chapter 9. Using Operator Lifecycle Manager on restricted networks ","date":"2020年5月28日","externalUrl":null,"permalink":"/posts/openshift4.4-install-offline-static-1-requirement/","section":"博客","summary":"本系列文章描述了离线环境下以 UPI (User Provisioned Infrastructure) 模式安装 Openshift Container Platform (OCP) 4.4.5 的步骤","title":"Openshift 4.4 静态 IP 离线安装系列：准备离线资源","type":"posts"},{"content":"","date":"2020年5月28日","externalUrl":null,"permalink":"/tags/quay/","section":"标签","summary":"","title":"Quay","type":"tags"},{"content":" 原文链接： Why Does My Docker Container Take 10+ Seconds to Stop?\n作为一名系统重启工程师（SRE），你可能经常需要重启容器，毕竟 Kubernetes 的优势就是快速弹性伸缩和故障恢复，遇到问题先重启容器再说，几秒钟即可恢复，实在不行再重启系统，这就是系统重启工程师的杀手锏。然而现实并没有理论上那么美好，某些容器需要花费 10s 左右才能停止，这是为啥？有以下几种可能性：\n容器中的进程没有收到 SIGTERM 信号。 容器中的进程收到了信号，但忽略了。 容器中应用的关闭时间确实就是这么长。 对于第 3 种可能性我们无能为力，本文主要解决 1 和 2。\n如果要构建一个新的 Docker 镜像，肯定希望镜像越小越好，这样它的下载和启动速度都很快，一般我们都会选择一个瘦了身的操作系统（例如 Alpine，Busybox 等）作为基础镜像。\n问题就在这里，这些基础镜像的 init 系统也被抹掉了，这就是问题的根源！\ninit 系统有以下几个特点：\n它是系统的第一个进程，负责产生其他所有用户进程。 init 以守护进程方式存在，是所有其他进程的祖先。 它主要负责： 启动守护进程 回收孤儿进程 将操作系统信号转发给子进程 1. Docker 容器停止过程 # 对于容器来说，init 系统不是必须的，当你通过命令 docker stop mycontainer 来停止容器时，docker CLI 会将 TERM 信号发送给 mycontainer 的 PID 为 1 的进程。\n如果 PID 1 是 init 进程 - 那么 PID 1 会将 TERM 信号转发给子进程，然后子进程开始关闭，最后容器终止。 如果没有 init 进程 - 那么容器中的应用进程（Dockerfile 中的 ENTRYPOINT 或 CMD 指定的应用）就是 PID 1，应用进程直接负责响应 TERM 信号。这时又分为两种情况： 应用不处理 SIGTERM - 如果应用没有监听 SIGTERM 信号，或者应用中没有实现处理 SIGTERM 信号的逻辑，应用就不会停止，容器也不会终止。 容器停止时间很长 - 运行命令 docker stop mycontainer 之后，Docker 会等待 10s，如果 10s 后容器还没有终止，Docker 就会绕过容器应用直接向内核发送 SIGKILL，内核会强行杀死应用，从而终止容器。 2. 容器进程收不到 SIGTERM 信号？ # 如果容器中的进程没有收到 SIGTERM 信号，很有可能是因为应用进程不是 PID 1，PID 1 是 shell，而应用进程只是 shell 的子进程。而 shell 不具备 init 系统的功能，也就不会将操作系统的信号转发到子进程上，这也是容器中的应用没有收到 SIGTERM 信号的常见原因。\n问题的根源就来自 Dockerfile，例如：\nFROM alpine:3.7 COPY popcorn.sh . RUN chmod +x popcorn.sh ENTRYPOINT ./popcorn.sh ENTRYPOINT 指令使用的是 shell 模式，这样 Docker 就会把应用放到 shell 中运行，因此 shell 是 PID 1。\n解决方案有以下几种：\n方案 1：使用 exec 模式的 ENTRYPOINT 指令 # 与其使用 shell 模式，不如使用 exec 模式，例如：\nFROM alpine:3.7 COPY popcorn.sh . RUN chmod +x popcorn.sh ENTRYPOINT [\u0026#34;./popcorn.sh\u0026#34;] 这样 PID 1 就是 ./popcorn.sh，它将负责响应所有发送到容器的信号，至于 ./popcorn.sh 是否真的能捕捉到系统信号，那是另一回事。\n举个例子，假设使用上面的 Dockerfile 来构建镜像，popcorn.sh 脚本每过一秒打印一次日期：\n#!/bin/sh while true do date sleep 1 done 构建镜像并创建容器：\n🐳 → docker build -t truek8s/popcorn . 🐳 → docker run -it --name corny --rm truek8s/popcorn 打开另外一个终端执行停止容器的命令，并计时：\n🐳 → time docker stop corny 因为 popcorn.sh 并没有实现捕获和处理 SIGTERM 信号的逻辑，所以需要 10s 左右才能停止容器。要想解决这个问题，就要往脚本中添加信号处理代码，让它捕获到 SIGTERM 信号时就终止进程：\n#!/bin/sh # catch the TERM signal and then exit trap \u0026#34;exit\u0026#34; TERM while true do date sleep 1 done 注意：下面这条指令与 shell 模式的 ENTRYPOINT 指令是等效的：\nENTRYPOINT [\u0026#34;/bin/sh\u0026#34;, \u0026#34;./popcorn.sh\u0026#34;] 方案 2：直接使用 exec 命令 # 如果你就想使用 shell 模式的 ENTRYPOINT 指令，也不是不可以，只需将启动命令追加到 exec 后面即可，例如：\nFROM alpine:3.7 COPY popcorn.sh . RUN chmod +x popcorn.sh ENTRYPOINT exec ./popcorn.sh 这样 exec 就会将 shell 进程替换为 ./popcorn.sh 进程，PID 1 仍然是 ./popcorn.sh。\n方案 3：使用 init 系统 # 如果容器中的应用默认无法处理 SIGTERM 信号，又不能修改代码，这时候方案 1 和 2 都行不通了，只能在容器中添加一个 init 系统。init 系统有很多种，这里推荐使用 tini，它是专用于容器的轻量级 init 系统，使用方法也很简单：\n安装 tini 将 tini 设为容器的默认应用 将 popcorn.sh 作为 tini 的参数 具体的 Dockerfile 如下：\nFROM alpine:3.7 COPY popcorn.sh . RUN chmod +x popcorn.sh RUN apk add --no-cache tini ENTRYPOINT [\u0026#34;/sbin/tini\u0026#34;, \u0026#34;--\u0026#34;, \u0026#34;./popcorn.sh\u0026#34;] 现在 tini 就是 PID 1，它会将收到的系统信号转发给子进程 popcorn.sh。\n如果你想直接通过 docker 命令来运行容器，可以直接通过参数 --init 来使用 tini，不需要在镜像中安装 tini。如果是 Kubernetes 就不行了，还得老老实实安装 tini。 3. 使用 tini 后应用还需要处理 SIGTERM 吗？ # 最后一个问题：如果移除 popcorn.sh 中对 SIGTERM 信号的处理逻辑，容器会在我们执行停止命令后立即终止吗？\n答案是肯定的。在 Linux 系统中，PID 1 和其他进程不太一样，准确地说应该是 init 进程和其他进程不一样，它不会执行与接收到的信号相关的默认动作，必须在代码中明确实现捕获处理 SIGTERM 信号的逻辑，方案 1 和 2 干的就是这个事。\n普通进程就简单多了，只要它收到系统信号，就会执行与该信号相关的默认动作，不需要在代码中显示实现逻辑，因此可以优雅终止。\n","date":"2020年5月27日","externalUrl":null,"permalink":"/posts/why-does-my-docker-container-take-10-seconds-to-stop/","section":"博客","summary":"原文链接： Why Does My Docker Container Take 10+ Seconds to Stop? 作为一名系统重启工程师（SRE","title":"Docker 容器优雅终止方案","type":"posts"},{"content":" 原文链接： How It Works — kubectl exec\n对于经常和 Kubernetes 打交道的 YAML 工程师来说，最常用的命令就是 kubectl exec 了，通过它可以直接在容器内执行命令来调试应用程序。如果你不满足于只是用用而已，想了解 kubectl exec 的工作原理，那么本文值得你仔细读一读。本文将通过参考 kubectl、API Server、Kubelet 和容器运行时接口（CRI）Docker API 中的相关代码来了解该命令是如何工作的。\nkubectl exec 的工作原理用一张图就可以表示：\n先来看一个例子：\n🐳 → kubectl version --short Client Version: v1.15.0 Server Version: v1.15.3 🐳 → kubectl run nginx --image=nginx --port=80 --generator=run-pod/v1 pod/nginx created 🐳 → kubectl get po NAME READY STATUS RESTARTS AGE nginx 1/1 Running 0 6s 🐳 → kubectl exec nginx -- date Sat Jan 25 18:47:52 UTC 2020 🐳 → kubectl exec -it nginx -- /bin/bash root@nginx:/# 第一个 kubectl exec 在容器内执行了 date 命令，第二个 kubectl exec 使用 -i 和 -t 参数进入了容器的交互式 shell。\n重复第二个 kubectl exec 命令，打印更详细的日志：\n🐳 → kubectl -v=7 exec -it nginx -- /bin/bash I0125 10:51:55.434043 28053 loader.go:359] Config loaded from file: /home/isim/.kube/kind-config-linkerd I0125 10:51:55.438595 28053 round_trippers.go:416] GET https://127.0.0.1:38545/api/v1/namespaces/default/pods/nginx I0125 10:51:55.438607 28053 round_trippers.go:423] Request Headers: I0125 10:51:55.438611 28053 round_trippers.go:426] Accept: application/json, */* I0125 10:51:55.438615 28053 round_trippers.go:426] User-Agent: kubectl/v1.15.0 (linux/amd64) kubernetes/e8462b5 I0125 10:51:55.445942 28053 round_trippers.go:441] Response Status: 200 OK in 7 milliseconds I0125 10:51:55.451050 28053 round_trippers.go:416] POST https://127.0.0.1:38545/api/v1/namespaces/default/pods/nginx/exec?command=%2Fbin%2Fbash\u0026amp;container=nginx\u0026amp;stdin=true\u0026amp;stdout=true\u0026amp;tty=true I0125 10:51:55.451063 28053 round_trippers.go:423] Request Headers: I0125 10:51:55.451067 28053 round_trippers.go:426] X-Stream-Protocol-Version: v4.channel.k8s.io I0125 10:51:55.451090 28053 round_trippers.go:426] X-Stream-Protocol-Version: v3.channel.k8s.io I0125 10:51:55.451096 28053 round_trippers.go:426] X-Stream-Protocol-Version: v2.channel.k8s.io I0125 10:51:55.451100 28053 round_trippers.go:426] X-Stream-Protocol-Version: channel.k8s.ioI0125 10:51:55.451121 28053 round_trippers.go:426] User-Agent: kubectl/v1.15.0 (linux/amd64) kubernetes/e8462b5 I0125 10:51:55.465690 28053 round_trippers.go:441] Response Status: 101 Switching Protocols in 14 milliseconds root@nginx:/# 这里有两个重要的 HTTP 请求：\nGET 请求用来 获取 Pod 信息。 POST 请求调用 Pod 的子资源 exec 在容器内执行命令。 子资源（subresource）隶属于某个 K8S 资源，表示为父资源下方的子路径，例如 /logs、/status、/scale、/exec 等。其中每个子资源支持的操作根据对象的不同而改变。 最后 API Server 返回了 101 Ugrade 响应，向客户端表示已切换到 SPDY 协议。\nSPDY 允许在单个 TCP 连接上复用独立的 stdin/stdout/stderr/spdy-error 流。 1. API Server 源码分析 # 请求首先会到底 API Server，先来看看 API Server 是如何注册 rest.ExecRest 处理器来处理子资源请求 /exec 的。这个处理器用来确定 exec 要进入的节点。\nAPI Server 启动过程中做的第一件事就是指挥内嵌的 GenericAPIServer 加载早期的遗留 API（legacy API）：\nif c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion) { // ... if err := m.InstallLegacyAPI(\u0026amp;c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider); err != nil { return nil, err } } 在 API 加载过程中，会将类型 LegacyRESTStorage 实例化，创建一个 storage.PodStorage 实例：\npodStorage, err := podstore.NewStorage( restOptionsGetter, nodeStorage.KubeletConnectionInfo, c.ProxyTransport, podDisruptionClient, ) if err != nil { return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err } 随后 storeage.PodStorage 实例会被添加到 map restStorageMap 中。注意，该 map 将路径 pods/exec 映射到了 podStorage 的 rest.ExecRest 处理器。\nrestStorageMap := map[string]rest.Storage{ \u0026#34;pods\u0026#34;: podStorage.Pod, \u0026#34;pods/attach\u0026#34;: podStorage.Attach, \u0026#34;pods/status\u0026#34;: podStorage.Status, \u0026#34;pods/log\u0026#34;: podStorage.Log, \u0026#34;pods/exec\u0026#34;: podStorage.Exec, \u0026#34;pods/portforward\u0026#34;: podStorage.PortForward, \u0026#34;pods/proxy\u0026#34;: podStorage.Proxy, \u0026#34;pods/binding\u0026#34;: podStorage.Binding, \u0026#34;bindings\u0026#34;: podStorage.LegacyBinding, podstorage 为 pod 和子资源提供了 CURD 逻辑和策略的抽象。更多详细信息请查看内嵌的 genericregistry.Store map restStorageMap 会成为实例 apiGroupInfo 的一部分，添加到 GenericAPIServer 中：\nif err := s.installAPIResources(apiPrefix, apiGroupInfo, openAPIModels); err != nil { return err } // Install the version handler. // Add a handler at /\u0026lt;apiPrefix\u0026gt; to enumerate the supported api versions. s.Handler.GoRestfulContainer.Add(discovery.NewLegacyRootAPIHandler(s.discoveryAddresses, s.Serializer, apiPrefix).WebService()) 其中 GoRestfulContainer.ServeMux 会将传入的请求 URL 映射到不同的处理器。\n接下来重点观察处理器 therest.ExecRest 的工作原理，它的 Connect() 方法会调用函数 pod.ExecLocation() 来确定 pod 中容器的 exec 子资源的 URL：\n// Connect returns a handler for the pod exec proxy func (r *ExecREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { execOpts, ok := opts.(*api.PodExecOptions) if !ok { return nil, fmt.Errorf(\u0026#34;invalid options object: %#v\u0026#34;, opts) } location, transport, err := pod.ExecLocation(r.Store, r.KubeletConn, ctx, name, execOpts) if err != nil { return nil, err } return newThrottledUpgradeAwareProxyHandler(location, transport, false, true, true, responder), nil } 函数 pod.ExecLocation() 返回的 URL 被 API Server 用来决定连接到哪个节点。\n下面接着分析节点上的 Kubelet 源码。\n2. Kubelet 源码分析 # 到了 Kubelet 这边，我们需要关心两点：\nKubelet 是如何注册 exec 处理器的？ Kubelet 与 Docker API 如何交互？ Kubelet 的初始化过程非常复杂，主要涉及到两个函数：\nPreInitRuntimeService() : 使用 dockershim 包来初始化 CRI。 RunKubelet() : 注册处理器，启动 Kubelet 服务。 注册处理器 # 当 Kubelet 启动时，它的 RunKubelet() 函数会调用私有函数 startKubelet() 来 启动 kubelet.Kubelet 实例的 ListenAndServe() 方法，然后该方法会 调用函数 ListenAndServeKubeletServer() ，使用构造函数 NewServer() 来安装 『debugging』处理器：\n// NewServer initializes and configures a kubelet.Server object to handle HTTP requests. func NewServer( // ... criHandler http.Handler) Server { // ... if enableDebuggingHandlers { server.InstallDebuggingHandlers(criHandler) if enableContentionProfiling { goruntime.SetBlockProfileRate(1) } } else { server.InstallDebuggingDisabledHandlers() } return server } InstallDebuggingHandlers() 函数使用 getExec() 处理器来注册 HTTP 请求模式：\n// InstallDebuggingHandlers registers the HTTP request patterns that serve logs or run commands/containers func (s *Server) InstallDebuggingHandlers(criHandler http.Handler) { // ... ws = new(restful.WebService) ws. Path(\u0026#34;/exec\u0026#34;) ws.Route(ws.GET(\u0026#34;/{podNamespace}/{podID}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) ws.Route(ws.POST(\u0026#34;/{podNamespace}/{podID}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) ws.Route(ws.GET(\u0026#34;/{podNamespace}/{podID}/{uid}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) ws.Route(ws.POST(\u0026#34;/{podNamespace}/{podID}/{uid}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) s.restfulCont.Add(ws) 其中 getExec() 处理器又会调用 s.host 实例中的 GetExec() 方法：\n// getExec handles requests to run a command inside a container. func (s *Server) getExec(request *restful.Request, response *restful.Response) { // ... podFullName := kubecontainer.GetPodFullName(pod) url, err := s.host.GetExec(podFullName, params.podUID, params.containerName, params.cmd, *streamOpts) if err != nil { streaming.WriteError(err, response.ResponseWriter) return } // ... } s.host 被实例化为 kubelet.Kubelet 类型的一个实例，它嵌套引用了 StreamingRuntime 接口，该接口又被 实例化为 kubeGenericRuntimeManager 的实例，即运行时管理器。该运行时管理器是 Kubelet 与 Docker API 交互的关键组件，GetExec() 方法就是由它实现的：\n// GetExec gets the endpoint the runtime will serve the exec request from. func (m *kubeGenericRuntimeManager) GetExec(id kubecontainer.ContainerID, cmd []string, stdin, stdout, stderr, tty bool) (*url.URL, error) { // ... resp, err := m.runtimeService.Exec(req) if err != nil { return nil, err } return url.Parse(resp.Url) } GetExec() 又会调用 runtimeService.Exec() 方法，进一步挖掘你会发现 runtimeService 是 CRI 包中定义的 接口。kuberuntime.kubeGenericRuntimeManager 的 runtimeService 被实例化为 kuberuntime.instrumentedRuntimeService 类型，由它来实现 runtimeService.Exec() 方法：\nfunc (in instrumentedRuntimeService) Exec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { const operation = \u0026#34;exec\u0026#34; defer recordOperation(operation, time.Now()) resp, err := in.service.Exec(req) recordError(operation, err) return resp, err } instrumentedRuntimeService 实例的嵌套服务对象被 实例化为 theremote.RemoteRuntimeService 类型的实例。该类型实现了 Exec() 方法：\n// Exec prepares a streaming endpoint to execute a command in the container, and returns the address. func (r *RemoteRuntimeService) Exec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { ctx, cancel := getContextWithTimeout(r.timeout) defer cancel() resp, err := r.runtimeClient.Exec(ctx, req) if err != nil { klog.Errorf(\u0026#34;Exec %s \u0026#39;%s\u0026#39; from runtime service failed: %v\u0026#34;, req.ContainerId, strings.Join(req.Cmd, \u0026#34; \u0026#34;), err) return nil, err } if resp.Url == \u0026#34;\u0026#34; { errorMessage := \u0026#34;URL is not set\u0026#34; klog.Errorf(\u0026#34;Exec failed: %s\u0026#34;, errorMessage) return nil, errors.New(errorMessage) } return resp, nil } Exec() 方法会向 /runtime.v1alpha2.RuntimeService/Exec 发起一个 gRPC 调用来让运行时端准备一个流式通信的端点，该端点用于在容器中执行命令（关于如何将 Docker shim 设置为 gRPC 服务端的更多信息请参考下一小节）。\ngRPC 服务端通过调用 RuntimeServiceServer.Exec() 方法来 处理请求，该方法由 dockershim.dockerService 结构体实现：\n// Exec prepares a streaming endpoint to execute a command in the container, and returns the address. func (ds *dockerService) Exec(_ context.Context, req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { if ds.streamingServer == nil { return nil, streaming.NewErrorStreamingDisabled(\u0026#34;exec\u0026#34;) } _, err := checkContainerStatus(ds.client, req.ContainerId) if err != nil { return nil, err } return ds.streamingServer.GetExec(req) } 第 10 行的 ThestreamingServer 是一个 streaming.Server 接口，它在构造函数 dockershim.NewDockerService() 中被实例化：\n// create streaming server if configured. if streamingConfig != nil { var err error ds.streamingServer, err = streaming.NewServer(*streamingConfig, ds.streamingRuntime) if err != nil { return nil, err } } 来看一下 GetExec() 方法的实现方式：\nfunc (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { if err := validateExecRequest(req); err != nil { return nil, err } token, err := s.cache.Insert(req) if err != nil { return nil, err } return \u0026amp;runtimeapi.ExecResponse{ Url: s.buildURL(\u0026#34;exec\u0026#34;, token), }, nil } 可以看到这里只是向客户端返回一个简单的 token 组合成的 URL， 之所以生成一个 token 是因为用户的命令中可能包含各种各样的字符，各种长度的字符，需要格式化为一个简单的 token。 该 token 会缓存在本地，后面真正的 exec 请求会携带这个 token，通过该 token 找到之前的具体请求。其中 restful.WebService 实例会将 pod exec 请求路由到这个端点：\n// InstallDebuggingHandlers registers the HTTP request patterns that serve logs or run commands/containers func (s *Server) InstallDebuggingHandlers(criHandler http.Handler) { // ... ws = new(restful.WebService) ws. Path(\u0026#34;/exec\u0026#34;) ws.Route(ws.GET(\u0026#34;/{podNamespace}/{podID}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) ws.Route(ws.POST(\u0026#34;/{podNamespace}/{podID}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) ws.Route(ws.GET(\u0026#34;/{podNamespace}/{podID}/{uid}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) ws.Route(ws.POST(\u0026#34;/{podNamespace}/{podID}/{uid}/{containerName}\u0026#34;). To(s.getExec). Operation(\u0026#34;getExec\u0026#34;)) s.restfulCont.Add(ws) 创建 Docker shim # PreInitRuntimeService() 函数 作为 gRPC 服务端，负责 创建并启动 Docker shim。在将dockershim.dockerService 类型实例化时，让其嵌套的 streamingRuntime 实例引用 dockershim.NativeExecHandler 的实例（该实例实现了 dockershim.ExecHandler 接口）。\nds := \u0026amp;dockerService{ // ... streamingRuntime: \u0026amp;streamingRuntime{ client: client, execHandler: \u0026amp;NativeExecHandler{}, }, // ... } 使用 Docker 的 exec API 在容器中执行命令的核心实现就是 NativeExecHandler.ExecInContainer() 方法：\nfunc (*NativeExecHandler) ExecInContainer(client libdocker.Interface, container *dockertypes.ContainerJSON, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize \u0026lt;-chan remotecommand.TerminalSize, timeout time.Duration) error { // ... startOpts := dockertypes.ExecStartCheck{Detach: false, Tty: tty} streamOpts := libdocker.StreamOptions{ InputStream: stdin, OutputStream: stdout, ErrorStream: stderr, RawTerminal: tty, ExecStarted: execStarted, } err = client.StartExec(execObj.ID, startOpts, streamOpts) if err != nil { return err } // ... 这里就是最终 Kubelet 调用 Docker exec API 的地方。\n最后需要搞清楚的是 streamingServer 处理器如何处理 exec 请求。首先需要找到它的 exec 处理器，我们直接从构造函数 streaming.NewServer() 开始往下找，因为这是将 /exec/{token} 路径绑定到 serveExec 处理器的地方：\nws := \u0026amp;restful.WebService{} endpoints := []struct { path string handler restful.RouteFunction }{ {\u0026#34;/exec/{token}\u0026#34;, s.serveExec}, {\u0026#34;/attach/{token}\u0026#34;, s.serveAttach}, {\u0026#34;/portforward/{token}\u0026#34;, s.servePortForward}, } 所有发送到 dockershim.dockerService 实例的请求最终都会在 streamingServer 处理器上完成，因为 dockerService.ServeHTTP() 方法会调用 streamingServer 实例的 ServeHTTP() 方法。\nserveExec 处理器会 调用 remoteCommand.ServeExec() 函数，这个函数又是干嘛的呢？它会调用前面提到的 Executor.ExecInContainer() 方法，而 ExecInContainer() 方法是知道如何与 Docker exec API 通信的：\n// ServeExec handles requests to execute a command in a container. After // creating/receiving the required streams, it delegates the actual execution // to the executor. func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podName string, uid types.UID, container string, cmd []string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) { // ... err := executor.ExecInContainer(podName, uid, container, cmd, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan, 0) if err != nil { // ... } else { // ...\t} } 3. 总结 # 本文通过解读 kubectl、API Server 和 CRI 的源码，帮助大家理解 kubectl exec 命令的工作原理，当然，这里并没有涉及到 Docker exec API 的细节，也没有涉及到 docker exec 的工作原理。\n首先，kubectl 向 API Server 发出了 GET 和 POST 请求，API Server 返回了 101 Ugrade 响应，向客户端表示已切换到 SPDY 协议。\n随后 API Server 使用 storage.PodStorage 和 rest.ExecRest 来提供处理器的映射和执行逻辑，其中 rest.ExecRest 处理器决定 exec 要进入的节点。\n最后 Kubelet 向 Docker shim 请求一个流式端点 URL，并将 exec 请求转发到 Docker exec API。kubelet 再将这个 URL 以 Redirect 的方式返回给 API Server，请求就会重定向到到对应 Streaming Server 上发起的 exec 请求，并维护长链。\n虽然本文只关注了 kubectl exec 命令，但其他的子命令（例如 attach、port-forward、log 等等）也遵循了类似的实现模式：\n","date":"2020年5月21日","externalUrl":null,"permalink":"/posts/how-it-works-kubectl-exec/","section":"博客","summary":"原文链接： How It Works — kubectl exec 对于经常和 Kubernetes 打交道的 YAML 工程师来说，最常","title":"Kubectl exec 的工作原理解读","type":"posts"},{"content":"","date":"2020年5月21日","externalUrl":null,"permalink":"/tags/kubelet/","section":"标签","summary":"","title":"Kubelet","type":"tags"},{"content":" 原文链接： Docker Images : Part II - Details Specific To Different Languages\n本系列文章将分为三个部分：\n第一部分着重介绍多阶段构建（multi-stage builds），因为这是镜像精简之路至关重要的一环。在这部分内容中，我会解释静态链接和动态链接的区别，它们对镜像带来的影响，以及如何避免那些不好的影响。中间会穿插一部分对 Alpine 镜像的介绍。链接： Docker 镜像制作教程：减小镜像体积\n第二部分将会针对不同的语言来选择适当的精简策略，其中主要讨论 Go，同时也涉及到了 Java，Node，Python，Ruby 和 Rust。这一部分也会详细介绍 Alpine 镜像的避坑指南。什么？你不知道 Alpine 镜像有哪些坑？我来告诉你。链接： Docker 镜像制作教程：针对不同语言的精简策略\n第三部分将会探讨适用于大多数语言和框架的通用精简策略，例如使用常见的基础镜像、提取可执行文件和减小每一层的体积。同时还会介绍一些更加奇特或激进的工具，例如 Bazel，Distroless，DockerSlim 和 UPX，虽然这些工具在某些特定场景下能带来奇效，但大多情况下会起到反作用。\n本文介绍第二部分。\n1. Go 语言镜像精简 # Go 语言程序编译时会将所有必须的依赖编译到二进制文件中，但也不能完全肯定它使用的是静态链接，因为 Go 的某些包是依赖系统标准库的，例如使用到 DNS 解析的包。只要代码中导入了这些包，编译的二进制文件就需要调用到某些系统库，为了这个需求，Go 实现了一种机制叫 cgo，以允许 Go 调用 C 代码，这样编译好的二进制文件就可以调用系统库。\n也就是说，如果 Go 程序使用了 net 包，就会生成一个动态的二进制文件，如果想让镜像能够正常工作，必须将需要的库文件复制到镜像中，或者直接使用 busybox:glibc 镜像。\n当然，你也可以禁止 cgo，这样 Go 就不会使用系统库，使用内置的实现来替代系统库（例如使用内置的 DNS 解析器），这种情况下生成的二进制文件就是静态的。可以通过设置环境变量 CGO_ENABLED=0 来禁用 cgo，例如：\nFROM golang COPY whatsmyip.go . ENV CGO_ENABLED=0 RUN go build whatsmyip.go FROM scratch COPY --from=0 /go/whatsmyip . CMD [\u0026#34;./whatsmyip\u0026#34;] 由于编译生成的是静态二进制文件，因此可以直接跑在 scratch 镜像中 🎉\n当然，也可以不用完全禁用 cgo，可以通过 -tags 参数指定需要使用的内建库，例如 -tags netgo 就表示使用内建的 net 包，不依赖系统库：\n$ go build -tags netgo whatsmyip.go 这样指定之后，如果导入的其他包都没有用到系统库，那么编译得到的就是静态二进制文件。也就是说，只要还有一个包用到了系统库，都会开启 cgo，最后得到的就是动态二进制文件。要想一劳永逸，还是设置环境变量 CGO_ENABLED=0 吧。\n2. Alpine 镜像探秘 # 上篇文章已经对 Alpine 镜像作了简要的介绍，并保证会在后面的文章中花很大的篇幅来讨论 Alpine 镜像，现在时候到了！\nAlpine 是众多 Linux 发行版中的一员，和 CentOS、Ubuntu、Archlinux 之类一样，只是一个发行版的名字，号称小巧安全，有自己的包管理工具 apk。\n与 CentOS 和 Ubuntu 不同，Alpine 并没有像 Red Hat 或 Canonical 之类的大公司为其提供维护支持，软件包的数量也比这些发行版少很多（如果只看开箱即用的默认软件仓库，Alpine 只有 10000 个软件包，而 Ubuntu、Debian 和 Fedora 的软件包数量均大于 50000。）\n容器崛起之前，Alpine 还是个无名之辈，可能是因为大家并不是很关心操作系统本身的大小，毕竟大家只关心业务数据和文档，程序、库文件和系统本身的大小通常可以忽略不计。\n容器技术席卷整个软件产业之后，大家都注意到了一个问题，那就是容器的镜像太大了，浪费磁盘空间，拉取镜像的时间也很长。于是，人们开始寻求适用于容器的更小的镜像。对于那些耳熟能详的发行版（例如 Ubuntu、Debian、Fedora）来说，只能通过删除某些工具（例如 ifconfig 和 netstat）将镜像体积控制在 100M 以下。而对于 Alpine 而言，什么都不用删除，镜像大小也就只有 5M 而已。\nAlpine 镜像的另一个优势是包管理工具的执行速度非常快，安装软件体验非常顺滑。诚然，在传统的虚拟机上不需要太关心软件包的安装速度，同一个包只需要装一次即可，无需不停重复安装。容器就不一样了，你可能会定期构建新镜像，也可能会在运行的容器中临时安装某些调试工具，如果软件包的安装速度很慢，会很快消磨掉我们的耐心。\n为了更直观，我们来做个简单的对比测试，看看不同的发行版安装 tcpdump 需要多长时间，测试命令如下：\n🐳 → time docker run \u0026lt;image\u0026gt; \u0026lt;packagemanager\u0026gt; install tcpdump 测试结果如下：\nBase image Size Time to install tcpdump --------------------------------------------------------- alpine:3.11 5.6 MB 1-2s archlinux:20200106 409 MB 7-9s centos:8 237 MB 5-6s debian:10 114 MB 5-7s fedora:31 194 MB 35-60s ubuntu:18.04 64 MB 6-8s 如果你想了解更多关于 Alpine 的内幕，可以看看 Natanel Copa 的演讲。\n好吧，既然 Alpine 这么棒，为什么不用它作为所有镜像的基础镜像呢？别急，先一步一步来，为了趟平所有的坑，需要分两种情况来考虑：\n使用 Alpine 作为第二构建阶段（run 阶段）的基础镜像 使用 ALpine 作为所有构建阶段（run 阶段和 build 阶段）的基础镜像 run 阶段使用 Alpine # 带着激动的心情，将 Alpine 镜像加入了 Dockerfile：\nFROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c FROM alpine COPY --from=mybuildstage hello . CMD [\u0026#34;./hello\u0026#34;] 第一个坑来了，启动容器出现了错误：\nstandard_init_linux.go:211: exec user process caused \u0026#34;no such file or directory\u0026#34; 这个报错在上篇文章已经见识过了，上篇文章的场景是使用 scratch 镜像作为 C 语言程序的基础镜像，错误的原因是 scratch 镜像中缺少动态库文件。可是为什么使用 Alpine 镜像也有报错，难道它也缺少动态库文件？\n也不完全是，Alpine 使用的也是动态库，毕竟它的设计目标之一就是占用更少的空间。但 Alpine 使用的标准库与大多数发行版不同，它使用的是 musl libc，这个库相比于 glibc 更小、更简单、更安全，但是与大家常用的标准库 glibc 并不兼容。\n你可能又要问了：『既然 musl libc 更小、更简单，还特么更安全，为啥其他发行版还在用 glibc？』\nmmm。。。因为 glibc 有很多额外的扩展，并且很多程序都用到了这些扩展，而 musl libc 是不包含这些扩展的。详情可以参考 musl 的文档。\n也就是说，如果想让程序跑在 Alpine 镜像中，必须在编译时使用 musl libc 作为动态库。\n所有阶段使用 Alpine # 为了生成一个与 musl libc 链接的二进制文件，有两条路：\n某些官方镜像提供了 Alpine 版本，可以直接拿来用。 还有些官方镜像没有提供 Alpine 版本，我们需要自己构建。 golang 镜像就属于第一种情况，golang:alpine 提供了基于 Alpine 构建的 Go 工具链。\n构建 Go 程序可以使用下面的 Dockerfile：\nFROM golang:alpine COPY hello.go . RUN go build hello.go FROM alpine COPY --from=0 /go/hello . CMD [\u0026#34;./hello\u0026#34;] 生成的镜像大小为 7.5M，对于一个只打印 『hello world』的程序来说确实有点大了，但我们可以换个角度：\n即使程序很复杂，生成的镜像也不会很大。 包含了很多有用的调试工具。 即使运行时缺少某些特殊的调试工具，也可以迅速安装。 Go 语言搞定了，C 语言呢？并没有 gcc:alpine 这样的镜像啊。只能以 Alpine 镜像作为基础镜像，自己安装 C 编译器了，Dockerfile 如下：\nFROM alpine RUN apk add build-base COPY hello.c . RUN gcc -o hello hello.c FROM alpine COPY --from=0 hello . CMD [\u0026#34;./hello\u0026#34;] 必须安装 build-base，如果安装 gcc，就只有编译器，没有标准库。build-base 相当于 Ubuntu 的 build-essentials，引入了编译器、标准库和 make 之类的工具。 最后来对比一下不同构建方法得到的 『hello world』镜像大小：\n使用基础镜像 golang 构建：805MB 多阶段构建，build 阶段使用基础镜像 golang，run 阶段使用基础镜像 ubuntu：66.2MB 多阶段构建，build 阶段使用基础镜像 golang:alpine，run 阶段使用基础镜像 alpine：7.6MB 多阶段构建，build 阶段使用基础镜像 golang，run 阶段使用基础镜像 scratch：2MB 最终镜像体积减少了 99.75%，相当惊人了。再来看一个更实际的例子，上一节提到的使用 net 的程序，最终的镜像大小对比：\n使用基础镜像 golang 构建：810MB 多阶段构建，build 阶段使用基础镜像 golang，run 阶段使用基础镜像 ubuntu：71.2MB 多阶段构建，build 阶段使用基础镜像 golang:alpine，run 阶段使用基础镜像 alpine：12.6MB 多阶段构建，build 阶段使用基础镜像 golang，run 阶段使用基础镜像 busybox:glibc：12.2MB 多阶段构建，build 阶段使用基础镜像 golang 并使用参数 CGO_ENABLED=0，run 阶段使用基础镜像 ubuntu：7MB 镜像体积仍然减少了 99%。\n3. Java 语言镜像精简 # Java 属于编译型语言，但运行时还是要跑在 JVM 中。那么对于 Java 语言来说，该如何使用多阶段构建呢？\n静态还是动态？ # 从概念上来看，Java 使用的是动态链接，因为 Java 代码需要调用 JVM 提供的 Java API，这些 API 的代码都在可执行文件之外，通常是 JAR 文件或 WAR 文件。\n然而这些 Java 库并不是完全独立于系统库的，某些 Java 函数最终还是会调用系统库，例如打开文件时需要调用 open(), fopen() 或它们的变体，因此 JVM 本身可能会与系统库动态链接。\n这就意味着理论上可以使用任意的 JVM 来运行 Java 程序，系统标准库是 musl libc 还是 glibc 都无所谓。因此，也就可以使用任意带有 JVM 的基础镜像来构建 Java 程序，也可以使用任意带有 JVM 的镜像作为运行 Java 程序的基础镜像。\n类文件格式 # Java 类文件（Java 编译器生成的字节码）的格式会随着版本而变化，且大部分变化都是 Java API 的变化。还有一部分更改与 Java 语言本身有关，例如 Java 5 中添加了泛型，这种变化就可能会导致类文件格式的变化，从而破坏与旧版本的兼容性。\n所以默认情况下，使用给定版本的 Java 编译器编译的类不能与更早版本的 JVM 兼容，但可以指定编译器的 -target （Java 8 及其以下版本）参数或者 --release （Java 9 及其以上版本）参数来使用较旧的类文件格式。--release 参数还可以指定类文件的路径，以确保程序运行在指定的 JVM 版本中（例如 Java 11），不会意外调用 Java 12 的 API。\nJDK vs JRE # 如果你对大多数平台上的 Java 打包方式很熟悉，那你应该知道 JDK 和 JRE。\nJRE 即 Java 运行时环境（Java Runtime Environment），包含了运行 Java 程序所需要的环境，即 JVM。\nJDK 即 Java 开发工具包（Java Development Kit），既包含了 JRE，也包含了开发 Java 程序所需的工具，即 Java 编译器。\n大多数 Java 镜像都提供了 JDK 和 JRE 两种标签，因此可以在多阶段构建的 build 阶段使用 JDK 作为基础镜像，run 阶段使用 JRE 作为基础镜像。\nJava vs OpenJDK # 推荐使用 openjdk，因为开源啊，更新勤快啊~~\n也可以使用 amazoncorretto，这是 Amazon fork OpenJDK 后打了补丁的版本，号称企业级。\n开始构建 # 说了那么多，到底该用哪个镜像呢？这里给出几个参考：\nopenjdk:8-jre-alpine（85MB） openjdk:11-jre（267MB）或者 openjdk:11-jre-slim（204MB） openjdk:14-alpine（338MB） 如果你想要更直观的数据，可以看我的例子，还是搬出屡试不爽的 『hello world』，只不过这次是 Java 版本：\nclass hello { public static void main(String [] args) { System.out.println(\u0026#34;Hello, world!\u0026#34;); } } 不同构建方法得到的镜像大小：\n使用基础镜像 java 构建：643MB 使用基础镜像 openjdk 构建：490MB 多阶段构建，build 阶段使用基础镜像 openjdk，run 阶段使用基础镜像 openjdk:jre：479MB 使用基础镜像 amazoncorretto 构建：390MB 多阶段构建，build 阶段使用基础镜像 openjdk:11，run 阶段使用基础镜像 openjdk:11-jre：267MB 多阶段构建，build 阶段使用基础镜像 openjdk:8，run 阶段使用基础镜像 openjdk:8-jre-alpine：85MB 所有的 Dockerfile 都可以在 这个仓库找到。\n4. 解释型语言镜像精简 # 对于诸如 Node、Python、Rust 之类的解释型语言来说，情况就比较复杂一点了。先来看看 Alpine 镜像。\nAlpine 镜像 # 对于解释型语言来说，如果程序仅用到了标准库或者依赖项和程序本身使用的是同一种语言，且无需调用 C 库和外部依赖，那么使用 Alpine 作为基础镜像一般是没有啥问题的。一旦你的程序需要调用外部依赖，情况就复杂了，想继续使用 Alpine 镜像，就得安装这些依赖。根据难度可以划分为三个等级：\n简单：依赖库有针对 Alpine 的安装说明，一般会说明需要安装哪些软件包以及如何建立依赖关系。但这种情况非常罕见，原因前面也提到了，Alpine 的软件包数量比大多数流行的发行版要少得多。 中等：依赖库没有针对 Alpine 的安装说明，但有针对别的发行版的安装说明。我们可以通过对比找到与别的发行版的软件包相匹配的 Alpine 软件包（假如有的话）。 困难：依赖库没有针对 Alpine 的安装说明，但有针对别的发行版的安装说明，但是 Alpine 也没有与之对应的软件包。这种情况就必须从源码开始构建！ 最后一种情况最不推荐使用 Alpine 作为基础镜像，不但不能减小体积，可能还会适得其反，因为你需要安装编译器、依赖库、头文件等等。。。更重要的是，构建时间会很长，效率低下。如果非要考虑多阶段构建，就更复杂了，你得搞清楚如何将所有的依赖编译成二进制文件，想想就头大。因此一般不推荐在解释型语言中使用多阶段构建。\n有一种特殊情况会同时遇到 Alpine 的绝大多数问题：将 Python 用于数据科学。numpy 和 pandas 之类的包都被预编译成了 wheel，wheel 是 Python 新的打包格式，被编译成了二进制，用于替代 Python 传统的 egg 文件，可以通过 pip 直接安装。但这些 wheel 都绑定了特定的 C 库，这就意味着在大多数使用 glibc 的镜像中都可以正常安装，但 Alpine 镜像就不行，原因你懂得，前面已经说过了。如果非要在 Alpine 中安装，你需要安装很多依赖，重头构建，耗时又费力，有一篇文章专门解释了这个问题： 使用 Alpine 构建 Pyhton 镜像会将构建速度拖慢 50 倍！。\n既然 Alpine 镜像这么坑，那么是不是只要是 Python 写的程序就不推荐使用 Alpine 镜像来构建呢？也不能完全这么肯定，至少 Python 用于数据科学时不推荐使用 Alpine，其他情况还是要具体情况具体分析，如果有可能，还是可以试一试 Alpine 的。\n:slim 镜像 # 如果实在不想折腾，可以选择一个折衷的镜像 xxx:slim。slim 镜像一般都基于 Debian 和 glibc，删除了许多非必需的软件包，优化了体积。如果构建过程中需要编译器，那么 slim 镜像不适合，除此之外大多数情况下还是可以使用 slim 作为基础镜像的。\n下面是主流的解释型语言的 Alpine 镜像和 slim 镜像大小对比：\nImage Size --------------------------- node 939 MB node:alpine 113 MB node:slim 163 MB python 932 MB python:alpine 110 MB python:slim 193 MB ruby 842 MB ruby:alpine 54 MB ruby:slim 149 MB 再来举个特殊情况的例子，同时安装 matplotlib，numpy 和 pandas，不同的基础镜像构建的镜像大小如下：\nImage and technique Size -------------------------------------- python 1.26 GB python:slim 407 MB python:alpine 523 MB python:alpine multi-stage 517 MB 可以看到这种情况下使用 Alpine 并没有任何帮助，即使使用多阶段构建也无济于事。\n但也不能全盘否定 Alpine，比如下面这种情况：包含大量依赖的 Django 应用。\nImage and technique Size -------------------------------------- python 1.23 GB python:alpine 636 MB python:alpine multi-stage 391 MB 最后来总结一下：到底使用哪个基础镜像并不能盖棺定论，有时使用 Alpine 效果更好，有时反而使用 slim 效果更好，如果你对镜像体积有着极致的追求，可以这两种镜像都尝试一下。相信随着时间的推移，我们就会积累足够的经验，知道哪种情况该用 Alpine，哪种情况该用 slim，不用再一个一个尝试。\n5. Rust 语言镜像精简 # Rust 是最初由 Mozilla 设计的现代编程语言，并且在 Web 和基础架构领域中越来越受欢迎。Rust 编译的二进制文件动态链接到 C 库，可以正常运行于 Ubuntu、Debian 和 Fedora 之类的镜像中，但不能运行于 busybox:glibc 中。因为 Rust 二进制需要调用 libdl 库，busybox:glibc 中不包含该库。\n还有一个 rust:alpine 镜像，Rust 编译的二进制也可以正常运行其中。\n如果考虑编译成静态链接，可以参考 Rust 官方文档。在 Linux 上需要构建一个特殊版本的 Rust 编译器，构建的依赖库就是 musl libc，你没有看错，就是 Alpine 中的那个 musl libc。如果你想获得更小的镜像，请按照文档中的说明进行操作，最后将生成的二进制文件扔进 scratch 镜像中就好了。\n6. 总结 # 本系列文章的前两部分介绍了优化 Docker 镜像体积的常用方法，以及如何针对不同类型的语言运用这些方法。最后一部分将会介绍如何在减少镜像体积的同时，还能减少 I/O 和内存使用量，同时还会介绍一些虽然与容器无关但对优化镜像有帮助的技术。\n","date":"2020年5月16日","externalUrl":null,"permalink":"/posts/docker-images-part2-details-specific-to-different-languages/","section":"博客","summary":"原文链接： Docker Images : Part II - Details Specific To Different Languages 本系列文章将分为三个部分： 第","title":"Docker 镜像制作教程：针对不同语言的精简策略","type":"posts"},{"content":"","date":"2020年5月16日","externalUrl":null,"permalink":"/series/docker-%E9%95%9C%E5%83%8F%E5%88%B6%E4%BD%9C%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"Docker 镜像制作系列","type":"series"},{"content":"在使用 Docker 和 Kubernetes 时，我们经常需要访问 gcr.io 和 quay.io 镜像仓库，由于众所周知的原因，这些镜像仓库在中国都无法访问，唯一能访问的是 Docker Hub，但速度也是奇慢无比。gcr.azk8s.cn 是 gcr.io 镜像仓库的代理站点，原来可以通过 gcr.azk8s.cn 访问 gcr.io 仓库里的镜像，但是目前 *.azk8s.cn 已经仅限于 Azure 中国的 IP 使用，不再对外提供服务了。国内其他的镜像加速方案大多都是采用定时同步的方式来缓存，这种方法是有一定延迟的，不能保证及时更新，ustc 和七牛云等镜像加速器我都试过了，非常不靠谱，很多镜像都没有。\n为了能够顺利访问 gcr.io 等镜像仓库，我们需要在墙外自己搭建一个类似于 gcr.azk8s.cn 的镜像仓库代理站点。利用 Docker 的开源项目 registry 就可以实现这个需求，registry 不仅可以作为本地私有镜像仓库，还可以作为上游镜像仓库的缓存，也就是 pull through cache。\n先来感受下速度：\n1. 前提条件 # 一台能够施展魔法的服务器（你懂得，可以直接访问 gcr.io） 一个域名和域名相关的 SSL 证书（docker pull 镜像时需要验证域名证书），一般用 Let\u0026rsquo;s Encrypt 就够了。 2. 核心思路 # registry 可以通过设置参数 remoteurl 将其作为远端仓库的缓存仓库，这样当你通过这个私有仓库的地址拉取镜像时，regiistry 会先将镜像缓存到本地存储，然后再提供给拉取的客户端（有可能这两个步骤是同时的，我也不太清楚）。我们可以先部署一个私有 registry，然后将 remoteurl 设为需要加速的镜像仓库地址，基本上就可以了。\n3. 定制 registry # 为了能够支持缓存 docker.io、gcr.io、k8s.gcr.io、quay.io 和 ghcr.io 等常见的公共镜像仓库，我们需要对 registry 的配置文件进行定制，Dockerfile 如下：\nFROM registry:2.6 LABEL maintainer=\u0026#34;registry-proxy Docker Maintainers https://icloudnative.io\u0026#34; ENV PROXY_REMOTE_URL=\u0026#34;\u0026#34; \\ DELETE_ENABLED=\u0026#34;\u0026#34; COPY entrypoint.sh /entrypoint.sh 其中 entrypoint.sh 用来将环境变量传入配置文件：\nentrypoint.sh #!/bin/sh set -e CONFIG_YML=/etc/docker/registry/config.yml if [ -n \u0026#34;$PROXY_REMOTE_URL\u0026#34; -a `grep -c \u0026#34;$PROXY_REMOTE_URL\u0026#34; $CONFIG_YML` -eq 0 ]; then echo \u0026#34;proxy:\u0026#34; \u0026gt;\u0026gt; $CONFIG_YML echo \u0026#34; remoteurl: $PROXY_REMOTE_URL\u0026#34; \u0026gt;\u0026gt; $CONFIG_YML echo \u0026#34; username: $PROXY_USERNAME\u0026#34; \u0026gt;\u0026gt; $CONFIG_YML echo \u0026#34; password: $PROXY_PASSWORD\u0026#34; \u0026gt;\u0026gt; $CONFIG_YML echo \u0026#34;------ Enabled proxy to remote: $PROXY_REMOTE_URL ------\u0026#34; elif [ $DELETE_ENABLED = true -a `grep -c \u0026#34;delete:\u0026#34; $CONFIG_YML` -eq 0 ]; then sed -i \u0026#39;/rootdirectory/a\\ delete:\u0026#39; $CONFIG_YML sed -i \u0026#39;/delete/a\\ enabled: true\u0026#39; $CONFIG_YML echo \u0026#34;------ Enabled local storage delete -----\u0026#34; fi sed -i \u0026#34;/headers/a\\ Access-Control-Allow-Origin: [\u0026#39;*\u0026#39;]\u0026#34; $CONFIG_YML sed -i \u0026#34;/headers/a\\ Access-Control-Allow-Methods: [\u0026#39;HEAD\u0026#39;, \u0026#39;GET\u0026#39;, \u0026#39;OPTIONS\u0026#39;, \u0026#39;DELETE\u0026#39;]\u0026#34; $CONFIG_YML sed -i \u0026#34;/headers/a\\ Access-Control-Expose-Headers: [\u0026#39;Docker-Content-Digest\u0026#39;]\u0026#34; $CONFIG_YML case \u0026#34;$1\u0026#34; in *.yaml|*.yml) set -- registry serve \u0026#34;$@\u0026#34; ;; serve|garbage-collect|help|-*) set -- registry \u0026#34;$@\u0026#34; ;; esac exec \u0026#34;$@\u0026#34; 4. 启动缓存服务 # 构建好 Docker 镜像之后，就可以启动服务了。如果你不想自己构建，可以直接用我的镜像：yangchuansheng/registry-proxy。\n一般来说，即使你要同时缓存 docker.io、gcr.io、k8s.gcr.io、quay.io 和 ghcr.io，一台 1C 2G 的云主机也足够了（前提是你不在上面跑其他的服务）。我的博客、评论服务和其他一堆乱七八糟的服务都要跑在云主机上，所以一台是不满足我的需求的，我直接买了两台腾讯云香港轻量级服务器。\n既然买了两台，肯定得 组个 k3s 集群啦，看主机名就知道我是用来干啥的。其中 2C 4G 作为 master 节点，1C 2G 作为 node 节点。\n以 docker.io 为例，创建资源清单：\ndockerhub.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dockerhub labels: app: dockerhub spec: replicas: 1 selector: matchLabels: app: dockerhub template: metadata: labels: app: dockerhub spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - dockerhub topologyKey: kubernetes.io/hostname weight: 1 dnsPolicy: None dnsConfig: nameservers: - 8.8.8.8 - 8.8.4.4 containers: - name: dockerhub image: yangchuansheng/registry-proxy:latest env: - name: PROXY_REMOTE_URL value: https://registry-1.docker.io - name: PROXY_USERNAME value: yangchuansheng - name: PROXY_PASSWORD value: ******** ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry --- apiVersion: v1 kind: Service metadata: name: dockerhub labels: app: dockerhub spec: selector: app: dockerhub ports: - protocol: TCP name: http port: 5000 targetPort: 5000 使用资源清单创建对应的服务：\n🐳 → kubectl apply -f dockerhub.yaml 如果你只有一台主机，可以使用 docker-compose 来编排容器，配置文件可以自己参考 k8s 的配置修改，本文就不赘述了。\n5. 代理选择 # 如果只缓存 docker.io，可以直接将 registry-proxy 的端口改成 443，并添加 SSL 证书配置。如果要缓存多个公共镜像仓库，就不太推荐这么做了，因为 443 端口只有一个，多个 registry-proxy 服务不能共用一个端口，合理的做法是使用边缘代理服务根据域名来转发请求到不同的 registry-proxy 服务。\n对于 Kubernetes 集群来说，Ingress Controller 即边缘代理，常见的 Ingress Controller 基本上都是由 Nginx 或者 Envoy 来实现。 Envoy 虽为代理界新秀，但生而逢时，它的很多特性都是原生为云准备的，是真正意义上的 Cloud Native L7 代理和通信总线。比如它的服务发现和动态配置功能，与 Nginx 等代理的热加载不同， Envoy 可以通过 API 来实现其控制平面，控制平面可以集中服务发现，并通过 API 接口动态更新数据平面的配置，不需要重启数据平面的代理。不仅如此，控制平面还可以通过 API 将配置进行分层，然后逐层更新。\n目前使用 Envoy 实现的 Ingress Controller 有 Contour、 Ambassador 和 Gloo 等，如果你对 Envoy 比较感兴趣，并且想使用 Ingress Controller 作为边缘代理，可以试试 Contour。Ingress Controller 对底层做了抽象，屏蔽了很多细节，无法顾及到所有细节的配置，必然不会支持底层代理所有的配置项，所以我选择使用原生的 Envoy 来作为边缘代理。如果你是单机跑的 registry-proxy 服务，也可以试试 Envoy。\n6. 代理配置 # 首先创建 Envoy 的资源清单：\nenvoy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: envoy namespace: kube-system labels: app: envoy spec: replicas: 2 selector: matchLabels: app: envoy strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: envoy spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet containers: - name: envoy image: envoyproxy/envoy:v1.17-latest imagePullPolicy: IfNotPresent command: - envoy - /etc/envoy/envoy.yaml ports: - containerPort: 443 name: https - containerPort: 80 name: http - containerPort: 15001 name: http-metrics volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /etc/envoy name: envoy - mountPath: /root/.acme.sh/icloudnative.io name: ssl volumes: - name: localtime hostPath: path: /etc/localtime - name: ssl hostPath: path: /root/.acme.sh/icloudnative.io - name: envoy hostPath: path: /etc/envoy 使用资源清单创建对应的服务：\n🐳 → kubectl apply -f envoy.yaml 这里选择使用 hostPath 将 envoy 的配置挂载到容器中，然后 通过文件来动态更新配置。来看下 Envoy 的配置，先进入 /etc/envoy 目录。\nbootstrap 配置：\nenvoy.yaml node: id: node0 cluster: cluster0 dynamic_resources: lds_config: path: /etc/envoy/lds.yaml cds_config: path: /etc/envoy/cds.yaml admin: access_log_path: \u0026#34;/dev/stdout\u0026#34; address: socket_address: address: \u0026#34;0.0.0.0\u0026#34; port_value: 15001 LDS 的配置：\nlds.yaml version_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.listener.v3.Listener name: listener_http address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO access_log: name: envoy.access_loggers.file typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout route_config: name: http_route virtual_hosts: - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: https_redirect: true port_redirect: 443 response_code: \u0026#34;FOUND\u0026#34; http_filters: - name: envoy.filters.http.router - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.listener.v3.Listener name: listener_https address: socket_address: address: 0.0.0.0 port_value: 443 listener_filters: - name: \u0026#34;envoy.filters.listener.tls_inspector\u0026#34; typed_config: {} filter_chains: - transport_socket: name: envoy.transport_sockets.tls typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext common_tls_context: alpn_protocols: h2,http/1.1 tls_certificates: - certificate_chain: filename: \u0026#34;/root/.acme.sh/icloudnative.io/fullchain.cer\u0026#34; private_key: filename: \u0026#34;/root/.acme.sh/icloudnative.io/icloudnative.io.key\u0026#34; filters: - name: envoy.filters.network.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_https codec_type: AUTO use_remote_address: true access_log: name: envoy.access_loggers.file typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout route_config: name: https_route response_headers_to_add: - header: key: Strict-Transport-Security value: \u0026#34;max-age=15552000; includeSubdomains; preload\u0026#34; virtual_hosts: - name: docker domains: - docker.icloudnative.io routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: dockerhub timeout: 600s http_filters: - name: envoy.filters.http.router typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.http.router.v3.Router CDS 的配置：\ncds.yaml version_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.cluster.v3.Cluster name: dockerhub connect_timeout: 15s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: dockerhub endpoints: - lb_endpoints: - endpoint: address: socket_address: address: dockerhub.default port_value: 5000 这里的 address 使用的是 Kubernetes 集群内部域名，其他部署方式请自己斟酌。\n配置好了 Envoy 之后，就可以通过代理服务器拉取 docker.io 的镜像了。\n7. 验证加速效果 # 现在你就可以通过代理服务器来拉取公共镜像了。比如你想拉取 nginx:alpine 镜像，可以使用下面的命令：\n🐳 → docker pull docker.icloudnative.io/library/nginx:alpine alpine: Pulling from library/nginx 801bfaa63ef2: Pull complete b1242e25d284: Pull complete 7453d3e6b909: Pull complete 07ce7418c4f8: Pull complete e295e0624aa3: Pull complete Digest: sha256:c2ce58e024275728b00a554ac25628af25c54782865b3487b11c21cafb7fabda Status: Downloaded newer image for docker.icloudnative.io/library/nginx:alpine docker.icloudnative.io/library/nginx:alpine 8. 缓存所有镜像仓库 # 前面的示例只是缓存了 docker.io，如果要缓存所有的公共镜像仓库，可以参考 4-6 节的内容。以 k8s.gcr.io 为例，先准备一个资源清单：\ngcr-k8s.yaml apiVersion: apps/v1 kind: Deployment metadata: name: gcr-k8s labels: app: gcr-k8s spec: replicas: 1 selector: matchLabels: app: gcr-k8s template: metadata: labels: app: gcr-k8s spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - gcr-k8s topologyKey: kubernetes.io/hostname weight: 1 dnsPolicy: None dnsConfig: nameservers: - 8.8.8.8 - 8.8.4.4 containers: - name: gcr-k8s image: yangchuansheng/registry-proxy:latest env: - name: PROXY_REMOTE_URL value: https://k8s.gcr.io ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry --- apiVersion: v1 kind: Service metadata: name: gcr-k8s labels: app: gcr-k8s spec: selector: app: gcr-k8s ports: - protocol: TCP name: http port: 5000 targetPort: 5000 将其部署到 Kubernetes 集群中：\n🐳 → kubectl apply -f gcr-k8s.yaml 在 lds.yaml 中添加相关配置：\nvirtual_hosts: - name: docker ... ... - name: k8s domains: - k8s.icloudnative.io routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: gcr-k8s timeout: 600s 在 cds.yaml 中添加相关配置：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.cluster.v3.Cluster name: gcr-k8s connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: gcr-k8s endpoints: - lb_endpoints: - endpoint: address: socket_address: address: gcr-k8s.default port_value: 5000 其他镜像仓库可照搬上述步骤，以下是我自己跑的所有缓存服务容器：\n🐳 → kubectl get pod -o wide gcr-8647ffb586-67c6g 1/1 Running 0 21h 10.42.1.52 blog-k3s02 ghcr-7765f6788b-hxxvc 1/1 Running 0 21h 10.42.1.55 blog-k3s01 dockerhub-94bbb7497-x4zwg 1/1 Running 0 21h 10.42.1.54 blog-k3s02 gcr-k8s-644db84879-7xssb 1/1 Running 0 21h 10.42.1.53 blog-k3s01 quay-559b65848b-ljclb 1/1 Running 0 21h 10.42.0.154 blog-k3s01 9. 容器运行时配置 # 配置好所有的缓存服务后，就可以通过代理来拉取公共镜像了，只需按照下面的列表替换镜像地址中的字段就行了：\n原 URL 替换后的 URL docker.io/xxx/xxx 或 xxx/xxx docker.icloudnative.io/xxx/xxx docker.io/library/xxx 或 xxx docker.icloudnative.io/library/xxx gcr.io/xxx/xxx gcr.icloudnative.io/xxx/xxx k8s.gcr.io/xxx/xxx k8s.icloudnative.io/xxx/xxx quay.io/xxx/xxx quay.icloudnative.io/xxx/xxx ghcr.io/xxx/xxx ghcr.icloudnative.io/xxx/xxx 当然，最好的方式还是直接配置 registry mirror，Docker 只支持配置 docker.io 的 registry mirror，Containerd 和 Podman 支持配置所有镜像仓库的 registry mirror。\nDocker # Docker 可以修改配置文件 /etc/docker/daemon.json，添加下面的内容：\n{ \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://docker.icloudnative.io\u0026#34; ] } 然后重启 Docker 服务，就可以直接拉取 docker.io 的镜像了，不需要显示指定代理服务器的地址，Docker 服务本身会自动通过代理服务器去拉取镜像。比如：\n🐳 → docker pull nginx:alpine 🐳 → docker pull docker.io/library/nginx:alpine Containerd # Containerd 就比较简单了，它支持任意 registry 的 mirror，只需要修改配置文件 /etc/containerd/config.toml，添加如下的配置：\n[plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;docker.io\u0026#34;] endpoint = [\u0026#34;https://docker.icloudnative.io\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;k8s.gcr.io\u0026#34;] endpoint = [\u0026#34;https://k8s.icloudnative.io\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;gcr.io\u0026#34;] endpoint = [\u0026#34;https://gcr.icloudnative.io\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;ghcr.io\u0026#34;] endpoint = [\u0026#34;https://ghcr.icloudnative.io\u0026#34;] [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.registry.mirrors.\u0026#34;quay.io\u0026#34;] endpoint = [\u0026#34;https://quay.icloudnative.io\u0026#34;] 重启 Containerd 服务后，就可以直接拉取所有镜像了，不需要修改任何前缀，Containerd 会根据配置自动选择相应的代理 URL 拉取镜像。\nPodman # Podman 也支持任意 registry 的 mirror，只需要修改配置文件 /etc/containers/registries.conf，添加如下的配置：\nunqualified-search-registries = [\u0026#39;docker.io\u0026#39;, \u0026#39;k8s.gcr.io\u0026#39;, \u0026#39;gcr.io\u0026#39;, \u0026#39;ghcr.io\u0026#39;, \u0026#39;quay.io\u0026#39;] [[registry]] prefix = \u0026#34;docker.io\u0026#34; insecure = true location = \u0026#34;registry-1.docker.io\u0026#34; [[registry.mirror]] location = \u0026#34;docker.icloudnative.io\u0026#34; [[registry]] prefix = \u0026#34;k8s.gcr.io\u0026#34; insecure = true location = \u0026#34;k8s.gcr.io\u0026#34; [[registry.mirror]] location = \u0026#34;k8s.icloudnative.io\u0026#34; [[registry]] prefix = \u0026#34;gcr.io\u0026#34; insecure = true location = \u0026#34;gcr.io\u0026#34; [[registry.mirror]] location = \u0026#34;gcr.icloudnative.io\u0026#34; [[registry]] prefix = \u0026#34;ghcr.io\u0026#34; insecure = true location = \u0026#34;ghcr.io\u0026#34; [[registry.mirror]] location = \u0026#34;ghcr.icloudnative.io\u0026#34; [[registry]] prefix = \u0026#34;quay.io\u0026#34; insecure = true location = \u0026#34;quay.io\u0026#34; [[registry.mirror]] location = \u0026#34;quay.icloudnative.io\u0026#34; 然后就可以直接拉取所有镜像了，不需要修改任何前缀，Podman 会根据配置自动选择相应的代理 URL 拉取镜像。而且 Podman 还有 fallback 机制，上面的配置表示先尝试通过 registry.mirror 中 location 字段的 URL 来拉取镜像，如果失败就会尝试通过 registry 中 location 字段的 URL 来拉取。\n10. 清理缓存 # 缓存服务会将拉取的镜像缓存到本地，所以需要消耗磁盘容量。一般云主机的磁盘容量都不是很大，OSS 和 s3 存储都比较贵，不太划算。\n为了解决这个问题，我推荐定期删除缓存到本地磁盘的部分镜像，或者删除所有镜像。方法也比较简单，单独再部署一个 registry，共用其他 registry 的存储，并启用 delete 功能，然后再通过 API 或者 Dashboard 进行删除。\n先准备一个资源清单：\nreg-local.yaml apiVersion: apps/v1 kind: Deployment metadata: name: reg-local labels: app: reg-local spec: replicas: 1 selector: matchLabels: app: reg-local template: metadata: labels: app: reg-local spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - reg-local topologyKey: kubernetes.io/hostname weight: 1 containers: - name: reg-local image: yangchuansheng/registry-proxy:latest env: - name: DELETE_ENABLED value: \u0026#34;true\u0026#34; ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry --- apiVersion: v1 kind: Service metadata: name: reg-local labels: app: reg-local spec: selector: app: reg-local ports: - protocol: TCP name: http port: 5000 targetPort: 5000 将其部署到 Kubernetes 集群中：\n🐳 → kubectl apply -f reg-local.yaml 再准备一个 Docker Registry UI 的资源清单：\nregistry-ui.yaml apiVersion: apps/v1 kind: Deployment metadata: name: registry-ui labels: app: registry-ui spec: replicas: 1 selector: matchLabels: app: registry-ui template: metadata: labels: app: registry-ui spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - registry-ui topologyKey: kubernetes.io/hostname weight: 1 tolerations: - key: node-role.kubernetes.io/ingress operator: Exists effect: NoSchedule containers: - name: registry-ui image: joxit/docker-registry-ui:static env: - name: REGISTRY_TITLE value: My Private Docker Registry - name: REGISTRY_URL value: \u0026#34;http://reg-local:5000\u0026#34; - name: DELETE_IMAGES value: \u0026#34;true\u0026#34; ports: - containerPort: 80 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime volumes: - name: localtime hostPath: path: /etc/localtime --- apiVersion: v1 kind: Service metadata: name: registry-ui labels: app: registry-ui spec: selector: app: registry-ui ports: - protocol: TCP name: http port: 80 targetPort: 80 将其部署到 Kubernetes 集群中：\n🐳 → kubectl apply -f registry-ui.yaml 这样就可以通过 Dashboard 来清理镜像释放空间了。\n或者直接简单粗暴，定时删除整个存储目录的内容。例如，执行命令 crontab -e，添加如下内容：\n* * */2 * * /usr/bin/rm -rf /var/lib/registry/* \u0026amp;\u0026gt;/dev/null 表示每过两天清理一次 /var/lib/registry/ 目录。\n11. 防白嫖认证 # 最后还有一个问题，我把缓存服务的域名全部公开了，如果大家都来白嫖，我的云主机肯定承受不住。为了防止白嫖，我得给 registry-proxy 加个认证，最简单的方法就是使用 basic auth，用 htpasswd 来存储密码。\n为用户 admin 创建一个密码文件，密码为 admin：\n🐳 → docker run \\ --entrypoint htpasswd \\ registry:2.6 -Bbn admin admin \u0026gt; htpasswd 创建 Secret：\n🐳 → kubectl create secret generic registry-auth --from-file=htpasswd 修改资源清单的配置，以 docker.io 为例：\ndockerhub.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dockerhub labels: app: dockerhub spec: replicas: 1 selector: matchLabels: app: dockerhub template: metadata: labels: app: dockerhub spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - dockerhub topologyKey: kubernetes.io/hostname weight: 1 dnsPolicy: None dnsConfig: nameservers: - 8.8.8.8 - 8.8.4.4 containers: - name: dockerhub image: yangchuansheng/registry-proxy:latest env: - name: PROXY_REMOTE_URL value: https://registry-1.docker.io - name: PROXY_USERNAME value: yangchuansheng - name: PROXY_PASSWORD value: ******** + - name: REGISTRY_AUTH_HTPASSWD_REALM + value: Registry Realm + - name: REGISTRY_AUTH_HTPASSWD_PATH + value: /auth/htpasswd ports: - containerPort: 5000 protocol: TCP volumeMounts: - mountPath: /etc/localtime name: localtime - mountPath: /var/lib/registry name: registry + - mountPath: /auth + name: auth volumes: - name: localtime hostPath: path: /etc/localtime - name: registry hostPath: path: /var/lib/registry + - name: auth + secret: + secretName: registry-auth apply 使其生效：\n🐳 → kubectl apply -f dockerhub.yaml 尝试拉取镜像：\n🐳 → docker pull docker.icloudnative.io/library/nginx:latest Error response from daemon: Get https://docker.icloudnative.io/v2/library/nginx/manifests/latest: no basic auth credentials 登录镜像仓库：\n🐳 → docker login docker.icloudnative.io Username: admin Password: WARNING! Your password will be stored unencrypted in /root/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded 现在就可以正常拉取镜像了。\n如果你想更细粒度地控制权限，可以使用 Token 的方式来进行认证，具体可以参考 docker_auth 这个项目。\n12. 费用评估 # 好了，现在我们来评估一下这一切的费用。首先你得有一个会魔法的服务器，国内的肯定不用考虑了，必须选择国外的，而且到国内的速度还过得去的，最低最低不会低于 30 人民币/月 吧。除此之外，你还得拥有一个个人域名，这个价格不好说，总而言之，加起来肯定不会低于 30 吧，多数人肯定是下不去这个手的。没关系，我有一个更便宜的方案，我已经部署好了一切，你可以直接用我的服务，当然我也是自己买的服务器，每个月也是要花钱的，如果你真的想用，只需要每月支付 3 元，以此来保障我每个月的服务器费用。当然肯定不止你一个人，目前大概有十几个用户，后面如果人数特别多，再考虑加服务器。这个需要你自己考虑清楚，有意者扫描下方的二维码向我咨询：\n","date":"2020年5月11日","externalUrl":null,"permalink":"/posts/docker-registry-proxy/","section":"博客","summary":"在使用 Docker 和 Kubernetes 时，我们经常需要访问 gcr.io 和 quay.io 镜像仓库，由于众所周知","title":"Docker 镜像加速教程","type":"posts"},{"content":"","date":"2020年5月11日","externalUrl":null,"permalink":"/tags/envoy/","section":"标签","summary":"","title":"Envoy","type":"tags"},{"content":"Envoy Proxy 在大多数情况下都是作为 Sidecar 与应用部署在同一网络环境中，每个应用只需要与 Envoy（localhost）交互，不需要知道其他服务的地址。然而这并不是 Envoy 仅有的使用场景，它本身就是一个七层代理，通过模块化结构实现了流量治理、信息监控等核心功能，比如流量治理功能就包括自动重连、熔断、全局限速、流量镜像和异常检测等多种高级功能，因此 Envoy 也常常被用于边缘代理，比如 Istio 的 Ingress Gateway、基于 Envoy 实现的 Ingress Controller（ Contour、 Ambassador、 Gloo 等）。\n我的博客也是部署在轻量级 Kubernetes 集群上的（其实是 k3s 啦），一开始使用 Contour 作为 Ingress Controller，暴露集群内的博客、评论等服务。但好景不长，由于我在集群内部署了各种奇奇怪怪的东西，有些个性化配置 Contour 无法满足我的需求，毕竟大家都知道，每抽象一层就会丢失很多细节。换一个 Controller 保不齐以后还会遇到这种问题，索性就直接裸用 Envoy 作为边缘代理，大不了手撸 YAML 呗。\n当然也不全是手撸，虽然没有所谓的控制平面，但仪式感还是要有的，我可以基于文件来动态更新配置啊，具体的方法参考 Envoy 基础教程：基于文件系统动态更新配置。\n1. UDS 介绍 # 说了那么多废话，下面进入正题。为了提高博客的性能，我选择将博客与 Envoy 部署在同一个节点上，并且全部使用 HostNetwork 模式，Envoy 通过 localhost 与博客所在的 Pod（Nginx） 通信。为了进一步提高性能，我盯上了 Unix Domain Socket（UDS，Unix域套接字），它还有另一个名字叫 IPC（inter-process communication，进程间通信）。为了理解 UDS，我们先来建立一个简单的模型。\n现实世界中两个人进行信息交流的整个过程被称作一次通信（Communication），通信的双方被称为端点（Endpoint）。工具通讯环境的不同，端点之间可以选择不同的工具进行通信，距离近可以直接对话，距离远可以选择打电话、微信聊天。这些工具就被称为 Socket。\n同理，在计算机中也有类似的概念：\n在 Unix 中，一次通信由两个端点组成，例如 HTTP 服务端和 HTTP 客户端。 端点之间想要通信，必须借助某些工具，Unix 中端点之间使用 Socket 来进行通信。 Socket 原本是为网络通信而设计的，但后来在 Socket 的框架上发展出一种 IPC 机制，就是 UDS。使用 UDS 的好处显而易见：不需要经过网络协议栈，不需要打包拆包、计算校验和、维护序号和应答等，只是将应用层数据从一个进程拷贝到另一个进程。这是因为，IPC 机制本质上是可靠的通讯，而网络协议是为不可靠的通讯设计的。\nUDS 与网络 Socket 最明显的区别在于，网络 Socket 地址是 IP 地址加端口号，而 UDS 的地址是一个 Socket 类型的文件在文件系统中的路径，一般名字以 .sock 结尾。这个 Socket 文件可以被系统进程引用，两个进程可以同时打开一个 UDS 进行通信，而且这种通信方式只会发生在系统内核里，不会在网络上进行传播。下面就来看看如何让 Envoy 通过 UDS 与上游集群 Nginx 进行通信吧，它们之间的通信模型大概就是这个样子：\n2. Nginx 监听 UDS # 首先需要修改 Nginx 的配置，让其监听在 UDS 上，至于 Socket 描述符文件的存储位置，就随你的意了。具体需要修改 listen 参数为下面的形式：\nlisten unix:/sock/hugo.sock; 当然，如果想获得更快的通信速度，可以放在 /dev/shm 目录下，这个目录是所谓的 tmpfs，它是 RAM 可以直接使用的区域，所以读写速度都会很快，下文会单独说明。\n3. Envoy\u0026ndash;\u0026gt;UDS\u0026ndash;\u0026gt;Nginx # Envoy 默认情况下是使用 IP 地址和端口号和上游集群通信的，如果想使用 UDS 与上游集群通信，首先需要修改服务发现的类型，将 type 修改为 static：\ntype: static 同时还需将端点定义为 UDS：\n- endpoint: address: pipe: path: \u0026#34;/sock/hugo.sock\u0026#34; 最终的 Cluster 配置如下：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: hugo connect_timeout: 15s type: static load_assignment: cluster_name: hugo endpoints: - lb_endpoints: - endpoint: address: pipe: path: \u0026#34;/sock/hugo.sock\u0026#34; 最后要让 Envoy 能够访问 Nginx 的 Socket 文件，Kubernetes 中可以将同一个 emptyDir 挂载到两个 Container 中来达到共享的目的，当然最大的前提是 Pod 中的 Container 是共享 IPC 的。配置如下：\nspec: ... template: ... spec: containers: - name: envoy ... volumeMounts: - mountPath: /sock name: hugo-socket ... - name: hugo ... volumeMounts: - mountPath: /sock name: hugo-socket ... volumes: ... - name: hugo-socket emptyDir: {} 现在你又可以愉快地访问我的 博客了，查看 Envoy 的日志，成功将请求通过 Socket 转发给了上游集群：\n[2020-04-27T02:49:47.943Z] \u0026#34;GET /posts/prometheus-histograms/ HTTP/1.1\u0026#34; 200 - 0 169949 1 0 \u0026#34;66.249.64.209,45.145.38.4\u0026#34; \u0026#34;Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\u0026#34; \u0026#34;9d490b2d-7c18-4dc7-b815-97f11bfc04d5\u0026#34; \u0026#34;icloudnative.io\u0026#34; \u0026#34;/dev/shm/hugo.sock\u0026#34; 嘿嘿，Google 的爬虫也来凑热闹。\n你可能会问我：你这里的 Socket 为什么在 /dev/shm/ 目录下啊？别急，还没结束呢，先来补充一个背景知识。\n4. Linux 共享内存机制 # 共享内存（shared memory），是 Linux 上一种用于进程间通信（IPC）的机制。\n进程间通信可以使用管道，Socket，信号，信号量，消息队列等方式，但这些方式通常需要在用户态、内核态之间拷贝，一般认为会有 4 次拷贝；相比之下，共享内存将内存直接映射到用户态空间，即多个进程访问同一块内存，理论上性能更高。嘿嘿，又可以改进上面的方案了。\n共享内存有两种机制：\nPOSIX 共享内存（shm_open()、shm_unlink()） System V 共享内存（shmget()、shmat()、shmdt()） 其中，System V 共享内存历史悠久，一般的 UNIX 系统上都有这套机制；而 POSIX 共享内存机制接口更加方便易用，一般是结合内存映射 mmap 使用。\nmmap 和 System V 共享内存的主要区别在于：\nSystem V shm 是持久化的，除非被一个进程明确的删除，否则它始终存在于内存里，直到系统关机。 mmap 映射的内存不是持久化的，如果进程关闭，映射随即失效，除非事先已经映射到了一个文件上。 /dev/shm 是 Linux 下 sysv 共享内存的默认挂载点。 POSIX 共享内存是基于 tmpfs 来实现的。实际上，更进一步，不仅 PSM(POSIX shared memory)，而且 SSM(System V shared memory) 在内核也是基于 tmpfs 实现的。\n从这里可以看到 tmpfs 主要有两个作用：\n用于 System V 共享内存，还有匿名内存映射；这部分由内核管理，用户不可见。 用于 POSIX 共享内存，由用户负责 mount，而且一般 mount 到 /dev/shm，依赖于 CONFIG_TMPFS。 虽然 System V 与 POSIX 共享内存都是通过 tmpfs 实现，但是受的限制却不相同。也就是说 /proc/sys/kernel/shmmax 只会影响 System V 共享内存，/dev/shm 只会影响 POSIX 共享内存。实际上，System V 与 POSIX 共享内存本来就是使用的两个不同的 tmpfs 实例。\nSystem V 共享内存能够使用的内存空间只受 /proc/sys/kernel/shmmax 限制；而用户通过挂载的 /dev/shm，默认为物理内存的 1/2。\n概括一下：\nPOSIX 共享内存与 System V 共享内存在内核都是通过 tmpfs 实现，但对应两个不同的 tmpfs 实例，相互独立。 通过 /proc/sys/kernel/shmmax 可以限制 System V 共享内存的最大值，通过 /dev/shm 可以限制 POSIX 共享内存的最大值。 5. Kubernetes 共享内存 # Kubernetes 创建的 Pod，其共享内存默认 64MB，且不可更改。\n为什么是这个值呢？其实，Kubernetes 本身是没有设置共享内存的大小的，64MB 其实是 Docker 默认的共享内存的大小。\nDocker run 的时候，可以通过 --shm-size 来设置共享内存的大小：\n🐳 → docker run --rm centos:7 df -h |grep shm shm 64M 0 64M 0% /dev/shm 🐳 → docker run --rm --shm-size 128M centos:7 df -h |grep shm shm 128M 0 128M 0% /dev/shm 然而，Kubernetes 并没有提供设置 shm 大小的途径。在这个 issue 里社区讨论了很久是否要给 shm 增加一个参数，但是最终并没有形成结论，只是有一个 workgroud 的办法：将 Memory 类型的 emptyDir 挂载到 /dev/shm 来解决。\nKubernetes 提供了一种特殊的 emptyDir：可以将 emptyDir.medium 字段设置为 \u0026quot;Memory\u0026quot;，以告诉 Kubernetes 使用 tmpfs（基于 RAM 的文件系统）作为介质。用户可以将 Memory 介质的 emptyDir 挂到任何目录，然后将这个目录当作一个高性能的文件系统来使用，当然也可以挂载到 /dev/shm，这样就可以解决共享内存不够用的问题了。\n使用 emptyDir 虽然可以解决问题，但也是有缺点的：\n不能及时禁止用户使用内存。虽然过 1~2 分钟 Kubelet 会将 Pod 挤出，但是这个时间内，其实对 Node 还是有风险的。 影响 Kubernetes 调度，因为 emptyDir 并不涉及 Node 的 Resources，这样会造成 Pod “偷偷”使用了 Node 的内存，但是调度器并不知晓。 用户不能及时感知到内存不可用。 由于共享内存也会受 Cgroup 限制，我们只需要给 Pod 设置 Memory limits 就可以了。如果将 Pod 的 Memory limits 设置为共享内存的大小，就会遇到一个问题：当共享内存被耗尽时，任何命令都无法执行，只能等超时后被 Kubelet 驱逐。\n这个问题也很好解决，将共享内存的大小设置为 Memory limits 的 50% 就好。综合以上分析，最终设计如下：\n将 Memory 介质的 emptyDir 挂载到 /dev/shm/。 配置 Pod 的 Memory limits。 配置 emptyDir 的 sizeLimit 为 Memory limits 的 50%。 6. 最终配置 # 根据上面的设计，最终的配置如下。\nNginx 的配置改为：\nlisten unix:/dev/shm/hugo.sock; Envoy 的配置改为：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: hugo connect_timeout: 15s type: static load_assignment: cluster_name: hugo endpoints: - lb_endpoints: - endpoint: address: pipe: path: \u0026#34;/dev/shm/hugo.sock\u0026#34; Kubernetes 的 manifest 改为：\nspec: ... template: ... spec: containers: - name: envoy resources: limits: memory: 256Mi ... volumeMounts: - mountPath: /dev/shm name: hugo-socket ... - name: hugo resources: limits: memory: 256Mi ... volumeMounts: - mountPath: /dev/shm name: hugo-socket ... volumes: ... - name: hugo-socket emptyDir: medium: Memory sizeLimit: 128Mi 7. 参考资料 # 设置kubernetes Pod的shared memory Kubernetes中Pod间共享内存方案 ","date":"2020年4月23日","externalUrl":null,"permalink":"/posts/envoy-cluster-socket/","section":"博客","summary":"Envoy Proxy 在大多数情况下都是作为 Sidecar 与应用部署在同一网络环境中，每个","title":"Envoy 基础教程：使用 Unix Domain Socket（UDS） 与上游集群通信","type":"posts"},{"content":"","date":"2020年4月23日","externalUrl":null,"permalink":"/categories/service-mesh/","section":"分类","summary":"","title":"服务网格","type":"categories"},{"content":"","date":"2020年4月18日","externalUrl":null,"permalink":"/tags/vxlan/","section":"标签","summary":"","title":"Vxlan","type":"tags"},{"content":" 上篇文章结尾提到 Linux 是支持 VXLAN 的，我们可以使用 Linux 搭建基于 VXLAN 的 overlay 网络，以此来加深对 VXLAN 的理解，毕竟光说不练假把式。\n1. 点对点的 VXLAN # 先来看看最简单的点对点 VXLAN 网络，点对点 VXLAN 即两台主机构建的 VXLAN 网络，每台主机上有一个 VTEP，VTEP 之间通过它们的 IP 地址进行通信。点对点 VXLAN 网络拓扑图如图所示：\n为了不影响主机的网络环境，我们可以使用 Linux VRF 来隔离 root network namespace 的路由。VRF（Virtual Routing and Forwarding）是由路由表和一组网络设备组成的路由实例，你可以理解为轻量级的 network namespace，只虚拟了三层的网络协议栈，而 network namespace 虚拟了整个网络协议栈。详情参看 Linux VRF(Virtual Routing Forwarding)的原理和实现。\nLinux Kernel 版本大于 4.3 才支持 VRF，建议做本文实验的同学先升级内核。 当然了，如果你有专门用来做实验的干净主机，可以不用 VRF 来隔离。\n下面结合 VRF 来创建一个点对点 VXLAN 网络。\n首先在 192.168.57.50 上创建 VXLAN 接口：\n$ ip link add vxlan0 type vxlan \\ id 42 \\ dstport 4789 \\ remote 192.168.57.54 \\ local 192.168.57.50 \\ dev eth0 重要参数解释：\nid 42 : 指定 VNI 的值，有效值在 1 到 $2^{24}$ 之间。 dstport : VTEP 通信的端口，IANA 分配的端口是 4789。如果不指定，Linux 默认使用 8472。 remote : 对端 VTEP 的地址。 local : 当前节点 VTEP 要使用的 IP 地址，即当前节点隧道口的 IP 地址。 dev eth0 : 当前节点用于 VTEP 通信的设备，用来获取 VTEP IP 地址。这个参数与 local 参数目的相同，二选一即可。 查看 vxlan0 的详细信息：\n$ ip -d link show vxlan0 11: vxlan0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master vrf-test state UNKNOWN mode DEFAULT group default qlen 1000 link/ether 82:f3:76:95🆎e1 brd ff:ff:ff:ff:ff:ff promiscuity 0 vxlan id 42 remote 192.168.57.54 local 192.168.57.50 srcport 0 0 dstport 4789 ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx 接下来创建一个 VRF，并将 vxlan0 绑定到该 VRF 中：\n$ ip link add vrf0 type vrf table 10 $ ip link set vrf0 up $ ip link set vxlan0 master vrf0 再次查看 vxlan0 的信息：\n$ ip -d link show vxlan0 13: vxlan0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master vrf0 state UNKNOWN mode DEFAULT group default qlen 1000 link/ether aa:4d:80:e3:75:e0 brd ff:ff:ff:ff:ff:ff promiscuity 0 vxlan id 42 remote 192.168.57.54 local 192.168.57.50 srcport 0 0 dstport 4789 ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx vrf_slave table 10 addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 你会发现多了 VRF 的信息。\n接下来为 vxlan0 配置 IP 地址并启用它：\n$ ip addr add 172.18.1.2/24 dev vxlan0 $ ip link set vxlan0 up 执行成功后会发现 VRF 路由表项多了下面的内容，所有目的地址是 172.18.1.0/24 网络包要通过 vxlan0 转发：\n$ ip route show vrf vrf0 172.18.1.0/24 dev vxlan0 proto kernel scope link src 172.18.1.2 同时也会增加一条 FDB 转发表：\n$ bridge fdb show 00:00:00:00:00:00 dev vxlan0 dst 192.168.57.54 self permanent 这个表项的意思是，默认的 VTEP 对端地址为 192.168.57.54。换句话说，原始报文经过 vxlan0 后会被内核添加上 VXLAN 头部，而外部 UDP 头的目的 IP 地址会被冠上 192.168.57.54。\n在另一台主机（192.168.57.54）上也进行相同的配置：\n$ ip link add vxlan0 type vxlan id 42 dstport 4789 remote 192.168.57.50 $ ip link add vrf0 type vrf table 10 $ ip link set vrf0 up $ ip link set vxlan0 master vrf0 $ ip addr add 172.18.1.3/24 dev vxlan0 $ ip link set vxlan0 up 一切大功告成之后，就可以相互通信了，在 192.168.57.50 上 ping 172.18.1.3：\n$ ping 172.18.1.3 -I vrf0 同时使用 wireshark 远程抓包：\n$ ssh root@192.168.57.54 \u0026#39;tcpdump -i any -s0 -c 10 -nn -w - port 4789\u0026#39; | /Applications/Wireshark.app/Contents/MacOS/Wireshark -k -i - 具体含义我就不解释了，参考 Tcpdump 示例教程。\n可以看到 VXLAN 报文可以分为三块：\n最内层是 overlay 网络中实际通信的实体看到的报文（比如这里的 ARP 请求），它们和经典网络的通信报文没有任何区别，除了因为 MTU 导致有些报文比较小。 中间一层是 VXLAN 头部，我们最关心的字段 VNI 确实是 42。 最外层是 VTEP 所在主机的通信报文头部，目的 IP 地址为对端 192.168.57.54。 下面来分析这个最简单的模式下 vxlan 通信的过程：\n发送 ping 报文到 172.18.1.3，查看路由表，报文会从 vxlan0 发出去。\n内核发现 vxlan0 的 IP 是 172.18.1.2/24，和目的 IP 在同一个网段，所以在同一个局域网，需要知道对方的 MAC 地址，因此会发送 ARP 报文查询。\nARP 报文源 MAC 地址为 vxlan0 的 MAC 地址，目的 MAC 地址为全 1 的广播地址（ff:ff:ff:ff:ff:ff）。\nVXLAN 根据配置（VNI 42）添加上头部。\n对端的 VTEP 地址为 192.168.57.54，将报文发送到该地址。\n对端主机接收到这个报文，内核发现是 VXLAN 报文，会根据 VNI 发送给对应的 VTEP。\nVTEP 去掉 VXLAN 头部，取出真正的 ARP 请求报文，同时，VTEP 会记录源 MAC 地址和 IP 地址信息到 FDB 表中，这便是一次学习过程。然后生成 ARP 应答报文。\n$ bridge fdb show 00:00:00:00:00:00 dev vxlan0 dst 192.168.57.50 self permanent aa:4d:80:e3:75:e0 dev vxlan0 dst 192.168.57.50 self 应答报文目的 MAC 地址是发送方 VTEP 的 MAC 地址，目的 IP 是发送方 VTEP 的 IP 地址，直接发送给目的 VTEP。\n应答报文通过 underlay 网络直接返回给发送方主机，发送方主机根据 VNI 把报文转发给 VTEP，VTEP 解包取出 ARP 应答报文，添加 ARP 缓存到内核，并根据报文学习到目的 VTEP 的 IP 地址和目的 MAC 地址，添加到 FDB 表中。\n$ ip neigh show vrf vrf0 172.18.1.3 dev vxlan0 lladdr 76:06:5c:15:d9:78 STALE $ bridge fdb show 00:00:00:00:00:00 dev vxlan0 dst 192.168.57.54 self permanent fe:4a:7e:a2:b5:5d dev vxlan0 dst 192.168.57.54 self 至此 VTEP 已经知道了通信需要的所有信息，后续 ICMP 的 ping 报文都是在这条逻辑隧道中单播进行的，不再需要发送 ARP 报文查询。\n总结以上过程：一个 VXLAN 网络的 ping 报文要经历 ARP 寻址 + ICMP 响应 两个过程，一旦 VTEP 设备学习到了对方 ARP 地址，后续通信就可以免去 ARP 寻址的过程。\n2. VXLAN + Bridge # 上述的点对点 VXLAN 网络通信双方只有一个 VTEP，且只有一个通信实体，而在实际生产中，每台主机上都有几十台甚至上百台虚拟机或容器需要通信，因此需要一种机制将这些通信实体组织起来，再通过隧道口 VTEP 转发出去。\n方案其实也很常见，Linux Bridge 就可以将多块虚拟网卡连接起来，因此可以选择使用 Bridge 将多个虚拟机或容器放到同一个 VXLAN 网络中，网络拓扑图如图所示：\n和上面的模式相比，这里只是多了一个 Bridge，用来连接不同 network namespace 中的 veth pair，同时 VXLAN 网卡也需要连接到该 Bridge。\n首先在 192.168.57.50 上创建 VXLAN 接口：\n$ ip link add vxlan0 type vxlan \\ id 42 \\ dstport 4789 \\ local 192.168.57.50 \\ remote 192.168.57.54 然后创建网桥 bridge0，把 VXLAN 网卡 vxlan0 绑定到上面，然后将 bridge0 绑定到 VRF 中，并启动它们：\n$ ip link add br0 type bridge $ ip link set vxlan0 master br0 $ ip link add vrf0 type vrf table 10 $ ip link set br0 master vrf0 $ ip link set vxlan0 up $ ip link set br0 up $ ip link set vrf0 up 下面创建 network namespace 和一对 veth pair，并把 veth pair 的其中一端绑定到网桥，然后把另一端放到 network namespace 并绑定 IP 地址 172.18.1.2：\n$ ip netns add ns0 $ ip link add veth0 type veth peer name eth0 netns ns0 $ ip link set veth0 master br0 $ ip link set veth0 up $ ip -n ns0 link set lo up $ ip -n ns0 addr add 172.18.1.2/24 dev eth0 $ ip -n ns0 link set eth0 up 用同样的方法在另一台主机上配置 VXLAN 网络，绑定 172.18.1.3 到另外一个 network namespace 中的 eth0：\n$ ip link add vxlan0 type vxlan \\ id 42 \\ dstport 4789 \\ local 192.168.57.54 \\ remote 192.168.57.50 $ ip link add br0 type bridge $ ip link set vxlan0 master br0 $ ip link add vrf0 type vrf table 10 $ ip link set br0 master vrf0 $ ip link set vxlan0 up $ ip link set br0 up $ ip link set vrf0 up $ ip netns add ns0 $ ip link add veth0 type veth peer name eth0 netns ns0 $ ip link set veth0 master br0 $ ip link set veth0 up $ ip -n ns0 link set lo up $ ip -n ns0 addr add 172.18.1.3/24 dev eth0 $ ip -n ns0 link set eth0 up 从 172.18.1.2 ping 172.18.1.3 发现整个通信过程和前面的实验类似，只不过容器发出的 ARP 报文会先经过网桥，再转发给 vxlan0，然后在 vxlan0 处由 Linux 内核添加 VXLAN 头部，最后发送给对端。\n逻辑上，VXLAN 网络下不同主机上的 network namespace 中的网卡都被连接到了同一个网桥上，这样就可以在同一个主机上创建同一 VXLAN 网络下的多个容器，并相互通信了。\n3. 多播模式的 VXLAN # 上面两种模式只能点对点连接，也就是说同一个 VXLAN 网络中只能有两个节点，这怎么能忍。。。有没有办法让同一个 VXLAN 网络中容纳多个节点呢？我们先来回顾一下 VXLAN 通信的两个关键信息：\n对方虚拟机（或容器）的 MAC 地址 对方所在主机的 IP 地址（即对端 VTEP 的 IP 地址） 跨主机的容器之间首次通信时需要知道对方的 MAC 地址，因此会发送 ARP 报文查询。如果有多个节点，就要把 ARP 查询报文发送到所有节点，但传统的 ARP 报文广播是做不到的，因为 Underlay 和 Overlay 不在同一个二层网络，默认情况下 ARP 广播是逃不出主机的。要想实现 Overlay 网络的广播，必须要把报文发送到所有 VTEP 所在的节点，为了解决这个问题，大概有两种思路：\n使用多播，把网络中的某些节点组成一个虚拟的整体。 事先知道 MAC 地址和 VTEP IP 信息，直接把 ARP 和 FDB 信息告诉发送方 VTEP。一般是通过外部的分布式控制中心来收集这些信息，收集到的信息会分发给同一个 VXLAN 网络的所有节点。 我们先来看看多播是怎么实现的，分布式控制中心留到下一篇再讲。\n如果 VXLAN 要使用多播模式，需要底层的网络支持多播功能，多播地址范围为 224.0.0.0~239.255.255.255。 和上面的 点对点 VXLAN + Bridge 模式相比，这里只是将对端的参数改成 group 参数，其他不变，命令如下：\n# 在主机 192.168.57.50 上执行 $ ip link add vxlan0 type vxlan \\ id 42 \\ dstport 4789 \\ local 192.168.57.50 \\ group 224.1.1.1 $ ip link add br0 type bridge $ ip link set vxlan0 master br0 $ ip link add vrf0 type vrf table 10 $ ip link set br0 master vrf0 $ ip link set vxlan0 up $ ip link set br0 up $ ip link set vrf0 up $ ip netns add ns0 $ ip link add veth0 type veth peer name eth0 netns ns0 $ ip link set veth0 master br0 $ ip link set veth0 up $ ip -n ns0 link set lo up $ ip -n ns0 addr add 172.18.1.2/24 dev eth0 $ ip -n ns0 link set eth0 up # 在主机 192.168.57.54 上执行 $ ip link add vxlan0 type vxlan \\ id 42 \\ dstport 4789 \\ local 192.168.57.54 \\ group 224.1.1.1 $ ip link add br0 type bridge $ ip link set vxlan0 master br0 $ ip link add vrf0 type vrf table 10 $ ip link set br0 master vrf0 $ ip link set vxlan0 up $ ip link set br0 up $ ip link set vrf0 up $ ip netns add ns0 $ ip link add veth0 type veth peer name eth0 netns ns0 $ ip link set veth0 master br0 $ ip link set veth0 up $ ip -n ns0 link set lo up $ ip -n ns0 addr add 172.18.1.3/24 dev eth0 $ ip -n ns0 link set eth0 up 和上面的实验明显有区别的是 FDB 表项的内容：\n$ bridge fdb show 00:00:00:00:00:00 dev vxlan0 dst 224.1.1.1 self permanent dst 字段的值变成了多播地址 224.1.1.1，而不是之前对方的 VTEP 地址，VTEP 会通过 IGMP（Internet Group Management Protocol） 加入同一个多播组 224.1.1.1。\n我们来分析下多播模式下 VXLAN 通信的全过程：\n发送 ping 报文到 172.18.1.3，查看路由表，报文会从 vxlan0 发出去。 内核发现 vxlan0 的 IP 是 172.18.1.2/24，和目的 IP 在同一个网段，所以在同一个局域网，需要知道对方的 MAC 地址，因此会发送 ARP 报文查询。 ARP 报文源 MAC 地址为 vxlan0 的 MAC 地址，目的 MAC 地址为全 1 的广播地址（ff:ff:ff:ff:ff:ff）。 VXLAN 根据配置（VNI 42）添加上头部。 到这一步就和之前不一样了，由于不知道对端 VTEP 在哪台主机，根据多播配置，VTEP 会往多播地址 224.1.1.1 发送多播报文。 多播组中的所有主机都会收到这个报文，内核发现是 VXLAN 报文，就会根据 VNI 发送给相应的 VTEP。 收到报文的所有主机的 VTEP 会去掉 VXLAN 的头部，取出真正的 ARP 请求报文。同时，VTEP 会记录源 MAC 地址和 IP 地址信息到 FDB 表中，这便是一次学习过程。如果发现 ARP 不是发送给自己的，就直接丢弃；如果是发送给自己的，则生成 ARP 应答报文。 后面的步骤就和上面的实验相同了。 整个通信过程和之前比较类似，只是 Underlay 采用组播的方式发送报文，对于多节点的 VXLAN 网络来说比较简单高效。但多播也是有它的问题的，并不是所有网络设备都支持多播（比如公有云），再加上多播方式带来的报文浪费，在实际生成中很少被采用。下篇文章就着重介绍如何通过分布式控制中心来自动发现 VTEP 和 MAC 地址等信息。\n4. 参考资料 # linux 上实现 vxlan 网络 ","date":"2020年4月18日","externalUrl":null,"permalink":"/posts/vxlan-linux/","section":"博客","summary":"上篇文章结尾提到 Linux 是支持 VXLAN 的，我们可以使用 Linux 搭建基于 VXLAN 的 overlay 网","title":"VXLAN 基础教程：在 Linux 上配置 VXLAN 网络","type":"posts"},{"content":"VXLAN（Virtual eXtensible Local Area Network，虚拟可扩展局域网），是一种虚拟化隧道通信技术。它是一种 Overlay（覆盖网络）技术，通过三层的网络来搭建虚拟的二层网络。\n简单来讲，VXLAN 是在底层物理网络（underlay）之上使用隧道技术，借助 UDP 层构建的 Overlay 的逻辑网络，使逻辑网络与物理网络解耦，实现灵活的组网需求。它对原有的网络架构几乎没有影响，不需要对原网络做任何改动，即可架设一层新的网络。也正是因为这个特性，很多 CNI 插件（Kubernetes 集群中的容器网络接口，这个大家应该都知道了吧，如果你不知道，现在你知道了）才会选择 VXLAN 作为通信网络。\nVXLAN 不仅支持一对一，也支持一对多，一个 VXLAN 设备能通过像网桥一样的学习方式学习到其他对端的 IP 地址，还可以直接配置静态转发表。\n一个典型的数据中心 VXLAN 网络拓扑图如图所示：\n其中 VM 指的是虚拟机，Hypervisor 指的是虚拟化管理器。\n1. 为什么需要 VXLAN？ # 与 VLAN 相比，VXLAN 很明显要复杂很多，再加上 VLAN 的先发优势，已经得到了广泛的支持，那还要 VXLAN 干啥？\nVLAN ID 数量限制 # VLAN tag 总共有 4 个字节，其中有 12 bit 用来标识不同的二层网络（即 LAN ID），故而最多只能支持 $2^{12}$，即 4096 个子网的划分。而虚拟化（虚拟机和容器）的兴起使得一个数据中心会有成千上万的机器需要通信，这时候 VLAN 就无法满足需求了。而 VXLAN 的报文 Header 预留了 24 bit 来标识不同的二层网络（即 VNI，VXLAN Network Identifier），即 3 个字节，可以支持 $2^{24}$ 个子网。\n交换机 MAC 地址表限制 # 对于同网段主机的通信而言，报文到底交换机后都会查询 MAC 地址表进行二层转发。数据中心虚拟化之后，VM 的数量与原有的物理机相比呈数量级增长，而应用容器化之后，容器与 VM 相比也是呈数量级增长。。。而交换机的内存是有限的，因而 MAC 地址表也是有限的，随着虚拟机（或容器）网卡 MAC 地址数量的空前增加，交换机表示压力山大啊！\n而 VXLAN 就厉害了，它用 VTEP（后面会解释）将二层以太网帧封装在 UDP 中，一个 VTEP 可以被一个物理机上的所有 VM（或容器）共用，一个物理机对应一个 VTEP。从交换机的角度来看，只是不同的 VTEP 之间在传递 UDP 数据，只需要记录与物理机数量相当的 MAC 地址表条目就可以了，一切又回到了和从前一样。\n虚机或容器迁移范围受限 # VLAN 与物理网络融合在一起，不存在 Overlay 网络，带来的问题就是虚拟网络不能打破物理网络的限制。举个例子，如果要在 VLAN 100 部署虚拟机（或容器），那只能在支持 VLAN 100 的物理设备上部署。\nVLAN 其实也有解决办法，就是将所有的交换机 Trunk 连接起来，产生一个大的二层，这样带来的问题就是广播域过分扩大，也包括更多未知的单播和多播，即 BUM（Broadcast，Unknown Unicast，Multicast），同时交换机 MAC 地址表也会有承受不住的问题。\n而 VXLAN 将二层以太网帧封装在 UDP 中（上面说过了），相当于在三层网络上构建了二层网络。这样不管你物理网络是二层还是三层，都不影响虚拟机（或容器）的网络通信，也就无所谓部署在哪台物理设备上了，可以随意迁移。\n总的来说，传统二层和三层的网络在应对这些需求时变得力不从心，虽然很多改进型的技术比如堆叠、SVF、TRILL 等能够增加二层的范围，努力改进经典网络，但是要做到对网络改动尽可能小的同时保证灵活性却非常困难。为了解决这些问题，有很多方案被提出来，Overlay 就是其中之一，而 VXLAN 是 Overlay 的一种典型的技术方案。下面就对 Overlay 做一个简要的介绍。\n2. Overlay 是个啥？ # Overlay 在网络技术领域，指的是一种网络架构上叠加的虚拟化技术模式，其大体框架是对基础网络不进行大规模修改的条件下，实现应用在网络上的承载，并能与其它网络业务分离，并且以基于 IP 的基础网络技术为主。\nIETF 在 Overlay 技术领域提出 VXLAN、NVGRE、STT 三大技术方案。大体思路均是将以太网报文承载到某种隧道层面，差异性在于选择和构造隧道的不同，而底层均是 IP 转发。VXLAN 和 STT 对于现网设备而言对流量均衡要求较低，即负载链路负载分担适应性好，一般的网络设备都能对 L2-L4 的数据内容参数进行链路聚合或等价路由的流量均衡，而 NVGRE 则需要网络设备对 GRE 扩展头感知并对 flow ID 进行 HASH，需要硬件升级；STT 对于 TCP 有较大修改，隧道模式接近 UDP 性质，隧道构造技术属于革新性，且复杂度较高，而 VXLAN 利用了现有通用的 UDP 传输，成熟性极高。\n总体比较，VLXAN 技术具有更大优势，而且当前 VLXAN 也得到了更多厂家和客户的支持，已经成为 Overlay 技术的主流标准。\n3. VXLAN 协议原理 # VXLAN 有几个常见的术语：\nVTEP（VXLAN Tunnel Endpoints，VXLAN 隧道端点）\nVXLAN 网络的边缘设备，用来进行 VXLAN 报文的处理（封包和解包）。VTEP 可以是网络设备（比如交换机），也可以是一台机器（比如虚拟化集群中的宿主机）。\nVNI（VXLAN Network Identifier，VXLAN 网络标识符）\nVNI 是每个 VXLAN 段的标识，是个 24 位整数，一共有 $2^{24} = 16777216$（一千多万），一般每个 VNI 对应一个租户，也就是说使用 VXLAN 搭建的公有云可以理论上可以支撑千万级别的租户。\nTunnel（VXLAN 隧道）\n隧道是一个逻辑上的概念，在 VXLAN 模型中并没有具体的物理实体向对应。隧道可以看做是一种虚拟通道，VXLAN 通信双方认为自己是在直接通信，并不知道底层网络的存在。从整体来说，每个 VXLAN 网络像是为通信的虚拟机搭建了一个单独的通信通道，也就是隧道。\n上图所示为 VXLAN 的工作模型，它创建在原来的 IP 网络（三层）上，只要是三层可达（能够通过 IP 相互通信）的网络就能部署 VXLAN。在 VXLAN 网络的每个端点都有一个 VTEP 设备，负责 VXLAN 协议报文的解包和封包，也就是在虚拟报文上封装 VTEP 通信的报文头部。\n物理网络上可以创建多个 VXLAN 网络，可以将这些 VXLAN 网络看成一个隧道，不同节点上的虚拟机/容器能够通过隧道直连。通过 VNI 标识不同的 VXLAN 网络，使得不同的 VXLAN 可以相互隔离。\nVXLAN 的报文结构如下图所示：\nVXLAN Header : 在原始二层帧的前面增加 8 字节的 VXLAN 的头部，其中最主要的是 VNID，占用 3 个字节（即 24 bit），类似 VLAN ID，可以具有 $2^{24}$ 个网段。\nUDP Header : 在 VXLAN 和原始二层帧的前面使用 8 字节 UDP 头部进行封装（MAC IN UDP），目的端口号缺省使用 4789，源端口按流随机分配（通过 MAC，IP，四层端口号进行 hash 操作）， 这样可以更好的做 ECMP。\nIANA（Internet As-signed Numbers Autority）分配了 4789 作为 VXLAN 的默认目的端口号。 在上面添加的二层封装之后，再添加底层网络的 IP 头部（20 字节）和 MAC 头部（14 字节），这里的 IP 和 MAC 是宿主机的 IP 地址和 MAC 地址。\n同时，这里需要注意 MTU 的问题，传统网络 MTU 一般为 1500，这里加上 VXLAN 的封装多出的（36+14/18，对于 14 的情况为 access 口，省去了 4 字节的 VLAN Tag）50 或 54 字节，需要调整 MTU 为 1550 或 1554，防止频繁分包。\n字段 长度 含义 UDP Header Source Port 16 bit 源端口号是内层以太报文头通过哈希算法计算后的值\nDest Port 16 bit 目的 UDP 端口号是 4789\nUDP Length 16 bit UDP 数据包长度 UDP Checksum 16 bit UDP 数据包校验和 VXLAN Header VXLAN Flags 8 bit 取值为 00001000 Reserved_1 24 bit 保留字段，必须设置为 0\nVXLAN Network Identifier 24 bit VXLAN 网络标识，用于区分 VXLAN 段 Reserved_2 8 bit 保留字段，必须设置为 0 VXLAN 的 Flood 与 Learn # 总的来说，VXLAN 报文的转发过程就是：原始报文经过 VTEP，被 Linux 内核添加上 VXLAN 头部以及外层的 UDP 头部，再发送出去，对端 VTEP 接收到 VXLAN 报文后拆除外层 UDP 头部，并根据 VXLAN 头部的 VNI 把原始报文发送到目的服务器。但这里有一个问题，第一次通信前双方如何知道所有的通信信息？这些信息包括：\n哪些 VTEP 需要加到一个相同的 VNI 组？ 发送方如何知道对方的 MAC 地址？ 如何知道目的服务器在哪个节点上（即目的 VTEP 的地址）？ 第一个问题简单，VTEP 通常由网络管理员来配置。要回答后面两个问题，还得回到 VXLAN 协议的报文上，看看一个完整的 VXLAN 报文需要哪些信息：\n内层报文 : 通信双方的 IP 地址已经明确，只需要 VXLAN 填充对方的 MAC 地址，因此需要一个机制来实现 ARP 功能。\nVXLAN 头部 : 只需要知道 VNI。一般直接配置在 VTEP 上，要么提前规划，要么根据内层报文自动生成。\nUDP 头部 : 需要知道源端口和目的端口，源端口由系统自动生成，目的端口默认是 4789。\nIP 头部 : 需要知道对端 VTEP 的 IP 地址，这个是最关键的部分。\n实际上，VTEP 也会有自己的转发表，转发表通过泛洪和学习机制来维护，对于目标 MAC 地址在转发表中不存在的未知单播，广播流量，都会被泛洪给除源 VTEP 外所有的 VTEP，目标 VTEP 响应数据包后，源 VTEP 会从数据包中学习到 MAC，VNI 和 VTEP 的映射关系，并添加到转发表中，后续当再有数据包转发到这个 MAC 地址时，VTEP 会从转发表中直接获取到目标 VTEP 地址，从而发送单播数据到目标 VTEP。\nVTEP 转发表的学习可以通过以下两种方式：\n多播 外部控制中心（如 Flannel、Cilium 等 CNI 插件） MAC 头部 : 确定了 VTEP 的 IP 地址，后面就好办了，MAC 地址可以通过经典的 ARP 方式获取。\n4. Linux 的 VXLAN # Linux 对 VXLAN 协议的支持时间并不久，2012 年 Stephen Hemminger 才把相关的工作合并到 kernel 中，并最终出现在 kernel 3.7.0 版本。为了稳定性和很多的功能，可能会看到某些软件推荐在 3.9.0 或者 3.10.0 以后版本的 kernel 上使用 VXLAN。\n到了 kernel 3.12 版本，Linux 对 VXLAN 的支持已经完备，支持单播和组播，IPv4 和 IPv6。利用 man 查看 ip 的 link 子命令，可以查看是否有 VXLAN type：\n$ man ip-link 搜索 VXLAN，可以看到如下描述：\n管理 VXLAN 接口 # Linux VXLAN 接口的基本管理如下：\n创建点对点的 VXLAN 接口：\n$ ip link add vxlan0 type vxlan id 4100 remote 192.168.1.101 local 192.168.1.100 dstport 4789 dev eth0 其中 id 为 VNI，remote 为远端主机的 IP，local 为你本地主机的 IP，dev 代表 VXLAN 数据从哪个接口传输。\n在 VXLAN 中，一般将 VXLAN 接口（本例中即 vxlan0）叫做 VTEP。\n创建多播模式的 VXLAN 接口：\n$ ip link add vxlan0 type vxlan id 4100 group 224.1.1.1 dstport 4789 dev eth0 多播组主要通过 ARP 泛洪来学习 MAC 地址，即在 VXLAN 子网内广播 ARP 请求，然后对应节点进行响应。group 指定多播组的地址。\n查看 VXLAN 接口详细信息：\n$ ip -d link show vxlan0 FDB 表 # FDB（Forwarding Database entry，即转发表）是 Linux 网桥维护的一个二层转发表，用于保存远端虚拟机/容器的 MAC地址，远端 VTEP IP，以及 VNI 的映射关系，可以通过 bridge fdb 命令来对 FDB 表进行操作：\n条目添加：\n$ bridge fdb add \u0026lt;remote_host_mac\u0026gt; dev \u0026lt;vxlan_interface\u0026gt; dst \u0026lt;remote_host_ip\u0026gt; 条目删除：\n$ bridge fdb del \u0026lt;remote_host_mac\u0026gt; dev \u0026lt;vxlan_interface\u0026gt; 条目更新：\n$ bridge fdb replace \u0026lt;remote_host_mac\u0026gt; dev \u0026lt;vxlan_interface\u0026gt; dst \u0026lt;remote_host_ip\u0026gt; 条目查询：\n$ bridge fdb show 5. 总结 # 本文通过介绍 VXLAN 出现的时代背景、VXLAN 的概念和网络模型、VXLAN 报文结构，让你对 VXLAN 有了初步的认识；通过介绍 VXLAN 转发表的泛洪和学习，让你知道了通信双方如何感知对方；最后介绍了 Linux 中 VXLAN 的基本配置，让你进一步了解如何在 Linux 中玩转 VXLAN。下一篇文章将会通过实战来说明如何搭建基于 VXLAN 的 Overlay 网络，顺便展开解读上文提到的多播和外部控制中心的工作原理。\n6. 参考资料 # vxlan 协议原理简介 VXLAN vs VLAN ","date":"2020年4月11日","externalUrl":null,"permalink":"/posts/vxlan-protocol-introduction/","section":"博客","summary":"VXLAN（Virtual eXtensible Local Area Network，虚拟可扩展局","title":"VXLAN 基础教程：VXLAN 协议原理介绍","type":"posts"},{"content":"","date":"2020年3月28日","externalUrl":null,"permalink":"/tags/cgroup/","section":"标签","summary":"","title":"Cgroup","type":"tags"},{"content":"这是 Cgroup 系列的第四篇，往期回顾：\nLinux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：CPU Linux Cgroup 入门教程：内存 通过 上篇文章的学习，我们学会了如何查看当前 cgroup 的信息，如何通过操作 /sys/fs/cgroup 目录来动态设置 cgroup，也学会了如何设置 CPU shares 和 CPU quota 来控制 slice 内部以及不同 slice 之间的 CPU 使用时间。本文将继续探讨对 CPU 使用时间的限制。\n对于某些 CPU 密集型的程序来说，不仅需要获取更多的 CPU 使用时间，还要减少工作负载在节流时引起的上下文切换。现在的多核系统中每个核心都有自己的缓存，如果频繁的调度进程在不同的核心上执行势必会带来缓存失效等开销。那么有没有方法针对 CPU 核心进行隔离呢？准确地说是把运行的进程绑定到指定的核心上运行。虽然对于操作系统来说，所有程序生而平等，但有些程序比其他程序更平等。\n对于那些更平等的程序来说，我们需要为它分配更多的 CPU 资源，毕竟人都是很偏心的。废话少说，我们来看看如何使用 cgroup 限制进程使用指定的 CPU 核心。\n1. 查看 CPU 配置 # CPU 核心的编号一般是从 0 开始的，4 个核心的编号范围是 0-3。我们可以通过查看 /proc/cpuinfo 的内容来确定 CPU 的某些信息：\n$ cat /proc/cpuinfo ... processor\t: 3 vendor_id\t: GenuineIntel cpu family\t: 6 model\t: 26 model name\t: Intel(R) Xeon(R) CPU X5650 @ 2.67GHz stepping\t: 4 microcode\t: 0x1f cpu MHz\t: 2666.761 cache size\t: 12288 KB physical id\t: 6 siblings\t: 1 core id\t: 0 cpu cores\t: 1 apicid\t: 6 initial apicid\t: 6 fpu\t: yes fpu_exception\t: yes cpuid level\t: 11 wp\t: yes flags\t: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc eagerfpu pni ssse3 cx16 sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer hypervisor lahf_lm ssbd ibrs ibpb stibp tsc_adjust arat spec_ctrl intel_stibp flush_l1d arch_capabilities bogomips\t: 5333.52 clflush size\t: 64 cache_alignment\t: 64 address sizes\t: 43 bits physical, 48 bits virtual processor : 表示核心的编号，但这不是物理 CPU 的核心，更确切地可以称之为**逻辑核编号。 physical id : 表示当前逻辑核所在的物理 CPU 的核心，也是从 0 开始编号，这里表示这个逻辑核在第 7 个 物理 CPU 上。 core id : 如果这个值大于 0，你就要注意了，你的服务器可能开启了超线程。如果启用了超线程，每个物理 CPU 核心会模拟出 2 个线程，也叫逻辑核（和上面的逻辑核是两回事，只是名字相同而已）。如果你想确认服务器有没有开启超线程，可以通过下面的命令查看： $ cat /proc/cpuinfo | grep -e \u0026#34;core id\u0026#34; -e \u0026#34;physical id\u0026#34; physical id\t: 0 core id\t: 0 physical id\t: 2 core id\t: 0 physical id\t: 4 core id\t: 0 physical id\t: 6 core id\t: 0 如果 physical id 和 core id 皆相同的 processor 出现了两次，就可以断定开启了超线程。显然我的服务器没有开启。\n2. NUMA 架构 # 这里需要涉及到一个概念叫 NUMA（Non-uniform memory access），即非统一内存访问架构。如果主机板上插有多块 CPU，那么就是 NUMA 架构。每块 CPU 独占一块面积，一般都有独立风扇。\n一个 NUMA 节点包含了直连在该区域的 CPU、内存等硬件设备，通信总线一般是 PCI-E。由此也引入了 CPU 亲和性的概念，即 CPU 访问同一个 NUMA 节点上的内存的速度大于访问另一个节点的。\n可以通过下面的命令查看本机的 NUMA 架构：\n$ numactl --hardware available: 1 nodes (0) node 0 cpus: 0 1 2 3 node 0 size: 2047 MB node 0 free: 1335 MB node distances: node 0 0: 10 可以看出该服务器并没有使用 NUMA 架构，总共只有一个 NUMA 节点，即只有一块 CPU，4 个逻辑核心均在此 CPU 上。\n3. isolcpus # Linux 最重要的职责之一就是调度进程，而进程只是程序运行过程的一种抽象，它会执行一系列指令，计算机会按照这些指令来完成实际工作。从硬件的角度来看，真正执行这些指令的是中央处理单元，即 CPU。默认情况下，进程调度器可能会将进程调度到任何一个 CPU 核心上，因为它要根据负载来均衡计算资源的分配。\n为了增加实验的明显效果，可以隔离某些逻辑核心，让系统默认情况下永远不会使用这些核心，除非我指定某些进程使用这些核心。要想做到这一点，就要使用到内核参数 isolcpus 了，例如：如果想让系统默认情况下不使用逻辑核心 2，3 和 4，可以将以下内容添加到内核参数列表中：\nisolcpus=1,2,3 # 或者 isolcpus=1-3 对于 CnetOS 7 来说，可以直接修改 /etc/default/grub：\n$ cat /etc/default/grub GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR=\u0026#34;$(sed \u0026#39;s, release .*$,,g\u0026#39; /etc/system-release)\u0026#34; GRUB_DEFAULT=saved GRUB_DISABLE_SUBMENU=true GRUB_TERMINAL_OUTPUT=\u0026#34;console\u0026#34; GRUB_CMDLINE_LINUX=\u0026#34;crashkernel=auto rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet isolcpus=1,2,3\u0026#34; GRUB_DISABLE_RECOVERY=\u0026#34;true\u0026#34; 然后重新构建 grub.conf：\n$ grub2-mkconfig -o /boot/grub2/grub.cfg 重启系统之后，系统将不再使用逻辑核心 2，3 和 4，只会使用核心 1。找个程序把 CPU 跑满（ 上篇文章用的程序），使用命令 top 查看 CPU 的使用状况：\n执行 top 命令后，在列表页按数字 1 键，就可以看到所有 CPU 了。 可以看到系统只使用了核心 1，下面我们来看看如何将程序绑到特定的 CPU 核心上。\n4. 创建 cgroup # 将程序绑到指定的核心其实很简单，只需设置好 cpuset 控制器就行了。 systemctl 可以管理受其控制资源的 cgroup 控制器，但只能管理有限的控制器（CPU、内存和 BlockIO），不能管理 cpuset 控制器。虽然 systemd 不支持 cpuset，但是相信以后会支持的，另外，现在有一个略显笨拙，但是可以实现同样的目标的方法，后面会介绍。\ncgroup 相关的所有操作都是基于内核中的 cgroup virtual filesystem，使用 cgroup 很简单，挂载这个文件系统就可以了。文件系统默认情况下都是挂载到 /sys/fs/cgroup 目录下，查看一下这个目录：\n$ ll /sys/fs/cgroup 总用量 0 drwxr-xr-x 2 root root 0 3月 28 2020 blkio lrwxrwxrwx 1 root root 11 3月 28 2020 cpu -\u0026gt; cpu,cpuacct lrwxrwxrwx 1 root root 11 3月 28 2020 cpuacct -\u0026gt; cpu,cpuacct drwxr-xr-x 2 root root 0 3月 28 2020 cpu,cpuacct drwxr-xr-x 2 root root 0 3月 28 2020 cpuset drwxr-xr-x 4 root root 0 3月 28 2020 devices drwxr-xr-x 2 root root 0 3月 28 2020 freezer drwxr-xr-x 2 root root 0 3月 28 2020 hugetlb drwxr-xr-x 2 root root 0 3月 28 2020 memory lrwxrwxrwx 1 root root 16 3月 28 2020 net_cls -\u0026gt; net_cls,net_prio drwxr-xr-x 2 root root 0 3月 28 2020 net_cls,net_prio lrwxrwxrwx 1 root root 16 3月 28 2020 net_prio -\u0026gt; net_cls,net_prio drwxr-xr-x 2 root root 0 3月 28 2020 perf_event drwxr-xr-x 2 root root 0 3月 28 2020 pids drwxr-xr-x 4 root root 0 3月 28 2020 systemd 可以看到 cpuset 控制器已经默认被创建并挂载好了。看一下 cpuset 目录下有什么：\n$ ll /sys/fs/cgroup/cpuset 总用量 0 -rw-r--r-- 1 root root 0 3月 28 2020 cgroup.clone_children --w--w--w- 1 root root 0 3月 28 2020 cgroup.event_control -rw-r--r-- 1 root root 0 3月 28 2020 cgroup.procs -r--r--r-- 1 root root 0 3月 28 2020 cgroup.sane_behavior -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.cpu_exclusive -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.cpus -r--r--r-- 1 root root 0 3月 28 2020 cpuset.effective_cpus -r--r--r-- 1 root root 0 3月 28 2020 cpuset.effective_mems -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.mem_exclusive -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.mem_hardwall -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.memory_migrate -r--r--r-- 1 root root 0 3月 28 2020 cpuset.memory_pressure -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.memory_pressure_enabled -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.memory_spread_page -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.memory_spread_slab -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.mems -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.sched_load_balance -rw-r--r-- 1 root root 0 3月 28 2020 cpuset.sched_relax_domain_level -rw-r--r-- 1 root root 0 3月 28 2020 notify_on_release -rw-r--r-- 1 root root 0 3月 28 2020 release_agent -rw-r--r-- 1 root root 0 3月 28 2020 tasks 该目录下只有默认的配置，没有任何 cgroup 子系统。接下来我们来创建 cpuset 子系统并设置相应的绑核参数：\n$ mkdir -p /sys/fs/cgroup/cpuset/test $ echo \u0026#34;3\u0026#34; \u0026gt; /sys/fs/cgroup/cpuset/test/cpuset.cpus $ echo \u0026#34;0\u0026#34; \u0026gt; /sys/fs/cgroup/cpuset/test/cpuset.mems 首先创建了一个 cpuset 子系统叫 test，然后将核心 4 绑到该子系统，即 cpu3。对于 cpuset.mems 参数而言，每个内存节点和 NUMA 节点一一对应。如果进程的内存需求量较大，可以把所有的 NUMA 节点都配置进去。这里就用到了 NUMA 的概念。出于性能的考虑，配置的逻辑核和内存节点一般属于同一个 NUMA 节点，可用 numactl --hardware 命令获知它们的映射关系。很显然，我的主机没有采用 NUMA 架构，只需将其设为节点 0 就好了。\n查看 test 目录：\n$ cd /sys/fs/cgroup/cpuset/test $ ll 总用量 0 -rw-rw-r-- 1 root root 0 3月 28 17:07 cgroup.clone_children --w--w---- 1 root root 0 3月 28 17:07 cgroup.event_control -rw-rw-r-- 1 root root 0 3月 28 17:07 cgroup.procs -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.cpu_exclusive -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.cpus -r--r--r-- 1 root root 0 3月 28 17:07 cpuset.effective_cpus -r--r--r-- 1 root root 0 3月 28 17:07 cpuset.effective_mems -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.mem_exclusive -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.mem_hardwall -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.memory_migrate -r--r--r-- 1 root root 0 3月 28 17:07 cpuset.memory_pressure -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.memory_spread_page -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.memory_spread_slab -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.mems -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.sched_load_balance -rw-rw-r-- 1 root root 0 3月 28 17:07 cpuset.sched_relax_domain_level -rw-rw-r-- 1 root root 0 3月 28 17:07 notify_on_release -rw-rw-r-- 1 root root 0 3月 28 17:07 tasks $ cat cpuset.cpus 3 $ cat cpuset.mems 0 目前 tasks 文件是空的，也就是说，还没有进程运行在该 cpuset 子系统上。需要想办法让指定的进程运行在该子系统上，有两种方法：\n将已经运行的进程的 PID 写入 tasks 文件中； 使用 systemd 创建一个守护进程，将 cgroup 的设置写入 service 文件中（本质上和方法 1 是一样的）。 先来看看方法 1，首先运行一个程序：\n$ nohup sha1sum /dev/zero \u0026amp; [1] 3767 然后将 PID 写入 test 目录的 tasks 中：\n$ echo \u0026#34;3767\u0026#34; \u0026gt; /sys/fs/cgroup/cpuset/test/tasks 查看 CPU 使用情况：\n可以看到绑核生效了，PID 为 3767 的进程被调度到了 cpu3 上。\n下面再来看看方法 2，虽然目前 systemd 不支持使用 cpuset 去指定一个 Service 的 CPU，但我们还是有一个变相的方法，Service 文件内容如下：\n$ cat /etc/systemd/system/foo.service [Unit] Description=foo After=syslog.target network.target auditd.service [Service] ExecStartPre=/usr/bin/mkdir -p /sys/fs/cgroup/cpuset/testset ExecStartPre=/bin/bash -c \u0026#39;/usr/bin/echo \u0026#34;2\u0026#34; \u0026gt; /sys/fs/cgroup/cpuset/testset/cpuset.cpus\u0026#39; ExecStartPre=/bin/bash -c \u0026#39;/usr/bin/echo \u0026#34;0\u0026#34; \u0026gt; /sys/fs/cgroup/cpuset/testset/cpuset.mems\u0026#39; ExecStart=/bin/bash -c \u0026#34;/usr/bin/sha1sum /dev/zero\u0026#34; ExecStartPost=/bin/bash -c \u0026#39;/usr/bin/echo $MAINPID \u0026gt; /sys/fs/cgroup/cpuset/testset/tasks\u0026#39; ExecStopPost=/usr/bin/rmdir /sys/fs/cgroup/cpuset/testset Restart=on-failure [Install] WantedBy=multi-user.target 启动该服务，然后查看 CPU 使用情况：\n该服务中的进程确实被调度到了 cpu2 上。\n5. 回到 Docker # 最后我们回到 Docker，Docker 实际上就是将系统底层实现的 cgroup 、 namespace 等技术集成在一个使用镜像方式发布的工具中，于是形成了 Docker，这个想必大家都知道了，我就不展开了。对于 Docker 来说，有没有办法让容器始终在一个或某几个 CPU 上运行呢？其实还是很简单的，只需要利用 --cpuset-cpus 参数就可以做到！\n下面就来演示一下，指定运行容器的 CPU 核心编号为 1：\n🐳 → docker run -d --name stress --cpuset-cpus=\u0026#34;1\u0026#34; progrium/stress -c 4 查看主机 CPU 的负载：\n只有 Cpu1 达到了 100%，其它的 CPU 并未被容器使用。\n如果你看过该系列的 第一篇文章，应该知道，在新的使用 systemd 实现 init 的系统中（比如 ConetOS 7），系统默认创建了 3 个顶级 slice：System, User 和 Machine，其中 machine.slice 是所有虚拟机和 Linux 容器的默认位置，而 Docker 其实是 machine.slice 的一个变种，你可以把它当成 machine.slice 。\n如果系统中运行的是 Kubernetes，machine.slice 就变成了 kubepods：\n为了便于管理 cgroup，systemd 会为每一个 slice 创建一个子系统，比如 docker 子系统：\n然后再根据容器的设置，将其放入相应的控制器下面，这里我们关心的是 cpuset 控制器，看看它的目录下有啥：\n查看 docker 目录：\n可以看到 Docker 为每个容器创建了一个子目录，7766.. 对应的就是之前我们创建的容器：\n🐳 → docker ps|grep stress 7766580dd0d7 progrium/stress \u0026#34;/usr/bin/stress --v…\u0026#34; 36 minutes ago Up 36 minutes stress 我们来检验一下该目录下的配置：\n$ cd /sys/fs/cgroup/cpuset/docker/7766580dd0d7d9728f3b603ed470b04d0cac1dd923f7a142fec614b12a4ba3be $ cat cpuset.cpus 1 $ cat cpuset.mems 0 $ cat tasks 6536 6562 6563 6564 6565 $ ps -ef|grep stress root 6536 6520 0 10:08 ? 00:00:00 /usr/bin/stress --verbose -c 4 root 6562 6536 24 10:08 ? 00:09:50 /usr/bin/stress --verbose -c 4 root 6563 6536 24 10:08 ? 00:09:50 /usr/bin/stress --verbose -c 4 root 6564 6536 24 10:08 ? 00:09:50 /usr/bin/stress --verbose -c 4 root 6565 6536 24 10:08 ? 00:09:50 /usr/bin/stress --verbose -c 4 当然，你也可以将容器绑到多个 CPU 核心上运行，这里我就不赘述了。下篇文章将会介绍如何通过 cgroup 来限制 BlockIO。\n","date":"2020年3月28日","externalUrl":null,"permalink":"/posts/understanding-cgroups-part-4-cpuset/","section":"博客","summary":"这是 Cgroup 系列的第四篇，往期回顾： Linux Cgroup 入门教程：基本概念 Linux Cgroup 入门","title":"Linux Cgroup 入门教程：cpuset","type":"posts"},{"content":"","date":"2020年3月28日","externalUrl":null,"permalink":"/series/linux-cgroup-%E5%85%A5%E9%97%A8%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"Linux Cgroup 入门系列","type":"series"},{"content":" 原文链接： Docker Images : Part I - Reducing Image Size\n对于刚接触容器的人来说，他们很容易被自己制作的 Docker 镜像体积吓到，我只需要一个几 MB 的可执行文件而已，为何镜像的体积会达到 1 GB 以上？本文将会介绍几个奇技淫巧来帮助你精简镜像，同时又不牺牲开发人员和运维人员的操作便利性。本系列文章将分为三个部分：\n第一部分着重介绍多阶段构建（multi-stage builds），因为这是镜像精简之路至关重要的一环。在这部分内容中，我会解释静态链接和动态链接的区别，它们对镜像带来的影响，以及如何避免那些不好的影响。中间会穿插一部分对 Alpine 镜像的介绍。链接： Docker 镜像制作教程：减小镜像体积\n第二部分将会针对不同的语言来选择适当的精简策略，其中主要讨论 Go，同时也涉及到了 Java，Node，Python，Ruby 和 Rust。这一部分也会详细介绍 Alpine 镜像的避坑指南。什么？你不知道 Alpine 镜像有哪些坑？我来告诉你。链接： Docker 镜像制作教程：针对不同语言的精简策略\n第三部分将会探讨适用于大多数语言和框架的通用精简策略，例如使用常见的基础镜像、提取可执行文件和减小每一层的体积。同时还会介绍一些更加奇特或激进的工具，例如 Bazel，Distroless，DockerSlim 和 UPX，虽然这些工具在某些特定场景下能带来奇效，但大多情况下会起到反作用。\n本文介绍第一部分。\n1. 万恶之源 # 我敢打赌，每一个初次使用自己写好的代码构建 Docker 镜像的人都会被镜像的体积吓到，来看一个例子。\n让我们搬出那个屡试不爽的 hello world C 程序：\n/* hello.c */ int main () { puts(\u0026#34;Hello, world!\u0026#34;); return 0; } 并通过下面的 Dockerfile 构建镜像：\nFROM gcc COPY hello.c . RUN gcc -o hello hello.c CMD [\u0026#34;./hello\u0026#34;] 然后你会发现构建成功的镜像体积远远超过了 1 GB。。。因为该镜像包含了整个 gcc 镜像的内容。\n如果使用 Ubuntu 镜像，安装 C 编译器，最后编译程序，你会得到一个大概 300 MB 大小的镜像，比上面的镜像小多了。但还是不够小，因为编译好的可执行文件还不到 20 KB：\n$ ls -l hello -rwxr-xr-x 1 root root 16384 Nov 18 14:36 hello 类似地，Go 语言版本的 hello world 会得到相同的结果：\npackage main import \u0026#34;fmt\u0026#34; func main () { fmt.Println(\u0026#34;Hello, world!\u0026#34;) } 使用基础镜像 golang 构建的镜像大小是 800 MB，而编译后的可执行文件只有 2 MB 大小：\n$ ls -l hello -rwxr-xr-x 1 root root 2008801 Jan 15 16:41 hello 还是不太理想，有没有办法大幅度减少镜像的体积呢？往下看。\n为了更直观地对比不同镜像的大小，所有镜像都使用相同的镜像名，不同的标签。例如：hello:gcc，hello:ubuntu，hello:thisweirdtrick 等等，这样就可以直接使用命令 docker images hello 列出所有镜像名为 hello 的镜像，不会被其他镜像所干扰。 2. 多阶段构建 # 要想大幅度减少镜像的体积，多阶段构建是必不可少的。多阶段构建的想法很简单：“我不想在最终的镜像中包含一堆 C 或 Go 编译器和整个编译工具链，我只要一个编译好的可执行文件！”\n多阶段构建可以由多个 FROM 指令识别，每一个 FROM 语句表示一个新的构建阶段，阶段名称可以用 AS 参数指定，例如：\nFROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c FROM ubuntu COPY --from=mybuildstage hello . CMD [\u0026#34;./hello\u0026#34;] 本例使用基础镜像 gcc 来编译程序 hello.c，然后启动一个新的构建阶段，它以 ubuntu 作为基础镜像，将可执行文件 hello 从上一阶段拷贝到最终的镜像中。最终的镜像大小是 64 MB，比之前的 1.1 GB 减少了 95%：\n🐳 → docker images minimage REPOSITORY TAG ... SIZE minimage hello-c.gcc ... 1.14GB minimage hello-c.gcc.ubuntu ... 64.2MB 还能不能继续优化？当然能。在继续优化之前，先提醒一下：\n在声明构建阶段时，可以不必使用关键词 AS，最终阶段拷贝文件时可以直接使用序号表示之前的构建阶段（从零开始）。也就是说，下面两行是等效的：\nCOPY --from=mybuildstage hello . COPY --from=0 hello . 如果 Dockerfile 内容不是很复杂，构建阶段也不是很多，可以直接使用序号表示构建阶段。一旦 Dockerfile 变复杂了，构建阶段增多了，最好还是通过关键词 AS 为每个阶段命名，这样也便于后期维护。\n使用经典的基础镜像 # 我强烈建议在构建的第一阶段使用经典的基础镜像，这里经典的镜像指的是 CentOS，Debian，Fedora 和 Ubuntu 之类的镜像。你可能还听说过 Alpine 镜像，不要用它！至少暂时不要用，后面我会告诉你有哪些坑。\nCOPY --from 使用绝对路径 # 从上一个构建阶段拷贝文件时，使用的路径是相对于上一阶段的根目录的。如果你使用 golang 镜像作为构建阶段的基础镜像，就会遇到类似的问题。假设使用下面的 Dockerfile 来构建镜像：\nFROM golang COPY hello.go . RUN go build hello.go FROM ubuntu COPY --from=0 hello . CMD [\u0026#34;./hello\u0026#34;] 你会看到这样的报错：\nCOPY failed: stat /var/lib/docker/overlay2/1be...868/merged/hello: no such file or directory 这是因为 COPY 命令想要拷贝的是 /hello，而 golang 镜像的 WORKDIR 是 /go，所以可执行文件的真正路径是 /go/hello。\n当然你可以使用绝对路径来解决这个问题，但如果后面基础镜像改变了 WORKDIR 怎么办？你还得不断地修改绝对路径，所以这个方案还是不太优雅。最好的方法是在第一阶段指定 WORKDIR，在第二阶段使用绝对路径拷贝文件，这样即使基础镜像修改了 WORKDIR，也不会影响到镜像的构建。例如：\nFROM golang WORKDIR /src COPY hello.go . RUN go build hello.go FROM ubuntu COPY --from=0 /src/hello . CMD [\u0026#34;./hello\u0026#34;] 最后的效果还是很惊人的，将镜像的体积直接从 800 MB 降低到了 66 MB：\n🐳 → docker images minimage REPOSITORY TAG ... SIZE minimage hello-go.golang ... 805MB minimage hello-go.golang.ubuntu-workdir ... 66.2MB 3. FROM scratch 的魔力 # 回到我们的 hello world，C 语言版本的程序大小为 16 kB，Go 语言版本的程序大小为 2 MB，那么我们到底能不能将镜像缩减到这么小？能否构建一个只包含我需要的程序，没有任何多余文件的镜像？\n答案是肯定的，你只需要将多阶段构建的第二阶段的基础镜像改为 scratch 就好了。scratch 是一个虚拟镜像，不能被 pull，也不能运行，因为它表示空、nothing！这就意味着新镜像的构建是从零开始，不存在其他的镜像层。例如：\nFROM golang COPY hello.go . RUN go build hello.go FROM scratch COPY --from=0 /go/hello . CMD [\u0026#34;./hello\u0026#34;] 这一次构建的镜像大小正好就是 2 MB，堪称完美！\n然而，但是，使用 scratch 作为基础镜像时会带来很多的不便，且听我一一道来。\n缺少 shell # scratch 镜像的第一个不便是没有 shell，这就意味着 CMD/RUN 语句中不能使用字符串，例如：\n... FROM scratch COPY --from=0 /go/hello . CMD ./hello 如果你使用构建好的镜像创建并运行容器，就会遇到下面的报错：\ndocker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused \u0026#34;exec: \\\u0026#34;/bin/sh\\\u0026#34;: stat /bin/sh: no such file or directory\u0026#34;: unknown. 从报错信息可以看出，镜像中并不包含 /bin/sh，所以无法运行程序。这是因为当你在 CMD/RUN 语句中使用字符串作为参数时，这些参数会被放到 /bin/sh 中执行，也就是说，下面这两条语句是等效的：\nCMD ./hello CMD /bin/sh -c \u0026#34;./hello\u0026#34; 解决办法其实也很简单 : 使用 JSON 语法取代字符串语法。 例如，将 CMD ./hello 替换为 CMD [\u0026quot;./hello\u0026quot;]，这样 Docker 就会直接运行程序，不会把它放到 shell 中运行。\n缺少调试工具 # scratch 镜像不包含任何调试工具，ls、ps、ping 这些统统没有，当然了，shell 也没有（上文提过了），你无法使用 docker exec 进入容器，也无法查看网络堆栈信息等等。\n如果想查看容器中的文件，可以使用 docker cp；如果想查看或调试网络堆栈，可以使用 docker run --net container:，或者使用 nsenter；为了更好地调试容器，Kubernetes 也引入了一个新概念叫 Ephemeral Containers，但现在还是 Alpha 特性。\n虽然有这么多杂七杂八的方法可以帮助我们调试容器，但它们会将事情变得更加复杂，我们追求的是简单，越简单越好。\n折中一下可以选择 busybox 或 alpine 镜像来替代 scratch，虽然它们多了那么几 MB，但从整体来看，这只是牺牲了少量的空间来换取调试的便利性，还是很值得的。\n缺少 libc # 这是最难解决的问题。使用 scratch 作为基础镜像时，Go 语言版本的 hello world 跑得很欢快，C 语言版本就不行了，或者换个更复杂的 Go 程序也是跑不起来的（例如用到了网络相关的工具包），你会遇到类似于下面的错误：\nstandard_init_linux.go:211: exec user process caused \u0026#34;no such file or directory\u0026#34; 从报错信息可以看出缺少文件，但没有告诉我们到底缺少哪些文件，其实这些文件就是程序运行所必需的动态库（dynamic library）。\n那么，什么是动态库？为什么需要动态库？\n所谓动态库、静态库，指的是程序编译的链接阶段，链接成可执行文件的方式。静态库指的是在链接阶段将汇编生成的目标文件.o 与引用到的库一起链接打包到可执行文件中，因此对应的链接方式称为静态链接（static linking）。而动态库在程序编译时并不会被连接到目标代码中，而是在程序运行时才被载入，因此对应的链接方式称为动态链接（dynamic linking）。\n90 年代的程序大多使用的是静态链接，因为当时的程序大多数都运行在软盘或者盒式磁带上，而且当时根本不存在标准库。这样程序在运行时与函数库再无瓜葛，移植方便。但对于 Linux 这样的分时系统，会在同一块硬盘上并发运行多个程序，这些程序基本上都会用到标准的 C 库，这时使用动态链接的优点就体现出来了。使用动态链接时，可执行文件不包含标准库文件，只包含到这些库文件的索引。例如，某程序依赖于库文件 libtrigonometry.so 中的 cos 和 sin 函数，该程序运行时就会根据索引找到并加载 libtrigonometry.so，然后程序就可以调用这个库文件中的函数。\n使用动态链接的好处显而易见：\n节省磁盘空间，不同的程序可以共享常见的库。 节省内存，共享的库只需从磁盘中加载到内存一次，然后在不同的程序之间共享。 更便于维护，库文件更新后，不需要重新编译使用该库的所有程序。 严格来说，动态库与共享库（shared libraries）相结合才能达到节省内存的功效。Linux 中动态库的扩展名是 .so（ shared object），而 Windows 中动态库的扩展名是 .DLL（ Dynamic-link library）。\n回到最初的问题，默认情况下，C 程序使用的是动态链接，Go 程序也是。上面的 hello world 程序使用了标准库文件 libc.so.6，所以只有镜像中包含该文件，程序才能正常运行。使用 scratch 作为基础镜像肯定是不行的，使用 busybox 和 alpine 也不行，因为 busybox 不包含标准库，而 alpine 使用的标准库是 musl libc，与大家常用的标准库 glibc 不兼容，后续的文章会详细解读，这里就不赘述了。\n那么该如何解决标准库的问题呢？有三种方案。\n1、使用静态库 # 我们可以让编译器使用静态库编译程序，办法有很多，如果使用 gcc 作为编译器，只需加上一个参数 -static：\n$ gcc -o hello hello.c -static 编译完的可执行文件大小为 760 kB，相比于之前的 16kB 是大了好多，这是因为可执行文件中包含了其运行所需要的库文件。编译完的程序就可以跑在 scratch 镜像中了。\n如果使用 alpine 镜像作为基础镜像来编译，得到的可执行文件会更小（\u0026lt; 100kB），下篇文章会详述。\n2、拷贝库文件到镜像中 # 为了找出程序运行需要哪些库文件，可以使用 ldd 工具：\n$ ldd hello linux-vdso.so.1 (0x00007ffdf8acb000) libc.so.6 =\u0026gt; /usr/lib/libc.so.6 (0x00007ff897ef6000) /lib64/ld-linux-x86-64.so.2 =\u0026gt; /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff8980f7000) 从输出结果可知，该程序只需要 libc.so.6 这一个库文件。linux-vdso.so.1 与一种叫做 VDSO 的机制有关，用来加速某些系统调用，可有可无。ld-linux-x86-64.so.2 表示动态链接器本身，包含了所有依赖的库文件的信息。\n你可以选择将 ldd 列出的所有库文件拷贝到镜像中，但这会很难维护，特别是当程序有大量依赖库时。对于 hello world 程序来说，拷贝库文件完全没有问题，但对于更复杂的程序（例如使用到 DNS 的程序），就会遇到令人费解的问题：glibc（GNU C library）通过一种相当复杂的机制来实现 DNS，这种机制叫 NSS（Name Service Switch, 名称服务开关）。它需要一个配置文件 /etc/nsswitch.conf 和额外的函数库，但使用 ldd 时不会显示这些函数库，因为这些库在程序运行后才会加载。如果想让 DNS 解析正确工作，必须要拷贝这些额外的库文件（/lib64/libnss_*）。\n我个人不建议直接拷贝库文件，因为它非常难以维护，后期需要不断地更改，而且还有很多未知的隐患。\n3、使用 busybox:glibc 作为基础镜像 # 有一个镜像可以完美解决所有的这些问题，那就是 busybox:glibc。它只有 5 MB 大小，并且包含了 glibc 和各种调试工具。如果你想选择一个合适的镜像来运行使用动态链接的程序，busybox:glibc 是最好的选择。\n注意：如果你的程序使用到了除标准库之外的库，仍然需要将这些库文件拷贝到镜像中。\n4. 总结 # 最后来对比一下不同构建方法构建的镜像大小：\n原始的构建方法：1.14 GB 使用 ubuntu 镜像的多阶段构建：64.2 MB 使用 alpine 镜像和静态 glibc：6.5 MB 使用 alpine 镜像和动态库：5.6 MB 使用 scratch 镜像和静态 glibc：940 kB 使用 scratch 镜像和静态 musl libc：94 kB 最终我们将镜像的体积减少了 99.99%。\n但我不建议使用 sratch 作为基础镜像，因为调试起来非常麻烦，但如果你喜欢，我也不会拦着你。\n下篇文章将会着重介绍 Go 语言的镜像精简策略，其中会花很大的篇幅来讨论 alpine 镜像，因为它实在是太酷了，在使用它之前必须得摸清它的底细。\n","date":"2020年3月19日","externalUrl":null,"permalink":"/posts/docker-images-part1-reducing-image-size/","section":"博客","summary":"原文链接： Docker Images : Part I - Reducing Image Size 对于刚接触容器的人来说，他们很容","title":"Docker 镜像制作教程：减小镜像体积","type":"posts"},{"content":"对于需要长期与终端打交道的工程师来说，拥有一款称手的终端管理器是很有必要的，对于 Windows 用户来说，最好的选择是 Xshell，这个大家都没有异议。但对于 MacOS 用户来说，仍然毋庸置疑，iTerm2 就是你要的利器，如果你觉得不是，那是你的问题，不是 iTerm2 的问题。今天我就来告诉你问题出在哪里，我将要向你展示的基本上都是你没见过的配方，擦亮眼睛吧！\n本教程总共分为三个部分：第一部分关于 iTerm2 自身的配置和美化；第二部分关于 连接远程服务器的配置和优化；第三部分关于 zsh 的配置和美化。\n今天来讲第一部分。\n1. 悬浮窗口 # 首先我们来解决第一个问题 : 如何在任何界面呼入呼出 iTerm2 的窗口，并且悬浮在界面的顶部？\n相信每个人都会有这样的使用场景：你正在全屏浏览器浏览网页，或者正在全屏编辑器写代码写文章之类的，突然想到了什么，或发现了什么，想快速打开终端，执行一两条命令（诸如打开文件、启动服务等），然后关闭。\n对于这种情况，我们的愿景是可以通过快捷键迅速打开终端，然后用同样的快捷键迅速隐藏它，直到我需要时再次来到我面前。至于实现方式，虽繁琐但并不复杂，下面跟着我的步骤走：\n创建新的 Profile # 首先打开 Preperence → Profiles，新建一个 Profiles，取名 HotKey Window。\n背景透明与模糊设置 # 在 Preperence → Profiles → Window → Window Appearance 进行设置\n窗口风格配置 # 在 Preperence → Profiles → Window → Settings for New Windows 进行设置\n解释一下这几个参数：\nFull-Width Top of Screen : 这个很好理解，让终端显示在屏幕顶部，并占满整个宽度。 Current Spce : 表示只显示在当前的工作空间，举个例子吧，假设你在当前屏幕打开了终端，你切换到下一个屏幕时它就不会跟到下一个屏幕。 Screen width Cursor : 这个和上面的参数搭配，用来判定哪个屏幕属于当前的工作空间，表示你的鼠标在哪，哪里就是当前的工作空间。 设置 HotKey # 在 Preperence → Profiles → Keys → HotKey Window 进行设置\n选中 A hotkey opens a dedicated window with this profile，表示此 profile 可以通过快捷键快速打开快速关闭。\n然后点击 Configure Hotkey Window，设置快捷键。\n为了只使用一个键，可以使用功能键作为快捷键，我选择的是 F12。\n对于没有 Touch Bar 的 MacBook，我们可以这么设置，首先打开系统偏好设置，选择键盘设置。\n选中 将 F1、F2 等键用作标准功能键。\n这样你就可以通过 F12 来快速打开关闭终端了。\n请注意：打开了标准功能键之后，以后再想使用功能键的特殊功能，比如 F11 减小音量、F12 增大音量，必须得和 Fn 键组合使用，例如增大音量就是 Fn+F12。\n对于新款带 Touch Bar 的 MacBook，可以这么设置，首先打开系统偏好设置，选择键盘设置。\n\u0026ldquo;按下 Fn 键以\u0026rdquo; 选择 “显示F1、F2等键”：\n这样就可以使用 Fn+F12 来快速打开终端。\n同时在 快捷键 → 功能键 设置中添加 iTerm 应用，这样打开 iTerm2 窗口时就可以直接使用 F12 键来关闭窗口。\n到目前为止，悬浮终端的 Profile 就配置完成了，你可以按下你设置的 HotKey 来方便快速打开和隐藏命令行。如果你想新建标签页也用这个 Profile，可以将 Hotkey Window 设为默认的 Profile：\n下面我们来做一些优化工作。\n2. 改变光标形状 # 在 Preperence → Profiles → Text 进行设置。\n默认光标形状是酱紫的：\n更改设置之后光标形状就顺眼多了：\n3. 为自己代言 # 如果你要为自己代言，新建任何一个终端窗口都想打上自己的 Logo，可以在 Preperence → Profiles → General 进行设置。\n这样不管我切换到哪个终端，都可以 fuck cloud native！\n4. 自定义标签页标题 # 在 Preperence → Profiles → General 进行设置。\n取消勾选 Applications in terminal may change the title：\nTitle 选择 Profile Name，取消勾选 Job Name：\n这样每个标签页的标题都会显示为对应的 Profile Name：\n5. 自定义配色方案 # 网上有现成的配色方案，我们可以直接拿来主义，地址在这里： https://iterm2colorschemes.com/。\n下载压缩包，解压后，打开 iTerm2 的设置：Preperence → Profiles → Colors → Color Presets。点击 import 选择解压好的主题目录下 schemes 目录下的你想要的主题导入。\n导入之后，再选择你想要的主题就好了，我当然选的是 Ubuntu 骚紫~~\n6. 统一配色 # 默认情况下标签的颜色是黑的，即使你改了配色也没用：\n如果想统一配色，需要稍微调整一下配置，打开 Appearence → General，将 Theme 改为 Minimal：\n7. 关闭启动界面 # 如果你不想每次打开 iTerm2 都打开默认的窗口，也就是静默打开程序，可以在 Appearence → General 设置：\n勾选 Exclude from Dock and ...，更改设置之后下次你再重新打开 iTerm2 就会变成这个样子：\n它再也不会出现在 Dock 中，也不会在启动时给你打开一个默认的窗口，你可以优雅地通过快捷键呼入呼出窗口，完美。\n8. 使用 shell integration # iTerm2 可以与 unix shell 集成在一起，在安装了 iTerm2 的 shell 集成工具后，可以在 iTerm2 中看到命令历史、当前工作目录、主机名、上传下载文件等。\n可以点击菜单栏 iTerm2 \u0026gt; Install Shell Integration 或者终端输入指令：\n# 如果你的默认 shell 是 bash，请将 zsh 换成 bash $ curl -L https://iterm2.com/misc/install_shell_integration.sh | zsh 该脚本会自动安装当前终端 shell 的对应脚本，并写入到对应的 shell 配置文件中。例如在 zsh shell 中执行完脚本后，.zshrc 中间中写入了下面语句：\n$ test -e \u0026#34;${HOME}/.iterm2_shell_integration.zsh\u0026#34; \u0026amp;\u0026amp; source \u0026#34;${HOME}/.iterm2_shell_integration.zsh\u0026#34; 在安装完 iTerm2 的 shell integration 后会在终端界面中最左侧多出一个蓝色三角形的标记。如图：\n有蓝色三角形的标记说明当前 shell 支持 shell integration。如需关闭标记，可以在 iTerm2 \u0026gt; Preferences \u0026gt; Profiles \u0026gt; (your profile) \u0026gt; Terminal 最下面 \u0026gt; Shell Integration 关闭 Show mark indicators 。\n所有工具：\nimgcat filename Displays the image inline. imgls Shows a directory listing with image thumbnails. it2api Command-line utility to manipulate iTerm2. it2attention start|stop|fireworks Gets your attention. it2check Checks if the terminal is iTerm2. it2copy [filename] Copies to the pasteboard. it2dl filename Downloads the specified file, saving it in your Downloads folder. it2setcolor ... Changes individual color settings or loads a color preset. it2setkeylabel ... Changes Touch Bar function key labels. it2ul Uploads a file. it2universion Sets the current unicode version. 例如，可以用 imgcat 直接在终端显示图片：\n9. 奇技淫巧 # 最后介绍一些 iTerm2 的奇技淫巧。\n剪贴板历史记录 # iTerm2 允许我们快速查看剪贴板内容 只需使用 Command + Shift + h 可以呼出粘贴历史，支持模糊检索。还可以设置将粘贴历史保存在磁盘上（Preferences -\u0026gt; General）\n智能选中 # 在 iTerm2 中，双击选中，三击选中整行，四击智能选中（智能规则可 配置），可以识别网址，引号引起的字符串，邮箱地址等。（很多时候双击的选中就已经很智能了）\n巧用 Command 键 # 按住 ⌘ 键：\n可以拖拽选中的字符串； 点击 url：调用默认浏览器访问该网址； 点击文件：调用默认程序打开文件； 点击文件夹：在 finder 中打开该文件夹； 同时按住 option 键，可以以矩形选中，类似于 vim 中的 ctrl v 操作。 将文本内容复制到剪切板 # $ pbcopy \u0026lt; text.md 在 Finder 中打开当前目录 # $ open . ok，关于 iTerm 自身的设置和优化到这里就结束了，下一篇将会介绍关于连接远程服务器的设置和优化，敬请期待。\n","date":"2020年3月10日","externalUrl":null,"permalink":"/posts/customize-iterm2-1/","section":"博客","summary":"对于需要长期与终端打交道的工程师来说，拥有一款称手的终端管理","title":"iTerm2 配置与美化-自定义配置和优化教程（上）","type":"posts"},{"content":"Grafana 自带两款主题 Light 和 Dark，都还不错，Light 有点刺眼，不建议使用。Dark 还马马虎虎，不过时间长了总会产生审美疲劳，anyway 还是有很多人需要自定义主题的，前几天我在票圈分享了魔改的 Grafana 界面之后，一大批童鞋让我分享主题。可是 Grafana 默认情况下是不支持自定义主题的，你想改变主题样式或新增主题只能修改源码重新编译。\n难道没有别的办法了？办法还是有的，只不过稍微有点繁琐，但不复杂。今天就来给大家分享一种不需要改源码的方法，老少皆宜，按照我的步骤来，最后一定能搞定。这里不得不提一句，很多事情都是没有什么技术含量的，靠的是敏锐的嗅觉、强大的信息收集能力和变通能力，有很多技术大神思维都很僵化，解决问题容易钻进死胡同，这里我就不多说了。\n就拿今天的主题来说，自定义 Grafana 主题的方法真的没有什么技术含量，当你知道了之后就会觉得它非常简单，但是为什么你搞不定呢？可以自己思考一下。\n下面我来演示一下我解决这个问题的思路和方法，最后给出结果。\n一开始我想到 Grafana 可以通过插件机制来扩展和自定义自身的功能，那就可以从这里入手，首先打开 Google 搜索，从 Grafana 官网搜索关于 theme 的插件：\n找了一圈发现只有 Boom theme plugin 符合要求，点进去发现这是一个 Panel 插件，这就意味着由于插件自身的局限性，不管你做了什么它只会对当前的仪表盘生效。如果你想改变当前仪表盘的样式，需要添加一个面板：\n点击 ”Choose Visualization“ 选择可视化类型，然后选择 \u0026lt;Boom Theme\u0026gt;，然后你就可以添加自定义主题了。\n但是现在问题又来了，我太懒了，不想自己写 CSS，怎么办？有没有别人写好的主题呢？Github 是一个宝库，可以去那里找找。通过关键词 grafana theme 搜索过去一年内活跃过的项目：\n最终选择了 theme.pak。找到自己心仪的主题添加到上面的面板中，就大功告成了：\n你可以将其中一个主题设为默认主题，这样每次打开当前仪表盘都会使用你设置的默认主题。自定义主题后的仪表盘是这个样子的：\n最上面的菜单是我们刚刚添加的主题，可以直接点击不同主题实时切换：\n如果想让所有的仪表盘都使用自定义主题，需要在所有的仪表盘上新增一个 Boom Theme Panel，为了避免重复的配置工作，可以直接复制 Panel，操作步骤如下：\n首先点击 Panel 上的到三角，鼠标悬停在选项 More 上：\n然后选择 Copy：\n到下一个仪表盘中新建一个面板，选择 Paste copied panel：\n搞定。\n怎么样，没什么技术含量吧？\n","date":"2020年2月29日","externalUrl":null,"permalink":"/posts/customize-grafana-theme/","section":"博客","summary":"Grafana 自带两款主题 Light 和 Dark，都还不错，Light 有点刺眼，不","title":"Grafana 自定义主题","type":"posts"},{"content":"我是一堆 K8s 控制器。\n你可能会疑惑为什么是一堆，因为我不是一个人，我只是众多控制器中的一员，你也可以把我看成是众多控制器的集合。我的职责就是监控集群内资源的实际状态，一旦发现其与期望的状态不相符，就采取行动使其符合期望状态。\n想当初，Kubernetes 老大哥创造我时，只是打算让我用控制循环简单维护下资源的状态。但我后来的发展，远远超出了他的想象。\n1. 控制循环 # 所谓控制循环就是一个用来调节系统状态的周期性操作，在 Kubernetes 中也叫调谐循环（Reconcile Loop）。我的手下控制着很多种不同类型的资源，比如 Pod，Deployment，Service 等等。就拿 Deployment 来说吧，我的控制循环主要分为三步：\n从 API Server 中获取到所有属于该 Deployment 的 Pod，然后统计一下它们的数量，即它们的实际状态。 检查 Deployment 的 Replicas 字段，看看期望状态是多少个 Pod。 将这两个状态做比较，如果期望状态的 Pod 数量比实际状态多，就创建新 Pod，多几个就创建几个新的；如果期望状态的 Pod 数量比实际状态少，就删除旧 Pod，少几个就删除几个旧的。 然而好景不长，我收到了 Kubernetes 掌门人（看大门的） API Server 的抱怨：“你访问我的次数太频繁了，非常消耗我的资源，我连上厕所的时间都没有了！”\n我仔细一想，当前的控制循环模式确实有这个缺陷——访问 API Server 的次数太频繁了，容易被老大反感。\n所以我决定，找一个小弟。\n2. Informer # 这次我招的小弟叫 Informer，它分担一部分我的任务，具体的做法是这样的：由 Informer 代替我去访问 API Server，而我不管是查状态还是对资源进行伸缩都和 Informer 进行交接。而且 Informer 不需要每次都去访问 API Server，它只要在初始化的时候通过 LIST API 获取所有资源的最新状态，然后再通过 WATCH API 去监听这些资源状态的变化，整个过程被称作 ListAndWatch。\n而 Informer 也不傻，它也有一个助手叫 Reflector，上面所说的 ListAndWatch 事实上是由 Reflector 一手操办的。\n这一次，API Server 的压力大大减轻了，因为 Reflector 大部分时间都在 WATCH，并没有通过 LIST 获取所有状态，这使 API Server 的压力大大减少。我想这次掌门人应该不会再批评我了吧。\n然而没过几天，掌门人又找我谈话了：“你的手下每次来 WATCH 我，都要 WATCH 所有兄弟的状态，依然很消耗我的资源啊！我就纳闷了，你一次搞这么多兄弟，你虎啊？”\n我一想有道理啊，没必要每次都 WATCH 所有兄弟的状态，于是告诉 Informer：“以后再去 API Server 那里 WATCH 状态的时候，只查 WATCH 特定资源的状态，不要一股脑儿全 WATCH。“\nInformer 再把这个决策告诉 Reflector，事情就这么愉快地决定了。\n本以为这次我会得到掌门人的夸奖，可没过几天安稳日子，它又来找我诉苦了：“兄弟，虽然你减轻了我的精神压力，但我的财力有限啊，如果每个控制器都招一个小弟，那我得多发多少人的工资啊，你想想办法。”\n3. SharedInformer # 经过和其他控制器的讨论，我们决定这么做：所有控制器联合起来作为一个整体来分配 Informer，针对每个（受多个控制器管理的）资源招一个 Informer 小弟，我们称之为 SharedInformer。你们可以理解为共享 Informer，因为有很多资源是受多个控制器管理的，比如 Pod 同时受 Deployment 和 StatefulSet 管理。这样当多个控制器同时想查 Pod 的状态时，只需要访问一个 Informer 就行了。\n但这又引来了新的问题，SharedInformer 无法同时给多个控制器提供信息，这就需要每个控制器自己排队和重试。\n为了配合控制器更好地实现排队和重试，SharedInformer 搞了一个 Delta FIFO Queue（增量先进先出队列），每当资源被修改时，它的助手 Reflector 就会收到事件通知，并将对应的事件放入 Delta FIFO Queue 中。与此同时，SharedInformer 会不断从 Delta FIFO Queue 中读取事件，然后更新本地缓存的状态。\n这还不行，SharedInformer 除了更新本地缓存之外，还要想办法将数据同步给各个控制器，为了解决这个问题，它又搞了个工作队列（Workqueue），一旦有资源被添加、修改或删除，就会将相应的事件加入到工作队列中。所有的控制器排队进行读取，一旦某个控制器发现这个事件与自己相关，就执行相应的操作。如果操作失败，就将该事件放回队列，等下次排到自己再试一次。如果操作成功，就将该事件从队列中删除。\n现在这个工作模式得到了大家的一致好评。虽然单个 SharedInformer 的工作量增加了，但 Informer 的数量大大减少了，老大可以把省下来的资金拿出一小部分给 SharedInformer 涨工资啊，这样大家都很开心。\n4. CRD # 全民 Kubernetes 时代到了。\n随着容器及其编排技术的普及，使用 Kubernetes 的用户大量增长，用户已经不满足 Kubernetes 自带的那些资源（Pod，Node，Service）了，大家都希望能根据具体的业务创建特定的资源，并且对这些资源的状态维护还要遵循上面所说的那一套控制循环机制。\n幸好最近掌门人做了一次升级，新增了一个插件叫 CRD（Custom Resource Definition），创建一个全新的资源实例，只需要经过以下两步：\n创建一个 CRD 资源（没错，CRD 也是一种资源类型），其中定义”自定义资源“的 API 组、API 版本和资源类型。这样就会向 API Server 注册该资源类型的 API。 指定上面定义的 API 组 和 API 版本，创建自定义资源。 当然，中间还要加入一些代码让 Kubernetes 认识自定义资源的各种参数。\n到这一步就基本上完成了自定义资源的创建，但 Kubernetes 并不知道该资源所对应的业务逻辑，比如你的自定义资源是宿主机，那么对应的业务逻辑就是创建一台真正的宿主机出来。那么怎样实现它的业务逻辑呢？\n5. 自定义控制器 # Controller Manager 见多识广，说：”这里的每个控制器都是我的一部分，当初创造你们是因为你们都属于通用的控制器，大家都能用得上。而自定义资源需要根据具体的业务来实现，我们不可能知道每个用户的具体业务是啥，自己一拍脑袋想出来的自定义资源，用户也不一定用得上。我们可以让用户自己编写自定义控制器，你们把之前使用的控制循环和 Informer 这些编码模式总结一下，然后提供给用户，让他们按照同样的方法编写自己的控制器。“\nDeployment 控制器一惊，要把自己的秘密告诉别人？那别人把自己取代了咋办？赶忙问道：”那将来我岂不是很危险，没有存在的余地了？“\nController Manager 赶忙解释道：”不用担心，虽然用户可以编写自定义控制器，但无论他们玩出什么花样，只要他们的业务跑在 Kubernetes 平台上，就免不了要跑容器，最后还是会来求你们帮忙的，你要知道，控制器是可以层层递进的，他们只不过是在你外面套了一层，最后还是要回到你这里，请求你帮忙控制 Pod。“\n这下大家都不慌了，决定就把自定义控制器这件事情交给用户自己去处理，将选择权留给用户。\n6. Operator # 用户自从获得了编写自定义控制器的权力之后，非常开心，有的用户（CoreOS）为了方便大家控制有状态应用，开发出了一种特定的控制器模型叫 Operator，并开始在社区内推广，得到了大家的一致好评。不可否认，Operator 这种模式是很聪明的，它把需要特定领域知识的应用单独写一个 Operator 控制器，将这种应用特定的操作知识编写到软件中，使其可以利用 Kubernetes 强大的抽象能力，达到正确运行和管理应用的目的。\n以 ETCD Operator 为例，假如你想手动扩展一个 ETCD 集群，一般的做法是：\n使用 ETCD 管理工具添加一个新成员。 为这个成员所在的节点生成对应的启动参数，并启动它。 而 ETCD Operator 将这些特定于 etcd 的操作手法编写到了它的控制循环中，你只需要通过修改自定义资源声明集群期望的成员数量，剩下的事情交给 Operator 就好了。\n本以为这是一个皆大欢喜的方案，但没过多久，就有开发 Operator 的小哥来抱怨了：“我们有很多开发的小伙伴都是不懂运维那一套的，什么高可用、容灾根本不懂啊，现在让我们将运维的操作知识编写到软件中，臣妾做不到啊。。”\n这确实是个问题，这样一来就把开发和运维的工作都塞到了开发手里，既懂开发又懂运维的可不多啊，为了照顾大家，还得继续想办法把开发和运维的工作拆分开来。\n7. OAM # 这时候阿里和微软发力了，他们联合发布了一个开放应用模型，叫 Open Application Model （OAM）。这个模型就是为了解决上面提到的问题，将开发和运维的职责解耦，不同的角色履行不同的职责，并形成一个统一的规范，如下图所示：\n这个规范告诉我们：\n开发人员负责描述组件的功能，如何配置组件，以及运行需要多少资源 运维人员负责将相关组件组合成一个应用，并配置运行时参数和运维支撑能力，比如是否需要监控，是否需要弹性伸缩。 基础设施工程师负责建立和维护应用的运行时环境（如底层系统）。 其中每一个团队负责的事情都用对应的 CRD 来配置。\n这样一来，开发和运维人员的职责就被区分开来了，简化了应用的组合和运维。它将应用的配置和运维特征（如自动伸缩、流量监控）进行解耦，然后通过建模构成一个整体，避免了 Operator 这种模型带来的大量冗余。\n自从用上了这个模型之后，运维和开发小哥表示现在他们的关系很融洽，没事还能一起出去喝两杯。\n","date":"2020年2月22日","externalUrl":null,"permalink":"/posts/controllers-confession/","section":"博客","summary":"我是一堆 K8s 控制器。 你可能会疑惑为什么是一堆，因为我不是一个人","title":"K8s 控制器的进化之旅","type":"posts"},{"content":"","date":"2020年2月22日","externalUrl":null,"permalink":"/tags/operator/","section":"标签","summary":"","title":"Operator","type":"tags"},{"content":"","date":"2020年2月10日","externalUrl":null,"permalink":"/tags/tcpdump/","section":"标签","summary":"","title":"Tcpdump","type":"tags"},{"content":" 本文主要内容翻译自 《Tcpdump Examples》。\ntcpdump 是一款强大的网络抓包工具，它使用 libpcap 库来抓取网络数据包，这个库在几乎在所有的 Linux/Unix 中都有。熟悉 tcpdump 的使用能够帮助你分析调试网络数据，本文将通过一个个具体的示例来介绍它在不同场景下的使用方法。不管你是系统管理员，程序员，云原生工程师还是 yaml 工程师，掌握 tcpdump 的使用都能让你如虎添翼，升职加薪。\n1. 基本语法和使用方法 # tcpdump 的常用参数如下：\n$ tcpdump -i eth0 -nn -s0 -v port 80 -i : 选择要捕获的接口，通常是以太网卡或无线网卡，也可以是 vlan 或其他特殊接口。如果该系统上只有一个网络接口，则无需指定。 -nn : 单个 n 表示不解析域名，直接显示 IP；两个 n 表示不解析域名和端口。这样不仅方便查看 IP 和端口号，而且在抓取大量数据时非常高效，因为域名解析会降低抓取速度。 -s0 : tcpdump 默认只会截取前 96 字节的内容，要想截取所有的报文内容，可以使用 -s number， number 就是你要截取的报文字节数，如果是 0 的话，表示截取报文全部内容。 -v : 使用 -v，-vv 和 -vvv 来显示更多的详细信息，通常会显示更多与特定协议相关的信息。 port 80 : 这是一个常见的端口过滤器，表示仅抓取 80 端口上的流量，通常是 HTTP。 额外再介绍几个常用参数：\n-p : 不让网络接口进入混杂模式。默认情况下使用 tcpdump 抓包时，会让网络接口进入混杂模式。一般计算机网卡都工作在非混杂模式下，此时网卡只接受来自网络端口的目的地址指向自己的数据。当网卡工作在混杂模式下时，网卡将来自接口的所有数据都捕获并交给相应的驱动程序。如果设备接入的交换机开启了混杂模式，使用 -p 选项可以有效地过滤噪声。 -e : 显示数据链路层信息。默认情况下 tcpdump 不会显示数据链路层信息，使用 -e 选项可以显示源和目的 MAC 地址，以及 VLAN tag 信息。例如： $ tcpdump -n -e -c 5 not ip6 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on br-lan, link-type EN10MB (Ethernet), capture size 262144 bytes 18:27:53.619865 24:5e:be:0c:17:af \u0026gt; 00:e2:69:23:d3:3b, ethertype IPv4 (0x0800), length 1162: 192.168.100.20.51410 \u0026gt; 180.176.26.193.58695: Flags [.], seq 2045333376:2045334484, ack 3398690514, win 751, length 1108 18:27:53.626490 00:e2:69:23:d3:3b \u0026gt; 24:5e:be:0c:17:af, ethertype IPv4 (0x0800), length 68: 220.173.179.66.36017 \u0026gt; 192.168.100.20.51410: UDP, length 26 18:27:53.626893 24:5e:be:0c:17:af \u0026gt; 00:e2:69:23:d3:3b, ethertype IPv4 (0x0800), length 1444: 192.168.100.20.51410 \u0026gt; 220.173.179.66.36017: UDP, length 1402 18:27:53.628837 00:e2:69:23:d3:3b \u0026gt; 24:5e:be:0c:17:af, ethertype IPv4 (0x0800), length 1324: 46.97.169.182.6881 \u0026gt; 192.168.100.20.59145: Flags [P.], seq 3058450381:3058451651, ack 14349180, win 502, length 1270 18:27:53.629096 24:5e:be:0c:17:af \u0026gt; 00:e2:69:23:d3:3b, ethertype IPv4 (0x0800), length 54: 192.168.100.20.59145 \u0026gt; 192.168.100.1.12345: Flags [.], ack 3058451651, win 6350, length 0 5 packets captured 显示 ASCII 字符串 # -A 表示使用 ASCII 字符串打印报文的全部数据，这样可以使读取更加简单，方便使用 grep 等工具解析输出内容。-X 表示同时使用十六进制和 ASCII 字符串打印报文的全部数据。这两个参数不能一起使用。例如：\n$ tcpdump -A -s0 port 80 抓取特定协议的数据 # 后面可以跟上协议名称来过滤特定协议的流量，以 UDP 为例，可以加上参数 udp 或 protocol 17，这两个命令意思相同。\n$ tcpdump -i eth0 udp $ tcpdump -i eth0 proto 17 同理，tcp 与 protocol 6 意思相同。\n抓取特定主机的数据 # 使用过滤器 host 可以抓取特定目的地和源 IP 地址的流量。\n$ tcpdump -i eth0 host 10.10.1.1 也可以使用 src 或 dst 只抓取源或目的地：\n$ tcpdump -i eth0 dst 10.10.1.20 将抓取的数据写入文件 # 使用 tcpdump 截取数据报文的时候，默认会打印到屏幕的默认输出，你会看到按照顺序和格式，很多的数据一行行快速闪过，根本来不及看清楚所有的内容。不过，tcpdump 提供了把截取的数据保存到文件的功能，以便后面使用其他图形工具（比如 wireshark，Snort）来分析。\n-w 选项用来把数据报文输出到文件：\n$ tcpdump -i eth0 -s0 -w test.pcap 行缓冲模式 # 如果想实时将抓取到的数据通过管道传递给其他工具来处理，需要使用 -l 选项来开启行缓冲模式（或使用 -c 选项来开启数据包缓冲模式）。使用 -l 选项可以将输出通过立即发送给其他命令，其他命令会立即响应。\n$ tcpdump -i eth0 -s0 -l port 80 | grep \u0026#39;Server:\u0026#39; 组合过滤器 # 过滤的真正强大之处在于你可以随意组合它们，而连接它们的逻辑就是常用的 与/AND/\u0026amp;\u0026amp; 、 或/OR/|| 和 非/not/!。\nand or \u0026amp;\u0026amp; or or || not or ! 2. 过滤器 # 关于 tcpdump 的过滤器，这里有必要单独介绍一下。\n机器上的网络报文数量异常的多，很多时候我们只关系和具体问题有关的数据报（比如访问某个网站的数据，或者 icmp 超时的报文等等），而这些数据只占到很小的一部分。把所有的数据截取下来，从里面找到想要的信息无疑是一件很费时费力的工作。而 tcpdump 提供了灵活的语法可以精确地截取关心的数据报，简化分析的工作量。这些选择数据包的语句就是过滤器（filter）！\nHost 过滤器 # Host 过滤器用来过滤某个主机的数据报文。例如：\n$ tcpdump host 1.2.3.4 该命令会抓取所有发往主机 1.2.3.4 或者从主机 1.2.3.4 发出的流量。如果想只抓取从该主机发出的流量，可以使用下面的命令：\n$ tcpdump src host 1.2.3.4 Network 过滤器 # Network 过滤器用来过滤某个网段的数据，使用的是 CIDR 模式。可以使用四元组（x.x.x.x）、三元组（x.x.x）、二元组（x.x）和一元组（x）。四元组就是指定某个主机，三元组表示子网掩码为 255.255.255.0，二元组表示子网掩码为 255.255.0.0，一元组表示子网掩码为 255.0.0.0。例如，\n抓取所有发往网段 192.168.1.x 或从网段 192.168.1.x 发出的流量：\n$ tcpdump net 192.168.1 抓取所有发往网段 10.x.x.x 或从网段 10.x.x.x 发出的流量：\n$ tcpdump net 10 和 Host 过滤器一样，这里也可以指定源和目的：\n$ tcpdump src net 10 也可以使用 CIDR 格式：\n$ tcpdump src net 172.16.0.0/12 Proto 过滤器 # Proto 过滤器用来过滤某个协议的数据，关键字为 proto，可省略。proto 后面可以跟上协议号或协议名称，支持 icmp, igmp, igrp, pim, ah, esp, carp, vrrp, udp 和 tcp。因为通常的协议名称是保留字段，所以在于 proto 指令一起使用时，必须根据 shell 类型使用一个或两个反斜杠（/）来转义。Linux 中的 shell 需要使用两个反斜杠来转义，MacOS 只需要一个。\n例如，抓取 icmp 协议的报文：\n$ tcpdump -n proto \\\\icmp # 或者 $ tcpdump -n icmp Port 过滤器 # Port 过滤器用来过滤通过某个端口的数据报文，关键字为 port。例如：\n$ tcpdump port 389 3. 理解 tcpdump 的输出 # 截取数据只是第一步，第二步就是理解这些数据，下面就解释一下 tcpdump 命令输出各部分的意义。\n21:27:06.995846 IP (tos 0x0, ttl 64, id 45646, offset 0, flags [DF], proto TCP (6), length 64) 192.168.1.106.56166 \u0026gt; 124.192.132.54.80: Flags [S], cksum 0xa730 (correct), seq 992042666, win 65535, options [mss 1460,nop,wscale 4,nop,nop,TS val 663433143 ecr 0,sackOK,eol], length 0 21:27:07.030487 IP (tos 0x0, ttl 51, id 0, offset 0, flags [DF], proto TCP (6), length 44) 124.192.132.54.80 \u0026gt; 192.168.1.106.56166: Flags [S.], cksum 0xedc0 (correct), seq 2147006684, ack 992042667, win 14600, options [mss 1440], length 0 21:27:07.030527 IP (tos 0x0, ttl 64, id 59119, offset 0, flags [DF], proto TCP (6), length 40) 192.168.1.106.56166 \u0026gt; 124.192.132.54.80: Flags [.], cksum 0x3e72 (correct), ack 2147006685, win 65535, length 0 最基本也是最重要的信息就是数据报的源地址/端口和目的地址/端口，上面的例子第一条数据报中，源地址 ip 是 192.168.1.106，源端口是 56166，目的地址是 124.192.132.54，目的端口是 80。 \u0026gt; 符号代表数据的方向。\n此外，上面的三条数据还是 tcp 协议的三次握手过程，第一条就是 SYN 报文，这个可以通过 Flags [S] 看出。下面是常见的 TCP 报文的 Flags:\n[S] : SYN（开始连接） [.] : 没有 Flag [P] : PSH（推送数据） [F] : FIN （结束连接） [R] : RST（重置连接） 而第二条数据的 [S.] 表示 SYN-ACK，就是 SYN 报文的应答报文。\n4. 例子 # 下面给出一些具体的例子，每个例子都可以使用多种方法来获得相同的输出，你使用的方法取决于所需的输出和网络上的流量。我们在排障时，通常只想获取自己想要的内容，可以通过过滤器和 ASCII 输出并结合管道与 grep、cut、awk 等工具来实现此目的。\n例如，在抓取 HTTP 请求和响应数据包时，可以通过删除标志 SYN/ACK/FIN 来过滤噪声，但还有更简单的方法，那就是通过管道传递给 grep。在达到目的的同时，我们要选择最简单最高效的方法。下面来看例子。\n提取 HTTP 用户代理 # 从 HTTP 请求头中提取 HTTP 用户代理：\n$ tcpdump -nn -A -s1500 -l | grep \u0026#34;User-Agent:\u0026#34; 通过 egrep 可以同时提取用户代理和主机名（或其他头文件）：\n$ tcpdump -nn -A -s1500 -l | egrep -i \u0026#39;User-Agent:|Host:\u0026#39; 只抓取 HTTP GET 和 POST 流量 # 抓取 HTTP GET 流量：\n$ tcpdump -s 0 -A -vv \u0026#39;tcp[((tcp[12:1] \u0026amp; 0xf0) \u0026gt;\u0026gt; 2):4] = 0x47455420\u0026#39; 也可以抓取 HTTP POST 请求流量：\n$ tcpdump -s 0 -A -vv \u0026#39;tcp[((tcp[12:1] \u0026amp; 0xf0) \u0026gt;\u0026gt; 2):4] = 0x504f5354\u0026#39; 注意：该方法不能保证抓取到 HTTP POST 有效数据流量，因为一个 POST 请求会被分割为多个 TCP 数据包。\n上述两个表达式中的十六进制将会与 GET 和 POST 请求的 ASCII 字符串匹配。例如，tcp[((tcp[12:1] \u0026amp; 0xf0) \u0026gt;\u0026gt; 2):4] 首先会 确定我们感兴趣的字节的位置（在 TCP header 之后），然后选择我们希望匹配的 4 个字节。\n提取 HTTP 请求的 URL # 提取 HTTP 请求的主机名和路径：\n$ tcpdump -s 0 -v -n -l | egrep -i \u0026#34;POST /|GET /|Host:\u0026#34; tcpdump: listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes POST /wp-login.php HTTP/1.1 Host: dev.example.com GET /wp-login.php HTTP/1.1 Host: dev.example.com GET /favicon.ico HTTP/1.1 Host: dev.example.com GET / HTTP/1.1 Host: dev.example.com 提取 HTTP POST 请求中的密码 # 从 HTTP POST 请求中提取密码和主机名：\n$ tcpdump -s 0 -A -n -l | egrep -i \u0026#34;POST /|pwd=|passwd=|password=|Host:\u0026#34; tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes 11:25:54.799014 IP 10.10.1.30.39224 \u0026gt; 10.10.1.125.80: Flags [P.], seq 1458768667:1458770008, ack 2440130792, win 704, options [nop,nop,TS val 461552632 ecr 208900561], length 1341: HTTP: POST /wp-login.php HTTP/1.1 .....s..POST /wp-login.php HTTP/1.1 Host: dev.example.com .....s..log=admin\u0026amp;pwd=notmypassword\u0026amp;wp-submit=Log+In\u0026amp;redirect_to=http%3A%2F%2Fdev.example.com%2Fwp-admin%2F\u0026amp;testcookie=1 提取 Cookies # 提取 Set-Cookie（服务端的 Cookie）和 Cookie（客户端的 Cookie）：\n$ tcpdump -nn -A -s0 -l | egrep -i \u0026#39;Set-Cookie|Host:|Cookie:\u0026#39; tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on wlp58s0, link-type EN10MB (Ethernet), capture size 262144 bytes Host: dev.example.com Cookie: wordpress_86be02xxxxxxxxxxxxxxxxxxxc43=admin%7C152xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxfb3e15c744fdd6; _ga=GA1.2.21343434343421934; _gid=GA1.2.927343434349426; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_86be654654645645645654645653fc43=admin%7C15275102testtesttesttestab7a61e; wp-settings-time-1=1527337439 抓取 ICMP 数据包 # 查看网络上的所有 ICMP 数据包：\n$ tcpdump -n icmp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes 11:34:21.590380 IP 10.10.1.217 \u0026gt; 10.10.1.30: ICMP echo request, id 27948, seq 1, length 64 11:34:21.590434 IP 10.10.1.30 \u0026gt; 10.10.1.217: ICMP echo reply, id 27948, seq 1, length 64 11:34:27.680307 IP 10.10.1.159 \u0026gt; 10.10.1.1: ICMP 10.10.1.189 udp port 59619 unreachable, length 115 抓取非 ECHO/REPLY 类型的 ICMP 数据包 # 通过排除 echo 和 reply 类型的数据包使抓取到的数据包不包括标准的 ping 包：\n$ tcpdump \u0026#39;icmp[icmptype] != icmp-echo and icmp[icmptype] != icmp-echoreply\u0026#39; tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes 11:37:04.041037 IP 10.10.1.189 \u0026gt; 10.10.1.20: ICMP 10.10.1.189 udp port 36078 unreachable, length 156 抓取 SMTP/POP3 协议的邮件 # 可以提取电子邮件的正文和其他数据。例如，只提取电子邮件的收件人：\n$ tcpdump -nn -l port 25 | grep -i \u0026#39;MAIL FROM\\|RCPT TO\u0026#39; 抓取 NTP 服务的查询和响应 # $ tcpdump dst port 123 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 21:02:19.112502 IP test33.ntp \u0026gt; 199.30.140.74.ntp: NTPv4, Client, length 48 21:02:19.113888 IP 216.239.35.0.ntp \u0026gt; test33.ntp: NTPv4, Server, length 48 21:02:20.150347 IP test33.ntp \u0026gt; 216.239.35.0.ntp: NTPv4, Client, length 48 21:02:20.150991 IP 216.239.35.0.ntp \u0026gt; test33.ntp: NTPv4, Server, length 48 抓取 SNMP 服务的查询和响应 # 通过 SNMP 服务，渗透测试人员可以获取大量的设备和系统信息。在这些信息中，系统信息最为关键，如操作系统版本、内核版本等。使用 SNMP 协议快速扫描程序 onesixtyone，可以看到目标系统的信息：\n$ onesixtyone 10.10.1.10 public Scanning 1 hosts, 1 communities 10.10.1.10 [public] Linux test33 4.15.0-20-generic #21-Ubuntu SMP Tue Apr 24 06:16:15 UTC 2018 x86_64 可以通过 tcpdump 抓取 GetRequest 和 GetResponse：\n$ tcpdump -n -s0 port 161 and udp tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on wlp58s0, link-type EN10MB (Ethernet), capture size 262144 bytes 23:39:13.725522 IP 10.10.1.159.36826 \u0026gt; 10.10.1.20.161: GetRequest(28) .1.3.6.1.2.1.1.1.0 23:39:13.728789 IP 10.10.1.20.161 \u0026gt; 10.10.1.159.36826: GetResponse(109) .1.3.6.1.2.1.1.1.0=\u0026#34;Linux testmachine 4.15.0-20-generic #21-Ubuntu SMP Tue Apr 24 06:16:15 UTC 2018 x86_64\u0026#34; 切割 pcap 文件 # 当抓取大量数据并写入文件时，可以自动切割为多个大小相同的文件。例如，下面的命令表示每 3600 秒创建一个新文件 capture-(hour).pcap，每个文件大小不超过 200*1000000 字节：\n$ tcpdump -w /tmp/capture-%H.pcap -G 3600 -C 200 这些文件的命名为 capture-{1-24}.pcap，24 小时之后，之前的文件就会被覆盖。\n抓取 IPv6 流量 # 可以通过过滤器 ip6 来抓取 IPv6 流量，同时可以指定协议如 TCP：\n$ tcpdump -nn ip6 proto 6 从之前保存的文件中读取 IPv6 UDP 数据报文：\n$ tcpdump -nr ipv6-test.pcap ip6 proto 17 检测端口扫描 # 在下面的例子中，你会发现抓取到的报文的源和目的一直不变，且带有标志位 [S] 和 [R]，它们与一系列看似随机的目标端口进行匹配。当发送 SYN 之后，如果目标主机的端口没有打开，就会返回一个 RESET。这是 Nmap 等端口扫描工具的标准做法。\n$ tcpdump -nn 21:46:19.693601 IP 10.10.1.10.60460 \u0026gt; 10.10.1.199.5432: Flags [S], seq 116466344, win 29200, options [mss 1460,sackOK,TS val 3547090332 ecr 0,nop,wscale 7], length 0 21:46:19.693626 IP 10.10.1.10.35470 \u0026gt; 10.10.1.199.513: Flags [S], seq 3400074709, win 29200, options [mss 1460,sackOK,TS val 3547090332 ecr 0,nop,wscale 7], length 0 21:46:19.693762 IP 10.10.1.10.44244 \u0026gt; 10.10.1.199.389: Flags [S], seq 2214070267, win 29200, options [mss 1460,sackOK,TS val 3547090333 ecr 0,nop,wscale 7], length 0 21:46:19.693772 IP 10.10.1.199.389 \u0026gt; 10.10.1.10.44244: Flags [R.], seq 0, ack 2214070268, win 0, length 0 21:46:19.693783 IP 10.10.1.10.35172 \u0026gt; 10.10.1.199.1433: Flags [S], seq 2358257571, win 29200, options [mss 1460,sackOK,TS val 3547090333 ecr 0,nop,wscale 7], length 0 21:46:19.693826 IP 10.10.1.10.33022 \u0026gt; 10.10.1.199.49153: Flags [S], seq 2406028551, win 29200, options [mss 1460,sackOK,TS val 3547090333 ecr 0,nop,wscale 7], length 0 21:46:19.695567 IP 10.10.1.10.55130 \u0026gt; 10.10.1.199.49154: Flags [S], seq 3230403372, win 29200, options [mss 1460,sackOK,TS val 3547090334 ecr 0,nop,wscale 7], length 0 21:46:19.695590 IP 10.10.1.199.49154 \u0026gt; 10.10.1.10.55130: Flags [R.], seq 0, ack 3230403373, win 0, length 0 21:46:19.695608 IP 10.10.1.10.33460 \u0026gt; 10.10.1.199.49152: Flags [S], seq 3289070068, win 29200, options [mss 1460,sackOK,TS val 3547090335 ecr 0,nop,wscale 7], length 0 21:46:19.695622 IP 10.10.1.199.49152 \u0026gt; 10.10.1.10.33460: Flags [R.], seq 0, ack 3289070069, win 0, length 0 21:46:19.695637 IP 10.10.1.10.34940 \u0026gt; 10.10.1.199.1029: Flags [S], seq 140319147, win 29200, options [mss 1460,sackOK,TS val 3547090335 ecr 0,nop,wscale 7], length 0 21:46:19.695650 IP 10.10.1.199.1029 \u0026gt; 10.10.1.10.34940: Flags [R.], seq 0, ack 140319148, win 0, length 0 21:46:19.695664 IP 10.10.1.10.45648 \u0026gt; 10.10.1.199.5060: Flags [S], seq 2203629201, win 29200, options [mss 1460,sackOK,TS val 3547090335 ecr 0,nop,wscale 7], length 0 21:46:19.695775 IP 10.10.1.10.49028 \u0026gt; 10.10.1.199.2000: Flags [S], seq 635990431, win 29200, options [mss 1460,sackOK,TS val 3547090335 ecr 0,nop,wscale 7], length 0 21:46:19.695790 IP 10.10.1.199.2000 \u0026gt; 10.10.1.10.49028: Flags [R.], seq 0, ack 635990432, win 0, length 0 过滤 Nmap NSE 脚本测试结果 # 本例中 Nmap NSE 测试脚本 http-enum.nse 用来检测 HTTP 服务的合法 URL。\n在执行脚本测试的主机上：\n$ nmap -p 80 --script=http-enum.nse targetip 在目标主机上：\n$ tcpdump -nn port 80 | grep \u0026#34;GET /\u0026#34; GET /w3perl/ HTTP/1.1 GET /w-agora/ HTTP/1.1 GET /way-board/ HTTP/1.1 GET /web800fo/ HTTP/1.1 GET /webaccess/ HTTP/1.1 GET /webadmin/ HTTP/1.1 GET /webAdmin/ HTTP/1.1 抓取 DNS 请求和响应 # 向 Google 公共 DNS 发起的出站 DNS 请求和 A 记录响应可以通过 tcpdump 抓取到：\n$ tcpdump -i wlp58s0 -s0 port 53 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on wlp58s0, link-type EN10MB (Ethernet), capture size 262144 bytes 14:19:06.879799 IP test.53852 \u0026gt; google-public-dns-a.google.com.domain: 26977+ [1au] A? play.google.com. (44) 14:19:07.022618 IP google-public-dns-a.google.com.domain \u0026gt; test.53852: 26977 1/0/1 A 216.58.203.110 (60) 抓取 HTTP 有效数据包 # 抓取 80 端口的 HTTP 有效数据包，排除 TCP 连接建立过程的数据包（SYN / FIN / ACK）：\n$ tcpdump \u0026#39;tcp port 80 and (((ip[2:2] - ((ip[0]\u0026amp;0xf)\u0026lt;\u0026lt;2)) - ((tcp[12]\u0026amp;0xf0)\u0026gt;\u0026gt;2)) != 0)\u0026#39; 将输出内容重定向到 Wireshark # 通常 Wireshark（或 tshark）比 tcpdump 更容易分析应用层协议。一般的做法是在远程服务器上先使用 tcpdump 抓取数据并写入文件，然后再将文件拷贝到本地工作站上用 Wireshark 分析。\n还有一种更高效的方法，可以通过 ssh 连接将抓取到的数据实时发送给 Wireshark 进行分析。以 MacOS 系统为例，可以通过 brew cask install wireshark 来安装，然后通过下面的命令来分析：\n$ ssh root@remotesystem \u0026#39;tcpdump -s0 -c 1000 -nn -w - not port 22\u0026#39; | /Applications/Wireshark.app/Contents/MacOS/Wireshark -k -i - 例如，如果想分析 DNS 协议，可以使用下面的命令：\n$ ssh root@remotesystem \u0026#39;tcpdump -s0 -c 1000 -nn -w - port 53\u0026#39; | /Applications/Wireshark.app/Contents/MacOS/Wireshark -k -i - 抓取到的数据：\n-c 选项用来限制抓取数据的大小。如果不限制大小，就只能通过 ctrl-c 来停止抓取，这样一来不仅关闭了 tcpdump，也关闭了 wireshark。\n找出发包最多的 IP # 找出一段时间内发包最多的 IP，或者从一堆报文中找出发包最多的 IP，可以使用下面的命令：\n$ tcpdump -nnn -t -c 200 | cut -f 1,2,3,4 -d \u0026#39;.\u0026#39; | sort | uniq -c | sort -nr | head -n 20 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes 200 packets captured 261 packets received by filter 0 packets dropped by kernel 108 IP 10.10.211.181 91 IP 10.10.1.30 1 IP 10.10.1.50 cut -f 1,2,3,4 -d \u0026lsquo;.\u0026rsquo; : 以 . 为分隔符，打印出每行的前四列。即 IP 地址。 sort | uniq -c : 排序并计数 sort -nr : 按照数值大小逆向排序 抓取用户名和密码 # 本例将重点放在标准纯文本协议上，过滤出于用户名和密码相关的报文：\n$ tcpdump port http or port ftp or port smtp or port imap or port pop3 or port telnet -l -A | egrep -i -B5 \u0026#39;pass=|pwd=|log=|login=|user=|username=|pw=|passw=|passwd=|password=|pass:|user:|username:|password:|login:|pass |user \u0026#39; 抓取 DHCP 报文 # 最后一个例子，抓取 DHCP 服务的请求和响应报文，67 为 DHCP 端口，68 为客户机端口。\n$ tcpdump -v -n port 67 or 68 tcpdump: listening on enp7s0, link-type EN10MB (Ethernet), capture size 262144 bytes 14:37:50.059662 IP (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328) 0.0.0.0.68 \u0026gt; 255.255.255.255.67: BOOTP/DHCP, Request from 00:0c:xx:xx:xx:d5, length 300, xid 0xc9779c2a, Flags [none] Client-Ethernet-Address 00:0c:xx:xx:xx:d5 Vendor-rfc1048 Extensions Magic Cookie 0x63825363 DHCP-Message Option 53, length 1: Request Requested-IP Option 50, length 4: 10.10.1.163 Hostname Option 12, length 14: \u0026#34;test-ubuntu\u0026#34; Parameter-Request Option 55, length 16: Subnet-Mask, BR, Time-Zone, Default-Gateway Domain-Name, Domain-Name-Server, Option 119, Hostname Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route NTP, Classless-Static-Route-Microsoft, Static-Route, Option 252 14:37:50.059667 IP (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328) 0.0.0.0.68 \u0026gt; 255.255.255.255.67: BOOTP/DHCP, Request from 00:0c:xx:xx:xx:d5, length 300, xid 0xc9779c2a, Flags [none] Client-Ethernet-Address 00:0c:xx:xx:xx:d5 Vendor-rfc1048 Extensions Magic Cookie 0x63825363 DHCP-Message Option 53, length 1: Request Requested-IP Option 50, length 4: 10.10.1.163 Hostname Option 12, length 14: \u0026#34;test-ubuntu\u0026#34; Parameter-Request Option 55, length 16: Subnet-Mask, BR, Time-Zone, Default-Gateway Domain-Name, Domain-Name-Server, Option 119, Hostname Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route NTP, Classless-Static-Route-Microsoft, Static-Route, Option 252 14:37:50.060780 IP (tos 0x0, ttl 64, id 53564, offset 0, flags [none], proto UDP (17), length 339) 10.10.1.1.67 \u0026gt; 10.10.1.163.68: BOOTP/DHCP, Reply, length 311, xid 0xc9779c2a, Flags [none] Your-IP 10.10.1.163 Server-IP 10.10.1.1 Client-Ethernet-Address 00:0c:xx:xx:xx:d5 Vendor-rfc1048 Extensions Magic Cookie 0x63825363 DHCP-Message Option 53, length 1: ACK Server-ID Option 54, length 4: 10.10.1.1 Lease-Time Option 51, length 4: 86400 RN Option 58, length 4: 43200 RB Option 59, length 4: 75600 Subnet-Mask Option 1, length 4: 255.255.255.0 BR Option 28, length 4: 10.10.1.255 Domain-Name-Server Option 6, length 4: 10.10.1.1 Hostname Option 12, length 14: \u0026#34;test-ubuntu\u0026#34; T252 Option 252, length 1: 10 Default-Gateway Option 3, length 4: 10.10.1.1 5. 总结 # 本文主要介绍了 tcpdump 的基本语法和使用方法，并通过一些示例来展示它强大的过滤功能。将 tcpdump 与 wireshark 进行组合可以发挥更强大的功效，本文也展示了如何优雅顺滑地结合 tcpdump 和 wireshark。如果你想了解更多的细节，可以查看 tcpdump 的 man 手册。\n","date":"2020年2月10日","externalUrl":null,"permalink":"/posts/tcpdump-examples/","section":"博客","summary":"本文主要内容翻译自 《Tcpdump Examples》。 tcpdump 是一","title":"Tcpdump 使用教程","type":"posts"},{"content":"Linux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。用官方的话来说，Linux Namespace 将全局系统资源封装在一个抽象中，从而使 namespace 内的进程认为自己具有独立的资源实例。这项技术本来没有掀起多大的波澜，是容器技术的崛起让他重新引起了大家的注意。\nLinux Namespace 有如下 6 个种类：\n分类 系统调用参数 相关内核版本 Mount namespaces CLONE_NEWNS Linux 2.4.19 UTS namespaces CLONE_NEWUTS Linux 2.6.19 IPC namespaces CLONE_NEWIPC Linux 2.6.19 PID namespaces CLONE_NEWPID Linux 2.6.24 Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29 User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8 namespace 的 API 由三个系统调用和一系列 /proc 文件组成，本文将会详细介绍这些系统调用和 /proc 文件。为了指定要操作的 namespace 类型，需要在系统调用的 flag 中通过常量 CLONE_NEW* 指定（包括 CLONE_NEWIPC，CLONE_NEWNS， CLONE_NEWNET，CLONE_NEWPID，CLONE_NEWUSER 和 CLONE_NEWUTS），可以指定多个常量，通过 |（位或）操作来实现。\n简单描述一下三个系统调用的功能：\nclone() : 实现线程的系统调用，用来创建一个新的进程，并可以通过设计上述系统调用参数达到隔离的目的。 unshare() : 使某进程脱离某个 namespace。 setns() : 把某进程加入到某个 namespace。 具体的实现原理请往下看。\n1. clone() # clone() 的原型如下：\nint clone(int (*child_func)(void *), void *child_stack, int flags, void *arg); child_func : 传入子进程运行的程序主函数。 child_stack : 传入子进程使用的栈空间。 flags : 表示使用哪些 CLONE_* 标志位。 args : 用于传入用户参数。 clone() 与 fork() 类似，都相当于把当前进程复制了一份，但 clone() 可以更细粒度地控制与子进程共享的资源（其实就是通过 flags 来控制），包括虚拟内存、打开的文件描述符和信号量等等。一旦指定了标志位 CLONE_NEW*，相对应类型的 namespace 就会被创建，新创建的进程也会成为该 namespace 中的一员。\nclone() 的原型并不是最底层的系统调用，而是封装过的，真正的系统调用内核实现函数为 do_fork()，形式如下：\nlong do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) 其中 clone_flags 可以赋值为上面提到的标志。\n下面来看一个例子：\n/* demo_uts_namespaces.c Copyright 2013, Michael Kerrisk Licensed under GNU General Public License v2 or later Demonstrate the operation of UTS namespaces. */ #define _GNU_SOURCE #include \u0026lt;sys/wait.h\u0026gt; #include \u0026lt;sys/utsname.h\u0026gt; #include \u0026lt;sched.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; /* A simple error-handling function: print an error message based on the value in \u0026#39;errno\u0026#39; and terminate the calling process */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \\ } while (0) static int /* Start function for cloned child */ childFunc(void *arg) { struct utsname uts; /* 在新的 UTS namespace 中修改主机名 */ if (sethostname(arg, strlen(arg)) == -1) errExit(\u0026#34;sethostname\u0026#34;); /* 获取并显示主机名 */ if (uname(\u0026amp;uts) == -1) errExit(\u0026#34;uname\u0026#34;); printf(\u0026#34;uts.nodename in child: %s\\n\u0026#34;, uts.nodename); /* Keep the namespace open for a while, by sleeping. This allows some experimentation--for example, another process might join the namespace. */ sleep(100); return 0; /* Terminates child */ } /* 定义一个给 clone 用的栈，栈大小1M */ #define STACK_SIZE (1024 * 1024) static char child_stack[STACK_SIZE]; int main(int argc, char *argv[]) { pid_t child_pid; struct utsname uts; if (argc \u0026lt; 2) { fprintf(stderr, \u0026#34;Usage: %s \u0026lt;child-hostname\u0026gt;\\n\u0026#34;, argv[0]); exit(EXIT_FAILURE); } /* 调用 clone 函数创建一个新的 UTS namespace，其中传出一个函数，还有一个栈空间（为什么传尾指针，因为栈是反着的）; 新的进程将在用户定义的函数 childFunc() 中执行 */ child_pid = clone(childFunc, child_stack + STACK_SIZE, /* 因为栈是反着的， 所以传尾指针 */ CLONE_NEWUTS | SIGCHLD, argv[1]); if (child_pid == -1) errExit(\u0026#34;clone\u0026#34;); printf(\u0026#34;PID of child created by clone() is %ld\\n\u0026#34;, (long) child_pid); /* Parent falls through to here */ sleep(1); /* 给子进程预留一定的时间来改变主机名 */ /* 显示当前 UTS namespace 中的主机名，和 子进程所在的 UTS namespace 中的主机名不同 */ if (uname(\u0026amp;uts) == -1) errExit(\u0026#34;uname\u0026#34;); printf(\u0026#34;uts.nodename in parent: %s\\n\u0026#34;, uts.nodename); if (waitpid(child_pid, NULL, 0) == -1) /* 等待子进程结束 */ errExit(\u0026#34;waitpid\u0026#34;); printf(\u0026#34;child has terminated\\n\u0026#34;); exit(EXIT_SUCCESS); } 该程序通过标志位 CLONE_NEWUTS 调用 clone() 函数创建一个 UTS namespace。UTS namespace 隔离了两个系统标识符 — 主机名和 NIS 域名 —它们分别通过 sethostname() 和 setdomainname() 这两个系统调用来设置，并通过系统调用 uname() 来获取。\n下面将对程序中的一些关键部分进行解读（为了简单起见，我们将省略其中的错误检查）。\n程序运行时后面需要跟上一个命令行参数，它将会创建一个在新的 UTS namespace 中执行的子进程，该子进程会在新的 UTS namespace 中将主机名改为命令行参数中提供的值。\n主程序的第一个关键部分是通过系统调用 clone() 来创建子进程：\nchild_pid = clone(childFunc, child_stack + STACK_SIZE, /* Points to start of downwardly growing stack */ CLONE_NEWUTS | SIGCHLD, argv[1]); printf(\u0026#34;PID of child created by clone() is %ld\\n\u0026#34;, (long) child_pid); 子进程将会在用户定义的函数 childFunc() 中开始执行，该函数将会接收 clone() 最后的参数（argv[1]）作为自己的参数，并且标志位包含了 CLONE_NEWUTS，所以子进程会在新创建的 UTS namespace 中执行。\n接下来主进程睡眠一段时间，让子进程能够有时间更改其 UTS namespace 中的主机名。然后调用 uname() 来检索当前 UTS namespace 中的主机名，并显示该主机名：\nsleep(1); /* Give child time to change its hostname */ uname(\u0026amp;uts); printf(\u0026#34;uts.nodename in parent: %s\\n\u0026#34;, uts.nodename); 与此同时，由 clone() 创建的子进程执行的函数 childFunc() 首先将主机名改为命令行参数中提供的值，然后检索并显示修改后的主机名：\nsethostname(arg, strlen(arg); uname(\u0026amp;uts); printf(\u0026#34;uts.nodename in child: %s\\n\u0026#34;, uts.nodename); 子进程退出之前也睡眠了一段时间，这样可以防止新的 UTS namespace 不会被关闭，让我们能够有机会进行后续的实验。\n执行程序，观察父进程和子进程是否处于不同的 UTS namespace 中：\n$ su # 需要特权才能创建 UTS namespace Password: # uname -n antero # ./demo_uts_namespaces bizarro PID of child created by clone() is 27514 uts.nodename in child: bizarro uts.nodename in parent: antero 除了 User namespace 之外，创建其他的 namespace 都需要特权，更确切地说，是需要相应的 Linux Capabilities，即 CAP_SYS_ADMIN。这样就可以避免设置了 SUID（Set User ID on execution）的程序因为主机名不同而做出一些愚蠢的行为。如果对 Linux Capabilities 不是很熟悉，可以参考我之前的文章： Linux Capabilities 入门教程：概念篇。\n2. proc 文件 # 每个进程都有一个 /proc/PID/ns 目录，其下面的文件依次表示每个 namespace, 例如 user 就表示 user namespace。从 3.8 版本的内核开始，该目录下的每个文件都是一个特殊的符号链接，链接指向 $namespace:[$namespace-inode-number]，前半部份为 namespace 的名称，后半部份的数字表示这个 namespace 的句柄号。句柄号用来对进程所关联的 namespace 执行某些操作。\n$ ls -l /proc/$$/ns # $$ 表示当前所在的 shell 的 PID total 0 lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 ipc -\u0026gt; ipc:[4026531839] lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 mnt -\u0026gt; mnt:[4026531840] lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 net -\u0026gt; net:[4026531956] lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 pid -\u0026gt; pid:[4026531836] lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 user -\u0026gt; user:[4026531837] lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 uts -\u0026gt; uts:[4026531838] 这些符号链接的用途之一是用来确认两个不同的进程是否处于同一 namespace 中。如果两个进程指向的 namespace inode number 相同，就说明他们在同一个 namespace 下，否则就在不同的 namespace 下。这些符号链接指向的文件比较特殊，不能直接访问，事实上指向的文件存放在被称为 nsfs 的文件系统中，该文件系统用户不可见，可以使用系统调用 stat() 在返回的结构体的 st_ino 字段中获取 inode number。在 shell 终端中可以用命令（实际上就是调用了 stat()）看到指向文件的 inode 信息：\n$ stat -L /proc/$$/ns/net File: /proc/3232/ns/net Size: 0 Blocks: 0 IO Block: 4096 regular empty file Device: 4h/4d\tInode: 4026531956 Links: 1 Access: (0444/-r--r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2020-01-17 15:45:23.783304900 +0800 Modify: 2020-01-17 15:45:23.783304900 +0800 Change: 2020-01-17 15:45:23.783304900 +0800 Birth: - 除了上述用途之外，这些符号链接还有其他的用途，如果我们打开了其中一个文件，那么只要与该文件相关联的文件描述符处于打开状态，即使该 namespace 中的所有进程都终止了，该 namespace 依然不会被删除。通过 bind mount 将符号链接挂载到系统的其他位置，也可以获得相同的效果：\n$ touch ~/uts $ mount --bind /proc/27514/ns/uts ~/uts 3. setns() # 加入一个已经存在的 namespace 可以通过系统调用 setns() 来完成。它的原型如下：\nint setns(int fd, int nstype); 更确切的说法是：setns() 将调用的进程与特定类型 namespace 的一个实例分离，并将该进程与该类型 namespace 的另一个实例重新关联。\nfd 表示要加入的 namespace 的文件描述符，可以通过打开其中一个符号链接来获取，也可以通过打开 bind mount 到其中一个链接的文件来获取。 nstype 让调用者可以去检查 fd 指向的 namespace 类型，值可以设置为前文提到的常量 CLONE_NEW*，填 0 表示不检查。如果调用者已经明确知道自己要加入了 namespace 类型，或者不关心 namespace 类型，就可以使用该参数来自动校验。 结合 setns() 和 execve() 可以实现一个简单但非常有用的功能：将某个进程加入某个特定的 namespace，然后在该 namespace 中执行命令。直接来看例子：\n/* ns_exec.c Copyright 2013, Michael Kerrisk Licensed under GNU General Public License v2 or later Join a namespace and execute a command in the namespace */ #define _GNU_SOURCE #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;sched.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; /* A simple error-handling function: print an error message based on the value in \u0026#39;errno\u0026#39; and terminate the calling process */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \\ } while (0) int main(int argc, char *argv[]) { int fd; if (argc \u0026lt; 3) { fprintf(stderr, \u0026#34;%s /proc/PID/ns/FILE cmd [arg...]\\n\u0026#34;, argv[0]); exit(EXIT_FAILURE); } fd = open(argv[1], O_RDONLY); /* 获取想要加入的 namespace 的文件描述符 */ if (fd == -1) errExit(\u0026#34;open\u0026#34;); if (setns(fd, 0) == -1) /* 加入该 namespace */ errExit(\u0026#34;setns\u0026#34;); execvp(argv[2], \u0026amp;argv[2]); /* 在加入的 namespace 中执行相应的命令 */ errExit(\u0026#34;execvp\u0026#34;); } 该程序运行需要两个或两个以上的命令行参数，第一个参数表示特定的 namespace 符号链接的路径（或者 bind mount 到这些符号链接的文件路径）；第二个参数表示要在该符号链接相对应的 namespace 中执行的程序名称，以及执行这个程序所需的命令行参数。关键步骤如下：\nfd = open(argv[1], O_RDONLY); /* 获取想要加入的 namespace 的文件描述符 */ setns(fd, 0); /* 加入该 namespace */ execvp(argv[2], \u0026amp;argv[2]); /* 在加入的 namespace 中执行相应的命令 */ 还记得我们之前已经通过 bind mount 将 demo_uts_namespaces 创建的 UTS namespace 挂载到 ~/uts 中了吗？可以将本例中的程序与之结合，让新进程可以在该 UTS namespace 中执行 shell：\n$ ./ns_exec ~/uts /bin/bash # ~/uts 被 bind mount 到了 /proc/27514/ns/uts My PID is: 28788 验证新的 shell 是否与 demo_uts_namespaces 创建的子进程处于同一个 UTS namespace：\n$ hostname bizarro $ readlink /proc/27514/ns/uts uts:[4026532338] $ readlink /proc/$$/ns/uts # $$ 表示当前 shell 的 PID uts:[4026532338] 在早期的内核版本中，不能使用 setns() 来加入 mount namespace、PID namespace 和 user namespace，从 3.8 版本的内核开始，setns() 支持加入所有的 namespace。\nutil-linux 包里提供了nsenter 命令，其提供了一种方式将新创建的进程运行在指定的 namespace 里面，它的实现很简单，就是通过命令行（-t 参数）指定要进入的 namespace 的符号链接，然后利用 setns() 将当前的进程放到指定的 namespace 里面，再调用 clone() 运行指定的执行文件。我们可以用 strace 来看看它的运行情况：\n# strace nsenter -t 27242 -i -m -n -p -u /bin/bash execve(\u0026#34;/usr/bin/nsenter\u0026#34;, [\u0026#34;nsenter\u0026#34;, \u0026#34;-t\u0026#34;, \u0026#34;27242\u0026#34;, \u0026#34;-i\u0026#34;, \u0026#34;-m\u0026#34;, \u0026#34;-n\u0026#34;, \u0026#34;-p\u0026#34;, \u0026#34;-u\u0026#34;, \u0026#34;/bin/bash\u0026#34;], [/* 21 vars */]) = 0 ………… ………… pen(\u0026#34;/proc/27242/ns/ipc\u0026#34;, O_RDONLY) = 3 open(\u0026#34;/proc/27242/ns/uts\u0026#34;, O_RDONLY) = 4 open(\u0026#34;/proc/27242/ns/net\u0026#34;, O_RDONLY) = 5 open(\u0026#34;/proc/27242/ns/pid\u0026#34;, O_RDONLY) = 6 open(\u0026#34;/proc/27242/ns/mnt\u0026#34;, O_RDONLY) = 7 setns(3, CLONE_NEWIPC) = 0 close(3) = 0 setns(4, CLONE_NEWUTS) = 0 close(4) = 0 setns(5, CLONE_NEWNET) = 0 close(5) = 0 setns(6, CLONE_NEWPID) = 0 close(6) = 0 setns(7, CLONE_NEWNS) = 0 close(7) = 0 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4deb1faad0) = 4968 4. unshare() # 最后一个要介绍的系统调用是 unshare()，它的原型如下：\nint unshare(int flags); unshare() 与 clone() 类似，但它运行在原先的进程上，不需要创建一个新进程，即：先通过指定的 flags 参数 CLONE_NEW* 创建一个新的 namespace，然后将调用者加入该 namespace。最后实现的效果其实就是将调用者从当前的 namespace 分离，然后加入一个新的 namespace。\nLinux 中自带的 unshare 命令，就是通过 unshare() 系统调用实现的，使用方法如下：\n$ unshare [options] program [arguments] options 指定要创建的 namespace 类型。\nunshare 命令的主要实现如下：\n/* 通过提供的命令行参数初始化 \u0026#39;flags\u0026#39; */ unshare(flags); /* Now execute \u0026#39;program\u0026#39; with \u0026#39;arguments\u0026#39;; \u0026#39;optind\u0026#39; is the index of the next command-line argument after options */ execvp(argv[optind], \u0026amp;argv[optind]); unshare 命令的完整实现如下：\n/* unshare.c Copyright 2013, Michael Kerrisk Licensed under GNU General Public License v2 or later A simple implementation of the unshare(1) command: unshare namespaces and execute a command. */ #define _GNU_SOURCE #include \u0026lt;sched.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; /* A simple error-handling function: print an error message based on the value in \u0026#39;errno\u0026#39; and terminate the calling process */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \\ } while (0) static void usage(char *pname) { fprintf(stderr, \u0026#34;Usage: %s [options] program [arg...]\\n\u0026#34;, pname); fprintf(stderr, \u0026#34;Options can be:\\n\u0026#34;); fprintf(stderr, \u0026#34; -i unshare IPC namespace\\n\u0026#34;); fprintf(stderr, \u0026#34; -m unshare mount namespace\\n\u0026#34;); fprintf(stderr, \u0026#34; -n unshare network namespace\\n\u0026#34;); fprintf(stderr, \u0026#34; -p unshare PID namespace\\n\u0026#34;); fprintf(stderr, \u0026#34; -u unshare UTS namespace\\n\u0026#34;); fprintf(stderr, \u0026#34; -U unshare user namespace\\n\u0026#34;); exit(EXIT_FAILURE); } int main(int argc, char *argv[]) { int flags, opt; flags = 0; while ((opt = getopt(argc, argv, \u0026#34;imnpuU\u0026#34;)) != -1) { switch (opt) { case \u0026#39;i\u0026#39;: flags |= CLONE_NEWIPC; break; case \u0026#39;m\u0026#39;: flags |= CLONE_NEWNS; break; case \u0026#39;n\u0026#39;: flags |= CLONE_NEWNET; break; case \u0026#39;p\u0026#39;: flags |= CLONE_NEWPID; break; case \u0026#39;u\u0026#39;: flags |= CLONE_NEWUTS; break; case \u0026#39;U\u0026#39;: flags |= CLONE_NEWUSER; break; default: usage(argv[0]); } } if (optind \u0026gt;= argc) usage(argv[0]); if (unshare(flags) == -1) errExit(\u0026#34;unshare\u0026#34;); execvp(argv[optind], \u0026amp;argv[optind]); errExit(\u0026#34;execvp\u0026#34;); } 下面我们执行 unshare.c 程序在一个新的 mount namespace 中执行 shell：\n$ echo $$ # 显示当前 shell 的 PID 8490 $ cat /proc/8490/mounts | grep mq # 显示当前 namespace 中的某个挂载点 mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0 $ readlink /proc/8490/ns/mnt # 显示当前 namespace 的 ID mnt:[4026531840] $ ./unshare -m /bin/bash # 在新创建的 mount namespace 中执行新的 shell $ readlink /proc/$$/ns/mnt # 显示新 namespace 的 ID mnt:[4026532325] 对比两个 readlink 命令的输出，可以知道两个shell 处于不同的 mount namespace 中。改变新的 namespace 中的某个挂载点，然后观察两个 namespace 的挂载点是否有变化：\n$ umount /dev/mqueue # 移除新 namespace 中的挂载点 $ cat /proc/$$/mounts | grep mq # 检查是否生效 $ cat /proc/8490/mounts | grep mq # 查看原来的 namespace 中的挂载点是否依然存在? mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0 可以看出，新的 namespace 中的挂载点 /dev/mqueue 已经消失了，但在原来的 namespace 中依然存在。\n5. 总结 # 本文仔细研究了 namespace API 的每个组成部分，并将它们结合起来一起使用。后续的文章将会继续深入研究每个单独的 namespace，尤其是 PID namespace 和 user namespace。\n参考链接 # Namespaces in operation, part 2: the namespaces API Docker 基础技术：Linux Namespace（上） ","date":"2020年1月17日","externalUrl":null,"permalink":"/posts/introduction-to-linux-namespaces-part-1-api/","section":"博客","summary":"Linux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。用官方的话来说，L","title":"Linux Namespace 基础教程：namespace API","type":"posts"},{"content":"","date":"2020年1月17日","externalUrl":null,"permalink":"/tags/namespace/","section":"标签","summary":"","title":"Namespace","type":"tags"},{"content":" 上篇文章介绍了如何基于文件系统动态更新 Envoy 配置，还没看过的同学可以去恶补一下。今天要介绍一个新的基于 Envoy 的奇技淫巧，其实很简单，几句话就可以说完。但鉴于网上并无与此相关的资料，决定还是写出来吧，目测我是用此方法的第一人，至少国内如此。\n想必大部分小朋友看标题就知道我要讲的是啥，没错，是用 Envoy 来反向代理 Google。网上铺天盖地都是 Nginx 反代 Google 的文章，看得我是真难受，还得添加各种模块自己编译，你累不累啊？今天让我用 Envoy 教你如何正确优雅地反代 Google，看懂的掌声。\n首先得准备一个访问 Google 不受限的云服务器，知道的同学自然懂，不多说。\nEnvoy 的配置方法继续沿用上篇文章的方法，基于文件系统来动态更新配置。步骤非常简单，先在 lds.yaml 中添加 Listener：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_https address: socket_address: address: 0.0.0.0 port_value: 443 filter_chains: - filter_chain_match: server_names: \u0026#34;google.icloudnative.io\u0026#34; transport_socket: name: envoy.transport_sockets.tls typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: \u0026#34;/etc/letsencrypt/live/www.icloudnative.io/fullchain.pem\u0026#34; private_key: filename: \u0026#34;/etc/letsencrypt/live/www.icloudnative.io/privkey.pem\u0026#34; filters: - name: envoy.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_https codec_type: AUTO access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout route_config: name: https_route_google virtual_hosts: - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: google host_rewrite: www.google.com http_filters: - name: envoy.router 将域名替换成你自己的域名 将配置中的证书替换成你自己的证书，至于证书如何申请我就不说了，不是本文的重点，请面向谷歌找答案。 下一步是向 cds.yaml 中添加 Cluster：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: google connect_timeout: 1s type: logical_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: google endpoints: - lb_endpoints: - endpoint: address: socket_address: address: www.google.com port_value: 443 tls_context: sni: www.google.com 这里解释一下 logical_dns 与 strict_dns 的区别：\n严格 DNS（strict_dns）: 当使用严格 DNS 服务发现时，Envoy 将持续并异步地解析指定的 DNS 目标。DNS 结果中的每个返回的IP地址将被视为上游群集中的显式主机。 这意味着如果查询返回三个 IP 地址，Envoy 将假定集群有三个主机，并且三个主机都应该负载均衡。简单直白一点 : 如果上游集群有多个 IP 地址，那么基于轮询算法，每隔一段时间都会连接到不同的 IP。 逻辑 DNS（logical_dns）: 与严格 DNS 服务发现类似，但在需要初始化新连接时仅使用返回的第一个 IP 地址。简单直白一点：即使上游集群有多个 IP 地址，相关联的连接每次都会连接到相同的 IP，直到连接被关闭。 tls_context 字段表示通过 HTTPS 协议连接上游集群。\n最后一步，使配置生效，如果你的 Envoy 跑在宿主机上，不用做任何操作配置就已经生效了。如果你的 Envoy 跑在容器中，可以执行我上篇文章中的脚本：\n$ bash apply.sh 现在就可以愉快地访问 Google 了：\n已添加到我的博客首页，快快收藏起来：\n以后请叫我云原生奇技淫巧之神。\n","date":"2019年12月28日","externalUrl":null,"permalink":"/posts/use-envoy-proxy-access-google/","section":"博客","summary":"上篇文章介绍了如何基于文件系统动态更新 Envoy 配置，还没看过的同学","title":"Envoy 基础教程：反向代理谷歌搜索","type":"posts"},{"content":"之前家里的路由器一直用的都是网件 R7000 搭载的梅林固件，虽说性能也还不错，比两百块钱的小米路由器强多了，但还是不能满足我的需求，装了某魔法软件后内存蹭蹭蹭爆满啊。终于有理由换软路由了，此时不换更待何时！\n经过一番对比，最后决定在某宝上入手了一款低功耗的 J3160，4 核 4 G，700 大洋左右，刷了个 LEDE 系统，这下绝对够用了。跑了几个魔法软件和一堆容器也没耗多少资源，还是 x86 香啊！\nR7000 就老老实实通过 Access Point 模式作为二级路由提供 WiFi 吧。\n到这里有人可能要问了，说了这么多跟这篇文章的主题有什么关系呢？别急，下面进入主题。\n背景 # 作为顶级贫苦玩家，肯定会在家里装上各种奇奇怪怪的应用，Aria2 和 Transmission 肯定不能少。作为顶级云原生狂热信徒，监控一条龙服务肯定不能少，至少应该上一套 Grafana 和 Prometheus。然而，这么多乱七八糟的端口，我可记不住。。。\n我需要一款负载均衡器来反代所有的服务，别跟我说 Nginx，作为云原生舔狗，用 Nginx 是不可能的，必须用我的偶像 Envoy 来做反代啊，既能反代 Web 服务，还能代替防火墙的端口映射功能（就是反代 TCP 啦），最重要的是还能暴露所有 Upstream 服务的 metrics，再结合 Prometheus 和 Grafana，不香吗？（你想想，连 samba 和 UDP 服务都能监控）\n第一步当然是让路由器获取外网 IP 了，现在上海电信用的都是 SDN 网关，破解都无从破解，但很多人不知道的是，其实你可以打电信客服电话让人家在后台把 SDN 网关改成桥接模式。。。改完桥接模式就好办了，直接路由器拨号就是外网 IP。\n下面就是改 LEDE Web 服务端口，因为 Envoy 得用 80 端口，所以把它的端口改成别的，比如 81 就不错：\n$ cat /etc/config/uhttpd config uhttpd \u0026#39;main\u0026#39; list listen_http \u0026#39;0.0.0.0:81\u0026#39; list listen_http \u0026#39;[::]:81\u0026#39; list listen_proxy \u0026#39;127.0.0.1:8000\u0026#39; list listen_https \u0026#39;0.0.0.0:6443\u0026#39; list listen_https \u0026#39;[::]:6443\u0026#39; option home \u0026#39;/www\u0026#39; ... 改完之后重启 httpd 服务：\n$ /etc/init.d/uhttpd restart DDNS 和申请 https 证书什么的我就不说了，不是本文的重点。\n基于文件的 xDS 动态更新 # 80 和 443 端口被腾出来之后，就可以愉快地使用反代了。可是安装 Envoy 是个头疼的问题啊，编译太复杂， GetEnvoy 项目又不支持 busybox，只能通过容器跑了。配置如图：\n下面就是老老实实写配置文件，没什么可说的，但问题就出在这里，Upstream 服务不多倒好办，一旦变多，Envoy 配置文件会过于冗长，很容易看花眼。虽想到了用控制平面来动态更新配置，但我没必要单独起个控制平面服务，还有没有别的办法呢？有的，其实 Envoy 是可以将文件作为配置的订阅来源的。方法很简单，首先需要参加一个 Bootstrap 引导程序配置文件，里面定义了 node 信息和动态资源：\n$ cat envoy.yaml node: id: node0 cluster: cluster0 dynamic_resources: lds_config: path: /etc/envoy/lds.yaml cds_config: path: /etc/envoy/cds.yaml admin: access_log_path: \u0026#34;/dev/stdout\u0026#34; address: socket_address: address: \u0026#34;::\u0026#34; ipv4_compat: true port_value: 15001 Envoy 将使用 inotify（MacOS 用的是 kqueue）来监视文件的更改，一旦检测到更改，就立即订阅更新。查看系统是否支持 inotify：\n$ ll /proc/sys/fs/inotify/ -rw-r--r-- 1 root root 0 Dec 23 16:05 max_queued_events -rw-r--r-- 1 root root 0 Dec 23 16:05 max_user_instances -rw-r--r-- 1 root root 0 Dec 23 16:05 max_user_watches lds.yaml 里是 Listener 的配置，cds.yaml 里是 Cluster 的配置，先往 lds.yaml 中加入如下的配置：\nversion_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_http_v4 address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout route_config: name: http_route_v4 virtual_hosts: - name: backend domains: - \u0026#34;router.icloudnative.io\u0026#34; - \u0026#34;mynas.icloudnative.io\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: https_redirect: true port_redirect: 8443 response_code: \u0026#34;FOUND\u0026#34; - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: lede http_filters: - name: envoy.router 域名改成你自己的就好，路由分为两部分，通过那两个域名访问的就会被转到 https，其他的都转到 lede Web 服务。其实 http 转 https 的那部分路由可以删掉，因为国内的运营商基本上都把 80 端口封了，外网是无法访问的。第二部分不能删除，删除之后就不能通过内网访问 lede Web 界面了。\n再加入 https 的配置：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_https_v4 address: socket_address: address: 0.0.0.0 port_value: 8443 filter_chains: - filter_chain_match: server_names: \u0026#34;router.icloudnative.io\u0026#34; transport_socket: name: envoy.transport_sockets.tls typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: \u0026#34;/etc/ssl/router.icloudnative.io/3207748_router.icloudnative.io.pem\u0026#34; private_key: filename: \u0026#34;/etc/ssl/router.icloudnative.io/3207748_router.icloudnative.io.key\u0026#34; filters: - name: envoy.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_https codec_type: AUTO access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout route_config: name: https_route_v4_default virtual_hosts: - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: lede http_filters: - name: envoy.router 接下来往 cds.yaml 中加入 Cluster 配置：\nversion_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: lede connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: lede endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 81 由于 Docker 对 inotify 的支持不太友好，有时不会检测不到文件系统的更改，所以最好的办法是强制更改，原理很简单，将文件重命名，然后再改回来。写一个脚本就好了：\n$ cat apply.sh #!/bin/bash mv cds.yaml cds.yaml.temp mv cds.yaml.temp cds.yaml mv lds.yaml lds.yaml.temp mv lds.yaml.temp lds.yaml 注意：必须先更新 CDS，后更新 LDS。\n执行脚本之后，查看 Envoy 日志，发现配置已经生效：\n$ docker logs -f envoy [2019-12-23 09:22:14.644][1][info][upstream] [source/common/upstream/cds_api_impl.cc:71] cds: add 1 cluster(s), remove 0 cluster(s) [2019-12-23 09:22:14.648][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;lede\u0026#39; [2019-12-23 09:22:30.186][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_http_v4\u0026#39; [2019-12-23 09:22:45.881][1][warning][config] [source/server/listener_impl.cc:287] adding listener \u0026#39;0.0.0.0:8443\u0026#39;: filter chain match rules require TLS Inspector listener filter, but it isn\u0026#39;t configured, trying to inject it (this might fail if Envoy is compiled without it) [2019-12-23 09:22:45.882][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_https_v4\u0026#39; ipv6 # 上面只是 ipv4 的配置，如果你的宽带开启了 ipv6，还可以开启 ipv6 端口。至于我为什么要将 ipv4 和 ipv6 分开呢，因为据我测试，电信运营商只封了 ipv4 的 80 和 443 端口，ipv6 还可以用，所以我需要为 ipv4 和 ipv6 分配不同的路由策略。在 lds.yaml 中加入 ipv6 的配置：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_http_v6 address: socket_address: address: \u0026#34;::\u0026#34; port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout route_config: name: http_route_v6 virtual_hosts: - name: backend domains: - \u0026#34;router.icloudnative.io\u0026#34; - \u0026#34;mynas.icloudnative.io\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: https_redirect: true port_redirect: 443 response_code: \u0026#34;FOUND\u0026#34; - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: lede http_filters: - name: envoy.router - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_https_v6 address: socket_address: address: \u0026#34;::\u0026#34; port_value: 443 filter_chains: - filter_chain_match: server_names: \u0026#34;router.icloudnative.io\u0026#34; transport_socket: name: envoy.transport_sockets.tls typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: \u0026#34;/etc/ssl/router.icloudnative.io/3207748_router.icloudnative.io.pem\u0026#34; private_key: filename: \u0026#34;/etc/ssl/router.icloudnative.io/3207748_router.icloudnative.io.key\u0026#34; filters: - name: envoy.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_https codec_type: AUTO access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout route_config: name: https_route_v6_default virtual_hosts: - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: lede http_filters: - name: envoy.router 执行 apply.sh 使配置生效，查看日志：\n$ docker logs -f envoy [2019-12-23 09:43:44.431][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_http_v6\u0026#39; [2019-12-23 09:43:44.441][1][warning][config] [source/server/listener_impl.cc:287] adding listener \u0026#39;[::]:443\u0026#39;: filter chain match rules require TLS Inspector listener filter, but it isn\u0026#39;t configured, trying to inject it (this might fail if Envoy is compiled without it) [2019-12-23 09:43:44.441][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_https_v6\u0026#39; Grafana # Grafana 的安装我就不多说了，直接容器跑，配置如下：\n为了能够通过反向代理正确访问 Grafana，需要对 Grafana 的配置做一些调整，修改 grafana.ini 中的以下几个字段：\n[server] domain = foo.bar root_url = %(protocol)s://%(domain)s/grafana/ 将 domain 的值换成你自己的域名。\n修改 Listener listener_https_v4 的路由：\nroute_config: name: https_route_v4_default virtual_hosts: - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/grafana/\u0026#34; route: cluster: grafana - match: prefix: \u0026#34;/\u0026#34; route: cluster: lede 修改 Listener listener_https_v6 的路由：\nroute_config: name: https_route_v6_default virtual_hosts: - name: default domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/grafana/\u0026#34; route: cluster: grafana - match: prefix: \u0026#34;/\u0026#34; route: cluster: lede 向 cds.yaml 中添加 Cluster：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: grafana connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: grafana endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 3000 应用更新：\n$ ./apply.sh 然后就可以通过 subpath 访问 Grafana 了。\nTCP # 一般情况下，路由器的端口映射都是通过 iptables 来做的，但我既然用了 Envoy，端口映射肯定还是要用 Envoy 来实现，毕竟 Grafana 真香。\nEnvoy 通过 TCP 代理即可实现端口映射功能，比如我想将 samba 服务暴露到公网，只需向 lds.yaml 中加入配置：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_smb_local address: socket_address: address: \u0026#34;::\u0026#34; ipv4_compat: true port_value: 139 filter_chains: - filters: - name: envoy.tcp_proxy typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy stat_prefix: smb_local cluster: smb_local access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Listener name: listener_smb_internet address: socket_address: address: \u0026#34;::\u0026#34; ipv4_compat: true port_value: 4450 filter_chains: - filters: - name: envoy.tcp_proxy typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy stat_prefix: smb_internet cluster: smb_internet access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout 其中 ipv4_compat: true 表示同时监听 ipv4 和 ipv6。445 端口也被运营商封了，所以可以使用 4450 端口。\n再向 cds.yaml 中加入如下的配置：\n- \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: smb_local connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: smb_local endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 192.168.100.20 port_value: 139 - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: smb_internet connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: smb_internet endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 192.168.100.20 port_value: 445 将其中的地址改成你的 samba 服务内网地址。\n配置生效后，就可以通过外网连接你的 samba 服务了。\n当然了，我自己的 Upstream 服务远远不止这些，我只是针对每一种类型举一个示例，大家可以举一反三。看看我的：\n$ docker logs -f envoy [2019-12-23 10:16:58.199][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;prometheus\u0026#39; [2019-12-23 10:16:58.201][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;transmission\u0026#39; [2019-12-23 10:16:58.204][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;mynas\u0026#39; [2019-12-23 10:16:58.205][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;aria2\u0026#39; [2019-12-23 10:16:58.207][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;aria2_bt\u0026#39; [2019-12-23 10:16:58.209][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;aria2_dht\u0026#39; [2019-12-23 10:16:58.211][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;smb_local\u0026#39; [2019-12-23 10:16:58.213][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;smb_internet\u0026#39; [2019-12-23 10:16:58.215][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;transmission_bt\u0026#39; [2019-12-23 10:16:58.217][1][info][upstream] [source/common/upstream/cds_api_impl.cc:87] cds: add/update cluster \u0026#39;time\u0026#39; [2019-12-23 10:16:58.233][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_http_v6\u0026#39; [2019-12-23 10:16:58.243][1][warning][config] [source/server/listener_impl.cc:287] adding listener \u0026#39;0.0.0.0:8443\u0026#39;: filter chain match rules require TLS Inspector listener filter, but it isn\u0026#39;t configured, trying to inject it (this might fail if Envoy is compiled without it) [2019-12-23 10:16:58.243][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_https_v4\u0026#39; [2019-12-23 10:16:58.254][1][warning][config] [source/server/listener_impl.cc:287] adding listener \u0026#39;[::]:443\u0026#39;: filter chain match rules require TLS Inspector listener filter, but it isn\u0026#39;t configured, trying to inject it (this might fail if Envoy is compiled without it) [2019-12-23 10:16:58.255][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_https_v6\u0026#39; [2019-12-23 10:16:58.255][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_ntp\u0026#39; [2019-12-23 10:16:58.260][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_aria2\u0026#39; [2019-12-23 10:16:58.263][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_aria2_bt\u0026#39; [2019-12-23 10:16:58.265][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_aria2_dht\u0026#39; [2019-12-23 10:16:58.269][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_smb_local\u0026#39; [2019-12-23 10:16:58.272][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_smb_internet\u0026#39; [2019-12-23 10:16:58.275][1][info][upstream] [source/server/lds_api.cc:71] lds: add/update listener \u0026#39;listener_transmission_bt\u0026#39; 监控截图：\n","date":"2019年12月23日","externalUrl":null,"permalink":"/posts/file-based-dynamic-routing-configuration/","section":"博客","summary":"之前家里的路由器一直用的都是网件 R7000 搭载的梅林固件，虽说性能也","title":"Envoy 基础教程：基于文件系统动态更新配置","type":"posts"},{"content":"","date":"2019年12月23日","externalUrl":null,"permalink":"/tags/openwrt/","section":"标签","summary":"","title":"OpenWrt","type":"tags"},{"content":"","date":"2019年12月19日","externalUrl":null,"permalink":"/tags/nftables/","section":"标签","summary":"","title":"Nftables","type":"tags"},{"content":" 上篇文章 给大家介绍了 nftables 的优点以及基本的使用方法，它的优点在于直接在用户态把网络规则编译成字节码，然后由内核的虚拟机执行，尽管和 iptables 一样都是基于 netfilter，但 nftables 的灵活性更高。\n之前用 iptables 匹配大量数据时，还得需要 ipset 配合，而 nftables 直接内置了集合和字典，可以直接匹配大量的数据，这一点比 iptables 方便多了，拿来练练魔法真是极好的，不多解释，请直接看 Linux全局智能分流方案。\n本文将会教你如何配置 nftables 来为服务器实现一个简单的防火墙，本文以 CentOS 7 为例，其他发行版类似。\n安装 nftables # 首先需要安装 nftables：\n$ yum install -y nftables 由于 nftables 默认没有内置的链，但提供了一些示例配置，我们可以将其 include 到主配置文件中。主配置文件为 /etc/sysconfig/nftables.conf，将下面一行内容取消注释：\n# include \u0026#34;/etc/nftables/inet-filter\u0026#34; 然后启动 nftables 服务：\n$ systemctl start nftables 现在再次查看规则，就会发现多了一张 filter 表和几条链：\n$ nft list ruleset table inet filter { chain input { type filter hook input priority 0; policy accept; } chain forward { type filter hook forward priority 0; policy accept; } chain output { type filter hook output priority 0; policy accept; } } 在 nftables 中，ipv4 和 ipv6 协议可以被合并到一个单一的地址簇 inet 中，使用了 inet 地址簇，就不需要分别为 ipv4 和 ipv6 指定两个不同的规则了。\n添加 INPUT 规则 # 和 iptables 一样，nftables 的 filter 表包含三条链：INPUT、FORWARD 和 OUTPUT，一般配置防火墙只需要配置 INPUT 链就好了。\n回环接口 # 首先允许访问 localhost：\n$ nft add rule inet filter input iif \u0026#34;lo\u0026#34; accept $ nft add rule inet filter input iif != \u0026#34;lo\u0026#34; ip daddr 127.0.0.0/8 drop 可以再优化一下，加上注解（comment）和计数器（counter）：\n$ nft add rule inet filter input \\ iif \u0026#34;lo\u0026#34; \\ accept \\ comment \\\u0026#34;Accept any localhost traffic\\\u0026#34; $ nft add rule inet filter input \\ iif != \u0026#34;lo\u0026#34; ip daddr 127.0.0.0/8 \\ counter \\ drop \\ comment \\\u0026#34;drop connections to loopback not coming from loopback\\\u0026#34; 查看规则：\n$ nft list chain inet filter input table inet filter { chain input { type filter hook input priority 0; policy accept; iif \u0026#34;lo\u0026#34; accept comment \u0026#34;Accept any localhost traffic\u0026#34; iif != \u0026#34;lo\u0026#34; ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment \u0026#34;drop connections to loopback not coming from loopback\u0026#34; } } 连接跟踪模块 # 接下来的规则用到一个内核模块叫 conntrack（connection tracking），它被用来跟踪一个连接的状态。最常见的使用场景是 NAT，为什么需要跟踪记录连接的状态呢？因为 nftables 需要记住数据包的目标地址被改成了什么，并且在返回数据包时再将目标地址改回来。\n和 iptables 一样，一个 TCP 连接在 nftables 中总共有四种状态：NEW，ESTABLISHED，RELATED 和 INVALID。\n除了本地产生的包由 OUTPUT 链处理外，所有连接跟踪都是在 PREROUTING 链里进行处理的，意思就是， iptables 会在 PREROUTING 链里从新计算所有的状态。如果我们发送一个流的初始化包，状态就会在 OUTPUT 链里被设置为 NEW，当我们收到回应的包时，状态就会在 PREROUTING 链里被设置为 ESTABLISHED。如果收到回应的第一个包不是本地产生的，那就会在 PREROUTING 链里被设置为 NEW 状态。综上，所有状态的改变和计算都是在 nat 表中的 PREROUTING 链和 OUTPUT 链里完成的。\n还有其他两种状态：\nRELATED : RELATED 状态有点复杂，当一个连接与另一个已经是 ESTABLISHED 的连接有关时，这个连接就被认为是 RELATED。这意味着，一个连接要想成为 RELATED，必须首先有一个已经是 ESTABLISHED 的连接存在。这个 ESTABLISHED 连接再产生一个主连接之外的新连接，这个新连接就是 RELATED 状态了。 INVAILD : 表示分组对应的连接是未知的，说明数据包不能被识别属于哪个连接或没有任何状态。有几个原因可以产生这种情况，比如，内存溢出，收到不知属于哪个连接的 ICMP 错误信息。我们需要 DROP 这个状态的任何东西，并打印日志： $ nft add rule inet filter input \\ ct state invalid \\ log prefix \\\u0026#34;Invalid-Input: \\\u0026#34; level info flags all \\ counter \\ drop \\ comment \\\u0026#34;Drop invalid connections\\\u0026#34; 查看规则：\n$ nft list chain inet filter input table inet filter { chain input { type filter hook input priority 0; policy accept; iif \u0026#34;lo\u0026#34; accept comment \u0026#34;Accept any localhost traffic\u0026#34; iif != \u0026#34;lo\u0026#34; ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment \u0026#34;drop connections to loopback not coming from loopback\u0026#34; ct state invalid log prefix \u0026#34;Invalid-Input: \u0026#34; level info flags all counter packets 0 bytes 0 drop comment \u0026#34;Drop invalid connections\u0026#34; } } 令牌桶 # 为了防止有恶意攻击者利用 ping 泛洪（ping flood）来进行攻击，可以利用令牌桶模型来对 ping 包限速。ping 泛洪的原理很简单，就是采用多线程的方法一次性发送多个 ICMP 请求报文，让目的主机忙于处理大量这些报文而造成速度缓慢甚至宕机。\n先来介绍一下令牌桶模型。\n熟悉 iptables 的朋友应该知道，iptables 通过 hashlimit 模块来实现限速的功能，而 hashlimit 的匹配方式就是基于令牌桶（Token bucket）的模型，nftables 也类似，\n令牌桶是一种网络通讯中常见的缓冲区工作原理，它有两个重要的参数，令牌桶容量 n 和 令牌产生速率 s ：\n令牌桶容量 n：可以把令牌当成是门票，而令牌桶则是负责制作和发放门票的管理员，它手里最多有n张令牌。初始时，管理员开始手里有 n 张令牌，每当一个数据包到达后，管理员就看看手里是否还有可用的令牌。如果有，就把令牌发给这个数据包，limit 就告诉nftables，这个数据包被匹配了，而当管理员把手上所有的令牌都发完了，再来的数据包就拿不到令牌了；这时，limit 模块就告诉 nftables ，这个数据包不能被匹配。 令牌产生速率 s：当令牌桶中的令牌数量少于 n，它就会以速率 s 来产生新的令牌，直到令牌数量到达 n 为止。 通过令牌桶机制，可以有效的控制单位时间内通过（匹配）的数据包数量，又可以容许短时间内突发的大量数据包的通过（只要数据包数量不超过令牌桶 n），真是妙哉啊。\nnftables 比 iptables 做的更绝，它不仅可以基于数据包来限速，也可以基于字节来限速。为了更精确地验证令牌桶模型，我们选择基于字节来限速：\n$ nft add rule inet filter input \\ ip protocol icmp icmp type echo-request \\ limit rate 20 bytes/second burst 500 bytes \\ counter \\ accept \\ comment \\\u0026#34;No ping floods\\\u0026#34; 上面的规则表示：\n为所有 echo-request 类型的 ICMP 包建立一个匹配项； 匹配项对应的令牌桶容量为 500 个字节； 令牌产生速率为 20 字节/s 再添加一条规则，拒绝不满足上诉条件的数据包：\n$ nft add rule inet filter input \\ ip protocol icmp icmp type echo-request \\ drop \\ comment \\\u0026#34;No ping floods\\\u0026#34; 同时还要接收状态为 ESTABLISHED 和 RELATED 的数据包：\n$ nft add rule inet filter input \\ ct state \\{ established, related \\} \\ counter \\ accept \\ comment \\\u0026#34;Accept traffic originated from us\\\u0026#34; 下面来做个实验，直接 ping 该服务器的 IP 地址，ping 包大小设置为 100 字节，每秒发送一次：\n$ ping -s 92 192.168.57.53 -i 1 PING 192.168.57.53 (192.168.57.53) 92(120) bytes of data. 100 bytes from 192.168.57.53: icmp_seq=1 ttl=64 time=0.402 ms 100 bytes from 192.168.57.53: icmp_seq=2 ttl=64 time=0.373 ms 100 bytes from 192.168.57.53: icmp_seq=3 ttl=64 time=0.465 ms 100 bytes from 192.168.57.53: icmp_seq=4 ttl=64 time=0.349 ms 100 bytes from 192.168.57.53: icmp_seq=5 ttl=64 time=0.411 ms 100 bytes from 192.168.57.53: icmp_seq=11 ttl=64 time=0.425 ms 100 bytes from 192.168.57.53: icmp_seq=17 ttl=64 time=0.383 ms 100 bytes from 192.168.57.53: icmp_seq=23 ttl=64 time=0.442 ms 100 bytes from 192.168.57.53: icmp_seq=29 ttl=64 time=0.464 ms ... 首先我们能看到前 5 个包的回应都非常正常，然后从第 6 个包开始，我们每 6 秒能收到一个正常的回应。这是因为我们设定了令牌桶的容量为 500 个字节，令牌产生速率为 20 字节/s，而发包的速率是每秒钟 100 个字节，即每个包 100 个字节，当发完 5 个包后，令牌桶的容量变为 0，这时开始以 20 字节/s 的速率产生新令牌（和前面提到的令牌桶算法不太一样，只有当令牌桶容量为 0 才开始产生新的令牌），5 秒钟之后，令牌桶的容量变为 100 个字节，所以 6 秒钟后又能收到正常回应。\nICMP \u0026amp; IGMP # 接收其他类型的 ICMP 协议数据包：\n$ nft add rule inet filter input \\ ip protocol icmp icmp type \\{ destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem \\} \\ accept \\ comment \\\u0026#34;Accept ICMP\\\u0026#34; 接收 IGMP 协议数据包：\n$ nft add rule inet filter input \\ ip protocol igmp \\ accept \\ comment \\\u0026#34;Accept IGMP\\\u0026#34; 分别处理 TCP 和 UDP # 这一步我们将 TCP 和 UDP 的流量拆分，然后分别处理。先创建两条链：\n$ nft add chain inet filter TCP $ nft add chain inet filter UDP 然后创建一个命名字典：\n$ nft add map inet filter input_vmap \\{ type inet_proto : verdict \\; \\} 字典的键表示协议类型，值表示判决动作。\n往字典中添加元素：\n$ nft add element inet filter input_vmap \\{ tcp : jump TCP, udp : jump UDP \\} 最后创建一条规则拆分 TCP 和 UDP 的流量：\n$ nft add rule inet filter input meta l4proto vmap @input_vmap 其中，meta l4proto 用来匹配协议的类型。\n最后再瞄一眼规则：\n$ nft list ruleset table inet filter { map input_vmap { type inet_proto : verdict elements = { tcp : jump TCP, udp : jump UDP } } chain input { type filter hook input priority 0; policy accept; iif \u0026#34;lo\u0026#34; accept comment \u0026#34;Accept any localhost traffic\u0026#34; iif != \u0026#34;lo\u0026#34; ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment \u0026#34;drop connections to loopback not coming from loopback\u0026#34; ct state invalid log prefix \u0026#34;Invalid-Input: \u0026#34; level info flags all counter packets 95 bytes 6479 drop comment \u0026#34;Drop invalid connections\u0026#34; icmp type echo-request limit rate 20 bytes/second burst 500 bytes counter packets 17 bytes 2040 accept comment \u0026#34;No ping floods\u0026#34; icmp type echo-request drop comment \u0026#34;No ping floods\u0026#34; ct state { established, related } counter packets 172135 bytes 99807569 accept comment \u0026#34;Accept traffic originated from us\u0026#34; icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment \u0026#34;Accept ICMP\u0026#34; ip protocol igmp accept comment \u0026#34;Accept IGMP\u0026#34; meta l4proto vmap @input_vmap } chain forward { type filter hook forward priority 0; policy accept; } chain output { type filter hook output priority 0; policy accept; } chain TCP { } chain UDP { } } 处理 TCP 流量 # 这一步我们来处理 TCP 流量，首当其冲的就是 ssh 了，必须得给这位大哥放行啊：\n$ nft add rule inet filter TCP \\ tcp dport 22 \\ ct state new \\ limit rate 15/minute \\ log prefix \\\u0026#34;New SSH connection: \\\u0026#34; \\ counter \\ accept \\ comment \\\u0026#34;Avoid brute force on SSH\\\u0026#34; 其次需要放行 Web 服务，和上面一样，为了易于管理，方便后续动态添加端口，需要先创建一个命名集合：\n$ nft add set inet filter web \\{ type inet_service \\; flags interval \\; \\} 查看集合：\n$ nft list set inet filter web table inet filter { set web { type inet_service flags interval } } 向集合中添加元素：\n$ nft add element inet filter web \\{ 80, 443 \\} 查看集合：\n$ nft list set inet filter web table inet filter { set web { type inet_service flags interval elements = { http, https } } } 放行 Web 服务：\n$ nft add rule inet filter TCP \\ tcp dport @web \\ counter \\ accept \\ comment \\\u0026#34;Accept web server\\\u0026#34; 如果你还有其他不可描述的应用，比如 v-2-r-a-y 之类的代理，可以按照上面的方式添加规则，先创建集合：\n$ nft add set inet filter v-2-r-a-y \\{ type inet_service \\; flags interval \\; \\} 再添加元素：\n$ nft add element inet filter v-2-r-a-y \\{ 9000-9005, 9007 \\} 查看集合：\n$ nft list set inet filter v-2-r-a-y table inet filter { set v-2-r-a-y { type inet_service flags interval elements = { 9000-9005, 9007 } } } 现在体会到 nftables 集合的强大了吧，可以是区间，可以是单个元素组成的集合，也可以混合，iptables 麻烦让一让。\n放行不可描述的服务：\n$ nft add rule inet filter TCP \\ tcp dport @v-2-r-a-y \\ counter \\ accept \\ comment \\\u0026#34;Accept v-2-r-a-y\\\u0026#34; 处理 UDP 流量 # 这一步我们来处理 UDP 流量，比如上面举例的不可描述的应用，除了 TCP 端口还有 UDP 端口，具体用处我就不解释了，自己面向谷歌找答案吧。\n到了这一步，连集合都不用创建， 直接复用之前创建的集合，放行不可描述应用的 UDP 数据：\n$ nft add rule inet filter UDP \\ udp dport @v-2-r-a-y \\ counter \\ accept \\ comment \\\u0026#34;Accept v-2-r-a-y\\\u0026#34; 查看规则：\n$ nft list chain inet filter UDP table inet filter { chain UDP { udp dport @v-2-r-a-y counter packets 0 bytes 0 accept comment \u0026#34;Accept v-2-r-a-y\u0026#34; } } 其他 UDP 数据都可按此套路模块化，简直不要太赏心悦目。\n为了使系统或 nftables 重启后能够继续生效，我们需要将这些规则持久化，直接将规则写入 /etc/nftables/inet-filter：\n$ echo \u0026#34;#! /usr/sbin/nft -f\u0026#34; \u0026gt; /etc/nftables/inet-filter $ nft list ruleset \u0026gt;\u0026gt; /etc/nftables/inet-filter 开机自动加载 nftables 服务：\n$ systemctl enable nftables 在 rsyslog 中记录日志 # 默认情况下，开启日志记录后，日志会直接进入 syslog，和系统日志混在一起，不好读取。最好的办法是将 nftables 的日志重定向到单独的文件。\n以本文为例，我们只开启了 ct state invalid 和 ssh 的日志记录，先在 /var/log 目录中创建一个名为 nftables 的目录，并在其中创建两个名为 invalid.log 和 ssh.log 的文件，分别存储各自的日志。\n$ mkdir /var/log/nftables $ touch /var/log/nftables/{ssh.log,invalid.log} 确保系统中已安装 rsyslog。现在进入 /etc/rsyslog.d 目录并创建一个名为 nftables.conf 的文件，其内容如下：\n:msg,regex,\u0026#34;Invalid-Input: \u0026#34; -/var/log/nftables/invalid.log :msg,regex,\u0026#34;New SSH connection: \u0026#34; -/var/log/nftables/ssh.log 最后，为了确保日志是可管理的，需要在 /etc/logrotate.d 中创建一个 nftables 文件：\n$ cat /etc/logrotate.d/nftables /var/log/nftables/* { rotate 5 daily maxsize 50M missingok notifempty delaycompress compress postrotate invoke-rc.d rsyslog rotate \u0026gt; /dev/null endscript } 重新通过 ssh 连接服务器，就能看到日志了：\n$ tail -f /var/log/nftables/ssh.log Dec 19 17:15:33 [localhost] kernel: New SSH connection: IN=ens192 OUT= MAC=00:50:56:bd:2f:3d:00:50:56:bd:d7:24:08:00 SRC=192.168.57.2 DST=192.168.57.53 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=43312 DF PROTO=TCP SPT=41842 DPT=22 WINDOW=29200 RES=0x00 SYN URGP=0 总结 # 本文教你如何使用 nftables 搭建一个简单的防火墙，并通过集合和字典将规则集模块化，后续可动态添加端口和 IP 等元素，而不用修改规则。更复杂的规则将会在后面的文章介绍，下篇文章将会教你如何使用 nftables 来防 DDoS 攻击，敬请期待。\n","date":"2019年12月19日","externalUrl":null,"permalink":"/posts/use-nftables-as-firewall/","section":"博客","summary":"上篇文章 给大家介绍了 nftables 的优点以及基本的使用方法，它的优点在于","title":"nftables 基础教程：使用 nftables 作为防火墙","type":"posts"},{"content":"","date":"2019年12月15日","externalUrl":null,"permalink":"/tags/istio/","section":"标签","summary":"","title":"Istio","type":"tags"},{"content":"没错，Istio 架构又换了。。。北京时间 2020 年 3 月 6 日 凌晨发布了 1.5 版本，该版本最大的变化是将控制平面的所有组件组合成一个单体结构叫 istiod。\n从架构图可以看出，在 Istio 1.5 中，饱受诟病的 Mixer 终于被废弃了，新版本的 HTTP 遥测默认基于 in-proxy Stats filter，同时可使用 WebAssembly 开发 in-proxy 扩展。更详细的说明请参考 Istio 1.5 发布公告。\n官方文档的部署方法比较笼统，不利于快速上手，为了帮助大家快速上手，本文将重点介绍 Istio 1.5 的部署方法。为了更方便地管理 Istio 各个组件的生命周期，推荐使用 Operator 进行部署。\n在部署 Istio 之前，首先需要确保 Kubernetes 集群（kubernetes 版本建议在 1.14 以上）已部署并配置好本地的 kubectl 客户端。\nKubernetes 环境准备 # 为了快速准备 kubernetes 环境，我们可以使用 sealos 来部署，步骤如下：\n前提条件 # 下载 kubernetes 离线安装包 下载 最新版本sealos 务必同步服务器时间 主机名不可重复 安装 kubernetes 集群 # $ sealos init --master 192.168.0.2 \\ --node 192.168.0.3 \\ --node 192.168.0.4 \\ --node 192.168.0.5 \\ --user root \\ --passwd your-server-password \\ --version v1.16.3 \\ --pkg-url /root/kube1.16.3.tar.gz 检查安装是否正常：\n$ kubectl get node NAME STATUS ROLES AGE VERSION sealos01 Ready master 18h v1.16.3 sealos02 Ready \u0026lt;none\u0026gt; 18h v1.16.3 sealos03 Ready \u0026lt;none\u0026gt; 18h v1.16.3 sealos04 Ready \u0026lt;none\u0026gt; 18h v1.16.3 下载 Istio 部署文件 # 你可以从 GitHub 的 release 页面下载 istio，或者直接通过下面的命令下载：\n$ curl -L https://istio.io/downloadIstio | sh - 下载完成后会得到一个 istio-1.5.0 目录，里面包含了：\ninstall/kubernetes : 针对 Kubernetes 平台的安装文件 samples : 示例应用 bin : istioctl 二进制文件，可以用来手动注入 sidecar proxy 进入 istio-1.5.0 目录。\n$ cd istio-1.5.0 $ tree -L 1 ./ ./ ├── bin ├── install ├── LICENSE ├── manifest.yaml ├── README.md ├── samples └── tools 4 directories, 4 files 将 istioctl 拷贝到 /usr/local/bin/ 中：\n$ cp bin/istioctl /usr/local/bin/ 开启 istioctl 的自动补全功能 # bash # 将 tools 目录中的 istioctl.bash 拷贝到 $HOME 目录中：\n$ cp tools/istioctl.bash ~/ 在 ~/.bashrc 中添加一行：\nsource ~/istioctl.bash 应用生效：\n$ source ~/.bashrc zsh # 将 tools 目录中的 _istioctl 拷贝到 $HOME 目录中：\n$ cp tools/_istioctl ~/ 在 ~/.zshrc 中添加一行：\nsource ~/_istioctl 应用生效：\n$ source ~/.zshrc 部署 Istio # istioctl 提供了多种安装配置文件，可以通过下面的命令查看：\n$ ll install/kubernetes/operator/profiles -rw-r--r-- 1 root root 18K 3月 4 20:40 default.yaml -rw-r--r-- 1 root root 3.2K 3月 4 20:40 demo.yaml -rw-r--r-- 1 root root 964 3月 4 20:40 empty.yaml -rw-r--r-- 1 root root 913 3月 4 20:40 minimal.yaml -rw-r--r-- 1 root root 579 3月 4 20:40 remote.yaml -rw-r--r-- 1 root root 554 3月 4 20:40 separate.yaml 它们之间的差异如下：\ndefault demo minimal remote 核心组件 istio-egressgateway X istio-ingressgateway X X istio-pilot X X X 附加组件 Grafana X istio-tracing X kiali X prometheus X X X 其中标记 X 表示该安装该组件。\n如果只是想快速试用并体验完整的功能，可以直接使用配置文件 demo 来部署。\n在正式部署之前，需要先说明两点：\nIstio CNI Plugin # 当前实现将用户 pod 流量转发到 proxy 的默认方式是使用 privileged 权限的 istio-init 这个 init container 来做的（运行脚本写入 iptables），需要用到 NET_ADMIN capabilities。对 linux capabilities 不了解的同学可以参考我的 Linux capabilities 系列。\nIstio CNI 插件的主要设计目标是消除这个 privileged 权限的 init container，换成利用 Kubernetes CNI 机制来实现相同功能的替代方案。具体的原理就是在 Kubernetes CNI 插件链末尾加上 Istio 的处理逻辑，在创建和销毁 pod 的这些 hook 点来针对 istio 的 pod 做网络配置：写入 iptables，让该 pod 所在的 network namespace 的网络流量转发到 proxy 进程。\n详细内容请参考 官方文档。\n使用 Istio CNI 插件来创建 sidecar iptables 规则肯定是未来的主流方式，不如我们现在就尝试使用这种方法。\nKubernetes 关键插件（Critical Add-On Pods） # 众所周知，Kubernetes 的核心组件都运行在 master 节点上，然而还有一些附加组件对整个集群来说也很关键，例如 DNS 和 metrics-server，这些被称为关键插件。一旦关键插件无法正常工作，整个集群就有可能会无法正常工作，所以 Kubernetes 通过优先级（PriorityClass）来保证关键插件的正常调度和运行。要想让某个应用变成 Kubernetes 的关键插件，只需要其 priorityClassName 设为 system-cluster-critical 或 system-node-critical，其中 system-node-critical 优先级最高。\n注意：关键插件只能运行在 kube-system namespace 中！\n详细内容可以参考 官方文档。\n接下来正式安装 Istio，首先部署 Istio operator：\n🐳 → istioctl operator init 该命令会创建一个 namespace istio-operator，并将 Istio operator 部署在此 namespace 中。\n🐳 → kubectl -n istio-operator get pod NAME READY STATUS RESTARTS AGE istio-operator-7c69599466-bz8lp 1/1 Running 0 3h29m 然后创建一个 CR IstioOperator：\n🐳 → kubectl create ns istio-system 🐳 → kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: install.istio.io/v1alpha1 kind: IstioOperator metadata: namespace: istio-system name: example-istiocontrolplane spec: profile: demo components: cni: enabled: true namespace: kube-system ingressGateways: - enabled: true k8s: service: type: ClusterIP strategy: rollingUpdate: maxUnavailable: 100% maxSurge: 0% nodeSelector: kubernetes.io/hostname: sealos02 overlays: - apiVersion: apps/v1 kind: Deployment name: istio-ingressgateway patches: - path: spec.template.spec value: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet - apiVersion: v1 kind: Service name: istio-ingressgateway patches: - path: spec.ports value: - name: status-port port: 15020 targetPort: 15020 - name: http2 port: 80 targetPort: 80 - name: https port: 443 targetPort: 443 values: cni: excludeNamespaces: - istio-system - kube-system - monitoring logLevel: info EOF 其中各个字段的详细含义请参考 IstioOperator API 文档，这里我简要说明一下：\nistio-ingressgateway 的 Service 默认类型为 LoadBalancer，需将其改为 ClusterIP。 为防止集群资源紧张，更新配置后无法创建新的 Pod，需将滚动更新策略改为先删除旧的，再创建新的。 将 istio-ingressgateway 调度到指定节点。 默认情况下除了 istio-system namespace 之外，istio cni 插件会监视其他所有 namespace 中的 Pod，然而这并不能满足我们的需求，更严谨的做法是让 istio CNI 插件至少忽略 kube-system、istio-system 这两个 namespace，如果你还有其他的特殊的 namespace，也应该加上，例如 monitoring。 下面着重解释 overlays 列表中的字段：\nHostNetwork # 为了暴露 Ingress Gateway，我们可以使用 hostport 暴露端口，并将其调度到某个固定节点。如果你的 CNI 插件不支持 hostport，可以使用 HostNetwork 模式运行，但你会发现无法启动 ingressgateway 的 Pod，因为如果 Pod 设置了 HostNetwork=true，则 dnsPolicy 就会从 ClusterFirst 被强制转换成 Default。而 Ingress Gateway 启动过程中需要通过 DNS 域名连接 pilot 等其他组件，所以无法启动。\n我们可以通过强制将 dnsPolicy 的值设置为 ClusterFirstWithHostNet 来解决这个问题，详情参考： Kubernetes DNS 高阶指南。\n当然你可以部署完成之后再修改 Ingress Gateway 的 Deployment，但这种方式还是不太优雅。经过我对 IstioOperator API 文档 的研究，发现了一个更为优雅的方法，那就是直接修改资源对象 IstioOperator 的内容，在 components.ingressGateways 下面加上么一段：\noverlays: - apiVersion: apps/v1 kind: Deployment name: istio-ingressgateway patches: - path: spec.template.spec value: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet 具体含义我就不解释了，请看上篇文章。这里只对 IstioOperator 的语法做简单说明：\noverlays 列表用来修改对应组件的各个资源对象的 manifest，这里修改的是组件 Ingress Gateway 的 Deployment。 patches 列表里是实际要修改或添加的字段，我就不解释了，应该很好理解。 只暴露必要端口 # 从安全的角度来考虑，我们不应该暴露那些不必要的端口，对于 Ingress Gateway 来说，只需要暴露 HTTP、HTTPS 和 metrics 端口就够了。方法和上面一样，直接在 components.ingressGateways 的 overlays 列表下面加上这么一段：\n- apiVersion: v1 kind: Service name: istio-ingressgateway patches: - path: spec.ports value: - name: status-port port: 15020 targetPort: 15020 - name: http2 port: 80 targetPort: 80 - name: https port: 443 targetPort: 443 部署完成后，查看各组件状态：\n🐳 → kubectl -n istio-system get pod NAME READY STATUS RESTARTS AGE grafana-5cc7f86765-rt549 1/1 Running 0 3h11m istio-egressgateway-57999c5b76-59z8v 1/1 Running 0 3h11m istio-ingressgateway-5b97647565-zjz4k 1/1 Running 0 71m istio-tracing-8584b4d7f9-2jbjp 1/1 Running 0 3h11m istiod-86798869b8-jmk9v 1/1 Running 0 3h11m kiali-76f556db6d-qnsfj 1/1 Running 0 3h11m prometheus-6fd77b7876-c4vzn 2/2 Running 0 3h11m 🐳 → kubectl -n kube-system get pod -l k8s-app=istio-cni-node NAME READY STATUS RESTARTS AGE istio-cni-node-4dlfb 2/2 Running 0 3h12m istio-cni-node-4s9s7 2/2 Running 0 3h12m istio-cni-node-8g22x 2/2 Running 0 3h12m istio-cni-node-x2drr 2/2 Running 0 3h12m 可以看到 cni 插件已经安装成功，查看配置是否已经追加到 CNI 插件链的末尾：\n🐳 → cat /etc/cni/net.d/10-calico.conflist { \u0026#34;name\u0026#34;: \u0026#34;k8s-pod-network\u0026#34;, \u0026#34;cniVersion\u0026#34;: \u0026#34;0.3.1\u0026#34;, \u0026#34;plugins\u0026#34;: [ ... { \u0026#34;cniVersion\u0026#34;: \u0026#34;0.3.1\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;istio-cni\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;istio-cni\u0026#34;, \u0026#34;log_level\u0026#34;: \u0026#34;info\u0026#34;, \u0026#34;kubernetes\u0026#34;: { \u0026#34;kubeconfig\u0026#34;: \u0026#34;/etc/cni/net.d/ZZZ-istio-cni-kubeconfig\u0026#34;, \u0026#34;cni_bin_dir\u0026#34;: \u0026#34;/opt/cni/bin\u0026#34;, \u0026#34;exclude_namespaces\u0026#34;: [ \u0026#34;istio-system\u0026#34;, \u0026#34;kube-system\u0026#34;, \u0026#34;monitoring\u0026#34; ] } } ] } 暴露 Dashboard # 这个没什么好说的，通过 Ingress Controller 暴露就好了，可以参考我以前写的 Istio 1.0 部署。如果使用 Contour 的可以参考我的另一篇文章： Contour 学习笔记（一）：使用 Contour 接管 Kubernetes 的南北流量。\n这里我再介绍一种新的方式，istioctl 提供了一个子命令来从本地打开各种 Dashboard：\n🐳 → istioctl dashboard --help Access to Istio web UIs Usage: istioctl dashboard [flags] istioctl dashboard [command] Aliases: dashboard, dash, d Available Commands: controlz Open ControlZ web UI envoy Open Envoy admin web UI grafana Open Grafana web UI jaeger Open Jaeger web UI kiali Open Kiali web UI prometheus Open Prometheus web UI zipkin Open Zipkin web UI 例如，要想在本地打开 Grafana 页面，只需执行下面的命令：\n🐳 → istioctl dashboard grafana http://localhost:36813 咋一看可能觉得这个功能很鸡肋，我的集群又不是部署在本地，而且这个命令又不能指定监听的 IP，在本地用浏览器根本打不开呀！其实不然，你可以在本地安装 kubectl 和 istioctl 二进制文件，然后通过 kubeconfig 连接到集群，最后再在本地执行上面的命令，就可以打开页面啦，开发人员用来测试是不是很方便？Windows 用户当我没说。。。\n接下来我们就可以在浏览器中通过 Gateway 的 URL 来访问服务网格中的服务了。后面我会开启一系列实验教程，本文的所有步骤都是为后面做准备，如果想跟着我做后面的实验，请务必做好本文所述的准备工作。\n","date":"2019年12月15日","externalUrl":null,"permalink":"/posts/istio-deploy/","section":"博客","summary":"没错，Istio 架构又换了。。。北京时间 2020 年 3 月 6 日 凌晨发布","title":"Istio 1.5 部署指南","type":"posts"},{"content":"","date":"2019年12月8日","externalUrl":null,"permalink":"/categories/blog/","section":"分类","summary":"","title":"Blog","type":"categories"},{"content":"","date":"2019年12月8日","externalUrl":null,"permalink":"/tags/hugo/","section":"标签","summary":"","title":"Hugo","type":"tags"},{"content":"随着当前 Web 技术的日新月异，网页界面内容越来越丰富，让人眼花缭乱，其中就包括了网页中的各种自定义字体。\n例如，个人博客的首页字体：\nCSS3 引入的 @font-face 这一属性可以很好的解决这个问题，可以帮助我们非常灵活的使用一些特殊的字体，即使用户电脑里面没有安装这个字体，网页也可以显示。\nEOT 字体是 IE 浏览器的首选格式，其他浏览器都不支持；其他浏览器更钟爱常见的 TTF、SVG、WOFF。\n基本语法如下：\n@font-face { font-family: \u0026lt;自定义一个字体的名称\u0026gt;; src: url(\u0026#39;\u0026lt;下载好的字体，在电脑中保存的路径\u0026gt;\u0026#39;); font-variant: \u0026lt;font-variant\u0026gt;; font-stretch: \u0026lt;font-stretch\u0026gt;; font-style: \u0026lt;style\u0026gt;; font-weight: \u0026lt;weight\u0026gt;; 例如：\n@font-face { font-family: \u0026#39;Lora\u0026#39;; src: url(\u0026#39;../fonts/STKaiti.eot\u0026#39;); /* IE9 Compat Modes */ src: url(\u0026#39;../fonts/STKaiti.eot?#iefix\u0026#39;) format(\u0026#39;embedded-opentype\u0026#39;), /* IE6-IE8 */ url(\u0026#39;../fonts/STKaiti.woff2\u0026#39;) format(\u0026#39;woff2\u0026#39;), /* Super Modern Browsers */ url(\u0026#39;../fonts/STKaiti.woff\u0026#39;) format(\u0026#39;woff\u0026#39;), /* Modern Browsers */ url(\u0026#39;../fonts/STKaiti.ttf\u0026#39;) format(\u0026#39;truetype\u0026#39;), /* Safari, Android, iOS */ url(\u0026#39;../fonts/STKaiti.svg#STKaiti\u0026#39;) format(\u0026#39;svg\u0026#39;); /* Legacy iOS */ font-style: normal; font-weight: normal; } body { font-family: STKaiti; ... } 测试效果：Chrome，Firefox，IE7-IE11 均可以实现\n字体难题 # 自定义中文字体虽炫酷，但有一个弊端，那就是中文字体太大了，很耗费资源，具体的原因其实很简单：英文只有 26 个字母，一张 ASCII 码表上 128 个字符集几乎可以表示任何英文语句。由于字符集小，字体文件也可以做的非常小；中文字体就完全不同，单单 GB2313 编码的中文字符（含符号）就达到 7445 个，字符数量是 ASCII 码表的 58 倍，而字体设计师需要为每一个中文字符设计字体，简单计算下，中文字体文件大小也几乎达到英文字体文件的数十倍。\n解决思路 # 解决思路其实也很简单，只在字库中保留页面中出现的文字，将其他大量不用的文字删掉，生成一个只包含特定字符的小字体文件，便可以大大减少字体文件，从而提高访问速度。现在思路有了，那么有没有现成的工具呢？\n裁剪工具 # 还真有。经过我一番搜寻，找到了两款工具：一个是华人开发的「 字蛛」，英文名 font-spider，依赖 Node.js 环境，是一款命令行工具。主要思路是采集线上网页使用到的字体，从字体文件中分离出来，完成大幅度压缩。另一个是腾讯的大佬改版后的 font-soider，叫 font-spider-plus。它们的工作原理如下：\n我选择使用 font-spider-plus，毕竟改版过的，bug 更少，功能更多，还支持线上动态渲染的页面。唯一的不足就是官方文档写的太含糊了，许多人看了根本不知道怎么用。下面我将给我一个详细的范例，手把手教你如何使用 font-spider-plus。\nfont-spider-plus 使用方法 # 根据官方文档，要想使用 font-spider-plus，首先要在 CSS 文件中通过 @font-face 引入全量大小的特殊字体。具体怎么做呢？并没有说，我来告诉你。\n书写 HTML 文件 # 首先我们新建一个文件夹用来放 html 文件：\n$ mkdir index 然后在 index 目录中创建一个 index.html 文件，内容如下：\n\u0026lt;div class=\u0026#34;test\u0026#34;\u0026gt; 米开朗基杨 \u0026lt;/div\u0026gt; \u0026lt;style\u0026gt; @font-face { font-family: \u0026#39;font\u0026#39;; src: url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.eot\u0026#39;); src: url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.eot?#font-spider\u0026#39;) format(\u0026#39;embedded-opentype\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.woff2\u0026#39;) format(\u0026#39;woff2\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.woff\u0026#39;) format(\u0026#39;woff\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.ttf\u0026#39;) format(\u0026#39;truetype\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.svg\u0026#39;) format(\u0026#39;svg\u0026#39;); font-weight: normal; font-style: normal; } .test{ font-family: \u0026#39;font\u0026#39;; } \u0026lt;/style\u0026gt; 请将\u0026lt;div class=\u0026quot;test\u0026quot;\u0026gt; \u0026lt;/div\u0026gt; 中的文字换成你自己的网站的文字。你可以选择将你的博客所有文章内容全选，然后粘贴到此处。 下载你想使用的字体到 fonts 文件夹，然后将 index.html 中的 \u0026lt;font\u0026gt; 换成你下载的字体的前缀。 特别说明： @font-face 中的 src 定义的 .ttf 文件必须存在，其余的格式将由工具自动生成 下面是中文字体对应的英文名称：\n新细明体：PMingLiU 细明体：MingLiU 标楷体：DFKai-SB 黑体：SimHei 宋体：SimSun 新宋体：NSimSun 仿宋：FangSong 楷体：KaiTi 仿宋_GB2312：FangSong_GB2312 楷体_GB2312：KaiTi_GB2312 微软正黑体：Microsoft JhengHei 微软雅黑体：Microsoft YaHei 装Office会多出来的一些字体： 隶书：LiSu 幼圆：YouYuan 华文细黑：STXihei 华文楷体：STKaiti 华文宋体：STSong 华文中宋：STZhongsong 华文仿宋：STFangsong 方正舒体：FZShuTi 方正姚体：FZYaoti 华文彩云：STCaiyun 华文琥珀：STHupo 华文隶书：STLiti 华文行楷：STXingkai 华文新魏：STXinwei 苹果电脑中的字体： 华文细黑：STHeiti Light [STXihei] 华文黑体：STHeiti 华文楷体：STKaiti 华文宋体：STSong 华文仿宋：STFangsong 丽黑 Pro：LiHei Pro Medium 丽宋 Pro：LiSong Pro Light 标楷体：BiauKai 苹果丽中黑：Apple LiGothic Medium 苹果丽细宋：Apple LiSung Light 压缩本地 WebFont # 然后执行下面的命令来压缩本地 WebFont：\n$ fsp local index/index.html 哦对了，你需要先通过 npm 安装 fsp 命令：\n$ npm i font-spider-plus -g 压缩完成后，就会在 fonts 目录下生成压缩后的字体文件：\n$ ll fonts/ total 41328 -rw-rw-rw- 1 cnsgyg staff 7.7K 11 21 01:08 STKaiti.eot -rw-rw-rw- 1 cnsgyg staff 8.2K 11 21 01:08 STKaiti.svg -rw-rw-rw- 1 cnsgyg staff 7.6K 11 21 01:08 STKaiti.ttf -rw-rw-rw- 1 cnsgyg staff 7.7K 11 21 01:08 STKaiti.woff -rw-rw-rw- 1 cnsgyg staff 3.9K 11 21 01:08 STKaiti.woff2 压缩之前的字体文件会被移到 fonts 目录下的 .font-spider 目录：\n$ ll fonts/.font-spider total 24880 -rw-rw-rw- 1 cnsgyg staff 12M 11 21 01:08 STKaiti.ttf 书写 CSS # 现在字体压缩完了，怎么应用到自己的网站中呢？也很简单，先写个 CSS 通过 @font-faxe 引入压缩后的字体，格式与第一步中的 index.html 类似：\n/* fonts-zh.css */ @font-face { font-family: \u0026#39;font\u0026#39;; src: url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.eot\u0026#39;); src: url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.eot?#font-spider\u0026#39;) format(\u0026#39;embedded-opentype\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.woff2\u0026#39;) format(\u0026#39;woff2\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.woff\u0026#39;) format(\u0026#39;woff\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.ttf\u0026#39;) format(\u0026#39;truetype\u0026#39;), url(\u0026#39;../fonts/\u0026lt;font\u0026gt;.svg\u0026#39;) format(\u0026#39;svg\u0026#39;); font-weight: normal; font-style: normal; } 这样还不行，你还需要将压缩后的字体文件拷贝你的网站中，CSS 中通过相对路径要能找到这些字体文件。可我不想这么做，太麻烦了，我还想更简单点。\nbase64 编码 # 灵机一动，想到了 base64，编码之后可以不用拷贝这些字体文件，还能减少网站字体的加载体积，真是一箭双雕啊！具体的步骤我就不解释了，直接把所有步骤放到脚本中：\n#!/bin/bash font=STKaiti eot=$(cat fonts/$font.eot|base64|tr -d \u0026#39;\\n\u0026#39;) woff=$(cat fonts/$font.woff|base64|tr -d \u0026#39;\\n\u0026#39;) woff2=$(cat fonts/$font.woff2|base64|tr -d \u0026#39;\\n\u0026#39;) ttf=$(cat fonts/$font.ttf|base64|tr -d \u0026#39;\\n\u0026#39;) svg=$(cat fonts/$font.svg|base64|tr -d \u0026#39;\\n\u0026#39;) cat \u0026gt; fonts-zh.css \u0026lt;\u0026lt;EOF @font-face { font-family: \u0026#39;$font\u0026#39;; src: url(data:application/font-eot;charset=utf-8;base64,$eot) format(\u0026#39;eot\u0026#39;); font-weight: normal; font-style: normal; } @font-face { font-family: \u0026#39;$font\u0026#39;; src: url(data:application/font-woff2;charset=utf-8;base64,$woff2) format(\u0026#39;woff2\u0026#39;), url(data:application/font-woff;charset=utf-8;base64,$woff) format(\u0026#39;woff\u0026#39;), url(data:application/font-ttf;charset=utf-8;base64,$ttf) format(\u0026#39;truetype\u0026#39;), url(data:application/font-svg;charset=utf-8;base64,$svg) format(\u0026#39;svg\u0026#39;); font-weight: normal; font-style: normal; } EOF 执行完上面的脚本后，就生成了一个 fonts-zh.css，这是我们唯一需要的东西，不再需要任何额外的文件。\n引入 CSS # 最后一步就是在你的网站中引入该 CSS，具体的做法大同小异，以 hugo 为例，先将 fonts-zh.css 复制到网站主题目录的 static/css/ 目录下，然后在 \u0026lt;head\u0026gt;\u0026lt;/head\u0026gt; 中引入该 css，以 beatifulhugo 主题为例，直接在 layouts/partials/head_custom.html 中加上下面一行：\n\u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ \u0026#34;css/fonts-zh.css\u0026#34; | absURL }}\u0026#34; /\u0026gt; 最后让网站的 body 使用该中文字体，具体的做法是修改 body 的 css，以 hugo 的 beatifulhugo 主题为例，修改 static/css/main.css 中的 body 属性：\nbody { font-family: STKaiti; ... } 可以再加上备用字体，例如：\nbody { font-family: STKaiti,Cambria; ... } 表示如果 STKaiti 字体不可用，将使用 Cambria 字体。到这里就大功告成了，具体的效果可以参考我的网站： https://icloudnative.io/。\n总结 # 如果你没有强迫症，到这一步就大功告成了，可我还觉得不够简单，那么多步骤实在是太繁琐了，我要让它们全部自动化，把所有的步骤放到一个自动化脚本中。这还不够，为了造福大众，我在 GitHUb 中新建了一个仓库，所有的脚本和步骤都在上面，有需求的小伙伴可以拿去 happy 啦~~\n项目地址： https://github.com/yangchuansheng/font-spider-plus\n参考资料 # 如何优雅的在网页里使用中文字体 字蛛（font-spider）让你爱上 @font-face 网页自定义字体 ","date":"2019年12月8日","externalUrl":null,"permalink":"/posts/font-spider-plus/","section":"博客","summary":"随着当前 Web 技术的日新月异，网页界面内容越来越丰富，让人眼花缭","title":"使用 font-spider 对 webfont 网页字体进行压缩","type":"posts"},{"content":"在 Kubernetes 社区中，PLEG is not healthy 成名已久，只要出现这个报错，就有很大概率造成 Node 状态变成 NotReady。社区相关的 issue 也有一大把，先列几个给你们看看：\nhttps://stackoverflow.com/questions/53872739/how-to-fix-container-runtime-is-down-pleg-is-not-healthy https://github.com/kubernetes/kubernetes/issues/45419 https://github.com/kubernetes/kubernetes/issues/61117 https://github.com/kubernetes/kubernetes/issues/72533 https://github.com/Azure/AKS/issues/102 本文我将尝试解释 PLEG 的工作原理，只要理解了工作原理，再遇到类似的问题就有排查思路了。\nPLEG 是个啥？ # PLEG 全称叫 Pod Lifecycle Event Generator，即 Pod 生命周期事件生成器。实际上它只是 Kubelet 中的一个模块，主要职责就是通过每个匹配的 Pod 级别事件来调整容器运行时的状态，并将调整的结果写入缓存，使 Pod 的缓存保持最新状态。先来聊聊 PLEG 的出现背景。\n在 Kubernetes 中，每个节点上都运行着一个守护进程 Kubelet 来管理节点上的容器，调整容器的实际状态以匹配 spec 中定义的状态。具体来说，Kubelet 需要对两个地方的更改做出及时的回应：\nPod spec 中定义的状态 容器运行时的状态 对于 Pod，Kubelet 会从多个数据来源 watch Pod spec 中的变化。对于容器，Kubelet 会定期（例如，10s）轮询容器运行时，以获取所有容器的最新状态。\n随着 Pod 和容器数量的增加，轮询会产生不可忽略的开销，并且会由于 Kubelet 的并行操作而加剧这种开销（为每个 Pod 分配一个 goruntine，用来获取容器的状态）。轮询带来的周期性大量并发请求会导致较高的 CPU 使用率峰值（即使 Pod 的定义和容器的状态没有发生改变），降低性能。最后容器运行时可能不堪重负，从而降低系统的可靠性，限制 Kubelet 的可扩展性。\n为了降低 Pod 的管理开销，提升 Kubelet 的性能和可扩展性，引入了 PLEG，改进了之前的工作方式：\n减少空闲期间的不必要工作（例如 Pod 的定义和容器的状态没有发生更改）。 减少获取容器状态的并发请求数量。 整体的工作流程如下图所示，虚线部分是 PLEG 的工作内容。\nPLEG is not healthy 是如何发生的？ # Healthy() 函数会以 “PLEG” 的形式添加到 runtimeState 中，Kubelet 在一个同步循环（SyncLoop() 函数）中会定期（默认是 10s）调用 Healthy() 函数。Healthy() 函数会检查 relist 进程（PLEG 的关键任务）是否在 3 分钟内完成。如果 relist 进程的完成时间超过了 3 分钟，就会报告 PLEG is not healthy。\n我会在流程的每一步通过源代码解释其相关的工作原理，源代码基于 Kubernetes 1.11（Openshift 3.11）。如果你不熟悉 Go 的语法也不用担心，只需要看代码中的注释就能明白其原理。我也会在放出代码之前先解读一番，并从源代码中裁剪掉不太重要的内容以提高代码的可读性。下面是调用 healthy() 函数的相关代码：\n//// pkg/kubelet/pleg/generic.go - Healthy() // The threshold needs to be greater than the relisting period + the // relisting time, which can vary significantly. Set a conservative // threshold to avoid flipping between healthy and unhealthy. relistThreshold = 3 * time.Minute : func (g *GenericPLEG) Healthy() (bool, error) { relistTime := g.getRelistTime() elapsed := g.clock.Since(relistTime) if elapsed \u0026gt; relistThreshold { return false, fmt.Errorf(\u0026#34;pleg was last seen active %v ago; threshold is %v\u0026#34;, elapsed, relistThreshold) } return true, nil } //// pkg/kubelet/kubelet.go - NewMainKubelet() func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, ... : klet.runtimeState.addHealthCheck(\u0026#34;PLEG\u0026#34;, klet.pleg.Healthy) //// pkg/kubelet/kubelet.go - syncLoop() func (kl *Kubelet) syncLoop(updates \u0026lt;-chan kubetypes.PodUpdate, handler SyncHandler) { : // The resyncTicker wakes up kubelet to checks if there are any pod workers // that need to be sync\u0026#39;d. A one-second period is sufficient because the // sync interval is defaulted to 10s. : const ( base = 100 * time.Millisecond max = 5 * time.Second factor = 2 ) duration := base for { if rs := kl.runtimeState.runtimeErrors(); len(rs) != 0 { glog.Infof(\u0026#34;skipping pod synchronization - %v\u0026#34;, rs) // exponential backoff time.Sleep(duration) duration = time.Duration(math.Min(float64(max), factor*float64(duration))) continue } : } : } //// pkg/kubelet/runtime.go - runtimeErrors() func (s *runtimeState) runtimeErrors() []string { : for _, hc := range s.healthChecks { if ok, err := hc.fn(); !ok { ret = append(ret, fmt.Sprintf(\u0026#34;%s is not healthy: %v\u0026#34;, hc.name, err)) } } : } 深入解读 relist 函数 # 上文提到 healthy() 函数会检查 relist 的完成时间，但 relist 究竟是用来干嘛的呢？解释 relist 之前，要先解释一下 Pod 的生命周期事件。Pod 的生命周期事件是在 Pod 层面上对底层容器状态改变的抽象，使其与底层的容器运行时无关，这样就可以让 Kubelet 不受底层容器运行时的影响。\ntype PodLifeCycleEventType string const ( ContainerStarted PodLifeCycleEventType = \u0026#34;ContainerStarted\u0026#34; ContainerStopped PodLifeCycleEventType = \u0026#34;ContainerStopped\u0026#34; NetworkSetupCompleted PodLifeCycleEventType = \u0026#34;NetworkSetupCompleted\u0026#34; NetworkFailed PodLifeCycleEventType = \u0026#34;NetworkFailed\u0026#34; ) // PodLifecycleEvent is an event reflects the change of the pod state. type PodLifecycleEvent struct { // The pod ID. ID types.UID // The type of the event. Type PodLifeCycleEventType // The accompanied data which varies based on the event type. Data interface{} } 以 Docker 为例，在 Pod 中启动一个 infra 容器就会在 Kubelet 中注册一个 NetworkSetupCompleted Pod 生命周期事件。\n那么 PLEG 是如何知道新启动了一个 infra 容器呢？它会定期重新列出节点上的所有容器（例如 docker ps），并与上一次的容器列表进行对比，以此来判断容器状态的变化。其实这就是 relist() 函数干的事情，尽管这种方法和以前的 Kubelet 轮询类似，但现在只有一个线程，就是 PLEG。现在不需要所有的线程并发获取容器的状态，只有相关的线程会被唤醒用来同步容器状态。而且 relist 与容器运行时无关，也不需要外部依赖，简直完美。\n下面我们来看一下 relist() 函数的内部实现。完整的流程如下图所示：\n注意图中的 RPC 调用部分，后文将会拎出来详细解读。完整的源代码在 这里。\n尽管每秒钟调用一次 relist，但它的完成时间仍然有可能超过 1s。因为下一次调用 relist 必须得等上一次 relist 执行结束，设想一下，如果容器运行时响应缓慢，或者一个周期内有大量的容器状态发生改变，那么 relist 的完成时间将不可忽略，假设是 5s，那么下一次调用 relist 将要等到 6s 之后。\n相关的源代码如下：\n//// pkg/kubelet/kubelet.go - NewMainKubelet() // Generic PLEG relies on relisting for discovering container events. // A longer period means that kubelet will take longer to detect container // changes and to update pod status. On the other hand, a shorter period // will cause more frequent relisting (e.g., container runtime operations), // leading to higher cpu usage. // Note that even though we set the period to 1s, the relisting itself can // take more than 1s to finish if the container runtime responds slowly // and/or when there are many container changes in one cycle. plegRelistPeriod = time.Second * 1 // NewMainKubelet instantiates a new Kubelet object along with all the required internal modules. // No initialization of Kubelet and its modules should happen here. func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, ... : klet.pleg = pleg.NewGenericPLEG(klet.containerRuntime, plegChannelCapacity, plegRelistPeriod, klet.podCache, clock.RealClock{}) //// pkg/kubelet/pleg/generic.go - Start() // Start spawns a goroutine to relist periodically. func (g *GenericPLEG) Start() { go wait.Until(g.relist, g.relistPeriod, wait.NeverStop) } //// pkg/kubelet/pleg/generic.go - relist() func (g *GenericPLEG) relist() { ... WE WILL REVIEW HERE ... } 回到上面那幅图，relist 函数第一步就是记录 Kubelet 的相关指标（例如 kubelet_pleg_relist_latency_microseconds），然后通过 CRI 从容器运行时获取当前的 Pod 列表（包括停止的 Pod）。该 Pod 列表会和之前的 Pod 列表进行比较，检查哪些状态发生了变化，然后同时生成相关的 Pod 生命周期事件和更改后的状态。\n//// pkg/kubelet/pleg/generic.go - relist() : // get a current timestamp timestamp := g.clock.Now() // kubelet_pleg_relist_latency_microseconds for prometheus metrics defer func() { metrics.PLEGRelistLatency.Observe(metrics.SinceInMicroseconds(timestamp)) }() // Get all the pods. podList, err := g.runtime.GetPods(true) : 其中 GetPods() 函数的调用堆栈如下图所示：\n相关的源代码如下：\n//// pkg/kubelet/kuberuntime/kuberuntime_manager.go - GetPods() // GetPods returns a list of containers grouped by pods. The boolean parameter // specifies whether the runtime returns all containers including those already // exited and dead containers (used for garbage collection). func (m *kubeGenericRuntimeManager) GetPods(all bool) ([]*kubecontainer.Pod, error) { pods := make(map[kubetypes.UID]*kubecontainer.Pod) sandboxes, err := m.getKubeletSandboxes(all) : } //// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go - getKubeletSandboxes() // getKubeletSandboxes lists all (or just the running) sandboxes managed by kubelet. func (m *kubeGenericRuntimeManager) getKubeletSandboxes(all bool) ([]*runtimeapi.PodSandbox, error) { : resp, err := m.runtimeService.ListPodSandbox(filter) : } //// pkg/kubelet/remote/remote_runtime.go - ListPodSandbox() // ListPodSandbox returns a list of PodSandboxes. func (r *RemoteRuntimeService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) { : resp, err := r.runtimeClient.ListPodSandbox(ctx, \u0026amp;runtimeapi.ListPodSandboxRequest{ : return resp.Items, nil } 获取所有的 Pod 列表后，relist 的完成时间就会更新成当前的时间戳。也就是说，Healthy() 函数可以根据这个时间戳来评估 relist 是否超过了 3 分钟。\n//// pkg/kubelet/pleg/generic.go - relist() // update as a current timestamp g.updateRelistTime(timestamp) 将当前的 Pod 列表和上一次 relist 的 Pod 列表进行对比之后，就会针对每一个变化生成相应的 Pod 级别的事件。相关的源代码如下：\n//// pkg/kubelet/pleg/generic.go - relist() pods := kubecontainer.Pods(podList) g.podRecords.setCurrent(pods) // Compare the old and the current pods, and generate events. eventsByPodID := map[types.UID][]*PodLifecycleEvent{} for pid := range g.podRecords { oldPod := g.podRecords.getOld(pid) pod := g.podRecords.getCurrent(pid) // Get all containers in the old and the new pod. allContainers := getContainersFromPods(oldPod, pod) for _, container := range allContainers { events := computeEvents(oldPod, pod, \u0026amp;container.ID) for _, e := range events { updateEvents(eventsByPodID, e) } } } 其中 generateEvents() 函数（computeEvents() 函数会调用它）用来生成相应的 Pod 级别的事件（例如 ContainerStarted、ContainerDied 等等），然后通过 updateEvents() 函数来更新事件。\ncomputeEvents() 函数的内容如下：\n//// pkg/kubelet/pleg/generic.go - computeEvents() func computeEvents(oldPod, newPod *kubecontainer.Pod, cid *kubecontainer.ContainerID) []*PodLifecycleEvent { : return generateEvents(pid, cid.ID, oldState, newState) } //// pkg/kubelet/pleg/generic.go - generateEvents() func generateEvents(podID types.UID, cid string, oldState, newState plegContainerState) []*PodLifecycleEvent { : glog.V(4).Infof(\u0026#34;GenericPLEG: %v/%v: %v -\u0026gt; %v\u0026#34;, podID, cid, oldState, newState) switch newState { case plegContainerRunning: return []*PodLifecycleEvent{{ID: podID, Type: ContainerStarted, Data: cid}} case plegContainerExited: return []*PodLifecycleEvent{{ID: podID, Type: ContainerDied, Data: cid}} case plegContainerUnknown: return []*PodLifecycleEvent{{ID: podID, Type: ContainerChanged, Data: cid}} case plegContainerNonExistent: switch oldState { case plegContainerExited: // We already reported that the container died before. return []*PodLifecycleEvent{{ID: podID, Type: ContainerRemoved, Data: cid}} default: return []*PodLifecycleEvent{{ID: podID, Type: ContainerDied, Data: cid}, {ID: podID, Type: ContainerRemoved, Data: cid}} } default: panic(fmt.Sprintf(\u0026#34;unrecognized container state: %v\u0026#34;, newState)) } } relist 的最后一个任务是检查是否有与 Pod 关联的事件，并按照下面的流程更新 podCache。\n//// pkg/kubelet/pleg/generic.go - relist() // If there are events associated with a pod, we should update the // podCache. for pid, events := range eventsByPodID { pod := g.podRecords.getCurrent(pid) if g.cacheEnabled() { // updateCache() will inspect the pod and update the cache. If an // error occurs during the inspection, we want PLEG to retry again // in the next relist. To achieve this, we do not update the // associated podRecord of the pod, so that the change will be // detect again in the next relist. // TODO: If many pods changed during the same relist period, // inspecting the pod and getting the PodStatus to update the cache // serially may take a while. We should be aware of this and // parallelize if needed. if err := g.updateCache(pod, pid); err != nil { glog.Errorf(\u0026#34;PLEG: Ignoring events for pod %s/%s: %v\u0026#34;, pod.Name, pod.Namespace, err) : } : } // Update the internal storage and send out the events. g.podRecords.update(pid) for i := range events { // Filter out events that are not reliable and no other components use yet. if events[i].Type == ContainerChanged { continue } g.eventChannel \u0026lt;- events[i] } } updateCache() 将会检查每个 Pod，并在单个循环中依次对其进行更新。因此，如果在同一个 relist 中更改了大量的 Pod，那么 updateCache 过程将会成为瓶颈。最后，更新后的 Pod 生命周期事件将会被发送到 eventChannel。\n某些远程客户端还会调用每一个 Pod 来获取 Pod 的 spec 定义信息，这样一来，Pod 数量越多，延时就可能越高，因为 Pod 越多就会生成越多的事件。\nupdateCache() 的详细调用堆栈如下图所示，其中 GetPodStatus() 用来获取 Pod 的 spec 定义信息：\n完整的代码如下：\n//// pkg/kubelet/pleg/generic.go - updateCache() func (g *GenericPLEG) updateCache(pod *kubecontainer.Pod, pid types.UID) error { : timestamp := g.clock.Now() // TODO: Consider adding a new runtime method // GetPodStatus(pod *kubecontainer.Pod) so that Docker can avoid listing // all containers again. status, err := g.runtime.GetPodStatus(pod.ID, pod.Name, pod.Namespace) : g.cache.Set(pod.ID, status, err, timestamp) return err } //// pkg/kubelet/kuberuntime/kuberuntime_manager.go - GetPodStatus() // GetPodStatus retrieves the status of the pod, including the // information of all containers in the pod that are visible in Runtime. func (m *kubeGenericRuntimeManager) GetPodStatus(uid kubetypes.UID, name, namespace string) (*kubecontainer.PodStatus, error) { podSandboxIDs, err := m.getSandboxIDByPodUID(uid, nil) : for idx, podSandboxID := range podSandboxIDs { podSandboxStatus, err := m.runtimeService.PodSandboxStatus(podSandboxID) : } // Get statuses of all containers visible in the pod. containerStatuses, err := m.getPodContainerStatuses(uid, name, namespace) : } //// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go - getSandboxIDByPodUID() // getPodSandboxID gets the sandbox id by podUID and returns ([]sandboxID, error). // Param state could be nil in order to get all sandboxes belonging to same pod. func (m *kubeGenericRuntimeManager) getSandboxIDByPodUID(podUID kubetypes.UID, state *runtimeapi.PodSandboxState) ([]string, error) { : sandboxes, err := m.runtimeService.ListPodSandbox(filter) : return sandboxIDs, nil } //// pkg/kubelet/remote/remote_runtime.go - PodSandboxStatus() // PodSandboxStatus returns the status of the PodSandbox. func (r *RemoteRuntimeService) PodSandboxStatus(podSandBoxID string) (*runtimeapi.PodSandboxStatus, error) { ctx, cancel := getContextWithTimeout(r.timeout) defer cancel() resp, err := r.runtimeClient.PodSandboxStatus(ctx, \u0026amp;runtimeapi.PodSandboxStatusRequest{ PodSandboxId: podSandBoxID, }) : return resp.Status, nil } //// pkg/kubelet/kuberuntime/kuberuntime_container.go - getPodContainerStatuses() // getPodContainerStatuses gets all containers\u0026#39; statuses for the pod. func (m *kubeGenericRuntimeManager) getPodContainerStatuses(uid kubetypes.UID, name, namespace string) ([]*kubecontainer.ContainerStatus, error) { // Select all containers of the given pod. containers, err := m.runtimeService.ListContainers(\u0026amp;runtimeapi.ContainerFilter{ LabelSelector: map[string]string{types.KubernetesPodUIDLabel: string(uid)}, }) : // TODO: optimization: set maximum number of containers per container name to examine. for i, c := range containers { status, err := m.runtimeService.ContainerStatus(c.Id) : } : return statuses, nil } 上面就是 relist() 函数的完整调用堆栈，我在讲解的过程中结合了相关的源代码，希望能为你提供有关 PLEG 的更多细节。为了实时了解 PLEG 的健康状况，最好的办法就是监控 relist。\n监控 relist # 我们可以通过监控 Kubelet 的指标来了解 relist 的延时。relist 的调用周期是 1s，那么 relist 的完成时间 + 1s 就等于 kubelet_pleg_relist_interval_microseconds 指标的值。你也可以监控容器运行时每个操作的延时，这些指标在排查故障时都能提供线索。\n你可以在每个节点上通过访问 URL https://127.0.0.1:10250/metrics 来获取 Kubelet 的指标。\n# HELP kubelet_pleg_relist_interval_microseconds Interval in microseconds between relisting in PLEG. # TYPE kubelet_pleg_relist_interval_microseconds summary kubelet_pleg_relist_interval_microseconds{quantile=\u0026#34;0.5\u0026#34;} 1.054052e+06 kubelet_pleg_relist_interval_microseconds{quantile=\u0026#34;0.9\u0026#34;} 1.074873e+06 kubelet_pleg_relist_interval_microseconds{quantile=\u0026#34;0.99\u0026#34;} 1.126039e+06 kubelet_pleg_relist_interval_microseconds_count 5146 # HELP kubelet_pleg_relist_latency_microseconds Latency in microseconds for relisting pods in PLEG. # TYPE kubelet_pleg_relist_latency_microseconds summary kubelet_pleg_relist_latency_microseconds{quantile=\u0026#34;0.5\u0026#34;} 53438 kubelet_pleg_relist_latency_microseconds{quantile=\u0026#34;0.9\u0026#34;} 74396 kubelet_pleg_relist_latency_microseconds{quantile=\u0026#34;0.99\u0026#34;} 115232 kubelet_pleg_relist_latency_microseconds_count 5106 # HELP kubelet_runtime_operations Cumulative number of runtime operations by operation type. # TYPE kubelet_runtime_operations counter kubelet_runtime_operations{operation_type=\u0026#34;container_status\u0026#34;} 472 kubelet_runtime_operations{operation_type=\u0026#34;create_container\u0026#34;} 93 kubelet_runtime_operations{operation_type=\u0026#34;exec\u0026#34;} 1 kubelet_runtime_operations{operation_type=\u0026#34;exec_sync\u0026#34;} 533 kubelet_runtime_operations{operation_type=\u0026#34;image_status\u0026#34;} 579 kubelet_runtime_operations{operation_type=\u0026#34;list_containers\u0026#34;} 10249 kubelet_runtime_operations{operation_type=\u0026#34;list_images\u0026#34;} 782 kubelet_runtime_operations{operation_type=\u0026#34;list_podsandbox\u0026#34;} 10154 kubelet_runtime_operations{operation_type=\u0026#34;podsandbox_status\u0026#34;} 315 kubelet_runtime_operations{operation_type=\u0026#34;pull_image\u0026#34;} 57 kubelet_runtime_operations{operation_type=\u0026#34;remove_container\u0026#34;} 49 kubelet_runtime_operations{operation_type=\u0026#34;run_podsandbox\u0026#34;} 28 kubelet_runtime_operations{operation_type=\u0026#34;start_container\u0026#34;} 93 kubelet_runtime_operations{operation_type=\u0026#34;status\u0026#34;} 1116 kubelet_runtime_operations{operation_type=\u0026#34;stop_container\u0026#34;} 9 kubelet_runtime_operations{operation_type=\u0026#34;stop_podsandbox\u0026#34;} 33 kubelet_runtime_operations{operation_type=\u0026#34;version\u0026#34;} 564 # HELP kubelet_runtime_operations_latency_microseconds Latency in microseconds of runtime operations. Broken down by operation type. # TYPE kubelet_runtime_operations_latency_microseconds summary kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;container_status\u0026#34;,quantile=\u0026#34;0.5\u0026#34;} 12117 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;container_status\u0026#34;,quantile=\u0026#34;0.9\u0026#34;} 26607 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;container_status\u0026#34;,quantile=\u0026#34;0.99\u0026#34;} 27598 kubelet_runtime_operations_latency_microseconds_count{operation_type=\u0026#34;container_status\u0026#34;} 486 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;list_containers\u0026#34;,quantile=\u0026#34;0.5\u0026#34;} 29972 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;list_containers\u0026#34;,quantile=\u0026#34;0.9\u0026#34;} 47907 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;list_containers\u0026#34;,quantile=\u0026#34;0.99\u0026#34;} 80982 kubelet_runtime_operations_latency_microseconds_count{operation_type=\u0026#34;list_containers\u0026#34;} 10812 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;list_podsandbox\u0026#34;,quantile=\u0026#34;0.5\u0026#34;} 18053 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;list_podsandbox\u0026#34;,quantile=\u0026#34;0.9\u0026#34;} 28116 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;list_podsandbox\u0026#34;,quantile=\u0026#34;0.99\u0026#34;} 68748 kubelet_runtime_operations_latency_microseconds_count{operation_type=\u0026#34;list_podsandbox\u0026#34;} 10712 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;podsandbox_status\u0026#34;,quantile=\u0026#34;0.5\u0026#34;} 4918 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;podsandbox_status\u0026#34;,quantile=\u0026#34;0.9\u0026#34;} 15671 kubelet_runtime_operations_latency_microseconds{operation_type=\u0026#34;podsandbox_status\u0026#34;,quantile=\u0026#34;0.99\u0026#34;} 18398 kubelet_runtime_operations_latency_microseconds_count{operation_type=\u0026#34;podsandbox_status\u0026#34;} 323 可以通过 Prometheus 对其进行监控：\n总结 # 以我的经验，造成 PLEG is not healthy 的因素有很多，而且我相信还有更多潜在的因素我们还没有遇到过。我只提供几个我能想到的原因：\nRPC 调用过程中容器运行时响应超时（有可能是性能下降，死锁或者出现了 bug）。 节点上的 Pod 数量太多，导致 relist 无法在 3 分钟内完成。事件数量和延时与 Pod 数量成正比，与节点资源无关。 relist 出现了死锁，该 bug 已在 Kubernetes 1.14 中修复。 获取 Pod 的网络堆栈信息时 CNI 出现了 bug。 参考资料 # Kubelet: Pod Lifecycle Event Generator (PLEG) Kubelet: Runtime Pod Cache relist() in kubernetes/pkg/kubelet/pleg/generic.go Past bug about CNI — PLEG is not healthy error, node marked NotReady ","date":"2019年12月1日","externalUrl":null,"permalink":"/posts/understanding-the-pleg-is-not-healthy/","section":"博客","summary":"在 Kubernetes 社区中，PLEG is not healthy 成名已久，只要出现这个报错，就有很","title":"深入理解 Kubelet 中的 PLEG is not healthy","type":"posts"},{"content":"在工作和生活中，我们可能经常需要将某个程序跑在不同的 CPU 架构上，比如让某些不可描述的软件运行在树莓派或嵌入式路由器设备上。特别是 Docker 席卷全球之后，我们可以轻松地在 ARM 设备上通过容器部署各种好玩的应用，而不用在意各种系统的差异性。\n但是想要跨平台构建 Docker 镜像可不是一件轻松的活，要么到不同 CPU 架构的系统上全部构建一遍，要么就得在当前系统上通过虚拟化技术模拟不同的 CPU 架构，最后可能还要想办法合并镜像，费力不讨好。\n不过值得庆幸的是，Docker 19.03 引入了一个新的实验性插件，该插件使得跨平台构建 Docker 镜像比以往更加容易了。在介绍这个新特性之前，我们先来了解一下跨 CPU 架构构建程序的基础知识。\n跨 CPU 架构编译程序的方法 # 先来快速回顾一下当前跨 CPU 架构编译程序的不同方法。\n方法一：直接在目标硬件上编译 # 如果你能够访问目标 CPU 架构的系统，并且该操作系统支持运行构建所需的各种工具，那么你可以直接在目标系统上编译程序。\n以构建 Docker 镜像为例，你可以在树莓派上安装 Docker，然后在树莓派上通过 Dockerfile 直接构建 arm 平台的镜像。\n如果无法访问目标 CPU 架构的系统该怎么办？有没有办法通过某种方式直接在当前系统上构建目标 CPU 架构的程序？请看下文\u0026hellip;\n方法二：模拟目标硬件 # 还记得我们小时候在各种网吧台球室之类的场合玩的街机游戏吗？放张图给你们回忆一下：\n如果现在我们想重新体验以前玩过的街机游戏该怎么办？这时候就需要用到模拟器（Emulator）了。借助模拟器，我们可以让时光倒流，体验经典游戏的乐趣。\n模拟器除了可以用来玩游戏之外，还可以用来跨 CPU 架构构建程序。最常用的模拟器是开源的 QEMU，QEMU 支持许多常见的 CPU 架构，包括 ARM、Power-PC 和 RISC-V 等。通过模拟一个完整的操作系统，可以创建通用的 ARM 虚拟机，该虚拟机可以引导 Linux，设置开发环境，也可以在虚拟机内编译程序。\n然而，模拟整个操作系统还是有点浪费，因为在这种模式下，QEMU 将会模拟整个系统，包括计时器、内存控制器、总线控制器等硬件。但编译程序根本不需要关心这些，还可以再精简些。\n方法三：通过 binfmt_misc 模拟目标硬件的用户空间 # 在 Linux 上，QEMU 除了可以模拟完整的操作系统之外，还有另外一种模式叫 用户态模式（User mod）。该模式下 QEMU 将通过 binfmt_misc 在 Linux 内核中注册一个二进制转换处理程序，并在程序运行时动态翻译二进制文件，根据需要将系统调用从目标 CPU 架构转换为当前系统的 CPU 架构。最终的效果看起来就像在本地运行目标 CPU 架构的二进制文件。\n通过 QEMU 的用户态模式，我们可以创建轻量级的虚拟机（ chroot 或容器），然后在虚拟机系统中编译程序，和本地编译一样简单轻松。后面我们就会看到，跨平台构建 Docker 镜像用的就是这个方法。\n方法四：使用交叉编译器 # 最后介绍一种嵌入式系统社区常用的方法：交叉编译（cross-compilation）。\n交叉编译器是专门为在给定的系统平台上运行而设计的编译器，但是可以编译出另一个系统平台的可执行文件。例如，amd64 架构的 Linux 系统上的 C++ 交叉编译器可以编译出运行在 aarch64(64-bit ARM) 架构的嵌入式设备上的可执行文件。再举个真实的例子，安卓设备的 APP 基本上都是通过这种方法来编译的。\n从性能角度来看，该方法与方法一没什么区别，因为不需要模拟器的参与，几乎没有性能损耗。但交叉编译不具有通用性，它的复杂度取决于程序使用的语言，如果使用 Golang 的话，那就超级容易了。\n在全民容器时代，我们讨论构建时不仅包括构建单个可执行文件，还包括构建容器镜像。而且构建容器镜像比上面说的方法更复杂，再加上 Docker 本身的复杂性，这几乎是一个老大难的问题。\n但引入了新的实验性插件之后，构建多平台架构的 Docker 镜像就比以前容易多了，至于这个插件到底是啥，下文会详细介绍。\n构建多平台 Docker 镜像 # 利用 Docker 19.03 引入的插件 buildx，可以很轻松地构建多平台 Docker 镜像。buildx 是 docker build ... 命令的下一代替代品，它利用 BuildKit 的全部功能扩展了 docker build 的功能。\n下面就来演示一下如何在短短几分钟内使用 buildx 构建出不同平台的 Docker 镜像。步骤如下：\n启用 buildx 插件 # 要想使用 buildx，首先要确保 Docker 版本不低于 19.03，同时还要通过设置环境变量 DOCKER_CLI_EXPERIMENTAL 来启用。可以通过下面的命令来为当前终端启用 buildx 插件：\n🐳 → export DOCKER_CLI_EXPERIMENTAL=enabled 验证是否开启：\n🐳 → docker buildx version github.com/docker/buildx v0.3.1-tp-docker 6db68d029599c6710a32aa7adcba8e5a344795a7 如果在某些系统上设置环境变量 DOCKER_CLI_EXPERIMENTAL 不生效（比如 Arch Linux）,你可以选择从源代码编译：\n🐳 → export DOCKER_BUILDKIT=1 🐳 → docker build --platform=local -o . git://github.com/docker/buildx 🐳 → mkdir -p ~/.docker/cli-plugins \u0026amp;\u0026amp; mv buildx ~/.docker/cli-plugins/docker-buildx 启用 binfmt_misc # 如果你使用的是 Docker 桌面版（MacOS 和 Windows），默认已经启用了 binfmt_misc，可以跳过这一步。 如果你使用的是 Linux，需要手动启用 binfmt_misc。大多数 Linux 发行版都很容易启用，不过还有一个更容易的办法，直接运行一个特权容器，容器里面写好了设置脚本：\n🐳 → docker run --privileged --rm tonistiigi/binfmt --install all 建议将 Linux 内核版本升级到 4.x 以上，特别是 CentOS 用户，你可能会遇到错误。 验证是 binfmt_misc 否开启：\n🐳 → ls -al /proc/sys/fs/binfmt_misc/ 总用量 0 总用量 0 -rw-r--r-- 1 root root 0 11月 18 00:12 qemu-aarch64 -rw-r--r-- 1 root root 0 11月 18 00:12 qemu-arm -rw-r--r-- 1 root root 0 11月 18 00:12 qemu-ppc64le -rw-r--r-- 1 root root 0 11月 18 00:12 qemu-s390x --w------- 1 root root 0 11月 18 00:09 register -rw-r--r-- 1 root root 0 11月 18 00:12 status 验证是否启用了相应的处理器：\n🐳 → cat /proc/sys/fs/binfmt_misc/qemu-aarch64 enabled interpreter /usr/bin/qemu-aarch64 flags: OCF offset 0 magic 7f454c460201010000000000000000000200b7 mask ffffffffffffff00fffffffffffffffffeffff 从默认的构建器切换到多平台构建器 # Docker 默认会使用不支持多 CPU 架构的构建器，我们需要手动切换。\n先创建一个新的构建器：\n🐳 → docker buildx create --use --name mybuilder 启动构建器：\n🐳 → docker buildx inspect mybuilder --bootstrap [+] Building 5.0s (1/1) FINISHED =\u0026gt; [internal] booting buildkit 5.0s =\u0026gt; =\u0026gt; pulling image moby/buildkit:buildx-stable-1 4.4s =\u0026gt; =\u0026gt; creating container buildx_buildkit_mybuilder0 0.6s Name: mybuilder Driver: docker-container Nodes: Name: mybuilder0 Endpoint: unix:///var/run/docker.sock Status: running Platforms: linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6 查看当前使用的构建器及构建器支持的 CPU 架构，可以看到支持很多 CPU 架构：\n🐳 → docker buildx ls NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS mybuilder * docker-container mybuilder0 unix:///var/run/docker.sock running linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6 default docker default default running linux/amd64, linux/386 构建多平台镜像 # 现在我们就可以构建支持多 CPU 架构的镜像了！假设有一个简单的 golang 程序源码：\n🐳 → cat hello.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; ) func main() { fmt.Printf(\u0026#34;Hello, %s!\\n\u0026#34;, runtime.GOARCH) } 创建一个 Dockerfile 将该应用容器化：\n🐳 → cat Dockerfile FROM golang:alpine AS builder RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o hello . FROM alpine RUN mkdir /app WORKDIR /app COPY --from=builder /app/hello . CMD [\u0026#34;./hello\u0026#34;] 这是一个多阶段构建 Dockerfile，使用 Go 编译器来构建应用，并将构建好的二进制文件拷贝到 alpine 镜像中。\n现在就可以使用 buildx 构建一个支持 arm、arm64 和 amd64 多架构的 Docker 镜像了，同时将其推送到 Docker Hub：\n🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/arm,linux/arm64,linux/amd64 . --push 需要提前通过 docker login 命令登录认证 Docker Hub。 现在就可以通过 docker pull mirailabs/hello-arch 拉取刚刚创建的镜像了，Docker 将会根据你的 CPU 架构拉取匹配的镜像。\n背后的原理也很简单，之前已经提到过了，buildx 会通过 QEMU 和 binfmt_misc 分别为 3 个不同的 CPU 架构（arm，arm64 和 amd64）构建 3 个不同的镜像。构建完成后，就会创建一个 manifest list，其中包含了指向这 3 个镜像的指针。\n如果想将构建好的镜像保存在本地，可以将 type 指定为 docker，但必须分别为不同的 CPU 架构构建不同的镜像，不能合并成一个镜像，即：\n🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/arm -o type=docker . 🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/arm64 -o type=docker . 🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/amd64 -o type=docker . 测试多平台镜像 # 由于之前已经启用了 binfmt_misc，现在我们就可以运行任何 CPU 架构的 Docker 镜像了，因此可以在本地系统上测试之前生成的 3 个镜像是否有问题。\n首先列出每个镜像的 digests：\n🐳 → docker buildx imagetools inspect yangchuansheng/hello-arch Name: docker.io/yangchuansheng/hello-arch:latest MediaType: application/vnd.docker.distribution.manifest.list.v2+json Digest: sha256:ec55f5ece9a12db0c6c367acda8fd1214f50ee502902f97b72f7bff268ebc35a Manifests: Name: docker.io/yangchuansheng/hello-arch:latest@sha256:38e083870044cfde7f23a2eec91e307ec645282e76fd0356a29b32122b11c639 MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/arm/v7 Name: docker.io/yangchuansheng/hello-arch:latest@sha256:de273a2a3ce92a5dc1e6f2d796bb85a81fe1a61f82c4caaf08efed9cf05af66d MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/arm64 Name: docker.io/yangchuansheng/hello-arch:latest@sha256:8b735708d7d30e9cd6eb993449b1047b7229e53fbcebe940217cb36194e9e3a2 MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/amd64 运行每一个镜像并观察输出结果：\n🐳 → docker run --rm docker.io/yangchuansheng/hello-arch:latest@sha256:38e083870044cfde7f23a2eec91e307ec645282e76fd0356a29b32122b11c639 Hello, arm! 🐳 → docker run --rm docker.io/yangchuansheng/hello-arch:latest@sha256:de273a2a3ce92a5dc1e6f2d796bb85a81fe1a61f82c4caaf08efed9cf05af66d Hello, arm64! 🐳 → docker run --rm docker.io/yangchuansheng/hello-arch:latest@sha256:8b735708d7d30e9cd6eb993449b1047b7229e53fbcebe940217cb36194e9e3a2 Hello, amd64! So cool！\n总结 # 回顾一下，本文带大家了解了在不同的 CPU 架构上运行软件的挑战性，以及 buildx 如何帮助我们解决了其中的一些挑战。使用 buildx，我们无需对 Dockerfile 进行任何修改，就可以创建支持多种 CPU 架构的 Docker 镜像，然后将其推送到 Docker Hub。任何安装了 Docker 的系统都可以拉取到与它的 CPU 架构相对应的镜像。\n未来 buildx 可能会成为 docker build 命令的一部分，最终所有上面提到的功能都会变成默认的功能，下沉到基础设施中交叉编译程序的做法将会变成远古时代的愚蠢行为。\n参考资料 # Building Multi-Arch Images for Arm and x86 with Docker Desktop Getting started with Docker for Arm on Linux Leverage multi-CPU architecture support ","date":"2019年11月17日","externalUrl":null,"permalink":"/posts/multiarch-docker-with-buildx/","section":"博客","summary":"在工作和生活中，我们可能经常需要将某个程序跑在不同的 CPU 架构上","title":"使用 buildx 构建多平台 Docker 镜像","type":"posts"},{"content":"","date":"2019年11月4日","externalUrl":null,"permalink":"/tags/golang/","section":"标签","summary":"","title":"Golang","type":"tags"},{"content":"现在我们都说设计可并行、高并发的程序，而且我们很多时候会在潜意识里觉得自己对并行（Parallelism）和并发（Concurrency）的区别很清楚，但如果要明确的说出二者的区别，又感觉没办法给出一个非常清晰的描述。\n那么什么是并发？什么又是并行呢？并行的概念比较简单，并行总是和执行（executions）相关，很多东西同时执行就是并行；而并发则是通过一些方式组织你的程序，让它可以分成多个模块去独立的执行。并行必然是需要多核的，一个处理器是无法并行的；但并发和处理器并没有什么必然联系，在一个处理器上面，我们的程序也可以是并发的。\n举个简单的例子，华罗庚泡茶，必须有烧水、洗杯子、拿茶叶等步骤。现在我们想尽快做完这件事，也就是“一共要处理很多事情”，有很多方法可以实现并发，例如请多个人同时做，这就是并行。并行是实现并发的一种方式，但不是唯一的方式。我们一个人也可以实现并发，例如先烧水、然后不用等水烧开就去洗杯子，所以通过调整程序运行方式也可以实现并发。\n如果你觉得以上的讲解还是太抽象了，下面通过一个小故事来讲解，故事原型来自 Go 语言创始人之一 Rob Pike 的一篇演讲。\n故事的开始有一个需求：有一群地鼠要把一堆废弃的说明书用小推车推到火炉去烧毁。\n刚开始只有一只地鼠，使用一辆推车，将书装到车上，运输到火炉旁，将书卸到火炉。完成任务必然需要比较长的时间。\n此时如果再增加一只地鼠，那也没什么用，因为一只地鼠在干活，另一只地鼠只能等待。（当然有人说两只地鼠轮流使用一辆推车，这样可以让地鼠得到休息，这样它们干活更快，也可以提高效率。）\n再找一辆推车来，两只地鼠分别使用各自的推车，将书装到车上，运输到火炉旁，将书卸到火炉。这样会提高运输效率，但它们会在装书和卸书时进行排队，降低了效率。\n这样虽然比之前快了，但还是有瓶颈的。因为书只有一堆，火炉也只有一个，所以我们还必须通过消息来协调两只地鼠的行动。好吧，那我们再把书分成两堆，再增加一个火炉。\n这样就比之前的效率高差不多一倍了。现在这个模型就是并发的，因为两只地鼠可以独立完成一件事了，这样提高了运输效率，而且在装书和卸书时不会进行排队，提高了装卸的效率。但这个模型不一定是并行的，比如同一时刻可能只有一只地鼠在干活。\n上面就是第一种并发模型，我们还可以设计更多的并发模型，继续看漫画。\n这次找了 3 只地鼠，一只负责把书装到车上，一只负责运输，一只负责把书卸到火炉焚烧。每只地鼠做一个独立的任务，当然三只地鼠之间需要使用一些诸如消息通信之类的手段进行协调。\n装书和烧书的两只地鼠都很轻松，负责运输的这只地鼠却很累，系统出现了瓶颈。那我们再找一只地鼠来，专门负责运回空推车。\n我们在一个已有的设计（指三个地鼠的那个设计）中添加一个并发的步骤（第四只地鼠）增强了系统的性能。这样一来，两只地鼠去搞运输，如果协调的好，理论情况下工作效率将是一只地鼠的 4 倍。\n总共有 4 个并发的步骤：\n把书装到车上； 把推车运到火炉旁； 把书卸到火炉里； 运回空推车。 可以再增加一个分组，将这个并发模型并行化。\n下面我们再来看另外一种并发模型。负责运输的地鼠抱怨说运输路程太长，那我们就增加一个中转站。\n然后再增加一个分组，将这个并发模型并行化，两个分组并行执行。\n可以把上面的并发模型再改进一下。增加中转站的同时，再增加两只地鼠，一只负责将从书堆运过来的书卸到中转站，另一只负责将书从中转站装到推车里，再让后面的地鼠运输到火炉旁。\n然后再增加一个分组，将这个并发模型并行化。\n漫画到这里就结束了，总共介绍了三种并发模型，每种模型都可以很容易地并行化。可以看到上面的并发模型每改进一次，其实就是将任务拆的更细了，一旦分解了问题，并发就自然而然产生了，每个人只专注于一个任务。\n回到程序中，书就代表着数据，地鼠就是 CPU，而车可能就是序列化、反序列化、网络等设施，火炉就是代理、浏览器或其他的消费者。而上面的并发模型就是一个可扩展的 Web Service。\n该演讲题目为 《Concurrency is not Parallelism》 ，原文链接：\n演讲幻灯片： https://talks.golang.org/2012/waza.slide 演讲视频： 本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 参考链接\nhttps://my.oschina.net/3233123/blog/1047239 https://blog.csdn.net/claram/article/details/52094587 ","date":"2019年11月4日","externalUrl":null,"permalink":"/posts/concurrency-is-not-parallelism/","section":"博客","summary":"现在我们都说设计可并行、高并发的程序，而且我们很多时候会在潜","title":"并发与并行的区别","type":"posts"},{"content":"该系列文章总共分为三篇：\nLinux Capabilities 入门教程：概念篇 Linux Capabilities 入门教程：基础实战篇 Linux Capabilities 入门教程：进阶实战篇 上篇文章介绍了 Linux capabilities 的诞生背景和基本原理，本文将会通过具体的示例来展示如何查看和设置文件的 capabilities。\nLinux 系统中主要提供了两种工具来管理 capabilities：libcap 和 libcap-ng。libcap 提供了 getcap 和 setcap 两个命令来分别查看和设置文件的 capabilities，同时还提供了 capsh 来查看当前 shell 进程的 capabilities。libcap-ng 更易于使用，使用同一个命令 filecap 来查看和设置 capabilities。\nlibcap # 安装很简单，以 CentOS 为例，可以通过以下命令安装：\n$ yum install -y libcap 如果想查看当前 shell 进程的 capabilities，可以用 capsh 命令。下面是 CentOS 系统中的 root 用户执行 capsh 的输出：\n$ capsh --print Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read Securebits: 00/0x0/1\u0026#39;b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) uid=0(root) gid=0(root) groups=0(root) 解释一下：\nCurrent : 表示当前 shell 进程的 Effective capabilities 和 Permitted capabilities。可以包含多个分组，每一个分组的表示形式为 capability[,capability…]+(e|i|p)，其中 e 表示 effective，i 表示 inheritable，p 表示 permitted。不同的分组之间通过空格隔开，例如：Current: = cap_sys_chroot+ep cap_net_bind_service+eip。再举一个例子，cap_net_bind_service+e cap_net_bind_service+ip 和 cap_net_bind_service+eip 等价。 Bounding set : 这里仅仅表示 Bounding 集合中的 capabilities，不包括其他集合，所以分组的末尾不用加上 +... 。 Securebits : 我也没搞清楚这是个什么鬼。 这个命令输出的信息比较有限，完整的信息可以查看 /proc 文件系统，比如当前 shell 进程就可以查看 /proc/$$/status。其中一个重要的状态就是 NoNewPrivs，可以通过以下命令查看：\ngrep NoNewPrivs /proc/$$/status NoNewPrivs: 0 根据 prctl(2) 中的描述，自从 Linux 4.10 开始，/proc/[pid]/status 中的 NoNewPrivs 值表示了线程的 no_new_privs 属性。至于 no_new_privs究竟是干嘛的，下面我单独解释一下。\nno_new_privs # 一般情况下，execve() 系统调用能够赋予新启动的进程其父进程没有的权限，最常见的例子就是通过 setuid 和 setgid 来设置程序进程的 uid 和 gid 以及文件的访问权限。这就给不怀好意者钻了不少空子，可以直接通过 fork 来提升进程的权限，从而达到不可告人的目的。\n为了解决这个问题，Linux 内核从 3.5 版本开始，引入了 no_new_privs 属性（实际上就是一个 bit，可以开启和关闭），提供给进程一种能够在 execve() 调用整个阶段都能持续有效且安全的方法。\n开启了 no_new_privs 之后，execve 函数可以确保所有操作都必须调用 execve() 判断并赋予权限后才能被执行。这就确保了线程及子线程都无法获得额外的权限，因为无法执行 setuid 和 setgid，也不能设置文件的权限。 一旦当前线程的 no_new_privs 被置位后，不论通过 fork，clone 或 execve 生成的子线程都无法将该位清零。 Docker 中可以通过参数 --security-opt 来开启 no_new_privs 属性，例如：docker run --security-opt=no_new_privs busybox。下面通过一个例子来体会一下 no_new_privs 属性的作用。\n首先撸一段 C 代码，显示当前进程的有效用户 id：\n$ cat testnnp.c #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; int main(int argc, char *argv[]) { printf(\u0026#34;Effective uid: %d\\n\u0026#34;, geteuid()); return 0; } $ make testnnp cc testnnp.c -o testnnp 将可执行文件打入 docker 镜像中：\nFROM fedora:latest ADD testnnp /root/testnnp RUN chmod +s /root/testnnp ENTRYPOINT /root/testnnp 构建镜像：\n$ docker build -t testnnp . Step 1 : FROM fedora:latest ---\u0026gt; 760a896a323f Step 2 : ADD testnnp /root/testnnp ---\u0026gt; 6c700f277948 Removing intermediate container 0981144fe404 Step 3 : RUN chmod +s /root/testnnp ---\u0026gt; Running in c1215bfbe825 ---\u0026gt; f1f07d05a691 Removing intermediate container c1215bfbe825 Step 4 : ENTRYPOINT /root/testnnp ---\u0026gt; Running in 5a4d324d54fa ---\u0026gt; 44f767c67e30 Removing intermediate container 5a4d324d54fa Successfully built 44f767c67e30 下面来做两个实验，先在没有开启 no-new-privileges 的情况下启动容器：\n$ docker run -it --rm --user=1000 testnnp Effective uid: 0 从输出结果来看，只要给可执行文件设置了 SUID 标识，即使我们使用普通用户（UID=1000）来运行容器，进程的有效用户也会变成 root。\n接着在开启 no-new-privileges 的前提下启动容器，以防止执行设置了 SUID 标识的可执行文件进行 UID 转换：\n$ docker run -it --rm --user=1000 --security-opt=no-new-privileges testnnp Effective uid: 1000 可以看到，开启了 no_new_privs 属性之后，即使可执行文件设置了 SUID 标识，线程的有效用户 ID 也不会变成 root。这样即使镜像中的代码有安全风险，仍然可以通过防止其提升权限来避免受到攻击。\nKubernetes 也可以开启 no_new_privs，不过逻辑稍微复杂一点。当 Pod 的 SecurityContext 定义下的 allowPrivilegeEscalation 字段值为 false 时（默认就是 false），如果不满足以下任何一个条件，就会开启 no_new_privs 属性：\n设置了 privileged=true 增加了 CAP_SYS_ADMIN capabilities，即 capAdd=CAP_SYS_ADMIN 以 root 用户运行，即 UID=0 例如，当设置了 privileged=true 和 allowPrivilegeEscalation=false 时，就不会开启 no_new_privs 属性。同理，设置了 capAdd=CAP_SYS_ADMIN 和 allowPrivilegeEscalation=false 也不会开启 no_new_privs 属性。\n管理 capabilities # 可以通过 getcap 来查看文件的 capabilities，例如：\n$ getcap /bin/ping /usr/sbin/arping /bin/ping = cap_net_admin,cap_net_raw+p /usr/sbin/arping = cap_net_raw+p 也可以使用 -r 参数来递归查询：\n$ getcap -r /usr 2\u0026gt;/dev/null /usr/bin/ping = cap_net_admin,cap_net_raw+p /usr/bin/newgidmap = cap_setgid+ep /usr/bin/newuidmap = cap_setuid+ep /usr/sbin/arping = cap_net_raw+p /usr/sbin/clockdiff = cap_net_raw+p 如果想查看某个进程的 capabilities，可以直接使用 getpcaps，后面跟上进程的 PID：\n$ getpcaps 1234 如果想查看一组相互关联的线程的 capabilities（比如 nginx），可以这么来看：\n$ getpcaps $(pgrep nginx) 这里你会看到只有主线程才有 capabilities，子线程和其他 workers 都没有 capabilities，这是因为只有 master 才需要特殊权限，例如监听网络端口，其他线程只需要响应请求就好了。\n设置文件的 capabilities 可以使用 setcap，语法如下：\n$ setcap CAP+set filename 例如，将 CAP_CHOWN 和 CAP_DAC_OVERRIDE capabilities 添加到 permitted 和 effective 集合：\n$ setcap CAP_CHOWN,CAP_DAC_OVERRIDE+ep file1 如果想移除某个文件的 capabilities，可以使用 -r 参数：\n$ setcap -r filename libcap-ng # 安装也很简单，以 CentOS 为例：\n$ yum install libcap-ng-utils 用法 # libcap-ng 使用 filecap 命令来管理文件的 capabilities。有几个需要注意的地方：\nfilecap 添加删除或查看 capabilities 时，capabilities 的名字不需要带 CAP_ 前缀（例如，使用 NET_ADMIN 代替 CAP_NET_ADMIN）； filecap 不支持相对路径，只支持绝对路径； filecap 不允许指定 capabilities 作用的集合，capabilities 只会被添加到 permitted 和 effective 集合。 查看文件的 capabilities：\n$ filecap /full/path/to/file 递归查看某个目录下所有文件的 capabilities：\n$ filecap /full/path/to/dir 例如：\n$ filecap /usr/bin file capabilities /usr/bin/newgidmap setgid /usr/bin/newuidmap setuid 注意 : filecap 只会显示“capabilities 被添加到 permitted 和 effective 集合中”的文件。所以这里没有显示 ping 和 arping。\n递归查看整个系统所有文件的 capabilities：\n$ filecap / # or $ filecap -a 设置文件的 capabilities 语法如下：\n$ filecap /full/path/to/file cap_name 例如：\n$ filecap /usr/bin/tac dac_override 移除某个文件的 capabilities：\n$ filecap /full/path/to/file none 总结 # 本文通过两种工具演示了如何对可执行文件的 capabilities 进行管理，并以 docker 为例，展现了 no_new_privs 的强大之处。如果条件允许，推荐大家以后尽量用 capabilities 来替代完整的 root 权限或者设置 SUID 标识。\n参考资料 # Added no-new-privileges Security Flag to Docker 关于 no new privs 翻译稿 ","date":"2019年11月3日","externalUrl":null,"permalink":"/posts/linux-capabilities-in-practice-1/","section":"博客","summary":"该系列文章总共分为三篇： Linux Capabilities 入门教程：概念篇 Linux Capabilities 入门教程：基","title":"Linux Capabilities 入门教程：基础实战篇","type":"posts"},{"content":"该系列文章总共分为三篇：\nLinux Capabilities 入门教程：概念篇 Linux Capabilities 入门教程：基础实战篇 Linux Capabilities 入门教程：进阶实战篇 Linux 是一种安全的操作系统，它把所有的系统权限都赋予了一个单一的 root 用户，只给普通用户保留有限的权限。root 用户拥有超级管理员权限，可以安装软件、允许某些服务、管理用户等。\n作为普通用户，如果想执行某些只有管理员才有权限的操作，以前只有两种办法：一是通过 sudo 提升权限，如果用户很多，配置管理和权限控制会很麻烦；二是通过 SUID（Set User ID on execution）来实现，它可以让普通用户允许一个 owner 为 root 的可执行文件时具有 root 的权限。\nSUID 的概念比较晦涩难懂，举个例子就明白了，以常用的 passwd 命令为例，修改用户密码是需要 root 权限的，但普通用户却可以通过这个命令来修改密码，这就是因为 /bin/passwd 被设置了 SUID 标识，所以普通用户执行 passwd 命令时，进程的 owner 就是 passwd 的所有者，也就是 root 用户。\nSUID 虽然可以解决问题，但却带来了安全隐患。当运行设置了 SUID 的命令时，通常只是需要很小一部分的特权，但是 SUID 给了它 root 具有的全部权限。这些可执行文件是黑客的主要目标，如果他们发现了其中的漏洞，就很容易利用它来进行安全攻击。简而言之，SUID 机制增大了系统的安全攻击面。\n为了对 root 权限进行更细粒度的控制，实现按需授权，Linux 引入了另一种机制叫 capabilities。\nLinux capabilities 是什么？ # Capabilities 机制是在 Linux 内核 2.2 之后引入的，原理很简单，就是将之前与超级用户 root（UID=0）关联的特权细分为不同的功能组，Capabilites 作为线程（Linux 并不真正区分进程和线程）的属性存在，每个功能组都可以独立启用和禁用。其本质上就是将内核调用分门别类，具有相似功能的内核调用被分到同一组中。\n这样一来，权限检查的过程就变成了：在执行特权操作时，如果线程的有效身份不是 root，就去检查其是否具有该特权操作所对应的 capabilities，并以此为依据，决定是否可以执行特权操作。\nCapabilities 可以在进程执行时赋予，也可以直接从父进程继承。所以理论上如果给 nginx 可执行文件赋予了 CAP_NET_BIND_SERVICE capabilities，那么它就能以普通用户运行并监听在 80 端口上。\ncapability 名称 描述 CAP_AUDIT_CONTROL 启用和禁用内核审计；改变审计过滤规则；检索审计状态和过滤规则 CAP_AUDIT_READ 允许通过 multicast netlink 套接字读取审计日志 CAP_AUDIT_WRITE 将记录写入内核审计日志 CAP_BLOCK_SUSPEND 使用可以阻止系统挂起的特性 CAP_CHOWN 修改文件所有者的权限 CAP_DAC_OVERRIDE 忽略文件的 DAC 访问限制 CAP_DAC_READ_SEARCH 忽略文件读及目录搜索的 DAC 访问限制 CAP_FOWNER 忽略文件属主 ID 必须和进程用户 ID 相匹配的限制 CAP_FSETID 允许设置文件的 setuid 位 CAP_IPC_LOCK 允许锁定共享内存片段 CAP_IPC_OWNER 忽略 IPC 所有权检查 CAP_KILL 允许对不属于自己的进程发送信号 CAP_LEASE 允许修改文件锁的 FL_LEASE 标志 CAP_LINUX_IMMUTABLE 允许修改文件的 IMMUTABLE 和 APPEND 属性标志 CAP_MAC_ADMIN 允许 MAC 配置或状态更改 CAP_MAC_OVERRIDE 忽略文件的 DAC 访问限制 CAP_MKNOD 允许使用 mknod() 系统调用 CAP_NET_ADMIN 允许执行网络管理任务 CAP_NET_BIND_SERVICE 允许绑定到小于 1024 的端口 CAP_NET_BROADCAST 允许网络广播和多播访问 CAP_NET_RAW 允许使用原始套接字 CAP_SETGID 允许改变进程的 GID CAP_SETFCAP 允许为文件设置任意的 capabilities CAP_SETPCAP 参考 capabilities man page CAP_SETUID 允许改变进程的 UID CAP_SYS_ADMIN 允许执行系统管理任务，如加载或卸载文件系统、设置磁盘配额等 CAP_SYS_BOOT 允许重新启动系统 CAP_SYS_CHROOT 允许使用 chroot() 系统调用 CAP_SYS_MODULE 允许插入和删除内核模块 CAP_SYS_NICE 允许提升优先级及设置其他进程的优先级 CAP_SYS_PACCT 允许执行进程的 BSD 式审计 CAP_SYS_PTRACE 允许跟踪任何进程 CAP_SYS_RAWIO 允许直接访问 /devport、/dev/mem、/dev/kmem 及原始块设备 CAP_SYS_RESOURCE 忽略资源限制 CAP_SYS_TIME 允许改变系统时钟 CAP_SYS_TTY_CONFIG 允许配置 TTY 设备 CAP_SYSLOG 允许使用 syslog() 系统调用 CAP_WAKE_ALARM 允许触发一些能唤醒系统的东西(比如 CLOCK_BOOTTIME_ALARM 计时器) capabilities 的赋予和继承 # Linux capabilities 分为进程 capabilities 和文件 capabilities。对于进程来说，capabilities 是细分到线程的，即每个线程可以有自己的capabilities。对于文件来说，capabilities 保存在文件的扩展属性中。\n下面分别介绍线程（进程）的 capabilities 和文件的 capabilities。\n线程的 capabilities # 每一个线程，具有 5 个 capabilities 集合，每一个集合使用 64 位掩码来表示，显示为 16 进制格式。这 5 个 capabilities 集合分别是：\nPermitted Effective Inheritable Bounding Ambient 每个集合中都包含零个或多个 capabilities。这5个集合的具体含义如下：\nPermitted # 定义了线程能够使用的 capabilities 的上限。它并不使能线程的 capabilities，而是作为一个规定。也就是说，线程可以通过系统调用 capset() 来从 Effective 或 Inheritable 集合中添加或删除 capability，前提是添加或删除的 capability 必须包含在 Permitted 集合中（其中 Bounding 集合也会有影响，具体参考下文）。 如果某个线程想向 Inheritable 集合中添加或删除 capability，首先它的 Effective 集合中得包含 CAP_SETPCAP 这个 capabiliy。\nEffective # 内核检查线程是否可以进行特权操作时，检查的对象便是 Effective 集合。如之前所说，Permitted 集合定义了上限，线程可以删除 Effective 集合中的某 capability，随后在需要时，再从 Permitted 集合中恢复该 capability，以此达到临时禁用 capability 的功能。\nInheritable # 当执行exec() 系统调用时，能够被新的可执行文件继承的 capabilities，被包含在 Inheritable 集合中。这里需要说明一下，包含在该集合中的 capabilities 并不会自动继承给新的可执行文件，即不会添加到新线程的 Effective 集合中，它只会影响新线程的 Permitted 集合。\nBounding # Bounding 集合是 Inheritable 集合的超集，如果某个 capability 不在 Bounding 集合中，即使它在 Permitted 集合中，该线程也不能将该 capability 添加到它的 Inheritable 集合中。\nBounding 集合的 capabilities 在执行 fork() 系统调用时会传递给子进程的 Bounding 集合，并且在执行 execve 系统调用后保持不变。\n当线程运行时，不能向 Bounding 集合中添加 capabilities。 一旦某个 capability 被从 Bounding 集合中删除，便不能再添加回来。 将某个 capability 从 Bounding 集合中删除后，如果之前 Inherited 集合包含该 capability，将继续保留。但如果后续从 Inheritable 集合中删除了该 capability，便不能再添加回来。 Ambient # Linux 4.3 内核新增了一个 capabilities 集合叫 Ambient ，用来弥补 Inheritable 的不足。Ambient 具有如下特性：\nPermitted 和 Inheritable 未设置的 capabilities，Ambient 也不能设置。 当 Permitted 和 Inheritable 关闭某权限（比如 CAP_SYS_BOOT）后，Ambient 也随之关闭对应权限。这样就确保了降低权限后子进程也会降低权限。 非特权用户如果在 Permitted 集合中有一个 capability，那么可以添加到 Ambient 集合中，这样它的子进程便可以在 Ambient、Permitted 和 Effective 集合中获取这个 capability。现在不知道为什么也没关系，后面会通过具体的公式来告诉你。 Ambient 的好处显而易见，举个例子，如果你将 CAP_NET_ADMIN 添加到当前进程的 Ambient 集合中，它便可以通过 fork() 和 execve() 调用 shell 脚本来执行网络管理任务，因为 CAP_NET_ADMIN 会自动继承下去。\n文件的 capabilities # 文件的 capabilities 被保存在文件的扩展属性中。如果想修改这些属性，需要具有 CAP_SETFCAP 的 capability。文件与线程的 capabilities 共同决定了通过 execve() 运行该文件后的线程的 capabilities。\n文件的 capabilities 功能，需要文件系统的支持。如果文件系统使用了 nouuid 选项进行挂载，那么文件的 capabilities 将会被忽略。\n类似于线程的 capabilities，文件的 capabilities 包含了 3 个集合：\nPermitted Inheritable Effective 这3个集合的具体含义如下：\nPermitted # 这个集合中包含的 capabilities，在文件被执行时，会与线程的 Bounding 集合计算交集，然后添加到线程的 Permitted 集合中。\nInheritable # 这个集合与线程的 Inheritable 集合的交集，会被添加到执行完 execve() 后的线程的 Permitted 集合中。\nEffective # 这不是一个集合，仅仅是一个标志位。如果设置开启，那么在执行完 execve() 后，线程 Permitted 集合中的 capabilities 会自动添加到它的 Effective 集合中。对于一些旧的可执行文件，由于其不会调用 capabilities 相关函数设置自身的 Effective 集合，所以可以将可执行文件的 Effective bit 开启，从而可以将 Permitted 集合中的 capabilities 自动添加到 Effective 集合中。\n详情请参考 Linux capabilities 的 man page。\n运行 execve() 后 capabilities 的变化 # 上面介绍了线程和文件的 capabilities，你们可能会觉得有些抽象难懂。下面通过具体的计算公式，来说明执行 execve() 后 capabilities 是如何被确定的。\n我们用 P 代表执行 execve() 前线程的 capabilities，P' 代表执行 execve() 后线程的 capabilities，F 代表可执行文件的 capabilities。那么：\nP\u0026rsquo;(ambient) = (file is privileged) ? 0 : P(ambient)\nP\u0026rsquo;(permitted) = (P(inheritable) \u0026amp; F(inheritable)) |\n(F(permitted) \u0026amp; P(bounding))) | P\u0026rsquo;(ambient)\nP\u0026rsquo;(effective) = F(effective) ? P\u0026rsquo;(permitted) : P\u0026rsquo;(ambient)\nP\u0026rsquo;(inheritable) = P(inheritable) [i.e., unchanged]\nP\u0026rsquo;(bounding) = P(bounding) [i.e., unchanged]\n我们一条一条来解释：\n如果用户是 root 用户，那么执行 execve() 后线程的 Ambient 集合是空集；如果是普通用户，那么执行 execve() 后线程的 Ambient 集合将会继承执行 execve() 前线程的 Ambient 集合。 执行 execve() 前线程的 Inheritable 集合与可执行文件的 Inheritable 集合取交集，会被添加到执行 execve() 后线程的 Permitted 集合；可执行文件的 capability bounding 集合与可执行文件的 Permitted 集合取交集，也会被添加到执行 execve() 后线程的 Permitted 集合；同时执行 execve() 后线程的 Ambient 集合中的 capabilities 会被自动添加到该线程的 Permitted 集合中。 如果可执行文件开启了 Effective 标志位，那么在执行完 execve() 后，线程 Permitted 集合中的 capabilities 会自动添加到它的 Effective 集合中。 执行 execve() 前线程的 Inheritable 集合会继承给执行 execve() 后线程的 Inheritable 集合。 这里有几点需要着重强调：\n上面的公式是针对系统调用 execve() 的，如果是 fork()，那么子线程的 capabilities 信息完全复制父进程的 capabilities 信息。\n可执行文件的 Inheritable 集合与线程的 Inheritable 集合并没有什么关系，可执行文件 Inheritable 集合中的 capabilities 不会被添加到执行 execve() 后线程的 Inheritable 集合中。如果想让新线程的 Inheritable 集合包含某个 capability，只能通过 capset() 将该 capability 添加到当前线程的 Inheritable 集合中（因为 P\u0026rsquo;(inheritable) = P(inheritable)）。\n如果想让当前线程 Inheritable 集合中的 capabilities 传递给新的可执行文件，该文件的 Inheritable 集合中也必须包含这些 capabilities（因为 P\u0026rsquo;(permitted) = (P(inheritable) \u0026amp; F(inheritable))|\u0026hellip;）。\n将当前线程的 capabilities 传递给新的可执行文件时，仅仅只是传递给新线程的 Permitted 集合。如果想让其生效，新线程必须通过 capset() 将 capabilities 添加到 Effective 集合中。或者开启新的可执行文件的 Effective 标志位（因为 P\u0026rsquo;(effective) = F(effective) ? P\u0026rsquo;(permitted) : P\u0026rsquo;(ambient)）。\n在没有 Ambient 集合之前，如果某个脚本不能调用 capset()，但想让脚本中的线程都能获得该脚本的 Permitted 集合中的 capabilities，只能将 Permitted 集合中的 capabilities 添加到 Inheritable 集合中（P\u0026rsquo;(permitted) = P(inheritable) \u0026amp; F(inheritable)|\u0026hellip;），同时开启 Effective 标志位（P\u0026rsquo;(effective) = F(effective) ? P\u0026rsquo;(permitted) : P\u0026rsquo;(ambient)）。有 有 Ambient 集合之后，事情就变得简单多了，后续的文章会详细解释。\n如果某个 UID 非零（普通用户）的线程执行了 execve()，那么 Permitted 和 Effective 集合中的 capabilities 都会被清空。\n从 root 用户切换到普通用户，那么 Permitted 和 Effective 集合中的 capabilities 都会被清空，除非设置了 SECBIT_KEEP_CAPS 或者更宽泛的 SECBIT_NO_SETUID_FIXUP。\n关于上述计算公式的逻辑流程图如下所示（不包括 Ambient 集合）：\n简单示例 # 下面我们用一个例子来演示上述公式的计算逻辑，以 ping 文件为例。如果我们将 CAP_NET_RAW capability添加到 ping 文件的 Permitted 集合中（F(Permitted)），它就会添加到执行后的线程的 Permitted 集合中（P\u0026rsquo;(Permitted)）。由于 ping 文件具有 capabilities 感知能力，即能够调用 capset() 和 capget() ，它在运行时会调用 capset() 将 CAP_NET_RAW capability 添加到线程的 Effective 集合中。\n换句话说，如果可执行文件不具有 capabilities 感知能力，我们就必须要开启 Effective 标志位（F(Effective)），这样就会将该 capability 自动添加到线程的 Effective 集合中。具有capabilities 感知能力的可执行文件更安全，因为它会限制线程使用该 capability 的时间。\n我们也可以将 capabilities 添加到文件的 Inheritable 集合中，文件的 Inheritable 集合会与当前线程的 Inheritable 集合取交集，然后添加到新线程的 Permitted 集合中。这样就可以控制可执行文件的运行环境。\n看起来很有道理，但有一个问题：如果可执行文件的有效用户是普通用户，且没有 Inheritable 集合，即 F(inheritable) = 0，那么 P(inheritable) 将会被忽略（P(inheritable) \u0026amp; F(inheritable)）。由于绝大多数可执行文件都是这种情况，因此 Inheritable 集合的可用性受到了限制。我们无法让脚本中的线程自动继承该脚本文件中的 capabilities，除非让脚本具有 capabilities 感知能力。\n要想改变这种状况，可以使用 Ambient 集合。Ambient 集合会自动从父线程中继承，同时会自动添加到当前线程的 Permitted 集合中。举个例子，在一个 Bash 环境中（例如某个正在执行的脚本），该环境所在的线程的 Ambient 集合中包含 CAP_NET_RAW capability，那么在该环境中执行 ping 文件可以正常工作，即使该文件是普通文件（没有任何 capabilities，也没有设置 SUID）。\n终极案例 # 最后拿 docker 举例，如果你使用普通用户来启动官方的 nginx 容器，会出现以下错误：\nbind() to 0.0.0.0:80 failed (13: Permission denied) 因为 nginx 进程的 Effective 集合中不包含 CAP_NET_BIND_SERVICE capability，且不具有 capabilities 感知能力（普通用户），所以启动失败。要想启动成功，至少需要将该 capability 添加到 nginx 文件的 Inheritable 集合中，同时开启 Effective 标志位，并且在 Kubernetes Pod 的部署清单中的 securityContext \u0026ndash;\u0026gt; capabilities 字段下面添加 NET_BIND_SERVICE（这个 capability 会被添加到 nginx 进程的 Bounding 集合中），最后还要将 capability 添加到 nginx 文件的 Permitted 集合中。如此一来就大功告成了，参考公式：P\u0026rsquo;(permitted) = \u0026hellip;|(F(permitted) \u0026amp; P(bounding)))|\u0026hellip;，P\u0026rsquo;(effective) = F(effective) ? P\u0026rsquo;(permitted) : P\u0026rsquo;(ambient)。\n如果容器开启了 securityContext/allowPrivilegeEscalation，上述设置仍然可以生效。如果 nginx 文件具有 capabilities 感知能力，那么只需要将 CAP_NET_BIND_SERVICE capability 添加到它的 Inheritable 集合中就可以正常工作了。\n当然了，除了上述使用文件扩展属性的方法外，还可以使用 Ambient 集合来让非 root 容器进程正常工作，但 Kubernetes 目前还不支持这个属性，具体参考 Kubernetes 项目的 issue。\n虽然 Kubernetes 官方不支持，但我们可以自己来实现，具体实现方式可以关注我后续的文章。\n参考资料 # Linux Capabilities: Why They Exist and How They Work Understanding Capabilities in Linux Linux Capabilities in a nutshell Linux的capabilities机制 ","date":"2019年10月27日","externalUrl":null,"permalink":"/posts/linux-capabilities-why-they-exist-and-how-they-work/","section":"博客","summary":"该系列文章总共分为三篇： Linux Capabilities 入门教程：概念篇 Linux Capabilities 入门教程：基","title":"Linux Capabilities 入门教程：概念篇","type":"posts"},{"content":" 郑重声明：本文不是 Podman 的入门篇，入门请阅读这篇文章： 再见 Docker，是时候拥抱下一代容器工具了\nPodman 原来是 CRI-O 项目的一部分，后来被分离成一个单独的项目叫 libpod。Podman 的使用体验和 Docker 类似，不同的是 Podman 没有 daemon。以前使用 Docker CLI 的时候，Docker CLI 会通过 gRPC API 去跟 Docker Engine 说「我要启动一个容器」，然后 Docker Engine 才会通过 OCI Container runtime（默认是 runc）来启动一个容器。这就意味着容器的进程不可能是 Docker CLI 的子进程，而是 Docker Engine 的子进程。\nPodman 比较简单粗暴，它不使用 Daemon，而是直接通过 OCI runtime（默认也是 runc）来启动容器，所以容器的进程是 podman 的子进程。这比较像 Linux 的 fork/exec 模型，而 Docker 采用的是 C/S（客户端/服务器）模型。与 C/S 模型相比，fork/exec 模型有很多优势，比如：\n系统管理员可以知道某个容器进程到底是谁启动的。 如果利用 cgroup 对 podman 做一些限制，那么所有创建的容器都会被限制。 SD_NOTIFY : 如果将 podman 命令放入 systemd 单元文件中，容器进程可以通过 podman 返回通知，表明服务已准备好接收任务。 socket 激活 : 可以将连接的 socket 从 systemd 传递到 podman，并传递到容器进程以便使用它们。 废话不多说，下面我们直接进入实战环节，本文将手把手教你如何用 podman 来部署静态博客，并通过 Sidecar 模式将博客所在的容器加入到 Envoy mesh 之中。\n方案架构 # 我的部署方案涉及到两层 Envoy：\n首先会有一个前端代理单独跑一个容器。前端代理的工作是给访问者提供一个入口，将来自外部的访问请求转发到具体的后端服务。 其次，博客静态页面由 nginx 提供，同时以 Sidecar 模式运行一个 Envoy 容器，它与 nginx 共享 network nemspace。 所有的 Envoy 形成一个 mesh，然后在他们之间共享路由信息。 我之前写过一篇用 Docker 部署 hugo 静态博客并配置 HTTPS 证书的文章，本文采用的是相同的方案，只是将 docker 换成了 podman，具体参考 为 Envoy 开启 TLS 验证实战。\n部署 hugo 和 sidecar proxy # 我的博客是通过 hugo 生成的静态页面，可以将其放到 nginx 中，其他静态网站工具类似（比如 hexo 等），都可以这么做。现在我要做的是让 nginx 容器和 envoy 容器共享同一个 network namespace，同时还要让前端代理能够通过域名来进行服务发现。以前用 docker 很简单，直接用 docker-compose 就搞定了，podman 就比较麻烦了，它又不能用 docker-compose，服务发现看来是搞不定了。\n好不容易在 Github 上发现了一个项目叫 podman-compose，以为有救了，试用了一下发现还是不行，podman-compose 创建容器时会将字段 network_mode: \u0026quot;service:hugo\u0026quot; 转化为 podman CLI 的参数 --network service:hugo（真脑残），导致容器创建失败，报错信息为 CNI network \u0026quot;service:hugo\u0026quot; not found。将该字段值改为 network_mode: \u0026quot;container:hugo_hugo_1\u0026quot; 可以启动成功，然而又引来了另一个问题：podman-compose 的做法是为每一个 service 创建一个 pod（pod 的名字为 docker-compose.yml 所在目录名称），然后往这个 pod 中添加容器。我总不能将前端代理和后端服务塞进同一个 pod 中吧？只能分别为前端代理和 hugo 创建两个目录，然后分别创建 docker-compose.yml。这个问题解决了，下个问题又来了，podman-compose 不支持通过 service name 进行服务发现，扒了一圈发现支持 links（其实就是加个参数 --add-host），然而 links 只在同一个 pod 下才生效，我都拆分成两个 pod 了，links 鞭长莫及啊，还是没什么卵用。我能怎么办，现在唯一的办法就是手撸命令行了。\n上面我提到了一个新名词叫 pod，这里花 30 秒的时间给大家简单介绍一下，如果你是 Kubernetes 的重度使用者，对这个词应该不陌生，但这里确实说的是 podman 的 pod，意思还是一样的，先创建一个 pause 容器，然后再创建业务容器，业务容器共享 pause 容器的各种 linux namespace，因此同一个 pod 中的容器之间可以通过 localhost 轻松地相互通信。不仅如此，podman 还可以将 pod 导出为 Kubernetes 的声明式资源定义，举个栗子：\n先创建一个 pod：\n$ podman pod create --name hugo 查看 pod：\n$ podman pod ls POD ID NAME STATUS CREATED # OF CONTAINERS INFRA ID 88226423c4d2 hugo Running 2 minutes ago 2 7e030ef2e7ca 在这个 pod 中启动一个 hugo 容器：\n$ podman run -d --pod hugo nginx:alpine 查看容器：\n$ podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3c91cab1e99d docker.io/library/nginx:alpine nginx -g daemon o... 3 minutes ago Up 3 minutes ago reverent_kirch 查看所有容器，包括 pause 容器：\n$ podman ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3c91cab1e99d docker.io/library/nginx:alpine nginx -g daemon o... 4 minutes ago Up 4 minutes ago reverent_kirch 7e030ef2e7ca k8s.gcr.io/pause:3.1 6 minutes ago Up 6 minutes ago 88226423c4d2-infra 查看所有容器，包括 pause 容器，并显示容器所属的 pod id：\n$ podman ps -ap CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES POD 3c91cab1e99d docker.io/library/nginx:alpine nginx -g daemon o... 4 minutes ago Up 4 minutes ago reverent_kirch 88226423c4d2 7e030ef2e7ca k8s.gcr.io/pause:3.1 6 minutes ago Up 6 minutes ago 88226423c4d2-infra 88226423c4d2 查看 pod 中进程的资源使用情况：\n$ podman pod top hugo USER PID PPID %CPU ELAPSED TTY TIME COMMAND root 1 0 0.000 8m5.045493912s ? 0s nginx: master process nginx -g daemon off; nginx 6 1 0.000 8m5.045600833s ? 0s nginx: worker process nginx 7 1 0.000 8m5.045638877s ? 0s nginx: worker process 0 1 0 0.000 9m41.051039367s ? 0s /pause 将 pod 导出为声明式部署清单：\n$ podman generate kube hugo \u0026gt; hugo.yaml 查看部署清单内容：\n$ cat hugo.yaml # Generation of Kubernetes YAML is still under development! # # Save the output of this file and use kubectl create -f to import # it into Kubernetes. # # Created with podman-1.0.2-dev apiVersion: v1 kind: Pod metadata: creationTimestamp: 2019-10-17T04:17:40Z labels: app: hugo name: hugo spec: containers: - command: - nginx - -g - daemon off; env: - name: PATH value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - name: TERM value: xterm - name: HOSTNAME - name: container value: podman - name: NGINX_VERSION value: 1.17.4 - name: NJS_VERSION value: 0.3.5 - name: PKG_RELEASE value: \u0026#34;1\u0026#34; image: docker.io/library/nginx:alpine name: reverentkirch resources: {} securityContext: allowPrivilegeEscalation: true capabilities: {} privileged: false readOnlyRootFilesystem: false workingDir: / status: {} 怎么样，是不是有种熟悉的味道？这是一个兼容 kubernetes 的 pod 定义，你可以直接通过 kubectl apply -f hugo.yaml 将其部署在 Kubernetes 集群中，也可以直接通过 podman 部署，步骤大致是这样的：\n先删除之前创建的 pod：\n$ podman pod rm -f hugo 然后通过部署清单创建 pod：\n$ podman play kube hugo.yaml 回到之前的问题，如果通过声明式定义来创建 pod，还是无法解决服务发现的问题，除非换个支持静态 IP 的 CNI 插件，而支持静态 IP 的这些 CNI 插件又需要 etcd 作为数据库，我就这么点资源，可不想再加个 etcd，还是手撸命令行吧。\n首先我要创建一个 hugo 容器，并指定容器的 IP：\n$ podman run -d --name hugo \\ --ip=10.88.0.10 \\ -v /opt/hugo/public:/usr/share/nginx/html \\ -v /etc/localtime:/etc/localtime \\ nginx:alpine 再创建一个 envoy 容器，与 hugo 容器共享 network namespace：\n$ podman run -d --name hugo-envoy \\ -v /opt/hugo/service-envoy.yaml:/etc/envoy/envoy.yaml \\ -v /etc/localtime:/etc/localtime \\ --net=container:hugo envoyproxy/envoy-alpine:latest service-envoy.yaml 的内容如下：\nstatic_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 8080 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http access_log: - name: envoy.file_access_log config: path: \u0026#34;/dev/stdout\u0026#34; route_config: name: local_route virtual_hosts: - name: service domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: local_service http_filters: - name: envoy.router config: {} clusters: - name: local_service connect_timeout: 0.25s type: strict_dns lb_policy: round_robin hosts: - socket_address: address: 127.0.0.1 port_value: 80 admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8081 具体的含义请参考 为 Envoy 开启 TLS 验证实战。\n本文开头提到 podman 创建的容器是 podman 的子进程，这个表述可能比较模糊，实际上 podman 由两部分组成，一个是 podman CLI，还有一个是 container runtime，container runtime 由 conmon 来负责，主要包括监控、日志、TTY 分配以及类似 out-of-memory 情况的杂事。也就是说，conmon 是所有容器的父进程。\nconmon 需要去做所有 systemd 不做或者不想做的事情。即使 CRI-O 不直接使用 systemd 来管理容器，它也将容器分配到 sytemd 兼容的 cgroup 中，这样常规的 systemd 工具比如 systemctl 就可以看见容器资源使用情况了。\n$ podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 42762bf7d37a docker.io/envoyproxy/envoy-alpine:latest /docker-entrypoin... About a minute ago Up About a minute ago hugo-envoy f0204fdc9524 docker.io/library/nginx:alpine nginx -g daemon o... 2 minutes ago Up 2 minutes ago hugo 对 cgroup 不熟的同学，可以参考下面这个系列：\n深入理解 Linux Cgroup 系列（一）：基本概念 深入理解 Linux Cgroup 系列（二）：玩转 CPU 深入理解 Linux Cgroup 系列（三）：内存 深入理解 Kubernetes 资源限制：CPU Kubernetes 内存资源限制实战 Kubernetes Pod 驱逐详解 零基础的同学建议按照上面的目录从上到下打怪升级，祝你好运！\n部署前端代理 # 这个很简单，直接创建容器就好了：\n$ podman run -d --name front-envoy \\ --add-host=hugo:10.88.0.10 \\ -v /opt/hugo/front-envoy.yaml:/etc/envoy/envoy.yaml \\ -v /etc/localtime:/etc/localtime \\ -v /root/.acme.sh/yangcs.net:/root/.acme.sh/yangcs.net \\ --net host envoyproxy/envoy 由于没办法自动服务发现，需要通过参数 --add-host 手动添加 hosts 到容器中。envoy 的配置文件中是通过域名来添加 cluster 的，front-envoy.yaml 内容如下：\nstatic_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http access_log: - name: envoy.file_access_log config: path: \u0026#34;/dev/stdout\u0026#34; route_config: virtual_hosts: - name: backend domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: https_redirect: true response_code: \u0026#34;FOUND\u0026#34; http_filters: - name: envoy.router config: {} - address: socket_address: address: 0.0.0.0 port_value: 443 filter_chains: - filter_chain_match: server_names: [\u0026#34;yangcs.net\u0026#34;, \u0026#34;icloudnative.io\u0026#34;] tls_context: common_tls_context: alpn_protocols: h2 tls_params: tls_maximum_protocol_version: TLSv1_3 tls_certificates: - certificate_chain: filename: \u0026#34;/root/.acme.sh/yangcs.net/fullchain.cer\u0026#34; private_key: filename: \u0026#34;/root/.acme.sh/yangcs.net/yangcs.net.key\u0026#34; filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: backend domains: - \u0026#34;yangcs.net\u0026#34; - \u0026#34;icloudnative.io\u0026#34; routes: - match: prefix: \u0026#34;/admin\u0026#34; route: prefix_rewrite: \u0026#34;/\u0026#34; cluster: envoy-ui - match: prefix: \u0026#34;/\u0026#34; route: cluster: hugo response_headers_to_add: - header: key: \u0026#34;Strict-Transport-Security\u0026#34; value: \u0026#34;max-age=63072000; includeSubDomains; preload\u0026#34; http_filters: - name: envoy.router config: {} clusters: - name: hugo connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: hugo port_value: 8080 admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8001 具体的含义请参考 为 Envoy 开启 TLS 验证实战。\n现在就可以通过公网域名访问博客网站了，如果后续还有其他应用，都可以参考第二节的步骤，然后重新创建前端代理，添加 --add-host 参数。以我的网站 https://icloudnative.io 为例：\n我好像透露了一些什么不得了的东西，就此打住，你也不要说，你也不要问。\n开机自启 # 由于 podman 不再使用 daemon 管理服务，--restart 参数被废弃了，要想实现开机自动启动容器，只能通过 systemd 来管理了。先创建 systemd 服务配置文件：\n$ vim /etc/systemd/system/hugo_container.service [Unit] Description=Podman Hugo Service After=network.target After=network-online.target [Service] Type=simple ExecStart=/usr/bin/podman start -a hugo ExecStop=/usr/bin/podman stop -t 10 hugo Restart=always [Install] WantedBy=multi-user.target $ vim /etc/systemd/system/hugo-envoy_container.service [Unit] Description=Podman Hugo Sidecar Service After=network.target After=network-online.target After=hugo_container.service [Service] Type=simple ExecStart=/usr/bin/podman start -a hugo-envoy ExecStop=/usr/bin/podman stop -t 10 hugo-envoy Restart=always [Install] WantedBy=multi-user.target $ vim /etc/systemd/system/front-envoy_container.service [Unit] Description=Podman Front Envoy Service After=network.target After=network-online.target After=hugo_container.service hugo-envoy_container.service [Service] Type=simple ExecStart=/usr/bin/podman start -a front-envoy ExecStop=/usr/bin/podman stop -t 10 front-envoy Restart=always [Install] WantedBy=multi-user.target 然后将之前停止之前创建的容器，注意：是停止，不是删除！\n$ podman stop $(podman ps -aq) 最后通过 systemd 服务启动这些容器。\n$ systemctl start hugo_container $ systemctl start hugo-envoy_container $ systemctl start front-envoy_container 设置开机自启。\n$ systemctl enable hugo_container $ systemctl enable hugo-envoy_container $ systemctl enable front-envoy_container 之后每次系统重启后 systemd 都会自动启动这个服务所对应的容器。\n总结 # 以上就是将博客从 Docker 迁移到 Podman 的所有变更操作，总体看下来还是比较曲折，因为 Podman 是为 Kubernetes 而设计的，而我要求太高了，就一个资源紧张的 vps，即不想上 Kubernetes，也不想上 etcd，既想搞 sidecar，又想搞自动服务发现，我能怎么办，我也很绝望啊，这个事怨不得 podman，为了防止在大家心里留下 “podman 不好用” 的印象，特此声明一下。啥都不想要，只能自己想办法了~~\n","date":"2019年10月18日","externalUrl":null,"permalink":"/posts/podman-sidecar/","section":"博客","summary":"郑重声明：本文不是 Podman 的入门篇，入门请阅读这篇文章： 再见 Doc","title":"Podman 使用指南","type":"posts"},{"content":"英文原文：Grafana v6.4 Released\n2019 年 10 月 2 日，也就是中国的国庆期间，Grafana 实验室正式发布了 Grafana 6.4 版本。这个版本主要围绕数据模型和指标查询对原有的功能进行增强，同时增加了一些新特性。\nGrafana 6.4 新特性 # Explore : 支持跳转到仪表盘面板 Explore : 改进日志的实时查看功能 Loki : 在仪表盘中将日志显示为注释 Loki : 支持在仪表盘面板中使用 Loki 面板 : 新增日志面板 面板 : Data Link 功能增强 图形 : 借助 Series Override 将点变成线 仪表盘 : 支持在不同面板间共享查询结果 插件 : grafana-toolkit 发布 Alpha 版 图形渲染 : 弃用 PhantomJS Docker : 基础镜像改为 Alpine LDAP : 新增 LDAP Debug UI 从 Explore 回到仪表盘 # 为了让使用者能够在 Explore 和仪表盘之间来回快速切换，Grafana 6.4 新增了一个功能，当你从仪表盘的下拉菜单中跳转到 Explore 后，还可以回到先前的仪表盘。\n当你从仪表盘跳转到 Explore 之后，你会看到 Explore 工具栏中有一个“返回”箭头。\n直接点击这个箭头就会回到先前的仪表盘。如果你想在回到仪表盘的同时保存 Explore 中的变更，只需要单击箭头旁边的倒三角即可显示 “Return to panel with changes” 菜单项。\n日志实时查看功能改进 # 新版本在日志查看面板中增加了一个暂停按钮，只要点击该按钮，就会暂停显示新日志。或者当你向上滚动查看日志时，也会自动暂停显示新日志。如果想恢复日志实时显示，只需重新点击暂停按钮。\n此外，还引入了一些性能优化，以允许实时跟踪更高吞吐量的日志流。还有各种 UI 的修复和改进，例如更一致的样式和新日志的高亮显示。\n新增日志面板 # 日志面板可以显示来自其他数据源的日志（例如 Elastic，Influx 和 Loki）。通常日志面板显示在监控面板旁边，以展示相关进程的日志输出。\n尽管日志面板也支持查看实时日志，但通常建议只在 Explore 中使用此功能。日志面板最好通过页面顶部的刷新按钮来同步日志数据。日志面板现在处于 Beta 阶段，慎用。\nData Link 功能改进 # Grafana 6.3 引入了一种新的方式来更进一步研究监控数据，即 Data Link。Data link 会帮您创建一个到外部仪表盘或外部系统的动态链接，它主要由标题和 URL 两部分组成，其中 URL 可以引用模板变量和指标查询的结果，例如时间序列的名称和标签，字段的名称、值和时间等。关于 Data link 的更多信息请参考 官方文档。\n在 Grafana 6.3 中，Data link 只支持 Graph 面板，Grafana 6.4 增强了该功能，使其支持 Guage 面板和 Bar Guage 面板。\n借助 Series Override 将点变成线 # 某些指标的查询结果比较特殊，每个时间序列仅由一个点组成，无法显示在 Graph 面板中。Grafana 6.4 可以借助 series overrides 将点变成一条平行于 X 轴的线，只需要依次选择 Transform \u0026gt; constant 就可以了。\n在不同面板间共享查询结果 # 如果某些指标的查询很耗费资源，你可以在不同的面板之间共享同一个查询结果，以此来避免重复查询。具体的操作方法是在新面板的数据源中选择 -- Dashboard --，然后选择相应的面板。\n除了共享某个面板所有的查询结果之外，还可以选择共享面板的部分查询结果。该功能目前处于 Alpha 阶段，需要在配置文件中显式启用。\ngrafana-toolkit 发布 Alpha 版 # grafana-toolkit 的目标是简化插件开发人员的工作，它可以使开发人员专注于插件的核心价值，不用关心环境和配置，也不用关心测试和打包流程。\n关于 grafana-toolkit 的更多信息请参考 官方文档。\n弃用 PhantomJS # 之前 Grafana 使用 PhantomJS 来渲染面板中的图像，现在已被弃用，在未来的版本中将会彻底删除。如果你仍然在使用 PhantomJS，每次 Grafana 启动时都会向你发出 PhantomJS 已被弃用的警告。\n从 Grafana 6.4 开始，建议从 PhantomJS 迁移到 Grafana 图像渲染插件。\n基础镜像改为 Alpine # 从 Grafana 6.4 将基础镜像改为 Alpine 3.10，现在再用镜像扫描工具来扫描镜像中的安全漏洞，应该会显示零漏洞了。\n升级 # 请查看 升级说明\n更新日志 # 更新日志请查看 CHANGELOG.md 文件。\n下载 # 下载页面： https://grafana.com/grafana/download\n","date":"2019年10月8日","externalUrl":null,"permalink":"/posts/grafana-v6.4-released/","section":"博客","summary":"英文原文：Grafana v6.4 Released 2019 年 10 月 2 日，也就是中国的国庆期","title":"Grafana 6.4 正式发布！","type":"posts"},{"content":"nftables 是一个 netfilter 项目，旨在替换现有的 {ip,ip6,arp,eb}tables 框架，为 {ip,ip6}tables 提供一个新的包过滤框架、一个新的用户空间实用程序（nft）和一个兼容层。它使用现有的钩子、链接跟踪系统、用户空间排队组件和 netfilter 日志子系统。\nnftables 主要由三个组件组成：内核实现、libnl netlink 通信和 nftables 用户空间。 其中内核提供了一个 netlink 配置接口以及运行时规则集评估，libnl 包含了与内核通信的基本函数，用户空间可以通过 nft 和用户进行交互。\n本文是 nftables 中文教程，主要介绍用户空间命令行工具 nft 的用法。\nnftables VS iptables # nftables 和 iptables 一样，由表（table）、链（chain）和规则（rule）组成，其中表包含链，链包含规则，规则是真正的 action。与 iptables 相比，nftables 主要有以下几个变化：\niptables 规则的布局是基于连续的大块内存的，即数组式布局；而 nftables 的规则采用链式布局。其实就是数组和链表的区别，好像 Kubernetes 用户对此应该很兴奋？ iptables 大部分工作在内核态完成，如果要添加新功能，只能重新编译内核；而 nftables 的大部分工作是在用户态完成的，添加新功能很 easy，不需要改内核。 iptables 有内置的链，即使你只需要一条链，其他的链也会跟着注册；而 nftables 不存在内置的链，你可以按需注册。由于 iptables 内置了一个数据包计数器，所以即使这些内置的链是空的，也会带来性能损耗。 简化了 IPv4/IPv6 双栈管理 原生支持集合、字典和映射 回到 nftables，先来看一下默认的规则集是啥：\n$ nft list ruleset 啥也没有，果然是没有内置的链啊（如果你关闭了 firewalld 服务）。\n创建表 # nftables 的每个表只有一个地址簇，并且只适用于该簇的数据包。表可以指定五个簇中的一个：\nnftables簇 iptables命令行工具 ip iptables ip6 ip6tables inet iptables和ip6tables arp arptables bridge ebtables inet 同时适用于 IPv4 和 IPv6 的数据包，即统一了 ip 和 ip6 簇，可以更容易地定义规则，下文的示例都将采用 inet 簇。\n先创建一个新的表：\n$ nft add table inet my_table 列出所有的规则：\n$ nft list ruleset table inet my_table { } 现在表中还没有任何规则，需要创建一个链来保存规则。\n创建链 # 链是用来保存规则的，和表一样，链也需要被显示创建，因为 nftables 没有内置的链。链有以下两种类型：\n常规链 : 不需要指定钩子类型和优先级，可以用来做跳转，从逻辑上对规则进行分类。 基本链 : 数据包的入口点，需要指定钩子类型和优先级。 创建常规链：\n$ nft add chain inet my_table my_utility_chain 创建基本链：\n$ nft add chain inet my_table my_filter_chain { type filter hook input priority 0 \\; } 反斜线（\\）用来转义，这样 shell 就不会将分号解释为命令的结尾。 priority 采用整数值，可以是负数，值较小的链优先处理。 列出链中的所有规则：\n$ nft list chain inet my_table my_utility_chain table inet my_table { chain my_utility_chain { } } $ nft list chain inet my_table my_filter_chain table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; } } 创建规则 # 有了表和链之后，就可以创建规则了，规则由语句或表达式构成，包含在链中。下面添加一条规则允许 SSH 登录：\n$ nft add rule inet my_table my_filter_chain tcp dport ssh accept add 表示将规则添加到链的末尾，如果想将规则添加到链的开头，可以使用 insert。\n$ nft insert rule inet my_table my_filter_chain tcp dport http accept 列出所有规则：\n$ nft list ruleset table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; tcp dport http accept tcp dport ssh accept } } 注意 http 规则排在 ssh 规则的前面，因为之前使用了 insert。\n也可以将规则插入到链的指定位置，有两种方法：\n1、 使用 index 来指定规则的索引。add 表示新规则添加在索引位置的规则后面，inser 表示新规则添加在索引位置的规则前面。index 的值从 0 开始增加。\n$ nft insert rule inet my_table my_filter_chain index 1 tcp dport nfs accept $ nft list ruleset table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; tcp dport http accept tcp dport nfs accept tcp dport ssh accept } } $ nft add rule inet my_table my_filter_chain index 0 tcp dport 1234 accept $ nft list ruleset table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; tcp dport http accept tcp dport 1234 accept tcp dport nfs accept tcp dport ssh accept } } index 类似于 iptables 的 -I 选项，但有两点需要注意：一是 index 的值是从 0 开始的；二是 index 必须指向一个存在的规则，比如 nft insert rule … index 0 就是非法的。\n2、 使用 handle 来指定规则的句柄。add 表示新规则添加在索引位置的规则后面，inser 表示新规则添加在索引位置的规则前面。handle 的值可以通过参数 --handle 获取。\n$ nft --handle list ruleset table inet my_table { # handle 10 chain my_filter_chain { # handle 2 type filter hook input priority 0; policy accept; tcp dport http accept # handle 4 tcp dport 1234 accept # handle 6 tcp dport nfs accept # handle 5 tcp dport ssh accept # handle 3 } } $ nft add rule inet my_table my_filter_chain handle 4 tcp dport 1234 accept $ nft insert rule inet my_table my_filter_chain handle 5 tcp dport nfs accept $ nft --handle list ruleset table inet my_table { # handle 10 chain my_filter_chain { # handle 2 type filter hook input priority 0; policy accept; tcp dport http accept # handle 4 tcp dport 2345 accept # handle 8 tcp dport 1234 accept # handle 6 tcp dport 3456 accept # handle 9 tcp dport nfs accept # handle 5 tcp dport ssh accept # handle 3 } } 在 nftables 中，句柄值是固定不变的，除非规则被删除，这就为规则提供了稳定的索引。而 index 的值是可变的，只要有新规则插入，就有可能发生变化。一般建议使用 handle 来插入新规则。\n也可以在创建规则时就获取到规则的句柄值，只需要在创建规则时同时加上参数 --echo 和 --handle。\n$ nft --echo --handle add rule inet my_table my_filter_chain udp dport 3333 accept add rule inet my_table my_filter_chain udp dport 3333 accept # handle 10 删除规则 # 单个规则只能通过其句柄删除，首先需要找到你想删除的规则句柄：\n$ nft --handle list ruleset table inet my_table { # handle 10 chain my_filter_chain { # handle 2 type filter hook input priority 0; policy accept; tcp dport http accept # handle 4 tcp dport 2345 accept # handle 8 tcp dport 1234 accept # handle 6 tcp dport 3456 accept # handle 9 tcp dport nfs accept # handle 5 tcp dport ssh accept # handle 3 udp dport 3333 accept # handle 10 } } 然后使用句柄值来删除该规则：\n$ nft delete rule inet my_table my_filter_chain handle 8 $ nft --handle list ruleset table inet my_table { # handle 10 chain my_filter_chain { # handle 2 type filter hook input priority 0; policy accept; tcp dport http accept # handle 4 tcp dport 1234 accept # handle 6 tcp dport 3456 accept # handle 9 tcp dport nfs accept # handle 5 tcp dport ssh accept # handle 3 udp dport 3333 accept # handle 10 } } 列出规则 # 前面的示例都是列出了所有规则，我们还可以根据自己的需求列出规则的一部分。例如：\n列出某个表中的所有规则：\n$ nft list table inet my_table table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; tcp dport http accept tcp dport 1234 accept tcp dport 3456 accept tcp dport nfs accept tcp dport ssh accept udp dport 3333 accept } } 列出某条链中的所有规则：\n$ nft list chain inet my_table my_other_chain table inet my_table { chain my_other_chain { udp dport 12345 log prefix \u0026#34;UDP-12345\u0026#34; } } 集合 # nftables 的语法原生支持集合，可以用来匹配多个 IP 地址、端口号、网卡或其他任何条件。\n匿名集合 # 集合分为匿名集合与命名集合，匿名集合比较适合用于将来不需要更改的规则。\n例如，下面的规则允许来自源 IP 处于 10.10.10.123 ~ 10.10.10.231 这个区间内的主机的流量。\n$ nft add rule inet my_table my_filter_chain ip saddr { 10.10.10.123, 10.10.10.231 } accept $ nft list ruleset table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; tcp dport http accept tcp dport nfs accept tcp dport ssh accept ip saddr { 10.10.10.123, 10.10.10.231 } accept } } 匿名集合的缺点是，如果需要修改集合，就得替换规则。如果后面需要频繁修改集合，推荐使用命名集合。\n之前的示例中添加的规则也可以通过集合来简化：\n$ nft add rule inet my_table my_filter_chain tcp dport { http, nfs, ssh } accept iptables 可以借助 ipset 来使用集合，而 nftables 原生支持集合，所以不需要借助 ipset。\n命名集合 # nftables 也支持命名集合，命名集合是可以修改的。创建集合需要指定其元素的类型，当前支持的数据类型有：\nipv4_addr : IPv4 地址 ipv6_addr : IPv6 地址 ether_addr : 以太网（Ethernet）地址 inet_proto : 网络协议 inet_service : 网络服务 mark : 标记类型 先创建一个空的命名集合：\n$ nft add set inet my_table my_set { type ipv4_addr \\; } $ nft list sets table inet my_table { set my_set { type ipv4_addr } } 要想在添加规则时引用集合，可以使用 @ 符号跟上集合的名字。下面的规则表示将集合 my_set 中的 IP 地址添加到黑名单中。\n$ nft insert rule inet my_table my_filter_chain ip saddr @my_set drop $ nft list chain inet my_table my_filter_chain table inet my_table { chain my_filter_chain { type filter hook input priority 0; policy accept; ip saddr @my_set drop tcp dport http accept tcp dport nfs accept tcp dport ssh accept ip saddr { 10.10.10.123, 10.10.10.231 } accept } } 向集合中添加元素：\n$ nft add element inet my_table my_set { 10.10.10.22, 10.10.10.33 } $ nft list set inet my_table my_set table inet my_table { set my_set { type ipv4_addr elements = { 10.10.10.22, 10.10.10.33 } } } 如果你向集合中添加一个区间就会报错：\n$ nft add element inet my_table my_set { 10.20.20.0-10.20.20.255 } Error: Set member cannot be range, missing interval flag on declaration add element inet my_table my_set { 10.20.20.0-10.20.20.255 } ^^^^^^^^^^^^^^^^^^^^^^^ 要想在集合中使用区间，需要加上一个 flag interval，因为内核必须提前确认该集合存储的数据类型，以便采用适当的数据结构。\n支持区间 # 创建一个支持区间的命名集合：\n$ nft add set inet my_table my_range_set { type ipv4_addr \\; flags interval $ nft add element inet my_table my_range_set { 10.20.20.0/24 } $ nft list set inet my_table my_range_set table inet my_table { set my_range_set { type ipv4_addr flags interval elements = { 10.20.20.0/24 } } } 子网掩码表示法会被隐式转换为 IP 地址的区间，你也可以直接使用区间 10.20.20.0-10.20.20.255 来获得相同的效果。\n级联不同类型 # 命名集合也支持对不同类型的元素进行级联，通过级联操作符 . 来分隔。例如，下面的规则可以一次性匹配 IP 地址、协议和端口号。\n$ nft add set inet my_table my_concat_set { type ipv4_addr . inet_proto . inet_service \\; } $ nft list set inet my_table my_concat_set table inet my_table { set my_concat_set { type ipv4_addr . inet_proto . inet_service } } 向集合中添加元素：\n$ nft add element inet my_table my_concat_set { 10.30.30.30 . tcp . telnet } 在规则中引用级联类型的集合和之前一样，但需要标明集合中每个元素对应到规则中的哪个位置。\n$ nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . tcp dport @my_concat_set accept 这就表示如果数据包的源 IP、协议类型、目标端口匹配 10.30.30.30、tcp、telnet 时，nftables 就会允许该数据包通过。\n匿名集合也可以使用级联元素，例如：\n$ nft add rule inet my_table my_filter_chain ip saddr . meta l4proto . udp dport { 10.30.30.30 . udp . bootps } accept 现在你应该能体会到 nftables 集合的强大之处了吧。\nnftables 级联类型的集合类似于 ipset 的聚合类型，例如 hash:ip,port。\n字典 # 字典是 nftables 的一个高级特性，它可以使用不同类型的数据并将匹配条件映射到某一个规则上面，并且由于是哈希映射的方式，可以完美的避免链式规则跳转的性能开销。\n例如，为了从逻辑上将对 TCP 和 UDP 数据包的处理规则拆分开来，可以使用字典来实现，这样就可以通过一条规则实现上述需求。\n$ nft add chain inet my_table my_tcp_chain $ nft add chain inet my_table my_udp_chain $ nft add rule inet my_table my_filter_chain meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain } $ nft list chain inet my_table my_filter_chain table inet my_table { chain my_filter_chain { ... meta nfproto ipv4 ip saddr . meta l4proto . udp dport { 10.30.30.30 . udp . bootps } accept meta l4proto vmap { tcp : jump my_tcp_chain, udp : jump my_udp_chain } } } 和集合一样，除了匿名字典之外，还可以创建命名字典：\n$ nft add map inet my_table my_vmap { type inet_proto : verdict \\; } 向字典中添加元素：\n$ nft add element inet my_table my_vmap { 192.168.0.10 : drop, 192.168.0.11 : accept } 后面就可以在规则中引用字典中的元素了：\n$ nft add rule inet my_table my_filter_chain ip saddr vmap @my_vmap 表与命名空间 # 在 nftables 中，每个表都是一个独立的命名空间，这就意味着不同的表中的链、集合、字典等都可以有相同的名字。例如：\n$ nft add table inet table_one $ nft add chain inet table_one my_chain $ nft add table inet table_two $ nft add chain inet table_two my_chain $ nft list ruleset ... table inet table_one { chain my_chain { } } table inet table_two { chain my_chain { } } 有了这个特性，不同的应用就可以在相互不影响的情况下管理自己的表中的规则，而使用 iptables 就无法做到这一点。\n当然，这个特性也有缺陷，由于每个表都被视为独立的防火墙，那么某个数据包必须被所有表中的规则放行，才算真正的放行，即使 table_one 允许该数据包通过，该数据包仍然有可能被 table_two 拒绝。为了解决这个问题，nftables 引入了优先级，priority 值越高的链优先级越低，所以 priority 值低的链比 priority 值高的链先执行。如果两条链的优先级相同，就会进入竞争状态。\n备份与恢复 # 以上所有示例中的规则都是临时的，要想永久生效，我们可以将规则备份，重启后自动加载恢复，其实 nftables 的 systemd 服务就是这么工作的。\n备份规则：\n$ nft list ruleset \u0026gt; /root/nftables.conf 加载恢复：\n$ nft -f /root/nftables.conf 在 CentOS 8 中，nftables.service 的规则被存储在 /etc/nftables.conf 中，其中 include 一些其他的示例规则，一般位于 /etc/sysconfig/nftables.conf 文件中，但默认会被注释掉。\n总结 # 希望通过本文的讲解，你能对 nftables 的功能和用法有所了解，当然本文只涉及了一些浅显的用法，更高级的用法可以查看 nftables 的官方 wiki，或者坐等我接下来的文章。相信有了本文的知识储备，你应该可以愉快地使用 nftables 实现 Linux 的智能分流了，具体参考这篇文章： Linux全局智能分流方案。\n","date":"2019年9月29日","externalUrl":null,"permalink":"/posts/using-nftables/","section":"博客","summary":"nftables 是一个 netfilter 项目，旨在替换现有的 {ip,ip6,arp,eb}tables 框架，为 {ip,ip6}tables 提供一个新的包过滤","title":"nftables 中文教程","type":"posts"},{"content":"","date":"2019年9月22日","externalUrl":null,"permalink":"/tags/adguard/","section":"标签","summary":"","title":"Adguard","type":"tags"},{"content":"通常我们使用网络时，宽带运营商会为我们分配一个 DNS 服务器。这个 DNS 通常是最快的，距离最近的服务器，但会有很多问题，比如：\n访问某些网络服务很缓慢，比如 Apple 的 iCloud 服务。 比较担心安全问题，希望能通过设置 DNS 来保证你访问安全的网站。 厌烦了每当你输入一个不正确的网址，运营商总会给你跳转到一个充满广告的界面。 这个时候我们就需要自定义 DNS，自定义 DNS 不仅能够加快网页开启的速度，还能够提高浏览网页的安全性。更重要的一点是，如果你使用过 Google Chrome，应该知道 Google 未来将会限制“拦截广告”的扩展，要想解决此问题只能装个全局的拦截广告软件或者直接从 DNS 服务器层面拦截广告（如果你不想换浏览器）。\nAdGuard Home 是一款全网广告拦截与反跟踪软件，可以将广告与追踪相关的域名屏蔽，指向空的主机（DNS 黑洞）。简单来说它就是一个开源的公共 DNS 服务，使用 Go 语言开发，支持家长控制和广告过滤！关键是它还支持 DNS over TLS 和 DNS over HTTPS，可以运行在 x86 Linux，树莓派上，也可以通过 Docker 部署在群晖 NAS 上。\nAdGuard Home 安装 # AdGuard Home 的安装方法根据你所使用的平台而有所不同，它的二进制文件位于 https://github.com/AdguardTeam/AdGuardHome/releases，可以根据自己的平台下载最新版本。MacOS 的安装方法如下：\n# 下载 AdGuard Home $ wget https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.98.1/AdGuardHome_MacOS.zip # 解压并进入 AdGuardHome_MacOS 目录 $ unzip AdGuardHome_MacOS.zip \u0026amp;\u0026amp; cd AdGuardHome_MacOS # 将二进制文件拷贝到 $PATH $ cp ./AdGuardHome /usr/local/bin/ # 创建 Launch Daemon 的 plist 文件并启动服务 $ AdGuardHome -s install 现在就可以看到服务的配置和状态信息了：\n$ sudo launchctl list AdGuardHome { \u0026#34;StandardOutPath\u0026#34; = \u0026#34;/var/log/AdGuardHome.stdout.log\u0026#34;; \u0026#34;LimitLoadToSessionType\u0026#34; = \u0026#34;System\u0026#34;; \u0026#34;StandardErrorPath\u0026#34; = \u0026#34;/var/log/AdGuardHome.stderr.log\u0026#34;; \u0026#34;Label\u0026#34; = \u0026#34;AdGuardHome\u0026#34;; \u0026#34;TimeOut\u0026#34; = 30; \u0026#34;OnDemand\u0026#34; = false; \u0026#34;LastExitStatus\u0026#34; = 0; \u0026#34;PID\u0026#34; = 1464; \u0026#34;Program\u0026#34; = \u0026#34;/usr/local/bin/AdGuardHome\u0026#34;; \u0026#34;ProgramArguments\u0026#34; = ( \u0026#34;/usr/local/bin/AdGuardHome\u0026#34;; \u0026#34;-s\u0026#34;; \u0026#34;run\u0026#34;; ); }; plist 文件位于 /Library/LaunchDaemons/ 目录下：\n$ cat /Library/LaunchDaemons/AdGuardHome.plist \u0026lt;?xml version=\u0026#39;1.0\u0026#39; encoding=\u0026#39;UTF-8\u0026#39;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple Computer//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34; \u0026gt; \u0026lt;plist version=\u0026#39;1.0\u0026#39;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt;\u0026lt;string\u0026gt;AdGuardHome\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/usr/local/bin/AdGuardHome\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-s\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;run\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;WorkingDirectory\u0026lt;/key\u0026gt;\u0026lt;string\u0026gt;/Users/freya/Downloads/Compressed/AdGuardHome_MacOS\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;SessionCreate\u0026lt;/key\u0026gt;\u0026lt;false/\u0026gt; \u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt;\u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt;\u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;Disabled\u0026lt;/key\u0026gt;\u0026lt;false/\u0026gt; \u0026lt;key\u0026gt;StandardOutPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/AdGuardHome.stdout.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/AdGuardHome.stderr.log\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 对 Launch Daemon 不熟悉的同学可以参考 Mac OS X 的 Launch Daemon / Agent。\n查看端口号：\n$ sudo lsof -iTCP -sTCP:LISTEN -P -n|grep AdGuard AdGuardHo 9990 root 3u IPv6 0xb76d091ec878f951 0t0 TCP *:3000 (LISTEN) 打开浏览器，输入网址 http://127.0.0.1:3000/ 即可访问 AdGuard Home 的管理界面。\n点击“开始配置”，然后设定网页管理界面和 DNS 服务的端口。\n点击“下一步”设置用户名和密码。\n最后点击“下一步”就大功告成了。\n在仪表盘上，我们可以看到 DNS 查询次数、被过滤器拦截的网站、查询 DNS 请求的客户端地址等等信息。\n现在再查看端口号，管理界面会变成你刚刚设定的端口，另外还会多出一个 DNS 服务的端口：\n$ sudo lsof -iTCP -sTCP:LISTEN -P -n|grep AdGuard AdGuardHo 10619 root 11u IPv6 0xb76d091eb6671751 0t0 TCP *:53 (LISTEN) AdGuardHo 10619 root 12u IPv6 0xb76d091ebc3c7751 0t0 TCP *:5300 (LISTEN) $ sudo lsof -iUDP -P -n|grep AdGuard AdGuardHo 10619 root 10u IPv6 0xb76d091eb89601c1 0t0 UDP *:53 配置优化 # 默认的配置比较简单，为了更强力地拦截广告，我们可以对配置进行优化。\n常规设置 # 勾选【使用过滤器和 Hosts 文件以拦截指定域名】、【使用 AdGuard 浏览安全网页服务】、【强制安全搜索】。如果你想拦截成人网站，也可以勾选【使用 AdGuard 家长控制服务】。\n过滤器 # 虽然 AdGuard 本身提供了 AdGuard、AdAway 的广告过滤规则，但在中国有点水土不服，如果要想更完美的实现广告屏蔽还需要自己添加规则，AdGuard 可以兼容 Adblock 的语法。最知名的过滤规则 EasyList 就是由 Adblock Plus 团队维护，过滤规则往往是一个 txt 文件，在文件的开头部分会显示规则的最后更新日期。\n推荐广告过滤规则：\nEasyList China : 国内网站广告过滤的主规则。 EasyPrivacy : EasyPrivacy 是隐私保护，不被跟踪。 CJX\u0026rsquo;s Annoyance List : 过滤烦人的自我推广，并补充EasyPrivacy隐私规则。 广告净化器规则 : 国内大部分视频网站的广告过滤。 I don\u0026rsquo;t care about cookies : 我不关心 Cookie 的问题，屏蔽网站的 cookies 相关的警告。 优酷网如果播放无限加载，那在自定义静态规则里加入一条规则 @@mp4.ts （参考下图）。\n上游 DNS 设置 # 官方默认使用 Cloudflare 的 DNS over HTTPS 作为上游服务器，在国内可能请求上游 DNS 延迟比较高，可以加上或替换国内的 DNS。我自己另外加了中科大的两组无污染 DNS，每次查询的时候会对所有的上游 DNS 同时查询，加速解析。\n查询日志 # 在这个界面里可以看见所有设备的 DNS 查询日志，可以下载整个日志文件，也可以针对某个域名进行快速拦截和放行。\n提升 QPS # 有两个参数可以明显提升 QPS：\nratelimit : DDoS 保护，客户端每秒接收的数据包数。建议禁用该参数（将值改为 0），默认值是 20。 blocked_response_ttl : TTL 缓存时间，建议设置为 60 配置文件默认路径是 /usr/local/bin/AdGuardHome.yaml\n使用 Envoy 作为前端代理 # 其实到这里已经算是结束了，但本人有强迫症，我可不想将应用的管理界面设置为一些奇奇怪怪的非标准端口。有人或许会说：那你为什么不将管理界面设置为 80 或 443 端口啊？问得好，因为我的电脑上部署了各种奇奇怪怪的应用，80 端口只有一个，不够用的，只能考虑加个前端代理了。\n作为一名云原生狂热信徒，当然是选 Envoy 了，虽然 Envoy 很难编译，但 Tetrate 的工程师（包括 Envoy 的核心贡献者和维护者）发起了一个 GetEnvoy 项目，目标是利用一套经过验证的构建工具来构建 Envoy，并通过常用的软件包管理器来分发，其中就包括 Homebrew。我们可以直接通过 Homebrew 来安装：\n$ brew tap tetratelabs/getenvoy ==\u0026gt; Tapping tetratelabs/getenvoy Cloning into \u0026#39;/usr/local/Homebrew/Library/Taps/tetratelabs/homebrew-getenvoy\u0026#39;... Tapped 1 formula. $ brew install envoy ==\u0026gt; Installing envoy from tetratelabs/getenvoy ==\u0026gt; Downloading ... ######################################################################## 100.0% 🍺 /usr/local/Cellar/envoy/1.10.0: 3 files, 27.9MB, built in 13 seconds $ envoy --version envoy version: e349fb6139e4b7a59a9a359be0ea45dd61e589c5/1.11.1/clean-getenvoy-930d4a5/RELEASE/BoringSSL 这是我的 envoy 配置文件：\nstatic_resources: listeners: - address: # Tells Envoy to listen on 0.0.0.0:80 socket_address: address: 0.0.0.0 port_value: 80 filter_chains: # Any requests received on this address are sent through this chain of filters - filters: # If the request is HTTP it will pass through this HTTP filter - name: envoy.http_connection_manager typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager codec_type: auto stat_prefix: http access_log: name: envoy.file_access_log typed_config: \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog path: /dev/stdout route_config: name: search_route virtual_hosts: - name: backend domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: adguard http_filters: - name: envoy.router typed_config: {} clusters: - name: adguard connect_timeout: 1s type: strict_dns dns_lookup_family: V4_ONLY lb_policy: round_robin load_assignment: cluster_name: adguard endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 5300 admin: access_log_path: \u0026#34;/dev/stdout\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 15001 创建 Launch Agent 的 plist 文件：\n$ cat /Library/LaunchAgents/envoy.plist \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple Computer//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;envoy\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/usr/local/bin/envoy\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;--config-path\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;/Users/freya/bin/front-proxy.yaml\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;StandardOutPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/envoy.stdout.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/envoy.stderr.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;Disabled\u0026lt;/key\u0026gt; \u0026lt;false/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 加载 envoy 服务：\n$ sudo launchctl load /Library/LaunchAgents/envoy.plist 现在就可以在浏览器中通过 url http://127.0.0.1/ 来访问 AdGuard Home 的管理界面啦~\n后续如果还有其他不可描述的应用，它们的管理界面都可以根据不同的 url 路径加到 envoy 的后端中。更高级的玩法还可以接入 Prometheus 监控，envoy 的 metrics 路径是 /stats/prometheus。\n如果你很好奇为什么我的浏览器能够输出彩色的 metrics，请在公众号后台回复◉prometheus◉\n最后，别忘了将 MacOS 的 DNS 设为 127.0.0.1，这个就不用我教了吧？\n","date":"2019年9月22日","externalUrl":null,"permalink":"/posts/adguard-home/","section":"博客","summary":"通常我们使用网络时，宽带运营商会为我们分配一个 DNS 服务器。这个","title":"AdGuard Home 安装使用教程","type":"posts"},{"content":"2018 年 7 月份，青云在 Cloud Insight 云计算峰会上推出了一款全新的容器平台——KubeSphere，旨在帮助企业快速低成本管理容器。并且 KubeSphere 本身是开源的，它是基于 Kubernetes 构建的分布式、多租户、企业级开源容器平台，具有强大且完善的网络与存储能力，并通过极简的人机交互提供完善的多集群管理、CI / CD 、微服务治理、应用管理等功能，帮助企业在云、虚拟化及物理机等异构基础设施上快速构建、部署及运维容器架构，实现应用的敏捷开发与全生命周期管理。\nKubeSphere 目前最新的版本为高级版 2.0.2，并且所有版本 100% 开源。它的 Dashboard 是这个样子的：\n这个颜值，比 Kubernetes Dashboard 不知道高到哪里去了，感兴趣的小伙伴可以给一个 Github Star 鼓励一下开发小哥。访问官网请戳这里： kubesphere.io\nKubeSphere 官网大致提供了两种安装方式，一种是安装 k8s 集群和 KubeSphere，一种是在现有的 k8s 集群上安装 KubeSphere。我想大多数用户的需求肯定是在现有的集群上安装，但官方文档给出的部署方案有很多奇怪的坑，本文就来为大家一一填平这些坑。\n环境准备 # 当然，还有些同学可能会和我一样有强迫症，即使目前没有现成的 Kubernetes 环境，我也不想让 KubeSphere 给我来个全家桶，还是想自己搭建 k8s 集群，怎么办，二进制部署好烦啊，像我这种菜鸟没有半天搞不定，有没有简单快捷的方法，十分钟就能建好集群？当然有，用 sealos 就好了，只需一条命令即可跨主机安装所有依赖，不需要 ansible，不需要 ssh 登录到其他机器，安装之前需要做一些准备工作：\n所有节点安装并启动 docker 下载 kubernetes 离线安装包 下载 最新版本 sealos（目前稳定版是 2.0.4） 我的机器规划是这样的：\nHostname IP Role sealos-node1 192.168.0.2 master sealos-node2 192.168.0.3 node sealos-node3 192.168.0.4 node 安装步骤分为以下几步：\n1、在 master 上执行以下命令：\n$ sealos init --master 192.168.0.2 \\ --node 192.168.0.3 \\ --node 192.168.0.4 \\ --user root \\ --passwd password \\ --version v1.14.5 \\ --pkg-url /root/kube1.14.5.tar.gz 2、没有了。\n真没有了，如果想了解原理，请查看 sealos 的 官方文档。\n下面就正式进入 KubeSphere 的安装环节。\n安装 KubeSphere # 1、首先将 ks-installer 仓库克隆到 master 节点上：\n$ git clone https://github.com/kubesphere/ks-installer -b advanced-2.0.2 2、在 Kubernetes 集群中创建名为 kubesphere-system 和 kubesphere-monitoring-system 的 namespace。\n$ cat \u0026lt;\u0026lt;EOF | kubectl create -f - --- apiVersion: v1 kind: Namespace metadata: name: kubesphere-system --- apiVersion: v1 kind: Namespace metadata: name: kubesphere-monitoring-system EOF 3、创建 Kubernetes 集群 CA 证书的 Secret。\n注：按照当前集群 ca.crt 和 ca.key 证书路径创建（Kubeadm 创建集群的证书路径一般为 /etc/kubernetes/pki）\n$ kubectl -n kubesphere-system create secret generic kubesphere-ca \\ --from-file=ca.crt=/etc/kubernetes/pki/ca.crt \\ --from-file=ca.key=/etc/kubernetes/pki/ca.key 4、创建 etcd 的证书 Secret。\n注：根据集群实际 etcd 证书位置创建；\n若 etcd 已经配置过证书，则参考如下创建：\n$ kubectl -n kubesphere-monitoring-system create secret generic kube-etcd-client-certs \\ --from-file=etcd-client-ca.crt=/etc/kubernetes/pki/etcd/ca.crt \\ --from-file=etcd-client.crt=/etc/kubernetes/pki/etcd/healthcheck-client.crt \\ --from-file=etcd-client.key=/etc/kubernetes/pki/etcd/healthcheck-client.key 若 etcd 没有配置证书，则创建空 Secret（以下命令适用于 Kubeadm 创建的 Kubernetes 集群环境）：\n$ kubectl -n kubesphere-monitoring-system create secret generic kube-etcd-client-certs 我这里是使用 sealos 搭建的集群，可以通过查看 etcd 的资源清单文件来获取它的证书：\n$ cat /etc/kubernetes/manifests/etcd.yaml ...... livenessProbe: exec: command: - /bin/sh - -ec - ETCDCTL_API=3 etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key get foo ...... 5、修改部署文件\n由于 KubeSphere 部署过程中涉及的组件非常多，安装过程中会有很多莫名其妙的坑，你可能会遇到以下几个问题：\n问题 1 : 如果现有集群中已经安装有 metrics-server，需要在配置文件中将 metrics_server_enable 设置为 False。我的集群中没有安装这个组件，所以不用设为 False。\n问题 2 : 在安装过程中卡死在 Waitting for ks-sonarqube port to become open 部分，节点上通过 NodePort 已经可以正常访问 sonarqube ，该问题没有解决，由于是一个不影响全局安装的一个操作，所以同样在配置文件中将 sonarqube_enable 设置为 False。\n问题 3 : 如果当前的集群资源不是很足，可以临时取消掉 istio 的安装，后续再开启 istio 的支持。\n问题 4 : KubeSphere 的组件默认情况下使用持久化存储，需要确保集群中有一个默认的 StorageClass 资源对象，如果确实没有，只是想临时部署一个 demo，可以在配置文件中将 persistence 里面的 enable 设置为 false。\n我最终用于安装 KubeSphere 的配置文件如下：\n--- apiVersion: v1 data: ks-config.yaml: | kube_apiserver_host: 192.168.0.2:6443 etcd_tls_enable: True etcd_endpoint_ips: 192.168.0.2 disableMultiLogin: True elk_prefix: logstash sonarqube_enable: False istio_enable: False persistence: enable: false storageClass: \u0026#34;\u0026#34; kind: ConfigMap metadata: name: kubesphere-config namespace: kubesphere-system ...... 只需要修改 ConfigMap 的值即可，其中 kube_apiserver_host 就是现有集群的 APIServer 地址，etcd_endpoint_ips 就是 etcd 的所在节点 IP，默认端口为 2379，如果你是集群模式 etcd，这里可以填写多个节点 IP，中间用 , 隔开，下面就是不需要安装的组件设置为 False。\n6、自定义 Docker 镜像。\n因为目前 ConfigMap 中不能禁用日志，所以只能强行修改 ansible playbook 了。进入 ks-installer 的根目录，将 kubesphere.yaml 中的 ks-logging 删除：\n--- - hosts: localhost gather_facts: false roles: - kubesphere-defaults - ks-devops/sonarqube - openpitrix - prepare/base - { role: metrics-server, when: \u0026#34;metrics_server_enable == true\u0026#34; } - ingress - ks-account - ks-apigateway - ks-controller-manager - ks-devops/s2i - ks-monitor - ks-console - ks-devops/ks-devops - ks-notification - ks-alerting - ks-devops/jenkins - ks-apiserver - { role: ks-istio, when: \u0026#34;istio_enable == true\u0026#34; } 然后修改 Dockerfile，将 Helm v2 替换为 Helm v3，原因你懂得，我可不想装 tiller。修改后的 Dockerfile 内容如下：\nFROM ubuntu:18.04 WORKDIR /usr/src/kubesphere RUN apt update \u0026amp;\u0026amp; apt install ansible python-netaddr openssl curl jq make software-properties-common -y \u0026amp;\u0026amp; apt-add-repository --yes --update ppa:ansible/ansible \u0026amp;\u0026amp; apt install ansible -y \u0026amp;\u0026amp; apt clean RUN curl -SsL https://storage.googleapis.com/kubernetes-release/release/v1.15.0/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \u0026amp;\u0026amp; chmod +x /usr/local/bin/kubectl RUN curl -OSsL https://get.helm.sh/helm-v3.0.0-beta.3-linux-amd64.tar.gz \u0026amp;\u0026amp; tar -zxf helm-v3.0.0-beta.3-linux-amd64.tar.gz \u0026amp;\u0026amp; mv linux-amd64/helm /usr/local/bin/helm \u0026amp;\u0026amp; rm -rf *linux-amd64* \u0026amp;\u0026amp; chmod +x /usr/local/bin/helm COPY roles . COPY kubesphere.yaml . 最后重新构建镜像，将部署文件中 Deployment 的镜像改为自定义的镜像，就可以直接部署了：\n$ kubectl apply -f deploy/kubesphere.yaml $ kubectl -n kubesphere-system get pod NAME READY STATUS RESTARTS AGE ks-account-585846bd44-mt7ss 1/1 Running 0 3h9m ks-apigateway-7d77cb9495-hxgz8 1/1 Running 0 3h9m ks-apiserver-697c5f4859-dsbmm 1/1 Running 0 3h7m ks-console-5b8fbf45c4-7hxrw 1/1 Running 0 3h8m ks-console-5b8fbf45c4-hj4bj 1/1 Running 0 3h8m ks-controller-manager-7497f6c944-4k8wd 1/1 Running 0 3h8m ks-docs-65999c97c9-5f9z7 1/1 Running 0 3h37m kubesphere-installer-6j49s 0/1 Completed 0 3h10m openldap-78df9f7b47-wvs5n 1/1 Running 0 3h38m redis-99f5985b8-2d62q 1/1 Running 0 3h38m $ kubectl -n kubesphere-system get job NAME COMPLETIONS DURATION AGE kubesphere-installer 1/1 2m9s 3h10m 如果上面用于安装的 Job 是完成状态的话，证明 KubeSphere 已经安装成功了。\n可以创建一个 IngressRoute 对象来访问 KubeSphere：\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: kubesphere namespace: kubesphere-system spec: virtualhost: fqdn: ks.yangcs.net routes: - match: / services: - name: ks-console port: 80 将域名信息加入本地电脑的 hosts 中，就可以在浏览器中访问 KubeSphere 的 Dashboard 了。\n默认的集群管理员账号为：\n用户名：admin 密码：P@88w0rd 详细的使用方式可以参考官方文档： https://kubesphere.io/docs/zh-CN/\n参考资料 # 在现有 Kubernetes 集群上安装 KubeSphere ","date":"2019年9月19日","externalUrl":null,"permalink":"/posts/kubesphere/","section":"博客","summary":"2018 年 7 月份，青云在 Cloud Insight 云计算峰会上推出了一款全新的容器平台—","title":"KubeSphere 安装教程","type":"posts"},{"content":"Istio 是 Google、IBM 和 Lyft 联合开源的服务网格（Service Mesh）框架，旨在解决大量微服务的发现、连接、管理、监控以及安全等问题。 Istio 对应用是透明的，不需要改动任何服务代码就可以实现透明的服务治理。 1.3 版本已经发布，距离上一个重要版本 1.2 发布已过去两个多月，我们来看看有哪些修改内容。\n智能协议检测 # 在之前的版本中，如果要使用 Istio 的路由功能，Service 的端口命名必须使用特殊的命名格式。如果用户不遵循该命名规则，就无法使用路由功能。从 1.3 版本开始，即使没有按照规则命名 Service 的端口，Istio 也会自动识别出站流量的协议为 HTTP 或 TCP。目前还不支持自动识别入站流量的协议，下个版本将会支持。\n无 Mixer 的遥测功能（实验性） # 这才是大家最期待的！该版本将大多数常见的安全策略相关的功能（如 RBAC）直接迁移到了 Envoy 中，同时也将大部分遥测功能迁移到了 Envoy 中。现在 Istio proxy 可以直接将收集到的 HTTP 指标暴露给 Prometheus，无需通过 istio-telemetry 服务来中转并丰富指标信息。如果你只关心 HTTP 服务的遥测，可以试试这个新功能，具体步骤参考 无 Mixer 的 HTTP 遥测。该功能接下来几个月将会逐渐完善，以便在启用双向 TLS 认证时支持 TCP 服务的遥测。\n无需定义 containerPort # 此前的版本要求网格中的每个 Pod 必须明确申明每个容器的 containerPort，任何未申明的端口都会绕过 Istio Proxy。1.3 版本使用了一种更为简单安全的方法，不需要显示申明 containerPort 就可以处理工作负载任何端口上的所有入站流量。之前的版本中，当工作负载向自己发送流量时，会陷入 iptables 规则表导致的无限循环，这个版本也修复了。\n支持完全自定义 Envoy 配置 # 虽然 Istio 1.3 专注于可用性，但高级用户仍然可以使用 Envoy 中不属于 Istio Networking API 的高级功能。1.3 版本增强了 EnvoyFilter API 以允许用户完全自定义以下的 Envoy 配置：\nLDS 返回的 HTTP/TCP 监听器以及 filter 链配置。 RDS 返回的 HTTP 路由配置。 CDS 返回的 Cluster 配置。 其他增强功能 # istioctl 新增了许多调试功能，可以帮助你排查安装过程中出现的各种问题。详细信息可以查看 istioctl 的 参考页面 区域感知负载均衡功能从实验分支转移到默认分支。现在 Istio 可以利用现有的位置信息来确定负载均衡池的优先级，并支持将请求转发到地理位置最近的后端。 Istio 开启双向 TLS 认证时可以更好地支持 headless service。 从以下几个方面增强了控制平面的监控： 添加新指标来监控配置的状态 新增了 sidecar injector 的指标 为 Citadel 添加了新的 Grafana 仪表板 改进了 Pilot 仪表板，新增了几个关键指标 新增了 Istio 部署模型文档，可以帮助你选择合适的部署模型。 重新组织了 操作指南中的内容，新增了一个 包含所有故障排除任务的章节，可以帮助你快速寻找所需信息。 详细内容请查看 发布公告。\n参考资料 # Announcing Istio 1.3 ","date":"2019年9月14日","externalUrl":null,"permalink":"/posts/istio-1.3/","section":"博客","summary":"Istio 是 Google、IBM 和 Lyft 联合开源的服务网格（Servic","title":"Istio 1.3 发布，HTTP 遥测不再需要 Mixer","type":"posts"},{"content":" 周末闲逛 Twitter 时，发现一个很有意思的小工具叫 kubeman，野心倒是不小，励志成为 kubectl 的替代品，用于实时监控和管理 kubernetes 集群，还可以调试与 Istio 相关的问题。\n如果只使用 kubectl，当网格中的服务出现问题时，可能需要运行很多命令，而且要交叉引用来自多个命令的输出信息，这就会导致问题分析的过程很复杂。kubeman 将这些交叉引用和相关信息分析的复杂逻辑隐藏起来，只暴露一个 UI 界面，针对每一种资源对象封装了一些常用的操作项，这样可以简化很多操作流程。\n安装很简单，到 release 页面下载相应的二进制，然后直接运行就好了。下面通过一个完整的示例来演示它的工作流程：\n1、运行 kubeman 二进制文件。\n2、点击 Select Cluster 菜单选择集群，还可以在 NAMESPACES 对话框中选择一个或多个 namespace，将后面操作项的会话限制在某些 namespace 中。\n3、之前选择的集群 context 现在会显示在顶部。\n4、左边一栏是菜单面板，操作项被按照不同的资源类型进行分组，你可以从菜单组中选择一个要执行的操作项。\n5、由于操作项的数量很庞大，从中寻找我们想要的操作项可能会很费劲，还好顶部有一个搜索框，你可以通过搜索来找到你想要的操作项，搜索结果会显示在 Matching Recipes 菜单中。\n6、某些操作项会做更进一步的筛选，例如 namesapce，service，pod 等。\n7、右边是输出面板，用来捕获并显示所有操作项的输出。还提供了一些额外的操作：\n一旦操作项运行并输出了结果，你就可以在输出面板顶部的搜索框里通过关键词搜索相应的文本。如果想删除搜索的关键词，可以按下键盘上的 esc 键。\n每个操作项的输出会按层级进行分组。最顶部的输出行（深蓝色）显示的是输出结果的标题，单击这一行会将整个输出折叠起来，只显示组和子组，这样就可以看到整个输出的概要。再次单击这一行就会显示整个输出。\n同理，你可以单击某一个组来折叠这个组的输出，只显示子组。同理适用于子组。\n不同的子组下的输出都可以展开和折叠，你可以上下滚动来选择感兴趣的子组，然后单击展开输出。\n8、有些操作项需要你在搜索框中输入关键词，然后才会显示输出。例如，操作项 Find component by IP 会等待你输入一个或多个 IP 地址，然后输出结果。此时搜索框扮演了两个角色，既作为输出结果的搜索框，也作为操作项的输入框。如果一个操作项支持输入，需要在输入的字符串前面加上 / 以表明这是操作项的输入。多个输入关键词可以用 , 隔开。\n9、有些操作项支持重复运行，一旦这些操作项执行完成，你就能在输出面板的顶部看到一个 ReRun 菜单，单击它就可以重新运行。你也可以在搜索框中输入命令 /r 来重新运行。\n10、有些操作项支持情况输出结果，一旦这些操作项执行完成，你就能在输出面板的顶部看到一个 Clear 菜单，单击它就可以清理输出结果。你也可以在搜索框中输入命令 /clear 或者 /c 来清理输出结果。\n11、有些操作项支持自动定期执行，这些操作项的菜单栏中有一个 Auto Refresh 选项，还可以自定义执行周期，默认的周期是 15s。\n12、搜索框支持更高级的搜索语法，例如操作符 or 表示或，! 表示非。\n总的来说，kubeman 还是很强大的，简直是个 k8s 集群调试神器，除了上面提到的功能之外，它支持窗口多开，窗口最大化，还可以选择暗黑主题，赶快试试吧！\n","date":"2019年9月8日","externalUrl":null,"permalink":"/posts/kubeman/","section":"博客","summary":"周末闲逛 Twitter 时，发现一个很有意思的小工具叫 kubeman，野心","title":"Kubeman 使用指南","type":"posts"},{"content":"","date":"2019年9月6日","externalUrl":null,"permalink":"/tags/contour/","section":"标签","summary":"","title":"Contour","type":"tags"},{"content":" 上篇文章介绍了 Contour 分布式架构的工作原理，顺便简单介绍了下 IngressRoute 的使用方式。本文将探讨 IngressRoute 更高级的用法，其中级联功能是重点。\nIngressRoute 大入门 # 上篇文章在 examples/example-workload 目录下创建了一个示例应用，我们来回顾一下它的 IngressRoute 配置：\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: labels: app: kuard name: kuard namespace: default spec: virtualhost: fqdn: kuard.local routes: - match: / services: - name: kuard port: 80 virtualhost : 该字段是 root IngressRoute，表示此域的顶级入口点。 fqdn : 该字段指定了 完整的域名，可以通过在 HTTP 请求头中指定 Host: 字段来访问该服务。 这是最简单是使用方法，看起来没什么特别的，我们来稍作修改一下：\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: labels: app: kuard name: kuard namespace: default spec: virtualhost: fqdn: kuard.local routes: - match: /test services: - name: kuard port: 80 将 match: / 改为 match: /test，然后重新应用新规则。这时如果你访问 url kuard.local/test 是不通的，因为 kuard 服务本身并没有 /test 这个路径，我们可以强制将路径重写为 /：\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: labels: app: kuard name: kuard namespace: default spec: virtualhost: fqdn: kuard.local routes: - match: /test prefixRewrite: \u0026#34;/\u0026#34; services: - name: kuard port: 80 重新 apply 之后，再次访问 url kuard.local/test 就通了。\n这里可以和标准的 ingress 对象对比一下，IngressRoute 的优势在于它可以分别对每个路由设置 rewrite 规则，而 Nginx Ingress Controller 只能设置全局的 rewrite 规则，因为它用的是 annotations。虽然可以通过其他手段来实现，但相对来说会比较麻烦。\n级联功能介绍 # 下面我们来看看 IngressRoute 的级联功能，这是个非常有特色的功能，你可以通过级联多个路由规则，上层 IngressRoute 的配置被下层继承。例如，我们可以将 url 路径 / 的路由规则级联到其他的 IngressRoute 中，其他的 IngressRoute 可以来自不同的 namespace。\n举个例子，我们可以先创建一个这样的 IngressRoute：\n$ cat \u0026gt; delegate-from-main.yaml \u0026lt;\u0026lt;EOF apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: delegate-from-main spec: routes: - match: / services: - name: kuard port: 80 EOF $ kubectl apply -f delegate-from-main.yaml $ kubectl get ingressroute delegate-from-main -o jsonpath=\u0026#39;{.status.currentStatus}\u0026#39; orphaned 该 IngressRoute 的状态为 orphaned，因为它没有包含一个合法的 fqdn。接下来需要创建一个 root IngressRoute 来和它进行级联：\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: labels: app: kuard name: kuard namespace: default spec: virtualhost: fqdn: kuard.local routes: - match: / delegate: name: delegate-from-main namespace: default 这时如果再检查 IngressRoute delegate-from-main 的状态，就会发现它从 orphaned 状态变成了 valid 状态，kuard.local 也能够顺利访问。\n了解了级联功能的用法之后，下面就来看看它的应用场景。\n场景一 : 可以使用级联功能来做蓝绿部署和灰度发布，只需要在上层 IngressRoute 中稍作修改，切换到另一个下层 IngressRoute，就可以切换流量的处理规则。 场景二 : 管理员可以利用级联功能将部分 ingress 的权限放行到其他的 namespace 中，在这些 namespace 中，用户可以自由更新与 root IngressRoute 级联的相关的 IngressRoute。例如，如果管理员想防止其他用户配置非法的域名或路径，可以将该部分的配置权限放到 root IngressRoute 中，其他 namespace 中的下层 IngressRoute 中只能配置各自的路径相关信息。 接下来主要探讨场景一。\n蓝绿部署 # 蓝绿部署简单来讲就是在生产环境中有两套系统：一套是正在提供服务的系统，标记为“绿色”；另一套是准备发布的系统，标记为“蓝色”。两套系统都是功能完善的，并且正在运行的系统，只是系统版本和对外服务情况不同。\n最初，没有任何系统，没有蓝绿之分。\n然后，第一套系统开发完成，直接上线，这个过程只有一个系统，也没有蓝绿之分。\n后来，开发了新版本，要用新版本替换线上的旧版本，在线上的系统之外，搭建了一个使用新版本代码的全新系统。 这时候，一共有两套系统在运行，正在对外提供服务的老系统是绿色系统，新部署的系统是蓝色系统。\n蓝色系统不对外提供服务，用来做啥？\n用来做发布前测试，测试过程中发现任何问题，可以直接在蓝色系统上修改，不干扰用户正在使用的系统。（注意，两套系统没有耦合的时候才能百分百保证不干扰）\n蓝色系统经过反复的测试、修改、验证，确定达到上线标准之后，直接将用户切换到蓝色系统：\n切换后的一段时间内，依旧是蓝绿两套系统并存，但是用户访问的已经是蓝色系统。这段时间内观察蓝色系统（新系统）工作状态，如果出现问题，直接切换回绿色系统。\n当确信对外提供服务的蓝色系统工作正常，不对外提供服务的绿色系统已经不再需要的时候，蓝色系统正式成为对外提供服务系统，成为新的绿色系统。 原先的绿色系统可以销毁，将资源释放出来，用于部署下一个蓝色系统。\n通过 IngressRoute 的级联功能可以很方便地实现蓝绿部署策略，首先创建一个上层的 root IngressRoute（假设名为 root-blog），然后将域名 yangcs.net/blogs 的路由策略级联到下层的 IngressRoute（名为 blog）。我们会同时部署”蓝色“版本和”绿色“版本的应用，此时只有”绿色“版本接收流量。\n--- apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: root-blog namespace: root-ingressroute spec: virtualhost: fqdn: yangcs.net tls: secretName: yangcs-net routes: - match: /blog delegate: name: blog namespace: marketing --- apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: blog namespace: marketing spec: routes: - match: /blog services: - name: green port: 80 --- apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: blog2 namespace: marketing spec: routes: - match: /blog services: - name: blue port: 80 在对蓝色版本进行测试验证之后，就可以将用户切换到蓝色应用了：\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: root-blog namespace: root-ingressroute spec: virtualhost: fqdn: yangcs.net tls: secretName: yangcs-net routes: - match: /blog delegate: name: blog2 namespace: marketing 金丝雀发布 # 金丝雀发布（Canary）也是一种发布策略，和国内常说的灰度发布是同一类策略。它和蓝绿有点像，但是它更加规避风险。你可以阶段性的进行，而不用一次性从蓝色版本切换到绿色版本。\n采用金丝雀部署，你可以在生产环境的基础设施中小范围的部署新的应用代码。一旦应用签署发布，只有少数用户被路由到它，可以最大限度的降低影响。\n如果没有错误发生，把剩余的 V1 版本全部升级为 V2 版本。如果有错误发生，则直接回退到老版本，发布失败。下图示范了金丝雀部署：\n其实金丝雀发布的名称来源于一个典故。在 17 世纪，英国矿井工人发现，金丝雀对瓦斯这种气体特别敏感，空气中哪怕有极其微量的瓦斯，金丝雀也会停止唱歌。当瓦斯含量超过一定限度时，人类毫无察觉，但金丝雀却会毒发身亡。当时在采矿设备相对简陋的条件下，工人们每次下井都会带上一只金丝雀作为”瓦斯检测指标“，以便在危险情况下紧急撤离。映射到这里就是先发布一小部分来试探整体是否能够正常运行，如果能正常运行则进行完全部署的发布方式，目前仍然是不少成长型技术组织的主流发布方式。\nIngressRoute 可以通过分配权重来实现金丝雀发布，和蓝绿部署一样，首先创建一个上层的 root IngressRoute（名为 root-blog），然后将域名 yangcs.net/blogs 的路由策略级联到下层的 IngressRoute（名为 blog）。在下层的 IngressRoute 中将流量按不同权重转发到不同的后端服务。\napiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: blog namespace: marketing spec: routes: - match: /blog services: - name: green port: 80 weight: 5 - name: blue port: 80 weight: 95 如果没有错误发生，就将 green 的权重调整为 100，blue 的权重调整为 0。至此就完成了金丝雀发布。\n本文主要介绍了 IngressRoute 级联功能的用法，探讨了如何使用级联功能来实现蓝绿部署和金丝雀发布，后面的文章将会陆续探讨其他的流量治理功能。\n参考资料 # 蓝绿部署、金丝雀发布（灰度发布）、A/B测试的准确定义 微信公众号 # 扫一扫下面的二维码关注微信公众号，在公众号中回复◉加群◉即可加入我们的云原生交流群，和孙宏亮、张馆长、阳明等大佬一起探讨云原生技术\n","date":"2019年9月6日","externalUrl":null,"permalink":"/posts/blue-green-deployments-contours-ingressroute/","section":"博客","summary":"上篇文章介绍了 Contour 分布式架构的工作原理，顺便简单介绍了下 IngressRoute 的使","title":"Contour 学习笔记（二）：使用级联功能实现蓝绿部署和金丝雀发布","type":"posts"},{"content":" 在 Kubernetes 中运行大规模以 Web 为中心的工作负载，最关键的需求之一就是在 L7 层实现高效流畅的入口流量管理。自从第一批 Kubernetes Ingress Controller 开发完成以来，Envoy（由 Matt Klein 和 Lyft 团队开发）已经成为云原生生态系统中的新生力量。Envoy 之所以受到支持，因为它是一个 CNCF 托管的项目，与整个容器圈和云原生架构有着天然的支持。\n容器公司 Heptio 开源的项目 Contour 使用 Envoy 作为 Kubernetes 的 Ingress Controller 实现，为大家提供了一条新的 Kubernetes 外部负载均衡实现思路。\n据 官方博客介绍，Heptio Contour 可以为用户提供以下好处：\n一种简单的安装机制来快速部署和集成 Envoy。 与 Kubernetes 对象模型的集成。 Ingress 配置的动态更新，而无需重启底层负载均衡器。 项目成熟后，将允许使用 Envoy 一些强大的功能，如熔断器、插件式的处理器链，以及可观测性和可调试性，可以非常方便地对接监控系统。 IngressRoute 之间可以级联，用来做蓝绿部署非常方便。 下面我们就来试用一下。\n安装步骤 # Contour Ingress controller 由两个组件组成：\nEnvoy : 提供高性能反向代理。 Contour : 充当 Envoy 的控制平面，为 Envoy 的路由配置提供统一的来源。 官方文档提供了三种部署方法：\n通过 DaemonSet 来部署，每个节点上跑一个 Contour 实例（Contour 与 Envoy 在同一个 Pod 中）。 通过 Deployment 来部署，总共跑两个 Contour 实例（Contour 与 Envoy 在同一个 Pod 中）。 通过 Deployment 来部署 Contour，总共跑两个 Contour 实例；通过 DaemonSet 来部署 Envoy，每个节点上跑一个 Envoy 实例。 经过老夫目测，第三种方案比较妙，这样可以让 Contour 和 Envoy 这两个组件解耦，可以分别按需对不同的组件进行扩展，具体的优势如下：\nEnvoy 以 Daemonset 的形式运行，具有很强的扩展性，后续可通过 ipvs 和 keepalived 等工具来实现其负载均衡和高可用。 Envoy 运行的网络模式是 hostNetwork，减少了额外的网络性能损耗。 Contour 与 Envoy 之间通过双向认证的自签名证书进行通信，大大增强了安全性。 升级 Contour 不需要重启 Envoy。 听起来好像不错的样子。\n我们就采用第三种方案来部署，首先克隆官方仓库，进入 manifest 清单目录：\n$ git clone https://github.com/heptio/contour $ cd contour/examples/ds-hostnet-split 为了便于查看 envoy 的配置，修改 03-envoy.yaml，将 envoy 的 admin-adress 设置为 0.0.0.0，并暴露 9001 端口：\n...省略... initContainers: - args: - bootstrap - --admin-address=0.0.0.0 - /config/contour.json ...省略... 将 Envoy Service 的类型改为 ClusterIP，并删除 annotation：\n$ cat 02-service-envoy.yaml apiVersion: v1 kind: Service metadata: name: envoy namespace: heptio-contour spec: ports: - port: 80 name: http protocol: TCP - port: 443 name: https protocol: TCP selector: app: envoy type: ClusterIP 部署：\n$ kubectl apply ./ 查看状态：\n$ kubectl -n heptio-contour get pod NAME READY STATUS RESTARTS AGE contour-767fd99989-27qjw 0/1 Running 0 21s contour-767fd99989-kcjxz 0/1 Running 0 21s contour-certgen-29nqs 0/1 Completed 0 21s envoy-cnzvm 0/1 Running 0 21s envoy-lb8mm 0/1 Running 0 21s envoy-qzmt4 0/1 Running 0 21s $ kubectl -n heptio-contour get job NAME COMPLETIONS DURATION AGE contour-certgen 1/1 2s 4m42s contour-certgen 是一个 Job，它会生成有效期为一年的 mTLS（双向认证）证书，并将其挂载到 Contour 和 Envoy 的容器中。如果你想自定义证书，可以参考 官方文档。\n如果你还没有部署 Kubernetes 集群怎么办？废话，当然是用 sealos 啊！分分钟搞起一个高可用集群。\nIngress 测试 # 安装结束后，我们就可以来测试 Ingress 了。在 examples/example-workload 目录下包含一个示例应用，可以直接使用：\n$ kubectl apply -f examples/example-workload/kuard-ingressroute.yaml 查看创建好的资源：\n$ kubectl get po,svc,ingressroute -l app=kuard NAME READY STATUS RESTARTS AGE pod/kuard-67789b8754-5c4w7 1/1 Running 0 63s pod/kuard-67789b8754-fpdfb 1/1 Running 0 63s pod/kuard-67789b8754-fx9bn 1/1 Running 0 63s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kuard ClusterIP 10.97.46.79 \u0026lt;none\u0026gt; 80/TCP 63s NAME FQDN TLS SECRET FIRST ROUTE STATUS STATUS DESCRIPTION ingressroute.contour.heptio.com/kuard kuard.local / valid valid IngressRoute 将域名加入本地电脑的 hosts：\n$ echo \u0026#34;$INGRESS_HOST kuard.local\u0026#34; \u0026gt;\u0026gt; /etc/hosts 其中 $INGRESS_HOST 是任意运行 Envoy 节点的 IP。\n现在我们就可以在浏览器中输入域名 kuard.local 访问应用了：\nContour 工作原理 # Contour 同时支持 Ingress 资源对象和 IngressRoute 资源对象（通过 CRD 创建），这些对象都是为进入集群的请求提供路由规则的集合。这两个对象的结构和实现方式有所不同，但它们的核心意图是相同的，都是为进入集群的请求提供路由规则。如不作特殊说明，后面当我们描述 “Ingress” 时，它将同时适用于 Ingress 和 IngressRoute 对象。\n通常情况下，当 Envoy 配置了 CDS 的 endpoint 时，它会定期轮询端点，然后将返回的 JSON 片段合并到其运行配置中。如果返回到 Envoy 的集群配置代表当前的 Ingress 对象的集合，则可以将 Contour 视为从 Ingress 对象到 Envoy 集群配置的转换器。随着 Ingress 对象的添加和删除，Envoy 会动态添加并删除相关配置，而无需不断重新加载配置。\n在实践中，将 Ingress 对象转换为 Envoy 配置更加微妙，需要将 Envoy 中的 xDS 配置（包括 CDS，EDS 和 RDS）映射到 Kubernetes 中。Contour 至少需要观察 Ingress、Service 和 Endpoint 这几个资源对象以构建这些服务的响应，它通过 client-go 的 cache/informer 机制免费获得这些 watchers。这些 watchers 提供添加，更新和删除对象的边缘触发通知，也可以通过 watch API 在本地缓存缓存对象，以便后续查询。\nContour 将收集到的这些对象处理为虚拟主机及其路由规则的有向非循环图（DAG），这表明 Contour 将有权构建路由规则的顶级视图，并将群集中的相应服务和TLS秘钥连接在一起。一旦构建了这个新的数据结构，我们就可以轻松实现 IngressRoute 对象的验证，授权和分发。改数据结构导出的 png 图片如下图所示：\nEnvoy API 调用和 Kubernetes API 资源之间的映射关系如下：\nCDS : 集群发现服务。映射为 Kubernetes 中的 Service 以及一部分 Ingress 对象的 TLS 配置。\nEDS : 服务发现服务。映射为 Kubernetes 中的 Endpoint。Envoy 使用 EDS 自动获取 Cluster 成员，这与 Endpoint 对象中包含的信息非常匹配。Envoy 使用 Contour 在 EDS 响应中返回的名称查询 EDS。\nRDS : 路由发现服务。映射为 Kubernetes 中的 Ingress。提供了虚拟主机名和前缀路由信息的 RDS 与 Ingress 匹配得更好。\n映射关系详情 # CDS # CDS 更像是 Kubernetes 中的 Service 资源，因为 Service 是具体 Endpoint（Pods）的抽象，Envoy Cluster 是指 Envoy 连接到的一组逻辑上相似的上游主机（参考下文的 RDS）。其中 TLS 配置也是 CDS 的一部分，而 Kubernetes 中的 TLS 信息由 Ingress 提供，所以这部分之间的映射关系会有些复杂。\nEDS # EDS 更像是 Kubernetes 中的 Endpoint 资源，这部分映射关系的实现最简单。Contour 将 Endpoint 的响应对象转换为 EDS 的 { address: [] } json 配置块。\nRDS # RDS 更像是 Kubernetes 中的 Ingress 资源。RDS 将前缀，路径或正则表达式之一路由到 Envoy 群集。Envoy 集群的名称可以从 Ingress 的 IngressSpec 的配置项中获取（比如：namespace/serviceName_servicePort），因为这是一个选择器，它会匹配 Service 对象被转换后返回的 CDS 对象。\nContour 架构分析 # Contour Ingress controller 由两个组件组成：\nEnvoy : 提供高性能反向代理。 Contour : 充当 Envoy 的控制平面，为 Envoy 的路由配置提供统一的来源。 以本文的部署方式为例，在 Envoy 的 Pod 初始化期间，Contour 作为 Init 容器运行，并将 bootstrap（初始化）配置写入一个 temporary volume。该 Volume 被传递给 Envoy 容器并告诉 Envoy 将另一个 Deployment 中的 Contour 容器视为控制平面。\n初始化完成后，Envoy 容器启动，检索 Contour 写入的 bootstrap 配置，并开始轮询 Contour 以热更新配置。如果控制平面无法访问，Envoy 将会进行优雅重试。\nContour 相当于 Kubernetes API 的客户端。它监视 Ingress，Service 和 Endpoint 对象，并通过将其对象缓存转换为相关的 JSON 字段来充当其 Envoy 的控制平面。\n从 Kubernetes 到 Contour 的信息转换是通过 SharedInformer 框架 watching API 来完成的；而从 Contour 到 Envoy 的信息转换是通过 Envoy 定期轮询来实现的。\nIngressRoute 介绍 # Ingress 对象从 Kubernetes 1.1 版本开始被引进，用来描述进入集群的请求的 HTTP 路由规则。但迄今为止 Ingress 对象还停留在 beta 阶段，不同的 Ingress Controller 插件为了添加 HTTP 路由的额外属性，只能通过添加大量的 annotation 来实现，而且每个插件的 annotation 都不一样，非常混乱。\nIngressRoute CRD 的目标就是扩展 Ingress API 的功能，以便提供更丰富的用户体验以及解决原始设计中的缺点。\n**目前 Contour 是唯一支持 IngressRoute CRD 的 Kubernetes Ingress Controller。**下面就来看看它与 Ingress 相比的优点：\n安全地支持多团队 Kubernetes 集群，能够限制哪些命名空间可以配置虚拟主机和 TLS 凭据。 允许将路径或域名的路由配置分发给另一个命名空间。 接受单个路由中的多个服务，并对它们之间的流量进行负载均衡。 无需通过添加 annotation 就可以定义服务权重和负载均衡策略。 在创建时验证 IngressRoute 对象，并在创建后报告验证是否有效。 从 Ingress 到 IngressRoute # 一个基本的 Ingress 对象如下所示：\n# ingress.yaml apiVersion: extensions/v1beta1 kind: Ingress metadata: name: basic spec: rules: - host: foo-basic.bar.com http: paths: - backend: serviceName: s1 servicePort: 80 这个 Ingress 对象名为 basic，它将传入的 HTTP 流量路由到头文件中 Host: 字段值为 foo-basic.bar.com 且端口为 80 的 s1 服务。该路由规则通过 IngressRoute 来实现如下：\n# ingressroute.yaml apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: name: basic spec: virtualhost: fqdn: foo-basic.bar.com routes: - match: / services: - name: s1 port: 80 对应关系很简单，我就不再详细介绍了，更多功能配置可以参考官方仓库的文档： IngressRoute。\nEnvoy 初始配置文件 # Contour 会根据启动参数和 K8S API Server 中的配置信息生成 Envoy 的初始配置文件，可以使用下面的命令将 Envoy Pod 中的配置文件导出来查看其中的内容：\n$ kubectl -n heptio-contour exec envoy-lb8mm -- cat /config/envoy.json \u0026gt; envoy.json 打开网站 https://www.bejson.com/jsonviewernew/，将配置文件内容复制粘贴进去，可以看到配置文件的结构如图所示：\n其中各个配置节点的内容如下：\nDynamic_resources # 配置动态资源,这里配置了 LDS 和 RDS 服务器。\n\u0026#34;dynamic_resources\u0026#34;: { \u0026#34;lds_config\u0026#34;: { \u0026#34;api_config_source\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;contour\u0026#34; } } ] } }, \u0026#34;cds_config\u0026#34;: { \u0026#34;api_config_source\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;contour\u0026#34; } } ] } } } Static_resources # 配置静态资源，包括了 contour 和 service-stats 两个 cluster，其中 contour cluster 对应前面 dynamic_resources 中的 LDS 和 RDS 配置，指明了 Envoy 用于获取动态资源的服务器地址。\n\u0026#34;static_resources\u0026#34;: { \u0026#34;clusters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;contour\u0026#34;, \u0026#34;alt_stat_name\u0026#34;: \u0026#34;heptio-contour_contour_8001\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;connect_timeout\u0026#34;: \u0026#34;5s\u0026#34;, \u0026#34;load_assignment\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;contour\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;lb_endpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;contour\u0026#34;, \u0026#34;port_value\u0026#34;: 8001 } } } } ] } ] }, \u0026#34;circuit_breakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ { \u0026#34;priority\u0026#34;: \u0026#34;HIGH\u0026#34;, \u0026#34;max_connections\u0026#34;: 100000, \u0026#34;max_pending_requests\u0026#34;: 100000, \u0026#34;max_requests\u0026#34;: 60000000, \u0026#34;max_retries\u0026#34;: 50 }, { \u0026#34;max_connections\u0026#34;: 100000, \u0026#34;max_pending_requests\u0026#34;: 100000, \u0026#34;max_requests\u0026#34;: 60000000, \u0026#34;max_retries\u0026#34;: 50 } ] }, \u0026#34;tls_context\u0026#34;: { \u0026#34;common_tls_context\u0026#34;: { \u0026#34;tls_certificates\u0026#34;: [ { \u0026#34;certificate_chain\u0026#34;: { \u0026#34;filename\u0026#34;: \u0026#34;/certs/tls.crt\u0026#34; }, \u0026#34;private_key\u0026#34;: { \u0026#34;filename\u0026#34;: \u0026#34;/certs/tls.key\u0026#34; } } ], \u0026#34;validation_context\u0026#34;: { \u0026#34;trusted_ca\u0026#34;: { \u0026#34;filename\u0026#34;: \u0026#34;/ca/cacert.pem\u0026#34; }, \u0026#34;verify_subject_alt_name\u0026#34;: [ \u0026#34;contour\u0026#34; ] } } }, \u0026#34;http2_protocol_options\u0026#34;: {} }, { \u0026#34;name\u0026#34;: \u0026#34;service-stats\u0026#34;, \u0026#34;alt_stat_name\u0026#34;: \u0026#34;heptio-contour_service-stats_9001\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;LOGICAL_DNS\u0026#34;, \u0026#34;connect_timeout\u0026#34;: \u0026#34;0.250s\u0026#34;, \u0026#34;load_assignment\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;service-stats\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;lb_endpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 9001 } } } } ] } ] } } ] } Admin # 配置 Envoy 的日志路径以及管理端口。\n\u0026#34;admin\u0026#34;: { \u0026#34;access_log_path\u0026#34;: \u0026#34;/dev/null\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 9001 } } } 结合 Envoy 的初始化配置文件和第 5 节的分析，我们可以大致看到 Contour 通过 Envoy 来实现南北流量管理的基本原理。即控制平面将 xDS server 信息通过 static resource 的方式配置到 Envoy 的初始化配置文件中，Envoy 启动后通过 xDS server 获取到 dynamic resource，包括集群中的 service 信息及路由规则。\nEnvoy 配置初始化流程：\nEnvoy initContainer 根据启动参数和 K8S API Server 中的配置信息生成 Envoy 的初始配置文件 envoy.json，该文件告诉 Envoy 从 xDS server 中获取动态配置信息，并配置了 xDS server 的地址信息，即控制平面的 Contour。 Envoy 使用配置文件 envoy.json 启动。 Envoy 根据获取到的动态配置启动 Listener，并根据 Listener 的配置，结合 Route 和 Cluster 对进入的流量进行处理。 IngressRoute 配置映射 # 通过上节的分析我们知道，Envoy 中实际生效的配置是由初始化配置文件中的静态配置和从 Contour 获取的动态配置一起组成的，我们可以通过 Envoy 的管理接口来获取 Envoy 的完整配置，先打开 Envoy 的管理接口：\n然后点击 config_dump，就能看到 Envoy 的完整配置：\n我们在第二节创建了一个 ingressroute，现在来看看它是怎么映射到 Envoy 的配置文件中的。你可以到 config_dump 中查找相关配置，但还有更好的办法，我们可以通过 Contour 的命令行工具直接调用 Contour 的 xDS gRPC 接口来分别查看 Envoy 的 Listener、Route、Cluster 和 Endpoint 配置。\nContour 总共有两个实例，通过选举来实现高可用，被选中的实例作为 leader 来对外提供服务。\n$ kubectl -n heptio-contour get pod -l app=contour NAME READY STATUS RESTARTS AGE contour-767fd99989-27qjw 1/1 Running 0 14h contour-767fd99989-kcjxz 0/1 Running 0 14h 看看哪个是 leader：\n可以看到该实例不是 leader，我们到另一个实例中去查看 Envoy xDS 配置。\nListener # Envoy 采用 listener 来接收并处理 downstream 发过来的请求，listener 的处理逻辑是插件式的，可以通过配置不同的 filter 来插入不同的处理逻辑。Listener 可以绑定到 IP Socket 或者 Unix Domain Socket 上，也可以不绑定到一个具体的端口上，而是接收从其他 listener 转发来的数据。\nListener 的配置可以通过下面的命令查看：\n$ kubectl -n heptio-contour exec -it contour-767fd99989-27qjw -- contour cli --cafile=/ca/cacert.pem --cert-file=/certs/tls.crt --key-file=/certs/tls.key lds 可以看到 Listener 被绑定到了 80 端口上，同时通过 RDS 配置了一个路由规则 ingress_http，在路由规则中再根据不同的请求目的地对请求进行处理。\nRoute # Route 用来配置 Envoy 的路由规则，根据 host 来对请求进行路由分发。\nRoute 的配置可以通过下面的命令查看：\n$ kubectl -n heptio-contour exec -it contour-767fd99989-27qjw -- contour cli --cafile=/ca/cacert.pem --cert-file=/certs/tls.crt --key-file=/certs/tls.key rds 上面是 ingress_http 的路由配置，对应了两个 virtual host，其中一个是默认路由（图中省略），上面展示的是 kuard 的路由，对应到 Cluster default/kuard/80/da39a3ee5e。其中 domains: \u0026quot;kuard.local:*\u0026quot; 表示允许访问的域名为 kuard.local，端口可以是任意值。\nCluster # Cluster 是一个服务集群，Cluster 中包含一个到多个 endpoint，每个 endpoint 都可以提供服务，Envoy 根据负载均衡算法将请求发送到这些 endpoint 中。\nCluster 的配置可以通过下面的命令查看：\n$ kubectl -n heptio-contour exec -it contour-767fd99989-27qjw -- contour cli --cafile=/ca/cacert.pem --cert-file=/certs/tls.crt --key-file=/certs/tls.key cds cluster_name: \u0026quot;contour\u0026quot; 表示通过 xDS 接口从 contour 控制平面动态获取 Endpoint 信息。获取到的 Endpoint 是 default/kuard。\nEndpoint # Endpoint 就对应到 Kubernetes 中的 Endpoint 资源，对应的即是 Pod 的 IP+Port。\nCluster 的配置可以通过下面的命令查看：\n$ kubectl -n heptio-contour exec -it contour-767fd99989-27qjw -- contour cli --cafile=/ca/cacert.pem --cert-file=/certs/tls.crt --key-file=/certs/tls.key eds|grep \u0026#34;default/kuard\u0026#34; -A 34 -B 2 验证一下：\n$ kubectl get ep -l app=kuard NAME ENDPOINTS AGE kuard 100.118.117.18:8080,100.119.55.150:8080,100.91.147.204:8080 17h 对接监控 # Contour 和 Envoy 都暴露一些监控指标可以被 Prometheus 抓取，官方也提供了 Prometheus 和 Grafana 的部署模板，但一般情况下我们都会有自己的监控系统，比如 prometheus-operator，只需要将官方的 Grafana 模板导入自己的 Grafana 中就可以了，后续会探讨详细步骤。\nEnvoy Metrics # Envoy 默认通过 admin 接口暴露监控指标，为了避免暴露 admin 接口，Contour 创建了一个静态 Listener，只将访问路径为 /stats 的流量转发到 service-stats Cluster，即 admin 接口，其他所有请求一律拒绝访问。\n本文只是为了方便查看，才将 admin 接口的 IP 改为 0.0.0.0，生产环境建议不要改，默认值为 127.0.0.1。\n所以 Envoy 在 8002 端口暴露监控指标，路径为 /stats/prometheus。\nContour Metrics # Contour 在 8000 端口上暴露监控指标，路径为 /metrics。包含以下监控指标：\ncontour_ingressroute_total (gauge) : IngressRoute 的总数量，包括状态为 Valid / Invalid / Orphaned 的 IngressRoute。 contour_ingressroute_orphaned_total (gauge) : 状态为 Orphaned 的 IngressRoute 数量。 contour_ingressroute_root_total (gauge) : Root IngressRoute 的数量（每个 vhost 只有一个 Root IngressRoute）。 contour_ingressroute_valid_total (gauge) : 状态为 Valid 的 IngressRoute 数量。 contour_ingressroute_invalid_total (gauge) : 状态为 Invalid 的 IngressRoute 数量。 contour_ingressroute_dagrebuild_timestamp (gauge) : 最近一次重建 DAG 的时间戳。 下面就来教大家怎么将 Contour 接入 Prometheus-Operator，对 Prometheus-Operator 不熟的同学，推荐看一下张馆长的这篇文章： 全手动部署prometheus-operator监控Kubernetes集群遇到的坑。\nRBAC 授权 # 为了让 Prometheus 能够 list 其他 namespace 中的 pod，我们需要赋予它相应的权限，首先进入 kube-prometheus 项目的 manifests 目录：\n$ cd kube-prometheus/manifests $ ll *SpecificNamespace* 4 -rw-r--r-- 1 root root 988 8月 27 05:22 prometheus-roleBindingSpecificNamespaces.yaml 4 -rw-r--r-- 1 root root 1078 8月 27 05:15 prometheus-roleSpecificNamespaces.yaml 修改 prometheus-roleSpecificNamespaces.yaml，向其中添加如下的 Role：\n- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: prometheus-k8s namespace: heptio-contour rules: - apiGroups: - \u0026#34;\u0026#34; resources: - services - endpoints - pods verbs: - get - list - watch 修改 prometheus-roleBindingSpecificNamespaces.yaml，向其中添加如下的 RoleBinding：\n- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: prometheus-k8s namespace: heptio-contour roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: prometheus-k8s subjects: - kind: ServiceAccount name: prometheus-k8s namespace: monitoring 然后创建相应的 Role 和 RoleBinding：\n$ kubectl apply -f prometheus-roleSpecificNamespaces.yaml $ kubectl apply -f prometheus-roleBindingSpecificNamespaces.yaml 修改 Contour manifest 文件 # Prometheus 监控的对象被叫做 Target，Target 通过 Cluster 中的 Endpoint 资源来定义，每个监控对象都有一个对应的 Endpoint。而 ServiceMonitor 是 Target 的抽象，ServiceMonitor 通过标签来找到对应的 Endpoint，然后将相应的 Target 添加到 Prometheus 的监控列表中。\n默认情况下 Contour 的 Service 是没有打标签的，所以我们需要修改 yaml 文件，加上相应的标签。首先修改 Contour Deployment 的 yaml 文件：\n# 03-contour.yaml ports: - containerPort: 8001 name: xds protocol: TCP - containerPort: 8000 name: http-metrics # 将 name 改为 http-metrics protocol: TCP 再修改 Contour Service 的 yaml 文件：\n# 02-service-envoy.yaml ports: - port: 80 name: http protocol: TCP - port: 443 name: https protocol: TCP # 添加新端口 - port: 8002 name: http-metrics protocol: TCP Envoy 类似，先修改 Envoy Deployment 的 yaml 文件：\n# 03-envoy.yaml ports: - containerPort: 80 hostPort: 80 name: http protocol: TCP - containerPort: 443 hostPort: 443 name: https protocol: TCP # 添加新端口 - containerPort: 8002 hostPort: 8002 name: http-metrics protocol: TCP 再修改 Envoy Service 的 yaml 文件：\n# 02-service-envoy.yaml ports: - port: 80 name: http protocol: TCP - port: 443 name: https protocol: TCP # 添加新端口 - port: 8002 name: http-metrics protocol: TCP 最后重新 apply 一下：\n# 在 contour/examples/ds-hostnet-split 目录下 $ kubectl apply -f ./ 创建 ServiceMonitor # 接下来就是创建相应的 ServiceMonitor 来抓取指标数据，没什么好说的，自己看 yaml 文件：\n$ cat prometheus-serviceMonitorContour.yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app: contour name: contour namespace: monitoring spec: endpoints: - interval: 30s port: http-metrics jobLabel: app namespaceSelector: matchNames: - heptio-contour selector: matchLabels: app: contour --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app: envoy name: envoy namespace: monitoring spec: endpoints: - interval: 30s path: /stats/prometheus port: http-metrics namespaceSelector: matchNames: - heptio-contour selector: matchLabels: app: envoy 创建 IngressRoute # 为了查看 Prometheus 和 Grafana 的 Dashboard，我们需要为它们创建相应的 IngressRoute，yaml 文件内容如下：\n# ingressroute-prometheus.yaml apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: labels: app: grafana name: grafana namespace: monitoring spec: virtualhost: fqdn: grafana.sealos.io routes: - match: / services: - name: grafana port: 3000 --- apiVersion: contour.heptio.com/v1beta1 kind: IngressRoute metadata: labels: app: prometheus name: prometheus namespace: monitoring spec: virtualhost: fqdn: prometheus.sealos.io routes: - match: / services: - name: prometheus-k8s port: 9090 直接 apply：\n$ kubectl apply -f ingressroute-prometheus.yaml 将域名写入本地电脑的 hosts 中：\n$ echo \u0026#34;$INGRESS_HOST grafana.sealos.io\u0026#34; \u0026gt;\u0026gt; /etc/hosts $ echo \u0026#34;$INGRESS_HOST prometheus.sealos.io\u0026#34; \u0026gt;\u0026gt; /etc/hosts 其中 $INGRESS_HOST 是任意运行 Envoy 节点的 IP。\n现在我们可以在浏览器中输入域名 prometheus.sealos.io 来查看 Prometheus Target 状态。\n可以看到这两个 Target 已经被抓取到了。\n导入 Grafana 模板 # 前面提到 Contour 官方仓库中提供了 Grafana Dashboard 模板，现在我们要做的事就是把这个模板导入到自己的 Grafana 中。官方的 Grafana Dashboard 模板定义在 contour/examples/grafana 目录下的 ConfigMap 文件中，可以先把它导出来：\n# 假设 contour 项目在你的 $HOME 目录 $ sed \u0026#39;/---/,$d\u0026#39; ~/contour/examples/grafana/02-grafana-configmap.yaml \\ |sed \u0026#39;s/grafana-dashs/grafana-dashboard-contour/\u0026#39; \\ |sed \u0026#39;s/contour-monitoring/monitoring/\u0026#39; \\ |sed \u0026#39;s/kubernetes_pod_name/pod/g\u0026#39; \\ |sed \u0026#39;s/ingress_http_update/update/g\u0026#39; \\ |kubectl apply -f - configmap/grafana-dashboard-contour created 创建了 ConfigMap 后，还要再挂载到 Grafana 的 Dashboard 中，所以需要修改 Grafana Deployment 的 yaml 文件：\n# ~/kube-prometheus/manifests/grafana-deployment.yaml volumeMounts: - mountPath: /var/lib/grafana name: grafana-storage readOnly: false ...省略... # 新增挂载 - mountPath: /grafana-dashboard-definitions/0/contour name: grafana-dashboard-contour readOnly: false ...省略... volumes: - emptyDir: {} name: grafana-storage ...省略... # 新增 ConfigMap - configMap: name: grafana-dashboard-contour name: grafana-dashboard-contour 重新 apply 一下：\n$ kubectl apply -f grafana-deployment.yaml 现在在浏览器中输入域名 grafana.sealos.io，就可以看到 Contour 和 Envoy 的 Dashboard 了。\n对接监控到这里就结束了，剩下的大家可以自己去探索，总体来说难度还是稍微有点大，希望我的细心讲解能够帮助到你。\n","date":"2019年8月29日","externalUrl":null,"permalink":"/posts/use-envoy-as-a-kubernetes-ingress/","section":"博客","summary":"在 Kubernetes 中运行大规模以 Web 为中心的工作负载，最关键的需求之一就是在","title":"Contour 学习笔记（一）：使用 Contour 接管 Kubernetes 的南北流量","type":"posts"},{"content":"认证与授权对任何安全系统来说都至关重要，Kubernetes 也不例外。即使我们不是安全工作人员，也需要了解我们的 Kubernetes 集群是否具有足够的访问控制权限。Kubernetes 社区也越来越关注容器的安全评估（包括渗透测试，配置审计，模拟攻击），如果你是应用安全工程师，或者是安全感知的 DevOps 工程师，最好了解一下 Kubernetes 的授权模型。\nKubernetes 的授权控制原则与大多数系统一样 : 在授予访问权限时采用最小授权原则。例如，如果某个 Pod 使用了特定的 serviceAccount，那么该 Pod 被限定为只能拥有指定的权限，只能访问特定的资源。\nKubernetes 从 1.6 开始支持基于角色的访问控制机制（Role-Based Access，RBAC），集群管理员可以对用户或服务账号的角色进行更精确的资源访问控制。先简单回顾一下 RBAC 的原理。\nRBAC 基础概念 # RBAC 授权策略会创建一系列的 Role 和 ClusterRole 来绑定相应的资源实体（serviceAccount 或 group），以此来限制其对集群的操作。每一个 Role 都基于 Create, Read, Update, Delete（CRUD）模型来构建，并使用“动词”来应用相应的权限。例如，动词 get 表示能够获取特定资源的详细信息。如果你想获取对 Secrets 的访问权限，可以创建如下的 ClusterRole：\napiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: secret-reader rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;secrets\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;list\u0026#34;] 关于 RBAC 的更多详细文档请参考 Kubernetes 官方文档或 CNCF 的博客。\nRBAC 实践 # RBAC 授权模型为我们提供了一种精确的访问控制机制，但随着环境越来越复杂，这些 RBAC 配置也越来越难维护。RBAC 配置可能包含了 Roles, RoleBindings, ClusterRoles, ClusterRoleBindings, ServiceAccounts 和 Groups 等，想要跟踪它们之间的关系非常困难。\n举个栗子，先创建一个名叫 helm 的 ServiceAccount，然后创建相应的 Role 绑定 “tiller-world” namespace，该 Role 只拥有 list pods 的权限，最后通过创建 RoleBinding 将该 Role 与之前创建的 ServiceAccount 绑定。\napiVersion: v1 kind: ServiceAccount metadata: name: helm namespace: helm-world --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tiller-user namespace: tiller-world rules: - apiGroups: - \u0026#34;\u0026#34; resources: - pods/portforward verbs: - create - apiGroups: - \u0026#34;\u0026#34; resources: - pods verbs: - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tiller-user-binding namespace: tiller-world roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: tiller-user subjects: - kind: ServiceAccount name: helm namespace: helm-world 如果你想知道新创建的授权对象是否仅被授予必要的访问权限，就需要审查这些对象及其在集群中的关系。有时候还需要确保其仅对特定的资源实例具有访问权限，不允许访问所有的资源实例。例如，如果你不想让上面的 ServiceAccount 访问所有的 Secret，只允许它访问特定的 Secret，可以使用 resourceNames 字段指定：\nrules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;secrets\u0026#34;] resourceNames: [\u0026#34;my-pod-secrets\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;list\u0026#34;] 这个方法的问题在于无法过滤集群中不存在的资源，这意味着如果资源的名称是动态变化的，那么就无法创建相应的 Role，除非在创建 Role 的同时创建资源。\n审计很重要 # 为了查看每个 Role 的作用以及每个资源对象应该能做哪些事情，我们不得不进行一些审计工作。最简单的审计就是确认某个 Service Account 是否拥有 Cluster Admin 的权限，再复杂一点，确认某个 CI/CD Service Account 在指定的 namespace 内是否拥有 Update Pod 的权限。\n基于审计的目标，大致可以分为两种审计模式：\n资源审计：识别风险最高的资源对象，并查看谁可以访问它们。 账户审计：查看账户的有效权限并确保它们的合理性。 账户审计比较彻底，但很耗时；资源审计可以更快发现问题，但可能会有所遗漏。举例：\n资源审计：查看谁能访问某个 Secret 资源，并确保其是否遵循最小授权原则。 账户审计：遍历所有的 user，group，Service Account 和 RoleBinding，确保它们是否被授予正确的访问权限，并只限定在特定的 namespace 内。 下面提供几种命令行工具来帮助大家更方便地审计 RBAC。\nKubectl Can-I # 某些生产环境不允许安装额外的服务，只能使用 kubectl，我们可以使用 kubectl 的内置命令 kubectl auth can-i 来查看 RBAC 权限。\n例如，查看你是否拥有 get pod 的权限：\n$ kubectl auth can-i get pods yes 查看你是否拥有 cluster-admin 的权限：\n$ kubectl auth can-i \u0026#34;*\u0026#34; \u0026#34;*\u0026#34; yes 列出你在某个 namesapce 中拥有的所有权限：\n$ kubectl auth can-i --list --namespace=secure Resources Non-Resource URLs Resource Names Verbs *.* [] [] [*] [*] [] [*] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] [/api/*] [] [get] [/api] [] [get] [/apis/*] [] [get] [/apis] [] [get] [/healthz] [] [get] [/healthz] [] [get] [/openapi/*] [] [get] [/openapi] [] [get] [/version/] [] [get] [/version/] [] [get] [/version] [] [get] [/version] [] [get] 来点更有趣的，我们还可以通过 Kubernetes 的 Impersonation API 来查看其他账户是否拥有访问特定资源的权限。例如，查看名为 unprivileged-service-account 的 Service Account 是否拥有 get pod 的权限：\n$ kubectl auth can-i get pod \\ --as system:serviceaccount:secure:unprivileged-service-account yes --as 参数用来指定账户名称，类似的参数还有 --as-group，背后的原理实际上是一组传递给 API Server 的请求头。\nKubernetes 中除了有 Service Account 之外还会有 User，每创建一个 Service Account，都会 自动创建一个对应的 User，名称格式为：system:serviceaccount:\u0026lt;namespace\u0026gt;\u0026lt;serviceaccount\u0026gt;。想知道某个 Service Account 的 username 可以通过它的 yaml 文件来推算：\n$ kubectl get serviceaccount unprivileged-service-account -o yaml apiVersion: v1 kind: ServiceAccount metadata: creationTimestamp: \u0026#34;2019-07-23T17:44:31Z\u0026#34; name: unprivileged-service-account namespace: default resourceVersion: \u0026#34;98089\u0026#34; selfLink: /api/v1/namespaces/default/serviceaccounts/unprivileged-service-account secrets: - name: unprivileged-service-account-token-9cggz 通过将 verbs 字段的值指定为 impersonate，可以让某个用户拥有其他用户的权限，即可以模拟其他用户。例如，管理员可以使用此功能通过暂时模拟其他用户并查看请求是否被拒绝来调试授权策略。\n例如，如果你想让非 Cluster Admin 账户能够模拟其他用户，可以创建如下的 ClusterRole：\napiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: impersonator rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;users\u0026#34;, \u0026#34;groups\u0026#34;, \u0026#34;serviceaccounts\u0026#34;] verbs: [\u0026#34;impersonate\u0026#34;] Kubectl Who Can # 下面介绍的这款工具是 kubectl 的插件，插件名叫 who-can，顾名思义，用来显示哪些账户拥有访问特定资源的权限。安装方法很简单，可以通过 kubectl 的插件管理框架 Krew 来安装：\n安装 krew。参考 https://github.com/kubernetes-sigs/krew/\n安装 who-can：\n$ kubectl krew install who-can 假设 secure namespace 中有一个 Secret 名为 cluster-admin-creds，你想查看谁拥有访问它的权限：\n$ kubectl who-can get secret cluster-admin-creds -n secure ROLEBINDING NAMESPACE SUBJECT TYPE SA-NAMESPACE unpriv_sa_binding secure unprivileged-service-account ServiceAccount secure CLUSTERROLEBINDING SUBJECT TYPE SA-NAMESPACE cluster-admin system:masters Group 输出信息也很一目了然，没什么可说的。提醒一下，该工具只支持查看 create、update 和 delete 这几个访问权限，不支持 use。use 用来将 Pod Security Policy 绑定到相应的 Role。\nRakkess # rakkess 与 who-can 类似，可以列出某个账户对所有资源的访问权限，可以通过 krew 来安装。\n使用方法也很简单，如果想查看当前用户对所有资源的访问权限，可使用如下命令：\n如果想查看某个特定的 Service Account 对所有资源的访问权限，可以使用如下命令：\n$ kubectl access-matrix --as system:serviceaccount:kube-ovn:ovn -n kube-ovn 更多用例可以参考官方文档。\nRBack # rback 用来对 kubectl 的输出结果进行可视化展示，可以输出为 .dot 格式，也可以输出为 .png 或任何格式。\n例如：\n$ kubectl get sa,roles,rolebindings \\ -n monitoring -o json | rback \u0026gt; rback.dot 或者保存为 png：\n$ kubectl get sa,roles,rolebindings \\ -n monitoring -o json \\ | rback | dot -Tpng \u0026gt; rback.png RBAC-View # rbac-view 也可以用来可视化账户与权限之间的关系，但与 rback 不同，它是一个 web 应用，安装方法参考官方文档。\n使用方式：\n$ kubectl rbac-view serving RBAC View and http://localhost:8800 在浏览器中打开链接 http://localhost:8800。\n终极测试 # 上面提到的所有方法都可以帮助我们快速收集信息，但有时难免会出现误报的情况。想要确认某账户到底有没有相应的权限，可以使用下面提到的终极方法。例如，要想确认 secure namespace 中的 unprivileged-service-account 是否具有 get secret 的权限，可以使用如下的命令：\n$ kubectl get secrets \\ --as system:serviceaccount:secure:unprivileged-service-account \\ -o yaml 模拟攻击 # 预防攻击最好的方法是模拟攻击，我们可以模拟一个黑客进入其中的某个 Pod，看看能否执行一些不可描述的操作。步骤如下：\n创建一个 Service Account。\n$ kubectl create serviceaccount ncc-sa 创建相应的角色。\n$ cat \u0026lt;\u0026lt;EOF | kubectl apply -f - apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: default name: pod-reader rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;pods\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;list\u0026#34;] EOF 将 Role 与 Service Account 绑定。\n$ cat \u0026lt;\u0026lt;EOF | kubectl apply -f - apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: read-pods namespace: default subjects: - kind: ServiceAccount name: ncc-sa namespace: default roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io EOF 创建一个测试 Pod，serviceAccountName 指定为 ncc-sa。\n$ cat \u0026lt;\u0026lt;EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: ncc-pod spec: serviceAccountName: ncc-sa containers: - name: busybox image: busybox args: - sleep - \u0026#34;1000000\u0026#34; 进入该 Pod\n$ kubectl exec -it ncc-pod sh $ wget https://storage.googleapis.com/kubernetes-release/release/v1.15.0/bin/linux/amd64/kubectl \u0026amp;\u0026amp; chmod +x $ install kubectl /bin 验证是否具有 get pod 的权限。\n$ kubectl get pod 总结 # 随着集群环境越来越复杂，RBAC Role 与其相关资源对象之间关系的复杂性也会呈指数级增长。如果 Role 太多，那么运维人员可能很难选择正确的 Role，容易犯错；如果 Role 太少，运维人员可能会被迫选择默认的 Role，这会导致某些 Pod 权限过大。所以我们需要找到一个平衡点，通常的做法是通过 ansible 或 terraform 将某些部署策略抽象出来变成模板，将 RBAC 策略写到模板中，这样可以大大减轻开发人员的压力。\n","date":"2019年8月20日","externalUrl":null,"permalink":"/posts/tools-and-methods-for-auditing-kubernetes-rbac-policies/","section":"博客","summary":"认证与授权对任何安全系统来说都至关重要，Kubernetes","title":"Kubernetes RBAC 策略审计指南","type":"posts"},{"content":"","date":"2019年8月6日","externalUrl":null,"permalink":"/tags/histogram/","section":"标签","summary":"","title":"Histogram","type":"tags"},{"content":"Prometheus 中提供了四种指标类型（参考： Prometheus 的指标类型），其中直方图（Histogram）和摘要（Summary）是最复杂和难以理解的，这篇文章就是为了帮助大家加深对这 histogram 类型指标的理解。\n1. 什么是 Histogram？ # 根据 上篇文档，Histogram 会在一段时间范围内对数据进行采样（通常是请求持续时间或响应大小等），并将其计入可配置的存储桶（bucket）中。但这句话还是不太好理解，下面通过具体的示例来说明。\n假设我们想监控某个应用在一段时间内的响应时间，最后监控到的样本的响应时间范围为 0s~10s。现在我们将样本的值域划分为不同的区间，即不同的 bucket，每个 bucket 的宽度是 0.2s。那么第一个 bucket 表示响应时间小于等于 0.2s 的请求数量，第二个 bucket 表示响应时间大于 0.2s 小于等于 0.4s 的请求数量，以此类推。\nPrometheus 的 histogram 是一种累积直方图，与上面的区间划分方式是有差别的，它的划分方式如下：还假设每个 bucket 的宽度是 0.2s，那么第一个 bucket 表示响应时间小于等于 0.2s 的请求数量，第二个 bucket 表示响应时间小于等于 0.4s 的请求数量，以此类推。也就是说，每一个 bucket 的样本包含了之前所有 bucket 的样本，所以叫累积直方图。\n2. 为什么是累积直方图？ # 上节内容告诉我们，Prometheus 中的 histogram 是累积的，这是很奇怪的，因为通常情况下非累积的直方图更容易理解。Prometheus 为什么要这么做呢？\n想象一下，如果 histogram 类型的指标中加入了额外的标签，或者划分了更多的 bucket，那么样本数据的分析就会变得越来越复杂。如果 histogram 是累积的，在抓取指标时就可以根据需要丢弃某些 bucket，这样可以在降低 Prometheus 维护成本的同时，还可以粗略计算样本值的分位数。通过这种方法，用户不需要修改应用代码，便可以动态减少抓取到的样本数量。\n假设某个 histogram 类型指标的样本数据如下：\n# HELP example_latency_seconds Some help text # TYPE example_latency_seconds histogram example_latency_seconds_bucket{le=\u0026#34;0.005\u0026#34;} 0.0 example_latency_seconds_bucket{le=\u0026#34;0.01\u0026#34;} 0.0 example_latency_seconds_bucket{le=\u0026#34;0.025\u0026#34;} 0.0 example_latency_seconds_bucket{le=\u0026#34;0.05\u0026#34;} 1.0 example_latency_seconds_bucket{le=\u0026#34;0.075\u0026#34;} 1.0 example_latency_seconds_bucket{le=\u0026#34;0.1\u0026#34;} 1.0 example_latency_seconds_bucket{le=\u0026#34;0.25\u0026#34;} 2.0 example_latency_seconds_bucket{le=\u0026#34;0.5\u0026#34;} 3.0 example_latency_seconds_bucket{le=\u0026#34;0.75\u0026#34;} 3.0 example_latency_seconds_bucket{le=\u0026#34;1.0\u0026#34;} 4.0 example_latency_seconds_bucket{le=\u0026#34;2.5\u0026#34;} 4.0 example_latency_seconds_bucket{le=\u0026#34;5.0\u0026#34;} 5.0 example_latency_seconds_bucket{le=\u0026#34;7.5\u0026#34;} 5.0 example_latency_seconds_bucket{le=\u0026#34;10.0\u0026#34;} 5.0 example_latency_seconds_bucket{le=\u0026#34;+Inf\u0026#34;} 5.0 example_latency_seconds_count 5.0 example_latency_seconds_sum 6.54 现在我们希望 Prometheus 在抓取指标时丢弃响应时间在 100ms 以下的 bucket，就可以通过下面的 relabel 配置来实现：\nscrape_configs: - job_name: \u0026#39;my_job\u0026#39; static_configs: - targets: - my_target:1234 metric_relabel_configs: - source_labels: [ __name__, le ] regex: \u0026#39;example_latency_seconds_bucket;(0\\.0.*)\u0026#39; action: drop 其中，example_latency_seconds_bucket 用来匹配标签 __name__ 的值，\u0026lsquo;0.0.*\u0026rsquo; 用来匹配标签 le 的值，即 le 的值为 0.0x。然后将匹配到的样本丢弃。\n通过这种方法，你可以丢弃任意的 bucket，但不能丢弃 le=\u0026quot;+Inf\u0026quot; 的 bucket，因为 histogram_quantile 函数需要使用这个标签。\n另外 histogram 还提供了 _sum 指标和 _count 指标，即使你丢弃了所有的 bucket，仍然可以通过这两个指标值来计算请求的平均响应时间。\n通过累积直方图的方式，还可以很轻松地计算某个 bucket 的样本数占所有样本数的比例。例如，想知道响应时间小于等于 1s 的请求占所有请求的比例，可以通过以下公式来计算：\nexample_latency_seconds_bucket{le=\u0026#34;1.0\u0026#34;} / ignoring (le) example_latency_seconds_bucket{le=\u0026#34;+Inf\u0026#34;} 3. 分位数计算 # Prometheus 通过 histogram_quantile 函数来计算分位数（quantile），而且是一个预估值，并不完全准确，因为这个函数是假定每个区间内的样本分布是线性分布来计算结果值的。预估的准确度取决于 bucket 区间划分的粒度，粒度越大，准确度越低。以下图为例：\n假设有 10000 个样本，第 9501 个样本落入了第 8 个 bucket。第 8 个 bucket 总共有 368 个样本，其中第 9501 个样本在该 bucket 中属于第 93 个样本。\n根据 Prometheus 源代码文件 promql/quantile.go 第 108 行的公式：\nreturn bucketStart + (bucketEnd-bucketStart)*float64(rank/count) 我们可以计算（quantile=0.95）的样本值为：\n这个值已经很接近精确的分位数值了。关于 histogram_quantile 函数的详细使用方式，请参考： PromQL 内置函数。\n4. 总结 # 本文主要介绍了 histogram 的工作原理以及分位数的计算方法，相信通过本文的抛砖引玉，大家应该对 Prometheus 的 histogram 有了更深一步的了解，下篇文章将会为大家呈现 Summary 的工作方式。\n5. 参考资料 # Prometheus and Histograms ","date":"2019年8月6日","externalUrl":null,"permalink":"/posts/prometheus-histograms/","section":"博客","summary":"Prometheus 中提供了四种指标类型（参考： Prometheus 的指标类型），其中直方图（H","title":"Prometheus Histogram 深入解读","type":"posts"},{"content":"Calico 是一个纯三层的数据中心网络方案，而且无缝集成像 OpenStack 这种 Iaas 云架构，能够提供可控的 VM、容器、裸机之间的 IP 通信。为什么说它是纯三层呢？因为所有的数据包都是通过路由的形式找到对应的主机和容器的，然后通过 BGP 协议来将所有路由同步到所有的机器或数据中心，从而完成整个网络的互联。\n简单来说，Calico 在主机上创建了一堆的 veth pair，其中一端在主机上，另一端在容器的网络命名空间里，然后在容器和主机中分别设置几条路由，来完成网络的互联。\nCalico 网络模型揭秘 # 下面我们通过具体的例子来帮助大家理解 Calico 网络的通信原理。任意选择 k8s 集群中的一个节点作为实验节点，进入容器 A，查看容器 A 的 IP 地址：\n$ ip a 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 3: eth0@if771: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN\u0026gt; mtu 1440 qdisc noqueue state UP link/ether 66:fb:34:db:c9:b4 brd ff:ff:ff:ff:ff:ff inet 172.17.8.2/32 scope global eth0 valid_lft forever preferred_lft forever 这里容器获取的是 /32 位主机地址，表示将容器 A 作为一个单点的局域网。\n瞄一眼容器 A 的默认路由：\n$ ip route default via 169.254.1.1 dev eth0 169.254.1.1 dev eth0 scope link 现在问题来了，从路由表可以知道 169.254.1.1 是容器的默认网关，但却找不到任何一张网卡对应这个 IP 地址，这是个什么鬼？\n莫慌，先回忆一下，当一个数据包的目的地址不是本机时，就会查询路由表，从路由表中查到网关后，它首先会通过 ARP 获得网关的 MAC 地址，然后在发出的网络数据包中将目标 MAC 改为网关的 MAC，而网关的 IP 地址不会出现在任何网络包头中。也就是说，没有人在乎这个 IP 地址究竟是什么，只要能找到对应的 MAC 地址，能响应 ARP 就行了。\n想到这里，我们就可以继续往下进行了，可以通过 ip neigh 命令查看一下本地的 ARP 缓存：\n$ ip neigh 169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee REACHABLE 这个 MAC 地址应该是 Calico 硬塞进去的，而且还能响应 ARP。但它究竟是怎么实现的呢？\n我们先来回想一下正常情况，内核会对外发送 ARP 请求，询问整个二层网络中谁拥有 169.254.1.1 这个 IP 地址，拥有这个 IP 地址的设备会将自己的 MAC\n地址返回给对方。但现在的情况比较尴尬，容器和主机都没有这个 IP 地址，甚至连主机上的端口 calicba2f87f6bb，MAC 地址也是一个无用的 ee:ee:ee:ee:ee:ee。按道理容器和主机网络根本就无法通信才对呀！所以 Calico 是怎么做到的呢？\n这里我就不绕弯子了，实际上 Calico 利用了网卡的代理 ARP 功能。代理 ARP 是 ARP 协议的一个变种，当 ARP 请求目标跨网段时，网关设备收到此 ARP 请求，会用自己的 MAC 地址返回给请求者，这便是代理 ARP（Proxy ARP）。举个例子：\n上面这张图中，电脑发送 ARP 请求服务器 8.8.8.8 的 MAC 地址，路由器（网关）收到这个请求时会进行判断，由于目标 8.8.8.8 不属于本网段（即跨网段），此时便返回自己的接口 MAC 地址给 PC，后续电脑访问服务器时，目标 MAC 直接封装为 MAC254。\n现在我们知道，Calico 本质上还是利用了代理 ARP 撒了一个“善意的谎言”，下面我们来确认一下。\n查看宿主机的网卡信息和路由信息：\n$ ip addr ... 771: calicba2f87f6bb@if4: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1440 qdisc noqueue state UP group default link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 14 inet6 fe80::ecee:eeff:feee:eeee/64 scope link valid_lft forever preferred_lft forever ... $ ip route ... 172.17.8.2 dev calicba2f87f6bb scope link ... 查看是否开启代理 ARP：\n$ cat /proc/sys/net/ipv4/conf/calicba2f87f6bb/proxy_arp 1 如果还不放心，可以通过 tcpdump 抓包验证一下：\n$ tcpdump -i calicba2f87f6bb -e -nn tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on calicba2f87f6bb, link-type EN10MB (Ethernet), capture size 262144 bytes 14:27:13.565539 ee:ee:ee:ee:ee:ee \u0026gt; 0a:58:ac:1c:ce:12, ethertype IPv4 (0x0800), length 4191: 10.96.0.1.443 \u0026gt; 172.17.8.2.36180: Flags [P.], seq 403862039:403866164, ack 2023703985, win 990, options [nop,nop,TS val 331780572 ecr 603755526], length 4125 14:27:13.565613 0a:58:ac:1c:ce:12 \u0026gt; ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 66: 172.17.8.2.36180 \u0026gt; 10.96.0.1.443: Flags [.], ack 4125, win 2465, options [nop,nop,TS val 603758497 ecr 331780572], length 0 总结：\nCalico 通过一个巧妙的方法将 workload 的所有流量引导到一个特殊的网关 169.254.1.1，从而引流到主机的 calixxx 网络设备上，最终将二三层流量全部转换成三层流量来转发。 在主机上通过开启代理 ARP 功能来实现 ARP 应答，使得 ARP 广播被抑制在主机上，抑制了广播风暴，也不会有 ARP 表膨胀的问题。 模拟组网 # 既然我们已经掌握了 Calico 的组网原理，接下来就可以手动模拟验证了。架构如图所示：\n先在 Host0 上执行以下命令：\n$ ip link add veth0 type veth peer name eth0 $ ip netns add ns0 $ ip link set eth0 netns ns0 $ ip netns exec ns0 ip a add 10.20.1.2/24 dev eth0 $ ip netns exec ns0 ip link set eth0 up $ ip netns exec ns0 ip route add 169.254.1.1 dev eth0 scope link $ ip netns exec ns0 ip route add default via 169.254.1.1 dev eth0 $ ip link set veth0 up $ ip route add 10.20.1.2 dev veth0 scope link $ ip route add 10.20.1.3 via 192.168.1.16 dev ens192 $ echo 1 \u0026gt; /proc/sys/net/ipv4/conf/veth0/proxy_arp 在 Host1 上执行以下命令：\n$ ip link add veth0 type veth peer name eth0 $ ip netns add ns1 $ ip link set eth0 netns ns1 $ ip netns exec ns1 ip a add 10.20.1.3/24 dev eth0 $ ip netns exec ns1 ip link set eth0 up $ ip netns exec ns1 ip route add 169.254.1.1 dev eth0 scope link $ ip netns exec ns1 ip route add default via 169.254.1.1 dev eth0 $ ip link set veth0 up $ ip route add 10.20.1.3 dev veth0 scope link $ ip route add 10.20.1.2 via 192.168.1.32 dev ens192 $ echo 1 \u0026gt; /proc/sys/net/ipv4/conf/veth0/proxy_arp 网络连通性测试：\n# Host0 $ ip netns exec ns1 ping 10.20.1.3 PING 10.20.1.3 (10.20.1.3) 56(84) bytes of data. 64 bytes from 10.20.1.3: icmp_seq=1 ttl=62 time=0.303 ms 64 bytes from 10.20.1.3: icmp_seq=2 ttl=62 time=0.334 ms 实验成功！\n具体的转发过程如下：\nns0 网络空间的所有数据包都转发到一个虚拟的 IP 地址 169.254.1.1，发送 ARP 请求。 Host0 的 veth 端收到 ARP 请求时通过开启网卡的代理 ARP 功能直接把自己的 MAC 地址返回给 ns0。 ns0 发送目的地址为 ns1 的 IP 数据包。 因为使用了 169.254.1.1 这样的地址，Host 判断为三层路由转发，查询本地路由 10.20.1.3 via 192.168.1.16 dev ens192 发送给对端 Host1，如果配置了 BGP，这里就会看到 proto 协议为 BIRD。 当 Host1 收到 10.20.1.3 的数据包时，匹配本地的路由表 10.20.1.3 dev veth0 scope link，将数据包转发到对应的 veth0 端，从而到达 ns1。 回程类似 通过这个实验，我们可以很清晰地掌握 Calico 网络的数据转发流程，首先需要给所有的 ns 配置一条特殊的路由，并利用 veth 的代理 ARP 功能让 ns 出来的所有转发都变成三层路由转发，然后再利用主机的路由进行转发。这种方式不仅实现了同主机的二三层转发，也能实现跨主机的转发。\n","date":"2019年7月30日","externalUrl":null,"permalink":"/posts/poke-calicos-lies/","section":"博客","summary":"Calico 是一个纯三层的数据中心网络方案，而且无缝集成像 OpenStack 这种 Iaas 云架","title":"Calico 网络通信原理揭秘","type":"posts"},{"content":"该系列文章总共分为三篇：\nLinux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：CPU Linux Cgroup 入门教程：内存 通过 上篇文章的学习，我们学会了如何查看当前 cgroup 的信息，如何通过操作 /sys/fs/cgroup 目录来动态设置 cgroup，也学会了如何设置 CPU shares 和 CPU quota 来控制 slice 内部以及不同 slice 之间的 CPU 使用时间。本文将把重心转移到内存上，通过具体的示例来演示如何通过 cgroup 来限制内存的使用。\n寻找走失内存 # 上篇文章告诉我们，CPU controller 提供了两种方法来限制 CPU 使用时间，其中 CPUShares 用来设置相对权重，CPUQuota 用来限制 user、service 或 VM 的 CPU 使用时间百分比。例如：如果一个 user 同时设置了 CPUShares 和 CPUQuota，假设 CPUQuota 设置成 50%，那么在该 user 的 CPU 使用量达到 50% 之前，可以一直按照 CPUShares 的设置来使用 CPU。\n对于内存而言，在 CentOS 7 中，systemd 已经帮我们将 memory 绑定到了 /sys/fs/cgroup/memory。systemd 只提供了一个参数 MemoryLimit 来对其进行控制，该参数表示某个 user 或 service 所能使用的物理内存总量。拿之前的用户 tom 举例， 它的 UID 是 1000，可以通过以下命令来设置：\n$ systemctl set-property user-1000.slice MemoryLimit=200M 现在使用用户 tom 登录该系统，通过 stress 命令产生 8 个子进程，每个进程分配 256M 内存：\n$ stress --vm 8 --vm-bytes 256M 按照预想，stress 进程的内存使用量已经超出了限制，此时应该会触发 oom-killer，但实际上进程仍在运行，这是为什么呢？我们来看一下目前占用的内存：\n$ cd /sys/fs/cgroup/memory/user.slice/user-1000.slice $ cat memory.usage_in_bytes 209661952 奇怪，占用的内存还不到 200M，剩下的内存都跑哪去了呢？别慌，你是否还记得 linux 系统中的内存使用除了包括物理内存，还包括交换分区，也就是 swap，我们来看看是不是 swap 搞的鬼。先停止刚刚的 stress 进程，稍等 30 秒，观察一下 swap 空间的占用情况：\n$ free -h total used free shared buff/cache available Mem: 3.7G 180M 3.2G 8.9M 318M 3.3G Swap: 3.9G 512K 3.9G 重新运行 stress 进程：\n$ stress --vm 8 --vm-bytes 256M 查看内存使用情况：\n$ cat memory.usage_in_bytes 209637376 发现内存占用刚好在 200M 以内。再看 swap 空间占用情况：\n$ free total used free shared buff/cache available Mem: 3880876 407464 3145260 9164 328152 3220164 Swap: 4063228 2031360 2031868 和刚刚相比，多了 2031360-512=2030848k，现在基本上可以确定当进程的使用量达到限制时，内核会尝试将物理内存中的数据移动到 swap 空间中，从而让内存分配成功。我们可以精确计算出 tom 用户使用的物理内存+交换空间总量，首先需要分别查看 tom 用户的物理内存和交换空间使用量：\n$ egrep \u0026#34;swap|rss\u0026#34; memory.stat rss 209637376 rss_huge 0 swap 1938804736 total_rss 209637376 total_rss_huge 0 total_swap 1938804736 可以看到物理内存使用量为 209637376 字节，swap 空间使用量为 1938804736 字节，总量为 (209637376+1938804736)/1024/1024=2048 M。而 stress 进程需要的内存总量为 256*8=2048 M，两者相等。\n这个时候如果你每隔几秒就查看一次 memory.failcnt 文件，就会发现这个文件里面的数值一直在增长：\n$ cat memory.failcnt 59390293 从上面的结果可以看出，当物理内存不够时，就会触发 memory.failcnt 里面的数量加 1，但此时进程不一定会被杀死，内核会尽量将物理内存中的数据移动到 swap 空间中。\n关闭 swap # 为了更好地观察 cgroup 对内存的控制，我们可以用户 tom 不使用 swap 空间，实现方法有以下几种：\n将 memory.swappiness 文件的值修改为 0：\n$ echo 0 \u0026gt; /sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.swappiness 这样设置完成之后，即使系统开启了交换空间，当前 cgroup 也不会使用交换空间。\n直接关闭系统的交换空间：\n$ swapoff -a 如果想永久生效，还要注释掉 /etc/fstab 文件中的 swap。\n如果你既不想关闭系统的交换空间，又想让 tom 不使用 swap 空间，上面给出的第一个方法是有问题的：\n你只能在 tom 用户登录的时候修改 memory.swappiness 文件的值，因为如果 tom 用户没有登录，当前的 cgroup 就会消失。 即使你修改了 memory.swappiness 文件的值，也会在重新登录后失效 如果按照常规思路去解决这个问题，可能会非常棘手，我们可以另辟蹊径，从 PAM 入手。\nLinux PAM( Pluggable Authentication Modules) 是一个系统级用户认证框架，PAM 将程序开发与认证方式进行分离，程序在运行时调用附加的“认证”模块完成自己的工作。本地系统管理员通过配置选择要使用哪些认证模块，其中 /etc/pam.d/ 目录专门用于存放 PAM 配置，用于为具体的应用程序设置独立的认证方式。例如，在用户通过 ssh 登录时，将会加载 /etc/pam.d/sshd 里面的策略。\n从 /etc/pam.d/sshd 入手，我们可以先创建一个 shell 脚本：\n$ cat /usr/local/bin/tom-noswap.sh #!/bin/bash if [ $PAM_USER == \u0026#39;tom\u0026#39; ] then echo 0 \u0026gt; /sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.swappiness fi 然后在 /etc/pam.d/sshd 中通过 pam_exec 调用该脚本，在 /etc/pam.d/sshd 的末尾添加一行，内容如下：\n$ session optional pam_exec.so seteuid /usr/local/bin/tom-noswap.sh 现在再使用 tom 用户登录，就会发现 memory.swappiness 的值变成了 0。\n这里需要注意一个前提：至少有一个用户 tom 的登录会话，且通过 systemctl set-property user-1000.slice MemoryLimit=200M 命令设置了 limit，/sys/fs/cgroup/memory/user.slice/user-1000.slice 目录才会存在。所以上面的所有操作，一定要保证至少保留一个用户 tom 的登录会话。 控制内存使用 # 关闭了 swap 之后，我们就可以严格控制进程的内存使用量了。还是使用开头提到的例子，使用用户 tom 登录该系统，先在第一个 shell 窗口运行以下命令：\n$ journalctl -f 打开第二个 shell 窗口（还是 tom 用户），通过 stress 命令产生 8 个子进程，每个进程分配 256M 内存：\n$ stress --vm 8 --vm-bytes 256M stress: info: [30150] dispatching hogs: 0 cpu, 0 io, 8 vm, 0 hdd stress: FAIL: [30150] (415) \u0026lt;-- worker 30152 got signal 9 stress: WARN: [30150] (417) stress: FAIL: [30150] (415) \u0026lt;-- worker 30151 got signal 9 stress: WARN: [30150] (417) now reaping child worker processes stress: FAIL: [30150] (415) \u0026lt;-- worker 30154 got signal 9 stress: WARN: [30150] (417) now reaping child worker processes stress: FAIL: [30150] (415) \u0026lt;-- worker 30157 got signal 9 stress: WARN: [30150] (417) now reaping child worker processes stress: FAIL: [30150] (415) \u0026lt;-- worker 30158 got signal 9 stress: WARN: [30150] (417) now reaping child worker processes stress: FAIL: [30150] (451) failed run completed in 0s 现在可以看到 stress 进程很快被 kill 掉了，回到第一个 shell 窗口，会输出以下信息：\n由此可见 cgroup 对内存的限制奏效了，stress 进程的内存使用量超出了限制，触发了 oom-killer，进而杀死进程。\n更多文档 # 加个小插曲，如果你想获取更多关于 cgroup 的文档，可以通过 yum 安装 kernel-doc 包。安装完成后，你就可以进入 /usr/share/docs 的子目录，查看每个 cgroup controller 的详细文档。\n$ cd /usr/share/doc/kernel-doc-3.10.0/Documentation/cgroups $ ll 总用量 172 4 -r--r--r-- 1 root root 918 6月 14 02:29 00-INDEX 16 -r--r--r-- 1 root root 16355 6月 14 02:29 blkio-controller.txt 28 -r--r--r-- 1 root root 27027 6月 14 02:29 cgroups.txt 4 -r--r--r-- 1 root root 1972 6月 14 02:29 cpuacct.txt 40 -r--r--r-- 1 root root 37225 6月 14 02:29 cpusets.txt 8 -r--r--r-- 1 root root 4370 6月 14 02:29 devices.txt 8 -r--r--r-- 1 root root 4908 6月 14 02:29 freezer-subsystem.txt 4 -r--r--r-- 1 root root 1714 6月 14 02:29 hugetlb.txt 16 -r--r--r-- 1 root root 14124 6月 14 02:29 memcg_test.txt 36 -r--r--r-- 1 root root 36415 6月 14 02:29 memory.txt 4 -r--r--r-- 1 root root 1267 6月 14 02:29 net_cls.txt 4 -r--r--r-- 1 root root 2513 6月 14 02:29 net_prio.txt 下一篇文章将会讨论如何使用 cgroup 来限制 I/O，敬请期待~\n","date":"2019年7月25日","externalUrl":null,"permalink":"/posts/understanding-cgroups-part-3-memory/","section":"博客","summary":"该系列文章总共分为三篇： Linux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：","title":"Linux Cgroup 入门教程：内存","type":"posts"},{"content":"本文首发于：微信公众号「云原生实验室」，公众号ID：cloud_native_yang。\n这是云原生周报第 3 期，主要分享云原生社区最新开源项目和相关资讯。\n开源项目推荐 # diving : 基于 dive 分析 docker 镜像，界面化展示了镜像每层的变动（增加、修改、删除等）、用户层数据大小等信息。便捷获取镜像信息和每层镜像内容的文件树，可以方便地浏览镜像信息。对于需要优化镜像体积时非常方便。\nWave : Kubernetes 的配置文件有两种，一种是 ConfigMap，用来存储明文；另一种是 Secret，用来存储密文。这两种配置文件应用都比较广泛，但遗憾的是，目前它们在大多数场景下都不支持热更新，只有当 ConfigMap 挂载为 Volume 时，才能支持热更新，其他场景均不支持。Wave 的做法比较机智，它向 API server 订阅来自指定的 Deployment（通过 annotations 识别） 的事件，一旦某个 Deployment 被执行了任何操作（Create/Read/Update/Delete），它就会通过算法来计算该 Deployment 中每个挂载的 ConfigMap and Secret 的 hash 值，如果挂载点发生了变化，或者挂载的数据发生了变化，都会改变 hash 值。由于该 hash 值被写到 Pod Template 的 Annotation 中，所以 hash 更新就会触发 Deployment 的滚动更新。\nkube-eventer : Kubernetes 的核心设计思想是状态机。在 Kubernetes 中，事件分为两种，一种是 Warning 事件，表示产生这个事件的状态转换是在非预期的状态之间产生的；另外一种是 Normal 事件，表示期望到达的状态，和目前达到的状态是一致的。通过事件的机制，可以丰富 Kuernetes 在监控方面的维度和准确性，弥补其他监控方案的缺欠。kube-eventer 是为了弥补事件监控场景的缺失，支持将 Kubernetes 事件发送到钉钉机器人、SLS 日志服务、Kafka 开源消息队列、InfluxDB 时序数据库等等。\nKubernetes 修仙路径 : 目前云计算行业对于 Kubernetes 学习的需求日益增加，但市面上关于 Kubernetes 的资源良莠不齐，存在几个问题：\n官方文档缺少明确的\u0026quot;梯度\u0026quot;，信息错综复杂 资料较为分散，查找信息费时费力 Kubernetes 发展很快，书籍或者网上教程容易过时 为了给广大从业者提供一个 Kubernetes 学习路径，为大家提供一定的指引， 才云科技（Caicloud） 推出了 Kubernetes 打怪升级指南，目标是让所有人剥茧抽丝般地了解 Kubernetes，不仅仅知道怎么用 Kubernetes，还知道 Kubernetes 各个功能是如何设计的。在学习路径后期，我们还可以很\u0026quot;自然\u0026quot;的联想到正确的设计思路。\nYugaByte DB : YugaByte DB 是一个高性能、云原生的分布式 SQL 数据库。YugaByte DB 具有基于 Google Spanner 的存储架构和基于 PostgreSQL 的查询层，旨在为现代应用程序在云原生基础架构上提供分布式 SQL 中的体验（类似 Oracle）。完全开源之后，其工程团队将带领 YugaByte DB 比以往更快地向云原生模式发展。\nGetEnvoy Project : 如果你的工作内容涉及到大型分布式系统，那你可能会听说过 Envoy，它是一款为云原生应用而设计、开源的边缘和服务代理，也是 Istio Service Mesh 默认的数据平面。但目前最痛苦的问题是 Envoy 很难编译，为了解决这个问题，Tetrate 的工程师（包括 Envoy 的核心贡献者和维护者）发起了 GetEnvoy 项目，目标是利用一套经过验证的构建工具来构建 Envoy，并通过常用的软件包管理器来分发，包括：apt、yum 和 Homebrew。下图是我通过 Homebrew 安装的 Envoy：\nGRBAC : Grbac 是一个快速，优雅和简洁的 RBAC 框架。它支持增强的通配符并使用 Radix 树匹配 HTTP 请求。令人惊奇的是，您可以在任何现有的数据库和数据结构中轻松使用它。\nccheck : 一个用来验证 Kubernetes 资源配置的命令行工具。它通过使用 reg 查询语言来编写针对 yaml 文件的测试。\nceph-study : Ceph 是一个可靠、自动均衡、自动恢复的分布式存储系统，通常可用于对象存储，块设备存储和文件系统存储。 Ceph 在存储的时候充分利用存储节点的计算能力，在存储每一个数据时都会通过计算得出该数据的位置，尽量的分布均衡。ceph-study 是网友整理的一份 ceph 学习指南，写的十分详细，欢迎初学者浏览学习。\n博客推荐 # 到底要不要把数据库运行在 Kubernetes 中 : 如今越来越多的应用都跑在 Kubernetes 上，Kubernetes 已经成为云时代的 Linux 操作系统。尽管如此，数据库的部署方式并没有因为 Kubernetes 的浪潮而受到太多影响，因为要想容器化，就要考虑数据库能否自动重启、横向扩展，能否适应容器隔离技术的限制。本文将会通过合理的逻辑推理告诉你到底要不要把数据库运行在 Kubernetes。\nKubernetes 中的 Java 应用性能优化 : 在 Kubernetes 中部署应用并没有想象中那么简单，如果配置不恰当，就会遇到频繁的 oom kills 和 重启，尤其是 Java 应用需要特别关注。本文以一个 Spring Boot 微服务应用为例，分析应用启动消耗的 CPU 和内存资源，然后告诉我们如何调整资源的 requests 和 limits 来提高应用的启动速度，并防止因为 OOM 机制被 kill 掉。\nK8S 避坑指南 - Deployment 更新 POD 内容器无法收到 SIGTERM 信号 : 正常情况下，在 Deployment 滚动更新时，当 pod 被 terminate 的时候，应用进程应该能够正确处理 SIGTERM 信号。如果应用不能正确处理 SIGTERM 信号，一般都是因为应用进程不是容器内的 1 号进程，需要调整容器的启动命令来解决问题。\n为容器提供更好的隔离：沙箱容器技术概览 : Docker、LXC 以及 RKT 等传统容器都是共享主机操作系统核心的，因此不能称之为真正的沙箱。这些技术的资源利用率很高，但是受攻击面积和潜在的攻击影响都很大，在多租户的云环境中，不同客户的容器会被同样的进行编排，这种威胁就尤其明显。主机操作系统在为每个容器创建虚拟的用户空间时，不同容器之间的隔离是很薄弱的，这是造成上述问题的根本原因。基于这样的现状，真正的沙箱式容器，成为很多研发工作的焦点。多数方案都对容器之间的边界进行了重新架构，以增强隔离。本文覆盖了四个项目，分别来自于 IBM、Google、Amazon 以及 OpenStack，几个方案的目标是一致的：为容器提供更强的隔离。如果你想抓住即将到来的转型机会，不妨关注一下这些新项目。\n解决在 Kubernetes 中删除 namespace 一直处于 terminating 状态的问题 : 作者在生产环境中删除某个 namespace 时一直处于 terminating 状态，最后发现是因为在 namespace 中通过 Finalizers 调用了 pre-delete hooks，所以一直卡在那边。具体的解决办法请查阅文章。\npapers-notebook : 这是一篇论文阅读笔记，其中的论文一部分来自于在上海交通大学软件学院的研究生课上需要阅读的论文，这部分会比较偏安全和虚拟化。还有一部分论文是作者感兴趣，想去了解的，这部分可能比较偏虚拟化和分布式。论文笔记希望能够记录自己在读论文的时候的想法，其中包括但不限于论文的大致 idea，实现方式，以及自己对论文的评价等等。\n从CNI到OVN : 本文主要介绍了 ovn ovs 怎么与 kubernetes 擦出火花。全文主要分为两个部分，第一部分先简单介绍 CNI 的工作原理，然后开始安装 OVS 和 OVN，并测试跨主机容器的连通性。第二部分主要介绍 Openflow 和 OVN 的工作原理和相关实践。\ngRPC 服务发现与服务治理技术选型 : gRPC 服务发现与服务治理,目前常见解决方案有以下两种：\nNginx + consul + consul-template Envoy 本文粗略讲解了两种方案的优缺点，最后总结相对于 nginx，更倾向于 envoy。首先 envoy 就是为微服务而生的负载匀衡工具，grpc 健康检查是微服务中重要的一环。但是 nginx 拥有活跃的社区，说不定不久将来也会有支持 grpc 健康检查的插件。\n在 Golang 中操作 Istio 和其他 CRD : 本文以 Istio 为例，演示如何使用 Golang 来对 Kubernetes 中的 CRD 资源进行增删改查。\n服务监听 pod id 在 istio 中路由异常分析 : 在 Istio 服务网格中，绝大部分场景下用户服务进程监听的 ip 是 0.0.0.0，这种服务可以透明加入 istio 服务网格，但是如果用户进程监听的本机具体 ip(pod ip)，这种服务就无法直接加入当前 isito 服务网格，因为在 Envoy 的 inbound cluster 配置中，socket_address 被写死了，值为 127.0.0.1。本文描述了如何尝试修复这个问题。\n使用 Kyverno 定义 Kubernetes 策略 : Kubernetes 的日常使用过程中，在对象提交给集群之前，我们会有很多机会，很多方法对资源的 Yaml 定义进行检查和处理。很多读者应该也会知道，资源提交之后，还有机会使用 Admission Controller 对资源动动手脚，这其中其实有很多可以提炼出来的标准动作，可以用统一的控制器来进行处理，Kyverno 就是这样一个工具。有了 Kyverno 的帮助，YAML 程序员可以根据条件对资源进行筛选。\n视频推荐 # 只有大气磅礴的 BGM，才配得上史诗级的《权力的游戏》。So，只有大气磅礴的 BGM，才配得上云原生时代的操作系统。\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 电子书推荐 # CIS Kubernetes Benchmark : 该文档提供了一份为 Kubernetes 1.13 创建安全配置的说明指南，主要用来帮助应用管理员、安全专家和平台部署人员规划在 Kubernetes 平台上开发部署应用的解决方案。\n获取方式：公众号后台回复：kubernetes benchmark\n福利篇 # ENFI下载器 : 这可能是最骚的百度网盘不限速下载器，不仅能为你提供高速下载，还能同时让你赚取收入，支持 Windows 和 MacOS 哦。来看看我赚的钱：\n下载速度基本满速，具体取决于你的带宽：\n测试链接：https://pan.baidu.com/s/1JlsJsTN0JpwzA3DUzvyeIA 提取码: 7uak\nPandownload 网页版 : 这可能是最没有存在感的百度网盘不限速下载工具。用过 Pandownload 的同学都知道，这是一款老牌的百度网盘第三方下载器。但是它只有 Windows 版本的客户端，macOS 用户只能无奈地摇摇头。最近 Pandownload 推出了网页版本，前段时间测试了一下确实好用，只需输入百度网盘下载链接和提取码，即可高速下载，亲测可以跑满带宽。相比之下，比下载各种客户端算是解决了 Mac 用户不支持的福利。\n还嫌不够方便？没关系，热心网友开发了一款油猴脚本，可以将百度网盘分享链接自动跳转到 PanDownload 网页版去下载。脚本地址： 百度网盘不限速直链下载\nbaidu-netdisk-downloaderx : 另一款图形界面的百度网盘不限速下载器，支持 Windows、Linux 和 Mac。又是 Golang 写的，不多介绍了，自己看吧。\n","date":"2019年7月21日","externalUrl":null,"permalink":"/posts/cloud-native-weekly-3/","section":"博客","summary":"本文首发于：微信公众号「云原生实验室」，公众号ID：clou","title":"云原生周报：第 3 期","type":"posts"},{"content":"2019 年 6 月 20 日，Kubernetes 重磅发布了 1.15 版本，不过笔者忙到现在才有空认真来看一下到底更新了哪些东西。这一版本更新主要是针对稳定性的持续改善和可扩展性，仔细把 25 个新增或改动的功能看完后，发现许多以前的小痛点都在这个版本中解决了，本文对每个特性的介绍格式如下：\n#492 : 前面是 GitHub issue 编号，后面是具体的特性\n进度 : 表示该特性目前处于什么阶段，如 Alpha，Beta，Stable\n特性分类 : 表示该特性属于哪个分类，如 API，CLI，Network 等。\n这里是具体的特性介绍，例如改进了什么，为什么要这么做，有的特性还会有简单的使用范例。\n亮点更新 # CustomResourceDefinitions 的改良 Event API 的重新设计 Execution hooks 的推出 新的 Scheduling Framework 核心功能 # #1024 NodeLocal DNSCache # 进度：迈向 Beta\n特性分类：Network\nNodeLocal DNSCache 通过在集群节点上以 Deamonset 的方式运行 DNS 缓存代理来提高集群的 DNS 性能，从而可以避免使用 iptables DNAT 规则和连接跟踪。如果本地 DNS 缓存代理在内存中找不到相应的 DNS 记录，就会向 kube-dns 服务发起查询请求（默认情况下以 cluster.local 为后缀）。\n想了解该特性的更多细节可以阅读 Kubernetes Enhancement Proposal (KEP) 文档中的设计说明。\n#383 Redesign event API # 进度：Alpha\n特性分类：Scalability\n这项工作主要有两个目标：\n减少 Events 对集群其余部分的性能影响； 向 Event 对象添加更多的数据结构，这是使 Event 分析自动化的必要步骤，也是第一步。 目前 Event API 的主要问题是包含太多垃圾信息，导致其难以摄取和分析有效信息。除此之外还有数个性能问题，例如当集群出现问题时，Events 可能会使 API server 过载（例如常见的 crashloop）\n关于该 issue 的讨论以及建议的解决方案和改进工作可以参考这里的 设计提案。\n#492 Admission webhook # 进度：Beta\n特性分类：API\nMutating 和 Validating Admission Webhook 已经成为扩展 API 的主流选择。在 1.15 以前，所有的 webhook 只会按照字母表顺序调用一次，这样就会导致一个问题：一个更早的 webhook 不能应对后面的 webhook 的更新，这可能会导致未知的问题，例如前面的 webhook 设置某个 pod 的启动参数，而随后的 webhook 将其更改或者移除了。\n在 Kubernetes 1.15 中，允许 webhook 被重复调用，即使是对同一对象的修改。如果想启用该特性，必须要确保你引入的任何 admission webhook 都是幂等操作，也就是说，同一个对象被执行任意多次操作与执行一次操作产生的效果相同。\n#624 Scheduling framework # 进度：Alpha\n特性分类：Scheduling\n该特性为 Kubernetes 1.15 的调度器设计了一个新的可插拔结构，主要是为了解决日益增加的定制化调度需求。Scheduler Framework 在原有的 Priority/Predicates 接口的基础上增加了 reserve, pre-bind 等十几个接口。\n下图显示了 Pod 在新的 Scheduling framework 中的调度过程：\n关于该特性的更多信息请查阅 官方设计文档。\n#606 Support 3rd party device monitoring plugins # 进度：迈向 Beta\n特性分类：Node\n该特性允许 Kubelet 将容器 binding 信息暴露给第三方监控插件，这样系统管理员就可以使用第三方的设备监控代理来监控自定义资源分配给 Pod 的使用率（例如，每个 Pod 的 GPU 使用率）。\n未解耦前，Kubelet 会检测所有支持的设备是否存在，即使节点并没有安装该设备。\n使用新的框架之后，Kubelet 会通过 /var/lib/kubelet/pod-resources/kubelet.sock 提供一个新的 GRPC 服务，该服务会把容器和设备所分配到资源相关的信息都暴露出来。\n#757 Pid limiting # 进度：迈向 Beta\n特性分类：Node\nPid 是 Linux 系统中很重要的资源，系统很容易在 CPU 或内存的使用量还没达到上限之前，进程数量就达到了上限。因此管理员必须得想办法确保 Pod 不会把系统的 Pid 用完，进而造成其他重要的服务无法运行（例如，container runtime，kubelet 等）。\n新的特性允许修改 Kubelet 配置来限制每个 Pod 的 Pid 数量。在 Node 层面限制 Pid 的功能现在可以直接使用，不再需要通过 feature gate 的参数 SupportNodePidsLimit=true 显示设置。\nKubernetes 官方博客有对此特性的详细介绍。\n#902 Add non-preempting option to PriorityClasses # 进度：Alpha\n特性分类：Scheduling\nKubernetes 1.15 在 PriorityClass 中添加 PreemptionPolicy 字段作为 Alpha 特性。\nPreemptionPolicy 字段的默认值为 PreemptLowerPriority，表示允许该优先级的 Pod 抢占低优先级的 Pod（这是默认的抢占行为）。如果 PreemptionPolicy 字段的值为 Never，则该 Pod 会被放置到调度队列中，并且放置的位置排在低优先级 Pod 的前面，但是不能抢占其他的 Pod。\n以数据科学领域为例：用户提交了一个 job，他希望此 job 的优先级比其他 job 高，但是不希望因为抢占 Pod 而导致目前的任务被搁置。\n#917 Add go module support to k8s.io/kubernetes # 进度：Stable\n特性分类：Architecture\n自从 Kubernetes 开源以来，一直使用 godep 来 vendoring 所有依赖库。随着 Go 生态系统越来越成熟，vendoring 已经变成主流，而 godep 已经不再维护了，于是 Kubernetes 一开始使用 godep 的定制版，这期间还有一些其他的 vendoring 工具（例如 glide 和 dep）也跟着出现，而现在 Go 的依赖库管理终于可以以 go module 的形式直接添加到 Go 中。\nGo 从 1.13 已经默认开启 go module，并且移除了 $GOPATH 模式。为了支持这个改动，Kubernetes 1.15 版本调整了好几个组件的代码以使用 go module。\n#956 Add Watch bookmarks support # 进度：Alpha\n特性分类：API\n一个 Kubernetes 集群只会保留一段时间内的变更历史记录，比如使用 etcd3 的集群默认会保留 5 分钟的变更历史记录。而为 Kubernetes Watch 事件添加一个书签（bookmark）可以想象成多了一个检测点，所有 Client 请求的对象如果符合预先想查找的资源版本（resourceVersion）就会被这个书签给筛选出来。\n例如：新增一个 Watch 的请求去查找所有资源版本为 X 的事件，这时 API server 知道该 Watch 请求对其他资源版本的事件没有兴趣，就会使用书签来略过所有其他事件，只将特定的事件发送给客户端，从而避免增加 API server 的负载。\n#962 Execution hooks # 进度：Alpha\n特性分类：storage\nExecutionHook 提供了一种通用机制，让用户可以在容器中触发想要执行的 hook 命令，例如：\n应用程序备份 升级 数据库迁移 重新加载配置文件 重启容器 hook 的定义中包含两条重要信息：\n需要执行什么命令 在哪执行命令（通过 Pod Selector） 下面提供一个简单示例：\napiVersion: apps.k8s.io/v1alpha1 kind: HookAction metadata: name: action-demo Action: exec: command: [\u0026#34;run_quiesce.sh\u0026#34;] actionTimeoutSeconds: 10 想了解该特性的更多细节可以阅读 Kubernetes Enhancement Proposal (KEP) 文档中的设计说明。\n#981 PDB support for custom resources with scale subresource # 进度：迈向 Beta\n特性分类：Apps\nPod Disruption Budget (PDB) 是一种 Kubernetes API，用于限制在同一时间自愿中断的应用程序（如 Deployment 或 ReplicaSet）中宕机的 Pod 的数量。PDB 可以通过指定最小可用数量或最大不可用数量的 Pod 来自定义中断预算。\n例如，对于一个无状态的前端应用：\n要求：服务能力不能减少超过 10% 解决方案：使用一个包含 minAvailable 90% 值的 PDB 使用 PDB 后，就可以允许管理员在不降低服务的可用性和性能的前提下操作 Kubernetes 的工作负载。\n自定义资源 # #95 CustomResourceDefinitions # 进度：Beta\n特性分类：API\n该特性没有什么实质性的功能，只是把在 Kubernetes 1.15 版本中跟 CRD 相关的修复和改进进行了分组：\nStructural schema using OpenAPI CRD pruning CRD defaulting Webhook conversion moved to beta Publishing the CRD OpenAPI schema #692 Publish CRD OpenAPI schema # 进度：迈向 Beta\n特性分类：API\n该特性允许开发者使用 OpenAPI v3 schema 来定义 Custom Resource Definition (CRD) ，以便开启 Server 端对 CustomResources (CR) 的验证。\n发布使用 OpenAPI 规范的 CRD 便可以开启客户端验证（例如 kubectl create 和 kubectl apply 时），也可以对规范进行描述（例如 kubectl explain），Client 也会因为 CRs 而自动生成，所以开发者可以轻易使用任何支持的编程语言来和 API 进行交互。\n使用 OpenAPI 规范有助于使 CRD 开发者和 Kubernetes API 的发展方向更加清晰，文档格式更加简洁精炼。\n#575 Defaulting and pruning for custom resources # 进度：Alpha\n特性分类：API\n下面的这两个特性主要是为了使与 CRD 相关的 JSON 处理更加方便。\nPruning : CRD 传统的存储方式是以 JSON 格式存储在 ETCD 中。现在如果它是以 OpenAPI v3 的规范来定义的，并且 preserveUnknownFields 的值为 false，未被定义的字段在创建或更新时便会被删除。\npreserveUnknownFields: false validation: openAPIV3Schema: type: object Defaulting : 此特性在 Kubernetes 1.15 版本处于 Alpha 阶段，默认处于关闭状态，可以通过 feature gate 的参数 CustomResourceDefaulting 来开启。Defaulting 和 Pruning 一样，在一开始就要将规范定好，不符合规范的就会被去掉。\nspec: type: object properties: cronSpec: type: string pattern: \u0026#39;^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$\u0026#39; default: \u0026#34;5 0 * * *\u0026#34; #598 Webhook conversion for custom resources # 进度：迈向 Beta\n特性分类：API\n不同的 CRD 版本可以有不同的规范，现在你可以在操作中处理不同版本之间的转换，并且实现了版本转换的 webhook。这个 webhook 会在下面几种情况下被调用：\n请求的自定义资源版本与原来储存的版本不一致 自定义资源在 Watch 时创建了某一版本，但在下次修改时发现跟存储的版本不一致 使用 PUT 请求自定义资源时，发现请求的版本与存储的版本不一致 这里有一个实现 自定义资源之间相互转换的 webhook server 的示例，大家可以作为参考。\n配置管理 # #515 Kubectl get and describe should work well with extensions # 进度：迈向 Stable\n特性分类：Cli\n目前已经可以使用 kubectl get 和 describe 来让第三方 API 扩展和 CRD 提供自定义格式化输出。该特性使输出可以打印到服务器端，从而实现了更好的扩展性，并且让 Kubectl 和扩展的实现细节进行解耦。\n想了解关于该特性的更多详细信息，可以查阅相关 设计文档。\n#970 Kubeadm: New v1beta2 config format # 进度：迈向 Beta\n特性分类：Cluster lifecycle\n随着时间的推移，在 kubeadm 的配置文件中配置 Kubernetes 集群创建时的选项数量已经大大增加，然后 CLI 参数的数量还是没有变化，所以导致使用配置文件来创建集群是目前唯一一个比较符合使用者需求方法。\n该特性的目标是重新设计配置的存储方式来改善当前版本遇到的问题，并放弃了使用包含所有选项的单个配置文件，使用子结构来为高可用集群提供更好的支持。\n#357 Ability to create dynamic HA clusters with kubeadm # 进度：迈向 Beta\n特性分类：Cluster lifecycle\nKubernetes 可以通过多个控制平面来提供高可用性。kubeadm 工具现在可以用来创建高可用集群，有两种方式：\netcd 与 Control Plane 节点 (Master) 共存 etcd 与 Control Plane 节点 (Master) 是分开的 这个版本的 kubeadm 将会自动复制其中需要的证书，减少人为干预的需求，目前的做法是使用一个暂时加密的秘钥来确保证书在传输过程中的安全性，更多细节可以参考 KEP 文档。\n云提供商 # #423 Support AWS network load balancer # 进度：迈向 Beta\n特性分类：AWS\n在 Kubernetes 1.15 中可以通过 annotations 的方式，在 Service 的种类是 LoadBalancer 时，直接请求建立 AWS NLB：\nmetadata: name: my-service annotations: service.beta.kubernetes.io/aws-load-balancer-type: \u0026#34;nlb\u0026#34; 与经典的弹性负载均衡器不同，Network Load Balancers (NLBs) 会把客户端的 IP 直接传递给节点。AWS NLB 其实从 1.9 的时候就已经处于 Alpha 阶段，现在代码和 API 都已经相对稳定，所以准备迁移到 Beta 阶段。\n#980 Finalizer protection for service LoadBalancers # 进度：Alpha\n特性分类：Network\n默认情况下，云服务商提供的 Load Balancer 资源，应该要在 Kubernetes Service 被删除的时候也跟着一起被删除才对，然而在各种极端的案例中，可以发现在删除关联的 Kubernetes Service 后，Load Balancer 资源却被孤立在一旁没有被清除掉，而引入 Finalizer 就是为了预防这种情况的发生。\n如果你的集群已经开启了和云服务商的整合，Finalizer 将会附加到任何包含 type=LoadBalancer 字段的 Kubernetes Service，当这类 Service 即将被删除时，Finalizer 会先将 Serivce 的删除动作给冻结住，直接确保 Load Balancer 资源被清除后，才会将 Service 真正删除。\n存储 # #625 In-tree storage plugin to CSI Driver Migration # 进度：Alpha\n特性分类：Storage\n存储插件最初都在 Kubernetes 的基础代码库中，增加了代码维护的复杂性，也阻碍了其扩展性。因此该特性的目标是将所有存储相关的代码移出来变成可加装的插件形式，并通过 Container Storage Interface（CSI）来和 Kubernetes 进行交互。如此一来便可降低开发的成本，并使其更加模块化，可扩展性更强，让不同版本的存储插件与 Kubernetes 之间有更好的兼容性。想了解该特性的最新进展可以参考 这里。\n#989 Extend allowed PVC DataSources # 进度：Alpha\n特性分类：Storage\n该特性可以让使用者复制现有的 PV。复制和备份其实还是不太一样的，复制会产生一个新的且内容和原来完全一样的存储卷。复制既有的 PV 会消耗用户的存储卷配额，并且会遵循和其他存储卷创建时一样的创建和检查流程，复制出来的 PV 也和普通的 PV 一样具有相同的生命周期和工作流程。使用该特性时，需要注意以下事项：\n复制功能的 VolumePVCDataSource 参数仅适用于 CSI Driver。 复制功能仅适用于动态存储卷配置。 到底能不能使用复制功能还要取决于 CSI Driver 有没有实现存储卷的复制功能。 #1029 Quotas for ephemeral storage # 进度：Alpha\n特性分类：Node\n目前限制临时存储卷使用量的机制是定期遍历查看每个临时存储卷的大小，这种做法很慢，具有很高的延迟。该特性中提出的机制利用文件系统的 Project Quota 来监控资源消耗程度，然后再决定要不要限制其使用量。希望能够实现以下目标：\n通过以非强制方式使用 Project Quota 来收集有关临时卷的使用情况，进而改善监控的性能。 检测出在 Pod 中已经被删除掉，但是因为文件还处于打开的状态下而被隐藏起来的存储卷。 如此一来便可以通过 Project Quota 来限制每一个存储卷的使用量。\n#531 Add support for online resizing of PVs # 进度：迈向 Beta\n特性分类：Storage\n该特性让使用者可以通过修改 PVC 来在线扩展存储卷使用到的文件系统，而不需要重启使用到该存储卷的 PVC。在线扩展 PVC 的功能目前还处于 Beta 阶段，且默认是开启的，你也可以通过 feature gate 参数 ExpandInUsePersistentVolumes 显示开启。\n文件系统的扩展行为会在以下情况下被触发：\n当 Pod 启动时 当 Pod 正在运行且底层的文件系统支持在线扩展（例如，XFS，ext3 或 ext4） 关于该特性的更多消息信息请参考 Kubernetes 官方文档。\n#559 Provide environment variables expansion in sub path mount # 进度：迈向 Beta\n特性分类：Storage\n目前 Kubernetes 对于挂载节点本地存储卷的支持有一个限制：如果有大于等于两个 Pod 运行在同一个节点上，同时把相同的 log 文件名称写入相同的存储卷会导致这些 Pod 发生冲突。\n使用 subPath 是个不错的选择，但 subPath 目前只能写死，无法提供灵活性。之前的解决办法是创建一个带有挂载路径的软链接的 Sidecar 容器。\n为了更方便地解决这个问题，现在提出了向 subPath 中添加环境变量来缓和这个限制，参考以下示例：\nenv: - name: POD_NAME valueFrom: fieldRef: apiVersion: v1 ieldPath: metadata.name ... volumeMounts: - name: workdir1 mountPath: /logs subPath: $(POD_NAME) 也可以写成这种格式：\nvolumeMounts: - name: workdir1 mountPath: /logs subPathExpr: $(POD_NAME) 总结 # 本文除了告知读者 Kubernetes 1.15 有什么新特性之外，更重要的在于提供了一个机会去了解 Kubernetes 这么庞大的系统在跟第三方整合或是某个组件的性能遇到瓶颈时该怎么解决，为我们以后设计架构时提供了参考依据。\n参考资料 # https://sysdig.com/blog/whats-new-kubernetes-1-15/ ","date":"2019年7月18日","externalUrl":null,"permalink":"/posts/whats-new-kubernetes-1-15/","section":"博客","summary":"2019 年 6 月 20 日，Kubernetes 重磅发布了 1.15 版本，不过笔者","title":"Kubernetes 1.15 详细介绍","type":"posts"},{"content":" 原文链接： Intro Guide to Dockerfile Best Practices\n如今 GitHub 仓库中已经包含了成千上万的 Dockerfile，但并不是所有的 Dockerfile 都是高效的。本文将从五个方面来介绍 Dockerfile 的最佳实践，以此来帮助大家编写更优雅的 Dockerfile。如果你是 Docker 的初学者，恭喜你，这篇文章就是为你准备的。后面的系列将会更加深入，敬请期待！\n本文使用一个基于 Maven 的 Java 项目作为示例，然后不断改进 Dockerfile 的写法，直到最后写出一个最优雅的 Dockerfile。中间的所有步骤都是为了说明某一方面的最佳实践。 减少构建时间 # 一个开发周期包括构建 Docker 镜像，更改代码，然后重新构建 Docker 镜像。在构建镜像的过程中，如果能够利用缓存，可以减少不必要的重复构建步骤。\n构建顺序影响缓存的利用率 # 镜像的构建顺序很重要，当你向 Dockerfile 中添加文件，或者修改其中的某一行时，那一部分的缓存就会失效，该缓存的后续步骤都会中断，需要重新构建。所以优化缓存的最佳方法是把不需要经常更改的行放到最前面，更改最频繁的行放到最后面。\n只拷贝需要的文件，防止缓存溢出 # 当拷贝文件到镜像中时，尽量只拷贝需要的文件，切忌使用 COPY . 指令拷贝整个目录。如果被拷贝的文件内容发生了更改，缓存就会被破坏。在上面的示例中，镜像中只需要构建好的 jar 包，因此只需要拷贝这个文件就行了，这样即使其他不相关的文件发生了更改也不会影响缓存。\n最小化可缓存的执行层 # 每一个 RUN 指令都会被看作是可缓存的执行单元。太多的 RUN 指令会增加镜像的层数，增大镜像体积，而将所有的命令都放到同一个 RUN 指令中又会破坏缓存，从而延缓开发周期。当使用包管理器安装软件时，一般都会先更新软件索引信息，然后再安装软件。推荐将更新索引和安装软件放在同一个 RUN 指令中，这样可以形成一个可缓存的执行单元，否则你可能会安装旧的软件包。\n减小镜像体积 # 镜像的体积很重要，因为镜像越小，部署的速度更快，攻击范围越小。\n删除不必要依赖 # 删除不必要的依赖，不要安装调试工具。如果实在需要调试工具，可以在容器运行之后再安装。某些包管理工具（如 apt）除了安装用户指定的包之外，还会安装推荐的包，这会无缘无故增加镜像的体积。apt 可以通过添加参数 -–no-install-recommends 来确保不会安装不需要的依赖项。如果确实需要某些依赖项，请在后面手动添加。\n删除包管理工具的缓存 # 包管理工具会维护自己的缓存，这些缓存会保留在镜像文件中，推荐的处理方法是在每一个 RUN 指令的末尾删除缓存。如果你在下一条指令中删除缓存，不会减小镜像的体积。\n当然了，还有其他更高级的方法可以用来减小镜像体积，如下文将会介绍的多阶段构建。接下来我们将探讨如何优化 Dockerfile 的可维护性、安全性和可重复性。\n可维护性 # 尽量使用官方镜像 # 使用官方镜像可以节省大量的维护时间，因为官方镜像的所有安装步骤都使用了最佳实践。如果你有多个项目，可以共享这些镜像层，因为他们都可以使用相同的基础镜像。\n使用更具体的标签 # 基础镜像尽量不要使用 latest 标签。虽然这很方便，但随着时间的推移，latest 镜像可能会发生重大变化。因此在 Dockerfile 中最好指定基础镜像的具体标签。我们使用 openjdk 作为示例，指定标签为 8。其他更多标签请查看 官方仓库。\n使用体积最小的基础镜像 # 基础镜像的标签风格不同，镜像体积就会不同。slim 风格的镜像是基于 Debian 发行版制作的，而 alpine 风格的镜像是基于体积更小的 Alpine Linux 发行版制作的。其中一个明显的区别是：Debian 使用的是 GNU 项目所实现的 C 语言标准库，而 Alpine 使用的是 Musl C 标准库，它被设计用来替代 GNU C 标准库（glibc）的替代品，用于嵌入式操作系统和移动设备。因此使用 Alpine 在某些情况下会遇到兼容性问题。 以 openjdk 为例，jre 风格的镜像只包含 Java 运行时，不包含 SDK，这么做也可以大大减少镜像体积。\n重复利用 # 到目前为止，我们一直都在假设你的 jar 包是在主机上构建的，这还不是理想方案，因为没有充分利用容器提供的一致性环境。例如，如果你的 Java 应用依赖于某一个特定的操作系统的库，就可能会出现问题，因为环境不一致（具体取决于构建 jar 包的机器）。\n在一致的环境中从源代码构建 # 源代码是你构建 Docker 镜像的最终来源，Dockerfile 里面只提供了构建步骤。\n首先应该确定构建应用所需的所有依赖，本文的示例 Java 应用很简单，只需要 Maven 和 JDK，所以基础镜像应该选择官方的体积最小的 maven 镜像，该镜像也包含了 JDK。如果你需要安装更多依赖，可以在 RUN 指令中添加。pom.xml 文件和 src 文件夹需要被复制到镜像中，因为最后执行 mvn package 命令（-e 参数用来显示错误，-B 参数表示以非交互式的“批处理”模式运行）打包的时候会用到这些依赖文件。\n虽然现在我们解决了环境不一致的问题，但还有另外一个问题 :** 每次代码更改之后，都要重新获取一遍 pom.xml 中描述的所有依赖项。**下面我们来解决这个问题。\n在单独的步骤中获取依赖项 # 结合前面提到的缓存机制，我们可以让获取依赖项这一步变成可缓存单元，只要 pom.xml 文件的内容没有变化，无论代码如何更改，都不会破坏这一层的缓存。上图中两个 COPY 指令中间的 RUN 指令用来告诉 Maven 只获取依赖项。\n现在又遇到了一个新问题：跟之前直接拷贝 jar 包相比，镜像体积变得更大了，因为它包含了很多运行应用时不需要的构建依赖项。\n使用多阶段构建来删除构建时的依赖项 # 多阶段构建可以由多个 FROM 指令识别，每一个 FROM 语句表示一个新的构建阶段，阶段名称可以用 AS 参数指定。本例中指定第一阶段的名称为 builder，它可以被第二阶段直接引用。两个阶段环境一致，并且第一阶段包含所有构建依赖项。\n第二阶段是构建最终镜像的最后阶段，它将包括应用运行时的所有必要条件，本例是基于 Alpine 的最小 JRE 镜像。上一个构建阶段虽然会有大量的缓存，但不会出现在第二阶段中。为了将构建好的 jar 包添加到最终的镜像中，可以使用 COPY --from=STAGE_NAME 指令，其中 STAGE_NAME 是上一构建阶段的名称。\n多阶段构建是删除构建依赖的首选方案。\n本文从在非一致性环境中构建体积较大的镜像开始优化，一直优化到在一致性环境中构建最小镜像，同时充分利用了缓存机制。下一篇文章将会介绍多阶段构建的更多其他用途。\n","date":"2019年7月9日","externalUrl":null,"permalink":"/posts/intro-guide-to-dockerfile-best-practices/","section":"博客","summary":"原文链接： Intro Guide to Dockerfile Best Practices 如今 GitHub 仓库中已经包含了成千上万的 Doc","title":"Dockerfile 编写指南","type":"posts"},{"content":"这是云原生周报第 2 期，主要分享云原生社区最新开源项目，优秀博客、电子书和视频。\n开源项目推荐 # Kube Forwarder : Kubernetes 端口转发的 GUI 客户端，支持多集群，断开后可自动重连（kubectl 可做不到这一点哦），可对多个 Service 同时进行端口转发。\nKube eagle : 这是一个 Prometheus Exporter，用来更精确地抓取 Kubernetes 集群中 Pod 资源的 requests、limits 和实际使用量。\nKube-hunter : Kubernetes 集群渗透测试工具，从事安全工作的相关人员可以关注一下。\nko : 用来在 Kubernetes 中构建并部署 golang 应用的工具。它的使用方法非常简单，如果你想构建一个 golang 应用，并把它部署到 Kubernetes 集群中，只需要编写一个如下的 YAML 文件：\n# helloworld.yaml apiVersion: apps/v1beta1 kind: Deployment metadata: name: hello-world spec: selector: matchLabels: foo: bar replicas: 1 template: metadata: labels: foo: bar spec: containers: - name: hello-world # 将 image 的值换成 golang 的项目路径 # 比如如果你的项目路径为 ~/gopath/src/github.com/mattmoor/examples # 那么 image 的值为 github.com/mattmoor/examples image: github.com/mattmoor/examples/http/cmd/helloworld ports: - containerPort: 8080 然后使用命令 ko apply -f helloworld.yaml 即可自动编译成二进制文件、构建镜像然后部署到集群中，一步到位！\nCluster version of VictoriaMetrics : VictoriaMetrics 是 Prometheus 支持的远程存储，而集群版 VictoriaMetrics 用来实现大规模 Prometheus 集群的高可用，并提供了全局视图和可靠的历史数据存储，与 Thanos 的功能类似，但比 Thanos 的架构更简单，值得一试！\nService Mesh Hub : solo.io 开源的 Service Mesh 仓库，提供了一个 Dashboard 用来发现和部署不同类型的 Service Mesh，也可以管理每个 Service Mesh 的扩展。\n这是仓库里包含的所有扩展：\nKubernetes Standardized Glossary : 这是 Kubernetes 官方文档新出的标准术语表，对每种资源类型和组件都有标准化的解释。\nnetramesh : 这是一个轻量级的 Service Mesh 框架。你没有听错，这是一个全新的 Service Mesh 框架。据官方文档所述，它比 Istio 和 Linkerd2 的资源消耗更少，性能更高，每个 Sidecar 大约消耗 10-50Mb 的内存和 1ms 的延迟开销。这是它的架构图：\nKubeOne : Golang 编写的 Kubernetes 高可用集群部署工具，底层使用的是 kubeadm。\ningress-nginx kubectl plugin : NGINX Ingress Controller 的 kubectl 插件，可用来方便快速地调试 ingress。通过该插件，你可以直接查看某个 ingress 资源后端有哪些 endpoint，直接导出某个域名的证书和秘钥，也可以导出 Nginx 的配置文件，非常实用。\nSinger : Printerest 开源的高性能可扩展日志收集 agent，可对接 Kafaka。\n博客推荐 # Multi-Container Pods in Kubernetes : 在 Kubernetes 中，Pod 是最小的调度单元，Pod 中可以只运行一个容器，也可以运行多个容器。本文主要讨论了在什么场景下需要在一个 Pod 中运行多个容器，主要包括三种需求：共享存储、进程间通信、共享网络。\n云原生架构的五大原则 : 这是一篇 Google Cloud 的官方博客，描述了云原生架构应该遵循的五个准则。\n使用 nftables 实现 API Server 的高可用 : 这篇文章比较有意思，详细描述了如何用 nftables 来实现 API Server 的高可用，后面还提到了如何用 nftables 来实现 kube-proxy 的四层负载均衡功能。\npodpreset批量添加时区配置 : 使用 Docker 镜像来部署应用时，大家都会遇到一个让人头疼的问题，那就是时区不一致。为了解决这个问题，也涌现出了各种各样的方法，例如改 Dockerfile，将宿主机的 /etc/localtime 挂载到容器中等。本文给出了一种一劳永逸的巧妙方法，大家可以尝试一下。\n容器环境中的应用弹性能力 : 本文介绍了如何在容器环境中提高应用的弹性能力和可用性。\n弹性能力设计模式：重试，回退，超时，断路器 : 本文主要讨论了松耦合、隔离和延迟控制是如何对系统的弹性能力产生积极的影响。其中重试模式可以通过多次尝试来恢复通信，回退模式可以在本地解决通信故障，断路器可以抵挡由于重试而导致的 DoS 攻击以及当持续出现通信错误时可以快速回退。\n明智的微服务之路 : 过去几年中，越来越多的创业公司转向了微服务架构，DevOps 相关招聘需求暴增，容器文化盛行。这篇文章试图解释这一切背后的原因，先列出了微服务架构的痛点，增加了系统的各种复杂度，最后告诉我们即使微服务架构增加了各种复杂度，你仍然可以转向微服务架构的原因。\nmacvtap实践教程 : macvtap 是网络虚拟化常用的一种技术，基于传统的 MACVLAN。它可以用来简化虚拟化环境中的交换网络，代替传统的 Linux TAP 设备加 Bridge 设备组合。kata 的虚拟化网络就用了这个技术，通过本文的实践可以帮助你理解 kata 的网络原理。\n解决 CoreDNS 缓存不一致而导致的域名解析问题 : 如果你在 CoreDNS 中启用了 cache 和 autopath 插件，并且 CoreDNS 版本低于 1.5.1，就会遇到缓存不一致的问题。本文作者是该 bug 的修复者，他会带领我们一步一步进行调查，最后找到问题所在。\n视频推荐 # Envoy SDS：增强 Istio 的安全性 : Istio 1.1 之前，Istio 为工作负载提供的密钥和证书是由 Citadel 生成并使用加载 Secret 卷的方式分发给 Sidecar 的，这种方式有很多缺陷，比如证书轮换造成的性能损失和安全漏洞。在 Istio 1.1 中，可以使用 SDS 来解决这些问题，它的主要工作原理如下：\n工作负载的 Sidecar 从 Citadel 代理中请求密钥和证书：Citadel 代理是一个 SDS 服务器，这一代理以 DaemonSet 的形式在每个节点上运行，在这一请求中，Envoy 把 Kubernetes service account 的 JWT 传递给 Citadel 代理。 Citadel 代理生成密钥对，并向 Citadel 发送 CSR 请求： Citadel 校验 JWT，并给 Citadel 代理签发证书。 Citadel 代理把密钥和证书返回给工作负载的 Sidecar。 本视频主要演示了 SDS 是如何高效地进行证书轮换，以及 Citadel 是如何独立于其他 Istio 组件工作的。\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 使用 Envoy，Cilium 和 BPF 进行透明混沌测试 : 混沌测试主要用来在分布式系统上做对照实验，引入混沌：服务器崩溃、硬盘异常、网络连接中断等，从而帮助建立对系统承受不可避免的故障的能力的信心。目前大部分的混沌测试都是手动完成的，本视频演示了如何将 Envoy 和 Cilium、BPF 结合使用，以完全透明的方式将服务不可用性、延迟和随机限速等混乱引入 Kubernetes 集群。\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 ","date":"2019年7月5日","externalUrl":null,"permalink":"/posts/cloud-native-weekly-2/","section":"博客","summary":"这是云原生周报第 2 期，主要分享云原生社区最新开源项目，优秀博","title":"云原生周报第 2 期","type":"posts"},{"content":" 前言 # 云原生不但可以很好的支持互联网应用，也在深刻影响着新的计算架构、新的智能数据应用。以容器、服务网格、微服务、Serverless 为代表的云原生技术，带来一种全新的方式来构建应用。笔者是一名云原生狂热信徒，长期以来我都不知道该怎么整理自己的收藏夹。最近想到，为了让大家能够掌握云原生最新资讯，我决定把我的收藏夹共享出来，大家一起嗨~~\n开源项目推荐 # kubeasy : 用来管理 Kubernetes 集群的 CLI 工具，提供了沉浸式的命令行界面\nkui : 也是一个 CLI 工具，与 kubeasy 目的相同，都是希望使用者能获取更多的集群信息，然后利用这些信息来做很多事。不同的是，kui 把网页内嵌到终端里了，你可以通过鼠标点击来操作。\nConfigurable HPA : 通过 CRD 来扩展 Kubernetes 原生 HPA 的功能，提供了更多可选参数。例如，原生的 HPA 不支持自定义弹性伸缩的速度，通过 CHPA 即可自定义。\nk8s-sidecar-injector : Tumblr（汤不热，你懂得）开源的一款自动注入 Sidecar 的工具。你只需要在 Pod 的 annotaion 中加上 injector.tumblr.com/request=sidecar-prod-v1 字段，就会自动在业务 Pod 中注入 sidecar-prod-v1 中定义的 Sidecar 容器、环境变量和存储卷。\ndns-discovery : 默认情况下，Istio 服务网格内的 Pod 无法与集群外的 URL 通信，如果想与集群外的 URL 通信，你必须显式地为每个 URL 创建相应的 Service Entry。dns-discovery 是一个运行在 Kubernetes DNS 前面的代理，它会监控集群内所有的 DNS 查询，然后为监控到的集群外 URL 自动创建 Service Entry。\nk-vswitch : 基于 Open vSwitch 的高性能 Kubernetes CNI 网络插件，网络协议支持 GRE 和 VxLAN，支持 Network Policy。\nkrontab : 如果你想在 Kubernetes 中创建一个 Cronjob，你得先编写一个 YAML 文件，然后再 apply 一下。krontab 可以让你免去这些繁琐的步骤，它类似于 Linux 系统中的 crontab，当你想创建一个 Cronjob 时，直接在终端输入命令 krontab -e 就会使用 vim 打开一个虚拟的文件，写好定时任务（语法和 crontab 一样）后输入 :wq 退出就会立即创建一个 Cronjob。是不是很爽？？\nAutocert : 一个 Kubernetes 附加组件，可自动向容器中注入 TLS/HTTPS 证书，加密容器之间的通信流量。\n博客推荐 # Kubernetes Pod 驱逐详解 : 本文详细分析了在什么情况下 Pod 会被 Kubernetes 从运行节点中驱逐，以及不同 QoS 等级 Pod 的驱逐顺序。\n基于 RabbitMQ 队列大小进行弹性伸缩 : 本文示范了如何使用 Custom Metrics，使得在 RabbitMQ 有太多未被消费的 Job 时，可以自动增加副本数量，让 Job 可以马上被处理。\nKubernetes Operator 最佳实践 : Openshift 写的一篇关于开发 Operator 的最佳守则，从 Operator 的主要精髓介绍，如 Operator 会 watch Master API 的事件，当相关事件发生后便会执行对应的动作。接着便提到了开发人员应该如何创建 Watches，Reconciliation Cycle，怎么对资源进行验证等。有想要开发 operator 的同学千万不要错过哦！\n使 Kubernetes 的 Service IP 路由可达 : Calico 官方博客，介绍了 Calico v3.4 引进的新特性。之前 calico 只能传播 Pod IP 的路由，引入该特性之后，calico 也能传播 Service IP 的路由了，同时还支持 ECMP 三层负载均衡策略。这个特性使得打通集群内外之间的流量更加容易。\n如何重启高可用 Kubernetes 集群 : 该篇文章介绍了如何安全地重启高可用 Kubernetes 集群，以及重启后对集群中服务造成的影响。\n如何使用 Istio 和 Kubernetes 进行金丝雀部署 : 本文主要讲述了如何通过 Kubernetes 和 Istio 来进行金丝雀部署，包括应用的打包、部署和流量拆分。\n在 Kubernetes 上通过 InfluxDB 和 Grafana 来收集 Twitter 统计信息 : 本文主要介绍了如何在 Kubernetes 上部署 InfluxDB 和 Grafana，通过 python 模块来收集你的 Twitter 账号统计信息，然后存储到 InfluxDB 中，最后通过 Grafana Dashboard 展现出来。\n内核集成容器特性的年度进展 : 本视频主要介绍了近几年尝试在内核中直接集成容器特性的工作进展，并通过代码来展示其中的大部分原理。\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 电子书推荐 # Docker and Kubernetes for Java Developers: Scale, deploy, and monitor multi-container applications : 本书主要内容是如何使用 Docker 和 Kubernetes 来构建、部署和管理 Java 应用。 获取方式：公众号后台回复：java\nlearning-k8s-source-code : k8s、docker源码分析笔记，记录源码学习和一些原理译文，力从应用出发，再去深究某个概念的原理。以 apiserver、controller-manager、scheduler、kubelet、proxy 和 kubectl 6个命令为主线。\nCloud Native DevOps with Kubernetes : 本书向开发人员和运维人员展示了如何在云原生环境中将行业标准 DevOps 实践应用于 Kubernetes。\n获取方式：公众号后台回复：devops\nThe Gorilla Guide to Kubernetes in the Enterprise : Gorilla 出版的一本小册子，用来指导如何在生产环境中部署和维护 Kubernetes，包括如何部署高可用控制平面，如何集成监控工具以及如何对集群进行在线升级。 获取方式：公众号后台回复：gorilla\n","date":"2019年6月27日","externalUrl":null,"permalink":"/posts/cloud-native-weekly-1/","section":"博客","summary":"前言 # 云原生不但可以很好的支持互联网应用，也在深刻影响着新的","title":"云原生周报第 1 期","type":"posts"},{"content":"该系列文章总共分为三篇：\nLinux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：CPU Linux Cgroup 入门教程：内存 上篇文章主要介绍了 cgroup 的一些基本概念，包括其在 CentOS 系统中的默认设置和控制工具，并以 CPU 为例阐述 cgroup 如何对资源进行控制。这篇文章将会通过具体的示例来演示如何通过 cgroup 来限制 CPU 的使用以及不同的 cgroup 设置对性能的影响。\n查看当前 cgroup 信息 # 有两种方法来查看系统的当前 cgroup 信息。第一种方法是通过 systemd-cgls 命令来查看，它会返回系统的整体 cgroup 层级，cgroup 树的最高层由 slice 构成，如下所示：\n$ systemd-cgls --no-page ├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 ├─user.slice │ ├─user-1000.slice │ │ └─session-11.scope │ │ ├─9507 sshd: tom [priv] │ │ ├─9509 sshd: tom@pts/3 │ │ └─9510 -bash │ └─user-0.slice │ └─session-1.scope │ ├─ 6239 sshd: root@pts/0 │ ├─ 6241 -zsh │ └─11537 systemd-cgls --no-page └─system.slice ├─rsyslog.service │ └─5831 /usr/sbin/rsyslogd -n ├─sshd.service │ └─5828 /usr/sbin/sshd -D ├─tuned.service │ └─5827 /usr/bin/python2 -Es /usr/sbin/tuned -l -P ├─crond.service │ └─5546 /usr/sbin/crond -n 可以看到系统 cgroup 层级的最高层由 user.slice 和 system.slice 组成。因为系统中没有运行虚拟机和容器，所以没有 machine.slice，所以当 CPU 繁忙时，user.slice 和 system.slice 会各获得 50% 的 CPU 使用时间。\nuser.slice 下面有两个子 slice：user-1000.slice 和 user-0.slice，每个子 slice 都用 User ID (UID) 来命名，因此我们很容易识别出哪个 slice 属于哪个用户。例如：从上面的输出信息中可以看出 user-1000.slice 属于用户 tom，user-0.slice 属于用户 root。\nsystemd-cgls 命令提供的只是 cgroup 层级的静态信息快照，要想查看 cgroup 层级的动态信息，可以通过 systemd-cgtop 命令查看：\n$ systemd-cgtop Path Tasks %CPU Memory Input/s Output/s / 161 1.2 161.0M - - /system.slice - 0.1 - - - /system.slice/vmtoolsd.service 1 0.1 - - - /system.slice/tuned.service 1 0.0 - - - /system.slice/rsyslog.service 1 0.0 - - - /system.slice/auditd.service 1 - - - - /system.slice/chronyd.service 1 - - - - /system.slice/crond.service 1 - - - - /system.slice/dbus.service 1 - - - - /system.slice/gssproxy.service 1 - - - - /system.slice/lvm2-lvmetad.service 1 - - - - /system.slice/network.service 1 - - - - /system.slice/polkit.service 1 - - - - /system.slice/rpcbind.service 1 - - - - /system.slice/sshd.service 1 - - - - /system.slice/system-getty.slice/getty@tty1.service 1 - - - - /system.slice/systemd-journald.service 1 - - - - /system.slice/systemd-logind.service 1 - - - - /system.slice/systemd-udevd.service 1 - - - - /system.slice/vgauthd.service 1 - - - - /user.slice 3 - - - - /user.slice/user-0.slice/session-1.scope 3 - - - - /user.slice/user-1000.slice 3 - - - - /user.slice/user-1000.slice/session-11.scope 3 - - - - /user.slice/user-1001.slice/session-8.scope 3 - - - - systemd-cgtop 提供的统计数据和控制选项与 top 命令类似，但该命令只显示那些开启了资源统计功能的 service 和 slice。比如：如果你想开启 sshd.service 的资源统计功能，可以进行如下操作：\n$ systemctl set-property sshd.service CPUAccounting=true MemoryAccounting=true 该命令会在 /etc/systemd/system/sshd.service.d/ 目录下创建相应的配置文件：\n$ ll /etc/systemd/system/sshd.service.d/ 总用量 8 4 -rw-r--r-- 1 root root 28 5月 31 02:24 50-CPUAccounting.conf 4 -rw-r--r-- 1 root root 31 5月 31 02:24 50-MemoryAccounting.conf $ cat /etc/systemd/system/sshd.service.d/50-CPUAccounting.conf [Service] CPUAccounting=yes $ cat /etc/systemd/system/sshd.service.d/50-MemoryAccounting.conf [Service] MemoryAccounting=yes 配置完成之后，再重启 sshd 服务：\n$ systemctl daemon-reload $ systemctl restart sshd 这时再重新运行 systemd-cgtop 命令，就能看到 sshd 的资源使用统计了：\n开启资源使用量统计功能可能会增加系统的负载，因为资源统计也要消耗 CPU 和内存，大多数情况下使用 top 命令来查看就足够了。当然了，这是 Linux 系统嘛，一切的控制权都在你自己手里，你想怎么做就怎么做。 分配 CPU 相对使用时间 # 通过上篇文章的学习我们知道了 CPU shares 可以用来设置 CPU 的相对使用时间，接下来我们就通过实践来验证一下。\n下面所做的实验都是在单核 CPU 的系统上进行的，多核与单核的情况完全不同，文末会单独讨论。 测试对象是 1 个 service 和两个普通用户，其中用户 tom 的 UID 是 1000，可以通过以下命令查看：\n$ cat /etc/passwd|grep tom tom❌1000:1000::/home/tom:/bin/bash 创建一个 foo.service：\n$ cat /etc/systemd/system/foo.service [Unit] Description=The foo service that does nothing useful After=remote-fs.target nss-lookup.target [Service] ExecStart=/usr/bin/sha1sum /dev/zero ExecStop=/bin/kill -WINCH ${MAINPID} [Install] WantedBy=multi-user.target /dev/zero 在 linux 系统中是一个特殊的设备文件，当你读它的时候，它会提供无限的空字符，因此 foo.service 会不断地消耗 CPU 资源。现在我们将 foo.service 的 CPU shares 改为 2048：\n$ mkdir /etc/systemd/system/foo.service.d $ cat \u0026lt;\u0026lt; EOF \u0026gt; /etc/systemd/system/foo.service.d/50-CPUShares.conf [Service] CPUShares=2048 EOF 由于系统默认的 CPU shares 值为 1024，所以设置成 2048 后，在 CPU 繁忙的情况下，foo.service 会尽可能获取 system.slice 的所有 CPU 使用时间。\n现在通过 systemctl start foo.service 启动 foo 服务，并使用 top 命令查看 CPU 使用情况：\n目前没有其他进程在消耗 CPU，所以 foo.service 可以使用几乎 100% 的 CPU。\n现在我们让用户 tom 也参与进来，先将 user-1000.slice 的 CPU shares 设置为 256：\n$ systemctl set-property user-1000.slice CPUShares=256 使用用户 tom 登录该系统，然后执行命令 sha1sum /dev/zero，再次查看 CPU 使用情况：\n现在是不是感到有点迷惑了？foo.service 的 CPU shares 是 2048，而用户 tom 的 CPU shares 只有 256，难道用户 tom 不是应该只能使用 10% 的 CPU 吗？回忆一下我在上一节提到的，当 CPU 繁忙时，user.slice 和 system.slice 会各获得 50% 的 CPU 使用时间。而这里恰好就是这种场景，同时 user.slice 下面只有 sha1sum 进程比较繁忙，所以会获得 50% 的 CPU 使用时间。\n最后让用户 jack 也参与进来，他的 CPU shares 是默认值 1024。使用用户 jack 登录该系统，然后执行命令 sha1sum /dev/zero，再次查看 CPU 使用情况：\n上面我们已经提到，这种场景下 user.slice 和 system.slice 会各获得 50% 的 CPU 使用时间。用户 tom 的 CPU shares 是 256，而用户 jack 的 CPU shares 是 1024，因此用户 jack 获得的 CPU 使用时间是用户 tom 的 4 倍。\n分配 CPU 绝对使用时间 # 上篇文章已经提到，如果想严格控制 CPU 资源，设置 CPU 资源的使用上限，即不管 CPU 是否繁忙，对 CPU 资源的使用都不能超过这个上限，可以通过 CPUQuota 参数来设置。下面我们将用户 tom 的 CPUQuota 设置为 5%：\n$ systemctl set-property user-1000.slice CPUQuota=5% 这时你会看到用户 tom 的 sha1sum 进程只能获得 5% 左右的 CPU 使用时间。\n如果此时停止 foo.service，关闭用户 jack 的 sha1sum 进程，你会看到用户 tom 的 sha1sum 进程仍然只能获得 5% 左右的 CPU 使用时间。\n如果某个非核心服务很消耗 CPU 资源，你可以通过这种方法来严格限制它对 CPU 资源的使用，防止对系统中其他重要的服务产生影响。\n动态设置 cgroup # cgroup 相关的所有操作都是基于内核中的 cgroup virtual filesystem，使用 cgroup 很简单，挂载这个文件系统就可以了。系统默认情况下都是挂载到 /sys/fs/cgroup 目录下，当 service 启动时，会将自己的 cgroup 挂载到这个目录下的子目录。以 foo.service 为例：\n先进入 system.slice 的 CPU 子系统：\n$ cd /sys/fs/cgroup/cpu,cpuacct/system.slice 查看 foo.service 的 cgroup 目录：\n$ ls foo.* zsh: no matches found: foo.* 因为 foo.service 没有启动，所以没有挂载 cgroup 目录，现在启动 foo.service，再次查看它的 cgroup 目录：\n$ ls foo.serice cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks 也可以查看它的 PID 和 CPU shares：\n$ cat foo.service/tasks 20225 $ cat foo.service/cpu.shares 2048 理论上我们可以在 /sys/fs/cgroup 目录中动态改变 cgroup 的配置，但我不建议你在生产环境中这么做。如果你想通过实验来深入理解 cgroup，可以多折腾折腾这个目录。 如果是多核 CPU 呢？ # 上面的所有实验都是在单核 CPU 上进行的，下面我们简单讨论一下多核的场景，以 2 个 CPU 为例。\n首先来说一下 CPU shares，shares 只能针对单核 CPU 进行设置，也就是说，无论你的 shares 值有多大，该 cgroup 最多只能获得 100% 的 CPU 使用时间（即 1 核 CPU）。还是用本文第 2 节的例子，将 foo.service 的 CPU shares 设置为 2048，启动 foo.service，这时你会看到 foo.service 仅仅获得了 100% 的 CPU 使用时间，并没有完全使用两个 CPU 核：\n再使用用户 tom 登录系统，执行命令 sha1sum /dev/zero，你会发现用户 tom 的 sha1sum 进程和 foo.service 各使用 1 个 CPU 核：\n再来说说 CPUQuota，这个上篇文章结尾已经提过了，如要让一个 cgroup 完全使用两个 CPU 核，可以通过 CPUQuota 参数来设置。例如：\n$ systemctl set-property foo.service CPUQuota=200% 至于进程最后能不能完全使用两个 CPU 核，就要看它自身的设计支持不支持了。\n总结 # 本文通过具体的示例来观察不同的 cgroup 设置对性能的影响，下面一篇文章将会演示如何通过 cgroup 来限制内存的使用。\n","date":"2019年5月31日","externalUrl":null,"permalink":"/posts/understanding-cgroups-part-2-cpu/","section":"博客","summary":"该系列文章总共分为三篇： Linux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：","title":"Linux Cgroup 入门教程：CPU","type":"posts"},{"content":"该系列文章总共分为三篇：\nLinux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：CPU Linux Cgroup 入门教程：内存 Linux Cgroups(Control groups) 是 Linux kernel 的一项功能：它是在一个系统中运行的层级制进程组，你可对其进行资源分配（如 CPU 时间、系统内存、网络带宽或者这些资源的组合）。通过使用 cgroup，系统管理员在分配、排序、拒绝、管理和监控系统资源等方面，可以进行精细化控制。硬件资源可以在应用程序和用户间智能分配，从而增加整体效率。\ncgroup 和 namespace 类似，也是将进程进行分组，但它的目的和 namespace 不一样，namespace 是为了隔离进程组之间的资源，而 cgroup 是为了对一组进程进行统一的资源监控和限制。\ncgroup 分 v1 和 v2 两个版本，v1 实现较早，功能比较多，但是由于它里面的功能都是零零散散的实现的，所以规划的不是很好，导致了一些使用和维护上的不便，v2 的出现就是为了解决 v1 中这方面的问题，在最新的 4.5 内核中，cgroup v2 声称已经可以用于生产环境了，但它所支持的功能还很有限，随着 v2 一起引入内核的还有 cgroup namespace。v1 和 v2 可以混合使用，但是这样会更复杂，所以一般没人会这样用。\n为什么需要 Cgroup # 在 Linux 里，一直以来就有对进程进行分组的概念和需求，比如 session group， progress group 等，后来随着人们对这方面的需求越来越多，比如需要追踪一组进程的内存和 IO 使用情况等，于是出现了 cgroup，用来统一将进程进行分组，并在分组的基础上对进程进行监控和资源控制管理等。\n什么是 Cgroup # 术语 cgroup 在不同的上下文中代表不同的意思，可以指整个 Linux 的 cgroup 技术，也可以指一个具体进程组。\ncgroup 是 Linux 下的一种将进程按组进行管理的机制，在用户层看来，cgroup 技术就是把系统中的所有进程组织成一颗一颗独立的树，每棵树都包含系统的所有进程，树的每个节点是一个进程组，而每颗树又和一个或者多个 subsystem 关联，树的作用是将进程分组，而 subsystem 的作用就是对这些组进行操作。为了更好地理解这个概念，我们可以用一个类比来帮助理解。\n首先，cgroup 可以被理解为一个为进程 (程序的运行实例) 提供资源管理和限制的系统。在这个系统中，有多个不同的 subsystem (子系统)，比如用于内存管理的 memory 子系统，用于 CPU 时间分配的 cpu 子系统等。每个子系统都负责管理系统的某个特定资源。\n现在，我们可以将 hierarchy (层级) 想象成一个办公大楼，而每个 subsystem 则是大楼内的一个办公室。这个大楼 (hierarchy) 的设计是这样的：\n每个办公室 (subsystem) 只能在一个大楼 (hierarchy) 中。这就好比一个团队 (比如财务团队) 只能在一个办公大楼里有一个办公室。他们不能在两个不同的大楼里同时有办公室。 同一个办公室 (subsystem) 不能同时在多个大楼 (hierarchy) 中。这就像是说，财务团队不能同时在 “A 大楼” 和 “B 大楼” 里都有办公室。 但是，不同的大楼可以有相同类型的办公室。例如，既可以在 “A 大楼” 也可以在 “B 大楼” 设有财务办公室，只不过这些办公室属于不同的团队。\n在 cgroup 的上下文中，这意味着一个特定的 subsystem (比如内存管理) 可以在不同的 cgroup 层级中被配置和使用，但是在任何给定时间点，一个 subsystem 只能与一个 cgroup 层级相关联。\n理解这一点对于配置和理解 Linux 资源管理至关重要，因为它决定了如何对不同的资源进行分组和限制。\n来看一个更直观的例子：\n父 cgroup 设置：假设你在 /sys/fs/cgroup/memory (我们称之为父 cgroup) 设置了最大内存使用量为 500MB。 子 cgroup 设置：然后你在 /sys/fs/cgroup/memory 下创建了一个子文件夹，也就是子 cgroup (我们称之为 cgroup A)，并在其中设置了最大内存使用量为 100MB。 在这种情况下，尽管父 cgroup 的限制是 500MB，cgroup A 仅能使用 100MB 的内存。但是，如果你没有为 cgroup A 设置特定的限制，它将默认受到父 cgroup 的限制，即最多 500MB 内存。\n现在，我们可以理解为什么 cgroup 中的 subsystem 不能属于多个 hierarchy 了。在上面的例子中，如果 memory 这个 subsystem 已经绑定到了一个特定的 hierarchy，你不能将它同时绑定到另一个 hierarchy。这意味着，虽然你可以在不同的 hierarchy 中设置内存限制，但任何时候 memory 这个 subsystem 都只能与一个 hierarchy 关联。\n将资源看作一块饼 # 在 CentOS 7 系统中（包括 Red Hat Enterprise Linux 7），通过将 cgroup 层级系统与 systemd 单位树捆绑，可以把资源管理设置从进程级别移至应用程序级别。默认情况下，systemd 会自动创建 slice、scope 和 service 单位的层级（具体的意思稍后再解释），来为 cgroup 树提供统一结构。可以通过 systemctl 命令创建自定义 slice 进一步修改此结构。\n如果我们将系统的资源看成一块馅饼，那么所有资源默认会被划分为 3 个 cgroup：System, User 和 Machine。每一个 cgroup 都是一个 slice，每个 slice 都可以有自己的子 slice，如下图所示：\n下面我们以 CPU 资源为例，来解释一下上图中出现的一些关键词。\n如上图所示，系统默认创建了 3 个顶级 slice（System, User 和 Machine），每个 slice 都会获得相同的 CPU 使用时间（仅在 CPU 繁忙时生效），如果 user.slice 想获得 100% 的 CPU 使用时间，而此时 CPU 比较空闲，那么 user.slice 就能够如愿以偿。这三种顶级 slice 的含义如下：\nsystem.slice —— 所有系统 service 的默认位置 user.slice —— 所有用户会话的默认位置。每个用户会话都会在该 slice 下面创建一个子 slice，如果同一个用户多次登录该系统，仍然会使用相同的子 slice。 machine.slice —— 所有虚拟机和 Linux 容器的默认位置 控制 CPU 资源使用的其中一种方法是 shares。shares 用来设置 CPU 的相对值（你可以理解为权重），并且是针对所有的 CPU（内核），默认值是 1024。因此在上图中，httpd, sshd, crond 和 gdm 的 CPU shares 均为 1024，System, User 和 Machine 的 CPU shares 也是 1024。\n假设该系统上运行了 4 个 service，登录了两个用户，还运行了一个虚拟机。同时假设每个进程都要求使用尽可能多的 CPU 资源（每个进程都很繁忙）。\nsystem.slice 会获得 33.333% 的 CPU 使用时间，其中每个 service 都会从 system.slice 分配的资源中获得 1/4 的 CPU 使用时间，即 8.25% 的 CPU 使用时间。 user.slice 会获得 33.333% 的 CPU 使用时间，其中每个登录的用户都会获得 16.5% 的 CPU 使用时间。假设有两个用户：tom 和 jack，如果 tom 注销登录或者杀死该用户会话下的所有进程，jack 就能够使用 33.333% 的 CPU 使用时间。 machine.slice 会获得 33.333% 的 CPU 使用时间，如果虚拟机被关闭或处于 idle 状态，那么 system.slice 和 user.slice 就会从这 33.333% 的 CPU 资源里分别获得 50% 的 CPU 资源，然后均分给它们的子 slice。 如果想严格控制 CPU 资源，设置 CPU 资源的使用上限，即不管 CPU 是否繁忙，对 CPU 资源的使用都不能超过这个上限。可以通过以下两个参数来设置：\ncpu.cfs_period_us = 统计CPU使用时间的周期，单位是微秒（us） cpu.cfs_quota_us = 周期内允许占用的CPU时间(指单核的时间，多核则需要在设置时累加) systemctl 可以通过 CPUQuota 参数来设置 CPU 资源的使用上限。例如，如果你想将用户 tom 的 CPU 资源使用上限设置为 20%，可以执行以下命令：\n$ systemctl set-property user-1000.slice CPUQuota=20% 在使用命令 systemctl set-property 时，可以使用 tab 补全：\n$ systemctl set-property user-1000.slice AccuracySec= CPUAccounting= Environment= LimitCPU= LimitNICE= LimitSIGPENDING= SendSIGKILL= BlockIOAccounting= CPUQuota= Group= LimitDATA= LimitNOFILE= LimitSTACK= User= BlockIODeviceWeight= CPUShares= KillMode= LimitFSIZE= LimitNPROC= MemoryAccounting= WakeSystem= BlockIOReadBandwidth= DefaultDependencies= KillSignal= LimitLOCKS= LimitRSS= MemoryLimit= BlockIOWeight= DeviceAllow= LimitAS= LimitMEMLOCK= LimitRTPRIO= Nice= BlockIOWriteBandwidth= DevicePolicy= LimitCORE= LimitMSGQUEUE= LimitRTTIME= SendSIGHUP= 这里有很多属性可以设置，但并不是所有的属性都是用来设置 cgroup 的，我们只需要关注 Block, CPU 和 Memory。\n如果你想通过配置文件来设置 cgroup，service 可以直接在 /etc/systemd/system/xxx.service.d 目录下面创建相应的配置文件，slice 可以直接在 /run/systemd/system/xxx.slice.d 目录下面创建相应的配置文件。事实上通过 systemctl 命令行工具设置 cgroup 也会写到该目录下的配置文件中：\n$ cat /run/systemd/system/user-1000.slice.d/50-CPUQuota.conf [Slice] CPUQuota=20% 查看对应的 cgroup 参数：\n$ cat /sys/fs/cgroup/cpu,cpuacct/user.slice/user-1000.slice/cpu.cfs_period_us 100000 $ cat /sys/fs/cgroup/cpu,cpuacct/user.slice/user-1000.slice/cpu.cfs_quota_us 20000 这表示用户 tom 在一个使用周期内（100 毫秒）可以使用 20 毫秒的 CPU 时间。不管 CPU 是否空闲，该用户使用的 CPU 资源都不会超过这个限制。\nCPUQuota 的值可以超过 100%，例如：如果系统的 CPU 是多核，且 CPUQuota 的值为 200%，那么该 slice 就能够使用 2 核的 CPU 时间。 总结 # 本文主要介绍了 cgroup 的一些基本概念，包括其在 CentOS 系统中的默认设置和控制工具，以 CPU 为例阐述 cgroup 如何对资源进行控制。下一篇文章将会通过具体的示例来观察不同的 cgroup 设置对性能的影响。\n参考资料 # Linux Cgroup系列（01）：Cgroup概述 ","date":"2019年5月22日","externalUrl":null,"permalink":"/posts/understanding-cgroups-part-1-basics/","section":"博客","summary":"该系列文章总共分为三篇： Linux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：","title":"Linux Cgroup 入门教程：基本概念","type":"posts"},{"content":"在 Kubernetes 中，Pod 使用的资源最重要的是 CPU、内存和磁盘 IO，这些资源可以被分为可压缩资源（CPU）和不可压缩资源（内存，磁盘 IO）。可压缩资源不可能导致 Pod 被驱逐，因为当 Pod 的 CPU 使用量很多时，系统可以通过重新分配权重来限制 Pod 的 CPU 使用。而对于不可压缩资源来说，如果资源不足，也就无法继续申请资源（内存用完就是用完了），此时 Kubernetes 会从该节点上驱逐一定数量的 Pod，以保证该节点上有充足的资源。\n当不可压缩资源不足时，Kubernetes 是通过 kubelet 来驱逐 Pod 的。kubelet 也不是随机驱逐的，它有自己的一套驱逐机制，每个计算节点的 kubelet 都会通过抓取 cAdvisor 的指标来监控节点的资源使用量，下面我们来具体分析每种情况。\n存储资源不足 # 下面是 kubelet 默认的关于节点存储的驱逐触发条件：\nnodefs.available\u0026lt;10%（容器 volume 使用的文件系统的可用空间，包括文件系统剩余大小和 inode 数量） imagefs.available\u0026lt;15%（容器镜像使用的文件系统的可用空间，包括文件系统剩余大小和 inode 数量） 当 imagefs 使用量达到阈值时，kubelet 会尝试删除不使用的镜像来清理磁盘空间。\n当 nodefs 使用量达到阈值时，kubelet 就会拒绝在该节点上运行新 Pod，并向 API Server 注册一个 DiskPressure condition。然后 kubelet 会尝试删除死亡的 Pod 和容器来回收磁盘空间，如果此时 nodefs 使用量仍然没有低于阈值，kubelet 就会开始驱逐 Pod。从 Kubernetes 1.9 开始，kubelet 驱逐 Pod 的过程中不会参考 Pod 的 QoS，只是根据 Pod 的 nodefs 使用量来进行排名，并选取使用量最多的 Pod 进行驱逐。所以即使 QoS 等级为 Guaranteed 的 Pod 在这个阶段也有可能被驱逐（例如 nodefs 使用量最大）。如果驱逐的是 Daemonset，kubelet 会阻止该 Pod 重启，直到 nodefs 使用量超过阈值。\n如果一个 Pod 中有多个容器，kubelet 会根据 Pod 中所有容器的 nodefs 使用量之和来进行排名。即所有容器的 container_fs_usage_bytes 指标值之和。\n举个栗子，假设某计算节点上运行着一系列已知 QoS 等级和 nodefs 使用量的 Pod：\nPod Name Pod QoS nodefs usage A Best Effort 800M B Guaranteed 1.3G C Burstable 1.2G D Burstable 700M E Best Effort 500M F Guaranteed 1G 当 nodefs 的使用量超过阈值时，kubelet 会根据 Pod 的 nodefs 使用量来对 Pod 进行排名，首先驱逐使用量最多的 Pod。排名如下图所示：\nPod Name Pod QoS nodefs usage B Guaranteed 1.3G C Burstable 1.2G F Guaranteed 1G A Best Effort 800M D Burstable 700M E Best Effort 500M 可以看到在本例中，QoS 等级为 Guaranteed 的 Pod 最先被驱逐。\n内存资源不足 # 下面是 kubelet 默认的关于节点内存资源的驱逐触发条件：\nmemory.available\u0026lt;100Mi 当内存使用量超过阈值时，kubelet 就会向 API Server 注册一个 MemoryPressure condition，此时 kubelet 不会接受新的 QoS 等级为 Best Effort 的 Pod 在该节点上运行，并按照以下顺序来驱逐 Pod：\nPod 的内存使用量是否超过了 request 指定的值 根据 priority 排序，优先级低的 Pod 最先被驱逐 比较它们的内存使用量与 request 指定的值之差。 按照这个顺序，可以确保 QoS 等级为 Guaranteed 的 Pod 不会在 QoS 等级为 Best Effort 的 Pod 之前被驱逐，但不能保证它不会在 QoS 等级为 Burstable 的 Pod 之前被驱逐。\n如果一个 Pod 中有多个容器，kubelet 会根据 Pod 中所有容器相对于 request 的内存使用量与之和来进行排名。即所有容器的 （container_memory_usage_bytes 指标值与 container_resource_requests_memory_bytes 指标值的差）之和。\n继续举例，假设某计算节点上运行着一系列已知 QoS 等级和内存使用量的 Pod：\nPod Name Pod QoS Memory requested Memory limits Memory usage A Best Effort 0 0 700M B Guaranteed 2Gi 2Gi 1.9G C Burstable 1Gi 2Gi 1.8G D Burstable 1Gi 2Gi 800M E Best Effort 0 0 300M F Guaranteed 2Gi 2Gi 1G 当节点的内存使用量超过阈值时，kubelet 会根据 Pod 相对于 request 的内存使用量来对 Pod 进行排名。排名如下所示：\nPod Name Pod QoS Memory requested Memory limits Memory usage 内存相对使用量 C Burstable 1Gi 2Gi 1.8G 800M A Best Effort 0 0 700M 700M E Best Effort 0 0 300M 300M B Guaranteed 2Gi 2Gi 1.9G -100M D Burstable 1Gi 2Gi 800M -200M F Guaranteed 2Gi 2Gi 1G -1G 可以看到在本例中，可以看到在本例中，QoS 等级为 Guaranteed 的 Pod 在 QoS 等级为 Burstable 的 Pod 之前被驱逐。\n当内存资源不足时，kubelet 在驱逐 Pod 时只会考虑 requests 和 Pod 的内存使用量，不会考虑 limits。\nNode OOM (Out Of Memory) # 因为 kubelet 默认每 10 秒抓取一次 cAdvisor 的监控数据，所以有可能在 kubelet 驱逐 Pod 回收内存之前发生内存使用量激增的情况，这时就有可能触发内核 OOM killer。这时删除容器的权利就由kubelet 转交到内核 OOM killer 手里，但 kubelet 仍然会起到一定的决定作用，它会根据 Pod 的 QoS 来设置其 oom_score_adj 值：\nQoS oom_score_adj Guaranteed -998 Burstable min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999) pod-infra-container -998 kubelet, docker daemon, systemd service -999 如果该节点在 kubelet 通过驱逐 Pod 回收内存之前触发了 OOM 事件，OOM killer 就会采取行动来降低系统的压力，它会根据下面的公式来计算 oom_score 的值：\n容器使用的内存占系统内存的百分比 + oom_score_adj = oom_score\nOOM killer 会杀掉 oom_score_adj 值最高的容器，如果有多个容器的 oom_score_adj 值相同，就会杀掉内存使用量最多的容器（其实是因为内存使用量最多的容器的 oom_score 值最高）。关于 OOM 的更多内容请参考： Kubernetes 内存资源限制实战。\n假设某节点运行着 4 个 Pod，且每个 Pod 中只有一个容器。每个 QoS 类型为 Burstable 的 Pod 配置的内存 requests 是 4Gi，节点的内存大小为 30Gi。每个 Pod 的 oom_score_adj 值如下所示：\nPod Name Pod QoS oom_score_adj A Best Effort 1000 B Guaranteed -998 C Burstable 867（根据上面的公式计算） D Best Effort 1000 当调用 OOM killer 时，它首先选择 oom_score_adj 值最高的容器（1000），这里有两个容器的 oom_score_adj 值都是 1000，OOM killer 最终会选择内存使用量最多的容器。\n总结 # 因为 kubelet 默认每 10 秒抓取一次 cAdvisor 的监控数据，所以可能在资源使用量低于阈值时，kubelet 仍然在驱逐 Pod。 kubelet 将 Pod 从节点上驱逐之后，Kubernetes 会将该 Pod 重新调度到另一个资源充足的节点上。但有时候 Scheduler 会将该 Pod 重新调度到与之前相同的节点上，比如设置了节点亲和性，或者该 Pod 以 Daemonset 的形式运行。 现在你应该理解了 kubelet 驱逐 Pod 的原理和过程，如果你在部署应用时设置了恰当的参数，知道了所有的可能性，你就能更好地掌控你的集群。\n","date":"2019年5月17日","externalUrl":null,"permalink":"/posts/kubernetes-eviction/","section":"博客","summary":"","title":"Kubernetes Pod 驱逐详解","type":"posts"},{"content":"Kubernetes 对内存资源的限制实际上是通过 cgroup 来控制的，cgroup 是容器的一组用来控制内核如何运行进程的相关属性集合。针对内存、CPU 和各种设备都有对应的 cgroup。cgroup 是具有层级的，这意味着每个 cgroup 拥有一个它可以继承属性的父亲，往上一直直到系统启动时创建的 root cgroup。关于其背后的原理可以参考： 深入理解Kubernetes资源限制：内存。\n今天我们将通过实验来探索容器在什么情况下会被 oom-killed。\n实验准备 # 首先你需要一个 Kubernetes 集群，然后通过 kubectl 创建一个 Pod，内存限制为 123Mi。\n$ kubectl run --restart=Never --rm -it --image=ubuntu --limits=\u0026#39;memory=123Mi\u0026#39; -- sh If you don\u0026#39;t see a command prompt, try pressing enter. root@sh:/# 重新打开一个 shell 窗口，找出刚创建的 Pod 的 uid：\n$ kubectl get pods sh -o yaml | grep uid uid: bc001ffa-68fc-11e9-92d7-5ef9efd9374c 在运行该 Pod 的节点上找出其 cgroup 的内存设置：\n$ cd /sys/fs/cgroup/memory/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c $ cat memory.limit_in_bytes 128974848 其中 memory.limit_in_bytes 表示当前限制的内存额度。128974848 正好等于 123*1024*1024。\n如果你查看一下这个 Pod 的 cgroup 目录，就会发现 Pod 中的每个容器都会在该目录下创建一个子 cgroup 目录：\n$ ll /sys/fs/cgroup/memory/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c 总用量 0 drwxr-xr-x 2 root root 0 4月 28 18:46 64ae20d221399e618bbf8c15f3b5ae5050062d497971d0af5346d5532fa5c585 drwxr-xr-x 2 root root 0 4月 28 18:25 a398d3c012bb37dd9fe5fef524842a8699de931bce3a4e3753a49ef1694b33ee -rw-r--r-- 1 root root 0 4月 28 18:46 cgroup.clone_children --w--w--w- 1 root root 0 4月 28 18:46 cgroup.event_control -rw-r--r-- 1 root root 0 4月 28 18:46 cgroup.procs -rw-r--r-- 1 root root 0 4月 28 18:46 memory.failcnt --w------- 1 root root 0 4月 28 18:46 memory.force_empty -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.failcnt -rw-r--r-- 1 root root 0 4月 28 18:24 memory.kmem.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.max_usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.kmem.slabinfo -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.failcnt -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.max_usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.kmem.usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:24 memory.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.max_usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.memsw.failcnt -rw-r--r-- 1 root root 0 4月 28 18:46 memory.memsw.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.memsw.max_usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.memsw.usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.move_charge_at_immigrate -r--r--r-- 1 root root 0 4月 28 18:46 memory.numa_stat -rw-r--r-- 1 root root 0 4月 28 18:46 memory.oom_control ---------- 1 root root 0 4月 28 18:46 memory.pressure_level -rw-r--r-- 1 root root 0 4月 28 18:46 memory.soft_limit_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.stat -rw-r--r-- 1 root root 0 4月 28 18:46 memory.swappiness -r--r--r-- 1 root root 0 4月 28 18:46 memory.usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.use_hierarchy -rw-r--r-- 1 root root 0 4月 28 18:46 notify_on_release -rw-r--r-- 1 root root 0 4月 28 18:46 tasks 输出结果的前两个目录其实就是 pause 容器的 pause 进程和业务容器的 bash 进程创建的两个子 cgroup 目录。可以来证实一下，我的环境使用的容器运行时是 containerd，可以通过 crictl 工具来查看，如果你使用的是 docker，方法类似。\n先在运行该容器的节点上找到该业务容器的 ID：\n$ crictl ps|grep \u0026#34;CONTAINER_RUNNING sh\u0026#34; 64ae20d221399 sha256:d131e0fa2585a7efbfb187f70d648aa50e251d9d3b7031edf4730ca6154e221e 17 hours ago CONTAINER_RUNNING sh 0 查看该容器的 pid：\n$ crictl inspect 64ae20d221399|grep pid \u0026#34;pid\u0026#34;: 32308, \u0026#34;pid\u0026#34;: 1 \u0026#34;type\u0026#34;: \u0026#34;pid\u0026#34; 查看该进程所属的 cgroup，即进程在 cgroup 树中的路径：\n$ cat /proc/32308/cgroup ... 4:memory:/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/64ae20d221399e618bbf8c15f3b5ae5050062d497971d0af5346d5532fa5c585 ... 进入该目录，查看内存限制：\n$ cd /sys/fs/cgroup/memory/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/64ae20d221399e618bbf8c15f3b5ae5050062d497971d0af5346d5532fa5c585 $ cat memory.limit_in_bytes 128974848 可以看到该 cgroup 的内存限制和父 cgroup 一样，而父 cgroup 其实就是 Pod 级别的 cgroup。\n按照预想，一旦 Pod 消耗的内存资源超过这个限制，cgroup 就会杀死容器进程，我们来测试一下。\n压力测试 # 先在容器中安装压力测试工具：\nroot@sh:/# apt update; apt install -y stress 在另一个一个 shell 窗口中执行 dmesg -Tw 命令查看系统的 Syslog。\n回到第一个 shell 窗口进行压力测试，限制内存在 100M 以内：\nroot@sh:/# stress --vm 1 --vm-bytes 100M \u0026amp; [1] 271 root@sh:/# stress: info: [271] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd 执行第二次压力测试：\nroot@sh:/# stress --vm 1 --vm-bytes 50M stress: info: [273] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd stress: FAIL: [271] (415) \u0026lt;-- worker 272 got signal 9 stress: WARN: [271] (417) now reaping child worker processes stress: FAIL: [271] (451) failed run completed in 7s 可以看到系统通过发送 signal 9（SIGKILL） 信号杀死了第一次压力测试的进程（进程 ID 为 271）。\n可以在另一个 shell 窗口中看到系统的 syslog 日志输出：\n[Sat Apr 27 22:56:09 2019] stress invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=939 [Sat Apr 27 22:56:09 2019] stress cpuset=a2ed67c63e828da3849bf9f506ae2b36b4dac5b402a57f2981c9bdc07b23e672 mems_allowed=0 [Sat Apr 27 22:56:09 2019] CPU: 0 PID: 32332 Comm: stress Not tainted 4.15.0-46-generic #49-Ubuntu [Sat Apr 27 22:56:09 2019] Hardware name: BHYVE, BIOS 1.00 03/14/2014 [Sat Apr 27 22:56:09 2019] Call Trace: [Sat Apr 27 22:56:09 2019] dump_stack+0x63/0x8b [Sat Apr 27 22:56:09 2019] dump_header+0x71/0x285 [Sat Apr 27 22:56:09 2019] oom_kill_process+0x220/0x440 [Sat Apr 27 22:56:09 2019] out_of_memory+0x2d1/0x4f0 [Sat Apr 27 22:56:09 2019] mem_cgroup_out_of_memory+0x4b/0x80 [Sat Apr 27 22:56:09 2019] mem_cgroup_oom_synchronize+0x2e8/0x320 [Sat Apr 27 22:56:09 2019] ? mem_cgroup_css_online+0x40/0x40 [Sat Apr 27 22:56:09 2019] pagefault_out_of_memory+0x36/0x7b [Sat Apr 27 22:56:09 2019] mm_fault_error+0x90/0x180 [Sat Apr 27 22:56:09 2019] __do_page_fault+0x4a5/0x4d0 [Sat Apr 27 22:56:09 2019] do_page_fault+0x2e/0xe0 [Sat Apr 27 22:56:09 2019] ? page_fault+0x2f/0x50 [Sat Apr 27 22:56:09 2019] page_fault+0x45/0x50 [Sat Apr 27 22:56:09 2019] RIP: 0033:0x558182259cf0 [Sat Apr 27 22:56:09 2019] RSP: 002b:00007fff01a47940 EFLAGS: 00010206 [Sat Apr 27 22:56:09 2019] RAX: 00007fdc18cdf010 RBX: 00007fdc1763a010 RCX: 00007fdc1763a010 [Sat Apr 27 22:56:09 2019] RDX: 00000000016a5000 RSI: 0000000003201000 RDI: 0000000000000000 [Sat Apr 27 22:56:09 2019] RBP: 0000000003200000 R08: 00000000ffffffff R09: 0000000000000000 [Sat Apr 27 22:56:09 2019] R10: 0000000000000022 R11: 0000000000000246 R12: ffffffffffffffff [Sat Apr 27 22:56:09 2019] R13: 0000000000000002 R14: fffffffffffff000 R15: 0000000000001000 [Sat Apr 27 22:56:09 2019] Task in /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/a2ed67c63e828da3849bf9f506ae2b36b4dac5b402a57f2981c9bdc07b23e672 killed as a result of limit of /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c [Sat Apr 27 22:56:09 2019] memory: usage 125952kB, limit 125952kB, failcnt 3632 [Sat Apr 27 22:56:09 2019] memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0 [Sat Apr 27 22:56:09 2019] kmem: usage 2352kB, limit 9007199254740988kB, failcnt 0 [Sat Apr 27 22:56:09 2019] Memory cgroup stats for /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c: cache:0KB rss:0KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:0KB inactive_file:0KB active_file:0KB unevictable:0KB [Sat Apr 27 22:56:09 2019] Memory cgroup stats for /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/79fae7c2724ea1b19caa343fed8da3ea84bbe5eb370e5af8a6a94a090d9e4ac2: cache:0KB rss:48KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:48KB inactive_file:0KB active_file:0KB unevictable:0KB [Sat Apr 27 22:56:09 2019] Memory cgroup stats for /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/a2ed67c63e828da3849bf9f506ae2b36b4dac5b402a57f2981c9bdc07b23e672: cache:0KB rss:123552KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:123548KB inactive_file:0KB active_file:0KB unevictable:0KB [Sat Apr 27 22:56:09 2019] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name [Sat Apr 27 22:56:09 2019] [25160] 0 25160 256 1 28672 0 -998 pause [Sat Apr 27 22:56:09 2019] [25218] 0 25218 4627 872 77824 0 939 bash [Sat Apr 27 22:56:09 2019] [32307] 0 32307 2060 275 57344 0 939 stress [Sat Apr 27 22:56:09 2019] [32308] 0 32308 27661 24953 253952 0 939 stress [Sat Apr 27 22:56:09 2019] [32331] 0 32331 2060 304 53248 0 939 stress [Sat Apr 27 22:56:09 2019] [32332] 0 32332 14861 5829 102400 0 939 stress [Sat Apr 27 22:56:09 2019] Memory cgroup out of memory: Kill process 32308 (stress) score 1718 or sacrifice child [Sat Apr 27 22:56:09 2019] Killed process 32308 (stress) total-vm:110644kB, anon-rss:99620kB, file-rss:192kB, shmem-rss:0kB [Sat Apr 27 22:56:09 2019] oom_reaper: reaped process 32308 (stress), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB 从宿主机的视角来看，PID 为 32308 的进程被 oom-killed 了，我们需要重点关注最后一段日志输出：\n对于刚刚创建的 Pod 而言，有好几个进程作为 OOM killer 的候选人，其中最重要的进程是 pause，用来为业务容器创建共享的 network namespace，其 oom_score_adj 值为 -998，可以确保不被杀死。oom_score_adj 值越低就越不容易被杀死。关于 Pod 的 QoS 与 OOM 值的对应关系，可以参考： Kubernetes 资源管理概述。\n除了 pause 进程外，剩下的进程 oom_score_adj 值均为 939，我们可以根据 Kubernetes 官方文档中公式来验证一下：\nmin(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999) 进程的 oom_score_adj 值可以通过以下命令来查看：\n$ cat /proc/32308/oom_score_adj 939 其中 memoryRequest 是 pod 申请的资源，memoryCapacity 是节点的内存总量。可以看到，申请的内存越多，oom 值越低，也就越不容易被杀死。\n查看运行该 Pod 的节点内存总量：\n$ kubectl describe nodes k3s | grep Allocatable -A 5 Allocatable: cpu: 1 ephemeral-storage: 49255941901 hugepages-1Gi: 0 hugepages-2Mi: 0 memory: 2041888Ki 如果只设置了 limits，Kubernetes 会自动把 Pod 的 requests 设置成和 limits 一样。所以其他进程的 oom_score_adj 值为 1000–123*1024/2041888=938.32，这个值已经很接近 syslog 中输出的 939 了。\nOOM killer 会根据进程的内存使用情况来计算 oom_score 的值，并根据 oom_score_adj 的值来进行微调。 进程的 oom_score 值可以通过以下命令来查看：\n$ cat /proc/32308/oom_score 1718 因为业务容器内所有进程的 oom_score_adj 值都相同，所以谁的内存使用量最多，oom_score 值就越高，也就越容易被杀死。因为第一个 stress 进程使的内存使用量最多（100M），oom_score 值最高（值为 1718），所以被杀死。\n总结 # Kubernetes 通过 cgroup 和 OOM killer 来限制 Pod 的内存资源，在实际使用中我们需要小心区分 OS 级别的 OOM 和 Pod 级别的 OOM。\n","date":"2019年4月29日","externalUrl":null,"permalink":"/posts/memory-limit-of-pod-and-oom-killer/","section":"博客","summary":"","title":"Kubernetes 内存资源限制实战","type":"posts"},{"content":" 原文链接：Kubernetes zero downtime deployment: when theory meets the database 如果你使用像 Gmail 这样的在线服务或者大型社交媒介和电子商务平台，你可能从来都没有遇到过哪个页面会提示你“请等待我们的应用更新完成”。\n事实上，现如今越来越多的服务需要始终保持启用和可访问的状态，主要有以下几个原因：\n如果你竞争对手的应用可以保持不宕机，那你可能会失去竞争优势；换句话说，如果你的竞争对手没法保持不宕机，而你的应用可以始终保持服务可用，那你就具有竞争优势。 在全球范围内，用户体验质量在不断提高，用户希望随着时间的推移能够提高应用的可用性。 如果你的应用会直接影响到你的收入，例如，以电子商务应用的形式进行销售。那么你应该能意识到宕机可能导致的业务影响。 虽然意外宕机不能完全避免，但在更新应用时保持零宕机还是有可能的。\n先驱：蓝绿部署 # 最早用来实现零宕机更新的方法是 蓝绿部署，简而言之，蓝绿部署规定应该有两个完全相似的环境，一个代表绿，一个代表蓝。无论任何时候，都有一个环境运行生产级别的应用，另一个环境运行预生产级别的应用。在集群的流量入口处有一个调度器，用来将请求路由到相应的环境：生产或预生产。当某个应用需要更新时，首先将它部署到预生产环境，进行一系列测试，然后将流量切换到该环境，使之暂时成为新的生产环境，反之亦然。\n在使用蓝绿部署的过程中，会遇到下面几个问题：\n用来路由请求的调度器必须是零延迟。\n一旦完成流量切换，环境就会发生转换，用户的流量就会被路由到新环境。调度器的实现有很多种方式：路由器、软件代理等，可能很难实现零延迟切换。\n当切换流量时，如果用户和应用已经发生了交互会怎么样？\n现代架构的终极目标是实现应用的弹性伸缩和无状态化。但实际情况下有些应用无法完全实现无状态化：比如购物车的无状态化就很难实现，唯一的办法是在购物车状态发生变化时将其从 A 环境迁移到 B 环境。但环境的迁移不是瞬间完成的，用户可能会发现自己处于中间状态，既不是完全处于 A 环境中，也不是完全处于 B 环境中。\n如果应用后端有数据库该如何处理？\n和上面讨论的类似，如果有一个 A 环境的数据库和一个 B 环境的数据库，就需要把数据从 A 环境迁移到 B 环境。推荐的做法是在流量切换之前完成数据的迁移，但在生产环境中数据可能会在流量完全切换之前发生变化，因此流量切换完成之后还要再进行一次数据迁移。但数据的迁移也不是瞬间完成的，需要一定的时间，这段时间内用户可能无法使用该服务。\n折中的解决方案是将数据库转移到 AB 环境之外的环境，然后将数据共享给 A 和 B 这两个环境。虽然这种架构对隔离性会产生一定的影响，但本文我不会展开详述。\nKubernetes 的滚动更新 # 如果你的应用部署在 Kubernetes 中，完全可以通过 Deployment 来实现应用的无缝升级。\nDeployment 控制器为 Pod 和 ReplicaSet 提供了声明式更新。关于声明式的详细信息可以参考： Kubernetes 设计与开发原则\n你可以在 Deployment 对象中声明期望的状态，Deployment Controller 可以通过不同的策略来不断调整实际状态，直到与期望状态保持一致。你可以选择让 Deployment 创建新的 ReplicaSet 来更新应用，或者删除旧的 Deployment，修改配置后重新创建新的 Deployment。\n重点在于“通过不同的控制策略”：这意味着 Deployment 中的 Pod 可以一个一个更新，也可以以两个为一组进行更新，或者先删除所有的 Pod，再创建新的 Pod，你可以有多种选择。具体的配置如下：\napiVersion: apps/v1 kind: Deployment spec: replicas: 3 strategy: rollingUpdate: maxSurge: 0 # ② maxUnavailable: 1 # ③ type: RollingUpdate # ① ① : type 表示新的 Pod 替换旧的 Pod 的策略，可以是 Recreate 或者 RollingUpdate。如果选择了 Recreate，就会在创建出新的 Pod 之前会先杀掉所有已存在的 Pod。这种策略不能实现零宕机升级，所以只能用在开发环境中。如果选择了 RollingUpdate，Deployment 就会使用滚动的方式更新 Pod，你可以指定 maxUnavailable 和 maxSurge 来控制 rolling update 进程。 ② : maxSurge 用来指定可以超过期望的 Pod 数量的最大个数。该值可以是一个绝对值（例如 5）或者是期望的 Pod 数量的百分比（例如 10%）。 ③ : maxUnavailable 用来指定在升级过程中不可用 Pod 的最大数量。该值可以是一个绝对值（例如 5），也可以是期望 Pod 数量的百分比（例如 10%）。 光看理论可能不太好理解，下面我们通过一些示例来理解它的工作原理。\nKubernetes 滚动更新实践 # 下文中展示的图表显示了随着时间的推移，不同版本的 Pod 数量的变化：\n竖轴表示 Pod 的数量 蓝色代表 v1 版本的 Pod 深蓝色代表 v2 版本的 Pod 横轴表示时间 先创建一个新 Pod，再删除一个旧 Pod # 上面的示例 yaml 表示更新过程中最多允许比期望的 Pod 数量多一个 Pod（maxSurge = 1），且最多允许比期望的 Pod 数量少 0 个 Pod（maxUnavailable = 0）。\n通过该配置，Kubernetes 会创建一个新 Pod，然后再删除一个旧 Pod，不断迭代下去。如果有其他计算节点可以运行新的 Pod，调度系统就会将新 Pod 调度到其他节点，否则就会调度到已有的计算节点，和节点上的其他 Pod 共同竞争计算资源。\n先删除一个旧 Pod，再创建一个新 Pod # 如果想在更新过程中最多允许比期望的 Pod 数量多 0 个 Pod，且最多允许比期望的 Pod 数量少 1 个 Pod，可以令 maxSurge = 0，maxUnavailable = 1。\n通过该配置，Kubernetes 会删除一个旧 Pod，然后再创建一个新 Pod，不断迭代下去。这种方式的好处是当集群的计算资源不足时，可以保持工作负载的数量不会大于现有的数量。\n尽快更新所有 Pod # 如果想在更新过程中最多允许比期望的 Pod 数量多 1 个 Pod，且最多允许比期望的 Pod 数量少 1 个 Pod，可以令 maxSurge = 1，maxUnavailable = 1。\n这种配置会尽快更新所有 Pod，大大减少了在应用版本之间切换所需的时间，但包含了前两种方式的所有缺点。\n考虑应用启动耗时 # Pod 从启动到能对外提供服务所用的时间是不容忽视的，为了确保容器在部署后确实处在正常运行状态，Kubernetes 提供了两种探针（Probe）来探测容器的状态：\nLivenessProbe：探测应用是否处于健康状态，如果不健康则删除并重新创建容器。 ReadinessProbe：探测应用是否启动完成并且处于正常服务状态，如果不正常则不会接收来自 Kubernetes Service 的流量。 默认情况下，这两种探针的成功返回值都是 Success，这就有可能会出现问题，因为 Pod 启动成功后，服务不一定会立即可用，这时如果 Service 将流量转发到该 Pod，不会有正确的响应。\n为了解决这个问题，应用需要提供给 Kubernetes 查询并能返回应用状态的端点。例如，假如我们在应用中添加了一个 /ready 端点，如果能处理请求就返回 200 状态码，否则就返回 500 状态码。\n通过下面的 yaml 可以将 /ready 端点与 Kubernetes 的就绪探针结合使用：\nspec: containers: - name: foo-app image: zerodowntime:1.0 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 10 ① periodSeconds: 2 ② ① : 第一次就绪检查前需要等待的时间。 ② : 两次就绪检查间隔的时间。 通过上述配置，只有当 Pod 中的应用能够处理流量时，Service 才会将流量转发到该 Pod。\n现在我们已经知道了如何正确处理像 “Hello World” 这种类型的应用，但 Kubernetes 的滚动更新会遇到与蓝绿部署相同的问题：数据库的数据结构变更需要向前向后兼容。\n滚动更新与数据结构的兼容性 # 上文提到过，数据库结构的更改必须向后兼容。下面用一个简单的示例来说明这个问题。\n假设数据库的数据结构如下：\n使用这种数据结构，PERSON 和 ADDRESS 之间的界限比较模糊，为了划清界限，可以将数据结构改成如下的形式：\n假设原来数据结构界限比较模糊的应用已经在生产环境中开始使用，现在我们的目标是在零宕机的情况下将数据结构更换成上图的最终架构。\n为了实现这个目标，我们可以设计一个同时可以处理新数据结构和旧数据结构的新版本应用，这样就可以直接通过滚动更新 Deployment 来更新数据结构。这个方案看起来比较合理，但还有一个问题：Deployment 不能回滚，因为旧版本的应用程序无法处理新的数据结构。因此，我们必须保证应用既能向后兼容，又能向前兼容。看来我们又回到了原点，因为数据结构不可能保证既能向前又能向后兼容。\n最好的办法是将数据结构的更新拆分成一系列小的数据结构更新。此外，应用需要以增量的方式进行更新，以便新版本的应用能够处理当前和以后的数据结构更新。具体的数据迁移步骤如下：\n1. 将需要更新的应用打上标签 2.1。更新过程中需要在数据库中创建一个 ADDRESS 表，PERSON 表中的每一个变化，都复制一份到 ADDRESS 表中：\n对 PERSON 表的操作 复制到 ADDRESS 表 INSERT 将相同的数据 INSERT 到 ADDRESS 表中 UPDATE 首先检查 ADDRESS 表中是否有该记录，如果没有该记录，就先创建一个新的记录，然后再更新和 PERSON 中相同的记录；如果有该记录，就直接更新。 DELETE 首先检查 ADDRESS 表中是否有该记录，如果有该记录，就将其删除。 这种做法肯定是向前兼容的，因为 1.0 版本的应用直接忽略了 ADDRESS 表。\n数据复制大致有两种方法：可以通过数据库来触发数据复制，也可以通过应用程序来触发。即使要通过数据库来触发，也要由应用来创建相应的触发器。\n2. 继续滚动更新，标签改为 2.2。和上面相反，ADDRESS 表中的每一个变化，都复制一份到 PERSON 表中。这是因为上面一步的更新过程中，旧版本的应用可能还没来得及更新数据库就被杀死了，这一步可以确保数据完全同步。考虑到兼容性，2.1 版本的应用会继续使用 PERSON 表。\n3. 继续滚动更新，标签改为 2.3。更新过程中需要从 PERSON 表中删除多余的字段，最终变成上文所述的最终数据结构。从这一步回滚到上一步也是向前兼容的，因为 2.2 版本的应用的所有数据都来自 ADDRESS 表，2.3 版本只是删除了 PERSON 表中的某些字段，所以 2.2 版本的应用完全可以处理 2.3 版本应用的数据结构。\n总结 # 尽管滚动更新背后的原理非常简单，但很少有人能在生产环境中利用好它，因为大多数情况下我们都忘记了 deployment 回滚的兼容性。即使你解决了本文提出的问题，也会有新的问题涌现，这就是实现零宕机架构的成本，无法避免。\n关于零宕机的理论部分就讲到这里，想必大家都已经理解了，如果你想通过实际的项目来实践，可以参考下一篇文章： 在 Kubernetes 中实现零宕机部署 Spring Boot 应用。\n","date":"2019年4月26日","externalUrl":null,"permalink":"/posts/kubernetes-zero-downtime-deployment/","section":"博客","summary":"","title":"在 Kubernetes 中实现零宕机部署应用","type":"posts"},{"content":" 原文链接：kube-proxy Subtleties: Debugging an Intermittent Connection Reset 最近我一直被一个间歇性连接重置的 bug 所困扰，经过一段时间的调试之后，发现该 bug 是由几个不同的网络子系统联合导致的。通过这几天的深入挖掘和调试，我对 Kubernetes 的网络机制更加熟悉了，对此也有了一些经验总结，分享给社区。\n症状 # 最近我们收到了一份用户报告，声称他们在使用 ClusterIP 类型的 Service 将大型文件提供给在同一群集中运行的 Pod时，会出现连接重置的情况。初步调试之后，没有发现任何有效信息：网络连接很正常，下载文件也没有遇到任何问题。但当我们通过多个客户端并行运行多个工作负载时，该问题就重现了。神奇的是，如果你只使用虚拟机，不使用 Kubernetes，就不会遇到该问题。该问题可以通过一个 简单的 app 来复现，现在可以确定的是这肯定与 Kubernetes 的网络有关，但问题到底出在哪呢？\nKubernetes 网络基础 # 在深入剖析问题根源之前，我们先来复习一下 Kubernetes 的网络基础。Kubernetes 处理从 Pod 发出的网络流量的方式与目标主机有关，这里主要分为三种类型：\nPod 到 Pod # 在 Kubernetes 集群中，每个 Pod 都有自己的 IP 地址，运行在 Pod 内的应用都可以使用标准的端口号，不用重新映射到不同的随机端口号。所有的 Pod 之间都可以保持三层网络的连通性，比如可以相互 ping 对方，相互发送 TCP/UDP 数据包。 CNI 就是用来实现这些网络功能的标准接口，目前有很多网络插件都支持 CNI。\nPod 到集群外 # 从 Pod 内部到集群外部的流量，Kubernetes 会通过 SNAT 来处理。SNAT 做的工作就是将数据包的源从 Pod 内部的 IP:Port 替换为宿主机的 IP:Port，当数据包返回时，再将目的从宿主机的 IP:Port 替换为 Pod 内部的 IP:Port，然后再发送给 Pod。当然了，中间的整个过程对 Pod 来说是完全透明的，它们对地址转换不会有任何感知。\nPod 到 Service # Pod 的生命周期是很短暂的，但客户需要的是可靠的服务，所以 Kubernetes 引入了新的资源对象 Service，其实它就是是 Pod 前面的四层负载均衡器。Service 总共有四种类型，其中最常用的类型是 CLusterIP，这种类型的 Service 会自动分配一个仅 cluster 内部可以访问的虚拟 IP。\nKubernetes 通过 kube-proxy 组件来实现这些功能，每台计算节点上都运行一个 kube-proxy 服务，通过复杂的 iptables 规则在 Pod 和 Service 之间进行各种过滤和 NAT。如果你登入某个计算节点的终端输入 iptables-save，就会看到 kube-proxy 和其他程序在 iptables 规则表中插入的规则。其中最主要的链是 KUBE-SERVICES，KUBE-SVC-* 和 KUBE-SEP-*。\nKUBE-SERVICES 链是访问集群内服务的数据包入口点，它会根据匹配到的目标 IP:port 将数据包分发到相应的 KUBE-SVC-* 链。 KUBE-SVC-* 链相当于一个负载均衡器，它会将数据包平均分发到 KUBE-SEP-* 链。每个 KUBE-SVC-* 链后面的 KUBE-SEP-* 链都和 Service 后面的 Endpoint 数量一样。 KUBE-SEP-* 链通过 DNAT 将目标从 Service 的 IP:port 替换为 Endpoint 的 IP:port，从而将流量转发到相应的 Pod。 所有在内核中由 Netfilter 的特定框架做的连接跟踪模块称作 conntrack（connection tracking）。在 DNAT 的过程中，conntrack 使用状态机来启动并跟踪连接状态。为什么需要记录连接的状态呢？因为 iptables 需要记住数据包的目标地址被改成了什么，并且在返回数据包时再将目标地址改回来。除此之外 iptables 还可以依靠 conntrack 的状态（cstate）来决定数据包的命运。其中最主要的四个 conntrack 状态是：\nNEW : 匹配连接的第一个包，这表示 conntrack 对该数据包的信息一无所知。通常发生在收到 SYN 数据包时。 ESTABLISHED : 匹配连接的响应包及后续的包，conntrack 知道该数据包属于一个已建立的连接。通常发生在 TCP 握手完成之后。 RELATED : RELATED 状态有点复杂，当一个连接与另一个已经是 ESTABLISHED 的连接有关时，这个连接就被认为是 RELATED。这意味着，一个连接要想成为 RELATED，必须首先有一个已经是 ESTABLISHED 的连接存在。这个 ESTABLISHED 连接再产生一个主连接之外的新连接，这个新连接就是 RELATED 状态了。 INVALID : 匹配那些无法识别或没有任何状态的数据包，conntrack 不知道如何去处理它。该状态在分析 Kubernetes 故障的过程中起着重要的作用。 TCP 连接在 Pod 和 Service 之间的工作流程如下图所示：\nTCP 连接的生命周期：\n左边的客户端发送数据包到 Service：192.168.0.2:80 数据包通过本地节点的 iptables 规则，目的地址被改为 Pod 的地址：10.0.1.2:80 提供服务的 Pod（Server Pod）处理完数据包后返回响应包给客户端：10.0.0.2 数据包到达客户端所在的节点后，被 conntrack 模块识别并将源地址改为 192.169.0.2:80 客户端接收到响应包 整个流程看起来工作的很完美。\n导致连接重置的原因是什么？ # 尽管 TCP 连接的工作过程看起来很完美，但在 Kubernetes 集群中还是遇到了连接重置的问题，到底是为什么呢？\n如下图所示，我们将数据包的生命周期分为 5 个阶段，问题就出在第三阶段。当 conntrack 不能识别返回的包时，就会将其标记为 INVALID 状态，包括以下几种情况：由于内存溢出，conntrack 无法继续跟踪连接；数据包超过了 TCP 窗口长度；等等。被 conntrack 标记为 INVALID 的数据包，没有相应的 iptables 规则来丢弃它，所以会被转发到客户端，但源地址没有被修改（图中的第4阶段）。因为该响应包的源 IP 是 Pod 的 IP，不是 Service 的 IP，所以客户端无法识别该响应包。这时客户端会说：“等一下，我不记得和这个 IP 有过任何连接，为什么这个家伙要向我发送这个数据包？” 然后客户端就会发送一个 RST 包给服务端的 Pod，也就是图中的第 5 阶段。不幸的是，这是 Pod 到 Pod 之间的合法数据包，会被安全送达服务端的 Pod。服务端 Pod 并不知道 DNAT 的过程，从它的视角来看，数据包 5 和 数据包 2 与 3 一样是合法的，现在服务端 Pod 只知道：“客户端准备跑路了，不想和我继续通信了，那我们就关闭连接吧！” 当然，如果想要正常关闭 TCP 连接，RST 包必须也是合法的，比如要使用正确的 TCP 序列号等。协商完成后，客户端与服务端都各自关闭了连接。\n如何避免连接重置？ # 现在我们已经找到了问题的根源，解决起来就没那么困难了。有两种方法可以避免连接重置：\n给 conntrack 提供更多的自由，让它无论什么情况下都不会将数据包标记为 INVALID。可以通过以下命令来实现：echo 1 \u0026gt; /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_be_liberal。 添加一个 iptables 规则来丢弃被标记为 INVALID 的数据包，这样数据包就不会到达客户端，也不会造成连接重置。 该 fix 已经开始起草（ https://github.com/kubernetes/kubernetes/pull/74840），但还没有合并到 v1.14 版本中。我这边提供了一种比较便利的方法在集群内所有的节点上应用此规则，只需要创建一个 Deamonset 就可以了：\napiVersion: extensions/v1beta1 kind: DaemonSet metadata: name: startup-script labels: app: startup-script spec: template: metadata: labels: app: startup-script spec: hostPID: true containers: - name: startup-script image: gcr.io/google-containers/startup-script:v1 imagePullPolicy: IfNotPresent securityContext: privileged: true env: - name: STARTUP_SCRIPT value: | #! /bin/bash echo 1 \u0026gt; /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_be_liberal echo done 总结 # 很显然，这个 bug 已经存在很长时间了，让我惊讶的是，这么长时间都没人注意到这个问题，直到最近才被发现。我觉得原因有 2：\n这个问题通常出现在负载很高导致服务端阻塞的情况中，这不是一个常规现象，比较少见。 应用层的重试可以容忍这种连接重置。 总之，无论 Kubernetes 发展得有多快，它仍然还是一个很年轻的项目。要想让 Kubernetes 真正变成运行应用程序的最佳平台，没有别的办法，只有不断聆听客户的反馈，不把任何事情看成理所当然，不断深入挖掘和优化。\n特别感谢 bowei 在我调试和写文章的过程中提供的咨询帮助，感谢 tcarmet 反馈该 bug 并提供了复现方法。\n","date":"2019年4月4日","externalUrl":null,"permalink":"/posts/kube-proxy-subtleties-debugging-an-intermittent-connection-reset/","section":"博客","summary":"","title":"当 kube-proxy 遇到连接重置","type":"posts"},{"content":"","date":"2019年4月1日","externalUrl":null,"permalink":"/tags/macvlan/","section":"标签","summary":"","title":"Macvlan","type":"tags"},{"content":"通过 上篇文章的学习，我们已经知道 Macvlan 四种模式的工作原理，其中最常用的就是 Bridge 模式，本文我们将通过实验来验证 Macvlan Bridge 模式的连通性。\nMacvlan 是 linux 内核比较新的特性，可以通过以下方法判断当前系统是否支持：\n$ modprobe macvlan $ lsmod | grep macvlan macvlan 19233 0 如果第一个命令报错，或者第二个命令没有返回，则说明当前系统不支持 Macvlan，需要升级系统或者升级内核。\n各个 Linux 发行版对 Macvlan 的支持 # Macvlan 对 Kernel 版本依赖：Linux kernel v3.9–3.19 and 4.0+。几个重要发行版支持情况：\nubuntu：\u0026gt;= saucy(13.10) RHEL(Red Hat Enterprise Linux): \u0026gt;= 7.0(3.10.0) Fedora: \u0026gt;=19(3.9) Debian: \u0026gt;=8(3.16) 各个发行版的内核都可以自行手动升级，具体操作可以参考官方提供的文档。\n以上版本信息参考了这些资料：\nList of ubuntu versions with corresponding linux kernel version Red Hat Enterprise Linux Release Dates 实验环境 # 后面的测试将会在以下环境进行：\nOS hostname 物理网卡 IP Gateway CentOS 7.3 node1 ens160 192.168.179.9/16 192.168.1.1 CentOS 7.3 node2 ens160 192.168.179.10/16 192.168.1.1 我的本地操作系统为 MacOS，IP 为 10.8.0.241，网关为 10.8.0.1。\n连通性测试 # 下面开始对 Bridge 模式下 Macvlan 的连通性进行测试。\n首先在 node1 上创建两个 network namespace：\n# 开启混杂模式 $ ip link set ens160 promisc on $ ip netns add ns1 $ ip netns add ns2 然后创建 Macvlan 接口:\n$ ip link add link ens160 mac1 type macvlan mode bridge 创建的格式为 ip link add link \u0026lt;PARENT\u0026gt; \u0026lt;NAME\u0026gt; type macvlan mode \u0026lt;MODE\u0026gt;，其中 \u0026lt;PARENT\u0026gt; 是 Macvlan 接口的父接口名称，\u0026lt;NAME\u0026gt; 是新建的 Macvlan 接口的名称，这个名字可以任意取，\u0026lt;MODE\u0026gt; 是 Macvlan 的模式。\n可以查看创建接口的详细信息：\n$ ip -d link show mac1 13: mac1@ens160: \u0026lt;BROADCAST,MULTICAST\u0026gt; mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000 link/ether 5a:94:85:a6:96:95 brd ff:ff:ff:ff:ff:ff promiscuity 0 macvlan mode bridge addrgenmode eui64 下面就是把创建的 Macvlan 接口放到 network namespace 中，配置好 IP 地址，然后启用它：\n$ ip link set mac1 netns ns1 $ ip netns exec ns1 ip addr add 192.168.179.12/16 dev mac1 $ ip netns exec ns1 ip link set dev mac1 up 同理可以配置另外一个 Macvlan 接口：\n$ ip link add link ens160 mac2 type macvlan mode bridge $ ip link set mac2 netns ns2 $ ip netns exec ns2 ip addr add 192.168.179.13/16 dev mac2 $ ip netns exec ns2 ip link set dev mac2 up 可以测试两个 IP 的连通性：\nns1 \u0026ndash;\u0026gt; ns2 # $ ip netns exec ns1 ping -c 3 192.168.179.13 PING 192.168.179.13 (192.168.179.13) 56(84) bytes of data. 64 bytes from 192.168.179.13: icmp_seq=1 ttl=64 time=0.090 ms 64 bytes from 192.168.179.13: icmp_seq=2 ttl=64 time=0.061 ms --- 192.168.179.13 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1000ms rtt min/avg/max/mdev = 0.061/0.075/0.090/0.016 ms ns2 \u0026ndash;\u0026gt; ns1 # $ ip netns exec ns2 ping -c 2 192.168.179.12 PING 192.168.179.12 (192.168.179.12) 56(84) bytes of data. 64 bytes from 192.168.179.12: icmp_seq=1 ttl=64 time=0.059 ms 64 bytes from 192.168.179.12: icmp_seq=2 ttl=64 time=0.043 ms --- 192.168.179.12 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1000ms rtt min/avg/max/mdev = 0.043/0.051/0.059/0.008 ms ns1 \u0026ndash;\u0026gt; 192.168/16 # 首先测试 ns1 与 node2 的连通性：\n$ ip netns exec ns1 ping -c 2 192.168.179.10 PING 192.168.179.10 (192.168.179.10) 56(84) bytes of data. 64 bytes from 192.168.179.10: icmp_seq=1 ttl=64 time=0.976 ms 64 bytes from 192.168.179.10: icmp_seq=2 ttl=64 time=0.430 ms --- 192.168.179.10 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 0.430/0.703/0.976/0.273 ms 下面测试 ns1 与 node2 中 network namespace 的连通性。\n先在 node2 中配置一个 Macvlan 接口：\n[root@node2 ~]# ip link set ens160 promisc on [root@node2 ~]# ip netns add ns1 [root@node2 ~]# ip link add link ens160 mac1 type macvlan mode bridge [root@node2 ~]# ip link set mac1 netns ns1 [root@node2 ~]# ip netns exec ns1 ip addr add 192.168.179.14/16 dev mac1 [root@node2 ~]# ip link set dev mac1 up 测试 node1 的 ns1 与 node2 的 ns1 的连通性：\n[root@node1 ~]# ip netns exec ns1 ping -c 2 192.168.179.14 PING 192.168.179.14 (192.168.179.14) 56(84) bytes of data. 64 bytes from 192.168.179.14: icmp_seq=1 ttl=64 time=0.976 ms 64 bytes from 192.168.179.14: icmp_seq=2 ttl=64 time=0.430 ms --- 192.168.179.14 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 0.430/0.703/0.976/0.273 ms 10.8/16 \u0026ndash;\u0026gt; ns1 # # 在本地的 MacOS 客户端 ping 192.168.179.12 $ ping 192.168.179.12 -c 2 PING 192.168.179.12 (192.168.179.12): 56 data bytes Request timeout for icmp_seq 0 --- 192.168.179.12 ping statistics --- 2 packets transmitted, 0 packets received, 100.0% packet loss 发现跨三层网段是 ping 不通的。这个问题很好解决，我们刚刚给 ns1 和 ns2 分配 IP 的时候并没有指定默认路由，指定个默认路由问题就迎刃而解了。\n$ ip netns exec ns1 ip route add default via 192.168.1.1 dev mac1 如果你想开发 Macvlan cni 插件，这个地方需要注意一下，每次给 Pod 分配好 IP 以后要添加一条默认路由指向网关，不然无法跨三层通信。 ns1 \u0026ndash;\u0026gt; ens160 # $ ip netns exec ns1 ping -c 2 192.168.179.9 PING 192.168.179.9 (192.168.179.9) 56(84) bytes of data. --- 192.168.179.9 ping statistics --- 2 packets transmitted, 0 received, 100% packet loss, time 999ms 这里就遇到了我在 上一篇文章开头提到的问题。到目前为止，整个实验的拓扑结构如下：\n其实也很好解决，额外创建一个 Macvlan 子接口，并把 ens160 的 IP 分给这个子接口，最后还要修改默认路由。\n$ ip link add link ens160 mac0 type macvlan mode bridge # 下面的命令一定要放在一起执行，否则中间会失去连接 $ ip addr del 192.168.179.9/16 dev ens160 \u0026amp;\u0026amp; \\ ip addr add 192.168.179.9/16 dev mac0 \u0026amp;\u0026amp; \\ ip link set dev mac0 up \u0026amp;\u0026amp; \\ ip route flush dev ens160 \u0026amp;\u0026amp; \\ ip route flush dev mac0 \u0026amp;\u0026amp; \\ ip route add 192.168.0.0/16 dev mac0 metric 0 \u0026amp;\u0026amp; \\ ip route add default via 192.168.1.1 dev mac0 \u0026amp; 这里一定不能 Down 掉 ens160，否则所有的子接口都将无法工作。 现在就能 ping 通了：\n$ ip netns exec ns1 ping -c 2 192.168.179.9 PING 192.168.179.9 (192.168.179.9) 56(84) bytes of data. 64 bytes from 192.168.179.9: icmp_seq=1 ttl=64 time=0.137 ms 64 bytes from 192.168.179.9: icmp_seq=2 ttl=64 time=0.078 ms --- 192.168.179.9 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 999ms rtt min/avg/max/mdev = 0.078/0.107/0.137/0.031 ms ","date":"2019年4月1日","externalUrl":null,"permalink":"/posts/macvlan-in-action/","section":"博客","summary":"通过 上篇文章的学习，我们已经知道 Macvlan 四种模式的工作原理，其中最","title":"Macvlan 网络方案实践","type":"posts"},{"content":" Macvlan 简介 # 在 Macvlan 出现之前，我们只能为一块以太网卡添加多个 IP 地址，却不能添加多个 MAC 地址，因为 MAC 地址正是通过其全球唯一性来标识一块以太网卡的，即便你使用了创建 ethx:y 这样的方式，你会发现所有这些“网卡”的 MAC 地址和 ethx 都是一样的，本质上，它们还是一块网卡，这将限制你做很多二层的操作。有了 Macvlan 技术，你可以这么做了。\nMacvlan 允许你在主机的一个网络接口上配置多个虚拟的网络接口，这些网络 interface 有自己独立的 MAC 地址，也可以配置上 IP 地址进行通信。Macvlan 下的虚拟机或者容器网络和主机在同一个网段中，共享同一个广播域。Macvlan 和 Bridge 比较相似，但因为它省去了 Bridge 的存在，所以配置和调试起来比较简单，而且效率也相对高。除此之外，Macvlan 自身也完美支持 VLAN。\n同一 VLAN 间数据传输是通过二层互访，即 MAC 地址实现的，不需要使用路由。不同 VLAN 的用户单播默认不能直接通信，如果想要通信，还需要三层设备做路由，Macvlan 也是如此。用 Macvlan 技术虚拟出来的虚拟网卡，在逻辑上和物理网卡是对等的。物理网卡也就相当于一个交换机，记录着对应的虚拟网卡和 MAC 地址，当物理网卡收到数据包后，会根据目的 MAC 地址判断这个包属于哪一个虚拟网卡。**这也就意味着，只要是从 Macvlan 子接口发来的数据包（或者是发往 Macvlan 子接口的数据包），物理网卡只接收数据包，不处理数据包，所以这就引出了一个问题：本机 Macvlan 网卡上面的 IP 无法和物理网卡上面的 IP 通信！**关于这个问题的解决方案我们下一节再讨论。\n我们先来看一下 Macvlan 技术的流程示意图：\n简单来说，Macvlan 虚拟网卡设备是寄生在物理网卡设备上的。发包时调用自己的发包函数，查找到寄生的物理设备，然后通过物理设备发包。收包时，通过注册寄生的物理设备的 rx_handler 回调函数，处理数据包。\nMacvlan vs Bridge # 说到 Macvlan，就不得不提 Bridge，因为你可以把 Macvlan 看成一个简单的 Bridge。但他们之间还是有很大的区别的。\nBridge # Bridge 实际上就是一种旧式交换机，他们之间并没有很大的差别。Bridge 与交换机的区别在与市场，而不在与技术。交换机对网络进行分段的方式与 Bridge 相同，交换机就是一个多端口的网桥。确切地说，高端口密度的 Bridge 就称为局域网交换机。\nBridge 有以下特点：\nBridge 是二层设备，仅用来处理二层的通讯。 Bridge 使用 MAC 地址表来决定怎么转发帧（Frame）。 Bridge 会从 host 之间的通讯数据包中学习 MAC 地址。 可以是硬件设备，也可以是纯软件实现(例如：Linux Bridge)。 以下是一个在 Linux 主机上，多个 VM 使用 bridge 相互通讯的状况：\nLinux 主机中可以通过命令行工具 brctl 来查看 Bridge 的配置，该工具可以通过安装软件包 bridge-utils 来获得。\n$ brctl show bridge name bridge id STP enabled interfaces br0 8000.080006ad34d1 no eth0 veth0 br1 8000.080021d2a187 no veth1 veth2 Bridge 有可能会遇到二层环路，如有需要，你可以开启 STP 来防止出现环路。 Macvlan # Macvlan 有以下特点：\n可让使用者在同一张实体网卡上设定多个 MAC 地址。 承上，带有上述设定的 MAC 地址的网卡称为子接口（sub interface）；而实体网卡则称为父接口（parent interface）。 parent interface 可以是一个物理接口（eth0），可以是一个 802.1q 的子接口（eth0.10），也可以是 bonding 接口。 可在 parent/sub interface 上设定的不只是 MAC 地址，IP 地址同样也可以被设定。 sub interface 无法直接与 parent interface 通讯 (带有 sub interface 的 VM 或容器无法与 host 直接通讯)。 承上，若 VM 或容器需要与 host 通讯，那就必须额外建立一个 sub interface 给 host 用。 sub interface 通常以 mac0@eth0 的形式来命名以方便区別。 用张图来解释一下设定 Macvlan 后的样子：\nMacvlan 的工作模式 # Macvlan 共支持四种模式，分别是：\nVEPA（Virtual Ethernet Port Aggregator） # 在 VEPA 模式下，所有从 Macvlan 接口发出的流量，不管目的地全部都发送给父接口，即使流量的目的地是共享同一个父接口的其它 Macvlan 接口。在二层网络场景下，由于生成树协议的原因，两个 Macvlan 接口之间的通讯会被阻塞，这时需要上层路由器上为其添加路由（需要外部交换机配置 Hairpin 支持，即需要兼容 802.1Qbg 的交换机支持，其可以把源和目的地址都是本地 Macvlan 接口地址的流量发回给相应的接口）。此模式下从父接口收到的广播包，会泛洪给 VEPA 模式的所有子接口。\n现在大多数交换机都不支持 Hairpin 模式，但 Linux 主机中可以通过一种 Harpin 模式的 Bridge 来让 VEPA 模式下的不同 Macvlan 接口通信(前文已经提到，Bridge 其实就是一种旧式交换机)。怎么配置呢？非常简单，通过一条命令就可以解决：\n$ brctl hairpin br0 eth1 on 或者使用 iproute2 来设置：\n$ bridge link set dev eth0 hairpin on 如果你的内核是你手工编译升级的，那么可能你的用户态程序并不支持新内核对应的所有特性，也就是说你的 brctl 可能版本过老不支持 hairpin 命令，那么可以 sysfs 来搞定：\n$ echo 1 \u0026gt;/sys/class/net/br0/brif/eth1/hairpin_mode 在 Linux 主机上配置了 Harpin 模式之后，源和目的地址都是本地 Macvlan 接口地址的流量，都会被 br0（假设你创建的 Bridge 是 br0）发回给相应的接口。\n如果想在物理交换机层面对虚拟机或容器之间的访问流量进行优化设定，VEPA 模式将是一种比较好的选择。\nVEPA 和 Passthru 模式下，两个 Macvlan 接口之间的通信会经过主接口两次：第一次是发出的时候，第二次是返回的时候。这样会影响物理接口的宽带，也限制了不同 Macvlan 接口之间通信的速度。如果多个 Macvlan 接口之间通信比较频繁，对于性能的影响会比较明显。 Bridge # 此种模式类似 Linux 的 Bridge，拥有相同父接口的两块 Macvlan 虚拟网卡是可以直接通讯的，不需要把流量通过父网卡发送到外部网络，广播帧将会被泛洪到连接在\u0026quot;网桥\u0026quot;上的所有其他子接口和物理接口。这比较适用于让共享同一个父接口的 Macvlan 网卡进行直接通讯的场景。\n这里所谓的 Bridge 指的是在这些网卡之间，数据流可以实现直接转发，不需要外部的协助，这有点类似于 Linux host 内建了一个 Bridge，即用 brctl 命令所做的那一切。但和 Linux bridge 绝不是一回事，它不需要学习 MAC 地址，也不需要 STP，因此效能比起使用 Linux bridge 好上很多。\nBridge 模式有个缺点：如果父接口 down 掉，所有的 Macvlan 子接口也会全部 down 掉，同时子接口之间也将无法进行通讯。 Private # 此种模式相当于 VEPA 模式的增强模式，其完全阻止共享同一父接口的 Macvlan 虚拟网卡之间的通讯，即使配置了 Hairpin 让从父接口发出的流量返回到宿主机，相应的通讯流量依然被丢弃。具体实现方式是丢弃广播/多播数据，这就意味着以太网地址解析 arp 将不可运行，除非手工探测 MAC 地址，否则通信将无法在同一宿主机下的多个 Macvlan 网卡间展开。之所以隔离广播流量，是因为以太网是基于广播的，隔离了广播，以太网将失去了依托。\nPassthru # 此种模式会直接把父接口和相应的MacVLAN接口捆绑在一起，这种模式每个父接口只能和一个 Macvlan 虚拟网卡接口进行捆绑，并且 Macvlan 虚拟网卡接口继承父接口的 MAC 地址。\n此种模式的优点是虚拟机和容器可以更改 MAC 地址和其它一些接口参。\nMacvlan 和 Bridge 的使用场景 # 最后我们再来讨论一下 Macvlan 和 Bridge 的各自使用场景。\n使用 Macvlan :\n仅仅需要为虚拟机或容器提供访问外部物理网络的连接。 Macvlan 占用较少的 CPU，同时提供较高的吞吐量。 当使用 Macvlan 时，宿主机无法和 VM 或容器直接进行通讯。 使用 Bridge :\n当在同一台宿主机上需要连接多个虚拟机或容器时。 对于拥有多个网桥的混合环境。 需要应用高级流量控制，FDB的维护。 Macvlan 的局限性 # Macvlan 是将 VM 或容器通过二层连接到物理网络的近乎理想的方案，但它也有一些局限性：\nLinux 主机连接的交换机可能会限制同一个物理端口上的 MAC 地址数量。虽然你可以让网络管理员更改这些策略，但有时这种方法是无法实行的（比如你要去给客户做一个快速的 PoC 演示）。 许多 NIC 也会对该物理网卡上的 MAC地址数量有限制。超过这个限制就会影响到系统的性能。 IEEE 802.11 不喜欢同一个客户端上有多个 MAC 地址，这意味着你的 Macvlan 子接口在无线网卡或 AP 中都无法通信。可以通过复杂的办法来突破这种限制，但还有一种更简单的办法，那就是使用 Ipvlan，感兴趣可以自己查阅相关资料。 总结 # 本文主要介绍了 Macvlan 的实现原理，比较了它和 Linux Bridge 模式之间的差异及其使用场景，还详细剖析了 Macvlan 四种模式的工作原理和相关注意项。下一节我们将通过实际演练来模拟 Macvlan 的四种工作模式。\n参考资料 # Bridge vs Macvlan 图解几个与Linux网络虚拟化相关的虚拟网卡-VETH/MACVLAN/MACVTAP/IPVLAN iproute2/iplink: add macvlan options for bridge mode ","date":"2019年3月25日","externalUrl":null,"permalink":"/posts/netwnetwork-virtualization-macvlan/","section":"博客","summary":"Macvlan 简介 # 在 Macvlan 出现之前，我们只能为一块以太网卡添加多个 IP 地址，","title":"Linux 虚拟网卡技术：Macvlan","type":"posts"},{"content":"Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致，它们就是 Kubernetes 的大脑。例如，ReplicaSet 控制器负责维护集群中运行的 Pod 数量；Node 控制器负责监控节点的状态，并在节点出现故障时及时做出响应。总而言之，在 Kubernetes 中，每个控制器只负责某种类型的特定资源。对于集群管理员来说，了解每个控制器的角色分工至关重要，如有必要，你还需要深入了解控制器的工作原理。\n本文我将会带你深入了解 Kubernetes 控制器的内部结构、基本组件以及它的工作原理。本文使用的所有代码都是从 Kubernetes 控制器的当前实现代码中提取的，基于 Go 语言的 client-go 库。\n控制器的模型 # Kubernetes 官方文档给出了控制器最完美的解释：\nIn applications of robotics and automation, a control loop is a non-terminating loop that regulates the state of the system. In Kubernetes, a controller is a control loop that watches the shared state of the cluster through the API server and makes changes attempting to move the current state towards the desired state. Examples of controllers that ship with Kubernetes today are the replication controller, endpoints controller, namespace controller, and serviceaccounts controller.\n翻译：\n在机器人设计和自动化的应用中，控制循环是一个用来调节系统状态的非终止循环。而在 Kubernetes 中，控制器就是前面提到的控制循环，它通过 API Server 监控整个集群的状态，并确保集群处于预期的工作状态。Kubernetes 自带的控制器有 ReplicaSet 控制器，Endpoint 控制器，Namespace 控制器和 Service Account 控制器等。\n官方文档： Kube-controller-manager\nKubernetes 控制器会监视资源的创建/更新/删除事件，并触发 Reconcile 函数作为响应。整个调整过程被称作 “Reconcile Loop”（调谐循环）或者 “Sync Loop”（同步循环）。Reconcile 是一个使用 object（Resource 的实例）的命名空间和 object 名来调用的函数，使 object 的实际状态与 object 的 Spec 中定义的状态保持一致。调用完成后，Reconcile 会将 object 的状态更新为当前实际状态。\n什么时候才会触发 Reconcile 函数呢？以 ReplicaSet 控制器为例，当收到了一个关于 ReplicaSet 的事件或者关于 ReplicaSet 创建 Pod 的事件时，就会触发 Reconcile 函数。\n为了降低复杂性，Kubernetes 将所有的控制器都打包到 kube-controller-manager 这个守护进程中。下面是控制器最简单的实现方式：\nfor { desired := getDesiredState() current := getCurrentState() makeChanges(desired, current) } 水平触发的 API # Kubernetes 的 API 和控制器都是基于水平触发的，可以促进系统的自我修复和周期协调。水平触发这个概念来自硬件的中断，中断可以是水平触发，也可以是边缘触发。\n水平触发 : 系统仅依赖于当前状态。即使系统错过了某个事件（可能因为故障挂掉了），当它恢复时，依然可以通过查看信号的当前状态来做出正确的响应。 边缘触发 : 系统不仅依赖于当前状态，还依赖于过去的状态。如果系统错过了某个事件（“边缘”），则必须重新查看该事件才能恢复系统。 Kubernetes 水平触发的 API 实现方式是：监视系统的实际状态，并与对象的 Spec 中定义的期望状态进行对比，然后再调用 Reconcile 函数来调整实际状态，使之与期望状态相匹配。\n水平触发的 API 也叫声明式 API。 水平触发的 API 有以下几个特点：\nReconcile 会跳过中间过程在 Spec 中声明的值，直接作用于当前 Spec 中声明的值。 在触发 Reconcile 之前，控制器会并发处理多个事件，而不是串行处理每个事件。 举两个例子：\n例 1：并发处理多个事件\n用户创建了 1000 个副本数的 ReplicaSet，然后 ReplicaSet 控制器会创建 1000 个 Pod，并维护 ReplicaSet 的 Status 字段。在水平触发系统中，控制器会在触发 Reconcile 之前并发更新所有 Pod（Reconcile 函数仅接收对象的 Namespace 和 Name 作为参数），只需要更新 Status 字段 1 次。而在边缘触发系统中，控制器会串行响应每个 Pod 事件，这样就会更新 Status 字段 1000 次。\n例 2：跳过中间状态\n用户修改了某个 Deployment 的镜像，然后进行回滚。在回滚过程中发现容器陷入 crash 循环，需要增加内存限制。然后用户更新了 Deployment 的内容，调整内存限制，重新开始回滚。在水平触发系统中，控制器会立即停止上一次回滚动作，开始根据最新值进行回滚。而在边缘触发系统中，控制器必须等上一次回滚操作完成才能进行下一次回滚。\n控制器的内部结构 # 每个控制器内部都有两个核心组件：Informer/SharedInformer 和 Workqueue。其中 Informer/SharedInformer 负责 watch Kubernetes 资源对象的状态变化，然后将相关事件（evenets）发送到 Workqueue 中，最后再由控制器的 worker 从 Workqueue 中取出事件交给控制器处理程序进行处理。\n事件 = 动作（create, update 或 delete） + 资源的 key（以 namespace/name 的形式表示） Informer # 控制器的主要作用是 watch 资源对象的当前状态和期望状态，然后发送指令来调整当前状态，使之更接近期望状态。为了获得资源对象当前状态的详细信息，控制器需要向 API Server 发送请求。\n但频繁地调用 API Server 非常消耗集群资源，因此为了能够多次 get 和 list 对象，Kubernetes 开发人员最终决定使用 client-go 库提供的缓存机制。控制器并不需要频繁调用 API Server，只有当资源对象被创建，修改或删除时，才需要获取相关事件。client-go 库提供了 Listwatcher 接口用来获得某种资源的全部 Object，缓存在内存中；然后，调用 Watch API 去 watch 这种资源，去维护这份缓存；最后就不再调用 Kubernetes 的任何 API :\nlw := cache.NewListWatchFromClient( client, \u0026amp;v1.Pod{}, api.NamespaceAll, fieldSelector) 上面的这些所有工作都是在 Informer 中完成的，Informer 的数据结构如下所示：\nstore, controller := cache.NewInformer { \u0026amp;cache.ListWatch{}, \u0026amp;v1.Pod{}, resyncPeriod, cache.ResourceEventHandlerFuncs{}, 尽管 Informer 还没有在 Kubernetes 的代码中被广泛使用（目前主要使用 SharedInformer，下文我会详述），但如果你想编写一个自定义的控制器，它仍然是一个必不可少的概念。\n你可以把 Informer 理解为 API Server 与控制器之间的事件代理，把 Workqueue 理解为存储事件的数据结构。 下面是用于构造 Informer 的三种模式：\nListWatcher # ListWatcher 是对某个特定命名空间中某个特定资源的 list 和 watch 函数的集合。这样做有助于控制器只专注于某种特定资源。fieldSelector 是一种过滤器，它用来缩小资源搜索的范围，让控制器只检索匹配特定字段的资源。Listwatcher 的数据结构如下所示：\ncache.ListWatch { listFunc := func(options metav1.ListOptions) (runtime.Object, error) { return client.Get(). Namespace(namespace). Resource(resource). VersionedParams(\u0026amp;options, metav1.ParameterCodec). FieldsSelectorParam(fieldSelector). Do(). Get() } watchFunc := func(options metav1.ListOptions) (watch.Interface, error) { options.Watch = true return client.Get(). Namespace(namespace). Resource(resource). VersionedParams(\u0026amp;options, metav1.ParameterCodec). FieldsSelectorParam(fieldSelector). Watch() } } Resource Event Handler # Resource Event Handler 用来处理相关资源发生变化的事件：\ntype ResourceEventHandlerFuncs struct { AddFunc func(obj interface{}) UpdateFunc func(oldObj, newObj interface{}) DeleteFunc func(obj interface{}) } AddFunc : 当资源创建时被调用 UpdateFunc : 当已经存在的资源被修改时就会调用 UpdateFunc。oldObj 表示资源的最近一次已知状态。如果 Informer 向 API Server 重新同步，则不管资源有没有发生更改，都会调用 UpdateFunc。 DeleteFunc : 当已经存在的资源被删除时就会调用 DeleteFunc。该函数会获取资源的最近一次已知状态，如果无法获取，就会得到一个类型为 DeletedFinalStateUnknown 的对象。 ResyncPeriod # ResyncPeriod 用来设置控制器遍历缓存中的资源以及执行 UpdateFunc 的频率。这样做可以周期性地验证资源的当前状态是否与期望状态匹配。\n如果控制器错过了 update 操作或者上一次操作失败了，ResyncPeriod 将会起到很大的弥补作用。如果你想编写自定义控制器，不要把周期设置太短，否则系统负载会非常高。\nSharedInformer # 通过上文我们已经了解到，Informer 会将资源缓存在本地以供自己后续使用。但 Kubernetes 中运行了很多控制器，有很多资源需要管理，难免会出现以下这种重叠的情况：一个资源受到多个控制器管理。\n为了应对这种场景，可以通过 SharedInformer 来创建一份供多个控制器共享的缓存。这样就不需要再重复缓存资源，也减少了系统的内存开销。使用了 SharedInformer 之后，不管有多少个控制器同时读取事件，SharedInformer 只会调用一个 Watch API 来 watch 上游的 API Server，大大降低了 API Server 的负载。实际上 kube-controller-manager 就是这么工作的。\nSharedInformer 提供 hooks 来接收添加、更新或删除某个资源的事件通知。还提供了相关函数用于访问共享缓存并确定何时启用缓存，这样可以减少与 API Server 的连接次数，降低 API Server 的重复序列化成本和控制器的重复反序列化成本。\nlw := cache.NewListWatchFromClient(…) sharedInformer := cache.NewSharedInformer(lw, \u0026amp;api.Pod{}, resyncPeriod) Workqueue # 由于 SharedInformer 提供的缓存是共享的，所以它无法跟踪每个控制器，这就需要控制器自己实现排队和重试机制。因此，大多数 Resource Event Handler 所做的工作只是将事件放入消费者工作队列中。\n每当资源被修改时，Resource Event Handler 就会放入一个 key 到 Workqueue 中。key 的表示形式为 \u0026lt;resource_namespace\u0026gt;/\u0026lt;resource_name\u0026gt;，如果提供了 \u0026lt;resource_namespace\u0026gt;，key 的表示形式就是 \u0026lt;resource_name\u0026gt;。每个事件都以 key 作为标识，因此每个消费者（控制器）都可以使用 workers 从 Workqueue 中读取 key。所有的读取动作都是串行的，这就保证了不会出现两个 worker 同时读取同一个 key 的情况。\nWorkqueue 在 client-go 库中的位置为 client-go/util/workqueue，支持的队列类型包括延迟队列，定时队列和速率限制队列。下面是速率限制队列的一个示例：\nqueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) Workqueue 提供了很多函数来处理 key，每个 key 在 Workqueue 中的生命周期如下图所示：\n如果处理事件失败，控制器就会调用 AddRateLimited() 函数将事件的 key 放回 Workqueue 以供后续重试（如果重试次数没有达到上限）。如果处理成功，控制器就会调用 Forget() 函数将事件的 key 从 Workqueue 中移除。注意：该函数仅仅只是让 Workqueue 停止跟踪事件历史，如果想从 Workqueue 中完全移除事件，需要调用 Done() 函数。\n现在我们知道，Workqueue 可以处理来自缓存的事件通知，但还有一个问题 :** 控制器应该何时启用 workers 来处理 Workqueue 中的事件呢？**\n控制器需要等到缓存完全同步到最新状态才能开始处理 Workqueue 中的事件，主要有两个原因：\n在缓存完全同步之前，获取的资源信息是不准确的。 对单个资源的多次快速更新将由缓存合并到最新版本中，因此控制器必须等到缓存变为空闲状态才能开始处理事件，不然只会把时间浪费在等待上。 这种做法的伪代码如下：\ncontroller.informer = cache.NewSharedInformer(...) controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) controller.informer.Run(stopCh) if !cache.WaitForCacheSync(stopCh, controller.HasSynched) { log.Errorf(\u0026#34;Timed out waiting for caches to sync\u0026#34;)) } // Now start processing controller.runWorker() 所有处理流程如下所示：\n控制器处理事件的流程\n参考资料 # A Deep Dive Into Kubernetes Controllers ","date":"2019年3月11日","externalUrl":null,"permalink":"/posts/a-deep-dive-into-kubernetes-controllers/","section":"博客","summary":"","title":"Kubernetes 控制器的工作原理解读","type":"posts"},{"content":"","date":"2019年3月9日","externalUrl":null,"permalink":"/tags/vector/","section":"标签","summary":"","title":"向量","type":"tags"},{"content":"","date":"2019年3月9日","externalUrl":null,"permalink":"/categories/math/","section":"分类","summary":"","title":"数学","type":"categories"},{"content":" 我在 2016 年的时候写过一篇关于 向量的叉乘与行列式的文章，没想到过去这么久了广大网友呼声还这么高，基于很多大学生的需求，我决定帮你们啃下难啃的高数和线性代数，让你们从根本上理解这两门课，而不是只知道背公式。到时候别忘了给我发红包哦~~~\n今天我们来讲讲矩阵的乘法。当然了，我告诉你的肯定不是大学教科书上那些填鸭式的云里雾里的计算规则，你可能将规则背下来了，但完全不理解为什么会这样。别怕，我将会在这篇文章中为你带来矩阵乘法的全新体验。\n先来回顾一下矩阵加法，还蛮简单的，就是相同位置的数字加一下。\n$$\\begin{bmatrix} 2 \u0026amp; 1 \\\\ 4 \u0026amp; 3 \\end{bmatrix} + \\begin{bmatrix} 1 \u0026amp; 2 \\\\ 1 \u0026amp; 0 \\end{bmatrix} = \\begin{bmatrix} 3 \u0026amp; 3 \\\\ 5 \u0026amp; 3 \\end{bmatrix}$$\n矩阵乘以一个常数，就是所有位置都乘以这个数。\n$$2 \\cdot \\begin{bmatrix} 2 \u0026amp; 1 \\\\ 4 \u0026amp; 3 \\end{bmatrix} = \\begin{bmatrix} 4 \u0026amp; 2 \\\\ 8 \u0026amp; 6 \\end{bmatrix}$$\n但是，等到矩阵乘以矩阵的时候，一切就不一样了。\n$$\\begin{bmatrix} 2 \u0026amp; 1 \\\\ 4 \u0026amp; 3 \\end{bmatrix} \\cdot \\begin{bmatrix} 1 \u0026amp; 2 \\\\ 1 \u0026amp; 0 \\end{bmatrix} = \\begin{bmatrix} 3 \u0026amp; 4 \\\\ 7 \u0026amp; 8 \\end{bmatrix}$$\n这个结果是怎么计算出来的呢？大多数人知道的计算方法应该是教科书上给出的，我们就先来看这种方法。\n教科书告诉你，计算规则是，第一个矩阵第一行的每个数字（2和1），各自乘以第二个矩阵第一列对应位置的数字（1和1），然后将乘积相加（ 2 x 1 + 1 x 1），得到结果矩阵左上角的那个值3。\n也就是说，结果矩阵第 m 行与第 n 列交叉位置的那个值，等于第一个矩阵第 m 行与第二个矩阵第 n 列，对应位置的每个值的乘积之和。\n假设 $$A = \\begin{bmatrix} a_{11} \u0026amp; a_{12} \u0026amp; \\cdots \u0026amp; a_{1n} \\\\ a_{21} \u0026amp; a_{22} \u0026amp; \\cdots \u0026amp; a_{2n} \\\\ \\vdots \u0026amp; \\vdots \u0026amp; \\ddots \u0026amp; \\vdots \\\\ a_{m1} \u0026amp; a_{m2} \u0026amp; \\cdots \u0026amp; a_{mn} \\end{bmatrix}$$\n$$B = \\begin{bmatrix} b_{11} \u0026amp; b_{12} \u0026amp; \\cdots \u0026amp; b_{1p} \\\\ b_{21} \u0026amp; b_{22} \u0026amp; \\cdots \u0026amp; b_{2p} \\\\ \\vdots \u0026amp; \\vdots \u0026amp; \\ddots \u0026amp; \\vdots \\\\ b_{n1} \u0026amp; b_{n2} \u0026amp; \\cdots \u0026amp; b_{np} \\end{bmatrix}$$\n令\n$$C = A \\cdot B$$\n其中，$C = \\begin{bmatrix} c_{11} \u0026amp; c_{12} \u0026amp; \\cdots \u0026amp; c_{1p} \\\\ c_{21} \u0026amp; c_{22} \u0026amp; \\cdots \u0026amp; c_{2p} \\\\ \\vdots \u0026amp; \\vdots \u0026amp; \\ddots \u0026amp; \\vdots \\\\ c_{m1} \u0026amp; c_{m2} \u0026amp; \\cdots \u0026amp; c_{mp} \\end{bmatrix}$\n可以得出矩阵 $C$ 每个元素的表达式为\n$$C_{ij} = a_{i1} \\cdot b_{1j} + a_{i2} \\cdot b_{2j} + \\cdots + a_{in} \\cdot b_{nj} = \\sum_{k=0}^{k=n} a_{ik} \\cdot b_{kj}$$\n这就是矩阵乘法的一般性法则，人们一般都用这个法则来计算，我也不例外。不过我觉得还是有必要讲讲其他几种方法，比如考虑整行或整列。下面还是继续拿矩阵 $A$ 和 $B$ 举例。\n列向量视角 # 先将矩阵 $A$ 和 $B$ 的每一列看成一个向量，例如：\n$$\\vec{a_1} = \\begin{bmatrix} a_{11} \\\\ a_{21} \\\\ a_{31} \\\\ \\vdots \\\\ a_{m1} \\end{bmatrix}$$\n$$\\vec{b_1} = \\begin{bmatrix} b_{11} \\\\ b_{21} \\\\ b_{31} \\\\ \\vdots \\\\ a_{n1} \\end{bmatrix}$$\n这样就可以把矩阵 $A$ 和 $B$ 写成如下的形式：\n$$A = \\begin{bmatrix} \\vec{a_1} \u0026amp; \\vec{a_2} \u0026amp; \\vec{a_3} \u0026amp; \\cdots \u0026amp; \\vec{a_n} \\end{bmatrix}$$\n$$B = \\begin{bmatrix} \\vec{b_1} \u0026amp; \\vec{b_2} \u0026amp; \\vec{b_3} \u0026amp; \\cdots \u0026amp; \\vec{b_p} \\end{bmatrix}$$\n现在如果我将矩阵 $A$ 和向量 $\\vec{b_1}$ 相乘会得到什么？通过前面的一般性法则我们知道大小为 m x n 的矩阵乘以大小为 n x p 的矩阵得到的矩阵大小为 m x p。\n我们来耍一些小聪明，让矩阵 $A$ 以列向量 $\\vec{a_i}$ 作为其元素，而矩阵 $\\vec{b_1}$ 以 $b_{j1}$ 作为其元素。这样看来，矩阵 $A$ 的大小为 1 x n，矩阵 $\\vec{b_1}$ 的大小为 n x 1，所以 $A \\cdot \\vec{b_1}$ 的大小为 1 x 1，这也是一个列向量。如果你代入上面的一般性法则，可以发现 $A \\cdot \\vec{b_1}$ 恰恰就是矩阵 $C$ 的第一列。同样，如果把矩阵 $C$ 的每一列看成一个向量，那么\n$$A \\cdot \\vec{b_1} = \\vec{c_1}$$\n其中，$\\vec{c_1} = \\begin{bmatrix} c_{11} \\\\ c_{21} \\\\ c_{31} \\\\ \\vdots \\\\ c_{m1} \\end{bmatrix} = \\vec{a_1} \\cdot b_{11} + \\vec{a_2} \\cdot b_{21} + \\vec{a_3} \\cdot b_{31} + \\cdots + \\vec{a_n} \\cdot b_{n1}$\n发现了什么？$\\vec{c_1}$ 其实就是矩阵 $A$ 中所有列的线性组合！\n更一般性地，我们可以推出：\n$$\\vec{c_i} = A \\cdot \\vec{b_i} = \\vec{a_1} \\cdot b_{1i} + \\vec{a_2} \\cdot b_{2i} + \\vec{a_3} \\cdot b_{3i} + \\cdots + \\vec{a_n} \\cdot b_{ni}$$\n至此我们得到了一个优美的结论：\n矩阵 $C$ 中的每一列都是矩阵 $A$ 中所有列的线性组合。 到这里你应该能领悟为什么矩阵 $C$ 的行数与矩阵 $A$ 的行数相同了，也就是矩阵 $C$ 的列向量与矩阵 $A$ 的列向量大小相同。\n怎么样，是不是有一种茅塞顿开的感觉？别急，下面我们再换一种理解角度。\n行向量视角 # 先将矩阵 $A$ 和 $B$ 的每一行看成一个向量，例如：\n$$\\vec{a_1} = \\begin{bmatrix} a_{11} \u0026amp; a_{12} \u0026amp; a_{13} \u0026amp; \\cdots \u0026amp; a_{1n} \\end{bmatrix}$$\n$$\\vec{b_1} = \\begin{bmatrix} b_{11} \u0026amp; b_{12} \u0026amp; b_{13} \u0026amp; \\cdots \u0026amp; b_{1p} \\end{bmatrix}$$\n这样就可以把矩阵 $A$ 和 $B$ 写成如下的形式：\n$$A = \\begin{bmatrix} \\vec{a_1} \\\\ \\vec{a_2} \\\\ \\vec{a_3} \\\\ \\vdots \\\\ \\vec{a_m} \\end{bmatrix}$$\n$$B = \\begin{bmatrix} \\vec{b_1} \\\\ \\vec{b_2} \\\\ \\vec{b_3} \\\\ \\vdots \\\\ \\vec{b_n} \\end{bmatrix}$$\n同理，你会发现 $\\vec{a_1} \\cdot B$ 恰好就等于矩阵 $C$ 的第一行。同样，如果把矩阵 $C$ 的每一行看成一个向量，那么\n$$\\vec{a_1} \\cdot B = \\vec{c_1}$$\n其中，$\\vec{c_1} = \\begin{bmatrix} c_{11} \u0026amp; c_{12} \u0026amp; c_{13} \u0026amp; \\cdots \u0026amp; c_{1p} \\end{bmatrix} = a_{11} \\cdot \\vec{b_1} + a_{12} \\cdot \\vec{b_2} + \\cdots + a_{1n} \\cdot \\vec{b_n}$\n更一般性地，我们可以推出：\n$$\\vec{c_j} = \\vec{a_j} \\cdot B = a_{j1} \\cdot \\vec{b_1} + a_{j2} \\cdot \\vec{b_2} + \\cdots + a_{jn} \\cdot \\vec{b_n}$$\n又得到了一个结论：\n矩阵 $C$ 中的每一行都是矩阵 $B$ 中所有行的线性组合。 现在你应该能领悟为什么矩阵 $C$ 的列数与矩阵 $B$ 的列数相同了，也就是矩阵 $C$ 的行向量与矩阵 $B$ 的行向量大小相同。\n故事到这里就结束了吗？远远没有，下面我们再换一种理解角度。\n鬼畜视角 # 常规性的一般性法则其实是拿矩阵 $A$ 的每一行去乘矩阵 $B$ 的每一列的。现在我们反过来思考一下，如果拿矩阵 $A$ 的每一列去乘矩阵 $B$ 的每一行会发生什么？\n为了方便计算，我们将矩阵 $A$ 的每一列看成一个向量，而将矩阵 $B$ 的每一行看成一个向量，即：\n$$\\vec{a_i} = \\begin{bmatrix} a_{1i} \\\\ a_{2i} \\\\ a_{3i} \\\\ \\vdots \\\\ a_{mi} \\end{bmatrix}$$\n$$\\vec{b_i} = \\begin{bmatrix} b_{i1} \u0026amp; b_{i2} \u0026amp; b_{i3} \u0026amp; \\cdots \u0026amp; b_{ip} \\end{bmatrix}$$\n矩阵 $\\vec{a_i}$ 的大小为 m x 1，矩阵 $\\vec{b_i}$ 的大小为 1 x n，发现了什么？$\\vec{a_i} \\cdot \\vec{b_i}$ 得到的是一个大小为 m x n 的矩阵！等等，矩阵 $C$ 的大小不也是 m x n 吗？没错，就是这么神奇，事实上矩阵 $C$ 等于矩阵 $A$ 的每一列与矩阵 $B$ 每一行的乘积之和。下面省略一万字的证明，直接给出公式：\n$$C = \\vec{a_1} \\cdot \\vec{b_1} + \\vec{a_2} \\cdot \\vec{b_2} + \\cdots + \\vec{a_n} \\cdot \\vec{b_n}$$\n结论：\n矩阵 $C$ 等于矩阵 $A$ 中各列与矩阵 $B$ 中各行乘积之和。 举个例子，设矩阵 $A = \\begin{bmatrix} 2 \u0026amp; 7 \\\\ 3 \u0026amp; 8 \\\\ 4 \u0026amp; 9 \\end{bmatrix}$，矩阵 $B = \\begin{bmatrix} 1 \u0026amp; 6 \\\\ 0 \u0026amp; 0 \\end{bmatrix}$，那么：\n$$A \\cdot B = \\begin{bmatrix} 2 \\\\ 3 \\\\ 4 \\end{bmatrix} \\cdot \\begin{bmatrix} 1 \u0026amp; 6 \\end{bmatrix} + \\begin{bmatrix} 7 \\\\ 8 \\\\ 9 \\end{bmatrix} \\cdot \\begin{bmatrix} 0 \u0026amp; 0 \\end{bmatrix}$$\n你有没有发现，你每切换一次视角，你就会对矩阵乘法理解的更深刻。事实上世间万物皆是如此，这里我顺便谈一下”理解“和”理解“的本质，因为理解是我们每个人的目标，我们想要去理解事物。我认为理解和切换视角的能力密切相关，如果你没有切换视角的能力，你就无法理解事物。关于数学，很多人认为数学就是加减乘除、分数、几何代数之类的东西，但实际上数学和模式密切相关，每切换一次视角，你就会得到一种全新的模式。我所说的模式是指影响我们观察的关系、结构以及规律。\n当然了，关于矩阵的乘法还有很多种理解方式，你可以自己去探索，我的讲解到此结束，拜了个拜~~\n","date":"2019年3月9日","externalUrl":null,"permalink":"/posts/matrix-multiplication/","section":"博客","summary":"我在 2016 年的时候写过一篇关于 向量的叉乘与行列式的文章，没想到过","title":"理解矩阵乘法","type":"posts"},{"content":"","date":"2019年3月9日","externalUrl":null,"permalink":"/tags/matrix/","section":"标签","summary":"","title":"矩阵","type":"tags"},{"content":"","date":"2019年3月6日","externalUrl":null,"permalink":"/tags/coredns/","section":"标签","summary":"","title":"CoreDNS","type":"tags"},{"content":" CoreDNS 是 Golang 编写的一个插件式 DNS 服务器，是 Kubernetes 1.13 后所内置的默认 DNS 服务器。CoreDNS 的目标是成为 cloud-native 环境下的 DNS 服务器和服务发现解决方案，即：\nOur goal is to make CoreDNS the cloud-native DNS server and service discovery solution.\n它有以下几个特性：\n插件化（Plugins）\n基于 Caddy 服务器框架，CoreDNS 实现了一个插件链的架构，将大量应用端的逻辑抽象成 plugin 的形式（如 Kubernetes 的 DNS 服务发现，Prometheus 监控等）暴露给使用者。CoreDNS 以预配置的方式将不同的 plugin 串成一条链，按序执行 plugin 的逻辑。从编译层面，用户选择所需的 plugin 编译到最终的可执行文件中，使得运行效率更高。CoreDNS 采用 Go 编写，所以从具体代码层面来看，每个 plugin 其实都是实现了其定义的 interface 的组件而已。第三方只要按照 CoreDNS Plugin API 去编写自定义插件，就可以很方便地集成于 CoreDNS。\n配置简单化\n引入表达力更强的 DSL，即 Corefile 形式的配置文件（也是基于 Caddy 框架开发）。\n一体化的解决方案\n区别于 kube-dns，CoreDNS 编译出来就是一个单独的二进制可执行文件，内置了 cache，backend storage，health check 等功能，无需第三方组件来辅助实现其他功能，从而使得部署更方便，内存管理更为安全。\n其实从功能角度来看，CoreDNS 更像是一个通用 DNS 方案（类似于 BIND），然后通过插件模式来极大地扩展自身功能，从而可以适用于不同的场景（比如 Kubernetes）。正如官方博客所说：\nCoreDNS is powered by plugins.\nCorefile 介绍 # Corefile 是 CoreDNS 的配置文件（源于 Caddy 框架的配置文件 Caddyfile），它定义了：\nserver 以什么协议监听在哪个端口（可以同时定义多个 server 监听不同端口） server 负责哪个 zone 的权威（authoritative）DNS 解析 server 将加载哪些插件 常见地，一个典型的 Corefile 格式如下所示：\nZONE:[PORT] { [PLUGIN] ... } ZONE : 定义 server 负责的 zone，PORT 是可选项，默认为 53； PLUGIN : 定义 server 所要加载的 plugin。每个 plugin 可以有多个参数； 比如：\n. { chaos CoreDNS-001 } 上述配置文件表达的是：server 负责根域 . 的解析，其中 plugin 是 chaos 且没有参数。\n定义 server # 一个最简单的配置文件可以为：\n.{} 即 server 监听 53 端口并不使用插件。**如果此时在定义其他 server，要保证监听端口不冲突；如果是在原来 server 增加 zone，则要保证 zone 之间不冲突，**如：\n. {} .:54 {} 另一个 server 运行于 54 端口并负责根域 . 的解析。\n又如：\nexample.org { whoami } org { whoami } 同一个 server 但是负责不同 zone 的解析，有不同插件链。\n定义 Reverse Zone # 跟其他 DNS 服务器类似，Corefile 也可以定义 Reverse Zone（反向解析 IP 地址对应的域名）：\n0.0.10.in-addr.arpa { whoami } 或者简化版本：\n10.0.0.0/24 { whoami } 可以通过 dig 进行反向查询：\n$ dig -x 10.0.0.1 使用不同的通信协议 # CoreDNS 除了支持 DNS 协议，也支持 TLS 和 gRPC，即 DNS-over-TLS 和 DNS-over-gRPC 模式：\ntls://example.org:1443 { #... } 插件的工作模式 # 当 CoreDNS 启动后，它将根据配置文件启动不同 server ，每台 server 都拥有自己的插件链。当有 DNS 请求时，它将依次经历如下 3 步逻辑：\n如果有当前请求的 server 有多个 zone，将采用贪心原则选择最匹配的 zone； 一旦找到匹配的 server，按照 plugin.cfg 定义的顺序执行插件链上的插件； 每个插件将判断当前请求是否应该处理，将有以下几种可能： 请求被当前插件处理\n插件将生成对应的响应并回给客户端，此时请求结束，下一个插件将不会被调用，如 whoami 插件；\n请求被当前插件以 Fallthrough 形式处理\n如果请求在该插件处理过程中有可能将跳转至下一个插件，该过程称为 fallthrough，并以关键字 fallthrough 来决定是否允许此项操作，例如 host 插件，当查询域名未位于 /etc/hosts，则调用下一个插件；\n请求在处理过程被携带 Hint\n请求被插件处理，并在其响应中添加了某些信息（hint）后继续交由下一个插件处理。这些额外的信息将组成对客户端的最终响应，如 metric 插件；\nCoreDNS 如何处理 DNS 请求 # 如果 Corefile 为：\ncoredns.io:5300 { file db.coredns.io } example.io:53 { log errors file db.example.io } example.net:53 { file db.example.net } .:53 { kubernetes proxy . 8.8.8.8 log health errors cache } 从配置文件来看，我们定义了两个 server（尽管有 4 个区块），分别监听在 5300 和 53 端口。其逻辑图可如下所示：\n每个进入到某个 server 的请求将按照 plugin.cfg 定义顺序执行其已经加载的插件。\n从上图，我们需要注意以下几点：\n尽管在 .:53 配置了 health 插件，但是它并为在上面的逻辑图中出现，原因是：该插件并未参与请求相关的逻辑（即并没有在插件链上），只是修改了 server 配置。更一般地，我们可以将插件分为两种： Normal 插件：参与请求相关的逻辑，且插入到插件链中； 其他插件：不参与请求相关的逻辑，也不出现在插件链中，只是用于修改 server 的配置，如 health，tls 等插件； 配置 CoreDNS # 既然 CoreDNS 如此优秀，我用它来抵御伟大的防火长城岂不美哉？研究了一圈，发现技术上还是可行的，唯一的一个缺点是不支持使用代理，不过你可以通过 proxychians-ng 或 proxifier 来强制使用代理。下面开始折腾。\n具体的思路其实非常简单，就是将国内的域名查询请求转发到 114 等国内的公共 DNS 服务器，将国外的域名查询请求转发到 8.8.8.8 等国外的公共 DNS 服务器。然而 CoreDNS 的插件链有点反直觉，同一个插件链上的每一个插件只能出现一次，如果只使用 forward 插件是满足不了需求的。\nCoreDNS 原来还有个插件叫 proxy，功能和 forward 类似，目测好像同时利用 proxy 和 forward 插件就可以实现咱的需求了。但理想与现实的差距总是很大，不知道从什么时候开始，CoreDNS 官方编译的二进制文件已经没有 proxy 插件了，真是气人。\ndnsredir # 偶然间发现了一个第三方插件 dnsredir，目测可以解决我的所有问题。该插件综合了 proxy 和 forward 插件的所有优点，支持 UDP、TCP、DNS-over-TLS 和 DNS-over-HTTPS，也支持多个后端，还具备健康检查和故障转移的功能，真是太香了！\n它的语法是这样的：\ndnsredir FROM... { to TO... } FROM... 是一个文件列表，包含了匹配的域名和解析该域名的服务器，说白了就是 dnsmasq 所使用的格式，直接看例子：\nserver=/0-100.com/114.114.114.114 server=/0-100.com/114.114.114.114 为什么要用这种格式呢？当然是为了方便啦。\n为什么这样会方便呢？当然是为了可以直接用上 FelixOnMars的大陆区域名列表了。。。FelixOnMars 同时还提供了 Google 和 Apple 的域名列表，这在某些地区某些ISP可以得到国内镜像的 IP，从而加速访问，想想就刺激。\n当然，除了使用文件列表外，还可以使用 .，类似于上面所说的根域。这个插件最大的亮点是可以在插件链中重复使用 dnsredir 插件，只要 FROM... 不重复就行。\nto TO... 用来将 DNS 解析请求发给上游 DNS 服务器。支持几乎所有 DNS 协议，例如：\ndns://1.1.1.1 8.8.8.8 tcp://9.9.9.9 udp://2606:4700:4700::1111 tls://1.1.1.1@one.one.one.one tls://8.8.8.8 tls://dns.quad9.net doh://cloudflare-dns.com/dns-query json-doh://1.1.1.1/dns-query json-doh://dns.google/resolve ietf-doh://dns.quad9.net/dns-query 增强版 CoreDNS # dnsredir 虽香，但大家别忘了，它是第三方插件，官方默认的二进制文件是不包含该插件的。你可以选择自己编译，但如果经常需要升级怎么办？总不能每次都手动编译吧，也太累了。\n好在有位大佬已经通过 CI/CD 流程将所需的第三方插件都集成编译进去了，并定期更新，简直就是我等的福音。大佬的项目地址为：\nhttps://github.com/missdeer/coredns_custom_build 现在只需要下载对应操作系统的二进制文件，到处拷贝，就可以运行了。\n下面统统以 MacOS 为例作讲解。Openwrt 的玩法也一样，参考本文的方法论即可，具体本文就不展开了。\n直接下载二进制文件：\n$ wget \u0026#39;https://appveyorcidatav2.blob.core.windows.net/missdeer-15199/coredns-custom-build/1-7-1-514/idbodwxwywg1xgdg/distrib/coredns-linux-amd64.zip?sv=2015-12-11\u0026amp;sr=c\u0026amp;sig=BhMWcOVtDuaETyz2DcjpOr9GdvkpNVOqoIa7iWFpFNQ%3D\u0026amp;st=2020-12-23T15%3A26%3A19Z\u0026amp;se=2020-12-23T15%3A32%3A19Z\u0026amp;sp=r\u0026#39; $ $ tar zxf coredns-linux-amd64.zip $ mv coredns-linux-amd64/coredns /usr/local/bin/ 配置 # 要深入了解 CoreDNS，请查看其 文档，及 plugins 的介绍。下面是我的配置文件：\ncat \u0026gt; /usr/local/etc/Corefile \u0026lt;\u0026lt;EOF # https://coredns.io/plugins/cache/ (global_cache) { cache { # [5, 60] success 65536 3600 300 # [1, 10] denial 8192 600 60 prefetch 1 60m 10% } } .:7913 { ads { default-lists blacklist https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-domains.txt whitelist https://files.krnl.eu/whitelist.txt log auto-update-interval 24h list-store ads-cache } errors hosts { fallthrough } health prometheus :9153 import global_cache template ANY AAAA { rcode NXDOMAIN } dnsredir accelerated-domains.china.conf google.china.conf apple.china.conf mydns.conf { expire 15s max_fails 3 health_check 3s policy round_robin path_reload 2s to 114.114.114.114 223.5.5.5 119.29.29.29 } dnsredir . { expire 60s max_fails 5 health_check 5s policy random spray to tls://8.8.8.8@dns.google tls://8.8.4.4@dns.google to tls://1.1.1.1@1dot1dot1dot1.cloudflare-dns.com tls://1.0.0.1@1dot1dot1dot1.cloudflare-dns.com # Global TLS server name # tls_servername cloudflare-dns.com } log loop reload 6s } EOF hosts : hosts 是 CoreDNS 的一个 plugin，这一节的意思是加载 /etc/hosts 文件里面的解析信息。hosts 在最前面，则如果一个域名在 hosts 文件中存在，则优先使用这个信息返回； fallthrough : 如果 hosts 中找不到，则进入下一个 plugin 继续。缺少这一个指令，后面的 plugins 配置就无意义了； cache : 溯源得到的结果，缓存指定时间。类似 TTL 的概念； reload : 多久扫描配置文件一次。如有变更，自动加载； errors : 打印/存储错误日志； dnsredir : 这是重点插件。第一段 dnsredir 配置使用了 4 个文件列表，均是 FelixOnMars的大陆区域名列表，这里我还加了一个自定义的文件列表 mydns.conf。第二段 dnsredir 配置表示默认的解析配置，可以理解为故障转移，如果某个域名没有匹配到任何一个文件列表，就使用第二段 dnsredir 的上游 DNS 服务器进行解析。通过这样的配置方式，就实现了将国内的域名查询请求转发到 114 等国内的公共 DNS 服务器，将国外的域名查询请求转发到 8.8.8.8 等国外的公共 DNS 服务器。 讲一下我自己的理解：\n配置文件类似于 nginx 配置文件的格式； 最外面一级的大括号，对应『服务』的概念。多个服务可以共用一个端口； 往里面一级的大括号，对应 plugins 的概念，每一个大括号都是一个 plugin。这里可以看出，plugins 是 CoreDNS 的一等公民； 服务之间顺序有无关联没有感觉，但 plugins 之间是严重顺序相关的。某些 plugin 必须用 fallthrough 关键字流向下一个 plugin； plugin 内部的配置选项是顺序无关的； 从 plugins 页面的介绍看，CoreDNS 的功能还是很强的，既能轻松从 bind 迁移，还能兼容 old-style dns server 的运维习惯； 从 CoreDNS 的性能指标看，适合做大型服务。 注意：该方案的前提是能够强制让 CoreDNS 使用代理，或者更精确一点，让 8.8.8.8 和 8.8.4.4 使用代理。这里的方法比较复杂一点，本文就不介绍了。如果你实在不知道怎么办，可以将 8.8.8.8 这一行删除，直接使用 Cloudflare 提供的 DNS 服务，虽然响应有点慢，但好在可以访问。\n如果你无法忍受 Cloudflare 的响应速度，可以考虑使用国内的无污染 DNS： 红鱼 DNS。然后直接一劳永逸：\ncat \u0026gt; /usr/local/etc/Corefile \u0026lt;\u0026lt;EOF # https://coredns.io/plugins/cache/ (global_cache) { cache { # [5, 60] success 65536 3600 300 # [1, 10] denial 8192 600 60 prefetch 1 60m 10% } } .:7913 { ads { default-lists blacklist https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-domains.txt whitelist https://files.krnl.eu/whitelist.txt log auto-update-interval 24h list-store ads-cache } errors hosts { fallthrough } health prometheus :9153 import global_cache template ANY AAAA { rcode NXDOMAIN } dnsredir accelerated-domains.china.conf google.china.conf apple.china.conf mydns.conf { expire 15s max_fails 3 health_check 3s policy round_robin path_reload 2s to 114.114.114.114 223.5.5.5 119.29.29.29 } dnsredir . { expire 60s max_fails 5 health_check 5s policy random spray to doh://13800000000.rubyfish.cn } log loop reload 6s } EOF 这样 CoreDNS 就不用担心走代理的问题了。\n定时更新国内域名列表 # 大陆域名列表每天都会更新，所以还需要写个脚本来更新文件列表。不用检查文件是否存在了，直接简单粗暴无脑更新：\n$ cat \u0026gt; /usr/local/bin/update_coredns.sh \u0026lt;\u0026lt;EOF #!/bin/bash rm accelerated-domains.china.conf wget https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf -O /usr/local/etc/accelerated-domains.china.conf rm apple.china.conf wget https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf -O /usr/local/etc/apple.china.conf rm google.china.conf wget https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/google.china.conf -O /usr/local/etc/google.china.conf EOF $ sudo chmod +x /usr/local/bin/update_coredns.sh 先执行一遍该脚本，更新 Corefile 的配置：\n$ /usr/local/bin/update_coredns.sh 然后通过 Crontab 制作定时任务，每隔两天下午两点更新域名列表：\n$ crontab -l 0 14 */2 * * /usr/local/bin/update_coredns.sh 开机自启 # MacOS 可以使用 launchctl 来管理服务，它可以控制启动计算机时需要开启的服务，也可以设置定时执行特定任务的脚本，就像 Linux crontab 一样, 通过加装 *.plist 文件执行相应命令。Launchd 脚本存储在以下位置, 默认需要自己创建个人的 LaunchAgents 目录：\n~/Library/LaunchAgents : 由用户自己定义的任务项 /Library/LaunchAgents : 由管理员为用户定义的任务项 /Library/LaunchDaemons : 由管理员定义的守护进程任务项 /System/Library/LaunchAgents : 由 MacOS 为用户定义的任务项 /System/Library/LaunchDaemons : 由 MacOS 定义的守护进程任务项 我们选择在 /Library/LaunchAgents/ 目录下创建 coredns.plist 文件，内容如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple Computer//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;coredns\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/usr/local/bin/coredns\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;-conf\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;/usr/local/etc/Corefile\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;StandardOutPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/coredns.stdout.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/var/log/coredns.stderr.log\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; 设置开机自动启动 coredns：\n$ sudo launchctl load -w /Library/LaunchAgents/coredns.plist 查看服务：\n$ sudo launchctl list|grep coredns 61676\t0\tcoredns $ sudo launchctl list coredns { \u0026#34;StandardOutPath\u0026#34; = \u0026#34;/var/log/coredns.stdout.log\u0026#34;; \u0026#34;LimitLoadToSessionType\u0026#34; = \u0026#34;System\u0026#34;; \u0026#34;StandardErrorPath\u0026#34; = \u0026#34;/var/log/coredns.stderr.log\u0026#34;; \u0026#34;Label\u0026#34; = \u0026#34;coredns\u0026#34;; \u0026#34;TimeOut\u0026#34; = 30; \u0026#34;OnDemand\u0026#34; = false; \u0026#34;LastExitStatus\u0026#34; = 0; \u0026#34;PID\u0026#34; = 61676; \u0026#34;Program\u0026#34; = \u0026#34;/usr/local/bin/coredns\u0026#34;; \u0026#34;ProgramArguments\u0026#34; = ( \u0026#34;/usr/local/bin/coredns\u0026#34;; \u0026#34;-conf\u0026#34;; \u0026#34;/usr/local/etc/Corefile\u0026#34;; ); }; 查看端口号：\n$ sudo ps -ef|egrep -v grep|grep coredns 0 81819 1 0 2:54下午 ?? 0:04.70 /usr/local/bin/coredns -conf /usr/local/etc/Corefile $ sudo lsof -P -p 81819|egrep \u0026#34;TCP|UDP\u0026#34; coredns 81819 root 5u IPv6 0x1509853aadbdf853 0t0 TCP *:5302 (LISTEN) coredns 81819 root 6u IPv6 0x1509853acd2f39ab 0t0 UDP *:5302 coredns 81819 root 7u IPv6 0x1509853aadbdc493 0t0 TCP *:53 (LISTEN) coredns 81819 root 8u IPv6 0x1509853acd2f5a4b 0t0 UDP *:53 coredns 81819 root 9u IPv6 0x1509853ac63bfed3 0t0 TCP *:5301 (LISTEN) coredns 81819 root 10u IPv6 0x1509853acd2f5d03 0t0 UDP *:5301 大功告成，现在你只需要将系统的 DNS IP 设置为 127.0.0.1 就可以了。\n验证 # $ doggo www.youtube.com @udp://127.0.0.1 NAME TYPE CLASS\tTTL ADDRESS NAMESERVER www.youtube.com. CNAME\tIN 293s\tyoutube-ui.l.google.com.\t127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t172.217.14.110 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t172.217.11.174 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t172.217.5.206 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t172.217.5.78 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t172.217.14.78 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t142.250.72.238 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t216.58.193.206 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t142.250.68.110 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t142.250.68.78 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t172.217.4.142 127.0.0.1:53 youtube-ui.l.google.com.\tA IN 293s\t142.250.68.14 127.0.0.1:53 搞定。\n什么？你问我 doggo 是个啥？自己谷歌。\n参考资料 # CoreDNS 使用与架构分析 CoreDNS搭建无污染DNS ","date":"2019年3月6日","externalUrl":null,"permalink":"/posts/install-coredns-on-macos/","section":"博客","summary":"CoreDNS 是 Golang 编写的一个插件式 DNS 服务器，是 Kubernetes 1.13 后所内置的默认 DNS 服务器","title":"使用 CoreDNS 来应对 DNS 污染","type":"posts"},{"content":" 原文地址：The Mechanics of Kubernetes Kubernetes 是一个用于在一组节点（通常称之为集群）上托管容器化应用程序的容器编排引擎。本系列教程旨在通过系统建模的方法帮助大家更好地理解 Kubernetes 及其基本概念。\n深入理解 Kubernetes API Server Kubernetes 设计哲学 本文可以帮助你理解 Kubernetes 对象存储和控制器的工作原理。\nKubernetes 是一个声明式容器编排引擎。在声明式系统中，你可以声明期望的状态，系统将不断地调整实际状态，直到与期望状态保持一致。因此，“声明式系统”这个术语表示一组经过精确计算的相互协调的操作，用来将系统的当前状态调整为期望状态。但实际上 Kubernetes 并不是这么工作的！\nKubernetes 不会基于系统当前状态和期望状态来来确定接下来要执行的一组经过精确计算的相互协调的命令，而是仅基于系统当前状态确定下一个要执行的命令，然后不断迭代，直到没有下一个命令可以执行，系统就达到了稳定状态。\n状态转换机制 # 下面我将用一个抽象模型来表示 Kubernetes 的状态转换机制。\nfact { all k8s : K8s - last | let k8s\u0026#39; = k8s.next { some c : NextCommand[k8s] { command.source = k8s and command.target = k8s\u0026#39; } } NextCommand[k8s.last] = none } 给定一个函数 NextCommand，用来表示下一个要执行的命令，系统会基于当前状态 k8s 来决定下一个要执行的命令，该命令会将系统从当前状态 k8s 转换成下一个状态 k8s'。\nfun NextCommand(k8s : K8s) : set Command { DeploymentController.NextCommand[k8s] + ReplicaSetController.NextCommand[k8s] + ... } NextCommand 函数事实上是每个 Kubernetes 控制器的 NextCommand 函数的集合。\npred Steady(k8s : K8s) { NextCommand[k8s] = none } 所有的状态组成一个状态序列，状态序列的终止状态是 k8s.last，该状态的 NextCommand 函数不会产生下一个命令，此时系统就会进入稳定（steady）状态。\nKubernetes 资源对象 # Kubernetes 对象存储表示持久化的 Kubernetes 资源对象集合。Kubernetes 的资源对象实际上是不同类型的数据记录，通常用 kind 来表示类型。\napiVersion: apps/v1 kind: Deployment metadata: name: my-deployment spec: replicas: 3 template: spec: containers: - name: busybox image: busybox 上述 manifest 内容描述了一个 Deployment 对象：\n.kind 等于 Deployment。 .spec.replicas 等于 3. .spec.template.spec.containers[0].image 等于 BusyBox。 Kubernetes 控制器 # 每一个控制器都是 NextCommand 函数的组成部分，控制器实际上是根据 Kubernetes 当前状态确定下一个要执行命令的一个连续的过程。\nprocess Controller = \u0026#34;Deployment Controller\u0026#34; begin ControlLoop: while TRUE do \\* The Deployment Controller monitors Deployment Objects with d ∈ {d ∈ k8s: d.kind = \u0026#34;Deployment\u0026#34;} do \\* 1. Enabling Condition if Cardinality({r \\in k8s: r.kind = \u0026#34;ReplicaSet\u0026#34; ∧ match(d.spec.labelSelector, r.meta.labels)}) \u0026lt; 1 then \\* Reconciling Command CREATE([kind |-\u0026gt; \u0026#34;ReplicaSet\u0026#34;, spec |-\u0026gt; [replicas |-\u0026gt; d.spec.replicas, template |-\u0026gt; d.spec.template]]); end if; \\* 2. Enabling Condition if Cardinality({r \\in k8s: r.kind = \u0026#34;ReplicaSet\u0026#34; ∧ match(d.spec.labelSelector, r.meta.labels)}) \u0026gt; 1 then \\* Reconciling Command with r ∈ {r \\in k8s: r.kind = \u0026#34;ReplicaSet\u0026#34; ∧ match(d.spec.labelSelector, r.meta.labels)} do DELETE(r); end with; end if; end with; end while; end process; 上述 Alloy 规范语言描述了 Deployment 控制器的实现原理：控制器对所有的 Deployment 对象进行监控，并为每个对象执行一组条件语句：\n条件 :\n如果匹配的 ReplicaSet 对象少于 1 个。\n命令 :\n控制器就会生成 Create ReplicaSet 命令。\n条件 :\n如果匹配的 ReplicaSet 对象多于 1 个。\n命令 :\n控制器就会生成 Delete ReplicaSet 命令。\n从控制器的视角来看，如果任何一个条件语句的条件都不满足，Deployment 对象就会进入稳定状态，控制器也不会执行任何命令。\n级联命令 # Kubernetes 的控制器可以相互级联启用，他们是层层控制的关系：\n给定一个当前状态 k8s，如果启用了控制器 C，C 会执行命令将状态转换为 k8s'。 给定一个当前状态 k8s'，如果启用了控制器 C'，C' 会执行命令将状态转换为 k8s''。 上图展示了用户将 Deployment 对象提交给 API Server 之后生成的级联命令。\nKubernetes 是声明式系统吗？ # fact { all sys : Sys - last | let sys\u0026#39; = sys.next { some c : Command { command.source = sys and command.target = sys\u0026#39; } } Desired[sys.last] } 上述规范语言描述了严格意义上的声明式系统的状态转换机制：给定一个期望状态，系统将找到一系列命令让自己从当前状态 sys.first 转换为期望状态 sys.last。\n如果我们不把 Kubernetes 的资源对象看成对实际描述的数据的记录，而是看成对最终期望的结果的记录，就可以认为 Kubernetes 是一个声明式系统。例如，我们可以将前文提到的 Deployment 对象解释为 :** 最终期望的结果是存在 3 个 Pod 对象。**\n这种理解方式的可取之处在于：如果你将一个资源对象看成对最终期望的结果的记录，你就会对接下来要执行的操作有多种选择。例如，一个 Deployment 对象可以被看成：\n一个 ReplicaSet 或 一组 Pod 按照这种理解方式，只有当存在一个 ReplicaSet 和与此相关联的一组 Pod时，才会被认为满足期望状态。\n如果按照严格意义的声明式系统的理解方式：\n只要有一个 ReplicaSet 对象，k8s 的 Deployment 对象就会进入稳定状态（Deployment 控制器不会产生命令）。 只要有一组 Pod 对象，k8s 的 ReplicaSet 对象就会进入稳定状态（ReplicaSet 控制器不会产生命令）。 总结 # 在大多数情况下，如果定义不是很严格，Kubernetes 可以被看成声明式系统，Kubernetes 资源对象被当成对最终期望的结果的记录。但当涉及到 Kubernetes 的行为时，你要知道它并不会像真正意义上的声明式系统那样通过一系列相互协作的命令来过渡到理想状态，而是通过持续迭代方式一步一步过渡到稳定状态。\n后记 # 本系列文章是 CNCF，Google 和 SAP 之间合作努力的结果，旨在促进大家对 Kubernetes 及其基本概念的理解。\n","date":"2019年2月23日","externalUrl":null,"permalink":"/posts/the-mechanics-of-kubernetes/","section":"博客","summary":"原文地址：The Mechanics of Kubernetes Kubernetes 是一个用于在一组节点（通常称之为集群","title":"Kubernetes 设计哲学","type":"posts"},{"content":"","date":"2019年1月30日","externalUrl":null,"permalink":"/tags/etcd/","section":"标签","summary":"","title":"Etcd","type":"tags"},{"content":"集群成员变更一直是 etcd 最棘手的问题之一，在变更过程中会遇到各种各样的挑战，我们稍后一一来看。为了把问题描述清楚，首先需要了解 etcd 内部的 raft 实现。\netcd 内部的 raft 实现 # leader 会存储所有 follower 对自身 log 数据的 progress（复制进度），leader 根据每个 follower 的 progress 向其发送 replication message。\nreplication message 是 msgApp 外加上 log 数据。\nprogress 有两个比较重要的属性：match 和 next。match 是 leader 知道的 follower 对自身数据的最新复制进度【或者说就是 follower 最新的 log entry sent index】，如果 leader 对 follower 的复制进度一无所知则这个值为 0。next 则是将要发送给 follower 的下一个 log entry sent 的序号。\nprogress 有三个状态：probe，replicate 和 snapshot。\n+--------------------------------------------------------+ | send snapshot | | | +---------+----------+ +----------v---------+ +---\u0026gt; probe | | snapshot | | | max inflight = 1 \u0026lt;----------------------------------+ max inflight = 0 | | +---------+----------+ +--------------------+ | | 1. snapshot success | | (next=snapshot.index + 1) | | 2. snapshot failure | | (no change) | | 3. receives msgAppResp(rej=false\u0026amp;\u0026amp;index\u0026gt;lastsnap.index) | | (match=m.index,next=match+1) receives msgAppResp(rej=true) (next=match+1)| | | | | | | | receives msgAppResp(rej=false\u0026amp;\u0026amp;index\u0026gt;match) | | (match=m.index,next=match+1) | | | | | | | +---------v----------+ | | replicate | +---+ max inflight = n | +--------------------+ 如果 follower 处于 probe 状态，则 leader 每个心跳包最多只发送一个 replication message。leader 会缓慢发送 replication message 并探测 follower 的处理速度。leader 收到 msgHeartbeatResp 或者收到 msgAppResp（其中 reject 值为 true）时，leader 会发送下 一个 replication message。\n当 follower 给 leader 的 msgAppResp 的 reject 为 false 的时候，它会被置为 replicate 状态，reject 为 false 就意味着 follower 能够跟上 leader 的发送速度。leader 会启动 stream 方式向以求最快的方式向 follower 发送 replication message。当 follower 与 leader 之间的连接断连或者 follower 给 leader 回复的 msgAppResp 的 reject 为 true 时，就会被重新置为 probe 状态，leader 当然也会把 next 置为 match+1。\n当 follower 处于 replicate 状态时，leader 会一次尽量多地把批量 replication message 发送给 follower，并把 next 取值为当前 log entry sent 的最大值，以让 follower 尽可能快地跟上 leader 的最新数据。\n当 follower 的 log entry set 与 leader 的 log entry sent 相差甚巨的时候，leader 会把 follower 的状态置为 snapshot，然后以 msgSnap 请求方式向其发送 snapshot 数据，发送完后 leader 就等待 follower 直到超时或者成功或者失败或者连接中断。当 follower 接收完毕 snapshot 数据后，就会回到 probe 状态。\n当 follower 处于 snapshot 状态时候，leader 不再发送 replication message 给 follower。\n新当选的 leader 会把所有 follower 的 state 置为 probe，把 match 置为0，把 next 置为自身 log entry set 的最大值。\nleader 与 follower 之间进行数据同步的时候，可以通过下面两个步骤进行流量控制：\n限制 message 的 max size。这个值是可以通过相关参数进行限定的，限定后可以降低探测 follower 接收速度的成本； 当 follower 处于 replicate 状态时候，限定每次批量发送消息的数目。leader 在网络层之上有一个发送 buffer，通过类似于 tcp 的发送窗口的算法动态调整 buffer 的大小，以防止 leader 由于发包过快导致 follower 大量地丢包，提高发送成功率。 snapshot，故名思议，是某个时间节点上系统状态的一个快照，保存的是此刻系统状态数据，以便于让用户可以恢复到系统任意时刻的状态。\netcd-raft 中的 snapshot 代表了应用的状态数据，而执行 snapshot 的动作也就是将应用状态数据持久化存储，这样，在该 snapshot 之前的所有日志便成为无效数据，可以删除。 集群成员变更 # 当集群加入新节点时，新加入的节点是没有任何数据的，因此新节点的 log entry sent 与 leader 的 log entry sent 相差很大，所以 leader 会向该节点发送 snapshot 数据。这时 leader 的网络有可能会过载、阻塞甚至丢弃 leader 发送给 follower 的 heartbeat，一段时间以后某个 follower 会因为选举超时将自己的状态切换为 candidate 并发起选举。所以新加入的节点很容易对集群造成影响，无论是 leader 选举还是将后续的更新传播给新成员，都很容易导致集群不可用。\n网络隔离 # 如果发生了网络隔离，集群还会正常工作吗？主要还是取决于 leader 被隔离到哪个区域。\n当集群的 Leader 在多数节点这一侧时，集群仍可以正常工作。例如一个 3 节点集群，它的 quorum 为 2，其中一个 follower 被网络隔离，因为 leader 所在的这一侧的 majority 为 2，所以不会发生重新选举。\nQuorum 机制，是一种分布式系统中常用的，用来保证数据冗余和最终一致性的投票算法，具体参考 分布式系统之 Quorum 机制。应用在 etcd 的场景中，quorum 表示能保证集群正常工作的最少节点数。而 majority 表示集群当前能参加投票的节点数量。\n如果 leader 被整个集群都隔离了，这时 leader 的 majority 为 1，无法发起选举，leader 就会将自己的状态切换为 follower，影响到了集群的可用性。\n拥有 3 个节点的集群加入 1 个新节点之后集群节点数量变为 4，quorum 大小变为 3。\n新加入节点后隔离网络 # 如果加入新节点之后发生了网络隔离，集群还会正常工作吗？主要还是取决于新加入的节点被隔离到了哪个区域。\n如果新加入的节点与 leader 被隔离在同一个区域内，leader 的 majority 数量仍然为 3，不会导致重新选举，也不会影响集群的可用性。\n如果新节点与 leader 不在同一区域内，并且集群被对半隔离，这时任何一侧的 majority 都不是 3，从而会发生重新选举，leader 将状态切换为 follower。\n隔离网络后再加入新节点 # 如果先发生网络隔离，后加入新节点，集群还会正常工作吗？\n假设一个拥有 3 个节点的集群已经有一个 foloower 被隔离了，这时再加入新节点，quorum 就会从 2 变为 3。但此时新加入的节点还没有启动，集群的 majority 为 2，从而会发生重新选举。\n因为 member add 命令会改变集群的 quorum 大小，所以建议先通过 member remove 命令移除处于崩溃状态的 follower。\n加入新节点带来的问题 # 向一个单节点集群中加入新节点后，集群的 quorum 大小变为 2，但这时还会发生重新选举，为什么呢？因为加入节点的操作是分成两步进行的：\n执行 member add 命令 启动新节点 当你执行完 member add 命令后，集群的 quorum 大小变为 2，但此时新节点还没有启动，从 leader 的视角来看，majority 仍然是 1，不满足 quorum，所以会重新选举。\n来看一种更糟糕的场景，如果新加入的节点配置错误（比如 --peer-urls 是非法的），当执行 member add 命令之后，单节点集群的 quorum 大小变为 2，发生重新选举，但此时新节点不会启动成功的，所以无法满足 quorum。一旦集群无法满足 quorum，就再也无法完成集群成员变更。\n多节点集群类似。例如一个拥有 3 个节点的集群，新加入一个配置错误的节点后，quorum 大小从 2 变为 3。此时只要有 1 个 follower 发生故障，整个集群就会变为不可用状态，因为集群的 majority 为 2，不满足 quorum（其中 1 个 follower 发生故障，另一个配置错误）。\n这就带来了一个很严峻的问题 :** 只要新加入的节点配置上出了点什么差错，整个集群的容错能力就会减 1。**这时你只能通过 etcd --force-new-cluster 命令来重新创建集群。\n但 etcd 可是 Kubernetes 集群至关重要的组件啊，即使是最轻微的中断也可能会对用户的生产环境产生重大影响。怎样才能使成员变更的操作更安全呢？相对于其他方面来说，leader 选举对 etcd 集群的可用性有着至关重要的影响：有没有办法在集群成员变更的时候不改变集群的 quorum 大小？能否让新加入的节点处于备用的空闲状态，缓慢接收 leader 的 replication message，直到与 leader 保持同步？新加入的节点如果配置错误，有没有办法能让其回退？或者有没有更安全的办法来完成集群成员变更的操作（新加入节点配置错误不会导致集群的容错能力下降）？集群管理员新加入节点时需要关心网络协议吗？无论节点的位置在哪，无论是否发生网络隔离，有没有办法让用来加入新节点的 API 都可以正常工作？\n引入 Raft Learner 角色 # 为了解决上一节提到的加入新节点带来的容错能力下降的问题， rfat 4.2.1 论文 中介绍了一种新的节点角色：Learner。以该角色加入集群的节点不参与投票选举，近接收 leader 的 replication message，直到与 leader 保持同步为止。\nv3.4 中的新特性 # 集群管理员向集群中添加新节点时要尽可能减少不必要的操作项。通过 member add --learner 命令可以向 etcd 集群中添加 learner 节点，不参加投票，只接收 replication message。\n当 Learner 节点与 leader 保持同步之后，可以通过 member promote 来将该节点的状态提升为 follower，然后将其计入 quorum 的大小之中。\nleader 会验证 promote 请求来确保其操作的安全性。只有当 learner 的 log 数据与 leader 保持一致后，learner 才能被提升为 follower 节点。\nLearner 被提升为 follower 之前会一直被当成备用节点，且 leader 节点不能被转换为 learner 节点。learner 节点也不会接受客户端的读写操作，这就意味着 learner 不需要向 leader 发送 Read Index 请求。这种限制简化了 etcd v3.4 中 learner 的实现方式。\n除此之外，etcd 还限制了集群中 Learner 节点数量的上限，以避免大量的 replication message 使 leader 过载。Learner 节点自身不能改变自己的状态，etcd 提供了 learner 状态检测和安全性检测，集群管理员必须自己决定要不要改变 learner 的状态。\nv3.5 中的新特性 # 新加入的节点默认就是 Learner 角色 当 learner 的 log 数据与 leader 保持一致后，集群会自动将 learner 转换为 follower。从用户的角度来看，你仍然可以使用 member add 命令来加入新节点，但集群会自动帮你把新加入的节点设置为 learner 状态。 新加入的节点被视为备用节点，一旦集群的可用性受到影响，就会被提升为 follower 状态。 learner 节点可以被设置为只读状态，被设置成只读状态后就永远不能被提升为 follower 状态。在弱一致性模式中，learner 只接收 leader 发送的数据，并且永远不会响应写操作。在没有共识开销的情况下从本地读取数据会大大减少 leader 的工作量，但向客户端提供的数据可能会过时。在强一致性模式中，learner 会向 leader 发送 read index 以获取最新的数据，但仍然拒绝写请求。 参考资料 # ETCD Progress ETCD Learner ","date":"2019年1月30日","externalUrl":null,"permalink":"/posts/etcd-server-learner/","section":"博客","summary":"集群成员变更一直是 etcd 最棘手的问题之一，在变更过程中会遇到各种","title":"Etcd 的分布式一致性详解","type":"posts"},{"content":"etcd 使用 raft 协议保证各个节点之间的状态一致。根据 raft 算法原理，节点数目越多，会降低集群的写性能。这是因为每一次写操作，需要集群中大多数节点将日志落盘成功后，Leader 节点才能将修改内部状态机，并返回将结果返回给客户端。但是根据 etcd 分布式数据冗余策略，集群节点越多，容错能力(Failure Tolerance)越强。所以关于集群大小的优化，其实就是容错和写性能的一个平衡。\n集群的大小指集群节点的个数。\n你可能会在很多文章中看到 etcd 推荐使用奇数作为集群节点个数。因为奇数个节点与和其配对的偶数个节点相比(比如 3 节点和 4 节点对比)，容错能力相同，却可以少一个节点。但却没有人告诉你为什么会这样，今天我就给你们带来详细解读。\n选举方法 # 首先简单介绍一下 etcd 的选举方法：\n初始启动时，节点处于 follower 状态并被设定一个 election timeout，如果在这一时间周期内没有收到来自 leader 的 heartbeat，节点将发起选举：将自己切换为 candidate 之后，向集群中其它 follower 节点发送请求，询问其是否选举自己成为 leader。 当收到来自集群中过半数节点的接受投票后，节点即成为 leader，开始接收保存 client 的数据并向其它的 follower 节点同步日志。如果没有达成一致，则 candidate 随机选择一个等待间隔（150ms ~ 300ms）再次发起投票，得到集群中半数以上 follower 接受的 candidate 将成为 leader。 leader 节点依靠定时向 follower 发送 heartbeat 来保持其地位。 任何时候如果其它 follower 在 election timeout 期间都没有收到来自 leader 的 heartbeat，同样会将自己的状态切换为 candidate 并发起选举。每成功选举一次，新 leader 的任期（Term）都会比之前 leader 的任期大 1。 请注意：这里说的超过半数节点接受投票，是包括 candidate 自身在内的！而且这里的半数是以原集群大小作为总数来计算的！举个例子：假设某个 etcd 集群有 N 个节点，挂了一个节点之后，如果重新发起选举，一定要有超过 N/2 个节点接受投票（包括 candidate 在内），即最少需要 (N+1)/2 个节点接受投票，参加选举的节点才能成为 leader。 集群大小与容错 # 假设有两个 etcd 集群，分别是 2 个节点和 1 个节点，2 节点集群需要所有节点接受投票才能选举成功（N=2），只要有一个节点发生故障，etcd 集群就会变成不可用状态，因为这时不可能选举成功。因此 2 节点集群的容错能力不如单节点集群，2 节点集群的容错能力为 0%。\n我们把“超过半数节点接受投票的节点数量”称为 majority。\n同样的方式，比较 3 节点集群和 4 节点集群。对于 3 节点集群来说，如果其中一个节点发生故障，majority 仍然是 2，集群依然可用。而对于 4 节点集群而言，majority 是 3，只允许一个节点发生故障，如果有两个节点发生故障，majority 就会变成 2，而 2 不满足 \u0026gt; N/2 的要求（N=4）。现在你应该理解为什么奇数个节点与和其配对的偶数个节点相比(比如 3 节点和 4 节点对比)，容错能力相同了。\n这里能选择偶数个节点吗？ 最好不要这样。原因有二：\n偶数个节点集群不可用风险更高，表现在选主过程中，有较大概率获得等额选票，从而触发下一轮选举。 偶数个节点集群在某些网络分割的场景下无法正常工作。试想，当网络分割发生后，将集群节点对半分割开。此时集群将无法工作。按照 raft 协议，此时集群写操作无法使得大多数节点同意，从而导致写失败，集群无法正常工作。 当网络分割后，etcd 集群如何处理的呢?\n当集群的 Leader 在多数节点这一侧时，集群仍可以正常工作。少数节点那一侧无法收到 Leader 心跳，也无法完成选举。 当集群的 Leader 在少数节点这一侧时，集群仍可以正常工作，多数派的节点能够选出新的 Leader, 集群服务正常进行。 当网络分割恢复后，少数派的节点会接受集群 Leader 的日志，直到和其他节点状态一致。\n所以综合考虑性能和容错能力，etcd 官方文档推荐的 etcd 集群大小是 3, 5, 7。至于到底选择 3，5 还是 7，根据需要的容错能力而定。\n关于节点数和容错能力对应关系，如下表所示：\n集群大小 最大容错 1 0 3 1 4 1 5 2 6 2 7 3 8 3 9 4 ","date":"2019年1月30日","externalUrl":null,"permalink":"/posts/etcd-cluster-number/","section":"博客","summary":"etcd 使用 raft 协议保证各个节点之间的状态一致。根据 raft 算法原理，节点","title":"etcd 集群大小迷思","type":"posts"},{"content":"","date":"2019年1月28日","externalUrl":null,"permalink":"/tags/nginx/","section":"标签","summary":"","title":"Nginx","type":"tags"},{"content":" 原文链接： nginx mirroring tips and tricks\n最近我在研究 Nginx 1.13.4 最新的 mirror 模块，利用 mirror 模块，你可以将线上实时流量拷贝至其他环境同时不影响源站请求的响应，因为 Nginx 会丢弃 mirror 的响应。mirror 模块可用于以下几个场景：\n通过预生产环境测试来观察新系统对生产环境流量的处理能力。 复制请求日志以进行安全分析。 复制请求用于数据科学研究。 等等 我已经用它来测试新系统对生产环境流量的处理能力，但遇到了一些小问题，经过一番努力我总结出了一些小窍门，现在分享给你们。\n基础配置 # 先来创建一个基本的配置，架构如下图所示，由一个用来实际处理流量的后端和一个前端代理组成：\nNginx 配置文件如下：\nupstream backend { server backend.local:10000; } server { server_name proxy.local; listen 8000; location / { proxy_pass http://backend; } } 配置文件由两部分组成：后端服务与代理。代理监听在 8000 端口，它会将流量转发到后端服务的 10000 端口。看起来没什么稀奇的，先做个压力测试看看性能吧。这里我选择用 hey 来测试压力，因为它很简单，可以施加稳定的负载，其他工具的负载施加很不稳定（例如，wrk, apache benchmark, siege）。\n$ hey -z 10s -q 1000 -n 100000 -c 1 -t 1 http://proxy.local:8000 Summary: Total:\t10.0016 secs Slowest:\t0.0225 secs Fastest:\t0.0003 secs Average:\t0.0005 secs Requests/sec:\t995.8393 Total data:\t6095520 bytes Size/request:\t612 bytes Response time histogram: 0.000 [1] | 0.003 [9954] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.005 [4] | 0.007 [0] | 0.009 [0] | 0.011 [0] | 0.014 [0] | 0.016 [0] | 0.018 [0] | 0.020 [0] | 0.022 [1] | Latency distribution: 10% in 0.0003 secs 25% in 0.0004 secs 50% in 0.0005 secs 75% in 0.0006 secs 90% in 0.0007 secs 95% in 0.0007 secs 99% in 0.0009 secs Details (average, fastest, slowest): DNS+dialup:\t0.0000 secs, 0.0003 secs, 0.0225 secs DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0008 secs req write:\t0.0000 secs, 0.0000 secs, 0.0003 secs resp wait:\t0.0004 secs, 0.0002 secs, 0.0198 secs resp read:\t0.0001 secs, 0.0000 secs, 0.0012 secs Status code distribution: [200]\t9960 responses 大多数请求都在 1 毫秒内处理完成，也没有错误响应，很好，但这只是我们的底线。\n基础流量镜像配置 # 现在我们向后端添加一个测试服务，并将发往源后端的流量复制一份到测试后端。\n流量镜像的配置文件如下：\nupstream backend { server backend.local:10000; } upstream test_backend { server test.local:20000; } server { server_name proxy.local; listen 8000; location / { mirror /mirror; proxy_pass http://backend; } location = /mirror { internal; proxy_pass http://test_backend$request_uri; } } mirror 指令制定镜像 uri 为 /mirror location = /mirror 中的 internal 指定此 location 只能被“内部的”请求调用，外部的调用请求会返回 ”Not found” (404) 在 mirror 配置中可以做很多事情，但这里我们只是单纯地转发所有的流量。\n再次进行压力测试，观察流量镜像是如何影响性能的：\n$ hey -z 10s -q 1000 -n 100000 -c 1 -t 1 http://proxy.local:8000 Summary: Total:\t10.0010 secs Slowest:\t0.0042 secs Fastest:\t0.0003 secs Average:\t0.0005 secs Requests/sec:\t997.3967 Total data:\t6104700 bytes Size/request:\t612 bytes Response time histogram: 0.000 [1] | 0.001 [9132] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.001 [792] |■■■ 0.001 [43] | 0.002 [3] | 0.002 [0] | 0.003 [2] | 0.003 [0] | 0.003 [0] | 0.004 [1] | 0.004 [1] | Latency distribution: 10% in 0.0003 secs 25% in 0.0004 secs 50% in 0.0005 secs 75% in 0.0006 secs 90% in 0.0007 secs 95% in 0.0008 secs 99% in 0.0010 secs Details (average, fastest, slowest): DNS+dialup:\t0.0000 secs, 0.0003 secs, 0.0042 secs DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0009 secs req write:\t0.0000 secs, 0.0000 secs, 0.0002 secs resp wait:\t0.0004 secs, 0.0002 secs, 0.0041 secs resp read:\t0.0001 secs, 0.0000 secs, 0.0021 secs Status code distribution: [200]\t9975 responses 和第一次的测试结果一样：大多数请求都在 1 毫秒内处理完成，也没有错误响应。可以得出结论：镜像流量不会影响源站请求的响应。\n将流量复制到故障后端 # 到目前为止，测试结果都很符合预期。考虑另外一种场景，如果镜像后端出现了故障，时不时会返回错误响应，这时会不会对原始请求产生影响呢？\n为了模拟这种场景，我用 golang 写了一个 小工具来随机注入故障，你可以通过以下命令来启动：\n$ mirror-backend -errors 2019/01/13 14:43:12 Listening on port 20000, delay is 0, error injecting is true 然后进行负载测试：\n$ hey -z 10s -q 1000 -n 100000 -c 1 -t 1 http://proxy.local:8000 Summary: Total:\t10.0008 secs Slowest:\t0.0027 secs Fastest:\t0.0003 secs Average:\t0.0005 secs Requests/sec:\t998.7205 Total data:\t6112656 bytes Size/request:\t612 bytes Response time histogram: 0.000 [1] | 0.001 [7388] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.001 [2232] |■■■■■■■■■■■■ 0.001 [324] |■■ 0.001 [27] | 0.002 [6] | 0.002 [2] | 0.002 [3] | 0.002 [2] | 0.002 [0] | 0.003 [3] | Latency distribution: 10% in 0.0003 secs 25% in 0.0003 secs 50% in 0.0004 secs 75% in 0.0006 secs 90% in 0.0007 secs 95% in 0.0008 secs 99% in 0.0009 secs Details (average, fastest, slowest): DNS+dialup:\t0.0000 secs, 0.0003 secs, 0.0027 secs DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0008 secs req write:\t0.0000 secs, 0.0000 secs, 0.0001 secs resp wait:\t0.0004 secs, 0.0002 secs, 0.0026 secs resp read:\t0.0001 secs, 0.0000 secs, 0.0006 secs Status code distribution: [200]\t9988 responses 仍然和之前的测试结果一样！这说明了故障后端的错误并不会影响源后端的响应。Nginx 忽略了镜像请求的响应，所以测试结果会和之前一样。\n将流量复制到响应缓慢的后端 # 继续设想下一种场景：镜像后端不会返回错误响应，仅仅只是响应很缓慢，这时候会对原始请求有影响吗？\n通过以下命令来让镜像后端对每个请求延迟 1 秒再响应：\n$ mirror-backend -delay 1 2019/01/13 14:50:39 Listening on port 20000, delay is 1, error injecting is false 然后进行负载测试：\n$ hey -z 10s -q 1000 -n 100000 -c 1 -t 1 http://proxy.local:8000 Summary: Total:\t10.0290 secs Slowest:\t0.0023 secs Fastest:\t0.0018 secs Average:\t0.0021 secs Requests/sec:\t1.9942 Total data:\t6120 bytes Size/request:\t612 bytes Response time histogram: 0.002 [1]\t|■■■■■■■■■■ 0.002 [0]\t| 0.002 [1]\t|■■■■■■■■■■ 0.002 [0]\t| 0.002 [0]\t| 0.002 [0]\t| 0.002 [1]\t|■■■■■■■■■■ 0.002 [1]\t|■■■■■■■■■■ 0.002 [0]\t| 0.002 [4]\t|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.002 [2]\t|■■■■■■■■■■■■■■■■■■■■ Latency distribution: 10% in 0.0018 secs 25% in 0.0021 secs 50% in 0.0022 secs 75% in 0.0023 secs 90% in 0.0023 secs 0% in 0.0000 secs 0% in 0.0000 secs Details (average, fastest, slowest): DNS+dialup:\t0.0007 secs, 0.0018 secs, 0.0023 secs DNS-lookup:\t0.0003 secs, 0.0002 secs, 0.0006 secs req write:\t0.0001 secs, 0.0001 secs, 0.0002 secs resp wait:\t0.0011 secs, 0.0007 secs, 0.0013 secs resp read:\t0.0002 secs, 0.0001 secs, 0.0002 secs Status code distribution: [200]\t10 responses Error distribution: [10]\tGet http://proxy.local:8000: net/http: request canceled (Client.Timeout exceeded while awaiting headers) 发生了什么？rps（requests per second） 变成了 1.9？之前的 1000 rps 到哪去了？为什么会有错误响应？\n为了解释这个现象，有必要来探究一下 Nginx 是怎样实现流量镜像的。\nNginx 如何实现流量镜像 # 当请求到达 Nginx 时，如果 Nginx 开启了流量镜像功能，它就会将请求复制一份，并根据 mirror location 中的配置来处理这份复制的请求。本文我们只是将复制的请求转发到镜像后端。\n下面到了关键部分，复制的镜像请求和原始请求是相关联的，按照我的理解，只要镜像请求没有处理完成，原始请求就会被阻塞。\n这就是为什么上一个测试的结果接近于 2 rps，hey 先发送了 10 个请求，没有响应；再发送 10 个请求，但这 10 个请求被阻塞了，因为之前的镜像请求发生了延迟，导致最后 10 个请求超时并返回错误响应。\n如果我们将测试工具可接受的延迟时间增加到 10 秒，就不会出现错误了：\n$ hey -z 10s -q 1000 -n 100000 -c 1 -t 10 http://proxy.local:8000 Summary: Total:\t10.0197 secs Slowest:\t1.0018 secs Fastest:\t0.0020 secs Average:\t0.9105 secs Requests/sec:\t1.0978 Total data:\t6732 bytes Size/request:\t612 bytes Response time histogram: 0.002 [1] |■■■■ 0.102 [0] | 0.202 [0] | 0.302 [0] | 0.402 [0] | 0.502 [0] | 0.602 [0] | 0.702 [0] | 0.802 [0] | 0.902 [0] | 1.002 [10] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ Latency distribution: 10% in 1.0011 secs 25% in 1.0012 secs 50% in 1.0016 secs 75% in 1.0016 secs 90% in 1.0018 secs 0% in 0.0000 secs 0% in 0.0000 secs Details (average, fastest, slowest): DNS+dialup:\t0.0001 secs, 0.0020 secs, 1.0018 secs DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0005 secs req write:\t0.0001 secs, 0.0000 secs, 0.0002 secs resp wait:\t0.9101 secs, 0.0008 secs, 1.0015 secs resp read:\t0.0002 secs, 0.0001 secs, 0.0003 secs Status code distribution: [200]\t11 responses 现在我们搞清楚了原因 : 如果镜像请求响应很缓慢，原始请求就会被阻塞。\n我不知道如何修复这个 bug，但我想到了一个方法可以缓解这个 bug 带来的影响：只复制流量的一部分。具体的实现方法见下文。\n只复制流量的一部分 # 如果你不确定镜像后端是否能够正确处理原始请求，你可以只复制一部分流量到镜像后端，例如 10%。\nmirror 指令没有更多的配置项，它只会将所有的请求复制一份，并根据 mirror location 中的配置来处理请求，所以在 mirror 指令中做文章是行不通的，我们只能修改 mirror location 中的配置。修改后的配置文件如下：\n1\tupstream backend { 2\tserver backend.local:10000; 3\t} 4\t5\tupstream test_backend { 6\tserver test.local:20000; 7\t} 8\t9\tsplit_clients $remote_addr $mirror_backend { 10\t50% test_backend; 11\t* \u0026#34;\u0026#34;; 12\t} 13\t14\tserver { 15\tserver_name proxy.local; 16\tlisten 8000; 17\t18\taccess_log /var/log/nginx/proxy.log; 19\terror_log /var/log/nginx/proxy.error.log info; 20\t21\tlocation / { 22\tmirror /mirror; 23\tproxy_pass http://backend; 24\t} 25\t26\tlocation = /mirror { 27\tinternal; 28\tif ($mirror_backend = \u0026#34;\u0026#34;) { 29\treturn 400; 30\t} 31\t32\tproxy_pass http://$mirror_backend$request_uri; 33\t} 34\t35\t} 在 mirror location 中，请求会被转发到 $mirror_backend 变量（32 行）定义的后端。$mirror_backend 变量由 split_clients 配置块定义，split_clients 会将左边的变量 $remote_addr（requests remote address）经过 MurmurHash2 算法进行哈希，得出的值如果在前 50%（从 0 到 2147483500），那么 $mirror_backend 的值为 test_backend；如果不在前 50%，那么 $mirror_backend 的值为空字符 \u0026quot;\u0026quot;。\n这样我们就实现了只复制部分流量到镜像后端，如果 $mirror_backend 变量的值为空字符串，就不复制流量；其他情况就会将流量到镜像后端。因为镜像请求的错误响应并不会影响原始请求，所以丢弃镜像请求并返回错误响应是很安全的。\n这个方法的优点在于你可以根据任何变量或变量组合来拆分镜像流量。如果你想真正区分用户，那么 remote address 可能不适合作为拆分镜像流量的依据，因为用户可能会更换 IP。这时你最好使用用户粘性密钥来拆分镜像流量，例如 API key。\n比如，如果你想根据请求中的 apikey 来拆分镜像流量，只需要将 split_client 配置块中的 $remote_addr 改为 $arg_apikey：\nsplit_clients $arg_apikey $mirror_backend { 50% test_backend; * \u0026#34;\u0026#34;; } 现在如果你查询从 1 到 20 这几个 apikey，只有一半（11）的请求会被复制到镜像后端：\n$ for i in {1..20};do curl -i \u0026#34;proxy.local:8000/?apikey=${i}\u0026#34; ;done 查看镜像后端的日志：\n... 2019/01/13 22:34:34 addr=127.0.0.1:47224 host=test_backend uri=\u0026#34;/?apikey=1\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47230 host=test_backend uri=\u0026#34;/?apikey=2\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47240 host=test_backend uri=\u0026#34;/?apikey=4\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47246 host=test_backend uri=\u0026#34;/?apikey=5\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47252 host=test_backend uri=\u0026#34;/?apikey=6\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47262 host=test_backend uri=\u0026#34;/?apikey=8\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47272 host=test_backend uri=\u0026#34;/?apikey=10\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47278 host=test_backend uri=\u0026#34;/?apikey=11\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47288 host=test_backend uri=\u0026#34;/?apikey=13\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47298 host=test_backend uri=\u0026#34;/?apikey=15\u0026#34; 2019/01/13 22:34:34 addr=127.0.0.1:47308 host=test_backend uri=\u0026#34;/?apikey=17\u0026#34; ... 这个方法的奇妙之处在于 split_client 对流量的拆分结果是保持恒定的，apikey=1 的请求会一直被复制到镜像后端。\n总结 # 这就是我使用 Nginx 的 mirror 模块过程中的一些趟坑经历，本文向你们展示了如何简单地复制所有的流量，以及如何通过 split_client 模块来复制部分流量，同时我还解释了当镜像后端响应缓慢时为什么原始请求会被阻塞，并给出了解决方案。\n","date":"2019年1月28日","externalUrl":null,"permalink":"/posts/nginx-mirror/","section":"博客","summary":"原文链接： nginx mirroring tips and tricks 最近我在研究 Nginx 1.13.4 最新的 mirror 模块，利用 mirror 模块","title":"Nginx 流量镜像使用技巧","type":"posts"},{"content":"","date":"2019年1月28日","externalUrl":null,"permalink":"/categories/load-balancing/","section":"分类","summary":"","title":"负载均衡","type":"categories"},{"content":"在微服务领域，各个服务需要在网络上执行大量的调用。而网络是很脆弱的，如果某个服务繁忙或者无法响应请求，将有可能引发集群的大规模级联故障，从而造成整个系统不可用，通常把这种现象称为 服务雪崩效应。为了使服务有一定的冗余，以便在系统故障期间能够保持服务能力，我们可以使用熔断机制。\n什么是熔断？ # 熔断（Circuit Breaking）这一概念来源于电子工程中的断路器（Circuit Breaker）。在互联网系统中，当下游服务因访问压力过大而响应变慢或失败，上游服务为了保护系统整体的可用性，可以暂时切断对下游服务的调用。这种牺牲局部，保全整体的措施就叫做熔断。\n如果不采取熔断措施，我们的系统会怎样呢？我们来看一个栗子。\n当前系统中有 A、B、C 三个服务，服务 A 是上游，服务 B 是中游，服务 C 是下游。它们的调用链如下：\n一旦下游服务 C 因某些原因变得不可用，积压了大量请求，服务 B 的请求线程也随之阻塞。线程资源逐渐耗尽，使得服务 B 也变得不可用。紧接着，服务 A 也变为不可用，整个调用链路被拖垮。\n像这种调用链路的连锁故障，就是上文所说的服务雪崩效应。\n正所谓刮骨疗毒，壮士断腕。在这种时候，就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程如下：\n这里需要解释两点：\n开启熔断：在固定时间窗口内，接口调用超时比率达到一个阈值，会开启熔断。进入熔断状态后，后续对该服务接口的调用不再经过网络，直接执行本地的默认方法，达到服务降级的效果。 熔断恢复：熔断不可能是永久的，当经过了规定时间之后，服务将从熔断状态恢复过来，再次接受调用方的远程调用。 Istio 中的熔断 # Istio 是通过 Envoy Proxy 来实现熔断机制的，Envoy 强制在网络层面配置熔断策略，这样就不必为每个应用程序单独配置或重新编程。下面就通过一个示例来演示如何为 Istio 网格中的服务配置熔断的连接数、请求数和异常检测。\n该示例的架构如图所示：\n该示例由客户端和服务端组成，其中客户端是一个 Java HTTP 应用程序，被打包在镜像 docker.io/ceposta/http-envoy-client-standalone:latest 中，它用来模拟对后端服务 httpbin 发起 http 调用，所有的调用首先都会被 Envoy Proxy 拦截。\n假设你的集群中已经部署了 Istio，没有启用 Sidecar 的自动注入，并且没有启用双向 TLS 身份验证。 部署示例 # 部署 httpbin 应用，该应用将会作为本示例的后端服务：\n# 进入 istio 根目录 $ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/httpbin/httpbin.yaml) 创建一个 DestinationRule，针对 httpbin 服务设置熔断策略：\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: httpbin spec: host: httpbin trafficPolicy: connectionPool: tcp: maxConnections: 1 http: http1MaxPendingRequests: 1 maxRequestsPerConnection: 1 outlierDetection: consecutiveErrors: 2 interval: 1s baseEjectionTime: 3m maxEjectionPercent: 100 EOF 查看该策略在 Envoy 中对应的 Cluster 配置：\n$ kubectl get pod -l app=httpbin NAME READY STATUS RESTARTS AGE httpbin-d6d68fb97-cswzc 2/2 Running 0 2m $ istioctl pc cluster httpbin-d6d68fb97-cswzc --fqdn httpbin.default.svc.cluster.local --direction outbound -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|8000||httpbin.default.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|8000||httpbin.default.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;maxRequestsPerConnection\u0026#34;: 1, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ { \u0026#34;maxConnections\u0026#34;: 1, \u0026#34;maxPendingRequests\u0026#34;: 1 } ] }, \u0026#34;outlierDetection\u0026#34;: { \u0026#34;interval\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;baseEjectionTime\u0026#34;: \u0026#34;180.000s\u0026#34;, \u0026#34;maxEjectionPercent\u0026#34;: 100, \u0026#34;enforcingConsecutive5xx\u0026#34;: 0, \u0026#34;consecutiveGatewayFailure\u0026#34;: 2, \u0026#34;enforcingConsecutiveGatewayFailure\u0026#34;: 100 } } ] 上面的配置告诉我们：\nmaxConnections : 限制对后端服务发起的 HTTP/1.1 连接数，如果超过了这个限制，就会开启熔断。 maxPendingRequests : 限制待处理请求列表的长度， 如果超过了这个限制，就会开启熔断。 maxRequestsPerConnection : 在任何给定时间内限制对后端服务发起的 HTTP/2 请求数，如果超过了这个限制，就会开启熔断。 下面分别对这几个参数做详细解释。\nmaxConnections : 表示在任何给定时间内， Envoy 与上游集群（这里指的是 httpbin 服务）建立的最大连接数。该配置仅适用于 HTTP/1.1 协议，因为 HTTP/2 协议可以在同一个 TCP 连接中发送多个请求，而 HTTP/1.1 协议在同一个连接中只能处理一个请求。如果超过了这个限制（即断路器溢出），集群的 upstream_cx_overflow 计数器就会递增。 maxPendingRequests : 表示待处理请求队列的长度。因为 HTTP/2 是通过单个连接并发处理多个请求的，因此该熔断策略仅在创建初始 HTTP/2 连接时有用，之后的请求将会在同一个 TCP 连接上多路复用。对于 HTTP/1.1 协议，只要没有足够的上游连接可用于立即分派请求，就会将请求添加到待处理请求队列中，因此该断路器将在该进程的生命周期内保持有效。如果该断路器溢出，集群的 upstream_rq_pending_overflow 计数器就会递增。 maxRequestsPerConnection : 表示在任何给定时间内，上游集群中所有主机（这里指的是 httpbin 服务）可以处理的最大请求数。实际上，这适用于仅 HTTP/2 集群，因为 HTTP/1.1 集群由最大连接数断路器控制。如果该断路器溢出，集群的 upstream_rq_pending_overflow 计数器就会递增。 Istio DestinationRule 与 Envoy 的熔断参数对照表如下所示：\nEnvoy paramether Envoy upon object Istio parameter Istio upon ojbect max_connections cluster.circuit_breakers maxConnections TCPSettings max_pending_requests cluster.circuit_breakers http1MaxPendingRequests HTTPSettings max_requests cluster.circuit_breakers http2MaxRequests HTTPSettings max_retries cluster.circuit_breakers maxRetries HTTPSettings connect_timeout_ms cluster connectTimeout TCPSettings max_requests_per_connection cluster maxRequestsPerConnection HTTPSettings 最大连接数 # 现在我们已经为 httpbin 服务设置了熔断策略，接下来创建一个 Java 客户端，用来向后端服务发送请求，观察是否会触发熔断策略。这个客户端可以控制连接数量、并发数、待处理请求队列，使用这一客户端，能够有效的触发前面在目标规则中设置的熔断策略。该客户端的 deployment yaml 内容如下：\n# httpbin-client-deploy.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: httpbin-client-v1 spec: replicas: 1 template: metadata: labels: app: httpbin-client version: v1 spec: containers: - image: ceposta/http-envoy-client-standalone:latest imagePullPolicy: IfNotPresent name: httpbin-client command: [\u0026#34;/bin/sleep\u0026#34;,\u0026#34;infinity\u0026#34;] 这里我们会把给客户端也进行 Sidecar 的注入，以此保证 Istio 对网络交互的控制：\n$ kubectl apply -f \u0026lt;(istioctl kube-inject -f httpbin-client-deploy.yaml) 下面来观察一下当客户端试图使用太多线程与上游集群建立并发连接时，Envoy 会如何应对。\n在上面的熔断设置中指定了 maxConnections: 1 以及 http1MaxPendingRequests: 1。这意味着如果超过了一个连接同时发起请求，Istio 就会熔断，阻止后续的请求或连接。\n先尝试通过单线程（NUM_THREADS=1）创建一个连接，并进行 5 次调用（默认值：NUM_CALLS_PER_CLIENT=5）：\n$ CLIENT_POD=$(kubectl get pod | grep httpbin-client | awk \u0026#39;{ print $1 }\u0026#39;) $ kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c \u0026#39;export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=1 \u0026amp;\u0026amp; java -jar http-client.jar\u0026#39; using num threads: 1 Starting pool-1-thread-1 with numCalls=5 delayBetweenCalls=0 url=http://localhost:15001/get mixedRespTimes=false pool-1-thread-1: successes=[5], failures=[0], duration=[545ms] 可以看到所有请求都通过了：\nsuccesses=[5] 我们可以查询 istio-proxy 的状态，获取更多相关信息：\n$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c \u0026#39;curl localhost:15000/stats\u0026#39; | grep httpbin ... cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_http1_total: 5 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_overflow: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 5 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_200: 5 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 5 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_retry: 0 ... 可以看出总共发送了 5 个 HTTP/1.1 连接，也就是 5 个请求，响应码均为 200。\n下面尝试把线程数提高到 2：\n$ kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c \u0026#39;export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=2 \u0026amp;\u0026amp; java -jar http-client.jar\u0026#39; using num threads: 2 Starting pool-1-thread-1 with numCalls=5 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false Starting pool-1-thread-2 with numCalls=5 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false pool-1-thread-1: successes=[3], failures=[2], duration=[96ms] pool-1-thread-2: successes=[4], failures=[1], duration=[87ms] 再次查看 istio-proxy 的状态：\n$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c \u0026#39;curl localhost:15000/stats\u0026#39; | grep httpbin ... cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_http1_total: 7 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_overflow: 3 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 10 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_200: 7 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 7 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_503: 3 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_5xx: 3 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 3 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_retry: 0 ... 总共发送了 10 个 HTTP/1 连接，只有 7 个 被允许通过，剩余请求被断路器拦截了。其中 upstream_cx_overflow 的值为 3，表明 maxConnections 断路器起作用了。Istio-proxy 允许一定的冗余，你可以将线程数提高到 3，熔断的效果会更明显。\n待处理请求队列 # 测试完 maxConnections 断路器之后，我们再来测试一下 maxPendingRequests 断路器。前面已经将 maxPendingRequests 的值设置为 1，现在按照预想，我们只需要模拟在单个 HTTP/1.1 连接中同时发送多个请求，就可以触发该断路器开启熔断。由于 HTTP/1.1 同一个连接只能处理一个请求，剩下的请求只能放到待处理请求队列中。通过限制待处理请求队列的长度，可以对恶意请求、DoS 和系统中的级联错误起到一定的缓解作用。\n现在尝试通过单线程（NUM_THREADS=1）创建一个连接，并同时发送 20 个请求（PARALLEL_SENDS=true，NUM_CALLS_PER_CLIENT=20）：\n$ kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c \u0026#39;export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=1 export PARALLEL_SENDS=true export NUM_CALLS_PER_CLIENT=20 \u0026amp;\u0026amp; java -jar http-client.jar\u0026#39; using num threads: 1 Starting pool-1-thread-1 with numCalls=20 parallelSends=true delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false finished batch 0 finished batch 5 finished batch 10 finished batch 15 pool-1-thread-1: successes=[16], failures=[4], duration=[116ms] 查询 istio-proxy 的状态：\n$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c \u0026#39;curl localhost:15000/stats\u0026#39; | grep httpbin | grep pending cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_active: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_failure_eject: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 4 cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_total: 16 upstream_rq_pending_overflow 的值是 4，说明有 4 次调用触发了 maxPendingRequests 断路器的熔断策略，被标记为熔断。\n如果服务完全崩溃怎么办？ # 现在我们知道 Envoy 的熔断策略对集群中压力过大的上游服务起到一定的保护作用，但还有一种极端的情况需要我们考虑，如果集群中的某些节点完全崩溃（或者即将完全崩溃）该怎么办？\n为了专门应对这种情况，Envoy 中引入了异常检测的功能，通过周期性的异常检测来动态确定上游集群中的某些主机是否异常，如果发现异常，就将该主机从连接池中隔离出去。异常检测是被动健康检查的一种形式，Envoy 同时支持 主动健康检查和被动健康检查，它们可以同时启用，联合决定上游主机的健康状况。\n异常检测的隔离算法 # 根据异常检测的类型，对主机的隔离可以连续执行（例如连续返回 5xx 状态码），也可以周期性执行（例如配置了周期性成功率检测）。隔离算法的工作流程如下：\n检测到了某个主机异常。 如果到目前为止负载均衡池中还没有主机被隔离出去，Envoy 将会立即隔离该异常主机；如果已经有主机被隔离出去，就会检查当前隔离的主机数是否低于设定的阈值（通过 outlier_detection.max_ejection_percent 指定），如果当前被隔离的主机数量不超过该阈值，就将该主机隔离出去，否则不隔离。 隔离不是永久的，会有一个时间限制。当主机被隔离后，该主机就会被标记为不健康，并且不会被加入到负载均衡池中，除非负载均衡处于 恐慌模式。隔离时间等于 outlier_detection.base_ejection_time_ms 的值乘以主机被隔离的次数。所以如果某个主机连续出现故障，会导致它被隔离的时间越来越长。 经过了规定的隔离时间之后，被隔离的主机将会自动恢复过来，重新接受调用方的远程调用。通常异常检测会与主动健康检查一起用于全面的健康检查解决方案。 恐慌模式指的是：在这种情况下，代理服务器会无视负载均衡池的健康标记，重新向所有主机发送数据。这是一个非常棒的机制。在分布式系统中，必须了解到的一点是，有时候“理论上”的东西可能不是正常情况，最好能降低一点要求来防止扩大故障影响。另外一方面，可以对这一比例进行控制（缺省情况下超过 50% 的驱逐就会进入恐慌状态），可以提高，也可以禁止这一阈值。 异常检测类型 # Envoy 支持一下几种异常检测类型：\n连续 5xx 响应：如果上游主机连续返回一定数量的 5xx 响应（包括 500），该主机就会被驱逐。注意，这里的 5xx 响应不仅包括返回的 5xx 状态码，也包括 HTTP 路由返回的一个事件（如连接超时和连接错误）。隔离主机所需的 5xx 响应数量由 outlier_detection.consecutive_5xx 的值控制。 连续网关故障：如果上游主机连续返回一定数量的 \u0026quot;gateway errors\u0026quot;（502，503 或 504 状态码，但不包括 500），该主机就会被驱逐。这里同样也包括 HTTP 路由返回的一个事件（如连接超时和连接错误）。隔离主机所需的连续网关故障数量由 outlier_detection.consecutive_gateway_failure 的值控制。 调用成功率：基于调用成功率的异常检测类型会聚合集群中每个主机的调用成功率，然后根据统计的数据以给定的周期来隔离主机。如果该主机的请求数量小于 outlier_detection.success_rate_request_volume 指定的值，则不会为该主机计算调用成功率，因此聚合的统计数据中不会包括该主机的调用成功率。如果在给定的周期内具有最小所需请求量的主机数小于 outlier_detection.success_rate_minimum_hosts 指定的值，则不会对该集群执行调用成功率检测。 Istio DestinationRule 与 Envoy 的异常检测参数对照表如下所示：\nEnvoy paramether Envoy upon object Istio parameter Istio upon ojbect consecutive_gateway_failure cluster.outlier_detection consecutiveErrors outlierDetection interval cluster.outlier_detection interval outlierDetection baseEjectionTime cluster.outlier_detection baseEjectionTime outlierDetection maxEjectionPercent cluster.outlier_detection maxEjectionPercent outlierDetection Envoy 中还有一些其他参数在 Istio 中暂时是不支持的，具体参考 Envoy 官方文档 Outlier detection。\n现在我们回头再来看一下本文最初创建的 DestinationRule 中关于异常检测的配置：\noutlierDetection: consecutiveErrors: 2 interval: 1s baseEjectionTime: 3m maxEjectionPercent: 100 该配置表示每秒钟扫描一次上游主机，连续失败 2 次返回 5xx 错误码的所有主机会被移出连接池 3 分钟。\n异常检测示例 # 下面我们通过调用一个 URL 来指定 httpbin 服务返回 502 状态码，以此来触发连续网关故障异常检测。总共发起 3 次调用，因为 DestinationRule 中的配置要求 Envoy 的异常检测机制必须检测到两个连续的网关故障才会将 httpbin 服务移除负载均衡池。\n$ kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c \u0026#39;export URL_UNDER_TEST=http://httpbin:8000/status/502 export NUM_CALLS_PER_CLIENT=3 \u0026amp;\u0026amp; java -jar http-client.jar\u0026#39; using num threads: 1 Starting pool-1-thread-1 with numCalls=3 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/status/502 mixedRespTimes=false pool-1-thread-1: successes=[0], failures=[3], duration=[99ms] 查看 istio-proxy 的状态：\n$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c \u0026#39;curl localhost:15000/stats\u0026#39; | grep httpbin | grep outlier cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_active: 1 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_consecutive_5xx: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_consecutive_5xx: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_consecutive_gateway_failure: 1 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_success_rate: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_consecutive_5xx: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_consecutive_gateway_failure: 1 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_success_rate: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_total: 1 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_overflow: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_success_rate: 0 cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_total: 0 确实检测到了连续网关故障，consecutive_gateway_failure 的值为 1。但是我通过查看 EDS，发现 Envoy 并没有将 httpbin 服务移出负载均衡池：\n$ export PILOT_SVC_IP=$(kubectl -n istio-system get svc istio-pilot -o go-template=\u0026#39;{{.spec.clusterIP}}\u0026#39;) $ curl -s http://$PILOT_SVC_IP:8080/debug/edsz|grep \u0026#34;outbound|8000||httpbin.default.svc.cluster.local\u0026#34; -B 1 -A 15 { \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|8000||httpbin.default.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;locality\u0026#34;: { }, \u0026#34;lbEndpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;172.30.104.27\u0026#34;, \u0026#34;portValue\u0026#34;: 8000 } } }, 回去重新看了一下 Envoy 的配置，enforcingConsecutiveGatewayFailure 的值为 100，理论上 outlierDetection 应该是生效的。但这里我疏忽了 Envoy 的恐慌模式，Envoy 默认的恐慌阈值是 50%，而 httpbin 应用只有一个 upstream，所以没有被移除。当然这只是个人猜测，暂时也没时间验证，大家可以自己验证一下。\n参考资料 # istio.networking.v1alpha3 OutlierDetection Istio 流量管理之熔断 Envoy Proxy和Netflix Hystrix，究竟谁才是熔断王者？ Envoy Outlier detection Envoy Panic threshold Microservices Patterns With Envoy Sidecar Proxy, Part I: Circuit Breaking ","date":"2018年12月28日","externalUrl":null,"permalink":"/posts/circuit_breaking-and-outlier-detection-in-istio/","section":"博客","summary":"在微服务领域，各个服务需要在网络上执行大量的调用。而网络是很","title":"熔断与异常检测在 Istio 中的应用","type":"posts"},{"content":" 原文链接：Optimizing Kubernetes resource allocation in production\n我和 Kubernetes 的初次接触就涉及到将应用容器化并部署到生产环境集群中，当时我的工作重点是把 buffer 吞吐量最高（低风险）的某个端点从单个应用程序中分离出来，因为这个特殊的端点会给我们带来很大的困扰，偶尔还会影响到其他更高优先级的流量。\n在使用 curl 进行一些手动测试之后，我们决定将这个剥离出来的端点部署在 Kubernetes 上。当有 1% 的流量打进来时，服务运行正常，一切看起来都是那么地美好；当流量增加到 10% 时，也没有什么大问题；最后我将流量增加到 50%，麻烦来了，这时候服务突然陷入了 crash 循环状态。当时我的第一反应是将该服务的副本数扩到 20 个，扩完之后有一点成效，但没过多久 Pod 仍然陷入 crash 循环状态。通过 kubectl describe 查看审计日志，我了解到 Kubelet 因为 OOMKilled 杀掉了 Pod，即内存不足。深入挖掘后，我找到了问题根源，当时我从另一个 deployment 文件中复制粘贴 YAML 内容时设置了一些严格的内存限制，从而导致了上述一系列问题。这段经历让我开始思考如何才能有效地设置资源的 requests 和 limits。\n请求（requests）和限制（limits） # Kubernetes 允许在 CPU，内存和本地存储（v1.12 中的 beta 特性）等资源上设置可配置的请求和限制。像 CPU 这样的资源是可压缩的，这意味着对 CPU 资源的限制是通过 CPU 管理策略来控制的。而内存等其他资源都是不可压缩的，它们都由 Kubelet 控制，如果超过限制就会被杀死。使用不同的 requests 和 limits 配置，可以为每个工作负载实现不同的服务质量（QoS）。\nLimits # limits 表示允许工作负载消耗资源的上限，如果资源的使用量越过配置的限制阈值将会触发 Kubelet 杀死 Pod。如果没有设置 limits，那么工作负载可以占用给定节点上的所有资源；如果有很多工作负载都没有设置 limits，那么资源将会被尽最大努力分配。\nRequests # 调度器使用 requests 来为工作负载分配资源，工作负载可以使用所有 requests 资源，而无需 Kubernetes 的干预。如果没有设置 limits 并且资源的使用量超过了 requests 的阈值，那么该容器的资源使用量很快会被限制到低于 requests 的阈值。如果只设置了 limits，Kubernetes 会自动把对应资源的 requests 设置成和 limits 一样。\nQoS（服务质量） # 在 Kubernetes 中通过资源和限制可以实现三种基本的 QoS，QoS 的最佳配置主要还是取决于工作负载的需求。\nGuaranteed QoS # 通过只设置 limits 而不设置 requests 就可以实现 Guaranteed QoS，这意味着容器可以使用调度器为其分配的所有资源。对于绑定 CPU 和具有相对可预测性的工作负载（例如，用来处理请求的 Web 服务）来说，这是一个很好的 QoS 等级。\nBurstable QoS # 通过配置 CPU 或内存的 limits 和 requests，并且 requests \u0026lt; limits，就可以实现 Burstable QoS。这意味着容器的资源使用量可以达到 requests 阈值，同时如果该容器运行的节点上资源充足，那么容器可以继续使用资源，只要不超过 limits 阈值就行。这对短时间内需要消耗大量资源或者初始化过程很密集的工作负载非常有用，例如：用来构建 Docker 容器的 Worker 和运行未优化的 JVM 进程的容器都可以使用该 QoS 等级。\nBest effort QoS # 通过既不设置 limits 也不设置 requests，可以实现 Best effort QoS。这意味着容器可以使用宿主机上任何可用的资源。从调度器的角度来看，这是最低优先级的任务，并且会在 Burstable QoS Pod 和 Guaranteed QoS Pod 之前被先杀掉。这对于可中断和低优先级的工作负载非常有用，例如：迭代运行的幂等优化过程。\n设置 requests 和 limits # 设置 limits 和 requests 的关键是找到单个 Pod 的断点。通过使用几种不同的负载测试技术，可以在应用程序部署到生产环境之前对应用程序的故障模式有一个全面的了解。当资源使用量达到限制阈值时，几乎每个应用程序都有自己的一组故障模式。\n在准备测试之前，请确保将 Pod 的副本数设置为 1，并且将 limits 设置为一组保守的数字，例如：\n# limits might look something like replicas: 1 ... cpu: 100m # ~1/10th of a core memory: 50Mi # 50 Mebibytes 注意 : 在测试过程中设置 limits 非常重要，它可以让我们看到预期的效果（在内存较高时限制 CPU 并杀死 Pod）。在测试的迭代过程中，最好每次只更改一种资源限制（CPU 或内存），不要同时更改。\n负载增加测试 # 负载增加测试会随着时间的推移增加负载，直到负载下的服务突然失败或测试完成。\n如果负载增加测试突然失败，则表明资源限制过于严格，这是一个很好的迹象。当观察到图像有明显抖动时，将资源限制增加一倍并重复，直到测试成功完成。\n当资源限制接近最优时，性能应该随着时间的推移而可预测地降低（至少对于 Web 服务而言应该是这样）。\n如果在增加负载的过程中性能并没有太大的变化，则说明为工作负载分配了太多的资源。\n负载不变测试 # 在运行负载增加测试并调整资源限制之后，下一步就开始进行负载不变测试。负载不变测试会在一段很长的时间内（至少 10 分钟，时间再长一点更好）对应用施加相同的负载，至于加多少负载，最好选择在图像出现断点之前的压力值（例如：客户端数量）。\n此测试的目的是识别内存泄漏和隐藏的排队机制，因为这些机制在负载增加测试中很难被捕获到。到了这个阶段，即使还要对资源限制进行调整，调整的幅度也应该很小。理想情况下，该阶段测试期间性能应该会保持稳定。\n记录失败日志 # 在测试过程中，记录服务失败时做了哪些操作是至关重要的。可以将发现的故障模式添加到相关的书籍和文档中，这对分类生产环境中出现的问题很有用。下面是我们在测试过程中发现的一些故障模式：\n内存缓慢增加 CPU 使用率达到 100% 响应时间太长 请求被丢弃 不同请求的响应时间差异很大 你最好将这些发现都收集起来，以备不时之需，因为有一天它们可能会为你或团队节省一整天的时间。\n一些有用的工具 # 虽然你可以使用 Apache Bench 等工具来增加负载，也可以使用 cAdvisor 来可视化资源使用率，但这里我要介绍一些更适合负载测试的工具。\nLoader.io # Loader.io 是一个在线负载测试工具，它允许你配置负载增加测试和负载不变测试，在测试过程中可视化应用程序的性能和负载，并能快速启动和停止测试。它也会保存测试结果的历史记录，因此在资源限制发生变化时很容易对结果进行比较。\nKubescope cli # Kubescope cli 是一个可以运行在本地或 Kubernetes 中的工具，可直接从 Docker Daemon 中收集容器指标并可视化。和 cAdvisor 等其他集群指标收集服务一样， kubescope cli 收集指标的周期是 1 秒（而不是 10-15 秒）。如果周期是 10-15 秒，你可能会在测试期间错过一些引发性能瓶颈的问题。如果你使用 cAdvisor 进行测试，每次都要使用新的 Pod 作为测试对象，因为 Kubernetes 在超过资源限制时就会将 Pod 杀死，然后重新启动一个全新的 Pod。而 kubescope cli 就没有这方面的忧虑，它直接从 Docker Daemon 中收集容器指标（你可以自定义收集指标的时间间隔），并使用正则表达式来选择和过滤你想要显示的容器。\n总结 # 我发现在搞清楚服务什么时候会出现故障以及为什么会出现故障之前，不应该将其部署到生产环境中。我希望您能从我的错误中吸取教训，并通过一些技术手段来设置应用的资源 limits 和 requests。这将会为你的系统增加弹性能力和可预测性，使你的客户更满意，并有望帮助你获得更多的睡眠。\n","date":"2018年12月18日","externalUrl":null,"permalink":"/posts/optimizing-kubernetes-resource-allocation-production/","section":"博客","summary":"原文链接：Optimizing Kubernetes resource allocation in production 我和 Kubernetes 的初次接触就涉","title":"优化生产环境中的 Kubernetes 资源分配","type":"posts"},{"content":"众所周知，当我们讨论 Istio 时，性能并不是它最大的痛点，最大的痛点是有时候会出现一些莫名其妙的问题，而我们根本不知道问题出在哪里，也无从下手，在很多方面它仍然是一个谜。你可能已经看过它的官方文档，有的人可能已经尝试使用了，但你真的理解它了吗？\n今天就为大家推荐一个高质量的视频，视频中的演讲内容主要通过跟踪一个网络包进入 Istio 网格，完成一系列的交互，然后再从网格出来的整个过程，以此来探索数据包在 Istio 网格中的生命周期。你将会了解到当数据包遇到每个组件时，会如何调用这些组件，这些组件为什么存在，它可以为数据包做些什么，其中还会涉及到数据包在进出网格的过程中是如何调用控制平面的，最后还会告诉你一些调试 Istio 的套路。\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 视频中的 PPT 下载： Download Now\n","date":"2018年12月17日","externalUrl":null,"permalink":"/posts/life-of-a-packet-through-istio/","section":"博客","summary":"众所周知，当我们讨论 Istio 时，性能并不是它最大的痛点，最大的痛点","title":"数据包在 Istio 网格中的生命周期","type":"posts"},{"content":"Istio 要求集群中 VirtualService 定义的所有目标主机都是唯一的。当使用目标主机的短名称时（不包含 '.' 的目标主机，例如使用 reviews，而不是 reviews.default.svc.cluster.local），Istio 会将该短名称转换为 VirtualService 规则所在的命名空间的 FQDN，而不是转换为目标主机所在的命名空间的 FQDN。因此，当在不同的命名空间中定义 VirtualService 资源时允许目标主机的短名称重复。当你的目标主机包含 * 通配符前缀、IP 地址或 Web 地址时，VirtualService 不会将其视为短名称，也就不会尝试将其转换为 FQDN。反正无论如何，目标主机必须是唯一的。\n目标主机冲突示例 # 下面举几个目标主机冲突的例子，以帮助大家加深对这方面的理解。\n示例 1 # 下面两个 VirtualService 的目标主机的 FQDN 分别是 reviews.foo.svc.cluster.local 和 reviews.bar.svc.cluster.local ，这是允许的。\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs1 namespace: foo spec: hosts: - reviews ... --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs2 namespace: bar spec: hosts: - reviews ... 示例 2 # 下面两个 VirtualService 的目标主机的 FQDN 都是 reviews.default.svc.cluster.local，这是不推荐的，会导致不确定的路由行为。\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs3 namespace: default spec: hosts: - reviews ... --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs4 namespace: default spec: hosts: - reviews ... 优化方案请参考下文的 使目标主机唯一。\n示例 3 # 下面这种写法也是不推荐的，因为它在两个不同的 VirtualService 资源中定义了相同的 Web 地址，会导致路由冗余。\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs5 namespace: foo spec: hosts: - google.com http: - match: - uri: prefix: /search route: - destination: host: search.foo.svc.cluster.local --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs6 namespace: foo spec: hosts: - google.com http: - match: - uri: prefix: /mail route: - destination: host: mail.foo.svc.cluster.local 优化方案请参考下文的 合并冲突的 VirtualService。\n优化方案 # 这里给出两个优化准则，可以改进上文的不恰当写法。\n使目标主机唯一 # 可以将冲突的 VirtualService 中定义的目标主机更改为唯一的。以下的 VirtualServices 具有唯一的目标主机 reviews 和 ratings，可以用来优化上面示例 2 的写法。\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs3 namespace: default spec: hosts: - reviews ... --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs4 namespace: default spec: hosts: - ratings ... 合并冲突的 VirtualService # 可以将冲突的 VirtualService 中定义的路由规则合并到同一个 VirtualService 中。下面的 VirtualService 可以解决示例 3 的问题，因为规则已合并，并且仅保留具有目标主机 google.com 的单个 VirtualService。\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs5 namespace: foo spec: hosts: - google.com http: - match: - uri: prefix: /search route: - destination: host: search.foo.svc.cluster.local - match: - uri: prefix: /mail route: - destination: host: mail.foo.svc.cluster.local ","date":"2018年12月15日","externalUrl":null,"permalink":"/posts/conflictingvirtualservicehost/","section":"博客","summary":"Istio 要求集群中 VirtualService 定义的所有目标主机都是唯一的。当使用目标主机的","title":"Istio 中 VirtualService 的注意事项","type":"posts"},{"content":" 原文链接： Migrating from NGINX to Envoy Proxy\n本文将会手把手教你如何从 Nginx 迁移到 Envoy Proxy，你可以将任何以前的经验和对 Nginx 的理解直接应用于 Envoy Proxy 中。\n主要内容：\n配置 Envoy Proxy 的 server 配置项 配置 Envoy Proxy 以将流量代理到外部服务 配置访问日志和错误日志 学完本教程之后，你将会了解 Envoy Proxy 的核心功能，以及如何将现有的 Nginx 配置文件迁移到 Envoy Proxy 中。\nNginx 与 Envoy Proxy 的核心模块 # 先来看一个 Nginx 配置文件的完整示例，该配置文件取自于 Nginx wiki，内容如下：\n$ cat nginx.conf user www www; pid /var/run/nginx.pid; worker_processes 2; events { worker_connections 2000; } http { gzip on; gzip_min_length 1100; gzip_buffers 4 8k; gzip_types text/plain; log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#39; \u0026#39;\u0026#34;$request\u0026#34; $status $bytes_sent \u0026#39; \u0026#39;\u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#39; \u0026#39;\u0026#34;$gzip_ratio\u0026#34;\u0026#39;; log_format download \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#39; \u0026#39;\u0026#34;$request\u0026#34; $status $bytes_sent \u0026#39; \u0026#39;\u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#39; \u0026#39;\u0026#34;$http_range\u0026#34; \u0026#34;$sent_http_content_range\u0026#34;\u0026#39;; upstream targetCluster { 172.18.0.3:80; 172.18.0.4:80; } server { listen 8080; server_name one.example.com www.one.example.com; access_log /var/log/nginx.access_log main; error_log /var/log/nginx.error_log info; location / { proxy_pass http://targetCluster/; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } } Nginx 的配置通常分为三个关键要素：\n配置 Server 块、日志和 gzip 功能，这些配置对全局生效，可以应用于所有示例。 配置 Nginx 以接收 8080 端口上对域名 one.example.com 的访问请求。 将 URL 的不同路径的流量转发到不同的目标后端。 并不是所有的 Nginx 配置项都适用于 Envoy Proxy，其中有一些配置在 Envoy 中可以忽略。Envoy Proxy 有四个关键组件，可以用来匹配 Nginx 的核心配置块：\n监听器（Listener）：监听器定义了 Envoy 如何处理入站请求，目前 Envoy 仅支持基于 TCP 的监听器。一旦建立连接之后，就会将该请求传递给一组过滤器（filter）进行处理。 过滤器（Filter）：过滤器是处理入站和出站流量的链式结构的一部分。在过滤器链上可以集成很多特定功能的过滤器，例如，通过集成 GZip 过滤器可以在数据发送到客户端之前压缩数据。 路由（Router）：路由用来将流量转发到具体的目标实例，目标实例在 Envoy 中被定义为集群。 集群（Cluster）：集群定义了流量的目标端点，同时还包括一些其他可选配置，如负载均衡策略等。 接下来我们将使用这四个关键组件创建一个 Envoy Proxy 配置文件，以匹配前面定义的 Nginx 配置文件。\nNginx 配置迁移 # Nginx 配置文件的第一部分定义了 Nginx 本身运行的工作特性。\nWorker 连接数 # 下面的配置定义了 Nginx 的 worker 进程数和最大连接数，这表明了 Nginx 是如何通过自身的弹性能力来满足各种需求的。\nworker_processes 2; events { worker_connections 2000; } 而 Envoy Proxy 则以不同的方式来管理 Worker 进程和连接。默认情况下，Envoy 为系统中的每个硬件线程生成一个工作线程。（可以通过 --concurrency 选项控制）。每个 Worker 线程是一个“非阻塞”事件循环，负责监听每个侦听器，接受新连接，为每个连接实例化过滤器栈，以及处理所有连接生命周期内 IO 事件。所有进一步的处理都在 Worker 线程内完成，其中包括转发。\nEnvoy 中的所有连接池都和 Worker 线程绑定。 尽管 HTTP/2 连接池一次只与每个上游主机建立一个连接，但如果有四个 Worker，则每个上游主机在稳定状态下将有四个 HTTP/2 连接。Envoy 以这种方式工作的原因是将所有连接都在单个 Worker 线程中处理，这样几乎所有代码都可以在无锁的情况下编写，就像它是单线程一样。拥有太多的 Worker 将浪费内存，创建更多空闲连接，并导致连接池命中率降低。\n你可以在 Envoy Proxy 博客上找到更多信息。\nHTTP 配置 # Nginx 的下一个配置块是 HTTP 块，包括资源的媒体类型（mime type）、默认超时和 gzip 压缩配置。这些功能在 Envoy Proxy 中都是通过过滤器来实现的，下文将会详细讨论。\nServer 配置迁移 # 在 HTTP 配置块中，Nginx 配置指定了监听 8080 端口并接收对域名 one.example.com 和 www.one.example.com 的访问请求。\nserver { listen 80; server_name one.example.com www.one.example.com; 这部分配置在 Envoy 中是由 Listener 管理的。\nEnvoy 监听器 # 让 Envoy 能正常工作最重要的一步是定义监听器。首先需要创建一个配置文件用来描述 Envoy 的运行参数。\n下面的配置项将创建一个新的监听器并将其绑定到 8080 端口。\nstatic_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } 这里不需要定义 server_name，域名将会交给过滤器来处理。\nLocation 配置迁移 # 当请求进入 Nginx 时，Location 块定义了如何处理流量的元数据，以及如何转发处理后的流量。在下面的配置项中，进入站点的所有流量都被代理到名为 targetCluster 的上游集群。上游集群定了用来接收流量的后端实例，下一节再详细讨论。\nlocation / { proxy_pass http://targetCluster/; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } 这部分配置在 Envoy 中是由过滤器管理的。\nEnvoy 过滤器 # 对于静态配置文件而言，过滤器定义了如何处理传入请求。这里我们将会创建一个与上一节 Nginx 配置中的 server_names 相匹配的过滤器，当收到与过滤器中定义的域名和路由相匹配的入站请求时，就会将该请求的流量转发到指定的集群。这里的集群相当于 Nginx 中的 upstream 配置。\nfilter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: backend domains: - \u0026#34;one.example.com\u0026#34; - \u0026#34;www.one.example.com\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: targetCluster http_filters: - name: envoy.router envoy.http_connection_manager 是 Envoy 中的内置 HTTP 过滤器。除了该过滤器，Envoy 中还内置了一些其他过滤器，包括 Redis、Mongo、TCP 等，完整的过滤器列表请参考 Envoy 官方文档。\nProxy 与 upstream 配置迁移 # 在 Nginx 中，upstream 配置项定义了用来接收流量的目标服务集群。下面的 upstream 配置项分配了两个后端实例：\nupstream targetCluster { 172.18.0.3:80; 172.18.0.4:80; } 这部分配置在 Envoy 中是由集群（Cluster）管理的。\nEnvoy 集群 # upstream 配置项在 Envoy 中被定义为 Cluster。Cluster 中的 hosts 列表用来处理被过滤器转发的流量，其中 hosts 的访问策略（例如超时）也在 Cluster 中进行配置，这有利于更精细化地控制超时和负载均衡。\nclusters: - name: targetCluster connect_timeout: 0.25s type: STRICT_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN hosts: [ { socket_address: { address: 172.18.0.3, port_value: 80 }}, { socket_address: { address: 172.18.0.4, port_value: 80 }} ] 当使用 STRICT_DNS 类型的服务发现时，Envoy 将持续并异步地解析指定的 DNS 目标。DNS 结果中每个返回的 IP 地址将被视为上游集群中的显式主机。这意味着如果查询返回三个 IP 地址，Envoy 将假定该集群有三台主机，并且所有三台主机应该负载均衡。如果有主机从 DNS 返回结果中删除，则 Envoy 会认为它不再存在，并且会将它从所有的当前连接池中排除。更多详细内容请参考 Envoy 官方文档。\n日志配置迁移 # 最后一部分需要迁移的配置是应用日志。Envoy Proxy 默认情况下没有将日志持久化到磁盘中，而是遵循云原生方法，其中所有应用程序日志都输出到 stdout 和 stderr。\n关于用户请求信息的访问日志属于可选项，默认情况下是禁用的。要为 HTTP 请求启用访问日志，请在 envoy.http_connection_manager 过滤器中添加 access_log 配置项，日志路径可以是块设备（如 stdout），也可以是磁盘上的文件，具体取决于你的需求。\n下面的配置项将所有的访问日志传递给 stdout：\naccess_log: - name: envoy.file_access_log config: path: \u0026#34;/dev/stdout\u0026#34; 将该配置项复制到 envoy.http_connection_manager 过滤器的配置中，完整的过滤器配置如下：\n- name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http access_log: - name: envoy.file_access_log config: path: \u0026#34;/dev/stdout\u0026#34; route_config: Envoy 默认情况下使用格式化字符串来输出 HTTP 请求的详细日志：\n[%START_TIME%] \u0026#34;%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\u0026#34; %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \u0026#34;%REQ(X-FORWARDED-FOR)%\u0026#34; \u0026#34;%REQ(USER-AGENT)%\u0026#34; \u0026#34;%REQ(X-REQUEST-ID)%\u0026#34; \u0026#34;%REQ(:AUTHORITY)%\u0026#34; \u0026#34;%UPSTREAM_HOST%\u0026#34;\\n 本示例中的日志输出如下所示：\n[2018-11-23T04:51:00.281Z] \u0026#34;GET / HTTP/1.1\u0026#34; 200 - 0 58 4 1 \u0026#34;-\u0026#34; \u0026#34;curl/7.47.0\u0026#34; \u0026#34;f21ebd42-6770-4aa5-88d4-e56118165a7d\u0026#34; \u0026#34;one.example.com\u0026#34; \u0026#34;172.18.0.4:80\u0026#34; 可以通过设置格式化字段来自定义日志输出内容，例如：\naccess_log: - name: envoy.file_access_log config: path: \u0026#34;/dev/stdout\u0026#34; format: \u0026#34;[%START_TIME%] \u0026#34;%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\u0026#34; %RESPONSE_CODE% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \u0026#34;%REQ(X-REQUEST-ID)%\u0026#34; \u0026#34;%REQ(:AUTHORITY)%\u0026#34; \u0026#34;%UPSTREAM_HOST%\u0026#34;\\n\u0026#34; 你也可以通过设置 json_format 字段来输出 JSON 格式的日志，例如：\naccess_log: - name: envoy.file_access_log config: path: \u0026#34;/dev/stdout\u0026#34; json_format: {\u0026#34;protocol\u0026#34;: \u0026#34;%PROTOCOL%\u0026#34;, \u0026#34;duration\u0026#34;: \u0026#34;%DURATION%\u0026#34;, \u0026#34;request_method\u0026#34;: \u0026#34;%REQ(:METHOD)%\u0026#34;} 关于 Envoy 日志配置的更多详细配置请参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/access_log#config-access-log-format-dictionaries。\n在生产环境中使用 Envoy Proxy 时，日志不是获取可观察性的唯一方法，Envoy 中还内置了更高级的功能，如分布式追踪和监控指标。你可以在 分布式追踪文档中找到更多详细内容。\n完整的 Envoy 配置文件如下所示：\nstatic_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: backend domains: - \u0026#34;one.example.com\u0026#34; - \u0026#34;www.one.example.com\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: targetCluster http_filters: - name: envoy.router clusters: - name: targetCluster connect_timeout: 0.25s type: STRICT_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN hosts: [ { socket_address: { address: 172.18.0.3, port_value: 80 }}, { socket_address: { address: 172.18.0.4, port_value: 80 }} ] admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9090 } 启动 Envoy Proxy # 现在已经将 Nginx 的所有配置转化为 Envoy Proxy 的配置，接下来就是启动 Envoy 实例并进行测试。\n以普通用户身份运行 # 在 Nginx 配置文件的顶部有一行配置 user www www;，表示以低权限用户身份运行 Nginx 以提高安全性。而 Envoy 则采用云原生的方法来管理进程所有者，当我们通过容器来启动 Envoy Proxy 时，可以通过命令行参数来指定一个低权限用户。\n启动 Envoy Proxy # 下面的命令将通过容器启动 Envoy Proxy，该命令将 Envoy 容器暴露在 80 端口上以监听入站请求，但容器内的 Envoy Proxy 监听在 8080 端口上。通过 --user 参数以允许进程以低权限用户身份运行。\n$ docker run --name proxy1 -p 80:8080 --user 1000:1000 -v /root/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy 测试 # 启动代理之后，现在就可以进行访问测试了。下面的 curl 命令使用 Envoy 配置文件中定义的 请求头文件中的 Host 字段发出请求：\n$ curl -H \u0026#34;Host: one.example.com\u0026#34; localhost -i 如果不出意外，该请求将会返回 503 错误，因为上游集群还没有运行，处于不可用状态，Envoy Proxy 找不到可用的目标后端来处理该请求。下面就来启动相应的 HTTP 服务：\n$ docker run -d katacoda/docker-http-server $ docker run -d katacoda/docker-http-server 启动这些服务之后，Envoy 就可以成功将流量代理到目标后端：\n$ curl -H \u0026#34;Host: one.example.com\u0026#34; localhost -i 现在你应该会看到请求已被成功响应，并且可以从日志中看到哪个容器响应了该请求。\n附加的 HTTP 响应头文件 # 如果请求成功，你会在请求的响应头文件中看到一些附加的字段，这些字段包含了上游主机处理请求所花费的时间（以毫秒为单位）。如果客户端想要确定因为网络延迟导致的请求处理延时，这些字段将会很有帮助。\nx-envoy-upstream-service-time: 0 server: envoy ","date":"2018年12月14日","externalUrl":null,"permalink":"/posts/migrating-from-nginx-to-envoy/","section":"博客","summary":"原文链接： Migrating from NGINX to Envoy Proxy 本文将会手把手教你如何从 Nginx 迁移到 Envoy Pr","title":"Envoy 基础教程：从 Nginx 迁移到 Envoy Proxy","type":"posts"},{"content":" 原文链接： Kubernetes Design and Development Explained\n本文是 8 月 29 日至 31 日在温哥华举行的 开源峰会上作者演讲内容的一部分，详细内容请查看下文。\nKubernetes 正迅速成为在分布式系统中部署工作负载的事实标准。在这篇文章中，我将通过揭示其底层的设计原则，帮助您更深入地了解 Kubernetes。\n声明式而不是命令式 # 一旦你学会了在 Kubernetes 编排引擎中部署第一个工作负载（Pod），你就会体会到 Kubernetes 的第一个原则 : Kubernetes API 是声明式的而不是命令式的。\n在命令式 API 中，你可以直接发出让服务器执行的命令，例如：“运行容器”，“停止容器” 等。而在声明式 API 中，你可以声明期望的状态，系统将不断地调整实际状态，直到与期望状态保持一致。你可以把这两者类比成手动驾驶与自动驾驶。\n因此，在 Kubernetes 中，你可以创建一个 API 对象（使用命令行或者 REST API）来表示你希望系统执行的操作。然后系统中所有的组件都会向该状态发展，最终于该状态保持一致，除非你删除了该对象。\n例如，如果想要调度容器化工作负载而不是发出 “运行容器” 的命令，可以创建一个描述所需状态的 API 对象：Pod\n# simple-pod.yaml apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: internal.mycorp.com:5000/mycontainer:1.7.9 $ kubectl create -f simple-pod.yaml pod \u0026#34;nginx\u0026#34; created 该对象创建之后被保存在 API Server 中：\n$ kubectl get pods NAME READY STATUS RESTARTS AGE nginx 1/1 Running 0 17s 如果容器由于某种原因崩溃，系统将会重新启动容器。如果想删除容器，请直接删除 Pod 对象：\n$ kubectl delete -f https://k8s.io/examples/pods/simple-pod.yaml pod \u0026#34;nginx\u0026#34; deleted 为什么选择声明式而不是命令式 # 因为声明式 API 可以使系统更加健壮。在分布式系统中，任何组件都可能随时发生故障，我们需要关心的是：当发生故障的组件恢复正常后，它们需要弄清楚接下来要做什么。\n当使用命令式 API 时，崩溃的组件可能在它挂掉时丢失了一个调用，如果想正常工作，就需要一些外部组件来保证它恢复时能够及时处理之前丢失的调用。如果用了声明式 API，这些组件只需要查看 API Server 的当前状态，即可确定接下来需要执行的操作（“啊，我只需要确保此容器正在运行就行了”）。\n声明式 API 也被描述为 水平触发。在 边缘触发 系统中，如果系统错过了某个事件（“边缘”），则必须重新查看该事件才能恢复系统。而在 水平触发 系统中，即使系统错过了某个事件（可能因为故障挂掉了），当它恢复时，依然可以通过查看信号的当前状态来做出正确的响应。\n因此，声明式 API 使 Kubernetes 系统更加健壮，可以更从容地应对组件故障。\n内部不存在隐藏的 API # 如果你了解 Kubernetes 各个组件的工作原理，就能体会到 Kubernetes 的第二个原则 : 控制平面是透明的，因为它的内部不存在隐藏的 API。\n这意味着 Kubernetes 各个组件之间相互交互使用的 API 和客户端与 Kubernetes 交互 使用的 API 相同。结合第一个原则（Kubernetes API 是声明式的）你可以发现，Kubernetes 的各个组件之间只能通过监视和修改 Kubernetes API 来相互交互（而不是直接用“下一步该做什么”这样的指令来相互调用）。\n让我们通过一个简单的示例来说明这一点。为了启动容器化工作负载，你可以在 Kubernetes API Server 上创建一个 Pod 对象，如前文所述。\nKubernetes 调度器根据可用资源来确定要运行的 Pod 的最佳节点，调度器通过监视 Kubernetes API Server 以获取新的 Pod 来完成调度工作。当新创建的 Pod 还没有被调度时，调度器就会运行其算法来查找运行该 Pod 的最佳节点。Pod 被成功调度之后（已经为该 Pod 选择了最佳节点），调度器并不需要通知所选的节点启动 Pod（记住：Kubernetes API 是声明式的，内部各个组件都使用相同的 API），只需要更新 Pod 对象中的 NodeName 字段来声明该 Pod 已被成功调度。\nKubelet（在节点上运行的 Kubernetes agent）也会监视 Kubernetes API（和其他组件一样），当它看到某个 Pod 的 NodeName 字段是该节点时，就知道该 Pod 被调度到了这个节点，必须要启动它。一旦了 kubelet 启动了 Pod，它就会继续监视 Pod 内部的容器状态，只要 API Server 中存在相应的 Pod 对象，它们就会一直保持运行状态。\nPod 对象被删除后，kubelet 就会明白不再需要该容器，并删除该容器。\n为什么内部不存在隐藏的 API # Kubernetes 各个组件之间相互交互使用的 API 和客户端与 Kubernetes 交互 使用的 API 相同，使得 Kubernetes 的可扩展性更强。\n如果由于某种原因，Kubernetes 的默认组件（例如，调度器）不满足你的需求，你可以将其替换为自己的使用相同 API 的组件。\n此外，如果你需要一些额外的功能，可以使用公共 API 轻松编写额外的组件来扩展 Kubernetes 的功能。\n随时随地满足用户需求 # Kubernetes API 允许存储一些工作负载可能感兴趣的信息，例如 Secret 和 ConfigMap。Secret 可以是你不想保存在容器镜像中的任何敏感数据，包括密码，证书和其他敏感信息。ComfigMap 可以包含独立于容器镜像的配置信息，例如容器启动参数和其他类似参数。\n通过上文描述的 Kubernetes 的第二个原则，我们可以修改在 Kubernetes 上运行的应用程序以直接从 Kubernetes API Server 获取 Secret 和 ConfigMap 信息。这意味着你需要修改应用程序使它意识到自己运行在 Kubernetes 中。\n这就是 Kubernetes 的第三个原则 : **随时随地满足用户需求。**指的是 Kubernetes 不应该要求重新编写应用程序才能在 Kubenretes 中运行。\n例如，许多应用程序都接受 Secret 和 ConfigMap 作为文件或环境变量。因此，Kubernetes 支持将 Secret 和 ConfigMap 作为文件或环境变量注入 Pod 之中。更多内容请参考 Secret 文档 中的 “使用 Secret” 部分。\n为什么需要随时随地满足用户需求 # 这种设计可以最大限度地减少在 Kubernetes 上部署工作负载的障碍，可以轻松地在 Kubernetes 上运行现有的工作负载，而无需对其进行重写或者更改。\n工作负载的可移植性 # 一旦可以在 Kubernetes 上运行无状态的工作负载，下一步自然就是尝试在 Kubernetes 上运行有状态的工作负载。Kubernetes提供了一个功能强大的 volume 插件系统，可以将许多不同类型的持久存储系统与 Kubernetes 工作负载一起使用。\n例如，用户可以轻松地向 API Server 请求将 Google Cloud Persistent Disk 挂载到 Pod 的特定路径中：\napiVersion: v1 kind: Pod metadata: name: sleepypod spec: volumes: - name: data gcePersistentDisk: pdName: panda-disk containers: - name: sleepycontainer image: gcr.io/google_containers/busybox command: - sleep - \u0026#34;6000\u0026#34; volumeMounts: - name: data mountPath: /data 当这个 Pod 被创建时，Kubernetes 将会自动将指定的 GCE PD 附加到 Pod 被调度到的节点，并将其挂载到指定的容器中。然后容器可以脱离容器或 Pod 的生命周期来将持久数据写入 GCE PD 挂载的路径。\n但该方法还是有点小问题的，YAML 文件中直接引用了 Google Cloud Persistent Disk，如果此 Pod 没有部署在 Google Cloud Kubernetes 集群上，则无法启动，因为无法使用 GCE PD。\n这就是 Kubernetes 下一个原则的用武之地 : 工作负载的定义应该可以跨群集移植。用户应该能够使用相同的工作负载定义文件（例如相同的 Pod yaml）来跨不同的群集部署工作负载。\n理想情况下，上面定义的 Pod 应该运行在没有 GCE PD 的集群上。为了使 Pod 能够成功运行，Kubernetes 引入了 PersistentVolumeClaim（PVC）和 PersistentVolume（PV）API 对象，这些对象将存储提供与存储使用分离开来。\nPersistentVolumeClaim 对象可以让用户请求存储资源而无需关心存储的实现方式。例如，用户可以创建 PVC 对象来请求 10 GB 的可读写存储资源，而不是请求特定的 GCE PD：\napiVersion: v1 kind: PersistentVolumeClaim metadata: name: mypvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi Kubernetes 系统会将创建此 Pod 的请求与包含该 PersistentVolume 对象的存储池中的卷相匹配，或者自动配置新卷以满足创建请求，这两种方式都可以跨 Kubernetes 集群移植工作负载的定义文件。\n为什么需要工作负载的可移植性 # 工作负载可移植性原则突出了 Kubernetes 的核心优势：就像操作系统使应用程序开发人员不必担心底层硬件的细节一样，Kubernetes 将分布式系统应用程序开发人员从底层集群的细节中解放出来。使用 Kubernetes 之后，分布式系统应用程序开发人员不必拘泥于特定的集群环境。针对 Kubernetes 部署的应用程序可以轻松地部署到本地和云环境的各种群集中，而无需针对特定的环境对应用程序或部署脚本进行更改（Kubernetes endpoint 除外）。\n总结 # 通过践行这些原则，Kubernetes 变得更强大，可扩展性和可移植性更强，且易于迁移。这就是 Kubernetes 正迅速成为在分布式系统中部署工作负载的事实标准的原因。\n相关资料 # 谈 Kubernetes 的架构设计与实现原理 开源峰会将开源生态系统连接在一起。它涵盖了基础的开源技术；通过多元化赋权峰会帮助生态系统领导者实现开源转型，并跟踪业务和合规性；同时也会深入研究涉及开源的最新技术和最新趋势，包括网络、云原生、边缘计算和 AI 等。这是开发人员，系统管理员，DevOps 专家和推动未来技术发展的 IT 架构师之间相互切磋交流的绝佳机会。 作者简介 :\nSaad Ali 是 Google 的高级软件工程师，负责 Kubernetes 项目。 他于 2014 年 12 月加入该项目，并领导了 Kubernetes 存储和 volume 子系统的开发。 他是 Kubernetes Storage SIG 的领导者，也是 Container Storage Interface 的共同作者和维护者。 在加入 Google 之前，他曾在 Microsoft 工作，领导开发 Outlook.com 的 IMAP 协议。\n","date":"2018年12月4日","externalUrl":null,"permalink":"/posts/kubernetes-design-and-development-explained/","section":"博客","summary":"原文链接： Kubernetes Design and Development Explained 本文是 8 月 29 日至 31 日在温哥华举行的 开源峰","title":"Kubernetes 设计与开发原则","type":"posts"},{"content":" 上一节我演示了如何通过 Egress Gateway 引导 Istio 的出口 HTTP 流量，但到 443 端口的 HTTPS 流量没有通过 Egress Gateway，而是直接转到 edition.cnn.com 。 Istio 出口流量的 TLS 演示了如何在网格内部直接通过 HTTP 协议访问外部加密服务。本文尝试将这两者结合起来，先将 HTTP 流量路由到 Egress Gateway，然后直接使用 Egress Gateway 发起 TLS 连接。\n前提条件与 上一篇文章相同。\nServiceEntry # 首先需要为 edition.cnn.com 定义一个 ServiceEntry 以允许网格内服务访问外部服务。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: cnn spec: hosts: - edition.cnn.com ports: - number: 80 name: http-port protocol: HTTP - number: 443 name: http-port-for-tls-origination protocol: HTTP resolution: DNS EOF 该 ServiceEntry 会在服务网格内的所有应用的所有 Pod上创建相应的路由规则和与之对应的 Cluster。具体可以参考： 控制 Egress 流量。\n验证 ServiceEntry 是否生效。发送 HTTPS 请求到 http://edition.cnn.com/politics。\n$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 301 Moved Permanently ... location: https://edition.cnn.com/politics ... command terminated with exit code 35 如果看到输出结果中包含 301 Moved Permanently，说明 ServiceEntry 配置正确。退出码 35 是由于 Istio 没有执行 TLS。 为了让 Egress gateway 执行 TLS，还要继续执行以下步骤进行配置。\nGateway # 为 edition.cnn.com 的 443 端口创建一个 Egress Gateway（假设没有启用双向 TLS 认证）。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway spec: selector: istio: egressgateway servers: - port: number: 443 name: http-port-for-tls-origination protocol: HTTP hosts: - edition.cnn.com EOF 此处 Istio 会将 Gateway 翻译成 Egress Gateway 所在的 Pod 的 Listener。具体配置如下：\n$ istioctl -n istio-system pc listener istio-egressgateway-f8b6469db-fj6zr -o json [ { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_443\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;portValue\u0026#34;: 443 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.http_connection_manager\u0026#34;, \u0026#34;config\u0026#34;: { ... \u0026#34;rds\u0026#34;: { \u0026#34;config_source\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;route_config_name\u0026#34;: \u0026#34;http.443\u0026#34; }, ... 可以看到经过该 Listener 的流量被转交给 RDS http.443，由于此时我们还没有创建 VirtualService，所以 RDS http.443 中不会包含任何有意义的路由，它会直接返回 404 状态码。\n$ istioctl -n istio-system pc route istio-egressgateway-f8b6469db-fj6zr -o json [ { \u0026#34;name\u0026#34;: \u0026#34;http.443\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;blackhole:443\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;*\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;directResponse\u0026#34;: { \u0026#34;status\u0026#34;: 404 }, \u0026#34;perFilterConfig\u0026#34;: { \u0026#34;mixer\u0026#34;: {} } } ] } ], \u0026#34;validateClusters\u0026#34;: false } ] VirtualService 和 DestinationRule # 创建一个 DestinationRule 和 VirtualService 来引导流量通过 Egress Gateway 与外部服务通信。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: egressgateway-for-cnn spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: cnn --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: direct-cnn-through-egress-gateway spec: hosts: - edition.cnn.com gateways: - istio-egressgateway - mesh http: - match: - gateways: - mesh port: 80 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local subset: cnn port: number: 443 weight: 100 - match: - gateways: - istio-egressgateway port: 443 route: - destination: host: edition.cnn.com port: number: 443 weight: 100 EOF 这里 VirtualService 会分别为网格内的应用和 Egress Gateway 各创建一条路由，以实现通过 Egress Gateway 访问目的地址 edition.cnn.com:443。具体的 Envoy 配置解析与 上一篇文章类似。\n但此时我们仍然不能访问外部服务，因为 Egress Gateway 通过 443 端口发起连接的时候，使用的仍然是 HTTP 协议。所以我们需要让 Egress Gateway 在出口流量上执行 TLS 发起，使用 HTTPS 协议来访问外部服务。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: originate-tls-for-edition-cnn-com spec: host: edition.cnn.com trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 443 tls: mode: SIMPLE # initiates HTTPS for connections to edition.cnn.com EOF 现在所有的配置都已经完成，只要是访问 edition.cnn.com:80 的流量都会被 Egress Gateway 路由到 Cluster outbound|443||edition.cnn.com，最后将流量转发到服务 https://edition.cnn.com:443。\n完整的流量转发流程如下图所示：\n网格内服务访问 http://edition.cnn.com 重新发送 HTTP 请求到 http://edition.cnn.com/politics。\n$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 200 OK ... content-length: 150793 ... 输出应该与 Istio 出口流量的 TLS 中的输出相同：没有 301 Moved Permanently 信息。\n注意，这里我们只将到 80 端口的 HTTP 流量重定向到 Egress Gateway，并通过 Egress Gateway 发起 TLS 连接；到 443 端口的 HTTP 流量仍然直接通过应用的 sidecar 代理发起 TLS 连接。 清理 # 删除之前创建的 Istio 配置项：\n$ kubectl delete gateway istio-egressgateway $ kubectl delete serviceentry cnn $ kubectl delete virtualservice direct-cnn-through-egress-gateway $ kubectl delete destinationrule originate-tls-for-edition-cnn-com $ kubectl delete destinationrule egressgateway-for-cnn 参考 # 配置 Egress gateway ","date":"2018年11月28日","externalUrl":null,"permalink":"/posts/egress-gateway-2/","section":"博客","summary":"上一节我演示了如何通过 Egress Gateway 引导 Istio 的出口 HTTP 流量，但到 443 端口的 HTTPS","title":"Istio 的高级边缘流量控制（二）","type":"posts"},{"content":"随着越来越多的企业开始大量使用 Kubernetes，持续交付越来越趋向于标准化，软件版本的更新也越来越趋向于自动化。但你有没有想过，如果新发布的版本有缺陷时该怎么办？你需要多少时间和精力来回滚到之前的版本？\n人生苦短，不能把有限的精力浪费在无限的手动回滚中，最好的办法还是让系统自己决定要不要回滚，通过设定一系列指标，并对这些指标进行监控，就可以在应用不满足该指标时触发控制器对应用进行回滚。 kuberbs（Kubernetes Rollback System）就是对该方案的一种尝试，它会监控 Kubernetes 的 Deployment 资源，如果该应用的错误率（用户定义的度量标准）高于指定的阈值，就会将该 Deployment 回滚到之前的版本。\n到目前为止，kuberbs 支持使用 Stackdriver 和 Datadog 的指标作为错误率指标，未来还计划增加更多对其他监控系统的支持。\nkuberbs 是一个 Operator 控制器，使用 CRD 来管理需要监控的 Deployment 的配置、指标和阈值。下面是一个示例：\napiVersion: \u0026#34;doit-intl.com/v1\u0026#34; kind: Rbs metadata: name: my-rbs-example spec: watchperiod: 5 metricssource: stackdriver namespaces: - name: default deployments: - deployment: name: hello-kubernetes-app #Stack driver metric metric: logging.googleapis.com/user/hello-kubernetes-app-errors threshold: 1 - deployment: name: kubernetes-app-2 # DataDog metric metric: gcp.container.cpu.usage_time{*} threshold: 85 - name: kube-system deployments: - deployment: name: kube-dns metric: logging.googleapis.com/user/dig threshold: 30 可以看到指标和阈值都是通过 CRD 资源 Rbs 来定义的，配置是通过 deployment 中的环境变量来定义的，而环境变量被存储在 kuberbs 的 ConfigMap 中。\napiVersion: v1 data: KUBERBS_CHECKMETRICSINTERVAL: \u0026#34;10\u0026#34; KUBERBS_APIKEY: \u0026#34;\u0026#34; KUBERBS_APPKEY: \u0026#34;\u0026#34; KUBERBS_DEBUG: \u0026#34;false\u0026#34; kind: ConfigMap metadata: labels: app: kuberbs name: kuberbs-config namespace: kube-system 你可以通过下面的小视频看到 KubeRBS 的运行状况：\n本视频一切权利归 bilibili 及原作者所有。如果觉得好，请点击跳转到 bilibili 给予支持。 ","date":"2018年11月28日","externalUrl":null,"permalink":"/posts/kuberbs-for-automatic-kubernetes-rollbacks-so-you-can-sleep-better-at-night/","section":"博客","summary":"随着越来越多的企业开始大量使用 Kubernetes，持续交付","title":"KubeRBS 助力 Kubernetes 自动回滚，让你晚上睡得更香","type":"posts"},{"content":"在上一篇文章 Istio 出口流量的 TLS 中，我演示了如何在网格内部直接通过 HTTP 协议访问外部加密服务，并揭示了其背后 Envoy 的配置逻辑。\n本文将会通过 Egress Gateway 来引导 Istio 的出口流量，与 Istio 出口流量的 TLS 任务中描述的功能的相同，唯一的区别就是，这里会使用 Egress Gateway 来完成这一任务。\nIstio 0.8 引入了 i ngress 和 Egress gateway 的概念。 Ingress Gateway 允许定义进入服务网格的流量入口，所有入站流量都通过该入口；Egress Gateway 与之相对，它定义了网格的流量出口。 Egress Gateway 允许将 Istio 的流量治理功能（例如，监控和路由规则）应用于 Egress 流量。\n用例 # 设想一个具有严格安全要求的组织。根据这些要求，服务网格的所有出口流量必须流经一组专用节点。这些节点与运行其他应用的节点分开，通过策略来控制出口流量。相比其他节点而言，对这些专用节点的监控也更加详细。\n另一个用例是设想一个集群，它的应用程序所在的节点没有外网 IP，因此在其上运行的网格内服务无法访问外网服务。通过定义 Egress Gateway，并将公共 IP 分配给 Egress Gateway 节点，然后通过它引导所有出口流量，就可以控制网格内服务访问外网服务了。\n前提条件 # 按照 安装指南中的说明设置 Istio 。 启动 sleep 示例，它将作为外部调用的测试源。 如果您已启用 自动注入 sidecar, 请按如下命令部署 sleep 应用程序:\n$ kubectl apply -f samples/sleep/sleep.yaml 否则，您必须在部署 sleep 应用程序之前手动注入 sidecar：\n$ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/sleep/sleep.yaml) 请注意，任何可以 exec 和 curl 的 pod 都可以执行以下步骤。\n创建一个 shell 变量来保存源 pod 的名称，以便将请求发送到外部服务, 如果您使用 sleep 示例，请按如下命令运行: $ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name}) 定义 Egress Gateway 来引导 Istio 的出口 HTTP 流量 # 首先创建一个 ServiceEntry 以允许网格内服务访问外部服务。\n1. 为 edition.cnn.com 定义一个 ServiceEntry：\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: cnn spec: hosts: - edition.cnn.com ports: - number: 80 name: http-port protocol: HTTP - number: 443 name: https protocol: HTTPS resolution: DNS EOF 2. 验证 ServiceEntry 是否生效。发送 HTTPS 请求到 http://edition.cnn.com/politics。\n$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 301 Moved Permanently ... location: https://edition.cnn.com/politics ... HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 ... Content-Length: 151654 ... 此处的返回结果应该与 Istio 出口流量的 TLS 中没有配置 TLS 发起的情况下的返回结果相同。\n3. 为 edition.cnn.com 的 80 端口创建一个 Egress Gateway（假设没有启用 双向 TLS 认证）。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway spec: selector: istio: egressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - edition.cnn.com EOF 此处 Istio 会将 Gateway 翻译成 Egress Gateway 所在的 Pod 的 Listener。具体配置如下：\n$ istioctl -n istio-system pc listeners istio-egressgateway-f8b6469db-4csb2 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_80\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;portValue\u0026#34;: 80 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.http_connection_manager\u0026#34;, \u0026#34;config\u0026#34;: { ... \u0026#34;rds\u0026#34;: { \u0026#34;config_source\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;route_config_name\u0026#34;: \u0026#34;http.80\u0026#34; }, ... 可以看到流量经过该 Listener 之后被转交给 RDS http.80，由于此时我们还没有创建 VirtualService，所以 RDS http.80 中不会包含任何有意义的路由，它会直接返回 404 状态码。\n$ istioctl -n istio-system pc route istio-egressgateway-f8b6469db-4csb2 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;http.80\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;blackhole:80\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;*\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;directResponse\u0026#34;: { \u0026#34;status\u0026#34;: 404 }, \u0026#34;perFilterConfig\u0026#34;: { \u0026#34;mixer\u0026#34;: {} } } ] } ], \u0026#34;validateClusters\u0026#34;: false } ] 此处的 validateClusters 用来决定集群管理器是否对路由中指向的 Cluster 进行验证。如果该参数设置为 true 且路由指向了不存在的集群，则不会加载该路由；如果该参数设置为 false 且路由指向了不存在的集群，则会继续加载该路由，最后找不到路由会返回 404。如果通过静态配置文件 route_config 定义路由，则该选项默认值为 true；如果通过 RDS 接口动态加载路由，则该选项默认值为 false。\n如果你启用了双向 TLS 认证，需要加上额外的 TLS 配置，这里我不展开详述，可以参考 官方文档。\n4. 创建一个 DestinationRule 和 VirtualService 来引导流量通过 Egress Gateway 与外部服务通信。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: direct-cnn-through-egress-gateway spec: hosts: - edition.cnn.com gateways: - istio-egressgateway - mesh http: - match: - gateways: - mesh ① port: 80 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local subset: cnn port: number: 80 weight: 100 - match: - gateways: - istio-egressgateway ② port: 80 route: - destination: host: edition.cnn.com port: number: 80 weight: 100 EOF 这里其实创建了两条路由，我们一个一个来看：\n① : gateway 选择了 mesh，表示该路由创建在网格内的应用中： $ istioctl pc route sleep-5bc866558c-5nl8k --name 80 -o json|grep \u0026#34;edition.cnn.com\u0026#34; -A 11 -B 1 { \u0026#34;name\u0026#34;: \u0026#34;edition.cnn.com:80\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;edition.cnn.com\u0026#34;, \u0026#34;edition.cnn.com:80\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|80|cnn|istio-egressgateway.istio-system.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;maxGrpcTimeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, 如果不指定 gateway，gateway 默认值就是 mesh。\n该 VirtualService 的作用就是将目的地址是 edition.cnn.com:80 的流量重定向到 Egress Gateway。\n这里我们将流量打向了 subset 为 cnn 的 Cluster，但现在不存在这个 Cluster，所以还需要通过 DestinationRule 定义一个 Cluster：\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: egressgateway-for-cnn spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: cnn EOF 查看创建好的 Cluster：\n$ istioctl pc cluster sleep-5bc866558c-5nl8k --fqdn istio-egressgateway.istio-system.svc.cluster.local --subset cnn --port 80 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|80|cnn|istio-egressgateway.istio-system.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|80|cnn|istio-egressgateway.istio-system.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] }, \u0026#34;http2ProtocolOptions\u0026#34;: { \u0026#34;maxConcurrentStreams\u0026#34;: 1073741824 } } ] ② : gateway 选择了 istio-egressgateway，表示该路由创建在 Egress Gateway 中： $ istioctl -n istio-system pc route istio-egressgateway-f8b6469db-fj6zr -o json [ { \u0026#34;name\u0026#34;: \u0026#34;http.80\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;edition.cnn.com:80\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;edition.cnn.com\u0026#34;, \u0026#34;edition.cnn.com:80\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|80||edition.cnn.com\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;maxGrpcTimeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, ... **该 VirtualService 的作用是通过 Egress Gateway 访问目的地址 edition.cnn.com:80。**这里 Egress Gateway 将流量路由到 Cluster outbound|80||edition.cnn.com，最后将流量转发到服务 edition.cnn.com:80。完整的流量转发流程如下图所示：\n通过 Egress Gateway 引导 Istio 的出口 HTTP 流量 5. 重新发送 HTTP 请求到 http://edition.cnn.com/politics。\n$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 301 Moved Permanently ... location: https://edition.cnn.com/politics ... HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 ... Content-Length: 151654 ... 输出应与步骤 2 中的输出相同。\n6. 查看 istio-egressgateway pod 中与我们的请求相对应的日志。\n$ kubectl logs $(kubectl get pod -l istio=egressgateway -n istio-system -o jsonpath=\u0026#39;{.items[0].metadata.name}\u0026#39;) istio-proxy -n istio-system | tail 你会在输出结果中看到与请求相关的日志：\n[2018-06-14T11:46:23.596Z] \u0026#34;GET /politics HTTP/1.1\u0026#34; 301 - 0 0 3 1 \u0026#34;172.30.146.87\u0026#34; \u0026#34;curl/7.35.0\u0026#34; \u0026#34;ab7be694-e367-94c5-83d1-086eca996dae\u0026#34; \u0026#34;edition.cnn.com\u0026#34; \u0026#34;151.101.193.67:80\u0026#34; 这里我们只将到 80 端口的 HTTP 流量重定向到 Egress Gateway，到 443 端口的 HTTPS 流量直接转到 edition.cnn.com 。\n清理 # 删除 Gateway、VirtualService、DestinationRule 和 ServiceEntry。\n$ kubectl delete gateway istio-egressgateway $ kubectl delete serviceentry cnn $ kubectl delete virtualservice direct-cnn-through-egress-gateway $ kubectl delete destinationrule egressgateway-for-cnn 参考 # 配置 Egress gateway ","date":"2018年11月26日","externalUrl":null,"permalink":"/posts/egress-gateway-1/","section":"博客","summary":"在上一篇文章 Istio 出口流量的 TLS 中，我演示了如何在网格内部直接通过","title":"Istio 的高级边缘流量控制（一）","type":"posts"},{"content":" 本篇博客于 2018 年 7 月 23 日更新。新版本使用了 Istio 1.0，并使用了新的 v1alpha3 流量管理 API。如果您使用的 Istio 是旧版本，请参考 这篇文档。\n在上一篇文章 在服务网格内部调用外部 Web 服务中，我描述了如何让 Istio 服务网格中的微服务通过 HTTPS 协议和外部的 Web 服务进行通信。本文我将着重介绍如何让 Istio 服务网格中的微服务通过 TCP 协议和外部服务进行通信。讲解的过程中会用到 Bookinfo 示例应用程序中将书籍评级数据保存在 MySQL 数据库中的那个版本。数据库部署在集群外，ratings 服务调用该数据库，还要定义一个 ServiceEntry 以允许网格内的应用程序访问外部的数据库。\nBookinfo 示例应用程序与外部评级数据库 # 首先，在 Kubernetes 集群之外设置了一个 MySQL 数据库实例来保存 Bookinfo 评级数据，然后修改 Bookinfo 示例应用程序以使用这个数据库。\n为评级数据设置数据库 # 首先你需要创建一个 MySQL 数据库实例，你可以使用任何 MySQL 实例，我自己用的是 Compose for MySQL，我使用 mysqlsh（ MySQL Shell）作为 MySQL 客户端来提供评级数据。\n1. 设置 MYSQL_DB_HOST 和 MYSQL_DB_PORT 环境变量。\n$ export MYSQL_DB_HOST=\u0026lt;your MySQL database host\u0026gt; $ export MYSQL_DB_PORT=\u0026lt;your MySQL database port\u0026gt; 如果你使用的是本地数据库，host 和 port 使用的是默认值，分别是 localhost 和 3306。\n2. 运行以下命令初始化数据库，请在出现提示时输入密码。这个命令通过 admin 数据库用户凭证来执行，该用户是通过 Compose for Mysql 创建数据库时默认创建的。\n$ curl -s https://raw.githubusercontent.com/istio/istio/master/samples/bookinfo/src/mysql/mysqldb-init.sql | mysqlsh --sql --ssl-mode=REQUIRED -u admin -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT 或者\n如果你使用的是本地数据库实例，且客户端使用的是 mysql，可以运行以下命令来初始化：\n$ curl -s https://raw.githubusercontent.com/istio/istio/master/samples/bookinfo/src/mysql/mysqldb-init.sql | mysql -u root -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT 3. 创建一个名为 bookinfo 的用户，并在 test.ratings 表上授予它 SELECT 权限：\n$ mysqlsh --sql --ssl-mode=REQUIRED -u admin -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;CREATE USER \u0026#39;bookinfo\u0026#39; IDENTIFIED BY \u0026#39;\u0026lt;password you choose\u0026gt;\u0026#39;; GRANT SELECT ON test.ratings to \u0026#39;bookinfo\u0026#39;;\u0026#34; 或者\n如果你使用的是本地数据库实例，且客户端使用的是 mysql，可以运行以下命令：\n$ mysql -u root -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;CREATE USER \u0026#39;bookinfo\u0026#39; IDENTIFIED BY \u0026#39;\u0026lt;password you choose\u0026gt;\u0026#39;; GRANT SELECT ON test.ratings to \u0026#39;bookinfo\u0026#39;;\u0026#34; 这里一般遵循 最小权限原则，这意味着在 Bookinfo 应用程序中不会直接使用 admin 用户。相反，应该为 Bookinfo 应用程序创建一个最小权限的特殊用户 bookinfo，该用户只对单个表具有 SELECT 特权。\n运行创建用户的命令后，你可能希望通过检查最后一个命令的编号和运行命令 history -d \u0026lt;创建用户的命令编号\u0026gt; 来清理 bash 历史记录，我相信你不会想把新用户的密码存储在 bash 历史记录中的。如果你使用的命令行工具是 mysql，记得要删除 ~/.mysql_history 文件中的最后一条命令。可以在 MySQL 官方文档中阅读有关新创建用户的密码保护的更多信息。\n4. 查看创建的评级数据是否跟预期的一致：\n$ mysqlsh --sql --ssl-mode=REQUIRED -u bookinfo -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;select * from test.ratings;\u0026#34; Enter password: +----------+--------+ | ReviewID | Rating | +----------+--------+ | 1 | 5 | | 2 | 4 | +----------+--------+ 或者\n如果你使用的是本地数据库实例，且客户端使用的是 mysql，可以运行以下命令：\n$ mysql -u bookinfo -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;select * from test.ratings;\u0026#34; Enter password: +----------+--------+ | ReviewID | Rating | +----------+--------+ | 1 | 5 | | 2 | 4 | +----------+--------+ 5. 暂时将评级设置为 1，以便在 Bookinfo ratings 服务调用数据库时提供直观的线索。\n$ mysqlsh --sql --ssl-mode=REQUIRED -u admin -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;update test.ratings set rating=1; select * from test.ratings;\u0026#34; Enter password: Rows matched: 2 Changed: 2 Warnings: 0 +----------+--------+ | ReviewID | Rating | +----------+--------+ | 1 | 1 | | 2 | 1 | +----------+--------+ 或者\n如果你使用的是本地数据库实例，且客户端使用的是 mysql，可以运行以下命令：\n$ mysql -u root -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;update test.ratings set rating=1; select * from test.ratings;\u0026#34; Enter password: +----------+--------+ | ReviewID | Rating | +----------+--------+ | 1 | 1 | | 2 | 1 | +----------+--------+ 最后一个命令使用了 admin 用户（本地数据库实例使用的是 root 用户），因为 bookinfo 用户对 test.ratings 这个表没有 UPDATE 权限。\n现在就可以部署一个使用外部数据库的 Bookinfo 应用程序了。\nBookinfo 应用程序的初始设置 # 为了演示使用外部数据库的场景，首先需要一个安装了 Istio 的 Kubernetes 集群，然后部署 Istio Bookinfo 示例应用程序，并且创建了默认的 DestinationRule。\n该应用程序使用 ratings 微服务来获取书籍评级，评级在 1 到 5 之间，评级显示为每个 review 的星号。有好几个版本的 ratings 微服务，有些版本使用 MongoDB 作为数据库，还有些版本使用 MySQL 作为数据库。\n本文的示例命令适用于 Istio 1.0+，无论你有没有启用 双向 TLS 认证。\n以下是原始版本的 Bookinfo 示例应用程序中应用程序端到端架构的副本。\n原 Bookinfo 应用程序 使用外部数据库存储 Bookinfo 应用程序的评级数据 # 1. 修改使用 MySQL 数据库的 ratings 服务版本的 deployment 配置文件中的环境变量，将其修改成你自己的数据库实例信息。该 yaml 文件位于 Istio 发行存档的 samples/bookinfo/platform/kube/bookinfo-ratings-v2-mysql.yaml中。修改以下几行：\n- name: MYSQL_DB_HOST value: mysqldb - name: MYSQL_DB_PORT value: \u0026#34;3306\u0026#34; - name: MYSQL_DB_USER value: root - name: MYSQL_DB_PASSWORD value: password 将数据库的 IP、端口、用户名和密码替换成实际的值。请注意，在 Kubernetes 中使用容器环境变量中密码的正确方法是 使用 secret，本文只是为了便于演示在 deployment spec 中直接配置明文密码。**切记！不要在真实环境中这样做！**我想你们应该也知道，\u0026quot;password\u0026quot; 这个值也不应该用作密码。\n2. 使用修改后的 deployment yaml 文件来创建使用外部数据库的 ratings 服务：v2-mysql。\n$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo-ratings-v2-mysql.yaml deployment \u0026#34;ratings-v2-mysql\u0026#34; created 3. 将发往 reviews 服务的所有流量都路由到 v3 版本，这样做是为了确保 reviews 服务始终调用 ratings 服务。此外，将发往 ratings 服务的所有流量都路由到使用外部数据库的 ratings v2-mysql。\n通过添加两个 VirtualService，可以为上述两种服务指定路由。这些 VirtualService 在 Istio 发行档案的 samples/bookinfo/networking/virtual-service-ratings-mysql.yaml 中指定。注意 : 确保你在添加了默认的 DestinationRule 之后再执行下面的命令。\n$ kubectl apply -f samples/bookinfo/networking/virtual-service-ratings-mysql.yaml 更新后的架构如下所示。请注意，网格内的蓝色箭头表示创建 VirtualService 之后的流量转发路径。根据创建的 VirtualService，流量将被转发到 reviews v3 和 ratings v2-mysql。\n使用外部 MySQL 数据库的 ratings v2-mysql 版本的 Bookinfo 应用程序\n请注意，MySQL 数据库位于 Istio 服务网格之外，或者更准确地说是在 Kubernetes 集群之外，服务网格的边界由虚线标记。\n访问 Web 页面 # 在 确定 ingress IP 和端口之后， 就可以访问应用程序的 Web 页面了。\n哎呀糟糕，出现问题了 😥 无论你怎么刷新浏览器，每个 review 下方都不会显示评级星标，而是显示 “Ratings service is currently unavailable”。\nRatings 服务的错误信息\n与 在服务网格内部调用外部 Web 服务这篇文章中遇到的情况一样，你会体验到优雅的服务降级，非常好。虽然 ratings 服务中有错误，但是应用程序并没有因此而崩溃，Web 页面虽然不能显示评级星标，但可以正确显示书籍信息、details 信息和 reviews 信息。\n默认情况下， Istio sidecar 代理（Envoy proxies） 会阻止到集群外服务的所有流量（TCP 和 HTTP），要为 TCP 启用此类流量，我们必须先定义 TCP 协议的 mesh-external ServiceEntry。\n外部 MySQL 实例的 Mesh-external ServiceEntry # 下面就该 mesh-external ServiceEntry 上场了。\n1. 获取 MySQL 数据库的 IP 地址。你可以通过 hosts 命令来获取：\n$ export MYSQL_DB_IP=$(host $MYSQL_DB_HOST | grep \u0026#34; has address \u0026#34; | cut -d\u0026#34; \u0026#34; -f4) 如果你使用的是本地数据库实例，设置 MYSQL_DB_IP 环境变量为你的本机 IP，并且要保证这个环境变量能被集群访问到。\n2. 定义一个 TCP mesh-external ServiceEntry：\nkubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: mysql-external spec: hosts: - $MYSQL_DB_HOST addresses: - $MYSQL_DB_IP/32 ports: - name: tcp number: $MYSQL_DB_PORT protocol: tcp location: MESH_EXTERNAL EOF 3. 查看创建好的 ServiceEntry：\n$ kubectl get serviceentry mysql-external -o yaml apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: ... 对于 TCP ServiceEntry，你需要指定 port 列表的 protocol 字段值为 tcp，还要在 addresses 列表里面指定外部服务的 IP 地址，该 IP 地址以网络号为 32 位的无类型域间选路（CIDR）形式表示。 下面我将详细讨论 TCP ServiceEntry。现在先来验证添加 ServiceEntry 之后是否解决了上面遇到的问题，再次访问 Web 页面，看看评级星标是不是回来了。\n果然有效！现在 Web 页面的报错已经消失了，正确显示了评级：\nBook Ratings 显示正常\n和预期的一样，你会看到两个 review 下面显示的都是一星评级。因为之前我们在数据库中将评级改为了一颗星，所以现在可以肯定 ratings 服务调用到了外部数据库。\n与 HTTP/HTTPS 协议的 ServiceEntry 一样，你也可以使用 kubectl 动态删除和创建 TCP ServiceEntry。\n控制出口 TCP 流量的动机 # 有时候，Istio 网格内的应用程序需要访问外部服务，如遗留系统。并且很多情况下，网格内的微服务都不会通过 HTTP 或 HTTPS 协议来访问外部服务，而是通过 TCP 协议或 TCP 协议的变种（如 MongoDB wire 协议 和 MySQL客户端/服务器协议）来和外部数据库通信。\n接下来我会重点介绍 TCP 流量的 ServiceEntry。\nTCP 流量的 ServiceEntry # 用于启用到特定端口的 TCP 流量的 ServiceEntry 必须将端口的协议指定为 TCP。此外，对于 MongoDB Wire 协议，可以将协议指定为 MONGO，而不是 TCP。\n对于 ServiceEntry 中的 addresses 列表，必须以网络号为 32 位的无类型域间选路（ CIDR）形式表示。注意：在 TCP ServiceEntry 中，hosts 字段会被忽略掉。\n想要通过其主机名（hostname）启用到外部服务的 TCP 流量，必须指定主机名的所有 IP，每个 IP 必须以 CIDR 的形式表示。\n有时候我们无法获取外部服务的所有 IP，这时候要想往集群外发送 TCP 流量，只能在 addresses 列表中指定那些已知的被应用程序使用的 IP。\n有些情况下，外部服务的 IP 并不总是静态 IP，例如在 CDN 的场景中。大多数情况下 IP 地址都是静态的，但有时 IP 地址会被更改，例如由于基础设施的变化。这时候如果你知道 IP 地址变化的范围，就可以通过 CIDR 的形式指定范围。如果你实在无法确定 IP 地址变化的范围，就不能使用 TCP ServiceEntry，必须绕过 sidecar 代理 直接调用外部服务。\n与网格扩展的关系 # 请注意，本文中描述的场景与 集成虚拟机示例中描述的网格扩展场景不同。 在集成虚拟机的场景中，MySQL 实例在与 Istio 服务网格集成的外部（集群外）机器（裸机或VM）上运行 ，MySQL 服务成为网格的一等公民，具有 Istio 的所有高级功能。除此之外，也不需要创建 ServiceEntry 来访问 MySQL 服务，可以直接通过本地集群域名（例如 mysqldb.vm.svc.cluster.local）来寻址，并且可以通过 双向 TLS 身份验证来保护与其之间的通信。但是该服务必须要在 Istio 中注册，要启用此类集成，必须在计算机上安装 Istio 组件（Envoy proxy，node-agent，istio-agent），并且必须可以从中访问 Istio 控制平面（Pilot，Mixer，Citadel）。详细信息请参考 Istio Mesh Expansion。\n但在本文的示例中，MySQL 实例可以在任何机器上运行，也可以由云提供商提供，无需与 Istio 集成，也无需从 MySQL 实例所在的机器上访问 Istio 控制平面。在 MySQL 作为服务的情况下，客户端可能无法访问 MySQL 所运行的机器，并且无法在该机器上安装所需组件。本文示例中的 MySQL 实例可以通过其全局域名进行寻址，这对希望使用域名来寻址的消费者客户端来说是有益的。当在消费者应用程序的部署配置中无法更改预期的域名时，这项功能显得尤为重要。\n清理 # 1. 删除 test 数据库和 bookinfo 用户：\n$ mysqlsh --sql --ssl-mode=REQUIRED -u admin -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;drop database test; drop user bookinfo;\u0026#34; 或者\n如果你使用的是本地数据库实例，且客户端使用的是 mysql，可以运行以下命令：\n$ mysql -u root -p --host $MYSQL_DB_HOST --port $MYSQL_DB_PORT -e \u0026#34;drop database test; drop user bookinfo;\u0026#34; 2. 删除 VirtualService\n$ kubectl delete -f samples/bookinfo/networking/virtual-service-ratings-mysql.yaml Deleted config: virtual-service/default/reviews Deleted config: virtual-service/default/ratings 3. 删除 ratings v2-mysql：\n$ kubectl delete -f samples/bookinfo/platform/kube/bookinfo-ratings-v2-mysql.yaml deployment \u0026#34;ratings-v2-mysql\u0026#34; deleted 4. 删除 ServiceEntry\n$ kubectl delete serviceentry mysql-external -n default Deleted config: serviceentry mysql-external 总结 # 本文演示了 Istio 服务网格中的微服务如何通过 TCP 协议调用外部服务。默认情况下， Istio sidecar 代理（Envoy proxies） 会阻止到集群外服务的所有流量（TCP 和 HTTP），要为 TCP 启用此类流量，我们必须先定义 TCP 协议的 mesh-external ServiceEntry。\n","date":"2018年11月23日","externalUrl":null,"permalink":"/posts/egress-tcp/","section":"博客","summary":"本篇博客于 2018 年 7 月 23 日更新。新版本使用了 Istio 1.0，并使用了新","title":"在服务网格内部调用外部 TCP 服务","type":"posts"},{"content":" 此博客文章于 2018 年 8 月 9 日更新。新版本使用了 Istio 1.0，并使用了新的 v1alpha3 流量管理 API。如果您使用的 Istio 是旧版本，请参考 使用外部 Web 服务归档版。\n在许多情况下，在 service mesh 中的微服务应用并不是应用程序的全部，有时，网格内部的微服务需要使用在服务网格外部的遗留系统提供的功能。虽然我们希望逐步将这些系统迁移到服务网格中，但是在迁移这些系统之前，必须让服务网格内的应用程序能访问它们。在其他情况下，应用程序使用外部组织提供的 Web 服务，这些服务通常是通过万维网提供的服务。\n在这篇博客文章中，我修改了 Istio Bookinfo 示例应用程序让它可以从外部 Web 服务（ Google Books APIs）中获取图书详细信息。 我将展示如何使用 mesh-external service entries 在 Istio 中启用外部 HTTPS 流量。我提供了两种方式来配置出口流量的 TLS，并描述了每个选项的优缺点。\n初始设定 # 为了演示使用外部 Web 服务的场景，首先需要一个安装了 Istio 的 Kubernetes 集群，然后部署 Istio Bookinfo 示例应用程序，此应用程序使用 details 微服务来获取书籍详细信息，例如页数和作者。原始的 details 微服务无需调用任何外部服务就可以提供书籍的详细信息。\n本文的示例命令适用于 Istio 1.0+，无论你有没有启用 双向 TLS 认证。Bookinfo 配置文件位于 Istio 发行存档的 samples/bookinfo 目录中。\n以下是原始版本的 Bookinfo 示例应用程序中应用程序端到端架构的副本。\n原 Bookinfo 应用程序\n首先按照 部署应用程序、 确认应用正在运行，以及 应用默认目标规则中的步骤进行操作。\nBookinfo 使用 HTTPS 访问 Google 图书 Web 服务 # 让我们添加一个新的 v2 版本的 details 微服务，用来从 Google Books APIs 中获取图书详细信息。执行下面的命令将新版本的 details 服务所在的容器的环境变量 DO_NOT_ENCRYPT 设置为 false，表示使用 HTTPS（而不是 HTTP ）来访问外部服务。\n$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo-details-v2.yaml --dry-run -o yaml | kubectl set env --local -f - \u0026#39;DO_NOT_ENCRYPT=false\u0026#39; -o yaml | kubectl apply -f - 更新后的架构如下所示：\ndetails V2 版本的 Bookinfo 应用程序\n请注意，Google Book 服务位于 Istio 服务网格之外，其边界由虚线标记。\n将指向 details 微服务的所有流量重定向到 details v2：\n$ kubectl apply -f samples/bookinfo/networking/virtual-service-details-v2.yaml 请注意，此处的 VirtualService 依赖于您在 应用默认目标规则部分中创建的目标规则。\n在 确定 ingress 的 IP 和端口之后， 就可以访问应用程序的 web 页面了。\n糟糕…页面显示的是 Error fetching product details，而不是书籍详细信息：\n获取产品详细信息的错误消息\n好消息是应用程序没有崩溃, 我们通过良好的微服务设计，没有让故障扩散。调用 details 服务失败并不会导致无法访问 productpage 服务，并且 productpage 的绝大多数功能仍然可用，它通过优雅降级来让评论和评级能够正确显示。\n那么问题到底出在哪里呢？啊……原来是我忘了启用从网格内部访问外部服务的流量，在本例中外部服务指的是 Google Book Web 服务。默认情况下，Istio sidecar 代理（ Envoy proxies） 阻止到集群外服务的所有流量，要启用此类流量，我们必须先定义 mesh-external service entry。\n启用对 Google 图书 Web 服务的 HTTPS 访问 # 别担心，创建一个 mesh-external ServiceEntry 就可以修复应用程序的访问错误啦，同时还需要定义一个 VirtualService 以使用 SNI 对外部服务进行路由。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: googleapis spec: hosts: - www.googleapis.com ports: - number: 443 name: https protocol: HTTPS location: MESH_EXTERNAL resolution: DNS --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: googleapis spec: hosts: - www.googleapis.com tls: - match: - port: 443 sni_hosts: - www.googleapis.com route: - destination: host: www.googleapis.com port: number: 443 weight: 100 EOF 现在再次访问应用程序的网页就会显示书籍的详细信息了：\n正确显示书籍详细信息\n查看创建的 ServiceEntry：\n$ kubectl get serviceentries NAME AGE googleapis 8m 删除该 ServiceEntry：\n$ kubectl delete serviceentry googleapis serviceentry \u0026#34;googleapis\u0026#34; deleted 删除 ServiceEntry 后访再问 Web 页面会产生我们之前遇到的相同错误，即 Error fetching product details。因为 ServiceEntry 和其他 Istio 的配置一样是动态定义的，Istio operators 可以在不重新部署微服务的情况下动态决定允许哪些微服务访问哪些域名，也可以动态启用和禁用外部服务的流量。\n清除对 Google 图书 Web 服务的 HTTPS 访问权限 # $ kubectl delete serviceentry googleapis $ kubectl delete virtualservice googleapis $ kubectl delete -f samples/bookinfo/networking/virtual-service-details-v2.yaml $ kubectl delete -f samples/bookinfo/platform/kube/bookinfo-details-v2.yaml 由 Istio 发起的 TLS # 假设你想监控你的你的微服务使用哪些特定的 Google API（ 书籍， 日历， 任务等）；假设你要强制执行仅允许使用 Books API 的策略；假设你要监控被微服务访问的书籍标识符。对于这些监控和策略任务，你需要知道确切的 URL 路径。例如考虑这样一个 URL：www.googleapis.com/books/v1/volumes?q=isbn:0486424618，在该 URL 中， Books API 由路径 /books 和路径 /volumes?q=isbn:0486424618 的 ISBN 编号指定。但在 HTTPS 中，所有 HTTP 详细信息（主机名，路径，头文件等）都是加密的，sidecar 代理的这种监控和策略执行是无法实现的，Istio 只能通过 SNI（Server Name Indication）得知加密请求中的主机名称，在这里就是 www.googleapis.com。\n为了让 Istio 能够根据 HTTP 详细信息对出口请求进行监控和过滤，微服务必须发出 HTTP 请求，然后 Istio 再打开到目标的 HTTPS 连接（执行 TLS 发起）。微服务的代码编写方式和配置方式需要根据该微服务运行在 Istio 服务网格内部还是外部来进行调整，虽然这与 Istio 的 最大化透明度设计目标相矛盾, 但有时我们需要妥协……\n下图显示了通过 HTTPS 协议将流量发送到外部服务的两种方式。上面这幅图中，微服务自己发送常规的端到端加密 HTTPS 请求。下图中微服务在同一个 Pod 内发送未加密的 HTTP 请求，这些请求被 sidecar Envoy 代理拦截，sidecar 代理执行 TLS 发起，因此 pod 和外部服务之间的流量被加密。\n对外发起 HTTPS 流量的两种方式：微服务自行发起，或由 Sidecar 代理发起\n以下代码展示了如何在 Bookinfo 的 details 微服务代码 中使用 Ruby net/http 模块：\nuri = URI.parse(\u0026#39;https://www.googleapis.com/books/v1/volumes?q=isbn:\u0026#39; + isbn) http = Net::HTTP.new(uri.host, uri.port) ... unless ENV[\u0026#39;DO_NOT_ENCRYPT\u0026#39;] === \u0026#39;true\u0026#39; then http.use_ssl = true end 请注意，默认的 HTTPS 端口 443 的取值是 URI.parse 通过对 URI (https://) 的解析得来的。当定义了 DO_NOT_ENCRYPT 环境变量时，请求将通过普通的 HTTP 协议发出。\n你可以在 details v2 的 deployment 配置文件的 container 配置项中将环境变量 DO_NOT_ENCRYPT 的值设置为 “true”。\nenv: - name: DO_NOT_ENCRYPT value: \u0026#34;true\u0026#34; 下一节将会配置 TLS 发起以访问外部 Web 服务。\n配置 Bookinfo 到 Google 图书 Web 服务之间的 TLS 发起 # 1. 部署 details v2 版本，将 HTTP 请求发送到 Google Books API。 在 bookinfo-details-v2.yaml 中， 将 DO_NOT_ENCRYPT 变量设置为 true。\n$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo-details-v2.yaml 2. 将指向 details 微服务的流量重定向到 details v2。\n$ kubectl apply -f samples/bookinfo/networking/virtual-service-details-v2.yaml 3. 为 www.google.apis 创建一个 mesh-external ServiceEntry，再创建一个 DestinationRule 用于执行 TLS 发起。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: googleapis spec: hosts: - www.googleapis.com ports: - number: 443 name: http-port-for-tls-origination protocol: HTTP resolution: DNS --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: originate-tls-for-googleapis spec: host: www.googleapis.com trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 443 tls: mode: SIMPLE # initiates HTTPS when accessing edition.cnn.com EOF 注意，端口 443 的名称以 http- 为前缀，使用的协议是 HTTP，你不需要在 Bookinfo 使用 443 端口发送 HTTP 请求时执行 TLS 发起。关于如何使用端口重定向来执行 TLS 发起可以参考这篇文章。 4. 访问应用程序的 Web 页面，并验证是否能显示图书的详细信息。\n5. 检查 details v2 的 sidecar 代理的日志，并查看 HTTP 请求。\n$ kubectl logs $(kubectl get pods -l app=details -l version=v2 -o jsonpath=\u0026#39;{.items[0].metadata.name}\u0026#39;) istio-proxy | grep googleapis [2018-08-09T11:32:58.171Z] \u0026#34;GET /books/v1/volumes?q=isbn:0486424618 HTTP/1.1\u0026#34; 200 - 0 1050 264 264 \u0026#34;-\u0026#34; \u0026#34;Ruby\u0026#34; \u0026#34;b993bae7-4288-9241-81a5-4cde93b2e3a6\u0026#34; \u0026#34;www.googleapis.com:443\u0026#34; \u0026#34;172.217.20.74:443\u0026#34; EOF 日志中的 URL 路径可以被监控，也可以根据该路径来应用访问策略。要了解有关HTTP 出口流量的监控和访问策略的更多信息，请查看 归档博客之出口流量监控之日志。\n清除 Bookinfo 到 Google 图书 Web 服务之间的 TLS 发起 # $ kubectl delete serviceentry googleapis $ kubectl delete destinationrule originate-tls-for-googleapis $ kubectl delete -f samples/bookinfo/networking/virtual-service-details-v2.yaml $ kubectl delete -f samples/bookinfo/platform/kube/bookinfo-details-v2.yaml 与 Istio 双向 TLS 的关系 # TLS 发起与 Istio 的 双向 TLS 无关，无论 Istio 是否开启双向 TLS，对外部服务的 TLS 发起都会起作用。双向 TLS 用来保护网格内的服务之间通信，并为每个服务提供强大的身份认证功能。而本文中的外部服务是使用单向 TLS 访问的，这种机制用于保护 Web 浏览器和 Web 服务器之间的通信。将 TLS 应用于与外部服务之间的通信，可以对流量加密，并对外部服务器进行身份认证。\n总结 # 本文我演示了如何让 Istio 服务网格中的微服务通过 HTTPS 协议和外部的 Web 服务进行通信。默认情况下，Istio 会阻止群集外主机的所有流量，想要访问外部服务，必须得为该服务创建一个 mesh-external ServiceEntry。可以通过发出 HTTPS 请求来访问外部服务，也可以只发出 HTTP 请求，然后通过 sidecar 代理执行 TLS 发起来访问外部服务。当微服务发出 HTTPS 请求时，流量是端到端加密的，Istio 无法监控到 HTTP 详细信息，例如请求的 URL 路径。当服务器发出 HTTP 请求时，Istio 就可以监控到 HTTP 详细信息了，并且可以强制执行基于 HTTP 的访问策略。 但此时微服务和 sidecar 代理之间的流量是未加密的，在有严格安全要求的环境中，这种方法还是不够的，你必须通过发出 HTTPS 请求来加密所有的流量。\n","date":"2018年11月21日","externalUrl":null,"permalink":"/posts/egress-https/","section":"博客","summary":"此博客文章于 2018 年 8 月 9 日更新。新版本使用了 Istio 1.0，并使用了","title":"在服务网格内部调用外部 Web 服务","type":"posts"},{"content":" 原文地址： Kubernetes API Server, Part I\n概念架构 Kubernetes 是一个用于在一组节点（通常称之为集群）上托管容器化应用程序的容器编排引擎。本系列教程旨在通过系统建模的方法帮助大家更好地理解 Kubernetes 及其基本概念。\n本文使用的语言是 Alloy，这是一种基于一阶逻辑表达结构和行为的 规范语言。文中我对每一段 Alloy 规范语言表达的意思都作了简明的描述。\n规约语言（英语：Specification language），或称规范语言，是在计算机科学领域的使用的一种形式语言。编程语言是用于系统实现的、可以直接运行的形式语言。与之不同，规约语言主要用于系统分析和设计的过程中。\n本系列文章总共分为三个部分：\n第一部分描述了 API Server 的架构和行为 第二部分描述了 Kubernetes API 第三部分描述了 Kubernetes 的对象存储 本文主要讲述第一部分的内容。\n前言——什么是 API Server # “API Server” 这个术语很宽泛，涉及了太多的概念，本文将尝试使用 API Server，Kubernetes API 和 Kubernetes 对象存储 这三个不同的术语来明确表示各个概念。\n图 1：API Server，Kubernetes API 和 Kubernetes 对象存储 Kubernetes API 表示处理读取和写入请求以及相应地查询或修改 Kubernetes 对象存储的组件。 Kubernetes 对象存储 表示持久化的 Kubernetes 对象集合。 API Server 表示 Kubernetes API 和 Kubernetes 对象存储的并集。 API Server 详解 # Kubernetes API Server 是 Kubernetes 的核心组件。从概念上来看，Kubernetes API Server 就是 Kubernetes 的数据库，它将集群的状态表示为一组 Kubernetes 对象，例如 Pod、ReplicaSet 和 Deployment 都属于 Kubernetes 对象。\n图 2：Kubernetes API Server \u0026amp; Kubernetes 对象 Kubernetes API Server 存在多个版本，每一个版本都是它在不同时间段的快照，类似于 git 仓库：\nKubernetes API Server 具有属性 rev，是 Kubernetes API Server 版本的缩写。该属性表示的是 Kubernetes API Server 在每个时间戳的快照。 Kubernetes 对象具有属性 mod，是 Kubernetes 对象版本的缩写。该属性表示的是该对象最后一次被修改的快照。 但实际上 Kubernetes API Server 在实现上会限制快照的时间长度，并且默认情况下会在 5 分钟后丢弃快照。\n图 3：Kubernetes API Server \u0026amp; 版本 Kubernetes API Server 暴露了一个不支持事务性语义的 CRUD （Create/Read/Update/Delete）接口：\n保证写入请求是针对最新版本执行的，并相应地增加版本号。 但不保证读取请求是针对最新版本执行的，这主要取决于 API Server 的安装与配置方式。 缺乏事务性语义会导致经典的 竞争危害现象，如非确定性写入。\n缺乏 read-last-write 语义会导致两个截然不同的后果，即过期读取和无序读取：\n过期读取（Stale reads） 指的是读取请求针对的不是最新版本的现象，因此会产生“过期”响应。 无序读取（Out-of-order reads） 指的是在两个连续的读取请求中，第一个请求读取的是较高版本，而第二个请求读取的是较低版本，因此会产生无序响应。 图 4：读取 防护 token 和新鲜度 token # 客户端可以使用属性 rev 作为用于写入操作的防护 token（fencing tokens），以此来抵消丢失的事务性语义。或者作为用于读取操作的新鲜度 token（freshness tokens），以此来抵消丢失的 read-last-write 语义。\n图 5：防护 token 在执行写入操作时，客户端使用 rev 或 mod 作为防护 token。客户端指定期望的 rev 或 mod 值，但只有当前 rev 或 mod 值等于期望值时，API Server 才会处理该请求。这一过程被称为乐观锁定（optimistic locking）。\n图 5 中客户端期望的 rev 值为 n，而当前的 rev 值为 n+1，与期望不符，因此 API Server 不处理该请求，rev 值仍然保持为 n+1。\n图 6：新鲜度 token 在执行读取操作时，客户端使用 rev 或 mod 作为新鲜度 token，该 token 用来确保读取请求返回的结果不早于新鲜度 token 的值指定的结果。\n架构规范 # sig Server {objects : set Object, rev : Int} sig Object {kind : Kind, name : Name, namespace : Namespace, mod : Int} // Equality of objects pred eq(o, o\u0026#39; : Object) { o.kind = o\u0026#39;.kind and o.name = o\u0026#39;.name and o.namespace = o\u0026#39;.namespace } // Uniqueness constraint fact { all s : Server { all disj o, o\u0026#39; : s.objects | not eq[o, o\u0026#39;] } } Kubernetes API Server 有一组 Kubernetes 对象和一个 rev 属性。 Kubernetes 对象具有 kind， name， namespace 和 mod 这几个属性。 对象由其 kind，name 和 namespace 三元组来标识。 API Server 中任何两个不同的 Kubernetes 对象都不可能具有相同的 kind，name 和 namespace 三元组。 行为规范 # 从概念上来看，Kubernetes API Server 提供了写入接口和读取接口。\n其中写入接口将所有更改状态的命令组合在一起，读取接口将所有查询状态的命令组合在一起。\n图 7：写入和读取接口 写入接口 # 写入接口提供创建、更新和删除对象的命令。\nabstract sig Command {server : one Server, server\u0026#39; : one Server} fact { all c : Command { c.server\u0026#39;.revision = c.server.revision.plus[1] } } 每一个 Command 表示一个状态转换：将 API Server 从当前状态转换到下一个状态。每个命令都会增加 API Server 的版本。\nabstract sig Event { origin : one Command, object : one Object } fact { all c : Command { one e : Event | e.origin = c } } 此外，每个命令都会生成一个事件。Event 表示命令执行的持久化可查询记录。\n图 8：API Server，命令和事件 图 8 描述了 API Server 的一系列命令和结果状态转换。总共分为三层结构，从下往上依次表示为 API Server，命令和事件。\nKubernetes API Server 的设计和实现方式保证了 API Server 在任何时间点的当前状态等于事件流到该时间点的聚合状况，这种模式也被称为 事件溯源（event sourcing）。\nstate = reduce(apply, events, {}) 创建命令 # sig Create extends Command {toCreate : one Object} fact { all c : Create { // pre-condition(s) not c.toCreate in c.server.objects // next state c.server\u0026#39;.objects = c.server.objects + c.toCreate // mod c.toCreate.mod = c.server\u0026#39;.rev } } 创建命令将 Kubernetes 对象添加到 API Server，并将对象的 mod 值设置为 API Server 的 rev 值。 如果想要创建的对象违反了 API Server 的唯一性约束，则会拒绝创建命令。 sig Created extends Event {} fact { all c : Create { one e : Created | e.origin = c and e.object = c.toCreate } } 每个创建命令都会生成一个持久且可查询的 Created Event，event 的 object 字段引用创建的 Kubernetes 对象。 更新命令 # sig Update extends Command {old : one Object, new : one Object, mod : Int} fact { all u : Update { // pre-condition(s) u.old in u.server.objects and not u.new in u.server.objects and eq[u.old, u.new] // optimistic locking u.old.mod = u.mod // next state u.server\u0026#39;.objects = u.server.objects - u.old + u.new // mod u.new.mod = u.server\u0026#39;.rev } } 更新命令将更新 API Server 中的 Kubernetes 对象，并将对象的 mod 值设置为 API Server 的 rev 值。 如果命令的 mod 值与对象的 rev 值不匹配，则拒绝更新命令。这里的 mod 用作防护 token。 sig Updated extends Event {} fact { all u : Update { one e : Updated | e.origin = u and e.object = u.new } } 每个更新命令都会生成一个持久且可查询的 Updated Event，event 的 object 字段引用新的 Kubernetes 对象。 删除命令 # sig Delete extends Command {toDelete : one Object, mod : Int} fact { all d : Delete { // pre-condition(s) d.toDelete in d.server.objects // optimistic locking d.toDelete.mod = d.mod // next state d.server\u0026#39;.objects = d.server.objects - d.toDelete } } 删除命令从 API Server 中删除 Kubernetes 对象。 如果命令的 mod 值与对象的 mod 值不匹配，则拒绝删除命令。这里的 mod 用作防护 token。 sig Deleted extends Event {} fact { all d : Delete { one e : Deleted | e.origin = d and e.object = d.toDelete } } 每个删除命令都会生成一个持久且可查询的 Deleted Event，event 的 object 字段引用已删除的 Kubernetes 对象。 读取接口 # Kubernetes API 读取接口提供两个字接口，一个接口与对象相关，另一个与事件相关。\n对象相关的子接口 # 对象相关的子接口提供读取对象和对象列表的命令。\nsig ReadO {kind : one Kind, name : one Name, namespace : one Namespace, min : Int, res : lone Object, rev : Int} fact { all r : ReadO { some s : Server { r.min \u0026lt;= server.rev r.rev = s.rev r.res = {o : s.objects | o.kind = r.kind and o.name = r.name and o.namespace = r.namespace} } } } 读取对象的请求接收 kind、name 和 namespace 三元组，同时也会接收用作新鲜度 token 的 min 参数。 API Server 至少在由 min 指定的 API Server 的版本处返回匹配的 Kubernetes 对象。 事件相关的子接口 # 事件相关的子接口提供命令以读取关于对象和对象列表的事件。\nsig WatchO {kind : Kind, name : Name, namespace : Namespace, min : Int, res : set Event} fact { all w : WatchO { w.res = {e : Event | e.origin.server.rev \u0026gt;= w.min and e.object.kind = w.kind and e.object.name = w.name and e.object.namespace = w.namespace} } } Watch 对象的请求接收 kind、name 和 namespace 三元组，同时也会接收用作新鲜度 token 的 min 参数。 API Server 从指定的 API Server 版本开始返回所有匹配的事件。 sig WatchL {kind : Kind, name : Name, min : Int, res : set Event} fact { all w : WatchL { w.res = {e : Event | e.origin.server.rev \u0026gt;= w.min and e.object.kind = w.kind and e.object.name = w.name} } } Watch List 对象的请求接收 kind、name 和 namespace 三元组，同时也会接收用作新鲜度 token 的 min 参数。 API Server 从指定的 API Server 版本开始返回所有匹配的事件。 例子 # 对象相关的子接口与事件相关的子接口一起组成了 Kubernetes 中广泛使用的有效查询机制，例如在 Kubernetes 控制器中就用到了这种机制。\n通过这种机制，客户端可以先请求一次当前状态，然后订阅后续事件流，而不是重复轮询对象或对象列表的当前状态。\npods, rev := request-object-list(kind=\u0026#34;pods\u0026#34;, namespace=\u0026#34;default\u0026#34;) for e in request-watch-list(kind=\u0026#34;pods\u0026#34;, namespace=\u0026#34;default\u0026#34;, rev) pods := apply(pods, e) 通过将读取请求最初返回的 Kubernetes API Server 版本线程化到 watch 请求，可以保证客户端能够接收到读取和写入请求之间以及之后发生的任何事件。\n这种实现机制可以确保客户端的状态与 API Server 的状态保持最终一致性。\n总结 # 本文描述了 Kubernetes API Server 的架构和行为。设计和实现一个适当的客户端的关键部分是正确使用 Kubernetes API Server 的版本和 Kubernetes 对象的版本作为防护 token 和新鲜度 token。\n下一篇文章将会为大家介绍 Kubernetes API 和 Kubernetes 对象存储。\n后记 # 本系列文章是 CNCF，Google 和 SAP 之间合作努力的结果，旨在促进大家对 Kubernetes 及其基本概念的理解。\n","date":"2018年11月19日","externalUrl":null,"permalink":"/posts/kubernetes-api-server-part-1/","section":"博客","summary":"原文地址： Kubernetes API Server, Part I 概念架构 Kubernetes 是一个用于在一组节点（通常称之","title":"深入理解 Kubernetes API Server（一）","type":"posts"},{"content":" 本文主要内容来自 Istio 官方文档，并对其进行了大量扩展和补充。\n控制出口流量任务演示了如何从网格内部的应用程序访问 Kubernetes 集群外部的 HTTP 和 HTTPS 服务, 如该主题中所述，默认情况下，启用了 Istio 的应用程序无法访问集群外的 URL, 要启用外部访问，必须定义外部服务的 ServiceEntry，或者在安装时配置为 直接访问外部服务。\n本文描述了如何在 Istio 中配置出口流量的 TLS。\n用例 # 考虑一个对外部站点执行 HTTP 调用的遗留应用程序, 假设运行应用程序的组织收到一个新要求，该要求规定必须加密所有外部流量, 使用 Istio，只需通过配置就可以实现这样的要求，而无需更改应用程序的代码。\n在此任务中，如果原始流量为 HTTP，则将 Istio 配置为打开与外部服务的 HTTPS 连接, 应用程序将像以前一样发送未加密的 HTTP 请求，Istio 将加密应用程序的请求。\n前提条件 # 按照 安装指南中的说明设置 Istio 。 启动 sleep 示例，它将作为外部调用的测试源。 如果您已启用 自动注入 sidecar, 请按如下命令部署 sleep 应用程序:\n$ kubectl apply -f samples/sleep/sleep.yaml 否则，您必须在部署 sleep 应用程序之前手动注入 sidecar：\n$ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/sleep/sleep.yaml) 请注意，任何可以 exec 和 curl 的 pod 都可以执行以下步骤。\n创建一个 shell 变量来保存源 pod 的名称，以便将请求发送到外部服务, 如果您使用 sleep 示例，请按如下命令运行: $ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name}) 配置 HTTP 和 HTTPS 外部服务 # 首先，与 控制出口流量任务相同的方式配置对 cnn.com 的访问。 请注意，在 hosts 中定义中使用 * 通配符：*.cnn.com , 使用通配符可以访问 www.cnn.com 以及 edition.cnn.com 。\n1. 创建一个 ServiceEntry 以允许访问外部 HTTP 和 HTTPS 服务：\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: cnn spec: hosts: - \u0026#34;*.cnn.com\u0026#34; ports: - number: 80 name: http-port protocol: HTTP - number: 443 name: https-port protocol: HTTPS resolution: NONE 2. 向外部 HTTP 服务发出请求：\n$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 301 Moved Permanently ... location: https://edition.cnn.com/politics ... HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 ... Content-Length: 151654 ... 输出应该与上面的类似（一些细节用省略号代替）。\n注意 curl 的 -L 标志，它指示 curl 遵循重定向, 在这种情况下， 服务器返回一个重定向响应（ 301 Moved Permanently）到 http://edition.cnn.com/politics 的 HTTP 请求, 重定向响应指示客户端通过 HTTPS 向 https://edition.cnn.com/politics 发送附加请求, 对于第二个请求，服务器返回所请求的内容和 200 OK 状态代码。\n而对于 curl 命令，这种重定向是透明的，这里有两个问题, 第一个问题是冗余的第一个请求，它使获取 http://edition.cnn.com/politics 内容的延迟加倍, 第二个问题是 URL 的路径，在这种情况下是 politics ，以明文形式发送, 如果有攻击者嗅探您的应用程序与 cnn.com 之间的通信，则攻击者会知道您的应用程序获取的 cnn.com 的哪些特定主题和文章, 出于隐私原因，您可能希望阻止攻击者披露此类信息。\n在下一节中，我们将通过配置 Istio 执行 TLS 来解决这两个问题, 在继续下一部分之前先清理配置：\n$ kubectl delete serviceentry cnn 出口流量的 TLS # 总共需要创建三个资源对象，先定义一个 ServiceEntry 以允许网格内部应用程序访问 edition.cnn.com ，然后定义一个 VirtualService 来执行请求端口的重定向，最后定义一个 DestinationRule 用来执行 TLS 发起。\nServiceEntry # $ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: cnn spec: hosts: - edition.cnn.com ports: - number: 80 name: http-port protocol: HTTP - number: 443 name: http-port-for-tls-origination protocol: HTTP resolution: DNS EOF 与上一节中的 ServiceEntry 不同，这里将端口 433 上的协议改为 HTTP，因为客户端将发送 HTTP 请求，而 Istio 将为它们执行 TLS 发起, 此外，在此示例中，必须将解析策略设置为 DNS 才能正确配置 Envoy。\n此处 ServiceEntry 与 Envoy 配置文件的映射关系可以参考我之前的文章 控制 Egress 流量 中的 HTTP ServiceEntry 配置深度解析这一部分，具体细节不再赘述。\nVirtualService # $ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: rewrite-port-for-edition-cnn-com spec: hosts: - edition.cnn.com http: - match: - port: 80 route: - destination: host: edition.cnn.com port: number: 443 EOF 通过 图 1 可以看出目的地址是 edition.cnn.com:80 的流量被路由到了 Cluster outbound|80||edition.cnn.com。创建了 VirtualService 之后我们再来看一下这部分的路由：\n$ istioctl pc routes $SOURCE_POD --name 80 -o json ... { \u0026#34;name\u0026#34;: \u0026#34;edition.cnn.com:80\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;edition.cnn.com\u0026#34;, \u0026#34;edition.cnn.com:80\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|443||edition.cnn.com\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;maxGrpcTimeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, \u0026#34;decorator\u0026#34;: { \u0026#34;operation\u0026#34;: \u0026#34;edition.cnn.com:443/*\u0026#34; }, ... ... 该 VirtualService 的作用就是将目的地址是 edition.cnn.com:80 的流量重新路由到 Cluster outbound|443||edition.cnn.com，以此来实现访问 80 端口重定向到 443 端口的功能。\n请注意 VirtualService 使用特定的主机 edition.cnn.com （没有通配符），因为 Envoy 代理需要确切地知道使用 HTTPS 访问哪个主机。\n但此时我们仍然不能访问外部服务，因为 istio 通过 443 端口发起连接的时候，使用的仍然是 HTTP 协议，具体可以看 Envoy 的配置文件：\n$ istioctl pc clusters $SOURCE_POD --fqdn edition.cnn.com --port 443 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|443||edition.cnn.com\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;edition.cnn.com\u0026#34;, \u0026#34;portValue\u0026#34;: 443 } } ], \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] }, \u0026#34;dnsLookupFamily\u0026#34;: \u0026#34;V4_ONLY\u0026#34; } ] DestinationRule # 现在只剩下最后一步，我们需要让 istio 在出口流量上执行 TLS 发起，使用 HTTPS 协议来访问外部服务。\n$ kubectl apply -f - \u0026lt;\u0026lt;EOF apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: originate-tls-for-edition-cnn-com spec: host: edition.cnn.com trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 443 tls: mode: SIMPLE # initiates HTTPS when accessing edition.cnn.com EOF 再来看一下 Envoy 的 Cluster 配置：\n$ istioctl pc clusters $SOURCE_POD --fqdn edition.cnn.com --port 443 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|443||edition.cnn.com\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;edition.cnn.com\u0026#34;, \u0026#34;portValue\u0026#34;: 443 } } ], \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] }, \u0026#34;tlsContext\u0026#34;: { \u0026#34;commonTlsContext\u0026#34;: {} }, \u0026#34;dnsLookupFamily\u0026#34;: \u0026#34;V4_ONLY\u0026#34; } ] 创建 DestinationRule 之后，Cluster 配置项中多了一个 tlsContext 字段，该字段用来指定连接到上游群集的 TLS 配置（由于这里是出口流量，所以上游集群在这里指的是外部服务 edition.cnn.com）。如果没有添加该字段，则 Envoy 将不会使用 TLS 发起新连接。具体参考： Envoy 官方文档\n现在发送 HTTP 请求到 http://edition.cnn.com/politics ，如上一节所述：\n$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 ... Content-Length: 151654 ... 这次你会直接收到 200 OK 状态码，因为 Istio 为 curl 执行了 TLS 发起，原始 HTTP 请求会被转化为 HTTPS 转发到 cnn.com，cnn.com 服务器直接返回内容，无需重定向。这就消除了客户端和服务器之间的双重往返，并且请求被 Istio 在网格中加密，而没有暴露出应用程序获取 cnn.com 的 politics 部分这一事实。\n请注意，这里使用的命令与上一节中的命令相同，如果你以编程方式访问外部服务的应用程序，配置出口流量的 TLS 之后代码也不需要更改。因此，您可以通过配置 Istio 来获得 TLS 的好处，而无需对代码进行更改。\n其他安全因素 # 请注意，应用程序 pod 与本地主机上的 sidecar 之间的流量仍未加密，这意味着如果攻击者能够穿透应用程序所在的节点，他们仍然可以在该节点的本地网络上看到未加密的通信。在某些环境中，可能存在严格的安全要求，即必须加密所有流量，即使在节点的本地网络上也是如此，如果有这么严格的要求，应用程序应该只使用 HTTPS（TLS），此任务中描述的 TLS 是不够的。\n另外还需要注意，即使对于应用程序发起的 HTTPS ，虽然所有 HTTP 详细信息（主机名，路径，标头等）都是加密的，但攻击者可以通过检查 服务器名称指示（SNI）得知加密请求中的主机名称，因为在 TLS 握手期间，发送 SNI 字段时是不加密的。使用 HTTPS 可防止攻击者了解特定的主题和文章，但这并不能阻止攻击者发现你访问的是 cnn.com。\n清理 # 删除创建的 Istio 资源对象：\n$ kubectl delete serviceentry cnn $ kubectl delete virtualservice rewrite-port-for-edition-cnn-com $ kubectl delete destinationrule originate-tls-for-edition-cnn-com 删除 sleep 服务：\n$ kubectl delete -f samples/sleep/sleep.yaml ","date":"2018年11月16日","externalUrl":null,"permalink":"/posts/egress-tls-origination/","section":"博客","summary":"本文主要内容来自 Istio 官方文档，并对其进行了大量扩展和补充。 控制","title":"Istio 出口流量的 TLS","type":"posts"},{"content":" 不蒜子 是 Bruce 开发的一款轻量级的网页计数器，它的口号是（非官方）：\n轻量级，但好用。\n如果你想尝试不蒜子计数器，可以查阅 不蒜子计数器的介绍文档。\n不蒜子虽好，但也有一些问题。Bruce 在文档中提到：\n我的网站已经运行一段时间了，想初始化访问次数怎么办？\n请先注册登录，自行修改阅读次数。\n但因为各(qi)种(shi)原(shi)因(lan)，注册登录的功能一直没有上线。所以现在，如果用户希望修改初始值，则必须联系 Bruce，让他手工升级。这无疑违背了 geek 的原则。于是这篇文章提出一个非官方的办法，解决这个问题。我们的口号是：\n非官方，但好用。\n分析问题 # 不蒜子之所以被成为「geek 的计数器」，就是因为它的安装使用非常简单——只需要加载计数器 js 脚本，以及使用 span 标签显示计数器结果就可以了。其余所有的事情，都交给用户的 css 去控制。因此，自然，这个「所有的事情」也包括了最终显示的值是多少。因此，我们可以在最终显示的数字上做一些手脚。\n不蒜子的站点 PV 对应的标签是这样的：\n\u0026lt;span id=\u0026#34;busuanzi_value_site_pv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; PV 即 Page View，网站浏览量 指页面的浏览次数，用以衡量网站用户访问的网页数量。用户没打开一个页面便记录 1 次 PV，多次打开同一页面则浏览量累计。 UV 即 Unique Visitor，独立访客数 指 1 天内访问某站点的人数，以 cookie 为依据。1 天内同一访客的多次访问只计为 1 个访客。 既然如此，我们只需要在页面上用 js 取得这个标签中的值，而后加上一个偏移量作为初始值就可以了。如果使用 jQuery，可以这样做：\n\u0026lt;script src=\u0026#34;//cdn.bootcss.com/jquery/3.2.1/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; $(document).ready( var busuanziSiteOffset = parseInt(100000); function fixCount() { if ($(\u0026#34;#busuanzi_container_site_pv\u0026#34;).css(\u0026#34;display\u0026#34;) != \u0026#34;none\u0026#34;) { $(\u0026#34;#busuanzi_value_site_pv\u0026#34;).html(parseInt($(\u0026#34;#busuanzi_value_site_pv\u0026#34;).html()) + busuanziSiteOffset); } } ); \u0026lt;/script\u0026gt; 余下唯一的问题，就是不蒜子的 js 代码，是通过异步的方式加载的。而在其加载完成之前，上述 span 标签会整个被隐藏起来，不可见。于是，这样的朴素的修复就会失效了。\n对付「异步」，一个朴素的处理方式是定期轮询。比如这样：\n\u0026lt;script src=\u0026#34;//cdn.bootcss.com/jquery/3.2.1/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; $(document).ready(function() { var int = setInterval(fixCount, 100); var busuanziSiteOffset = parseInt(10000); function fixCount() { if ($(\u0026#34;#busuanzi_container_site_pv\u0026#34;).css(\u0026#34;display\u0026#34;) != \u0026#34;none\u0026#34;) { clearInterval(int); $(\u0026#34;#busuanzi_value_site_pv\u0026#34;).html(parseInt($(\u0026#34;#busuanzi_value_site_pv\u0026#34;).html()) + busuanziSiteOffset); } } }); \u0026lt;/script\u0026gt; Hugo 的解法 # 在上面的分析中，我们实际上已经有了完整的解法。不过，这样的解法可定制性非常差。试想，在需要修改初始值的时候，都需要深入到代码中去，而后修改 var busuanziSiteOffset = parseInt(10000); 的值。这种事情，想想就令人崩溃。\n对于 Hugo 来说，在站点或主题配置中的变量，可以在主题模版中引用得到。于是，我们可以这样做：\n$ cat config.toml ... [Params] ... # busuanzi busuanzi = true busuanzi_site_offset = 100000 ... 然后将 js 脚本添加到 header 信息中：\n$ cat themes/beautifulhugo/layouts/partials/head_custom.html {{ if isset .Site.Params \u0026#34;busuanzi\u0026#34; }} \u0026lt;!-- 不蒜子 --\u0026gt; \u0026lt;script async src=\u0026#34;//cdn.busuanzi.ibruce.info/cdn/busuanzi/2.3/busuanzi.pure.mini.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- 不蒜子计数初始值纠正 --\u0026gt; \u0026lt;script src=\u0026#34;//cdn.bootcss.com/jquery/3.2.1/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; $(document).ready(function() { var int = setInterval(fixCount, 100); var busuanziSiteOffset = {{ .Site.Params.busuanzi_site_offset }} function fixCount() { if ($(\u0026#34;#busuanzi_container_site_pv\u0026#34;).css(\u0026#34;display\u0026#34;) != \u0026#34;none\u0026#34;) { clearInterval(int); $(\u0026#34;#busuanzi_value_site_pv\u0026#34;).html(parseInt($(\u0026#34;#busuanzi_value_site_pv\u0026#34;).html()) + busuanziSiteOffset); } } }); \u0026lt;/script\u0026gt; {{ end }} 添加站点 PV 和 UV：\n$ cat themes/beautifulhugo/layouts/partials/footer.html \u0026lt;p class=\u0026#34;credits copyright text-muted\u0026#34;\u0026gt; \u0026amp;copy;2017-2018 {{ if .Site.Author.name }} {{ if .Site.Author.website }} \u0026lt;a href=\u0026#34;{{ .Site.Author.website }}\u0026#34;\u0026gt;{{ .Site.Author.name }}\u0026lt;/a\u0026gt; {{ else }} {{ .Site.Author.name }} {{ end }} {{ end }} \u0026amp;nbsp;\u0026amp;bull;\u0026amp;nbsp; {{ .Site.LastChange.Format \u0026#34;January 2,2006\u0026#34; }} updated {{ if .Site.Title }} \u0026amp;nbsp;\u0026amp;bull;\u0026amp;nbsp; \u0026lt;a href=\u0026#34;{{ \u0026#34;\u0026#34; | absLangURL }}\u0026#34;\u0026gt;Home\u0026lt;/a\u0026gt; {{ end }} \u0026lt;/p\u0026gt; ... \u0026lt;p class=\u0026#34;credits theme-by text-muted\u0026#34;\u0026gt; ... \u0026lt;span id=\u0026#34;busuanzi_container_site_pv\u0026#34;\u0026gt; 本站访问量：\u0026lt;span id=\u0026#34;busuanzi_value_site_pv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;次 \u0026lt;/span\u0026gt; \u0026amp;nbsp; \u0026lt;span id=\u0026#34;busuanzi_container_site_uv\u0026#34;\u0026gt; 您是本站第 \u0026lt;span id=\u0026#34;busuanzi_value_site_uv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 位访问者 \u0026lt;/span\u0026gt; \u0026lt;/p\u0026gt; 展示效果：\n添加页面 PV：\n$ cat themes/beautifulhugo/layouts/partials/postmeta.html {{ $baseurl := .Site.BaseURL }} \u0026lt;div\u0026gt; \u0026lt;section id=\u0026#34;datecount\u0026#34;\u0026gt; \u0026lt;h4 id=\u0026#34;date\u0026#34;\u0026gt; {{ .Date.Format \u0026#34;Mon Jan 2, 2006\u0026#34; }}\u0026lt;/h4\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;h5 id=\u0026#34;wc\u0026#34;\u0026gt;{{ .FuzzyWordCount }} Words|Read in about {{ .ReadingTime }} Min|本文总阅读量\u0026lt;span id=\u0026#34;busuanzi_value_page_pv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;次\u0026lt;/h5\u0026gt; \u0026lt;h5 id=\u0026#34;tags\u0026#34;\u0026gt;Tags: {{ range .Params.tags }} \u0026lt;!--tags前的那个/不要去掉，否则点击链接后无法跳转--\u0026gt; \u0026lt;a href=\u0026#34;{{ $baseurl }}tags/{{ . | urlize }}/\u0026#34;\u0026gt;{{ . }}\u0026lt;/a\u0026gt; \u0026amp;nbsp;{{ end }} \u0026lt;/h5\u0026gt; \u0026lt;/div\u0026gt; 展示效果：\n大家可以根据自己的审美，功能来定制主题，首先需要对主题的结构，调用等信息清楚，然后再添加自己的改动。\n参考资料 # 不蒜子计数器初始化的非官方办法 ","date":"2018年11月12日","externalUrl":null,"permalink":"/posts/hugo-add-busuanzi/","section":"博客","summary":"不蒜子 是 Bruce 开发的一款轻量级的网页计数器，它的口号是（非官方）","title":"Hugo 添加站点统计信息","type":"posts"},{"content":" 原文链接： Understanding How the Service Mesh Fits with Performance Testing\n现代 IT 企业的数字基础设施极其复杂，通过手动配置防火墙来保护一台连接到路由器的服务器的日子已经一去不复返了。\n今天我们生活在虚拟化和弹性计算的世界中，计算资源基础设施需要具有自动扩展和收缩的能力来满足当下的各种需求，因为 IP 地址来得快去得也快，安全策略每分钟都可能发生变化，任何一个服务都可能要随时随地运行。这时候就需要新的自动化技术来支撑超越人类管理能力的企业，而在数字化领域， 服务网格（Service Mesh）正是我们所需要的这种技术。\n服务网格正在扩展分布式系统在服务发现、操作和维护方面的能力，它不仅影响了服务如何部署到公司的数字环境中，而且该技术还将在系统可靠性和性能方面发挥更大作用。因此，我建议那些关注性能和可靠性测试的童鞋掌握服务网络的工作原理，特别是关于路由和重试这一部分。\n随着服务网格逐渐成为标准控制平面，性能测试工程师在创建适用于服务网格体系架构的测试计划时提前熟悉该技术将会变得很有必要。\n服务网格的用例 # 服务网格解决了现代分布式计算中的两个基本问题：如何在系统中查找服务的位置，以及定义了当服务出现故障时该如何应对。\n在服务网格出现之前，每个服务都需要知道它所依赖的服务的位置才能正常工作。例如，如下图所示，为了使服务 A 能够将请求任务传递给服务 C，它需要知道服务 C 的确切位置。服务 C 的位置可以定义为 IP 地址或 DNS 域名。一旦服务 C 的位置发生了变化，如果情况不是太糟，改一下服务 A 的配置就可以继续工作了，更糟糕的情况下整个服务 A 可能都需要重写。\n服务之间的紧耦合将会导致系统很脆弱，并且难以扩展，因此很多公司开始使用诸如 ZooKeeper， Consul 和 Etcd 等服务发现工具，这样服务就不再需要知道它所依赖的服务的位置也可以正常工作了。如下图所示：\n然而还有一个问题需要解决：当服务 A 调用其中一个依赖服务失败时，服务 A 会执行什么操作，它应该报错还是重试？如果重试，那么应该重试多少次才算失败？这时候服务网格就派上用场了。\n服务网格聚合了服务发现和故障策略等其他功能，也就是说服务网格不仅允许各个服务之间相互交互，还会根据配置的策略执行重试、重定向或终止等操作。如下图所示：\n服务网格是一个控制平面，可以在各个服务之间路由流量，并为每个服务提供故障安全机制。此外，服务网格还将网格内流量的所有活动都记录下来，从而提供对系统整体性能的可观察性。这种记录方式增加了分布式链路追踪的可能性，这样不需要关心每个服务的位置就可以对这些服务进行观测和故障排除。\n目前比较流行的服务网格技术是 Linkerd、 Envoy 和 Istio。\n从性能测试的角度来理解服务网格的原因在于该技术对系统性能有着直接影响。因此，测试工程师至少应该掌握服务网格技术的原理和实践方法。同时，测试工程师可以通过将服务网格中生成的数据集成到测试计划和报告中获得很多好处。\n在性能测试计划中使用服务网格 # 性能测试工程师该如何利用服务网络提供的这些功能？这取决于性能测试的范围和测试工程师对服务网格的兴趣。如果工程师只关心 Web 客户端和 Web 服务端之间的响应时间，那么只需要理解服务网格的原理和使用方式就够了。但如果测试过程中需要关注服务端任何一个应用程序的性能，那么事情就会变得有趣了。\n第一个也是最有说服力的好处是服务网格支持分布式链路追踪。这意味着服务网格可以观察到分布式架构中所有服务在调用期间的执行时间，因此测试工程师可以更准确地识别系统的性能瓶颈。一旦确定了瓶颈所在，就可以根据追踪数据找到与之相关的具体配置，以便发现性能问题的本质原因。\n服务网格除了为测试服务提供相关信息之外，它本身也会成为测试的关注点。记住：服务网格的配置将会对系统性能产生直接影响，这种影响为性能测试增加了一个新的维度。在测试过程中除了需要关注应用程序本身的逻辑，还需要关注服务网格本身，比如在测试自动重试时，如何配置好请求截止时间和熔断将会起到很重要的作用。\n自动重试 : 自动重试是服务网格中的一项配置，可以使消费服务在返回特定类型的错误代码时重新尝试调用依赖服务。例如，如果服务 A 调用服务 B，而服务 B 返回了 502 错误（网关出错），则服务 A 将会根据配置自动尝试重新调用服务 B 若干次。由于 502 错误可能是暂时的环境抖动，会很快恢复，所以重试是一个很合理的行为。 请求截止时间 : 请求截止时间与超时类似，允许在特定的时间段内对特定的服务执行调用请求。如果到了截止时间，无论如何配置重试策略，调用请求都会失败，从而防止被调用服务的负载过高。 熔断 : 当系统中的某个单点（例如某个服务）发生故障并导致其他其他单点也接连发生故障时，可以通过熔断来防止系统出现级联故障。熔断器是一个围绕在服务周围的组件，如果服务处于故障状态，熔断器就会”跳闸“，这时对失败服务的调用请求会立即被作为错误拒绝，而不必承担流量转发和服务调用的开销。如果是在服务网格中，熔断器还会记录对故障服务的尝试调用过程，同时通过对网格内的服务配置监控和告警策略来应对熔断器的打开和关闭。 随着服务网格渐渐成为企业系统架构的一部分，性能测试工程师会逐渐将服务网格本身的测试作为整体性能测试计划的一部分。\n总结 # 使用传统的方式进行性能测试的日子即将结束，现代化的应用程序过于复杂，中间有太多的依赖服务，不能仅依靠测试客户端和服务端之间的请求和响应时间来判断其性能。作为一个合格的企业架构师，无论基础设施的规模有多大，变化速度有多快，都不会为了实现动态配置和管理的需求而牺牲观察和管理系统的能力。\n随着 DevOps 的精神不断渗透到 IT 文化中，服务网络正在成为使用现代分布式架构的企业的关键组成部分。如果能深入理解服务网格技术的价值和使用方式，测试工程师就能为性能测试添加新的维度。如果能够精通该技术，那么服务网格将会为你带来最大的优化效益。\n本文作者是 Bob Reselman，著名的软件开发者，系统架构师，行业分析师和技术作家。Bob 撰写了许多关于计算机编程方面的书籍和数十篇关于软件开发技术以及软件开发文化的文章。Bob 是 Cap Gemini 的首席顾问，也是计算机制造商 Gateway 的平台架构师。除了软件开发和测试之外，Bob 还在编写一本关于自动化对人类就业影响的书。他住在洛杉矶，可以直接通过他的 LinkedIn 链接 和他联系。\n","date":"2018年11月11日","externalUrl":null,"permalink":"/posts/service-mesh-performance-testing/","section":"博客","summary":"原文链接： Understanding How the Service Mesh Fits with Performance Testing 现代 IT 企业的数字基础设施极其复杂","title":"了解如何在服务网格中进行性能测试","type":"posts"},{"content":" 原文链接： Understanding resource limits in kubernetes: cpu time\n建议先阅读下面的系列文章：\nLinux Cgroup 入门教程：基本概念 Linux Cgroup 入门教程：CPU Linux Cgroup 入门教程：内存 在关于 Kubernetes 资源限制的系列文章的 第一篇文章中，我讨论了如何使用 ResourceRequirements 对象来设置 Pod 中容器的内存资源限制，以及如何通过容器运行时和 linux control group（cgroup）来实现这些限制。我还谈到了 Requests 和 Limits 之间的区别，其中 Requests 用于在调度时通知调度器 Pod 需要多少资源才能调度，而 Limits 用来告诉 Linux 内核什么时候你的进程可以为了清理空间而被杀死。在这篇文章中，我会继续仔细分析 CPU 资源限制。想要理解这篇文章所说的内容，不一定要先阅读上一篇文章，但我建议那些工程师和集群管理员最好还是先阅读完第一篇，以便全面掌控你的集群。\nCPU 限制 # 正如我在上一篇文章中提到的，CPU 资源限制比内存资源限制更复杂，原因将在下文详述。幸运的是 CPU 资源限制和内存资源限制一样都是由 cgroup 控制的，上文中提到的思路和工具在这里同样适用，我们只需要关注他们的不同点就行了。首先，让我们将 CPU 资源限制添加到之前示例中的 yaml：\nresources: requests: memory: 50Mi cpu: 50m limits: memory: 100Mi cpu: 100m 单位后缀 m 表示千分之一核，也就是说 1 Core = 1000m。因此该资源对象指定容器进程需要 50/1000 核（5%）才能被调度，并且允许最多使用 100/1000 核（10%）。同样，2000m 表示两个完整的 CPU 核心，你也可以写成 2 或者 2.0。为了了解 Docker 和 cgroup 如何使用这些值来控制容器，我们首先创建一个只配置了 CPU requests 的 Pod：\n$ kubectl run limit-test --image=busybox --requests \u0026#34;cpu=50m\u0026#34; --command -- /bin/sh -c \u0026#34;while true; do sleep 2; done\u0026#34; deployment.apps \u0026#34;limit-test\u0026#34; created 通过 kubectl 命令我们可以验证这个 Pod 配置了 50m 的 CPU requests：\n$ kubectl get pods limit-test-5b4c495556-p2xkr -o=jsonpath=\u0026#39;{.spec.containers[0].resources}\u0026#39; map[requests:map[cpu:50m]] 我们还可以看到 Docker 为容器配置了相同的资源限制：\n$ docker ps | grep busy | cut -d\u0026#39; \u0026#39; -f1 f2321226620e $ docker inspect f2321226620e --format \u0026#39;{{.HostConfig.CpuShares}}\u0026#39; 51 这里显示的为什么是 51，而不是 50？这是因为 Linux cgroup 和 Docker 都将 CPU 核心数分成了 1024 个时间片（shares），而 Kubernetes 将它分成了 1000 个 shares。\nshares 用来设置 CPU 的相对值，并且是针对所有的 CPU（内核），默认值是 1024，假如系统中有两个 cgroup，分别是 A 和 B，A 的 shares 值是 1024，B 的 shares 值是 512，那么 A 将获得 1024/(1204+512)=66% 的 CPU 资源，而 B 将获得 33% 的 CPU 资源。shares 有两个特点：\n如果 A 不忙，没有使用到 66% 的 CPU 时间，那么剩余的 CPU 时间将会被系统分配给 B，即 B 的 CPU 使用率可以超过 33%。 如果添加了一个新的 cgroup C，且它的 shares 值是 1024，那么 A 的限额变成了 1024/(1204+512+1024)=40%，B 的变成了 20%。 从上面两个特点可以看出：\n在闲的时候，shares 基本上不起作用，只有在 CPU 忙的时候起作用，这是一个优点。 由于 shares 是一个绝对值，需要和其它 cgroup 的值进行比较才能得到自己的相对限额，而在一个部署很多容器的机器上，cgroup 的数量是变化的，所以这个限额也是变化的，自己设置了一个高的值，但别人可能设置了一个更高的值，所以这个功能没法精确的控制 CPU 使用率。 与配置内存资源限制时 Docker 配置容器进程的内存 cgroup 的方式相同，设置 CPU 资源限制时 Docker 会配置容器进程的 cpu,cpuacct cgroup：\n$ ps ax | grep /bin/sh 60554 ? Ss 0:00 /bin/sh -c while true; do sleep 2; done $ sudo cat /proc/60554/cgroup ... 4:cpu,cpuacct:/kubepods/burstable/pode12b33b1-db07-11e8-b1e1-42010a800070/3be263e7a8372b12d2f8f8f9b4251f110b79c2a3bb9e6857b2f1473e640e8e75 $ ls -l /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pode12b33b1-db07-11e8-b1e1-42010a800070/3be263e7a8372b12d2f8f8f9b4251f110b79c2a3bb9e6857b2f1473e640e8e75 total 0 drwxr-xr-x 2 root root 0 Oct 28 23:19 . drwxr-xr-x 4 root root 0 Oct 28 23:19 .. ... -rw-r--r-- 1 root root 0 Oct 28 23:19 cpu.shares Docker 容器的 HostConfig.CpuShares 属性映射到 cgroup 的 cpu.shares 属性，可以验证一下：\n$ sudo cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/podb5c03ddf-db10-11e8-b1e1-42010a800070/64b5f1b636dafe6635ddd321c5b36854a8add51931c7117025a694281fb11444/cpu.shares 51 你可能会很惊讶，设置了 CPU requests 竟然会把值传播到 cgroup，而在上一篇文章中我们设置内存 requests 时并没有将值传播到 cgroup。这是因为内存的 soft limit 内核特性对 Kubernetes 不起作用，而设置了 cpu.shares 却对 Kubernetes 很有用。后面我会详细讨论为什么会这样。现在让我们先看看设置 CPU limits 时会发生什么：\n$ kubectl run limit-test --image=busybox --requests \u0026#34;cpu=50m\u0026#34; --limits \u0026#34;cpu=100m\u0026#34; --command -- /bin/sh -c \u0026#34;while true; do sleep 2; done\u0026#34; deployment.apps \u0026#34;limit-test\u0026#34; created 再一次使用 kubectl 验证我们的资源配置：\n$ kubectl get pods limit-test-5b4fb64549-qpd4n -o=jsonpath=\u0026#39;{.spec.containers[0].resources}\u0026#39; map[limits:map[cpu:100m] requests:map[cpu:50m]] 查看对应的 Docker 容器的配置：\n$ docker ps | grep busy | cut -d\u0026#39; \u0026#39; -f1 f2321226620e $ docker inspect 472abbce32a5 --format \u0026#39;{{.HostConfig.CpuShares}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}\u0026#39; 51 10000 100000 可以明显看出，CPU requests 对应于 Docker 容器的 HostConfig.CpuShares 属性。而 CPU limits 就不太明显了，它由两个属性控制：HostConfig.CpuPeriod 和 HostConfig.CpuQuota。Docker 容器中的这两个属性又会映射到进程的 cpu,couacct cgroup 的另外两个属性：cpu.cfs_period_us 和 cpu.cfs_quota_us。我们来看一下：\n$ sudo cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod2f1b50b6-db13-11e8-b1e1-42010a800070/f0845c65c3073e0b7b0b95ce0c1eb27f69d12b1fe2382b50096c4b59e78cdf71/cpu.cfs_period_us 100000 $ sudo cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod2f1b50b6-db13-11e8-b1e1-42010a800070/f0845c65c3073e0b7b0b95ce0c1eb27f69d12b1fe2382b50096c4b59e78cdf71/cpu.cfs_quota_us 10000 如我所说，这些值与容器配置中指定的值相同。但是这两个属性的值是如何从我们在 Pod 中设置的 100m cpu limits 得出的呢，他们是如何实现该 limits 的呢？这是因为 cpu requests 和 cpu limits 是使用两个独立的控制系统来实现的。Requests 使用的是 cpu shares 系统，cpu shares 将每个 CPU 核心划分为 1024 个时间片，并保证每个进程将获得固定比例份额的时间片。如果总共有 1024 个时间片，并且两个进程中的每一个都将 cpu.shares 设置为 512，那么它们将分别获得大约一半的 CPU 可用时间。但 cpu shares 系统无法精确控制 CPU 使用率的上限，如果一个进程没有设置 shares，则另一个进程可用自由使用 CPU 资源。\n大约在 2010 年左右，谷歌团队和其他一部分人注意到了 这个问题。为了解决这个问题，后来在 linux 内核中增加了第二个功能更强大的控制系统 : CPU 带宽控制组。带宽控制组定义了一个 周期，通常为 1/10 秒（即 100000 微秒）。还定义了一个 配额，表示允许进程在设置的周期长度内所能使用的 CPU 时间数，两个文件配合起来设置CPU的使用上限。两个文件的单位都是微秒（us），cfs_period_us 的取值范围为 1 毫秒（ms）到 1 秒（s），cfs_quota_us 的取值大于 1ms 即可，如果 cfs_quota_us 的值为 -1（默认值），表示不受 CPU 时间的限制。\n下面是几个例子：\n# 1.限制只能使用1个CPU（每250ms能使用250ms的CPU时间） $ echo 250000 \u0026gt; cpu.cfs_quota_us /* quota = 250ms */ $ echo 250000 \u0026gt; cpu.cfs_period_us /* period = 250ms */ # 2.限制使用2个CPU（内核）（每500ms能使用1000ms的CPU时间，即使用两个内核） $ echo 1000000 \u0026gt; cpu.cfs_quota_us /* quota = 1000ms */ $ echo 500000 \u0026gt; cpu.cfs_period_us /* period = 500ms */ # 3.限制使用1个CPU的20%（每50ms能使用10ms的CPU时间，即使用一个CPU核心的20%） $ echo 10000 \u0026gt; cpu.cfs_quota_us /* quota = 10ms */ $ echo 50000 \u0026gt; cpu.cfs_period_us /* period = 50ms */ 在本例中我们将 Pod 的 cpu limits 设置为 100m，这表示 100/1000 个 CPU 核心，即 100000 微秒的 CPU 时间周期中的 10000。所以该 limits 翻译到 cpu,cpuacct cgroup 中被设置为 cpu.cfs_period_us=100000 和 cpu.cfs_quota_us=10000。顺便说一下，其中的 cfs 代表 Completely Fair Scheduler（绝对公平调度），这是 Linux 系统中默认的 CPU 调度算法。还有一个实时调度算法，它也有自己相应的配额值。\n现在让我们来总结一下：\n在 Kubernetes 中设置的 cpu requests 最终会被 cgroup 设置为 cpu.shares 属性的值， cpu limits 会被带宽控制组设置为 cpu.cfs_period_us 和 cpu.cfs_quota_us 属性的值。与内存一样，cpu requests 主要用于在调度时通知调度器节点上至少需要多少个 cpu shares 才可以被调度。 与 内存 requests 不同，设置了 cpu requests 会在 cgroup 中设置一个属性，以确保内核会将该数量的 shares 分配给进程。 cpu limits 与 内存 limits 也有所不同。如果容器进程使用的内存资源超过了内存使用限制，那么该进程将会成为 oom-killing 的候选者。但是容器进程基本上永远不能超过设置的 CPU 配额，所以容器永远不会因为尝试使用比分配的更多的 CPU 时间而被驱逐。系统会在调度程序中强制进行 CPU 资源限制，以确保进程不会超过这个限制。 如果你没有在容器中设置这些属性，或将他们设置为不准确的值，会发生什么呢？与内存一样，如果只设置了 limits 而没有设置 requests，Kubernetes 会将 CPU 的 requests 设置为 与 limits 的值一样。如果你对你的工作负载所需要的 CPU 时间了如指掌，那再好不过了。如果只设置了 CPU requests 却没有设置 CPU limits 会怎么样呢？这种情况下，Kubernetes 会确保该 Pod 被调度到合适的节点，并且该节点的内核会确保节点上的可用 cpu shares 大于 Pod 请求的 cpu shares，但是你的进程不会被阻止使用超过所请求的 CPU 数量。既不设置 requests 也不设置 limits 是最糟糕的情况：调度程序不知道容器需要什么，并且进程对 cpu shares 的使用是无限制的，这可能会对 node 产生一些负面影响。\n最后我还想告诉你们的是：为每个 pod 都手动配置这些参数是挺麻烦的事情，kubernetes 提供了 LimitRange 资源，可以让我们配置某个 namespace 默认的 request 和 limit 值。\n默认限制 # 通过上文的讨论大家已经知道了忽略资源限制会对 Pod 产生负面影响，因此你可能会想，如果能够配置某个 namespace 默认的 request 和 limit 值就好了，这样每次创建新 Pod 都会默认加上这些限制。Kubernetes 允许我们通过 LimitRange 资源对每个命名空间设置资源限制。要创建默认的资源限制，需要在对应的命名空间中创建一个 LimitRange 资源。下面是一个例子：\napiVersion: v1 kind: LimitRange metadata: name: default-limit spec: limits: - default: memory: 100Mi cpu: 100m defaultRequest: memory: 50Mi cpu: 50m - max: memory: 512Mi cpu: 500m - min: memory: 50Mi cpu: 50m type: Container 这里的几个字段可能会让你们有些困惑，我拆开来给你们分析一下。\nlimits 字段下面的 default 字段表示每个 Pod 的默认的 limits 配置，所以任何没有分配资源的 limits 的 Pod 都会被自动分配 100Mi limits 的内存和 100m limits 的 CPU。 defaultRequest 字段表示每个 Pod 的默认 requests 配置，所以任何没有分配资源的 requests 的 Pod 都会被自动分配 50Mi requests 的内存和 50m requests 的 CPU。 max 和 min 字段比较特殊，如果设置了这两个字段，那么只要这个命名空间中的 Pod 设置的 limits 和 requests 超过了这个上限和下限，就不会允许这个 Pod 被创建。我暂时还没有发现这两个字段的用途，如果你知道，欢迎在留言告诉我。 LimitRange 中设定的默认值最后由 Kubernetes 中的准入控制器 LimitRanger 插件来实现。准入控制器由一系列插件组成，它会在 API 接收对象之后创建 Pod 之前对 Pod 的 Spec 字段进行修改。对于 LimitRanger 插件来说，它会检查每个 Pod 是否设置了 limits 和 requests，如果没有设置，就给它配置 LimitRange 中设定的默认值。通过检查 Pod 中的 annotations 注释，你可以看到 LimitRanger 插件已经在你的 Pod 中设置了默认值。例如：\napiVersion: v1 kind: Pod metadata: annotations: kubernetes.io/limit-ranger: \u0026#39;LimitRanger plugin set: cpu request for container limit-test\u0026#39; name: limit-test-859d78bc65-g6657 namespace: default spec: containers: - args: - /bin/sh - -c - while true; do sleep 2; done image: busybox imagePullPolicy: Always name: limit-test resources: requests: cpu: 100m 以上就是我对 Kubernetes 资源限制的全部见解，希望能对你有所帮助。如果你想了解更多关于 Kubernetes 中资源的 limits 和 requests、以及 linux cgroup 和内存管理的更多详细信息，可以查看我在文末提供的参考链接。\n参考资料 # Understanding Linux Container Scheduling Managing Compute Resources for Containers Red Hat Customer Portal Chapter 1. Introduction to Control Groups (Cgroups) Configure Default Memory Requests and Limits for a Namespace ","date":"2018年11月10日","externalUrl":null,"permalink":"/posts/understanding-resource-limits-in-kubernetes-cpu-time/","section":"博客","summary":"原文链接： Understanding resource limits in kubernetes: cpu time 建议先阅读下面的系列文章： Linux Cgroup 入门教","title":"深入理解 Kubernetes 资源限制：CPU","type":"posts"},{"content":"很多站长开发网站时为了推广页面，或者获得更多的回访和流量，会在网站页面添加 “分享到” 插件，用来发布到某些社交网站。因此社会化分享是很多网站常用的功能之一，国内也有很多专业的公司在做，比较出名的包括 j*this，B*hare 等。不过很悲伤的是，这些公司的产品，无一例外的具有一个特点：奇丑无比。丑就算了，还不允许别人修改其设计，结果就是，再好的 UI 设计也毁在这些插件手里了。\n还好我发现了一款简单高效的社交分享组件，只看一眼便可以确认这就是我要寻找的那个它。直接上预览，你看完一定会喜欢上：\n简介 # share.js 是一款简单高效的社交分享组件，直接引入使用即可，无须依赖其他库。它有以下这些特点：\n一个标签完成初始化 自定义启用/禁用分享站点 更美观的 UI 体验 基于标签data属性轻松实现分享数据的自定义 支持分别对不同站点设置分享内容 同页面个分享组件 支持npm安装 引入 share.js # 由于我的博客使用的是 hugo，而且使用的主题是 Jimmy Song 的 beautifulhugo，官方文档提供的安装方式不适用，需要稍作改动。\n如果你使用的是其他主题，安装方式类似，你可以自己研究一下。\n导入静态资源 # 首先克隆 share.js 的代码仓库：\n$ git clone https://github.com/overtrue/share.js 然后分别将 css、js 和 fonts 拷贝到 beautiful 主题中的相应目录下：\n# \u0026lt;hugo_home\u0026gt; 表示 hugo 的根目录 $ cp share.js/css/share.min.css \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/static/css/ $ cp share.js/js/social-share.min.js \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/static/js/ $ cp -r share.js/fonts/* \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/static/fonts/ 默认的 css 样式图标太小，我稍微调整了一下，将图标放大一点，修改后的 css 内容如下：\n$ cat \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/static/css/share.min.css @font-face{font-family:\u0026#34;socialshare\u0026#34;;src:url(\u0026#34;../fonts/iconfont.eot\u0026#34;);src:url(\u0026#34;../fonts/iconfont.eot?#iefix\u0026#34;) format(\u0026#34;embedded-opentype\u0026#34;),url(\u0026#34;../fonts/iconfont.woff\u0026#34;) format(\u0026#34;woff\u0026#34;),url(\u0026#34;../fonts/iconfont.ttf\u0026#34;) format(\u0026#34;truetype\u0026#34;),url(\u0026#34;../fonts/iconfont.svg#iconfont\u0026#34;) format(\u0026#34;svg\u0026#34;)} .social-share{font-family:\u0026#34;socialshare\u0026#34; !important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:0.2px;-moz-osx-font-smoothing:grayscale} .social-share *{font-family:\u0026#34;socialshare\u0026#34; !important} .social-share .icon-tencent:before{content:\u0026#34;\\f07a\u0026#34;} .social-share .icon-qq:before{content:\u0026#34;\\f11a\u0026#34;} .social-share .icon-weibo:before{content:\u0026#34;\\f12a\u0026#34;} .social-share .icon-wechat:before{content:\u0026#34;\\f09a\u0026#34;} .social-share .icon-douban:before{content:\u0026#34;\\f10a\u0026#34;} .social-share .icon-heart:before{content:\u0026#34;\\f20a\u0026#34;} .social-share .icon-like:before{content:\u0026#34;\\f00a\u0026#34;} .social-share .icon-qzone:before{content:\u0026#34;\\f08a\u0026#34;} .social-share .icon-linkedin:before{content:\u0026#34;\\f01a\u0026#34;} .social-share .icon-diandian:before{content:\u0026#34;\\f05a\u0026#34;} .social-share .icon-facebook:before{content:\u0026#34;\\f03a\u0026#34;} .social-share .icon-google:before{content:\u0026#34;\\f04a\u0026#34;} .social-share .icon-twitter:before{content:\u0026#34;\\f06a\u0026#34;} .social-share a{position:relative;text-decoration:none;margin:4px;display:inline-block;outline:none} .social-share .social-share-icon{position:relative;display:inline-block;width:42px;height:42px;font-size:25px;border-radius:50%;line-height:37px;border:2px solid #666;color:#666;text-align:center;vertical-align:middle;transition:background 0.6s ease-out 0s} .social-share .social-share-icon:hover{background:#666;color:#fff} .social-share .icon-weibo{color:#ff763b;border-color:#ff763b} .social-share .icon-weibo:hover{background:#ff763b} .social-share .icon-tencent{color:#56b6e7;border-color:#56b6e7} .social-share .icon-tencent:hover{background:#56b6e7} .social-share .icon-qq{color:#56b6e7;border-color:#56b6e7} .social-share .icon-qq:hover{background:#56b6e7} .social-share .icon-qzone{color:#FDBE3D;border-color:#FDBE3D} .social-share .icon-qzone:hover{background:#FDBE3D} .social-share .icon-douban{color:#33b045;border-color:#33b045} .social-share .icon-douban:hover{background:#33b045} .social-share .icon-linkedin{color:#0077B5;border-color:#0077B5} .social-share .icon-linkedin:hover{background:#0077B5} .social-share .icon-facebook{color:#44619D;border-color:#44619D} .social-share .icon-facebook:hover{background:#44619D} .social-share .icon-google{color:#db4437;border-color:#db4437} .social-share .icon-google:hover{background:#db4437} .social-share .icon-twitter{color:#55acee;border-color:#55acee} .social-share .icon-twitter:hover{background:#55acee} .social-share .icon-diandian{color:#307DCA;border-color:#307DCA} .social-share .icon-diandian:hover{background:#307DCA} .social-share .icon-wechat{position:relative;color:#7bc549;border-color:#7bc549} .social-share .icon-wechat:hover{background:#7bc549} .social-share .icon-wechat .wechat-qrcode{display:none;border:1px solid #eee;position:absolute;z-index:9;top:-205px;left:-84px;width:200px;height:192px;color:#666;font-size:12px;text-align:center;background-color:#fff;box-shadow:0 2px 10px #aaa;transition:all 200ms;-webkit-tansition:all 350ms;-moz-transition:all 350ms} .social-share .icon-wechat .wechat-qrcode.bottom{top:40px;left:-84px} .social-share .icon-wechat .wechat-qrcode.bottom:after{display:none} .social-share .icon-wechat .wechat-qrcode h4{font-weight:normal;height:26px;line-height:26px;font-size:12px;background-color:#f3f3f3;margin:0;padding:0;color:#777} .social-share .icon-wechat .wechat-qrcode .qrcode{width:105px;margin:10px auto} .social-share .icon-wechat .wechat-qrcode .qrcode table{margin:0 !important} .social-share .icon-wechat .wechat-qrcode .help p{font-weight:normal;line-height:16px;padding:0;margin:0} .social-share .icon-wechat .wechat-qrcode:after{content:\u0026#39;\u0026#39;;position:absolute;left:50%;margin-left:-6px;bottom:-13px;width:0;height:0;border-width:8px 6px 6px 6px;border-style:solid;border-color:#fff transparent transparent transparent} .social-share .icon-wechat:hover .wechat-qrcode{display:block} 主要修改了这一段：\n.social-share .social-share-icon{position:relative;display:inline-block;width:42px;height:42px;font-size:25px;border-radius:50%;line-height:37px;border:2px solid #666;color:#666;text-align:center;vertical-align:middle;transition:background 0.6s ease-out 0s} 将分享插件嵌入到网页中 # 为了将分享插件嵌入到每篇文章的网页中，我们需要修改一些模板。首先需要引入 css 样式，通过修改文件 \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/layouts/partials/head.html，在其中引入 share.min.css。\n... \u0026lt;!-- bootcss cdn 国外访问太慢 --\u0026gt; \u0026lt;!-- \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.bootcss.com/KaTeX/0.7.1/katex.min.css\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css\u0026#34; /\u0026gt; --\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ \u0026#34;css/main.css\u0026#34; | absURL }}\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ \u0026#34;css/share.min.css\u0026#34; | absURL }}\u0026#34; /\u0026gt; ... 然后在 \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/layouts/partials/目录下创建一个 html。\n$ cat \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/layouts/partials/share.html \u0026lt;div class=\u0026#34;social-share\u0026#34; data-initialized=\u0026#34;true\u0026#34; data-wechat-qrcode-title=\u0026#34;不扫别后悔\u0026#34;\u0026gt; \u0026lt;center\u0026gt; \u0026lt;font style=\u0026#34;font-size:18px;color:darkcyan;\u0026#34;\u0026gt;分享到：\u0026lt;/font\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-weibo\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-wechat\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-twitter\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-linkedin\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-facebook\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-qq\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; class=\u0026#34;social-share-icon icon-qzone\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;/center\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- css \u0026amp; js --\u0026gt; \u0026lt;script src=\u0026#34;https://hugo-picture.oss-cn-beijing.aliyuncs.com/social-share.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 修改模板 \u0026lt;hugo_home\u0026gt;/themes/beautifulhugo/layouts/_default/single.html，加载 share.html。\n\u0026lt;div class=\u0026#34;container\u0026#34; role=\u0026#34;main\u0026#34; itemscope itemtype=\u0026#34;http://schema.org/Article\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\u0026#34;\u0026gt; \u0026lt;!-- post metadata--\u0026gt; {{ if isset .Params \u0026#34;postmeta\u0026#34; }} {{ else }} {{ partial \u0026#34;postmeta.html\u0026#34; . }} {{ end }} \u0026lt;article role=\u0026#34;main\u0026#34; class=\u0026#34;blog-post\u0026#34; itemprop=\u0026#34;articleBody\u0026#34; id=\u0026#34;content\u0026#34;\u0026gt; ... {{ .Content }} {{ partial \u0026#34;share.html\u0026#34; }} \u0026lt;/article\u0026gt; ... 如果你想让某些页面不开启分享插件，可以通过参数 (.Params.noshare) 来控制是否加载分享插件。\n\u0026lt;div class=\u0026#34;container\u0026#34; role=\u0026#34;main\u0026#34; itemscope itemtype=\u0026#34;http://schema.org/Article\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1\u0026#34;\u0026gt; \u0026lt;!-- post metadata--\u0026gt; {{ if isset .Params \u0026#34;postmeta\u0026#34; }} {{ else }} {{ partial \u0026#34;postmeta.html\u0026#34; . }} {{ end }} \u0026lt;article role=\u0026#34;main\u0026#34; class=\u0026#34;blog-post\u0026#34; itemprop=\u0026#34;articleBody\u0026#34; id=\u0026#34;content\u0026#34;\u0026gt; ... {{ .Content }} {{ if not (.Params.noshare) }} {{ partial \u0026#34;share.html\u0026#34; }} {{ end }} \u0026lt;/article\u0026gt; ... 这样我们就可以在页面中通过 noshare 参数来控制了。如下是不想加载分享插件的文章的 meta 信息参数：\n--- title: xxxxxx date: xxxxxx ... noshare: true --- 更多 # 关于分享插件的更多自定义配置请参考代码仓库的 README。\n","date":"2018年11月7日","externalUrl":null,"permalink":"/posts/hugo-social-share-plugin/","section":"博客","summary":"很多站长开发网站时为了推广页面，或者获得更多的回访和流量，会","title":"Hugo 集成社交分享插件","type":"posts"},{"content":" 原文链接： SRE: Resiliency: Bolt on Rate Limiting using Envoy\n速率限制是缓解级联故障和防止耗尽共享资源的一种简单有效的方法。Envoy 是一个功能丰富的代理，可以为任何服务轻松添加速率限制的功能。本文将介绍在不更改应用程序本身配置的前提下如何配置 Envoy 来强制对应用进行速率限制。\n问题 # 你是否遇到过资源被大量的请求淹没或耗尽的情况？你的客户端是否具有回退重试或速率限制的逻辑？在微服务架构中，不对其使用量进行限制的资源很容易被客户端发出的大量请求所淹没。当然可能存在一定数量的客户端，这些客户端本身就已经实现了各种重试/回退和速率限制的策略。不对访问量进行限制的客户端会耗尽服务端的资源，从而使其他客户端无法访问服务，甚至有些客户端会一直发起请求，直到使服务完全不可用。\n对 API 的使用进行约束的常用方法是启用速率限制。与基于 IP 的速率限制或者 web 框架提供的应用级别速率限制不同，Envoy 允许在 HTTP 层实现快速，高性能和可靠的全局速率限制。\n上图中左侧的 Service Client 代表使用率特别高的客户端。在运行期间，它可以使负载均衡后端的所有服务实例流量饱和，并使其他更高优先级的客户端丢弃其请求。\nEnvoy 能够对网络级别的任何服务进行速率限制，而无需对应用程序进行任何修改。此外，由于 Envoy 工作在 7 层，也就是应用程序级别，所以它可以检查 HTTP 速率信息并对其进行速率限制。\n在本教程中， vegata 负载测试工具用于模拟上述示例中的批处理作业。下图显示了请求速率大约为 500次/秒 的稳定状态。\n译者注：首先克隆 grokking-go 项目。\n$ make load-test echo \u0026#34;GET http://localhost:8080/slow\u0026#34; | vegeta attack -rate=500 -duration=0 | tee results.bin | vegeta report 在模拟后台作业期间，对 API 资源 /slow 的访问速率达到了每秒 3500 个请求，影响到了其他端点和客户端。\n为了解决这个问题，下面的解决方案将使用 Envoy 强制限制请求速率为 500个请求/秒。但首先\u0026hellip;\nEnvoy 是什么？ # Envoy 是一个轻量级代理服务器，能够处理任何 TCP/IP/HTTP/GRPC/HTTP2 等协议的连接。它具有高度可配置性，并支持许多不同的插件。它还使可观察性成为一等公民。\n在 Envoy 横空出世之前，应用程序级别的重试、延迟注入、速率限制和熔断都要通过应用程序本身的代码逻辑来实现。Envoy 将这些功能从应用程序中剥离出来，并让运维管理人员能够配置和启用这些功能，无需对应用程序本身作任何修改。\nEnvoy 的 官方文档 和 Matt Klein 的文章提供了一个比我更好的对 Envoy 的介绍：\nEnvoy 是一款由 Lyft 开源的，使用 C++ 编写的高性能分布式代理，专为单体服务和应用而设计。它也被作为大型微服务框架 Istio service mesh 的通信总线和通用数据平面。通过借鉴 NGINX、HAProxy、硬件负载均衡器和云负载均衡器等解决方案，Envoy 作为一个独立的进程与应用程序一起运行，并通过与平台无关的方式提供一些高级特性，从而形成一个对应用透明的通信网格。当基础设施中的所有服务流量通过 Envoy 网格流动时，通过一致的可观察性，调整整体性能和添加更多底层特性，一旦发生网络和应用程序故障，能够很容易定位出问题的根源。\n解决方案 # 所有代码和示例都可以在 GitHub 上找到。\n下面给出具体的解决方案：\n将 Envoy 配置为 API 负载均衡器的前端代理；仍然允许所有流量通过。 配置并运行全局速率限制服务。 配置 Envoy 使用全局速率限制服务。 我们需要一种方法来限制同一时间发出的请求数量，以便将 API 负载均衡器与请求达到高峰的客户端隔离，并确保其他客户端在执行这些批处理作业（通过 vegeta 来模拟）期间可以继续访问 API。为了达到这个目的，我们将 Envoy 代理和批处理客户端 vegeta 部署在同一台机器上。\n通过将 Envoy 作为 Sidecar 与批处理客户端一起运行，在请求达到负载均衡之前就可以对请求进行速率限制。使用 Envoy 是一个很明智的选择，因为它具有高度可配置性，高性能，并且可以很好地处理 HTTP 请求之间的平衡。\n将 Envoy 配置为 API 负载均衡器的前端代理 # 第一步是将 Envoy 配置为处于批处理作业客户端和 API 负载均衡器之间，客户端向 API 发起的所有请求都会首先经过 Envoy 的处理。首先需要让 Envoy 知道如何连接 API，然后再更新批处理作业的配置，使该客户端向 Envoy 发出请起，而不是直接向 API 发出请求。配置完之后的最终状态如下图所示：\n此步骤仅通过 Envoy 来对 API 流量进行路由，尚未对应用进行速率限制。为了达到限速的目的，我们还需要做一些额外的配置：\ncluster # Cluster 表示 Envoy 连接到的一组逻辑上相似的上游主机（在本示例中表示 API 负载均衡器）。Cluster 的配置非常简单：\nclusters: - name: api connect_timeout: 1s type: strict_dns lb_policy: round_robin hosts: - socket_address: address: localhost port_value: 8080 在本示例中，我们运行了一个监听在 localhost:8080 上的 fakapi 来模拟上图中的负载均衡器。通过 Envoy 向 API 发出的任何请求都会被发送到 localhost:8080。\nvirtual_host # virtual_host 部分的配置用来确保所有请求都会路由到上面定义的 API 集群。\n- name: api domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: api 其余的配置文件用来确定 Envoy 本身监听在哪个地址以及 Envoy 与其他服务之间的连接规则。\nstatic_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 10000} filter_chains: - filters: - name: envoy.http_connection_manager config: stat_prefix: ingress_http codec_type: AUTO route_config: name: remote_api virtual_hosts: - name: api domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: api http_filters: - name: envoy.router clusters: - name: api connect_timeout: 1s type: strict_dns lb_policy: round_robin hosts: - socket_address: address: localhost port_value: 8080 admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 9901 更新负载测试工具的参数，直接访问本地的 Envoy 代理，通过仪表板可以观察到 Envoy 正在接收流量。下图的 Envoy 仪表板来自 Grafana 官方仪表板仓库（ Lyft 也提供了一份 Envoy 仪表板）。\n$ make load-test LOAD_TEST_TARGET=http://localhost:10000 LOAD_TEST_RATE=500 echo \u0026#34;GET http://localhost:10000/slow\u0026#34; | vegeta attack -rate=500 -duration=0 | tee results.bin | vegeta report 上图显示 Envoy 现在正在接收客户端发送给 API 的所有请求，并将它们发送到上游的负载均衡器！\n配置并运行全局速率限制服务 # 此步骤将配置运行 Lyft 开源的全局 速率限制 服务。运行该服务非常简单，只需要克隆它的代码仓库，修改一部分配置文件，然后通过 docker-compose 启动就行了。\n首先克隆 Ratelimit 代码仓库并修改配置文件，更新 domain 字段以及 descriptor 字段的 key 和 value：\n$ cat examples/ratelimit/config/config.yaml --- domain: apis descriptors: - key: generic_key value: default rate_limit: unit: second requests_per_unit: 500 接下来使用docker-compose 的配置文件（docker-compose.yml）来启动全局速率限制服务（详细步骤请参考 README）：\n$ docker-compose down \u0026amp;\u0026amp; docker-compose up 配置 Envoy 使用全局速率限制服务 # 最后一步是配置 Envoy 使用全局速率限制服务，以强制执行速率限制并降低对 API 的请求速率。配置生效后，Envoy 将会检查每个传入连接的速率限制，并根据上面的配置过滤掉一部分请求（限制最多 500 个请求/秒）。\n开启了速率限制的 Envoy 配置文件如下所示：\nstatic_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 10000} filter_chains: - filters: - name: envoy.http_connection_manager config: use_remote_address: true stat_prefix: ingress_http codec_type: AUTO route_config: name: remote_api virtual_hosts: - name: api domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: api rate_limits: - stage: 0 actions: - {generic_key: {descriptor_value: \u0026#34;default\u0026#34;}} http_filters: - name: envoy.rate_limit config: domain: apis stage: 0 - name: envoy.router clusters: - name: api connect_timeout: 1s type: strict_dns lb_policy: round_robin hosts: - socket_address: address: localhost port_value: 8080 - name: rate_limit_cluster type: strict_dns connect_timeout: 0.25s lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: localhost port_value: 8081 rate_limit_service: grpc_service: envoy_grpc: cluster_name: rate_limit_cluster timeout: 0.25s admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 9901 然后，我们以 1000个请求/秒 的速率（速率限制的2倍）运行负载测试工具：\n$ make load-test LOAD_TEST_TARGET=http://localhost:10000 LOAD_TEST_RATE=1000 echo \u0026#34;GET http://localhost:10000/slow\u0026#34; | vegeta attack -rate=1000 -duration=0 | tee results.bin | vegeta report 可以查看一下 ratelimiter 服务的日志，日志中显示了它接收的请求和它进行速率限制检查的过程：\nmsg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 35\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 34\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 33\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 31\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 32\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 42\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;starting get limit lookup\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;cache key: apis_generic_key_default_1540829538 current: 46\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;looking up key: generic_key_default\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;looking up key: generic_key_default\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;looking up key: generic_key_default\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;looking up key: generic_key_default\u0026#34; ratelimit_1 | time=\u0026#34;2018-10-29T16:12:18Z\u0026#34; level=debug msg=\u0026#34;looking up key: generic_key_default\u0026#34; 如果速率限制功能无法生效，可以参考该 issue 中的讨论。\n运行一段时间后，停止负载测试打印出测试报告，可以看到其中 1/2 的请求被 Envoy 限制了，被限制的请求的状态码为 429 ！！！\n$ make load-test LOAD_TEST_TARGET=http://localhost:10000 LOAD_TEST_RATE=1000 echo \u0026#34;GET http://localhost:10000/slow\u0026#34; | vegeta attack -rate=1000 -duration=0 | tee results.bin | vegeta report Requests [total, rate] 128093, 1000.02 Duration [total, attack, wait] 2m8.102168403s, 2m8.090470728s, 11.697675ms Latencies [mean, 50, 95, 99, max] 10.294365ms, 11.553135ms, 33.428287ms, 52.678127ms, 177.709494ms Bytes In [total, mean] 1207354, 9.43 Bytes Out [total, mean] 0, 0.00 Success [ratio] 52.69% Status Codes [code:count] 200:67494 429:60599 Error Set: 429 Too Many Requests 通过 Envoy 暴露的速率限制指标（envoy_cluster_ratelimit_over_limit）或（4xx 响应）的速率来绘制仪表板，可以看到相应的可视化图表：\n通过可视化 API 服务实际看到的请求数量，可以证明请求速率在 500个请求/秒 上下波动，这正是我们所期望的！\n再查看一下 Envoy 传出的 API 连接，可以看到传出请求速率也在 500个请求/秒 上下波动！\n实验成功！\n总结 # 希望通过本文的讲解能让你明白配置 Envoy 以减轻贪婪客户端对 API 资源的消耗是多么简单。我发现这种模式非常有用，因为弹性能力是为应用开发更多功能的基础。在 Envoy 横空出世之前，应用程序级别的重试、延迟注入、速率限制和熔断都要通过应用程序本身的代码逻辑来实现。Envoy 将这些功能从应用程序中剥离出来，并让运维管理人员能够配置和启用这些功能，无需对应用程序本身作任何修改。Envoy 完全颠覆了我们对服务弹性能力的认知，希望你读这篇文章时能和我写这篇文章时一样兴奋！\n参考资料 # https://www.datawire.io/envoyproxy/getting-started-lyft-envoy-microservices-resilience/ https://www.envoyproxy.io/docs/envoy/latest/ https://blog.turbinelabs.io/deploying-envoy-as-a-front-proxy-5b7e0a453f65 http://blog.christianposta.com/microservices/00-microservices-patterns-with-envoy-proxy-series/ https://blog.envoyproxy.io/introduction-to-modern-network-load-balancing-and-proxying-a57f6ff80236 https://eng.lyft.com/announcing-ratelimit-c2e8f3182555 https://github.com/dm03514/grokking-go/compare/blog/bolt-on-rate-limits?expand=1 ","date":"2018年11月1日","externalUrl":null,"permalink":"/posts/sre-resiliency-bolt-on-sidecar-rate-limiting-with-envoy-sidecar/","section":"博客","summary":"原文链接： SRE: Resiliency: Bolt on Rate Limiting using Envoy 速率限制是缓解级联故障和防止耗尽共","title":"Envoy 基础教程：对应用进行速率限制","type":"posts"},{"content":" 原文链接： Extending the Envoy Admin Interface\nEnvoy 是一个动态可配置的高性能现代化代理工具，现在几乎所有的 IT 潮男都用它来构建服务网格。Envoy 有许多吸引人的功能，其中包括对网络流量的高级可观察性。Envoy 可以通过好几种方式来暴露数据，其中最主要的是 stats 和 tracing：stats 由内置的 statsd 模块提供，方便集成诸如 prometheus 等监控方案。开启了 tracing 可以方便集成 Open Tracing 系统，追踪请求。然而 Envoy 管理界面本身却很少被提及到。\n最近，我看到某些公司在讨论将由 Haproxy 驱动的数据平面替换为 Envoy。如果你以前使用过 Haproxy，应该熟悉 Haproxy 的管理界面 UI（稍微有点过时了），它会暴露出后端服务列表、健康状态、活动状态和每个服务的统计信息。\n每当添加新的后端服务或修改 ACL 时，如果出现了故障，我就会用此管理界面 UI 来调试网络。例如，在任何给定时间，很容易确定集群中的单个故障后端，以及哪些后端服务运行状况不佳。\nEnvoy 管理界面也提供了很多功能，其中 /clusters 和 /config_dump 端点提供了大量有用的信息（例如， /clusters 端点中的所有动态配置的 Cluster 和 Endpoint，以及 /config_dump 端点中的所有其他当前状态的配置，稍后我会详细介绍）。\n不幸的是，在我看来，由于某些原因，Envoy 的管理界面做得并不是很友好，但我们可以做一些改进来使管理 Envoy 更容易，对用户更加友好。\n首先，它并不像 Haproxy 的管理界面那样直观，特别是当你想在某些事件中快速找到一些有用信息时（比如回答我上面提到的各种问题）。\n其次，通过此 UI 可以执行某些潜在的危险操作（例如，停止正在运行的 Envoy 进程）。对于管理员或者某些团队而言，这可能不是什么大问题，但对服务使用者而言，需要以更安全的只读模式来使用此 UI（该模式仅公开一部分功能），而 Envoy 目前是不支持的。理想情况是：我想让服务所有者能够看到 /clusters 和 /config_dump 的输出，但没有任何潜在的危险行为。这个问题已经作为一个 open issue 在 Github 中进行讨论，同时 官方文档 中也提到了这一点。我们将遵循 Matt Klein 在此 comment 中提出的有关使用真正的 Listener 来增强管理界面的建议。\n最后，我们希望将 Listener 的配置嵌入到 Envoy 的配置文件中，以便工程师可以通过一致的工作流程来处理与 Envoy 相关的配置。\n还好我们是软件工程师，这些都不是问题！我们拥有所需的所有原始数据，可以自己构建一个简单的 UI！\n你需要做的第一件事就是开启 Envoy 的管理界面。你只需要将这部分配置放在 Envoy 配置文件中以设置管理员访问日志路径和运行管理服务的端口：\nadmin: access_log_path: /var/log/envoy/envoy_admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 5000 } 启动 Envoy 进程后，你就可以在浏览器中通过 URL \u0026lt;public_ip\u0026gt;:5000 访问 Envoy 的管理界面了。\n现在有一个小问题——你会注意到，如果你尝试构建一个从远程 Envoy 实例（\u0026lt;public_ip\u0026gt;:5000）中提取数据的网站，你将看到浏览器（不禁用任何安全设置）不会允许我们获取数据，因为远程 Envoy 实例的端点不包含正确的 CORS（跨域资源共享） 响应头（如文档中所述，以防止 CSRF 攻击）。\n正如 Matt Klein 在上面的 Github comment 中所建议的一样，我们可以设置一个包含正确 CORS 响应头的代理，并让我们的网站从该代理获取数据。\n这个很简单，只需要使用单个 VirtualHost 配置一个 Listener 和 Cluster，该 VirtualHost 代理本地 5000 端口或运行管理服务的任何其他端口。我们只需要确保该代理包含正确的 CORS 响应头——对于我上面提到的情况，只需要配置响应头 Access-Control-Allow-Origin。\n由于我们一直在使用 java 控制平面库 编写一个控制平面，我们决定动态生成这些对象以防以后想要更改或更新它们，但通过静态配置文件也很容易配置。请参考以下需要包含在引导程序文件中以实现此代理的静态资源配置示例：\nstatic_resources: clusters: - name: local_admin_cluster connect_timeout: 30s type: STATIC lb_policy: ROUND_ROBIN hosts: [{ socket_address: { address: 0.0.0.0, port_value: 5000 }}] listeners: - name: local_admin_listener address: socket_address: { address: 0.0.0.0, port_value: 5001 } filter_chains: - filters: - name: envoy.http_connection_manager config: stat_prefix: ingress_http codec_type: AUTO route_config: name: local_admin_route virtual_hosts: - name: local_service domains: [\u0026#34;*\u0026#34;] response_headers_to_add: - header: key: \u0026#34;Access-Control-Allow-Origin\u0026#34; value: \u0026#34;*\u0026#34; routes: - match: { path: \u0026#34;/clusters\u0026#34; } route: { cluster: local_admin } - match: { path: \u0026#34;/config_dump\u0026#34; } route: { cluster: local_admin } http_filters: - name: envoy.router 如你所见，我已经确保路由仅匹配 /clusters 和 /config_dump，这样就不会造成潜在的危险。此外，由于我们已经将其作为我们的控制平面驱动的动态配置文件实现，因此我们可以根据是开发环境还是生产环境来更改这些路径的匹配方式（比如在开发环境中可以公开管理界面的所有端点，但在生产环境中不能这么做）。\n注意，因为我们使用的是 Listener，所以我们应该使用 HTTPs，我会将其作为读者的练习。 前面我没有提到如何使用 /config_dump 端点导的数据（Haproxy 的 UI 可以通过 /clusters 端点的信息来构建等效的数据），因为我不太清楚我想要构建什么。由于 Envoy 的配置实在是太复杂了（即使不使用动态控制平面，只使用静态配置文件），以至于其他使用者很难确定每个 Envoy 实例的配置。/config_dump 的输出可能非常冗长，特别是如果有非常多复杂的 Cluster、Listener 和 Route。我认为这将是一个很好的用例，可以用来演示一些展示当前流量分割、流量镜像、Cluster 子集和权重的可视化。\n此外，当我们在控制平面中开发更多功能时，为手动测试提供一些视觉反馈并向团队的其他人展示新功能将会起到很棒的效果。\n我认为我们真的只是在获得对网络的深入见解时找到了冰山一角，我很高兴看到我们通过扩展 Envoy 管理界面等强大工具使我们的网络更加健壮和易懂。\n如果您对此有任何想法，请随时通过 Twitter 或通过电子邮件与我联系：\nTwitter: @mitchfriedman5\nEmail: mitchfriedman5@gmail.com\n扫一扫关注微信公众号 ","date":"2018年10月25日","externalUrl":null,"permalink":"/posts/extending-the-envoy-admin-interface/","section":"博客","summary":"原文链接： Extending the Envoy Admin Interface Envoy 是一个动态可配置的高性能现代化代理工具","title":"Envoy 基础教程：扩展 Envoy 的管理界面","type":"posts"},{"content":" 原文链接： Istio, mTLS, debugging a 503 error\n大家好，本文我将与你们分享我在 Istio 官方文档中尝试 熔断教程时遇到的问题。我会记录下解决此问题的所有步骤，希望对你们有所帮助。至少对我自己来说，在整个排错过程中学到了很多关于 Istio 的知识。\n我的实践步骤非常简单，总共分为两步：\n部署两个应用（一个 httpbin 示例应用 + 一个带有命令行工具 curl 的客户端） 创建一个 目标规则以限制对 httpbin 服务的调用（熔断） 是不是非常简单？让我们开始吧！\n首先安装 httpbin 服务和客户端：\n$ kubectl create ns foo $ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/httpbin/httpbin.yaml) -n foo $ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/sleep/sleep.yaml) -n foo $ kubectl -n foo get pod,svc NAME READY STATUS RESTARTS AGE pod/httpbin-6bbb775889-wcp45 2/2 Running 0 35s pod/sleep-5b597748b4-77kj5 2/2 Running 0 35s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/httpbin ClusterIP 10.105.25.98 8000/TCP 36s service/sleep ClusterIP 10.111.0.72 80/TCP 35s 接下来就登入客户端 Pod 并使用 curl 来调用 httpbin：\n$ kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl http://httpbin:8000/get { \u0026#34;args\u0026#34;: {}, \u0026#34;headers\u0026#34;: { \u0026#34;Accept\u0026#34;: \u0026#34;*/*\u0026#34;, \u0026#34;Content-Length\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;Host\u0026#34;: \u0026#34;httpbin:8000\u0026#34;, \u0026#34;User-Agent\u0026#34;: \u0026#34;curl/7.35.0\u0026#34;, \u0026#34;X-B3-Sampled\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;X-B3-Spanid\u0026#34;: \u0026#34;b5d006d3d9bf1f4d\u0026#34;, \u0026#34;X-B3-Traceid\u0026#34;: \u0026#34;b5d006d3d9bf1f4d\u0026#34;, \u0026#34;X-Request-Id\u0026#34;: \u0026#34;970b84b2-999b-990c-91b4-b6c8d2534e77\u0026#34; }, \u0026#34;origin\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;http://httpbin:8000/get\u0026#34; } 到目前为止一切正常。下面创建一个目标规则针对 httpbin 服务设置断路器：\n$ cat \u0026lt;\u0026lt;EOF | kubectl -n foo apply -f - apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: httpbin spec: host: httpbin trafficPolicy: connectionPool: tcp: maxConnections: 1 http: http1MaxPendingRequests: 1 maxRequestsPerConnection: 1 outlierDetection: consecutiveErrors: 1 interval: 1s baseEjectionTime: 3m maxEjectionPercent: 100 EOF 现在尝试再次调用 httpbin 服务：\n$ kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl http://httpbin:8000/get upstream connect error or disconnect/reset before headers 哎呀出事了！我们可以让 curl 输出更加详细的信息：\n$ kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl -v http://httpbin:8000/get * Hostname was NOT found in DNS cache * Trying 10.105.235.142... * Connected to httpbin (10.105.235.142) port 8000 (#0) \u0026gt; GET /get HTTP/1.1 \u0026gt; User-Agent: curl/7.35.0 \u0026gt; Host: httpbin:8000 \u0026gt; Accept: */* \u0026gt; \u0026lt; HTTP/1.1 503 Service Unavailable \u0026lt; content-length: 57 \u0026lt; content-type: text/plain \u0026lt; date: Tue, 28 Aug 2018 12:26:54 GMT * Server envoy is not blacklisted \u0026lt; server: envoy \u0026lt; * Connection #0 to host httpbin left intact upstream connect error or disconnect/reset before headers 发现了 503 错误。。。为什么呢？根据刚刚创建的 DestinationRule，应该可以成功调用 httpbin 服务的。因为我们将 TCP 连接的最大数量设置为 1，而 curl 命令只生成了一个连接。那么到底哪里出问题了呢？\n我能想到的第一件事就是通过查询 istio-proxy 的状态来验证熔断策略是否生效：\n$ kubectl -n foo exec -it -c istio-proxy sleep-5b597748b4-77kj5 -- curl localhost:15000/stats | grep httpbin | grep pending cluster.outbound|8000||httpbin.foo.svc.cluster.local.upstream_rq_pending_active: 0 cluster.outbound|8000||httpbin.foo.svc.cluster.local.upstream_rq_pending_failure_eject: 0 cluster.outbound|8000||httpbin.foo.svc.cluster.local.upstream_rq_pending_overflow: 0 cluster.outbound|8000||httpbin.foo.svc.cluster.local.upstream_rq_pending_total: 5 upstream_rq_pending_overflow 的值是 0，说明没有任何调用被标记为熔断。\nIstio sidecar（名为 istio-proxy 的 Envoy 容器）暴露出 15000 端口以提供一些实用的功能，可以通过 HTTP 访问这个端口，例如打印相关服务的一些统计信息。\n因此，在上面的的命令中，我们在客户端 Pod（sleep-5b597748b4-77kj5）的 sidecar 容器（-c istio-proxy）中执行 curl（curl localhost:15000/stats），过滤出我们要检查的服务的统计信息（| grep httpbin），然后过滤出熔断器挂起状态（| grep pending）。\n为了确认 DestinationRule 才是罪魁祸首，我决定将它删除然后再尝试调用：\n$ kubectl -n foo delete DestinationRule httpbin destinationrule.networking.istio.io \u0026#34;httpbin\u0026#34; deleted $ kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl -v http://httpbin:8000/get ... \u0026lt; HTTP/1.1 200 OK ... 再将该 DestinationRule 加回来，然后再次尝试调用：\n... \u0026lt; HTTP/1.1 503 Service Unavailable ... 看来问题确实出在 DestinationRule 这里，但是还是不知道为什么，我们需要进一步研究。我灵机一动，要不先来看看 Envoy（istio-proxy sidecar）的日志吧：\n$ kubectl -n foo logs -f sleep-5b597748b4-77kj5 -c istio-proxy # 在另一个终端执行以下命令 (kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl -v http://httpbin:8000/get) # 然后会输出下面的日志： [2018-08-28T13:06:56.454Z] \u0026#34;GET /get HTTP/1.1\u0026#34; 503 UC 0 57 0 - \u0026#34;-\u0026#34; \u0026#34;curl/7.35.0\u0026#34; \u0026#34;19095d07-320a-9be0-8ba5-e0d08cf58f52\u0026#34; \u0026#34;httpbin:8000\u0026#34; \u0026#34;172.17.0.14:8000\u0026#34; 并没有看到什么有用的信息。日志告诉我们 Envoy 从服务器收到了 503 错误，OK，那我们就来检查一下服务器端（httpbin）的日志：\n$ kubectl -n foo logs -f httpbin-94fdb8c79-h9zrq -c istio-proxy # 在另一个终端执行以下命令 (kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl -v http://httpbin:8000/get) # 日志输出为空 什么？日志输出中竟然没有任何内容，就好像请求根本没有到达服务器一样。那么现在该怎么办呢，可不可以增加日志输出等级？也许请求已经收到了，只是没有被输出而已。\n还记得我上面讲过的 Envoy 暴露了 15000 端口作为管理接口吗？我们可以用它来获取统计数据。看看它都提供了哪些功能：\n$ kubectl -n foo exec -it -c istio-proxy httpbin-94fdb8c79-h9zrq -- curl http://localhost:15000/help admin commands are: /: Admin home page /certs: print certs on machine ... /logging: query/change logging levels ... 嘿嘿，似乎找到了我们需要的东西：/logging，试试吧：\n$ kubectl -n foo exec -it -c istio-proxy httpbin-94fdb8c79-h9zrq -- curl http://localhost:15000/logging?level=trace active loggers: admin: trace ... 上面的命令将服务器 Envoy 的日志等级设为 trace，该日志等级输出的日志信息最详细。关于管理接口的更多信息，请查看 Envoy 官方文档。现在我们再来重新查看服务器 Envoy 的日志，希望能够得到一些有用的信息：\n$ kubectl -n foo logs -f httpbin-94fdb8c79-h9zrq -c istio-proxy # 在另一个终端执行以下命令 (kubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl -v http://httpbin:8000/get) # 然后会输出下面的日志：（我过滤了一些不相关的内容） [debug][filter] external/envoy/source/extensions/filters/listener/original_dst/original_dst.cc:18] original_dst: New connection accepted [debug][main] external/envoy/source/server/connection_handler_impl.cc:217] [C31] new connection [trace][connection] external/envoy/source/common/network/connection_impl.cc:389] [C31] socket event: 2 [trace][connection] external/envoy/source/common/network/connection_impl.cc:457] [C31] write ready [debug][connection] external/envoy/source/common/ssl/ssl_socket.cc:111] [C31] handshake error: 2 [trace][connection] external/envoy/source/common/network/connection_impl.cc:389] [C31] socket event: 3 [trace][connection] external/envoy/source/common/network/connection_impl.cc:457] [C31] write ready [debug][connection] external/envoy/source/common/ssl/ssl_socket.cc:111] [C31] handshake error: 1 [debug][connection] external/envoy/source/common/ssl/ssl_socket.cc:139] [C31] SSL error: 268435612:SSL routines:OPENSSL_internal:HTTP_REQUEST [debug][connection] external/envoy/source/common/network/connection_impl.cc:133] [C31] closing socket: 0 现在我们可以看到请求确实已经到达服务器了，但由于握手错误导致了请求失败，并且 Envoy 正在关闭连接。现在的问题是 : 为什么会发生握手错误？为什么会涉及到 SSL？\n当在 Istio 中谈到 SSL 时，一般指的是双向 TLS。然后我就去查看 Istio 官方文档，视图找到与我的问题相关的内容，最后终于在 基础认证策略 这篇文档中找到了我想要的东西。\n我发现我在部署 Istio 时启用了 Sidecar 之间的双向 TLS 认证！\n检查一下：\n$ kubectl get MeshPolicy default -o yaml apiVersion: authentication.istio.io/v1alpha1 kind: MeshPolicy metadata: ... spec: peers: - mtls: {} $ kubectl -n istio-system get DestinationRule default -o yaml apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: ... spec: host: \u0026#39;*.local\u0026#39; trafficPolicy: tls: mode: ISTIO_MUTUAL 上面这些输出表明集群中开启了双向 TLS 认证，因为这些全局身份验证策略和目标规则只有在开启双向 TLS 认证时才会存在。\n再回到最初的问题：为什么调用 httpbin 服务会失败？现在我们已经知道了网格中开启了双向 TLS 认证，通过阅读文档可以推断出服务器端仅接受使用 TLS 的加密请求，而客户端仍在使用明文请求。现在来重新修改一个问题 :** 为什么客户端（sleep pod）会使用明文来请求服务器端（httpbin pod）？**\n再次仔细阅读官方文档可以找到答案。双向 TLS 认证（mTLS）在 Istio 中的工作方式很简单：它会创建一个默认的 DestinationRule 对象（名称为 default），它表示网格中的所有客户端都使用双向 TLS。但是当我们为了实现熔断策略创建自己的 DestinationRule 时，用自己的配置（根本就没有设置 TLS！）覆盖了默认配置。\n这是 基础认证策略 文档中的原文：\n除了认证场合之外，目标规则还有其它方面的应用，例如金丝雀部署。但是所有的目标规则都适用相同的优先顺序。因此，如果一个服务需要配置其它目标规则（例如配置负载均衡），那么新规则定义中必须包含类似的 TLS 块来定义 ISTIO_MUTUAL 模式，否则它将覆盖网格或命名空间范围的 TLS 设置并禁用 TLS。 现在知道问题出在哪了，解决办法就是：修改 DestinationRule 以包含 TLS 配置项：\ncat \u0026lt;\u0026lt;EOF | kubectl -n foo apply -f - apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: httpbin spec: host: httpbin trafficPolicy: connectionPool: tcp: maxConnections: 1 http: http1MaxPendingRequests: 1 maxRequestsPerConnection: 1 outlierDetection: consecutiveErrors: 1 interval: 1s baseEjectionTime: 3m maxEjectionPercent: 100 tls: mode: ISTIO_MUTUAL EOF 再次尝试调用 httpbin 服务：\nkubectl -n foo exec -it -c sleep sleep-5b597748b4-77kj5 -- curl -v http://httpbin:8000/get ... \u0026lt; HTTP/1.1 200 OK ... 现在我可以继续实验熔断教程了！\n总结 :\n确认是否启用了 mTLS，如果启用了可能会遇到很多错误。😊 所有的目标规则都适用相同的优先顺序，具体的规则会覆盖全局的规则。 有时候可以充分利用 Sidecar 的管理接口（本地端口 15000）。 仔细阅读官方文档。😊 ","date":"2018年10月11日","externalUrl":null,"permalink":"/posts/istio-mtls-debugging-a-503-error/","section":"博客","summary":"原文链接： Istio, mTLS, debugging a 503 error 大家好，本文我将与你们分享我在 Istio 官方文","title":"在 Istio 中调试 503 错误","type":"posts"},{"content":" 原文链接： xDS REST and gRPC protocol\nEnvoy 通过查询文件或管理服务器来动态发现资源。这些发现服务及其相应的 API 被统称为 xDS。Envoy 通过订阅（subscription）方式来获取资源，如监控指定路径下的文件、启动 gRPC 流（streaming）或轮询 REST-JSON URL。后两种方式会发送 DiscoveryRequest 请求消息，发现的对应资源则包含在响应消息 DiscoveryResponse 中。下面，我们将具体讨论每种订阅类型。\n文件订阅 # 发现动态资源的最简单方式就是将其保存于文件，并将路径配置在 ConfigSource 中的 path 参数中。Envoy 使用 inotify（Mac OS X 上为 kqueue）来监控文件的变化，在文件被更新时，Envoy 读取保存的 DiscoveryResponse 数据进行解析，数据格式可以为二进制 protobuf、JSON、YAML 和协议文本等。\n译者注：core.ConfigSource 配置格式如下：\n{ \u0026#34;path\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;api_config_source\u0026#34;: \u0026#34;{...}\u0026#34;, \u0026#34;ads\u0026#34;: \u0026#34;{...}\u0026#34; } 文件订阅方式可提供统计数据和日志信息，但是缺少 ACK/NACK 更新的机制。如果更新的配置被拒绝，xDS API 则继续使用最后一个有效配置。\nACK 在 TCP 连接中是数据包确认消息，在 TCP 连接中，数据接收端在接收到一个数据包的时候会立即发送一个 ACK 消息给发送端，通知已经接收到此数据包，然后发送端再继续发送下一个数据包。 NACK 与 ACK 刚好相反，在 UDP 通信中，数据接收端接收到数据包后是不需要通知发送端的，发送端始终不断的发送数据包而不关心对方是否正确收到，亦不关心所发生的数据包是否有序到达。只有在接收端意识到有某个或某几个数据包没有接收到的情况下才会构造一个 NACK 消息包发送给发送端。请求发送端重发丢失包。 比如接收端收到数据包 100， 101， 103，105，然后发现 102， 104 丢了，会构造一个 NACK 包发送给发送端。 gRPC 流式订阅 # 单例资源类型发现 # 每个 xDS API 可以单独配置 ApiConfigSource，指向对应的上游管理服务器的集群地址。每个 xDS 资源类型会启动一个独立的双向 gRPC 流（每个 xDS 资源类型对应的管理服务器可能不同）。API 交付方式采用最终一致性。可以参考后续聚合服务发现（ADS） 章节来了解必要的显式控制序列。\n译者注：core.ApiConfigSource 配置格式如下：\n{ \u0026#34;api_type\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;cluster_names\u0026#34;: [], \u0026#34;grpc_services\u0026#34;: [], \u0026#34;refresh_delay\u0026#34;: \u0026#34;{...}\u0026#34;, \u0026#34;request_timeout\u0026#34;: \u0026#34;{...}\u0026#34; } 类型 URL # 每个 xDS API 都与给定的资源类型一一对应。关系如下：\nLDS ： envoy.api.v2.Listener RDS : envoy.api.v2.RouteConfiguration CDS : envoy.api.v2.Cluster EDS ： envoy.api.v2.ClusterLoadAssignment SDS ：envoy.api.v2.Auth.Secret 类型 URL 的概念如下所示，其采用 type.googleapis.com/\u0026lt;resource type\u0026gt; 的形式，例如 CDS 对应于 type.googleapis.com/envoy.api.v2.Cluster。在 Envoy 发起的发现请求和管理服务器返回的发现响应中，都包括了资源类型 URL。\nACK/NACK 和版本 # 每个 Envoy 流以发送一个 DiscoveryRequest 开始，包括了列表订阅的资源、订阅资源对应的类型 URL、节点标识符和空的 version_info。EDS 请求示例如下：\nversion_info: node: { id: envoy } resource_names: - foo - bar type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment response_nonce: 管理服务器可立刻或等待资源就绪时发送 DiscoveryResponse 作为响应，示例如下：\nversion_info: X resources: - foo ClusterLoadAssignment proto encoding - bar ClusterLoadAssignment proto encoding type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment nonce: A Envoy 在处理 DiscoveryResponse 响应后，将通过流发送一个新的请求，请求包含应用成功的最后一个版本号和管理服务器提供的 nonce。如果本次更新已成功应用，则 version_info 的值设置为 X，如下序列图所示：\nack 更新 在此序列图及后续章节中，将统一使用以下缩写格式：\nDiscoveryRequest ：(V=version_info，R=resource_names，N=response_nonce，T=type_url) DiscoveryResponse ： (V=version_info，R=resources，N=nonce，T=type_url) 在信息安全中，Nonce 是一个在加密通信只能使用一次的数字。在认证协议中，它往往是一个随机或伪随机数，以避免重放攻击。Nonce 也用于流密码以确保安全。如果需要使用相同的密钥加密一个以上的消息，就需要 Nonce 来确保不同的消息与该密钥加密的密钥流不同。（引用自维基百科）在本文中 nonce 是每次更新的数据包的唯一标识。 有了版本（version_info）这个概念，就可以为 Envoy 和管理服务器共享当前应用配置，以及提供了通过 ACK/NACK 来进行配置更新的机制。如果 Envoy 拒绝了配置更新 X，则回复 error_detail 及前一个版本号，在本例中为空的初始版本号，error_detail 包含了有关错误的更加详细的信息：\nnack 更新 重新发送 DiscoveryRequest 后，API 更新可能会在新版本 Y 上成功应用：\n每个流都有自己的版本概念，但不同的资源类型不能共享资源版本。在不使用 ADS 的情况下，每个资源类型可能具有不同的版本，因为 Envoy API 允许不同的 EDS/RDS 资源配置指向不同的 ConfigSources。\n何时发送更新 # 管理服务器应该只向 Envoy 客户端发送上次 DiscoveryResponse 后更新过的资源。Envoy 则会根据接受或拒绝 DiscoveryResponse 的情况，立即回复包含 ACK/NACK 的 DiscoveryRequest 请求。如果管理服务器不等待更新完成，每次返回相同的资源结果集合，则会导致 Envoy 和管理服务器通讯效率大打折扣。\n在同一个流中，新的 DiscoveryRequests 将取代此前具有相同资源类型的 DiscoveryRequest 请求。这意味着管理服务器只需要响应给定资源类型最新的 DiscoveryRequest 请求即可。\n资源提示 # DiscoveryRequest 中的 resource_names 信息作为资源提示出现。一些资源类型，例如 Cluster 和 Listener 将使用一个空的 resource_names，因为 Envoy 需要获取对应节点标识的管理服务器的所有 Cluster（CDS）和 Listener（LDS）。对于其他资源类型，如 RouteConfigurations（RDS）和 ClusterLoadAssignments（EDS），则遵循此前的 CDS/LDS 更新，Envoy 能够通过枚举这些资源找到明确的资源。\nLDS/CDS 资源提示信息将始终为空，并且期望管理服务器的每个响应都提供 LDS/CDS 资源的完整状态。不存在的 Listener 或 Cluster 将被删除。如果 RDS 或 EDS 更新中缺少请求的资源，Envoy 将保留此资源的最后已知值。管理服务器能够从 DiscoveryRequest 中的节点标识（node.id）推断出所有所需的 EDS/RDS 资源，在这种情况下，该提示信息可能被丢弃。从 Envoy 的资源角度来看，空的 EDS/RDS DiscoveryResponse 响应实际上表示一个空的资源。\n当 Listener 或 Cluster 被删除时，其对应的 EDS 和 RDS 资源也会在 Envoy 实例中被删除。为使 EDS 资源能被 Envoy 获取或跟踪，就必须存在已经应用过的 Cluster 定义（如通过 CDS 获取）。RDS 和 Listeners 之间存在类似的关系（如通过 LDS 获取）。\n对于 EDS/RDS ，Envoy 可以为每个给定类型的资源生成不同的流（如每个 ConfigSource 都有自己的上游管理服务器集群）或当指定资源类型的请求发送到同一个管理服务器的时候，允许将多个资源请求组合在一起发送。虽然可以单个实现，但管理服务器应具备为每个请求中的给定资源类型处理一个或多个 resource_names 的能力。下面的两个序列图都可用于获取两个 EDS 资源 {foo，bar}：\n资源更新 # 如上所述，Envoy 可能会更新 DiscoveryRequest 中出现的 resource_names 列表，其中 DiscoveryRequest 是用来 ACK/NACK 管理服务器的特定的 DiscoveryResponse 。此外，Envoy 后续可能会在给定的 version_info 上发送额外的 DiscoveryRequests ，以使用新的资源提示来更新管理服务器。\n例如，如果 Envoy 在 EDS 版本 X 时仅知道集群 foo，但在随后收到的 CDS 更新时额外获取了集群 bar ，它可能会为版本 X 发出额外的 DiscoveryRequest 请求，并将 {foo，bar} 作为请求的 resource_names。\n这里可能会出现竞争状况；如果 Envoy 在版本 X 上发布了资源提示更新请求，但在管理服务器处理该请求之前发送了新的版本号为 Y 的响应，针对 version_info 为 X 的版本，资源提示更新可能会被解释为拒绝 Y 。为避免这种情况，通过使用管理服务器提供的 nonce，Envoy 可用来保证每个 DiscoveryRequest 对应到相应的 DiscoveryResponse：\n管理服务器不应该为含有过期 nonce 的 DiscoveryRequest 发送 DiscoveryResponse 响应。如果向 Envoy 发送的 DiscoveryResponse 中包含了的新 nonce，则此前的 nonce 将过期。在确定新版本可用之前，管理服务器不需要向 Envoy 发送更新。同版本的早期请求将会过期。在新版本就绪时，管理服务器可能会处理同一个版本号的多个 DiscoveryRequests 请求。\n上述资源更新序列表明 Envoy 并不能期待其发出的每个 DiscoveryRequest 都得到 DiscoveryResponse 响应。\n最终一致性考虑 # 由于 Envoy 的 xDS API 采用最终一致性，因此在更新期间可能导致流量被丢弃。例如，如果通过 CDS/EDS 仅获取到了集群 X，而且 RouteConfiguration 引用了集群 X；在 CDS/EDS 更新集群 Y 配置之前，如果将 RouteConfiguration 将引用的集群调整为 Y ，那么流量将被吸入黑洞而丢弃，直至集群 Y 被 Envoy 实例获取。\n对某些应用程序，可接受临时的流量丢弃，客户端或其他 Envoy sidecar 的重试可以解决该问题，并不影响业务逻辑。那些对流量丢弃不能容忍的场景，可以通过以下方式避免流量丢失，CDS/EDS 更新同时携带 X 和 Y ，然后发送 RDS 更新从 X 切换到 Y ，此后发送丢弃 X 的 CDS/EDS 更新。\n一般来说，为避免流量丢弃，更新的顺序应该遵循 make before break 模型，其中：\nCDS 首先更新 Cluster 数据（如果有变化） EDS 更新相应 Cluster 的 Endpoint 信息（如果有变化） LDS 更新 CDS/EDS 相应的 Listener RDS 最后更新新增 Listener 相关的 Route 配置 删除不再使用的 CDS cluster 和 EDS endpoints（不再被引用的 endpoint） 如果没有添加新的集群/路由/监听器，或者在更新期间暂时丢弃流量，则可以独立推送 xDS 更新。请注意，在 LDS 更新的情况下，监听器须在接收流量之前被预热，例如如其配置了依赖的路由，则需要先从 RDS 中获取。添加/删除/更新集群信息时，集群也需要进行预热。另一方面，如果管理平面确保路由更新时所引用的集群已经准备就绪，则路由可以不用预热。\n聚合服务发现（ADS） # 当管理服务器进行资源分发时，通过上述保证交互顺序的方式来避免流量被丢弃是一项很有挑战的工作。ADS 允许单一管理服务器通过单个 gRPC 流来提供所有的 API 更新。配合仔细规划的更新顺序，ADS 可规避更新过程中的流量丢失。使用 ADS，在单个流上可通过类型 URL 来进行复用多个独立的 DiscoveryRequest/DiscoveryResponse 序列。对于任何给定类型的 URL，以上 DiscoveryRequest 和 DiscoveryResponse 消息序列都适用。 更新序列可能如下所示：\n每个 Envoy 实例可使用单独的 ADS 流。\n最小化 ADS 配置的 bootstrap.yaml 片段示例如下：\nnode: id: \u0026lt;node identifier\u0026gt; dynamic_resources: cds_config: {ads: {}} lds_config: {ads: {}} ads_config: api_type: GRPC grpc_services: envoy_grpc: cluster_name: ads_cluster static_resources: clusters: - name: ads_cluster connect_timeout: { seconds: 5 } type: STATIC hosts: - socket_address: address: \u0026lt;ADS management server IP address\u0026gt; port_value: \u0026lt;ADS management server port\u0026gt; lb_policy: ROUND_ROBIN http2_protocol_options: {} admin: ... 增量 xDS # 增量 xDS 是可用于 ADS、CDS 和 RDS 的单独 xDS 端点，允许以下操作：\nxDS 客户端对跟踪资源列表进行增量更新。这支持 Envoy 按需/惰性地请求额外资源（例如，当与未知集群相对应的请求到达时）。 xDS 服务器可以增量更新客户端上的资源。这可以实现 xDS 资源可伸缩性的目标。管理服务器只需交付更改的单个集群，而不是在修改单个集群时交付所有上万个集群。 xDS 增量 session 始终位于 gRPC 双向流的上下文中。这允许 xDS 服务器能够跟踪到连接的 xDS 客户端的状态。xDS REST 版本（v1）不支持增量。在增量 xDS 中，nonce 字段是必需的，用于将 IncrementalDiscoveryResponse 与关联的 ACK 或 NACK IncrementalDiscoveryRequest 进行匹配。IncrementalDiscoveryResponse 中的响应消息级别（system_version_info）仅用于调试目的。\nIncrementalDiscoveryRequest 可在以下 3 种情况下发送：\nxDS 双向 gRPC 流的初始消息。 作为对先前的 IncrementalDiscoveryResponse 的 ACK 或 NACK 响应。在这种情况下，response_nonce 被设置为响应中的 nonce 值。到底是 ACK 还是 NACK 可由 error_detail 字段是否出现来区分。 客户端自发的 IncrementalDiscoveryRequest。此场景下可以从跟踪的 resource_names 集合中动态添加或删除元素。此时必须忽略 response_nonce。 在下面的示例中，客户端连接并接收它的第一个更新并 ACK。第二次更新失败，客户端发送 NACK 拒绝更新。xDS客户端后续会自发地请求 wc 相关资源。\n在下面的示例中，当 xDS 客户端断开重新连接时，支持增量的 xDS 客户端可能会告诉服务器其已经获取的资源从而避免服务端通过网络重新发送它们。\nREST-JSON 轮询订阅 # 单个 xDS API 可以通过 REST 端点进行同步（长）轮询。除了无持久流与管理服务器交互外，消息交互顺序与上述两个订阅方式相似。在任何时间点，只存在一个未完成的请求，因此响应消息中的 nonce 在 REST-JSON 中是可选的。DiscoveryRequest 和 DiscoveryResponse 的消息编码遵循 JSON 变换 proto3 规范。ADS 不支持 REST-JSON 轮询订阅。\n当轮询周期设置为较小的值时，为了进行长轮询，这时要求避免发送 DiscoveryResponse， 除非发生了对请求的资源的更改。\n","date":"2018年10月10日","externalUrl":null,"permalink":"/posts/envoy-xds-protocol/","section":"博客","summary":"原文链接： xDS REST and gRPC protocol Envoy 通过查询文件或管理服务器来动态发现资源","title":"Envoy 基础教程：xDS REST 和 gRPC 协议详解","type":"posts"},{"content":" 本文转载自 赵化冰的博客\n前言 # Istio 作为一个 service mesh 开源项目,其中最重要的功能就是对网格中微服务之间的流量进行管理,包括服务发现,请求路由和服务间的可靠通信。Istio 实现了 service mesh 的控制平面，并整合 Envoy 开源项目作为数据平面的 sidecar，一起对流量进行控制。\nIstio 体系中流量管理配置下发以及流量规则如何在数据平面生效的机制相对比较复杂，通过官方文档容易管中窥豹，难以了解其实现原理。本文尝试结合系统架构、配置文件和代码对 Istio 流量管理的架构和实现机制进行分析，以达到从整体上理解 Pilot 和 Envoy 的流量管理机制的目的。\nPilot高层架构 # Istio 控制平面中负责流量管理的组件为 Pilot，Pilot 的高层架构如下图所示：\nPilot Architecture（来自 Isio官网文档) 根据上图,Pilot 主要实现了下述功能：\n统一的服务模型 # Pilot 定义了网格中服务的标准模型，这个标准模型独立于各种底层平台。由于有了该标准模型，各个不同的平台可以通过适配器和 Pilot 对接，将自己特有的服务数据格式转换为标准格式，填充到 Pilot 的标准模型中。\n例如 Pilot 中的 Kubernetes 适配器通过 Kubernetes API 服务器得到 kubernetes 中 service 和 pod 的相关信息，然后翻译为标准模型提供给 Pilot 使用。通过适配器模式，Pilot 还可以从 Mesos, Cloud Foundry, Consul 等平台中获取服务信息，还可以开发适配器将其他提供服务发现的组件集成到 Pilot 中。\n标准数据平面 API # Pilot 使用了一套起源于 Envoy 项目的标准数据平面 API 来将服务信息和流量规则下发到数据平面的 sidecar 中。\n通过采用该标准 API，Istio 将控制平面和数据平面进行了解耦，为多种数据平面 sidecar 实现提供了可能性。事实上基于该标准 API 已经实现了多种 Sidecar 代理和 Istio 的集成，除 Istio 目前集成的 Envoy 外，还可以和 Linkerd, Nginmesh 等第三方通信代理进行集成，也可以基于该 API 自己编写 Sidecar 实现。\n控制平面和数据平面解耦是 Istio 后来居上，风头超过 Service mesh 鼻祖 Linkerd 的一招妙棋。Istio 站在了控制平面的高度上，而 Linkerd 则成为了可选的一种 sidecar 实现，可谓降维打击的一个典型成功案例！\n数据平面标准 API 也有利于生态圈的建立，开源、商业的各种 sidecar 以后可能百花齐放，用户也可以根据自己的业务场景选择不同的 sidecar 和控制平面集成，如高吞吐量的，低延迟的，高安全性的等等。有实力的大厂商可以根据该 API 定制自己的 sidecar，例如蚂蚁金服开源的 Golang 版本的 Sidecar MOSN(Modular Observable Smart Netstub)（SOFAMesh 中 Golang 版本的 Sidecar)；小厂商则可以考虑采用成熟的开源项目或者提供服务的商业 sidecar 实现。\nIstio 和 Envoy 项目联合制定了 Envoy V2 API,并采用该 API 作为 Istio 控制平面和数据平面流量管理的标准接口。\n业务 DSL 语言 # Pilot 还定义了一套 DSL（Domain Specific Language）语言，DSL 语言提供了面向业务的高层抽象，可以被运维人员理解和使用。运维人员使用该 DSL 定义流量规则并下发到 Pilot，这些规则被 Pilot 翻译成数据平面的配置，再通过标准 API 分发到 Envoy 实例，可以在运行期对微服务的流量进行控制和调整。\nPilot 的规则 DSL 是采用 K8S API Server 中的 Custom Resource (CRD) 实现的，因此和其他资源类型如 Service，Pod 和 Deployment 的创建和使用方法类似，都可以用 Kubectl 进行创建。\n通过运用不同的流量规则，可以对网格中微服务进行精细化的流量控制，如按版本分流，断路器，故障注入，灰度发布等。\nIstio 流量管理相关组件 # 我们可以通过下图了解 Istio 流量管理涉及到的相关组件。虽然该图来自 Istio Github old pilot repo, 但图中描述的组件及流程和目前 Pilot 的最新代码的架构基本是一致的。\nPilot Design Overview (来自 Istio old_pilot_repo) 图例说明：图中红色的线表示控制流，黑色的线表示数据流。蓝色部分为和Pilot相关的组件。\n从上图可以看到，Istio 中和流量管理相关的有以下组件：\n控制平面组件 # Discovery Services # 对应的 docker 镜像为 gcr.io/istio-release/pilot,进程为 pilot-discovery，该组件的功能包括：\n从 Service provider（如kubernetes或者consul）中获取服务信息 从 K8S API Server 中获取流量规则（K8S CRD Resource） 将服务信息和流量规则转化为数据平面可以理解的格式，通过标准的数据平面 API 下发到网格中的各个 sidecar 中 K8S API Server # 提供 Pilot 相关的 CRD Resource 的增、删、改、查。和 Pilot 相关的 CRD 有以下几种：\nVirtualservice : 用于定义路由规则，如根据来源或 Header 制定规则，或在不同服务版本之间分拆流量。 DestinationRule : 定义目的服务的配置策略以及可路由子集。策略包括断路器、负载均衡以及 TLS 等。 ServiceEntry : 用 ServiceEntry 可以向 Istio 中加入附加的服务条目，以使网格内可以向 Istio 服务网格之外的服务发出请求。 Gateway : 为网格配置网关，以允许一个服务可以被网格外部访问。 EnvoyFilter : 可以为 Envoy 配置过滤器。由于 Envoy 已经支持 Lua 过滤器，因此可以通过 EnvoyFilter 启用 Lua 过滤器，动态改变 Envoy 的过滤链行为。我之前一直在考虑如何才能动态扩展 Envoy 的能力，EnvoyFilter 提供了很灵活的扩展性。 数据平面组件 # 在数据平面有两个进程 Pilot-agent 和 envoy，这两个进程被放在一个 docker 容器 gcr.io/istio-release/proxyv2 中。\nPilot-agent # 该进程根据 K8S API Server 中的配置信息生成 Envoy 的配置文件，并负责启动 Envoy 进程。注意 Envoy 的大部分配置信息都是通过 xDS 接口从 Pilot 中动态获取的，因此 Agent 生成的只是用于初始化 Envoy 的少量静态配置。在后面的章节中，本文将对 Agent 生成的 Envoy 配置文件进行进一步分析。\nEnvoy # Envoy 由 Pilot-agent 进程启动，启动后，Envoy 读取 Pilot-agent 为它生成的配置文件，然后根据该文件的配置获取到 Pilot 的地址，通过数据平面标准 API 的 xDS 接口从 pilot 拉取动态配置信息，包括路由（route），监听器（listener），服务集群（cluster）和服务端点（endpoint）。Envoy 初始化完成后，就根据这些配置信息对微服务间的通信进行寻址和路由。\n命令行工具 # kubectl 和 istioctl，由于 Istio 的配置是基于 K8S 的 CRD，因此可以直接采用 kubectl 对这些资源进行操作。Istioctl 则针对 Istio 对 CRD 的操作进行了一些封装。Istioctl 支持的功能参见该 表格。\n数据平面标准 API # 前面讲到，Pilot 采用了一套标准的 API 来向数据平面 Sidecar 提供服务发现，负载均衡池和路由表等流量管理的配置信息。该标准 API 的文档参见 Envoy v2 API。 Data Plane API Protocol Buffer Definition 给出了 v2 grpc 接口相关的数据结构和接口定义。\nIstio 早期采用了 Envoy v1 API，目前的版本中则使用 V2 API，V1 已被废弃。\n基本概念和术语 # 首先我们需要了解数据平面 API 中涉及到的一些基本概念：\nHost ：能够进行网络通信的实体（如移动设备、服务器上的应用程序）。在此文档中，主机是逻辑网络应用程序。一块物理硬件上可能运行有多个主机，只要它们是可以独立寻址的。在 EDS 接口中，也使用 Endpoint 来表示一个应用实例，对应一个 IP+Port 的组合。 Downstream : 下游主机连接到 Envoy，发送请求并接收响应。 Upstream : 上游主机接收来自 Envoy 的连接和请求，并返回响应。 Listener : 监听器是命名网地址（例如，端口、unix domain socket 等)，可以被下游客户端连接。Envoy 暴露一个或者多个监听器给下游主机连接。在 Envoy 中，Listener 可以绑定到端口上直接对外服务，也可以不绑定到端口上，而是接收其他 listener 转发的请求。 Cluster : 集群是指 Envoy 连接到的逻辑上相同的一组上游主机。Envoy 通过服务发现来发现集群的成员。可以选择通过主动健康检查来确定集群成员的健康状态。Envoy 通过负载均衡策略决定将请求路由到哪个集群成员。 XDS 服务接口 # Istio 数据平面 API 定义了 xDS 服务接口，Pilot 通过该接口向数据平面 sidecar 下发动态配置信息，以对 Mesh 中的数据流量进行控制。xDS 中的 DS 表示 discovery service，即发现服务，表示 xDS 接口使用动态发现的方式提供数据平面所需的配置数据。而 x 则是一个代词，表示有多种 discover service。这些发现服务及对应的数据结构如下：\nLDS (Listener Discovery Service) : envoy.api.v2.Listener CDS (Cluster Discovery Service) : envoy.api.v2.RouteConfiguration EDS (Endpoint Discovery Service) : envoy.api.v2.Cluster RDS (Route Discovery Service) : envoy.api.v2.ClusterLoadAssignment XDS 服务接口的最终一致性考虑 # xDS 的几个接口是相互独立的，接口下发的配置数据是最终一致的。但在配置更新过程中，可能暂时出现各个接口的数据不匹配的情况，从而导致部分流量在更新过程中丢失。\n设想这种场景：在 CDS/EDS 只知道 cluster X 的情况下，RDS 的一条路由配置将指向Cluster X 的流量调整到了 Cluster Y。在 CDS/EDS 向 Mesh 中 Envoy 提供 Cluster Y 的更新前，这部分导向 Cluster Y 的流量将会因为 Envoy 不知道 Cluster Y 的信息而被丢弃。\n对于某些应用来说，短暂的部分流量丢失是可以接受的，例如客户端重试可以解决该问题，并不影响业务逻辑。对于另一些场景来说，这种情况可能无法容忍。可以通过调整 xDS 接口的更新逻辑来避免该问题，对上面的情况，可以先通过 CDS/EDS 更新 Y Cluster，然后再通过 RDS 将 X 的流量路由到Y。\n一般来说，为了避免 Envoy 配置数据更新过程中出现流量丢失的情况，xDS 接口应采用下面的顺序：\nCDS 首先更新 Cluster 数据（如果有变化） EDS 更新相应 Cluster 的 Endpoint 信息（如果有变化） LDS 更新 CDS/EDS 相应的 Listener RDS 最后更新新增 Listener 相关的 Route 配置 删除不再使用的 CDS cluster 和 EDS endpoints ADS 聚合发现服务 # 保证控制平面下发数据一致性，避免流量在配置更新过程中丢失的另一个方式是使用 ADS(Aggregated Discovery Services)，即聚合的发现服务。ADS 通过一个 gRPC 流来发布所有的配置更新，以保证各个 xDS 接口的调用顺序，避免由于 xDS 接口更新顺序导致的配置数据不一致问题。\n关于 XDS 接口的详细介绍可参考 xDS REST and gRPC protocol\nBookinfo 示例程序分析 # 下面我们以 Bookinfo 为例对 Istio 中的流量管理实现机制，以及控制平面和数据平面的交互进行进一步分析。\nBookinfo 程序结构 # 下图显示了 Bookinfo 示例程序中各个组件的 IP 地址，端口和调用关系，以用于后续的分析。\nxDS 接口调试方法 # 首先我们看看如何对 xDS 接口的相关数据进行查看和分析。Envoy v2 接口采用了 gRPC，由于 gRPC 是基于二进制的 RPC 协议，无法像 V1 的 REST 接口一样通过 curl 和浏览器进行进行分析。但我们还是可以通过 Pilot 和 Envoy 的调试接口查看 xDS 接口的相关数据。\nPilot 调试方法 # Pilot 在 9093 端口提供了下述 调试接口 下述方法查看 xDS 接口相关数据。\nPILOT=istio-pilot.istio-system:9093 # What is sent to envoy # Listeners and routes curl $PILOT/debug/adsz # Endpoints curl $PILOT/debug/edsz # Clusters curl $PILOT/debug/cdsz Envoy 调试方法 # Envoy 提供了管理接口，缺省为 localhost 的 15000 端口，可以获取 listener，cluster 以及完整的配置数据导出功能。\n$ kubectl exec productpage-v1-54b8b9f55-bx2dq -c istio-proxy curl http://127.0.0.1:15000/help /: Admin home page /certs: print certs on machine /clusters: upstream cluster status /config_dump: dump current Envoy configs (experimental) /cpuprofiler: enable/disable the CPU profiler /healthcheck/fail: cause the server to fail health checks /healthcheck/ok: cause the server to pass health checks /help: print out list of admin commands /hot_restart_version: print the hot restart compatibility version /listeners: print listener addresses /logging: query/change logging levels /quitquitquit: exit the server /reset_counters: reset all counters to zero /runtime: print runtime values /runtime_modify: modify runtime values /server_info: print server version/status information /stats: print server stats /stats/prometheus: print server stats in prometheus format 进入 productpage pod 中的 istio-proxy(Envoy) container，可以看到有下面的监听端口：\n9080 : productpage 进程对外提供的服务端口 15001 : Envoy 的入口监听器，iptable 会将 pod 的流量导入该端口中由 Envoy 进行处理 15000 : Envoy 管理端口，该端口绑定在本地环回地址上，只能在 Pod 内访问。 $ kubectl exec productpage-v1-76474f6fb7-j8fm4 -c istio-proxy -- ss -tulnp Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port tcp LISTEN 0 128 127.0.0.1:15000 *:* users:((\u0026#34;envoy\u0026#34;,pid=12,fd=9)) tcp LISTEN 0 128 *:9080 *:* tcp LISTEN 0 128 *:15001 *:* users:((\u0026#34;envoy\u0026#34;,pid=12,fd=85)) Envoy 启动过程分析 # Istio 通过 K8s 的 Admission webhook 机制实现了 sidecar 的自动注入，Mesh 中的每个微服务会被加入 Envoy 相关的容器。下面是 Productpage 微服务的 Pod 内容，可见除 productpage 之外，Istio 还在该 Pod 中注入了两个容器 gcr.io/istio-release/proxy_init 和 gcr.io/istio-release/proxyv2。\n下面 Pod description 中只保留了需要关注的内容，删除了其它不重要的部分。为方便查看，本文中后续的其它配置文件以及命令行输出也会进行类似处理。 $ kubectl describe pod productpage-v1-54b8b9f55-bx2dq Name: productpage-v1-54b8b9f55-bx2dq Namespace: default Init Containers: istio-init: Image: gcr.io/istio-release/proxy_init:1.0.0 Args: -p 15001 -u 1337 -m REDIRECT -i * -x -b 9080, -d Containers: productpage: Image: istio/examples-bookinfo-productpage-v1:1.8.0 Port: 9080/TCP istio-proxy: Image: gcr.io/istio-release/proxyv2:1.0.0 Args: proxy sidecar --configPath /etc/istio/proxy --binaryPath /usr/local/bin/envoy --serviceCluster productpage --drainDuration 45s --parentShutdownDuration 1m0s --discoveryAddress istio-pilot.istio-system:15007 --discoveryRefreshDelay 1s --zipkinAddress zipkin.istio-system:9411 --connectTimeout 10s --statsdUdpAddress istio-statsd-prom-bridge.istio-system:9125 --proxyAdminPort 15000 --controlPlaneAuthPolicy NONE Proxy_init # Productpage 的 Pod 中有一个 InitContainer proxy_init，InitContrainer 是 K8S 提供的机制，用于在 Pod 中执行一些初始化任务。在 Initialcontainer 执行完毕并退出后，才会启动 Pod 中的其它 container。\n我们看一下 proxy_init 容器中的内容：\n$ docker image inspect gcr.io/istio-release/proxy_init:1.0.0 [ { \u0026#34;RepoTags\u0026#34;: [ \u0026#34;gcr.io/istio-release/proxy_init:1.0.0\u0026#34; ], \u0026#34;ContainerConfig\u0026#34;: { \u0026#34;Env\u0026#34;: [ \u0026#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34; ], \u0026#34;Cmd\u0026#34;: [ \u0026#34;/bin/sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;#(nop) \u0026#34;, \u0026#34;ENTRYPOINT [\\\u0026#34;/usr/local/bin/istio-iptables.sh\\\u0026#34;]\u0026#34; ], \u0026#34;Entrypoint\u0026#34;: [ \u0026#34;/usr/local/bin/istio-iptables.sh\u0026#34; ], }, } ] 从上面的命令行输出可以看到，Proxy_init 中执行的命令是 istio-iptables.sh，该脚本源码较长，就不列出来了，有兴趣可以在 Istio 源码仓库的 tools/deb/istio-iptables.sh 查看。\n该脚本的作用是通过配置 iptables 来劫持 Pod 中的流量。结合前面 Pod 中该容器的命令行参数 -p 15001，可以得知 Pod 中的数据流量被 iptables 拦截，并发向 Envoy 的 15001 端口。 -u 1337 参数用于排除用户 ID 为 1337，即 Envoy 自身的流量，以避免 Iptables 把 Envoy 发出的数据又重定向到 Envoy，形成死循环。\nProxyv2 # 前面提到，该容器中有两个进程 Pilot-agent 和 envoy。我们进入容器中看看这两个进程的相关信息。\n$ kubectl exec productpage-v1-54b8b9f55-bx2dq -c istio-proxy -- ps -ef UID PID PPID C STIME TTY TIME CMD istio-p+ 1 0 0 Sep06 ? 00:00:00 /usr/local/bin/pilot-agent proxy sidecar --configPath /etc/istio/proxy --binaryPath /usr/local/bin/envoy --serviceCluster productpage --drainDuration 45s --parentShutdownDuration 1m0s --discoveryAddress istio-pilot.istio-system:15007 --discoveryRefreshDelay 1s --zipkinAddress zipkin.istio-system:9411 --connectTimeout 10s --statsdUdpAddress istio-statsd-prom-bridge.istio-system:9125 --proxyAdminPort 15000 --controlPlaneAuthPolicy NONE istio-p+ 13 1 0 Sep06 ? 00:47:37 /usr/local/bin/envoy -c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster productpage --service-node sidecar~192.168.206.23~productpage-v1-54b8b9f55-bx2dq.default~default.svc.cluster.local --max-obj-name-len 189 -l warn --v2-config-only Envoy 的大部分配置都是 dynamic resource，包括网格中服务相关的 service cluster, listener, route 规则等。这些 dynamic resource 是通过 xDS 接口从 Istio 控制平面中动态获取的。但 Envoy 如何知道 xDS server 的地址呢？这是在 Envoy 初始化配置文件中以 static resource 的方式配置的。\nEnvoy 初始配置文件 # Pilot-agent 进程根据启动参数和 K8S API Server 中的配置信息生成 Envoy 的初始配置文件，并负责启动 Envoy 进程。从 ps 命令输出可以看到 Pilot-agent 在启动 Envoy 进程时传入了 pilot 地址和 zipkin 地址，并为 Envoy 生成了一个初始化配置文件 envoy-rev0.json。\nPilot agent 生成初始化配置文件的代码： https://github.com/istio/istio/blob/release-1.0/pkg/bootstrap/bootstrap_config.go 137行\n// WriteBootstrap generates an envoy config based on config and epoch, and returns the filename. // TODO: in v2 some of the LDS ports (port, http_port) should be configured in the bootstrap. func WriteBootstrap(config *meshconfig.ProxyConfig, node string, epoch int, pilotSAN []string, opts map[string]interface{}) (string, error) { if opts == nil { opts = map[string]interface{}{} } if err := os.MkdirAll(config.ConfigPath, 0700); err != nil { return \u0026#34;\u0026#34;, err } // attempt to write file fname := configFile(config.ConfigPath, epoch) cfg := config.CustomConfigFile if cfg == \u0026#34;\u0026#34; { cfg = config.ProxyBootstrapTemplatePath } if cfg == \u0026#34;\u0026#34; { cfg = DefaultCfgDir } ...... if config.StatsdUdpAddress != \u0026#34;\u0026#34; { h, p, err = GetHostPort(\u0026#34;statsd UDP\u0026#34;, config.StatsdUdpAddress) if err != nil { return \u0026#34;\u0026#34;, err } StoreHostPort(h, p, \u0026#34;statsd\u0026#34;, opts) } fout, err := os.Create(fname) if err != nil { return \u0026#34;\u0026#34;, err } // Execute needs some sort of io.Writer err = t.Execute(fout, opts) return fname, err } 可以使用下面的命令将 productpage pod 中该文件导出来查看其中的内容：\n$ kubectl exec productpage-v1-54b8b9f55-bx2dq -c istio-proxy -- cat /etc/istio/proxy/envoy-rev0.json \u0026gt; envoy-rev0.json 配置文件的结构如图所示：\n其中各个配置节点的内容如下：\nNode\n包含了 Envoy 所在节点相关信息。\n\u0026#34;node\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;sidecar~192.168.206.23~productpage-v1-54b8b9f55-bx2dq.default~default.svc.cluster.local\u0026#34;, //用于标识 envoy 所代理的 node（在k8s中对应为Pod）上的 service cluster，来自于 Envoy 进程启动时的 service-cluster 参数 \u0026#34;cluster\u0026#34;: \u0026#34;productpage\u0026#34;, \u0026#34;metadata\u0026#34;: { \u0026#34;INTERCEPTION_MODE\u0026#34;: \u0026#34;REDIRECT\u0026#34;, \u0026#34;ISTIO_PROXY_SHA\u0026#34;: \u0026#34;istio-proxy:6166ae7ebac7f630206b2fe4e6767516bf198313\u0026#34;, \u0026#34;ISTIO_PROXY_VERSION\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;ISTIO_VERSION\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;POD_NAME\u0026#34;: \u0026#34;productpage-v1-54b8b9f55-bx2dq\u0026#34;, \u0026#34;istio\u0026#34;: \u0026#34;sidecar\u0026#34; } } Admin\n配置 Envoy 的日志路径以及管理端口。\n\u0026#34;admin\u0026#34;: { \u0026#34;access_log_path\u0026#34;: \u0026#34;/dev/stdout\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port_value\u0026#34;: 15000 } } } Dynamic_resources\n配置动态资源,这里配置了 ADS 服务器。\n\u0026#34;dynamic_resources\u0026#34;: { \u0026#34;lds_config\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;cds_config\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;ads_config\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;refresh_delay\u0026#34;: {\u0026#34;seconds\u0026#34;: 1, \u0026#34;nanos\u0026#34;: 0}, \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds-grpc\u0026#34; } } ] } } Static_resources\n配置静态资源，包括了 xds-grpc 和 zipkin 两个 cluster。其中 xds-grpc cluster 对应前面 dynamic_resources 中 ADS 配置，指明了 Envoy 用于获取动态资源的服务器地址。\n\u0026#34;static_resources\u0026#34;: { \u0026#34;clusters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;xds-grpc\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;connect_timeout\u0026#34;: { \u0026#34;seconds\u0026#34;: 10, \u0026#34;nanos\u0026#34;: 0 }, \u0026#34;lb_policy\u0026#34;: \u0026#34;ROUND_ROBIN\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;istio-pilot.istio-system\u0026#34;, \u0026#34;port_value\u0026#34;: 15010 } } ], \u0026#34;circuit_breakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ { \u0026#34;priority\u0026#34;: \u0026#34;default\u0026#34;, \u0026#34;max_connections\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;max_pending_requests\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;max_requests\u0026#34;: \u0026#34;100000\u0026#34; }, { \u0026#34;priority\u0026#34;: \u0026#34;high\u0026#34;, \u0026#34;max_connections\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;max_pending_requests\u0026#34;: \u0026#34;100000\u0026#34;, \u0026#34;max_requests\u0026#34;: \u0026#34;100000\u0026#34; } ] }, \u0026#34;upstream_connection_options\u0026#34;: { \u0026#34;tcp_keepalive\u0026#34;: { \u0026#34;keepalive_time\u0026#34;: 300 } }, \u0026#34;http2_protocol_options\u0026#34;: {} }, { \u0026#34;name\u0026#34;: \u0026#34;zipkin\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;connect_timeout\u0026#34;: { \u0026#34;seconds\u0026#34;: 1 }, \u0026#34;lb_policy\u0026#34;: \u0026#34;ROUND_ROBIN\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;zipkin.istio-system\u0026#34;, \u0026#34;port_value\u0026#34;: 9411 } } ] } ] } Tracing\n配置分布式链路跟踪。\n\u0026#34;tracing\u0026#34;: { \u0026#34;http\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;envoy.zipkin\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;collector_cluster\u0026#34;: \u0026#34;zipkin\u0026#34; } } } Stats_sinks\n这里配置的是和 Envoy 直连的 metrics 收集 sink,和 Mixer telemetry 没有关系。Envoy 自带 stats 格式的 metrics 上报。\n\u0026#34;stats_sinks\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.statsd\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;10.254.238.237\u0026#34;, \u0026#34;port_value\u0026#34;: 9125 } } } } ] 在Gist https://gist.github.com/zhaohuabing/14191bdcf72e37bf700129561c3b41ae 中可以查看该配置文件的完整内容。\nEnvoy 配置分析 # 通过管理接口获取完整配置 # 从 Envoy 初始化配置文件中，我们可以大致看到 Istio 通过 Envoy 来实现服务发现和流量管理的基本原理。即控制平面将 xDS server 信息通过 static resource 的方式配置到 Envoy 的初始化配置文件中，Envoy 启动后通过 xDS server 获取到 dynamic resource，包括网格中的 service 信息及路由规则。\nEnvoy 配置初始化流程：\nPilot-agent 根据启动参数和 K8S API Server 中的配置信息生成 Envoy 的初始配置文件 envoy-rev0.json，该文件告诉 Envoy 从 xDS server 中获取动态配置信息，并配置了 xDS server 的地址信息，即控制平面的 Pilot。 Pilot-agent 使用 envoy-rev0.json 启动 Envoy 进程。 Envoy 根据初始配置获得 Pilot 地址，采用 xDS 接口从 Pilot 获取到 Listener，Cluster，Route 等动态配置信息。 Envoy 根据获取到的动态配置启动 Listener，并根据 Listener 的配置，结合 Route 和 Cluster 对拦截到的流量进行处理。 可以看到，Envoy 中实际生效的配置是由初始化配置文件中的静态配置和从 Pilot 获取的动态配置一起组成的。因此只对 envoy-rev0.json 进行分析并不能看到 Mesh 中流量管理的全貌。那么有没有办法可以看到 Envoy 中实际生效的完整配置呢？答案是可以的，我们可以通过 Envoy 的管理接口来获取 Envoy 的完整配置。\n$ kubectl exec -it productpage-v1-54b8b9f55-bx2dq -c istio-proxy curl http://127.0.0.1:15000/config_dump \u0026gt; config_dump 该文件内容长达近7000行，本文中就不贴出来了，在Gist https://gist.github.com/zhaohuabing/034ef87786d290a4e89cd6f5ad6fcc97 中可以查看到全文。\nEnvoy 配置文件结构 # 文件中的配置节点包括：\nBootstrap # 从名字可以大致猜出这是 Envoy 的初始化配置，打开该节点，可以看到文件中的内容和前一章节中介绍的 envoy-rev0.json 是一致的，这里不再赘述。\nClusters # 在 Envoy 中，Cluster 是一个服务集群，Cluster 中包含一个到多个 endpoint，每个 endpoint 都可以提供服务，Envoy 根据负载均衡算法将请求发送到这些 endpoint 中。\n在 Productpage 的 clusters 配置中包含 static_clusters 和 dynamic_active_clusters 两部分，其中 static_clusters 是来自于 envoy-rev0.json 的 xDS server 和 zipkin server 信息。dynamic_active_clusters 是通过 xDS 接口从 Istio 控制平面获取的动态服务信息。\nDynamic Cluster 中有以下几类 Cluster：\nOutbound Cluster\n这部分的 Cluster 占了绝大多数，该类 Cluster 对应于 Envoy 所在节点的外部服务。以 details 为例，对于 Productpage 来说，details 是一个外部服务，因此其 Cluster 名称中包含 outbound 字样。\n从 details 服务对应的 cluster 配置中可以看到，其类型为 EDS，即表示该 Cluster 的 endpoint 来自于动态发现，动态发现中 eds_config 则指向了 ads，最终指向 static Resource 中配置的 xds-grpc cluster，即 Pilot 的地址。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:19Z\u0026#34;, \u0026#34;cluster\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;eds_cluster_config\u0026#34;: { \u0026#34;eds_config\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;service_name\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34; }, \u0026#34;connect_timeout\u0026#34;: \u0026#34;1s\u0026#34;, \u0026#34;circuit_breakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] } }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-06T09:34:20.404Z\u0026#34; } 可以通过 Pilot 的调试接口获取该 Cluster 的 endpoint：\n$ curl http://10.96.8.103:9093/debug/edsz \u0026gt; pilot_eds_dump 导出的文件长达 1300 多行，本文只贴出 details 服务相关的 endpoint 配置，完整文件参见: https://gist.github.com/zhaohuabing/a161d2f64746acd18097b74e6a5af551\n从下面的文件内容可以看到，details cluster 配置了 1 个 endpoint 地址，是 details 的 pod ip。\n{ \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;locality\u0026#34;: { }, \u0026#34;lbEndpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;192.168.206.21\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } }, \u0026#34;metadata\u0026#34;: { \u0026#34;filterMetadata\u0026#34;: { \u0026#34;istio\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;kubernetes://details-v1-6764bbc7f7-qwzdg.default\u0026#34; } } } } ] } ] } Inbound Cluster\n该类 Cluster 对应于 Envoy 所在节点上的服务。如果该服务接收到请求，当然就是一个入站请求。对于 Productpage Pod 上的 Envoy，其对应的 Inbound Cluster 只有一个，即 productpage。该 cluster 对应的 host 为 127.0.0.1，即环回地址上 productpage 的监听端口。由于 iptables 规则中排除了 127.0.0.1,入站请求通过该 Inbound cluster 处理后将跳过 Envoy，直接发送给 Productpage 进程处理。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-14T01:44:05Z\u0026#34;, \u0026#34;cluster\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;inbound|9080||productpage.default.svc.cluster.local\u0026#34;, \u0026#34;connect_timeout\u0026#34;: \u0026#34;1s\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port_value\u0026#34;: 9080 } } ], \u0026#34;circuit_breakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] } }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-14T01:44:05.291Z\u0026#34; } BlackHoleCluster\n这是一个特殊的 Cluster，并没有配置后端处理请求的 Host。如其名字所暗示的一样，请求进入后将被直接丢弃掉。如果一个请求没有找到其对的目的服务，则被发到 BlackHoleCluster。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:19Z\u0026#34;, \u0026#34;cluster\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;BlackHoleCluster\u0026#34;, \u0026#34;connect_timeout\u0026#34;: \u0026#34;5s\u0026#34; }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-06T09:34:20.408Z\u0026#34; } Listeners # Envoy 采用 listener 来接收并处理 downstream 发过来的请求，listener 的处理逻辑是插件式的，可以通过配置不同的 filter 来插入不同的处理逻辑。Istio 就在 Envoy 中加入了用于 policy check 和 metric report 的 Mixer filter。\nListener 可以绑定到 IP Socket 或者 Unix Domain Socket 上，也可以不绑定到一个具体的端口上，而是接收从其他 listener 转发来的数据。Istio 就是利用了 Envoy listener 的这一特点实现了将来发向不同服务的请求转交给不同的 listener 处理。\nVirtual Listener\nEnvoy 创建了一个在 15001 端口监听的入口监听器。Iptables 将请求截取后发向 15001 端口，该监听器接收后并不进行业务处理，而是根据请求目的地分发给其他监听器处理。该监听器取名为 virtual（虚拟）监听器也是这个原因。\nEnvoy 是如何做到按服务分发的呢？ 可以看到该 Listener 的配置项 use_original_dest 设置为 true,该配置要求监听器将接收到的请求转交给和请求原目的地址关联的 listener 进行处理。\n从其 filter 配置可以看到，如果找不到和请求目的地配置的 listener 进行转交，则请求将被发送到 BlackHoleCluster,由于 BlackHoleCluster 并没有配置 host，因此找不到对应目的地对应监听器的请求实际上会被丢弃。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:19Z\u0026#34;, \u0026#34;listener\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;virtual\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 15001 } }, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.tcp_proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;stat_prefix\u0026#34;: \u0026#34;BlackHoleCluster\u0026#34;, \u0026#34;cluster\u0026#34;: \u0026#34;BlackHoleCluster\u0026#34; } } ] } ], \u0026#34;use_original_dst\u0026#34;: true }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-06T09:34:26.262Z\u0026#34; } Inbound Listener\n在 Productpage Pod 上的 Envoy 创建了 Listener 192.168.206.23_9080，当外部调用 Productpage 服务的请求到达 Pod 上 15001 的 Virtual Listener 时，Virtual Listener 根据请求目的地匹配到该 Listener,请求将被转发过来。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-14T01:44:05Z\u0026#34;, \u0026#34;listener\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;192.168.206.23_9080\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;192.168.206.23\u0026#34;, \u0026#34;port_value\u0026#34;: 9080 } }, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;mixer\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;transport\u0026#34;: { \u0026#34;check_cluster\u0026#34;: \u0026#34;outbound|9091||istio-policy.istio-system.svc.cluster.local\u0026#34;, \u0026#34;network_fail_policy\u0026#34;: { \u0026#34;policy\u0026#34;: \u0026#34;FAIL_CLOSE\u0026#34; }, \u0026#34;report_cluster\u0026#34;: \u0026#34;outbound|9091||istio-telemetry.istio-system.svc.cluster.local\u0026#34;, \u0026#34;attributes_for_mixer_proxy\u0026#34;: { \u0026#34;attributes\u0026#34;: { \u0026#34;source.uid\u0026#34;: { \u0026#34;string_value\u0026#34;: \u0026#34;kubernetes://productpage-v1-54b8b9f55-bx2dq.default\u0026#34; } } } }, \u0026#34;mixer_attributes\u0026#34;: { \u0026#34;attributes\u0026#34;: { \u0026#34;destination.port\u0026#34;: { \u0026#34;int64_value\u0026#34;: \u0026#34;9080\u0026#34; }, \u0026#34;context.reporter.uid\u0026#34;: { \u0026#34;string_value\u0026#34;: \u0026#34;kubernetes://productpage-v1-54b8b9f55-bx2dq.default\u0026#34; }, \u0026#34;destination.namespace\u0026#34;: { \u0026#34;string_value\u0026#34;: \u0026#34;default\u0026#34; }, \u0026#34;destination.ip\u0026#34;: { \u0026#34;bytes_value\u0026#34;: \u0026#34;AAAAAAAAAAAAAP//wKjOFw==\u0026#34; }, \u0026#34;destination.uid\u0026#34;: { \u0026#34;string_value\u0026#34;: \u0026#34;kubernetes://productpage-v1-54b8b9f55-bx2dq.default\u0026#34; }, \u0026#34;context.reporter.kind\u0026#34;: { \u0026#34;string_value\u0026#34;: \u0026#34;inbound\u0026#34; } } } } }, { \u0026#34;name\u0026#34;: \u0026#34;envoy.tcp_proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;stat_prefix\u0026#34;: \u0026#34;inbound|9080||productpage.default.svc.cluster.local\u0026#34;, \u0026#34;cluster\u0026#34;: \u0026#34;inbound|9080||productpage.default.svc.cluster.local\u0026#34; } } ] } ], \u0026#34;deprecated_v1\u0026#34;: { \u0026#34;bind_to_port\u0026#34;: false } }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-14T01:44:05.754Z\u0026#34; } 从上面的配置 ”bind_to_port”: false 可以得知该 listener 创建后并不会被绑定到 tcp 端口上直接接收网络上的数据，因此其所有请求都转发自 15001 端口。\n该 listener 配置的 envoy.tcp_proxy filter 对应的 cluster为 inbound|9080||productpage.default.svc.cluster.local,该 cluster 配置的 host 为 127.0.0.1:9080，因此 Envoy 会将该请求发向 127.0.0.1:9080。由于 iptables 设置中 127.0.0.1 不会被拦截,该请求将发送到 Productpage 进程的 9080 端口进行业务处理。\n除此以外，Listenter 中还包含 Mixer filter 的配置信息，配置了策略检查(Mixer check)和 Metrics 上报(Mixer report)服务器地址，以及 Mixer上 报的一些 attribute 取值。\nOutbound Listener\nEnvoy 为网格中的外部服务按端口创建多个 Listener，以用于处理出向请求。\nProductpage Pod 中的 Envoy 创建了多个 Outbound Listener：\n0.0.0.0_9080 : 处理对 details，reviews 和 rating 服务的出向请求 0.0.0.0_9411 : 处理对 zipkin 的出向请求 0.0.0.0_15031 :处理对 ingressgateway 的出向请求 0.0.0.0_3000 : 处理对 grafana 的出向请求 0.0.0.0_9093 :处理对 citadel、galley、pilot、(Mixer)policy、(Mixer)telemetry 的出向请求 0.0.0.0_15004 : 处理对 (Mixer)policy、(Mixer)telemetry 的出向请求 \u0026hellip;\u0026hellip; 除了 9080 这个 Listener 用于处理应用的业务之外，其他 listener 都是 Istio 用于处理自身组件之间通信使用的，有的控制平面组件如 Pilot，Mixer 对应多个 listener，是因为该组件有多个端口提供服务。\n我们这里主要分析一下 9080 这个业务端口的 Listenrer。和 Outbound Listener 一样，该 Listener 同样配置了 ”bind_to_port”: false 属性，因此该 listener 也没有被绑定到 tcp 端口上，其接收到的所有请求都转发自 15001 端口的 Virtual listener。\n监听器 name 为 0.0.0.0_9080，推测其含义应为匹配发向任意 IP 的 9080 的请求，从 bookinfo 程序结构可以看到该程序中的 productpage，revirews，ratings，details 四个 service 都是 9080 端口，那么 Envoy 如何区别处理这四个 service 呢？\n首先需要区分入向（发送给productpage）请求和出向（发送给其他几个服务）请求：\n发给 productpage 的入向请求，virtual listener 根据其目的 IP 和 Port 首先匹配到 192.168.206.23_9080 这个 listener 上，不会进入 0.0.0.0_9080 listener处理。 从 productpage 外发给 reviews、details 和 ratings 的出向请求，virtual listener 无法找到和其目的 IP 完全匹配的 listener，因此根据通配原则转交给 0.0.0.0_9080 处理。 备注：\n1. 该转发逻辑为根据 Envoy 配置进行的推测，并未分析 Envoy 代码进行验证。欢迎了解 Envoy 代码和实现机制的朋友指正。 2. 根据业务逻辑，实际上 productpage 并不会调用 ratings 服务，但 Istio 并不知道各个业务之间会如何调用，因此将所有的服务信息都下发到了 Envoy 中。这样做对效率和性能理论上有一定影响，存在一定的优化空间。\n由于对应到 reviews、details 和 ratings 三个服务，当 0.0.0.0_9080 接收到出向请求后，并不能直接发送到一个 downstream cluster 中，而是需要根据请求目的地进行不同的路由。\n在该 listener 的配置中，我们可以看到并没有像 inbound listener 那样通过 envoy.tcp_proxy 直接指定一个 downstream 的 cluster，而是通过 rds 配置了一个路由规则 9080，在路由规则中再根据不同的请求目的地对请求进行处理。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:19Z\u0026#34;, \u0026#34;listener\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_9080\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 9080 } }, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.http_connection_manager\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;access_log\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.file_access_log\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;path\u0026#34;: \u0026#34;/dev/stdout\u0026#34; } } ], \u0026#34;http_filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;mixer\u0026#34;, \u0026#34;config\u0026#34;: { ...... } }, { \u0026#34;name\u0026#34;: \u0026#34;envoy.cors\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;envoy.fault\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;envoy.router\u0026#34; } ], \u0026#34;tracing\u0026#34;: { \u0026#34;operation_name\u0026#34;: \u0026#34;EGRESS\u0026#34;, \u0026#34;client_sampling\u0026#34;: { \u0026#34;value\u0026#34;: 100 }, \u0026#34;overall_sampling\u0026#34;: { \u0026#34;value\u0026#34;: 100 }, \u0026#34;random_sampling\u0026#34;: { \u0026#34;value\u0026#34;: 100 } }, \u0026#34;use_remote_address\u0026#34;: false, \u0026#34;stat_prefix\u0026#34;: \u0026#34;0.0.0.0_9080\u0026#34;, \u0026#34;rds\u0026#34;: { \u0026#34;route_config_name\u0026#34;: \u0026#34;9080\u0026#34;, \u0026#34;config_source\u0026#34;: { \u0026#34;ads\u0026#34;: {} } }, \u0026#34;stream_idle_timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;generate_request_id\u0026#34;: true, \u0026#34;upgrade_configs\u0026#34;: [ { \u0026#34;upgrade_type\u0026#34;: \u0026#34;websocket\u0026#34; } ] } } ] } ], \u0026#34;deprecated_v1\u0026#34;: { \u0026#34;bind_to_port\u0026#34;: false } }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-06T09:34:26.172Z\u0026#34; }, Routes # 配置 Envoy 的路由规则。Istio 下发的缺省路由规则中对每个端口设置了一个路由规则，根据 host 来对请求进行路由分发。\n下面是 9080 的路由配置，从文件中可以看到对应了 3 个 virtual host，分别是 details、ratings 和 reviews，这三个 virtual host 分别对应到不同的 outbound cluster。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-14T01:38:20Z\u0026#34;, \u0026#34;route_config\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;9080\u0026#34;, \u0026#34;virtual_hosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;details.default.svc.cluster.local:9080\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;details.default.svc.cluster.local\u0026#34;, \u0026#34;details.default.svc.cluster.local:9080\u0026#34;, \u0026#34;details\u0026#34;, \u0026#34;details:9080\u0026#34;, \u0026#34;details.default.svc.cluster\u0026#34;, \u0026#34;details.default.svc.cluster:9080\u0026#34;, \u0026#34;details.default.svc\u0026#34;, \u0026#34;details.default.svc:9080\u0026#34;, \u0026#34;details.default\u0026#34;, \u0026#34;details.default:9080\u0026#34;, \u0026#34;10.101.163.201\u0026#34;, \u0026#34;10.101.163.201:9080\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0s\u0026#34;, \u0026#34;max_grpc_timeout\u0026#34;: \u0026#34;0s\u0026#34; }, \u0026#34;decorator\u0026#34;: { \u0026#34;operation\u0026#34;: \u0026#34;details.default.svc.cluster.local:9080/*\u0026#34; }, \u0026#34;per_filter_config\u0026#34;: { \u0026#34;mixer\u0026#34;: { ...... } } } ] }, { \u0026#34;name\u0026#34;: \u0026#34;ratings.default.svc.cluster.local:9080\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;ratings.default.svc.cluster.local\u0026#34;, \u0026#34;ratings.default.svc.cluster.local:9080\u0026#34;, \u0026#34;ratings\u0026#34;, \u0026#34;ratings:9080\u0026#34;, \u0026#34;ratings.default.svc.cluster\u0026#34;, \u0026#34;ratings.default.svc.cluster:9080\u0026#34;, \u0026#34;ratings.default.svc\u0026#34;, \u0026#34;ratings.default.svc:9080\u0026#34;, \u0026#34;ratings.default\u0026#34;, \u0026#34;ratings.default:9080\u0026#34;, \u0026#34;10.99.16.205\u0026#34;, \u0026#34;10.99.16.205:9080\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||ratings.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0s\u0026#34;, \u0026#34;max_grpc_timeout\u0026#34;: \u0026#34;0s\u0026#34; }, \u0026#34;decorator\u0026#34;: { \u0026#34;operation\u0026#34;: \u0026#34;ratings.default.svc.cluster.local:9080/*\u0026#34; }, \u0026#34;per_filter_config\u0026#34;: { \u0026#34;mixer\u0026#34;: { ...... }, \u0026#34;disable_check_calls\u0026#34;: true } } } ] }, { \u0026#34;name\u0026#34;: \u0026#34;reviews.default.svc.cluster.local:9080\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;reviews.default.svc.cluster.local\u0026#34;, \u0026#34;reviews.default.svc.cluster.local:9080\u0026#34;, \u0026#34;reviews\u0026#34;, \u0026#34;reviews:9080\u0026#34;, \u0026#34;reviews.default.svc.cluster\u0026#34;, \u0026#34;reviews.default.svc.cluster:9080\u0026#34;, \u0026#34;reviews.default.svc\u0026#34;, \u0026#34;reviews.default.svc:9080\u0026#34;, \u0026#34;reviews.default\u0026#34;, \u0026#34;reviews.default:9080\u0026#34;, \u0026#34;10.108.25.157\u0026#34;, \u0026#34;10.108.25.157:9080\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||reviews.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0s\u0026#34;, \u0026#34;max_grpc_timeout\u0026#34;: \u0026#34;0s\u0026#34; }, \u0026#34;decorator\u0026#34;: { \u0026#34;operation\u0026#34;: \u0026#34;reviews.default.svc.cluster.local:9080/*\u0026#34; }, \u0026#34;per_filter_config\u0026#34;: { \u0026#34;mixer\u0026#34;: { ...... }, \u0026#34;disable_check_calls\u0026#34;: true } } } ] } ], \u0026#34;validate_clusters\u0026#34;: false }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-27T07:17:50.242Z\u0026#34; } Bookinfo 端到端调用分析 # 通过前面章节对 Envoy 配置文件的分析，我们了解到 Istio 控制平面如何将服务和路由信息通过 xDS 接口下发到数据平面中；并介绍了 Envoy 上生成的各种配置数据的结构，包括 listener，cluster，route 和 endpoint。\n下面我们来分析一个端到端的调用请求，通过调用请求的流程把这些配置串连起来，以从全局上理解 Istio 控制平面的流量控制是如何在数据平面的 Envoy 上实现的。\n下图描述了一个 Productpage 服务调用 Details 服务的请求流程：\n1、Productpage 发起对 Details 的调用：http://details:9080/details/0。\n2、请求被 Pod 的 iptables 规则拦截，转发到 15001 端口。\n3、Envoy 的 Virtual Listener 在 15001 端口上监听，收到了该请求。\n4、请求被 Virtual Listener 根据原目标 IP（通配）和端口（9080）转发到 0.0.0.0_9080 这个 listener。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:19Z\u0026#34;, \u0026#34;listener\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;virtual\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 15001 } } ...... \u0026#34;use_original_dst\u0026#34;: true //请求转发给和原始目的IP:Port匹配的listener }, 5、根据 0.0.0.0_9080 listener 的 http_connection_manager filter 配置,该请求采用 “9080” route 进行分发。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:19Z\u0026#34;, \u0026#34;listener\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_9080\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 9080 } }, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.http_connection_manager\u0026#34;, \u0026#34;config\u0026#34;: { ...... \u0026#34;rds\u0026#34;: { \u0026#34;route_config_name\u0026#34;: \u0026#34;9080\u0026#34;, \u0026#34;config_source\u0026#34;: { \u0026#34;ads\u0026#34;: {} } }, } ] } ], \u0026#34;deprecated_v1\u0026#34;: { \u0026#34;bind_to_port\u0026#34;: false } }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-06T09:34:26.172Z\u0026#34; }, { }, 6、9080 这个 route 的配置中，host name 为 details:9080 的请求对应的 cluster 为 outbound|9080||details.default.svc.cluster.local\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-14T01:38:20Z\u0026#34;, \u0026#34;route_config\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;9080\u0026#34;, \u0026#34;virtual_hosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;details.default.svc.cluster.local:9080\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;details.default.svc.cluster.local\u0026#34;, \u0026#34;details.default.svc.cluster.local:9080\u0026#34;, \u0026#34;details\u0026#34;, \u0026#34;details:9080\u0026#34;, \u0026#34;details.default.svc.cluster\u0026#34;, \u0026#34;details.default.svc.cluster:9080\u0026#34;, \u0026#34;details.default.svc\u0026#34;, \u0026#34;details.default.svc:9080\u0026#34;, \u0026#34;details.default\u0026#34;, \u0026#34;details.default:9080\u0026#34;, \u0026#34;10.101.163.201\u0026#34;, \u0026#34;10.101.163.201:9080\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0s\u0026#34;, \u0026#34;max_grpc_timeout\u0026#34;: \u0026#34;0s\u0026#34; }, ...... } } } ] }, ...... { }, 7、outbound|9080||details.default.svc.cluster.local cluster 为动态资源，通过 eds 查询得到其 endpoint 为 192.168.206.21:9080。\n{ \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;locality\u0026#34;: { }, \u0026#34;lbEndpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;192.168.206.21\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } }, ...... } ] } ] } 8、请求被转发到 192.168.206.21，即 Details 服务所在的 Pod，被 iptables 规则拦截，转发到 15001 端口。\n9、Envoy 的 Virtual Listener 在 15001 端口上监听，收到了该请求。\n10、请求被 Virtual Listener 根据请求原目标地址 IP（192.168.206.21）和端口（9080）转发到 192.168.206.21_9080 这个 listener。\n11、根据 92.168.206.21_9080 listener 的 http_connection_manager filter 配置，该请求对应的 cluster 为 inbound|9080||details.default.svc.cluster.local。\n{ \u0026#34;version_info\u0026#34;: \u0026#34;2018-09-06T09:34:16Z\u0026#34;, \u0026#34;listener\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;192.168.206.21_9080\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;192.168.206.21\u0026#34;, \u0026#34;port_value\u0026#34;: 9080 } }, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.http_connection_manager\u0026#34;, ...... \u0026#34;route_config\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;inbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;validate_clusters\u0026#34;: false, \u0026#34;virtual_hosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;inbound|http|9080\u0026#34;, \u0026#34;routes\u0026#34;: [ ...... \u0026#34;route\u0026#34;: { \u0026#34;max_grpc_timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;cluster\u0026#34;: \u0026#34;inbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, ...... \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; } } ], \u0026#34;domains\u0026#34;: [ \u0026#34;*\u0026#34; ] } ] }, ...... ] } } ] } ], \u0026#34;deprecated_v1\u0026#34;: { \u0026#34;bind_to_port\u0026#34;: false } }, \u0026#34;last_updated\u0026#34;: \u0026#34;2018-09-06T09:34:22.184Z\u0026#34; } 12、inbound|9080||details.default.svc.cluster.local cluster 配置的 host 为127.0.0.1:9080。\n13、请求被转发到 127.0.0.1:9080，即 Details 服务进行处理。\n上述调用流程涉及的完整 Envoy 配置文件参见：\nProudctpage ： https://gist.github.com/zhaohuabing/034ef87786d290a4e89cd6f5ad6fcc97 Details ： https://gist.github.com/zhaohuabing/544d4d45447b65d10150e528a190f8ee 小结 # 本文介绍了 Istio 流量管理相关组件，Istio 控制平面和数据平面之间的标准接口，以及 Istio 下发到 Envoy 的完整配置数据的结构和内容。然后通过 Bookinfo 示例程序的一个端到端调用分析了 Envoy 是如何实现服务网格中服务发现和路由转发的，希望能帮助大家透过概念更进一步深入理解 Istio 流量管理的实现机制。\n参考资料 # Istio Traffic Managment Concept Data Plane API kubernetes Custom Resource Istio Pilot Design Overview Envoy V2 API Overview Data Plane API Protocol Buffer Definition xDS REST and gRPC protocol Pilot Debug interface Istio Sidecar自动注入原理 ","date":"2018年10月9日","externalUrl":null,"permalink":"/posts/istio-traffic-management-impl-intro/","section":"博客","summary":"本文转载自 赵化冰的博客 前言 # Istio 作为一个 service mesh 开源项目,其中最重","title":"Istio 流量管理实现机制深度解析","type":"posts"},{"content":"我的博客之前是使用 Nginx 来反代的，由于 Nginx 性能优异，目前有很多国内网站采用 Nginx 作为 Web 服务器，而且参考文档比较丰富，无论是对于其部署，配置还是调优都更为有经验。但是还是会碰到几个绕不开的问题：\nNginx 的反向代理不支持 http2/grpc (好像今年 3 月份刚支持) 不像 Envoy 几乎所有的网络配置都可以利用 xDS API 来实现动态变更，Nginx 缺乏有效的配置热变更机制(除非深入开发或者不断地 reload)。 Nginx 的很多微服务功能都是要买 Nginx Plus 才有的。 而 Envoy 是一款现代化的，高性能，小体积的边缘及服务代理，浑身散发出一股时尚潮流的气息。作为一名斜杠青年，在经过一定地了解后，我果断入了 Envoy 的坑。\n关于如何为 Envoy 开启证书验证可以参考我之间的文章： 为 Envoy 启用证书验证。本文将直接进入实战部分，通过 Envoy 来反向代理我的博客静态页面，并且加密客户端和 Envoy 代理之间的所有流量。\n方案架构 # 本方案涉及到两层 Envoy：\n首先会有一个前端代理在某个地方单独运行。前端代理的工作是给其他地方提供一个入口。来自外部的传入连接请求到这里，前端代理将会决定他们在内部的转发路径。 其次，博客静态页面由 nginx 提供，同时运行一个 “服务 Envoy”，它与 nginx 容器共享 network nemspace（相当于 Kubernetes 的 Sidecar）。 所有的 Envoy 形成一个网格，然后在他们之间共享路由信息。 注意，通常情况下你也可以只使用前端代理，然后去掉服务 Envoy 这一层。但是，使用完整网格的话，服务 Envoy 可以对应用服务进行健康监控等，让网格知道尝试联系一个挂掉的服务是否是毫无意义的。此外，Envoy 的统计数据收集最适合用在全网格上。\n但本文需要开启 TLS 验证，如果前端代理开启了 TLS 验证，那么必须配合服务 Envoy 使用，否则验证将无法通过。\n部署服务 Envoy # 我的博客是通过 hugo 生成的，其他生成静态页面的软件类似，都可以采用我的方案。由于我的 hugo 根目录是 /home/hugo，首先进入该目录，然后创建容器编排的 docker-compose.yml 文件。\nversion: \u0026#39;2\u0026#39; services: hugo: image: nginx:alpine restart: always volumes: - /home/hugo/public:/usr/share/nginx/html ① networks: - default expose: - \u0026#34;80\u0026#34; - \u0026#34;8080\u0026#34; service-envoy: image: envoyproxy/envoy-alpine:latest restart: always volumes: - ./service-envoy.yaml:/etc/envoy/envoy.yaml ② network_mode: \u0026#34;service:hugo\u0026#34; ③ networks: default: external: name: yang ④ ① : 将博客的静态页面挂载到 nginx 的 root 目录。 ② : 将服务 Envoy 的配置文件挂载到 Envoy 容器中。 ③ : 与 hugo 容器共享 network namespace。 ④ : 这是我自定义的网络，你可以换成你自己的。 接下来需要创建服务 Envoy 的配置文件 service-envoy.yaml：\nstatic_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 8080 ① filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: service domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: local_service http_filters: - name: envoy.router config: {} clusters: - name: local_service connect_timeout: 0.25s type: strict_dns lb_policy: round_robin hosts: - socket_address: address: 127.0.0.1 port_value: 80 ② admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8081 ① 8080 : 服务 Envoy 的监听端口。 ② 80 : hugo 静态页面的监听端口。 部署前端代理 # 在 docker-compose.yml 文件中添加前端代理部分：\nversion: \u0026#39;2\u0026#39; services: ... front-envoy: image: envoyproxy/envoy restart: always volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml - /etc/letsencrypt:/etc/letsencrypt labels: EnvironmentName: \u0026#34;proxy\u0026#34; ServiceName: \u0026#34;envoy\u0026#34; ProxyMode: \u0026#34;tcp\u0026#34; networks: - default expose: - \u0026#34;80\u0026#34; - \u0026#34;443\u0026#34; ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; 创建前端代理需要的配置文件 front-envoy.yaml：\nstatic_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto ① stat_prefix: ingress_http route_config: virtual_hosts: - name: backend domains: ② - \u0026#34;yangcs.net\u0026#34; - \u0026#34;icloudnative.io\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: path_redirect: \u0026#34;/\u0026#34; https_redirect: true http_filters: - name: envoy.router config: {} - address: socket_address: address: 0.0.0.0 port_value: 443 filter_chains: - tls_context: common_tls_context: alpn_protocols: h2,http/1.1 ③ tls_certificates: ④ - certificate_chain: filename: \u0026#34;/etc/letsencrypt/live/icloudnative.io/fullchain.pem\u0026#34; private_key: filename: \u0026#34;/etc/letsencrypt/live/icloudnative.io/privkey.pem\u0026#34; filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: backend domains: - \u0026#34;yangcs.net\u0026#34; - \u0026#34;icloudnative.io\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; route: cluster: hugo http_filters: - name: envoy.router config: {} clusters: - name: hugo connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: hugo port_value: 8080 admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8001 ① : 编码/解码方式。参考： HttpConnectionManager.CodecType ② : 允许访问的域名（这里使用公网可以访问的域名）。 ③ : TLS 监听器支持 ALPN。HTTP 连接管理器使用这个信息（以及协议接口）来确定客户端使用的是 HTTP/1.1 还是 HTTP/2。 ④ : 网站使用的证书。可以通过 Let\u0026rsquo;s Encrypt 申请免费的证书。 其他配置详细说明请参考： 为 Envoy 启用证书验证。\n准备好所有配置以后，我们就可以通过以下命令来启动所有服务了：\n$ docker-compose up -d Creating front-proxy_hugo_1 ... done Creating front-proxy_front-envoy_1 ... done Creating front-proxy_service-envoy_1 ... done 接下来就可以通过公网域名访问博客网站啦！没错，你现在浏览的我的博客就是通过 Envoy 反向代理的。 不信请看：\n$ curl -I https://icloudnative.io HTTP/2 200 server: envoy date: Fri, 30 Nov 2018 06:42:52 GMT content-type: text/html content-length: 40537 last-modified: Thu, 29 Nov 2018 05:41:29 GMT etag: \u0026#34;5bff7c09-9e59\u0026#34; accept-ranges: bytes x-envoy-upstream-service-time: 0 strict-transport-security: max-age=63072000; includeSubDomains; preload ","date":"2018年9月26日","externalUrl":null,"permalink":"/posts/setting-up-ssl-in-envoy-practice/","section":"博客","summary":"我的博客之前是使用 Nginx 来反代的，由于 Nginx 性能优异，目前有很多国内","title":"Envoy 基础教程：开启 TLS 验证实战","type":"posts"},{"content":" 什么是准入控制 # 准入控制（Admission Controller）是 Kubernetes API Server 用于拦截请求的一种手段。Admission 可以做到对请求的资源对象进行校验，修改。service mesh 最近很火的项目 Istio 天生支持 Kubernetes，利用的就是 Admission 对服务实例自动注入 sidecar。\n假如对 Kubernetes 有一定的了解的话，应该会知道在 Kubernetes 中还有 authn/authz，为什么还会引入 admission 这种机制？\nauthn/authz 是 Kubernetes 的认证和鉴权，运行在 filter 中，只能获取 http 请求 header 以及证书，并不能获取请求的 body。所以 authn/authz 只能对客户端进行认证和鉴权，不可以对请求的对象进行任何操作，因为这里根本还获取不到对象。\nAdmission 运行在 API Server 的增删改查 handler 中，可以自然地操作 API resource。它是在经过授权之后，资源持久化之前的一个处理 API server 请求的步骤。准入过程能获取到和认证过程一致的信息（用户、URL 等），以及绝大多数 API 请求的完整报文。\n准入阶段由不同的插件组成，每个插件都能 “各司其职”，并明确知道自己要检查的对象结构。例如：PodNodeSelector（影响调度决策），PodSecurityPolicy（防止升级的容器）和 ResourceQuota（为每个 Namespace 限制资源配额）。\n准入分为两个阶段：\n修改 (Mutation) 阶段 : 在对象持久化之前修改对象的主体内容以及拒绝 API 请求。\n验证 (Validation) 阶段 ：在对象持久化之前进行校验以及拒绝 API 请求。\n一个准入插件可以在这两个阶段应用，但是所有的修改阶段都发生在验证阶段之前。\n修改 （Mutation）阶段 # Admission 的 Mutation 阶段允许在资源内容生成前进行修改。因为同一个字段在 Admission 链上可以被多次修改，因此 Admission 插件的执行顺序很重要。\n准入修改插件（Mutating Admission Plugin）中的一个例子就是 PodNodeSelector，它使用 Namespace 的一个 annotation：namespace.annotations[“scheduler.alpha.kubernetes.io/node-selector”] 来查找标签选择器并将其添加到 pod.spec.nodeselector 字段。这一功能正向限制了特定 Namespace 中的 pod 能够落在哪个节点上，这与提供反向限制的 taints 正相反（也是通过 Admission 插件来实现的）。\n验证 （Validating）阶段 # 我们可以在 Admisson 的验证阶段来检查特定 API 资源以保证其不变。验证阶段在所有的 mutators 完成之后运行，以确保资源在做完验证之后不会被再次改变。\n准入验证插件（Validation Admission Plugin）的一个例子也是 PodNodeSelector 插件，它可以确保所有 pod 的 spec.nodeSelector 字段都能符合 Namespace 上节点选择器的约束。即使在 Mutating 链中运行 PodNodeSelector 之后，有其他修改插件试图更改 spec.nodeSelector 字段，验证链中的 PodNodeSelector 插件也会因验证失败而阻止 API 资源的创建。\n下面将对准入控制工作流做一番详解。\nAPI Server 接收到客户端请求后首先进行认证鉴权，认证鉴权通过后才会进行后续的 endpoint handler 处理。\n当 API Server 接收到对象后首先根据 http 的路径可以知道对象的版本号，然后将 request body 反序列化成 versioned object。 versioned object 转化为 internal object，即没有版本的内部类型，这种资源类型是所有 versioned 类型的超集。只有转化为 internal 后才能适配所有的客户端 versioned object 的校验。 Admission Controller 具体的 admit 操作，可以通过这里修改资源对象，例如为 Pod 挂载一个默认的 Service Account 等。 API Server internal object validation，校验某个资源对象数据和格式是否合法，例如：Service Name 的字符个数不能超过 63 等。 Admission Controller validate，可以自定义任何的对象校验规则。 internal object 转化为 versioned object，并且持久化存储到 etcd。 如何使用准入控制 # Kubernetes 1.10 之前的版本可以使用 --admission-control 打开准入控制。同时 --admission-control 的顺序决定 Admission 运行的先后。其实这种方式对于用户来讲其实是挺复杂的，因为这要求用户对所有的准入控制器需要完全了解。\n如果使用 Kubernetes 1.10 之后的版本，--admission-control 已经废弃，建议使用\n--enable-admission-plugins 和 --disable-admission-plugins 指定需要打开或者关闭的准入控制器。 同时用户指定的顺序并不影响实际准入控制器的执行顺序，对用户来讲非常友好。\n值得一提的是，有些准入控制器可能会使用 Alpha 版本的 API，这时必须首先使能其使用的 API 版本。否则准入控制器不能工作，可能会影响系统功能。\nAdmission Webhook # 目前 Kubernetes 中已经有非常多的 Admission 插件， 但是并不能保证满足所有开发者的需求。 众所周知，Kbernetes 之所以受到推崇，它的可扩展能力功不可没。Admission 也提供了一种 webhook 的扩展机制。\nMutatingAdmissionWebhook ：在对象持久化之前进行修改 ValidatingAdmissionWebhook ：在对象持久化之前进行校验 Admission Webhook 允许 Kubernetes 安装人员或集群管理员，不需要进行重新编译，就可以直接添加修改（Mutation）和验证（Validation）这两种插件到 kube-apiserver 和任何基于 k8s.io/apiserver 1.9 扩展的 apiserver (如 metrics, service-catalog, kube-projects 等) 准入链中。这两种 Admission Webhook 插件分别会在修改和验证链的最后执行，与编译的准入插件具有相同的功能。\n可能有读者接触过另外一种动态可扩展的机制 Initializers，不过至今还是 Apha 特性，社区讨论有可能会把它移除。所以选择动态 Admission 首选 webhook。\nWebhook Admission 插件的优势 # Webhook Admission 插件允许对任何 API server 的任何资源进行修改和验证，所以应用场景非常广泛，比较常见的用例包括：\n修改如 pod 这样的资源 : Istio 通过修改 pod 资源，把 sidecar 容器注入到 pod 中。你也可以编写一个能够强制将镜像 tag 解析成 SHA 的插件。 命名限制 : 在多租户系统上，保留 Namespace 已经成为一种用例。 复杂的 CustomResource 验证 : 因为整个对象是可见的，所以插件可以对字段间依赖（A 需要 B）甚至外部资源（对比 LimitRanges）进行复杂的验证。 安全响应 : 如果你把镜像 tag 改成了 SHA，你可以通过写一个插件来阻止对应某些 SHA 的镜像运行。 注册 # 这两种类型的 Webhook Admission 插件都需要在 API 中注册，所有 API servers（kube-apiserver 和所有扩展 API servers ）都共享一个通用配置。在注册过程中，一个 Webhook Admission 插件描述了以下信息：\n如何连接到 Webhook Admission Server 如何验证 Webhook Admission Server（是否是我们期望的 server） 数据应该发送到 Server 的哪个 URL 路径 它将处理哪些资源和哪些 HTTP 动词 API server 在连接失败后应该做什么（例如如果 Webhook Admission Server 停止服务了） apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: namespacereservations.admission.online.openshift.io webhooks: - name: namespacereservations.admission.online.openshift.io ① clientConfig: ② service: namespace: default name: kubernetes path: /apis/admission.online.openshift.io/v1alpha1/namespacereservations caBundle: KUBE\\_CA\\_HERE ⑤ rules: ③ - apiGroups: - \u0026#34;\u0026#34; apiVersions: - \u0026#34;\u0026#34; operations: - CREATE resources: - namespaces failurePolicy: Fail ④ ① name : Webhook 的名称。mutating Webhooks 会根据名称进行排序。\n② clientConfig : 提供关于如何连接、信任以及发送数据给 Webhook Admission Server 的信息。\n③ rules : 用来描述 API server 应该在什么时候调用 Admission 插件。在这个例子中，只有创建 Namespace 的时候才触发。你可以指定任何资源，例如 serviceinstances.servicecatalog.k8s.io 的 create 操作也是可行的。\n④ failurePolicy : 如果 Webhook Admission Server 无法连接时如何处理。有两个选项分别是 “Ignore”（故障时开放） 和 “Fail”（故障时关闭）。“故障时开放”可能会导致无法预测的行为。\n⑤ caBundle : 注意 API server 调用 Webhook 时一定是通过 TLS 认证的，所以 MutatingWebhookConfiguration 中一定要配置 caBundle。\n对比 initializerConfiguration，ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 在 rule 的定义时，增加了 operations field，在 resources 定义时候可以指定 subresource，格式为 resource/subresource。 认证和信任 # 由于 Webhook Admission 插件具有强大的功能（他们可以查看 API 资源内容中任何发给他们的请求，并可以通过插件进行修改），所以在使用时需要考虑的重点是：\n各个 API servers 如何验证其与 Webhook Admission Server 的连接。 Webhook Admission Server 如何准确地认证哪个 API server 正在与它连接。 该特定的 API server 是否有权进行请求。 连接可以分为以下三大类：\n从 kube-apiserver 或 extension-apiservers 到运行在集群外部的 Admission Webhooks 从 kube-apiserver 到运行在集群内部的 Admission Webhooks。 从 extension-apiservers 到运行在集群内部的 Admission Webhooks。 为了支持这三大类连接，Webhook Admission 插件可以支持从 kubeconfig 文件中读取连接各个 server 的信息。由于认证/授权和访问路径是由用户所连接的服务器所决定的，因此为了与运行在集群外部的 Admission Webhooks 进行交互，除了手动配置这个文件之外，实际上没有其他选择。\n对于在集群内运行的 Admission Webhook 来说，一个巧妙构建的 Webhook Admission Server 和拓扑结构，就是能够利用 Admission 插件中内置的安全默认值，并具有可从任何 API server 运行的安全、可移植和零配置的拓扑结构。\n简单安全，可移植的拓扑结构 # 如果你建立的 Webhook Admission Server 也是一个 extension API server，就有可能把它作为一个普通的 API server 来聚合。这具有许多优点：\n你的 Webhook 在默认 kube-apiserver 服务 kubernetes.default.svc 下变得可用 （例如，https://kubernetes.default.svc/apis/admission.example.com/v1/mymutatingadmissionreviews）。另一个好处是，你可以使用 kubectl 进行测试。\n你的 Webhook 会自动（无需任何配置）使用 kube-apiserver 提供的集群内认证和授权。你可以使用正常的 RBAC 规则限制对 Webhook 的访问。\n你的 extension API servers 和 kube-apiserver （无需任何配置）可以自动利用其集群内的凭证与 Webhook 进行通信。\n因为中间会经过 kube-apiserver 这个安全的前端代理，所以 extension API servers 不会将其 service account token 泄漏给 Webhook。\n**简而言之：一个安全的拓扑结构可以使用 API server 聚合 (API server aggregation) 的所有安全机制，不需要额外的配置。**其他的拓扑结构也是可行的，但是需要额外的手动配置以及创建安全设置工作。尤其是像 service catalog 这种 extension API servers，上面的拓扑结构就是零配置，并且可移植到任何 Kubernetes 集群中。\n如何使用 Admission Webhook # Webhook Admission 属于同步调用，需要用户部署自己的 webhook server，创建自定义的配置资源对象： ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration。\n开发 Webhook Server # 这里我推荐参考社区 e2e 测试用的 server，对细节源代码感兴趣的读者可以自行参考\ngithub.com/kubernetes/…，这里面利用 golang 标准库实现的一个基本的 http server，并注册多个路由，同时服务于多种 resource 的准入控制。重点关注一下资源对象的 decode 过程，这是 k8s apimachinery 的高级功能。利用了 apimachinery 的 scheme 的能力，使用之前必须要将 api 注册到 scheme 中，代码详见：\ngithub.com/kubernetes/…。一个典型的 webhook 修改资源对象（Pod）的样例代码如下所示。\nfunc mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { glog.V(2).Info(\u0026#34;mutating pods\u0026#34;) podResource := metav1.GroupVersionResource{Group: \u0026#34;\u0026#34;, Version: \u0026#34;v1\u0026#34;, Resource: \u0026#34;pods\u0026#34;} if ar.Request.Resource != podResource { glog.Errorf(\u0026#34;expect resource to be %s\u0026#34;, podResource) return nil } raw := ar.Request.Object.Raw pod := corev1.Pod{} deserializer := codecs.UniversalDeserializer() // pod的解码，利用apimachinery if _, _, err := deserializer.Decode(raw, nil, \u0026amp;pod); err != nil { glog.Error(err) return toAdmissionResponse(err) } reviewResponse := v1beta1.AdmissionResponse{} reviewResponse.Allowed = true if pod.Name == \u0026#34;webhook-to-be-mutated\u0026#34; { reviewResponse.Patch = []byte(addInitContainerPatch) pt := v1beta1.PatchTypeJSONPatch reviewResponse.PatchType = \u0026amp;pt } return \u0026amp;reviewResponse } 部署 Webhook Server # $ kubectl create –f webhook-server.yaml apiVersion: v1 kind: Namespace metadata: name: e2e-tests-webhook-gbgt6 spec: finalizers: - kubernetes --- apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: sample-webhook webhook: \u0026#34;true\u0026#34; name: sample-webhook-deployment namespace: e2e-tests-webhook-gbgt6 spec: replicas: 1 selector: matchLabels: app: sample-webhook webhook: \u0026#34;true\u0026#34; template: metadata: labels: app: sample-webhook webhook: \u0026#34;true\u0026#34; spec: containers: - args: - --tls-cert-file=/webhook.local.config/certificates/tls.crt - --tls-private-key-file=/webhook.local.config/certificates/tls.key - --alsologtostderr - -v=4 - 2\u0026gt;\u0026amp;1 image: gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.10v2 imagePullPolicy: IfNotPresent name: sample-webhook volumeMounts: - mountPath: /webhook.local.config/certificates name: webhook-certs readOnly: true volumes: - name: webhook-certs secret: defaultMode: 420 secretName: sample-webhook-secret --- apiVersion: v1 kind: Service metadata: labels: test: webhook name: e2e-test-webhook namespace: e2e-tests-webhook-gbgt6 spec: ports: - port: 443 protocol: TCP targetPort: 443 selector: webhook: \u0026#34;true\u0026#34; sessionAffinity: None type: ClusterIP 创建 webhook server Deployment 以及 Service，供 API Server 调用。\n创建 MutatingWebhookConfiguration # $ kubectl create –f webhook-config.yaml apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: e2e-test-mutating-webhook-pod webhooks: - clientConfig: caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMyRENDQWNDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFkTVJzd0dRWURWUVFERXhKbE1tVXQKYzJWeWRtVnlMV05sY25RdFkyRXdIaGNOTVRnd056RTVNRGMwT1RJeFdoY05Namd3TnpFMk1EYzBPVEl4V2pBZApNUnN3R1FZRFZRUURFeEpsTW1VdGMyVnlkbVZ5TFdObGNuUXRZMkV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBCkE0SUJEd0F3Z2dFS0FvSUJBUURFVVFEWVN6SGl3SUFHU1dHSWRBSmVBbnMrNFhaYjlZc3VuQlBVTkJPdHZqeFoKV3NSbUxydE0zVU9lcEszeGsvMzZCSS96RkdXdUNpMlJ0TWUxSWtEa2tVMzNEZE83K0ExVyt2NVZNVnFqL0lDTApsc29USml3TFhTcGowTHNwSUNVdGtqT1dlRjVhK3lJVHgyR01TMG9ZbWtuaHB0RXMrc2tKQjFMWm1uVTBaWFpzClRKak9Lb05ueHdVaTl4QnRUTXBQRWw2cVhmb3dCWlpvYjlkUzNtNzFLbjJCdU5Ec0s3YnVRcGJvdk9XdUQyNDAKdzNLQVJnT04xcjA4Vm4zd1I1MHVXS09tSkVsLzRUZ2JnSTRkaG85WHNIWUhUdnk4R3JRMXhYZE43ZEhSTlpHNQo5aDhmOUUzdjg1VWxwSEVWQThqUHB4RE5SSm9qRXVGQk9raFJEZEY1QWdNQkFBR2pJekFoTUE0R0ExVWREd0VCCi93UUVBd0lDcERBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDWWl4VUsKYkhsRUpCK2t4THdqdktySDQ1OVVsNUJjb0VXZE1BNnArUC8yWXVZa2NuWC9GRVNjUFRxUS9vdkF3ejU1ZG1FUwpJTjVZOWd2ZlJxdWhZcEdWOHVFSWpzVkczTjdKQm1wM0NyclEyd3FYeHV3cndkVXV1dDltQSt2RkQ4Q2FQSE8xCmVad1J6NEkzTktFQ0xHMHJXQWxseEVvUm9tQ2UvaWZIUnRNRklTRk5sSnZVNlhIbzFDVWNFQ2FwOG9hYXN2cFcKT2JBQjVqQzc5WWJXN2lWVm54cjZGMnRvOG9oSEdNSEpXR1pwSTNKbVpNbGVOK01kVm5ySFdXSXBkOG9iS2E3TgpqSlZTczgzRmlDMzd4d2dqMUQyaTNHUnh5bHNKZEdJWTl4WVpQVmNNUTh6Z2FMMUpJUk1BdVZYbHczUkRzSDR0Cms5WmFybGY1NG9BOUN0Nk8KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= service: name: e2e-test-webhook namespace: e2e-tests-webhook-gbgt6 path: /mutating-pods failurePolicy: Ignore name: adding-init-container.k8s.io namespaceSelector: {} rules: - apiGroups: - \u0026#34;\u0026#34; apiVersions: - v1 operations: - CREATE resources: - pods - rules 表示对于 core/v1/pods 资源对象创建的时候调用 mutating webhook。server 的地址及路径通过 clientConfig 指明。\n/mutating-pods 是指调用 webhook server 执行 mutatePods，为 pod 增加 init initContainers。\nfunc mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { glog.V(2).Info(\u0026#34;mutating pods\u0026#34;) podResource := metav1.GroupVersionResource{Group: \u0026#34;\u0026#34;, Version: \u0026#34;v1\u0026#34;, Resource: \u0026#34;pods\u0026#34;} if ar.Request.Resource != podResource { glog.Errorf(\u0026#34;expect resource to be %s\u0026#34;, podResource) return nil } raw := ar.Request.Object.Raw pod := corev1.Pod{} deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, \u0026amp;pod); err != nil { glog.Error(err) return toAdmissionResponse(err) } reviewResponse := v1beta1.AdmissionResponse{} reviewResponse.Allowed = true if pod.Name == \u0026#34;webhook-to-be-mutated\u0026#34; { reviewResponse.Patch = []byte(addInitContainerPatch) pt := v1beta1.PatchTypeJSONPatch reviewResponse.PatchType = \u0026amp;pt } return \u0026amp;reviewResponse } 创建 Pod # $ kubectl create –f pod.yaml apiVersion: v1 kind: Pod metadata: name: webhook-to-be-mutated namespace: e2e-tests-webhook-gbgt6 spec: containers: - image: k8s.gcr.io/pause:3.1 name: example 查询 Pod # $ kubectl get pod webhook-to-be-mutated –n e2e-tests-webhook-gbgt6 -oyaml apiVersion: v1 kind: Pod metadata: creationTimestamp: 2018-07-19T07:49:37Z name: webhook-to-be-mutated namespace: e2e-tests-webhook-gbgt6 resourceVersion: \u0026#34;806\u0026#34; selfLink: /api/v1/namespaces/e2e-tests-webhook-gbgt6/pods/webhook-to-be-mutated uid: 48d2e91d-8b28-11e8-b16d-286ed488dc10 spec: containers: - image: k8s.gcr.io/pause:3.1 imagePullPolicy: IfNotPresent name: example resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: default-token-jhqlb readOnly: true dnsPolicy: ClusterFirst initContainers: - image: webhook-added-image imagePullPolicy: Always name: webhook-added-init-container resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File nodeName: 127.0.0.1 priority: 0 restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 volumes: - name: default-token-jhqlb secret: defaultMode: 420 secretName: default-token-jhqlb 可以看出，创建成功的pod已经多了一个名字为 webhook-added-init-container 的 initContainers。\nIstio 就是使用 ValidatingAdmissionWebhooks 验证 Istio 配置，使用 MutatingAdmissionWebhooks 自动将 sidecar 代理注入至用户 pod。可以参考： 动态准入 Webhooks 概述。\n总结 # 最后我们来总结下 webhook Admission 的优势：\nwebhook 可动态扩展 Admission 能力，满足自定义客户的需求。 不需要重启 API Server，可通过创建 webhook configuration 热加载 webhook admission。 参考 # Kubernetes 准入控制 Admission Controller 介绍 Kubernetes 1.9 |可扩展准入机制进入 Beta 阶段 扫一扫关注微信公众号 ","date":"2018年9月22日","externalUrl":null,"permalink":"/posts/kubernetes-extensible-admission/","section":"博客","summary":"什么是准入控制 # 准入控制（Admission Controll","title":"Kubernetes 准入控制介绍","type":"posts"},{"content":" 原文链接： Increasing Security of Istio Deployments by Removing the Need for Privileged Containers\n随着 1.0 版本的发布，Istio 正在为开发云原生应用并希望采用服务网格解决方案的公司准备黄金时间。但是，有一个潜在的问题可能会降低这些公司的采用率：服务网格内的 Pod 需要提升权限才能正常运行。\n为了从一定程度上缓解这个问题，本文将介绍一个新的工具： istio-pod-network-controller.\n问题 # 作为服务网格正常操作的一部分，Istio 需要操作 Pod 的 iptables 规则，以拦截所有的进出 Pod 的流量，并注入使 Istio 能够发挥作用的 Sidecar。由于 iptables 规则是针对网络命名空间操作的，所以在某个 Pod 中修改 iptables 规则不会影响到其他 Pod 或运行该 Pod 的节点。\ninit 容器是 Istio Pod 的一部分，负责在应用程序容器启动之前添加这些 iptables 规则。如果想在容器中操作 iptables 规则，必须通过开启 NET_ADMIN capability 来提升操作权限。NET_ADMIN 是一种允许你重新配置网络的 Linux Capability，这意味着具有该特权的 Pod 不仅可以将自身添加到 Istio 网格，还可以干扰其他 Pod 的网络配置以及节点本身的网络配置。但是在通常情况下，我们是不建议在共享租户的集群中运行具有此特权权限的应用程序 Pod 的。\nOpenShift 提供了一种通过称为 Security Context Context (SCC) 的机制来控制 Pod 可以拥有的权限的方法（在本例中指的是 Linux Capabilities）。Openshift 中提供了一些开箱即用的 SCC 配置文件，集群管理员还可以添加更多自定义配置文件。允许正常运行 Istio 的唯一开箱即用的 SCC 配置文件是 privileged 配置文件。为了将某个命名空间中的 Pod 添加到 Istio 服务网格，必须执行以下命令才能访问 privileged SCC：\n$ oc adm policy add-scc-to-user privileged -z default -n \u0026lt;target-namespace\u0026gt; 但是这样做本质上就为此命名空间中的所有 Pod 提供了 root 权限。而运行普通应用程序时，由于潜在的安全隐患，通常又不建议使用 root 权限。\n虽然这个问题一直困扰着 Istio 社区，但迄今为止 Kubernetes 还没有提供一种机制来控制给予 Pod 的权限。从 Kubernetes 1.11 开始， Pod 安全策略（PSP）功能已经作为 beta feature 引入，PSP 与 SCC 的功能类似。一旦其他 Kubernetes 发行版开始支持开箱即用的 PSP，Istio 网格中的 Pod 就需要提升权限才能正常运行。\n解决方案 # 解决这个问题的一种方法是将配置 Pod 的 iptables 规则的逻辑移出 Pod 本身。该方案通过一个名叫 istio-pod-network-controller 的 DaemonSet 控制器，来监视新 Pod 的创建，并在创建后立即在这些新 Pod 中配置相应的 iptables 规则。下图描绘了该解决方案的整体架构：\n流程如下：\n创建一个新 Pod 创建该 Pod 的节点上运行的 istio-pod-network-controller 检测新创建的 Pod 是否属于 Istio 网格，如果属于则对其进行初始化。 Pod 中的 init 容器等待初始化 annotation 出现，确保应用程序容器和 Sidecar Envoy 代理仅在 iptables 初始化完成后再启动。 启动 Sidecar 容器和应用程序容器。 有了这个解决方案，由于 Envoy Sidecar 需要以特定的非 root 用户 ID 运行，在 Istio 网格中运行的 Pod 只需要 nonroot SCC 就行了。\n理想情况下，我们希望 Istio 中的应用程序通过 restricted SCC 运行，这是 Openshift 中的默认值。虽然 nonroot SCC 比 restricted SCC 的权限稍微宽泛一些，但这种折衷方案是可以接受的，这与使用 privileged SCC 运行每个 Istio 应用程序 Pod 相比，是一个巨大的进步。\n现在，我们通过给 istio-pod-network-controller 提供 privileged 配置文件和 NET_ADMIN capability 来允许它修改其他 Pod 的 iptables 规则，这通常是可以接受的方案，因为该组件将由集群管理员以与 Istio 控制平面类似的方式安装和管理。\n安装指南 # 根据安装指南假设 Istio 已成功安装在 istio-system 命名空间中，并且已经开启了 自动注入功能。克隆 istio-pod-network-controller 仓库，然后执行以下命令以使用 Helm 安装 istio-pod-network-controller：\n$ helm template -n istio-pod-network-controller ./chart/istio-pod-network-controller | kubectl apply -f - 测试自动注入功能 # 执行以下命令测试自动注入功能：\n$ kubectl create namespace bookinfo $ kubectl label namespace bookinfo istio-injection=enabled $ kubectl annotate namespace bookinfo istio-pod-network-controller/initialize=true $ kubectl apply -f examples/bookinfo.yaml -n bookinfo 其他部署方案请参考 官方仓库的文档。\n总结 # istio-pod-network-controller 是一个用来提高 Istio Deployment 安全性的可选工具，它通过消除在 Istio 网格中运行使用 privileged SCC 的 Pod 的需求，并让这些 Pod 只通过 nonroot SCC 运行，以此来提高安全性。如果您决定采用此解决方案，请注意这并不是 Red Hat 正式支持的项目。\n扫一扫关注微信公众号 ","date":"2018年9月21日","externalUrl":null,"permalink":"/posts/increasing-security-of-istio-deployments/","section":"博客","summary":"原文链接： Increasing Security of Istio Deployments by Removing the Need for Privileged Containers 随着 1.0 版本的发布，Isti","title":"通过消除对特权容器的需求来提高 Istio Deployment 的安全性","type":"posts"},{"content":"在之前的文章 Istio 服务网格中的网关 中，我已经介绍了简单的暴露 Ingress Gateway 的方案。当时的方案只是用于临时测试，不适合在大规模场景下使用，本文将探讨更加优化的暴露 Ingress Gateway 的方案。\nHostNetwork # 第一种方法比较简单，可以直接使用 HostNetwork 模式运行 Ingress Gateway。但你会发现无法启动 ingressgateway 的 Pod，因为如果 Pod 设置了 HostNetwork=true，则 dnsPolicy 就会从 ClusterFirst 被强制转换成 Default。而 Ingress Gateway 启动过程中需要通过 DNS 域名连接 pilot 等其他组件，所以无法启动。\n我们可以通过强制将 dnsPolicy 的值设置为 ClusterFirstWithHostNet 来解决这个问题，详情参考： Kubernetes DNS 高阶指南。\n修改后的 ingressgateway deployment 配置文件如下：\napiVersion: extensions/v1beta1 kind: Deployment metadata: name: istio-ingressgateway namespace: istio-system ... spec: ... template: metadata: ... spec: affinity: nodeAffinity: ... requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - 192.168.123.248 # 比如你想调度到这台主机上 ... dnsPolicy: ClusterFirstWithHostNet hostNetwork: true restartPolicy: Always ... 接下来我们就可以在浏览器中通过 Gateway 的 URL 来访问服务网格中的服务了。\n但是作为服务网格的流量接入层，Ingress Gateway 的高可靠性显得尤为重要，高可靠性首先要解决的就是单点故障问题，一般常用的是采用多副本部署的方式。而上述方案只适用于单实例（Deployment 的副本数为 1）的情况，为了适应多节点部署架构，需要寻求更好的暴露方案。\n使用 Envoy 作为前端代理 # 我们已经知道，Ingress Gateway 实际上内部运行的是 Envoy 代理，我们可以在 Ingress Gateway 前面再加一层代理，这样就解决了高可用问题，你可以将 Ingress Gateway 的副本数扩展为多个，前端代理只需要通过 Service Name 来连接后端的 Gateway 就行了。同时建议采用独占节点的方式部署前端代理，以避免业务应用与前端代理服务发生资源争抢。\n前端代理可以使用一般的负载均衡软件（如 Haproxy、Nginx 等），也可以使用 Envoy。由于 Envoy 是 Istio Service Mesh 中默认的 data plane，所以这里推荐使用 Envoy。\nEnvoy 官方提供了一组 Envoy 的用例，我们只需要用到其中的 Dockerfile。首先克隆 Envoy 的代码仓库并转到 examples/front-proxy 目录：\n$ git clone https://github.com/envoyproxy/envoy $ cd envoy/examples/front-proxy 修改 front-envoy.yaml 配置文件，修改后的内容如下：\nstatic_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.tcp_proxy ① config: stat_prefix: ingress_tcp cluster: ingressgateway access_log: - name: envoy.file_access_log config: path: /dev/stdout - address: socket_address: address: 0.0.0.0 port_value: 443 filter_chains: - filters: - name: envoy.tcp_proxy config: stat_prefix: ingress_tcp cluster: ingressgateway_tls access_log: - name: envoy.file_access_log config: path: /dev/stdout clusters: - name: ingressgateway connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: istio-ingressgateway.istio-system ② port_value: 80 - name: ingressgateway_tls connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: istio-ingressgateway.istio-system port_value: 443 admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8001 ① envoy.tcp_proxy 表示要实例化的过滤器的名称。该名称必须与内置支持的过滤器匹配，也就是说，该字段的值不可随意填写，必须使用指定的几个值。这里 envoy.tcp_proxy 表示使用 TCP 代理。详情参考： listener.Filter ② istio-ingressgateway.istio-system 表示 Ingress Gateway 在集群内部的 DNS 域名。 其他配置解析请参考： Envoy 的架构与基本术语\n接下来通过 Dockerfile-frontenvoy 和 front-envoy.yaml 来构建 Docker 镜像，我们来看下该 Dockerfile 的内容。\nFROM envoyproxy/envoy:latest RUN apt-get update \u0026amp;\u0026amp; apt-get -q install -y \\ curl CMD /usr/local/bin/envoy -c /etc/front-envoy.yaml --service-cluster front-proxy 其中 /etc/front-envoy.yaml 是本地的 front-envoy.yaml 挂载进去的。在 Kubernetes 中可以通过 ConfigMap 来挂载，所以我们还要创建一个 ConfigMap：\n$ kubectl -n istio-system create cm front-envoy --from-file=front-envoy.yaml 你可以将构建好的镜像 push 到私有镜像仓库中或者公共仓库中，也可以使用我已经上传好的镜像。\n最后我们就可以通过该镜像来部署前端代理了，需要创建一个 Deployment，配置文件 front-envoy-deploy.yaml 内容如下：\napiVersion: extensions/v1beta1 kind: Deployment metadata: name: front-envoy spec: replicas: 1 template: metadata: labels: app: front-envoy spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - 192.168.123.248 # 比如你想调度到这台主机上 containers: - name: front-envoy image: yangchuansheng/front-envoy ports: - containerPort: 80 volumeMounts: - name: front-envoy mountPath: /etc/front-envoy.yaml subPath: front-envoy.yaml hostNetwork: true volumes: - name: front-envoy configMap: name: front-envoy 你可以将镜像换成你自己的镜像，然后通过该 yaml 文件来部署：\n$ kubectl -n istio-system create -f front-envoy-deploy.yaml 接下来我们就可以在浏览器中通过前端代理所在节点的 URL 来访问服务网格中的服务了。\n更一般的场景，我们还可以配置前端代理的高可用。对于 Kubernetes 集群以外只暴露一个访问入口，可以使用 keepalived 排除单节点问题。具体实现方式与 Ingress 的高可用类似，可以参考 Ingress 的高可用方案。\n扫一扫关注微信公众号 ","date":"2018年9月17日","externalUrl":null,"permalink":"/posts/expose-gateway-of-istio/","section":"博客","summary":"在之前的文章 Istio 服务网格中的网关 中，我已经介绍了简单的暴露 Ingress Gateway","title":"暴露 Istio Service Mesh 中的 Gateway","type":"posts"},{"content":" 原文链接： Kubernetes DNS setting in your Pod DNS 是 Kubernetes 的核心功能之一，Kubernetes 通过 kube-dns 或 CoreDNS 作为集群的必备扩展来提供命名服务，通过 DNS 扩展，每一个 Service 都会产生一个独一无二的 FQDN（Fully Qualified Domain Name）名称。\n在大多数使用场景下，我们并不会太关心 DNS 插件的内部运作细节，直接使用 Kubernetes 预设的 DNS 配置和策略就可以满足需求。然而随着使用场景越来越复杂，譬如跟 NFV（Network Function Virtualization）相关的场景，我们的应用（Pod）可能就会需要更加个性化的 DNS 配置。\n接下来使用下面这张架构图来说明可能的使用场景：\n为什么需要自定义 DNS # 一般的使用场景下，我们的 Kubernetes 集群的使用方式就像图中**紫色/粉红色（Pod3）**区域一样，所有的 Pod 如果有任何要存取 DNS 的需求，都会透过集群内的的 k8s DNS 来处理对应的请求与回复。\n然而在 NFV 的使用场景下，网络变成一个很重要的区域，整体的性能都取决于该应用的设计与集群的网络架构设计。这部分应用通常都会追求高输出或是低延迟，为了得到更好的性能，需要避免这些流量跟其他无关的流量使用相同的网络线路进行传输。\n在这种情况下，通常就会把整个集群的网络设计成两种架构，分别是 Control Network 和 Data Network 这两个不同用途的网络架构。在 Kubernetes 中，Control Network 就类似于图中的 Cluster Network，负责整个集群之间的沟通。图中**绿色/橘色（Pod1，Pod2）**这两个区域就是所谓的 Data Network，其网卡本身也被独立出来，不会与本来的 Kubernetes 集群发生冲突，它们之间的流量通过独立的网络进行传输。\n存在于独立出来的网络架构中的这些特殊的 Pod 基本上没法跟 Kubernetes 集群内的 DNS 互连，而且这些应用还有可能在外部有自己的 DNS Server，所以在这种场景下，我们希望这些应用（Pod1/Pod2）能够使用自定义的 DNS Server。\n如何自定义 DNS # 为了让用户更容易控制 Pod 中的 DNS 设置，Kubernetes v1.9 引入了一项新的 Alpha 特性（在 v1.10 中处于 Beta 阶段）。该特性在 v1.10 中被默认启用，在 v1.9 中如果想要启用此功能，集群管理员需要在 apiserver 和 kubelet 上启用 CustomPodDNS 特性，例如：“--feature-gates=CustomPodDNS=true,...”。启用了该特性之后，用户可以将 Pod 的 dnsPolicy 字段设置为 \u0026quot;None\u0026quot;，并且可以在 Pod.Spec 中添加新的字段 dnsConfig。\n其中 dnsConfig 用来自定义 DNS 参数，而 dnsPolicy 用来给 Pod 选取预设的 DNS。接下来就看看可以通过哪些手段自定义 DNS。\ndnsConfig # dnsConfig 可以让操作者延伸到 Pod 内部关于 DNS 的配置，这边需要特别注意的是，我使用的字眼是 延伸 而不是 配置，这是因为通过下一节的 dnsPolicy，每个 Pod 都会有一组预设的 DNS 配置。通过 dnsConfig 我们可以继续往上叠加相关的 DNS 参数到 Pod 之中。\n目前总共支持三个参数，分别是：\nnameservers searches options 这三个参数对应的就是大家熟悉的 /etc/resolv.conf 里面的三个参数，这里就不针对 DNS 进行详细解释了，不熟悉的朋友可以自行去 Google 学一下这些参数的意思。\n在 Kubernetes 里面，这三个参数都包含在 dnsConfig 配置项中，而 dnsConfig 包含在 PodSpec 配置项中，因为 Pod 内所有的容器都共享相同的 Network Namespace，所以网络方面的配置都会共享。\n这边提供一个简单的 yaml 示例：\napiVersion: v1 kind: Pod metadata: name: ubuntu-setting namespace: default spec: containers: - image: hwchiu/netutils command: - sleep - \u0026#34;360000\u0026#34; imagePullPolicy: IfNotPresent name: ubuntu restartPolicy: Always dnsConfig: nameservers: - 1.2.3.4 searches: - ns1.svc.cluster.local - my.dns.search.suffix options: - name: ndots value: \u0026#34;2\u0026#34; - name: edns0 通过上述 yaml 创建 Pod 之后，通过下面的命令可以观察到容器中 DNS 配置文件中会出现额外的配置。\n$ kubectl exec ubuntu-setting cat /etc/resolv.conf nameserver 10.254.0.2 nameserver 1.2.3.4 search default.svc.cluster.local svc.cluster.local cluster.local ns1.svc.cluster.local my.dns.search.suffix options ndots:2 edns0 可以看到 nameserver 多了一个 1.2.3.4，而 search 则多了 ns1.svc.cluster.local my.dns.search.suffix 这两个自定义的值，最后 options 则增加了我们示例中指定的 ndots:2 edns0。\ndnsConfig 非常简单直观，如果你需要自定义 DNS 参数，就可以通过这个字段来指定。\ndnsPolicy # 前面提过，dnsConfig 提供的是延伸 Pod 内预设的 DNS 配置，而 dnsPolicy 就是决定 Pod 内预设的 DNS 配置有哪些。\n目前总共有四个类型可以选择：\nNone Default ClusterFirst ClusterFirstHostNet 接下来针对这四个类型分别介绍。\nNone\nNone 表示会清除 Pod 预设的 DNS 配置，当 dnsPolicy 设置成这个值之后，Kubernetes 不会为 Pod 预先载入任何自身逻辑判断得到的 DNS 配置。因此若要将 dnsPolicy 的值设为 None，为了避免 Pod 里面没有配置任何 DNS，最好再添加 dnsConfig 来描述自定义的 DNS 参数。\n使用下面的示例来进行测试：\napiVersion: v1 kind: Pod metadata: name: ubuntu-none namespace: default spec: containers: - image: hwchiu/netutils command: - sleep - \u0026#34;360000\u0026#34; imagePullPolicy: IfNotPresent name: ubuntu restartPolicy: Always dnsPolicy: None dnsConfig: nameservers: - 1.2.3.4 searches: - ns1.svc.cluster.local - my.dns.search.suffix options: - name: ndots value: \u0026#34;2\u0026#34; - name: edns0 通过上述 yaml 创建 Pod 之后，通过下面的命令可以观察容器中的 DNS 配置文件，可以观察到跟之前的 dnsConfig 的结果有一点差异，这里只有我们在 yaml 里配置的那些参数，而没有加入集群预设的 DNS 配置。\n$ kubectl exec ubuntu-none cat /etc/resolv.conf nameserver 1.2.3.4 search ns1.svc.cluster.local my.dns.search.suffix options ndots:2 edns0 Default\nDefault 表示 Pod 里面的 DNS 配置继承了宿主机上的 DNS 配置。简单来说，就是该 Pod 的 DNS 配置会跟宿主机完全一致。\n使用下面的示例来进行测试：\napiVersion: v1 kind: Pod metadata: name: ubuntu-default namespace: default spec: containers: - image: hwchiu/netutils command: - sleep - \u0026#34;360000\u0026#34; imagePullPolicy: IfNotPresent name: ubuntu restartPolicy: Always dnsPolicy: Default 首先，我们先观察 Node 上面的 DNS 配置：\n$ cat /etc/resolv.conf # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN nameserver 10.0.2.3 可以观察到，Node 上面的 DNS 配置得很简单，只有单纯的 10.0.2.3。\n接下来我们观察该 Pod 内的 DNS 配置：\n$ kubectl exec ubuntu-default cat /etc/resolv.conf nameserver 10.0.2.3 可以看到这两个的 DNS 配置完全一致，该 Pod 内的 DNS 配置已经直接继承 Node 上面的配置了。\nClusterFirst\n相对于上述的 Default，ClusterFirst 是完全相反的操作，它会预先把 kube-dns（或 CoreDNS）的信息当作预设参数写入到该 Pod 内的 DNS 配置。\nClusterFirst 是预设的行为，若没有在 Pod 內特別描述 PodPolicy, 则会将 dnsPolicy 预设为 ClusterFirst。 使用下面的示例来进行测试：\napiVersion: v1 kind: Pod metadata: name: ubuntu-clusterfirst namespace: default spec: containers: - image: hwchiu/netutils command: - sleep - \u0026#34;360000\u0026#34; imagePullPolicy: IfNotPresent name: ubuntu restartPolicy: Always dnsPolicy: ClusterFirst 通过上述 yaml 创建 Pod 之后，通过下面的命令观察容器中的 DNS 配置文件：\n$ kubectl exec ubuntu-clusterfirst cat /etc/resolv.conf nameserver 10.254.0.2 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5 可以看到这里使用的是 k8s DNS 的设置。\n此外，ClusterFirst 还有一个冲突，如果你的 Pod 设置了 HostNetwork=true，则 ClusterFirst 就会被强制转换成 Default。\nHostNetwork\n使用下面的示例来进行测试：\napiVersion: v1 kind: Pod metadata: name: ubuntu-hostnetwork-policy-default namespace: default spec: containers: - image: hwchiu/netutils command: - sleep - \u0026#34;360000\u0026#34; imagePullPolicy: IfNotPresent name: ubuntu hostNetwork: true restartPolicy: Always dnsPolicy: ClusterFirst 通过上述 yaml 创建 Pod 之后，通过下面的命令观察容器中的 DNS 配置文件：\n$ kubectl exec ubuntu-hostnetwork-policy-default cat /etc/resolv.conf nameserver 10.0.2.3 可以观察到，Pod 里面的 DNS 配置直接继承了宿主机上的 DNS 配置。\n这边稍微来解释一下这个设计上的原理以及流程：\n因为设置了 HostNetwork=true, 会让该 Pod 与该节点共用相同的网路空间(网卡/路由等功能)。 预设的 k8s DNS 是使用 ClusterIP 的 kubernetes serivce. 这种情况下，只有属于 Cluster 內的 Pod 可以获取该 ClusterIP。 所以设置了 HostNetwork=true 的 Pod 就没有办法获取该 ClusterIP。 于是预设就会将对应的 DNS 配置改回 Default 的形式，从节点继承其 DNS 配置信息。 这种情况下，就会有人想要问，如果我刻意想要这样设置不行吗？\n原先的设计中，是没有办法刻意处理的，原因是当 Pod yaml 配置文件被发送出去后，在发现没有设定 dnsPolicy 的情况下，会自动帮你把该 dnsPolicy 补上 ClusterFirst 的数值。\n然后最后面的程序处理逻辑中，其实并没有办法分別下列两种情况：\nHostNetwork：我希望走 Host DNS HostNetwork \u0026amp; dnsPolicy=ClusterFirst：我希望走 ClusterIP DNS 上述两种情况对于后端的程序来看都长得一样，完全没有办法分辨，我们可以直接从 Kubernetes 源码 来阅读一下其运作流程：\nfunc getPodDNSType(pod *v1.Pod) (podDNSType, error) { dnsPolicy := pod.Spec.DNSPolicy switch dnsPolicy { case v1.DNSNone: if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) { return podDNSNone, nil } // This should not happen as kube-apiserver should have rejected // setting dnsPolicy to DNSNone when feature gate is disabled. return podDNSCluster, fmt.Errorf(fmt.Sprintf(\u0026#34;invalid DNSPolicy=%v: custom pod DNS is disabled\u0026#34;, dnsPolicy)) case v1.DNSClusterFirstWithHostNet: return podDNSCluster, nil case v1.DNSClusterFirst: if !kubecontainer.IsHostNetworkPod(pod) { return podDNSCluster, nil } // Fallback to DNSDefault for pod on hostnetowrk. fallthrough case v1.DNSDefault: return podDNSHost, nil } // This should not happen as kube-apiserver should have rejected // invalid dnsPolicy. return podDNSCluster, fmt.Errorf(fmt.Sprintf(\u0026#34;invalid DNSPolicy=%v\u0026#34;, dnsPolicy)) } 这边可以看到一旦是 DNSClusterFirst 的情况下，若设置了 HostNetwork, 最后就会直节回传 podDNSHost 节点的 DNS 设定回去。\n为了解决上述的问题，所以引进了一个新的类型 ClusterFirstHostNet。\nClusterFirstWithHostNet\nClusterFirstWithHostNet 用途非常简单，我希望满足使用 HostNetwork 同时使用 k8s DNS 作为我 Pod 预设 DNS 的配置。\n根据上面的源码也可以观察到：\ncase v1.DNSClusterFirstWithHostNet: return podDNSCluster, nil 只要将 dnsPolicy 设置为 ClusterFirstWithHostNet, 就会一律返回 k8s DNS 的 clusterIP 这种形式。\n使用下面的示例来进行测试：\napiVersion: v1 kind: Pod metadata: name: ubuntu-hostnetwork-policy namespace: default spec: containers: - image: hwchiu/netutils command: - sleep - \u0026#34;360000\u0026#34; imagePullPolicy: IfNotPresent name: ubuntu hostNetwork: true restartPolicy: Always dnsPolicy: ClusterFirstWithHostNet 通过上述 yaml 创建 Pod 之后，通过下面的命令观察该 Pod 的状态：\n$ kubectl exec ubuntu-hostnetwork-policy cat /etc/resolv.conf nameserver 10.254.0.2 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5 可以发现这时候的 DNS 就会配置成 k8s DNS 的 ClusterIP 了。\n扫一扫关注微信公众号 ","date":"2018年8月27日","externalUrl":null,"permalink":"/posts/kubernetes-dns/","section":"博客","summary":"原文链接： Kubernetes DNS setting in your Pod DNS 是 Kubernetes 的核心功能之一，Kubernet","title":"Kubernetes DNS 高阶指南","type":"posts"},{"content":" 本文主要内容来自 Istio 官方文档，并对其进行了大量扩展和补充。\n缺省情况下，Istio 服务网格内的 Pod，由于其 iptables 将所有外发流量都透明的转发给了 Sidecar，所以这些集群内的服务无法访问集群之外的 URL，而只能处理集群内部的目标。\n本文的任务描述了如何将外部服务暴露给 Istio 集群中的客户端。你将会学到如何通过定义 ServiceEntry 来调用外部服务；或者简单的对 Istio 进行配置，要求其直接放行对特定 IP 范围的访问。\n开始之前 # 根据 安装指南的内容，部署 Istio。 启动 sleep 示例应用，我们将会使用这一应用来完成对外部服务的调用过程。 如果启用了 Sidecar 的自动注入功能，运行： $ kubectl apply -f samples/sleep/sleep.yaml 否则在部署 sleep 应用之前，就需要手工注入 Sidecar：\n$ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/sleep/sleep.yaml) 实际上任何可以 exec 和 curl 的 Pod 都可以用来完成这一任务。\nIstio 中配置外部服务 # 通过配置 Istio ServiceEntry，可以从 Istio 集群中访问外部任意的可用服务。这里我们会使用 httpbin.org 以及 www.baidu.com 进行试验。\n配置外部服务 # 1. 创建一个 ServiceEntry 对象，放行对一个外部 HTTP 服务的访问：\n$ cat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: httpbin-ext spec: hosts: - httpbin.org ports: - number: 80 name: http protocol: HTTP EOF 2. 另外创建一个 ServiceEntry 对象和一个 VirtualService，放行对一个外部 HTTPS 服务的访问：\n$ cat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: baidu spec: hosts: - www.baidu.com ports: - number: 443 name: https protocol: HTTPS resolution: DNS --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: baidu spec: hosts: - www.baidu.com tls: - match: - port: 443 sniHosts: - www.baidu.com route: - destination: host: www.baidu.com port: number: 443 weight: 100 EOF 发起对外部服务的访问 # 使用 kubectl exec 命令进入测试 Pod。假设使用的是 sleep 服务，运行如下命令：\n$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o go-template=\u0026#39;{{range .items}}{{.metadata.name}}{{end}}\u0026#39;) $ kubectl exec -it $SOURCE_POD -c sleep bash 发起一个对外部 HTTP 服务的请求：\n$ curl http://httpbin.org/headers 发起一个对外部 HTTPS 服务的请求：\n$ curl https://www.baidu.com HTTP ServiceEntry 配置深度解析 # 按照之前的惯例，还是先来解读一下 HTTP 协议的 ServiceEntry 映射到 Envoy 配置层面具体是哪些内容，这样才能对 ServiceEntry 有更加深刻的认识。\n创建一个 HTTP 协议的 ServiceEntry（不指定 GateWay） 本质上是在服务网格内的所有应用的所有 Pod上创建相应的路由规则和与之对应的 Cluster。指定 GateWay 的 ServiceEntry 遵循的是另一套法则，后面我们再说。 可以通过 istioctl 来验证一下（以 httpbin-ext 为例）：\n# 查看 sleep 的 Pod Name： $ kubectl get pod -l app=sleep NAME READY STATUS RESTARTS AGE sleep-5bc866558c-89shb 2/2 Running 0 49m 查看路由\n$ istioctl pc routes sleep-5bc866558c-89shb --name 80 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;80\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;httpbin.org:80\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;httpbin.org\u0026#34;, \u0026#34;httpbin.org:80\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|80||httpbin.org\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;maxGrpcTimeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, \u0026#34;decorator\u0026#34;: { \u0026#34;operation\u0026#34;: \u0026#34;httpbin.org:80/*\u0026#34; }, ... 可以看到从 Pod sleep-5bc866558c-89shb 内部对域名 httpbin.org 发起的请求通过 HTTP 路由被定向到集群 outbound|80||httpbin.org。outbound 表示这是出站流量\n查看 Cluster：\n$ istioctl pc clusters sleep-5bc866558c-89shb --fqdn httpbin.org -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|80||httpbin.org\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;ORIGINAL_DST\u0026#34;, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;lbPolicy\u0026#34;: \u0026#34;ORIGINAL_DST_LB\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] } } ] type : 服务发现类型。ORIGINAL_DST 表示原始目的地类型，大概意思就是：连接进入之前已经被解析为一个特定的目标 IP 地址。这种连接通常是由代理使用 IP table REDIRECT 或者 eBPF 之类的机制转发而来的。完成路由相关的转换之后，代理服务器会将连接转发到该 IP 地址。httpbin.org 是外网域名，当然可以解析，所以连接进入之前可以被解析为一个特定的目标 IP 地址。Envoy 服务发现类型的详细解析可以参考： Service discovery。ServiceEntry.Resolution 字段的解析可以参考： ServiceEntry.Resolution。\n这里我简要说明一下，ServiceEntry 的 resolution 字段可以取三个不同的值，分别对应 Envoy 中的三种服务发现策略：\nNONE : 对应于 Envoy 中的 ORIGINAL_DST。如果不指定 resolution 字段，默认使用这个策略。 STATIC : 对应于 Envoy 中的 STATIC。表示使用 endpoints 中指定的静态 IP 地址作为服务后端。 DNS : 对应于 Envoy 中的 STRICT_DNS。表示处理请求时尝试向 DNS 查询 IP 地址。如果没有指定 endpoints，并且没有使用通配符，代理服务器会使用 DNS 解析 hosts 字段中的地址。如果指定了 endpoints，那么指定的地址就会作为目标 IP 地址。 lbPolicy : 负载均衡策略。ORIGINAL_DST_LB 表示使用原始目的地的负载均衡策略。具体参考: Load balancing。\n如果你还部署了 bookinfo 示例应用，可以通过执行 istioctl pc routes \u0026lt;productpage_pod_name\u0026gt; --name 80 -o json 和 istioctl pc clusters \u0026lt;productpage_pod_name\u0026gt; --fqdn httpbin.org -o json 来验证一下，你会发现输出的结果和上面一模一样。如果还不放心，可以查看 bookinfo 应用内的所有 Pod，你会得到相同的答案。至此你应该可以理解在服务网格内的所有应用的所有 Pod上创建相应的路由规则和与之对应的 Cluster这句话的含义了。\nHTTPS ServiceEntry 配置深度解析 # HTTPS 协议的 ServiceEntry 与 Envoy 配置文件的映射关系与 HTTP 协议有所不同。\n创建一个 HTTPS 协议的 ServiceEntry（不指定 GateWay） 本质上是在服务网格内的所有应用的所有 Pod上创建相应的监听器和与之对应的 Cluster。指定 GateWay 的 ServiceEntry 我会另行发文详说。 可以通过 istioctl 来验证（以 baidu 为例）。为了更精确地分析该 ServiceEntry，可以先把 VirtualService 删除：\n$ istioctl delete virtualservice baidu 查看监听器：\n$ istioctl pc listeners sleep-5bc866558c-89shb --address 0.0.0.0 --port 443 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_443\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;portValue\u0026#34;: 443 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ ... { \u0026#34;name\u0026#34;: \u0026#34;envoy.tcp_proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|443||www.baidu.com\u0026#34;, \u0026#34;stat_prefix\u0026#34;: \u0026#34;outbound|443||www.baidu.com\u0026#34; } } ... name : 监听器过滤器的名称。该字段的值必须与 Envoy 所支持的过滤器匹配，不可随意填写，具体参考： listener.Filter。此处 envoy.tcp_proxy 表示使用 TCP 代理，而 TCP 代理是无法基于路由过滤的，所以这里不会创建路由规则，而是直接将请求转到 Cluster。 查看 Cluster：\n$ istioctl pc clusters sleep-5bc866558c-89shb --fqdn www.baidu.com -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|443||www.baidu.com\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;www.baidu.com\u0026#34;, \u0026#34;portValue\u0026#34;: 443 } } ], \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] }, \u0026#34;dnsLookupFamily\u0026#34;: \u0026#34;V4_ONLY\u0026#34; } ] 从监听器的配置来看，由于绑定的是 0.0.0.0，而且也没有指定域名，看起来应该可以访问集群外任何 443 端口的服务。实际上这是行不通的，因为当请求通过监听器转到 Cluster 之后，由于 Cluster 采用的是严格的 DNS 服务发现策略，只要域名不是 www.baidu.com，都不会解析。你可以使用 kubectl exec 命令进入 sleep Pod 来测试一下：\n$ kubectl exec -it $SOURCE_POD -c sleep bash 发起对外部 HTTPS 服务的请求：\n$ curl https://www.163.com curl: (51) SSL: no alternative certificate subject name matches target host name \u0026#39;www.163.com\u0026#39; $ curl https://www.taobao.com curl: (51) SSL: no alternative certificate subject name matches target host name \u0026#39;www.taobao.com\u0026#39; $ curl https://192.192.192.192 curl: (51) SSL: certificate subject name \u0026#39;baidu.com\u0026#39; does not match target host name \u0026#39;192.192.192.192\u0026#39; 而如果你将服务发现策略改为 NONE，就会发现除了可以访问 www.baidu.com，还可以访问 www.163.com 和 www.taobao.com 等其他 https 协议的网站，至于为什么会这样，前面介绍服务发现策略的时候我已经详细解释过了。\nTLS VirtualService 配置深度解析 # 关于 VirtualService 的解析之前的文章已有相关说明，不过这里的 VirtualService 与之前遇到的不同，涉及到了 TLSRoute。\ntls : 透传 TLS 和 HTTPS 流量。TLS 路由通常应用在 https-、tls- 前缀的平台服务端口，或者经 Gateway 透传的 HTTPS、TLS 协议 端口，以及使用 HTTPS 或者 TLS 协议的 ServiceEntry 端口上。具体参考： TLSRoute。 sniHosts : 必要字段。要匹配的 SNI（服务器名称指示）。可以在 SNI 匹配值中使用通配符。比如 *.com 可以同时匹配 foo.example.com 和 example.com。 route : 流量的转发目标。目前 TLS 服务只允许一个转发目标(所以权重必须设置为 100)。当 Envoy 支持 TCP 权重路由之后，这里就可以使用多个目标了。 查看映射到 Envoy 中的配置：\n$ istioctl pc listeners sleep-5bc866558c-89shb --address 0.0.0.0 --port 443 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_443\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;portValue\u0026#34;: 443 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filterChainMatch\u0026#34;: { \u0026#34;serverNames\u0026#34;: [ \u0026#34;www.baidu.com\u0026#34; ] }, \u0026#34;filters\u0026#34;: [ ... { \u0026#34;name\u0026#34;: \u0026#34;envoy.tcp_proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|443||www.baidu.com\u0026#34;, \u0026#34;stat_prefix\u0026#34;: \u0026#34;outbound|443||www.baidu.com\u0026#34; } } ... filterChainMatch : 用于为监听器过滤器链指定匹配条件，具体参考： listener.FilterChainMatch。 最后我们来思考一下：既然不创建 TLS VirtualService 也可以访问 www.baidu.com，那么创建 TLS VirtualService 和不创建 TLS VirtualService 有什么区别呢？正确答案是：没有关联 VirtualService 的 https- 或者 tls- 端口流量会被视为透传 TCP 流量，而不是透传 TLS 和 HTTPS 流量。\n为外部服务设置路由规则 # 通过 ServiceEntry 访问外部服务的流量，和网格内流量类似，都可以进行 Istio 路由规则 的配置。下面我们使用 istioctl 为 httpbin.org 服务设置一个超时规则。\n1. 在测试 Pod 内部，调用 httpbin.org 这一外部服务的 /delay 端点：\n$ kubectl exec -it $SOURCE_POD -c sleep bash $ time curl -o /dev/null -s -w \u0026#34;%{http_code}\\n\u0026#34; http://httpbin.org/delay/5 200 real 0m5.024s user 0m0.003s sys 0m0.003s 这个请求会在大概五秒钟左右返回一个内容为 200 (OK) 的响应。\n2. 退出测试 Pod，使用 istioctl 为 httpbin.org 外部服务的访问设置一个 3 秒钟的超时：\ncat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: httpbin-ext spec: hosts: - httpbin.org http: - timeout: 3s route: - destination: host: httpbin.org weight: 100 EOF 3. 等待几秒钟之后，再次发起 curl 请求：\n$ kubectl exec -it $SOURCE_POD -c sleep bash $ time curl -o /dev/null -s -w \u0026#34;%{http_code}\\n\u0026#34; http://httpbin.org/delay/5 504 real 0m3.149s user 0m0.004s sys 0m0.004s 这一次会在 3 秒钟之后收到一个内容为 504 (Gateway Timeout) 的响应。虽然 httpbin.org 还在等待他的 5 秒钟，Istio 却在 3 秒钟的时候切断了请求。\n直接调用外部服务 # 如果想要跳过 Istio，直接访问某个 IP 范围内的外部服务，就需要对 Envoy sidecar 进行配置，阻止 Envoy 对外部请求的 劫持。可以在 Helm 中设置 global.proxy.includeIPRanges 变量，然后使用 kubectl apply 命令来更新名为 istio-sidecar-injector 的 Configmap。在 istio-sidecar-injector 更新之后，global.proxy.includeIPRanges 会在所有未来部署的 Pod 中生效。\n使用 global.proxy.includeIPRanges 变量的最简单方式就是把内部服务的 IP 地址范围传递给它，这样就在 Sidecar proxy 的重定向列表中排除掉了外部服务的地址了。\n内部服务的 IP 范围取决于集群的部署情况。例如你的集群中这一范围是 10.0.0.1/24，这个配置中，就应该这样更新 istio-sidecar-injector：\n$ helm template install/kubernetes/helm/istio \u0026lt;安装 Istio 时所使用的参数\u0026gt; --set global.proxy.includeIPRanges=\u0026#34;10.0.0.1/24\u0026#34; -x templates/sidecar-injector-configmap.yaml | kubectl apply -f - 注意这里应该使用和之前部署 Istio 的时候同样的 Helm 命令，尤其是 \u0026ndash;namespace 参数。在安装 Istio 原有命令的基础之上，加入 --set global.proxy.includeIPRanges=\u0026quot;10.0.0.1/24\u0026quot; -x templates/sidecar-injector-configmap.yaml 即可。\n然后和前面一样，重新部署 sleep 应用。更新了 ConfigMap istio-sidecar-injector 并且重新部署了 sleep 应用之后，Istio sidecar 就应该只劫持和管理集群内部的请求了。任意的外部请求都会简单的绕过 Sidecar，直接访问目的地址。\n$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o go-template=\u0026#39;{{range .items}}{{.metadata.name}}{{end}}\u0026#39;) $ kubectl exec -it $SOURCE_POD -c sleep curl http://httpbin.org/headers 总结 # 这个任务中，我们使用两种方式从 Istio 服务网格内部来完成对外部服务的调用：\n使用 ServiceEntry (推荐方式) 配置 Istio sidecar，从它的重定向 IP 表中排除外部服务的 IP 范围 第一种方式（ServiceEntry）中，网格内部的服务不论是访问内部还是外部的服务，都可以使用同样的 Istio 服务网格的特性。我们通过为外部服务访问设置超时规则的例子，来证实了这一优势。\n第二种方式越过了 Istio sidecar proxy，让服务直接访问到对应的外部地址。然而要进行这种配置，需要了解云供应商特定的知识和配置。\n清理 # 1. 删除规则：\n$ istioctl delete serviceentry httpbin-ext baidu $ istioctl delete virtualservice httpbin-ext baidu 2. 停止 sleep 服务：\n$ kubectl delete -f samples/sleep/sleep.yaml ","date":"2018年8月16日","externalUrl":null,"permalink":"/posts/control-egress-traffic/","section":"博客","summary":"本文主要内容来自 Istio 官方文档，并对其进行了大量扩展和补充。 缺省","title":"控制 Egress 流量","type":"posts"},{"content":"书接前文，上文我们通过跟踪集群外通过 ingressgateway 发起的请求来探寻流量在 Istio 服务网格之间的流动方向，先部署 bookinfo 示例应用，然后创建一个监听在 ingressgateway 上的 GateWay 和 VirtualService，通过分析我们追踪到请求最后转交给了 productpage。\n在继续追踪请求之前，先对之前的内容做一个补充说明。\nPod 在服务网格之间如何通信？ # 大家都知道，在 Istio 尚未出现之前，Kubernetes 集群内部 Pod 之间是通过 ClusterIP 来进行通信的，那么通过 Istio 在 Pod 内部插入了 Sidecar 之后，微服务应用之间是否仍然还是通过 ClusterIP 来通信呢？我们来一探究竟！\n继续拿上文的步骤举例子，来看一下 ingressgateway 和 productpage 之间如何通信，请求通过 ingressgateway 到达了 endpoint ，那么这个 endpoint 到底是 ClusterIP + Port 还是 PodIP + Port 呢？由于 istioctl 没有提供 eds 的查看参数，可以通过 pilot 的 xds debug 接口来查看：\n# 获取 istio-pilot 的 ClusterIP $ export PILOT_SVC_IP=$(kubectl -n istio-system get svc -l app=istio-pilot -o go-template=\u0026#39;{{range .items}}{{.spec.clusterIP}}{{end}}\u0026#39;) # 查看 eds $ curl http://$PILOT_SVC_IP:8080/debug/edsz|grep \u0026#34;outbound|9080||productpage.default.svc.cluster.local\u0026#34; -A 27 -B 1 { \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|9080||productpage.default.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;lbEndpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;172.30.135.40\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } }, \u0026#34;metadata\u0026#34;: { \u0026#34;filterMetadata\u0026#34;: { \u0026#34;istio\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;kubernetes://productpage-v1-76474f6fb7-pmglr.default\u0026#34; } } } } ] } ] }, 从这里可以看出，各个微服务之间是直接通过 PodIP + Port 来通信的，Service 只是做一个逻辑关联用来定位 Pod，实际通信的时候并没有通过 Service。\n部署 bookinfo 应用的时候发生了什么？ # 通过 Istio 来部署 bookinfo 示例应用时，Istio 会向应用程序的所有 Pod 中注入 Envoy 容器。但是我们仍然还不清楚注入的 Envoy 容器的配置文件里都有哪些东西，这时候就是 istioctl 命令行工具发挥强大功效的时候了，可以通过 proxy-config 参数来深度解析 Envoy 的配置文件（上一节我们已经使用过了）。\n我们先把目光锁定在某一个固定的 Pod 上，以 productpage 为例。先查看 productpage 的 Pod Name：\n$ kubectl get pod -l app=productpage NAME READY STATUS RESTARTS AGE productpage-v1-76474f6fb7-pmglr 2/2 Running 0 7h 1. 查看 productpage 的监听器的基本基本摘要\n$ istioctl proxy-config listeners productpage-v1-76474f6fb7-pmglr ADDRESS PORT TYPE 172.30.135.40 9080 HTTP // ③ Receives all inbound traffic on 9080 from listener `0.0.0.0_15001` 10.254.223.255 15011 TCP \u0026lt;---+ 10.254.85.22 20001 TCP | 10.254.149.167 443 TCP | 10.254.14.157 42422 TCP | 10.254.238.17 9090 TCP | ② Receives outbound non-HTTP traffic for relevant IP:PORT pair from listener `0.0.0.0_15001` 10.254.184.32 5556 TCP | 10.254.0.1 443 TCP | 10.254.52.199 8080 TCP | 10.254.118.224 443 TCP \u0026lt;---+ 0.0.0.0 15031 HTTP \u0026lt;--+ 0.0.0.0 15004 HTTP | 0.0.0.0 9093 HTTP | 0.0.0.0 15030 HTTP | 0.0.0.0 8080 HTTP | ④ Receives outbound HTTP traffic for relevant port from listener `0.0.0.0_15001` 0.0.0.0 8086 HTTP | 0.0.0.0 9080 HTTP | 0.0.0.0 15010 HTTP \u0026lt;--+ 0.0.0.0 15001 TCP // ① Receives all inbound and outbound traffic to the pod from IP tables and hands over to virtual listener Istio 会生成以下的监听器：\n① 0.0.0.0:15001 上的监听器接收进出 Pod 的所有流量，然后将请求移交给虚拟监听器。 ② 每个 Service IP 配置一个虚拟监听器，每个出站 TCP/HTTPS 流量一个非 HTTP 监听器。 ③ 每个 Pod 入站流量暴露的端口配置一个虚拟监听器。 ④ 每个出站 HTTP 流量的 HTTP 0.0.0.0 端口配置一个虚拟监听器。 上一节提到服务网格之间的应用是直接通过 PodIP 来进行通信的，但还不知道服务网格内的应用与服务网格外的应用是如何通信的。大家应该可以猜到，这个秘密就隐藏在 Service IP 的虚拟监听器中，以 kube-dns 为例，查看 productpage 如何与 kube-dns 进行通信：\n$ istioctl proxy-config listeners productpage-v1-76474f6fb7-pmglr --address 10.254.0.2 --port 53 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;10.254.0.2_53\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;10.254.0.2\u0026#34;, \u0026#34;portValue\u0026#34;: 53 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ ... { \u0026#34;name\u0026#34;: \u0026#34;envoy.tcp_proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|53||kube-dns.kube-system.svc.cluster.local\u0026#34;, \u0026#34;stat_prefix\u0026#34;: \u0026#34;outbound|53||kube-dns.kube-system.svc.cluster.local\u0026#34; } } ] } ], \u0026#34;deprecatedV1\u0026#34;: { \u0026#34;bindToPort\u0026#34;: false } } ] # 查看 eds $ curl http://$PILOT_SVC_IP:8080/debug/edsz|grep \u0026#34;outbound|53||kube-dns.kube-system.svc.cluster.local\u0026#34; -A 27 -B 1 { \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|53||kube-dns.kube-system.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;lbEndpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;172.30.135.21\u0026#34;, \u0026#34;portValue\u0026#34;: 53 } } }, \u0026#34;metadata\u0026#34;: { \u0026#34;filterMetadata\u0026#34;: { \u0026#34;istio\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;kubernetes://coredns-64b597b598-4rstj.kube-system\u0026#34; } } } } ] }, 可以看出，服务网格内的应用仍然通过 ClusterIP 与网格外的应用通信，但有一点需要注意 :** 这里并没有 kube-proxy 的参与！**Envoy 自己实现了一套流量转发机制，当你访问 ClusterIP 时，Envoy 就把流量转发到具体的 Pod 上去，不需要借助 kube-proxy 的 iptables 或 ipvs 规则。\n2. 从上面的摘要中可以看出，每个 Sidecar 都有一个绑定到 0.0.0.0:15001 的监听器，IP tables 将 pod 的所有入站和出站流量路由到这里。此监听器把 useOriginalDst 设置为 true，这意味着它将请求交给最符合请求原始目标的监听器。如果找不到任何匹配的虚拟监听器，它会将请求发送给返回 404 的 BlackHoleCluster。\n$ istioctl proxy-config listeners productpage-v1-76474f6fb7-pmglr --port 15001 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;virtual\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;portValue\u0026#34;: 15001 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.tcp_proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;BlackHoleCluster\u0026#34;, \u0026#34;stat_prefix\u0026#34;: \u0026#34;BlackHoleCluster\u0026#34; } } ] } ], \u0026#34;useOriginalDst\u0026#34;: true } ] 3. 我们的请求是到 9080 端口的 HTTP 出站请求，这意味着它被切换到 0.0.0.0:9080 虚拟监听器。然后，此监听器在其配置的 RDS 中查找路由配置。在这种情况下，它将查找由 Pilot 配置的 RDS 中的路由 9080（通过 ADS）。\n$ istioctl proxy-config listeners productpage-v1-76474f6fb7-pmglr --address 0.0.0.0 --port 9080 -o json ... \u0026#34;rds\u0026#34;: { \u0026#34;config_source\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;route_config_name\u0026#34;: \u0026#34;9080\u0026#34; } ... 4. 9080 路由配置仅为每个服务提供虚拟主机。我们的请求正在前往 reviews 服务，因此 Envoy 将选择我们的请求与域匹配的虚拟主机。一旦在域上匹配，Envoy 会查找与请求匹配的第一条路径。在这种情况下，我们没有任何高级路由，因此只有一条路由匹配所有内容。这条路由告诉 Envoy 将请求发送到 outbound|9080||reviews.default.svc.cluster.local 集群。\n$ istioctl proxy-config routes productpage-v1-76474f6fb7-pmglr --name 9080 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;9080\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;reviews.default.svc.cluster.local:9080\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;reviews.default.svc.cluster.local\u0026#34;, \u0026#34;reviews.default.svc.cluster.local:9080\u0026#34;, \u0026#34;reviews\u0026#34;, \u0026#34;reviews:9080\u0026#34;, \u0026#34;reviews.default.svc.cluster\u0026#34;, \u0026#34;reviews.default.svc.cluster:9080\u0026#34;, \u0026#34;reviews.default.svc\u0026#34;, \u0026#34;reviews.default.svc:9080\u0026#34;, \u0026#34;reviews.default\u0026#34;, \u0026#34;reviews.default:9080\u0026#34;, \u0026#34;172.21.152.34\u0026#34;, \u0026#34;172.21.152.34:9080\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||reviews.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, ... 5. 此集群配置为从 Pilot（通过 ADS）检索关联的端点。因此，Envoy 将使用 serviceName 字段作为密钥来查找端点列表并将请求代理到其中一个端点。\n$ istioctl proxy-config clusters productpage-v1-76474f6fb7-pmglr --fqdn reviews.default.svc.cluster.local -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|9080||reviews.default.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|9080||reviews.default.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] } } ] 上面的整个过程就是在不创建任何规则的情况下请求从 productpage 到 reviews 的过程，从 reviews 到网格内其他应用的流量与上面类似，就不展开讨论了。接下来分析创建规则之后的请求转发过程。\nVirtualService 和 DestinationRule 配置解析 # VirtualService # 首先创建一个 VirtualService。\n$ cat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews http: - route: - destination: host: reviews subset: v1 EOF 上一篇文章已经介绍过，VirtualService 映射的就是 Envoy 中的 Http Route Table，还是将目标锁定在 productpage 上，我们来查看一下路由配置：\n$ istioctl proxy-config routes productpage-v1-76474f6fb7-pmglr --name 9080 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;9080\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;reviews.default.svc.cluster.local:9080\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;reviews.default.svc.cluster.local\u0026#34;, \u0026#34;reviews.default.svc.cluster.local:9080\u0026#34;, \u0026#34;reviews\u0026#34;, \u0026#34;reviews:9080\u0026#34;, \u0026#34;reviews.default.svc.cluster\u0026#34;, \u0026#34;reviews.default.svc.cluster:9080\u0026#34;, \u0026#34;reviews.default.svc\u0026#34;, \u0026#34;reviews.default.svc:9080\u0026#34;, \u0026#34;reviews.default\u0026#34;, \u0026#34;reviews.default:9080\u0026#34;, \u0026#34;172.21.152.34\u0026#34;, \u0026#34;172.21.152.34:9080\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080|v1|reviews.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, ... 注意对比一下没创建 VirtualService 之前的路由，现在路由的 cluster 字段的值已经从之前的 outbound|9080|reviews.default.svc.cluster.local 变为 outbound|9080|v1|reviews.default.svc.cluster.local。\n请注意 : 我们现在还没有创建 DestinationRule！\n你可以尝试搜索一下有没有 outbound|9080|v1|reviews.default.svc.cluster.local 这个集群，如果不出意外，你将找不到 SUBSET=v1 的集群。\n由于找不到这个集群，所以该路由不可达，这就是为什么你打开 productpage 的页面会出现如下的报错：\nDestinationRule # 为了使上面创建的路由可达，我们需要创建一个 DestinationRule：\n$ cat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: reviews spec: host: reviews subsets: - name: v1 labels: version: v1 EOF 其实 DestinationRule 映射到 Envoy 的配置文件中就是 Cluster。现在你应该能看到 SUBSET=v1 的 Cluster 了：\n$ istioctl proxy-config clusters productpage-v1-76474f6fb7-pmglr --fqdn reviews.default.svc.cluster.local --subset=v1 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|9080|v1|reviews.default.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|9080|v1|reviews.default.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] } } ] 到了这一步，一切皆明了，后面的事情就跟之前的套路一样了，具体的 Endpoint 对应打了标签 version=v1 的 Pod：\n$ kubectl get pod -l app=reviews,version=v1 -o wide NAME READY STATUS RESTARTS AGE IP NODE reviews-v1-5b487cc689-njx5t 2/2 Running 0 11h 172.30.104.38 192.168.123.248 $ curl http://$PILOT_SVC_IP:8080/debug/edsz|grep \u0026#34;outbound|9080|v1|reviews.default.svc.cluster.local\u0026#34; -A 27 -B 2 { \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|9080|v1|reviews.default.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;lbEndpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;172.30.104.38\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } }, \u0026#34;metadata\u0026#34;: { \u0026#34;filterMetadata\u0026#34;: { \u0026#34;istio\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;kubernetes://reviews-v1-5b487cc689-njx5t.default\u0026#34; } } } } ] } ] }, 现在再次用浏览器访问 productpage，你会发现报错已经消失了。\n参考 # 调试 Envoy 和 Pilot 扫一扫关注微信公众号 ","date":"2018年8月13日","externalUrl":null,"permalink":"/posts/where-is-the-request-2/","section":"博客","summary":"书接前文，上文我们通过跟踪集群外通过 ingressgateway 发起的请求来探寻流量在","title":"数据包在 Istio 网格中的生命周期（下）","type":"posts"},{"content":"通过前几篇文章的学习与实践，我们对 Gateway、VirtualService 和 Destinationrule 的概念和原理有了初步的认知，本篇将对这几个对象资源的配置文件进行深度地解析，具体细节将会深入到每一个配置项与 Envoy 配置项的映射关系。\n在开始之前，需要先搞清楚我们创建的这些对象资源最后都交给谁来处理了，负责处理这些资源的就是 pilot。\npilot总体架构 # 首先我们回顾一下 pilot 总体架构，上面是 官方关于pilot的架构图，因为是 old_pilot_repo 目录下，可能与最新架构有出入，仅供参考。所谓的 pilot 包含两个组件：pilot-agent 和 pilot-discovery。图里的 agent 对应 pilot-agent 二进制，proxy 对应 Envoy 二进制，它们两个在同一个容器中，discovery service 对应 pilot-discovery 二进制，在另外一个跟应用分开部署的单独的 Deployment 中。\ndiscovery service : 从 Kubernetes apiserver list/watch service、endpoint、pod、node 等资源信息，监听 istio 控制平面配置信息（如VirtualService、DestinationRule等）， 翻译为 Envoy 可以直接理解的配置格式。 proxy : 也就是 Envoy，直接连接 discovery service，间接地从 Kubernetes 等服务注册中心获取集群中微服务的注册情况。 agent : 生成 Envoy 配置文件，管理 Envoy 生命周期。 service A/B : 使用了 Istio 的应用，如 Service A/B，的进出网络流量会被 proxy 接管。 简单来说 Istio 做为管理面，集合了配置中心和服务中心两个功能，并把配置发现和服务发现以一组统一的 xDS 接口提供出来，数据面的 Envoy 通过 xDS 获取需要的信息来做服务间通信和服务治理。\npilot-discovery 为 Envoy 提供的 xds 服务 # 所谓 xds # pilot-discovery 为数据面（运行在 sidecar 中的 Envoy 等 proxy 组件）提供控制信息服务，也就是所谓的 discovery service 或者 xds 服务。这里的 x 是一个代词，类似云计算里的 XaaS 可以指代 IaaS、PaaS、SaaS 等。在 Istio 中，xds 包括 cds(cluster discovery service)、lds(listener discovery service)、rds(route discovery service)、eds(endpoint discovery service)，而 ads(aggregated discovery service) 是对这些服务的一个统一封装。\n以上 cluster、endpoint、route 等概念的详细介绍和实现细节可以参考 Envoy 在社区推广的 data plane api（ github.com/envoyproxy/data-plane-api），这里只做简单介绍：\nendpoint : 一个具体的“应用实例”，对应 ip 和端口号，类似 Kubernetes 中的一个 Pod。 cluster : 一个 cluster 是一个“应用集群”，它对应提供相同服务的一个或多个 endpoint。cluster 类似 Kubernetes 中 Service 的概念，即一个 Kubernetes Service 对应一个或多个用同一镜像启动，提供相同服务的 Pod。 route : 当我们做灰度发布、金丝雀发布时，同一个服务会同时运行多个版本，每个版本对应一个 cluster。这时需要通过 route 规则规定请求如何路由到其中的某个版本的 cluster 上。 以上这些内容实际上都是对 Envoy 等 proxy 的配置信息，而所谓的 cluster discovery service、route discovery service 等 xxx discovery service 就是 Envoy 等从 pilot-discovery 动态获取 endpoint、cluster 等配置信息的协议和实现。为什么要做动态配置加载，自然是为了使用 istioctl 等工具统一、灵活地配置 service mesh。至于如何通过 istioctl 来查看 xds 信息，下文将会详细介绍。\n而为什么要用 ads 来“聚合”一系列 xds，并非仅为了在同一个 gRPC 连接上实现多种 xds 来省下几个网络连接，ads 还有一个非常重要的作用是解决 cds、rds 信息更新顺序依赖的问题，从而保证以一定的顺序同步各类配置信息，这方面的讨论可以详见 Envoy官网。\n如何查看 xds # pilot-discovery 在初始化阶段依次 init 了各种模块，其中 discovery service 就是 xDS 相关实现。 envoy API reference 可以查到 v1 和 v2 两个版本的 API 文档。 envoy control plane 给了 v2 grpc 接口相关的数据结构和接口。\n那么如何查看 xds 的信息呢？虽然 v2 是 grpc 的接口，但是 pilot 提供了 InitDebug，可以通过 debug 接口查询服务和 routes 等服务和配置信息。\n查看 eds\n首先找到 Service istio-pilot 的 Cluster IP：\n$ export PILOT_SVC_IP=$(kubectl -n istio-system get svc istio-pilot -o go-template=\u0026#39;{{.spec.clusterIP}}\u0026#39;) 然后查看 eds：\n$ curl http://$PILOT_SVC_IP:8080/debug/edsz [{ \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|9080||reviews.nino.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [{ \u0026#34;lbEndpoints\u0026#34;: [{ \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;10.244.0.56\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } } }, { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;10.244.0.58\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } } }, { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;10.244.2.25\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } } }] }] }, { \u0026#34;clusterName\u0026#34;: \u0026#34;outbound|9080|v3|reviews.nino.svc.cluster.local\u0026#34;, \u0026#34;endpoints\u0026#34;: [{ \u0026#34;lbEndpoints\u0026#34;: [{ \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;10.244.0.58\u0026#34;, \u0026#34;portValue\u0026#34;: 9080 } } } }] }] }] 查看 cds\n$ curl http://$PILOT_SVC_IP:8080/debug/cdsz [{\u0026#34;node\u0026#34;: \u0026#34;sidecar~172.30.104.45~fortio-deploy-56dcc85457-b2pkc.default~default.svc.cluster.local-10\u0026#34;, \u0026#34;addr\u0026#34;: \u0026#34;172.30.104.45:43876\u0026#34;, \u0026#34;connect\u0026#34;: \u0026#34;2018-08-07 06:31:08.161483005 +0000 UTC m=+54.337448884\u0026#34;,\u0026#34;Clusters\u0026#34;:[{ \u0026#34;name\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: { } }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|9080||details.default.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ { } ] } }, ... { \u0026#34;name\u0026#34;: \u0026#34;outbound|9090||prometheus-k8s.monitoring.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: { } }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|9090||prometheus-k8s.monitoring.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ { } ] } }, { \u0026#34;name\u0026#34;: \u0026#34;BlackHoleCluster\u0026#34;, \u0026#34;connectTimeout\u0026#34;: \u0026#34;5.000s\u0026#34; }]} ] 查看 ads\n$ curl http://$PILOT_SVC_IP:8080/debug/adsz Envoy 基本术语回顾 # 为了让大家更容易理解后面所讲的内容，先来回顾一下 Envoy 的基本术语。\nListener : 监听器（listener）是服务(程序)监听者，就是真正干活的。 它是可以由下游客户端连接的命名网络位置（例如，端口、unix域套接字等）。Envoy 公开一个或多个下游主机连接的侦听器。一般是每台主机运行一个 Envoy，使用单进程运行，但是每个进程中可以启动任意数量的 Listener（监听器），目前只监听 TCP，每个监听器都独立配置一定数量的（L3/L4）网络过滤器。Listenter 也可以通过 Listener Discovery Service（LDS）动态获取。 Listener filter : Listener 使用 listener filter（监听器过滤器）来操作链接的元数据。它的作用是在不更改 Envoy 的核心功能的情况下添加更多的集成功能。Listener filter 的 API 相对简单，因为这些过滤器最终是在新接受的套接字上运行。在链中可以互相衔接以支持更复杂的场景，例如调用速率限制。Envoy 已经包含了多个监听器过滤器。 Http Route Table : HTTP 的路由规则，例如请求的域名，Path 符合什么规则，转发给哪个 Cluster。 Cluster : 集群（cluster）是 Envoy 连接到的一组逻辑上相似的上游主机。Envoy 通过服务发现发现集群中的成员。Envoy 可以通过主动运行状况检查来确定集群成员的健康状况。Envoy 如何将请求路由到集群成员由负载均衡策略确定。 更多详细信息可以参考 Envoy 的架构与基本术语，本文重点突出 Listener、Route 和 Cluster 这三个基本术语，同时需要注意流量经过这些术语的先后顺序，请求首先到达 Listener，然后通过 Http Route Table 转到具体的 Cluster，最后由具体的 Cluster 对请求做出响应。\nGateway 和 VirtualService 配置解析 # 还是拿之前 Istio 流量管理 这篇文章中的例子来解析吧，首先创建了一个 Gateway，配置文件如下：\napiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: bookinfo-gateway spec: selector: istio: ingressgateway # use istio default controller servers: - port: number: 80 name: http protocol: HTTP hosts: - \u0026#34;*\u0026#34; 然后又创建了一个 VirtualService：\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: bookinfo spec: hosts: - \u0026#34;*\u0026#34; gateways: - bookinfo-gateway http: - match: - uri: exact: /productpage - uri: exact: /login - uri: exact: /logout - uri: prefix: /api/v1/products route: - destination: host: productpage port: number: 9080 VirtualService 映射的就是 Envoy 中的 Http Route Table，大家可以注意到上面的 VirtualService 配置文件中有一个 gateways 字段，如果有这个字段，就表示这个 Http Route Table 是绑在 ingressgateway 的 Listener 中的；如果没有这个字段，就表示这个 Http Route Table 是绑在 Istio 所管理的所有微服务应用的 Pod 上的。\n为了分清主次，我决定将本文拆分成两篇文章来讲解，本篇主要围绕 ingressgateway 来解析 Gateway 和 VirtualService，而微服务应用本身的 VirtualService 和 DestinationRule 解析放到下一篇文章再说。 显而易见，上面这个 VirtualService 映射的 Http Route Table 是被绑在 ingressgateway 中的，可以通过 istioctl 来查看，istioctl 的具体用法请参考： 调试 Envoy 和 Pilot。\n首先查看 Listener 的配置项：\n$ istioctl -n istio-system pc listeners istio-ingressgateway-b6db8c46f-qcfks --port 80 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;0.0.0.0_80\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socketAddress\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;portValue\u0026#34;: 80 } }, \u0026#34;filterChains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;envoy.http_connection_manager\u0026#34;, \u0026#34;config\u0026#34;: { ... \u0026#34;rds\u0026#34;: { \u0026#34;config_source\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;route_config_name\u0026#34;: \u0026#34;http.80\u0026#34; }, ... } } ] } ] } ] 通过 rds 配置项的 route_config_name 字段可以知道该 Listener 使用的 Http Route Table 的名字是 http.80。\n查看 Http Route Table 配置项：\n$ istioctl -n istio-system pc routes istio-ingressgateway-b6db8c46f-qcfks --name http.80 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;http.80\u0026#34;, \u0026#34;virtualHosts\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;bookinfo:80\u0026#34;, \u0026#34;domains\u0026#34;: [ \u0026#34;*\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;path\u0026#34;: \u0026#34;/productpage\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||productpage.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;maxGrpcTimeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, ... }, ... { \u0026#34;match\u0026#34;: { \u0026#34;prefix\u0026#34;: \u0026#34;/api/v1/products\u0026#34; }, \u0026#34;route\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;outbound|9080||productpage.default.svc.cluster.local\u0026#34;, \u0026#34;timeout\u0026#34;: \u0026#34;0.000s\u0026#34;, \u0026#34;maxGrpcTimeout\u0026#34;: \u0026#34;0.000s\u0026#34; }, ... }, ... ] } ], \u0026#34;validateClusters\u0026#34;: false } ] VirtualService 中的 hosts 字段对应 Http Route Table 中 virtualHosts 配置项的 domains 字段。这里表示可以使用任何域名来通过 ingressgateway 访问服务（也可以直接通过 IP 来访问）。 VirtualService 中的 exact 字段对应 Http Route Table 中 routes.match 配置项的 path 字段。 VirtualService 中的 prefix 字段对应 Http Route Table 中 routes.match 配置项的 prefix 字段。 VirtualService 中的 route.destination 配置项对应 Http Route Table 中 routes.route 配置项的 cluster 字段。 关于 Envoy 中的 HTTP 路由解析可以参考我之前的文章： HTTP 路由解析。\n查看 Cluster 配置项：\n$ istioctl -n istio-system pc clusters istio-ingressgateway-b6db8c46f-qcfks --fqdn productpage.default.svc.cluster.local --port 9080 -o json [ { \u0026#34;name\u0026#34;: \u0026#34;outbound|9080||productpage.default.svc.cluster.local\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;EDS\u0026#34;, \u0026#34;edsClusterConfig\u0026#34;: { \u0026#34;edsConfig\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;serviceName\u0026#34;: \u0026#34;outbound|9080||productpage.default.svc.cluster.local\u0026#34; }, \u0026#34;connectTimeout\u0026#34;: \u0026#34;1.000s\u0026#34;, \u0026#34;circuitBreakers\u0026#34;: { \u0026#34;thresholds\u0026#34;: [ {} ] } } ] 可以看到，Cluster 最终将集群外通过 ingressgateway 发起的请求转发给实际的 endpoint，也就是 Kubernetes 集群中的 Service productpage 下面的 Pod（由 serviceName 字段指定）。\n实际上 istioctl 正是通过 pilot 的 xds 接口来查看 Listener 、Route 和 Cluster 等信息的。 好了，现在请求已经转交给 productpage 了，那么接下来这个请求将会如何走完整个旅程呢？请听下回分解！\n参考 # Service Mesh深度学习系列（三）| istio源码分析之pilot-discovery模块分析（中） 调试 Envoy 和 Pilot Envoy 的架构与基本术语 ","date":"2018年8月8日","externalUrl":null,"permalink":"/posts/where-is-the-request-1/","section":"博客","summary":"通过前几篇文章的学习与实践，我们对 Gateway、Virtu","title":"数据包在 Istio 网格中的生命周期（上）","type":"posts"},{"content":"本文转载自 Cizixs 的博客。\n什么是资源？ # 在 kubernetes 中，有两个基础但是非常重要的概念：node 和 pod。node 翻译成节点，是对集群资源的抽象；pod 是对容器的封装，是应用运行的实体。node 提供资源，而 pod 使用资源，这里的资源分为计算（cpu、memory、gpu）、存储（disk、ssd）、网络（network bandwidth、ip、ports）。这些资源提供了应用运行的基础，正确理解这些资源以及集群调度如何使用这些资源，对于大规模的 kubernetes 集群来说至关重要，不仅能保证应用的稳定性，也可以提高资源的利用率。\n在这篇文章，我们主要介绍 CPU 和内存这两个重要的资源，它们虽然都属于计算资源，但也有所差距。CPU 可分配的是使用时间，也就是操作系统管理的时间片，每个进程在一定的时间片里运行自己的任务（另外一种方式是绑核，也就是把 CPU 完全分配给某个 pod 使用，但这种方式不够灵活会造成严重的资源浪费，kubernetes 中并没有提供）；而对于内存，系统提供的是内存大小。\nCPU 的使用时间是可压缩的，换句话说它本身无状态，申请资源很快，也能快速正常回收；而内存大小是不可压缩的，因为它是有状态的（内存里面保存的数据），申请资源很慢（需要计算和分配内存块的空间），并且回收可能失败（被占用的内存一般不可回收）。\n把资源分成 可压缩 和 不可压缩，是因为在资源不足的时候，它们的表现很不一样。对于不可压缩资源，如果资源不足，也就无法继续申请资源（内存用完就是用完了），并且会导致 pod 的运行产生无法预测的错误（应用申请内存失败会导致一系列问题）；而对于可压缩资源，比如 CPU 时间片，即使 pod 使用的 CPU 资源很多，CPU 使用也可以按照权重分配给所有 pod 使用，虽然每个人使用的时间片减少，但不会影响程序的逻辑。\n在 kubernetes 集群管理中，有一个非常核心的功能：就是为 pod 选择一个主机运行。调度必须满足一定的条件，其中最基本的是主机上要有足够的资源给 pod 使用。\n资源除了和调度相关之外，还和很多事情紧密相连，这正是这篇文章要解释的。\nkubernetes 资源的表示 # 用户在 pod 中可以配置要使用的资源总量，kubernetes 根据配置的资源数进行调度和运行。目前主要可以配置的资源是 CPU 和 memory，对应的配置字段是 spec.containers[].resource.limits/request.cpu/memory。\n需要注意的是，用户是对每个容器配置 request 值，所有容器的资源请求之和就是 pod 的资源请求总量，而我们一般会说 pod 的资源请求和 limits。\nlimits 和 requests 的区别我们下面会提到，这里先说说比较容易理解的 cpu 和 memory。\nCPU 一般用核数来标识，一核CPU 相对于物理服务器的一个超线程核，也就是操作系统 /proc/cpuinfo 中列出来的核数。因为对资源进行了池化和虚拟化，因此 kubernetes 允许配置非整数个的核数，比如 0.5 是合法的，它标识应用可以使用半个 CPU 核的计算量。CPU 的请求有两种方式，一种是刚提到的 0.5，1 这种直接用数字标识 CPU 核心数；另外一种表示是 500m，它等价于 0.5，也就是说 1 Core = 1000m。\n内存比较容易理解，是通过字节大小指定的。如果直接一个数字，后面没有任何单位，表示这么多字节的内存；数字后面还可以跟着单位， 支持的单位有 E、P、T、G、M、K，前者分别是后者的 1000 倍大小的关系，此外还支持 Ei、Pi、Ti、Gi、Mi、Ki，其对应的倍数关系是 2^10 = 1024。比如要使用 100M 内存的话，直接写成 100Mi 即可。\n节点可用资源 # 理想情况下，我们希望节点上所有的资源都可以分配给 pod 使用，但实际上节点上除了运行 pods 之外，还会运行其他的很多进程：系统相关的进程（比如 sshd、udev等），以及 kubernetes 集群的组件（kubelet、docker等）。我们在分配资源的时候，需要给这些进程预留一些资源，剩下的才能给 pod 使用。预留的资源可以通过下面的参数控制：\n--kube-reserved=[cpu=100m][,][memory=100Mi][,][ephemeral-storage=1Gi]：控制预留给 kubernetes 集群组件的 CPU、memory 和存储资源 --system-reserved=[cpu=100mi][,][memory=100Mi][,][ephemeral-storage=1Gi]：预留给系统的 CPU、memory 和存储资源 这两块预留之后的资源才是 pod 真正能使用的，不过考虑到 eviction 机制（下面的章节会提到），kubelet 会保证节点上的资源使用率不会真正到 100%，因此 pod 的实际可使用资源会稍微再少一点。主机上的资源逻辑分配图如下所示：\n需要注意的是，allocatable 不是指当前机器上可以分配的资源，而是指能分配给 pod 使用的资源总量，一旦 kubelet 启动这个值是不会变化的。 allocatable 的值可以在 node 对象的 status 字段中读取，比如下面这样：\nstatus: allocatable: cpu: \u0026#34;2\u0026#34; ephemeral-storage: \u0026#34;35730597829\u0026#34; hugepages-2Mi: \u0026#34;0\u0026#34; memory: 3779348Ki pods: \u0026#34;110\u0026#34; capacity: cpu: \u0026#34;2\u0026#34; ephemeral-storage: 38770180Ki hugepages-2Mi: \u0026#34;0\u0026#34; memory: 3881748Ki pods: \u0026#34;110\u0026#34; kubernetes 资源对象 # 在这部分，我们来介绍 kubernetes 中提供的让我们管理 pod 资源的原生对象。\n请求（requests）和上限（limits） # 前面说过用户在创建 pod 的时候，可以指定每个容器的 Requests 和 Limits 两个字段，下面是一个实例：\nresources: requests: memory: \u0026#34;64Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; limits: memory: \u0026#34;128Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; Requests 是容器请求要使用的资源，kubernetes 会保证 pod 能使用到这么多的资源。请求的资源是调度的依据，只有当节点上的可用资源大于 pod 请求的各种资源时，调度器才会把 pod 调度到该节点上（如果 CPU 资源足够，内存资源不足，调度器也不会选择该节点）。\n需要注意的是，调度器只关心节点上可分配的资源，以及节点上所有 pods 请求的资源，而不关心节点资源的实际使用情况，换句话说，如果节点上的 pods 申请的资源已经把节点上的资源用满，即使它们的使用率非常低，比如说 CPU 和内存使用率都低于 10%，调度器也不会继续调度 pod 上去。\nLimits 是 pod 能使用的资源上限，是实际配置到内核 cgroups 里面的配置数据。对于内存来说，会直接转换成 docker run 命令行的 --memory 大小，最终会配置到 cgroups 对应任务的 /sys/fs/cgroup/memory/……/memory.limit_in_bytes 文件中。\n如果 limit 没有配置，则表明没有资源的上限，只要节点上有对应的资源，pod 就可以使用。 使用 requests 和 limits 概念，我们能分配更多的 pod，提升整体的资源使用率。但是这个体系有个非常重要的问题需要考虑，那就是**怎么去准确地评估 pod 的资源 requests？**如果评估地过低，会导致应用不稳定；如果过高，则会导致使用率降低。这个问题需要开发者和系统管理员共同讨论和定义。\nlimit range（默认资源配置) # 为每个 pod 都手动配置这些参数是挺麻烦的事情，kubernetes 提供了 LimitRange 资源，可以让我们配置某个 namespace 默认的 request 和 limit 值，比如下面的实例：\napiVersion: \u0026#34;v1\u0026#34; kind: \u0026#34;LimitRange\u0026#34; metadata: name: you-shall-have-limits spec: limits: - type: \u0026#34;Container\u0026#34; max: cpu: \u0026#34;2\u0026#34; memory: \u0026#34;1Gi\u0026#34; min: cpu: \u0026#34;100m\u0026#34; memory: \u0026#34;4Mi\u0026#34; default: cpu: \u0026#34;500m\u0026#34; memory: \u0026#34;200Mi\u0026#34; defaultRequest: cpu: \u0026#34;200m\u0026#34; memory: \u0026#34;100Mi\u0026#34; 如果对应 namespace 创建的 pod 没有写资源的 requests 和 limits 字段，那么它会自动拥有下面的配置信息：\n内存请求是 100Mi，上限是 200Mi CPU 请求是 200m，上限是 500m 当然，如果 pod 自己配置了对应的参数，kubernetes 会使用 pod 中的配置。使用 LimitRange 能够让 namespace 中的 pod 资源规范化，便于统一的资源管理。\n资源配额（resource quota） # 前面讲到的资源管理和调度可以认为 kubernetes 把这个集群的资源整合起来，组成一个资源池，每个应用（pod）会自动从整个池中分配资源来使用。默认情况下只要集群还有可用的资源，应用就能使用，并没有限制。kubernetes 本身考虑到了多用户和多租户的场景，提出了 namespace 的概念来对集群做一个简单的隔离。\n基于 namespace，kubernetes 还能够对资源进行隔离和限制，这就是 resource quota 的概念，翻译成资源配额，它限制了某个 namespace 可以使用的资源总额度。这里的资源包括 cpu、memory 的总量，也包括 kubernetes 自身对象（比如 pod、services 等）的数量。通过 resource quota，kubernetes 可以防止某个 namespace 下的用户不加限制地使用超过期望的资源，比如说不对资源进行评估就大量申请 16核 CPU 32G内存的 pod。\n下面是一个资源配额的实例，它限制了 namespace 只能使用 20核 CPU 和 1G 内存，并且能创建 10 个 pod、20个 rc、5个 service，可能适用于某个测试场景。\napiVersion: v1 kind: ResourceQuota metadata: name: quota spec: hard: cpu: \u0026#34;20\u0026#34; memory: 1Gi pods: \u0026#34;10\u0026#34; replicationcontrollers: \u0026#34;20\u0026#34; resourcequotas: \u0026#34;1\u0026#34; services: \u0026#34;5\u0026#34; resource quota 能够配置的选项还很多，比如 GPU、存储、configmaps、persistentvolumeclaims 等等，更多信息可以参考官方的文档。\nResource quota 要解决的问题和使用都相对独立和简单，但是它也有一个限制：那就是它不能根据集群资源动态伸缩。一旦配置之后，resource quota 就不会改变，即使集群增加了节点，整体资源增多也没有用。kubernetes 现在没有解决这个问题，但是用户可以通过编写一个 controller 的方式来自己实现。\n应用优先级 # QoS（服务质量） # Requests 和 limits 的配置除了表明资源情况和限制资源使用之外，还有一个隐藏的作用：它决定了 pod 的 QoS 等级。\n上一节我们提到了一个细节：如果 pod 没有配置 limits ，那么它可以使用节点上任意多的可用资源。这类 pod 能灵活使用资源，但这也导致它不稳定且危险，对于这类 pod 我们一定要在它占用过多资源导致节点资源紧张时处理掉。优先处理这类 pod，而不是资源使用处于自己请求范围内的 pod 是非常合理的想法，而这就是 pod QoS 的含义：根据 pod 的资源请求把 pod 分成不同的重要性等级。\nkubernetes 把 pod 分成了三个 QoS 等级：\nGuaranteed ：优先级最高，可以考虑数据库应用或者一些重要的业务应用。除非 pods 使用超过了它们的 limits，或者节点的内存压力很大而且没有 QoS 更低的 pod，否则不会被杀死\nBurstable ：这种类型的 pod 可以多于自己请求的资源（上限有 limit 指定，如果 limit 没有配置，则可以使用主机的任意可用资源），但是重要性认为比较低，可以是一般性的应用或者批处理任务\nBest Effort ：优先级最低，集群不知道 pod 的资源请求情况，调度不考虑资源，可以运行到任意节点上（从资源角度来说），可以是一些临时性的不重要应用。pod 可以使用节点上任何可用资源，但在资源不足时也会被优先杀死\nPod 的 requests 和 limits 是如何对应到这三个 QoS 等级上的，可以用下面一张表格概括：\n看到这里，你也许看出来一个问题了 :** 如果不配置 requests 和 limits，pod 的 QoS 竟然是最低的**。没错，所以推荐大家理解 QoS 的概念，并且按照需求一定要给 pod 配置 requests 和 limits 参数，不仅可以让调度更准确，也能让系统更加稳定。\n按照现在的方法根据 pod 请求的资源进行配置不够灵活和直观，更理想的情况是用户可以直接配置 pod 的 QoS，而不用关心具体的资源申请和上限值。但 kubernetes 目前还没有这方面的打算。 Pod 的 QoS 还决定了容器的 OOM（out-of-memory）值，它们对应的关系如下：\n可以看到，QoS 越高的 pod oom 值越低，也就越不容易被系统杀死。对于 Bustable pod，它的值是根据 request 和节点内存总量共同决定的:\noomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity 其中 memoryRequest 是 pod 申请的资源，memoryCapacity 是节点的内存总量。可以看到，申请的内存越多，oom 值越低，也就越不容易被杀死。\nQoS 的作用会在后面介绍 eviction 的时候详细讲解。\nPod 优先级（priority） # 除了 QoS，kubernetes 还允许我们自定义 pod 的优先级，比如：\napiVersion: scheduling.k8s.io/v1alpha1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: \u0026#34;This priority class should be used for XYZ service pods only.\u0026#34; 优先级的使用也比较简单，只需要在 pod.spec.PriorityClassName 指定要使用的优先级名字，即可以设置当前 pod 的优先级为对应的值。\nPod 的优先级在调度的时候会使用到。首先，待调度的 pod 都在同一个队列中，启用了 pod priority 之后，调度器会根据优先级的大小，把优先级高的 pod 放在前面，提前调度。\n另外，如果在调度的时候，发现某个 pod 因为资源不足无法找到合适的节点，调度器会尝试 preempt 的逻辑。 简单来说，调度器会试图找到这样一个节点：找到它上面优先级低于当前要调度 pod 的所有 pod，如果杀死它们，能腾足够的资源，调度器会执行删除操作，把 pod 调度到节点上。\n驱逐（Eviction） # 至此，我们讲述的都是理想情况下 kubernetes 的工作状况，我们假设资源完全够用，而且应用也都是在使用规定范围内的资源。\n但现实不会如此简单，在管理集群的时候我们常常会遇到资源不足的情况，在这种情况下我们要保证整个集群可用，并且尽可能减少应用的损失。保证集群可用比较容易理解，首先要保证系统层面的核心进程正常，其次要保证 kubernetes 本身组件进程不出问题；但是如果量化应用的损失呢？首先能想到的是如果要杀死 pod，要尽量减少总数。另外一个就和 pod 的优先级相关了，那就是尽量杀死不那么重要的应用，让重要的应用不受影响。\nPod 的驱逐是在 kubelet 中实现的，因为 kubelet 能动态地感知到节点上资源使用率实时的变化情况。其核心的逻辑是：kubelet 实时监控节点上各种资源的使用情况，一旦发现某个不可压缩资源出现要耗尽的情况，就会主动终止节点上的 pod，让节点能够正常运行。被终止的 pod 所有容器会停止，状态会被设置为 failed。\n驱逐触发条件 # 那么哪些资源不足会导致 kubelet 执行驱逐程序呢？目前主要有三种情况：实际内存不足、节点文件系统的可用空间（文件系统剩余大小和 inode 数量）不足、以及镜像文件系统的可用空间（包括文件系统剩余大小和 inode 数量）不足。\n下面这图是具体的触发条件：\n有了数据的来源，另外一个问题是触发的时机，也就是到什么程度需要触发驱逐程序？kubernetes 运行用户自己配置，并且支持两种模式：按照百分比和按照绝对数量。比如对于一个 32G 内存的节点当可用内存少于 10% 时启动驱逐程序，可以配置 memory.available\u0026lt;10% 或者 memory.available\u0026lt;3.2Gi。\n默认情况下，kubelet 的驱逐规则是 memory.available\u0026lt;100Mi，对于生产环境这个配置是不可接受的，所以一定要根据实际情况进行修改。 软驱逐（soft eviction）和硬驱逐（hard eviction） # 因为驱逐 pod 是具有毁坏性的行为，因此必须要谨慎。有时候内存使用率增高只是暂时性的，有可能 20s 内就能恢复，这时候启动驱逐程序意义不大，而且可能会导致应用的不稳定，我们要考虑到这种情况应该如何处理；另外需要注意的是，如果内存使用率过高，比如高于 95%（或者 90%，取决于主机内存大小和应用对稳定性的要求），那么我们不应该再多做评估和考虑，而是赶紧启动驱逐程序，因为这种情况再花费时间去判断可能会导致内存继续增长，系统完全崩溃。\n为了解决这个问题，kubernetes 引入了 soft eviction 和 hard eviction 的概念。\n软驱逐 可以在资源紧缺情况并没有哪些严重的时候触发，比如内存使用率为 85%，软驱逐还需要配置一个时间指定软驱逐条件持续多久才触发，也就是说 kubelet 在发现资源使用率达到设定的阈值之后，并不会立即触发驱逐程序，而是继续观察一段时间，如果资源使用率高于阈值的情况持续一定时间，才开始驱逐。并且驱逐 pod 的时候，会遵循 grace period ，等待 pod 处理完清理逻辑。和软驱逐相关的启动参数是：\n--eviction-soft：软驱逐触发条件，比如 memory.available\u0026lt;1Gi --eviction-sfot-grace-period：触发条件持续多久才开始驱逐，比如 memory.available=2m30s --eviction-max-pod-grace-period：kill pod 时等待 grace period 的时间让 pod 做一些清理工作，如果到时间还没有结束就做 kill 前面两个参数必须同时配置，软驱逐才能正常工作；后一个参数会和 pod 本身配置的 grace period 比较，选择较小的一个生效。\n硬驱逐 更加直接干脆，kubelet 发现节点达到配置的硬驱逐阈值后，立即开始驱逐程序，并且不会遵循 grace period，也就是说立即强制杀死 pod。对应的配置参数只有一个 --evictio-hard，可以选择上面表格中的任意条件搭配。\n设置这两种驱逐程序是为了平衡节点稳定性和对 pod 的影响，软驱逐照顾到了 pod 的优雅退出，减少驱逐对 pod 的影响；而硬驱逐则照顾到节点的稳定性，防止资源的快速消耗导致节点不可用。\n软驱逐和硬驱逐可以单独配置，不过还是推荐两者都进行配置，一起使用。\n驱逐哪些 pods？ # 上面我们已经整体介绍了 kubelet 驱逐 pod 的逻辑和过程，那这里就牵涉到一个具体的问题 : 要驱逐哪些 pod？ 驱逐的重要原则是尽量减少对应用程序的影响。\n如果是存储资源不足，kubelet 会根据情况清理状态为 Dead 的 pod 和它的所有容器，以及清理所有没有使用的镜像。如果上述清理并没有让节点回归正常，kubelet 就开始清理 pod。\n一个节点上会运行多个 pod，驱逐所有的 pods 显然是不必要的，因此要做出一个抉择：在节点上运行的所有 pod 中选择一部分来驱逐。虽然这些 pod 乍看起来没有区别，但是它们的地位是不一样的，正如乔治·奥威尔在《动物庄园》的那句话：\n所有动物生而平等，但有些动物比其他动物更平等。\nPod 也是不平等的，有些 pod 要比其他 pod 更重要。只管来说，系统组件的 pod 要比普通的 pod 更重要，另外运行数据库的 pod 自然要比运行一个无状态应用的 pod 更重要。kubernetes 又是怎么决定 pod 的优先级的呢？这个问题的答案就藏在我们之前已经介绍过的内容里：pod requests 和 limits、优先级（priority），以及 pod 实际的资源使用。\n简单来说，kubelet 会根据以下内容对 pod 进行排序：pod 是否使用了超过请求的紧张资源、pod 的优先级、然后是使用的紧缺资源和请求的紧张资源之间的比例。具体来说，kubelet 会按照如下的顺序驱逐 pod：\n使用的紧张资源超过请求数量的 BestEffort 和 Burstable pod，这些 pod 内部又会按照优先级和使用比例进行排序 紧张资源使用量低于 requests 的 Burstable 和 Guaranteed 的 pod 后面才会驱逐，只有当系统组件（kubelet、docker、journald 等）内存不够，并且没有上面 QoS 比较低的 pod 时才会做。执行的时候还会根据 priority 排序，优先选择优先级低的 pod 防止波动 # 这里的波动有两种情况，我们先说说第一种。驱逐条件出发后，如果 kubelet 驱逐一部分 pod，让资源使用率低于阈值就停止，那么很可能过一段时间资源使用率又会达到阈值，从而再次出发驱逐，如此循环往复……为了处理这种问题，我们可以使用 --eviction-minimum-reclaim 解决，这个参数配置每次驱逐至少清理出来多少资源才会停止。\n另外一个波动情况是这样的：Pod 被驱逐之后并不会从此消失不见，常见的情况是 kubernetes 会自动生成一个新的 pod 来取代，并经过调度选择一个节点继续运行。如果不做额外处理，有理由相信 pod 选择原来节点的可能性比较大（因为调度逻辑没变，而它上次调度选择的就是该节点），之所以说可能而不是绝对会再次选择该节点，是因为集群 pod 的运行和分布和上次调度时极有可能发生了变化。\n无论如何，如果被驱逐的 pod 再次调度到原来的节点，很可能会再次触发驱逐程序，然后 pod 再次被调度到当前节点，循环往复…… 这种事情当然是我们不愿意看到的，虽然看似复杂，但这个问题解决起来非常简单：驱逐发生后，kubelet 更新节点状态，调度器感知到这一情况，暂时不往该节点调度 pod 即可。--eviction-pressure-transition-period 参数可以指定 kubelet 多久才上报节点的状态，因为默认的上报状态周期比较短，频繁更改节点状态会导致驱逐波动。\n做一个总结，下面是一个使用了上面多种参数的驱逐配置实例（你应该能看懂它们是什么意思了）：\n–-eviction-soft=memory.available\u0026lt;80%,nodefs.available\u0026lt;2Gi \\ –-eviction-soft-grace-period=memory.available=1m30s,nodefs.available=1m30s \\ –-eviction-max-pod-grace-period=120 \\ –-eviction-hard=memory.available\u0026lt;500Mi,nodefs.available\u0026lt;1Gi \\ –-eviction-pressure-transition-period=30s \\ --eviction-minimum-reclaim=\u0026#34;memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi\u0026#34; 碎片整理和重调度 # Kubernetes 的调度器在为 pod 选择运行节点的时候，只会考虑到调度那个时间点集群的状态，经过一系列的算法选择一个当时最合适的节点。但是集群的状态是不断变化的，用户创建的 pod 也是动态的，随着时间变化，原来调度到某个节点上的 pod 现在看来可能有更好的节点可以选择。比如考虑到下面这些情况：\n调度 pod 的条件已经不再满足，比如节点的 taints 和 labels 发生了变化 新节点加入了集群。如果默认配置了把 pod 打散，那么应该有一些 pod 最好运行在新节点上 节点的使用率不均匀。调度后，有些节点的分配率和使用率比较高，另外一些比较低 节点上有资源碎片。有些节点调度之后还剩余部分资源，但是又低于任何 pod 的请求资源；或者 memory 资源已经用完，但是 CPU 还有挺多没有使用 想要解决上述的这些问题，都需要把 pod 重新进行调度（把 pod 从当前节点移动到另外一个节点）。但是默认情况下，一旦 pod 被调度到节点上，除非给杀死否则不会移动到另外一个节点的。\n为此 kubernetes 社区孵化了一个称为 descheduler 的项目，专门用来做重调度。重调度的逻辑很简单：找到上面几种情况中已经不是最优的 pod，把它们驱逐掉（eviction）。\n目前，descheduler 不会决定驱逐的 pod 应该调度到哪台机器，而是假定默认的调度器会做出正确的调度抉择。也就是说，之所以 pod 目前不合适，不是因为调度器的算法有问题，而是因为集群的情况发生了变化。如果让调度器重新选择，调度器现在会把 pod 放到合适的节点上。这种做法让 descheduler 逻辑比较简单，而且避免了调度逻辑出现在两个组件中。\nDescheduler 执行的逻辑是可以配置的，目前有几种场景：\nRemoveDuplicates：RS、deployment 中的 pod 不能同时出现在一台机器上 LowNodeUtilization：找到资源使用率比较低的 node，然后驱逐其他资源使用率比较高节点上的 pod，期望调度器能够重新调度让资源更均衡 RemovePodsViolatingInterPodAntiAffinity：找到已经违反 Pod Anti Affinity 规则的 pods 进行驱逐，可能是因为反亲和是后面加上去的 RemovePodsViolatingNodeAffinity：找到违反 Node Affinity 规则的 pods 进行驱逐，可能是因为 node 后面修改了 label 当然，为了保证应用的稳定性，descheduler 并不会随意地驱逐 pod，还是会尊重 pod 运行的规则，包括 pod 的优先级（不会驱逐 Critical pod，并且按照优先级顺序进行驱逐）和 PDB（如果违反了 PDB，则不会进行驱逐），并且不会驱逐没有 deployment、rs、jobs 的 pod 不会驱逐，daemonset pod 不会驱逐，有 local storage 的 pod 也不会驱逐。\nDescheduler 不是一个常驻的任务，每次执行完之后会退出，因此推荐使用 CronJob 来运行。\n总的来说，descheduler 是对原生调度器的补充，用来解决原生调度器的调度决策随着时间会变得失效，或者不够优化的缺陷。\n资源动态调整 # 动态调整的思路：应用的实际流量会不断变化，因此使用率也是不断变化的，为了应对应用流量的变化，我们应用能够自动调整应用的资源。比如在线商品应用在促销的时候访问量会增加，我们应该自动增加 pod 运算能力来应对；当促销结束后，有需要自动降低 pod 的运算能力防止浪费。\n运算能力的增减有两种方式：改变单个 pod 的资源，已经增减 pod 的数量。这两种方式对应了 kubernetes 的 HPA 和 VPA。\nHorizontal Pod AutoScaling（横向 Pod 自动扩展） # 横向 pod 自动扩展的思路是这样的：kubernetes 会运行一个 controller，周期性地监听 pod 的资源使用情况，当高于设定的阈值时，会自动增加 pod 的数量；当低于某个阈值时，会自动减少 pod 的数量。自然，这里的阈值以及 pod 的上限和下限的数量都是需要用户配置的。\n上面这句话隐藏了一个重要的信息：HPA 只能和 RC、deployment、RS 这些可以动态修改 replicas 的对象一起使用，而无法用于单个 pod、daemonset（因为它控制的 pod 数量不能随便修改）等对象。\n目前官方的监控数据来源是 metrics server 项目，可以配置的资源只有 CPU，但是用户可以使用自定义的监控数据（比如 prometheus），其他资源（比如 memory）的 HPA 支持也已经在路上了。\nVertical Pod AutoScaling # 和 HPA 的思路相似，只不过 VPA 调整的是单个 pod 的 request 值（包括 CPU 和 memory）。VPA 包括三个组件：\nRecommander：消费 metrics server 或者其他监控组件的数据，然后计算 pod 的资源推荐值 Updater：找到被 vpa 接管的 pod 中和计算出来的推荐值差距过大的，对其做 update 操作（目前是 evict，新建的 pod 在下面 admission controller 中会使用推荐的资源值作为 request） Admission Controller：新建的 pod 会经过该 Admission Controller，如果 pod 是被 vpa 接管的，会使用 recommander 计算出来的推荐值 可以看到，这三个组件的功能是互相补充的，共同实现了动态修改 pod 请求资源的功能。相对于 HPA，目前 VPA 还处于 alpha，并且还没有合并到官方的 kubernetes release 中，后续的接口和功能很可能会发生变化。\nCluster Auto Scaler # 随着业务的发展，应用会逐渐增多，每个应用使用的资源也会增加，总会出现集群资源不足的情况。为了动态地应对这一状况，我们还需要 CLuster Auto Scaler，能够根据整个集群的资源使用情况来增减节点。\n对于公有云来说，Cluster Auto Scaler 就是监控这个集群因为资源不足而 pending 的 pod，根据用户配置的阈值调用公有云的接口来申请创建机器或者销毁机器。对于私有云，则需要对接内部的管理平台。\n目前 HPA 和 VPA 不兼容，只能选择一个使用，否则两者会相互干扰。而且 VPA 的调整需要重启 pod，这是因为 pod 资源的修改是比较大的变化，需要重新走一下 apiserver、调度的流程，保证整个系统没有问题。目前社区也有计划在做原地升级，也就是说不通过杀死 pod 再调度新 pod 的方式，而是直接修改原有 pod 来更新。\n理论上 HPA 和 VPA 是可以共同工作的，HPA 负责瓶颈资源，VPA 负责其他资源。比如对于 CPU 密集型的应用，使用 HPA 监听 CPU 使用率来调整 pods 个数，然后用 VPA 监听其他资源（memory、IO）来动态扩展这些资源的 request 大小即可。当然这只是理想情况。\n总结 # 从前面介绍的各种 kubernetes 调度和资源管理方案可以看出来，提高应用的资源使用率、保证应用的正常运行、维护调度和集群的公平性是件非常复杂的事情，kubernetes 并没有完美的方法，而是对各种可能的问题不断提出一些针对性的方案。\n集群的资源使用并不是静态的，而是随着时间不断变化的，目前 kubernetes 的调度决策都是基于调度时集群的一个静态资源切片进行的，动态地资源调整是通过 kubelet 的驱逐程序进行的，HPA 和 VPA 等方案也不断提出，相信后面会不断完善这方面的功能，让 kubernetes 更加智能。\n资源管理和调度、应用优先级、监控、镜像中心等很多东西相关，是个非常复杂的领域。在具体的实施和操作的过程中，常常要考虑到企业内部的具体情况和需求，做出针对性的调整，并且需要开发者、系统管理员、SRE、监控团队等不同小组一起合作。但是这种付出从整体来看是值得的，提升资源的利用率能有效地节约企业的成本，也能让应用更好地发挥出作用。\n扫一扫关注微信公众号 ","date":"2018年8月3日","externalUrl":null,"permalink":"/posts/kubernetes-resource-management/","section":"博客","summary":"本文转载自 Cizixs 的博客。 什么是资源？ # 在 kubernetes 中，有两个基础但是非常","title":"Kubernetes 资源管理概述","type":"posts"},{"content":" 本文转载自 Jimmy Song 的博客，并且有很多改动。\nVizceral 是 Netflix 发布的一个开源项目，用于近乎实时地监控应用程序和集群之间的网络流量。 Vistio 是使用 Vizceral 对 Istio 和网格监控的改进。它利用 Istio Mixer 生成的指标，然后将其输入 Prometheus。Vistio 查询 Prometheus 并将数据存储在本地以允许重播流量。关于 Vizceral 可以参考这篇文章： Vizceral Open Source。\nVizceral 有两个可视化级别，全局可视化和集群级别可视化。在全局范围内（如上所示），您可以通过 Istio Ingress Gateway 等入口点将从 Internet 到 Istio 服务网格网络的网络流量可视化，或者您可以在 Istio 服务网格网络中显示总网络流量。\n在集群级别（如下所示），您可以可视化内部网格的流量。通过设置警告和错误级别警报，当应用程序出现问题时可以被快速检测出来。\n在 Istio 服务网格中安装 Vistio # 依赖 # Prometheus Istio 1.0 假设 # 以下 Demo 使得这些假设更容易部署。如果您的环境设置不同，则可能需要将代码下载到本地并编辑一些文件。\nPrometheus 部署在 istio-system namespace 下，可以通过 http://prometheus.istio-system:9090 地址访问 Istio mixer 启用了 istio_request_count metric Kubernetes 集群包含有 standard StorageClass 为了便于部署已安装了 Helm（可选） 由于测试环境大多数都没有外部网络存储，无法创建 StorageClass，待会儿我们可以将这部分的配置修改为 hostPath。\n前言 # 如果您还尚未部署服务网格，可以按照此 Istio Bookinfo Demo 中的说明部署 Istio 及其示例应用程序。您需要能够在应用程序之间生成流量。要测试指标是否从 Mixer 正确发送到 Prometheus，您可以打开 Prometheus 查询 istio_request_bytes_count，应该会看到多个条目。\n部署 Vistio # 您可以选择通过 kubectl 或者 Helm 来部署 Vistio，下面会主要介绍 Helm 部署方式。有些变量可能需要根据您自己的环境来修改。\n如果你想通过 Helm 部署 Vistio，你将需要在 GitHub 上下载项目来获取 Helm 模板。此外，如果上述假设之一不符合您的需求（例如 prometheus url 不同），则应手动编辑文件。\n$ git clone https://github.com/nmnellis/vistio.git 使用 Helm 部署 # 由于我们使用的是 Istio 1.0 版本，而 Vistio 已经有相当一段时间没有更新了，很多配置项已经不适用了，需要改动很多地方。\n切换到 Vistio 项目的根目录，修改 values-with-ingress.yaml 配置文件。\n$ vim helm/vistio/values-with-ingress.yaml vistioConfig: graphName: Vistio globalLevel: maxVolume: 2000000 clusterConnections: # Total requests per second coming into the ingress controller from internet # 将 istio_request_count 修改为 istio_request_bytes_count # 将 destination_service=\u0026#34;istio-ingressgateway.istio-system.svc.cluster.local\u0026#34; 修改为 source_workload=\u0026#34;istio-ingressgateway\u0026#34; - query: sum(rate(istio_request_bytes_count{source_workload=\u0026#34;istio-ingressgateway\u0026#34;}[1m])) by (response_code) prometheusURL: http://prometheus.istio-system:9090 ... clusterLevel: # Cluster name must match \u0026#39;target\u0026#39; name in global - cluster: istio-mesh maxVolume: 3000 serviceConnections: # 将 istio_request_count 修改为 istio_request_bytes_count # 将 source_service 修改为 source_app - query: sum(rate(istio_request_bytes_count[1m])) by (source_app,destination_service,response_code) prometheusURL: http://prometheus.istio-system:9090 source: # 将 source_service 修改为 source_app label: source_app ... 修改 values.yaml 配置文件。\n$ vim helm/vistio/values.yaml ... ###################################### ## Vistio-web ###################################### web: env: # Vistio-web 需要调用 Vistio-api 的 url，而且这个 url 必须是通过浏览器可以访问的，所以可以使用 ingress，后面将会创建 updateURL: \u0026#34;http://vistio-api.istio.io/graph\u0026#34; 修改 statefulset.yaml 配置文件。\n$ vim helm/vistio/templates/statefulset.yaml apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: vistio-api ... spec: replicas: {{ .Values.api.replicaCount }} serviceName: vistio template: metadata: ... spec: volumes: - name: config configMap: name: vistio-api-config # 添加 volume vistio-db - name: vistio-db hostPath: path: /data/vistio # 将 volumeClaimTemplates 配置项注释或删除 #volumeClaimTemplates: #- metadata: # annotations: # volume.beta.kubernetes.io/storage-class: {{ .Values.api.storage.class }} # name: vistio-db # spec: # accessModes: # - ReadWriteOnce # resources: # requests: # storage: {{ .Values.api.storage.size }} 同时你需要在运行 vistio-api 的节点上提前创建 /data/vistio 目录。\n运行 helm install 部署 Vistio。\n$ helm install helm/vistio -f helm/vistio/values-with-ingress.yaml --name vistio --namespace default $ kubectl get pod vistio-api-0 1/1 Running 0 2m vistio-web-5c44b7f76d-hmjdc 1/1 Running 0 2m 验证和暴露 Vistio Web/API # 暴露 Vistio Web/API # 为 Service vistio-api 和 vistio-web 创建 Ingress：\n$ cat ingress.yaml --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: vistio-web namespace: default spec: rules: - host: vistio-web.istio.io http: paths: - path: / backend: serviceName: vistio-web servicePort: 8080 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: vistio-api namespace: default spec: rules: - host: vistio-api.istio.io http: paths: - path: / backend: serviceName: vistio-api servicePort: 9091 $ kubectl create -f ingress.yaml 然后在你的本地电脑上添加两条 hosts：\n$Ingree_host vistio-web.istio.io $Ingree_host vistio-api.istio.io 将 $Ingree_host 替换为 Ingress Controller 运行节点的 IP。\n验证 visito-api # vistio-web 调用 vistio-api 来渲染服务网格。访问 http://vistio-api.istio.io/graph 您应该会看到类似下列的输出。\n访问 Vistio # 如果一切都已经启动并准备就绪，您就可以访问 Vistio UI，开始探索服务网格网络，访问http://vistio-web.istio.io 您将会看到类似下图的输出。\n探索 # 在全局范围内，您将看到Istio网格内所有请求的总和，如果你点击 istio-mesh 气泡，就能查看你的网状网络。\n在你的 Istio 网格中，您可以使用许多可视化工具来帮助您查明故障的应用程序。\n使用屏幕右上方的过滤器可以快速过滤出错误率较高的应用程序。通过高级配置，当错误率超过特定值时，也可以触发警报。警报将显示给定应用程序的当前错误率趋势。\n问题排查 # 访问 http://vistio-api.istio.io/graph，如果你从 vistio-api 中看到以下输出，表示某些功能无法正常工作。正确的输出显示在教程上面。\n检查 vistio-api 日志中是否有错误——在大多数情况下，vistio-api 将记录与 Prometheus 通信时遇到的任何问题。\n$ kubectl logs -f $(kubectl get pod -l app=vistio-api -o go-template=\u0026#39;{{range .items}}{{.metadata.name}}{{end}}\u0026#39;) -c vistio-api 验证 Prometheus 查询——vistio-api 使用以下查询检索其数据。您应该确保 Prometheus 内部的数据都存在。\n# Global Level Query sum(rate(istio_request_bytes_count{source_workload=\u0026#34;istio-ingressgateway\u0026#34;}[1m])) by (response_code) # Cluster Level Query sum(rate(istio_request_bytes_count[1m])) by (source_app,destination_service,response_code) 提交 Issue——如果遇到问题无法解决请提交 Issue： https://github.com/nmnellis/vistio/issues\n","date":"2018年8月3日","externalUrl":null,"permalink":"/posts/vistio-visualize-your-istio-mesh-using-netflixs-vizceral/","section":"博客","summary":"本文转载自 Jimmy Song 的博客，并且有很多改动。 Vizceral 是 Netflix 发布的一个开源项","title":"Vistio—使用 Netflix 的 Vizceral 可视化 Istio service mesh","type":"posts"},{"content":"在一个典型的网格中，通常有一个或多个用于终结外部 TLS 链接，将流量引入网格的负载均衡器（我们称之为 gateway）。 然后流量通过边车网关（sidecar gateway）流经内部服务。 应用程序使用外部服务的情况也很常见（例如访问 Google Maps API），一些情况下，这些外部服务可能被直接调用；但在某些部署中，网格中所有访问外部服务的流量可能被要求强制通过专用的出口网关（Egress gateway）。 下图描绘了网关在网格中的使用情况。\nIstio服务网格中的网关 其中 Gateway 是一个独立于平台的抽象，用于对流入专用中间设备的流量进行建模。下图描述了跨多个配置资源的控制流程。\n不同v1alpha3元素之间的关系 Gateway 介绍 # Gateway 用于为 HTTP / TCP 流量配置负载均衡器，并不管该负载均衡器将在哪里运行。 网格中可以存在任意数量的 Gateway，并且多个不同的 Gateway 实现可以共存。 实际上，通过在配置中指定一组工作负载（Pod）标签，可以将 Gateway 配置绑定到特定的工作负载，从而允许用户通过编写简单的 Gateway Controller 来重用现成的网络设备。\n对于入口流量管理，您可能会问： 为什么不直接使用 Kubernetes Ingress API ？ 原因是 Ingress API 无法表达 Istio 的路由需求。 Ingress 试图在不同的 HTTP 代理之间取一个公共的交集，因此只能支持最基本的 HTTP 路由，最终导致需要将代理的其他高级功能放入到注解（annotation）中，而注解的方式在多个代理之间是不兼容的，无法移植。\nIstio Gateway 通过将 L4-L6 配置与 L7 配置分离的方式克服了 Ingress 的这些缺点。 Gateway 只用于配置 L4-L6 功能（例如，对外公开的端口，TLS 配置），所有主流的L7代理均以统一的方式实现了这些功能。 然后，通过在 Gateway 上绑定 VirtualService 的方式，可以使用标准的 Istio 规则来控制进入 Gateway 的 HTTP 和 TCP 流量。\n例如，下面这个简单的 Gateway 配置了一个 Load Balancer，以允许访问 host bookinfo.com 的 https 外部流量进入网格中：\napiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: bookinfo-gateway spec: servers: - port: number: 80 name: http protocol: HTTP hosts: - bookinfo.com 要为进入上面的 Gateway 的流量配置相应的路由，必须为同一个 host 定义一个 VirtualService（参考上一篇博文），并使用配置中的 gateways 字段绑定到前面定义的 Gateway 上：\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: bookinfo spec: hosts: - bookinfo.com gateways: - bookinfo-gateway # \u0026lt;---- bind to gateway http: - match: - uri: prefix: /reviews route: ... Gateway 可以用于建模边缘代理或纯粹的内部代理，如第一张图所示。 无论在哪个位置，所有网关都可以用相同的方式进行配置和控制。\n下面通过一个示例来演示如何配置 Istio 以使用 Istio Gateway 在服务网格外部公开服务。\n使用 Istio 网关配置 Ingress # 让我们看看如何为 Gateway 在 HTTP 80 端口上配置流量。\n创建一个 Istio Gateway\n$ cat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: httpbin-gateway spec: selector: istio: ingressgateway # use Istio default gateway implementation servers: - port: number: 80 name: http protocol: HTTP hosts: - \u0026#34;httpbin.example.com\u0026#34; EOF 为通过 Gateway 进入的流量配置路由\n$ cat \u0026lt;\u0026lt;EOF | istioctl create -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: httpbin spec: hosts: - \u0026#34;httpbin.example.com\u0026#34; gateways: - httpbin-gateway http: - match: - uri: prefix: /status - uri: prefix: /delay route: - destination: port: number: 8000 host: httpbin EOF 在这里，我们 为服务创建了一个 VirtualService 配置 httpbin ，其中包含两条路由规则，允许路径 /status 和 路径的流量 /delay。\n该 网关列表指定，只有通过我们的要求 httpbin-gateway 是允许的。所有其他外部请求将被拒绝，并返回 404 响应。\n请注意，在此配置中，来自网格中其他服务的内部请求不受这些规则约束，而是简单地默认为循环路由。要将这些（或其他规则）应用于内部调用，我们可以将特殊值 mesh 添加到 gateways 的列表中。\n使用 curl 访问 httpbin 服务。\n首先获取 Ingress Gateway 的 IP 和 端口，参考上一篇文章： Istio 流量管理\n$ curl -I -HHost:httpbin.example.com http://$INGRESS_HOST:$INGRESS_PORT/status/200 HTTP/1.1 200 OK server: envoy date: Thu, 02 Aug 2018 04:18:41 GMT content-type: text/html; charset=utf-8 access-control-allow-origin: * access-control-allow-credentials: true content-length: 0 x-envoy-upstream-service-time: 9 请注意，我们使用该 -H 标志将 Host HTTP Header 设置为 “httpbin.example.com”。这是必需的，因为我们的 ingress Gateway 被配置为处理 “httpbin.example.com”，但在我们的测试环境中，我们没有该主机的 DNS 绑定，并且只是将我们的请求发送到 ingress IP。\n访问任何未明确公开的其他 URL。您应该看到一个 HTTP 404 错误：\n$ curl -I -HHost:httpbin.example.com http://$INGRESS_HOST:$INGRESS_PORT/headers HTTP/1.1 404 Not Found date: Thu, 02 Aug 2018 04:21:39 GMT server: envoy transfer-encoding: chunked 使用浏览器访问 Ingress 服务 # 如果你想在浏览器中输入 httpbin 服务的 URL 来访问是行不通的，因为我们没有办法像使用 curl 一样告诉浏览器假装访问 httpbin.example.com，只能通过向 /etc/hosts 文件中添加 hosts 来解决这个问题。\n但是麻烦又来了，目前这种状况下即使你添加了 hosts，也仍然无法访问，因为 Istio Gateway 使用的是 NodePort 模式，暴露出来的不是 80 端口和 443 端口，而我们要想通过域名来访问服务，必须要求 Gateway 暴露出来的端口是 80 和 443。\n所以我们只能曲线救国了，通过修改 Ingress Gateway 的 Deployment，将 80 端口和 443 端口配置为 hostPort 模式，然后再通过 Node 亲和性将 Gateway 调度到某个固定的主机上。\n$ kubectl -n istio-system edit deployment istio-ingressgateway apiVersion: extensions/v1beta1 kind: Deployment metadata: name: istio-ingressgateway namespace: istio-system ... spec: ... template: ... spec: affinity: nodeAffinity: ... requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - 192.168.123.248 # 比如你想调度到这台主机上 containers: - name: ISTIO_META_POD_NAME ... ports: - containerPort: 80 hostPort: 80 protocol: TCP - containerPort: 443 hostPort: 443 protocol: TCP ... 修改完之后保存退出，等待 Gateway 的 Pod 重新调度，然后在你的浏览器所在的本地电脑上添加一条 hosts：\n192.168.123.248 httpbin.example.com 重新配置 VirtualService：\n$ cat \u0026lt;\u0026lt;EOF | istioctl replace -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: httpbin spec: hosts: - \u0026#34;httpbin.example.com\u0026#34; gateways: - httpbin-gateway http: - match: - uri: prefix: /status - uri: prefix: /delay - uri: prefix: /headers route: - destination: port: number: 8000 host: httpbin EOF 接下来就可以在浏览器中输入 URL：http://httpbin.example.com/headers 来访问服务啦！\n清理 # 删除 Gateway、VirtualService 和 httpbin 服务：\n$ istioctl delete gateway httpbin-gateway $ istioctl delete virtualservice httpbin $ kubectl delete --ignore-not-found=true -f samples/httpbin/httpbin.yaml 参考 # 控制 Ingress 流量 Gateway ","date":"2018年8月2日","externalUrl":null,"permalink":"/posts/istio-ingress/","section":"博客","summary":"在一个典型的网格中，通常有一个或多个用于终结外部 TLS 链接，将流","title":"Istio 服务网格中的网关","type":"posts"},{"content":"Istio 从 0.8 版本开始出现了一个新的 API 组： networking.istio.io/v1alpha3，应该会替代现有的 config.istio.io/v1alpha2 API。新的 API 不管是结构上还是功能上、以及命名上，都有很大差异。如果不作特殊说明，本文所有的示例将采用新版 API。\n本文将通过简单的示例来演示通过 Istio 实现应用的金丝雀部署。\n正常情况下 istioctl 和 kubectl 都可以用来操作这些对象，但是 kubectl 缺乏验证功能，因此调试阶段使用 istioctl 会更方便一些。 Bookinfo 应用介绍 # 以 Bookinfo 应用为示例，它由四个单独的微服务构成，用来演示多种 Istio 特性。这个应用模仿在线书店的一个分类，显示一本书的信息。页面上会显示一本书的描述，书籍的细节（ISBN、页数等），以及关于这本书的一些评论。\nBookinfo 应用分为四个单独的微服务：\nproductpage ：productpage 微服务会调用 details 和 reviews 两个微服务，用来生成页面。 details ：这个微服务包含了书籍的信息。 reviews ：这个微服务包含了书籍相关的评论。它还会调用 ratings 微服务。 ratings ：ratings 微服务中包含了由书籍评价组成的评级信息。 reviews 微服务有 3 个版本：\nv1 版本不会调用 ratings 服务。 v2 版本会调用 ratings 服务，并使用 1 到 5 个黑色星形图标来显示评分信息。 v3 版本会调用 ratings 服务，并使用 1 到 5 个红色星形图标来显示评分信息。 下图展示了这个应用的端到端架构。\nIstio 注入之前的 Bookinfo 应用 Bookinfo 是一个异构应用，几个微服务是由不同的语言编写的。这些服务对 Istio 并无依赖，但是构成了一个有代表性的服务网格的例子：它由多个服务、多个语言构成，并且 reviews 服务具有多个版本。\n部署 Bookinfo 应用 # 要在 Istio 中运行这一应用，无需对应用自身做出任何改变。我们只要简单的在 Istio 环境中对服务进行配置和运行，具体一点说就是把 Envoy sidecar 注入到每个服务之中。这个过程所需的具体命令和配置方法由运行时环境决定，而部署结果较为一致，如下图所示：\nBookinfo 应用 所有的微服务都和 Envoy sidecar 集成在一起，被集成服务所有的出入流量都被 sidecar 所劫持，这样就为外部控制准备了所需的 Hook，然后就可以利用 Istio 控制平面为应用提供服务路由、遥测数据收集以及策略实施等功能。\n接下来可以根据 Istio 的运行环境，按照下面的讲解完成应用的部署。\n进入 Istio 安装目录。\n启动应用容器：\n如果集群用的是 手工 Sidecar 注入，使用如下命令：\n$ kubectl apply -f \u0026lt;(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml) istioctl kube-inject 命令用于在在部署应用之前修改 bookinfo.yaml\n如果集群使用的是 自动 Sidecar 注入，只需简单的 kubectl 就能完成服务的部署\n$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml 上面的命令会启动全部的四个服务，其中也包括了 reviews 服务的三个版本（v1、v2 以及 v3）\n给应用定义 Ingress gateway：\n$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml 确认所有的服务和 Pod 都已经正确的定义和启动：\n$ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE details ClusterIP 10.254.86.98 \u0026lt;none\u0026gt; 9080/TCP 3h kubernetes ClusterIP 10.254.0.1 \u0026lt;none\u0026gt; 443/TCP 149d productpage ClusterIP 10.254.199.214 \u0026lt;none\u0026gt; 9080/TCP 3h ratings ClusterIP 10.254.102.147 \u0026lt;none\u0026gt; 9080/TCP 3h reviews ClusterIP 10.254.249.86 \u0026lt;none\u0026gt; 9080/TCP 3h $ kubectl get pods NAME READY STATUS RESTARTS AGE details-v1-6456dbdb9-crqnw 2/2 Running 0 3h productpage-v1-6f6887645c-52qhn 2/2 Running 0 3h ratings-v1-648cf76d8f-g65s5 2/2 Running 0 3h reviews-v1-7dcbc85bb5-j748n 2/2 Running 0 3h reviews-v2-65fd78f5df-r8n6r 2/2 Running 0 3h reviews-v3-95c85969c-zmpfx 2/2 Running 0 3h 确定 Ingress 的 IP 和端口\n执行以下命令以确定 ingressgateway 是否启用了 NodePort 模式。\n$ kubectl -n istio-system get svc istio-ingressgateway NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway NodePort 10.254.160.93 \u0026lt;none\u0026gt; 80:31380/TCP,443:31390/TCP,31400:31400/TCP,15011:25059/TCP,8060:36612/TCP,15030:25049/TCP,15031:36810/TCP 3h 确定 ingress IP：\n$ export INGRESS_HOST=$(kubectl -n istio-system get po -l istio=ingressgateway -o go-template=\u0026#39;{{range .items}}{{.status.hostIP}}{{end}}\u0026#39;) 确定端口：\n$ export INGRESS_PORT=$(kubectl -n istio-system get svc istio-ingressgateway -o go-template=\u0026#39;{{range .spec.ports}}{{if eq .name \u0026#34;http\u0026#34;}}{{.nodePort}}{{end}}{{end}}\u0026#39;) 设置 GATEWAY_URL：\n$ export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT 下面可以用 curl 命令来确认 Bookinfo 应用的运行情况：\n$ curl -o /dev/null -s -w \u0026#34;%{http_code}\\n\u0026#34; http://${GATEWAY_URL}/productpage 200 还可以用浏览器打开网址 http://$GATEWAY_URL/productpage，来浏览应用的 Web 页面。如果刷新几次应用的页面，就会看到页面中会随机展示 reviews 服务的不同版本的效果（红色、黑色的星形或者没有显示）。reviews 服务出现这种情况是因为我们还没有使用 Istio 来控制版本的路由。\n金丝雀部署 # 由于 Bookinfo 示例部署了三个版本的 reviews 微服务，因此我们需要设置默认路由。 否则，如果您当多次访问应用程序，您会注意到有时输出包含星级评分，有时又没有。 这是因为没有为应用明确指定缺省路由时，Istio 会将请求随机路由到该服务的所有可用版本上。\n此任务假定您尚未设置任何路由。 如果您已经为示例应用程序创建了存在冲突的路由规则，则需要在下面的命令中使用 replace 代替 create。 请注意：本文档假设还没有设置任何路由规则。 首先将所有微服务的默认路由设置为 v1。\n$ istioctl create -f samples/bookinfo/networking/virtual-service-all-v1.yaml $ istioctl create -f samples/bookinfo/networking/destination-rule-all.yaml 可以通过下面的命令来显示已创建的路由规则：\n$ istioctl get virtualservices -o yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: details ... spec: hosts: - details http: - route: - destination: host: details subset: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: productpage ... spec: gateways: - bookinfo-gateway - mesh hosts: - productpage http: - route: - destination: host: productpage subset: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings ... spec: hosts: - ratings http: - route: - destination: host: ratings subset: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews ... spec: hosts: - reviews http: - route: - destination: host: reviews subset: v1 --- 由于路由规则是通过异步方式分发到代理的，因此在尝试访问应用程序之前，您应该等待几秒钟，以便规则传播到所有 pod 上。\n现在在浏览器中打开 Bookinfo 应用程序的 URL (http://$GATEWAY_URL/productpage)，你应该可以看到 Bookinfo 应用程序的 productpage 页面。 请注意， productpage 页面显示的内容中没有评分星级，这是因为 reviews:v1 服务不会访问 ratings 服务。\n由于新的 API 引入了一些新的配置资源，而且不向后兼容，所以很有必要来解释一下上面两个 yaml 文件提到的两个新概念：VirtualService 和 DestinationRule。\nVirtualService # 过去的路由分配比较简单，使用标签即可。新的版本中，提出了 VirtualService 的概念。VirtualService 由一组路由规则构成，用于对服务实体（在 K8S 中对应为 Pod）进行寻址。一旦有流量符合其中规则的选择条件，就会发送流量给对应的服务（或者服务的一个版本/子集）。\nVirtualService 描述了一个或多个用户可寻址目标到网格内实际工作负载之间的映射。其中可寻址的目标服务使用 hosts 字段来指定，而网格内的实际工作负载由每个 route 配置项中的 destination 字段指定。在上面的示例中，这两个地址是相同的，但实际上用户可寻址目标可以是任何用于定位服务的、具有可选通配符前缀或 CIDR 前缀的 DNS 名称。\n流量的特征除了请求数据之外，还包括流量的来源，这样就能根据一些上下文来进行灵活的定义了。\n例如，以下规则定义来自打了标签 app=sleep 的 Pod 对 php-server 的请求，都转向 v1：\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: sleep-server-route spec: hosts: - \u0026#34;php-server\u0026#34; http: - match: - sourceLabels: app: sleep route: - destination: name: php-server subset: v1 - route: - destination: name: php-server subset: v2 这里的匹配策略是具有从上到下的优先级的，也就是说，最下一条就是缺省路由。所以没有打标签 app=sleep 的 Pod 对 php-server 的请求，都转向 v2。\n而本文的 Bookinfo 示例中创建的路由规则表示：\n所有对 details 的请求，都转向 details 的 v1 版本。 所有对 productpage 的请求，都转向 productpage 的 v1 版本。 所有对 ratings 的请求，都转向 ratings 的 v1 版本。 所有对 reviews 的请求，都转向 reviews 的 v1 版本。 可以看到，match 中不再包含 source，这里使用标签来过滤。写完应用之后，我们再次访问 Bookinfo 应用程序的 URL (http://$GATEWAY_URL/productpage)，会发现并没有生效。这是因为，在 v3 版本的 API 中，目标规则不再是透明了，路由定义必须以目标策略为基础。\nDestinationRule # 因此这里需要定义一个 DestinationRule 对象，来满足上面的目标需求：\napiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: productpage spec: host: productpage subsets: - name: v1 labels: version: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: reviews spec: host: reviews subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 - name: v3 labels: version: v3 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: ratings spec: host: ratings subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 - name: v2-mysql labels: version: v2-mysql - name: v2-mysql-vm labels: version: v2-mysql-vm --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: details spec: host: details subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 --- DestinationRule 用于配置在将流量转发到服务时应用的策略集。这些策略应由服务提供者撰写，用于描述断路器、负载均衡、TLS 设置等。\nDestinationRule 的 host 可以包含通配符前缀，以允许单个规则应用于多个服务。 DestinationRule 定义了目的 host 的子集 subsets （例如：命名版本）。 这些 subset 用于 VirtualService 的路由规则设置中，可以将流量导向服务的某些特定版本。通过这种方式为版本命名后，可以在不同的虚拟服务中明确地引用这些命名版本的 subset，简化 Istio 代理发出的统计数据，并可以将 subsets 编码到 SNI 头中。 现在再次访问 Bookinfo 应用程序的 URL (http://$GATEWAY_URL/productpage)，会发现规则已经生效了。\n示例一：将 10% 请求发送到 v2 版本而其余 90% 发送到 v1 版本 # $ cat \u0026lt;\u0026lt;EOF | istioctl replace -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews http: - route: - destination: host: reviews subset: v1 weight: 90 - destination: host: reviews subset: v2 weight: 10 EOF 现在的规则就是刷新 productpage 页面，90% 的概率看到黑色星标的评论，10%的概率看不到星标。\n注意 : 因为使用Envoy sidecar的实现，你需要刷新页面很多次才能看到接近规则配置的概率分布。\n示例二：将 jason 用户的请求全部发到 v2 版本 # cat \u0026lt;\u0026lt;EOF | istioctl replace -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews http: - match: - headers: end-user: exact: jason route: - destination: host: reviews subset: v2 - route: - destination: host: reviews subset: v1 EOF 使用 jason 用户登陆 productpage 页面，你可以看到每个刷新页面时，页面上都有一个1到5颗星的评级。如果你使用其他用户登陆的话，将因继续使用 reviews:v1 而看不到星标评分。\n示例三：全部切换到 v3 版本 # cat \u0026lt;\u0026lt;EOF | istioctl replace -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: reviews spec: hosts: - reviews http: - route: - destination: host: reviews subset: v3 EOF 现在不论你使用什么用户登陆 productpage 页面，你都可以看到带红色星标评分的评论了。\n参考 # 摸索：Istio 路由规则 Alpha v3 配置请求路由 ","date":"2018年8月1日","externalUrl":null,"permalink":"/posts/istio-traffic-management/","section":"博客","summary":"Istio 从 0.8 版本开始出现了一个新的 API 组： networking.is","title":"Istio 流量管理","type":"posts"},{"content":"北京时间 2018 年 8 月 1 日（建军节）凌晨 0 点，Istio 宣布推出 1.0 正式版本，并表示已可用于生产环境。这距离最初的 0.1 版本发布已过去一年多的时间。这个项目的组件相对比较复杂，原有的一些选项是靠 ConfigMap 以及 istioctl 分别调整的，现在通过重新设计的 Helm Chart，安装选项用 values.yml 或者 helm 命令行的方式来进行集中管理了。\n在安装 Istio 之前要确保 Kubernetes 集群（仅支持 v1.9 及以后版本）已部署并配置好本地的 kubectl 客户端。\n下载 Istio # $ wget https://github.com/istio/istio/releases/download/1.0.0/istio-1.0.0-linux.tar.gz $ tar zxf istio-1.0.0-linux.tar.gz $ cp istio-1.0.0/bin/istioctl /usr/local/bin/ 使用 Helm 部署 Istio 服务 # 克隆 Istio 仓库：\n$ git clone https://github.com/istio/istio.git $ cd istio 安装包内的 Helm 目录中包含了 Istio 的 Chart，官方提供了两种方法：\n用 Helm 生成 istio.yaml，然后自行安装。 用 Tiller 直接安装。 很明显，两种方法并没有什么本质区别，这里我们采用第一种方法来部署。\n$ helm template install/kubernetes/helm/istio --name istio --namespace istio-system --set sidecarInjectorWebhook.enabled=true --set ingress.service.type=NodePort --set gateways.istio-ingressgateway.type=NodePort --set gateways.istio-egressgateway.type=NodePort --set tracing.enabled=true --set servicegraph.enabled=true --set prometheus.enabled=true --set tracing.jaeger.enabled=true --set grafana.enabled=true \u0026gt; istio.yaml $ kubectl create namespace istio-system $ kubectl create -f istio.yaml 这里说的是使用 install/kubernetes/helm/istio 目录中的 Chart 进行渲染，生成的内容保存到 ./istio.yaml 文件之中。将 sidecarInjectorWebhook.enabled 设置为 true，从而使自动注入属性生效。\n部署完成后，可以检查 isotio-system namespace 中的服务是否正常运行：\n$ kubectl -n istio-system get pods -o go-template=\u0026#39;{{range .items}}{{.metadata.name}}{{\u0026#34;\\n\u0026#34;}}{{end}}\u0026#39; istio-citadel-f5779fbbb-brbxd istio-cleanup-secrets-jjqg5 istio-egressgateway-6c5cc7dd86-l2c82 istio-galley-6bf8f6f4b7-twvzl istio-ingressgateway-fbfdfc5c7-fg9xh istio-pilot-85df58955d-g5bfh istio-policy-74c48c8ccb-wd6h6 istio-sidecar-injector-cf5999cf8-h9smx istio-statsd-prom-bridge-55965ff9c8-2hmzf istio-telemetry-cb49594cc-gfd84 istio-tracing-77f9f94b98-9xvzs prometheus-7456f56c96-xcdh4 servicegraph-5b8d7b4d5-lzhth 过去的 istio-ca 现已更名 istio-citadel。 istio-cleanup-secrets 是一个 job，用于清理过去的 Istio 遗留下来的 CA 部署（包括 sa、deploy 以及 svc 三个对象）。 egressgateway、ingress 以及 ingressgateway，可以看出边缘部分的变动很大，以后会另行发文。 Prometheus、Grafana、Servicegraph 和 Jaeger # 等所有 Pod 启动后，可以通过 NodePort、Ingress 或者 kubectl proxy 来访问这些服务。比如可以通过 Ingress 来访问服务。\n首先为 Prometheus、Grafana、Servicegraph 和 Jaeger 服务创建 Ingress：\n$ cat ingress.yaml --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: prometheus namespace: istio-system spec: rules: - host: prometheus.istio.io http: paths: - path: / backend: serviceName: prometheus servicePort: 9090 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: grafana namespace: istio-system spec: rules: - host: grafana.istio.io http: paths: - path: / backend: serviceName: grafana servicePort: 3000 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: servicegraph namespace: istio-system spec: rules: - host: servicegraph.istio.io http: paths: - path: / backend: serviceName: servicegraph servicePort: 8088 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: tracing namespace: istio-system spec: rules: - host: tracing.istio.io http: paths: - path: / backend: serviceName: tracing servicePort: 80 $ kubectl create -f ingress.yaml 然后在你的本地电脑上添加四条 hosts：\n$Ingree_host prometheus.istio.io $Ingree_host grafana.istio.io $Ingree_host servicegraph.istio.io $Ingree_host tracing.istio.io 将 $Ingree_host 替换为 Ingress Controller 运行节点的 IP。\n通过 http://grafana.istio.io 访问 Grafana 服务：\n通过 http://servicegraph.istio.io 访问 ServiceGraph 服务，展示服务之间调用关系图。\nhttp://servicegraph.istio.io/force/forcegraph.html : As explored above, this is an interactive D3.js visualization. http://servicegraph.istio.io/dotviz : is a static Graphviz visualization. http://servicegraph.istio.io/dotgraph : provides a DOT serialization. http://servicegraph.istio.io/d3graph : provides a JSON serialization for D3 visualization. http://servicegraph.istio.io/graph : provides a generic JSON serialization. 通过 http://tracing.istio.io/ 访问 Jaeger 跟踪页面：\n通过 http://prometheus.istio.io/ 访问 Prometheus 页面：\n如果你已经部署了 Prometheus-operator，可以不必部署 Grafana，直接将 addons/grafana/dashboards 目录下的 Dashboard 模板复制出来放到 Prometheus-operator 的 Grafana 上，然后添加 istio-system 命名空间中的 Prometheus 数据源就可以监控 Istio 了。 Mesh Expansion # Istio 还支持管理非 Kubernetes 管理的应用。此时，需要在应用所在的 VM 或者物理中部署 Istio，具体步骤请参考 Mesh Expansion\n部署好后，就可以向 Istio 注册应用，如：\n# istioctl register servicename machine-ip portname:port $ istioctl -n onprem register mysql 1.2.3.4 3306 $ istioctl -n onprem register svc1 1.2.3.4 http:7000 参考 # Istio 0.8 的 Helm Chart 解析 ","date":"2018年8月1日","externalUrl":null,"permalink":"/posts/istio-1.0-deploy/","section":"博客","summary":"北京时间 2018 年 8 月 1 日（建军节）凌晨 0 点，Istio 宣布推出 1.0","title":"Istio 1.0 部署","type":"posts"},{"content":"在微服务领域，各个服务之间经常会相互调用。如果某个服务繁忙或者无法响应请求，将有可能引发集群的大规模级联故障，从而造成整个系统不可用，通常把这种现象称为 服务雪崩效应。为了应对这种情况，可以使用熔断器（circuit breaking）。\n熔断器 是分布式系统的关键组件，默认情况下处于关闭状态，这时请求被允许通过熔断器。它调用失败次数积累，如果当前健康状况低于设定阈值则启动熔断机制，这时请求被禁止通过。这样做可以实现更优雅的故障处理，并在问题被放大之前做出及时的响应。你可以选择在基础架构层面实现熔断机制，但熔断器本身会很容易受到故障的影响。为了更好地实现熔断机制，可以在 Envoy 的网络层面配置熔断器，这样做的好处是 Envoy 在网络级别强制实现断路，而不必为每个应用程序单独配置或编程。\n熔断器配置 # Envoy 支持各种类型的完全分布式（非协调的）熔断，设置熔断时，需要考虑系统的具体情况，可以通过向 Envoy 的 clusters 配置项中添加 circuit_breakers 来为 Envoy 配置熔断器。下面是一个简单的示例：\ncircuit_breakers: thresholds: - priority: DEFAULT max_connections: 1000 max_requests: 1000 - priority: HIGH max_connections: 2000 max_requests: 2000 thresholds : 阈值允许我们定义服务响应的流量类型的优先级和限制。 priority : 优先级是指熔断器如何处理定义为 DEFAULT 或 HIGH 的路由。示例中的设置表示将任何不应该在长连接队列中等待的请求设置为 HIGH（例如，用户在购物网站上提交购买请求或保存当前状态的 POST 请求）。 max_connections : Envoy 将为上游集群中的所有主机建立的最大连接数，默认值是 1024。实际上，这仅适用于 HTTP/1.1集群，因为 HTTP/2 使用到每个主机的单个连接。 max_requests : 在任何给定时间内，集群中所有主机可以处理的最大请求数，默认值也是 1024。实际上，这适用于仅 HTTP/2 集群，因为 HTTP/1.1 集群由最大连接断路器控制。 基本的熔断策略 # 由于 HTTP/1.1 协议和 HTTP/2 协议具有不同的连接行为（HTTP/1.1 : 同一个连接只能处理一个请求；HTTP/2 : 同一个连接能并发处理多个请求，而且并发请求的数量比HTTP1.1大了好几个数量级），使用不同协议的集群将各自使用不同的配置项：\nHTTP/1.1 协议 : 使用 max_connections。 HTTP/2 协议 ： 使用 max_requests。 这两个配置项都可以很好地实现熔断机制，主要取决于两个指标：服务的请求/连接数量和请求延时。例如，具有 1000个请求/second 和平均延迟 2 秒的 HTTP/1 服务通常会在任何给定时间内打开 2000 个连接。由于当存在大量非正常连接时熔断器会启动熔断机制，因此建议将参数 max_connections 的值最少设置为 10 x 2000，这样当最后 10 秒内的大多数请求未能返回正确的响应时就会打开熔断器。当然，具体的熔断器配置还得取决于系统的负载以及相关服务的具体配置。\n高级熔断策略 # 上面讨论了一些基本的熔断策略，下面将介绍更高级的熔断策略，这些高级熔断策略可以为你的网络基础架构增加更多的弹性。\n基于延迟设置熔断 # 如上所述，熔断器最常见的用例之一就是预防服务响应过慢但未完全瘫痪时引发的故障。虽然 Envoy 没有直接提供熔断器的延迟配置项，但可以通过自动重试配置项来模拟延迟。自动重试配置项通过 max_retries 字段定义，表示在任何给定时间内，集群中所有主机可以执行的最大重试次数。\n基于长连接重试队列设置熔断 # 由于重试有可能将请求流量增加到两倍以上甚至更多，因此通过 max_retries 参数可以防止服务因为过多的重试而过载。建议将此参数的值设置为服务通常在 10 秒窗口中处理的请求总数的一小部分，最好不要将重试次数设置为与服务在 10 秒窗口中处理的请求总数差不多。\n扫一扫关注微信公众号 ","date":"2018年7月13日","externalUrl":null,"permalink":"/posts/circuit-breaking/","section":"博客","summary":"在微服务领域，各个服务之间经常会相互调用。如果某个服务繁忙或","title":"Envoy 基础教程：熔断器的原理和使用","type":"posts"},{"content":"当微服务集群规模非常庞大时，控制平面包含了大量的 Envoy 配置项和基础设施状态，这时最好将数据平面与控制平面分离。控制平面最主要的功能包括自动重试和 集成服务发现。\n单独创建控制平面的最大优势之一是可以为路由配置提供统一的来源。传统架构的路由定义分散存储在 Web 服务器的配置文件、负载均衡器配置文件和特定应用程序的配置中（如 routes.rb），使用单独的控制平面可以集中所有的路由配置，使它们更易于更改和管理，同时也为应用的迁移和发布提供了更高的灵活性。\n通过 RDS 提供路由 # Envoy 的动态配置功能允许通过路由发现服务（RDS）的 API 来动态获取路由配置。控制平面通过 RDS 提供路由配置，将 域名+路径 映射到 Envoy 中的某个集群（cluster），而实际的流量控制由 Envoy 实例来完成。\n这里是一个使用 RDS 来动态获取路由的示例：\nversion_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.RouteConfiguration name: local_route virtual_hosts: - name: local_service domains: [\u0026#34;*\u0026#34;] routes: - match: { prefix: \u0026#34;/\u0026#34; } route: { cluster: some_service } 开源项目 go-control-plane， Istio Pilot 和 商业项目 Houston 都提供了 RDS 的 API，Envoy 官方文档也定义了一个 完整的 RDS 规范。RDS 规范只是一种流量传输机制，如何对路由进行管理还是要取决于你。\n路由定义的最佳实践 # 当你的系统中有数千个 Envoy 实例时，应该选择控制平面来作为所有路由的统一来源。客户端请求可以直接来自用户、内部服务或者来自不同的云区域，因此最好使用 Envoy 来处理这些不同的网络拓扑（例如，作为客户流量的前端代理以及内部流量的服务网格），虽然流量来自不同的方向，但它们的行为都是相似的。\n为了扩展单个系统的路由定义，通常需要遵循以下三个关键原则：\n将路由视为数据，而不是配置 将控制权分配给具有 ACL 权限的团队 使用审计日志和回滚操作来管理路由的更改 将路由视为数据 # 将路由视为一组相互关联的服务的数据可以防止发生冲突，同时确保了其语义的正确性。虽然像 Istio 这样的工具可以很容易地编写基于 YAML 配置文件的路由，但是在数千行 YAML 文件中管理数百条路由很难保证每个定义都是有效的路由。或许你也想过使用版本控制来管理这些配置文件，但如果合并分支时发生致命错误将会导致灾难性的后果（如路由丢失或通过 API 重写）。\n实际上，从静态配置文件转移到动态配置文件是在大规模集群中使用 Envoy 的第一步。为了能够将 Envoy 投入生产，建议至少使用像 go-control-plane 这样实现了 xDS 的控制平面统一提供路由配置。通过将路由的来源转移到 RDS API 背后，可以实现路由的并发更新，同时也可以防止对路由进行无意义的更新。\n将控制权分配给具有 ACL 权限的团队 # 通过对流量进行管控可以解锁更强大的工作流程（如蓝绿发布和增量迁移），同时也能让服务团队确保各个服务之间的路由是安全可用的。你可以根据需要来隐藏管控区域之外的路由以防止误点击或者发生意外事故，你也可以完全禁止某些成员修改路由。\n管理路由的更改 # 了解路由何时被更改以及被谁更改是极其重要的，许多团队会发现，在他们分配好了定义路由的任务之后，实际路由的数量将会超出他们的预期，因此为了保险起见，最好对路由的更改进行记录。例如，master 分支中的自动蓝绿发布应该打上最后一个合并分支的人的标签。\n为了更好地管理路由，团队内部必须要知道如何在两个时间点之间更改路由以及如何在必要时将其回滚，同时最好将这些操作收集到监控系统中。当你需要进一步优化时，这些操作记录是很有价值的（例如 git 历史记录在编写新代码时很有帮助）。\n","date":"2018年7月6日","externalUrl":null,"permalink":"/posts/routing-with-a-control-plane/","section":"博客","summary":"当微服务集群规模非常庞大时，控制平面包含了大量的 Envoy 配置项和基","title":"Envoy 基础教程：通过控制平面提供路由","type":"posts"},{"content":"在微服务中使用 Envoy，需要明确两个核心概念 : 数据平面和控制平面。\n数据平面 由一组 Envoy 实例组成，用来调解和控制微服务之间的所有网络通信。 控制平面 从 Envoy 代理和其他服务收集和验证配置，并在运行时执行访问控制和使用策略。 你可以使用静态类型的配置文件来实现控制平面，但为了能做出更加智能的负载均衡决策，最好的方法是通过 API 接口来实现。通过 API 接口来集中发现服务可以充分利用 Envoy 动态更新配置文件的能力。设置控制平面的第一步就是将 Envoy 连接到服务发现服务（SDS），通常分为三步：\n实现一个控制平面 将控制平面中定义的服务发布到 Envoy 的 clusters 中 将 主机/容器/实例 发布到 Envoy 的 endpoints 中 实现一个控制平面 # 控制平面必须要满足 Envoy v2 xDS APIs，同时为了更好地使用服务发现能力，你的控制平面最好能够实现集群发现服务（CDS）和端点发现服务（EDS）。为了避免重复造轮子，你可以选择社区已经实现好的控制平面：\nRotor : Rotor 是一种快速、轻量级的 xDS 实现，它可以和 Kubernetes、Consul 和 AWS 等服务集成，并且提供了一组默认的路由发现服务（RDS）和监听器发现服务（LDS）。它也是 Turbine Labs 的商业解决方案 Houston 的组件之一，Houston 在 Rotor 的基础上增加了更多的路由、指标和弹性方面的配置。 go-control-plane : Envoy 官方仓库提供了一个开源版本的控制平面： go-control-plane。如果你想弄清楚如何从服务发现服务中获取所有内容，可以好好研究一下这个项目。 Pilot : 如果想将 Envoy 和 Kubernetes 集成，你可以选择 Istio 项目。Istio 中的控制平面是由 Pilot 组件来实现的，它会将 YAMl 文件的内容转换为相应的 xDS 响应。如果你不想使用 Istio，也不用担心，因为 Pilot 完全可以脱离 Istio 的其他组件（如 Mixer）来单独和 Envoy 集成。 将服务发布到 CDS # 集群是 Envoy 连接到的一组逻辑上相似的上游主机，通过它可以对流量进行负载均衡。你可以通过调用集群发现服务（CDS）的 API 来动态获取集群管理成员，Envoy 会定期轮询 CDS 端点以进行集群配置，配置文件形式如下：\nversion_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.Cluster name: some_service connect_timeout: 1.0s lb_policy: ROUND_ROBIN type: EDS eds_cluster_config: eds_config: api_config_source: api_type: GRPC cluster_names: [xds_cluster] 服务发现收集的每个服务都会映射到 resources 下面的一个配置项，除此之外你还需要为负载均衡设置一些额外的配置参数：\nlb_policy : 集群的负载均衡类型，有以下几种方式： round_robin : 轮询主机 weighted_least_request : 最近获得最少请求的主机 random : 随机 connect_timeout : 设置连接超时。越小越好，从 1 秒开始慢慢往上增加，直到网络没有明显的抖动为止。 api_type : 设置服务的协议。Envoy 通过该协议和服务发现服务进行通信。 通常情况下，你可以对端点（Endpoint）列表进行硬编码，但如果基础架构是动态的，则需要将 type 设置为 EDS，这将告诉 Envoy 轮询 EDS API 以获取可用的 IP/Port 列表。完整示例可以参考 Envoy 的官方文档。\n设置好 CDS 之后，就可以为此集群设置端点发现服务（EDS）了。\n将实例发布到 EDS # Envoy 将端点（Endpoint）定义为集群中可用的 IP 和端口。为了能够对服务之间的流量进行负载均衡，Envoy 希望 EDS API 能够提供每个服务的端点列表。Envoy 会定期轮询 EDS 端点，然后生成响应：\nversion_info: \u0026#34;0\u0026#34; resources: - \u0026#34;@type\u0026#34;: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment cluster_name: some_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.2 port_value: 1234 通过这种配置方式，Envoy 唯一需要知道的就是该端点属于哪个集群，这比直接定义集群更简单。\nEnvoy 将 CDS 和 EDS 视为一份份的报告并保持服务发现的最终一致性。如果到该端点的请求经常失败，就会从负载均衡中删除该端点，直到再次恢复正常访问。\n最佳实践：对配置进行分区 # 当集群中有很多服务时，Envoy 与 CDS 和 EDS 的交互量将会非常庞大，一旦出现问题，从几千个 API 响应中排查问题是很不现实的。标准的做法是以两种方式对配置进行分区：\n根据数据中心/区域划分 : 通常情况下，一个数据中心的服务不需要知道其他数据中心可用的服务端点。要想在不同区域之间建立通信，需要将远程数据中心的前端代理添加到本地的负载均衡器中。 根据服务需求划分 : 通过为不同的服务配置 Envoy 边车代理（服务 envoy 与每个 serivce 实例一起运行），设置白名单来限制不同服务之间的相互通信，可以降低 1000 个级别的微服务之间相互通信的复杂度。同时边车（Sidecars）代理还可以通过阻止对服务的意外调用来加强安全保护。 对配置进行分区可以降低对不同服务的运营和管理的难度，但它的代价是使控制平面变得更加复杂，但客户往往是不关心控制平面的，所以牺牲控制平面的复杂度还是很值得的。\n下一步 # 一旦控制平面发现了所有可用服务，就可以在这些服务上配置路由了。下一节将会介绍如何配置路由发现服务（RDS）。\n扫一扫关注微信公众号 ","date":"2018年7月4日","externalUrl":null,"permalink":"/posts/integrating-service-discovery-with-envoy/","section":"博客","summary":"在微服务中使用 Envoy，需要明确两个核心概念 : 数据平面和控","title":"Envoy 基础教程：集成服务发现","type":"posts"},{"content":"如果你准备将服务暴露在互联网上，最好启用 SSL/TLS 加密协议。当使用 Envoy 作为前端代理或者服务网格代理时，可以通过 SSL/TLS 协议来加密客户端和代理之间的所有通信流量。\nEnvoy 同时支持监听器中的 TLS 终止 和与上游集群建立连接时的 TLS 发起。不管是为现代 web 服务提供标准的边缘代理功能，还是同具有高级 TLS 要求（TLS1.2, SNI, 等等）的外部服务建立连接，Envoy 都提供了充分的支持。\n本文将会演示如何在前端代理中设置 TLS 终止，同时指定访问域名。主要分三个步骤：\n创建 Envoy 需要使用的证书 为 Envoy 启用证书验证 配置 Envoy 将 80 端口重定向到 443 端口 创建证书 # 如果要启用 HTTPS，我们就需要从证书授权机构(以下简称 CA) 处获取一个证书。如果你还没有证书，你可以从 Let’s Encrypt 获得网站域名的免费的证书，因为 Let’s Encrypt 就是一个 CA。本文为了测试使用 OpenSSL 生成私钥文件 example-com.key 和 自签名证书 example-com.crt。\n需要注意的是 Common Name 字段，本文测试使用的是 example.com。 # 继续沿用前文使用的示例 $ cd envoy/examples/front-proxy # 生成2048位的加密私钥 $ openssl genrsa -out example-com.key 2048 #生成证书签名请求(CSR) $ openssl req -new -key example-com.key -out example-com.csr You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter \u0026#39;.\u0026#39;, the field will be left blank. ----- Country Name (2 letter code) [XX]:CN State or Province Name (full name) []:CA Locality Name (eg, city) [Default City]:Shanghai Organization Name (eg, company) [Default Company Ltd]:Daocloud Organizational Unit Name (eg, section) []:Envoy Division Common Name (eg, your name or your server\u0026#39;s hostname) []:example.com Email Address []:chuansheng.yang@daocloud.io Please enter the following \u0026#39;extra\u0026#39; attributes to be sent with your certificate request A challenge password []: An optional company name []: # 生成X509自签名证书 $ openssl x509 -req -days 365 -in example-com.csr -signkey example-com.key -out example-com.crt 为 Envoy 启用证书验证 # 修改 Dockerfile-frontenvoy 文件：\nADD ./example-com.crt /etc/example-com.crt ADD ./example-com.key /etc/example-com.key 修改 front-envoy.yaml 配置文件，在 filters 列表后面添加 tls_context 配置项：\ntls_context: common_tls_context: tls_certificates: - certificate_chain: filename: \u0026#34;/etc/example-com.crt\u0026#34; private_key: filename: \u0026#34;/etc/example-com.key\u0026#34; 将监听器的监听端口改为标准的 TLS 端口：443。\n- address: socket_address: address: 0.0.0.0 port_value: 443 还要指定访问的域名，不再使用之前的通配符匹配：\ndomains: - \u0026#34;example.com\u0026#34; Envoy 可以通过在同一个监听器中配置多个监听器过滤器链来支持多个域名的 SNI（如 example.com 和 www.example.com），你可以在 Envoy 官方文档 中看到一个示例。\n最后修改 docker-compose.yaml 文件，将 443 端口暴露出来，同时将 8080 端口替换为 80 端口。\nservices: front-envoy: ... expose: - \u0026#34;80\u0026#34; - \u0026#34;443\u0026#34; ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; 重启该示例服务：\n$ docker-compose down --remove-orphans $ docker-compose up --build -d 下面就可以使用 curl 来进行测试了。这里有两个需要注意的地方：\n为了确保 curl 能成功验证证书，必须通过 --cacert 参数将证书文件传递给 Envoy。 由于 DNS 无法解析 example.com，所以需要通过参数 --connect-to 明确指定连接到 localhost，同时在请求的头文件中申明 localhost 的域名为 example.com。 $ curl --cacert example-com.crt --connect-to localhost -H \u0026#39;Host: example.com\u0026#39; https://localhost/service/1 Hello from behind Envoy (service 1)! hostname: 56e8a5bff6bd resolvedhostname: 172.18.0.2 如果你的 curl 版本不支持 --connect-to 参数，可以在 /etc/hosts 中添加一个条目：127.0.0.1 example.com，然后直接通过域名访问：\n$ curl --cacert example-com.crt https://example.com/service/1 将 80 端口重定向到 443 端口 # 为了将所有 80 端口的流量重定向到 443 端口，可以将 443 端口的路由配置复制一份，然后稍作修改：\n- address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: virtual_hosts: - name: backend domains: - \u0026#34;example.com\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: path_redirect: \u0026#34;/\u0026#34; https_redirect: true http_filters: - name: envoy.router config: {} 重启服务：\n$ docker-compose down --remove-orphans $ docker-compose up --build -d 再次通过 HTTP 协议访问 service1，将会返回 301 状态码：\n$ curl -I -H \u0026#39;Host: example.com\u0026#39; http://localhost/service/1 HTTP/1.1 301 Moved Permanently location: https://example.com/ date: Tue, 03 Jul 2018 06:32:13 GMT server: envoy content-length: 0 OK，大功告成！完整的 front-proxy.yaml 配置文件内容如下：\nstatic_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: virtual_hosts: - name: backend domains: - \u0026#34;example.com\u0026#34; routes: - match: prefix: \u0026#34;/\u0026#34; redirect: path_redirect: \u0026#34;/\u0026#34; https_redirect: true http_filters: - name: envoy.router config: {} - address: socket_address: address: 0.0.0.0 port_value: 443 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: backend domains: - \u0026#34;example.com\u0026#34; routes: - match: prefix: \u0026#34;/service/1\u0026#34; route: cluster: service1 - match: prefix: \u0026#34;/service/2\u0026#34; route: cluster: service2 http_filters: - name: envoy.router config: {} tls_context: common_tls_context: tls_certificates: - certificate_chain: filename: \u0026#34;/etc/example-com.crt\u0026#34; private_key: filename: \u0026#34;/etc/example-com.key\u0026#34; clusters: - name: service1 connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service1 port_value: 80 - name: service2 connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service2 port_value: 80 admin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8001 扫一扫关注微信公众号 ","date":"2018年7月3日","externalUrl":null,"permalink":"/posts/setting-up-ssl-in-envoy/","section":"博客","summary":"如果你准备将服务暴露在互联网上，最好启用 SSL/TLS 加密协议。当使用 Envoy","title":"Envoy 基础教程：启用证书验证","type":"posts"},{"content":"微服务最常见的工作流程之一就是版本更新。不同于基础架构更新，通过流量管理可以优雅地实现微服务的版本更新。当新发布的版本有缺陷时，这种方法就可以避免版本缺陷对用户造成的不良影响。\n本文将继续沿用前文使用的示例，在原有配置文件的基础上新增了个别服务的新版本来演示流量是如何切换的（包括基于请求头的路由和加权负载均衡）。\n基于请求头的路由 # 为了说明基于请求头的路由对微服务产生的影响，首先创建一个新版本的 service1 。这里仍然使用 Envoy 仓库中的 front-proxy 示例，修改 docker-compose.yml 文件，添加一个名为 service1a 的新服务。\nservice1a: build: context: . dockerfile: Dockerfile-service volumes: - ./service-envoy.yaml:/etc/service-envoy.yaml networks: envoymesh: aliases: - service1a environment: - SERVICE_NAME=1a expose: - \u0026#34;80\u0026#34; 为了确保 Envoy 可以发现该服务，需要将该服务添加到 clusters 配置项中。\n- name: service1a connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service1a port_value: 80 为了使新加的服务路由可达，需要在 match 配置项中添加一个带有 headers 字段的新路由。因为路由规则列表是按顺序匹配的，所以我们需要将该规则添加到路由规则列表的顶部，这样与新规则匹配的包含该头文件的请求就会被转发到新服务，而不包含该头文件的请求仍然被转发到 service1。\nroutes: - match: prefix: \u0026#34;/service/1\u0026#34; headers: - name: \u0026#34;x-canary-version\u0026#34; value: \u0026#34;service1a\u0026#34; route: cluster: service1a - match: prefix: \u0026#34;/service/1\u0026#34; route: cluster: service1 - match: prefix: \u0026#34;/service/2\u0026#34; route: cluster: service2 然后重启该示例服务：\n$ docker-compose down --remove-orphans $ docker-compose up --build -d 如果客户端发出的请求没有携带头文件，就会收到来自 service1 的响应：\n$ curl localhost:8000/service/1 Hello from behind Envoy (service 1)! hostname: d0adee810fc4 resolvedhostname: 172.18.0.2 如果请求携带了头文件 x-canary-version，Envoy 就会将请求转发到 service 1a。\n$ curl -H \u0026#39;x-canary-version: service1a\u0026#39; localhost:8000/service/1 Hello from behind Envoy (service 1a)! hostname: 569ee89eebc8 resolvedhostname: 172.18.0.6 Envoy 基于头文件的路由功能解锁了 在生产环境中测试开发代码的能力。\n加权负载均衡 # 接下来进一步修改配置来实现对 service1 新版本的增量发布，使用 clusters 数组替代原来的 cluster 键值对，从而实现将 25% 的流量转发到该服务的新版本上。\n- match: prefix: \u0026#34;/service/1\u0026#34; route: weighted_clusters: clusters: - name: service1a weight: 25 - name: service1 weight: 75 然后重启该示例服务：\n$ docker-compose down --remove-orphans $ docker-compose up --build -d 此时如果客户端发出的请求没有携带头文件，就会有 25% 的流量转发到 service 1a。\n增量部署是个非常强大的功能，它还可以和监控配合使用，以确保服务的版本差异（或者后端服务的架构差异）不会对该服务的版本更新产生不良影响。如果想模拟新版本的成功发布，可以将 service1a 的权重设置为 100，然后所有的流量都会被转发到 service 1a。同样，如果新发布的版本有缺陷，你可以通过将 service1a 的权重设置为 0 来回滚到之前的版本。\n最佳实践 # 学会了如何配置基于请求头的路由和加权负载均衡之后，就可以在生产或测试环境中进行实践了。首先需要将部署和发布这两个流程分离，部署了新版本之后，你就可以通过配置基于请求头的路由来让你的团队在内部进行测试，同时又不影响用户的使用。一旦测试通过，就可以通过滚动发布模式（逐步增加权重，如 1%，5%，10%，50% \u0026hellip;）来优雅地发布新版本。\n通过将部署和发布这两个流程分离，使用基于请求头的路由在新版本发布之前进行测试，然后通过滚动部署模式来增量发布，你的团队将会从中受益匪浅。\n","date":"2018年7月2日","externalUrl":null,"permalink":"/posts/incremental-deploys/","section":"博客","summary":"微服务最常见的工作流程之一就是版本更新。不同于基础架构更新，","title":"Envoy 基础教程：实现增量部署","type":"posts"},{"content":"本文将更详细地讨论 Envoy 的 HTTP 路由，如果你已经看过了我的上篇文章： 在你的笔记本上运行 Envoy，现在就可以更深入地了解如何在静态文件中配置路由（Route）、集群（Cluster）和监听器（Listener）了。\n相关组件 # 路由 # 路由 是一组将虚拟主机与集群相匹配的规则，通过路由你可以很轻松地创建流量切换规则。路由的定义方式有两种：通过静态配置文件定义或通过路由发现服务（RDS）进行配置。\n集群 # 集群 是一组逻辑上相似的上游主机，它接收来自 Envoy 的流量。集群可以通过负载均衡策略来提高基础架构的弹性。集群可以通过静态文件进行配置，也可以通过集群发现服务（CDS）API 动态获取。\n监听器 # 监听器 是可以接受来自下游客户端的连接的命名网络位置（例如，端口，unix域套接字等）。Envoy 公开一个或多个下游主机连接的侦听器。同样，监听器可以通过静态定义，也可以通过监听器发现服务（LDS）动态获取。\n配置路由 # Envoy 的路由定义将 域 + URL 映射到集群。在上一篇文章中，我们定义了两个集群（service1 和 service2），每一个集群都匹配一个单独的 URL（/service1 和 /service2）。\nvirtual_hosts: - name: backend domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/service/1\u0026#34; route: cluster: service1 - match: prefix: \u0026#34;/service/2\u0026#34; route: cluster: service2 集群从 DNS 中获取集群成员数据，并对集群中的所有主机使用轮询的方式进行负载均衡。\nclusters: - name: service1 connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service1 port_value: 80 - name: service2 connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service2 port_value: 80 配置监听器 # 路由的配置包含在监听器的配置中，现在我们再回过头来看一下监听器的配置。监听器通过监听器过滤器（Listener filter）来操作路由配置中定义的两个服务。监听器的 API 非常简单，它的作用是在不更改 Envoy 的核心功能的情况下添加更多的集成功能。\nlisteners: - address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: backend domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/service/1\u0026#34; route: cluster: service1 - match: prefix: \u0026#34;/service/2\u0026#34; route: cluster: service2 http_filters: - name: envoy.router config: {} 动态发现路由、集群和监听器 # 到目前为止我们都是通过静态配置文件来配置路由和集群，但你也可以通过 RDS 和 CDS 来动态更新路由和集群。特别是当你的基础架构规模非常大时，你可以通过配置动态服务发现的规则来简化你的重复配置成本，并且可以将同一套动态服务发现规则应用于多个 Envoy 集群。\n现在你已经了解了如何配置基本的路由、集群和监听器，下一节我们将学习如何在增量部署中设置更复杂的流量切换和过滤规则。\n参考 # Envoy 官方文档中文版 ","date":"2018年6月29日","externalUrl":null,"permalink":"/posts/routing-basics/","section":"博客","summary":"本文将更详细地讨论 Envoy 的 HTTP 路由，如果你已经看过了我的上篇文章：","title":"Envoy 基础教程：HTTP 路由解析","type":"posts"},{"content":" 前言 # 过去一年中，Kubernetes 已经赢得了容器编排大战，如果说 2017 年是 Kubernetes 的元年，那么 2018 将会是 Service Mesh（服务网格） 的元年，在未来两年中，Service Mesh 将迎来爆发式增长，成为下一代的微服务架构。\nIstio 作为 Service Mesh 新秀，初出茅庐便声势浩荡，前有 Google，IBM 和 Lyft 倾情奉献，后有业界大佬俯首膜拜。作为一名斜杠青年，如果再不停下脚步认真审视一下这位后起之秀，未免显得太不符合潮流了。\nIstio 这个大家庭的家庭成员很多，为了能够顺利打入 Istio 内部，我们先从它的核心家庭成员 Envoy 入手。\n从今天起，我将带领大家从零开始学习和使用 Envoy，着重于经验分享和总结，同时也会有相关的概念解析，希望能够帮助大家少走弯路，能不采坑尽量不采坑。\n本篇是 Envoy 系列教程的第一篇，介绍如何在笔记本电脑上运行 Envoy、测试代理配置并观察结果，让我们开始吧！\n前提 # 你可以选择从源代码构建 Envoy，但最简单的办法是通过 Docker 容器来运行。所以在开始之前，你需要安装并配置以下工具：\nDocker Docker Compose Git curl 我们使用 Docker 和 Docker Compose 来编排和运行 Envoy 的示例服务，使用 curl 来访问 Envoy 示例服务。\n部署 Envoy # Envoy 官方提供了一组 Envoy 的用例，我们将要使用的用例是前端代理，它会将流量发送到两个服务后端。首先克隆 Envoy 的代码仓库并转到 examples/front-proxy 目录：\n$ git clone https://github.com/envoyproxy/envoy $ cd envoy/examples/front-proxy 后端服务 是一个非常简单的 Flask 应用程序，在 service.py 中定义。其中 Envoy 作为一个边车（Sidecar）伴随每个服务一起运行在同一个容器中，所有的规则配置都通过 YAML 文件 service-envoy.yaml 来完成。最后 Dockerfile-service 创建一个在启动时同时运行服务和 Envoy 的容器。\n前端代理 比后端服务更简单，它使用配置文件 front-envoy.yaml 来运行 Envoy，使用 Dockerfile-frontenvoy 来构建容器镜像。\ndocker-compose.yaml 文件描述了如何构建、打包和运行前端代理与服务。\n整体架构如下：\n使用 docker-compose 启动容器：\n$ docker-compose up --build -d $ docker-compose ps Name Command State Ports ---------------------------------------------------------------------------------------------------------------- frontproxy_front-envoy_1 /bin/sh -c /usr/local/bin/ ... Up 0.0.0.0:8000-\u0026gt;80/tcp, 0.0.0.0:8001-\u0026gt;8001/tcp frontproxy_service1_1 /bin/sh -c /usr/local/bin/ ... Up 80/tcp frontproxy_service2_1 /bin/sh -c /usr/local/bin/ ... Up 80/tcp 该命令将会启动一个前端代理和两个服务实例：service1 和 service2。\n配置 Envoy # 为了达到演示的目的，本文采用的是 Envoy 的静态配置。后续教程将会告诉你们如何使用动态配置来发挥 Envoy 的强大功能。\n为了了解 Envoy 是如何配置的，先来看看 docker-compose.yaml 文件的前端代理部分的配置：\nfront-envoy: build: context: ../ dockerfile: front-proxy/Dockerfile-frontenvoy volumes: - ./front-envoy.yaml:/etc/front-envoy.yaml networks: - envoymesh expose: - \u0026#34;80\u0026#34; - \u0026#34;8001\u0026#34; ports: - \u0026#34;8000:80\u0026#34; - \u0026#34;8001:8001\u0026#34; 从上到下做了这么几件事：\n使用位于当前目录中的 Dockerfile-frontenvoy 构建镜像。 将 front-envoy.yaml 文件作为 /etc/front-envoy.yaml 挂载到容器中的 /etc 目录。 为这个容器创建并使用名为 envoymesh 的 Docker 网络。 暴露 80 端口（用于一般通用流量）和 8001 端口（用于管理服务）。 将主机的 8000 端口和 8001 端口分别映射到容器的 80 端口和 8001 端口。 前面已经了解到了前端代理使用 front-envoy.yaml 来配置 Envoy，下面来深入解析一下。该配置文件有两大配置项：static_resources 和 admin：\nstatic_resources: admin: admin 配置项的内容非常简单：\nadmin: access_log_path: \u0026#34;/dev/null\u0026#34; address: socket_address: address: 0.0.0.0 port_value: 8001 access_log_path 字段的值设置为 /dev/null，意味着 admin 服务的访问日志将会被丢弃，在测试或生产环境中，你最好将这个值修改为不同的目录。socket_address 字段告诉 Envoy 创建一个监听在 8001 端口的 admin 服务。\nstatic_resources 配置项定义了一组静态配置的集群（Cluster）和侦听器（Listener）。\n集群是 Envoy 连接到的一组逻辑上相似的上游主机。Envoy 通过服务发现发现集群中的成员。Envoy 可以通过主动运行状况检查来确定集群成员的健康状况。Envoy 如何将请求路由到集群成员由负载均衡策略确定。\n侦听器是服务(程序)监听者，就是真正干活的。 它是可以由下游客户端连接的命名网络位置（例如，端口、unix域套接字等）。\nListener 配置 # 该示例中的前端代理有一个监听在 80 端口的侦听器，并配置了一个监听器过滤器链（filter_chains），用来管理 HTTP 流量：\nlisteners: - address: socket_address: address: 0.0.0.0 port_value: 80 filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route 在 HTTP 连接管理过滤器中，每一个虚拟主机都有单独的配置，并且都配置为接收所有域的流量：\nvirtual_hosts: - name: backend domains: - \u0026#34;*\u0026#34; routes: - match: prefix: \u0026#34;/service/1\u0026#34; route: cluster: service1 - match: prefix: \u0026#34;/service/2\u0026#34; route: cluster: service2 HTTP 路由规则将 /service/1 和 /service/1 的流量转发到各自的 Cluster。\nCluster 配置 # 接下来看一下静态 Cluster 的定义：\nclusters: - name: service1 connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service1 port_value: 80 - name: service2 connect_timeout: 0.25s type: strict_dns lb_policy: round_robin http2_protocol_options: {} hosts: - socket_address: address: service2 port_value: 80 在 Cluster 的配置中，你可以自定义超时、断路器和服务发现等。Cluster 由 Endpoint（端点）组成，其中 Endpoint 是一组可以为 Cluster 的请求提供服务的网络位置。本例中的 Endpoint 是通过 DNS 域名的方式定义的，Envoy 可以从域名中读取 Endpoint。Endpoint 也可以直接定义为 socket 地址，或者通过 EDS（Endpoint Discovery Service）动态读取。\n修改配置 # 你可以通过修改配置文件重新构建镜像来进行测试。Listener filter（监听器过滤器）的作用是在不更改 Envoy 的核心功能的情况下添加更多的集成功能。例如，如果想要将访问日志添加到 HTTP 过滤器中，可以在 filter 的配置中添加 access_log 配置项：\n- filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http access_log: - name: envoy.file_access_log config: path: \u0026#34;/var/log/access.log\u0026#34; route_config: 然后停止服务，重新构建并运行容器：\n$ docker-compose down $ docker-compose up --build -d 通过 curl 访问服务，然后通过 docker-compose exec front-envoy /bin/bash 命令进入容器的终端，你会看到 /var/log/access.log 文件记录着你的请求结果。\nAdmin Server # Envoy 的一大特色是内置的 Admin 服务，如果你在浏览器中访问 http://localhost:8001 ，可以看到 Envoy admin 提供以下管理 API 端点。\n命令 描述 / Admin 主页 /certs 打印机器上的 certs /clusters upstream cluster 状态 /config_dump 输出当前的 Envoy 配置 /cpuprofiler 开启/关闭 CPU profiler /healthcheck/fail 导致服务失败健康检查 /healthcheck/ok 导致服务通过健康检查 /help 打印管理命令的帮助信息 /hot_restart_version 打印热重启兼容版本 /listeners 打印 listener 地址 /logging 查询/更改日志级别 /quitquitquit 退出服务 /reset_counters 将计数器重置为 1 /runtime 打印运行时值 /runtime_modify 修改运行时值 /server_info 打印服务器版本/状态信息 /stats 打印服务器状态统计信息 /stats/prometheus 打印 prometheus 格式的服务器状态统计信息 通过 API 管理端可以对 Envoy 进行动态配置，参考 v2 API reference。\n进一步探索 # 如果你有兴趣探索 Envoy 的更多其他功能， Envoy 官方示例还有一些更复杂的拓扑结构，但这些示例仍然使用静态类型的服务发现。如果你还想了解有关如何在生产环境中使用 Envoy 的更多信息，请参阅 Integrating Service Discovery with Envoy 以了解将 Envoy 与现有环境集成的意义。如果你在测试 Envoy 的过程中遇到问题，请访问 Getting Help 页面以获取更多的帮助信息。\n参考 # Envoy 的架构与基本术语 使用 Envoy 作为前端代理 扫一扫关注微信公众号 ","date":"2018年6月28日","externalUrl":null,"permalink":"/posts/run-envoy-on-your-laptop/","section":"博客","summary":"前言 # 过去一年中，Kubernetes 已经赢得了容器编排大战","title":"Envoy 基础教程：入门篇","type":"posts"},{"content":"在过去一年中，服务网格技术的崛起引发了吃瓜群众对 Istio 的持续关注，而 Istio 的核心组件 Envoy 是一款由 Lyft 开源的，使用 C++ 编写的 L7 代理和通信总线，目前是 CNCF 旗下的开源项目，代码托管在 GitHub 上，它也是 Istio service mesh 中默认的 data plane。\n目前网上关于 Envoy 的文档非常少，能够找到的比较权威的资料只有官方文档，但官方文档的痛点是只有理论概念，没有实际应用指南。\nEnvoy 官方文档 Envoy 官方文档中文版 要想真正掌握 Envoy，只有通过实践融入该语境才能真正理解这门技术，而目前能够找到的最佳实践项目就是 Envoy Docker Shim。在实践该项目之前，你需要了解 Envoy 中的基本术语和概念，可以参考 Jimmy Song 的文章： Envoy 的架构与基本术语。下面我就为大家简单地介绍下这个项目。\nEnvoy Docker Shim # Envoy Docker Shim 是一个预生产项目，它使用 Envoy 来替代 Docker 的 docker-proxy，这样做的目的是为在 Docker 上运行的服务启用 Envoy 的指标收集和分布式路由跟踪功能。使用了 Envoy Docker Shim，就相当于获得了服务网格的一半功能。该项目由以下四个组件组成：\nenvoy-docker-server : 运行在 Docker 主机上的注册中心，用来向 Envoy 提供 服务发现 API。 envoy-docker-shim : 命令行应用程序，采用与 docker-proxy 相同的命令参数，但它的作用是将新的端点（endpoint）注册到注册中心。 Envoy 实例 : 以 host 网络模式运行在 Docker 容器中。 resync : shell 脚本，当容器重启时用来恢复注册中心的状态。 通过将这些组件结合在一起，就形成了一个通过 Envoy 来代理 HTTP 和 TCP 流量的系统，对 UDP 流量的处理继续使用 docker-proxy 的代码逻辑，目前暂不支持 SCTP 协议。\n安装步骤 # 要想使用 Envoy Docker Shim，首先需要修改 dockerd 的启动参数：\n--userland-proxy-path=/path/to/envoy-docker-shim : 使用 envoy-docker-shim 替换 docker-proxy，用来告诉 Envoy 需要监听的服务以及如何转发。 --iptables=false : 禁用 Iptables 来转发 Docker 的流量，强制使用 Envoy 来转发 Docker 流量。 一旦设置了 \u0026ndash;iptables=false，Docker 流量就不会再通过内核直接流入桥接网络。通过配置不同的代理模式，所有的流量都会在 4 层或 7 层进行代理。\n修改完启动参数后，就可以重启 Docker 服务了：\n$ systemctl daemon-reload $ systemctl restart docker 下载二进制文件和脚本 # 安装 envoy-docker-server：\n$ go get github.com/Nitro/envoy-docker-shim/cmd/envoy-docker-server 安装 envoy-docker-shim：\n$ go get github.com/Nitro/envoy-docker-shim/cmd/envoy-docker-shim 克隆 envoy-docker-shim 项目：\n$ git clone https://github.com/Nitro/envoy-docker-shim 将脚本 resync 复制到 /usr/local/bin 目录下：\n$ cd envoy-docker-shim $ cp scripts/resync /usr/local/bin 配置 envoy-docker-server 和 resync # 修改 examples 目录下的 envoy-docker-server.service 文件：\n[Unit] Description=Envoy Docker Shim PartOf=docker.service [Service] ExecStart=$GOPATH/bin/envoy-docker-server ExecStartPost=/usr/local/bin/resync ExecReload=/usr/local/bin/resync KillMode=process Restart=on-failure Type=simple [Install] WantedBy=multi-user.target 为了防止出现状态不同步的问题，envoy-docker-server 仅将状态存储在内存中，一旦重启了 envoy-docker-server，就需要执行脚本 resync 来同步容器的状态。\n将 envoy-docker-server.service 文件复制到 /etc/systemd/system/ 目录下并启动该服务：\n$ cp examples/envoy-docker-server.service /etc/systemd/system/ $ systemctl start envoy-docker-server 启动 Envoy 实例 # 在 examples 目录中包含了一个 envoy.yaml 文件，你可以通过该配置文件启动 Envoy 实例（在 1.6 和 1.7 版本上测试通过）。你也可以选择通过 Docker 容器来运行 Envoy 实例，这里我们通过容器的方式来启动 Envoy：\n$ docker run -d --name envoyproxy --restart always --net host --cap-add NET_ADMIN -e ENVOY_BASE_ID=100 -e ENVOY_PORT=9902 -e ENVOY_UI_LISTEN_PORT=8081 gonitro/envoyproxy:1.7.0-27960f3-tracing 该 Envoy 容器还提供了一个 UI 来展示指标和路由，可以通过在浏览器中输入 url：http://host_ip:8081 来打开 UI 界面：\n至此，Envoy Docker Shim 已经完美地完成了替代 docker-proxy 的工作，接下来就可以不通过 iptables 而使用 Envoy 来实现 Docker 容器的端口映射啦！\n容器配置 # 一切准备就绪后，就可以启动一个容器试试了。如果想完美地使用 envoy-docker-shim，你需要在启动容器时指定两个或三个标签，虽然这不是必须的，但可以更方便地跟踪流量。这些 Docker 标签被映射成报告给 Zipkin 或 Jaeger 的服务的标签。这三个标签是从 Sidecar 项目集成而来的：\nEnvironmentName : 这个标签可以成某个客户的名字，或者表示这是生产项目还是测试项目，反正只要对你来说有意义就行。 ServiceName : 设置 Envoy 跟踪的服务名。例如，如果你将这个标签设置为 nginx，将 EnvironmentName 标签设置为 prod，那么该服务名为 nginx-prod。 ProxyMode : 设置 Envoy 的代理模式。默认使用 http 代理模式，如果想使用 tcp 代理模式，需要指定该标签值为 tcp。 示例：\n$ docker run -d -p 80:80 -p 443:443 -l EnvironmentName=proxy -l ServiceName=nginx -l ProxyMode=tcp nginx:alpine 打开 Envoy UI：\n可以看到 nginx 的服务名为 nginx-proxy。\n","date":"2018年6月22日","externalUrl":null,"permalink":"/posts/envoy-docker-shim/","section":"博客","summary":"在过去一年中，服务网格技术的崛起引发了吃瓜群众对 Istio 的持续关注","title":"使用 envoy-docker-shim 替代 docker-proxy","type":"posts"},{"content":"从 Kubernetes 1.8 开始，资源使用指标（如容器 CPU 和内存使用率）可以通过 Metrics API 在 Kubernetes 中获取。 这些指标可以直接被用户访问(例如通过使用 kubectl top 命令)，或由集群中的控制器使用(例如，Horizontal Pod Autoscale 可以使用这些指标作出决策)。\n例如，可以使用 kubectl top node 和 kubectl top pod 查看资源使用情况：\n$ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% 192.168.123.248 245m 12% 2687Mi 34% 192.168.123.249 442m 22% 3270Mi 42% 192.168.123.250 455m 22% 4014Mi 52% $ kubectl top pod NAME CPU(cores) MEMORY(bytes) details-v1-64b86cd49-52g82 0m 11Mi podinfo-6b86c8ccc9-5qr8b 0m 7Mi podinfo-6b86c8ccc9-hlxm7 0m 12Mi podinfo-6b86c8ccc9-qxhng 0m 6Mi Resource Metrics API # 通过 Metrics API，您可以获取指定 node 或 pod 当前使用的资源量。这个 API 不存储指标值， 因此想要获取某个指定 node 10分钟前的资源使用量是不可能的。\nMetrics API 和其他的 API 没有什么不同，它可以通过与 /apis/metrics.k8s.io/ 路径下的其他 Kubernetes API 相同的端点来发现，并且提供了相同的安全性、可扩展性和可靠性保证，Metrics API 在 k8s.io/metrics 仓库中定义，你可以在这里找到关于 Metrics API 的更多信息。\n注意 : Metrics API 需要在集群中部署 Metrics Server。否则它将不可用。\nMetrics Server # Metrics Server 实现了Resource Metrics API。\nMetrics Server 是集群范围资源使用数据的聚合器。 从 Kubernetes 1.8 开始，它作为一个 Deployment 对象默认部署在由 kube-up.sh 脚本创建的集群中。 如果你使用了其他的 Kubernetes 安装方法，您可以使用 Kubernetes 1.7+ (请参阅下面的详细信息) 中引入的 deployment yamls 文件来部署。\nMetrics Server 从每个节点上的 Kubelet 公开的 Summary API 中采集指标信息。\n通过在主 API server 中注册的 Metrics Server Kubernetes 聚合器 来采集指标信息， 这是在 Kubernetes 1.7 中引入的。在 设计文档 中可以了解到有关 Metrics Server 的更多信息。\ncustom metrics api # 该 API 允许消费者访问通过任意指标描述的 Kubernetes 资源。如果你想实现这个 API Service，请参阅 kubernetes-incubator/custom-metrics-apiserver，这是一个用来实现 Kubernetes 自定义指标的框架。\nHPA # 自动伸缩是一种根据资源使用情况自动伸缩工作负载的方法。自动伸缩在 Kubernetes 中有两个维度：\nCluster Autoscaler : 用来处理节点扩容。 Horizontal Pod Autoscaler : 自动缩放 rs 或 rc 中的 pod。 Cluster Autoscaler 和 Horizontal Pod Autoscaler 一起可用于动态调整集群的计算能力。虽然 Cluster Autoscaler 高度依赖于托管集群的云服务商提供的底层功能，但是 HPA 可以独立于你的 IaaS/PaaS 提供商进行操作。\nKubernetes 自 1.2 版本引入 HPA 机制，到 1.6 版本之前一直是通过 kubelet 来获取监控指标来判断是否需要扩缩容，1.6 版本之后必须通过 API server、Heapseter 或者 kube-aggregator 来获取监控指标。\nKubernetes 1.7 引入了聚合层，允许第三方应用程序通过注册为 API 附加组件来扩展 Kubernetes API。 自定义指标 API 以及聚合层使得像 Prometheus 这样的监控系统可以向 HPA 控制器公开针对特定应用程序的指标。\nhpa 实现了一个控制环，可以周期性的从 Resource Metrics API 查询特定应用的 CPU 和内存信息。\n实战 # 以下是一份为 Kubernetes 1.9 或更高版本配置 HPA v2 的分步指南。首先将会安装提供核心指标的 Metrics Server 附件组件，然后使用一个 demo 来演示基于 CPU 和内存使用的 Pod 的自动伸缩。在指南的第二部分，将会部署 Prometheus 和一个 custom metrics apiserver。聚合层会自动注册 custom metrics apiserver，然后通过一个 demo 来演示自定义指标的 HPA。\n前提 # 开启聚合层 API go 1.8+ 克隆 k8s-prom-hpa 仓库 $ cd $GOPATH $ git clone https://github.com/stefanprodan/k8s-prom-hpa 安装 Metrics Server # Kubernetes Metrics Server 是一个集群范围内的资源使用量的聚合器，它是 Heapster 的继承者。Metrics Server 通过汇集来自 kubernetes.summary_api 的数据来收集 node 和 pod 的 CPU 和内存使用情况。summary API 是用于将数据从 Kubelet/cAdvisor 传递到 Metrics Server 的高效内存 API。\n在安装 Metrics Server 之前需要先进行如下配置：\n将 kube-controller-manager 的启动参数 --horizontal-pod-autoscaler-use-rest-clients 的值设置为 true。 在 kube-controller-manager 的启动参数 \u0026ndash;master 设置为 kube-apiserver 的地址，如：--master=http://172.20.0.113:8080。 在 kube-system 命名空间部署 metrics-server：\n$ kubectl create -f ./metrics-server 一分钟后，度量服务器开始报告节点和 Pod 的 CPU 和内存使用情况。 查看 nodes 指标：\n$ kubectl get --raw \u0026#34;/apis/metrics.k8s.io/v1beta1/nodes\u0026#34; | jq . 查看 pods 指标：\n$ kubectl get --raw \u0026#34;/apis/metrics.k8s.io/v1beta1/pods\u0026#34; | jq . 基于 CPU 和内存使用的自动缩放 # 下面使用一个基于 golang 的小程序来测试 HPA。\n在 default 命名空间中部署 podinfo：\n$ kubectl create -f ./podinfo/podinfo-svc.yaml,./podinfo/podinfo-dep.yaml 可以通过 http://PODINFO_SVC_IP:9898 来访问 podinfo。\n接下来定义一个保持最少两个副本的 HPA，如果 CPU 平均使用量超过 80％ 或内存超过 200Mi，则最高可扩展到 10 个副本：\napiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: podinfo spec: scaleTargetRef: apiVersion: extensions/v1beta1 kind: Deployment name: podinfo minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu targetAverageUtilization: 80 - type: Resource resource: name: memory targetAverageValue: 200Mi 创建 HPA：\n$ kubectl create -f ./podinfo/podinfo-hpa.yaml 几秒钟之后，HPA 控制器与 metrics server 进行通信，然后获取 CPU 和内存使用情况。\n$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE podinfo Deployment/podinfo 2826240 / 200Mi, 15% / 80% 2 10 2 5m 为了提高 CPU 使用率、运行 rakyll/hey 进行压力测试：\n#install hey $ go get -u github.com/rakyll/hey #do 10K requests hey -n 10000 -q 10 -c 5 http://PODINFO_SVC_IP:9898/ 你可以通过以下命令获取 HPA event：\n$ kubectl describe hpa podinfo Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulRescale 7m horizontal-pod-autoscaler New size: 4; reason: cpu resource utilization (percentage of request) above target Normal SuccessfulRescale 3m horizontal-pod-autoscaler New size: 8; reason: cpu resource utilization (percentage of request) above target 先将 podinfo 删除，稍后将会重新部署：\n$ kubectl delete -f ./podinfo/podinfo-hpa.yaml,./podinfo/podinfo-dep.yaml,./podinfo/podinfo-svc.yaml 安装 Custom Metrics Server # 为了让 HPA 可以根据 custom metrics 进行扩展，你需要有两个组件：\nPrometheus : 从应用程序中收集指标并将其存储为 Prometheus 时间序列数据库。 custom-metrics-apiserver : 使用 k8s-prometheus-adapter 提供的 metrics 来扩展 Kubernetes 自定义指标 API。 创建 monitoring 命名空间：\n$ kubectl create -f ./namespaces.yaml 将 Prometheus v2 部署到 monitoring 命名空间：\n$ kubectl create -f ./prometheus 生成 Prometheus adapter 所需的 TLS 证书：\n$ make certs 部署 custom-metrics-apiserver：\n$ kubectl create -f ./custom-metrics-api 列出由 Prometheus 提供的自定义指标：\n$ kubectl get --raw \u0026#34;/apis/custom.metrics.k8s.io/v1beta1\u0026#34; | jq . 获取 monitoring 命名空间中所有 pod 的 FS 信息：\n$ kubectl get --raw \u0026#34;/apis/custom.metrics.k8s.io/v1beta1/namespaces/monitoring/pods/*/fs_usage_bytes\u0026#34; | jq . 基于自定义指标的自动扩容 # 在 default 命名空间中部署 podinfo：\n$ kubectl create -f ./podinfo/podinfo-svc.yaml,./podinfo/podinfo-dep.yaml podinfo 应用暴露了一个自定义的度量指标：http_requests_total。Prometheus adapter（即 custom-metrics-apiserver）删除了 _total 后缀并将该指标标记为 counter metric。\n从自定义指标 API 获取每秒的总请求数：\n$ kubectl get --raw \u0026#34;/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/http_requests\u0026#34; | jq . { \u0026#34;kind\u0026#34;: \u0026#34;MetricValueList\u0026#34;, \u0026#34;apiVersion\u0026#34;: \u0026#34;custom.metrics.k8s.io/v1beta1\u0026#34;, \u0026#34;metadata\u0026#34;: { \u0026#34;selfLink\u0026#34;: \u0026#34;/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/%2A/http_requests\u0026#34; }, \u0026#34;items\u0026#34;: [ { \u0026#34;describedObject\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;Pod\u0026#34;, \u0026#34;namespace\u0026#34;: \u0026#34;default\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;podinfo-6b86c8ccc9-kv5g9\u0026#34;, \u0026#34;apiVersion\u0026#34;: \u0026#34;/__internal\u0026#34; }, \u0026#34;metricName\u0026#34;: \u0026#34;http_requests\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2018-01-10T16:49:07Z\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;901m\u0026#34; }, { \u0026#34;describedObject\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;Pod\u0026#34;, \u0026#34;namespace\u0026#34;: \u0026#34;default\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;podinfo-6b86c8ccc9-nm7bl\u0026#34;, \u0026#34;apiVersion\u0026#34;: \u0026#34;/__internal\u0026#34; }, \u0026#34;metricName\u0026#34;: \u0026#34;http_requests\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2018-01-10T16:49:07Z\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;898m\u0026#34; } ] } m 表示 毫，例如，901m 表示 901 毫次/每秒。\n创建一个 HPA，如果请求数超过每秒 10 次将扩大 podinfo 副本数：\napiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: podinfo spec: scaleTargetRef: apiVersion: extensions/v1beta1 kind: Deployment name: podinfo minReplicas: 2 maxReplicas: 10 metrics: - type: Pods pods: metricName: http_requests targetAverageValue: 10 在 default 命名空间部署 podinfo HPA：\n$ kubectl create -f ./podinfo/podinfo-hpa-custom.yaml 几秒钟后 HPA 从 metrics API 获取 http_requests 的值：\n$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE podinfo Deployment/podinfo 899m / 10 2 10 2 1m 以每秒 25 个请求数的速度给 podinfo 加压：\n#install hey $ go get -u github.com/rakyll/hey #do 10K requests rate limited at 25 QPS $ hey -n 10000 -q 5 -c 5 http://PODINFO_SVC_IP:9898/healthz 几分钟后，HPA 开始扩大 podinfo 的副本数：\n$ kubectl describe hpa podinfo Name: podinfo Namespace: default Reference: Deployment/podinfo Metrics: ( current / target ) \u0026#34;http_requests\u0026#34; on pods: 9059m / 10 Min replicas: 2 Max replicas: 10 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulRescale 2m horizontal-pod-autoscaler New size: 3; reason: pods metric http_requests above target 以目前的请求速度，podinfo 的副本数永远不会扩展到最大值，三个副本足以让每个 Pod 的请求速度保持在每秒 10 次以下。\n停止加压后，HPA 会将副本数缩减成最小值：\nEvents: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulRescale 5m horizontal-pod-autoscaler New size: 3; reason: pods metric http_requests above target Normal SuccessfulRescale 21s horizontal-pod-autoscaler New size: 2; reason: All metrics below target 总结 # 并非所有的系统都可以仅依靠 CPU 和内存指标来满足 SLA，大多数 Web 应用的后端都需要基于每秒的请求数量进行弹性伸缩来处理突发流量。对于 ETL 应用程序，可以通过设置 Job 队列长度超过某个阈值来触发弹性伸缩。通过 Prometheus 来监控应用程序并暴露出用于弹性伸缩的指标，可以微调应用程序以更好地处理突发事件，从而确保其高可用性。\n参考 # Kubernetes Horizontal Pod Autoscaler with Prometheus custom metrics k8s-prometheus-adapter 扫一扫关注微信公众号 ","date":"2018年6月19日","externalUrl":null,"permalink":"/posts/custom-metrics-hpa/","section":"博客","summary":"从 Kubernetes 1.8 开始，资源使用指标（如容器 CPU 和内存使用率）可以通过 Metrics API","title":"使用自定义指标进行弹性伸缩","type":"posts"},{"content":"Aggregated（聚合的）API server 是为了将原来的 API server 这个巨石（monolithic）应用给拆分开，为了方便用户开发自己的 API server 集成进来，而不用直接修改 Kubernetes 官方仓库的代码，这样一来也能将 API server 解耦，方便用户使用实验特性。这些 API server 可以跟 core API server 无缝衔接，使用 kubectl 也可以管理它们。\n在 1.7+ 版本中，聚合层和 kube-apiserver 一起运行。在扩展资源被注册前，聚合层不执行任何操，要注册其 API,用户必需添加一个 APIService 对象，该对象需在 Kubernetes API 中声明 URL 路径，聚合层将发送到该 API 路径(e.g. /apis/myextension.mycompany.io/v1/…)的所有对象代理到注册的 APIService。\n通常，通过在集群中的一个 Pod 中运行一个 extension-apiserver 来实现 APIService。如果已添加的资源需要主动管理，这个 extension-apiserver 通常需要和一个或多个控制器配对。\n创建聚合层 API 证书 # 如果想开启聚合层 API，需要创建几个与聚合层 API 相关的证书。\n安装 cfssl # 方式一：直接使用二进制源码包安装\n$ wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 $ chmod +x cfssl_linux-amd64 $ mv cfssl_linux-amd64 /usr/local/bin/cfssl $ wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 $ chmod +x cfssljson_linux-amd64 $ mv cfssljson_linux-amd64 /usr/local/bin/cfssljson $ wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 $ chmod +x cfssl-certinfo_linux-amd64 $ mv cfssl-certinfo_linux-amd64 /usr/local/bin/cfssl-certinfo $ export PATH=/usr/local/bin:$PATH 方式二：使用go命令安装\n$ go get -u github.com/cloudflare/cfssl/cmd/... $ls $GOPATH/bin/cfssl* cfssl cfssl-bundle cfssl-certinfo cfssljson cfssl-newkey cfssl-scan 在 $GOPATH/bin 目录下得到以 cfssl 开头的几个命令。\n注意：以下文章中出现的 cat 的文件名如果不存在需要手工创建。\n创建 CA (Certificate Authority) # 创建 CA 配置文件\n$ mkdir /root/ssl $ cd /root/ssl $ cfssl print-defaults config \u0026gt; config.json $ cfssl print-defaults csr \u0026gt; csr.json # 根据config.json文件的格式创建如下的ca-config.json文件 # 过期时间设置成了 87600h $ cat \u0026gt; aggregator-ca-config.json \u0026lt;\u0026lt;EOF { \u0026#34;signing\u0026#34;: { \u0026#34;default\u0026#34;: { \u0026#34;expiry\u0026#34;: \u0026#34;87600h\u0026#34; }, \u0026#34;profiles\u0026#34;: { \u0026#34;aggregator\u0026#34;: { \u0026#34;usages\u0026#34;: [ \u0026#34;signing\u0026#34;, \u0026#34;key encipherment\u0026#34;, \u0026#34;server auth\u0026#34;, \u0026#34;client auth\u0026#34; ], \u0026#34;expiry\u0026#34;: \u0026#34;87600h\u0026#34; } } } } EOF 字段说明：\nprofiles : 可以定义多个 profiles，分别指定不同的过期时间、使用场景等参数；后续在签名证书时使用某个 profile。 signing ：表示该证书可用于签名其它证书；生成的 aggregator-ca.pem 证书中 CA=TRUE。 server auth ：表示 Client 可以用该 CA 对 Server 提供的证书进行验证。 client auth ：表示 Server 可以用该 CA 对 Client 提供的证书进行验证。 创建 CA 证书签名请求\n创建 aggregator-ca-csr.json 文件，内容如下：\n{ \u0026#34;CN\u0026#34;: \u0026#34;aggregator\u0026#34;, \u0026#34;key\u0026#34;: { \u0026#34;algo\u0026#34;: \u0026#34;rsa\u0026#34;, \u0026#34;size\u0026#34;: 2048 }, \u0026#34;names\u0026#34;: [ { \u0026#34;C\u0026#34;: \u0026#34;CN\u0026#34;, \u0026#34;ST\u0026#34;: \u0026#34;Shanghai\u0026#34;, \u0026#34;L\u0026#34;: \u0026#34;Shanghai\u0026#34;, \u0026#34;O\u0026#34;: \u0026#34;k8s\u0026#34;, \u0026#34;OU\u0026#34;: \u0026#34;System\u0026#34; } ], \u0026#34;ca\u0026#34;: { \u0026#34;expiry\u0026#34;: \u0026#34;87600h\u0026#34; } } 字段说明：\n\u0026ldquo;CN\u0026rdquo; ：Common Name，kube-apiserver 从证书中提取该字段作为请求的用户名 (User Name)；浏览器使用该字段验证网站是否合法。 \u0026ldquo;O\u0026rdquo; ：Organization，kube-apiserver 从证书中提取该字段作为请求用户所属的组 (Group)； 生成 CA 证书和私钥\n$ cfssl gencert -initca aggregator-ca-csr.json | cfssljson -bare aggregator-ca $ ls aggregator-ca* aggregator-ca-config.json aggregator-ca.csr aggregator-ca-csr.json aggregator-ca-key.pem aggregator-ca.pem 创建 kubernetes 证书 # 创建 aggregator 证书签名请求文件 aggregator-csr.json ：\n{ \u0026#34;CN\u0026#34;: \u0026#34;aggregator\u0026#34;, \u0026#34;hosts\u0026#34;: [ \u0026#34;127.0.0.1\u0026#34;, \u0026#34;192.168.123.250\u0026#34;, \u0026#34;192.168.123.248\u0026#34;, \u0026#34;192.168.123.249\u0026#34;, \u0026#34;10.254.0.1\u0026#34;, \u0026#34;kubernetes\u0026#34;, \u0026#34;kubernetes.default\u0026#34;, \u0026#34;kubernetes.default.svc\u0026#34;, \u0026#34;kubernetes.default.svc.cluster\u0026#34;, \u0026#34;kubernetes.default.svc.cluster.local\u0026#34; ], \u0026#34;key\u0026#34;: { \u0026#34;algo\u0026#34;: \u0026#34;rsa\u0026#34;, \u0026#34;size\u0026#34;: 2048 }, \u0026#34;names\u0026#34;: [ { \u0026#34;C\u0026#34;: \u0026#34;CN\u0026#34;, \u0026#34;ST\u0026#34;: \u0026#34;Shanghai\u0026#34;, \u0026#34;L\u0026#34;: \u0026#34;Shanghai\u0026#34;, \u0026#34;O\u0026#34;: \u0026#34;k8s\u0026#34;, \u0026#34;OU\u0026#34;: \u0026#34;System\u0026#34; } ] } 如果 hosts 字段不为空则需要指定授权使用该证书的 IP 或域名列表，由于该证书后续被 etcd 集群和 kubernetes master 集群使用，所以上面分别指定了 etcd 集群、kubernetes master 集群的主机 IP 和 kubernetes 服务的服务 IP（一般是 kube-apiserver 指定的 service-cluster-ip-range 网段的第一个 IP，如 10.254.0.1）。 以上物理节点的 IP 也可以更换为主机名。 生成 aggregator 证书和私钥\n$ cfssl gencert -ca=aggregator-ca.pem -ca-key=aggregator-ca-key.pem -config=aggregator-ca-config.json -profile=aggregator aggregator-csr.json | cfssljson -bare aggregator $ ls aggregator* aggregator.csr aggregator-csr.json aggregator-key.pem aggregator.pem 分发证书 # 将生成的证书和秘钥文件（后缀名为.pem）拷贝到 Master 节点的 /etc/kubernetes/ssl 目录下备用。\n$ cp *.pem /etc/kubernetes/ssl 开启聚合层 API # kube-apiserver 增加以下配置：\n--requestheader-client-ca-file=/etc/kubernetes/ssl/aggregator-ca.pem --requestheader-allowed-names=aggregator --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-group-headers=X-Remote-Group --requestheader-username-headers=X-Remote-User --proxy-client-cert-file=/etc/kubernetes/ssl/aggregator.pem --proxy-client-key-file=/etc/kubernetes/ssl/aggregator-key.pem 前面创建的证书的 CN 字段的值必须和参数 \u0026ndash;requestheader-allowed-names 指定的值 aggregator 相同。 重启 kube-apiserver：\n$ systemctl daemon-reload $ systemctl restart kube-apiserver 如果 kube-proxy 没有在 Master 上面运行，kube-proxy 还需要添加配置：\n--enable-aggregator-routing=true 参考 # Extending the Kubernetes API with the aggregation layer Configure the aggregation layer 创建TLS证书和秘钥 ","date":"2018年6月19日","externalUrl":null,"permalink":"/posts/api-aggregation/","section":"博客","summary":"Aggregated（聚合的）API server 是为了将原来的 API server 这个","title":"Kubernetes API 扩展","type":"posts"},{"content":"几个月前，我在更新 Kubernetes 集群中的 Deployment 时发现了一个很奇怪的连接超时现象，在更新 Deployment 之后的 30 秒到两分钟左右，所有与以该 Deployment 作为服务后端的 Service 的连接都会超时或失败。同时我还注意到其他应用在这段时间内也会出现莫名其妙的延迟现象。\n一开始我怀疑是 应用没有优雅删除导致的，但当我在更新 Deployment 的过程中（删除旧的 Pod，启动新的 Pod）通过 curl 来测试该应用的健康检查（liveness）和就绪检查（readiness）Endpoints 时，很快就排除了这个可能性。\n我开始怀疑人生，开始怀疑我的职业选择，几个小时之后我忽然想起来 Service 并不是直接与 Deployment 关联的，而是按照标签对一组提供相同功能的 Pods 的抽象，并为它们提供一个统一的入口。更重要的是，Service 是由一组 Endpoint 组成的，只要 Service 中的一组 Pod 发生变更，Endpoint 就会被更新。\n想到这里，就可以继续排查问题了。下面在更新 Deployment 的过程中通过 watch 命令来观察有问题的 Service 的 Endpoint。\n$ watch kubectl describe endpoints [endpoint name] 然后我就发现了罪魁祸首，在旧 Pod 被移除的 30 秒到几分钟左右的时间段内，这些被删除的 Pod 的 IP:Port 仍然出现在 Endpoint 的就绪列表中，同时新启动的 Pod 的 IP:Port 也没有被添加到 Endpoint 中。终于发现了连接失败的根源，但是为什么会出现这种状况呢？仍然无解。\n又经历了几天折腾之后，我又有了新点子，那就是调试负责更新 Endpoint 的组件：kube-controller-manager，最后终于在 kube-controller-manager 的日志输出中发现了如下可疑的信息：\nI0412 22:59:59.914517 1 request.go:638] Throttling request took 2.489742918s, request: GET:https://10.3.0.1:443/api/v1/namespaces/[some namespace]/endpoints/[some endpoints]\u0026#34; 但还是感觉哪里不对劲，明明延迟了几分钟，为什么这里显示的只有两秒？\n在阅读了 kube-controller-manager 的源码后，我发现了问题所在。Kube-controller-manager 的主要职责是通过内部的众多 Controller 将集群的当前状态调整到期望状态，其中 Endpoint Controller 用于监控 Pod 的生命周期事件并根据这些事件更新 Endpoint。\nEndpoint Controller 内部运行了一组 workers 来处理这些事件并更新 Endpoint，如果有足够多的对 Endpoint 发起的请求被阻塞，那么所有的 workers 都会忙于等待被阻塞的请求，这时候新事件只能被添加到队列中排队等待，如果该队列很长，就会花很长时间来更新 Endpoint。\n为了解决这个问题，首先我通过调整 kube-controller-manager 的 参数 --concurrent-endpoints-syncs 来增加 Endpoint Controller 的 workers，但收效甚微。\n再次仔细阅读源码后，我找到了两个可以可以扭转战局的参数：--kube-api-qps 和 --kube-api-burst。kube-controller-manager 可以通过这两个参数来限制任何 Controller（包括 Endpoint Controller）对 kube-apiserver 发起的请求的速率。\n这两个参数的默认值是 20，但当集群中的主机数量非常多时，默认值显然不满足集群运行的工作负载。经过不断调试之后，我将参数 --kube-api-qps 的值设置为 300，将 --kube-api-burst 的值设置为 325，上面的日志信息便消失了，同时添加或移除 Pod 时 Endpoint 也能够立即更新。\n\u0026ndash;kube-api-qps 和 \u0026ndash;kube-api-burst 参数的值越大，kube-apiserver 和 etcd 的负载就越高。在我的集群中，通过适当地增加一些负载来解决这个问题是很值得的。 原文链接 # Kubernetes: Fixing Delayed Service Endpoint Updates ","date":"2018年6月15日","externalUrl":null,"permalink":"/posts/kubernetes-fixing-delayed-service-endpoint-updates/","section":"博客","summary":"几个月前，我在更新 Kubernetes 集群中的 Deployment 时发现了一个很奇怪的连接超时现","title":" 修复 Service Endpoint 更新的延迟","type":"posts"},{"content":"Kubernetes 作为云原生时代的“操作系统”，熟悉和使用它是每名用户（User）的必备技能。如果你正在 Kubernetes 上工作，你需要正确的工具和技巧来确保 Kubernetes 集群的高可用以及工作负载的稳定运行。\n随着 Kubernetes 的发展和演变，人们可以从内部来驯服它的无节制行为。但有些人并不情愿干等 Kubernetes 变得易于使用，并且为已投入生产的 Kubernetes 中遇到的很多常见问题制定了自己的解决方案。\n这里我们将介绍一些提高操作效率的技巧，同时列举几个比较有用的开源 Kubernetes 工具，这些工具以各种方式简化 Kubernetes，包括简化命令行交互，简化应用程序部署语法等。\nkubectl 自动补全 # kubectl 这个命令行工具非常重要，与之相关的命令也很多，我们也记不住那么多的命令，而且也会经常写错，所以命令自动补全是很有必要的，kubectl 工具本身就支持自动补全，只需简单设置一下即可。\nbash 用户 # 大多数用户的 shell 使用的是 bash，Linux 系统可以通过下面的命令来设置：\n$ echo \u0026#34;source \u0026lt;(kubectl completion bash)\u0026#34; \u0026gt;\u0026gt; ~/.bashrc $ source ~/.bashrc 如果发现不能自动补全，可以尝试安装 bash-completion 然后刷新即可！\nzsh 用户 # 如果你使用的 shell 是 zsh，可以通过下面的命令来设置：\n$ echo \u0026#34;source \u0026lt;(kubectl completion zsh)\u0026#34; \u0026gt;\u0026gt; ~/.zshrc $ source ~/.zshrc 自定义 kubectl get 输出 # kubectl get 相关资源，默认输出为 kubectl 内置，一般我们也可以使用 -o json 或者 -o yaml 查看其完整的资源信息。但是很多时候，我们需要关心的信息并不全面，因此我们需要自定义输出的列，那么可以使用 go-template 来进行实现。\ngo-template 是 golang 的一种模板，可以参考 template的相关说明。\n比如仅仅想要查看获取的 pods 中的各个 pod 的 uid，则可以使用以下命令：\n$ kubectl get pods --all-namespaces -o go-template=\u0026#39;{{range .items}}{{.metadata.uid}} {{end}}\u0026#39; 2ea418d4-533e-11e8-b722-005056a1bc83 7178b8bf-4e93-11e8-8175-005056a1bc83 a0341475-5338-11e8-b722-005056a1bc83 ... $ kubectl get pods -o yaml apiVersion: v1 items: - apiVersion: v1 kind: Pod metadata: name: nginx-deployment-1751389443-26gbm namespace: default uid: a911e34b-f445-11e7-9cda-40f2e9b98448 ... - apiVersion: v1 kind: Pod metadata: name: nginx-deployment-1751389443-rsbkc namespace: default uid: a911d2d2-f445-11e7-9cda-40f2e9b98448 ... - apiVersion: v1 kind: Pod metadata: name: nginx-deployment-1751389443-sdbkx namespace: default uid: a911da1a-f445-11e7-9cda-40f2e9b98448 ... kind: List metadata: {} resourceVersion: \u0026#34;\u0026#34; 因为 get pods 的返回结果是 List 类型，获取的 pods 都在 items 这个的 value 中，因此需要遍历 items，也就有了 {{range .items}}。而后通过模板选定需要展示的内容，就是 items 中的每个 {{.metadata.uid}}。\n这里特别注意，要做一个特别的处理，就是要把 {{end}} 前进行换行，以便在模板中插入换行符。\n当然，如果觉得这样处理不优雅的话，也可以使用 printf 函数，在其中使用 \\n 即可实现换行符的插入。\n$ kubectl get pods --all-namespaces -o go-template --template=\u0026#39;{{range .items}}{{printf \u0026#34;%s\\n\u0026#34; .metadata.uid}}{{end}}\u0026#39; 或者可以这样：\n$ kubectl get pods --all-namespaces -o go-template --template=\u0026#39;{{range .items}}{{.metadata.uid}}{{\u0026#34;\\n\u0026#34;}}{{end}}\u0026#39; 其实有了 printf，就可以很容易的实现对应字段的输出，且样式可以进行自己控制。比如可以这样\n$ kubectl get pods --all-namespaces -o go-template --template=\u0026#39;{{range .items}}{{printf \u0026#34;|%-20s|%-50s|%-30s|\\n\u0026#34; .metadata.namespace .metadata.name .metadata.uid}}{{end}}\u0026#39; |default |details-v1-64b86cd49-85vks |2e7a2a66-533e-11e8-b722-005056a1bc83| |default |productpage-v1-84f77f8747-7tkwb |2eb4e840-533e-11e8-b722-005056a1bc83| |default |ratings-v1-5f46655b57-qlrxp |2e89f981-533e-11e8-b722-005056a1bc83| ... 下面举两个 go-template 高级用法的例子：\nrange 嵌套 # 列出所有容器使用的镜像名 $ kubectl get pods --all-namespaces -o go-template --template=\u0026#39;{{range .items}}{{range .spec.containers}}{{printf \u0026#34;%s\\n\u0026#34; .image}}{{end}}{{end}}\u0026#39; istio/examples-bookinfo-details-v1:1.5.0 istio/examples-bookinfo-productpage-v1:1.5.0 istio/examples-bookinfo-ratings-v1:1.5.0 ... 条件判断 # 列出所有不可调度节点的节点名与 IP $ kubectl get no -o go-template=\u0026#39;{{range .items}}{{if .spec.unschedulable}}{{.metadata.name}} {{.spec.externalID}}{{\u0026#34;\\n\u0026#34;}}{{end}}{{end}}\u0026#39; 除了使用 go-template 之外，还可以使用逗号分隔的自定义列列表打印表格：\n$ kubectl -n kube-system get pods coredns-64b597b598-7547d -o custom-columns=NAME:.metadata.name,hostip:.status.hostIP NAME hostip coredns-64b597b598-7547d 192.168.123.250 也可以使用 go-template-file 自定义模板列表，模板不用通过参数传进去，而是写成一个文件，然后需要指定 template 指向该文件即可。\n$ cat \u0026gt; test.tmpl \u0026lt;\u0026lt; EOF NAME HOSTIP metadata.name status.hostIP EOF $ kubectl -n kube-system get pods coredns-64b597b598-7547d -o custom-columns-file=test.tmpl NAME HOSTIP coredns-64b597b598-7547d 192.168.123.250 Kube-prompt：交互式 Kubernetes 客户端 # Kube-prompt 可以让你在 Kubernetes 客户端输入相当于交互式命令会话的东西，并为每个命令提供自动填充的背景信息，你不必键入 kubectl 来为每个命令添加前缀。\nKubectl Aliases：生成 kubectl 别名 # 如果你需要频繁地使用 kubectl 和 kubernetes api 进行交互，使用别名将会为你节省大量的时间，开源项目 kubectl-aliases 可以通过编程的方式生成 kubectl 别名，别名生成规则如下：\n简单别名示例 kd → kubectl describe\n高级别名示例 kgdepallw → kubectl get deployment \u0026ndash;all-namespaces \u0026ndash;watch\nKubeval：校验配置文件 # 如果你手动写 Kubernetes manifest 文件，检查 manifest 文件的语法是很困难的，特别是当你有多个不同版本的 Kubernetes 集群时，确认配置文件语法是否正确更是难上加难。\nKubeval 是一个用于校验Kubernetes YAML或JSON配置文件的工具，支持多个Kubernetes版本，可以帮助我们解决不少的麻烦。\n使用示例 $ kubeval nginx.yaml The document nginx.yaml contains an invalid Deployment ---\u0026gt; spec.replicas: Invalid type. Expected: integer, given: string Kedge：简化 Kubernetes 部署定义 # 很多人都抱怨 Kubernetes manifest 文件的定义太复杂和冗长。它们很难写，而且很难维护，如果能够简化部署定义就会极大地降低维护难度。\nKedge 提供更简单、更简洁的语法，然后 kedge 将其转换为 Kubernetes manifest 文件。\n使用示例 # Web server Kedge example name: httpd deployments: - containers: - image: centos/httpd services: - name: httpd type: LoadBalancer portMappings: - 8080:80 # Converted Kubernetes artifact file(s) --- apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: httpd name: httpd spec: ports: - name: httpd-8080 port: 8080 protocol: TCP targetPort: 80 selector: app: httpd type: LoadBalancer status: loadBalancer: {} --- apiVersion: extensions/v1beta1 kind: Deployment metadata: creationTimestamp: null labels: app: httpd name: httpd spec: strategy: {} template: metadata: creationTimestamp: null labels: app: httpd name: httpd spec: containers: - image: centos/httpd name: httpd resources: {} status: {} 参考 # 为高效 Ops 和 SRE 团队准备的 10 个开源 k8s 工具 打造高效的Kubernetes命令行终端 ","date":"2018年6月11日","externalUrl":null,"permalink":"/posts/kubernetes-fucking-trick/","section":"博客","summary":"Kubernetes 作为云原生时代的“操作系统”，熟悉和使用它是每名用户（Us","title":"Kubernetes 的奇技淫巧","type":"posts"},{"content":"kube-scheduler 是 Kubernetes 中负责调度的组件，它本身的调度功能已经很强大了。但由于 Kubernetes 集群非常活跃，它的状态会随时间而改变，由于各种原因，你可能需要将已经运行的 Pod 移动到其他节点：\n某些节点负载过高 某些资源对象被添加了 node 亲和性 或 pod （反）亲和性 集群中加入了新节点 一旦 Pod 启动之后 kube-scheduler 便不会再尝试重新调度它。根据环境的不同，你可能会有很多需要手动调整 Pod 的分布，例如：如果集群中新加入了一个节点，那么已经运行的 Pod 并不会被分摊到这台节点上，这台节点可能只运行了少量的几个 Pod，这并不理想，对吧？\nDescheduler 如何工作？ # Descheduler 会检查 Pod 的状态，并根据自定义的策略将不满足要求的 Pod 从该节点上驱逐出去。Descheduler 并不是 kube-scheduler 的替代品，而是要依赖于它。该项目目前放在 Kubernetes 的孵化项目中，还没准备投入生产，但经过我实验发现它的运行效果很好，而且非常稳定。那么该如何安装呢？\n部署方法 # 你可以通过 Job 或 CronJob 来运行 descheduler。我已经创建了一个镜像 komljen/descheduler:v0.5.0-4-ga7ceb671（包含在下面的 yaml 文件中），但由于这个项目的更新速度很快，你可以通过以下的命令创建你自己的镜像：\n$ git clone https://github.com/kubernetes-incubator/descheduler $ cd descheduler \u0026amp;\u0026amp; make image 然后打好标签 push 到自己的镜像仓库中。\n通过我创建的 chart 模板，你可以用 Helm 来部署 descheduler，该模板支持 RBAC 并且已经在 Kubernetes v1.9 上测试通过。\n添加我的 helm 私有仓库，然后部署 descheduler：\n$ helm repo add akomljen-charts \\ https://raw.githubusercontent.com/komljen/helm-charts/master/charts/ $ helm install --name ds \\ --namespace kube-system \\ akomljen-charts/descheduler 你也可以不使用 helm，通过手动部署。首先创建 serviceaccount 和 clusterrolebinding：\n# Create a cluster role $ cat \u0026lt;\u0026lt; EOF| kubectl create -n kube-system -f - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: descheduler rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;nodes\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;list\u0026#34;] - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;pods\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;list\u0026#34;, \u0026#34;delete\u0026#34;] - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;pods/eviction\u0026#34;] verbs: [\u0026#34;create\u0026#34;] EOF # Create a service account $ kubectl create sa descheduler -n kube-system # Bind the cluster role to the service account $ kubectl create clusterrolebinding descheduler \\ -n kube-system \\ --clusterrole=descheduler \\ --serviceaccount=kube-system:descheduler 然后通过 configmap 创建 descheduler 策略。目前只支持四种策略：\nRemoveDuplicates LowNodeUtilization RemovePodsViolatingInterPodAntiAffinity RemovePodsViolatingNodeAffinity 默认这四种策略全部开启，你可以根据需要关闭它们。下面在 kube-suystem 命名空间中创建一个 configmap：\n$ cat \u0026lt;\u0026lt; EOF| kubectl create -n kube-system -f - apiVersion: v1 kind: ConfigMap metadata: name: descheduler data: policy.yaml: |- apiVersion: descheduler/v1alpha1 kind: DeschedulerPolicy strategies: RemoveDuplicates: enabled: false LowNodeUtilization: enabled: true params: nodeResourceUtilizationThresholds: thresholds: cpu: 20 memory: 20 pods: 20 targetThresholds: cpu: 50 memory: 50 pods: 50 RemovePodsViolatingInterPodAntiAffinity: enabled: true RemovePodsViolatingNodeAffinity: enabled: true params: nodeAffinityType: - requiredDuringSchedulingIgnoredDuringExecution EOF 在 kube-system 命名空间中创建一个 CronJob：\n$ cat \u0026lt;\u0026lt; EOF| kubectl create -n kube-system -f - apiVersion: batch/v1beta1 kind: CronJob metadata: name: descheduler spec: schedule: \u0026#34;*/30 * * * *\u0026#34; jobTemplate: metadata: name: descheduler annotations: scheduler.alpha.kubernetes.io/critical-pod: \u0026#34;true\u0026#34; spec: template: spec: serviceAccountName: descheduler containers: - name: descheduler image: komljen/descheduler:v0.5.0-4-ga7ceb671 volumeMounts: - mountPath: /policy-dir name: policy-volume command: - /bin/descheduler - --v=4 - --max-pods-to-evict-per-node=10 - --policy-config-file=/policy-dir/policy.yaml restartPolicy: \u0026#34;OnFailure\u0026#34; volumes: - name: policy-volume configMap: name: descheduler EOF $ kubectl get cronjobs -n kube-system NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE descheduler */30 * * * * False 0 2m 32m 该 CroJob 每 30 分钟运行一次，当 CronJob 开始工作后，可以通过以下命令查看已经成功结束的 Pod：\n$ kubectl get pods -n kube-system -a | grep Completed descheduler-1525520700-297pq 0/1 Completed 0 1h descheduler-1525521000-tz2ch 0/1 Completed 0 32m descheduler-1525521300-mrw4t 0/1 Completed 0 2m 也可以查看这些 Pod 的日志，然后根据需要调整 descheduler 策略：\n$ kubectl logs descheduler-1525521300-mrw4t -n kube-system I0505 11:55:07.554195 1 reflector.go:202] Starting reflector *v1.Node (1h0m0s) from github.com/kubernetes-incubator/descheduler/pkg/descheduler/node/node.go:84 I0505 11:55:07.554255 1 reflector.go:240] Listing and watching *v1.Node from github.com/kubernetes-incubator/descheduler/pkg/descheduler/node/node.go:84 I0505 11:55:07.767903 1 lownodeutilization.go:147] Node \u0026#34;ip-10-4-63-172.eu-west-1.compute.internal\u0026#34; is appropriately utilized with usage: api.ResourceThresholds{\u0026#34;cpu\u0026#34;:41.5, \u0026#34;memory\u0026#34;:1.3635487207675927, \u0026#34;pods\u0026#34;:8.181818181818182} I0505 11:55:07.767942 1 lownodeutilization.go:149] allPods:9, nonRemovablePods:9, bePods:0, bPods:0, gPods:0 I0505 11:55:07.768141 1 lownodeutilization.go:144] Node \u0026#34;ip-10-4-36-223.eu-west-1.compute.internal\u0026#34; is over utilized with usage: api.ResourceThresholds{\u0026#34;cpu\u0026#34;:48.75, \u0026#34;memory\u0026#34;:61.05259502942694, \u0026#34;pods\u0026#34;:30} I0505 11:55:07.768156 1 lownodeutilization.go:149] allPods:33, nonRemovablePods:12, bePods:1, bPods:19, gPods:1 I0505 11:55:07.768376 1 lownodeutilization.go:144] Node \u0026#34;ip-10-4-41-14.eu-west-1.compute.internal\u0026#34; is over utilized with usage: api.ResourceThresholds{\u0026#34;cpu\u0026#34;:39.125, \u0026#34;memory\u0026#34;:98.19259268881142, \u0026#34;pods\u0026#34;:33.63636363636363} I0505 11:55:07.768390 1 lownodeutilization.go:149] allPods:37, nonRemovablePods:8, bePods:0, bPods:29, gPods:0 I0505 11:55:07.768538 1 lownodeutilization.go:147] Node \u0026#34;ip-10-4-34-29.eu-west-1.compute.internal\u0026#34; is appropriately utilized with usage: api.ResourceThresholds{\u0026#34;memory\u0026#34;:43.19826999287199, \u0026#34;pods\u0026#34;:30.90909090909091, \u0026#34;cpu\u0026#34;:35.25} I0505 11:55:07.768552 1 lownodeutilization.go:149] allPods:34, nonRemovablePods:11, bePods:8, bPods:15, gPods:0 I0505 11:55:07.768556 1 lownodeutilization.go:65] Criteria for a node under utilization: CPU: 20, Mem: 20, Pods: 20 I0505 11:55:07.768571 1 lownodeutilization.go:69] No node is underutilized, nothing to do here, you might tune your thersholds further I0505 11:55:07.768576 1 pod_antiaffinity.go:45] Processing node: \u0026#34;ip-10-4-63-172.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.779313 1 pod_antiaffinity.go:45] Processing node: \u0026#34;ip-10-4-36-223.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.796766 1 pod_antiaffinity.go:45] Processing node: \u0026#34;ip-10-4-41-14.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.813303 1 pod_antiaffinity.go:45] Processing node: \u0026#34;ip-10-4-34-29.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.829109 1 node_affinity.go:40] Executing for nodeAffinityType: requiredDuringSchedulingIgnoredDuringExecution I0505 11:55:07.829133 1 node_affinity.go:45] Processing node: \u0026#34;ip-10-4-63-172.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.840416 1 node_affinity.go:45] Processing node: \u0026#34;ip-10-4-36-223.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.856735 1 node_affinity.go:45] Processing node: \u0026#34;ip-10-4-41-14.eu-west-1.compute.internal\u0026#34; I0505 11:55:07.945566 1 request.go:480] Throttling request took 88.738917ms, request: GET:https://100.64.0.1:443/api/v1/pods?fieldSelector=spec.nodeName%3Dip-10-4-41-14.eu-west-1.compute.internal%2Cstatus.phase%21%3DFailed%2Cstatus.phase%21%3DSucceeded I0505 11:55:07.972702 1 node_affinity.go:45] Processing node: \u0026#34;ip-10-4-34-29.eu-west-1.compute.internal\u0026#34; I0505 11:55:08.145559 1 request.go:480] Throttling request took 172.751657ms, request: GET:https://100.64.0.1:443/api/v1/pods?fieldSelector=spec.nodeName%3Dip-10-4-34-29.eu-west-1.compute.internal%2Cstatus.phase%21%3DFailed%2Cstatus.phase%21%3DSucceeded I0505 11:55:08.160964 1 node_affinity.go:72] Evicted 0 pods 哇哦，现在你的集群中已经运行了一个 descheduler！\n总结 # Kubernetes 的默认调度器已经做的很好，但由于集群处于不断变化的状态中，某些 Pod 可能运行在错误的节点上，或者你想要均衡集群资源的分配，这时候就需要 descheduler 来帮助你将某些节点上的 Pod 驱逐到正确的节点上去。我很期待正式版的发布！\n原文链接 # Meet a Kubernetes Descheduler ","date":"2018年5月23日","externalUrl":null,"permalink":"/posts/introduce-kubernetes-descheduler/","section":"博客","summary":"kube-scheduler 是 Kubernetes 中负责调度的组件，它本身的调度功能已经很强大了。但由于","title":"Descheduler 使用指南","type":"posts"},{"content":"本文我们将从实践者的角度仔细研究整个pod生命周期，包括如何影响启动和关闭行为，并通过实践来理解对应用程序健康状况的检查。\nPod 的生命周期 # Pod phase # Pod 的 status 在信息保存在 PodStatus 中定义，其中有一个 phase 字段。\nPod 的相位（phase）是 Pod 在其生命周期中的简单宏观概述。该阶段并不是对容器或 Pod 的综合汇总，也不是为了做为综合状态机。\nPod 相位的数量和含义是严格指定的。除了本文档中列举的状态外，不应该再假定 Pod 有其他的 phase 值。\n无论你是手动创建 Pod，还是通过 deployment、daemonset 或 statefulset来创建，Pod 的 phase 都有以下几个可能的值：\n挂起（Pending）：Pod 已被 Kubernetes 系统接受，但有一个或者多个容器镜像尚未创建。等待时间包括调度 Pod 的时间和通过网络下载镜像的时间，这可能需要花点时间。 运行中（Running）：该 Pod 已经绑定到了一个节点上，Pod 中所有的容器都已被创建。至少有一个容器正在运行，或者正处于启动或重启状态。 成功（Successed）：Pod 中的所有容器都被成功终止，并且不会再重启。 失败（Failed）：Pod 中的所有容器都已终止了，并且至少有一个容器是因为失败终止。也就是说，容器以非0状态退出或者被系统终止。 未知（Unkonwn）：因为某些原因无法取得 Pod 的状态，通常是因为与 Pod 所在主机通信失败。 下图是 Pod 的生命周期示意图，从图中可以看到 Pod 状态的变化。\nPod的生命周期示意图 Pod 状态 # Pod 有一个 PodStatus 对象，其中包含一个 PodCondition 数组。 PodCondition 数组的每个元素都有一个 type 字段和一个 status 字段。type 字段是字符串，可能的值有 PodScheduled、Ready、Initialized 和 Unschedulable。status 字段是一个字符串，可能的值有 True、False 和 Unknown。\n当你通过 kubectl get pod 查看 Pod 时，STATUS 这一列可能会显示与上述5个状态不同的值，例如 Init:0/1 和 CrashLoopBackOff。这是因为 Pod 状态的定义除了包含 phase 之外，还有 InitContainerStatuses 和 containerStatuses 等其他字段，具体代码参考 overall status of a pod .\n如果想知道究竟发生了什么，可以通过命令 kubectl describe pod/$PODNAME 查看输出信息的 Events 条目。通过 Events 条目可以看到一些具体的信息，比如正在拉取容器镜像，Pod 已经被调度，或者某个 container 处于 unhealthy 状态。\nPod 的启动关闭流程 # 下面通过一个具体的示例来探究一下 Pod 的整个生命周期流程。为了确定事情发生的顺序，通过下面的 manifest 来部署一个 deployment。\nkind: Deployment apiVersion: apps/v1beta1 metadata: name: loap spec: replicas: 1 template: metadata: labels: app: loap spec: initContainers: - name: init image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $(date +%s): INIT \u0026gt;\u0026gt; /loap/timing\u0026#39;] volumeMounts: - mountPath: /loap name: timing containers: - name: main image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $(date +%s): START \u0026gt;\u0026gt; /loap/timing; sleep 10; echo $(date +%s): END \u0026gt;\u0026gt; /loap/timing;\u0026#39;] volumeMounts: - mountPath: /loap name: timing livenessProbe: exec: command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $(date +%s): LIVENESS \u0026gt;\u0026gt; /loap/timing\u0026#39;] readinessProbe: exec: command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $(date +%s): READINESS \u0026gt;\u0026gt; /loap/timing\u0026#39;] lifecycle: postStart: exec: command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $(date +%s): POST-START \u0026gt;\u0026gt; /loap/timing\u0026#39;] preStop: exec: command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo $(date +%s): PRE-HOOK \u0026gt;\u0026gt; /loap/timing\u0026#39;] volumes: - name: timing hostPath: path: /tmp/loap 等待 Pod 状态变为 Running 之后，通过以下命令来强制停止 Pod：\n$ kubectl scale deployment loap --replicas=0 查看 /tmp/loap/timing 文件的内容：\n$ cat /tmp/loap/timing 1525334577: INIT 1525334581: START 1525334581: POST-START 1525334584: READINESS 1525334584: LIVENESS 1525334588: PRE-HOOK 1525334589: END /tmp/loap/timing 文件的内容很好地体现了 Pod 的启动和关闭流程，具体过程如下：\nPod 的启动和关闭流程 首先启动一个 Infra 容器（又叫 Pause 容器），用来和 Pod 中的其他容器共享 linux 命名空间，并开启 init 进程。（上图中忽略了这一步） 然后启动 Init 容器，它是一种专用的容器，在应用程序容器启动之前运行，用来对 Pod 进行一些初始化操作，并包括一些应用镜像中不存在的实用工具和安装脚本。 4 秒之后，应用程序容器和 post-start hook 同时启动。 7 秒之后开始启动 liveness 和 readiness 探针。 11 秒之后，通过手动杀掉 Pod，pre-stop hook 执行，优雅删除期限过期后（默认是 30 秒），应用程序容器停止。实际的 Pod 终止过程要更复杂，具体参考 Pod 的终止。 必须主动杀掉 Pod 才会触发 pre-stop hook，如果是 Pod 自己 Down 掉，则不会执行 pre-stop hook。 如何快速 DEBUG # 当 Pod 出现致命的错误时，如果能够快速 DEBUG，将会帮助我们快速定位问题。为了实现这个目的，可以把把致命事件的信息通过 .spec.terminationMessagePath 配置写入指定位置的文件，就像打印错误、异常和堆栈信息一样。该位置的内容可以很方便的通过 dashboards、监控软件等工具检索和展示，默认路径为 /dev/termination-log。\n以下是一个小例子：\n# termination-demo.yaml apiVersion: v1 kind: Pod metadata: name: termination-demo spec: containers: - name: termination-demo-container image: alpine command: [\u0026#34;/bin/sh\u0026#34;] args: [\u0026#34;-c\u0026#34;, \u0026#34;sleep 10 \u0026amp;\u0026amp; echo Sleep expired \u0026gt; /dev/termination-log\u0026#34;] 这些消息的最后部分会使用其他的规定来单独存储：\n$ kubectl create -f termination-demo.yaml $ sleep 20 $ kubectl get pod termination-demo -o go-template=\u0026#39;{{range .status.containerStatuses}}{{.lastState.terminated.message}}{{end}}\u0026#39; Sleep expired $ kubectl get pod termination-demo -o go-template=\u0026#39;{{range .status.containerStatuses}}{{.lastState.terminated.exitCode}}{{end}}\u0026#39; 0 参考 # Pod hook Kubernetes: A Pod’s Life 确定 Pod 失败的原因 ","date":"2018年5月3日","externalUrl":null,"permalink":"/posts/pods-life/","section":"博客","summary":"本文我们将从实践者的角度仔细研究整个pod生命周期，包括如何","title":"Kubernetes 中 Pod 的生命周期管理","type":"posts"},{"content":" Kube-router 是一个挺有想法的项目，兼备了 calico 和 kube-proxy 的功能，是基于 Kubernetes 网络设计的一个集负载均衡器、防火墙和容器网络的综合方案。\n体系架构 # Kube-router 是围绕 观察者 和 控制器 的概念而建立的。\n观察者 使用 Kubernetes watch API 来获取与创建，更新和删除 Kubernetes 对象有关的事件的通知。 每个观察者获取与特定 API 对象相关的通知。 在从 API 服务器接收事件时，观察者广播事件。\n控制器 注册以获取观察者的事件更新，并处理事件。\nKube-router 由3个核心控制器和多个观察者组成，如下图所示。\n流程分析 # Kube-router 启动之后，首先创建 wathcer:\nfunc (kr *KubeRouter) Run() error { ... err = kr.startApiWatchers() 在 startApiWatchers 中，会启动 endpoint、namespace、pod、node、networkpolicy、service 这六个 wather。\n这六个 wathcer 将监听的变化发送到 Broadcaster。\nfunc NewBroadcaster() *Broadcaster { return \u0026amp;Broadcaster{} } func (b *Broadcaster) Add(listener Listener) { b.listenerLock.Lock() defer b.listenerLock.Unlock() b.listeners = append(b.listeners, listener) } func (b *Broadcaster) Notify(instance interface{}) { b.listenerLock.RLock() listeners := b.listeners b.listenerLock.RUnlock() for _, listener := range listeners { go listener.OnUpdate(instance) } } 之后创建三个 controller：NetworkPolicyController、NetworkRoutingController、NetworkServicesControllers。 每个 controller 会监听所关心的资源的变化。\nfunc NewNetworkServicesController(clientset *kubernetes.Clientset,\\ config *options.KubeRouterConfig) (*NetworkServicesController, error) { ... nsc := NetworkServicesController{} ... watchers.EndpointsWatcher.RegisterHandler(\u0026amp;nsc) watchers.ServiceWatcher.RegisterHandler(\u0026amp;nsc) ... 每个 controller 遵循以下结构。\nfunc Run() { for { Sync() // control loop that runs for ever and perfom sync at periodic interval } } func OnUpdate() { Sync() // on receiving update of a watched API object (namespace, node, pod, network policy etc) } Sync() { //re-concile any state changes } Cleanup() { // cleanup any changes (to iptables, ipvs, network etc) done to the system } 主要功能 # 基于 IPVS/LVS 的负载均衡器 | --run-service-proxy # Kube-router 采用 Linux 内核的 IPVS 模块为 K8s 提供 Service 的代理。\nKube-router 的负载均衡器功能，会在物理机上创建一个虚拟的 kube-dummy-if 网卡，然后利用 k8s 的 watch APi 实时更新 svc 和 ep 的信息。svc 的 cluster_ip 会绑定在 kube-dummy-if 网卡上，作为 lvs 的 virtual server 的地址。realserver 的 ip 则通过 ep 获取到容器的IP地址。\n基于 Kubernetes 网络服务代理的 Kube-router IPVS 演示\n特征：\n轮询负载均衡 基于客户端IP的会话保持 如果服务控制器与网络路由控制器（带有 –-run-router 标志的 kube-router）一起使用，源IP将被保留 用 –-masquerade-all 参数明确标记伪装(SNAT) 更多详情可以参考：\nKubernetes network services prox with IPVS/LVS Kernel Load-Balancing for Docker Containers Using IPVS LVS负载均衡之持久性连接介绍 容器网络 | --run-router # Kube-router 利用 BGP 协议和 Go 的 GoBGP 库和为容器网络提供直连的方案。因为用了原生的 Kubernetes API 去构建容器网络，意味着在使用 kube-router 时，不需要在你的集群里面引入其他依赖。\n同样的，kube-router 在引入容器 CNI 时也没有其它的依赖，官方的 bridge 插件就能满足 kube-rouetr 的需求。\n更多关于 BGP 协议在 Kubernetes 中的使用可以参考：\nKubernetes pod networking and beyond with BGP 网络策略管理 | --run-firewall # 网络策略控制器负责从 Kubernetes API 服务器读取命名空间、网络策略和 pod 信息，并相应地使用 ipset 配置 iptables 以向 pod 提供入口过滤，保证防火墙的规则对系统性能有较低的影响。\nKube-router 支持 networking.k8s.io/NetworkPolicy 接口或网络策略 V1/GA semantics 以及网络策略的 beta 语义。\n更多关于 kube-router 防火墙的功能可以参考：\nEnforcing Kubernetes network policies with iptables 使用 kube-router 替代 kube-proxy # 下面进入实战阶段，本方案只使用 kube-router 的 service-proxy 功能，网络插件仍然使用 calico（估计只有我能想到这么奇葩的组合了 ✌️）\n前提 # 已有一个 k8s 集群 kube-router 能够连接 apiserver 如果您选择以 daemonset 运行 kube-router，那么 kube-apiserver 和 kubelet 必须以 –allow-privileged=true 选项运行 集群环境 # 角色 IP 地址 主机名 k8s master 192.168.123.250 node1 k8s node 192.168.123.248 node2 k8s node 192.168.123.249 node3 安装步骤 # 如果你正在使用 kube-proxy，需要先停止 kube-proxy 服务，并且删除相关 iptables 规则。\n$ systemctl stop kube-proxy $ kube-proxy --cleanup-iptables 接下来以 daemonset 运行 kube-router，这里我们使用 DR 模式。\n$ kubectl --namespace=kube-system create configmap kube-proxy --from-file=kubeconfig.conf=/root/.kube/config $ wget https://raw.githubusercontent.com/cloudnativelabs/kube-router/master/daemonset/kubeadm-kuberouter-all-features-dsr.yaml # 将 kubeadm-kuberouter-all-features-dsr.yaml 里的 --run-router 参数和 --run-firewall 参数的值改为 false $ kubectl create -f kubeadm-kuberouter-all-features-dsr.yaml 在每台机器上查看 lvs 条目\n$ ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -\u0026gt; RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.254.0.1:443 rr persistent 10800 -\u0026gt; 192.168.123.250:6443 Masq 1 0 0 $ ipvsadm -S -n -A -t 10.254.0.1:443 -s rr -p 10800 -a -t 10.254.0.1:443 -r 192.168.123.250:6443 -m -w 1 可以看出，kube-router 使用的是 lvs 的 nat 模式。\n创建一个应用测试 kube-router # $ kubectl run whats-my-ip --image=cloudnativelabs/whats-my-ip --replicas=3 # 暴露服务 $ kubectl expose deploy whats-my-ip --target-port=8080 --port=8080 查看创建好的服务\n$ kubectl get pods -owide NAME READY STATUS RESTARTS AGE IP NODE whats-my-ip-845d4ff4f6-d2ptz 1/1 Running 0 23h 172.20.135.8 192.168.123.249 whats-my-ip-845d4ff4f6-jxzzn 1/1 Running 0 23h 172.20.166.130 192.168.123.250 whats-my-ip-845d4ff4f6-szhhd 1/1 Running 0 34s 172.20.104.9 192.168.123.248 $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.254.0.1 \u0026lt;none\u0026gt; 443/TCP 45d whats-my-ip ClusterIP 10.254.108.117 \u0026lt;none\u0026gt; 8080/TCP 16s 查看 lvs 规则条目\n$ ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -\u0026gt; RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.254.0.1:443 rr persistent 10800 -\u0026gt; 192.168.123.250:6443 Masq 1 0 0 TCP 10.254.175.147:8080 rr -\u0026gt; 172.20.104.9:8080 Masq 1 0 0 -\u0026gt; 172.20.135.8:8080 Masq 1 0 0 -\u0026gt; 172.20.166.130:8080 Masq 1 0 0 可以发现本机的 Cluster IP 代理后端真实 Pod IP，使用 rr 算法。\n通过 ip a 可以看到，每添加一个服务，node 节点上面的 kube-dummy-if 网卡就会增加一个虚IP。\nsession affinity # Service 默认的策略是，通过 round-robin 算法来选择 backend Pod。 要实现基于客户端 IP 的会话亲和性，可以通过设置 service.spec.sessionAffinity 的值为 ClientIP （默认值为 \u0026ldquo;None\u0026rdquo;）。\n$ kubectl delete svc whats-my-ip $ kubectl expose deploy whats-my-ip --target-port=8080 --port=8080 --session-affinity=ClientIP $ ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -\u0026gt; RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.254.0.1:443 rr persistent 10800 -\u0026gt; 192.168.123.250:6443 Masq 1 0 0 TCP 10.254.226.105:8080 rr persistent 10800 -\u0026gt; 172.20.135.8:8080 Masq 1 0 0 -\u0026gt; 172.20.166.130:8080 Masq 1 0 0 -\u0026gt; 172.20.104.9:8080 Masq 1 0 0 $ ipvsadm -S -n -A -t 10.254.0.1:443 -s rr -p 10800 -a -t 10.254.0.1:443 -r 192.168.123.250:6443 -m -w 1 -A -t 10.254.226.105:8080 -s rr -p 10800 -a -t 10.254.226.105:8080 -r 172.20.135.8:8080 -m -w 1 -a -t 10.254.226.105:8080 -r 172.20.166.130:8080 -m -w 1 -a -t 10.254.226.105:8080 -r 172.20.104.9:8080 -m -w 1 可以看到 lvs 的规则条目里多了个 persistent，即 lvs 的持久连接，关于 lvs 持久连接的具体内容可以参考我的另一篇博文 LVS负载均衡之持久性连接介绍。\n可以通过设置 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 的值来修改 lvs 的 persistence_timeout 超时时间。\n$ kubectl get svc whats-my-ip -o yaml apiVersion: v1 kind: Service metadata: creationTimestamp: 2018-04-20T08:16:38Z labels: run: whats-my-ip name: whats-my-ip namespace: default resourceVersion: \u0026#34;6323769\u0026#34; selfLink: /api/v1/namespaces/default/services/whats-my-ip uid: 26315fdf-4473-11e8-8388-005056a1bc83 spec: clusterIP: 10.254.226.105 ports: - port: 8080 protocol: TCP targetPort: 8080 selector: run: whats-my-ip sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800 type: ClusterIP status: loadBalancer: {} NodePort # $ kubectl delete svc whats-my-ip $ kubectl expose deploy whats-my-ip --target-port=8080 --port=8080 --type=NodePort $ ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -\u0026gt; RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 192.168.123.249:34507 rr -\u0026gt; 172.20.135.8:8080 Masq 1 0 0 -\u0026gt; 172.20.166.130:8080 Masq 1 0 0 -\u0026gt; 172.20.104.9:8080 Masq 1 0 0 TCP 10.254.0.1:443 rr persistent 10800 -\u0026gt; 192.168.123.250:6443 Masq 1 0 0 TCP 10.254.175.147:8080 rr -\u0026gt; 172.20.135.8:8080 Masq 1 0 0 -\u0026gt; 172.20.166.130:8080 Masq 1 0 0 -\u0026gt; 172.20.104.9:8080 Masq 1 0 0 可以看到不仅有虚拟IP条目，还多了对应主机的 lvs 条目。\n更改算法 # 最少连接数 $ kubectl annotate service my-service \u0026#34;kube-router.io/service.scheduler=lc\u0026#34; 轮询 $ kubectl annotate service my-service \u0026#34;kube-router.io/service.scheduler=rr\u0026#34; 源地址哈希 $ kubectl annotate service my-service \u0026#34;kube-router.io/service.scheduler=sh\u0026#34; 目的地址哈希 $ kubectl annotate service my-service \u0026#34;kube-router.io/service.scheduler=dh\u0026#34; 问题解决 # 接下来需要面对一些非常棘手的问题，我尽可能将问题描述清楚。\n问题1：在集群内某个节点主机上通过 SVC IP+Port 访问某个应用时，如果 lvs 转到后端的 pod 在本主机上，那么可以访问，如果该 pod 不在本主机上，那么无法访问。 可以通过抓包来看一下，现在 service whats-my-ip 后端有三个 pod，分别运行在 node1、node2 和 node3 上。\n$ kubectl get pods -owide NAME READY STATUS RESTARTS AGE IP NODE whats-my-ip-845d4ff4f6-d2ptz 1/1 Running 0 23h 172.20.135.8 192.168.123.249 whats-my-ip-845d4ff4f6-jxzzn 1/1 Running 0 23h 172.20.166.130 192.168.123.250 whats-my-ip-845d4ff4f6-szhhd 1/1 Running 0 34s 172.20.104.9 192.168.123.248 在 node3 上访问 whats-my-ip 服务：\n$ ip a show|grep 10.254.175.147 inet 10.254.175.147/32 brd 10.254.175.147 scope link kube-dummy-if $ ipvsadm -Ln -t 10.254.175.147:8080 Prot LocalAddress:Port Scheduler Flags -\u0026gt; RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.254.175.147:8080 rr -\u0026gt; 172.20.104.9:8080 Masq 1 0 0 -\u0026gt; 172.20.135.8:8080 Masq 1 0 0 -\u0026gt; 172.20.166.130:8080 Masq 1 0 0 # 第一次访问，不通 $ curl 10.254.175.147:8080 # 第二次访问 $ curl 10.254.175.147:8080 HOSTNAME:whats-my-ip-845d4ff4f6-d2ptz IP:172.20.135.8 # 第三次访问，不通 $ curl 10.254.175.147:8080 同时在 node1 上抓包：\n$ tcpdump -i ens160 host 172.20.166.130 -nn tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes 03:27:26.337553 IP 10.254.175.147.42036 \u0026gt; 172.20.166.130.8080: Flags [S], seq 405854371, win 43690, options [mss 65495,sackOK,TS val 359417229 ecr 0,nop,wscale 7], length 0 03:27:27.340131 IP 10.254.175.147.42036 \u0026gt; 172.20.166.130.8080: Flags [S], seq 405854371, win 43690, options [mss 65495,sackOK,TS val 359418232 ecr 0,nop,wscale 7], length 0 可以看到 node1 将数据包丢弃了，因为源IP是 10.254.175.147，系统认为这是 node1 自己本身。\n根本原因可以查看 node3 的路由表：\n$ ip route show table local|grep 10.254.175.147 local 10.254.175.147 dev kube-dummy-if proto kernel scope host src 10.254.175.147 broadcast 10.254.175.147 dev kube-dummy-if proto kernel scope link src 10.254.175.147 src 的值用来告诉该 host 使用 10.254.175.147 作为 source address，可以通过修改路由表来解决这个问题：\n$ ip route replace local 10.254.175.147 dev kube-dummy-if proto kernel scope host src 192.168.123.249 table local 再次在 node1 上抓包可以发现源IP已经变成了 192.168.123.249。\n$ tcpdump -i ens160 host 172.20.166.130 -nn tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes 03:39:42.824412 IP 192.168.123.249.52684 \u0026gt; 172.20.166.130.8080: Flags [S], seq 3520353543, win 43690, options [mss 65495,sackOK,TS val 360153716 ecr 0,nop,wscale 7], length 0 03:39:42.824542 IP 172.20.166.130.8080 \u0026gt; 192.168.123.249.52684: Flags [S.], seq 4057001749, ack 3520353544, win 28960, options [mss 1460,sackOK,TS val 360143668 ecr 360153716,nop,wscale 7], length 0 03:39:42.824706 IP 192.168.123.249.52684 \u0026gt; 172.20.166.130.8080: Flags [.], ack 1, win 342, options [nop,nop,TS val 360153716 ecr 360143668], length 0 03:39:42.825066 IP 192.168.123.249.52684 \u0026gt; 172.20.166.130.8080: Flags [P.], seq 1:84, ack 1, win 342, options [nop,nop,TS val 360153716 ecr 360143668], length 83: HTTP: GET / HTTP/1.1 03:39:42.825112 IP 172.20.166.130.8080 \u0026gt; 192.168.123.249.52684: Flags [.], ack 84, win 227, options [nop,nop,TS val 360143669 ecr 360153716], length 0 03:39:42.825589 IP 172.20.166.130.8080 \u0026gt; 192.168.123.249.52684: Flags [P.], seq 1:174, ack 84, win 227, options [nop,nop,TS val 360143669 ecr 360153716], length 173: HTTP: HTTP/1.1 200 OK 03:39:42.825735 IP 192.168.123.249.52684 \u0026gt; 172.20.166.130.8080: Flags [.], ack 174, win 350, options [nop,nop,TS val 360153717 ecr 360143669], length 0 03:39:42.825787 IP 192.168.123.249.52684 \u0026gt; 172.20.166.130.8080: Flags [F.], seq 84, ack 174, win 350, options [nop,nop,TS val 360153717 ecr 360143669], length 0 03:39:42.825882 IP 172.20.166.130.8080 \u0026gt; 192.168.123.249.52684: Flags [F.], seq 174, ack 85, win 227, options [nop,nop,TS val 360143669 ecr 360153717], length 0 03:39:42.826002 IP 192.168.123.249.52684 \u0026gt; 172.20.166.130.8080: Flags [.], ack 175, win 350, options [nop,nop,TS val 360153718 ecr 360143669], length 0 问题2：在集群内某个节点主机上通过 SVC IP+Port 访问 service kubernetes 时，如果该节点是 master 节点（即 kube-apiserver 运行在该节点上），那么可以访问，如果该节点不是 master 节点，那么无法访问。 原因和问题1类似，可以通过修改路由表解决：\n# 例如在 node3 节点上 $ ip route replace local 10.254.0.1 dev kube-dummy-if proto kernel scope host src 192.168.123.249 table local 问题3：在某个 pod 内访问该 pod 本身的 ClusterIP:Port，如果 lvs 转到后端的 IP 是该 pod 的 IP，那么无法访问，如果不是则可以访问。 kube-proxy 的 iptables 模式也有同样的问题，这个问题可以忽略。\n总结 # 问题1和问题2修改路由表可以通过批量 shell 脚本来解决：\n#!/bin/sh default_if=$(ip route|grep default|awk \u0026#39;{print $5}\u0026#39;) localip=$(ip a show ${default_if}|egrep -v inet6|grep inet|awk \u0026#39;{print $2}\u0026#39;|awk -F\u0026#34;/\u0026#34; \u0026#39;{print $1}\u0026#39;) svc_ip=$(ip route show table local|egrep -v broadcast|grep kube-dummy-if|awk \u0026#39;{print $2}\u0026#39;) for ip in $svc_ip; do ip route replace local $ip dev kube-dummy-if proto kernel scope host src $localip table local; done 如果想要在创建 service 时自动修改路由表，最好还是将该 fix 整合进 kube-router 的源码中。\n参考 # Kube-router Documentation kube-router之负载均衡器 bad routing from host to service IP on same host ","date":"2018年4月20日","externalUrl":null,"permalink":"/posts/kube-router/","section":"博客","summary":"Kube-router 是一个挺有想法的项目，兼备了 calico 和 kube-proxy 的功能，是基于 Kubernetes 网络设计","title":"Kube-router 使用指南","type":"posts"},{"content":" 前言 # 在实际生产环境中，往往需要根据业务应用场景来设置 lvs 的会话超时时间以及防止 session 连接丢失的问题，如在业务支付环节，如若 session 丢失会导致重复扣款问题，严重影响到安全性，本小节解将会讲到关于 lvs 持久性连接问题。\n为什么用到持久连接？ # 在 Web 服务通信中，当用户在一个网站浏览了A网页并跳转到B网页，此时服务器就认为B网页是一个新的用户请求，你之前的登陆的信息就都丢失了。\n为了记录用户的会话信息，我们的开发者就在客户端/服务器端软件提供了 cookie/session 机制，当你访问网站时，服务器端建立一个 session 会话区，并建立一个 cookie 与这个 session 绑定，将信息发送给你的浏览器。\n这样，只要你的 cookie 存在，服务器端的 session 存在，那么当你打开新页面的时候，服务器依然会认识你。\n在做了负载均衡的时候，上面的机制就出现了问题。假设有以下场景 :\n某电商网站为了实现更多用户的访问，提供了A、B两台服务器，并在前面做了 LVS 负载均衡。于是某用户打开了该购物网站，选中了一件衣服，并加入了购物车(此时背后的操作是：LVS 负载均衡器接受了用户请求，并将其分发到了选中的服务器，并将用户添加了一件衣服记录到这个会话的 session 中)。这时当用户打开了第二个网页，又选中了一件帽子并加入购物车(此时背后的操作是：LVS 负载均衡器接受了用户请求，进行计算，将其发送到选中的服务器上，该服务器将用户添加了一件帽子记录到 session 中)。\n由于 LVS 是一个四层负载均衡器，仅能根据 IP:Port 对数据报文进行分发，不能确保将同一用户根据 session 发往同一个服务器，也就是用户第一次被分配到了A服务器，而第二次可能分配到了B服务器，但是B服务器并没有A服务器用户的 session 记录，直接导致这个例子里的用户发现自己的购物车没有了之前的衣服，而仅有帽子。这是不可接受的。\n为了避免上面的问题，一般站点会有两种方法解决该问题：\n1. 将来自于同一个用户的请求发往同一个服务器\n2. 将 session 信息在服务器集群内共享，每个服务器都保存整个集群的 session 信息\n3. 建立一个 session 存储池，所有 session 信息都保存到存储池中 当然通过 session 共享解决是比较完美的，但实现起来相对复杂： 一需要额外增加服务器设备 二需要代码改动，在用户操作前，需要先获取该用户的session信息 总结下来，第一种方法是最简单的。\nhash算法与持久连接 # LVS 的八种轮询算法中有（Source Hashing）源地址 hash，它和持久连接的作用都是将来自同一个IP的请求都转发到同一个 Server，从而保证了 session 会话定位的问题。两者的不同是：\nSource Hashing 算法 # 该算法在内核中会自动维护一个哈希表，此哈希表中用每一个请求的源IP地址经过哈希计算得出的值作为键，把请求所到达的 RS 的地址作为值。\n在后面的请求中，每一个请求会先经过此哈希表，如果请求在此哈希表中有键值，那么直接定向至特定 RS，如没有，则会新生成一个键值，以便后续请求的定向。\n但是此种方法在时间的记录上比较模糊（依据TCP的连接时长计算）。而且通过 hash 算法无法公平均担后端 real server 的请求，即不能与 rr 等算法同时使用。\n持久连接 # 此种方法实现了无论使用哪一种调度方法，持久连接功能都能保证在指定时间范围之内，来自于同一个IP的请求将始终被定向至同一个 RS，还可以把多种服务绑定后统一进行调度。\n在 director 内有一个 LVS 持久连接模板，模板中记录了每一个请求的来源、调度至的 RS、维护时长等等，所以，在新的请求进入时，首先在此模板中检查是否有记录（有内置的时间限制，比如限制是300秒，当在到达300秒时依然有用户访问，那么持久连接模板就会将时间增加两分钟，再计数，依次类推，每次只延长2分钟），如果该记录未超时，则使用该记录所指向的 RS，如果是超时记录或者是新请求，则会根据调度算法先调度至特定 RS，再将调度的记录添加至此表中。\n这并不与 SH 算法冲突，lvs 持久连接会在新请求达到时，检查后端 RS 的负载状况，这就是比较精细的调度和会话保持方法。\nlvs 的持久性连接有两方面 # 1、把同一个 client 的请求信息记录到 lvs 的 hash 表里，保存时间使用 persistence_timeout 控制，单位为秒。\npersistence_granularity 参数是配合 persistence_timeout 的，在某些情况特别有用。他的值是子网掩码，表示持久连接的粒度，默认是 255.255.255.255，也就是单独的 client ip，如果改成 255.255.255.0，和 client ip 在同一个网段的都会被分配到同一个 real server。\n2、一个连接创建后空闲时的超时时间，这个时间为3种。\ntcp: tcp的空闲超时时间 tcpfin: lvs收到客户端tcp fin的超时时间 udp: udp的超时时间 lvs 相关超时时间查看 # 通过 ipvsadm -Ln 可以查看 persistence_timeout 超时时间(默认超时时间 360s)\n$ ipvsadm -Ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -\u0026gt; RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.254.66.97:8080 rr persistent 10800 -\u0026gt; 172.20.104.7:8080 Masq 1 0 0 -\u0026gt; 172.20.135.6:8080 Masq 1 0 0 -\u0026gt; 172.20.135.7:8080 Masq 1 0 0 通过 ipvsadm -Ln --timeout 可以查看 tcp tcpfin udp 的超时时间（默认: 900 120 300)\n$ ipvsadm -Ln --timeout Timeout (tcp tcpfin udp): 900 120 300 lvs 如何控制这些超时时间工作 # $ ipvsadm -Lnc IPVS connection entries pro expire state source virtual destination TCP 01:54 TIME_WAIT 192.168.123.248:35672 10.254.66.97:8080 172.20.135.6:8080 TCP 180:03 NONE 192.168.123.248:0 10.254.66.97:8080 172.20.135.6:8080 当一个 client 访问 vip 的时候，这时 ipvs 就会记录一条状态为 NONE 的信息，如述上所示，expire 初始值为 persistence_timeout 的值，然后根据时钟主键变小，在以下记录存在期间，同一 client ip 连接上来，都会被分配到同一个后端。\nTIME_WAIT 的值就是 tcp tcpfin udp 中的 tcpfin 的超时时间，当 NONE 的值为0时，如果 TIME_WAIT 还存在，那么 NONE 的值会从新变成 persistence_timeout 的值，再减少，直到 TIME_WAIT 消失以后，NONE 才会消失，只要 NONE 存在，同一 client 的访问，都会分配到统一 real server。\nlvs 关于相关超时时间的设置 # persistence_timeout 可以通过 ipvsadm -p timeout 来设置，默认 360 秒。\n$ ipvsadm -A -t 192.168.20.154:80 -s rr -p 60 上面命令中红色标记的 80 端口，表示如果是同一客户端访问服务器的 80 端口，会被定义到同一个 real server，如果把 80 端口改为 0，那么同一客户端访问服务器的任何服务都会被转发到同一个 real server。\ntcp tcpfin udp 可以通过 ipvsadm --set 对应超时时间 来设置。\n$ ipvsadm --set tcp tcpfin udp tcpfin 的值最好小于 persistence_timeout 的值，这样比较方便计算，也有利于 tcpfin 回收 持久连接定义与原理 # 定义 # 持久连接是指无论使用什么算法，LVS 持久都能实现在一定时间内，将来自同一个客户端请求派发至此前选定的 RS。\n原理 # 当使用 LVS 持久性的时候，Director 在内部使用一个连接根据记录称之为 持久连接模板 来确保所有来自同一个客户端的请求被分发到同一台 Real Server 上。\n持久连接模板是指每一个客户端及分配给它的 RS 的映射关系。 持久连接分类 # 1、 持久端口连接，简称 PPC（Persistent Port Connections）：将来自于同一个客户端对同一个集群某个服务的请求，始终定向至此前选定的 RS\n例如 : client----\u0026gt;LVS(80)----\u0026gt;RS1 或 client----\u0026gt;LVS(23)----\u0026gt;RS2\n缺陷 : 期望访问不同的端口到同一台 RS 上，无法实现。\n配置：\n$ ipvsadm -A -t 172.16.100.1:80 -s rr -p 3600 $ ipvsadm -a -t 172.16.100.1:80 -r 172.16.100.10 -g -w 2 $ ipvsadm -a -t 172.16.100.1:80 -r 172.16.100.11 -g -w 2 2、持久客户端连接，简称 PCC（Persistent Client Connections） : 将来自于同一个客户端对所有端口的请求，始终定向至此前选定的 RS\n说明 : PCC 是一个虚拟服务没有端口号（或者端口号为 0），以 -p 来标识服务。\n缺陷 : 定向所有服务，期望访问不同的 Real Server 无法实现。\n配置：\n$ ipvsadm -A -t 172.16.100.1:0 -s rr -p 3600 $ ipvsadm -a -t 172.16.100.1:0 -r 172.16.100.10 -g -w 2 $ ipvsadm -a -t 172.16.100.1:0 -r 172.16.100.11 -g -w 2 3、基于防火墙设置端口绑定的持久连接，简称 PNMPP（Persistent Netfilter Marked Packet Persistence） : 例如后台 real server 同时提供 80 和 443 端口的服务，并且两个服务之间有联系，这时就要用到 PNMPC\n先对某一特定类型的数据包打上标记，然后再将基于某一类标记的服务送到后台的 Real Server 上去，后台的 Real Server 并不识别这些标记。将持久连接和防火墙标记结合起来就能够实现端口姻亲功能，只要是来自某一客户端的对某一特定服务（需要不同的端口）的访问都定义到同一台 Real Server 上去。\n案例 : 一个用户在访问购物网站时同时使用 HTTP（80）和 HTTPS（443）两种协议，我们需要将其定义到同一台 Real Server 上，而其他的服务不受限制。\n配置：\n$ iptables -t mangle -A PREROUTING -d 172.16.100.1 -i eth0 -p tcp --dport 80 -j MARK --set-mark 8 $ iptables -t mangle -A PREROUTING -d 172.16.100.1 -i eth0 -p tcp --dport 443 -j MARK --set-mark 8 $ ipvsadm -A -f 8 -s rr -p 600 $ ipvsadm -a -f 8 -r 172.16.100.10 -g -w 2 $ ipvsadm -a -f 8 -r 172.16.100.11 -g -w 1 总结 # 如何设置 lvs 持久性连接需要根据业务场景来选择，比如电商平台，对应的持久性连接应该是 PNMPP，另外还需要根据连接类型，比如长连接和短连接，来设置相关超时时间，总之,根据应用场景来选择！\n","date":"2018年4月18日","externalUrl":null,"permalink":"/posts/lvs-persistent-connection/","section":"博客","summary":"前言 # 在实际生产环境中，往往需要根据业务应用场景来设置 lvs 的会","title":"LVS负载均衡之持久性连接介绍","type":"posts"},{"content":" 上一篇文章 介绍了什么是容器运行时，并列出了不同的容器运行时。本篇重点介绍其中的一种容器运行时 CRI-O。\nCRI-O 的诞生 # 当容器运行时（Container Runtime）的标准被提出以后，Red Hat 的一些人开始想他们可以构建一个更简单的运行时，而且这个运行时仅仅为 Kubernetes 所用。这样就有了 skunkworks项目，最后定名为 CRI-O， 它实现了一个最小的 CRI 接口。在 2017 Kubecon Austin 的一个演讲中， Walsh 解释说， ”CRI-O 被设计为比其他的方案都要小，遵从 Unix 只做一件事并把它做好的设计哲学，实现组件重用“。\n根据 Red Hat 的 CRI-O 开发者 Mrunal Patel 在研究里面说的， 最开始 Red Hat 在 2016 年底为它的 OpenShift 平台启动了这个项目，同时项目也得到了 Intel 和 SUSE 的支持。CRI-O 与 CRI 规范兼容，并且与 OCI 和 Docker 镜像的格式也兼容。它也支持校验镜像的 GPG 签名。 它使用容器网络接口 Container Network Interface（CNI）处理网络，以便任何兼容 CNI 的网络插件可与该项目一起使用，OpenShift 也用它来做软件定义存储层。 它支持多个 CoW 文件系统，比如常见的 overlay，aufs，也支持不太常见的 Btrfs。\nCRI-O 的原理及架构 # CRI-O 最出名的特点是它支持“受信容器”和“非受信容器”的混合工作负载。比如，CRI-O 可以使用 Clear Containers 做强隔离，这样在多租户配置或者运行非信任代码时很有用。这个功能如何集成进 Kubernetes 现在还不太清楚，Kubernetes 现在认为所有的后端都是一样的。\n当 Kubernetes 需要运行容器时，它会与 CRI-O 进行通信，CRI-O 守护程序与 runc（或另一个符合 OCI 标准的运行时）一起启动容器。当 Kubernetes 需要停止容器时，CRI-O 会来处理，它只是在幕后管理 Linux 容器，以便用户不需要担心这个关键的容器编排。\nCRI-O 有一个有趣的架构（见下图），它重用了很多基础组件，下面我们来看一下各个组件的功能及工作流程。\nKubernetes 通知 kubelet 启动一个 pod。\nkubelet 通过 CRI(Container runtime interface) 将请求转发给 CRI-O daemon。\nCRI-O 利用 containers/image 库从镜像仓库拉取镜像。\n下载好的镜像被解压到容器的根文件系统中，并通过 containers/storage 库存储到 COW 文件系统中。\n在为容器创建 rootfs 之后，CRI-O 通过 oci-runtime-tool 生成一个 OCI 运行时规范 json 文件，描述如何使用 OCI Generate tools 运行容器。\n然后 CRI-O 使用规范启动一个兼容 CRI 的运行时来运行容器进程。默认的运行时是 runc。\n每个容器都由一个独立的 conmon 进程监控，conmon 为容器中 pid 为 1 的进程提供一个 pty。同时它还负责处理容器的日志记录并记录容器进程的退出代码。\n网络是通过 CNI 接口设置的，所以任何 CNI 插件都可以与 CRI-O 一起使用。\n隆重介绍一下 conmon # 根据 Patel 所说，conmon 程序是“纯C编写的，用来提高稳定性和性能”，conmon 负责监控，日志，TTY 分配，以及类似 out-of-memory 情况的杂事。\nconmon 需要去做所有 systemd 不做或者不想做的事情。即使 CRI-O 不直接使用 systemd 来管理容器，它也将容器分配到 sytemd 兼容的 cgroup 中，这样常规的 systemd 工具比如 systemctl 就可以看见容器资源使用情况了。\n因为 conmon（不是CRI daemon）是容器的父进程，它允许 CRI-O 的部分组件重启而不会影响容器，这样可以保证更加平滑的升级。现在 Docker 部署的问题就是 Docker 升级需要重起所有的容器。 通常这对于 Kubernetes 集群来说不是问题，但因为它可以将容器迁移来滚动升级。\n下一步 # CRI-O 1.0 在2017年10月发布，支持 Kubernetes 1.7，后来 CRI-O 1.8，1.9 相继发布，支持 Kubernetes 的 1.8， 1.9（此时版本命名规则改为与Kubernetes一致）。\nCRI-O 在 Openshift 3.7 中作为 beta 版提供，Patel 考虑在 Openshift 3.9 中让它进步一步稳定，在 3.10 中成为缺省的运行时，同时让 Docker 作为候选的运行时。\n下一步的工作包括集成新的 Kata Containers 的这个基于 VM 的运行时，增加 kube-spawn 的支持，支持更多类似 NFS， GlusterFS 的存储后端等。 团队也在讨论如何通过支持 casync 或者 libtorrent 来优化多节点间的镜像同步。\n如果你想贡献或者关注开发，就去 CRI-O 项目的 GitHub 仓库，然后关注 CRI-O 博客。\n参考 # CRI-O and Alternative Runtimes in Kubernetes Lightweight Container Runtime for Kubernetes CRI-O Support for Kubernetes CRI-O 1.0 简介 ","date":"2018年4月3日","externalUrl":null,"permalink":"/posts/cri-o/","section":"博客","summary":"上一篇文章 介绍了什么是容器运行时，并列出了不同的容器运行时。","title":"CRI-O 简介","type":"posts"},{"content":"容器运行时（Container Runtime）是 Kubernetes 最重要的组件之一，负责真正管理镜像和容器的生命周期。Kubelet 通过 Container Runtime Interface (CRI) 与容器运行时交互，以管理镜像和容器。\n容器运行时接口(Container Runtime Interface (CRI)) 是 Kubelet 1.5 和 kubelet 1.6 中主要负责的一块项目，它重新定义了 Kubelet Container Runtime API，将原来完全面向 Pod 级别的 API 拆分成面向 Sandbox 和 Container 的 API，并分离镜像管理和容器引擎到不同的服务。\nCRI 最早从从 1.4 版就开始设计讨论和开发，在 v1.5 中发布第一个测试版。在 v1.6 时已经有了很多外部容器运行时，如 frakti、cri-o 的 alpha 支持。v1.7 版本新增了 cri-containerd 的 alpha 支持，而 frakti 和 cri-o 则升级到 beta 支持。\nCRI 接口 # CRI 基于 gRPC 定义了 RuntimeService 和 ImageService，分别用于容器运行时和镜像的管理。其定义在\nv1.10+: pkg/kubelet/apis/cri/v1alpha2/runtime v1.7~v1.9: pkg/kubelet/apis/cri/v1alpha1/runtime v1.6: pkg/kubelet/api/v1alpha1/runtime Kubelet 作为 CRI 的客户端，而 Runtime 维护者则需要实现 CRI 服务端，并在启动 kubelet 时将其传入：\n$ kubelet --container-runtime=remote --container-runtime-endpoint=unix:///var/run/crio/crio.sock .. 如何开发新的 Container Runtime # 开发新的 Container Runtime 只需要实现 CRI gRPC Server，包括 RuntimeService 和 ImageService。该 gRPC Server 需要监听在本地的 unix socket（Linux 支持 unix socket 格式，Windows 支持 tcp 格式）。\n具体的实现方法可以参考下面已经支持的 Container Runtime 列表。\n目前支持的 Container Runtime # 目前，有多家厂商都在基于 CRI 集成自己的容器引擎，其中包括:\nDocker: 核心代码依然保留在 kubelet 内部（pkg/kubelet/dockershim），依然是最稳定和特性支持最好的 Runtime\nHyperContainer: 支持 Kubernetes v1.6+，提供基于 hypervisor 和 docker 的混合运行时，适用于运行非可信应用，如多租户和 NFV 等场景\nRunc 有两个实现，cri-o 和 cri-containerd\ncri-containerd: 支持 kubernetes v1.7+ cri-o: 支持 Kubernetes v1.6+，底层运行时支持 runc 和 intel clear container Rkt: 开发中\nMirantis: 直接管理 libvirt 虚拟机，镜像须是 qcow2 格式\nInfranetes: 直接管理 IaaS 平台虚拟机，如 GCE、AWS 等\ncri-containerd # 以 Containerd 为例，在 1.0 及以前版本将 dockershim 和 docker daemon 替换为 cri-containerd + containerd，而在 1.1 版本直接将 cri-containerd 内置在 Containerd 中，简化为一个 CRI 插件。\nContainerd 内置的 CRI 插件实现了 Kubelet CRI 接口中的 Image Service 和 Runtime Service，通过内部接口管理容器和镜像，并通过 CNI 插件给 Pod 配置网络。\nCRI Tools # 为了方便开发、调试和验证新的 Container Runtime，社区还维护了一个 cri-tools 工具，它提供两个组件\ncrictl: 类似于 docker 的命令行工具，不需要通过 Kubelet 就可以跟 Container Runtime 通信，可用来调试或排查问题 critest: CRI 的验证测试工具，用来验证新的 Container Runtime 是否实现了 CRI 需要的功能 另外一个工具是 libpod，它也提供了一个组件： podman，功能和 crictl 类似。\n如果想构建 oci 格式的镜像，可以使用工具： buildah\n","date":"2018年4月3日","externalUrl":null,"permalink":"/posts/container-runtime/","section":"博客","summary":"容器运行时（Container Runtime）是 Kubernetes 最重要的组","title":"Kubernetes 中的容器运行时","type":"posts"},{"content":"docker 里面可以通过 docker pull、docker build、docker commit、docker load、docker import 等方式得到一个 image，得到 image 之后 docker 在本地是怎么存储的呢？本篇将以 docker pull 为例，简述 image 的获取和存储方式。\n镜像相关的配置 # docker 里面和 image 有关的目录为 /var/lib/docker，里面存放着 image 的所有信息，可以通过下面这个 dockerd 的启动参数来修改这个目录的路径。\n--graph, -g /var/lib/docker Root of the Docker runtime 镜像的引用方式 # 在需要引用 image 的时候，比如 docker pull 的时候，或者运行容器的时候，都需要指定一个image名称，引用一个镜像有多种方式，下面以 alpine 为例进行说明.\n由于 sha256 码太长，所以用 abcdef\u0026hellip; 来表示完整的 sha256，节约空间 docker hub 上的官方镜像 # alpine: 官方提供的最新 alpine 镜像，对应的完整名称为 docker.io/library/alpine:latest alpine:3.7: 官方提供的 alpine 3.7 镜像，对应的完整名称为 docker.io/library/alpine:3.7 alpine:@sha256:abcdef\u0026hellip;: 官方提供的 digest 码为 sha256:abcdef\u0026hellip; 的 alpine 镜像，对应的完整名称为 docker.io/library/alpine@sha256:abcdef... docker hub 上的非官方（个人）镜像 # 引用方式和官方镜像一样，唯一不同的是需要在镜像名称前面带上用户前缀，如：\nuser1/alpine: 由 user1 提供的最新 alpine 镜像， 对应的完整名称为 docker.io/user1/alpine:latest user1/alpine:3.7 和 user1/alpine:@sha256:abcdef... 这两种方式也是和上面一样，等同于 docker.io/user1/alpine:3.7 和 docker.io/user1/alpine:@sha256:abcdef...\n自己搭建的 registry 里的镜像 # 引用方式和 docker hub 一样，唯一不同的是需要在镜像名称最前面带上地址，如：\nlocalhost:5000/alpine: 本地自己搭建的 registry（localhost:5000）里面的官方 alpine 的最新镜像，对应的完整名称为 localhost:5000/library/alpine:latest localhost:5000/user1/alpine@sha256:a123def\u0026hellip;: 本地自己搭建的 registry（localhost:5000）里面由用户 user1 提供的 digest 为 sha256:a123def 的 alpine 镜像 其它的几种情况和上面的类似。\n为什么需要镜像的 digest？ # 对于某些 image 来说，可能在发布之后还会做一些更新，比如安全方面的，这时虽然镜像的内容变了，但镜像的名称和 tag 没有变，所以会造成前后两次通过同样的名称和 tag 从服务器得到不同的两个镜像的问题，于是 docker 引入了镜像的 digest 的概念，一个镜像的 digest 就是镜像的 manifes 文件的 sha256 码，当镜像的内容发生变化的时候，即镜像的 layer 发生变化，从而 layer 的 sha256 发生变化，而 manifest 里面包含了每一个 layer 的 sha256，所以 manifest 的 sha256 也会发生变化，即镜像的 digest 发生变化，这样就保证了 digest 能唯一的对应一个镜像。\ndocker pull的大概过程 # 如果对 Image manifest，Image Config 和 Filesystem Layers 等概念不是很了解，请先参考 image(镜像)是什么。\n取 image 的大概过程如下：\ndocker 发送 image 的名称+tag（或者 digest）给 registry 服务器，服务器根据收到的 image 的名称+tag（或者 digest），找到相应 image 的 manifest，然后将 manifest 返回给 docker docker 得到 manifest 后，读取里面 image 配置文件的 digest(sha256)，这个 sha256 码就是 image 的 ID 根据 ID 在本地找有没有存在同样 ID 的 image，有的话就不用继续下载了 如果没有，那么会给 registry 服务器发请求（里面包含配置文件的 sha256 和 media type），拿到 image 的配置文件（Image Config） 根据配置文件中的 diff_ids（每个 diffid 对应一个 layer tar 包的 sha256，tar 包相当于 layer 的原始格式），在本地找对应的 layer 是否存在 如果 layer 不存在，则根据 manifest 里面 layer 的 sha256 和 media type 去服务器拿相应的 layer（相当去拿压缩格式的包）。 拿到后进行解压，并检查解压后 tar 包的 sha256 能否和配置文件（Image Config）中的 diff_id 对的上，对不上说明有问题，下载失败 根据 docker 所用的后台文件系统类型，解压 tar 包并放到指定的目录 等所有的 layer 都下载完成后，整个 image 下载完成，就可以使用了 对于 layer 来说，config 文件中 diffid 是 layer 的 tar 包的 sha256，而 manifest 文件中的 digest 依赖于 media type，比如 media type 是 tar+gzip，那 digest 就是 layer 的 tar 包经过 gzip 压缩后的内容的 sha256，如果 media type 就是 tar 的话，diffid 和 digest 就会一样。 dockerd 和 registry 服务器之间的协议为 Registry HTTP API V2。\nimage 本地存放位置 # 这里以 ubuntu 的 image 为例，展示 docker 的 image 存储方式。\n先看看 ubuntu 的 image id 和 digest，然后再分析 image 数据都存在哪里。\n$ docker images --digests REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE ubuntu latest sha256:e348fbbea0e0a0e73ab0370de151e7800684445c509d46195aef73e090a49bd6 f975c5035748 3 weeks ago 112MB ...... 对于本地生成的镜像来说，由于没有上传到 registry 上去，所以没有 digest，因为镜像的 manifest 由 registry 生成。 repositories.json # repositories.json 中记录了和本地 image 相关的 repository 信息，主要是 name 和 image id 的对应关系，当 image 从 registry 上被 pull 下来后，就会更新该文件：\n#这里目录中的 overlay2 为 docker 后台所采用的存储文件系统名称， #如果是其他的文件系统的话，名字会是其他的，比如btrfs、aufs、devicemapper等。 $ cat /var/lib/docker/image/overlay2/repositories.json|jq . { \u0026#34;Repositories\u0026#34;: { \u0026#34;ubuntu\u0026#34;: { \u0026#34;ubuntu:latest\u0026#34;: \u0026#34;sha256:f975c50357489439eb9145dbfa16bb7cd06c02c31aa4df45c77de4d2baa4e232\u0026#34;, \u0026#34;ubuntu@sha256:e348fbbea0e0a0e73ab0370de151e7800684445c509d46195aef73e090a49bd6\u0026#34;: \u0026#34;sha256:f975c50357489439eb9145dbfa16bb7cd06c02c31aa4df45c77de4d2baa4e232\u0026#34; } ...... } } ubuntu: repository 的名称，前面没有服务器信息的表示这是官方 registry(docker hub) 里面的 repository，里面包含的都是 image 标识和 image ID 的对应关系\nubuntu:latest 和 ubuntu@sha256:e348fbb\u0026hellip;: 他们都指向同一个image（sha256:f975c5...）\n配置文件（image config） # docker 根据后台所采用的文件系统不同，在 /var/lib/docker 目录下创建了不同的子目录，对于 CentOS 来说，默认文件系统是 overlay2。本文以 CentOS 为例。\ndocker 根据第一步得到的 manifest，从 registry 拿到 config 文件，然后保存在 image/overlay2/imagedb/content/sha256/ 目录下，文件名称就是文件内容的 sha256 码，即 image id：\n$ sha256sum /var/lib/docker/image/overlay2/imagedb/content/sha256/f975c50357489439eb9145dbfa16bb7cd06c02c31aa4df45c77de4d2baa4e232 f975c50357489439eb9145dbfa16bb7cd06c02c31aa4df45c77de4d2baa4e232 /var/lib/docker/image/overlay2/imagedb/content/sha256/f975c50357489439eb9145dbfa16bb7cd06c02c31aa4df45c77de4d2baa4e232 #这里我们只关注这个 image 的 rootfs， #从 diff_ids 里可以看出 ubuntu:latest 这个 image 包含了 5 个 layer， #从上到下依次是从底层到顶层，a94e0d...是最底层，db584c...是最顶层 $ cat /var/lib/docker/image/overlay2/imagedb/content/sha256/f975c50357489439eb9145dbfa16bb7cd06c02c31aa4df45c77de4d2baa4e232|jq . ...... \u0026#34;rootfs\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;layers\u0026#34;, \u0026#34;diff_ids\u0026#34;: [ \u0026#34;sha256:a94e0d5a7c404d0e6fa15d8cd4010e69663bd8813b5117fbad71365a73656df9\u0026#34;, \u0026#34;sha256:88888b9b1b5b7bce5db41267e669e6da63ee95736cb904485f96f29be648bfda\u0026#34;, \u0026#34;sha256:52f389ea437ebf419d1c9754d0184b57edb45c951666ee86951d9f6afd26035e\u0026#34;, \u0026#34;sha256:52a7ea2bb533dc2a91614795760a67fb807561e8a588204c4858a300074c082b\u0026#34;, \u0026#34;sha256:db584c622b50c3b8f9b8b94c270cc5fe235e5f23ec4aacea8ce67a8c16e0fbad\u0026#34; ] } ...... layer 的 diff_id 和 digest 的对应关系 # layer 的 diff_id 存在 image 的配置文件中，而 layer 的 digest 存在 image 的 manifest 中，他们的对应关系被存储在了 image/overlay2/distribution 目录下：\n$ tree -d /var/lib/docker/image/overlay2/distribution /var/lib/docker/image/overlay2/distribution ├── diffid-by-digest │ └── sha256 └── v2metadata-by-diffid └── sha256 diffid-by-digest : 存放 digest 到 diffid 的对应关系 v2metadata-by-diffid : 存放 diffid 到 digest 的对应关系 #这里以最底层 layer(a94e0d...) 为例，查看其 digest 信息 $ cat /var/lib/docker/image/overlay2/distribution/v2metadata-by-diffid/sha256/db584c622b50c3b8f9b8b94c270cc5fe235e5f23ec4aacea8ce67a8c16e0fbad|jq . [ { \u0026#34;Digest\u0026#34;: \u0026#34;sha256:b78396653dae2bc0d9c02c0178bd904bb12195b2b4e541a92cd8793ac7d7d689\u0026#34;, \u0026#34;SourceRepository\u0026#34;: \u0026#34;docker.io/library/ubuntu\u0026#34;, \u0026#34;HMAC\u0026#34;: \u0026#34;\u0026#34; } ] #根据 digest 得到 diffid $ cat /var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256/b78396653dae2bc0d9c02c0178bd904bb12195b2b4e541a92cd8793ac7d7d689 sha256:db584c622b50c3b8f9b8b94c270cc5fe235e5f23ec4aacea8ce67a8c16e0fbad layer 的元数据 # layer 的属性信息都放在了 image/overlay2/layerdb 目录下，目录名称是 layer 的 chainid，由于最底层的 layer 的 chainid 和 diffid 相同，所以这里我们用第二层（fe9a3f\u0026hellip;）作为示例：\n计算 chainid 时，用到了所有祖先 layer 的信息，从而能保证根据 chainid 得到的 rootfs 是唯一的。比如我在 debian 和 ubuntu 的 image 基础上都添加了一个同样的文件，那么 commit 之后新增加的这两个 layer 具有相同的内容，相同的 diffid，但由于他们的父 layer 不一样，所以他们的 chainid 会不一样，从而根据 chainid 能找到唯一的 rootfs。计算 chainid 的方法请参考 image spec #计算 chainid #这里 88888b... 是第二层的 diffid，而 a94e0d... 是 88888b... 父层的 chainid， #由于a94e0d...是最底层，它没有父层，所以 a94e0d... 的 chainid 就是 a94e0d... $ echo -n \u0026#34;sha256:a94e0d5a7c404d0e6fa15d8cd4010e69663bd8813b5117fbad71365a73656df9 sha256:88888b9b1b5b7bce5db41267e669e6da63ee95736cb904485f96f29be648bfda\u0026#34;|sha256sum - 14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20 - #根据 chainid 来看看相应目录的内容 $ ll /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20 total 20K -rw-r--r-- 1 root root 64 Apr 1 22:16 cache-id -rw-r--r-- 1 root root 71 Apr 1 22:16 diff -rw-r--r-- 1 root root 71 Apr 1 22:16 parent -rw-r--r-- 1 root root 3 Apr 1 22:16 size -rw-r--r-- 1 root root 1.5K Apr 1 22:16 tar-split.json.gz #每个 layer 都有这样一个对应的文件夹 #cache-id 是 docker 下载 layer 的时候在本地生成的一个随机 uuid， #指向真正存放 layer 文件的地方 $ cat /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20/cache-id 658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb #diff 文件存放 layer 的 diffid $ cat /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20/diff sha256:88888b9b1b5b7bce5db41267e669e6da63ee95736cb904485f96f29be648bfda #parent 文件存放当前 layer 的父 layer 的 diffid， #注意：对于最底层的 layer 来说，由于没有父 layer，所以没有这个文件 $ cat /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20/parent sha256:a94e0d5a7c404d0e6fa15d8cd4010e69663bd8813b5117fbad71365a73656df9 #当前 layer 的大小，单位是字节 $ cat /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20/size 745 #tar-split.json.gz，layer 压缩包的 split 文件，通过这个文件可以还原 layer 的 tar 包， #在 docker save 导出 image 的时候会用到 #详情可参考 https://github.com/vbatts/tar-split $ ll /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20/tar-split.json.gz -rw-r--r-- 1 root root 1.5K Apr 1 22:16 /var/lib/docker/image/overlay2/layerdb/sha256/14a40a140881d18382e13b37588b3aa70097bb4f3fb44085bc95663bdc68fe20/tar-split.json.gz layer数据 # 以 CentOS 为例，所有 layer 的文件都放在了 /var/lib/docker/overlay2 目录下。\n$ tree -d -L 2 /var/lib/docker/overlay2 ├── 658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb │ ├── diff │ └── work ├── 66ce99b5da081f65afea7ebaf612229179b620dc728b7407adcb44a51a27ae24 │ ├── diff │ └── work ... └── l ├── DYWQJVCIPQ2P2VFWZ4KBCV2JFW -\u0026gt; ../658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb/diff ├── 27O3MGWIL6SIN7K4GLVU4DLPSQ -\u0026gt; ../9028bae38f520a09220f67fbcf698aae2326c8318390a1d6005457d51ad97369/diff ... ”l“ 目录包含一些符号链接作为缩短的层标识符. 这些缩短的标识符用来避免挂载时超出页面大小的限制。\n$ ll /var/lib/docker/overlay2/l/ total 0 lrwxrwxrwx 1 root root 72 Mar 29 06:25 27O3MGWIL6SIN7K4GLVU4DLPSQ -\u0026gt; ../9028bae38f520a09220f67fbcf698aae2326c8318390a1d6005457d51ad97369/diff/ lrwxrwxrwx 1 root root 72 Mar 21 00:55 2AYPFAXSXLNCCEA6WRFCAPFPX3 -\u0026gt; ../23eb8415aec245a0291cca62d2da322de241263b8cbdfc690c0a77b353530b10/diff/ lrwxrwxrwx 1 root root 72 Mar 29 02:55 2H2XLZTCOYYSDT3XU2BDJRC2SB -\u0026gt; ../6b27128471bdfc742696ff9820bdfcdda73020753c26efeecea29b98096f0c5d/diff/ lrwxrwxrwx 1 root root 77 Mar 29 05:44 2JQ3OQVJBRYD75J4WTG4CSWA4Z -\u0026gt; ../641300d147b30f162167fed340cebcaae25f46db608939f6af09dbdb7078dcd4-init/diff/ lrwxrwxrwx 1 root root 72 Mar 21 00:55 2TCUOOM7Y7HMGIERRS4CX4YHVA -\u0026gt; ../cd3a3bd11269dc846ee9f79fca86c05336b8dd475d5ca8151991dc5d9fd7261f/diff/ lrwxrwxrwx 1 root root 77 Mar 29 06:24 36WQQRTYLT4P3J7DYLQAUMUPJE -\u0026gt; ../7ee9cc176abeb603ab0461650edd87890d167c579011813d0e864b7524f9fe24-init/diff/ ... 注意：由于 docker 所采用的文件系统不同，/var/lib/docker/ 目录下的目录结构及组织方式也会不一样，要具体文件系统具体分析，本文只介绍 overlay2 这种情况。\n关于 aufs 和 btrfs 的相关特性可以参考 Linux 文件系统之 aufs 和 Btrfs 文件系统之 subvolume 与 snapshot 还是以刚才的第二层 layer（88888b\u0026hellip;）为例，看看实际的数据：\n最底层包含 link 文件(不包含 lower 文件，因为是最底层)，在上面的结果中 a94e0d... 为最底层。 这个文件记录着作为标识符的更短的符号链接的名字、最底层还有一个 diff 目录(包含实际内容)。\n# 查看底层 a94e0d... 的 layer 存放的地方 $ cat /var/lib/docker/image/overlay2/layerdb/sha256/a94e0d5a7c404d0e6fa15d8cd4010e69663bd8813b5117fbad71365a73656df9/cache-id 8feee71ff338d03a22ef090f8e5a49771ca8c1f418db345782ff0fb9b9fff3ce $ ll /var/lib/docker/overlay2/8feee71ff338d03a22ef090f8e5a49771ca8c1f418db345782ff0fb9b9fff3ce/ total 4.0K drwxr-xr-x 21 root root 224 Apr 1 22:16 diff/ -rw-r--r-- 1 root root 26 Apr 1 22:15 link $ cat /var/lib/docker/overlay2/8feee71ff338d03a22ef090f8e5a49771ca8c1f418db345782ff0fb9b9fff3ce/link 3GQZNQYZNRAXT6X453L5O73Y5U $ ll /var/lib/docker/overlay2/l/|grep 3GQZNQYZNRAXT6X453L5O73Y5U lrwxrwxrwx 1 root root 72 Apr 1 22:15 3GQZNQYZNRAXT6X453L5O73Y5U -\u0026gt; ../8feee71ff338d03a22ef090f8e5a49771ca8c1f418db345782ff0fb9b9fff3ce/diff/ # diff 目录下面是层的内容 $ ll /var/lib/docker/overlay2/8feee71ff338d03a22ef090f8e5a49771ca8c1f418db345782ff0fb9b9fff3ce/diff/ total 16K drwxr-xr-x 2 root root 4.0K Feb 28 14:14 bin/ drwxr-xr-x 2 root root 6 Apr 12 2016 boot/ drwxr-xr-x 4 root root 4.0K Feb 28 14:14 dev/ drwxr-xr-x 42 root root 4.0K Feb 28 14:14 etc/ drwxr-xr-x 2 root root 6 Apr 12 2016 home/ ... 从第二层开始，每层镜像层包含 lower 文件，该文件的内容表示父层镜像的符号链接，根据这个文件可以索引构建出整个镜像的层次结构。同时还包含 merged 和 work 目录。\n每当启动一个容器时，会将 link 指向的镜像层目录以及 lower 指向的镜像层目录联合挂载到 merged 目录，因此，容器内的视角就是 merged 目录下的内容。 而 work 目录则是用来完成如 copy-on_write 的操作。 $ tree -L 1 /var/lib/docker/overlay2/658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb /var/lib/docker/overlay2/658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb ├── diff ├── link ├── lower └── work # 父层镜像的符号链接 $ cat /var/lib/docker/overlay2/658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb/lower l/3GQZNQYZNRAXT6X453L5O73Y5U $ ll /var/lib/docker/overlay2/658299560be0fd7eaf4a14b0927e134049d13eb31070a9902b0d275836a13cfb/diff total 0 drwxr-xr-x 4 root root 29 Mar 6 17:17 etc/ drwxr-xr-x 2 root root 21 Mar 6 17:17 sbin/ drwxr-xr-x 3 root root 18 Feb 28 14:13 usr/ drwxr-xr-x 3 root root 17 Feb 28 14:14 var/ manifest文件去哪了？ # 从前面介绍 docker pull 的过程中得知，docker 是先得到 manifest，然后根据 manifest 得到 config 文件和 layer。\n前面已经介绍了 config 文件和 layer 的存储位置，但唯独不见 manifest，去哪了呢？\nmanifest 里面包含的内容就是对 config 和 layer 的 sha256 + media type 描述，目的就是为了下载 config 和 layer，等 image 下载完成后，manifest 的使命就完成了，里面的信息对于 image 的本地管理来说没什么用，所以 docker 在本地没有单独的存储一份 manifest 文件与之对应。\n结束语 # 本篇介绍了image在本地的存储方式，包括了 /var/lib/docker/image 和 /var/lib/docker/overlay2 这两个目录，但 /var/lib/docker/image 下面有两个目录没有涉及：\n/var/lib/docker/image/overlay2/imagedb/metadata：里面存放的是本地 image 的一些信息，从服务器上 pull 下来的 image 不会存数据到这个目录，下次有机会再补充这部分内容。\n/var/lib/docker/image/overlay2/layerdb/mounts: 创建 container 时，docker 会为每个 container 在 image 的基础上创建一层新的 layer，里面主要包含 /etc/hosts、/etc/hostname、/etc/resolv.conf 等文件，创建的这一层 layer 信息就放在这里，后续在介绍容器的时候，会专门介绍这个目录的内容。\n参考 # docker源代码 ","date":"2018年4月2日","externalUrl":null,"permalink":"/posts/how-manage-image/","section":"博客","summary":"docker 里面可以通过 docker pull、docker build、docke","title":"docker 在本地如何管理 image（镜像）?","type":"posts"},{"content":"在云计算环境中，服务的作用距离范围从近到远一般可以有：\n同主机（Host，Node） 跨主机同可用区（Available Zone） 跨可用区同地区（Region） 跨地区同服务商（Cloud Service Provider） 跨云平台 K8s 的设计定位是单一集群在同一个地域内，因为同一个地区的网络性能才能满足 K8s 的调度和计算存储连接要求。\n但是实际情况中经常遇到的一些问题，就是单个集群通常无法跨单个云厂商的多个 Region，更不用说支持跨跨域不同的云厂商。这样会给企业带来一些担忧，如何应对可用区级别的 Fail，以及容灾备份？是否会造成厂商锁定，增加迁移成本？如何应对线上线下突发流量？如何统一管理调度容器资源？单个集群规模的上限等等。\n集群联邦（Federation）可以一定程度上解决这些问题。Federation 是可以将分布在多个 Region 或者多个云厂商的 Kubernetes 集群整合成一个大的集群，统一管理与调度。\nKubernetes集群联邦介绍 # 管理多个 kuberntes 集群 # 集群联邦在架构上同 kubernetes 集群很相似。有一个集群联邦的 API server 提供一个标准的 Kubernetes API，并且通过 etcd 来存储状态。不同的是，一个通常的Kubernetes 只是管理节点计算，而集群联邦管理所有的 kubernetes 集群。\nFederation主要包括三个组件：\nfederation-apiserver : 类似 kube-apiserver，但提供的是跨集群的 REST API federation-controller-manager : 类似 kube-controller-manager，但提供多集群状态的同步机制 kubefed : Federation 管理命令行工具 用户可以通过 Federation 的 API Server 注册该 Federation 的成员 K8s Cluster。当用户通过 Federation 的 API Server 创建、更改 API 对象时，Federation API Server 会在自己所有注册的子 K8s Cluster 都创建一份对应的 API 对象。\n在提供业务请求服务时，K8s Federation 会先在自己的各个子 Cluster 之间做负载均衡，而对于发送到某个具体 K8s Cluster 的业务请求，会依照这个 K8s Cluster 独立提供服务时一样的调度模式去做 K8s Cluster 内部的负载均衡。而Cluster 之间的负载均衡是通过域名服务的负载均衡来实现的。\n所有的设计都尽量不影响 K8s Cluster 现有的工作机制，这样对于每个子 K8s 集群来说，并不需要更外层的有一个 K8s Federation，也就是意味着所有现有的 K8s 代码和机制不需要因为 Federation 功能有任何变化。\n跨集群服务发现 # Kubernetes 有一个标准的插件：kube-dns，这个插件可以在集群内部提供 DNS 服务，通过 DNS 解析 service 名字来访问 kubernetes 服务。\nKubernetes 服务是由一组 kubernetes POD 组成的，这些 POD 是一些已经容器化了的应用，这些 POD 前面使用到了负载均衡器。\n假如我们有一个 kubernetes 集群，这个集群里面有一个服务叫做 mysql，这个服务是由一组 mysql POD 组成的。在这个 kubernetes 集群中，其他应用可以通过 DNS 来访问这个 mysql 服务。\n跨集群调度 # 为了追求高可用性和更高的性能，集群联邦能够把不同 POD 指定给不同的 Kubernetes 集群中。集群联邦调度器将决定如何在不同 kubernetes 集群中分配工作负载。\n通过跨集群调度，我们可以：\n跨 kubernetes 集群均匀的调度任务负载 将各个 kubernetes 集群的工作负载进行最大化，如果当前 kubernetes 集群超出了承受能力，那么将额外的工作负载路由到另一个比较空闲的 kubernetes 集群中 根据应用地理区域需求，调度工作负载到不同的 kubernetes 集群中，对于不同的终端用户，提供更高的带宽和更低的延迟。 集群高可用，故障自动迁移 # 集群联邦可以跨集群冗馀部署，当某个集群所在区域出现故障时，并不影响整个服务。集群联邦还可以检测集群是否为不可用状态，如果发现某个集群为不可用状态时，可以将失败的任务重新分配给集群联邦中其他可用状态的集群上。\n使用集群联邦实现多集群管理 # 系统环境 # 功能组件 系统组件 系统版本 设备数量 备注 联邦集群控制平面 k8s 1.9+Federation CentOS 7.3 3台 联邦集群控制平面 K8s集群01 k8s 1.9 master+node CentOS 7.3 3台 联邦集群节点 安装 kubefed # 选择其中的一个集群作为主集群，这个主集群将运行组成联邦控制面板的所有组件。\n使用下列命令下载对应最新发行的 kubefed 安装包并将安装包里的二进制文件解压出来：\n$ curl -LO https://storage.cloud.google.com/kubernetes-federation-release/release/${RELEASE-VERSION}/federation-client-linux-amd64.tar.gz $ tar -xzvf federation-client-linux-amd64.tar.gz 请用 federation release page页面实际的版本号替换变量 RELEASE-VERSION。\n将解压出来的内容复制到你的环境变量 $PATH 里的随便一个路径， 并设置可执行权限。\n$ cp federation/client/bin/kubefed /usr/local/bin $ chmod +x /usr/local/bin/kubefed 配置 context # 在准备配置联邦集群的 DCE 集群中配置两个 DCE 集群的 context。让改节点能通过切换 context 连接不同的子集群。\n先创建本地集群的 kubeconfig 文件\n$ export KUBE_APISERVER=\u0026#34;https://192.168.123.250:6443\u0026#34; # 设置集群参数 $ kubectl config set-cluster kubernetes \\ --certificate-authority=/etc/kubernetes/ssl/ca.pem \\ --embed-certs=true \\ --server=${KUBE_APISERVER} # 设置客户端认证参数 $ kubectl config set-credentials admin \\ --client-certificate=/etc/kubernetes/ssl/admin.pem \\ --embed-certs=true \\ --client-key=/etc/kubernetes/ssl/admin-key.pem # 设置上下文参数 $ kubectl config set-context kubernetes \\ --cluster=kubernetes \\ --user=admin # 设置默认上下文 $ kubectl config use-context kubernetes 生成的 kubeconfig 被保存到 ~/.kube/config 文件。\n~/.kube/config 文件拥有对该集群的最高权限，请妥善保管。 配置结果如下：\n$ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * kubernetes kubernetes admin 设置 CoreDNS 作为集群联邦的 DNS 提供商 # 前提 # 为启用 CoreDNS 来实现跨联邦集群的服务发现，联邦的成员集群中必须支持 LoadBalancer 服务。（本地集群默认不支持 LoadBalancer 服务，所以要让本地集群支持 LoadBalancer 服务才能使用 coredns 来实现 federation 的服务发现功能！！！） 我们可以利用 helm charts 来部署 CoreDNS。 CoreDNS 部署时会以 etcd 作为后端，并且 etcd 应预先安装。 etcd 也可以利用 helm charts 进行部署。 所有加入 federation 的集群的 node 必须打上以下的标签：\nfailure-domain.beta.kubernetes.io/region=\u0026lt;region\u0026gt;\nfailure-domain.beta.kubernetes.io/zone=\u0026lt;zone\u0026gt; 使本地集群支持 LoadBalancer 服务 # 为了使本地集群支持 LoadBalancer 服务，可以参考以下两种实现方案：\nkeepalived-cloud-provider metalLB 这里我们选择使用 metalLB。\nmetalLB 的部署很简单，直接使用 yaml 文件部署：\n$ kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.5.0/manifests/metallb.yaml 具体参考 https://metallb.universe.tf/installation/\n部署完成后需要为 LoadBalancer 服务选择一个特定的 IP 地址池，这里通过 configmap 来创建。\n下面是一个简单示例：\n$ cat metallb-cm.yaml apiVersion: v1 kind: ConfigMap metadata: namespace: metallb-system name: config data: config: | address-pools: - name: default protocol: layer2 addresses: - 192.168.1.58-192.168.1.60 $ kubectl create -f metallb-cm.yaml 更多高级配置请参考： https://metallb.universe.tf/configuration/\n现在本地集群已经支持 LoadBalancer 服务了，下面我们开始 federation 的旅程吧！👏\n安装 helm # 首先需要安装 helm 客户端\n$ curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get \u0026gt; get_helm.sh $ chmod 700 get_helm.sh $ ./get_helm.sh 创建 tiller 的 serviceaccount 和 clusterrolebinding\n$ kubectl create serviceaccount --namespace kube-system tiller $ kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller 然后安装 helm 服务端 tiller\n$ helm init 为应用程序设置 serviceAccount：\n$ kubectl patch deploy --namespace kube-system tiller-deploy -p \u0026#39;{\u0026#34;spec\u0026#34;:{\u0026#34;template\u0026#34;:{\u0026#34;spec\u0026#34;:{\u0026#34;serviceAccount\u0026#34;:\u0026#34;tiller\u0026#34;}}}}\u0026#39; 检查是否安装成功：\n$ kubectl -n kube-system get pods|grep tiller tiller-deploy-7bf964fff8-sklts 1/1 Running 0 7h $ helm version Client: \u0026amp;version.Version{SemVer:\u0026#34;v2.8.2\u0026#34;, GitCommit:\u0026#34;a80231648a1473929271764b920a8e346f6de844\u0026#34;, GitTreeState:\u0026#34;clean\u0026#34;} Server: \u0026amp;version.Version{SemVer:\u0026#34;v2.8.2\u0026#34;, GitCommit:\u0026#34;a80231648a1473929271764b920a8e346f6de844\u0026#34;, GitTreeState:\u0026#34;clean\u0026#34;} 部署 etcd # 下载 helm charts 仓库\n$ git clone https://github.com/kubernetes/charts.git 部署 etcd-operator（etcd-operator 会通过 kubernetes 的 CustomResourceDefinition 自动创建 etcd cluster）\n$ cd charts $ helm install --name etcd-operator stable/etcd-operator --set rbac.install=true,rbac.apiVersion=v1,customResources.createEtcdClusterCRD=true 检查是否部署成功\n$ kubectl get pods NAME READY STATUS RESTARTS AGE etcd-cluster-6skfqj9mwp 1/1 Running 0 7m etcd-cluster-6w8ntzvkwm 1/1 Running 0 8m etcd-cluster-mclzhqrldf 1/1 Running 0 7m etcd-operator-etcd-operator-etcd-backup-operator-5df985959bvvkw 1/1 Running 0 9m etcd-operator-etcd-operator-etcd-operator-58d98b95c-x44bz 1/1 Running 0 9m etcd-operator-etcd-operator-etcd-restore-operator-8688c7684nmdh 1/1 Running 0 9m $ kubectl get crd NAME AGE etcdbackups.etcd.database.coreos.com 1d etcdclusters.etcd.database.coreos.com 1d etcdrestores.etcd.database.coreos.com 1d $ kubectl get crd etcdclusters.etcd.database.coreos.com -o yaml apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: ... ... spec: group: etcd.database.coreos.com names: kind: EtcdCluster listKind: EtcdClusterList plural: etcdclusters shortNames: - etcd singular: etcdcluster scope: Namespaced version: v1beta2 ... ... $ kubectl get EtcdCluster NAME AGE etcd-cluster 11m $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE etcd-cluster ClusterIP None \u0026lt;none\u0026gt; 2379/TCP,2380/TCP 17m etcd-cluster-client ClusterIP 10.254.140.7 \u0026lt;none\u0026gt; 2379/TCP 17m etcd-restore-operator ClusterIP 10.254.177.113 \u0026lt;none\u0026gt; 19999/TCP 18m kubernetes ClusterIP 10.254.0.1 \u0026lt;none\u0026gt; 443/TCP 16d 部署成功后，可以在 host 集群内通过 http://etcd-cluster-client.default:2379 端点访问 etcd。\n部署 CoreDNS # 首先需要定制 CoreDNS chart 模板的默认配置，它会覆盖 CoreDNS chart 的默认配置参数。\n$ cat Value.yaml isClusterService: false serviceType: \u0026#34;NodePort\u0026#34; plugins: kubernetes: enabled: false etcd: enabled: true zones: - \u0026#34;example.com.\u0026#34; endpoint: \u0026#34;http://etcd-cluster-client.default:2379\u0026#34; 参数说明：\nisClusterService: 指定 CoreDNS 是否以集群服务的形式部署（默认为是）。 需要将其设置为 “false”，以使 CoreDNS 以 Kubernetes 应用服务的形式部署，否则会与集群的 dns 服务 kubedns 冲突。 serviceType: 指定为 CoreDNS 创建的 Kubernetes 服务类型。 选择 “NodePort”，以使得 CoreDNS 服务能够从 Kubernetes 集群外部访问。 plugins.kubernetes: 默认是启用的，通过将 plugins.kubernetes.enabled 设置为 “false” 来禁用 plugins.kubernetes。 通过将 plugins.etcd.enabled 设置为 “true” 来启用 plugins.etcd。 通过设置 plugins.etcd.zones 来配置 CoreDNS 被授权的 DNS 区域（联邦区域）。 通过设置 plugins.etcd.endpoint 来设置先前部署的 etcd 的端点。 现在运行以下命令来部署 CoreDNS：\n$ helm install --name coredns -f Values.yaml stable/coredns 验证部署：\n$ kubectl get pods -l app=coredns-coredns NAME READY STATUS RESTARTS AGE coredns-coredns-57b54ddb97-xffnc 1/1 Running 0 1d $ kubectl get svc -l app=coredns-coredns NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE coredns-coredns NodePort 10.254.198.211 \u0026lt;none\u0026gt; 53:27165/UDP,53:27165/TCP,9153:26492/TCP 1d 使用 CoreDNS 作为 DNS 提供商来部署 Federation # 可以使用 kubefed init 来部署联邦控制平面。 可以通过指定两个附加参数来选择 CoreDNS 作为 DNS 提供商。\n--dns-provider=coredns --dns-provider-config=coredns-provider.conf coredns-provider.conf 内容如下：\n[Global] etcd-endpoints = http://etcd-cluster-client.default:2379 zones = example.com. coredns-endpoints = \u0026lt;coredns-server-ip\u0026gt;:\u0026lt;port\u0026gt; etcd-endpoints 是访问 etcd 的端点。 zones 是 CoreDNS 被授权的联邦区域，其值与 kubefed init 的 –-dns-zone-name 参数相同。 coredns-endpoints 是访问 CoreDNS 服务器的端点。 这是一个 1.7 版本开始引入的可选参数。 CoreDNS 配置中的 plugins.etcd.zones 与 kubefed init 的 --dns-zone-name 参数应匹配。 给所有 node 打上 region 和 zone 的标签：\n$ kubectl label nodes 192.168.123.248 failure-domain.beta.kubernetes.io/zone=shanghai failure-domain.beta.kubernetes.io/region=yangpu $ kubectl label nodes 192.168.123.249 failure-domain.beta.kubernetes.io/zone=shanghai failure-domain.beta.kubernetes.io/region=yangpu $ kubectl label nodes 192.168.123.250 failure-domain.beta.kubernetes.io/zone=shanghai failure-domain.beta.kubernetes.io/region=yangpu 通过本条命令初始化 federation 控制平面，参数如下：\n$ kubefed init federation \\ # 联邦的名字 --host-cluster-context=kubernetes \\ # 主集群的context名字 --dns-provider=coredns \\ # DNS服务提供商 --dns-zone-name=\u0026#34;example.com.\u0026#34; \\ # 前面注册好的域名，必须以.结束 --dns-provider-config=\u0026#34;coredns-provider.conf\u0026#34; \\ # coredns 配置文件 --api-server-service-type=\u0026#34;NodePort\u0026#34; \\ --api-server-advertise-address=\u0026#34;192.168.123.250\u0026#34; Creating a namespace federation-system for federation system components... done Creating federation control plane service..... done Creating federation control plane objects (credentials, persistent volume claim)... done Creating federation component deployments... done Updating kubeconfig... done Waiting for federation control plane to come up..................................................................................................................................................... done Federation API server is running at: 10.110.151.216 观察以上输出信息，该命令做了以下几件事情：\n创建一个 namespace federation-system\n$ kubectl get ns NAME STATUS AGE default Active 8d federation-system Active 8s kube-public Active 8d kube-system Active 8d my-namespace Active 7d 创建两个服务 federation-apiserver 和 federation-controller-manager\n$ kubectl -n federation-system get pods NAME READY STATUS RESTARTS AGE federation-apiserver-909415585-wktmw 1/1 Running 0 2s federation-controller-manager-4247980660-c8ls5 1/1 Running 1 3s 创建一个 ServiceAccount federation-controller-manager\n$ kubectl -n federation-system get sa NAME SECRETS AGE default 1 31m federation-controller-manager 1 31m 创建一个 Role federation-system:federation-controller-manager\n$ kubectl -n federation-system get role NAME AGE federation-system:federation-controller-manager 38m 创建一个 RoleBinding federation-system:federation-controller-manager\n$ kubectl -n federation-system get rolebinding NAME AGE federation-system:federation-controller-manager 39m 创建一个 context federation\n$ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE federation federation federation * kubernetes kubernetes admin 默认情况下，kubefed init 通过动态创建 PV 的方式为 etcd 创建持久化存储。如果 kubernetes 集群不支持动态创建 PV，则可以预先创建 PV，注意 PV 要匹配 kubefed 的 PVC。或者使用 hostpath，同时指定调度节点。 添加集群至 federation # 目前为止您已经成功的初始化好了 Federation 的控制平面。接下来需要将各个子集群加入到 Federation 集群中。\n添加集群 kubernetes：\n$ kubefed join kubernetes \\ #加入联邦的集群命名名字 --context=federation \\ #联邦的context --cluster-context=kubernetes \\ #要添加集群的context --host-cluster-context=kubernetes #主集群的context $ kubectl --context=federation get cluster NAME STATUS AGE kubernetes Ready 6d 通过我的观察，以上过程在 加入 federation 的 kubernetes 集群中 做了以下几件事情：\n在 namespace federation-system 中创建一个 ServiceAccount kubernetes-kubernetes\n$ kubectl -n federation-system get sa NAME SECRETS AGE default 1 45m federation-controller-manager 1 45m kubernetes-kubernetes 1 8m 创建一个 ClusterRole federation-controller-manager:federation-kubernetes-kubernetes\n$ kubectl get clusterrole|egrep \u0026#34;NAME|federation\u0026#34; NAME AGE federation-controller-manager:federation-kubernetes-kubernetes 10m 创建一个 ClusterRoleBinding federation-controller-manager:federation-kubernetes-kubernetes\n$ kubectl get clusterrolebinding|egrep \u0026#34;NAME|federation\u0026#34; NAME AGE federation-controller-manager:federation-kubernetes-kubernetes 11m 整个过程是否还进行了其他操作，暂时没有发现，有待继续研究。\n介绍下集群查询，移除集群，删除联邦等命令 :\n查询注册到 Federation 的 kubernetes 集群列表\n$ kubectl --context=federation get clusters NAME STATUS AGE kubernetes Ready 8m 移除 kubernetes 集群\n$ kubefed unjoin kubernetes --host-cluster-context=kubernetes --context=federation Successfully removed cluster \u0026#34;kubernetes\u0026#34; from federation $ kubectl --context=federation get clusters No resources found. 集群联邦控制平面的删除功能还在开发中，目前可以通过删除 namespace federation-system 的方法来清理（注意pv不会删除）。命令在 host-cluster-context 上执行。\n$ kubectl delete ns federation-system Federation 支持的服务 # 集群联邦支持以下联邦资源，这些资源会自动在所有注册的 kubernetes 集群中创建。\nFederated ConfigMap Federated Service Federated DaemonSet Federated Deployment Federated Ingress Federated Namespaces Federated ReplicaSets Federated Secrets Federated Events（仅存在federation控制平面） Federated Jobs（v1.8+） Federated Horizontal Pod Autoscaling (HPA，v1.8+) 示例：\n创建 deployment\n$ cat nginx-deployment.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: nginx-deployment spec: replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 $ kubectl --context=federation create ns default $ kubectl --context=federation create -f nginx-deployment.yaml 可以通过 kubectl scale deploy nginx --replicas=3 --context=federation 来扩展 nginx 副本，然后观察 nginx 应用在各个子集群中的分布情况。\n$ kubectl --context=kubernetes get deploy 通过 Federated Service 来实现跨集群服务发现\n$ cat nginx-svc.yaml apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: type: LoadBalancer ports: - port: 80 targetPort: 80 protocol: TCP name: http selector: app: nginx # 这会在所有注册到联邦的 kubernetes 集群中创建服务 $ kubectl --context=federation create -f nginx-svc.yaml # 查看服务状态 $ kubectl --context=federation describe services nginx Name: nginx Namespace: default Labels: app=nginx Annotations: federation.kubernetes.io/service-ingresses={\u0026#34;items\u0026#34;:[{\u0026#34;cluster\u0026#34;:\u0026#34;kubernetes\u0026#34;,\u0026#34;items\u0026#34;:[{\u0026#34;ip\u0026#34;:\u0026#34;192.168.1.58\u0026#34;}]}]} Selector: app=nginx Type: LoadBalancer IP: LoadBalancer Ingress: 192.168.1.58 Port: http 80/TCP TargetPort: 80/TCP Endpoints: \u0026lt;none\u0026gt; Session Affinity: None External Traffic Policy: Cluster Events: \u0026lt;none\u0026gt; 可以通过 DNS 来访问联邦服务，访问格式包括以下几种：\n\u0026lt;service name\u0026gt;.\u0026lt;namespace\u0026gt;.\u0026lt;federation\u0026gt; \u0026lt;service name\u0026gt;.\u0026lt;namespace\u0026gt;.\u0026lt;federation\u0026gt;.svc.\u0026lt;domain\u0026gt; \u0026lt;service name\u0026gt;.\u0026lt;namespace\u0026gt;.\u0026lt;federation\u0026gt;.svc.\u0026lt;region\u0026gt;.\u0026lt;domain\u0026gt; \u0026lt;service name\u0026gt;.\u0026lt;namespace\u0026gt;.\u0026lt;federation\u0026gt;.svc.\u0026lt;zone\u0026gt;.\u0026lt;region\u0026gt;.\u0026lt;domain\u0026gt; 本例中可以通过以下几个域名来访问：\nnginx.default.federation nginx.default.federation.svc.example.com nginx.default.federation.svc.shanghai.example.com nginx.default.federation.svc.shanghai.yangpu.example.com DNS 在 etcd 下的存储路径为：/skydns\n$ kubectl exec etcd-cluster-fznzsrttt9 etcdctl ls /skydns/com/example/ /skydns/com/example/kubernetes /skydns/com/example/svc /skydns/com/example/yangpu $ kubectl exec etcd-cluster-fznzsrttt9 etcdctl ls /skydns/com/example/yangpu/ /skydns/com/example/yangpu/shanghai /skydns/com/example/yangpu/svc 参考文档 # Kubernetes federation Set up CoreDNS as DNS provider for Cluster Federation ","date":"2018年3月22日","externalUrl":null,"permalink":"/posts/federation/","section":"博客","summary":"在云计算环境中，服务的作用距离范围从近到远一般可以有： 同主机","title":"Kubernetes 使用集群联邦实现多集群管理","type":"posts"},{"content":" Kubernetes 中服务暴露的方式 # k8s 的服务暴露分为以下几种情况：\nhostNetwork hostPort NodePort LoadBalancer Ingress 说是暴露 Pod 其实跟暴露 Service 是一回事，因为 Pod 就是 Service 的 backend。\nHostNetwork # 这是一种直接定义 Pod 网络的方式。\n如果在 Pod 中使用 hostNotwork:true 配置的话，在这种 pod 中运行的应用程序可以直接看到 pod 启动的主机的网络接口。在主机的所有网络接口上都可以访问到该应用程序。以下是使用主机网络的 pod 的示例定义：\napiVersion: v1 kind: Pod metadata: name: influxdb spec: hostNetwork: true containers: - name: influxdb image: influxdb 这种 Pod 的网络模式有一个用处就是可以将网络插件包装在 Pod 中然后部署在每个宿主机上，这样该 Pod 就可以控制该宿主机上的所有网络。\n缺点：每次启动这个Pod的时候都可能被调度到不同的节点上，所有外部访问Pod的IP也是变化的，而且调度Pod的时候还需要考虑是否与宿主机上的端口冲突，因此一般情况下除非您知道需要某个特定应用占用特定宿主机上的特定端口时才使用 hostNetwork: true 的方式。\nhostPort # 这是一种直接定义 Pod 网络的方式。\nhostPort 是直接将容器的端口与所调度的节点上的端口路由，这样用户就可以通过宿主机的 IP 加上来访问 Pod 了，如:\napiVersion: v1 kind: Pod metadata: name: influxdb spec: containers: - name: influxdb image: influxdb ports: - containerPort: 8086 hostPort: 8086 缺点：因为 Pod 重新调度的时候该Pod被调度到的宿主机可能会变动，这样就变化了，用户必须自己维护一个 Pod 与所在宿主机的对应关系。\nNodePort # NodePort 在 kubenretes 里是一个广泛应用的服务暴露方式。Kubernetes 中的 service 默认情况下都是使用的 ClusterIP 这种类型，这样的 service 会产生一个 ClusterIP，这个 IP 只能在集群内部访问，要想让外部能够直接访问 service，需要将 service type 修改为 nodePort。\napiVersion: v1 kind: Pod metadata: name: influxdb labels: name: influxdb spec: containers: - name: influxdb image: influxdb ports: - containerPort: 8086 同时还可以给 service 指定一个 nodePort 值，范围是 30000-32767，这个值在 API server 的配置文件中，用\u0026ndash; service-node-port-range 定义。\nkind: Service apiVersion: v1 metadata: name: influxdb spec: type: NodePort ports: - port: 8086 nodePort: 30000 selector: name: influxdb 集群外就可以使用 kubernetes 任意一个节点的 IP 加上 30000 端口访问该服务了。kube-proxy 会自动将流量以 round-robin 的方式转发给该 service 的每一个 pod。\n缺点：所有 node 上都会开启端口监听，且需要记住端口号。\nLoadBalancer # LoadBalancer 只能在 service 上定义。这是公有云提供的负载均衡器，如 AWS、Azure、CloudStack、GCE 等。\nkind: Service apiVersion: v1 metadata: name: influxdb spec: type: LoadBalancer ports: - port: 8086 selector: name: influxdb 查看服务：\n$ kubectl get svc influxdb NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE influxdb 10.97.121.42 10.13.242.236 8086:30051/TCP 39s 内部可以使用 ClusterIP 加端口来访问服务，如 19.97.121.42:8086。\n外部可以用以下两种方式访问该服务：\n使用任一节点的 IP 加 30051 端口访问该服务 使用 EXTERNAL-IP 来访问，这是一个 VIP，是云供应商提供的负载均衡器 IP，如 10.13.242.236:8086。 缺点：需要云服务商支持。\nIngress # Ingress 是自 kubernetes1.1 版本后引入的资源类型。必须要部署 Ingress controller 才能创建 Ingress 资源，Ingress controller 是以一种插件的形式提供。Ingress controller 是部署在 Kubernetes 之上的 Docker 容器。它的 Docker 镜像包含一个像 nginx 或 HAProxy 的负载均衡器和一个控制器守护进程。控制器守护程序从 Kubernetes 接收所需的 Ingress 配置。它会生成一个 nginx 或 HAProxy 配置文件，并重新启动负载平衡器进程以使更改生效。换句话说，Ingress controller 是由 Kubernetes 管理的负载均衡器。\nKubernetes Ingress 提供了负载平衡器的典型特性：HTTP 路由，粘性会话，SSL 终止，SSL 直通，TCP 和 UDP 负载平衡等。目前并不是所有的 Ingress controller 都实现了这些功能，需要查看具体的 Ingress controller 文档。\napiVersion: extensions/v1beta1 kind: Ingress metadata: name: influxdb spec: rules: - host: influxdb.kube.example.com http: paths: - backend: serviceName: influxdb servicePort: 8086 外部访问 URL http://influxdb.kube.example.com/ping 访问该服务，入口就是 80 端口，然后 Ingress controller 直接将流量转发给后端 Pod，不需再经过 kube-proxy 的转发，比 LoadBalancer 方式更高效。\n缺点：80 端口暴露 必需通过域名引入，而且一次只能一条规则，很麻烦。\n但是在正常的虚拟机环境下，我们只需要一个 IP 地址+端口 即可访问服务。\n为什么我们不能做到像访问虚拟机一样直接访问 k8s 集群服务呢？当然可以，以下架构可以实现：\n打通 k8s 网络和物理网络直通 物理网络的 dns 域名服务直接调用 k8s-dns 域名服务直接互访 集群环境 # 架构环境 # k8s 集群网络：172.28.0.0/16 k8s-service 网络：10.96.0.0/12 物理机网络：192.168.0.0/16 k8s 集群节点 # $ kubectl get cs NAME STATUS MESSAGE ERROR scheduler Healthy ok controller-manager Healthy ok etcd-0 Healthy {\u0026#34;health\u0026#34;: \u0026#34;true\u0026#34;} $ kubectl get nodes -owide NAME STATUS AGE VERSION EXTERNAL-IP OS-IMAGE KERNEL-VERSION node1 Ready 13d v1.7.11 \u0026lt;none\u0026gt; CentOS Linux 7 (Core) 3.10.0-514.el7.x86_64 node2 Ready 13d v1.7.11 \u0026lt;none\u0026gt; CentOS Linux 7 (Core) 3.10.0-514.el7.x86_64 node3 Ready 13d v1.7.11 \u0026lt;none\u0026gt; CentOS Linux 7 (Core) 3.10.0-514.el7.x86_64 角色定义：\n角色名称 IP 地址 主机名 边界网关路由器 192.168.2.173 calico-gateway 边界 dns 代理服务器 192.168.1.62 node3 假设我们要访问 k8s 中的 dao-2048 服务：\n$ kubectl get svc|egrep \u0026#39;NAME|2048\u0026#39; NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE dao-2048 10.98.217.155 \u0026lt;none\u0026gt; 80/TCP 13m 该方案的架构原理如下：\n+-----------------+ | | | 192.168.0.0/16 | # 物理网络以域名或tcp方式发起访问k8s service以及端口 | | +-----------------+ | | +------------------------------------+ | dao-2048.default.svc.cluster.local | # 请求k8s服务所在空间的服务名，完整域名 +------------------------------------+ | | +-----------------+ | | # dns代理服务以ingress-udp pod的模式运行在此节点udp53号端口上， | 192.168.1.62 | # 为物理网络提供仿问k8s-dns的桥梁解析dns | | # 此节点应固定做为一个节点布署，所有外部机器设置dns为此 192.168.1.62 +-----------------+ | | +-----------------+ | | | 10.98.217.155 | # 获取 svc 的实际 clusterip | | +-----------------+ | | +-----------------+ # 边界网关,用于物理网络连接k8s集群，需要开启内核转发：net.ipv4.ip_forward=1 | | # 所有外部物理机加一条静态路由：访问 k8s 网络 10.96.0.0/12 网段必需经过网关 192.168.2.173 | 192.168.2.173 | # ip route add 10.96.0.0/12 via 192.168.2.173 | | # 边界网关运行 kube-proxy 用于防火墙规则同步实现 svc 分流，此节点不运行 kubele 服务，不受 k8s 管控 +-----------------+ | | +-------------------+ | | | calico-Iface接口 | | | +-------------------+ | | +-----------------+ | k8s 集群网络 | # 流量最终到达 k8s 集群 +-----------------+ 以下为该方案的实施步骤。\n部署边界 dns 代理服务器 # 布署 dns 代理服务节点为外部提供 dns 服务,以 hostNetwork: true 为非 k8s 集群网络物理机节点提供 dns 服务\n$ cd ~/dns-udp; ll ./ total 20K -rw-r--r--. 1 root root 1.2K Feb 12 05:13 default-backend.yaml -rw-r--r--. 1 root root 140 Feb 12 05:14 nginx-udp-ingress-configmap.yaml -rw-r--r--. 1 root root 1.8K Feb 12 05:35 nginx-udp-ingress-controller.yaml -rw-r--r--. 1 root root 2.4K Feb 12 05:15 rbac.yaml $ cat default-backend.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: default-http-backend labels: app: default-http-backend namespace: kube-system spec: replicas: 1 selector: matchLabels: app: default-http-backend template: metadata: labels: app: default-http-backend spec: terminationGracePeriodSeconds: 60 containers: - name: default-http-backend # Any image is permissible as long as: # It serves a 404 page at / # It serves 200 on a /healthz endpoint image: gcr.io/google_containers/defaultbackend:1.4 livenessProbe: httpGet: path: /healthz port: 8080 scheme: HTTP initialDelaySeconds: 30 timeoutSeconds: 5 ports: - containerPort: 8080 resources: limits: cpu: 10m memory: 20Mi requests: cpu: 10m memory: 20Mi --- apiVersion: v1 kind: Service metadata: name: default-http-backend namespace: kube-system labels: app: default-http-backend spec: ports: - port: 80 targetPort: 8080 selector: app: default-http-backend $ cat nginx-udp-ingress-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: nginx-udp-ingress-configmap namespace: kube-system data: 53: \u0026#34;kube-system/kube-dns:53\u0026#34; $ cat nginx-udp-ingress-controller.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: nginx-udp-ingress-controller labels: k8s-app: nginx-udp-ingress-lb namespace: kube-system spec: replicas: 1 selector: matchLabels: k8s-app: nginx-udp-ingress-lb template: metadata: labels: k8s-app: nginx-udp-ingress-lb name: nginx-udp-ingress-lb spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node3 hostNetwork: true serviceAccountName: nginx-ingress-serviceaccount terminationGracePeriodSeconds: 60 containers: - image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.10.2 name: nginx-udp-ingress-lb readinessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP livenessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 1 env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace ports: - containerPort: 80 hostPort: 80 - containerPort: 443 hostPort: 443 - containerPort: 53 hostPort: 53 args: - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --udp-services-configmap=$(POD_NAMESPACE)/nginx-udp-ingress-configmap $ cat rbac.yaml apiVersion: v1 kind: ServiceAccount metadata: name: nginx-ingress-serviceaccount namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: nginx-ingress-clusterrole rules: - apiGroups: - \u0026#34;\u0026#34; resources: - configmaps - endpoints - nodes - pods - secrets verbs: - list - watch - apiGroups: - \u0026#34;\u0026#34; resources: - nodes verbs: - get - apiGroups: - \u0026#34;\u0026#34; resources: - services verbs: - get - list - watch - apiGroups: - \u0026#34;extensions\u0026#34; resources: - ingresses verbs: - get - list - watch - apiGroups: - \u0026#34;\u0026#34; resources: - events verbs: - create - patch - apiGroups: - \u0026#34;extensions\u0026#34; resources: - ingresses/status verbs: - update --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: Role metadata: name: nginx-ingress-role namespace: kube-system rules: - apiGroups: - \u0026#34;\u0026#34; resources: - configmaps - pods - secrets - namespaces verbs: - get - apiGroups: - \u0026#34;\u0026#34; resources: - configmaps resourceNames: # Defaults to \u0026#34;\u0026lt;election-id\u0026gt;-\u0026lt;ingress-class\u0026gt;\u0026#34; # Here: \u0026#34;\u0026lt;ingress-controller-leader\u0026gt;-\u0026lt;nginx\u0026gt;\u0026#34; # This has to be adapted if you change either parameter # when launching the nginx-ingress-controller. - \u0026#34;ingress-controller-leader-nginx\u0026#34; verbs: - get - update - apiGroups: - \u0026#34;\u0026#34; resources: - configmaps verbs: - create - apiGroups: - \u0026#34;\u0026#34; resources: - endpoints verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: RoleBinding metadata: name: nginx-ingress-role-nisa-binding namespace: kube-system roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: nginx-ingress-role subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: nginx-ingress-clusterrole-nisa-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: nginx-ingress-clusterrole subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: kube-system $ kubectl create -f ./ deployment \u0026#34;default-http-backend\u0026#34; created service \u0026#34;default-http-backend\u0026#34; created configmap \u0026#34;nginx-udp-ingress-configmap\u0026#34; created deployment \u0026#34;nginx-udp-ingress-controller\u0026#34; created 通过 nginx 反向代理 kube-dns 服务，同时以 hostNetwork: true 向集群外部暴露 53 端口，为非 k8s 集群网络物理机节点提供 dns 服务。\n部署 gateway 边界网关节点 # 此节点只运行 calico 和 kube-proxy。\n首先开启内核转发 # $ echo \u0026#39;net.ipv4.ip_forward=1\u0026#39; \u0026gt;\u0026gt;/etc/sysctl.conf $ sysctl -p 运行 calico # $ docker run --net=host --privileged --name=calico-node -d --restart=always \\ -v /etc/etcd/ssl:/etc/kubernetes/ssl \\ -e ETCD_ENDPOINTS=https://192.168.1.60:12379 \\ -e ETCD_KEY_FILE=/etc/kubernetes/ssl/peer-key.pem \\ -e ETCD_CERT_FILE=/etc/kubernetes/ssl/peer-cert.pem \\ -e ETCD_CA_CERT_FILE=/etc/kubernetes/ssl/ca.pem \\ -e NODENAME=${HOSTNAME} \\ -e IP= \\ -e CALICO_IPV4POOL_CIDR=172.28.0.0/16 \\ -e NO_DEFAULT_POOLS= \\ -e AS= \\ -e CALICO_LIBNETWORK_ENABLED=true \\ -e IP6= \\ -e CALICO_NETWORKING_BACKEND=bird \\ -e FELIX_DEFAULTENDPOINTTOHOSTACTION=ACCEPT \\ -v /var/run/calico:/var/run/calico \\ -v /lib/modules:/lib/modules \\ -v /run/docker/plugins:/run/docker/plugins \\ -v /var/run/docker.sock:/var/run/docker.sock \\ -v /var/log/calico:/var/log/calico \\ calico/node:v2.6.7 需要提前将相关证书拷贝到 /etc/kubernetes/ssl/ 目录下。\n注意：此处的 -e CALICO_IPV4POOL_CIDR=172.28.0.0/16 要与 k8s 集群网络的网段一致\n创建边界路由器 # 以下命令在 k8s 的 master 节点上进行操作\n$ cat bgpPeer.yaml apiVersion: v1 kind: bgpPeer metadata: peerIP: 192.168.2.173 scope: global spec: asNumber: 64512 $ calicoctl create -f bgpPeer.yaml 查看 node 情况\n$ calicoctl node status Calico process is running. IPv4 BGP status +---------------+-------------------+-------+------------+-------------+ | PEER ADDRESS | PEER TYPE | STATE | SINCE | INFO | +---------------+-------------------+-------+------------+-------------+ | 192.168.1.61 | node-to-node mesh | up | 2018-01-29 | Established | | 192.168.1.62 | node-to-node mesh | up | 2018-02-09 | Established | | 192.168.2.173 | node-to-node mesh | up | 2018-02-09 | Established | | 192.168.2.173 | global | start | 2018-02-09 | Idle | +---------------+-------------------+-------+------------+-------------+ IPv6 BGP status No IPv6 peers found. 查看全局对等体节点\n$ calicoctl get bgpPeer --scope=global SCOPE PEERIP NODE ASN global 192.168.2.173 64512 部署 kube-proxy # 安装 conntrack # $ yum install -y conntrack-tools 创建 kube-proxy 的 service 配置文件 # 文件路径 /usr/lib/systemd/system/kube-proxy.service。\n[Unit] Description=Kubernetes Kube-Proxy Server Documentation=https://github.com/GoogleCloudPlatform/kubernetes After=network.target [Service] EnvironmentFile=-/etc/kubernetes/config EnvironmentFile=-/etc/kubernetes/proxy ExecStart=/usr/local/bin/kube-proxy \\ --logtostderr=true \\ --v=2 \\ --bind-address=192.168.2.173 \\ --hostname-override=192.168.2.173 \\ --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig \\ --proxy-mode=iptables \\ --cluster-cidr=172.28.0.0/16 Restart=on-failure LimitNOFILE=65536 [Install] WantedBy=multi-user.target 需要提前将 kube-proxy.kubeconfig 文件拷贝到 /etc/kubernetes/ 目录下。\n启动 kube-proxy # $ systemctl daemon-reload $ systemctl enable kube-proxy $ systemctl start kube-proxy 测试网关和 dns 解析以及服务访问情况 # 找台集群外的机器来验证，这台机器只有一个网卡，没有安装 calico。\n添加路由 $ ip route add 10.96.0.0/12 via 192.168.2.173 dev ens160 修改 dns 为 192.168.1.62 $ cat /etc/resolv.conf nameserver 192.168.1.62 search default.svc.cluster.local svc.cluster.local cluster.local nameserver 223.5.5.5 解析 dao-2048 服务的域名 $ dig dao-2048.default.svc.cluster.local ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.9.4-RedHat-9.9.4-51.el7_4.2 \u0026lt;\u0026lt;\u0026gt;\u0026gt; dao-2048.default.svc.cluster.local ;; global options: +cmd ;; Got answer: ;; -\u0026gt;\u0026gt;HEADER\u0026lt;\u0026lt;- opcode: QUERY, status: NOERROR, id: 57053 ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;dao-2048.default.svc.cluster.local. IN A ;; ANSWER SECTION: dao-2048.default.svc.cluster.local. 5 IN A 10.98.217.155 ;; Query time: 1 msec ;; SERVER: 192.168.1.62#53(192.168.1.62) ;; WHEN: Mon Feb 12 06:46:38 EST 2018 ;; MSG SIZE rcvd: 79 访问 dao-2048 服务 $ curl dao-2048.default.svc.cluster.local * About to connect() to dao-2048.default.svc.cluster.local port 80 (#0) * Trying 10.98.217.155... * Connected to dao-2048.default.svc.cluster.local (10.98.217.155) port 80 (#0) \u0026gt; GET / HTTP/1.1 \u0026gt; User-Agent: curl/7.29.0 \u0026gt; Host: dao-2048.default.svc.cluster.local \u0026gt; Accept: */* \u0026gt; \u0026lt; HTTP/1.1 200 OK \u0026lt; Server: nginx/1.10.1 \u0026lt; Date: Mon, 12 Feb 2018 11:47:58 GMT \u0026lt; Content-Type: text/html \u0026lt; Content-Length: 4085 \u0026lt; Last-Modified: Sun, 11 Feb 2018 11:31:27 GMT \u0026lt; Connection: keep-alive \u0026lt; ETag: \u0026#34;5a80298f-ff5\u0026#34; \u0026lt; Accept-Ranges: bytes \u0026lt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;2048\u0026lt;/title\u0026gt; ......... 成功访问！\n如果是 windows 用户，添加路由可以用管理员打开 cmd 命令运行：\n$ route ADD -p 172.28.0.0 MASK 255.255.0.0 192.168.2.173 PS：如果你不想一台台机器加路由和 dns，你可以把路由信息加入物理路由器上，这样就不用每台机都加路由和 dns 了，直接打通所有链路。\n参考 # k8s-dns-gateway 网关网络扩展实战\n","date":"2018年2月11日","externalUrl":null,"permalink":"/posts/k8s-network-expand/","section":"博客","summary":"Kubernetes 中服务暴露的方式 # k8s 的服务暴露分为以下几种情况： hostNetwork hostPort NodePort LoadBalancer Ingress","title":"Kubernetes 网络扩展","type":"posts"},{"content":" 名词解释 # endpoint：接入到网络中的设备称为 endpoint ❤️\nAS：网络自治系统，一个完全自治的网络，通过 BGP 协议与其它 AS 交换路由信息\nibgp：AS 内部的 BGP Speaker，与同一个 AS 内部的 ibgp、ebgp 交换路由信息\nebgp：AS 边界的 BGP Speaker，与同一个 AS 内部的 ibgp、其它 AS 的 ebgp 交换路由信息\nworkloadEndpoint：Calico 网络中的分配虚拟机、容器使用的 endpoint\nhostEndpoints：Calico 网络中的物理机(node)的地址\n组网原理 # Calico 组网的核心原理就是IP路由，每个容器或者虚拟机会分配一个 workload-endpoint(wl)。\n从 nodeA 上的容器 A 内访问 nodeB 上的容器 B 时:\n+--------------------+ +--------------------+ | +------------+ | | +------------+ | | | | | | | | | | | ConA | | | | ConB | | | | | | | | | | | +-----+------+ | | +-----+------+ | | | | | | | | wl-A | | wl-B | | | | | | | +-------node-A-------+ +-------node-B-------+ | | | | | | type1. in the same lan | | | +-------------------------------+ | | | | type2. in different network | | +-------------+ | | | | | +-------------+ Routers |-------------+ | | +-------------+ 从 ConA 中发送给 ConB 的报文被 nodeA 的 wl-A 接收，根据 nodeA 上的路由规则，经过各种 iptables 规则后，转发到 nodeB。 如果 nodeA 和 nodeB 在同一个二层网段，下一条地址直接就是 node-B，经过二层交换机即可到达。 如果 nodeA 和 nodeB 在不同的网段，报文被路由到下一跳，经过三层交换或路由器，一步步跳转到 node-B。 核心问题是，nodeA 怎样得知下一跳的地址？答案是 node 之间通过 BGP 协议交换路由信息。\n每个 node 上运行一个软路由软件 bird，并且被设置成 BGP Speaker，与其它 node 通过 BGP 协议交换路由信息。\n可以简单理解为，每一个 node 都会向其它 node 通知这样的信息:\n我是X.X.X.X，某个IP或者网段在我这里，它们的下一跳地址是我。\n通过这种方式每个 node 知晓了每个 workload-endpoint 的下一跳地址。\nBGP 与 AS # BGP 是路由器之间的通信协议，主要用于 AS（Autonomous System,自治系统）之间的互联。\nAS，自治系统，是一个自治的网络，拥有独立的交换机、路由器等，可以独立运转。\n每个 AS 拥有一个全球统一分配的 16 位的 ID 号，其中 64512 到 65535 共 1023 个 AS 号码被预留用于本地或者私用。\n# calico默认使用的AS号是64512，可以修改： # 查看 $ calicoctl config get asNumber # 设置 $ calicoctl config set asNumber 64512 AS 内部有多个 BGP speaker，分为 ibgp、ebgp，ebgp 还与其它的 AS 中的 ebgp 建立 BGP 连接。\nAS 内部的 BGP speaker 通过 BGP 协议交换路由信息，最终每一个 BGP speaker 拥有整个 AS 的路由信息。\nBGP speaker 一般是网络中的物理路由器，calico 将 node 改造成了一个路由器（软件bird)，node 上的虚拟机、容器等就是接入这个路由器的设备。\nAS 内部的 BGP Speaker 之间有两种互联方式:\n全互联模式模式 Router reflection(RR) 模式 BGP Speaker全互联模式 # 全互联模式，就是一个 BGP Speaker 需要与其它所有的 BGP Speaker 建立 bgp 连接（形成一个bgp mesh）。\n网络中 bgp 总连接数是按照 O(n^2) 增长的，有太多的 BGP Speaker 时，会消耗大量的连接。\nCalico 默认使用全互联的方式，扩展性比较差，只能支持小规模集群，可以打开/关闭全互联模式：\n$ calicoctl config set nodeTonodeMesh off $ calicoctl config set nodeTonodeMesh on BGP Speaker RR 模式 # RR模式，就是在网络中指定一个或多个 BGP Speaker 作为 反射路由（Router Reflector），RR 与所有的 BGP Speaker 建立 bgp 连接。\n每个 BGP Speaker 只需要与 RR 交换路由信息，就可以得到全网路由信息。\nRR 必须与所有的 BGP Speaker 建立 BGP 连接，以保证能够得到全网路由信息。\n在 Calico 中可以通过 Global Peer 实现 RR 模式。\nGlobal Peer 是一个 BGP Speaker ，需要手动在 Calico 中创建，所有的 node 都会与 Global peer 建立 BGP 连接。\n关闭了全互联模式后，再将 RR 作为 Global Peers 添加到 Calico 中，Calico 网络就切换到了 RR 模式，可以支撑容纳更多的 node。\nRR 模式部署 # 集群环境：\nIP 主机名 10.10.31.190 kube-master 10.10.31.193 kube-node1 10.10.31.194 kube-node2 10.10.31.168 node1 在 node1 节点上启动反射路由实例 # $ docker run --privileged --net=host -d \\ -e IP=\u0026lt;IPv4_RR\u0026gt; \\ [-e IP6=\u0026lt;IPv6_RR\u0026gt;] \\ -e ETCD_ENDPOINTS=\u0026lt;https://ETCD_IP:PORT\u0026gt; \\ -v \u0026lt;FULL_PATH_TO_CERT_DIR\u0026gt;:\u0026lt;MOUNT_DIR\u0026gt; \\ -e ETCD_CA_CERT_FILE=\u0026lt;MOUNT_DIR\u0026gt;/\u0026lt;CA_FILE\u0026gt; \\ -e ETCD_CERT_FILE=\u0026lt;MOUNT_DIR\u0026gt;/\u0026lt;CERT_FILE\u0026gt; \\ -e ETCD_KEY_FILE=\u0026lt;MOUNT_DIR\u0026gt;/\u0026lt;KEY_FILE\u0026gt; \\ calico/routereflector:v0.4.0 # \u0026lt;FULL_PATH_TO_CERT_DIR\u0026gt; 是你的宿主机的 etcd 证书和秘钥的存放目录 配置反射路由的集群 # 反射路由关于 ipv4 的配置在 etcd 中的存储路径为：\n/calico/bgp/v1/rr_v4/\u0026lt;RR IPv4 address\u0026gt; ipv6 的配置在 etcd 中的存储路径为：\n/calico/bgp/v1/rr_v6/\u0026lt;RR IPv6 address\u0026gt; 数据格式为 json：\n{ \u0026#34;ip\u0026#34;: \u0026#34;\u0026lt;IP address of Route Reflector\u0026gt;\u0026#34;, \u0026#34;cluster_id\u0026#34;: \u0026#34;\u0026lt;Cluster ID for this RR (see notes)\u0026gt;\u0026#34; } 通过 curl 将该条目添加到 etcd 中\n# IPv4 entries $ curl --cacert \u0026lt;path_to_ca_cert\u0026gt; --cert \u0026lt;path_to_cert\u0026gt; --key \u0026lt;path_to_key\u0026gt; -L https://\u0026lt;ETCD_IP:PORT\u0026gt;:2379/v2/keys/calico/bgp/v1/rr_v4/\u0026lt;IPv4_RR\u0026gt; -XPUT -d value=\u0026#34;{\\\u0026#34;ip\\\u0026#34;:\\\u0026#34;\u0026lt;IPv4_RR\u0026gt;\\\u0026#34;,\\\u0026#34;cluster_id\\\u0026#34;:\\\u0026#34;\u0026lt;CLUSTER_ID\u0026gt;\\\u0026#34;}\u0026#34; # IPv6 entries $ curl --cacert \u0026lt;path_to_ca_cert\u0026gt; --cert \u0026lt;path_to_cert\u0026gt; --key \u0026lt;path_to_key\u0026gt; -L https://\u0026lt;ETCD_IP:PORT\u0026gt;:2379/v2/keys/calico/bgp/v1/rr_v6/\u0026lt;IPv6_RR\u0026gt; -XPUT -d value=\u0026#34;{\\\u0026#34;ip\\\u0026#34;:\\\u0026#34;\u0026lt;IPv6_RR\u0026gt;\\\u0026#34;,\\\u0026#34;cluster_id\\\u0026#34;:\\\u0026#34;\u0026lt;CLUSTER_ID\u0026gt;\\\u0026#34;}\u0026#34; 例如：\n$ curl --cacert \u0026lt;path_to_ca_cert\u0026gt; --cert \u0026lt;path_to_cert\u0026gt; --key \u0026lt;path_to_key\u0026gt; -L https://10.10.31.190:2379/v2/keys/calico/bgp/v1/rr_v4/10.10.31.168 -XPUT -d value=\u0026#34;{\\\u0026#34;ip\\\u0026#34;:\\\u0026#34;10.10.31.168\\\u0026#34;,\\\u0026#34;cluster_id\\\u0026#34;:\\\u0026#34;1.0.0.1\\\u0026#34;}\u0026#34; 配置 calico 使用反射路由 # 关闭全互联模式\n$ calicoctl config set nodeToNodeMesh off 确定你的网络的 AS 号码\n$ calicoctl get nodes --output=wide # 或者使用以下命令 $ calicoctl config get asNumber 将 RR 作为 Global Peers 添加到 Calico 中\n$ calicoctl create -f - \u0026lt;\u0026lt; EOF apiVersion: v1 kind: bgpPeer metadata: peerIP: \u0026lt;IP_RR\u0026gt; scope: global spec: asNumber: \u0026lt;AS_NUM\u0026gt; EOF # \u0026lt;IP_RR\u0026gt;：反射路由的 ipv4 或 ipv6 地址 # \u0026lt;AS_NUM\u0026gt;：网络的 AS 号码 由于 BGP 协议使用 TCP 179 端口进行通信，可以在 node1 上查看一下\n$ ss -tnp|grep 179 ESTAB 0 0 10.10.31.168:179 10.10.31.196:55967 users:((\u0026#34;bird\u0026#34;,pid=10601,fd=8)) ESTAB 0 0 10.10.31.168:56393 10.10.31.193:179 users:((\u0026#34;bird\u0026#34;,pid=10601,fd=9)) ESTAB 0 0 10.10.31.168:41164 10.10.31.194:179 users:((\u0026#34;bird\u0026#34;,pid=10601,fd=10)) 在 node1 上查看反射路由配置\n$ docker exec 52e584f5bcf3 cat /config/bird.cfg # Generated by confd router id 10.10.31.168; # Watch interface up/down events. protocol device { scan time 2; # Scan interfaces every 2 seconds } # Template for all BGP clients template bgp bgp_template { debug off; description \u0026#34;Connection to BGP peer\u0026#34;; multihop; import all; # Import all routes, since we don\u0026#39;t know what the upstream # topology is and therefore have to trust the ToR/RR. export all; # Export all. source address 10.10.31.168; # The local address we use for the TCP connection graceful restart; # See comment in kernel section about graceful restart. } # ------------- RR-to-RR full mesh ------------- # For RR 10.10.31.168 # Skipping ourselves # ------------- RR as a global peer ------------- # This RR is a global peer with *all* calico nodes. # Peering with Calico node node1 protocol bgp Global_10_10_31_193 from bgp_template { local as 64512; neighbor 10.10.31.193 as 64512; rr client; rr cluster id 1.0.0.1; } # Peering with Calico node node2 protocol bgp Global_10_10_31_194 from bgp_template { local as 64512; neighbor 10.10.31.194 as 64512; rr client; rr cluster id 1.0.0.1; } # Peering with Calico node node3 protocol bgp Global_10_10_31_196 from bgp_template { local as 64512; neighbor 10.10.31.196 as 64512; rr client; rr cluster id 1.0.0.1; } # ------------- RR as a node-specific peer ------------- 在 kube-node1 上查看 calico 的配置\n$ docker exec 7fcb072515d6 cat /etc/calico/confd/config/bird.cfg # Generated by confd include \u0026#34;bird_aggr.cfg\u0026#34;; include \u0026#34;custom_filters.cfg\u0026#34;; include \u0026#34;bird_ipam.cfg\u0026#34;; router id 10.10.31.193; # Configure synchronization between routing tables and kernel. protocol kernel { learn; # Learn all alien routes from the kernel persist; # Don\u0026#39;t remove routes on bird shutdown scan time 2; # Scan kernel routing table every 2 seconds import all; export filter calico_ipip; # Default is export none graceful restart; # Turn on graceful restart to reduce potential flaps in # routes when reloading BIRD configuration. With a full # automatic mesh, there is no way to prevent BGP from # flapping since multiple nodes update their BGP # configuration at the same time, GR is not guaranteed to # Watch interface up/down events. protocol device { debug { states }; scan time 2; # Scan interfaces every 2 seconds } protocol direct { debug { states }; interface -\u0026#34;cali*\u0026#34;, \u0026#34;*\u0026#34;; # Exclude cali* but include everything else. } # Template for all BGP clients template bgp bgp_template { debug { states }; description \u0026#34;Connection to BGP peer\u0026#34;; local as 64512; multihop; gateway recursive; # This should be the default, but just in case. import all; # Import all routes, since we don\u0026#39;t know what the upstream # topology is and therefore have to trust the ToR/RR. export filter calico_pools; # Only want to export routes for workloads. next hop self; # Disable next hop processing and always advertise our # local address as nexthop source address 10.10.31.193; # The local address we use for the TCP connection add paths on; graceful restart; # See comment in kernel section about graceful restart. } # ------------- Global peers ------------- # For peer /global/peer_v4/10.10.31.168 protocol bgp Global_10_10_31_168 from bgp_template { neighbor 10.10.31.168 as 64512; } # ------------- Node-specific peers ------------- 多 cluster ID 实例拓扑 # 当拓扑包含了多个反射路由时,BGP 利用集群 id 来保证分配路由时不陷入循环路由。\n反射路由镜像帮助每个反射路由提供固定的集群 id 而不是依赖单一平行原则进行配置,这简化了整个网络的配置,但也给拓扑带来了一些限制:\nThe Route Reflector image provided assumes that it has a fixed cluster ID for each Route Reflector rather than being configurable on a per peer basis.\nFor example, the topology outlined in the diagram below is based on the Top of Rack model:\nEach rack is assigned its own cluster ID (a unique number in IPv4 address format). Each node (server in the rack) peers with a redundant set of route reflectors specific to that rack. All of the ToR route reflectors form a full mesh with each other. For example, to set up the topology described above, you would:\nSpin up nodes N1 - N9 Spin up Route Reflectors RR1 - RR6 Add node specific peers, peering:\nN1, N2 and N3 with RR1 and RR2\nN4, N5 and N6 with RR3 and RR4\nN7, N8 and N9 with RR5 and RR6 Add etcd config for the Route Reflectors:\nRR1 and RR2 both using the cluster ID 1.0.0.1\nRR2 and RR3 both using the cluster ID 1.0.0.2\nRR4 and RR5 both using the cluster ID 1.0.0.3 ","date":"2018年2月1日","externalUrl":null,"permalink":"/posts/calico-rr/","section":"博客","summary":"名词解释 # endpoint：接入到网络中的设备称为 endpoint ❤️ AS：","title":"calico Router reflection(RR) 模式介绍及部署","type":"posts"},{"content":"","date":"2018年1月23日","externalUrl":null,"permalink":"/tags/iptables/","section":"标签","summary":"","title":"Iptables","type":"tags"},{"content":"","date":"2018年1月22日","externalUrl":null,"permalink":"/tags/device-mapper/","section":"标签","summary":"","title":"Device Mapper","type":"tags"},{"content":" 准备条件 # devicemapper 存储驱动是 RHEL, CentOS 和 Oracle Linux 系统上唯一一个支持 Docker EE 和 Commercially Supported Docker Engine (CS-Engine) 的存储驱动，具体参考 Product compatibility matrix.\ndevicemapper 在 CentOS, Fedora, Ubuntu 和 Debian 上也支持 Docker CE。\n如果你更改了 Docker 的存储驱动，那么你之前在本地创建的所有容器都将无法访问。\n配置Docker使用devicemapper # Docker 主机运行 devicemapper 存储驱动时，默认的配置模式为 loop-lvm。此模式使用空闲的文件来构建用于镜像和容器快照的精简存储池。该模式设计为无需额外配置开箱即用(out-of-the-box)。不过生产部署不应该以 loop-lvm 模式运行。\n2.1 生产环境配置direct-lvm模式 # CentOS7 从 Docker 17.06 开始支持通过 Docker 自动配置 direct-lvm，所以推荐使用该工具配置。当然也可以手动配置 lvm，添加相关配置选项，不过过程较为繁琐一点。\n自动配置 direct-lvm 模式 # 该方法只适用于一个块设备，如果你有多个块设备，请通过手动配置 direct-lvm 模式。\n示例配置文件位置 /usr/lib/docker-storage-setup/docker-storage-setup，可以查看其中相关配置的详细说明，或者通过 man docker-storage-setup 获取帮助，以下介绍几个关键的选项：\n参数 解释 是否必须 默认值 示例 dm.directlvm_device 准备配置 direct-lvm 的块设备的路径 是 dm.directlvm_device=\u0026quot;/dev/xvdf\u0026quot; dm.thinp_percent 定义创建 data thin pool 的大小 否 95 dm.thinp_percent=95 dm.thinp_metapercent 定义创建 metadata thin pool 的大小 否 1 dm.thinp_metapercent=1 dm.thinp_autoextend_threshold 定义自动扩容的百分比，100 表示 disable，最小为 50，参考 lvmthin — LVM thin provisioning 否 80 dm.thinp_autoextend_threshold=80 dm.thinp_autoextend_percent 定义每次扩容的大小，100 表示 disable 否 20 dm.thinp_autoextend_percent=20 dm.directlvm_device_force 当块设备已经存在文件系统时，是否格式化块设备 否 false dm.directlvm_device_force=true 编辑 /etc/docker/daemon.json，设置好参数后重新启动 Docker 使更改生效。下面是一个示例：\n{ \u0026#34;storage-driver\u0026#34;: \u0026#34;devicemapper\u0026#34;, \u0026#34;storage-opts\u0026#34;: [ \u0026#34;dm.directlvm_device=/dev/xdf\u0026#34;, \u0026#34;dm.thinp_percent=95\u0026#34;, \u0026#34;dm.thinp_metapercent=1\u0026#34;, \u0026#34;dm.thinp_autoextend_threshold=80\u0026#34;, \u0026#34;dm.thinp_autoextend_percent=20\u0026#34;, \u0026#34;dm.directlvm_device_force=false\u0026#34; ] } 关于存储的更多参数请参考：\nStable\nEdge\n手动配置 direct-lvm 模式 # 下面的步骤创建一个逻辑卷，配置用作存储池的后端。我们假设你有在 /dev/xvdf 的充足空闲空间的块设备。也假设你的 Docker daemon 已停止。\n1.登录你要配置的 Docker 主机并停止 Docker daemon。\n2.安装LVM2软件包。LVM2软件包含管理Linux上逻辑卷的用户空间工具集。\nRHEL / CentOS: device-mapper-persistent-data, lvm2 以及相关依赖 Ubuntu / Debian: thin-provisioning-tools, lvm2 以及相关依赖 3.创建物理卷。\n$ pvcreate /dev/xvdf Physical volume \u0026#34;/dev/xvdf\u0026#34; successfully created. 4.创建一个 “docker” 卷组。 $ vgcreate docker /dev/xvdf Volume group \u0026#34;docker\u0026#34; successfully created 5.创建一个名为thinpool的存储池。\n在此示例中，设置池大小为 “docker” 卷组大小的 95％。 其余的空闲空间可以用来自动扩展数据或元数据。\n$ lvcreate --wipesignatures y -n thinpool docker -l 95%VG Logical volume \u0026#34;thinpool\u0026#34; created. $ lvcreate --wipesignatures y -n thinpoolmeta docker -l 1%VG Logical volume \u0026#34;thinpoolmeta\u0026#34; created. 6.将存储池转换为 thinpool 格式。 $ lvconvert -y \\ --zero n \\ -c 512K \\ --thinpool docker/thinpool \\ --poolmetadata docker/thinpoolmeta WARNING: Converting logical volume docker/thinpool and docker/thinpoolmeta to thin pool\u0026#39;s data and metadata volumes with metadata wiping. THIS WILL DESTROY CONTENT OF LOGICAL VOLUME (filesystem etc.) Converted docker/thinpool to thin pool. 7.通过 lvm profile 配置存储池的自动扩展。 $ vi /etc/lvm/profile/docker-thinpool.profile 8.设置参数 thin_pool_autoextend_threshold 和 thin_pool_autoextend_percent 的值。 设置 thin_pool_autoextend_threshold 值。这个值应该是之前设置存储池余下空间的百分比(100 = disabled)。\nthin_pool_autoextend_threshold = 80 设置当存储池自动扩容时，增加存储池的空间百分比（100 =禁用）\nthin_pool_autoextend_percent = 20 检查你的 docker-thinpool.profile 的设置。一个示例 /etc/lvm/profile/docker-thinpool.profile 应该类似如下：\nactivation { thin_pool_autoextend_threshold=80 thin_pool_autoextend_percent=20 } 9.应用新的 lvm 配置。 $ lvchange --metadataprofile docker-thinpool docker/thinpool Logical volume docker/thinpool changed. 10.查看卷的信息，验证 lv 是否受监控。 $ lvs -o+seg_monitor LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert Monitor thinpool docker twi-a-t--- 95.00g 0.00 0.01 monitored 11.备份 Docker 存储。 $ mkdir /var/lib/docker.bk $ mv /var/lib/docker/* /var/lib/docker.bk 12.配置一些特定的 devicemapper 选项。 $ cat /etc/docker/daemon.json { \u0026#34;storage-driver\u0026#34;: \u0026#34;devicemapper\u0026#34;, \u0026#34;storage-opts\u0026#34;: [ \u0026#34;dm.thinpooldev=/dev/mapper/docker-thinpool\u0026#34;, \u0026#34;dm.use_deferred_removal=true\u0026#34;, \u0026#34;dm.use_deferred_deletion=true\u0026#34; ] } Note: Always set both dm.use_deferred_removal=true and dm.use_deferred_deletion=true to prevent unintentionally leaking mount points.\n启用上述2个参数来阻止可能意外产生的挂载点泄漏问题\n检查主机上的 devicemapper 结构 # 你可以使用 lsblk 命令来查看以上创建的设备文件和存储池。\n$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdf 202:80 0 10G 0 disk ├─vg--docker-data 253:0 0 90G 0 lvm │ └─docker-202:1-1032-pool 253:2 0 10G 0 dm └─vg--docker-metadata 253:1 0 4G 0 lvm └─docker-202:1-1032-pool 253:2 0 10G 0 dm 下图显示由 lsblk 命令输出的之前镜像的详细信息。\n可以看出，名为 Docker-202:1-1032-pool 的 pool 横跨在 data 和 metadata 设备之上。pool 的命名规则为：\nDocker-主设备号:二级设备号-inode号-pool\n管理 devicemapper # 3.1 监控 thin pool # 不要过于依赖 lvm 的自动扩展，通常情况下 Volume Group 会自动扩展，但有时候 volume 还是会被塞满，你可以通过命令 lvs 或 lvs -a 来监控 volume 剩余的空间。也可以考虑使用 nagios 等监控工具来进行监控。\n可以查看 lvm 日志，了解 thin pool 在自动扩容触及阈值时的状态：\n$ journalctl -fu dm-event.service 如果你在使用精简池（thin pool）的过程中频繁遇到问题，你可以在 /etc/docker.daemon.json 中设置参数 dm.min_free_space 的值（表示百分比）。例如将其设置为 10，以确保当可用空间达到或接近 10％ 时操作失败，并发出警告。参考 storage driver options in the Engine daemon reference. 3.2 为正在运行的设备增加容量 # 如果 lv 的存储空间已满，并且 vg 处于满负荷状态，你可以为正在运行的 thin-pool 设备增加存储卷的容量，具体过程取决于您是使用 loop-lvm 精简池还是使用 direct-lvm 精简池。\n调整 loop-lvm 精简池的大小 # 调整 loop-lvm 精简池的最简单方法是使用 device_tool 工具，你也可以使用操作系统自带的工具。\na. 使用 device_tool 工具 # 在 docker 官方 github 仓库的 contrib/ 目录中有一个社区贡献的脚本 device_tool.go，你可以通过此工具免去繁琐的步骤来调整 loop-lvm 精简池的大小。这个工具不能保证 100% 有效，最好不要在生产环境中使用 loop-lvm 模式。\nclone 整个仓库 docker-ce，切换到目录 contrib/docker-device-tool ，按照 README.md 中的说明编译该工具。\n使用该工具。例如调整 thin pool 的大小为 200GB。\n$ ./device_tool resize 200GB b. 使用操作系统工具 # 如果你不想使用 device_tool 工具，可以通过操作系统工具手动调整 loop-lvm 精简池的大小。\n在 loop-lvm 模式中，Docker 使用的 Device Mapper 设备默认使用 loopback 设备，后端为自动生成的稀疏文件，如下:\n$ ls -lsh /var/lib/docker/devicemapper/devicemapper/ 总用量 510M 508M -rw-------. 1 root root 100G 10月 30 00:00 data 1.9M -rw-------. 1 root root 2.0G 10月 30 00:00 metadata data [存放数据] 和 metadata [存放元数据] 的大小从输出可以看出初始化默认为 100G 和 2G 大小，都是稀疏文件，使用多少占用多少。\nDocker 在初始化的过程中，创建 data 和 metadata 这两个稀疏文件，并分别附加到回环设备 /dev/loop0 和 /dev/loop1 上，然后基于回环设备创建 thin pool。 默认一个 container 最大存放数据不超过 10G。\n查看 data 和 metadata 的文件路径：\n$ docker info |grep \u0026#39;loop file\u0026#39; Data loop file: /var/lib/docker/devicemapper/data Metadata loop file: /var/lib/docker/devicemapper/metadata 按照以下步骤来增加精简池的大小。在这个例子中，thin-pool 原来的容量为 100GB，增加到200GB。\n查看 data 和 metadata 的大小。\n$ ls -lh /var/lib/docker/devicemapper/ total 1175492 -rw------- 1 root root 100G Mar 30 05:22 data -rw------- 1 root root 2.0G Mar 31 11:17 metadata 使用 truncate 命令将数据文件的大小增加到 200G。\n$ truncate -s 200G /var/lib/docker/devicemapper/data 注意：减小数据文件的大小有可能会对数据造成破坏，请慎重考虑。 验证文件大小。\n$ ls -lh /var/lib/docker/devicemapper/ total 1.2G -rw------- 1 root root 200G Apr 14 08:47 data -rw------- 1 root root 2.0G Apr 19 13:27 metadata 可以看到 loopback 文件的大小已经改变，但还没有保存到内存中。\n在内存中列出环回设备的大小，重新加载该设备，然后再次列出大小。\n$ echo $[ $(sudo blockdev --getsize64 /dev/loop0) / 1024 / 1024 / 1024 ] 100 $ losetup -c /dev/loop0 $ echo $[ $(sudo blockdev --getsize64 /dev/loop0) / 1024 / 1024 / 1024 ] 200 重新加载之后，loopback 设备的大小变为 200GB。\n重新加载 devicemapper thin pool。\n查看 thin pool 的名称 $ dmsetup status | grep \u0026#39; thin-pool \u0026#39; | awk -F \u0026#39;: \u0026#39; {\u0026#39;print $1\u0026#39;} 查看当前卷的信息表 $ dmsetup table docker-8:1-123141-pool 0 209715200 thin-pool 7:1 7:0 128 32768 1 skip_block_zeroing 第二个数字是设备的大小，表示有多少个 512－bytes 的扇区。\n128 是最小的可分配的 sector 数。 32768 是最少可用 sector 的 water mark，也就是一个 threshold。 1 代表有一个附加参数。 skip_block_zeroing是个附加参数，表示略过用0填充的块。 使用输出的第二个字段计算扩展后的 thin pool 总大小，该字段表示有多少个扇区。100G 的文件含有 209715200 个扇区，扩展到 200G 后，扇区数为 419430400。\n使用新的扇区数重新加载 thin pool。\n$ dmsetup suspend docker-8:1-123141-pool $ dmsetup reload docker-8:1-123141-pool --table \u0026#39;0 419430400 thin-pool 7:1 7:0 128 32768 1 skip_block_zeroing\u0026#39; $ dmsetup resume docker-8:1-123141-pool 调整 direct-lvm 精简池的大小 # 要调整 direct-lvm 精简池的大小，需要添加一块新的块设备到 Docker 的宿主机。并记下内核分配给它的设备名称。例如新的块设备名称为 /dev/xvdg。\n按照以下步骤来增加 direct-lvm 精简池的大小，请根据实际情况替换以下部分参数。\n查看卷组的信息。\n使用 pvdisplay 命令查看精简池当前正在使用的物理块设备以及卷组的名称\n$ pvdisplay |grep \u0026#39;VG Name\u0026#39; PV Name /dev/xvdf VG Name docker 扩展卷组。\n$ vgextend docker /dev/xvdg Physical volume \u0026#34;/dev/xvdg\u0026#34; successfully created. Volume group \u0026#34;docker\u0026#34; successfully extended 扩展逻辑卷 docker/thinpool。\n$ lvextend -l+100%FREE -n docker/thinpool Size of logical volume docker/thinpool_tdata changed from 95.00 GiB (24319 extents) to 198.00 GiB (50688 extents). Logical volume docker/thinpool_tdata successfully resized. 该命令使用了存储卷的全部空间，没有配置自动扩展。如果要扩展 metadata 精简池，请使用 docker/thinpool_tmeta 替换 docker/thinpool。\n验证新的 thin pool 的大小。\n$ docker info ...... Storage Driver: devicemapper Pool Name: docker-thinpool Pool Blocksize: 524.3 kB Base Device Size: 10.74 GB Backing Filesystem: xfs Data file: Metadata file: Data Space Used: 212.3 MB Data Space Total: 212.6 GB Data Space Available: 212.4 GB Metadata Space Used: 286.7 kB Metadata Space Total: 1.07 GB Metadata Space Available: 1.069 GB \u0026lt;output truncated\u0026gt; 通过 Data Space Available 字段的值查看 thin pool 的大小。\n重启操作系统后重新激活 devicemapper # 如果重启系统后发现 docker 服务启动失败，你会看到像 “Non existing device” 这样的报错信息。这时需要重新激活逻辑卷。\n$ lvchange -ay docker/thinpool devicemapper 存储驱动的工作原理 # 注意：不要直接操作 /var/lib/docker/ 中的任何文件或目录，这些文件和目录由 docker 自动管理。 查看设备和存储池：\n$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdf 202:80 0 100G 0 disk ├─docker-thinpool_tmeta 253:0 0 1020M 0 lvm │ └─docker-thinpool 253:2 0 95G 0 lvm └─docker-thinpool_tdata 253:1 0 95G 0 lvm └─docker-thinpool 253:2 0 95G 0 lvm 查看 docker 正在使用的挂载点：\n$ mount |grep devicemapper /dev/xvda1 on /var/lib/docker/devicemapper type xfs (rw,relatime,seclabel,attr2,inode64,noquota) 使用 devicemapper 后，Docker 将镜像和层级内容存储在 thin pool 中，并将它们挂载到 /var/lib/docker/devicemapper/ 目录中暴露给容器使用。\n4.1 磁盘上的镜像和容器层 # /var/lib/docker/devicemapper/metadata/ 目录中包含了有关 devicemapper 配置本身的元数据，以及卷、快照和每个卷的块或者快照同存储池中块的映射信息。devicemapper 使用了快照技术，元数据中也包含了这些快照的信息，以 json 格式保存在文本中。\n/var/lib/devicemapper/mnt/ 目录包含了所有镜像和容器层的挂载点。镜像层的挂载点表现为空目录，容器层的挂载点显示的是容器内部的文件系统。\n4.2 镜像分层与共享 # devicemapper 存储驱动使用专用块设备而不是格式化的文件系统，通过在块级别上对文件进行操作，能够在写时复制（CoW）期间实现最佳性能。\ndevicemapper 驱动将所有的镜像和容器存储到 /var/lib/docker/devicemapper/ 目录，该目录由一个或多个块级设备、环回设备（仅测试）或物理硬盘组成。\n使用 devicemapper 创建一个镜像的过程如下：\ndevicemapper 存储驱动创建一个精简池(thin pool)。这个池是从块设备或循环挂载的文件。\n下一步是创建一个 base 设备。一个 base 设备是具有文件系统的精简设备。你可以通过运行 docker info 命令检查 Backing filesystem 来查看使用的是哪个文件系统。\n每一个新镜像(和镜像数据层)是这个 base 设备的一个快照。这些是精简置备写时拷贝快照。这意味着它们初始为空，只在往它们写入数据时才消耗池中的空间。\n使用 devicemapper 驱动时，容器数据层是从其创建的镜像的快照。与镜像一样，容器快照是精简置备写时拷贝快照。容器快照存储着容器的所有更改。当数据写入容器时，devicemapper 从存储池按需分配空间。\n下图显示一个具有一个base设备和两个镜像的精简池。\n如果你仔细查看图表你会发现快照一个连着一个。每一个镜像数据层是它下面数据层的一个快照。每个镜像的最底端数据层是存储池中 base 设备的快照。此 base 设备是 Device Mapper 的工件，而不是 Docker 镜像数据层。\n一个容器是从其创建的镜像的一个快照。下图显示两个容器： 一个基于 Ubuntu 镜像和另一个基于 Busybox 镜像。\ndevicemapper 读写数据的过程 # 5.1 读数据 # 我们来看下使用 devicemapper 存储驱动如何进行读文件。下图显示在示例容器中读取一个单独的块 [0x44f] 的过程。\n一个应用程序请求读取容器中 0x44f 数据块。由于容器是一个镜像的一个精简快照，它没有那个数据，只有一个指向镜像存储的地方的指针。\n存储驱动根据指针，到镜像快照的 a005e 镜像层寻找 0xf33 块区。\ndevicemapper 从镜像快照复制数据块 0xf33 的内容到容器内存中。\n存储驱动最后将数据返回给请求的应用。\n5.2 写数据 # 写入新数据 : 使用 devicemapper 驱动，通过按需分配（allocate-on-demand）操作来实现写入新数据到容器，所有的新数据都被写入容器的可写层中。 例如要写入 56KB 的新数据到容器：\n一个应用程序请求写入56KB的新数据到容器。 按需分配操作给容器快照分配一个新的64KB数据块。如果写操作大于64KB，就分配多个新数据块给容器快照。 新的数据写入到新分配的数据块。 覆盖存在的数据 : 更新存在的数据使用写时拷贝（copy-on-write）操作，先从最近的镜像层中读取与该文件相关的数据块；然后分配新的空白数据块给容器快照并复制数据到这些数据块；最后更新好的数据写入到新分配的数据块。\n删除数据 : 当从容器的可写层中删除文件或目录时，或者从镜像层中删除其父层镜像中已存在的文件时，devicemapper 存储驱动会截获对该文件或目录的进一步读取尝试，并响应该文件或目录不存在。\n写入新数据并删除旧数据 : 当你向容器中写入新数据并删除旧数据时，所有这些操作都发生在容器的可写层。如果你使用的是 direct-lvm 模式，删除的数据块将会被释放；如果你使用的是 loop-lvm 模式，那么这些数据块就不会被释放。因此不建议在生产环境中使用 loop-lvm 模式。\nDevice Mapper 对 Docker 性能的影响 # 了解按需分配和写时拷贝操作对整体容器性能的影响很重要。\n6.1 按需分配对性能的影响 # devicemapper 存储驱动通过按需分配操作给容器分配新的数据块。这意味着每次应用程序写入容器内的某处时，一个或多个空数据块从存储池中分配并映射到容器中。\n所有数据块为 64KB。 写小于 64KB 的数据仍然分配一个 64KB 数据块。写入超过 64KB 的数据分配多个 64KB 数据块。所以，特别是当发生很多小的写操作时，就会比较影响容器的性能。不过一旦数据块分配给容器，后续的读和写可以直接在该数据块上操作。\n6.2 写时拷贝对性能的影响 # 每当容器首次更新现有数据时，devicemapper 存储驱动必须执行写时拷贝操作。这会从镜像快照复制数据到容器快照。此过程对容器性能产生显着影响。因此，更新一个 1GB 文件的 32KB 数据只复制一个 64KB 数据块到容器快照。这比在文件级别操作需要复制整个 1GB 文件到容器数据层有明显的性能优势。\n不过在实践中，当容器执行很多小于 64KB 的写操作时，devicemapper 的性能会比 AUFS 要差。\n6.3 其他注意事项 # 还有其他一些影响 devicemapper 存储驱动性能的因素。\n模式 Docker 使用的 devicemapper 存储驱动的默认模式是 loop-lvm。这个模式使用空闲文件来构建存储池，性能非常低。不建议用到生产环境。推荐用在生产环境的模式是 direct-lvm。\n存取速度 如果希望获得更佳的性能，可以将数据文件和元数据文件放在 SSD 这样的高速存储上。\n内存使用 devicemapper 并不是一个有效使用内存的存储驱动。当一个容器运行 n 个时，它的文件也会被拷贝 n 份到内存中，这对 docker 宿主机的内存使用会造成明显影响。因此，不建议在 PaaS 或者资源密集场合使用。\n对于写操作较大的，可以采用挂载 data volumes。使用 data volumes 可以绕过存储驱动，从而避免 thin provisioning 和 copy-on-write 引入的额外开销。\n","date":"2018年1月22日","externalUrl":null,"permalink":"/posts/use-devicemapper/","section":"博客","summary":"准备条件 # devicemapper 存储驱动是 RHEL, CentOS 和 Oracle Linux 系统上唯一一个支持 Docker EE 和 Commercially Supported","title":"Device Mapper基础教程：Docker 中使用 devicemapper 存储驱动","type":"posts"},{"content":" Thin Provisioning Snapshot 演示 # 上一篇我们介绍了 Device Mapper 框架的技术原理及其核心概念，下面，我们用一系列的命令来演示一下 Device Mapper 的 Thin Provisioning Snapshot 是怎么玩的。\n首先，我们需要先建两个文件，一个是data.img，一个是meta.data.img：\n$ dd if=/dev/zero of=/tmp/data.img bs=1K count=1 seek=10M 1+0 records in 1+0 records out 1024 bytes (1.0 kB) copied, 0.000621428 s, 1.6 MB/s $ dd if=/dev/zero of=/tmp/meta.data.img bs=1K count=1 seek=1G 1+0 records in 1+0 records out 1024 bytes (1.0 kB) copied, 0.000140858 s, 7.3 MB/s 注意命令中 seek 选项，其表示为略过 of 选项指定的输出文件的前 10G 个 output 的 bloksize 的空间后再写入内容。\n因为 bs 是 1 个字节，所以也就是 10G 的尺寸，但其实在硬盘上是没有占有空间的，占有空间只有 1k 的内容。当向其写入内容时，才会在硬盘上为其分配空间。\n我们可以用 ls 命令看一下，实际分配了 12K 和 4K。\n$ ls -lsh /tmp/data.img 12K -rw-r--r--. 1 root root 11G Aug 25 23:01 /tmp/data.img $ ls -slh /tmp/meta.data.img 4.0K -rw-r--r--. 1 root root 101M Aug 25 23:17 /tmp/meta.data.img 然后，我们为这个文件创建一个 loopback 设备。（loop2015 和 loop2016 是我乱取的两个名字）\n$ losetup /dev/loop2015 /tmp/data.img $ losetup /dev/loop2016 /tmp/meta.data.img $ losetup -a /dev/loop2015: [64768]:103991768 (/tmp/data.img) /dev/loop2016: [64768]:103991765 (/tmp/meta.data.img) 现在，我们为这个设备建一个 Thin Provisioning 的 Pool，用 dmsetup 命令：\n$ dmsetup create hchen-thin-pool \\ --table \u0026#34;0 20971522 thin-pool /dev/loop2016 /dev/loop2015 \\ 128 65536 1 skip_block_zeroing\u0026#34; 其中的参数解释如下（更多信息可参看 Thin Provisioning 的 man page）:\ndmsetup create 是用来创建 thin pool 的命令 hchen-thin-pool 是自定义的一个 pool 名，不冲突就好。 –-table 是这个 pool 的参数设置 0 代表起的 sector 位置 20971522 代表结尾的 sector 号，前面说过，一个 sector 是 512 字节，所以，20971522 个正好是 10GB /dev/loop2016 是 meta 文件的设备（前面我们建好了） /dev/loop2015 是 data 文件的设备 128 是最小的可分配的 sector 数 65536 是最少可用 sector 的 water mark，也就是一个 threshold 1 代表有一个附加参数 skip_block_zeroing 是个附加参数，表示略过用 0 填充的块 然后，我们就可以看到一个 Device Mapper 的设备了：\n$ ll /dev/mapper/hchen-thin-pool lrwxrwxrwx. 1 root root 7 Aug 25 23:24 /dev/mapper/hchen-thin-pool -\u0026gt; ../dm-4 接下来，我们的初始还没有完成，还要创建一个 Thin Provisioning 的 Volume：\n$ dmsetup message /dev/mapper/hchen-thin-pool 0 \u0026#34;create_thin 0\u0026#34; $ dmsetup create hchen-thin-volumn-001 \\ --table \u0026#34;0 2097152 thin /dev/mapper/hchen-thin-pool 0\u0026#34; 其中：\n第一个命令中的 create_thin 是关键字，后面的 0 表示这个 Volume 的 device 的 id。 第二个命令，是真正的为这个 Volumn 创建一个可以 mount 的设备，名字叫 hchen-thin-volumn-001。2097152 只有 1GB。 好了，在 mount 前，我们还要格式化一下：\n$ mkfs.ext4 /dev/mapper/hchen-thin-volumn-001 mke2fs 1.42.9 (28-Dec-2013) Discarding device blocks: done Filesystem label= OS type: Linux Block size=4096 (log=2) Fragment size=4096 (log=2) Stride=16 blocks, Stripe width=16 blocks 65536 inodes, 262144 blocks 13107 blocks (5.00%) reserved for the super user First data block=0 Maximum filesystem blocks=268435456 8 block groups 32768 blocks per group, 32768 fragments per group 8192 inodes per group Superblock backups stored on blocks: 32768, 98304, 163840, 229376 Allocating group tables: done Writing inode tables: done Creating journal (8192 blocks): done Writing superblocks and filesystem accounting information: done 好了，我们可以 mount 了（下面的命令中，我还创建了一个文件）\n$ mkdir -p /mnt/base $ mount /dev/mapper/hchen-thin-volumn-001 /mnt/base $ echo \u0026#34;hello world, I am a base\u0026#34; \u0026gt; /mnt/base/id.txt $ cat /mnt/base/id.txt hello world, I am a base 接下来，我们来看看 snapshot 怎么搞：\n$ dmsetup message /dev/mapper/hchen-thin-pool 0 \u0026#34;create_snap 1 0\u0026#34; $ dmsetup create mysnap1 \\ --table \u0026#34;0 2097152 thin /dev/mapper/hchen-thin-pool 1\u0026#34; $ ll /dev/mapper/mysnap1 lrwxrwxrwx. 1 root root 7 Aug 25 23:49 /dev/mapper/mysnap1 -\u0026gt; ../dm-5 上面的命令中：\n第一条命令是向 hchen-thin-pool 发一个 create_snap 的消息，后面跟两个 id，第一个是新的 dev id，第二个是要从哪个已有的 dev id 上做 snapshot（0 这个 dev id 是我们前面就创建了了） 第二条命令是创建一个 mysnap1 的 device，并可以被 mount。 下面我们来看看：\n$ mkdir -p /mnt/mysnap1 $ mount /dev/mapper/mysnap1 /mnt/mysnap1 $ ll /mnt/mysnap1/ total 20 -rw-r--r--. 1 root root 25 Aug 25 23:46 id.txt drwx------. 2 root root 16384 Aug 25 23:43 lost+found $ cat /mnt/mysnap1/id.txt hello world, I am a base 我们来修改一下 /mnt/mysnap1/id.txt，并加上一个 snap1.txt 的文件：\n$ echo \u0026#34;I am snap1\u0026#34; \u0026gt;\u0026gt; /mnt/mysnap1/id.txt $ echo \u0026#34;I am snap1\u0026#34; \u0026gt; /mnt/mysnap1/snap1.txt $ cat /mnt/mysnap1/id.txt hello world, I am a base I am snap1 $ cat /mnt/mysnap1/snap1.txt I am snap1 我们再看一下 /mnt/base，你会发现没有什么变化：\n$ ls /mnt/base id.txt lost+found $ cat /mnt/base/id.txt hello world, I am a base 好了，我相信你看到了分层镜像的样子了。\nDocker 的 devicemapper # 上面基本上就是 Docker 的玩法了，我们可以看一下 docker 的 loopback 设备：\n$ losetup -a /dev/loop0: [64768]:38050288 (/var/lib/docker/devicemapper/devicemapper/data) /dev/loop1: [64768]:38050289 (/var/lib/docker/devicemapper/devicemapper/metadata) 其中 data 100GB，metadata 2.0GB\n$ ls -alsh /var/lib/docker/devicemapper/devicemapper 506M -rw-------. 1 root root 100G Sep 10 20:15 data 1.1M -rw-------. 1 root root 2.0G Sep 10 20:15 metadata 下面是相关的 thin-pool。其中，有个当一大串 hash 串的 device 是正在启动的容器：\n$ ll /dev/mapper/dock* lrwxrwxrwx. 1 root root 7 Aug 25 07:57 /dev/mapper/docker-253:0-104108535-pool -\u0026gt; ../dm-2 lrwxrwxrwx. 1 root root 7 Aug 25 11:13 /dev/mapper/docker-253:0-104108535-deefcd630a60aa5ad3e69249f58a68e717324be4258296653406ff062f605edf -\u0026gt; ../dm-3 我们可以看一下它的 device id（Docker 都把它们记下来了）：\n$ cat /var/lib/docker/devicemapper/metadata/deefcd630a60aa5ad3e69249f58a68e717324be4258296653406ff062f605edf device_id 是 24，size 是 10737418240，除以 512，就是 20971520 个 sector。\n我们用这些信息来做个 snapshot 看看（注：我用了一个比较大的 dev id – 1024）：\n$ dmsetup message \u0026#34;/dev/mapper/docker-253:0-104108535-pool\u0026#34; 0 \\ \u0026#34;create_snap 1024 24\u0026#34; $ dmsetup create dockersnap --table \\ \u0026#34;0 20971520 thin /dev/mapper/docker-253:0-104108535-pool 1024\u0026#34; $ mkdir /mnt/docker $ mount /dev/mapper/dockersnap /mnt/docker/ $ ls /mnt/docker/ id lost+found rootfs $ ls /mnt/docker/rootfs/ bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var 我们在 docker 的容器里用 findmnt 命令也可以看到相关的 mount 的情况（因为太长，下面只是摘要）：\n$ findmnt TARGET SOURCE / /dev/mapper/docker-253:0-104108535-deefcd630a60[/rootfs] /etc/resolv.conf /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/resolv.conf] /etc/hostname /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/hostname] /etc/hosts /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/hosts] ","date":"2018年1月22日","externalUrl":null,"permalink":"/posts/thin-provisioning/","section":"博客","summary":"Thin Provisioning Snapshot 演示 # 上一篇我们介绍了 Device Mapper 框架的技术原理及其核心概念，","title":"Device Mapper系列基础教程：Thin Provisioning 实践","type":"posts"},{"content":" Device Mapper 简介 # Device Mapper 是 linux 的内核用来将块设备映射到虚拟块设备的 framework，它支持许多高级卷管理技术。docker 的 devicemapper 存储驱动程序利用此框架的自动精简配置(thin provisioning) 和快照功能来管理 docker 镜像和容器。本文将 Device Mapper 存储驱动称为 devicemapper，将它的内核框架称为 Device Mapper。 Device Mapper 不同于 AUFS、ext4、NFS 等，因为它并不是一个文件系统（File System），而是 Linux 内核映射块设备的一种技术框架。提供的一种从逻辑设备（虚拟设备）到物理设备的映射框架机制，在该机制下，用户可以很方便的根据自己的需要制定实现存储资源的管理策略。\n当前比较流行的 Linux 下的逻辑卷管理器如 LVM2（Linux Volume Manager 2 version)、EVMS(Enterprise Volume Management System)、dmraid(Device Mapper Raid Tool)等都是基于该机制实现的。\n值得一提的是 Device Mapper 工作在块级别（block），并不工作在文件级别（file）。Device Mapper 自 Linux 2.6.9 后编入 Linux 内核，所有基于 Linux 内核 2.6.9 以后的发行版都内置 Device Mapper，但你需要进行一些额外的配置才能在 docker 中使用它。比如在 RHEL 和 CentOS 系统中，docker 默认使用的存储驱动是 overlay。\ndevicemapper 存储驱动使用专用于 docker 的块设备，它运行在块级别上而不是文件级别。使用块设备比直接使用文件系统性能更好，通过向 Docker 的宿主机添加物理存储可以扩展块设备的存储空间。\n用户空间和内核空间 # Device Mapper主要分为用户空间部分和内核空间部分\n用户空间相关部分主要负责配置具体的策略和控制逻辑，比如逻辑设备和哪些物理设备建立映射，怎么建立这些映射关系等，包含 device mapper 库和 dmsetup 工具。对用户空间创建删除 device mapper 设备的操作进行封装。\n内核中主要提供完成这些用户空间策略所需要的机制，负责具体过滤和重定向 IO 请求。通过不同的驱动插件，转发 IO 请求至目的设备上。附上 Device Mapper 架构图。\nDevice Mapper 技术分析 # Device Mapper 作为 Linux 块设备映射技术框架，向外部提供逻辑设备。包含三个重要概念，映射设备（mapped device），映射表（map table），目标设备（target device）。\n映射设备即对外提供的逻辑设备，映射设备向下寻找必须找到支撑的目标设备。 映射表存储映射设备和目标设备的映射关系。 目标设备可以是映射设备或者物理设备，如果目标设备是一块映射设备，则属于嵌套，理论上可以无限迭代下去。 简而言之，Device Mapper 对外提供一个虚拟设备供使用，而这块虚拟设备可以通过映射表找到相应的地址，该地址可以指向一块物理设备，也可以指向一个虚拟设备。\n映射表，是由用户空间创建，传递到内核空间。映射表里有映射设备逻辑的起始地址、范围、和表示在目标设备所在物理设备的地址偏移量以及Target 类型等信息（注：这些地址和偏移量都是以磁盘的扇区为单位的，即 512 个字节大小，所以，当你看到 128 的时候，其实表示的是 128*512=64K）。\n映射驱动在内核空间是插件，Device Mapper 在内核中通过一个一个模块化的 Target Driver 插件实现对 IO 请求的过滤或者重新定向等工作，当前已经实现的插件包括软 Raid、加密、多路径、镜像、快照等，这体现了在 Linux 内核设计中策略和机制分离的原则。\nDevice Mapper 中的 IO 流处理，从虚拟设备（逻辑设备）根据映射表并指定特定的映射驱动转发到目标设备上。\nDocker 中的 Device Mapper 核心技术 # Docker 的 devicemapper 驱动有三个核心概念，copy on-write（写复制），thin-provisioning（精简配置）。snapshot（快照），首先简单介绍一下这三种技术。\nCoW（copy on write）写复制：一些文件系统提供的写时复制策略。\naufs 的 cow 原理如下：\n当容器需要修改一个文件，而该文件位于低层 branch 时，顶层 branch 会直接复制低层 branch 的文件至顶层再进行修改，而低层的文件不变，这种方式即是 CoW 技术（写复制）。\n当容器删除一个低层 branch 文件时，只是在顶层 branch 对该文件进行重命名并隐藏，实际并未删除文件，只是不可见。\n下图所示，容器层所见 file1 文件为镜像层文件，当需要修改 file1 时，会从镜像层把文件复制到容器层，然后进行修改，从而保证镜像层数据的完整性和复用性。\n下图所示，当需要删除 file1 时，由于 file1 是镜像层文件，容器层会创建一个 .wh 前置的隐藏文件，从而实现对 file1 的隐藏，实际并未删除 file1，从而保证镜像层数据的完整性和复用性。\ndevicemapper 支持在块级别（block）写复制。\nSnapshot（快照技术）：关于指定数据集合的一个完全可用拷贝，该拷贝包括相应数据在某个时间点（拷贝开始的时间点）的映像。快照可以是其所表示的数据的一个副本，也可以是数据的一个复制品。而从具体的技术细节来讲，快照是指向保存在存储设备中的数据的引用标记或指针。\nThin-provisioning（精简配置），直译为精简配置。Thin-provisioning 是动态分配，需要多少分配多少，区别于传统分配固定空间从而造成的资源浪费。\n它是什么意思呢？你可以联想一下我们计算机中的内存管理中用到的——“虚拟内存技术”——操作系统给每个进程 N 多 N 多用不完的内址地址（32 位下，每个进程可以有最多 2GB 的内存空间），但是呢，我们知道，物理内存是没有那么多的，如果按照进程内存和物理内存一一映射来玩的话，那么，我们得要多少的物理内存啊。所以，操作系统引入了虚拟内存的设计，意思是，我逻辑上给你无限多的内存，但是实际上是实报实销，因为我知道你一定用不了那么多，于是，达到了内存使用率提高的效果。（今天云计算中很多所谓的虚拟化其实完全都是在用和“虚拟内存”相似的 Thin Provisioning 的技术，所谓的超配，或是超卖）。\n好了，话题拉回来，我们这里说的是存储。看下面两个图，第一个是 Fat Provisioning，第二个是 Thin Provisioning，其很好的说明了是个怎么一回事（和虚拟内存是一个概念）。\n下图中展示了某位用户向服务器管理员请求分配 10TB 的资源的情形。实际情况中这个数值往往是峰值，根据使用情况，分配 2TB 就已足够。因此，系统管理员准备 2TB 的物理存储，并给服务器分配 10TB 的虚拟卷。服务器即可基于仅占虚拟卷容量 1/5 的现有物理磁盘池开始运行。这样的“始于小”方案能够实现更高效地利用存储容量。\n那么，Docker 是怎么使用 Thin Provisioning 这个技术做到像 UnionFS 那样的分层镜像的呢？答案是，Docker 使用了 Thin Provisioning 的 Snapshot 的技术。下面一篇我们来介绍一下 Thin Provisioning 的 Snapshot。\n参考资料 # DOCKER基础技术：DEVICEMAPPER Docker存储驱动DeviceMapper ","date":"2018年1月21日","externalUrl":null,"permalink":"/posts/devicemapper-theory/","section":"博客","summary":"Device Mapper 简介 # Device Mapper 是 linux 的内核用来将块设备映射到虚拟块设备的 fra","title":"Device Mapper系列基础教程：Device Mapper 的原理","type":"posts"},{"content":" 为了循序渐进，先从二维开始讲起，然后过渡到三维\n1. 二维空间 # 我们从一个五边形的面积开始说起\n比如我们要求这个正五边形的面积，该怎样用向量求呢？\n先简化这个问题，不用考虑五边形，只需考虑三角形。\n现在，我们把正五边形分割成三个三角形，再把三角形的面积加起来，就得到了五边形的面积。\n那么问题来了：怎样求三角形的面积？\n设三角形的面积为S，那么\n$$ S = \\frac{1}{2}\\left|\\vec{A}\\right|\\left|\\vec{MN}\\right| = \\frac{1}{2}\\left|\\vec{A}\\right|\\left|\\vec{B}\\right|\\sin(\\theta) \\tag{1} $$ $\\sin(\\theta)$ 该如何求呢？\n如果你学过向量的点积，应该知道$\\vec{a}\\cdot\\vec{b}=\\left|\\vec{a}\\right|\\left|\\vec{b}\\right|\\cos(\\theta)$.\n所以为了求$\\sin(\\theta)$，我们可以先求出$\\cos(\\theta)$\n$$ \\cos(\\theta)=\\frac{\\vec{a}\\cdot\\vec{b}}{\\left|\\vec{a}\\right|\\left|\\vec{b}\\right|} \\tag{2} $$ 再利用公式\n$$ \\cos^2(\\theta)+\\sin^2(\\theta)=1 \\tag{3} $$ 便可以求出 $\\sin(\\theta)$ 的值。\n通过以上步骤，可以看出这样做很麻烦，有没有更简单的办法呢？当然有\n求 $\\sin(\\theta)$ 太麻烦了，但是求 $\\cos(\\theta)$ 却很简单，为了避免求 $\\sin(\\theta)$，我们能否找到一个角，使这个角的余弦等于 $\\sin(\\theta)$ ?\n作向量$\\vec{A}$、$\\vec{B}$，夹角记为$\\theta$，将向量$\\vec{A}$逆时针旋转 $90^\\circ$ 得到 $\\vec{A^\\prime}$，如下图所示：\n通过上图给的条件，我们已知：\n$$ \\begin{cases} \\beta=\\frac{\\pi}{2}-\\theta \\\\ \\cos(\\beta)=\\sin(\\theta) \\end{cases} $$ 这意味着$\\vec{A}$的模长乘以$\\vec{B}$的模长，再乘以$\\sin(\\theta)$，等于$\\vec{A^\\prime}$的模长乘以$\\vec{B}$的模长，再乘以$\\cos(\\beta)$，得到：\n$$ \\begin{aligned} \u0026\\left|\\vec{A}\\right|\\left|\\vec{B}\\right|\\sin(\\theta) \\\\ = \u0026\\left|\\vec{A^\\prime}\\right|\\left|\\vec{B}\\right|\\cos(\\beta) \\\\ = \u0026\\vec{A^\\prime}\\cdot\\vec{B} \\end{aligned} $$ 即：\n$$ \\left|\\vec{A}\\right|\\left|\\vec{B}\\right|\\sin(\\theta)=\\vec{A^\\prime}\\cdot\\vec{B} \\tag{4} $$ 这个方法看起来不错，不过还有一点是不知道的，就是怎么求$\\vec{A^\\prime}$呢?\n假设$\\vec{A}$的坐标为$\\left\\langle a_1,a_2 \\right\\rangle$，由我画的图可知，逆时针旋转 $90^\\circ$ 后，得到：$A^\\prime=\\left\\langle -a_2,a_1 \\right\\rangle$ 。\n同时再假设$\\vec{B}$的坐标为$\\left\\langle b_1,b_2 \\right\\rangle$，现在将$\\vec{A}$和$\\vec{B}$的坐标分别带入(4)式，得到：\n$$ \\begin{aligned} \u0026 \\vec{A^\\prime}\\cdot\\vec{B} \\\\ = \u0026 \\left\\langle -a_2,a_1 \\right\\rangle \\cdot \\left\\langle b_1,b_2 \\right\\rangle \\\\ = \u0026 a_1b_2-a_2b_1 \\end{aligned} $$ 如果你学过行列式，应该知道\n$$ \\begin{aligned} \u0026 a_1b_2-a_2b_1 \\\\ = \u0026 \\begin{vmatrix} a_1 \u0026 a_2 \\\\\\ b_1 \u0026 b_2 \\end{vmatrix} \\\\ = \u0026 det(\\vec{A},\\vec{B}) \\end{aligned} $$ 由此可知，三角形的面积\n$$ S=\\frac{1}{2}det(\\vec{A},\\vec{B}) \\tag{5} $$ 现在可以得出结论：\n向量$\\vec{A}$与向量$\\vec{B}$的行列式表示一个以$\\vec{A}$和$\\vec{B}$为边构成的平行四边形的面积\n还可以表述得更严格一点，因为面积没有负数，而行列式的值有正有负，符号取决于两个向量之间的夹角，所以我们可以这样描述：\n向量$\\vec{A}$与向量$\\vec{B}$的行列式的绝对值表示一个以$\\vec{A}$和$\\vec{B}$为边构成的平行四边形的面积\n2. 三维空间 # 在空间中，从简单的开始，我们可以做两件事情：\n求平行六面体的体积 求平行六面体的表面积 咱们先来求平行六面体的体积。\n平行六面体的体积 # 求体积之前，需要了解几个定义\n空间中的行列式 # 空间中也有行列式的概念，假设有三个向量$\\vec{A}$、$\\vec{B}$和$\\vec{C}$，定义：\n$$ \\begin{aligned} det(\\vec{A},\\vec{B},\\vec{C}) \u0026 = \\begin{vmatrix} a_1 \u0026 a_2 \u0026 a_3 \\\\\\ b_1 \u0026 b_2 \u0026 b_3 \\\\\\ c_1 \u0026 c_2 \u0026 c_3 \\end{vmatrix} \\\\ \u0026 = a_1\\begin{vmatrix} b_2 \u0026 b_3 \\\\\\ c_2 \u0026 c_3 \\end{vmatrix} - a_2\\begin{vmatrix} b_1 \u0026 b_3 \\\\\\ c_1 \u0026 c_3 \\end{vmatrix} + a_3\\begin{vmatrix} b_1 \u0026 b_2 \\\\\\ c_1 \u0026 c_2 \\end{vmatrix} \\end{aligned} $$ 如果你学过行列式的知识，上面的计算过程应该很容易理解，我就不作过多解释了。\n叉乘 # 叉乘适用于两个在空间内的向量（这里我指的是三维空间），定义：\n$$ \\begin{aligned} \\vec{A}\\times\\vec{B}=\\begin{vmatrix} \\hat{i} \u0026 \\hat{j} \u0026 \\hat{k} \\\\\\ a_1 \u0026 a_2 \u0026 a_3 \\\\\\ b_1 \u0026 b_2 \u0026 b_3 \\end{vmatrix} \\end{aligned} $$ 其中，$\\hat{i}$,$\\hat{j}$,$\\hat{k}$分别为三维空间中的三个坐标轴上的单位向量。\n我们把$\\vec{A}\\times\\vec{B}$称为向量$\\vec{A}$与$\\vec{B}$的叉乘！\n如果你仔细观察，你会发现，这个行列式的第二行和第三行分别是向量$\\vec{A}$和$\\vec{B}$的坐标，但是第一行却是三个单位向量，这意味着后面两行的元素都是数值，而第一行的元素都是向量。这意味着什么？这不是常理上的行列式，如果你尝试在计算器中这样计算，它会显示这是错误的，向量不该出现在这里。\n那么，为什么要这么做呢？\n如果使用上面提到的空间中的行列式的定义，可以得到：\n$$ \\begin{vmatrix} \\hat{i} \u0026 \\hat{j} \u0026 \\hat{k} \\\\ a_1 \u0026 a_2 \u0026 a_3 \\\\ b_1 \u0026 b_2 \u0026 b_3 \\end{vmatrix} = \\begin{vmatrix} a_2 \u0026 a_3 \\\\ b_2 \u0026 b_3 \\end{vmatrix}\\hat{i} - \\begin{vmatrix} a_1 \u0026 a_3 \\\\ b_1 \u0026 b_3 \\end{vmatrix}\\hat{j} + \\begin{vmatrix} a_1 \u0026 a_2 \\\\ b_1 \u0026 b_2 \\end{vmatrix}\\hat{k} \\tag{6} $$ 你发现了什么？没错，我们得到的结果不是一个数，而是一个向量，这就是向量叉乘的定义。\n那么问题在于，这样定义有什么好处呢？这种怪异计算的几何意义在哪里？为什么我们要费心去这样做？\n下面我们对上面的式子进行转化，看看会出现什么神奇的结果。\n将式(6)进一步化简，得到：\n$$ \\begin{aligned} \\vec{A}\\times\\vec{B}=(a_2b_3-a_3b_2)\\hat{i}-(a_1b_3-a_3b_1)\\hat{j}+(a_1b_2-a_2b_1)\\hat{k} \\end{aligned} $$ 看起来没什么特别的，试着求一下$\\vec{A}\\times\\vec{B}$的模，为了方便计算，我们求$\\vec{A}\\times\\vec{B}$的模的平方\n$$ \\begin{aligned} \\left| \\vec{A}\\times\\vec{B} \\right|^2 \u0026 = (a_2b_3-a_3b_2)^2 + (a_1b_3-a_3b_1)^2 + (a_1b_2-a_2b_1)^2 \\\\ \u0026 = (a_1^2b_2^2+a_1^2b_3^2) + (a_2^2b_1^2+a_2^2b_3^2) + (a_3^2b_1^2+a_3^2b_2^2) \\\\ \u0026 - 2(a_1a_2b_1b_2+a_1a_3b_1b_3+a_2a_3b_2b_3) \\\\ \u0026 = (\\underbrace{a_1^2b_2^2+a_1^2b_3^2}+a_1^2b_1^2) + (\\underbrace{a_2^2b_1^2+a_2^2b_3^2}+a_2^2b_2^2) + (\\underbrace{a_3^2b_1^2+a_3^2b_2^2}+a_3^2b_3^2)i - (a_1^2b_1^2+a_2^2b_2^2+a_3^2b_3^2) - 2(a_1a_2b_1b_2+a_1a_3b_1b_3+a_2a_3b_2b_3) \\\\ \u0026 = {(\\underbrace{a_1^2b_1^2+a_1^2b_2^2+a_1^2b_3^2) + (a_2^2b_1^2+a_2^2b_2^2+a_2^2b_3^2) + (a_3^2b_1^2+a_3^2b_2^2+a_3^2b_3^2)}} - {\\underbrace{(a_1^2b_1^2+a_2^2b_2^2+a_3^2b_3^2) + 2(a_1a_2b_1b_2+a_1a_3b_1b_3+a_2a_3b_2b_3)}} \\\\ \u0026 = (a_1^2+a_2^2+a_3^2)(b_1^2+b_2^2+b_3^2) - (a_1b_1+a_2b_2+a_3b_3)^2 \\\\ \u0026 = \\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2-\\left|\\vec{A}\\cdot\\vec{B}\\right|^2 \\\\ \u0026 = \\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2\\cdot1-\\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2\\cdot\\frac{\\left|\\vec{A}\\cdot\\vec{B}\\right|^2}{\\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2} \\\\ \u0026 = \\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2(1-\\frac{\\left|\\vec{A}\\cdot\\vec{B}\\right|^2}{\\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2}) \\\\ \u0026 = \\left|\\vec{A}\\right|^2\\cdot\\left|\\vec{B}\\right|^2(1-cos^2\\left\\langle\\vec{A},\\vec{B}\\right\\rangle) \\\\ \u0026 = (\\left|\\vec{A}\\right|\\cdot\\left|\\vec{B}\\right|\\cdot\\sin\\left\\langle\\vec{A},\\vec{B}\\right\\rangle)^2 \\end{aligned} $$ 发现了什么？原来$\\vec{A}\\times\\vec{B}$的模长等于一个以$\\vec{A}$和$\\vec{B}$为边构成的平行四边形的面积。\n接下来的问题是：既然$\\vec{A}\\times\\vec{B}$的结果是一个向量，那么这个向量的方向是什么呢？\n答案是：它的方向垂直于向量$\\vec{A}$与$\\vec{B}$构成的平面，并且遵循右手定则。\n如果你不知道右手定则，我可以解释一下：\n首先，你的右手平行于向量 $\\vec{A}$ 的方向，然后，你的手指向向量 $\\vec{B}$ 的方向弯曲，这时，你的大拇指竖直的方向就是 $\\vec{A}\\times\\vec{B}$ 的方向。\n下面我们来证明一下为什么$\\vec{A}\\times\\vec{B}$的方向垂直于向量$\\vec{A}$与$\\vec{B}$构成的平面。\n为了简化，令\n$$ \\begin{cases} m_1=a_2b_3-a_3b_2 \\\\ m_2=a_1b_3-a_3b_1 \\\\ m_3=a_1b_2-a_2b_1 \\end{cases} $$ 于是可以得到\n$$ \\vec{A}\\times\\vec{B}=m_1\\hat{i}-m_2\\hat{j}+m_3\\hat{k} \\tag{7} $$ 基本思路是这样 :** 我们从$\\vec{A}$与$\\vec{B}$构成的平面中找两个方向不在同一条直线上的向量，如果$\\vec{A}\\times\\vec{B}$与这两个向量均垂直,那么它就垂直于$\\vec{A}$与$\\vec{B}$构成的平面**\n为了方便计算，我们这样定义三个互相垂直的单位向量：其中，向量$\\hat{i}$与$\\hat{j}$在$\\vec{A}$与$\\vec{B}$构成的平面上，而向量$\\hat{k}$垂直于这个平面。\n现在问题简单了，只要我们能证明$\\hat{i}\\times\\hat{j}$的方向平行于$\\hat{k}$，就说明$\\vec{A}\\times\\vec{B}$的方向垂直于向量$\\vec{A}$与$\\vec{B}$构成的平面。\n设 $\\hat{i}=\\left\\langle1,0,0\\right\\rangle$,$\\hat{j}=\\left\\langle0,1,0\\right\\rangle$,$\\hat{k}=\\left\\langle0,0,1\\right\\rangle$，那么\n$$ \\begin{aligned} \\hat{i}\\times\\hat{j} \u0026 = \\begin{vmatrix} \\hat{i} \u0026 \\hat{j} \u0026 \\hat{k} \\\\ 1 \u0026 0 \u0026 0 \\\\ 0 \u0026 1 \u0026 0 \\end{vmatrix} \\\\ \u0026 = \\begin{vmatrix} 0 \u0026 0 \\\\ 1 \u0026 0 \\end{vmatrix}\\hat{i}-\\begin{vmatrix} 1 \u0026 0 \\\\ 0 \u0026 0 \\end{vmatrix}\\hat{j}+\\begin{vmatrix} 1 \u0026 0 \\\\ 0 \u0026 1 \\end{vmatrix}\\hat{k} \\\\ \u0026 = \\hat{k} \\end{aligned} $$ 太神奇了，$\\hat{i}\\times\\hat{j}$竟然等于$\\hat{k}$，所以当然也平行于$\\hat{k}$，所以$\\vec{A}\\times\\vec{B}$的方向垂直于向量$\\vec{A}$与$\\vec{B}$构成的平面，并且遵循右手定则。\n于是可以得到如下的结论：\n$\\left|\\vec{A}\\times\\vec{B}\\right|$等于一个以$\\vec{A}$和$\\vec{B}$为边构成的平行四边形的面积 $\\vec{A}\\times\\vec{B}$的方向垂直于向量$\\vec{A}$与$\\vec{B}$构成的平面，并且遵循右手定则 下面我们回到最初提出的问题 :** 求平行六面体的体积**\n如上图所示，我们要求由三个向量 $\\vec{A}$,$\\vec{B}$ 与 $\\vec{C}$ 构成的平行六面体的体积。\n设体积为V，向量$\\vec{A}$与$\\vec{B}$构成的平行四边形的面积为S，高为h，那么：\n$$ V=S \\cdot h \\tag{8} $$ 通过上面的分析，可以得知$S=\\left|\\vec{A}\\times\\vec{B}\\right|$，那么高度h该怎么求呢？\n假设高度h的方向为$\\vec{H}$，那么h等于向量$\\vec{C}$在向量$\\vec{H}$上的投影，所以\n$$ \\begin{aligned} h \u0026 = \\left|\\vec{C}\\cdot\\right|\\cos\\left\\langle\\vec{C},\\vec{H}\\right\\rangle \\\\ \u0026 = \\left|\\vec{C}\\right|\\cdot\\frac{\\vec{C}\\cdot\\vec{H}}{\\left|\\vec{C}\\right|\\left|\\vec{H}\\right|} \\\\ \u0026 = \\vec{C}\\cdot\\frac{\\vec{H}}{\\left|\\vec{H}\\right|} \\\\ \u0026 = \\vec{C}\\cdot\\vec{h}, \u0026 \\text{设$\\vec{h}$为向量$\\vec{H}$方向上的单位向量} \\end{aligned} $$ 带入(8)式，得：\n$$ \\begin{aligned} V \u0026 = \\left|\\vec{A}\\times\\vec{B}\\right|\\cdot(\\vec{C}\\cdot\\vec{h}) \\\\ \u0026 = \\left|\\vec{A}\\times\\vec{B}\\right|\\cdot(\\vec{C}\\cdot\\frac{\\vec{A}\\times\\vec{B}}{\\left|\\vec{A}\\times\\vec{B}\\right|}) \\\\ \u0026 = \\vec{C}\\cdot(\\vec{A}\\times\\vec{B}) \\\\ \u0026 = \\left\\langle c_1,c_2,c_3 \\right\\rangle\\cdot\\lbrace(a_2b_3-a_3b_2)\\hat{i} - (a_1b_3-a_3b_1)\\hat{j} + (a_1b_2-a_2b_1)\\hat{k}\\rbrace \\\\ \u0026 = \\left\\langle c_1,c_2,c_3 \\right\\rangle\\cdot\\left\\langle a_2b_3-a_3b_2,a_1b_3-a_3b_1,a_1b_2-a_2b_1 \\right\\rangle \\\\ \u0026 = c_1\\begin{vmatrix} a_2 \u0026 a_3 \\\\ b_2 \u0026 b_3 \\end{vmatrix} - c_2\\begin{vmatrix} a_1 \u0026 a_3 \\\\ b_1 \u0026 b_3 \\end{vmatrix} + c_3\\begin{vmatrix} a_1 \u0026 a_2 \\\\ b_1 \u0026 b_2 \\end{vmatrix} \\\\ \u0026 = det(\\vec{A},\\vec{B},\\vec{C}) \\end{aligned} $$ 即：\n$$ V=det(\\vec{A},\\vec{B},\\vec{C})=\\vec{C}\\cdot(\\vec{A}\\times\\vec{B}) \\tag{9} $$ $\\vec{C}\\cdot(\\vec{A}\\times\\vec{B})$ 称为向量的混合积。\n现在可以得出结论：\n向量$\\vec{A}$、$\\vec{B}$与$\\vec{C}$的行列式等于由向量$\\vec{A}$、$\\vec{B}$与$\\vec{C}$构成的平行六面体的体积\n体积的部分暂时就讲到这里，接下来的一篇将会介绍平行六面体的面积。\n","date":"2016年12月3日","externalUrl":null,"permalink":"/posts/%E5%90%91%E9%87%8F%E7%9A%84%E5%8F%89%E4%B9%98%E4%B8%8E%E8%A1%8C%E5%88%97%E5%BC%8F/","section":"博客","summary":"为了循序渐进，先从二维开始讲起，然后过渡到三维 1. 二维空间 # 我","title":"向量的叉乘与行列式","type":"posts"},{"content":" 为了弄明白子空间投影是怎么一回事，我们遵循从低维到高维的规律，先从二维开始讲起。\n1. 二维空间 # 如下图所示（我随手画的，不要介意），设向量p是向量b在向量a上面的投影，向量e垂直于向量p及a。\n于是我们可以得到这样的一个等式：\n$$ a^Te = 0 $$\n即\n$$a^T(b-xa) = 0\\tag{1}$$\n解得：\n$$x = \\frac{a^Tb}{a^Ta}$$\n于是向量p可表示为：\n$$p = xa = a\\frac{a^Tb}{a^Ta}\\tag{2}$$\n现在我们设\n$$p = Pb\\tag{3}$$\n我们把这个矩阵P称为投影矩阵。\n比较式(2)和式(3)，立即可以知道：\n$${P = \\frac{a \\cdot a^T}{a^T \\cdot a}}\\tag{4}$$\n2. 三维空间 # 为了让你们能够有一个直观的认识，我仍然用我高超的画艺画了一幅美图：\n假设图中的那个平面由向量$a_1$和$a_2$构成，令\n$$A = \\begin{bmatrix} a_1 \u0026amp; a_2 \\end{bmatrix}$$\n由于向量p在平面上，所以p可以表示为：\n$$p = \\hat{x_1}a_1 + \\hat{x_2}a_2\\tag{5}$$\n即\n$$p = A\\hat{x}\\tag{6}$$\n与二维空间类似，设向量p是向量b在平面上的投影，向量e垂直于那个平面，当然也垂直于向量p，同样也垂直于向量$a_1$和$a_2$,于是可以得到方程组：\n$$ \\begin{cases} a_1^T(b - A\\hat{x}) = 0 \\\\ a_2^T(b - A\\hat{x}) = 0 \\\\ \\end{cases}\\tag{7} $$ 即\n$$ \\begin{bmatrix} a_1^T \\\\a_2^T \\end{bmatrix}(b - A\\hat{x}) = \\begin{bmatrix} 0 \\\\0 \\end{bmatrix} $$ 进一步化简得到：\n$$A^T(b - A\\hat{x}) = 0\\tag{8}$$\n解得：\n$$\\hat{x} = (A^TA)^{-1}(A^Tb)\\tag{9}$$\n将(9)代入(6)得：\n$$p = A\\hat{x} = A(A^TA)^{-1}A^Tb\\tag{10}$$\n与二维空间类似，我们设\n$$p = Pb\\tag{11}$$\n比较式(10)和式(11)，立即可以得到：\n$${P = A(A^TA)^{-1}A^T}\\tag{12}$$\n这就是投影矩阵的表达式！\n","date":"2016年6月4日","externalUrl":null,"permalink":"/posts/%E5%AD%90%E7%A9%BA%E9%97%B4%E6%8A%95%E5%BD%B1/","section":"博客","summary":"为了弄明白子空间投影是怎么一回事，我们遵循从低维到高维的规律","title":"子空间投影","type":"posts"},{"content":" 曾经看过国内各种关于讲解最小二乘法的教科书，但都是一大堆枯燥的推导公式，看起来很高深的样子，其实根本不知道它在说些什么！传授知识本来就应该告诉你这个东西到底是什么，它到底是干嘛的，就应该把复杂的问题简单化，可国内大多数教科书都是反其道而行，全是看起来很牛逼的样子，学生看了却什么也不懂。今天我就用最通俗易懂的方式，从线性代数和线性空间的角度告诉你什么是最小二乘法。\n最小二乘法是一种最优化技术，它的做法是找到一组估计值，使得估计值与实际值的平方和的值最小，通过使误差的平方和最小，我们可以得到一下线性方程组，对这个线性方程组进行求解就可以得到拟合曲线。我们可以通过一个例子来进行讲解。\n假设有一组数据点$t_1$，$t_2$和$t_3$，它们的坐标分别是(1，1)，(2，2)，(3，2)，而我想找到最优的那条线，假设这条直线为：\n$$y = C + Dt\\tag{1}$$\n它不会通过所有的点，因为不存在这样的直线，所以我要选一条最优的使总误差最小的直线。我们需要知道总误差怎么度量，因为它决定了哪条线胜出，我们必须先定出误差是什么，才能通过最小化这个量而找到C和D。在此我就不作图了，因为图像很简单，你们可以自己想象一下，或者自己拿笔画画。\n将这三个点带入方程，得到：\n$$ \\begin{cases} C + D = 1 \\\\ C + 2D =2 \\\\ C + 3D =2 \\end{cases}\\tag{2} $$ 通过计算我们知道，它们联立是无解的，但可以有最优解。\n这个方程组可写成矩阵的形式\n$$AX = B\\tag{3}$$\n其中，\\(A = \\begin{bmatrix} 1 \u0026amp; 1 \\\\1 \u0026amp; 2 \\\\1 \u0026amp; 3 \\end{bmatrix}\\)，\\(X = \\begin{bmatrix} C \u0026amp; D \\end{bmatrix}\\)，\\(B = \\begin{bmatrix} 1 \\\\2 \\\\3 \\end{bmatrix}\\)。\nA的列向量线性无关，所以它们构成了列空间的一组基，但列空间不包括向量B，所以方程无解。那么最优解是什么呢？\n我们将AX与B之间的差值相加，得到：\n$$AX - B = E\\tag{4}$$\n其中，$E = \\begin{bmatrix} e_1 \\\\e_2 \\\\e_3 \\end{bmatrix}$，$e_1 = C + D - 1$，$e_2 = C + 2D - 2$，$e_3 = C + 3D - 2$，E称为误差向量。\n我们要求的是$\\left|AX - B\\right|^2$ = $\\left|E\\right|^2$的最小值。\n$$\\left|E\\right|^2 = \\left|e_1\\right|^2 + \\left|e_2\\right|^2 + \\left|e_3\\right|^2\\tag{5}$$\n分别过点$t_1$,$t_2$和$t_3$作与x轴垂直的直线，与直线y=C+Dt的交点分别为$s_1$,$s_2$,$s_3$。于是\n$$ \\begin{cases} \\left|e_1\\right| = \\left|t_1-s_1\\right| \\\\ \\left|e_2\\right| = \\left|t_2-s_2\\right| \\\\ \\left|e_3\\right| = \\left|t_3-s_3\\right| \\end{cases}\\tag{6} $$ 假设$s_1$,$s_2$,$s_3$的纵坐标分别为$p_1$,$p_2$,$p_3$，令$p = \\begin{bmatrix}p_1 \\\\p_2\\\\p_3\\end{bmatrix}$。\n我们知道方程组$AX = B$是无解的，但方程组$AX = p$有解，我们来求解方程组$AX = p$。为了提醒自己这里表示的是最优的估计，而不是完美的结果，我们在X上面加个小帽子，使方程组变为\n$$A\\hat{X} = p\\tag{7}$$\n其中p是B在p向量这个方向上的投影，设投影矩阵为P，则\n$$p = PB\\tag{8}$$\n如果不懂什么是投影矩阵，可以参考我的另一篇文章 子空间投影，在此不作赘述。\n通过投影矩阵的知识我们知道投影矩阵P的表达式为\n$$P = A(A^TA)^{-1}A^T\\tag{9}$$\n代入(8)，得：\n$$p = A(A^TA)^{-1}A^TB\\tag{10}$$\n将(10)代入(7)，得：\n$$A\\hat{X} = A(A^TA)^{-1}A^TB$$\n最后得到方程组为：\n$$A^TAX = A^TB\\tag{11}$$\n解得\n$$\\hat{X} = \\begin{bmatrix} \\frac{2}{3} \\\\ \\frac{1}{2} \\end{bmatrix}$$\n这就是最优解，所以最优的那条直线为：\n$$y = \\frac{2}{3} + \\frac{1}{2}t\\tag{12}$$\n","date":"2016年6月4日","externalUrl":null,"permalink":"/posts/%E5%8D%81%E5%88%86%E9%92%9F%E5%91%8A%E8%AF%89%E4%BD%A0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B0%8F%E4%BA%8C%E4%B9%98%E6%B3%95/","section":"博客","summary":"曾经看过国内各种关于讲解最小二乘法的教科书，但都是一大堆枯燥","title":"最小二乘法的本质","type":"posts"},{"content":"","date":"1 January 0001","externalUrl":null,"permalink":"/en/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","date":"1 January 0001","externalUrl":null,"permalink":"/en/go/","section":"Cloud Native Labs","summary":"","title":"Redirect","type":"go"}]