2026-03-05 00:07:36
最近,新接手的一个 5 年前的 go 服务偶发性响应报错 “context deadline exceeded”,一旦有一个请求出现这个问题,所有向这个服务发送的请求都会返回超时错误,包括所有接口,且无法自行恢复,服务进程还存活但已经无法响应任何请求,彻底 hang 住了,只能重启服务。
服务所用的框架为 Echo,数据库为 PG,连接驱动为 jackc/pgx。
这个问题之前也算是碰到过,不过之前的服务用的是 GoFrame V1,当时已经排查出问题是出在数据库交互逻辑,因为日志中显示请求能正常收到,唯独是查询数据库没有结果输出,当时隐约猜到是事务逻辑出了问题,在处理请求开始,就先开启了事务,这一般都是不合理的,后面用最小化原则优化了事务,在真正执行写入操作时才开启事务,写入完成就关闭,优化事务操作之后,之前的那个服务就再没出过 “context deadline exceeded” 问题,但没有继续深究原因,只能说是技术人的直觉解决了这个问题。
现在又碰到了这个问题,趁着目前业务还不算忙,正好深入研究一下,由于有之前的经验,很快就定位了也是事务问题,代码写法和之前差不多,也是收到请求后,立马就开启了事务。起初以为是 Go 连接 PG 数据库驱动的通用问题,但一想,如果是通用问题,那这种 bug 应该早就有人反馈修复了。于是在 lib/pq 和 jackc/pgx 的 issue 上搜索,也算是运气好,正好看到个三周前的 issue,完美复现了这个超时现象,并且有大佬完美解释了这个问题。
issue 中示例代码已经很清楚了,这里再引用一下:
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
39 // 连接池初始化,连接池默认配置 MaxConns = 10
func New(dburl string) (*pgxpool.Pool, error) {
dbpool, err := pgxpool.New(context.Background(), dburl)
if err != nil {
return nil, fmt.Errorf("failed to connect db: %w", err)
}
return dbpool, nil
}
// Repository 结构体
type Repository struct {
pgclient *pgxpool.Pool
}
// 测试方法,相当于一次请求
func (repo *Repository) Test(i int) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 开启事务 - 获取一个连接
tx, beginErr := repo.pgclient.Begin(ctx)
if beginErr != nil {
return beginErr
}
defer func() {
if err != nil {
tx.Rollback(ctx)
return
}
tx.Commit(ctx)
}()
var c int
// 问题所在:使用连接池执行查询,而非事务对象
// 这会尝试获取另一个连接
err = repo.pgclient.QueryRow(ctx, "SELECT COUNT(*) FROM achievements").Scan(&c)
return err
}压测调用代码:
1
2
3 for i := 0; i < 10; i++ { // MaxConns = 10,恰好等于连接池大小
go repository.Test(i)
}
表面上看代码逻辑清晰:开启事务、执行查询、提交或回滚。但深入排查就会发现,这段代码会造成死锁,问题出在查询那行代码上——使用了 repo.pgclient.QueryRow() 而非 tx.QueryRow()。示例代码的 Test 方法需要两个连接才能完成操作:1、 repo.pgclient.Begin(ctx) —— 获取第一个连接,用于开启事务;2、 repo.pgclient.QueryRow(ctx, ...) —— 尝试获取第二个连接,用于执行查询。当 10 个 goroutine 同时执行 Test 方法时:
| 阶段 | 状态 | 连接池剩余 |
|---|---|---|
| 初始 | 连接池有 10 个连接 | 10 |
| 阶段一 | 每个 goroutine 调用 Begin(),各获取 1 个连接 | 0 |
| 阶段二 | 所有 goroutine 尝试 QueryRow(),需要再获取 1 个连接 | 等待中... |
每个 goroutine 都持有一个连接,等待另一个连接。但没有 goroutine 能释放连接——因为释放连接需要完成查询和事务,而完成查询需要另一个连接。这就是经典的死锁场景,相互等待。
该问题能稳定复现,主要原因也与 Go 调度器的行为有关。Go 调度器是协作式的,当一个 goroutine 执行网络 I/O(如向 PostgreSQL 发送 BEGIN)时,运行时会将其停放(park),让其他 goroutine 运行。这导致所有 goroutine 几乎同时执行到 Begin(),拿到第一个连接后同时被停放,唤醒时连接池已空,全部进入等待状态。
业务上最佳实践为:
当然,数据库连接驱动也可以进一步优化:
这个问题本质上不是 pgx 的 bug,而是对事务使用方式的误解。事务与特定的数据库连接绑定,事务内的操作应该在同一个连接上执行,这不是 pgx 特有的规则,几乎所有数据库事务都是如此。在使用事务时,牢记一个原则:事务内部永远使用事务对象的方法。
2026-03-01 21:02:19
还记得八年前通关「奥日与黑暗森林」之后,就想将奥日移植到自己的网页上,作为一个桌面宠物,当时还尝试用 unity 解包工具导出游戏中的资源,后来发现,导出也只是图片,缺少对应的数据,很难直接使用,遂作罢。
去年偶然发现一个帖子,有人做了个 Ori 桌宠,又在 B站 搜索了下,果然发现了原作者的演示视频和更好的下载链接,基本符合 Shaun 当初想做的事,把程序包下载并解压之后,发现了所有的动画图片资源,对 Shaun 来说,开发自己的 Ori 桌宠,最难的一步算是解决了。但由于去年接触到的新事情太多,没时间弄,又拖延了一年(都是借口 (╯‵□′)╯︵┴─┴),才真正开始启动。
原作者总共制作了 55 个动画,图片总资源占用 200M,对于网页端,这显然不能直接使用,得先处理一下图片。前期用 AI 做了下技术调研,最终选定用 TexturePacker 打包图片资源,用 PixiJS 引擎实现动画逻辑,在打包图片之前,用 AI 先拆分了图片资源,一个动画一个独立的文件夹存储对应的动画帧。这两个关键工具对 Shaun 来说都是完全陌生的,还好 TexturePacker 操作很简单,PixiJS 有 AI 辅助开发功能。全部动画帧图片打包之后,占用空间就相对比较小了,再用 TinyPNG 压缩了一下,在网页端就算能用了。
整体桌宠框架由 AI 迭代完成,Shaun 只是提出了自己的想法,所有动画逻辑支持配置化,一个原始动画可能产生多个动作(eg:翻转,旋转都是新的动作),AI 在一步步的追问中最终使用 ECS架构 完成了这个桌宠主体程序。Shaun 顺着这个基础架构针对每一个动画进行调试,设计实现了边界碰撞,点击,延时/随机动画序列等逻辑,这些逻辑设计上花的时间是最多的,毕竟全凭个人喜好,随意发挥,AI 目前还无法做这种决策。慢慢悠悠断断续续的花了一个月的休息时间,最终好歹在春节前完成了,算是送给自己的新年礼物。
最终成果为:Ori 桌面宠物
从拿到动画图片资源,到正式实现,拖延了一年,这一年,AI 的变化确实巨大,一年前让 AI 实现这个应该不会有这样顺利,拖延也算是有拖延的好处 :P。当然也碰到了很多必须要人工介入的问题,比如 PixiJS 版本不兼容问题,明明指定了版本,但有时生成的代码会冒出老版本的 api,导致报错,因为莫名其妙的原因还无法自我纠正,只能人工介入,重开对话,甚至需要人工调试,查阅文档,手动解决该问题。
在 AI 的使用中,也发现了,AI 与 AI 之间,差距也不小,这个 AI 不行,换个 AI 可能就解决了。多轮对话错误之后,最好是新开对话。语言描述能力对现在的 AI 非常重要,可预见的未来,干活模式会和现在完全不一样。
拥抱 AI,拥抱未来。
2025-03-22 23:52:12
最近,一个数据查询服务被业务方反馈拿不到数据,但接口响应是成功的,不报错,仔细排查后发现数据查询库用的是 pgx,但 pgx 返回的错误未被处理,导致服务接口没有响应错误。
在后续的排查过程中,发现这其实不算是 pgx 的问题,而是 database/sql 中的坑,所有涉及用 database/sql 查询的都需要显式处理 rows.Err()。
服务所用 pgx 版本为 4.10.1。查询函数主要用的是 QueryRow(返回一条数据) 和 Query(返回多条数据),更近一步的测试中(人为制造查询错误,eg:锁表)发现,调用 QueryRow 函数的接口,如果发生查询错误的问题,服务接口会正常响应错误。深入 pgx 源码发现,QueryRow 本质是对 Query 的进一步封装,对应的 Scan 函数源码为:
1 |
// QueryRow 返回的 connRow 对应继承自 Query 返回的 connRows 对象 |
从源码中可看出 QueryRow 的 Scan 函数有一系列的错误处理,而 Query 对应的 Scan 是更底层的函数,返回的仅是 Scan 过程中的错误,其他的错误需要在业务上层处理。Close 函数同样可能会出现错误,需要调用 rows.Err() 主动检查错误(这一步至关重要)。对于 Close 报的错,可以这样处理:
1 |
var err error |
数据库执行 sql 失败的错误(eg:canceling statement due to conflict with recovery),在 Close 后才会暴露出来,所以不处理这个错误,就不会返回错误,但数据又查不到,服务接口也表现为响应成功,导致上层业务误认为数据库里还真没数据。
最好的方式还是避免每次都手动 Scan,pgx 其实还提供了更上层的函数 QueryFunc,该函数封装了大部分错误处理:
1 |
func (c *Conn) QueryFunc(ctx context.Context, sql string, args []interface{}, scans []interface{}, f func(QueryFuncRow) error) (pgconn.CommandTag, error) { |
不过 QueryFunc 函数在新版本中(5.7.2)已被 ForEachRow 替代:
1 |
func ForEachRow(rows Rows, scans []any, fn func() error) (pgconn.CommandTag, error) { |
而 ForEachRow 的使用示例可以看这个函数:
1 |
func (c *Conn) getCompositeFields(ctx context.Context, oid uint32) ([]pgtype.CompositeCodecField, error) { |
对于一个不熟悉的底层库,最好的学习方式还是看它的示例代码,库的开发者很难知道用户会踩哪些坑,文档中自然不会有,毕竟当局者迷。只从文档出发,很容易陷进未知的坑里,甚至掉坑里都不知道,业务出问题后,花费大代价排查之后,才知道掉坑里了。陌生的开源库在使用的时候还是先全库 clone 下来,用 api 的时候,就去源码里搜一下,看看开发者写的示例(不管是测试,还是其他地方的调用),当然现在也可以让 AI 先写,人只要再核实一下文档和源码,能节省很多学习的功夫。
2025-01-26 23:26:15
出去玩看风景很大程度上受天气的影响,所以一般会使用「莉景天气」和「天文通」提前看下天气预报。莉景天气对于各种气象奇观都有明确的预报概率,但需收费;天文通免费,但只能根据其上的数据自行推断,Shaun 根据网上的一些资料和个人见识总结了一些奇观的出现条件。天气数据都有个准确率的问题,尤其是在高海拔地区,气象多变,数据仅供参考,所以下面列出的条件也都仅供参考,气象奇观出现是幸运,不出现才是正常的。
2025-01-12 16:47:28
最近看到这么一个工具——ollama-ocr,利用本地大模型直接进行 OCR,试用了一下,感觉效果还不错,联想之前看到的一个项目——PDFMathTranslate,感觉本地大模型已经非常成熟了,部署使用也越来越简单了。
如果需要提供 LLM 服务,还是用 vLLM 部署相对合适。
硬件:M3 MBP,10 核 GPU(Metal 3),16G 内存
系统&软件:macOS Sonoma 14.5,Ollama 0.5.4,AnythingLLM 1.7.2;
第一次启动 Ollama 时,会出现安装「command line」的引导界面,输入电脑用户密码安装即可,安装命令行之后,可以直接点击「Finish」,之后通过终端命令 ollama 操作 Ollama,毕竟 Ollama 没有提供 UI 界面,当然也有很多三方的界面。
之后在终端中直接运行 ollama run llama3.2:3b,等待模型拉取完成后,即可直接与模型对话。之后可以在 Ollama 官网 Models 搜索尝试更多模型。
命令解析:
Mac 中修改全局环境变量可通过 launchctl setenv 命令,eg: launchctl setenv OLLAMA_ORIGINS "*",允许 ollama 请求跨域。
Mac 中 ollama 拉取的模型文件默认放在 ~/.ollama/models 目录中,可通过修改 OLLAMA_MODELS 环境变量更改模型安装目录。
Ollama 默认服务地址端口是:127.0.0.1:11434,Mac 中查看进程监听的端口号命令为 lsof -nP -p <pid>。可通过修改 OLLAMA_HOST 环境变量更改默认端口,eg:launchctl setenv OLLAMA_HOST "0.0.0.0:6006"。
默认情况下,运行模型后,如果 5 分钟未与模型进行交互,将会自动停止该模型。
Mac 启动 Ollama 后,会在菜单栏上出现一个羊驼图标,但有时这个图标会被“刘海”挡住,导致无法退出 Ollama,这时可以使用 osascript -e 'tell app "Ollama" to quit' 命令退出 Ollama。
对于开发者,若需要更改默认端口,需修改环境变量:export OLLAMA_HOST=0.0.0.0:6006(如此可将 ollama 的默认端口修改为 6006),之后通过 ollama serve 命令启动 Ollama,通过该命令启动的不会在菜单栏上出现羊驼图标。若出现跨域问题,同样需要修改环境变量:export OLLAMA_ORIGINS="*"。
AnythingLLM 有两种安装模式,一种是桌面版,一种是 Docker 版,桌面版只能本地使用,Docker 版相当于是服务版,支持多用户云端使用。本次选用的桌面版,基本的 RAG 功能也都有。Docker vs Desktop Version
第一次启动 AnythingLLM 时,有一些设置引导,设置「LLM 偏好」时选择 Ollama,其他的都默认即可。创建工作空间之后就可以上传本地文件,建立自己的知识库。
Shaun 在使用中感觉,AnythingLLM 响应还是比较慢,分析/提炼/归纳/总结本地文档的速度有限。可能是机器配置还是有点低了。
命令行使用:
1 |
用 ollama 将本地文件 Black Hat Rust.pdf 从英文翻译为中文 |
出现 NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3' 警告时可忽略,不影响使用。
如果出现 huggingface_hub.errors.LocalEntryNotFoundError 错误,需要配置 hugging_face 国内镜像:
1 |
pip3 install -U huggingface_hub hf_transfer -i https://pypi.tuna.tsinghua.edu.cn/simple |
同时下载模型到本地(需要具体看报错的是哪个模型,这里是wybxc/DocLayout-YOLO-DocStructBench-onnx):
1 |
huggingface-cli download --resume-download wybxc/DocLayout-YOLO-DocStructBench-onnx --local-dir . |
选择本地ollama模型作为翻译服务的话,需要配置环境变量:
1 |
本地ollama服务地址 |
有个浏览器插件「沉浸式翻译 - Immersive Translate」同样是个比较好用的翻译工具。不过对于 PDF 文件,都有可能出现译文重叠的现象,需要二次编辑一下,或者将 PDF 格式转换为其他格式(eg:html,epub 等,相关 issue)。
Continue 插件可结合本地的 ollama 使用 qwen2.5-coder:7b 模型,可辅助读/写代码,需打开以下设置:
模型设置添加:
1 |
{ |
自动补全设置中 apiKey 保持为空字符串就行。
若需要使用远程部署的 ollama 服务,可以新增参数 "apiBase": "http://<my endpoint>:11434"。
Cline 插件同样可结合本地的 ollama 使用,可辅助 CR 以及自动化优化修改代码。如果使用 vLLM 部署的 AI 服务,API Provider 选择 OpenAI Compatible,Bsae URL 填 http://ip:port/v1,API Key 随便填就行(eg: ollama),Model ID 则是模型名称(eg: deepseek-r1:14b)。
20250207 更新:
本地部署 LLM 的好处在于无数据泄漏问题,对于个人使用而言,轻量级的模型也差不多够用了,但即使已经轻量化了,本地运行大模型还是有点吃力,在 Shaun 的电脑上运行 phi4:14b 略显勉强(8 tok/s)。Mac 的内存和显存是共享的,后续如果买新的,有部署 LLM 的需求,最好把内存拉满,由于模型文件也相对较大,有条件的可以把 SSD 也拉满。希望后续大模型的推理能够进一步轻量化,效果也更好,真正实现人人都能使用。
自 GPT-3 出现以来,也就短短 4 年不到,从大规模的高性能 GPU 集群到单机部署,从胡言乱语到精准命中,各行各业都迎来了 LLM 的冲击,在可预见的未来,LLM 将深刻影响到每一个人,这种影响无关好坏,单纯只是时代的浪潮,LLM 将和操作系统,数据库一样,成为整个 IT 行业的基础设施,就 Shaun 而言,应该很难亲自动手去开发优化 LLM,能做的也就是尽可能的熟练使用。
20260121更新:
2024-12-22 22:26:19
工作中偶尔会遇到一些特殊需求需要解决,这里记录一下。
使用 setfile 命令:
修改创建日期:
setfile -d "mm/dd/yy hh:mm:ss" filename修改修改日期:
setfile -m "mm/dd/yy hh:mm:ss" filename同时修改 xxx.txt 文件两个时间为 2023-07-27 01:23:53:
setfile -d "07/27/2023 01:23:53" -m "07/27/2023 01:23:53" ./xxx.txt
word 和 excel 本质上都是 zip 文件,可利用 openpyxl 修改 xlsx 文件元信息创建时间。对于 xls 文件,若文件有密码,需先去除密码,再将 xls 转换为 xlsx 文件,之后使用 openpyxl 修改时间。具体步骤如下:
用 AppleScript 将 xls 转换为 xlsx
1 |
def run_applescript(script): |
修改 xlsx 文件创建时间
1 |
import openpyxl |
将 xlsx 转换为 xls 文件
1 |
def xlsx_to_xls(file_path="./xxx_tmp.xlsx"): |
利用 pillow 和 piexif 修改 jpg 文件 exif 信息时间
1 |
from PIL import Image |
Pillow 保存 jpg 图片默认会同时保存 JFIF 和 EXIF 头,若需要去掉 JFIF 头,需修改 Pillow JpegEncode.c 文件源码:
1 |
if (context->xdpi > 0 && context->ydpi > 0) { |
修改为:
1 |
context->cinfo.write_JFIF_header = FALSE; |
之后执行:python3 -m pip -v install . 从本地源码安装 Pillow。
使用 pikepdf 修改 pdf 文件元信息时间
1 |
import pikepdf |