K8S 1.29 部署 Nexus 3:StatefulSet、存储与探针避坑
Nexus 这套制品仓库,我前后在 K8s 上搭过好几回,每次都会在同样几个地方卡住:Pod 起来了但 Ready 一直是 0/1、日志里刷 Permission denied、上传一个几百兆的 Docker 镜像被网关 413 拦掉。这些问题单看都不复杂,可凑在一起就很容易让人怀疑是“云原生环境不兼容 Nexus”。实际情况恰恰相反——Nexus 在 Kubernetes 1.29 上跑得很稳,只是它是一套有状态、单写、重磁盘、慢启动的 Java 应用,用部署无状态服务的思路去套它,必然出问题。
这篇内容要解决的就是这件事:把 Nexus Repository 3 按正确姿势落到 K8S 1.29 集群上。适合两类人看——一类是刚开始接手内部制品仓库、想自己搭一套 Maven/npm/Docker 私服的开发者;另一类是已经在集群里跑着 Nexus、但总被 OOMKilled 或者探针误杀搞得不得安宁的运维同学。我会从“为什么这么选”讲到“YAML 每一行在干什么”,再到几个必踩坑的完整排查链路,尽量把话说到能直接抄作业的程度。
2. Nexus 在交付链路里的真实位置,决定了它不能当无状态服务部署
2.1 制品仓库卡住的到底是哪个环节
很多人对 Nexus 的印象还停留在“存 Maven jar 的地方”,实际上它现在承担的角色要重得多。一条典型的构建流水线里,它至少同时扮演三个身份:上游代理(把中央仓库、npm registry、Docker Hub 的产物缓存到内网,避免每次构建都出去拉)、内部发布点(团队自己的 SNAPSHOT、release 包、基础镜像版本都往这里推)、审计与合规的落点(哪个版本的构件被谁在什么时候下载过,出了供应链问题要能追溯)。
这三个身份带来的负载特征完全不同。代理身份是读多写少但突发极强,一次 CI 全量构建可能瞬间打进来几百个并发 GET;发布点是写少但对一致性极敏感,写坏了整个团队的构建都会挂;审计身份是长期留存,数据只增不减。把这三者压在一套 Nexus 上,意味着它的磁盘会持续膨胀、内存里要缓存大量元数据、并且任何一个写操作都不能丢。
这就解释了一个很常见的误判:有人看 Nexus 的 Pod “能跑起来”,就觉得它是个普通 Web 应用,随手用 Deployment + replicas: 2 部署。结果两个副本同时挂同一块盘,几天后数据库文件损坏,仓库直接打不开。Nexus 3 的元数据存储是嵌入式数据库,它没有为多实例并发写做任何设计,多副本共享存储等于自杀。
2.2 单写、重磁盘、慢启动:三个特征对应三套配置
我把 Nexus 的运行时特征拆成三条,每条都直接决定一个 K8s 配置项,这个映射关系理清了,后面的 YAML 就不用死记。
| 运行时特征 | 具体表现 | 对应的 K8s 配置 |
|---|---|---|
| 单写 | 元数据库不允许并发写,多副本必损坏 | StatefulSet + replicas: 1,禁止 HPA |
| 重磁盘 | blob store 随构件线性增长,IO 敏感 | PVC + 独立 StorageClass,容量预留 3 倍冗余 |
| 慢启动 | JVM 冷启动 + 元数据加载,首次可达 3~8 分钟 | startupProbe 而非 livenessProbe 兜底 |
“单写”这条最容易被违反,因为 K8s 的默认心智就是“副本越多越可靠”。但对 Nexus 来说,可靠性来自数据不损坏,不是来自副本数。真要高可用,正确做法是在前面挂只读镜像/缓存层,或者做仓库级的主备切换,而不是让两个 Pod 抢一块盘。
“慢启动”这条则是探针误杀的重灾区。Nexus 首次启动时 JVM 要加载大量类、初始化嵌入式数据库、扫描 blob store 目录建立索引,这个过程在数据量大的仓库上能拖到好几分钟。如果你只配了 livenessProbe,initialDelaySeconds 给个 60 秒,kubelet 会在第 60 秒开始探测,探不通就杀容器,容器重启后又要从头加载一遍——你会看到一个无限重启的循环,日志里却没有任何错误,因为每次都是被外部杀掉的。
2.3 K8S 1.29 这个版本号带来的实际差异
版本号不是随便写的,1.29 有几个变化会实打实影响部署。第一,flowcontrol.apiserver.k8s.io/v1beta2这个 API 在 1.29 中被移除,如果你打算用某个第三方 Helm chart 或者自研 Operator 来装 Nexus,而它依赖的 CRD 里还写着 v1beta2,kubectl apply会直接报 no matches for kind。上线前先kubectl api-resources | grep flowcontrol确认一下,比事后排查省事得多。
第二,LegacyServiceAccountTokenCleanUp在 1.29 默认开启,长时间未使用的 Secret 型 ServiceAccount token 会被自动清理。这条本身是好事,但如果你的 CI 流水线是靠在 Nexus 旁边挂一个长期 token 去做免密推送的,得确认那个 token 不是靠自动生成的 Secret 撑着的,否则某天早上构建突然 401。
第三,ReadWriteOncePod(RWOP)在 1.29 里已经是默认可用的 beta 能力。这个对我们特别有用——它能从准入层面保证一块 PVC 在同一时刻只被一个 Pod 挂载,等于给“禁止多副本”这条规则上了一把硬锁。下面讲到存储的时候我会展开。
需要说明的是,这些版本特性以官方 release notes 为准,我这里讲的是它对我们这套部署产生的具体影响,具体小版本可能还有微调,落地前扫一眼升级说明总没错。
3. 存储与状态的组合:StatefulSet、PVC 和那块绕不开的盘
3.1 为什么 Deployment + emptyDir 是错误答案
先说清楚错误答案长什么样,因为它是新手最自然的直觉。用 Deployment,数据目录挂 emptyDir,Pod 重建数据就没了——这个太明显,一般没人这么干。稍微“讲究”一点的版本是挂 hostPath 或者 NFS 的 subPath,看起来数据保住了,但埋了三个雷。
第一个雷是调度漂移。hostPath 绑定的是节点本地路径,Pod 被调度到另一台节点,路径就不存在了,或者更糟——存在但是空的,Nexus 会当成全新实例初始化,你的仓库“消失”了。要避免就得加 nodeSelector 钉死节点,那又牺牲了调度灵活性,节点故障时 Pod 起不来。
第二个雷是权限。Nexus 容器里以 UID 200 运行,hostPath 目录默认属主是 root,容器起来第一件事就是往/nexus-data写文件,直接 Permission denied。
第三个雷是扩容时的数据分裂。Deployment 滚动更新时新旧 Pod 可能短暂并存,两个进程同时操作同一个数据目录,这比多副本还危险。
StatefulSet 解决的正是“身份稳定”这件事:它给 Pod 一个稳定的序号(nexus-0)、稳定的网络标识、以及通过volumeClaimTemplates自动绑定到该序号的独立 PVC。即使 Pod 被删掉重建,它还是会拿回同一块盘,数据天然连续。对单实例应用来说,StatefulSet 带来的额外复杂度几乎为零,收益却是决定性的。
3.2 存储类怎么挑,容量怎么估
存储类的选择直接决定了后面权限坑的严重程度。我的经验是分三档来看:
| 存储类型 | 适用场景 | 注意点 |
|---|---|---|
| 块存储(云盘、Ceph RBD 等) | 生产首选 | IO 稳定,支持快照,扩容方便 |
| NFS / 网络文件系统 | 中小团队、测试环境 | fsGroup 行为不一致,需 initContainer 兜底 |
| 本地盘 / local-path | 单节点或边缘场景 | 无副本,节点挂了数据就没了 |
块存储是生产环境的默认答案,原因不光是性能。它通常配套 VolumeSnapshot 能力,备份的时候一个 snapshot 就能拿到一致性数据,比你在容器里 tar 半天靠谱得多。NFS 的优势是多个 Pod 都能挂,但对 Nexus 这种单写应用,“能多挂”反而是风险。本地盘只建议在明确接受丢失风险的环境里用。
容量估算我给一个粗略公式,实测偏差不大:
- Maven 私服:单构件平均 200KB~2MB,一个中型团队一年新增约 20~50GB(含 SNAPSHOT 历史版本)
- npm 代理:单个包平均几 MB,热门包会被反复缓存,一年 10~30GB
- Docker 镜像仓库:这是大头,单个镜像层几十到几百 MB,一个 50 人的团队一年轻松吃掉 200GB~1TB
- 元数据与索引:按数据量的 5%~10% 预留
加起来之后,至少再乘 1.5 到 2 的系数。原因很实际:Nexus 的 blob store 在删除构件后不会立刻回收空间,需要手动跑 compact 任务;升级前你要留出快照空间;万一哪次大批量导入把盘打满,PVC 在线扩容虽然能做,但很多 CSI 扩容后有文件系统层面的等待期,业务会中断。宁可一开始申请大一点,云盘扩容容易,缩容几乎不可能。
3.3 用 RWOP 给“禁止多副本”上一把锁
前面说了 Nexus 不能多副本,但“知道不能”和“系统保证不能”是两回事。团队里总有新同学看到replicas: 1想改成 3 试试,或者写个 HPA 想让它弹性伸缩。这时候如果只是靠文档约束,早晚出事。
1.29 里的ReadWriteOncePod访问模式就是干这个的。把 PVC 的accessModes从ReadWriteOnce改成ReadWriteOncePod,语义立刻收紧:这块卷在同一时刻只能被单个 Pod 挂载,第二个 Pod 会因为卷已被占用而卡在 ContainerCreating。真要有人手贱改副本数,表现是第二个 Pod 起不来,而不是两个 Pod 一起把数据库写坏——故障从“数据损坏”降级成了“Pod Pending”,这个降级非常值。
有一点要注意:RWOP 需要 CSI 驱动支持。主流的块存储 CSI 基本都支持了,但一些老的 NFS provisioner 可能不支持,创建 PVC 时会直接报错。落地前先用一个测试 PVC 验证一下,别等到生产环境才发现。
配合 StatefulSet 的podManagementPolicy: OrderedReady(默认值)和updateStrategy: RollingUpdate,单副本场景下更新就是“先删旧的、再建新的”,中间有短暂中断,但数据绝对安全。对内部制品仓库来说,几十秒的不可用完全可以接受。
volumeClaimTemplates: - metadata: name: nexus-data spec: accessModes: ["ReadWriteOncePod"] storageClassName: csi-block-ssd resources: requests: storage: 500Gi4. 把清单写对:从命名空间到 Service 的逐字段拆解
4.1 命名空间、镜像与那层容易漏掉的权限
先把外围准备好。命名空间本身没什么好说的,但有两个细节值得单独提。
镜像标签绝对不要用 latest。Nexus 的升级是有明确路径的,跨版本升级需要看对应的 upgrade notes,有些版本对存储引擎有迁移要求。用 latest 的后果是某天 Pod 重启,顺手拉了个新版本,然后仓库起不来了。写成具体的稳定 tag,比如sonatype/nexus3:3.65.0,你落地时换成当时的稳定版即可。
镜像拉取凭证这块,如果镜像放在私有 registry,记得创建imagePullSecrets。这一步本身简单,坑在于凭证过期后 Pod 重启才报错,而平时没人会去验证。建议在 CI 里加一个定时任务,每周拉一次镜像做连通性检查。
然后是安全上下文,这是第一个真正的坑点:
securityContext: runAsUser: 200 runAsGroup: 200 fsGroup: 200 fsGroupChangePolicy: OnRootMismatch为什么是 200?因为 Nexus 官方镜像里的nexus用户就是 UID 200。你在容器里ls -ln /nexus-data能看到属主是200 200。挂载 PVC 之后,如果卷的根目录属主是 root(大部分 provisioner 的默认行为),Nexus 进程就没法在里面建目录、写数据库文件,启动会以java.io.IOException: Permission denied或Failed to create directory收场。
fsGroup: 200是让 kubelet 在挂载前把卷的组改成 200 并赋予组读写权限。这个机制对大多数 CSI 卷有效,但对 NFS 和一些第三方 provisioner 不一定生效。fsGroupChangePolicy: OnRootMismatch的意思是“只在根目录属主不对时才递归改”,这个优化很重要——数据目录里有几十万个文件的时候,每次启动都递归 chown 会让 Pod 卡在 ContainerCreating 好几分钟,甚至超时。
如果fsGroup不生效(判断方法是看 Pod 事件里有没有failed to set fsgroup,或者直接进容器看目录属主),就得用 initContainer 兜底:
initContainers: - name: fix-permissions image: busybox:1.36 command: ["sh", "-c", "chown -R 200:200 /nexus-data && chmod 750 /nexus-data"] securityContext: runAsUser: 0 volumeMounts: - name: nexus-data mountPath: /nexus-data注意这个 initContainer 必须runAsUser: 0才能 chown,而主容器仍然是 200,两者不冲突。chown -R在大目录上很慢,所以更好的写法是只 chown 顶层目录,或者在首次部署时手动跑一次、之后注释掉。我曾经在一个 300GB 的仓库上让这个 initContainer 跑了四十分钟,Pod 一直 PreInit,看着像卡死,实际在老老实实改属主。
4.2 StatefulSet 里每个字段都在解决一个具体问题
完整的主容器定义大概是这样:
containers: - name: nexus image: sonatype/nexus3:3.65.0 ports: - name: http containerPort: 8081 - name: docker containerPort: 8082 env: - name: INSTALL4J_ADD_VM_PARAMS value: "-Xms4g -Xmx4g -XX:MaxDirectMemorySize=2g -Djava.util.prefs.userRoot=/nexus-data/javaprefs" - name: TZ value: "Asia/Shanghai" resources: requests: cpu: "1" memory: 6Gi limits: cpu: "4" memory: 10Gi startupProbe: httpGet: path: /service/rest/v1/status port: 8081 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 60 readinessProbe: httpGet: path: /service/rest/v1/status port: 8081 periodSeconds: 15 timeoutSeconds: 5 livenessProbe: httpGet: path: /service/rest/v1/status port: 8081 periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 5INSTALL4J_ADD_VM_PARAMS这个环境变量是关键。Nexus 打包用的是 install4j 启动器,它在启动时会读取nexus.vmoptions里的 JVM 参数,而这个环境变量的内容会被追加进去、并且优先级更高。为什么非要用它而不是改配置文件?因为配置文件在镜像里,改了不持久;挂 ConfigMap 覆盖又容易和版本升级时的默认值打架。用环境变量是最干净的覆盖方式。
-Djava.util.prefs.userRoot=/nexus-data/javaprefs这一项容易被漏掉。不加的话 Java 会往用户 home 目录写 preferences,而容器里 home 目录可能是只读的或者不可写的,日志里会出现一堆Couldn't flush user prefs的警告。不致命,但会污染日志,排查问题时很碍眼。
TZ环境变量是给操作审计用的。任务执行时间、日志时间戳都依赖它,默认 UTC 会让你在对照业务时间线时反复做心算。
端口这里要开两个。8081 是主 HTTP 端口,UI、REST API、Maven/npm 仓库都走它;8082 是给 Docker 仓库用的独立 connector 端口。很多人第一次配 Docker 私服时卡在这里——在 Nexus 里建了 docker hosted 仓库,connector 填了 8082,然后发现外部完全连不上,因为 Service 里根本没暴露这个端口。
4.3 Service 的两个端口与一个容易被忽略的细节
Service 定义很直白:
apiVersion: v1 kind: Service metadata: name: nexus spec: type: ClusterIP selector: app: nexus ports: - name: http port: 8081 targetPort: http - name: docker port: 8082 targetPort: docker用 ClusterIP 而不是 NodePort,是因为对外暴露交给 Ingress 做,这样 TLS、域名、访问控制都统一在一层管理。只有当 Docker 客户端必须直连端口、而又不想为它单独配域名时,才考虑 LoadBalancer。
有一个细节值得单独说:Service 的 selector 必须和 StatefulSet 的 Pod 标签严格对上。StatefulSet 的spec.selector.matchLabels是不可变的,创建后改不了;如果你先建了 Service 再改标签,会出现 Service 有 Endpoints 但连不通、或者 Endpoints 为空的情况。排查命令是kubectl get endpoints nexus -n <ns>,看到<none>就说明标签对不上,别去怀疑网络插件。
另外,Nexus 的nexus.properties里有个上下文路径配置,默认是/。如果你打算让 Nexus 跑在子路径下(比如/nexus/),要在环境变量里设NEXUS_CONTEXT。这个改动会连带影响 REST API 的健康检查路径,探针里的/service/rest/v1/status要跟着改成/nexus/service/rest/v1/status,改漏了就是无休止的重启。我的建议是能不用子路径就不用,省掉这一整串连带修改。
5. 对外提供服务这一层:Ingress、TLS 与大文件上传的三个坎
5.1 Ingress 注解的最小可用集合
Nexus 挂在 Ingress 后面,最常见的三个故障是:上传大构件 413、长时间操作 504、下载断流。这三个都能用注解解决,但注解的取值需要理解原理。
annotations: nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" nginx.ingress.kubernetes.io/proxy-buffering: "off" nginx.ingress.kubernetes.io/proxy-request-buffering: "off"proxy-body-size: "0"表示不限请求体大小。默认值通常是 1m,意味着超过 1MB 的构件上传直接被网关以 413 拒掉,而客户端的报错信息往往是含糊的 “unexpected EOF” 或者 “connection reset”,完全不提 413。这个坑我踩过不止一次,每次都要翻网关日志才能定位。
proxy-read-timeout和proxy-send-timeout拉到 1800 秒,是给大镜像推送留余量。一个几 GB 的镜像层在带宽一般的链路上传十几分钟很正常,默认 60 秒超时会在传到一半时断开,客户端报 “blob upload unknown”。
proxy-buffering: off和proxy-request-buffering: off这两项是性能优化。Nexus 传输大文件时如果用网关做缓冲,数据要先完整落到网关的临时目录再转发,等于把一份流量写了两遍盘,既慢又占空间。关掉之后是流式透传,内存占用也更可控。
还有一点:如果前面还有一层外部负载均衡或者 CDN,光改 Ingress 注解不够,那一层也有自己的 body size 和超时限制,得一起改。这种多层网关的配置不一致,是最难排查的一类问题,因为你在集群内怎么测都正常。
5.2 反代带来的 Host 头与协议头问题
Nexus 生成下载链接、重定向、以及某些 API 的返回体里,会带上它认为自己所在的外部地址。如果它以为自己是http://nexus.default.svc:8081,那返回给浏览器的链接就是内网地址,用户点开就 404 或者打不开。
解决办法有两个层面。Ingress 侧要正确传递头部:
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"Nexus 侧则要在仓库配置里指定 “Force base URL”。具体位置在 Repository 的 HTTP connector 配置里,填上对外的完整地址(比如https://nexus.example.internal)。这个配置是仓库级的,Docker 仓库和 Maven 仓库要分别设,漏一个就会出现“Maven 能拉、Docker 拉不了”的诡异现象。
另外,如果 Ingress 做了 TLS 终止(绝大多数情况都是),要确保传给后端的X-Forwarded-Proto是https。Nexus 在生成绝对 URL 时会用这个头判断协议,头不对就会生成 http 链接,浏览器在 https 页面里加载 http 资源会被拦,表现是页面能打开但样式和脚本全丢。
5.3 Docker 仓库接入的正确姿势
Docker 仓库这块值得单独展开,因为它是新手最容易卡住的地方。
第一个问题是端口独立性。Nexus 里创建 Docker 仓库时,connector 的 HTTP 端口必须和主端口 8081 不同,通常是 8082。这意味着你的 Service 必须暴露 8082,前面已经说过。但更深一层的问题是:Docker 客户端访问的是/v2/开头的 API 路径,它需要能直接路由到这个 connector 端口。
有两种接法。接法一是用 Ingress 按域名分流,把registry.example.internal的所有请求转发到 Service 的 8082 端口。这种方式走标准的 443 + TLS,客户端不需要任何额外配置,是最干净的。
接法二是直接用 NodePort 或 LoadBalancer 暴露 8082。这时候客户端连的是IP:8082,走的是明文 HTTP,需要在 Docker 的daemon.json里把这个地址加进insecure-registries。这种方式配置简单,但明文传输在生产环境里不合适,只建议在测试环境用。
第二个问题是路径前缀。Docker 仓库如果你在 Nexus 里给它配了docker-hosted这样的名字,客户端推送时的地址是registry.example.internal/docker-hosted/镜像名:tag,那个仓库名是路径的一部分。很多人以为只是给仓库起个显示名,结果推的时候不带这个前缀,报 “repository not found”。
第三个问题是认证。Docker 客户端和 Nexus 的认证交互走的是 token 流程,如果 Ingress 层开了某些缓存或者对Authorization头做了处理,会出现登录成功但推送 401 的情况。排查方法是直接在集群内用curl -v打一遍/v2/接口,看返回的WWW-Authenticate头是否正常透传。
6. 三个必踩坑的完整排查链路
6.1 权限:fsGroup 生效与否的判定方法
Pod 起不来的时候,第一件事是看日志:
kubectl logs nexus-0 -n nexus --previous如果看到java.io.IOException: Permission denied或者Failed to create directory: /nexus-data/...,基本可以锁定是权限问题。但别急着加 initContainer,先确认 fsGroup 到底生效了没有:
kubectl exec -it nexus-0 -n nexus -- ls -ln /nexus-data如果输出里属主是0 0(root),说明 fsGroup 没生效;如果是200 200但还是写不进去,那问题在别处——可能是卷被挂成了只读,或者 SELinux 标签不对。
还有一种情况是卷的根目录权限对了,但子目录不对。这通常发生在你之前用 root 跑过容器、或者在别的环境里往这块盘写过数据。这种情况下fsGroup只在挂载时改根目录,不会递归处理里面已有的内容,必须用 initContainer 递归 chown 一次。
排查顺序我习惯这样走:先看 Pod 事件(kubectl describe pod的 Events 段),有没有failed to set fsgroup或者mount failed;再看容器日志确认是不是到了写文件阶段才失败;最后 exec 进去看实际属主。三步做完基本就能定位,不用瞎猜。
6.2 内存:JVM 参数和容器 limit 的错配
Nexus 被 OOMKilled 的场景,十有八九是 JVM 堆参数和容器 memory limit 没对齐。
具体机制是这样的:Nexus 镜像里nexus.vmoptions默认写的是-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m。2703 这个数字看着像随手填的,但它是固定值,不会跟着容器 limit 自动调整(老版本镜像里有-XX:+UseCGroupMemoryLimitForHeap会读 cgroup,但新版 JVM 的行为变了,依赖它很不可靠)。
也就是说,如果你给容器 limit 设了 4Gi,JVM 认为堆能用 2.7G、直接内存还能再要 2.7G,加上元空间、线程栈、GC 自身的开销,很容易突破 4Gi。容器一超限,内核直接 kill,kubectl describe pod里显示Reason: OOMKilled,日志可能干净得什么都没有,因为进程是被 SIGKILL 干掉的,来不及写任何东西。
正确的配比是让 JVM 的堆 + 直接内存大约占 limit 的 60%~70%:
| 使用场景 | 容器 limit | -Xmx | MaxDirectMemory | 备注 |
|---|---|---|---|---|
| 个人/小团队试用 | 4Gi | 2g | 1g | 仓库数量控制在 10 个以内 |
| 10~50 人团队 | 8Gi | 4g | 2g | 常规配置,覆盖面最广 |
| 百人以上、多格式仓库 | 16Gi | 8g | 4g | 建议同时加 PVC 快照 |
| 超大仓库(TB 级) | 32Gi | 16g | 6g | 需要关注 GC 停顿 |
另外,requests.memory不要设得太贴近 limit。设成 limit 的一半左右比较合适,既能保证调度时有足够空间,又不会因为预留过多导致节点装不下别的服务。
有一个反直觉的点:CPU limit 设太低也会导致 OOM 假象。Nexus 启动时如果 CPU 被限流得厉害,JVM 的 GC 线程跑不动,垃圾回收跟不上分配速度,堆会迅速涨满然后 OOM。所以 CPU limit 别设成 0.5 这种数字,给到 2~4 核比较稳妥。我见过一个案例,把 CPU limit 从 0.5 调到 4,OOMKilled 就自己消失了,内存参数一个没改。
6.3 探针:Pod 一直 Running 但 Ready 是 0/1
这个现象最迷惑人,因为 Pod 状态看着挺好,就是不接流量。
先分清楚是哪一类。如果kubectl get pod显示Running但READY是0/1,且RESTARTS是 0,说明容器在跑但 readinessProbe 没通过;如果RESTARTS一直在涨,那是 livenessProbe 在杀它。这两种情况的原因完全不同。
第一种情况的排查路径:先kubectl describe pod看 readinessProbe 的失败事件,通常会带具体错误。如果报connection refused,说明端口没监听——Nexus 还在启动过程中,只是 startupProbe 已经过了(startupProbe 过了之后 readinessProbe 才开始生效,但如果 startupProbe 的 failureThreshold 给得太小,可能在 Nexus 完成初始化之前就误判为“启动成功”)。如果报404,说明路径不对,检查是不是改了NEXUS_CONTEXT。
第二种情况的典型表现是无限重启。这时候要看kubectl logs --previous,但很可能什么都没有——因为容器是被 kubelet 杀的,不是自己崩的。判断方法是看 Pod 的 Events 里有没有Liveness probe failed加Killing container。有的话就是探针超时太短。
我的经验参数是:startupProbe的failureThreshold × periodSeconds总时长给到10 分钟以上(60 × 10 = 600 秒,加上 initialDelay 30 秒,总共 10 分半),livenessProbe的timeoutSeconds给到 10 秒、failureThreshold给到 5,这样单次抖动不会触发重启。readiness 可以稍微激进一点,因为它只影响接不接流量,不影响进程存活。
还有一个细节:健康检查用/service/rest/v1/status比用/好。访问首页会触发一堆模板渲染和静态资源加载,在高负载时可能超时,而 status 接口只是个轻量的服务状态查询。不过要注意某些旧版本的 status 接口行为有差异,落地前先手动 curl 一次确认返回码是 200。
7. 跑起来之后:初始化、备份与升级的现实做法
7.1 首次登录与最小安全加固
Pod Ready 之后,第一件事是拿初始密码:
kubectl exec nexus-0 -n nexus -- cat /nexus-data/admin.password这个文件里是随机生成的 admin 初始密码。登录后系统会强制你改密码,改完这个文件会被删除。如果发现文件不存在、又登不进去,通常是因为之前初始化过一次、密码已经改过了但没人记录——这种时候只能重置数据目录(如果数据不重要)或者联系之前的管理员。建议在首次部署的 runbook 里明确写上“改完密码立即存入密码管理工具”。
加固这块,至少做三件事。第一,禁用匿名访问,Nexus 默认允许匿名读取,内部仓库如果只是想给团队用,关掉更安全。第二,给 CI 建专用账号并只授最小权限,不要用 admin 跑流水线,一旦凭证泄露影响面太大。第三,把 Realm 顺序调一下,默认的NexusAuthenticatingRealm之外如果接了 LDAP,顺序不对会导致本地账号登不进去。
还有一点容易被忽略:Nexus 的 REST API 里有些管理接口默认是可以被有读权限的账号调用的,做权限规划时要把 API 权限和 UI 权限一起考虑,别只想着“他能看见就行”。
7.2 备份:运行时直接打包数据目录是错误的做法
我见过不止一次有人在 Pod 里tar -czf backup.tar.gz /nexus-data然后拷出来当备份。这个操作在 Nexus 运行期间做,拿到的很可能是不一致的数据——数据库文件正在被写入,打包时抓到的可能是写了一半的状态,恢复的时候直接起不来。
正确做法有三档,按可靠性从高到低:
第一档是 VolumeSnapshot。如果存储类支持 CSI 快照,创建一个 VolumeSnapshot 对象就能拿到块级别的瞬时一致性快照,对业务几乎无感。恢复时从快照建新 PVC 挂上去即可。这是生产环境的首选,前提是你的 CSI 支持。
第二档是 Nexus 内置的 export 任务。在 UI 的 Tasks 里可以创建 “Export databases” 任务,它会在服务运行状态下导出一份逻辑备份。注意这导出的是数据库和配置,blob store 里的构件本体还得单独处理——通常是配合 blob store 的 rsync 或者对象存储版本控制来做。
第三档是停服冷备。把 StatefulSet 缩到 0,等 Pod 完全终止后,用一个临时的 Pod 挂载同一块 PVC 去打包,打完再恢复副本数。这种方式最笨但最可靠,适合没有快照能力的中小环境,代价是有一段时间不可用。
不管用哪一档,都要做恢复演练。备份文件躺在那里从来不验证,等于没有备份。我会建议每季度挑一个非高峰时段,把备份恢复到另一个命名空间里,确认能正常启动、能拉到构件,再删掉。这一步花不了多少时间,但能在真出事的时候救你一命。
7.3 升级的边界在哪里
StatefulSet 的默认更新策略RollingUpdate在单副本场景下就是“删旧建新”,中间会短暂中断。对内部仓库来说这个可以接受,但要提前通知使用者,别赶在月末发布日升级。
升级流程我的习惯是这样:先看一眼目标版本的 upgrade notes,确认有没有“必须先升到某个中间版本”的要求。Nexus 3 的大部分小版本可以直接跨,但涉及存储引擎变更的版本会有前置条件,跳过中间版本可能导致元数据迁移失败。这一步偷懒的代价可能是几个小时的恢复时间。
然后做一次完整备份,再改镜像 tag。StatefulSet 检测到 Pod 模板变化后会自动重建 nexus-0。这时候盯紧 startupProbe 和日志,首次启动可能会做数据迁移,耗时比平时长,别因为看着慢就手动去删 Pod。
回滚的边界要清楚:镜像可以回滚,数据不能。如果新版本已经完成了元数据迁移,把镜像切回旧版本很可能会因为数据格式不认识而起不来。所以升级前那次备份,是唯一的退路。反过来说,如果你确认了新版本运行正常,那备份可以保留一段时间再清理,别急着删。
最后分享一个我自己用了挺久的小习惯:给 Nexus 的 StatefulSet 加一个kubectl annotate记录当前版本和升级时间,比如nexus.io/deployed-version: "3.65.0"。半年后回头看,一眼就知道这个环境什么时候动过、从哪个版本来的,比翻 CI 记录快得多。这类元信息看着琐碎,但在多环境多集群的场景下,能省掉大量“这个集群到底跑的什么版本”的确认成本。