K8s 网络分层全景:OSI 七层视角下看清 Pod IP、Service、Ingress 与组件分工

为什么 K8s 网络这么难懂 因为 K8s 的"网络"根本不是一张网,而是多张网叠在一起,而且不同组件在不同的协议层各干各的。以本文的 kind 集群为例,从外到内是四层: 层 网段(本文 kind 集群实测) 谁的"地盘" 物理机/宿主机 192.168.8.26 (debian 宿主机) 真实网卡(kind 之外的真实世界) 节点容器(kind 特有) 172.18.0.0/16 (节点 IP:172.18.0.2/3/4) kind 节点 = Docker 容器,这是 Docker 网络分给节点容器的 IP,不是物理机 IP Pod 网段 10.244.0.0/16 (Pod IP:10.244.1.x、10.244.2.x) 集群内每个 Pod 一个 IP(CNI 的虚拟网) Service 网段 10.96.0.0/16 (ClusterIP:10.96.x.x) 虚拟的"服务名"入口 ⚠️ 新手提示:kind 里看到的 172.18.0.x 是"节点容器"的 IP,不是物理机 IP——kind 的"容器即节点"让节点本身就是 Docker 容器(网络栈因此多一层)。kubeadm/生产集群没有这一层:节点就是物理机/虚拟机,节点 IP = 物理机 IP(如 192.168.8.26)。看下面的图就清楚了。 再加上:CoreDNS 在 L7 解析服务名、kube-proxy 在 L4 做转发、kindnet/CNI 在 L3 管 Pod IP 和路由、Ingress 在 L7 做域名路由——初学者拿着传统网络的知识套进来,发现"Pod 的 IP 不是 DNS 服务器的 IP"、“Service 的 IP 没有网卡”,自然就绕晕了。 ...

五月 22, 2024 · 10 分钟 · 1921 字 · yaomingye

K8s 组件职责全景:一条 kubectl 命令背后的控制面与节点协作

谁在干活:kubectl 命令背后的组件分工 系列前十几篇,集群一直当"黑盒"用—— kubectl apply 一个清单,应用就起来了,至于是谁把这件事做完的,没拆开看过。对兼职运维来说,这个黑盒必须拆开:排障的第一问不是"怎么修",而是"哪一环出了问题、该看谁"——Pod 一直 Pending 是调度的问题还是资源的问题?探针失败是应用的问题还是 kubelet 的问题?服务访问不通是 Service 配置还是网络插件?组件职责 = 排障归属地图。 这篇用一条生产里每天都在用的命令( kubectl apply )当主线案例,把控制面四个组件和节点四个组件的职责、工作流程讲清楚,并用 kind 集群上的实测事件证明"谁在干活"。理论向,不做源码级剖析——兼职运维只需要知道"每个组件干什么、出了事找谁"。 1. 组件全景:控制面管"想",节点管"做" K8s 的所有组件分成两组,职责边界非常清晰: 控制面(control plane):负责决策——存状态、做调度、收敛声明,全在控制面; 节点(node):负责执行——拉镜像、起容器、转发流量、管网络,全在节点。 %% K8s 组件全景: 控制面 4 件套 + 节点 4 件套 flowchart TD subgraph CP["控制面(决策)"] API["kube-apiserver\n唯一入口 + 收费站"] ETCD[("etcd\n唯一真相存储")] SCH["kube-scheduler\n给新 Pod 找节点"] CM["kube-controller-manager\n控制器集合(把声明收敛成动作)"] end subgraph N1["节点 learn-worker"] KL1["kubelet\nPod 生命周期 + 探针"] KP1["kube-proxy\nService 转发规则"] CT1["containerd\n容器运行时"] CNI1["kindnet\nPod 网络 + IP"] end subgraph N2["节点 learn-worker2"] KL2["kubelet"] KP2["kube-proxy"] CT2["containerd"] CNI2["kindnet"] end API --> ETCD API <-->|"watch/上报"| KL1 API <-->|"watch/上报"| KL2 API <-->|"watch"| KP1 API <-->|"watch"| KP2 API <-->|"watch"| SCH API <-->|"watch"| CM style API fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold style SCH fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold style CM fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold style ETCD fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold style KL1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style KL2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style KP1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style KP2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style CT1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style CT2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style CNI1 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style CNI2 fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff 在 kind 里这些组件都是真实运行的 Pod(生产 K8s 同款,只是 kind 把它们跑在 Docker 里)。看它们: ...

二月 22, 2024 · 5 分钟 · 869 字 · yaomingye

为什么是 K8s:传统微服务运维痛点与 K8s 的设计回应

先回答为什么,再谈怎么用 这是系列的自我批评篇,也是全系列的导航页。回看前十三篇文章,我发现一个通病:每篇都把"K8s 怎么做"讲得很细——清单、命令、预期输出、踩坑,可复现性拉满;但"为什么必须这么做“几乎没讲——没有 K8s 之前,同样的流程是怎么跑的?痛点在哪?K8s 的机制到底回应了什么? 作为 Java 开发者,你手里握着最好的参照系:Spring Cloud 时代的微服务。Nacos 配置中心、Eureka 注册中心、停机发布、人工巡检——这些痛点我们亲历过。这篇就用它当镜子,逐话题对照 K8s 的设计回应,每个话题都附上对应教程的链接——先看这篇理解"为什么”,再点链接去"怎么做"。 📌 系列结构:环境搭建见 kind 集群实战,开发者总览见 Java 开发工程师的 K8s 职责清单。 1. 总设计思想:声明式 + 控制回路 K8s 所有机制的地基是两个思想,先立起来: 声明式(Declarative):你不说"怎么做到",只说"我要什么"。传统方式是命令式——“把包拷到这台机器、改这个配置、重启这个进程”;K8s 方式是"这是我想要的最终状态(YAML),你来实现"。 控制回路(Control Loop):K8s 的控制器永远在循环"当前状态 vs 期望状态"——不一致就动手收敛,一致就闲着。这跟空调温控一个原理:设定 26 度(期望),温度计(当前),制冷/制热(动作),周而复始。 %% K8s 核心设计思想: 声明式期望 + 控制回路收敛 flowchart LR DESIRED["期望状态\nkubectl apply 提交的 YAML"] CTRL["控制器\n对比 期望 vs 当前"] ACTUAL["当前状态\n集群里的实际情况"] ACT["执行动作\n创建/重启/扩容/摘流"] DESIRED --> CTRL ACTUAL -->|"读取"| CTRL CTRL -->|"不一致 →"| ACT ACT -->|"改变"| ACTUAL CTRL -.->|"一致 → 空闲"| CTRL style DESIRED fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#ffffff,font-weight:bold style CTRL fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#ffffff style ACTUAL fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold style ACT fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#ffffff,font-weight:bold 这个思想替代了什么:传统运维的"巡检 + 手动修复"是人肉控制回路——人发现、人决策、人执行,慢且会忘。K8s 把回路自动化了。下面七个话题,全是这个思想的展开。 ...

二月 16, 2024 · 2 分钟 · 419 字 · yaomingye

数据库金额字段该用 decimal 还是 bigint:从历史惯例到现代支付栈的选型之路

钱的字段到底该用 decimal 还是 bigint 某开发者最近在设计一套统一支付服务,走到金额字段这一步,跟数据库里的老订单表吵了一架:新表想用 bigint 存"分",老表是 decimal(10,2)。写代码前先把这个历史遗留问题捋清楚,发现这背后是一整段软件史。 数据库里的金额字段,可能是除了主键之外被争论最多的一种类型。打开任何一本数据库教材,都会看到一句名言——“钱的字段千万别用 float”。但这句话的下半句往往没人讲:不用 float,那到底用 decimal 还是 bigint? 教科书里写的是 decimal。现代支付 API 的契约里写的是"整数最小单位"——也就是 bigint 存分。两边都合理,为什么结论会分叉? 从一次选型冲突说起 设计支付服务时,金额字段出现了两个候选人: ** decimal(10,2) ** —— 存的就是 100.00 ,肉眼可读 ** bigint ** —— 存 10000 ,单位是分,代码里到处都是 ÷100 老 ERP 系统的订单表选了前者,支付服务想选后者。这不是口味问题,是两个时代的设计碰撞。要理解它,得先从 float 为什么被禁说起——因为 float 才是那个真正不配碰钱的类型。 📌 前置知识:浮点数、定点数、IEEE 754 这三个概念是本文的地基,建议先有个印象再往下看。 float 的罪与罚:二进制算不清十进制 先复现那个经典翻车现场: SELECT 0.1 + 0.2; 结果是 0.30000000000000004 。 float / double 用二进制科学计数法存储: M × 2^E ,M 是尾数,E 是指数。但十进制小数 0.1 转成二进制是无限循环小数: ...

十月 25, 2023 · 3 分钟 · 631 字 · yaomingye

从语法迷雾到内存物理本质:C 指针 / Java 引用 / Go 接口 / C++ 类的底层统一模型

语法迷雾之下,物理内存里只有「数值」与「地址」 一、导言:被抽象概念包围的困惑 某开发者学了三年 Java,突然被扔去写 C——struct 是什么? *p 又是什么? void * 是什么东西?回头再看 Go,interface 怎么不用 implements ?再翻翻 C++ 源码,一个 class 里又是 virtual 又是 this——每个语言都有一套自己的"数据创造论",把内存的本质层层包裹起来。 Java 说「一切皆对象」,C 说「一切皆指针」,Go 说「用组合不要继承」,C++ 说「我全都要」。刚入门的读者站在这些口号中间,CPU 和 RAM 到底是怎么看待这些概念的? 破局点只有一句话:CPU 和 RAM 根本不懂面向对象。物理内存里永远只有两样东西——「数值」与「地址」。 int 是数值,指针是地址,对象的字段是数值和地址的排列组合,接口变量是两个地址凑一对。所有语言的语法特性,最终在内存里都还原为这个二元模型。 flowchart LR subgraph APP["应用层"] JAVA["Java: 对象/引用"] CPP["C++: class/虚表"] GO["Go: struct/interface"] C["C: struct/指针"] end subgraph COMPILER["编译器/Runtime"] LANG["语法糖脱糖"] LAYOUT["内存布局计算"] VTABLE["虚函数表生成"] end subgraph HARDWARE["物理层"] VAL["数值"] ADDR["地址"] end JAVA & CPP & GO & C -->|"编译/解释"| COMPILER COMPILER -->|"最终形态"| HARDWARE classDef appStyle fill:#2d2522,stroke:#ea580c,stroke-width:2px,color:#f8fafc; classDef compStyle fill:#1e293b,stroke:#0284c7,stroke-width:2px,color:#f8fafc; classDef hwStyle fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0; class JAVA,CPP,GO,C appStyle; class COMPILER compStyle; class VAL,ADDR hwStyle; 这篇文章的任务:把 C、C++、Java、Go 的语法糖一颗颗剥开,露出底下那个统一的物理内存画卷。 ...

十月 17, 2023 · 6 分钟 · 1191 字 · yaomingye

内存管理的破局者:虚拟地址、物理地址与页表——从哲学追问到公式推导

虚拟地址、物理地址与页表:从懵圈到通透只差这篇文章 一、起源:一个看似简单的哲学追问 1.1 初始疑惑:虚拟地址和物理地址一样,也是面向 CPU 的编号吗? 一个常见的困惑:虚拟地址(Virtual Address,VA,程序看到的地址)和物理地址(Physical Address,PA,硬件真正使用的地址)都是数字,那它们到底有什么区别?某位开发者曾盯着 printf("%p", ptr) 打印出的十六进制数,天真地以为这个数字就是内存条上的物理位置——直到他发现进程的虚拟内存空间比物理 RAM 还大,才意识到事情没那么简单。 核心区别在于它们服务的 CPU 阶段不同: VA(虚拟地址):面向程序与 MMU(Memory Management Unit,内存管理单元)。CPU 在执行 LOAD 指令时,发出的第一个请求就是 VA。程序中的每一个指针、每一个变量地址,都是在 VA 空间中定义的。 PA(物理地址):面向 RAM 与内存控制器。经过 MMU 翻译之后,VA 变成 PA,才是内存条上真正的硬件编号。内存控制器只认 PA,它不关心也不理解虚拟化这件事。 来看一段最直观的 C 代码: #include <stdio.h> #include <stdlib.h> int main() { int x = 42; int *ptr = &x; // ptr 的值是一个虚拟地址,不是物理地址 printf("变量 x 的虚拟地址: %p\n", (void *)ptr); printf("变量 x 的值: %d\n", *ptr); return 0; } ptr 里存储的地址是 VA。当 printf 读取 *ptr 时,CPU 发出 LOAD [ptr] 指令,硬件自动经过 MMU 翻译为 PA 之后才去 RAM 中取值。整个过程对程序员透明——这也是为什么很多人长期把 VA 误当作「内存的真实坐标」。 ...

十月 15, 2023 · 7 分钟 · 1369 字 · yaomingye

RocketMQ 存储模型硬核拆解:NameServer → CommitLog → ConsumeQueue 三层架构精析

RocketMQ 存储模型拆解:别再拿它当 BlockingQueue 用了 0. 引言:一个 Javaer 的认知崩塌 每一个刚接触 RocketMQ 的 Java 开发者,大概都会经历一次认知崩塌: 翻开 RocketMQ 源码,找遍所有 package,也找不到一个像 BlockingQueue 那样的 Queue 实现。作为一个"消息队列"(Message Queue),它的 Queue 到底在哪里?如果找不到 Queue 对象,消息是怎么"入队"和"出队"的? 答案很残酷,但也很优雅:RocketMQ 根本没有什么 Queue 数据结构。 RocketMQ 的 Queue 是一个文件夹——硬盘上的文件夹。消息不是"入队",而是 append 到磁盘文件末尾。消费者不是"出队",而是从磁盘文件读取一段定长的字节数组。 整个 RocketMQ,本质上就是一套精心设计的磁盘文件操作方案。 所有的分布式消息特性——消息重试、死信队列、消息积压、限流熔断——都是在这套磁盘文件模型上玩出的花样。理解了 RocketMQ 的文件长什么样、文件之间怎么关联,你就理解了 RocketMQ 的一切。 这篇从最底层开始,一层层往上拆,共七层: NameServer(路由层)——只管路标,不存消息 CommitLog(存储层)——所有消息顺序写入的大文件 ConsumeQueue(索引层)——被误称为队列的定长指针数组 Topic 参数(运维配置)——目录数量和权限的配置映射 高级特性——基于文件模型的策略实现 开发视角——你的代码在操作什么 总结——一张物理模型图覆盖所有概念 1. 第一层:NameServer(路由层) NameServer 是 RocketMQ 的路由层。它是一个轻量级的注册中心——只维护 Topic 到 Broker 地址的映射关系,不存储任何消息数据。 传统模式 在 RocketMQ 的传统模式下,NameServer 内存中维护着两张核心映射表: BrokerData:Topic 名称 → Broker IP 列表。消费者拿到这个列表,就知道该从哪台机器拉数据。 QueueData:Queue 数量 + 权限设置。Broker 上报时携带每个 Topic 配置了多少 Queue。 生产者发消息时先问 NameServer:“TopicA 在哪台机器上?有几个 Queue?” NameServer 返回 Broker 地址列表,生产者选择一个 Broker 发过去。消费者同理。 ...

十月 13, 2023 · 7 分钟 · 1408 字 · yaomingye

Spring Bean 生命周期与懒加载:从一次 Starter 踩坑说起

Spring Bean 生命周期与懒加载:从一次 Starter 踩坑说起 起因:一个 @PostConstruct 引发的血案 封装了一个 Redis 工具类的 Spring Boot Starter,里面有个组件叫 WorkIdAllocator ,它在 @PostConstruct 中做了这么一件事: @PostConstruct public void init() { setNextSnowFlaskWorkerId(); // 连接 Redis,分配一个雪花算法 WorkerId } 看起来没毛病——服务启动时自动分配到 WorkerId。但问题来了:Redis 的连接参数( spring.data.redis.host )放在 Nacos 配置中心,通过 shared-configs 加载。而 @PostConstruct 在 Bean 属性注入完成后就立刻执行,那时候 Redis 配置还没加载到 Spring 的 Environment 中。 结果: host = null → 默认 localhost:6379 → 连不上 → 启动失败。 修复方式也很简单——加个 @Lazy : @Lazy @Component public class WorkIdAllocator { // 第一次被调用时才执行 @PostConstruct } 问题虽然解决了,但某个开发者好奇心被勾起来了:Spring Bean 的生命周期到底分几个阶段?扩展点在什么时候执行?@Lazy 到底干了什么? ...

三月 8, 2023 · 6 分钟 · 1277 字 · yaomingye

单体拆分微服务:9 个服务踩出来的 7 个典型错误

单体拆微服务,这 7 个坑你踩过几个? 接手了一套从单体架构拆分为微服务的 Spring Cloud Alibaba 项目。9 个服务,Spring Boot 3.3.5,集齐了 Nacos、Gateway、Sentinel、RocketMQ、ShardingSphere、Elasticsearch 全家桶——看 POM 文件像一份微服务教科书。 实际跑起来就发现问题了:项目虽然拆成了 9 个模块,但在架构思维上仍然是个单体。 用了微服务的壳,没改掉单体时代的坏习惯。一顿排查下来,发现了 7 个典型错误。 错误一:全量包扫描——每个服务都在扫整个宇宙 第一个映入眼帘的就是各个 Application 类上的注解: @ComponentScan(basePackages = "cn.net.mall") 9 个服务里有好几个直接扫整个项目包树。这意味着什么? mall-pay 启动的时候,Spring 会去扫描 mall-common 下的所有类,包括 cn.net.mall.util.RedisUtil 。而 RedisUtil 又依赖 StringRedisTemplate ,这个类来自 spring-boot-starter-data-redis ,偏偏 mall-pay 的 POM 里没加这个依赖。 mall-pay 启动 → 扫描 cn.net.mall → 发现 RedisUtil → 尝试创建 → StringRedisTemplate 不在 classpath → ClassNotFoundException → 启动失败 flowchart LR subgraph SCAN["全量扫描 `cn.net.mall `"] PAY["mall-pay\n@ComponentScan"] COMMON["mall-common\nRedisUtil ← 依赖 → StringRedisTemplate"] end subgraph CLASSPATH["pay 的 classpath"] DEPS["mall-common.jar\n(但有 optional=true)"] MISSING["❌ StringRedisTemplate 不在"] end PAY -->|"扫描到"| COMMON COMMON -.->|"尝试创建 bean"| MISSING MISSING -->|"ClassNotFoundException"| CRASH["启动崩溃"] classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca; classDef highlight fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa; class PAY,COMMON process; class MISSING,CRASH reject; class DEPS highlight; 更隐蔽的问题是:全量扫描让分仓库成为泡影。 微服务的核心理念之一就是独立开发、独立部署。如果每个服务都假设"所有模块在同一个 classpath 上",那一旦把服务拆到独立 Git 仓库,全量扫描就会漏掉其他服务的类——因为它根本不在 classpath 上。 ...

三月 6, 2023 · 8 分钟 · 1559 字 · yaomingye

B/S架构常见网络攻击与SpringBoot/Cloud防御实践——XSS、CSRF、SQL注入、DDoS与JWT安全的攻防图谱

B/S攻防:五类攻击与Spring体系的应对 📌 前置知识:本文假设读者用过 Spring Boot、知道 Cookie/Session/Token 的基本概念。Spring Security 的 Filter Chain 不熟没关系——每段防御代码会说明它在过滤器链中的位置。 XSS:当用户输入变成了可执行脚本 跨站脚本攻击(Cross-Site Scripting)的本质是:攻击者把 JavaScript 塞进用户输入,服务器原样输出到 HTML,浏览器执行了这段恶意脚本。 sequenceDiagram participant Attacker as 攻击者 participant Victim as 受害者浏览器 participant Server as 有漏洞的服务器 participant DB as 数据库 Attacker->>Server: "POST /comment 提交评论\n内容: script stealCookie()" Server->>DB: 存入评论(未做转义) Victim->>Server: GET /article?id=123 Server->>DB: 查询评论列表 DB-->>Server: 返回含脚本的评论 Server-->>Victim: "script stealCookie()\n浏览器解析并执行" Victim->>Attacker: "恶意脚本执行\nCookie 被发送到攻击者服务器" Spring Boot 的防御分三层: ① 输出转义——Thymeleaf 默认做 // Thymeleaf 模板中默认对变量做 HTML 转义 // <div th:text="${comment.content}"> → < 变成 &lt;,脚本失效 // 如果你用 JSP 或手动拼 HTML,务必用 escapeHtml: String safe = HtmlUtils.htmlEscape(userInput); ② 输入过滤——Spring 全局拦截 ...

二月 24, 2023 · 5 分钟 · 927 字 · yaomingye
Cat Radio