简介:一份面向企业级微服务容器化落地的解决方案文档,围绕 Kubernetes 与 OpenShift 容器云平台,系统讲解单体应用拆分为微服务后,如何应对服务依赖、负载均衡、集群管理及有状态数据等核心挑战。资源为单个 docx 文件,约 417KB,内容涵盖容器云部署框架、权限管理、多租户隔离、日志监控四大模块,适合架构师、运维开发以及正在规划容器云平台的团队参考。文档以问答与方案说明相结合,给出 DMZ 和内网双 Openshift 隔离部署、基于 OAuth 的认证鉴权、SCC 细粒度控制、Project 租户网络及物理资源池隔离等具体做法,并说明了 EFK 日志平台与 Heapster/Cadvisor 监控组件的落地路径。对于容器日志持久化、中间件日志分类、监控信息采集与展示等实操细节,也给出可对照的组件选型思路。目前已有 497 人学习下载,内容由社区专家顾文俊等整理,可作为理解 K8S 微服务容器化部署路径的入门与选型参考。
1. 从单体拆完就乱说起:K8S 容器云平台到底解决了什么
微服务架构的核心是把一个巨大的单体应用拆成许多小的互相连接的微服务,但拆完之后问题才刚开始——服务之间有依赖关系,发布时每个服务单独启动会非常痛苦,登录服务、支付服务想一次全部启动,必须要用到编排的动作。K8S 是第一个把“一切以服务为中心,一切围绕服务运转”作为指导思想的编排产品,调度、负载均衡、集群管理、有状态数据管理这些微服务面临的痛点,它都提供了原生解法。这份方案资料来自社区专家顾文俊的线上交流整理,覆盖容器云部署框架、权限与多租户、日志监控、服务发布、集群安全和微服务拆分落地等内容,适合正准备做 K8S 容器云平台选型或已经把微服务拆完但不知道怎么部署上线的团队。
2. 容器云部署框架:DMZ/内网双环境与多租户隔离的四层实现
2.1 双环境隔离:为什么 DMZ 和内网要各建一套 OpenShift
很多团队一上来就问“K8S 集群应该建多大”,其实第一个该问的是“我要不要建两套”。资料里给出的方案是在 DMZ 和内网分别部署彼此独立的 2 套 OpenShift,分别为内网和 DMZ 区两个网段,两套环境彼此隔离。这个思路在企业环境里非常基础也非常关键。
DMZ 区的 OpenShift 部署对外发布的应用,负责处理外网的访问;内网的 OpenShift 部署针对内网的应用,仅负责处理内网的访问。这样做的直接好处是:外网流量再大也不至于打穿内网,数据库等敏感资源不必暴露给 DMZ 区。就算 DMZ 区某个应用被攻破,攻击者拿到的也只是一套隔离环境里的容器,想横向移动进内网,中间还隔着防火墙和两套独立的集群。
我见过不少团队为了省成本只建一套集群,然后用 namespace 硬隔离内外网应用,结果每次安全审计都提心吊胆。如果业务上有明确的内外网边界要求,两套独立环境基本是没得商量的选项,这不是技术洁癖,是安全边界问题。
2.2 权限管理:认证、鉴权与 SCC 细粒度控制的落地
企业级应用平台会有来自企业内外不同角色的用户,所以灵活的、细粒度的、可扩展的权限管理是必不可少的。OCP 从设计初期就考虑到企业级用户的需求,在平台内部集成了标准化的认证服务器,并且定义了详细的权限策略和角色。
认证层面,OCP 平台的用户是基于对 OCP API 的调用权限来定义的。因为 OCP 所有的操作都是基于 API 的,用户可以是开发人员或者管理员,直接和 OCP 进行交互。OCP 内置了一个基于 OAuth 的通用身份认证服务器,这个 OAuth 服务器可以通过多种不同类型的认证源对用户进行认证,比如 LDAP、AD 或者 GitHub 这类外部身份源。
鉴权层面,权限策略决定了一个用户是否具有对某个对象的操作权限。管理员可以设置不同规则和角色,对用户或者用户组赋予一定的角色,角色包含了一系列的操作规则。这就是典型的 RBAC 模型。除了传统的认证和鉴权功能,OCP 还提供了针对 Pod 的细粒度权限控制 SCC(security context constraints),可以限制 Pod 具备何种类型的权限,比如:容器是否可以运行在特权模式下、是否可以挂载宿主机的目录、是否可以使用宿主机的端口、是否可以以 root 用户运行。
这里有个常见的误解:很多人觉得配好 RBAC 就万事大吉,其实 SCC 才是容器安全里最容易漏的一环。我见过有团队为了让某个监控 Agent 能读宿主机指标,直接把整个 namespace 的 SCC 放开成 privileged,结果所有应用都能以 root 跑任意特权容器,这在生产环境里等于把门锁拆了。
2.3 多租户隔离:从 namespace 到 Project 的四层隔离
租户是指多组不同的应用或者用户同时运行在一个基础资源池之上,实现软件、硬件资源的共享。为了安全需求,平台需要提供资源隔离的能力。在 OCP 中,project 是一个进行租户隔离的概念,它来源于 Kubernetes 的 namespace,并对其进行了功能扩展。利用 Project,OCP 平台从多个层面提供了多租户的支持。
第一层是权限控制。通过细粒度的权限管理机制,管理员可以对不同的用户和组设置不同 project 的权限,不同用户登录以后只能操作和管理特定的 project。这个和上一节讲的 RBAC 是配套的,project 就是权限的作用域。
第二层是网络隔离。OCP 平台使用 openvswitch 来管理内部的容器网络,提供两种类型的网络模式:一种是集群范围内互通的平面网络,另一种是 project 级别隔离的网络。每个 project 都有一个虚拟网络 ID(VNID),不同 VNID 的流量被 openvswitch 自动隔离,所以不同项目之间的服务在网络层不能互通。
这里要特别提醒一下:默认安装的 OpenShift 用的是 ovs-subnet 插件,网络实现类似于 flat 网络,所有 Pod 都能互通。要实现多租户隔离,必须在安装时显式指定插件参数:
os_sdn_network_plugin_name='redhat/openshift-ovs-multitenant'这个参数必须在安装阶段就定好,装完再切网络插件是非常痛苦的过程,基本等于重装集群。很多团队前期图省事用了默认的 flat 网络,等业务方开始抱怨“我们的服务怎么被别人调用了”的时候才想起来要隔离,这时候就陷入两难:重装伤筋动骨,不重装审计过不了。
第三层是 Router 隔离。Router 是 OCP 平台的一个重要软件资源,它提供了外部请求导入 OCP 集群内部的能力。OCP 提供了 Router 分组的功能,不同的 project 可以使用独立的 Router,不互相干扰,这样就避免了由于某些应用流量过大时对其他应用造成干扰。这一点在外网访问量差异大的场景里很实用——流量大的业务用独立的 Router,不会把其它业务的入口带宽吃掉。
第四层是物理资源池隔离。在多租户的环境中,为了提高资源的利用率,一般情况下物理资源池是共享的,但是有些用户也会提供独占资源池的需求。针对这种类型的需求,OCP 平台利用 nodeSelector 的功能可以将基础设施资源池划分给特定的 project 独享,实现从物理层面的隔离。部署应用时给节点打标签,再在 Deployment 里指定 nodeSelector:
apiVersion: apps/v1 kind: Deployment metadata: name: dmz-app spec: replicas: 2 template: spec: nodeSelector: zone: dmz这个 YAML 的意思是:这个应用只会被调度到带有zone=dmz标签的节点上。在双环境方案里,DMZ 区的计算节点打上zone=dmz的标签,内网节点打上zone=intranet的标签,应用部署时按安全级别指定目标节点,从物理层面保证隔离。注意 nodeSelector 匹配不到节点时 Pod 会一直处于 Pending 状态,排查调度问题时要先看节点标签是否打对了。
3. 日志与监控:EFK 落地方案和监控栈的选型对比
3.1 日志分类与持久化:传统应用日志怎么进 NFS
传统应用日志有别于流行的容器应用,传统应用同时一个中间件会运行多个应用,且应用通过 log4j 等机制保存在文件中方便查看和排错。因为容器运行的特性,对于这部分的日志需要持久化到外置存储中。
日志分类大概有三类:中间件日志、dump 文件、应用日志。日志保存在计算节点上挂载的 NFS 存储。为了规范和方便查找,日志按 OCP 平台中的 namespace 建立目录进行划分。这样设计的好处是:一个 namespace 对应一个业务线,日志目录和业务对得上,出了问题直接去对应目录捞日志,不用在一堆容器目录里翻。
这里有一个关于存储的细节值得注意,传统应用日志恰恰不能只靠 stdout。很多人一上容器就习惯用docker logs看日志,但传统中间件(比如 WebLogic、MQ)的日志是写文件的,dump 文件更是要落盘的,Pod 一重建文件就没了。所以这类日志必须挂 NFS 这类外置存储,并且要规划好目录结构。我见过一套比较省心的目录划分方式:
| 日志类型 | 存储位置 | 目录划分 |
|---|---|---|
| 中间件日志 | NFS | /logs/ / / |
| dump 文件 | NFS | /dumps/ / / |
| 应用日志 | NFS | /applogs/ / / |
| 新应用 stdout 日志 | EFK | 由 Fluentd 收集索引 |
目录按 namespace 分的做法我比较认同,因为 namespace 天然对应业务边界,运维同学查日志时不需要知道具体 Pod 在哪台机器上,只需要知道业务属于哪个 namespace,路径就确定了。
3.2 EFK 日志平台:Fluentd 为什么能成为容器日志标准
分布式环境下日志分散的解决办法是收集日志,将其集中到一个地方。收集到的海量日志需要经过结构化处理,进而交给需要的人员分析,挖掘日志的价值信息。同时不同的人员对日志的需求是不一样的:运营人员关注访问日志,运维人员关注系统日志,开发人员关注应用日志。这就需要有一种足够开放、灵活的方法,让所有关心日志的人在日志收集过程中对其定义、分割、过滤、索引、查询。
OpenShift 使用 EFK 来实现日志管理平台,EFK 是 Elasticsearch + Fluentd + Kibana 的简称。ES 负责数据的存储和索引,Fluentd 负责数据的调整、过滤、传输,Kibana 负责数据的展示。这个平台具备以下能力:日志采集,将日志集中在一起;索引日志内容,快速返回查询结果;具有伸缩性,在各个环节都能够扩容;强大的图形查询工具、报表产出工具。
Fluentd 无论在性能上还是在功能上都表现突出,尤其在收集容器日志领域更是独树一帜,成为众多 PaaS 平台日志收集的标准方案。它之所以能成为标准,核心原因是它对容器环境的适配做得很好:Kubernetes 的 Pod 是动态创建销毁的,Fluentd 通过监听 Docker 的元数据,能够自动感知新起的容器并开始收集日志,不需要人工配置日志源。这一点在微服务频繁发布扩容的场景里非常重要——你不可能每次发布都手动改一遍日志采集配置。
在明确要上 EFK 的时候,有个部署方式的问题需要提前想清楚:ES 集群在 K8S 里面怎么跑。这里有两条路,一条是用 ansible 之类的工具自动化部署 ES 集群,另一条是直接跑在 K8S 里。社区里一个比较一致的建议是:不建议 elasticsearch 采用分布式存储,日志量大情况下如果是分布式存储,ES 写会是瓶颈。也就是说,ES 的数据卷宁可挂在本地盘或者传统集中存储上,也不要图省事扔给 Ceph 这类分布式存储。分布式存储的延迟和写放大对 ES 这种写密集型的场景很不友好,我们内部做过压测,同样的写入量,分布式存储在高峰期延迟能差出好几倍。
3.3 监控技术栈:从 heapster 到 Prometheus 的演进
PaaS 平台的监控包括系统监控、容器监控等。监控流程由信息收集、信息汇总和信息展示等几个部分组成。在 OpenShift 中默认使用 Kubernetes 的监控信息收集机制,在每个节点上部署 cAdvisor 的代理,负责收集容器级别的监控信息。然后将所有信息汇总到 heapster,heapster 后台的数据持久化平台是 Cassandra,最后由 hawkular 从 Cassandra 获取信息进行统一的展示。
监控组件的作用是对 Pod 运行状态的 CPU、内存、网络进行实时监控,和 Kubernetes 使用的监控技术栈一样,包括三个部分:
| 组件 | 作用 | 备注 |
|---|---|---|
| Heapster | 监控数据的采集和汇总 | 调各节点 kubelet 内置 cAdvisor 的接口 |
| Hawkular Metrics | 监控数据的存储与展示 | 基于 JSON 格式管理、展示监控数据 |
| Cassandra | 监控数据的持久化 | 专门用于处理大数据量业务 |
这套监控方案在早期是主流,但演进非常快。到了 OpenShift 3.12 之后,heapster 被 Prometheus 替换掉,这基本成了后续的事实标准。Prometheus 作为一个时间序列数据收集、处理、存储的服务,能够监控的对象必须直接或间接提供 Prometheus 认可的数据模型,通过 HTTP API 的形式发出来。cAdvisor 支持 Prometheus,同样包含了 cAdvisor 的 kubelet 也支持 Prometheus,每个节点都提供了供 Prometheus 调用的 API。Prometheus 支持通过调用 Master 的 API Server 获取节点信息,然后去调取每个节点的数据。
如果现在新起一个 K8S 集群,我建议不要纠结 heapster 了,直接上 Prometheus + Grafana。采集层用 nodeExporter 收集主机监控信息,cAdvisor 收集容器监控信息,Prometheus 负责时序数据的存储和查询,Grafana 做展示。这套组合是目前社区里最主流的开源监控方案,社区资料多,遇到问题能搜到大量现成的排错经验。
4. 服务发布与负载均衡:从 ClusterIP 到 Router 的完整链路
4.1 Service 与服务发现:ClusterIP、Endpoints 的工作原理
K8S 中微服务化的应用每一个组件都以 Service 进行抽象,组件与组件之间只需要访问 Service 即可互相通信,而无须感知组件的集群变化,这就是服务发现。Service 的核心价值是解耦动态变化的 Pod IP——Pod 可以随意关停,IP 可以任意变,只要 DNS 正常,服务访问不受影响。
Service 在逻辑层面上被认为是真实应用的抽象,每一个 Service 关联着一系列的 Pod。在物理层面上,Service 是真实应用的代理服务器,对外表现为一个单一访问入口,通过 k8s Proxy 转发请求到 Service 关联的 Pod。Service 根据 Label Selector 来筛选 Pod 进行关联,实际上 K8S 在 Service 和 Pod 之间通过 Endpoint 衔接。Endpoints 同 Service 关联的 Pod 相对应,可以认为是 Service 的服务代理后端,K8S 会根据 Service 关联到的 Pod 的 PodIP 信息组合成一个 Endpoints。
K8S 分配给 Service 一个固定 IP,这是一个虚拟 IP(也称 ClusterIP),并不是一个真实存在的 IP,而是由 K8S 虚拟出来的。虚拟 IP 的范围通过 K8S API Server 的启动参数--service-cluster-ip-range=19.254.0.0/16配置。虚拟 IP 属于 K8S 内部的虚拟网络,外部是寻址不到的。在 K8S 系统中,实际上是由 kube-proxy 组件负责实现虚拟 IP 路由和转发的,所以每个 Node 中都必须运行 kube-proxy,从而在容器覆盖网络之上又实现了 K8S 层级的虚拟转发网络。
一个最基础的 Service 定义长这样:
apiVersion: v1 kind: Service metadata: name: payment-service spec: selector: app: payment ports: - protocol: TCP port: 8080 targetPort: 8080这个 Service 会把所有带app=payment标签的 Pod 的 8080 端口抽象成一个稳定的 ClusterIP:8080 入口。其它微服务访问支付服务时,只需要访问payment-service这个 DNS 名,K8S 的 DNS 组件会解析到 ClusterIP,再经过 kube-proxy 转发到具体 Pod。这里要留意的坑是:Service 的port是服务对外暴露的端口,targetPort是容器内实际监听的端口,两者可以不一样。如果容器内应用改过监听端口而 Service 没有同步改targetPort,就会出现服务通但实际报错的玄学问题。
另外,Service 不仅可以代理 Pod,还可以代理任意其他后端,比如运行在 K8S 外部的服务。假设现在要使用一个 Service 代理外部 MySQL 服务,不用设置 Service 的 Label Selector,而是手动创建 Endpoints 指向外部数据库的 IP。这个技巧在数据库迁到容器外、或者微服务要访问遗留系统的场景里非常实用,不用改代码就能把外部服务抽象成集群内的 Service。
4.2 外部访问:NodePort、LoadBalancer 与 Router 的取舍
K8S 提供了 NodePort Service、LoadBalancer Service 和 Ingress 三种方式发布 Service。三者适用的场景差异非常大,选错了后续维护会很痛苦。
NodePort Service 是类型为 NodePort 的 Service,K8S 除了会分配给 NodePort Service 一个内部的虚拟 IP,另外会在每一个 Node 上暴露端口 NodePort,外部网络可以通过 [NodeIP]:[NodePort] 访问到 Service。OpenShift 产品推荐通过 NodePort 类型的 Service 为某个应用对外暴露一个服务端口。NodePort 类型的 Service 会在集群中的所有节点上监听一个特定的端口,访问任意一个计算机节点的端口,即可访问内部容器中的服务。在集群的所有节点的这个端口都会预留给该应用所用。
这里要解释一下为什么说“在集群的所有节点都会预留给该应用”:NodePort 的端口分配是集群级的,一旦 Service 占用了某个端口,所有节点都会监听这个端口。这意味着 NodePort 的端口资源是有限的(默认范围 30000-32767),如果应用数量多,端口会不够用。另外,用户访问任意节点 IP 加端口都能访问到服务,如果其中一台节点挂了,用户访问这台节点的端口就会失败。
所以生产环境一般不在 NodePort 上裸奔,而是用 F5 这类负载均衡设备做前端入口。在 F5 VS 的 Pool Member 中配置所有节点,通过 Keepalived 来实现 HA。外部流量先打到 F5 虚拟地址,F5 转发到各节点 NodePort,再进入集群内部的 Service。应用系统和用户不用改变现有的访问方式,这对接入层的平滑过渡非常重要。
LoadBalancer Service 是类型为 LoadBalancer 的 Service,它是建立在 NodePort Service 集群基础上的,K8S 会分配给 LoadBalancer Service 一个内部的虚拟 IP,并且暴露 NodePort。除此之外,K8S 请求底层云平台创建一个负载均衡器,将每个 Node 作为后端,负载均衡器将转发请求到 [NodeIP]:[NodePort]。这个类型需要底层云平台支持创建负载均衡器,比如 GCE、AWS 这些公有云,或者企业内部有对接好的 LB 插件,否则创建出来的 LoadBalancer Service 会一直处于 Pending 状态。
在 OpenShift 里,外部访问的入口是 Router。Router 是平台的一个重要软件资源,它提供了外部请求导入 OCP 集群内部的能力,如果应用有多个容器实例,Router 也可实现负载均衡的功能。Router 会动态地检测平台的元数据仓库,当有新的应用部署或者应用实例发生变化时,Router 会自动根据变化更新路由信息,从而实现动态负载均衡的能力。这就是 Router 比 NodePort 更合适做微服务入口的原因:Pod 伸缩、重建、迁移都不需要人工干预路由配置,Router 自动感知并更新。
4.3 高可用设计:镜像仓库、Master、计算节点的三层保障
高可用主要分为外部镜像仓库高可用、Master 主控节点高可用、计算节点(容器应用)高可用和应用高可用几个层面。群里讨论的时候有人问“K8S 的负载均衡策略和总体思想是什么”,其实拆开看就是这四层。
外部镜像仓库独立于 OCP 平台之外,用于存储平台构建过程中所使用的系统组件镜像。因为外部无法直接访问 OCP 平台的内部镜像仓库,所以由 QA 环境 CD 推送到生产环境的镜像也是先复制到外部镜像仓库,再由平台导入至内部镜像仓库。为了保证外部镜像仓库的高可用,使用了 2 台服务器,前端使用 F5 进行负载均衡,所有的请求均发至 F5 的虚拟地址,由 F5 进行转发。后端镜像仓库通过挂载 NFS 共享存储。这个方案说白了就是“无状态服务 + 共享存储”的经典组合,镜像仓库本身是应用层无状态的,共享存储保证镜像数据不丢,F5 保证入口不挂。
Master 主控节点承担了集群的管理工作,它的高可用是整个集群的命脉。Master 挂了,调度、API 调用全部中断。生产环境一般至少三台 Master,配合 etcd 集群做数据一致。这里有个经验之谈:Master 节点千万不要省资源,etcd 的磁盘 IO 性能直接影响整个集群的稳定性,有条件上 SSD 就上 SSD。
计算节点高可用指计算节点上运行的容器应用的高可用。一个计算节点异常停机后,其上的容器将会被逐步迁移到其他节点上,从而保证了高可用。同时可以通过标签的方式来管理计算节点,在不同的计算节点划分为不同的可用区或组。在部署应用时,使用节点选择器将应用部署至带有指定标签的目标计算节点上。为了保证高可用,标签组合的目标计算节点数要大于 1,这样可以避免一台目标节点宕机后,调度器还能找到满足条件的计算节点进行容器部署。这个细节特别重要——很多人给应用只配了一个节点,当时看起来没问题,等节点宕机才发现调度器找不到第二个满足条件的节点,应用直接停摆。
应用高可用的核心是基于软件(HAproxy)负载均衡服务,容器服务弹性伸缩时无需人工对负载均衡设备进行配置干预,即可保证容器化应用的持续、正常访问。可通过图形界面自定义负载均衡会话保持策略。因为平台内部通过软件定义网络为每个应用容器分配了 IP 地址,而此地址是内网地址,外部客户无法直接访问到该地址,所以平台使用路由器转发外部的流量到集群内部具体的应用容器上。
业务方内部的访问则是 service 驱动的:内部服务之间访问通过 Service 解决了,外部访问集群内服务通过 Router 解决。大规模高并发情况下外部负载均衡是肯定的,外部负载均衡通常需要用户自己搞定,F5 或者开源的 HAproxy 都行。
5. 部署避坑与排查:认证、网络、存储和监控的常见问题
这个章节把我在实际部署和运维过程中踩过的坑集中写一下,每条按“现象 → 原因 → 解决”来拆。
5.1 集群安全配置:双向认证的流程与性能权衡
现象:集群组件之间通信走 HTTP,安全扫描一问一个准;但配了全链路 HTTPS 双向认证之后,API Server 的响应延迟明显上升,压测数据不好看。
原因:Kubernetes 系统提供了三种认证方式:CA 认证、Token 认证和 Base 认证。安全功能是一把双刃剑,它保护系统不被攻击,但是也带来额外的性能损耗。集群内的各组件访问 API Server 时,由于它们与 API Server 同时处于同一局域网内,建议用非安全的方式访问 API Server,效率更高。
解决:双向认证配置方式是最为严格和安全的集群安全配置方式,主要配置流程分两步:第一步,生成根证书、API Server 服务端证书、服务端私钥、各个组件所用的客户端证书和客户端私钥;第二步,修改 Kubernetes 各个服务进程的启动参数,启用双向认证模式。如果不想引入证书体系的复杂度,可以退一步用基于 Token 和 HTTP Base 的简单认证方式,通信方仍然采用 HTTPS,但不使用数字证书。采用基于 Token 和 HTTP Base 的简单认证方式时,API Server 对外暴露 HTTPS 端口,客户端提供 Token 或用户名、密码来完成认证过程。
我的建议是:API Server 对外的管理面必须走 HTTPS 加认证,集群内部组件之间的通信可以用 Token 方式代替双向证书,性能和安全的平衡点在这里比较合适。有些团队一上来就全链路双向认证,性能损耗先不说,证书轮换本身就是个巨大的运维负担,证书过期导致集群失联的事故我见过不止一次。
5.2 网络隔离翻车:VNID、防火墙与 overlay on overlay
现象:开启了 ovs-multitenant 多租户插件后,某些跨 project 的服务调用变得不通;或者把 OpenShift 部署在已有云平台上之后,容器网络性能下降明显。
原因:多租户网络隔离是靠 VNID 实现的,每个 project 有一个虚拟网络 ID,不同 VNID 的流量被 openvswitch 自动隔离。如果两个 project 之间有服务调用需求(比如公共组件),默认策略下网络层直接不通。而且,如果利用已有的公有云或私有云部署 OpenShift,使用 OVS 插件时,OpenShift 中的 SDN 可能出现 overlay on overlay 的情况——底层 IaaS 已经做了一层 overlay,容器网络又是一层 overlay,两层封装叠加导致网络性能下降。
解决:网络层面的多租户隔离要提前规划好哪些项目之间需要互通,在开启多租户插件时预先配置好相应的网络策略。对于 overlay on overlay 的问题,借助三方 SDN 插件是个不错的选择,比如 flannel + hostgw 在性能上就优于默认的 ovs-multitenant,因为 hostgw 模式走的是宿主机路由,不叠加封装。如果性能要求更高,calico 基于 BGP 的方案更佳,但基础设施改动大,不是所有客户都能接受。原则就是尽量防止二次封装叠加,致使网络性能下降过多。
5.3 ES 存储选型踩坑:分布式存储为什么可能是瓶颈
现象:EFK 日志平台上线后,Kibana 查询响应慢,ES 集群写入高峰期 CPU 打满,日志出现积压,Fluentd 上游不断重试。
原因:ES 是典型的写入密集型应用,每次写入涉及索引、分片复制、刷新到磁盘一系列操作。如果后端用的分布式存储(比如 Ceph),每次写入都会产生多副本的网络传输和确认,延迟被放大。社区里的共识是:不建议 elasticsearch 采用分布式存储,日志量大情况下如果是分布式存储,ES 写会是瓶颈。
解决:ES 的数据卷尽量用本地盘或者传统集中存储。虽然分布式存储有弹性伸缩、无中心节点的优势,但对于 ES 这种场景,写放大太致命了。日志量大时本地盘的 SSD 会明显改善写入性能。要控制好日志保留策略,索引按天滚动,老索引定期删除或者归档到冷存储,别让 ES 集群无限膨胀。ES 集群本身不要在容器里跑,除非对 ES 容器化运维非常有把握,否则独立部署更省心。
5.4 节点调度的隐形坑:nodeSelector、标签与资源预留
现象:应用部署后 Pod 一直处于 Pending 状态,describe 看事件只有一句 “0/8 nodes are available”,没有更详细的提示。
原因:大概率是 nodeSelector 指定的标签没有任何节点匹配,或者匹配到的节点资源不足。物理资源池隔离是依赖 nodeSelector 实现的,如果节点没有打上对应标签,或者标签值写错(比如大小写不一致),调度器就找不到可用的节点。另外,有些团队只给应用分配了一个目标节点,节点宕机后调度器找不到第二个满足条件的节点,应用直接停摆。
解决:排查步骤按顺序来:先kubectl get nodes --show-labels确认节点标签;再kubectl describe deployment <name>看 selector 和 template 是否一致;最后用kubectl describe pod <name>看 Pending 原因。生产环境部署时,标签组合的目标计算节点数要大于 1,给应用至少留两个满足调度条件的节点,避免单节点故障时无家可归。
还有资源预留的问题:K8S 节点上的 kubelet 需要配置kube-reserved和system-reserved参数给系统预留内存。如果不预留,Pod 可以把节点内存吃满,最后 kubelet 和系统组件因为资源不足开始 OOM,表现为节点状态频繁 NotReady,Docker 容器批量被杀。这个坑在压测的时候最容易暴露,一压测节点就挂,查了半天才发现是系统内存没预留。
5.5 监控数据正常但告警不触发:cAdvisor 与告警规则配置
现象:Grafana 上看 CPU、内存曲线都正常,但配置好的告警规则就是不触发,或者触发之后重复报警,值班同学被骚扰得不行。
原因:cAdvisor 收集的数据是实时的,而告警判断针对的是连续时间窗。很多人在 Prometheus 里配置告警规则时,把for字段设置得太短,瞬时抖动就触发了;或者把阈值设得太贴近基线,正常波动就会来回触碰。还有一种是团队直接沿用了传统监控的思路,只在主机层面配了告警,没有覆盖容器层面,Pod 频繁重启时主机指标看着完全正常,但容器已经在大规模 CrashLoopBackOff 了。
解决:告警规则要设置合理的for持续时间和触发阈值。一般建议 CPU 使用率阈值在 85% 以上并持续 5 分钟才告警,避免瞬时峰值干扰。同时把重复告警的 group_interval 调大,比如 15 分钟,防止同一问题反复刷屏。另外监控存储的数据要结合 Prometheus 的 nodeExporter 和 cAdvisor 一起看,主机层面的指标(磁盘 IO、网络带宽)和容器层面的指标(CPU、内存)要分开配告警,维度不同告警阈值也应该不一样。Pod 层面的重启次数、CrashLoopBackOff 状态这类 K8S 事件也要单独接一条告警链路,别只盯着资源曲线。
6. 微服务拆分与 CI/CD:把单体拆成可独立发布的服务
微服务架构按照什么细粒度拆分,这是群里被问得最多的问题。老实说这没有标准答案,拆多拆少都可能翻车。比较靠谱的切入点是先按业务功能划分,得到粗粒度模块,比如计算服务、网络服务、存储服务;然后再基于服务模块的“原子性”拆分,拆分到不能再往下拆为止,拆完后通常就是彼此独立的单进程。
想找参考案例的,强烈推荐去看看 OpenStack 中的 Kolla 项目。Kolla 干的事情就是把 OpenStack 服务拆分成微服务的形式跑在容器中。OpenStack 号称全球最大开源 Python 项目,由几十个开源子项目组成,如果能把这样复杂的集群项目都拆分成微服务,你一定会得到很多别人给不了的心得体会。Kolla 的拆分思路从镜像依赖上看得非常清楚:父镜像 centos-base → 一级子镜像 centos-openstack-base → 二级子镜像 centos-nova-base → 叶子节点镜像 centos-nova-api。粗粒度模块共享 base 镜像,原子化拆分后的进程使用自己专属的叶子镜像。这个思路拿到业务系统里照样成立:先把系统模块化解耦,接口化、动静分离(查询和修改分开)、元数据抽取,这些都是拆分的真功夫,而不是简单按代码目录切一刀。
拆分完了,CI/CD 就要跟上。SVN 环境下能不能做 CI/CD?能,但看你怎么触发。SVN 可以用 hook(post commit)的方式来实现,但需要编写 hook 脚本,灵活度存在问题;这在 svn-repo 的粒度较细的情况下还可行,如果是一个大的 repo,管理起来较复杂,不建议使用。建议使用 Jenkins 轮询 SCM 的方式触发 pipeline/job。能不能实现 CI/CD 与 SVN 无关,关键是你如何构建 pipeline。微服务理念下大致这样:gitlab/svn → Jenkins → build images → push images → docker-registry → pull images → containers。用 Jenkins Pipeline 来描述这个流程,大概是这样的结构:
pipeline { agent any stages { stage('Checkout') { steps { checkout scm } } stage('Build Image') { steps { sh "docker build -t registry.example.com/order-service:${BUILD_NUMBER} ." } } stage('Push Image') { steps { sh "docker push registry.example.com/order-service:${BUILD_NUMBER}" } } stage('Deploy') { steps { sh "kubectl set image deployment/order-service order-service=registry.example.com/order-service:${BUILD_NUMBER}" } } } }这个流水线把代码提交到镜像构建再到 K8S 滚动更新串起来了。注意最后一步用的kubectl set image会触发 Deployment 的滚动更新,而不是直接删 Pod,这样能保证发布过程中服务不中断。
最后说一个 Dubbo 场景里非常典型的问题:如果 K8S 上的应用是 provider,注册到 ZooKeeper 时是容器地址,这时如果 consumer 在集群外面,根本访问不到容器 IP。只靠 K8S 的 Service 也没法直接解决,因为 Service 的 ClusterIP 也是集群内虚拟地址,外部同样寻址不到。我们对 Dubbo 这类需要服务注册中心的老服务上 K8S 的常规处理是:provider 侧不注册容器 IP,而是注册宿主机 IP 加 NodePort 端口,或者引入 Dubbo 的 K8S 注册中心插件,让 provider 注册 Service 的 ClusterIP 地址,然后在集群内部消费。从那以后我每次做微服务容器化改造,都强制走一遍这个清单:服务注册地址是集群外可达的吗?配置中心里有没有写死容器 IP?下线旧实例时注册中心的数据清理干净了吗?这几个问题过一遍,能省掉后面一大半的联调时间。希望帮到你。
本文还有配套的精品资源,点击获取