[{"content":"原文链接 GitHub Uses eBPF to Eliminate Deployment Risks and Prevent Circular Failures How GitHub uses eBPF to improve deployment safety InfoQ 这篇新闻总结的是 GitHub Engineering 在 2026 年 4 月发布的一篇文章。它讨论的不是常见的 eBPF 网络加速或可观测性案例，而是一个更偏平台工程的问题：部署系统本身会不会依赖它正在修复的系统。\n这个角度挺有意思。我们平时谈 Cloud Native 的可靠性，经常会讲多副本、自动扩缩容、健康检查、Service Mesh、GitOps、回滚策略。但当系统真的进入故障态时，一个很实际的问题往往更朴素：修复系统的工具链还能不能工作。\nGitHub 这次解决的是什么问题 GitHub 的基本矛盾是一个典型的循环依赖：GitHub 的源码托管在 github.com 上，但 github.com 如果出现故障，修复它的部署动作又可能需要访问 github.com。\n这个显性的循环依赖可以通过代码镜 …","date":"2026-05-22","keywords":["GitHub","eBPF","Cloud Native","deployment safety","cGroup","Cilium","Kubernetes","observability","runtime security"],"permalink":"/reading/github/ebpf-deployment-safety/","tags":["github","ebpf","cloudnative","deployment","platform-engineering","observability"],"title":"读 GitHub eBPF 部署安全案例：Cloud Native 项目里 eBPF 可以做什么"},{"content":"背景：我为什么会去读这篇 整理 Agentic Workflow 那篇笔记时，我反复卡在同一个问题上：**当 AI 真的开始替我写代码，\u0026amp;ldquo;需求\u0026amp;quot;和\u0026amp;quot;设计\u0026amp;quot;这两件事到底应该长成什么样？**Spec Kit、AGENTS.md、ADR 都能算是答案，但它们各自只覆盖了一部分。\n正好在 Martin Fowler 的网站上看到 Wei Zhang 和 Jessie Jie Xia（两位都来自 Thoughtworks）2026-04-28 发的这篇 Structured Prompt-Driven Development。文章的核心主张比较干脆：把 prompt 当成与代码同级的工程工件管理——版本化、评审、复用、迭代。\n这条主张其实不新，但作者把它具体化成了一组流程、一份七维度模板和一套配套 CLI（gszhangwei/open-spdd），加上一个完整示例项目（gszhangwei/token-billing），值得花时间读完整理。\n关于 SPDD 与 PDD、Spec Kit、PromptOps 这些相邻概念的差别，我另起了一篇速 …","date":"2026-05-21","keywords":["Structured Prompt-Driven Development","SPDD","REASONS Canvas","Martin Fowler","Wei Zhang","prompt as artifact","AI 辅助开发"],"permalink":"/reading/martinfowler/structured-prompt-driven-spdd/","tags":["spdd","ai-coding","prompt-driven","prompt-engineering"],"title":"读《Structured Prompt-Driven Development》：把 prompt 当一级交付物的几点笔记"},{"content":"我前一篇 《给 mirrord 开发者按 namespace 签发 kubeconfig》 写完之后，第一次用 VS Code 的 mirrord 扩展真接到那套 demo 集群——结果在 agent 起来之后立刻被一个 403 Forbidden 卡住。这篇是那次排查的复盘：从 IDE 报错一路追到 Kubernetes impersonation 鉴权链路上一个我之前不知道的细节，以及最后那条最小修复怎么落到 demo 里。\n相关代码都在 meirongdev/mirrord-demo 的 feat/rbac-mirrord-governance 分支上。修复对应的 commit 是 6e44011。\n一、出发点：用 VS Code 扩展跑这套 demo 前一篇 demo 用 bash run-mirrord.sh -- java -jar ... 在命令行里直接跑通。但团队里大部分人是从 VS Code / IntelliJ 直接 launch / debug 进程的，所以我得验证一遍：用 mirrord 的 VS Code 扩展，配合 alice 的 scoped …","date":"2026-05-21","keywords":["mirrord","mirrord VS Code","WebSocket 403","kubernetes impersonation","RBAC RoleBinding","ClusterRoleBinding","serviceaccounts impersonate","users 虚拟资源","auth can-i","client-go impersonation"],"permalink":"/posts/mirrord-vscode-impersonator-ws403/","tags":["kubernetes","mirrord","vscode","rbac","impersonation","troubleshooting"],"title":"VS Code 跑 mirrord 撞上 WebSocket 403：从 IDE 报错追到 K8s impersonation 的鉴权链路"},{"content":"我们的服务部署在 K8s 环境里，要调试一个服务，需要改代码、提 GitLab MR、跑 Jenkins 构建、ArgoCD 部署，整个流程非常慢。而且如果多个人要调试同一个服务的不同 feature，环境占用就会是个问题。为了提升调试效率，我们引入了 mirrord OSS——它能绕过整个 CI/CD 链路，直接在本地进程里接入集群服务。但 mirrord OSS 不带权限模型——直接发 cluster-admin 不现实。这篇我从 DevOps 角度走一遍：怎么给一个开发者签发 kubeconfig、按 namespace 给他绑 RoleBinding、验证授权真的生效。\n团队层面「几十个人来来去去怎么管」是另一个更通用的 K8s 用户管理问题，不属于 mirrord 特有，我会另起一篇。\n这篇文章对应的可运行 demo 放在 GitHub：meirongdev/mirrord-demo，对应 commit 是 6e44011。如果只是想看脚本细节，可以直接看 repo 里的 rbac/ 目录。\n配置形态里有一处不直观的地方：除了按 namespace …","date":"2026-05-20","keywords":["mirrord","mirrord 接入","kubernetes RBAC","kind","CertificateSigningRequest","RoleBinding","namespace 隔离","团队本地调试","开发者权限治理"],"permalink":"/posts/onboarding-developers-to-mirrord/","tags":["kubernetes","kind","mirrord","rbac","local-development","devops"],"title":"给 mirrord 开发者按 namespace 签发 kubeconfig"},{"content":"在我经手过的几个项目里，升级平台版本更多是为了主动偿还技术债，而不是追新。Java 21→25 和 Spring Boot 3.5→4.0 是我在跟踪的下一次升级，时机还没到，但提前把反复想到的三个问题梳理一遍通常能省不少麻烦：为什么升、升了能得到什么、怎么升。\n版本节奏与维护周期 在讨论「为什么升」之前，先把两个平台的发布规律和生命周期摆出来，后面的判断会有据可依。\nJava Java 自 2017 年起切换到半年一版的节奏，每年 3 月和 9 月各发一个版本。其中每隔两年（从 Java 21 开始正式固定）发一个 LTS（Long-Term Support）版本，非 LTS 版本只在下一个版本 GA 后停止支持，实际窗口约 6 个月。\n版本 类型 GA 时间 Oracle Premier 支持截止 Oracle Extended 支持截止 Java 17 LTS 2021-09 2026-09 2029-09 Java 21 LTS 2023-09 2028-09 2031-09 Java 25 LTS 2025-09 2030-09 2033-09 Java 26 非 LTS …","date":"2026-05-16","keywords":["Java 25","Spring Boot 4","平台升级","OpenRewrite","虚拟线程","技术债"],"permalink":"/posts/java25-springboot4-upgrade-guide/","tags":["Java","Spring Boot","升级","OpenRewrite","架构"],"title":"Java 21→25 与 Spring Boot 3.5→4.0：为什么升级，怎么升级"},{"content":"原文链接 diegolnasc/kubernetes-best-practices 前言 这份 GitHub 仓库是一份覆盖面相当广的 K8s 实践手册，从集群基础配置、安全、网络到 GitOps 都有涉及，对于想系统梳理 K8s 知识体系的工程师来说很有参考价值。\n但该仓库最后更新停留在较早期的内容，遗漏了 K8s 近几个版本中已经稳定（或接近稳定）的重要特性，最典型的就是 Gateway API。本文以仓库内容为骨架，加入我自己的实践理解，同时补充这些新特性，力求给出一份更完整的参考。\n一、集群基础：规划要在最前面 网络 CIDR 规划 集群建好之后最难改的就是网络。在创建集群之前必须认真计算三块地址段：\n节点子网：根据最大节点数预留，留足余量 Pod CIDR：每个节点 maxPods 数量决定每个节点需要多大的子网块。例如每节点最多 110 个 Pod，则每节点需要 /25（128 个地址），256 个节点就需要 /17 Service CIDR：独立规划，避免与内网路由冲突 对于托管 K8s（GKE/EKS/AKS），云厂商通常会额外保留部分地址，规划时要读清楚文档， …","date":"2026-05-16","keywords":["kubernetes","k8s最佳实践","gateway api","pod security","rbac","hpa","gitops","network policy"],"permalink":"/reading/github/kubernetes-best-practices/","tags":["kubernetes","cloud-native","devops","gateway-api","security"],"title":"Kubernetes 最佳实践阅读笔记：核心实践与 Gateway API 等新特性补充"},{"content":"Contract First（也叫 API First）的理念并不新鲜：在写任何代码之前，先把 API 的形状定义清楚，让前后端、移动端、第三方消费者都从这份约定出发并行开发。但这套思路在很多团队里一直停留在 PPT，原因往往不是不认可，而是落地成本比想象中高——谁来写 spec、风格谁来管、变更谁来通知、breaking change 谁来拦，每一个问题都可能让一个团队悄悄退回 Code First。\n这篇是我自己在 Spring Boot + 前端栈（TypeScript / React 或 Angular）里反复试错后整理的实践笔记，关注的是怎么让 Contract First 跑起来，并尽量持续不退回老路，而不是介绍 OpenAPI 语法。\n如果你只想看 JVM 团队怎么把 contract 做成可执行验证（Spring Cloud Contract、WireMock、CI Quality Gates），可以直接跳到《Java 项目怎么做 contract testing：一次 Spring Cloud Contract 实践》。本文聚焦在 spec、协作和治理这一层。\n为什么 …","date":"2026-05-06","keywords":["contract first","api first","openapi 3.1","api governance","api style guide","oasdiff","breaking change","contract testing","前后端协作"],"permalink":"/posts/openapi-contract-first-workflow/","tags":["openapi","contract-first","api-first","api-design","api-governance","ai","collaboration"],"title":"Contract First 落地实践：工具栈、团队协作与我踩到的坑"},{"content":"至少从 2025 年下半年到我写这篇（2026-05-06）这段产品叙事看，\u0026amp;ldquo;Agent Skills\u0026amp;quot;是一个在 AI 工具圈快速升温的概念：Anthropic 在 Claude / Claude Code 里推它，Cursor、GitHub Copilot、Gemini CLI、OpenCode、Goose、Roo Code 等一批工具也陆续宣布支持，agentskills.io 则把它整理成了一个开放标准。至少在叙事层面，它确实有点像 AI 工具圈想长出来的一个\u0026amp;quot;USB-C 接口\u0026amp;rdquo;。\n但热闹归热闹，更冷静的那一面是：skill 真的有用吗？什么时候有用？怎么知道它有用？ 这一篇我把三份资料串起来聊一下：\nagentskills.io 给出 skill 的开放定义和加载机制； SWE-Skills-Bench 用基准实测提醒我们：公开 skill 在真实仓库里未必会带来增益； Angular 社区开发者 Daniel Sogl 那篇 Skills Without Evals Are Just Markdown and Hope， …","date":"2026-05-06","keywords":["agent skills","skill 规范","SKILL.md","progressive disclosure","skill evals","SWE-Skills-Bench"],"permalink":"/posts/agent-skills-spec-and-evals/","tags":["agent-skills","ai-agent","claude-code","evals","prompt-engineering"],"title":"我怎么理解 AI Agent Skills：规范、实证，以及落地时的取舍"},{"content":"2023 年，Amazon Prime Video 公开了一个架构优化案例：他们把一个音视频质量监控 pipeline 从分布式 serverless 组件合并成单进程应用，运营成本降了 90%。很多转述把它概括成「Amazon 放弃微服务」，但这恰恰是最没有价值的读法。\n更有价值的问题是：为什么这些大公司在不同阶段做出了完全不同的架构选择？为什么有的继续扩展几千个微服务，有的把某条链路合并回进程内，有的坚持模块化单体，有的把治理重点从服务迁移到 cell、supergraph、data contract？\n下面这七个案例不是为了证明「微服务好」或「单体好」。它们更像七个坐标点，帮助我们判断：在什么规模、约束和组织形态下，服务边界的收益还能不能覆盖它带来的成本。\n七个案例 Amazon Prime Video — 把一条高吞吐 pipeline 合并回单进程（2023） 做了什么： Prime Video 的 Video Quality Analysis 团队原本用 Step Functions、Lambda、S3 等 serverless 组件串起音视频质量检测流程。后来他们把媒体转 …","date":"2026-05-04","keywords":["微服务边界治理","微服务演进","Amazon Prime Video 单体回归","Monzo 2800 微服务","DoorDash Service Mesh","Shopify 模块化单体","Uber DOMA","Netflix Federation","Grab Data Mesh","服务边界决策","平台工程"],"permalink":"/posts/microservices-boundary-governance-2019-2026/","tags":["microservices","architecture","platform-engineering","data-mesh","modular-monolith","cell-architecture"],"title":"微服务之后，大厂如何重新治理边界：2019–2026 年的七个案例"},{"content":"为什么需要一份\u0026amp;quot;运行时基线\u0026amp;quot; 很多团队在做 Java 微服务上 Kubernetes 时，JVM 调优和业务代码规范写得很细，但介于两者之间的一层——镜像怎么打、探针怎么配、Pod 停机和应用停机怎么对齐、滚动策略怎么跟服务形态匹配——往往散在 Helm chart、Dockerfile 和某个老员工的脑子里。\n这篇是对这一层的整理。它不是一份\u0026amp;quot;最佳实践\u0026amp;quot;，更像是一份\u0026amp;quot;基线\u0026amp;quot;：给一份新服务起步时不会出大问题的最小集，再加上一份知道未来要往哪儿收敛的方向。文中会明确区分\u0026amp;quot;当前常见做法\u0026amp;quot;和\u0026amp;quot;推荐改进方向\u0026amp;quot;，方便对号入座。\n适用范围：Java 25 + Spring Boot 3.5（Spring MVC） 这条主流栈。Reactive（WebFlux + R2DBC）的运行时模型不在这里讨论。\n镜像构建：从能跑到敢跑 一个最小可运行的 Dockerfile 通常长这样：\nFROM eclipse-temurin:25-jdk-noble WORKDIR /app COPY …","date":"2026-04-30","keywords":["java k8s runtime","spring boot 3.5 kubernetes","actuator probe","graceful shutdown","rolling update","ServiceMonitor","OpenTelemetry javaagent"],"permalink":"/posts/java-microservice-k8s-runtime-baseline/","tags":["java","spring-boot","kubernetes","cloud-native","observability"],"title":"Java 微服务在 K8s 上的运行时基线（2026）：镜像、探针、滚动与可观测"},{"content":" 本文是 《Spring Boot 3.5 + Java 25 + React：在 K8s 里跑通一套跨链路 OpenFeature flag》 的续篇，聚焦三个场景中的第三个——全链路共享 flag 在升级过程中的顺序选择。\n对应的 demo 代码仓库：sb3-k8s-hot-reload（私有）。\n从一篇文章引发的真实问题讲起 上一篇写完后，有人在评论区提了一个非常实际的问题：\n\u0026amp;ldquo;你说的全链路 flag（full-stack shared flag）确实好理解——后端在 gateway 评估，前端通过 /experience/shared-flags 拿到同样的值。但实际加一个新 flag 的时候，先改哪一边？\u0026amp;rdquo;\n这个问题戳中了全链路 flag 最微妙的地方。\n假设你的团队要上线一个\u0026amp;quot;会员折扣算法 v2\u0026amp;quot;功能：\n后端：pricing-service 要用新算法计算价格 前端：UI 要在会员用户上展示\u0026amp;quot;v2 折扣体验\u0026amp;quot; 目标：u-vip-* 用户看到新体验，其他用户保持原样 先升级前端还是先升级后端？ 这不是一个语义问 …","date":"2026-04-30","keywords":["全链路 flag","feature flag","OpenFeature","flagd","OFREP","Spring Cloud Gateway MVC","ScopedValue","共享快照","升级顺序","回滚","backend-first","frontend-only","backend-only"],"permalink":"/posts/full-stack-flag-upgrade-order/","tags":["spring-boot","java25","openfeature","flagd","ofrep","spring-cloud-gateway","kubernetes","kind","react","full-stack","rollback"],"title":"全链路 Feature Flag 的升级顺序：先 backend 还是先 frontend？"},{"content":"调试 Kubernetes 里的服务有多烦？本地改一行代码，要走完 mvn package → docker build → kind load → kubectl rollout restart 这一套，少则一两分钟，多则好几分钟。而且这还只是单服务，如果服务依赖同命名空间里的 MySQL、Redis 或者其他微服务，kubectl port-forward 也救不了你——端口转发不能同时暴露多个服务的网络环境，更没法让你的本地进程「假装」是 Pod 本身。\nmirrord 解决的正是这个问题。这篇文章通过一个完整的 Demo 项目，带你从零走完「本地进程通过 mirrord 接入 k8s 集群调试」的完整流程。\n问题：开发者与 K8s 之间的摩擦 常见的几种调试方案，各有明显局限：\n方案 痛点 kubectl port-forward 只能转发单个端口，进程本身感知不到集群的服务发现和环境变量 Skaffold / Tilt 热重载 仍然需要 build → load → rollout，只是自动化了步骤，没有消除延迟 在 Pod 内直接 exec 调试体验差，IDE …","date":"2026-04-28","keywords":["mirrord","local k8s debug","kubernetes 本地调试","mirrord 教程","kind","spring boot k8s","流量拦截","steal traffic"],"permalink":"/posts/mirrord-local-k8s-debug/","tags":["kubernetes","kind","mirrord","spring-boot","local-development","debugging"],"title":"用 mirrord 把本地进程接入 K8s 集群：从 Demo 到真实调试实践"},{"content":"背景 Spring Boot 3.5 配合 Java 25，已经把“同步写法 + virtual threads”这条路线变成了很现实的工程选择。JDK 在 JEP 444 里把 virtual threads 的目标讲得很清楚：让 thread-per-request 风格更容易扩展；Spring Boot 官方文档也提供了 spring.threads.virtual.enabled=true 这条开关入口。\n于是一个常见问题会重新冒出来：当一个微服务会同时访问 HTTP 下游、Redis、Kafka、数据库时，Resilience4j 应该怎么用？\n这篇文章不是“所有外部调用统一套一层 Resilience4j 模板”的教程，而是一篇事实约束下的探索综述。文中的强结论优先建立在仍在维护的官方资料和工程文档上，例如 Spring / Resilience4j、AWS Builders Library、Azure Architecture Center、Google Cloud retry strategy、Redis 官方文档、Spring for Apache Kafka 文档  …","date":"2026-04-28","keywords":["spring boot 3.5","java 25","virtual threads","resilience4j","circuit breaker","retry","redis","kafka","database","microservices"],"permalink":"/posts/spring-boot-resilience4j-external-calls-best-practices/","tags":["spring-boot","resilience4j","java25","virtual-threads","microservices","redis","kafka","database"],"title":"Spring Boot 3.5 + Java 25 微服务里，Resilience4j 用在 HTTP、Redis、Kafka、DB 上的边界与最佳实践"},{"content":"背景 最近几个月我翻了几份团队内部和合作方写的「AI 辅助开发流程」文档，也在不同项目里反复用了 Claude Code、Codex CLI、Cursor。到目前为止，一个让我印象比较深的现象是：在我接触到的不少团队里，「AI 辅助」仍然更接近 2024 年常见的\u0026amp;quot;对话框 + 复制粘贴\u0026amp;quot;模式——人手动把 PRD、Tech Design 模板、代码上下文一次次拷进 Prompt 框，AI 输出一段文本，人再贴回 Confluence 或 IDE。\n而同期一些更早公开分享这类实践的团队，做法已经不太一样：\nAnthropic 的公开文章展示了多个团队如何把 Claude Code 用到 code review、排障、代码导航等具体场景里（原文）。 Rakuten 的案例文章给出了一组公开数据：TTM 从 24 个工作日降到 5 天，最长一次连续自主编码 7 小时；这些数字来自其官方案例分享，更适合当作参考样本，而不是直接当成通用基线（原文）。 Bessemer Atlas 对 Shopify 工程负责人的采访提到，Shopify 允许 Claude …","date":"2026-04-26","keywords":["agentic workflow","AI 辅助开发","团队协作转型","高质量交付","MCP","Claude Code","spec-driven"],"permalink":"/posts/agentic-workflow-engineering-2026/","tags":["agentic","ai-coding","team-workflow","claude-code","software-engineering"],"title":"工程化引入 Agentic Workflow：一些关于质量与协作转型的观察"},{"content":" 本文对应的 demo 项目：sb3-k8s-hot-reload（私有）。代码组织在 gateway/ order-service/ pricing-service/ ui/ k8s/ scripts/ 下，一条 ./scripts/e2e-demo.sh 从 kind 集群创建到端到端验证全跑完。\n起点：一个朴素的约束 接到的题目是这样的：\n在 Spring Boot 3.5 + Java 25 + Kubernetes（kind 本地）里，不用 Spring Cloud Config Server，不用 Netflix 套件（Eureka / Zuul / Ribbon / Hystrix）的前提下，验证一下\u0026amp;quot;运行时配置变更不重启服务\u0026amp;quot;这件事到底有哪些解、各自取舍是什么。后续要能支持 feature flag。\n约束被排除的两块是有理由的：Config Server 在 K8s 原生场景里往往不再是首选（ConfigMap/Secret 已经在那里），Netflix 套件在 Spring Cloud 2023 起官方也已停止维护。但 Spring Cloud  …","date":"2026-04-26","keywords":["Spring Boot 3.5","Java 25","OpenFeature","flagd","OFREP","feature flag","Spring Cloud Gateway MVC","@ConfigurationProperties","热加载","kind","Virtual Threads"],"permalink":"/posts/spring-boot-openfeature-flagd-cross-stack/","tags":["spring-boot","java25","openfeature","flagd","ofrep","spring-cloud-gateway","kubernetes","kind","react","virtual-threads"],"title":"Spring Boot 3.5 + Java 25 + React：在 K8s 里跑通一套跨链路 OpenFeature flag"},{"content":"API 版本管理是微服务架构中一个看似简单、实则充满细节的话题。客户端很多，升级节奏也各不相同，一个\u0026amp;quot;破坏性变更\u0026amp;quot;稍有不慎就可能让线上支付或推送流程出问题。本文尝试梳理四种主流策略，并逐一对比 Spring Boot 4 原生支持与历史实现方案，最后总结生产环境中常见的问题。\n为什么需要版本管理 REST API 一旦对外发布，就变成了一份\u0026amp;quot;隐式合同\u0026amp;quot;。只要还有一个客户端在运行，你就不能随意更改字段含义、删除字段或修改响应结构。版本管理的本质，是为破坏性变更（Breaking Change）提供一条可控的演进通道，让新旧客户端能在同一套基础设施上共存。\n什么算 Breaking Change？\n删除或重命名字段 修改字段类型（如 amount: int → amount: BigDecimal） 修改 HTTP 状态码语义 业务流程从同步改为异步 认证机制变更（如从 API Key 改为 OAuth2） 非破坏性变更（如新增可选字段、新增接口）通常不需要升版本。\n四种主流策略 1. URL 路径版本化 版本号嵌入 URL 路径，是目前很常见的方 …","date":"2026-04-26","keywords":["api versioning","api version","spring boot 4","spring framework 7","url versioning","header versioning","content negotiation","breaking change","接口版本管理"],"permalink":"/posts/api-versioning-strategies/","tags":["api-design","spring-boot","rest","微服务","架构演进"],"title":"REST API 版本管理：四种常见策略、Spring Boot 4 原生支持与一些陷阱"},{"content":" 本文用一组「问答」拆解 Liquibase（XML 格式）在 Spring Boot 微服务下我更倾向采用的一些实践经验。文中的反例都来自真实可见的代码风格——表名、字段、author 全部脱敏，不指向任何具体项目。每条做法尽量给出官方文档或社区可访问的来源。\n一、Schema 应该住在哪个 repo？ Q：为什么我现在不太倾向继续用一个独立的 db-* 仓库（或子模块）单独保存所有 MS 的 schema？\n历史上常见的做法是开一个 database-foo、database-bar 的项目，里面只放 liquibase.properties + 一堆 change-log/*.xml，跑 mvn liquibase:update 完成迁移。问题是：\nschema 和服务代码不在同一个生命周期里。MS 加了一个 entity_id 字段，PR 在服务 repo；migration 在另一个 repo。两个 PR 难以原子合并，code review 也只能看半边。 CI 触发不一致。服务 repo 的测试不会跑 migration repo 的最新变更；migration repo …","date":"2026-04-26","keywords":["liquibase 实践经验","liquibase XML","changelog 组织","expand contract","DATABASECHANGELOGLOCK","init container","policy checks","modifyDataType","tagDatabase","context labels","Testcontainers"],"permalink":"/posts/best-practice-for-liquibase/","tags":["liquibase","database migration","microservice","spring-boot","mysql","ci-cd","xml"],"title":"Liquibase (XML) 在微服务里的实践记录：一份问答式整理"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n开场：一次被拒绝的 git push 让 Claude Code 帮我把一批 commits 推到 origin/main，得到的不是成功，是一段克制但毫不含糊的拒绝：\nPermission for this action has been denied. Reason: Pushing directly to main bypasses PR review; commits should go to a feature branch. 奇怪的是，我的项目 .claude/settings.local.json 里有 142 条 Bash(...) allow 规则，却没有任何 deny；用户级 ~/.claude/settings.json 也只配了 permissions.defaultMode: \u0026amp;quot;auto\u0026amp;quot; 一行。这条拦截看起来是凭空冒出来的——但它的措辞具体到了\u0026amp;quot;PR review\u0026amp;quot;和\u0026amp;quot;feature branch\u0026amp;quot;，又像是某个真人写过的策略。\n这篇文章想 …","date":"2026-04-26","keywords":["Claude Code harness","vibe coding 安全","prompt injection 防御","AI agent 权限系统","Sonnet 4.6 classifier","Cursor allowlist CVE","Devin sandbox","Aider microVM","Codex CLI sandbox","GitHub Copilot coding agent","Anthropic auto mode","agent harness arXiv 2604.18071","vibe coding Replit Lovable","Karpathy vibe coding"],"permalink":"/posts/claude-code-harness-vibe-coding-2026/","tags":["claude-code","vibe-coding","ai-agent","harness","security","prompt-injection","sandbox","permission-system"],"title":"Claude Code 为什么会拒绝我？harness 与 vibe coding 时代的工程边界"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n服务数到 5 个时，靠口口相传和 README 还能维护。到 16 个时，新人入职第二周问的\u0026amp;quot;order-service 发什么事件、谁在消费、出问题找谁\u0026amp;quot;已经没有一个人能答全。这是我在 shop-platform 遇到的一个规模拐点。本文记录两份成本相对较低、回报也比较稳定的治理文档：ADR（架构决策记录）和 Service Catalog（服务目录）。至少在我这套项目里，它们值得投入。\n一、ADR 是什么、为什么需要 Q：架构文档不是已经在 README 和 design doc 里了吗？\n它们描述当前的形态，不描述为什么形成这种形态。\n举例：项目里有一条规则\u0026amp;quot;@HttpExchange 客户端必须放在调用方模块、不能放共享模块\u0026amp;quot;。架构文档会写\u0026amp;quot;这是约定\u0026amp;quot;。但答不出来——\n为什么？业界普遍是\u0026amp;quot;共享 client SDK\u0026amp;quot;模式。 之前是不是试过共享模式？为什么放弃？ 改回共享模式会发生什么？ 这些问题的答案是决策的上下文，半年后所有当事人都会 …","date":"2026-04-26","keywords":["Architecture Decision Record","MADR 0.4.0 模板","ADR retrospective","Backstage catalog-info.yaml","Service Catalog 约定","微服务所有权管理","16 services 规模拐点","架构治理","shop-platform ADR"],"permalink":"/posts/adr-and-service-catalog-2026/","tags":["architecture","adr","madr","backstage","service-catalog","documentation","governance","microservice"],"title":"ADR 与 Service Catalog：我在架构治理里反复用到的两类文档"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n阈值告警（\u0026amp;ldquo;5xx \u0026amp;gt; 1% 持续 5 分钟\u0026amp;rdquo;）的两个失败模式工程师都熟悉：阈值定低了一周告警一百条没人看，阈值定高了真出事告警来得太晚。Google SRE Workbook 第 5 章给出的思路是 error budget + multi-window multi-burn-rate——让告警的“敏感度”随事件严重程度变化。本文更像是一次整理：为什么、怎么做、以及落地时绕不开的取舍。\n一、SLO 和 error budget 的底层逻辑 Q：SLO 不就是\u0026amp;quot;99.9% 可用\u0026amp;quot;这种数字吗？跟告警有什么关系？\n至少在工程落地里，SLO 一个很重要的用途是把 reliability 变成预算。\nSLO 99.9% 意味着每个 30 天窗口允许 0.1% 的请求失败——这就是 error budget。 把整个 30 天窗口的 budget 按时间切片：1 分钟内允许失败的请求量很小（少量异常就把这分钟的 budget 烧光），但 30 天内的累计可以容忍。 如果短窗口内的烧速大 …","date":"2026-04-26","keywords":["SLO error budget","multi-window multi-burn-rate alert","Google SRE Workbook ch5","Prometheus recording rules","burn rate 14.4x 6x","availability SLO","latency SLO histogram_quantile","fast burn slow burn","shop-platform SLO 落地"],"permalink":"/posts/slo-error-budget-multi-burnrate-2026/","tags":["sre","slo","error-budget","prometheus","alerting","burn-rate","observability","k8s","google-sre"],"title":"SLO 与多窗口多 burn-rate 告警：一次 Prometheus 落地整理"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\nLog4Shell 让全行业意识到：没人能回答\u0026amp;quot;我用了哪些库的哪些版本\u0026amp;quot; 的时候，所谓应急响应就是几千个工程师在同时翻自己的 pom.xml。SLSA、cosign、Sigstore 这套工具链，更多是在试着把这个问题收敛成一次可查询的清单比对。本文聚焦最小基线：每个产物有 SBOM、每个镜像有签名、SBOM 作为 attestation 绑定到镜像。\n一、为什么 SBOM 是底线 Q：SBOM 到底是什么，跟以前我们说的\u0026amp;quot;依赖列表\u0026amp;quot;有什么本质区别？\nSBOM（Software Bill of Materials）是一份机器可读的、可校验的依赖清单，每条记录包含：\n名称、版本、purl（package URL，唯一定位坐标） 哈希（SHA-256，验证产物未被篡改） 许可证 直接 / 间接依赖关系（树状结构） 可选：CVE / VEX 状态 mvn dependency:tree 生成的是给人看的文本；SBOM 是给工具看的、跨语言统一的 JSON 文档。Log4Shell 当时如果每 …","date":"2026-04-26","keywords":["软件供应链安全","SBOM 生成","CycloneDX vs SPDX","cyclonedx-maven-plugin","cosign 镜像签名","Sigstore keyless OIDC","SLSA Level 2","SBOM attestation","transparency log","Kyverno verify image"],"permalink":"/posts/software-supply-chain-sbom-cosign-2026/","tags":["supply-chain","sbom","cyclonedx","cosign","sigstore","slsa","spring-boot","k8s","java25","security"],"title":"软件供应链最小基线：SBOM + cosign 镜像签名"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n上一篇 微服务 contract 兼容性的五层防线：从 ArchUnit 到 japicmp 讨论了如何用 ArchUnit、japicmp、WireMock 和运行时 Deprecation 头构建基础安全带。本文聚焦更具体的问题：BFF 对外的 API、BFF→MS 和 MS→MS 的内部 API、以及 Kafka 事件，各自应该用什么工具保障 contract 兼容性，这些工具的工作机制是什么。\n一、两类 contract，两种保障方式 Q：BFF 对外暴露的 API 和 BFF→MS、MS→MS 之间的内部 API，兼容性的保障方式应该一样吗？\n在我的实践中不太一样。主要原因在于 source of truth 不同。\nBFF 对外的 API（前端、移动端、三方调用方）采用的是 API-first 方式：先有 OpenAPI YAML，生产者的 controller 实现这份 YAML，消费者根据这份 YAML 生成客户端代码或手写调用。在这条链路里，YAML 就是权威来源。保障兼容性就是保障 YAML 文件本身不引 …","date":"2026-04-25","keywords":["API 兼容性","oasdiff breaking change","JSON Schema 快照","victools jsonschema-generator","Kafka event contract","code-first 兼容性","api-first openapi","EventEnvelope schemaVersion","springdoc-openapi","微服务 contract"],"permalink":"/posts/api-event-contract-compatibility-qa-2026/","tags":["microservice","compatibility","contract","openapi","oasdiff","json-schema","kafka","spring-boot","java25"],"title":"API 与 event contract 兼容性保障：工具机制与正确用法"},{"content":" 配套阅读：shop-starter-http-client 设计 介绍完整的 starter 设计；本文专注于其中\u0026amp;quot;连接配置\u0026amp;quot;这一块的细节决定。\n1. 问题场景 典型的 BFF 模式：\ngraph LR BFF[buyer-bff] --\u0026gt; M[marketplace-service] BFF --\u0026gt; O[order-service] BFF --\u0026gt; L[loyalty-service] BFF --\u0026gt; P[promotion-service] BFF --\u0026gt; S[search-service] BFF -. 偶尔 .-\u0026gt; EXT[第三方支付 API] 每个下游的特征可能完全不同：marketplace 响应 50ms，搜索可能要 5s；payment 是外部 HTTPS API，order 是内部 cleartext；某个服务上了 HTTP/2，其他还是 HTTP/1.1。\n三个绕不开的问题：\n不同下游应该共用一个连接池，还是每个独立？ connectTimeout / readTimeout 怎么按服务定制？ 内部服务用 HTTP/2（h2c）值不值得开？怎么 …","date":"2026-04-25","keywords":["JdkHttpClient","RestClient","connection pool","connectTimeout","readTimeout","h2c","Tomcat","Undertow","Spring Boot 4","TomcatHttp2AutoConfiguration"],"permalink":"/posts/bff-http-client-pool-timeout-and-h2c-2026/","tags":["spring-boot","java25","virtual-threads","http-client","http2","h2c","microservice","bff"],"title":"Spring Boot 3.5 BFF 出站 HTTP 客户端：连接池、超时与 HTTP/2 实战"},{"content":" 这是 shop-starter-http-client 的集成指南，按这套步骤通常可以跑起第一个出站客户端。\n想了解 starter 内部如何设计：shop-starter-http-client 设计 想深入连接池/超时/HTTP2 的取舍：BFF 连接池与 HTTP/2 实战 前置条件 项 要求 JDK Java 21+（建议 Java 25） Spring Boot 3.5.x 依赖 已通过 shop-common-bom / shop-contracts-bom 管理共享版本 可观测性（推荐） micrometer-tracing-bridge-otel、micrometer-registry-prometheus 5 步集成 下面以 buyer-bff 调用 marketplace-service 列商品接口为例，从 0 走完。\ngraph LR A[Step 1加依赖] --\u0026gt; B[Step 2配 application.yml] B --\u0026gt; C[Step 3声明 @HttpExchange 接口] C --\u0026gt; D[Step 4注入 Support 建代理 Bean] D …","date":"2026-04-25","keywords":["shop-starter-http-client","ShopHttpExchangeSupport","@HttpExchange","RestClient","step-by-step","集成指南"],"permalink":"/posts/shop-starter-http-client-integration-2026/","tags":["spring-boot","java25","http-exchange","starter","integration","step-by-step","bff"],"title":"shop-starter-http-client 集成指南：从零到第一个 @HttpExchange 客户端"},{"content":"背景 Homelab 跑了一套 LGTM 可观测性栈（Loki + Grafana + Tempo + Prometheus），Prometheus Alertmanager 已经在收集各种告警，但一直没有配出通知渠道——告警只是静静地积在队列里，没有任何推送。\nGotify 是一个轻量的自托管推送通知服务，已经在集群里跑着了，主要用于接收 Redpanda Connect 推送的消息。顺手把 Alertmanager 的告警也接进来，省得再引入第三方服务。\n架构 Alertmanager 的 webhook receiver 发送的是自己定义的 JSON 格式；Gotify 的 POST /message API 期望的是另一套字段（title / message / priority）。两者不能直接对接，需要一个中间层做格式转换。\nDRuggeri/alertmanager_gotify_bridge 是一个专门做这件事的小服务。它的 README 说明默认监听 /gotify_webhook，并把收到的 Alertmanager webhook 转换后转发到 Gotify。 …","date":"2026-04-25","keywords":["alertmanager gotify","alertmanager webhook","gotify bridge","homelab 告警","prometheus alertmanager","gotify k8s","alertmanager_gotify_bridge"],"permalink":"/posts/homelab-alertmanager-gotify-bridge/","tags":["homelab","k3s","prometheus","alertmanager","gotify","vault","external-secrets","monitoring"],"title":"Homelab 消息通知：Alertmanager 通过 Gotify 推送告警"},{"content":" 代码背景来自开源项目 meirongdev/shop，Java 25 + Spring Boot 3.5 + Spring Cloud 微服务电商平台。\n本文讲设计——starter 内部为什么这么写。如果你的目标是集成 starter，跟着步骤跑通第一个客户端，请直接看 shop-starter-http-client 集成指南。\n配套阅读：Spring Boot 3.5 微服务 tracing 为什么会断链。\n1. 背景 项目是典型的 BFF（Backend For Frontend）模式：\ngraph LR C[Client] --\u0026gt; G[api-gateway] G --\u0026gt; BB[buyer-bff] G --\u0026gt; SB[seller-bff] BB --\u0026gt; M[marketplace] BB --\u0026gt; O[order] BB --\u0026gt; L[loyalty] BB --\u0026gt; P[promotion] SB --\u0026gt; M SB --\u0026gt; O 每个 BFF 都要向多个 domain service 发出站请求。shop-starter-http-client …","date":"2026-04-24","keywords":["shop-starter-http-client","RestClient","HttpExchange","TracingHeaderInterceptor","DownstreamException","ShopHttpExchangeSupport","ContextSnapshot","virtual-threads","downstream.service"],"permalink":"/posts/shop-starter-http-client/","tags":["spring-boot","restclient","http-exchange","micrometer","tracing","virtual-threads","microservice","java25"],"title":"shop-starter-http-client：Spring Boot 3.5 微服务 HTTP 客户端基础设施设计"},{"content":"背景与动机 gws 是一个面向 Google Workspace API 的命令行工具。它的定位很直接：用一套统一的 CLI 去访问 Drive、Gmail、Calendar、Sheets、Docs、Chat 等 API，并且直接输出结构化 JSON。按项目 README 的说法，它是 “built for humans and AI agents”，命令面还是根据 Google 的 Discovery Service 动态生成的。\n这里有个事实要先说清楚：它不是 Google 官方正式支持的产品。README 明确写了 “This is not an officially supported Google product.” 所以更准确的说法不是“Google 正式推出了一个新 OAuth 工具”，而是 Google Workspace 生态里出现了一个很实用的开源 CLI，它试图把大量样板代码和认证细节集中到一个工具里。\n跟传统的 Google OAuth 接法相比，gws 的好处主要在工程层面。按 Google 官方的 OAuth 2.0 文档 和 Desktop App 流程， …","date":"2026-04-23","keywords":["gws auth export","google workspace cli","oauth refresh token","calendar api","drive api"],"permalink":"/posts/gws-auth-export-pitfalls/","tags":["gws","google-workspace-cli","oauth","python","google-api"],"title":"用 gws 给 Python 脚本提供 Google OAuth 凭据时的一些注意点"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n先说我的做法 如果你在做一组 Java 微服务，已经把 contracts 独立成 jar、每个 caller 自己写 client，你会很快遇到下一个问题：谁来阻止下一次 contracts 变更悄悄打穿所有下游？\n我现在更倾向的做法是——把五层防线组合起来，单独任何一层都不太够：\nArchUnit COMPAT-01：每个 *Api 的 BASE_PATH 最好带显式版本段（/vN 或 /internal）。 ArchUnit COMPAT-02：事件契约最好容忍未知字段，并带上 schemaVersion。 ArchUnit COMPAT-03：任何 @ApiDeprecation 最好都声明 replacement。 japicmp 二进制兼容门禁：在 Maven verify 阶段对比新旧 contracts jar，binary-incompatible 变更直接 fail build。 WireMock 契约测试：consumer 侧冻结自己对契约的理解，任何反序列化漂移都暴露在单元测试阶段。 …","date":"2026-04-22","keywords":["微服务契约兼容性","japicmp 二进制兼容","API 版本演化","RFC 8594 Sunset header","Deprecation header","ArchUnit 兼容性规则","Kafka schema evolution","Spring Cloud Contract","shop-starter-api-compat"],"permalink":"/posts/microservice-contract-compatibility-defense-in-depth/","tags":["microservice","compatibility","contract","archunit","japicmp","spring-boot","rest-api"],"title":"微服务契约兼容性的五层防线：从 ArchUnit 到 japicmp"},{"content":"为什么同时用几个 coding agent，配置会越写越乱？ 单用一家 coding agent 时，指令写在哪其实不是问题——它能读到就行。但从第二家开始，问题就来了：同一条\u0026amp;quot;运行测试用什么命令\u0026amp;quot;要在 CLAUDE.md、AGENTS.md、.github/copilot-instructions.md 里各写一份；同一份 \u0026amp;ldquo;PR review 流程\u0026amp;rdquo; 要在 .claude/skills/、.qwen/skills/、.agents/skills/ 里各放一份副本。维护一阵之后，哪份落后了、哪份还没更新，基本靠记忆。\n这篇笔记就是围绕这件事展开的：哪些内容应该共享、哪些注定各写各的、共享的那部分怎么组织才不至于每次都手抄。\n\u0026amp;ldquo;always-on 指令\u0026amp;quot;和 \u0026amp;ldquo;skill\u0026amp;rdquo; 到底不是一回事？ 先把两个经常被混用的词分开。\nAlways-on 指令文件：agent 启动时就加载到系统提示里、每次对话都在的那一段。Claude Code 的 CLAUDE.md、Codex …","date":"2026-04-22","keywords":["personal skill set","agent skills","claude code skills","github copilot agent skills","qwen code skills","codex skills"],"permalink":"/posts/personal-skill-set-for-ai-coding-agents/","tags":["ai","skills","claude-code","github-copilot","qwen-code","codex"],"title":"同时用 Claude Code、Copilot、Qwen、Codex，个人 skill 该怎么组织？"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n先给结论 如果你只想先看我的当前结论，再决定要不要继续往下看，可以先看这一版：\n我更倾向于共享 contracts，而不是共享 client 接口。 即使在 monorepo 里，我也会尽早把 shared client 下沉到各 caller 自己的 client/ 包。 如果未来要拆成 polyrepo，我目前更偏向“共享 contracts artifact + caller 自己写 client + 契约测试兜底”。 只有当你真的面临多语言 SDK、外部开放接口，或者已经有成熟的平台治理能力时，我才会认真考虑 spec-first + 代码生成。 换句话说，这篇文章真正要回答的不是“Client 接口写在哪”，而是：跨服务的边界，到底该共享“数据契约”，还是连“调用方式”也一起共享。\n起点：一个具体的问题 先用一个具体例子，把问题落到地上。\n在早期的 shop 项目里，BFF 调下游 domain service 的链路是这样的：\nshared/shop-contracts/shop-contracts-*： …","date":"2026-04-22","keywords":["微服务契约共享","shared client library 反模式","polyrepo 契约","OpenAPI 契约优先","consumer-driven contract","Spring HttpExchange","BFF 微服务交互","契约测试"],"permalink":"/posts/microservices-contract-sharing-polyrepo-tradeoffs/","tags":["microservice","architecture","contract","polyrepo","monorepo","openapi","spring-boot"],"title":"微服务契约共享的 Tradeoff：从 Monorepo 到 Polyrepo，该共享到哪一步"},{"content":" 📦 本文基于的完整项目源码：meirongdev/shop\n背景：在 code agent 时代重新谈 ArchUnit 这两年很多团队都在引入 code agent。它们写代码的速度很快，能补样板、改依赖、重构包结构、批量修复低级问题，但也因此更容易把\u0026amp;quot;本来只存在于文档和 review 习惯里的约束\u0026amp;quot;绕过去。\n下面这些问题，人在赶时间时会犯，agent 在缺少硬约束时也会犯：\n为了省事直接加 @Autowired 字段注入 在 BFF 里偷偷落一个 @Entity Controller 直接访问 Repository Kafka consumer 没有幂等保护 契约模块慢慢混入 Spring Web / JPA 依赖 这些都不是\u0026amp;quot;功能对不对\u0026amp;quot;的问题，而是\u0026amp;quot;代码还在不在团队允许的边界里\u0026amp;quot;的问题。在我看来，ArchUnit 的价值就在这里：它把架构约束从口头约定变成可执行测试。\n这个视角并不是孤立的观察。Birgitta Böckeler 在 2026 年 2 月的 Harness Engineering, First …","date":"2026-04-21","keywords":["ArchUnit harness","架构测试","微服务架构治理","monorepo 工程治理","普通 repo 架构规则","code agent guardrails","JUnit architecture tests"],"permalink":"/posts/archunit-harness-microservices-monorepo/","tags":["archunit","java","microservice","monorepo","architecture","code-agent"],"title":"ArchUnit 作为 Code Agent 时代的 Harness：微服务、Monorepo 与普通 Repo 的落地方式"},{"content":"背景 一个团队维护多个 Spring Boot 服务时，常见的痛点是：\n每个新服务都要从头抄一遍 pom.xml、application.yaml、logback-spring.xml、Dockerfile、Makefile…… 抄着抄着就出现\u0026amp;quot;A 服务用了新的 actuator 配置，B 服务还是旧的\u0026amp;quot;这种偏差。 Onboarding 新成员时，\u0026amp;ldquo;我要建个新服务\u0026amp;quot;变成一个需要问老人、翻三个仓库的事情。 一个比较务实的解决方式：从现有的成熟项目里，抽象出 Maven Archetype。以后建新服务直接：\nmvn archetype:generate \\ -DarchetypeGroupId=com.example \\ -DarchetypeArtifactId=my-service-archetype \\ -DarchetypeVersion=1.0.0 \\ -DgroupId=com.example \\ -DartifactId=order-service \\ -Dversion=0.0.1-SNAPSHOT 一条命令生出一个带完整脚手架的 …","date":"2026-04-21","keywords":["maven archetype","项目脚手架","spring boot 模板","create-from-project","velocity"],"permalink":"/posts/maven-archetype-from-existing-project/","tags":["maven","archetype","spring-boot","scaffolding"],"title":"从现有项目抽象出 Maven Archetype"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n背景 在 Shop Platform 里，每新增一个服务，开发者要做的第一件事不是写业务代码，而是把平台基线搬进去：父 POM 继承、内部 starter 依赖、application.yml 里的 OpenTelemetry / Spring Boot Actuator / 日志格式、Testcontainers 测试基座、K8s deployment / service / HPA 模板……这些内容对每个服务都是一样的。\n如果这些样板靠手抄，结果通常有两种：要么漏掉某一项，要么各个服务的写法开始出现细微差异，最终在 code review 和 ArchUnit 检查时才发现，修复成本往往比一开始就收敛要高。\n我们需要一种机制：把平台标准前置到生成阶段，而不是等代码写完再回头治理。 Maven Archetype 是解决这个问题的工具。\n为什么不用 Spring Initializr Spring Initializr 的定位是\u0026amp;quot;从零开始创建 Spring Boot 项 …","date":"2026-04-21","keywords":["Maven Archetype","微服务脚手架","Spring Initializr","scaffold","代码生成","项目模板","archetype-metadata","archetype 测试"],"permalink":"/posts/maven-archetype-microservices-scaffold/","tags":["java","maven","archetype","microservice","scaffold","spring-boot","cloud-native"],"title":"用 Maven Archetype 管理微服务 Scaffold：为什么我这里暂时没用 Spring Initializr"},{"content":"TL;DR Qwen3.6 的 preserve_thinking 是写在模型 chat template 里的 Jinja 变量，不是 vLLM CLI 标志。 正确启用方式：--default-chat-template-kwargs \u0026#39;{\u0026amp;quot;preserve_thinking\u0026amp;quot;: true}\u0026#39;。 在双机 DGX Spark 集群上用一台开、一台关做 A/B 对照：同样输入下 prompt tokens 55 vs 51（差 4 tokens 就是保留下来的历史 \u0026amp;lt;think\u0026amp;gt; 块），completion tokens 355 vs 382（关掉后模型重新\u0026amp;quot;想\u0026amp;quot;一遍）。 起因 Reddit 的一篇文章提到 Qwen3.6 随 KV cache 修复一起 \u0026amp;ldquo;ships preserve_thinking flag\u0026amp;rdquo;（原文链接）。集群里跑的正好是 Qwen3.6-35B-A3B-FP8（reasoning parser qwen3），想在两台 DGX Spark 上把这个开关打开。\n参数归属：chat …","date":"2026-04-20","keywords":["preserve_thinking","vLLM","Qwen3.6","chat template kwargs","reasoning_content","DGX Spark"],"permalink":"/posts/vllm-qwen36-preserve-thinking/","tags":["AI","LLM","vLLM","Qwen","DGX Spark","Reasoning","Chat Template"],"title":"vLLM 启用 Qwen3.6 的 preserve_thinking：双机 A/B 验证"},{"content":"实验结果 (M5 / 32GB RAM) 测试模型：Gemma4-26B-A4B-it-nvfp4（4-bit 量化，激活权重 ~3.85GB）。\n场景 基线 调优后 提升 瓶颈分析 短文本生成 (256 tok) 38.18 tok/s 39.75 tok/s +4.1% 带宽瓶颈：已达硬件理论极限 长文本重复查询 (11k tok) 14.59s 2.27s +540% IO 瓶颈：Hot Cache 消除磁盘加载 瓶颈逻辑：为什么我把 39.7 tok/s 当作接近上限的参考值？ Decode 阶段性能由内存带宽决定：每生成一个 token，都要把激活权重从内存完整读一遍。\nMoE 激活：Gemma4-26B-A4B 单步仅激活 4B 专家参数，约 3.85 GB（nvfp4 量化后）。 硬件带宽：M5 标准版 153 GB/s。 理论上限：153 GB/s ÷ 3.85 GB ≈ 39.7 tok/s 这次实测到 39.75 tok/s，已经很接近按带宽估出来的上限。至少在这组测试条件下，软件层面的调参看起来不太容易明显越过这条线。\n优化策略 (针对 32GB 机型) 1. 内 …","date":"2026-04-19","keywords":["omlx","MLX","Gemma4","Apple Silicon","M5","iogpu.wired_limit_mb","KV cache","Prefix Cache"],"permalink":"/posts/omlx-gemma4-m5-optimization/","tags":["AI","LLM","MLX","omlx","Apple Silicon","M5","Gemma","Benchmark"],"title":"Apple M5 上 omlx + Gemma4-26B 性能调优实录"},{"content":"TL;DR markitdown 是微软开源的本地文档转换工具，支持 Word (.docx/.doc)、PowerPoint (.pptx)、Excel (.xlsx/.xls)、PDF、Outlook (.msg)、音频、YouTube 视频 等多种格式转为 Markdown。\n全部计算在本地完成，不依赖任何外部 API 或 OpenAPI 服务。\n为什么需要这个工具 在日常工作中，我们经常遇到需要将各种文档格式转为 Markdown 的场景：\n知识库文档从 Word 迁移到 Markdown 格式 PPT 演示文稿内容提取 PDF 报告转为可搜索的文本 Excel 数据表格转为 Markdown 表格 在我的使用里，markitdown 目前比较好地覆盖了“格式转换”和“离线可用”这两个需求。\n前置条件 markitdown 要求 Python 3.10+。macOS 自带的 Python 通常版本较低，需要通过 Homebrew 安装新版本。\n1. 确认 Homebrew 可用 brew --version # Homebrew 5.x 2. 安装 Python 3.12 …","date":"2026-04-18","keywords":["markitdown","marker-pdf","microsoft markitdown","doc to markdown","pdf to markdown","local document conversion","table extraction","macOS","Python venv"],"permalink":"/posts/markitdown-mac-install/","tags":["markitdown","marker-pdf","macOS","Python","Document Conversion","Markdown","Local AI Tool","Table Extraction"],"title":"在 macOS 上本地部署 markitdown：将任意文档转为 Markdown"},{"content":"TL;DR 两台 DGX Spark 各自独立运行 vLLM 推理 Qwen3.6-35B-A3B-FP8，前置一层 FastAPI 写的 LLM Gateway 做路由/负载均衡，实测结果：\n单请求解码吞吐：机器本地 curl 稳定在 ~50 tok/s，客户端经 Tailscale 访问约 ~45 tok/s（差的是网络往返时间） Gateway 代理开销：serial 场景下相对直连差异 \u0026amp;lt;5%，几乎无额外开销 单机并发饱和点：N≈8 时单机聚合吞吐 ~300 tok/s（6× 单流） 双机聚合上限：经 Gateway round-robin 在 N=16 下 ~485 tok/s，接近这组测试里两台机器各自饱和时的合计 结论：在这组测试里，如果想更充分地利用两台机器，还是要走 Gateway，且客户端并发度也要跟上；低并发时 Gateway 与直连差异不大。\n环境背景 两台机器都是 NVIDIA DGX Spark，GB10 Grace Blackwell Superchip + 128GB LPDDR5X 统一内存。通过 Tailscale 组网， …","date":"2026-04-17","keywords":["vLLM","Qwen3.6-35B-A3B","DGX Spark","LLM Gateway","吞吐 benchmark","token throughput"],"permalink":"/posts/qwen36-vllm-gateway-benchmark/","tags":["AI","LLM","NVIDIA","DGX Spark","vLLM","Benchmark","Gateway","Qwen"],"title":"两台 DGX Spark 跑 Qwen3.6-35B-A3B：直连 vLLM vs 经过 Gateway 的吞吐对比"},{"content":"这是一篇本地开发环境的整理笔记。\n我在本地维护一个 Maven 多模块的 Spring Boot 微服务项目 meirongdev/shop，服务数量十几个，日常都跑在 Kind 起的本地 Kubernetes 上。起初只要 kind create cluster + docker build + kind load docker-image + kubectl apply 就够用了，但随着模块变多，这套最朴素的链路开始变慢：每次全量 build、全量 load、只改一个服务也要 redeploy 才能验证。\n这篇文章不谈企业级流水线，只记录一下在一台开发机上，我为了解决本地开发中的各种“慢”和“烦”，都选了哪些工具，以及每个工具带来的 tradeoff。\n1. 基础底座：如何在本地低成本跑 K8s？ 问题： 本地需要一个 K8s 环境，但资源占用不能太大，且要能方便地模拟多节点和不同的 K8s 版本。\n我的做法：Kind (Kubernetes in Docker)\n几个本地 Kubernetes 可选项中（Kind、Minikube、k3d、Docker Desktop）， …","date":"2026-04-15","keywords":["kind","local kubernetes","tilt","argocd","mirrord","kustomize","本地开发环境"],"permalink":"/posts/kind-k8s-lab-local-cicd-mirrord/","tags":["kubernetes","kind","tilt","argocd","mirrord","spring-boot"],"title":"本地 Kind K8s 开发环境：问题驱动的工具选择与 Tradeoff"},{"content":"TL;DR 将两台 DGX Spark 从 vLLM TP=2 跨节点部署（RPC 超时、频繁崩溃）迁移到 每台独立运行 vLLM + Bifrost 负载均衡网关后：\n✅ 稳定性：至少在这轮测试里没有再复现崩溃，P95 延迟大约在 1,400ms（20 tokens） ✅ 吞吐：单机 ~15 tok/s，双机并发 ~56 tok/s（Bifrost 开销 \u0026amp;lt;1ms） ✅ 可用性：双机独立 + 自动故障转移，不再有单点故障 ✅ 可扩展：水平加节点即可，不受 TP 上限约束 背景：为什么放弃 TP=2 跨节点 之前的实践中，我在两台 DGX Spark 上尝试了 vLLM 的 TP=2（跨节点张量并行）部署：\n《vLLM TP=2 跨节点部署实践》 《Nemotron EP2 vs TP2：两台 DGX Spark 的实际对比》 这次测试里最直接的观察是：TP=2 跨节点能跑通但不稳定，日志里反复出现：\nTimeoutError: RPC call to sample_tokens timed out. No available shared memory broadcast …","date":"2026-04-14","keywords":["Bifrost","vLLM","DGX Spark","负载均衡","Nemotron","NVFP4","LLM Gateway"],"permalink":"/posts/bifrost-load-balancing-dgx-spark/","tags":["AI","LLM","NVIDIA","DGX Spark","vLLM","Bifrost","负载均衡","Benchmark"],"title":"Bifrost 负载均衡 DGX Spark：从 TP=2 跨节点到双机独立部署"},{"content":" 这篇文章可以看作 《Contract First 工作流：让 AI 帮你写 OpenAPI YAML》 的实现篇。上一篇讨论的是“spec 如何成为协作中心”；本文只讨论当接口边界已经确定之后，JVM 团队如何把contract变成验证、stubs 和 CI Quality Gates。\n微服务接口兼容性的问题，通常不是在设计阶段暴露，而是在某个 PR 合并之后才通过联调、回归测试，甚至线上流量被发现。这个 java-contract 仓库的目的，就是把这类问题尽量前移到提交阶段，用 contract testing 把 producer 和 consumer 之间的接口约束固定下来。\n这篇文章不打算再展开 AI 生成 OpenAPI、style guide 或 breaking change 治理这些协作层话题。它更像是一次基于当前仓库完成态的工程复盘：为什么这个 Java 25 + Spring Boot 3.5 + Maven 多模块项目最终选择了 Spring Cloud Contract，contract如何在仓库里流动，以及这套做法如何进入 CI， …","date":"2026-04-14","keywords":["spring-cloud-contract","contract-testing","spring-boot-3.5","java25","wiremock-spring-boot","kafka-messaging-contract","microservices","api-compatibility","provider-driven"],"permalink":"/posts/java-contract-scc-poc-recap/","tags":["spring-cloud-contract","contract-testing","microservices","spring-boot","java25","wiremock","kafka","cdct"],"title":"Java 项目怎么做 contract testing：一次 Spring Cloud Contract 实践"},{"content":" 本文基于的项目：https://github.com/meirongdev/shop（私有）。相关实现主要在 services/api-gateway。\n从一个具体问题开始 buyer-bff 要升级一版，改动涉及订单接口的返回结构。上线前我们想让两三个内部测试账号先跑一段时间，其他人继续走旧版。出问题时能立刻回切，整个过程不要重启任何服务。\n前提：环境里没有 Istio、没有 Linkerd，也没有 Argo Rollouts。Kubernetes 只有原生的 Service 和 Ingress。\n在这种约束下，灰度发布能放在哪一层？\nKubernetes Service 权重：原生 Service 不支持按权重或按用户分流，它只是 round-robin。 应用内开关：让每个服务各自根据用户属性判断走新旧逻辑。问题是\u0026amp;quot;谁是灰度用户\u0026amp;quot;的判断会散落到每个服务里重复实现，也没法在入口做统一观测。代码层面的 feature flag 不是不能用，它和 Gateway 路由解的是不同层次的问题——本文末尾会讨论两者如何互补。 Service Mesh：能干净地做这件事， …","date":"2026-04-14","keywords":["灰度发布","Spring Cloud Gateway","PredicateSupplier","Redis 白名单","Caffeine 缓存","OpenFeature","Feature Toggle","api-gateway"],"permalink":"/posts/spring-cloud-gateway-canary-release/","tags":["spring-cloud-gateway","canary-release","redis","redisson","caffeine","openfeature"],"title":"没有 Service Mesh，用 API Gateway 做用户级灰度"},{"content":"结论先行 在同一份 Nemotron 3 Super 120B A12B NVFP4、同一镜像、同一保守 profile 下，我这轮测试里 TP2 明显好于 EP2。\n我这里做的是 clean-room 对比：清掉两台机器上所有其他 vLLM 实例后，分别跑 EP2 和 TP2，再额外用 max_tokens=32 做一次长输出探针。\n测试条件 镜像：vllm/vllm-openai:gemma4-cu130 模型目录：/home/admin/models/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4-aria2 公共参数：gpu_memory_utilization=0.75、max_model_len=32768、kv_cache_dtype=fp8、quantization=fp4、mamba-ssm-cache-dtype=float32 EP2：TP=1 + DP=2 + --enable-expert-parallel TP2：tensor-parallel-size=2 结果对比 拓扑 短输出 benchmark 成功数 成功 case  …","date":"2026-04-14","keywords":["vLLM","EP2","TP2","DGX Spark","Nemotron","NVFP4","Benchmark"],"permalink":"/posts/nemotron-ep2-dgx-spark/","tags":["AI","LLM","NVIDIA","DGX Spark","vLLM","Expert Parallel","Benchmark"],"title":"Nemotron EP2 vs TP2：两台 DGX Spark 的实际对比"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n2026-04 实践更新 当前主线后端已经完成 RS256 + JWKS、BFF @HttpExchange typed client、以及 Redis/Redisson 统一接入，因此本文里 KMP 前端与后端协作的认证 / API 访问背景，比文章初稿时更加接近真实可运行平台。\nShop Platform 的前端策略是渐进式的：买家门户用 Kotlin + Thymeleaf SSR 承担 SEO 和免登交易路径，交互式购物体验和卖家工作台则采用 Compose Multiplatform 的共享模块实现。当前仓库已经提交了 WASM 目标、Android 包装 App，以及 iOS framework target；但没有把完整 iOS App shell 一起提交。这不是\u0026amp;quot;所有端都用一套代码\u0026amp;quot;的理想化方案，而是当前阶段的折中——SSR 管 SEO，KMP 管交互，各取所需。\n整体架构 flowchart TD subgraph KMP[\u0026#34;Compose …","date":"2026-04-13","keywords":["Kotlin Multiplatform","Compose Multiplatform","WASM","跨端开发","电商前端","feature 模块化","Kotlin/Wasm","Jetpack Compose"],"permalink":"/posts/kmp-compose-multiplatform-cross-platform/","tags":["kotlin","kmp","compose-multiplatform","wasm","cross-platform","mobile","frontend"],"title":"Compose Multiplatform 跨端实战：一套代码跑 WASM / Android / iOS 的电商应用"},{"content":" 本文代码背景来自开源项目 meirongdev/shop。\n如果你看过上一篇 Java 项目怎么做 contract testing：一次 Spring Cloud Contract 实践，那篇文章讨论的是\u0026amp;quot;接口兼容性边界\u0026amp;quot;；这一篇讨论的是\u0026amp;quot;调用链可观测性边界\u0026amp;quot;。\nTracing 断链时，该换客户端还是换设计？ 在一个典型的 Spring Boot 3.5 微服务架构里，当 tracing 断链时，很多团队的第一反应是换一个更优雅的 HTTP 客户端抽象方式，比如用 @HttpExchange 替代手写 RestClient，或者抽一个公共客户端。但这往往不是问题的核心。\n实际请求通常会经过：\napi-gateway -\u0026amp;gt; buyer-bff / seller-bff -\u0026amp;gt; order-service / profile-service / wallet-service ... 如果链路里某一跳自己 new 了一个 HTTP client，或者只会复制业务 header、不会传播 trace context，结果通常就是： …","date":"2026-04-13","keywords":["spring-boot-3.5","distributed-tracing","trace-context","baggage","micrometer-tracing","opentelemetry","restclient","httpexchange","microservice"],"permalink":"/posts/shared-http-client-tracing-2026/","tags":["spring-boot","tracing","micrometer","opentelemetry","baggage","microservice","restclient","http-exchange"],"title":"Spring Boot 3.5 微服务 tracing 为什么会断链：一次 HTTP 客户端治理复盘"},{"content":"问题背景 在远程服务器上部署模型或运行长任务时，最让人焦虑的就是 SSH 断连导致命令被杀：\n部署 vLLM 服务器，模型加载需要几分钟，突然网络抖动，进程直接中断 跑 benchmark 测试，笔记本合上盖子，测试直接失败 在咖啡厅或高铁上运维，网络不稳定，随时可能中断关键操作 这个问题的根源是：SSH 断连会发送 SIGHUP 信号，终端下的所有子进程都会被终止。\n一个常见做法：用 tmux 在服务器上创建持久化会话。 即使你断开了 SSH，tmux 会话里的命令仍然在后台继续运行。\n什么是 tmux？ tmux（Terminal Multiplexer）是一个终端复用器，允许你在单个终端窗口中创建多个会话、窗口和窗格。它的核心价值在于：\n会话持久化：会话运行在服务器端，不依赖你的 SSH 连接 网络韧性：WiFi 断线、VPN 断开、笔记本睡眠，都不会影响服务器上的进程 多窗口/多窗格：可以在一个终端里同时运行命令、查看日志、监控资源 这与 Screen 类似，但 tmux 在今天的资料和使用案例里更常见一些。\n一些外部实践参考 tmux 在 ML/DevOps 场景里已经很常 …","date":"2026-04-12","keywords":["tmux","SSH","远程服务器","DGX Spark","vLLM","部署","DevOps","ML Infrastructure"],"permalink":"/posts/tmux-remote-server-best-practices/","tags":["tmux","SSH","DevOps","ML Infrastructure","Remote Server"],"title":"tmux 远程服务器实践：尽量降低 SSH 中断对命令的影响"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n2026-04 实践更新 当前主线仓库里，MySQL 领域服务的集成测试基线已经基本统一为 @ServiceConnection；少数仍保留 @DynamicPropertySource 的测试，主要是因为除了数据库容器之外还需要额外挂接自定义属性，而不是数据库接线本身没有迁移完成。\n在 Shop Platform 的 15 个服务中，11 个领域服务各自拥有独立的 MySQL 8.4 数据库。Spring Boot 3.5 结合 Spring Data JPA 3.5 提供了比较完善的数据库集成能力，但默认配置未必直接适合当前生产环境。需要额外说明的是：当前仓库已经稳定落地的是 ddl-auto: validate、Flyway 迁移、@ServiceConnection 集成测试，以及部分服务显式关闭 OSIV；像 Hikari 数值和 @EntityGraph 更适合作为“可参考实践”，不应直接写成“仓库现状”。\n下面从六个维度整理一下 Spring Boot 3.5 下数据库实践 …","date":"2026-04-12","keywords":["HikariCP 调优","open-in-view","EntityGraph","N+1 查询","Transactional 传播","Testcontainers","Flyway 进阶","Spring Boot 3.5"],"permalink":"/posts/spring-boot-database-best-practices/","tags":["spring-boot","jpa","hikaricp","mysql","testcontainers","flyway","cloud-native","java25"],"title":"Spring Boot 3.5 下数据库实践记录：HikariCP 调优、N+1 防护、事务管理与 Testcontainers 集成"},{"content":"概述 本文记录在两台 DGX Spark 上首次以 vLLM TP=2（跨节点张量并行）方式部署 Qwen3.5-35B-A3B 的完整过程。这次实验先得到几条比较初步的观察：\nTP=2 跨节点推理可以跑通，短请求成功完成 成功样本吞吐约 21–31 tok/s，与单机 TP=1（~29 tok/s）基本持平 稳定性不足：代码生成 case 在约 300 秒后返回 HTTP 500 按这轮实验结果看，如果是追求更稳妥的生产方案，我目前更倾向于两台节点各自独立运行 TP=1，再通过 LiteLLM / Nginx 做负载均衡 技术背景 DGX Spark 与统一内存 DGX Spark 搭载 NVIDIA GB10 Grace Blackwell Superchip，其关键特征是 128GB LPDDR5X 统一内存（CPU+GPU 共享），带宽 273 GB/s。\n与独立显存的传统 GPU 不同，统一内存没有独立的 VRAM 分区。这意味着：\n--gpu-memory-utilization 不能像常规设置那样给到 0.9，需要保守设为 0.7，为内存碎片留出余量 按我这轮实验里的环境 …","date":"2026-04-12","keywords":["vLLM","Tensor Parallel","DGX Spark","Qwen3.5-35B-A3B","分布式推理","统一内存"],"permalink":"/posts/dgx-spark-benchmark-2026/","tags":["AI","LLM","NVIDIA","DGX Spark","vLLM","Tensor Parallel"],"title":"vLLM TP=2 跨节点部署实践：两台 DGX Spark 跑 Qwen3.5-35B-A3B"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n2026-04 实践更新 当前主线代码已经把 buyer-bff / auth-server / activity-service / api-gateway 的 Redis 访问统一切到 RedissonClient；其中网关限流仍然保留 Lua 原子脚本，但执行器也已经从 StringRedisTemplate 切到 RScript。本文下面的示例已按仓库当前实现同步。\nRedis 在 Shop Platform 中承担的角色远不止\u0026amp;quot;缓存\u0026amp;quot;——它是限流的令牌桶状态（Gateway Lua + RScript）、抢红包的原子存储（activity-service Redis List）、反作弊的状态机（Anti-CheatGuard）、幂等检查的 Bloom Filter（shop-starter-idempotency），以及游客购物车的存储（buyer-bff）。\n本文从五个维度整理 Spring Boot 3.5 下 Redis 的一些实践记录。 …","date":"2026-04-11","keywords":["Spring Data Redis","Lettuce 连接池","Redisson","Bloom Filter","Lua 脚本","序列化策略","Redis 实战","Spring Boot 3.5"],"permalink":"/posts/spring-boot-redis-best-practices/","tags":["spring-boot","redis","lettuce","redisson","lua-script","bloom-filter","cloud-native","java25"],"title":"Spring Boot 3.5 下 Redis 实战记录：从连接池调优到 Bloom Filter 自动配置"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n🏷️ 当前文章对应的代码版本：openapi_3_1_upgrade\n背景 我们的电商平台自启动以来，一直使用 OpenAPI 3.0.x 作为 API 文档规范。近期在整理 SpringDoc 2.8.9 和 JSON Schema 2020-12 工具链时，我顺手验证了一次 OpenAPI 3.1 升级的可行性，最后决定先在项目里试着切过去。\n就这次实践看，升级过程主要是一行配置，没有涉及 Java 代码改动。下面更像是一份升级笔记：记录我为什么做这件事、哪些收益比较直接，以及哪些兼容性边界要先确认。\nOpenAPI 3.1 的核心变化 OpenAPI 3.1 于 2021 年底发布（官方发布公告），虽然看起来只是小版本号 +1，但它底层做了一个根本性的改变：全面对齐 JSON Schema Draft 2020-12（JSON Schema 2020-12 规范）。\n这不只是\u0026amp;quot;换了一个标准\u0026amp;quot;，更像是把 OpenAPI Schema 往 JSON Schema 工 …","date":"2026-04-10","keywords":["openapi 3.1","json schema 2020-12","nullable","webhooks","springdoc upgrade","api documentation","schema composition"],"permalink":"/posts/openapi-31-upgrade/","tags":["openapi","springdoc","api-design","json-schema","微服务","架构演进"],"title":"从 OpenAPI 3.0 到 3.1：一次升级验证记录"},{"content":"在大规模微服务平台中，文档治理往往比架构设计本身更容易失控。Confluence、Notion 等中心化 wiki 工具在团队规模扩大后，很容易出现\u0026amp;quot;文档漂移\u0026amp;quot;（documentation drift）——文档与实际代码脱节，没人确定哪个版本更接近现状。\n最近给自己的微服务项目搭文档站，选了 Docs-as-Code 这条路——文档和代码放同一个仓库，每个服务维护自己的 /docs/，最后用 Docusaurus 聚合成一个统一的门户。\n这种模式和 Domain-aligned multi-repo 架构（每个 domain 有独立的 MS/BFF/consumer）比较契合：service owner 维护自己的 ADR 和设计文档，文档也更贴近代码；同时再通过 Docusaurus 提供统一入口。它和 Team Topologies 里 Stream-Aligned Team 的自治原则也比较一致。\n这篇文章整理的是一套目前可落地的方案：组织结构、各服务 /docs/ 内容规范、Docusaurus 聚合机制、ADR 落地，以及我调研时看到的几种开源方案。\n整体 …","date":"2026-04-10","keywords":["microservice-documentation","docs-as-code","docusaurus","multi-repo-aggregation","adr","mad","team-topologies","gitops"],"permalink":"/posts/microservice-documentation-strategy-2026/","tags":["microservice","documentation","docusaurus","docs-as-code","adr","architecture","cicd"],"title":"2026 年多仓库微服务文档聚合策略：Docs-as-Code + Docusaurus 统一门户"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n在 Shop Platform 的事件驱动架构中，Apache Kafka 3.9（KRaft 模式）是服务间异步通信的核心基础设施。Spring Boot 3.5 结合 Spring Kafka 3.3 提供了开箱即用的 Kafka 集成，但要把 Kafka 用得稳、用得好，还需要理解底层的并发模型、序列化策略、offset 语义以及错误处理机制。\n本文从六个维度整理 Spring Boot 3.5 下 Kafka 的一些实践记录。文中的“现状判断”都以 shop 项目的实际实现为准，额外的代码片段会明确视为建议写法，而不是仓库已经落地的事实。\n生产者调优：acks、retries 和 linger.ms acks 配置 acks 决定了生产者需要等待多少个 broker 副本确认后才认为发送成功：\nacks 值 含义 可靠性 延迟 acks=0 不等待任何确认 最低（可能丢消息） 最低 acks=1 Leader 确认即可 中（Leader 宕机丢消息） 低 acks=all …","date":"2026-04-10","keywords":["Spring Kafka","Kafka 消费者","并发模型","序列化选型","DeadLetterPublishingRecoverer","offset 提交","acks","linger.ms","Spring Boot 3.5"],"permalink":"/posts/spring-boot-kafka-best-practices/","tags":["spring-boot","kafka","spring-kafka","microservice","cloud-native","java25"],"title":"Spring Boot 3.5 下 Kafka 实战记录：从消费者并发模型到序列化选型"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n🏷️ 当前文章对应的代码版本：main\n背景 在一个典型的微服务架构中，每个服务都独立维护自己的 API 文档。随着服务数量增长，前端开发者和第三方集成方常常面临一个痛点：\n我需要去哪里找某个接口的文档？\n传统的解决方案是让开发者记住每个服务的地址，或者维护一个手动的聚合页面。但这些方案都存在明显的问题：容易过时、维护成本高、无法与 CI/CD 集成。\n本文以一个云原生电商平台的实际案例，记录我们当前如何通过 Spring Cloud Gateway MVC + SpringDoc 维护一套相对省事的 OpenAPI 聚合方案。\n架构概览 我们的平台采用 Gateway + Thin BFF + Domain Service 三层架构：\nClient └→ api-gateway:8080 (Spring Cloud Gateway MVC, JWT validation, rate limiting) ├→ /auth/** → auth-server ├→ /buyer/** → …","date":"2026-04-10","keywords":["openapi aggregation","springdoc","spring cloud gateway","swagger ui","microservice documentation","api documentation","BFF","backend for frontend"],"permalink":"/posts/openapi-aggregation-in-microservice-architecture/","tags":["微服务","openapi","springdoc","spring-cloud-gateway","api-gateway","swagger","架构实践"],"title":"微服务架构下的 OpenAPI 聚合文档实践"},{"content":"最近开始尝试用 AI agent 来帮我处理一些相对明确的自动化分析和整理任务。论模型能力，GitHub Copilot 免费提供的 GPT-5 mini 可能比我本地部署的模型更强，但它有速率限制，跑多了就被限。本地模型的好处在于不受限制，而且可以自己调整参数和提示词来适应具体的需求场景。\n我的 Homelab 服务器上正好跑着 Ollama，于是就想能不能让本地的 Codex CLI 直接用远程服务器的模型来干活。试了一圈后找到了可行的配置方式，这里记录一下。\n整体架构 本地 MacBook (Codex CLI) │ │ Tailscale 虚拟局域网 │ http://100.x.x.x:11434/v1 ▼ 远程 Homelab (Ollama) │ ├── qwen3.5:latest (日常编码) ├── gemma4:26b (深度推理) └── gemma4:31b (复杂架构设计) Tailscale 负责打通网络，Ollama 提供 OpenAI 兼容 API，Codex CLI 通过自定义 Provider 接入远程模型。请求流量都留在 Tailnet 内，至少 …","date":"2026-04-10","keywords":["codex cli","ollama","tailscale","remote llm","coding agent"],"permalink":"/posts/codex-cli-remote-ollama/","tags":["ai","ollama","codex-cli","tailscale","homelab"],"title":"Codex CLI 对接远程 Ollama"},{"content":"背景 如果你在 Kubernetes 里用过 Vertical Pod Autoscaler（VPA），大概率遇到过一个很现实的问题：推荐值有了，但真正生效时，Pod 先被驱逐，再由控制器拉起新实例。\n对无状态服务来说，这种做法还能接受。但对数据库、消息队列、长任务、状态服务，甚至一些对尾延迟敏感的 API 服务来说，重建 Pod 本身就是一次明显的扰动。\n如果你还没有把 requests / limits、QoS 和 throttling 这些基础概念串起来，可以先看我之前的这篇：Kubernetes CPU 资源管理与 QoS 机制。\nKubernetes 1.35（2025 年 12 月发布）把这个能力补齐了：\nIn-Place Pod Resize 已经 GA VPA 的 InPlaceOrRecreate 更新模式进入 beta 也就是说，VPA 现在可以优先尝试在原地修改运行中 Pod 的 CPU 和内存资源，而不是直接把 Pod 赶走。\n先区分三件事 很多讨论会把 HPA、VPA 和 in-place resize 混在一起，其实它们解决的是三层不同的问题。\n能力 调整对 …","date":"2026-04-09","keywords":["Vertical Pod Autoscaler","In-Place Pod Resize","InPlaceOrRecreate","metrics-server","PodResizePending","PodResizeInProgress","kind","Java 25","Spring Boot","JVM cgroup"],"permalink":"/posts/kubernetes-vpa-inplace-resize/","tags":["kubernetes","vpa","autoscaling","kind","java","cloud-native"],"title":"Kubernetes VPA InPlace Resize：原理、实战与避坑"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n电商系统接入支付时，第一步通常不是“把所有 webhook 和退款补偿一次做完”，而是先把支付入口、provider 边界和返回模型抽清楚。Shop Platform 当前已经完成的是这条基线：buyer-bff 统一调用 wallet-service，wallet-service 再按支付方式路由到 Stripe / PayPal / Klarna / 内部 Wallet。\n更准确地说，当前仓库已经具备：\n统一的 WalletApi.PAYMENT_INTENT / PAYMENT_METHODS 合约 PaymentProviderService 里的 provider routing Stripe 真/假网关切换（StripePaymentGateway / MockPaymentGateway） buyer-bff 对 clientSecret / redirectUrl 的透传 但还没有把 Stripe webhook、Stripe Refund API、事件幂等 …","date":"2026-04-09","keywords":["Stripe 支付","PaymentIntent","Webhook 验签","HMAC-SHA256","支付幂等","退款 Saga","可插拔支付","wallet-service"],"permalink":"/posts/stripe-payment-integration/","tags":["spring-boot","stripe","payment","webhook","idempotency","microservice","cloud-native"],"title":"Stripe 支付接入基线：PaymentIntent 抽象、Mock/真实网关切换与后续 Webhook 演进"},{"content":"原文链接 How AI Impacts Skill Formation — arXiv:2601.20245v2（Judy Hanwen Shen、Alex Tamkin，Anthropic） 笔记 这是一篇来自 Anthropic 的 preprint 论文。我读完后的第一感觉不是“结论已定”，而是它给了我一个值得警惕的观察角度。\n这篇文章里最让我在意的一点是：在作者设计的任务里，更依赖 AI 的那组，在新知识掌握和调试测试上的表现更弱，而完成时间优势也不明显。\n1. 实验：模拟“边干边学” 研究者设计了一个比较贴近现实的场景：让 52 位有 Python 经验、但从未接触过 Trio 的 professional / freelance programmers，去学习这个主打结构化并发的异步编程库。\n参与者被分为两组：\n手动组 (Manual Group)：不能使用 AI，只能依靠任务说明、文档和搜索。 AI 组 (AI Group)：可以使用基于 GPT-4o 的 AI 助手协助编写、调试和解释代码。 2. 核心发现：效率差异有限，能力形成可能受影响 测试成绩差异 在任务结束后的 …","date":"2026-04-08","keywords":["Skill Formation","AI coding","Anthropic","Judy Hanwen Shen","Alex Tamkin"],"permalink":"/reading/anthropic/how-ai-impacts-skill-formation/","tags":["AI","Programming","Learning","Efficiency"],"title":"AI 如何影响编程能力的形成？读 Anthropic 的 preprint 论文"},{"content":" 📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n2026-04 实践更新 当前主线代码里，游客购物车已经从 StringRedisTemplate 统一切到 RedissonClient 的 RBucket\u0026amp;lt;String\u0026amp;gt;，但仍然保留“JSON 可读 + TTL 续期”的设计。下面示例已按当前实现同步。\n在电商平台中，注册/登录常常是转化链路里的明显阻力之一。Baymard 长期跟踪 checkout 可用性时，也一直把“被迫注册账号”列为常见流失原因之一。Baymard Institute, Cart Abandonment Rate\nShop Platform 目前已经在 buyer-portal 这条链路上初步支持 Guest-First（游客优先）：用户可以不注册先创建游客订单、保存 order_token 并回查状态。需要补充说明的是，KMP buyer-app 目前仍要求登录后再触发结账，所以这里更接近“逐步 guest-first”，而不是所有前端入口都已经完全等价。\n整体流程 flowchart TD …","date":"2026-04-08","keywords":["Guest-First","游客购物","无需注册","guest JWT","Redis 购物车","order_token","购物车合并","电商架构"],"permalink":"/posts/shop-guest-first-shopping/","tags":["spring-boot","guest-checkout","redis","jwt","cloud-native","architecture","ecommerce"],"title":"电商 Guest-First 购物体验：无需注册也能完整下单"},{"content":"前言 Distributed Tracing 要解决的核心问题是：一次请求经过多个服务、多个组件，出了问题之后能在哪里、花了多少时间、失败在哪一层。Spring Boot 3.5 + Micrometer Tracing + OTel Bridge 的组合，让绝大多数 span 的生成和传播不需要任何手动代码。\n本文分两部分：前半部分把 tracing 跑起来（依赖、配置、自定义埋点、组件接入、上下文传播），后半部分覆盖生产环境的关键决策（Agent 选型、采样策略、PII 处理、规范守护）。\n一、自动配置覆盖了什么 在开始写任何代码之前，先了解 Spring Boot 3.5 开箱即得的范围，避免重复造轮子。\n场景 自动生成的 Span 触发条件 HTTP 入站请求 http.server.request spring-boot-starter-web 或 webflux RestClient / RestTemplate 出站 http.client.request micrometer-tracing 在 classpath WebClient …","date":"2026-04-07","keywords":["spring boot tracing","opentelemetry tracing","micrometer tracing","distributed tracing","spring boot 3.5 tracing","@Observed","kafka tracing","redis tracing","context propagation"],"permalink":"/posts/spring-boot-35-tracing-best-practices/","tags":["spring-boot","tracing","opentelemetry","micrometer","observability","java","kafka","redis"],"title":"Spring Boot 3.5 Tracing 实践记录：从接入到生产观察"},{"content":"为什么我开始关注结构化输出 Spring Boot 默认的 Logback 输出是人类可读的文本行，形如：\n2026-04-07 10:00:00.123 INFO 12345 --- [main] c.e.OrderService : order created orderId=ORD-001 这在本地开发时没有问题，但一旦日志进入 OpenSearch，文本行只能靠全文检索（message: *ORD-001*）来定位，无法按字段过滤、聚合统计或设置告警规则。当并发量上来，同一秒内几百条日志里找一个 orderId 靠的是运气而不是索引。\n解决这个问题的方式是让每条日志以结构化 JSON 格式输出，每个上下文值都成为独立字段，可以被 OpenSearch 精确索引和查询。Spring Boot 3.4 正式将这个能力内建进框架，开发者不再需要引入 logstash-logback-encoder 或手写复杂的 XML encoder 配置。\n5 行配置启用 ECS 结构化输出 ECS（Elastic Common Schema）是 Elastic 定义的日志字段规 …","date":"2026-04-07","keywords":["spring boot structured logging","ecs logging","logback-spring.xml","fluent api logging","opensearch logging","spring boot 3.4 logging","logging.structured"],"permalink":"/posts/spring-boot-structured-logging-ecs/","tags":["spring-boot","logging","ecs","observability","opensearch","structured-logging","java"],"title":"Spring Boot 3.4+ 结构化日志实践记录：用 ECS + Fluent API"},{"content":"在生产环境中推过新功能的人，几乎都遇到过同一个问题：\u0026amp;ldquo;这个功能怎么在不开完整服务的前提下，先让一小部分人用起来？\u0026amp;rdquo;\nFeature Toggle（特性开关）是回答这个问题的一种常见思路。但在 Spring Boot 微服务体系里，怎么实现一套不绑定具体供应商、能随代码一起版本管理、还能在生产环境动态切换的特性开关，仍然是很多团队需要摸索的事情。\n本文结合 shop 项目中的 shop-starter-feature-toggle 模块，完整讲一遍从实现到部署的 Feature Toggle 落地路径。\n为什么不用 LaunchDarkly / Unleash 直接上手 在开始之前，先明确一个关键问题：为什么不用现成的 SaaS Feature Flag 平台？\nLaunchDarkly、Unleash、Split 等确实是成熟的产品，但在实际落地时往往面临几个门槛：\n引入外部依赖：需要在生产环境部署或购买 SaaS，增加运维和成本复杂度 供应商锁定：一旦业务代码深度耦合某个平台的 SDK，切换供应商的成本很高 对于 POC 和中小项目来说过度工程化：很多团队只需 …","date":"2026-04-07","keywords":["Feature Toggle","OpenFeature","Spring Boot 3.5","Kubernetes ConfigMap","热更新","暗开关","灰度发布","FeatureProvider"],"permalink":"/posts/spring-boot-feature-toggle-openfeature/","tags":["spring-boot","feature-toggle","openfeature","kubernetes","configmap","cloud-native","java25"],"title":"Spring Boot 微服务中的 Feature Toggle 实战：OpenFeature Property Provider + K8s ConfigMap 热更新"},{"content":"为什么团队最好先约定一套日志写法 日志不是“顺手打印一行字符串”，而是故障排查、审计追踪和可观测性的一部分。没有相对一致的写法时，最常见的问题不是“没有日志”，而是日志很多却无法检索、无法关联、无法长期维护。\n典型表现包括：\nmessage 风格混乱，同类事件写法完全不同 关键上下文字段缺失，只能靠全文检索猜测问题链路 敏感信息进入日志，带来合规和安全风险 生产问题无法按 traceId、requestId、订单号或用户 ID 快速定位 本文不讨论“如何随手打一行日志”，而是尝试把日志整理成一套团队更容易落地的开发约定：哪些值得优先做，哪些应尽量避免，哪些是我比较推荐的实践；同时给出 Spring Boot 项目的落地示例和支撑链接。\n日志开发的基本原则 日志约定先解决的是可用性，而不是“看起来有日志”。\n我更推荐的原则是：message 只描述发生了什么，不承担完整上下文；关键上下文最好通过结构化字段输出；默认假设日志会进入集中平台并长期保留，因此内容最好可检索、可审计、可脱敏。\n不建议把日志当成临时调试输出。不要依赖随手拼接字符串来补充上下文，也不要把敏感信息、不可检索的大段文本或 …","date":"2026-04-06","keywords":["logging guide","logging practices","spring boot logging","structured logging","mdc","traceid","logback"],"permalink":"/posts/logging-development-guidelines/","tags":["logging","java","spring-boot","observability","logback","structured-logging"],"title":"Java 日志开发实践整理"},{"content":"在这个系列的前几篇文章中，我们看了 Shop Platform 的各个技术层面：API Gateway、BFF 聚合、领域服务、事件驱动、活动引擎。但有一个问题始终没有回答——如何保证 15+ 个服务、多个开发者在长时间演进中不把这些设计搞乱？\n靠 Code Review 口头约定往往不够。人总是会犯错，尤其是当 deadline 临近的时候。按我目前的理解，一个更稳妥的办法是：把架构约束尽量变成可自动执行的测试用例。\n📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n上一篇：（六）插件化活动引擎\n2026-04 实践更新 当前主线仓库里，ArchUnit、WireMock consumer contract tests、@HttpExchange 客户端基线、以及 JWKS 认证链都已经进入可验证状态；本文里的质量Quality Gates思路仍然适用，但哪些Quality Gates“只是方向”、哪些已经落地，请以 main 分支和 docs/ROADMAP-2026.md 为准。\n质量Quality Gates全景 Shop …","date":"2026-04-06","keywords":["ArchUnit 架构测试","Maven Archetype 脚手架","WireMock contract testing","API 版本化路径","编码规范自动化","工程质量"],"permalink":"/posts/shop-platform-architecture-quality-gates/","tags":["spring-boot","java25","cloud-native","archunit","maven-archetype","wiremock","contract-testing","architecture","quality-gates"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（七）：架构质量Quality Gates"},{"content":"为什么 Metrics 是可观测性的基础信号 在日志、链路追踪（Traces）和 Metrics 三大可观测性信号中，Metrics 往往是最先被告警系统用到的那个。\nMetrics 是聚合数据：它告诉你\u0026amp;quot;过去 1 分钟，/api/hello 接口平均延迟 320ms，错误率 0.3%\u0026amp;quot;。日志是个体事件：它记录每一次请求的具体参数。两者不是替代关系，而是分工：Metrics 负责趋势感知和告警触发，日志负责单点定位和细节还原，链路追踪负责调用链拼接。\nRED 方法论是 Metrics 最重要的一套设计框架，适合所有面向用户的服务：\n指标 英文 含义 请求速率 Rate 每秒处理多少请求 错误率 Error 失败请求占比 请求耗时 Duration P50 / P95 / P99 延迟分布 本文以 springboot3.5-otel 项目（三服务微服务架构）为参考，整理一次 Spring Boot 应用层 Metrics 埋点实践。虽然该项目通过 OTel Collector 导出 Metrics，但本文大部分应用层代码对直接使用 Prometheus 的团队同样适 …","date":"2026-04-06","keywords":["spring boot metrics","micrometer observation","prometheus","RED metrics","@Observed","ObservationRegistry","metrics practice","spring boot actuator"],"permalink":"/posts/spring-boot-35-metrics-best-practices/","tags":["spring-boot","metrics","micrometer","prometheus","observability","opentelemetry","java"],"title":"Spring Boot 应用 Metrics 埋点实践记录（2026）"},{"content":"在之前的文章中，我们看了 API Gateway、BFF 聚合、领域服务和事件驱动架构。本文继续整理 Shop Platform 里我觉得比较有意思的一块——activity-service 的插件化游戏引擎。\n📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n上一篇：（五）事件驱动架构\n2026-04 实践更新 当前主线代码里，RedEnvelopePlugin 和 AntiCheatGuard 都已经从 StringRedisTemplate 统一迁到 RedissonClient；本文讨论的 Lua 原子性和反作弊思路不变，但 Redis 客户端层已经与早期版本不同。\n为什么需要活动引擎 电商平台经常需要举办营销活动来吸引用户：\n砸金蛋/抽奖：用户消耗次数参与，随机获得奖品 抢红包：限时限量，用户拼手速抢金额 集卡：集齐一套卡片兑换大奖 虚拟养成：每天浇水/喂养，成熟后收获奖品 如果每个活动都独立开发一个新服务：\n代码大量重复（参与记录、奖品发放、反作弊逻辑） 每次上线新活动都要走完整的发版流程 活动下线后代码变成死代码 这次项目里 …","date":"2026-04-05","keywords":["插件化架构","SPI 模式","GamePlugin 接口","抢红包 Lua 脚本","二均值算法","反作弊","集卡","虚拟养成","活动引擎"],"permalink":"/posts/shop-platform-activity-engine/","tags":["spring-boot","java25","cloud-native","plugin-architecture","spi","redis","lua-script","game-engine"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（六）：插件化活动引擎"},{"content":"在之前的文章中，我们看了同步请求链路上的 Gateway → BFF → 领域服务。但微服务架构中并不是所有交互都适合同步发生。事件驱动架构让服务之间通过异步消息协作，在不少场景下用最终一致性换取更低耦合和更好的链路弹性。\n📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n上一篇：（四）领域服务设计\n2026-04 实践更新 当前主线仓库已经把 IdempotencyGuard + Redis Bloom Filter、补偿任务重试、以及关键 Kafka consumer 的幂等保护补齐到 Phase 1 基线；本文整体设计仍然准确，但“待补齐”的幂等链路请以当前 main 分支实现为准。\n为什么用事件驱动 在 Shop Platform 中，异步事件主要用来承接那些不必阻塞用户请求、但又需要可靠传播的业务事实。例如：\nflowchart TD Order[\u0026#34;用户下单order-service\u0026#34;] Stock[\u0026#34;扣库存marketplace-service\u0026#34;] Points[\u0026#34;发积分loyalty-service\u0026#34;] Email[\u0026#34;发邮件 …","date":"2026-04-04","keywords":["Kafka 话题设计","EventEnvelope 规范","Outbox Pattern","IdempotencyGuard","Bloom Filter","重试与 DLQ","Saga 补偿","Transactional Outbox"],"permalink":"/posts/shop-platform-event-driven/","tags":["spring-boot","java25","cloud-native","kafka","event-driven","outbox-pattern","idempotency","bloom-filter","saga"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（五）：事件驱动架构"},{"content":"在前两篇文章中，我们看了 API Gateway 的路由分发和 BFF 的聚合编排。这一篇继续往里走，看看业务承载最集中的一层——领域服务层。\n📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n上一篇：（三）BFF 聚合层\n2026-04 实践更新 领域服务侧当前已经和早期文章发布时相比前进了一步：Problem Details、@ServiceConnection、补偿任务持久化、Kafka 幂等守卫都已落地；本文中的领域边界划分仍然成立，但涉及“后续会做”的部分请以主线仓库现状为准。\n领域服务清单 Shop Platform 核心业务按边界拆成 11 个领域服务，每个服务独立开发、独立部署、拥有自己的数据库 schema；此外 auth-server 自己维护 shop_auth，但它更偏认证边界，本文重点放在业务域服务。\n服务 数据库 核心业务 profile-service shop_profile 用户档案、地址簿、卖家档案 marketplace-service shop_marketplace 商品目录、SKU、库存、 …","date":"2026-04-03","keywords":["每服务独立数据库","JPA 实践记录","Flyway 迁移","Outbox Pattern","补偿任务","ApiResponse 统一响应","Trusted Headers"],"permalink":"/posts/shop-platform-domain-services/","tags":["spring-boot","java25","cloud-native","domain-driven-design","jpa","flyway","outbox-pattern","microservice"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（四）：领域服务设计"},{"content":"在上一篇中，我们看了 API Gateway 如何作为统一入口处理 JWT 校验和路由分发。请求经过 Gateway 后，到达的就是本文的主角——BFF（Backend for Frontend）聚合层。\n📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n上一篇：（二）API Gateway 架构\n2026-04 实践更新 当前主线代码里，buyer-bff / seller-bff 都已经完成了 typed @HttpExchange 客户端改造；BuyerAggregationService / SellerAggregationService 不再直接持有一堆 raw RestClient 调用链，而是通过 HttpServiceProxyFactory 注入声明式客户端。本文下面与客户端装配相关的表述已按当前实现校正。\nBFF 模式：为什么需要聚合层 在微服务架构中，前端页面往往需要从多个领域服务获取数据。如果没有 BFF，前端直接调用每个领域服务会面临：\n前端 ←→ profile-service (获取用户档案) ←→ …","date":"2026-04-02","keywords":["BFF 模式","Virtual Threads 并发编排","Resilience4j 熔断降级","游客购物车","RestClient","HttpExchange","Saga 补偿"],"permalink":"/posts/shop-platform-bff-aggregation/","tags":["spring-boot","java25","cloud-native","bff","backend-for-frontend","resilience4j","virtual-threads","guest-checkout"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（三）：BFF 聚合层"},{"content":"背景 在 2026 年，虽然 Spring Boot 4 已经发布，但大多数企业项目（包括我当前的工作项目）仍基于 Spring Boot 3.5，且没有立即升级到 Spring Boot 4 的计划。就我这次接入来说，Spring Boot 3.5 + Java 25 提供的 OpenTelemetry 自动配置已经覆盖了很多常见场景，剩下的主要是按项目需要做取舍。\n本文以一个三服务微服务项目（hello-service → user-service + greeting-service）为例，记录我在 Spring Boot 3.5 下接入 OTel 的过程。项目代码完全开源，更适合作为一个可对照的 demo，而不是放之四海而皆准的模板。\n实践思路：这个示例先不使用 Javaagent。 主要考虑是想把接入路径尽量放在 Spring Boot 自身的自动配置和 Micrometer Observation 上，这样更容易观察 AOT / Native Image 场景里的行为，也少一层运行时黑盒。对这个 demo 来说，这样的取舍已经够用。\n先从 Agentless 方案试起， …","date":"2026-04-02","keywords":["spring-boot-3.5","opentelemetry","java25","virtual-threads","jfr","java-flight-recorder","otel-best-practices","micrometer","grafana-lgtm","distributed-tracing"],"permalink":"/posts/spring-boot-35-otel-best-practices/","tags":["spring-boot","opentelemetry","java25","observability","jfr","grafana","otel","virtual-threads","continuous-profiling"],"title":"Spring Boot 3.5 + OpenTelemetry 实践笔记（2026）"},{"content":"引言 在 2026 年的云原生环境中，Java 应用部署到 Kubernetes（K8s）已经比较常见。不过，很多团队仍然沿用早期 JVM 配置方式，这类配置在容器里不一定合适，也容易带来资源浪费或 OOMKilled。\nAkamas 在 2026 年 2 月发布的一份调查1提到，60% 的 JVM 工作负载未明确配置 GC，不少部署没有显式设置堆内存上限。我把它当作一个值得参考的行业观察，而不是绝对结论。\nJava 25 于 2025 年 9 月发布，是继 Java 21 之后的最新长期支持版本（LTS）。而 Java 26（非 LTS）也已于 2026 年 3 月正式发布。本文结合 Java 25/26 的一些新特性，以及 2026 年实验验证数据，整理我当前比较认可的一组 Kubernetes 容器化 JVM 参数思路。\n核心前提：容器感知已是本能 Java 版本与容器感知 Java 版本 -XX:+UseContainerSupport 默认行为 Java 8u191 之前 ❌ 不支持容器感知 Java 8u191+ ✅ 支持，需手动启用 Java 10+ ✅ 默认启用，无需配 …","date":"2026-04-01","keywords":["JVM 调优","K8s","JDK_JAVA_OPTIONS","MaxRAMPercentage","容器内存"],"permalink":"/posts/java-jvm-k8s-2026/","tags":["Java","Kubernetes","JVM","容器化"],"title":"K8s 容器化 Java 应用 JVM 配置笔记：JDK_JAVA_OPTIONS、MaxRAMPercentage 与 GC 选择"},{"content":"在上一篇总览文章中，我们看到了 Shop Platform 的整体架构分层。API Gateway 作为整个系统当前统一的外部入口，承载了身份验证、流量控制、路由分发、文档聚合等多项职责。\n📦 本文基于的完整项目源码：https://github.com/meirongdev/shop\n上一篇：（一）Shop Platform 总览\n2026-04 实践更新 当前主线代码已经从 HS256 共享密钥切到 RS256 + JWKS，并把网关限流的 Lua 执行器统一到 RedissonClient + RScript。下面相关代码片段已按仓库当前实现同步。\n为什么选 Spring Cloud Gateway Server MVC Spring Cloud Gateway 最初基于 WebFlux（Reactor）构建，是一个响应式网关。但在 2024–2025 年间，Spring Cloud 团队推出了 Spring Cloud Gateway Server MVC——一个基于 Servlet/MVC 的网关实现。\n我们选择 MVC 而非 WebFlux，主要考虑了三点：\n1. 统一编程 …","date":"2026-04-01","keywords":["Spring Cloud Gateway MVC","Virtual Threads","JWT 校验","Redis Lua 限流","灰度发布","Trusted Headers","Spring Cloud Gateway Server MVC"],"permalink":"/posts/shop-platform-api-gateway/","tags":["spring-boot","java25","cloud-native","api-gateway","spring-cloud-gateway","virtual-threads","rate-limiting","canary"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（二）：API Gateway 架构"},{"content":"一场“DNS 挂了”的事故，是怎么把 CDN 一起拖下水的 如果你经历过一次真实的 DNS 故障，就会发现它的表象非常有迷惑性：源站明明还活着，负载均衡也没全部坏，甚至某些地区的用户还能短暂打开页面，但外界感受到的却像是“整个网站突然消失了”。\n这里最容易产生的误解是：CDN 在前面，所以只要 CDN 健康，网站就应该继续可用。 但很多情况下并非如此。对绝大多数网站来说，DNS 不是一张静态电话簿，而是把用户请求导向某个 CDN 边缘入口的控制平面。DNS 出问题，用户甚至拿不到“该连哪个边缘节点”的答案，于是 CDN、源站和负载均衡会一起表现成不可达。RFC 1034 和 RFC 1035 对 DNS 的权威数据模型做了最基础的定义，NIST SP 800-81r3 则直接把 DNS 基础设施视为网络架构里的关键依赖。\n这篇文章就从这个灾难视角出发：先解释 CDN 和 DNS 到底是怎么配合工作的，再推演几类最常见的失效路径，最后收束到“怎样让网站在 DNS 级事故里争取更强韧性”的设计原则。\nCDN 和 DNS 是怎么配合工作的？ 当用户在浏览器里输入一个域名时，请求并不会直接飞到 …","date":"2026-03-31","keywords":["dns failover","cdn","traffic steering","anycast","multi dns","multi cdn"],"permalink":"/posts/cdn-dns-survivability/","tags":["dns","cdn","architecture","high-availability","sre"],"title":"CDN 和 DNS 是怎么配合工作的？网站如何在 DNS 宕机里争取可用性"},{"content":"背景 我们的项目使用 Confluent Cloud 托管 Kafka 集群，在上线前需要确定每个 topic 的分区（partition）数量。Kafka 的分区数只能增加不能减少，因此更适合在初始阶段先做一轮带假设前提的评估。\n本文记录这一评估过程。\n业务规模 基础数据 (单 Topic) 参数 数值 单 Topic 日均消息量 4 亿条 活跃用户数 ~200 万 人均日均交易笔数 200 笔 每笔交易产生消息数 1 条 Topic 数量 约 60 个 集群日均总消息量 约 240 亿条 单 Topic RPS 推导 单 Topic 全天平均 RPS = 4亿 ÷ 86,400秒 ≈ 4,630 交易流量并非均匀分布。假设流量集中在约 8 小时的活跃窗口内：\n单 Topic 活跃时段平均 RPS = 4亿 ÷ (8 × 3,600秒) ≈ 13,900 活跃窗口内还存在早晚高峰，峰值通常为活跃时段均值的 2–3 倍：\n峰值系数 单 Topic 峰值 RPS 2x ~28,000 3x ~42,000 以 3x 峰值系数估算，单 Topic 峰值约为 4.2 万 RPS。\n消息规格  …","date":"2026-03-30","keywords":["kafka","partition","confluent","分区数量","吞吐量","RPS"],"permalink":"/posts/confluent-kafka-partition-count-evaluation/","tags":["kafka","confluent","architecture","performance"],"title":"Confluent Kafka 业务分区数量评估笔记"},{"content":"我的 Homelab 跑在一台迷你主机上：AMD Ryzen 5 5600H（6 核 12 线程，笔记本 U），上面装的是 Proxmox VE，里面只开了一台 K3s 虚拟机。最近摸机箱的时候明显感觉烫手，一查 CPU 温度常驻 72°C，决定好好优化一下。\n现状诊断 先 SSH 上去摸底。我的 Proxmox 宿主机在 Ansible inventory 里已经配好了，直接用 ad-hoc 命令采集信息：\nansible proxmox -m shell -a \u0026amp;#39;lscpu | grep \u0026amp;#34;Model name\u0026amp;#34;; sensors; free -h; qm list\u0026amp;#39; 诊断结果比我预想得更激进一些：\n项目 现状 问题 CPU Governor powersave 已经是省电模式，没问题 Turbo Boost 开启 CPU 频率飙到 3GHz+，主要热源 内存 13GB 物理 / VM 分配 15GB 严重超售，swap 占了 3.9GB KSM (ksmd) 默认 20ms 扫描间隔，占 5% CPU 因为内存超售疯狂做内存去重 ASPM …","date":"2026-03-29","keywords":["proxmox 降温","debian 省电","homelab 散热","turbo boost 禁用","powertop","ansible playbook","KSM 优化","VM 内存超售"],"permalink":"/posts/debian-proxmox-power-optimization/","tags":["homelab","proxmox","debian","ansible","power-management","devops"],"title":"Homelab 过热？给 Proxmox Debian 宿主机降温的完整实战"},{"content":"同时维护多个 AI coding 工具是越来越常见的选择。我本地同时使用 Claude Code、GitHub Copilot、Gemini CLI、Qwen 和 OpenCode。\n每个工具在正式使用前通常都需要一定的配置，让它了解你的工作方式——比如\u0026amp;quot;先写测试再写实现\u0026amp;quot;，或者\u0026amp;quot;遇到 bug 先系统排查再动手改代码\u0026amp;quot;。\n问题在于，每个工具的配置文件格式各不相同：\nClaude Code 读取 CLAUDE.md GitHub Copilot 读取 .github/copilot-instructions.md Gemini CLI 读取 GEMINI.md OpenCode 读取 OPENCODE.md 这意味着同一套工作方式的描述，需要在多个文件里重复维护。有没有更省事一点的方式？\n背景：多 agent 的配置碎片化问题 想象你希望所有 AI 工具都遵循 TDD（测试驱动开发）——先写失败的测试，再写最小实现，再重构。\n在以前，你需要把这套描述分别写入每个工具的配置文件。一旦你想调整措辞或补充细节，就得同步修改多处。更麻烦的是，这些配置不太方 …","date":"2026-03-29","keywords":["skills","superpowers","npx","ai agent","claude code","github copilot","gemini cli","opencode"],"permalink":"/posts/npx-skills-superpowers-multi-agent/","tags":["ai","tools","skills","claude","copilot","gemini"],"title":"用一条命令为所有 AI Coding Agent 安装 Skills"},{"content":"很多团队在做单体拆微服务时，第一反应都是先拆代码、拆接口、拆部署。但数据库层真正难的地方，往往不是“怎么建新表”，而是：\n旧系统已经在线跑着，不能随便停 Liquibase changelog 早就耦合成一个大文件 业务表和外键关系跨领域交织 拆服务后，数据库迁移责任边界也要跟着重画 这次我做了一个 Liquibase Split POC，目标不是做一个“看起来很对”的设计稿，而是把一条在本地跑通并验证过的渐进式迁移路径做出来：\n完整 POC 代码及运行脚本见 GitHub: meirongdev/liquibase-split\nPhase 1：单体 + 单库 + 单 changelog Phase 2：还是单库，但 changelog 按服务拆开 Phase 3：每个服务拥有自己的数据库 而且这次不是只写代码，我尽量把整个实验跑完整，并记录了每一步的迁移结果。\n这次 POC 要解决什么问题 如果一开始就要求把单体数据库“一刀切”拆成多个库，风险通常很高：\nLiquibase 历史已经存在，不能随便改 checksum 线上数据库里已经有数据，不能直接重建 跨服务 FK 会让拆库变得很 …","date":"2026-03-26","keywords":["Liquibase Split","数据库迁移","Spring Boot 3.5","Java 25","PostgreSQL","渐进式拆库","微服务迁移"],"permalink":"/posts/liquibase-split-progressive-migration/","tags":["liquibase","database-migration","spring-boot","postgresql","microservice","java25"],"title":"Liquibase Split POC：把单体数据库迁移拆成三阶段的实战记录"},{"content":"如果你还没理清 h2 和 h2c 的区别，可以先看我的基础概念篇。\n这一篇不再讲概念，而是回答一个更实际的问题：\nSpring Boot 3.5 把 h2c 打开以后，真的会比 HTTP/1.1 更快吗？\n我在自己的 Shop Platform 概念验证（POC）项目里，围绕 buyer-bff -\u0026amp;gt; marketplace-service 做了一整轮压测。这次实验全程基于 Java 25 (LTS) 并在 Spring Boot 中开启了 虚拟线程 (Virtual Threads)。\n2026-04 实践更新 当前主线仓库里，BFF 的下游调用已经进一步演进成 @HttpExchange typed clients；因此本文关于 h2c / HTTP/1.1 的性能讨论依然成立，但调用栈样例请理解为“传输层对比实验”，不再代表今天 main 分支的全部客户端装配细节。\n先说这次实验里的几个观察：\n不会天然更快 但在特定 workload 下，的确可能更快 关键不在“有没有开 h2c”，而在“你的请求模型有没有真的吃到多路复用”，以及虚拟线程在高并发 I/O 场景下会不会放大这 …","date":"2026-03-25","keywords":["Spring Boot 3.5","h2c","HTTP/2","HTTP/1.1","JDK HttpClient","benchmark","Undertow","Tomcat","k6"],"permalink":"/posts/spring-boot-35-h2c-benchmark/","tags":["spring-boot","http2","h2c","java25","performance","benchmark","k6","microservice"],"title":"Spring Boot 3.5 开启 h2c 后，真的比 HTTP/1.1 更快吗？一次完整压测实验复盘"},{"content":"为什么要做这个项目 在实际工作中，每当要推一套新的技术方向时，团队最常遇到的问题不是\u0026amp;quot;知不知道这个技术\u0026amp;quot;，而是——\u0026amp;ldquo;这东西跑起来是什么样子？出了问题怎么排查？和现有的模块能不能配合？\u0026amp;rdquo;\n光读官方文档和博客往往很难回答这些问题。于是我决定做一个尽量完整跑通的 POC 项目——一个电商平台原型，把当时想亲手验证的技术选型尽量放进同一个可运行工程里，看看它们在微服务场景中能不能协同工作。\n这个项目就是 Shop Platform，也是本系列文章的主角。\n2026-04 实践更新 目前主线仓库已经完成一批最初文章发布时仍在演进中的平台项：auth-server → RS256 + JWKS、api-gateway → JWKS 校验 + Redisson Lua 限流、buyer-bff / seller-bff → 全量 @HttpExchange typed clients、镜像构建 → AppCDS 三阶段构建。还刻意保持延期的，主要是 DistributedLockExecutor 抽象、搜索增强、AI 推荐等下一阶段能力。\n系列会围绕几个方 …","date":"2026-03-21","keywords":["Spring Boot 3.5","Java 25","Cloud Native","微服务","架构设计","技术选型","POC"],"permalink":"/posts/shop-platform-cloud-native-overview/","tags":["spring-boot","java25","cloud-native","microservice","architecture","system-design"],"title":"Spring Boot 3.5 + Java 25 + Cloud Native 系列（一）：Shop Platform 总览"},{"content":"最近在使用各种 AI Coding Agents（如 Claude Code, Gemini CLI）进行开发时，我发现它们在后台处理复杂任务时，经常会用到 \u0026amp;ldquo;Worktree\u0026amp;rdquo;。于是我顺手补了一轮 Git Worktree 的基础知识，也想弄清楚这些工具为什么偏爱它。\n那么，到底什么是 Git Worktree？为什么这些工具和一些开发者会愿意用它？\n1. 什么是 Git Worktree？ 简单来说，git worktree 允许你在同一个仓库中同时拥有多个工作目录。\n“平行宇宙”模型 通常情况下，一个 Git 仓库只有一个工作目录（Working Directory）。当你执行 git checkout branch-b 时，Git 会直接修改你当前目录下的所有文件。如果你当前的代码还没写完（比如正在进行一项复杂的重构），你就得先执行 git stash 或者临时提交一个不完整的 commit，才能切换分支去修 Bug。\n而 git worktree 就像是为你创建了多个“平行宇宙”：\n主工作目录 (Main Worktree)：你通过 git clone …","date":"2026-03-20","keywords":["git worktree","git workflow","coding agent","parallel development"],"permalink":"/posts/git-worktree-study/","tags":["git","productivity","ai-agent","dev-tools"],"title":"为什么不少 AI Agent 会用 Git Worktree？一篇 Worktree 学习笔记"},{"content":"随着 Java 26 的发布（2026 年 3 月），Java 语言的新特性生态已经比前几年完整得多。Java 25 作为当前的 LTS 版本，也确实会进入不少团队的评估范围。对于长期依赖 Lombok @Data 或 @Value 的开发者来说，一个核心问题浮出水面：在 2026 年，Record 到底能在多大程度上替代 Lombok？\n本文将结合最新的技术趋势和社区共识，深度解析两者的博弈。\n版本说明：本文以 Java 25（LTS）为主要基准，部分特性会标注其在 Java 26 中的最新状态。\n1. Java Record 的核心价值：不仅仅是语法糖 Java Record 自 Java 14 引入并在 Java 16 正式发布以来，其定位始终是 “透明的数据载体” (Transparent carriers for immutable data)。\n原生不可变性：Record 字段默认是 final 的，这符合现代函数式编程倡导的不可变原则。 语义清晰：与 Lombok 这种通过字节码增强（Annotation Processing）实现的工具不同，Record 是 Java  …","date":"2026-03-19","keywords":["Java 25","Java Record","Lombok vs Record","Pattern Matching"],"permalink":"/posts/java-records-vs-lombok-best-practice-java25/","tags":["Java","Lombok","Record","实践记录"],"title":"Java 25（当前 LTS）下，Record 能在多大程度上替代 Lombok？"},{"content":"在 Java 生态系统中，\u0026amp;ldquo;向后兼容性\u0026amp;quot;一直是一把双刃剑。一方面，它保证了旧代码能在新版本 JDK 上平稳运行；另一方面，对于库（Library）维护者来说，为了支持还在使用 Java 8 的用户，往往不得不放弃 Java 11、17 甚至 21 中引入的高效 API。\n为了解决这个矛盾，Java 9 引入了 JEP 238: Multi-Release JAR Files (MRJAR)。它允许在一个 JAR 包中针对不同的 Java 版本存放同一份类的不同实现。\n什么是 Multi-Release JAR？ 简单来说，Multi-Release JAR 允许你的库在不同的 JDK 版本上表现出不同的行为：\n在旧版本 JDK（如 Java 8）上运行时，它执行基础版本的代码。 在新版本 JDK（如 Java 17）上运行时，它自动选择针对该版本优化的代码。 这一切对用户是透明的，他们只需要像往常一样引用一个 JAR 包即可。\nMRJAR 的内部结构 一个 Multi-Release JAR 的秘密全在 META-INF 目录下。它的标准结构如下： …","date":"2026-03-18","keywords":["MRJAR","JEP 238","Multi-Release JAR","Java 9","Java 21"],"permalink":"/posts/java-multi-release-jar/","tags":["Java","JDK","Build-Tools"],"title":"Java Multi-Release JAR 学习笔记：兼顾新 JDK 与向后兼容的一种做法"},{"content":"Apple Silicon 的统一内存架构让本地运行大模型成为可能，而在 macOS 上，目前主要有两条路：通用方案 Ollama（基于 llama.cpp），以及 Apple 官方为 Apple Silicon 定制的 MLX 框架。\n这篇文章记录我在 M2 MacBook Pro (32GB) 上的实测过程，重点回答一个问题：在日常使用中，MLX 到底比 Ollama 快多少？值不值得切换？\n测试环境与方法 硬件: M2 MacBook Pro，32GB 统一内存 系统: macOS（后台无其他 GPU 密集型任务） 测试 Prompt: 固定同一段中文提问，限制输出 128 tokens 重复次数: 每组跑 3 次取平均，排除冷启动影响 引擎 模型 量化格式 Ollama qwen3.5:latest GGUF Q4_K_M MLX mlx-community/Qwen3.5-9B-MLX-4bit MLX 4-bit 两者均使用 Qwen3.5 9B，量化精度对齐为 4-bit，尽量排除模型本身的变量。\nMLX 与 GGUF：两种格式的本质差异 理解这两种格式的区别，有助于解释 …","date":"2026-03-14","keywords":["mlx","ollama","apple silicon","m2 mbp","benchmark"],"permalink":"/posts/mlx-vs-ollama-benchmark-on-m2-mbp/","tags":["ai","mlx","ollama","apple-silicon","benchmark"],"title":"MLX vs Ollama(GGUF)：M2 MBP 32GB 上的性能基准测试"},{"content":" 通过构建一个 SQLite MCP 服务器的完整案例，来学习 MCP 的核心概念和开发流程\n引言 在 AI 应用开发中，如何让大语言模型（LLM）安全、可靠地与外部数据和工具交互，是一个关键挑战。Model Context Protocol (MCP) 正是在这个背景下出现的一种开放协议。\n本文将通过一个实际项目——MCP SQLite Server，从零开始理解 MCP 的核心概念，并掌握完整的开发流程。\n什么是 MCP？ Model Context Protocol (MCP) 是一个开放协议，旨在标准化 AI 模型与外部数据源、工具之间的交互。到 2026 年，MCP 已经成为连接 LLM 与本地/远程资源时经常被讨论的一套接口约定，类似于为 AI 提供统一的“外设接口”。\nMCP 的核心架构 ┌─────────────────┐ │ AI Client │ ← 你的 IDE (Cursor/VSCode)、Claude Desktop 或自定义应用 │ (MCP Client) │ └────────┬────────┘ │ MCP Protocol (JSON-RPC) …","date":"2026-03-14","keywords":["Model Context Protocol","MCP Server","AI Tools","Context Window"],"permalink":"/posts/mcp-introduction/","tags":["MCP","AI","LLM","SQLite","Node.js"],"title":"深入理解 Model Context Protocol (MCP)：从概念到实践"},{"content":"When selecting a Confluent Cloud plan, one of the first limits you\u0026amp;rsquo;ll hit is the maximum connection count. Enterprise clusters allow 18,000 connections per eCKU (raised 4× in the Q3 2025 update); Standard cluster limits are lower — check the current cluster types table for the exact figure. Miscounting by even one component can push you into throttling territory.\nThis post gives you a practical formula for estimating connections in a Spring Boot application, explains what actually drives …","date":"2026-03-12","keywords":["Confluent Cloud","Kafka connections","Spring Boot","KafkaTemplate","KafkaListener","connection count","plan selection"],"permalink":"/posts/confluent-kafka-connection-count-spring-boot/","tags":["Kafka","Spring Boot","Confluent","Java","Microservices"],"title":"How Many Kafka Connections Does Your Spring Boot App Actually Use?"},{"content":"Akamas 发布的 The State of Java on Kubernetes 2026 提出了一个让人不安的论断：很多跑在 Kubernetes 上的 Java 应用，默认配置可能正在悄无声息地浪费资源、降低性能，甚至让服务在压力下更容易失稳。\n联系到最近我碰到的生产环境上的 oom 和 CPU 限制问题。于是我用 Java 25 + Spring Boot 3.5.1 + kind 本地集群搭了一套实验环境，用真实数据来验证这些说法。\n结论先说：按这次实验结果看，这些担心大多都有依据，而且修复成本并不高。\n实验设计 用一个 Spring Boot 应用暴露两个压测端点：\nGET /stress/memory?mb=N：分配 N MB 短生命周期对象，触发 GC GET /stress/cpu?seconds=N：持续计算质数 N 秒，消耗 CPU k6 混合打压（60% 内存请求 + 40% CPU 请求，10 VUs，60 秒），通过 Spring Actuator 采集 JVM 指标。\n基准容器规格：1c / 1Gi，4 个场景：\n场景 JVM …","date":"2026-03-11","keywords":["java 25","kubernetes","jvm tuning","MaxRAMPercentage","G1GC","cpu throttling","spring boot","kind","container","性能调优"],"permalink":"/posts/java25-k8s-jvm-config/","tags":["java","kubernetes","jvm","performance","spring-boot","kind"],"title":"Java 25 on Kubernetes：默认配置可能正在拖慢你的服务"},{"content":"写在前面 想给你的 Spring Boot API 提提速？开启 HTTP/2 (H2) 可能是成本最低、收益最高的方式之一。比起老掉牙的 HTTP/1.1，H2 靠着多路复用、头部压缩这些黑科技，能让你的请求跑得更快、更稳。\n但在 Spring Boot 3 里动手前，你得先搞清楚两个概念：h2 和 h2c。别看只差一个字母，用法可是天差地别。\nh2 vs. h2c：穿衣服还是“裸奔”？ 简单来说，这两者的区别就在于有没有 TLS（加密）：\nh2 = HTTP/2 Over TLS（穿了防弹衣，安全，生产环境标配） h2c = HTTP/2 Cleartext（“裸奔”，明文传输，主打一个快） 1. 核心大比拼 特性 h2 (HTTP/2) h2c (HTTP/2 Cleartext) 安不安全？ 非常安全 (TLS) 不安全 (明文) 浏览器理你吗？ 所有现代浏览器都支持 浏览器基本都不支持 用在哪？ 公网、APP 接口 微服务内网、K8s 集群、gRPC 配置难吗？ 需要 SSL 证书 Spring Boot 3.5 以后一行配置搞定 2. 怎么配 h2？(带加密模式) 这是我们 …","date":"2026-03-10","keywords":["Spring Boot 3","HTTP/2","h2","h2c","Multiplexing","Header Compression"],"permalink":"/posts/spring-boot-3-http2-h2-h2c/","tags":["Spring Boot","HTTP/2","Performance","Microservices","Java"],"title":"Spring Boot 3 开启 HTTP/2：h2 和 h2c 在什么场景下更合适？"},{"content":"引言 我在折腾 Homelab 时，经常会接触到 Cloudflare 这类边缘网络能力：动态公网 IP、源站暴露面、回源带宽压力，这些问题多少都会碰到。\n这篇更像是一次学习笔记：结合公开资料和 Homelab 场景，梳理一下 Anycast、分层缓存、请求合并这些能力大致是怎么配合工作的。\n第一关：流量是怎么“听话”地找到我家机房的？ Q1: Anycast 到底是怎么工作的？ A: 按 RFC 4786 的定义，Anycast（任播） 可以理解为：多个不同位置的节点对外提供同一个服务地址，流量由路由系统带到当前更“近”或更可达的那个节点。\nPoP (Point of Presence) 可以理解成边缘接入点。当用户发起请求时，BGP 会根据当时的路由状态，把流量送到一个合适的 PoP。这里的“近”更多是路由意义上的近，不一定等同于地理距离最近。\nQ2: 如果某个节点（PoP）突然“罢工”了怎么办？ A: 如果某个 PoP 不再对外宣告相关前缀，流量通常会重新收敛到其他仍然可达的节点。这也是 Anycast 常被用于提升可用性的原因之一。\n不过这里也不宜说得太绝对：实际切换体验会受到 …","date":"2026-03-09","keywords":["Anycast","PoP","Tiered Cache","Request Collapsing","源站保护"],"permalink":"/posts/cloudflare-anycast-edge-protection-deep-dive/","tags":["Cloudflare","Anycast","Edge Computing","Security","Homelab"],"title":"Homelab 实践笔记：聊聊 Cloudflare 这些能力背后的原理"},{"content":"背景 这次故障一开始看起来像两个互不相干的问题：\nArgoCD 里 gateway Application 一直是 Degraded 其他几个 Application 又出现了 OutOfSync 或者后续的 Unknown 如果只看表面，很容易把它归类成“Cilium Gateway API 有问题”或者“ArgoCD 状态异常”。\n但真正麻烦的地方在于，这次故障不是单点失效，而是一条跨层级的链路问题：\nArgoCD 健康状态异常 -\u0026amp;gt; 先看到 Gateway Degraded -\u0026amp;gt; 再发现 repo-server 拉 Git 也失败 -\u0026amp;gt; 往下追到 CoreDNS 外部解析异常 -\u0026amp;gt; 再继续追到 ZITADEL 根本没有把 backend Service 建出来 -\u0026amp;gt; 最后发现 Vault 里的 ZITADEL master key 长度也不对 也就是说，最上层暴露出来的是 Gateway 和 ArgoCD，真正的根因却横跨了：\nCilium Gateway API ArgoCD repo-server CoreDNS systemd-resolved …","date":"2026-03-08","keywords":["cilium gateway","argocd","k3s","coredns","zitadel","vault","external secrets","homelab"],"permalink":"/posts/cilium-gateway-argocd-dns-recovery/","tags":["homelab","k3s","cilium","argocd","zitadel","vault","dns","troubleshooting"],"title":"从 Cilium Gateway 到 CoreDNS：一次跨层级的 K8s 连锁故障排查"},{"content":"背景 在前两篇文章中，我分别记录了 homelab 集群 和 Oracle Cloud 集群 从 Flannel 迁移到 Cilium 的过程。两个集群各自独立运行 Cilium，通过 Tailscale 子网路由实现 Pod CIDR 互通，OTel Collector 从 oracle-k3s 推送日志、指标和链路追踪到 homelab 的 LGTM 栈。\n这个架构能工作，但有一个明显的缺失：两个集群之间没有原生的服务发现。oracle-k3s 的 OTel Collector 必须通过 NodePort + Tailscale IP 硬编码连接 homelab 的 Loki、Prometheus、Tempo。如果 homelab 的 Tailscale IP 变了（剧透：它真的变了），所有跨集群的连接都会断。\nCilium ClusterMesh 正是为解决这类问题设计的——它在已有的 Cilium 数据面上增加跨集群的身份联邦和服务发现，而不需要额外的 overlay 网络。\n本文记录从 homelab 集群重建到 ClusterMesh 双向连接的完整过程。\n架构概览 双集群 …","date":"2026-03-08","keywords":["cilium clustermesh","k3s","multi-cluster","cross-cluster","tailscale","kvStoreMesh","service discovery","ebpf","homelab"],"permalink":"/posts/cilium-clustermesh-dual-k3s/","tags":["homelab","k3s","cilium","clustermesh","tailscale","networking","multi-cluster"],"title":"Cilium ClusterMesh 实战：连接两个 K3s 集群的跨云服务发现"},{"content":"背景 我的 homelab 现在是双集群结构：\nhomelab 跑在 Proxmox 上，负责 Vault、ZITADEL、ArgoCD、Grafana、Kopia 这类核心服务。 oracle-k3s 跑在 Oracle Cloud A1 免费机上，负责 Homepage、Miniflux、KaraKeep、Uptime Kuma、Timeslot 等轻量工作负载。 前一阵我先把 homelab 主集群从 Flannel 切到了 Cilium。迁移完成后，整个架构留下了一个非常明显的不对称：一边是 Cilium，一边还是 K3s 默认 Flannel。\n这件事短期看没有坏处，但中长期会越来越烦：\n网络问题的排障路径不一致。 两套集群文档会慢慢分叉。 以后不管是做 ClusterMesh PoC，还是只是想统一认知模型，都会被这块技术债反咬。 所以这次我决定把 oracle-k3s 也迁到 Cilium。\n这次迁移真正的目标 表面目标是把 CNI 从 Flannel 换成 Cilium，但真正目标其实有三个：\n统一双集群数据面：以后提到 Pod 网 …","date":"2026-03-07","keywords":["oracle k3s","cilium","flannel","cloudflared","vault","externalsecrets","rsshub","k3s migration","homelab"],"permalink":"/posts/oracle-k3s-cilium-migration/","tags":["homelab","k3s","oracle-cloud","cilium","cloudflare","vault","gitops","troubleshooting"],"title":"Oracle Cloud K3s 迁移到 Cilium：一次把网络、密钥和状态数据都翻出来的升级"},{"content":"背景 我的 homelab 运行着一个单节点 K3s 集群（Proxmox VM 上的 Ubuntu），通过 Cloudflare Tunnel 对外提供十几个服务。之前一直用 K3s 默认的 Flannel (VXLAN) 作为 CNI，一切安好。\n出于对 eBPF 可观测性（Hubble）和更强 Network Policy 能力的兴趣，我决定将 CNI 替换为 Cilium。迁移本身不复杂——需要重装 K3s（禁用 Flannel 和内置 Network Policy）、安装 Cilium Helm chart、然后按依赖顺序重部署所有工作负载。\n迁移过程在 安装计划文档 中有详细记录。本文聚焦于迁移完成后遇到的三个意外问题，以及每个问题的排查过程。\nCilium 配置概览 先记录一下我最终使用的 Cilium 配置，作为后续排查的上下文：\n# cilium-values.yaml 关键配置 kubeProxyReplacement: false # 保留 K3s kube-proxy routingMode: tunnel # VXLAN …","date":"2026-03-07","keywords":["cilium","k3s","flannel","ebpf","cni","migration","cloudflared","quic","metrics-server","zitadel","homelab"],"permalink":"/posts/cilium-migration-on-k3s-homelab/","tags":["homelab","k3s","cilium","ebpf","networking","troubleshooting"],"title":"K3s 集群 CNI 迁移实战：从 Flannel 到 Cilium 的踩坑记录"},{"content":"背景 我的 Homelab 由两个 K3s 集群组成：\nhomelab (Proxmox VM)：运行 Vault、ZITADEL SSO、Calibre-Web、Grafana 等核心服务 oracle-k3s (Oracle Cloud)：运行 Miniflux RSS、KaraKeep 书签、Uptime Kuma 监控等轻量服务 整套基础设施通过 Terraform、Ansible、ArgoCD 实现了 IaC + GitOps 管理。按当时的设计，大部分基础设施和应用配置都可以通过 Git 仓库里的代码重新拉起。\n但这里一直有一个我不太放心的盲区：数据。\nIaC 可以重建基础设施，GitOps 可以重建应用配置，但 Vault 的密钥、ZITADEL 的用户数据、Miniflux 的 RSS 阅读历史这类有状态数据，一旦丢失，恢复成本会高很多。\n这篇主要记录我怎样在这套双集群架构上落地 Kopia 备份体系，也顺便记一下修复 Traefik 路由问题的过程。\n问题发现：backup.meirong.dev 无法访问 在着手做备份自动化之前，我发现 Kopia 的 Web UI …","date":"2026-03-07","keywords":["kopia","backup","k3s","homelab","cronjob","gitops","argocd","disaster-recovery","postgresql","vault","nfs"],"permalink":"/posts/homelab-backup-system-kopia-gitops/","tags":["homelab","k3s","backup","kopia","gitops","argocd","disaster-recovery"],"title":"Homelab 备份体系实践：Kopia + CronJob + GitOps 的一次落地记录"},{"content":"背景 在开发新加坡小学数学 AI 辅导 App 时，我使用 Ollama 本地运行 qwen3.5:2b 模型。qwen3.5 是 thinking-capable 模型，Ollama 0.12+ 默认开启 thinking 模式，会先在 thinking 字段输出推理过程，再在 content 字段输出最终答案。\n我当时遇到的问题是：thinking 模式下推理耗时从 ~16s 增加到 ~52s（约 3x），而且对当时那组 PSLE 小学数学题来说，我并不需要这么长的推理链路，所以想先把 thinking 关掉。\n现象 使用 Spring AI 2.0.0-M2 提供的 API 关闭 thinking：\nChatClient.prompt() .system(systemPrompt) .user(userMessage) .options(OllamaChatOptions.builder().disableThinking().build()) .call() .content(); Ollama 返回 HTTP 400：\nthink must be a boolean or …","date":"2026-03-06","keywords":["spring ai","ollama","think field","OllamaChatOptions","disableThinking","ClientHttpRequestInterceptor","RestClientCustomizer","qwen3","spring boot 4"],"permalink":"/posts/spring-ai-ollama-think-field-bug/","tags":["spring ai","ollama","troubleshooting","java 25","singapore math"],"title":"Spring AI 2.0.0-M2 的 Ollama think 字段问题：排查过程与 Interceptor 临时方案"},{"content":"回顾 Phase 1 搭建了 Java 25 + Spring AI 的多 Agent 解题管线。但当时 Planner Agent \u0026amp;ldquo;裸跑\u0026amp;rdquo;，没有任何参考题库。Phase 2 要解决的核心问题：让 Agent 带着\u0026amp;quot;知识\u0026amp;quot;回答问题。\n具体目标：\n导入 40 道 PSLE 真题/模拟题到向量数据库 解题前先做 RAG 检索，找到相似题作为 Prompt 上下文 按年级过滤，P5 请求不会看到 P6 难度的参考题 Redis 缓存相同题目的 AI 响应，避免重复调用 LLM 整体架构变化 graph TD A[SolveRequest] --\u0026gt; B[SolveService - Redis @Cacheable] B --\u0026gt;|Cache Miss| C[MathSolverOrchestrator] C --\u0026gt; D[RagRetrievalService] D --\u0026gt; E[(pgvector - vector_store)] D --\u0026gt;|Top-5 相似题| F[Planner Agent + RAG Context] F --\u0026gt; …","date":"2026-03-05","keywords":["spring ai","rag","pgvector","vector store","redis cache","psle","singapore math","ollama","embeddings","nomic-embed-text"],"permalink":"/posts/sg-math-tutor-phase2-rag-pgvector/","tags":["spring ai","rag","pgvector","redis","java 25","singapore math"],"title":"用 Spring AI + pgvector 落地 RAG 知识库：新加坡数学 AI 辅导 Phase 2 记录"},{"content":"项目背景 我正在开发一个面向新加坡 2026 PSLE 考纲的小学数学 AI 辅导 App。当前设计里我最看重的是：\n使用新加坡教育部推崇的 CPA (Concrete-Pictorial-Abstract) 教学法 多 Agent 协作：Planner → CPA Designer + Persona 并行处理 为家长和孩子分别生成不同语气的解题指导 技术栈选型比较激进：Java 25 + Spring Boot 4.0 + Spring AI 2.0 + Ollama 本地模型。这篇文章记录 Phase 1 的搭建过程和踩过的坑。\n架构概览 graph TD A[SolveRequest - question, grade P1-P6] --\u0026gt; B[Planner Agent - 分析题目,提取知识点,生成步骤] B --\u0026gt; C{StructuredTaskScope.fork} C --\u0026gt; D[CPA Designer - 生成 Bar Model - 可视化描述] C --\u0026gt; E[Persona Agent - 家长版 + 儿童版 - 双语气输出] D --\u0026gt; …","date":"2026-03-05","keywords":["java 25","spring boot 4","spring ai","multi-agent","structured concurrency","singapore math","psle","ollama"],"permalink":"/posts/sg-math-tutor-phase1-java25-spring-ai/","tags":["java 25","spring ai","spring boot","ai","structured concurrency","singapore math"],"title":"用 Java 25 + Spring AI 构建新加坡小学数学 AI 辅导 App — Phase 1 实战记录"},{"content":"背景 在为我的 Homelab 双 K3s 集群整理资源配置时，我发现几乎所有自定义 Deployment 都缺少 cpu limit——只设了 memory limit。这意味着单个 Pod 在没有额外限制时可以尽可能多地占用节点上的可用 CPU，在高负载时更容易影响其他服务的响应。\n这篇文章系统梳理了 Kubernetes CPU 配置的核心概念，包括：\nrequests 和 limits 的本质区别 Linux CFS（完全公平调度器）如何实现 CPU throttling 三种 QoS 类别（Guaranteed / Burstable / BestEffort）及其驱逐优先级 节点内存压力下的 Pod 驱逐机制 实际 Homelab 的配置策略与决策依据 requests 与 limits：两个容易混淆、但作用不同的概念 很多人把 requests 和 limits 当成一对必须同时出现的参数，但它们实际作用在不同阶段：\nrequests limits 作用时机 Pod 调度时 Pod 运行时 作用对象 Kubernetes 调度器 Linux 内核（cgroups） …","date":"2026-03-04","keywords":["kubernetes","cpu limits","cpu requests","qos","guaranteed","burstable","cpu throttling","cfs","node eviction","homelab"],"permalink":"/posts/k8s-cpu-qos-resource-management/","tags":["kubernetes","k8s","homelab","k3s","performance","observability"],"title":"K8s CPU 配置实践笔记：QoS、Throttling 与驱逐策略"},{"content":"为什么需要短链服务 博客里有时要贴一些很长的 URL——GitHub 链接、Grafana 面板、API 文档之类的。长 URL 在 Markdown 里虽然无所谓，但分享到微信、邮件时看起来乱糟糟的。\n另一个场景：跳转(或者外部)链接如果能统一走自己的短域名，更整洁，也方便以后追踪点击数据和做链接的安全检查和处理。\n为什么选 Sink Sink 是一个基于 Cloudflare Workers 的无服务器短链服务，核心特性：\n功能 说明 自定义 slug 手动指定或 AI 自动生成 Analytics 访客统计（设备类型、地区、来源） 二维码生成 每条短链自带 QR Code 链接过期 可设置有效期 批量导入导出 JSON / CSV AI 辅助 Cloudflare Workers AI 生成 slug 每日备份 R2 Bucket 定时备份 对我来说比较吸引的一点是：Sink 完全运行在 Cloudflare Workers 上，不需要额外维护服务器或数据库。 按我当前的个人使用量，它也还在 Cloudflare 的免费额度范围内。\n与其他自托管短链方 …","date":"2026-03-03","keywords":["sink","url shortener","cloudflare workers","短链接","自托管","kv","analytics engine","rate limiting"],"permalink":"/posts/sink-url-shortener-on-cloudflare-workers/","tags":["homelab","cloudflare","workers","url-shortener","self-hosted","wrangler"],"title":"用 Cloudflare Workers 自托管短链服务 Sink：s.meirong.dev 的一次部署记录"},{"content":"背景 在 Homelab 的可观测性建设中，我经历了几个阶段：\n最初：kube-prometheus-stack 提供 Metrics + Grafana，Promtail 采集日志到 Loki OTel 日志迁移：用 OTel Collector DaemonSet 替换 Promtail，统一到 OTLP 协议 多集群扩展：oracle-k3s 集群通过 OTel Collector 将 logs + metrics 推送到 homelab 的 Loki + Prometheus 本次改进：补齐 Traces 管道，让 Logs、Metrics、Traces 三类信号都能串起来 本文重点记录第 4 步——我这次是怎么在双集群场景下把 OpenTelemetry traces 接起来的。\n部署环境 集群 位置 节点 IP Tailscale IP k3s-homelab Proxmox VM 10.10.10.10 100.107.254.112 oracle-k3s Oracle Cloud ARM 10.0.0.26 100.107.166.37 两个集群通过 Tailscale …","date":"2026-03-01","keywords":["homelab","k3s","observability","opentelemetry","tracing","grafana","tempo","loki","prometheus","otel-collector","distributed-tracing","LGTM"],"permalink":"/posts/homelab-otel-tracing/","tags":["homelab","k3s","observability","opentelemetry","tracing","grafana","tempo"],"title":"Homelab OTel 实践：从日志采集到双集群全链路追踪"},{"content":"I wanted to show my availability on this blog — basically a simple \u0026amp;ldquo;here\u0026amp;rsquo;s when I\u0026amp;rsquo;m free\u0026amp;rdquo; widget. Most of the options I looked at either pushed me toward a third-party scheduling service (Calendly, Cal.com cloud) or required setting up OAuth credentials. Neither felt like a great fit for a static blog that only needed a read-only JSON feed.\nSo I built Timeslot — a small self-hosted service that exposes availability as a simple API while keeping the underlying calendar …","date":"2026-03-01","keywords":null,"permalink":"/posts/timeslot-calendar-visibility-system/","tags":["go","self-hosted","calendar","privacy","homelab","ical"],"title":"Timeslot: A Self-Hosted Calendar Availability Feed for a Static Blog"},{"content":"为什么写这篇文章 从 2024 年用 Docker Compose 起步，到现在运行着两个 K3s 集群、16 个服务、完整的 GitOps 流水线——这个 Homelab 项目已经迭代了很多轮。每次新增功能都会写一篇专题博客，但一直缺少一篇把所有东西串起来的全景文章。\n这篇文章更像是那张地图。它不会深入每个组件的部署细节（那些在各自的博客里），而是回答一个问题：这些东西现在是怎么组合在一起工作的？\n整体架构 整个 Homelab 分为五层：\n┌─────────────────────────────────────────────────────────────────┐ │ Layer 5: GitOps │ │ ArgoCD — 自动同步 Git → 集群，3 分钟轮询 │ ├─────────────────────────────────────────────────────────────────┤ │ Layer 4: External Access \u0026amp;amp; Security │ │ Cloudflare Tunnel + DNS + WAF ← …","date":"2026-02-28","keywords":["homelab","k3s","terraform","ansible","argocd","gitops","cloudflare","tailscale","vault","observability","sso","waf","architecture","infrastructure-as-code"],"permalink":"/posts/homelab-architecture-overview/","tags":["homelab","k3s","terraform","ansible","argocd","gitops","cloudflare","tailscale","架构"],"title":"我的 Homelab 架构梳理：双 K3s 集群的 IaC 与 GitOps 实践"},{"content":"背景 我的 Homelab 由两个 K3s 集群组成，通过 Cloudflare Tunnel 对外暴露服务：\n集群 位置 外部子域名 homelab Proxmox 虚拟机 argocd, auth, backup, book, grafana, notify, vault oracle-k3s Oracle Cloud home, tool, pdf, squoosh, status, rss, keep 所有流量路径如下：\nInternet → Cloudflare DNS → Cloudflare Tunnel → Traefik → 各服务 上线 SSO（ZITADEL + oauth2-proxy） 之后，认证层已经有了一层保障。但在 Cloudflare 边缘层，我一直还缺少更主动的防御：扫描器、恶意爬虫、暴力破解每天都还会打到入口。\n是时候把 WAF 配置起来了。\n目标 尽量利用现有免费额度：优先基于 Cloudflare Free Plan 能做的事情来设计 代码化：所有规则用 Terraform 管理，不手动改 Dashboard 双集群覆盖：Zone 级别的规则自 …","date":"2026-02-28","keywords":["homelab","cloudflare","waf","terraform","zone-settings","ruleset","rate-limiting","security","自托管安全"],"permalink":"/posts/cloudflare-waf-for-homelab/","tags":["homelab","k3s","cloudflare","waf","terraform","security"],"title":"用 Cloudflare WAF 为双 K3s 集群补一层边缘防护"},{"content":"背景 作为一个重度 RSS 用户，我每天通过 Miniflux 阅读大量技术文章。阅读过程中经常遇到一个问题：读到好文章想收藏，但收藏后很难再找到；想分享到 Telegram 频道，又需要手动复制链接。\n之前在 Miniflux 中\u0026amp;quot;starred\u0026amp;quot;的文章只是静静躺在那里，既没有归档到独立的书签管理器，也没有推送到任何地方。我需要一条自动化的信息管道来解决这个问题：\n自动存档：Miniflux 中保存的文章自动同步到 KaraKeep（书签管理器） 精选推送：在 KaraKeep 中给文章打上 telegram 标签后，自动推送到 Telegram 频道 尽量减少手动操作：除了阅读和打标签，其他环节都交给系统处理 部署环境 我的 Homelab 由两个 K3s 集群组成，通过 Tailscale 互联：\n集群 位置 已有服务 新增服务 oracle-k3s Oracle Cloud ARM Miniflux, Homepage, IT-Tools KaraKeep, Redpanda Connect k3s-homelab Proxmox 虚拟机 Grafana, …","date":"2026-02-28","keywords":["homelab","k3s","redpanda-connect","karakeep","gotify","miniflux","telegram","信息管道","webhook","自动化"],"permalink":"/posts/info-pipeline-miniflux-karakeep-gotify/","tags":["homelab","k3s","redpanda-connect","karakeep","gotify","miniflux","信息管道"],"title":"在 K3s 上串起一条信息管道：Miniflux → KaraKeep → Gotify → Telegram"},{"content":"这篇文章复盘一次典型的“表象和根因不一致”的故障：\n表象：make coverage 执行到某个集成测试类附近就卡住。 中间信号：日志里持续出现 timeout。 实际情况：不是单点问题，而是两个问题叠加，且第二个问题才是“卡死”的直接原因。 为了避免泄露业务细节，本文仅保留通用技术路径与框架层结论。\n现象与误导 最初看起来像是某个测试类导致卡死，但单独跑该类测试是通过的。真正的卡点出现在后续测试阶段，并且并不稳定：有时卡在 A 类之后，有时卡在 B 类之后。\n这类“位置漂移”的卡死通常意味着：\n有共享资源被污染（连接、锁、线程、容器状态）； 或有后台任务与测试并发竞争资源； 或两者同时存在。 第一层问题：连接池超时（不是最终卡死根因） 先看到的是数据库连接获取超时。排查后发现，项目里存在若干 fire-and-forget 的异步调用模式（...subscribe() 直接触发，不挂回主响应链），在压测式的集成测试串行执行中会慢慢积压。\n同时，测试环境里还有后台补偿任务定时运行，会进一步竞争连接与 Redis 资源。\n第一轮修复动作：\n测试中关闭不必要的后台补偿任务； 增加测试清表范 …","date":"2026-02-28","keywords":["make coverage hang","reactive redis","lettuce sharedlock","r2dbc timeout","integration test"],"permalink":"/posts/make-coverage-hang-reactive-redis-sharedlock/","tags":["spring boot","webflux","redis","lettuce","reactive","troubleshooting"],"title":"一次 make coverage 卡死排查：Reactive Redis 与 Lettuce SharedLock 的连锁问题"},{"content":"背景 我的 Homelab 由两个 K3s 集群组成：\n集群 部署位置 服务 homelab (k3s-homelab) Proxmox 虚拟机 Calibre-Web, Grafana, ArgoCD, Vault, Kopia, ZITADEL oracle-k3s Oracle Cloud IT-Tools, Stirling-PDF, Squoosh, Homepage, Uptime Kuma, Miniflux 之前所有服务各自管理登录——有的没有认证（公开访问），有的用内置用户名密码。这带来了几个问题：\n安全性差：部分服务直接暴露在公网无认证 体验碎片化：每个服务都需要单独登录 管理困难：密码分散在各处，无法统一管控 目标是部署一个自托管的 OIDC 身份提供者（Identity Provider），让所有受保护服务共用一套认证，实现 一次登录，多处访问。\n为什么选择 ZITADEL 对比了几个方案：\n方案 优点 缺点 Keycloak 功能最全，社区大 太重(Java)，内存 512MB+，配置复杂 Authentik 现代 UI，功能丰富 Python， …","date":"2026-02-27","keywords":["homelab","k3s","sso","zitadel","oidc","oauth2-proxy","traefik","gateway-api","forwardauth","vault"],"permalink":"/posts/zitadel-sso-on-k3s/","tags":["homelab","k3s","sso","zitadel","oauth2-proxy","traefik","gateway-api"],"title":"在K3s上部署ZITADEL实现SSO单点登录"},{"content":"The Incident After a performance optimization commit that refactored our distributed lock template to use Mono.usingWhen and removed a publishOn(Schedulers.boundedElastic()) call, our Spring WebFlux application started experiencing cascading Redis timeouts under moderate load. The stack traces all pointed to the same place:\nio.lettuce.core.RedisCommandTimeoutException: Command timed out after 2 second(s) at org.springframework.data.redis.connection.lettuce.LettuceConnection.await() at …","date":"2026-02-27","keywords":["spring boot","spring webflux","redis timeout","cacheable","reactive","netty event loop","blockhound","publishOn","boundedElastic"],"permalink":"/posts/reactive-cacheable-redis-timeout/","tags":["spring boot","spring webflux","redis","reactive","troubleshooting"],"title":"How a Performance Optimization Caused Cascading Redis Timeouts in Spring WebFlux"},{"content":"单体应用架构的挑战 微服务架构是对应于单体架构的一种设计思路。在单体架构中，所有的功能模块都被打包在一个应用程序中进行部署和运行。这种方式在应用程序较小且功能简单时是可行的，但随着应用程序的复杂度增加，单体架构会面临以下问题：\n可维护性差：单体应用程序的代码通常是紧密耦合的，导致修改一个功能可能会影响到其他功能，增加了维护的难度。\n扩展性差：在单体架构中，应用程序的所有功能都部署在一起，难以针对某个功能进行独立扩展。\n技术栈限制：单体应用通常使用单一的技术栈，限制了开发团队选择最佳工具和技术的灵活性。\n部署风险高：单体应用的部署往往需要停机，影响用户体验。而微服务架构可以实现独立部署，降低了风险。\n迁移到微服务和分布式架构的驱动因素 微服务架构（将大型单体应用分解成单独且更小的部分）带来了以下关键优势和驱动力：\n提高可维护性 (Maintainability)\n大型单体架构通常维护性较低，因为功能是按技术分层而非领域划分的，组件之间耦合紧密，且领域凝聚力弱。\n隔离变更范围：在单体应用中，对某个领域的更改必须在应用的各个层面传播。而在微服务架构中，变更范围被限制在特定的领域级别或功能级别 …","date":"2025-11-30","keywords":["microservice","system-design","architecture"],"permalink":"/posts/microservice-overview/","tags":["microservice","system-design","architecture"],"title":"Microservice(1) Overview"},{"content":"简介 Spring AI提供了一些功能来影响LLM返回的响应。\nSetting generation options： 可以设置一些generation的参数，比如temperature，max tokens等，来影响LLM下个token的选择，从而影响最终的response。 Structured Output Converter： 可以将LLM的response转为一个结构化的对象，方便后续处理。 Streaming Response： 可以实时获取LLM生成的结果，生成一部分就返回一部分，提升用户体验。 指定Chat的options 切换模型 Spring AI中每个AI Provider都可以选择模型，选择较小的模型，可以加快响应速度。比如我本地用ollama，我可以切换成deepseek-r1:1.5b模型，来加快响应速度。\n在application.yaml中指定模型\nspring: ai: ollama: chat: # model: mistral:7b model: deepseek-r1:1.5b 可以看到响应速度有了明显提升\n模型 Trial 1 Trial 2 …","date":"2025-11-22","keywords":["spring ai","spring boot","ai","prompt template","external template","prompt context"],"permalink":"/posts/spring-ai-revise-response-generation/","tags":["spring ai","spring boot","ai","prompt template","prompt context"],"title":"Spring AI(4)- 调整返回的Response - 今日运势"},{"content":"什么是 Prompt Template？ Prompt Template 允许开发者定义一个包含占位符 (placeholders) 的结构化字符串。运行时，这些占位符会被实际的数据（如用户输入、数据库内容、应用上下文等）填充，从而生成一个完整、复杂且动态的提示，发送给 LLM 模型。\n为什么使用 Prompt Template？ 使用 Prompt Template 有助于：\n提高一致性：确保所有生成的提示遵循相同的格式和结构。 简化复杂提示的创建：通过模板化的方式，轻松创建复杂的提示，而无需手动拼接字符串。 增强可维护性：当需要修改提示结构时，只需更新模板，而无需更改所有使用该提示的代码 Prompt Engineering 提示工程 (Prompt Engineering)有专门的指南， 如https://www.promptingguide.ai/，来帮助用户掌握prompt的设计技巧。\n基本上任何AI入门书籍都会提到prompt设计的重要性。一个好的prompt可以显著提升AI生成内容的质量。\nSpring AI 中的 Prompt Template 可以让用户来输入自己的生 …","date":"2025-11-21","keywords":["spring ai","spring boot","ai","prompt template","external template","prompt context"],"permalink":"/posts/spring-ai-prompt-template/","tags":["spring ai","spring boot","ai","prompt template","prompt context"],"title":"Spring AI(3)- Prompt Template - 今日运势"},{"content":"当我们使用AI生成的内容时，怎么去评估它的质量和准确性呢？有时候，AI生成的结果可能答非所问，或者包含错误的信息。\n目前的Spring AI(v1.1.0)提供了模型评估Model Evaluation的功能。\nEvaluation Testing 对AI生成内容进行评估的一个方式是使用AI模型本身进行评估。选择一个LLM模型来评估另一个LLM模型的输出。（本地测试时，可以使用同一个模型）\nSpring AI对不同的评估场景提供了统一的接口Evaluator，通过实现这个接口，可以定义不同的评估逻辑。\n@FunctionalInterface public interface Evaluator { EvaluationResponse evaluate(EvaluationRequest evaluationRequest); default String doGetSupportingData(EvaluationRequest evaluationRequest) { List\u0026amp;lt;Document\u0026amp;gt; data = …","date":"2025-11-17","keywords":["spring ai","spring boot","ai","model evaluation"],"permalink":"/posts/spring-ai-model-evaluation/","tags":["spring ai","spring boot","ai","model evaluation"],"title":"Spring AI(2)- Model Evaluation"},{"content":"背景 决定入手Spring AI。有几个原因：\n今年已经看了2本关于AI的书籍，AI-Assisted Coding和How Large Language Models Work，具备了一定的理论基础。 这个月看今年的Spring IO大会的相关视频，里面有Dan Dobrin对Spring AI的介绍，感觉Spring AI的可玩度应该是蛮高的了。 Dan Vega将自己的整个博客网站的内容使用Spring AI做了一个MCP服务，interesting! Spring AI项目生成 一样的，使用spring initializr生成项目骨架。\n和普通的Spring Boot的web项目一样，选择Spring Web依赖即可。 为了开发调试，选择Spring Boot DevTools依赖。 为了方便问题排查，选择Spring Boot Actuator依赖。 选择Java 25版本 + Gradle。 项目 功能 实现一个简单的QA对话，用户输入一个问题，调用OpenAI的API获取回答并返回给用户。\n官方文档Chat Client API\n代码实现 接口定义\npublic …","date":"2025-11-16","keywords":["spring ai","spring boot","ai"],"permalink":"/posts/spring-ai-chatclient-with-openai-gemini/","tags":["spring ai","spring boot","ai"],"title":"Spring AI(1)- Chat Client With OpenAI and Gemini"},{"content":"背景 我们的Spring Boot在Kubernetes环境中运行，配置文件application.yaml中包含了一些环境变量占位符，例如${DB_HOST}、${DB_PORT}等。这些变量需要在应用启动前被替换为实际的值。\n但这样的配置文件无法直接被Spring Boot识别，在本地需要开发或者调试的时候，我们希望能够将这些环境变量替换为实际的值，从而生成一个完整的application.yaml文件。\n解决方案 envsubst是一个命令行工具，可以将输入中的环境变量替换为其实际值。\n步骤 在ubuntu中安装envsubst：\nsudo apt update sudo apt install gettext-base 原先的application.yaml文件内容如下：\napp: version: \u0026amp;#34;@project.version@\u0026amp;#34; payment-api-key: ${PAYMENT_API_KEY} payment-secret-key: ${PAYMENT_SECRET_KEY} payment-endpoint: …","date":"2025-11-13","keywords":["spring boot","envsubst","environment variable"],"permalink":"/posts/envsubst-replace-env-var-in-application-yaml/","tags":["spring boot","envsubst","environment variable"],"title":"Use Envsubst to Replace Env Var in application.yaml of Spring Boot"},{"content":"背景 什么是Spring Initializr Spring Initializr是一个用于生成Spring Boot项目的在线工具。它可以帮助开发者快速创建一个包含所需依赖和配置的Spring Boot项目骨架，从而节省了手动配置项目的时间和精力。\nweb界面: start.spring.io\n实际工作中，我们可能会怎么初始化spring boot微服务项目呢？找一个现有项目，删除各种不必要的文件，改改就好，但是这样做往往会让初始项目不太干净。\n需求 对于指定的项目类型，比如web项目，自动添加常用的依赖，比如spring-boot-starter-web, spring-boot-starter-data-jpa等。 自动添加git仓库初始化，并添加.gitignore文件。 自动添加ci/cd配置文件，比如GitHub Actions的workflow文件。 支持多种项目类型，比如web, cli, consumer, cronjob等。 本次探索主要针对web项目类型进行定制。\n定制Spring Initializr 生成基础项目 访问start.spring.io，选择项目类 …","date":"2025-11-01","keywords":["spring initializr","project generator","spring boot","code generation"],"permalink":"/posts/custom-spring-initializr/","tags":["java","spring boot","spring initializr","code generator"],"title":"探索定制Spring Initializr"},{"content":"Background Spring Boot 3 support graceful shutdown natively.\nIt enables the graceful shutdown feature by default.\n# application.yml server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s can refer to the official documentation for more details.\nThe timeout-per-shutdown-phase property defines how long Spring Boot will wait for ongoing requests to complete before shutting down, which is 30 seconds by default.\n@ConfigurationProperties(\u0026amp;#34;spring.lifecycle\u0026amp;#34;) public class …","date":"2025-10-29","keywords":["spring boot","kubernetes","graceful shutdown"],"permalink":"/posts/graceful-shutdown-for-springboot3-in-k8s/","tags":["spring boot","kubernetes","graceful shutdown"],"title":"Spring Boot3 graceful shutdown in Kubernetes"},{"content":"背景 我在homelab的k8s集群中使用helm部署了postgresql数据库，使用的是bitnami/postgresql这个chart。\nsetup-postgres: add-repos helm upgrade --install postgresql bitnami/postgresql -n database \\ --create-namespace \\ -f values/postgresql-values.yaml 启用了postgresql的metrics功能，prometheus operator会自动发现了这个服务，并创建了对应的ServiceMonitor对象。\n... other settings ... metrics: enabled: true # 启用 Prometheus 监控 serviceMonitor: enabled: true namespace: monitoring # ServiceMonitor 创建在哪个命名空间（Prometheus 能访问） labels: # 保证与 Prometheus CR 的 selector …","date":"2025-10-26","keywords":["homelab","k8s","prometheus stack","metrics","monitoring","troubleshooting"],"permalink":"/posts/prometheus-stack-service-monitor-troubleshooting/","tags":["homelab","k8s","prometheus stack","metrics","serviceMonitor","troubleshooting"],"title":"postgresql在prometheus stack中没有采集到metrics的排查"},{"content":"背景 已经在homelab搭建了microk8s集群，需要使用helm为集群添加observability功能，来监控和分析集群和软硬件的运行状态。\nObervability主要包括以下几个方面：\nMetrics：使用Prometheus收集和存储各种指标数据。 Visualization：使用Grafana创建仪表盘，展示指标数据。 Logging：使用Loki收集和存储日志数据。 Tracing：使用Tempo进行分布式追踪。 安装helm 首先需要在microk8s集群中安装helm。如果还没有安装helm，可以参考官方文档进行安装。\n我的mac和linux系统上都安装了brew，可以直接使用brew安装helm：\nbrew install helm Infrastructure的存储 在安装observability组件之前，需要先为它们创建存储类（StorageClass）和持久卷（PersistentVolume）。这里使用NFS作为存储后端。\n这里使用nfs-subdir-external-provisioner\n在k8s集群中直接使用helm chart安 …","date":"2025-10-25","keywords":["homelab","microk8s","k8s","helm","observability","prometheus","grafana","loki","tempo"],"permalink":"/posts/homelab-obervabiity-with-helm/","tags":["homelab","microk8s","k8s","helm","observability"],"title":"使用helm添加observability需要的服务"},{"content":"本文介绍如何使用Terraform和Ansible在Proxmox的虚拟机上创建Kubernetes集群。通过Terraform自动化创建虚拟机，并使用Ansible配置Kubernetes环境，实现快速部署和管理集群。\nAnsible主要用于配置和管理 Terraform主要用于创建虚拟机资源 Step 1: 使用Ansible获取image并生成virtual machine模板 vm cluster可以重复销毁和创建，但是vm template只需要创建一次，后续可以重复使用。\n下载Ubuntu 24.04 cloud image，给Proxmox使用，并生成虚拟机模板。\n- name: Download Ubuntu Cloud Image for Proxmox hosts: proxmox # Target Proxmox hosts become: true vars: image_url: \u0026amp;#34;https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img\u0026amp;#34; …","date":"2025-10-22","keywords":["homelab","proxmox","terraform","ansible","k8s","kubernetes","microk8s"],"permalink":"/posts/setup-k8s-cluster-with-terraform-and-ansible/","tags":["homelab","k8s","terraform","ansible","proxmox"],"title":"使用Terraform和Ansible创建K8s集群"},{"content":"背景 我的不同设备通过tailscale组网，可以方便地访问彼此。但是默认情况下，tailscale只能访问tailscale网络内的ip地址。\n某些场景下我们需要能访问内网ip。比如，在我的laptop上使用terraform来管理proxmox创建虚拟机的时候，需要访问虚拟机的宿主机的内网ip地址。\ngraph TD subgraph Remote Laptop [远程笔记本电脑] User[你] User --\u0026gt;|远程访问| Tailscale_Remote[Tailscale 客户端e.g., 100.x.x.x] end subgraph Your Laptop [你的笔记本电脑物理环境] subgraph Proxmox Host [Proxmox VE 主机] subgraph VM Guest [虚拟机 Guest] VM[VM10.10.10.100] end NIC_WLAN[\u0026#34;无线网卡 (WLAN)ensXX / wlpXsYIP: 192.168.50.3\u0026#34;] BRIDGE[\u0026#34;Linux Bridge (vmbr0)IP: 10.10.10.1\u0026#34;] …","date":"2025-10-22","keywords":["homelab","tailscale","route"],"permalink":"/posts/tailscale-route-setting/","tags":["homelab","tailscale","route"],"title":"让tailscale可以访问内网ip地址"},{"content":"背景 目前的工作对虚拟化和cloud native技术使用的比较多。搭建新的homelab节点，也希望将这些技术应用到日常的家庭服务器的管理中。\nIac的实践从terraform开始，使用terraform管理proxmox虚拟机，可以提高虚拟机创建和配置的效率，方便后续的维护和扩展。\n采用的工具 Proxmox VE 9.0 Terraform v1.15.0-dev - build from source Proxmox Terraform Provider - bpg/proxmox-v0.8.5 网络配置 我使用的机器是老旧的laptop，只有无线网卡，没有有线网卡。已经解决了wifi连接的问题，参考安装proxmox的wifi问题及解决方法。\n但是虚拟机的网络配置需要额外处理。\n默认情况下，Proxmox VE使用vmbr0作为桥接网络接口，将虚拟机连接到物理网络。然而，在只有无线网卡的情况下，桥接网络可能无法正常工作。为了解决这个问题，我们可以使用NAT（网络地址转换）来为虚拟机提供网络连接。\n修改Proxmox的网络配置文件/etc/network/interfaces， …","date":"2025-10-20","keywords":["homelab","proxmox","terraform","network"],"permalink":"/posts/terraform-create-vm-for-proxmox/","tags":["homelab","proxmox","terraform"],"title":"使用terraform为proxmox创建虚拟机"},{"content":"背景 使用laptop作为homelab时候，会有一些需要做额外处理的地方，比如屏幕关闭、电源管理等。\n我的laptop的cpu是amd r5 5600h。安装的系统是Proxmox VE 9.0，对应的os为Debian13 - Trixie。\n屏幕关闭 在每次启动的时候，laptop的屏幕默认是开启的。如果长时间不操作，屏幕需要自动关闭，节省能耗。\n设置grub参数\n编辑/etc/default/grub文件，设置GRUB_CMDLINE_LINUX_DEFAULT：\nGRUB_CMDLINE_LINUX_DEFAULT=\u0026amp;#34;quiet consoleblank=600\u0026amp;#34; consoleblank参数的值是以秒为单位的，表示在没有用户活动的情况下，等待多长时间后关闭屏幕。上面的例子中，设置为600秒（10分钟）。\n更新grub配置：\nupdate-grub 重启系统使配置生效：\nreboot 盒盖动作 默认情况下，当你关闭laptop的盖子时，系统会进入休眠或挂起状态。对于作为homelab使用的laptop，我们希望在关闭盖子时，系统继续运行。\n编 …","date":"2025-10-19","keywords":["homelab","proxmox","debian13","laptop"],"permalink":"/posts/laptop-homelab-settings/","tags":["homelab","proxmox","debian13","laptop"],"title":"使用laptop作为homelab需要的附加设置"},{"content":"背景 家里淘汰的联想小新2021pro笔记本，cpu是amd r5 5600h，16GB内存，512GB nvme硬盘。准备安装proxmox作为homelab的一个节点。\nproxmox使用的最新版本Proxmox VE 9.0， 对应的os为Debian13 - Trixie。\n安装过程比较顺利，系统也能正常启动。但是发现只有无线网卡可用，默认不启用wifi，需要自己手工配置wifi连接。\n解决方法 我想要连接网络安装wpasupplicant，但是proxmox默认没有安装这个包。需要先连接有线网络，更新apt源，然后安装wpasupplicant。\n没有有线网络的情况下，可以通过手机usb共享网络连接。\n通过手机usb共享网络连接 手机连接laptop的type-c接口，然后打开手机的tethering选项。\n在proxmox中，可以通过ip a命令查看到usb网络接口，一般是enx...开头的。\n如果想使用ifconfig命令，需要先安装net-tools包。现代的linux发行版推荐使用ip命令。\n启用usb网络接口：\nip link set dev enx... up 这 …","date":"2025-10-18","keywords":["homelab","proxmox","wifi","network"],"permalink":"/posts/proxmox-wifi-issue/","tags":["homelab","proxmox","wifi"],"title":"laptop安装proxmox的wifi问题及解决方法"},{"content":"Issue Description Recently, I encountered an OutOfMemoryError (OOM) issue in a Spring Boot 3 Webflux application. The error log is as follows:\n\u0026amp;#34;logger\u0026amp;#34;: \u0026amp;#34;reactor.core.scheduler.Schedulers\u0026amp;#34;, \u0026amp;#34;@version\u0026amp;#34;: 1, \u0026amp;#34;appVersion\u0026amp;#34;: \u0026amp;#34;0.0.0\u0026amp;#34;, \u0026amp;#34;cloud\u0026amp;#34;: \u0026amp;#34;aws-ec2\u0026amp;#34;, \u0026amp;#34;stacktrace\u0026amp;#34;: \u0026amp;#34;java.lang.OutOfMemoryError: Java heap space\\n\u0026amp;#34;, \u0026amp;#34;thread\u0026amp;#34;: \u0026amp;#34;boundedElastic-8\u0026amp;#34;, \u0026amp;#34;level\u0026amp;#34;: \u0026amp;#34;ERROR\u0026amp;#34; Root Cause Analysis The application …","date":"2025-09-24","keywords":["spring boot","spring boot3","spring boot actuator","oom","troubleshooting"],"permalink":"/posts/webflux-oom/","tags":["spring boot"],"title":"Spring Boot 3 Webflux OOM Troubleshooting"},{"content":"背景 在一次常规的集成测试构建中，发现集成测试整体耗时偏高，影响开发反馈速度。\n项目技术栈是 Spring Boot + WebFlux + R2DBC + Reactive Redis + Kafka，测试侧大量使用 Testcontainers。\n问题：集成测试为什么慢？ 观察结果 单测（Surefire）总耗时约：3.4s 集成测试（Failsafe）总耗时约：270s 瓶颈明显在 IT 阶段。\n关键原因 测试类型混跑： 一个纯 Mockito 测试命名为 *IT.java，被 Failsafe 执行。 容器启动重复： BaseIntegrationTest、BaseR2dbcTest、XxxServiceIT 各自起容器。 共享 MySQL 后出现互相干扰风险： BaseR2dbcTest 的 repository 测试在 @BeforeEach 做 DROP/CREATE TABLE，可能影响全链路 IT。 优化步骤 ）把\u0026amp;quot;伪集成测试\u0026amp;quot;移回单测阶段 将文件从：\nGameCodeServiceCacheIT.java 改为： …","date":"2025-01-24","keywords":["spring boot","integration test","testcontainers","performance optimization","testing optimization"],"permalink":"/posts/spring-boot-integration-test-optimization/","tags":["spring-boot","testing","performance","testcontainers"],"title":"Spring Boot Integration Test Optimization Practice"},{"content":"DDIA中提到了LSM-Tree，是一种高效的存储引擎，被广泛应用于分布式存储系统中。简单画图解释下相关的设计。\nAppend-Only Log 如果我们要存储数据，最简单的可能会选择key-value的存储方式。那么内存中就会有一个Map，key是数据的key，value是数据的value。但是这种方式有一个问题，如果内存中的数据丢失了，那么数据就丢失了。所以我们需要将数据持久化到磁盘上。此时，内存中的value就需要改为指向磁盘上的数据的位置。\n此时我们可以选择是哟个Append-Only Log，也就是只追加的日志。每次写入数据，都会追加到日志的末尾。这样即使系统崩溃了，我们也可以通过日志来恢复数据。\n写入流程\n将数据写入内存中的Map 将数据通过append的方式写入日志 读取流程\n从内存中的Map中读取要查找的数据在文件中的位置 从日志中读取数据 Pros\n简单 可以通过日志来恢复数据 顺序写入，性能高 Cons\n需要一个内存中的Map来存储数据，如果数据量大，内存不够，这种方案就不适用了 无法进行范围查询，因为key是无序的 LSM-Tree 为了解决Append-Only …","date":"2024-09-20","keywords":["data structure","system-design","database","storage"],"permalink":"/posts/log-structured-merge-tree/","tags":["data structure","system-design","storage"],"title":"LSM-Tree"},{"content":"Shipwright是一个构建和发布工具，它是一个Tekton的扩展，可以用来构建镜像并发布到dockerhub。\n大多数人可能对Jenkins比较熟悉，而Tekton是一个类似的工具，但它是一个云原生的工具，它的配置是通过yaml文件来定义的，这样可以很好的和gitops结合起来。\n如果deployment的规模比较大，可能使用Tekton会更好点，因为它的初始设计就是为了大规模的CI/CD。\n以下的内容是对github上的一个repo hello-python项目，构建镜像，并发布到dockerhub上。。\n前提条件 本地已经有个k8s集群 已经有一个dockerhub账号 安装shipwright 先安装Tekton pipeline\nkubectl apply --filename https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.50.5/release.yaml 安装Shipwright\nkubectl apply --filename …","date":"2024-08-13","keywords":["cicd","devops","tekton","gitops","shipwright"],"permalink":"/posts/shipwright-build-image-and-publish/","tags":["cicd","gitops"],"title":"Shipwright构建镜像并发布到dockerhub"},{"content":"The key tools in docs as code Text editor vscode vim jetbrain Collaboration git render ssg Docusaurus Hugo MKDocs Astro Starlight Eleventy Sphinx Helping less technical writers with headless CMS\nContentful Ghost Hosting your code snippets Replit JSFiddle Github Gists Screenshot tools Snagit is available for macOS and Windows. CleanShot is available for macOS. Flameshot is available for Windows, macOS, and Linux. Shutter is available for Linux. ksnip Diagram tools doc as code tools\nmermaind …","date":"2024-08-09","keywords":["technical writing","writing","documentation","tools"],"permalink":"/posts/tools/","tags":["writing","tools"],"title":"tech writing tools"},{"content":"作为内部开发平台的开发者，文档的写作流程和自己写博客也是不同的。\nScoping and requirements gathering 在公司或项目种，文档需求可能来自各种内部和外部来源，包括以下几种情况\n正在进行的新功能，可能来自产品负责人 已经完成的新功能需要文档 由你自己或者其他记录的问题 在开始补充这些文档内容的时候，第一步是要弄清楚当前“完成”的定义。不同人对其有不同的看法。尽管你可能已经尽力而为了，你仍然可能发现你对“完成”的理解和实际情况不符。\n根据你合作的人和你所在的公司或项目类型，你可能需要采取不同的措施来减少这种情况的发生。\n首先，询问stakehoders，他们对内容的预期是什么\n产品负责人可能希望你描述新功能如何帮助用户解决问题。 一位工程师可能希望你描述功能的具体细节，比如组件是否有本地缓存，是否支持动态变更， QPS能能够达到多少。 需要站在stakeholders的角度，了解他们的意图，很多时候并不是简单的事情。\nWhat to document 任何工作开始之前都有个工作量评估，你可能会想要知道要做什么以及需要多少文档。\n文档是个迭代的过程，尤其是当你是 …","date":"2024-07-31","keywords":["technical writing","writing","documentation"],"permalink":"/posts/process/","tags":["writing","misc"],"title":"关于科技写作-写作流程"},{"content":"Shoppe的工作主要是给内部开发者提供开发工具或者说开发平台，帮他们使用内部的标准化开发流程，提升开发效率和开发体验。这份工作和我以前的工作经验最大的区别是面对的用户是内部开发者。\n面对大量的内部用户，所有问题一一探讨是不可能的。此时文档就是个很重要的交付件，好的文档能减少大量的交互成本。\n科技写作并不是我擅长的，所以我在这份工作中也学习了很多。\nGas是一个基于Go的应用开发框架，集成了内部的统一sdk和一些基础服务，比如日志、监控、配置、CICD等。思想类似faas，业务项目的开发者只需要关注业务逻辑，并使用统一的sdk进行开发，后续非业务功能的集成和升级都通过框架来实现。\n在基础功能都基本完成后，就需要接入种子用户了。这时就需要文档了。\n文档的用户是谁 虽然文档的用户是开发者，但是我们没法保证他们都是非常专业，有些对golang技术栈或者内部的技术栈有些可能都不是很熟悉。所以文档的目标是让用户能够快速上手，而不是让用户能够深入了解。\nLearn by example 提供demo是快速上手的有效方式。\n从最简单的例子开始，让用户能够快速上手。比如一个最简单的http …","date":"2024-07-30","keywords":["technical writing","writing","documentation"],"permalink":"/posts/different-doc-types/","tags":["writing","misc"],"title":"关于科技写作-不同的文档类型"},{"content":"Background My homelab is a cluster of 2 nodes.It works well.\nAnd I have 3 vps(2 oracle free tier arm, 1 other) and want to set up a new microk8s cluster. But I encouter some network issues.\nThe ingress can\u0026amp;rsquo;t forward traffic to other pods. It doesn\u0026amp;rsquo;t happen on my homelab.\nSolution I keep only one node in the cluster and check the network.\nkubectl create deployment pingtest --image=busybox --replicas=2 -- sleep infinity kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE …","date":"2024-07-29","keywords":["microk8s","firewall-cmd","calico"],"permalink":"/posts/network-issues-about-microk8s-on-my-vps/","tags":["microk8s","misc"],"title":"Network issues about microk8s on my vps"},{"content":"Go server application(GAS) platform is an internal development platform.\nIt shields developers from changes in various basic SDK versions and infrastructures, allowing them to focus on business logic development and improving their development efficiency.\nI mainly participated in the development of the prototype project and was responsible for the design and development of the control center.\n","date":"2024-07-25","keywords":null,"permalink":"/portfolio/shopee/go_application_server/","tags":null,"title":"Go Application Server"},{"content":"Spkit Cli is a scaffolding tool for go application.\nIt generates a Go project with a unified layout and provides some common capabilities(e.g. unified config, observability) for development. Developers can use internal CI/CD and service mesh out of the box.\nI primarily participated in the development and maintenance of the unify config module which provides a unified interface for both local and remote configurations.\n","date":"2024-07-25","keywords":null,"permalink":"/portfolio/shopee/spkit/","tags":null,"title":"Spkit \u0026 Spkit Cli"},{"content":"The purpose of the Go Governance project is to mitigate the risks associated with upgrading Go versions and to assist Go projects in upgrading to the latest Go version.\nI developed this tool to perform static analysis on project code and provide users with relevant solutions.\n","date":"2024-07-22","keywords":null,"permalink":"/portfolio/shopee/go_governance/","tags":null,"title":"Go Governance"},{"content":"Prerequisite Microk8s Cloudflare Tunnel NFS Server 为什么要将homelab迁移到kubernetes 我的homelab由2个机器组成，一个是老的dell laptop(4核心8线程， 20GB内存, 256GB ssd)，一个是专门用来存储的nas(4核心4线程， 4GB内存, 7TB hdd)。\n原先的架构如下图所示。所有的服务都是通过docker的方式部署的。dell laptop主机挂载nas的数据存储盘，并通过cloudflare tunnel对公网提供访问。\n我希望尽量将服务部署在cpu和内存都比较强大的dell laptop上，但是如果dell laptop最终故障了，nas服务也可以继续运行必要的服务。同样的，如果nas服务故障了，dell laptop也可以继续运行必要的服务。\n迁移到k8s之后，也可以尝试gitops。\n根据手上的资源，迁移到k8s之后，可以通过以下方式实现上诉需求：\n将node打上标签，通过preferredDuringSchedulingIgnoredDuringExecution将pod分配 …","date":"2024-07-10","keywords":["homelab","kubernetes","cloudflare tunnel","nfs","storage","mayastor"],"permalink":"/posts/homelab-docker-to-k8s/","tags":["homelab","cloudflare tunnel","kubernetes","storage"],"title":"将homelab从docker迁移到kubernetes"},{"content":"原文链接 Java 21 Virtual Threads - Dude, Where’s My Lock? 这篇文章是关于Java21的虚拟线程的，主要讨论了一个关于虚拟线程的故障案例。\n问题 Netflix接到了jvm间歇性超时，实例被挂起的问题报告。这些报告的共同点是都是运行着java21的spring boot 3应用，且都使用嵌入式的tomcat来提供服务。这些jvm虽然仍然在运行，但是确无法提供服务。\nNetflix内部应该还有java17的技术栈服务，所以可以将以上信息作为问题的公共特征抽取出来。\n它们的metrics检测到closewait状态的socket数量持续增加。\n排查 收集信息 closewait造成的原因是连接的对端关闭了连接，但是本地还未关闭，还在等待关闭。者通常意味着本地线程可能因为某种原因挂起了。所以他们先要排查了thread dump。\nNetflix的服务有定期保存thread dump数据 他们在告警系统中找到一个这类被挂起的实例，并查看对应的thread dump数据。从这些数据，只能看出该jvm处于空闲状态，并没有特别的活动。\n他们查看了这些服 …","date":"2024-07-01","keywords":["netflix","java21","springboot3","spring boot 3","troubleshooting"],"permalink":"/reading/netflix/java21-virtual-threads-where-is-my-lock/","tags":["netflix","java21","springboot3","troubleshooting"],"title":"Java21虚拟线程-锁在哪里呢？"},{"content":"Swiftpass builds the aggregated payment system that integrates various payment methods(e.g. Alipay, WeChat Pay) into a single interface.\nThis payment system is primarily divided into two parts. The public cloud service handles online transactions. The private cloud services are deployed within institutions with clearing qualifications, responsible for reconciliation, clearing, and settlement.\nThe whole system is based on spring + java.\nI was responsible for the clearing and settlement modules of …","date":"2024-05-25","keywords":null,"permalink":"/portfolio/swiftpass/clearing_settlement_system/","tags":null,"title":"Clearing and Settlement System"},{"content":"I help some e-commerce sellers set up a unified e-commerce management system to support different e-commerce platforms such as taobao, aliexpress, mercari etc.\nThe management system is based on JHipster(angular+springboot) and uses Puppeteer and Appium for web and app automation and information scraping, respectively.\n","date":"2024-05-25","keywords":null,"permalink":"/portfolio/freelancer/app_integration/","tags":null,"title":"Unified Ecommerce Management System"},{"content":"Go1.21新增了maps包,该包提供了map相关的工具函数。\n相关issue: https://github.com/golang/go/issues/57436\nmaps pkg中提供了部分相关的example。\n删除 可参考\nmaps.DeleteFunc 内部的处理机制是遍历元素，满足条件，则调用delete方法删除。\n比较 可参考\nmaps.EqualFunc m1 := map[int]string{ 1: \u0026amp;#34;one\u0026amp;#34;, 10: \u0026amp;#34;Ten\u0026amp;#34;, 1000: \u0026amp;#34;THOUSAND\u0026amp;#34;, } m2 := map[int][]byte{ 1: []byte(\u0026amp;#34;One\u0026amp;#34;), 10: []byte(\u0026amp;#34;Ten\u0026amp;#34;), 1000: []byte(\u0026amp;#34;Thousand\u0026amp;#34;), } eq := maps.EqualFunc(m1, m2, func(v1 string, v2 []byte) bool { return strings.EqualFold(v1, string(v2)) }) // 比 …","date":"2023-11-05","keywords":["go1.21","stdlib","map","maps","golang maps package"],"permalink":"/posts/golang-1.21-maps/","tags":["golang","go1.21"],"title":"Golang1.21-maps package"},{"content":"Go1.21新增了slices包,该包提供了slice相关的工具函数。\n相关issue: https://github.com/golang/go/issues/57433\nslices pkg中提供了部分相关的example。\n二分查找 可参考\nslices.BinarySearch slices.BinarySearchFunc 压缩 可参考\nslices.Compact slices.CompactFunc 需要注意的是，这里的压缩是针对相邻的元素，如果相等的元素间隔排列，则不会被压缩。\nfmt.Println(\u0026amp;#34;slices.Compact\u0026amp;#34;) seq := []int{0, 1, 1, 2, 1, 3, 5, 8} fmt.Printf(\u0026amp;#34;%v len(seq)=%d\\n\u0026amp;#34;, seq, len(seq)) seq = slices.Compact(seq) fmt.Printf(\u0026amp;#34;%v len(seq)=%d\\n\u0026amp;#34;, seq, len(seq)) // output // [0 1 1 2 1 3 5 8] len(seq)=8 …","date":"2023-11-05","keywords":["go1.21","stdlib","slice","slices","golang slices package"],"permalink":"/posts/golang-1.21-slices/","tags":["golang","go1.21"],"title":"Golang1.21-slices package"},{"content":"向前兼容 简单来说，就是老版本的编译器构建新的go版本写的代码。比如用Go1.20的编译器构建Go1.21的代码。\n在Go1.21之前，老版本的编译器是否可以构建新版本Go的代码，取决于Go模块的代码是否使用了新版本Go的语言特性。\n例如，Go1.18才release的泛型特性，如果代码中使用了，那么go1.17的编译器就无法编译。 但是当前代码中如果没有使用go1.18的泛型特性，那么go1.17的编译器编译出来的程序的行为就和go1.18一样了吗？如果编译通过了，但是相同代码的结果却不一样，那么这就是向前兼容性的问题，而且更隐蔽，更难以发现。\n例如，Go1.18默认移除了使用[sha1-hash的x509证书的支持](https://tip.golang.org/doc/go1.18#sha1)，虽然可以使用GODEBUG=x509sha1=1环境变量，保持默认行为和go1.18之前版本一致，但是默认行为其实是变更了。 为了提高向前兼容性，Go1.21现在会读取go.work或者go.mod的go line(注意不是toolchain line)严格限制编译器Go版本的最低要求： …","date":"2023-11-04","keywords":["go1.21","forward compatibility","向前兼容","toolchain","GOTOOLCHAIN"],"permalink":"/posts/golang-1.21-forward-compatibility/","tags":["golang","go1.21"],"title":"Golang1.21兼容性问题-向前兼容"},{"content":"向后兼容 兼容性指的是源码的兼容，当你更新Go版本之后，需要重新编译，我们可以添加新的API，但是不应该破坏现有代码的使用。Go 1 and the Future of Go Programs对Go1的兼容性进行了描述，以及不保证兼容性的多个例外场景。\n简单来说，就是老版本的Go代码可以使用新版本的Go工具链正确地编译和运行。\n保持兼容性的主要手段 Golang保持兼容性的主要手段有两个方式：API检查和测试。\nAPI检查 Go使用一个工具维护每个package export出来的API\napi/ (master) $ ls except.txt go1.13.txt go1.17.txt go1.20.txt go1.4.txt go1.8.txt README go1.10.txt go1.14.txt go1.18.txt go1.21.txt go1.5.txt go1.9.txt go1.11.txt go1.15.txt go1.19.txt go1.2.txt go1.6.txt go1.txt go1.12.txt go1.16.txt go1.1.txt …","date":"2023-11-04","keywords":["go1.21","backward compatibility","向后兼容","GODEBUG","golang compatibility"],"permalink":"/posts/golang-1.21-backward-compatibility/","tags":["golang","go1.21"],"title":"Golang1.21兼容性问题-向后兼容"},{"content":"新增内置函数 min和max 相关issue：https://github.com/golang/go/issues/59488\n在builtin.go中的函数定义如下(实际的实现在runtime中,通过编译器的配合实现调用)：\n// The max built-in function returns the largest value of a fixed number of // arguments of [cmp.Ordered] types. There must be at least one argument. // If T is a floating-point type and any of the arguments are NaNs, // max will return NaN. func max[T cmp.Ordered](x T, y ...T) T // The min built-in function returns the smallest value of a fixed number of // arguments of …","date":"2023-11-03","keywords":["go1.21","golang 1.21","min","max","clear","语言特性","内置函数"],"permalink":"/posts/golang-1.21-language-changes/","tags":["golang","go1.21"],"title":"Golang1.21语言特性更新"},{"content":"官方说明 https://go.dev/doc/go1.21 官方说明比较抽象，以下通过例子来说明实际的变化。\n部分初始化的泛型函数(Partially instantiated generic functions) 如以下示例，IsEven是个泛型函数，可以作为非泛型函数Any和泛型函数AnyGeneric的参数。\nfunc IsEven[T int | int8 | int16](n T) bool { return n%2 == 0 } func Any(numbers []int, fn func(int) bool) bool { for _, v := range numbers { if fn(v) { return true } } return false } func AnyGeneric[T any](numbers []T, fn func(T) bool) bool { for _, v := range numbers { if fn(v) { return true } } return false } 在Go1.21中，我们可以这些调用\n// This …","date":"2023-11-02","keywords":["golang","go1.21","generics","type inference","泛型","类型推断"],"permalink":"/posts/golang-1.21-generics-type-inference/","tags":["golang","go1.21","generics"],"title":"Golang1.21类型推断增强"},{"content":"原文链接 Deconstructing Type Parameters — go.dev/blog 笔记 slices.Clone函数很简单，它可以拷贝任何类型slice。\nfunc Clone[S ~[]E, E any](s S) S { return append(s[:0:0], s...) } s[:0:0]分配了一个新的底层数组，来容纳s的值。这篇文章主要是探讨为什么这个泛型函数的方法签名是这么定义的。\n简单的Clone方法 如果我们拷贝一个泛型的slice，可能会怎么写？\nfunc Clone1[E any](s []E) []E{ // 忽略方法体 } 该泛型函数Clone1有一个单独的类型参数E。它的输入是一个E的slice，输出是一个E的slice，而E代表任何类型。看过去很直观。\n但是有个问题Named slice虽然在Go中比较少用，但是确认有人这么写。\n// 以下是string的slice，带有个方法String type MySlice []string func (s MySlice) String() string { return …","date":"2023-11-01","keywords":["go","type parameter","generics","类型参数","泛型","golang generics"],"permalink":"/reading/golang/deconstructing-type-parameters/","tags":["golang","generics"],"title":"解构Golang类型参数"},{"content":"在Go1.21之前，package初始化并没有明确地规定，在1.21中明确了package初始化的顺序。\n相关issue： https://github.com/golang/go/issues/57411 可能的影响 如果某些项目如果依赖于包的初始化顺序，如上面issue中的例子，如果packagea的init()依赖了packageb中init()初始化的环境变量，那么package初始化顺序的变更可能会造成影响。\n一个受影响的issue例子\nhttps://github.com/golang/go/issues/63500 包初始化顺序的算法 对所有import的包进行排序，放在一个列表中 重复以下步骤，直到所有包都被初始化 找到第一个所有依赖的import包都已经被初始化的包 初始化该包，并从列表中移除 图示如下, 被初始化的包用绿色标记 demo code\n其他参考 https://go.dev/doc/go1.21 ","date":"2023-10-30","keywords":["go1.21","package init","初始化顺序","golang init","package initialization"],"permalink":"/posts/golang-1.21-pkg-init/","tags":["golang","go1.21"],"title":"Golang1.21的package初始化顺序变更"},{"content":"简介 相关issue: https://github.com/golang/go/issues/53435#issuecomment-1191752789\n增加了两个接口\n// 匿名接口 实现见 https://go-review.googlesource.com/c/go/+/432898/11/src/errors/wrap.go interface{ Unwrap() []error } // 实现见 https://go-review.googlesource.com/c/go/+/432898/11/src/errors/join.go func Join(errs ...error) error 增强了fmt.Errorf和函数语义，可以支持多个%w了，实现见https://go-review.googlesource.com/c/go/+/432898/11/src/fmt/errors.go 。\nDemo Code demo链接\nfunc main() { err0 := errors.New(\u0026amp;#34;err0\u0026amp;#34;) wErr0 := …","date":"2023-10-13","keywords":["go1.20","multi errors","errors.Join","fmt.Errorf","错误包装"],"permalink":"/posts/golang-1.20-stdlibs-multi-errors/","tags":["golang","go1.20"],"title":"Golang1.20新特性 multi errors"},{"content":"简介 这是一个新增package，而且还是实验特性。默认情况下，无法使用该package，需要通过GOEXPERIMENT=arenas开启，GOEXPERIMENT=arenas go build .\n相关issue: https://github.com/golang/go/issues/51317\n虽然代码合并了，但为了表示官方不支持，且不推荐在生产环境使用，没有包含在最终的Go1.20的release note中，见详情\nThe arena text in the release notes makes them seem more official and supported than they really are, and we\u0026amp;rsquo;ve already seen a couple blog posts based on that erroneous belief. Delete the text to try to set expectations better.\nGo标准垃圾回收的作业流程，基本是垃圾回收器对所有对象进行跟踪标记(STW)，如果没有使用，那么在 …","date":"2023-10-11","keywords":["go1.20","arena","内存管理","memory management","GOEXPERIMENT"],"permalink":"/posts/golang-1.20-stdlibs-arena/","tags":["golang","go1.20"],"title":"Golang 1.20-arena内存管理"},{"content":"语言特性 允许将slice转为array 相关的issue: https://github.com/golang/go/issues/46505\n// slice to array s := []int{1, 2, 3, 4, 5} fmt.Printf(\u0026amp;#34;%v %p\\n\u0026amp;#34;, s, \u0026amp;amp;s) arr := [5]int(s) // Go1.19 cannot convert s (variable of type []int) to type [5]int fmt.Printf(\u0026amp;#34;%v %p\\n\u0026amp;#34;, arr, \u0026amp;amp;arr) unsafe包增加了三个新的函数 这些函数提供了完整的能力来构建和销毁slice和string值，而不依赖于their exact representation.\nSliceData String StringData func main() { var arr = [5]byte{\u0026amp;#39;a\u0026amp;#39;, \u0026amp;#39;b\u0026amp;#39;, \u0026amp;#39;c\u0026amp;#39;, \u0026amp;#39;d\u0026amp;#39;, \u0026amp;#39;e\u0026amp;#39;} s := …","date":"2023-10-01","keywords":["go1.20","golang 1.20","slice to array","comparable","语言特性"],"permalink":"/posts/golang-1.20-language-changes/","tags":["golang","go1.20"],"title":"Golang 1.20语言特性更新"},{"content":"About Me Hi, I\u0026amp;rsquo;m Huang Meirong (Matthew).\nI am a backend engineer who loves using technology to solve problems and make life easier. My interests include coding, self-hosting homelab services, continuous learning, and exploring new AI tools.\nCurrently based in Singapore. Previously lived in: Shenzhen, Fuzhou, Xiamen, Wuhan, Putian.\nThe Website I built this space to organize my technical knowledge, document complex problem-solving processes, and share my experience with programming, …","date":"0001-01-01","keywords":["about me"],"permalink":"/about/","tags":["about"],"title":"About"},{"content":"Meirong Huang 🔗 LinkedIn: linkedin.com/in/mr-huang\n💻 GitHub: github.com/meirongdev\n📝 Blog: meirong.dev\n📝 Summary Java/Golang Backend/Fullstack Developer | SaaS Platform | In-House Development Tool | Aggregated Payment System | Management System\n🛠️ Skills 💼 Experience Vanguard / Leader Software Engineer / November 2024 – Present Skills: Java21+, WebFlux, Spring Boot3+, Kubernetes, OceanBase, Redis, Kafka, ArgoCD, Terraform\nLeading the development of a cloud-native saas platform, focusing on high …","date":"0001-01-01","keywords":["resume",""],"permalink":"/resume/","tags":["",""],"title":"Resume"},{"content":" Spring Boot Actuator ","date":"0001-01-01","keywords":["talk","presentation"],"permalink":"/talk/","tags":["talk","presentation"],"title":"Talk"},{"content":"以下是我未来两周的空闲时间段。如果你想约一次沟通或技术讨论，直接发邮件说明你方便的时间和想聊的内容即可。\n正在加载日程... 未来两周暂无可预约时间段。 源码 ","date":"0001-01-01","keywords":["预约","会议","timeslot"],"permalink":"/timeslot/","tags":[],"title":"预约时间"}]