1. 项目概述与架构设计
1.1 项目背景
本文将以我的个人开源项目 Kiwi-Hub (奇异果社区) 为例,详细记录如何在一台 阿里云 2核2G 的服务器上,通过 Docker + Nginx + Nacos 成功部署一套包含三个微服务的完整系统,并实现 Swagger/Knife4j 接口文档的聚合。
Kiwi-Hub(奇异果社区)是一个基于微服务架构设计的社交内容分享平台。该项目旨在模拟主流社交媒体的核心功能,包括用户管理、内容发布与互动、以及短链接生成服务。
1.2 架构演进说明
项目仓库主要包含 main 和 nginx 两个分支。
- Main 分支:采用 Spring Cloud Gateway 作为统一网关入口,基于 Netty 响应式编程模型。
- Nginx 分支(当前部署版本):鉴于生产环境服务器配置的限制(2核 CPU,2GB 内存),Java 编写的 Spring Cloud Gateway 启动后需占用约 300MB-500MB 内存,对系统资源造成较大压力。因此,当前部署方案移除了 Spring Cloud Gateway,改用轻量级的高性能 Web 服务器 Nginx 作为反向代理网关。
1.3 微服务拆分
系统被拆分为三个核心无状态服务,通过 HTTP RESTful API 进行通信:
- kiwi-user (用户服务):负责用户注册、登录认证、个人信息管理。
- kiwi-content (内容服务):核心业务模块,负责帖子发布、评论管理、点赞逻辑。
- kiwi-link (短链服务):负责长链接到短链接的映射生成与解析跳转。
1.4 技术栈与基础设施
- 计算资源:阿里云 ECS(2 vCPU, 2 GiB RAM, CentOS Stream 10)。
- 容器化技术:Docker Engine, Docker Compose。
- 注册与配置中心:Nacos (Standalone 模式)。
- 反向代理与网关:Nginx。
- 数据存储与消息队列(SaaS 托管) :
- Redis: Upstash (Serverless Redis)。
- MongoDB: MongoDB Atlas。
- RabbitMQ: CloudAMQP。
2. 服务器基础环境配置
在低配置服务器上运行 Java 微服务集群,操作系统级别的参数调优是保障服务稳定运行的前提。
2.1 操作系统初始化与依赖安装
首先更新系统软件包索引,并安装 Docker 及 Docker Compose。
bash
# 更新 Yum 源
sudo yum update -y
# 安装必要的工具包
sudo yum install -y yum-utils device-mapper-persistent-data lvm2 git vim curl
# 添加 Docker 官方源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 安装 Docker Engine
sudo yum install -y docker-ce docker-ce-cli containerd.io
# 启动 Docker 并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
# 安装 Docker Compose (独立二进制文件方式)
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
2.2 虚拟内存(Swap)配置详解
由于服务器物理内存仅为 2GB,而三个 Spring Boot 应用加上 Nacos 的堆内存需求很容易超出此限制,导致 OOM Killer 强制杀死进程。配置 Swap 分区是防止进程崩溃的必要手段。
操作步骤:
-
创建交换文件 :使用
dd命令创建一个 2GB 的文件。if=/dev/zero表示输入源为零设备,bs=1M表示块大小为 1MB,count=2048表示块数量。bashdd if=/dev/zero of=/swapfile bs=1M count=2048 -
权限设置:出于安全考虑,Swap 文件权限必须设置为 600(仅 root 读写)。
bashchmod 600 /swapfile -
格式化与挂载:
bashmkswap /swapfile swapon /swapfile -
持久化配置 :修改
/etc/fstab文件,确保服务器重启后 Swap 依然有效。bashecho '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
2.3 Swappiness 参数调优
Linux 内核的 vm.swappiness 参数控制系统使用 Swap 的积极程度,取值范围 0-100。默认值 60 表示内存使用率达到 40% 时开始使用 Swap。在小内存机器上,建议将其调整为 100,即系统会极其积极地将非活跃内存页换出到磁盘,从而为活跃的 JVM 堆内存腾出物理空间。
bash
# 修改内核参数
sysctl vm.swappiness=100
# 写入配置文件永久生效
echo 'vm.swappiness=100' >> /etc/sysctl.conf
注:虽然 Swap 速度远慢于内存,但对于非高并发的个人项目,保证服务"不挂"比"响应极快"更重要。
3.3 "白嫖"云端中间件
如果在 2G 的服务器上再部署 Redis、MongoDB 和 RabbitMQ,服务器必死无疑。因此,我采用了**云端托管数据库(SaaS)**的方案。这不仅节省了资源,还免去了运维数据库的烦恼。
- Redis : 使用 Upstash 免费层。
- MongoDB : 使用 MongoDB Atlas M0 免费集群。
- RabbitMQ : 使用 CloudAMQP 免费实例。
配置提示 :
确保 Nacos 配置文件中,所有数据库连接地址均已指向这些云服务的公网地址,并启用了 SSL(如果服务商要求)。
3. 目录结构与数据持久化规划
规范的目录结构有助于后续的运维管理和日志排查。本项目采用 /opt/kiwihub 作为根目录,实现配置、日志、数据的分离。
text
/opt
└── kiwihub/ <-- 项目总目录
├── docker-compose.yml <-- 核心编排文件
├── .env <-- (可选) 存放密码等环境变量
├── logs <-- 日志文件
├── services/ <-- 存放微服务 JAR 包和 Dockerfile
│ ├── user/
│ │ ├── Dockerfile
│ │ └── kiwi-user.jar
│ ├── content/
│ │ ├── Dockerfile
│ │ └── kiwi-content.jar
│ └── link/
│ ├── Dockerfile
│ └── kiwi-link.jar
├── nginx/ <-- 存放 Nginx 配置
│ ├── conf/
│ │ └── nginx.conf <-- 反向代理配置
│ ├── logs/ <-- Nginx 日志映射
│ └── html/ <-- Knife4j 导航页 index.html 放这
└── data/ <-- (重要) 数据挂载目录
├── redis/ <-- Redis 数据持久化 (使用的云端数据库,实际没有)
├── mongo/ <-- MongoDB 数据持久化 (使用的云端数据库,实际没有)
└── nacos/ <-- Nacos 数据
4. 微服务镜像构建策略
为了加快传输速度并减少磁盘占用,我们放弃传统的 java -jar 方式,利用 Spring Boot 的**分层构建(Layered Jar)**特性和 Alpine 镜像。
4.1 通用 Dockerfile 模版
以下 Dockerfile 适用于所有三个微服务,通过多阶段构建(Multi-stage Build)实现环境分离。只需在构建时传入 JAR_FILE 参数即可。
dockerfile
# 第一阶段:构建/提取层 (Builder Stage)
# 使用 JRE 而不是 JDK,且使用 Alpine 版本,体积最小 (约 50MB-60MB)
FROM eclipse-temurin:17-jre-alpine AS builder
# 设置工作目录
WORKDIR /application
# 接收构建参数,指向你的 jar 包位置
ARG JAR_FILE=*.jar
# 将 jar 包复制进去并重命名
COPY ${JAR_FILE} application.jar
# 利用 Spring Boot 的 layertools 提取分层
# 这会将 jar 包拆解为依赖、Spring加载器、快照依赖、业务代码四部分
RUN java -Djarmode=layertools -jar application.jar extract
# ----------------------------------------------------------------
# 第二阶段:运行层 (Runtime Stage)
# 再次使用最小的基础镜像,丢弃第一阶段的构建工具,只保留提取后的文件
FROM eclipse-temurin:17-jre-alpine
WORKDIR /application
# 按照分层顺序复制文件 (顺序很重要!变动最少的放在最前面)
# dependencies: 第三方依赖 (最不常变,Docker 会缓存这层)
COPY --from=builder /application/dependencies/ ./
# spring-boot-loader: Spring 启动加载器
COPY --from=builder /application/spring-boot-loader/ ./
# snapshot-dependencies: 内部快照依赖 (比如 common 模块)
COPY --from=builder /application/snapshot-dependencies/ ./
# application: 业务代码 (最常变,只有这层会重新构建)
COPY --from=builder /application/application/ ./
# 设置时区为上海 (解决日志时间不对的问题)
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone
# 暴露端口
EXPOSE 8070
# 核心启动命令
# 使用 Spring 的 JarLauncher 启动,而不是 java -jar
# 这样能自动加载分层文件
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
4.2 构建优势分析
- 构建速度优化 :当只修改业务代码时,Docker 仅需重新构建最上层的
application层,底层的依赖层直接利用缓存,大幅缩短构建时间。 - 存储效率:多个微服务往往共享大部分第三方依赖(如 Spring Boot Starter),这些相同的层在磁盘上只会存储一份。
- 安全性与体积:Alpine 镜像剔除了大量非必要的系统工具,减小了攻击面,且最终镜像大小控制在 150MB 左右。(三个镜像实际构建后不足140MB)
5. Nginx 反向代理配置详解
5.1 配置文件 (kiwi.conf)
Nginx 在此架构中承担两个关键角色
- 业务路由:将请求转发到对应的微服务。
- 文档聚合:让前端能访问到后端 Knife4j/Swagger 的静态资源。
文件路径:/opt/kiwihub/nginx/conf/conf.d/kiwi.conf
nginx
server {
listen 80;
server_name kiwihub.com 120.**.***.5; # 绑定域名与公网IP
# =========================================================
# 前端导航页路由
# =========================================================
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# 前端路由支持,防止刷新 404
try_files $uri $uri/ /index.html;
}
# =========================================================
# Swagger/Knife4j 静态资源聚合逻辑
# =========================================================
# Knife4j 请求静态资源(CSS/JS/JSON)的通用路径
location ~ ^/(webjars|v3|swagger-resources) {
# 如果来源页面包含 /users/,转发到用户服务
if ($http_referer ~* "/users/") {
proxy_pass http://kiwi-user:8070;
}
# 如果来源页面包含 /links/,转发到短链服务
if ($http_referer ~* "/links/") {
proxy_pass http://kiwi-link:8030;
}
# 默认转发到内容服务(或根据需要添加更多 if 逻辑)
proxy_pass http://kiwi-content:8010;
}
# =========================================================
# 微服务反向代理配置
# =========================================================
# 用户服务代理
location /users/ {
# 使用 rewrite 去除 URL 前缀,根据后端 ContextPath 配置决定是否需要
rewrite ^/users/(.*)$ /$1 break;
proxy_pass http://kiwi-user:8070;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 短链服务代理
location /links/ {
rewrite ^/links/(.*)$ /$1 break;
proxy_pass http://kiwi-link:8030;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 内容服务代理
# 注意:/content/ 必须放在具体路径匹配之后,防止优先级冲突
location /content/ {
rewrite ^/content/(.*)$ /$1 break;
proxy_pass http://kiwi-content:8010;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.2 静态文档聚合页面 (index.html)
为了方便查看接口文档,在 /opt/kiwihub/nginx/html/ 下放置一个简单的 HTML 页面,通过 iframe 或超链接指向各服务的 Knife4j 地址,配合 Nginx 配合完成页面的路由。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>KiwiHub API 导航</title>
<style>
body { font-family: 'Segoe UI', sans-serif; text-align: center; padding-top: 50px; background-color: #f4f4f4; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 40px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
h1 { color: #333; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 30px; }
.card { padding: 20px; border: 1px solid #eee; border-radius: 8px; transition: 0.3s; text-decoration: none; color: inherit; display: block; }
.card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); border-color: #409EFF; }
.icon { font-size: 40px; margin-bottom: 10px; display: block; }
.title { font-weight: bold; font-size: 18px; color: #409EFF; }
</style>
</head>
<body>
<div class="container">
<h1>KiwiHub 微服务接口文档</h1>
<p>基于 Nginx 反向代理与 Knife4j 实现</p>
<div class="grid">
<a href="/users/doc.html" class="card" target="_blank">
<span class="title">用户服务</span>
</a>
<a href="/content/doc.html" class="card" target="_blank">
<span class="title">内容服务</span>
</a>
<a href="/links/doc.html" class="card" target="_blank">
<span class="title">短链服务</span>
</a>
</div>
</div>
</body>
</html>
6. Docker Compose 编排与资源限制
docker-compose.yml 是整个部署的核心,定义了服务间的网络依赖、环境变量注入以及关键的资源限制(Resources Limits)。
yaml
version: '3.8'
networks:
kiwi-net:
driver: bridge
services:
# ----------------------------------------------------
# Nacos 服务 (注册中心 & 配置中心)
# ----------------------------------------------------
nacos:
image: nacos/nacos-server:v2.4.1
container_name: nacos-standalone
restart: always
environment:
# 开启单机模式
- MODE=standalone
# 默认用户名/密码: nacos / nacos
- NACOS_AUTH_ENABLE=true
# 内部身份识别 Key (随意填)
- NACOS_CORE_AUTH_SERVER_IDENTITY_KEY=kiwihub_server_id
# 内部身份识别 Value (随意填)
- NACOS_CORE_AUTH_SERVER_IDENTITY_VALUE=kiwihub_server_value
# Token 密钥 (必须是 Base64 编码且足够长,否则会报错)
- NACOS_CORE_AUTH_PLUGIN_NACOS_TOKEN_SECRET_KEY=VGhpc0lzQVNlY3JldEtleUZvck5hY29zVG9rZW5WYWxpZGF0aW9u
# 极致 JVM 内存压缩
# 默认是 2G/2G,这里压到 200M/200M
- JVM_XMS=200m
- JVM_XMX=200m
- JVM_MaxDirectMemorySize=64m # 限制堆外内存
- JVM_XMN=128m
- JVM_MS=128m
- JVM_MMS=256m
ports:
# Nacos 控制台和 API 端口
- "8848:8848"
# Nacos 2.x gRPC 端口 (必须暴露,否则服务注册不上)
# 9848 = 8848 + 1000
- "9848:9848"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8848/nacos/v1/console/health/readiness"]
interval: 10s
timeout: 10s
retries: 10
start_period: 15s # 给它 15秒 的缓冲期再开始检查
# - "9849:9849" # 这个端口通常用于集群同步,单机模式可不映射到宿主机,但在容器间网络要通
volumes:
# 挂载日志,方便排错
- ./nacos/logs:/home/nacos/logs
- /opt/kiwihub/data/nacos:/home/nacos/data
deploy:
resources:
limits:
# Docker 硬限制:如果 Nacos 尝试使用超过 512M 内存,直接杀掉重启
# 保护你的 2G 服务器不被它拖垮
memory: 512M
networks:
- kiwi-net
# ----------------------------------------------------
# Nginx (作为唯一入口)
# ----------------------------------------------------
nginx:
image: nginx:alpine
container_name: kiwi-nginx
restart: always
ports:
- "80:80" # 只暴露 80 端口给公网
volumes:
# 挂载配置文件
- ./nginx/conf.d:/etc/nginx/conf.d
# 挂载静态资源(文档导航页)
- ./nginx/html:/usr/share/nginx/html
# 挂载日志
- ./nginx/logs:/var/log/nginx
environment:
- TZ=Asia/Shanghai
deploy:
resources:
limits:
memory: 64M # Nginx 非常省内存,64M 绰绰有余
networks:
- kiwi-net
# 3 个微服务
kiwi-user:
image: kiwi-user:v1
container_name: kiwi-user
restart: always
environment:
# 这里对应上面 YAML 里的变量名
- NACOS_SERVER_ADDR=nacos:8848
- SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR=nacos:8848
- SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR=nacos:8848
# 限制内存
- JVM_TOOL_OPTIONS=-Xms128m -Xmx256m
# ========== 对应spring.data.redis核心连接配置 ==========
- SPRING_DATA_REDIS_URL=rediss://default:**********@******.upstash.io:6379
- SPRING_DATA_REDIS_DATABASE=0
# ========== 对应spring.data.redis.ssl.enabled ==========
- SPRING_DATA_REDIS_SSL_ENABLED=true
# ========== 对应spring.data.redis.lettuce.pool连接池配置 ==========
- SPRING_DATA_REDIS_LETTUCE_POOL_MAX_ACTIVE=8
- SPRING_DATA_REDIS_LETTUCE_POOL_MAX_IDLE=8
- SPRING_DATA_REDIS_LETTUCE_POOL_MIN_IDLE=0
- SPRING_DATA_REDIS_LETTUCE_POOL_MAX_WAIT=-1ms
# 修正时区
- TZ=Asia/Shanghai
deploy:
resources:
limits:
# 限制容器最大使用 312M 内存
memory: 312M
networks:
- kiwi-net
depends_on:
nacos:
condition: service_healthy
kiwi-content:
image: kiwi-content:v1
container_name: kiwi-content
restart: always
environment:
# 这里对应上面 YAML 里的变量名
- NACOS_SERVER_ADDR=nacos:8848
- SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR=nacos:8848
- SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR=nacos:8848
# 限制内存
- JVM_TOOL_OPTIONS=-Xms128m -Xmx256m
# ========== 对应spring.data.redis核心连接配置 ==========
- SPRING_DATA_REDIS_URL=rediss://default:**********@******.upstash.io:6379
- SPRING_DATA_REDIS_DATABASE=0
# ========== 对应spring.data.redis.ssl.enabled ==========
- SPRING_DATA_REDIS_SSL_ENABLED=true
# ========== 对应spring.data.redis.lettuce.pool连接池配置 ==========
- SPRING_DATA_REDIS_LETTUCE_POOL_MAX_ACTIVE=8
- SPRING_DATA_REDIS_LETTUCE_POOL_MAX_IDLE=8
- SPRING_DATA_REDIS_LETTUCE_POOL_MIN_IDLE=0
- SPRING_DATA_REDIS_LETTUCE_POOL_MAX_WAIT=-1ms
# 修正时区
- TZ=Asia/Shanghai
deploy:
resources:
limits:
# 限制容器最大使用 312M 内存
memory: 312M
networks:
- kiwi-net
depends_on:
nacos:
condition: service_healthy
kiwi-link:
image: kiwi-link:v1
container_name: kiwi-link
restart: always
environment:
# 这里对应上面 YAML 里的变量名
- NACOS_SERVER_ADDR=nacos:8848
- SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR=nacos:8848
- SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR=nacos:8848
# 限制内存
- JVM_TOOL_OPTIONS=-Xms128m -Xmx256m
# 修正时区
- TZ=Asia/Shanghai
deploy:
resources:
limits:
# 限制容器最大使用 312M 内存
memory: 312M
networks:
- kiwi-net
depends_on:
nacos:
condition: service_healthy
7. 部署与运维流程
7.1 Nacos 配置管理
在启动微服务之前,必须先配置 Nacos 中的 Config Service。
-
启动 Nacos:
bashdocker-compose up -d nacos -
访问控制台 :浏览器访问
http://<服务器IP>:8848/nacos,使用默认账号密码nacos/nacos登录。 -
配置录入:
-
外部数据源配置 :
在 Yaml 配置文件中,修改数据源连接地址为云端 SaaS 服务提供的公网地址:
- MongoDB :
spring.data.mongodb.uri=mongodb+srv://user:pass@cluster.mongodb.net/kiwi - RabbitMQ :
spring.rabbitmq.host=cloud-amqp-url - Redis: 虽然 Docker Compose 环境变量已注入,但建议在 Nacos 中也保留一份作为兜底。
- MongoDB :
7.2 镜像构建与服务启动
-
上传 JAR 包到对应的
/opt/kiwihub/services/xxx/目录。 -
执行构建命令:
bashcd /opt/kiwihub/services/user docker build -t kiwi-user:v1 . cd ../content docker build -t kiwi-content:v1 . cd ../link docker build -t kiwi-link:v1 . -
全量启动:
bashcd /opt/kiwihub docker-compose up -d
7.3 验证与排查
-
查看容器状态:
bashdocker-compose ps所有状态应为
Up。如果出现Exit **,说明触发了内存限制(OOM),需检查 Swap 是否生效或进一步调低 JVM 参数。 -
查看服务日志:
bashdocker-compose logs -f kiwi-user确认日志中出现
Started UserApplication in x.xxx seconds且成功注册到 Nacos。 -
防火墙配置 :
在阿里云安全组中开放以下端口:
- 80 (Web访问)
- 8848 (Nacos控制台)
8. 成果展示与验证
部署完成后,通过浏览器访问服务器 IP。
-
首页导航:你将看到自定义的 index.html 页面,列出了三个服务的文档入口。

-
接口调试:点击任意一个服务,Nginx 会根据 Referer 自动转发,展示 Knife4j 文档。你可以直接在页面上进行接口测试(例如登录、发帖)。

-
**内存占用:**实际内存占用情况

9. 总结与心得
- 因地制宜:Spring Cloud Gateway 虽然强大,但在小内存场景下,Nginx 才是王者。
- 云端借力:不要死磕本地数据库,合理利用云厂商的 Free Tier 可以极大减轻服务器压力。
- 容器限制:在 Docker Compose 中配置 deploy.resources.limits 和 JVM 参数 (-Xmx) 是防止服务器宕机的必要手段。
- 虚拟内存:Swap 是低配服务器运行 Java 全家桶的底气。
希望这篇笔记能给同样想用低配服务器跑微服务的朋友一些参考!