微服务不是「把服务拆碎」,而是一种围绕业务能力组织的分布式系统工程。Clojure 在微服务上有独特优势:不可变与纯函数让服务边界清晰、REPL 驱动让单服务开发高效、小而快的启动(尤其 GraalVM native 化后)适合容器编排。本文从拆分原则讲到落地,覆盖服务发现、API 网关、配置、观测、部署与分布式事务,帮助你用 Clojure 搭建一套可运维的微服务架构。
1. 拆分原则:先想清楚要不要拆
1.1 什么时候该拆
| 信号 | 说明 |
|---|---|
| 组织通信成本高 | 团队按 Conway 定律需要独立边界 |
| 独立扩缩容 | 不同模块负载差异大 |
| 独立发布 | 高频变更与低频变更耦合 |
| 技术异构 | 确实需要不同技术栈 |
反模式:为了「微服务」而微服务。一个团队维护的、负载均匀的中型应用,模块化单体(modular monolith)更划算——微服务引入的分布式复杂度是真实成本。
1.2 按业务能力拆
订单服务 / 支付服务 / 库存服务 / 用户服务 / 营销服务
边界判断:能否独立修改、独立部署、独立失败而不影响其他服务。
2. 服务发现与注册
2.1 两种模式
| 模式 | 机制 | 适用 |
|---|---|---|
| 客户端发现 | 客户端从注册中心拉取实例列表 | 需自建负载均衡 |
| 服务端发现 | 网关/路由器发现并转发 | 网关集中管理 |
2.2 注册中心选型
Clojure 服务接入注册中心通常用 Java 生态方案:
| 注册中心 | 特性 | Clojure 集成 |
|---|---|---|
| Consul | DNS/HTTP、健康检查 | clj-consul / 直接 HTTP API |
| etcd | 键值 + 租约 | HTTP API |
| ZooKeeper | 经典协调服务 | zookeeper 客户端 |
| Kubernetes Service | 内建服务发现 | 无需额外注册中心 |
在 K8s 里,首选「K8s Service + DNS」作为服务发现——注册中心由平台承担,业务服务只负责暴露端口与健康探针。
2.3 健康检查
;; 每个服务暴露 /healthz,供探针与注册中心轮询
(defn health-handler [_]
{:status 200 :body (json/write-str {:status "ok" :ts (System/currentTimeMillis)})})
;; 组合 Liveness(存活)与 Readiness(就绪)
(defn liveness [_] {:status 200 :body "alive"})
(defn readiness [deps]
(fn [_]
(if (db/ping (:db deps)) {:status 200} {:status 503})))
3. API 网关与 BFF
3.1 网关职责
| 职责 | 说明 |
|---|---|
| 路由 | 按路径转发到下游服务 |
| 鉴权 | 统一认证、校验 JWT |
| 限流 | 全局与按用户的速率限制 |
| 聚合 | BFF 模式下聚合多个服务响应 |
| 协议转换 | REST ↔ 内部 RPC/事件 |
3.2 用 reitit + Ring 搭建轻量网关
;; 网关:解析 JWT → 路由转发 → 聚合
(defn auth-middleware [handler]
(fn [req]
(if-let [claims (jwt/verify (:token req))]
(handler (assoc req :claims claims))
{:status 401 :body "unauthorized"})))
(def gateway-app
(-> (reitit/router
[["/api/orders/**" {:middleware [auth-middleware] :get (fn [req] (forward-to "order-svc" req))}]
["/api/users/**" {:middleware [auth-middleware] :get (fn [req] (forward-to "user-svc" req))}]])
(reitit/ring-handler)))
4. 配置管理与环境隔离
4.1 12-Factor 配置
配置要与代码分离,通过环境变量或配置中心注入:
;; 集中读取配置:环境变量覆盖默认值
(def config
(memoize
(fn []
{:db-host (or (System/getenv "DB_HOST") "localhost")
:db-port (or (System/getenv "DB_PORT") "5432")
:redis (or (System/getenv "REDIS_URL") "redis://localhost")
:mq (or (System/getenv "MQ_URL") "kafka://localhost:9092")})))
(defn get-config [] (config))
4.2 环境矩阵
本地 REPL(dev)→ CI 测试(test)→ 预发(staging)→ 生产(prod)
每个环境:独立配置源 + 独立密钥(Secret 管理)
密钥用 System/getenv 注入(K8s Secret),绝不进代码库。
5. 可观测性:日志、指标、追踪
5.1 结构化日志
;; 结构化日志(JSON),便于集中采集
(require '[jsonista.core :as json])
(defn log [level event ctx]
(println (json/write-value-as-string
{:level level :event event :ts (System/currentTimeMillis)
:service (:service env) :ctx ctx})))
5.2 指标
用 Prometheus 客户端暴露 /metrics:
(require '[prometheus.clj :as prom])
(def orders-counter (prom/counter :orders_total "累计订单数"))
(def latency-hist (prom/histogram :order_latency_seconds "订单耗时"))
(defn record-order! []
(prom/inc orders-counter)
(prom/time! latency-hist (do (place-order!) ...)))
5.3 分布式追踪
跨服务调用用 OpenTelemetry(可观测性专题 专题有 OTel 深入),链路 ID 贯穿:
;; 注入/透传 trace context,跨服务串联
(defn with-trace [ctx f]
(let [span (ot/inject ctx)]
(try (f) (finally (ot/finish span)))))
6. 容器化与 K8s 部署
6.1 镜像构建
;; deps.edn + tools.build 打 uberjar
(defn build-uber [_]
(uberjar {:basis (clojure/create-basis {})
:main 'my.service.core}))
;; 极小镜像(配合 GraalVM native-image 可到几十 MB)
FROM gcr.io/distroless/java17-debian11:nonroot
COPY target/app.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
6.2 K8s 部署清单
apiVersion: apps/v1
kind: Deployment
metadata: { name: order-svc }
spec:
replicas: 3
selector: { matchLabels: { app: order-svc } }
template:
metadata: { labels: { app: order-svc } }
spec:
containers:
- name: app
image: registry/order-svc:1.2.0
ports: [{ containerPort: 8080 }]
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
livenessProbe:
httpGet: { path: /livez, port: 8080 }
envFrom: [{ secretRef: { name: app-secrets } }]
---
apiVersion: v1
kind: Service
metadata: { name: order-svc }
spec:
selector: { matchLabels: { app: order-svc } }
ports: [{ port: 80, targetPort: 8080 }]
部署要点:就绪探针 + 存活探针分开、滚动更新、优雅停机(
hk/run-server加 shutdown hook)——参见 /clojure-modern-web-stack/ 的容器化部分。
7. 分布式事务:事件驱动与 Saga
7.1 微服务下没有「本地事务」
跨服务的数据一致性不能靠数据库事务,常用两种方案:
| 方案 | 机制 | 适用 |
|---|---|---|
| Saga | 链式本地事务 + 补偿 | 长流程业务 |
| 事件驱动 + 最终一致 | 发事件、异步处理、对账 | 可容忍延迟一致 |
7.2 Saga 编排
;; 订单 Saga:下单 → 扣库存 → 支付,任一失败走补偿
(def steps
[{:name :create-order :do create-order! :compensate cancel-order!}
{:name :reserve-stock :do reserve-stock! :compensate release-stock!}
{:name :charge :do charge! :compensate refund!}])
(defn run-saga [steps ctx]
(loop [steps steps, ctx ctx, done []]
(if-let [step (first steps)]
(let [res (try {:ok ((:do step) ctx)} (catch Exception e {:err e}))]
(if (:ok res)
(recur (rest steps) (:ok res) (conj done step))
;; 失败 → 逆序补偿
(doseq [s (reverse done)] ((:compensate s) ctx))
res))
{:ok ctx})))
8. 服务间通信选型速查
| 场景 | 选择 |
|---|---|
| 同步请求-响应 | gRPC(内部)/ REST(对外) |
| 异步解耦 | Kafka / RabbitMQ 事件 |
| 实时推送 | WebSocket / SSE |
| 批量数据 | 消息队列 + 消费组 |
同步调用会产生「级联故障」,异步事件则带来「可观测性缺口」——选型要在一致性与复杂度间权衡。可参考 /clojure-data-pipeline/ 的事件接入。
9. 常见陷阱
| 陷阱 | 现象 | 规避 |
|---|---|---|
| 同步调用链过长 | 接口 RT 累加 | 聚合层并行 + 异步化 |
| 每个服务一套配置 | 环境漂移 | 统一配置中心/模板 |
| 无追踪 | 排查靠猜 | OTel 全链路追踪 |
| 探针不分 liveness/readiness | 滚动更新中断 | 分开两种探针 |
| Saga 无补偿幂等 | 重复扣款 | 补偿操作幂等化 |
10. 总结
Clojure 微服务架构的落地路径:先想清楚要不要拆(多数场景模块化单体更划算),拆则按业务能力定边界;服务发现与网关负责「找得到、进得去、管得住」;配置与密钥走环境注入;观测三支柱(日志/指标/追踪)缺一不可;部署用 K8s + 就绪/存活探针;跨服务一致性用 Saga 与事件最终一致,所有写操作与补偿都要幂等。微服务的复杂度是真实成本,Clojure 的简洁与 REPL 效率只是帮你把复杂度花在刀刃上。
延伸阅读
- Clojure 现代 Web 全栈开发 — 单个服务的骨架与部署
- Clojure 网络服务深入 — 服务间通信的具体实现
- Clojure 并发设计模式 — core.async 在网关/消息中的并发
- 系统架构专题 — 微服务架构模式全景
- 可观测性专题 — 可观测性三支柱工程
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。