场景: SaaS 软件,客户可以自定义域名
现在方案: 提交证书,动态生成对应的 nginx 配置文件,nginx -s reload 。有个主机进行集中分发。
问题: 经过长时间业务发展,现在有 1w 多个客户的 1w 多个自定义域名相配也有 1w 多个证书。 服务器也越来越多,reload 一次耗时将近 1min.
求解: 像阿里云、腾讯云、蓝汛啥的 CDN 服务是咋做的。
想过用 openresty 的 lua 在 tls 握手阶段,拦截请求,通过 redis:get(domain+'.crt') redis.get(domain+'.key') 的形式。但是性能影响略大。
然后用 go+fasthttp 写了个 tlsproxy https->localhost:80 ,性能也是不理想。
求个最优解。
ps: 不同域名指向不同的 root(版本) 如 vip999.com->root /opt/www/branch/gold_vip/public
最终 go fasthttp
1
privil 2022-06-16 18:35:26 +08:00
……要不用 rust 写一个
|
2
Buges 2022-06-16 18:38:23 +08:00 via Android
caddy on demand tls 完全符合你的应用场景,不需要生成任何证书。再配合 dns 泛解析,用户输入一个域名提交上去,然后就可以直接访问这个域名了,证书会在第一次 tls 握手时根据 sni 自动生成。
https://caddyserver.com/docs/automatic-https#on-demand-tls |
3
helone 2022-06-16 18:42:06 +08:00
常见的几个 cdn 厂都是用的 OpenResty 吧,redis get 你都觉得有性能影响的话,不妨考虑前面再加层 memory cache
|
4
Showfom 2022-06-16 18:42:22 +08:00
那么多域名就一个机器做这事情么?别人都是集群一起上呀
或者说,你那么多域名,就跑一个 nginx 服务?不会多绑定几个 IP 多跑几个 nginx 服务么?每个 nginx 服务绑定不同的 IP |
5
1423 2022-06-16 19:06:06 +08:00 1
用 caddy 的话,这里有一个支持把证书存储到 s3 上的拓展
https://github.com/ss098/certmagic-s3 |
6
harmless 2022-06-16 19:24:59 +08:00 via iPhone
新版 nginx 支持动态证书加载吧
|
7
alswl 2022-06-16 19:45:38 +08:00
Nginx 管控面集群化 + DNS 管理自动化接入。
|
9
neoblackcap 2022-06-16 20:03:42 +08:00
@wellsc 连 luajit 都觉得性能消耗过大,那么就只能干掉网络 IO 。如果还要再 nginx 上开发,那么就只能上 C/C++/Rust 来开发插件。所以建议 Rust 写一个插件也不算不合适。
毕竟 cf 等企业,很多就是直接用系统语言开发一个插件,然后在 nginx/openresty 的基础上跑起来。 |
10
ss098 2022-06-16 20:07:45 +08:00
1. 对性能要求非常高的话,可以基于 Kubernetes + Cert Manager 自定义 CRDs 实现一套,可以用自己的任何支持 Kubernetes 的 Web 服务器,但需要自己开发 + 业务适配
2. 追求简单,使用 Caddy ,上面有人贴了我写的 S3 兼容 Storage 接口,支持分布式 Stateless + 多实例部署 |
11
qfdk 2022-06-16 20:28:05 +08:00 via iPhone
简单啊..... 其实用 Openresry 就好了 然后 nodejs 写个检测脚本. 把证书扔到 redis 里面 然后 每晚定点更新就是 还有一个月到期的时候自动更新就好
|
12
wy315700 2022-06-16 20:29:33 +08:00 via Android
申请个 ca 自己签名
|
13
FrankAdler 2022-06-16 20:54:32 +08:00 via iPhone 1
考虑容器吗,把一万多个客户的入口分散在不同的 pod 下,reload 的时候可能就只有部分,数量取决于你,流量入口 sni 分流,可以不需要证书。
大厂 cdn 也不可能在一台机器上管理所有的域名啊 |
14
cheng6563 2022-06-16 21:01:27 +08:00
nginx 前面弄个 L4 的负载均衡,或者用 dns 动态解析弄个负载均衡,这样你就可以多节点 nginx 了。不过每个 nginx 节点还是要配全证书,可能还是有问题。
或许可以用 go 或 rust ,自己写个 L4 的均衡负载,TLS 握手时会先发个 SNI 域名告知后台用哪个域名验证,这时把流量反代到域名对应的 nginx 节点上去,这样 nginx 只需要加载自己的证书就行了。 |
15
joesonw 2022-06-16 21:06:54 +08:00 via iPhone
treadik 可以通过从 consul 读配置
|
16
learningman 2022-06-16 21:26:04 +08:00
@Buges 这是第三方签了张新证书,而不是用户的证书,如果用户那边配了 CAA ,证书都签不出来。要是用户的客户端做了证书装订之类的东西,你这个实现就把人家服务搞炸了。
况且证书的数量级这个问题还是没解决,go 肯定比 C 慢 |
17
Buges 2022-06-16 21:44:10 +08:00 via Android
@learningman 场景是客户自定义域名,没要求支持客户上传自己的证书吧。
数量应该不是问题,lz 这里慢是因为用 nginx 解析生成的巨大配置文件且启动时加载全部证书。而 caddy on-demand tls 是懒加载的,有连接来了才去申请 /加载缓存中的证书。如果性能还是不够也可以很容易地扩容,因为根本不需要你在配置文件里指定域名和证书。 |
18
dzdh OP @Buges
@1423 caddy 的那个确实也看过。但是因为用到了大量的 nginx rewrite rule 。caddy 的适配怎么样。另外不说和 nginx 的 https 性能持平吧,相差能差多少。然后就是如果是客户自己上传的证书能支持吗? 我都想直接. if https caddy -> localhost:80(nginx)了。 自己搞了一个 go+fasthttp 是 tls.Config{ GetCertificate: func( info clientHello) *tls.Certificate 这个方案。不同域名的证书缓存在内存里. map[domain:string]*tls.Certificate @Showfom 是集群 nginx 的,每台服务器都是 /etc/nginx/vhosts/certs/1w 个.. @neoblackcap 不光证书,不同域名可能 root 路径也不一样。比如 vip 客户或者定制客户或者尝鲜客户根据域名判断 root 是哪个目录比如. /opt/www/stage-2022/public 、/opt/www/dingzhi-01 ,所以 nginx 配置文件的数量也有很多。当然也有可能是我的 lua 脚本写的太垃圾了。。 @cheng6563 golang 怎么在 4 层获取 SNI 。 @Buges 现在确实是客户自己上传证书,都是独立域名或二级域名的专用这个业务的证书。 @harmless 翻了翻文档没找到,https://nginx.org/en/docs/http/configuring_https_servers.html 。懒加载可以实现一个配置文件 自动从三方存储或定制化存储甚至 rest 接口获取证书吗? |
19
Buges 2022-06-16 22:44:29 +08:00 via Android
@dzdh caddy 有 API ,也可以写 plugin ,动态加载、自己上传证书当然也没问题。但自动证书能满足大部分需要吧,有特殊需求的客户再让他自己上传证书。
你要是都要自己上传证书且管理的话,可能就不太适合用 caddy/nginx 这些一般的 web 服务器了,应该用自己的后端,证书存数据库里,自己实现懒加载。 性能方面 caddy 确实慢一些,但你不是特别高并发的服务根本不用在意,绝大多数场景都不会成为短板。 |
20
dzdh OP @Buges 咋说呢。电商场景,平常也确实没啥事,特殊情况比如 618 天天都是做秒杀抢购的,偶尔一个小时或者十几分钟就是一个陡坡上去了。。。不知道能不能扛得住。还有这个东西我要先自己测,要不然线上流量我都不敢切过来 1%测。
|
21
harmless 2022-06-16 23:22:10 +08:00 via iPhone
@dzdh 我也没实际用过,不过看配置比传统的简化了不少,配合懒加载和 reload 可能可以快速刷新证书配置
|
22
harmless 2022-06-16 23:23:34 +08:00 via iPhone
|
24
kennylam777 2022-06-17 04:52:47 +08:00 1
其實在 NGiNX Ingress 的方法是用 Lua 讀取 filesystem 上的 crt/key, 然後 filesystem 上的內容是 ConfigMap/Secret 的更新, 那就可以免除一次 nginx -s reload ,畢竟要把所有 processes swap 過也是會有一點影響。
不過這種上千上萬的, 應該還是要 Lua 控制吧,只是你的 implementation 是直接在 redis get 過來,這種外部 IO 當然會慢。 可以看看 Lua NGINX Module 的 ngx.shared.DICT ,保留一份本地的證書快取,有類似 Redis 的 expire/ttl functions 可以用,然在 init_worker_by_lua 階段掛一套背景更新 DICT 的程式就好。 |
25
blackboom 2022-06-17 07:25:51 +08:00
In-memory cache
|
26
holulu 2022-06-17 07:50:01 +08:00
以前做过相似的场景,流量要求可能没有你的大。openresty 弄个接口,上传证书之后就调一下,把证书加载到 In-memory cache ,之后再开启域名的 https 访问。如果内存够大,缓存时间可以是证书的过期时间。现在证书一般最多是 2 年。如果用户把证书删了,就再弄个删除接口。
|
28
SteveWoo 2022-06-17 09:10:24 +08:00
首先,”然后用 go+fasthttp 写了个 tlsproxy https->localhost:80 ,性能也是不理想。“,我恰好做过,性能一点问题没有。 注意还要调整服务器的参数配置(例如:文件打开数,端口范围调大点,send recv buffer )
其次,开发代理可以用二层有,即在 tcp 层 tls 这里证书校验在这里做就好了。 证书校验通过再往后做 tcp 转发,避免重复 http 解包。 这个我恰好也写过,贴段代码 ``` TlsConfig = &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // info.ServerName 这个就是域名 return GetAndCreateCert(info.ServerName) }, } ``` 最后,能不自己开发最好, 前面大家的建议都很好,如服务器和域名解析分组,nginx lua ,haproxy |
29
picone 2022-06-17 09:30:26 +08:00 1
看看百度开源的 [BFE]( https://github.com/bfenetworks/bfe),把证书都加载到内存里,而且本身是可以通过 API 管理的,很适合 SaaS 场景。
|
30
dzdh OP @SteveWoo 现在就是这么做的。但是“tls 建立连接成功后,直接做 tcp 转发”这个是怎么做的?
我现在 tlsCfg := tls.Config{ GetCertificate:...没错 tlsLn := tls.Listen("tcp",":443",&tlsCfg) handler: servehttp() { req.port=80;req.host=$domain; fasthttp.client.do(req server.Server(tlsLn) 在哪一步做 tcp 转发呢?能把 tls 连接的内容直接转发给 80 嘛?如果还要转发给 nginx 的 tls 那没啥意义了。 |
32
TMaize 2022-06-17 13:33:28 +08:00 1
场景应该差不多,我们方案是用户主动解析域名到指定 CNAME
自动通过 acme.sh 签发证书,控制 apisix 配置证书和路由规则,我还特意写了个工具 [apisix-acme]( https://github.com/TMaize/apisix-acme) |
33
SteveWoo 2022-06-17 14:00:26 +08:00
@dzdh
伪代码如下 // 分桶减少锁碰撞 // conn 对应了 host 。https keepalive 的一个连接只能是唯一的 host 。 这与 http 不同 // bucketMap := [30]map[net.Conn]string // bucketMapMutex:=[30]sync.Mutex cfg := &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // 通过 info.Conn.LocalAddr() 确定 bucketMapMutex 分桶 // bucketMap[info.Conn]=info.ServerName // 连接与 host 对应好 return GetAndCreateCert(info.ServerName) }, } ln, err := net.Listen("tcp", ":12345") assert(err) lsn := tls.NewListener(ln, cfg) for { c, _ := lsn.Accept() go func(conn net.Conn) /* 这个协程可以用协程池复用*/{ // 通过 info.Conn.LocalAddr() 确定 bucketMapMutex 分桶 //serverName:=bucketMap[idx][conn] //addr:=serverName// 根据 serverName 确定后面的地址,如果无差别沦陷 remote, err := net.Dial("tcp", addr) assert(err)// 做好 error 呴错误处理 // conn 设置 keepalive retmote 设置好 keepalive 建议搞成配置 // 优化合理设计,使一条代理只需要两个协程,做到如下内容: // 1. 再包装一层 reader weiter 方便设置断开时间 conn.SetReadDeadline() // 2. 原子操作协调断开 // connFlag atomic.Int32 // remoteFlag atomic.Int32 go func() { // 3. 加上异常处理 断开 defer conn.Close remote.Close io.Copy(conn, remote) }() go func() { // 加上异常处理 断开 defer conn.Close remote.Close io.Copy(remote, conn) }() }(c) } |
34
SteveWoo 2022-06-17 14:14:26 +08:00
上面有个重要 bug 往 bucket 存 ssl hello 如果 环节失败可能会导致 conn 泄漏 这要好好处理下。
刚翻了下原来的代码, 为了考虑各个场景,超时控制、大包检查、限流、统计,总共写了 700 多行了。 |
35
gollwang 2022-06-17 14:52:17 +08:00
这不是现成的? https://certcloud.cn/
|