2025-10-28 16:39:15
今天线上服务器通过 smtp.gmail.com 发送邮件时,持续报错:
fail to send email: 421 4.7.0 Try again later, closing connection.
比较奇怪的是:
有问题的服务器上测试:
# nc -v smtp.gmail.com 587
Connection to smtp.gmail.com (2404:6800:4003:c05::6c) 587 port [tcp/submission] succeeded!
421 4.7.0 Try again later, closing connection.
而正常的服务器上测试:
# nc -v smtp.gmail.com 587
Connection to smtp.gmail.com 587 port [tcp/submission] succeeded!
220 smtp.gmail.com ESMTP a640c23a62f3a-b6d85372e5asm1014513766b.32 - gsmtp
比较奇怪的是,有问题的服务器上,nc 命令连接到的 IP 地址是 IPv6 地址,而正常的服务器上,连接到的是 IPv4 地址。
参考 stackoverflow 上的建议(这个老哥的测试真是细致),换成 IPv4 地址试试:
https://stackoverflow.com/questions/63375834/gmail-421-4-7-0-try-again-later-closing-connection
在 /etc/hosts 文件中添加一行:
108.177.15.109 smtp.gmail.com
再次使用 nc 命令测试:
# nc -v smtp.gmail.com 587
Connection to smtp.gmail.com (108.177.15.109) 587 port [tcp/submission] succeeded!
220 smtp.gmail.com ESMTP ffacd0b85a97d-429952d9e80sm17755367f8f.28 - gsmtp
此时,连接成功,没有再出现 421 错误。
然后在 golang 代码和 PHP 代码中测试,也都能成功发送邮件了。🥲
虽然通过修改 /etc/hosts 文件解决了问题,但这并不是一个根本的解决方案。 因为 Gmail smtp 服务器可能会更改其 IP 地址,导致 /etc/hosts 文件中的记录失效。
我查了一下,可以通过系统级的配置,强制让系统使用 IPv4 地址。但是这会影响到所有的服务,感觉不太合适。 因为之前遇到过有的服务商只支持 IPv6 地址访问。参考前文:
docker 误朕,Joomla 发送邮件超时失败,原因竟是 docker 默认不支持 IPv6
参考这个帖子:
https://stackoverflow.com/questions/65377421/django-force-ipv4-for-email-host-gmail-421-4-7-0-try-again-later-closing
My IPv6 reverse DNS did not resolve. That was the problem. After adding my ipv6 address to the DNS things worked.
但是我查了半天还是没有理解这跟 reverse DNS (rDNS)有什么关系? 😅 按理说我是利用 gmail 账号发送邮件的,跟我的 IPv6 地址有什么关系呢? 难道是 IPv6 地址被 ban 了?检查了半天日志,发现发送的邮件也不多,一天不到 20 封,应该不会被 ban 吧 ……
今天先不查了,后续有时间再研究吧。
2025-10-27 16:16:48
继续前文 使用 gobackup 自动将 MySQL 数据库备份到七牛云私有空间,在本地测试了 gobackup 的基本功能之后,今天在几台线上服务器,包括阿里云和腾讯云的各种服务器,依次部署了 gobackup 备份服务。这里记录一下一些踩坑的地方。
用 163 邮箱设置了邮件通知,测试失败日志如下:
2025/10/27 09:32:58 [Notifier] Running 1 Notifiers
2025/10/27 09:33:18 [Notifier] EOF
从 github issue 看,似乎是不支持 TLS:
When send mail via port 465, the error EOF will occur.
https://github.com/gobackup/gobackup/pull/288
对提交看是支持了的,但是我测试了几次,都是 EOF 错误。
所以,我决定,还是换成 webhook 方式吧,我在自己服务器上写一个接收接口,然后在自己的服务器上发邮件通知。
https://gobackup.github.io/configuration/notifiers/webhook
只需要处理 gobackup 发过来的 json 数据即可,格式如下:
{
"title": "This is title",
"message": "This is message"
}
果然,改成 webhook 方式后,测试成功了。邮件格式如下图:

但是,如果每天备份成功都发邮件的话,未免太多了。
The on_success, on_failure option (default: true) for toggle when backup has been successfully or failed.
可以独立设置成功和失败的通知开关:
on_success: true
on_failure: true
还是都开启吧。我担心如果真失败了,而且邮件服务也出问题了,那就麻烦了。
专门用于存储数据库备份文件,并且不允许外部访问下载。选了个离我最近的机房,对应的 region id:
不能给独立的空间设置 access key 和 secret key,这个地方的体验不太好。
yaml 的注释是用 # 符号开头的,后面的内容都会被忽略掉。
可以方便的在配置文件中添加注释,解释每个配置项的作用和用法。或者存档一些废弃的配置项,以备后续参考。
两个位置任选其一,如下:
~/.gobackup/gobackup.yml
/etc/gobackup/gobackup.yml
感觉第一种更方便一点。毕竟也用不到 root 权限,直接使用非 root 用户即可。
注意需要把文件权限修改,防止其他用户读取:
chmod 600 gobackup.yml
将本地的 gobackup 可执行文件 scp 上传到服务器上即可。
做成 Makefile 任务更方便一些。
.PHONY: deploy_gobackup
deploy_gobackup:
scp -P $(SSH_PORT) /usr/local/bin/gobackup $(SERVER_USER)@$(SERVER_HOST):~/
然后登录服务器,移动到 /usr/local/bin 目录下即可。
sudo mv gobackup /usr/local/bin/
确认
$ which gobackup
/usr/local/bin/gobackup
参考:
https://github.com/gobackup/gobackup/issues/193
my_db_default: &my_db_default
type: mysql
host: localhost
port: 3306
username: root
password: root
args: --single-transaction --quick
models:
my_app:
databases:
my_db1:
<<: *my_db_default
database: my_db1
my_db2:
<<: *my_db_default
database: my_db2
my_db3:
<<: *my_db_default
database: my_db3
yaml 的配置还是太灵活了,怪不得 rasa 也是用的 yaml 配置。
https://gobackup.github.io/configuration#webconfig
没有禁用 web 功能,所以,干脆设置一个非常复杂的账号和密码吧。
同时阿里云的防火墙也阻止了 2703 端口的访问。
甚至直接不用 gobackup start 服务,改成 crontab 系统定时任务。
crontab -e
每天凌晨 0 点跑一次:
0 0 * * * /usr/local/bin/gobackup perform >> ~/.gobackup/gobackup.log
Ubuntu 系统下,可以查看系统日志:
less /var/log/syslog
搜索 cron
Oct 27 14:41:01 localhost CRON[2698]: (ubuntu) CMD (/usr/local/bin/gobackup perform >> ~/.gobackup/gobackup.log)
看到 gobackup 的执行日志:
2025/10/27 14:41:01 [Config] Load config from default path.
2025/10/27 14:41:01 [Config] Load gobackup config failed: While parsing config: yaml: control characters are not allowed
2025/10/27 14:41:01 [Config] While parsing config: yaml: control characters are not allowed
这个问题,很诡异,发现 yaml 配置文件并没有任何问题。改成 root 用户的 crontab 后,就可以正常执行了。 看起来并不是权限的问题。而且不同用户的加载目录不同,而非 root 用户的目录下有一个我没有删除的 gobackup 可执行文件, 估计是把这个文件当成配置文件去加载了,导致报错。删除掉这个文件后,就可以正常执行了。
2025-10-25 16:55:44
现在后台开发和运维都是我一个人,所以服务器上的 MySQL 数据库备份也只能自己来做了。 由于服务分散在多台服务器上,并且有几十个数据库需要备份,手动备份实在是太麻烦了。 需要一套自动化的备份流程。定时将数据库导出,并存储到云存储空间中,例如七牛云存储或者阿里云 OSS 上。
我昨天纠结了半天,到底用什么方案或者工具来做数据库的自动备份:
就是下面这个 gobackup 项目:
https://github.com/gobackup/gobackup
Ruby China 社区的服务器就是用 gobackup 来备份数据库的,然后上传到阿里云 OSS 上。 哈哈,还开发个球,直接用现成的工具就行了,省时省力,不重复造轮子 🤣
其实就是调用 mysqldump 命令来导出数据库,然后把导出的 SQL 文件打包压缩,最后上传到云存储空间。
有些情况下,MySQL 被安装到了 Docker 容器中,这种情况下,如何通过 gobackup 来备份 MySQL 数据库呢?
从 gobackup 的文档看,官方有提供 docker 镜像:
https://gobackup.github.io/docker
We builded a Docker image for GoBackup. You can use it to run GoBackup in a container.
参考:
https://hub.docker.com/r/huacnlee/gobackup
Dockerfile 内容:
RUN |1 VERSION=latest /bin/sh -c apk add curl ca-certificates openssl postgresql-client mariadb-connector-c mysql-client mariadb-backup redis mongodb-tools sqlite tar gzip pigz bzip2 coreutils lzip xz-dev lzop xz zstd libstdc++ gcompat icu tzdata && rm -rf /var/cache/apk/* # buildkit
看来是在 docker 容器中安装了 mysql-client,然后通过 mysqldump 来备份 mysql 数据库的。
curl -sSL https://gobackup.github.io/install | sh
说实话,我对这种直接执行安装脚本的方式,非常担心,生怕里面有 bug 导致服务器文件被删。。。 所以,我大致看了一下安装脚本的脚本逻辑,发现它其实就是下载 gobackup 的二进制文件,然后放到 /usr/local/bin 目录下。
https://github.com/gobackup/gobackup/blob/main/install
也可以 release 页面下载对应的二进制文件 gobackup-linux-amd64.tar.gz (仅有 10M),然后解压到 /usr/local/bin 目录下即可。
安装完成,确认:
> gobackup --version
gobackup version 2.16.0
先用开发机本地的数据库做测试,确认 gobackup 能正常备份
首先创建目录:
mkdir ~/.gobackup
创建配置文件:~/.gobackup/gobackup.yml
models:
my_app:
compress_with:
type: tgz
storages:
local:
type: local
keep: 20
path: /data/backups
databases:
my_app:
database: myproject_db
type: mysql
host: localhost
port: 3306
username: xxx
password: password
archive:
includes:
- /var/www/my_app/uploads
- /var/www/my_app/shared/ssl
这个还挺贴心的,还支持把指定的目录一起打包归档。非常适合服务器的一些配置文件和上传文件的备份。确实有经验。
> gobackup run
2025/10/25 14:00:26 [Config] Load config from default path.
2025/10/25 14:00:26 [Config] Other users are able to access /home/xxx/.gobackup/gobackup.yml with mode -rw-r--r--
2025/10/25 14:00:26 [Config] Config file: /home/xxx/.gobackup/gobackup.yml
2025/10/25 14:00:26 [Config] Config loaded, found 1 models.
2025/10/25 14:00:26 [API] You are running with insecure API server. Please don't forget setup `web.password` in config file for more safety.
2025/10/25 14:00:26 [API] Starting API server on port http://0.0.0.0:2703
浏览器访问 http://localhost:2703 能看到 gobackup 的 Web 界面。感觉有点鸡肋啊。 需要点击页面里的执行备份按钮,才能执行备份任务。

这个执行测试还行,生产环境中,我不需要这个 Web 界面,得看看怎么禁用掉。 找到了方法,使用命令行方式执行备份任务即可:
gobackup perform
这样就是执行一次备份任务,并且不启动 Web 界面。
执行了两次备份任务,并且将其中一次备份做了解压缩,查看 /data/backups 目录的结构:
> tree /data/backups/
/data/backups/
├── 2025.10.25.14.05.39.tar.gz
├── 2025.10.25.14.14.14.tar.gz
└── my_app
├── archive.tar
└── mysql
└── my_app
└── myproject_db.sql
3 directories, 4 files
可以看到:
解压确认了一下,文件内容都是完整的。
参考:
https://gobackup.github.io/schedule
有两种方式:
因为每个 model 可以设置独立的 schedule 配置,所以可以实现不同的备份时间粒度。 例如,重要的数据库,可以设置为每小时备份一次;不太重要的数据库,可以设置为每天备份一次; 有的一个月备份一次都可以。
即便是 perform 命令,也是可以指定 model 名称的, 参考 help:
> gobackup -h perform
NAME:
gobackup perform
USAGE:
gobackup perform [command options] [arguments...]
OPTIONS:
--model value, -m value [ --model value, -m value ] Model name that you want perform
--config value, -c value Special a config file
--help, -h show help (default: false)
配置文件中,添加七牛云的存储配置,替换掉 local 存储配置:
models:
my_app:
compress_with:
type: tgz
storages:
kodo:
type: kodo
keep: 2
bucket: private-bucket-name
region: cn-south-1
path: backups/myproject_db
access_key_id: <七牛的 Access Key>
secret_access_key: <七牛的 Secret Key>
storage_class: LINE # 可选参数,默认 STANDARD,这个 LINE 是低频存储,节省成本
databases:
my_app:
database: myproject_db
type: mysql
host: localhost
port: 3306
username: xxx
password: password
archive:
includes:
- /var/www/my_app/uploads
- /var/www/my_app/shared/ssl
主要是为了节省云存储的成本,参考官方文档:
https://gobackup.github.io/configuration/storages#cycling
可以通过 keep 参数,来限制备份文件的保留数量,超过数量的备份文件会被自动删除。
不知道云存储的备份文件,是否也会被自动删除?需要测试一下。测试了一下确实可以,例如 keep: 3,上传到 S3 的备份文件也只会保留最新的 3 个,超过的会被删除。
如果用 perform 命令执行备份任务,日志中会有类似如下的删除备份文件的日志:
2025/10/25 15:43:19 [Cycler] Removed 2025.10.25.15.41.10.tar.gz
到此,基本功能算是测试完成了。其他的通知功能,openssl 加密功能,暂时用不上, 先不测试了。
今天周六,还是不着急上线到生产环境,怕出问题,等下周一再说吧。
目前 gobackup 官方并没有支持 Linode Bucket 的存储方式,只有 AWS S3 兼容的存储方式。
得测试一下 gobackup 是否能通过 S3 兼容的方式,来上传到 Linode Bucket 上。
2025-10-22 10:59:48
最近在开发公司内部使用的人事系统,发现手动测试非常麻烦。每次改动代码后,都需要手动打开浏览器,点击各种按钮,填写各种表单,确认功能是否正常。这个过程既耗时,又容易出错。
同时,我自己的网站也有类似的需求。写了一堆功能,每次上线都心惊胆战,生怕修改了一处代码,导致其他功能出错。而自动化测试可以大大降低这种焦虑感。尤其,现在好多功能都是用 AI 来实现的,我经常性的不看 AI 的具体实现代码,而是看它的输出结果是否符合预期。这种情况下,自动化测试就显得尤为重要。
我看现在好多人推荐使用 Playwright,而且是微软出品的,感觉应该还不错。于是决定尝试一下。
从 Playwright 官网的文档看,官方支持了多种语言,包括 JavaScript/TypeScript (Node.js)、Python、Java、C#(.NET)。看起来默认主推的是 TypeScript。

但是,我感觉后续如果要集成 AI 接口之类,还是 Python 更方便一些,所以我选择了 Python 版本的 Playwright。其次,我对 Node.js 和 TypeScript 都厌恶至极,所以也不想用这个生态。十一期间,听了一个 Ruby on Rails 作者的播客,发现其也对 TypeScript 充满厌恶,看来这种情绪还是挺普遍的。我宁愿直接用 JavaScript,也不想用 TypeScript。好了,抱怨到此为止,继续正题。
我对比了一下 Python 和 TypeScript 版本的 Playwright 文档,发现 API 基本一致,差别不大。功能也没有分别。所以,选择 Python 版本的 Playwright 没有任何问题。
https://playwright.dev/python/docs/intro
安装非常简单,就两行命令:
pip install pytest-playwright
playwright install
注意,如果是 Windows 上,建议不要使用 WSL1 下安装,毕竟没法运行肉眼可见的浏览器。(感觉有时候调试,还是用可见的直观一些)。 还是用 PowerShell 安装比较好。
以测试我自己的博客网站为例。
需要新建一个以 test_ 开头的 Python 文件,比如 test_blog.py。
注意,不要用 main.py 之类的名字,否则 pytest 无法识别。
pytest 只执行以 test_ 开头的 python 文件。
测试代码如下:
import re
from playwright.sync_api import Page, expect
# 打开首页,确认标题正确
def test_has_title(page: Page):
page.goto("https://www.sunzhongwei.com/")
# Expect a title "to contain" a substring.
expect(page).to_have_title(re.compile("大象笔记"))
# 点击关于我链接,确认跳转正确
def test_get_started_link(page: Page):
page.goto("https://www.sunzhongwei.com/")
# Click the about me link.
page.get_by_role("link", name="关于我").click()
# Expects page to have a heading with the name of About Me.
expect(page.get_by_role("heading", name="自建博客")).to_be_visible()
> pytest
==== test session starts ====
platform win32 -- Python 3.11.9, pytest-8.4.2, pluggy-1.6.0
rootdir: D:\work\nonews
plugins: anyio-3.7.1, base-url-2.1.0, playwright-0.7.1
collected 2 items
test_blog.py ..
==== 2 passed in 7.97s ====
除了初始化慢点,需要几秒钟,后续的测试都很快。
如果执行失败,会看到具体的错误信息:
page = <Page url='https://www.sunzhongwei.com/'>
def test_has_title(page: Page):
page.goto("https://www.sunzhongwei.com/")
# Expect a title "to contain" a substring.
> expect(page).to_have_title(re.compile("Playwright"))
E AssertionError: Page title expected to be 're.compile('Playwright')'
E Actual value: 大象笔记 - Notes of Elephant Leg
E Call log:
E - Expect "to_have_title" with timeout 5000ms
E 9 × unexpected value "大象笔记 - Notes of Elephant Leg"
test_blog.py:8: AssertionError
对于点击操作,如果不符合预期,会看到:
Expect "to_be_visible" with timeout 5000ms
非常的人性化,会自动等待元素出现,直到超时为止。
在服务器 Nginx 日志中,看了一下请求头:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit /537.36 (KHTML, like Gecko) HeadlessChrome/140.0.7339.16 Safari/537.36
这个 Headless Chrome 是 Chrome 浏览器的无界面形态,可以在不打开浏览器的前提下,使用所有 Chrome 支持的特性运行你的程序。相比于现代浏览器,Headless Chrome 更加方便测试 web 应用,获得网站的截图,做爬虫抓取信息等。
代码简单易懂,安装也方便。下一步试试用这个写个爬虫,抓取一些网页数据。
2025-10-17 20:52:28
大概是上个月,我发现国内已经不能在 vscode GitHub copilot 中使用 claude 相关的模型了。对我来说,不太影响日常的搬砖性的 crud 开发任务。但是今天,遇到了出设计稿的任务,想让 ai 出一版平板 app 的设计稿。之前用 claude 给出的效果非常赞。参考前文 Claude 3.7 生成 APP/网站 UI 设计稿的提示词。
没有 claude 的情况下,我用 gpt 5 codex 和 gemini 2.5 pro 分别除了一版设计稿,效果惨不忍睹。而且让 gemini 微调,也是一直嗯嗯嗯,就是不改。无法忍受🤣 不得不请回 claude了。
网上查了一下,方法很简单,只要不是国内的 ip 就能继续使用 claude。于是,我打开 vscode 的左下角设置,选择 settings
在搜索框里输入 proxy,找到 http proxy 配置项,输入自己的本地代理地址。例如,我的是 socks5。这样就搞定了
例如:
socks5://localhost:4455
再次打开 vscode,在聊天窗口,就能 claude 相关的模型了。

我用 claude 4.5 使用了跟 gpt codex 和 gemini 一样的提示词。还得是 claude,ui 效果非常赞,逻辑清晰,审美在线。唯一的缺点是,让她用中文输出界面,非得给我用英文,我有用 gpt 5 mini 修复了一轮才解决。再就是很容易出现返回超过长度限制的问题。但是这些都能接受,毕竟效果是真的好。
2025-10-14 15:25:00
用 AI 生成的网页代码,如果使用了 tailwindcss V4 的 CDN 版本,会看到浏览器控制台报了一段警告信息:
cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation
奈何 AI 用 tailwindcss 生成的页面就是比用 bootstrap 的好看,不得不解决一下。
参考 github 上的这个 issue:
https://github.com/tailwindlabs/tailwindcss/issues/18731
The warning is there on purpose. The v3 CDN build is not intended for production use. It's meant for prototyping during development.
Certain things like transitions may not function properly in cases where new, unseen classes are added to an element. DOM updates are batched in the browser meaning that we won't get notified until after the transition has "started" and as such its possible for a transition to not animate in the first time.
大致意思就是,CDN 版本在生产环境中可能会导致一些问题,例如元素上动态添加的类。
但是,我觉得主要原因是,CDN 版本会加载所有的样式,文件大,而项目实际使用的样式很少。所以需要使用 PostCSS 插件或者 Tailwind CLI 来生成按需加载的 CSS 文件。
简单看了一下 PostCSS 的介绍,感觉还是太复杂了(不想花时间在前端复杂度上),不如直接使用 Tailwind CLI 来生成 CSS 文件。
其实,我连 Tailwind CLI 都不想用,要不是感觉浏览器终端里有警告信息显得不专业,我都想继续用 CDN 版本。
Tailwind CSS works by scanning all of your HTML files, JavaScript components, and any other templates for class names, generating the corresponding styles and then writing them to a static CSS file.
即,Tailwind CLI 会扫描你的 HTML 文件、JavaScript 组件和其他模板中的类名,生成相应的样式,然后将它们写入一个静态 CSS 文件。
npm install tailwindcss @tailwindcss/cli
也可以不使用 node,直接下载预编译的二进制文件。但是 tailwindcss-windows-x64.exe 文件大小 127M,感觉太大了,算了。
在哪里执行呢?我是在 golang gin 项目根目录执行的。执行完成后,会看到根目录下多了一个 node_modules 目录(需要添加到 .gitignore 中), 以及一个 package.json 文件,内容如下:
{
"dependencies": {
"@tailwindcss/cli": "^4.1.14"
}
}
> npx @tailwindcss/cli
≈ tailwindcss v4.1.14
Usage:
tailwindcss [--input input.css] [--output output.css] [--watch] [options…]
Options:
-i, --input ················· Input file
-o, --output ················ Output file [default: `-`]
-w, --watch[=always] ········ Watch for changes and rebuild as needed, and use `always` to keep watching when stdin is closed
-m, --minify ················ Optimize and minify the output
--optimize ·············· Optimize the output without minifying
--cwd ··················· The current working directory [default: `.`]
--map ··················· Generate a source map [default: `false`]
-h, --help ·················· Display usage information
npx @tailwindcss/cli -i public/css/style.css -o public/css/output.css --cwd=templates
输出提示:
≈ tailwindcss v4.1.14
Done in 136ms
需要注意 --cwd 参数,指定扫描的目录。因为我的 HTML 模板文件都在 templates 目录下。 而我的 public 目录也放到了 templates 目录下,所以 public/css/style.css 这个输入文件路径是相对于 templates 目录的。 这里比较特殊,跟正常的项目结构不太一样。没办法,我用 golang gin 感觉这样方便调试。
$ tree templates/
templates/
├── product.html
├── products.html
├── public
│ ├── css
│ │ ├── output.css
│ │ └── style.css
│ ├── images
│ │ ├── default_avatar.png
│ │ ├── image_404.jpg
│ └── js
│ └── main.js
└── thanks.html
不压缩的话,output.css 文件大小 33K. 如果加上 --minify 参数:
npx @tailwindcss/cli -i public/css/style.css -o public/css/output.min.css --cwd=templates --minify
减少为 25K。
好了,大功告成。下面是记录的一些报错信息,可以忽略。
npx tailwindcss init
报错:
npm ERR! could not determine executable to run
网上说是因为 V4 版本会出现这个问题,降级到 V3 版本就可以了。但是,我想用 V4 版本啊。
V4 直接在命令参数里指定 cwd 参数就可以了。其实也用不上 init 命令。
> node --version
v16.14.2
是 Node 的版本问题:structuredClone() was added in Node.js 17.0.0 (Sept 2021) 需要高版本的 Node.js 才支持 structuredClone()。
$ nvm install 17
$ node -v
v17.9.1