news 2026/10/1 4:19:13

K8s落地CI/CD流水线:从架构设计到故障排查全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
K8s落地CI/CD流水线:从架构设计到故障排查全记录

最近把团队的CI/CD流水线整体迁移到了K8s上,整个交付节奏从原来的一周一发变成了想发就发,发布这件事也从“高危操作”变成了日常操作。做这套东西之前我也犹豫过,项目本身不算大,非要上一套K8s流水线是不是有点小题大做。跑完第一个版本之后我确认了:这套改造不只是把构建部署的脚本换了换,而是把整个交付的思路从头到尾理了一遍。这篇文章把我整个落地过程、设计取舍和踩过的坑完整记录下来,给准备在K8s上做CI/CD的同行一个参考。篇幅不短,基本上从架构选择讲到具体命令,重点是解释清楚每个环节的“为什么”,而不是单纯贴配置。

1. 先从设计说起:为什么CI/CD跑在K8s上才是最终形态

1.1 传统交付流程到底卡在哪里

先说一个我亲身经历的典型场景。好几年前做项目交付,流程大概是这样的:开发本地编译打包,然后通过跳板机拷到服务器上,关掉旧进程,启动新进程,整个过程中还有人在群里喊“大家先别提交”“快发版了都别动”。这套模式最大的问题不是慢,而是不确定——本地环境、测试环境、生产环境,三套环境存在各种微妙差异,代码没变但运行结果变了,排查起来极其痛苦。

即使后来引入了Jenkins,把“编译-打包-拷贝-重启”自动化了一部分,也只是把脚本串起来而已。一旦换了机器,或者依赖版本变动,构建出来的产物可能就直接失真。回滚就更麻烦了,没有一套机制告诉你上次可用版本是什么、对应的配置是什么,全靠翻命令历史。

这些痛点在容器化时代被放大了,同时也有了解决方案。镜像一旦构建出来,就是不可变交付物——代码、运行时、依赖、配置全都打在镜像里,测试环境跑的是它,生产环境跑的还是它,环境差异从根上被抹掉了。但光把应用打包成镜像还不够,你得有一套机制把这些镜像稳定、可靠、可回滚地发布到运行环境里,这就是K8s要解决的第二个问题。

1.2 K8s把“部署”变成了“声明”

K8s给我的最大感受是,它把传统运维中“黑话式”的操作流程变成了“声明式”的状态管理。以前你要告诉运维“帮我把包放到这台机器上,然后执行启动脚本”,现在你只需要描述“我希望这个应用最终是3个副本,跑最新镜像,端口8080”,剩下的调度、重启、伸缩、健康检查,K8s自己搞定。

这跟CI/CD结合之后,威力非常大。你想滚动更新,Deployment自带RollingUpdate策略;你想回滚,一条kubectl rollout undo就回到上一个版本;你想扩容,改一下replicas或者配个HPA就完了。而且这些“声明”本身都是YAML文本,可以放进Git仓库,于是整个发布历史变成了可审计、可追溯的Git记录。

很多人觉得K8s门槛高,我觉得门槛不在K8s本身,而在思维转换——从“如何执行某个动作”变成“如何描述期望状态”。一旦接受这个思路,后面做CI/CD会流畅很多。

1.3 工具选型对比与我的选择

关于CI/CD工具链,圈子里的方案五花八门。我把常见几套对比一下,这张表花了不少时间整理,几乎每个方案我都实际用过或至少做过PoC验证。

工具典型定位优点需要注意的地方
Jenkins传统CI服务器生态庞大、插件多、老团队容易上手维护成本和系统复杂度偏高,新项目用起来有点重
GitLab CI与GitLab深度集成配置简单、Pipeline自定义程度高、内置镜像仓库部分高级功能需要收费版
Tekton云原生CI/CD框架完全跑在K8s上、CRD方式定义流水线、支持可复用任务学习曲线比较陡,生态还在成长
Argo CDGitOps部署工具把Git仓库作为唯一事实来源,自动同步部署状态只管CD不管CI,需要配合一套CI工具一起用
Harbor镜像仓库支持镜像漏洞扫描、权限分级、镜像复制本身不是CI工具,是配合整个链路的基础设施

我个人最终的组合是:GitLab CI + Kaniko + K8s原生Deployment发布,后期引入Argo CD做GitOps。选GitLab CI的原因很直接——团队代码本来就在GitLab上,少维护一套Jenkins就少一堆事。Runner配Kubernetes executor后,每个Job本质上就是一个Pod,CI任务跟K8s结合得最紧密,资源调度、并发数这些都能交给集群处理。

2. 拆解流水线关键环节:从镜像构建到滚动更新的原理

2.1 镜像构建:kaniko为什么比DinD更适合在K8s上跑

在K8s环境里跑CI,第一个绕不开的问题是:镜像怎么构建?最“惯性”的做法是Docker-in-Docker,也就是在构建容器里再装一个Docker Daemon。听起来简单,实际用起来问题不少——特权模式会让安全边界变成摆设,Daemon崩溃可能导致所有并发构建互相影响,镜像缓存管理也是一团乱。

所以我在K8s上做镜像构建时用的是kaniko。kaniko是Google开源的工具,它的运行方式跟平常的docker build不太一样,不需要Daemon,直接解析Dockerfile里的每一条指令,在用户态把镜像层解包、执行、再打包。作为Pod里的一个普通容器进程,它不需要特权,不会污染宿主机,很适合K8s这种隔离环境。

下面是我常用的一个多阶段构建Dockerfile,以Spring Boot服务为例:

# 第一阶段:编译 FROM maven:3.8-openjdk-11 AS builder WORKDIR /src COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 第二阶段:运行 FROM openjdk:11-jre-slim RUN useradd --create-home appuser USER appuser WORKDIR /app COPY --from=builder /src/target/demo-0.0.1-SNAPSHOT.jar ./app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

第一阶段装依赖、编译、出包,第二阶段只把最终产物放进去。这样阶段一用的Maven镜像比较重也没关系,阶段二精简到极致,生产镜像可以控制在几十MB。这里还有个容易忽略的点——RUN useradd那一步。生产容器如果默认root启动,安全扫描大概率会有告警,后续排查问题也会很痛苦,还是一开始就建个低权限用户比较稳。

在GitLab CI里配合kaniko的实际执行命令是这样:

build-image: stage: build image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

2.2 镜像版本管理:tag设计决定可回滚性

镜像构建出来之后,第一个要命的决策是tag怎么打。我看到很多团队一上来就爱用latest,这样确实省事,但很快就会发现两个严重问题:一是没法表达“我部署的是哪个代码版本”,二是镜像仓库里的latest被反复覆盖,本地缓存跟线上镜像不一致,出问题后拿着latest根本无法回滚。

我的做法是tag尽量跟代码提交绑定。GitLab CI里自带CI_COMMIT_SHORT_SHA这个变量,拿它当镜像tag就是最直观的做法——每个commit都能找到对应的镜像,回滚时改一下tag重新部署就完事。如果还要区分正式环境以及测试环境,可以在后面拼一层环境标识,比如demo-app:effa6c1-prod,demo-app:effa6c1-staging。

这里特别想强调一下:如果团队里有多个人维护流水线,tag命名规范一定要先定下来,白纸黑字写到流程文档里。我见过太多线上事故是因为某个人手动打了一个不规范的tag,结果部署出错后所有人都不知道这个镜像是哪来的。规范这东西不能靠人自觉,得靠系统约束。

2.3 部署更新三阶段:kubectl、Helm、GitOps

镜像推送到仓库后,就到了部署环节。部署方式选择其实是跟团队的成熟度挂钩的,不需要一步到位,但方向要想清楚。

最开始可以只用kubectl set image。这个命令在CI Job里执行一行就完成镜像升级:

kubectl -n demo set image deployment/demo-app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

如果应用只有一个或少数几个Deployment,这种直白的方式足够用了。但随着应用变多,配置开始重复,values、环境差异、版本管理这些问题冒出来,就需要引入Helm。Helm把一组K8s资源打成一个chart包,通过参数化渲染不同环境的部署配置,升级和回滚也都有版本记录。

再往后走,就到了GitOps模式。GitOps的核心思想是“Git仓库里保存的YAML就是环境的唯一事实来源”,Argo CD这类工具会持续监控Git仓库,一旦仓库里的内容有变化,就自动把线上环境调整成跟仓库一致的状态。CI流水线只负责构建镜像和更新Git仓库里的tag,部署动作完全交给Argo CD完成。这种模式的好处是权限边界特别清晰——开发者可以改应用代码,但不一定有直接操作生产集群的权限,所有变更先过Git,审核流程自然就有了。

2.4 滚动更新参数的计算逻辑

Deployment的滚动更新策略里有两个关键参数:maxSurge和maxUnavailable。这两个参数看着不起眼,实际发布安全和发布速度全看它们怎么配。

说一个最常见的例子。假设一个服务有4个副本,滚动更新默认参数是maxSurge=25%、maxUnavailable=25%。这意味着发布过程中,允许最多比期望副本数多25%(也就是多1个新Pod),同时允许最多有25%的Pod处于不可用状态。整个发布过程里,旧Pod逐步减少,新Pod逐步增加,集群里Pod总数的变动范围在3到5之间。在任意时刻,至少有3个Pod在正常服务,不会出现全部副本同时重建的情况。

如果服务对可用性要求特别高,可以把maxUnavailable调成0,这意味着发布期间旧Pod一个都不提前销毁,只允许新增Pod,等新Pod通过就绪检查后再轮流摘掉旧的。代价是发布过程中需要两倍Pod资源量,发布周期也会长一些,但服务始终是满血状态。

这里必须要配合就绪探针readinessProbe才有意义。没有就绪探针的话,K8s无法判断新Pod是不是真的能接流量,滚动更新可能只是形式上的“起了一圈新Pod”,实际服务入口还在旧Pod上,发布失败但是滚动状态还是显示成功。我自己踩过这个坑,那次排查花了大半天,最后发现就是少写了一个readinessProbe。

3. 完整实操:搭建一套能跑的K8s CI/CD流水线

3.1 环境准备:K3s + GitLab Runner的快速起法

搭建这套环境,我不建议一开始就上多节点生产集群。自己学习或者小团队实验的话,K3s是最适合的起点。K3s是CNCF认证的轻量Kubernetes发行版,二进制小、内存占用低,API跟原生K8s完全兼容,一台2C4G的机器就能跑得很流畅。后续要切到正式环境的多节点集群,流水线配置基本不需要改。

安装K3s非常简单,一条命令就完成了:

curl -sfL https://get.k3s.io | sh -

装完后执行kubectl get nodes确认节点状态,你会发现Node已经Ready了。这一步验证过了,说明集群基础是好的,不用像传统K8s那样装完还要折腾CNI插件和kubelet配置。

还有一套GitLab服务,最简单的做法是用Docker Compose在另一台机器上跑一个轻量的GitLab实例。这里有个经验之谈:GitLab是个内存大户,跑起来至少需要4G内存,如果机器内存不够的话,建议直接用gitlab/gitlab-ce镜像把CI功能关掉一部分再跑。不是生产环境的话,把Prometheus监控这些耗时耗内存的组件关掉,体验会好很多。

3.2 配置Runner走Kubernetes executor

Runner的安装方式很多,但要让CI/CD真正跑在K8s上,关键是让Runner使用Kubernetes executor。所谓Kubernetes executor,是指Runner每次接收到Job之后,不会在本机起一个进程来执行,而是去K8s集群里创建一个Pod,Job在这个Pod里运行,执行完就销毁。每个Job相互隔离,不会有环境残留,并发跑几十个Job也只是集群资源的事。

安装Runner我是直接用Docker跑一个gitlab-runner容器,然后注册到GitLab实例。注册后的Runner配置文件通常在/etc/gitlab-runner/config.toml里,open这个文件,把executor改成kubernetes:

[[runners]] name = "k8s-runner" url = "https://gitlab.example.com/" token = "你的注册token" executor = "kubernetes" [runners.kubernetes] namespace = "ci-cd" service_account = "gitlab-runner" image = "docker:20.10"

这一段配置里,namespace指定了Job Pod会被创建到哪个命名空间,service_account指定了Job Pod使用哪个服务账号。权限最小化原则下,这个ServiceAccount只需要能操作必要的资源就行,下面会专门写。

3.3 为部署授权:ServiceAccount与RBAC

Runner在K8s集群里要干活,总得有授权。最安全的做法是给它最小权限的ServiceAccount,不要一上来就cluster-admin。以下是一份比较推荐的RBAC配置,只允许操作指定命名空间内的Deployment和Pod:

apiVersion: v1 kind: ServiceAccount metadata: name: gitlab-runner namespace: ci-cd --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: deployment-manager namespace: ci-cd rules: - apiGroups: ["apps"] resources: ["deployments", "deployments/scale"] verbs: ["get", "list", "watch", "create", "update", "patch"] - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] - apiGroups: ["extensions", "networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch", "create", "update", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: deployment-manager-binding namespace: ci-cd subjects: - kind: ServiceAccount name: gitlab-runner namespace: ci-cd roleRef: kind: Role name: deployment-manager apiGroup: rbac.authorization.k8s.io

一个常见的坑是,Runner配置了ServiceAccount但是没绑定权限,Job启动后调用kubectl一直报forbidden。这时候不要急着给cluster-admin,先看RoleBinding的namespace对不对——很多权限问题最后查下来就是Role建对了,RoleBinding却建到了别的namespace。

3.4 编写流水线与落地一次发布

前置条件都准备好之后,核心的.gitlab-ci.yml长这样。我这里把构建、测试、推送、部署四个阶段都放进去,并把关键参数写清楚:

stages: - build - test - package - deploy variables: KUBE_NAMESPACE: ci-cd APP_NAME: demo-app build: stage: build image: maven:3.8-openjdk-11 script: - mvn compile test: stage: test image: maven:3.8-openjdk-11 script: - mvn test artifacts: when: always reports: junit: target/surefire-reports/TEST-*.xml package: stage: package image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA deploy: stage: deploy image: bitnami/kubectl:latest script: - kubectl -n $KUBE_NAMESPACE set image deployment/$APP_NAME app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - kubectl -n $KUBE_NAMESPACE rollout status deployment/$APP_NAME

这是最简单、最直白的一套流水线,跑通一次从提交代码到上线新版本的完整链路。你在GitLab上提交一个MR或者直接push到主干,Pipeline就会依次执行。执行过程中可以在GitLab的Pipeline页面看到每个Job的实时日志,哪一步挂了直接就能定位到。

部署应用前需要先把Service、Deployment这些基础资源定义好,比如Deployment的滚动更新参数和健康检查就按下面的格式写在manifests里:

apiVersion: apps/v1 kind: Deployment metadata: name: demo-app namespace: ci-cd spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: app image: registry.example.com/demo-app:latest ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 15 periodSeconds: 20 resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi

这里maxUnavailable设成0、maxSurge设成1,是保可用性优先的配置——发布过程中始终有3个旧Pod在提供服务,新Pod最多提前起来1个,只有它ready了才继续往下滚动。

发布完成后用一下命令查看状态,确认Pod全部就绪:

kubectl -n ci-cd get pods -l app=demo-app kubectl -n ci-cd rollout status deployment/demo-app

如果新版本有问题,一条命令回到上个版本:

kubectl -n ci-cd rollout undo deployment/demo-app

4. 故障排查实录:K8s CI/CD最常见的坑与解法

4.1 控制平面初始化失败

很多第一次自己搭K8s集群的朋友都会遇到一个问题:master节点初始化之后卡住,输入kubeadm init后来一句类似“the apiserver is not healthy after 4m”的报错。这句话翻译过来是:kubelet已经启动了,但是控制平面的apiserver容器长时间没有通过健康检查。

这种问题九成出在三个地方。第一是swap没关,kubelet对swap很敏感,kubeadm初始化之前必须swapoff -a,并且把/etc/fstab里的swap条目注释掉。第二是kubelet和容器运行时的cgroup驱动不一致,kubelet默认用systemd,容器运行时如果用了cgroupfs,两者配合不起来,apiserver就起不来。第三是网络插件没就位,CNI不装的话,apiserver容器即使起来了也无法正常工作,可以用kubectl get pods -n kube-system看看有没有Pending的Pod。

排查顺序建议这样:先看kubelet状态,systemctl status kubelet,再看日志journalctl -u kubelet -f,最后确认为基础环境问题。查完再init一次,通常问题就好解决。

4.2 Pod常见异常与排查思路

流水线跑起来之后,大部分时间都是在跟Pod的异常状态打交道。我把平时遇到最多的几种异常整理成一个速查表,方便遇到问题直接对照。

异常状态典型原因排查方向
Pending资源不足、节点亲和性不匹配、PVC未绑定kubectl describe pod看事件,查request是否超节点容量
CrashLoopBackOff启动命令错误、健康检查失败、依赖服务没就绪看容器日志、检查启动参数、确认探针路径
ImagePullBackOff镜像仓库地址错、imagePullSecret失效、私有仓库认证问题重启Pod之前先手动docker pull一遍,看报错明细
Running但无法访问Service selector不对、端口映射错、Pod健康但Endpoint没更新kubectl describe service,看Endpoints列表
OOMKilledlimits内存设置过低、Java应用堆内存超限调大limits,同时优化JVM参数
CreateContainerConfigErrorconfigmap或者secret引用不存在kubectl describe pod会显示具体缺失对象

逐个说一下我的排查心法。遇到Pod状态异常,第一步永远是kubectl describe pod,而不是kubectl logs。describe输出里最后一段Events几乎把原因写完了——调度失败会告诉你insufficient cpu/memory,镜像拉取失败会告诉你具体错误,探针失败会告诉你健康检查哪个环节挂了。99%的Pod问题都能从Events里找到线索,真正需要看容器日志的场景反而是少数。

关于readinessProbe和livenessProbe,这里有个特别容易搞混的细节。readinessProbe失败只会导致Pod从Service Endpoints中摘除,不会被杀掉;livenessProbe失败才会触发容器重启。如果你看到Pod一直重启,先检查livenessProbe检查的接口是否真的返回200,以及initialDelaySeconds是否给足。有些应用启动慢,2秒容器都没起来就执行健康检查,那就会被K8s无限重启,形成死循环。

4.3 镜像与权限问题

镜像拉取失败绝对是高频坑。特别是接了私有镜像仓库之后,很容易遇到ImagePullBackOff。我遇到过几次,主要原因都是Deployment模板里忘了配置imagePullSecrets。K8s默认不会把每个Secret都拿去向镜像仓库做认证,必须显式声明:

spec: template: spec: imagePullSecrets: - name: registry-secret

创建这个secret也有固定格式:

kubectl -n ci-cd create secret docker-registry registry-secret \ --docker-server=registry.example.com \ --docker-username=xxx \ --docker-password=xxx

需要注意的是,secret是namespace级别的。如果生产、测试、开发各占一个namespace,每个namespace都要创建对应secret,否则换个namespace部署就拉不到镜像。

权限问题的另一个常见场景是RBAC配置。Runner的ServiceAccount权限给少了,Job调用kubectl执行部署就会报forbidden。我建议在部署阶段使用独立ServiceAccount,并明确授予对应namespace的Deployment操作权限,而不是把整个集群的管理员权限都交出去。

4.4 实战总结与经验心得

整套流水线跑通之后,我自己复盘了几条心得,可能对刚准备接手这套体系的朋友更有价值。

第一条,镜像tag永远不要用latest。这条我必须再强调一次。任何“图省事”的tag方案最后都会变成事故的起点,找不到对应代码、没法回滚、缓存不一致,全是latest惹的祸。

第二条,流水线脚本里的凭证不要明文放在.gitlab-ci.yml里,全部通过CI/CD的Secret变量注入。仓库里一旦出现了真实密码,撤销和重新分发凭证的代价可比配置变量的成本高得多。

第三条,CI和CD要区分清楚。CI负责代码到镜像的过程,包括编译、测试、扫描;CD负责镜像到运行环境的过程,包括部署、滚动、回滚。很多人把两者揉在一起,导致只要代码一提交就直接上生产,这是非常危险的行为。至少要加一个环境校验或审核步骤,让发布到生产环境前有一道人工确认关卡。

第四条,把Argo CD当成下一阶段目标。这套方案用kubectl set image已经很顺手,但遇到多集群部署、需要细粒度权限管理、想要更好的审计能力时,GitOps模式的优势会逐渐体现出来。把期望状态放进Git仓库,Argo CD自动调谐,发布行为完全可追溯,这才是K8s上CI/CD的最终形态。

另外如果你想把Operator模式也引入进来,这条路会更有意思——很多通用发布场景可以封装成自定义控制器,让熟悉状态调谐逻辑的程序帮你管理应用生命周期。不过这是另一个话题了,先把流水线基础和故障排查能力打扎实,后续再往深了走会更从容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:19:12

论文AI率太高?亲测有效的4个改写指令与3个人工技巧

先说个背景:这半年我帮不少朋友看过论文,几乎每个人都在愁一件事——“论文AI率太高”。学校查AIGC检测,一拉数据就是60%、80%,甚至90%的都有。明明是自己一个字一个字改过的,但检测工具就是铁着脸说你有AI痕迹。更离谱…

作者头像 李华
网站建设 2026/10/1 4:18:54

Logisim启动报错‘requires JRE 1.5.0’的真相与三种根治方案

1. 这个报错不是Java版本太低,而是Logisim在“假装”需要旧版JRE 第一次看到 "This application requires a Java Runtime Environment 1.5.0" 这个弹窗时,我下意识点开Java官网准备下载JDK 1.5——结果发现连Oracle官网都早已下架了这个20…

作者头像 李华
网站建设 2026/10/1 4:18:30

Carla自动驾驶仿真平台从零上手:安装、配置与常见运行错误详解

1. Carla到底是什么:它在自动驾驶仿真里处于什么位置有朋友问我,Carla到底怎么跑起来?我的第一反应总是反问一句:你跑Carla是想解决什么问题?因为同样一个Carla,有人拿它做感知算法验证,有人拿它…

作者头像 李华
网站建设 2026/10/1 4:18:24

Jev架构解析:Laya与QwenRLCD的硬件感知与业务驱动reward设计

1. 项目概述:从Jev爆火现象切入,直击Laya与QwenRLCD开源实现的本质差异最近两周,技术圈里“Jev”这个词几乎刷屏——不是某个新出的消费级AI产品,也不是某家大厂发布的闭源模型,而是一个在极短时间内被大量开发者自发复…

作者头像 李华
网站建设 2026/10/1 4:17:26

云服务器Linux选型:Ubuntu、Rocky、Debian稳定与维护对比

上个月帮一个朋友排查他的云服务器,2C4G的配置,跑着Ubuntu 22.04 LTS,结果磁盘被 /var/lib/snapd 怼满了,什么服务都写不进去。后来一问才知道,他根本没装什么大型软件,就是 snap 后台自动刷了一堆运行时。…

作者头像 李华
网站建设 2026/10/1 4:17:23

JDK 11 安装配置全指南:企业级稳定环境搭建与多版本共存

1. 为什么现在还要专门讲 JDK 11?不是早该用 JDK 17 或 JDK 21 了吗?JDK 11 是 Java 发展史上一个极其特殊的存在——它不是“过渡版本”,而是第一个长期支持版(LTS)中真正被企业大规模落地的“分水岭”。我从 2018 年…

作者头像 李华