Rust 异步数据库访问:连接池、超时与稳定性

背景 Rust 写后端服务时,数据库访问通常是绕不开的一层。 很多人刚开始用 sqlx 或 tokio-postgres 时,会把关注点放在: 能不能异步查询 类型映射是否方便 宏检查 SQL 是否好用 这些当然重要,但线上跑起来以后,更现实的问题通常是: 连接池应该开多大 请求等连接要等多久 数据库抖动时怎么避免把整个服务拖死 这些问题不处理好,异步只能让你“更高效地把数据库打爆”。 先建立一个基本事实 异步不是无限并发。 你的 Tokio 任务可以很多,但数据库连接永远是稀缺资源。无论是 PostgreSQL、MySQL 还是其他关系型数据库,都不可能让应用无限开连接而没有代价。 所以数据库访问的第一原则不是“尽快发查询”,而是: 连接数可控 排队时间可控 查询超时可控 以 sqlx 为例初始化连接池 use sqlx::postgres::PgPoolOptions; use std::time::Duration; async fn create_pool(database_url: &str) -> Result<sqlx::PgPool, sqlx::Error> { PgPoolOptions::new() .max_connections(32) .min_connections(4) .acquire_timeout(Duration::from_secs(2)) .idle_timeout(Duration::from_secs(300)) .max_lifetime(Duration::from_secs(1800)) .connect(database_url) .await } 这几个参数都很关键: max_connections:连接池上限 min_connections:最小保活连接数 acquire_timeout:拿连接最多等多久 max_lifetime:连接多久轮换一次 尤其是 acquire_timeout,它能防止高峰时请求无止境排队。 连接池大小不是越大越好 很多人看到连接池耗尽,就第一时间把池子调大。 这有时能缓一口气,但经常只是把压力继续往数据库推。 连接池大小应该结合几个因素来定: ...

2026年4月16日 · 2 分钟 · BvBeJ

Vue3 性能优化:从响应式细节到页面加载

背景 前端性能这件事,经常有两个极端: 一种是完全不管,页面卡了再说 另一种是上来就讲虚拟列表、SSR、代码分割,结果项目里真正拖慢页面的点根本不在那 Vue3 本身已经做了不少优化,但框架快,不代表业务代码就一定快。 真正影响体验的,通常还是这些问题: 不必要的响应式开销 大列表重复渲染 首屏加载资源过大 watch 写得太随意,副作用失控 这篇文章只聊实战里最常见、最值回票价的优化点。 先判断瓶颈在哪 优化前先确认问题类型。通常分三类: 首屏慢 JS 包太大、资源太多、接口太慢。 交互卡 某个状态变更引起大面积重渲染。 长列表卡 DOM 数量过多,滚动和 patch 开销都很高。 这三类问题的解决手段完全不同。不要把“页面卡”都归因到 Vue 响应式。 不要把所有东西都塞进 reactive 很多项目里常见这种写法: const state = reactive({ tableData: [], chartInstance: null, editor: null, wsConnection: null, filters: { keyword: '', status: 'all', }, }) 看起来统一,实际上问题不少。 像图表实例、编辑器对象、WebSocket 连接这种第三方对象,本来就不是拿来做细粒度响应式追踪的。把它们塞进深层响应式对象里,只会增加代理成本,还可能带来奇怪副作用。 更合理的拆法是: import { reactive, shallowRef, markRaw } from 'vue' const filters = reactive({ keyword: '', status: 'all', }) const tableData = shallowRef<User[]>([]) const chartInstance = shallowRef<any>(null) const editor = shallowRef<any>(null) function initChart(el: HTMLDivElement) { chartInstance.value = markRaw(createChart(el)) } 这里的思路很明确: 业务表单状态,用 reactive 大数组、外部实例,用 shallowRef 不希望被代理的对象,用 markRaw 这不是“写法偏好”,而是直接影响更新成本。 ...

2026年4月16日 · 2 分钟 · BvBeJ

Docker 多阶段构建:让你的镜像小而美

从一个真实案例说起 之前帮一个项目 Dockerize,初始镜像 800MB,每次部署慢得让人怀疑人生。后来用多阶段构建优化完,8MB,部署时间从 3 分钟变成 10 秒。 这篇文章讲讲多阶段构建的原理和实战技巧。 为什么镜像那么大 看个典型 Dockerfile: FROM golang:1.22 WORKDIR /app COPY . . RUN go mod download RUN go build -o myapp . ENTRYPOINT ["./myapp"] 问题在哪? golang:1.22 镜像本身就 800MB+,因为包含了完整的编译工具链 你的源代码、依赖缓存、编译中间产物全在里面 运行一个简单程序,凭什么需要 gcc、git、make ? 多阶段构建:原理很简单 Dockerfile 可以有多个 FROM 指令,每个阶段是独立的。前面的阶段可以复制文件到后面,最终镜像只包含最后一个阶段的内容。 # 第一阶段:构建 FROM golang:1.22 AS builder WORKDIR /app COPY . . RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o myapp . # 第二阶段:运行 FROM alpine:3.19 RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app COPY --from=builder /app/myapp . USER appuser ENTRYPOINT ["./myapp"] 关键点: ...

2026年4月15日 · 3 分钟 · BvBeJ

Rust Tokio 优雅停机:让服务真正可控

为什么要关心停机 很多人写异步服务时,把注意力都放在“怎么启动”,很少认真想“怎么结束”。 但线上真正麻烦的,往往是停机阶段: Kubernetes 滚动发布,Pod 收到 SIGTERM 服务还在处理请求,但新流量已经切走 后台任务没停干净,日志和指标都丢了 数据库连接突然断掉,导致半成功半失败 如果退出流程没有设计好,服务看起来可用,实际上很难运维。 Tokio 默认不会帮你解决一切 Tokio 的运行时很好用,但它不会自动替你处理这些问题: 谁来监听退出信号 如何通知所有任务停止 正在跑的任务是立即取消,还是等它收尾 超时之后要不要强制退出 这些都需要业务自己定义。 一个基础模型 一个比较稳妥的思路是分三步: 接收退出信号 广播 shutdown 事件 等待任务收尾,必要时超时强退 先看一个简化版结构: use tokio::signal; use tokio::sync::broadcast; use tokio::time::{timeout, Duration}; #[tokio::main] async fn main() -> anyhow::Result<()> { let (shutdown_tx, _) = broadcast::channel::<()>(16); let server_handle = tokio::spawn(run_http_server(shutdown_tx.subscribe())); let worker_handle = tokio::spawn(run_background_worker(shutdown_tx.subscribe())); signal::ctrl_c().await?; println!("received shutdown signal"); let _ = shutdown_tx.send(()); let _ = timeout(Duration::from_secs(10), async { let _ = server_handle.await; let _ = worker_handle.await; }).await; Ok(()) } 这段代码表达的核心思想很重要: ...

2026年4月15日 · 2 分钟 · BvBeJ

从零理解分布式追踪:OpenTelemetry 实战

问题是:微服务出问题了怎么办 假设你负责一个电商系统,用户下单失败,你知道问题在哪吗? 用户 → API Gateway → Order Service → Inventory Service → Database ↓ Payment Service → 第三方支付 可能出问题的点太多了:网络超时、数据库慢查询、第三方接口挂了、代码 bug… 如果没有任何追踪手段,你只能靠经验和日志去猜。 分布式追踪就是来解决这个问题的。 核心概念:三根柱子 可观测性有三根支柱:Traces(追踪)、Metrics(指标)、Logs(日志)。这里重点聊 Traces。 Trace:一次请求的完整路径 一个 Trace 由多个 Span 组成,每个 Span 代表一个操作: Trace: 用户下单流程 ├── Span: 接收 HTTP 请求 │ ├── Span: 查询库存 (Inventory Service) │ │ ├── Span: SELECT * FROM inventory WHERE sku = ? │ ├── Span: 创建订单 (Order Service) │ │ ├── Span: INSERT INTO orders ... │ ├── Span: 调用支付 (Payment Service) │ │ └── Span: POST /api/pay 每个 Span 记录: ...

2026年4月15日 · 4 分钟 · BvBeJ

写给 C++ 程序员的 Rust 入门指南

为什么要学 Rust C++ 写多了,总会遇到这些问题: 悬空指针、野指针 内存泄漏 数据竞争(data race) 未定义行为(UB) Rust 从语言层面就帮你杜绝了这些。它的所有权系统(Ownership)是核心——编译期检查,零运行时开销。 本文用 C++ 对比着讲 Rust,帮你快速上手。 基础类型:差不多 // C++ int x = 42; double pi = 3.14; bool flag = true; char c = 'A'; auto* ptr = new int(42); // Rust let x: i32 = 42; let pi: f64 = 3.14; let flag: bool = true; let c: char = 'A'; let mut ptr = Box::new(42); // 堆分配,Box 就是智能指针 关键区别: Rust 变量默认不可变,let mut x 才是可变的 i32、f64、bool 这些名字和 C++ 相似 没有裸指针,但有 *const T 和 *mut T(安全受限) 所有权:核心概念 这是 Rust 和 C++ 最核心的区别。 C++ 的问题 int* create_buffer() { int* buffer = new int[100]; // ... 处理 ... return buffer; // 谁负责释放? } void process() { int* data = create_buffer(); // 用完了 delete[] data; // 忘了 delete 就内存泄漏 } Rust 的解决方案:所有权转移 fn create_buffer() -> Vec<i32> { let buffer = vec![0; 100]; // Vec 是堆数组,类似 std::vector buffer // 所有权转移给调用方,函数结束后 buffer 不被 drop } fn process() { let data = create_buffer(); // 用完了,data 离开作用域,自动释放(drop) } // data 在这里被 drop,无需手动管理 Rust 三条规则: ...

2026年4月15日 · 5 分钟 · BvBeJ

Go 服务限流:用令牌桶保护你的 API

背景 服务刚上线时,大家最担心的是没流量。等真有流量了,新的问题马上出现: 某个接口被脚本打爆 下游数据库扛不住瞬时峰值 重试风暴把正常请求一起拖死 这时候如果没有限流,服务再快也会被拖垮。 Go 很适合写高并发服务,限流逻辑做成中间件也很自然。今天聊一个最常见、也最实用的方案:令牌桶。 为什么是令牌桶 令牌桶的规则很简单: 系统按固定速率往桶里放令牌 请求到来时先拿一个令牌 拿到就放行,拿不到就拒绝或排队 它的好处是同时兼顾两件事: 控制平均速率 允许短时间突发 比如接口平时稳定在每秒 100 个请求,但偶尔瞬间冲到 150,只要桶里有积累的令牌,就不一定要马上拒绝。 单机场景:HTTP 中间件 Go 官方扩展库里已经有现成实现:golang.org/x/time/rate。 package main import ( "net/http" "time" "golang.org/x/time/rate" ) func RateLimitMiddleware(limiter *rate.Limiter, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !limiter.Allow() { w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte("rate limit exceeded")) return } next.ServeHTTP(w, r) }) } func main() { mux := http.NewServeMux() mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) }) limiter := rate.NewLimiter(rate.Every(10*time.Millisecond), 20) server := &http.Server{ Addr: ":8080", Handler: RateLimitMiddleware(limiter, mux), } _ = server.ListenAndServe() } 这段配置的含义: ...

2026年4月14日 · 2 分钟 · BvBeJ

Hello World

博客开张,随便写点什么。 为什么写博客 工作这么多年,一直没有认真打理过一个技术博客。这次用 Hugo + PaperMod 搭了这个站点,顺便记录一些踩坑经历和技术思考。 选 Hugo 的原因很简单:快、简单、不需要数据库。Markdown 写文章,Git 管理版本,Nginx 静态托管,完美。 博客定位 主要记录这几类内容: 🛠️ 技术实践 — 踩过的坑、做过的项目、代码片段 📖 学习笔记 — Go、Rust、C++、前端这些日常接触的技术 💡 随想 — 偶尔的技术思考和行业观察 不追求高频更新,只写有价值的东西。 关于这个站 🏗️ Hugo + PaperMod 主题 🚀 跑在一台 Raspberry Pi 4 上 🌐 通过 Cloudflare 代理访问 ⚡ Nginx 托管,静态页面加载飞快 树莓派功耗低,7x24 小时开着跑博客挺合适。 开头语 Talk is cheap, show me the code. 这句话虽然被说烂了,但确实是程序员最好的写照。 欢迎来访,欢迎交流。

2026年4月13日 · 1 分钟 · BvBeJ

Go 微服务可观测性:日志、指标、追踪实战

背景 微服务架构下,服务间调用链路错综复杂。一旦出问题,没有可观测性支撑,排查起来就是噩梦。 可观测性三驾马车:日志(Logs)、指标(Metrics)、追踪(Traces)。 日志:结构化日志是基础 别再用 fmt.Printf 了,结构化日志才是正道: import "github.com/rs/zerolog" func main() { log := zerolog.New(os.Stdout). With(). Timestamp(). Caller(). Logger() log.Info(). Str("service", "user-service"). Int("request_id", 12345). Msg("User login successful") } 输出: {"level":"info","service":"user-service","request_id":12345,"time":"2026-04-11T10:00:00Z","caller":"main.go:25","message":"User login successful"} 指标:Prometheus + Grafana import "github.com/prometheus/client_golang/prometheus" import "github.com/prometheus/client_golang/prometheus/promhttp" var ( httpRequests = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total HTTP requests", }, []string{"method", "path", "status"}, ) httpDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Buckets: prometheus.DefBuckets, }, []string{"method", "path"}, ) ) func init() { prometheus.MustRegister(httpRequests, httpDuration) } // 中间件示例 func promMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := &responseWriter{ResponseWriter: w, statusCode: 200} next.ServeHTTP(rw, r) duration := time.Since(start).Seconds() httpRequests.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(rw.statusCode)).Inc() httpDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration) }) } 分布式追踪:OpenTelemetry import "go.opentelemetry.io/otel" import "go.opentelemetry.io/otel/exporters/jaeger" import "go.opentelemetry.io/otel/sdk/trace" func initTracer() (func(), error) { exp, err := jaeger.New(jaeger.WithAgentEndpoint()) if err != nil { return nil, err } tp := trace.NewTracerProvider( trace.WithBatcher(exp), trace.WithSampler(trace.AlwaysSample()), ) otel.SetTracerProvider(tp) return func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() tp.Shutdown(ctx) }, nil } // 在 HTTP handler 中使用 func handleGetUser(w http.ResponseWriter, r *http.Request) { ctx, span := otel.Tracer("user-service").Start(r.Context(), "GetUser") defer span.End() span.SetAttributes( attribute.String("user.id", r.URL.Query().Get("id")), ) user, err := getUserFromDB(ctx, r.URL.Query().Get("id")) if err != nil { span.RecordError(err) // ... } // 传递给后续调用 go someAsyncOperation(ctx, user) } 三者结合:一个完整示例 type UserService struct { logger zerolog.Logger tracer trace.Tracer metrics *UserMetrics userRepo *UserRepository } func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) { // 1. 开始追踪 ctx, span := s.tracer.Start(ctx, "UserService.GetUser") defer span.End() span.SetAttributes(attribute.String("user.id", id)) // 2. 记录指标 s.metrics.requests.Inc() timer := s.metrics.duration.NewTimer() // 3. 结构化日志 s.logger.Info(). Str("user_id", id). Str("trace_id", span.SpanContext().TraceID().String()). Msg("Fetching user") // 4. 业务逻辑 user, err := s.userRepo.FindByID(ctx, id) if err != nil { // 记录错误,包含追踪上下文 s.logger.Error(). Err(err). Str("user_id", id). Str("trace_id", span.SpanContext().TraceID().String()). Msg("Failed to fetch user") span.RecordError(err) s.metrics.errors.Inc() return nil, err } timer.ObserveDuration() return user, nil } 可视化:用 Grafana 大盘 常见 Dashboard 布局: ...

2026年4月11日 · 2 分钟 · BvBeJ

Rust 错误处理:从 panic 到 anyhow

C++ 错误处理的痛 C++ 里错误处理方式一大堆,但没一个完美的: // 方式1: 返回值 + 特殊值 int get_value() { if (failed) return -1; // -1 是魔法值 } // 方式2: 异常 try { do_something(); } catch (const std::exception& e) { // 异常才是正文... } 异常的问题是:不知道会抛什么,不知道该不该 catch,析构函数里抛异常还会 std::terminate。 Rust 的错误哲学 Rust 把错误分为两类: 可恢复错误 → Result<T, E> 不可恢复错误 → panic! // 可恢复:用 Result fn read_file(path: &str) -> Result<String, std::io::Error> { std::fs::read_to_string(path) } // 不可恢复:用 panic fn main() { let v = vec![1, 2, 3]; v.get(10).expect("索引超出范围"); // 程序员的bug } 实战:错误处理的几种模式 1. 基本用法 use std::fs::File; use std::io::{self, Read}; fn read_config() -> Result<String, io::Error> { let mut file = File::open("config.toml")?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } fn main() { match read_config() { Ok(config) => println!("配置: {}", config), Err(e) => eprintln!("读取配置失败: {}", e), } } ? 操作符是灵魂——错误自动向上传播,不需要手写 match。 ...

2026年4月9日 · 2 分钟 · BvBeJ