从 frp 暴露端口,到泛域名 + Caddy,再到 frp Host 分流:一次内网穿透架构的升级

最近我在折腾一个很实际的问题:

我有一台内网机器,上面跑着很多服务,每个服务都有自己的端口。现在我想把其中某些服务暴露到公网,让外部可以访问。

这个需求很常见。

比如我家里或者局域网里有一台 Ubuntu 机器,上面跑着:

MCP 服务:8120
API 服务:8122
测试后台:8080
某个 Go 服务:9000
某个 Python 服务:7860

这些服务本身都在内网里,外部是访问不到的。

如果机器没有公网 IP,或者路由器不方便做端口映射,那传统方式就很麻烦。于是我用了 frp。

一开始我对 frp 的理解还比较简单:

它就是一个内网穿透工具,可以把内网端口暴露到公网服务器上。

但真正跑通之后,我对它的理解一步一步升级了。


一、frp 的核心逻辑:公网服务器帮内网机器“代收连接”

frp 的结构其实很清晰:

外部用户
   ↓
公网服务器某个端口
   ↓
frps
   ↓
frp 隧道
   ↓
frpc
   ↓
内网真实服务

公网服务器上跑的是 frps,内网机器上跑的是 frpc

最关键的一点是:

内网机器是主动连接公网服务器的。

这就绕开了一个非常麻烦的问题:内网机器不需要公网 IP,也不需要在家里路由器上做端口映射。

只要内网机器能主动访问公网服务器,frpc 就能和 frps 建立连接。之后外部访问公网服务器的某个端口,frps 就可以通过这条连接把流量转回内网机器。

比如:

公网服务器:18120  ->  内网机器:8120

这个过程看起来像是外部直接访问了内网服务,但实际上中间是公网服务器在帮忙转发。

这个设计非常优雅。


二、真正让我觉得优雅的是:新增服务时,frps 通常不用改

一开始我以为,每暴露一个新服务,公网服务器上的 frps 也要改配置。

后来实际搞了一下发现:

大多数情况下,新增端口映射只需要改 frpc,frps 根本不用动。

这点非常爽。

frps 更像一个已经部署好的“中转中心”。它只需要提前启动,并监听一个控制端口,比如:

frps:7000

之后内网机器上的 frpc 可以不断声明:

我要把本地 8120 映射到公网服务器 18120
我要把本地 8122 映射到公网服务器 18122
我要把本地 8080 映射到公网服务器 18080

frps 收到这些注册后,就负责监听对应的端口,并把流量转回去。

也就是说,公网服务器上的 frps 是一个相对稳定的入口。后续新增什么服务,主要由内网侧的 frpc 决定。

这个时候我第一次感觉到:

frp 不只是“穿透工具”,它更像是一个内网服务注册到公网入口的隧道系统。


三、第一阶段:每个服务暴露一个公网端口

最直观的做法是这样:

公网服务器:18120 -> 内网 MCP 服务:8120
公网服务器:18122 -> 内网 API 服务:8122
公网服务器:18080 -> 内网后台服务:8080

访问的时候就是:

http://公网IP:18120
http://公网IP:18122
http://公网IP:18080

这个方式非常直接,适合临时调试,也适合自己用。

但是用着用着就会发现一些问题。

1. 每加一个服务,都要去服务器控制台开放端口

比如你新增一个服务,要用 18123,那你不仅要改 frpc,还要去云服务器安全组或者防火墙里开放 18123

服务一多,这个操作很烦。

2. 端口越来越多,很难管理

一开始几个端口还好,后面可能变成:

18080
18120
18121
18122
18123
19000
19001

时间长了就容易忘:

18120 是哪个服务?
18122 又是哪个服务?
这个端口还在用吗?

这对于我这种一堆 Go/Python/Docker 服务,每个服务一个端口的模式来说,迟早会变混乱。

3. 业务端口直接暴露在公网,不够干净

虽然 frp 很方便,但如果每个业务端口都直接暴露到公网,本质上就是把这些端口都变成了公网入口。

如果暴露的是普通 Web 服务还好,如果不小心暴露了:

Redis
PostgreSQL
后台管理
无鉴权 API
MCP 服务
Docker API

那风险就很高。

所以第一阶段的方案虽然能用,但不是最优雅的长期方案。


四、第二阶段:用 Caddy 接管公网入口

后来我突然想到一个问题:

我的公网服务器上本来就有 Caddy,也有自己的域名。

既然 Caddy 本来就可以根据域名做反向代理,那我是不是可以不直接暴露每个 frp 业务端口,而是让 Caddy 来统一接收公网请求?

也就是说,外部访问:

https://mcp.dev.weini.xin

然后 Caddy 根据域名转发到服务器本机的某个端口:

127.0.0.1:18120

这个 18120 再通过 frp 回到内网的真实服务:

内网机器:8120

最终链路就变成:

用户访问 mcp.dev.weini.xin
        ↓
公网服务器 Caddy:443
        ↓
reverse_proxy 127.0.0.1:18120
        ↓
frps/frpc 隧道
        ↓
内网机器:8120

这一下架构就清晰了。

公网入口只需要 Caddy 负责,frp 业务端口不再直接暴露给外部。

这个阶段相比第一阶段已经进步很多。

公网不再到处开放业务端口,而是统一通过:

80
443

对外提供服务。


五、真正点睛之笔:泛域名解析

不过这里还有一个问题:

如果每新增一个服务,我都要去 DNS 控制台新建一个子域名,那还是麻烦。

比如:

mcp.dev.weini.xin
api.dev.weini.xin
test.dev.weini.xin
admin.dev.weini.xin

如果每个都手动添加 DNS 解析,也挺烦。

然后就想到了泛域名解析:

*.dev.weini.xin  ->  公网服务器 IP
*.test.weini.xin ->  公网服务器 IP

这一下就彻底舒服了。

有了泛域名解析之后,只要是:

xxx.dev.weini.xin

都会自动解析到我的公网服务器。

比如:

mcp.dev.weini.xin
api.dev.weini.xin
ghost.dev.weini.xin
demo.dev.weini.xin
anything.dev.weini.xin

这些域名都不需要单独配置 DNS。

DNS 这一层只负责一件事:

只要是 *.dev.weini.xin,都指向我的公网服务器。

至于具体哪个子域名转发到哪个服务,则交给 Caddy 决定。


六、DNS 这次突然变得很有存在感

说实话,以前我对 DNS 的感觉很普通。

以前我理解 DNS,大概就是:

把一个域名解析到一个 IP

比如:

weini.xin -> 服务器 IP
www.weini.xin -> 服务器 IP

它更像是一个“域名和 IP 的映射表”。有用是有用,但平时感知并不强。

这次搞 frp、Caddy、泛域名之后,我第一次明显感觉到:

DNS 不是一个简单的配置项,它其实是整个公网入口体系的第一层。

尤其是泛域名解析:

*.dev.weini.xin  ->  公网服务器 IP
*.test.weini.xin ->  公网服务器 IP

这个东西一下子把体验改变了。

以前新增一个服务,我的脑子里想的是:

我要开哪个端口?
我要不要去安全组放行?
我要不要再配一个域名?
这个端口以后我还能不能记住?

有了泛域名之后,思路直接变成:

我给这个服务起个什么名字?

比如:

mcp.dev.weini.xin
api.dev.weini.xin
demo.test.weini.xin
ghost-test.test.weini.xin

这个变化挺关键的。

它让服务管理从“端口思维”变成了“命名思维”。

端口是偏底层的,适合机器识别;域名是偏上层的,适合人理解。

这次我才真切感觉到,DNS 不只是为了让用户访问网站方便,它也可以成为服务治理的一部分。

DNS 负责把一类域名都引到公网入口
Caddy 负责根据具体域名分流
frp 负责把请求带回内网

这三层配合起来之后,整个结构就很自然。

也正是这个时候,我第一次觉得:

原来 DNS 这么有用。

以前觉得它只是“配一下解析”。

现在发现它其实是在帮我定义整个服务入口的命名空间。

比如我可以直接规划:

*.dev.weini.xin   开发服务命名空间
*.test.weini.xin  测试服务命名空间

以后我看到一个域名,就大概知道它属于哪个环境、是什么用途。

这比一堆端口号直观太多了。

比如:

https://mcp.dev.weini.xin

一看就知道是开发环境里的 MCP 服务。

而如果是:

http://服务器IP:18120

时间一长,我自己都可能忘了它到底是什么。

所以这次最让我意外的点之一就是:

我从没觉得 DNS 这么有用。

尤其是泛域名解析配合 Caddy 的时候,它不是锦上添花,而是直接把整个多服务暴露体验变成了另一种形态。

从手动配置每个服务的入口,变成提前规划好一个域名命名空间,然后所有服务都往这个命名空间里挂。

这个体验真的很爽。


七、第二阶段的结构:DNS 负责入口,Caddy 负责分流,frp 负责回内网

到这里为止,我的理解是这样的:

泛域名 DNS
   ↓
公网服务器 Caddy 80/443
   ↓
根据 Host 分流到本机端口
   ↓
frps/frpc 隧道
   ↓
内网真实服务

更具体一点:

mcp.dev.weini.xin
   ↓
Caddy
   ↓
127.0.0.1:18120
   ↓
frp
   ↓
内网机器:8120
api.dev.weini.xin
   ↓
Caddy
   ↓
127.0.0.1:18122
   ↓
frp
   ↓
内网机器:8122
demo.test.weini.xin
   ↓
Caddy
   ↓
127.0.0.1:18080
   ↓
frp
   ↓
内网机器:8080

这个阶段已经非常好用了。

新增服务时,流程变成:

1. frpc 新增一个端口映射
2. Caddyfile 新增一个域名分流
3. reload Caddy

DNS 不用动。

服务器安全组也不用每次开新业务端口。

比如新增一个服务:

内网服务:8120
服务器本机 frp 端口:18120
域名:mcp.dev.weini.xin

frpc 里加映射:

[[proxies]]
name = "mcp-8120"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8120
remotePort = 18120

Caddy 里加:

mcp.dev.weini.xin {
    reverse_proxy 127.0.0.1:18120
}

然后 reload Caddy。

外部访问就是:

https://mcp.dev.weini.xin

这已经从“端口管理”升级成了“域名管理”。

但是后来我又意识到一个问题。


八、第三次顿悟:为什么新增服务还要登录云服务器改 Caddy?

当我以为这套已经很优雅的时候,又突然发现一个不爽的地方:

虽然 DNS 不用改了,安全组不用开新端口了,但是每新增一个服务,我还是要登录云服务器改 Caddy 配置。

也就是说,新增服务仍然要做两件事:

1. 改内网 frpc
2. 改云端 Caddy

这就有点不彻底。

我真正想要的是:

新增服务时,只改内网 frpc
云端服务器完全不用登录
DNS 不用动
Caddy 不用动
安全组不用动

这个想法出来之后,整个理解又升级了一层。

因为 frp 本身不只是能做 TCP 端口转发,它也可以做 HTTP 类型代理,并根据 HTTP 请求里的 Host 来分流。

也就是说,可以让 frp 自己来识别:

Host: mcp.dev.weini.xin
Host: api.dev.weini.xin
Host: demo.test.weini.xin

然后把不同域名转发到不同的内网服务。

这样 Caddy 就不需要再为每个服务单独写一段配置了。

Caddy 只需要做一件事:

所有 *.dev.weini.xin*.test.weini.xin 的请求,统一转发给 frps 的 HTTP vhost 端口。

后面的具体服务分流,交给 frp。


九、终极形态:Caddy 只做统一入口,frp 根据 Host 分流

新的链路变成:

用户访问 mcp.dev.weini.xin
        ↓
DNS 泛解析到云服务器
        ↓
Caddy 接收 443
        ↓
Caddy 统一转发给 frps 的 HTTP vhost 端口
        ↓
frps 根据 Host 判断该给哪个 frpc
        ↓
内网服务 8120

也就是:

用户
 ↓
*.dev.weini.xin
 ↓
Caddy 统一入口
 ↓
frps HTTP vhost
 ↓
frp 根据 Host 分流
 ↓
内网真实服务

这个阶段里,Caddy 不再关心:

mcp.dev.weini.xin 应该去哪个端口
api.dev.weini.xin 应该去哪个端口
demo.test.weini.xin 应该去哪个端口

Caddy 只关心:

所有 dev/test 的请求都交给 frps

然后 frp 根据 frpc 注册的信息分流。


十、云端 frps 的配置

云端 frps 需要开启一个 HTTP vhost 端口,比如:

bindPort = 7000
vhostHTTPPort = 8080

auth.token = "你的复杂token"

这里的意思是:

7000:frpc 连接 frps 的控制端口
8080:frps 接收 HTTP Host 分流流量

注意,这个 8080 不需要对公网开放。

因为外部用户访问的是 Caddy 的 443,Caddy 再从服务器本机转发到:

127.0.0.1:8080

所以公网安全组仍然只需要:

80
443
7000

8080 不必对外开。


十一、云端 Caddy 只配置一次

云端 Caddy 可以变成统一入口。

理想情况下,Caddy 配置类似这样:

*.dev.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

*.test.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

这样所有:

xxx.dev.weini.xin
xxx.test.weini.xin

都会先进入 Caddy,再统一转给 frps 的 HTTP vhost 端口。

Caddy 不再配置每一个服务。

以前 Caddy 是这样:

mcp.dev.weini.xin {
    reverse_proxy 127.0.0.1:18120
}

api.dev.weini.xin {
    reverse_proxy 127.0.0.1:18122
}

demo.test.weini.xin {
    reverse_proxy 127.0.0.1:18080
}

现在变成:

*.dev.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

*.test.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

云端配置从“每个服务一条”变成“每个命名空间一条”。

这就是质变。


十二、内网 frpc 负责注册具体服务

以后新增服务,只需要在内网机器的 frpc 里加:

[[proxies]]
name = "mcp"
type = "http"
localIP = "127.0.0.1"
localPort = 8120
customDomains = ["mcp.dev.weini.xin"]

再新增一个 API 服务:

[[proxies]]
name = "api"
type = "http"
localIP = "127.0.0.1"
localPort = 8122
customDomains = ["api.dev.weini.xin"]

再新增一个测试服务:

[[proxies]]
name = "demo"
type = "http"
localIP = "127.0.0.1"
localPort = 8080
customDomains = ["demo.test.weini.xin"]

之后访问:

https://mcp.dev.weini.xin
https://api.dev.weini.xin
https://demo.test.weini.xin

就分别进入不同的内网服务。

云端 Caddy 不用动。

DNS 不用动。

安全组不用动。

只改内网 frpc。

这才是我真正想要的体验。


十三、这两种模式的区别

到这里,其实有两种模式。

模式一:Caddy 分流模式

结构是:

mcp.dev.weini.xin
   ↓
Caddy
   ↓
127.0.0.1:18120
   ↓
frp TCP
   ↓
内网 8120

特点:

frp 用 type = "tcp"
Caddy 每个服务都要配置
新增服务要登录云服务器改 Caddy
理解简单,控制清晰

这个模式适合刚开始使用,也适合需要非常明确控制每个路由的场景。

模式二:frp Host 分流模式

结构是:

mcp.dev.weini.xin
   ↓
Caddy 通配入口
   ↓
frps vhostHTTPPort
   ↓
frp 根据 Host 分流
   ↓
内网 8120

特点:

frp 用 type = "http"
frpc 写 customDomains
Caddy 只配置一次通配入口
新增服务只改内网 frpc
云端不用动

这个模式才更接近“平台化”。


十四、这个方案的真正意义:云端入口固定,服务注册下沉到内网

这次认知升级最关键的点是:

云端不应该总是跟着业务服务变化。

如果每新增一个服务,云端就要改一次配置,那云端还是承担了太多动态路由的职责。

更优雅的方式是:

云端入口固定
内网服务自己注册

也就是:

DNS 固定
Caddy 固定
frps 固定
安全组固定
frpc 动态新增服务

这个结构就很舒服。

它有点像一个轻量级的服务注册系统:

我在内网启动了一个服务
我在 frpc 里声明它的域名
frps 收到注册
外部访问这个域名时,frps 自动转回对应服务

这样新增服务的操作边界就很清楚:

服务在哪里,就在哪里改配置

内网服务在内网机器上,所以新增服务只改内网机器。

云端只是入口,不需要反复登录修改。

这才是真的优雅。


十五、但是这里有一个现实门槛:通配 HTTPS 证书

这个终极方案有一个需要注意的地方:

Caddy 如果写:

*.dev.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

那 Caddy 需要给 *.dev.weini.xin 申请通配 HTTPS 证书。

通配证书一般需要 DNS Challenge。

也就是说,Caddy 需要能通过 DNS 服务商的 API 自动验证域名所有权。

如果 DNS 在阿里云,就需要 Caddy 支持阿里云 DNS 插件。

如果 DNS 托管到 Cloudflare,就可以用 Cloudflare DNS 插件。

所以这里有两条路线。

路线 A:暂时不用通配证书

继续让 Caddy 对每个具体域名单独自动签证书。

这种最简单,但还是需要给每个域名写 Caddy 配置。

路线 B:搞通配证书

一次性搞定:

*.dev.weini.xin
*.test.weini.xin

之后 Caddy 统一接 HTTPS,后面全交给 frp。

这才是最接近最终形态的方案。

不过这一步会多一个证书和 DNS API 的配置门槛。

所以它更适合作为第二阶段升级,不一定一开始就上。


十六、现在最理想的最终架构

最终我现在理解的理想架构是:

DNS

*.dev.weini.xin  -> 云服务器 IP
*.test.weini.xin -> 云服务器 IP

云服务器安全组

80   对外开放
443  对外开放
7000 给 frpc 连接 frps

业务端口不对公网开放。

云端 Caddy

*.dev.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

*.test.weini.xin {
    reverse_proxy 127.0.0.1:8080
}

云端 frps

bindPort = 7000
vhostHTTPPort = 8080

auth.token = "你的复杂token"

内网 frpc

[[proxies]]
name = "mcp"
type = "http"
localIP = "127.0.0.1"
localPort = 8120
customDomains = ["mcp.dev.weini.xin"]

[[proxies]]
name = "api"
type = "http"
localIP = "127.0.0.1"
localPort = 8122
customDomains = ["api.dev.weini.xin"]

[[proxies]]
name = "demo"
type = "http"
localIP = "127.0.0.1"
localPort = 8080
customDomains = ["demo.test.weini.xin"]

访问链路:

https://mcp.dev.weini.xin
   ↓
DNS
   ↓
Caddy
   ↓
frps HTTP vhost
   ↓
frpc
   ↓
内网 8120

新增服务时:

只改 frpc
不改 DNS
不改 Caddy
不开放新端口
不登录云服务器

这才是我现在认为最爽的形态。


十七、为什么这套东西接近生产思路

我前面一直说,这套东西“接近生产”,不是说它本身就是严肃生产级架构,而是它的入口设计思路已经接近生产环境。

生产环境一般不会让用户这样访问:

http://服务器IP:18120
http://服务器IP:18122
http://服务器IP:18080

更常见的是:

https://api.example.com
https://admin.example.com
https://dashboard.example.com

也就是:

用户
 ↓
域名
 ↓
DNS
 ↓
统一入口网关 / 反向代理 / 负载均衡
 ↓
内部服务

而我现在这套是:

用户
 ↓
*.dev.weini.xin / *.test.weini.xin
 ↓
DNS 泛解析
 ↓
Caddy 80/443
 ↓
frps HTTP vhost
 ↓
frp 隧道
 ↓
内网服务

虽然后面用的是 frp 回内网,不是 Kubernetes、Ingress、负载均衡或者云内网服务,但整体抽象很像:

DNS 负责入口
Caddy 负责 HTTPS 和统一网关
frp 负责内部路由和回源
业务服务藏在后面

这已经不是简单的“开个端口能访问就行”。

它开始有了入口治理、命名空间、服务分层、安全边界这些东西。


十八、安全上需要注意的点

这套方案虽然优雅,但不能因为优雅就忽略安全。

尤其是 frp 和 Caddy 结合后,服务更容易被公网访问到,所以有几个点要注意。

1. frps 的 7000 端口必须开放,但最好限制来源 IP

frpc 要连接 frps,所以 7000 端口必须能被内网机器访问。

但是如果条件允许,最好在云服务器安全组里限制:

只允许自己的家宽出口 IP 访问 7000

如果家宽 IP 不固定,至少要保证 frp 的 token 足够复杂。

2. frps 的 vhostHTTPPort 不要对公网开放

比如:

vhostHTTPPort = 8080

这个端口只需要给 Caddy 在服务器本机访问:

127.0.0.1:8080

公网安全组不应该开放它。

外部用户只应该访问:

80
443

3. 后台类服务一定要加鉴权

不要因为套了域名就以为安全了。

像这些服务:

MCP
后台管理
测试 API
数据库管理工具
任务队列面板
内部调试接口

如果暴露到公网,至少要加:

Basic Auth
登录鉴权
IP 白名单
路径限制
访问 token

Caddy 可以很方便地加 Basic Auth,这对于临时内部工具很有用。

4. 不要暴露数据库和 Redis

这类东西尽量不要直接通过 frp 暴露到公网域名后面:

PostgreSQL
Redis
Docker API
MinIO 管理端
消息队列管理端

即使要远程访问,也应该走更受控的方式,比如 SSH Tunnel、VPN、ZeroTier、WireGuard,或者至少严格限制来源 IP。

5. dev 和 test 域名也不是绝对安全的

devtest 只是命名空间,不是安全机制。

不是说服务挂在:

xxx.dev.weini.xin

它就天然安全了。

只要公网能访问,它就可能被扫描到。

所以该加鉴权还是要加鉴权。


十九、这次折腾的总结

这次折腾之后,我对 frp、Caddy、DNS 泛解析的理解连续升级了好几次。

一开始我的需求只是:

把内网的一个端口暴露出去。

第一阶段,我发现 frp 很优雅。

因为它可以让内网机器主动连接公网服务器,不需要公网 IP,也不需要路由器端口映射。

第二阶段,我发现只用 frp 暴露端口还不够优雅。

因为端口多了以后,会出现:

端口难记
安全组反复开放
业务端口裸露公网
访问形式不优雅

于是我想到用 Caddy 接管公网入口。

第三阶段,我发现 DNS 泛解析非常关键。

通过:

*.dev.weini.xin
*.test.weini.xin

可以提前规划好开发和测试的域名命名空间。

这让我从“端口思维”切换到了“域名思维”。

以前是:

我这个服务用哪个端口?

现在是:

我这个服务叫什么名字?

这个变化非常大。

第四阶段,我又发现即使 DNS 和安全组都不用动了,每新增一个服务还是要登录云服务器改 Caddy。

这还不够彻底。

于是理解继续升级:

让 Caddy 只做统一入口,让 frp 根据 Host 做服务分流。

最终形态就变成:

DNS 泛解析
   ↓
Caddy 通配入口
   ↓
frps HTTP vhost
   ↓
frp 根据 Host 分流
   ↓
内网真实服务

这样新增服务时,只需要改内网 frpc。

不改 DNS
不改 Caddy
不开放新端口
不登录云服务器

这才是真的舒服。


二十、最终认知

这次最大的收获不是“学会了 frp 怎么用”。

而是理解了一个更大的东西:

公网入口应该稳定,服务注册应该灵活。

DNS 负责定义命名空间。

Caddy 负责统一 HTTPS 入口。

frp 负责把服务从内网注册到公网入口。

内网服务只负责自己的业务。

最终结构是:

公网入口层:DNS + Caddy
服务注册层:frp
业务运行层:内网服务

以前是:

记端口、开端口、配端口

后来是:

配域名、走 HTTPS、Caddy 分流、frp 回内网

现在进一步升级成:

DNS 和 Caddy 固定
frp 动态注册服务
新增服务只改内网配置

这就是从“端口暴露”升级到“域名入口管理”,再升级到“轻量服务注册”。

对我这种本地和服务器上跑了很多小服务的人来说,这套方式非常适合:

开发调试
MCP 服务
API 测试
临时演示
内部后台
多项目多端口管理

我以前从没觉得 DNS 这么有用。

这次是真的感觉到了:

DNS 不是简单地把域名指向 IP,它是在定义整个服务入口的命名空间。

配合 Caddy 和 frp 之后,整个体验确实优雅很多。

Read more

Mac 上 Skill CLI 无法执行的坑:最后其实一条命令就够了

我在做 Amazon skills 的过程中,逐步把本地 CLI 从 Python 脚本切到 Go 二进制。这样做的好处很明显:用户不用装 Python、不用配依赖,解压 skill 后直接运行。但在 macOS 上,我们反复遇到一个看起来很玄的问题:同一个二进制,在 Linux/Windows 上正常,在 Mac 上就是执行不了。 当时遇到的现象 常见报错大概有几类: * 双击或 agent 调用 CLI 时,系统提示文件来自未知开发者,无法打开。 * 终端里执行时提示 Permission denied。 * 已经 chmod +x 了,仍然被 macOS 拦截。 * Apple

By ladydd

当我把全世界人群的基因 PCA 跑出来后,看见了一个倒 L 型

最近我把之前学的一些分子人类学知识,终于真正落地了。 不是停留在看论文、看别人画图、看别人解释“人群结构”这些概念,而是自己把数据处理完,自己跑 PCA,自己把全世界不同人群放到一张图上。 然后那一刻,我真的被击中了。 图上出现了一个非常漂亮的倒 L 型。 一端是非洲,另一端逐渐拉向东亚,中间有中东、欧洲、南亚、欧亚大陆上的各种过渡人群。它不是那种随机散点图,而是有方向、有骨架、有历史感的结构。 我第一眼看到的时候,脑子里直接冒出一句话: 这不像是一张普通统计图,这像是人类迁徙史在二维空间里留下的影子。 当然,后来我也提醒自己,PCA 不能被过度浪漫化。它不是地图,也不是时间轴,更不是“谁从哪里走到哪里”的直接证据。PCA 本质上是把高维基因差异压缩到几个主成分上,用最大方差方向把样本摊开。它可以帮助我们观察人群结构、相似性、分化和混合,但不能单独承担全部历史解释。PCA 在群体遗传学里常用于观察 population structure

By ladydd

吞吐与延迟:一个厨房比喻讲透性能压测

写于 2026-06-26。背景: MCP 服务 跑在 3 台 ClickHouse(每台 16 核 / 64G,1 分片 3 副本)上。 我们花了一整轮做公网压测,把这套系统的极限、天花板和杠杆全摸清了。这篇把"吞吐 / 延迟 / 排队"这三个最容易混的概念讲透,配我们自己的真实实测数据。 一句话结论 我们这套系统的吞吐天花板 ≈ 76 req/s。 往里塞再多并发(100、200、300、500),每秒"做完"的还是大约 76 个,多出来的全在排队。系统不会崩,只会让每个人等得更久。 天花板能不能抬?

By ladydd

四卡 3090 本地模型部署复盘:Ollama 跑通 35B,以及 GPU0 掉卡问题

这次做的是一轮真实的本地模型部署摸底。 目标不是搭一个临时 Demo,而是把一台四卡 3090 GPU 机器接进自己的日常 AI 使用环境:本机跑 Open WebUI,负责账号、会话和前端配置;GPU 机器只负责模型推理。这样以后换模型、换推理框架、重启服务,都尽量不影响本机的使用入口。 最后结论比较清楚:qwen3.5:35b 的 GGUF Q4_K_M 量化版已经通过 Ollama 跑通,本机 Open WebUI 可以接入,热加载后的聊天速度也能用;但 GPU0 存在明显稳定性问题,重启后能短暂恢复,跑过负载后又会掉到 NVML 异常状态。 状态快照 当前能用的部分: * 本机 Open WebUI 已部署,

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号