当前位置: 首页 > news >正文

从 Kubernetes 学习大规模 Go 项目架构

在尝试使用 Go 构建一个具有高可扩展性、高可靠性和高可维护性的大型项目之前,先来看一下 Kubernetes 的项目结构,了解它是如何组织一系列用于容器编排的功能模块的。

Kubernetes 代码布局

下面是 Kubernetes 主目录下的各个顶层目录及其主要功能,随后我们会逐一说明它们的用途。

  • api:存放接口协议;
  • build:与构建相关的脚本和代码;
  • cmd:各个可执行程序的入口;
  • pkg:各组件的核心实现和导出包;
  • staging:临时存放组件间相互依赖的代码。

api

该目录存放 OpenAPI 和 Swagger 文件,包括 JSON 与 Protocol Buffer 的定义。

build

该目录包含构建 Kubernetes 项目所需的脚本,包括各组件的编译和镜像(例如 pause 镜像)的构建。

cmd

cmd 目录存放用于生成可执行文件的 main 包源代码。若需构建多个可执行程序,可将它们分别放在各自的子目录下。以 Kubernetes 为例,cmd 目录下有:

  • kube-proxy:负责网络相关规则的实现 ;
  • kube-apiserver:暴露 Kubernetes API 并处理请求,为 Pod、ReplicaSet、Service 等资源提供增删改查(CRUD)操作;
  • kube-controller-manager:kubernetes 资源控制器;
  • kube-scheduler:监控新建的 Pod,并为其选择运行节点;
  • kubectl:访问集群的命令行工具;

由此可见,像 kube-proxy、kube-apiserver 这样的核心组件,都在这里有对应的可执行入口。

pkg

pkg 目录既包含项目自身的依赖,也包含对外导出的包。主要实现了各个核心组件的业务逻辑。

staging

staging 目录下的包通过符号链接映射到 k8s.io 下。首先,由于 Kubernetes 项目体量庞大,这样做可以避免因仓库分散带来的开发障碍,使所有代码能够在一次 PR 中提交和审查。通过这种方式,既保证了模块化,又保持了主仓库的完整性。

同时,借助 go mod 中的replace指令,无需为每个依赖单独打 tag,简化了版本管理和发布流程。

如果不这么做,而是采用将 staging 目录下的所有代码拆分到独立仓库的方式,那么每次子仓库代码变化时,都需要先在子仓库提交 PR、发布新 tag,然后在主仓库中更新 go mod 依赖,才能继续开发。这无疑会大幅增加整体开发成本。

因此,通过符号链接将 staging 目录下的包关联到主仓库,能够有效简化版本管理和发布流程。

与标准 Go 项目结构的对比

在 Go 中,internal目录用于存放不对外导出的包。它的原则是在项目内部可以正常使用,但外部项目无法访问。

然而,Kubernetes 并没有internal 目录。这是因为 Kubernetes 项目最早启动于 2014 年左右,而 Go 1.4(于 2014 年底发布)才引入了 internal 目录的概念。在 Kubernetes 早期开发阶段,使用 internal 目录的惯例尚未普及,且后来也未进行大规模重构来添加它。

此同时,Kubernetes 的设计目标之一就是模块化与解耦。它通过明确的包组织和代码结构来实现封装,而无需依赖internal=目录来限制包的访问权限。

至此,我们已经了解了构建项目的标准顶层目录结构。

Go 并不像 Java 那样有一套统一的目录框架。因此,不同项目往往各自为政。即使在同一个团队中,也可能存在多种结构,这对新人理解项目是一大障碍。

正因为如此,协作往往会变得困难。统一的顶层目录结构能让我们快速定位代码,并在接手项目时拥有标准的切入点,从而提高开发效率,减少协作中的定位混乱。

但仅仅有统一的目录结构,就能构建完美的大型项目吗?答案显然是否定的。

单靠统一目录结构,无法从根本上解决代码随着项目规模扩大而逐渐“衰变”“混乱”的问题。唯有遵循良好的设计原则,才能在项目不断扩张时始终保持清晰的设计脉络。

声明式设计理念

声明式 API 贯穿 Kubernetes 的整个代码设计,防止系统陷入过程式编程。

例如,当需要改变某个资源的状态时,我们应告诉 K8s “期望的状态”,而不是告诉它 “要执行哪些步骤”。这也是为何 kubelet 的滚动更新最终被废弃:它的设计对更新 Pod 的整个过程进行了过度微观管理。

通过告知 Kubernetes 目标状态,kubelet 可以根据该状态自主采取相应措施,而无需外部过度干预。

此时你可能会疑问:声明式 API 在项目扩展时如何有助于保持模块清晰?这不正是用户在使用Kubernetes 时的感受吗?它与内部设计有什么关系?

如果我们在设计接口时,将整个操作流程完全暴露给用户,让他们逐步干预 Pod 更新的每个步骤,那么我们设计的模块就不可避免地演变为过程式。这会导致代码模块与大量用户操作高度耦合,难以保持清晰。

而采用声明式 API 后,我们仅向 K8s 传达期望状态,集群内部的多个组件便能协同工作,最终实现该状态。用户无需关心内部的更新细节。更重要的是,当需要增加新的协作插件时,只需新增相应模块,无需再对外暴露更多用户操作的 API。

以 cAdvisor 为例,它独立监控 Kubernetes 部署的资源并收集容器指标,不依赖外部组件。控制器再将这些指标与用户声明的目标进行对比,以判断是否满足扩容或缩容条件。

由于各模块相互独立,cAdvisor 只需专注于采集并返回监控数据,而无需关心这些数据是用于观测还是自动伸缩。

这也是设计不同任务组件时的关键原则:

  1. 明确要达成的需求;
  2. 传递信息时只关注输入与输出;
  3. 内部实现则封装起来,不向外暴露,让外部业务调用保持尽可能简单。

避免过度设计

过度的工程设计往往比设计不足更糟糕。

Kubernetes 的最早版本是 0.4。在网络方面,官方的实现方案是让 GCE 运行 Salt 脚本来创建网桥,而在其他环境下推荐使用 Flannel 和 OVS。

随着 Kubernetes 的发展,Flannel 在某些场景下已不足以满足需求。大约在 2015 年,社区中出现了 Calico 和 Weave,基本解决了网络问题。于是 Kubernetes 就不用再花力气自己去做网络实现,而是引入了 CNI 以标准化网络插件。

很显然,Kubernetes 并不是一开始就设计得十分完美,而是在新问题出现时,才逐步引入新的设计来适应不同环境的变化。

在项目启动初期,依赖关系相对清晰,所以工程设计阶段一般不会出现循环依赖。但随着项目规模增长,这些问题会逐渐显现。产品功能的不断演进,会导致代码设计中出现互相引用。

即便我们在项目启动前尽力去了解所有业务背景和待解问题,随着产品功能的变化和程序的迭代,总会出现新的问题。我们能做的,是关注模块设计和依赖管理,尽可能保持功能内聚,并在后续添加抽象时,避免对已有代码进行大规模“重构”式的改动。

为“可扩展性”过度设计系统、只为设计而设计,反而会成为未来变更的绊脚石。

下面用一个电商业务场景来说明设计演进。

初始阶段,系统包含两个模块:

  • 订单模块: 负责创建订单、支付、状态更新等,依赖用户模块获取用户信息(如收货地址、联系方式等)。
  • 用户模块: 负责管理用户信息、注册、登录,存储用户数据,不依赖订单模块。

在这个阶段,依赖关系是单向的:订单模块 → 用户模块。此时无需过度抽象,过早的设计投入并不划算;许多项目并不知道是否能够成功,从产品发布角度看,投入过多设计成本并不可行,而且若产品定位发生剧烈变化,过度设计会成为后续修改的障碍。

随着需求演变,出现了个性化推荐的需求:平台需要根据用户的购买历史(订单记录)向用户推荐商品。

为实现个性化推荐,用户模块需要调用订单模块的 API 来获取用户的历史订单。此时依赖关系变为:

  • 订单模块 依赖 用户模块(获取用户信息)
  • 用户模块 依赖 订单模块(获取订单历史)

这就形成了循环依赖。为了解决它,可以考虑责任拆分:引入一个新的“推荐模块”,专门处理个性化推荐逻辑。推荐模块分别从用户模块和订单模块获取数据,避免它们之间直接依赖。通过提取模块,我们解决了用户与订单模块之间的耦合。

然而,新需求又来了:在促销活动期间,用户购买特定活动商品时,产品经理希望推荐模块能实时感知此类订单,并推荐相关的促销商品(例如用户买了打折运动手表,就推荐打折蓝牙运动耳机,以提高复购率)。

在这个场景中,让订单模块直接调用推荐模块来传递数据显然不合适:推荐模块已经依赖订单模块获取用户购买数据,如果再让订单模块调用推荐模块,就又会产生循环依赖。

那么,推荐模块该如何快速感知订单变化?这就需要事件驱动架构。

  • 当用户下单时,订单模块触发一个事件;
  • 推荐模块订阅与用户订单相关的事件;
  • 通过事件传递数据后,推荐模块立即触发模型重训练,并向用户推荐相关商品。

从上述例子可见,企业级应用的一大挑战是:业务域建模。在建模过程中,需要随着需求的持续演进,不断优化设计。

上面提到的用户、订单与推荐模块,也是大多数 To-C(面向消费者)产品演进中的常见场景。如何在演进过程中持续优化模块设计和代码结构、提升迭代速度,是我们需要深入思考和探索的问题。

总结

让我们回顾下本文内容:

  • 在构建大型项目时,统一的目录结构可以提高协作效率,但良好的设计原则才是在项目不断增长时保持清晰性和可扩展性的关键;
  • Kubernetes 的声明式 API 能让模块保持独立,避免过程式编程的陷阱;
  • 项目设计应根据实际需求逐步演进,避免过度设计;
  • 着重模块职责和依赖的合理拆分,并使用事件驱动的方法解决模块间的耦合。

例行海报:10+ 高质量体系课、15+ 实战项目助你提高技术天花板,入大厂、拿高薪

相关文章:

  • 初级程序员入门指南
  • Psychopy音频的使用
  • (一)单例模式
  • 【Blender】Blender 通过 Python 实现模型大小压缩
  • 作为点的对象CenterNet论文阅读
  • GitHub 常见高频问题与解决方案(实用手册)
  • Compose笔记(二十六)--DatePicker
  • 数据类型 -- 布尔
  • 第二章 无刷电机硬件控制
  • 智警杯备赛--机器学习算法实践
  • 【Linux】gcc、g++编译器
  • 6月8日day48打卡
  • Java线程池核心原理与最佳实践
  • 思澈sdk-新建lcd
  • Linux下GCC和C++实现统计Clickhouse数据仓库指定表中各字段的空值、空字符串或零值比例
  • “图像说话,文本有图”——用Python玩转跨模态数据关联分析
  • 从代码学习深度强化学习 - 多臂老虎机 PyTorch版
  • Cesium快速入门到精通系列教程七:粒子效果
  • Java 中字节流的使用详解
  • 【GESP真题解析】第 18 集 GESP 三级 2025 年 3 月编程题 1:2025
  • 视频上到什么地方可以做网站链接/重要新闻今天8条新闻
  • 好利蛋糕店官方网站/webview播放视频
  • 哪里有网站建设培训班/电话销售如何快速吸引客户
  • 酒泉市建设局网站招标办/网址解析ip地址
  • 阿里云可以建网站吗/百度指数官方
  • 2017网站发展趋势/营销软文代写