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"] 关键点: ...
从零理解分布式追踪: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 记录: ...
写给 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 三条规则: ...
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. 这句话虽然被说烂了,但确实是程序员最好的写照。 欢迎来访,欢迎交流。
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 布局: ...
Go 并发模式:Pipeline 实战
背景 Go 的并发模型是其最强大的特性之一。goroutine + channel 的组合让我们能以极低的成本构建高性能的并发系统。 今天聊聊 Pipeline 模式——一种将数据处理流程抽象为一系列阶段的编程范式。 什么是 Pipeline 想象工厂流水线:原料从一端进入,经过多个工序处理,最终成品从另一端出来。 func main() { // 生成数据 data := generate(1, 2, 3, 4, 5) // 流水线:平方 -> 过滤偶数 -> 输出 result := pipeline(data, square, filterEven, printResult, ) <-result.done // 等待完成 } 实战:图片处理流水线 假设我们要处理一批图片:下载 → 缩放 → 添加水印 → 上传。 type Image struct { URL string Data []byte } func ProcessImages(urls []string) error { downloads := make(chan Image, 100) resized := make(chan Image, 100) watermarked := make(chan Image, 100) var wg sync.WaitGroup // 下载阶段 wg.Add(1) go func() { defer wg.Done() for _, url := range urls { img, err := download(url) if err != nil { log.Printf("下载失败: %v", err) continue } downloads <- img } close(downloads) }() // 缩放阶段 (3个worker) for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() for img := range downloads { resizedImg, _ := resize(img, 800, 600) resized <- resizedImg } }() } // 水印阶段 (2个worker) for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() for img := range resized { watermarkedImg, _ := watermark(img, "© My Blog") watermarked <- watermarkedImg } }() } // 上传阶段 wg.Add(1) go func() { defer wg.Done() for img := range watermarked { if err := upload(img); err != nil { log.Printf("上传失败: %v", err) } } }() wg.Wait() return nil } 优雅的错误处理 Pipeline 中如何处理错误?一个不错的方案是用错误 channel: ...
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。 ...
Rust 所有权与借用:我的理解之路
缘起 从 C++ 转 Rust,最不适应的不是语法,而是一种全新的思维模式。 Rust 的所有权系统(Ownership)是语言最核心的创新,也是最陡峭的学习曲线。 C++ 的惯性思维 在 C++ 里,我们习惯了这样的写法: std::string get_name() { return "BvBeJ"; // 编译器会处理返回值优化 } void process() { std::string name = get_name(); std::string alias = name; // 拷贝?还是引用? // ... } // name 和 alias 都会析构 直觉告诉我们这里发生了拷贝。但在 Rust 里,同样的思维会让你碰壁。 Rust 的所有权规则 Rust 遵循三条简单规则: 每个值有一个所有者(Owner) 同一时间只有一个所有者 当所有者离开作用域,值被丢弃(Dropped) fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 被"移动"到 s2 // println!("{}", s1); // ❌ 编译错误!s1 已经无效 println!("{}", s2); // ✅ } // s2 离开作用域,内存被释放 “移动"语义取代了 C++ 的拷贝——这是最大的思维转变。 ...
C++ 反射实现:从 0 到依赖注入容器
背景 C++ 一直缺少反射(Reflection),虽然 C++20 引入了 std::reflect,但目前编译器支持还很有限。 实际项目中,我们经常需要: 根据字符串创建对象 自动注入依赖 序列化/反序列化 这篇文章聊聊怎么在 C++ 里实现一套轻量反射系统。 反射的核心:类型注册 反射的本质是在运行时动态查询类型信息。实现思路很简单——用全局注册表。 1. 基础类型注册表 #include <functional> #include <unordered_map> #include <string> #include <memory> class TypeRegistry { public: template<typename T> static void registerType(const std::string& name) { creators()[name] = []() -> std::any { return std::make_any<T>(); }; } static std::any create(const std::string& name) { auto it = creators().find(name); if (it != creators().end()) { return it->second(); } throw std::runtime_error("Unknown type: " + name); } private: static auto& creators() { static std::unordered_map<std::string, std::function<std::any()>> map; return map; } }; 2. 宏简化注册 #define REGISTER_TYPE(T) \ namespace { \ struct Registrar##T { \ Registrar##T() { \ TypeRegistry::registerType<T>(#T); \ } \ }; \ static Registrar##T registrar_##T; \ } 3. 使用 struct User { std::string name; int age; }; REGISTER_TYPE(User) int main() { auto any_user = TypeRegistry::create("User"); auto& user = std::any_cast<User&>(any_user); user.name = "BvBeJ"; user.age = 28; } 进阶:带构造函数参数 上面的实现只能调用默认构造函数。实际场景往往需要传参: ...
C++ 协程:从 asyncio 理解现代异步编程
背景 现代编程离不开异步。Python 有 asyncio,JavaScript 有 async/await,Go 有 goroutine。C++20 终于引入了协程(Coroutines)。 今天从 Python asyncio 的视角来看看 C++ 协程怎么用。 Python asyncio 的模式 async def fetch(url: str) -> str: # 模拟异步IO await asyncio.sleep(1) return f"data from {url}" async def main(): results = await asyncio.gather( fetch("a.com"), fetch("b.com"), fetch("c.com"), ) print(results) asyncio.run(main()) 核心概念:async def 定义协程函数,await 挂起等待,asyncio.gather 并发执行。 C++20 协程入门 C++ 协程的关键类型: co_await — 挂起协程 co_return — 返回值(相当于 return) co_yield — 产出值(用于生成器) std::suspend_never / std::suspend_always — 挂起策略 简单例子:模拟异步任务 #include <coroutine> #include <future> #include <iostream> template<typename T> struct Task { struct promise_type { T value; std::exception_ptr error; auto get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; } auto initial_suspend() { return std::suspend_never{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void return_value(T v) { value = v; } void unhandled_exception() { error = std::current_exception(); } }; std::coroutine_handle<promise_type> handle; Task(std::coroutine_handle<promise_type> h) : handle(h) {} ~Task() { if (handle) handle.destroy(); } T get() { handle.resume(); return handle.promise().value; } }; // 模拟异步操作 Task<int> async_fetch() { std::cout << "开始异步任务...\n"; co_await std::suspend_always{}; // 挂起,模拟异步等待 std::cout << "异步任务完成\n"; co_return 42; } int main() { auto task = async_fetch(); std::cout << "做一些其他事情...\n"; int result = task.get(); std::cout << "结果: " << result << "\n"; } 和 Python asyncio 对比 Python C++ async def Task<T> 返回类型 await co_await return value co_return value asyncio.sleep(1) std::suspend_always{} 事件循环自动调度 手动 resume() 实际应用:HTTP 客户端 Task<Response> http_get(const std::string& url) { co_await socket_.async_connect(url); co_await socket_.async_write(request_); Response resp = co_await socket_.async_read(); co_return resp; } Task<std::vector<Response>> fetch_all(const std::vector<std::string>& urls) { std::vector<Task<Response>> tasks; for (const auto& url : urls) { tasks.push_back(http_get(url)); } std::vector<Response> results; for (auto& task : tasks) { results.push_back(task.get()); } co_return results; } 坑点 协程不能用在模板参数里 — std::vector<Task<int>> 可以,但 Task<Task<int>> 不行 生命周期管理 — 协程句柄必须手动 destroy(),除非用 RAII 封装 调试困难 — 栈帧被编译器切分,gdb 支持还在完善中 总结 C++20 协程还很年轻,库支持不如 Python 完善。但对于构建高性能网络服务,它的零成本抽象是其他语言难以比拟的。 ...