Go 代理项目:14 周每日学习路线

项目全貌

一个从零开始的 Go 反向代理,分四个阶段进化成一个生产级 SRE 工具。每个阶段解决一类真实问题,边学语法边写功能。每周按 5 个工作日拆分,周末可以复习或补进度。


先说一下 gRPC

你原始规划里提到了”协议升级:把 HTTP 请求转成 gRPC”。简单解释一下这是什么。

gRPC 是 Google 搞的一个远程过程调用(RPC)框架。普通的 HTTP API 用 JSON 传数据,人能读懂但体积大、解析慢。gRPC 用一种叫 Protocol Buffers(protobuf)的二进制格式传数据,体积小、速度快,而且跑在 HTTP/2 上,天然支持多路复用和流式传输。

在微服务架构里,服务之间的内部通信经常用 gRPC 而不是 HTTP+JSON,因为性能好很多。代理做”协议升级”的意思是:外部客户端发普通 HTTP+JSON 请求进来,代理把它转成 gRPC 格式发给后端微服务。这样外部用户不需要知道后端用的是 gRPC,代理帮你做了翻译。

gRPC 和普通 HTTP API 的区别在于三个层面。传输层:普通 HTTP API 跑在 HTTP/1.1 上,一个连接同一时间只能处理一个请求;gRPC 跑在 HTTP/2 上,一个连接可以同时处理多个请求(多路复用)。序列化格式:普通 API 用 JSON(文本),gRPC 用 protobuf(二进制),同样的数据 protobuf 通常比 JSON 小 3-10 倍。接口定义:gRPC 用 .proto 文件严格定义接口,用 protoc 编译器自动生成代码,客户端和服务端的数据结构是编译时确定的。

gRPC 代理涉及 HTTP/2 帧处理、protobuf 序列化/反序列化、服务定义文件编译、动态反射调用等,复杂度比较高,放在第 13-14 周作为进阶目标。


阶段一:隐身斗篷——透明代理 / 协议转换(第 1-4 周)

第 1 周:Go 基础 + 读配置

这周不急着写代理,先把 Go 的基础语法打牢。

Day 1:环境搭建 + A Tour of Go(上)

安装 Go,配置好编辑器。打开 A Tour of Go(https://go.dev/tour/),过 Basics 部分:变量声明(var:=)、基本类型(int、string、bool)、函数定义(func)、多返回值。不用全记住,有个印象就行。

Day 2:A Tour of Go(中)

继续过 Flow Control(for、if、switch)和 More Types 的前半部分(指针、struct)。Go 没有 while 循环,所有循环都用 for。struct 是 Go 里组织数据的核心方式,类似其他语言的 class 但没有继承。

Day 3:A Tour of Go(下)

过 More Types 的后半部分(slice、map、range)和 Methods and Interfaces 的前半部分。slice 和 map 会贯穿整个项目。如果 interface 看不懂没关系,后面第 9 周会专门学。

Day 4:第一个程序 + struct 和 json

关掉教程,自己写一个 main.go。定义一个 Config struct,包含 ListenPort intTargetURL string 两个字段,加上 json tag(比如 `json:"listen_port"`)。创建一个 config.json 文件,用 os.ReadFile 读取,用 json.Unmarshal 映射到 struct,用 fmt.Println 打印出来。确认 go run main.go 能跑通。

Day 5:错误处理 + 练习

Go 的错误处理模式是 if err != nil { log.Fatal(err) },从今天开始习惯它。给 Day 4 的代码加上完整的错误处理:文件不存在怎么办、JSON 格式错误怎么办。然后做个小练习:改 config.json 的内容,故意写错 JSON 格式,看程序怎么报错。学会用 log.Printf 输出带格式的日志。

第 2 周:HTTP 服务器 + 请求转发

Day 1:启动一个 HTTP 服务器

net/http 包。用 http.HandleFunc("/", handler) 注册一个处理函数,用 http.ListenAndServe(":8080", nil) 启动服务器。handler 函数的签名是 func(w http.ResponseWriter, r *http.Request),先让它返回一个固定字符串 “hello proxy”。用 curl localhost:8080 验证。

Day 2:理解 http.Request 和 http.ResponseWriter

在 handler 里打印请求的各种信息:r.Method(GET/POST)、r.URL.Path(路径)、r.Header(所有请求头)、r.RemoteAddr(客户端地址)。用 w.WriteHeader(200) 设置状态码,用 w.Write([]byte("hello")) 写响应体。用 curl 发不同的请求(GET、POST、带 Header),观察打印结果。

Day 3:用 http.Client 发送请求

http.NewRequest 构造请求和 http.Client{}Do 方法发送请求。写一个小程序:向 http://httpbin.org/get 发 GET 请求,打印响应状态码和 Body。理解 resp.Body 是一个 io.ReadCloser,必须用 defer resp.Body.Close() 关闭。

Day 4:实现请求转发

把前几天学的串起来:handler 收到请求后,用 http.NewRequest 构造一个新请求(方法、URL、Body 都从原始请求复制),用 http.Client 发送到 config.json 里配置的目标地址,用 io.Copy(w, resp.Body) 把上游响应写回客户端。

Day 5:处理请求头复制 + 测试

转发时还需要把原始请求的 Header 复制到新请求上(遍历 r.Header,逐个 Set 到新请求)。同时把上游响应的 Header 也复制回客户端。用 curl localhost:8080/get 测试,确认返回的内容和直接访问 httpbin.org/get 一样。这就是一个能跑的最简代理了。

第 3 周:伪装 Host + HTTPS 卸载

Day 1:伪装 Host

在转发逻辑里,转发之前加两行:req.Header.Set("Host", targetHost)req.Host = targetHost。这样目标服务器看到的 Host 头就是你指定的值,而不是 localhost:8080。用 httpbin.org/headers 验证:转发后返回的 headers 里 Host 应该是 httpbin.org。

Day 2:理解 TLS/HTTPS + 生成自签名证书

花半天理解 HTTPS 的工作原理:客户端和服务器通过 TLS 握手协商加密方式,服务器出示证书证明身份,之后所有数据加密传输。”HTTPS 卸载”的意思是代理对外提供 HTTPS,对内用 HTTP 转发给后端。然后用 openssl 生成自签名证书:openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes

Day 3:启动 HTTPS 服务器

crypto/tls 包,用 tls.LoadX509KeyPair("cert.pem", "key.pem") 加载证书。把 http.ListenAndServe 换成 http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)。用 curl -k https://localhost:8443/get 测试(-k 跳过证书验证)。转发到后端仍然用 HTTP,HTTPS 卸载完成。

Arch Linux 提示:你可以用 mkcert 代替 openssl 生成证书(pacman -S mkcert)。mkcert 会自动创建一个本地信任的 CA 并安装到系统信任链里,生成的证书浏览器和 curl 都不会报红,测试时不需要 -k 参数。运行 mkcert -install 初始化 CA,然后 mkcert localhost 127.0.0.1 生成证书。

Day 4:同时监听 HTTP 和 HTTPS

实际场景中代理可能需要同时提供 HTTP(8080)和 HTTPS(8443)。用两个 goroutine 分别启动两个服务器:go http.ListenAndServe(...)http.ListenAndServeTLS(...)。这里会第一次用到 goroutine,但很简单,就是在函数调用前加个 go

Day 5:学习 httputil.ReverseProxy + 回顾

Go 标准库自带了 net/http/httputil.ReverseProxy,它就是一个现成的反向代理实现。读一下它的源码(不长),理解它怎么处理请求复制、响应转发、错误处理。对比你自己写的转发逻辑,看看有什么可以改进的。不需要替换你的代码,理解思路就行。回顾这三周的代码,整理一下结构。

第 4 周:WebSocket 协议升级

这周比较有挑战,不要急。

Day 1:理解 WebSocket 协议

不写代码。WebSocket 通过一次 HTTP 升级握手(Upgrade: websocket)建立连接后,双方可以随时互发消息,连接一直保持。适合聊天、实时推送这类场景。用浏览器开发者工具打开一个 WebSocket 网站(比如 websocket.org 的 echo 测试),观察握手过程和消息收发。

Day 2:搭建 WebSocket 测试服务

安装 gorilla/websocket 库(go get github.com/gorilla/websocket),这里会学到 go get 安装第三方库和 go.mod 依赖管理。写一个最简单的 WebSocket echo 服务器:客户端发什么,服务端原样返回。安装 wscatnpm install -g wscat)测试连接。

Day 3:学习 http.Hijacker 接口

http.Hijacker 能从 HTTP 连接中”劫持”底层的 TCP 连接。调用 w.(http.Hijacker).Hijack() 后,你拿到的是一个裸的 net.Conn,HTTP 层不再管这个连接了。写一个小例子:收到请求后 Hijack 连接,直接往 TCP 连接上写一段文字,用 curl 观察效果。

Day 4:实现 WebSocket 代理转发

代理收到 WebSocket 升级请求后,先向后端建立 WebSocket 连接,然后用两个 goroutine 做双向转发:一个从客户端读数据写到后端,一个从后端读数据写到客户端。用 wscat 通过代理连接 Day 2 的 echo 服务,验证双向通信。

Day 5:错误处理 + 连接关闭

处理各种边界情况:客户端断开时关闭后端连接,后端断开时通知客户端。加上日志,打印 WebSocket 连接的建立和关闭事件。如果 Day 4 没搞定,今天继续调试,不要急着往下走。


阶段二:安检员——认证与过滤(第 5-6 周)

原来的鉴权和过滤分了两周,但 Token 鉴权和 UA 黑名单的语法很简单(就是 if-else 和 slice 遍历),所以压缩成一周,把省下来的时间给内容过滤和动态路由多留一天缓冲。

第 5 周:Token 鉴权 + UA 黑名单 + 内容过滤

Day 1:学 slice 和 for…range + 实现 Token 鉴权

Go 的 slice(切片)是动态数组,用 []string{"a", "b"} 创建,用 len() 获取长度,用 append() 添加元素,用 for i, v := range slice 遍历。在 config.json 里加一个 "token": "my-secret-token" 字段。在转发逻辑的最前面加检查:r.Header.Get("X-Token") 读取请求头,不等于配置里的 token 就返回 403。用 curl -H "X-Token: my-secret-token" localhost:8080/get 测试通过,不带 header 测试返回 403。

Day 2:实现 UA 黑名单

在 config.json 里加 "ua_blacklist": ["BadBot", "Scrapy"]。读取请求的 User-Agent 头,用 for...range 遍历黑名单,用 strings.Contains(配合 strings.ToLower 做大小写不敏感匹配)检查 UA 是否包含黑名单关键词。命中就返回 403。把鉴权和 UA 检查抽成独立函数:func checkToken(r *http.Request, token string) boolfunc checkUA(r *http.Request, blacklist []string) bool,保持 handler 干净。

Day 3:学 map + bytes 包 + 理解 Body 一次性读取问题

Go 的 map[string]string 是键值对集合,用 m[key] 读取,用 for k, v := range m 遍历。然后理解一个关键问题:HTTP 请求的 Body 是一个 io.ReadCloser,只能读一次。读完之后如果还要转发,必须用 bytes.NewReader(bodyBytes) 重新包装成一个新的 Reader 赋回 r.Body。写个小例子验证这个行为。

Day 4:实现内容过滤(敏感词拦截)

在 config.json 里加 "sensitive_words": ["password", "secret"]。在转发之前,用 io.ReadAll(r.Body) 读取整个 Body,用 bytes.Contains 检查是否包含敏感词。如果包含,返回 400。如果不包含,用 bytes.NewReader(bodyBytes) 重新包装 Body 继续转发。用 curl -X POST -d "my password is 123" localhost:8080/post 测试拦截。

Day 5:测试所有安检功能

同时测试 Token 鉴权、UA 黑名单、内容过滤的各种组合:不带 Token、带错误 Token、带正确 Token 但 UA 在黑名单里、Token 和 UA 都通过但 Body 有敏感词。加上日志,打印每个被拒绝的请求的原因。确保所有情况都正确处理。

第 6 周:动态路由 + 轮询负载均衡

原来路由和负载均衡分了两周,但路由的核心就是 map + 前缀匹配,负载均衡的核心就是取模运算,合并到一周刚好。

Day 1:实现动态路由

把 config.json 的单个 target_url 改成路由表:"routes": {"/api": "http://backend-a:8080", "/static": "http://backend-b:8080"}。在 handler 里用 strings.HasPrefix(r.URL.Path, prefix) 遍历路由表找到匹配的后端。注意 map 遍历顺序是随机的,所以把路由前缀放进一个 slice 里按长度从长到短排序,先匹配最具体的路由。加一个默认路由兜底。

Day 2:学方法(method)和指针 + 定义 LoadBalancer struct

Go 的 struct 可以绑定方法:func (lb *LoadBalancer) Next() string*LoadBalancer 是指针接收者,方法可以修改 struct 的字段。如果用值接收者,每次调用都是在副本上操作,修改不会生效。定义一个 LoadBalancer struct,包含 backends []stringcounter int64,写一个 Next() 方法返回 backends[counter % len(backends)] 并递增 counter。

Day 3:用 atomic 实现并发安全 + 改造路由表

sync/atomic 包,把 counter++ 换成 atomic.AddInt64(&lb.counter, 1)。然后改造 config.json 的路由表,支持多后端:"routes": {"/api": ["http://pod1:8080", "http://pod2:8080", "http://pod3:8080"]}。Config struct 对应改成 Routes map[string][]string。每个路由前缀对应一个 LoadBalancer 实例。

Day 4:集成到代理 + 测试轮询

在 handler 里先匹配路由,再调用对应 LoadBalancer 的 Next() 获取后端地址。启动 3 个简单后端(分别返回 “I am pod1”、”I am pod2”、”I am pod3”),用 for i in {1..9}; do curl localhost:8080/api; done 连续请求 9 次,确认响应依次是 pod1、pod2、pod3 循环。

进阶思考:基础的 Round Robin 假设所有后端性能一样,但 SRE 场景下后端节点性能不一是很常见的。如果有余力,可以尝试实现加权轮询(Weighted Round Robin)——在 config.json 里给每个后端配一个权重,权重高的分到更多请求。或者实现最少连接(Least Connections)——每次选当前活跃连接数最少的后端。这两个算法不复杂,但能让你的代理在真实场景下更实用。不急,可以留到 12 周收尾时再加。

Day 5:整合测试阶段一和阶段二

这是一个里程碑。同时验证所有已完成的功能:HTTPS 卸载、Host 伪装、WebSocket 转发、Token 鉴权、UA 黑名单、内容过滤、动态路由、轮询负载均衡。整理代码结构,把不同功能拆到不同的 .go 文件里(比如 auth.gorouter.goloadbalancer.go)。


阶段三:交警——流量调度与限流(第 7-9 周)

这是整个路线里最陡的坡,goroutine、channel、select、context、Mutex 集中在这里。原来 2 周,现在给 3 周,多出来的一周用来消化并发模型。

第 7 周:Go 并发模型 + 限流器

Day 1:学 goroutine 基础

goroutine 是 Go 的轻量级线程,用 go func() { ... }() 启动。写几个小练习:启动 5 个 goroutine 分别打印不同的数字,观察输出顺序是随机的。理解 time.Sleep 可以让 goroutine 等待,但主 goroutine 退出后所有子 goroutine 都会被杀掉。

Day 2:学 channel 基础

channel 是 goroutine 之间传递数据的管道。ch := make(chan int) 创建无缓冲 channel,ch <- 42 发送(会阻塞直到有人接收),v := <-ch 接收(会阻塞直到有人发送)。写一个小练习:一个 goroutine 往 channel 发 10 个数字,主 goroutine 接收并打印。然后学带缓冲的 channel:ch := make(chan int, 5),缓冲没满时发送不阻塞。

Day 3:学 select + time.Ticker

select 同时监听多个 channel,谁先有数据就执行谁的分支,类似 switch 但用于 channel。time.Ticker 是一个定时器,ticker := time.NewTicker(time.Second) 每秒往 ticker.C 这个 channel 发一个信号。写一个小程序:用 select 同时监听一个 ticker(每秒打印 “tick”)和一个 quit channel(收到信号就退出)。

Day 4:学 sync.Map + 实现限流器核心逻辑

sync.Map 是并发安全的 map,不需要手动加锁。用 m.Store(key, value) 存,m.Load(key) 取,m.Range(func(k, v) bool { ... }) 遍历。创建一个 RateLimiter struct,内部用 sync.Map 存储每个 IP 的请求计数。Allow(ip string) bool 方法:读取当前计数,超过 5 就返回 false,否则加 1 返回 true。启动一个 goroutine 监听 Ticker,每秒清零所有计数。

Day 5:集成限流器到代理 + 测试

net.SplitHostPort(r.RemoteAddr) 提取客户端 IP,调用 limiter.Allow(ip),返回 false 就响应 429 Too Many Requests。用 ab -n 100 -c 10 http://localhost:8080/api 压测,观察日志确认同一个 IP 每秒只有前 5 个请求通过。

第 8 周:熔断与降级

Day 1:理解熔断器状态机

不写代码,先在纸上画状态图。三种状态:Closed(关闭,正常转发)→ 连续错误超过阈值 → Open(打开,直接返回 503)→ 等待超时到期 → Half-Open(半开,放一个请求试探)→ 试探成功回到 Closed,失败回到 Open。理解每个状态转换的触发条件。

Day 2:学 sync.Mutex + 定义 CircuitBreaker struct

sync.Mutex 是互斥锁,mu.Lock() 加锁,mu.Unlock() 解锁,通常配合 defer mu.Unlock() 使用。定义一个 CircuitBreaker struct,包含:state string(当前状态)、failCount int(连续失败次数)、threshold int(阈值,比如 10)、lastFailTime time.Timetimeout time.Duration(恢复等待时间,比如 60 秒)、mu sync.Mutex

Day 3:学 context.WithTimeout + 实现 Allow 和 RecordResult

context.WithTimeout 给操作设超时:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),别忘了 defer cancel()。实现 Allow() bool:Closed 状态返回 true;Open 状态检查是否超过 timeout,超过了切到 Half-Open 返回 true,没超过返回 false;Half-Open 状态返回 true(放一个请求试探)。实现 RecordResult(success bool):成功则重置 failCount 切到 Closed;失败则递增 failCount,超过阈值切到 Open。

Day 4:集成到代理

在转发逻辑里加入熔断器:转发前调用 cb.Allow(),返回 false 就直接响应 503 “系统繁忙”。转发请求时用 req.WithContext(ctx) 加上 5 秒超时。转发后根据响应状态码调用 cb.RecordResult(statusCode < 500)

Day 5:测试熔断和恢复

启动后端,正常请求确认通过。关掉后端,连续发请求,观察日志:前 10 个请求报错,第 11 个开始直接返回 503。等 60 秒后再发请求,观察半开状态的试探行为。重启后端,确认代理自动恢复转发。如果有 bug,今天调试。

第 9 周:并发复习 + 耗时统计 + defer

这周节奏放缓,消化前两周的并发知识,同时开始做可观测性。

Day 1:并发复习

回顾 goroutine、channel、select、Mutex 的用法。重新读一遍限流器和熔断器的代码,确保每一行都理解。如果有不清楚的地方,去 Go by Example(https://gobyexample.com/)查对应的例子。

Day 2:学 defer + time 包做耗时统计

defer 在函数退出时执行,不管是正常 return 还是 panic。在 handler 开头写 start := time.Now(),然后 defer func() { log.Printf("[%s] %s → %d (%v)", r.Method, r.URL.Path, statusCode, time.Since(start)) }()。注意 statusCode 需要用一个自定义的 ResponseWriter 包装器来捕获(因为标准的 ResponseWriter 写完状态码后你读不回来)。

Day 3:写一个 StatusCapture ResponseWriter

定义一个 struct 包装 http.ResponseWriter,重写 WriteHeader 方法来记录状态码。这里会更深入理解 Go 的 interface:你的 struct 只要实现了 http.ResponseWriter 的三个方法(Header()Write()WriteHeader()),就可以当 ResponseWriter 用。

Day 4:学 interface + 理解 http.Handler

Go 的 http.Handler 接口只有一个方法:ServeHTTP(w http.ResponseWriter, r *http.Request)。任何实现了这个方法的 struct 都自动满足接口,不需要显式声明。理解中间件模式的签名:func(next http.Handler) http.Handler——接收一个 Handler,返回一个新的 Handler。

Day 5:写第一个中间件(Logging)

把 Day 2 的耗时统计逻辑封装成一个中间件函数。在调用 next.ServeHTTP(w, r) 之前记录开始时间,之后计算耗时并打印。验证中间件能正确包装现有的 handler。


阶段四:监控探针——可观测性(第 10-14 周)

第 10 周:Prometheus 指标 + 全量日志存数据库

Day 1:引入 Prometheus 客户端库

运行 go get github.com/prometheus/client_golang/prometheusgo get github.com/prometheus/client_golang/prometheus/promhttp。注册两个指标:一个 Counter proxy_requests_total(按状态码和路径分标签),一个 Histogram proxy_request_duration_seconds(记录延迟分布)。在 Logging 中间件里调用 counter.Inc()histogram.Observe(elapsed.Seconds())

Day 2:暴露 /metrics 端点 + 验证

http.Handle("/metrics", promhttp.Handler()) 暴露指标端点。启动代理,发几个请求,然后 curl localhost:8080/metrics 查看输出。你应该能看到 Prometheus 格式的指标数据。如果你的 K3s 集群里跑着 Prometheus,可以配置它来抓取这个端点。

Day 3:学 database/sql + 连接 SQLite

Go 的 database/sql 是标准库的数据库接口,具体数据库通过驱动实现。先用 SQLite(轻量,不需要装数据库服务):go get github.com/mattn/go-sqlite3。用 sql.Open("sqlite3", "./proxy.db") 连接,用 db.Exec 创建一张 request_logs 表(字段:id、timestamp、method、url、status_code、duration_ms、request_body)。

避坑:SQLite 在并发写时会报 database is locked 错误。你的代理是高并发的,必须在连接后设置 db.SetMaxOpenConns(1) 限制为单连接,让写操作排队。这能解决问题但会成为瓶颈。如果后续发现 SQLite 扛不住,可以换成 PostgreSQL(驱动用 github.com/jackc/pgx/v5/stdlib),连接字符串改一下就行,database/sql 的接口是通用的,业务代码几乎不用改。

Day 4:学 io.TeeReader + 实现同步写库

io.TeeReader(r, w) 返回一个 Reader,从它读取数据时,数据会同时写入 w。用它复制请求 Body:var bodyBuf bytes.BufferteeReader := io.TeeReader(r.Body, &bodyBuf)。转发完成后,用 db.Exec("INSERT INTO request_logs ...") 把请求信息写入数据库。先做同步写,验证数据能正确存入。

Day 5:用带缓冲 channel 改成异步写库

同步写库会拖慢请求。创建 logCh := make(chan LogEntry, 1000),handler 里把日志条目扔进 channel 就返回,另一个 goroutine 从 channel 取数据写库。用 selectdefault 分支处理 channel 满的情况——丢弃日志而不是阻塞请求。加一个 Prometheus Counter 记录丢弃数量。

第 11 周:流量镜像

Day 1:理解流量镜像的原理

流量镜像就是把线上请求复制一份发到测试环境。生产后端正常处理并返回响应给客户端,测试后端也收到一模一样的请求但响应被丢弃。关键原则:镜像是”发了就忘”(fire and forget),镜像失败不能影响正常请求。

Day 2:用 bytes.Buffer 复制请求 Body

io.ReadAll(r.Body) 把 Body 读出来存到 []byte,然后用 bytes.NewReader(bodyBytes) 分别创建两个 Reader——一个给正常转发,一个给镜像请求。在 config.json 里加 "mirror_target": "http://test-backend:8080"

Day 3:用 goroutine 异步发送镜像请求

http.NewRequest 构造一个新请求(复制方法、URL、Header、Body),go sendMirror(mirrorReq) 异步发送。主流程不等待镜像结果。给镜像请求用 context.WithTimeout 设独立的 3 秒超时。

Day 4:学 recover + 保护镜像 goroutine

recover 能捕获 goroutine 里的 panic。在镜像 goroutine 开头加 defer func() { if r := recover(); r != nil { log.Printf("mirror panic: %v", r) } }(),防止镜像崩溃影响主进程。

Day 5:测试镜像功能

启动两个后端:一个”生产”后端返回正常响应,一个”测试”后端只打印收到的请求。发请求到代理,确认客户端收到生产后端的响应,同时测试后端也出现了请求日志。关掉测试后端,确认镜像失败不影响正常响应。

第 12 周:中间件链整合 + 收尾

Day 1:把所有功能重构为中间件

把鉴权、限流、UA 黑名单、内容过滤、Prometheus 指标、日志等逻辑都重构成独立的中间件函数,签名统一为 func(next http.Handler) http.Handler。比如 func TokenAuth(token string) func(http.Handler) http.Handler,检查 Token 不通过就返回 403,通过就调用 next.ServeHTTP(w, r)

Day 2:串联中间件链

写一个 Chain 函数把多个中间件串起来。最终效果:请求依次经过 日志 → Prometheus → 限流 → Token 认证 → UA 黑名单 → 内容过滤 → 路由 → 负载均衡 → 熔断 → 转发。每一层都可以决定继续传递还是直接返回错误。

Day 3:学 flag 包 + 命令行参数

flag 包支持命令行参数:--config 指定配置文件路径,--port 指定监听端口。调用 flag.Parse() 解析。这样启动代理时可以 ./proxy --config /etc/proxy/config.json --port 9090

Day 4:实现优雅关闭(Graceful Shutdown)

os/signal 监听 SIGINTSIGTERM。收到信号后调用 server.Shutdown(ctx) 停止接受新连接,等待在途请求处理完再退出。同时关闭数据库连接、停止日志 goroutine。

Docker 化:写一个多阶段构建(Multi-stage Build)的 Dockerfile。第一阶段用 golang:1.22 编译,第二阶段用 scratchgcr.io/distroless/static 作为底座,只复制编译好的二进制文件进去。Go 编译出来的是静态链接的单文件,打包出来的镜像可能只有 10-15MB。然后 docker build -t go-proxy . 构建,docker run -p 8080:8080 go-proxy 运行,确认代理在容器里正常工作。这个镜像可以直接推到你的 K3s 集群里跑。

Day 5:写单元测试 + 最终验收

testing 包。给限流器的 Allow() 写测试:连续调用 6 次,前 5 次返回 true 第 6 次返回 false。给熔断器写测试:连续 RecordResult(false) 超过阈值后 Allow() 返回 false。给轮询写测试:3 个后端调用 6 次,每个出现 2 次。用 go test ./... 运行。然后做一次完整的端到端验收。


进阶:gRPC 协议升级(第 13-14 周)

不需要重写代理。是在已有的中间件链和路由系统上加一个新的路由规则:当请求路径匹配 /grpc/... 时,走 gRPC 转发 handler;其他路径还是走原来的 HTTP 转发。限流、鉴权、Prometheus 指标这些中间件照常工作,gRPC 转换只是在链的末端替换了转发方式。

第 13 周:protobuf + gRPC 基础

Day 1:安装 protoc + 写第一个 .proto 文件

安装 Protocol Buffers 编译器(macOS 用 brew install protobuf),安装 Go 插件(go install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest)。写一个 greeter.proto,定义一个 Greeter 服务,有一个 SayHello 方法,请求参数是 name(string),返回值是 message(string)。

Day 2:生成 Go 代码 + 写 gRPC 服务端

运行 protoc --go_out=. --go-grpc_out=. greeter.proto 生成两个 .go 文件。写一个 gRPC 服务端:实现生成的接口,用 grpc.NewServer() 创建服务器,监听 50051 端口。学 google.golang.org/grpc 这个核心库。

Day 3:写 gRPC 客户端 + 验证通信

写一个 gRPC 客户端:用 grpc.Dial 连接服务端,调用 SayHello 方法。运行服务端和客户端,确认通信正常。理解完整链路:.proto 定义 → protoc 生成代码 → 服务端实现 → 客户端调用。

Day 4:学 JSON ↔ protobuf 转换

google.golang.org/protobuf/encoding/protojson 包。protojson.Unmarshal 把 JSON 转成 protobuf 消息,protojson.Marshal 把 protobuf 转回 JSON。写个小程序验证:JSON {"name": "world"} → protobuf HelloRequest → JSON,确认数据一致。

Day 5:学 gRPC 反射 + grpcurl

gRPC 反射让代理在运行时查询后端支持哪些方法和参数结构,不需要编译时知道 .proto 定义。在 gRPC 服务端开启反射(reflection.Register(server))。安装 grpcurlbrew install grpcurl),用它测试:grpcurl -plaintext localhost:50051 list 列出服务,grpcurl -plaintext -d '{"name": "world"}' localhost:50051 greeter.Greeter/SayHello 调用方法。

第 14 周:在代理中实现 HTTP→gRPC 转换

Day 1:学动态调用 API

google.golang.org/grpcInvoke 方法和 google.golang.org/protobuf/types/dynamicpb 包。dynamicpb 允许在不 import 生成代码的情况下,根据运行时获取的服务描述符动态构造 protobuf 消息。这是通用 gRPC 代理的关键。

性能考量:动态反射转换(JSON ↔ Protobuf)虽然通用,但每次请求都要通过反射查询服务描述符,性能开销很大。可以加一个 LRU Cache(用 github.com/hashicorp/golang-lru 或自己用 sync.Map + 链表实现),缓存已经解析过的服务描述符和方法描述符。第一次请求某个方法时走反射,之后直接从缓存取,减少反射频率。

Day 2:在代理中加 gRPC 路由

加一个新路由规则 /grpc/<service>/<method>。当请求匹配时:读取 JSON Body → 通过反射获取目标方法的描述符 → 用 protojson.Unmarshal 转成动态 protobuf 消息 → 用 grpc.Invoke 发送到后端 → 把 protobuf 响应用 protojson.Marshal 转回 JSON → 返回给客户端。

Day 3:处理 gRPC 错误码映射

gRPC 有自己的错误码(NotFound、Internal、Unavailable 等),需要映射到 HTTP 状态码:NotFound → 404,Internal → 500,Unavailable → 503。学 google.golang.org/grpc/status 包提取错误码。

Day 4:端到端测试

启动 gRPC 后端(开启反射),启动代理。用 curl -X POST -d '{"name": "world"}' localhost:8080/grpc/greeter.Greeter/SayHello 发送 HTTP+JSON 请求,确认代理翻译成 gRPC 发给后端,再翻译回 JSON 返回。客户端全程只用了 HTTP 和 JSON。

Day 5:最终收尾

验证 gRPC 路由和 HTTP 路由共存:/grpc/... 走 gRPC 转发,其他路径走 HTTP 转发。确认中间件链(限流、鉴权、Prometheus)对两种路由都生效。整理代码,更新 README。


14 周总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
阶段一:隐身斗篷(透明代理 / 协议转换)— 4 周
第 1 周 Go 基础语法 + 读 JSON 配置(多留 1 天给零基础缓冲)
第 2 周 HTTP 服务器 + 请求转发
第 3 周 伪装 Host + HTTPS 卸载
第 4 周 WebSocket 协议升级

阶段二:安检员(认证与过滤)— 2 周(压缩,语法简单)
第 5 周 Token 鉴权 + UA 黑名单 + 内容过滤
第 6 周 动态路由 + 轮询负载均衡

阶段三:交警(流量调度与限流)— 3 周(展开,并发是最陡的坡)
第 7 周 Go 并发模型(goroutine/channel/select)+ 限流器
第 8 周 熔断与降级(Mutex/context/状态机)
第 9 周 并发复习 + 耗时统计 + defer + 中间件模式入门

阶段四:监控探针(可观测性)— 3 周
第 10 周 Prometheus 指标 + 全量日志存数据库
第 11 周 流量镜像
第 12 周 中间件链整合 + 优雅关闭 + 单元测试

进阶:gRPC 协议升级 — 2 周
第 13 周 protobuf + gRPC 基础
第 14 周 在代理中实现 HTTP→gRPC 转换(不重写,加新路由)

推荐资源

  • A Tour of Go:语法速通,第 1 周前三天过完。
  • Go by Example:每个语法点一个可运行的例子,当字典查。
  • 《Go 语言圣经》:系统学习,重点看 struct、interface、goroutine、channel 章节。
  • 直接写代理项目:边做边学,遇到不会的语法再查,这是最快的路径。

一句话

14 周后你手里会有一个能伪装 Host、能卸载 HTTPS、能转发 WebSocket 和 gRPC、能鉴权、能过滤、能路由、能负载均衡、能限流、能熔断、能出 Prometheus 指标、能全量记录日志、能镜像流量的 Go 代理——装进你封装的 Linux 发行版里,就是一套完整的自动化运维体系。

月度TODO

  • 极限科技 4篇文章
  • 懒猫微服 4篇文章
  • 生活感悟1篇
  • 纯技术文章 2篇
  • 英语

在 Kubernetes 上用 Fluent Bit 收集 Nginx 日志到 Easysearch

本文基于 k3s + Easysearch 2.0.3 实测验证,从零开始搭建一套完整的日志收集方案。

什么是 Fluent Bit

Fluent Bit 是一个轻量级的日志收集和转发工具,用 C 语言写的,内存占用极低(通常只需要几十 MB)。它的工作很简单:从某个地方读日志(INPUT),可选地处理一下(FILTER),然后发到某个地方(OUTPUT)。

1
2
INPUT → FILTER → OUTPUT
读日志 处理 发送

常见用法:

  • 从文件读日志(tail 插件,类似 tail -f
  • 从容器 stdout 读日志
  • 发送到 Elasticsearch / Easysearch / Kafka / S3 等

和 Fluentd 的区别:Fluent Bit 更轻量(C 语言 vs Ruby),适合作为 Agent 部署在每个节点或 Pod 里。Fluentd 功能更丰富,适合做日志聚合层。在 Kubernetes 场景下,Fluent Bit 是更常见的选择。

什么是 Easysearch

INFINI Easysearch 是兼容 Elasticsearch API 的搜索引擎。Fluent Bit 的 es(Elasticsearch)输出插件可以直接对接,不需要改配置。简单理解:Easysearch 是 Elasticsearch 的国产替代品。

为什么用 Sidecar 模式

本文用 Sidecar 模式部署 Fluent Bit:把它和 Nginx 放在同一个 Pod 里,共享日志目录。

另一种常见方式是 DaemonSet 模式:在每个节点上跑一个 Fluent Bit,收集该节点上所有 Pod 的 stdout 日志。DaemonSet 适合收集所有 Pod 的日志,Sidecar 适合收集特定应用的日志文件。

阅读更多

在AWS EC2 上从零搭建 Kubernetes 集群(kubeadm)

今天讲解在AWS EC2 上使用kubeadm搭建Kubernetes 集群。

kubeadm 是 Kubernetes 官方提供的集群引导工具,用来快速创建符合最佳实践的 K8s 集群。除了初始化集群,它还能做节点的升级、降级等生命周期管理。用 kubeadm 建集群是学习 K8s 的推荐方式,也适合搭建小规模集群或作为更复杂企业级方案的基础组件。

本文基于 Ubuntu,使用三台 EC2 实例:一台作为控制面(Master),两台作为工作节点(Worker)。

我们会在 Master 节点上从头安装 kubeadm 及其依赖,然后初始化集群,最后把 Worker 节点加入进来。

阅读更多

修复 GitHub Pages 推送后 CNAME 自动重置旧域名的问题

部署后 GitHub Pages 域名自动变成已经取消的实效的域名,而不是预期的 *.github.io

排查之后是source/CNAME 文件中配置了旧域名 airag.click

删除 CNAME 文件后重新部署就可以解决

1
rm source/CNAME
  • CNAME 文件会被 Hexo 复制到生成目录,告诉 GitHub Pages 使用自定义域名
  • 删除后将使用默认的 <username>.github.io 域名

k3s + Helm 部署 Easysearch

最近学了K8S,为了测试方便测试搭了一个K3S集群,然后使用helm运行一下Easysearch。

参考文档:https://docs.infinilabs.com/easysearch/main/docs/deployment/install-guide/helm/

首先添加helm仓库并更新。

1
2
helm repo add infinilabs https://helm.infinilabs.com
helm repo update

然后新建命名空间,我这里叫做es(下同),也可以使用其他名字。

1
kubectl create namespace es
阅读更多

k8s yml小抄.md

源码来自 cloudacademy/intro-to-k8s,用作学习笔记

1. Pod 基础

1.1 最简 Pod(1.1-basic_pod.yaml)
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: nginx:latest

这是最小化的 Pod 定义。只需要四个顶级字段:

  • apiVersion: v1 — 核心 API 版本
  • kind: Pod — 资源类型
  • metadata.name — Pod 名称,命名空间内唯一
  • spec.containers — 至少一个容器,必须指定 nameimage

注意:使用 nginx:latest 时,Kubernetes 默认 imagePullPolicy: Always,每次启动都会拉取镜像。


1.2 声明端口(1.2-port_pod.yaml)
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: nginx:latest
ports:
- containerPort: 80

相比 1.1 新增了 ports.containerPort: 80。如果不声明端口,kubectl describe 中端口显示为 none,外部无法知道容器监听哪个端口。声明端口是让 Kubernetes 知道容器对外提供服务的方式。

注意:即使声明了端口,从集群外部仍然无法直接访问 Pod IP(Pod IP 在容器网络内),需要通过 Service 暴露。


1.3 添加标签(1.3-labeled_pod.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: mypod
labels:
app: webserver
spec:
containers:
- name: mycontainer
image: nginx:latest
ports:
- containerPort: 80

新增 labels.app: webserver。标签是键值对,用途:

  • 标识资源属性(应用类型、层级、区域等)
  • 被 Service 的 selector 用来匹配目标 Pod
  • kubectl get-l 选项用来过滤资源

标签是 Kubernetes 中资源关联的核心机制。


1.4 资源请求与限制(1.4-resources_pod.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
name: mypod
labels:
app: webserver
spec:
containers:
- name: mycontainer
image: nginx:latest
resources:
requests:
memory: "128Mi" # 128 MiB
cpu: "500m" # 0.5 CPU
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80

新增 resources 字段:

  • requests:调度器据此选择节点,节点必须有足够的可分配资源
  • limits:容器运行时的资源上限,超过内存限制会被 OOMKilled

这里 requests = limits,QoS 等级为 Guaranteed(最不容易被驱逐)。不设置任何资源则为 BestEffort(最先被驱逐)。生产环境应始终设置资源请求。


2. Service

2.1 NodePort Service(2.1-web_service.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
labels:
app: webserver
name: webserver
spec:
ports:
- port: 80
selector:
app: webserver
type: NodePort
  • selector.app: webserver — 匹配带有 app=webserver 标签的 Pod
  • ports.port: 80 — Service 端口,对应 Pod 的 containerPort
  • type: NodePort — 在每个节点上分配一个端口(30000–32767),集群外部可通过 节点IP:NodePort 访问

Service 解决的核心问题:Pod IP 不固定,Service 提供稳定入口并自动负载均衡。


3. 多容器 Pod 与命名空间

3.1 命名空间(3.1-namespace.yaml)
1
2
3
4
5
6
apiVersion: v1
kind: Namespace
metadata:
name: microservice
labels:
app: counter

命名空间用于隔离资源。不需要 spec,只需 name。使用 -n microservice 指定命名空间。


3.2 多容器 Pod(3.2-multi_container.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent # 防止每次拉取 latest
ports:
- containerPort: 6379

- name: server
image: lrakai/microservices:server-v1
ports:
- containerPort: 8080
env:
- name: REDIS_URL
value: redis://localhost:6379 # 同 Pod 内用 localhost

- name: counter
image: lrakai/microservices:counter-v1
env:
- name: API_URL
value: http://localhost:8080

- name: poller
image: lrakai/microservices:poller-v1
env:
- name: API_URL
value: http://localhost:8080

4 个容器在同一个 Pod 中,共享网络栈:

  • Redis(数据层)监听 6379
  • Server(应用层)监听 8080,通过 localhost:6379 连接 Redis
  • Counter 和 Poller(支持层)通过 localhost:8080 连接 Server

imagePullPolicy: IfNotPresent 用于 latest 标签时防止每次都拉取。使用具体标签时默认就是 IfNotPresent。

局限性:Kubernetes 以 Pod 为最小扩缩单位,无法单独扩缩某个容器。如果需要独立扩缩,应拆分为多个 Pod + Service。


4. 服务发现(Service Discovery)

4.1 命名空间(4.1-namespace.yaml)
1
2
3
4
5
6
apiVersion: v1
kind: Namespace
metadata:
name: service-discovery
labels:
app: counter

为服务发现课程创建独立的命名空间 service-discovery,隔离本课资源。后续所有命令需要加 -n service-discovery


4.2 数据层 — Pod + ClusterIP Service(4.2-data_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Service
apiVersion: v1
kind: Service
metadata:
name: data-tier
labels:
app: microservices
spec:
ports:
- port: 6379
protocol: TCP
name: redis
selector:
tier: data
type: ClusterIP # 默认类型,仅集群内可访问
---
# Pod
apiVersion: v1
kind: Pod
metadata:
name: data-tier
labels:
app: microservices
tier: data # 被 Service selector 匹配
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379

将 Redis 拆分为独立 Pod + ClusterIP Service。type: ClusterIP 是默认值,仅集群内部可访问。name: redis 为端口命名,后续可通过环境变量引用。


4.3 应用层 — 环境变量服务发现(4.3-app_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: v1
kind: Service
metadata:
name: app-tier
labels:
app: microservices
spec:
ports:
- port: 8080
selector:
tier: app
---
apiVersion: v1
kind: Pod
metadata:
name: app-tier
labels:
app: microservices
tier: app
spec:
containers:
- name: server
image: lrakai/microservices:server-v1
ports:
- containerPort: 8080
env:
- name: REDIS_URL
# Environment variable service discovery
# Naming pattern:
# IP address: <all_caps_service_name>_SERVICE_HOST
# Port: <all_caps_service_name>_SERVICE_PORT
# Named Port: <all_caps_service_name>_SERVICE_PORT_<all_caps_port_name>
value: redis://$(DATA_TIER_SERVICE_HOST):$(DATA_TIER_SERVICE_PORT_REDIS)
# In multi-container example value was
# value: redis://localhost:6379

Kubernetes 自动为同命名空间的每个 Service 注入环境变量:

  • <SERVICE_NAME>_SERVICE_HOST → Service 的 ClusterIP
  • <SERVICE_NAME>_SERVICE_PORT → Service 的端口
  • <SERVICE_NAME>_SERVICE_PORT_<PORT_NAME> → 命名端口

这里 DATA_TIER_SERVICE_HOSTDATA_TIER_SERVICE_PORT_REDIS 由 Kubernetes 自动注入,无需硬编码 IP。对比多容器 Pod 中的 redis://localhost:6379,现在通过 Service 跨 Pod 通信。


4.4 支持层 — DNS 服务发现(4.4-support_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
name: support-tier
labels:
app: microservices
tier: support
spec:
containers:

- name: counter
image: lrakai/microservices:counter-v1
env:
- name: API_URL
# DNS for service discovery
# Naming pattern:
# IP address: <service_name>.<service_namespace>
# Port: needs to be extracted from SRV DNS record
value: http://app-tier.service-discovery:8080

- name: poller
image: lrakai/microservices:poller-v1
env:
- name: API_URL
# omit namespace to only search in the same namespace
value: http://app-tier:$(APP_TIER_SERVICE_PORT)

两种服务发现方式对比:

  • DNS:<service-name>.<namespace> 或同命名空间内直接用 <service-name>
  • 环境变量:$(<SERVICE_NAME>_SERVICE_PORT)

counter 用了完整 DNS(app-tier.service-discovery:8080),poller 省略了命名空间并混合使用了环境变量。DNS 方式更灵活,推荐使用。


5. Deployment

5.2 数据层 Deployment(5.2-data_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1          # Deployment 使用 apps API 组
kind: Deployment
metadata:
name: data-tier
labels:
app: microservices
tier: data
spec:
replicas: 1 # 副本数
selector:
matchLabels:
tier: data # 必须与 template.metadata.labels 匹配
template: # Pod 模板
metadata:
labels:
app: microservices
tier: data
spec: # Pod spec
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379

从裸 Pod 升级为 Deployment:

  • replicas:期望的 Pod 副本数
  • selector.matchLabels:Deployment 用来管理 Pod 的标签选择器
  • template:Pod 模板,Deployment 据此创建和管理 Pod
  • Deployment 提供滚动更新、回滚、自愈(Pod 挂了自动重建)

5.3 应用层 Deployment(5.3-app_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: v1
kind: Service
metadata:
name: app-tier
labels:
app: microservices
spec:
ports:
- port: 8080
selector:
tier: app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-tier
labels:
app: microservices
tier: app
spec:
replicas: 1
selector:
matchLabels:
tier: app
template:
metadata:
labels:
app: microservices
tier: app
spec:
containers:
- name: server
image: lrakai/microservices:server-v1
ports:
- containerPort: 8080
env:
- name: REDIS_URL
value: redis://$(DATA_TIER_SERVICE_HOST):$(DATA_TIER_SERVICE_PORT_REDIS)

与 4.3 相同的 Service + 环境变量服务发现,但 Pod 改为 Deployment 管理。


5.4 支持层 Deployment(5.4-support_tier.yaml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
name: support-tier
labels:
app: microservices
tier: support
spec:
replicas: 1
selector:
matchLabels:
tier: support
template:
metadata:
labels:
app: microservices
tier: support
spec:
containers:

- name: counter
image: lrakai/microservices:counter-v1
env:
- name: API_URL
value: http://app-tier.deployments:8080

- name: poller
image: lrakai/microservices:poller-v1
env:
- name: API_URL
value: http://app-tier:$(APP_TIER_SERVICE_PORT)

与 4.4 相同的 DNS 服务发现,但 Pod 改为 Deployment 管理。注意支持层没有 Service(不需要被其他组件访问)。DNS 中的命名空间从 service-discovery 变成了 deployments

懒猫微服实战入门(三十六):懒猫微服QEMU虚拟机快速上手

对于 NAS 玩家来说,虚拟机绝对是标配。今天我们要介绍的主角是 QEMU。你可能会觉得它太过底层、全命令行操作太硬核,但别担心,看过这篇文章之后,你就能轻松在懒猫微服上操作它。

在传统 Linux 下装 QEMU,你可能要折腾一堆 kvm-ok 检测、各种动态库依赖。但在懒猫微服上,直接从商店下载即可。这就是全容器化的好处:环境全封闭,不会把宿主机的依赖搞坏,不用再和底层依赖打交道,这就是懒猫微服全容器化的好处,彻底解决了让人头疼的环境问题。

image-20260226215103173

阅读更多

Easysearch 数据映射之 Deep Dive:我踩过的 Volume 坑

最近在用 Docker 部署 Easysearch,本以为是个简单的事情,结果在数据持久化上栽了跟头,每次停止再启动容器之后都会503,在后面成了我百思不得其解的问题,后来一直在某次的meetup中,请教了原厂的罗老师,一句话点醒梦中人,Easysearch用的具名卷,防止宿主机的数据覆盖容器里的数据。

阅读更多

超效率手册

  1. 周/日 计划

  2. 限制时间做事(30-90min) 紧迫感、

  3. 任务分解

  4. 短跑。起床坚持10分钟,其他事再坚持20分钟休息。 短跑30天习惯

  5. 日程校对,相信自己要做的事情。完全日程,避免过度工作和拖延。

  6. 自律。停下来之前再坚持10分钟。下次做事再坚持10-20%,专注一件事

  7. 语录刺激生产效率(便利贴

阅读更多
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×