Kafka + 微服务
Docker
Docker 也是一个 CS 架构的应用,Docker 由两个核心部分组成:
Docker Daemon 是服务器,也叫 dockerd,负责实际执行所有操作,比如构建镜像、运行容器、管理网络等。
Docker CLI 是客户端,就是平常使用的 docker 命令行工具,用于发送指令给 Docker Daemon。
最常见的模式是本地模式,就是 CLI 和 Daemon 都运行在自己的电脑上。这时的通信方式是 Unix socket / Windows named pipe。
也可能是远程模式,就是 CLI 和 Daemon 不在同一台机器上,通信方式是 HTTP API / SSH / TLS。
docker context use ...
的作用就是切换当前的 Docker 客户端(CLI)要连接哪个 Docker Daemon(服务器)
换句话说,它就是告诉 CLI,你现在要去哪一台 Docker 主机发命令?
执行 docker context use default
就是告诉 Docker CLI,回到默认的 Docker Daemon,通常是本地的 Docker Daemon。
那么,Docker Desktop 是什么?它是一个完整的本地 Docker 平台,其中包含了:
- Docker CLI(客户端)
- Docker Daemon(服务端)
- 图形界面
- WSL2 集成机制...
我们在运行 wsl -l -v
的时候,可能会看到:
1 | NAME STATE VERSION |
这里,wsl -l -v
的命令是列出当前注册在系统上的所有 WSL2 子系统(虚拟机)以及他们的状态。
那么,我们就好理解了,docker-desktop 和 docker-desktop-data 是 WSL2 环境中 Docker Desktop 的专用子系统。docker-desktop 是 Docker Daemon 实际运行的地方,也可以运行容器,它运行了 dockerd。
docker-desktop-data 是持久化数据存储层, 比如卷、镜像、网络设置等。
换言之,Docker Desktop 在后台通过 WSL2 启动了两个轻量 Linux 虚拟机,一个负责运行容器和服务端,一个负责存储数据。
🔁 它们是怎么协同工作的?
当你在 Windows 上安装并运行 Docker Desktop,它会自动:
- 启动
docker-desktop
(运行 Daemon)- 启动
docker-desktop-data
(提供镜像和容器数据持久存储)- 把 Docker CLI 和 Kubernetes CLI 都配置好
所以你输入 docker ps
时,CLI 实际是通过 socket 与 WSL2 中的 docker-desktop
子系统中的 dockerd
通信!也就是说,当我们在 Windows 上运行 Docker 容器时,实际上是:
1 | Windows 系统(物理机) |
容器是基于 Linux 内核的“操作系统层虚拟化”,虚拟机(比如 WSL2)是基于 硬件层的完整虚拟化。所以可以这样理解:容器是“轻量虚拟化”技术,跑在“真正虚拟机”里的进程级虚拟环境。
1 | 物理层:Windows 系统 |
从结构上讲,容器确实跑在虚拟机里,所以你说的:
“Windows 上的容器是虚拟之虚拟” ✅ 是准确的结构理解
minikube
什么是 minikube?minikube 本身不是一个容器,而是一个运行 kubernetes 的本地集群管理器。它会根据我们选择的驱动(如 Docker、WSL2、VirtualBox)来创建一个 Kubernetes 节点的运行环境,这个环境可能是:
- 一个容器、一个 VM、一个 WSL2 子系统。
在 Docker 驱动下,Minikube 会创建一个大容器,里面运行完整的 Kubernetes 节点。这个容器就是我们的集群。这个实际结构图是如下所示的:
1 | Windows |
K8s 的基本单位是 Pod,一个 Pod 只运行一个容器,K8s 目前使用的容器运行引擎是 containerd:轻量、稳定、专为 K8s 设计(其实 Docker 背后的引擎也是它)。在 1.20 以前的 K8s 中,K8s 使用 Docker 引擎,但目前的 K8s 已经废弃了对 Docker 引擎的支持。Kubernetes 最早是通过一个叫 dockershim
的适配器来调用 Docker 运行容器的。
Kubernetes 并不直接运行容器,它通过一个叫 CRI(Container Runtime Interface) 的接口来调用底层容器运行时。
✅ 支持的容器运行时(也就是 CRI 实现)有:
容器运行时 | 是否受支持 | 特点 |
---|---|---|
containerd |
✅ 官方推荐 | 轻量、稳定、由 Docker 团队分离出来的核心 |
CRI-O |
✅ 推荐 | Red Hat 主推,专为 Kubernetes 打造 |
Docker |
❌(已弃用) | 必须通过 dockershim 才能支持,现已移除 |
Mirantis Docker Engine |
⚠️(需要额外插件) | Mirantis 接管了 dockershim 的维护 |
但是,K8s 虽然不再用 Docker 运行容器,但是它依然需要我们提供符合 OCI 标准的容器镜像,而 Docker 恰好是构建这种镜像最主流、最方便的工具。
阶段 | 工具 | 说明 |
---|---|---|
🛠️ 构建阶段 | Docker CLI / Dockerfile | 开发者将微服务代码 + 依赖 + 环境 打包成镜像 |
📦 镜像格式 | OCI 标准(如 .tar 或远程镜像) |
Kubernetes 通过容器运行时(如 containerd)运行它 |
🚀 部署阶段 | Kubernetes | 使用 Deployment , Pod , Service 等来部署、调度这些镜像 |
我们还是应该用 Docker 构建微服务镜像,然后交付给 K8s 去运行它。
你有一个微服务架构(比如 10 个服务),你通常会这么做:
每个服务目录下写一个
Dockerfile
使用 CI/CD 或本地 Docker 命令打包:
1
docker build -t my-service-a:v1 .
上传到镜像仓库(Docker Hub、Harbor、阿里云镜像仓库等):
1
docker push my-service-a:v1
在 Kubernetes 中部署(Deployment YAML):
1
2
3containers:
- name: service-a
image: my-service-a:v1
Kubernetes 不再依赖 Docker 来运行容器,但 Docker 仍然是构建、打包、测试微服务的核心工具,完全可以、也应该继续使用。
Kubernetes
容器的数量很多,如何管理和维护这些容器成为了很大的挑战,比如一千个容器。
Kubernetes 就是用于管理容器的,是 Google 开源的。
K8s 如何解决这些问题?
高可用:系统在长时间内持续正常运行,并不会因为某一个组件或者服务的故障,导致整个系统不可用。
K8s 自动重启、自动重建、自我修复等,可以帮助提高集群的可用性,从而让用户在任何时间内正常地使用系统。
可扩展性:系统根据负载的变化,动态扩展或者缩减资源,从而提高系统性能。
每年双11,各大电商平台就会根据负载的变化来动态地扩展或者缩减系统的资源。比如缩减一些不太重要的服务资源,增加一些关键的服务资源,保证系统平稳度过流量高峰阶段。
灾难恢复、弹性伸缩...
这些特性可以提升应用系统的性能。
K8s 中的组件对象。
一个 Node 节点就是一个物理机或者一个虚拟机,在这个节点上,我们可以运行一个或者多个 Pod,Pod 是 K8s 的最小调度单元。一个 Pod 就是一个容器或者多个容器的组合。在这个 Pod 环境中,容器可以共享一些网络、存储、运行时资源。
假如我们的系统包含一个应用程序和数据库,就可将一个应用程序和一个数据库分别放到两个 Pod 中,建议一个 Pod 放置一个容器。放置多个容器时,是高度耦合的服务。Sidecar 边车模式,建一个主服务容器+日志配置容器放到一起。
应用程序要访问数据库时,就需要数据库的 IP 地址,创建 Pod 时会创建一个集群内部的 Pod 地址,可以相互访问。但是 Pod 并不是一个稳定的实体,会频繁创建和销毁。因此这个 IP 并不是固定的。
K8s 提供了一个叫作 Service 的对象,svc 可以将一组 pod 封装成一个统一的服务。应用程序可以根据 service 的地址,来访问数据库。service 的 IP 地址不会变化,service 会将请求自动转发到 Pod 上。有内部服务,比如内部的 Node 数据库,这些服务只会在 Node 的内部使用。有些服务会暴漏给外部,比如给用户的前端界面,或者微服务的 API。
一种类型的服务叫作 node:port,会把节点端口映射到内部 service 的端口上。
在生产环境中,如果通过域名呢?Ingress 是另一个对象,用来管理从集群外部访问集群内部的。还可以通过 Ingress 来配置域名。
如何解耦数据库服务和应用程序服务?如果数据库的端口变化了呢?
ConfigMap 可以封装一些信息,保持容器化应用程序的可移植性。当数据库的地址变化了,只需要修改 ConfigMap 对象的配置信息,然后重新加载 Pod,不需要重新编译和部署应用程序。实现应用程序和数据库的解耦。
ConfigMap 中的配置信息都是明文的,敏感信息不建议存储在 ConfigMap 中。
K8s 提供了另一个叫作 Secret 的组件,但是它可以将一些敏感信息封装起来,可以在应用程序中读取和使用。像是 Github Actions 中。
K8s 还提供了很多安全方式。
容器被销毁后,容器中的数据也会消失。怎么解决持久化呢?K8s 提供了一个叫作 Volume 的组件,将一些持久化的资源挂载到集群中的本地磁盘中,或者磁盘外部的存储上,实现了容器中数据的持久化存储。
程序可以运行在 K8s 中了,那么高可用性,怎么办呢?解决方案很简单,只有一个节点不行,那么就多加几个节点,把所有东西都复制一份,放到另外一个节点上。这样,当一个节点坏了,svc 将请求自动转发到另一个节点上。
deploy 组件就是用来解决这个问题的,它可以简化应用程序的部署和副本数量。
deploy 可以理解为在 pod 上的更多一层抽象,将一个或者多个 pod 组合到一起,有很多自动缩容的高级特性。
副本控制,可以定义应用程序的副本数量,比如把一个 pod 复制 3 个,自动创建一个新的副本替代他,始终保持有 3 个副本在集群中运行。
稳定更新,可以轻松地升级应用程序的版本,逐渐使用新的版本来替换掉旧的版本。确保平滑升级。
数据库不适用 deployment,因为各个副本状态可能不一致。比如把数据写入到统一个存储中,或者共享。 k8s 提供了 statefulset,同样可以定义副本数量等,但它保证了每个副本独立存储?
更简单地是把数据库拨出来。
架构
K8s 是一个典型的 Master-Worker 架构,Master-Node 负责管理整个集群,Worker-Node 负责运行应用程序和服务。K8s 会将容器放在 Node 的 Pod 中来运行应用程序。每个 Node 包含三个组件,Kubelet、kube-proxy、container-runtime。container-runtime 就是运行容器的软件环境把,拉取镜像运行容器等。每个工作节点都必须安装容器运行时。Docker-Engine:Docker 中的容器运行时。除了 Docker-Engine,在 K8s 可以使用各种容器运行时。kubelet 负责管理每个 Node 上的 Pod,也会定期从 API 组件订阅新的组件,也会监控运行情况,将信息汇报给 apiserver。kube-proxy 负责为 pod 对象提供网络代理和负载均衡。
通常,k8s 集群包含多个节点,通过 service 来进行通信。这需要一个负载均衡器来发送请求,完成负载均衡。kube-proxy 就是这个组件,为每个 node 启动网络代理,使得发往 node 的信息,高效地转发到内部的 pod 中。
Master-Node 有什么组件呢?四个基本组件:kube-apiserver、etcd、controllermanager、scheduler。
apiserver 提供了 k8s 集群的 api 接口服务。所有的组件通过这个接口来通信。创建更新 pod,或者查询集群状态。增删改查的认证、授权控制。
kubectl 是一个终端工具。
sched 调度器,监控节点的使用情况,把 pod 放到合适的 Node 上运行,看看哪个负载低。
cm 是监控故障并处理的,监控集群各种控件状态,做出响应。
etcd 是一个键值存储系统,集群的数据存储中心,比如每个控件的状态信息。
如果是云 k8s,可能有 cloud controller manager。
minikube 可以在本地运行一个单节点服务器集群,模拟生产环境。
创建 pod
如何创建一个 pod 呢?比如我们要创建一个 pod 来运行 Nginx,这个命令其实是我们理解 Kubernetes 的命令行为和容器镜像机制的重点。
1 | kubectl run nginx --image=nginx |
这里的 --image=nginx
会默认使用容器运行时(containerd
)去拉取 image=nginx
,这个镜像名称在不带地址前缀的情况下,默认解析为:
1 | docker.io/library/nginx:latest |
这里的 run nginx
是要创建的 pod 名称。
所以上面这个命令的作用就是,创建一个名字为 nginx
的 Pod,Pod 中运行的容器,容器的镜像是 nginx:latest
。
这里可以再提一下,Docker 镜像的官方命名规范:
1 | [registry]/[namespace]/[repository]:[tag] |
比如上面的镜像地址,docker.io
就是默认仓库地址,library
是镜像命名空间,nginx
是镜像名称,latest
是镜像标签。
我们也可以做自己项目的命名空间,比如我们登录 Docker Hub 后,创建了一个镜像 shen/nginx-demo:1.0
,我们在 Kubernetes 中就可以这样写:
1 | containers: |
云原生及流程
云原生(Cloud Native)不是一个具体技术,而是一种理念、一种方法论,也是一套技术体系。它是描述如何在云环境下构建、部署和运行现代软件系统的最佳实践集合。
传统开发部署 | 云原生方式 |
---|---|
开发打包为 .jar / .war |
每个服务用 Docker 打包镜像 |
部署到物理机 / 虚拟机 | 用 Kubernetes 自动部署到集群中 |
手动配置 nginx / 服务注册 | 用 K8s Ingress + Service + DNS 自动服务发现 |
流量高了要手动加服务器 | 自动弹性扩容,按需调度 Pod |
运维靠远程 SSH,日志分散 | 有 Prometheus + Grafana + 集中日志 + 自动告警 |
🧱 .jar
包的来源和用途:
阶段 | 文件类型 | 说明 |
---|---|---|
编写源码 | .java |
用 Java 写的源代码文件 |
编译 | .class |
编译器(javac )将源代码变成字节码 |
打包 | .jar |
将 .class 文件和资源打成一个压缩包 |
运行 | .jar + JVM |
JVM 解释执行 .jar 中的 .class 字节码 |
🏭 传统制造业为何也需要云原生?
制造业传统 IT 系统(如 MES、ERP、SCADA、PLC 接口)多数是:
- 🧱 单体架构
- 🖥️ 桌面端、C/S 结构
- ⚠️ 部署维护困难
- 📉 扩展和改版慢
云原生能带来的变革:
问题 | 云原生的解决方式 |
---|---|
应用发布慢 | Docker + CI/CD 一键发布 |
系统扩展难 | 拆分微服务 + Kubernetes 动态调度 |
多车间/工厂部署麻烦 | 用私有云 / 混合云统一部署,集中管理 |
系统不能弹性处理高峰 | Kubernetes 支持自动扩容 Pod |
桌面端升级困难 | 前端云化为 Web App,浏览器即可访问 |
其实云原生的思路并不是很困难的哈,大体流程就是 Docker 打包微服务,用镜像仓库交付,用 Kubernetes 管理运行,用 DevOps 自动化整个流程。
在具体的微服务开发和打包阶段,每个微服务都编写自己的 Dockerfile
,在本地或者 CI/CD 系统中用 docker build
打包镜像;
之后,将镜像上传到 Docker Hub 等仓库。
部署阶段,由 Kubernetes 运行,通过编写 YAML 文件或者使用 Helm 等工具,K8s 自动拉取镜像并启动容器。
最后,实现自动化,使用 Github Actions 等工具。
整个流程的最佳实践就是,先通一条主线,再扩展横向服务,稳扎稳打。
步骤 | 动作说明 |
---|---|
1️⃣ | 编写 Dockerfile ,能构建并运行你第一个服务 |
2️⃣ | docker build + docker run 本地验证服务正常 |
3️⃣ | 将镜像 docker push 到 Docker Hub(或私有仓库) |
4️⃣ | 编写 K8s 的 Deployment.yaml + Service.yaml 部署到 Minikube |
5️⃣ | 编写 GitHub Actions:触发构建、测试、打包、推送 |
6️⃣ | 测试:git push 后自动部署成功,Pod 能访问 👍 |
Kubenetes 和 Spring Cloud
Kubernetes 和 Spring Cloud 是有一定功能重合的。Kubernetes 是容器编排平台,属于基础设施层。它的核心功能是自动化部署、扩展和管理容器化应用。它是语言无关的,可以管理任何语言编写的容器化应用。它主要关注的是资源的调度、隔离、高可用和自愈能力。Spring Cloud 是一个微服务框架,属于应用层。它主要是用于简化分布式系统的开发,提供服务治理能力。Spring Cloud 主要面向的是 Java 和 Spring 生态,主要关注微服务间的通信、熔断、路由和分布式追踪等。
在很多场景下,将微服务打包成 Docker 容器后,仅使用 Kubernetes 就足够了。当你把所有的微服务容器化并部署在 K8s 上时,就立即获得了强大的基础设施层能力:
- 服务发现与负载均衡 (Service Discovery & Load Balancing): 你不再需要Eureka或Nacos。K8s的Service资源对象通过DNS为你的服务提供了一个稳定的入口。你可以在一个服务里直接调用另一个服务的名称(例如 http://user-service/users/1),K8s会自动解析并负载均衡到后端的某个Pod上。
- 配置管理 (Configuration Management): 你不再需要Spring Cloud Config。K8s的ConfigMap和Secret可以用来注入配置文件或环境变量,实现配置与代码的分离。
- 健康检查与自愈 (Health Checks & Self-healing): 你不需要Actuator的健康端点来做服务注册(虽然保留它仍然是好习惯)。K8s的Liveness和Readiness探针会定期检查你的应用,如果发现不健康,会自动重启容器或将流量从该实例中移除。
- 弹性伸缩 (Scaling): 你不需要手动启动更多实例。K8s的Horizontal Pod Autoscaler (HPA) 可以根据CPU或内存使用率自动增加或减少Pod的数量。
值得注意的是,很多Spring Cloud高级功能(特别是熔断、高级路由、分布式追踪),现在有了一种“Kubernetes原生”的解决方案,那就是服务网格(Service Mesh),例如 Istio 或 Linkerd。服务网格通过在每个Pod中注入一个“边车代理”(Sidecar Proxy),将这些网络通信和治理能力从应用代码中剥离出来,下沉到基础设施层。
功能 | Spring Cloud (应用层) | Service Mesh (基础设施层) |
---|---|---|
熔断 | Resilience4J | Istio提供 |
高级路由 | Spring Cloud Gateway | Istio提供 |
分布式追踪 | Micrometer Tracing | Istio自动完成 |
服务间加密 | 手动实现 | Istio自动提供mTLS |
实现方式 | Java代码,与业务逻辑耦合 | YAML配置,与业务逻辑解耦,语言无关 |
最终的抉择
- 简单场景: 如果你的微服务数量不多,业务逻辑不复杂,只用K8s完全足够。
- 复杂的Java生态系统: 如果你的团队精通Java和Spring,并且需要快速开发,K8s + Spring Cloud 仍然是一个非常成熟、高效的组合。你可以选择性地使用Spring Cloud的组件(比如只用Gateway和OpenFeign),而把服务发现和配置交给K8s。
- 多语言(Polyglot)环境或追求云原生终极形态: 如果你的团队中有Go, Python, Java等多种语言的微服务,或者你希望将所有治理能力从应用中解耦,那么 K8s + Service Mesh (如Istio) 是更先进和长远的方案。
总而言之,K8s解决了微服务的“生存”问题(部署、伸缩、自愈),而Spring Cloud或Service Mesh则解决了微服务“活得更好”的问题(韧性、可观测性、开发效率)。 你可以根据项目的具体需求、团队的技术栈和未来的架构方向来做出选择。
Spring Cloud 也是一个临时过渡期的技术了。这是为什么呢?因为 Spring Cloud 是一个面向 Java/JVM 生态的微服务开发框架。如果团队决定使用它,那么数据接入服务、参数处理服务、AI分析等微服务就需要用 Java 和 Spring Boot 等来编写,这会使得技术栈和 Java 深度绑定。Kubernetes 提供了强大的、与语言无关的基础设施能力,实现了更好的解耦。
对于数据处理这个 Kafka + 微服务集群 的系统:
- 服务发现与通信:
- 您的架构模式:您的微服务之间主要通过 Apache Kafka 进行通信,这是一个异步、事件驱动的模式。例如,“参数处理服务”并不直接调用“AI分析服务”,而是向 Kafka 的一个 Topic 发布“熟数据”,“AI分析服务”从这个 Topic 订阅数据。
- 这意味着什么:这种模式天生就是解耦和有弹性的。如果“AI分析服务”暂时不可用,Kafka 会为它暂存数据,等它恢复后再进行消费。因此,您几乎不需要 Spring Cloud 中用于同步调用(RESTful API)的服务发现(Eureka)、客户端负载均衡(Ribbon/LoadBalancer)或服务熔断(Resilience4J)等功能。您的核心中间件 Kafka 和运行环境 K8s 已经保证了系统的韧性。
- 配置管理:
- 每个微服务都需要知道 Kafka 的地址、数据库的连接信息等。这些配置完全可以通过 K8s 的 ConfigMap 和 Secret 来管理,并以环境变量或配置文件的形式注入到容器中。这比使用 Spring Cloud Config 更原生、更符合 GitOps 的理念,并且对所有语言的微服务都适用。
- 部署、伸缩和健康检查:
- 这些毫无疑问是 K8s 的核心职责。K8s 会负责部署您的 Docker 容器,根据负载(例如 Kafka Topic 的消费延迟)自动伸缩 Pod 数量,并通过健康探针(Liveness/Readiness Probes)确保服务的可用性。
基于您这个优秀的事件驱动架构,我的建议是:您完全可以不使用 Spring Cloud,仅依靠 Kubernetes + Docker 来运行您的微服务集群,这样做是更优、更“云原生”的选择。
- 解耦:K8s 方案让您的微服务可以用任何最合适的语言编写。例如,“AI分析服务”用 Python 写可能更方便(有大量现成的库),而“数据接入服务”用 Go 或 Java 写可能性能更好。您不受限于单一技术栈。
- 简洁:避免了在应用代码中引入复杂的 Spring Cloud 依赖和配置。您的开发人员可以更专注于业务逻辑(参数如何处理、AI 模型如何分析),而不是服务治理的框架细节。
但是 API 网关部分,可以用 Spring Cloud Gateway 来实现,这里提供了强大全面的能力。
Kafka
一个用户订单购买流程,可能是由下面一些微服务构成的:
创建订单-》检查库存-》更新积分-》处理支付-》物流发货-》通知用户
一个串行的流程,其中某一个步骤卡住了,后面就无法操作。实际上,很多步骤是可以异步进行的,比如用户可能根本不关心库存,也不是立即在乎积分。如何让服务之间的通信更加高效和可靠?
Kafka 就是这样一个中间件,将服务之间的通信和数据交换解耦。每个服务可以将自己的操作封装成一个事件,比如用户创建订单后,订单服务会产生一个订单已创建事件。订单服务就是一个生产者,然后这个事件会被发送到Kafka中,库存、支付、积分等其他服务就可以订阅这个事件,他们也就是消费者。他们可以从Kafka中读取这个事件并进行处理。这就是生产者-消费者模式,中间的Kafka就是一个消费队列,将生产者与消费者解耦。每个事件在Kafka中有一个唯一的序号,叫作 offset。消费者可以跟踪这个 offset 来跟踪已消费的事件,确保不会重复消费或者漏消费。这些事件也会被持久化到 Kafka 中,即使服务断了,某一消费者也可以读取上次的 offset 进行处理。不同服务的速度可能会有差异,比如订单服务可能处理得比较快,而支付服务处理得比较慢。这可能导致消费者的处理速度跟不上生产者的生产速度,导致消息在 Kafka 中的积压。如果消费者不够快,可以增加更多的消费者,生产者同理。当有很多个生产者和消费者时,如何让消息更加有序地进行分类和组织呢?
Kafka 提供了一个强大的主题 Topic 机制,可以将消息按照主题进行分类和组织。可以把主题看成是文件夹,事件是文件。生产者可以把不同类型的消息放到不同的主题中,不同的消费者订阅不同的主题,每个消费者只需要关注自己订阅的主题,然后独立地进行处理,而不需要关心其他主题的消息。每个主题还可以进一步分成多个 partition 分区,每个分区可以被不同的消费者线程并行处理。需要注意的是,Kafka只会保证每个分区内的消息是有序的,无法保证整个主题的全局顺序。所以在设计 Kafka 的主题和分区时,需要根据业务需求来合理划分。比如如果保证用户的交易记录是有序的,那么就把同一个用户的消息放到一个分区里即可。可以把用户的 ID 作为分区的 Hash Key。
Kafka 集群通常由多个 Broker 组成,每个 Broker 是一个独立的服务器,负责存储和转发消息。每个 Broker 可以存储多个主题的多个分区,也可以存储副本。Leader 是实际请求,Follower 复制 Leader,保证宕机数据不丢失。
Kafka 提供了消费者组的概念,多个消费者可以组成一个消费者组,共同消费统一个或者多个主题的消息,每条消息只能被同一个消费者组中的一个消费者消费,但是可以被多个不同的消费者组消费。这样可以实现多种不同的消费场景。最常用的场景是,当多个服务需要消费同一个主题的消息时,可以让不同的服务使用不同的消费者组。可以把库存、积分、支付等分别放到不同的消费者组中,每个消费者组可以独立地消费消息和处理业务。这样即便订阅的是同一主题,系统之间也不会互相干扰。
Bootstrap 指引导程序,当然有一个流行的前端框架,也叫这个名字。在 Kafka 中,--bootstrap-server
是 Kafka 客户端的一个参数,表示第一次要连接的 Kafka 节点地址。在 Kafka 中,--bootstrap-server
是客户端(生产者、消费者、管理命令等)连接 Kafka 集群时需要提供的入口地址。它的核心作用是,从这个 broker 开始连接集群,拿到完整的 broker 列表。
broker 是隐藏在后端的,Kafka 自动做备份和负载均衡。实际上在数据处理流水线上,关键的是 partition 和 consumer。partition 决定并发通道数,partition 是分区的意思。假如有一个生产者,每秒产生 100 条数据,一个消费者每秒只能消费 10 条,那么就需要把这个生产者生产的数据划分 10 个 partition,然后并行启动 10 个消费者来匹配处理能力。总结一下,一个 partition 只能由一个消费者来消费,partition 决定并发通道数,consumer 实例数决定并发处理能力,二者最好匹配。 Kafka 的并行消费能力 = min(Partition 数, Consumer 实例数), 如果你要满血并发,一定要让 Partition 数量 ≥ 实例数。
试飞的 Kafka 延迟
这里我们需要理解 Kafka 的通信,以及在微服务 K8s 集群中的通信协议。
Kafka 的客户端和服务器之间使用的不是 HTTP/JSON,而是一个高度优化的、基于 TCP 的自定义二进制协议。这个协议就是为“高效、实时、大容量”而生的:
- 二进制格式:协议本身是二进制的,没有文本协议(如 JSON)的冗余信息,解析效率极高。
- 批处理机制:这是性能的关键。生产者可以将多个消息(比如 100 条 32Hz 的数据)打包成一个批次,进行一次网络发送。这大大减少了网络往返的开销,极大地提高了吞吐量。消费者同样可以一次拉取一个批次的数据进行处理。
- 零拷贝:在数据从 Kafka Broker 传递给消费者时,Kafka 可以使用操作系统的“零拷贝”技术,直接将数据从内核空间的页面缓存(Page Cache)发送到网卡,避免了数据在内核空间和用户空间之间的多次复制,这是 Kafka 实现超高吞吐量的核心秘密武器之一。
- 智能压缩:生产者在发送数据前可以对整个批次进行压缩(支持 Snappy, Gzip, LZ4, ZSTD 等算法)。对于您这种有规律的试飞数据,压缩率会非常高,可以成倍地降低网络带宽占用和 Kafka 的磁盘存储空间。
即使微服务运行在 Kubernetes 的容器里,它与 Kafka 总线之间的通信也绝对不是 HTTP,而是 Kafka 自身的、基于 TCP 的、高效二进制协议。让我们把这个过程拆解一下,就更清晰了:
- 应用层 vs. 网络层:
- HTTP 是一种应用层协议,它通常用于客户端-服务器的请求-响应模式(比如浏览器访问网站,或者一个微服务调用另一个微服务的 REST API)。它的特点是通用、易于理解,但开销相对较大(尤其是头部信息)。
- Kafka 的协议 也是一种应用层协议,但它是为流式数据处理这个特定场景量身定制的。它运行在更底层的 TCP 协议之上,追求的是极致的吞吐量和低延迟。
- 如何实现通信?—— 通过 Kafka 客户端库
- 当您在编写一个微服务时(无论是用 Java, Python, Go 还是其他语言),您不会去手动创建 TCP 套接字来和 Kafka 通信。
- 您会在您的代码中引入一个 "Kafka 客户端库"(例如 Java 的 kafka-clients,Python 的 kafka-python)。
- 您的业务代码只需要调用这个库提供的简单接口,比如 producer.send(record) 来发送消息,或者 consumer.poll() 来拉取消息。
- 真正负责打包数据、使用 Kafka 二进制协议、与 Kafka 服务器建立和管理 TCP 连接的,正是这个客户端库。 它把所有复杂的底层通信细节都封装好了。
- 在 K8s 环境中是如何工作的?
- 您的微服务容器(Pod A)和 Kafka 服务器容器(Pod B)都运行在 K8s 集群的节点上。
- 在您的微服务配置中,您会指定 Kafka 的地址,这通常是一个 K8s 的 Service 地址(例如 kafka-broker.kafka-namespace.svc.cluster.local:9092)。
- 当您的微服务启动时,其内置的 Kafka 客户端库会解析这个地址,然后与 Kafka Broker 的 Pod 建立一个直接的、持久的 TCP 连接。
- 之后所有的数据交换——无论是发送 32Hz 的高频数据还是消费数据——都是通过这条已经建立好的 TCP 连接,使用高效的二进制协议来回传输。
总结一下:
- K8s 负责的是“容器的生命周期管理”和“网络路由”。它确保您的微服务 Pod 能够通过网络找到并连接到 Kafka Broker 的 Pod。它提供的是“路”。
- Kafka 客户端库 和 Kafka 服务器 负责的是“路上跑什么车”。它们决定了使用 Kafka 自家的、为大数据流优化的“高铁”(二进制协议),而不是普通的“公交车”(HTTP)。
这个组合正是您架构强大的原因:利用 K8s 实现部署和管理的自动化、弹性化,同时利用 Kafka 的原生协议确保数据传输的高性能。
应用层协议
传输层协议我们是没法改的,就两种:
- 一个 TCP(socket.SOCK_STREAM):面向连接、可靠的。能保证数据按顺序、无差错地到达。对于大多数需要数据完整性的场景,这是首选。
- 一个 UDP(socket.SOCK_DGRAM):无连接、不可靠的。速度快、开销小,但不保证数据到达或顺序,适合可以容忍少量丢包的场景。
然后就可以定义编码方式了,比如头部啊内容啊一类的东西,这个可以单独地研究这些编码方式。我们在这里,一定要区分开传输层的协议和应用层的协议啊。假如我们用 Docker 部署了一堆服务,然后每个服务都暴露出各自的端口,我们可以看看他们的端口、传输层和应用层的各自协议:
应用 (公司) | 默认端口 (房间号) | 传输层 (交通工具) | 应用层协议 (语言和流程) |
---|---|---|---|
Flask | 5000 / 8080 | TCP | HTTP (Hypertext Transfer Protocol)。这是Web的标准语言,用于请求网页、API等。你用浏览器或 curl 和它交谈。 |
MySQL | 3306 | TCP | MySQL Wire Protocol。这是 MySQL 自己定义的二进制协议,专门用于高效地传输 SQL 查询语句和表格形式的结果集。mysql 客户端或各种编程语言的库说的就是这种“方言”。 |
PostgreSQL | 5432 | TCP | PostgreSQL Frontend/Backend Protocol。和 MySQL 类似,这也是 PostgreSQL 自定义的二进制协议,用于在客户端和数据库之间传递 SQL 和数据。 |
Kafka | 9092 | TCP | Kafka Binary Protocol。我们之前讨论过,这是 Kafka 为实现超高吞吐量和批处理而量身打造的高效二进制协议。 |
Redis | 6379 | TCP | RESP (REdis Serialization Protocol)。这是 Redis 定义的一种简单文本协议。它虽然是文本,但比 HTTP 简洁得多,专门用于发送 SET key value 这样的命令和接收结果。 |
核心结论
- 各说各话:当你用一个 MySQL 客户端去连接 PostgreSQL 的 5432 端口时,即使 TCP 连接成功建立(你坐专车到达了正确的房间),通信也会立刻失败。因为 MySQL 客户端说的是“MySQL方言”,而 PostgreSQL 服务器完全听不懂,它只懂“PostgreSQL方言”。
- 端口是门牌,协议是语言:IP:端口 的组合,比如 127.0.0.1:3306,仅仅是帮你找到了正确的目标程序(MySQL 服务器)的大门。你进门之后,必须使用它能理解的协议(MySQL Wire Protocol)才能和它进行有效的沟通。
- 应用层协议百花齐放:正如您所说,传输层主要就是 TCP 和 UDP。但应用层协议的数量是无穷无尽的,任何一个需要网络通信的应用程序都可以定义自己的应用层协议,就像我们之前讨论的自己开发协议一样。HTTP、FTP、SMTP、DNS... 这些都是标准化的应用层协议,而像 MySQL、Kafka 这些则是它们自己定义的专用协议。
MMVM
MMVM,全称是 Model-View-ViewModel,是一种主要用于前端开发的架构模式,最早源自于微软的 WPF 框架。它的主要目的是 解耦 UI 和业务逻辑,使界面逻辑更清晰、可维护性更强。
Model 是数据模型,负责数据库操作、业务逻辑等。
View 是视图层,UI 界面,展示内容。
ViewModel 是连接 View 和 Model 的桥梁,处理界面行为的逻辑。它封装了 View 所有的状态和行为,是一种前端控制器。
Vue 是 MMVM 架构吗?是的,Vue 的设计思想就是基于数据驱动视图和双向绑定。Vue 有两种使用方式,一种是通过 <script>
标签引用 Vue 的 CDN,另一种是构建完整的 Vue 项目,包括 public
目录下的网页入口模板 index.html
,src
目录下的项目入口 JS main.js
以及根组件 App.vue
,以及 components
。
1 | my-vue-project/ |
在这里,MMVM 是怎么体现的呢?M 即是 data: { count: 0}
的部分,是数据模型表示状态。V 就是 HTML 中的页面部分,<div id=app>
以及 {{count}}
,页面展示同时绑定了数据。VM 就是 new Vue({})
实例 + methods
,负责逻辑数据变化后通知 View。
1 |
|
如果没有 Vue,我们需要直接操作文档对象模型(Document Object Model)来完成绑定逻辑和页面更新。DOM 将 HTML 文档表示为节点和对象,这样,编程语言就可以与页面交互。
1 |
|
那么对比来看,不去管 MMVM 具体是怎么实现的,MMVM 带来的好处和特性主要是什么呢?实际上,所有架构都必须处理数据变化,因为这是业务逻辑,更新变量这些逻辑是不可省略的。但是在非 MMVM 架构下,除了数据变化,还需要手动处理页面变化!Vue、WPF 这些会自动刷新前端界面,不需要我们手动把变量的值更新后,再更新一版界面。而通过 ViewModel 的强大功能,他会自动把 Model 的值同步给 View,使得我们只需要写逻辑,UI 会自动跟上。假如我们要改 10 个变量,对于非 MMVM 写法,必须手动更新 10 次 DOM,而在 MMVM 中只需要改 10 个数据的逻辑,UI 自动变。所以要理解一个在发展了很多年后出现的新的流行的框架或机制,一个好的办法就是看看早期是怎么实现的,这个新的技术是要解决什么问题。
那么 Vue 的 MMVM 到底是怎么绑定的呢?我们可以看看这段 js:
1 | new Vue({ |
这里就是把 <div id="app">
这个 HTML View 结构绑定到了 Vue 实例(ViewModel)上了。Vue 在启动时做了以下几件事:
- 找到
el: '#app'
对应的 DOM 元素,也就是这个<div>
- 把它的内容交给 Vue 接管(Vue 会编译模板)
- 将模板里的变量
{{ count }}
、事件@click="increment"
绑定到 ViewModel 中的 data/methods - 建立响应式连接,当更改
this.count
,Vue 自动更新 DOM
Vue 多页面
一言以蔽之,Vue 本质上是一个单页应用(Single Page Application),所有页面切换实际上都是在这个页面中通过 JavaScript 实现内容切换的。所谓不同页面,其实是 App.vue
中 <router-view>
区域里动态加载不同的组件,URL 变化但页面不刷新。
要理解这件事,首先我们要理解 Vue 的工作图景。这是一个典型 Vue CLI 项目的结构:
1 | my-vue-app/ |
我们来梳理一下各文件的作用及调用关系。
index.html
是项目的静态 HTML 模板,是 Vue 项目最终渲染的载体。这个文件不直接编写内容,只有:
1 |
|
打包后,<div id="app">
会被 Vue 渲染出来的内容替换。当然,可以在这个文件的 <head>
部分编写一些网页的元信息,比如设置标题啊,icon 一类的。
src/main.js
是项目的 JS 入口文件,作用是创建 Vue 实例并挂载到 #app
,作用相当于 C 程序的 main()
。
1 | import { createApp } from 'vue'; |
最终起到的效果是,App.vue
会替换掉 <div id="app">
中的内容, Vue 接管整个前端页面的显示和交互,加载 router/index.js
注册路由系统。
src/App.vue
是 Vue 项目的根组件,是所有页面的容器。Vue 会把 App.vue 渲染到 <div id="app">
中。如果有多个页面,他就是 <router-view>
的容器。
1 | <template> |
router/index.js
是用于配置路由的,配置路径与视图之间的关系,实现 URL 对应页面的加载:
1 | import { createRouter, createWebHistory } from 'vue-router'; |
单体应用
单体应用(Monolithic Application),客户端集中运行在一个进程里,未拆分为微服务、独立模块或者可水平扩展的子系统。
WPF
WPF(Windows Presentation Foundation)是微软推出的一套桌面应用开发框架,属于 .NET 平台,主要用于构建 Windows 桌面应用程序的图形用户界面。支持 MMVM,采用 XML 风格的语言来定义 UI,与代码逻辑分离。不跨平台,开发出来的桌面程序只能运行在 Windows 上。
新的时代
AI 正在接管大量如何实现的细节工作how,人类工程师更关注上层的做什么what和为什么这么做why。
过去的价值核心 | 未来的价值核心 |
---|---|
精通语言语法和框架细节 | 深刻理解业务领域和用户痛点 |
编写高效、无误的算法和逻辑 | 设计健壮、可扩展、有韧性的系统架构 |
手动完成部署和运维 | 掌握云原生工具链(K8s, Docker, IaC)进行声明式管理 |
在单一领域深耕(纯前端/纯后端) | 具备跨领域整合能力(端到端系统思维) |
自己解决所有问题 | 善于利用 AI 和工具,提出正确的问题 |
这一切和分析程序性能,或者解决芯片热管理问题都一样,都是要分析主要矛盾在什么地方。随着时代的发展,主要矛盾也在发生变化... AI 时代的全栈工程师,也不再是传统意义上的既会写前端也会写后端。他应该能够:
- 理解需求,将模糊的需求转换为清晰的技术指标。
- 进行技术选型。
- 设计系统架构,定义服务边界、通信模式和数据流。
- 指导实现,利用 AI 快速生成和迭代代码,并对 AI 的产出进行审查、优化和整合。
- 保障交付,利用云原生工具链实现自动化部署、监控和运维。